feat: 支持待分配/待抢单修改付款金额并增加价格历史
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
-- 120_work_order_user_payment_amount_audit.sql
|
||||
-- 工单「用户付款金额」最近一次修正快照:旧价、修改时间、修改人。
|
||||
-- 完整修改历史仍在 work_order_events.payment_amount_updated。
|
||||
|
||||
ALTER TABLE work_orders
|
||||
ADD COLUMN IF NOT EXISTS user_payment_amount_previous_fen BIGINT,
|
||||
ADD COLUMN IF NOT EXISTS user_payment_amount_updated_at TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS user_payment_amount_updated_by TEXT;
|
||||
|
||||
COMMENT ON COLUMN work_orders.user_payment_amount_previous_fen IS
|
||||
'最近一次修改用户付款金额前的金额(分);NULL 表示首次修改前金额未知';
|
||||
COMMENT ON COLUMN work_orders.user_payment_amount_updated_at IS
|
||||
'最近一次修改用户付款金额的时间';
|
||||
COMMENT ON COLUMN work_orders.user_payment_amount_updated_by IS
|
||||
'最近一次修改用户付款金额的操作人展示名/用户名快照';
|
||||
@@ -0,0 +1,72 @@
|
||||
-- 121_work_order_payment_amount_history.sql
|
||||
-- 工单「用户付款金额」完整修改历史:每次修改一行。
|
||||
-- 独立于 work_order_events(默认只保留 180 天),工单删除时级联删除。
|
||||
|
||||
CREATE TABLE IF NOT EXISTS work_order_payment_amount_history (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
work_order_id BIGINT NOT NULL REFERENCES work_orders(id) ON DELETE CASCADE,
|
||||
previous_amount_fen BIGINT,
|
||||
amount_fen BIGINT NOT NULL,
|
||||
reason TEXT NOT NULL DEFAULT '',
|
||||
changed_by TEXT NOT NULL DEFAULT '',
|
||||
changed_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_work_order_payment_amount_history_order
|
||||
ON work_order_payment_amount_history(work_order_id, changed_at DESC, id DESC);
|
||||
|
||||
COMMENT ON TABLE work_order_payment_amount_history IS
|
||||
'工单用户付款金额完整修改历史;每次实际修改一行,供后台价格历史查看';
|
||||
COMMENT ON COLUMN work_order_payment_amount_history.previous_amount_fen IS
|
||||
'本次修改前的金额(分);NULL 表示首次修改前金额未知';
|
||||
COMMENT ON COLUMN work_order_payment_amount_history.amount_fen IS
|
||||
'本次修改后的金额(分)';
|
||||
COMMENT ON COLUMN work_order_payment_amount_history.reason IS
|
||||
'修改原因(选填,最多 200 字)';
|
||||
COMMENT ON COLUMN work_order_payment_amount_history.changed_by IS
|
||||
'修改人展示名/用户名快照';
|
||||
COMMENT ON COLUMN work_order_payment_amount_history.changed_at IS
|
||||
'本次修改时间';
|
||||
|
||||
-- 回填已有的 payment_amount_updated 事件。迁移只执行一次,仍按
|
||||
-- (work_order_id, changed_at, amount_fen) 防重,避免手工重跑时重复插入。
|
||||
WITH parsed AS (
|
||||
SELECT
|
||||
e.work_order_id,
|
||||
CASE
|
||||
WHEN e.payload_json->>'previousAmountFen' ~ '^-?[0-9]+$'
|
||||
THEN (e.payload_json->>'previousAmountFen')::bigint
|
||||
ELSE NULL
|
||||
END AS previous_amount_fen,
|
||||
(e.payload_json->>'amountFen')::bigint AS amount_fen,
|
||||
COALESCE(e.payload_json->>'reason', '') AS reason,
|
||||
COALESCE(e.actor_id, '') AS changed_by,
|
||||
e.created_at
|
||||
FROM work_order_events e
|
||||
WHERE e.event_type = 'payment_amount_updated'
|
||||
AND e.payload_json->>'amountFen' ~ '^-?[0-9]+$'
|
||||
)
|
||||
INSERT INTO work_order_payment_amount_history (
|
||||
work_order_id,
|
||||
previous_amount_fen,
|
||||
amount_fen,
|
||||
reason,
|
||||
changed_by,
|
||||
changed_at
|
||||
)
|
||||
SELECT
|
||||
p.work_order_id,
|
||||
p.previous_amount_fen,
|
||||
p.amount_fen,
|
||||
p.reason,
|
||||
p.changed_by,
|
||||
p.created_at
|
||||
FROM parsed p
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM work_order_payment_amount_history h
|
||||
WHERE h.work_order_id = p.work_order_id
|
||||
AND h.changed_at = p.created_at
|
||||
AND h.amount_fen = p.amount_fen
|
||||
);
|
||||
@@ -9,6 +9,7 @@ export * from './worker-ranking-repo.js'
|
||||
export * from './work-order-repo.js'
|
||||
export * from './work-order-query-repo.js'
|
||||
export * from './work-order-management-repo.js'
|
||||
export * from './work-order-payment-history-repo.js'
|
||||
export * from './work-order-grab-repo.js'
|
||||
export * from './work-order-cancel-repo.js'
|
||||
export * from './work-order-issue-repo.js'
|
||||
|
||||
@@ -399,6 +399,12 @@ export type WorkOrderRow = {
|
||||
last_assigned_worker_id: number | null
|
||||
/** 来源订单实付金额(分);取消部分时用于按比例计算退款。 */
|
||||
source_order_amount_fen: number
|
||||
/** 最近一次修改用户付款金额前的金额(分);NULL 表示首次修改前金额未知。 */
|
||||
user_payment_amount_previous_fen?: number | null
|
||||
/** 最近一次修改用户付款金额的时间。 */
|
||||
user_payment_amount_updated_at?: string | null
|
||||
/** 最近一次修改用户付款金额的操作人展示名/用户名快照。 */
|
||||
user_payment_amount_updated_by?: string | null
|
||||
material_json: string | Record<string, unknown>
|
||||
requirement_json: string | Record<string, unknown>
|
||||
acceptance_json: string | Record<string, unknown>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { query, withTransaction } from '../../db/client.js'
|
||||
import { resolveSourceOrderAmountFen, toJsonString, toPositiveInteger } from './shared.js'
|
||||
import { getWorkOrderById, getWorkOrderByIdWithClient } from './work-order-query-repo.js'
|
||||
import { createWorkOrderEventWithClient } from './work-order-event-repo.js'
|
||||
import { insertWorkOrderPaymentAmountHistoryWithClient } from './work-order-payment-history-repo.js'
|
||||
import {
|
||||
normalizeWorkOrderAcceptanceMode,
|
||||
normalizeWorkOrderGiftCooldownHours,
|
||||
@@ -306,39 +307,83 @@ export async function updateWorkOrderBasic(
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台修正「用户付款金额」:同时更新 083 派生列与 material_json 来源快照。
|
||||
* 后台修正「用户付款金额」:同时更新 083 派生列、material_json 来源快照、
|
||||
* 最近一次修改痕迹,并同步来源订单 total_amount。
|
||||
*
|
||||
* 只允许待完善(pending_material)阶段;WHERE 里再守一次状态与接单人,
|
||||
* 避免读完到写入之间状态已推进或被打手接走。
|
||||
* 状态与接单人在 WHERE 里再守一次,避免读完到写入之间状态已推进或被打手接走。
|
||||
* 同一来源订单的其它工单不联动(按产品确认:只改当前工单)。
|
||||
*/
|
||||
export async function updateWorkOrderPaymentAmount(input: {
|
||||
workOrderId: number | string
|
||||
amountFen: number
|
||||
materialJson: unknown
|
||||
previousAmountFen: number | null
|
||||
reason: string
|
||||
updatedBy: string
|
||||
allowedStatuses: string[]
|
||||
now: string
|
||||
}): Promise<WorkOrderRow | null> {
|
||||
const result = await query<{ id: number }>(
|
||||
`
|
||||
UPDATE work_orders
|
||||
SET source_order_amount_fen = $1,
|
||||
material_json = $2::jsonb,
|
||||
updated_at = $3
|
||||
WHERE id = $4
|
||||
AND assigned_worker_id IS NULL
|
||||
AND status = ANY($5::text[])
|
||||
RETURNING id
|
||||
`,
|
||||
[
|
||||
input.amountFen,
|
||||
toJsonString(input.materialJson),
|
||||
input.now,
|
||||
Number(input.workOrderId),
|
||||
input.allowedStatuses,
|
||||
],
|
||||
)
|
||||
if (!result.rows[0]) return null
|
||||
return getWorkOrderById(result.rows[0].id)
|
||||
const updatedId = await withTransaction(async (client) => {
|
||||
const result = await client.query<{ id: number; order_id: number | null }>(
|
||||
`
|
||||
UPDATE work_orders
|
||||
SET source_order_amount_fen = $1::bigint,
|
||||
material_json = $2::jsonb,
|
||||
user_payment_amount_previous_fen = $3::bigint,
|
||||
user_payment_amount_updated_at = $4::timestamptz,
|
||||
user_payment_amount_updated_by = $5::text,
|
||||
updated_at = $4::timestamptz
|
||||
WHERE id = $6
|
||||
AND assigned_worker_id IS NULL
|
||||
AND status = ANY($7::text[])
|
||||
AND (
|
||||
sharing_enabled = FALSE
|
||||
OR COALESCE((
|
||||
SELECT SUM(wos.quantity)
|
||||
FROM work_order_shares wos
|
||||
WHERE wos.work_order_id = work_orders.id
|
||||
AND wos.status != 'cancelled'
|
||||
), 0) < sharing_total_quantity
|
||||
)
|
||||
RETURNING id, order_id
|
||||
`,
|
||||
[
|
||||
input.amountFen,
|
||||
toJsonString(input.materialJson),
|
||||
input.previousAmountFen,
|
||||
input.now,
|
||||
input.updatedBy,
|
||||
Number(input.workOrderId),
|
||||
input.allowedStatuses,
|
||||
],
|
||||
)
|
||||
const row = result.rows[0]
|
||||
if (!row) return null
|
||||
|
||||
if (row.order_id) {
|
||||
const orderUpdate = await client.query(
|
||||
'UPDATE orders SET total_amount = $1::bigint, updated_at = $2::timestamptz WHERE id = $3',
|
||||
[input.amountFen, input.now, Number(row.order_id)],
|
||||
)
|
||||
if ((orderUpdate.rowCount || 0) === 0) {
|
||||
throw new Error('work_order_payment_amount_order_missing')
|
||||
}
|
||||
}
|
||||
|
||||
await insertWorkOrderPaymentAmountHistoryWithClient(client, {
|
||||
workOrderId: row.id,
|
||||
previousAmountFen: input.previousAmountFen,
|
||||
amountFen: input.amountFen,
|
||||
reason: input.reason,
|
||||
changedBy: input.updatedBy,
|
||||
changedAt: input.now,
|
||||
})
|
||||
|
||||
return Number(row.id)
|
||||
})
|
||||
|
||||
if (!updatedId) return null
|
||||
return getWorkOrderById(updatedId)
|
||||
}
|
||||
|
||||
export async function updateWorkOrderAdminNote(
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { PoolClient } from 'pg'
|
||||
|
||||
import { query } from '../../db/client.js'
|
||||
|
||||
export type WorkOrderPaymentAmountHistoryRow = {
|
||||
id: number | string
|
||||
work_order_id: number | string
|
||||
previous_amount_fen: number | string | null
|
||||
amount_fen: number | string
|
||||
reason: string
|
||||
changed_by: string
|
||||
changed_at: string
|
||||
}
|
||||
|
||||
/** 按时间倒序读取单个工单的全部付款金额修改记录。 */
|
||||
export async function listWorkOrderPaymentAmountHistory(
|
||||
workOrderId: number | string,
|
||||
): Promise<WorkOrderPaymentAmountHistoryRow[]> {
|
||||
const result = await query<WorkOrderPaymentAmountHistoryRow>(
|
||||
`
|
||||
SELECT id, work_order_id, previous_amount_fen, amount_fen, reason, changed_by, changed_at
|
||||
FROM work_order_payment_amount_history
|
||||
WHERE work_order_id = $1
|
||||
ORDER BY changed_at DESC, id DESC
|
||||
`,
|
||||
[Number(workOrderId)],
|
||||
)
|
||||
return result.rows
|
||||
}
|
||||
|
||||
/** 列表页批量判断哪些工单存在价格历史,避免逐行查询。 */
|
||||
export async function listWorkOrderIdsWithPaymentAmountHistory(
|
||||
workOrderIds: number[],
|
||||
): Promise<Set<number>> {
|
||||
if (workOrderIds.length === 0) return new Set()
|
||||
const result = await query<{ work_order_id: number | string }>(
|
||||
`
|
||||
SELECT DISTINCT work_order_id
|
||||
FROM work_order_payment_amount_history
|
||||
WHERE work_order_id = ANY($1::bigint[])
|
||||
`,
|
||||
[workOrderIds],
|
||||
)
|
||||
return new Set(result.rows.map((row) => Number(row.work_order_id)))
|
||||
}
|
||||
|
||||
/** 与工单金额修改在同一事务内写入,保证金额和历史同时成功或同时回滚。 */
|
||||
export async function insertWorkOrderPaymentAmountHistoryWithClient(
|
||||
client: PoolClient,
|
||||
input: {
|
||||
workOrderId: number | string
|
||||
previousAmountFen: number | null
|
||||
amountFen: number
|
||||
reason: string
|
||||
changedBy: string
|
||||
changedAt: string
|
||||
},
|
||||
): Promise<void> {
|
||||
await client.query(
|
||||
`
|
||||
INSERT INTO work_order_payment_amount_history (
|
||||
work_order_id, previous_amount_fen, amount_fen, reason, changed_by, changed_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6)
|
||||
`,
|
||||
[
|
||||
Number(input.workOrderId),
|
||||
input.previousAmountFen,
|
||||
input.amountFen,
|
||||
input.reason,
|
||||
input.changedBy,
|
||||
input.changedAt,
|
||||
],
|
||||
)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
deleteAdminMockCustomShop,
|
||||
deleteAdminWorkOrder,
|
||||
getAdminWorkOrderEvents,
|
||||
getAdminWorkOrderPaymentAmountHistory,
|
||||
getAdminWorkOrderSharing,
|
||||
listAdminMockCustomShops,
|
||||
listAdminWorkOrderShopNames,
|
||||
@@ -558,6 +559,19 @@ router.get(
|
||||
}),
|
||||
)
|
||||
|
||||
router.get(
|
||||
'/worker-platform/orders/:workOrderId/payment-amount-history',
|
||||
requireAdminPermission('worker_order.view'),
|
||||
createJsonHandler(
|
||||
(req) => getAdminWorkOrderPaymentAmountHistory(String(req.params.workOrderId || '')),
|
||||
{
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取价格历史失败',
|
||||
scope: '[admin/worker-platform/orders/:workOrderId/payment-amount-history]',
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/sharing',
|
||||
requireAdminPermission('worker_order.configure_share'),
|
||||
|
||||
@@ -32,16 +32,19 @@ import {
|
||||
normalizeProblemResolutionAction,
|
||||
normalizeRequirementFieldsFromPayload,
|
||||
resolveRequirementFields,
|
||||
resolveWorkOrderOperationalStatus,
|
||||
} from './mappers.js'
|
||||
import { resolveWorkOrderRealtimeWorkerIds } from './admin-work-order-realtime-service.js'
|
||||
import { getWorkerHallConfig } from './worker-hall-config-service.js'
|
||||
import {
|
||||
USER_PAYMENT_AMOUNT_EDITABLE_STATUSES,
|
||||
USER_PAYMENT_AMOUNT_REASON_MAX_LENGTH,
|
||||
canModifyUserPaymentAmountStatus,
|
||||
mergeUserPaymentAmountIntoMaterial,
|
||||
normalizeUserPaymentAmountFen,
|
||||
resolveDisplayedUserPaymentAmountFen,
|
||||
} from './work-order-payment-amount.js'
|
||||
import { invalidateAdminWorkOrderStatisticsCache } from './admin-work-order-query-service.js'
|
||||
import { invalidateWorkOrderShopOptionsCache } from './work-order-shop-options-cache.js'
|
||||
import { getRequiredWorkOrder } from './worker-session-context-service.js'
|
||||
import { normalizeWorkOrderTimeoutPolicy } from './work-order-timeout-policy.js'
|
||||
@@ -131,8 +134,8 @@ export async function updateAdminWorkOrder(
|
||||
/**
|
||||
* 后台修正「用户付款金额」。
|
||||
*
|
||||
* 只允许「待完善(pending_material)」阶段修改:金额进入未分配/待抢单后已参与
|
||||
* 发单与大厅展示,再改会破坏对账口径。金额按元提交、按分落库;修改会写入
|
||||
* 允许「待完善 / 待分配 / 待抢单」阶段修改;修改会同步来源订单 total_amount,
|
||||
* 并记录最近一次旧价、修改时间、修改人。金额按元提交、按分落库;修改会写入
|
||||
* work_order_events + 后台审计,并同步 material_json.source 快照。
|
||||
*/
|
||||
export async function updateAdminWorkOrderPaymentAmount(
|
||||
@@ -141,8 +144,10 @@ export async function updateAdminWorkOrderPaymentAmount(
|
||||
options: { actorName?: string } = {},
|
||||
) {
|
||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||
if (!canModifyUserPaymentAmountStatus(workOrder.status)) {
|
||||
throw createHttpError('只有待完善的工单可以修改用户付款金额', {
|
||||
// 按运营态判断:拼单母单 rawStatus 仍是 open,但满额后展示为代练中/待验收,不能再改。
|
||||
const operationalStatus = resolveWorkOrderOperationalStatus(workOrder)
|
||||
if (!canModifyUserPaymentAmountStatus(operationalStatus)) {
|
||||
throw createHttpError('只有待完善、待分配或待抢单的工单可以修改用户付款金额', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_payment_amount_status_invalid',
|
||||
})
|
||||
@@ -181,13 +186,27 @@ export async function updateAdminWorkOrderPaymentAmount(
|
||||
}
|
||||
|
||||
const now = nowIso()
|
||||
const updated = await updateWorkOrderPaymentAmount({
|
||||
workOrderId: workOrder.id,
|
||||
amountFen,
|
||||
materialJson: mergeUserPaymentAmountIntoMaterial(workOrder.material_json, amountFen),
|
||||
allowedStatuses: [WORK_ORDER_STATUS.PENDING_MATERIAL],
|
||||
now,
|
||||
})
|
||||
let updated: Awaited<ReturnType<typeof updateWorkOrderPaymentAmount>>
|
||||
try {
|
||||
updated = await updateWorkOrderPaymentAmount({
|
||||
workOrderId: workOrder.id,
|
||||
amountFen,
|
||||
materialJson: mergeUserPaymentAmountIntoMaterial(workOrder.material_json, amountFen),
|
||||
previousAmountFen,
|
||||
reason,
|
||||
updatedBy: options.actorName || '',
|
||||
allowedStatuses: [...USER_PAYMENT_AMOUNT_EDITABLE_STATUSES],
|
||||
now,
|
||||
})
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'work_order_payment_amount_order_missing') {
|
||||
throw createHttpError('来源订单不存在,无法同步用户付款金额', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_payment_amount_order_missing',
|
||||
})
|
||||
}
|
||||
throw error
|
||||
}
|
||||
if (!updated) {
|
||||
throw createHttpError('订单状态已变化,无法修改用户付款金额,请刷新后重试', {
|
||||
statusCode: 409,
|
||||
@@ -195,6 +214,8 @@ export async function updateAdminWorkOrderPaymentAmount(
|
||||
})
|
||||
}
|
||||
|
||||
invalidateAdminWorkOrderStatisticsCache()
|
||||
|
||||
await createWorkOrderEvent({
|
||||
workOrderId: workOrder.id,
|
||||
actorType: 'admin',
|
||||
|
||||
@@ -2,6 +2,8 @@ import {
|
||||
getWorkOrderStatistics,
|
||||
listConsignmentInventoriesByIds,
|
||||
listWorkOrderEventsByOrderId,
|
||||
listWorkOrderIdsWithPaymentAmountHistory,
|
||||
listWorkOrderPaymentAmountHistory,
|
||||
listWorkOrderSharesByOrderIds,
|
||||
listWorkOrders,
|
||||
sumPendingUnfreezeByOrderIds,
|
||||
@@ -43,6 +45,11 @@ import { getRequiredWorkOrder } from './worker-session-context-service.js'
|
||||
const WORK_ORDER_STATISTICS_CACHE_TTL_MS = 3_000
|
||||
const workOrderStatisticsCache = new WorkerListCache(WORK_ORDER_STATISTICS_CACHE_TTL_MS, 32)
|
||||
|
||||
/** 工单金额等影响统计口径的字段修改后,主动逐出短缓存。 */
|
||||
export function invalidateAdminWorkOrderStatisticsCache() {
|
||||
workOrderStatisticsCache.clear()
|
||||
}
|
||||
|
||||
/** 只把真正影响统计结果的筛选字段拼进指纹,避免无关参数造成缓存穿透。 */
|
||||
function buildWorkOrderStatisticsCacheKey(filters: JsonObject): string {
|
||||
return JSON.stringify([
|
||||
@@ -70,6 +77,25 @@ export async function getAdminWorkOrderEvents(workOrderId: number | string) {
|
||||
return { items: await mapWorkOrderEvents(events, { includeWorkerSummary: true }) }
|
||||
}
|
||||
|
||||
/** 后台「价格历史」:只返回用户付款金额的实际修改记录,按时间倒序。 */
|
||||
export async function getAdminWorkOrderPaymentAmountHistory(workOrderId: number | string) {
|
||||
await getRequiredWorkOrder(workOrderId)
|
||||
const rows = await listWorkOrderPaymentAmountHistory(workOrderId)
|
||||
return {
|
||||
items: rows.map((row) => ({
|
||||
id: Number(row.id),
|
||||
previousAmountFen:
|
||||
row.previous_amount_fen === null || row.previous_amount_fen === undefined
|
||||
? null
|
||||
: Number(row.previous_amount_fen),
|
||||
amountFen: Number(row.amount_fen),
|
||||
reason: String(row.reason || '').trim(),
|
||||
changedBy: String(row.changed_by || '').trim(),
|
||||
changedAt: new Date(row.changed_at).toISOString(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台接单工单列表的筛选条件(纯函数,便于单测)。
|
||||
*
|
||||
@@ -154,13 +180,16 @@ export async function listAdminWorkOrders(query: JsonObject = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
const [pendingUnfreezeByOrderId, shares, inventories] = await Promise.all([
|
||||
sumPendingUnfreezeByOrderIds(itemIds),
|
||||
listWorkOrderSharesByOrderIds(itemIds),
|
||||
inventoryIds.size > 0
|
||||
? listConsignmentInventoriesByIds([...inventoryIds])
|
||||
: Promise.resolve([]),
|
||||
])
|
||||
const [pendingUnfreezeByOrderId, shares, inventories, paymentHistoryOrderIds] = await Promise.all(
|
||||
[
|
||||
sumPendingUnfreezeByOrderIds(itemIds),
|
||||
listWorkOrderSharesByOrderIds(itemIds),
|
||||
inventoryIds.size > 0
|
||||
? listConsignmentInventoriesByIds([...inventoryIds])
|
||||
: Promise.resolve([]),
|
||||
listWorkOrderIdsWithPaymentAmountHistory(itemIds),
|
||||
],
|
||||
)
|
||||
|
||||
const inventoriesById = new Map<number, ConsignmentInventoryRow>()
|
||||
for (const inv of inventories) {
|
||||
@@ -181,6 +210,7 @@ export async function listAdminWorkOrders(query: JsonObject = {}) {
|
||||
const inv = invId > 0 ? inventoriesById.get(invId) || null : null
|
||||
return {
|
||||
...mapWorkOrderAdmin(item, sharesByOrderId.get(Number(item.id)) || [], inv),
|
||||
hasPaymentAmountHistory: paymentHistoryOrderIds.has(Number(item.id)),
|
||||
pendingUnfreezeAmount: pendingUnfreezeByOrderId.get(Number(item.id)) || 0,
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -396,6 +396,14 @@ export function mapWorkOrderAdmin(
|
||||
requiredDepositAmount: Number(workOrder.required_deposit_amount || 0),
|
||||
/** 来源订单实付金额(分):取消部分时用于预填退款金额。 */
|
||||
sourceOrderAmount: Number(workOrder.source_order_amount_fen || 0),
|
||||
/** 最近一次修改用户付款金额的痕迹,仅后台可见。 */
|
||||
userPaymentAmountPreviousFen:
|
||||
workOrder.user_payment_amount_previous_fen === null ||
|
||||
workOrder.user_payment_amount_previous_fen === undefined
|
||||
? null
|
||||
: Number(workOrder.user_payment_amount_previous_fen),
|
||||
userPaymentAmountUpdatedAt: workOrder.user_payment_amount_updated_at || null,
|
||||
userPaymentAmountUpdatedBy: String(workOrder.user_payment_amount_updated_by || '').trim(),
|
||||
sharing: {
|
||||
enabled: workOrder.sharing_enabled === true,
|
||||
totalQuantity: Number(workOrder.sharing_total_quantity || 1),
|
||||
@@ -552,6 +560,9 @@ export function mapWorkOrderForWorker(
|
||||
hallAvailable: _hallAvailable,
|
||||
capabilities: _capabilities,
|
||||
adminNote: _adminNote,
|
||||
userPaymentAmountPreviousFen: _userPaymentAmountPreviousFen,
|
||||
userPaymentAmountUpdatedAt: _userPaymentAmountUpdatedAt,
|
||||
userPaymentAmountUpdatedBy: _userPaymentAmountUpdatedBy,
|
||||
...visibleOrder
|
||||
} = mapped
|
||||
const isTimedOutForMe =
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import test from 'node:test'
|
||||
|
||||
import {
|
||||
@@ -22,11 +23,11 @@ test('normalizeUserPaymentAmountFen:0、负数、超上限与非数字都判
|
||||
}
|
||||
})
|
||||
|
||||
test('canModifyUserPaymentAmountStatus:只有待完善(pending_material)可改', () => {
|
||||
assert.equal(canModifyUserPaymentAmountStatus('pending_material'), true)
|
||||
test('canModifyUserPaymentAmountStatus:待完善、待分配、待抢单可改', () => {
|
||||
for (const status of ['pending_material', 'unassigned', 'open']) {
|
||||
assert.equal(canModifyUserPaymentAmountStatus(status), true, `${status} 应允许修改用户付款金额`)
|
||||
}
|
||||
for (const status of [
|
||||
'unassigned',
|
||||
'open',
|
||||
'in_progress',
|
||||
'pending_acceptance',
|
||||
'problem',
|
||||
@@ -146,3 +147,33 @@ test('buildWorkOrderPaymentAmountAuditData:未变化不写审计,变化时
|
||||
|
||||
assert.equal(buildWorkOrderPaymentAmountAuditData(null), null)
|
||||
})
|
||||
|
||||
test('付款金额更新仓储同时写旧价/时间/修改人并同步来源订单金额', async () => {
|
||||
const source = await readFile(
|
||||
new URL('../../repositories/worker-platform/work-order-management-repo.ts', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
assert.ok(source.includes('user_payment_amount_previous_fen = $3::bigint'))
|
||||
assert.ok(source.includes('user_payment_amount_updated_at = $4::timestamptz'))
|
||||
assert.ok(source.includes('user_payment_amount_updated_by = $5::text'))
|
||||
assert.ok(source.includes('UPDATE orders SET total_amount = $1::bigint'))
|
||||
assert.ok(source.includes('status = ANY($7::text[])'))
|
||||
assert.ok(source.includes('assigned_worker_id IS NULL'))
|
||||
assert.ok(source.includes('insertWorkOrderPaymentAmountHistoryWithClient(client'))
|
||||
})
|
||||
|
||||
test('付款金额可修改状态按运营态判断,拼单满额后禁止', async () => {
|
||||
const service = await readFile(
|
||||
new URL('./admin-work-order-management-service.ts', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
assert.ok(service.includes('resolveWorkOrderOperationalStatus(workOrder)'))
|
||||
assert.ok(service.includes('canModifyUserPaymentAmountStatus(operationalStatus)'))
|
||||
|
||||
const repo = await readFile(
|
||||
new URL('../../repositories/worker-platform/work-order-management-repo.ts', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
assert.ok(repo.includes('SUM(wos.quantity)'))
|
||||
assert.ok(repo.includes('sharing_total_quantity'))
|
||||
})
|
||||
|
||||
@@ -30,14 +30,22 @@ export function normalizeUserPaymentAmountFen(value: unknown): number | null {
|
||||
return fen
|
||||
}
|
||||
|
||||
/** 允许后台修正用户付款金额的工单状态。 */
|
||||
export const USER_PAYMENT_AMOUNT_EDITABLE_STATUSES = [
|
||||
WORK_ORDER_STATUS.PENDING_MATERIAL,
|
||||
WORK_ORDER_STATUS.UNASSIGNED,
|
||||
WORK_ORDER_STATUS.OPEN,
|
||||
] as const
|
||||
|
||||
/**
|
||||
* 用户付款金额只允许在「待完善」阶段修改。
|
||||
* 用户付款金额允许在「待完善 / 待分配 / 待抢单」阶段修改。
|
||||
*
|
||||
* 进入未分配/待抢单后金额已经参与发单与大厅展示,后台再改会破坏对账口径;
|
||||
* 已接单/已验收阶段更不允许改动。
|
||||
* 待分配、待抢单也会允许修改,修改会同步来源订单金额并保留修改痕迹;
|
||||
* 已接单/已验收/已取消阶段仍然不允许改动。
|
||||
*/
|
||||
export function canModifyUserPaymentAmountStatus(status: unknown): boolean {
|
||||
return String(status || '').trim() === WORK_ORDER_STATUS.PENDING_MATERIAL
|
||||
const normalized = String(status || '').trim()
|
||||
return (USER_PAYMENT_AMOUNT_EDITABLE_STATUSES as readonly string[]).includes(normalized)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import test from 'node:test'
|
||||
|
||||
function read(relativeUrl: string) {
|
||||
return readFile(new URL(relativeUrl, import.meta.url), 'utf8')
|
||||
}
|
||||
|
||||
test('价格历史迁移建表、建索引并回填 payment_amount_updated 事件', async () => {
|
||||
const sql = await read('../../db/migrations/121_work_order_payment_amount_history.sql')
|
||||
assert.ok(sql.includes('CREATE TABLE IF NOT EXISTS work_order_payment_amount_history'))
|
||||
assert.ok(sql.includes('idx_work_order_payment_amount_history_order'))
|
||||
assert.ok(sql.includes("e.event_type = 'payment_amount_updated'"))
|
||||
assert.ok(sql.includes("e.payload_json->>'amountFen' ~ '^-?[0-9]+$'"))
|
||||
assert.ok(sql.includes('WHERE NOT EXISTS'))
|
||||
})
|
||||
|
||||
test('价格历史仓储按时间倒序读取、批量判断并写入', async () => {
|
||||
const source = await read('../../repositories/worker-platform/work-order-payment-history-repo.ts')
|
||||
assert.ok(source.includes('ORDER BY changed_at DESC, id DESC'))
|
||||
assert.ok(source.includes('SELECT DISTINCT work_order_id'))
|
||||
assert.ok(source.includes('INSERT INTO work_order_payment_amount_history'))
|
||||
})
|
||||
|
||||
test('后台列表返回是否有价格历史,并注册历史查询接口', async () => {
|
||||
const service = await read('./admin-work-order-query-service.ts')
|
||||
assert.ok(service.includes('hasPaymentAmountHistory: paymentHistoryOrderIds.has'))
|
||||
assert.ok(service.includes('getAdminWorkOrderPaymentAmountHistory'))
|
||||
|
||||
const route = await read('../../routes/admin/worker-platform/orders.ts')
|
||||
assert.ok(route.includes("'/worker-platform/orders/:workOrderId/payment-amount-history'"))
|
||||
})
|
||||
@@ -1,9 +1,28 @@
|
||||
import { EditOutlined } from '@ant-design/icons'
|
||||
import { App, Input, InputNumber, Modal, Space, Tag, Typography } from 'antd'
|
||||
import { EditOutlined, HistoryOutlined } from '@ant-design/icons'
|
||||
import {
|
||||
App,
|
||||
Drawer,
|
||||
Empty,
|
||||
Input,
|
||||
InputNumber,
|
||||
List,
|
||||
Modal,
|
||||
Space,
|
||||
Spin,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useRef, useState } from 'react'
|
||||
|
||||
import { updateAdminWorkOrder, updateAdminWorkOrderPaymentAmount } from '@/services/admin'
|
||||
import {
|
||||
fetchAdminWorkOrderPaymentAmountHistory,
|
||||
updateAdminWorkOrder,
|
||||
updateAdminWorkOrderPaymentAmount,
|
||||
} from '@/services/admin'
|
||||
import type { WorkOrder } from '@/types/worker-platform'
|
||||
import { formatDateTime } from '@/utils/date-time'
|
||||
import { appendProductNameSourceSuffix } from '@/utils/product-name-source-suffix'
|
||||
|
||||
import { formatMoney } from './shared'
|
||||
@@ -111,13 +130,37 @@ export function UserPaymentAmountEditableCell({
|
||||
onSaved: () => Promise<void> | void
|
||||
}) {
|
||||
const { message } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const editable = canEditUserPaymentAmount(order)
|
||||
const currentFen = getUserPaymentFen(order)
|
||||
const lastModifiedText = order.userPaymentAmountUpdatedAt
|
||||
? `上次修改:${
|
||||
order.userPaymentAmountPreviousFen === null ||
|
||||
order.userPaymentAmountPreviousFen === undefined
|
||||
? '-'
|
||||
: formatMoney(order.userPaymentAmountPreviousFen)
|
||||
} → ${currentFen === null ? '-' : formatMoney(currentFen)} · ${formatDateTime(
|
||||
order.userPaymentAmountUpdatedAt,
|
||||
)}${order.userPaymentAmountUpdatedBy ? ` · ${order.userPaymentAmountUpdatedBy}` : ''}`
|
||||
: ''
|
||||
// 三个可编辑状态共用同一套弹窗;没有修改记录时也保留占位,避免高度和内容随状态变化。
|
||||
const lastModifiedDisplay = lastModifiedText || '上次修改:暂无记录'
|
||||
const cellTitle = [editable ? '点击修改用户付款金额' : '', lastModifiedText]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
const [open, setOpen] = useState(false)
|
||||
const [draft, setDraft] = useState<number | null>(null)
|
||||
const [reason, setReason] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
const savingRef = useRef(false)
|
||||
const [historyOpen, setHistoryOpen] = useState(false)
|
||||
const historyQuery = useQuery({
|
||||
queryKey: ['work-order-payment-history', order.workOrderId],
|
||||
queryFn: () => fetchAdminWorkOrderPaymentAmountHistory(order.workOrderId),
|
||||
enabled: historyOpen,
|
||||
retry: false,
|
||||
})
|
||||
const historyItems = historyQuery.data?.data?.items || []
|
||||
|
||||
function openEditor() {
|
||||
if (!editable || savingRef.current) return
|
||||
@@ -152,6 +195,9 @@ export function UserPaymentAmountEditableCell({
|
||||
})
|
||||
message.success('用户付款金额已更新')
|
||||
setOpen(false)
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['work-order-payment-history', order.workOrderId],
|
||||
})
|
||||
await onSaved()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '保存失败')
|
||||
@@ -164,7 +210,6 @@ export function UserPaymentAmountEditableCell({
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
title={editable ? '点击修改用户付款金额' : undefined}
|
||||
style={{ cursor: editable ? 'pointer' : 'default', whiteSpace: 'nowrap' }}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
@@ -172,10 +217,24 @@ export function UserPaymentAmountEditableCell({
|
||||
}}
|
||||
>
|
||||
<Space size={4} align="center">
|
||||
<Typography.Text style={editable ? { color: 'rgba(0,0,0,0.65)' } : undefined}>
|
||||
<Typography.Text
|
||||
title={cellTitle || undefined}
|
||||
style={editable ? { color: 'rgba(0,0,0,0.65)' } : undefined}
|
||||
>
|
||||
{currentFen === null ? '-' : formatMoney(currentFen)}
|
||||
</Typography.Text>
|
||||
{editable ? <EditOutlined style={{ color: 'rgba(0,0,0,0.35)', fontSize: 12 }} /> : null}
|
||||
{order.hasPaymentAmountHistory ? (
|
||||
<Tooltip title="查看价格历史" placement="topLeft">
|
||||
<HistoryOutlined
|
||||
style={{ color: 'rgba(0,0,0,0.35)', fontSize: 12, cursor: 'pointer' }}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
setHistoryOpen(true)
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
@@ -197,6 +256,9 @@ export function UserPaymentAmountEditableCell({
|
||||
<Typography.Text type="secondary">
|
||||
当前金额:{currentFen === null ? '-' : formatMoney(currentFen)}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{lastModifiedDisplay}
|
||||
</Typography.Text>
|
||||
<InputNumber
|
||||
autoFocus
|
||||
min={0.01}
|
||||
@@ -219,10 +281,67 @@ export function UserPaymentAmountEditableCell({
|
||||
onChange={(event) => setReason(event.target.value)}
|
||||
/>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
修改会记录到工单流转记录与后台审计日志;财务统计按下一次定时同步更新。
|
||||
修改会同步来源订单金额,并记录到工单流转记录与后台审计日志;财务统计会同步更新,不会改变发单金额或已加入份额的报酬。
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Drawer
|
||||
title="价格历史记录"
|
||||
open={historyOpen}
|
||||
width={480}
|
||||
destroyOnHidden
|
||||
onClose={() => setHistoryOpen(false)}
|
||||
>
|
||||
<div style={{ display: 'grid', gap: 16 }}>
|
||||
<Typography.Text type="secondary">
|
||||
工单 {order.workOrderNo} · 共 {historyItems.length} 次修改
|
||||
</Typography.Text>
|
||||
{historyQuery.isLoading ? (
|
||||
<Spin />
|
||||
) : historyQuery.error ? (
|
||||
<Typography.Text type="secondary">
|
||||
{historyQuery.error instanceof Error
|
||||
? historyQuery.error.message
|
||||
: '读取价格历史失败'}
|
||||
</Typography.Text>
|
||||
) : historyItems.length === 0 ? (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无价格历史" />
|
||||
) : (
|
||||
<List
|
||||
dataSource={historyItems}
|
||||
renderItem={(item, index) => (
|
||||
<List.Item style={{ display: 'block', paddingInline: 0 }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<Typography.Text strong>
|
||||
{item.previousAmountFen === null
|
||||
? `- → ${formatMoney(item.amountFen)}`
|
||||
: `${formatMoney(item.previousAmountFen)} → ${formatMoney(item.amountFen)}`}
|
||||
</Typography.Text>
|
||||
{index === 0 ? <Tag color="gold">最近一次</Tag> : null}
|
||||
</div>
|
||||
<Typography.Text
|
||||
type="secondary"
|
||||
style={{ display: 'block', marginTop: 4, fontSize: 12 }}
|
||||
>
|
||||
{formatDateTime(item.changedAt)} · {item.changedBy || '未知'}
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
原因:{item.reason || '未填写'}
|
||||
</Typography.Text>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Drawer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1760,7 +1760,7 @@ export default function WorkOrdersPanel() {
|
||||
},
|
||||
{
|
||||
title: '用户付款金额',
|
||||
width: 130,
|
||||
width: 160,
|
||||
render: (_, row) => <UserPaymentAmountEditableCell order={row} onSaved={refreshAll} />,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -33,9 +33,13 @@ export function getUserPaymentFen(order: PaymentSourceOrder): number | null {
|
||||
return fromColumn !== null && fromColumn > 0 ? fromColumn : null
|
||||
}
|
||||
|
||||
/** 用户付款金额仅在「待完善」阶段可编辑,与后端校验同口径。 */
|
||||
const USER_PAYMENT_AMOUNT_EDITABLE_STATUSES = new Set(['pending_material', 'unassigned', 'open'])
|
||||
|
||||
/** 用户付款金额在「待完善 / 待分配 / 待抢单」可编辑,与后端校验同口径。 */
|
||||
export function canEditUserPaymentAmount(order: { status?: string; rawStatus?: string }): boolean {
|
||||
return String(order.rawStatus || order.status || '').trim() === 'pending_material'
|
||||
// 运营态优先:拼单母单 rawStatus 仍是 open,但满额后界面展示为代练中/待验收,不能继续改。
|
||||
const effectiveStatus = String(order.status || order.rawStatus || '').trim()
|
||||
return USER_PAYMENT_AMOUNT_EDITABLE_STATUSES.has(effectiveStatus)
|
||||
}
|
||||
|
||||
/** 发单金额高于用户付款时返回两边金额,否则 null(付款未知时不提示) */
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
WorkCategory,
|
||||
WorkOrder,
|
||||
WorkOrderEvent,
|
||||
WorkOrderPaymentAmountHistoryItem,
|
||||
WorkOrderShare,
|
||||
WorkOrderStatistics,
|
||||
WorkProductRule,
|
||||
@@ -726,6 +727,12 @@ export function fetchAdminWorkOrderEvents(workOrderId: number) {
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminWorkOrderPaymentAmountHistory(workOrderId: number) {
|
||||
return apiGet<{ items: WorkOrderPaymentAmountHistoryItem[] }>(
|
||||
`/api/v1/admin/worker-platform/orders/${workOrderId}/payment-amount-history`,
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminMockCustomShops() {
|
||||
return apiGet<{ items: string[] }>('/api/v1/admin/worker-platform/orders/mock/custom-shops')
|
||||
}
|
||||
|
||||
@@ -684,6 +684,14 @@ export type WorkOrder = {
|
||||
rewardAmount: number
|
||||
/** 来源订单实付金额(分):取消部分时用于预填退款金额。 */
|
||||
sourceOrderAmount?: number
|
||||
/** 最近一次修改用户付款金额前的金额(分);NULL 表示修改前金额未知,仅后台可见。 */
|
||||
userPaymentAmountPreviousFen?: number | null
|
||||
/** 最近一次修改用户付款金额的时间,仅后台可见。 */
|
||||
userPaymentAmountUpdatedAt?: string | null
|
||||
/** 最近一次修改用户付款金额的操作人展示名/用户名快照,仅后台可见。 */
|
||||
userPaymentAmountUpdatedBy?: string
|
||||
/** 是否存在用户付款金额修改历史,仅后台列表返回;用于显示「价格历史」入口。 */
|
||||
hasPaymentAmountHistory?: boolean
|
||||
/** 当前打手生效的免押额度,仅打手端订单数据返回。 */
|
||||
depositFreeAmount?: number
|
||||
requiredDepositAmount?: number
|
||||
@@ -771,6 +779,16 @@ export type WorkOrderStatistics = {
|
||||
statusCounts: Record<string, number>
|
||||
}
|
||||
|
||||
/** 用户付款金额的一次修改记录,仅后台可见。 */
|
||||
export type WorkOrderPaymentAmountHistoryItem = {
|
||||
id: number
|
||||
previousAmountFen: number | null
|
||||
amountFen: number
|
||||
reason: string
|
||||
changedBy: string
|
||||
changedAt: string
|
||||
}
|
||||
|
||||
/** 工单流转事件(订单详情中的操作日志) */
|
||||
export type WorkOrderEvent = {
|
||||
id: number
|
||||
|
||||
@@ -318,7 +318,16 @@ export function patchAdminWorkOrderListItems(
|
||||
)
|
||||
if (index >= 0) {
|
||||
previousRow = items[index]
|
||||
nextItems = items.map((item, itemIndex) => (itemIndex === index ? snapshot : item))
|
||||
nextItems = items.map((item, itemIndex) =>
|
||||
itemIndex === index
|
||||
? {
|
||||
...(snapshot as Record<string, unknown>),
|
||||
// 列表专用派生字段不在实时快照里,沿用旧值,避免价格历史入口消失。
|
||||
hasPaymentAmountHistory:
|
||||
(item as { hasPaymentAmountHistory?: boolean }).hasPaymentAmountHistory === true,
|
||||
}
|
||||
: item,
|
||||
)
|
||||
} else if (options.insert) {
|
||||
nextItems = [snapshot, ...items]
|
||||
} else {
|
||||
|
||||
@@ -287,6 +287,20 @@ test('已存在的行只做原地替换,不改变位置', () => {
|
||||
assert.equal(totalOf(patched), 124, '行数没变,总数不能变')
|
||||
})
|
||||
|
||||
test('实时替换行时保留价格历史标记,避免入口消失', () => {
|
||||
const existing = {
|
||||
...buildSnapshot({ workOrderId: 100 }),
|
||||
hasPaymentAmountHistory: true,
|
||||
}
|
||||
const patched = patchAdminWorkOrderListItems(
|
||||
buildEnvelope([existing]),
|
||||
buildSnapshot({ workOrderId: 100, status: 'in_progress' }),
|
||||
{ remove: false, insert: false },
|
||||
)
|
||||
const row = itemsOfWorkOrders(patched)[0] as { hasPaymentAmountHistory?: boolean }
|
||||
assert.equal(row.hasPaymentAmountHistory, true)
|
||||
})
|
||||
|
||||
test('状态筛选:已不命中的工单会被移除并扣减总数', () => {
|
||||
const filters = parseAdminWorkOrderListFilters(buildQueryKey({ statuses: 'open' }))!
|
||||
const accepted = buildSnapshot({ workOrderId: 100, status: 'accepted' })
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import test from 'node:test'
|
||||
|
||||
test('价格历史入口只在 hasPaymentAmountHistory 为真时显示,并调用专用接口', async () => {
|
||||
const cell = await readFile(
|
||||
new URL('../src/pages/admin/panels/WorkOrderTableCells.tsx', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
assert.ok(cell.includes('order.hasPaymentAmountHistory ?'))
|
||||
assert.ok(cell.includes('fetchAdminWorkOrderPaymentAmountHistory'))
|
||||
assert.ok(cell.includes('价格历史记录'))
|
||||
|
||||
const service = await readFile(
|
||||
new URL('../src/services/admin/worker-platform.ts', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
assert.ok(service.includes('/payment-amount-history'))
|
||||
})
|
||||
@@ -64,22 +64,16 @@ test('getUserPaymentFen:空值或非法值不误判成 0 元', () => {
|
||||
assert.equal(getUserPaymentFen({ material: {}, sourceOrderAmount: 0 }), null)
|
||||
})
|
||||
|
||||
test('用户付款金额仅在待完善(pending_material)可编辑', () => {
|
||||
assert.equal(canEditUserPaymentAmount({ status: 'pending_material' }), true)
|
||||
assert.equal(
|
||||
canEditUserPaymentAmount({ status: 'pending_material', rawStatus: 'pending_material' }),
|
||||
true,
|
||||
)
|
||||
test('用户付款金额在待完善、待分配、待抢单可编辑', () => {
|
||||
for (const status of ['pending_material', 'unassigned', 'open']) {
|
||||
assert.equal(
|
||||
canEditUserPaymentAmount({ status, rawStatus: status }),
|
||||
true,
|
||||
`${status} 应允许编辑用户付款金额`,
|
||||
)
|
||||
}
|
||||
|
||||
for (const status of [
|
||||
'unassigned',
|
||||
'open',
|
||||
'in_progress',
|
||||
'pending_acceptance',
|
||||
'problem',
|
||||
'accepted',
|
||||
'cancelled',
|
||||
]) {
|
||||
for (const status of ['in_progress', 'pending_acceptance', 'problem', 'accepted', 'cancelled']) {
|
||||
assert.equal(
|
||||
canEditUserPaymentAmount({ status, rawStatus: status }),
|
||||
false,
|
||||
@@ -87,7 +81,11 @@ test('用户付款金额仅在待完善(pending_material)可编辑', () => {
|
||||
)
|
||||
}
|
||||
|
||||
// rawStatus 是后端原始状态,优先级高于运营态 status
|
||||
assert.equal(canEditUserPaymentAmount({ status: 'open', rawStatus: 'pending_material' }), true)
|
||||
assert.equal(canEditUserPaymentAmount({ status: 'pending_material', rawStatus: 'open' }), false)
|
||||
// 运营态优先:拼单母单 rawStatus 仍是 open,但满额后界面显示代练中/待验收,不能再改
|
||||
assert.equal(canEditUserPaymentAmount({ status: 'in_progress', rawStatus: 'open' }), false)
|
||||
assert.equal(canEditUserPaymentAmount({ status: 'pending_acceptance', rawStatus: 'open' }), false)
|
||||
// 未满额时运营态仍是 open,继续允许
|
||||
assert.equal(canEditUserPaymentAmount({ status: 'open', rawStatus: 'open' }), true)
|
||||
// 老数据没有运营态时回退 rawStatus
|
||||
assert.equal(canEditUserPaymentAmount({ status: '', rawStatus: 'pending_material' }), true)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user