支持领取链接长期有效

This commit is contained in:
yml2213
2026-08-11 14:06:29 +08:00
parent e72c936986
commit e097ddad3d
13 changed files with 65 additions and 9 deletions
+2 -1
View File
@@ -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: {
@@ -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',
@@ -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(
() => {
@@ -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,
@@ -0,0 +1,4 @@
-- 领取链接支持长期有效:expired_at 为 NULL 表示仅能由后台手动关闭。
-- 不回填已有令牌,历史限时链接仍按其原过期时间执行。
ALTER TABLE claim_tokens
ALTER COLUMN expired_at DROP NOT NULL;
@@ -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
@@ -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',
)
})
@@ -10,7 +10,7 @@ type TaskClaimToken = NonNullable<Awaited<ReturnType<typeof createClaimToken>>>
export async function createTaskClaimToken(taskId: number | string): Promise<TaskClaimToken> {
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<Tas
}
}
/** 0 表示长期有效;正整数保留为可选的限时领取策略。 */
export function resolveClaimTokenExpiration(
createdAt: string,
tokenTtlHours: unknown,
): string | null {
const ttlHours = Number(tokenTtlHours)
return Number.isFinite(ttlHours) && ttlHours > 0
? addHours(createdAt, ttlHours)
: null
}
export function buildClaimUrl(token: string): string {
const baseUrl = String(runtimeConfig.orders.claimBaseUrl || '').trim()
return baseUrl ? `${baseUrl.replace(/\/+$/, '')}/${token}` : token
@@ -21,7 +21,7 @@ export type FulfillmentPrepareDeps = {
updateTask: (taskId: number | string, patch: TaskUpdatePatch) => Promise<TaskRow | null>
createTaskClaimToken: (taskId: number | string) => Promise<{
token: string
expired_at: string
expired_at: string | null
[key: string]: unknown
}>
notifyTaskAutoManualReview: (payload: {
+1 -1
View File
@@ -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
}