From e68079216123198352434e0feff282f88b6c6815 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 28 Aug 2026 20:17:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=8E=A5=E5=8D=95):=20=E5=A5=BD=E5=8F=8B?= =?UTF-8?q?=E8=B5=A0=E9=80=81=E5=86=B7=E5=8D=B4=E6=94=AF=E6=8C=81=E5=88=86?= =?UTF-8?q?=E9=92=9F=E5=8D=95=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../064_work_order_gift_cooldown_minutes.sql | 14 ++++ .../domain/work-order-acceptance-mode.test.ts | 9 +++ .../src/domain/work-order-acceptance-mode.ts | 14 +++- .../src/repositories/worker-platform/types.ts | 3 + .../worker-platform/work-order-gift-repo.ts | 11 +-- .../work-order-management-repo.ts | 8 ++- .../worker-platform/work-product-rule-repo.ts | 12 ++-- .../admin-mock-work-order-service.ts | 2 + .../admin-worker-catalog-service.ts | 8 +++ .../sync-worker-orders-service.ts | 2 + .../work-order-display-mappers.ts | 2 + .../work-product-match-mappers.ts | 2 + .../pages/admin/panels/ProductRulesPanel.tsx | 72 ++++++++++++++----- .../pages/admin/panels/WorkOrdersPanel.tsx | 40 ++++++----- .../pages/worker/WorkerOrderDetailDrawer.tsx | 6 +- .../src/services/admin/worker-platform.ts | 1 + apps/frontend/src/types/worker-platform.ts | 3 + 17 files changed, 159 insertions(+), 50 deletions(-) create mode 100644 apps/backend/src/db/migrations/064_work_order_gift_cooldown_minutes.sql diff --git a/apps/backend/src/db/migrations/064_work_order_gift_cooldown_minutes.sql b/apps/backend/src/db/migrations/064_work_order_gift_cooldown_minutes.sql new file mode 100644 index 00000000..680c6da2 --- /dev/null +++ b/apps/backend/src/db/migrations/064_work_order_gift_cooldown_minutes.sql @@ -0,0 +1,14 @@ +-- 064_work_order_gift_cooldown_minutes.sql —— 好友赠送冷却支持按分钟配置。 +-- 小时与分钟两种单位互斥;保留原小时字段,历史模板与工单自动沿用小时配置。 + +ALTER TABLE work_product_rules + ADD COLUMN IF NOT EXISTS gift_cooldown_minutes INTEGER NOT NULL DEFAULT 0; + +ALTER TABLE work_orders + ADD COLUMN IF NOT EXISTS gift_cooldown_minutes INTEGER NOT NULL DEFAULT 0; + +COMMENT ON COLUMN work_product_rules.gift_cooldown_minutes + IS '好友赠送按分钟配置时的总分钟数(1-43200);非零时小时字段为 0,建单时拷贝到工单'; + +COMMENT ON COLUMN work_orders.gift_cooldown_minutes + IS '好友赠送按分钟配置时的总分钟数(1-43200);非零时小时字段为 0,从模板拷贝'; diff --git a/apps/backend/src/domain/work-order-acceptance-mode.test.ts b/apps/backend/src/domain/work-order-acceptance-mode.test.ts index 10898609..ac1ec8c1 100644 --- a/apps/backend/src/domain/work-order-acceptance-mode.test.ts +++ b/apps/backend/src/domain/work-order-acceptance-mode.test.ts @@ -4,6 +4,8 @@ import assert from 'node:assert/strict' import { isWorkOrderGiftDepositForfeitable, normalizeWorkOrderAcceptanceMode, + normalizeWorkOrderGiftCooldownHours, + normalizeWorkOrderGiftCooldownMinutes, resolveWorkOrderGiftPhase, WORK_ORDER_ACCEPTANCE_MODE, WORK_ORDER_GIFT_PHASE, @@ -58,3 +60,10 @@ test('赠送阶段按冷却到点实时推导 gift_ready', () => { assert.equal(normalizeWorkOrderAcceptanceMode('friend_gift'), 'friend_gift') assert.equal(normalizeWorkOrderAcceptanceMode(''), 'standard') }) + +test('好友赠送按分钟冷却支持最长三十天并忽略负数', () => { + assert.equal(normalizeWorkOrderGiftCooldownHours(0), 0) + assert.equal(normalizeWorkOrderGiftCooldownMinutes(15), 15) + assert.equal(normalizeWorkOrderGiftCooldownMinutes(43_201), 43_200) + assert.equal(normalizeWorkOrderGiftCooldownMinutes(-1), 0) +}) diff --git a/apps/backend/src/domain/work-order-acceptance-mode.ts b/apps/backend/src/domain/work-order-acceptance-mode.ts index bdfadd0c..ffa76830 100644 --- a/apps/backend/src/domain/work-order-acceptance-mode.ts +++ b/apps/backend/src/domain/work-order-acceptance-mode.ts @@ -9,6 +9,7 @@ export type WorkOrderAcceptanceMode = export const DEFAULT_GIFT_COOLDOWN_HOURS = 72 export const MAX_GIFT_COOLDOWN_HOURS = 24 * 30 +export const MAX_GIFT_COOLDOWN_MINUTES = MAX_GIFT_COOLDOWN_HOURS * 60 /** 好友赠送模式的落库阶段;gift_ready 由 gift_available_at 到点后实时计算,不落库。 */ export const WORK_ORDER_GIFT_PHASE = { @@ -33,11 +34,18 @@ export function isFriendGiftAcceptanceMode(value: unknown): boolean { return normalizeWorkOrderAcceptanceMode(value) === WORK_ORDER_ACCEPTANCE_MODE.FRIEND_GIFT } -/** 冷却小时数收敛到 [0, 30 天],默认 3 天。 */ +/** 冷却小时数收敛到 [0, 30 天],默认 3 天。0 用于选择按分钟配置。 */ export function normalizeWorkOrderGiftCooldownHours(value: unknown): number { const hours = Number(value) - if (!Number.isFinite(hours) || hours <= 0) return DEFAULT_GIFT_COOLDOWN_HOURS - return Math.min(MAX_GIFT_COOLDOWN_HOURS, Math.max(1, Math.round(hours))) + if (!Number.isFinite(hours) || hours < 0) return DEFAULT_GIFT_COOLDOWN_HOURS + return Math.min(MAX_GIFT_COOLDOWN_HOURS, Math.round(hours)) +} + +/** 按分钟配置时的总分钟数,最大 30 天。 */ +export function normalizeWorkOrderGiftCooldownMinutes(value: unknown): number { + const minutes = Number(value) + if (!Number.isFinite(minutes)) return 0 + return Math.min(MAX_GIFT_COOLDOWN_MINUTES, Math.max(0, Math.round(minutes))) } export type WorkOrderGiftSource = { diff --git a/apps/backend/src/repositories/worker-platform/types.ts b/apps/backend/src/repositories/worker-platform/types.ts index 78485681..2da809e3 100644 --- a/apps/backend/src/repositories/worker-platform/types.ts +++ b/apps/backend/src/repositories/worker-platform/types.ts @@ -270,6 +270,7 @@ export type WorkProductRuleRow = { timeout_policy: string acceptance_mode: string gift_cooldown_hours: number + gift_cooldown_minutes: number requirement_json: string | Record match_json: string | Record sort_order: number @@ -378,6 +379,7 @@ export type WorkOrderRow = { friend_added_at: string | null gift_available_at: string | null gift_cooldown_hours: number + gift_cooldown_minutes: number created_at: string updated_at: string /** 拼单聚合字段,由工单查询统一计算。 */ @@ -546,6 +548,7 @@ export type CreateWorkOrderInput = { timeoutPolicy?: string acceptanceMode?: string giftCooldownHours?: number + giftCooldownMinutes?: number materialJson: string requirementJson: string now: string diff --git a/apps/backend/src/repositories/worker-platform/work-order-gift-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-gift-repo.ts index 1da8367c..1a777242 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-gift-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-gift-repo.ts @@ -5,6 +5,7 @@ import type { WorkOrderRow } from './types.js' import { normalizeWorkOrderAcceptanceMode, normalizeWorkOrderGiftCooldownHours, + normalizeWorkOrderGiftCooldownMinutes, WORK_ORDER_ACCEPTANCE_MODE, WORK_ORDER_GIFT_PHASE, } from '../../domain/work-order-acceptance-mode.js' @@ -121,6 +122,7 @@ export async function confirmWorkOrderFriendAdded(input: { } const cooldownHours = normalizeWorkOrderGiftCooldownHours(workOrder.gift_cooldown_hours) + const cooldownMinutes = normalizeWorkOrderGiftCooldownMinutes(workOrder.gift_cooldown_minutes) // 参数必须显式转型:未知类型参数与 interval 做运算时 PostgreSQL 无法唯一解析操作符。 await client.query( ` @@ -128,16 +130,16 @@ export async function confirmWorkOrderFriendAdded(input: { SET gift_phase = 'friend_countdown', friend_added_at = $1::timestamptz, - gift_available_at = $1::timestamptz + ($2::int * INTERVAL '1 hour'), + gift_available_at = $1::timestamptz + ($2::int * INTERVAL '1 hour') + ($3::int * INTERVAL '1 minute'), deadline_at = CASE WHEN timeout_minutes > 0 - THEN $1::timestamptz + ($2::int * INTERVAL '1 hour') + (timeout_minutes * INTERVAL '1 minute') + THEN $1::timestamptz + ($2::int * INTERVAL '1 hour') + ($3::int * INTERVAL '1 minute') + (timeout_minutes * INTERVAL '1 minute') ELSE NULL END, updated_at = $1::timestamptz - WHERE id = $3 + WHERE id = $4 `, - [input.now, cooldownHours, input.workOrderId], + [input.now, cooldownHours, cooldownMinutes, input.workOrderId], ) await createWorkOrderEventWithClient(client, { workOrderId: input.workOrderId, @@ -148,6 +150,7 @@ export async function confirmWorkOrderFriendAdded(input: { toStatus: workOrder.status, payloadJson: JSON.stringify({ cooldownHours, + cooldownMinutes, friendAddedAt: input.now, }), now: input.now, diff --git a/apps/backend/src/repositories/worker-platform/work-order-management-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-management-repo.ts index 0e98f11a..40937f3a 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-management-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-management-repo.ts @@ -4,6 +4,7 @@ import { getWorkOrderById } from './work-order-query-repo.js' import { normalizeWorkOrderAcceptanceMode, normalizeWorkOrderGiftCooldownHours, + normalizeWorkOrderGiftCooldownMinutes, WORK_ORDER_ACCEPTANCE_MODE, WORK_ORDER_GIFT_PHASE, } from '../../domain/work-order-acceptance-mode.js' @@ -19,13 +20,13 @@ export async function createWorkOrder(input: CreateWorkOrderInput): Promise 0 ? 'minutes' : 'hours', + giftCooldownValue: + Number(rule.giftCooldownMinutes || 0) > 0 + ? Number(rule.giftCooldownMinutes || 0) + : Number(rule.giftCooldownHours ?? 72), fieldsText: formatRuleFields(rule.requirement?.fields || []), sortOrder: rule.sortOrder, enabled: rule.enabled, @@ -247,7 +251,8 @@ export default function ProductRulesPanel() { timeoutMinutes?: number timeoutPolicy?: string acceptanceMode?: string - giftCooldownHours?: number + giftCooldownUnit?: 'hours' | 'minutes' + giftCooldownValue?: number fieldsText?: string enabled?: boolean autoCreate?: boolean @@ -265,13 +270,17 @@ export default function ProductRulesPanel() { ? Number(values.sharingTotalQuantity || 0) > 0 || Number(values.sharingUnitReward || 0) > 0 : hasManualSharingInput(values.sharingTotalQuantity, values.sharingUnitReward) - const { pricingMode: _pricingMode, ...payload } = values + const { pricingMode: _pricingMode, giftCooldownUnit, giftCooldownValue, ...payload } = values const res = await saveAdminWorkProductRule({ ...payload, ruleId: editingRule?.ruleId, ruleKey: editingRule?.ruleKey || '', unitPrice: unitMode ? Number(values.unitPrice || 0) : 0, acceptanceMode: isFriendGift ? 'friend_gift' : 'standard', + giftCooldownHours: + isFriendGift && giftCooldownUnit !== 'minutes' ? Number(giftCooldownValue || 0) : 0, + giftCooldownMinutes: + isFriendGift && giftCooldownUnit === 'minutes' ? Number(giftCooldownValue || 0) : 0, sharingEnabled: isFriendGift ? false : values.sharingEnabled === true, sharingAutoFromOrder: unitMode && values.sharingEnabled === true && !hasManualSharingOverride, @@ -745,7 +754,8 @@ export default function ProductRulesPanel() { autoCreate: true, sharingEnabled: false, acceptanceMode: 'standard', - giftCooldownHours: 72, + giftCooldownUnit: 'hours', + giftCooldownValue: 72, fieldsText: 'gameId:游戏编号\ngameNickname:游戏昵称\nsystem:系统#安卓,苹果\nserverZone:区服#QQ区,微信区', }} @@ -838,19 +848,47 @@ export default function ProductRulesPanel() { {({ getFieldValue }) => getFieldValue('acceptanceMode') === 'friend_gift' ? ( - - + + + +