refactor(collect): 移除旧补资料链接 token 体系
- 删除管理端生成补资料链接接口、链接仓储读写与 TTL 配置服务 - 删除补资料链接配置面板及前端相关服务、类型 - 新增迁移 113:删除 work_order_material_links 表与 work_order_material_link 配置 - 「复制补资料链接」改为本地复制 /supplement/<平台订单号>,不再请求后端
This commit is contained in:
@@ -9,8 +9,6 @@ export const APP_CONFIG_KEYS = {
|
||||
workerHall: 'worker_hall',
|
||||
workerRiskControl: 'worker_risk_control',
|
||||
workerMockShops: 'worker_mock_shops',
|
||||
/** 接单工单:一次性补资料链接配置(有效期分钟数) */
|
||||
workOrderMaterialLink: 'work_order_material_link',
|
||||
kuaishouIndustrySource: 'kuaishou_industry_source',
|
||||
affiliateDash: 'affiliate_dash',
|
||||
pddSource: 'pdd_source',
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
-- 113_drop_work_order_material_links.sql —— 下线旧的「一次性补资料链接 token」体系
|
||||
--
|
||||
-- 背景:补资料页已改为按平台订单号直连(/supplement/:订单号),
|
||||
-- 不再需要客服先生成 token 链接:
|
||||
-- - 后端:管理端 /orders/:id/material-link、链接仓储读写、material-link-config 配置接口
|
||||
-- - 前端:补资料链接有效期设置面板、生成链接请求
|
||||
-- - 数据:work_order_material_links 表、app_config_entries.work_order_material_link
|
||||
-- 历史 token 记录已无任何读取入口,直接随表删除。
|
||||
|
||||
DROP TABLE IF EXISTS work_order_material_links;
|
||||
|
||||
DELETE FROM app_config_entries WHERE config_key = 'work_order_material_link';
|
||||
@@ -2,20 +2,6 @@ import type { PoolClient } from 'pg'
|
||||
|
||||
import type { WorkOrderRow } from './types.js'
|
||||
|
||||
export type WorkOrderMaterialLinkStatus = 'active' | 'used' | 'expired' | 'revoked'
|
||||
|
||||
export type WorkOrderMaterialLinkRow = {
|
||||
id: number
|
||||
work_order_id: number
|
||||
token: string
|
||||
status: WorkOrderMaterialLinkStatus
|
||||
expires_at: string
|
||||
used_at: string | null
|
||||
created_by: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 锁工单并回读「有效拼单份额」。
|
||||
*
|
||||
@@ -41,63 +27,6 @@ export async function getWorkOrderForUpdateWithClient(
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
export async function findActiveWorkOrderMaterialLinkWithClient(
|
||||
client: PoolClient,
|
||||
workOrderId: number | string,
|
||||
now: string,
|
||||
): Promise<WorkOrderMaterialLinkRow | null> {
|
||||
const result = await client.query<WorkOrderMaterialLinkRow>(
|
||||
`SELECT * FROM work_order_material_links
|
||||
WHERE work_order_id = $1 AND status = 'active' AND expires_at > $2
|
||||
ORDER BY id DESC
|
||||
LIMIT 1`,
|
||||
[Number(workOrderId), now],
|
||||
)
|
||||
return result.rows[0] || null
|
||||
}
|
||||
|
||||
/** 重新生成前清掉旧 active:已过期标 expired,未过期标 revoked。 */
|
||||
export async function revokeActiveWorkOrderMaterialLinksWithClient(
|
||||
client: PoolClient,
|
||||
workOrderId: number | string,
|
||||
now: string,
|
||||
): Promise<number> {
|
||||
const result = await client.query(
|
||||
`UPDATE work_order_material_links
|
||||
SET status = CASE WHEN expires_at <= $2 THEN 'expired' ELSE 'revoked' END,
|
||||
updated_at = $2
|
||||
WHERE work_order_id = $1 AND status = 'active'`,
|
||||
[Number(workOrderId), now],
|
||||
)
|
||||
return result.rowCount || 0
|
||||
}
|
||||
|
||||
export async function createWorkOrderMaterialLinkWithClient(
|
||||
client: PoolClient,
|
||||
input: {
|
||||
workOrderId: number | string
|
||||
token: string
|
||||
expiresAt: string
|
||||
createdBy: string
|
||||
now: string
|
||||
},
|
||||
): Promise<WorkOrderMaterialLinkRow> {
|
||||
const result = await client.query<WorkOrderMaterialLinkRow>(
|
||||
`INSERT INTO work_order_material_links (
|
||||
work_order_id, token, status, expires_at, created_by, created_at, updated_at
|
||||
) VALUES ($1, $2, 'active', $3, $4, $5, $5)
|
||||
RETURNING *`,
|
||||
[
|
||||
Number(input.workOrderId),
|
||||
input.token,
|
||||
input.expiresAt,
|
||||
String(input.createdBy || '').trim(),
|
||||
input.now,
|
||||
],
|
||||
)
|
||||
return result.rows[0]!
|
||||
}
|
||||
|
||||
export async function updateWorkOrderMaterialWithClient(
|
||||
client: PoolClient,
|
||||
input: {
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
getAdminWorkerPlatformNotificationConfig,
|
||||
getAdminWorkerPlatformSummary,
|
||||
getAdminWorkerProductMatchConfig,
|
||||
getWorkerMaterialLinkConfig,
|
||||
listAdminKuaishouMatchSources,
|
||||
listAdminWorkCategories,
|
||||
listAdminWorkProductMatchLogs,
|
||||
@@ -30,7 +29,6 @@ import {
|
||||
saveAdminWorkerLevel,
|
||||
saveAdminWorkerPlatformNotificationConfig,
|
||||
saveAdminWorkerProductMatchConfig,
|
||||
saveWorkerMaterialLinkConfig,
|
||||
syncAdminWorkProductRulePrice,
|
||||
testAdminKuaishouProductMatch,
|
||||
testAdminProductMatchByOrderItem,
|
||||
@@ -295,32 +293,6 @@ router.post(
|
||||
}),
|
||||
)
|
||||
|
||||
router.get(
|
||||
'/worker-platform/material-link-config',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
createJsonHandler(() => getWorkerMaterialLinkConfig(), {
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取补资料链接配置失败',
|
||||
scope: '[admin/worker-platform/material-link-config]',
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/worker-platform/material-link-config',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
createJsonHandler((req) => saveWorkerMaterialLinkConfig(req.body || {}), {
|
||||
successMessage: '补资料链接配置已保存',
|
||||
errorMessage: '保存补资料链接配置失败',
|
||||
scope: '[admin/worker-platform/material-link-config]',
|
||||
audit: (_req, data) => ({
|
||||
action: 'worker_material_link_config_saved',
|
||||
targetType: 'work_order_material_link_config',
|
||||
targetId: 'work_order_material_link',
|
||||
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/worker-platform/announcement',
|
||||
requireAdminRoles(['admin']),
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
completeAdminWorkOrderGiftCooldown,
|
||||
confirmAdminWorkOrderFriendAdded,
|
||||
createAdminMockWorkOrder,
|
||||
createAdminWorkOrderMaterialLink,
|
||||
deleteAdminMockCustomShop,
|
||||
deleteAdminWorkOrder,
|
||||
getAdminWorkOrderEvents,
|
||||
@@ -187,36 +186,6 @@ router.post(
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/material-link',
|
||||
requireAdminPermission('worker_order.update_material'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
createAdminWorkOrderMaterialLink(
|
||||
String(req.params.workOrderId || ''),
|
||||
req.adminSession?.displayName || req.adminSession?.username || '',
|
||||
),
|
||||
{
|
||||
successMessage: '补资料链接已生成',
|
||||
errorMessage: '生成补资料链接失败',
|
||||
scope: '[admin/worker-platform/orders/:workOrderId/material-link]',
|
||||
audit: (req, data) => ({
|
||||
action: 'work_order_material_link_created',
|
||||
targetType: 'work_order',
|
||||
targetId: String(req.params.workOrderId || ''),
|
||||
// 审计只记录有效期与是否复用,绝不落 token。
|
||||
data:
|
||||
data && typeof data === 'object'
|
||||
? {
|
||||
expiresAt: (data as Record<string, unknown>).expiresAt,
|
||||
reused: (data as Record<string, unknown>).reused,
|
||||
}
|
||||
: {},
|
||||
}),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/reset-material',
|
||||
requireAdminPermission('worker_order.update_material'),
|
||||
|
||||
@@ -65,4 +65,3 @@ export * from './worker-acceptance-config-service.js'
|
||||
export * from './sync-worker-orders-service.js'
|
||||
export * from './consignment-contact-service.js'
|
||||
export * from './work-order-material-link-service.js'
|
||||
export * from './worker-material-link-config-service.js'
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
pickSubmittedMaterialFields,
|
||||
resolveMaterialResetBlockReason,
|
||||
resolvePublicMaterialOrderState,
|
||||
resolveWorkOrderMaterialLinkOrderBlockReason,
|
||||
} from './work-order-material-link-service.js'
|
||||
|
||||
test('公开补资料:只有待完善且未指派/无份额可提交', () => {
|
||||
@@ -129,50 +128,7 @@ test('客服重置补资料:只允许待分配、未指派、无份额、未
|
||||
)
|
||||
})
|
||||
|
||||
test('管理端链接准入:待完善/待分配可用,已指派/有份额/其他状态拒绝', () => {
|
||||
assert.equal(
|
||||
resolveWorkOrderMaterialLinkOrderBlockReason({
|
||||
status: 'pending_material',
|
||||
assigned_worker_id: null,
|
||||
active_share_quantity: 0,
|
||||
}),
|
||||
'',
|
||||
)
|
||||
assert.equal(
|
||||
resolveWorkOrderMaterialLinkOrderBlockReason({
|
||||
status: 'unassigned',
|
||||
assigned_worker_id: null,
|
||||
active_share_quantity: 0,
|
||||
}),
|
||||
'',
|
||||
)
|
||||
assert.equal(
|
||||
resolveWorkOrderMaterialLinkOrderBlockReason({
|
||||
status: 'open',
|
||||
assigned_worker_id: null,
|
||||
active_share_quantity: 0,
|
||||
}),
|
||||
'status',
|
||||
)
|
||||
assert.equal(
|
||||
resolveWorkOrderMaterialLinkOrderBlockReason({
|
||||
status: 'pending_material',
|
||||
assigned_worker_id: 9,
|
||||
active_share_quantity: 0,
|
||||
}),
|
||||
'assigned',
|
||||
)
|
||||
assert.equal(
|
||||
resolveWorkOrderMaterialLinkOrderBlockReason({
|
||||
status: 'pending_material',
|
||||
assigned_worker_id: null,
|
||||
active_share_quantity: 2,
|
||||
}),
|
||||
'shares',
|
||||
)
|
||||
})
|
||||
|
||||
test('补资料链接:提交字段只保留 requirements 白名单,额外字段被丢弃', () => {
|
||||
test('公开补资料:提交字段只保留 requirements 白名单,额外字段被丢弃', () => {
|
||||
const result = pickSubmittedMaterialFields([{ key: 'gameId' }, { key: 'gameNickname' }], {
|
||||
gameId: '123',
|
||||
gameNickname: '张三',
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { randomBytes } from 'node:crypto'
|
||||
|
||||
import { withTransaction } from '../../db/client.js'
|
||||
import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
|
||||
import {
|
||||
createWorkOrderEventWithClient,
|
||||
createWorkOrderMaterialLinkWithClient,
|
||||
findActiveWorkOrderMaterialLinkWithClient,
|
||||
getWorkOrderForUpdateWithClient,
|
||||
listWorkOrdersByPlatformOrderId,
|
||||
revokeActiveWorkOrderMaterialLinksWithClient,
|
||||
updateWorkOrderMaterialWithClient,
|
||||
} from '../../repositories/worker-platform/index.js'
|
||||
import type { WorkOrderRow } from '../../repositories/worker-platform/types.js'
|
||||
@@ -21,20 +16,13 @@ import { resolveAdminNotificationEntity } from '../admin/admin-notification-serv
|
||||
import { publishWorkOrderRealtimeChange } from '../realtime/realtime-event-service.js'
|
||||
import {
|
||||
mapCollectLookupOrder,
|
||||
mapWorkOrderAdmin,
|
||||
normalizeOptionalId,
|
||||
normalizeSubmittedFields,
|
||||
normalizeUploadedFiles,
|
||||
resolveRequirementFields,
|
||||
} from './mappers.js'
|
||||
import { getWorkerMaterialLinkConfig } from './worker-material-link-config-service.js'
|
||||
import { getRequiredWorkOrder } from './worker-session-context-service.js'
|
||||
|
||||
const ALLOWED_MATERIAL_LINK_ORDER_STATUSES = new Set<string>([
|
||||
WORK_ORDER_STATUS.PENDING_MATERIAL,
|
||||
WORK_ORDER_STATUS.UNASSIGNED,
|
||||
])
|
||||
|
||||
const MAX_MATERIAL_SCREENSHOTS = 5
|
||||
|
||||
const PUBLIC_MATERIAL_STATUS_LABELS: Record<string, string> = {
|
||||
@@ -49,8 +37,6 @@ const PUBLIC_MATERIAL_STATUS_LABELS: Record<string, string> = {
|
||||
vip_evidence_pending: '待补图',
|
||||
}
|
||||
|
||||
export type WorkOrderMaterialLinkOrderBlockReason = 'status' | 'assigned' | 'shares' | ''
|
||||
|
||||
export type PublicMaterialOrderBlockReason =
|
||||
| 'already_submitted'
|
||||
| 'status'
|
||||
@@ -154,21 +140,6 @@ export function resolveMaterialResetBlockMessage(reason: MaterialResetBlockReaso
|
||||
return '只有待分配(未指派、无份额、未发布)的工单可以重置补资料状态'
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理端生成/复用一次性链接的准入判断。
|
||||
*
|
||||
* 用户要求保留现有“复制补资料链接”按钮,所以这里仍按原口径允许
|
||||
* 待完善 / 待分配,并拒绝已指派/有份额的工单。
|
||||
*/
|
||||
export function resolveWorkOrderMaterialLinkOrderBlockReason(
|
||||
order: Pick<WorkOrderRow, 'status' | 'assigned_worker_id' | 'active_share_quantity'>,
|
||||
): WorkOrderMaterialLinkOrderBlockReason {
|
||||
if (!ALLOWED_MATERIAL_LINK_ORDER_STATUSES.has(order.status)) return 'status'
|
||||
if (order.assigned_worker_id) return 'assigned'
|
||||
if (Number(order.active_share_quantity || 0) > 0) return 'shares'
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 只保留工单 requirement 里定义过的字段。
|
||||
*
|
||||
@@ -196,29 +167,6 @@ function throwPublicMaterialBlocked(state: PublicMaterialOrderState): never {
|
||||
})
|
||||
}
|
||||
|
||||
/** 管理端生成补资料链接时仍沿用旧的准入文案。 */
|
||||
function assertWorkOrderAllowedForMaterialLink(order: WorkOrderRow): void {
|
||||
const reason = resolveWorkOrderMaterialLinkOrderBlockReason(order)
|
||||
if (reason === 'status') {
|
||||
throw createHttpError('当前状态不能生成补资料链接', {
|
||||
statusCode: 409,
|
||||
errorCode: 'material_link_status_invalid',
|
||||
})
|
||||
}
|
||||
if (reason === 'assigned') {
|
||||
throw createHttpError('订单已指派打手,不能生成补资料链接', {
|
||||
statusCode: 409,
|
||||
errorCode: 'material_link_status_invalid',
|
||||
})
|
||||
}
|
||||
if (reason === 'shares') {
|
||||
throw createHttpError('订单已有拼单份额,不能生成补资料链接', {
|
||||
statusCode: 409,
|
||||
errorCode: 'material_link_status_invalid',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function mapPublicOrder(order: WorkOrderRow) {
|
||||
return {
|
||||
workOrderNo: order.work_order_no,
|
||||
@@ -228,12 +176,6 @@ function mapPublicOrder(order: WorkOrderRow) {
|
||||
}
|
||||
}
|
||||
|
||||
/** PostgreSQL 返回的 timestamptz 可能是 "YYYY-MM-DD HH:mm:ss" 字符串,统一转 ISO。 */
|
||||
function toIsoTimestamp(value: string): string {
|
||||
const date = new Date(value)
|
||||
return Number.isNaN(date.getTime()) ? String(value || '') : date.toISOString()
|
||||
}
|
||||
|
||||
function mapPublicMaterialOrder(order: WorkOrderRow) {
|
||||
const state = resolvePublicMaterialOrderState(order)
|
||||
const base = {
|
||||
@@ -290,76 +232,6 @@ async function resolvePublicMaterialTarget(
|
||||
return orders[0]!
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理端生成/复用一次性链接。
|
||||
*
|
||||
* 用户要求保留现有“复制补资料链接”按钮,因此链接记录仍会生成;
|
||||
* 但公开页已经不再依赖它,复制出的 `/supplement/<平台订单号>` 始终可用。
|
||||
*/
|
||||
export async function createAdminWorkOrderMaterialLink(
|
||||
workOrderId: number | string,
|
||||
actorName = '',
|
||||
) {
|
||||
const { ttlMinutes } = getWorkerMaterialLinkConfig()
|
||||
|
||||
return withTransaction(async (client) => {
|
||||
const order = await getWorkOrderForUpdateWithClient(client, workOrderId)
|
||||
if (!order) {
|
||||
throw createHttpError('接单订单不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'work_order_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
if (!mapWorkOrderAdmin(order).capabilities.canUpdateMaterial) {
|
||||
throw createHttpError('当前状态不能生成补资料链接', {
|
||||
statusCode: 409,
|
||||
errorCode: 'material_link_status_invalid',
|
||||
})
|
||||
}
|
||||
|
||||
const platformOrderId = String(order.platform_order_id || '').trim()
|
||||
if (!platformOrderId) {
|
||||
throw createHttpError('该工单没有平台订单号,不能生成补资料链接', {
|
||||
statusCode: 409,
|
||||
errorCode: 'material_link_platform_order_missing',
|
||||
})
|
||||
}
|
||||
|
||||
assertWorkOrderAllowedForMaterialLink(order)
|
||||
|
||||
// 锁到手后再取时间,避免等锁期间旧链接过期却仍按旧时间复用。
|
||||
const now = nowIso()
|
||||
const existing = await findActiveWorkOrderMaterialLinkWithClient(client, order.id, now)
|
||||
if (existing) {
|
||||
return {
|
||||
token: existing.token,
|
||||
platformOrderId,
|
||||
expiresAt: toIsoTimestamp(existing.expires_at),
|
||||
reused: true,
|
||||
}
|
||||
}
|
||||
|
||||
await revokeActiveWorkOrderMaterialLinksWithClient(client, order.id, now)
|
||||
const token = randomBytes(24).toString('base64url')
|
||||
const expiresAt = new Date(Date.now() + ttlMinutes * 60 * 1000).toISOString()
|
||||
const link = await createWorkOrderMaterialLinkWithClient(client, {
|
||||
workOrderId: order.id,
|
||||
token,
|
||||
expiresAt,
|
||||
createdBy: String(actorName || '').trim(),
|
||||
now,
|
||||
})
|
||||
|
||||
return {
|
||||
token: link.token,
|
||||
platformOrderId,
|
||||
expiresAt: toIsoTimestamp(link.expires_at),
|
||||
reused: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 公开页按平台订单号读取补资料内容。
|
||||
*
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import {
|
||||
createDefaultWorkerMaterialLinkConfig,
|
||||
normalizeWorkerMaterialLinkConfig,
|
||||
WORK_ORDER_MATERIAL_LINK_DEFAULT_TTL_MINUTES,
|
||||
WORK_ORDER_MATERIAL_LINK_MAX_TTL_MINUTES,
|
||||
WORK_ORDER_MATERIAL_LINK_MIN_TTL_MINUTES,
|
||||
} from './worker-material-link-config-service.js'
|
||||
|
||||
test('补资料链接配置:默认 30 分钟', () => {
|
||||
assert.deepEqual(createDefaultWorkerMaterialLinkConfig(), {
|
||||
ttlMinutes: WORK_ORDER_MATERIAL_LINK_DEFAULT_TTL_MINUTES,
|
||||
})
|
||||
assert.equal(WORK_ORDER_MATERIAL_LINK_DEFAULT_TTL_MINUTES, 30)
|
||||
assert.deepEqual(normalizeWorkerMaterialLinkConfig(undefined), { ttlMinutes: 30 })
|
||||
})
|
||||
|
||||
test('补资料链接配置:接受 ttlMinutes / ttl_minutes / 数字字符串', () => {
|
||||
assert.deepEqual(normalizeWorkerMaterialLinkConfig({ ttlMinutes: 45 }), { ttlMinutes: 45 })
|
||||
assert.deepEqual(normalizeWorkerMaterialLinkConfig({ ttl_minutes: '10' }), { ttlMinutes: 10 })
|
||||
assert.deepEqual(normalizeWorkerMaterialLinkConfig({ ttlMinutes: 90.4 }), { ttlMinutes: 90 })
|
||||
})
|
||||
|
||||
test('补资料链接配置:越界值收敛到 1 - 10080 分钟', () => {
|
||||
assert.deepEqual(normalizeWorkerMaterialLinkConfig({ ttlMinutes: 0 }), {
|
||||
ttlMinutes: WORK_ORDER_MATERIAL_LINK_MIN_TTL_MINUTES,
|
||||
})
|
||||
assert.deepEqual(normalizeWorkerMaterialLinkConfig({ ttlMinutes: -5 }), { ttlMinutes: 1 })
|
||||
assert.deepEqual(normalizeWorkerMaterialLinkConfig({ ttlMinutes: 999999 }), {
|
||||
ttlMinutes: WORK_ORDER_MATERIAL_LINK_MAX_TTL_MINUTES,
|
||||
})
|
||||
assert.equal(WORK_ORDER_MATERIAL_LINK_MAX_TTL_MINUTES, 10080)
|
||||
})
|
||||
|
||||
test('补资料链接配置:非法输入回退默认值,不会写入 NaN', () => {
|
||||
assert.deepEqual(normalizeWorkerMaterialLinkConfig({ ttlMinutes: 'abc' }), { ttlMinutes: 30 })
|
||||
assert.deepEqual(normalizeWorkerMaterialLinkConfig({ ttlMinutes: null }), { ttlMinutes: 30 })
|
||||
assert.deepEqual(normalizeWorkerMaterialLinkConfig('not-an-object'), { ttlMinutes: 30 })
|
||||
})
|
||||
@@ -1,62 +0,0 @@
|
||||
import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import { readAppConfigEntry, saveAppConfigEntry } from '../config/app-config-store.js'
|
||||
|
||||
/** 补资料链接默认有效期(分钟):30 分钟。 */
|
||||
export const WORK_ORDER_MATERIAL_LINK_DEFAULT_TTL_MINUTES = 30
|
||||
/** 允许配置的最短有效期(分钟)。 */
|
||||
export const WORK_ORDER_MATERIAL_LINK_MIN_TTL_MINUTES = 1
|
||||
/** 允许配置的最长有效期(分钟):7 天。 */
|
||||
export const WORK_ORDER_MATERIAL_LINK_MAX_TTL_MINUTES = 7 * 24 * 60
|
||||
|
||||
export type WorkerMaterialLinkConfig = {
|
||||
/** 新生成的补资料链接有效期,单位分钟。 */
|
||||
ttlMinutes: number
|
||||
}
|
||||
|
||||
/** 读取配置(内存缓存),未配置时返回默认 30 分钟。 */
|
||||
export function getWorkerMaterialLinkConfig(): WorkerMaterialLinkConfig {
|
||||
return readAppConfigEntry({
|
||||
configKey: APP_CONFIG_KEYS.workOrderMaterialLink,
|
||||
fallback: createDefaultWorkerMaterialLinkConfig,
|
||||
normalize: normalizeWorkerMaterialLinkConfig,
|
||||
})
|
||||
}
|
||||
|
||||
export async function saveWorkerMaterialLinkConfig(
|
||||
rawValue: unknown,
|
||||
): Promise<WorkerMaterialLinkConfig> {
|
||||
return saveAppConfigEntry({
|
||||
configKey: APP_CONFIG_KEYS.workOrderMaterialLink,
|
||||
value: rawValue,
|
||||
normalize: normalizeWorkerMaterialLinkConfig,
|
||||
})
|
||||
}
|
||||
|
||||
export function createDefaultWorkerMaterialLinkConfig(): WorkerMaterialLinkConfig {
|
||||
return { ttlMinutes: WORK_ORDER_MATERIAL_LINK_DEFAULT_TTL_MINUTES }
|
||||
}
|
||||
|
||||
export function normalizeWorkerMaterialLinkConfig(rawValue: unknown): WorkerMaterialLinkConfig {
|
||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
||||
const raw = source.ttlMinutes ?? source.ttl_minutes
|
||||
if (raw === undefined || raw === null || raw === '') {
|
||||
return createDefaultWorkerMaterialLinkConfig()
|
||||
}
|
||||
|
||||
const value = Number(raw)
|
||||
if (!Number.isFinite(value)) {
|
||||
return createDefaultWorkerMaterialLinkConfig()
|
||||
}
|
||||
|
||||
return {
|
||||
ttlMinutes: Math.min(
|
||||
WORK_ORDER_MATERIAL_LINK_MAX_TTL_MINUTES,
|
||||
Math.max(WORK_ORDER_MATERIAL_LINK_MIN_TTL_MINUTES, Math.round(value)),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is JsonObject {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
Reference in New Issue
Block a user