feat: 支持待分配/待抢单修改付款金额并增加价格历史

This commit is contained in:
2026-11-03 10:31:46 +08:00
parent 232ef6ce79
commit 96676841b5
22 changed files with 626 additions and 78 deletions
@@ -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'"))
})