feat(接单): 好友赠送冷却支持分钟单位
This commit is contained in:
@@ -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,从模板拷贝';
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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<string, unknown>
|
||||
match_json: string | Record<string, unknown>
|
||||
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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Work
|
||||
product_name, category_id, status, reward_amount, required_deposit_amount,
|
||||
deposit_threshold_amount, sharing_enabled, sharing_total_quantity,
|
||||
sharing_unit_reward, timeout_minutes, timeout_policy,
|
||||
acceptance_mode, gift_phase, gift_cooldown_hours,
|
||||
acceptance_mode, gift_phase, gift_cooldown_hours, gift_cooldown_minutes,
|
||||
material_json, requirement_json, created_at, updated_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11,
|
||||
$12, $13, $14, $15, $16, $17,
|
||||
$18, $19, $20,
|
||||
$21::jsonb, $22::jsonb, $23, $24
|
||||
$18, $19, $20, $21,
|
||||
$22::jsonb, $23::jsonb, $24, $25
|
||||
)
|
||||
RETURNING id
|
||||
`,
|
||||
@@ -50,6 +51,7 @@ export async function createWorkOrder(input: CreateWorkOrderInput): Promise<Work
|
||||
acceptanceMode,
|
||||
isFriendGift ? WORK_ORDER_GIFT_PHASE.MATERIAL_REQUIRED : '',
|
||||
normalizeWorkOrderGiftCooldownHours(input.giftCooldownHours),
|
||||
normalizeWorkOrderGiftCooldownMinutes(input.giftCooldownMinutes),
|
||||
input.materialJson,
|
||||
input.requirementJson,
|
||||
input.now,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { toPositiveInteger } from './shared.js'
|
||||
import {
|
||||
normalizeWorkOrderAcceptanceMode,
|
||||
normalizeWorkOrderGiftCooldownHours,
|
||||
normalizeWorkOrderGiftCooldownMinutes,
|
||||
} from '../../domain/work-order-acceptance-mode.js'
|
||||
import type {
|
||||
ProductRuleListInput,
|
||||
@@ -342,6 +343,7 @@ export async function upsertWorkProductRule(input: {
|
||||
timeoutPolicy?: string
|
||||
acceptanceMode?: string
|
||||
giftCooldownHours?: number
|
||||
giftCooldownMinutes?: number
|
||||
requirementJson: string
|
||||
matchJson?: string
|
||||
sortOrder: number
|
||||
@@ -356,7 +358,7 @@ export async function upsertWorkProductRule(input: {
|
||||
required_deposit_amount, deposit_threshold_amount,
|
||||
sharing_enabled, sharing_total_quantity, sharing_unit_reward, sharing_auto_from_order,
|
||||
timeout_minutes, timeout_policy,
|
||||
acceptance_mode, gift_cooldown_hours,
|
||||
acceptance_mode, gift_cooldown_hours, gift_cooldown_minutes,
|
||||
requirement_json, match_json,
|
||||
sort_order, created_at, updated_at
|
||||
) VALUES (
|
||||
@@ -366,9 +368,9 @@ export async function upsertWorkProductRule(input: {
|
||||
$13, $14,
|
||||
$15, $16, $17, $18,
|
||||
$19, $20,
|
||||
$21, $22,
|
||||
$23::jsonb, $24::jsonb,
|
||||
$25, $26, $27
|
||||
$21, $22, $23,
|
||||
$24::jsonb, $25::jsonb,
|
||||
$26, $27, $28
|
||||
)
|
||||
ON CONFLICT (rule_key) DO UPDATE
|
||||
SET
|
||||
@@ -393,6 +395,7 @@ export async function upsertWorkProductRule(input: {
|
||||
timeout_policy = EXCLUDED.timeout_policy,
|
||||
acceptance_mode = EXCLUDED.acceptance_mode,
|
||||
gift_cooldown_hours = EXCLUDED.gift_cooldown_hours,
|
||||
gift_cooldown_minutes = EXCLUDED.gift_cooldown_minutes,
|
||||
requirement_json = EXCLUDED.requirement_json,
|
||||
match_json = EXCLUDED.match_json,
|
||||
sort_order = EXCLUDED.sort_order,
|
||||
@@ -422,6 +425,7 @@ export async function upsertWorkProductRule(input: {
|
||||
String(input.timeoutPolicy || 'reopen').trim(),
|
||||
normalizeWorkOrderAcceptanceMode(input.acceptanceMode),
|
||||
normalizeWorkOrderGiftCooldownHours(input.giftCooldownHours),
|
||||
normalizeWorkOrderGiftCooldownMinutes(input.giftCooldownMinutes),
|
||||
input.requirementJson,
|
||||
input.matchJson || '{}',
|
||||
input.sortOrder,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
|
||||
import {
|
||||
normalizeWorkOrderAcceptanceMode,
|
||||
normalizeWorkOrderGiftCooldownHours,
|
||||
normalizeWorkOrderGiftCooldownMinutes,
|
||||
} from '../../domain/work-order-acceptance-mode.js'
|
||||
import {
|
||||
createWorkOrder,
|
||||
@@ -114,6 +115,7 @@ export async function createAdminMockWorkOrder(payload: JsonObject = {}) {
|
||||
timeoutPolicy: normalizeWorkOrderTimeoutPolicy(payload.timeoutPolicy),
|
||||
acceptanceMode: normalizeWorkOrderAcceptanceMode(payload.acceptanceMode),
|
||||
giftCooldownHours: normalizeWorkOrderGiftCooldownHours(payload.giftCooldownHours),
|
||||
giftCooldownMinutes: normalizeWorkOrderGiftCooldownMinutes(payload.giftCooldownMinutes),
|
||||
materialJson: JSON.stringify(material),
|
||||
requirementJson: JSON.stringify({ fields }),
|
||||
now,
|
||||
|
||||
@@ -26,6 +26,7 @@ import type { WorkOrderRow, WorkProductRuleRow } from '../../repositories/worker
|
||||
import {
|
||||
normalizeWorkOrderAcceptanceMode,
|
||||
normalizeWorkOrderGiftCooldownHours,
|
||||
normalizeWorkOrderGiftCooldownMinutes,
|
||||
WORK_ORDER_ACCEPTANCE_MODE,
|
||||
} from '../../domain/work-order-acceptance-mode.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
@@ -374,6 +375,13 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) {
|
||||
payload.giftCooldownHours ?? payload.gift_cooldown_hours,
|
||||
)
|
||||
: normalizeWorkOrderGiftCooldownHours(existingRule?.gift_cooldown_hours),
|
||||
giftCooldownMinutes:
|
||||
hasOwnPayload(payload, 'giftCooldownMinutes') ||
|
||||
hasOwnPayload(payload, 'gift_cooldown_minutes')
|
||||
? normalizeWorkOrderGiftCooldownMinutes(
|
||||
payload.giftCooldownMinutes ?? payload.gift_cooldown_minutes,
|
||||
)
|
||||
: normalizeWorkOrderGiftCooldownMinutes(existingRule?.gift_cooldown_minutes),
|
||||
requirementJson: JSON.stringify({ fields }),
|
||||
matchJson: JSON.stringify(match),
|
||||
sortOrder: normalizeInteger(payload.sortOrder, 100),
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
isFriendGiftAcceptanceMode,
|
||||
normalizeWorkOrderAcceptanceMode,
|
||||
normalizeWorkOrderGiftCooldownHours,
|
||||
normalizeWorkOrderGiftCooldownMinutes,
|
||||
} from '../../domain/work-order-acceptance-mode.js'
|
||||
import {
|
||||
createWorkProductMatchLog,
|
||||
@@ -138,6 +139,7 @@ export async function syncWorkerOrdersForSourceOrder(
|
||||
timeoutPolicy: String(rule.timeout_policy || 'reopen').trim(),
|
||||
acceptanceMode: normalizeWorkOrderAcceptanceMode(rule.acceptance_mode),
|
||||
giftCooldownHours: normalizeWorkOrderGiftCooldownHours(rule.gift_cooldown_hours),
|
||||
giftCooldownMinutes: normalizeWorkOrderGiftCooldownMinutes(rule.gift_cooldown_minutes),
|
||||
materialJson: JSON.stringify({
|
||||
source: {
|
||||
orderId: Number(order.id),
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
isFriendGiftAcceptanceMode,
|
||||
normalizeWorkOrderAcceptanceMode,
|
||||
normalizeWorkOrderGiftCooldownHours,
|
||||
normalizeWorkOrderGiftCooldownMinutes,
|
||||
resolveWorkOrderGiftPhase,
|
||||
WORK_ORDER_ACCEPTANCE_MODE,
|
||||
WORK_ORDER_GIFT_PHASE,
|
||||
@@ -212,6 +213,7 @@ export function mapWorkOrderAdmin(workOrder: WorkOrderRow, shares?: WorkOrderSha
|
||||
phase: resolveWorkOrderGiftPhase(workOrder),
|
||||
rawPhase: giftPhase,
|
||||
cooldownHours: normalizeWorkOrderGiftCooldownHours(workOrder.gift_cooldown_hours),
|
||||
cooldownMinutes: normalizeWorkOrderGiftCooldownMinutes(workOrder.gift_cooldown_minutes),
|
||||
friendAddedAt: workOrder.friend_added_at,
|
||||
giftAvailableAt: workOrder.gift_available_at,
|
||||
boosterMaterial: mapWorkOrderBoosterMaterial(workOrder.booster_material_json),
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
import {
|
||||
normalizeWorkOrderAcceptanceMode,
|
||||
normalizeWorkOrderGiftCooldownHours,
|
||||
normalizeWorkOrderGiftCooldownMinutes,
|
||||
} from '../../domain/work-order-acceptance-mode.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
|
||||
@@ -51,6 +52,7 @@ export function mapWorkProductRule(rule: WorkProductRuleRow | null | undefined)
|
||||
timeoutPolicy: String(rule.timeout_policy || 'reopen').trim(),
|
||||
acceptanceMode: normalizeWorkOrderAcceptanceMode(rule.acceptance_mode),
|
||||
giftCooldownHours: normalizeWorkOrderGiftCooldownHours(rule.gift_cooldown_hours),
|
||||
giftCooldownMinutes: normalizeWorkOrderGiftCooldownMinutes(rule.gift_cooldown_minutes),
|
||||
requirement: {
|
||||
fields: normalizeRequirementFields(requirement.fields),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user