优化后端代码质量基础

This commit is contained in:
yml2213
2026-05-26 10:48:30 +08:00
parent a2bce8b272
commit d126b35579
13 changed files with 142 additions and 25 deletions
+6
View File
@@ -0,0 +1,6 @@
{
"semi": false,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "all"
}
+17
View File
@@ -15,6 +15,7 @@
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/node": "^25.6.0", "@types/node": "^25.6.0",
"@types/pg": "^8.20.0", "@types/pg": "^8.20.0",
"prettier": "^3.8.3",
"tsx": "^4.22.3", "tsx": "^4.22.3",
"typescript": "^6.0.2" "typescript": "^6.0.2"
} }
@@ -1299,6 +1300,22 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/prettier": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
"integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/proxy-addr": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+2
View File
@@ -8,6 +8,7 @@
"build": "tsc -p tsconfig.build.json && rm -rf dist/db/migrations && mkdir -p dist/db && cp -R src/db/migrations dist/db/migrations", "build": "tsc -p tsconfig.build.json && rm -rf dist/db/migrations && mkdir -p dist/db && cp -R src/db/migrations dist/db/migrations",
"db:migrate": "tsx src/db/migrate.ts", "db:migrate": "tsx src/db/migrate.ts",
"dev": "tsx watch --clear-screen=false src/index.ts", "dev": "tsx watch --clear-screen=false src/index.ts",
"format": "prettier --write .",
"test": "node --import tsx --test $(find src \\( -name '*.test.ts' -o -name '*.test.js' \\) -print)", "test": "node --import tsx --test $(find src \\( -name '*.test.ts' -o -name '*.test.js' \\) -print)",
"typecheck": "tsc -p tsconfig.json --noEmit", "typecheck": "tsc -p tsconfig.json --noEmit",
"start": "node dist/index.js", "start": "node dist/index.js",
@@ -21,6 +22,7 @@
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/node": "^25.6.0", "@types/node": "^25.6.0",
"@types/pg": "^8.20.0", "@types/pg": "^8.20.0",
"prettier": "^3.8.3",
"tsx": "^4.22.3", "tsx": "^4.22.3",
"typescript": "^6.0.2" "typescript": "^6.0.2"
} }
@@ -55,6 +55,7 @@ import {
mapAdminCloudtentaclesSourceConfig, mapAdminCloudtentaclesSourceConfig,
maskPhone, maskPhone,
} from "./mappers.js"; } from "./mappers.js";
import { createHttpError } from "../../../../utils/http.js";
import type { import type {
AdminCloudtentaclesCatalogQueryInput, AdminCloudtentaclesCatalogQueryInput,
@@ -153,7 +154,10 @@ export function updateAdminCloudtentaclesSourceConfig(
export function deleteAdminCloudtentaclesSource(sourceKey: string) { export function deleteAdminCloudtentaclesSource(sourceKey: string) {
const key = String(sourceKey || "").trim(); const key = String(sourceKey || "").trim();
if (!key) { if (!key) {
throw new Error("sourceKey is required for deletion"); throw createHttpError("cloudtentacles sourceKey 不能为空", {
statusCode: 400,
errorCode: "cloudtentacles_source_key_required",
});
} }
deleteCloudtentaclesSessionStateByKey(key); deleteCloudtentaclesSessionStateByKey(key);
deleteCloudtentaclesSourceByKey(key); deleteCloudtentaclesSourceByKey(key);
@@ -1,5 +1,6 @@
import { runtimeConfig } from '../../config/runtime.js' import { runtimeConfig } from '../../config/runtime.js'
import { createClaimToken } from '../../repositories/claim-token-repo.js' import { createClaimToken } from '../../repositories/claim-token-repo.js'
import { createHttpError } from '../../utils/http.js'
import { addHours, nowIso } from '../../utils/time.js' import { addHours, nowIso } from '../../utils/time.js'
import { randomToken } from '../../utils/random.js' import { randomToken } from '../../utils/random.js'
@@ -23,7 +24,10 @@ export async function createTaskClaimToken(taskId: number | string): Promise<Tas
}) })
if (!token) { if (!token) {
throw new Error('领取 token 创建失败') throw createHttpError('领取 token 创建失败', {
statusCode: 500,
errorCode: 'claim_token_create_failed',
})
} }
return { return {
@@ -2,7 +2,7 @@ import { createHttpError } from '../../utils/http.js'
const DEFAULT_TIMEOUT_MS = 10000 const DEFAULT_TIMEOUT_MS = 10000
type JsonObject = Record<string, any> type JsonObject = Record<string, unknown>
type BarkSendInput = { type BarkSendInput = {
serverUrl?: string serverUrl?: string
recipient?: { recipient?: {
@@ -4,7 +4,7 @@ import { sendInternalNotification } from './notification-service.js'
const DEFAULT_COOLDOWN_MS = 10 * 60 * 1000 const DEFAULT_COOLDOWN_MS = 10 * 60 * 1000
const notificationCooldownMap = new Map<string, number>() const notificationCooldownMap = new Map<string, number>()
type JsonObject = Record<string, any> type JsonObject = Record<string, unknown>
type InternalNotificationPayload = { type InternalNotificationPayload = {
title: string title: string
body?: string body?: string
@@ -13,6 +13,55 @@ type InternalNotificationPayload = {
cooldownKey?: string cooldownKey?: string
cooldownMs?: number cooldownMs?: number
} }
type KuaishouCloudAssetNotEnoughPayload = {
task?: unknown
flow?: unknown
assetBefore?: unknown
requiredAsset?: unknown
skuName?: unknown
}
type CloudtentaclesAuthExpiredPayload = {
pathname?: unknown
errorCode?: unknown
message?: unknown
cooldownSeconds?: unknown
sourceKey?: unknown
accountLabel?: unknown
}
type CloudtentaclesAssetLowPayload = {
asset?: unknown
threshold?: unknown
cooldownSeconds?: unknown
sourceKey?: unknown
accountLabel?: unknown
}
type Open91PendingConfigPayload = {
order?: unknown
orderNo?: unknown
productNo?: unknown
buyNum?: unknown
reason?: unknown
}
type TaskNotificationPayload = {
task?: unknown
reason?: unknown
source?: unknown
errorMessage?: unknown
}
type KuaishouCloudConsumeFailedPayload = {
task?: unknown
order?: unknown
ticketCodeMasked?: unknown
shopId?: unknown
shopName?: unknown
errorMessage?: unknown
}
type ClaimRedeemNeedsAttentionPayload = {
task?: unknown
order?: unknown
status?: unknown
errorMessage?: unknown
}
export async function notifyInternalSafely(payload: InternalNotificationPayload) { export async function notifyInternalSafely(payload: InternalNotificationPayload) {
const cooldownKey = String(payload.cooldownKey || '').trim() const cooldownKey = String(payload.cooldownKey || '').trim()
@@ -52,7 +101,7 @@ export function notifyKuaishouCloudAssetNotEnough({
assetBefore = 0, assetBefore = 0,
requiredAsset = 0, requiredAsset = 0,
skuName = '', skuName = '',
}: JsonObject = {}) { }: KuaishouCloudAssetNotEnoughPayload = {}) {
const taskRecord = toRecord(task) const taskRecord = toRecord(task)
const flowRecord = toRecord(flow) const flowRecord = toRecord(flow)
const binding = toRecord(flowRecord.binding) const binding = toRecord(flowRecord.binding)
@@ -81,7 +130,7 @@ export function notifyCloudtentaclesAuthExpired({
cooldownSeconds = 600, cooldownSeconds = 600,
sourceKey = '', sourceKey = '',
accountLabel = '', accountLabel = '',
}: JsonObject = {}) { }: CloudtentaclesAuthExpiredPayload = {}) {
const normalizedSourceKey = String(sourceKey || '').trim() const normalizedSourceKey = String(sourceKey || '').trim()
const normalizedAccountLabel = String(accountLabel || '').trim() const normalizedAccountLabel = String(accountLabel || '').trim()
@@ -107,7 +156,7 @@ export function notifyCloudtentaclesAssetLow({
cooldownSeconds = 1800, cooldownSeconds = 1800,
sourceKey = '', sourceKey = '',
accountLabel = '', accountLabel = '',
}: JsonObject = {}) { }: CloudtentaclesAssetLowPayload = {}) {
const normalizedSourceKey = String(sourceKey || '').trim() const normalizedSourceKey = String(sourceKey || '').trim()
const normalizedAccountLabel = String(accountLabel || '').trim() const normalizedAccountLabel = String(accountLabel || '').trim()
@@ -133,7 +182,7 @@ export function notifyOpen91PendingConfig({
productNo = '', productNo = '',
buyNum = 0, buyNum = 0,
reason = '未命中履约配置', reason = '未命中履约配置',
}: JsonObject = {}) { }: Open91PendingConfigPayload = {}) {
const orderRecord = toRecord(order) const orderRecord = toRecord(order)
return notifyInternalSafely({ return notifyInternalSafely({
@@ -152,7 +201,7 @@ export function notifyOpen91PendingConfig({
export function notifyKuaishouCloudBindUrlRefreshFailed({ export function notifyKuaishouCloudBindUrlRefreshFailed({
task = {}, task = {},
errorMessage = '', errorMessage = '',
}: JsonObject = {}) { }: TaskNotificationPayload = {}) {
const taskRecord = toRecord(task) const taskRecord = toRecord(task)
return notifyInternalSafely({ return notifyInternalSafely({
@@ -174,7 +223,7 @@ export function notifyKuaishouCloudConsumeFailed({
shopId = '', shopId = '',
shopName = '', shopName = '',
errorMessage = '', errorMessage = '',
}: JsonObject = {}) { }: KuaishouCloudConsumeFailedPayload = {}) {
const taskRecord = toRecord(task) const taskRecord = toRecord(task)
const orderRecord = toRecord(order) const orderRecord = toRecord(order)
@@ -196,7 +245,7 @@ export function notifyTaskAutoManualReview({
task = {}, task = {},
reason = '', reason = '',
source = '', source = '',
}: JsonObject = {}) { }: TaskNotificationPayload = {}) {
const taskRecord = toRecord(task) const taskRecord = toRecord(task)
return notifyInternalSafely({ return notifyInternalSafely({
@@ -216,7 +265,7 @@ export function notifyClaimRedeemNeedsAttention({
order = {}, order = {},
status = '', status = '',
errorMessage = '', errorMessage = '',
}: JsonObject = {}) { }: ClaimRedeemNeedsAttentionPayload = {}) {
const taskRecord = toRecord(task) const taskRecord = toRecord(task)
const orderRecord = toRecord(order) const orderRecord = toRecord(order)
@@ -6,7 +6,7 @@ import { createHttpError } from '../../utils/http.js'
const DEFAULT_WPUSH_ENDPOINT = 'https://api.wpush.cn/api/v1/send' const DEFAULT_WPUSH_ENDPOINT = 'https://api.wpush.cn/api/v1/send'
const DEFAULT_TIMEOUT_MS = 10000 const DEFAULT_TIMEOUT_MS = 10000
type JsonObject = Record<string, any> type JsonObject = Record<string, unknown>
type NodeHttpResponse = { type NodeHttpResponse = {
ok: boolean ok: boolean
status: number status: number
+7 -3
View File
@@ -1,5 +1,6 @@
import { runtimeConfig } from '../../config/runtime.js' import { runtimeConfig } from '../../config/runtime.js'
import { createHttpError } from '../../utils/http.js' import { createHttpError } from '../../utils/http.js'
import type { RuntimeConfig } from '../../types/runtime-config.js'
export const OPEN_91_PROVIDER = '91kaquan' export const OPEN_91_PROVIDER = '91kaquan'
export const OPEN_91_PLATFORM = 'kuaishou' export const OPEN_91_PLATFORM = 'kuaishou'
@@ -7,10 +8,13 @@ export const OPEN_91_SUCCESS_MESSAGE = '接口调用成功'
export const OPEN_91_DEFAULT_FAIL_CODE = 1204 export const OPEN_91_DEFAULT_FAIL_CODE = 1204
export const OPEN_91_COST_EXCEED_FAIL_CODE = 1220 export const OPEN_91_COST_EXCEED_FAIL_CODE = 1220
type JsonObject = Record<string, any> type Open91RuntimeConfig = RuntimeConfig['platforms']['ninetyone']
export function getOpen91Config() { export function getOpen91Config(overrides: Partial<Open91RuntimeConfig> = {}) {
const config = (runtimeConfig.platforms?.ninetyone || {}) as JsonObject const config = {
...(runtimeConfig.platforms?.ninetyone || {}),
...overrides,
}
return { return {
userId: String(config.userId || '').trim(), userId: String(config.userId || '').trim(),
@@ -9,6 +9,7 @@ import { syncDeliveryTasksForOrder } from './delivery-task-service.js'
import { resolveOrderItemForFulfillment } from './product-match-service.js' import { resolveOrderItemForFulfillment } from './product-match-service.js'
import { nowIso } from '../../utils/time.js' import { nowIso } from '../../utils/time.js'
import { logIntegration } from '../../utils/logger.js' import { logIntegration } from '../../utils/logger.js'
import { createHttpError } from '../../utils/http.js'
import type { OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js' import type { OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
type SourceOrderItem = { type SourceOrderItem = {
@@ -173,7 +174,10 @@ export async function upsertOrderFromSource(
}) })
if (!order) { if (!order) {
throw new Error('订单写入失败') throw createHttpError('订单写入失败', {
statusCode: 500,
errorCode: 'order_write_failed',
})
} }
const orderItems = await replaceOrderItems( const orderItems = await replaceOrderItems(
@@ -2,6 +2,7 @@ import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import { PROJECT_ROOT } from '../../../config/runtime.js' import { PROJECT_ROOT } from '../../../config/runtime.js'
import { createHttpError } from '../../../utils/http.js'
const CLOUDTENTACLES_SESSION_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'cloudtentacles-session.json') const CLOUDTENTACLES_SESSION_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'cloudtentacles-session.json')
@@ -58,7 +59,10 @@ export function saveCloudtentaclesSessionState(rawValue: unknown) {
export function saveCloudtentaclesSessionStateByKey(sourceKey: unknown, rawValue: unknown) { export function saveCloudtentaclesSessionStateByKey(sourceKey: unknown, rawValue: unknown) {
const key = String(sourceKey || '').trim() const key = String(sourceKey || '').trim()
if (!key) { if (!key) {
throw new Error('saveCloudtentaclesSessionStateByKey: sourceKey is required') throw createHttpError('cloudtentacles sourceKey 不能为空', {
statusCode: 400,
errorCode: 'cloudtentacles_source_key_required',
})
} }
const states = loadCloudtentaclesSessionStatesFromFile() const states = loadCloudtentaclesSessionStatesFromFile()
@@ -75,7 +79,10 @@ export function saveCloudtentaclesSessionStateByKey(sourceKey: unknown, rawValue
export function deleteCloudtentaclesSessionStateByKey(sourceKey: unknown) { export function deleteCloudtentaclesSessionStateByKey(sourceKey: unknown) {
const key = String(sourceKey || '').trim() const key = String(sourceKey || '').trim()
if (!key) { if (!key) {
throw new Error('deleteCloudtentaclesSessionStateByKey: sourceKey is required') throw createHttpError('cloudtentacles sourceKey 不能为空', {
statusCode: 400,
errorCode: 'cloudtentacles_source_key_required',
})
} }
const states = loadCloudtentaclesSessionStatesFromFile() const states = loadCloudtentaclesSessionStatesFromFile()
@@ -99,7 +106,10 @@ export function clearCloudtentaclesSessionState() {
export function clearCloudtentaclesSessionStateByKey(sourceKey: unknown) { export function clearCloudtentaclesSessionStateByKey(sourceKey: unknown) {
const key = String(sourceKey || '').trim() const key = String(sourceKey || '').trim()
if (!key) { if (!key) {
throw new Error('clearCloudtentaclesSessionStateByKey: sourceKey is required') throw createHttpError('cloudtentacles sourceKey 不能为空', {
statusCode: 400,
errorCode: 'cloudtentacles_source_key_required',
})
} }
const cleared = createDefaultCloudtentaclesSessionState() const cleared = createDefaultCloudtentaclesSessionState()
@@ -2,6 +2,7 @@ import fs from 'node:fs'
import path from 'node:path' import path from 'node:path'
import { PROJECT_ROOT } from '../../../config/runtime.js' import { PROJECT_ROOT } from '../../../config/runtime.js'
import { createHttpError } from '../../../utils/http.js'
const CLOUDTENTACLES_SOURCES_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'cloudtentacles-sources.json') const CLOUDTENTACLES_SOURCES_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'cloudtentacles-sources.json')
@@ -63,7 +64,10 @@ export function saveCloudtentaclesSourcesList(rawValue: unknown) {
export function saveCloudtentaclesSourceByKey(sourceKey: unknown, data: JsonObject = {}) { export function saveCloudtentaclesSourceByKey(sourceKey: unknown, data: JsonObject = {}) {
const key = String(sourceKey || '').trim() const key = String(sourceKey || '').trim()
if (!key) { if (!key) {
throw new Error('saveCloudtentaclesSourceByKey: sourceKey is required') throw createHttpError('cloudtentacles sourceKey 不能为空', {
statusCode: 400,
errorCode: 'cloudtentacles_source_key_required',
})
} }
const config = loadCloudtentaclesSourcesConfigFromFile() const config = loadCloudtentaclesSourcesConfigFromFile()
@@ -87,10 +91,16 @@ export function saveCloudtentaclesSourceByKey(sourceKey: unknown, data: JsonObje
export function deleteCloudtentaclesSourceByKey(sourceKey: unknown) { export function deleteCloudtentaclesSourceByKey(sourceKey: unknown) {
const key = String(sourceKey || '').trim() const key = String(sourceKey || '').trim()
if (!key) { if (!key) {
throw new Error('deleteCloudtentaclesSourceByKey: sourceKey is required') throw createHttpError('cloudtentacles sourceKey 不能为空', {
statusCode: 400,
errorCode: 'cloudtentacles_source_key_required',
})
} }
if (key === 'default') { if (key === 'default') {
throw new Error('deleteCloudtentaclesSourceByKey: cannot delete the default source') throw createHttpError('不能删除默认 cloudtentacles 账号', {
statusCode: 409,
errorCode: 'cloudtentacles_default_source_forbidden',
})
} }
const config = loadCloudtentaclesSourcesConfigFromFile() const config = loadCloudtentaclesSourcesConfigFromFile()
@@ -1,4 +1,5 @@
import { logError, logInfo, logWarn } from '../../utils/logger.js' import { logError, logInfo, logWarn } from '../../utils/logger.js'
import { createHttpError } from '../../utils/http.js'
import { import {
getCloudtentaclesHealthJob, getCloudtentaclesHealthJob,
getScheduledJobsConfig, getScheduledJobsConfig,
@@ -53,7 +54,10 @@ export async function runScheduledJobNow(jobId: unknown) {
const job = config.jobs.find((item) => String(item.id || '').trim() === String(jobId || '').trim()) const job = config.jobs.find((item) => String(item.id || '').trim() === String(jobId || '').trim())
if (!job) { if (!job) {
throw new Error('定时任务不存在') throw createHttpError('定时任务不存在', {
statusCode: 404,
errorCode: 'scheduled_job_not_found',
})
} }
const currentTimer = timers.get(job.id) const currentTimer = timers.get(job.id)
@@ -171,7 +175,10 @@ function dispatchJob(job: JsonObject) {
return runCloudtentaclesHealthJob(job) return runCloudtentaclesHealthJob(job)
} }
throw new Error(`不支持的定时任务类型:${job.type}`) throw createHttpError(`不支持的定时任务类型:${job.type}`, {
statusCode: 400,
errorCode: 'unsupported_scheduled_job_type',
})
} }
function updateJobState(jobId: string, patch: JsonObject) { function updateJobState(jobId: string, patch: JsonObject) {