feat(consignment): 内部现货履约改为停在「待加好友」,由客服确认后进赠送冷却
需求:发布匹配成功后订单要停在待加好友,客服线下通知加好友、手动确认后才进赠送冷却。 - 新增 parkWorkOrderWaitingFriendAddWithClient:占库落地时把工单停在 acceptance_mode=friend_gift + gift_phase=material_required 的「待加好友」阶段, friend_added_at / gift_available_at / deadline_at 都留空(没有打手在跑,不该背时限), 并把商品规则里的冷却时长落到工单上(两者都为 0 时补默认 72 小时, 否则客服确认后会得到 0 秒冷却而立刻可赠送)。 - 履约服务不再传 forceFriendGift(该开关会直接把 friend_added_at 写成当前时间、 跳过客服确认);同步删除这个已无调用方的开关,冷却起始事件里的 skippedWorkerMaterial 改为按是否内部现货履约判断。 - 前端:内部现货单在该阶段显示「待加好友」(原为「待打手提交资料」), 确认按钮统一为「确认加好友」,确认弹窗补上"先用库存账号加好友、通知进展记备注"的引导。 - 后台备注增加快捷项「已通知加好友」,一键追加「[已通知加好友] 操作人 时间」。 - 阶段文案抽到 utils/gift-phase.ts 以便单测覆盖这条口径。 验证:真实库跑通全链路(停阶段 → 确认加好友进冷却 → 重复确认被拒 → 事件流正确,临时单已清理); 后端 415 例(412 通过,新增 3 例)、前端 25 例(新增 4 例)全绿。
This commit is contained in:
@@ -83,8 +83,15 @@ export function hasInternalConsignmentFulfillment(workOrder: {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 开始好友赠送冷却:内部寄售跳过打手资料,直接进入等 N 天。 */
|
/**
|
||||||
export async function startWorkOrderGiftCountdownWithClient(
|
* 内部寄售现货履约落地:占库成功,但**不**算已加好友。
|
||||||
|
*
|
||||||
|
* 把工单停在「待加好友」阶段,由客服线下通知买家/加好友后**手动**点确认,
|
||||||
|
* 才进入等 N 天赠送冷却(见 confirmWorkOrderFriendAdded)。落库复用
|
||||||
|
* acceptance_mode=friend_gift 的初始阶段 material_required:内部现货单本来就没有打手资料,
|
||||||
|
* 该阶段在这里只表示"待加好友",前端对内部现货单展示为「待加好友」。
|
||||||
|
*/
|
||||||
|
export async function parkWorkOrderWaitingFriendAddWithClient(
|
||||||
client: PoolClient,
|
client: PoolClient,
|
||||||
workOrder: WorkOrderRow,
|
workOrder: WorkOrderRow,
|
||||||
input: {
|
input: {
|
||||||
@@ -93,14 +100,15 @@ export async function startWorkOrderGiftCountdownWithClient(
|
|||||||
eventType?: string | undefined
|
eventType?: string | undefined
|
||||||
materialJson?: unknown
|
materialJson?: unknown
|
||||||
extraPayload?: Record<string, unknown>
|
extraPayload?: Record<string, unknown>
|
||||||
forceFriendGift?: boolean
|
|
||||||
},
|
},
|
||||||
): Promise<WorkOrderRow | null> {
|
): Promise<WorkOrderRow | null> {
|
||||||
if (GIFT_COUNTDOWN_BLOCKED_STATUSES.has(workOrder.status)) return null
|
if (GIFT_COUNTDOWN_BLOCKED_STATUSES.has(workOrder.status)) return null
|
||||||
|
|
||||||
|
// 冷却时长按商品规则落到工单上:两者都为 0 时补默认 72 小时,
|
||||||
|
// 否则客服确认后会得到 0 秒冷却(gift_available_at 立刻到点)。
|
||||||
let cooldownHours = normalizeWorkOrderGiftCooldownHours(workOrder.gift_cooldown_hours)
|
let cooldownHours = normalizeWorkOrderGiftCooldownHours(workOrder.gift_cooldown_hours)
|
||||||
const cooldownMinutes = normalizeWorkOrderGiftCooldownMinutes(workOrder.gift_cooldown_minutes)
|
const cooldownMinutes = normalizeWorkOrderGiftCooldownMinutes(workOrder.gift_cooldown_minutes)
|
||||||
if (input.forceFriendGift && cooldownHours === 0 && cooldownMinutes === 0) {
|
if (cooldownHours === 0 && cooldownMinutes === 0) {
|
||||||
cooldownHours = DEFAULT_GIFT_COOLDOWN_HOURS
|
cooldownHours = DEFAULT_GIFT_COOLDOWN_HOURS
|
||||||
}
|
}
|
||||||
const nextStatus = ['problem'].includes(workOrder.status) ? workOrder.status : 'in_progress'
|
const nextStatus = ['problem'].includes(workOrder.status) ? workOrder.status : 'in_progress'
|
||||||
@@ -118,8 +126,72 @@ export async function startWorkOrderGiftCountdownWithClient(
|
|||||||
UPDATE work_orders
|
UPDATE work_orders
|
||||||
SET
|
SET
|
||||||
status = $1,
|
status = $1,
|
||||||
acceptance_mode = CASE WHEN $7 THEN 'friend_gift' ELSE acceptance_mode END,
|
acceptance_mode = 'friend_gift',
|
||||||
gift_cooldown_hours = CASE WHEN $7 THEN $3 ELSE gift_cooldown_hours END,
|
gift_cooldown_hours = $3,
|
||||||
|
gift_phase = 'material_required',
|
||||||
|
friend_added_at = NULL,
|
||||||
|
gift_available_at = NULL,
|
||||||
|
deadline_at = NULL,
|
||||||
|
accepted_at = CASE WHEN $4 THEN NULL ELSE accepted_at END,
|
||||||
|
material_json = COALESCE($5::jsonb, material_json),
|
||||||
|
updated_at = $2::timestamptz
|
||||||
|
WHERE id = $6
|
||||||
|
`,
|
||||||
|
[nextStatus, input.now, cooldownHours, clearAcceptedAt, materialJson, workOrder.id],
|
||||||
|
)
|
||||||
|
await createWorkOrderEventWithClient(client, {
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
actorType: 'admin',
|
||||||
|
actorId: input.actorName || '',
|
||||||
|
eventType: input.eventType || 'consignment_waiting_friend_add',
|
||||||
|
fromStatus: workOrder.status,
|
||||||
|
toStatus: nextStatus,
|
||||||
|
payloadJson: JSON.stringify({
|
||||||
|
cooldownHours,
|
||||||
|
cooldownMinutes,
|
||||||
|
giftPhase: WORK_ORDER_GIFT_PHASE.MATERIAL_REQUIRED,
|
||||||
|
internalConsignment: hasInternalConsignmentFulfillment({
|
||||||
|
...workOrder,
|
||||||
|
material_json: materialForInternalCheck,
|
||||||
|
}),
|
||||||
|
...(input.extraPayload || {}),
|
||||||
|
}),
|
||||||
|
now: input.now,
|
||||||
|
})
|
||||||
|
return getWorkOrderByIdWithClient(client, workOrder.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 开始好友赠送冷却:客服确认加好友后进入等 N 天。 */
|
||||||
|
export async function startWorkOrderGiftCountdownWithClient(
|
||||||
|
client: PoolClient,
|
||||||
|
workOrder: WorkOrderRow,
|
||||||
|
input: {
|
||||||
|
now: string
|
||||||
|
actorName?: string | undefined
|
||||||
|
eventType?: string | undefined
|
||||||
|
materialJson?: unknown
|
||||||
|
extraPayload?: Record<string, unknown>
|
||||||
|
},
|
||||||
|
): Promise<WorkOrderRow | null> {
|
||||||
|
if (GIFT_COUNTDOWN_BLOCKED_STATUSES.has(workOrder.status)) return null
|
||||||
|
|
||||||
|
const cooldownHours = normalizeWorkOrderGiftCooldownHours(workOrder.gift_cooldown_hours)
|
||||||
|
const cooldownMinutes = normalizeWorkOrderGiftCooldownMinutes(workOrder.gift_cooldown_minutes)
|
||||||
|
const nextStatus = ['problem'].includes(workOrder.status) ? workOrder.status : 'in_progress'
|
||||||
|
const clearAcceptedAt = ['accepted', 'completed'].includes(workOrder.status)
|
||||||
|
const materialJson =
|
||||||
|
input.materialJson === undefined
|
||||||
|
? null
|
||||||
|
: typeof input.materialJson === 'string'
|
||||||
|
? input.materialJson
|
||||||
|
: JSON.stringify(input.materialJson)
|
||||||
|
const materialForInternalCheck =
|
||||||
|
input.materialJson === undefined ? workOrder.material_json : input.materialJson
|
||||||
|
await client.query(
|
||||||
|
`
|
||||||
|
UPDATE work_orders
|
||||||
|
SET
|
||||||
|
status = $1,
|
||||||
gift_phase = 'friend_countdown',
|
gift_phase = 'friend_countdown',
|
||||||
friend_added_at = $2::timestamptz,
|
friend_added_at = $2::timestamptz,
|
||||||
gift_available_at = $2::timestamptz + ($3::int * INTERVAL '1 hour') + ($4::int * INTERVAL '1 minute'),
|
gift_available_at = $2::timestamptz + ($3::int * INTERVAL '1 hour') + ($4::int * INTERVAL '1 minute'),
|
||||||
@@ -129,7 +201,7 @@ export async function startWorkOrderGiftCountdownWithClient(
|
|||||||
ELSE NULL
|
ELSE NULL
|
||||||
END,
|
END,
|
||||||
accepted_at = CASE WHEN $5 THEN NULL ELSE accepted_at END,
|
accepted_at = CASE WHEN $5 THEN NULL ELSE accepted_at END,
|
||||||
material_json = COALESCE($8::jsonb, material_json),
|
material_json = COALESCE($7::jsonb, material_json),
|
||||||
updated_at = $2::timestamptz
|
updated_at = $2::timestamptz
|
||||||
WHERE id = $6
|
WHERE id = $6
|
||||||
`,
|
`,
|
||||||
@@ -140,7 +212,6 @@ export async function startWorkOrderGiftCountdownWithClient(
|
|||||||
cooldownMinutes,
|
cooldownMinutes,
|
||||||
clearAcceptedAt,
|
clearAcceptedAt,
|
||||||
workOrder.id,
|
workOrder.id,
|
||||||
Boolean(input.forceFriendGift),
|
|
||||||
materialJson,
|
materialJson,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -159,7 +230,10 @@ export async function startWorkOrderGiftCountdownWithClient(
|
|||||||
...workOrder,
|
...workOrder,
|
||||||
material_json: materialForInternalCheck,
|
material_json: materialForInternalCheck,
|
||||||
}),
|
}),
|
||||||
skippedWorkerMaterial: Boolean(input.forceFriendGift),
|
skippedWorkerMaterial: hasInternalConsignmentFulfillment({
|
||||||
|
...workOrder,
|
||||||
|
material_json: materialForInternalCheck,
|
||||||
|
}),
|
||||||
giftPhase: WORK_ORDER_GIFT_PHASE.FRIEND_COUNTDOWN,
|
giftPhase: WORK_ORDER_GIFT_PHASE.FRIEND_COUNTDOWN,
|
||||||
...(input.extraPayload || {}),
|
...(input.extraPayload || {}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
createWorkOrderEvent,
|
createWorkOrderEvent,
|
||||||
createWorkOrderEventWithClient,
|
createWorkOrderEventWithClient,
|
||||||
getWorkOrderById,
|
getWorkOrderById,
|
||||||
startWorkOrderGiftCountdownWithClient,
|
parkWorkOrderWaitingFriendAddWithClient,
|
||||||
updateWorkOrder,
|
updateWorkOrder,
|
||||||
type WorkOrderRow,
|
type WorkOrderRow,
|
||||||
} from '../../repositories/worker-platform/index.js'
|
} from '../../repositories/worker-platform/index.js'
|
||||||
@@ -198,7 +198,8 @@ export async function fulfillWorkOrderWithConsignment(input: {
|
|||||||
|
|
||||||
const now = nowIso()
|
const now = nowIso()
|
||||||
|
|
||||||
// 开启原子事务:占库 + 工单进入赠送冷静期。押金留到验收再释放。
|
// 开启原子事务:占库 + 工单停在「待加好友」。押金留到验收再释放,
|
||||||
|
// 客服线下通知加好友并手动确认后(confirm-friend)才进入赠送冷却。
|
||||||
const { updatedOrder, updatedInventory } = await withTransaction(async (client: PoolClient) => {
|
const { updatedOrder, updatedInventory } = await withTransaction(async (client: PoolClient) => {
|
||||||
const lockedOrderRes = await client.query<WorkOrderRow>(
|
const lockedOrderRes = await client.query<WorkOrderRow>(
|
||||||
'SELECT * FROM work_orders WHERE id = $1 AND status = $2 FOR UPDATE',
|
'SELECT * FROM work_orders WHERE id = $1 AND status = $2 FOR UPDATE',
|
||||||
@@ -265,12 +266,11 @@ export async function fulfillWorkOrderWithConsignment(input: {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const wo = await startWorkOrderGiftCountdownWithClient(client, lockedOrder, {
|
const wo = await parkWorkOrderWaitingFriendAddWithClient(client, lockedOrder, {
|
||||||
now,
|
now,
|
||||||
actorName: input.actorName,
|
actorName: input.actorName,
|
||||||
eventType: 'consignment_fulfilled',
|
eventType: 'consignment_fulfilled',
|
||||||
materialJson: updatedMaterial,
|
materialJson: updatedMaterial,
|
||||||
forceFriendGift: true,
|
|
||||||
extraPayload: {
|
extraPayload: {
|
||||||
inventoryId: inv.id,
|
inventoryId: inv.id,
|
||||||
inventoryNo: inv.inventory_no,
|
inventoryNo: inv.inventory_no,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
resolveOpenSharingSyncReward,
|
resolveOpenSharingSyncReward,
|
||||||
} from '../../repositories/worker-platform/index.js'
|
} from '../../repositories/worker-platform/index.js'
|
||||||
import { resolveProblemReturnToWorkerStatus } from '../../repositories/worker-platform/work-order-issue-repo.js'
|
import { resolveProblemReturnToWorkerStatus } from '../../repositories/worker-platform/work-order-issue-repo.js'
|
||||||
|
import type { WorkOrderRow } from '../../repositories/worker-platform/types.js'
|
||||||
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
|
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
|
||||||
import {
|
import {
|
||||||
assertWithdrawChannelAllowedForAmount,
|
assertWithdrawChannelAllowedForAmount,
|
||||||
@@ -69,6 +70,7 @@ import {
|
|||||||
} from '../../repositories/worker-platform/consignment-repo.js'
|
} from '../../repositories/worker-platform/consignment-repo.js'
|
||||||
import { ADMIN_PERMISSION_KEYS } from '../admin/admin-rbac-service.js'
|
import { ADMIN_PERMISSION_KEYS } from '../admin/admin-rbac-service.js'
|
||||||
import { updateAdminConsignmentInventory } from './consignment-admin-service.js'
|
import { updateAdminConsignmentInventory } from './consignment-admin-service.js'
|
||||||
|
import { parkWorkOrderWaitingFriendAddWithClient } from '../../repositories/worker-platform/work-order-gift-repo.js'
|
||||||
import {
|
import {
|
||||||
assertConsignmentRevocableWorkOrderStatus,
|
assertConsignmentRevocableWorkOrderStatus,
|
||||||
resolveRevocableConsignmentInventoryId,
|
resolveRevocableConsignmentInventoryId,
|
||||||
@@ -2048,3 +2050,86 @@ test('编辑接口仍然拒绝直接改物品状态(回归)', async () => {
|
|||||||
(error as { errorCode?: string }).errorCode === 'consignment_status_update_forbidden',
|
(error as { errorCode?: string }).errorCode === 'consignment_status_update_forbidden',
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('内部现货履约停在「待加好友」:不写 friend_added_at,也不进冷却', async () => {
|
||||||
|
const calls: Array<{ sql: string; values?: unknown[] }> = []
|
||||||
|
const client = {
|
||||||
|
query: async (sql: string, values?: unknown[]) => {
|
||||||
|
calls.push({ sql, values })
|
||||||
|
return { rows: [] }
|
||||||
|
},
|
||||||
|
} as unknown as PoolClient
|
||||||
|
|
||||||
|
await parkWorkOrderWaitingFriendAddWithClient(
|
||||||
|
client,
|
||||||
|
{
|
||||||
|
id: 100154,
|
||||||
|
status: 'pending_material',
|
||||||
|
acceptance_mode: 'standard',
|
||||||
|
gift_cooldown_hours: 0,
|
||||||
|
gift_cooldown_minutes: 0,
|
||||||
|
assigned_worker_id: null,
|
||||||
|
material_json: { consignment: { inventoryId: 1, fulfillmentType: 'internal_stock' } },
|
||||||
|
} as unknown as WorkOrderRow,
|
||||||
|
{ now: '2026-09-16T12:00:00.000Z', actorName: '客服A' },
|
||||||
|
)
|
||||||
|
|
||||||
|
const update = calls.find((call) => call.sql.includes('UPDATE work_orders'))
|
||||||
|
assert.ok(update, '停阶段必须更新工单')
|
||||||
|
const sql = update.sql
|
||||||
|
assert.ok(sql.includes("gift_phase = 'material_required'"), '停留阶段应为待加好友阶段')
|
||||||
|
assert.ok(sql.includes('friend_added_at = NULL'), '不能自动确认加好友')
|
||||||
|
assert.ok(sql.includes('gift_available_at = NULL'))
|
||||||
|
assert.ok(sql.includes('deadline_at = NULL'), '没有打手在跑,不应背任务时限')
|
||||||
|
assert.ok(sql.includes("acceptance_mode = 'friend_gift'"))
|
||||||
|
assert.ok(!sql.includes('friend_added_at = $2'), '不能把当前时间写成已加好友')
|
||||||
|
|
||||||
|
// 冷却时长两者都为 0 时补默认 72 小时,否则客服确认后会得到 0 秒冷却
|
||||||
|
assert.equal(update.values?.[2], 72)
|
||||||
|
|
||||||
|
const event = calls.find((call) => call.sql.includes('INSERT INTO work_order_events'))
|
||||||
|
assert.ok(
|
||||||
|
String(event?.values?.join(' ')).includes('"giftPhase":"material_required"'),
|
||||||
|
'事件要记录停留在待加好友阶段',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('停在待加好友时保留商品规则已配的冷却时长', async () => {
|
||||||
|
const calls: Array<{ sql: string; values?: unknown[] }> = []
|
||||||
|
const client = {
|
||||||
|
query: async (sql: string, values?: unknown[]) => {
|
||||||
|
calls.push({ sql, values })
|
||||||
|
return { rows: [] }
|
||||||
|
},
|
||||||
|
} as unknown as PoolClient
|
||||||
|
|
||||||
|
await parkWorkOrderWaitingFriendAddWithClient(
|
||||||
|
client,
|
||||||
|
{
|
||||||
|
id: 100155,
|
||||||
|
status: 'pending_material',
|
||||||
|
acceptance_mode: 'friend_gift',
|
||||||
|
gift_cooldown_hours: 48,
|
||||||
|
gift_cooldown_minutes: 0,
|
||||||
|
material_json: {},
|
||||||
|
} as unknown as WorkOrderRow,
|
||||||
|
{ now: '2026-09-16T12:00:00.000Z' },
|
||||||
|
)
|
||||||
|
|
||||||
|
const update = calls.find((call) => call.sql.includes('UPDATE work_orders'))
|
||||||
|
assert.equal(update?.values?.[2], 48, '规则里配了 48 小时就不能被默认值覆盖')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('内部现货履约不再自动跳过加好友直接进冷却', async () => {
|
||||||
|
const readFile = await import('node:fs/promises')
|
||||||
|
const src = await readFile.readFile(
|
||||||
|
new URL('./consignment-fulfillment-service.ts', import.meta.url),
|
||||||
|
'utf8',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
src.includes('parkWorkOrderWaitingFriendAddWithClient('),
|
||||||
|
'履约成功后必须停在待加好友,等客服确认',
|
||||||
|
)
|
||||||
|
assert.ok(!src.includes('forceFriendGift'), '不能再有"自动确认加好友"的强制进冷却开关')
|
||||||
|
})
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ import type { UploadedFile, WorkOrder, WorkOrderShare } from '@/types/worker-pla
|
|||||||
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
|
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
|
||||||
import dayjs, { ADMIN_DATE_FORMAT } from '@/lib/dayjs'
|
import dayjs, { ADMIN_DATE_FORMAT } from '@/lib/dayjs'
|
||||||
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
|
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
|
||||||
import { hasAdminPermission, hasAdminRole } from '@/utils/admin-auth'
|
import { getAdminUsername, hasAdminPermission, hasAdminRole } from '@/utils/admin-auth'
|
||||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||||
import {
|
import {
|
||||||
DeadlineCountdown,
|
DeadlineCountdown,
|
||||||
@@ -122,6 +122,12 @@ import {
|
|||||||
shouldSkipPublishConfirm,
|
shouldSkipPublishConfirm,
|
||||||
} from './work-order-panel-utils'
|
} from './work-order-panel-utils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客服备注快捷项。点击后往备注里追加一行「[标签] 操作人 时间」,
|
||||||
|
* 保留手动补充的空间,同时让"什么时候通知的、谁通知的"自动留痕。
|
||||||
|
*/
|
||||||
|
const ADMIN_NOTE_QUICK_TAGS = ['已通知加好友'] as const
|
||||||
|
|
||||||
const FILTERABLE_WORK_ORDER_STATUSES = [
|
const FILTERABLE_WORK_ORDER_STATUSES = [
|
||||||
'pending_material',
|
'pending_material',
|
||||||
'unassigned',
|
'unassigned',
|
||||||
@@ -994,6 +1000,12 @@ export default function WorkOrdersPanel() {
|
|||||||
noteForm.setFieldsValue({ note: row.adminNote || '' })
|
noteForm.setFieldsValue({ note: row.adminNote || '' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendNoteQuickTag(tag: string) {
|
||||||
|
const current = String(noteForm.getFieldValue('note') || '')
|
||||||
|
const stamp = `[${tag}] ${getAdminUsername() || '客服'} ${formatAdminDateTime(new Date().toISOString())}`
|
||||||
|
noteForm.setFieldValue('note', current.trim() ? `${current.trimEnd()}\n${stamp}` : stamp)
|
||||||
|
}
|
||||||
|
|
||||||
async function submitNote(values: { note?: string }) {
|
async function submitNote(values: { note?: string }) {
|
||||||
if (!noteOrder) return
|
if (!noteOrder) return
|
||||||
const succeeded = await runAction(
|
const succeeded = await runAction(
|
||||||
@@ -1106,14 +1118,17 @@ export default function WorkOrdersPanel() {
|
|||||||
const cooldownLabel = cooldownMinutes > 0 ? `${cooldownMinutes} 分钟` : `${cooldownHours} 小时`
|
const cooldownLabel = cooldownMinutes > 0 ? `${cooldownMinutes} 分钟` : `${cooldownHours} 小时`
|
||||||
const internal = isInternalConsignmentOrder(row)
|
const internal = isInternalConsignmentOrder(row)
|
||||||
modal.confirm({
|
modal.confirm({
|
||||||
title: internal ? '跳过打手资料,开始赠送冷却?' : '确认已添加打手为游戏好友?',
|
title: internal ? '确认已加好友,开始赠送冷却?' : '确认已添加打手为游戏好友?',
|
||||||
content: (
|
content: (
|
||||||
<div>
|
<div>
|
||||||
{internal ? (
|
{internal ? (
|
||||||
<>
|
<>
|
||||||
<Typography.Paragraph>
|
<Typography.Paragraph>
|
||||||
内部寄售履约没有打手交资料。确认后直接开始 {cooldownLabel}
|
内部寄售履约单没有打手资料。订单会一直停在「待加好友」,请先用库存账号添加买家为游戏好友;
|
||||||
赠送冷却,冷却结束后再用库存账号给买家赠送并验收。
|
通知进展可以记在订单备注里(备注支持一键填写「已通知加好友」)。
|
||||||
|
</Typography.Paragraph>
|
||||||
|
<Typography.Paragraph>
|
||||||
|
确认后开始 {cooldownLabel} 赠送冷却,冷却结束后再用库存账号赠送并验收。
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -1129,13 +1144,13 @@ export default function WorkOrdersPanel() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
okText: internal ? '开始冷却' : '已加好友,开始冷却',
|
okText: internal ? '确认加好友,开始冷却' : '已加好友,开始冷却',
|
||||||
cancelText: '取消',
|
cancelText: '取消',
|
||||||
onOk: () =>
|
onOk: () =>
|
||||||
runAction(
|
runAction(
|
||||||
() => confirmAdminWorkOrderFriend(row.workOrderId),
|
() => confirmAdminWorkOrderFriend(row.workOrderId),
|
||||||
internal
|
internal
|
||||||
? `已跳过打手资料,${cooldownLabel} 冷却开始`
|
? `已确认加好友,${cooldownLabel} 冷却开始`
|
||||||
: `已确认加好友,${cooldownLabel} 冷却开始`,
|
: `已确认加好友,${cooldownLabel} 冷却开始`,
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
@@ -1326,7 +1341,7 @@ export default function WorkOrdersPanel() {
|
|||||||
(!['pending_material', 'unassigned', 'open'].includes(row.rawStatus || row.status) ||
|
(!['pending_material', 'unassigned', 'open'].includes(row.rawStatus || row.status) ||
|
||||||
isInternalConsignmentOrder(row)) ? (
|
isInternalConsignmentOrder(row)) ? (
|
||||||
<Tag color={resolveGiftPhaseColor(row.gift.phase)}>
|
<Tag color={resolveGiftPhaseColor(row.gift.phase)}>
|
||||||
{formatGiftPhase(row.gift.phase)}
|
{formatGiftPhase(row.gift.phase, isInternalConsignmentOrder(row))}
|
||||||
</Tag>
|
</Tag>
|
||||||
) : null}
|
) : null}
|
||||||
</Space>
|
</Space>
|
||||||
@@ -1546,9 +1561,7 @@ export default function WorkOrdersPanel() {
|
|||||||
icon={<UserAddOutlined />}
|
icon={<UserAddOutlined />}
|
||||||
onClick={() => confirmFriendAdded(row)}
|
onClick={() => confirmFriendAdded(row)}
|
||||||
>
|
>
|
||||||
{isInternalConsignmentOrder(row) && row.gift?.phase === 'material_required'
|
确认加好友
|
||||||
? '开始赠送冷却'
|
|
||||||
: '确认加好友'}
|
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{row.capabilities?.canCompleteGiftCooldown && canManageProblem ? (
|
{row.capabilities?.canCompleteGiftCooldown && canManageProblem ? (
|
||||||
@@ -1954,7 +1967,10 @@ export default function WorkOrdersPanel() {
|
|||||||
<Space>
|
<Space>
|
||||||
<Tag color="purple">{formatAcceptanceMode(detailOrder.acceptanceMode)}</Tag>
|
<Tag color="purple">{formatAcceptanceMode(detailOrder.acceptanceMode)}</Tag>
|
||||||
<Tag color={resolveGiftPhaseColor(detailOrder.gift.phase)}>
|
<Tag color={resolveGiftPhaseColor(detailOrder.gift.phase)}>
|
||||||
{formatGiftPhase(detailOrder.gift.phase)}
|
{formatGiftPhase(
|
||||||
|
detailOrder.gift.phase,
|
||||||
|
isInternalConsignmentOrder(detailOrder),
|
||||||
|
)}
|
||||||
</Tag>
|
</Tag>
|
||||||
{detailOrder.capabilities?.canConfirmFriend && canManageProblem ? (
|
{detailOrder.capabilities?.canConfirmFriend && canManageProblem ? (
|
||||||
<Button
|
<Button
|
||||||
@@ -1962,10 +1978,7 @@ export default function WorkOrdersPanel() {
|
|||||||
size="small"
|
size="small"
|
||||||
onClick={() => confirmFriendAdded(detailOrder)}
|
onClick={() => confirmFriendAdded(detailOrder)}
|
||||||
>
|
>
|
||||||
{isInternalConsignmentOrder(detailOrder) &&
|
确认加好友
|
||||||
detailOrder.gift?.phase === 'material_required'
|
|
||||||
? '开始赠送冷却'
|
|
||||||
: '确认加好友'}
|
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{detailOrder.capabilities?.canCompleteGiftCooldown && canManageProblem ? (
|
{detailOrder.capabilities?.canCompleteGiftCooldown && canManageProblem ? (
|
||||||
@@ -2137,6 +2150,14 @@ export default function WorkOrdersPanel() {
|
|||||||
placeholder="请输入本单客服备注"
|
placeholder="请输入本单客服备注"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Space size={8} wrap style={{ marginTop: -8 }}>
|
||||||
|
<Typography.Text type="secondary">快捷填写:</Typography.Text>
|
||||||
|
{ADMIN_NOTE_QUICK_TAGS.map((tag) => (
|
||||||
|
<Button key={tag} size="small" onClick={() => appendNoteQuickTag(tag)}>
|
||||||
|
{tag}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Space>
|
||||||
</Form>
|
</Form>
|
||||||
) : null}
|
) : null}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -249,25 +249,8 @@ export function formatAcceptanceMode(mode: string) {
|
|||||||
return '标准验收'
|
return '标准验收'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatGiftPhase(phase: string) {
|
// 阶段文案与配色在 utils 里实现,便于单测覆盖「内部现货单显示待加好友」这条口径。
|
||||||
const labels: Record<string, string> = {
|
export { formatGiftPhase, resolveGiftPhaseColor } from '@/utils/gift-phase'
|
||||||
material_required: '待打手提交资料',
|
|
||||||
material_submitted: '待确认加好友',
|
|
||||||
friend_countdown: '赠送冷却中',
|
|
||||||
gift_ready: '可赠送',
|
|
||||||
}
|
|
||||||
return labels[phase] || phase
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveGiftPhaseColor(phase: string) {
|
|
||||||
const colors: Record<string, string> = {
|
|
||||||
material_required: 'orange',
|
|
||||||
material_submitted: 'geekblue',
|
|
||||||
friend_countdown: 'gold',
|
|
||||||
gift_ready: 'green',
|
|
||||||
}
|
|
||||||
return colors[phase] || 'default'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveSharingShareStatusColor(status: string) {
|
export function resolveSharingShareStatusColor(status: string) {
|
||||||
if (status === 'accepted') return 'green'
|
if (status === 'accepted') return 'green'
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* 好友赠送阶段文案与配色。
|
||||||
|
*
|
||||||
|
* 注意 material_required 在不同履约方式下含义不同:
|
||||||
|
* - 打手履约单:等打手提交好友资料,显示「待打手提交资料」;
|
||||||
|
* - 内部现货履约单:没有打手资料这一步,占库成功后停在这里等客服线下通知加好友并手动确认,
|
||||||
|
* 所以要显示「待加好友」。
|
||||||
|
*/
|
||||||
|
export function formatGiftPhase(phase: string, internalConsignment = false) {
|
||||||
|
if (internalConsignment && phase === 'material_required') return '待加好友'
|
||||||
|
const labels: Record<string, string> = {
|
||||||
|
material_required: '待打手提交资料',
|
||||||
|
material_submitted: '待确认加好友',
|
||||||
|
friend_countdown: '赠送冷却中',
|
||||||
|
gift_ready: '可赠送',
|
||||||
|
}
|
||||||
|
return labels[phase] || phase
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveGiftPhaseColor(phase: string) {
|
||||||
|
const colors: Record<string, string> = {
|
||||||
|
material_required: 'orange',
|
||||||
|
material_submitted: 'geekblue',
|
||||||
|
friend_countdown: 'gold',
|
||||||
|
gift_ready: 'green',
|
||||||
|
}
|
||||||
|
return colors[phase] || 'default'
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
|
import { formatGiftPhase, resolveGiftPhaseColor } from '../src/utils/gift-phase.ts'
|
||||||
|
|
||||||
|
test('内部现货履约单停在 material_required 时显示待加好友', () => {
|
||||||
|
assert.equal(formatGiftPhase('material_required', true), '待加好友')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('打手履约单的 material_required 仍是待打手提交资料', () => {
|
||||||
|
assert.equal(formatGiftPhase('material_required'), '待打手提交资料')
|
||||||
|
assert.equal(formatGiftPhase('material_required', false), '待打手提交资料')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('其余阶段文案不受履约方式影响', () => {
|
||||||
|
assert.equal(formatGiftPhase('material_submitted', true), '待确认加好友')
|
||||||
|
assert.equal(formatGiftPhase('friend_countdown', true), '赠送冷却中')
|
||||||
|
assert.equal(formatGiftPhase('gift_ready', true), '可赠送')
|
||||||
|
assert.equal(formatGiftPhase('unknown_phase', true), 'unknown_phase')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('阶段配色保持稳定', () => {
|
||||||
|
assert.equal(resolveGiftPhaseColor('material_required'), 'orange')
|
||||||
|
assert.equal(resolveGiftPhaseColor('friend_countdown'), 'gold')
|
||||||
|
assert.equal(resolveGiftPhaseColor('unknown_phase'), 'default')
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user