统一前后端代码格式化配置

This commit is contained in:
yml2213
2026-08-16 17:27:12 +08:00
parent 8705f6a6a1
commit 5c7c3e14e3
278 changed files with 6386 additions and 5500 deletions
+13
View File
@@ -0,0 +1,13 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
+36
View File
@@ -0,0 +1,36 @@
name: 代码质量
on:
push:
pull_request:
jobs:
backend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/backend
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: apps/backend/package-lock.json
- run: npm ci
- run: npm run check
frontend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/frontend
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: apps/frontend/package-lock.json
- run: npm ci
- run: npm run check
+8
View File
@@ -0,0 +1,8 @@
**/node_modules
**/dist
**/.vite
**/coverage
**/data
**/package-lock.json
**/.env*
@@ -4,3 +4,4 @@
"printWidth": 100, "printWidth": 100,
"trailingComma": "all" "trailingComma": "all"
} }
+4
View File
@@ -0,0 +1,4 @@
{
"recommendations": ["esbenp.prettier-vscode", "editorconfig.editorconfig"]
}
+5
View File
@@ -0,0 +1,5 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"prettier.configPath": ".prettierrc.json"
}
+8 -3
View File
@@ -88,8 +88,11 @@ bash deploy/mac-dev.sh
```bash ```bash
docker compose -f docker-compose.dev.yml exec -T backend npm test docker compose -f docker-compose.dev.yml exec -T backend npm test
docker compose -f docker-compose.dev.yml exec -T backend npm run format:check
docker compose -f docker-compose.dev.yml exec -T backend npm run typecheck docker compose -f docker-compose.dev.yml exec -T backend npm run typecheck
docker compose -f docker-compose.dev.yml exec -T backend npm run build docker compose -f docker-compose.dev.yml exec -T backend npm run build
docker compose -f docker-compose.dev.yml exec -T frontend npm test
docker compose -f docker-compose.dev.yml exec -T frontend npm run format:check
docker compose -f docker-compose.dev.yml exec -T frontend npm run typecheck docker compose -f docker-compose.dev.yml exec -T frontend npm run typecheck
docker compose -f docker-compose.dev.yml exec -T frontend npm run build docker compose -f docker-compose.dev.yml exec -T frontend npm run build
``` ```
@@ -249,11 +252,13 @@ bash deploy/ubuntu-deploy.sh
## 提交前检查 ## 提交前检查
```bash ```bash
docker compose -f docker-compose.dev.yml exec -T backend npm test docker compose -f docker-compose.dev.yml exec -T backend npm run check
docker compose -f docker-compose.dev.yml exec -T backend npm run typecheck docker compose -f docker-compose.dev.yml exec -T frontend npm run check
docker compose -f docker-compose.dev.yml exec -T frontend npm run typecheck
``` ```
代码格式由仓库根目录的 Prettier 配置统一管理。修改代码后可在对应应用目录执行
`npm run format`,提交前使用 `npm run format:check` 只检查、不修改文件。
确认: 确认:
- 未提交真实账号、Cookie、token、手机号或生产配置 - 未提交真实账号、Cookie、token、手机号或生产配置
+1 -1
View File
@@ -23,7 +23,7 @@
"@types/multer": "^2.2.0", "@types/multer": "^2.2.0",
"@types/node": "^25.6.0", "@types/node": "^25.6.0",
"@types/pg": "^8.20.0", "@types/pg": "^8.20.0",
"prettier": "^3.8.3", "prettier": "3.8.3",
"tsx": "^4.22.3", "tsx": "^4.22.3",
"typescript": "^6.0.2" "typescript": "^6.0.2"
} }
+4 -2
View File
@@ -10,7 +10,8 @@
"db:migrate:create": "tsx scripts/create-migration.ts", "db:migrate:create": "tsx scripts/create-migration.ts",
"db:migrate:status": "tsx scripts/migration-status.ts", "db:migrate:status": "tsx scripts/migration-status.ts",
"dev": "tsx watch --clear-screen=false src/index.ts", "dev": "tsx watch --clear-screen=false src/index.ts",
"format": "prettier --write .", "format": "prettier --config ../../.prettierrc.json --ignore-path ../../.prettierignore --write \"src/**/*.{ts,js,json}\" \"scripts/**/*.{ts,js}\" package.json tsconfig.json tsconfig.build.json",
"format:check": "prettier --config ../../.prettierrc.json --ignore-path ../../.prettierignore --check \"src/**/*.{ts,js,json}\" \"scripts/**/*.{ts,js}\" package.json tsconfig.json tsconfig.build.json",
"mock:claim": "tsx scripts/mock-kuaishou-cloud-claim.ts", "mock:claim": "tsx scripts/mock-kuaishou-cloud-claim.ts",
"mock:open91": "tsx scripts/mock-open91-order.ts", "mock:open91": "tsx scripts/mock-open91-order.ts",
"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)",
@@ -19,6 +20,7 @@
"test:industry:gen": "tsx scripts/gen-test-cases.ts", "test:industry:gen": "tsx scripts/gen-test-cases.ts",
"test:industry:curl": "tsx scripts/curl-send-callback.ts", "test:industry:curl": "tsx scripts/curl-send-callback.ts",
"typecheck": "tsc -p tsconfig.json --noEmit", "typecheck": "tsc -p tsconfig.json --noEmit",
"check": "npm run format:check && npm run typecheck && npm test && npm run build",
"start": "node dist/index.js", "start": "node dist/index.js",
"start:src": "tsx src/index.ts", "start:src": "tsx src/index.ts",
"mock:feifei": "tsx scripts/mock-feifei-claim.ts" "mock:feifei": "tsx scripts/mock-feifei-claim.ts"
@@ -39,7 +41,7 @@
"@types/multer": "^2.2.0", "@types/multer": "^2.2.0",
"@types/node": "^25.6.0", "@types/node": "^25.6.0",
"@types/pg": "^8.20.0", "@types/pg": "^8.20.0",
"prettier": "^3.8.3", "prettier": "3.8.3",
"tsx": "^4.22.3", "tsx": "^4.22.3",
"typescript": "^6.0.2" "typescript": "^6.0.2"
} }
+8 -3
View File
@@ -36,7 +36,10 @@ try {
console.log('\n已清空测试业务数据。') console.log('\n已清空测试业务数据。')
console.log('保留内容:fulfillment profiles/bindings、admin users、配置文件。') console.log('保留内容:fulfillment profiles/bindings、admin users、配置文件。')
} catch (error) { } catch (error) {
console.error('\n清理失败:', error instanceof Error ? error.message : String(error || '未知错误')) console.error(
'\n清理失败:',
error instanceof Error ? error.message : String(error || '未知错误'),
)
process.exitCode = 1 process.exitCode = 1
} finally { } finally {
await closeDb() await closeDb()
@@ -64,7 +67,8 @@ function parseArgs(argv) {
} }
function printHelp() { function printHelp() {
console.log(` console.log(
`
用法: 用法:
npm run cleanup:dev-data -- [--apply] npm run cleanup:dev-data -- [--apply]
@@ -80,7 +84,8 @@ function printHelp() {
admin_users / admin_audit_logs admin_users / admin_audit_logs
本地配置文件 本地配置文件
数据库连接: ${String(runtimeConfig.database?.url || '').trim() || '(未配置 DATABASE_URL)'} 数据库连接: ${String(runtimeConfig.database?.url || '').trim() || '(未配置 DATABASE_URL)'}
`.trim()) `.trim(),
)
} }
function printSummary(counts) { function printSummary(counts) {
+27 -13
View File
@@ -20,10 +20,7 @@ const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
const PROJECT_ROOT = path.resolve(CURRENT_DIR, '..') const PROJECT_ROOT = path.resolve(CURRENT_DIR, '..')
const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, '../..') const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, '../..')
loadEnvFiles([ loadEnvFiles([path.join(WORKSPACE_ROOT, '.env'), path.join(PROJECT_ROOT, '.env')])
path.join(WORKSPACE_ROOT, '.env'),
path.join(PROJECT_ROOT, '.env'),
])
const accessToken = String(process.env.KUASHOU_INDUSTRY_ACCESS_TOKEN || 'your_access_token').trim() const accessToken = String(process.env.KUASHOU_INDUSTRY_ACCESS_TOKEN || 'your_access_token').trim()
const appKey = String(process.env.KUASHOU_INDUSTRY_APP_KEY || 'your_app_key').trim() const appKey = String(process.env.KUASHOU_INDUSTRY_APP_KEY || 'your_app_key').trim()
@@ -41,7 +38,8 @@ const KUAISHOU_OPEN_API = 'https://openapi.kwaixiaodian.com'
const now = Date.now() const now = Date.now()
const validEnd = now + 30 * 24 * 60 * 60 * 1000 const validEnd = now + 30 * 24 * 60 * 60 * 1000
const bizParams: JsonObject = mode === 'consume' const bizParams: JsonObject =
mode === 'consume'
? { ? {
oid, oid,
etickets: Array.from({ length: sendNum }, (_, i) => ({ etickets: Array.from({ length: sendNum }, (_, i) => ({
@@ -81,13 +79,15 @@ const bizParams: JsonObject = mode === 'consume'
token, token,
} }
const method = mode === 'consume' const method =
mode === 'consume'
? 'integration.callback.virtual.eticket.consume' ? 'integration.callback.virtual.eticket.consume'
: mode === 'destroy' : mode === 'destroy'
? 'integration.callback.virtual.eticket.destroy' ? 'integration.callback.virtual.eticket.destroy'
: 'integration.callback.virtual.eticket.send' : 'integration.callback.virtual.eticket.send'
const endpoint = mode === 'consume' const endpoint =
mode === 'consume'
? '/integration/callback/virtual/eticket/consume' ? '/integration/callback/virtual/eticket/consume'
: mode === 'destroy' : mode === 'destroy'
? '/integration/callback/virtual/eticket/destroy' ? '/integration/callback/virtual/eticket/destroy'
@@ -112,7 +112,9 @@ const modeLabel = mode === 'consume' ? '核销' : mode === 'destroy' ? '销毁'
console.log(`# 电子凭证${modeLabel}回调 curl 命令`) console.log(`# 电子凭证${modeLabel}回调 curl 命令`)
console.log(`# oid=${oid} sendNum=${sendNum} access_token=${bodyAccessToken.slice(0, 10)}...`) console.log(`# oid=${oid} sendNum=${sendNum} access_token=${bodyAccessToken.slice(0, 10)}...`)
console.log(`# 先测试网络连通性:`) console.log(`# 先测试网络连通性:`)
console.log(`curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 https://openapi.kwaixiaodian.com/`) console.log(
`curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 https://openapi.kwaixiaodian.com/`,
)
console.log('# 如果返回 000,说明网络不通,需要关闭代理或换服务器') console.log('# 如果返回 000,说明网络不通,需要关闭代理或换服务器')
console.log('') console.log('')
console.log('curl -X POST \\') console.log('curl -X POST \\')
@@ -130,10 +132,15 @@ console.log('')
// 本地签名校验(不调快手) // 本地签名校验(不调快手)
console.log('# 本地签名校验(不会真正调用快手):') console.log('# 本地签名校验(不会真正调用快手):')
const localEndpoint = mode === 'consume' ? 'consume-code' : mode === 'destroy' ? 'destroy-code' : 'send-code' const localEndpoint =
console.log(`curl -s -X POST http://127.0.0.1:3000/api/v1/open/kuaishou-industry/${localEndpoint} \\`) mode === 'consume' ? 'consume-code' : mode === 'destroy' ? 'destroy-code' : 'send-code'
console.log(
`curl -s -X POST http://127.0.0.1:3000/api/v1/open/kuaishou-industry/${localEndpoint} \\`,
)
console.log(" -H 'Content-Type: application/x-www-form-urlencoded' \\") console.log(" -H 'Content-Type: application/x-www-form-urlencoded' \\")
console.log(` -d 'appkey=${appKey}&version=1&timestamp=${now}&signMethod=MD5&access_token=&method=&sign=${sign}&param=${encodeURIComponent(paramStr)}'`) console.log(
` -d 'appkey=${appKey}&version=1&timestamp=${now}&signMethod=MD5&access_token=&method=&sign=${sign}&param=${encodeURIComponent(paramStr)}'`,
)
console.log('') console.log('')
function signPayload(params: JsonObject, secret: string): string { function signPayload(params: JsonObject, secret: string): string {
@@ -141,7 +148,11 @@ function signPayload(params: JsonObject, secret: string): string {
.filter(([k]) => k !== 'sign' && k !== 'signSecret') .filter(([k]) => k !== 'sign' && k !== 'signSecret')
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
const qs = entries.map(([k, v]) => `${k}=${stringify(v)}`).join('&') const qs = entries.map(([k, v]) => `${k}=${stringify(v)}`).join('&')
return crypto.createHash('md5').update(`${qs}&signSecret=${secret}`, 'utf8').digest('hex').toLowerCase() return crypto
.createHash('md5')
.update(`${qs}&signSecret=${secret}`, 'utf8')
.digest('hex')
.toLowerCase()
} }
function stringify(v: unknown): string { function stringify(v: unknown): string {
@@ -164,7 +175,10 @@ function parseArgs(raw: string[]): JsonObject {
if (!arg) continue if (!arg) continue
const n = arg.startsWith('--') ? arg.slice(2) : arg const n = arg.startsWith('--') ? arg.slice(2) : arg
const eq = n.indexOf('=') const eq = n.indexOf('=')
if (eq < 0) { p[n] = true; continue } if (eq < 0) {
p[n] = true
continue
}
p[n.slice(0, eq).trim()] = n.slice(eq + 1).trim() p[n.slice(0, eq).trim()] = n.slice(eq + 1).trim()
} }
return p return p
+20 -10
View File
@@ -19,16 +19,15 @@ const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
const PROJECT_ROOT = path.resolve(CURRENT_DIR, '..') const PROJECT_ROOT = path.resolve(CURRENT_DIR, '..')
const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, '../..') const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, '../..')
loadEnvFiles([ loadEnvFiles([path.join(WORKSPACE_ROOT, '.env'), path.join(PROJECT_ROOT, '.env')])
path.join(WORKSPACE_ROOT, '.env'),
path.join(PROJECT_ROOT, '.env'),
])
const args = parseArgs(process.argv.slice(2)) const args = parseArgs(process.argv.slice(2))
const baseUrl = String(args.baseUrl || 'http://127.0.0.1').replace(/\/+$/, '') const baseUrl = String(args.baseUrl || 'http://127.0.0.1').replace(/\/+$/, '')
const appKey = String(args.appKey || process.env.KUASHOU_INDUSTRY_APP_KEY || '').trim() const appKey = String(args.appKey || process.env.KUASHOU_INDUSTRY_APP_KEY || '').trim()
const signSecret = String(args.signSecret || process.env.KUASHOU_INDUSTRY_SIGN_SECRET || '').trim() const signSecret = String(args.signSecret || process.env.KUASHOU_INDUSTRY_SIGN_SECRET || '').trim()
const signMethod = String(args.signMethod || 'MD5').trim().toUpperCase() const signMethod = String(args.signMethod || 'MD5')
.trim()
.toUpperCase()
if (!appKey || !signSecret) { if (!appKey || !signSecret) {
console.error('请配置 appKey 和 signSecret') console.error('请配置 appKey 和 signSecret')
@@ -85,7 +84,8 @@ function signKuaishou(params: JsonObject, method: string): string {
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
const qs = entries.map(([k, v]) => `${k}=${stringifyVal(v)}`).join('&') const qs = entries.map(([k, v]) => `${k}=${stringifyVal(v)}`).join('&')
const src = `${qs}&signSecret=${signSecret}` const src = `${qs}&signSecret=${signSecret}`
if (method === 'HMAC_SHA256') return crypto.createHmac('sha256', signSecret).update(src, 'utf8').digest('base64') if (method === 'HMAC_SHA256')
return crypto.createHmac('sha256', signSecret).update(src, 'utf8').digest('base64')
return crypto.createHash('md5').update(src, 'utf8').digest('hex').toLowerCase() return crypto.createHash('md5').update(src, 'utf8').digest('hex').toLowerCase()
} }
@@ -106,7 +106,8 @@ async function call(endpoint: string, params: JsonObject) {
body.append('param', String(params.param || '{}')) body.append('param', String(params.param || '{}'))
const started = Date.now() const started = Date.now()
let status = 0; let json: JsonObject = {} let status = 0
let json: JsonObject = {}
try { try {
const res = await fetch(url, { const res = await fetch(url, {
method: 'POST', method: 'POST',
@@ -115,8 +116,14 @@ async function call(endpoint: string, params: JsonObject) {
}) })
status = res.status status = res.status
const text = await res.text() const text = await res.text()
try { json = JSON.parse(text) } catch { json = { raw: text } } try {
} catch (err) { json = { error: String(err) } } json = JSON.parse(text)
} catch {
json = { raw: text }
}
} catch (err) {
json = { error: String(err) }
}
return { status, json, ms: Date.now() - started } return { status, json, ms: Date.now() - started }
} }
@@ -409,7 +416,10 @@ function parseArgs(rawArgs: string[]) {
if (!arg) continue if (!arg) continue
const n = arg.startsWith('--') ? arg.slice(2) : arg const n = arg.startsWith('--') ? arg.slice(2) : arg
const eq = n.indexOf('=') const eq = n.indexOf('=')
if (eq < 0) { parsed[n] = true; continue } if (eq < 0) {
parsed[n] = true
continue
}
parsed[n.slice(0, eq).trim()] = n.slice(eq + 1).trim() parsed[n.slice(0, eq).trim()] = n.slice(eq + 1).trim()
} }
return parsed return parsed
+6 -4
View File
@@ -40,7 +40,11 @@ async function main() {
let appliedNames = new Set<string>() let appliedNames = new Set<string>()
try { try {
const result = await query<{ name: string | null; filename: string | null; run_on: string | null }>( const result = await query<{
name: string | null
filename: string | null
run_on: string | null
}>(
` `
SELECT name, filename, run_on SELECT name, filename, run_on
FROM schema_migrations FROM schema_migrations
@@ -49,9 +53,7 @@ async function main() {
) )
appliedNames = new Set( appliedNames = new Set(
result.rows result.rows.map((row) => normalizeMigrationName(row.filename || row.name)).filter(Boolean),
.map((row) => normalizeMigrationName(row.filename || row.name))
.filter(Boolean),
) )
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : String(error) const message = error instanceof Error ? error.message : String(error)
@@ -20,7 +20,9 @@ if (args.help) {
} }
if (!isDevMockEnabled() && args.force !== true) { if (!isDevMockEnabled() && args.force !== true) {
console.error('当前是生产环境,已拒绝生成 mock 数据。如确实要执行,请追加 --force,或设置 ENABLE_DEV_MOCK=1。') console.error(
'当前是生产环境,已拒绝生成 mock 数据。如确实要执行,请追加 --force,或设置 ENABLE_DEV_MOCK=1。',
)
process.exit(1) process.exit(1)
} }
+9 -7
View File
@@ -10,10 +10,7 @@ const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
const PROJECT_ROOT = path.resolve(CURRENT_DIR, '..') const PROJECT_ROOT = path.resolve(CURRENT_DIR, '..')
const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, '../..') const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, '../..')
loadEnvFiles([ loadEnvFiles([path.join(WORKSPACE_ROOT, '.env'), path.join(PROJECT_ROOT, '.env')])
path.join(WORKSPACE_ROOT, '.env'),
path.join(PROJECT_ROOT, '.env'),
])
const args = parseArgs(process.argv.slice(2)) const args = parseArgs(process.argv.slice(2))
@@ -24,7 +21,9 @@ if (args.help) {
const secret = readEnv('KAQUAN91_SECRET') const secret = readEnv('KAQUAN91_SECRET')
const version = readEnv('KAQUAN91_VERSION') || '1.0' const version = readEnv('KAQUAN91_VERSION') || '1.0'
const baseUrl = String(args.baseUrl || process.env.OPEN91_MOCK_BASE_URL || 'http://127.0.0.1').replace(/\/$/, '') const baseUrl = String(
args.baseUrl || process.env.OPEN91_MOCK_BASE_URL || 'http://127.0.0.1',
).replace(/\/$/, '')
const mode = String(args.mode || 'create').trim() const mode = String(args.mode || 'create').trim()
if (!secret || secret.length !== 32) { if (!secret || secret.length !== 32) {
@@ -40,7 +39,8 @@ if (!['create', 'query'].includes(mode)) {
const orderNo = String(args.orderNo || `MOCK91${Date.now()}`).trim() const orderNo = String(args.orderNo || `MOCK91${Date.now()}`).trim()
const timestamp = Number(args.timestamp || Math.floor(Date.now() / 1000)) const timestamp = Number(args.timestamp || Math.floor(Date.now() / 1000))
const payload = mode === 'query' const payload =
mode === 'query'
? { ? {
orderNo, orderNo,
timestamp, timestamp,
@@ -75,7 +75,9 @@ const response = await fetch(endpoint, {
}).catch((error) => { }).catch((error) => {
console.error('请求 91 mock 接口失败,请确认后端入口可访问。') console.error('请求 91 mock 接口失败,请确认后端入口可访问。')
console.error(`当前地址:${endpoint}`) console.error(`当前地址:${endpoint}`)
console.error('Docker 开发环境通常使用 http://127.0.0.1;本地直启后端通常使用 http://127.0.0.1:3000。') console.error(
'Docker 开发环境通常使用 http://127.0.0.1;本地直启后端通常使用 http://127.0.0.1:3000。',
)
throw error throw error
}) })
@@ -34,10 +34,7 @@ const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
const PROJECT_ROOT = path.resolve(CURRENT_DIR, '..') const PROJECT_ROOT = path.resolve(CURRENT_DIR, '..')
const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, '../..') const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, '../..')
loadEnvFiles([ loadEnvFiles([path.join(WORKSPACE_ROOT, '.env'), path.join(PROJECT_ROOT, '.env')])
path.join(WORKSPACE_ROOT, '.env'),
path.join(PROJECT_ROOT, '.env'),
])
const args = parseArgs(process.argv.slice(2)) const args = parseArgs(process.argv.slice(2))
@@ -65,13 +62,21 @@ if (args.provider && providerPresets[String(args.provider)]) {
console.log(` 预设环境: ${preset.description}`) console.log(` 预设环境: ${preset.description}`)
} }
} else { } else {
baseUrl = String(args.baseUrl || process.env.KUASHOU_INDUSTRY_TEST_BASE_URL || 'http://127.0.0.1:3000').replace(/\/+$/, '') baseUrl = String(
args.baseUrl || process.env.KUASHOU_INDUSTRY_TEST_BASE_URL || 'http://127.0.0.1:3000',
).replace(/\/+$/, '')
} }
// 签名配置:CLI 参数 > 环境变量 > 默认值 // 签名配置:CLI 参数 > 环境变量 > 默认值
const appKey = (String(args.appKey || '') || process.env.KUASHOU_INDUSTRY_APP_KEY || '').trim() const appKey = (String(args.appKey || '') || process.env.KUASHOU_INDUSTRY_APP_KEY || '').trim()
const signSecret = (String(args.signSecret || '') || process.env.KUASHOU_INDUSTRY_SIGN_SECRET || '').trim() const signSecret = (
const signMethod = (String(args.signMethod || 'MD5').trim().toUpperCase()) String(args.signSecret || '') ||
process.env.KUASHOU_INDUSTRY_SIGN_SECRET ||
''
).trim()
const signMethod = String(args.signMethod || 'MD5')
.trim()
.toUpperCase()
const isLoadTest = Boolean(args.load) const isLoadTest = Boolean(args.load)
const targetTps = normalizePositiveInteger(args.tps, 100) const targetTps = normalizePositiveInteger(args.tps, 100)
@@ -119,7 +124,9 @@ if (!isLoadTest) {
let failed = 0 let failed = 0
// 1. 通知商家发码 // 1. 通知商家发码
const sendParams = buildSignedParams(appKey, { const sendParams = buildSignedParams(
appKey,
{
oid: testOid, oid: testOid,
sellerId: '2174425348', sellerId: '2174425348',
num: testNum, num: testNum,
@@ -133,10 +140,13 @@ if (!isLoadTest) {
certExpDays: 30, certExpDays: 30,
certActualStartTime: Date.now(), certActualStartTime: Date.now(),
certActualEndTime: Date.now() + 30 * 24 * 60 * 60 * 1000, certActualEndTime: Date.now() + 30 * 24 * 60 * 60 * 1000,
}, signMethod) },
signMethod,
)
const r1 = await post('/send-code', sendParams, { oid: testOid }) const r1 = await post('/send-code', sendParams, { oid: testOid })
if (assertResult(1, '通知商家发码', r1)) passed++; else failed++ if (assertResult(1, '通知商家发码', r1)) passed++
else failed++
if (r1.ok) { if (r1.ok) {
const len = r1.data?.data?.etickets?.length || 0 const len = r1.data?.data?.etickets?.length || 0
const firstId = r1.data?.data?.etickets?.[0]?.id const firstId = r1.data?.data?.etickets?.[0]?.id
@@ -145,12 +155,16 @@ if (!isLoadTest) {
// 2. 重复发码(幂等性) // 2. 重复发码(幂等性)
const r2 = await post('/send-code', sendParams, { oid: testOid }) const r2 = await post('/send-code', sendParams, { oid: testOid })
if (assertResult(2, '重复发码(幂等性)', r2)) passed++; else failed++ if (assertResult(2, '重复发码(幂等性)', r2)) passed++
else failed++
// 3. 查询全部卡券 // 3. 查询全部卡券
const qBiz: JsonObject = { oid: testOid, sendType: 'VIRTUAL', eticketType: 'DINING_OPEN_TICKET' } const qBiz: JsonObject = { oid: testOid, sendType: 'VIRTUAL', eticketType: 'DINING_OPEN_TICKET' }
const r3 = await post('/query-code', buildSignedParams(appKey, qBiz, signMethod), { oid: testOid }) const r3 = await post('/query-code', buildSignedParams(appKey, qBiz, signMethod), {
if (assertResult(3, '查询全部卡券', r3)) passed++; else failed++ oid: testOid,
})
if (assertResult(3, '查询全部卡券', r3)) passed++
else failed++
if (r3.ok) { if (r3.ok) {
console.log(` 卡券数: ${r3.data?.data?.etickets?.length || 0}`) console.log(` 卡券数: ${r3.data?.data?.etickets?.length || 0}`)
r3.data?.data?.etickets?.forEach((e: JsonObject, i: number) => { r3.data?.data?.etickets?.forEach((e: JsonObject, i: number) => {
@@ -161,13 +175,31 @@ if (!isLoadTest) {
// 4. 查询单个卡券 // 4. 查询单个卡券
const firstEticketId = r3.data?.data?.etickets?.[0]?.id const firstEticketId = r3.data?.data?.etickets?.[0]?.id
if (firstEticketId) { if (firstEticketId) {
const r4 = await post('/query-code', buildSignedParams(appKey, { oid: testOid, eticketId: String(firstEticketId), sendType: 'VIRTUAL', eticketType: 'DINING_OPEN_TICKET' }, signMethod), { oid: testOid }) const r4 = await post(
if (assertResult(4, `查询单个卡券 (eticketId=${firstEticketId})`, r4)) passed++; else failed++ '/query-code',
buildSignedParams(
appKey,
{
oid: testOid,
eticketId: String(firstEticketId),
sendType: 'VIRTUAL',
eticketType: 'DINING_OPEN_TICKET',
},
signMethod,
),
{ oid: testOid },
)
if (assertResult(4, `查询单个卡券 (eticketId=${firstEticketId})`, r4)) passed++
else failed++
} }
// 5. 查询不存在的订单 // 5. 查询不存在的订单
const fakeOid = `FAKE_${Date.now()}` const fakeOid = `FAKE_${Date.now()}`
const r5 = await post('/query-code', buildSignedParams(appKey, { oid: fakeOid, sendType: 'VIRTUAL' }, signMethod), { oid: fakeOid }) const r5 = await post(
'/query-code',
buildSignedParams(appKey, { oid: fakeOid, sendType: 'VIRTUAL' }, signMethod),
{ oid: fakeOid },
)
const expect4012002 = r5.data?.result === 4012002 const expect4012002 = r5.data?.result === 4012002
if (expect4012002) { if (expect4012002) {
passed++ passed++
@@ -179,24 +211,52 @@ if (!isLoadTest) {
// 6. 销毁指定卡券 // 6. 销毁指定卡券
if (firstEticketId) { if (firstEticketId) {
const r6 = await post('/destroy-code', buildSignedParams(appKey, { oid: testOid, reason: 'USER_APPLY_REFUND', etickets: [{ id: String(firstEticketId), num: 1, goodsValue: 100 }] }, signMethod), { oid: testOid }) const r6 = await post(
if (assertResult(6, `销毁卡券 (id=${firstEticketId})`, r6)) passed++; else failed++ '/destroy-code',
buildSignedParams(
appKey,
{
oid: testOid,
reason: 'USER_APPLY_REFUND',
etickets: [{ id: String(firstEticketId), num: 1, goodsValue: 100 }],
},
signMethod,
),
{ oid: testOid },
)
if (assertResult(6, `销毁卡券 (id=${firstEticketId})`, r6)) passed++
else failed++
// 7. 验证销毁后状态 // 7. 验证销毁后状态
const r7 = await post('/query-code', buildSignedParams(appKey, { oid: testOid, eticketId: String(firstEticketId), sendType: 'VIRTUAL' }, signMethod), { oid: testOid }) const r7 = await post(
'/query-code',
buildSignedParams(
appKey,
{ oid: testOid, eticketId: String(firstEticketId), sendType: 'VIRTUAL' },
signMethod,
),
{ oid: testOid },
)
const destroyed = r7.data?.result === 1 && r7.data?.data?.etickets?.[0]?.status === 'DESTROYED' const destroyed = r7.data?.result === 1 && r7.data?.data?.etickets?.[0]?.status === 'DESTROYED'
if (destroyed) { if (destroyed) {
passed++ passed++
console.log(` ✅ [7] 销毁后验证: status=DESTROYED`) console.log(` ✅ [7] 销毁后验证: status=DESTROYED`)
} else { } else {
failed++ failed++
console.log(` ❌ [7] 销毁后验证: 期望 DESTROYED 实际 ${r7.data?.data?.etickets?.[0]?.status}`) console.log(
` ❌ [7] 销毁后验证: 期望 DESTROYED 实际 ${r7.data?.data?.etickets?.[0]?.status}`,
)
} }
} }
// 8. 整单销毁 // 8. 整单销毁
const r8 = await post('/destroy-code', buildSignedParams(appKey, { oid: testOid, reason: 'ETICKET_EXPIRED' }, signMethod), { oid: testOid }) const r8 = await post(
if (assertResult(8, '整单销毁', r8)) passed++; else failed++ '/destroy-code',
buildSignedParams(appKey, { oid: testOid, reason: 'ETICKET_EXPIRED' }, signMethod),
{ oid: testOid },
)
if (assertResult(8, '整单销毁', r8)) passed++
else failed++
// 9. 签名错误测试 // 9. 签名错误测试
const badParams = buildSignedParams(appKey, { oid: testOid, sendType: 'VIRTUAL' }, signMethod) const badParams = buildSignedParams(appKey, { oid: testOid, sendType: 'VIRTUAL' }, signMethod)
@@ -213,7 +273,9 @@ if (!isLoadTest) {
console.log('') console.log('')
console.log('╔══════════════════════════════════════════════════════╗') console.log('╔══════════════════════════════════════════════════════╗')
console.log(`║ HTTP 功能测试: ${String(passed).padStart(2)}/${passed + failed} 通过, ${String(failed).padStart(2)}/${passed + failed} 失败 ║`) console.log(
`║ HTTP 功能测试: ${String(passed).padStart(2)}/${passed + failed} 通过, ${String(failed).padStart(2)}/${passed + failed} 失败 ║`,
)
console.log('╚══════════════════════════════════════════════════════╝') console.log('╚══════════════════════════════════════════════════════╝')
console.log('') console.log('')
process.exit(failed > 0 ? 1 : 0) process.exit(failed > 0 ? 1 : 0)
@@ -222,16 +284,16 @@ if (!isLoadTest) {
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
// 压力测试 // 压力测试
// ───────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────
const endpointsToTest = testEndpoint const endpointsToTest = testEndpoint ? [testEndpoint] : ['send-code', 'query-code', 'destroy-code']
? [testEndpoint]
: ['send-code', 'query-code', 'destroy-code']
const loadTestOid = `LOAD_${Date.now()}` const loadTestOid = `LOAD_${Date.now()}`
// 先发码创建订单以供查询/销毁 // 先发码创建订单以供查询/销毁
console.log('') console.log('')
console.log(` 准备测试数据: 通知商家发码 (oid=${loadTestOid})...`) console.log(` 准备测试数据: 通知商家发码 (oid=${loadTestOid})...`)
const prepParams = buildSignedParams(appKey, { const prepParams = buildSignedParams(
appKey,
{
oid: loadTestOid, oid: loadTestOid,
sellerId: '2174425348', sellerId: '2174425348',
num: 10, num: 10,
@@ -245,12 +307,16 @@ const prepParams = buildSignedParams(appKey, {
certExpDays: 30, certExpDays: 30,
certActualStartTime: Date.now(), certActualStartTime: Date.now(),
certActualEndTime: Date.now() + 30 * 24 * 60 * 60 * 1000, certActualEndTime: Date.now() + 30 * 24 * 60 * 60 * 1000,
}, signMethod) },
signMethod,
)
const prepResult = await post('/send-code', prepParams, {}) const prepResult = await post('/send-code', prepParams, {})
if (prepResult.data?.result === 1) { if (prepResult.data?.result === 1) {
console.log(` 数据准备完成 (${prepResult.ms}ms)`) console.log(` 数据准备完成 (${prepResult.ms}ms)`)
} else { } else {
console.log(` ⚠ 数据准备失败: result=${prepResult.data?.result} error_msg="${prepResult.data?.error_msg}" (${prepResult.ms}ms)`) console.log(
` ⚠ 数据准备失败: result=${prepResult.data?.result} error_msg="${prepResult.data?.error_msg}" (${prepResult.ms}ms)`,
)
console.log(` 请确认服务端已配置正确的 appKey/signSecret 且未触发限流`) console.log(` 请确认服务端已配置正确的 appKey/signSecret 且未触发限流`)
} }
@@ -258,14 +324,18 @@ for (const endpoint of endpointsToTest) {
console.log('') console.log('')
console.log('╔══════════════════════════════════════════════════════╗') console.log('╔══════════════════════════════════════════════════════╗')
console.log(`║ 压测: ${endpoint.padEnd(46)}`) console.log(`║ 压测: ${endpoint.padEnd(46)}`)
console.log(`║ 目标: ${String(targetTps).padEnd(4)} TPS, 持续 ${String(testDuration).padEnd(3)}s ║`) console.log(
`║ 目标: ${String(targetTps).padEnd(4)} TPS, 持续 ${String(testDuration).padEnd(3)}s ║`,
)
console.log('╚══════════════════════════════════════════════════════╝') console.log('╚══════════════════════════════════════════════════════╝')
const result = await runLoadTest(endpoint, targetTps, testDuration, loadTestOid) const result = await runLoadTest(endpoint, targetTps, testDuration, loadTestOid)
console.log(` 成功 TPS : ${result.successTps.toFixed(1)} (业务 result=1)`) console.log(` 成功 TPS : ${result.successTps.toFixed(1)} (业务 result=1)`)
console.log(` 总请求数 : ${result.totalReqs}`) console.log(` 总请求数 : ${result.totalReqs}`)
console.log(` 业务成功 : ${result.successes}`) console.log(` 业务成功 : ${result.successes}`)
console.log(` 业务失败 : ${result.bizErrors}${result.bizSample ? ` (示例: ${result.bizSample})` : ''}`) console.log(
` 业务失败 : ${result.bizErrors}${result.bizSample ? ` (示例: ${result.bizSample})` : ''}`,
)
console.log(` 被限流 : ${result.rateLimited}`) console.log(` 被限流 : ${result.rateLimited}`)
console.log(` 平均耗时 : ${result.avgMs.toFixed(1)} ms`) console.log(` 平均耗时 : ${result.avgMs.toFixed(1)} ms`)
console.log(` P50 : ${result.p50Ms.toFixed(1)} ms`) console.log(` P50 : ${result.p50Ms.toFixed(1)} ms`)
@@ -275,7 +345,9 @@ for (const endpoint of endpointsToTest) {
console.log(` 最大耗时 : ${result.maxMs.toFixed(1)} ms`) console.log(` 最大耗时 : ${result.maxMs.toFixed(1)} ms`)
if (result.rateLimited > result.totalReqs * 0.1) { if (result.rateLimited > result.totalReqs * 0.1) {
console.log(` ⚠ 注意: ${((result.rateLimited / result.totalReqs) * 100).toFixed(0)}% 请求被限流,建议调高 KUASHOU_INDUSTRY_RATE_LIMIT_MAX`) console.log(
` ⚠ 注意: ${((result.rateLimited / result.totalReqs) * 100).toFixed(0)}% 请求被限流,建议调高 KUASHOU_INDUSTRY_RATE_LIMIT_MAX`,
)
} }
} }
@@ -311,7 +383,11 @@ async function post(
}) })
status = res.status status = res.status
const text = await res.text() const text = await res.text()
try { data = JSON.parse(text) } catch { data = { raw: text } } try {
data = JSON.parse(text)
} catch {
data = { raw: text }
}
ok = res.ok && data.result === 1 ok = res.ok && data.result === 1
} catch (err) { } catch (err) {
data = { error: String(err) } data = { error: String(err) }
@@ -455,9 +531,7 @@ function signKuaishouIndustry(params: JsonObject, method: string): string {
.filter(([key]) => key !== 'sign' && key !== 'signSecret') .filter(([key]) => key !== 'sign' && key !== 'signSecret')
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
const queryString = entries const queryString = entries.map(([key, value]) => `${key}=${stringifySignVal(value)}`).join('&')
.map(([key, value]) => `${key}=${stringifySignVal(value)}`)
.join('&')
const source = `${queryString}&signSecret=${signSecret}` const source = `${queryString}&signSecret=${signSecret}`
@@ -472,23 +546,30 @@ function stringifySignVal(value: unknown): string {
if (typeof value === 'number' && Number.isFinite(value)) return String(value) if (typeof value === 'number' && Number.isFinite(value)) return String(value)
if (typeof value === 'boolean') return value ? 'true' : 'false' if (typeof value === 'boolean') return value ? 'true' : 'false'
if (value == null) return '' if (value == null) return ''
if (typeof value === 'object') return JSON.stringify(value, Object.keys(value as JsonObject).sort()) if (typeof value === 'object')
return JSON.stringify(value, Object.keys(value as JsonObject).sort())
return String(value) return String(value)
} }
// ─────────────────── 工具 ─────────────────── // ─────────────────── 工具 ───────────────────
function assertResult(num: number, label: string, r: { ok: boolean; status: number; data: JsonObject; ms: number }) { function assertResult(
num: number,
label: string,
r: { ok: boolean; status: number; data: JsonObject; ms: number },
) {
if (r.ok) { if (r.ok) {
console.log(` ✅ [${num}] ${label}: result=1 (${r.ms}ms)`) console.log(` ✅ [${num}] ${label}: result=1 (${r.ms}ms)`)
return true return true
} }
console.log(` ❌ [${num}] ${label}: HTTP ${r.status} result=${r.data?.result} error_msg="${r.data?.error_msg}" (${r.ms}ms)`) console.log(
` ❌ [${num}] ${label}: HTTP ${r.status} result=${r.data?.result} error_msg="${r.data?.error_msg}" (${r.ms}ms)`,
)
return false return false
} }
function delay(ms: number): Promise<void> { function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms)) return new Promise((resolve) => setTimeout(resolve, ms))
} }
function avg(arr: number[]): number { function avg(arr: number[]): number {
@@ -512,10 +593,16 @@ function parseArgs(rawArgs: string[]): JsonObject {
for (const rawArg of rawArgs) { for (const rawArg of rawArgs) {
const arg = String(rawArg || '').trim() const arg = String(rawArg || '').trim()
if (!arg) continue if (!arg) continue
if (arg === '--help' || arg === '-h') { parsed.help = true; continue } if (arg === '--help' || arg === '-h') {
parsed.help = true
continue
}
const normalized = arg.startsWith('--') ? arg.slice(2) : arg const normalized = arg.startsWith('--') ? arg.slice(2) : arg
const eqIdx = normalized.indexOf('=') const eqIdx = normalized.indexOf('=')
if (eqIdx < 0) { parsed[normalized] = true; continue } if (eqIdx < 0) {
parsed[normalized] = true
continue
}
const key = normalized.slice(0, eqIdx).trim() const key = normalized.slice(0, eqIdx).trim()
const value = normalized.slice(eqIdx + 1).trim() const value = normalized.slice(eqIdx + 1).trim()
if (key) parsed[key] = value if (key) parsed[key] = value
+31 -15
View File
@@ -21,13 +21,12 @@ const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
const PROJECT_ROOT = path.resolve(CURRENT_DIR, '..') const PROJECT_ROOT = path.resolve(CURRENT_DIR, '..')
const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, '../..') const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, '../..')
loadEnvFiles([ loadEnvFiles([path.join(WORKSPACE_ROOT, '.env'), path.join(PROJECT_ROOT, '.env')])
path.join(WORKSPACE_ROOT, '.env'),
path.join(PROJECT_ROOT, '.env'),
])
process.env.KUASHOU_INDUSTRY_APP_KEY = process.env.KUASHOU_INDUSTRY_APP_KEY || 'test_industry_app_key' process.env.KUASHOU_INDUSTRY_APP_KEY =
process.env.KUASHOU_INDUSTRY_SIGN_SECRET = process.env.KUASHOU_INDUSTRY_SIGN_SECRET || 'test_industry_sign_secret' process.env.KUASHOU_INDUSTRY_APP_KEY || 'test_industry_app_key'
process.env.KUASHOU_INDUSTRY_SIGN_SECRET =
process.env.KUASHOU_INDUSTRY_SIGN_SECRET || 'test_industry_sign_secret'
process.env.KUASHOU_INDUSTRY_SHOP_ID = process.env.KUASHOU_INDUSTRY_SHOP_ID || 'test_shop' process.env.KUASHOU_INDUSTRY_SHOP_ID = process.env.KUASHOU_INDUSTRY_SHOP_ID || 'test_shop'
const args = parseArgs(process.argv.slice(2)) const args = parseArgs(process.argv.slice(2))
@@ -56,7 +55,9 @@ const [
const config = getKuaishouIndustryConfig() const config = getKuaishouIndustryConfig()
const testOid = String(args.oid || `TEST_KS${Date.now()}`).trim() const testOid = String(args.oid || `TEST_KS${Date.now()}`).trim()
const testNum = normalizePositiveInteger(args.num, 3) const testNum = normalizePositiveInteger(args.num, 3)
const signMethod = String(args.signMethod || 'MD5').trim().toUpperCase() const signMethod = String(args.signMethod || 'MD5')
.trim()
.toUpperCase()
console.log('') console.log('')
console.log('╔══════════════════════════════════════════════════════╗') console.log('╔══════════════════════════════════════════════════════╗')
@@ -149,7 +150,9 @@ try {
console.log(` 已发货数量: ${queryCodeResult.data?.sendNum || 0}`) console.log(` 已发货数量: ${queryCodeResult.data?.sendNum || 0}`)
console.log(` 卡券列表数: ${queryCodeResult.data?.etickets?.length || 0}`) console.log(` 卡券列表数: ${queryCodeResult.data?.etickets?.length || 0}`)
queryCodeResult.data?.etickets?.forEach((eticket: Record<string, unknown>, index: number) => { queryCodeResult.data?.etickets?.forEach((eticket: Record<string, unknown>, index: number) => {
console.log(` [${index + 1}] id=${eticket.id} status=${eticket.status} num=${eticket.num}`) console.log(
` [${index + 1}] id=${eticket.id} status=${eticket.status} num=${eticket.num}`,
)
}) })
} else { } else {
failed++ failed++
@@ -206,7 +209,9 @@ try {
console.log(` 错误信息: ${queryMissingResult.error_msg}`) console.log(` 错误信息: ${queryMissingResult.error_msg}`)
} else { } else {
failed++ failed++
console.log(` └─ ❌ 查询不存在订单: 期望 result=4012002,实际 result=${queryMissingResult.result}`) console.log(
` └─ ❌ 查询不存在订单: 期望 result=4012002,实际 result=${queryMissingResult.result}`,
)
} }
// ───────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────
@@ -214,7 +219,9 @@ try {
// ───────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────
if (queryCodeResult.data?.etickets?.length > 0) { if (queryCodeResult.data?.etickets?.length > 0) {
total++ total++
const destroyTargetIds = queryCodeResult.data.etickets.slice(0, 1).map((e: Record<string, unknown>) => ({ const destroyTargetIds = queryCodeResult.data.etickets
.slice(0, 1)
.map((e: Record<string, unknown>) => ({
id: String(e.id), id: String(e.id),
num: 1, num: 1,
goodsValue: 100, goodsValue: 100,
@@ -248,7 +255,11 @@ try {
eticketType: 'DINING_OPEN_TICKET', eticketType: 'DINING_OPEN_TICKET',
} }
const verifyDestroyParams = buildRequestParams(config.appKey, verifyDestroyBizParams, signMethod) const verifyDestroyParams = buildRequestParams(
config.appKey,
verifyDestroyBizParams,
signMethod,
)
const verifyDestroyResult = await handleQueryCode(verifyDestroyParams) const verifyDestroyResult = await handleQueryCode(verifyDestroyParams)
printResult('销毁后查询', verifyDestroyResult) printResult('销毁后查询', verifyDestroyResult)
@@ -258,7 +269,9 @@ try {
console.log(` 卡券状态: ${verifyStatus}`) console.log(` 卡券状态: ${verifyStatus}`)
} else { } else {
failed++ failed++
console.log(` 期望 status=DESTROYED,实际 status=${verifyStatus}, result=${verifyDestroyResult.result}`) console.log(
` 期望 status=DESTROYED,实际 status=${verifyStatus}, result=${verifyDestroyResult.result}`,
)
} }
} else { } else {
console.log(' ⚠ 跳过(无卡券可销毁)') console.log(' ⚠ 跳过(无卡券可销毁)')
@@ -284,7 +297,6 @@ try {
} else { } else {
failed++ failed++
} }
} finally { } finally {
await closeDb() await closeDb()
} }
@@ -292,7 +304,9 @@ try {
// ───────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────
console.log('') console.log('')
console.log('╔══════════════════════════════════════════════════════╗') console.log('╔══════════════════════════════════════════════════════╗')
console.log(`║ 测试完成: ${String(passed).padStart(2)}/${total} 通过, ${String(failed).padStart(2)}/${total} 失败 ║`) console.log(
`║ 测试完成: ${String(passed).padStart(2)}/${total} 通过, ${String(failed).padStart(2)}/${total} 失败 ║`,
)
console.log('╚══════════════════════════════════════════════════════╝') console.log('╚══════════════════════════════════════════════════════╝')
console.log('') console.log('')
@@ -338,7 +352,9 @@ function printStep(stepNum: number, title: string) {
function printResult(label: string, result: Record<string, unknown>) { function printResult(label: string, result: Record<string, unknown>) {
const ok = result.result === 1 const ok = result.result === 1
const icon = ok ? '✅' : '❌' const icon = ok ? '✅' : '❌'
const resultText = ok ? `result=${result.result}` : `result=${result.result} error_msg="${result.error_msg}"` const resultText = ok
? `result=${result.result}`
: `result=${result.result} error_msg="${result.error_msg}"`
console.log(` └─ ${icon} ${label}: ${resultText}`) console.log(` └─ ${icon} ${label}: ${resultText}`)
} }
+20 -5
View File
@@ -27,8 +27,18 @@ async function main() {
const config = runtimeConfig.worker.sms const config = runtimeConfig.worker.sms
console.log('当前短信配置:') console.log('当前短信配置:')
console.log(' provider :', config.provider) console.log(' provider :', config.provider)
console.log(' accessKeyId :', config.accessKeyId ? `${config.accessKeyId.slice(0, 4)}...${config.accessKeyId.slice(-4)}` : '(空)') console.log(
console.log(' accessKeySecret :', config.accessKeySecret ? `${config.accessKeySecret.slice(0, 4)}...${config.accessKeySecret.slice(-4)}(长度 ${config.accessKeySecret.length}` : '(空)') ' accessKeyId :',
config.accessKeyId
? `${config.accessKeyId.slice(0, 4)}...${config.accessKeyId.slice(-4)}`
: '(空)',
)
console.log(
' accessKeySecret :',
config.accessKeySecret
? `${config.accessKeySecret.slice(0, 4)}...${config.accessKeySecret.slice(-4)}(长度 ${config.accessKeySecret.length}`
: '(空)',
)
console.log(' signName :', config.signName) console.log(' signName :', config.signName)
console.log(' templateCode :', config.templateCode) console.log(' templateCode :', config.templateCode)
console.log('') console.log('')
@@ -64,13 +74,18 @@ async function main() {
const url = `https://dysmsapi.aliyuncs.com/?${queryString}` const url = `https://dysmsapi.aliyuncs.com/?${queryString}`
console.log('stringToSign:') console.log('stringToSign:')
console.log(' ', `GET&%2F&${percentEncode( console.log(
' ',
`GET&%2F&${percentEncode(
Object.entries(params) Object.entries(params)
.filter(([key]) => key !== 'Signature') .filter(([key]) => key !== 'Signature')
.map(([key, value]) => `${percentEncode(key)}=${percentEncode(value)}`) .map(([key, value]) => `${percentEncode(key)}=${percentEncode(value)}`)
.sort((left, right) => (String(left[0]) < String(right[0]) ? -1 : String(left[0]) > String(right[0]) ? 1 : 0)) .sort((left, right) =>
String(left[0]) < String(right[0]) ? -1 : String(left[0]) > String(right[0]) ? 1 : 0,
)
.join('&'), .join('&'),
)}`) )}`,
)
console.log('') console.log('')
console.log(`发送测试短信到 ${phone}(验证码 123456...`) console.log(`发送测试短信到 ${phone}(验证码 123456...`)
+36 -36
View File
@@ -1,98 +1,98 @@
import fs from "node:fs"; import fs from 'node:fs'
import process from "node:process"; import process from 'node:process'
export function loadEnvFiles(filePaths: string[]): void { export function loadEnvFiles(filePaths: string[]): void {
for (const filePath of filePaths) { for (const filePath of filePaths) {
loadEnvFile(filePath); loadEnvFile(filePath)
} }
} }
function loadEnvFile(filePath: string): void { function loadEnvFile(filePath: string): void {
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
return; return
} }
const rawText = fs.readFileSync(filePath, "utf8"); const rawText = fs.readFileSync(filePath, 'utf8')
const lines = rawText.split(/\r?\n/); const lines = rawText.split(/\r?\n/)
for (const rawLine of lines) { for (const rawLine of lines) {
const line = rawLine.trim(); const line = rawLine.trim()
if (!line || line.startsWith("#")) { if (!line || line.startsWith('#')) {
continue; continue
} }
const separatorIndex = line.indexOf("="); const separatorIndex = line.indexOf('=')
if (separatorIndex <= 0) { if (separatorIndex <= 0) {
continue; continue
} }
const key = line.slice(0, separatorIndex).trim(); const key = line.slice(0, separatorIndex).trim()
if (!key || key in process.env) { if (!key || key in process.env) {
continue; continue
} }
process.env[key] = parseEnvValue(line.slice(separatorIndex + 1)); process.env[key] = parseEnvValue(line.slice(separatorIndex + 1))
} }
} }
function parseEnvValue(rawValue: string): string { function parseEnvValue(rawValue: string): string {
const value = String(rawValue || "").trim(); const value = String(rawValue || '').trim()
if (!value) { if (!value) {
return ""; return ''
} }
const quote = value[0]; const quote = value[0]
if ((quote === '"' || quote === "'") && value.endsWith(quote)) { if ((quote === '"' || quote === "'") && value.endsWith(quote)) {
return value.slice(1, -1); return value.slice(1, -1)
} }
return value; return value
} }
export function parseBoolean(rawValue: unknown): boolean | null { export function parseBoolean(rawValue: unknown): boolean | null {
const normalized = String(rawValue || "") const normalized = String(rawValue || '')
.trim() .trim()
.toLowerCase(); .toLowerCase()
if (!normalized) { if (!normalized) {
return null; return null
} }
if (["1", "true", "yes", "on"].includes(normalized)) { if (['1', 'true', 'yes', 'on'].includes(normalized)) {
return true; return true
} }
if (["0", "false", "no", "off"].includes(normalized)) { if (['0', 'false', 'no', 'off'].includes(normalized)) {
return false; return false
} }
return null; return null
} }
export function parseInteger(rawValue: unknown): number | null { export function parseInteger(rawValue: unknown): number | null {
const normalized = String(rawValue || "").trim(); const normalized = String(rawValue || '').trim()
if (!normalized) { if (!normalized) {
return null; return null
} }
const parsed = Number(normalized); const parsed = Number(normalized)
return Number.isFinite(parsed) ? parsed : null; return Number.isFinite(parsed) ? parsed : null
} }
export function parseJsonArray<T = unknown>(rawValue: unknown): T[] | null { export function parseJsonArray<T = unknown>(rawValue: unknown): T[] | null {
const normalized = String(rawValue || "").trim(); const normalized = String(rawValue || '').trim()
if (!normalized) { if (!normalized) {
return null; return null
} }
try { try {
const parsed = JSON.parse(normalized); const parsed = JSON.parse(normalized)
return Array.isArray(parsed) ? (parsed as T[]) : null; return Array.isArray(parsed) ? (parsed as T[]) : null
} catch { } catch {
return null; return null
} }
} }
+10 -13
View File
@@ -1,17 +1,14 @@
import path from "node:path"; import path from 'node:path'
import { fileURLToPath } from "node:url"; import { fileURLToPath } from 'node:url'
import { createDefaultRuntimeConfig } from "./defaults.js"; import { createDefaultRuntimeConfig } from './defaults.js'
import { applyEnvOverrides } from "./env-overrides.js"; import { applyEnvOverrides } from './env-overrides.js'
import { loadEnvFiles } from "./runtime-env.js"; import { loadEnvFiles } from './runtime-env.js'
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url)); const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
export const PROJECT_ROOT = path.resolve(CURRENT_DIR, "../.."); export const PROJECT_ROOT = path.resolve(CURRENT_DIR, '../..')
const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, "../.."); const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, '../..')
loadEnvFiles([ loadEnvFiles([path.join(WORKSPACE_ROOT, '.env'), path.join(PROJECT_ROOT, '.env')])
path.join(WORKSPACE_ROOT, ".env"),
path.join(PROJECT_ROOT, ".env"),
]);
export const runtimeConfig = applyEnvOverrides(createDefaultRuntimeConfig(PROJECT_ROOT)); export const runtimeConfig = applyEnvOverrides(createDefaultRuntimeConfig(PROJECT_ROOT))
+1 -3
View File
@@ -76,9 +76,7 @@ function assertMigrationFilesOrdered() {
const invalid = files.filter((name) => !MIGRATION_FILE_PATTERN.test(name)) const invalid = files.filter((name) => !MIGRATION_FILE_PATTERN.test(name))
if (invalid.length > 0) { if (invalid.length > 0) {
throw new Error( throw new Error(`迁移文件命名必须为 NNN_name.sql(三位序号),非法文件: ${invalid.join(', ')}`)
`迁移文件命名必须为 NNN_name.sql(三位序号),非法文件: ${invalid.join(', ')}`,
)
} }
const versions = files.map((name) => Number(name.slice(0, 3))) const versions = files.map((name) => Number(name.slice(0, 3)))
+12 -3
View File
@@ -11,11 +11,20 @@ import {
} from './task-status.js' } from './task-status.js'
test('任务状态机声明 kuaishou-lewan 主流程跳转', () => { test('任务状态机声明 kuaishou-lewan 主流程跳转', () => {
assert.equal(canTaskTransition(TASK_STATUS.PENDING_BINDING_PREPARE, TASK_STATUS.WAITING_BINDING), true) assert.equal(
canTaskTransition(TASK_STATUS.PENDING_BINDING_PREPARE, TASK_STATUS.WAITING_BINDING),
true,
)
assert.equal(canTaskTransition(TASK_STATUS.WAITING_BINDING, TASK_STATUS.ROLE_CONFIRMED), true) assert.equal(canTaskTransition(TASK_STATUS.WAITING_BINDING, TASK_STATUS.ROLE_CONFIRMED), true)
assert.equal(canTaskTransition(TASK_STATUS.ROLE_CONFIRMED, TASK_STATUS.REDEEMING), true) assert.equal(canTaskTransition(TASK_STATUS.ROLE_CONFIRMED, TASK_STATUS.REDEEMING), true)
assert.equal(canTaskTransition(TASK_STATUS.REDEEMING, TASK_STATUS.DISPATCHED_PENDING_RETURN), true) assert.equal(
assert.equal(canTaskTransition(TASK_STATUS.DISPATCHED_PENDING_RETURN, TASK_STATUS.COMPLETED), true) canTaskTransition(TASK_STATUS.REDEEMING, TASK_STATUS.DISPATCHED_PENDING_RETURN),
true,
)
assert.equal(
canTaskTransition(TASK_STATUS.DISPATCHED_PENDING_RETURN, TASK_STATUS.COMPLETED),
true,
)
}) })
test('任务状态机收敛领取和 91 查询的业务判断', () => { test('任务状态机收敛领取和 91 查询的业务判断', () => {
+1 -4
View File
@@ -259,10 +259,7 @@ export function isKuaishouCloudRedeemSettledStatus(status: unknown): boolean {
export function canRedeemKuaishouCloudClaimStatus(status: unknown): boolean { export function canRedeemKuaishouCloudClaimStatus(status: unknown): boolean {
const normalized = normalizeTaskStatus(status) const normalized = normalizeTaskStatus(status)
// 允许 WAITING_BINDINGUID 匹配后可一键确认+兑换 // 允许 WAITING_BINDINGUID 匹配后可一键确认+兑换
return ( return normalized === TASK_STATUS.ROLE_CONFIRMED || normalized === TASK_STATUS.WAITING_BINDING
normalized === TASK_STATUS.ROLE_CONFIRMED ||
normalized === TASK_STATUS.WAITING_BINDING
)
} }
export function canRegenerateClaimLinkStatus(status: unknown): boolean { export function canRegenerateClaimLinkStatus(status: unknown): boolean {
+3 -1
View File
@@ -9,7 +9,9 @@ export const WORK_ORDER_STATUS = {
CANCELLED: 'cancelled', CANCELLED: 'cancelled',
} as const } as const
export type WorkOrderStatus = (typeof WORK_ORDER_STATUS)[keyof typeof WORK_ORDER_STATUS] | (string & {}) export type WorkOrderStatus =
| (typeof WORK_ORDER_STATUS)[keyof typeof WORK_ORDER_STATUS]
| (string & {})
const FINAL_STATUSES = new Set<WorkOrderStatus>([ const FINAL_STATUSES = new Set<WorkOrderStatus>([
WORK_ORDER_STATUS.ACCEPTED, WORK_ORDER_STATUS.ACCEPTED,
+37 -43
View File
@@ -1,69 +1,63 @@
import process from "node:process"; import process from 'node:process'
import { createApp } from "./app.js"; import { createApp } from './app.js'
import { runtimeConfig } from "./config/runtime.js"; import { runtimeConfig } from './config/runtime.js'
import { assertRuntimeConfigValid } from "./config/runtime-validation.js"; import { assertRuntimeConfigValid } from './config/runtime-validation.js'
import { bootstrapCoreServices } from "./startup/bootstrap.js"; import { bootstrapCoreServices } from './startup/bootstrap.js'
import { createShutdownController } from "./startup/shutdown.js"; import { createShutdownController } from './startup/shutdown.js'
import { createStartupState, formatStartupError } from "./startup/state.js"; import { createStartupState, formatStartupError } from './startup/state.js'
import { logError, logInfo } from "./utils/logger.js"; import { logError, logInfo } from './utils/logger.js'
const port = Number(runtimeConfig.server.port || 3000); const port = Number(runtimeConfig.server.port || 3000)
const host = "0.0.0.0"; const host = '0.0.0.0'
const startupState = createStartupState(); const startupState = createStartupState()
let shutdownController: ReturnType<typeof createShutdownController> | null = null; let shutdownController: ReturnType<typeof createShutdownController> | null = null
try { try {
assertRuntimeConfigValid(runtimeConfig); assertRuntimeConfigValid(runtimeConfig)
} catch (error) { } catch (error) {
logError("[startup]", "运行时配置校验失败,服务停止启动", error); logError('[startup]', '运行时配置校验失败,服务停止启动', error)
process.exit(1); process.exit(1)
} }
const app = createApp({ const app = createApp({
startupState, startupState,
isShutdownStarted: () => shutdownController?.isShutdownStarted() || false, isShutdownStarted: () => shutdownController?.isShutdownStarted() || false,
config: runtimeConfig, config: runtimeConfig,
}); })
const server = app.listen(port, host, () => { const server = app.listen(port, host, () => {
logInfo( logInfo('[startup]', `order-site-backend kuaishou-lite listening on http://${host}:${port}`)
"[startup]", void bootstrapCoreServices(startupState, () => shutdownController?.isShutdownStarted() || false)
`order-site-backend kuaishou-lite listening on http://${host}:${port}` })
);
void bootstrapCoreServices(
startupState,
() => shutdownController?.isShutdownStarted() || false
);
});
shutdownController = createShutdownController(server, startupState); shutdownController = createShutdownController(server, startupState)
server.on("error", (error) => { server.on('error', (error) => {
logError("[startup]", "HTTP server failed", error); logError('[startup]', 'HTTP server failed', error)
}); })
process.on("SIGINT", () => { process.on('SIGINT', () => {
void shutdownController?.shutdown("SIGINT"); void shutdownController?.shutdown('SIGINT')
}); })
process.on("SIGTERM", () => { process.on('SIGTERM', () => {
void shutdownController?.shutdown("SIGTERM"); void shutdownController?.shutdown('SIGTERM')
}); })
process.on("unhandledRejection", (reason) => { process.on('unhandledRejection', (reason) => {
startupState.process.lastUnhandledRejection = { startupState.process.lastUnhandledRejection = {
time: new Date().toISOString(), time: new Date().toISOString(),
message: formatStartupError(reason), message: formatStartupError(reason),
}; }
logError("[process]", "unhandled promise rejection", reason); logError('[process]', 'unhandled promise rejection', reason)
}); })
process.on("uncaughtException", (error) => { process.on('uncaughtException', (error) => {
startupState.process.lastUncaughtException = { startupState.process.lastUncaughtException = {
time: new Date().toISOString(), time: new Date().toISOString(),
message: formatStartupError(error), message: formatStartupError(error),
}; }
logError("[process]", "uncaught exception captured", error); logError('[process]', 'uncaught exception captured', error)
}); })
+24 -31
View File
@@ -1,51 +1,44 @@
import type { Request, Response, NextFunction } from "express"; import type { Request, Response, NextFunction } from 'express'
import { createRequestId, logInfo } from "../utils/logger.js"; import { createRequestId, logInfo } from '../utils/logger.js'
export function accessLogMiddleware( export function accessLogMiddleware(req: Request, res: Response, next: NextFunction): void {
req: Request, const startedAt = Date.now()
res: Response, const requestId = resolveRequestId(req)
next: NextFunction
): void {
const startedAt = Date.now();
const requestId = resolveRequestId(req);
req.requestId = requestId; req.requestId = requestId
res.setHeader("X-Request-Id", requestId); res.setHeader('X-Request-Id', requestId)
res.on("finish", () => { res.on('finish', () => {
if (shouldSkipAccessLog(req.originalUrl, res.statusCode)) { if (shouldSkipAccessLog(req.originalUrl, res.statusCode)) {
return; return
} }
logInfo("[http/access]", "request completed", { logInfo('[http/access]', 'request completed', {
requestId, requestId,
method: req.method, method: req.method,
originalUrl: req.originalUrl, originalUrl: req.originalUrl,
statusCode: res.statusCode, statusCode: res.statusCode,
durationMs: Date.now() - startedAt, durationMs: Date.now() - startedAt,
ip: req.ip, ip: req.ip,
forwardedFor: String(req.headers["x-forwarded-for"] || ""), forwardedFor: String(req.headers['x-forwarded-for'] || ''),
userAgent: String(req.headers["user-agent"] || ""), userAgent: String(req.headers['user-agent'] || ''),
referer: String(req.headers.referer || ""), referer: String(req.headers.referer || ''),
contentLength: Number(res.getHeader("content-length") || 0), contentLength: Number(res.getHeader('content-length') || 0),
actorUserId: req.adminSession?.userId || "", actorUserId: req.adminSession?.userId || '',
actorUsername: req.adminSession?.username || "", actorUsername: req.adminSession?.username || '',
actorRole: req.adminSession?.role || "", actorRole: req.adminSession?.role || '',
}); })
}); })
next(); next()
} }
function resolveRequestId(req: Request): string { function resolveRequestId(req: Request): string {
const fromHeader = String(req.headers["x-request-id"] || "").trim(); const fromHeader = String(req.headers['x-request-id'] || '').trim()
return fromHeader || createRequestId("req"); return fromHeader || createRequestId('req')
} }
function shouldSkipAccessLog(originalUrl: string, statusCode: number): boolean { function shouldSkipAccessLog(originalUrl: string, statusCode: number): boolean {
const pathname = String(originalUrl || "").split("?")[0] || ""; const pathname = String(originalUrl || '').split('?')[0] || ''
return ( return ['/health', '/health/live', '/health/ready'].includes(pathname) && Number(statusCode) < 400
["/health", "/health/live", "/health/ready"].includes(pathname) &&
Number(statusCode) < 400
);
} }
+18 -29
View File
@@ -1,43 +1,32 @@
import type { Request, Response, NextFunction } from "express"; import type { Request, Response, NextFunction } from 'express'
import type { RuntimeConfig } from "../types/runtime-config.js"; import type { RuntimeConfig } from '../types/runtime-config.js'
export function createCorsMiddleware( export function createCorsMiddleware(
config: RuntimeConfig config: RuntimeConfig,
): (req: Request, res: Response, next: NextFunction) => void { ): (req: Request, res: Response, next: NextFunction) => void {
const allowedOrigins = config.cors.allowedOrigins; const allowedOrigins = config.cors.allowedOrigins
const isWildcard = const isWildcard = allowedOrigins.length === 1 && allowedOrigins[0] === '*'
allowedOrigins.length === 1 && allowedOrigins[0] === "*";
return function corsMiddleware( return function corsMiddleware(req: Request, res: Response, next: NextFunction): void {
req: Request,
res: Response,
next: NextFunction
): void {
if (isWildcard) { if (isWildcard) {
res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader('Access-Control-Allow-Origin', '*')
} else { } else {
const origin = req.headers.origin; const origin = req.headers.origin
if (origin && allowedOrigins.includes(origin)) { if (origin && allowedOrigins.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin); res.setHeader('Access-Control-Allow-Origin', origin)
res.setHeader("Access-Control-Allow-Credentials", "true"); res.setHeader('Access-Control-Allow-Credentials', 'true')
} }
} }
res.setHeader( res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
"Access-Control-Allow-Headers", res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
"Content-Type, Authorization" res.setHeader('Access-Control-Max-Age', '86400')
);
res.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, PUT, PATCH, DELETE, OPTIONS"
);
res.setHeader("Access-Control-Max-Age", "86400");
if (req.method === "OPTIONS") { if (req.method === 'OPTIONS') {
res.sendStatus(204); res.sendStatus(204)
return; return
} }
next(); next()
}; }
} }
+64 -67
View File
@@ -1,116 +1,113 @@
import test from "node:test"; import test from 'node:test'
import assert from "node:assert/strict"; import assert from 'node:assert/strict'
import type { NextFunction, Request, Response } from "express"; import type { NextFunction, Request, Response } from 'express'
import { import { createRateLimitMiddleware, resetRateLimitBucketsForTest } from './rate-limit.js'
createRateLimitMiddleware,
resetRateLimitBucketsForTest,
} from "./rate-limit.js";
test("createRateLimitMiddleware allows requests within the window", () => { test('createRateLimitMiddleware allows requests within the window', () => {
resetRateLimitBucketsForTest(); resetRateLimitBucketsForTest()
const limiter = createRateLimitMiddleware({ const limiter = createRateLimitMiddleware({
scope: "test:allow", scope: 'test:allow',
windowMs: 60_000, windowMs: 60_000,
max: 2, max: 2,
}); })
const req = createMockRequest(); const req = createMockRequest()
const res = createMockResponse(); const res = createMockResponse()
let nextCount = 0; let nextCount = 0
const next: NextFunction = () => { const next: NextFunction = () => {
nextCount += 1; nextCount += 1
}; }
limiter(req, res, next); limiter(req, res, next)
limiter(req, res, next); limiter(req, res, next)
assert.equal(nextCount, 2); assert.equal(nextCount, 2)
assert.equal(res.statusCode, 200); assert.equal(res.statusCode, 200)
}); })
test("createRateLimitMiddleware blocks requests over the limit", () => { test('createRateLimitMiddleware blocks requests over the limit', () => {
resetRateLimitBucketsForTest(); resetRateLimitBucketsForTest()
const limiter = createRateLimitMiddleware({ const limiter = createRateLimitMiddleware({
scope: "test:block", scope: 'test:block',
windowMs: 60_000, windowMs: 60_000,
max: 1, max: 1,
}); })
const req = createMockRequest(); const req = createMockRequest()
const res = createMockResponse(); const res = createMockResponse()
let nextCount = 0; let nextCount = 0
const next: NextFunction = () => { const next: NextFunction = () => {
nextCount += 1; nextCount += 1
}; }
limiter(req, res, next); limiter(req, res, next)
limiter(req, res, next); limiter(req, res, next)
assert.equal(nextCount, 1); assert.equal(nextCount, 1)
assert.equal(res.statusCode, 429); assert.equal(res.statusCode, 429)
assert.equal(res.headers["Retry-After"], "60"); assert.equal(res.headers['Retry-After'], '60')
assert.equal(res.body.errorCode, "rate_limited"); assert.equal(res.body.errorCode, 'rate_limited')
}); })
test("createRateLimitMiddleware supports custom limit responses", () => { test('createRateLimitMiddleware supports custom limit responses', () => {
resetRateLimitBucketsForTest(); resetRateLimitBucketsForTest()
const limiter = createRateLimitMiddleware({ const limiter = createRateLimitMiddleware({
scope: "test:custom", scope: 'test:custom',
windowMs: 60_000, windowMs: 60_000,
max: 1, max: 1,
onLimit: (_req, res) => { onLimit: (_req, res) => {
res.status(200).json({ code: 429, message: "limited" }); res.status(200).json({ code: 429, message: 'limited' })
}, },
}); })
const req = createMockRequest(); const req = createMockRequest()
const res = createMockResponse(); const res = createMockResponse()
limiter(req, res, () => {}); limiter(req, res, () => {})
limiter(req, res, () => {}); limiter(req, res, () => {})
assert.equal(res.statusCode, 200); assert.equal(res.statusCode, 200)
assert.deepEqual(res.body, { code: 429, message: "limited" }); assert.deepEqual(res.body, { code: 429, message: 'limited' })
}); })
function createMockRequest(): Request { function createMockRequest(): Request {
return { return {
ip: "127.0.0.1", ip: '127.0.0.1',
originalUrl: "/test", originalUrl: '/test',
headers: {}, headers: {},
socket: { socket: {
remoteAddress: "127.0.0.1", remoteAddress: '127.0.0.1',
}, },
} as Request; } as Request
} }
function createMockResponse(): Response & { function createMockResponse(): Response & {
statusCode: number; statusCode: number
headers: Record<string, string>; headers: Record<string, string>
body: any; body: any
} { } {
const res = { const res = {
statusCode: 200, statusCode: 200,
headers: {} as Record<string, string>, headers: {} as Record<string, string>,
body: null as any, body: null as any,
setHeader(name: string, value: string) { setHeader(name: string, value: string) {
this.headers[name] = value; this.headers[name] = value
return this; return this
}, },
status(statusCode: number) { status(statusCode: number) {
this.statusCode = statusCode; this.statusCode = statusCode
return this; return this
}, },
json(body: any) { json(body: any) {
this.body = body; this.body = body
return this; return this
}, },
}; }
return res as Response & { return res as Response & {
statusCode: number; statusCode: number
headers: Record<string, string>; headers: Record<string, string>
body: any; body: any
}; }
} }
+56 -55
View File
@@ -1,30 +1,30 @@
import type { NextFunction, Request, Response } from "express"; import type { NextFunction, Request, Response } from 'express'
import { buildErrorPayload, createHttpError } from "../utils/http.js"; import { buildErrorPayload, createHttpError } from '../utils/http.js'
import { logWarn } from "../utils/logger.js"; import { logWarn } from '../utils/logger.js'
type RateLimitKeyResolver = (req: Request) => string; type RateLimitKeyResolver = (req: Request) => string
type RateLimitExceededContext = { type RateLimitExceededContext = {
scope: string; scope: string
retryAfterSeconds: number; retryAfterSeconds: number
}; }
type RateLimitOptions = { type RateLimitOptions = {
scope: string; scope: string
windowMs: number; windowMs: number
max: number; max: number
key?: RateLimitKeyResolver; key?: RateLimitKeyResolver
onLimit?: (req: Request, res: Response, context: RateLimitExceededContext) => void; onLimit?: (req: Request, res: Response, context: RateLimitExceededContext) => void
}; }
type RateLimitBucket = { type RateLimitBucket = {
count: number; count: number
resetAt: number; resetAt: number
}; }
const buckets = new Map<string, RateLimitBucket>(); const buckets = new Map<string, RateLimitBucket>()
let lastCleanupAt = 0; let lastCleanupAt = 0
export function createRateLimitMiddleware({ export function createRateLimitMiddleware({
scope, scope,
@@ -33,90 +33,91 @@ export function createRateLimitMiddleware({
key = defaultRateLimitKey, key = defaultRateLimitKey,
onLimit, onLimit,
}: RateLimitOptions) { }: RateLimitOptions) {
const normalizedScope = String(scope || "default").trim() || "default"; const normalizedScope = String(scope || 'default').trim() || 'default'
const normalizedWindowMs = Math.max(1000, Number(windowMs || 0)); const normalizedWindowMs = Math.max(1000, Number(windowMs || 0))
const normalizedMax = Math.max(1, Number(max || 0)); const normalizedMax = Math.max(1, Number(max || 0))
return (req: Request, res: Response, next: NextFunction): void => { return (req: Request, res: Response, next: NextFunction): void => {
const now = Date.now(); const now = Date.now()
cleanupExpiredBuckets(now); cleanupExpiredBuckets(now)
const bucketKey = `${normalizedScope}:${key(req)}`; const bucketKey = `${normalizedScope}:${key(req)}`
const current = buckets.get(bucketKey); const current = buckets.get(bucketKey)
const bucket = current && current.resetAt > now const bucket =
? current current && current.resetAt > now ? current : { count: 0, resetAt: now + normalizedWindowMs }
: { count: 0, resetAt: now + normalizedWindowMs };
bucket.count += 1; bucket.count += 1
buckets.set(bucketKey, bucket); buckets.set(bucketKey, bucket)
if (bucket.count <= normalizedMax) { if (bucket.count <= normalizedMax) {
next(); next()
return; return
} }
const retryAfterSeconds = Math.max(1, Math.ceil((bucket.resetAt - now) / 1000)); const retryAfterSeconds = Math.max(1, Math.ceil((bucket.resetAt - now) / 1000))
res.setHeader("Retry-After", String(retryAfterSeconds)); res.setHeader('Retry-After', String(retryAfterSeconds))
logWarn("[rate-limit]", "请求触发限流", { logWarn('[rate-limit]', '请求触发限流', {
scope: normalizedScope, scope: normalizedScope,
ip: req.ip, ip: req.ip,
originalUrl: req.originalUrl, originalUrl: req.originalUrl,
retryAfterSeconds, retryAfterSeconds,
}); })
if (onLimit) { if (onLimit) {
onLimit(req, res, { onLimit(req, res, {
scope: normalizedScope, scope: normalizedScope,
retryAfterSeconds, retryAfterSeconds,
}); })
return; return
} }
const error = createHttpError("请求过于频繁,请稍后再试", { const error = createHttpError('请求过于频繁,请稍后再试', {
statusCode: 429, statusCode: 429,
errorCode: "rate_limited", errorCode: 'rate_limited',
}); })
res.status(429).json(buildErrorPayload(error, "请求过于频繁,请稍后再试")); res.status(429).json(buildErrorPayload(error, '请求过于频繁,请稍后再试'))
}; }
} }
export function resetRateLimitBucketsForTest(): void { export function resetRateLimitBucketsForTest(): void {
buckets.clear(); buckets.clear()
lastCleanupAt = 0; lastCleanupAt = 0
} }
export function getBodyFieldRateLimitKey(fieldName: string): RateLimitKeyResolver { export function getBodyFieldRateLimitKey(fieldName: string): RateLimitKeyResolver {
return (req) => [defaultRateLimitKey(req), normalizeKeyPart(req.body?.[fieldName])].join(":"); return (req) => [defaultRateLimitKey(req), normalizeKeyPart(req.body?.[fieldName])].join(':')
} }
export function getParamRateLimitKey(paramName: string): RateLimitKeyResolver { export function getParamRateLimitKey(paramName: string): RateLimitKeyResolver {
return (req) => [defaultRateLimitKey(req), normalizeKeyPart(req.params?.[paramName])].join(":"); return (req) => [defaultRateLimitKey(req), normalizeKeyPart(req.params?.[paramName])].join(':')
} }
function defaultRateLimitKey(req: Request): string { function defaultRateLimitKey(req: Request): string {
return normalizeKeyPart( return normalizeKeyPart(
req.ip || req.ip ||
String(req.headers["x-forwarded-for"] || "").split(",")[0] || String(req.headers['x-forwarded-for'] || '').split(',')[0] ||
req.socket.remoteAddress || req.socket.remoteAddress ||
"unknown", 'unknown',
); )
} }
function normalizeKeyPart(value: unknown): string { function normalizeKeyPart(value: unknown): string {
const normalized = String(value || "").trim().toLowerCase(); const normalized = String(value || '')
return normalized || "unknown"; .trim()
.toLowerCase()
return normalized || 'unknown'
} }
function cleanupExpiredBuckets(now: number): void { function cleanupExpiredBuckets(now: number): void {
if (now - lastCleanupAt < 60_000) { if (now - lastCleanupAt < 60_000) {
return; return
} }
lastCleanupAt = now; lastCleanupAt = now
for (const [key, bucket] of buckets.entries()) { for (const [key, bucket] of buckets.entries()) {
if (bucket.resetAt <= now) { if (bucket.resetAt <= now) {
buckets.delete(key); buckets.delete(key)
} }
} }
} }
@@ -39,7 +39,9 @@ type AdminAuditLogListResult = {
total: number total: number
} }
export async function createAdminAuditLog(input: AdminAuditLogCreateInput): Promise<AdminAuditLogRow | null> { export async function createAdminAuditLog(
input: AdminAuditLogCreateInput,
): Promise<AdminAuditLogRow | null> {
const result = await query<AdminAuditLogRow>( const result = await query<AdminAuditLogRow>(
` `
INSERT INTO admin_audit_logs ( INSERT INTO admin_audit_logs (
@@ -69,12 +71,19 @@ export async function createAdminAuditLog(input: AdminAuditLogCreateInput): Prom
return result.rows[0] || null return result.rows[0] || null
} }
export async function getAdminAuditLogById(logId: number | string): Promise<AdminAuditLogRow | null> { export async function getAdminAuditLogById(
const result = await query<AdminAuditLogRow>('SELECT * FROM admin_audit_logs WHERE id = $1 LIMIT 1', [Number(logId)]) logId: number | string,
): Promise<AdminAuditLogRow | null> {
const result = await query<AdminAuditLogRow>(
'SELECT * FROM admin_audit_logs WHERE id = $1 LIMIT 1',
[Number(logId)],
)
return result.rows[0] || null return result.rows[0] || null
} }
export async function listAdminAuditLogs(queryInput: AdminAuditLogListInput = {}): Promise<AdminAuditLogListResult> { export async function listAdminAuditLogs(
queryInput: AdminAuditLogListInput = {},
): Promise<AdminAuditLogListResult> {
const conditions: string[] = [] const conditions: string[] = []
const params: unknown[] = [] const params: unknown[] = []
@@ -108,7 +117,7 @@ export async function listAdminAuditLogs(queryInput: AdminAuditLogListInput = {}
const pageSize = Number(queryInput.pageSize) || 20 const pageSize = Number(queryInput.pageSize) || 20
const offset = (page - 1) * pageSize const offset = (page - 1) * pageSize
const totalResult = await query<{ [column: string]: unknown, total: number }>( const totalResult = await query<{ [column: string]: unknown; total: number }>(
`SELECT COUNT(*)::int AS total FROM admin_audit_logs ${whereClause}`, `SELECT COUNT(*)::int AS total FROM admin_audit_logs ${whereClause}`,
params, params,
) )
@@ -20,10 +20,12 @@ type AdminUserCreateInput = {
updatedAt: string updatedAt: string
} }
type AdminUserPatch = Partial<Pick< type AdminUserPatch = Partial<
Pick<
AdminUserRow, AdminUserRow,
'username' | 'password_hash' | 'role' | 'status' | 'session_version' | 'updated_at' 'username' | 'password_hash' | 'role' | 'status' | 'session_version' | 'updated_at'
>> >
>
type AdminUserListInput = { type AdminUserListInput = {
page?: number page?: number
@@ -44,23 +46,23 @@ const ADMIN_USER_SELECT = `
` `
export async function getAdminUserById(userId: number | string): Promise<AdminUserRow | null> { export async function getAdminUserById(userId: number | string): Promise<AdminUserRow | null> {
const result = await query<AdminUserRow>( const result = await query<AdminUserRow>(`${ADMIN_USER_SELECT} WHERE au.id = $1 LIMIT 1`, [
`${ADMIN_USER_SELECT} WHERE au.id = $1 LIMIT 1`, Number(userId),
[Number(userId)], ])
)
return result.rows[0] || null return result.rows[0] || null
} }
export async function getAdminUserByUsername(username: string): Promise<AdminUserRow | null> { export async function getAdminUserByUsername(username: string): Promise<AdminUserRow | null> {
const result = await query<AdminUserRow>( const result = await query<AdminUserRow>(`${ADMIN_USER_SELECT} WHERE au.username = $1 LIMIT 1`, [
`${ADMIN_USER_SELECT} WHERE au.username = $1 LIMIT 1`, String(username || '')
[String(username || '').trim().toLowerCase()], .trim()
) .toLowerCase(),
])
return result.rows[0] || null return result.rows[0] || null
} }
export async function createAdminUser(input: AdminUserCreateInput): Promise<AdminUserRow | null> { export async function createAdminUser(input: AdminUserCreateInput): Promise<AdminUserRow | null> {
const result = await query<{ [column: string]: unknown, id: number }>( const result = await query<{ [column: string]: unknown; id: number }>(
` `
INSERT INTO admin_users ( INSERT INTO admin_users (
username, username,
@@ -97,7 +99,7 @@ export async function updateAdminUser(
} }
const next = { ...current, ...patch } const next = { ...current, ...patch }
const result = await query<{ [column: string]: unknown, id: number }>( const result = await query<{ [column: string]: unknown; id: number }>(
` `
UPDATE admin_users UPDATE admin_users
SET SET
@@ -151,7 +153,7 @@ export async function listAdminUsers({
} }
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '' const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
const totalResult = await query<{ [column: string]: unknown, total: number }>( const totalResult = await query<{ [column: string]: unknown; total: number }>(
`SELECT COUNT(*)::int AS total FROM admin_users ${whereClause}`, `SELECT COUNT(*)::int AS total FROM admin_users ${whereClause}`,
params, params,
) )
@@ -175,7 +177,7 @@ export async function listAdminUsers({
} }
export async function countActiveAdminUsers(): Promise<number> { export async function countActiveAdminUsers(): Promise<number> {
const result = await query<{ [column: string]: unknown, total: number }>( const result = await query<{ [column: string]: unknown; total: number }>(
`SELECT COUNT(*)::int AS total FROM admin_users WHERE role = 'admin' AND status = 'active'`, `SELECT COUNT(*)::int AS total FROM admin_users WHERE role = 'admin' AND status = 'active'`,
) )
return Number(result.rows[0]?.total || 0) return Number(result.rows[0]?.total || 0)
@@ -26,12 +26,16 @@ type ClaimTokenCreateInput = {
updatedAt: string updatedAt: string
} }
type ClaimTokenPatch = Partial<Pick< type ClaimTokenPatch = Partial<
Pick<
ClaimTokenRow, ClaimTokenRow,
'status' | 'expired_at' | 'used_at' | 'max_use_count' | 'used_count' | 'updated_at' 'status' | 'expired_at' | 'used_at' | 'max_use_count' | 'used_count' | 'updated_at'
>> >
>
export async function createClaimToken(input: ClaimTokenCreateInput): Promise<ClaimTokenRow | null> { export async function createClaimToken(
input: ClaimTokenCreateInput,
): Promise<ClaimTokenRow | null> {
const result = await query<ClaimTokenRow>( const result = await query<ClaimTokenRow>(
` `
INSERT INTO claim_tokens ( INSERT INTO claim_tokens (
@@ -74,16 +78,23 @@ export async function createClaimToken(input: ClaimTokenCreateInput): Promise<Cl
} }
export async function getClaimTokenById(tokenId: number | string): Promise<ClaimTokenRow | null> { export async function getClaimTokenById(tokenId: number | string): Promise<ClaimTokenRow | null> {
const result = await query<ClaimTokenRow>('SELECT * FROM claim_tokens WHERE id = $1 LIMIT 1', [Number(tokenId)]) const result = await query<ClaimTokenRow>('SELECT * FROM claim_tokens WHERE id = $1 LIMIT 1', [
Number(tokenId),
])
return result.rows[0] || null return result.rows[0] || null
} }
export async function findClaimTokenByToken(token: string): Promise<ClaimTokenRow | null> { export async function findClaimTokenByToken(token: string): Promise<ClaimTokenRow | null> {
const result = await query<ClaimTokenRow>('SELECT * FROM claim_tokens WHERE token = $1 LIMIT 1', [String(token || '').trim()]) const result = await query<ClaimTokenRow>('SELECT * FROM claim_tokens WHERE token = $1 LIMIT 1', [
String(token || '').trim(),
])
return result.rows[0] || null return result.rows[0] || null
} }
export async function updateClaimToken(tokenId: number | string, patch: ClaimTokenPatch): Promise<ClaimTokenRow | null> { export async function updateClaimToken(
tokenId: number | string,
patch: ClaimTokenPatch,
): Promise<ClaimTokenRow | null> {
const current = await getClaimTokenById(tokenId) const current = await getClaimTokenById(tokenId)
if (!current) { if (!current) {
return null return null
@@ -47,7 +47,9 @@ export type FulfillmentProfileRequirementInput = {
configJson?: string | Record<string, unknown> configJson?: string | Record<string, unknown>
} }
export async function getFulfillmentProfileByKey(profileKey: string): Promise<FulfillmentProfileRow | null> { export async function getFulfillmentProfileByKey(
profileKey: string,
): Promise<FulfillmentProfileRow | null> {
const result = await query<FulfillmentProfileRow>( const result = await query<FulfillmentProfileRow>(
'SELECT * FROM fulfillment_profiles WHERE profile_key = $1 LIMIT 1', 'SELECT * FROM fulfillment_profiles WHERE profile_key = $1 LIMIT 1',
[String(profileKey || '').trim()], [String(profileKey || '').trim()],
@@ -104,10 +106,9 @@ export async function replaceFulfillmentProfileRequirements(
timestamp: string, timestamp: string,
): Promise<void> { ): Promise<void> {
await withTransaction(async (client) => { await withTransaction(async (client) => {
await client.query( await client.query('DELETE FROM fulfillment_profile_requirements WHERE profile_id = $1', [
'DELETE FROM fulfillment_profile_requirements WHERE profile_id = $1', Number(profileId),
[Number(profileId)], ])
)
for (const requirement of requirements) { for (const requirement of requirements) {
await client.query( await client.query(
@@ -10,7 +10,8 @@ test('syncKuaishouCloudTaskStateForTask 将 Date 更新时间规范化为 ISO
return { rows: [] } return { rows: [] }
} }
await syncKuaishouCloudTaskStateForTask({ await syncKuaishouCloudTaskStateForTask(
{
taskId: 9, taskId: 9,
executorKey: 'kuaishou_ct_assisted', executorKey: 'kuaishou_ct_assisted',
contextJson: { contextJson: {
@@ -35,7 +36,9 @@ test('syncKuaishouCloudTaskStateForTask 将 Date 更新时间规范化为 ISO
}, },
}, },
updatedAt: new Date('2026-05-29T09:05:46.658Z'), updatedAt: new Date('2026-05-29T09:05:46.658Z'),
}, executor) },
executor,
)
assert.equal(calls.length, 1) assert.equal(calls.length, 1)
assert.match(calls[0].text, /INSERT INTO kuaishou_cloud_task_states/) assert.match(calls[0].text, /INSERT INTO kuaishou_cloud_task_states/)
@@ -64,7 +64,10 @@ test('resolveOrderItemSyncPlan matches by identity before falling back to positi
const plan = resolveOrderItemSyncPlan(existingItems, nextItems) const plan = resolveOrderItemSyncPlan(existingItems, nextItems)
assert.deepEqual(plan.updates.map((item) => item.orderItemId), [22, 21]) assert.deepEqual(
plan.updates.map((item) => item.orderItemId),
[22, 21],
)
assert.deepEqual(plan.creates, []) assert.deepEqual(plan.creates, [])
assert.deepEqual(plan.deletes, []) assert.deepEqual(plan.deletes, [])
}) })
@@ -88,7 +91,10 @@ test('resolveOrderItemSyncPlan keeps surplus existing ids for conditional cleanu
const plan = resolveOrderItemSyncPlan(existingItems, nextItems) const plan = resolveOrderItemSyncPlan(existingItems, nextItems)
assert.deepEqual(plan.updates.map((item) => item.orderItemId), [31]) assert.deepEqual(
plan.updates.map((item) => item.orderItemId),
[31],
)
assert.deepEqual(plan.creates, []) assert.deepEqual(plan.creates, [])
assert.deepEqual(plan.deletes, [32]) assert.deepEqual(plan.deletes, [32])
}) })
@@ -204,6 +204,8 @@ async function listOrderItemsByOrderIdWithExecutor(
} }
export async function getOrderItemById(orderItemId: number | string): Promise<OrderItemRow | null> { export async function getOrderItemById(orderItemId: number | string): Promise<OrderItemRow | null> {
const result = await query<OrderItemRow>('SELECT * FROM order_items WHERE id = $1 LIMIT 1', [Number(orderItemId)]) const result = await query<OrderItemRow>('SELECT * FROM order_items WHERE id = $1 LIMIT 1', [
Number(orderItemId),
])
return result.rows[0] || null return result.rows[0] || null
} }
@@ -29,12 +29,7 @@ export async function createTaskEvent(
) VALUES ($1, $2, $3::jsonb, $4) ) VALUES ($1, $2, $3::jsonb, $4)
RETURNING * RETURNING *
`, `,
[ [Number(taskId), String(eventType || '').trim(), JSON.stringify(payload || {}), createdAt],
Number(taskId),
String(eventType || '').trim(),
JSON.stringify(payload || {}),
createdAt,
],
) )
return result.rows[0] || null return result.rows[0] || null
+53 -22
View File
@@ -9,10 +9,7 @@ import type {
TaskRuntimeContextPatch, TaskRuntimeContextPatch,
TaskUpdatePatch, TaskUpdatePatch,
} from '../types/repository/inputs.js' } from '../types/repository/inputs.js'
import type { import type { TaskListQueryResult, TaskRow } from '../types/repository/rows.js'
TaskListQueryResult,
TaskRow,
} from '../types/repository/rows.js'
type TaskQueryExecutor = (text: string, params?: unknown[]) => Promise<{ rows: unknown[] }> type TaskQueryExecutor = (text: string, params?: unknown[]) => Promise<{ rows: unknown[] }>
const KUAISHOU_FEIFEI_EXECUTOR_KEY = 'kuaishou_feifei' const KUAISHOU_FEIFEI_EXECUTOR_KEY = 'kuaishou_feifei'
@@ -150,21 +147,32 @@ export async function createTask(input: TaskCreateInput): Promise<TaskRow | null
const taskId = Number(taskResult.rows[0]?.id || 0) const taskId = Number(taskResult.rows[0]?.id || 0)
if (input.runtimeContext) { if (input.runtimeContext) {
await upsertTaskRuntimeContextWithClient(client, taskId, input.runtimeContext, input.createdAt) await upsertTaskRuntimeContextWithClient(
client,
taskId,
input.runtimeContext,
input.createdAt,
)
} }
await syncKuaishouCloudTaskStateForTask({ await syncKuaishouCloudTaskStateForTask(
{
taskId, taskId,
executorKey: input.executorKey, executorKey: input.executorKey,
contextJson: input.contextJson || '{}', contextJson: input.contextJson || '{}',
updatedAt: input.updatedAt, updatedAt: input.updatedAt,
}, client.query.bind(client) as TaskQueryExecutor) },
client.query.bind(client) as TaskQueryExecutor,
)
return getTaskByIdWithExecutor(client.query.bind(client) as TaskQueryExecutor, taskId) return getTaskByIdWithExecutor(client.query.bind(client) as TaskQueryExecutor, taskId)
}) })
} }
export async function updateTask(taskId: number | string, patch: TaskUpdatePatch): Promise<TaskRow | null> { export async function updateTask(
taskId: number | string,
patch: TaskUpdatePatch,
): Promise<TaskRow | null> {
const current = await getTaskById(taskId) const current = await getTaskById(taskId)
if (!current) { if (!current) {
return null return null
@@ -218,26 +226,38 @@ export async function updateTask(taskId: number | string, patch: TaskUpdatePatch
if (containsRuntimeContextPatch(patch)) { if (containsRuntimeContextPatch(patch)) {
const runtimeContextPatch: TaskRuntimeContextPatch = {} const runtimeContextPatch: TaskRuntimeContextPatch = {}
if (patch.runtime_session_id !== undefined) runtimeContextPatch.runtimeSessionId = patch.runtime_session_id if (patch.runtime_session_id !== undefined)
runtimeContextPatch.runtimeSessionId = patch.runtime_session_id
if (patch.login_type !== undefined) runtimeContextPatch.loginType = patch.login_type if (patch.login_type !== undefined) runtimeContextPatch.loginType = patch.login_type
if (patch.nickname !== undefined) runtimeContextPatch.nickname = patch.nickname if (patch.nickname !== undefined) runtimeContextPatch.nickname = patch.nickname
if (patch.role_id !== undefined) runtimeContextPatch.roleId = patch.role_id if (patch.role_id !== undefined) runtimeContextPatch.roleId = patch.role_id
if (patch.role_name !== undefined) runtimeContextPatch.roleName = patch.role_name if (patch.role_name !== undefined) runtimeContextPatch.roleName = patch.role_name
if (patch.area !== undefined) runtimeContextPatch.area = patch.area if (patch.area !== undefined) runtimeContextPatch.area = patch.area
if (patch.partition_name !== undefined) runtimeContextPatch.partitionName = patch.partition_name if (patch.partition_name !== undefined)
if (patch.screenshot_path !== undefined) runtimeContextPatch.screenshotPath = patch.screenshot_path runtimeContextPatch.partitionName = patch.partition_name
if (patch.artifacts_json !== undefined) runtimeContextPatch.artifactsJson = patch.artifacts_json if (patch.screenshot_path !== undefined)
runtimeContextPatch.screenshotPath = patch.screenshot_path
if (patch.artifacts_json !== undefined)
runtimeContextPatch.artifactsJson = patch.artifacts_json
if (patch.state_json !== undefined) runtimeContextPatch.stateJson = patch.state_json if (patch.state_json !== undefined) runtimeContextPatch.stateJson = patch.state_json
await upsertTaskRuntimeContextWithClient(client, Number(taskId), runtimeContextPatch, patch.updated_at || current.updated_at) await upsertTaskRuntimeContextWithClient(
client,
Number(taskId),
runtimeContextPatch,
patch.updated_at || current.updated_at,
)
} }
await syncKuaishouCloudTaskStateForTask({ await syncKuaishouCloudTaskStateForTask(
{
taskId, taskId,
executorKey: next.executor_key, executorKey: next.executor_key,
contextJson: next.context_json, contextJson: next.context_json,
updatedAt: next.updated_at, updatedAt: next.updated_at,
}, client.query.bind(client) as TaskQueryExecutor) },
client.query.bind(client) as TaskQueryExecutor,
)
return getTaskByIdWithExecutor(client.query.bind(client) as TaskQueryExecutor, taskId) return getTaskByIdWithExecutor(client.query.bind(client) as TaskQueryExecutor, taskId)
}) })
@@ -296,12 +316,15 @@ export async function updateTaskStatusIfCurrent(
return null return null
} }
await syncKuaishouCloudTaskStateForTask({ await syncKuaishouCloudTaskStateForTask(
{
taskId: updated.id, taskId: updated.id,
executorKey: updated.executor_key, executorKey: updated.executor_key,
contextJson: updated.context_json, contextJson: updated.context_json,
updatedAt: updated.updated_at, updatedAt: updated.updated_at,
}, client.query.bind(client) as TaskQueryExecutor) },
client.query.bind(client) as TaskQueryExecutor,
)
return getTaskByIdWithExecutor(client.query.bind(client) as TaskQueryExecutor, taskId) return getTaskByIdWithExecutor(client.query.bind(client) as TaskQueryExecutor, taskId)
}) })
@@ -311,7 +334,9 @@ export async function getTaskById(taskId: number | string): Promise<TaskRow | nu
return getTaskByIdWithExecutor(query, taskId) return getTaskByIdWithExecutor(query, taskId)
} }
export async function findTaskByClaimTokenId(claimTokenId: number | string): Promise<TaskRow | null> { export async function findTaskByClaimTokenId(
claimTokenId: number | string,
): Promise<TaskRow | null> {
const result = await query<TaskRow>( const result = await query<TaskRow>(
`${buildTaskSelect()} `${buildTaskSelect()}
JOIN claim_tokens ctf ON ctf.task_id = ft.id JOIN claim_tokens ctf ON ctf.task_id = ft.id
@@ -441,7 +466,7 @@ export async function listTasks({
} }
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '' const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : ''
const totalResult = await query<{ [column: string]: unknown, total: number }>( const totalResult = await query<{ [column: string]: unknown; total: number }>(
` `
SELECT COUNT(*)::int AS total SELECT COUNT(*)::int AS total
FROM fulfillment_tasks ft FROM fulfillment_tasks ft
@@ -525,8 +550,12 @@ async function upsertTaskRuntimeContextWithClient(
next.area, next.area,
next.partition_name, next.partition_name,
next.screenshot_path, next.screenshot_path,
typeof next.artifacts_json === 'string' ? next.artifacts_json : JSON.stringify(next.artifacts_json || {}), typeof next.artifacts_json === 'string'
typeof next.state_json === 'string' ? next.state_json : JSON.stringify(next.state_json || {}), ? next.artifacts_json
: JSON.stringify(next.artifacts_json || {}),
typeof next.state_json === 'string'
? next.state_json
: JSON.stringify(next.state_json || {}),
timestamp, timestamp,
timestamp, timestamp,
], ],
@@ -560,7 +589,9 @@ async function upsertTaskRuntimeContextWithClient(
next.area, next.area,
next.partition_name, next.partition_name,
next.screenshot_path, next.screenshot_path,
typeof next.artifacts_json === 'string' ? next.artifacts_json : JSON.stringify(next.artifacts_json || {}), typeof next.artifacts_json === 'string'
? next.artifacts_json
: JSON.stringify(next.artifacts_json || {}),
typeof next.state_json === 'string' ? next.state_json : JSON.stringify(next.state_json || {}), typeof next.state_json === 'string' ? next.state_json : JSON.stringify(next.state_json || {}),
timestamp, timestamp,
Number(taskId), Number(taskId),
@@ -2,7 +2,6 @@ import type { PoolClient } from 'pg'
import type { WorkerWalletRow } from './types.js' import type { WorkerWalletRow } from './types.js'
export async function ensureWorkerWalletWithClient( export async function ensureWorkerWalletWithClient(
client: PoolClient, client: PoolClient,
workerId: number, workerId: number,
@@ -1,5 +1,3 @@
export type WorkerLevelRow = { export type WorkerLevelRow = {
id: number id: number
level_key: string level_key: string
@@ -1,10 +1,25 @@
import { query, withTransaction } from '../../db/client.js' import { query, withTransaction } from '../../db/client.js'
import { ensureWorkerWalletWithClient, getWorkerWalletWithClient, toJsonString, toPositiveInteger } from './shared.js' import {
import type { CreateWorkOrderInput, GrabWorkOrderResult, ListInput, ProblemWorkOrderResolutionAction, ProductRuleListInput, WorkCategoryRow, WorkerDepositUnfreezeRow, WorkOrderRow, WorkOrderShareRow, WorkProductRuleRow } from './types.js' ensureWorkerWalletWithClient,
getWorkerWalletWithClient,
toJsonString,
toPositiveInteger,
} from './shared.js'
import type {
CreateWorkOrderInput,
GrabWorkOrderResult,
ListInput,
ProblemWorkOrderResolutionAction,
ProductRuleListInput,
WorkCategoryRow,
WorkerDepositUnfreezeRow,
WorkOrderRow,
WorkOrderShareRow,
WorkProductRuleRow,
} from './types.js'
import type { PoolClient } from 'pg' import type { PoolClient } from 'pg'
import { maybeUpgradeWorkerLevelWithClient } from './worker-repo.js' import { maybeUpgradeWorkerLevelWithClient } from './worker-repo.js'
const WORK_ORDER_SELECT = ` const WORK_ORDER_SELECT = `
SELECT SELECT
wo.*, wo.*,
@@ -248,7 +263,10 @@ export async function submitWorkOrderShareAcceptance(input: {
workerId: number workerId: number
acceptanceJson: string acceptanceJson: string
now: string now: string
}): Promise<{ share: WorkOrderShareRow | null; failureReason: 'share_not_found' | 'share_status_invalid' | null }> { }): Promise<{
share: WorkOrderShareRow | null
failureReason: 'share_not_found' | 'share_status_invalid' | null
}> {
return withTransaction(async (client) => { return withTransaction(async (client) => {
const lockResult = await client.query<{ id: number }>( const lockResult = await client.query<{ id: number }>(
` `
@@ -436,7 +454,9 @@ export async function getWorkCategoryByKey(categoryKey: string): Promise<WorkCat
return result.rows[0] || null return result.rows[0] || null
} }
export async function getWorkCategoryById(categoryId: number | string): Promise<WorkCategoryRow | null> { export async function getWorkCategoryById(
categoryId: number | string,
): Promise<WorkCategoryRow | null> {
const result = await query<WorkCategoryRow>( const result = await query<WorkCategoryRow>(
'SELECT * FROM work_categories WHERE id = $1 LIMIT 1', 'SELECT * FROM work_categories WHERE id = $1 LIMIT 1',
[Number(categoryId)], [Number(categoryId)],
@@ -747,8 +767,7 @@ export async function listWorkOrders({
pinnedFirst = false, pinnedFirst = false,
sort = 'id_desc', sort = 'id_desc',
}: ListInput = {}): Promise<{ items: WorkOrderRow[]; total: number }> { }: ListInput = {}): Promise<{ items: WorkOrderRow[]; total: number }> {
const effectiveStatuses = const effectiveStatuses = statuses.length > 0 ? statuses : status.trim() ? [status.trim()] : []
statuses.length > 0 ? statuses : status.trim() ? [status.trim()] : []
const { whereClause, params } = buildWorkOrderWhere({ const { whereClause, params } = buildWorkOrderWhere({
statuses: effectiveStatuses, statuses: effectiveStatuses,
keyword, keyword,
@@ -787,9 +806,7 @@ export async function listWorkOrders({
) DESC NULLS LAST, wo.id DESC` ) DESC NULLS LAST, wo.id DESC`
})() })()
: 'wo.id DESC' : 'wo.id DESC'
const effectiveOrderBy = pinnedFirst const effectiveOrderBy = pinnedFirst ? `wo.pinned_at DESC NULLS LAST, ${orderBy}` : orderBy
? `wo.pinned_at DESC NULLS LAST, ${orderBy}`
: orderBy
params.push(pageSize, offset) params.push(pageSize, offset)
const itemsResult = await query<WorkOrderRow>( const itemsResult = await query<WorkOrderRow>(
`${WORK_ORDER_SELECT} `${WORK_ORDER_SELECT}
@@ -911,14 +928,8 @@ export async function updateWorkOrderBasic(
String((patch.productName ?? current.product_name) || '').trim(), String((patch.productName ?? current.product_name) || '').trim(),
patch.categoryId === undefined ? current.category_id : patch.categoryId, patch.categoryId === undefined ? current.category_id : patch.categoryId,
toPositiveInteger(patch.rewardAmount ?? current.reward_amount, 0), toPositiveInteger(patch.rewardAmount ?? current.reward_amount, 0),
toPositiveInteger( toPositiveInteger(patch.requiredDepositAmount ?? current.required_deposit_amount, 0),
patch.requiredDepositAmount ?? current.required_deposit_amount, toPositiveInteger(patch.depositThresholdAmount ?? current.deposit_threshold_amount, 0),
0,
),
toPositiveInteger(
patch.depositThresholdAmount ?? current.deposit_threshold_amount,
0,
),
toJsonString(patch.requirementJson ?? current.requirement_json), toJsonString(patch.requirementJson ?? current.requirement_json),
toPositiveInteger(patch.timeoutMinutes ?? current.timeout_minutes, 0), toPositiveInteger(patch.timeoutMinutes ?? current.timeout_minutes, 0),
String((patch.timeoutPolicy ?? current.timeout_policy) || 'reopen').trim(), String((patch.timeoutPolicy ?? current.timeout_policy) || 'reopen').trim(),
@@ -1196,10 +1207,7 @@ export async function cancelWorkerWorkOrder(input: {
if (releaseAmount > 0) { if (releaseAmount > 0) {
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount
const nextFrozen = Math.max( const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
0,
Number(wallet?.frozen_deposit_amount || 0) - releaseAmount,
)
await client.query( await client.query(
` `
UPDATE worker_wallets UPDATE worker_wallets
@@ -1237,10 +1245,7 @@ export async function cancelWorkerWorkOrder(input: {
}) })
} }
export async function unassignWorkOrder(input: { export async function unassignWorkOrder(input: { workOrderId: number; now: string }): Promise<{
workOrderId: number
now: string
}): Promise<{
order: WorkOrderRow | null order: WorkOrderRow | null
failureReason: 'work_order_not_in_progress' | 'work_order_no_worker' | null failureReason: 'work_order_not_in_progress' | 'work_order_no_worker' | null
}> { }> {
@@ -1286,10 +1291,7 @@ export async function unassignWorkOrder(input: {
if (releaseAmount > 0) { if (releaseAmount > 0) {
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount
const nextFrozen = Math.max( const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
0,
Number(wallet?.frozen_deposit_amount || 0) - releaseAmount,
)
await client.query( await client.query(
` `
UPDATE worker_wallets UPDATE worker_wallets
@@ -1378,10 +1380,7 @@ export async function returnAssignedWorkOrderToHall(input: {
if (releaseAmount > 0) { if (releaseAmount > 0) {
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount
const nextFrozen = Math.max( const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
0,
Number(wallet?.frozen_deposit_amount || 0) - releaseAmount,
)
await client.query( await client.query(
` `
UPDATE worker_wallets UPDATE worker_wallets
@@ -1474,10 +1473,7 @@ export async function cancelAssignedWorkOrder(input: {
if (releaseAmount > 0) { if (releaseAmount > 0) {
const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount const nextAvailable = Number(wallet?.available_amount || 0) + releaseAmount
const nextFrozen = Math.max( const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
0,
Number(wallet?.frozen_deposit_amount || 0) - releaseAmount,
)
await client.query( await client.query(
` `
UPDATE worker_wallets UPDATE worker_wallets
@@ -1608,10 +1604,7 @@ export async function acceptWorkOrderAndSettle(input: {
Number(wallet?.available_amount || 0) + Number(wallet?.available_amount || 0) +
shareReward + shareReward +
(shouldDelayUnfreeze ? 0 : releaseAmount) (shouldDelayUnfreeze ? 0 : releaseAmount)
const nextFrozen = Math.max( const nextFrozen = Math.max(0, Number(wallet?.frozen_deposit_amount || 0) - releaseAmount)
0,
Number(wallet?.frozen_deposit_amount || 0) - releaseAmount,
)
await client.query( await client.query(
` `
@@ -1829,7 +1822,10 @@ export async function acceptWorkOrderAndSettle(input: {
await maybeUpgradeWorkerLevelWithClient(client, workerId, input.now) await maybeUpgradeWorkerLevelWithClient(client, workerId, input.now)
} }
return { order: await getWorkOrderByIdWithClient(client, input.workOrderId), failureReason: null } return {
order: await getWorkOrderByIdWithClient(client, input.workOrderId),
failureReason: null,
}
}) })
} }
@@ -2263,7 +2259,10 @@ export async function listDueDepositUnfreezes({
export async function releaseDepositUnfreeze({ export async function releaseDepositUnfreeze({
unfreezeId, unfreezeId,
now, now,
}: { unfreezeId: number; now: string }): Promise<WorkerDepositUnfreezeRow | null> { }: {
unfreezeId: number
now: string
}): Promise<WorkerDepositUnfreezeRow | null> {
return withTransaction(async (client) => { return withTransaction(async (client) => {
const currentResult = await client.query<WorkerDepositUnfreezeRow>( const currentResult = await client.query<WorkerDepositUnfreezeRow>(
` `
@@ -2295,10 +2294,7 @@ export async function releaseDepositUnfreeze({
if (amount > 0) { if (amount > 0) {
const nextAvailable = Number(wallet?.available_amount || 0) + amount const nextAvailable = Number(wallet?.available_amount || 0) + amount
const nextPending = Math.max( const nextPending = Math.max(0, Number(wallet?.pending_unfreeze_amount || 0) - amount)
0,
Number(wallet?.pending_unfreeze_amount || 0) - amount,
)
await client.query( await client.query(
` `
UPDATE worker_wallets UPDATE worker_wallets
@@ -2337,7 +2333,13 @@ export async function releaseDepositUnfreeze({
async function enqueueDepositUnfreezeWithClient( async function enqueueDepositUnfreezeWithClient(
client: PoolClient, client: PoolClient,
input: { workerId: number; workOrderId: number; amount: number; unfreezeDays: number; now: string }, input: {
workerId: number
workOrderId: number
amount: number
unfreezeDays: number
now: string
},
) { ) {
if (input.amount <= 0) return if (input.amount <= 0) return
const unfreezeAt = new Date( const unfreezeAt = new Date(
@@ -2375,7 +2377,9 @@ export async function countWorkerActiveOrders(workerId: number | string): Promis
return Number(result.rows[0]?.total || 0) return Number(result.rows[0]?.total || 0)
} }
export async function countTimeoutEventsByWorkerIds(workerIds: number[]): Promise<Map<number, number>> { export async function countTimeoutEventsByWorkerIds(
workerIds: number[],
): Promise<Map<number, number>> {
const counts = new Map<number, number>() const counts = new Map<number, number>()
const uniqueIds = [...new Set(workerIds.map((id) => Number(id)).filter((id) => id > 0))] const uniqueIds = [...new Set(workerIds.map((id) => Number(id)).filter((id) => id > 0))]
if (uniqueIds.length === 0) return counts if (uniqueIds.length === 0) return counts
@@ -2481,7 +2485,10 @@ async function getOutstandingDepositAmountWithClient(
return resolveOutstandingDepositAmount(result.rows[0]) return resolveOutstandingDepositAmount(result.rows[0])
} }
async function countWorkerActiveOrdersWithClient(client: PoolClient, workerId: number): Promise<number> { async function countWorkerActiveOrdersWithClient(
client: PoolClient,
workerId: number,
): Promise<number> {
const result = await client.query<{ total: number }>( const result = await client.query<{ total: number }>(
` `
SELECT ( SELECT (
@@ -2542,11 +2549,7 @@ function buildWorkOrderWhere({
const filters: string[] = [] const filters: string[] = []
const params: unknown[] = [] const params: unknown[] = []
const normalizedStatuses = [ const normalizedStatuses = [
...new Set( ...new Set(statuses.map((item) => String(item || '').trim()).filter(Boolean)),
statuses
.map((item) => String(item || '').trim())
.filter(Boolean),
),
] ]
if (normalizedStatuses.length > 0) { if (normalizedStatuses.length > 0) {
params.push(normalizedStatuses) params.push(normalizedStatuses)
@@ -1,9 +1,18 @@
import { query, withTransaction } from '../../db/client.js' import { query, withTransaction } from '../../db/client.js'
import { ensureWorkerWalletWithClient, getWorkerWalletWithClient } from './shared.js' import { ensureWorkerWalletWithClient, getWorkerWalletWithClient } from './shared.js'
import type { CreateWorkerInput, FinanceRequestListInput, ListInput, WalletLedgerListInput, WorkerFinanceRequestRow, WorkerLevelRow, WorkerUserRow, WorkerWalletLedgerRow, WorkerWalletRow } from './types.js' import type {
CreateWorkerInput,
FinanceRequestListInput,
ListInput,
WalletLedgerListInput,
WorkerFinanceRequestRow,
WorkerLevelRow,
WorkerUserRow,
WorkerWalletLedgerRow,
WorkerWalletRow,
} from './types.js'
import type { PoolClient } from 'pg' import type { PoolClient } from 'pg'
const WORKER_USER_SELECT = ` const WORKER_USER_SELECT = `
SELECT SELECT
wu.*, wu.*,
@@ -119,10 +128,9 @@ export async function getWorkerUserByUsername(username: string): Promise<WorkerU
} }
export async function getWorkerUserByPhone(phone: string): Promise<WorkerUserRow | null> { export async function getWorkerUserByPhone(phone: string): Promise<WorkerUserRow | null> {
const result = await query<WorkerUserRow>( const result = await query<WorkerUserRow>(`${WORKER_USER_SELECT} WHERE wu.phone = $1 LIMIT 1`, [
`${WORKER_USER_SELECT} WHERE wu.phone = $1 LIMIT 1`, String(phone || '').trim(),
[String(phone || '').trim()], ])
)
return result.rows[0] || null return result.rows[0] || null
} }
@@ -136,12 +144,14 @@ export async function getWorkerUserByDisplayName(
return result.rows[0] || null return result.rows[0] || null
} }
export async function getWorkerUserByInviteCode( export async function getWorkerUserByInviteCode(inviteCode: string): Promise<WorkerUserRow | null> {
inviteCode: string,
): Promise<WorkerUserRow | null> {
const result = await query<WorkerUserRow>( const result = await query<WorkerUserRow>(
`${WORKER_USER_SELECT} WHERE wu.invite_code = $1 LIMIT 1`, `${WORKER_USER_SELECT} WHERE wu.invite_code = $1 LIMIT 1`,
[String(inviteCode || '').trim().toUpperCase()], [
String(inviteCode || '')
.trim()
.toUpperCase(),
],
) )
return result.rows[0] || null return result.rows[0] || null
} }
@@ -827,23 +837,18 @@ export async function maybeUpgradeWorkerLevelWithClient(
return null return null
} }
await client.query( await client.query('UPDATE worker_users SET level_id = $1, updated_at = $2 WHERE id = $3', [
'UPDATE worker_users SET level_id = $1, updated_at = $2 WHERE id = $3', nextLevelId,
[nextLevelId, now, workerId], now,
) workerId,
])
await client.query( await client.query(
` `
INSERT INTO worker_level_logs ( INSERT INTO worker_level_logs (
worker_id, from_level_id, to_level_id, reason, payload_json, created_at worker_id, from_level_id, to_level_id, reason, payload_json, created_at
) VALUES ($1, $2, $3, 'auto', $4::jsonb, $5) ) VALUES ($1, $2, $3, 'auto', $4::jsonb, $5)
`, `,
[ [workerId, currentRow.level_id || null, nextLevelId, JSON.stringify({ acceptedCount }), now],
workerId,
currentRow.level_id || null,
nextLevelId,
JSON.stringify({ acceptedCount }),
now,
],
) )
return getWorkerUserByIdWithClient(client, workerId) return getWorkerUserByIdWithClient(client, workerId)
} }
@@ -903,10 +908,7 @@ function buildWorkerUserWhere({
} }
} }
function buildWorkerWalletLedgerWhere({ function buildWorkerWalletLedgerWhere({ workerId = 0, ledgerType = '' }: WalletLedgerListInput) {
workerId = 0,
ledgerType = '',
}: WalletLedgerListInput) {
const filters: string[] = [] const filters: string[] = []
const params: unknown[] = [] const params: unknown[] = []
if (workerId) { if (workerId) {
+5 -5
View File
@@ -10,13 +10,13 @@ const router = Router()
router.use('/audit-logs', requireAdminRoles(['admin'])) router.use('/audit-logs', requireAdminRoles(['admin']))
router.get('/audit-logs', createJsonHandler( router.get(
(req) => getAdminAuditLogs(req.query as AdminAuditLogRouteQuery), '/audit-logs',
{ createJsonHandler((req) => getAdminAuditLogs(req.query as AdminAuditLogRouteQuery), {
successMessage: 'ok', successMessage: 'ok',
errorMessage: '读取操作审计日志失败', errorMessage: '读取操作审计日志失败',
scope: '[admin/audit-logs]', scope: '[admin/audit-logs]',
}, }),
)) )
export default router export default router
+19 -14
View File
@@ -11,13 +11,17 @@ import { createJsonHandler, extractBearerToken } from './session.js'
const router = Router() const router = Router()
router.post('/auth/login', createRateLimitMiddleware({ router.post(
'/auth/login',
createRateLimitMiddleware({
scope: 'admin:login', scope: 'admin:login',
windowMs: 60_000, windowMs: 60_000,
max: 10, max: 10,
key: getBodyFieldRateLimitKey('username'), key: getBodyFieldRateLimitKey('username'),
}), createJsonHandler( }),
(req) => loginAdmin(req.body?.username, req.body?.password, { createJsonHandler(
(req) =>
loginAdmin(req.body?.username, req.body?.password, {
ip: resolveClientIp(req), ip: resolveClientIp(req),
userAgent: resolveUserAgent(req), userAgent: resolveUserAgent(req),
location: resolveClientLocation(req), location: resolveClientLocation(req),
@@ -27,24 +31,25 @@ router.post('/auth/login', createRateLimitMiddleware({
errorMessage: '后台登录失败', errorMessage: '后台登录失败',
scope: '[admin/auth/login]', scope: '[admin/auth/login]',
}, },
)) ),
)
router.get('/auth/session', createJsonHandler( router.get(
(req) => getAdminSessionSummary(extractBearerToken(req)), '/auth/session',
{ createJsonHandler((req) => getAdminSessionSummary(extractBearerToken(req)), {
successMessage: 'ok', successMessage: 'ok',
errorMessage: '读取后台登录态失败', errorMessage: '读取后台登录态失败',
scope: '[admin/auth/session]', scope: '[admin/auth/session]',
}, }),
)) )
router.post('/auth/logout', createJsonHandler( router.post(
() => ({ success: true }), '/auth/logout',
{ createJsonHandler(() => ({ success: true }), {
successMessage: '已退出登录', successMessage: '已退出登录',
errorMessage: '后台退出失败', errorMessage: '后台退出失败',
scope: '[admin/auth/logout]', scope: '[admin/auth/logout]',
}, }),
)) )
export default router export default router
@@ -14,7 +14,12 @@ import { createHttpError, sendRouteError } from '../../utils/http.js'
import type { AdminCloudtentaclesDeliveryRecordQueryRouteBody } from '../../types/admin/route-inputs.js' import type { AdminCloudtentaclesDeliveryRecordQueryRouteBody } from '../../types/admin/route-inputs.js'
const router = Router() const router = Router()
const RECORD_IMAGE_CACHE_DIR = path.join(PROJECT_ROOT, 'data', 'cache', 'cloudtentacles-record-images') const RECORD_IMAGE_CACHE_DIR = path.join(
PROJECT_ROOT,
'data',
'cache',
'cloudtentacles-record-images',
)
router.get( router.get(
'/cloudtentacles-records/sources', '/cloudtentacles-records/sources',
@@ -155,7 +160,10 @@ function buildRecordImageCacheMeta(imageUrl: string) {
return { return {
candidateFilePaths: candidateTypes.map((contentType) => ({ candidateFilePaths: candidateTypes.map((contentType) => ({
contentType, contentType,
filePath: path.join(RECORD_IMAGE_CACHE_DIR, `${digest}.${extensionForContentType(contentType)}`), filePath: path.join(
RECORD_IMAGE_CACHE_DIR,
`${digest}.${extensionForContentType(contentType)}`,
),
})), })),
filePathForContentType(contentType: string) { filePathForContentType(contentType: string) {
return path.join(RECORD_IMAGE_CACHE_DIR, `${digest}.${extensionForContentType(contentType)}`) return path.join(RECORD_IMAGE_CACHE_DIR, `${digest}.${extensionForContentType(contentType)}`)
@@ -164,7 +172,12 @@ function buildRecordImageCacheMeta(imageUrl: string) {
} }
function normalizeImageContentType(contentType: string) { function normalizeImageContentType(contentType: string) {
return String(contentType || 'image/jpeg').split(';')[0]?.trim().toLowerCase() || 'image/jpeg' return (
String(contentType || 'image/jpeg')
.split(';')[0]
?.trim()
.toLowerCase() || 'image/jpeg'
)
} }
function extensionForContentType(contentType: string) { function extensionForContentType(contentType: string) {
+10 -10
View File
@@ -7,22 +7,22 @@ import { createJsonHandler } from './session.js'
const router = Router() const router = Router()
router.get('/dashboard/summary', createJsonHandler( router.get(
() => getAdminDashboardSummary(), '/dashboard/summary',
{ createJsonHandler(() => getAdminDashboardSummary(), {
successMessage: 'ok', successMessage: 'ok',
errorMessage: '读取后台概览失败', errorMessage: '读取后台概览失败',
scope: '[admin/dashboard/summary]', scope: '[admin/dashboard/summary]',
}, }),
)) )
router.get('/dashboard/login-logs', createJsonHandler( router.get(
(req) => getAdminLoginLogs(req.query as JsonObject), '/dashboard/login-logs',
{ createJsonHandler((req) => getAdminLoginLogs(req.query as JsonObject), {
successMessage: 'ok', successMessage: 'ok',
errorMessage: '读取登录记录失败', errorMessage: '读取登录记录失败',
scope: '[admin/dashboard/login-logs]', scope: '[admin/dashboard/login-logs]',
}, }),
)) )
export default router export default router
+1 -5
View File
@@ -15,11 +15,7 @@ import { createJsonHandler, requireAdminRoles } from './session.js'
const router = Router() const router = Router()
function requireDevMockEnabled() { function requireDevMockEnabled() {
return ( return (_req: unknown, _res: unknown, next: (error?: unknown) => void) => {
_req: unknown,
_res: unknown,
next: (error?: unknown) => void,
) => {
if (!isDevMockEnabled()) { if (!isDevMockEnabled()) {
next( next(
createHttpError('开发 Mock 仅在非 production 环境可用(或设置 ENABLE_DEV_MOCK=1', { createHttpError('开发 Mock 仅在非 production 环境可用(或设置 ENABLE_DEV_MOCK=1', {
@@ -57,7 +57,8 @@ router.post(
successMessage: '售后单列表已查询', successMessage: '售后单列表已查询',
errorMessage: '查询快手售后单列表失败', errorMessage: '查询快手售后单列表失败',
scope: '[admin/kuaishou-industry/refunds/list]', scope: '[admin/kuaishou-industry/refunds/list]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_refund_list', req.body, data), audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_refund_list', req.body, data),
}, },
), ),
) )
@@ -72,7 +73,8 @@ router.post(
successMessage: '同意退款接口已执行', successMessage: '同意退款接口已执行',
errorMessage: '执行同意退款失败', errorMessage: '执行同意退款失败',
scope: '[admin/kuaishou-industry/refunds/approve]', scope: '[admin/kuaishou-industry/refunds/approve]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_refund_approve', req.body, data), audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_refund_approve', req.body, data),
}, },
), ),
) )
@@ -87,7 +89,8 @@ router.post(
successMessage: '不同意退款接口已执行', successMessage: '不同意退款接口已执行',
errorMessage: '执行不同意退款失败', errorMessage: '执行不同意退款失败',
scope: '[admin/kuaishou-industry/refunds/disagree]', scope: '[admin/kuaishou-industry/refunds/disagree]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_refund_disagree', req.body, data), audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_refund_disagree', req.body, data),
}, },
), ),
) )
@@ -104,7 +107,8 @@ router.post(
successMessage: '电子凭证有效性已检查', successMessage: '电子凭证有效性已检查',
errorMessage: '检查电子凭证有效性失败', errorMessage: '检查电子凭证有效性失败',
scope: '[admin/kuaishou-industry/vouchers/check-available]', scope: '[admin/kuaishou-industry/vouchers/check-available]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_voucher_check_available', req.body, data), audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_voucher_check_available', req.body, data),
}, },
), ),
) )
@@ -119,7 +123,8 @@ router.post(
successMessage: '电子凭证冲正回调已执行', successMessage: '电子凭证冲正回调已执行',
errorMessage: '执行电子凭证冲正失败', errorMessage: '执行电子凭证冲正失败',
scope: '[admin/kuaishou-industry/vouchers/reverse]', scope: '[admin/kuaishou-industry/vouchers/reverse]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_voucher_reverse', req.body, data), audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_voucher_reverse', req.body, data),
}, },
), ),
) )
@@ -136,7 +141,8 @@ router.post(
successMessage: '电子凭证已手动核销', successMessage: '电子凭证已手动核销',
errorMessage: '手动核销电子凭证失败', errorMessage: '手动核销电子凭证失败',
scope: '[admin/kuaishou-industry/vouchers/consume]', scope: '[admin/kuaishou-industry/vouchers/consume]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_voucher_consume', req.body, data), audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_voucher_consume', req.body, data),
}, },
), ),
) )
@@ -153,7 +159,8 @@ router.post(
successMessage: '电子凭证发码回调已重发', successMessage: '电子凭证发码回调已重发',
errorMessage: '重发电子凭证发码回调失败', errorMessage: '重发电子凭证发码回调失败',
scope: '[admin/kuaishou-industry/vouchers/resend-code]', scope: '[admin/kuaishou-industry/vouchers/resend-code]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_voucher_resend_code', req.body, data), audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_voucher_resend_code', req.body, data),
}, },
), ),
) )
@@ -170,14 +177,15 @@ router.post(
successMessage: '电子凭证已手动销毁', successMessage: '电子凭证已手动销毁',
errorMessage: '手动销毁电子凭证失败', errorMessage: '手动销毁电子凭证失败',
scope: '[admin/kuaishou-industry/vouchers/destroy]', scope: '[admin/kuaishou-industry/vouchers/destroy]',
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_voucher_destroy', req.body, data), audit: (req, data) =>
buildKuaishouIndustryAudit('kuaishou_industry_voucher_destroy', req.body, data),
}, },
), ),
) )
function buildKuaishouIndustryAudit(action: string, body: unknown, data: unknown) { function buildKuaishouIndustryAudit(action: string, body: unknown, data: unknown) {
const payload = body && typeof body === 'object' ? body as Record<string, unknown> : {} const payload = body && typeof body === 'object' ? (body as Record<string, unknown>) : {}
const result = data && typeof data === 'object' ? data as Record<string, unknown> : {} const result = data && typeof data === 'object' ? (data as Record<string, unknown>) : {}
return { return {
action, action,
+12 -12
View File
@@ -3,23 +3,22 @@ import { Router } from 'express'
import { getAdminOrderDetail, getAdminOrders } from '../../services/admin/admin-read-service.js' import { getAdminOrderDetail, getAdminOrders } from '../../services/admin/admin-read-service.js'
import { resendAdminOrderKuaishouIndustryVoucherCodes } from '../../services/admin/write/kuaishou-cloud-actions.js' import { resendAdminOrderKuaishouIndustryVoucherCodes } from '../../services/admin/write/kuaishou-cloud-actions.js'
import { createJsonHandler, requireAdminRoles } from './session.js' import { createJsonHandler, requireAdminRoles } from './session.js'
import type { import type { AdminOrderRouteParams, AdminOrderRouteQuery } from '../../types/admin/route-inputs.js'
AdminOrderRouteParams,
AdminOrderRouteQuery,
} from '../../types/admin/route-inputs.js'
const router = Router() const router = Router()
router.get('/orders', createJsonHandler( router.get(
(req) => getAdminOrders(req.query as AdminOrderRouteQuery), '/orders',
{ createJsonHandler((req) => getAdminOrders(req.query as AdminOrderRouteQuery), {
successMessage: 'ok', successMessage: 'ok',
errorMessage: '读取订单列表失败', errorMessage: '读取订单列表失败',
scope: '[admin/orders]', scope: '[admin/orders]',
}, }),
)) )
router.get('/orders/:orderId', createJsonHandler( router.get(
'/orders/:orderId',
createJsonHandler(
(req) => (req) =>
getAdminOrderDetail( getAdminOrderDetail(
String((req.params as AdminOrderRouteParams).orderId || ''), String((req.params as AdminOrderRouteParams).orderId || ''),
@@ -30,7 +29,8 @@ router.get('/orders/:orderId', createJsonHandler(
errorMessage: '读取订单详情失败', errorMessage: '读取订单详情失败',
scope: '[admin/orders/:orderId]', scope: '[admin/orders/:orderId]',
}, },
)) ),
)
router.post( router.post(
'/orders/:orderId/kuaishou-industry/resend-code', '/orders/:orderId/kuaishou-industry/resend-code',
@@ -49,7 +49,7 @@ router.post(
action: 'order_kuaishou_industry_resend_code', action: 'order_kuaishou_industry_resend_code',
targetType: 'order', targetType: 'order',
targetId: String((req.params as AdminOrderRouteParams).orderId || ''), targetId: String((req.params as AdminOrderRouteParams).orderId || ''),
data: data && typeof data === 'object' ? data as Record<string, unknown> : {}, data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
}), }),
}, },
), ),
@@ -1,23 +1,23 @@
import { Router } from "express"; import { Router } from 'express'
import { requireAdminRoles } from "./session.js"; import { requireAdminRoles } from './session.js'
import cloudtentaclesRouter from "./platform-config/cloudtentacles.js"; import cloudtentaclesRouter from './platform-config/cloudtentacles.js'
import kuaishouIndustryRouter from "./platform-config/kuaishou-industry.js"; import kuaishouIndustryRouter from './platform-config/kuaishou-industry.js'
import kuaishouFeifeiRouter from "./platform-config/kuaishou-feifei.js"; import kuaishouFeifeiRouter from './platform-config/kuaishou-feifei.js'
import ninetyoneRouter from "./platform-config/ninetyone.js"; import ninetyoneRouter from './platform-config/ninetyone.js'
import notificationsRouter from "./platform-config/notifications.js"; import notificationsRouter from './platform-config/notifications.js'
import fulfillmentRoutingRouter from "./platform-config/fulfillment-routing.js"; import fulfillmentRoutingRouter from './platform-config/fulfillment-routing.js'
import affiliateDashRouter from "./platform-config/affiliate-dash.js"; import affiliateDashRouter from './platform-config/affiliate-dash.js'
const router = Router(); const router = Router()
router.use("/platform-config", kuaishouIndustryRouter); router.use('/platform-config', kuaishouIndustryRouter)
router.use("/platform-config", requireAdminRoles(["admin"])); router.use('/platform-config', requireAdminRoles(['admin']))
router.use("/platform-config", notificationsRouter); router.use('/platform-config', notificationsRouter)
router.use("/platform-config", kuaishouFeifeiRouter); router.use('/platform-config', kuaishouFeifeiRouter)
router.use("/platform-config", ninetyoneRouter); router.use('/platform-config', ninetyoneRouter)
router.use("/platform-config", fulfillmentRoutingRouter); router.use('/platform-config', fulfillmentRoutingRouter)
router.use("/platform-config", cloudtentaclesRouter); router.use('/platform-config', cloudtentaclesRouter)
router.use("/platform-config", affiliateDashRouter); router.use('/platform-config', affiliateDashRouter)
export default router; export default router
@@ -1,4 +1,4 @@
import { Router } from "express"; import { Router } from 'express'
import { import {
getAdminAffiliateDashConfig, getAdminAffiliateDashConfig,
@@ -6,84 +6,77 @@ import {
listAdminAffiliateDashProducts, listAdminAffiliateDashProducts,
matchAdminAffiliateDashSku, matchAdminAffiliateDashSku,
updateAdminAffiliateDashConfig, updateAdminAffiliateDashConfig,
} from "../../../services/admin/platform-config/affiliate-dash-service.js"; } from '../../../services/admin/platform-config/affiliate-dash-service.js'
import { createJsonHandler } from "../session.js"; import { createJsonHandler } from '../session.js'
import type { JsonRecord } from "../../../types/json.js"; import type { JsonRecord } from '../../../types/json.js'
const router = Router(); const router = Router()
router.get( router.get(
"/affiliate-dash", '/affiliate-dash',
createJsonHandler(() => getAdminAffiliateDashConfig(), { createJsonHandler(() => getAdminAffiliateDashConfig(), {
successMessage: "ok", successMessage: 'ok',
errorMessage: "读取 affiliate-dash 配置失败", errorMessage: '读取 affiliate-dash 配置失败',
scope: "[admin/platform-config/affiliate-dash]", scope: '[admin/platform-config/affiliate-dash]',
}) }),
); )
router.post( router.post(
"/affiliate-dash", '/affiliate-dash',
createJsonHandler( createJsonHandler((req) => updateAdminAffiliateDashConfig(req.body as JsonRecord), {
(req) => updateAdminAffiliateDashConfig(req.body as JsonRecord), successMessage: 'affiliate-dash 配置已保存',
{ errorMessage: '保存 affiliate-dash 配置失败',
successMessage: "affiliate-dash 配置已保存", scope: '[admin/platform-config/affiliate-dash]',
errorMessage: "保存 affiliate-dash 配置失败",
scope: "[admin/platform-config/affiliate-dash]",
audit: (_req, data) => { audit: (_req, data) => {
const result = data as JsonRecord; const result = data as JsonRecord
const source = result.source as JsonRecord | undefined; const source = result.source as JsonRecord | undefined
const effective = result.effective as JsonRecord | undefined; const effective = result.effective as JsonRecord | undefined
return { return {
action: "platform_affiliate_dash_config_updated", action: 'platform_affiliate_dash_config_updated',
targetType: "platform_config", targetType: 'platform_config',
targetId: "affiliate_dash", targetId: 'affiliate_dash',
data: { data: {
enabled: effective?.enabled !== false, enabled: effective?.enabled !== false,
hasAppKey: Boolean(effective?.hasAppKey), hasAppKey: Boolean(effective?.hasAppKey),
hasAppSecret: Boolean(effective?.hasAppSecret), hasAppSecret: Boolean(effective?.hasAppSecret),
hasCallbackSecret: Boolean(effective?.hasCallbackSecret), hasCallbackSecret: Boolean(effective?.hasCallbackSecret),
skuMappingCount: Number(source?.skuMapping && typeof source.skuMapping === "object" skuMappingCount: Number(
source?.skuMapping && typeof source.skuMapping === 'object'
? Object.keys(source.skuMapping).length ? Object.keys(source.skuMapping).length
: 0), : 0,
}, ),
};
}, },
} }
},
}),
) )
);
router.post( router.post(
"/affiliate-dash/match", '/affiliate-dash/match',
createJsonHandler( createJsonHandler((req) => matchAdminAffiliateDashSku(req.body as JsonRecord), {
(req) => matchAdminAffiliateDashSku(req.body as JsonRecord), successMessage: 'ok',
{ errorMessage: '匹配 affiliate-dash sku 失败',
successMessage: "ok", scope: '[admin/platform-config/affiliate-dash/match]',
errorMessage: "匹配 affiliate-dash sku 失败", }),
scope: "[admin/platform-config/affiliate-dash/match]",
}
) )
);
router.post( router.post(
"/affiliate-dash/products", '/affiliate-dash/products',
createJsonHandler( createJsonHandler((req) => listAdminAffiliateDashProducts(req.body as JsonRecord), {
(req) => listAdminAffiliateDashProducts(req.body as JsonRecord), successMessage: 'ok',
{ errorMessage: '查询 affiliate-dash 商品失败',
successMessage: "ok", scope: '[admin/platform-config/affiliate-dash/products]',
errorMessage: "查询 affiliate-dash 商品失败", }),
scope: "[admin/platform-config/affiliate-dash/products]",
}
) )
);
router.post( router.post(
"/affiliate-dash/wallet", '/affiliate-dash/wallet',
createJsonHandler(() => getAdminAffiliateDashWallet(), { createJsonHandler(() => getAdminAffiliateDashWallet(), {
successMessage: "ok", successMessage: 'ok',
errorMessage: "查询 affiliate-dash 钱包失败", errorMessage: '查询 affiliate-dash 钱包失败',
scope: "[admin/platform-config/affiliate-dash/wallet]", scope: '[admin/platform-config/affiliate-dash/wallet]',
}) }),
); )
export default router; export default router
@@ -1,4 +1,4 @@
import { Router } from "express"; import { Router } from 'express'
import { import {
appointAdminCloudtentaclesVirtualNumber, appointAdminCloudtentaclesVirtualNumber,
@@ -23,7 +23,7 @@ import {
useAdminCloudtentaclesSku, useAdminCloudtentaclesSku,
validateAdminCloudtentaclesSession, validateAdminCloudtentaclesSession,
verifyAdminCloudtentaclesLoginCode, verifyAdminCloudtentaclesLoginCode,
} from "../../../services/admin/platform-config/cloudtentacles/index.js"; } from '../../../services/admin/platform-config/cloudtentacles/index.js'
import type { import type {
AdminCloudtentaclesCatalogQueryRouteBody, AdminCloudtentaclesCatalogQueryRouteBody,
AdminCloudtentaclesFullFlowRouteBody, AdminCloudtentaclesFullFlowRouteBody,
@@ -35,398 +35,353 @@ import type {
AdminCloudtentaclesTestLoginRouteBody, AdminCloudtentaclesTestLoginRouteBody,
AdminCloudtentaclesValidateSessionRouteBody, AdminCloudtentaclesValidateSessionRouteBody,
AdminCloudtentaclesVirtualNumberRouteBody, AdminCloudtentaclesVirtualNumberRouteBody,
} from "../../../types/admin/route-inputs.js"; } from '../../../types/admin/route-inputs.js'
import { createJsonHandler } from "../session.js"; import { createJsonHandler } from '../session.js'
import type { JsonRecord } from "../../../types/json.js"; import type { JsonRecord } from '../../../types/json.js'
const router = Router(); const router = Router()
router.get( router.get(
"/cloudtentacles-source", '/cloudtentacles-source',
createJsonHandler(() => listAdminCloudtentaclesSources(), { createJsonHandler(() => listAdminCloudtentaclesSources(), {
successMessage: "ok", successMessage: 'ok',
errorMessage: "读取 cloudtentacles 履约平台配置失败", errorMessage: '读取 cloudtentacles 履约平台配置失败',
scope: "[admin/platform-config/cloudtentacles-source]", scope: '[admin/platform-config/cloudtentacles-source]',
}) }),
); )
router.delete( router.delete(
"/cloudtentacles-source/:sourceKey", '/cloudtentacles-source/:sourceKey',
createJsonHandler( createJsonHandler(
(req) => (req) => deleteAdminCloudtentaclesSource(String(req.params.sourceKey || '').trim()),
deleteAdminCloudtentaclesSource(
String(req.params.sourceKey || "").trim()
),
{ {
successMessage: "cloudtentacles 履约平台配置已删除", successMessage: 'cloudtentacles 履约平台配置已删除',
errorMessage: "删除 cloudtentacles 履约平台配置失败", errorMessage: '删除 cloudtentacles 履约平台配置失败',
scope: "[admin/platform-config/cloudtentacles-source/:sourceKey]", scope: '[admin/platform-config/cloudtentacles-source/:sourceKey]',
audit: (req) => ({ audit: (req) => ({
action: "platform_cloudtentacles_source_deleted", action: 'platform_cloudtentacles_source_deleted',
targetType: "platform_config", targetType: 'platform_config',
targetId: String(req.params.sourceKey || "").trim(), targetId: String(req.params.sourceKey || '').trim(),
data: {}, data: {},
}), }),
} },
),
) )
);
router.post( router.post(
"/cloudtentacles-source", '/cloudtentacles-source',
createJsonHandler( createJsonHandler(
(req) => (req) =>
updateAdminCloudtentaclesSourceConfig( updateAdminCloudtentaclesSourceConfig(req.body as AdminCloudtentaclesSourceConfigRouteBody),
req.body as AdminCloudtentaclesSourceConfigRouteBody
),
{ {
successMessage: "cloudtentacles 履约平台配置已保存", successMessage: 'cloudtentacles 履约平台配置已保存',
errorMessage: "保存 cloudtentacles 履约平台配置失败", errorMessage: '保存 cloudtentacles 履约平台配置失败',
scope: "[admin/platform-config/cloudtentacles-source]", scope: '[admin/platform-config/cloudtentacles-source]',
audit: (_req, data) => { audit: (_req, data) => {
const result = data as JsonRecord; const result = data as JsonRecord
return { return {
action: "platform_cloudtentacles_source_updated", action: 'platform_cloudtentacles_source_updated',
targetType: "platform_config", targetType: 'platform_config',
targetId: "cloudtentacles_source", targetId: 'cloudtentacles_source',
data: { data: {
filePath: String(result.filePath || "").trim(), filePath: String(result.filePath || '').trim(),
username: String(result.source?.username || "").trim(), username: String(result.source?.username || '').trim(),
enabled: Boolean(result.source?.enabled), enabled: Boolean(result.source?.enabled),
}, },
};
},
} }
},
},
),
) )
);
router.get( router.get(
"/cloudtentacles/override-rules", '/cloudtentacles/override-rules',
createJsonHandler(() => getAdminCloudtentaclesOverrideRules(), { createJsonHandler(() => getAdminCloudtentaclesOverrideRules(), {
successMessage: "ok", successMessage: 'ok',
errorMessage: "读取 cloudtentacles 覆盖规则失败", errorMessage: '读取 cloudtentacles 覆盖规则失败',
scope: "[admin/platform-config/cloudtentacles/override-rules]", scope: '[admin/platform-config/cloudtentacles/override-rules]',
}) }),
); )
router.post( router.post(
"/cloudtentacles/override-rules", '/cloudtentacles/override-rules',
createJsonHandler( createJsonHandler(
(req) => (req) =>
updateAdminCloudtentaclesOverrideRules( updateAdminCloudtentaclesOverrideRules(
req.body as AdminCloudtentaclesOverrideRuleConfigRouteBody req.body as AdminCloudtentaclesOverrideRuleConfigRouteBody,
), ),
{ {
successMessage: "cloudtentacles 覆盖规则已保存", successMessage: 'cloudtentacles 覆盖规则已保存',
errorMessage: "保存 cloudtentacles 覆盖规则失败", errorMessage: '保存 cloudtentacles 覆盖规则失败',
scope: "[admin/platform-config/cloudtentacles/override-rules]", scope: '[admin/platform-config/cloudtentacles/override-rules]',
audit: (_req, data) => { audit: (_req, data) => {
const result = data as JsonRecord; const result = data as JsonRecord
return { return {
action: "platform_cloudtentacles_override_rules_updated", action: 'platform_cloudtentacles_override_rules_updated',
targetType: "platform_config", targetType: 'platform_config',
targetId: "cloudtentacles_override_rules", targetId: 'cloudtentacles_override_rules',
data: { data: {
filePath: String(result.filePath || "").trim(), filePath: String(result.filePath || '').trim(),
enabled: result.enabled !== false, enabled: result.enabled !== false,
ruleCount: Array.isArray(result.rules) ? result.rules.length : 0, ruleCount: Array.isArray(result.rules) ? result.rules.length : 0,
}, },
};
},
} }
},
},
),
) )
);
router.post( router.post(
"/cloudtentacles/send-sms-code", '/cloudtentacles/send-sms-code',
createJsonHandler( createJsonHandler(
(req) => (req) => sendAdminCloudtentaclesSmsCode(req.body as AdminCloudtentaclesSendSmsCodeRouteBody),
sendAdminCloudtentaclesSmsCode(
req.body as AdminCloudtentaclesSendSmsCodeRouteBody
),
{ {
successMessage: "cloudtentacles 短信验证码已发送", successMessage: 'cloudtentacles 短信验证码已发送',
errorMessage: "cloudtentacles 发送短信验证码失败", errorMessage: 'cloudtentacles 发送短信验证码失败',
scope: "[admin/platform-config/cloudtentacles/send-sms-code]", scope: '[admin/platform-config/cloudtentacles/send-sms-code]',
audit: (req, data) => { audit: (req, data) => {
const body = req.body as AdminCloudtentaclesSendSmsCodeRouteBody; const body = req.body as AdminCloudtentaclesSendSmsCodeRouteBody
const result = data as JsonRecord; const result = data as JsonRecord
return { return {
action: "platform_cloudtentacles_send_sms_code", action: 'platform_cloudtentacles_send_sms_code',
targetType: "platform_config", targetType: 'platform_config',
targetId: targetId: String(result.username || body.username || '').trim() || 'cloudtentacles',
String(result.username || body.username || "").trim() ||
"cloudtentacles",
data: { data: {
baseUrl: result.baseUrl || String(body.baseUrl || "").trim(), baseUrl: result.baseUrl || String(body.baseUrl || '').trim(),
phoneMasked: result.phoneMasked || "", phoneMasked: result.phoneMasked || '',
},
};
}, },
} }
},
},
),
) )
);
router.post( router.post(
"/cloudtentacles/test-login", '/cloudtentacles/test-login',
createJsonHandler( createJsonHandler(
(req) => (req) => testAdminCloudtentaclesLogin(req.body as AdminCloudtentaclesTestLoginRouteBody),
testAdminCloudtentaclesLogin(
req.body as AdminCloudtentaclesTestLoginRouteBody
),
{ {
successMessage: "cloudtentacles 登录测试成功", successMessage: 'cloudtentacles 登录测试成功',
errorMessage: "cloudtentacles 登录测试失败", errorMessage: 'cloudtentacles 登录测试失败',
scope: "[admin/platform-config/cloudtentacles/test-login]", scope: '[admin/platform-config/cloudtentacles/test-login]',
audit: (req, data) => { audit: (req, data) => {
const body = req.body as AdminCloudtentaclesTestLoginRouteBody; const body = req.body as AdminCloudtentaclesTestLoginRouteBody
const result = data as JsonRecord; const result = data as JsonRecord
return { return {
action: "platform_cloudtentacles_test_login", action: 'platform_cloudtentacles_test_login',
targetType: "platform_config", targetType: 'platform_config',
targetId: targetId: String(result.username || body.username || '').trim() || 'cloudtentacles',
String(result.username || body.username || "").trim() ||
"cloudtentacles",
data: { data: {
baseUrl: result.baseUrl || String(body.baseUrl || "").trim(), baseUrl: result.baseUrl || String(body.baseUrl || '').trim(),
permissionCount: Number(result.session?.permissionCount || 0), permissionCount: Number(result.session?.permissionCount || 0),
}, },
};
},
} }
},
},
),
) )
);
router.post( router.post(
"/cloudtentacles/validate-session", '/cloudtentacles/validate-session',
createJsonHandler( createJsonHandler(
(req) => (req) =>
validateAdminCloudtentaclesSession( validateAdminCloudtentaclesSession(req.body as AdminCloudtentaclesValidateSessionRouteBody),
req.body as AdminCloudtentaclesValidateSessionRouteBody
),
{ {
successMessage: "cloudtentacles 会话校验成功", successMessage: 'cloudtentacles 会话校验成功',
errorMessage: "cloudtentacles 会话校验失败", errorMessage: 'cloudtentacles 会话校验失败',
scope: "[admin/platform-config/cloudtentacles/validate-session]", scope: '[admin/platform-config/cloudtentacles/validate-session]',
audit: (_req, data) => { audit: (_req, data) => {
const result = data as JsonRecord; const result = data as JsonRecord
return { return {
action: "platform_cloudtentacles_validate_session", action: 'platform_cloudtentacles_validate_session',
targetType: "platform_config", targetType: 'platform_config',
targetId: "cloudtentacles_session", targetId: 'cloudtentacles_session',
data: { data: {
baseUrl: String(result.baseUrl || "").trim(), baseUrl: String(result.baseUrl || '').trim(),
permissionCount: Number(result.session?.permissionCount || 0), permissionCount: Number(result.session?.permissionCount || 0),
}, },
}; }
},
}, },
}
)
);
router.post(
"/cloudtentacles/asset",
createJsonHandler(
(req) =>
getAdminCloudtentaclesAsset(
req.body as AdminCloudtentaclesCatalogQueryRouteBody
), ),
{
successMessage: "cloudtentacles 余额查询成功",
errorMessage: "cloudtentacles 余额查询失败",
scope: "[admin/platform-config/cloudtentacles/asset]",
}
) )
);
router.post( router.post(
"/cloudtentacles/categories", '/cloudtentacles/asset',
createJsonHandler( createJsonHandler(
(req) => (req) => getAdminCloudtentaclesAsset(req.body as AdminCloudtentaclesCatalogQueryRouteBody),
getAdminCloudtentaclesCategories( {
req.body as AdminCloudtentaclesCatalogQueryRouteBody successMessage: 'cloudtentacles 余额查询成功',
errorMessage: 'cloudtentacles 余额查询失败',
scope: '[admin/platform-config/cloudtentacles/asset]',
},
), ),
{
successMessage: "cloudtentacles 分类查询成功",
errorMessage: "cloudtentacles 分类查询失败",
scope: "[admin/platform-config/cloudtentacles/categories]",
}
) )
);
router.post( router.post(
"/cloudtentacles/sku/list", '/cloudtentacles/categories',
createJsonHandler( createJsonHandler(
(req) => (req) => getAdminCloudtentaclesCategories(req.body as AdminCloudtentaclesCatalogQueryRouteBody),
getAdminCloudtentaclesSkuList( {
req.body as AdminCloudtentaclesCatalogQueryRouteBody successMessage: 'cloudtentacles 分类查询成功',
errorMessage: 'cloudtentacles 分类查询失败',
scope: '[admin/platform-config/cloudtentacles/categories]',
},
), ),
{
successMessage: "cloudtentacles SKU 列表查询成功",
errorMessage: "cloudtentacles SKU 列表查询失败",
scope: "[admin/platform-config/cloudtentacles/sku/list]",
}
) )
);
router.post( router.post(
"/cloudtentacles/sku/buy", '/cloudtentacles/sku/list',
createJsonHandler( createJsonHandler(
(req) => (req) => getAdminCloudtentaclesSkuList(req.body as AdminCloudtentaclesCatalogQueryRouteBody),
buyAdminCloudtentaclesSku(req.body as AdminCloudtentaclesSkuBuyRouteBody),
{ {
successMessage: "cloudtentacles SKU 购买成功", successMessage: 'cloudtentacles SKU 列表查询成功',
errorMessage: "cloudtentacles SKU 购买失败", errorMessage: 'cloudtentacles SKU 列表查询失败',
scope: "[admin/platform-config/cloudtentacles/sku/buy]", scope: '[admin/platform-config/cloudtentacles/sku/list]',
} },
)
);
router.post(
"/cloudtentacles/sku/use",
createJsonHandler(
(req) =>
useAdminCloudtentaclesSku(req.body as AdminCloudtentaclesSkuUseRouteBody),
{
successMessage: "cloudtentacles 发货成功",
errorMessage: "cloudtentacles 发货失败",
scope: "[admin/platform-config/cloudtentacles/sku/use]",
}
)
);
router.post(
"/cloudtentacles/knapsack",
createJsonHandler(
(req) =>
getAdminCloudtentaclesKnapsack(
req.body as AdminCloudtentaclesCatalogQueryRouteBody
), ),
{
successMessage: "cloudtentacles 背包查询成功",
errorMessage: "cloudtentacles 背包查询失败",
scope: "[admin/platform-config/cloudtentacles/knapsack]",
}
) )
);
router.post( router.post(
"/cloudtentacles/vn/list", '/cloudtentacles/sku/buy',
createJsonHandler(
(req) => buyAdminCloudtentaclesSku(req.body as AdminCloudtentaclesSkuBuyRouteBody),
{
successMessage: 'cloudtentacles SKU 购买成功',
errorMessage: 'cloudtentacles SKU 购买失败',
scope: '[admin/platform-config/cloudtentacles/sku/buy]',
},
),
)
router.post(
'/cloudtentacles/sku/use',
createJsonHandler(
(req) => useAdminCloudtentaclesSku(req.body as AdminCloudtentaclesSkuUseRouteBody),
{
successMessage: 'cloudtentacles 发货成功',
errorMessage: 'cloudtentacles 发货失败',
scope: '[admin/platform-config/cloudtentacles/sku/use]',
},
),
)
router.post(
'/cloudtentacles/knapsack',
createJsonHandler(
(req) => getAdminCloudtentaclesKnapsack(req.body as AdminCloudtentaclesCatalogQueryRouteBody),
{
successMessage: 'cloudtentacles 背包查询成功',
errorMessage: 'cloudtentacles 背包查询失败',
scope: '[admin/platform-config/cloudtentacles/knapsack]',
},
),
)
router.post(
'/cloudtentacles/vn/list',
createJsonHandler( createJsonHandler(
(req) => (req) =>
listAdminCloudtentaclesVirtualNumbers( listAdminCloudtentaclesVirtualNumbers(req.body as AdminCloudtentaclesVirtualNumberRouteBody),
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
{ {
successMessage: "cloudtentacles 虚拟号列表查询成功", successMessage: 'cloudtentacles 虚拟号列表查询成功',
errorMessage: "cloudtentacles 虚拟号列表查询失败", errorMessage: 'cloudtentacles 虚拟号列表查询失败',
scope: "[admin/platform-config/cloudtentacles/vn/list]", scope: '[admin/platform-config/cloudtentacles/vn/list]',
} },
),
) )
);
router.post( router.post(
"/cloudtentacles/vn/appoint", '/cloudtentacles/vn/appoint',
createJsonHandler( createJsonHandler(
(req) => (req) =>
appointAdminCloudtentaclesVirtualNumber( appointAdminCloudtentaclesVirtualNumber(
req.body as AdminCloudtentaclesVirtualNumberRouteBody req.body as AdminCloudtentaclesVirtualNumberRouteBody,
), ),
{ {
successMessage: "cloudtentacles 虚拟号申请成功", successMessage: 'cloudtentacles 虚拟号申请成功',
errorMessage: "cloudtentacles 虚拟号申请失败", errorMessage: 'cloudtentacles 虚拟号申请失败',
scope: "[admin/platform-config/cloudtentacles/vn/appoint]", scope: '[admin/platform-config/cloudtentacles/vn/appoint]',
} },
),
) )
);
router.post( router.post(
"/cloudtentacles/vn/generate-login-code", '/cloudtentacles/vn/generate-login-code',
createJsonHandler( createJsonHandler(
(req) => (req) =>
generateAdminCloudtentaclesLoginCode( generateAdminCloudtentaclesLoginCode(req.body as AdminCloudtentaclesVirtualNumberRouteBody),
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
{ {
successMessage: "cloudtentacles 登录码生成成功", successMessage: 'cloudtentacles 登录码生成成功',
errorMessage: "cloudtentacles 登录码生成失败", errorMessage: 'cloudtentacles 登录码生成失败',
scope: "[admin/platform-config/cloudtentacles/vn/generate-login-code]", scope: '[admin/platform-config/cloudtentacles/vn/generate-login-code]',
} },
),
) )
);
router.post( router.post(
"/cloudtentacles/vn/fetch-code", '/cloudtentacles/vn/fetch-code',
createJsonHandler( createJsonHandler(
(req) => (req) =>
fetchAdminCloudtentaclesVirtualNumberCode( fetchAdminCloudtentaclesVirtualNumberCode(
req.body as AdminCloudtentaclesVirtualNumberRouteBody req.body as AdminCloudtentaclesVirtualNumberRouteBody,
), ),
{ {
successMessage: "cloudtentacles 验证码获取成功", successMessage: 'cloudtentacles 验证码获取成功',
errorMessage: "cloudtentacles 验证码获取失败", errorMessage: 'cloudtentacles 验证码获取失败',
scope: "[admin/platform-config/cloudtentacles/vn/fetch-code]", scope: '[admin/platform-config/cloudtentacles/vn/fetch-code]',
} },
),
) )
);
router.post( router.post(
"/cloudtentacles/vn/verify-code", '/cloudtentacles/vn/verify-code',
createJsonHandler( createJsonHandler(
(req) => (req) =>
verifyAdminCloudtentaclesLoginCode( verifyAdminCloudtentaclesLoginCode(req.body as AdminCloudtentaclesVirtualNumberRouteBody),
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
{ {
successMessage: "cloudtentacles 登录码校验成功", successMessage: 'cloudtentacles 登录码校验成功',
errorMessage: "cloudtentacles 登录码校验失败", errorMessage: 'cloudtentacles 登录码校验失败',
scope: "[admin/platform-config/cloudtentacles/vn/verify-code]", scope: '[admin/platform-config/cloudtentacles/vn/verify-code]',
} },
),
) )
);
router.post( router.post(
"/cloudtentacles/vn/bind-url", '/cloudtentacles/vn/bind-url',
createJsonHandler( createJsonHandler(
(req) => (req) => getAdminCloudtentaclesBindUrl(req.body as AdminCloudtentaclesVirtualNumberRouteBody),
getAdminCloudtentaclesBindUrl(
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
{ {
successMessage: "cloudtentacles 兑换链接获取成功", successMessage: 'cloudtentacles 兑换链接获取成功',
errorMessage: "cloudtentacles 兑换链接获取失败", errorMessage: 'cloudtentacles 兑换链接获取失败',
scope: "[admin/platform-config/cloudtentacles/vn/bind-url]", scope: '[admin/platform-config/cloudtentacles/vn/bind-url]',
} },
),
) )
);
router.post( router.post(
"/cloudtentacles/vn/back", '/cloudtentacles/vn/back',
createJsonHandler( createJsonHandler(
(req) => (req) =>
backAdminCloudtentaclesVirtualNumber( backAdminCloudtentaclesVirtualNumber(req.body as AdminCloudtentaclesVirtualNumberRouteBody),
req.body as AdminCloudtentaclesVirtualNumberRouteBody
),
{ {
successMessage: "cloudtentacles 号码退还成功", successMessage: 'cloudtentacles 号码退还成功',
errorMessage: "cloudtentacles 号码退还失败", errorMessage: 'cloudtentacles 号码退还失败',
scope: "[admin/platform-config/cloudtentacles/vn/back]", scope: '[admin/platform-config/cloudtentacles/vn/back]',
} },
),
) )
);
router.post( router.post(
"/cloudtentacles/debug/full-flow", '/cloudtentacles/debug/full-flow',
createJsonHandler( createJsonHandler(
(req) => (req) => runAdminCloudtentaclesFullFlow(req.body as AdminCloudtentaclesFullFlowRouteBody),
runAdminCloudtentaclesFullFlow(
req.body as AdminCloudtentaclesFullFlowRouteBody
),
{ {
successMessage: "cloudtentacles 完整调试流程执行成功", successMessage: 'cloudtentacles 完整调试流程执行成功',
errorMessage: "cloudtentacles 完整调试流程执行失败", errorMessage: 'cloudtentacles 完整调试流程执行失败',
scope: "[admin/platform-config/cloudtentacles/debug/full-flow]", scope: '[admin/platform-config/cloudtentacles/debug/full-flow]',
} },
),
) )
);
export default router; export default router
@@ -21,9 +21,7 @@ router.get(
router.post( router.post(
'/fulfillment-routing', '/fulfillment-routing',
createJsonHandler( createJsonHandler((req) => updateAdminFulfillmentRoutingConfig(req.body as JsonRecord), {
(req) => updateAdminFulfillmentRoutingConfig(req.body as JsonRecord),
{
successMessage: '履约路由配置已保存', successMessage: '履约路由配置已保存',
errorMessage: '保存履约路由配置失败', errorMessage: '保存履约路由配置失败',
scope: '[admin/platform-config/fulfillment-routing]', scope: '[admin/platform-config/fulfillment-routing]',
@@ -40,20 +38,16 @@ router.post(
}, },
} }
}, },
}, }),
),
) )
router.post( router.post(
'/fulfillment-routing/preview', '/fulfillment-routing/preview',
createJsonHandler( createJsonHandler((req) => previewAdminFulfillmentRouting(req.body as JsonRecord), {
(req) => previewAdminFulfillmentRouting(req.body as JsonRecord),
{
successMessage: 'ok', successMessage: 'ok',
errorMessage: '预览履约路由失败', errorMessage: '预览履约路由失败',
scope: '[admin/platform-config/fulfillment-routing/preview]', scope: '[admin/platform-config/fulfillment-routing/preview]',
}, }),
),
) )
export default router export default router
@@ -1,4 +1,4 @@
import { Router } from "express"; import { Router } from 'express'
import { import {
createAdminKuaishouFeifeiTestOrder, createAdminKuaishouFeifeiTestOrder,
@@ -8,137 +8,117 @@ import {
queryAdminKuaishouFeifeiTestOrder, queryAdminKuaishouFeifeiTestOrder,
syncAdminKuaishouFeifeiProductRules, syncAdminKuaishouFeifeiProductRules,
updateAdminKuaishouFeifeiConfig, updateAdminKuaishouFeifeiConfig,
} from "../../../services/admin/platform-config/kuaishou-feifei-service.js"; } from '../../../services/admin/platform-config/kuaishou-feifei-service.js'
import { createJsonHandler } from "../session.js"; import { createJsonHandler } from '../session.js'
import type { JsonRecord } from "../../../types/json.js"; import type { JsonRecord } from '../../../types/json.js'
const router = Router(); const router = Router()
router.get( router.get(
"/kuaishou-feifei", '/kuaishou-feifei',
createJsonHandler(() => getAdminKuaishouFeifeiConfig(), { createJsonHandler(() => getAdminKuaishouFeifeiConfig(), {
successMessage: "ok", successMessage: 'ok',
errorMessage: "读取 kuaishou-feifei 配置失败", errorMessage: '读取 kuaishou-feifei 配置失败',
scope: "[admin/platform-config/kuaishou-feifei]", scope: '[admin/platform-config/kuaishou-feifei]',
}) }),
); )
router.post( router.post(
"/kuaishou-feifei", '/kuaishou-feifei',
createJsonHandler( createJsonHandler((req) => updateAdminKuaishouFeifeiConfig(req.body as JsonRecord), {
(req) => updateAdminKuaishouFeifeiConfig(req.body as JsonRecord), successMessage: 'kuaishou-feifei 配置已保存',
{ errorMessage: '保存 kuaishou-feifei 配置失败',
successMessage: "kuaishou-feifei 配置已保存", scope: '[admin/platform-config/kuaishou-feifei]',
errorMessage: "保存 kuaishou-feifei 配置失败",
scope: "[admin/platform-config/kuaishou-feifei]",
audit: (_req, data) => { audit: (_req, data) => {
const result = data as JsonRecord; const result = data as JsonRecord
const source = result.source as JsonRecord | undefined; const source = result.source as JsonRecord | undefined
return { return {
action: "platform_kuaishou_feifei_config_updated", action: 'platform_kuaishou_feifei_config_updated',
targetType: "platform_config", targetType: 'platform_config',
targetId: "kuaishou_feifei", targetId: 'kuaishou_feifei',
data: { data: {
filePath: String(result.filePath || "").trim(), filePath: String(result.filePath || '').trim(),
enabled: source?.enabled !== false, enabled: source?.enabled !== false,
ruleCount: Array.isArray(source?.productRules) ruleCount: Array.isArray(source?.productRules) ? source.productRules.length : 0,
? source.productRules.length
: 0,
},
};
}, },
} }
},
}),
) )
);
router.post( router.post(
"/kuaishou-feifei/match", '/kuaishou-feifei/match',
createJsonHandler( createJsonHandler((req) => matchAdminKuaishouFeifeiProduct(req.body as JsonRecord), {
(req) => matchAdminKuaishouFeifeiProduct(req.body as JsonRecord), successMessage: 'ok',
{ errorMessage: '匹配 kuaishou-feifei 商品失败',
successMessage: "ok", scope: '[admin/platform-config/kuaishou-feifei/match]',
errorMessage: "匹配 kuaishou-feifei 商品失败", }),
scope: "[admin/platform-config/kuaishou-feifei/match]",
}
) )
);
router.post( router.post(
"/kuaishou-feifei/products", '/kuaishou-feifei/products',
createJsonHandler( createJsonHandler((req) => listAdminKuaishouFeifeiProducts(req.body as JsonRecord), {
(req) => listAdminKuaishouFeifeiProducts(req.body as JsonRecord), successMessage: 'ok',
{ errorMessage: '查询 kuaishou-feifei 商品失败',
successMessage: "ok", scope: '[admin/platform-config/kuaishou-feifei/products]',
errorMessage: "查询 kuaishou-feifei 商品失败", }),
scope: "[admin/platform-config/kuaishou-feifei/products]",
}
) )
);
router.post( router.post(
"/kuaishou-feifei/sync-products", '/kuaishou-feifei/sync-products',
createJsonHandler( createJsonHandler((req) => syncAdminKuaishouFeifeiProductRules(req.body as JsonRecord), {
(req) => syncAdminKuaishouFeifeiProductRules(req.body as JsonRecord), successMessage: 'kuaishou-feifei 商品映射已同步',
{ errorMessage: '同步 kuaishou-feifei 商品映射失败',
successMessage: "kuaishou-feifei 商品映射已同步", scope: '[admin/platform-config/kuaishou-feifei/sync-products]',
errorMessage: "同步 kuaishou-feifei 商品映射失败",
scope: "[admin/platform-config/kuaishou-feifei/sync-products]",
audit: (_req, data) => { audit: (_req, data) => {
const result = data as JsonRecord; const result = data as JsonRecord
const sync = result.sync as JsonRecord | undefined; const sync = result.sync as JsonRecord | undefined
return { return {
action: "platform_kuaishou_feifei_products_synced", action: 'platform_kuaishou_feifei_products_synced',
targetType: "platform_config", targetType: 'platform_config',
targetId: "kuaishou_feifei", targetId: 'kuaishou_feifei',
data: { data: {
productCount: Number(sync?.productCount || 0) || 0, productCount: Number(sync?.productCount || 0) || 0,
ruleCount: Number(sync?.ruleCount || 0) || 0, ruleCount: Number(sync?.ruleCount || 0) || 0,
status: String(sync?.status || "").trim(), status: String(sync?.status || '').trim(),
},
};
}, },
} }
},
}),
) )
);
router.post( router.post(
"/kuaishou-feifei/test-order", '/kuaishou-feifei/test-order',
createJsonHandler( createJsonHandler((req) => createAdminKuaishouFeifeiTestOrder(req.body as JsonRecord), {
(req) => createAdminKuaishouFeifeiTestOrder(req.body as JsonRecord), successMessage: 'kuaishou-feifei 测试单已创建',
{ errorMessage: '创建 kuaishou-feifei 测试单失败',
successMessage: "kuaishou-feifei 测试单已创建", scope: '[admin/platform-config/kuaishou-feifei/test-order]',
errorMessage: "创建 kuaishou-feifei 测试单失败",
scope: "[admin/platform-config/kuaishou-feifei/test-order]",
audit: (_req, data) => { audit: (_req, data) => {
const result = data as JsonRecord; const result = data as JsonRecord
return { return {
action: "platform_kuaishou_feifei_test_order_created", action: 'platform_kuaishou_feifei_test_order_created',
targetType: "platform_config", targetType: 'platform_config',
targetId: "kuaishou_feifei", targetId: 'kuaishou_feifei',
data: { data: {
orderNo: String(result.orderNo || "").trim(), orderNo: String(result.orderNo || '').trim(),
platformOrderNo: String(result.platformOrderNo || "").trim(), platformOrderNo: String(result.platformOrderNo || '').trim(),
productCode: String(result.productCode || "").trim(), productCode: String(result.productCode || '').trim(),
rechargeStatus: Number(result.rechargeStatus || 0) || 0, rechargeStatus: Number(result.rechargeStatus || 0) || 0,
}, },
};
},
} }
},
}),
) )
);
router.post( router.post(
"/kuaishou-feifei/query-order", '/kuaishou-feifei/query-order',
createJsonHandler( createJsonHandler((req) => queryAdminKuaishouFeifeiTestOrder(req.body as JsonRecord), {
(req) => queryAdminKuaishouFeifeiTestOrder(req.body as JsonRecord), successMessage: 'ok',
{ errorMessage: '查询 kuaishou-feifei 订单失败',
successMessage: "ok", scope: '[admin/platform-config/kuaishou-feifei/query-order]',
errorMessage: "查询 kuaishou-feifei 订单失败", }),
scope: "[admin/platform-config/kuaishou-feifei/query-order]",
}
) )
);
export default router; export default router
@@ -1,77 +1,75 @@
import { Router } from "express"; import { Router } from 'express'
import { import {
failAdminNinetyoneOrder, failAdminNinetyoneOrder,
getAdminNinetyoneOrders, getAdminNinetyoneOrders,
retryAdminNinetyoneOrder, retryAdminNinetyoneOrder,
} from "../../../services/admin/platform-config/ninetyone-service.js"; } from '../../../services/admin/platform-config/ninetyone-service.js'
import type { AdminEntityRouteParams } from "../../../types/admin/route-inputs.js"; import type { AdminEntityRouteParams } from '../../../types/admin/route-inputs.js'
import { createJsonHandler } from "../session.js"; import { createJsonHandler } from '../session.js'
import type { JsonRecord } from "../../../types/json.js"; import type { JsonRecord } from '../../../types/json.js'
const router = Router(); const router = Router()
router.get( router.get(
"/ninetyone/orders", '/ninetyone/orders',
createJsonHandler((req) => getAdminNinetyoneOrders(req.query), { createJsonHandler((req) => getAdminNinetyoneOrders(req.query), {
successMessage: "ok", successMessage: 'ok',
errorMessage: "读取 91卡券订单失败", errorMessage: '读取 91卡券订单失败',
scope: "[admin/platform-config/ninetyone/orders]", scope: '[admin/platform-config/ninetyone/orders]',
}) }),
); )
router.post( router.post(
"/ninetyone/orders/:id/retry", '/ninetyone/orders/:id/retry',
createJsonHandler( createJsonHandler(
(req) => retryAdminNinetyoneOrder(String((req.params as AdminEntityRouteParams).id || "")), (req) => retryAdminNinetyoneOrder(String((req.params as AdminEntityRouteParams).id || '')),
{ {
successMessage: "91卡券订单已重试", successMessage: '91卡券订单已重试',
errorMessage: "重试 91卡券订单失败", errorMessage: '重试 91卡券订单失败',
scope: "[admin/platform-config/ninetyone/orders/:id/retry]", scope: '[admin/platform-config/ninetyone/orders/:id/retry]',
audit: (_req, data) => { audit: (_req, data) => {
const result = data as JsonRecord; const result = data as JsonRecord
return { return {
action: "platform_ninetyone_order_retried", action: 'platform_ninetyone_order_retried',
targetType: "order", targetType: 'order',
targetId: String(result.orderId || "").trim(), targetId: String(result.orderId || '').trim(),
data: { data: {
orderNo: String(result.orderNo || "").trim(), orderNo: String(result.orderNo || '').trim(),
taskCount: Number(result.taskCount || 0), taskCount: Number(result.taskCount || 0),
}, },
};
},
} }
},
},
),
) )
);
router.post( router.post(
"/ninetyone/orders/:id/fail", '/ninetyone/orders/:id/fail',
createJsonHandler( createJsonHandler(
(req) => (req) =>
failAdminNinetyoneOrder( failAdminNinetyoneOrder(
String((req.params as AdminEntityRouteParams).id || ""), String((req.params as AdminEntityRouteParams).id || ''),
req.body as { reason?: string } req.body as { reason?: string },
), ),
{ {
successMessage: "91卡券订单已标记失败", successMessage: '91卡券订单已标记失败',
errorMessage: "标记 91卡券订单失败", errorMessage: '标记 91卡券订单失败',
scope: "[admin/platform-config/ninetyone/orders/:id/fail]", scope: '[admin/platform-config/ninetyone/orders/:id/fail]',
audit: (req, data) => { audit: (req, data) => {
const result = data as JsonRecord; const result = data as JsonRecord
return { return {
action: "platform_ninetyone_order_failed", action: 'platform_ninetyone_order_failed',
targetType: "order", targetType: 'order',
targetId: String(result.orderId || "").trim(), targetId: String(result.orderId || '').trim(),
data: { data: {
orderNo: String(result.orderNo || "").trim(), orderNo: String(result.orderNo || '').trim(),
reason: String( reason: String((req.body as { reason?: string }).reason || '').trim(),
(req.body as { reason?: string }).reason || ""
).trim(),
},
};
}, },
} }
},
},
),
) )
);
export default router; export default router
@@ -1,4 +1,4 @@
import { Router } from "express"; import { Router } from 'express'
import { import {
getAdminNotificationConfig, getAdminNotificationConfig,
@@ -7,152 +7,132 @@ import {
testAdminNotification, testAdminNotification,
updateAdminNotificationConfig, updateAdminNotificationConfig,
updateAdminScheduledJobsConfig, updateAdminScheduledJobsConfig,
} from "../../../services/admin/platform-config/notification-service.js"; } from '../../../services/admin/platform-config/notification-service.js'
import type { import type {
AdminEntityRouteParams, AdminEntityRouteParams,
AdminNotificationConfigRouteBody, AdminNotificationConfigRouteBody,
AdminNotificationTestRouteBody, AdminNotificationTestRouteBody,
AdminScheduledJobsConfigRouteBody, AdminScheduledJobsConfigRouteBody,
} from "../../../types/admin/route-inputs.js"; } from '../../../types/admin/route-inputs.js'
import { createJsonHandler } from "../session.js"; import { createJsonHandler } from '../session.js'
import type { JsonRecord } from "../../../types/json.js"; import type { JsonRecord } from '../../../types/json.js'
const router = Router(); const router = Router()
router.get( router.get(
"/notifications", '/notifications',
createJsonHandler(() => getAdminNotificationConfig(), { createJsonHandler(() => getAdminNotificationConfig(), {
successMessage: "ok", successMessage: 'ok',
errorMessage: "读取内部通知配置失败", errorMessage: '读取内部通知配置失败',
scope: "[admin/platform-config/notifications]", scope: '[admin/platform-config/notifications]',
}) }),
); )
router.post( router.post(
"/notifications", '/notifications',
createJsonHandler( createJsonHandler(
(req) => (req) => updateAdminNotificationConfig(req.body as AdminNotificationConfigRouteBody),
updateAdminNotificationConfig(
req.body as AdminNotificationConfigRouteBody
),
{ {
successMessage: "内部通知配置已保存", successMessage: '内部通知配置已保存',
errorMessage: "保存内部通知配置失败", errorMessage: '保存内部通知配置失败',
scope: "[admin/platform-config/notifications]", scope: '[admin/platform-config/notifications]',
audit: (_req, data) => { audit: (_req, data) => {
const result = data as JsonRecord; const result = data as JsonRecord
return { return {
action: "platform_notification_config_updated", action: 'platform_notification_config_updated',
targetType: "platform_config", targetType: 'platform_config',
targetId: "notifications", targetId: 'notifications',
data: { data: {
filePath: String(result.filePath || "").trim(), filePath: String(result.filePath || '').trim(),
enabled: Boolean(result.source?.enabled), enabled: Boolean(result.source?.enabled),
barkRecipientCount: Array.isArray( barkRecipientCount: Array.isArray(result.source?.channels?.bark?.recipients)
result.source?.channels?.bark?.recipients
)
? result.source.channels.bark.recipients.length ? result.source.channels.bark.recipients.length
: 0, : 0,
wpushRecipientCount: Array.isArray( wpushRecipientCount: Array.isArray(result.source?.channels?.wpush?.recipients)
result.source?.channels?.wpush?.recipients
)
? result.source.channels.wpush.recipients.length ? result.source.channels.wpush.recipients.length
: 0, : 0,
}, },
};
},
} }
},
},
),
) )
);
router.post( router.post(
"/notifications/test", '/notifications/test',
createJsonHandler( createJsonHandler((req) => testAdminNotification(req.body as AdminNotificationTestRouteBody), {
(req) => testAdminNotification(req.body as AdminNotificationTestRouteBody), successMessage: '内部通知测试已执行',
{ errorMessage: '内部通知测试失败',
successMessage: "内部通知测试已执行", scope: '[admin/platform-config/notifications/test]',
errorMessage: "内部通知测试失败",
scope: "[admin/platform-config/notifications/test]",
audit: (_req, data) => { audit: (_req, data) => {
const result = data as JsonRecord; const result = data as JsonRecord
return { return {
action: "platform_notification_test_sent", action: 'platform_notification_test_sent',
targetType: "platform_config", targetType: 'platform_config',
targetId: "notifications", targetId: 'notifications',
data: { data: {
channel: String(result.channel || "").trim(), channel: String(result.channel || '').trim(),
successCount: Number(result.successCount || 0), successCount: Number(result.successCount || 0),
failedCount: Number(result.failedCount || 0), failedCount: Number(result.failedCount || 0),
}, },
};
},
} }
},
}),
) )
);
router.get( router.get(
"/scheduled-jobs", '/scheduled-jobs',
createJsonHandler(() => getAdminScheduledJobsConfig(), { createJsonHandler(() => getAdminScheduledJobsConfig(), {
successMessage: "ok", successMessage: 'ok',
errorMessage: "读取定时任务配置失败", errorMessage: '读取定时任务配置失败',
scope: "[admin/platform-config/scheduled-jobs]", scope: '[admin/platform-config/scheduled-jobs]',
}) }),
); )
router.post( router.post(
"/scheduled-jobs", '/scheduled-jobs',
createJsonHandler( createJsonHandler(
(req) => (req) => updateAdminScheduledJobsConfig(req.body as AdminScheduledJobsConfigRouteBody),
updateAdminScheduledJobsConfig(
req.body as AdminScheduledJobsConfigRouteBody
),
{ {
successMessage: "定时任务配置已保存", successMessage: '定时任务配置已保存',
errorMessage: "保存定时任务配置失败", errorMessage: '保存定时任务配置失败',
scope: "[admin/platform-config/scheduled-jobs]", scope: '[admin/platform-config/scheduled-jobs]',
audit: (_req, data) => { audit: (_req, data) => {
const result = data as JsonRecord; const result = data as JsonRecord
return { return {
action: "platform_scheduled_jobs_updated", action: 'platform_scheduled_jobs_updated',
targetType: "platform_config", targetType: 'platform_config',
targetId: "scheduled_jobs", targetId: 'scheduled_jobs',
data: { data: {
filePath: String(result.filePath || "").trim(), filePath: String(result.filePath || '').trim(),
enabled: Boolean(result.source?.enabled), enabled: Boolean(result.source?.enabled),
jobCount: Array.isArray(result.source?.jobs) jobCount: Array.isArray(result.source?.jobs) ? result.source.jobs.length : 0,
? result.source.jobs.length
: 0,
},
};
}, },
} }
},
},
),
) )
);
router.post( router.post(
"/scheduled-jobs/:id/run", '/scheduled-jobs/:id/run',
createJsonHandler( createJsonHandler((req) => runAdminScheduledJobNow((req.params as AdminEntityRouteParams).id), {
(req) => runAdminScheduledJobNow((req.params as AdminEntityRouteParams).id), successMessage: '定时任务已执行',
{ errorMessage: '执行定时任务失败',
successMessage: "定时任务已执行", scope: '[admin/platform-config/scheduled-jobs/:id/run]',
errorMessage: "执行定时任务失败",
scope: "[admin/platform-config/scheduled-jobs/:id/run]",
audit: (req, data) => { audit: (req, data) => {
const result = data as JsonRecord; const result = data as JsonRecord
return { return {
action: "platform_scheduled_job_run", action: 'platform_scheduled_job_run',
targetType: "platform_config", targetType: 'platform_config',
targetId: String( targetId: String((req.params as AdminEntityRouteParams).id || '').trim(),
(req.params as AdminEntityRouteParams).id || ""
).trim(),
data: { data: {
status: String(result.result?.status || "").trim(), status: String(result.result?.status || '').trim(),
message: String(result.result?.message || "").trim(), message: String(result.result?.message || '').trim(),
},
};
}, },
} }
},
}),
) )
);
export default router; export default router
+9 -2
View File
@@ -1,5 +1,8 @@
import type { Request, Response, NextFunction } from 'express' import type { Request, Response, NextFunction } from 'express'
import { requireAdminRole, verifyAdminSessionToken } from '../../services/admin/admin-auth-service.js' import {
requireAdminRole,
verifyAdminSessionToken,
} from '../../services/admin/admin-auth-service.js'
import { writeAdminAuditLog } from '../../services/admin/admin-audit-service.js' import { writeAdminAuditLog } from '../../services/admin/admin-audit-service.js'
import { buildSuccessPayload, createHttpError, sendRouteError } from '../../utils/http.js' import { buildSuccessPayload, createHttpError, sendRouteError } from '../../utils/http.js'
import { logWarn } from '../../utils/logger.js' import { logWarn } from '../../utils/logger.js'
@@ -53,7 +56,11 @@ export function createFileHandler(
} }
} }
export async function requireAdminSession(req: Request, res: Response, next: NextFunction): Promise<void> { export async function requireAdminSession(
req: Request,
res: Response,
next: NextFunction,
): Promise<void> {
try { try {
req.adminSession = await verifyAdminSessionToken(extractBearerToken(req)) req.adminSession = await verifyAdminSessionToken(extractBearerToken(req))
next() next()
+2 -7
View File
@@ -105,11 +105,7 @@ router.post(
'/tasks/:taskId/kuaishou-cloud/dispatch', '/tasks/:taskId/kuaishou-cloud/dispatch',
requireAdminRoles(['admin', 'operator']), requireAdminRoles(['admin', 'operator']),
createJsonHandler( createJsonHandler(
(req) => (req) => dispatchAdminTaskKuaishouCloudFulfillment(getTaskId(req), req.adminSession || null),
dispatchAdminTaskKuaishouCloudFulfillment(
getTaskId(req),
req.adminSession || null,
),
{ {
successMessage: '已完成绑定确认并发货', successMessage: '已完成绑定确认并发货',
errorMessage: '执行发货失败', errorMessage: '执行发货失败',
@@ -152,8 +148,7 @@ router.post(
'/tasks/:taskId/kuaishou-industry/resend-code', '/tasks/:taskId/kuaishou-industry/resend-code',
requireAdminRoles(['admin', 'operator', 'support']), requireAdminRoles(['admin', 'operator', 'support']),
createJsonHandler( createJsonHandler(
(req) => (req) => resendAdminTaskKuaishouIndustryVoucherCode(getTaskId(req), req.adminSession || null),
resendAdminTaskKuaishouIndustryVoucherCode(getTaskId(req), req.adminSession || null),
{ {
successMessage: '电子凭证发码回调已重发', successMessage: '电子凭证发码回调已重发',
errorMessage: '重发电子凭证发码回调失败', errorMessage: '重发电子凭证发码回调失败',
+37 -18
View File
@@ -22,18 +22,18 @@ const router = Router()
router.use('/users', requireAdminRoles(['admin'])) router.use('/users', requireAdminRoles(['admin']))
router.get('/users', createJsonHandler( router.get(
(req) => getAdminUserList(req.query), '/users',
{ createJsonHandler((req) => getAdminUserList(req.query), {
successMessage: 'ok', successMessage: 'ok',
errorMessage: '读取后台用户列表失败', errorMessage: '读取后台用户列表失败',
scope: '[admin/users]', scope: '[admin/users]',
}, }),
)) )
router.post('/users', createJsonHandler( router.post(
(req) => createManagedAdminUser(req.body), '/users',
{ createJsonHandler((req) => createManagedAdminUser(req.body), {
successMessage: '后台用户已创建', successMessage: '后台用户已创建',
errorMessage: '创建后台用户失败', errorMessage: '创建后台用户失败',
scope: '[admin/users:create]', scope: '[admin/users:create]',
@@ -50,11 +50,18 @@ router.post('/users', createJsonHandler(
}, },
} }
}, },
}, }),
)) )
router.post('/users/:userId/role', createJsonHandler( router.post(
(req) => updateManagedAdminUserRole(String(req.params.userId || ''), req.body, getRequiredAdminSession(req)), '/users/:userId/role',
createJsonHandler(
(req) =>
updateManagedAdminUserRole(
String(req.params.userId || ''),
req.body,
getRequiredAdminSession(req),
),
{ {
successMessage: '用户角色已更新', successMessage: '用户角色已更新',
errorMessage: '更新用户角色失败', errorMessage: '更新用户角色失败',
@@ -72,10 +79,18 @@ router.post('/users/:userId/role', createJsonHandler(
} }
}, },
}, },
)) ),
)
router.post('/users/:userId/status', createJsonHandler( router.post(
(req) => updateManagedAdminUserStatus(String(req.params.userId || ''), req.body, getRequiredAdminSession(req)), '/users/:userId/status',
createJsonHandler(
(req) =>
updateManagedAdminUserStatus(
String(req.params.userId || ''),
req.body,
getRequiredAdminSession(req),
),
{ {
successMessage: '用户状态已更新', successMessage: '用户状态已更新',
errorMessage: '更新用户状态失败', errorMessage: '更新用户状态失败',
@@ -93,9 +108,12 @@ router.post('/users/:userId/status', createJsonHandler(
} }
}, },
}, },
)) ),
)
router.post('/users/:userId/reset-password', createJsonHandler( router.post(
'/users/:userId/reset-password',
createJsonHandler(
(req) => resetManagedAdminUserPassword(String(req.params.userId || ''), req.body), (req) => resetManagedAdminUserPassword(String(req.params.userId || ''), req.body),
{ {
successMessage: '用户密码已重置', successMessage: '用户密码已重置',
@@ -113,6 +131,7 @@ router.post('/users/:userId/reset-password', createJsonHandler(
} }
}, },
}, },
)) ),
)
export default router export default router
@@ -266,8 +266,7 @@ router.post(
'/worker-platform/finance-requests/:requestId/review', '/worker-platform/finance-requests/:requestId/review',
requireAdminRoles(['admin', 'operator']), requireAdminRoles(['admin', 'operator']),
createJsonHandler( createJsonHandler(
(req) => (req) => reviewAdminWorkerFinanceRequest(String(req.params.requestId || ''), req.body || {}),
reviewAdminWorkerFinanceRequest(String(req.params.requestId || ''), req.body || {}),
{ {
successMessage: '资金申请已处理', successMessage: '资金申请已处理',
errorMessage: '处理资金申请失败', errorMessage: '处理资金申请失败',
@@ -387,7 +386,9 @@ router.post(
router.post( router.post(
'/worker-platform/orders/:workOrderId/cancel', '/worker-platform/orders/:workOrderId/cancel',
requireAdminRoles(['admin', 'operator', 'support']), requireAdminRoles(['admin', 'operator', 'support']),
createJsonHandler((req) => cancelAdminWorkOrder(String(req.params.workOrderId || ''), req.body || {}), { createJsonHandler(
(req) => cancelAdminWorkOrder(String(req.params.workOrderId || ''), req.body || {}),
{
successMessage: '撤单处理完成,打手额度已恢复', successMessage: '撤单处理完成,打手额度已恢复',
errorMessage: '撤单失败', errorMessage: '撤单失败',
scope: '[admin/worker-platform/orders/:workOrderId/cancel]', scope: '[admin/worker-platform/orders/:workOrderId/cancel]',
@@ -400,7 +401,8 @@ router.post(
targetId: String(req.params.workOrderId || ''), targetId: String(req.params.workOrderId || ''),
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {}, data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
}), }),
}), },
),
) )
router.delete( router.delete(
@@ -576,10 +578,7 @@ router.post(
requireAdminRoles(['admin', 'operator']), requireAdminRoles(['admin', 'operator']),
createJsonHandler( createJsonHandler(
(req) => (req) =>
deductAdminWorkOrderPendingDeposit( deductAdminWorkOrderPendingDeposit(String(req.params.workOrderId || ''), req.body || {}),
String(req.params.workOrderId || ''),
req.body || {},
),
{ {
successMessage: '待解冻押金已扣减', successMessage: '待解冻押金已扣减',
errorMessage: '扣减待解冻押金失败', errorMessage: '扣减待解冻押金失败',
+7 -2
View File
@@ -44,11 +44,16 @@ router.post('/', notifyRateLimit, async (req, res) => {
}) })
res.status(200).json({ code: 0 }) res.status(200).json({ code: 0 })
} catch (error) { } catch (error) {
logIntegration('[affiliate-dash/notify]', '订单回调处理失败', { logIntegration(
'[affiliate-dash/notify]',
'订单回调处理失败',
{
requestId, requestId,
durationMs: Date.now() - startedAt, durationMs: Date.now() - startedAt,
error, error,
}, { level: 'error' }) },
{ level: 'error' },
)
sendRouteError(res, error, 'affiliate-dash 回调处理失败', '[affiliate-dash/notify]') sendRouteError(res, error, 'affiliate-dash 回调处理失败', '[affiliate-dash/notify]')
} }
}) })
+6 -3
View File
@@ -42,18 +42,21 @@ router.post(
'/upload', '/upload',
collectRateLimit, collectRateLimit,
uploadSingleFile, uploadSingleFile,
createRouteHandler((req) => { createRouteHandler(
(req) => {
return uploadFileAsset({ return uploadFileAsset({
file: req.file, file: req.file,
scene: req.body?.scene || 'collect-material', scene: req.body?.scene || 'collect-material',
uploaderType: 'collect', uploaderType: 'collect',
uploaderId: String(req.body?.orderNo || 'anonymous'), uploaderId: String(req.body?.orderNo || 'anonymous'),
}) })
}, { },
{
successMessage: '文件已上传', successMessage: '文件已上传',
errorMessage: '文件上传失败', errorMessage: '文件上传失败',
scope: '[collect/upload]', scope: '[collect/upload]',
}), },
),
) )
router.use((req, res) => { router.use((req, res) => {
+7 -2
View File
@@ -45,11 +45,16 @@ router.post('/notify', notifyRateLimit, async (req, res) => {
}) })
res.status(200).json({ code: 0 }) res.status(200).json({ code: 0 })
} catch (error) { } catch (error) {
logIntegration('[kuaishou-feifei/notify]', '订单通知处理失败', { logIntegration(
'[kuaishou-feifei/notify]',
'订单通知处理失败',
{
requestId, requestId,
durationMs: Date.now() - startedAt, durationMs: Date.now() - startedAt,
error, error,
}, { level: 'error' }) },
{ level: 'error' },
)
sendRouteError(res, error, 'kuaishou-feifei 通知处理失败', '[kuaishou-feifei/notify]') sendRouteError(res, error, 'kuaishou-feifei 通知处理失败', '[kuaishou-feifei/notify]')
} }
}) })
+28 -8
View File
@@ -45,11 +45,16 @@ router.post('/send-code', industryRateLimit, async (req, res) => {
res.status(200).json(result) res.status(200).json(result)
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : '系统异常' const message = error instanceof Error ? error.message : '系统异常'
logIntegration('[kuaishou-industry/send-code]', '发码处理失败', { logIntegration(
'[kuaishou-industry/send-code]',
'发码处理失败',
{
requestId, requestId,
durationMs: Date.now() - startedAt, durationMs: Date.now() - startedAt,
error, error,
}, { level: 'error' }) },
{ level: 'error' },
)
res.status(200).json(buildIndustryErrorResponse(4010003, message)) res.status(200).json(buildIndustryErrorResponse(4010003, message))
} }
}) })
@@ -78,11 +83,16 @@ router.post('/destroy-code', industryRateLimit, async (req, res) => {
res.status(200).json(result) res.status(200).json(result)
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : '系统异常' const message = error instanceof Error ? error.message : '系统异常'
logIntegration('[kuaishou-industry/destroy-code]', '销毁处理失败', { logIntegration(
'[kuaishou-industry/destroy-code]',
'销毁处理失败',
{
requestId, requestId,
durationMs: Date.now() - startedAt, durationMs: Date.now() - startedAt,
error, error,
}, { level: 'error' }) },
{ level: 'error' },
)
res.status(200).json(buildIndustryErrorResponse(4010003, message)) res.status(200).json(buildIndustryErrorResponse(4010003, message))
} }
}) })
@@ -111,11 +121,16 @@ router.post('/query-code', industryRateLimit, async (req, res) => {
res.status(200).json(result) res.status(200).json(result)
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : '系统异常' const message = error instanceof Error ? error.message : '系统异常'
logIntegration('[kuaishou-industry/query-code]', '查询处理失败', { logIntegration(
'[kuaishou-industry/query-code]',
'查询处理失败',
{
requestId, requestId,
durationMs: Date.now() - startedAt, durationMs: Date.now() - startedAt,
error, error,
}, { level: 'error' }) },
{ level: 'error' },
)
res.status(200).json(buildIndustryErrorResponse(4010003, message)) res.status(200).json(buildIndustryErrorResponse(4010003, message))
} }
}) })
@@ -144,11 +159,16 @@ router.post('/consume-code', industryRateLimit, async (req, res) => {
res.status(200).json(result) res.status(200).json(result)
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : '系统异常' const message = error instanceof Error ? error.message : '系统异常'
logIntegration('[kuaishou-industry/consume-code]', '核销处理失败', { logIntegration(
'[kuaishou-industry/consume-code]',
'核销处理失败',
{
requestId, requestId,
durationMs: Date.now() - startedAt, durationMs: Date.now() - startedAt,
error, error,
}, { level: 'error' }) },
{ level: 'error' },
)
res.status(200).json(buildIndustryErrorResponse(4010003, message)) res.status(200).json(buildIndustryErrorResponse(4010003, message))
} }
}) })
+14 -4
View File
@@ -47,11 +47,16 @@ router.post('/orders/create', open91RateLimit, async (req, res) => {
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : '系统错误' const message = error instanceof Error ? error.message : '系统错误'
const code = resolveErrorStatusCode(error) >= 500 ? 500 : 400 const code = resolveErrorStatusCode(error) >= 500 ? 500 : 400
logIntegration('[open-91/create]', '91卡券异步下单处理失败', { logIntegration(
'[open-91/create]',
'91卡券异步下单处理失败',
{
requestId, requestId,
durationMs: Date.now() - startedAt, durationMs: Date.now() - startedAt,
error, error,
}, { level: 'error' }) },
{ level: 'error' },
)
res.status(200).json(buildOpen91ErrorResponse(message, code)) res.status(200).json(buildOpen91ErrorResponse(message, code))
} }
}) })
@@ -79,11 +84,16 @@ router.post('/orders/query', open91RateLimit, async (req, res) => {
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : '系统错误' const message = error instanceof Error ? error.message : '系统错误'
const code = resolveErrorStatusCode(error) >= 500 ? 500 : 400 const code = resolveErrorStatusCode(error) >= 500 ? 500 : 400
logIntegration('[open-91/query]', '91卡券订单查询处理失败', { logIntegration(
'[open-91/query]',
'91卡券订单查询处理失败',
{
requestId, requestId,
durationMs: Date.now() - startedAt, durationMs: Date.now() - startedAt,
error, error,
}, { level: 'error' }) },
{ level: 'error' },
)
res.status(200).json(buildOpen91ErrorResponse(message, code)) res.status(200).json(buildOpen91ErrorResponse(message, code))
} }
}) })
+7 -2
View File
@@ -25,12 +25,17 @@ router.get('/:code', async (req, res) => {
}) })
res.redirect(302, row.target_url) res.redirect(302, row.target_url)
} catch (error) { } catch (error) {
logIntegration('[short-link]', '短链跳转失败', { logIntegration(
'[short-link]',
'短链跳转失败',
{
requestId, requestId,
code: req.params.code, code: req.params.code,
durationMs: Date.now() - startedAt, durationMs: Date.now() - startedAt,
error, error,
}, { level: 'warn' }) },
{ level: 'warn' },
)
sendRouteError(res, error, '短链不可用', '[short-link]') sendRouteError(res, error, '短链不可用', '[short-link]')
} }
}) })
+20 -8
View File
@@ -111,40 +111,52 @@ router.get(
router.get( router.get(
'/profile/wallet-ledgers', '/profile/wallet-ledgers',
createRouteHandler((req) => listWorkerProfileWalletLedgers(req.query, getRequiredWorkerSession(req)), { createRouteHandler(
(req) => listWorkerProfileWalletLedgers(req.query, getRequiredWorkerSession(req)),
{
successMessage: 'ok', successMessage: 'ok',
errorMessage: '读取钱包流水失败', errorMessage: '读取钱包流水失败',
scope: '[worker/profile/wallet-ledgers]', scope: '[worker/profile/wallet-ledgers]',
}), },
),
) )
router.get( router.get(
'/profile/finance-requests', '/profile/finance-requests',
createRouteHandler((req) => listWorkerProfileFinanceRequests(req.query, getRequiredWorkerSession(req)), { createRouteHandler(
(req) => listWorkerProfileFinanceRequests(req.query, getRequiredWorkerSession(req)),
{
successMessage: 'ok', successMessage: 'ok',
errorMessage: '读取资金申请记录失败', errorMessage: '读取资金申请记录失败',
scope: '[worker/profile/finance-requests]', scope: '[worker/profile/finance-requests]',
}), },
),
) )
router.post( router.post(
'/profile/recharge-requests', '/profile/recharge-requests',
requireActiveWorker, requireActiveWorker,
createRouteHandler((req) => createWorkerRechargeRequest(req.body || {}, getRequiredWorkerSession(req)), { createRouteHandler(
(req) => createWorkerRechargeRequest(req.body || {}, getRequiredWorkerSession(req)),
{
successMessage: '充值申请已提交', successMessage: '充值申请已提交',
errorMessage: '提交充值申请失败', errorMessage: '提交充值申请失败',
scope: '[worker/profile/recharge-requests]', scope: '[worker/profile/recharge-requests]',
}), },
),
) )
router.post( router.post(
'/profile/withdraw-requests', '/profile/withdraw-requests',
requireActiveWorker, requireActiveWorker,
createRouteHandler((req) => createWorkerWithdrawRequest(req.body || {}, getRequiredWorkerSession(req)), { createRouteHandler(
(req) => createWorkerWithdrawRequest(req.body || {}, getRequiredWorkerSession(req)),
{
successMessage: '提现申请已提交', successMessage: '提现申请已提交',
errorMessage: '提交提现申请失败', errorMessage: '提交提现申请失败',
scope: '[worker/profile/withdraw-requests]', scope: '[worker/profile/withdraw-requests]',
}), },
),
) )
router.post( router.post(
+5 -1
View File
@@ -7,7 +7,11 @@ import {
} from '../../services/worker-platform/index.js' } from '../../services/worker-platform/index.js'
import { createHttpError, sendRouteError } from '../../utils/http.js' import { createHttpError, sendRouteError } from '../../utils/http.js'
export async function requireWorkerSession(req: Request, res: Response, next: NextFunction): Promise<void> { export async function requireWorkerSession(
req: Request,
res: Response,
next: NextFunction,
): Promise<void> {
try { try {
req.workerSession = await verifyWorkerSessionToken(extractWorkerBearerToken(req)) req.workerSession = await verifyWorkerSessionToken(extractWorkerBearerToken(req))
next() next()
@@ -1,9 +1,17 @@
import { createAdminAuditLog, listAdminAuditLogs } from '../../repositories/admin-audit-log-repo.js' import { createAdminAuditLog, listAdminAuditLogs } from '../../repositories/admin-audit-log-repo.js'
import { nowIso } from '../../utils/time.js' import { nowIso } from '../../utils/time.js'
import { normalizeDateQuery, normalizePage, normalizePageSize, safeParseJson } from './admin-query-utils.js' import {
normalizeDateQuery,
normalizePage,
normalizePageSize,
safeParseJson,
} from './admin-query-utils.js'
import type { JsonObject } from '../../types/json.js' import type { JsonObject } from '../../types/json.js'
export async function writeAdminAuditLog(session: JsonObject | null | undefined, payload: JsonObject = {}) { export async function writeAdminAuditLog(
session: JsonObject | null | undefined,
payload: JsonObject = {},
) {
if (!session?.userId) { if (!session?.userId) {
return null return null
} }
@@ -32,10 +32,14 @@ type AdminUserStatus = 'active' | 'disabled'
export async function ensureAdminUsersBootstrapped(): Promise<void> { export async function ensureAdminUsersBootstrapped(): Promise<void> {
ensureAdminAuthConfigured() ensureAdminAuthConfigured()
const configuredUsers = Array.isArray(runtimeConfig.admin?.defaultUsers) ? runtimeConfig.admin.defaultUsers : [] const configuredUsers = Array.isArray(runtimeConfig.admin?.defaultUsers)
? runtimeConfig.admin.defaultUsers
: []
for (const configuredUser of configuredUsers) { for (const configuredUser of configuredUsers) {
const username = String(configuredUser?.username || '').trim().toLowerCase() const username = String(configuredUser?.username || '')
.trim()
.toLowerCase()
const password = String(configuredUser?.password || '').trim() const password = String(configuredUser?.password || '').trim()
const role = normalizeAdminRole(configuredUser?.role) const role = normalizeAdminRole(configuredUser?.role)
@@ -70,7 +74,9 @@ export async function loginAdmin(
): Promise<JsonObject> { ): Promise<JsonObject> {
ensureAdminAuthConfigured() ensureAdminAuthConfigured()
const normalizedUsername = String(username || '').trim().toLowerCase() const normalizedUsername = String(username || '')
.trim()
.toLowerCase()
const normalizedPassword = String(password || '').trim() const normalizedPassword = String(password || '').trim()
if (!normalizedUsername || !normalizedPassword) { if (!normalizedUsername || !normalizedPassword) {
@@ -87,7 +93,11 @@ export async function loginAdmin(
} }
const user = await getAdminUserByUsername(normalizedUsername) const user = await getAdminUserByUsername(normalizedUsername)
if (!user || user.status !== 'active' || !verifyAdminPassword(normalizedPassword, user.password_hash)) { if (
!user ||
user.status !== 'active' ||
!verifyAdminPassword(normalizedPassword, user.password_hash)
) {
await recordAdminLoginLog({ await recordAdminLoginLog({
userId: user ? Number(user.id) : null, userId: user ? Number(user.id) : null,
username: normalizedUsername, username: normalizedUsername,
@@ -201,7 +211,10 @@ export async function getAdminSessionSummary(token: unknown): Promise<JsonObject
} }
} }
export function requireAdminRole(session: { role?: string } | null | undefined, allowedRoles: string[]): void { export function requireAdminRole(
session: { role?: string } | null | undefined,
allowedRoles: string[],
): void {
if (session && allowedRoles.includes(session.role || '')) { if (session && allowedRoles.includes(session.role || '')) {
return return
} }
@@ -342,7 +355,10 @@ export async function updateManagedAdminUserStatus(
} }
} }
export async function resetManagedAdminUserPassword(userId: number | string, payload: JsonObject = {}): Promise<JsonObject> { export async function resetManagedAdminUserPassword(
userId: number | string,
payload: JsonObject = {},
): Promise<JsonObject> {
const user = await getRequiredAdminUser(userId) const user = await getRequiredAdminUser(userId)
const password = normalizePassword(payload.password) const password = normalizePassword(payload.password)
@@ -373,7 +389,9 @@ export async function resetManagedAdminUserPassword(userId: number | string, pay
export function ensureAdminAuthConfigured(): void { export function ensureAdminAuthConfigured(): void {
const sessionSecret = String(runtimeConfig.admin?.sessionSecret || '').trim() const sessionSecret = String(runtimeConfig.admin?.sessionSecret || '').trim()
const configuredUsers = Array.isArray(runtimeConfig.admin?.defaultUsers) ? runtimeConfig.admin.defaultUsers : [] const configuredUsers = Array.isArray(runtimeConfig.admin?.defaultUsers)
? runtimeConfig.admin.defaultUsers
: []
if (sessionSecret && configuredUsers.length > 0) { if (sessionSecret && configuredUsers.length > 0) {
return return
@@ -435,7 +453,9 @@ function signPayload(encodedPayload: string): string {
} }
export function normalizeAdminRole(role: unknown): AdminRole { export function normalizeAdminRole(role: unknown): AdminRole {
const normalized = String(role || '').trim().toLowerCase() const normalized = String(role || '')
.trim()
.toLowerCase()
if (normalized === 'admin') { if (normalized === 'admin') {
return 'admin' return 'admin'
@@ -449,7 +469,11 @@ export function normalizeAdminRole(role: unknown): AdminRole {
} }
export function normalizeAdminUserStatus(status: unknown): AdminUserStatus { export function normalizeAdminUserStatus(status: unknown): AdminUserStatus {
return String(status || '').trim().toLowerCase() === 'disabled' ? 'disabled' : 'active' return String(status || '')
.trim()
.toLowerCase() === 'disabled'
? 'disabled'
: 'active'
} }
function safeCompare(input: unknown, expected: unknown): boolean { function safeCompare(input: unknown, expected: unknown): boolean {
@@ -464,17 +488,23 @@ function safeCompare(input: unknown, expected: unknown): boolean {
} }
function normalizeRoleQuery(role: unknown): string { function normalizeRoleQuery(role: unknown): string {
const normalized = String(role || '').trim().toLowerCase() const normalized = String(role || '')
.trim()
.toLowerCase()
return ['admin', 'operator', 'support'].includes(normalized) ? normalized : '' return ['admin', 'operator', 'support'].includes(normalized) ? normalized : ''
} }
function normalizeStatusQuery(status: unknown): string { function normalizeStatusQuery(status: unknown): string {
const normalized = String(status || '').trim().toLowerCase() const normalized = String(status || '')
.trim()
.toLowerCase()
return ['active', 'disabled'].includes(normalized) ? normalized : '' return ['active', 'disabled'].includes(normalized) ? normalized : ''
} }
function normalizeUsername(username: unknown): string { function normalizeUsername(username: unknown): string {
return String(username || '').trim().toLowerCase() return String(username || '')
.trim()
.toLowerCase()
} }
function normalizePassword(password: unknown): string { function normalizePassword(password: unknown): string {
@@ -523,7 +553,7 @@ async function getRequiredAdminUser(userId: number | string): Promise<AdminUserR
async function ensureAdminUserChangeAllowed( async function ensureAdminUserChangeAllowed(
user: AdminUserRow, user: AdminUserRow,
options: { nextRole?: AdminRole, nextStatus?: AdminUserStatus } = {}, options: { nextRole?: AdminRole; nextStatus?: AdminUserStatus } = {},
session: AdminSession, session: AdminSession,
): Promise<void> { ): Promise<void> {
const nextRole = options.nextRole || user.role const nextRole = options.nextRole || user.role
@@ -536,7 +566,11 @@ async function ensureAdminUserChangeAllowed(
}) })
} }
if (user.role === 'admin' && (nextRole !== 'admin' || nextStatus !== 'active') && await countActiveAdminUsers() <= 1) { if (
user.role === 'admin' &&
(nextRole !== 'admin' || nextStatus !== 'active') &&
(await countActiveAdminUsers()) <= 1
) {
throw createHttpError('至少保留一个启用中的管理员账号', { throw createHttpError('至少保留一个启用中的管理员账号', {
statusCode: 409, statusCode: 409,
errorCode: 'admin_user_last_admin_not_allowed', errorCode: 'admin_user_last_admin_not_allowed',
@@ -555,11 +589,13 @@ function mapAdminUser(user: AdminUserRow): JsonObject {
} }
} }
function pickLoginMeta(meta: { function pickLoginMeta(
meta: {
ip?: string ip?: string
userAgent?: string userAgent?: string
location?: string location?: string
} = {}) { } = {},
) {
const result: { const result: {
ip?: string ip?: string
userAgent?: string userAgent?: string
@@ -11,15 +11,8 @@ export async function getAdminDashboardSummary() {
TASK_STATUS.PENDING_BINDING_PREPARE, TASK_STATUS.PENDING_BINDING_PREPARE,
TASK_STATUS.WAITING_BINDING, TASK_STATUS.WAITING_BINDING,
] ]
const claimingStatuses = [ const claimingStatuses = [TASK_STATUS.CLAIMED, TASK_STATUS.ROLE_CONFIRMED, TASK_STATUS.REDEEMING]
TASK_STATUS.CLAIMED, const abnormalStatuses = [TASK_STATUS.RETRY_PENDING, TASK_STATUS.MANUAL_REVIEW]
TASK_STATUS.ROLE_CONFIRMED,
TASK_STATUS.REDEEMING,
]
const abnormalStatuses = [
TASK_STATUS.RETRY_PENDING,
TASK_STATUS.MANUAL_REVIEW,
]
const result = await query( const result = await query(
` `
SELECT SELECT
@@ -1,17 +1,10 @@
import type { Request } from 'express' import type { Request } from 'express'
import type { JsonObject } from '../../types/json.js' import type { JsonObject } from '../../types/json.js'
import { import { createAdminLoginLog, listAdminLoginLogs } from '../../repositories/admin-login-log-repo.js'
createAdminLoginLog,
listAdminLoginLogs,
} from '../../repositories/admin-login-log-repo.js'
import { logWarn } from '../../utils/logger.js' import { logWarn } from '../../utils/logger.js'
import { nowIso } from '../../utils/time.js' import { nowIso } from '../../utils/time.js'
import { import { normalizeDateQuery, normalizePage, normalizePageSize } from './admin-query-utils.js'
normalizeDateQuery,
normalizePage,
normalizePageSize,
} from './admin-query-utils.js'
type RecordAdminLoginInput = { type RecordAdminLoginInput = {
userId?: number | null userId?: number | null
@@ -108,16 +101,8 @@ export function resolveClientLocation(req: Request) {
'cloudfront-viewer-country', 'cloudfront-viewer-country',
'x-country-code', 'x-country-code',
]) ])
const city = firstHeader(req, [ const city = firstHeader(req, ['cf-ipcity', 'x-vercel-ip-city', 'x-city'])
'cf-ipcity', const region = firstHeader(req, ['cf-region', 'x-vercel-ip-country-region', 'x-region'])
'x-vercel-ip-city',
'x-city',
])
const region = firstHeader(req, [
'cf-region',
'x-vercel-ip-country-region',
'x-region',
])
return [country, region, city].filter(Boolean).join(' · ') return [country, region, city].filter(Boolean).join(' · ')
} }
@@ -34,7 +34,10 @@ export async function mapAdminOrderListItem(item: OrderListRow): Promise<AdminOr
createdAt: item.created_at, createdAt: item.created_at,
updatedAt: item.updated_at, updatedAt: item.updated_at,
itemCount: orderItems.length, itemCount: orderItems.length,
totalQuantity: orderItems.reduce((sum, orderItem) => sum + Math.max(1, Number(orderItem.quantity || 1)), 0), totalQuantity: orderItems.reduce(
(sum, orderItem) => sum + Math.max(1, Number(orderItem.quantity || 1)),
0,
),
itemSummary, itemSummary,
taskCount: Number(item.task_count || tasks.length || 0), taskCount: Number(item.task_count || tasks.length || 0),
resourceStatus: fulfillmentProgress.resourceStatus, resourceStatus: fulfillmentProgress.resourceStatus,
@@ -52,8 +55,9 @@ export function summarizeOrderItems(items: OrderItemRow[] | null | undefined): s
} }
const [firstItem] = normalizedItems const [firstItem] = normalizedItems
const firstLabel = resolveOrderItemTitle(firstItem) const firstLabel =
|| String(firstItem?.sku_name || firstItem?.sku_code || '').trim() resolveOrderItemTitle(firstItem) ||
String(firstItem?.sku_name || firstItem?.sku_code || '').trim()
if (normalizedItems.length === 1) { if (normalizedItems.length === 1) {
return firstLabel return firstLabel
@@ -66,10 +66,7 @@ import type {
AdminTaskListQueryInput, AdminTaskListQueryInput,
AdminViewerSessionInput, AdminViewerSessionInput,
} from '../../types/admin/read-inputs.js' } from '../../types/admin/read-inputs.js'
import type { import type { KuaishouIndustryVoucherRow, TaskRow } from '../../types/repository/rows.js'
KuaishouIndustryVoucherRow,
TaskRow,
} from '../../types/repository/rows.js'
export async function getAdminOrders( export async function getAdminOrders(
query: AdminOrderListQueryInput = {}, query: AdminOrderListQueryInput = {},
@@ -229,7 +226,8 @@ export async function getAdminTaskDetail(
} }
const primaryClaimTokenId = getTaskPrimaryClaimTokenId(task) const primaryClaimTokenId = getTaskPrimaryClaimTokenId(task)
const [order, claimToken, taskEvents, taskKuaishouIndustryVouchers, oidKuaishouIndustryVouchers] = await Promise.all([ const [order, claimToken, taskEvents, taskKuaishouIndustryVouchers, oidKuaishouIndustryVouchers] =
await Promise.all([
getOrderById(task.order_id), getOrderById(task.order_id),
primaryClaimTokenId ? getClaimTokenById(primaryClaimTokenId) : Promise.resolve(null), primaryClaimTokenId ? getClaimTokenById(primaryClaimTokenId) : Promise.resolve(null),
listTaskEventsByTaskId(task.id), listTaskEventsByTaskId(task.id),
@@ -252,10 +250,7 @@ export async function getAdminTaskDetail(
cloudSourceLabelMap, cloudSourceLabelMap,
}) || mapKuaishouCloudTaskStateProjection(task, { cloudSourceLabelMap }) }) || mapKuaishouCloudTaskStateProjection(task, { cloudSourceLabelMap })
const claimIdentity = buildClaimIdentityAdminSummary(taskContext, { const claimIdentity = buildClaimIdentityAdminSummary(taskContext, {
flowLike: flowLike: taskContext.kuaishouCloudFulfillment || taskContext.kuaishouFeifei || null,
taskContext.kuaishouCloudFulfillment ||
taskContext.kuaishouFeifei ||
null,
taskRoleId: task.role_id, taskRoleId: task.role_id,
taskRoleName: task.role_name, taskRoleName: task.role_name,
}) })
@@ -265,17 +260,20 @@ export async function getAdminTaskDetail(
taskKuaishouIndustryVouchers, taskKuaishouIndustryVouchers,
oidKuaishouIndustryVouchers, oidKuaishouIndustryVouchers,
) )
const kuaishouIndustryVoucher = const kuaishouIndustryVoucher = firstKuaishouIndustryVoucher
firstKuaishouIndustryVoucher
? mapAdminKuaishouIndustryVoucher(firstKuaishouIndustryVoucher) ? mapAdminKuaishouIndustryVoucher(firstKuaishouIndustryVoucher)
: mapKuaishouIndustryVoucherContext(taskContext.kuaishouIndustryVoucher) : mapKuaishouIndustryVoucherContext(taskContext.kuaishouIndustryVoucher)
const hasKuaishouIndustryVoucher = Boolean( const hasKuaishouIndustryVoucher = Boolean(
kuaishouIndustryVoucher && String(kuaishouIndustryVoucher.voucherCode || '').trim(), kuaishouIndustryVoucher && String(kuaishouIndustryVoucher.voucherCode || '').trim(),
) )
const kuaishouIndustryVoucherStatus = const kuaishouIndustryVoucherStatus = String(kuaishouIndustryVoucher?.status || '')
String(kuaishouIndustryVoucher?.status || '').trim().toUpperCase() .trim()
const kuaishouIndustryVoucherSendCallbackStatus = .toUpperCase()
String(kuaishouIndustryVoucher?.sendCallbackStatus || '').trim().toLowerCase() const kuaishouIndustryVoucherSendCallbackStatus = String(
kuaishouIndustryVoucher?.sendCallbackStatus || '',
)
.trim()
.toLowerCase()
return { return {
task: mapAdminTaskListItem( task: mapAdminTaskListItem(
@@ -435,7 +433,7 @@ function resolveKuaishouIndustryVoucherForTask(
taskContext.kuaishouIndustryVoucher && taskContext.kuaishouIndustryVoucher &&
typeof taskContext.kuaishouIndustryVoucher === 'object' && typeof taskContext.kuaishouIndustryVoucher === 'object' &&
!Array.isArray(taskContext.kuaishouIndustryVoucher) !Array.isArray(taskContext.kuaishouIndustryVoucher)
? taskContext.kuaishouIndustryVoucher as JsonRecord ? (taskContext.kuaishouIndustryVoucher as JsonRecord)
: {} : {}
const voucherCode = String(voucherContext.voucherCode || voucherContext.eticketId || '').trim() const voucherCode = String(voucherContext.voucherCode || voucherContext.eticketId || '').trim()
if (voucherCode) { if (voucherCode) {
@@ -457,7 +455,7 @@ function resolveKuaishouIndustryVoucherForTask(
} }
} }
return oidVouchers.length === 1 ? (oidVouchers[0] || null) : null return oidVouchers.length === 1 ? oidVouchers[0] || null : null
} }
function buildCloudSourceLabelMap() { function buildCloudSourceLabelMap() {
@@ -15,7 +15,10 @@ test('resolveAdminTaskScreenshotUrl lets support view final redeemed screenshot'
runtime_session_id: '', runtime_session_id: '',
} }
const screenshotUrl = await resolveAdminTaskScreenshotUrl(task, createAdminViewerContext({ role: 'support' })) const screenshotUrl = await resolveAdminTaskScreenshotUrl(
task,
createAdminViewerContext({ role: 'support' }),
)
assert.equal(screenshotUrl, '/api/v1/admin/tasks/12/screenshot') assert.equal(screenshotUrl, '/api/v1/admin/tasks/12/screenshot')
}) })
@@ -27,7 +30,10 @@ test('resolveAdminTaskScreenshotUrl falls back to review screenshot when runtime
runtime_session_id: 'runtime-session-13', runtime_session_id: 'runtime-session-13',
} }
const screenshotUrl = await resolveAdminTaskScreenshotUrl(task, createAdminViewerContext({ role: 'support' })) const screenshotUrl = await resolveAdminTaskScreenshotUrl(
task,
createAdminViewerContext({ role: 'support' }),
)
assert.equal(screenshotUrl, '/api/v1/admin/tasks/13/screenshot') assert.equal(screenshotUrl, '/api/v1/admin/tasks/13/screenshot')
}) })
@@ -79,8 +79,7 @@ export function mapKuaishouCloudFulfillmentContext(
const role = asJsonObject(record.role) const role = asJsonObject(record.role)
const purchase = asJsonObject(record.purchase) const purchase = asJsonObject(record.purchase)
const dispatch = asJsonObject(record.dispatch) const dispatch = asJsonObject(record.dispatch)
const returnNumber = const returnNumber = asJsonObject(record.returnNumber)
asJsonObject(record.returnNumber)
const consume = asJsonObject(record.consume) const consume = asJsonObject(record.consume)
const cloudSourceKeys = Array.isArray(binding.cloudSourceKeys) const cloudSourceKeys = Array.isArray(binding.cloudSourceKeys)
? binding.cloudSourceKeys.map((value: unknown) => String(value || '').trim()).filter(Boolean) ? binding.cloudSourceKeys.map((value: unknown) => String(value || '').trim()).filter(Boolean)
@@ -373,7 +372,8 @@ export function createAdminViewerContext(
canViewSensitiveTaskData: role === 'admin' || role === 'operator', canViewSensitiveTaskData: role === 'admin' || role === 'operator',
canManageTaskLifecycle: role === 'admin' || role === 'operator', canManageTaskLifecycle: role === 'admin' || role === 'operator',
canOperateAssistedTask: role === 'admin' || role === 'operator' || role === 'support', canOperateAssistedTask: role === 'admin' || role === 'operator' || role === 'support',
canOperateKuaishouIndustryVoucher: role === 'admin' || role === 'operator' || role === 'support', canOperateKuaishouIndustryVoucher:
role === 'admin' || role === 'operator' || role === 'support',
} }
} }
@@ -17,10 +17,7 @@ import type {
AdminTaskListItem, AdminTaskListItem,
} from '../../types/admin/read-models.js' } from '../../types/admin/read-models.js'
import type { AdminTaskActionPayload } from '../../types/admin/write-models.js' import type { AdminTaskActionPayload } from '../../types/admin/write-models.js'
import type { import type { TaskEventRow, TaskRow } from '../../types/repository/rows.js'
TaskEventRow,
TaskRow,
} from '../../types/repository/rows.js'
import type { AdminViewerContext } from './admin-read-shared-helpers.js' import type { AdminViewerContext } from './admin-read-shared-helpers.js'
type TaskFulfillmentState = { type TaskFulfillmentState = {
@@ -28,9 +25,7 @@ type TaskFulfillmentState = {
customerStatus: string customerStatus: string
} }
export function mapAdminTaskSummary( export function mapAdminTaskSummary(task: TaskRow): JsonRecord {
task: TaskRow,
): JsonRecord {
const fulfillment = buildTaskFulfillmentState(task) const fulfillment = buildTaskFulfillmentState(task)
return { return {
@@ -111,8 +106,10 @@ export function mapAdminTaskListItem(
lastError: task.last_error, lastError: task.last_error,
createdAt: task.created_at, createdAt: task.created_at,
updatedAt: task.updated_at, updatedAt: task.updated_at,
claimToken: viewerContext.canViewSensitiveTaskData ? (task.primary_claim_token || task.claim_token || '') : '', claimToken: viewerContext.canViewSensitiveTaskData
screenshotPath: viewerContext.role === 'support' ? '' : (task.screenshot_path || ''), ? task.primary_claim_token || task.claim_token || ''
: '',
screenshotPath: viewerContext.role === 'support' ? '' : task.screenshot_path || '',
} }
} }
@@ -134,7 +131,9 @@ export function buildOrderFulfillmentProgress(
const totalTaskCount = normalizedTasks.length const totalTaskCount = normalizedTasks.length
const taskFulfillments = normalizedTasks.map((task) => buildTaskFulfillmentState(task)) const taskFulfillments = normalizedTasks.map((task) => buildTaskFulfillmentState(task))
const preparedTaskCount = normalizedTasks.filter((task) => isTaskResourcePrepared(task)).length const preparedTaskCount = normalizedTasks.filter((task) => isTaskResourcePrepared(task)).length
const completedTaskCount = normalizedTasks.filter((task) => isTaskFulfillmentCompleted(task)).length const completedTaskCount = normalizedTasks.filter((task) =>
isTaskFulfillmentCompleted(task),
).length
let resourceStatus = 'pending_prepare' let resourceStatus = 'pending_prepare'
let customerStatus = 'not_started' let customerStatus = 'not_started'
@@ -152,7 +151,11 @@ export function buildOrderFulfillmentProgress(
if (normalizedTasks.every((task) => isTaskFulfillmentCompleted(task))) { if (normalizedTasks.every((task) => isTaskFulfillmentCompleted(task))) {
resourceStatus = 'resource_ready' resourceStatus = 'resource_ready'
customerStatus = 'customer_completed' customerStatus = 'customer_completed'
} else if (taskFulfillments.some((item) => ['customer_processing', 'customer_confirmed', 'link_opened'].includes(item.customerStatus))) { } else if (
taskFulfillments.some((item) =>
['customer_processing', 'customer_confirmed', 'link_opened'].includes(item.customerStatus),
)
) {
resourceStatus = preparedTaskCount > 0 ? 'resource_ready' : 'pending_prepare' resourceStatus = preparedTaskCount > 0 ? 'resource_ready' : 'pending_prepare'
customerStatus = 'customer_processing' customerStatus = 'customer_processing'
} else if (taskFulfillments.some((item) => item.customerStatus === 'waiting_customer')) { } else if (taskFulfillments.some((item) => item.customerStatus === 'waiting_customer')) {
@@ -163,7 +166,11 @@ export function buildOrderFulfillmentProgress(
customerStatus = 'customer_exception' customerStatus = 'customer_exception'
} else if (preparedTaskCount > 0) { } else if (preparedTaskCount > 0) {
resourceStatus = 'resource_ready' resourceStatus = 'resource_ready'
} else if (taskFulfillments.some((item) => ['manual_review', 'retry_pending'].includes(item.resourceStatus))) { } else if (
taskFulfillments.some((item) =>
['manual_review', 'retry_pending'].includes(item.resourceStatus),
)
) {
resourceStatus = 'resource_exception' resourceStatus = 'resource_exception'
} }
@@ -1,8 +1,6 @@
import { getAffiliateDashConfig } from '../../platforms/affiliate-dash/config.js' import { getAffiliateDashConfig } from '../../platforms/affiliate-dash/config.js'
import type { JsonObject } from '../../../types/json.js' import type { JsonObject } from '../../../types/json.js'
import { import { getAffiliateDashWallet } from '../../platforms/affiliate-dash/order-service.js'
getAffiliateDashWallet,
} from '../../platforms/affiliate-dash/order-service.js'
import { listAffiliateDashProducts } from '../../platforms/affiliate-dash/product-service.js' import { listAffiliateDashProducts } from '../../platforms/affiliate-dash/product-service.js'
import { import {
getAffiliateDashSourceConfig, getAffiliateDashSourceConfig,
@@ -35,9 +33,7 @@ export async function updateAdminAffiliateDashConfig(payload: JsonObject = {}) {
export function matchAdminAffiliateDashSku(payload: JsonObject = {}) { export function matchAdminAffiliateDashSku(payload: JsonObject = {}) {
const productNo = String(payload.productNo || payload.product_no || '').trim() const productNo = String(payload.productNo || payload.product_no || '').trim()
const source = getAdminEditableAffiliateDashConfig() const source = getAdminEditableAffiliateDashConfig()
const sku = productNo const sku = productNo ? String(source.skuMapping[productNo] || '').trim() : ''
? String(source.skuMapping[productNo] || '').trim()
: ''
return { return {
productNo, productNo,
@@ -52,7 +52,13 @@ test('resolveCloudtentaclesAdminContext merges payload source and persisted sess
test('hasCloudtentaclesCredentialContextChanged detects normalized credential changes', () => { test('hasCloudtentaclesCredentialContextChanged detects normalized credential changes', () => {
assert.equal( assert.equal(
hasCloudtentaclesCredentialContextChanged( hasCloudtentaclesCredentialContextChanged(
{ baseUrl: ' https://a ', username: 'u1', phone: '13812345678', deviceId: 'd1', deviceType: 1 }, {
baseUrl: ' https://a ',
username: 'u1',
phone: '13812345678',
deviceId: 'd1',
deviceType: 1,
},
{ baseUrl: 'https://a', username: 'u1', phone: '13812345678', deviceId: 'd1', deviceType: 1 }, { baseUrl: 'https://a', username: 'u1', phone: '13812345678', deviceId: 'd1', deviceType: 1 },
), ),
false, false,
@@ -1,70 +1,57 @@
export function pickFirstNonEmpty(values: unknown[]) { export function pickFirstNonEmpty(values: unknown[]) {
for (const value of values) { for (const value of values) {
const normalized = String(value || "").trim(); const normalized = String(value || '').trim()
if (normalized) { if (normalized) {
return normalized; return normalized
} }
} }
return ""; return ''
} }
export function isPlainObject(value: unknown): value is JsonObject { export function isPlainObject(value: unknown): value is JsonObject {
return Boolean(value) && typeof value === "object" && !Array.isArray(value); return Boolean(value) && typeof value === 'object' && !Array.isArray(value)
} }
import { getCloudtentaclesSourceByKey } from "../../../platforms/cloudtentacles/source-config-service.js"; import { getCloudtentaclesSourceByKey } from '../../../platforms/cloudtentacles/source-config-service.js'
import { getCloudtentaclesSessionStateByKey } from "../../../platforms/cloudtentacles/session-state-service.js"; import { getCloudtentaclesSessionStateByKey } from '../../../platforms/cloudtentacles/session-state-service.js'
import type { JsonObject } from '../../../../types/json.js' import type { JsonObject } from '../../../../types/json.js'
import { import {
normalizeCloudtentaclesDeviceId, normalizeCloudtentaclesDeviceId,
normalizeCloudtentaclesDeviceType, normalizeCloudtentaclesDeviceType,
} from "../../../platforms/cloudtentacles/defaults.js"; } from '../../../platforms/cloudtentacles/defaults.js'
export function resolveCloudtentaclesAdminContext(payload: JsonObject = {}, options: JsonObject = {}) { export function resolveCloudtentaclesAdminContext(
const sourceKey = String( payload: JsonObject = {},
payload.sourceKey || options.sourceKey || "default" options: JsonObject = {},
).trim(); ) {
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {}; const sourceKey = String(payload.sourceKey || options.sourceKey || 'default').trim()
const persistedSession = options.persistedSession || const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {}
getCloudtentaclesSessionStateByKey(sourceKey) || const persistedSession =
{}; options.persistedSession || getCloudtentaclesSessionStateByKey(sourceKey) || {}
const defaultBaseUrl = String( const defaultBaseUrl = String(options.defaultBaseUrl || 'https://123.207.217.176').trim()
options.defaultBaseUrl || "https://123.207.217.176"
).trim();
return { return {
sourceKey, sourceKey,
baseUrl: pickFirstNonEmpty([ baseUrl: pickFirstNonEmpty([payload.baseUrl, savedSource.baseUrl, defaultBaseUrl]),
payload.baseUrl,
savedSource.baseUrl,
defaultBaseUrl,
]),
token: pickFirstNonEmpty([payload.token, persistedSession.token]), token: pickFirstNonEmpty([payload.token, persistedSession.token]),
deviceId: normalizeCloudtentaclesDeviceId( deviceId: normalizeCloudtentaclesDeviceId(
pickFirstNonEmpty([ pickFirstNonEmpty([payload.deviceId, savedSource.deviceId, persistedSession.deviceId]),
payload.deviceId,
savedSource.deviceId,
persistedSession.deviceId,
])
), ),
deviceType: normalizeCloudtentaclesDeviceType( deviceType: normalizeCloudtentaclesDeviceType(
payload.deviceType ?? savedSource.deviceType ?? persistedSession.deviceType payload.deviceType ?? savedSource.deviceType ?? persistedSession.deviceType,
), ),
}; }
} }
export function hasCloudtentaclesCredentialContextChanged( export function hasCloudtentaclesCredentialContextChanged(
current: JsonObject = {}, current: JsonObject = {},
next: JsonObject = {} next: JsonObject = {},
) { ) {
return ( return (
String(current.baseUrl || "").trim() !== String(current.baseUrl || '').trim() !== String(next.baseUrl || '').trim() ||
String(next.baseUrl || "").trim() || String(current.username || '').trim() !== String(next.username || '').trim() ||
String(current.username || "").trim() !== String(current.phone || '').trim() !== String(next.phone || '').trim() ||
String(next.username || "").trim() || String(current.deviceId || '').trim() !== String(next.deviceId || '').trim() ||
String(current.phone || "").trim() !== String(next.phone || "").trim() ||
String(current.deviceId || "").trim() !==
String(next.deviceId || "").trim() ||
Number(current.deviceType || 0) !== Number(next.deviceType || 0) Number(current.deviceType || 0) !== Number(next.deviceType || 0)
); )
} }
File diff suppressed because it is too large Load Diff
@@ -1,11 +1,7 @@
import test from 'node:test' import test from 'node:test'
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
import { import { mapAdminCloudtentaclesSession, maskPhone, maskSecret } from './mappers.js'
mapAdminCloudtentaclesSession,
maskPhone,
maskSecret,
} from './mappers.js'
test('maskSecret preserves edges while hiding middle characters', () => { test('maskSecret preserves edges while hiding middle characters', () => {
assert.equal(maskSecret('abcdef1234567890'), 'abcdef****567890') assert.equal(maskSecret('abcdef1234567890'), 'abcdef****567890')
@@ -2,44 +2,44 @@ import type { JsonObject } from '../../../../types/json.js'
import { import {
maskPhone as maskPhoneValue, maskPhone as maskPhoneValue,
maskSecret as maskSecretValue, maskSecret as maskSecretValue,
} from "../../../../utils/masking.js"; } from '../../../../utils/masking.js'
import { import {
normalizeCloudtentaclesDeviceId, normalizeCloudtentaclesDeviceId,
normalizeCloudtentaclesDeviceType, normalizeCloudtentaclesDeviceType,
} from "../../../platforms/cloudtentacles/defaults.js"; } from '../../../platforms/cloudtentacles/defaults.js'
export function maskSecret(value: unknown) { export function maskSecret(value: unknown) {
return maskSecretValue(value); return maskSecretValue(value)
} }
export function maskPhone(value: unknown) { export function maskPhone(value: unknown) {
return maskPhoneValue(value, { maskShort: false }); return maskPhoneValue(value, { maskShort: false })
} }
export function mapAdminCloudtentaclesSourceConfig(config: JsonObject = {}) { export function mapAdminCloudtentaclesSourceConfig(config: JsonObject = {}) {
return { return {
key: String(config.key || "").trim(), key: String(config.key || '').trim(),
label: String(config.label || "").trim(), label: String(config.label || '').trim(),
enabled: config.enabled !== false, enabled: config.enabled !== false,
baseUrl: String(config.baseUrl || "").trim(), baseUrl: String(config.baseUrl || '').trim(),
username: String(config.username || "").trim(), username: String(config.username || '').trim(),
password: String(config.password || "").trim(), password: String(config.password || '').trim(),
phone: String(config.phone || "").trim(), phone: String(config.phone || '').trim(),
deviceId: normalizeCloudtentaclesDeviceId(config.deviceId), deviceId: normalizeCloudtentaclesDeviceId(config.deviceId),
deviceType: normalizeCloudtentaclesDeviceType(config.deviceType), deviceType: normalizeCloudtentaclesDeviceType(config.deviceType),
}; }
} }
export function mapAdminCloudtentaclesSession(session: JsonObject = {}) { export function mapAdminCloudtentaclesSession(session: JsonObject = {}) {
return { return {
token: String(session.token || "").trim(), token: String(session.token || '').trim(),
tokenMasked: maskSecret(session.token), tokenMasked: maskSecret(session.token),
baseUrl: String(session.baseUrl || "").trim(), baseUrl: String(session.baseUrl || '').trim(),
username: String(session.username || "").trim(), username: String(session.username || '').trim(),
phoneMasked: maskPhone(session.phone), phoneMasked: maskPhone(session.phone),
loggedInAt: String(session.loggedInAt || "").trim(), loggedInAt: String(session.loggedInAt || '').trim(),
deviceId: normalizeCloudtentaclesDeviceId(session.deviceId), deviceId: normalizeCloudtentaclesDeviceId(session.deviceId),
deviceType: normalizeCloudtentaclesDeviceType(session.deviceType), deviceType: normalizeCloudtentaclesDeviceType(session.deviceType),
hasToken: Boolean(String(session.token || "").trim()), hasToken: Boolean(String(session.token || '').trim()),
}; }
} }
@@ -1,96 +1,80 @@
import { getCloudtentaclesSourceByKey } from "../../../platforms/cloudtentacles/source-config-service.js"; import { getCloudtentaclesSourceByKey } from '../../../platforms/cloudtentacles/source-config-service.js'
import { getCloudtentaclesSessionStateByKey } from "../../../platforms/cloudtentacles/session-state-service.js"; import { getCloudtentaclesSessionStateByKey } from '../../../platforms/cloudtentacles/session-state-service.js'
import type { JsonObject } from '../../../../types/json.js' import type { JsonObject } from '../../../../types/json.js'
import { import {
normalizeCloudtentaclesDeviceId, normalizeCloudtentaclesDeviceId,
normalizeCloudtentaclesDeviceType, normalizeCloudtentaclesDeviceType,
} from "../../../platforms/cloudtentacles/defaults.js"; } from '../../../platforms/cloudtentacles/defaults.js'
import { import { pickFirstNonEmpty, resolveCloudtentaclesAdminContext } from './context.js'
pickFirstNonEmpty, import { maskPhone, maskSecret } from './mappers.js'
resolveCloudtentaclesAdminContext,
} from "./context.js";
import { maskPhone, maskSecret } from "./mappers.js";
const DEFAULT_CLOUDTENTACLES_BASE_URL = "https://123.207.217.176"; const DEFAULT_CLOUDTENTACLES_BASE_URL = 'https://123.207.217.176'
function _resolveSourceKey(payload: JsonObject = {}) { function _resolveSourceKey(payload: JsonObject = {}) {
return String(payload.sourceKey || "").trim() || "default"; return String(payload.sourceKey || '').trim() || 'default'
} }
export function resolveAdminCloudtentaclesCredentialPayload( export function resolveAdminCloudtentaclesCredentialPayload(
payload: JsonObject = {}, payload: JsonObject = {},
options: JsonObject = {} options: JsonObject = {},
) { ) {
const sourceKey = _resolveSourceKey(payload); const sourceKey = _resolveSourceKey(payload)
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {}; const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {}
const defaultBaseUrl = String( const defaultBaseUrl = String(options.defaultBaseUrl || DEFAULT_CLOUDTENTACLES_BASE_URL).trim()
options.defaultBaseUrl || DEFAULT_CLOUDTENTACLES_BASE_URL
).trim();
return { return {
sourceKey, sourceKey,
baseUrl: pickFirstNonEmpty([ baseUrl: pickFirstNonEmpty([payload.baseUrl, savedSource.baseUrl, defaultBaseUrl]),
payload.baseUrl,
savedSource.baseUrl,
defaultBaseUrl,
]),
username: pickFirstNonEmpty([payload.username, savedSource.username]), username: pickFirstNonEmpty([payload.username, savedSource.username]),
password: pickFirstNonEmpty([payload.password, savedSource.password]), password: pickFirstNonEmpty([payload.password, savedSource.password]),
phone: pickFirstNonEmpty([payload.phone, savedSource.phone]), phone: pickFirstNonEmpty([payload.phone, savedSource.phone]),
deviceId: normalizeCloudtentaclesDeviceId( deviceId: normalizeCloudtentaclesDeviceId(
pickFirstNonEmpty([payload.deviceId, savedSource.deviceId]) pickFirstNonEmpty([payload.deviceId, savedSource.deviceId]),
), ),
deviceType: normalizeCloudtentaclesDeviceType( deviceType: normalizeCloudtentaclesDeviceType(payload.deviceType ?? savedSource.deviceType),
payload.deviceType ?? savedSource.deviceType }
),
};
} }
export function resolveAdminCloudtentaclesSessionPayload( export function resolveAdminCloudtentaclesSessionPayload(
payload: JsonObject = {}, payload: JsonObject = {},
options: JsonObject = {} options: JsonObject = {},
) { ) {
const sourceKey = _resolveSourceKey(payload); const sourceKey = _resolveSourceKey(payload)
const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {}; const savedSource = options.savedSource || getCloudtentaclesSourceByKey(sourceKey) || {}
const persistedSession = options.persistedSession || const persistedSession =
getCloudtentaclesSessionStateByKey(sourceKey) || options.persistedSession || getCloudtentaclesSessionStateByKey(sourceKey) || {}
{};
return resolveCloudtentaclesAdminContext(payload, { return resolveCloudtentaclesAdminContext(payload, {
savedSource, savedSource,
persistedSession, persistedSession,
sourceKey, sourceKey,
defaultBaseUrl: options.defaultBaseUrl || DEFAULT_CLOUDTENTACLES_BASE_URL, defaultBaseUrl: options.defaultBaseUrl || DEFAULT_CLOUDTENTACLES_BASE_URL,
}); })
} }
export function buildAdminCloudtentaclesPersistedSessionPayload( export function buildAdminCloudtentaclesPersistedSessionPayload(
session: JsonObject = {}, session: JsonObject = {},
options: JsonObject = {} options: JsonObject = {},
) { ) {
return { return {
token: String(session.token || "").trim(), token: String(session.token || '').trim(),
baseUrl: String(session.baseUrl || "").trim(), baseUrl: String(session.baseUrl || '').trim(),
username: pickFirstNonEmpty([options.username, session.username]), username: pickFirstNonEmpty([options.username, session.username]),
phone: pickFirstNonEmpty([options.phone, session.phone]), phone: pickFirstNonEmpty([options.phone, session.phone]),
loggedInAt: String(session.loggedInAt || "").trim(), loggedInAt: String(session.loggedInAt || '').trim(),
deviceId: normalizeCloudtentaclesDeviceId( deviceId: normalizeCloudtentaclesDeviceId(
pickFirstNonEmpty([options.deviceId, session.deviceId]) pickFirstNonEmpty([options.deviceId, session.deviceId]),
), ),
deviceType: normalizeCloudtentaclesDeviceType( deviceType: normalizeCloudtentaclesDeviceType(options.deviceType ?? session.deviceType),
options.deviceType ?? session.deviceType }
),
};
} }
export function buildAdminCloudtentaclesSessionSummary( export function buildAdminCloudtentaclesSessionSummary(
session: JsonObject = {}, session: JsonObject = {},
savedSession: JsonObject = {}, savedSession: JsonObject = {},
sourceKey = "default" sourceKey = 'default',
) { ) {
const permissions = Array.isArray(session.permissions) const permissions = Array.isArray(session.permissions) ? session.permissions : []
? session.permissions
: [];
return { return {
sourceKey, sourceKey,
@@ -98,47 +82,39 @@ export function buildAdminCloudtentaclesSessionSummary(
permissionCount: permissions.length, permissionCount: permissions.length,
permissions, permissions,
persisted: Boolean(savedSession.token), persisted: Boolean(savedSession.token),
}; }
} }
export function buildAdminCloudtentaclesLoginResult( export function buildAdminCloudtentaclesLoginResult(
session: JsonObject = {}, session: JsonObject = {},
savedSession: JsonObject = {}, savedSession: JsonObject = {},
sourceKey = "default" sourceKey = 'default',
) { ) {
return { return {
sourceKey, sourceKey,
baseUrl: String(session.baseUrl || "").trim(), baseUrl: String(session.baseUrl || '').trim(),
username: String(session.username || "").trim(), username: String(session.username || '').trim(),
phoneMasked: maskPhone(session.phone), phoneMasked: maskPhone(session.phone),
loggedInAt: String(session.loggedInAt || "").trim(), loggedInAt: String(session.loggedInAt || '').trim(),
responseMessage: String(session.responseMessage || "").trim(), responseMessage: String(session.responseMessage || '').trim(),
token: String(session.token || "").trim(), token: String(session.token || '').trim(),
session: buildAdminCloudtentaclesSessionSummary( session: buildAdminCloudtentaclesSessionSummary(session, savedSession, sourceKey),
session,
savedSession,
sourceKey
),
userInfo: session.userInfo, userInfo: session.userInfo,
asset: session.asset, asset: session.asset,
}; }
} }
export function buildAdminCloudtentaclesValidateResult( export function buildAdminCloudtentaclesValidateResult(
session: JsonObject = {}, session: JsonObject = {},
savedSession: JsonObject = {}, savedSession: JsonObject = {},
sourceKey = "default" sourceKey = 'default',
) { ) {
return { return {
sourceKey, sourceKey,
baseUrl: String(session.baseUrl || "").trim(), baseUrl: String(session.baseUrl || '').trim(),
loggedInAt: String(session.loggedInAt || "").trim(), loggedInAt: String(session.loggedInAt || '').trim(),
session: buildAdminCloudtentaclesSessionSummary( session: buildAdminCloudtentaclesSessionSummary(session, savedSession, sourceKey),
session,
savedSession,
sourceKey
),
userInfo: session.userInfo, userInfo: session.userInfo,
asset: session.asset, asset: session.asset,
}; }
} }
@@ -2,22 +2,19 @@ import type { JsonObject } from '../../../../types/json.js'
import { import {
normalizeCloudtentaclesDeviceId, normalizeCloudtentaclesDeviceId,
normalizeCloudtentaclesDeviceType, normalizeCloudtentaclesDeviceType,
} from "../../../platforms/cloudtentacles/defaults.js"; } from '../../../platforms/cloudtentacles/defaults.js'
export function normalizeAdminCloudtentaclesSourceConfigPayload( export function normalizeAdminCloudtentaclesSourceConfigPayload(payload: JsonObject = {}) {
payload: JsonObject = {} const sourceKey = String(payload.sourceKey || payload.key || '').trim() || 'default'
) {
const sourceKey =
String(payload.sourceKey || payload.key || "").trim() || "default";
return { return {
key: sourceKey, key: sourceKey,
label: String(payload.label || "").trim(), label: String(payload.label || '').trim(),
enabled: payload.enabled !== false, enabled: payload.enabled !== false,
baseUrl: String(payload.baseUrl || "").trim() || "https://123.207.217.176", baseUrl: String(payload.baseUrl || '').trim() || 'https://123.207.217.176',
username: String(payload.username || "").trim(), username: String(payload.username || '').trim(),
password: String(payload.password || "").trim(), password: String(payload.password || '').trim(),
phone: String(payload.phone || "").trim(), phone: String(payload.phone || '').trim(),
deviceId: normalizeCloudtentaclesDeviceId(payload.deviceId), deviceId: normalizeCloudtentaclesDeviceId(payload.deviceId),
deviceType: normalizeCloudtentaclesDeviceType(payload.deviceType), deviceType: normalizeCloudtentaclesDeviceType(payload.deviceType),
}; }
} }
@@ -60,14 +60,18 @@ export async function listAdminKuaishouFeifeiProducts(payload: JsonObject = {})
page: Number(payload.page || 1) || 1, page: Number(payload.page || 1) || 1,
perPage: Number(payload.perPage || payload.per_page || 20) || 20, perPage: Number(payload.perPage || payload.per_page || 20) || 20,
status: String(payload.status || 'on_sale').trim(), status: String(payload.status || 'on_sale').trim(),
supplyProductName: String(payload.supplyProductName || payload.supply_product_name || '').trim(), supplyProductName: String(
payload.supplyProductName || payload.supply_product_name || '',
).trim(),
}) })
} }
export async function syncAdminKuaishouFeifeiProductRules(payload: JsonObject = {}) { export async function syncAdminKuaishouFeifeiProductRules(payload: JsonObject = {}) {
const source = getAdminEditableKuaishouFeifeiConfig() const source = getAdminEditableKuaishouFeifeiConfig()
const status = String(payload.status || 'on_sale').trim() || 'on_sale' const status = String(payload.status || 'on_sale').trim() || 'on_sale'
const supplyProductName = String(payload.supplyProductName || payload.supply_product_name || '').trim() const supplyProductName = String(
payload.supplyProductName || payload.supply_product_name || '',
).trim()
const products = await listAllKuaishouFeifeiProducts({ const products = await listAllKuaishouFeifeiProducts({
status, status,
supplyProductName, supplyProductName,
@@ -185,10 +189,7 @@ function mapEffectiveKuaishouFeifeiConfig(config: ReturnType<typeof getKuaishouF
} }
} }
async function listAllKuaishouFeifeiProducts(input: { async function listAllKuaishouFeifeiProducts(input: { status: string; supplyProductName: string }) {
status: string
supplyProductName: string
}) {
const perPage = 100 const perPage = 100
const firstPage = await listKuaishouFeifeiProducts({ const firstPage = await listKuaishouFeifeiProducts({
page: 1, page: 1,
@@ -14,16 +14,9 @@ import {
refreshKuaishouIndustryAccessToken, refreshKuaishouIndustryAccessToken,
} from '../../platforms/kuaishou-industry/token-service.js' } from '../../platforms/kuaishou-industry/token-service.js'
const SECRET_FIELDS = [ const SECRET_FIELDS = ['appSecret', 'signSecret', 'messageSecret'] as const
'appSecret',
'signSecret',
'messageSecret',
] as const
const SHOP_SECRET_FIELDS = [ const SHOP_SECRET_FIELDS = ['accessToken', 'refreshToken'] as const
'accessToken',
'refreshToken',
] as const
export function getAdminKuaishouIndustrySourceConfig() { export function getAdminKuaishouIndustrySourceConfig() {
const config = getKuaishouIndustrySourceConfig() const config = getKuaishouIndustrySourceConfig()
@@ -38,15 +31,23 @@ export async function updateAdminKuaishouIndustrySourceConfig(payload: JsonObjec
const current = getKuaishouIndustrySourceConfig() const current = getKuaishouIndustrySourceConfig()
const saved = await saveKuaishouIndustrySourceConfig({ const saved = await saveKuaishouIndustrySourceConfig({
...current, ...current,
enabled: hasPayloadField(payload, 'enabled') ? payload.enabled !== false : current.enabled !== false, enabled: hasPayloadField(payload, 'enabled')
? payload.enabled !== false
: current.enabled !== false,
baseUrl: readConfigString(payload, 'baseUrl', current.baseUrl), baseUrl: readConfigString(payload, 'baseUrl', current.baseUrl),
authBaseUrl: readConfigString(payload, 'authBaseUrl', current.authBaseUrl), authBaseUrl: readConfigString(payload, 'authBaseUrl', current.authBaseUrl),
redirectUri: readConfigString(payload, 'redirectUri', current.redirectUri, { allowBlank: true }), redirectUri: readConfigString(payload, 'redirectUri', current.redirectUri, {
scopes: normalizeScopeText(readConfigString(payload, 'scopes', current.scopes, { allowBlank: true })), allowBlank: true,
}),
scopes: normalizeScopeText(
readConfigString(payload, 'scopes', current.scopes, { allowBlank: true }),
),
authState: readConfigString(payload, 'authState', current.authState, { allowBlank: true }), authState: readConfigString(payload, 'authState', current.authState, { allowBlank: true }),
appKey: readConfigString(payload, 'appKey', current.appKey, { allowBlank: true }), appKey: readConfigString(payload, 'appKey', current.appKey, { allowBlank: true }),
openId: readConfigString(payload, 'openId', current.openId, { allowBlank: true }), openId: readConfigString(payload, 'openId', current.openId, { allowBlank: true }),
grantedScopes: normalizeScopeText(readConfigString(payload, 'grantedScopes', current.grantedScopes, { allowBlank: true })), grantedScopes: normalizeScopeText(
readConfigString(payload, 'grantedScopes', current.grantedScopes, { allowBlank: true }),
),
sellerId: readConfigString(payload, 'sellerId', current.sellerId, { allowBlank: true }), sellerId: readConfigString(payload, 'sellerId', current.sellerId, { allowBlank: true }),
provider: readConfigString(payload, 'provider', current.provider), provider: readConfigString(payload, 'provider', current.provider),
platform: readConfigString(payload, 'platform', current.platform), platform: readConfigString(payload, 'platform', current.platform),
@@ -245,33 +246,77 @@ function normalizeShopConfigPayload(
const payload = rawShop as JsonObject const payload = rawShop as JsonObject
const current = resolveCurrentShopConfig(payload, index, currentShops) const current = resolveCurrentShopConfig(payload, index, currentShops)
const sellerId = readConfigString(payload, 'sellerId', current?.sellerId || '', { allowBlank: true }) const sellerId = readConfigString(payload, 'sellerId', current?.sellerId || '', {
const shopId = readConfigString(payload, 'shopId', current?.shopId || sellerId, { allowBlank: true }) || sellerId allowBlank: true,
const shopName = readConfigString(payload, 'shopName', current?.shopName || '', { allowBlank: true }) })
const customShopName = readConfigString(payload, 'customShopName', current?.customShopName || '', { allowBlank: true }) const shopId =
readConfigString(payload, 'shopId', current?.shopId || sellerId, { allowBlank: true }) ||
sellerId
const shopName = readConfigString(payload, 'shopName', current?.shopName || '', {
allowBlank: true,
})
const customShopName = readConfigString(
payload,
'customShopName',
current?.customShopName || '',
{ allowBlank: true },
)
const accessToken = readSecretString(payload, 'accessToken', current?.accessToken || '') const accessToken = readSecretString(payload, 'accessToken', current?.accessToken || '')
const refreshToken = readSecretString(payload, 'refreshToken', current?.refreshToken || '') const refreshToken = readSecretString(payload, 'refreshToken', current?.refreshToken || '')
const openId = readConfigString(payload, 'openId', current?.openId || '', { allowBlank: true }) const openId = readConfigString(payload, 'openId', current?.openId || '', { allowBlank: true })
if (!sellerId && !shopId && !shopName && !customShopName && !accessToken && !refreshToken && !openId) { if (
!sellerId &&
!shopId &&
!shopName &&
!customShopName &&
!accessToken &&
!refreshToken &&
!openId
) {
return null return null
} }
return { return {
enabled: hasPayloadField(payload, 'enabled') ? payload.enabled !== false : current?.enabled !== false, enabled: hasPayloadField(payload, 'enabled')
? payload.enabled !== false
: current?.enabled !== false,
sellerId, sellerId,
shopId, shopId,
shopName, shopName,
customShopName, customShopName,
authState: readConfigString(payload, 'authState', current?.authState || '', { allowBlank: true }), authState: readConfigString(payload, 'authState', current?.authState || '', {
allowBlank: true,
}),
accessToken, accessToken,
refreshToken, refreshToken,
accessTokenExpiresAt: readConfigString(payload, 'accessTokenExpiresAt', current?.accessTokenExpiresAt || '', { allowBlank: true }), accessTokenExpiresAt: readConfigString(
refreshTokenExpiresAt: readConfigString(payload, 'refreshTokenExpiresAt', current?.refreshTokenExpiresAt || '', { allowBlank: true }), payload,
'accessTokenExpiresAt',
current?.accessTokenExpiresAt || '',
{ allowBlank: true },
),
refreshTokenExpiresAt: readConfigString(
payload,
'refreshTokenExpiresAt',
current?.refreshTokenExpiresAt || '',
{ allowBlank: true },
),
openId, openId,
grantedScopes: normalizeScopeText(readConfigString(payload, 'grantedScopes', current?.grantedScopes || '', { allowBlank: true })), grantedScopes: normalizeScopeText(
lastRefreshedAt: readConfigString(payload, 'lastRefreshedAt', current?.lastRefreshedAt || '', { allowBlank: true }), readConfigString(payload, 'grantedScopes', current?.grantedScopes || '', {
lastRefreshError: readConfigString(payload, 'lastRefreshError', current?.lastRefreshError || '', { allowBlank: true }), allowBlank: true,
}),
),
lastRefreshedAt: readConfigString(payload, 'lastRefreshedAt', current?.lastRefreshedAt || '', {
allowBlank: true,
}),
lastRefreshError: readConfigString(
payload,
'lastRefreshError',
current?.lastRefreshError || '',
{ allowBlank: true },
),
...resolveShopSecretPatch(payload, current), ...resolveShopSecretPatch(payload, current),
} }
} }
@@ -305,16 +350,14 @@ function resolveShopSecretPatch(
return patch return patch
} }
function readSecretString( function readSecretString(payload: JsonObject, field: string, fallback: string): string {
payload: JsonObject,
field: string,
fallback: string,
): string {
const text = String(payload[field] || '').trim() const text = String(payload[field] || '').trim()
return text || fallback return text || fallback
} }
function resolveAccessTokenStatus(config: Pick<KuaishouIndustrySourceConfig, 'accessToken' | 'accessTokenExpiresAt'>) { function resolveAccessTokenStatus(
config: Pick<KuaishouIndustrySourceConfig, 'accessToken' | 'accessTokenExpiresAt'>,
) {
if (!config.accessToken) { if (!config.accessToken) {
return { return {
status: 'missing', status: 'missing',
@@ -94,23 +94,27 @@ function mapAdminNotificationConfig(config: JsonObject = {}) {
bark: { bark: {
enabled: bark.enabled !== false, enabled: bark.enabled !== false,
serverUrl: String(bark.serverUrl || 'https://api.day.app').trim(), serverUrl: String(bark.serverUrl || 'https://api.day.app').trim(),
recipients: (Array.isArray(bark.recipients) ? bark.recipients : []).map((item: JsonObject) => ({ recipients: (Array.isArray(bark.recipients) ? bark.recipients : []).map(
(item: JsonObject) => ({
id: String(item.id || '').trim(), id: String(item.id || '').trim(),
name: String(item.name || '').trim(), name: String(item.name || '').trim(),
deviceKey: String(item.deviceKey || '').trim(), deviceKey: String(item.deviceKey || '').trim(),
deviceKeyMasked: maskSecret(item.deviceKey), deviceKeyMasked: maskSecret(item.deviceKey),
enabled: item.enabled !== false, enabled: item.enabled !== false,
})), }),
),
}, },
wpush: { wpush: {
enabled: wpush.enabled !== false, enabled: wpush.enabled !== false,
recipients: (Array.isArray(wpush.recipients) ? wpush.recipients : []).map((item: JsonObject) => ({ recipients: (Array.isArray(wpush.recipients) ? wpush.recipients : []).map(
(item: JsonObject) => ({
id: String(item.id || '').trim(), id: String(item.id || '').trim(),
name: String(item.name || '').trim(), name: String(item.name || '').trim(),
apiKey: String(item.apiKey || item.apikey || '').trim(), apiKey: String(item.apiKey || item.apikey || '').trim(),
apiKeyMasked: maskSecret(item.apiKey || item.apikey), apiKeyMasked: maskSecret(item.apiKey || item.apikey),
enabled: item.enabled !== false, enabled: item.enabled !== false,
})), }),
),
}, },
}, },
} }
@@ -118,8 +122,7 @@ function mapAdminNotificationConfig(config: JsonObject = {}) {
function mapAdminScheduledJobsConfig(config: JsonObject = {}) { function mapAdminScheduledJobsConfig(config: JsonObject = {}) {
const cloudtentaclesAccountMap = new Map( const cloudtentaclesAccountMap = new Map(
listAdminCloudtentaclesMonitorAccounts() listAdminCloudtentaclesMonitorAccounts().map((item) => [item.sourceKey, item]),
.map((item) => [item.sourceKey, item]),
) )
return { return {
@@ -147,7 +150,8 @@ function listAdminCloudtentaclesMonitorAccounts() {
const sessionsConfig = getAllCloudtentaclesSessionStates() const sessionsConfig = getAllCloudtentaclesSessionStates()
const sessions: Record<string, JsonObject> = sessionsConfig.sessions || {} const sessions: Record<string, JsonObject> = sessionsConfig.sessions || {}
return (Array.isArray(sourcesConfig.sources) ? sourcesConfig.sources : []).map((source) => { return (Array.isArray(sourcesConfig.sources) ? sourcesConfig.sources : [])
.map((source) => {
const sourceKey = String(source.key || '').trim() const sourceKey = String(source.key || '').trim()
const session = sessions[sourceKey] || {} const session = sessions[sourceKey] || {}
const label = String(source.label || source.username || sourceKey).trim() || sourceKey const label = String(source.label || source.username || sourceKey).trim() || sourceKey
@@ -161,7 +165,8 @@ function listAdminCloudtentaclesMonitorAccounts() {
hasToken: Boolean(String(session.token || '').trim()), hasToken: Boolean(String(session.token || '').trim()),
loggedInAt: String(session.loggedInAt || '').trim(), loggedInAt: String(session.loggedInAt || '').trim(),
} }
}).filter((item) => item.sourceKey) })
.filter((item) => item.sourceKey)
} }
function mapScheduledJobCloudtentaclesAccounts( function mapScheduledJobCloudtentaclesAccounts(
@@ -193,30 +198,35 @@ function mapScheduledJobCloudtentaclesAccounts(
}, },
] as const ] as const
}) })
.filter(Boolean) as Array<readonly [string, { .filter(Boolean) as Array<
readonly [
string,
{
sourceKey: string sourceKey: string
label: string label: string
enabled: boolean enabled: boolean
assetThreshold: number assetThreshold: number
}]>, },
]
>,
) )
// 读配置时合并全部 kuaishou-lewan 账号,避免前端/任务只看到历史 default。 // 读配置时合并全部 kuaishou-lewan 账号,避免前端/任务只看到历史 default。
const merged = Array.from(cloudtentaclesAccountMap.values()).map((option) => { const merged = Array.from(cloudtentaclesAccountMap.values())
.map((option) => {
const sourceKey = String(option.sourceKey || '').trim() const sourceKey = String(option.sourceKey || '').trim()
const configured = configuredMap.get(sourceKey) const configured = configuredMap.get(sourceKey)
return { return {
sourceKey, sourceKey,
label: String(configured?.label || option.label || sourceKey).trim(), label: String(configured?.label || option.label || sourceKey).trim(),
enabled: configured enabled: configured ? configured.enabled !== false : option.enabled !== false,
? configured.enabled !== false
: option.enabled !== false,
assetThreshold: normalizeNonNegativeNumber( assetThreshold: normalizeNonNegativeNumber(
configured?.assetThreshold, configured?.assetThreshold,
defaultAssetThreshold, defaultAssetThreshold,
), ),
} }
}).filter((item) => item.sourceKey) })
.filter((item) => item.sourceKey)
const mergedKeys = new Set(merged.map((item) => item.sourceKey)) const mergedKeys = new Set(merged.map((item) => item.sourceKey))
for (const [sourceKey, configured] of configuredMap.entries()) { for (const [sourceKey, configured] of configuredMap.entries()) {
@@ -1,8 +1,5 @@
import { buildClaimUrl, createTaskClaimToken } from '../claim/claim-service.js' import { buildClaimUrl, createTaskClaimToken } from '../claim/claim-service.js'
import { import { maskCode as maskCodeValue, maskPhone as maskPhoneValue } from '../../utils/masking.js'
maskCode as maskCodeValue,
maskPhone as maskPhoneValue,
} from '../../utils/masking.js'
import type { TaskRow } from '../../types/repository/rows.js' import type { TaskRow } from '../../types/repository/rows.js'
@@ -31,7 +28,9 @@ export function isRecoverableTaskSessionCloseError(error: ErrorLike | null | und
} }
export function normalizeManualDispatchOutcome(value: unknown): 'delivered' | 'failed' { export function normalizeManualDispatchOutcome(value: unknown): 'delivered' | 'failed' {
const normalized = String(value || '').trim().toLowerCase() const normalized = String(value || '')
.trim()
.toLowerCase()
if (normalized === 'failed') { if (normalized === 'failed') {
return 'failed' return 'failed'
@@ -25,18 +25,13 @@ import {
refreshFulfillmentRole, refreshFulfillmentRole,
returnFulfillmentNumber, returnFulfillmentNumber,
} from '../../fulfillment/executors/registry.js' } from '../../fulfillment/executors/registry.js'
import { import { isKuaishouCloudTask, normalizeKuaishouCloudFlow } from './kuaishou-cloud-helpers.js'
isKuaishouCloudTask,
normalizeKuaishouCloudFlow,
} from './kuaishou-cloud-helpers.js'
import type { import type {
AdminEntityIdInput, AdminEntityIdInput,
AdminViewerSessionInput, AdminViewerSessionInput,
} from '../../../types/admin/read-inputs.js' } from '../../../types/admin/read-inputs.js'
import type { import type { AdminTaskKuaishouIndustryConsumeInput } from '../../../types/admin/write-inputs.js'
AdminTaskKuaishouIndustryConsumeInput,
} from '../../../types/admin/write-inputs.js'
import type { AdminTaskActionResponse } from '../../../types/admin/write-models.js' import type { AdminTaskActionResponse } from '../../../types/admin/write-models.js'
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../../types/repository/rows.js' import type { KuaishouIndustryVoucherRow, TaskRow } from '../../../types/repository/rows.js'
@@ -491,7 +486,9 @@ export async function consumeAdminTaskKuaishouIndustryVoucher(
return { task: mapTaskActionPayload(updatedTask || consumeTargetTask) } return { task: mapTaskActionPayload(updatedTask || consumeTargetTask) }
} }
async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise<KuaishouIndustryVoucherRow> { async function getRequiredIndustryVoucherForTask(
task: TaskRow,
): Promise<KuaishouIndustryVoucherRow> {
const vouchers = await listKuaishouIndustryVouchersByTaskId(task.id) const vouchers = await listKuaishouIndustryVouchersByTaskId(task.id)
const firstVoucher = vouchers[0] const firstVoucher = vouchers[0]
if (firstVoucher) { if (firstVoucher) {
@@ -499,7 +496,9 @@ async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise<Kuaisho
} }
const taskContext = parseTaskContext(task) const taskContext = parseTaskContext(task)
const voucherContext = normalizeAdminTaskIndustryVoucherContext(taskContext.kuaishouIndustryVoucher) const voucherContext = normalizeAdminTaskIndustryVoucherContext(
taskContext.kuaishouIndustryVoucher,
)
const voucherCode = String(voucherContext.voucherCode || voucherContext.eticketId || '').trim() const voucherCode = String(voucherContext.voucherCode || voucherContext.eticketId || '').trim()
if (voucherCode) { if (voucherCode) {
const voucher = await findKuaishouIndustryVoucherByCode(voucherCode, task.platform_order_id) const voucher = await findKuaishouIndustryVoucherByCode(voucherCode, task.platform_order_id)
@@ -539,12 +538,12 @@ async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise<Kuaisho
} }
function normalizeAdminTaskIndustryVoucherContext(value: unknown): JsonObject { function normalizeAdminTaskIndustryVoucherContext(value: unknown): JsonObject {
return value && typeof value === 'object' && !Array.isArray(value) return value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {}
? value as JsonObject
: {}
} }
async function resolveIndustryVouchersForOrder(platformOrderId: string): Promise<KuaishouIndustryVoucherRow[]> { async function resolveIndustryVouchersForOrder(
platformOrderId: string,
): Promise<KuaishouIndustryVoucherRow[]> {
const normalizedOid = String(platformOrderId || '').trim() const normalizedOid = String(platformOrderId || '').trim()
if (!normalizedOid) { if (!normalizedOid) {
return [] return []
@@ -52,7 +52,9 @@ export type PreparedKuaishouCloudBindResource = {
bindUrl: string bindUrl: string
} }
export function resolvePersistedCloudtentaclesContext(sourceKeys: unknown[]): CloudtentaclesContext { export function resolvePersistedCloudtentaclesContext(
sourceKeys: unknown[],
): CloudtentaclesContext {
try { try {
return resolvePersistedCloudtentaclesContextBySourceKeys(sourceKeys) return resolvePersistedCloudtentaclesContextBySourceKeys(sourceKeys)
} catch (error) { } catch (error) {
@@ -4,14 +4,8 @@ import { createTaskEvent } from '../../../repositories/task-event-repo.js'
import { createHttpError } from '../../../utils/http.js' import { createHttpError } from '../../../utils/http.js'
import { nowIso } from '../../../utils/time.js' import { nowIso } from '../../../utils/time.js'
import { TASK_STATUS } from '../../../domain/task-status.js' import { TASK_STATUS } from '../../../domain/task-status.js'
import { import { canViewerCloseTask, createAdminViewerContext } from '../admin-read-shared-helpers.js'
canViewerCloseTask, import { getRequiredTask, mapTaskActionPayload } from '../admin-task-read-helpers.js'
createAdminViewerContext,
} from '../admin-read-shared-helpers.js'
import {
getRequiredTask,
mapTaskActionPayload,
} from '../admin-task-read-helpers.js'
import type { import type {
AdminEntityIdInput, AdminEntityIdInput,
@@ -62,7 +56,10 @@ export async function closeAdminTask(
}) })
} }
await createTaskEvent(task.id, 'task_closed', { await createTaskEvent(
task.id,
'task_closed',
{
closedBy: session closedBy: session
? { ? {
userId: session.userId, userId: session.userId,
@@ -71,7 +68,9 @@ export async function closeAdminTask(
} }
: null, : null,
claimTokenClosed: claimTokenId > 0, claimTokenClosed: claimTokenId > 0,
}, now) },
now,
)
return { return {
task: mapTaskActionPayload(updatedTask), task: mapTaskActionPayload(updatedTask),
@@ -69,7 +69,11 @@ export function getClaimIdentityFromTask(task: Partial<TaskRow> | null | undefin
} }
export function hasClaimExpectedUid(taskOrContext: unknown): boolean { export function hasClaimExpectedUid(taskOrContext: unknown): boolean {
if (taskOrContext && typeof taskOrContext === 'object' && 'context_json' in (taskOrContext as object)) { if (
taskOrContext &&
typeof taskOrContext === 'object' &&
'context_json' in (taskOrContext as object)
) {
return Boolean(getClaimIdentityFromTask(taskOrContext as TaskRow).expectedUid) return Boolean(getClaimIdentityFromTask(taskOrContext as TaskRow).expectedUid)
} }
return Boolean(getClaimIdentityFromContext(taskOrContext).expectedUid) return Boolean(getClaimIdentityFromContext(taskOrContext).expectedUid)
@@ -158,16 +162,11 @@ export function buildClaimIdentityAdminSummary(
role.name || binding.roleName || options.taskRoleName || '', role.name || binding.roleName || options.taskRoleName || '',
).trim() ).trim()
const boundUid = useShipped ? shippedUid : liveBoundUid const boundUid = useShipped ? shippedUid : liveBoundUid
const boundRoleName = useShipped const boundRoleName = useShipped ? shippedRoleName || liveBoundRoleName : liveBoundRoleName
? shippedRoleName || liveBoundRoleName
: liveBoundRoleName
const ready = Boolean(identity.expectedUid) const ready = Boolean(identity.expectedUid)
const uidMatched = ready && boundUid ? isClaimUidMatched(identity.expectedUid, boundUid) : null const uidMatched = ready && boundUid ? isClaimUidMatched(identity.expectedUid, boundUid) : null
const liveMismatched = const liveMismatched =
useShipped && useShipped && liveBoundUid && shippedUid && !isClaimUidMatched(shippedUid, liveBoundUid)
liveBoundUid &&
shippedUid &&
!isClaimUidMatched(shippedUid, liveBoundUid)
let note = '旧单或未提交 UID:用户须先打开领取页填写 UID,否则禁止自动发货' let note = '旧单或未提交 UID:用户须先打开领取页填写 UID,否则禁止自动发货'
if (ready) { if (ready) {
@@ -90,8 +90,5 @@ test('assertBoundUidMatchesExpected 在不匹配时抛错', () => {
}) })
test('assertClaimExpectedUidReady 要求 claimIdentity', () => { test('assertClaimExpectedUidReady 要求 claimIdentity', () => {
assert.throws( assert.throws(() => assertClaimExpectedUidReady({ context_json: '{}' }), /填写游戏 UID/)
() => assertClaimExpectedUidReady({ context_json: '{}' }),
/填写游戏 UID/,
)
}) })
@@ -42,9 +42,7 @@ export function resolveClaimTokenExpiration(
tokenTtlHours: unknown, tokenTtlHours: unknown,
): string | null { ): string | null {
const ttlHours = Number(tokenTtlHours) const ttlHours = Number(tokenTtlHours)
return Number.isFinite(ttlHours) && ttlHours > 0 return Number.isFinite(ttlHours) && ttlHours > 0 ? addHours(createdAt, ttlHours) : null
? addHours(createdAt, ttlHours)
: null
} }
export function buildClaimUrl(token: string): string { export function buildClaimUrl(token: string): string {
@@ -157,7 +157,12 @@ export function buildClaimDetailPayload({ claimToken, task, order, orderItem }:
} }
} }
function buildAffiliateDashClaimDetailPayload({ claimToken, task, order, orderItem }: ClaimContext) { function buildAffiliateDashClaimDetailPayload({
claimToken,
task,
order,
orderItem,
}: ClaimContext) {
const context = parseTaskContext(task) const context = parseTaskContext(task)
const claimIdentity = buildClaimIdentityPayload(context.claimIdentity) const claimIdentity = buildClaimIdentityPayload(context.claimIdentity)
const flow = normalizeAffiliateDashFlow(context.affiliateDash) const flow = normalizeAffiliateDashFlow(context.affiliateDash)
@@ -169,11 +174,13 @@ function buildAffiliateDashClaimDetailPayload({ claimToken, task, order, orderIt
skuCode: String(orderItem.sku_code || '').trim(), skuCode: String(orderItem.sku_code || '').trim(),
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1), quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
isBundle: false, isBundle: false,
items: [{ items: [
{
cloudSkuId: 0, cloudSkuId: 0,
name: productTitle, name: productTitle,
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1), quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
}], },
],
} }
return { return {
@@ -227,7 +234,12 @@ function buildAffiliateDashClaimDetailPayload({ claimToken, task, order, orderIt
} }
} }
function buildKuaishouFeifeiClaimDetailPayload({ claimToken, task, order, orderItem }: ClaimContext) { function buildKuaishouFeifeiClaimDetailPayload({
claimToken,
task,
order,
orderItem,
}: ClaimContext) {
const context = parseTaskContext(task) const context = parseTaskContext(task)
const claimIdentity = buildClaimIdentityPayload(context.claimIdentity) const claimIdentity = buildClaimIdentityPayload(context.claimIdentity)
const expectedUid = getClaimIdentityFromContext(context).expectedUid const expectedUid = getClaimIdentityFromContext(context).expectedUid
@@ -237,11 +249,13 @@ function buildKuaishouFeifeiClaimDetailPayload({ claimToken, task, order, orderI
skuCode: String(orderItem.sku_code || '').trim(), skuCode: String(orderItem.sku_code || '').trim(),
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1), quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
isBundle: false, isBundle: false,
items: [{ items: [
{
cloudSkuId: 0, cloudSkuId: 0,
name: String(flow.productName || orderItem.sku_name || orderItem.sku_code || '').trim(), name: String(flow.productName || orderItem.sku_name || orderItem.sku_code || '').trim(),
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1), quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
}], },
],
} }
return { return {
@@ -362,13 +376,16 @@ function buildClaimProductPayload(
quantity: item.quantity, quantity: item.quantity,
})) }))
.filter((item) => item.name) .filter((item) => item.name)
const normalizedDeliveryItems = deliveryItems.length > 0 const normalizedDeliveryItems =
deliveryItems.length > 0
? mergeClaimProductItems(deliveryItems) ? mergeClaimProductItems(deliveryItems)
: [{ : [
{
cloudSkuId: 0, cloudSkuId: 0,
name: displaySkuName, name: displaySkuName,
quantity: Math.max(1, Number(orderItem.quantity || 1) || 1), quantity: Math.max(1, Number(orderItem.quantity || 1) || 1),
}] },
]
return { return {
title: displaySkuName, title: displaySkuName,
@@ -452,7 +469,9 @@ export function mapClaimKuaishouCloudFulfillment(task: TaskRow, order: OrderRow)
binding: { binding: {
prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending', prepareStatus: String(binding.prepareStatus || 'pending').trim() || 'pending',
cloudSourceKeys: Array.isArray(binding.cloudSourceKeys) cloudSourceKeys: Array.isArray(binding.cloudSourceKeys)
? binding.cloudSourceKeys.map((value: unknown) => String(value || '').trim()).filter(Boolean) ? binding.cloudSourceKeys
.map((value: unknown) => String(value || '').trim())
.filter(Boolean)
: [], : [],
resolvedSourceKey: String(binding.resolvedSourceKey || '').trim(), resolvedSourceKey: String(binding.resolvedSourceKey || '').trim(),
skuId: Number(binding.skuId || 0) || 0, skuId: Number(binding.skuId || 0) || 0,
@@ -566,7 +585,9 @@ function normalizeClaimDeliveryItems(value: unknown, binding: JsonObject) {
const rawItems = Array.isArray(value) ? value : [] const rawItems = Array.isArray(value) ? value : []
const items = rawItems const items = rawItems
.map((item) => normalizeClaimDeliveryItem(item)) .map((item) => normalizeClaimDeliveryItem(item))
.filter((item): item is { cloudSkuId: number; cloudSkuName: string; quantity: number } => Boolean(item)) .filter((item): item is { cloudSkuId: number; cloudSkuName: string; quantity: number } =>
Boolean(item),
)
if (items.length > 0) { if (items.length > 0) {
return mergeClaimDeliveryItems(items) return mergeClaimDeliveryItems(items)
@@ -577,11 +598,13 @@ function normalizeClaimDeliveryItems(value: unknown, binding: JsonObject) {
return [] return []
} }
return [{ return [
{
cloudSkuId, cloudSkuId,
cloudSkuName: String(binding.skuName || '').trim(), cloudSkuName: String(binding.skuName || '').trim(),
quantity: 1, quantity: 1,
}] },
]
} }
function normalizeClaimDeliveryItem(value: unknown) { function normalizeClaimDeliveryItem(value: unknown) {
@@ -633,12 +656,13 @@ async function expireClaimContext(claimToken: ClaimTokenRow, task: TaskRow) {
let nextTask = task let nextTask = task
if (!isTaskFinalStatus(task.task_status)) { if (!isTaskFinalStatus(task.task_status)) {
nextTask = await updateTask(task.id, { nextTask =
(await updateTask(task.id, {
task_status: TASK_STATUS.EXPIRED, task_status: TASK_STATUS.EXPIRED,
user_action_status: TASK_STATUS.EXPIRED, user_action_status: TASK_STATUS.EXPIRED,
last_error: '领取链接已过期', last_error: '领取链接已过期',
updated_at: now, updated_at: now,
}) || task })) || task
} }
return { return {
@@ -61,7 +61,9 @@ async function requireExecutorAction<T>(
} }
function isKuaishouCloudBindingReady(flow: ReturnType<typeof normalizeKuaishouCloudFlow>) { function isKuaishouCloudBindingReady(flow: ReturnType<typeof normalizeKuaishouCloudFlow>) {
return flow.binding.prepareStatus === 'ready' && Boolean(String(flow.binding.bindUrl || '').trim()) return (
flow.binding.prepareStatus === 'ready' && Boolean(String(flow.binding.bindUrl || '').trim())
)
} }
/** /**
@@ -160,7 +162,8 @@ async function verifyIndustryVoucherTicket(
shopId: context.order.shop_id, shopId: context.order.shop_id,
shopName: context.order.shop_name, shopName: context.order.shop_name,
autoConsumeEnabled: true, autoConsumeEnabled: true,
consumedAt: voucherContext.status === 'CONSUMED' consumedAt:
voucherContext.status === 'CONSUMED'
? voucherContext.consumedAt || flow.consume.consumedAt || now ? voucherContext.consumedAt || flow.consume.consumedAt || now
: flow.consume.consumedAt, : flow.consume.consumedAt,
}, },
@@ -242,8 +245,10 @@ export async function getKuaishouCloudClaimDetail(token: unknown): Promise<Claim
// 已核销也要走 verify:内部会补 prepare 绑定资源 // 已核销也要走 verify:内部会补 prepare 绑定资源
if (flow.consume.status !== 'success' || !isKuaishouCloudBindingReady(flow)) { if (flow.consume.status !== 'success' || !isKuaishouCloudBindingReady(flow)) {
const now = nowIso() const now = nowIso()
const prepared: ClaimDetailPayload | null = await verifyIndustryVoucherTicket(context, now) const prepared: ClaimDetailPayload | null = await verifyIndustryVoucherTicket(
.catch(() => null) context,
now,
).catch(() => null)
if (prepared) { if (prepared) {
return prepared return prepared
} }
@@ -255,15 +260,14 @@ export async function getKuaishouCloudClaimDetail(token: unknown): Promise<Claim
const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment) const flow = normalizeKuaishouCloudFlow(taskContext.kuaishouCloudFulfillment)
const needsIndustryVoucherPrepare = const needsIndustryVoucherPrepare =
hasUsableIndustryVoucher(taskContext) && hasUsableIndustryVoucher(taskContext) &&
( (flow.ticket.status !== 'verified' || !isKuaishouCloudBindingReady(flow))
flow.ticket.status !== 'verified' ||
!isKuaishouCloudBindingReady(flow)
)
if (needsIndustryVoucherPrepare) { if (needsIndustryVoucherPrepare) {
const now = nowIso() const now = nowIso()
const prepared: ClaimDetailPayload | null = await verifyIndustryVoucherTicket(context, now) const prepared: ClaimDetailPayload | null = await verifyIndustryVoucherTicket(
.catch(() => null) context,
now,
).catch(() => null)
if (prepared) { if (prepared) {
return prepared return prepared
} }
@@ -273,7 +277,8 @@ export async function getKuaishouCloudClaimDetail(token: unknown): Promise<Claim
if (!isKuaishouCloudMockTask(task)) { if (!isKuaishouCloudMockTask(task)) {
const latestFlow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment) const latestFlow = normalizeKuaishouCloudFlow(parseTaskContext(task).kuaishouCloudFulfillment)
if ( if (
(latestFlow.ticket.status === 'verified' || hasUsableIndustryVoucher(parseTaskContext(task))) && (latestFlow.ticket.status === 'verified' ||
hasUsableIndustryVoucher(parseTaskContext(task))) &&
!isKuaishouCloudBindingReady(latestFlow) !isKuaishouCloudBindingReady(latestFlow)
) { ) {
task = await ensureKuaishouCloudBindingPrepared(task, { task = await ensureKuaishouCloudBindingPrepared(task, {
@@ -923,10 +928,11 @@ function hasUsableIndustryVoucher(context: JsonObject = {}) {
} }
function normalizeIndustryVoucherContext(value: unknown): JsonObject { function normalizeIndustryVoucherContext(value: unknown): JsonObject {
const source = value && typeof value === 'object' && !Array.isArray(value) const source =
? value as JsonObject value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {}
: {} const status = String(source.status || 'UNUSED')
const status = String(source.status || 'UNUSED').trim().toUpperCase() .trim()
.toUpperCase()
return { return {
...source, ...source,
@@ -1,9 +1,6 @@
import fs from 'node:fs' import fs from 'node:fs'
import { import { listAppConfigEntries, upsertAppConfigEntry } from '../../repositories/app-config-repo.js'
listAppConfigEntries,
upsertAppConfigEntry,
} from '../../repositories/app-config-repo.js'
import { readJsonFile } from '../../utils/json-file-store.js' import { readJsonFile } from '../../utils/json-file-store.js'
type NormalizeJsonValue<T> = (value: unknown) => T type NormalizeJsonValue<T> = (value: unknown) => T
@@ -67,8 +67,8 @@ const JSON_CONFIG_MIGRATION_ITEMS: JsonConfigMigrationItem[] = [
] ]
export async function migrateJsonConfigFilesToDatabase() { export async function migrateJsonConfigFilesToDatabase() {
const migrated: Array<{ configKey: string, filePath: string }> = [] const migrated: Array<{ configKey: string; filePath: string }> = []
const skipped: Array<{ configKey: string, filePath: string, reason: string }> = [] const skipped: Array<{ configKey: string; filePath: string; reason: string }> = []
for (const item of JSON_CONFIG_MIGRATION_ITEMS) { for (const item of JSON_CONFIG_MIGRATION_ITEMS) {
const filePath = path.join(DATA_DIR, item.fileName) const filePath = path.join(DATA_DIR, item.fileName)
@@ -65,7 +65,11 @@ export function isDevMockEnabled(env: NodeJS.ProcessEnv = process.env): boolean
if (String(env.ENABLE_DEV_MOCK || '').trim() === '1') { if (String(env.ENABLE_DEV_MOCK || '').trim() === '1') {
return true return true
} }
if (String(env.ENABLE_DEV_MOCK || '').trim().toLowerCase() === 'true') { if (
String(env.ENABLE_DEV_MOCK || '')
.trim()
.toLowerCase() === 'true'
) {
return true return true
} }
return !isProductionLike(env) return !isProductionLike(env)
@@ -111,14 +115,16 @@ export function getDevMockStatus() {
} }
} }
export async function createLewanMockClaim(input: { export async function createLewanMockClaim(
input: {
step?: unknown step?: unknown
orderNo?: unknown orderNo?: unknown
productNo?: unknown productNo?: unknown
uid?: unknown uid?: unknown
items?: unknown items?: unknown
frontendBaseUrl?: unknown frontendBaseUrl?: unknown
} = {}): Promise<DevMockCreateResult> { } = {},
): Promise<DevMockCreateResult> {
assertDevMockEnabled() assertDevMockEnabled()
const step = normalizeLewanStep(input.step) const step = normalizeLewanStep(input.step)
@@ -192,7 +198,10 @@ export async function createLewanMockClaim(input: {
}) })
if (!task) { if (!task) {
throw createHttpError('履约任务创建失败', { statusCode: 500, errorCode: 'dev_mock_task_failed' }) throw createHttpError('履约任务创建失败', {
statusCode: 500,
errorCode: 'dev_mock_task_failed',
})
} }
const claimToken = await createTaskClaimToken(task.id) const claimToken = await createTaskClaimToken(task.id)
@@ -223,7 +232,8 @@ export async function createLewanMockClaim(input: {
}) })
} }
export async function createFeifeiMockClaim(input: { export async function createFeifeiMockClaim(
input: {
step?: unknown step?: unknown
orderNo?: unknown orderNo?: unknown
productName?: unknown productName?: unknown
@@ -231,7 +241,8 @@ export async function createFeifeiMockClaim(input: {
uid?: unknown uid?: unknown
h5Url?: unknown h5Url?: unknown
frontendBaseUrl?: unknown frontendBaseUrl?: unknown
} = {}): Promise<DevMockCreateResult> { } = {},
): Promise<DevMockCreateResult> {
assertDevMockEnabled() assertDevMockEnabled()
const step = normalizeFeifeiStep(input.step) const step = normalizeFeifeiStep(input.step)
@@ -296,7 +307,12 @@ export async function createFeifeiMockClaim(input: {
? TASK_STATUS.MANUAL_REVIEW ? TASK_STATUS.MANUAL_REVIEW
: TASK_STATUS.LINK_GENERATED, : TASK_STATUS.LINK_GENERATED,
deliveryStatus: step === 'completed' ? 'delivered' : 'pending', deliveryStatus: step === 'completed' ? 'delivered' : 'pending',
resultCode: step === 'completed' ? 'kuaishou_feifei_completed' : step === 'failed' ? 'kuaishou_feifei_status_40' : '', resultCode:
step === 'completed'
? 'kuaishou_feifei_completed'
: step === 'failed'
? 'kuaishou_feifei_status_40'
: '',
resultMessage: rechargeStatusLabel, resultMessage: rechargeStatusLabel,
claimToken: '', claimToken: '',
claimExpiresAt: null, claimExpiresAt: null,
@@ -349,7 +365,10 @@ export async function createFeifeiMockClaim(input: {
}) })
if (!task) { if (!task) {
throw createHttpError('履约任务创建失败', { statusCode: 500, errorCode: 'dev_mock_task_failed' }) throw createHttpError('履约任务创建失败', {
statusCode: 500,
errorCode: 'dev_mock_task_failed',
})
} }
const claimToken = await createTaskClaimToken(task.id) const claimToken = await createTaskClaimToken(task.id)
@@ -383,14 +402,16 @@ type AffiliateDashMockStep = 'uid' | 'bind' | 'submitted' | 'completed' | 'faile
* 生成 affiliate-dash 领取 mock:不调用真实 affiliate-dash 平台。 * 生成 affiliate-dash 领取 mock:不调用真实 affiliate-dash 平台。
* 上下文带 mock 标记,sync/refresh/submit 全部短路,领取页可走完四步。 * 上下文带 mock 标记,sync/refresh/submit 全部短路,领取页可走完四步。
*/ */
export async function createAffiliateDashMockClaim(input: { export async function createAffiliateDashMockClaim(
input: {
step?: unknown step?: unknown
orderNo?: unknown orderNo?: unknown
productName?: unknown productName?: unknown
productSku?: unknown productSku?: unknown
uid?: unknown uid?: unknown
frontendBaseUrl?: unknown frontendBaseUrl?: unknown
} = {}): Promise<DevMockCreateResult> { } = {},
): Promise<DevMockCreateResult> {
assertDevMockEnabled() assertDevMockEnabled()
const step = normalizeAffiliateDashStep(input.step) const step = normalizeAffiliateDashStep(input.step)
@@ -457,11 +478,7 @@ export async function createAffiliateDashMockClaim(input: {
? TASK_STATUS.REDEEMING ? TASK_STATUS.REDEEMING
: TASK_STATUS.LINK_GENERATED, : TASK_STATUS.LINK_GENERATED,
deliveryStatus: deliveryStatus:
step === 'completed' step === 'completed' ? 'delivered' : step === 'submitted' ? 'delivering' : 'pending',
? 'delivered'
: step === 'submitted'
? 'delivering'
: 'pending',
resultCode: resultCode:
step === 'completed' step === 'completed'
? 'affiliate_dash_delivered' ? 'affiliate_dash_delivered'
@@ -541,7 +558,10 @@ export async function createAffiliateDashMockClaim(input: {
}) })
if (!task) { if (!task) {
throw createHttpError('履约任务创建失败', { statusCode: 500, errorCode: 'dev_mock_task_failed' }) throw createHttpError('履约任务创建失败', {
statusCode: 500,
errorCode: 'dev_mock_task_failed',
})
} }
const claimToken = await createTaskClaimToken(task.id) const claimToken = await createTaskClaimToken(task.id)
@@ -573,9 +593,11 @@ export async function createAffiliateDashMockClaim(input: {
* 生成电子凭证列表测试数据,不调用快手接口。 * 生成电子凭证列表测试数据,不调用快手接口。
* 覆盖未使用、已核销、已销毁和发码失败等运营页面常见状态。 * 覆盖未使用、已核销、已销毁和发码失败等运营页面常见状态。
*/ */
export async function createKuaishouIndustryVoucherMockData(input: { export async function createKuaishouIndustryVoucherMockData(
input: {
sellerId?: unknown sellerId?: unknown
} = {}): Promise<DevMockKuaishouIndustryVoucherResult> { } = {},
): Promise<DevMockKuaishouIndustryVoucherResult> {
assertDevMockEnabled() assertDevMockEnabled()
const now = nowIso() const now = nowIso()
@@ -668,11 +690,13 @@ export async function createKuaishouIndustryVoucherMockData(input: {
/** /**
* 生成 91 查询请求体(带签名),可选对本机发起查询。 * 生成 91 查询请求体(带签名),可选对本机发起查询。
*/ */
export async function buildOpen91QueryMock(input: { export async function buildOpen91QueryMock(
input: {
orderNo?: unknown orderNo?: unknown
execute?: unknown execute?: unknown
baseUrl?: unknown baseUrl?: unknown
} = {}) { } = {},
) {
assertDevMockEnabled() assertDevMockEnabled()
const orderNo = String(input.orderNo || '').trim() const orderNo = String(input.orderNo || '').trim()
@@ -767,7 +791,10 @@ async function ensureProfile(profileKey: string, name: string, requiresClaim: bo
}) })
if (!profile) { if (!profile) {
throw createHttpError('履约配置创建失败', { statusCode: 500, errorCode: 'dev_mock_profile_failed' }) throw createHttpError('履约配置创建失败', {
statusCode: 500,
errorCode: 'dev_mock_profile_failed',
})
} }
return profile return profile
} }
@@ -885,7 +912,10 @@ async function createBaseOrderItem(input: {
]) ])
if (!orderItem) { if (!orderItem) {
throw createHttpError('订单商品创建失败', { statusCode: 500, errorCode: 'dev_mock_item_failed' }) throw createHttpError('订单商品创建失败', {
statusCode: 500,
errorCode: 'dev_mock_item_failed',
})
} }
return orderItem return orderItem
} }
@@ -931,9 +961,7 @@ function buildLewanMockContext(input: {
configId: `mock:${input.productName}`, configId: `mock:${input.productName}`,
internalSkuCode: input.productName, internalSkuCode: input.productName,
internalSkuName: input.productName, internalSkuName: input.productName,
deliveryItems: input.deliveryItems.length deliveryItems: input.deliveryItems.length ? input.deliveryItems : [primaryItem],
? input.deliveryItems
: [primaryItem],
mock: { mock: {
enabled: true, enabled: true,
orderNo: input.orderNo, orderNo: input.orderNo,
@@ -960,9 +988,7 @@ function buildLewanMockContext(input: {
vnKey: '1', vnKey: '1',
vnId: roleReady ? 900001 : 0, vnId: roleReady ? 900001 : 0,
vnPhone: roleReady ? '13800000000' : '', vnPhone: roleReady ? '13800000000' : '',
bindUrl: roleReady bindUrl: roleReady ? `https://example.com/mock-kuaishou-cloud-bind/${input.orderNo}` : '',
? `https://example.com/mock-kuaishou-cloud-bind/${input.orderNo}`
: '',
bindPreparedAt: roleReady ? input.timestamp : null, bindPreparedAt: roleReady ? input.timestamp : null,
bindExpiresAt: roleReady ? addHours(input.timestamp, 24) : null, bindExpiresAt: roleReady ? addHours(input.timestamp, 24) : null,
bindProbeAt: roleReady ? input.timestamp : null, bindProbeAt: roleReady ? input.timestamp : null,
@@ -1190,7 +1216,10 @@ function buildAffiliateDashTips(step: AffiliateDashMockStep, uid: string) {
return [`打开领取链接,Step1 填 UID${uid}(或自定义)`, '提交后进入绑定步,mock 会给出二维码'] return [`打开领取链接,Step1 填 UID${uid}(或自定义)`, '提交后进入绑定步,mock 会给出二维码']
} }
if (step === 'bind') { if (step === 'bind') {
return [`已预填 UID=${uid},绑定二维码为 mock 生成(扫描无效)`, '正常流程:扫码完成真实绑定后自动进入下一步'] return [
`已预填 UID=${uid},绑定二维码为 mock 生成(扫描无效)`,
'正常流程:扫码完成真实绑定后自动进入下一步',
]
} }
if (step === 'submitted') { if (step === 'submitted') {
return ['已模拟绑定成功,可点「提交发货」(mock 直接模拟发货成功)'] return ['已模拟绑定成功,可点「提交发货」(mock 直接模拟发货成功)']

Some files were not shown because too many files have changed in this diff Show More