fix(admin-worker): 发单金额高于用户付款时强制确认,避免只靠红色字提醒

发布、指派和改价都会弹出亏损确认,且不能用「不再提示」跳过;列表发单金额旁增加「高于付款」标签。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yml2213
2026-09-21 15:48:23 +08:00
co-authored by Cursor
parent 9298b31b9b
commit ef491660e7
5 changed files with 200 additions and 77 deletions
@@ -1,11 +1,11 @@
import { App, Input, InputNumber, Typography } from 'antd'
import { App, Input, InputNumber, Space, Tag, Typography } from 'antd'
import { useRef, useState } from 'react'
import { updateAdminWorkOrder } from '@/services/admin'
import type { WorkOrder } from '@/types/worker-platform'
import { formatMoney } from './shared'
import { getUserPaymentFen } from './work-order-panel-utils'
import { getRewardAbovePayment } from './work-order-panel-utils'
/** 商品名:单击看详情,双击原地修改(仅未接走工单) */
export function ProductNameEditableCell({
@@ -107,8 +107,8 @@ export function AmountEditableCell({
const editable = order.capabilities?.canEdit === true
const currentFen = Number(order[field] || 0)
const currentYuan = currentFen / 100
const paymentFen = field === 'rewardAmount' ? getUserPaymentFen(order) : null
const isLossOrder = paymentFen !== null && currentFen > paymentFen
const loss = field === 'rewardAmount' ? getRewardAbovePayment(order, currentFen) : null
const isLossOrder = Boolean(loss)
async function doSave(nextYuan: number) {
if (savingRef.current) return
@@ -141,19 +141,21 @@ export function AmountEditableCell({
return
}
const nextFen = Math.round(nextYuan * 100)
if (paymentFen !== null && nextFen > paymentFen) {
const nextLoss = field === 'rewardAmount' ? getRewardAbovePayment(order, nextFen) : null
if (nextLoss) {
modal.confirm({
title: '发单金额高于用户付款金额',
content: (
<span>
该订单用户付款 {formatMoney(paymentFen)},发单金额{' '}
该订单用户付款 {formatMoney(nextLoss.paymentFen)},发单金额{' '}
<Typography.Text strong type="danger">
{formatMoney(nextFen)}
{formatMoney(nextLoss.rewardFen)}
</Typography.Text>
,本单平台将亏损。确认保存?
</span>
),
okText: '确认保存',
okButtonProps: { danger: true },
cancelText: '取消',
onOk: () => doSave(nextYuan),
})
@@ -164,22 +166,26 @@ export function AmountEditableCell({
if (!editing) {
return (
<Typography.Text
<div
title={editable ? '双击修改金额' : undefined}
strong={isLossOrder}
type={isLossOrder ? 'danger' : undefined}
style={{
cursor: editable ? 'pointer' : 'default',
...(editable && !isLossOrder ? { color: 'rgba(0,0,0,0.65)' } : {}),
}}
style={{ cursor: editable ? 'pointer' : 'default' }}
onDoubleClick={() => {
if (!editable) return
setDraft(currentYuan)
setEditing(true)
}}
>
{formatMoney(currentFen)}
</Typography.Text>
<Space size={4} wrap>
<Typography.Text
strong={isLossOrder}
type={isLossOrder ? 'danger' : undefined}
style={editable && !isLossOrder ? { color: 'rgba(0,0,0,0.65)' } : undefined}
>
{formatMoney(currentFen)}
</Typography.Text>
{isLossOrder ? <Tag color="red">高于付款</Tag> : null}
</Space>
</div>
)
}
@@ -119,7 +119,7 @@ import {
formatRequirementFieldsText,
formatUserPaymentAmount,
getDisplayOrderNo,
getUserPaymentFen,
getRewardAbovePayment,
parseWorkerMaterialText,
renderWorkOrderWorkers,
setSkipPublishConfirm,
@@ -546,9 +546,21 @@ export default function WorkOrdersPanel() {
}, [acceptanceMode])
function openAssignModal(row: WorkOrder) {
setAssignOrder(row)
setAssignWorkerId(undefined)
setAssignWorkerKeyword('')
const open = () => {
setAssignOrder(row)
setAssignWorkerId(undefined)
setAssignWorkerKeyword('')
}
if (
confirmRewardAbovePayment(row, {
okText: '继续指派',
question: '确认继续指派?',
onOk: open,
})
) {
return
}
open()
}
function openCancelModal(row: WorkOrder) {
@@ -730,6 +742,36 @@ export default function WorkOrdersPanel() {
}
}
function confirmRewardAbovePayment(
order: WorkOrder,
options: {
rewardFen?: number
okText: string
question: string
onOk: () => void | Promise<unknown>
},
) {
const loss = getRewardAbovePayment(order, options.rewardFen)
if (!loss) return false
modal.confirm({
title: '发单金额高于用户付款金额',
content: (
<span>
该订单用户付款 {formatMoney(loss.paymentFen)},发单金额{' '}
<Typography.Text strong type="danger">
{formatMoney(loss.rewardFen)}
</Typography.Text>
,本单平台将亏损。{options.question}
</span>
),
okText: options.okText,
okButtonProps: { danger: true },
cancelText: '取消',
onOk: options.onOk,
})
return true
}
function publishWorkOrder(row: WorkOrder) {
return runAction(
() => publishAdminWorkOrder(row.workOrderId),
@@ -745,6 +787,16 @@ export default function WorkOrdersPanel() {
}
function confirmPublishWorkOrder(row: WorkOrder) {
if (
confirmRewardAbovePayment(row, {
okText: '仍要发布',
question: '确认发布?',
onOk: () => publishWorkOrder(row),
})
) {
return
}
if (shouldSkipPublishConfirm()) {
void publishWorkOrder(row)
return
@@ -983,28 +1035,44 @@ export default function WorkOrdersPanel() {
materialOrder.status === 'in_progress' && !materialOrder.sharing?.enabled
? Number(values.rewardAmount || 0)
: undefined
const succeeded = await runAction(
async () => {
const response = await submitAdminWorkOrderMaterial(
materialOrder.workOrderId,
values.fields || {},
materialScreenshots,
requestedRewardAmount,
)
if (
requestedRewardAmount !== undefined &&
Number(response.data.order.rewardAmount || 0) !== Math.round(requestedRewardAmount * 100)
) {
throw new Error('接单价格保存未生效,请刷新后重试')
}
return response
},
materialOrder.status === 'in_progress' ? '订单资料已更新' : '订单资料已保存',
)
if (!succeeded) return
setMaterialOrder(null)
materialForm.resetFields()
setMaterialScreenshots([])
const doSubmit = async () => {
const succeeded = await runAction(
async () => {
const response = await submitAdminWorkOrderMaterial(
materialOrder.workOrderId,
values.fields || {},
materialScreenshots,
requestedRewardAmount,
)
if (
requestedRewardAmount !== undefined &&
Number(response.data.order.rewardAmount || 0) !==
Math.round(requestedRewardAmount * 100)
) {
throw new Error('接单价格保存未生效,请刷新后重试')
}
return response
},
materialOrder.status === 'in_progress' ? '订单资料已更新' : '订单资料已保存',
)
if (!succeeded) return
setMaterialOrder(null)
materialForm.resetFields()
setMaterialScreenshots([])
}
if (requestedRewardAmount !== undefined) {
if (
confirmRewardAbovePayment(materialOrder, {
rewardFen: Math.round(requestedRewardAmount * 100),
okText: '确认保存',
question: '确认保存资料?',
onOk: doSubmit,
})
) {
return
}
}
await doSubmit()
}
function isExplicitOrderNoSearch(text: string) {
@@ -1144,35 +1212,24 @@ export default function WorkOrdersPanel() {
}),
'工单信息已保存',
)
const paymentFen = getUserPaymentFen(editOrder)
const rewardFen = Math.round(Number(values.rewardAmount || 0) * 100)
if (paymentFen !== null && rewardFen > paymentFen) {
modal.confirm({
title: '发单金额高于用户付款金额',
content: (
<span>
该订单用户付款 {formatMoney(paymentFen)},发单金额{' '}
<Typography.Text strong type="danger">
{formatMoney(rewardFen)}
</Typography.Text>
,本单平台将亏损。确认保存?
</span>
),
const doFinish = async () => {
const succeeded = await doSave()
if (!succeeded) return
setEditOrder(null)
editForm.resetFields()
}
if (
confirmRewardAbovePayment(editOrder, {
rewardFen,
okText: '确认保存',
cancelText: '取消',
onOk: async () => {
const succeeded = await doSave()
if (!succeeded) return
setEditOrder(null)
editForm.resetFields()
},
question: '确认保存?',
onOk: doFinish,
})
) {
return
}
const succeeded = await doSave()
if (!succeeded) return
setEditOrder(null)
editForm.resetFields()
await doFinish()
}
function requestDeleteWorkOrder(row: WorkOrder) {
@@ -1393,7 +1450,7 @@ export default function WorkOrdersPanel() {
},
{
title: '发单金额',
width: 130,
width: 160,
render: (_, row) => (
<AmountEditableCell order={row} field="rewardAmount" onSaved={refreshAll} />
),
@@ -3,7 +3,10 @@ import { Space, Tag, Typography } from 'antd'
import type { CollectField, WorkOrder } from '@/types/worker-platform'
import { resolveConsignmentWorkerDisplay } from '@/utils/consignment-display'
import { asRecord, formatMoney } from './shared'
import { formatMoney } from './shared'
import { getUserPaymentFen } from './work-order-payment'
export { getRewardAbovePayment, getUserPaymentFen } from './work-order-payment'
const SKIP_PUBLISH_CONFIRM_KEY = 'admin-work-order-skip-publish-confirm'
@@ -88,15 +91,6 @@ export function getDisplayOrderNo(
return String(order?.workOrderNo || order?.platformOrderId || '').trim() || '-'
}
/** 用户付款金额(分);未知返回 null(不参与超价提示) */
export function getUserPaymentFen(order: WorkOrder): number | null {
const material = asRecord(order.material)
const source = asRecord(material.source)
const sendCode = asRecord(source.kuaishouSendCode)
const paymentFen = Number(sendCode.paymentFen ?? source.orderAmountFen)
return Number.isFinite(paymentFen) ? paymentFen : null
}
/** 解析客服粘贴文本(如「安卓还是苹果(必填):安卓」),返回字段 key → 值 */
export function parseWorkerMaterialText(text: string): Record<string, string> {
const result: Record<string, string> = {}
@@ -0,0 +1,29 @@
type PaymentSourceOrder = {
material?: unknown
rewardAmount?: number
}
function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
: {}
}
/** 用户付款金额(分);未知返回 null(不参与超价提示) */
export function getUserPaymentFen(order: PaymentSourceOrder): number | null {
const material = asRecord(order.material)
const source = asRecord(material.source)
const sendCode = asRecord(source.kuaishouSendCode)
const paymentFen = Number(sendCode.paymentFen ?? source.orderAmountFen)
return Number.isFinite(paymentFen) ? paymentFen : null
}
/** 发单金额高于用户付款时返回两边金额,否则 null(付款未知时不提示) */
export function getRewardAbovePayment(order: PaymentSourceOrder, rewardFen?: number) {
const paymentFen = getUserPaymentFen(order)
const nextRewardFen = Number(rewardFen === undefined ? order.rewardAmount || 0 : rewardFen)
if (paymentFen === null || !Number.isFinite(nextRewardFen) || nextRewardFen <= paymentFen) {
return null
}
return { paymentFen, rewardFen: nextRewardFen }
}
@@ -0,0 +1,37 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { getRewardAbovePayment } from '../src/pages/admin/panels/work-order-payment.ts'
function orderWithPayment(paymentFen: number, rewardFen: number) {
return {
rewardAmount: rewardFen,
material: {
source: {
kuaishouSendCode: { paymentFen },
},
},
}
}
test('发单金额高于用户付款时返回两边金额', () => {
const loss = getRewardAbovePayment(orderWithPayment(900, 6800))
assert.ok(loss)
assert.equal(loss.paymentFen, 900)
assert.equal(loss.rewardFen, 6800)
})
test('发单金额不超过用户付款时不提示', () => {
assert.equal(getRewardAbovePayment(orderWithPayment(6800, 6800)), null)
assert.equal(getRewardAbovePayment(orderWithPayment(6800, 6700)), null)
})
test('用户付款未知时不参与超价提示', () => {
assert.equal(getRewardAbovePayment({ rewardAmount: 6800, material: {} }), null)
})
test('可传入即将保存的发单金额做预检', () => {
const order = orderWithPayment(900, 800)
assert.equal(getRewardAbovePayment(order), null)
assert.ok(getRewardAbovePayment(order, 1000))
})