From e097ddad3d5be78908f275b8d43ec93ed149d540 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Tue, 11 Aug 2026 14:06:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E9=A2=86=E5=8F=96=E9=93=BE?= =?UTF-8?q?=E6=8E=A5=E9=95=BF=E6=9C=9F=E6=9C=89=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.server.example | 1 + README.md | 1 + apps/backend/src/config/defaults.ts | 3 ++- apps/backend/src/config/env-overrides.test.ts | 8 ++++++++ .../backend/src/config/runtime-validation.test.ts | 15 ++++++++++++++- apps/backend/src/config/runtime-validation.ts | 2 +- .../015_allow_permanent_claim_tokens.sql | 4 ++++ apps/backend/src/repositories/claim-token-repo.ts | 4 ++-- .../src/services/claim/claim-service.test.ts | 15 +++++++++++++++ apps/backend/src/services/claim/claim-service.ts | 13 ++++++++++++- .../src/services/fulfillment/executors/types.ts | 2 +- apps/backend/src/types/repository/rows.ts | 2 +- .../src/pages/admin/AdminTaskDetailPage.tsx | 4 +++- 13 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 apps/backend/src/db/migrations/015_allow_permanent_claim_tokens.sql create mode 100644 apps/backend/src/services/claim/claim-service.test.ts diff --git a/.env.server.example b/.env.server.example index b917687b..242463fa 100644 --- a/.env.server.example +++ b/.env.server.example @@ -15,6 +15,7 @@ BACKEND_PORT=3000 # Claim Links CLAIM_BASE_URL=https://order.khhao.com/#/claim +CLAIM_TOKEN_TTL_HOURS=0 # Database POSTGRES_DB=order_site diff --git a/README.md b/README.md index 055e1ef7..ab0bea7a 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,7 @@ cp .env.server.example .env | --- | --- | | `APP_DOMAIN` / `CADDY_SITE_ADDR` | 对外域名与 Caddy 监听地址 | | `CLAIM_BASE_URL` | 领取页完整前缀,如 `https://域名/#/claim` | +| `CLAIM_TOKEN_TTL_HOURS` | 领取链接有效期(小时);`0` 表示长期有效,正整数表示限时 | | `DATABASE_URL` | PostgreSQL 连接串(容器内主机名用 `postgres`) | | `ADMIN_SESSION_SECRET` | 后台登录态签名密钥 | | `ADMIN_DEFAULT_USERS_JSON` | 默认后台用户 | diff --git a/apps/backend/src/config/defaults.ts b/apps/backend/src/config/defaults.ts index d1a05d1c..e4d0cc10 100644 --- a/apps/backend/src/config/defaults.ts +++ b/apps/backend/src/config/defaults.ts @@ -36,7 +36,8 @@ export function createDefaultRuntimeConfig(projectRoot: string): RuntimeConfig { orders: { claimBaseUrl: 'http://127.0.0.1:5173/#/claim', - tokenTtlHours: 24, + // 0 表示长期有效;后台手动关闭任务后领取链接仍会立即失效。 + tokenTtlHours: 0, }, admin: { diff --git a/apps/backend/src/config/env-overrides.test.ts b/apps/backend/src/config/env-overrides.test.ts index 133958d2..f90e4f2e 100644 --- a/apps/backend/src/config/env-overrides.test.ts +++ b/apps/backend/src/config/env-overrides.test.ts @@ -42,6 +42,14 @@ test('applyEnvOverrides derives claim base url from app base url when explicit c assert.equal(config.orders.claimBaseUrl, 'https://example.test/#/claim') }) +test('CLAIM_TOKEN_TTL_HOURS=0 表示领取链接长期有效', () => { + const config = applyEnvOverrides(createRuntimeConfig(), { + CLAIM_TOKEN_TTL_HOURS: '0', + }) + + assert.equal(config.orders.tokenTtlHours, 0) +}) + test('applyEnvOverrides prefers explicit claim base url over app base url', () => { const config = applyEnvOverrides(createRuntimeConfig(), { APP_BASE_URL: 'https://example.test', diff --git a/apps/backend/src/config/runtime-validation.test.ts b/apps/backend/src/config/runtime-validation.test.ts index fda772ab..ebff99b8 100644 --- a/apps/backend/src/config/runtime-validation.test.ts +++ b/apps/backend/src/config/runtime-validation.test.ts @@ -88,7 +88,7 @@ test('validateRuntimeConfig catches invalid numeric and url values', () => { }, orders: { claimBaseUrl: 'not-a-url', - tokenTtlHours: 0, + tokenTtlHours: -1, }, }), { @@ -102,6 +102,19 @@ test('validateRuntimeConfig catches invalid numeric and url values', () => { ) }) +test('validateRuntimeConfig accepts zero claim token TTL for permanent links', () => { + const issues = validateRuntimeConfig( + createRuntimeConfig({ + orders: { + tokenTtlHours: 0, + }, + }), + { env: { NODE_ENV: 'development' } }, + ) + + assert.deepEqual(issues, []) +}) + test('assertRuntimeConfigValid throws a readable validation error', () => { assert.throws( () => { diff --git a/apps/backend/src/config/runtime-validation.ts b/apps/backend/src/config/runtime-validation.ts index 238f90e5..985ad346 100644 --- a/apps/backend/src/config/runtime-validation.ts +++ b/apps/backend/src/config/runtime-validation.ts @@ -49,7 +49,7 @@ export function validateRuntimeConfig( min: 1, max: 100, }) - requireInteger(issues, 'orders.tokenTtlHours', config.orders?.tokenTtlHours, { min: 1 }) + requireInteger(issues, 'orders.tokenTtlHours', config.orders?.tokenTtlHours, { min: 0 }) requireInteger(issues, 'admin.sessionTtlHours', config.admin?.sessionTtlHours, { min: 1 }) requireInteger( issues, diff --git a/apps/backend/src/db/migrations/015_allow_permanent_claim_tokens.sql b/apps/backend/src/db/migrations/015_allow_permanent_claim_tokens.sql new file mode 100644 index 00000000..93312181 --- /dev/null +++ b/apps/backend/src/db/migrations/015_allow_permanent_claim_tokens.sql @@ -0,0 +1,4 @@ +-- 领取链接支持长期有效:expired_at 为 NULL 表示仅能由后台手动关闭。 +-- 不回填已有令牌,历史限时链接仍按其原过期时间执行。 +ALTER TABLE claim_tokens + ALTER COLUMN expired_at DROP NOT NULL; diff --git a/apps/backend/src/repositories/claim-token-repo.ts b/apps/backend/src/repositories/claim-token-repo.ts index 32bfcf78..a209c8d0 100644 --- a/apps/backend/src/repositories/claim-token-repo.ts +++ b/apps/backend/src/repositories/claim-token-repo.ts @@ -6,7 +6,7 @@ type ClaimTokenRow = { task_id: number token: string status: string - expired_at: string + expired_at: string | null used_at: string | null max_use_count: number used_count: number @@ -18,7 +18,7 @@ type ClaimTokenCreateInput = { taskId: number token: string status: string - expiredAt: string + expiredAt: string | null usedAt?: string | null maxUseCount: number usedCount: number diff --git a/apps/backend/src/services/claim/claim-service.test.ts b/apps/backend/src/services/claim/claim-service.test.ts new file mode 100644 index 00000000..f5e18d99 --- /dev/null +++ b/apps/backend/src/services/claim/claim-service.test.ts @@ -0,0 +1,15 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { resolveClaimTokenExpiration } from './claim-service.js' + +test('领取链接默认可配置为长期有效', () => { + assert.equal(resolveClaimTokenExpiration('2026-08-11T08:00:00.000Z', 0), null) +}) + +test('领取链接保留正整数小时的限时配置能力', () => { + assert.equal( + resolveClaimTokenExpiration('2026-08-11T08:00:00.000Z', 24), + '2026-08-12T08:00:00.000Z', + ) +}) diff --git a/apps/backend/src/services/claim/claim-service.ts b/apps/backend/src/services/claim/claim-service.ts index b45951b5..3c7ea908 100644 --- a/apps/backend/src/services/claim/claim-service.ts +++ b/apps/backend/src/services/claim/claim-service.ts @@ -10,7 +10,7 @@ type TaskClaimToken = NonNullable>> export async function createTaskClaimToken(taskId: number | string): Promise { const createdAt = nowIso() - const expiredAt = addHours(createdAt, Number(runtimeConfig.orders.tokenTtlHours || 24)) + const expiredAt = resolveClaimTokenExpiration(createdAt, runtimeConfig.orders.tokenTtlHours) const token = await createClaimToken({ taskId: Number(taskId), token: randomToken(24), @@ -36,6 +36,17 @@ export async function createTaskClaimToken(taskId: number | string): Promise 0 + ? addHours(createdAt, ttlHours) + : null +} + export function buildClaimUrl(token: string): string { const baseUrl = String(runtimeConfig.orders.claimBaseUrl || '').trim() return baseUrl ? `${baseUrl.replace(/\/+$/, '')}/${token}` : token diff --git a/apps/backend/src/services/fulfillment/executors/types.ts b/apps/backend/src/services/fulfillment/executors/types.ts index d37e0982..d5b6f8d5 100644 --- a/apps/backend/src/services/fulfillment/executors/types.ts +++ b/apps/backend/src/services/fulfillment/executors/types.ts @@ -21,7 +21,7 @@ export type FulfillmentPrepareDeps = { updateTask: (taskId: number | string, patch: TaskUpdatePatch) => Promise createTaskClaimToken: (taskId: number | string) => Promise<{ token: string - expired_at: string + expired_at: string | null [key: string]: unknown }> notifyTaskAutoManualReview: (payload: { diff --git a/apps/backend/src/types/repository/rows.ts b/apps/backend/src/types/repository/rows.ts index 45921655..592df153 100644 --- a/apps/backend/src/types/repository/rows.ts +++ b/apps/backend/src/types/repository/rows.ts @@ -92,7 +92,7 @@ export type ClaimTokenRow = { id: number token: string status: string - expired_at: string + expired_at: string | null created_at?: string updated_at?: string } diff --git a/apps/frontend/src/pages/admin/AdminTaskDetailPage.tsx b/apps/frontend/src/pages/admin/AdminTaskDetailPage.tsx index 345c58a8..8caef346 100644 --- a/apps/frontend/src/pages/admin/AdminTaskDetailPage.tsx +++ b/apps/frontend/src/pages/admin/AdminTaskDetailPage.tsx @@ -391,7 +391,9 @@ export default function AdminTaskDetailPage() { 外部交付链接 ) : ( - 到期:{formatAdminDateTime(resolvedDetail.claimToken.expiredAt)} + {resolvedDetail.claimToken.expiredAt + ? `到期:${formatAdminDateTime(resolvedDetail.claimToken.expiredAt)}` + : '长期有效'} )}