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