fix(admin-worker): 发单金额高于用户付款时强制确认,避免只靠红色字提醒
发布、指派和改价都会弹出亏损确认,且不能用「不再提示」跳过;列表发单金额旁增加「高于付款」标签。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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))
|
||||
})
|
||||
Reference in New Issue
Block a user