From 5c7c3e14e355c1c258020c5f8d343e557260c62e Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sun, 16 Aug 2026 17:27:12 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=89=8D=E5=90=8E=E7=AB=AF?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=A0=BC=E5=BC=8F=E5=8C=96=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .editorconfig | 13 + .gitea/workflows/quality.yml | 36 + .prettierignore | 8 + .../.prettierrc.json => .prettierrc.json | 1 + .vscode/extensions.json | 4 + .vscode/settings.json | 5 + README.md | 11 +- apps/backend/package-lock.json | 2 +- apps/backend/package.json | 6 +- apps/backend/scripts/cleanup-dev-data.ts | 11 +- apps/backend/scripts/curl-send-callback.ts | 134 ++-- apps/backend/scripts/gen-test-cases.ts | 30 +- apps/backend/scripts/migration-status.ts | 10 +- .../scripts/mock-kuaishou-cloud-claim.ts | 4 +- apps/backend/scripts/mock-open91-order.ts | 44 +- .../scripts/test-kuaishou-industry-http.ts | 223 ++++-- .../backend/scripts/test-kuaishou-industry.ts | 54 +- apps/backend/scripts/verify-aliyun-sms.ts | 33 +- apps/backend/src/config/runtime-env.ts | 72 +- apps/backend/src/config/runtime.ts | 23 +- apps/backend/src/db/migrate.ts | 4 +- apps/backend/src/domain/task-status.test.ts | 15 +- apps/backend/src/domain/task-status.ts | 5 +- apps/backend/src/domain/work-order-status.ts | 4 +- apps/backend/src/index.ts | 80 +- apps/backend/src/middleware/access-log.ts | 55 +- apps/backend/src/middleware/cors.ts | 47 +- .../backend/src/middleware/rate-limit.test.ts | 131 ++-- apps/backend/src/middleware/rate-limit.ts | 111 +-- .../src/repositories/admin-audit-log-repo.ts | 19 +- .../src/repositories/admin-user-repo.ts | 34 +- .../src/repositories/claim-token-repo.ts | 27 +- .../repositories/fulfillment-profile-repo.ts | 11 +- .../kuaishou-cloud-task-state-repo.test.ts | 49 +- .../src/repositories/order-item-repo.test.ts | 10 +- .../src/repositories/order-item-repo.ts | 4 +- .../src/repositories/task-event-repo.ts | 7 +- apps/backend/src/repositories/task-repo.ts | 99 ++- .../repositories/worker-platform/shared.ts | 1 - .../src/repositories/worker-platform/types.ts | 2 - .../worker-platform/work-order-repo.ts | 115 +-- .../worker-platform/worker-repo.ts | 52 +- apps/backend/src/routes/admin/audit-logs.ts | 10 +- apps/backend/src/routes/admin/auth.ts | 57 +- .../routes/admin/cloudtentacles-records.ts | 19 +- apps/backend/src/routes/admin/dashboard.ts | 20 +- apps/backend/src/routes/admin/dev-mock.ts | 6 +- .../src/routes/admin/kuaishou-industry.ts | 28 +- apps/backend/src/routes/admin/orders.ts | 44 +- .../src/routes/admin/platform-config.ts | 38 +- .../admin/platform-config/affiliate-dash.ts | 125 ++- .../admin/platform-config/cloudtentacles.ts | 525 ++++++------- .../platform-config/fulfillment-routing.ts | 50 +- .../admin/platform-config/kuaishou-feifei.ts | 212 +++-- .../routes/admin/platform-config/ninetyone.ts | 88 +-- .../admin/platform-config/notifications.ts | 198 +++-- apps/backend/src/routes/admin/session.ts | 11 +- apps/backend/src/routes/admin/tasks.ts | 9 +- apps/backend/src/routes/admin/users.ts | 151 ++-- .../src/routes/admin/worker-platform.ts | 39 +- apps/backend/src/routes/affiliate-dash.ts | 15 +- apps/backend/src/routes/collect.ts | 27 +- apps/backend/src/routes/kuaishou-feifei.ts | 15 +- apps/backend/src/routes/kuaishou-industry.ts | 60 +- apps/backend/src/routes/open-91.ts | 30 +- apps/backend/src/routes/short-links.ts | 17 +- apps/backend/src/routes/worker.ts | 52 +- apps/backend/src/routes/worker/session.ts | 6 +- .../src/services/admin/admin-audit-service.ts | 12 +- .../src/services/admin/admin-auth-service.ts | 74 +- .../services/admin/admin-dashboard-service.ts | 11 +- .../services/admin/admin-login-log-service.ts | 23 +- .../admin/admin-order-read-helpers.ts | 10 +- .../src/services/admin/admin-read-service.ts | 52 +- .../admin/admin-read-shared-helpers.test.ts | 10 +- .../admin/admin-read-shared-helpers.ts | 8 +- .../services/admin/admin-task-read-helpers.ts | 33 +- .../platform-config/affiliate-dash-service.ts | 8 +- .../cloudtentacles/context.test.ts | 8 +- .../platform-config/cloudtentacles/context.ts | 65 +- .../platform-config/cloudtentacles/index.ts | 741 +++++++++--------- .../cloudtentacles/mappers.test.ts | 6 +- .../platform-config/cloudtentacles/mappers.ts | 34 +- .../cloudtentacles/payloads.ts | 112 ++- .../platform-config/cloudtentacles/writes.ts | 21 +- .../kuaishou-feifei-service.ts | 13 +- .../kuaishou-industry-service.ts | 105 ++- .../platform-config/notification-service.ts | 112 +-- .../src/services/admin/write-helpers.ts | 9 +- .../admin/write/kuaishou-cloud-actions.ts | 29 +- .../admin/write/kuaishou-cloud-helpers.ts | 4 +- .../src/services/admin/write/task-actions.ts | 35 +- .../src/services/claim/claim-identity.ts | 15 +- .../claim/claim-redeem-transition.test.ts | 5 +- .../src/services/claim/claim-service.ts | 4 +- .../claim/kuaishou-cloud-claim-context.ts | 120 +-- .../claim/kuaishou-cloud-claim-service.ts | 44 +- .../src/services/config/app-config-store.ts | 5 +- .../config/json-config-migration-service.ts | 4 +- .../src/services/dev-mock/dev-mock-service.ts | 159 ++-- .../fulfillment/affiliate-dash/index.test.ts | 5 +- .../fulfillment/affiliate-dash/index.ts | 70 +- .../fulfillment/delivery-link-service.test.ts | 54 +- .../fulfillment/delivery-link-service.ts | 4 +- .../executors/kuaishou-cloud-executor.ts | 9 +- .../fulfillment/executors/registry.ts | 35 +- .../services/fulfillment/executors/types.ts | 12 +- .../kuaishou-cloud/account-selector.test.ts | 19 +- .../kuaishou-cloud/binding-freeze.test.ts | 10 +- .../kuaishou-cloud/binding-resources.ts | 210 +++-- .../kuaishou-cloud/cloudtentacles-context.ts | 98 ++- .../kuaishou-cloud/confirm-role.ts | 7 +- .../kuaishou-cloud/dispatch-fulfillment.ts | 11 +- .../kuaishou-cloud/dispatch-role-sync.ts | 94 +-- .../kuaishou-cloud/dispatch-types.ts | 5 +- .../fulfillment/kuaishou-cloud/domain.ts | 15 +- .../kuaishou-cloud/prepare-fulfillment.ts | 28 +- .../kuaishou-cloud/probe-bind-url.ts | 5 +- .../fulfillment/kuaishou-cloud/rebind-role.ts | 14 +- .../kuaishou-cloud/redeem-fulfillment.test.ts | 10 +- .../kuaishou-cloud/redeem-fulfillment.ts | 28 +- .../kuaishou-cloud/return-fulfillment.ts | 32 +- .../kuaishou-cloud/task-context.ts | 34 +- .../kuaishou-cloud/task-finalization.test.ts | 40 +- .../fulfillment/kuaishou-feifei/index.test.ts | 53 +- .../fulfillment/kuaishou-feifei/index.ts | 44 +- ...rder-fulfillment-readiness-service.test.ts | 4 +- .../order-fulfillment-readiness-service.ts | 4 +- .../src/services/fulfillment/planner.ts | 16 +- .../product-resolution-service.test.ts | 8 +- .../fulfillment/product-resolution-service.ts | 79 +- .../fulfillment/routing-config-service.ts | 40 +- .../src/services/notification/bark-service.ts | 15 +- .../notification/config-service.test.ts | 5 +- .../services/notification/config-service.ts | 17 +- .../notification/domain-notifications.ts | 37 +- .../notification/notification-service.ts | 126 +-- .../notification/wpush-service.test.ts | 11 +- .../services/notification/wpush-service.ts | 64 +- apps/backend/src/services/open-91/config.ts | 3 +- .../open-91/consume-on-deliver.test.ts | 16 +- .../services/open-91/consume-on-deliver.ts | 29 +- .../services/open-91/order-query-service.ts | 12 +- apps/backend/src/services/open-91/payload.ts | 5 +- .../src/services/open-91/protocol.test.ts | 16 +- apps/backend/src/services/open-91/response.ts | 21 +- .../cloudtentacles-name-match-service.ts | 50 +- ...oudtentacles-override-rule-service.test.ts | 4 +- .../cloudtentacles-override-rule-service.ts | 31 +- .../order/delivery-task-service.test.ts | 4 +- .../services/order/delivery-task-service.ts | 10 +- .../src/services/order/order-service.test.ts | 15 +- .../src/services/order/order-service.ts | 2 +- .../platforms/affiliate-dash/config.ts | 11 +- .../platforms/affiliate-dash/http-client.ts | 19 +- .../platforms/affiliate-dash/order-service.ts | 10 +- .../affiliate-dash/product-service.ts | 18 +- .../platforms/affiliate-dash/sign.test.ts | 5 +- .../services/platforms/affiliate-dash/sign.ts | 7 +- .../affiliate-dash/source-config-service.ts | 8 +- .../affiliate-dash/verify-callback.test.ts | 10 +- .../affiliate-dash/verify-callback.ts | 14 +- .../cloudtentacles/catalog-service.ts | 57 +- .../cloudtentacles/crypto-service.ts | 11 +- .../cloudtentacles/debug-flow-service.ts | 135 +++- .../platforms/cloudtentacles/defaults.ts | 17 +- .../platforms/cloudtentacles/helpers.ts | 106 ++- .../cloudtentacles/http-client.test.ts | 24 +- .../platforms/cloudtentacles/http-client.ts | 97 ++- .../cloudtentacles/knapsack-service.ts | 5 +- .../cloudtentacles/record-service.ts | 8 +- .../cloudtentacles/session-service.ts | 30 +- .../cloudtentacles/session-state-service.ts | 35 +- .../cloudtentacles/source-config-service.ts | 40 +- .../cloudtentacles/virtual-number-service.ts | 308 ++++++-- .../platforms/kuaishou-feifei/config.ts | 8 +- .../platforms/kuaishou-feifei/http-client.ts | 2 +- .../kuaishou-feifei/notify-service.ts | 23 +- .../kuaishou-feifei/order-service.ts | 34 +- .../kuaishou-feifei/product-rule-service.ts | 6 +- .../kuaishou-feifei/source-config-service.ts | 9 +- .../platforms/kuaishou-industry/config.ts | 6 +- .../consume-callback-service.test.ts | 22 +- .../kuaishou-industry/consume-code-service.ts | 15 +- .../platforms/kuaishou-industry/crypto.ts | 29 +- .../destroy-code-service.test.ts | 5 +- .../kuaishou-industry/destroy-code-service.ts | 31 +- .../kuaishou-industry/openapi-client.ts | 66 +- .../kuaishou-industry/openapi-values.ts | 4 +- .../platforms/kuaishou-industry/payload.ts | 39 +- .../kuaishou-industry/query-code-service.ts | 10 +- .../kuaishou-industry/refund-service.ts | 43 +- .../send-callback-service.ts | 51 +- .../send-code-service.test.ts | 22 +- .../kuaishou-industry/send-code-service.ts | 6 +- .../source-config-service.ts | 51 +- .../kuaishou-industry/token-service.test.ts | 89 ++- .../kuaishou-industry/token-service.ts | 133 ++-- .../voucher-binding-service.ts | 18 +- .../kuaishou-industry/voucher-service.test.ts | 15 +- .../kuaishou-industry/voucher-service.ts | 86 +- .../platforms/ninetyone/order-service.test.ts | 72 +- .../scheduler/cloudtentacles-health-job.ts | 75 +- .../cloudtentacles-number-recycle-service.ts | 14 +- .../src/services/scheduler/config-service.ts | 49 +- .../services/scheduler/scheduler-service.ts | 12 +- .../short-links/short-link-service.ts | 14 +- .../services/worker-platform/admin-service.ts | 107 ++- .../src/services/worker-platform/mappers.ts | 62 +- .../worker-finance-config-service.ts | 9 +- .../worker-platform-service.test.ts | 35 +- .../worker-platform/worker-service.ts | 144 ++-- apps/backend/src/startup/bootstrap.ts | 79 +- apps/backend/src/startup/shutdown.ts | 43 +- apps/backend/src/startup/state.ts | 48 +- apps/backend/src/types/admin/route-inputs.ts | 14 +- apps/backend/src/types/json.ts | 5 +- apps/backend/src/utils/http.ts | 223 +++--- apps/backend/src/utils/logger.ts | 12 +- apps/backend/src/utils/task-json.ts | 8 +- apps/backend/tsconfig.build.json | 8 +- apps/backend/tsconfig.json | 10 +- apps/frontend/package-lock.json | 17 + apps/frontend/package.json | 4 + .../src/components/admin/AdminDatePicker.tsx | 15 +- .../src/components/files/ImageUpload.tsx | 14 +- apps/frontend/src/domain/task-status.ts | 54 +- apps/frontend/src/layouts/AdminLayout.tsx | 4 +- apps/frontend/src/lib/feedback.ts | 6 +- apps/frontend/src/lib/http.ts | 31 +- .../src/pages/admin/AdminAuditLogsPage.tsx | 6 +- .../admin/AdminCloudtentaclesRecordsPage.tsx | 12 +- .../src/pages/admin/AdminDashboardPage.tsx | 18 +- .../src/pages/admin/AdminDevMockPage.tsx | 9 +- .../pages/admin/AdminKuaishouIndustryPage.tsx | 79 +- .../src/pages/admin/AdminOrdersPage.tsx | 4 +- .../src/pages/admin/AdminTaskDetailPage.tsx | 58 +- .../src/pages/admin/AdminTasksPage.tsx | 25 +- .../src/pages/admin/AdminUsersPage.tsx | 13 +- .../pages/admin/AdminWorkerPlatformPage.tsx | 10 +- .../pages/admin/panels/CategoriesPanel.tsx | 19 +- .../src/pages/admin/panels/FinancePanel.tsx | 55 +- .../src/pages/admin/panels/LevelsPanel.tsx | 19 +- .../pages/admin/panels/ProductRulesPanel.tsx | 35 +- .../pages/admin/panels/WorkOrdersPanel.tsx | 172 +--- .../src/pages/admin/panels/WorkersPanel.tsx | 22 +- .../src/pages/admin/panels/shared.tsx | 30 +- .../platform/AdminPlatformFulfillmentPage.tsx | 148 +++- .../admin/platform/AdminPlatformShopsPage.tsx | 469 +++++++---- .../pages/claim/ClaimAffiliateDashSteps.tsx | 8 +- .../src/pages/claim/ClaimFeifeiSteps.tsx | 22 +- .../src/pages/claim/ClaimHeaderCard.tsx | 5 +- .../src/pages/claim/ClaimLewanSteps.tsx | 11 +- .../frontend/src/pages/claim/ClaimUidStep.tsx | 4 +- .../src/pages/collect/CollectPage.tsx | 25 +- .../src/pages/worker/WorkerHallPage.tsx | 100 ++- .../src/pages/worker/WorkerLoginPage.tsx | 4 +- .../src/pages/worker/WorkerOrdersPage.tsx | 102 +-- .../src/pages/worker/WorkerProfilePage.tsx | 185 +++-- apps/frontend/src/services/admin/dashboard.ts | 16 +- .../src/services/admin/kuaishou-industry.ts | 4 +- .../admin/platform-config/affiliate-dash.ts | 14 +- .../admin/platform-config/cloudtentacles.ts | 30 +- .../platform-config/fulfillment-routing.ts | 4 +- .../admin/platform-config/kuaishou-feifei.ts | 14 +- .../platform-config/kuaishou-industry.ts | 8 +- .../src/services/admin/worker-platform.ts | 91 +-- apps/frontend/src/services/files.ts | 6 +- apps/frontend/src/services/worker.ts | 19 +- apps/frontend/src/types/admin/index.ts | 6 +- apps/frontend/src/types/claim.ts | 7 +- apps/frontend/src/utils/admin-pagination.ts | 32 +- apps/frontend/src/utils/image-identity.ts | 4 +- apps/frontend/src/utils/image-upload.ts | 15 +- apps/frontend/tests/worker-routes.test.ts | 5 +- apps/frontend/tsconfig.json | 5 +- apps/frontend/vite.config.ts | 8 +- docker-compose.dev.yml | 4 + 278 files changed, 6386 insertions(+), 5500 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitea/workflows/quality.yml create mode 100644 .prettierignore rename apps/backend/.prettierrc.json => .prettierrc.json (98%) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..6fe3ca16 --- /dev/null +++ b/.editorconfig @@ -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 + diff --git a/.gitea/workflows/quality.yml b/.gitea/workflows/quality.yml new file mode 100644 index 00000000..bc6f472e --- /dev/null +++ b/.gitea/workflows/quality.yml @@ -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 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..f41d57bf --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +**/node_modules +**/dist +**/.vite +**/coverage +**/data +**/package-lock.json +**/.env* + diff --git a/apps/backend/.prettierrc.json b/.prettierrc.json similarity index 98% rename from apps/backend/.prettierrc.json rename to .prettierrc.json index 3ee2d113..7c7a69e9 100644 --- a/apps/backend/.prettierrc.json +++ b/.prettierrc.json @@ -4,3 +4,4 @@ "printWidth": 100, "trailingComma": "all" } + diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..cbe46abc --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + "recommendations": ["esbenp.prettier-vscode", "editorconfig.editorconfig"] +} + diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..0f1a3dc9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true, + "prettier.configPath": ".prettierrc.json" +} diff --git a/README.md b/README.md index a9383777..bedd6ee7 100644 --- a/README.md +++ b/README.md @@ -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、手机号或生产配置 diff --git a/apps/backend/package-lock.json b/apps/backend/package-lock.json index e085689e..f8d1410b 100644 --- a/apps/backend/package-lock.json +++ b/apps/backend/package-lock.json @@ -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" } diff --git a/apps/backend/package.json b/apps/backend/package.json index 6bf4b261..0dd76307 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -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" } diff --git a/apps/backend/scripts/cleanup-dev-data.ts b/apps/backend/scripts/cleanup-dev-data.ts index 871157bf..9efef61d 100644 --- a/apps/backend/scripts/cleanup-dev-data.ts +++ b/apps/backend/scripts/cleanup-dev-data.ts @@ -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) { diff --git a/apps/backend/scripts/curl-send-callback.ts b/apps/backend/scripts/curl-send-callback.ts index 82680454..53b68e13 100644 --- a/apps/backend/scripts/curl-send-callback.ts +++ b/apps/backend/scripts/curl-send-callback.ts @@ -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 diff --git a/apps/backend/scripts/gen-test-cases.ts b/apps/backend/scripts/gen-test-cases.ts index 9729aee1..ffe695c2 100644 --- a/apps/backend/scripts/gen-test-cases.ts +++ b/apps/backend/scripts/gen-test-cases.ts @@ -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 diff --git a/apps/backend/scripts/migration-status.ts b/apps/backend/scripts/migration-status.ts index fabaadda..c9aa05ed 100644 --- a/apps/backend/scripts/migration-status.ts +++ b/apps/backend/scripts/migration-status.ts @@ -40,7 +40,11 @@ async function main() { let appliedNames = new Set() 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) diff --git a/apps/backend/scripts/mock-kuaishou-cloud-claim.ts b/apps/backend/scripts/mock-kuaishou-cloud-claim.ts index 2c4665c8..ca0ddc05 100644 --- a/apps/backend/scripts/mock-kuaishou-cloud-claim.ts +++ b/apps/backend/scripts/mock-kuaishou-cloud-claim.ts @@ -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) } diff --git a/apps/backend/scripts/mock-open91-order.ts b/apps/backend/scripts/mock-open91-order.ts index 77d6fc5e..c6b53cb1 100644 --- a/apps/backend/scripts/mock-open91-order.ts +++ b/apps/backend/scripts/mock-open91-order.ts @@ -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 }) diff --git a/apps/backend/scripts/test-kuaishou-industry-http.ts b/apps/backend/scripts/test-kuaishou-industry-http.ts index 48f5b8fb..2269ef20 100644 --- a/apps/backend/scripts/test-kuaishou-industry-http.ts +++ b/apps/backend/scripts/test-kuaishou-industry-http.ts @@ -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 { - 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 diff --git a/apps/backend/scripts/test-kuaishou-industry.ts b/apps/backend/scripts/test-kuaishou-industry.ts index bca6205a..3bb4256e 100644 --- a/apps/backend/scripts/test-kuaishou-industry.ts +++ b/apps/backend/scripts/test-kuaishou-industry.ts @@ -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, 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) => ({ - id: String(e.id), - num: 1, - goodsValue: 100, - })) + const destroyTargetIds = queryCodeResult.data.etickets + .slice(0, 1) + .map((e: Record) => ({ + id: String(e.id), + num: 1, + goodsValue: 100, + })) printStep(6, `通知商家销毁(指定卡券 id=${destroyTargetIds[0].id})`) const destroyBizParams: Record = { @@ -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) { 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}`) } diff --git a/apps/backend/scripts/verify-aliyun-sms.ts b/apps/backend/scripts/verify-aliyun-sms.ts index ddf46f97..eb5ea2cb 100644 --- a/apps/backend/scripts/verify-aliyun-sms.ts +++ b/apps/backend/scripts/verify-aliyun-sms.ts @@ -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)...`) diff --git a/apps/backend/src/config/runtime-env.ts b/apps/backend/src/config/runtime-env.ts index 706dd5aa..6f01802f 100644 --- a/apps/backend/src/config/runtime-env.ts +++ b/apps/backend/src/config/runtime-env.ts @@ -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(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 } } diff --git a/apps/backend/src/config/runtime.ts b/apps/backend/src/config/runtime.ts index c4366cfe..07930372 100644 --- a/apps/backend/src/config/runtime.ts +++ b/apps/backend/src/config/runtime.ts @@ -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)) diff --git a/apps/backend/src/db/migrate.ts b/apps/backend/src/db/migrate.ts index 1b0306ca..0f35c3b4 100644 --- a/apps/backend/src/db/migrate.ts +++ b/apps/backend/src/db/migrate.ts @@ -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))) diff --git a/apps/backend/src/domain/task-status.test.ts b/apps/backend/src/domain/task-status.test.ts index f5448df5..6fb8a406 100644 --- a/apps/backend/src/domain/task-status.test.ts +++ b/apps/backend/src/domain/task-status.test.ts @@ -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 查询的业务判断', () => { diff --git a/apps/backend/src/domain/task-status.ts b/apps/backend/src/domain/task-status.ts index d27a3499..6523c7b8 100644 --- a/apps/backend/src/domain/task-status.ts +++ b/apps/backend/src/domain/task-status.ts @@ -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 { diff --git a/apps/backend/src/domain/work-order-status.ts b/apps/backend/src/domain/work-order-status.ts index 28e162aa..e55ecca9 100644 --- a/apps/backend/src/domain/work-order-status.ts +++ b/apps/backend/src/domain/work-order-status.ts @@ -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([ WORK_ORDER_STATUS.ACCEPTED, diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index ffb144c5..15758f51 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -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 | null = null; +let shutdownController: ReturnType | 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) +}) diff --git a/apps/backend/src/middleware/access-log.ts b/apps/backend/src/middleware/access-log.ts index feb0ce56..b5044bd7 100644 --- a/apps/backend/src/middleware/access-log.ts +++ b/apps/backend/src/middleware/access-log.ts @@ -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 } diff --git a/apps/backend/src/middleware/cors.ts b/apps/backend/src/middleware/cors.ts index fae529ff..b514bf5a 100644 --- a/apps/backend/src/middleware/cors.ts +++ b/apps/backend/src/middleware/cors.ts @@ -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() + } } diff --git a/apps/backend/src/middleware/rate-limit.test.ts b/apps/backend/src/middleware/rate-limit.test.ts index 9da2755f..a7231e3c 100644 --- a/apps/backend/src/middleware/rate-limit.test.ts +++ b/apps/backend/src/middleware/rate-limit.test.ts @@ -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; - body: any; + statusCode: number + headers: Record + body: any } { const res = { statusCode: 200, headers: {} as Record, 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; - body: any; - }; + statusCode: number + headers: Record + body: any + } } diff --git a/apps/backend/src/middleware/rate-limit.ts b/apps/backend/src/middleware/rate-limit.ts index 2b9a2d41..ef6218c0 100644 --- a/apps/backend/src/middleware/rate-limit.ts +++ b/apps/backend/src/middleware/rate-limit.ts @@ -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(); -let lastCleanupAt = 0; +const buckets = new Map() +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) } } } diff --git a/apps/backend/src/repositories/admin-audit-log-repo.ts b/apps/backend/src/repositories/admin-audit-log-repo.ts index e828d349..1ce0a20f 100644 --- a/apps/backend/src/repositories/admin-audit-log-repo.ts +++ b/apps/backend/src/repositories/admin-audit-log-repo.ts @@ -39,7 +39,9 @@ type AdminAuditLogListResult = { total: number } -export async function createAdminAuditLog(input: AdminAuditLogCreateInput): Promise { +export async function createAdminAuditLog( + input: AdminAuditLogCreateInput, +): Promise { const result = await query( ` 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 { - const result = await query('SELECT * FROM admin_audit_logs WHERE id = $1 LIMIT 1', [Number(logId)]) +export async function getAdminAuditLogById( + logId: number | string, +): Promise { + const result = await query( + 'SELECT * FROM admin_audit_logs WHERE id = $1 LIMIT 1', + [Number(logId)], + ) return result.rows[0] || null } -export async function listAdminAuditLogs(queryInput: AdminAuditLogListInput = {}): Promise { +export async function listAdminAuditLogs( + queryInput: AdminAuditLogListInput = {}, +): Promise { 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, ) diff --git a/apps/backend/src/repositories/admin-user-repo.ts b/apps/backend/src/repositories/admin-user-repo.ts index b8f0f99d..e0910149 100644 --- a/apps/backend/src/repositories/admin-user-repo.ts +++ b/apps/backend/src/repositories/admin-user-repo.ts @@ -20,10 +20,12 @@ type AdminUserCreateInput = { updatedAt: string } -type AdminUserPatch = Partial> +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 { - const result = await query( - `${ADMIN_USER_SELECT} WHERE au.id = $1 LIMIT 1`, - [Number(userId)], - ) + const result = await query(`${ADMIN_USER_SELECT} WHERE au.id = $1 LIMIT 1`, [ + Number(userId), + ]) return result.rows[0] || null } export async function getAdminUserByUsername(username: string): Promise { - const result = await query( - `${ADMIN_USER_SELECT} WHERE au.username = $1 LIMIT 1`, - [String(username || '').trim().toLowerCase()], - ) + const result = await query(`${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 { - 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 { - 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) diff --git a/apps/backend/src/repositories/claim-token-repo.ts b/apps/backend/src/repositories/claim-token-repo.ts index a209c8d0..6f475ad3 100644 --- a/apps/backend/src/repositories/claim-token-repo.ts +++ b/apps/backend/src/repositories/claim-token-repo.ts @@ -26,12 +26,16 @@ type ClaimTokenCreateInput = { updatedAt: string } -type ClaimTokenPatch = Partial> +type ClaimTokenPatch = Partial< + Pick< + ClaimTokenRow, + 'status' | 'expired_at' | 'used_at' | 'max_use_count' | 'used_count' | 'updated_at' + > +> -export async function createClaimToken(input: ClaimTokenCreateInput): Promise { +export async function createClaimToken( + input: ClaimTokenCreateInput, +): Promise { const result = await query( ` INSERT INTO claim_tokens ( @@ -74,16 +78,23 @@ export async function createClaimToken(input: ClaimTokenCreateInput): Promise { - const result = await query('SELECT * FROM claim_tokens WHERE id = $1 LIMIT 1', [Number(tokenId)]) + const result = await query('SELECT * FROM claim_tokens WHERE id = $1 LIMIT 1', [ + Number(tokenId), + ]) return result.rows[0] || null } export async function findClaimTokenByToken(token: string): Promise { - const result = await query('SELECT * FROM claim_tokens WHERE token = $1 LIMIT 1', [String(token || '').trim()]) + const result = await query('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 { +export async function updateClaimToken( + tokenId: number | string, + patch: ClaimTokenPatch, +): Promise { const current = await getClaimTokenById(tokenId) if (!current) { return null diff --git a/apps/backend/src/repositories/fulfillment-profile-repo.ts b/apps/backend/src/repositories/fulfillment-profile-repo.ts index 901fc01f..fec24220 100644 --- a/apps/backend/src/repositories/fulfillment-profile-repo.ts +++ b/apps/backend/src/repositories/fulfillment-profile-repo.ts @@ -47,7 +47,9 @@ export type FulfillmentProfileRequirementInput = { configJson?: string | Record } -export async function getFulfillmentProfileByKey(profileKey: string): Promise { +export async function getFulfillmentProfileByKey( + profileKey: string, +): Promise { const result = await query( 'SELECT * FROM fulfillment_profiles WHERE profile_key = $1 LIMIT 1', [String(profileKey || '').trim()], @@ -104,10 +106,9 @@ export async function replaceFulfillmentProfileRequirements( timestamp: string, ): Promise { 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( diff --git a/apps/backend/src/repositories/kuaishou-cloud-task-state-repo.test.ts b/apps/backend/src/repositories/kuaishou-cloud-task-state-repo.test.ts index 76b0620c..c8a250b7 100644 --- a/apps/backend/src/repositories/kuaishou-cloud-task-state-repo.test.ts +++ b/apps/backend/src/repositories/kuaishou-cloud-task-state-repo.test.ts @@ -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/) diff --git a/apps/backend/src/repositories/order-item-repo.test.ts b/apps/backend/src/repositories/order-item-repo.test.ts index 4dbfc258..071b6ea9 100644 --- a/apps/backend/src/repositories/order-item-repo.test.ts +++ b/apps/backend/src/repositories/order-item-repo.test.ts @@ -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]) }) diff --git a/apps/backend/src/repositories/order-item-repo.ts b/apps/backend/src/repositories/order-item-repo.ts index 10299618..b58a6a68 100644 --- a/apps/backend/src/repositories/order-item-repo.ts +++ b/apps/backend/src/repositories/order-item-repo.ts @@ -204,6 +204,8 @@ async function listOrderItemsByOrderIdWithExecutor( } export async function getOrderItemById(orderItemId: number | string): Promise { - const result = await query('SELECT * FROM order_items WHERE id = $1 LIMIT 1', [Number(orderItemId)]) + const result = await query('SELECT * FROM order_items WHERE id = $1 LIMIT 1', [ + Number(orderItemId), + ]) return result.rows[0] || null } diff --git a/apps/backend/src/repositories/task-event-repo.ts b/apps/backend/src/repositories/task-event-repo.ts index c5507ada..ac0d44a7 100644 --- a/apps/backend/src/repositories/task-event-repo.ts +++ b/apps/backend/src/repositories/task-event-repo.ts @@ -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 diff --git a/apps/backend/src/repositories/task-repo.ts b/apps/backend/src/repositories/task-repo.ts index f5dc2846..7f5f63dd 100644 --- a/apps/backend/src/repositories/task-repo.ts +++ b/apps/backend/src/repositories/task-repo.ts @@ -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 { +export async function updateTask( + taskId: number | string, + patch: TaskUpdatePatch, +): Promise { 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 { +export async function findTaskByClaimTokenId( + claimTokenId: number | string, +): Promise { const result = await query( `${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), diff --git a/apps/backend/src/repositories/worker-platform/shared.ts b/apps/backend/src/repositories/worker-platform/shared.ts index dbd26e5f..3eba6b3b 100644 --- a/apps/backend/src/repositories/worker-platform/shared.ts +++ b/apps/backend/src/repositories/worker-platform/shared.ts @@ -2,7 +2,6 @@ import type { PoolClient } from 'pg' import type { WorkerWalletRow } from './types.js' - export async function ensureWorkerWalletWithClient( client: PoolClient, workerId: number, diff --git a/apps/backend/src/repositories/worker-platform/types.ts b/apps/backend/src/repositories/worker-platform/types.ts index ab335998..d90a7ca7 100644 --- a/apps/backend/src/repositories/worker-platform/types.ts +++ b/apps/backend/src/repositories/worker-platform/types.ts @@ -1,5 +1,3 @@ - - export type WorkerLevelRow = { id: number level_key: string diff --git a/apps/backend/src/repositories/worker-platform/work-order-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-repo.ts index f3881cfd..c9561236 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-repo.ts @@ -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 { +export async function getWorkCategoryById( + categoryId: number | string, +): Promise { const result = await query( '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( `${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 { +}: { + unfreezeId: number + now: string +}): Promise { return withTransaction(async (client) => { const currentResult = await client.query( ` @@ -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> { +export async function countTimeoutEventsByWorkerIds( + workerIds: number[], +): Promise> { const counts = new Map() 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 { +async function countWorkerActiveOrdersWithClient( + client: PoolClient, + workerId: number, +): Promise { 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) diff --git a/apps/backend/src/repositories/worker-platform/worker-repo.ts b/apps/backend/src/repositories/worker-platform/worker-repo.ts index d662aa87..6f9ea728 100644 --- a/apps/backend/src/repositories/worker-platform/worker-repo.ts +++ b/apps/backend/src/repositories/worker-platform/worker-repo.ts @@ -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 { - const result = await query( - `${WORKER_USER_SELECT} WHERE wu.phone = $1 LIMIT 1`, - [String(phone || '').trim()], - ) + const result = await query(`${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 { +export async function getWorkerUserByInviteCode(inviteCode: string): Promise { const result = await query( `${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) { diff --git a/apps/backend/src/routes/admin/audit-logs.ts b/apps/backend/src/routes/admin/audit-logs.ts index fa9a1ca2..c9f71cb7 100644 --- a/apps/backend/src/routes/admin/audit-logs.ts +++ b/apps/backend/src/routes/admin/audit-logs.ts @@ -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 diff --git a/apps/backend/src/routes/admin/auth.ts b/apps/backend/src/routes/admin/auth.ts index 86a1925f..a01937cd 100644 --- a/apps/backend/src/routes/admin/auth.ts +++ b/apps/backend/src/routes/admin/auth.ts @@ -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 diff --git a/apps/backend/src/routes/admin/cloudtentacles-records.ts b/apps/backend/src/routes/admin/cloudtentacles-records.ts index 1e98061d..5c4b4039 100644 --- a/apps/backend/src/routes/admin/cloudtentacles-records.ts +++ b/apps/backend/src/routes/admin/cloudtentacles-records.ts @@ -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) { diff --git a/apps/backend/src/routes/admin/dashboard.ts b/apps/backend/src/routes/admin/dashboard.ts index 4c89db47..1e2261b8 100644 --- a/apps/backend/src/routes/admin/dashboard.ts +++ b/apps/backend/src/routes/admin/dashboard.ts @@ -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 diff --git a/apps/backend/src/routes/admin/dev-mock.ts b/apps/backend/src/routes/admin/dev-mock.ts index c362d0e1..b559407a 100644 --- a/apps/backend/src/routes/admin/dev-mock.ts +++ b/apps/backend/src/routes/admin/dev-mock.ts @@ -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)', { diff --git a/apps/backend/src/routes/admin/kuaishou-industry.ts b/apps/backend/src/routes/admin/kuaishou-industry.ts index 383402bb..a0cfa648 100644 --- a/apps/backend/src/routes/admin/kuaishou-industry.ts +++ b/apps/backend/src/routes/admin/kuaishou-industry.ts @@ -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 : {} - const result = data && typeof data === 'object' ? data as Record : {} + const payload = body && typeof body === 'object' ? (body as Record) : {} + const result = data && typeof data === 'object' ? (data as Record) : {} return { action, diff --git a/apps/backend/src/routes/admin/orders.ts b/apps/backend/src/routes/admin/orders.ts index d23520eb..42217f12 100644 --- a/apps/backend/src/routes/admin/orders.ts +++ b/apps/backend/src/routes/admin/orders.ts @@ -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 : {}, + data: data && typeof data === 'object' ? (data as Record) : {}, }), }, ), diff --git a/apps/backend/src/routes/admin/platform-config.ts b/apps/backend/src/routes/admin/platform-config.ts index fedd8f53..85494c61 100644 --- a/apps/backend/src/routes/admin/platform-config.ts +++ b/apps/backend/src/routes/admin/platform-config.ts @@ -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 diff --git a/apps/backend/src/routes/admin/platform-config/affiliate-dash.ts b/apps/backend/src/routes/admin/platform-config/affiliate-dash.ts index d792f66e..bd982fd9 100644 --- a/apps/backend/src/routes/admin/platform-config/affiliate-dash.ts +++ b/apps/backend/src/routes/admin/platform-config/affiliate-dash.ts @@ -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 diff --git a/apps/backend/src/routes/admin/platform-config/cloudtentacles.ts b/apps/backend/src/routes/admin/platform-config/cloudtentacles.ts index 7edd814d..7e21a78e 100644 --- a/apps/backend/src/routes/admin/platform-config/cloudtentacles.ts +++ b/apps/backend/src/routes/admin/platform-config/cloudtentacles.ts @@ -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 diff --git a/apps/backend/src/routes/admin/platform-config/fulfillment-routing.ts b/apps/backend/src/routes/admin/platform-config/fulfillment-routing.ts index 7f5c7336..6fa8448b 100644 --- a/apps/backend/src/routes/admin/platform-config/fulfillment-routing.ts +++ b/apps/backend/src/routes/admin/platform-config/fulfillment-routing.ts @@ -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 diff --git a/apps/backend/src/routes/admin/platform-config/kuaishou-feifei.ts b/apps/backend/src/routes/admin/platform-config/kuaishou-feifei.ts index 24d8665f..3b9917bf 100644 --- a/apps/backend/src/routes/admin/platform-config/kuaishou-feifei.ts +++ b/apps/backend/src/routes/admin/platform-config/kuaishou-feifei.ts @@ -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 diff --git a/apps/backend/src/routes/admin/platform-config/ninetyone.ts b/apps/backend/src/routes/admin/platform-config/ninetyone.ts index d36db6ab..3361e68a 100644 --- a/apps/backend/src/routes/admin/platform-config/ninetyone.ts +++ b/apps/backend/src/routes/admin/platform-config/ninetyone.ts @@ -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 diff --git a/apps/backend/src/routes/admin/platform-config/notifications.ts b/apps/backend/src/routes/admin/platform-config/notifications.ts index 07f20686..e10c6c1f 100644 --- a/apps/backend/src/routes/admin/platform-config/notifications.ts +++ b/apps/backend/src/routes/admin/platform-config/notifications.ts @@ -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 diff --git a/apps/backend/src/routes/admin/session.ts b/apps/backend/src/routes/admin/session.ts index 11cc5c3d..0ed47a01 100644 --- a/apps/backend/src/routes/admin/session.ts +++ b/apps/backend/src/routes/admin/session.ts @@ -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 { +export async function requireAdminSession( + req: Request, + res: Response, + next: NextFunction, +): Promise { try { req.adminSession = await verifyAdminSessionToken(extractBearerToken(req)) next() diff --git a/apps/backend/src/routes/admin/tasks.ts b/apps/backend/src/routes/admin/tasks.ts index d1ca8a4d..93b3a1dd 100644 --- a/apps/backend/src/routes/admin/tasks.ts +++ b/apps/backend/src/routes/admin/tasks.ts @@ -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: '重发电子凭证发码回调失败', diff --git a/apps/backend/src/routes/admin/users.ts b/apps/backend/src/routes/admin/users.ts index 6f3c9b2f..e0f5cee8 100644 --- a/apps/backend/src/routes/admin/users.ts +++ b/apps/backend/src/routes/admin/users.ts @@ -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 diff --git a/apps/backend/src/routes/admin/worker-platform.ts b/apps/backend/src/routes/admin/worker-platform.ts index a5cbc4b4..ada78972 100644 --- a/apps/backend/src/routes/admin/worker-platform.ts +++ b/apps/backend/src/routes/admin/worker-platform.ts @@ -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) : {}, - }), - }), + 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) : {}, + }), + }, + ), ) 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: '扣减待解冻押金失败', diff --git a/apps/backend/src/routes/affiliate-dash.ts b/apps/backend/src/routes/affiliate-dash.ts index 937ea5a9..c91ab622 100644 --- a/apps/backend/src/routes/affiliate-dash.ts +++ b/apps/backend/src/routes/affiliate-dash.ts @@ -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]') } }) diff --git a/apps/backend/src/routes/collect.ts b/apps/backend/src/routes/collect.ts index 4ca99619..d46a86c4 100644 --- a/apps/backend/src/routes/collect.ts +++ b/apps/backend/src/routes/collect.ts @@ -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) => { diff --git a/apps/backend/src/routes/kuaishou-feifei.ts b/apps/backend/src/routes/kuaishou-feifei.ts index 11008c4d..a82b3153 100644 --- a/apps/backend/src/routes/kuaishou-feifei.ts +++ b/apps/backend/src/routes/kuaishou-feifei.ts @@ -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]') } }) diff --git a/apps/backend/src/routes/kuaishou-industry.ts b/apps/backend/src/routes/kuaishou-industry.ts index d5529da1..a18c1c6b 100644 --- a/apps/backend/src/routes/kuaishou-industry.ts +++ b/apps/backend/src/routes/kuaishou-industry.ts @@ -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)) } }) diff --git a/apps/backend/src/routes/open-91.ts b/apps/backend/src/routes/open-91.ts index 4320e4ee..a979fa46 100644 --- a/apps/backend/src/routes/open-91.ts +++ b/apps/backend/src/routes/open-91.ts @@ -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)) } }) diff --git a/apps/backend/src/routes/short-links.ts b/apps/backend/src/routes/short-links.ts index 831f47c8..9a42d0c0 100644 --- a/apps/backend/src/routes/short-links.ts +++ b/apps/backend/src/routes/short-links.ts @@ -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]') } }) diff --git a/apps/backend/src/routes/worker.ts b/apps/backend/src/routes/worker.ts index 4d4d0fd3..b3e1683b 100644 --- a/apps/backend/src/routes/worker.ts +++ b/apps/backend/src/routes/worker.ts @@ -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( diff --git a/apps/backend/src/routes/worker/session.ts b/apps/backend/src/routes/worker/session.ts index 5439a064..c704fcdd 100644 --- a/apps/backend/src/routes/worker/session.ts +++ b/apps/backend/src/routes/worker/session.ts @@ -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 { +export async function requireWorkerSession( + req: Request, + res: Response, + next: NextFunction, +): Promise { try { req.workerSession = await verifyWorkerSessionToken(extractWorkerBearerToken(req)) next() diff --git a/apps/backend/src/services/admin/admin-audit-service.ts b/apps/backend/src/services/admin/admin-audit-service.ts index 3ed239f4..6c3ee1d0 100644 --- a/apps/backend/src/services/admin/admin-audit-service.ts +++ b/apps/backend/src/services/admin/admin-audit-service.ts @@ -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 } diff --git a/apps/backend/src/services/admin/admin-auth-service.ts b/apps/backend/src/services/admin/admin-auth-service.ts index 2847c1bb..7da3da63 100644 --- a/apps/backend/src/services/admin/admin-auth-service.ts +++ b/apps/backend/src/services/admin/admin-auth-service.ts @@ -32,10 +32,14 @@ type AdminUserStatus = 'active' | 'disabled' export async function ensureAdminUsersBootstrapped(): Promise { 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 { 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 { +export async function resetManagedAdminUserPassword( + userId: number | string, + payload: JsonObject = {}, +): Promise { 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 { 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 diff --git a/apps/backend/src/services/admin/admin-dashboard-service.ts b/apps/backend/src/services/admin/admin-dashboard-service.ts index b3243a7f..67882967 100644 --- a/apps/backend/src/services/admin/admin-dashboard-service.ts +++ b/apps/backend/src/services/admin/admin-dashboard-service.ts @@ -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 diff --git a/apps/backend/src/services/admin/admin-login-log-service.ts b/apps/backend/src/services/admin/admin-login-log-service.ts index 0e60ff4c..9a7510fb 100644 --- a/apps/backend/src/services/admin/admin-login-log-service.ts +++ b/apps/backend/src/services/admin/admin-login-log-service.ts @@ -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(' · ') } diff --git a/apps/backend/src/services/admin/admin-order-read-helpers.ts b/apps/backend/src/services/admin/admin-order-read-helpers.ts index 17481412..a4a19651 100644 --- a/apps/backend/src/services/admin/admin-order-read-helpers.ts +++ b/apps/backend/src/services/admin/admin-order-read-helpers.ts @@ -34,7 +34,10 @@ export async function mapAdminOrderListItem(item: OrderListRow): Promise 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 diff --git a/apps/backend/src/services/admin/admin-read-service.ts b/apps/backend/src/services/admin/admin-read-service.ts index 2e8adbf6..efd2b654 100644 --- a/apps/backend/src/services/admin/admin-read-service.ts +++ b/apps/backend/src/services/admin/admin-read-service.ts @@ -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() { diff --git a/apps/backend/src/services/admin/admin-read-shared-helpers.test.ts b/apps/backend/src/services/admin/admin-read-shared-helpers.test.ts index 132e8dcc..b4a7562c 100644 --- a/apps/backend/src/services/admin/admin-read-shared-helpers.test.ts +++ b/apps/backend/src/services/admin/admin-read-shared-helpers.test.ts @@ -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') }) diff --git a/apps/backend/src/services/admin/admin-read-shared-helpers.ts b/apps/backend/src/services/admin/admin-read-shared-helpers.ts index c12a90a5..8664a3de 100644 --- a/apps/backend/src/services/admin/admin-read-shared-helpers.ts +++ b/apps/backend/src/services/admin/admin-read-shared-helpers.ts @@ -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', } } diff --git a/apps/backend/src/services/admin/admin-task-read-helpers.ts b/apps/backend/src/services/admin/admin-task-read-helpers.ts index a399a247..ef0b6113 100644 --- a/apps/backend/src/services/admin/admin-task-read-helpers.ts +++ b/apps/backend/src/services/admin/admin-task-read-helpers.ts @@ -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' } diff --git a/apps/backend/src/services/admin/platform-config/affiliate-dash-service.ts b/apps/backend/src/services/admin/platform-config/affiliate-dash-service.ts index 1a559d52..9a18de83 100644 --- a/apps/backend/src/services/admin/platform-config/affiliate-dash-service.ts +++ b/apps/backend/src/services/admin/platform-config/affiliate-dash-service.ts @@ -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, diff --git a/apps/backend/src/services/admin/platform-config/cloudtentacles/context.test.ts b/apps/backend/src/services/admin/platform-config/cloudtentacles/context.test.ts index 6d37b1f1..702dc17b 100644 --- a/apps/backend/src/services/admin/platform-config/cloudtentacles/context.test.ts +++ b/apps/backend/src/services/admin/platform-config/cloudtentacles/context.test.ts @@ -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, diff --git a/apps/backend/src/services/admin/platform-config/cloudtentacles/context.ts b/apps/backend/src/services/admin/platform-config/cloudtentacles/context.ts index 20d7dfe6..263c831a 100644 --- a/apps/backend/src/services/admin/platform-config/cloudtentacles/context.ts +++ b/apps/backend/src/services/admin/platform-config/cloudtentacles/context.ts @@ -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) - ); + ) } diff --git a/apps/backend/src/services/admin/platform-config/cloudtentacles/index.ts b/apps/backend/src/services/admin/platform-config/cloudtentacles/index.ts index df2403f5..00aeeb1c 100644 --- a/apps/backend/src/services/admin/platform-config/cloudtentacles/index.ts +++ b/apps/backend/src/services/admin/platform-config/cloudtentacles/index.ts @@ -6,7 +6,7 @@ import { saveCloudtentaclesSourceByKey, saveCloudtentaclesSourcesList, deleteCloudtentaclesSourceByKey, -} from "../../../platforms/cloudtentacles/source-config-service.js"; +} from '../../../platforms/cloudtentacles/source-config-service.js' import { clearCloudtentaclesSessionStateByKey, deleteCloudtentaclesSessionStateByKey, @@ -15,21 +15,21 @@ import { saveCloudtentaclesSessionStateByKey, getAllCloudtentaclesSessionStates, pruneCloudtentaclesSessionStates, -} from "../../../platforms/cloudtentacles/session-state-service.js"; +} from '../../../platforms/cloudtentacles/session-state-service.js' import { loginCloudtentaclesSession, sendCloudtentaclesSmsCode, validateCloudtentaclesSession, -} from "../../../platforms/cloudtentacles/session-service.js"; +} from '../../../platforms/cloudtentacles/session-service.js' import { buyCloudtentaclesSku, getCloudtentaclesAsset, getCloudtentaclesCategories, listCloudtentaclesSku, useCloudtentaclesSku, -} from "../../../platforms/cloudtentacles/catalog-service.js"; -import { getCloudtentaclesKnapsack } from "../../../platforms/cloudtentacles/knapsack-service.js"; -import { listCloudtentaclesDeliveryRecords } from "../../../platforms/cloudtentacles/record-service.js"; +} from '../../../platforms/cloudtentacles/catalog-service.js' +import { getCloudtentaclesKnapsack } from '../../../platforms/cloudtentacles/knapsack-service.js' +import { listCloudtentaclesDeliveryRecords } from '../../../platforms/cloudtentacles/record-service.js' import { appointCloudtentaclesVirtualNumber, backCloudtentaclesVirtualNumber, @@ -38,33 +38,30 @@ import { getCloudtentaclesBindUrl, listCloudtentaclesVirtualNumbers, verifyCloudtentaclesLoginCode, -} from "../../../platforms/cloudtentacles/virtual-number-service.js"; -import { runCloudtentaclesFullDebugFlow } from "../../../platforms/cloudtentacles/debug-flow-service.js"; +} from '../../../platforms/cloudtentacles/virtual-number-service.js' +import { runCloudtentaclesFullDebugFlow } from '../../../platforms/cloudtentacles/debug-flow-service.js' import { getCloudtentaclesOverrideRuleConfig, getCloudtentaclesOverrideRuleFilePath, saveCloudtentaclesOverrideRuleConfig, -} from "../../../order/cloudtentacles-override-rule-service.js"; -import { - hasCloudtentaclesCredentialContextChanged, - pickFirstNonEmpty, -} from "./context.js"; +} from '../../../order/cloudtentacles-override-rule-service.js' +import { hasCloudtentaclesCredentialContextChanged, pickFirstNonEmpty } from './context.js' import { buildAdminCloudtentaclesLoginResult, buildAdminCloudtentaclesPersistedSessionPayload, buildAdminCloudtentaclesValidateResult, resolveAdminCloudtentaclesCredentialPayload, resolveAdminCloudtentaclesSessionPayload, -} from "./payloads.js"; -import { normalizeAdminCloudtentaclesSourceConfigPayload } from "./writes.js"; +} from './payloads.js' +import { normalizeAdminCloudtentaclesSourceConfigPayload } from './writes.js' import { mapAdminCloudtentaclesSession, mapAdminCloudtentaclesSourceConfig, maskPhone, -} from "./mappers.js"; -import { createHttpError } from "../../../../utils/http.js"; -import { query as dbQuery } from "../../../../db/client.js"; -import { parseJsonObject } from "../../../../utils/task-json.js"; +} from './mappers.js' +import { createHttpError } from '../../../../utils/http.js' +import { query as dbQuery } from '../../../../db/client.js' +import { parseJsonObject } from '../../../../utils/task-json.js' import type { AdminCloudtentaclesCatalogQueryInput, @@ -78,67 +75,67 @@ import type { AdminCloudtentaclesTestLoginInput, AdminCloudtentaclesValidateSessionInput, AdminCloudtentaclesVirtualNumberInput, -} from "../../../../types/admin/write-inputs.js"; +} from '../../../../types/admin/write-inputs.js' type LocalCloudtentaclesTask = { - taskId: number; - taskNo: string; - platformOrderId: string; - orderShopId: string; - orderShopName: string; - taskStatus: string; - deliveryStatus: string; - resultCode: string; - resultMessage: string; - createdAt: string; - updatedAt: string; - skuCode: string; - skuName: string; - sourceKey: string; - vnId: number; - vnPhone: string; - roleName: string; - roleId: string; - localDispatchStatus: string; - localDispatchAt: string; - consumeShopId: string; - consumeShopName: string; -}; + taskId: number + taskNo: string + platformOrderId: string + orderShopId: string + orderShopName: string + taskStatus: string + deliveryStatus: string + resultCode: string + resultMessage: string + createdAt: string + updatedAt: string + skuCode: string + skuName: string + sourceKey: string + vnId: number + vnPhone: string + roleName: string + roleId: string + localDispatchStatus: string + localDispatchAt: string + consumeShopId: string + consumeShopName: string +} type LocalCloudtentaclesTaskRow = { - id: number; - task_no: string; - platform_order_id: string; - shop_id: string; - shop_name: string; - task_status: string; - delivery_status: string; - result_code: string; - result_message: string; - context_json: unknown; - created_at: string; - updated_at: string; - sku_code?: string; - sku_name?: string; - kuaishou_source_key?: string; - kuaishou_vn_phone?: string; - kuaishou_role_name?: string; - kuaishou_role_id?: string; - kuaishou_dispatch_status?: string; - kuaishou_consume_status?: string; -}; + id: number + task_no: string + platform_order_id: string + shop_id: string + shop_name: string + task_status: string + delivery_status: string + result_code: string + result_message: string + context_json: unknown + created_at: string + updated_at: string + sku_code?: string + sku_name?: string + kuaishou_source_key?: string + kuaishou_vn_phone?: string + kuaishou_role_name?: string + kuaishou_role_id?: string + kuaishou_dispatch_status?: string + kuaishou_consume_status?: string +} export function listAdminCloudtentaclesSources() { - const list = listCloudtentaclesSources(); - const sessions = getAllCloudtentaclesSessionStates(); + const list = listCloudtentaclesSources() + const sessions = getAllCloudtentaclesSessionStates() const sources = Array.isArray(list.sources) ? list.sources.map((s) => mapAdminCloudtentaclesSourceConfig(s)) - : []; + : [] - const mappedSessions: JsonObject = {}; + const mappedSessions: JsonObject = {} for (const [key, session] of Object.entries(sessions.sessions || {})) { - mappedSessions[key] = mapAdminCloudtentaclesSession(session); + mappedSessions[key] = mapAdminCloudtentaclesSession(session) } return { @@ -147,67 +144,65 @@ export function listAdminCloudtentaclesSources() { enabled: list.enabled, sources, sessions: mappedSessions, - }; + } } export function listAdminCloudtentaclesRecordSources() { - const list = listCloudtentaclesSources(); - const sessions = getAllCloudtentaclesSessionStates(); - const sources = Array.isArray(list.sources) ? list.sources : []; + const list = listCloudtentaclesSources() + const sessions = getAllCloudtentaclesSessionStates() + const sources = Array.isArray(list.sources) ? list.sources : [] return { sources: sources.map((source) => { - const key = String(source.key || "").trim(); - const session = (sessions.sessions?.[key] || {}) as JsonObject; + const key = String(source.key || '').trim() + const session = (sessions.sessions?.[key] || {}) as JsonObject return { key, - label: String(source.label || source.username || key || "未命名账号").trim(), + label: String(source.label || source.username || key || '未命名账号').trim(), enabled: source.enabled !== false, - hasToken: Boolean(String(session.token || "").trim()), - loggedInAt: String(session.loggedInAt || "").trim(), - }; + hasToken: Boolean(String(session.token || '').trim()), + loggedInAt: String(session.loggedInAt || '').trim(), + } }), - }; + } } export function getAdminCloudtentaclesOverrideRules() { return { filePath: getCloudtentaclesOverrideRuleFilePath(), ...getCloudtentaclesOverrideRuleConfig(), - }; + } } export async function updateAdminCloudtentaclesOverrideRules( - payload: AdminCloudtentaclesOverrideRuleConfigInput = {} + payload: AdminCloudtentaclesOverrideRuleConfigInput = {}, ) { return { filePath: getCloudtentaclesOverrideRuleFilePath(), ...(await saveCloudtentaclesOverrideRuleConfig(payload)), - }; + } } export async function updateAdminCloudtentaclesSourceConfig( - payload: AdminCloudtentaclesSourceConfigInput = {} + payload: AdminCloudtentaclesSourceConfigInput = {}, ) { // 批量保存:payload.sources 是数组时替换整份账号列表。 if (Array.isArray(payload.sources)) { const normalizedList = payload.sources.map((item) => - normalizeAdminCloudtentaclesSourceConfigPayload(item) - ); + normalizeAdminCloudtentaclesSourceConfigPayload(item), + ) const savedList = await saveCloudtentaclesSourcesList({ enabled: payload.enabled !== false, sources: normalizedList, - }); - const sessions = await pruneCloudtentaclesSessionStates( - normalizedList.map((item) => item.key) - ); + }) + const sessions = await pruneCloudtentaclesSessionStates(normalizedList.map((item) => item.key)) const sources = Array.isArray(savedList.sources) ? savedList.sources.map((s) => mapAdminCloudtentaclesSourceConfig(s)) - : []; - const mappedSessions: JsonObject = {}; + : [] + const mappedSessions: JsonObject = {} for (const [key, session] of Object.entries(sessions.sessions || {})) { - mappedSessions[key] = mapAdminCloudtentaclesSession(session); + mappedSessions[key] = mapAdminCloudtentaclesSession(session) } return { @@ -216,194 +211,166 @@ export async function updateAdminCloudtentaclesSourceConfig( enabled: savedList.enabled, sources, sessions: mappedSessions, - }; + } } // 单账号保存。 - const sourceKey = String(payload.sourceKey || "").trim() || "default"; - const current = getCloudtentaclesSourceByKey(sourceKey); + const sourceKey = String(payload.sourceKey || '').trim() || 'default' + const current = getCloudtentaclesSourceByKey(sourceKey) const normalized = normalizeAdminCloudtentaclesSourceConfigPayload({ ...payload, sourceKey, - }); - const saved = await saveCloudtentaclesSourceByKey(sourceKey, normalized); - const shouldClearSession = hasCloudtentaclesCredentialContextChanged( - current || undefined, - saved - ); + }) + const saved = await saveCloudtentaclesSourceByKey(sourceKey, normalized) + const shouldClearSession = hasCloudtentaclesCredentialContextChanged(current || undefined, saved) const session = shouldClearSession ? (await clearCloudtentaclesSessionStateByKey(sourceKey), getCloudtentaclesSessionStateByKey(sourceKey)) - : getCloudtentaclesSessionStateByKey(sourceKey); + : getCloudtentaclesSessionStateByKey(sourceKey) return { filePath: getCloudtentaclesSourcesFilePath(), sessionFilePath: getCloudtentaclesSessionFilePath(), source: mapAdminCloudtentaclesSourceConfig(saved), session: mapAdminCloudtentaclesSession(session || undefined), - }; + } } export async function deleteAdminCloudtentaclesSource(sourceKey: string) { - const key = String(sourceKey || "").trim(); + const key = String(sourceKey || '').trim() if (!key) { - throw createHttpError("cloudtentacles sourceKey 不能为空", { + throw createHttpError('cloudtentacles sourceKey 不能为空', { statusCode: 400, - errorCode: "cloudtentacles_source_key_required", - }); + errorCode: 'cloudtentacles_source_key_required', + }) } - await deleteCloudtentaclesSessionStateByKey(key); - await deleteCloudtentaclesSourceByKey(key); + await deleteCloudtentaclesSessionStateByKey(key) + await deleteCloudtentaclesSourceByKey(key) } export async function sendAdminCloudtentaclesSmsCode( - payload: AdminCloudtentaclesSendSmsCodeInput = {} + payload: AdminCloudtentaclesSendSmsCodeInput = {}, ) { const result = await sendCloudtentaclesSmsCode( - resolveAdminCloudtentaclesCredentialPayload(payload) - ); + resolveAdminCloudtentaclesCredentialPayload(payload), + ) return { ...result, phoneMasked: maskPhone(result.phone), - }; + } } export async function testAdminCloudtentaclesLogin( - payload: AdminCloudtentaclesTestLoginInput = {} + payload: AdminCloudtentaclesTestLoginInput = {}, ) { - const sourceKey = String(payload.sourceKey || "").trim() || "default"; - const savedSource = getCloudtentaclesSourceByKey(sourceKey) || {}; - const credentialContext = resolveAdminCloudtentaclesCredentialPayload( - payload, - { savedSource } - ); + const sourceKey = String(payload.sourceKey || '').trim() || 'default' + const savedSource = getCloudtentaclesSourceByKey(sourceKey) || {} + const credentialContext = resolveAdminCloudtentaclesCredentialPayload(payload, { savedSource }) const session = await loginCloudtentaclesSession({ ...credentialContext, - code: String(payload.code || "").trim(), - }); + code: String(payload.code || '').trim(), + }) const savedSession = await saveCloudtentaclesSessionStateByKey( sourceKey, buildAdminCloudtentaclesPersistedSessionPayload(session, { deviceId: credentialContext.deviceId, deviceType: credentialContext.deviceType, - }) - ); + }), + ) - return buildAdminCloudtentaclesLoginResult(session, savedSession, sourceKey); + return buildAdminCloudtentaclesLoginResult(session, savedSession, sourceKey) } export async function validateAdminCloudtentaclesSession( - payload: AdminCloudtentaclesValidateSessionInput = {} + payload: AdminCloudtentaclesValidateSessionInput = {}, ) { - const sourceKey = String(payload.sourceKey || "").trim() || "default"; - const savedSource: JsonObject = getCloudtentaclesSourceByKey(sourceKey) || {}; - const persistedSession: JsonObject = getCloudtentaclesSessionStateByKey(sourceKey) || {}; + const sourceKey = String(payload.sourceKey || '').trim() || 'default' + const savedSource: JsonObject = getCloudtentaclesSourceByKey(sourceKey) || {} + const persistedSession: JsonObject = getCloudtentaclesSessionStateByKey(sourceKey) || {} const sessionContext = resolveAdminCloudtentaclesSessionPayload(payload, { savedSource, persistedSession, - }); - const session = await validateCloudtentaclesSession(sessionContext); + }) + const session = await validateCloudtentaclesSession(sessionContext) const savedSession = await saveCloudtentaclesSessionStateByKey( sourceKey, buildAdminCloudtentaclesPersistedSessionPayload(session, { - username: pickFirstNonEmpty([ - persistedSession.username, - savedSource.username, - ]), + username: pickFirstNonEmpty([persistedSession.username, savedSource.username]), phone: pickFirstNonEmpty([persistedSession.phone, savedSource.phone]), deviceId: sessionContext.deviceId, deviceType: sessionContext.deviceType, - }) - ); + }), + ) - return buildAdminCloudtentaclesValidateResult( - session, - savedSession, - sourceKey - ); + return buildAdminCloudtentaclesValidateResult(session, savedSession, sourceKey) } export async function getAdminCloudtentaclesAsset( - payload: AdminCloudtentaclesCatalogQueryInput = {} + payload: AdminCloudtentaclesCatalogQueryInput = {}, ) { - return getCloudtentaclesAsset( - resolveAdminCloudtentaclesSessionPayload(payload) - ); + return getCloudtentaclesAsset(resolveAdminCloudtentaclesSessionPayload(payload)) } export async function getAdminCloudtentaclesCategories( - payload: AdminCloudtentaclesCatalogQueryInput = {} + payload: AdminCloudtentaclesCatalogQueryInput = {}, ) { - return getCloudtentaclesCategories( - resolveAdminCloudtentaclesSessionPayload(payload) - ); + return getCloudtentaclesCategories(resolveAdminCloudtentaclesSessionPayload(payload)) } export async function getAdminCloudtentaclesSkuList( - payload: AdminCloudtentaclesCatalogQueryInput = {} + payload: AdminCloudtentaclesCatalogQueryInput = {}, ) { - return listCloudtentaclesSku( - resolveAdminCloudtentaclesSessionPayload(payload) - ); + return listCloudtentaclesSku(resolveAdminCloudtentaclesSessionPayload(payload)) } -export async function buyAdminCloudtentaclesSku( - payload: AdminCloudtentaclesSkuBuyInput = {} -) { - const context = resolveAdminCloudtentaclesSessionPayload(payload); +export async function buyAdminCloudtentaclesSku(payload: AdminCloudtentaclesSkuBuyInput = {}) { + const context = resolveAdminCloudtentaclesSessionPayload(payload) return buyCloudtentaclesSku({ ...context, id: payload.id, count: payload.count, - }); + }) } -export async function useAdminCloudtentaclesSku( - payload: AdminCloudtentaclesSkuUseInput = {} -) { - const context = resolveAdminCloudtentaclesSessionPayload(payload); +export async function useAdminCloudtentaclesSku(payload: AdminCloudtentaclesSkuUseInput = {}) { + const context = resolveAdminCloudtentaclesSessionPayload(payload) return useCloudtentaclesSku({ ...context, id: payload.id, virtualNumberId: payload.virtualNumberId, phone: payload.phone, - }); + }) } export async function getAdminCloudtentaclesKnapsack( - payload: AdminCloudtentaclesCatalogQueryInput = {} + payload: AdminCloudtentaclesCatalogQueryInput = {}, ) { - return getCloudtentaclesKnapsack( - resolveAdminCloudtentaclesSessionPayload(payload) - ); + return getCloudtentaclesKnapsack(resolveAdminCloudtentaclesSessionPayload(payload)) } export async function listAdminCloudtentaclesDeliveryRecords( - payload: AdminCloudtentaclesDeliveryRecordQueryInput = {} + payload: AdminCloudtentaclesDeliveryRecordQueryInput = {}, ) { - const platformOrderId = normalizeText(payload.platformOrderId); - const preferredSourceKey = normalizeText(payload.sourceKey); + const platformOrderId = normalizeText(payload.platformOrderId) + const preferredSourceKey = normalizeText(payload.sourceKey) const orderMatchedTasks = platformOrderId ? await listLocalCloudtentaclesTasks({ platformOrderId, limit: 50, }) - : []; + : [] const orderSourceKey = orderMatchedTasks.find((task) => !preferredSourceKey || task.sourceKey === preferredSourceKey) - ?.sourceKey || orderMatchedTasks.find((task) => task.sourceKey)?.sourceKey; - const resolvedSourceKey = - orderSourceKey || - preferredSourceKey || - "default"; + ?.sourceKey || orderMatchedTasks.find((task) => task.sourceKey)?.sourceKey + const resolvedSourceKey = orderSourceKey || preferredSourceKey || 'default' const context = resolveAdminCloudtentaclesSessionPayload({ ...payload, sourceKey: resolvedSourceKey, - }); - const recordWindow = resolveRecordQueryWindow(payload, orderMatchedTasks); - const recordPage = platformOrderId ? 1 : payload.page; - const recordSize = platformOrderId ? 1000 : payload.size; + }) + const recordWindow = resolveRecordQueryWindow(payload, orderMatchedTasks) + const recordPage = platformOrderId ? 1 : payload.page + const recordSize = platformOrderId ? 1000 : payload.size const query = { ...context, ...pickDefined({ @@ -413,31 +380,34 @@ export async function listAdminCloudtentaclesDeliveryRecords( endDate: recordWindow.endDate, recordCode: payload.recordCode, }), - }; + } const [records, skuList] = await Promise.all([ listCloudtentaclesDeliveryRecords(query), listCloudtentaclesSku(context), - ]); - const skuImageByName = new Map(); + ]) + const skuImageByName = new Map< + string, + { + skuId: number + skuName: string + skuImage: string + } + >() for (const sku of Array.isArray(skuList.items) ? skuList.items : []) { - const name = normalizeSkuName(sku.name); - const skuImage = String(sku.image || "").trim(); + const name = normalizeSkuName(sku.name) + const skuImage = String(sku.image || '').trim() if (!name || !skuImage) { - continue; + continue } skuImageByName.set(name, { skuId: Number(sku.id || 0), - skuName: String(sku.name || "").trim(), + skuName: String(sku.name || '').trim(), skuImage, - }); + }) } - const recordItems: JsonObject[] = Array.isArray(records.items) ? records.items : []; + const recordItems: JsonObject[] = Array.isArray(records.items) ? records.items : [] const localTasks = platformOrderId || recordItems.length === 0 ? orderMatchedTasks @@ -446,42 +416,44 @@ export async function listAdminCloudtentaclesDeliveryRecords( startDate: recordWindow.startDate, endDate: recordWindow.endDate, limit: 800, - }); + }) const enrichedItems = recordItems .map((record) => { const localMatch = platformOrderId ? findStrictLocalTaskForOrderRecord(record, localTasks) - : findBestLocalTaskForRecord(record, localTasks); + : findBestLocalTaskForRecord(record, localTasks) return { ...record, ...(skuImageByName.get(normalizeSkuName(record.name)) || { skuId: 0, - skuName: "", - skuImage: "", + skuName: '', + skuImage: '', }), - ...mapLocalTaskRecordEnhancement(localMatch?.task || null, localMatch?.confidence || ""), - }; + ...mapLocalTaskRecordEnhancement(localMatch?.task || null, localMatch?.confidence || ''), + } }) .filter((record) => { if (!platformOrderId) { - return true; + return true } - return normalizeText(record.platformOrderId) === platformOrderId; - }); + return normalizeText(record.platformOrderId) === platformOrderId + }) const fallbackItems = platformOrderId ? orderMatchedTasks - .filter((task) => !enrichedItems.some((record) => Number(record.taskId || 0) === task.taskId)) + .filter( + (task) => !enrichedItems.some((record) => Number(record.taskId || 0) === task.taskId), + ) .map((task) => ({ ...createLocalTaskFallbackRecord(task), ...(skuImageByName.get(normalizeSkuName(task.skuName)) || { skuId: 0, - skuName: "", - skuImage: "", + skuName: '', + skuImage: '', }), - ...mapLocalTaskRecordEnhancement(task, "local"), + ...mapLocalTaskRecordEnhancement(task, 'local'), })) - : []; - const finalItems = platformOrderId ? [...enrichedItems, ...fallbackItems] : enrichedItems; + : [] + const finalItems = platformOrderId ? [...enrichedItems, ...fallbackItems] : enrichedItems return { ...records, @@ -493,203 +465,206 @@ export async function listAdminCloudtentaclesDeliveryRecords( size: platformOrderId ? recordSize : records.size, localOrderMatched: platformOrderId ? orderMatchedTasks.length > 0 : false, items: finalItems, - }; + } } export async function listAdminCloudtentaclesVirtualNumbers( - payload: AdminCloudtentaclesVirtualNumberInput = {} + payload: AdminCloudtentaclesVirtualNumberInput = {}, ) { - const context = resolveAdminCloudtentaclesSessionPayload(payload); + const context = resolveAdminCloudtentaclesSessionPayload(payload) return listCloudtentaclesVirtualNumbers({ ...context, key: payload.key, - }); + }) } export async function appointAdminCloudtentaclesVirtualNumber( - payload: AdminCloudtentaclesVirtualNumberInput = {} + payload: AdminCloudtentaclesVirtualNumberInput = {}, ) { - const context = resolveAdminCloudtentaclesSessionPayload(payload); + const context = resolveAdminCloudtentaclesSessionPayload(payload) return appointCloudtentaclesVirtualNumber({ ...context, key: payload.key, - }); + }) } export async function generateAdminCloudtentaclesLoginCode( - payload: AdminCloudtentaclesVirtualNumberInput = {} + payload: AdminCloudtentaclesVirtualNumberInput = {}, ) { - const context = resolveAdminCloudtentaclesSessionPayload(payload); + const context = resolveAdminCloudtentaclesSessionPayload(payload) return generateCloudtentaclesLoginCode({ ...context, key: payload.key, id: payload.id, - }); + }) } export async function fetchAdminCloudtentaclesVirtualNumberCode( - payload: AdminCloudtentaclesVirtualNumberInput = {} + payload: AdminCloudtentaclesVirtualNumberInput = {}, ) { - const context = resolveAdminCloudtentaclesSessionPayload(payload); + const context = resolveAdminCloudtentaclesSessionPayload(payload) return fetchCloudtentaclesVirtualNumberCode({ ...context, key: payload.key, phone: payload.phone, - }); + }) } export async function verifyAdminCloudtentaclesLoginCode( - payload: AdminCloudtentaclesVirtualNumberInput = {} + payload: AdminCloudtentaclesVirtualNumberInput = {}, ) { - const context = resolveAdminCloudtentaclesSessionPayload(payload); + const context = resolveAdminCloudtentaclesSessionPayload(payload) return verifyCloudtentaclesLoginCode({ ...context, key: payload.key, id: payload.id, code: payload.code, - }); + }) } export async function getAdminCloudtentaclesBindUrl( - payload: AdminCloudtentaclesVirtualNumberInput = {} + payload: AdminCloudtentaclesVirtualNumberInput = {}, ) { - const context = resolveAdminCloudtentaclesSessionPayload(payload); + const context = resolveAdminCloudtentaclesSessionPayload(payload) return getCloudtentaclesBindUrl({ ...context, key: payload.key, id: payload.id, - }); + }) } export async function backAdminCloudtentaclesVirtualNumber( - payload: AdminCloudtentaclesVirtualNumberInput = {} + payload: AdminCloudtentaclesVirtualNumberInput = {}, ) { - const context = resolveAdminCloudtentaclesSessionPayload(payload); + const context = resolveAdminCloudtentaclesSessionPayload(payload) return backCloudtentaclesVirtualNumber({ ...context, key: payload.key, id: payload.id, - }); + }) } export async function runAdminCloudtentaclesFullFlow( - payload: AdminCloudtentaclesFullFlowInput = {} + payload: AdminCloudtentaclesFullFlowInput = {}, ) { - const context = resolveAdminCloudtentaclesSessionPayload(payload); + const context = resolveAdminCloudtentaclesSessionPayload(payload) return runCloudtentaclesFullDebugFlow({ ...context, skuId: payload.skuId, skuCount: payload.skuCount, vnKey: payload.vnKey, - }); + }) } function pickDefined(values: JsonObject = {}) { return Object.fromEntries( - Object.entries(values).filter(([, value]) => typeof value !== "undefined") - ); + Object.entries(values).filter(([, value]) => typeof value !== 'undefined'), + ) } function normalizeSkuName(value: unknown) { - return String(value || "").trim().replace(/\s+/g, "").toLowerCase(); + return String(value || '') + .trim() + .replace(/\s+/g, '') + .toLowerCase() } function normalizeText(value: unknown) { - return String(value || "").trim(); + return String(value || '').trim() } function normalizePhone(value: unknown) { - return normalizeText(value).replace(/\D+/g, ""); + return normalizeText(value).replace(/\D+/g, '') } function normalizeNumber(value: unknown) { - const parsed = Number(value); - return Number.isFinite(parsed) && parsed > 0 ? parsed : 0; + const parsed = Number(value) + return Number.isFinite(parsed) && parsed > 0 ? parsed : 0 } function asRecord(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) : {} } function pickFirstNonEmptyText(values: unknown[]) { for (const value of values) { - const normalized = normalizeText(value); + const normalized = normalizeText(value) if (normalized) { - return normalized; + return normalized } } - return ""; + return '' } function resolveRecordQueryWindow( payload: AdminCloudtentaclesDeliveryRecordQueryInput = {}, - tasks: LocalCloudtentaclesTask[] = [] + tasks: LocalCloudtentaclesTask[] = [], ) { if (tasks.length === 0) { return { startDate: payload.startDate, endDate: payload.endDate, - }; + } } const timestamps = tasks .flatMap((task) => [task.createdAt, task.updatedAt, task.localDispatchAt]) .map((value) => new Date(value).getTime()) - .filter((value) => Number.isFinite(value)); + .filter((value) => Number.isFinite(value)) if (timestamps.length === 0) { return { startDate: payload.startDate, endDate: payload.endDate, - }; + } } - const hourMs = 60 * 60 * 1000; + const hourMs = 60 * 60 * 1000 return { startDate: new Date(Math.min(...timestamps) - hourMs).toISOString(), endDate: new Date(Math.max(...timestamps) + 6 * hourMs).toISOString(), - }; + } } -async function listLocalCloudtentaclesTasks(options: { - platformOrderId?: string | undefined; - sourceKey?: string | undefined; - startDate?: string | undefined; - endDate?: string | undefined; - limit?: number; -} = {}): Promise { - const filters: string[] = ["ft.executor_key = 'kuaishou_ct_assisted'"]; - const params: unknown[] = []; - const platformOrderId = normalizeText(options.platformOrderId); - const sourceKey = normalizeText(options.sourceKey); - const startDate = normalizeText(options.startDate); - const endDate = normalizeText(options.endDate); +async function listLocalCloudtentaclesTasks( + options: { + platformOrderId?: string | undefined + sourceKey?: string | undefined + startDate?: string | undefined + endDate?: string | undefined + limit?: number + } = {}, +): Promise { + const filters: string[] = ["ft.executor_key = 'kuaishou_ct_assisted'"] + const params: unknown[] = [] + const platformOrderId = normalizeText(options.platformOrderId) + const sourceKey = normalizeText(options.sourceKey) + const startDate = normalizeText(options.startDate) + const endDate = normalizeText(options.endDate) if (platformOrderId) { - params.push(`%${platformOrderId}%`); - filters.push(`ft.platform_order_id ILIKE $${params.length}`); + params.push(`%${platformOrderId}%`) + filters.push(`ft.platform_order_id ILIKE $${params.length}`) } if (sourceKey) { - params.push(sourceKey); + params.push(sourceKey) filters.push( - `COALESCE(NULLIF(kcts.source_key, ''), ft.context_json #>> '{kuaishouCloudFulfillment,binding,resolvedSourceKey}', '') = $${params.length}` - ); + `COALESCE(NULLIF(kcts.source_key, ''), ft.context_json #>> '{kuaishouCloudFulfillment,binding,resolvedSourceKey}', '') = $${params.length}`, + ) } if (startDate) { - params.push(startDate); - filters.push(`ft.updated_at >= ($${params.length})::timestamptz - interval '1 day'`); + params.push(startDate) + filters.push(`ft.updated_at >= ($${params.length})::timestamptz - interval '1 day'`) } if (endDate) { - params.push(endDate); - filters.push(`ft.created_at <= ($${params.length})::timestamptz + interval '1 day'`); + params.push(endDate) + filters.push(`ft.created_at <= ($${params.length})::timestamptz + interval '1 day'`) } - params.push(Math.min(Math.max(Number(options.limit || 500), 1), 1000)); + params.push(Math.min(Math.max(Number(options.limit || 500), 1), 1000)) const result = await dbQuery( ` SELECT @@ -716,23 +691,23 @@ async function listLocalCloudtentaclesTasks(options: { FROM fulfillment_tasks ft LEFT JOIN order_items oi ON oi.id = ft.order_item_id LEFT JOIN kuaishou_cloud_task_states kcts ON kcts.task_id = ft.id - WHERE ${filters.join(" AND ")} + WHERE ${filters.join(' AND ')} ORDER BY ft.id DESC LIMIT $${params.length} `, - params - ); + params, + ) - return result.rows.map(mapLocalCloudtentaclesTask); + return result.rows.map(mapLocalCloudtentaclesTask) } function mapLocalCloudtentaclesTask(row: LocalCloudtentaclesTaskRow): LocalCloudtentaclesTask { - const context = parseJsonObject(row.context_json); - const flow = asRecord(context.kuaishouCloudFulfillment); - const binding = asRecord(flow.binding); - const role = asRecord(flow.role); - const dispatch = asRecord(flow.dispatch); - const consume = asRecord(flow.consume); + const context = parseJsonObject(row.context_json) + const flow = asRecord(context.kuaishouCloudFulfillment) + const binding = asRecord(flow.binding) + const role = asRecord(flow.role) + const dispatch = asRecord(flow.dispatch) + const consume = asRecord(flow.consume) return { taskId: Number(row.id || 0), @@ -757,158 +732,152 @@ function mapLocalCloudtentaclesTask(row: LocalCloudtentaclesTaskRow): LocalCloud localDispatchAt: normalizeText(dispatch.dispatchAt), consumeShopId: normalizeText(consume.shopId), consumeShopName: normalizeText(consume.shopName), - }; + } } -function findBestLocalTaskForRecord( - record: JsonObject, - tasks: LocalCloudtentaclesTask[] = [] -) { - let best: { task: LocalCloudtentaclesTask; score: number; confidence: string } | null = null; +function findBestLocalTaskForRecord(record: JsonObject, tasks: LocalCloudtentaclesTask[] = []) { + let best: { task: LocalCloudtentaclesTask; score: number; confidence: string } | null = null for (const task of tasks) { - const score = scoreLocalTaskRecordMatch(record, task); + const score = scoreLocalTaskRecordMatch(record, task) if (score <= 0 || (best && score <= best.score)) { - continue; + continue } best = { task, score, - confidence: score >= 80 ? "high" : score >= 45 ? "medium" : "low", - }; + confidence: score >= 80 ? 'high' : score >= 45 ? 'medium' : 'low', + } } - return best && best.score >= 35 ? best : null; + return best && best.score >= 35 ? best : null } function findStrictLocalTaskForOrderRecord( record: JsonObject, - tasks: LocalCloudtentaclesTask[] = [] + tasks: LocalCloudtentaclesTask[] = [], ) { - let best: { task: LocalCloudtentaclesTask; score: number; confidence: string } | null = null; + let best: { task: LocalCloudtentaclesTask; score: number; confidence: string } | null = null for (const task of tasks) { if (!isSameDispatchedResource(record, task)) { - continue; + continue } - const score = scoreLocalTaskRecordMatch(record, task); + const score = scoreLocalTaskRecordMatch(record, task) if (score <= 0 || (best && score <= best.score)) { - continue; + continue } best = { task, score, - confidence: score >= 80 ? "high" : "medium", - }; - } - - return best; -} - -function isSameDispatchedResource(record: JsonObject, task: LocalCloudtentaclesTask) { - const recordVnId = normalizeNumber(record.virtualNumberId); - const recordPhone = normalizePhone(record.phone); - const taskPhone = normalizePhone(task.vnPhone); - - if (recordVnId > 0 && task.vnId > 0 && recordVnId === task.vnId) { - return true; - } - - return Boolean(recordPhone && taskPhone && recordPhone === taskPhone); -} - -function scoreLocalTaskRecordMatch(record: JsonObject, task: LocalCloudtentaclesTask) { - let score = 0; - const recordVnId = normalizeNumber(record.virtualNumberId); - const recordPhone = normalizePhone(record.phone); - const taskPhone = normalizePhone(task.vnPhone); - const recordName = normalizeSkuName(record.name); - const taskSkuName = normalizeSkuName(task.skuName); - const recordRoleName = normalizeSkuName(record.fulfillUser); - const taskRoleName = normalizeSkuName(task.roleName); - - if (recordVnId > 0 && task.vnId > 0 && recordVnId === task.vnId) { - score += 50; - } - if (recordPhone && taskPhone && recordPhone === taskPhone) { - score += 30; - } - if (recordName && taskSkuName && recordName === taskSkuName) { - score += 20; - } - if (recordRoleName && taskRoleName && recordRoleName.includes(taskRoleName)) { - score += 10; - } - - const recordTime = new Date(record.createdAt).getTime(); - const dispatchTime = new Date(task.localDispatchAt || task.updatedAt || task.createdAt).getTime(); - if (Number.isFinite(recordTime) && Number.isFinite(dispatchTime)) { - const distanceHours = Math.abs(recordTime - dispatchTime) / (60 * 60 * 1000); - if (distanceHours <= 6) { - score += 15; - } else if (distanceHours <= 24) { - score += 6; + confidence: score >= 80 ? 'high' : 'medium', } } - return score; + return best } -function mapLocalTaskRecordEnhancement( - task: LocalCloudtentaclesTask | null, - confidence = "" -) { +function isSameDispatchedResource(record: JsonObject, task: LocalCloudtentaclesTask) { + const recordVnId = normalizeNumber(record.virtualNumberId) + const recordPhone = normalizePhone(record.phone) + const taskPhone = normalizePhone(task.vnPhone) + + if (recordVnId > 0 && task.vnId > 0 && recordVnId === task.vnId) { + return true + } + + return Boolean(recordPhone && taskPhone && recordPhone === taskPhone) +} + +function scoreLocalTaskRecordMatch(record: JsonObject, task: LocalCloudtentaclesTask) { + let score = 0 + const recordVnId = normalizeNumber(record.virtualNumberId) + const recordPhone = normalizePhone(record.phone) + const taskPhone = normalizePhone(task.vnPhone) + const recordName = normalizeSkuName(record.name) + const taskSkuName = normalizeSkuName(task.skuName) + const recordRoleName = normalizeSkuName(record.fulfillUser) + const taskRoleName = normalizeSkuName(task.roleName) + + if (recordVnId > 0 && task.vnId > 0 && recordVnId === task.vnId) { + score += 50 + } + if (recordPhone && taskPhone && recordPhone === taskPhone) { + score += 30 + } + if (recordName && taskSkuName && recordName === taskSkuName) { + score += 20 + } + if (recordRoleName && taskRoleName && recordRoleName.includes(taskRoleName)) { + score += 10 + } + + const recordTime = new Date(record.createdAt).getTime() + const dispatchTime = new Date(task.localDispatchAt || task.updatedAt || task.createdAt).getTime() + if (Number.isFinite(recordTime) && Number.isFinite(dispatchTime)) { + const distanceHours = Math.abs(recordTime - dispatchTime) / (60 * 60 * 1000) + if (distanceHours <= 6) { + score += 15 + } else if (distanceHours <= 24) { + score += 6 + } + } + + return score +} + +function mapLocalTaskRecordEnhancement(task: LocalCloudtentaclesTask | null, confidence = '') { return { - platformOrderId: task?.platformOrderId || "", + platformOrderId: task?.platformOrderId || '', taskId: task?.taskId || 0, - taskNo: task?.taskNo || "", - orderShopId: task?.orderShopId || "", - orderShopName: task?.orderShopName || "", - consumeShopId: task?.consumeShopId || "", - consumeShopName: task?.consumeShopName || "", - roleName: task?.roleName || "", - roleId: task?.roleId || "", - localTaskStatus: task?.taskStatus || "", - localDeliveryStatus: task?.deliveryStatus || "", - localDispatchStatus: task?.localDispatchStatus || "", - localDispatchAt: task?.localDispatchAt || "", - localResultCode: task?.resultCode || "", - localResultMessage: task?.resultMessage || "", + taskNo: task?.taskNo || '', + orderShopId: task?.orderShopId || '', + orderShopName: task?.orderShopName || '', + consumeShopId: task?.consumeShopId || '', + consumeShopName: task?.consumeShopName || '', + roleName: task?.roleName || '', + roleId: task?.roleId || '', + localTaskStatus: task?.taskStatus || '', + localDeliveryStatus: task?.deliveryStatus || '', + localDispatchStatus: task?.localDispatchStatus || '', + localDispatchAt: task?.localDispatchAt || '', + localResultCode: task?.resultCode || '', + localResultMessage: task?.resultMessage || '', matchConfidence: confidence, - }; + } } function createLocalTaskFallbackRecord(task: LocalCloudtentaclesTask) { return { createdAt: task.localDispatchAt || task.updatedAt || task.createdAt, - recordId: "", + recordId: '', virtualNumberId: task.vnId, userId: 0, count: 1, - cdk: "", - exchangeUrl: "", + cdk: '', + exchangeUrl: '', name: task.skuName, phone: task.vnPhone, status: mapLocalDispatchStatusToCloudStatus(task.localDispatchStatus), fulfillUser: task.roleName, - operatorName: "", + operatorName: '', raw: {}, - }; + } } function mapLocalDispatchStatusToCloudStatus(status: string) { - const normalized = normalizeText(status); - if (normalized === "success") { - return 4; + const normalized = normalizeText(status) + if (normalized === 'success') { + return 4 } - if (normalized === "failed") { - return 3; + if (normalized === 'failed') { + return 3 } - if (normalized === "pending") { - return 2; + if (normalized === 'pending') { + return 2 } - return 0; + return 0 } diff --git a/apps/backend/src/services/admin/platform-config/cloudtentacles/mappers.test.ts b/apps/backend/src/services/admin/platform-config/cloudtentacles/mappers.test.ts index 8dc39268..27c37b7c 100644 --- a/apps/backend/src/services/admin/platform-config/cloudtentacles/mappers.test.ts +++ b/apps/backend/src/services/admin/platform-config/cloudtentacles/mappers.test.ts @@ -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') diff --git a/apps/backend/src/services/admin/platform-config/cloudtentacles/mappers.ts b/apps/backend/src/services/admin/platform-config/cloudtentacles/mappers.ts index e0d59080..295bc060 100644 --- a/apps/backend/src/services/admin/platform-config/cloudtentacles/mappers.ts +++ b/apps/backend/src/services/admin/platform-config/cloudtentacles/mappers.ts @@ -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()), + } } diff --git a/apps/backend/src/services/admin/platform-config/cloudtentacles/payloads.ts b/apps/backend/src/services/admin/platform-config/cloudtentacles/payloads.ts index 9e1ee3a3..389e507a 100644 --- a/apps/backend/src/services/admin/platform-config/cloudtentacles/payloads.ts +++ b/apps/backend/src/services/admin/platform-config/cloudtentacles/payloads.ts @@ -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, - }; + } } diff --git a/apps/backend/src/services/admin/platform-config/cloudtentacles/writes.ts b/apps/backend/src/services/admin/platform-config/cloudtentacles/writes.ts index 8ed13610..30f994b9 100644 --- a/apps/backend/src/services/admin/platform-config/cloudtentacles/writes.ts +++ b/apps/backend/src/services/admin/platform-config/cloudtentacles/writes.ts @@ -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), - }; + } } diff --git a/apps/backend/src/services/admin/platform-config/kuaishou-feifei-service.ts b/apps/backend/src/services/admin/platform-config/kuaishou-feifei-service.ts index e7be0aa4..4fd46136 100644 --- a/apps/backend/src/services/admin/platform-config/kuaishou-feifei-service.ts +++ b/apps/backend/src/services/admin/platform-config/kuaishou-feifei-service.ts @@ -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) { +function resolveAccessTokenStatus( + config: Pick, +) { if (!config.accessToken) { return { status: 'missing', diff --git a/apps/backend/src/services/admin/platform-config/notification-service.ts b/apps/backend/src/services/admin/platform-config/notification-service.ts index ff8b6181..42ef857c 100644 --- a/apps/backend/src/services/admin/platform-config/notification-service.ts +++ b/apps/backend/src/services/admin/platform-config/notification-service.ts @@ -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 = 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, + .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()) { diff --git a/apps/backend/src/services/admin/write-helpers.ts b/apps/backend/src/services/admin/write-helpers.ts index ab9370df..8136eaea 100644 --- a/apps/backend/src/services/admin/write-helpers.ts +++ b/apps/backend/src/services/admin/write-helpers.ts @@ -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' diff --git a/apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts b/apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts index 2fc01ec4..0f3ada3e 100644 --- a/apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts +++ b/apps/backend/src/services/admin/write/kuaishou-cloud-actions.ts @@ -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 { +async function getRequiredIndustryVoucherForTask( + task: TaskRow, +): Promise { const vouchers = await listKuaishouIndustryVouchersByTaskId(task.id) const firstVoucher = vouchers[0] if (firstVoucher) { @@ -499,7 +496,9 @@ async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise { +async function resolveIndustryVouchersForOrder( + platformOrderId: string, +): Promise { const normalizedOid = String(platformOrderId || '').trim() if (!normalizedOid) { return [] diff --git a/apps/backend/src/services/admin/write/kuaishou-cloud-helpers.ts b/apps/backend/src/services/admin/write/kuaishou-cloud-helpers.ts index e055b41d..fb68da4a 100644 --- a/apps/backend/src/services/admin/write/kuaishou-cloud-helpers.ts +++ b/apps/backend/src/services/admin/write/kuaishou-cloud-helpers.ts @@ -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) { diff --git a/apps/backend/src/services/admin/write/task-actions.ts b/apps/backend/src/services/admin/write/task-actions.ts index 790b5bad..4e4d6087 100644 --- a/apps/backend/src/services/admin/write/task-actions.ts +++ b/apps/backend/src/services/admin/write/task-actions.ts @@ -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), diff --git a/apps/backend/src/services/claim/claim-identity.ts b/apps/backend/src/services/claim/claim-identity.ts index 587b4c27..bdc9a283 100644 --- a/apps/backend/src/services/claim/claim-identity.ts +++ b/apps/backend/src/services/claim/claim-identity.ts @@ -69,7 +69,11 @@ export function getClaimIdentityFromTask(task: Partial | 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) { diff --git a/apps/backend/src/services/claim/claim-redeem-transition.test.ts b/apps/backend/src/services/claim/claim-redeem-transition.test.ts index 60962ef1..22df30ca 100644 --- a/apps/backend/src/services/claim/claim-redeem-transition.test.ts +++ b/apps/backend/src/services/claim/claim-redeem-transition.test.ts @@ -90,8 +90,5 @@ test('assertBoundUidMatchesExpected 在不匹配时抛错', () => { }) test('assertClaimExpectedUidReady 要求 claimIdentity', () => { - assert.throws( - () => assertClaimExpectedUidReady({ context_json: '{}' }), - /填写游戏 UID/, - ) + assert.throws(() => assertClaimExpectedUidReady({ context_json: '{}' }), /填写游戏 UID/) }) diff --git a/apps/backend/src/services/claim/claim-service.ts b/apps/backend/src/services/claim/claim-service.ts index 3c7ea908..4d4c3332 100644 --- a/apps/backend/src/services/claim/claim-service.ts +++ b/apps/backend/src/services/claim/claim-service.ts @@ -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 { diff --git a/apps/backend/src/services/claim/kuaishou-cloud-claim-context.ts b/apps/backend/src/services/claim/kuaishou-cloud-claim-context.ts index ef668d31..f862c596 100644 --- a/apps/backend/src/services/claim/kuaishou-cloud-claim-context.ts +++ b/apps/backend/src/services/claim/kuaishou-cloud-claim-context.ts @@ -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 { diff --git a/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts b/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts index 2550e953..3c65b048 100644 --- a/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts +++ b/apps/backend/src/services/claim/kuaishou-cloud-claim-service.ts @@ -61,7 +61,9 @@ async function requireExecutorAction( } function isKuaishouCloudBindingReady(flow: ReturnType) { - 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 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 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 = (value: unknown) => T diff --git a/apps/backend/src/services/config/json-config-migration-service.ts b/apps/backend/src/services/config/json-config-migration-service.ts index f74cfd32..b142be3a 100644 --- a/apps/backend/src/services/config/json-config-migration-service.ts +++ b/apps/backend/src/services/config/json-config-migration-service.ts @@ -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) diff --git a/apps/backend/src/services/dev-mock/dev-mock-service.ts b/apps/backend/src/services/dev-mock/dev-mock-service.ts index 4d375ea6..049f5236 100644 --- a/apps/backend/src/services/dev-mock/dev-mock-service.ts +++ b/apps/backend/src/services/dev-mock/dev-mock-service.ts @@ -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 { +export async function createLewanMockClaim( + input: { + step?: unknown + orderNo?: unknown + productNo?: unknown + uid?: unknown + items?: unknown + frontendBaseUrl?: unknown + } = {}, +): Promise { 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 { +export async function createFeifeiMockClaim( + input: { + step?: unknown + orderNo?: unknown + productName?: unknown + productCode?: unknown + uid?: unknown + h5Url?: unknown + frontendBaseUrl?: unknown + } = {}, +): Promise { 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 { +export async function createAffiliateDashMockClaim( + input: { + step?: unknown + orderNo?: unknown + productName?: unknown + productSku?: unknown + uid?: unknown + frontendBaseUrl?: unknown + } = {}, +): Promise { 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 { +export async function createKuaishouIndustryVoucherMockData( + input: { + sellerId?: unknown + } = {}, +): Promise { 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 直接模拟发货成功)'] diff --git a/apps/backend/src/services/fulfillment/affiliate-dash/index.test.ts b/apps/backend/src/services/fulfillment/affiliate-dash/index.test.ts index e1d0c15d..76853c8d 100644 --- a/apps/backend/src/services/fulfillment/affiliate-dash/index.test.ts +++ b/apps/backend/src/services/fulfillment/affiliate-dash/index.test.ts @@ -50,10 +50,7 @@ function buildTask(overrides: Partial = {}): TaskRow { test('buildAffiliateDashClientOrderNo uses the platform order number for a single task', () => { const task = buildTask() - assert.equal( - buildAffiliateDashClientOrderNo(task, [task]), - '2622300001260431', - ) + assert.equal(buildAffiliateDashClientOrderNo(task, [task]), '2622300001260431') }) test('buildAffiliateDashClientOrderNo appends a stable position for split affiliate-dash tasks', () => { diff --git a/apps/backend/src/services/fulfillment/affiliate-dash/index.ts b/apps/backend/src/services/fulfillment/affiliate-dash/index.ts index 8c0fdd44..94738508 100644 --- a/apps/backend/src/services/fulfillment/affiliate-dash/index.ts +++ b/apps/backend/src/services/fulfillment/affiliate-dash/index.ts @@ -52,10 +52,13 @@ export async function prepareAffiliateDashTask(task: TaskRow) { } if (!flow.sku) { - throw createHttpError('affiliate-dash 商品 sku 缺失,请配置 91 商品 → affiliate_dash sku 映射', { - statusCode: 409, - errorCode: 'affiliate_dash_sku_missing', - }) + throw createHttpError( + 'affiliate-dash 商品 sku 缺失,请配置 91 商品 → affiliate_dash sku 映射', + { + statusCode: 409, + errorCode: 'affiliate_dash_sku_missing', + }, + ) } const siblingTasks = await listTasksByOrderId(task.order_id) @@ -97,13 +100,18 @@ export async function prepareAffiliateDashTask(task: TaskRow) { ) } if (!consumeResult.ok) { - logIntegration('[affiliate-dash]', '建单成功但电子凭证核销失败,delivered 回调将兜底重试', { - taskId: task.id, - orderNo: order.orderNo, - voucherCount: consumeResult.vouchers.length, - failedCount: consumeResult.failed.length, - errorMessage: consumeResult.failed[0]?.errorMessage || '', - }, { level: 'warn' }) + logIntegration( + '[affiliate-dash]', + '建单成功但电子凭证核销失败,delivered 回调将兜底重试', + { + taskId: task.id, + orderNo: order.orderNo, + voucherCount: consumeResult.vouchers.length, + failedCount: consumeResult.failed.length, + errorMessage: consumeResult.failed[0]?.errorMessage || '', + }, + { level: 'warn' }, + ) } } else { nextFlow.consumeStatus = 'not_required' @@ -231,11 +239,16 @@ export async function syncAffiliateDashTaskStatus( resultMessage = consumeResult.failed[0]?.errorMessage || '电子凭证核销失败,请人工处理' lastError = resultMessage nextFlow.consumeStatus = 'failed' - logIntegration('[affiliate-dash]', 'affiliate-dash 履约完成但核销失败', { - taskId: task.id, - orderNo: flow.orderNo, - errorMessage: resultMessage, - }, { level: 'warn' }) + logIntegration( + '[affiliate-dash]', + 'affiliate-dash 履约完成但核销失败', + { + taskId: task.id, + orderNo: flow.orderNo, + errorMessage: resultMessage, + }, + { level: 'warn' }, + ) } break } @@ -256,7 +269,12 @@ export async function syncAffiliateDashTaskStatus( } const isAlreadyTerminal = ( - [TASK_STATUS.REDEEMED, TASK_STATUS.MANUAL_REVIEW, TASK_STATUS.CLOSED, TASK_STATUS.FAILED] as string[] + [ + TASK_STATUS.REDEEMED, + TASK_STATUS.MANUAL_REVIEW, + TASK_STATUS.CLOSED, + TASK_STATUS.FAILED, + ] as string[] ).includes(task.task_status) // 终态保护:已收敛成功的任务,非 delivered 状态不允许降级(如轮询时平台详情短暂返回 // delivering/paid 会把 REDEEMED 打回 REDEEMING,导致结果页闪烁/倒退)。 @@ -316,9 +334,8 @@ export type AffiliateDashFlow = { } export function normalizeAffiliateDashFlow(value: unknown): AffiliateDashFlow { - const source = value && typeof value === 'object' && !Array.isArray(value) - ? value as JsonObject - : {} + const source = + value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {} return { flowType: 'affiliate_dash', @@ -397,10 +414,15 @@ async function preflightAffiliateDashWallet(sku: string) { if ((error as { errorCode?: string })?.errorCode === 'affiliate_dash_wallet_not_enough') { throw error } - logIntegration('[affiliate-dash]', '余额预检失败(忽略,继续下单)', { - sku, - error: error instanceof Error ? error.message : String(error), - }, { level: 'warn' }) + logIntegration( + '[affiliate-dash]', + '余额预检失败(忽略,继续下单)', + { + sku, + error: error instanceof Error ? error.message : String(error), + }, + { level: 'warn' }, + ) } } diff --git a/apps/backend/src/services/fulfillment/delivery-link-service.test.ts b/apps/backend/src/services/fulfillment/delivery-link-service.test.ts index fe559f23..c03fb102 100644 --- a/apps/backend/src/services/fulfillment/delivery-link-service.test.ts +++ b/apps/backend/src/services/fulfillment/delivery-link-service.test.ts @@ -6,11 +6,13 @@ import { resolveTaskDeliveryLink } from './delivery-link-service.js' import type { TaskRow } from '../../types/repository/rows.js' test('resolveTaskDeliveryLink 为 cloud 任务返回内部领取链接', async () => { - const result = await resolveTaskDeliveryLink(createTask({ - executor_key: 'kuaishou_ct_assisted', - primary_claim_token: 'cloud-token', - primary_claim_expires_at: '2026-07-09T00:00:00.000Z', - })) + const result = await resolveTaskDeliveryLink( + createTask({ + executor_key: 'kuaishou_ct_assisted', + primary_claim_token: 'cloud-token', + primary_claim_expires_at: '2026-07-09T00:00:00.000Z', + }), + ) assert.deepEqual(result, { claimUrl: buildClaimUrl('cloud-token'), @@ -19,11 +21,13 @@ test('resolveTaskDeliveryLink 为 cloud 任务返回内部领取链接', async ( }) test('resolveTaskDeliveryLink 兼容历史 kuaishou-industry cloud 任务', async () => { - const result = await resolveTaskDeliveryLink(createTask({ - executor_key: 'kuaishou-industry', - claim_token: 'industry-token', - claim_expires_at: '2026-07-09T08:00:00.000Z', - })) + const result = await resolveTaskDeliveryLink( + createTask({ + executor_key: 'kuaishou-industry', + claim_token: 'industry-token', + claim_expires_at: '2026-07-09T08:00:00.000Z', + }), + ) assert.deepEqual(result, { claimUrl: buildClaimUrl('industry-token'), @@ -32,18 +36,20 @@ test('resolveTaskDeliveryLink 兼容历史 kuaishou-industry cloud 任务', asyn }) test('resolveTaskDeliveryLink 为 feifei 任务返回本站领取链接', async () => { - const result = await resolveTaskDeliveryLink(createTask({ - executor_key: 'kuaishou_feifei', - primary_claim_token: 'feifei-token', - primary_claim_expires_at: '2026-07-09T12:00:00.000Z', - context_json: { - kuaishouFeifei: { - h5: { - rechargeUrl: 'https://feifei.example.com/h5/bind?code=very-long', + const result = await resolveTaskDeliveryLink( + createTask({ + executor_key: 'kuaishou_feifei', + primary_claim_token: 'feifei-token', + primary_claim_expires_at: '2026-07-09T12:00:00.000Z', + context_json: { + kuaishouFeifei: { + h5: { + rechargeUrl: 'https://feifei.example.com/h5/bind?code=very-long', + }, }, }, - }, - })) + }), + ) assert.deepEqual(result, { claimUrl: buildClaimUrl('feifei-token'), @@ -52,9 +58,11 @@ test('resolveTaskDeliveryLink 为 feifei 任务返回本站领取链接', async }) test('resolveTaskDeliveryLink 对人工履约任务返回 null', async () => { - const result = await resolveTaskDeliveryLink(createTask({ - executor_key: 'manual_dispatch', - })) + const result = await resolveTaskDeliveryLink( + createTask({ + executor_key: 'manual_dispatch', + }), + ) assert.equal(result, null) }) diff --git a/apps/backend/src/services/fulfillment/delivery-link-service.ts b/apps/backend/src/services/fulfillment/delivery-link-service.ts index 8221fa71..42477a80 100644 --- a/apps/backend/src/services/fulfillment/delivery-link-service.ts +++ b/apps/backend/src/services/fulfillment/delivery-link-service.ts @@ -4,8 +4,6 @@ import type { TaskRow } from '../../types/repository/rows.js' export type TaskDeliveryLink = FulfillmentDeliveryLink -export async function resolveTaskDeliveryLink( - task: TaskRow, -): Promise { +export async function resolveTaskDeliveryLink(task: TaskRow): Promise { return resolveFulfillmentDeliveryLink(task) } diff --git a/apps/backend/src/services/fulfillment/executors/kuaishou-cloud-executor.ts b/apps/backend/src/services/fulfillment/executors/kuaishou-cloud-executor.ts index 2bdede41..944c491a 100644 --- a/apps/backend/src/services/fulfillment/executors/kuaishou-cloud-executor.ts +++ b/apps/backend/src/services/fulfillment/executors/kuaishou-cloud-executor.ts @@ -1,8 +1,5 @@ import { buildClaimUrl } from '../../claim/claim-service.js' -import { - shouldEnsureKuaishouCloudClaimLink, - TASK_STATUS, -} from '../../../domain/task-status.js' +import { shouldEnsureKuaishouCloudClaimLink, TASK_STATUS } from '../../../domain/task-status.js' import { confirmKuaishouCloudTaskRole, dispatchKuaishouCloudFulfillmentTask, @@ -95,9 +92,7 @@ async function prepareBinding( options: FulfillmentActionOptions = {}, ): Promise { // 后台/显式 force:旧号可被 CT 回收,忽略退号失败并取新号+新绑链 - const force = - options.force === true || - String(options.source || '').includes('admin_') + const force = options.force === true || String(options.source || '').includes('admin_') const result = await prepareKuaishouCloudFulfillmentTask(task, { source: options.source || 'executor_prepare_binding', actor: options.actor, diff --git a/apps/backend/src/services/fulfillment/executors/registry.ts b/apps/backend/src/services/fulfillment/executors/registry.ts index 1cdb3ce6..97603093 100644 --- a/apps/backend/src/services/fulfillment/executors/registry.ts +++ b/apps/backend/src/services/fulfillment/executors/registry.ts @@ -78,51 +78,30 @@ async function runExecutorAction( return handler(task, options) } -export function prepareFulfillmentBinding( - task: TaskRow, - options: FulfillmentActionOptions = {}, -) { +export function prepareFulfillmentBinding(task: TaskRow, options: FulfillmentActionOptions = {}) { return runExecutorAction(task, 'prepareBinding', options) } -export function rebindFulfillmentRole( - task: TaskRow, - options: FulfillmentActionOptions = {}, -) { +export function rebindFulfillmentRole(task: TaskRow, options: FulfillmentActionOptions = {}) { return runExecutorAction(task, 'rebindRole', options) } -export function refreshFulfillmentRole( - task: TaskRow, - options: FulfillmentActionOptions = {}, -) { +export function refreshFulfillmentRole(task: TaskRow, options: FulfillmentActionOptions = {}) { return runExecutorAction(task, 'refreshRole', options) } -export function confirmFulfillmentRole( - task: TaskRow, - options: FulfillmentActionOptions = {}, -) { +export function confirmFulfillmentRole(task: TaskRow, options: FulfillmentActionOptions = {}) { return runExecutorAction(task, 'confirmRole', options) } -export function redeemFulfillmentTask( - task: TaskRow, - options: FulfillmentActionOptions = {}, -) { +export function redeemFulfillmentTask(task: TaskRow, options: FulfillmentActionOptions = {}) { return runExecutorAction(task, 'redeemTask', options) } -export function dispatchFulfillmentTask( - task: TaskRow, - options: FulfillmentActionOptions = {}, -) { +export function dispatchFulfillmentTask(task: TaskRow, options: FulfillmentActionOptions = {}) { return runExecutorAction(task, 'dispatchTask', options) } -export function returnFulfillmentNumber( - task: TaskRow, - options: FulfillmentActionOptions = {}, -) { +export function returnFulfillmentNumber(task: TaskRow, options: FulfillmentActionOptions = {}) { return runExecutorAction(task, 'returnNumber', options) } diff --git a/apps/backend/src/services/fulfillment/executors/types.ts b/apps/backend/src/services/fulfillment/executors/types.ts index d5b6f8d5..d225fbeb 100644 --- a/apps/backend/src/services/fulfillment/executors/types.ts +++ b/apps/backend/src/services/fulfillment/executors/types.ts @@ -10,7 +10,8 @@ export const FULFILLMENT_EXECUTOR_KEYS = { } as const export type FulfillmentExecutorKey = - (typeof FULFILLMENT_EXECUTOR_KEYS)[keyof typeof FULFILLMENT_EXECUTOR_KEYS] | (string & {}) + | (typeof FULFILLMENT_EXECUTOR_KEYS)[keyof typeof FULFILLMENT_EXECUTOR_KEYS] + | (string & {}) export type FulfillmentDeliveryLink = { claimUrl: string @@ -53,10 +54,7 @@ export type FulfillmentActionResult = { export type FulfillmentExecutor = { key: FulfillmentExecutorKey - preparePaidTask?: ( - task: TaskRow, - deps: FulfillmentPrepareDeps, - ) => Promise + preparePaidTask?: (task: TaskRow, deps: FulfillmentPrepareDeps) => Promise resolveDeliveryLink?: (task: TaskRow) => Promise /** lewan:准备绑定资源(虚拟号 / bindUrl) */ prepareBinding?: ( @@ -104,8 +102,10 @@ export function normalizeExecutorKey(value: unknown): FulfillmentExecutorKey { export function isKuaishouCloudExecutor(value: unknown): boolean { const executorKey = normalizeExecutorKey(value) - return executorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD || + return ( + executorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD || executorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_INDUSTRY + ) } export function isKuaishouFeifeiExecutor(value: unknown): boolean { diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/account-selector.test.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/account-selector.test.ts index ba62f268..8067bf18 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/account-selector.test.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/account-selector.test.ts @@ -65,7 +65,8 @@ test('selectCloudtentaclesSourceForFulfillment 无号码时忽略残留固定账 }, }, { - resolveContextBySourceKeys: (sourceKeys) => contexts[String(sourceKeys[0]) as keyof typeof contexts], + resolveContextBySourceKeys: (sourceKeys) => + contexts[String(sourceKeys[0]) as keyof typeof contexts], listSourceLoadStats: async () => [ { sourceKey: 'account-a', activeCount: 20, redeemingCount: 0 }, { sourceKey: 'account-b', activeCount: 1, redeemingCount: 0 }, @@ -86,7 +87,8 @@ test('selectCloudtentaclesSourceForFulfillment 跳过真实占用已达20的账 const selection = await selectCloudtentaclesSourceForFulfillment( { binding: { cloudSourceKeys: ['account-a', 'account-b'] } }, { - resolveContextBySourceKeys: (sourceKeys) => contexts[String(sourceKeys[0]) as keyof typeof contexts], + resolveContextBySourceKeys: (sourceKeys) => + contexts[String(sourceKeys[0]) as keyof typeof contexts], listSourceLoadStats: async () => [], listVirtualNumbers: async (payload) => { const count = payload.sourceKey === 'account-a' ? 20 : 3 @@ -115,12 +117,14 @@ test('selectCloudtentaclesSourceForFulfillment 不把 status=0 的空闲号码 const selection = await selectCloudtentaclesSourceForFulfillment( { binding: { cloudSourceKeys: ['account-a', 'account-b'] } }, { - resolveContextBySourceKeys: (sourceKeys) => contexts[String(sourceKeys[0]) as keyof typeof contexts], + resolveContextBySourceKeys: (sourceKeys) => + contexts[String(sourceKeys[0]) as keyof typeof contexts], listSourceLoadStats: async () => [], listVirtualNumbers: async (payload) => { - const items = payload.sourceKey === 'account-a' - ? Array.from({ length: 20 }, (_, id) => ({ id: id + 1, status: 0 })) - : Array.from({ length: 3 }, (_, id) => ({ id: id + 1, status: 1 })) + const items = + payload.sourceKey === 'account-a' + ? Array.from({ length: 20 }, (_, id) => ({ id: id + 1, status: 0 })) + : Array.from({ length: 3 }, (_, id) => ({ id: id + 1, status: 1 })) return { baseUrl: 'https://cloud.example.com', key: '1', @@ -145,7 +149,8 @@ test('selectCloudtentaclesSourceForFulfillment 真实占用优先于数据库历 const selection = await selectCloudtentaclesSourceForFulfillment( { binding: { cloudSourceKeys: ['account-a', 'account-b'] } }, { - resolveContextBySourceKeys: (sourceKeys) => contexts[String(sourceKeys[0]) as keyof typeof contexts], + resolveContextBySourceKeys: (sourceKeys) => + contexts[String(sourceKeys[0]) as keyof typeof contexts], listSourceLoadStats: async () => [ { sourceKey: 'account-a', activeCount: 99, redeemingCount: 10 }, { sourceKey: 'account-b', activeCount: 0, redeemingCount: 0 }, diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/binding-freeze.test.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/binding-freeze.test.ts index b5b8cae1..c9274d73 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/binding-freeze.test.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/binding-freeze.test.ts @@ -10,14 +10,8 @@ import { } from './domain.js' test('isKuaishouCloudDispatchSucceeded 识别 dispatch.success', () => { - assert.equal( - isKuaishouCloudDispatchSucceeded({ dispatch: { status: 'success' } }), - true, - ) - assert.equal( - isKuaishouCloudDispatchSucceeded({ dispatch: { status: 'pending' } }), - false, - ) + assert.equal(isKuaishouCloudDispatchSucceeded({ dispatch: { status: 'success' } }), true) + assert.equal(isKuaishouCloudDispatchSucceeded({ dispatch: { status: 'pending' } }), false) }) test('isKuaishouCloudBindingMutationFrozen:dispatch.success 即使 status=waiting_binding 也冻结', () => { diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/binding-resources.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/binding-resources.ts index d8fd8634..25a4b3ed 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/binding-resources.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/binding-resources.ts @@ -1,5 +1,5 @@ -import { createHttpError } from "../../../utils/http.js"; -import { normalizeProductName } from "../product-resolution-service.js"; +import { createHttpError } from '../../../utils/http.js' +import { normalizeProductName } from '../product-resolution-service.js' import { appointCloudtentaclesVirtualNumber, backCloudtentaclesVirtualNumber, @@ -7,107 +7,95 @@ import { generateCloudtentaclesLoginCode, getCloudtentaclesBindUrl, verifyCloudtentaclesLoginCode, -} from "../../platforms/cloudtentacles/virtual-number-service.js"; -import { KUAISHOU_CLOUD_FIXED_VN_KEY, type JsonObject } from "./domain.js"; -import { logInfo } from "../../../utils/logger.js"; +} from '../../platforms/cloudtentacles/virtual-number-service.js' +import { KUAISHOU_CLOUD_FIXED_VN_KEY, type JsonObject } from './domain.js' +import { logInfo } from '../../../utils/logger.js' /** 账号号码配额占满后冷却:避免领取页轮询 / open-91 交付每轮都狂打上游 */ -const APPOINT_QUOTA_COOLDOWN_MS = 60_000; -const appointQuotaCooldowns = new Map(); +const APPOINT_QUOTA_COOLDOWN_MS = 60_000 +const appointQuotaCooldowns = new Map() export function resolveKuaishouCloudBindingResources( flow: JsonObject, - { skuItems = [], knapsackItems = [] }: { skuItems?: unknown[], knapsackItems?: unknown[] } = {} + { skuItems = [], knapsackItems = [] }: { skuItems?: unknown[]; knapsackItems?: unknown[] } = {}, ) { - const normalizedSkuItems = Array.isArray(skuItems) - ? skuItems.filter(isCloudSkuLikeItem) - : []; + const normalizedSkuItems = Array.isArray(skuItems) ? skuItems.filter(isCloudSkuLikeItem) : [] const normalizedKnapsackItems = Array.isArray(knapsackItems) ? knapsackItems.filter(isCloudSkuLikeItem) - : []; - const currentSkuId = Number(flow?.binding?.skuId || 0) || 0; - const currentSkuName = String(flow?.binding?.skuName || "").trim(); - const nameCandidates = collectKuaishouCloudNameCandidates(flow); + : [] + const currentSkuId = Number(flow?.binding?.skuId || 0) || 0 + const currentSkuName = String(flow?.binding?.skuName || '').trim() + const nameCandidates = collectKuaishouCloudNameCandidates(flow) const skuItemById = currentSkuId > 0 - ? normalizedSkuItems.find( - (item) => Number(item.id || 0) === currentSkuId - ) || null - : null; + ? normalizedSkuItems.find((item) => Number(item.id || 0) === currentSkuId) || null + : null const knapsackItemById = currentSkuId > 0 - ? normalizedKnapsackItems.find( - (item) => Number(item.id || 0) === currentSkuId - ) || null - : null; + ? normalizedKnapsackItems.find((item) => Number(item.id || 0) === currentSkuId) || null + : null if (skuItemById || knapsackItemById) { - const matchedItem = skuItemById || knapsackItemById; + const matchedItem = skuItemById || knapsackItemById return { skuId: Number(matchedItem?.id || 0) || 0, - skuName: String(currentSkuName || matchedItem?.name || "").trim(), + skuName: String(currentSkuName || matchedItem?.name || '').trim(), vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY, skuItem: skuItemById, knapsackItem: knapsackItemById, resolvedByName: false, - }; + } } - const matchedSkuItem = findCloudItemByNames( - normalizedSkuItems, - nameCandidates - ); + const matchedSkuItem = findCloudItemByNames(normalizedSkuItems, nameCandidates) const matchedKnapsackItem = findCloudItemByNames( normalizedKnapsackItems, nameCandidates, - matchedSkuItem ? Number(matchedSkuItem.id || 0) : 0 - ); - const matchedItem = matchedSkuItem || matchedKnapsackItem; + matchedSkuItem ? Number(matchedSkuItem.id || 0) : 0, + ) + const matchedItem = matchedSkuItem || matchedKnapsackItem return { skuId: Number(matchedItem?.id || 0) || 0, - skuName: String(currentSkuName || matchedItem?.name || "").trim(), + skuName: String(currentSkuName || matchedItem?.name || '').trim(), vnKey: KUAISHOU_CLOUD_FIXED_VN_KEY, skuItem: matchedSkuItem, knapsackItem: matchedKnapsackItem, resolvedByName: Boolean(matchedItem), - }; + } } export function resolveKuaishouCloudVnKeyCandidates(_input: JsonObject = {}) { - return [KUAISHOU_CLOUD_FIXED_VN_KEY]; + return [KUAISHOU_CLOUD_FIXED_VN_KEY] } export async function prepareKuaishouCloudBindResourceWithFallback(input: JsonObject = {}) { - const { cloudContext = {}, vnKeyCandidates = [] } = input; - const candidates = Array.isArray(vnKeyCandidates) ? vnKeyCandidates : []; - const sourceKey = String(cloudContext.resolvedSourceKey || "").trim(); - let lastError = null; + const { cloudContext = {}, vnKeyCandidates = [] } = input + const candidates = Array.isArray(vnKeyCandidates) ? vnKeyCandidates : [] + const sourceKey = String(cloudContext.resolvedSourceKey || '').trim() + let lastError = null for (const vnKey of candidates) { - let vnId = 0; - let vnPhone = ""; - const cooldownKey = `${sourceKey}|${vnKey}`; + let vnId = 0 + let vnPhone = '' + const cooldownKey = `${sourceKey}|${vnKey}` - const cooldownUntil = appointQuotaCooldowns.get(cooldownKey) || 0; + const cooldownUntil = appointQuotaCooldowns.get(cooldownKey) || 0 if (cooldownUntil > Date.now()) { - throw createHttpError( - "账号虚拟号配额已满,请先退回已占用号码或稍后重试", - { - statusCode: 409, - errorCode: "cloudtentacles_vn_quota_cooldown", - } - ); + throw createHttpError('账号虚拟号配额已满,请先退回已占用号码或稍后重试', { + statusCode: 409, + errorCode: 'cloudtentacles_vn_quota_cooldown', + }) } try { const appointed = await appointCloudtentaclesVirtualNumber({ ...cloudContext, key: vnKey, - }); - vnId = Number(appointed.item?.id || 0); - vnPhone = String(appointed.item?.phone || "").trim(); + }) + vnId = Number(appointed.item?.id || 0) + vnPhone = String(appointed.item?.phone || '').trim() logInfo('[kuaishou-cloud/binding]', '虚拟号申请成功', { sourceKey, @@ -115,51 +103,51 @@ export async function prepareKuaishouCloudBindResourceWithFallback(input: JsonOb purpose: String(input.purpose || 'prepare_binding'), vnKey, vnId, - }); + }) if (!vnId || !vnPhone) { - throw createHttpError("申请虚拟号成功但返回数据不完整", { + throw createHttpError('申请虚拟号成功但返回数据不完整', { statusCode: 502, - errorCode: "kuaishou_cloud_invalid_vn", - }); + errorCode: 'kuaishou_cloud_invalid_vn', + }) } await generateCloudtentaclesLoginCode({ ...cloudContext, key: vnKey, id: vnId, - }); + }) const fetchedCode = await fetchCloudtentaclesVirtualNumberCode({ ...cloudContext, key: vnKey, phone: vnPhone, - }); + }) await verifyCloudtentaclesLoginCode({ ...cloudContext, key: vnKey, id: vnId, code: fetchedCode.code, - }); + }) const bindUrlResult = await getCloudtentaclesBindUrl({ ...cloudContext, key: vnKey, id: vnId, - }); + }) return { vnKey, vnId, vnPhone, - bindUrl: String(bindUrlResult.bindUrl || "").trim(), - }; + bindUrl: String(bindUrlResult.bindUrl || '').trim(), + } } catch (error) { - lastError = error; + lastError = error if (isAppointQuotaExhaustedError(error)) { - appointQuotaCooldowns.set(cooldownKey, Date.now() + APPOINT_QUOTA_COOLDOWN_MS); + appointQuotaCooldowns.set(cooldownKey, Date.now() + APPOINT_QUOTA_COOLDOWN_MS) } if (vnId > 0) { @@ -168,112 +156,108 @@ export async function prepareKuaishouCloudBindResourceWithFallback(input: JsonOb ...cloudContext, key: vnKey, id: vnId, - }); + }) } catch { // 退号失败保留主错误 } } if (!isRecoverableKuaishouCloudVnKeyError(error)) { - throw error; + throw error } } } throw ( lastError || - createHttpError("没有找到可用的 VN Key", { + createHttpError('没有找到可用的 VN Key', { statusCode: 409, - errorCode: "kuaishou_cloud_missing_binding_config", + errorCode: 'kuaishou_cloud_missing_binding_config', }) - ); + ) } function collectKuaishouCloudNameCandidates(flow: JsonObject) { return Array.from( new Set( [ - String(flow?.binding?.skuName || "").trim(), - String(flow?.internalSkuName || "").trim(), - String(flow?.internalSkuCode || "").trim(), - ].filter(Boolean) - ) - ); + String(flow?.binding?.skuName || '').trim(), + String(flow?.internalSkuName || '').trim(), + String(flow?.internalSkuCode || '').trim(), + ].filter(Boolean), + ), + ) } function findCloudItemByNames(items: JsonObject[], nameCandidates: string[], preferredId = 0) { - const normalizedItems = Array.isArray(items) ? items : []; + const normalizedItems = Array.isArray(items) ? items : [] const normalizedNames = nameCandidates .map((item: string) => ({ - raw: String(item || "").trim(), + raw: String(item || '').trim(), normalized: normalizeProductName(item), })) - .filter((item) => item.raw && item.normalized); + .filter((item) => item.raw && item.normalized) if (normalizedNames.length === 0 || normalizedItems.length === 0) { return preferredId > 0 - ? normalizedItems.find((item: JsonObject) => Number(item.id || 0) === preferredId) || - null - : null; + ? normalizedItems.find((item: JsonObject) => Number(item.id || 0) === preferredId) || null + : null } if (preferredId > 0) { const preferred = - normalizedItems.find((item: JsonObject) => Number(item.id || 0) === preferredId) || - null; + normalizedItems.find((item: JsonObject) => Number(item.id || 0) === preferredId) || null if (preferred) { - return preferred; + return preferred } } const exactMatches = normalizedItems.filter((item: JsonObject) => { - const itemName = normalizeProductName(item.name); + const itemName = normalizeProductName(item.name) return normalizedNames.some( - (candidate: { normalized: string }) => candidate.normalized === itemName - ); - }); + (candidate: { normalized: string }) => candidate.normalized === itemName, + ) + }) if (exactMatches.length > 0) { - return exactMatches[0]; + return exactMatches[0] } const partialMatches = normalizedItems.filter((item: JsonObject) => { - const itemName = normalizeProductName(item.name); + const itemName = normalizeProductName(item.name) return normalizedNames.some( (candidate: { normalized: string }) => - itemName.includes(candidate.normalized) || - candidate.normalized.includes(itemName) - ); - }); + itemName.includes(candidate.normalized) || candidate.normalized.includes(itemName), + ) + }) if (partialMatches.length > 0) { return partialMatches.sort( - (left, right) => - String(left.name || "").length - String(right.name || "").length - )[0]; + (left, right) => String(left.name || '').length - String(right.name || '').length, + )[0] } - return null; + return null } function isCloudSkuLikeItem(item: unknown): item is JsonObject { - const current = item && typeof item === "object" ? item as JsonObject : {}; - return Number(current.id || 0) > 0; + const current = item && typeof item === 'object' ? (item as JsonObject) : {} + return Number(current.id || 0) > 0 } function isAppointQuotaExhaustedError(error: unknown) { - const current = error && typeof error === "object" ? error as JsonObject : {}; + const current = error && typeof error === 'object' ? (error as JsonObject) : {} return ( - String(current.errorCode || current.code || "").trim() === - "cloudtentacles_vn_appoint_failed" && - String(current.message || "").trim().includes("最多同时占用") - ); + String(current.errorCode || current.code || '').trim() === 'cloudtentacles_vn_appoint_failed' && + String(current.message || '') + .trim() + .includes('最多同时占用') + ) } function isRecoverableKuaishouCloudVnKeyError(error: unknown) { - const current = error && typeof error === "object" ? error as JsonObject : {}; - const errorCode = String(current.errorCode || current.code || "").trim(); - const errorMessage = String(current.message || "").trim(); + const current = error && typeof error === 'object' ? (error as JsonObject) : {} + const errorCode = String(current.errorCode || current.code || '').trim() + const errorMessage = String(current.message || '').trim() return ( - errorCode === "cloudtentacles_vn_bind_url_failed" && - errorMessage.includes("不支持的游戏类型") - ); + errorCode === 'cloudtentacles_vn_bind_url_failed' && errorMessage.includes('不支持的游戏类型') + ) } diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/cloudtentacles-context.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/cloudtentacles-context.ts index 3d5f07dc..12ccb2a4 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/cloudtentacles-context.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/cloudtentacles-context.ts @@ -1,11 +1,11 @@ -import { createHttpError } from "../../../utils/http.js"; +import { createHttpError } from '../../../utils/http.js' import { normalizeCloudtentaclesDeviceId, normalizeCloudtentaclesDeviceType, -} from "../../platforms/cloudtentacles/defaults.js"; -import { getCloudtentaclesSourceByKey } from "../../platforms/cloudtentacles/source-config-service.js"; -import { getCloudtentaclesSessionStateByKey } from "../../platforms/cloudtentacles/session-state-service.js"; -import { normalizeStringArray } from "./domain.js"; +} from '../../platforms/cloudtentacles/defaults.js' +import { getCloudtentaclesSourceByKey } from '../../platforms/cloudtentacles/source-config-service.js' +import { getCloudtentaclesSessionStateByKey } from '../../platforms/cloudtentacles/session-state-service.js' +import { normalizeStringArray } from './domain.js' /** * 严格模式:只解析列表中第一个账号(调用方均把任务实际取号账号 resolvedSourceKey 放首位), @@ -14,21 +14,19 @@ import { normalizeStringArray } from "./domain.js"; * 退号、取链、发货等「操作任务已有号码」的场景必须使用此函数。 * 将实际使用的 resolvedSourceKey 也返回,确保后续操作使用同一个 sourceKey。 */ -export function resolvePersistedCloudtentaclesContextBySourceKeys( - sourceKeys: unknown[] = [] -) { - const candidates = [...new Set(normalizeStringArray(sourceKeys))]; +export function resolvePersistedCloudtentaclesContextBySourceKeys(sourceKeys: unknown[] = []) { + const candidates = [...new Set(normalizeStringArray(sourceKeys))] if (candidates.length === 0) { - throw createHttpError( - "cloudtentacles 没有可用账号,请先到平台配置完成账号配置", - { statusCode: 409, errorCode: "kuaishou_cloud_no_source_keys" } - ); + throw createHttpError('cloudtentacles 没有可用账号,请先到平台配置完成账号配置', { + statusCode: 409, + errorCode: 'kuaishou_cloud_no_source_keys', + }) } - const sourceKey = String(candidates[0] || "").trim(); + const sourceKey = String(candidates[0] || '').trim() - return resolveSingleCloudtentaclesAccountContext(sourceKey); + return resolveSingleCloudtentaclesAccountContext(sourceKey) } /** @@ -36,77 +34,69 @@ export function resolvePersistedCloudtentaclesContextBySourceKeys( * 仅用于「取新号」场景(新号码归属被选中的账号,不会产生跨账号孤儿), * 以及 account-selector 单候选解析。严禁用于退号/取链/发货等操作已有号码的场景。 */ -export function resolvePersistedCloudtentaclesContextWithFallback( - sourceKeys: unknown[] = [] -) { - const candidates = [...new Set(normalizeStringArray(sourceKeys))]; +export function resolvePersistedCloudtentaclesContextWithFallback(sourceKeys: unknown[] = []) { + const candidates = [...new Set(normalizeStringArray(sourceKeys))] - let lastError: unknown = null; + let lastError: unknown = null for (const sourceKey of candidates) { try { - return resolveSingleCloudtentaclesAccountContext(sourceKey); + return resolveSingleCloudtentaclesAccountContext(sourceKey) } catch (error) { - lastError = error; + lastError = error } } throw ( lastError || - createHttpError( - "所有 cloudtentacles 账号均不可用,请先到平台配置完成登录校验", - { statusCode: 409, errorCode: "kuaishou_cloud_all_source_keys_exhausted" } - ) - ); + createHttpError('所有 cloudtentacles 账号均不可用,请先到平台配置完成登录校验', { + statusCode: 409, + errorCode: 'kuaishou_cloud_all_source_keys_exhausted', + }) + ) } function resolveSingleCloudtentaclesAccountContext(sourceKey: string) { - const source = getCloudtentaclesSourceByKey(sourceKey); - const session = getCloudtentaclesSessionStateByKey(sourceKey); + const source = getCloudtentaclesSourceByKey(sourceKey) + const session = getCloudtentaclesSessionStateByKey(sourceKey) if (!source) { throw createHttpError( `cloudtentacles 账号 ${sourceKey} 不存在(取号账号已变更或被删除,无法继续操作其虚拟号)`, - { statusCode: 409, errorCode: "kuaishou_cloud_source_missing" } - ); + { statusCode: 409, errorCode: 'kuaishou_cloud_source_missing' }, + ) } if (source.enabled === false) { - throw createHttpError( - `cloudtentacles 账号 ${sourceKey} 已停用`, - { statusCode: 409, errorCode: "kuaishou_cloud_source_disabled" } - ); + throw createHttpError(`cloudtentacles 账号 ${sourceKey} 已停用`, { + statusCode: 409, + errorCode: 'kuaishou_cloud_source_disabled', + }) } if (!session) { - throw createHttpError( - `cloudtentacles 账号 ${sourceKey} 没有可用 token`, - { statusCode: 409, errorCode: "kuaishou_cloud_missing_cloud_token" } - ); + throw createHttpError(`cloudtentacles 账号 ${sourceKey} 没有可用 token`, { + statusCode: 409, + errorCode: 'kuaishou_cloud_missing_cloud_token', + }) } - const token = String(session.token || "").trim(); + const token = String(session.token || '').trim() if (!token) { - throw createHttpError( - `cloudtentacles 账号 ${sourceKey} 没有可用 token`, - { statusCode: 409, errorCode: "kuaishou_cloud_missing_cloud_token" } - ); + throw createHttpError(`cloudtentacles 账号 ${sourceKey} 没有可用 token`, { + statusCode: 409, + errorCode: 'kuaishou_cloud_missing_cloud_token', + }) } return { - baseUrl: - String(session.baseUrl || source.baseUrl || "").trim() || - "https://123.207.217.176", + baseUrl: String(session.baseUrl || source.baseUrl || '').trim() || 'https://123.207.217.176', token, - deviceId: normalizeCloudtentaclesDeviceId( - session.deviceId || source.deviceId - ), - deviceType: normalizeCloudtentaclesDeviceType( - session.deviceType ?? source.deviceType - ), + deviceId: normalizeCloudtentaclesDeviceId(session.deviceId || source.deviceId), + deviceType: normalizeCloudtentaclesDeviceType(session.deviceType ?? source.deviceType), // 透传给 http-client / 错误包装,便于定位跨账号问题 sourceKey, resolvedSourceKey: sourceKey, accountLabel: String(source.label || sourceKey).trim() || sourceKey, - }; + } } diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/confirm-role.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/confirm-role.ts index de9677ab..2d703912 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/confirm-role.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/confirm-role.ts @@ -47,7 +47,8 @@ export async function confirmKuaishouCloudTaskRole( } const expectedUid = assertClaimExpectedUidReady(task) - const mockMode = isKuaishouCloudMockTask(task) || isKuaishouCloudMockContext(parseTaskContext(task)) + const mockMode = + isKuaishouCloudMockTask(task) || isKuaishouCloudMockContext(parseTaskContext(task)) const refreshed = mockMode ? { task } @@ -58,9 +59,7 @@ export async function confirmKuaishouCloudTaskRole( forceProbe: options.forceProbe !== false, }) - const flow = normalizeKuaishouCloudFlow( - parseTaskContext(refreshed.task).kuaishouCloudFulfillment, - ) + const flow = normalizeKuaishouCloudFlow(parseTaskContext(refreshed.task).kuaishouCloudFulfillment) if (!flow.binding.vnPhone || !flow.binding.roleName || !flow.binding.roleId) { throw createHttpError('角色信息还未刷新到系统,请完成绑定后稍等片刻再试', { diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/dispatch-fulfillment.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/dispatch-fulfillment.ts index 5c07a580..b3783a8f 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/dispatch-fulfillment.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/dispatch-fulfillment.ts @@ -68,8 +68,7 @@ export async function dispatchKuaishouCloudFulfillmentTask( const industryVoucherCode = String( voucherContext.voucherCode || voucherContext.eticketId || '', ).trim() - const hasIndustryVoucherForDispatch = - Boolean(industryVoucherCode) || isIndustryEVoucherTask(task) + const hasIndustryVoucherForDispatch = Boolean(industryVoucherCode) || isIndustryEVoucherTask(task) const resolvedTicketCode = persistedTicketCode || industryVoucherCode if (!resolvedTicketCode && !hasIndustryVoucherForDispatch) { @@ -190,9 +189,7 @@ export async function dispatchKuaishouCloudFulfillmentTask( purchaseTriggered: stockResult.purchaseTriggered, assetBefore: stockResult.assetBefore, assetAfter: stockResult.assetAfter, - purchaseAt: stockResult.purchaseTriggered - ? now - : syncedFlow.purchase.purchaseAt, + purchaseAt: stockResult.purchaseTriggered ? now : syncedFlow.purchase.purchaseAt, items: stockResult.items, }, }, @@ -255,9 +252,7 @@ export async function dispatchKuaishouCloudFulfillmentTask( return { task: updatedTask, - flow: normalizeKuaishouCloudFlow( - parseTaskContext(updatedTask).kuaishouCloudFulfillment, - ), + flow: normalizeKuaishouCloudFlow(parseTaskContext(updatedTask).kuaishouCloudFulfillment), } } diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/dispatch-role-sync.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/dispatch-role-sync.ts index 9fc188d3..7cd4f59b 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/dispatch-role-sync.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/dispatch-role-sync.ts @@ -1,75 +1,65 @@ -import { createTaskEvent } from "../../../repositories/task-event-repo.js"; -import { updateTask } from "../../../repositories/task-repo.js"; -import type { TaskRow } from "../../../types/repository/rows.js"; -import { createHttpError } from "../../../utils/http.js"; -import { getCloudtentaclesBindInfo } from "../../platforms/cloudtentacles/virtual-number-service.js"; +import { createTaskEvent } from '../../../repositories/task-event-repo.js' +import { updateTask } from '../../../repositories/task-repo.js' +import type { TaskRow } from '../../../types/repository/rows.js' +import { createHttpError } from '../../../utils/http.js' +import { getCloudtentaclesBindInfo } from '../../platforms/cloudtentacles/virtual-number-service.js' import { assertLewanAutoFulfillmentUidReady, isClaimUidMatched, normalizeClaimUid, -} from "../../claim/claim-identity.js"; -import { asJsonObject } from "../../../types/json.js"; +} from '../../claim/claim-identity.js' +import { asJsonObject } from '../../../types/json.js' import { normalizeKuaishouCloudFlow, normalizeKuaishouCloudRoleInfo, type JsonObject, -} from "./domain.js"; +} from './domain.js' export async function syncKuaishouCloudRoleInfoBeforeDispatch( task: TaskRow, - input: JsonObject = {} + input: JsonObject = {}, ) { - const now = String(input.now || "").trim() || new Date().toISOString(); - const source = - String(input.source || "system_before_dispatch").trim() || - "system_before_dispatch"; + const now = String(input.now || '').trim() || new Date().toISOString() + const source = String(input.source || 'system_before_dispatch').trim() || 'system_before_dispatch' const errorCodePrefix = - String(input.errorCodePrefix || "kuaishou_cloud").trim() || - "kuaishou_cloud"; - const cloudContext = - asJsonObject(input.cloudContext); - const taskContext = - asJsonObject(input.taskContext); - const flow = normalizeKuaishouCloudFlow( - input.flow || taskContext.kuaishouCloudFulfillment - ); + String(input.errorCodePrefix || 'kuaishou_cloud').trim() || 'kuaishou_cloud' + const cloudContext = asJsonObject(input.cloudContext) + const taskContext = asJsonObject(input.taskContext) + const flow = normalizeKuaishouCloudFlow(input.flow || taskContext.kuaishouCloudFulfillment) // 新策略:lewan 自动发货必须有 expectedUid,旧单无 UID 不可静默回落 const expectedUid = assertLewanAutoFulfillmentUidReady(task, { allowMockSkip: true, errorCodePrefix, - }); + }) if (!flow.binding.vnId || !flow.binding.vnKey) { - throw createHttpError("当前任务缺少可同步角色的虚拟号信息,暂时不能发货", { + throw createHttpError('当前任务缺少可同步角色的虚拟号信息,暂时不能发货', { statusCode: 409, errorCode: `${errorCodePrefix}_dispatch_missing_bind_info_context`, - }); + }) } const bindInfoResult = await getCloudtentaclesBindInfo({ ...cloudContext, key: flow.binding.vnKey, id: flow.binding.vnId, - }); - const roleInfo = normalizeKuaishouCloudRoleInfo(bindInfoResult.bindInfo); + }) + const roleInfo = normalizeKuaishouCloudRoleInfo(bindInfoResult.bindInfo) if (!roleInfo.name || !roleInfo.rid) { - throw createHttpError( - "cloudtentacles 还没有同步到客户绑定角色,请稍后刷新角色信息后再发货", - { - statusCode: 409, - errorCode: `${errorCodePrefix}_dispatch_role_not_ready`, - } - ); + throw createHttpError('cloudtentacles 还没有同步到客户绑定角色,请稍后刷新角色信息后再发货', { + statusCode: 409, + errorCode: `${errorCodePrefix}_dispatch_role_not_ready`, + }) } - const liveBoundUid = normalizeClaimUid(roleInfo.rid); + const liveBoundUid = normalizeClaimUid(roleInfo.rid) if (expectedUid && !isClaimUidMatched(expectedUid, liveBoundUid)) { await createTaskEvent( task.id, - "kuaishou_cloud_dispatch_uid_mismatch", + 'kuaishou_cloud_dispatch_uid_mismatch', { source, vnId: flow.binding.vnId, @@ -78,16 +68,16 @@ export async function syncKuaishouCloudRoleInfoBeforeDispatch( cloudtentaclesRoleId: roleInfo.rid, actor: input.actor || null, }, - now - ); + now, + ) throw createHttpError( `cloudtentacles 当前绑定角色 ID(${roleInfo.rid})与用户填写 UID(${expectedUid})不一致,不能发货`, { statusCode: 409, errorCode: `${errorCodePrefix}_dispatch_uid_mismatch`, - } - ); + }, + ) } const nextFlow = normalizeKuaishouCloudFlow({ @@ -99,36 +89,36 @@ export async function syncKuaishouCloudRoleInfoBeforeDispatch( }, role: { ...flow.role, - status: "ready", + status: 'ready', name: roleInfo.name, rid: roleInfo.rid, refreshedAt: now, - errorMessage: "", + errorMessage: '', rawInfo: roleInfo.rawInfo, }, - }); + }) const nextContext = { ...taskContext, kuaishouCloudFulfillment: nextFlow, - }; + } const updatedTask = await updateTask(task.id, { role_id: roleInfo.rid, role_name: roleInfo.name, context_json: JSON.stringify(nextContext), updated_at: now, - }); + }) if (!updatedTask) { - throw createHttpError("发货前角色信息同步失败", { + throw createHttpError('发货前角色信息同步失败', { statusCode: 500, errorCode: `${errorCodePrefix}_dispatch_role_update_failed`, - }); + }) } await createTaskEvent( task.id, - "kuaishou_cloud_role_info_synced_before_dispatch", + 'kuaishou_cloud_role_info_synced_before_dispatch', { source, roleName: roleInfo.name, @@ -136,15 +126,13 @@ export async function syncKuaishouCloudRoleInfoBeforeDispatch( vnId: flow.binding.vnId, actor: input.actor || null, }, - now - ); + now, + ) return { task: updatedTask, taskContext: nextContext, flow: nextFlow, roleInfo, - }; + } } - - diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/dispatch-types.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/dispatch-types.ts index 0e7efc7a..1fba208c 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/dispatch-types.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/dispatch-types.ts @@ -51,9 +51,8 @@ export function resolveDispatchDeliveryItems(flow: JsonObject): DispatchDelivery return items } - const binding = flow.binding && typeof flow.binding === 'object' - ? (flow.binding as JsonObject) - : {} + const binding = + flow.binding && typeof flow.binding === 'object' ? (flow.binding as JsonObject) : {} const fallbackSkuId = Number(binding.skuId || 0) || 0 if (!fallbackSkuId) { return [] diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/domain.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/domain.ts index 5a8bb100..14ddf2c1 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/domain.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/domain.ts @@ -1,10 +1,6 @@ import { resolveCloudtentaclesConfig } from '../../platforms/cloudtentacles/helpers.js' import { maskCode as maskCodeValue, maskPhone as maskPhoneValue } from '../../../utils/masking.js' -import { - isTaskFinalStatus, - normalizeTaskStatus, - TASK_STATUS, -} from '../../../domain/task-status.js' +import { isTaskFinalStatus, normalizeTaskStatus, TASK_STATUS } from '../../../domain/task-status.js' import { asJsonObject, type JsonObject } from '../../../types/json.js' export type { JsonObject } @@ -138,8 +134,7 @@ export function normalizeKuaishouCloudFlow(value: unknown): KuaishouCloudFlow { const role = asJsonObject(source.role) const purchase = asJsonObject(source.purchase) const dispatch = asJsonObject(source.dispatch) - const returnNumber = - asJsonObject(source.returnNumber) + const returnNumber = asJsonObject(source.returnNumber) const consume = asJsonObject(source.consume) const ticket = asJsonObject(source.ticket) const rebind = asJsonObject(source.rebind) @@ -280,7 +275,8 @@ export function normalizeKuaishouCloudShippedSnapshot( roleName: String(source.roleName || source.name || '').trim(), vnId, vnPhone: String(source.vnPhone || '').trim(), - vnKey: String(source.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() || KUAISHOU_CLOUD_FIXED_VN_KEY, + vnKey: + String(source.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() || KUAISHOU_CLOUD_FIXED_VN_KEY, dispatchedAt, } } @@ -331,8 +327,7 @@ export function buildKuaishouCloudShippedSnapshot(input: { roleName: String(input.roleName || '').trim(), vnId: Number(input.vnId || 0) || 0, vnPhone: String(input.vnPhone || '').trim(), - vnKey: - String(input.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() || KUAISHOU_CLOUD_FIXED_VN_KEY, + vnKey: String(input.vnKey || KUAISHOU_CLOUD_FIXED_VN_KEY).trim() || KUAISHOU_CLOUD_FIXED_VN_KEY, dispatchedAt: input.dispatchedAt ? String(input.dispatchedAt) : null, } } diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/prepare-fulfillment.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/prepare-fulfillment.ts index 66dfb016..7a0b944e 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/prepare-fulfillment.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/prepare-fulfillment.ts @@ -33,11 +33,7 @@ import { } from './account-selector.js' import { resolvePersistedCloudtentaclesContextBySourceKeys } from './cloudtentacles-context.js' import { wrapCloudtentaclesOperationError } from './cloudtentacles-errors.js' -import { - getTaskClaimExpiresAt, - normalizeActor, - parseTaskContext, -} from './task-context.js' +import { getTaskClaimExpiresAt, normalizeActor, parseTaskContext } from './task-context.js' import { resolveKuaishouCloudDeliveryPlan } from './delivery-plan.js' import { ensureTaskClaimLink } from './ensure-claim-link.js' import { @@ -332,11 +328,14 @@ export async function prepareKuaishouCloudFulfillmentTask(task: TaskRow, options } function isRetryableCloudtentaclesSourceError(error: unknown) { - const current = error && typeof error === 'object' ? error as JsonObject : {} + const current = error && typeof error === 'object' ? (error as JsonObject) : {} const code = String(current.errorCode || current.code || '').trim() return ( code === 'cloudtentacles_vn_quota_cooldown' || - (code === 'cloudtentacles_vn_appoint_failed' && String(current.message || '').trim().includes('最多同时占用')) || + (code === 'cloudtentacles_vn_appoint_failed' && + String(current.message || '') + .trim() + .includes('最多同时占用')) || code === 'cloudtentacles_vn_list_failed' || code === 'cloudtentacles_sku_list_failed' || code === 'cloudtentacles_knapsack_failed' || @@ -484,8 +483,9 @@ export async function refreshKuaishouCloudTaskBindUrl(task: TaskRow, options: Js binding: { ...flow.binding, prepareStatus: 'ready', - resolvedSourceKey: - String(cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '').trim(), + resolvedSourceKey: String( + cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '', + ).trim(), bindUrl, bindPreparedAt: now, bindExpiresAt: resolveKuaishouCloudBindUrlExpiresAt(now), @@ -667,8 +667,9 @@ export async function refreshKuaishouCloudTaskBindUrl(task: TaskRow, options: Js binding: { ...flow.binding, prepareStatus: 'ready', - resolvedSourceKey: - String(cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '').trim(), + resolvedSourceKey: String( + cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '', + ).trim(), vnKey: preparedBinding.vnKey, vnId: preparedBinding.vnId, vnPhone: preparedBinding.vnPhone, @@ -739,7 +740,6 @@ export async function refreshKuaishouCloudTaskBindUrl(task: TaskRow, options: Js } } - async function markKuaishouCloudBindUrlRefreshFailed( task: TaskRow, { @@ -855,7 +855,7 @@ async function clearKuaishouCloudStaleBinding( error: unknown claimUrl: string token: string - } + }, ) { const errorMessage = error instanceof Error ? error.message : String(error || '绑定已失效') const oldVnId = flow.binding.vnId @@ -912,7 +912,7 @@ async function clearKuaishouCloudStaleBinding( errorMessage, actor, }, - now + now, ) return { diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/probe-bind-url.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/probe-bind-url.ts index c72b1a80..5e2115ab 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/probe-bind-url.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/probe-bind-url.ts @@ -51,9 +51,7 @@ export async function probeKuaishouCloudTaskBindUrl(task: TaskRow, options: Json const probeIntervalMs = Number(resolveCloudtentaclesConfig().bindUrlProbeIntervalSeconds || 30) * 1000 - const lastProbeAt = flow.binding.bindProbeAt - ? Date.parse(String(flow.binding.bindProbeAt)) - : NaN + const lastProbeAt = flow.binding.bindProbeAt ? Date.parse(String(flow.binding.bindProbeAt)) : NaN if ( !options.force && Number.isFinite(lastProbeAt) && @@ -187,4 +185,3 @@ async function markKuaishouCloudBindUrlRefreshFailed( return updatedTask } - diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/rebind-role.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/rebind-role.ts index 6067ab8f..129215d6 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/rebind-role.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/rebind-role.ts @@ -24,11 +24,7 @@ import { isCloudtentaclesPermissionLikeError, wrapCloudtentaclesOperationError, } from './cloudtentacles-errors.js' -import { - getTaskClaimExpiresAt, - normalizeActor, - parseTaskContext, -} from './task-context.js' +import { getTaskClaimExpiresAt, normalizeActor, parseTaskContext } from './task-context.js' import { ensureTaskClaimLink } from './ensure-claim-link.js' import { buildPendingRoleWithDefaultSnapshot, @@ -115,7 +111,8 @@ export async function rebindKuaishouCloudTaskRole(task: TaskRow, options: JsonOb bindExpiresAt: flow.binding.bindExpiresAt, roleName: flow.binding.roleName || flow.role.name || task.role_name || '', roleId: flow.binding.roleId || flow.role.rid || task.role_id || '', - resolvedSourceKey: flow.binding.resolvedSourceKey || String(cloudContext.resolvedSourceKey || ''), + resolvedSourceKey: + flow.binding.resolvedSourceKey || String(cloudContext.resolvedSourceKey || ''), } const previousRebind: JsonObject = flow.rebind && typeof flow.rebind === 'object' ? (flow.rebind as JsonObject) : {} @@ -339,8 +336,9 @@ export async function rebindKuaishouCloudTaskRole(task: TaskRow, options: JsonOb binding: { ...flow.binding, prepareStatus: 'ready', - resolvedSourceKey: - String(cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '').trim(), + resolvedSourceKey: String( + cloudContext.resolvedSourceKey || flow.binding.resolvedSourceKey || '', + ).trim(), vnKey: preparedBinding.vnKey, vnId: preparedBinding.vnId, vnPhone: preparedBinding.vnPhone, diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/redeem-fulfillment.test.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/redeem-fulfillment.test.ts index 0297df59..d434fdd7 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/redeem-fulfillment.test.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/redeem-fulfillment.test.ts @@ -6,7 +6,10 @@ import { canRedeemKuaishouCloudClaimStatus, isKuaishouCloudRedeemSettledStatus, } from '../../../domain/task-status.js' -import { assertBoundUidMatchesExpected, assertClaimExpectedUidReady } from '../../claim/claim-identity.js' +import { + assertBoundUidMatchesExpected, + assertClaimExpectedUidReady, +} from '../../claim/claim-identity.js' /** * redeem 用例前置条件单测(不打 DB / CT)。 @@ -73,10 +76,7 @@ test('redeem 前置:uid 不一致拒绝', () => { }) test('redeem 闸门函数与 claim-identity 一致', () => { - assert.throws( - () => assertClaimExpectedUidReady({ context_json: '{}' }), - /填写游戏 UID/, - ) + assert.throws(() => assertClaimExpectedUidReady({ context_json: '{}' }), /填写游戏 UID/) assert.throws( () => assertBoundUidMatchesExpected( diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/redeem-fulfillment.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/redeem-fulfillment.ts index da40a0ca..44a161ad 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/redeem-fulfillment.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/redeem-fulfillment.ts @@ -20,10 +20,7 @@ import { } from '../../claim/claim-identity.js' import { isKuaishouCloudTask, normalizeKuaishouCloudFlow, type JsonObject } from './domain.js' import { dispatchKuaishouCloudFulfillmentTask } from './dispatch-fulfillment.js' -import { - completeMockKuaishouCloudTask, - isKuaishouCloudMockTask, -} from './mock-helpers.js' +import { completeMockKuaishouCloudTask, isKuaishouCloudMockTask } from './mock-helpers.js' import { normalizeActor, parseTaskContext } from './task-context.js' export type RedeemKuaishouCloudTaskResult = { @@ -74,9 +71,7 @@ export async function redeemKuaishouCloudTask( // WAITING_BINDING + UID 匹配:自动升为 ROLE_CONFIRMED,实现一键兑换 if (currentStatus === TASK_STATUS.WAITING_BINDING) { - const flow = normalizeKuaishouCloudFlow( - parseTaskContext(workingTask).kuaishouCloudFulfillment, - ) + const flow = normalizeKuaishouCloudFlow(parseTaskContext(workingTask).kuaishouCloudFulfillment) if (!isKuaishouCloudMockTask(workingTask)) { assertBoundUidMatchesExpected(workingTask, flow, { errorCodePrefix, @@ -122,16 +117,12 @@ export async function redeemKuaishouCloudTask( assertClaimExpectedUidReady(workingTask) } - const lockedTask = await updateTaskStatusIfCurrent( - workingTask.id, - TASK_STATUS.ROLE_CONFIRMED, - { - task_status: TASK_STATUS.REDEEMING, - user_action_status: 'not_required', - last_error: '', - updated_at: now, - }, - ) + const lockedTask = await updateTaskStatusIfCurrent(workingTask.id, TASK_STATUS.ROLE_CONFIRMED, { + task_status: TASK_STATUS.REDEEMING, + user_action_status: 'not_required', + last_error: '', + updated_at: now, + }) if (!lockedTask) { const latest = (await getTaskById(workingTask.id)) || workingTask @@ -174,8 +165,7 @@ export async function redeemKuaishouCloudTask( throw error } - const message = - error instanceof Error ? error.message : '兑换请求提交失败,请联系客服处理' + const message = error instanceof Error ? error.message : '兑换请求提交失败,请联系客服处理' await updateTask(lockedTask.id, { task_status: TASK_STATUS.MANUAL_REVIEW, user_action_status: 'not_required', diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/return-fulfillment.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/return-fulfillment.ts index 8982bb82..39918b48 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/return-fulfillment.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/return-fulfillment.ts @@ -23,10 +23,7 @@ import { normalizeActor, parseTaskContext } from './task-context.js' * - 默认(发货后 autoFinalize):退号 + 尝试行业电子凭证核销收尾 * - `consumeIndustryVoucher: false`(admin 清理):只退虚拟号,避免占号;与核销无关 */ -export async function returnKuaishouCloudFulfillmentTask( - task: TaskRow, - options: JsonObject = {}, -) { +export async function returnKuaishouCloudFulfillmentTask(task: TaskRow, options: JsonObject = {}) { if (!isKuaishouCloudTask(task)) { throw createHttpError('当前任务不是 kuaishou-lewan 履约任务', { statusCode: 409, @@ -79,7 +76,7 @@ export async function returnKuaishouCloudFulfillmentTask( errorMessage: error instanceof Error ? error.message : String(error || ''), actor, }, - now + now, ) } else { throw error @@ -113,8 +110,7 @@ export async function returnKuaishouCloudFulfillmentTask( } else if (shouldConsumeIndustryVoucher) { const industryResult = hasIndustryVoucher ? await consumeKuaishouIndustryVouchersForTask(task, { - source: - String(options.source || 'system_auto_finalize').trim() || 'system_auto_finalize', + source: String(options.source || 'system_auto_finalize').trim() || 'system_auto_finalize', token: String( isPlainObject(taskContext.kuaishouIndustryVoucher) ? taskContext.kuaishouIndustryVoucher.token || '' @@ -122,7 +118,11 @@ export async function returnKuaishouCloudFulfillmentTask( ).trim(), consumeTime: Date.now(), }) - : { ok: true, consumed: [] as Array>, failed: [] as Array<{ errorMessage?: string }> } + : { + ok: true, + consumed: [] as Array>, + failed: [] as Array<{ errorMessage?: string }>, + } if (industryResult.ok) { consumeStatus = 'success' @@ -158,8 +158,7 @@ export async function returnKuaishouCloudFulfillmentTask( if (shouldConsumeIndustryVoucher) { nextTaskStatus = TASK_STATUS.MANUAL_REVIEW nextResultCode = 'kuaishou_cloud_consume_failed' - nextResultMessage = - consumeErrorMessage || '号码已退还,但电子凭证核销未完成,请人工处理' + nextResultMessage = consumeErrorMessage || '号码已退还,但电子凭证核销未完成,请人工处理' } else { nextResultCode = allowConsumeIndustryVoucher ? 'kuaishou_cloud_completed_without_eticket_consume' @@ -256,12 +255,15 @@ export async function returnKuaishouCloudFulfillmentTask( } } -function hasKuaishouIndustryVoucherContext(value: JsonObject): boolean { const voucher = isPlainObject(value.kuaishouIndustryVoucher) - ? value.kuaishouIndustryVoucher - : {} +function hasKuaishouIndustryVoucherContext(value: JsonObject): boolean { + const voucher = isPlainObject(value.kuaishouIndustryVoucher) ? value.kuaishouIndustryVoucher : {} const voucherCode = String(voucher.voucherCode || voucher.eticketId || '').trim() - const status = String(voucher.status || 'UNUSED').trim().toUpperCase() - const sendCallbackStatus = String(voucher.sendCallbackStatus || 'success').trim().toLowerCase() + const status = String(voucher.status || 'UNUSED') + .trim() + .toUpperCase() + const sendCallbackStatus = String(voucher.sendCallbackStatus || 'success') + .trim() + .toLowerCase() return Boolean(voucherCode && status !== 'DESTROYED' && sendCallbackStatus === 'success') } diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/task-context.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/task-context.ts index 472a2ea5..b6da5ac6 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/task-context.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/task-context.ts @@ -1,20 +1,20 @@ -import { parseTaskContext as parseTaskContextValue } from "../../../utils/task-json.js"; -import type { TaskRow } from "../../../types/repository/rows.js"; +import { parseTaskContext as parseTaskContextValue } from '../../../utils/task-json.js' +import type { TaskRow } from '../../../types/repository/rows.js' import type { JsonObject } from '../../../types/json.js' export function normalizeActor(actor: unknown) { - if (!actor || typeof actor !== "object") { - return null; + if (!actor || typeof actor !== 'object') { + return null } - const current = actor as JsonObject; - const source = String(current.source || "").trim(); - const userId = Number(current.userId || 0) || 0; - const username = String(current.username || "").trim(); - const role = String(current.role || "").trim(); + const current = actor as JsonObject + const source = String(current.source || '').trim() + const userId = Number(current.userId || 0) || 0 + const username = String(current.username || '').trim() + const role = String(current.role || '').trim() if (!source && !userId && !username && !role) { - return null; + return null } return { @@ -22,22 +22,24 @@ export function normalizeActor(actor: unknown) { userId, username, role, - }; + } } export function parseTaskContext(task: Partial | null | undefined) { - return parseTaskContextValue(task); + return parseTaskContextValue(task) } export function getTaskClaimExpiresAt(task: Partial | null | undefined) { - return task?.claim_expires_at || task?.primary_claim_expires_at || null; + return task?.claim_expires_at || task?.primary_claim_expires_at || null } export function isClaimExpired(expiredAt: unknown) { if (!expiredAt) { - return false; + return false } - const timestamp = new Date(expiredAt instanceof Date ? expiredAt : String(expiredAt || "")).getTime(); - return Number.isFinite(timestamp) && timestamp <= Date.now(); + const timestamp = new Date( + expiredAt instanceof Date ? expiredAt : String(expiredAt || ''), + ).getTime() + return Number.isFinite(timestamp) && timestamp <= Date.now() } diff --git a/apps/backend/src/services/fulfillment/kuaishou-cloud/task-finalization.test.ts b/apps/backend/src/services/fulfillment/kuaishou-cloud/task-finalization.test.ts index 1844b674..0f7bfe83 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-cloud/task-finalization.test.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-cloud/task-finalization.test.ts @@ -1,14 +1,14 @@ -import assert from "node:assert/strict"; -import test from "node:test"; +import assert from 'node:assert/strict' +import test from 'node:test' -import { buildDispatchStockItems } from "./task-finalization.js"; +import { buildDispatchStockItems } from './task-finalization.js' -test("buildDispatchStockItems 只计算兑换阶段需要补买的库存缺口", () => { +test('buildDispatchStockItems 只计算兑换阶段需要补买的库存缺口', () => { const result = buildDispatchStockItems( [ { cloudSkuId: 101, - cloudSkuName: "", + cloudSkuName: '', quantity: 3, }, ], @@ -16,38 +16,38 @@ test("buildDispatchStockItems 只计算兑换阶段需要补买的库存缺口", skuItems: [ { id: 101, - name: "套装-暗影哥特", + name: '套装-暗影哥特', price: 88, }, ], knapsackItems: [ { id: 101, - name: "套装-暗影哥特", + name: '套装-暗影哥特', count: 1, }, ], - } - ); + }, + ) assert.deepEqual(result, [ { cloudSkuId: 101, - cloudSkuName: "套装-暗影哥特", + cloudSkuName: '套装-暗影哥特', quantity: 3, requiredCount: 3, knapsackCount: 1, purchasedCount: 2, }, - ]); -}); + ]) +}) -test("buildDispatchStockItems 背包足够时不需要补买", () => { +test('buildDispatchStockItems 背包足够时不需要补买', () => { const result = buildDispatchStockItems( [ { cloudSkuId: 202, - cloudSkuName: "套装-浪漫天命", + cloudSkuName: '套装-浪漫天命', quantity: 1, }, ], @@ -55,13 +55,13 @@ test("buildDispatchStockItems 背包足够时不需要补买", () => { knapsackItems: [ { id: 202, - name: "套装-浪漫天命", + name: '套装-浪漫天命', count: 5, }, ], - } - ); + }, + ) - assert.equal(result[0]?.knapsackCount, 5); - assert.equal(result[0]?.purchasedCount, 0); -}); + assert.equal(result[0]?.knapsackCount, 5) + assert.equal(result[0]?.purchasedCount, 0) +}) diff --git a/apps/backend/src/services/fulfillment/kuaishou-feifei/index.test.ts b/apps/backend/src/services/fulfillment/kuaishou-feifei/index.test.ts index 81d7d7eb..43cb5c1c 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-feifei/index.test.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-feifei/index.test.ts @@ -64,43 +64,56 @@ test('resolveKuaishouFeifeiH5UrlWithUid appends uid query', () => { test('buildFeifeiPlatformOrderNo prefers source order number', () => { assert.equal( - buildFeifeiPlatformOrderNo(createTask({ - platform_order_id: 'KS202607080001', - task_no: 'DT600a8bd23b8', - })), + buildFeifeiPlatformOrderNo( + createTask({ + platform_order_id: 'KS202607080001', + task_no: 'DT600a8bd23b8', + }), + ), 'KS202607080001', ) }) test('buildFeifeiPlatformOrderNo falls back to task number', () => { assert.equal( - buildFeifeiPlatformOrderNo(createTask({ - platform_order_id: '', - task_no: 'DT600a8bd23b8', - })), + buildFeifeiPlatformOrderNo( + createTask({ + platform_order_id: '', + task_no: 'DT600a8bd23b8', + }), + ), 'DT600a8bd23b8', ) }) test('resolveKuaishouFeifeiPlatformAmount converts order fen amount to yuan', () => { - assert.equal(resolveKuaishouFeifeiPlatformAmount({ - totalAmountFen: 3800, - quantity: 1, - }), 38) + assert.equal( + resolveKuaishouFeifeiPlatformAmount({ + totalAmountFen: 3800, + quantity: 1, + }), + 38, + ) }) test('resolveKuaishouFeifeiPlatformAmount splits amount by item quantity', () => { - assert.equal(resolveKuaishouFeifeiPlatformAmount({ - totalAmountFen: 7600, - quantity: 2, - }), 38) + assert.equal( + resolveKuaishouFeifeiPlatformAmount({ + totalAmountFen: 7600, + quantity: 2, + }), + 38, + ) }) test('resolveKuaishouFeifeiPlatformAmount skips empty amount', () => { - assert.equal(resolveKuaishouFeifeiPlatformAmount({ - totalAmountFen: 0, - quantity: 1, - }), 0) + assert.equal( + resolveKuaishouFeifeiPlatformAmount({ + totalAmountFen: 0, + quantity: 1, + }), + 0, + ) }) function createTask(patch: Partial = {}): TaskRow { diff --git a/apps/backend/src/services/fulfillment/kuaishou-feifei/index.ts b/apps/backend/src/services/fulfillment/kuaishou-feifei/index.ts index 75c0fd04..c44dfe25 100644 --- a/apps/backend/src/services/fulfillment/kuaishou-feifei/index.ts +++ b/apps/backend/src/services/fulfillment/kuaishou-feifei/index.ts @@ -173,19 +173,25 @@ export async function syncKuaishouFeifeiTaskStatus(task: TaskRow) { resultMessage = consumeResult.failed[0]?.errorMessage || '电子凭证核销失败,请人工处理' lastError = resultMessage nextFlow.consumeStatus = 'failed' - logIntegration('[kuaishou-feifei]', '飞飞履约完成,但行业电子凭证核销失败', { - taskId: task.id, - platformOrderNo: nextFlow.platformOrderNo, - orderNo: nextFlow.orderNo, - voucherCount: consumeResult.vouchers.length, - failedCount: consumeResult.failed.length, - errorMessage: resultMessage, - }, { level: 'warn' }) + logIntegration( + '[kuaishou-feifei]', + '飞飞履约完成,但行业电子凭证核销失败', + { + taskId: task.id, + platformOrderNo: nextFlow.platformOrderNo, + orderNo: nextFlow.orderNo, + voucherCount: consumeResult.vouchers.length, + failedCount: consumeResult.failed.length, + errorMessage: resultMessage, + }, + { level: 'warn' }, + ) } } else if ([40, 50, 60].includes(order.rechargeStatus)) { nextTaskStatus = order.rechargeStatus === 60 ? TASK_STATUS.CLOSED : TASK_STATUS.MANUAL_REVIEW resultCode = `kuaishou_feifei_status_${order.rechargeStatus}` - resultMessage = order.rechargeResultMessage || order.rechargeStatusLabel || 'kuaishou-feifei 履约异常' + resultMessage = + order.rechargeResultMessage || order.rechargeStatusLabel || 'kuaishou-feifei 履约异常' lastError = resultMessage } @@ -229,10 +235,9 @@ export type KuaishouFeifeiFlow = { } export function normalizeKuaishouFeifeiFlow(value: unknown): KuaishouFeifeiFlow { - const source = value && typeof value === 'object' && !Array.isArray(value) - ? value as JsonObject - : {} - const h5 = source.h5 && typeof source.h5 === 'object' ? source.h5 as JsonObject : {} + const source = + value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {} + const h5 = source.h5 && typeof source.h5 === 'object' ? (source.h5 as JsonObject) : {} return { flowType: 'kuaishou_feifei', @@ -263,10 +268,7 @@ export function resolveKuaishouFeifeiClaimUrl(value: unknown) { } /** 在已有 expectedUid 时返回拼好 uid 的 H5 链接。 */ -export function resolveKuaishouFeifeiH5UrlWithUid( - value: unknown, - expectedUid?: unknown, -) { +export function resolveKuaishouFeifeiH5UrlWithUid(value: unknown, expectedUid?: unknown) { const directUrl = resolveKuaishouFeifeiClaimUrl(value) if (!directUrl) { return '' @@ -319,9 +321,7 @@ function mergeKuaishouFeifeiOrder( } } -function resolveKuaishouFeifeiDirectClaimUrl( - flow: KuaishouFeifeiFlow, -) { +function resolveKuaishouFeifeiDirectClaimUrl(flow: KuaishouFeifeiFlow) { const h5ClaimUrl = flow.h5.rechargeUrl || flow.h5.entryUrl if (h5ClaimUrl) { return h5ClaimUrl @@ -335,7 +335,9 @@ function resolveKuaishouFeifeiDirectClaimUrl( return isLocalClaimUrl(flow.claimUrl) ? '' : flow.claimUrl } -function resolveKuaishouFeifeiOrderClaimUrl(order: Awaited>) { +function resolveKuaishouFeifeiOrderClaimUrl( + order: Awaited>, +) { return String(order.h5.rechargeUrl || order.h5.entryUrl || '').trim() } diff --git a/apps/backend/src/services/fulfillment/order-fulfillment-readiness-service.test.ts b/apps/backend/src/services/fulfillment/order-fulfillment-readiness-service.test.ts index f04d164b..5f6a4e7c 100644 --- a/apps/backend/src/services/fulfillment/order-fulfillment-readiness-service.test.ts +++ b/apps/backend/src/services/fulfillment/order-fulfillment-readiness-service.test.ts @@ -62,7 +62,9 @@ test('resolveOrderFulfillmentReadiness 电子凭证发码未确认时阻止履 }) }) -function createVoucher(patch: Partial = {}): KuaishouIndustryVoucherRow { +function createVoucher( + patch: Partial = {}, +): KuaishouIndustryVoucherRow { return { id: 1, voucher_code: 'ETICKET-1', diff --git a/apps/backend/src/services/fulfillment/order-fulfillment-readiness-service.ts b/apps/backend/src/services/fulfillment/order-fulfillment-readiness-service.ts index c86026af..e7b07593 100644 --- a/apps/backend/src/services/fulfillment/order-fulfillment-readiness-service.ts +++ b/apps/backend/src/services/fulfillment/order-fulfillment-readiness-service.ts @@ -39,7 +39,9 @@ export async function resolveOrderFulfillmentReadiness( } } - const blockedVoucher = vouchers.find((voucher) => !isKuaishouIndustryVoucherSendCallbackSuccess(voucher)) + const blockedVoucher = vouchers.find( + (voucher) => !isKuaishouIndustryVoucherSendCallbackSuccess(voucher), + ) return { allowed: false, diff --git a/apps/backend/src/services/fulfillment/planner.ts b/apps/backend/src/services/fulfillment/planner.ts index 38e70ead..2533b30b 100644 --- a/apps/backend/src/services/fulfillment/planner.ts +++ b/apps/backend/src/services/fulfillment/planner.ts @@ -54,9 +54,9 @@ export async function planFulfillmentTaskForOrderItem({ const profileId = Number(profile.profile_id || profile.id || 0) const profileKey = String(profile.profile_key || '').trim() const profileName = String(profile.profile_name || profile.name || '').trim() - const executorKey = String(profile.executor_key || FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH) - .trim() - || FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH + const executorKey = + String(profile.executor_key || FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH).trim() || + FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH const fulfillmentConfig = parseJsonObject(profile.config_json) const context = buildFulfillmentTaskContext({ order, @@ -229,9 +229,7 @@ function buildFulfillmentTaskContext({ ? { flowType: 'affiliate_dash', sku: String(affiliateDashConfig.sku || '').trim(), - productName: String( - affiliateDashConfig.productName || item.sku_name || '', - ).trim(), + productName: String(affiliateDashConfig.productName || item.sku_name || '').trim(), orderNo: '', clientOrderNo: '', orderStatus: '', @@ -279,9 +277,9 @@ async function resolveDynamicFulfillmentProfile( } return ( - await resolveDynamicCloudtentaclesProfile(item, getProfileByKey) || - await resolveDynamicKuaishouFeifeiProfile(item, getProfileByKey) || - await resolveDynamicAffiliateDashProfile(item, getProfileByKey) + (await resolveDynamicCloudtentaclesProfile(item, getProfileByKey)) || + (await resolveDynamicKuaishouFeifeiProfile(item, getProfileByKey)) || + (await resolveDynamicAffiliateDashProfile(item, getProfileByKey)) ) } diff --git a/apps/backend/src/services/fulfillment/product-resolution-service.test.ts b/apps/backend/src/services/fulfillment/product-resolution-service.test.ts index 65727905..7fe4ed6b 100644 --- a/apps/backend/src/services/fulfillment/product-resolution-service.test.ts +++ b/apps/backend/src/services/fulfillment/product-resolution-service.test.ts @@ -8,23 +8,23 @@ import { } from './product-resolution-service.js' test('matchAffiliateDashSku 原样精确命中', () => { - const mapping = { '幸运币90个': 'lucky_coin_x90' } + const mapping = { 幸运币90个: 'lucky_coin_x90' } assert.equal(matchAffiliateDashSku(mapping, '幸运币90个'), 'lucky_coin_x90') }) test('matchAffiliateDashSku 全角数字/空格归一命中', () => { - const mapping = { '幸运币90个': 'lucky_coin_x90' } + const mapping = { 幸运币90个: 'lucky_coin_x90' } assert.equal(matchAffiliateDashSku(mapping, '幸运币90个'), 'lucky_coin_x90') assert.equal(matchAffiliateDashSku(mapping, ' 幸运币 90 个 '), 'lucky_coin_x90') }) test('matchAffiliateDashSku 大小写归一命中', () => { - const mapping = { '星际漫游服装礼包': 'pack_star_roam_outfit' } + const mapping = { 星际漫游服装礼包: 'pack_star_roam_outfit' } assert.equal(matchAffiliateDashSku(mapping, '星际漫游服装礼包'), 'pack_star_roam_outfit') }) test('matchAffiliateDashSku 未命中返回空串', () => { - const mapping = { '幸运币90个': 'lucky_coin_x90' } + const mapping = { 幸运币90个: 'lucky_coin_x90' } assert.equal(matchAffiliateDashSku(mapping, '神秘新商品'), '') assert.equal(matchAffiliateDashSku(mapping, ''), '') }) diff --git a/apps/backend/src/services/fulfillment/product-resolution-service.ts b/apps/backend/src/services/fulfillment/product-resolution-service.ts index da7e59ea..3060d954 100644 --- a/apps/backend/src/services/fulfillment/product-resolution-service.ts +++ b/apps/backend/src/services/fulfillment/product-resolution-service.ts @@ -7,10 +7,7 @@ import { resolveKuaishouFeifeiProductByName, type KuaishouFeifeiProductMatch, } from '../platforms/kuaishou-feifei/product-rule-service.js' -import { - resolveFulfillmentRoute, - type FulfillmentRouteResult, -} from './routing-config-service.js' +import { resolveFulfillmentRoute, type FulfillmentRouteResult } from './routing-config-service.js' import { getAffiliateDashConfig } from '../platforms/affiliate-dash/config.js' import { listAllAffiliateDashProducts } from '../platforms/affiliate-dash/product-service.js' import type { AffiliateDashSkuMapping } from '../../types/runtime-config.js' @@ -97,9 +94,7 @@ export async function resolveOrderItemForFulfillment({ selectedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI ? kuaishouFeifeiMatch?.productCode : '', - selectedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.AFFILIATE_DASH - ? affiliateDashMatch?.sku - : '', + selectedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.AFFILIATE_DASH ? affiliateDashMatch?.sku : '', externalSkuCode, externalItemId, ]) @@ -110,9 +105,7 @@ export async function resolveOrderItemForFulfillment({ selectedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_FEIFEI ? kuaishouFeifeiMatch?.skuName : '', - selectedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.AFFILIATE_DASH - ? affiliateDashMatch?.sku - : '', + selectedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.AFFILIATE_DASH ? affiliateDashMatch?.sku : '', item.skuName, externalSkuName, resolvedSkuCode, @@ -187,11 +180,13 @@ export async function hasConfiguredOrderItems({ items = [], }: HasConfiguredOrderItemsInput): Promise { const candidates = await Promise.all( - (Array.isArray(items) ? items : []).map((item) => resolveConfiguredItemCandidate({ - provider, - platform, - item, - })), + (Array.isArray(items) ? items : []).map((item) => + resolveConfiguredItemCandidate({ + provider, + platform, + item, + }), + ), ) return candidates.some((item) => item.isConfigured) @@ -217,20 +212,9 @@ async function resolveConfiguredItemCandidate({ platform = '', item = {}, }: ResolveOrderItemForFulfillmentInput): Promise { - const externalItemId = pickFirstNonEmpty([ - item.externalItemId, - item.itemId, - ]) - const externalSkuCode = pickFirstNonEmpty([ - item.externalSkuCode, - item.skuCode, - externalItemId, - ]) - const externalSkuName = pickFirstNonEmpty([ - item.externalSkuName, - item.skuName, - externalSkuCode, - ]) + const externalItemId = pickFirstNonEmpty([item.externalItemId, item.itemId]) + const externalSkuCode = pickFirstNonEmpty([item.externalSkuCode, item.skuCode, externalItemId]) + const externalSkuName = pickFirstNonEmpty([item.externalSkuName, item.skuName, externalSkuCode]) const externalSkuNameNormalized = normalizeProductName(externalSkuName) if (isOpen91KuaishouOrder(provider, platform)) { @@ -293,10 +277,7 @@ async function resolveConfiguredItemCandidate({ } } - const resolvedSkuCode = pickFirstNonEmpty([ - externalSkuCode, - externalItemId, - ]) + const resolvedSkuCode = pickFirstNonEmpty([externalSkuCode, externalItemId]) return { externalItemId, @@ -370,22 +351,32 @@ let affiliateDashSkuCache: { skus: Set; fetchedAt: number } | null = nul async function getAffiliateDashSkuSet(): Promise> { const now = Date.now() - if (affiliateDashSkuCache && now - affiliateDashSkuCache.fetchedAt < AFFILIATE_DASH_SKU_CACHE_TTL_MS) { + if ( + affiliateDashSkuCache && + now - affiliateDashSkuCache.fetchedAt < AFFILIATE_DASH_SKU_CACHE_TTL_MS + ) { return affiliateDashSkuCache.skus } try { const result = await listAllAffiliateDashProducts() - const skus = new Set(result.list.map((product) => String(product.sku || '').trim()).filter(Boolean)) + const skus = new Set( + result.list.map((product) => String(product.sku || '').trim()).filter(Boolean), + ) affiliateDashSkuCache = { skus, fetchedAt: now } return skus } catch (error) { // 拉取失败:返回上次缓存(即使过期)或空集合,匹配 miss 走其他通道,不阻塞下单; // warn 日志便于线上排查(密钥未配/平台不可用都会导致透传降级为 miss)。 - logIntegration('[affiliate-dash]', 'affiliate-dash 商品列表拉取失败,透传降级为未命中', { - cachedSkuCount: affiliateDashSkuCache?.skus.size || 0, - error: error instanceof Error ? error.message : String(error), - }, { level: 'warn' }) + logIntegration( + '[affiliate-dash]', + 'affiliate-dash 商品列表拉取失败,透传降级为未命中', + { + cachedSkuCount: affiliateDashSkuCache?.skus.size || 0, + error: error instanceof Error ? error.message : String(error), + }, + { level: 'warn' }, + ) return affiliateDashSkuCache?.skus || new Set() } } @@ -394,10 +385,7 @@ async function getAffiliateDashSkuSet(): Promise> { * 91 商品编码为中文名(如「幸运币90个」),映射键查询做归一化: * NFKC(全角→半角)→ 去所有空白 → 小写。先原样精确,再归一化遍历。 */ -export function matchAffiliateDashSku( - mapping: AffiliateDashSkuMapping, - productNo: string, -): string { +export function matchAffiliateDashSku(mapping: AffiliateDashSkuMapping, productNo: string): string { const raw = String(productNo || '').trim() if (!raw) { return '' @@ -425,8 +413,9 @@ export function normalizeAffiliateDashMappingKey(value: unknown): string { } function isOpen91KuaishouOrder(provider: unknown, platform: unknown) { - return String(provider || '').trim() === '91kaquan' && - String(platform || '').trim() === 'kuaishou' + return ( + String(provider || '').trim() === '91kaquan' && String(platform || '').trim() === 'kuaishou' + ) } function isPlainObject(value: unknown): value is Record { diff --git a/apps/backend/src/services/fulfillment/routing-config-service.ts b/apps/backend/src/services/fulfillment/routing-config-service.ts index 9463e9d8..64462e62 100644 --- a/apps/backend/src/services/fulfillment/routing-config-service.ts +++ b/apps/backend/src/services/fulfillment/routing-config-service.ts @@ -2,10 +2,7 @@ import path from 'node:path' import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js' import { PROJECT_ROOT } from '../../config/runtime.js' -import { - readAppConfigEntry, - saveAppConfigEntry, -} from '../config/app-config-store.js' +import { readAppConfigEntry, saveAppConfigEntry } from '../config/app-config-store.js' import { normalizeCloudtentaclesMatchName } from '../order/cloudtentacles-match-utils.js' import type { JsonObject } from '../../types/json.js' import { FULFILLMENT_EXECUTOR_KEYS } from './executors/types.js' @@ -113,11 +110,13 @@ export function normalizeFulfillmentRoutingConfig(rawValue: unknown): Fulfillmen defaultExecutorPriority: defaultExecutorPriority.length > 0 ? defaultExecutorPriority : [...DEFAULT_EXECUTOR_PRIORITY], unmatchedExecutorKey: - unmatchedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH ? unmatchedExecutorKey : '', + unmatchedExecutorKey === FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH + ? unmatchedExecutorKey + : '', executors: ROUTABLE_EXECUTOR_KEYS.reduce>( (result, executorKey) => { const executorConfig = isPlainObject(sourceExecutors[executorKey]) - ? sourceExecutors[executorKey] as JsonObject + ? (sourceExecutors[executorKey] as JsonObject) : {} result[executorKey] = { enabled: executorConfig.enabled !== false, @@ -383,17 +382,19 @@ function findMatchedRoutingRule(productName: unknown, rules: FulfillmentRoutingR return null } - return rules.find((rule) => { - if (rule.enabled === false || !rule.normalizedProductName) { - return false - } + return ( + rules.find((rule) => { + if (rule.enabled === false || !rule.normalizedProductName) { + return false + } - if (rule.matchType === 'contains') { - return normalizedProductName.includes(rule.normalizedProductName) - } + if (rule.matchType === 'contains') { + return normalizedProductName.includes(rule.normalizedProductName) + } - return normalizedProductName === rule.normalizedProductName - }) || null + return normalizedProductName === rule.normalizedProductName + }) || null + ) } function normalizeFulfillmentRoutingRule(rawValue: unknown): FulfillmentRoutingRule | null { @@ -409,7 +410,8 @@ function normalizeFulfillmentRoutingRule(rawValue: unknown): FulfillmentRoutingR } return { - id: String(source.id || `${normalizedProductName}:${executorKey}`).trim() || + id: + String(source.id || `${normalizedProductName}:${executorKey}`).trim() || `${normalizedProductName}:${executorKey}`, enabled: source.enabled !== false, productName, @@ -446,7 +448,11 @@ function normalizeExecutorPriority(value: unknown) { for (const item of rawItems) { const executorKey = normalizeExecutorKey(item) - if (!executorKey || seen.has(executorKey) || executorKey === FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH) { + if ( + !executorKey || + seen.has(executorKey) || + executorKey === FULFILLMENT_EXECUTOR_KEYS.MANUAL_DISPATCH + ) { continue } seen.add(executorKey) diff --git a/apps/backend/src/services/notification/bark-service.ts b/apps/backend/src/services/notification/bark-service.ts index 8c873872..6d98b195 100644 --- a/apps/backend/src/services/notification/bark-service.ts +++ b/apps/backend/src/services/notification/bark-service.ts @@ -17,7 +17,9 @@ type BarkSendInput = { } export async function sendBarkNotification(input: BarkSendInput) { - const serverUrl = String(input.serverUrl || 'https://api.day.app').trim().replace(/\/+$/, '') + const serverUrl = String(input.serverUrl || 'https://api.day.app') + .trim() + .replace(/\/+$/, '') const deviceKey = String(input.recipient?.deviceKey || '').trim() const title = String(input.title || '').trim() const body = String(input.body || '').trim() @@ -88,11 +90,7 @@ export async function sendBarkNotification(input: BarkSendInput) { } function buildBarkEndpoint(serverUrl: string, deviceKey: string, title: string, body: string) { - const segments = [ - serverUrl, - encodeURIComponent(deviceKey), - encodeURIComponent(title), - ] + const segments = [serverUrl, encodeURIComponent(deviceKey), encodeURIComponent(title)] if (body) { segments.push(encodeURIComponent(body)) @@ -124,7 +122,10 @@ function isBarkFailure(parsed: unknown) { function resolveBarkErrorMessage(parsed: unknown, responseText: string, status: number) { if (isPlainObject(parsed)) { - return String(parsed.message || parsed.msg || parsed.error || '').trim() || `Bark 通知发送失败,HTTP ${status}` + return ( + String(parsed.message || parsed.msg || parsed.error || '').trim() || + `Bark 通知发送失败,HTTP ${status}` + ) } return String(responseText || '').trim() || `Bark 通知发送失败,HTTP ${status}` diff --git a/apps/backend/src/services/notification/config-service.test.ts b/apps/backend/src/services/notification/config-service.test.ts index d7b33062..14fa3166 100644 --- a/apps/backend/src/services/notification/config-service.test.ts +++ b/apps/backend/src/services/notification/config-service.test.ts @@ -1,10 +1,7 @@ import test from 'node:test' import assert from 'node:assert/strict' -import { - createDefaultNotificationConfig, - normalizeNotificationConfig, -} from './config-service.js' +import { createDefaultNotificationConfig, normalizeNotificationConfig } from './config-service.js' test('normalizeNotificationConfig normalizes bark and wpush recipients', () => { assert.deepEqual( diff --git a/apps/backend/src/services/notification/config-service.ts b/apps/backend/src/services/notification/config-service.ts index 7a6b9e06..a9868f7e 100644 --- a/apps/backend/src/services/notification/config-service.ts +++ b/apps/backend/src/services/notification/config-service.ts @@ -3,10 +3,7 @@ import type { JsonObject } from '../../types/json.js' import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js' import { PROJECT_ROOT } from '../../config/runtime.js' -import { - readAppConfigEntry, - saveAppConfigEntry, -} from '../config/app-config-store.js' +import { readAppConfigEntry, saveAppConfigEntry } from '../config/app-config-store.js' const NOTIFICATION_CONFIG_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'notification-config.json') const DEFAULT_BARK_SERVER_URL = 'https://api.day.app' @@ -46,8 +43,9 @@ export function listEnabledBarkRecipients(config: JsonObject = getNotificationCo return [] } - return (Array.isArray(config.channels?.bark?.recipients) ? config.channels.bark.recipients : []) - .filter((item: JsonObject) => item.enabled !== false && String(item.deviceKey || '').trim()) + return ( + Array.isArray(config.channels?.bark?.recipients) ? config.channels.bark.recipients : [] + ).filter((item: JsonObject) => item.enabled !== false && String(item.deviceKey || '').trim()) } export function listEnabledWpushRecipients(config: JsonObject = getNotificationConfig()) { @@ -55,8 +53,11 @@ export function listEnabledWpushRecipients(config: JsonObject = getNotificationC return [] } - return (Array.isArray(config.channels?.wpush?.recipients) ? config.channels.wpush.recipients : []) - .filter((item: JsonObject) => item.enabled !== false && String(item.apiKey || item.apikey || '').trim()) + return ( + Array.isArray(config.channels?.wpush?.recipients) ? config.channels.wpush.recipients : [] + ).filter( + (item: JsonObject) => item.enabled !== false && String(item.apiKey || item.apikey || '').trim(), + ) } export function normalizeNotificationConfig(rawValue: unknown) { diff --git a/apps/backend/src/services/notification/domain-notifications.ts b/apps/backend/src/services/notification/domain-notifications.ts index 8a229d6c..5fe6ba82 100644 --- a/apps/backend/src/services/notification/domain-notifications.ts +++ b/apps/backend/src/services/notification/domain-notifications.ts @@ -107,11 +107,9 @@ export function notifyKuaishouCloudAssetNotEnough({ `商品:${String(skuName || binding.skuName || flowRecord.internalSkuName || '').trim() || '-'}`, ].join('\n'), category: 'kuaishou_cloud_asset_not_enough', - cooldownKey: [ - 'kuaishou_cloud_asset_not_enough', - taskRecord.id || '', - binding.skuId || '', - ].join(':'), + cooldownKey: ['kuaishou_cloud_asset_not_enough', taskRecord.id || '', binding.skuId || ''].join( + ':', + ), }) } @@ -135,9 +133,16 @@ export function notifyCloudtentaclesAuthExpired({ `接口:${String(pathname || '').trim() || '-'}`, `错误码:${String(errorCode || '').trim() || '-'}`, `原因:${String(message || '').trim() || '登录态已失效,请到后台重新登录'}`, - ].filter(Boolean).join('\n'), + ] + .filter(Boolean) + .join('\n'), category: 'cloudtentacles_auth_expired', - cooldownKey: ['cloudtentacles_auth_expired', normalizedSourceKey || 'default', pathname, errorCode].join(':'), + cooldownKey: [ + 'cloudtentacles_auth_expired', + normalizedSourceKey || 'default', + pathname, + errorCode, + ].join(':'), cooldownMs: Number(cooldownSeconds || 600) * 1000, }) } @@ -161,9 +166,13 @@ export function notifyCloudtentaclesAssetLow({ `当前余额:${Number(asset || 0)}`, `提醒阈值:${Number(threshold || 0)}`, '请及时补充 cloudtentacles 余额,避免自动履约失败。', - ].filter(Boolean).join('\n'), + ] + .filter(Boolean) + .join('\n'), category: 'cloudtentacles_asset_low', - cooldownKey: ['cloudtentacles_asset_low', normalizedSourceKey || 'default', threshold].join(':'), + cooldownKey: ['cloudtentacles_asset_low', normalizedSourceKey || 'default', threshold].join( + ':', + ), cooldownMs: Number(cooldownSeconds || 1800) * 1000, }) } @@ -186,7 +195,11 @@ export function notifyOpen91PendingConfig({ `原因:${String(reason || '').trim() || '未命中履约配置'}`, ].join('\n'), category: 'open91_pending_config', - cooldownKey: ['open91_pending_config', orderNo || orderRecord.platform_order_id || '', productNo].join(':'), + cooldownKey: [ + 'open91_pending_config', + orderNo || orderRecord.platform_order_id || '', + productNo, + ].join(':'), }) } @@ -258,9 +271,7 @@ function formatTaskLine(task: unknown) { } function toRecord(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) : {} } function isNotificationCoolingDown(cooldownKey: unknown, cooldownMs = DEFAULT_COOLDOWN_MS) { diff --git a/apps/backend/src/services/notification/notification-service.ts b/apps/backend/src/services/notification/notification-service.ts index 32df0d96..a8246c3f 100644 --- a/apps/backend/src/services/notification/notification-service.ts +++ b/apps/backend/src/services/notification/notification-service.ts @@ -37,59 +37,79 @@ export async function sendInternalNotification(input: NotificationInput = {}) { } const results = [ - ...(await Promise.all(barkRecipients.map(async (recipient: JsonObject) => { - try { - const result = await sendBarkNotification({ - recipient, - title, - body, - group: `订单系统/${category}`, - ...(bark.serverUrl !== undefined ? { serverUrl: bark.serverUrl } : {}), - ...(input.url !== undefined ? { url: input.url } : {}), - }) - return mapNotificationResult('bark', recipient, recipient.deviceKey, true, '', result.status, result.response) - } catch (error) { - logWarn('[notification/bark]', 'Bark 内部通知发送失败', { - recipientId: recipient.id, - recipientName: recipient.name, - error, - }) - return mapNotificationResult( - 'bark', - recipient, - recipient.deviceKey, - false, - error instanceof Error ? error.message : 'Bark 内部通知发送失败', - isPlainObject(error) ? Number(error.statusCode || 0) || 0 : 0, - isPlainObject(error) ? error.context || null : null, - ) - } - }))), - ...(await Promise.all(wpushRecipients.map(async (recipient: JsonObject) => { - try { - const result = await sendWpushNotification({ - recipient, - title, - body, - }) - return mapNotificationResult('wpush', recipient, recipient.apiKey, true, '', result.status, result.response) - } catch (error) { - logWarn('[notification/wpush]', 'WPush 内部通知发送失败', { - recipientId: recipient.id, - recipientName: recipient.name, - error, - }) - return mapNotificationResult( - 'wpush', - recipient, - recipient.apiKey, - false, - error instanceof Error ? error.message : 'WPush 内部通知发送失败', - isPlainObject(error) ? Number(error.statusCode || 0) || 0 : 0, - isPlainObject(error) ? error.context || null : null, - ) - } - }))), + ...(await Promise.all( + barkRecipients.map(async (recipient: JsonObject) => { + try { + const result = await sendBarkNotification({ + recipient, + title, + body, + group: `订单系统/${category}`, + ...(bark.serverUrl !== undefined ? { serverUrl: bark.serverUrl } : {}), + ...(input.url !== undefined ? { url: input.url } : {}), + }) + return mapNotificationResult( + 'bark', + recipient, + recipient.deviceKey, + true, + '', + result.status, + result.response, + ) + } catch (error) { + logWarn('[notification/bark]', 'Bark 内部通知发送失败', { + recipientId: recipient.id, + recipientName: recipient.name, + error, + }) + return mapNotificationResult( + 'bark', + recipient, + recipient.deviceKey, + false, + error instanceof Error ? error.message : 'Bark 内部通知发送失败', + isPlainObject(error) ? Number(error.statusCode || 0) || 0 : 0, + isPlainObject(error) ? error.context || null : null, + ) + } + }), + )), + ...(await Promise.all( + wpushRecipients.map(async (recipient: JsonObject) => { + try { + const result = await sendWpushNotification({ + recipient, + title, + body, + }) + return mapNotificationResult( + 'wpush', + recipient, + recipient.apiKey, + true, + '', + result.status, + result.response, + ) + } catch (error) { + logWarn('[notification/wpush]', 'WPush 内部通知发送失败', { + recipientId: recipient.id, + recipientName: recipient.name, + error, + }) + return mapNotificationResult( + 'wpush', + recipient, + recipient.apiKey, + false, + error instanceof Error ? error.message : 'WPush 内部通知发送失败', + isPlainObject(error) ? Number(error.statusCode || 0) || 0 : 0, + isPlainObject(error) ? error.context || null : null, + ) + } + }), + )), ] return { diff --git a/apps/backend/src/services/notification/wpush-service.test.ts b/apps/backend/src/services/notification/wpush-service.test.ts index 63d71717..c3944084 100644 --- a/apps/backend/src/services/notification/wpush-service.test.ts +++ b/apps/backend/src/services/notification/wpush-service.test.ts @@ -66,11 +66,12 @@ test('sendWpushNotification sends POST json payload', async () => { test('sendWpushNotification rejects missing api key', async () => { await assert.rejects( - () => sendWpushNotification({ - recipient: {}, - title: '测试标题', - body: '测试内容', - }), + () => + sendWpushNotification({ + recipient: {}, + title: '测试标题', + body: '测试内容', + }), (error: any) => { assert.equal(error?.errorCode, 'wpush_api_key_required') return true diff --git a/apps/backend/src/services/notification/wpush-service.ts b/apps/backend/src/services/notification/wpush-service.ts index 79c63f43..67dd89c4 100644 --- a/apps/backend/src/services/notification/wpush-service.ts +++ b/apps/backend/src/services/notification/wpush-service.ts @@ -97,7 +97,12 @@ export async function sendWpushNotification(input: WpushSendInput) { async function requestViaNodeHttp( url: URL, - { method, headers, body, signal }: { + { + method, + headers, + body, + signal, + }: { method: string headers: Record body?: string @@ -107,25 +112,29 @@ async function requestViaNodeHttp( const transport = url.protocol === 'https:' ? https : http return new Promise((resolve, reject) => { - const request = transport.request(url, { - method, - headers, - rejectUnauthorized: false, - }, (response) => { - const chunks: Buffer[] = [] + const request = transport.request( + url, + { + method, + headers, + rejectUnauthorized: false, + }, + (response) => { + const chunks: Buffer[] = [] - response.on('data', (chunk) => { - chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) - }) - - response.on('end', () => { - resolve({ - ok: Number(response.statusCode || 0) >= 200 && Number(response.statusCode || 0) < 300, - status: Number(response.statusCode || 0), - bodyText: Buffer.concat(chunks).toString('utf8'), + response.on('data', (chunk) => { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) }) - }) - }) + + response.on('end', () => { + resolve({ + ok: Number(response.statusCode || 0) >= 200 && Number(response.statusCode || 0) < 300, + status: Number(response.statusCode || 0), + bodyText: Buffer.concat(chunks).toString('utf8'), + }) + }) + }, + ) request.on('error', reject) @@ -135,11 +144,15 @@ async function requestViaNodeHttp( error.name = 'AbortError' request.destroy(error) } else { - signal.addEventListener('abort', () => { - const error = new Error('Request aborted') - error.name = 'AbortError' - request.destroy(error) - }, { once: true }) + signal.addEventListener( + 'abort', + () => { + const error = new Error('Request aborted') + error.name = 'AbortError' + request.destroy(error) + }, + { once: true }, + ) } } @@ -179,7 +192,10 @@ function isWpushFailure(parsed: unknown) { function resolveWpushErrorMessage(parsed: unknown, responseText: string, status: number) { if (isPlainObject(parsed)) { - return String(parsed.message || parsed.msg || parsed.error || '').trim() || `WPush 通知发送失败,HTTP ${status}` + return ( + String(parsed.message || parsed.msg || parsed.error || '').trim() || + `WPush 通知发送失败,HTTP ${status}` + ) } return String(responseText || '').trim() || `WPush 通知发送失败,HTTP ${status}` diff --git a/apps/backend/src/services/open-91/config.ts b/apps/backend/src/services/open-91/config.ts index af4a3781..bf8d4a44 100644 --- a/apps/backend/src/services/open-91/config.ts +++ b/apps/backend/src/services/open-91/config.ts @@ -23,7 +23,8 @@ export function getOpen91Config(overrides: Partial = {}) { shopId: String(config.shopId || OPEN_91_PROVIDER).trim() || OPEN_91_PROVIDER, shopName: String(config.shopName || '91卡券').trim() || '91卡券', timestampToleranceSeconds: Number(config.timestampToleranceSeconds || 600) || 600, - cardsEncoding: String(config.cardsEncoding || 'aes-256-ecb-base64').trim() || 'aes-256-ecb-base64', + cardsEncoding: + String(config.cardsEncoding || 'aes-256-ecb-base64').trim() || 'aes-256-ecb-base64', } } diff --git a/apps/backend/src/services/open-91/consume-on-deliver.test.ts b/apps/backend/src/services/open-91/consume-on-deliver.test.ts index d65677b1..323a7084 100644 --- a/apps/backend/src/services/open-91/consume-on-deliver.test.ts +++ b/apps/backend/src/services/open-91/consume-on-deliver.test.ts @@ -4,9 +4,11 @@ import test from 'node:test' import { resolveIndustryVoucherForTask } from './consume-on-deliver.js' import type { KuaishouIndustryVoucherRow } from '../../types/repository/rows.js' -function voucher(partial: Partial & { - voucher_code: string -}): KuaishouIndustryVoucherRow { +function voucher( + partial: Partial & { + voucher_code: string + }, +): KuaishouIndustryVoucherRow { return { id: partial.id || 1, oid: partial.oid || 'OID-1', @@ -43,18 +45,14 @@ test('resolveIndustryVoucherForTask prefers task_id over unit_index', () => { }) test('resolveIndustryVoucherForTask falls back to unit_index when task_id missing', () => { - const vouchers = [ - voucher({ id: 1, voucher_code: 'A', task_id: null, unit_index: 2 }), - ] + const vouchers = [voucher({ id: 1, voucher_code: 'A', task_id: null, unit_index: 2 })] const matched = resolveIndustryVoucherForTask({ id: 99, unit_index: 2 }, vouchers) assert.equal(matched?.voucher_code, 'A') }) test('resolveIndustryVoucherForTask returns null when no match', () => { - const vouchers = [ - voucher({ id: 1, voucher_code: 'A', task_id: 1, unit_index: 1 }), - ] + const vouchers = [voucher({ id: 1, voucher_code: 'A', task_id: 1, unit_index: 1 })] assert.equal(resolveIndustryVoucherForTask({ id: 2, unit_index: 3 }, vouchers), null) }) diff --git a/apps/backend/src/services/open-91/consume-on-deliver.ts b/apps/backend/src/services/open-91/consume-on-deliver.ts index 77d0b009..27fafb91 100644 --- a/apps/backend/src/services/open-91/consume-on-deliver.ts +++ b/apps/backend/src/services/open-91/consume-on-deliver.ts @@ -8,22 +8,14 @@ * 交付后会 best-effort 自动准备 Cloud 绑定资源(取号/绑链),避免 * 「已核销却停在待准备资源」只能靠后台手动点「准备资源」。 */ -import { - attachKuaishouIndustryVoucherToTask, -} from '../platforms/kuaishou-industry/voucher-binding-service.js' -import { - consumeKuaishouIndustryVoucher, -} from '../platforms/kuaishou-industry/voucher-service.js' +import { attachKuaishouIndustryVoucherToTask } from '../platforms/kuaishou-industry/voucher-binding-service.js' +import { consumeKuaishouIndustryVoucher } from '../platforms/kuaishou-industry/voucher-service.js' import { prepareFulfillmentBinding } from '../fulfillment/executors/registry.js' import { normalizeKuaishouCloudFlow } from '../fulfillment/kuaishou-cloud/index.js' import { listKuaishouIndustryVouchersByOid } from '../../repositories/kuaishou-industry-voucher-repo.js' import { logIntegration, logWarn } from '../../utils/logger.js' import { parseTaskContext } from '../../utils/task-json.js' -import type { - KuaishouIndustryVoucherRow, - OrderRow, - TaskRow, -} from '../../types/repository/rows.js' +import type { KuaishouIndustryVoucherRow, OrderRow, TaskRow } from '../../types/repository/rows.js' export const OPEN_91_DELIVER_CONSUME_SOURCE = 'open91_query_deliver' @@ -69,7 +61,11 @@ export function resolveIndustryVoucherForTask( } function isVoucherConsumed(voucher: Pick): boolean { - return String(voucher.status || '').trim().toUpperCase() === 'CONSUMED' + return ( + String(voucher.status || '') + .trim() + .toUpperCase() === 'CONSUMED' + ) } /** @@ -83,8 +79,9 @@ export async function consumeIndustryVouchersBeforeOpen91Deliver(options: { requestId?: string source?: string }): Promise { - const source = String(options.source || OPEN_91_DELIVER_CONSUME_SOURCE).trim() - || OPEN_91_DELIVER_CONSUME_SOURCE + const source = + String(options.source || OPEN_91_DELIVER_CONSUME_SOURCE).trim() || + OPEN_91_DELIVER_CONSUME_SOURCE const readySet = new Set( (Array.isArray(options.readyTaskIds) ? options.readyTaskIds : []) .map((id) => Number(id)) @@ -213,9 +210,7 @@ async function ensureBindingPreparedAfterDeliver(options: { return } - const flow = normalizeKuaishouCloudFlow( - parseTaskContext(options.task).kuaishouCloudFulfillment, - ) + const flow = normalizeKuaishouCloudFlow(parseTaskContext(options.task).kuaishouCloudFulfillment) if (flow.binding.prepareStatus === 'ready' && String(flow.binding.bindUrl || '').trim()) { return } diff --git a/apps/backend/src/services/open-91/order-query-service.ts b/apps/backend/src/services/open-91/order-query-service.ts index 1803564a..6b9ab664 100644 --- a/apps/backend/src/services/open-91/order-query-service.ts +++ b/apps/backend/src/services/open-91/order-query-service.ts @@ -3,7 +3,7 @@ import { listKuaishouIndustryVouchersByOid } from '../../repositories/kuaishou-i import { listTasksByOrderId } from '../../repositories/task-repo.js' import { resolveTaskDeliveryLink } from '../fulfillment/delivery-link-service.js' import { logIntegration } from '../../utils/logger.js' -import { asJsonObject, type JsonObject } from '../../types/json.js' +import { asJsonObject, type JsonObject } from '../../types/json.js' import { OPEN_91_MANUAL_FAILED_STATUS, OPEN_91_PENDING_CONFIG_STATUS, @@ -114,10 +114,12 @@ export async function queryOpen91Order(payload: JsonObject = {}, { requestId = ' } readyTaskIds.push(Number(task.id)) - cardItems.push(buildOpen91CardItem({ - claimUrl, - expireTime: deliveryLink?.expireTime || '', - })) + cardItems.push( + buildOpen91CardItem({ + claimUrl, + expireTime: deliveryLink?.expireTime || '', + }), + ) } const queryState = resolveOpen91QueryState({ diff --git a/apps/backend/src/services/open-91/payload.ts b/apps/backend/src/services/open-91/payload.ts index 3a064fd9..d9062cab 100644 --- a/apps/backend/src/services/open-91/payload.ts +++ b/apps/backend/src/services/open-91/payload.ts @@ -64,7 +64,10 @@ export function assertOpen91QueryPayload(payload: JsonObject, config = assertOpe assertOpen91CommonPayload(payload, config) } -export function assertOpen91Timestamp(timestamp: unknown, toleranceSeconds = assertOpen91Config().timestampToleranceSeconds) { +export function assertOpen91Timestamp( + timestamp: unknown, + toleranceSeconds = assertOpen91Config().timestampToleranceSeconds, +) { const nowSeconds = Math.floor(Date.now() / 1000) if (Math.abs(nowSeconds - Number(timestamp)) > Math.max(1, Number(toleranceSeconds) || 600)) { throw createHttpError('timestamp 已过期', { diff --git a/apps/backend/src/services/open-91/protocol.test.ts b/apps/backend/src/services/open-91/protocol.test.ts index b72625d3..8e44d467 100644 --- a/apps/backend/src/services/open-91/protocol.test.ts +++ b/apps/backend/src/services/open-91/protocol.test.ts @@ -7,11 +7,7 @@ import { signOpen91Payload, verifyOpen91Signature, } from './crypto.js' -import { - buildOpen91CardItem, - formatOpen91ExpireTime, - resolveOpen91QueryState, -} from './response.js' +import { buildOpen91CardItem, formatOpen91ExpireTime, resolveOpen91QueryState } from './response.js' test('signOpen91Payload signs only request body fields', () => { const config = { @@ -77,7 +73,10 @@ test('encryptOpen91Cards matches documented AES ECB base64 sample', () => { test('resolveOpen91QueryState returns success only when every task is ready', () => { assert.deepEqual( resolveOpen91QueryState({ - tasks: [{ id: 1, task_status: 'pending_binding_prepare' }, { id: 2, task_status: 'waiting_binding' }], + tasks: [ + { id: 1, task_status: 'pending_binding_prepare' }, + { id: 2, task_status: 'waiting_binding' }, + ], readyTaskIds: [1, 2], }), { @@ -89,7 +88,10 @@ test('resolveOpen91QueryState returns success only when every task is ready', () assert.deepEqual( resolveOpen91QueryState({ - tasks: [{ id: 1, task_status: 'pending_binding_prepare' }, { id: 2, task_status: 'waiting_binding' }], + tasks: [ + { id: 1, task_status: 'pending_binding_prepare' }, + { id: 2, task_status: 'waiting_binding' }, + ], readyTaskIds: [1], }), { diff --git a/apps/backend/src/services/open-91/response.ts b/apps/backend/src/services/open-91/response.ts index 10ebe62c..68f4fd30 100644 --- a/apps/backend/src/services/open-91/response.ts +++ b/apps/backend/src/services/open-91/response.ts @@ -1,8 +1,5 @@ import type { JsonObject } from '../../types/json.js' -import { - OPEN_91_DEFAULT_FAIL_CODE, - OPEN_91_SUCCESS_MESSAGE, -} from './config.js' +import { OPEN_91_DEFAULT_FAIL_CODE, OPEN_91_SUCCESS_MESSAGE } from './config.js' import { isOpen91FailedTaskStatus } from '../../domain/task-status.js' import { normalizeOpen91String } from './payload.js' @@ -32,7 +29,10 @@ export function buildOpen91OrderCost(value = 0) { return Number.isFinite(amount) ? amount.toFixed(4) : '0.0000' } -export function buildOpen91CardItem({ claimUrl = '', expireTime = '' }: { claimUrl?: unknown, expireTime?: unknown } = {}) { +export function buildOpen91CardItem({ + claimUrl = '', + expireTime = '', +}: { claimUrl?: unknown; expireTime?: unknown } = {}) { const normalizedClaimUrl = normalizeOpen91String(claimUrl) return { cardNo: normalizedClaimUrl, @@ -72,11 +72,14 @@ export function formatOpen91ExpireTime(value: unknown = '') { return `${byType.year}-${byType.month}-${byType.day} ${byType.hour}:${byType.minute}:${byType.second}` } -export function resolveOpen91QueryState( - { tasks = [], readyTaskIds = [] }: { tasks?: JsonObject[], readyTaskIds?: unknown[] } = {}, -) { +export function resolveOpen91QueryState({ + tasks = [], + readyTaskIds = [], +}: { tasks?: JsonObject[]; readyTaskIds?: unknown[] } = {}) { const normalizedTasks = Array.isArray(tasks) ? tasks : [] - const readyTaskIdSet = new Set((Array.isArray(readyTaskIds) ? readyTaskIds : []).map((item) => Number(item))) + const readyTaskIdSet = new Set( + (Array.isArray(readyTaskIds) ? readyTaskIds : []).map((item) => Number(item)), + ) if (normalizedTasks.length === 0) { return { diff --git a/apps/backend/src/services/order/cloudtentacles-name-match-service.ts b/apps/backend/src/services/order/cloudtentacles-name-match-service.ts index 5afa9959..f7009720 100644 --- a/apps/backend/src/services/order/cloudtentacles-name-match-service.ts +++ b/apps/backend/src/services/order/cloudtentacles-name-match-service.ts @@ -1,17 +1,11 @@ import type { JsonObject } from '../../types/json.js' -import { - listCloudtentaclesSources, -} from '../platforms/cloudtentacles/source-config-service.js' -import { - getCloudtentaclesSessionStateByKey, -} from '../platforms/cloudtentacles/session-state-service.js' +import { listCloudtentaclesSources } from '../platforms/cloudtentacles/source-config-service.js' +import { getCloudtentaclesSessionStateByKey } from '../platforms/cloudtentacles/session-state-service.js' import { normalizeCloudtentaclesDeviceId, normalizeCloudtentaclesDeviceType, } from '../platforms/cloudtentacles/defaults.js' -import { - listCloudtentaclesSku, -} from '../platforms/cloudtentacles/catalog-service.js' +import { listCloudtentaclesSku } from '../platforms/cloudtentacles/catalog-service.js' import { resolveCloudtentaclesOverrideRule, type CloudtentaclesOverrideRule, @@ -79,9 +73,11 @@ export async function resolveCloudtentaclesSkuByProductName( } const listSources = deps.listCloudtentaclesSources || listCloudtentaclesSources - const getSessionByKey = deps.getCloudtentaclesSessionStateByKey || getCloudtentaclesSessionStateByKey + const getSessionByKey = + deps.getCloudtentaclesSessionStateByKey || getCloudtentaclesSessionStateByKey const listSku = deps.listCloudtentaclesSku || listCloudtentaclesSku - const resolveOverrideRule = deps.resolveCloudtentaclesOverrideRule || resolveCloudtentaclesOverrideRule + const resolveOverrideRule = + deps.resolveCloudtentaclesOverrideRule || resolveCloudtentaclesOverrideRule const sourcesConfig = listSources() if (sourcesConfig.enabled === false) { @@ -89,8 +85,13 @@ export async function resolveCloudtentaclesSkuByProductName( } const sourceContexts = (Array.isArray(sourcesConfig.sources) ? sourcesConfig.sources : []) - .map((source: CloudtentaclesSource) => buildCloudtentaclesSourceContext(source, getSessionByKey)) - .filter((context): context is NonNullable> => Boolean(context)) + .map((source: CloudtentaclesSource) => + buildCloudtentaclesSourceContext(source, getSessionByKey), + ) + .filter( + (context): context is NonNullable> => + Boolean(context), + ) if (sourceContexts.length === 0) { return null @@ -98,7 +99,13 @@ export async function resolveCloudtentaclesSkuByProductName( const overrideRule = resolveOverrideRule(productName) const overrideMatch = overrideRule - ? await resolveOverrideMatch(productName, normalizedProductName, overrideRule, sourceContexts, listSku) + ? await resolveOverrideMatch( + productName, + normalizedProductName, + overrideRule, + sourceContexts, + listSku, + ) : null if (overrideMatch) { return overrideMatch @@ -215,7 +222,8 @@ async function resolveOverrideMatch( } const firstDeliveryItem = deliveryItems[0] - const firstSku = items.find((item) => Number(item.id || 0) === firstDeliveryItem.cloudSkuId) || {} + const firstSku = + items.find((item) => Number(item.id || 0) === firstDeliveryItem.cloudSkuId) || {} const cloudSourceKeys = rule.sourceKey ? [context.sourceKey] : sourceContexts.map((sourceContext) => sourceContext.sourceKey) @@ -246,13 +254,17 @@ async function resolveOverrideMatch( return null } -function resolveOverrideDeliveryItems(rule: CloudtentaclesOverrideRule, skuItems: CloudtentaclesSku[]) { +function resolveOverrideDeliveryItems( + rule: CloudtentaclesOverrideRule, + skuItems: CloudtentaclesSku[], +) { const resolvedItems: CloudtentaclesResolvedDeliveryItem[] = [] for (const ruleItem of rule.deliveryItems) { - const matchedSku = ruleItem.cloudSkuId > 0 - ? skuItems.find((item) => Number(item.id || 0) === ruleItem.cloudSkuId) - : findCloudtentaclesSkuByName(skuItems, ruleItem.cloudSkuName) + const matchedSku = + ruleItem.cloudSkuId > 0 + ? skuItems.find((item) => Number(item.id || 0) === ruleItem.cloudSkuId) + : findCloudtentaclesSkuByName(skuItems, ruleItem.cloudSkuName) const cloudSkuId = Number(matchedSku?.id || 0) const cloudSkuName = String(matchedSku?.name || '').trim() diff --git a/apps/backend/src/services/order/cloudtentacles-override-rule-service.test.ts b/apps/backend/src/services/order/cloudtentacles-override-rule-service.test.ts index 50306db4..9cf89d25 100644 --- a/apps/backend/src/services/order/cloudtentacles-override-rule-service.test.ts +++ b/apps/backend/src/services/order/cloudtentacles-override-rule-service.test.ts @@ -1,9 +1,7 @@ import test from 'node:test' import assert from 'node:assert/strict' -import { - normalizeCloudtentaclesOverrideRuleConfig, -} from './cloudtentacles-override-rule-service.js' +import { normalizeCloudtentaclesOverrideRuleConfig } from './cloudtentacles-override-rule-service.js' test('normalizeCloudtentaclesOverrideRuleConfig merges duplicated delivery items', () => { const config = normalizeCloudtentaclesOverrideRuleConfig({ diff --git a/apps/backend/src/services/order/cloudtentacles-override-rule-service.ts b/apps/backend/src/services/order/cloudtentacles-override-rule-service.ts index 725589e7..7254ca91 100644 --- a/apps/backend/src/services/order/cloudtentacles-override-rule-service.ts +++ b/apps/backend/src/services/order/cloudtentacles-override-rule-service.ts @@ -3,10 +3,7 @@ import type { JsonObject } from '../../types/json.js' import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js' import { PROJECT_ROOT } from '../../config/runtime.js' -import { - readAppConfigEntry, - saveAppConfigEntry, -} from '../config/app-config-store.js' +import { readAppConfigEntry, saveAppConfigEntry } from '../config/app-config-store.js' import { normalizeCloudtentaclesMatchName } from './cloudtentacles-match-utils.js' const CLOUDTENTACLES_OVERRIDE_RULE_FILE_PATH = path.join( @@ -49,7 +46,9 @@ export function getCloudtentaclesOverrideRuleConfig(): CloudtentaclesOverrideRul }) } -export function saveCloudtentaclesOverrideRuleConfig(rawValue: unknown): Promise { +export function saveCloudtentaclesOverrideRuleConfig( + rawValue: unknown, +): Promise { return saveAppConfigEntry({ configKey: APP_CONFIG_KEYS.cloudtentaclesOverrideRules, legacyFilePath: CLOUDTENTACLES_OVERRIDE_RULE_FILE_PATH, @@ -69,14 +68,19 @@ export function resolveCloudtentaclesOverrideRule(productName: unknown) { return null } - return config.rules.find((rule) => - rule.enabled !== false && - rule.normalizedProductName === normalizedProductName && - rule.deliveryItems.length > 0 - ) || null + return ( + config.rules.find( + (rule) => + rule.enabled !== false && + rule.normalizedProductName === normalizedProductName && + rule.deliveryItems.length > 0, + ) || null + ) } -export function normalizeCloudtentaclesOverrideRuleConfig(rawValue: unknown): CloudtentaclesOverrideRuleConfig { +export function normalizeCloudtentaclesOverrideRuleConfig( + rawValue: unknown, +): CloudtentaclesOverrideRuleConfig { const source = isPlainObject(rawValue) ? rawValue : {} const rules = Array.isArray(source.rules) ? source.rules : [] @@ -142,7 +146,10 @@ function mergeDeliveryItems(items: CloudtentaclesOverrideDeliveryItem[]) { const merged = new Map() for (const item of items) { - const key = item.cloudSkuId > 0 ? `id:${item.cloudSkuId}` : `name:${normalizeCloudtentaclesMatchName(item.cloudSkuName)}` + const key = + item.cloudSkuId > 0 + ? `id:${item.cloudSkuId}` + : `name:${normalizeCloudtentaclesMatchName(item.cloudSkuName)}` const existing = merged.get(key) if (existing) { existing.quantity += item.quantity diff --git a/apps/backend/src/services/order/delivery-task-service.test.ts b/apps/backend/src/services/order/delivery-task-service.test.ts index e30b0a8f..5a244811 100644 --- a/apps/backend/src/services/order/delivery-task-service.test.ts +++ b/apps/backend/src/services/order/delivery-task-service.test.ts @@ -13,7 +13,9 @@ type CreatedTaskInput = { [key: string]: unknown } -function stubOrder(patch: Partial & Pick): OrderRow { +function stubOrder( + patch: Partial & Pick, +): OrderRow { return { id: patch.id, order_id: patch.id, diff --git a/apps/backend/src/services/order/delivery-task-service.ts b/apps/backend/src/services/order/delivery-task-service.ts index bc631d3e..b8e2cd28 100644 --- a/apps/backend/src/services/order/delivery-task-service.ts +++ b/apps/backend/src/services/order/delivery-task-service.ts @@ -10,10 +10,7 @@ import { preparePaidFulfillmentTask } from '../fulfillment/executors/registry.js import type { FulfillmentPrepareDeps } from '../fulfillment/executors/types.js' import { nowIso } from '../../utils/time.js' import { randomId } from '../../utils/random.js' -import { - TASK_STATUS, - resolveInitialPaidTaskStatus, -} from '../../domain/task-status.js' +import { TASK_STATUS, resolveInitialPaidTaskStatus } from '../../domain/task-status.js' import type { OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js' type ClaimTokenLike = { @@ -152,10 +149,7 @@ export async function syncDeliveryTasksForOrderWithDeps( return preparedTasks.filter(isTaskRow) } -async function preparePaidTasks( - tasks: TaskRow[], - deps: FulfillmentPrepareDeps, -) { +async function preparePaidTasks(tasks: TaskRow[], deps: FulfillmentPrepareDeps) { const preparedTasks = await Promise.all( tasks.map((task) => preparePaidFulfillmentTask(task, deps)), ) diff --git a/apps/backend/src/services/order/order-service.test.ts b/apps/backend/src/services/order/order-service.test.ts index bd091c6b..c7351a9b 100644 --- a/apps/backend/src/services/order/order-service.test.ts +++ b/apps/backend/src/services/order/order-service.test.ts @@ -1,10 +1,7 @@ import test from 'node:test' import assert from 'node:assert/strict' -import { - isOrderItemConfiguredForFulfillment, - mergeSourceOrderState, -} from './order-service.js' +import { isOrderItemConfiguredForFulfillment, mergeSourceOrderState } from './order-service.js' import type { OrderItemRow } from '../../types/repository/rows.js' function buildOrderItem(snapshot: OrderItemRow['item_snapshot_json']): OrderItemRow { @@ -20,17 +17,11 @@ function buildOrderItem(snapshot: OrderItemRow['item_snapshot_json']): OrderItem } test('isOrderItemConfiguredForFulfillment accepts PostgreSQL JSONB object results', () => { - assert.equal( - isOrderItemConfiguredForFulfillment(buildOrderItem({ isConfigured: true })), - true, - ) + assert.equal(isOrderItemConfiguredForFulfillment(buildOrderItem({ isConfigured: true })), true) }) test('isOrderItemConfiguredForFulfillment accepts serialized snapshots', () => { - assert.equal( - isOrderItemConfiguredForFulfillment(buildOrderItem('{"isConfigured":true}')), - true, - ) + assert.equal(isOrderItemConfiguredForFulfillment(buildOrderItem('{"isConfigured":true}')), true) }) test('isOrderItemConfiguredForFulfillment rejects unconfigured or malformed snapshots', () => { diff --git a/apps/backend/src/services/order/order-service.ts b/apps/backend/src/services/order/order-service.ts index 32c1b421..aa9a8064 100644 --- a/apps/backend/src/services/order/order-service.ts +++ b/apps/backend/src/services/order/order-service.ts @@ -297,7 +297,7 @@ function parseOrderItemSnapshot(value: unknown): Record | null try { const parsed = JSON.parse(String(value || '{}')) return parsed && typeof parsed === 'object' && !Array.isArray(parsed) - ? parsed as Record + ? (parsed as Record) : null } catch { return null diff --git a/apps/backend/src/services/platforms/affiliate-dash/config.ts b/apps/backend/src/services/platforms/affiliate-dash/config.ts index 02f8f95f..b76070dc 100644 --- a/apps/backend/src/services/platforms/affiliate-dash/config.ts +++ b/apps/backend/src/services/platforms/affiliate-dash/config.ts @@ -21,14 +21,19 @@ export function getAffiliateDashConfig(overrides: Partial export type AffiliateDashProductListResult = ReturnType -export async function listAffiliateDashProducts(input: { - page?: number | undefined - size?: number | undefined -} = {}) { +export async function listAffiliateDashProducts( + input: { + page?: number | undefined + size?: number | undefined + } = {}, +) { const params = new URLSearchParams() if (input.page !== undefined) { params.set('page', String(input.page)) @@ -23,9 +25,11 @@ export async function listAffiliateDashProducts(input: { } /** 翻页拉取全部可售商品(默认每页 100)。 */ -export async function listAllAffiliateDashProducts(input: { - pageSize?: number | undefined -} = {}) { +export async function listAllAffiliateDashProducts( + input: { + pageSize?: number | undefined + } = {}, +) { const size = Math.max(1, Number(input.pageSize || 100) || 100) const products: AffiliateDashProduct[] = [] let page = 1 diff --git a/apps/backend/src/services/platforms/affiliate-dash/sign.test.ts b/apps/backend/src/services/platforms/affiliate-dash/sign.test.ts index 5ae180d0..5cb17095 100644 --- a/apps/backend/src/services/platforms/affiliate-dash/sign.test.ts +++ b/apps/backend/src/services/platforms/affiliate-dash/sign.test.ts @@ -86,10 +86,7 @@ test('verifyCallbackSign accepts valid sign and rejects tampering', () => { const timestamp = '1783394218' const sign = buildCallbackSign({ callbackSecret, rawBody, timestamp }) - assert.equal( - verifyCallbackSign({ callbackSecret, rawBody, timestamp, sign }), - true, - ) + assert.equal(verifyCallbackSign({ callbackSecret, rawBody, timestamp, sign }), true) assert.equal( verifyCallbackSign({ callbackSecret, diff --git a/apps/backend/src/services/platforms/affiliate-dash/sign.ts b/apps/backend/src/services/platforms/affiliate-dash/sign.ts index 20e30263..79d42ed0 100644 --- a/apps/backend/src/services/platforms/affiliate-dash/sign.ts +++ b/apps/backend/src/services/platforms/affiliate-dash/sign.ts @@ -96,7 +96,12 @@ export function verifyCallbackSign(input: { timestamp: input.timestamp, }) - return timingSafeEqualString(expected, String(input.sign || '').trim().toLowerCase()) + return timingSafeEqualString( + expected, + String(input.sign || '') + .trim() + .toLowerCase(), + ) } export function timingSafeEqualString(left: string, right: string): boolean { diff --git a/apps/backend/src/services/platforms/affiliate-dash/source-config-service.ts b/apps/backend/src/services/platforms/affiliate-dash/source-config-service.ts index d7722ee5..a85f301d 100644 --- a/apps/backend/src/services/platforms/affiliate-dash/source-config-service.ts +++ b/apps/backend/src/services/platforms/affiliate-dash/source-config-service.ts @@ -35,7 +35,9 @@ export function getAffiliateDashSourceConfig(): AffiliateDashSourceConfig { }) } -export function saveAffiliateDashSourceConfig(rawValue: unknown): Promise { +export function saveAffiliateDashSourceConfig( + rawValue: unknown, +): Promise { return saveAppConfigEntry({ configKey: AFFILIATE_DASH_CONFIG_KEY, value: rawValue, @@ -48,7 +50,9 @@ export function normalizeAffiliateDashSourceConfig(rawValue: unknown): Affiliate return { enabled: source.enabled !== false, - baseUrl: String(source.baseUrl || '').trim().replace(/\/+$/, ''), + baseUrl: String(source.baseUrl || '') + .trim() + .replace(/\/+$/, ''), appKey: String(source.appKey || '').trim(), appSecret: String(source.appSecret || '').trim(), callbackSecret: String(source.callbackSecret || '').trim(), diff --git a/apps/backend/src/services/platforms/affiliate-dash/verify-callback.test.ts b/apps/backend/src/services/platforms/affiliate-dash/verify-callback.test.ts index 8791dfa4..37cb3505 100644 --- a/apps/backend/src/services/platforms/affiliate-dash/verify-callback.test.ts +++ b/apps/backend/src/services/platforms/affiliate-dash/verify-callback.test.ts @@ -59,7 +59,8 @@ test('verifyAffiliateDashCallback rejects tampered body', () => { assert.throws( () => verifyAffiliateDashCallback(input, { callbackSecret: CALLBACK_SECRET }), - (error: Error & { errorCode?: string }) => error.errorCode === 'affiliate_dash_callback_sign_invalid', + (error: Error & { errorCode?: string }) => + error.errorCode === 'affiliate_dash_callback_sign_invalid', ) }) @@ -107,10 +108,9 @@ test('verifyAffiliateDashCallback passes non-JSON body as long as sign matches', const timestamp = String(Math.floor(Date.now() / 1000)) const rawBody = 'not-json' - const result = verifyAffiliateDashCallback( - makeCallback({ rawBody, timestamp }), - { callbackSecret: CALLBACK_SECRET }, - ) + const result = verifyAffiliateDashCallback(makeCallback({ rawBody, timestamp }), { + callbackSecret: CALLBACK_SECRET, + }) assert.equal(result.event, '') assert.deepEqual(result.data, {}) diff --git a/apps/backend/src/services/platforms/affiliate-dash/verify-callback.ts b/apps/backend/src/services/platforms/affiliate-dash/verify-callback.ts index 110a7f08..19736b23 100644 --- a/apps/backend/src/services/platforms/affiliate-dash/verify-callback.ts +++ b/apps/backend/src/services/platforms/affiliate-dash/verify-callback.ts @@ -75,12 +75,14 @@ export function verifyAffiliateDashCallback( }) } - if (!verifyCallbackSign({ - callbackSecret: config.callbackSecret, - rawBody, - timestamp, - sign, - })) { + if ( + !verifyCallbackSign({ + callbackSecret: config.callbackSecret, + rawBody, + timestamp, + sign, + }) + ) { throw createHttpError('affiliate-dash 回调验签失败', { statusCode: 401, errorCode: 'affiliate_dash_callback_sign_invalid', diff --git a/apps/backend/src/services/platforms/cloudtentacles/catalog-service.ts b/apps/backend/src/services/platforms/cloudtentacles/catalog-service.ts index 792460c5..098417a1 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/catalog-service.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/catalog-service.ts @@ -4,7 +4,11 @@ import { resolveCloudtentaclesConfig } from './helpers.js' import type { JsonObject } from '../../../types/json.js' export async function getCloudtentaclesAsset(payload: JsonObject = {}) { - const token = requireToken(payload.token, 'cloudtentacles 余额查询缺少 token', 'cloudtentacles_asset_missing_token') + const token = requireToken( + payload.token, + 'cloudtentacles 余额查询缺少 token', + 'cloudtentacles_asset_missing_token', + ) const config = resolveCloudtentaclesConfig(payload) const result = await cloudtentaclesRequest(config.assetPath, { @@ -14,7 +18,10 @@ export async function getCloudtentaclesAsset(payload: JsonObject = {}) { contentType: 'application/json', sourceKey: payload.sourceKey || payload.resolvedSourceKey, accountLabel: payload.accountLabel, - context: { operation: 'asset_get', sourceKey: payload.sourceKey || payload.resolvedSourceKey || '' }, + context: { + operation: 'asset_get', + sourceKey: payload.sourceKey || payload.resolvedSourceKey || '', + }, businessErrorStatusCode: 401, businessErrorCode: 'cloudtentacles_asset_failed', }) @@ -27,7 +34,11 @@ export async function getCloudtentaclesAsset(payload: JsonObject = {}) { } export async function getCloudtentaclesCategories(payload: JsonObject = {}) { - const token = requireToken(payload.token, 'cloudtentacles 分类查询缺少 token', 'cloudtentacles_categories_missing_token') + const token = requireToken( + payload.token, + 'cloudtentacles 分类查询缺少 token', + 'cloudtentacles_categories_missing_token', + ) const config = resolveCloudtentaclesConfig(payload) const result = await cloudtentaclesRequest(config.categoriesPath, { @@ -37,7 +48,10 @@ export async function getCloudtentaclesCategories(payload: JsonObject = {}) { contentType: 'application/json', sourceKey: payload.sourceKey || payload.resolvedSourceKey, accountLabel: payload.accountLabel, - context: { operation: 'categories_list', sourceKey: payload.sourceKey || payload.resolvedSourceKey || '' }, + context: { + operation: 'categories_list', + sourceKey: payload.sourceKey || payload.resolvedSourceKey || '', + }, businessErrorStatusCode: 401, businessErrorCode: 'cloudtentacles_categories_failed', }) @@ -53,7 +67,11 @@ export async function getCloudtentaclesCategories(payload: JsonObject = {}) { } export async function listCloudtentaclesSku(payload: JsonObject = {}) { - const token = requireToken(payload.token, 'cloudtentacles SKU 列表查询缺少 token', 'cloudtentacles_sku_list_missing_token') + const token = requireToken( + payload.token, + 'cloudtentacles SKU 列表查询缺少 token', + 'cloudtentacles_sku_list_missing_token', + ) const config = resolveCloudtentaclesConfig(payload) const result = await cloudtentaclesRequest(config.skuListPath, { @@ -63,7 +81,10 @@ export async function listCloudtentaclesSku(payload: JsonObject = {}) { contentType: 'application/json', sourceKey: payload.sourceKey || payload.resolvedSourceKey, accountLabel: payload.accountLabel, - context: { operation: 'sku_list', sourceKey: payload.sourceKey || payload.resolvedSourceKey || '' }, + context: { + operation: 'sku_list', + sourceKey: payload.sourceKey || payload.resolvedSourceKey || '', + }, businessErrorStatusCode: 401, businessErrorCode: 'cloudtentacles_sku_list_failed', }) @@ -79,8 +100,16 @@ export async function listCloudtentaclesSku(payload: JsonObject = {}) { } export async function buyCloudtentaclesSku(payload: JsonObject = {}) { - const token = requireToken(payload.token, 'cloudtentacles 购买 SKU 缺少 token', 'cloudtentacles_sku_buy_missing_token') - const skuId = requireId(payload.id, 'cloudtentacles 购买 SKU 缺少商品 id', 'cloudtentacles_sku_buy_missing_id') + const token = requireToken( + payload.token, + 'cloudtentacles 购买 SKU 缺少 token', + 'cloudtentacles_sku_buy_missing_token', + ) + const skuId = requireId( + payload.id, + 'cloudtentacles 购买 SKU 缺少商品 id', + 'cloudtentacles_sku_buy_missing_id', + ) const count = requireCount(payload.count) const config = resolveCloudtentaclesConfig(payload) @@ -114,8 +143,16 @@ export async function buyCloudtentaclesSku(payload: JsonObject = {}) { } export async function useCloudtentaclesSku(payload: JsonObject = {}) { - const token = requireToken(payload.token, 'cloudtentacles 发货缺少 token', 'cloudtentacles_sku_use_missing_token') - const skuId = requireId(payload.id, 'cloudtentacles 发货缺少商品 id', 'cloudtentacles_sku_use_missing_id') + const token = requireToken( + payload.token, + 'cloudtentacles 发货缺少 token', + 'cloudtentacles_sku_use_missing_token', + ) + const skuId = requireId( + payload.id, + 'cloudtentacles 发货缺少商品 id', + 'cloudtentacles_sku_use_missing_id', + ) const virtualNumberId = requireId( payload.virtualNumberId, 'cloudtentacles 发货缺少虚拟号 id', diff --git a/apps/backend/src/services/platforms/cloudtentacles/crypto-service.ts b/apps/backend/src/services/platforms/cloudtentacles/crypto-service.ts index 7e6f2ba8..e23985f3 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/crypto-service.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/crypto-service.ts @@ -5,7 +5,9 @@ import { createHttpError } from '../../../utils/http.js' import { resolveCloudtentaclesConfig } from './helpers.js' export function md5CloudtentaclesPassword(password: unknown) { - return createHash('md5').update(String(password || ''), 'utf8').digest('hex') + return createHash('md5') + .update(String(password || ''), 'utf8') + .digest('hex') } export function encryptCloudtentaclesPayload(payload: JsonObject = {}, options: JsonObject = {}) { @@ -20,7 +22,8 @@ export function encryptCloudtentaclesPayload(payload: JsonObject = {}, options: } const timestamp = Number(options.timestamp || Date.now()) - const randomValue = String(options.randomValue || Math.random().toString(16)).trim() || Math.random().toString(16) + const randomValue = + String(options.randomValue || Math.random().toString(16)).trim() || Math.random().toString(16) const normalizedPayload = removeEmptyFields(payload) const plaintext = JSON.stringify({ t: timestamp, @@ -59,6 +62,8 @@ function removeEmptyFields(payload: unknown) { } return Object.fromEntries( - Object.entries(payload).filter(([, value]) => value !== '' && value !== null && typeof value !== 'undefined'), + Object.entries(payload).filter( + ([, value]) => value !== '' && value !== null && typeof value !== 'undefined', + ), ) } diff --git a/apps/backend/src/services/platforms/cloudtentacles/debug-flow-service.ts b/apps/backend/src/services/platforms/cloudtentacles/debug-flow-service.ts index eb257c19..d29fd90b 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/debug-flow-service.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/debug-flow-service.ts @@ -1,5 +1,9 @@ import { createHttpError } from '../../../utils/http.js' -import { getCloudtentaclesAsset, listCloudtentaclesSku, buyCloudtentaclesSku } from './catalog-service.js' +import { + getCloudtentaclesAsset, + listCloudtentaclesSku, + buyCloudtentaclesSku, +} from './catalog-service.js' import { getCloudtentaclesKnapsack } from './knapsack-service.js' import type { JsonObject } from '../../../types/json.js' import { @@ -55,15 +59,22 @@ export async function runCloudtentaclesFullDebugFlow(payload: JsonObject = {}) { } await sleep(350) - const beforeKnapsack = await runFlowStep('查询购买前背包', () => getCloudtentaclesKnapsack(payload)) + const beforeKnapsack = await runFlowStep('查询购买前背包', () => + getCloudtentaclesKnapsack(payload), + ) const beforeItem = findKnapsackItem(beforeKnapsack.items, skuId) await sleep(500) - const buyResult = await runFlowStep('购买 SKU', () => buyCloudtentaclesSku({ - ...payload, - id: skuId, - count: skuCount, - }), { retries: 1, retryDelayMs: 1200 }) + const buyResult = await runFlowStep( + '购买 SKU', + () => + buyCloudtentaclesSku({ + ...payload, + id: skuId, + count: skuCount, + }), + { retries: 1, retryDelayMs: 1200 }, + ) await sleep(1200) const afterAsset = await runFlowStep('查询购买后余额', () => getCloudtentaclesAsset(payload), { @@ -71,10 +82,14 @@ export async function runCloudtentaclesFullDebugFlow(payload: JsonObject = {}) { retryDelayMs: 1200, }) await sleep(1200) - const afterKnapsack = await runFlowStep('查询购买后背包', () => getCloudtentaclesKnapsack(payload), { - retries: 2, - retryDelayMs: 1200, - }) + const afterKnapsack = await runFlowStep( + '查询购买后背包', + () => getCloudtentaclesKnapsack(payload), + { + retries: 2, + retryDelayMs: 1200, + }, + ) const afterItem = findKnapsackItem(afterKnapsack.items, skuId) const beforeCount = Number(beforeItem?.count || 0) @@ -82,17 +97,25 @@ export async function runCloudtentaclesFullDebugFlow(payload: JsonObject = {}) { const knapsackIncreased = afterCount >= beforeCount + skuCount if (!knapsackIncreased) { - throw createHttpError(`购买后背包校验失败,购买前 ${beforeCount},购买后 ${afterCount},期望至少 ${beforeCount + skuCount}`, { - statusCode: 409, - errorCode: 'cloudtentacles_full_flow_knapsack_not_updated', - }) + throw createHttpError( + `购买后背包校验失败,购买前 ${beforeCount},购买后 ${afterCount},期望至少 ${beforeCount + skuCount}`, + { + statusCode: 409, + errorCode: 'cloudtentacles_full_flow_knapsack_not_updated', + }, + ) } await sleep(700) - const appointed = await runFlowStep('申请虚拟号', () => appointCloudtentaclesVirtualNumber({ - ...payload, - key: vnKey, - }), { retries: 1, retryDelayMs: 1200 }) + const appointed = await runFlowStep( + '申请虚拟号', + () => + appointCloudtentaclesVirtualNumber({ + ...payload, + key: vnKey, + }), + { retries: 1, retryDelayMs: 1200 }, + ) const appointedId = Number(appointed.item?.id || 0) const appointedPhone = String(appointed.item?.phone || '').trim() @@ -104,33 +127,53 @@ export async function runCloudtentaclesFullDebugFlow(payload: JsonObject = {}) { } await sleep(1200) - const generateCodeResult = await runFlowStep('生成登录码', () => generateCloudtentaclesLoginCode({ - ...payload, - key: vnKey, - id: appointedId, - }), { retries: 1, retryDelayMs: 1200 }) + const generateCodeResult = await runFlowStep( + '生成登录码', + () => + generateCloudtentaclesLoginCode({ + ...payload, + key: vnKey, + id: appointedId, + }), + { retries: 1, retryDelayMs: 1200 }, + ) await sleep(1600) - const fetchedCode = await runFlowStep('获取验证码', () => fetchCloudtentaclesVirtualNumberCode({ - ...payload, - key: vnKey, - phone: appointedPhone, - }), { retries: 2, retryDelayMs: 1500 }) + const fetchedCode = await runFlowStep( + '获取验证码', + () => + fetchCloudtentaclesVirtualNumberCode({ + ...payload, + key: vnKey, + phone: appointedPhone, + }), + { retries: 2, retryDelayMs: 1500 }, + ) await sleep(1000) - const verified = await runFlowStep('校验验证码', () => verifyCloudtentaclesLoginCode({ - ...payload, - key: vnKey, - id: appointedId, - code: fetchedCode.code, - }), { retries: 1, retryDelayMs: 1200 }) + const verified = await runFlowStep( + '校验验证码', + () => + verifyCloudtentaclesLoginCode({ + ...payload, + key: vnKey, + id: appointedId, + code: fetchedCode.code, + }), + { retries: 1, retryDelayMs: 1200 }, + ) await sleep(1000) - const bindUrlResult = await runFlowStep('获取兑换链接', () => getCloudtentaclesBindUrl({ - ...payload, - key: vnKey, - id: appointedId, - }), { retries: 1, retryDelayMs: 1200 }) + const bindUrlResult = await runFlowStep( + '获取兑换链接', + () => + getCloudtentaclesBindUrl({ + ...payload, + key: vnKey, + id: appointedId, + }), + { retries: 1, retryDelayMs: 1200 }, + ) return { sku: { @@ -173,7 +216,10 @@ function normalizePositiveInteger(value: unknown, fallback: number) { } function findKnapsackItem(items: unknown, skuId: unknown) { - return (Array.isArray(items) ? items : []).find((item) => Number(item.id || 0) === Number(skuId)) || null + return ( + (Array.isArray(items) ? items : []).find((item) => Number(item.id || 0) === Number(skuId)) || + null + ) } async function runFlowStep( @@ -184,7 +230,8 @@ async function runFlowStep( const rawRetries = Number(options.retries) const rawRetryDelayMs = Number(options.retryDelayMs) const retries = Number.isInteger(rawRetries) && rawRetries > 0 ? rawRetries : 0 - const retryDelayMs = Number.isFinite(rawRetryDelayMs) && rawRetryDelayMs > 0 ? rawRetryDelayMs : 1000 + const retryDelayMs = + Number.isFinite(rawRetryDelayMs) && rawRetryDelayMs > 0 ? rawRetryDelayMs : 1000 for (let attempt = 0; attempt <= retries; attempt += 1) { try { @@ -221,7 +268,9 @@ function wrapStepError(label: string, error: unknown) { if (error && typeof error === 'object' && 'statusCode' in error) { const statusCode = Number(Reflect.get(error, 'statusCode') || 500) - const errorCode = String(Reflect.get(error, 'errorCode') || 'cloudtentacles_full_flow_step_failed') + const errorCode = String( + Reflect.get(error, 'errorCode') || 'cloudtentacles_full_flow_step_failed', + ) return createHttpError(`cloudtentacles ${label}失败:${message}`, { statusCode, diff --git a/apps/backend/src/services/platforms/cloudtentacles/defaults.ts b/apps/backend/src/services/platforms/cloudtentacles/defaults.ts index 784759e2..3c444f41 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/defaults.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/defaults.ts @@ -1,17 +1,12 @@ -export const DEFAULT_CLOUDTENTACLES_DEVICE_ID = - "08bc9d8c-fd15-48ea-bc00-8d754076cafc"; -export const DEFAULT_CLOUDTENTACLES_DEVICE_TYPE = 1; +export const DEFAULT_CLOUDTENTACLES_DEVICE_ID = '08bc9d8c-fd15-48ea-bc00-8d754076cafc' +export const DEFAULT_CLOUDTENTACLES_DEVICE_TYPE = 1 export function normalizeCloudtentaclesDeviceId(value: unknown) { - const normalized = String(value || "").trim(); - return normalized && normalized !== "-" - ? normalized - : DEFAULT_CLOUDTENTACLES_DEVICE_ID; + const normalized = String(value || '').trim() + return normalized && normalized !== '-' ? normalized : DEFAULT_CLOUDTENTACLES_DEVICE_ID } export function normalizeCloudtentaclesDeviceType(value: unknown) { - const parsed = Number(value); - return Number.isInteger(parsed) && parsed > 0 - ? parsed - : DEFAULT_CLOUDTENTACLES_DEVICE_TYPE; + const parsed = Number(value) + return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_CLOUDTENTACLES_DEVICE_TYPE } diff --git a/apps/backend/src/services/platforms/cloudtentacles/helpers.ts b/apps/backend/src/services/platforms/cloudtentacles/helpers.ts index df4e97d7..66f75805 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/helpers.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/helpers.ts @@ -52,46 +52,98 @@ export function resolveCloudtentaclesConfig(overrides: Partial [String(key || '').trim().toLowerCase(), String(value || '').trim()]) + .map(([key, value]) => [ + String(key || '') + .trim() + .toLowerCase(), + String(value || '').trim(), + ]) .filter(([key, value]) => key && value), ) } diff --git a/apps/backend/src/services/platforms/cloudtentacles/http-client.test.ts b/apps/backend/src/services/platforms/cloudtentacles/http-client.test.ts index 9602b007..13589b85 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/http-client.test.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/http-client.test.ts @@ -24,9 +24,8 @@ test('cloudtentaclesRequest retries high-frequency business errors', async () => const server = await createMockServer(() => { callCount += 1 return { - body: callCount === 1 - ? { code: 1, message: 'high-frequency Request' } - : { code: 0, data: 'ok' }, + body: + callCount === 1 ? { code: 1, message: 'high-frequency Request' } : { code: 0, data: 'ok' }, } }) @@ -155,15 +154,16 @@ async function createMockServer(handler: () => MockResponse) { return { baseUrl: `http://127.0.0.1:${address?.port}`, - close: () => new Promise((resolve, reject) => { - server.close((error) => { - if (error) { - reject(error) - return - } + close: () => + new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error) + return + } - resolve() - }) - }), + resolve() + }) + }), } } diff --git a/apps/backend/src/services/platforms/cloudtentacles/http-client.ts b/apps/backend/src/services/platforms/cloudtentacles/http-client.ts index 87bd49a8..79d88b04 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/http-client.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/http-client.ts @@ -5,7 +5,11 @@ import type { JsonObject } from '../../../types/json.js' import { createHttpError } from '../../../utils/http.js' import { logInfo, logWarn } from '../../../utils/logger.js' import { notifyCloudtentaclesAuthExpired } from '../../notification/domain-notifications.js' -import { buildCloudtentaclesHeaders, buildCloudtentaclesUrl, resolveCloudtentaclesConfig } from './helpers.js' +import { + buildCloudtentaclesHeaders, + buildCloudtentaclesUrl, + resolveCloudtentaclesConfig, +} from './helpers.js' type HeaderAdapter = { get(name: unknown): string | null @@ -32,10 +36,13 @@ export async function cloudtentaclesRequest(pathname: unknown, options: JsonObje const config = resolveCloudtentaclesConfig(options) const url = buildCloudtentaclesUrl(config.baseUrl, normalizedPathname, options.searchParams) const timeoutMs = Number(options.timeoutMs || config.timeoutMs || 5000) - const method = String(options.method || 'GET').trim().toUpperCase() + const method = String(options.method || 'GET') + .trim() + .toUpperCase() const headers = buildCloudtentaclesHeaders({ token: options.token, - contentType: options.contentType === null ? '' : options.contentType || inferContentType(options.body), + contentType: + options.contentType === null ? '' : options.contentType || inferContentType(options.body), deviceId: options.deviceId ?? config.deviceId, deviceType: options.deviceType ?? config.deviceType, extra: options.headers, @@ -52,7 +59,10 @@ export async function cloudtentaclesRequest(pathname: unknown, options: JsonObje options.rateLimitIntervalMs, resolveDefaultRateLimitIntervalMs(normalizedPathname), ) - const maxRetries = normalizeNonNegativeInteger(options.rateLimitRetries, DEFAULT_RATE_LIMIT_RETRIES) + const maxRetries = normalizeNonNegativeInteger( + options.rateLimitRetries, + DEFAULT_RATE_LIMIT_RETRIES, + ) const retryDelayMs = normalizeNonNegativeInteger( options.rateLimitRetryDelayMs, resolveDefaultRateLimitRetryDelayMs(normalizedPathname), @@ -219,7 +229,9 @@ export async function cloudtentaclesRequest(pathname: unknown, options: JsonObje /** 导出供单测:识别上游限频文案/错误码(含中文 110001) */ export function isHighFrequencyMessage(message: unknown, code: unknown = null) { - const text = String(message || '').trim().toLowerCase() + const text = String(message || '') + .trim() + .toLowerCase() const codeText = String(code ?? '').trim() if (codeText === '110001' || Number(code) === 110001) { @@ -243,8 +255,12 @@ export function isHighFrequencyMessage(message: unknown, code: unknown = null) { } function isCloudtentaclesExpiredMessage(message: unknown) { - const normalized = String(message || '').trim().toLowerCase() - return normalized.includes('expired') || normalized.includes('过期') || normalized.includes('失效') + const normalized = String(message || '') + .trim() + .toLowerCase() + return ( + normalized.includes('expired') || normalized.includes('过期') || normalized.includes('失效') + ) } function isCloudtentaclesHttpRateLimited(status: unknown) { @@ -267,7 +283,9 @@ function resolveDefaultRateLimitRetryDelayMs(pathname: string) { } function isBindInfoPath(pathname: string) { - const normalized = String(pathname || '').trim().toLowerCase() + const normalized = String(pathname || '') + .trim() + .toLowerCase() return normalized.includes('bind_info') || normalized.endsWith('/vn/bind_info') } @@ -445,7 +463,12 @@ function tryParseJson(text: string) { async function requestViaNodeHttp( url: URL, - { method, headers, body, signal }: { + { + method, + headers, + body, + signal, + }: { method: string headers: JsonObject body?: string | URLSearchParams @@ -456,27 +479,31 @@ async function requestViaNodeHttp( const transport = isHttps ? https : http return new Promise((resolve, reject) => { - const request = transport.request(url, { - method, - headers, - rejectUnauthorized: false, - }, (response) => { - const chunks: Buffer[] = [] + const request = transport.request( + url, + { + method, + headers, + rejectUnauthorized: false, + }, + (response) => { + const chunks: Buffer[] = [] - response.on('data', (chunk) => { - chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) - }) - - response.on('end', () => { - const bodyText = Buffer.concat(chunks).toString('utf8') - resolve({ - ok: Number(response.statusCode || 0) >= 200 && Number(response.statusCode || 0) < 300, - status: Number(response.statusCode || 0), - headers: createHeaderAdapter(response.headers), - bodyText, + response.on('data', (chunk) => { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) }) - }) - }) + + response.on('end', () => { + const bodyText = Buffer.concat(chunks).toString('utf8') + resolve({ + ok: Number(response.statusCode || 0) >= 200 && Number(response.statusCode || 0) < 300, + status: Number(response.statusCode || 0), + headers: createHeaderAdapter(response.headers), + bodyText, + }) + }) + }, + ) request.on('error', reject) @@ -486,11 +513,15 @@ async function requestViaNodeHttp( error.name = 'AbortError' request.destroy(error) } else { - signal.addEventListener('abort', () => { - const error = new Error('Request aborted') - error.name = 'AbortError' - request.destroy(error) - }, { once: true }) + signal.addEventListener( + 'abort', + () => { + const error = new Error('Request aborted') + error.name = 'AbortError' + request.destroy(error) + }, + { once: true }, + ) } } diff --git a/apps/backend/src/services/platforms/cloudtentacles/knapsack-service.ts b/apps/backend/src/services/platforms/cloudtentacles/knapsack-service.ts index 7a06864c..3e382d17 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/knapsack-service.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/knapsack-service.ts @@ -20,7 +20,10 @@ export async function getCloudtentaclesKnapsack(payload: JsonObject = {}) { contentType: 'application/json', sourceKey: payload.sourceKey || payload.resolvedSourceKey, accountLabel: payload.accountLabel, - context: { operation: 'knapsack_get', sourceKey: payload.sourceKey || payload.resolvedSourceKey || '' }, + context: { + operation: 'knapsack_get', + sourceKey: payload.sourceKey || payload.resolvedSourceKey || '', + }, businessErrorStatusCode: 401, businessErrorCode: 'cloudtentacles_knapsack_failed', }) diff --git a/apps/backend/src/services/platforms/cloudtentacles/record-service.ts b/apps/backend/src/services/platforms/cloudtentacles/record-service.ts index ccc6f3fe..8df93465 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/record-service.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/record-service.ts @@ -26,11 +26,9 @@ export async function listCloudtentaclesDeliveryRecords( deviceId: payload.deviceId, deviceType: payload.deviceType, }), - ...( - typeof payload.timeoutMs === 'undefined' - ? {} - : { timeoutMs: normalizePositiveInteger(payload.timeoutMs, 5000) } - ), + ...(typeof payload.timeoutMs === 'undefined' + ? {} + : { timeoutMs: normalizePositiveInteger(payload.timeoutMs, 5000) }), }) const page = normalizePositiveInteger(payload.page, 1) const size = normalizePageSize(payload.size) diff --git a/apps/backend/src/services/platforms/cloudtentacles/session-service.ts b/apps/backend/src/services/platforms/cloudtentacles/session-service.ts index 5eb76807..cc0692bc 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/session-service.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/session-service.ts @@ -24,10 +24,13 @@ export async function sendCloudtentaclesSmsCode(payload: JsonObject = {}) { } const config = resolveCloudtentaclesConfig(payload) - const encrypted = encryptCloudtentaclesPayload({ - account: username, - phone, - }, config) + const encrypted = encryptCloudtentaclesPayload( + { + account: username, + phone, + }, + config, + ) const result = await cloudtentaclesRequest(config.sendSmsPath, { ...config, @@ -90,12 +93,15 @@ export async function loginCloudtentaclesSession(payload: JsonObject = {}) { } const config = resolveCloudtentaclesConfig(payload) - const encrypted = encryptCloudtentaclesPayload({ - account: username, - password: md5CloudtentaclesPassword(password), - phone, - code, - }, config) + const encrypted = encryptCloudtentaclesPayload( + { + account: username, + password: md5CloudtentaclesPassword(password), + phone, + code, + }, + config, + ) const loginResult = await cloudtentaclesRequest(config.loginPath, { ...config, @@ -175,7 +181,9 @@ export async function validateCloudtentaclesSession(payload: JsonObject = {}) { userInfo: isPlainObject(userInfoResult.payload?.data) ? userInfoResult.payload.data : {}, asset: Number(assetResult.payload?.data || 0), permissions: Array.isArray(permissionResult.payload?.data) - ? permissionResult.payload.data.map((item: unknown) => String(item || '').trim()).filter(Boolean) + ? permissionResult.payload.data + .map((item: unknown) => String(item || '').trim()) + .filter(Boolean) : [], } } diff --git a/apps/backend/src/services/platforms/cloudtentacles/session-state-service.ts b/apps/backend/src/services/platforms/cloudtentacles/session-state-service.ts index 1b724218..a65870a0 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/session-state-service.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/session-state-service.ts @@ -4,10 +4,7 @@ import type { JsonObject } from '../../../types/json.js' import { APP_CONFIG_KEYS } from '../../../config/app-config-keys.js' import { PROJECT_ROOT } from '../../../config/runtime.js' import { createHttpError } from '../../../utils/http.js' -import { - readAppConfigEntry, - saveAppConfigEntry, -} from '../../config/app-config-store.js' +import { readAppConfigEntry, saveAppConfigEntry } from '../../config/app-config-store.js' import { DEFAULT_CLOUDTENTACLES_DEVICE_ID, DEFAULT_CLOUDTENTACLES_DEVICE_TYPE, @@ -15,7 +12,11 @@ import { normalizeCloudtentaclesDeviceType, } from './defaults.js' -const CLOUDTENTACLES_SESSION_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'cloudtentacles-session.json') +const CLOUDTENTACLES_SESSION_FILE_PATH = path.join( + PROJECT_ROOT, + 'data', + 'cloudtentacles-session.json', +) type CloudtentaclesSessionState = ReturnType type CloudtentaclesSessionStatesFile = { @@ -115,12 +116,14 @@ export async function pruneCloudtentaclesSessionStates(sourceKeys: unknown[] = [ const allowedKeys = new Set( Array.isArray(sourceKeys) ? sourceKeys.map((value) => String(value || '').trim()).filter(Boolean) - : [] + : [], ) const states = loadCloudtentaclesSessionStates() states.sessions = Object.fromEntries( - Object.entries(states.sessions || {}).filter(([key]) => allowedKeys.has(String(key || '').trim())) + Object.entries(states.sessions || {}).filter(([key]) => + allowedKeys.has(String(key || '').trim()), + ), ) await saveCloudtentaclesSessionState(states) @@ -156,17 +159,21 @@ export function normalizeSessionStatesFile(rawValue: unknown): CloudtentaclesSes if (isPlainObject(rawValue) && !rawValue.sessions) { return { sessions: { - 'default': normalizeCloudtentaclesSessionState(rawValue), + default: normalizeCloudtentaclesSessionState(rawValue), }, } } return { - sessions: isPlainObject(rawValue) && isPlainObject(rawValue.sessions) - ? Object.fromEntries( - Object.entries(rawValue.sessions).map(([k, v]) => [k, normalizeCloudtentaclesSessionState(v)]) - ) - : {}, + sessions: + isPlainObject(rawValue) && isPlainObject(rawValue.sessions) + ? Object.fromEntries( + Object.entries(rawValue.sessions).map(([k, v]) => [ + k, + normalizeCloudtentaclesSessionState(v), + ]), + ) + : {}, } } @@ -185,7 +192,7 @@ function createDefaultCloudtentaclesSessionState() { function createDefaultCloudtentaclesSessionStates(): CloudtentaclesSessionStatesFile { return { sessions: { - 'default': createDefaultCloudtentaclesSessionState(), + default: createDefaultCloudtentaclesSessionState(), }, } } diff --git a/apps/backend/src/services/platforms/cloudtentacles/source-config-service.ts b/apps/backend/src/services/platforms/cloudtentacles/source-config-service.ts index 4fe79f50..84c33ae9 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/source-config-service.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/source-config-service.ts @@ -4,16 +4,14 @@ import type { JsonObject } from '../../../types/json.js' import { APP_CONFIG_KEYS } from '../../../config/app-config-keys.js' import { PROJECT_ROOT } from '../../../config/runtime.js' import { createHttpError } from '../../../utils/http.js' -import { - readAppConfigEntry, - saveAppConfigEntry, -} from '../../config/app-config-store.js' -import { - normalizeCloudtentaclesDeviceId, - normalizeCloudtentaclesDeviceType, -} from './defaults.js' +import { readAppConfigEntry, saveAppConfigEntry } from '../../config/app-config-store.js' +import { normalizeCloudtentaclesDeviceId, normalizeCloudtentaclesDeviceType } from './defaults.js' -const CLOUDTENTACLES_SOURCES_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'cloudtentacles-sources.json') +const CLOUDTENTACLES_SOURCES_FILE_PATH = path.join( + PROJECT_ROOT, + 'data', + 'cloudtentacles-sources.json', +) export function getCloudtentaclesSourcesFilePath() { return CLOUDTENTACLES_SOURCES_FILE_PATH @@ -22,7 +20,7 @@ export function getCloudtentaclesSourcesFilePath() { // 兼容旧调用:返回 key='default' 的单账号配置。 export function getCloudtentaclesSourceConfig() { const config = loadCloudtentaclesSourcesConfig() - const defaultSource = config.sources.find(s => s.key === 'default') + const defaultSource = config.sources.find((s) => s.key === 'default') return defaultSource || normalizeCloudtentaclesSourceItem({ key: 'default' }) } @@ -31,7 +29,7 @@ export function getCloudtentaclesSourceByKey(sourceKey: unknown) { const config = loadCloudtentaclesSourcesConfig() const key = String(sourceKey || '').trim() if (!key) return null - return config.sources.find(s => s.key === key) || null + return config.sources.find((s) => s.key === key) || null } // 返回完整的多账号配置。 @@ -67,7 +65,7 @@ export async function saveCloudtentaclesSourceByKey(sourceKey: unknown, data: Js const config = loadCloudtentaclesSourcesConfig() const normalizedItem = normalizeCloudtentaclesSourceItem({ ...data, key }) - const existingIndex = config.sources.findIndex(s => s.key === key) + const existingIndex = config.sources.findIndex((s) => s.key === key) if (existingIndex >= 0) { config.sources[existingIndex] = normalizedItem @@ -90,7 +88,7 @@ export async function deleteCloudtentaclesSourceByKey(sourceKey: unknown) { } const config = loadCloudtentaclesSourcesConfig() - const existingIndex = config.sources.findIndex(s => s.key === key) + const existingIndex = config.sources.findIndex((s) => s.key === key) if (existingIndex < 0) { return config @@ -119,7 +117,8 @@ function normalizeCloudtentaclesSourceItem(rawValue: unknown) { key: String(source.key || 'default').trim() || 'default', label: String(source.label || '').trim(), enabled: source.enabled !== false, - baseUrl: String(source.baseUrl || 'https://123.207.217.176').trim() || 'https://123.207.217.176', + baseUrl: + String(source.baseUrl || 'https://123.207.217.176').trim() || 'https://123.207.217.176', username: String(source.username || '').trim(), password: String(source.password || '').trim(), phone: String(source.phone || '').trim(), @@ -137,7 +136,7 @@ export function normalizeCloudtentaclesSourcesConfig(rawValue: unknown) { normalizeCloudtentaclesSourceItem({ key: 'default', label: '默认账号', - ...rawValue, // 旧字段自动映射为 default 账号。 + ...rawValue, // 旧字段自动映射为 default 账号。 }), ].filter(Boolean), } @@ -145,18 +144,17 @@ export function normalizeCloudtentaclesSourcesConfig(rawValue: unknown) { return { enabled: isPlainObject(rawValue) ? rawValue.enabled !== false : true, - sources: isPlainObject(rawValue) && Array.isArray(rawValue.sources) - ? rawValue.sources.map(s => normalizeCloudtentaclesSourceItem(s)).filter(Boolean) - : [], + sources: + isPlainObject(rawValue) && Array.isArray(rawValue.sources) + ? rawValue.sources.map((s) => normalizeCloudtentaclesSourceItem(s)).filter(Boolean) + : [], } } function createDefaultCloudtentaclesSourcesConfig() { return { enabled: true, - sources: [ - normalizeCloudtentaclesSourceItem({ key: 'default', label: '默认账号' }), - ], + sources: [normalizeCloudtentaclesSourceItem({ key: 'default', label: '默认账号' })], } } diff --git a/apps/backend/src/services/platforms/cloudtentacles/virtual-number-service.ts b/apps/backend/src/services/platforms/cloudtentacles/virtual-number-service.ts index 179aad09..c60a72e9 100644 --- a/apps/backend/src/services/platforms/cloudtentacles/virtual-number-service.ts +++ b/apps/backend/src/services/platforms/cloudtentacles/virtual-number-service.ts @@ -41,8 +41,16 @@ type AmsSignatureParams = { } export async function listCloudtentaclesVirtualNumbers(payload: JsonObject = {}) { - const token = requireToken(payload.token, 'cloudtentacles 虚拟号列表缺少 token', 'cloudtentacles_vn_list_missing_token') - const key = requireKey(payload.key, 'cloudtentacles 虚拟号列表缺少 key', 'cloudtentacles_vn_list_missing_key') + const token = requireToken( + payload.token, + 'cloudtentacles 虚拟号列表缺少 token', + 'cloudtentacles_vn_list_missing_token', + ) + const key = requireKey( + payload.key, + 'cloudtentacles 虚拟号列表缺少 key', + 'cloudtentacles_vn_list_missing_key', + ) const config = resolveCloudtentaclesConfig(payload) const result = await cloudtentaclesRequest(config.vnListPath, { @@ -52,13 +60,21 @@ export async function listCloudtentaclesVirtualNumbers(payload: JsonObject = {}) body: { key }, sourceKey: payload.sourceKey || payload.resolvedSourceKey, accountLabel: payload.accountLabel, - context: { operation: 'vn_list', vnKey: key, sourceKey: payload.sourceKey || payload.resolvedSourceKey || '' }, + context: { + operation: 'vn_list', + vnKey: key, + sourceKey: payload.sourceKey || payload.resolvedSourceKey || '', + }, businessErrorStatusCode: 401, businessErrorCode: 'cloudtentacles_vn_list_failed', }) - const items = Array.isArray(result.payload?.data) ? result.payload.data.map(mapVirtualNumberItem) : [] - const occupiedItemCount = items.filter((item: ReturnType) => item.status !== 0).length + const items = Array.isArray(result.payload?.data) + ? result.payload.data.map(mapVirtualNumberItem) + : [] + const occupiedItemCount = items.filter( + (item: ReturnType) => item.status !== 0, + ).length return { baseUrl: config.baseUrl, @@ -71,8 +87,16 @@ export async function listCloudtentaclesVirtualNumbers(payload: JsonObject = {}) } export async function appointCloudtentaclesVirtualNumber(payload: JsonObject = {}) { - const token = requireToken(payload.token, 'cloudtentacles 申请虚拟号缺少 token', 'cloudtentacles_vn_appoint_missing_token') - const key = requireKey(payload.key, 'cloudtentacles 申请虚拟号缺少 key', 'cloudtentacles_vn_appoint_missing_key') + const token = requireToken( + payload.token, + 'cloudtentacles 申请虚拟号缺少 token', + 'cloudtentacles_vn_appoint_missing_token', + ) + const key = requireKey( + payload.key, + 'cloudtentacles 申请虚拟号缺少 key', + 'cloudtentacles_vn_appoint_missing_key', + ) const config = resolveCloudtentaclesConfig(payload) const result = await cloudtentaclesRequest(config.vnAppointPath, { @@ -82,7 +106,11 @@ export async function appointCloudtentaclesVirtualNumber(payload: JsonObject = { body: { key }, sourceKey: payload.sourceKey || payload.resolvedSourceKey, accountLabel: payload.accountLabel, - context: { operation: 'vn_appoint', vnKey: key, sourceKey: payload.sourceKey || payload.resolvedSourceKey || '' }, + context: { + operation: 'vn_appoint', + vnKey: key, + sourceKey: payload.sourceKey || payload.resolvedSourceKey || '', + }, businessErrorStatusCode: 401, businessErrorCode: 'cloudtentacles_vn_appoint_failed', }) @@ -96,9 +124,21 @@ export async function appointCloudtentaclesVirtualNumber(payload: JsonObject = { } export async function generateCloudtentaclesLoginCode(payload: JsonObject = {}) { - const token = requireToken(payload.token, 'cloudtentacles 生成登录码缺少 token', 'cloudtentacles_vn_generate_code_missing_token') - const key = requireKey(payload.key, 'cloudtentacles 生成登录码缺少 key', 'cloudtentacles_vn_generate_code_missing_key') - const id = requireId(payload.id, 'cloudtentacles 生成登录码缺少 id', 'cloudtentacles_vn_generate_code_missing_id') + const token = requireToken( + payload.token, + 'cloudtentacles 生成登录码缺少 token', + 'cloudtentacles_vn_generate_code_missing_token', + ) + const key = requireKey( + payload.key, + 'cloudtentacles 生成登录码缺少 key', + 'cloudtentacles_vn_generate_code_missing_key', + ) + const id = requireId( + payload.id, + 'cloudtentacles 生成登录码缺少 id', + 'cloudtentacles_vn_generate_code_missing_id', + ) const config = resolveCloudtentaclesConfig(payload) const result = await cloudtentaclesRequest(config.vnGenerateLoginCodePath, { @@ -108,7 +148,12 @@ export async function generateCloudtentaclesLoginCode(payload: JsonObject = {}) body: { key, id }, sourceKey: payload.sourceKey || payload.resolvedSourceKey, accountLabel: payload.accountLabel, - context: { operation: 'vn_generate_code', vnKey: key, vnId: id, sourceKey: payload.sourceKey || payload.resolvedSourceKey || '' }, + context: { + operation: 'vn_generate_code', + vnKey: key, + vnId: id, + sourceKey: payload.sourceKey || payload.resolvedSourceKey || '', + }, businessErrorStatusCode: 401, businessErrorCode: 'cloudtentacles_vn_generate_code_failed', }) @@ -122,8 +167,16 @@ export async function generateCloudtentaclesLoginCode(payload: JsonObject = {}) } export async function fetchCloudtentaclesVirtualNumberCode(payload: JsonObject = {}) { - const token = requireToken(payload.token, 'cloudtentacles 获取验证码缺少 token', 'cloudtentacles_vn_verif_code_missing_token') - const key = requireKey(payload.key, 'cloudtentacles 获取验证码缺少 key', 'cloudtentacles_vn_verif_code_missing_key') + const token = requireToken( + payload.token, + 'cloudtentacles 获取验证码缺少 token', + 'cloudtentacles_vn_verif_code_missing_token', + ) + const key = requireKey( + payload.key, + 'cloudtentacles 获取验证码缺少 key', + 'cloudtentacles_vn_verif_code_missing_key', + ) const phone = String(payload.phone || '').trim() if (!phone) { throw createHttpError('cloudtentacles 获取验证码缺少手机号', { @@ -140,7 +193,12 @@ export async function fetchCloudtentaclesVirtualNumberCode(payload: JsonObject = body: { key, phone }, sourceKey: payload.sourceKey || payload.resolvedSourceKey, accountLabel: payload.accountLabel, - context: { operation: 'vn_fetch_code', vnKey: key, phoneMasked: maskPhone(phone), sourceKey: payload.sourceKey || payload.resolvedSourceKey || '' }, + context: { + operation: 'vn_fetch_code', + vnKey: key, + phoneMasked: maskPhone(phone), + sourceKey: payload.sourceKey || payload.resolvedSourceKey || '', + }, businessErrorStatusCode: 401, businessErrorCode: 'cloudtentacles_vn_verif_code_failed', }) @@ -155,9 +213,21 @@ export async function fetchCloudtentaclesVirtualNumberCode(payload: JsonObject = } export async function verifyCloudtentaclesLoginCode(payload: JsonObject = {}) { - const token = requireToken(payload.token, 'cloudtentacles 校验登录码缺少 token', 'cloudtentacles_vn_verify_code_missing_token') - const key = requireKey(payload.key, 'cloudtentacles 校验登录码缺少 key', 'cloudtentacles_vn_verify_code_missing_key') - const id = requireId(payload.id, 'cloudtentacles 校验登录码缺少 id', 'cloudtentacles_vn_verify_code_missing_id') + const token = requireToken( + payload.token, + 'cloudtentacles 校验登录码缺少 token', + 'cloudtentacles_vn_verify_code_missing_token', + ) + const key = requireKey( + payload.key, + 'cloudtentacles 校验登录码缺少 key', + 'cloudtentacles_vn_verify_code_missing_key', + ) + const id = requireId( + payload.id, + 'cloudtentacles 校验登录码缺少 id', + 'cloudtentacles_vn_verify_code_missing_id', + ) const code = String(payload.code || '').trim() if (!code) { throw createHttpError('cloudtentacles 校验登录码缺少验证码', { @@ -174,7 +244,12 @@ export async function verifyCloudtentaclesLoginCode(payload: JsonObject = {}) { body: { key, id, code }, sourceKey: payload.sourceKey || payload.resolvedSourceKey, accountLabel: payload.accountLabel, - context: { operation: 'vn_verify_code', vnKey: key, vnId: id, sourceKey: payload.sourceKey || payload.resolvedSourceKey || '' }, + context: { + operation: 'vn_verify_code', + vnKey: key, + vnId: id, + sourceKey: payload.sourceKey || payload.resolvedSourceKey || '', + }, businessErrorStatusCode: 401, businessErrorCode: 'cloudtentacles_vn_verify_code_failed', }) @@ -189,9 +264,21 @@ export async function verifyCloudtentaclesLoginCode(payload: JsonObject = {}) { } export async function getCloudtentaclesBindUrl(payload: JsonObject = {}) { - const token = requireToken(payload.token, 'cloudtentacles 获取兑换链接缺少 token', 'cloudtentacles_vn_bind_url_missing_token') - const key = requireKey(payload.key, 'cloudtentacles 获取兑换链接缺少 key', 'cloudtentacles_vn_bind_url_missing_key') - const id = requireId(payload.id, 'cloudtentacles 获取兑换链接缺少 id', 'cloudtentacles_vn_bind_url_missing_id') + const token = requireToken( + payload.token, + 'cloudtentacles 获取兑换链接缺少 token', + 'cloudtentacles_vn_bind_url_missing_token', + ) + const key = requireKey( + payload.key, + 'cloudtentacles 获取兑换链接缺少 key', + 'cloudtentacles_vn_bind_url_missing_key', + ) + const id = requireId( + payload.id, + 'cloudtentacles 获取兑换链接缺少 id', + 'cloudtentacles_vn_bind_url_missing_id', + ) const config = resolveCloudtentaclesConfig(payload) const result = await cloudtentaclesRequest(config.vnBindUrlPath, { @@ -247,7 +334,9 @@ export async function probeCloudtentaclesBindUrl(payload: JsonObject = {}) { const timeoutMs = Number(payload.timeoutMs || config.bindUrlProbeTimeoutMs || 5000) const userAgent = String(payload.userAgent || config.bindUrlProbeUserAgent || '').trim() - const endpoint = String(payload.endpoint || config.bindUrlProbeEndpoint || 'https://comm.ams.game.qq.com/ide/').trim() + const endpoint = String( + payload.endpoint || config.bindUrlProbeEndpoint || 'https://comm.ams.game.qq.com/ide/', + ).trim() const startedAt = Date.now() try { @@ -262,10 +351,7 @@ export async function probeCloudtentaclesBindUrl(payload: JsonObject = {}) { const expired = isAmsSignatureExpired(raw) const bindInfo = extractAmsBindInfo(raw) const roleInfo = normalizeAmsBindRoleInfo(bindInfo) - const ok = response.status >= 200 - && response.status < 300 - && !expired - && isAmsBusinessOk(raw) + const ok = response.status >= 200 && response.status < 300 && !expired && isAmsBusinessOk(raw) return { valid: ok, @@ -275,7 +361,7 @@ export async function probeCloudtentaclesBindUrl(payload: JsonObject = {}) { ret: String(raw?.ret ?? raw?.iRet ?? ''), iRet: String(raw?.iRet ?? raw?.jData?.iRet ?? ''), message: String(raw?.sMsg || raw?.jData?.sMsg || ''), - reason: expired ? 'signature_expired' : (ok ? 'ok' : 'ams_business_not_ok'), + reason: expired ? 'signature_expired' : ok ? 'ok' : 'ams_business_not_ok', roleInfo, raw, durationMs: Date.now() - startedAt, @@ -295,9 +381,21 @@ export async function probeCloudtentaclesBindUrl(payload: JsonObject = {}) { } export async function getCloudtentaclesBindInfo(payload: JsonObject = {}) { - const token = requireToken(payload.token, 'cloudtentacles 获取绑定信息缺少 token', 'cloudtentacles_vn_bind_info_missing_token') - const key = requireKey(payload.key, 'cloudtentacles 获取绑定信息缺少 key', 'cloudtentacles_vn_bind_info_missing_key') - const id = requireId(payload.id, 'cloudtentacles 获取绑定信息缺少 id', 'cloudtentacles_vn_bind_info_missing_id') + const token = requireToken( + payload.token, + 'cloudtentacles 获取绑定信息缺少 token', + 'cloudtentacles_vn_bind_info_missing_token', + ) + const key = requireKey( + payload.key, + 'cloudtentacles 获取绑定信息缺少 key', + 'cloudtentacles_vn_bind_info_missing_key', + ) + const id = requireId( + payload.id, + 'cloudtentacles 获取绑定信息缺少 id', + 'cloudtentacles_vn_bind_info_missing_id', + ) const config = resolveCloudtentaclesConfig(payload) const sourceKey = String(payload.sourceKey || '').trim() const cacheKey = `${sourceKey || token}|${id}|${key}` @@ -325,11 +423,17 @@ export async function getCloudtentaclesBindInfo(payload: JsonObject = {}) { rateLimitIsolatePath: true, sourceKey: payload.sourceKey || payload.resolvedSourceKey, accountLabel: payload.accountLabel, - context: { operation: 'vn_bind_info', vnKey: key, vnId: id, sourceKey: payload.sourceKey || payload.resolvedSourceKey || '' }, + context: { + operation: 'vn_bind_info', + vnKey: key, + vnId: id, + sourceKey: payload.sourceKey || payload.resolvedSourceKey || '', + }, }) const items = Array.isArray(result.payload?.data) ? result.payload.data : [] - const matchedItem = items.find((item: JsonObject) => Number(item?.id || 0) === id) || items[0] || {} + const matchedItem = + items.find((item: JsonObject) => Number(item?.id || 0) === id) || items[0] || {} const bindInfo = parseBindInfo(matchedItem?.bind_info) const value: BindInfoResult = { @@ -359,9 +463,21 @@ export async function getCloudtentaclesBindInfo(payload: JsonObject = {}) { } export async function backCloudtentaclesVirtualNumber(payload: JsonObject = {}) { - const token = requireToken(payload.token, 'cloudtentacles 退还号码缺少 token', 'cloudtentacles_vn_back_missing_token') - const key = requireKey(payload.key, 'cloudtentacles 退还号码缺少 key', 'cloudtentacles_vn_back_missing_key') - const id = requireId(payload.id, 'cloudtentacles 退还号码缺少 id', 'cloudtentacles_vn_back_missing_id') + const token = requireToken( + payload.token, + 'cloudtentacles 退还号码缺少 token', + 'cloudtentacles_vn_back_missing_token', + ) + const key = requireKey( + payload.key, + 'cloudtentacles 退还号码缺少 key', + 'cloudtentacles_vn_back_missing_key', + ) + const id = requireId( + payload.id, + 'cloudtentacles 退还号码缺少 id', + 'cloudtentacles_vn_back_missing_id', + ) const config = resolveCloudtentaclesConfig(payload) const result = await cloudtentaclesRequest(config.vnBackPath, { @@ -454,7 +570,10 @@ function parseBindInfo(value: unknown) { return tryParseJson(bindInfoText) || bindInfoText || null } -async function requestAmsIdeProbe(endpoint: string, options: JsonObject = {}): Promise { +async function requestAmsIdeProbe( + endpoint: string, + options: JsonObject = {}, +): Promise { const url = new URL(endpoint) if (!['http:', 'https:'].includes(url.protocol)) { throw new Error('unsupported_ams_probe_protocol') @@ -463,41 +582,46 @@ async function requestAmsIdeProbe(endpoint: string, options: JsonObject = {}): P const timeoutMs = Number(options.timeoutMs || 5000) const isHttps = url.protocol === 'https:' const transport = isHttps ? https : http - const body = options.body instanceof URLSearchParams ? options.body.toString() : String(options.body || '') + const body = + options.body instanceof URLSearchParams ? options.body.toString() : String(options.body || '') return new Promise((resolve, reject) => { - const request = transport.request(url, { - method: 'POST', - rejectUnauthorized: false, - headers: { - ...normalizeHeaderMap(options.headers), - 'content-length': Buffer.byteLength(body), - 'accept-encoding': 'identity', + const request = transport.request( + url, + { + method: 'POST', + rejectUnauthorized: false, + headers: { + ...normalizeHeaderMap(options.headers), + 'content-length': Buffer.byteLength(body), + 'accept-encoding': 'identity', + }, }, - }, (response) => { - const chunks: Buffer[] = [] - let receivedBytes = 0 + (response) => { + const chunks: Buffer[] = [] + let receivedBytes = 0 - response.on('data', (chunk) => { - if (receivedBytes >= BIND_URL_PROBE_MAX_BODY_BYTES) { - return - } + response.on('data', (chunk) => { + if (receivedBytes >= BIND_URL_PROBE_MAX_BODY_BYTES) { + return + } - const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk) - const remainingBytes = BIND_URL_PROBE_MAX_BODY_BYTES - receivedBytes - chunks.push(buffer.length > remainingBytes ? buffer.subarray(0, remainingBytes) : buffer) - receivedBytes += Math.min(buffer.length, remainingBytes) - }) - - response.on('end', () => { - clearTimeout(timer) - resolve({ - status: Number(response.statusCode || 0), - headers: createHeaderAdapter(response.headers), - bodyText: Buffer.concat(chunks).toString('utf8'), + const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk) + const remainingBytes = BIND_URL_PROBE_MAX_BODY_BYTES - receivedBytes + chunks.push(buffer.length > remainingBytes ? buffer.subarray(0, remainingBytes) : buffer) + receivedBytes += Math.min(buffer.length, remainingBytes) }) - }) - }) + + response.on('end', () => { + clearTimeout(timer) + resolve({ + status: Number(response.statusCode || 0), + headers: createHeaderAdapter(response.headers), + bodyText: Buffer.concat(chunks).toString('utf8'), + }) + }) + }, + ) const timer = setTimeout(() => { request.destroy(new Error('bind_url_probe_timeout')) @@ -534,7 +658,11 @@ function createHeaderAdapter(headers: http.IncomingHttpHeaders): HeaderAdapter { } } -function buildAmsIdeProbeBody(signatureParams: AmsSignatureParams, config: JsonObject, payload: JsonObject = {}) { +function buildAmsIdeProbeBody( + signatureParams: AmsSignatureParams, + config: JsonObject, + payload: JsonObject = {}, +) { const body = new URLSearchParams() const chartId = String(payload.chartId || config.bindUrlProbeChartId || '323794').trim() const subChartId = String(payload.subChartId || config.bindUrlProbeSubChartId || chartId).trim() @@ -544,8 +672,20 @@ function buildAmsIdeProbeBody(signatureParams: AmsSignatureParams, config: JsonO body.set('sIdeToken', String(payload.ideToken || config.bindUrlProbeIdeToken || 'z90Syo').trim()) body.set('e_code', '0') body.set('g_code', '0') - body.set('eas_url', String(payload.easUrl || config.bindUrlProbeActivityUrl || 'http%3A%2F%2Fgp.qq.com%2Fcp%2Fa20240828cmcc%2F').trim()) - body.set('eas_refer', String(payload.easRefer || `http%3A%2F%2Fnoreferrer%2F%3Freqid%3D${Date.now()}%26version%3D27`).trim()) + body.set( + 'eas_url', + String( + payload.easUrl || + config.bindUrlProbeActivityUrl || + 'http%3A%2F%2Fgp.qq.com%2Fcp%2Fa20240828cmcc%2F', + ).trim(), + ) + body.set( + 'eas_refer', + String( + payload.easRefer || `http%3A%2F%2Fnoreferrer%2F%3Freqid%3D${Date.now()}%26version%3D27`, + ).trim(), + ) body.set('sMiloTag', String(payload.miloTag || `AMS-gp-${Date.now()}`).trim()) body.set('userId', signatureParams.userId) body.set('timestamp', signatureParams.timestamp) @@ -608,7 +748,9 @@ function extractBindUrlSignatureParams(bindUrl: string): AmsSignatureParams | nu } const tokenParams = String(params.get('tokenParams') || '').trim() if (tokenParams) { - const nested = new URLSearchParams(tokenParams.startsWith('?') ? tokenParams.slice(1) : tokenParams) + const nested = new URLSearchParams( + tokenParams.startsWith('?') ? tokenParams.slice(1) : tokenParams, + ) for (const [key, value] of nested.entries()) { if (!params.has(key)) { params.set(key, value) @@ -653,21 +795,18 @@ function isAmsSignatureExpired(raw: any) { raw?.jData?.arrErrNodeInfo?.errorCode, ].map((value) => String(value ?? '').trim()) - const messages = [ - raw?.sMsg, - raw?.jData?.sMsg, - raw?.jData?.jData?.sMsg, - ].map((value) => String(value ?? '').trim()) + const messages = [raw?.sMsg, raw?.jData?.sMsg, raw?.jData?.jData?.sMsg].map((value) => + String(value ?? '').trim(), + ) - return codes.includes(AMS_SIGNATURE_EXPIRED_CODE) || messages.some((message) => message.includes('签名已过期')) + return ( + codes.includes(AMS_SIGNATURE_EXPIRED_CODE) || + messages.some((message) => message.includes('签名已过期')) + ) } function extractAmsBindInfo(raw: any) { - const candidates = [ - raw?.jData?.sBindInfo, - raw?.jData?.jData?.sBindInfo, - raw?.sBindInfo, - ] + const candidates = [raw?.jData?.sBindInfo, raw?.jData?.jData?.sBindInfo, raw?.sBindInfo] return candidates.find((item) => isPlainObject(item)) || null } @@ -689,7 +828,12 @@ function normalizeHeaderMap(headers: unknown) { return Object.fromEntries( Object.entries(headers) - .map(([key, value]) => [String(key || '').trim().toLowerCase(), String(value || '').trim()]) + .map(([key, value]) => [ + String(key || '') + .trim() + .toLowerCase(), + String(value || '').trim(), + ]) .filter(([key, value]) => key && value), ) } diff --git a/apps/backend/src/services/platforms/kuaishou-feifei/config.ts b/apps/backend/src/services/platforms/kuaishou-feifei/config.ts index 0f91dbca..d7f02bd0 100644 --- a/apps/backend/src/services/platforms/kuaishou-feifei/config.ts +++ b/apps/backend/src/services/platforms/kuaishou-feifei/config.ts @@ -13,14 +13,14 @@ type KuaishouFeifeiRuntimeConfig = RuntimeConfig['platforms']['kuaishouFeifei'] export function getKuaishouFeifeiConfig(overrides: Partial = {}) { const runtimeValue = runtimeConfig.platforms?.kuaishouFeifei || {} - const savedValue = hasKuaishouFeifeiConfigFile() - ? getKuaishouFeifeiSourceConfig() - : null + const savedValue = hasKuaishouFeifeiConfigFile() ? getKuaishouFeifeiSourceConfig() : null const config = mergeKuaishouFeifeiConfig(runtimeValue, savedValue, overrides) return { enabled: config.enabled !== false, - baseUrl: String(config.baseUrl || 'http://skin-exchange.yiquyou.icu').trim().replace(/\/+$/, ''), + baseUrl: String(config.baseUrl || 'http://skin-exchange.yiquyou.icu') + .trim() + .replace(/\/+$/, ''), appKey: String(config.appKey || '').trim(), appSecret: String(config.appSecret || '').trim(), timeoutMs: Math.max(1, Number(config.timeoutMs || 10000) || 10000), diff --git a/apps/backend/src/services/platforms/kuaishou-feifei/http-client.ts b/apps/backend/src/services/platforms/kuaishou-feifei/http-client.ts index 23fbcd29..c42d0899 100644 --- a/apps/backend/src/services/platforms/kuaishou-feifei/http-client.ts +++ b/apps/backend/src/services/platforms/kuaishou-feifei/http-client.ts @@ -1,5 +1,5 @@ import crypto from 'node:crypto' -import { asJsonObject, type JsonObject } from '../../../types/json.js' +import { asJsonObject, type JsonObject } from '../../../types/json.js' import { createHttpError } from '../../../utils/http.js' import { createRequestId, logExternalHttpPacket } from '../../../utils/logger.js' diff --git a/apps/backend/src/services/platforms/kuaishou-feifei/notify-service.ts b/apps/backend/src/services/platforms/kuaishou-feifei/notify-service.ts index 6af16b26..d878c814 100644 --- a/apps/backend/src/services/platforms/kuaishou-feifei/notify-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-feifei/notify-service.ts @@ -44,13 +44,15 @@ export async function handleKuaishouFeifeiNotify(input: { }) } - if (!verifyKuaishouFeifeiNotifySignature({ - appKey, - appSecret: config.appSecret, - timestamp, - rawBody, - sign, - })) { + if ( + !verifyKuaishouFeifeiNotifySignature({ + appKey, + appSecret: config.appSecret, + timestamp, + rawBody, + sign, + }) + ) { throw createHttpError('kuaishou-feifei 通知验签失败', { statusCode: 401, errorCode: 'kuaishou_feifei_notify_sign_invalid', @@ -129,7 +131,12 @@ export function verifyKuaishouFeifeiNotifySignature(input: { body: input.rawBody, }) - return timingSafeEqualString(expected, String(input.sign || '').trim().toLowerCase()) + return timingSafeEqualString( + expected, + String(input.sign || '') + .trim() + .toLowerCase(), + ) } function normalizeHeader(value: string | string[] | undefined) { diff --git a/apps/backend/src/services/platforms/kuaishou-feifei/order-service.ts b/apps/backend/src/services/platforms/kuaishou-feifei/order-service.ts index afc93cf7..ed5dd8a4 100644 --- a/apps/backend/src/services/platforms/kuaishou-feifei/order-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-feifei/order-service.ts @@ -4,12 +4,14 @@ import type { JsonObject } from '../../../types/json.js' export type KuaishouFeifeiProductListResult = ReturnType -export async function listKuaishouFeifeiProducts(input: { - page?: number - perPage?: number - status?: string - supplyProductName?: string -} = {}) { +export async function listKuaishouFeifeiProducts( + input: { + page?: number + perPage?: number + status?: string + supplyProductName?: string + } = {}, +) { const page = Math.max(1, Number(input.page || 1) || 1) const perPage = Math.min(100, Math.max(1, Number(input.perPage || 20) || 20)) const filters: JsonObject = {} @@ -72,10 +74,9 @@ export async function queryKuaishouFeifeiOrder(input: { } export function mapKuaishouFeifeiOrder(value: unknown) { - const source = value && typeof value === 'object' && !Array.isArray(value) - ? value as JsonObject - : {} - const h5 = source.h5 && typeof source.h5 === 'object' ? source.h5 as JsonObject : {} + const source = + value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {} + const h5 = source.h5 && typeof source.h5 === 'object' ? (source.h5 as JsonObject) : {} return { orderNo: String(source.order_no || '').trim(), @@ -100,9 +101,8 @@ export function mapKuaishouFeifeiOrder(value: unknown) { } export function mapKuaishouFeifeiProductList(value: unknown) { - const source = value && typeof value === 'object' && !Array.isArray(value) - ? value as JsonObject - : {} + const source = + value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {} const list = Array.isArray(source.list) ? source.list : [] return { @@ -115,9 +115,8 @@ export function mapKuaishouFeifeiProductList(value: unknown) { } export function mapKuaishouFeifeiProduct(value: unknown) { - const source = value && typeof value === 'object' && !Array.isArray(value) - ? value as JsonObject - : {} + const source = + value && typeof value === 'object' && !Array.isArray(value) ? (value as JsonObject) : {} return { productCode: String(source.product_code || '').trim(), @@ -131,7 +130,8 @@ export function mapKuaishouFeifeiProduct(value: unknown) { feePoints: Number(source.fee_points || 0) || 0, unitCostPoints: Number(source.unit_cost_points || 0) || 0, maxOrderQuantity: Number(source.max_order_quantity || 0) || 0, - salePricePoints: source.sale_price_points == null ? null : Number(source.sale_price_points || 0) || 0, + salePricePoints: + source.sale_price_points == null ? null : Number(source.sale_price_points || 0) || 0, raw: source, } } diff --git a/apps/backend/src/services/platforms/kuaishou-feifei/product-rule-service.ts b/apps/backend/src/services/platforms/kuaishou-feifei/product-rule-service.ts index 26737627..6f1163da 100644 --- a/apps/backend/src/services/platforms/kuaishou-feifei/product-rule-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-feifei/product-rule-service.ts @@ -23,8 +23,10 @@ export function resolveKuaishouFeifeiProductByName( } const rules = listKuaishouFeifeiProductRules(options) - const matched = rules.find((rule) => - normalizeCloudtentaclesMatchName(rule.productName || rule.normalizedProductName) === normalizedProductName, + const matched = rules.find( + (rule) => + normalizeCloudtentaclesMatchName(rule.productName || rule.normalizedProductName) === + normalizedProductName, ) if (!matched) { diff --git a/apps/backend/src/services/platforms/kuaishou-feifei/source-config-service.ts b/apps/backend/src/services/platforms/kuaishou-feifei/source-config-service.ts index 35b9231b..99227903 100644 --- a/apps/backend/src/services/platforms/kuaishou-feifei/source-config-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-feifei/source-config-service.ts @@ -32,10 +32,7 @@ export function getKuaishouFeifeiConfigFilePath() { } export function hasKuaishouFeifeiConfigFile() { - return hasAppConfigEntry( - APP_CONFIG_KEYS.kuaishouFeifei, - KUAISHOU_FEIFEI_CONFIG_FILE_PATH, - ) + return hasAppConfigEntry(APP_CONFIG_KEYS.kuaishouFeifei, KUAISHOU_FEIFEI_CONFIG_FILE_PATH) } export function getKuaishouFeifeiSourceConfig(): KuaishouFeifeiSourceConfig { @@ -47,7 +44,9 @@ export function getKuaishouFeifeiSourceConfig(): KuaishouFeifeiSourceConfig { }) } -export function saveKuaishouFeifeiSourceConfig(rawValue: unknown): Promise { +export function saveKuaishouFeifeiSourceConfig( + rawValue: unknown, +): Promise { return saveAppConfigEntry({ configKey: APP_CONFIG_KEYS.kuaishouFeifei, legacyFilePath: KUAISHOU_FEIFEI_CONFIG_FILE_PATH, diff --git a/apps/backend/src/services/platforms/kuaishou-industry/config.ts b/apps/backend/src/services/platforms/kuaishou-industry/config.ts index 96091dad..18714e34 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/config.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/config.ts @@ -33,8 +33,10 @@ export function getKuaishouIndustryConfig(overrides: Partial { const bizParams = buildConsumeCallbackBizParams({ oid: '2619000086068982', - etickets: [{ - id: 'KSVASXT6Q955WT4SS36', - num: 1, - status: 'CONSUMED', - } as any], + etickets: [ + { + id: 'KSVASXT6Q955WT4SS36', + num: 1, + status: 'CONSUMED', + } as any, + ], status: 'CONSUMED', consumeType: 'consume', consumeTime: 1783596649024, @@ -19,10 +21,12 @@ test('buildConsumeCallbackBizParams omits unsupported consume fields', () => { seriallNum: 'CONSUME-KSVASXT6Q955WT4SS36', } as any) - assert.deepEqual(bizParams.etickets, [{ - id: 'KSVASXT6Q955WT4SS36', - num: 1, - }]) + assert.deepEqual(bizParams.etickets, [ + { + id: 'KSVASXT6Q955WT4SS36', + num: 1, + }, + ]) assert.equal(bizParams.consumeType, 'consume') assert.equal('eticketType' in bizParams, false) }) diff --git a/apps/backend/src/services/platforms/kuaishou-industry/consume-code-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/consume-code-service.ts index b7c4c527..28c3a3c2 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/consume-code-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/consume-code-service.ts @@ -4,16 +4,10 @@ import { normalizeTimestampIso } from '../../../utils/time.js' import { logWarn } from '../../../utils/logger.js' import { TASK_STATUS } from '../../../domain/task-status.js' import type { JsonObject } from '../../../types/json.js' -import { - getKuaishouIndustryConfig, - assertMatchingAppKey, -} from './config.js' +import { getKuaishouIndustryConfig, assertMatchingAppKey } from './config.js' import { normalizeConsumeCodePayload, assertConsumeCodePayload } from './payload.js' import { assertKuaishouIndustrySignature } from './crypto.js' -import { - buildIndustrySuccessResponse, - buildIndustryErrorResponse, -} from './response.js' +import { buildIndustrySuccessResponse, buildIndustryErrorResponse } from './response.js' import { consumeCallback } from './consume-callback-service.js' import { consumeKuaishouIndustryVoucher } from './voucher-service.js' import { attachKuaishouIndustryVoucherToTask } from './voucher-binding-service.js' @@ -85,7 +79,10 @@ export async function handleConsumeCode(rawBody: JsonObject = {}) { } if (consumedCount === 0) { - return buildIndustryErrorResponse(4012005, `未找到匹配的卡券: ${params.etickets.map((e) => e.id).join(',')}`) + return buildIndustryErrorResponse( + 4012005, + `未找到匹配的卡券: ${params.etickets.map((e) => e.id).join(',')}`, + ) } fireConsumeCallback({ diff --git a/apps/backend/src/services/platforms/kuaishou-industry/crypto.ts b/apps/backend/src/services/platforms/kuaishou-industry/crypto.ts index 4ca8e603..3f71d23f 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/crypto.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/crypto.ts @@ -7,7 +7,10 @@ import { assertKuaishouIndustryConfig } from './config.js' export type SignMethod = 'MD5' | 'HMAC_SHA256' -export function buildKuaishouIndustrySignSource(params: JsonObject = {}, { signSecret } = assertKuaishouIndustryConfig()) { +export function buildKuaishouIndustrySignSource( + params: JsonObject = {}, + { signSecret } = assertKuaishouIndustryConfig(), +) { const entries = Object.entries(params) .filter(([key]) => key !== 'sign' && key !== 'signSecret') .sort(([left], [right]) => { @@ -17,9 +20,7 @@ export function buildKuaishouIndustrySignSource(params: JsonObject = {}, { signS return left < right ? -1 : 1 }) - const queryString = entries - .map(([key, value]) => `${key}=${stringifySignValue(value)}`) - .join('&') + const queryString = entries.map(([key, value]) => `${key}=${stringifySignValue(value)}`).join('&') return `${queryString}&signSecret=${signSecret}` } @@ -35,11 +36,7 @@ export function signKuaishouIndustryPayload( return hmacSha256Sign(source, config.signSecret) } - return crypto - .createHash('md5') - .update(source, 'utf8') - .digest('hex') - .toLowerCase() + return crypto.createHash('md5').update(source, 'utf8').digest('hex').toLowerCase() } export function verifyKuaishouIndustrySignature( @@ -64,7 +61,10 @@ export function verifyKuaishouIndustrySignature( return true } -export function assertKuaishouIndustrySignature(params: JsonObject = {}, config = assertKuaishouIndustryConfig()) { +export function assertKuaishouIndustrySignature( + params: JsonObject = {}, + config = assertKuaishouIndustryConfig(), +) { if (!verifyKuaishouIndustrySignature(params, config)) { throw createHttpError('签名验证失败', { statusCode: 400, @@ -74,14 +74,13 @@ export function assertKuaishouIndustrySignature(params: JsonObject = {}, config } function hmacSha256Sign(source: string, secret: string) { - return crypto - .createHmac('sha256', secret) - .update(source, 'utf8') - .digest('base64') + return crypto.createHmac('sha256', secret).update(source, 'utf8').digest('base64') } function normalizeSignMethod(value: unknown): SignMethod { - const normalized = String(value || '').trim().toUpperCase() + const normalized = String(value || '') + .trim() + .toUpperCase() if (normalized === 'HMAC_SHA256' || normalized === 'HMAC-SHA256') { return 'HMAC_SHA256' } diff --git a/apps/backend/src/services/platforms/kuaishou-industry/destroy-code-service.test.ts b/apps/backend/src/services/platforms/kuaishou-industry/destroy-code-service.test.ts index 4e948f82..325ecfce 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/destroy-code-service.test.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/destroy-code-service.test.ts @@ -28,10 +28,7 @@ test('normalizeDestroyCodePayload keeps destroy callback token when provided', ( }) test('resolveDestroyCallbackToken falls back to stored voucher token', () => { - const token = resolveDestroyCallbackToken([ - { token: '' }, - { token: ' stored-token ' }, - ]) + const token = resolveDestroyCallbackToken([{ token: '' }, { token: ' stored-token ' }]) assert.equal(token, 'stored-token') }) diff --git a/apps/backend/src/services/platforms/kuaishou-industry/destroy-code-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/destroy-code-service.ts index 78d7507c..b2c6baf6 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/destroy-code-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/destroy-code-service.ts @@ -7,15 +7,10 @@ import { import { normalizeTimestampIso } from '../../../utils/time.js' import { logWarn } from '../../../utils/logger.js' import { TASK_STATUS } from '../../../domain/task-status.js' -import { - getKuaishouIndustryConfig, - assertMatchingAppKey, -} from './config.js' +import { getKuaishouIndustryConfig, assertMatchingAppKey } from './config.js' import { normalizeDestroyCodePayload, assertDestroyCodePayload } from './payload.js' import { assertKuaishouIndustrySignature } from './crypto.js' -import { - buildIndustrySuccessResponse, -} from './response.js' +import { buildIndustrySuccessResponse } from './response.js' import { destroyCallback } from './destroy-callback-service.js' import { attachKuaishouIndustryVoucherToTask } from './voucher-binding-service.js' import { resolveKuaishouIndustryEticketType } from './voucher-service.js' @@ -34,18 +29,22 @@ export async function handleDestroyCode(rawBody: JsonObject = {}) { const vouchers = await listKuaishouIndustryVouchersByOid(normalizedOid) const targetIds = new Set(params.etickets.map((e) => String(e.id || '').trim()).filter(Boolean)) - const targetVouchers = targetIds.size > 0 - ? vouchers.filter((voucher) => targetIds.has(String(voucher.voucher_code || '').trim())) - : vouchers - const callbackSellerId = params.sellerId - || String(targetVouchers.find((voucher) => String(voucher.seller_id || '').trim())?.seller_id || '').trim() + const targetVouchers = + targetIds.size > 0 + ? vouchers.filter((voucher) => targetIds.has(String(voucher.voucher_code || '').trim())) + : vouchers + const callbackSellerId = + params.sellerId || + String( + targetVouchers.find((voucher) => String(voucher.seller_id || '').trim())?.seller_id || '', + ).trim() const callbackToken = params.token || resolveDestroyCallbackToken(targetVouchers) - const eticketType = resolveKuaishouIndustryEticketType( - targetVouchers[0] || null, - ) + const eticketType = resolveKuaishouIndustryEticketType(targetVouchers[0] || null) for (const voucher of targetVouchers) { - const status = String(voucher.status || '').trim().toUpperCase() + const status = String(voucher.status || '') + .trim() + .toUpperCase() if (status === 'CONSUMED') { continue } diff --git a/apps/backend/src/services/platforms/kuaishou-industry/openapi-client.ts b/apps/backend/src/services/platforms/kuaishou-industry/openapi-client.ts index f22281da..b12a5252 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/openapi-client.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/openapi-client.ts @@ -36,11 +36,7 @@ export async function requestKuaishouIndustryOpenApi( const apiLabel = formatKuaishouOpenApiLabel(input.apiMethod) if (!config.enabled) { - logKuaishouOpenApi( - input, - `快手 OpenAPI 未启用,跳过 ${apiLabel}`, - undefined, - ) + logKuaishouOpenApi(input, `快手 OpenAPI 未启用,跳过 ${apiLabel}`, undefined) return { success: true, skippedReason: 'disabled' } } @@ -70,12 +66,7 @@ export async function requestKuaishouIndustryOpenApi( } if (!config.accessToken) { - logKuaishouOpenApi( - input, - `快手 OpenAPI accessToken 缺失 ${apiLabel}`, - undefined, - 'warn', - ) + logKuaishouOpenApi(input, `快手 OpenAPI accessToken 缺失 ${apiLabel}`, undefined, 'warn') return { success: false, skippedReason: 'missing_access_token', @@ -115,13 +106,14 @@ export async function requestKuaishouIndustryOpenApi( try { const startedAt = Date.now() - const res = httpMethod === 'GET' - ? await fetch(`${url}?${body.toString()}`, { method: 'GET' }) - : await fetch(url, { - method: 'POST', - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - body: body.toString(), - }) + const res = + httpMethod === 'GET' + ? await fetch(`${url}?${body.toString()}`, { method: 'GET' }) + : await fetch(url, { + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + body: body.toString(), + }) const text = await res.text() let json: JsonObject = {} try { @@ -134,9 +126,7 @@ export async function requestKuaishouIndustryOpenApi( const success = res.ok && Number(json.result) === 1 logKuaishouOpenApi( input, - success - ? `快手 OpenAPI 调用成功 ${apiLabel}` - : `快手 OpenAPI 调用失败 ${apiLabel}`, + success ? `快手 OpenAPI 调用成功 ${apiLabel}` : `快手 OpenAPI 调用失败 ${apiLabel}`, { durationMs, httpStatus: res.status, @@ -201,17 +191,21 @@ export function buildKuaishouOpenApiRequestLog({ } export function resolveKuaishouOpenApiBaseUrl(value: unknown): string { - return String(value || DEFAULT_KUAISHOU_OPEN_API).trim().replace(/\/+$/, '') || - DEFAULT_KUAISHOU_OPEN_API + return ( + String(value || DEFAULT_KUAISHOU_OPEN_API) + .trim() + .replace(/\/+$/, '') || DEFAULT_KUAISHOU_OPEN_API + ) } export function resolveKuaishouOpenApiErrorDetail(error: unknown): JsonObject { const detail: JsonObject = { error: error instanceof Error ? error.message : String(error), } - const cause = error && typeof error === 'object' && 'cause' in error - ? (error as { cause?: unknown }).cause - : null + const cause = + error && typeof error === 'object' && 'cause' in error + ? (error as { cause?: unknown }).cause + : null if (!cause || typeof cause !== 'object') { return detail } @@ -285,16 +279,16 @@ function resolveKuaishouOpenApiName(method: unknown): string { 'integration.callback.virtual.eticket.destroy': '电子凭证销毁回调', 'integration.callback.virtual.eticket.reverse': '电子凭证冲正回调', 'open.virtual.eticket.checkavailable': '检查电子凭证有效性', - 'kwaishop_refund_addRefund': '新增退款单消息', - 'kwaishop_refund_updateRefund': '退款单更新消息', - 'kwaishop_aftersales_addRefund': '售后单新增消息', - 'kwaishop_order_addOrder': '订单新增消息', - 'kwaishop_order_statusChange': '订单状态变更消息', - 'kwaishop_order_totalFeeChange': '订单费用变更消息', - 'kwaishop_order_addNote': '订单备注消息', - 'KwaishopVirtualEticketSendService': '快手电子凭证通知发货', - 'KwaishopVirtualEticketQueryService': '电子凭证查询发码结果或券码状态', - 'KwaishopDigitalETicketDestoryService': '电子凭证发起销毁', + kwaishop_refund_addRefund: '新增退款单消息', + kwaishop_refund_updateRefund: '退款单更新消息', + kwaishop_aftersales_addRefund: '售后单新增消息', + kwaishop_order_addOrder: '订单新增消息', + kwaishop_order_statusChange: '订单状态变更消息', + kwaishop_order_totalFeeChange: '订单费用变更消息', + kwaishop_order_addNote: '订单备注消息', + KwaishopVirtualEticketSendService: '快手电子凭证通知发货', + KwaishopVirtualEticketQueryService: '电子凭证查询发码结果或券码状态', + KwaishopDigitalETicketDestoryService: '电子凭证发起销毁', 'integration.virtual.eticket.send': '电子凭证通知发码', 'integration.virtual.eticket.query': '电子凭证查询发码结果或券码状态', 'integration.virtual.eticket.destroy': '电子凭证发起销毁', diff --git a/apps/backend/src/services/platforms/kuaishou-industry/openapi-values.ts b/apps/backend/src/services/platforms/kuaishou-industry/openapi-values.ts index 69913708..f15e8f0f 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/openapi-values.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/openapi-values.ts @@ -47,7 +47,5 @@ export function normalizeStringList(value: unknown): string[] { return [] } - return value - .map((item) => String(item || '').trim()) - .filter(Boolean) + return value.map((item) => String(item || '').trim()).filter(Boolean) } diff --git a/apps/backend/src/services/platforms/kuaishou-industry/payload.ts b/apps/backend/src/services/platforms/kuaishou-industry/payload.ts index 5ec90d2c..3711fdcb 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/payload.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/payload.ts @@ -1,6 +1,6 @@ import { createHttpError } from '../../../utils/http.js' import { assertKuaishouIndustryConfig } from './config.js' -import { asJsonObject, type JsonObject } from '../../../types/json.js' +import { asJsonObject, type JsonObject } from '../../../types/json.js' export function normalizeIndustryString(value: unknown) { return String(value || '').trim() @@ -80,7 +80,10 @@ export function normalizeQueryCodePayload(raw: JsonObject = {}) { } } -export function assertSendCodePayload(payload: ReturnType, config = assertKuaishouIndustryConfig()) { +export function assertSendCodePayload( + payload: ReturnType, + config = assertKuaishouIndustryConfig(), +) { if (!payload.oid) { throw createHttpError('缺少 oid', { statusCode: 400, @@ -105,7 +108,10 @@ export function assertSendCodePayload(payload: ReturnType, config = assertKuaishouIndustryConfig()) { +export function assertDestroyCodePayload( + payload: ReturnType, + config = assertKuaishouIndustryConfig(), +) { if (!payload.oid) { throw createHttpError('缺少 oid', { statusCode: 400, @@ -123,7 +129,10 @@ export function assertDestroyCodePayload(payload: ReturnType, config = assertKuaishouIndustryConfig()) { +export function assertQueryCodePayload( + payload: ReturnType, + config = assertKuaishouIndustryConfig(), +) { if (!payload.oid) { throw createHttpError('缺少 oid', { statusCode: 400, @@ -165,7 +174,10 @@ export function normalizeConsumeCodePayload(raw: JsonObject = {}) { } } -export function assertConsumeCodePayload(payload: ReturnType, config = assertKuaishouIndustryConfig()) { +export function assertConsumeCodePayload( + payload: ReturnType, + config = assertKuaishouIndustryConfig(), +) { if (!payload.oid) { throw createHttpError('缺少 oid', { statusCode: 400, @@ -205,7 +217,16 @@ export function assertConsumeCodePayload(payload: ReturnType { +export async function sendCallback( + input: SendCallbackInput, +): Promise<{ success: boolean; response?: JsonObject; error?: string }> { const bizParams: JsonObject = { oid: input.oid, sendType: input.sendType, @@ -56,7 +58,11 @@ export async function sendCallback(input: SendCallbackInput): Promise<{ success: sellerId: input.sellerId || '', ...request, } - logIntegration('[kuaishou-industry/send-callback]', `发起电子凭证发货回调 oid=${input.oid}`, requestLog) + logIntegration( + '[kuaishou-industry/send-callback]', + `发起电子凭证发货回调 oid=${input.oid}`, + requestLog, + ) }, }) @@ -67,9 +73,14 @@ export async function sendCallback(input: SendCallbackInput): Promise<{ success: if (result.skippedReason === 'token_error') { const message = result.error || 'accessToken 刷新失败' - logIntegration('[kuaishou-industry/send-callback]', 'accessToken 刷新失败,无法发起发货回调', { - error: message, - }, { level: 'warn' }) + logIntegration( + '[kuaishou-industry/send-callback]', + 'accessToken 刷新失败,无法发起发货回调', + { + error: message, + }, + { level: 'warn' }, + ) return { success: false, error: message } } @@ -94,17 +105,27 @@ export async function sendCallback(input: SendCallbackInput): Promise<{ success: response: result.response || null, }) } else if (result.error) { - logIntegration('[kuaishou-industry/send-callback]', `电子凭证发货回调异常 oid=${input.oid}`, { - ...(result.errorDetail || { error: result.error }), - request: requestLog, - }, { level: 'warn' }) + logIntegration( + '[kuaishou-industry/send-callback]', + `电子凭证发货回调异常 oid=${input.oid}`, + { + ...(result.errorDetail || { error: result.error }), + request: requestLog, + }, + { level: 'warn' }, + ) } else { - logIntegration('[kuaishou-industry/send-callback]', `电子凭证发货回调失败 oid=${input.oid}`, { - durationMs: result.durationMs, - status: result.httpStatus, - request: requestLog, - response: result.response || null, - }, { level: 'warn' }) + logIntegration( + '[kuaishou-industry/send-callback]', + `电子凭证发货回调失败 oid=${input.oid}`, + { + durationMs: result.durationMs, + status: result.httpStatus, + request: requestLog, + response: result.response || null, + }, + { level: 'warn' }, + ) } return { diff --git a/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.test.ts b/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.test.ts index 5d0dc90f..f904015b 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.test.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.test.ts @@ -29,11 +29,7 @@ test('resolveSendCallbackPreferredTotalGoodsValue falls back to order total amou }) test('resolveSendCallbackGoodsValuePlan prefers 91 amount over ext payment', () => { - const plan = resolveSendCallbackGoodsValuePlan( - [{}], - '{"payment":100,"platformBearAmount":0}', - 1, - ) + const plan = resolveSendCallbackGoodsValuePlan([{}], '{"payment":100,"platformBearAmount":0}', 1) assert.deepEqual(plan.goodsValues, [1]) assert.equal(plan.totalGoodsValue, 1) @@ -41,11 +37,7 @@ test('resolveSendCallbackGoodsValuePlan prefers 91 amount over ext payment', () }) test('resolveSendCallbackGoodsValuePlan splits preferred amount and keeps sum equal to total', () => { - const plan = resolveSendCallbackGoodsValuePlan( - [{}, {}, {}], - '{"payment":999}', - 100, - ) + const plan = resolveSendCallbackGoodsValuePlan([{}, {}, {}], '{"payment":999}', 100) assert.deepEqual(plan.goodsValues, [33, 33, 34]) assert.equal(plan.totalGoodsValue, 100) @@ -53,20 +45,14 @@ test('resolveSendCallbackGoodsValuePlan splits preferred amount and keeps sum eq }) test('resolveSendCallbackGoodsValuePlan uses ext payment for a single voucher', () => { - const plan = resolveSendCallbackGoodsValuePlan( - [{}], - '{"payment":1,"platformBearAmount":0}', - ) + const plan = resolveSendCallbackGoodsValuePlan([{}], '{"payment":1,"platformBearAmount":0}') assert.deepEqual(plan.goodsValues, [1]) assert.equal(plan.totalGoodsValue, 1) }) test('resolveSendCallbackGoodsValuePlan splits ext payment and keeps sum equal to total', () => { - const plan = resolveSendCallbackGoodsValuePlan( - [{}, {}, {}], - '{"payment":100}', - ) + const plan = resolveSendCallbackGoodsValuePlan([{}, {}, {}], '{"payment":100}') assert.deepEqual(plan.goodsValues, [33, 33, 34]) assert.equal(plan.totalGoodsValue, 100) diff --git a/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts index 62ea6310..412dfb4d 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/send-code-service.ts @@ -505,9 +505,9 @@ async function syncOpen91OrderAfterSendCallbackSuccess( function isOpen91OrderAwaitingFulfillmentConfig(error: unknown) { return Boolean( error && - typeof error === 'object' && - String((error as { errorCode?: unknown }).errorCode || '').trim() === - 'open91_order_still_unconfigured', + typeof error === 'object' && + String((error as { errorCode?: unknown }).errorCode || '').trim() === + 'open91_order_still_unconfigured', ) } diff --git a/apps/backend/src/services/platforms/kuaishou-industry/source-config-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/source-config-service.ts index 195c78c3..828af66c 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/source-config-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/source-config-service.ts @@ -3,12 +3,13 @@ import type { JsonObject } from '../../../types/json.js' import { APP_CONFIG_KEYS } from '../../../config/app-config-keys.js' import { PROJECT_ROOT, runtimeConfig } from '../../../config/runtime.js' -import { - readAppConfigEntry, - saveAppConfigEntry, -} from '../../config/app-config-store.js' +import { readAppConfigEntry, saveAppConfigEntry } from '../../config/app-config-store.js' -const KUAISHOU_INDUSTRY_SOURCE_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'kuaishou-industry-source.json') +const KUAISHOU_INDUSTRY_SOURCE_FILE_PATH = path.join( + PROJECT_ROOT, + 'data', + 'kuaishou-industry-source.json', +) const DEFAULT_CALLBACK_BASE_URL = 'https://openapi.kwaixiaodian.com' const DEFAULT_AUTH_BASE_URL = 'https://open.kwaixiaodian.com' const LEGACY_DEFAULT_REDIRECT_URI = 'https://ks.khhao.com/oauth-callback' @@ -73,7 +74,9 @@ export function getKuaishouIndustrySourceConfig(): KuaishouIndustrySourceConfig }) } -export function saveKuaishouIndustrySourceConfig(rawValue: unknown): Promise { +export function saveKuaishouIndustrySourceConfig( + rawValue: unknown, +): Promise { return saveAppConfigEntry({ configKey: APP_CONFIG_KEYS.kuaishouIndustrySource, legacyFilePath: KUAISHOU_INDUSTRY_SOURCE_FILE_PATH, @@ -106,8 +109,11 @@ export function findKuaishouIndustryShopConfig( return null } - return listKuaishouIndustryShopConfigs(source) - .find((shop) => String(shop.sellerId || shop.shopId || '').trim() === normalizedSellerId) || null + return ( + listKuaishouIndustryShopConfigs(source).find( + (shop) => String(shop.sellerId || shop.shopId || '').trim() === normalizedSellerId, + ) || null + ) } export function patchKuaishouIndustryShopConfig( @@ -127,8 +133,9 @@ export function patchKuaishouIndustryShopConfig( return Promise.resolve(source) } - const shops = listKuaishouIndustryShopConfigs(source) - .filter((shop) => String(shop.sellerId || shop.shopId || '').trim() !== normalizedSellerId) + const shops = listKuaishouIndustryShopConfigs(source).filter( + (shop) => String(shop.sellerId || shop.shopId || '').trim() !== normalizedSellerId, + ) return saveKuaishouIndustrySourceConfig({ ...source, @@ -136,7 +143,9 @@ export function patchKuaishouIndustryShopConfig( }) } -export function normalizeKuaishouIndustrySourceConfig(rawValue: unknown): KuaishouIndustrySourceConfig { +export function normalizeKuaishouIndustrySourceConfig( + rawValue: unknown, +): KuaishouIndustrySourceConfig { const fallback = createDefaultKuaishouIndustrySourceConfig() const source = isPlainObject(rawValue) ? rawValue : {} const shops = normalizeKuaishouIndustryShopList(source) @@ -204,9 +213,7 @@ function createDefaultKuaishouIndustrySourceConfig(): KuaishouIndustrySourceConf } function normalizeKuaishouIndustryShopList(source: JsonObject): KuaishouIndustryShopConfig[] { - const rawShops = Array.isArray(source.shops) - ? source.shops - : buildLegacySingleShopList(source) + const rawShops = Array.isArray(source.shops) ? source.shops : buildLegacySingleShopList(source) const deduped = new Map() let unnamedIndex = 0 @@ -237,7 +244,15 @@ function normalizeKuaishouIndustryShopConfig(rawValue: unknown): KuaishouIndustr const refreshToken = normalizeString(rawValue.refreshToken, '') const openId = normalizeString(rawValue.openId, '') - if (!sellerId && !shopId && !shopName && !customShopName && !accessToken && !refreshToken && !openId) { + if ( + !sellerId && + !shopId && + !shopName && + !customShopName && + !accessToken && + !refreshToken && + !openId + ) { return null } @@ -336,7 +351,11 @@ function normalizeScopeText(value: unknown): string { } function normalizeUrlLike(value: unknown, fallback: string): string { - return String(value || fallback).trim().replace(/\/+$/, '') || fallback + return ( + String(value || fallback) + .trim() + .replace(/\/+$/, '') || fallback + ) } function normalizeNullableIso(value: unknown): string { diff --git a/apps/backend/src/services/platforms/kuaishou-industry/token-service.test.ts b/apps/backend/src/services/platforms/kuaishou-industry/token-service.test.ts index ede8b6aa..b3e5d498 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/token-service.test.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/token-service.test.ts @@ -1,54 +1,77 @@ import test from 'node:test' import assert from 'node:assert/strict' -import { - ensureKuaishouIndustryAccessToken, - shouldRefreshAccessToken, -} from './token-service.js' +import { ensureKuaishouIndustryAccessToken, shouldRefreshAccessToken } from './token-service.js' import type { KuaishouIndustryShopConfig, KuaishouIndustrySourceConfig, } from './source-config-service.js' test('shouldRefreshAccessToken refreshes missing or expiring tokens', () => { - assert.equal(shouldRefreshAccessToken(createConfig({ - accessToken: '', - accessTokenExpiresAt: '', - })), true) + assert.equal( + shouldRefreshAccessToken( + createConfig({ + accessToken: '', + accessTokenExpiresAt: '', + }), + ), + true, + ) - assert.equal(shouldRefreshAccessToken(createConfig({ - accessToken: 'token', - accessTokenExpiresAt: new Date(Date.now() + 10 * 60 * 1000).toISOString(), - })), true) + assert.equal( + shouldRefreshAccessToken( + createConfig({ + accessToken: 'token', + accessTokenExpiresAt: new Date(Date.now() + 10 * 60 * 1000).toISOString(), + }), + ), + true, + ) - assert.equal(shouldRefreshAccessToken(createConfig({ - accessToken: 'token', - accessTokenExpiresAt: new Date(Date.now() - 10 * 1000).toISOString(), - })), true) + assert.equal( + shouldRefreshAccessToken( + createConfig({ + accessToken: 'token', + accessTokenExpiresAt: new Date(Date.now() - 10 * 1000).toISOString(), + }), + ), + true, + ) }) test('shouldRefreshAccessToken keeps valid tokens without a forced refresh', () => { - assert.equal(shouldRefreshAccessToken(createConfig({ - accessToken: 'token', - accessTokenExpiresAt: '', - })), false) + assert.equal( + shouldRefreshAccessToken( + createConfig({ + accessToken: 'token', + accessTokenExpiresAt: '', + }), + ), + false, + ) - assert.equal(shouldRefreshAccessToken(createConfig({ - accessToken: 'token', - accessTokenExpiresAt: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(), - })), false) + assert.equal( + shouldRefreshAccessToken( + createConfig({ + accessToken: 'token', + accessTokenExpiresAt: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(), + }), + ), + false, + ) }) test('ensureKuaishouIndustryAccessToken requires sellerId when multiple shops are enabled', async () => { await assert.rejects( - () => ensureKuaishouIndustryAccessToken({ - config: createConfig({ - shops: [ - createShop({ sellerId: 'seller-a', accessToken: 'token-a' }), - createShop({ sellerId: 'seller-b', accessToken: 'token-b' }), - ], + () => + ensureKuaishouIndustryAccessToken({ + config: createConfig({ + shops: [ + createShop({ sellerId: 'seller-a', accessToken: 'token-a' }), + createShop({ sellerId: 'seller-b', accessToken: 'token-b' }), + ], + }), }), - }), { message: '存在多个快手行业电子凭证店铺授权,请指定 sellerId', errorCode: 'kuaishou_industry_missing_seller_id', @@ -105,9 +128,7 @@ function createConfig( } } -function createShop( - patch: Partial = {}, -): KuaishouIndustryShopConfig { +function createShop(patch: Partial = {}): KuaishouIndustryShopConfig { return { enabled: true, sellerId: 'seller-a', diff --git a/apps/backend/src/services/platforms/kuaishou-industry/token-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/token-service.ts index 117458bd..f5d2bada 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/token-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/token-service.ts @@ -159,12 +159,18 @@ export async function exchangeKuaishouIndustryAuthorizationCode( await patchKuaishouIndustrySourceConfig({ lastRefreshError: message, }) - logWarn('[kuaishou-industry/token]', '授权码换取 accessToken 失败', resolveRefreshErrorDetail(error)) + logWarn( + '[kuaishou-industry/token]', + '授权码换取 accessToken 失败', + resolveRefreshErrorDetail(error), + ) throw error } } -export function shouldRefreshAccessToken(config: Pick): boolean { +export function shouldRefreshAccessToken( + config: Pick, +): boolean { if (!String(config.accessToken || '').trim()) { return true } @@ -201,8 +207,9 @@ function resolveTokenShop( return matched } - const enabledShops = listKuaishouIndustryShopConfigs(config) - .filter((shop) => shop.enabled !== false) + const enabledShops = listKuaishouIndustryShopConfigs(config).filter( + (shop) => shop.enabled !== false, + ) if (enabledShops.length === 1) { return enabledShops[0] as KuaishouIndustryShopConfig @@ -237,7 +244,10 @@ function createEmptyShopConfig(): KuaishouIndustryShopConfig { } } -function assertRefreshConfig(config: KuaishouIndustrySourceConfig, shop: KuaishouIndustryShopConfig) { +function assertRefreshConfig( + config: KuaishouIndustrySourceConfig, + shop: KuaishouIndustryShopConfig, +) { if (!config.baseUrl) { throw createHttpError('快手开放平台 API 地址未配置', { statusCode: 400, @@ -298,15 +308,18 @@ async function requestKuaishouIndustryToken({ failureMessage: string errorCode: string }) { - const baseUrl = String(config.baseUrl || '').trim().replace(/\/+$/, '') + const baseUrl = String(config.baseUrl || '') + .trim() + .replace(/\/+$/, '') const url = new URL(`${baseUrl}${path}`) - const response = method === 'GET' - ? await fetch(appendSearchParams(url, params)) - : await fetch(url, { - method: 'POST', - headers: { 'content-type': 'application/x-www-form-urlencoded' }, - body: params.toString(), - }) + const response = + method === 'GET' + ? await fetch(appendSearchParams(url, params)) + : await fetch(url, { + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + body: params.toString(), + }) const text = await response.text() const json = parseJsonObject(text) const tokenPayload = normalizeTokenResponse(json, config) @@ -347,8 +360,11 @@ async function saveTokenPayload( accessToken: tokenPayload.accessToken, refreshToken: nextRefreshToken, accessTokenExpiresAt: tokenPayload.accessTokenExpiresAt, - refreshTokenExpiresAt: tokenPayload.refreshTokenExpiresAt - || (tokenPayload.refreshToken ? new Date(Date.now() + DEFAULT_REFRESH_TOKEN_TTL_MS).toISOString() : shop.refreshTokenExpiresAt), + refreshTokenExpiresAt: + tokenPayload.refreshTokenExpiresAt || + (tokenPayload.refreshToken + ? new Date(Date.now() + DEFAULT_REFRESH_TOKEN_TTL_MS).toISOString() + : shop.refreshTokenExpiresAt), openId: tokenPayload.openId || shop.openId, grantedScopes: tokenPayload.grantedScopes || shop.grantedScopes, lastRefreshedAt: new Date().toISOString(), @@ -361,16 +377,9 @@ async function saveTokenPayload( } } -function normalizeTokenResponse( - json: JsonObject, - currentConfig: KuaishouIndustrySourceConfig, -) { +function normalizeTokenResponse(json: JsonObject, currentConfig: KuaishouIndustrySourceConfig) { const payload = isPlainObject(json.data) ? json.data : json - const accessToken = pickFirstString(payload, [ - 'access_token', - 'accessToken', - 'accessTokenValue', - ]) + const accessToken = pickFirstString(payload, ['access_token', 'accessToken', 'accessTokenValue']) const refreshToken = pickFirstString(payload, [ 'refresh_token', 'refreshToken', @@ -386,26 +395,24 @@ function normalizeTokenResponse( sellerId, openId, grantedScopes, - accessTokenExpiresAt: resolveExpiresAt(payload, [ - 'access_token_expires_at', - 'accessTokenExpiresAt', - 'accessTokenExpireAt', - 'expires_at', - 'expireAt', - ], [ - 'expires_in', - 'expiresIn', - 'access_token_expires_in', - 'accessTokenExpiresIn', - ], DEFAULT_ACCESS_TOKEN_TTL_MS), - refreshTokenExpiresAt: resolveExpiresAt(payload, [ - 'refresh_token_expires_at', - 'refreshTokenExpiresAt', - 'refreshTokenExpireAt', - ], [ - 'refresh_token_expires_in', - 'refreshTokenExpiresIn', - ], 0), + accessTokenExpiresAt: resolveExpiresAt( + payload, + [ + 'access_token_expires_at', + 'accessTokenExpiresAt', + 'accessTokenExpireAt', + 'expires_at', + 'expireAt', + ], + ['expires_in', 'expiresIn', 'access_token_expires_in', 'accessTokenExpiresIn'], + DEFAULT_ACCESS_TOKEN_TTL_MS, + ), + refreshTokenExpiresAt: resolveExpiresAt( + payload, + ['refresh_token_expires_at', 'refreshTokenExpiresAt', 'refreshTokenExpireAt'], + ['refresh_token_expires_in', 'refreshTokenExpiresIn'], + 0, + ), } } @@ -438,15 +445,11 @@ function resolveExpiresAt( } } - return fallbackDurationMs > 0 - ? new Date(Date.now() + fallbackDurationMs).toISOString() - : '' + return fallbackDurationMs > 0 ? new Date(Date.now() + fallbackDurationMs).toISOString() : '' } function parseTimestampLike(value: unknown): number { - const raw = typeof value === 'number' - ? value - : Number(String(value || '').trim()) + const raw = typeof value === 'number' ? value : Number(String(value || '').trim()) if (Number.isFinite(raw) && raw > 0) { return raw > 10_000_000_000 ? raw : raw * 1000 @@ -467,14 +470,16 @@ function isTokenResponseSuccess(json: JsonObject): boolean { function resolveTokenErrorMessage(json: JsonObject, status: number, fallback: string): string { const payload = isPlainObject(json.data) ? json.data : json - return pickFirstString(payload, [ - 'error_msg', - 'errorMsg', - 'message', - 'msg', - 'error_description', - 'errorDescription', - ]) || `${fallback},HTTP ${status}` + return ( + pickFirstString(payload, [ + 'error_msg', + 'errorMsg', + 'message', + 'msg', + 'error_description', + 'errorDescription', + ]) || `${fallback},HTTP ${status}` + ) } function parseJsonObject(text: string): JsonObject { @@ -500,7 +505,10 @@ function pickFirstString(payload: JsonObject, keys: string[]): string { function pickScopes(payload: JsonObject): string { const value = payload.scopes ?? payload.scope if (Array.isArray(value)) { - return value.map((item) => String(item || '').trim()).filter(Boolean).join(',') + return value + .map((item) => String(item || '').trim()) + .filter(Boolean) + .join(',') } return String(value || '').trim() @@ -510,9 +518,10 @@ function resolveRefreshErrorDetail(error: unknown): JsonObject { const detail: JsonObject = { error: error instanceof Error ? error.message : String(error), } - const cause = error && typeof error === 'object' && 'cause' in error - ? (error as { cause?: unknown }).cause - : null + const cause = + error && typeof error === 'object' && 'cause' in error + ? (error as { cause?: unknown }).cause + : null if (!cause || typeof cause !== 'object') { return detail } diff --git a/apps/backend/src/services/platforms/kuaishou-industry/voucher-binding-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/voucher-binding-service.ts index 77284c39..43db451e 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/voucher-binding-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/voucher-binding-service.ts @@ -79,11 +79,14 @@ export async function attachKuaishouIndustryVoucherToTask( if ( sendCallbackConfirmed && - (String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' || context.kuaishouCloudFulfillment) + (String(task.executor_key || '').trim() === 'kuaishou_ct_assisted' || + context.kuaishouCloudFulfillment) ) { const flow = normalizeKuaishouCloudFlow(context.kuaishouCloudFulfillment) const consumedAt = voucher.consumed_at || nextVoucherContext.consumedAt || null - const status = String(voucher.status || 'UNUSED').trim().toUpperCase() + const status = String(voucher.status || 'UNUSED') + .trim() + .toUpperCase() nextContext.kuaishouCloudFulfillment = { ...flow, @@ -138,10 +141,13 @@ export function buildKuaishouIndustryVoucherContext( existingValue: unknown, now: string, ) { - const existing = existingValue && typeof existingValue === 'object' && !Array.isArray(existingValue) - ? existingValue as JsonObject - : {} - const status = String(voucher.status || 'UNUSED').trim().toUpperCase() + const existing = + existingValue && typeof existingValue === 'object' && !Array.isArray(existingValue) + ? (existingValue as JsonObject) + : {} + const status = String(voucher.status || 'UNUSED') + .trim() + .toUpperCase() return { ...existing, diff --git a/apps/backend/src/services/platforms/kuaishou-industry/voucher-service.test.ts b/apps/backend/src/services/platforms/kuaishou-industry/voucher-service.test.ts index 670be9a4..dd8d07de 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/voucher-service.test.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/voucher-service.test.ts @@ -81,12 +81,15 @@ test('normalizeKuaishouIndustryConsumeType defaults to consume', () => { }) test('buildKuaishouIndustryEticketFromVoucher mirrors voucher code into id and code', () => { - const eticket = buildKuaishouIndustryEticketFromVoucher({ - voucher_code: 'KSV15K821FDHPMTX1ST', - status: 'UNUSED', - valid_start_time: 1783427213868, - valid_end_time: 1786019213868, - } as any, 'GAME_OPEN_TICKET_CONSUME') + const eticket = buildKuaishouIndustryEticketFromVoucher( + { + voucher_code: 'KSV15K821FDHPMTX1ST', + status: 'UNUSED', + valid_start_time: 1783427213868, + valid_end_time: 1786019213868, + } as any, + 'GAME_OPEN_TICKET_CONSUME', + ) assert.equal(eticket.id, 'KSV15K821FDHPMTX1ST') assert.equal(eticket.code, 'KSV15K821FDHPMTX1ST') diff --git a/apps/backend/src/services/platforms/kuaishou-industry/voucher-service.ts b/apps/backend/src/services/platforms/kuaishou-industry/voucher-service.ts index 70225a26..746ace18 100644 --- a/apps/backend/src/services/platforms/kuaishou-industry/voucher-service.ts +++ b/apps/backend/src/services/platforms/kuaishou-industry/voucher-service.ts @@ -1,6 +1,6 @@ import { createTaskEvent } from '../../../repositories/task-event-repo.js' import { getTaskById, updateTask } from '../../../repositories/task-repo.js' -import { asJsonObject, type JsonObject } from '../../../types/json.js' +import { asJsonObject, type JsonObject } from '../../../types/json.js' import { findKuaishouIndustryVoucherByCode, listKuaishouIndustryVouchersByTaskId, @@ -104,7 +104,9 @@ export function buildKuaishouIndustryEticketFromVoucher( } export function normalizeKuaishouIndustrySendCallbackStatus(value: unknown) { - const normalized = String(value || '').trim().toLowerCase() + const normalized = String(value || '') + .trim() + .toLowerCase() if ( normalized === KUAISHOU_INDUSTRY_SEND_CALLBACK_STATUS.PENDING || normalized === KUAISHOU_INDUSTRY_SEND_CALLBACK_STATUS.FAILED @@ -118,8 +120,10 @@ export function normalizeKuaishouIndustrySendCallbackStatus(value: unknown) { export function isKuaishouIndustryVoucherSendCallbackSuccess( voucher: Pick, ) { - return normalizeKuaishouIndustrySendCallbackStatus(voucher.send_callback_status) === + return ( + normalizeKuaishouIndustrySendCallbackStatus(voucher.send_callback_status) === KUAISHOU_INDUSTRY_SEND_CALLBACK_STATUS.SUCCESS + ) } export function resolveKuaishouIndustryVoucherSendCallbackMessage( @@ -237,7 +241,9 @@ export async function consumeKuaishouIndustryVoucher( const now = new Date().toISOString() const consumeTime = Number(input.consumeTime || Date.now()) || Date.now() const consumeType = normalizeKuaishouIndustryConsumeType(input.consumeType) - const serialNum = String(input.serialNum || voucher.consume_serial_num || `CONSUME-${voucher.voucher_code}`).trim() + const serialNum = String( + input.serialNum || voucher.consume_serial_num || `CONSUME-${voucher.voucher_code}`, + ).trim() const token = String(input.token || voucher.token || '').trim() const consumeDetail = { serialNum, @@ -255,10 +261,12 @@ export async function consumeKuaishouIndustryVoucher( : await consumeCallback({ oid: voucher.oid, sellerId: String(voucher.seller_id || '').trim(), - etickets: [{ - id: voucher.voucher_code, - num: 1, - }], + etickets: [ + { + id: voucher.voucher_code, + num: 1, + }, + ], status: 'CONSUMED', consumeType, consumeTime, @@ -349,12 +357,14 @@ export async function destroyKuaishouIndustryVoucher( : await destroyCallback({ oid: voucher.oid, sellerId: String(voucher.seller_id || '').trim(), - etickets: [{ - id: voucher.voucher_code, - code: voucher.voucher_code, - num: 1, - ...(input.goodsValue != null ? { goodsValue: input.goodsValue } : {}), - }], + etickets: [ + { + id: voucher.voucher_code, + code: voucher.voucher_code, + num: 1, + ...(input.goodsValue != null ? { goodsValue: input.goodsValue } : {}), + }, + ], reason, token, ...(eticketType ? { eticketType } : {}), @@ -375,25 +385,23 @@ export async function destroyKuaishouIndustryVoucher( status: 'DESTROYED', destroyedAt: now, updatedAt: now, - ...(eticketType && !String(voucher.eticket_type || '').trim() - ? { eticketType } - : {}), + ...(eticketType && !String(voucher.eticket_type || '').trim() ? { eticketType } : {}), }) updated = next || voucher } let task = - input.task || - (voucher.task_id ? await getTaskById(voucher.task_id).catch(() => null) : null) + input.task || (voucher.task_id ? await getTaskById(voucher.task_id).catch(() => null) : null) if (task && !alreadyDestroyed) { - task = await updateTask(task.id, { - task_status: TASK_STATUS.CLOSED, - delivery_status: 'cancelled', - result_code: reason, - result_message: `电子凭证销毁: ${reason}`, - updated_at: now, - }) || task + task = + (await updateTask(task.id, { + task_status: TASK_STATUS.CLOSED, + delivery_status: 'cancelled', + result_code: reason, + result_message: `电子凭证销毁: ${reason}`, + updated_at: now, + })) || task await createTaskEvent( task.id, @@ -420,7 +428,9 @@ export async function destroyKuaishouIndustryVoucher( } export function normalizeKuaishouIndustryDestroyReason(value: unknown): string { - const normalized = String(value || '').trim().toUpperCase() + const normalized = String(value || '') + .trim() + .toUpperCase() if ( normalized === KUAISHOU_INDUSTRY_DESTROY_REASONS.ETICKET_EXPIRED || normalized === KUAISHOU_INDUSTRY_DESTROY_REASONS.USER_APPLY_REFUND || @@ -448,7 +458,7 @@ function resolveTaskVouchers(task: TaskRow): Promise row ? [row] : []) + .then((row) => (row ? [row] : [])) .catch((error) => { logWarn('[kuaishou-industry/voucher]', '按上下文查询电子凭证失败', { taskId: task?.id || null, @@ -469,11 +479,13 @@ function resolveVoucherConsumeDetails(voucher: KuaishouIndustryVoucherRow): Json return [] } - return [{ - serialNum: voucher.consume_serial_num, - consumeType: KUAISHOU_INDUSTRY_DEFAULT_CONSUME_TYPE, - consumeTime: voucher.consumed_at ? Date.parse(voucher.consumed_at) : 0, - }] + return [ + { + serialNum: voucher.consume_serial_num, + consumeType: KUAISHOU_INDUSTRY_DEFAULT_CONSUME_TYPE, + consumeTime: voucher.consumed_at ? Date.parse(voucher.consumed_at) : 0, + }, + ] } function appendConsumeDetail(details: JsonObject[], nextDetail: JsonObject): JsonObject[] { @@ -486,7 +498,9 @@ function appendConsumeDetail(details: JsonObject[], nextDetail: JsonObject): Jso } function normalizeVoucherStatus(value: unknown): string { - const normalized = String(value || '').trim().toUpperCase() + const normalized = String(value || '') + .trim() + .toUpperCase() if (normalized === 'CONSUMED' || normalized === 'DESTROYED') { return normalized } @@ -495,8 +509,10 @@ function normalizeVoucherStatus(value: unknown): string { } export function normalizeKuaishouIndustryConsumeType(value: unknown): string { - return String(value || KUAISHOU_INDUSTRY_DEFAULT_CONSUME_TYPE).trim() || + return ( + String(value || KUAISHOU_INDUSTRY_DEFAULT_CONSUME_TYPE).trim() || KUAISHOU_INDUSTRY_DEFAULT_CONSUME_TYPE + ) } function normalizePositiveTimestamp(value: unknown): number { diff --git a/apps/backend/src/services/platforms/ninetyone/order-service.test.ts b/apps/backend/src/services/platforms/ninetyone/order-service.test.ts index c3488bca..e2efd695 100644 --- a/apps/backend/src/services/platforms/ninetyone/order-service.test.ts +++ b/apps/backend/src/services/platforms/ninetyone/order-service.test.ts @@ -1,24 +1,24 @@ import test from 'node:test' import assert from 'node:assert/strict' -import { - buildOpen91SourceEvent, - parseOpen91ProductNo, -} from './order-service.js' +import { buildOpen91SourceEvent, parseOpen91ProductNo } from './order-service.js' test('buildOpen91SourceEvent maps 91 productNo into source item identities', () => { - const event = buildOpen91SourceEvent({ - orderNo: 'P91KS202605130001', - productNo: 'KS-CLOUD-SKU-001', - buyNum: 2, - maxAmount: '0.01', - timestamp: 1778670308, - version: '1.0', - sign: 'SIGN', - }, { - shopId: '91kaquan', - shopName: '91卡券', - }) + const event = buildOpen91SourceEvent( + { + orderNo: 'P91KS202605130001', + productNo: 'KS-CLOUD-SKU-001', + buyNum: 2, + maxAmount: '0.01', + timestamp: 1778670308, + version: '1.0', + sign: 'SIGN', + }, + { + shopId: '91kaquan', + shopName: '91卡券', + }, + ) assert.equal(event.provider, '91kaquan') assert.equal(event.platform, 'kuaishou') @@ -34,29 +34,29 @@ test('buildOpen91SourceEvent maps 91 productNo into source item identities', () }) test('parseOpen91ProductNo splits product name and kuaishou shop id by four dashes', () => { - assert.deepEqual( - parseOpen91ProductNo('测试-关联商品1----3676797936'), - { - rawProductNo: '测试-关联商品1----3676797936', - productName: '测试-关联商品1', - shopId: '3676797936', - }, - ) + assert.deepEqual(parseOpen91ProductNo('测试-关联商品1----3676797936'), { + rawProductNo: '测试-关联商品1----3676797936', + productName: '测试-关联商品1', + shopId: '3676797936', + }) }) test('buildOpen91SourceEvent keeps productNo suffix for kuaishou consume only', () => { - const event = buildOpen91SourceEvent({ - orderNo: 'P91KS202605130002', - productNo: '测试-关联商品1----3676797936', - buyNum: 1, - maxAmount: '0.01', - timestamp: 1778670308, - version: '1.0', - sign: 'SIGN', - }, { - shopId: '91kaquan', - shopName: '91卡券', - }) + const event = buildOpen91SourceEvent( + { + orderNo: 'P91KS202605130002', + productNo: '测试-关联商品1----3676797936', + buyNum: 1, + maxAmount: '0.01', + timestamp: 1778670308, + version: '1.0', + sign: 'SIGN', + }, + { + shopId: '91kaquan', + shopName: '91卡券', + }, + ) assert.equal(event.shopId, '91kaquan') assert.equal(event.shopName, '91卡券') diff --git a/apps/backend/src/services/scheduler/cloudtentacles-health-job.ts b/apps/backend/src/services/scheduler/cloudtentacles-health-job.ts index fd7e4a28..e5fc1e9d 100644 --- a/apps/backend/src/services/scheduler/cloudtentacles-health-job.ts +++ b/apps/backend/src/services/scheduler/cloudtentacles-health-job.ts @@ -13,9 +13,7 @@ import { notifyCloudtentaclesAssetLow, notifyCloudtentaclesAuthExpired, } from '../notification/domain-notifications.js' -import { - retryCloudtentaclesPendingReturns, -} from './cloudtentacles-number-recycle-service.js' +import { retryCloudtentaclesPendingReturns } from './cloudtentacles-number-recycle-service.js' type CloudtentaclesHealthAccountResult = { sourceKey: string @@ -50,11 +48,12 @@ export async function runCloudtentaclesHealthJob(job: JsonObject) { const summary = buildCloudtentaclesHealthSummary(results) const status = resolveCloudtentaclesHealthStatus(results) const orphanTaskCount = Number(returnRetryResult?.orphanCount || 0) - const recycleNote = orphanTaskCount > 0 - ? `,另有 ${orphanTaskCount} 个孤儿号码待人工/上游清理` - : returnRetryResult?.returned - ? ',发货后退号重试正常' - : '' + const recycleNote = + orphanTaskCount > 0 + ? `,另有 ${orphanTaskCount} 个孤儿号码待人工/上游清理` + : returnRetryResult?.returned + ? ',发货后退号重试正常' + : '' const finalMessage = `${summary}${recycleNote}` return { @@ -66,12 +65,11 @@ export async function runCloudtentaclesHealthJob(job: JsonObject) { okCount: okResults.length, lowAssetCount: lowAssetResults.length, failedCount: failedResults.length, - asset: assetValues.length > 0 - ? Math.min(...assetValues) - : null, - threshold: checkedResults.length > 0 - ? Math.max(...checkedResults.map((item) => Number(item.threshold || 0))) - : normalizeNonNegativeInteger(job.config?.assetThreshold, 500), + asset: assetValues.length > 0 ? Math.min(...assetValues) : null, + threshold: + checkedResults.length > 0 + ? Math.max(...checkedResults.map((item) => Number(item.threshold || 0))) + : normalizeNonNegativeInteger(job.config?.assetThreshold, 500), accounts: results, pendingReturnRetry: returnRetryResult, } @@ -84,7 +82,8 @@ async function runCloudtentaclesHealthAccount( const sourceKey = String(account.sourceKey || '').trim() || 'default' const source = getCloudtentaclesSourceByKey(sourceKey) const session: JsonObject = getCloudtentaclesSessionStateByKey(sourceKey) || {} - const label = String(account.label || source?.label || source?.username || sourceKey).trim() || sourceKey + const label = + String(account.label || source?.label || source?.username || sourceKey).trim() || sourceKey const threshold = normalizeNonNegativeInteger(account.assetThreshold, 500) const checkedAt = new Date().toISOString() @@ -170,9 +169,10 @@ async function runCloudtentaclesHealthAccount( return { ok: true, status: asset < threshold ? 'asset_low' : 'ok', - message: asset < threshold - ? `账号 ${label} 余额 ${asset} 低于阈值 ${threshold}` - : `账号 ${label} 余额 ${asset} 正常`, + message: + asset < threshold + ? `账号 ${label} 余额 ${asset} 低于阈值 ${threshold}` + : `账号 ${label} 余额 ${asset} 正常`, asset, threshold, sourceKey, @@ -230,16 +230,11 @@ function resolveCloudtentaclesHealthAccounts(job: JsonObject) { const configured = configuredMap.get(sourceKey) return { sourceKey, - label: String( - configured?.label || source.label || source.username || sourceKey, - ).trim() || sourceKey, - enabled: configured - ? configured.enabled !== false - : source.enabled !== false, - assetThreshold: normalizeNonNegativeInteger( - configured?.assetThreshold, - defaultThreshold, - ), + label: + String(configured?.label || source.label || source.username || sourceKey).trim() || + sourceKey, + enabled: configured ? configured.enabled !== false : source.enabled !== false, + assetThreshold: normalizeNonNegativeInteger(configured?.assetThreshold, defaultThreshold), } }) .filter(Boolean) as JsonObject[] @@ -258,12 +253,14 @@ function resolveCloudtentaclesHealthAccounts(job: JsonObject) { // 兼容旧配置:没有任何平台账号时,仍检查历史 default。 const fallback = configuredMap.get('default') - return [{ - sourceKey: 'default', - label: String(fallback?.label || '默认账号').trim() || '默认账号', - enabled: fallback ? fallback.enabled !== false : true, - assetThreshold: normalizeNonNegativeInteger(fallback?.assetThreshold, defaultThreshold), - }] + return [ + { + sourceKey: 'default', + label: String(fallback?.label || '默认账号').trim() || '默认账号', + enabled: fallback ? fallback.enabled !== false : true, + assetThreshold: normalizeNonNegativeInteger(fallback?.assetThreshold, defaultThreshold), + }, + ] } function normalizeCloudtentaclesHealthAccount( @@ -322,7 +319,9 @@ function buildCloudtentaclesHealthSummary(results: CloudtentaclesHealthAccountRe lowAssetResults.length > 0 ? `余额预警 ${lowAssetResults.length} 个` : '', failedResults.length > 0 ? `异常 ${failedResults.length} 个` : '', disabledCount > 0 ? `停用 ${disabledCount} 个` : '', - ].filter(Boolean).join(',') + ] + .filter(Boolean) + .join(',') } return `检查 ${checkedResults.length} 个账号,余额均正常${disabledCount > 0 ? `,停用 ${disabledCount} 个` : ''}` @@ -334,7 +333,11 @@ function resolveCloudtentaclesHealthStatus(results: CloudtentaclesHealthAccountR return 'skipped' } - if (checkedResults.some((item) => ['auth_missing', 'source_missing', 'failed'].includes(item.status))) { + if ( + checkedResults.some((item) => + ['auth_missing', 'source_missing', 'failed'].includes(item.status), + ) + ) { return 'failed' } diff --git a/apps/backend/src/services/scheduler/cloudtentacles-number-recycle-service.ts b/apps/backend/src/services/scheduler/cloudtentacles-number-recycle-service.ts index d6c99996..d78cd8bc 100644 --- a/apps/backend/src/services/scheduler/cloudtentacles-number-recycle-service.ts +++ b/apps/backend/src/services/scheduler/cloudtentacles-number-recycle-service.ts @@ -99,7 +99,7 @@ async function clearStaleKuaishouCloudBinding(task: TaskRow, errorMessage: strin errorMessage, actor: { source: 'system' }, }, - now + now, ) } @@ -128,7 +128,7 @@ export async function recycleCloudtentaclesStaleNumbers(): Promise String(item.id || '').trim() === CLOUDTENTACLES_HEALTH_JOB_ID) - || createDefaultCloudtentaclesHealthJob() + return ( + (Array.isArray(config.jobs) ? config.jobs : []).find( + (item) => String(item.id || '').trim() === CLOUDTENTACLES_HEALTH_JOB_ID, + ) || createDefaultCloudtentaclesHealthJob() + ) } export function getWorkOrderTimeoutJob(config: JsonObject = getScheduledJobsConfig()) { - return (Array.isArray(config.jobs) ? config.jobs : []) - .find((item) => String(item.id || '').trim() === WORK_ORDER_TIMEOUT_JOB_ID) - || createDefaultWorkOrderTimeoutJob() + return ( + (Array.isArray(config.jobs) ? config.jobs : []).find( + (item) => String(item.id || '').trim() === WORK_ORDER_TIMEOUT_JOB_ID, + ) || createDefaultWorkOrderTimeoutJob() + ) } export function getDepositUnfreezeJob(config: JsonObject = getScheduledJobsConfig()) { - return (Array.isArray(config.jobs) ? config.jobs : []) - .find((item) => String(item.id || '').trim() === DEPOSIT_UNFREEZE_JOB_ID) - || createDefaultDepositUnfreezeJob() + return ( + (Array.isArray(config.jobs) ? config.jobs : []).find( + (item) => String(item.id || '').trim() === DEPOSIT_UNFREEZE_JOB_ID, + ) || createDefaultDepositUnfreezeJob() + ) } export function getScheduledJobById(jobId: unknown, config: JsonObject = getScheduledJobsConfig()) { - return (Array.isArray(config.jobs) ? config.jobs : []) - .find((item) => String(item.id || '').trim() === String(jobId || '').trim()) || null + return ( + (Array.isArray(config.jobs) ? config.jobs : []).find( + (item) => String(item.id || '').trim() === String(jobId || '').trim(), + ) || null + ) } export function normalizeScheduledJobsConfig(rawValue: unknown) { @@ -159,10 +165,15 @@ function normalizeCloudtentaclesHealthAccounts(config: JsonObject, defaultAssetT .filter(Boolean) as ReturnType[] if (accounts.length === 0) { - accounts.push(normalizeCloudtentaclesHealthAccount({ - sourceKey: config.sourceKey || 'default', - assetThreshold: defaultAssetThreshold, - }, defaultAssetThreshold)) + accounts.push( + normalizeCloudtentaclesHealthAccount( + { + sourceKey: config.sourceKey || 'default', + assetThreshold: defaultAssetThreshold, + }, + defaultAssetThreshold, + ), + ) } const seen = new Set() diff --git a/apps/backend/src/services/scheduler/scheduler-service.ts b/apps/backend/src/services/scheduler/scheduler-service.ts index b3844f6d..622f6007 100644 --- a/apps/backend/src/services/scheduler/scheduler-service.ts +++ b/apps/backend/src/services/scheduler/scheduler-service.ts @@ -1,10 +1,7 @@ import { logError, logInfo, logWarn } from '../../utils/logger.js' import { createHttpError } from '../../utils/http.js' import type { JsonObject } from '../../types/json.js' -import { - getScheduledJobById, - getScheduledJobsConfig, -} from './config-service.js' +import { getScheduledJobById, getScheduledJobsConfig } from './config-service.js' import { runCloudtentaclesHealthJob } from './cloudtentacles-health-job.js' import { runDepositUnfreezeJob } from './deposit-unfreeze-job.js' import { runWorkOrderTimeoutJob } from './work-order-timeout-job.js' @@ -52,7 +49,9 @@ export function getScheduledJobRuntimeStates() { export async function runScheduledJobNow(jobId: unknown) { const config = getScheduledJobsConfig() - const job = config.jobs.find((item) => String(item.id || '').trim() === String(jobId || '').trim()) + const job = config.jobs.find( + (item) => String(item.id || '').trim() === String(jobId || '').trim(), + ) if (!job) { throw createHttpError('定时任务不存在', { @@ -131,8 +130,7 @@ async function runJob(job: JsonObject, { manual = false }: { manual?: boolean } try { const result = await dispatchJob(job) - const summary = - result && typeof result === 'object' ? (result as JsonObject) : {} + const summary = result && typeof result === 'object' ? (result as JsonObject) : {} updateJobState(jobId, { running: false, lastFinishedAt: new Date().toISOString(), diff --git a/apps/backend/src/services/short-links/short-link-service.ts b/apps/backend/src/services/short-links/short-link-service.ts index 9e84252f..5f86fdbf 100644 --- a/apps/backend/src/services/short-links/short-link-service.ts +++ b/apps/backend/src/services/short-links/short-link-service.ts @@ -51,9 +51,10 @@ export async function ensureShortLinkForTarget( } const targetUrlHash = hashTargetUrl(normalizedTargetUrl) const now = nowIso() - const expiresAt = options.expiresAt !== undefined - ? normalizeNullableIso(options.expiresAt) - : addHours(now, DEFAULT_SHORT_LINK_TTL_HOURS) + const expiresAt = + options.expiresAt !== undefined + ? normalizeNullableIso(options.expiresAt) + : addHours(now, DEFAULT_SHORT_LINK_TTL_HOURS) for (let attempt = 1; attempt <= SHORT_LINK_MAX_CREATE_ATTEMPTS; attempt += 1) { try { @@ -241,9 +242,10 @@ function isShortLinkExpired(row: ShortLinkRow): boolean { } function isShortLinkCodeConflict(error: unknown): boolean { - const current = error && typeof error === 'object' - ? error as { code?: unknown; constraint?: unknown; detail?: unknown } - : {} + const current = + error && typeof error === 'object' + ? (error as { code?: unknown; constraint?: unknown; detail?: unknown }) + : {} if (String(current.code || '') !== '23505') { return false } diff --git a/apps/backend/src/services/worker-platform/admin-service.ts b/apps/backend/src/services/worker-platform/admin-service.ts index 715f6cf4..a78922c5 100644 --- a/apps/backend/src/services/worker-platform/admin-service.ts +++ b/apps/backend/src/services/worker-platform/admin-service.ts @@ -87,17 +87,57 @@ import { refreshStoredFileUrl, refreshUploadedFileUrls, } from '../file-storage/file-storage-service.js' -import { - getWorkerFinanceConfig, - saveWorkerFinanceConfig, -} from './worker-finance-config-service.js' +import { getWorkerFinanceConfig, saveWorkerFinanceConfig } from './worker-finance-config-service.js' import { consumeIndustryVouchersBeforeWorkOrderAssign, consumeIndustryVouchersBeforeWorkOrderPublish, } from './publish-voucher-service.js' -import { DEFAULT_CATEGORY_KEY, DEFAULT_DEPOSIT_THRESHOLD_AMOUNT, DEFAULT_LEVEL_KEY, DEFAULT_LEVEL_NAME, mapFinanceRequest, mapWallet, mapWorkCategory, mapWorkOrderAdmin, mapWorkOrderShare, mapWorkProductRule, mapWorkerLevel, mapWorkerUser, normalizeAdminFinanceReviewStatus, normalizeAmountFen, normalizeBoolean, normalizeEnabledStatus, normalizeFinanceRequestStatus, normalizeFinanceRequestType, normalizeInteger, normalizeMatchType, normalizeOptionalId, normalizePositiveInteger, normalizeProblemResolutionAction, normalizeRequirementFields, normalizeRequirementFieldsFromPayload, normalizeReviewStatus, normalizeSessionVersion, normalizeSlugKey, normalizeStatuses, normalizeSubmittedFields, normalizeUploadedFiles, normalizeWorkerType, resolveFreezeDepositAmount, resolveMatchingProductRule, resolveRequirementFields, resolveSkuNameQuantity, resolveWorkerPermissions } from './mappers.js' -import { ensureWorkerPlatformDefaults, getRequiredWorkOrder, getRequiredWorker, normalizeWorkOrderTimeoutPolicy } from './worker-service.js' +import { + DEFAULT_CATEGORY_KEY, + DEFAULT_DEPOSIT_THRESHOLD_AMOUNT, + DEFAULT_LEVEL_KEY, + DEFAULT_LEVEL_NAME, + mapFinanceRequest, + mapWallet, + mapWorkCategory, + mapWorkOrderAdmin, + mapWorkOrderShare, + mapWorkProductRule, + mapWorkerLevel, + mapWorkerUser, + normalizeAdminFinanceReviewStatus, + normalizeAmountFen, + normalizeBoolean, + normalizeEnabledStatus, + normalizeFinanceRequestStatus, + normalizeFinanceRequestType, + normalizeInteger, + normalizeMatchType, + normalizeOptionalId, + normalizePositiveInteger, + normalizeProblemResolutionAction, + normalizeRequirementFields, + normalizeRequirementFieldsFromPayload, + normalizeReviewStatus, + normalizeSessionVersion, + normalizeSlugKey, + normalizeStatuses, + normalizeSubmittedFields, + normalizeUploadedFiles, + normalizeWorkerType, + resolveFreezeDepositAmount, + resolveMatchingProductRule, + resolveRequirementFields, + resolveSkuNameQuantity, + resolveWorkerPermissions, +} from './mappers.js' +import { + ensureWorkerPlatformDefaults, + getRequiredWorkOrder, + getRequiredWorker, + normalizeWorkOrderTimeoutPolicy, +} from './worker-service.js' export async function listAdminWorkerLevels() { await ensureWorkerPlatformDefaults() @@ -229,10 +269,7 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) { payload.unitPrice === undefined && payload.unitPriceYuan === undefined ? 0 : normalizeAmountFen(payload.unitPrice ?? payload.unitPriceYuan, 0) - const sharingEnabled = normalizeBoolean( - payload.sharingEnabled ?? payload.sharing_enabled, - false, - ) + const sharingEnabled = normalizeBoolean(payload.sharingEnabled ?? payload.sharing_enabled, false) const sharingUnitReward = normalizeAmountFen( payload.sharingUnitReward ?? payload.sharingUnitRewardYuan, 0, @@ -337,9 +374,7 @@ export async function listAdminWorkerUsers(query: JsonObject = {}) { status: String(query.status || '').trim(), keyword: String(query.keyword || '').trim(), }) - const timeoutCounts = await countTimeoutEventsByWorkerIds( - items.map((item) => Number(item.id)), - ) + const timeoutCounts = await countTimeoutEventsByWorkerIds(items.map((item) => Number(item.id))) return { items: items.map((item) => { const worker = mapWorkerUser(item) @@ -389,10 +424,7 @@ export async function assignAdminWorkOrderToWorker( payload: JsonObject = {}, ) { const workOrder = await getRequiredWorkOrder(workOrderId) - if ( - !['unassigned', 'open'].includes(workOrder.status) || - workOrder.assigned_worker_id - ) { + if (!['unassigned', 'open'].includes(workOrder.status) || workOrder.assigned_worker_id) { throw createHttpError('只有未分配或待抢单且未指派打手的订单才能指派', { statusCode: 409, errorCode: 'work_order_not_assignable', @@ -627,9 +659,7 @@ export async function listAdminWorkOrders(query: JsonObject = {}) { const joinedQuantity = orderShares .filter((share) => share.status !== 'cancelled') .reduce((sum, share) => sum + Number(share.quantity || 0), 0) - const pendingSubmissionCount = orderShares.filter( - (share) => share.status === 'joined', - ).length + const pendingSubmissionCount = orderShares.filter((share) => share.status === 'joined').length return { ...mapWorkOrderAdmin(item), pendingUnfreezeAmount: pendingUnfreezeByOrderId.get(Number(item.id)) || 0, @@ -713,7 +743,13 @@ export async function updateAdminWorkOrderSharing( payload: JsonObject = {}, ) { const workOrder = await getRequiredWorkOrder(workOrderId) - if (![WORK_ORDER_STATUS.PENDING_MATERIAL, WORK_ORDER_STATUS.UNASSIGNED, WORK_ORDER_STATUS.OPEN].includes(workOrder.status as never)) { + if ( + ![ + WORK_ORDER_STATUS.PENDING_MATERIAL, + WORK_ORDER_STATUS.UNASSIGNED, + WORK_ORDER_STATUS.OPEN, + ].includes(workOrder.status as never) + ) { throw createHttpError('只有待完善、未分配或待抢单的工单可以配置拼单', { statusCode: 409, errorCode: 'work_order_sharing_config_status_invalid', @@ -787,9 +823,7 @@ export async function submitAdminWorkOrderMaterial( WORK_ORDER_STATUS.PENDING_MATERIAL, WORK_ORDER_STATUS.UNASSIGNED, WORK_ORDER_STATUS.IN_PROGRESS, - ].includes( - workOrder.status as never, - ) + ].includes(workOrder.status as never) ) { throw createHttpError('当前状态不能更新订单资料', { statusCode: 409, @@ -985,14 +1019,12 @@ export async function updateAdminWorkOrder( const categoryId = payload.categoryId === undefined || payload.categoryId === null || payload.categoryId === '' ? workOrder.category_id - : normalizeOptionalId(payload.categoryId) ?? workOrder.category_id + : (normalizeOptionalId(payload.categoryId) ?? workOrder.category_id) const now = nowIso() const updated = await updateWorkOrderBasic(workOrder.id, { productName: String(payload.productName ?? workOrder.product_name).trim(), - platformOrderId: String( - payload.platformOrderId ?? workOrder.platform_order_id ?? '', - ).trim(), + platformOrderId: String(payload.platformOrderId ?? workOrder.platform_order_id ?? '').trim(), categoryId, rewardAmount, requiredDepositAmount, @@ -1298,7 +1330,9 @@ export async function acceptAdminWorkOrders(payload: JsonObject = {}) { }) } - const workOrders = await Promise.all(workOrderIds.map((workOrderId) => getRequiredWorkOrder(workOrderId))) + const workOrders = await Promise.all( + workOrderIds.map((workOrderId) => getRequiredWorkOrder(workOrderId)), + ) if (workOrders.some((workOrder) => workOrder.status !== WORK_ORDER_STATUS.PENDING_ACCEPTANCE)) { throw createHttpError('选中的订单包含非待验收状态,请刷新后重试', { statusCode: 409, @@ -1325,10 +1359,7 @@ export async function acceptAdminWorkOrders(payload: JsonObject = {}) { export async function unassignAdminWorkOrder(workOrderId: number | string) { const workOrder = await getRequiredWorkOrder(workOrderId) - if ( - workOrder.status !== WORK_ORDER_STATUS.IN_PROGRESS || - !workOrder.assigned_worker_id - ) { + if (workOrder.status !== WORK_ORDER_STATUS.IN_PROGRESS || !workOrder.assigned_worker_id) { throw createHttpError('只有进行中且已指派打手的订单可以取消指派', { statusCode: 409, errorCode: 'work_order_unassign_status_invalid', @@ -1360,15 +1391,9 @@ export async function unassignAdminWorkOrder(workOrderId: number | string) { return { order: mapWorkOrderAdmin(updated) } } -export async function cancelAdminWorkOrder( - workOrderId: number | string, - payload: JsonObject = {}, -) { +export async function cancelAdminWorkOrder(workOrderId: number | string, payload: JsonObject = {}) { const workOrder = await getRequiredWorkOrder(workOrderId) - if ( - workOrder.status !== WORK_ORDER_STATUS.IN_PROGRESS || - !workOrder.assigned_worker_id - ) { + if (workOrder.status !== WORK_ORDER_STATUS.IN_PROGRESS || !workOrder.assigned_worker_id) { throw createHttpError('只有代练中且已接单的订单可以撤单', { statusCode: 409, errorCode: 'work_order_cancel_status_invalid', diff --git a/apps/backend/src/services/worker-platform/mappers.ts b/apps/backend/src/services/worker-platform/mappers.ts index 1e6be415..d2fcfa89 100644 --- a/apps/backend/src/services/worker-platform/mappers.ts +++ b/apps/backend/src/services/worker-platform/mappers.ts @@ -78,12 +78,7 @@ import { refreshStoredFileUrl, refreshUploadedFileUrls, } from '../file-storage/file-storage-service.js' -import { - getWorkerFinanceConfig, - saveWorkerFinanceConfig, -} from './worker-finance-config-service.js' - - +import { getWorkerFinanceConfig, saveWorkerFinanceConfig } from './worker-finance-config-service.js' export const DEFAULT_LEVEL_KEY = 'vip1' export const DEFAULT_LEVEL_NAME = 'VIP1' @@ -309,9 +304,7 @@ export function mapWalletLedger(ledger: WorkerWalletLedgerRow | null | undefined amount: Number(ledger.amount || 0), balanceAfter: Number(ledger.balance_after || 0), frozenAfter: Number(ledger.frozen_after || 0), - relatedWorkOrderId: ledger.related_work_order_id - ? Number(ledger.related_work_order_id) - : null, + relatedWorkOrderId: ledger.related_work_order_id ? Number(ledger.related_work_order_id) : null, auditStatus: ledger.audit_status || '', note: ledger.note || '', payload: safeParseJson(ledger.payload_json), @@ -536,10 +529,38 @@ export function normalizeRequirementFields(value: unknown) { return fields } return [ - { key: 'gameId', label: '游戏编号', required: true, type: 'text', options: [], mockValue: 'test_uid' }, - { key: 'gameNickname', label: '游戏昵称', required: true, type: 'text', options: [], mockValue: '测试角色' }, - { key: 'system', label: '系统', required: false, type: 'select', options: ['安卓', '苹果'], mockValue: '安卓' }, - { key: 'serverZone', label: '区服', required: false, type: 'select', options: ['QQ区', '微信区'], mockValue: 'QQ区' }, + { + key: 'gameId', + label: '游戏编号', + required: true, + type: 'text', + options: [], + mockValue: 'test_uid', + }, + { + key: 'gameNickname', + label: '游戏昵称', + required: true, + type: 'text', + options: [], + mockValue: '测试角色', + }, + { + key: 'system', + label: '系统', + required: false, + type: 'select', + options: ['安卓', '苹果'], + mockValue: '安卓', + }, + { + key: 'serverZone', + label: '区服', + required: false, + type: 'select', + options: ['QQ区', '微信区'], + mockValue: 'QQ区', + }, ] } @@ -685,9 +706,7 @@ export function resolveSkuNameQuantity(productName: unknown): number { if (Number.isSafeInteger(parsed) && parsed > 0) return parsed } - const chineseMatch = text.match( - /([零一二三四五六七八九十百千两]+)\s*(?:个|份|张|枚)/, - ) + const chineseMatch = text.match(/([零一二三四五六七八九十百千两]+)\s*(?:个|份|张|枚)/) if (chineseMatch?.[1]) { const parsed = parseChineseNumber(chineseMatch[1]) if (parsed > 0) return parsed @@ -730,7 +749,8 @@ function parseChineseNumber(text: string): number { return section + digit } -export function matchesOptionalText(ruleValue: unknown, sourceValue: unknown) { const ruleText = String(ruleValue || '') +export function matchesOptionalText(ruleValue: unknown, sourceValue: unknown) { + const ruleText = String(ruleValue || '') .trim() .toLowerCase() if (!ruleText) { @@ -771,13 +791,7 @@ export function normalizeStatuses(value: unknown): string[] { : value === undefined || value === null ? [] : [value] - return [ - ...new Set( - rawList - .map((item) => String(item || '').trim()) - .filter(Boolean), - ), - ] + return [...new Set(rawList.map((item) => String(item || '').trim()).filter(Boolean))] } export function normalizeUploadedFiles(value: unknown) { diff --git a/apps/backend/src/services/worker-platform/worker-finance-config-service.ts b/apps/backend/src/services/worker-platform/worker-finance-config-service.ts index d1daf27b..f245cb01 100644 --- a/apps/backend/src/services/worker-platform/worker-finance-config-service.ts +++ b/apps/backend/src/services/worker-platform/worker-finance-config-service.ts @@ -61,9 +61,7 @@ export function normalizeWorkerFinanceConfig(rawValue: unknown): WorkerFinanceCo channelName: String(recharge.channelName || recharge.channel_name || '').trim(), accountName: String(recharge.accountName || recharge.account_name || '').trim(), accountNo: String(recharge.accountNo || recharge.account_no || '').trim(), - qrCodeImage: normalizeUploadedFileForStorage( - recharge.qrCodeImage || recharge.qr_code_image, - ), + qrCodeImage: normalizeUploadedFileForStorage(recharge.qrCodeImage || recharge.qr_code_image), instructions: String(recharge.instructions || '').trim(), }, adminContact: { @@ -144,10 +142,7 @@ function normalizeUploadedFileForStorage( return { objectKey, url: normalizeStoredFileUrl(value.url, objectKey), - thumbnailUrl: normalizeStoredFileUrl( - value.thumbnailUrl || value.thumbnail_url, - objectKey, - ), + thumbnailUrl: normalizeStoredFileUrl(value.thumbnailUrl || value.thumbnail_url, objectKey), mediumUrl: normalizeStoredFileUrl(value.mediumUrl || value.medium_url, objectKey), filename: String(value.filename || '').trim() || fallbackFilename, contentType: String(value.contentType || value.content_type || '').trim(), diff --git a/apps/backend/src/services/worker-platform/worker-platform-service.test.ts b/apps/backend/src/services/worker-platform/worker-platform-service.test.ts index d396bc9e..58ecff87 100644 --- a/apps/backend/src/services/worker-platform/worker-platform-service.test.ts +++ b/apps/backend/src/services/worker-platform/worker-platform-service.test.ts @@ -39,9 +39,9 @@ test('assertWorkerLoginAllowed blocks rejected (frozen) workers with worker_reje (error: unknown) => Boolean( error && - typeof error === 'object' && - (error as { statusCode?: number }).statusCode === 403 && - (error as { errorCode?: string }).errorCode === 'worker_rejected', + typeof error === 'object' && + (error as { statusCode?: number }).statusCode === 403 && + (error as { errorCode?: string }).errorCode === 'worker_rejected', ), ) }) @@ -52,9 +52,9 @@ test('assertWorkerLoginAllowed blocks pending review workers with worker_pending (error: unknown) => Boolean( error && - typeof error === 'object' && - (error as { statusCode?: number }).statusCode === 403 && - (error as { errorCode?: string }).errorCode === 'worker_pending_review', + typeof error === 'object' && + (error as { statusCode?: number }).statusCode === 403 && + (error as { errorCode?: string }).errorCode === 'worker_pending_review', ), ) }) @@ -65,9 +65,9 @@ test('assertWorkerLoginAllowed blocks disabled workers with worker_disabled', () (error: unknown) => Boolean( error && - typeof error === 'object' && - (error as { statusCode?: number }).statusCode === 403 && - (error as { errorCode?: string }).errorCode === 'worker_disabled', + typeof error === 'object' && + (error as { statusCode?: number }).statusCode === 403 && + (error as { errorCode?: string }).errorCode === 'worker_disabled', ), ) }) @@ -119,10 +119,7 @@ test('resolveSkuNameQuantity falls back to 1 when no quantity is found', () => { assert.equal(resolveSkuNameQuantity('指挥官秘钥礼包'), 1) }) - -function buildWorkOrderRow( - overrides: Partial = {}, -): WorkOrderRow { +function buildWorkOrderRow(overrides: Partial = {}): WorkOrderRow { return { id: 1, work_order_no: 'WO0001', @@ -168,9 +165,9 @@ test('resolveCollectSubmitTargetWorkOrder requires explicit work order id when m (error: unknown) => Boolean( error && - typeof error === 'object' && - (error as { statusCode?: number }).statusCode === 409 && - (error as { errorCode?: string }).errorCode === 'collect_work_order_required', + typeof error === 'object' && + (error as { statusCode?: number }).statusCode === 409 && + (error as { errorCode?: string }).errorCode === 'collect_work_order_required', ), ) }) @@ -193,9 +190,9 @@ test('resolveCollectSubmitTargetWorkOrder rejects unknown workOrderId', () => { (error: unknown) => Boolean( error && - typeof error === 'object' && - (error as { statusCode?: number }).statusCode === 404 && - (error as { errorCode?: string }).errorCode === 'collect_work_order_not_found', + typeof error === 'object' && + (error as { statusCode?: number }).statusCode === 404 && + (error as { errorCode?: string }).errorCode === 'collect_work_order_not_found', ), ) }) diff --git a/apps/backend/src/services/worker-platform/worker-service.ts b/apps/backend/src/services/worker-platform/worker-service.ts index 56d8310d..4d1794ad 100644 --- a/apps/backend/src/services/worker-platform/worker-service.ts +++ b/apps/backend/src/services/worker-platform/worker-service.ts @@ -96,12 +96,52 @@ import { refreshStoredFileUrl, refreshUploadedFileUrls, } from '../file-storage/file-storage-service.js' -import { - getWorkerFinanceConfig, - saveWorkerFinanceConfig, -} from './worker-finance-config-service.js' +import { getWorkerFinanceConfig, saveWorkerFinanceConfig } from './worker-finance-config-service.js' -import { DEFAULT_CATEGORY_KEY, DEFAULT_LEVEL_KEY, DEFAULT_LEVEL_NAME, INVITE_CODE_LENGTH, INVITE_CODE_RETRY_LIMIT, createWorkerSession, ensureWorkerAuthConfigured, hashWorkerPassword, isIgnorableWorkerAuthError, mapCollectLookupOrder, mapFinanceRequest, mapWalletLedger, mapWorkCategory, mapWorkOrderForWorker, mapWorkOrderPublic, mapWorkOrderShare, mapWorkerUser, normalizeAmountFen, normalizeFinanceRequestStatus, normalizeFinanceRequestType, normalizeInteger, normalizeOptionalId, normalizePassword, normalizePositiveInteger, normalizeProofFiles, normalizeSessionVersion, normalizeStatuses, normalizeStringArray, normalizeSubmittedFields, normalizeUploadedFiles, normalizeUsername, normalizeWalletLedgerType, normalizeWithdrawChannel, resolveFreezeDepositAmount, resolveRequirementFields, resolveWorkerPermissions, safeCompare, signWorkerPayload, throwGrabWorkOrderFailure, validateWorkerPassword, validateWorkerUsername, verifyWorkerPassword } from './mappers.js' +import { + DEFAULT_CATEGORY_KEY, + DEFAULT_LEVEL_KEY, + DEFAULT_LEVEL_NAME, + INVITE_CODE_LENGTH, + INVITE_CODE_RETRY_LIMIT, + createWorkerSession, + ensureWorkerAuthConfigured, + hashWorkerPassword, + isIgnorableWorkerAuthError, + mapCollectLookupOrder, + mapFinanceRequest, + mapWalletLedger, + mapWorkCategory, + mapWorkOrderForWorker, + mapWorkOrderPublic, + mapWorkOrderShare, + mapWorkerUser, + normalizeAmountFen, + normalizeFinanceRequestStatus, + normalizeFinanceRequestType, + normalizeInteger, + normalizeOptionalId, + normalizePassword, + normalizePositiveInteger, + normalizeProofFiles, + normalizeSessionVersion, + normalizeStatuses, + normalizeStringArray, + normalizeSubmittedFields, + normalizeUploadedFiles, + normalizeUsername, + normalizeWalletLedgerType, + normalizeWithdrawChannel, + resolveFreezeDepositAmount, + resolveRequirementFields, + resolveWorkerPermissions, + safeCompare, + signWorkerPayload, + throwGrabWorkOrderFailure, + validateWorkerPassword, + validateWorkerUsername, + verifyWorkerPassword, +} from './mappers.js' export type WorkerSession = { sessionId: string @@ -112,7 +152,6 @@ export type WorkerSession = { sessionVersion: number } - export async function ensureWorkerPlatformDefaults() { const now = nowIso() const existingLevel = await getWorkerLevelByKey(DEFAULT_LEVEL_KEY) @@ -196,7 +235,9 @@ export async function registerWorker(payload: JsonObject = {}) { const code = String(payload.code || payload.smsCode || '').trim() const password = normalizePassword(payload.password) const displayName = String(payload.displayName || payload.nickname || '').trim() - const inviteCode = String(payload.inviteCode || '').trim().toUpperCase() + const inviteCode = String(payload.inviteCode || '') + .trim() + .toUpperCase() validateWorkerPassword(password) if (!displayName) { @@ -215,9 +256,9 @@ export async function registerWorker(payload: JsonObject = {}) { const latest = await getLatestWorkerSmsCode(phone, 'register') const codeValid = Boolean( latest && - !latest.consumed_at && - new Date(latest.expires_at).getTime() > Date.now() && - latest.code_hash === hashSmsCode(code), + !latest.consumed_at && + new Date(latest.expires_at).getTime() > Date.now() && + latest.code_hash === hashSmsCode(code), ) if (!codeValid) { throw createHttpError('短信验证码错误或已过期', { @@ -308,7 +349,10 @@ function generateSmsCode(): string { } function hashSmsCode(code: string): string { - return crypto.createHash('sha256').update(String(code || '')).digest('hex') + return crypto + .createHash('sha256') + .update(String(code || '')) + .digest('hex') } async function generateInviteCode(now: string): Promise { @@ -495,13 +539,14 @@ export function requireActiveWorkerSession(session: WorkerSession | null | undef export async function getWorkerProfile(session: WorkerSession) { const worker = await getRequiredWorker(session.workerId) - const [financeSummary, acceptedOrderCount, activeOrderCount, timeoutOrderCount, financeConfig] = await Promise.all([ - getWorkerFinanceRequestSummary(session.workerId), - countWorkerAcceptedOrders(session.workerId), - countWorkerActiveOrders(session.workerId), - countWorkerTimeoutEvents(session.workerId), - Promise.resolve(getWorkerFinanceConfig()), - ]) + const [financeSummary, acceptedOrderCount, activeOrderCount, timeoutOrderCount, financeConfig] = + await Promise.all([ + getWorkerFinanceRequestSummary(session.workerId), + countWorkerAcceptedOrders(session.workerId), + countWorkerActiveOrders(session.workerId), + countWorkerTimeoutEvents(session.workerId), + Promise.resolve(getWorkerFinanceConfig()), + ]) const permissions = resolveWorkerPermissions(worker) const levelProgress = await resolveLevelProgress(worker, acceptedOrderCount) return { @@ -533,9 +578,16 @@ async function resolveLevelProgress( const active = levels.filter((level) => level.status === 'active') const current = active.find((level) => Number(level.id) === Number(worker.level_id)) if (!current) return null - const currentThreshold = normalizeInteger(safeParseJson(current.permission_json).upgradeThreshold, 0) + const currentThreshold = normalizeInteger( + safeParseJson(current.permission_json).upgradeThreshold, + 0, + ) const next = active - .filter((level) => normalizeInteger(safeParseJson(level.permission_json).upgradeThreshold, 0) > currentThreshold) + .filter( + (level) => + normalizeInteger(safeParseJson(level.permission_json).upgradeThreshold, 0) > + currentThreshold, + ) .sort( (a, b) => normalizeInteger(safeParseJson(a.permission_json).upgradeThreshold, 0) - @@ -692,9 +744,7 @@ export async function createWorkerWithdrawRequest( }) } - const accountChannel = normalizeWithdrawChannel( - payload.accountChannel || payload.channel, - ) + const accountChannel = normalizeWithdrawChannel(payload.accountChannel || payload.channel) const accountName = String(payload.accountName || payload.realName || '').trim() const accountNo = String(payload.accountNo || payload.account || '').trim() if (!accountName) { @@ -712,10 +762,7 @@ export async function createWorkerWithdrawRequest( const financeSummary = await getWorkerFinanceRequestSummary(worker.id) const availableAmount = Number(worker.available_amount || 0) - const availableForWithdraw = Math.max( - 0, - availableAmount - financeSummary.pendingWithdrawAmount, - ) + const availableForWithdraw = Math.max(0, availableAmount - financeSummary.pendingWithdrawAmount) if (amount > availableForWithdraw) { throw createHttpError('可提现余额不足,请先减少提现金额或等待已提交申请处理', { statusCode: 409, @@ -751,14 +798,9 @@ export async function createWorkerWithdrawRequest( } } -export async function changeWorkerPassword( - payload: JsonObject = {}, - session: WorkerSession, -) { +export async function changeWorkerPassword(payload: JsonObject = {}, session: WorkerSession) { const worker = await getRequiredWorker(session.workerId) - const currentPassword = normalizePassword( - payload.currentPassword || payload.oldPassword, - ) + const currentPassword = normalizePassword(payload.currentPassword || payload.oldPassword) const nextPassword = normalizePassword(payload.newPassword || payload.password) if (!currentPassword) { @@ -814,9 +856,7 @@ export async function listWorkerHallOrders(query: JsonObject = {}, session: Work const permissions = resolveWorkerPermissions(worker) const visibleDelaySeconds = Number(permissions.visibleDelaySeconds || 0) const visibleAfterIso = - visibleDelaySeconds > 0 - ? new Date(Date.now() - visibleDelaySeconds * 1000).toISOString() - : '' + visibleDelaySeconds > 0 ? new Date(Date.now() - visibleDelaySeconds * 1000).toISOString() : '' const { items, total } = await listWorkOrders({ page, pageSize, @@ -902,10 +942,7 @@ export async function grabWorkerHallOrder(workOrderId: number | string, session: return { order: mapWorkOrderForWorker(grabbed.order, permissions) } } -function resolveWorkOrderDeadlineAt( - workOrder: WorkOrderRow, - now: string, -): string | null { +function resolveWorkOrderDeadlineAt(workOrder: WorkOrderRow, now: string): string | null { const timeoutMinutes = Number(workOrder.timeout_minutes || 0) if (timeoutMinutes <= 0) return null return new Date(new Date(now).getTime() + timeoutMinutes * 60 * 1000).toISOString() @@ -947,17 +984,14 @@ export async function settleDueDepositUnfreezes( function resolveChinaDayRange(): { start: string; end: string } { const utcNow = new Date() const utcDayStartMs = - Date.UTC(utcNow.getUTCFullYear(), utcNow.getUTCMonth(), utcNow.getUTCDate()) - - 8 * 3_600_000 + Date.UTC(utcNow.getUTCFullYear(), utcNow.getUTCMonth(), utcNow.getUTCDate()) - 8 * 3_600_000 return { start: new Date(utcDayStartMs).toISOString(), end: new Date(utcDayStartMs + 86_400_000).toISOString(), } } -export async function settleOverdueWorkOrders( - options: { limit?: number; workerId?: number } = {}, -) { +export async function settleOverdueWorkOrders(options: { limit?: number; workerId?: number } = {}) { const overdue = await listOverdueWorkOrders({ limit: Math.max(1, Number(options.limit || 50)), workerId: Number(options.workerId || 0), @@ -999,9 +1033,7 @@ export async function listWorkerMyOrders(query: JsonObject = {}, session: Worker }) const permissions = resolveWorkerPermissions(worker) const myShares = await listWorkerSharesByWorker(worker.id) - const myShareByOrderId = new Map( - myShares.map((share) => [Number(share.work_order_id), share]), - ) + const myShareByOrderId = new Map(myShares.map((share) => [Number(share.work_order_id), share])) const notes = await listWorkerWorkOrderNotes( worker.id, items.map((item) => Number(item.id)), @@ -1089,7 +1121,14 @@ export async function joinWorkerSharingOrder( } function resolveWorkOrderShareJoinError( - failureReason: 'work_order_not_open' | 'sharing_disabled' | 'work_order_owner_conflict' | 'sharing_quantity_full' | 'worker_active_order_limit' | 'worker_deposit_insufficient' | null, + failureReason: + | 'work_order_not_open' + | 'sharing_disabled' + | 'work_order_owner_conflict' + | 'sharing_quantity_full' + | 'worker_active_order_limit' + | 'worker_deposit_insufficient' + | null, ): Error { const messages: Record = { work_order_not_open: '该订单当前不可拼单', @@ -1215,10 +1254,7 @@ export async function submitWorkerOrderAcceptance( } } return { - order: mapWorkOrderForWorker( - updated || workOrder, - resolveWorkerPermissions(worker), - ), + order: mapWorkOrderForWorker(updated || workOrder, resolveWorkerPermissions(worker)), } } diff --git a/apps/backend/src/startup/bootstrap.ts b/apps/backend/src/startup/bootstrap.ts index 73a3eec6..45fee38e 100644 --- a/apps/backend/src/startup/bootstrap.ts +++ b/apps/backend/src/startup/bootstrap.ts @@ -1,71 +1,70 @@ -import { runDatabaseMigrations } from "../db/migrate.js"; -import { ensureAdminUsersBootstrapped } from "../services/admin/admin-auth-service.js"; -import { ensureFulfillmentCatalogBootstrapped } from "../services/bootstrap/fulfillment-bootstrap-service.js"; -import { migrateJsonConfigFilesToDatabase } from "../services/config/json-config-migration-service.js"; -import { startKuaishouIndustrySendCallbackRetryWorker } from "../services/platforms/kuaishou-industry/send-code-service.js"; -import { startScheduledJobs } from "../services/scheduler/scheduler-service.js"; -import { logError, logInfo } from "../utils/logger.js"; -import { formatStartupError, type StartupState } from "./state.js"; +import { runDatabaseMigrations } from '../db/migrate.js' +import { ensureAdminUsersBootstrapped } from '../services/admin/admin-auth-service.js' +import { ensureFulfillmentCatalogBootstrapped } from '../services/bootstrap/fulfillment-bootstrap-service.js' +import { migrateJsonConfigFilesToDatabase } from '../services/config/json-config-migration-service.js' +import { startKuaishouIndustrySendCallbackRetryWorker } from '../services/platforms/kuaishou-industry/send-code-service.js' +import { startScheduledJobs } from '../services/scheduler/scheduler-service.js' +import { logError, logInfo } from '../utils/logger.js' +import { formatStartupError, type StartupState } from './state.js' -const CORE_BOOT_RETRY_DELAY_MS = 5_000; +const CORE_BOOT_RETRY_DELAY_MS = 5_000 export async function bootstrapCoreServices( startupState: StartupState, - isShutdownStarted: () => boolean + isShutdownStarted: () => boolean, ) { if (startupState.core.running || isShutdownStarted()) { - return; + return } - startupState.core.running = true; + startupState.core.running = true while (!isShutdownStarted() && !startupState.core.ready) { - startupState.phase = - startupState.core.attemptCount === 0 ? "starting" : "retrying"; - startupState.core.attemptCount += 1; - startupState.core.lastAttemptAt = new Date().toISOString(); + startupState.phase = startupState.core.attemptCount === 0 ? 'starting' : 'retrying' + startupState.core.attemptCount += 1 + startupState.core.lastAttemptAt = new Date().toISOString() try { - logInfo("[startup]", "开始执行快手核心启动步骤", { + logInfo('[startup]', '开始执行快手核心启动步骤', { attempt: startupState.core.attemptCount, - }); + }) - await runDatabaseMigrations(); - await migrateJsonConfigFilesToDatabase(); - await ensureFulfillmentCatalogBootstrapped(); - await ensureAdminUsersBootstrapped(); + await runDatabaseMigrations() + await migrateJsonConfigFilesToDatabase() + await ensureFulfillmentCatalogBootstrapped() + await ensureAdminUsersBootstrapped() - startupState.core.ready = true; - startupState.core.lastError = ""; - startupState.phase = "ready"; - startupState.core.readyAt = new Date().toISOString(); + startupState.core.ready = true + startupState.core.lastError = '' + startupState.phase = 'ready' + startupState.core.readyAt = new Date().toISOString() - logInfo("[startup]", "快手核心启动步骤完成,服务已就绪", { + logInfo('[startup]', '快手核心启动步骤完成,服务已就绪', { attempt: startupState.core.attemptCount, - }); + }) - startScheduledJobs(); - startKuaishouIndustrySendCallbackRetryWorker(); - break; + startScheduledJobs() + startKuaishouIndustrySendCallbackRetryWorker() + break } catch (error) { - const message = formatStartupError(error); - startupState.phase = "retrying"; - startupState.core.lastError = message; - logError("[startup]", "快手核心启动步骤失败,将自动重试", { + const message = formatStartupError(error) + startupState.phase = 'retrying' + startupState.core.lastError = message + logError('[startup]', '快手核心启动步骤失败,将自动重试', { attempt: startupState.core.attemptCount, retryDelayMs: CORE_BOOT_RETRY_DELAY_MS, error, - }); + }) - await sleep(CORE_BOOT_RETRY_DELAY_MS); + await sleep(CORE_BOOT_RETRY_DELAY_MS) } } - startupState.core.running = false; + startupState.core.running = false } function sleep(ms: number) { return new Promise((resolve) => { - setTimeout(resolve, ms); - }); + setTimeout(resolve, ms) + }) } diff --git a/apps/backend/src/startup/shutdown.ts b/apps/backend/src/startup/shutdown.ts index 80ee4ded..07254a02 100644 --- a/apps/backend/src/startup/shutdown.ts +++ b/apps/backend/src/startup/shutdown.ts @@ -1,46 +1,43 @@ -import type { Server } from "node:http"; +import type { Server } from 'node:http' -import { stopKuaishouIndustrySendCallbackRetryWorker } from "../services/platforms/kuaishou-industry/send-code-service.js"; -import { stopScheduledJobs } from "../services/scheduler/scheduler-service.js"; -import { logError, logInfo } from "../utils/logger.js"; -import type { StartupState } from "./state.js"; +import { stopKuaishouIndustrySendCallbackRetryWorker } from '../services/platforms/kuaishou-industry/send-code-service.js' +import { stopScheduledJobs } from '../services/scheduler/scheduler-service.js' +import { logError, logInfo } from '../utils/logger.js' +import type { StartupState } from './state.js' -export function createShutdownController( - server: Server, - startupState: StartupState -) { - let shutdownStarted = false; +export function createShutdownController(server: Server, startupState: StartupState) { + let shutdownStarted = false async function shutdown(signal: string) { if (shutdownStarted) { - return; + return } - shutdownStarted = true; - startupState.phase = "shutting_down"; - logInfo("[shutdown]", `received ${signal}, closing HTTP server`); + shutdownStarted = true + startupState.phase = 'shutting_down' + logInfo('[shutdown]', `received ${signal}, closing HTTP server`) - stopScheduledJobs(); - stopKuaishouIndustrySendCallbackRetryWorker(); + stopScheduledJobs() + stopKuaishouIndustrySendCallbackRetryWorker() if (!server.listening) { - return; + return } await new Promise((resolve) => { server.close((error) => { if (error) { - logError("[shutdown]", "failed to close HTTP server", error); - process.exitCode = 1; + logError('[shutdown]', 'failed to close HTTP server', error) + process.exitCode = 1 } - resolve(); - }); - }); + resolve() + }) + }) } return { isShutdownStarted: () => shutdownStarted, shutdown, - }; + } } diff --git a/apps/backend/src/startup/state.ts b/apps/backend/src/startup/state.ts index 9cc3c531..c4575dd6 100644 --- a/apps/backend/src/startup/state.ts +++ b/apps/backend/src/startup/state.ts @@ -1,42 +1,42 @@ type StartupProcessError = { - time: string; - message: string; -}; + time: string + message: string +} export type StartupState = { - phase: string; - startedAt: string; + phase: string + startedAt: string core: { - ready: boolean; - running: boolean; - attemptCount: number; - readyAt: string; - lastAttemptAt: string; - lastError: string; - }; + ready: boolean + running: boolean + attemptCount: number + readyAt: string + lastAttemptAt: string + lastError: string + } process: { - lastUnhandledRejection: StartupProcessError | null; - lastUncaughtException: StartupProcessError | null; - }; -}; + lastUnhandledRejection: StartupProcessError | null + lastUncaughtException: StartupProcessError | null + } +} export function createStartupState(): StartupState { return { - phase: "starting", + phase: 'starting', startedAt: new Date().toISOString(), core: { ready: false, running: false, attemptCount: 0, - readyAt: "", - lastAttemptAt: "", - lastError: "", + readyAt: '', + lastAttemptAt: '', + lastError: '', }, process: { lastUnhandledRejection: null, lastUncaughtException: null, }, - }; + } } export function buildHealthPayload(startupState: StartupState, shutdownStarted: boolean) { @@ -57,13 +57,13 @@ export function buildHealthPayload(startupState: StartupState, shutdownStarted: lastUnhandledRejection: startupState.process.lastUnhandledRejection, lastUncaughtException: startupState.process.lastUncaughtException, }, - }; + } } export function formatStartupError(error: unknown) { if (error instanceof Error && error.message) { - return error.message.split("\n")[0]?.trim() || "未知错误"; + return error.message.split('\n')[0]?.trim() || '未知错误' } - return String(error || "未知错误").trim(); + return String(error || '未知错误').trim() } diff --git a/apps/backend/src/types/admin/route-inputs.ts b/apps/backend/src/types/admin/route-inputs.ts index 19f7cb5c..548c3f41 100644 --- a/apps/backend/src/types/admin/route-inputs.ts +++ b/apps/backend/src/types/admin/route-inputs.ts @@ -37,11 +37,13 @@ export type AdminTaskRouteQuery = AdminTaskListQueryInput export type AdminRouteAdminSession = AdminViewerSessionInput export type AdminKuaishouIndustrySourceConfigRouteBody = AdminKuaishouIndustrySourceConfigInput -export type AdminKuaishouIndustryAuthorizationCodeRouteBody = AdminKuaishouIndustryAuthorizationCodeInput +export type AdminKuaishouIndustryAuthorizationCodeRouteBody = + AdminKuaishouIndustryAuthorizationCodeInput export type AdminKuaishouIndustryRefundListRouteBody = AdminKuaishouIndustryRefundListInput export type AdminKuaishouIndustryRefundApproveRouteBody = AdminKuaishouIndustryRefundApproveInput export type AdminKuaishouIndustryRefundDisagreeRouteBody = AdminKuaishouIndustryRefundDisagreeInput -export type AdminKuaishouIndustryVoucherCheckAvailableRouteBody = AdminKuaishouIndustryVoucherCheckAvailableInput +export type AdminKuaishouIndustryVoucherCheckAvailableRouteBody = + AdminKuaishouIndustryVoucherCheckAvailableInput export type AdminKuaishouIndustryVoucherReverseRouteBody = AdminKuaishouIndustryVoucherReverseInput export type AdminKuaishouIndustryVoucherConsumeRouteBody = AdminKuaishouIndustryVoucherConsumeInput export type AdminKuaishouIndustryVoucherResendRouteBody = AdminKuaishouIndustryVoucherResendInput @@ -50,12 +52,14 @@ export type AdminNotificationConfigRouteBody = AdminNotificationConfigInput export type AdminNotificationTestRouteBody = AdminNotificationTestInput export type AdminScheduledJobsConfigRouteBody = AdminScheduledJobsConfigInput export type AdminCloudtentaclesSourceConfigRouteBody = AdminCloudtentaclesSourceConfigInput -export type AdminCloudtentaclesOverrideRuleConfigRouteBody = AdminCloudtentaclesOverrideRuleConfigInput +export type AdminCloudtentaclesOverrideRuleConfigRouteBody = + AdminCloudtentaclesOverrideRuleConfigInput export type AdminCloudtentaclesSendSmsCodeRouteBody = AdminCloudtentaclesSendSmsCodeInput export type AdminCloudtentaclesTestLoginRouteBody = AdminCloudtentaclesTestLoginInput export type AdminCloudtentaclesValidateSessionRouteBody = AdminCloudtentaclesValidateSessionInput export type AdminCloudtentaclesCatalogQueryRouteBody = AdminCloudtentaclesCatalogQueryInput -export type AdminCloudtentaclesDeliveryRecordQueryRouteBody = AdminCloudtentaclesDeliveryRecordQueryInput +export type AdminCloudtentaclesDeliveryRecordQueryRouteBody = + AdminCloudtentaclesDeliveryRecordQueryInput export type AdminCloudtentaclesSkuBuyRouteBody = AdminCloudtentaclesSkuBuyInput export type AdminCloudtentaclesSkuUseRouteBody = AdminCloudtentaclesSkuUseInput export type AdminCloudtentaclesVirtualNumberRouteBody = AdminCloudtentaclesVirtualNumberInput @@ -64,6 +68,6 @@ export type AdminTaskManualDispatchRouteBody = AdminTaskManualDispatchInput export type AdminTaskKuaishouIndustryConsumeRouteBody = AdminTaskKuaishouIndustryConsumeInput export type AdminOrderRouteParams = { orderId?: string } -export type AdminTaskRouteParams = { taskId?: string, bindingId?: string } +export type AdminTaskRouteParams = { taskId?: string; bindingId?: string } export type AdminEntityRouteParams = { id?: string } export type AdminCloudtentaclesSourceRouteParams = { sourceKey?: string } diff --git a/apps/backend/src/types/json.ts b/apps/backend/src/types/json.ts index 3b37a5d2..734d4238 100644 --- a/apps/backend/src/types/json.ts +++ b/apps/backend/src/types/json.ts @@ -9,10 +9,7 @@ /** 严格 JSON(推荐新代码) */ export type JsonPrimitive = string | number | boolean | null -export type StrictJson = - | JsonPrimitive - | StrictJson[] - | { readonly [key: string]: StrictJson } +export type StrictJson = JsonPrimitive | StrictJson[] | { readonly [key: string]: StrictJson } export type StrictJsonObject = { [key: string]: StrictJson } /** diff --git a/apps/backend/src/utils/http.ts b/apps/backend/src/utils/http.ts index 7e1c0569..50bece2b 100644 --- a/apps/backend/src/utils/http.ts +++ b/apps/backend/src/utils/http.ts @@ -1,37 +1,40 @@ -import type { Request, Response } from "express"; -import { logError, logWarn } from "./logger.js"; +import type { Request, Response } from 'express' +import { logError, logWarn } from './logger.js' -const RESPONSE_TIME_ZONE = "Asia/Shanghai"; +const RESPONSE_TIME_ZONE = 'Asia/Shanghai' export type HttpErrorLike = { - statusCode?: number; - errorCode?: string; - cause?: unknown; - context?: unknown; - [key: string]: unknown; -}; + statusCode?: number + errorCode?: string + cause?: unknown + context?: unknown + [key: string]: unknown +} type CreateHttpErrorOptions = { - statusCode?: number; - errorCode?: string; - cause?: unknown; - context?: unknown; -}; + statusCode?: number + errorCode?: string + cause?: unknown + context?: unknown +} -export function buildSuccessPayload(data: unknown, msg: string = "ok") { +export function buildSuccessPayload(data: unknown, msg: string = 'ok') { return { code: 0, msg, time: Math.floor(Date.now() / 1000), data: normalizeResponseDateTime(data), - }; + } } export function buildErrorPayload(error: unknown, fallbackMessage: string) { - const classification = classifyRouteError(error); - const message = classification.statusCode >= 500 - ? fallbackMessage || "服务内部错误" - : error instanceof Error ? error.message : fallbackMessage; + const classification = classifyRouteError(error) + const message = + classification.statusCode >= 500 + ? fallbackMessage || '服务内部错误' + : error instanceof Error + ? error.message + : fallbackMessage return { code: 1, @@ -39,25 +42,23 @@ export function buildErrorPayload(error: unknown, fallbackMessage: string) { errorCode: classification.errorCode, time: Math.floor(Date.now() / 1000), data: null as null, - }; + } } export function sendRouteError( res: Response, error: unknown, - fallbackMessage = "服务内部错误", - scope = "[route]" + fallbackMessage = '服务内部错误', + scope = '[route]', ): void { - const classification = classifyRouteError(error); - const logger = classification.statusCode >= 500 ? logError : logWarn; + const classification = classifyRouteError(error) + const logger = classification.statusCode >= 500 ? logError : logWarn logger(scope, fallbackMessage, { statusCode: classification.statusCode, errorCode: classification.errorCode, error, - }); - res - .status(classification.statusCode) - .json(buildErrorPayload(error, fallbackMessage)); + }) + res.status(classification.statusCode).json(buildErrorPayload(error, fallbackMessage)) } export function buildNotFoundPayload(req: Request) { @@ -66,201 +67,193 @@ export function buildNotFoundPayload(req: Request) { msg: `未实现接口: ${req.method} ${req.originalUrl}`, time: Math.floor(Date.now() / 1000), data: null as null, - }; + } } export function createHttpError( message: string, - options: CreateHttpErrorOptions = {} + options: CreateHttpErrorOptions = {}, ): Error & HttpErrorLike { - const { statusCode = 500, errorCode = "", cause, context } = options; - const error = new Error(message) as Error & HttpErrorLike; - error.statusCode = statusCode; - error.errorCode = errorCode || defaultErrorCodeForStatus(statusCode); + const { statusCode = 500, errorCode = '', cause, context } = options + const error = new Error(message) as Error & HttpErrorLike + error.statusCode = statusCode + error.errorCode = errorCode || defaultErrorCodeForStatus(statusCode) - if (typeof cause !== "undefined") { - error.cause = cause; + if (typeof cause !== 'undefined') { + error.cause = cause } - if (typeof context !== "undefined") { - error.context = context; + if (typeof context !== 'undefined') { + error.context = context } - return error; + return error } function classifyRouteError(error: unknown): { - statusCode: number; - errorCode: string; + statusCode: number + errorCode: string } { - if (error && typeof error === "object") { - const currentError = error as HttpErrorLike; - const statusCode = Number(currentError.statusCode); - const errorCode = String(currentError.errorCode || "").trim(); + if (error && typeof error === 'object') { + const currentError = error as HttpErrorLike + const statusCode = Number(currentError.statusCode) + const errorCode = String(currentError.errorCode || '').trim() if (Number.isInteger(statusCode) && statusCode >= 400 && statusCode < 600) { return { statusCode, errorCode: errorCode || defaultErrorCodeForStatus(statusCode), - }; + } } } - const message = error instanceof Error ? error.message : String(error || ""); + const message = error instanceof Error ? error.message : String(error || '') - if ( - message.includes("缺少") || - message.includes("无效") || - message.includes("验签失败") - ) { - return { statusCode: 400, errorCode: "invalid_request" }; + if (message.includes('缺少') || message.includes('无效') || message.includes('验签失败')) { + return { statusCode: 400, errorCode: 'invalid_request' } } - if (message.includes("不存在")) { - return { statusCode: 404, errorCode: "not_found" }; + if (message.includes('不存在')) { + return { statusCode: 404, errorCode: 'not_found' } } - if (message.includes("已关闭")) { - return { statusCode: 410, errorCode: "gone" }; + if (message.includes('已关闭')) { + return { statusCode: 410, errorCode: 'gone' } } - if (message.includes("不能") || message.includes("冲突")) { - return { statusCode: 409, errorCode: "conflict" }; + if (message.includes('不能') || message.includes('冲突')) { + return { statusCode: 409, errorCode: 'conflict' } } - return { statusCode: 500, errorCode: "internal_error" }; + return { statusCode: 500, errorCode: 'internal_error' } } function defaultErrorCodeForStatus(statusCode: number): string { if (statusCode === 400) { - return "invalid_request"; + return 'invalid_request' } if (statusCode === 401) { - return "unauthorized"; + return 'unauthorized' } if (statusCode === 403) { - return "forbidden"; + return 'forbidden' } if (statusCode === 404) { - return "not_found"; + return 'not_found' } if (statusCode === 409) { - return "conflict"; + return 'conflict' } if (statusCode === 410) { - return "gone"; + return 'gone' } if (statusCode === 503) { - return "dependency_unavailable"; + return 'dependency_unavailable' } - return "internal_error"; + return 'internal_error' } function normalizeResponseDateTime(value: unknown): unknown { if (value instanceof Date) { - return formatResponseDateTime(value.toISOString()); + return formatResponseDateTime(value.toISOString()) } if (Array.isArray(value)) { - return value.map((item) => normalizeResponseDateTime(item)); + return value.map((item) => normalizeResponseDateTime(item)) } - if (value && typeof value === "object") { + if (value && typeof value === 'object') { return Object.fromEntries( Object.entries(value).map(([key, currentValue]) => [ key, normalizeResponseDateTime(currentValue), - ]) - ); + ]), + ) } if (isIsoDateTimeString(value)) { - return formatResponseDateTime(value as string); + return formatResponseDateTime(value as string) } - return value; + return value } function isIsoDateTimeString(value: unknown): boolean { return ( - typeof value === "string" && - /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/.test( - value.trim() - ) - ); + typeof value === 'string' && + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/.test(value.trim()) + ) } function formatResponseDateTime(value: string): string { - const date = new Date(value); + const date = new Date(value) if (Number.isNaN(date.getTime())) { - return String(value || "") - .replace("T", " ") - .replace("Z", ""); + return String(value || '') + .replace('T', ' ') + .replace('Z', '') } - const parts = new Intl.DateTimeFormat("zh-CN", { + const parts = new Intl.DateTimeFormat('zh-CN', { timeZone: RESPONSE_TIME_ZONE, hour12: false, - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - }).formatToParts(date); + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }).formatToParts(date) const tokens = Object.fromEntries( - parts - .filter((item) => item.type !== "literal") - .map((item) => [item.type, item.value]) - ); + parts.filter((item) => item.type !== 'literal').map((item) => [item.type, item.value]), + ) - return `${tokens.year}-${tokens.month}-${tokens.day} ${tokens.hour}:${tokens.minute}:${tokens.second}`; + return `${tokens.year}-${tokens.month}-${tokens.day} ${tokens.hour}:${tokens.minute}:${tokens.second}` } type RouteHandlerOptions = { - successMessage?: string; - errorMessage?: string; - scope?: string; -}; + successMessage?: string + errorMessage?: string + scope?: string +} type RouteFileHandlerOptions = { - errorMessage?: string; - scope?: string; -}; + errorMessage?: string + scope?: string +} export function createRouteHandler( action: (req: Request, res: Response) => unknown | Promise, - { successMessage = "ok", errorMessage, scope }: RouteHandlerOptions = {} + { successMessage = 'ok', errorMessage, scope }: RouteHandlerOptions = {}, ) { return async (req: Request, res: Response): Promise => { try { - const data = await action(req, res); - res.json(buildSuccessPayload(data, successMessage)); + const data = await action(req, res) + res.json(buildSuccessPayload(data, successMessage)) } catch (error) { - sendRouteError(res, error, errorMessage, scope); + sendRouteError(res, error, errorMessage, scope) } - }; + } } export function createRouteFileHandler( action: (req: Request, res: Response) => string | Promise, - { errorMessage, scope }: RouteFileHandlerOptions = {} + { errorMessage, scope }: RouteFileHandlerOptions = {}, ) { return async (req: Request, res: Response): Promise => { try { - const filePath = await action(req, res); - res.sendFile(filePath); + const filePath = await action(req, res) + res.sendFile(filePath) } catch (error) { - sendRouteError(res, error, errorMessage, scope); + sendRouteError(res, error, errorMessage, scope) } - }; + } } diff --git a/apps/backend/src/utils/logger.ts b/apps/backend/src/utils/logger.ts index e7ebeea0..efeb1f97 100644 --- a/apps/backend/src/utils/logger.ts +++ b/apps/backend/src/utils/logger.ts @@ -175,9 +175,7 @@ function enqueueFileWrite( sanitize: boolean, jsonLine = false, ): void { - const line = jsonLine - ? JSON.stringify(entry) - : formatLogEntry(entry, { color: false, sanitize }) + const line = jsonLine ? JSON.stringify(entry) : formatLogEntry(entry, { color: false, sanitize }) const dateKey = extractLogDateKey(entry.time) writeQueue = writeQueue @@ -279,7 +277,9 @@ export function resolveLogFilePath(channel = 'app', time = new Date().toISOStrin } function normalizeLogChannelName(value: unknown): string { - const normalized = String(value || '').trim().toLowerCase() + const normalized = String(value || '') + .trim() + .toLowerCase() if (!normalized) { return 'app' } @@ -344,7 +344,9 @@ function normalizeLogValue(value: unknown, sanitize = true): unknown { try { return sanitize - ? sanitizeLogValue(JSON.parse(JSON.stringify(value, (_key, current) => normalizeCurrent(current)))) + ? sanitizeLogValue( + JSON.parse(JSON.stringify(value, (_key, current) => normalizeCurrent(current))), + ) : JSON.parse(JSON.stringify(value, (_key, current) => normalizeCurrent(current))) } catch { return sanitize ? sanitizeLogString(String(value)) : String(value) diff --git a/apps/backend/src/utils/task-json.ts b/apps/backend/src/utils/task-json.ts index d10e4bf9..32e32838 100644 --- a/apps/backend/src/utils/task-json.ts +++ b/apps/backend/src/utils/task-json.ts @@ -21,14 +21,10 @@ export function parseJsonObject(value: unknown): JsonObject { } } -export function parseTaskContext( - task: { context_json?: unknown } | null | undefined, -): TaskContext { +export function parseTaskContext(task: { context_json?: unknown } | null | undefined): TaskContext { return parseJsonObject(task?.context_json) as TaskContext } -export function parseTaskState( - task: { state_json?: unknown } | null | undefined, -): JsonObject { +export function parseTaskState(task: { state_json?: unknown } | null | undefined): JsonObject { return parseJsonObject(task?.state_json) } diff --git a/apps/backend/tsconfig.build.json b/apps/backend/tsconfig.build.json index b66dbb2b..312b0195 100644 --- a/apps/backend/tsconfig.build.json +++ b/apps/backend/tsconfig.build.json @@ -7,11 +7,5 @@ "declaration": false, "sourceMap": true }, - "exclude": [ - "node_modules", - "data", - "dist", - "src/**/*.test.js", - "src/**/*.test.ts" - ] + "exclude": ["node_modules", "data", "dist", "src/**/*.test.js", "src/**/*.test.ts"] } diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json index a2c1528f..56450b4e 100644 --- a/apps/backend/tsconfig.json +++ b/apps/backend/tsconfig.json @@ -15,12 +15,6 @@ "types": ["node"], "lib": ["ES2022"] }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "node_modules", - "data", - "src/**/*.test.ts" - ] + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "data", "src/**/*.test.ts"] } diff --git a/apps/frontend/package-lock.json b/apps/frontend/package-lock.json index 48317392..5feed6c7 100644 --- a/apps/frontend/package-lock.json +++ b/apps/frontend/package-lock.json @@ -24,6 +24,7 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", + "prettier": "3.8.3", "typescript": "~6.0.2", "vite": "^8.0.7" } @@ -2261,6 +2262,22 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", diff --git a/apps/frontend/package.json b/apps/frontend/package.json index a05c4c00..f0f7e13f 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -6,8 +6,11 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", + "format": "prettier --config ../../.prettierrc.json --ignore-path ../../.prettierignore --write \"src/**/*.{ts,tsx,js,jsx,json,css}\" \"tests/**/*.{ts,tsx,js,jsx}\" package.json tsconfig.json vite.config.ts index.html", + "format:check": "prettier --config ../../.prettierrc.json --ignore-path ../../.prettierignore --check \"src/**/*.{ts,tsx,js,jsx,json,css}\" \"tests/**/*.{ts,tsx,js,jsx}\" package.json tsconfig.json vite.config.ts index.html", "test": "node --experimental-strip-types --test tests/*.test.ts", "typecheck": "tsc -b --noEmit", + "check": "npm run format:check && npm run typecheck && npm test && npm run build", "preview": "vite preview" }, "dependencies": { @@ -27,6 +30,7 @@ "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", + "prettier": "3.8.3", "typescript": "~6.0.2", "vite": "^8.0.7" } diff --git a/apps/frontend/src/components/admin/AdminDatePicker.tsx b/apps/frontend/src/components/admin/AdminDatePicker.tsx index 1fe09d48..97ef3849 100644 --- a/apps/frontend/src/components/admin/AdminDatePicker.tsx +++ b/apps/frontend/src/components/admin/AdminDatePicker.tsx @@ -21,8 +21,7 @@ export function AdminDatePicker({ placeholder = '请选择日期', ...rest }: AdminDatePickerProps) { - const resolvedFormat = - format ?? (rest.showTime ? ADMIN_DATE_TIME_FORMAT : ADMIN_DATE_FORMAT) + const resolvedFormat = format ?? (rest.showTime ? ADMIN_DATE_TIME_FORMAT : ADMIN_DATE_FORMAT) return } @@ -30,22 +29,14 @@ export function AdminDatePicker({ /** * 后台统一日期范围:默认 YYYY-MM-DD;showTime 时 YYYY-MM-DD HH:mm:ss。 */ -export function AdminRangePicker({ - format, - placeholder, - ...rest -}: AdminRangePickerProps) { +export function AdminRangePicker({ format, placeholder, ...rest }: AdminRangePickerProps) { const withTime = Boolean(rest.showTime) const resolvedFormat = format ?? (withTime ? ADMIN_DATE_TIME_FORMAT : ADMIN_DATE_FORMAT) const resolvedPlaceholder = placeholder ?? (withTime ? ADMIN_DATE_TIME_RANGE_PLACEHOLDER : ADMIN_DATE_RANGE_PLACEHOLDER) return ( - + ) } diff --git a/apps/frontend/src/components/files/ImageUpload.tsx b/apps/frontend/src/components/files/ImageUpload.tsx index 88fd280d..56e7a16f 100644 --- a/apps/frontend/src/components/files/ImageUpload.tsx +++ b/apps/frontend/src/components/files/ImageUpload.tsx @@ -78,10 +78,7 @@ export default function ImageUpload({ thumbUrl: file.thumbnailUrl || file.url, })) - async function uploadImages( - images: File[], - source: UploadSource = 'default', - ) { + async function uploadImages(images: File[], source: UploadSource = 'default') { const remaining = maxCount - valueRef.current.length if (remaining <= 0) { message.warning(`最多上传 ${maxCount} 张图片`) @@ -102,10 +99,7 @@ export default function ImageUpload({ } } - async function uploadFiles( - files: File[], - source: UploadSource = 'default', - ) { + async function uploadFiles(files: File[], source: UploadSource = 'default') { const images = files.filter((file) => file.type.startsWith('image/')) if (images.length === 0) return await uploadImages(images, source) @@ -223,9 +217,7 @@ export default function ImageUpload({ maxCount={maxCount} customRequest={customRequest} onRemove={(file) => { - const next = valueRef.current.filter( - (item) => (item.objectKey || item.url) !== file.uid, - ) + const next = valueRef.current.filter((item) => (item.objectKey || item.url) !== file.uid) valueRef.current = next onChange?.(next) return true diff --git a/apps/frontend/src/domain/task-status.ts b/apps/frontend/src/domain/task-status.ts index 796e27da..9c1fc150 100644 --- a/apps/frontend/src/domain/task-status.ts +++ b/apps/frontend/src/domain/task-status.ts @@ -1,24 +1,24 @@ export const TASK_STATUS = { - PENDING_PAYMENT: "pending_payment", - PAID: "paid", - LINK_GENERATED: "link_generated", - CLAIMED: "claimed", - PENDING_BINDING_PREPARE: "pending_binding_prepare", - WAITING_BINDING: "waiting_binding", - ROLE_CONFIRMED: "role_confirmed", - REDEEMING: "redeeming", - DISPATCHED_PENDING_RETURN: "dispatched_pending_return", - COMPLETED: "completed", - REDEEMED: "redeemed", - RETRY_PENDING: "retry_pending", - MANUAL_REVIEW: "manual_review", - FAILED: "failed", - EXPIRED: "expired", - CLOSED: "closed", -} as const; + PENDING_PAYMENT: 'pending_payment', + PAID: 'paid', + LINK_GENERATED: 'link_generated', + CLAIMED: 'claimed', + PENDING_BINDING_PREPARE: 'pending_binding_prepare', + WAITING_BINDING: 'waiting_binding', + ROLE_CONFIRMED: 'role_confirmed', + REDEEMING: 'redeeming', + DISPATCHED_PENDING_RETURN: 'dispatched_pending_return', + COMPLETED: 'completed', + REDEEMED: 'redeemed', + RETRY_PENDING: 'retry_pending', + MANUAL_REVIEW: 'manual_review', + FAILED: 'failed', + EXPIRED: 'expired', + CLOSED: 'closed', +} as const -export type KnownTaskStatus = (typeof TASK_STATUS)[keyof typeof TASK_STATUS]; -export type TaskStatus = KnownTaskStatus | (string & {}); +export type KnownTaskStatus = (typeof TASK_STATUS)[keyof typeof TASK_STATUS] +export type TaskStatus = KnownTaskStatus | (string & {}) const CLAIM_INACTIVE_STATUSES = new Set([ TASK_STATUS.COMPLETED, @@ -27,7 +27,7 @@ const CLAIM_INACTIVE_STATUSES = new Set([ TASK_STATUS.FAILED, TASK_STATUS.EXPIRED, TASK_STATUS.CLOSED, -]); +]) const KUAISHOU_CLOUD_RESULT_STATUSES = new Set([ TASK_STATUS.DISPATCHED_PENDING_RETURN, @@ -35,25 +35,25 @@ const KUAISHOU_CLOUD_RESULT_STATUSES = new Set([ TASK_STATUS.REDEEMED, TASK_STATUS.MANUAL_REVIEW, TASK_STATUS.FAILED, -]); +]) export function normalizeTaskStatus(value: unknown): TaskStatus { - return String(value || "").trim() as TaskStatus; + return String(value || '').trim() as TaskStatus } export function isClaimInactiveTaskStatus(status: unknown): boolean { - return CLAIM_INACTIVE_STATUSES.has(normalizeTaskStatus(status)); + return CLAIM_INACTIVE_STATUSES.has(normalizeTaskStatus(status)) } export function isKuaishouCloudRoleConfirmedStatus(status: unknown): boolean { - return normalizeTaskStatus(status) === TASK_STATUS.ROLE_CONFIRMED; + return normalizeTaskStatus(status) === TASK_STATUS.ROLE_CONFIRMED } export function isKuaishouCloudCompletedStatus(status: unknown): boolean { - const normalized = normalizeTaskStatus(status); - return normalized === TASK_STATUS.COMPLETED || normalized === TASK_STATUS.REDEEMED; + const normalized = normalizeTaskStatus(status) + return normalized === TASK_STATUS.COMPLETED || normalized === TASK_STATUS.REDEEMED } export function hasKuaishouCloudRedeemResultStatus(status: unknown): boolean { - return KUAISHOU_CLOUD_RESULT_STATUSES.has(normalizeTaskStatus(status)); + return KUAISHOU_CLOUD_RESULT_STATUSES.has(normalizeTaskStatus(status)) } diff --git a/apps/frontend/src/layouts/AdminLayout.tsx b/apps/frontend/src/layouts/AdminLayout.tsx index c715375e..a0d3c116 100644 --- a/apps/frontend/src/layouts/AdminLayout.tsx +++ b/apps/frontend/src/layouts/AdminLayout.tsx @@ -75,9 +75,7 @@ export default function AdminLayout() { key: 'group-dev', type: 'group', label: '开发', - children: [ - { key: '/admin/dev-mock', icon: , label: '开发 Mock' }, - ], + children: [{ key: '/admin/dev-mock', icon: , label: '开发 Mock' }], }) } diff --git a/apps/frontend/src/lib/feedback.ts b/apps/frontend/src/lib/feedback.ts index ecd22e13..c25770ad 100644 --- a/apps/frontend/src/lib/feedback.ts +++ b/apps/frontend/src/lib/feedback.ts @@ -24,11 +24,7 @@ export function showError(message: string, options: MessageOptions = {}) { }) } -export function showConfirm( - message: string, - title = '确认操作', - options: ModalFuncProps = {}, -) { +export function showConfirm(message: string, title = '确认操作', options: ModalFuncProps = {}) { return new Promise((resolve, reject) => { Modal.confirm({ title, diff --git a/apps/frontend/src/lib/http.ts b/apps/frontend/src/lib/http.ts index f5cc9ddc..f221c5b8 100644 --- a/apps/frontend/src/lib/http.ts +++ b/apps/frontend/src/lib/http.ts @@ -37,13 +37,9 @@ http.interceptors.response.use( }, (error) => { const responseMessage = - typeof error?.response?.data?.msg === 'string' - ? error.response.data.msg.trim() - : '' + typeof error?.response?.data?.msg === 'string' ? error.response.data.msg.trim() : '' const fallbackMessage = resolveFallbackHttpMessage(error) - const normalizedError = new Error( - responseMessage || fallbackMessage, - ) as Error & { + const normalizedError = new Error(responseMessage || fallbackMessage) as Error & { errorCode?: string status?: number } @@ -56,10 +52,7 @@ http.interceptors.response.use( normalizedError.status = error.response.status } - if ( - error?.config?.url && - String(error.config.url).startsWith('/api/v1/admin') - ) { + if (error?.config?.url && String(error.config.url).startsWith('/api/v1/admin')) { const status = Number(error?.response?.status || 0) const errorCode = String(error?.response?.data?.errorCode || '').trim() @@ -75,10 +68,7 @@ http.interceptors.response.use( } } - if ( - error?.config?.url && - String(error.config.url).startsWith('/api/v1/worker') - ) { + if (error?.config?.url && String(error.config.url).startsWith('/api/v1/worker')) { const status = Number(error?.response?.status || 0) const errorCode = String(error?.response?.data?.errorCode || '').trim() @@ -126,13 +116,8 @@ function resolveFallbackHttpMessage(error: unknown) { return '网络连接错误' } - if ( - typeof (error as { response?: { statusText?: string } })?.response - ?.statusText === 'string' - ) { - return String( - (error as { response: { statusText: string } }).response.statusText, - ).trim() + if (typeof (error as { response?: { statusText?: string } })?.response?.statusText === 'string') { + return String((error as { response: { statusText: string } }).response.statusText).trim() } return '接口请求失败' @@ -158,9 +143,7 @@ function shouldClearWorkerSession(errorCode: string) { ].includes(errorCode) } -function request( - config: Parameters>>[0], -) { +function request(config: Parameters>>[0]) { return http.request, ApiEnvelope>(config) } diff --git a/apps/frontend/src/pages/admin/AdminAuditLogsPage.tsx b/apps/frontend/src/pages/admin/AdminAuditLogsPage.tsx index 20a27432..805f8acc 100644 --- a/apps/frontend/src/pages/admin/AdminAuditLogsPage.tsx +++ b/apps/frontend/src/pages/admin/AdminAuditLogsPage.tsx @@ -128,7 +128,11 @@ export default function AdminAuditLogsPage() { 共 {data?.pagination.total || 0} 条审计记录} + extra={ + + 共 {data?.pagination.total || 0} 条审计记录 + + } /> {!isAdmin ? ( diff --git a/apps/frontend/src/pages/admin/AdminCloudtentaclesRecordsPage.tsx b/apps/frontend/src/pages/admin/AdminCloudtentaclesRecordsPage.tsx index 6e9cf09f..9b925834 100644 --- a/apps/frontend/src/pages/admin/AdminCloudtentaclesRecordsPage.tsx +++ b/apps/frontend/src/pages/admin/AdminCloudtentaclesRecordsPage.tsx @@ -332,7 +332,12 @@ export default function AdminCloudtentaclesRecordsPage() { - @@ -555,7 +560,10 @@ function drawTicketCard( drawEllipsisText(ctx, `账号:${record.fulfillUser || record.phone || '-'}`, contentX, y + 90, 250) } -function drawRecordMeta(ctx: CanvasRenderingContext2D, record: AdminCloudtentaclesDeliveryRecordItem) { +function drawRecordMeta( + ctx: CanvasRenderingContext2D, + record: AdminCloudtentaclesDeliveryRecordItem, +) { const left = 80 const top = 250 const lineHeight = 22 diff --git a/apps/frontend/src/pages/admin/AdminDashboardPage.tsx b/apps/frontend/src/pages/admin/AdminDashboardPage.tsx index 27c8f589..4de80eb3 100644 --- a/apps/frontend/src/pages/admin/AdminDashboardPage.tsx +++ b/apps/frontend/src/pages/admin/AdminDashboardPage.tsx @@ -24,10 +24,7 @@ import StatusTag from '@/components/admin/StatusTag' import dayjs from '@/lib/dayjs' import { fetchAdminDashboardSummary, fetchAdminLoginLogs } from '@/services/admin' import type { AdminLoginLogItem } from '@/types/admin' -import { - ADMIN_DEFAULT_PAGE_SIZE, - buildAdminTablePagination, -} from '@/utils/admin-pagination' +import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination' import { formatAdminDateTime } from '@/utils/admin-time' const cards = [ @@ -98,11 +95,7 @@ export default function AdminDashboardPage() { title: '结果', width: 90, render: (_, row) => - row.success ? ( - 成功 - ) : ( - 失败 - ), + row.success ? 成功 : 失败, }, { title: '登录地点', @@ -132,8 +125,7 @@ export default function AdminDashboardPage() { const range = Array.isArray(values.dateRange) ? values.dateRange : [] const dateFrom = range[0] && dayjs(range[0]).isValid() ? dayjs(range[0]).format('YYYY-MM-DD') : '' - const dateTo = - range[1] && dayjs(range[1]).isValid() ? dayjs(range[1]).format('YYYY-MM-DD') : '' + const dateTo = range[1] && dayjs(range[1]).isValid() ? dayjs(range[1]).format('YYYY-MM-DD') : '' setPage(1) setFilters({ username: String(values.username || '').trim(), @@ -223,9 +215,7 @@ export default function AdminDashboardPage() { showIcon style={{ marginBottom: 16 }} message={ - loginQuery.error instanceof Error - ? loginQuery.error.message - : '读取登录记录失败' + loginQuery.error instanceof Error ? loginQuery.error.message : '读取登录记录失败' } /> ) : null} diff --git a/apps/frontend/src/pages/admin/AdminDevMockPage.tsx b/apps/frontend/src/pages/admin/AdminDevMockPage.tsx index 9fd170ee..1d5d7ba7 100644 --- a/apps/frontend/src/pages/admin/AdminDevMockPage.tsx +++ b/apps/frontend/src/pages/admin/AdminDevMockPage.tsx @@ -102,10 +102,7 @@ export default function AdminDevMockPage() { }, }) - const defaultOrderNo = useMemo( - () => lastCreate?.orderNo || '', - [lastCreate?.orderNo], - ) + const defaultOrderNo = useMemo(() => lastCreate?.orderNo || '', [lastCreate?.orderNo]) if (statusQuery.isLoading) { return ( @@ -138,9 +135,7 @@ export default function AdminDevMockPage() { title="开发 Mock" description="一键造单、测领取页与 91 查单,无需真实 91 / 快手 / feifei 环境。" extra={ - - NODE_ENV={status?.nodeEnv || '-'} - + NODE_ENV={status?.nodeEnv || '-'} } /> diff --git a/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx b/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx index f5a34127..3ee7ce6a 100644 --- a/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx +++ b/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx @@ -120,8 +120,7 @@ export default function AdminKuaishouIndustryPage() { const [actionLoading, setActionLoading] = useState('') const [consumingVoucherId, setConsumingVoucherId] = useState(null) const [vouchers, setVouchers] = useState([]) - const [selectedVoucher, setSelectedVoucher] = - useState(null) + const [selectedVoucher, setSelectedVoucher] = useState(null) const [voucherTotal, setVoucherTotal] = useState(0) const [voucherFilters, setVoucherFilters] = useState({ oid: '', @@ -247,7 +246,10 @@ export default function AdminKuaishouIndustryPage() { return (
- + {row.oid || '-'} {productLabel ? ( @@ -277,7 +279,11 @@ export default function AdminKuaishouIndustryPage() { 发码:{row.sendCallbackStatus || '-'} {row.eticketType ? ( - + {row.eticketType} ) : ( @@ -306,7 +312,9 @@ export default function AdminKuaishouIndustryPage() { const consumedAt = formatAdminDateTime(row.consumedAt) return (
- {consumedAt !== '-' ? consumedAt : serial || '-'} + + {consumedAt !== '-' ? consumedAt : serial || '-'} + {serial && consumedAt !== '-' ? ( {serial} @@ -493,9 +501,7 @@ export default function AdminKuaishouIndustryPage() { setActionLoading('consume') try { const response = await consumeAdminKuaishouIndustryVoucher(buildVoucherToolPayload()) - publishActionResult( - buildIndustryActionResultView('consume', '手动核销', response.data), - ) + publishActionResult(buildIndustryActionResultView('consume', '手动核销', response.data)) showSuccess('手动核销已完成') await loadVouchers() } catch (error) { @@ -533,9 +539,7 @@ export default function AdminKuaishouIndustryPage() { etickets: [{ id: row.voucherCode, code: row.voucherCode, num: 1 }], }) if (!isSupportOnly) { - publishActionResult( - buildIndustryActionResultView('consume', '手动核销', response.data), - ) + publishActionResult(buildIndustryActionResultView('consume', '手动核销', response.data)) } showSuccess(`券码 ${row.voucherCode} 已核销`) await loadVouchers() @@ -560,9 +564,7 @@ export default function AdminKuaishouIndustryPage() { setActionLoading('resend') try { const response = await resendAdminKuaishouIndustryVoucherCode(buildVoucherToolPayload()) - publishActionResult( - buildIndustryActionResultView('resend', '重发发码回调', response.data), - ) + publishActionResult(buildIndustryActionResultView('resend', '重发发码回调', response.data)) if (response.data.success) { showSuccess('发码回调已重发') } else { @@ -590,9 +592,7 @@ export default function AdminKuaishouIndustryPage() { ...buildVoucherToolPayload(), reason: toolForm.reason || 'USER_APPLY_REFUND', }) - publishActionResult( - buildIndustryActionResultView('destroy', '手动销毁', response.data), - ) + publishActionResult(buildIndustryActionResultView('destroy', '手动销毁', response.data)) showSuccess( response.data.alreadyDestroyed ? `券码已是销毁状态,销毁回调已重试(${response.data.reason || 'USER_APPLY_REFUND'})` @@ -677,9 +677,7 @@ export default function AdminKuaishouIndustryPage() { title: string, loadingKey: string, action: () => Promise<{ data: AdminKuaishouIndustryOpenApiResult }>, - afterSuccess?: ( - result: AdminKuaishouIndustryOpenApiResult, - ) => { refundCount?: number } | void, + afterSuccess?: (result: AdminKuaishouIndustryOpenApiResult) => { refundCount?: number } | void, ) { setActionLoading(loadingKey) try { @@ -1069,9 +1067,7 @@ export default function AdminKuaishouIndustryPage() { items={[ { key: 'manual-target', - label: selectedVoucher - ? '手动指定卡券(清除当前选择后可用)' - : '手动指定卡券', + label: selectedVoucher ? '手动指定卡券(清除当前选择后可用)' : '手动指定卡券', children: (
@@ -1568,11 +1560,9 @@ function renderShopCell( return - } - const tipParts = [ - name || null, - id ? `ID:${id}` : null, - token ? `Token:${token}` : null, - ].filter(Boolean) + const tipParts = [name || null, id ? `ID:${id}` : null, token ? `Token:${token}` : null].filter( + Boolean, + ) const tip = tipParts.join('\n') if (compact) { @@ -1671,11 +1661,7 @@ function buildIndustryActionResultView( '展示状态', pickFirstString([etickets[0].displayStatus, etickets[0].status]) || '-', ) - pushHighlight( - highlights, - '平台类型', - pickFirstString([etickets[0].eticketType]) || '-', - ) + pushHighlight(highlights, '平台类型', pickFirstString([etickets[0].eticketType]) || '-') pushHighlight(highlights, '剩余数量', String(etickets[0].leftNum ?? etickets[0].num ?? 1)) } } @@ -1689,11 +1675,7 @@ function buildIndustryActionResultView( ) { if (voucher) { pushHighlight(highlights, '券码', String(voucher.voucherCode || '-')) - pushHighlight( - highlights, - '券状态', - getVoucherStatusLabel(String(voucher.status || '')), - ) + pushHighlight(highlights, '券状态', getVoucherStatusLabel(String(voucher.status || ''))) if (voucher.eticketType) { pushHighlight(highlights, '凭证类型', String(voucher.eticketType)) } @@ -1724,9 +1706,7 @@ function buildIndustryActionResultView( if (kind === 'refund-list') { const count = - typeof meta?.refundCount === 'number' - ? meta.refundCount - : extractRefundRows(response).length + typeof meta?.refundCount === 'number' ? meta.refundCount : extractRefundRows(response).length pushHighlight(highlights, '售后单数量', String(count)) const pcursor = pickFirstString([ responseData?.pcursor, @@ -1739,10 +1719,7 @@ function buildIndustryActionResultView( } if (kind === 'refund-approve' || kind === 'refund-disagree') { - const refundId = pickFirstString([ - responseData?.refundId, - asRecord(openApi.request)?.refundId, - ]) + const refundId = pickFirstString([responseData?.refundId, asRecord(openApi.request)?.refundId]) if (refundId) { pushHighlight(highlights, '退款单号', refundId) } diff --git a/apps/frontend/src/pages/admin/AdminOrdersPage.tsx b/apps/frontend/src/pages/admin/AdminOrdersPage.tsx index cc95f488..fb767523 100644 --- a/apps/frontend/src/pages/admin/AdminOrdersPage.tsx +++ b/apps/frontend/src/pages/admin/AdminOrdersPage.tsx @@ -86,7 +86,9 @@ export default function AdminOrdersPage() { minWidth: 180, render: (_, row) => (
- {row.totalAmount || '0.00'} {row.currency} + + {row.totalAmount || '0.00'} {row.currency} + {formatAdminDateTime(row.createdAt)}
), diff --git a/apps/frontend/src/pages/admin/AdminTaskDetailPage.tsx b/apps/frontend/src/pages/admin/AdminTaskDetailPage.tsx index 8caef346..b6860cb0 100644 --- a/apps/frontend/src/pages/admin/AdminTaskDetailPage.tsx +++ b/apps/frontend/src/pages/admin/AdminTaskDetailPage.tsx @@ -102,9 +102,7 @@ export default function AdminTaskDetailPage() { return lastClaimUrl || detail?.claimToken?.claimUrl || '' }, [detail?.claimToken?.claimUrl, detail?.claimToken?.status, detail?.task.status, lastClaimUrl]) const claimLinkInvalid = Boolean( - detail?.claimToken?.claimUrl && - claimTokenStatus && - !isUsableClaimLinkStatus(claimTokenStatus), + detail?.claimToken?.claimUrl && claimTokenStatus && !isUsableClaimLinkStatus(claimTokenStatus), ) useEffect(() => { @@ -364,7 +362,8 @@ export default function AdminTaskDetailPage() { {resolvedDetail.orderItem?.skuName || resolvedDetail.task.skuName || '-'} - {resolvedDetail.task.roleName || '-'} {resolvedDetail.task.roleId ? `(${resolvedDetail.task.roleId})` : ''} + {resolvedDetail.task.roleName || '-'}{' '} + {resolvedDetail.task.roleId ? `(${resolvedDetail.task.roleId})` : ''} {formatAdminDateTime(resolvedDetail.task.updatedAt)} @@ -654,7 +653,10 @@ function TaskActionPanel({ ) : null} - {!canManageTaskLifecycle && !canOperateIndustryVoucher && !canCloseTasks && !hasCloudActions ? ( + {!canManageTaskLifecycle && + !canOperateIndustryVoucher && + !canCloseTasks && + !hasCloudActions ? ( 当前账号没有可执行的任务操作 ) : null}
@@ -668,7 +670,9 @@ function TaskActionPanel({ } function isUsableClaimLinkStatus(value: unknown) { - const normalized = String(value || '').trim().toLowerCase() + const normalized = String(value || '') + .trim() + .toLowerCase() return normalized === 'active' || normalized === 'external' } @@ -684,12 +688,7 @@ function FulfillmentOverviewPanel({ const executor = resolveTaskExecutorDisplay(detail.task.executorKey) const steps = buildFulfillmentOverviewSteps(detail, claimUrl, claimLinkInvalid) const claimIdentity = detail.claimIdentity - const currentBlocker = resolveTaskBlocker( - detail.task, - claimUrl, - claimLinkInvalid, - claimIdentity, - ) + const currentBlocker = resolveTaskBlocker(detail.task, claimUrl, claimLinkInvalid, claimIdentity) const uidMatchTag = claimIdentity?.uidMatched === true ? { color: 'success' as const, text: 'UID 已匹配' } @@ -700,10 +699,7 @@ function FulfillmentOverviewPanel({ : { color: 'warning' as const, text: '未填 UID' } return ( - {executor.label}} - > + {executor.label}}> - {claimIdentity?.expectedUid || ( - 未提交 - )} + {claimIdentity?.expectedUid || 未提交} {claimIdentity?.boundUid || detail.task.roleId || '-'} @@ -855,7 +849,10 @@ function resolveTaskBlocker( claimLinkInvalid: boolean, claimIdentity?: AdminTaskDetail['claimIdentity'], ): { level: 'success' | 'info' | 'warning' | 'error'; title: string; detail: string } { - if (['completed', 'redeemed', 'delivered'].includes(task.status) || task.deliveryStatus === 'delivered') { + if ( + ['completed', 'redeemed', 'delivered'].includes(task.status) || + task.deliveryStatus === 'delivered' + ) { return { level: 'success', title: '履约已完成', @@ -871,11 +868,7 @@ function resolveTaskBlocker( } } - if ( - task.executorKey === 'kuaishou_ct_assisted' && - claimIdentity && - !claimIdentity.ready - ) { + if (task.executorKey === 'kuaishou_ct_assisted' && claimIdentity && !claimIdentity.ready) { return { level: 'warning', title: '等待用户填写 UID', @@ -883,10 +876,7 @@ function resolveTaskBlocker( } } - if ( - task.executorKey === 'kuaishou_ct_assisted' && - claimIdentity?.uidMatched === false - ) { + if (task.executorKey === 'kuaishou_ct_assisted' && claimIdentity?.uidMatched === false) { return { level: 'error', title: 'UID 与绑定角色不一致', @@ -963,7 +953,11 @@ function resolveDeliveryStepSummary(detail: AdminTaskDetail) { } if (detail.manualDispatch) { - return detail.manualDispatch.resultMessage || detail.manualDispatch.deliveryReference || '人工履约已回写' + return ( + detail.manualDispatch.resultMessage || + detail.manualDispatch.deliveryReference || + '人工履约已回写' + ) } return detail.task.resultMessage || detail.task.lastError || '等待执行器推进' @@ -1119,9 +1113,7 @@ function KuaishouIndustryVoucherPanel({ voucher }: { voucher: KuaishouIndustryVo {formatAdminDateTime(voucher.sendCallbackSentAt)} - - {voucher.consumeSerialNum || '-'} - + {voucher.consumeSerialNum || '-'} {formatAdminDateTime(voucher.consumedAt)} diff --git a/apps/frontend/src/pages/admin/AdminTasksPage.tsx b/apps/frontend/src/pages/admin/AdminTasksPage.tsx index f8eb942c..2ba8e84a 100644 --- a/apps/frontend/src/pages/admin/AdminTasksPage.tsx +++ b/apps/frontend/src/pages/admin/AdminTasksPage.tsx @@ -144,11 +144,7 @@ export default function AdminTasksPage() { return (
- + {blocker.label} {blocker.showDetail ? ( @@ -326,7 +322,10 @@ export default function AdminTasksPage() { }) } - function resolveTaskListAction(actionKey: TaskListAction, item: AdminTaskListItem): { + function resolveTaskListAction( + actionKey: TaskListAction, + item: AdminTaskListItem, + ): { action: () => Promise<{ data: AdminTaskActionResponse }> confirmText: string successMessage: string @@ -570,9 +569,13 @@ function resolveTaskListBlocker(item: AdminTaskListItem): TaskListBlocker { } if ( - ['not_started', 'pending', 'pending_prepare', 'pending_binding', 'pending_binding_prepare'].includes( - resourceStatus, - ) + [ + 'not_started', + 'pending', + 'pending_prepare', + 'pending_binding', + 'pending_binding_prepare', + ].includes(resourceStatus) ) { return { label: '等资源', @@ -619,7 +622,9 @@ function resolveTaskListBlocker(item: AdminTaskListItem): TaskListBlocker { } function normalizeTaskListStatus(value: unknown) { - return String(value || '').trim().toLowerCase() + return String(value || '') + .trim() + .toLowerCase() } function cleanParams(params: Record) { diff --git a/apps/frontend/src/pages/admin/AdminUsersPage.tsx b/apps/frontend/src/pages/admin/AdminUsersPage.tsx index f7379796..9843b424 100644 --- a/apps/frontend/src/pages/admin/AdminUsersPage.tsx +++ b/apps/frontend/src/pages/admin/AdminUsersPage.tsx @@ -1,9 +1,4 @@ -import { - DownOutlined, - PlusOutlined, - ReloadOutlined, - SearchOutlined, -} from '@ant-design/icons' +import { DownOutlined, PlusOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons' import { Alert, App, @@ -403,7 +398,11 @@ export default function AdminUsersPage() { 共 {data?.pagination.total || 0} 个账号} + extra={ + + 共 {data?.pagination.total || 0} 个账号 + + } > rowKey="userId" diff --git a/apps/frontend/src/pages/admin/AdminWorkerPlatformPage.tsx b/apps/frontend/src/pages/admin/AdminWorkerPlatformPage.tsx index 03a9b893..16608878 100644 --- a/apps/frontend/src/pages/admin/AdminWorkerPlatformPage.tsx +++ b/apps/frontend/src/pages/admin/AdminWorkerPlatformPage.tsx @@ -75,10 +75,7 @@ import type { WorkerLevel, WorkerUser, } from '@/types/worker-platform' -import { - ADMIN_DEFAULT_PAGE_SIZE, - buildAdminTablePagination, -} from '@/utils/admin-pagination' +import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination' import { formatAdminDateTime } from '@/utils/admin-time' import CategoriesPanel from './panels/CategoriesPanel' import FinancePanel from './panels/FinancePanel' @@ -97,10 +94,7 @@ export default function AdminWorkerPlatformPage() { return (
- + - +
- + @@ -252,7 +248,8 @@ export default function CategoriesPanel() { cancelText="取消" > - 确定删除分类「{deletingCategory?.name}」?已引用该分类的工单和物品规则将无法删除,需先处理。 + 确定删除分类「{deletingCategory?.name} + 」?已引用该分类的工单和物品规则将无法删除,需先处理。
diff --git a/apps/frontend/src/pages/admin/panels/FinancePanel.tsx b/apps/frontend/src/pages/admin/panels/FinancePanel.tsx index 1c6dc21a..0e4142ff 100644 --- a/apps/frontend/src/pages/admin/panels/FinancePanel.tsx +++ b/apps/frontend/src/pages/admin/panels/FinancePanel.tsx @@ -76,12 +76,14 @@ import type { WorkerLevel, WorkerUser, } from '@/types/worker-platform' -import { - ADMIN_DEFAULT_PAGE_SIZE, - buildAdminTablePagination, -} from '@/utils/admin-pagination' +import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination' import { formatAdminDateTime } from '@/utils/admin-time' -import { asRecord, formatMoney, formatSharingShareStatus, resolveSharingShareStatusColor } from './shared' +import { + asRecord, + formatMoney, + formatSharingShareStatus, + resolveSharingShareStatusColor, +} from './shared' type FinanceConfigFormValues = { depositUnfreezeDays?: number recharge?: { @@ -230,9 +232,7 @@ export default function FinancePanel() { {row.worker?.displayName || row.worker?.username || '-'} - - {row.worker?.username || '-'} - + {row.worker?.username || '-'}
), }, @@ -272,9 +272,7 @@ export default function FinancePanel() {
{row.note || '-'} {row.reviewedNote ? ( - - 审核备注:{row.reviewedNote} - + 审核备注:{row.reviewedNote} ) : null}
), @@ -350,11 +348,7 @@ export default function FinancePanel() { initialValues={mapFinanceConfigToFormValues()} > - + @@ -378,10 +372,7 @@ export default function FinancePanel() { - + @@ -400,11 +391,7 @@ export default function FinancePanel() {
- + @@ -494,7 +481,9 @@ export default function FinancePanel() { {renderFinanceRequestAccountText(request)} {request.requestType === 'withdraw' && request.accountNo ? ( - - {maskFinanceAccount(request.accountNo)} - + {maskFinanceAccount(request.accountNo)} ) : null}
) @@ -629,11 +614,7 @@ function renderFinanceRequestAccountText(request: WorkerFinanceRequest) { } const rechargeSnapshot = getRechargeSnapshot(request) - if ( - rechargeSnapshot.channelName || - rechargeSnapshot.accountName || - rechargeSnapshot.accountNo - ) { + if (rechargeSnapshot.channelName || rechargeSnapshot.accountName || rechargeSnapshot.accountNo) { return [ rechargeSnapshot.channelName, rechargeSnapshot.accountName, diff --git a/apps/frontend/src/pages/admin/panels/LevelsPanel.tsx b/apps/frontend/src/pages/admin/panels/LevelsPanel.tsx index 4bf03b30..46c47327 100644 --- a/apps/frontend/src/pages/admin/panels/LevelsPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/LevelsPanel.tsx @@ -76,10 +76,7 @@ import type { WorkerLevel, WorkerUser, } from '@/types/worker-platform' -import { - ADMIN_DEFAULT_PAGE_SIZE, - buildAdminTablePagination, -} from '@/utils/admin-pagination' +import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination' import { formatAdminDateTime } from '@/utils/admin-time' import { formatMoney } from './shared' @@ -179,8 +176,7 @@ export default function LevelsPanel() { { title: '可见延迟', width: 110, - render: (_, row) => - row.visibleDelaySeconds > 0 ? `${row.visibleDelaySeconds} 秒` : '立即', + render: (_, row) => (row.visibleDelaySeconds > 0 ? `${row.visibleDelaySeconds} 秒` : '立即'), }, { title: '状态', @@ -212,10 +208,7 @@ export default function LevelsPanel() { return (
- + - + diff --git a/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx b/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx index 5bc089be..5f1f3a9e 100644 --- a/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx @@ -66,7 +66,11 @@ export default function ProductRulesPanel() { rule.provider, rule.shopId, rule.categoryName, - ].some((value) => String(value || '').toLowerCase().includes(keywordText)) + ].some((value) => + String(value || '') + .toLowerCase() + .includes(keywordText), + ) }) function resetRuleForm() { @@ -212,9 +216,7 @@ export default function ProductRulesPanel() { }) { const values = form.getFieldsValue() const unitReward = Number(next.sharingUnitReward ?? values.sharingUnitReward ?? 0) - const totalQuantity = Number( - next.sharingTotalQuantity ?? values.sharingTotalQuantity ?? 1, - ) + const totalQuantity = Number(next.sharingTotalQuantity ?? values.sharingTotalQuantity ?? 1) const totalAmount = Number(next.sharingTotalAmount ?? values.sharingTotalAmount ?? 0) if (next.sharingTotalAmount !== undefined) { const quantity = unitReward > 0 ? Math.max(1, Math.round(totalAmount / unitReward)) : 1 @@ -260,9 +262,7 @@ export default function ProductRulesPanel() { const shopId = String(row.shopId || '') return (
- - {row.platform || 'kuaishou'} - + {row.platform || 'kuaishou'} {shopId ? ( {shopId} @@ -290,7 +290,9 @@ export default function ProductRulesPanel() { width: '11%', render: (value) => value ? ( - {value} + + {value} + ) : ( - ), @@ -413,11 +415,7 @@ export default function ProductRulesPanel() { 取消编辑 ) : null} - @@ -455,10 +453,7 @@ export default function ProductRulesPanel() { { validator: (_, value) => { const key = String(value || '').trim() - if ( - !editingRule && - allRules.some((rule) => rule.ruleKey === key) - ) { + if (!editingRule && allRules.some((rule) => rule.ruleKey === key)) { return Promise.reject(new Error('标识已存在')) } return Promise.resolve() @@ -677,11 +672,7 @@ export default function ProductRulesPanel() { > 刷新 - diff --git a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx index 215e372a..598f662c 100644 --- a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx @@ -91,10 +91,7 @@ import type { WorkerLevel, WorkerUser, } from '@/types/worker-platform' -import { - ADMIN_DEFAULT_PAGE_SIZE, - buildAdminTablePagination, -} from '@/utils/admin-pagination' +import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination' import { hasAdminRole } from '@/utils/admin-auth' import { formatAdminDateTime } from '@/utils/admin-time' import { @@ -187,8 +184,7 @@ export default function WorkOrdersPanel() { totalAmount?: number }>() // 仅单独筛选「待验收」时提供批量验收入口(多选状态下不启用勾选,避免误操作) - const isPendingAcceptanceOnly = - statuses.length === 1 && statuses[0] === 'pending_acceptance' + const isPendingAcceptanceOnly = statuses.length === 1 && statuses[0] === 'pending_acceptance' const ordersQuery = useQuery({ queryKey: [ @@ -436,8 +432,7 @@ export default function WorkOrdersPanel() { async function submitProblem(values: { note?: string }) { if (!problemOrder) return const succeeded = await runAction( - () => - markAdminWorkOrderProblem(problemOrder.workOrderId, values.note || ''), + () => markAdminWorkOrderProblem(problemOrder.workOrderId, values.note || ''), '已标记问题单', ) if (!succeeded) return @@ -473,9 +468,7 @@ export default function WorkOrdersPanel() { action, reason: String(values.reason || '').trim(), }), - action === 'return_to_hall' - ? '订单已退回大厅,打手额度已恢复' - : '订单已取消,打手额度已恢复', + action === 'return_to_hall' ? '订单已退回大厅,打手额度已恢复' : '订单已取消,打手额度已恢复', ) if (!succeeded) return setCancelOrder(null) @@ -535,8 +528,7 @@ export default function WorkOrdersPanel() { platformOrderId: row.platformOrderId || '', categoryId: row.categoryId ?? undefined, rewardAmount: Math.round(Number(row.rewardAmount || 0)) / 100, - requiredDepositAmount: - Math.round(Number(row.requiredDepositAmount || 0)) / 100, + requiredDepositAmount: Math.round(Number(row.requiredDepositAmount || 0)) / 100, fieldsText: formatRequirementFieldsText(getRequirementFields(row)), timeoutMinutes: Number(row.timeoutMinutes || 0), timeoutPolicy: row.timeoutPolicy || 'reopen', @@ -586,9 +578,7 @@ export default function WorkOrdersPanel() { categoryId: values.categoryId || null, rewardAmount: Number(values.rewardAmount || 0), requiredDepositAmount: Number(values.requiredDepositAmount || 0), - ...(canEditRequirementFields - ? { fieldsText: String(values.fieldsText || '') } - : {}), + ...(canEditRequirementFields ? { fieldsText: String(values.fieldsText || '') } : {}), timeoutMinutes: Math.max(0, Number(values.timeoutMinutes || 0)), timeoutPolicy: values.timeoutPolicy || 'reopen', }), @@ -602,7 +592,9 @@ export default function WorkOrdersPanel() { content: ( 该订单用户付款 {formatMoney(paymentFen)},发单金额{' '} - {formatMoney(rewardFen)} + + {formatMoney(rewardFen)} + ,本单平台将亏损。确认保存? ), @@ -631,11 +623,7 @@ export default function WorkOrdersPanel() { okText: '确认删除', okButtonProps: { danger: true }, cancelText: '取消', - onOk: () => - runAction( - () => deleteAdminWorkOrder(row.workOrderId), - '工单已删除', - ), + onOk: () => runAction(() => deleteAdminWorkOrder(row.workOrderId), '工单已删除'), }) } @@ -657,9 +645,7 @@ export default function WorkOrdersPanel() { {row.categoryName ? {row.categoryName} : null} {row.orderId ? ( - - 源订单 #{row.orderId} - + 源订单 #{row.orderId} ) : null} {shopName || provider ? ( @@ -669,9 +655,7 @@ export default function WorkOrdersPanel() { {shopName ? `店铺:${shopName}` : ''} ) : null} - - {getDisplayOrderNo(row)} - + {getDisplayOrderNo(row)}
) }, @@ -748,11 +732,11 @@ export default function WorkOrdersPanel() { width: 150, render: (_, row) => ( - - {formatStatus(row.status)} - + {formatStatus(row.status)} {row.status === 'open' && row.pinnedAt ? ( - }>置顶 + }> + 置顶 + ) : null} ), @@ -760,8 +744,7 @@ export default function WorkOrdersPanel() { { title: '打手', width: 140, - render: (_, row) => - row.worker?.displayName || row.worker?.username || '-', + render: (_, row) => row.worker?.displayName || row.worker?.username || '-', }, { title: '资料', @@ -787,27 +770,17 @@ export default function WorkOrdersPanel() { {['pending_material', 'unassigned', 'open'].includes(row.status) ? ( - ) : null} {['pending_material', 'unassigned', 'cancelled'].includes(row.status) ? ( - ) : null} {['pending_material', 'unassigned', 'open'].includes(row.status) ? ( - ) : null} @@ -817,19 +790,12 @@ export default function WorkOrdersPanel() { ) : null} {row.status === 'in_progress' && row.worker ? ( - ) : null} {['pending_material', 'in_progress'].includes(row.status) ? ( - ) : null} @@ -838,9 +804,7 @@ export default function WorkOrdersPanel() { icon={} disabled={Number(row.rewardAmount || 0) <= 0} title={ - Number(row.rewardAmount || 0) <= 0 - ? '接单金额为 0,不能发布到大厅' - : undefined + Number(row.rewardAmount || 0) <= 0 ? '接单金额为 0,不能发布到大厅' : undefined } onClick={() => confirmPublishWorkOrder(row)} > @@ -871,10 +835,7 @@ export default function WorkOrdersPanel() { okText: '确认下架', cancelText: '取消', onOk: () => - runAction( - () => unpublishAdminWorkOrder(row.workOrderId), - '订单已撤回到未分配', - ), + runAction(() => unpublishAdminWorkOrder(row.workOrderId), '订单已撤回到未分配'), }) }} > @@ -885,31 +846,18 @@ export default function WorkOrdersPanel() { ) : null} {row.status === 'accepted' && Number(row.pendingUnfreezeAmount || 0) > 0 ? ( - ) : null} {['in_progress', 'pending_acceptance'].includes(row.status) ? ( - ) : null} @@ -1078,9 +1026,7 @@ export default function WorkOrdersPanel() { {detailOrder.orderItemId || '-'} - - {detailOrder.productName || '-'} - + {detailOrder.productName || '-'} {detailOrder.categoryName || '-'} @@ -1090,9 +1036,7 @@ export default function WorkOrdersPanel() { - {detailOrder.worker?.displayName || - detailOrder.worker?.username || - '-'} + {detailOrder.worker?.displayName || detailOrder.worker?.username || '-'} {formatMoney(detailOrder.rewardAmount)} @@ -1111,10 +1055,7 @@ export default function WorkOrdersPanel() { {detailOrder.deadlineAt ? ( - + {formatAdminDateTime(detailOrder.deadlineAt)} @@ -1150,10 +1091,7 @@ export default function WorkOrdersPanel() {
- {renderDetailFieldValues( - getMaterialDetailItems(detailOrder), - '当前还没有填写资料', - )} + {renderDetailFieldValues(getMaterialDetailItems(detailOrder), '当前还没有填写资料')}
买家主页截图
@@ -1186,9 +1124,7 @@ export default function WorkOrdersPanel() { size={72} /> ) : ( - - 暂无验收图 - + 暂无验收图 )}
@@ -1282,12 +1218,7 @@ export default function WorkOrdersPanel() { name="timeoutMinutes" tooltip="打手抢单后开始计时,超时由系统自动按策略处置;0 表示不限时" > - + ({ validator(_, value) { - if ( - getFieldValue('action') !== 'cancel_order' || - String(value || '').trim() - ) { + if (getFieldValue('action') !== 'cancel_order' || String(value || '').trim()) { return Promise.resolve() } return Promise.reject(new Error('取消订单时请填写退款或撤单原因')) @@ -1465,10 +1393,7 @@ export default function WorkOrdersPanel() { }), ]} > - + ) : null} @@ -1658,9 +1583,7 @@ export default function WorkOrdersPanel() { min={0.01} step={1} addonAfter="元" - onChange={(value) => - syncSharingForm({ unitReward: Number(value) || 0 }) - } + onChange={(value) => syncSharingForm({ unitReward: Number(value) || 0 })} /> - syncSharingForm({ totalAmount: Number(value) || 0 }) - } + onChange={(value) => syncSharingForm({ totalAmount: Number(value) || 0 })} /> @@ -1717,9 +1638,7 @@ export default function WorkOrdersPanel() { { title: '提交时间', render: (_, share) => - share.submittedAt - ? formatAdminDateTime(share.submittedAt) - : '-', + share.submittedAt ? formatAdminDateTime(share.submittedAt) : '-', }, ]} /> @@ -1910,7 +1829,9 @@ function AmountEditableCell({ content: ( 该订单用户付款 {formatMoney(paymentFen)},发单金额{' '} - {formatMoney(nextFen)} + + {formatMoney(nextFen)} + ,本单平台将亏损。确认保存? ), @@ -2002,9 +1923,7 @@ function formatRequirementFieldsText(fields: CollectField[]): string { .map((field) => { const labelPart = `${field.key}:${field.label}` const options = (field.options || []).filter(Boolean) - return options.length > 0 - ? `${labelPart}#${options.join(',')}` - : labelPart + return options.length > 0 ? `${labelPart}#${options.join(',')}` : labelPart }) .join('\n') } @@ -2069,12 +1988,7 @@ function MockWorkOrderForm({ onCreated }: { onCreated: () => Promise }) { - diff --git a/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx b/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx index 081293ad..6462ace3 100644 --- a/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkersPanel.tsx @@ -76,10 +76,7 @@ import type { WorkerLevel, WorkerUser, } from '@/types/worker-platform' -import { - ADMIN_DEFAULT_PAGE_SIZE, - buildAdminTablePagination, -} from '@/utils/admin-pagination' +import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination' import { formatAdminDateTime } from '@/utils/admin-time' import { formatMoney, formatWorkerStatus } from './shared' @@ -189,9 +186,7 @@ export default function WorkersPanel() { render: (_, row) => (
- - {row.displayName || row.username} - + {row.displayName || row.username} {row.workerType === 'internal' ? '内部' : '外部'} @@ -368,17 +363,8 @@ export default function WorkersPanel() { destroyOnHidden >
- - + + diff --git a/apps/frontend/src/pages/admin/panels/shared.tsx b/apps/frontend/src/pages/admin/panels/shared.tsx index 4c53c21e..ca36667c 100644 --- a/apps/frontend/src/pages/admin/panels/shared.tsx +++ b/apps/frontend/src/pages/admin/panels/shared.tsx @@ -1,9 +1,6 @@ import { Descriptions, Space, Tag, Typography } from 'antd' import ImagePreviewList from '@/components/files/ImagePreviewList' -import { - DeadlineCountdown, - formatTimeoutPolicyLabel, -} from '@/components/DeadlineCountdown' +import { DeadlineCountdown, formatTimeoutPolicyLabel } from '@/components/DeadlineCountdown' import type { CollectField, UploadedFile, WorkOrder, WorkOrderShare } from '@/types/worker-platform' import { formatAdminDateTime } from '@/utils/admin-time' @@ -19,9 +16,7 @@ type MappedRequirementField = { export function getRequirementFields(order: WorkOrder | null): CollectField[] { if (!order) return [] - const rawFields = Array.isArray(order.requirement?.fields) - ? order.requirement.fields - : [] + const rawFields = Array.isArray(order.requirement?.fields) ? order.requirement.fields : [] return rawFields .map((item) => { const source = asRecord(item) @@ -83,10 +78,7 @@ export function getMaterialDetailItems(order: WorkOrder): DetailFieldItem[] { return items } -export function renderDetailFieldValues( - items: DetailFieldItem[], - emptyText: string, -) { +export function renderDetailFieldValues(items: DetailFieldItem[], emptyText: string) { if (items.length === 0) { return {emptyText} } @@ -139,9 +131,7 @@ export function renderMaterialSummary(order: WorkOrder) { getRequirementFields(order).map((field) => [field.key, field.label]), ) const fields = getMaterialFields(order) - const entries = Object.entries(fields).filter(([, value]) => - String(value || '').trim(), - ) + const entries = Object.entries(fields).filter(([, value]) => String(value || '').trim()) const screenshots = getMaterialScreenshots(order) if (entries.length === 0 && screenshots.length === 0) { return - @@ -154,9 +144,7 @@ export function renderMaterialSummary(order: WorkOrder) { {fieldLabelMap.get(key) || key}: {value} ))} - {screenshots.length > 0 ? ( - - ) : null} + {screenshots.length > 0 ? : null}
) } @@ -167,17 +155,13 @@ export function getAcceptanceFiles(row: WorkOrder): UploadedFile[] { export function getAcceptanceImageUrls(row: WorkOrder): string[] { return Array.isArray(row.acceptance?.imageUrls) - ? row.acceptance.imageUrls - .map((item) => String(item || '').trim()) - .filter(Boolean) + ? row.acceptance.imageUrls.map((item) => String(item || '').trim()).filter(Boolean) : [] } export function getAcceptanceSubmittedAt(order: WorkOrder): string | null { const acceptanceSubmittedAt = - typeof order.acceptance?.submittedAt === 'string' - ? order.acceptance.submittedAt - : null + typeof order.acceptance?.submittedAt === 'string' ? order.acceptance.submittedAt : null return order.submittedAt || acceptanceSubmittedAt || null } diff --git a/apps/frontend/src/pages/admin/platform/AdminPlatformFulfillmentPage.tsx b/apps/frontend/src/pages/admin/platform/AdminPlatformFulfillmentPage.tsx index 18f436cc..f58dafca 100644 --- a/apps/frontend/src/pages/admin/platform/AdminPlatformFulfillmentPage.tsx +++ b/apps/frontend/src/pages/admin/platform/AdminPlatformFulfillmentPage.tsx @@ -126,7 +126,9 @@ function FulfillmentRoutingPanel() { const [executors, setExecutors] = useState({}) const [rules, setRules] = useState([]) const [previewInput, setPreviewInput] = useState('') - const [previewResult, setPreviewResult] = useState(null) + const [previewResult, setPreviewResult] = useState( + null, + ) const enabledRuleCount = rules.filter((rule) => rule.enabled !== false).length const selectedExecutorKey = previewResult?.fulfillmentRoute?.selectedExecutorKey || '' @@ -276,7 +278,11 @@ function FulfillmentRoutingPanel() {
- +
履约路由 - +
{ROUTING_EXECUTORS.map((executor) => (
@@ -610,14 +621,23 @@ function KuaishouFeifeiFulfillmentPanel() { {errorMessage ? : null}
- + - @@ -626,7 +646,11 @@ function KuaishouFeifeiFulfillmentPanel() { {lastSyncText || 'feifei 商品名会按规范化名字匹配 91 商品名。'}} + extra={ + + {lastSyncText || 'feifei 商品名会按规范化名字匹配 91 商品名。'} + + } > sum + Math.max(0, Number(item.inventory || 0)), 0), + inventoryTotal: skuItems.reduce( + (sum, item) => sum + Math.max(0, Number(item.inventory || 0)), + 0, + ), overrideRuleCount: overrideRules.length, } @@ -739,7 +766,9 @@ function KuaishouCloudFulfillmentPanel() { fetchAdminCloudtentaclesSourceConfig(), fetchAdminCloudtentaclesOverrideRules(), ]) - const nextSources = Array.isArray(sourceResponse.data.sources) ? sourceResponse.data.sources : [] + const nextSources = Array.isArray(sourceResponse.data.sources) + ? sourceResponse.data.sources + : [] const nextSessions = sourceResponse.data.sessions || {} const nextRows = nextSources.map((source) => { const session = nextSessions[source.key] || null @@ -839,7 +868,11 @@ function KuaishouCloudFulfillmentPanel() { ) } - function updateDeliveryItem(ruleIndex: number, itemIndex: number, patch: Partial) { + function updateDeliveryItem( + ruleIndex: number, + itemIndex: number, + patch: Partial, + ) { setOverrideRules((current) => current.map((rule, currentRuleIndex) => { if (currentRuleIndex !== ruleIndex) { @@ -877,7 +910,9 @@ function KuaishouCloudFulfillmentPanel() { return rule } - const nextItems = rule.deliveryItems.filter((_, currentItemIndex) => currentItemIndex !== itemIndex) + const nextItems = rule.deliveryItems.filter( + (_, currentItemIndex) => currentItemIndex !== itemIndex, + ) return { ...rule, deliveryItems: nextItems.length ? nextItems : [createDeliveryItem(skuItems)], @@ -903,7 +938,10 @@ function KuaishouCloudFulfillmentPanel() { {errorMessage ? : null}
- + @@ -935,7 +973,9 @@ function KuaishouCloudFulfillmentPanel() { {formatSourceLabel(source)} {source.username || source.key} - + {source.ready ? '可用' : source.enabled === false ? '已停用' : '未登录'} @@ -964,7 +1004,12 @@ function KuaishouCloudFulfillmentPanel() { } > {skuErrorMessage ? ( - + ) : null} @@ -980,7 +1025,9 @@ function KuaishouCloudFulfillmentPanel() { {matchedSku ? ( - 命中 #{matchedSku.id} {matchedSku.name} + + 命中 #{matchedSku.id} {matchedSku.name} + ) : matchInput.trim() ? ( 未命中 ) : null} @@ -1012,7 +1059,12 @@ function KuaishouCloudFulfillmentPanel() { } > {overrideErrorMessage ? ( - + ) : null} {overrideRules.length === 0 ? ( @@ -1048,7 +1100,9 @@ function KuaishouCloudFulfillmentPanel() { updateRule(ruleIndex, { productName: event.target.value })} + onChange={(event) => + updateRule(ruleIndex, { productName: event.target.value }) + } /> handleDeliverySkuChange(ruleIndex, itemIndex, Number(skuId || 0))} + onChange={(skuId) => + handleDeliverySkuChange(ruleIndex, itemIndex, Number(skuId || 0)) + } /> - +
))} @@ -1133,7 +1195,9 @@ function KuaishouCloudFulfillmentPanel() { minWidth: 260, render: (_, row) => (
- {row.name || '-'} + + {row.name || '-'} + #{row.id} · {row.description || row.name || '-'} @@ -1147,7 +1211,12 @@ function KuaishouCloudFulfillmentPanel() { render: (value) => {String(value || '-')}, }, { title: '价格', dataIndex: 'price', width: 120, render: formatCloudPrice }, - { title: '库存', dataIndex: 'inventory', width: 120, sorter: (a, b) => a.inventory - b.inventory }, + { + title: '库存', + dataIndex: 'inventory', + width: 120, + sorter: (a, b) => a.inventory - b.inventory, + }, { title: '发货限制', width: 150, @@ -1186,9 +1255,9 @@ function MetricCard({ label, value, detail }: { label: string; value: string; de function normalizeRoutingPriority(value: unknown) { const rawItems = Array.isArray(value) ? value : [] - const allowed = ROUTING_EXECUTORS - .filter((executor) => executor.key !== 'manual_dispatch') - .map((executor) => executor.key) + const allowed = ROUTING_EXECUTORS.filter((executor) => executor.key !== 'manual_dispatch').map( + (executor) => executor.key, + ) const seen = new Set() const items = rawItems .map((item) => String(item || '').trim()) @@ -1198,16 +1267,20 @@ function normalizeRoutingPriority(value: unknown) { } function normalizeRoutingExecutors(value: unknown): AdminFulfillmentRoutingConfig['executors'] { - const source = value && typeof value === 'object' && !Array.isArray(value) - ? value as AdminFulfillmentRoutingConfig['executors'] - : {} + const source = + value && typeof value === 'object' && !Array.isArray(value) + ? (value as AdminFulfillmentRoutingConfig['executors']) + : {} - return ROUTING_EXECUTORS.reduce((result, executor) => { - result[executor.key] = { - enabled: source[executor.key]?.enabled !== false, - } - return result - }, {}) + return ROUTING_EXECUTORS.reduce( + (result, executor) => { + result[executor.key] = { + enabled: source[executor.key]?.enabled !== false, + } + return result + }, + {}, + ) } function normalizeRoutingRule(rule: AdminFulfillmentRoutingRule): EditableRoutingRule { @@ -1272,7 +1345,9 @@ function normalizeEditableRule(rule: AdminCloudtentaclesOverrideRule): EditableO } } -function createDeliveryItem(skuItems: AdminCloudtentaclesSkuItem[]): AdminCloudtentaclesOverrideDeliveryItem { +function createDeliveryItem( + skuItems: AdminCloudtentaclesSkuItem[], +): AdminCloudtentaclesOverrideDeliveryItem { const firstSku = skuItems[0] return { cloudSkuId: Number(firstSku?.id || 0) || 0, @@ -1300,7 +1375,9 @@ function normalizeMatchName(value: unknown) { .trim() } -function formatSourceLabel(source: Pick) { +function formatSourceLabel( + source: Pick, +) { return source.label || source.username || source.key } @@ -1316,4 +1393,3 @@ function formatSkuOption(sku: AdminCloudtentaclesSkuItem) { function createRuleId() { return `rule_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` } - diff --git a/apps/frontend/src/pages/admin/platform/AdminPlatformShopsPage.tsx b/apps/frontend/src/pages/admin/platform/AdminPlatformShopsPage.tsx index 1d22067a..aba78133 100644 --- a/apps/frontend/src/pages/admin/platform/AdminPlatformShopsPage.tsx +++ b/apps/frontend/src/pages/admin/platform/AdminPlatformShopsPage.tsx @@ -124,9 +124,13 @@ export default function AdminPlatformShopsPage() { const activeTab = normalizePlatformTab(searchParams.get('tab')) const [loading, setLoading] = useState(true) const [errorMessage, setErrorMessage] = useState('') - const [notificationConfig, setNotificationConfig] = useState(null) + const [notificationConfig, setNotificationConfig] = useState( + null, + ) const [scheduledJobs, setScheduledJobs] = useState(null) - const [industryConfig, setIndustryConfig] = useState(null) + const [industryConfig, setIndustryConfig] = useState( + null, + ) const [feifeiConfig, setFeifeiConfig] = useState(null) const [cloudtentaclesConfig, setCloudtentaclesConfig] = useState(null) @@ -213,25 +217,23 @@ export default function AdminPlatformShopsPage() { { key: 'notifications', label: '内部通知', - children: notificationConfig && scheduledJobs ? ( - - ) : ( - - ), + children: + notificationConfig && scheduledJobs ? ( + + ) : ( + + ), }, { key: 'kuaishouIndustry', label: '行业电子凭证', children: industryConfig ? ( - + ) : ( ), @@ -399,9 +401,7 @@ function NotificationPanel({ function updateJob(index: number, nextJob: AdminScheduledJobItem) { updateJobs({ ...scheduledJobs.source, - jobs: jobsWithMergedAccounts.map((item, jobIndex) => - jobIndex === index ? nextJob : item, - ), + jobs: jobsWithMergedAccounts.map((item, jobIndex) => (jobIndex === index ? nextJob : item)), }) } @@ -411,10 +411,19 @@ function NotificationPanel({ title="通知渠道" extra={ - - @@ -554,8 +563,14 @@ function RecipientList }: { title: string secretField: 'deviceKey' | 'apiKey' - items: Array>> - onChange: (items: Array>>) => void + items: Array< + T & Partial> + > + onChange: ( + items: Array< + T & Partial> + >, + ) => void }) { function addRecipient() { onChange([ @@ -569,12 +584,24 @@ function RecipientList ]) } - function updateRecipient(index: number, patch: Partial>) { + function updateRecipient( + index: number, + patch: Partial>, + ) { onChange(items.map((item, itemIndex) => (itemIndex === index ? { ...item, ...patch } : item))) } return ( - } onClick={addRecipient}>新增}> + } onClick={addRecipient}> + 新增 + + } + > {items.length === 0 ? ( ) : ( @@ -583,23 +610,37 @@ function RecipientList
updateRecipient(index, { enabled } as Partial>)} + onChange={(enabled) => + updateRecipient(index, { enabled } as Partial< + T & Record<'deviceKey' | 'apiKey', string> + >) + } /> updateRecipient(index, { name: event.target.value } as Partial>)} + onChange={(event) => + updateRecipient(index, { name: event.target.value } as Partial< + T & Record<'deviceKey' | 'apiKey', string> + >) + } /> updateRecipient(index, { [secretField]: event.target.value, } as Partial>) } /> -
))} @@ -634,9 +675,7 @@ function ScheduledJobCard({ onChange={(enabled) => onChange({ ...job, enabled })} /> {formatScheduledJobTitle(job)} - - {job.enabled ? '已启用' : '已停用'} - + {job.enabled ? '已启用' : '已停用'} } extra={ @@ -660,9 +699,7 @@ function ScheduledJobCard({ label="每次扫描数量" value={Number(job.config?.scanLimit ?? 50)} min={1} - onChange={(scanLimit) => - onChange({ ...job, config: { ...job.config, scanLimit } }) - } + onChange={(scanLimit) => onChange({ ...job, config: { ...job.config, scanLimit } })} />
{runtime ? ( @@ -701,9 +738,7 @@ function ScheduledJobCard({ onChange={(enabled) => onChange({ ...job, enabled })} /> {formatScheduledJobTitle(job)} - - {job.enabled ? '已启用' : '已停用'} - + {job.enabled ? '已启用' : '已停用'} } extra={ @@ -713,8 +748,7 @@ function ScheduledJobCard({ } > - 将有押金工单验收通过后进入待解冻的押金,在到期后自动转入可用余额(验收后 3 - 天到账)。 + 将有押金工单验收通过后进入待解冻的押金,在到期后自动转入可用余额(验收后 3 天到账)。
- onChange({ ...job, config: { ...job.config, scanLimit } }) - } + onChange={(scanLimit) => onChange({ ...job, config: { ...job.config, scanLimit } })} />
{runtime ? ( @@ -758,7 +790,10 @@ function ScheduledJobCard({ const accounts = job.config?.accounts || [] const defaultThreshold = Number(job.config?.assetThreshold ?? 500) - function updateAccount(sourceKey: string, patch: Partial) { + function updateAccount( + sourceKey: string, + patch: Partial, + ) { onChange({ ...job, config: { @@ -849,7 +884,8 @@ function ScheduledJobCard({ {formatHealthStatus(accountRuntime.status)} - 余额 {accountRuntime.asset ?? '-'} / 阈值 {accountRuntime.threshold ?? account.assetThreshold} + 余额 {accountRuntime.asset ?? '-'} / 阈值{' '} + {accountRuntime.threshold ?? account.assetThreshold} @@ -869,12 +905,19 @@ function ScheduledJobCard({ size="small" title={ - onChange({ ...job, enabled })} /> + onChange({ ...job, enabled })} + /> {formatScheduledJobTitle(job)} {job.enabled ? '已启用' : '已停用'} } - extra={} + extra={ + + } >
) : null} @@ -953,9 +994,7 @@ function mergeScheduledJobAccounts( sourceKey, label: String(configured?.label || item.label || sourceKey).trim(), enabled: configured ? configured.enabled !== false : item.enabled !== false, - assetThreshold: Number( - configured?.assetThreshold ?? defaultAssetThreshold, - ), + assetThreshold: Number(configured?.assetThreshold ?? defaultAssetThreshold), } }) const mergedKeys = new Set(merged.map((item) => item.sourceKey)) @@ -997,13 +1036,15 @@ function formatMonitorAccountMeta( return '账号配置不存在' } - return [ - source.username ? `登录名 ${source.username}` : '', - source.phoneMasked ? `手机 ${source.phoneMasked}` : '', - source.hasToken ? 'Token 已保存' : '未登录', - ] - .filter(Boolean) - .join(' · ') || account.sourceKey + return ( + [ + source.username ? `登录名 ${source.username}` : '', + source.phoneMasked ? `手机 ${source.phoneMasked}` : '', + source.hasToken ? 'Token 已保存' : '未登录', + ] + .filter(Boolean) + .join(' · ') || account.sourceKey + ) } function getAccountRuntime( @@ -1190,9 +1231,7 @@ function KuaishouIndustryPanel({ function updateShop(index: number, patch: Partial) { updateSource({ - shops: shops.map((shop, shopIndex) => - shopIndex === index ? { ...shop, ...patch } : shop, - ), + shops: shops.map((shop, shopIndex) => (shopIndex === index ? { ...shop, ...patch } : shop)), }) } @@ -1281,7 +1320,9 @@ function KuaishouIndustryPanel({ return (
{status.label} - {row.accessTokenMasked ? {row.accessTokenMasked} : null} + {row.accessTokenMasked ? ( + {row.accessTokenMasked} + ) : null}
) }, @@ -1291,8 +1332,12 @@ function KuaishouIndustryPanel({ minWidth: 220, render: (_, row) => (
- {row.accessTokenExpiresAt ? formatAdminDateTime(row.accessTokenExpiresAt) : '-'} - {formatIndustryTokenCountdown(row.accessTokenExpiresInSeconds)} + + {row.accessTokenExpiresAt ? formatAdminDateTime(row.accessTokenExpiresAt) : '-'} + + + {formatIndustryTokenCountdown(row.accessTokenExpiresInSeconds)} +
), }, @@ -1325,10 +1370,18 @@ function KuaishouIndustryPanel({ - -
updateShop(index, { accessTokenExpiresAt })} + onChange={(accessTokenExpiresAt) => + updateShop(index, { accessTokenExpiresAt }) + } /> updateShop(index, { refreshTokenExpiresAt })} + onChange={(refreshTokenExpiresAt) => + updateShop(index, { refreshTokenExpiresAt }) + } />
最近错误 updateShop(index, { lastRefreshError: event.target.value })} + onChange={(event) => + updateShop(index, { lastRefreshError: event.target.value }) + } />
@@ -1596,7 +1657,9 @@ function KuaishouFeifeiPlatformPanel({ try { const response = await syncAdminKuaishouFeifeiProducts({ status: 'on_sale' }) onChange(response.data) - showSuccess(`商品映射已同步:商品 ${response.data.sync.productCount} 个,规则 ${response.data.sync.ruleCount} 条`) + showSuccess( + `商品映射已同步:商品 ${response.data.sync.productCount} 个,规则 ${response.data.sync.ruleCount} 条`, + ) } catch (error) { showError(error instanceof Error ? error.message : '同步 kuaishou-feifei 商品映射失败') } finally { @@ -1636,9 +1699,20 @@ function KuaishouFeifeiPlatformPanel({ return (
- - - + + +
- updateSource({ enabled })} /> + updateSource({ enabled })} + />
接口地址 - updateSource({ baseUrl: event.target.value })} /> + updateSource({ baseUrl: event.target.value })} + />
App Key - updateSource({ appKey: event.target.value })} /> + updateSource({ appKey: event.target.value })} + />
App Secret - updateSource({ appSecret: event.target.value })} /> + updateSource({ appSecret: event.target.value })} + />
- updateSource({ timeoutMs })} /> + updateSource({ timeoutMs })} + />
通知地址 - updateSource({ notifyUrl: event.target.value })} /> + updateSource({ notifyUrl: event.target.value })} + />
- setMatchInput(event.target.value)} onPressEnter={previewMatch} /> - + setMatchInput(event.target.value)} + onPressEnter={previewMatch} + /> + {matchResult ? ( ${matchResult.match.productCode}` : matchResult.normalizedProductName} + description={ + matchResult.match + ? `${matchResult.match.productName} -> ${matchResult.match.productCode}` + : matchResult.normalizedProductName + } /> ) : null} @@ -1702,35 +1808,50 @@ function KuaishouFeifeiPlatformPanel({ title: '启用', width: 80, render: (_, row, index) => ( - updateRule(index, { enabled })} /> + updateRule(index, { enabled })} + /> ), }, { title: '商品名', minWidth: 260, render: (_, row, index) => ( - updateRule(index, { productName: event.target.value })} /> + updateRule(index, { productName: event.target.value })} + /> ), }, { title: 'product_code', minWidth: 160, render: (_, row, index) => ( - updateRule(index, { productCode: event.target.value })} /> + updateRule(index, { productCode: event.target.value })} + /> ), }, { title: '展示名', minWidth: 220, render: (_, row, index) => ( - updateRule(index, { skuName: event.target.value })} /> + updateRule(index, { skuName: event.target.value })} + /> ), }, { title: '备注', minWidth: 180, render: (_, row, index) => ( - updateRule(index, { notes: event.target.value })} /> + updateRule(index, { notes: event.target.value })} + /> ), }, ]} @@ -1773,9 +1894,10 @@ function CloudtentaclesPlatformPanel({ sources: config.sources, }) onChange(response.data) - const nextKey = selectedSourceKey && response.data.sources.some((item) => item.key === selectedSourceKey) - ? selectedSourceKey - : response.data.sources[0]?.key || '' + const nextKey = + selectedSourceKey && response.data.sources.some((item) => item.key === selectedSourceKey) + ? selectedSourceKey + : response.data.sources[0]?.key || '' setSelectedSourceKey(nextKey) showSuccess('kuaishou-lewan 账号配置已保存') } catch (error) { @@ -1791,9 +1913,8 @@ function CloudtentaclesPlatformPanel({ await deleteAdminCloudtentaclesSource(sourceKey) const response = await fetchAdminCloudtentaclesSourceConfig() onChange(response.data) - const nextKey = selectedSourceKey === sourceKey - ? response.data.sources[0]?.key || '' - : selectedSourceKey + const nextKey = + selectedSourceKey === sourceKey ? response.data.sources[0]?.key || '' : selectedSourceKey setSelectedSourceKey( response.data.sources.some((item) => item.key === nextKey) ? nextKey @@ -1906,9 +2027,18 @@ function CloudtentaclesPlatformPanel({ title="kuaishou-lewan 履约账号" extra={ - updateConfig({ enabled })} /> - - + updateConfig({ enabled })} + /> + + } > @@ -1932,8 +2062,20 @@ function CloudtentaclesPlatformPanel({ {formatCloudtentaclesSourceLabel(source)} {source.username || source.key} - - {session?.hasToken ? '已登录' : source.enabled === false ? '已停用' : '未登录'} + + {session?.hasToken + ? '已登录' + : source.enabled === false + ? '已停用' + : '未登录'} ) @@ -1943,7 +2085,11 @@ function CloudtentaclesPlatformPanel({ - !nextConfig.data.sources.some( - (item) => item.key === source.key, - ), + !nextConfig.data.sources.some((item) => item.key === source.key), ), ], }) @@ -2158,7 +2302,9 @@ function CloudtentaclesPlatformPanel({ 查看号码列表 - {debugResult ?
{JSON.stringify(debugResult, null, 2)}
: null} + {debugResult ? ( +
{JSON.stringify(debugResult, null, 2)}
+ ) : null} {vnItems !== null ? ( className="platform-section-gap" @@ -2195,7 +2341,13 @@ function CloudtentaclesPlatformPanel({ dataIndex: 'countTime', render: (countTime: number) => ( {formatCountdownSeconds(Number(countTime))} @@ -2217,11 +2369,7 @@ function CloudtentaclesPlatformPanel({ cancelText="取消" onConfirm={() => void backVn(item)} > - @@ -2237,7 +2385,9 @@ function CloudtentaclesPlatformPanel({ ) } -function formatCloudtentaclesSourceLabel(source: Pick) { +function formatCloudtentaclesSourceLabel( + source: Pick, +) { return String(source.label || source.username || source.key || '').trim() || source.key } @@ -2606,8 +2756,20 @@ function AffiliateDashPlatformPanel() { { title: 'sku', dataIndex: 'sku', key: 'sku', width: 180 }, { title: '名称', dataIndex: 'displayName', key: 'displayName' }, { title: '单价', dataIndex: 'priceAmount', key: 'priceAmount', width: 90 }, - { title: '库存', dataIndex: 'stock', key: 'stock', width: 70, render: (v: number) => (v === -1 ? '不限' : v) }, - { title: '状态', dataIndex: 'status', key: 'status', width: 80, render: (v: string) => {v} }, + { + title: '库存', + dataIndex: 'stock', + key: 'stock', + width: 70, + render: (v: number) => (v === -1 ? '不限' : v), + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 80, + render: (v: string) => {v}, + }, ] return ( @@ -2615,15 +2777,21 @@ function AffiliateDashPlatformPanel() { title="affiliate-dash 发货平台" extra={ - - + + } > - {errorMessage ? : null} + {errorMessage ? ( + + ) : null} @@ -2637,25 +2805,53 @@ function AffiliateDashPlatformPanel() { BASE_URL - setBaseUrl(e.target.value)} placeholder="https://skin.khhao.com" /> + setBaseUrl(e.target.value)} + placeholder="https://skin.khhao.com" + /> App Key - setAppKey(e.target.value)} placeholder="ak_..." /> + setAppKey(e.target.value)} + placeholder="ak_..." + /> App Secret - setAppSecret(e.target.value)} placeholder="sk_..." /> + setAppSecret(e.target.value)} + placeholder="sk_..." + /> 回调 Secret - setCallbackSecret(e.target.value)} placeholder="cb_..." /> + setCallbackSecret(e.target.value)} + placeholder="cb_..." + /> 超时(ms) - setTimeoutMs(Number(v || 10000))} /> + setTimeoutMs(Number(v || 10000))} + /> 时间容差(s) - setTimestampToleranceSeconds(Number(v || 300))} /> + setTimestampToleranceSeconds(Number(v || 300))} + /> 回调 URL(展示) @@ -2664,7 +2860,9 @@ function AffiliateDashPlatformPanel() { {wallet ? ( 钱包余额 - {wallet.availableBalance} {wallet.currency} + + {wallet.availableBalance} {wallet.currency} + (冻结 {wallet.frozenBalance}) ) : null} @@ -2693,7 +2891,10 @@ function AffiliateDashPlatformPanel() { /> ))} - @@ -2708,7 +2909,9 @@ function AffiliateDashPlatformPanel() { value={matchInput} onChange={(e) => setMatchInput(e.target.value)} /> - + {matchResult ? ( {matchResult.matched diff --git a/apps/frontend/src/pages/claim/ClaimAffiliateDashSteps.tsx b/apps/frontend/src/pages/claim/ClaimAffiliateDashSteps.tsx index 84c5d0e3..145092f1 100644 --- a/apps/frontend/src/pages/claim/ClaimAffiliateDashSteps.tsx +++ b/apps/frontend/src/pages/claim/ClaimAffiliateDashSteps.tsx @@ -42,7 +42,7 @@ export function AffiliateDashClaimPanel({ ) const bindMismatch = Boolean( affiliateDash.bindMismatch || - (bound && boundAccount && expectedAccount && !boundAccountMatched), + (bound && boundAccount && expectedAccount && !boundAccountMatched), ) const canSubmit = Boolean(bound && boundAccountMatched && !bindMismatch) const displayBoundAccount = boundAccount || '-' @@ -173,11 +173,7 @@ export function AffiliateDashClaimPanel({ ) } -export function AffiliateDashResultStep({ - snapshot, -}: { - snapshot: ClaimSnapshot -}) { +export function AffiliateDashResultStep({ snapshot }: { snapshot: ClaimSnapshot }) { const affiliateDash = snapshot.affiliateDash const success = Boolean(affiliateDash && !snapshot.isRedeemFailed) diff --git a/apps/frontend/src/pages/claim/ClaimFeifeiSteps.tsx b/apps/frontend/src/pages/claim/ClaimFeifeiSteps.tsx index 36edf0e7..fbb6f722 100644 --- a/apps/frontend/src/pages/claim/ClaimFeifeiSteps.tsx +++ b/apps/frontend/src/pages/claim/ClaimFeifeiSteps.tsx @@ -1,14 +1,6 @@ -import { - CheckCircleOutlined, - ExclamationCircleOutlined, - LinkOutlined, -} from '@ant-design/icons' +import { CheckCircleOutlined, ExclamationCircleOutlined, LinkOutlined } from '@ant-design/icons' import { Alert, Button, Card, Typography } from 'antd' -import type { - ClaimKuaishouFeifeiFlowInfo, - ClaimOrderInfo, - ClaimProductInfo, -} from '@/types/claim' +import type { ClaimKuaishouFeifeiFlowInfo, ClaimOrderInfo, ClaimProductInfo } from '@/types/claim' import type { ClaimSnapshot } from './claim-snapshot' import { InfoTile, UidEditRow } from './claim-shared' @@ -59,11 +51,7 @@ export function FeifeiClaimPanel({
- + @@ -303,7 +308,9 @@ export function ClaimResultStep({ snapshot }: { snapshot: ClaimSnapshot }) { {snapshot.resultVariant === 'warning' ? ( ) : ( - + )}
{snapshot.resultTitle} diff --git a/apps/frontend/src/pages/claim/ClaimUidStep.tsx b/apps/frontend/src/pages/claim/ClaimUidStep.tsx index 9c12c3ff..99f245a7 100644 --- a/apps/frontend/src/pages/claim/ClaimUidStep.tsx +++ b/apps/frontend/src/pages/claim/ClaimUidStep.tsx @@ -36,9 +36,7 @@ export function ClaimUidStep({ -

- 游戏编号需与游戏内角色编号一致,填错将导致无法发货。 -

+

游戏编号需与游戏内角色编号一致,填错将导致无法发货。

- {complete ? ( - + ) : null} {!complete && lookupOrders.length > 1 ? ( @@ -103,7 +112,9 @@ export default function CollectPage() { key={field.key} label={field.label} name={field.key} - rules={field.required ? [{ required: true, message: `请选择/填写${field.label}` }] : []} + rules={ + field.required ? [{ required: true, message: `请选择/填写${field.label}` }] : [] + } > {field.type === 'select' && (field.options || []).length > 0 ? ( setScreenshotsByOrder((prev) => ({ @@ -171,5 +180,7 @@ export default function CollectPage() { } function isCollectOrderNotFoundError(error: unknown) { - return String((error as { errorCode?: string })?.errorCode || '').trim() === 'collect_order_not_found' + return ( + String((error as { errorCode?: string })?.errorCode || '').trim() === 'collect_order_not_found' + ) } diff --git a/apps/frontend/src/pages/worker/WorkerHallPage.tsx b/apps/frontend/src/pages/worker/WorkerHallPage.tsx index 44b873bb..cde7817b 100644 --- a/apps/frontend/src/pages/worker/WorkerHallPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerHallPage.tsx @@ -1,4 +1,10 @@ -import { ClockCircleOutlined, PushpinOutlined, ReloadOutlined, SearchOutlined, TrophyOutlined } from '@ant-design/icons' +import { + ClockCircleOutlined, + PushpinOutlined, + ReloadOutlined, + SearchOutlined, + TrophyOutlined, +} from '@ant-design/icons' import { useQuery, useQueryClient } from '@tanstack/react-query' import { Alert, @@ -96,8 +102,7 @@ export default function WorkerHallPage() { }), ]) - const pendingAcceptanceCount = - pendingAcceptanceResponse.data.pagination.total + const pendingAcceptanceCount = pendingAcceptanceResponse.data.pagination.total const problemCount = problemResponse.data.pagination.total return { @@ -124,10 +129,8 @@ export default function WorkerHallPage() { const pendingAcceptanceCount = hallSummaryQuery.data?.pendingAcceptanceCount || 0 const problemCount = hallSummaryQuery.data?.problemCount || 0 const maxActiveOrders = Number(worker?.level?.permissions.maxActiveOrders || 0) - const remainingSlots = - maxActiveOrders > 0 ? Math.max(0, maxActiveOrders - activeCount) : null - const shouldWarn = - (remainingSlots !== null && remainingSlots <= 0) || problemCount > 0 + const remainingSlots = maxActiveOrders > 0 ? Math.max(0, maxActiveOrders - activeCount) : null + const shouldWarn = (remainingSlots !== null && remainingSlots <= 0) || problemCount > 0 async function refreshAll() { await Promise.all([ @@ -199,14 +202,13 @@ export default function WorkerHallPage() { { label: '当前余额', value: worker ? formatMoney(worker.wallet.availableAmount) : '-', - note: worker ? `冻结押金 ${formatMoney(worker.wallet.frozenDepositAmount)}` : '可用于抢单冻结', + note: worker + ? `冻结押金 ${formatMoney(worker.wallet.frozenDepositAmount)}` + : '可用于抢单冻结', }, { label: '接单上限', - value: - remainingSlots === null - ? '未配置' - : `${activeCount}/${maxActiveOrders} 单`, + value: remainingSlots === null ? '未配置' : `${activeCount}/${maxActiveOrders} 单`, note: remainingSlots === null ? '当前等级待配置' @@ -222,11 +224,17 @@ export default function WorkerHallPage() {
- 余额 {worker ? formatMoney(worker.wallet.availableAmount) : '-'} + 余额{' '} + + {worker ? formatMoney(worker.wallet.availableAmount) : '-'} + | - 上限 {remainingSlots === null ? '-' : `${activeCount}/${maxActiveOrders}`} + 上限{' '} + + {remainingSlots === null ? '-' : `${activeCount}/${maxActiveOrders}`} +
@@ -242,20 +250,14 @@ export default function WorkerHallPage() {
自动刷新 - +
-
© {new Date().getFullYear()} 王大锤 · 保留所有权利
+
+ © {new Date().getFullYear()} 王大锤 · 保留所有权利 +
{/* 登录注册表单区 */} diff --git a/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx b/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx index a38e6df6..6afd4604 100644 --- a/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx @@ -80,7 +80,9 @@ const PRESET_NOTE_TAGS: Array<{ label: string; theme: NoteTheme }> = [ ] function resolveNoteTheme(note?: string): NoteTheme { - const text = String(note || '').trim().toLowerCase() + const text = String(note || '') + .trim() + .toLowerCase() if (!text) return 'default' // 1. 危险 / 异常 / 问题类 (Red) @@ -352,7 +354,11 @@ export default function WorkerOrdersPage() { {row.categoryName ? {row.categoryName} : null} {row.myShare ? 拼单 : null} - + {getDisplayOrderNo(row)} @@ -373,7 +379,11 @@ export default function WorkerOrdersPage() { ) : null} {row.status === 'problem' && String(row.problemNote || '').trim() ? ( - + {row.problemNote} ) : null} @@ -410,7 +420,11 @@ export default function WorkerOrdersPage() { const draftCount = draftFiles.length if (!hasAcceptance && !submittedAt && draftCount === 0) { - return 未提交 + return ( + + 未提交 + + ) } return (
@@ -431,7 +445,11 @@ export default function WorkerOrdersPage() {
) : null} {getWorkerAcceptance(row)?.note ? ( - + {getWorkerAcceptance(row)?.note} ) : null} @@ -443,16 +461,18 @@ export default function WorkerOrdersPage() { title: '快捷复制', width: 250, render: (_, row) => { - const items = getMaterialDetailItems(row).filter((item) => - String(item.value || '').trim(), - ) + const items = getMaterialDetailItems(row).filter((item) => String(item.value || '').trim()) const canCopyInfo = getCopyOrderInfoItems(row).length > 0 const screenshots = getMaterialScreenshots(row) return (
{items.length > 0 ? ( items.map((item) => ( - + {item.label}: {item.value} )) @@ -461,9 +481,7 @@ export default function WorkerOrdersPage() { 暂无资料信息 )} - {screenshots.length > 0 ? ( - - ) : null} + {screenshots.length > 0 ? : null}
待解冻押金 @@ -1029,10 +1058,7 @@ export default function WorkerProfilePage() { -