feat:拼单订单新增一个取消部分的按钮功能,点击之后,打手进行中和待接单的都会取消订单

This commit is contained in:
2026-10-26 12:15:52 +08:00
parent b0ab8cd410
commit 6a5a8d70b0
15 changed files with 822 additions and 38 deletions
@@ -0,0 +1,103 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import {
resolveSharingPartCancelPlan,
resolveSharingPartCancelRefund,
} from './work-order-sharing-part-cancel.js'
test('取消部分:保留已验收份数,取消进行中 + 待接单份数', () => {
const plan = resolveSharingPartCancelPlan({
workOrderStatus: 'open',
totalQuantity: 10,
acceptedQuantity: 8,
})
assert.deepEqual(plan, { nextTotalQuantity: 8, cancelledQuantity: 2, failureReason: null })
})
test('取消部分:一份都没验收时整单取消(总量归零)', () => {
const plan = resolveSharingPartCancelPlan({
workOrderStatus: 'pending_acceptance',
totalQuantity: 10,
acceptedQuantity: 0,
})
assert.deepEqual(plan, { nextTotalQuantity: 0, cancelledQuantity: 10, failureReason: null })
})
test('取消部分:全部份额已验收时没有可取消份数', () => {
const plan = resolveSharingPartCancelPlan({
workOrderStatus: 'accepted',
totalQuantity: 8,
acceptedQuantity: 8,
})
// accepted 不在允许状态里,优先报状态非法
assert.equal(plan.failureReason, 'status_invalid')
const openPlan = resolveSharingPartCancelPlan({
workOrderStatus: 'open',
totalQuantity: 8,
acceptedQuantity: 8,
})
assert.deepEqual(openPlan, {
nextTotalQuantity: 8,
cancelledQuantity: 0,
failureReason: 'nothing_to_cancel',
})
})
test('取消部分:问题单允许操作,已取消/已验收单不允许', () => {
assert.equal(
resolveSharingPartCancelPlan({
workOrderStatus: 'problem',
totalQuantity: 3,
acceptedQuantity: 1,
}).failureReason,
null,
)
for (const status of ['cancelled', 'accepted', 'unassigned']) {
assert.equal(
resolveSharingPartCancelPlan({
workOrderStatus: status,
totalQuantity: 3,
acceptedQuantity: 0,
}).failureReason,
'status_invalid',
)
}
})
test('取消部分:已验收份数异常时按总量截断', () => {
const plan = resolveSharingPartCancelPlan({
workOrderStatus: 'open',
totalQuantity: 5,
acceptedQuantity: 9,
})
assert.deepEqual(plan, {
nextTotalQuantity: 5,
cancelledQuantity: 0,
failureReason: 'nothing_to_cancel',
})
})
test('取消部分退款:按实付金额比例计算并四舍五入到分', () => {
assert.equal(
resolveSharingPartCancelRefund({
paidAmountFen: 20000,
totalQuantity: 10,
cancelledQuantity: 9,
}),
18000,
)
assert.equal(
resolveSharingPartCancelRefund({ paidAmountFen: 999, totalQuantity: 3, cancelledQuantity: 1 }),
333,
)
assert.equal(
resolveSharingPartCancelRefund({ paidAmountFen: 0, totalQuantity: 10, cancelledQuantity: 10 }),
0,
)
// 取消份数超过总量时按总量截断
assert.equal(
resolveSharingPartCancelRefund({ paidAmountFen: 5000, totalQuantity: 5, cancelledQuantity: 8 }),
5000,
)
})
@@ -0,0 +1,56 @@
/**
* 拼单「取消部分」口径(已确认):
* - 只保留已验收份额;打手进行中(joined / submitted)与待接单的剩余份数一起取消;
* - 取消的份额退还押金,已验收份额的报酬 / 押金 / 验收资料一律不动;
* - 取消后剩下的份额全部是已验收,所以母单直接收尾为已验收;一份都没验收则整单取消。
*/
/** 允许执行取消部分的母单状态:问题单也允许,问题标记随份额一起取消。 */
export const SHARING_PART_CANCEL_STATUSES = [
'open',
'in_progress',
'pending_acceptance',
'problem',
] as const
export function resolveSharingPartCancelPlan(input: {
workOrderStatus: string
/** 母单当前总份数 */
totalQuantity: number
/** 已验收份额的份数合计 */
acceptedQuantity: number
}): {
/** 取消后保留的总份数(= 已验收份数) */
nextTotalQuantity: number
/** 本次要取消的份数(进行中份额 + 待接单剩余) */
cancelledQuantity: number
failureReason: 'status_invalid' | 'nothing_to_cancel' | null
} {
const status = String(input.workOrderStatus || '').trim()
if (!SHARING_PART_CANCEL_STATUSES.includes(status as never)) {
return { nextTotalQuantity: 0, cancelledQuantity: 0, failureReason: 'status_invalid' }
}
const total = Math.max(0, Math.floor(Number(input.totalQuantity) || 0))
const accepted = Math.min(total, Math.max(0, Math.floor(Number(input.acceptedQuantity) || 0)))
const cancelled = total - accepted
if (cancelled <= 0) {
return { nextTotalQuantity: accepted, cancelledQuantity: 0, failureReason: 'nothing_to_cancel' }
}
return { nextTotalQuantity: accepted, cancelledQuantity: cancelled, failureReason: null }
}
/**
* 建议退款金额:按订单实付金额 × 取消份数 / 原总量,四舍五入到分。
* 只是默认值,后台可以在弹窗里手工改。
*/
export function resolveSharingPartCancelRefund(input: {
paidAmountFen: number
totalQuantity: number
cancelledQuantity: number
}): number {
const paid = Math.max(0, Math.round(Number(input.paidAmountFen) || 0))
const total = Math.max(0, Math.floor(Number(input.totalQuantity) || 0))
const cancelled = Math.max(0, Math.floor(Number(input.cancelledQuantity) || 0))
if (paid <= 0 || total <= 0 || cancelled <= 0) return 0
return Math.round((paid * Math.min(cancelled, total)) / total)
}