feat:拼单明细管理加一个撤份额的功能,指定数量
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
-- 107_work_order_share_cancel_note.sql
|
||||
-- 目标:后台「拼单明细」列表要能直接看到撤销原因。
|
||||
-- 1. cancelled_reason 仍保留枚举口径(admin/timeout/worker_request/problem/settlement),
|
||||
-- 用于状态标签与既有逻辑判断,不塞自由文本;
|
||||
-- 2. cancel_note 存运营在「单人撤单 / 撤份额」弹窗里填写的原因(选填),只做展示;
|
||||
-- 3. 未填写时保持空串,列表显示为「-」;历史数据不做回填(原因只存在于事件流里)。
|
||||
|
||||
ALTER TABLE work_order_shares
|
||||
ADD COLUMN IF NOT EXISTS cancel_note TEXT NOT NULL DEFAULT '';
|
||||
|
||||
COMMENT ON COLUMN work_order_shares.cancel_note IS '后台撤单/撤份额时填写的原因(自由文本,选填),仅用于展示';
|
||||
@@ -0,0 +1,12 @@
|
||||
-- 108_work_order_share_cancel_quantity.sql
|
||||
-- 目标:拼单明细的「撤销原因」要带上撤销份数(列表显示为「撤 4 份 · 原因」)。
|
||||
-- 1. cancel_quantity 记录这一份份额被撤掉的份数:
|
||||
-- - 撤份额:每次累加 reduceQuantity(同一份额多次撤则累计);
|
||||
-- - 单人撤单:等于撤销时该份额的全部份数;
|
||||
-- 2. 0 = 从未撤过;超时 / 打手退单 / 问题单处置不写这一列,原因仍由状态标签表达;
|
||||
-- 3. 不做历史回填:改动前的撤单没有份数记录,列表保持「-」。
|
||||
|
||||
ALTER TABLE work_order_shares
|
||||
ADD COLUMN IF NOT EXISTS cancel_quantity INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
COMMENT ON COLUMN work_order_shares.cancel_quantity IS '被撤掉的份数(撤份额累计 / 单人撤单为整份份数;0 = 未撤过),仅用于展示';
|
||||
@@ -0,0 +1,51 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { resolveShareReduceTargetQuantity } from './work-order-share-quantity.js'
|
||||
|
||||
test('按份数撤份额:正常扣减', () => {
|
||||
assert.deepEqual(resolveShareReduceTargetQuantity({ currentQuantity: 9, reduceQuantity: 4 }), {
|
||||
nextQuantity: 5,
|
||||
failureReason: null,
|
||||
})
|
||||
assert.deepEqual(resolveShareReduceTargetQuantity({ currentQuantity: 2, reduceQuantity: 1 }), {
|
||||
nextQuantity: 1,
|
||||
failureReason: null,
|
||||
})
|
||||
})
|
||||
|
||||
test('按份数撤份额:不能撤光、不能撤 0、不能超量', () => {
|
||||
assert.equal(
|
||||
resolveShareReduceTargetQuantity({ currentQuantity: 1, reduceQuantity: 1 }).failureReason,
|
||||
'invalid_quantity',
|
||||
)
|
||||
assert.equal(
|
||||
resolveShareReduceTargetQuantity({ currentQuantity: 9, reduceQuantity: 0 }).failureReason,
|
||||
'invalid_quantity',
|
||||
)
|
||||
assert.equal(
|
||||
resolveShareReduceTargetQuantity({ currentQuantity: 9, reduceQuantity: -2 }).failureReason,
|
||||
'invalid_quantity',
|
||||
)
|
||||
assert.equal(
|
||||
resolveShareReduceTargetQuantity({ currentQuantity: 9, reduceQuantity: 9 }).failureReason,
|
||||
'invalid_quantity',
|
||||
)
|
||||
assert.equal(
|
||||
resolveShareReduceTargetQuantity({ currentQuantity: 9, reduceQuantity: 10 }).failureReason,
|
||||
'invalid_quantity',
|
||||
)
|
||||
})
|
||||
|
||||
test('按份数撤份额:非法输入(非数字/小数/空)一律拒绝', () => {
|
||||
assert.equal(
|
||||
resolveShareReduceTargetQuantity({ currentQuantity: Number.NaN, reduceQuantity: 4 })
|
||||
.failureReason,
|
||||
'invalid_quantity',
|
||||
)
|
||||
assert.equal(
|
||||
resolveShareReduceTargetQuantity({ currentQuantity: 9, reduceQuantity: 4.7 }).nextQuantity,
|
||||
5,
|
||||
'小数向下取整后按 4 份处理',
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 后台按份数撤销拼单份额:校验并算出保留下来的份数。
|
||||
*
|
||||
* 口径(已确认):
|
||||
* - 只能部分撤销,撤到 0 份走「单人撤单」,所以 `1 ≤ 撤掉份数 ≤ 当前份数 − 1`;
|
||||
* - 押金不随份数减少而退还(保持原冻结额),这里只算数量。
|
||||
*/
|
||||
export function resolveShareReduceTargetQuantity(input: {
|
||||
currentQuantity: number
|
||||
reduceQuantity: number
|
||||
}): { nextQuantity: number; failureReason: 'invalid_quantity' | null } {
|
||||
const current = Math.floor(Number(input.currentQuantity) || 0)
|
||||
const reduce = Math.floor(Number(input.reduceQuantity) || 0)
|
||||
if (!Number.isFinite(current) || !Number.isFinite(reduce)) {
|
||||
return { nextQuantity: 0, failureReason: 'invalid_quantity' }
|
||||
}
|
||||
if (current < 2 || reduce < 1 || reduce > current - 1) {
|
||||
return { nextQuantity: 0, failureReason: 'invalid_quantity' }
|
||||
}
|
||||
return { nextQuantity: current - reduce, failureReason: null }
|
||||
}
|
||||
@@ -365,6 +365,10 @@ export type WorkOrderShareRow = {
|
||||
timeout_policy?: string
|
||||
/** 取消来源:timeout/worker_request/admin/problem/settlement */
|
||||
cancelled_reason?: string
|
||||
/** 后台撤单/撤份额时填写的原因(自由文本,选填),仅用于列表展示 */
|
||||
cancel_note?: string
|
||||
/** 被撤掉的份数(撤份额累计 / 单人撤单为整份份数;0 = 未撤过) */
|
||||
cancel_quantity?: number
|
||||
/** 份额级问题单:备注 / 标记时间(NULL = 正常)/ 操作人;不影响母单状态 */
|
||||
problem_note?: string
|
||||
problem_marked_at?: string | null
|
||||
|
||||
@@ -817,13 +817,15 @@ export async function cancelWorkOrderShare(input: {
|
||||
`UPDATE work_order_shares
|
||||
SET status = 'cancelled',
|
||||
cancelled_reason = 'admin',
|
||||
cancel_note = $2,
|
||||
cancel_quantity = COALESCE(quantity, 0),
|
||||
acceptance_json = '{}'::jsonb,
|
||||
draft_acceptance_json = '{}'::jsonb,
|
||||
submitted_at = NULL,
|
||||
accepted_at = NULL,
|
||||
updated_at = $1
|
||||
WHERE id = $2`,
|
||||
[input.now, input.shareId],
|
||||
WHERE id = $3`,
|
||||
[input.now, String(input.reason || '').trim(), input.shareId],
|
||||
)
|
||||
// 母单状态保持不变:这里只取消这一份份额,不再按剩余份额重推母单状态。
|
||||
// (旧实现会 recompute:待验收的母单撤掉一份后 allocated < total → 变回 open,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PoolClient } from 'pg'
|
||||
|
||||
import { query, withTransaction } from '../../db/client.js'
|
||||
import { resolveShareReduceTargetQuantity } from '../../domain/work-order-share-quantity.js'
|
||||
import {
|
||||
ensureWorkerWalletWithClient,
|
||||
getWorkerWalletWithClient,
|
||||
@@ -11,6 +12,7 @@ import { createWorkOrderEventWithClient } from './work-order-event-repo.js'
|
||||
import {
|
||||
countWorkerActiveOrdersWithClient,
|
||||
getWorkOrderById,
|
||||
getWorkOrderByIdWithClient,
|
||||
isWorkOrderWithinHallCapacityWithClient,
|
||||
} from './work-order-repo.js'
|
||||
import type { WorkOrderRow, WorkOrderShareRow } from './types.js'
|
||||
@@ -414,6 +416,123 @@ export async function joinWorkOrderShare(input: {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台按份数撤销某个打手的部分份额。
|
||||
*
|
||||
* 只改这一行的数量与份额报酬:
|
||||
* - 大厅余量 = 总份数 − Σ 有效份额数量,所以减少的份数会自动回到大厅;
|
||||
* - 母单状态不动(与「单人撤单」同口径);
|
||||
* - 押金按要求保持原冻结额,不做退还;
|
||||
* - 验收资料(验收图/视频/备注)留在原份额上,不清空。
|
||||
*/
|
||||
export async function reduceWorkOrderShareQuantity(input: {
|
||||
workOrderId: number
|
||||
shareId: number
|
||||
reduceQuantity: number
|
||||
note?: string
|
||||
actorName?: string
|
||||
now: string
|
||||
}): Promise<{
|
||||
order: WorkOrderRow | null
|
||||
share: WorkOrderShareRow | null
|
||||
failureReason:
|
||||
| 'work_order_not_reducible'
|
||||
| 'share_not_found'
|
||||
| 'share_status_not_reducible'
|
||||
| 'share_quantity_invalid'
|
||||
| null
|
||||
}> {
|
||||
return withTransaction(async (client) => {
|
||||
const orderResult = await client.query<WorkOrderRow>(
|
||||
`SELECT * FROM work_orders WHERE id = $1 FOR UPDATE`,
|
||||
[input.workOrderId],
|
||||
)
|
||||
const workOrder = orderResult.rows[0] || null
|
||||
if (
|
||||
!workOrder ||
|
||||
!['open', 'in_progress', 'pending_acceptance'].includes(String(workOrder.status || ''))
|
||||
) {
|
||||
return { order: workOrder, share: null, failureReason: 'work_order_not_reducible' as const }
|
||||
}
|
||||
|
||||
const shareResult = await client.query<WorkOrderShareRow>(
|
||||
`SELECT * FROM work_order_shares WHERE id = $1 AND work_order_id = $2 FOR UPDATE`,
|
||||
[input.shareId, input.workOrderId],
|
||||
)
|
||||
const share = shareResult.rows[0] || null
|
||||
if (!share) {
|
||||
return { order: workOrder, share: null, failureReason: 'share_not_found' as const }
|
||||
}
|
||||
if (!['joined', 'submitted'].includes(String(share.status || ''))) {
|
||||
return { order: workOrder, share, failureReason: 'share_status_not_reducible' as const }
|
||||
}
|
||||
|
||||
const target = resolveShareReduceTargetQuantity({
|
||||
currentQuantity: Number(share.quantity || 0),
|
||||
reduceQuantity: input.reduceQuantity,
|
||||
})
|
||||
if (target.failureReason || target.nextQuantity <= 0) {
|
||||
return { order: workOrder, share, failureReason: 'share_quantity_invalid' as const }
|
||||
}
|
||||
|
||||
const unitReward = toPositiveInteger(share.unit_reward, 0)
|
||||
const nextRewardAmount = target.nextQuantity * unitReward
|
||||
// 原因只做展示:本次没填就保留上一次填的,避免后续撤份额把已有说明覆盖成空
|
||||
const nextCancelNote = String(input.note || '').trim() || String(share.cancel_note || '')
|
||||
// 撤销份数累计:同一份额多次撤份额时,列表显示总共撤了多少份
|
||||
const nextCancelQuantity =
|
||||
Math.max(0, Number(share.cancel_quantity || 0)) + input.reduceQuantity
|
||||
await client.query(
|
||||
`UPDATE work_order_shares
|
||||
SET quantity = $1, share_reward = $2, cancel_note = $3,
|
||||
cancel_quantity = $4, updated_at = $5
|
||||
WHERE id = $6`,
|
||||
[
|
||||
target.nextQuantity,
|
||||
nextRewardAmount,
|
||||
nextCancelNote,
|
||||
nextCancelQuantity,
|
||||
input.now,
|
||||
input.shareId,
|
||||
],
|
||||
)
|
||||
|
||||
await createWorkOrderEventWithClient(client, {
|
||||
workOrderId: input.workOrderId,
|
||||
actorType: 'admin',
|
||||
actorId: input.actorName || '',
|
||||
eventType: 'sharing_share_quantity_reduced',
|
||||
fromStatus: workOrder.status,
|
||||
toStatus: workOrder.status,
|
||||
payloadJson: JSON.stringify({
|
||||
shareId: input.shareId,
|
||||
workerId: Number(share.worker_id || 0),
|
||||
fromQuantity: Number(share.quantity || 0),
|
||||
toQuantity: target.nextQuantity,
|
||||
reduceQuantity: input.reduceQuantity,
|
||||
rewardBefore: Number(share.share_reward || 0),
|
||||
rewardAfter: nextRewardAmount,
|
||||
// 押金口径:本次保持原冻结额,不退不补
|
||||
shareDeposit: Number(share.share_deposit || 0),
|
||||
note: String(input.note || '').trim(),
|
||||
}),
|
||||
now: input.now,
|
||||
})
|
||||
|
||||
const updatedShare = await client.query<WorkOrderShareRow>(
|
||||
`${WORK_ORDER_SHARE_SELECT} WHERE wos.id = $1 LIMIT 1`,
|
||||
[input.shareId],
|
||||
)
|
||||
// 必须回读聚合口径的母单(active_share_quantity 等),否则前端拿到的拼单进度会归零
|
||||
const updatedOrder = await getWorkOrderByIdWithClient(client, input.workOrderId)
|
||||
return {
|
||||
order: updatedOrder || workOrder,
|
||||
share: updatedShare.rows[0] || null,
|
||||
failureReason: null,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function submitWorkOrderShareAcceptance(input: {
|
||||
workOrderId: number
|
||||
workerId: number
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
deductAdminWorkOrderPendingDeposit,
|
||||
markAdminWorkOrderProblem,
|
||||
markAdminWorkOrderShareProblem,
|
||||
reduceAdminWorkOrderShareQuantity,
|
||||
resolveAdminProblemWorkOrder,
|
||||
disposeAdminWorkOrderShareProblem,
|
||||
} from '../../../services/worker-platform/index.js'
|
||||
@@ -163,6 +164,32 @@ router.post(
|
||||
),
|
||||
)
|
||||
|
||||
// 按份数撤销部分份额:减少的份数自动回到大厅,押金保持原冻结额。
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/shares/:shareId/reduce-quantity',
|
||||
requireAdminPermission('worker_order.cancel_share'),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
reduceAdminWorkOrderShareQuantity(
|
||||
String(req.params.workOrderId || ''),
|
||||
String(req.params.shareId || ''),
|
||||
req.body || {},
|
||||
req.adminSession?.displayName || req.adminSession?.username || '',
|
||||
),
|
||||
{
|
||||
successMessage: '已撤销指定份数,剩余份数继续履约',
|
||||
errorMessage: '撤销拼单份额失败',
|
||||
scope: '[admin/worker-platform/orders/:workOrderId/shares/:shareId/reduce-quantity]',
|
||||
audit: (req, data) => ({
|
||||
action: 'work_order_share_quantity_reduced',
|
||||
targetType: 'work_order_share',
|
||||
targetId: String(req.params.shareId || ''),
|
||||
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
|
||||
}),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
// 份额级问题单:只影响该份额持有人,母单状态不变。
|
||||
router.post(
|
||||
'/worker-platform/orders/:workOrderId/shares/:shareId/problem',
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
cancelWorkOrderShare,
|
||||
cancelWorkOrderSharing,
|
||||
countWorkOrderPendingSharingSubmissions,
|
||||
reduceWorkOrderShareQuantity,
|
||||
updateWorkOrder,
|
||||
} from '../../repositories/worker-platform/index.js'
|
||||
import type { JsonObject } from '../../types/json.js'
|
||||
@@ -189,6 +190,70 @@ export async function cancelAdminWorkOrderShare(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台按份数撤销某个打手的部分份额。
|
||||
*
|
||||
* 减少的份数会随「大厅余量 = 总份数 − Σ有效份额数量」自动回到大厅;押金保持原冻结额,
|
||||
* 母单状态与验收资料都不动(之后仍可对该份额「单独验收」)。
|
||||
*/
|
||||
export async function reduceAdminWorkOrderShareQuantity(
|
||||
workOrderId: number | string,
|
||||
shareId: number | string,
|
||||
payload: JsonObject = {},
|
||||
actorName = '',
|
||||
) {
|
||||
const reduceQuantity = Math.floor(Number(payload.reduceQuantity ?? payload.quantity ?? 0))
|
||||
if (!Number.isFinite(reduceQuantity) || reduceQuantity <= 0) {
|
||||
throw createHttpError('请填写要撤掉的份数', {
|
||||
statusCode: 400,
|
||||
errorCode: 'work_order_sharing_share_reduce_quantity_required',
|
||||
})
|
||||
}
|
||||
|
||||
const result = await reduceWorkOrderShareQuantity({
|
||||
workOrderId: Number(workOrderId),
|
||||
shareId: Number(shareId),
|
||||
reduceQuantity,
|
||||
note: String(payload.note || payload.reason || '').trim(),
|
||||
actorName,
|
||||
now: nowIso(),
|
||||
})
|
||||
if (result.failureReason === 'work_order_not_reducible') {
|
||||
throw createHttpError('当前拼单状态不能撤销份额', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_sharing_share_reduce_status_invalid',
|
||||
})
|
||||
}
|
||||
if (result.failureReason === 'share_not_found') {
|
||||
throw createHttpError('未找到该拼单参与记录', {
|
||||
statusCode: 404,
|
||||
errorCode: 'work_order_sharing_share_not_found',
|
||||
})
|
||||
}
|
||||
if (result.failureReason === 'share_status_not_reducible' || !result.share || !result.order) {
|
||||
throw createHttpError('只有已参与或已提交验收的拼单份额可以撤销份数', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_sharing_share_reduce_share_status_invalid',
|
||||
})
|
||||
}
|
||||
if (result.failureReason === 'share_quantity_invalid') {
|
||||
throw createHttpError('撤掉的份数必须在 1 到「当前份数 − 1」之间;要全部撤销请用「单人撤单」', {
|
||||
statusCode: 409,
|
||||
errorCode: 'work_order_sharing_share_reduce_quantity_invalid',
|
||||
})
|
||||
}
|
||||
|
||||
publishWorkOrderRealtimeChange({
|
||||
workOrderId: Number(workOrderId),
|
||||
workerIds: await resolveWorkOrderRealtimeWorkerIds(result.order),
|
||||
hallChanged: true,
|
||||
})
|
||||
return {
|
||||
order: mapWorkOrderAdmin(result.order),
|
||||
share: mapWorkOrderShare(result.share),
|
||||
}
|
||||
}
|
||||
|
||||
export async function cancelAdminWorkOrderSharing(
|
||||
workOrderId: number | string,
|
||||
payload: JsonObject = {},
|
||||
|
||||
@@ -54,6 +54,7 @@ export {
|
||||
acceptAdminWorkOrders,
|
||||
cancelAdminWorkOrderShare,
|
||||
cancelAdminWorkOrderSharing,
|
||||
reduceAdminWorkOrderShareQuantity,
|
||||
} from './admin-work-order-acceptance-service.js'
|
||||
export * from './worker-product-match-config-service.js'
|
||||
export * from './worker-announcement-config-service.js'
|
||||
|
||||
@@ -788,6 +788,10 @@ export function mapWorkOrderShare(share: WorkOrderShareRow) {
|
||||
timeoutMinutes: Number(share.timeout_minutes || 0),
|
||||
timeoutPolicy: share.timeout_policy || '',
|
||||
cancelledReason: share.cancelled_reason || '',
|
||||
/** 后台撤单/撤份额时填写的原因(自由文本,选填) */
|
||||
cancelNote: share.cancel_note || '',
|
||||
/** 被撤掉的份数:撤份额累计,单人撤单为整份份数;0 = 未撤过 */
|
||||
cancelQuantity: Math.max(0, Number(share.cancel_quantity || 0)),
|
||||
worker: share.worker_id
|
||||
? {
|
||||
workerId: Number(share.worker_id),
|
||||
|
||||
@@ -3364,6 +3364,64 @@ test('后台单人撤单不改母单状态;份额超时仍交给 recompute 收
|
||||
)
|
||||
})
|
||||
|
||||
test('撤份额与单人撤单都把原因和份数写进份额行,列表才能显示「撤 N 份 · 原因」', () => {
|
||||
const settlementSrc = fs.readFileSync(
|
||||
new URL('../../repositories/worker-platform/work-order-settlement-repo.ts', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
const cancelBlock = settlementSrc.slice(
|
||||
settlementSrc.indexOf('export async function cancelWorkOrderShare('),
|
||||
settlementSrc.indexOf('export async function settleOverdueWorkOrderShare('),
|
||||
)
|
||||
assert.ok(cancelBlock.length > 0, 'cancelWorkOrderShare 片段必须存在')
|
||||
assert.ok(cancelBlock.includes('cancel_note = $'), '单人撤单必须把原因写进 cancel_note')
|
||||
assert.ok(
|
||||
cancelBlock.includes("String(input.reason || '').trim()"),
|
||||
'单人撤单写入 cancel_note 的必须是运营填写的自由文本原因',
|
||||
)
|
||||
assert.ok(
|
||||
cancelBlock.includes("cancelled_reason = 'admin'"),
|
||||
'cancelled_reason 仍保留枚举口径,自由文本不得塞进去',
|
||||
)
|
||||
assert.ok(
|
||||
cancelBlock.includes('cancel_quantity = COALESCE(quantity, 0)'),
|
||||
'单人撤单要按整份数量记录撤销份数,列表才能显示「撤 N 份」',
|
||||
)
|
||||
|
||||
const shareSrc = fs.readFileSync(
|
||||
new URL('../../repositories/worker-platform/work-order-share-repo.ts', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
const reduceBlock = shareSrc.slice(
|
||||
shareSrc.indexOf('export async function reduceWorkOrderShareQuantity('),
|
||||
shareSrc.indexOf('export async function submitWorkOrderShareAcceptance('),
|
||||
)
|
||||
assert.ok(reduceBlock.length > 0, 'reduceWorkOrderShareQuantity 片段必须存在')
|
||||
assert.ok(reduceBlock.includes('cancel_note = $'), '撤份额必须把原因写进 cancel_note')
|
||||
assert.ok(
|
||||
reduceBlock.includes('String(share.cancel_note'),
|
||||
'后续撤份额没填原因时必须保留上一次填的说明',
|
||||
)
|
||||
assert.ok(
|
||||
reduceBlock.includes('cancel_quantity = $') &&
|
||||
reduceBlock.includes('Number(share.cancel_quantity'),
|
||||
'撤份额必须按「已有撤销份数 + 本次份数」累计写入 cancel_quantity',
|
||||
)
|
||||
|
||||
const mapperSrc = fs.readFileSync(
|
||||
new URL('./work-order-display-mappers.ts', import.meta.url),
|
||||
'utf8',
|
||||
)
|
||||
assert.ok(
|
||||
mapperSrc.includes('cancelNote: share.cancel_note'),
|
||||
'拼单明细接口必须下发 cancelNote,否则列表显示不出来',
|
||||
)
|
||||
assert.ok(
|
||||
mapperSrc.includes('cancelQuantity: Math.max(0, Number(share.cancel_quantity'),
|
||||
'拼单明细接口必须下发 cancelQuantity,列表才能拼「撤 N 份」前缀',
|
||||
)
|
||||
})
|
||||
|
||||
test('份额级问题单标记/处置只写份额字段,不得改母单状态或触发整单重算', () => {
|
||||
const src = fs.readFileSync(
|
||||
new URL('../../repositories/worker-platform/work-order-issue-repo.ts', import.meta.url),
|
||||
|
||||
Reference in New Issue
Block a user