fix(consignment): 下线 H5 收集链接渠道并封闭寄卖履约状态机

下线定向收集链接(H5 填报)整条线,提报统一走接单平台打手端:
- 删除前端 ConsignmentCollectPage、consignment-public 服务与 /consignment-collect/:token 路由
- 删除后端公开路由 routes/consignment.ts、consignment-public-service 与管理端 /consignment/links 三接口
- 迁移 079 删表 consignment_collect_links、删列 consignment_submissions.link_id,
  source_type 的 collection_link 归并为 worker_portal 并替换 CHECK

修复手工入库/编辑的凭据与截图静默丢失:后端统一按 credentialsJson ?? credentials、
screenshotsJson ?? screenshots 读取(入库/编辑/审核入库三个入口)。

按"已验收一律禁止撤销"封闭状态机:
- 撤销寄售履约加状态白名单(in_progress|pending_acceptance|problem)+ 事务内 FOR UPDATE
  锁单 + UPDATE 带 status 条件(0 行 409)+ 原因必填 + 清空指派/时限/验收草稿痕迹
- 库存改状态一律带条件:撤销要求 bound_work_order_id 命中本工单;作废/回库要求未被
  进行中工单占用(被挡时提示具体工单号);079 加部分唯一索引并清理历史重复绑定
- 工单终止统一解绑回库:打手撤单/取消指派/退回大厅/撤单/打手退单申请/超时处置接入
  releaseConsignmentInventoryBindingForWorkOrderWithClient(售后不改工单状态,无需接入)
- 回库路径保留运营的 allow_matching 开关设置(不再强制开启),前端文案同步更正

顺带修掉资金与归属的同源缺陷:
- 押金归属与重冻金额改取不可变的冻结核销行,审核改派不再"退给错人/向错人扣款"
- 新增 assigned_worker_id 拆开"卖家"与"被指派履约人",占库不再覆盖卖家
- PUT /inventories/:id 禁止直接改 status;发布接口补 requireActiveWorker
- 押金冻结/释放后补推钱包实时事件;迁移 079 给发布冻结加台账幂等唯一索引

验证:后端 401 测试/398 通过/3 skip(新增 5 例回归),前端 16 通过;
两侧 typecheck、lint:check、format:check 与 check:sql、build 全绿。
迁移 079 未经真实数据库执行(本环境无 Postgres),上库前请先 dry-run。
This commit is contained in:
yml2213
2026-09-16 20:05:49 +08:00
parent e828e677a5
commit 632dddc4c3
25 changed files with 960 additions and 1363 deletions
-2
View File
@@ -35,7 +35,6 @@ const AdminDevMockPage = lazy(() => import('@/pages/admin/AdminDevMockPage'))
const AdminWorkerPlatformPage = lazy(() => import('@/pages/admin/AdminWorkerPlatformPage'))
const ClaimPage = lazy(() => import('@/pages/claim/ClaimPage'))
const CollectPage = lazy(() => import('@/pages/collect/CollectPage'))
const ConsignmentCollectPage = lazy(() => import('@/pages/collect/ConsignmentCollectPage'))
const NotFoundPage = lazy(() => import('@/pages/NotFoundPage'))
const WorkerHallPage = lazy(() => import('@/pages/worker/WorkerHallPage'))
const WorkerLoginPage = lazy(() => import('@/pages/worker/WorkerLoginPage'))
@@ -131,7 +130,6 @@ function AdminApplication() {
</Route>
<Route path="/claim/:token" element={<ClaimPage />} />
<Route path="/collect" element={<CollectPage />} />
<Route path="/consignment-collect/:token" element={<ConsignmentCollectPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</Suspense>
@@ -305,7 +305,7 @@ export default function ConsignmentInventoryPanel() {
if (!returnItem) return
try {
await returnAdminConsignmentInventory(returnItem.id, { reason: returnReason })
message.success('物品已退单回库,重新进入待匹配池!')
message.success('物品已退单回库,可在列表中查看匹配开关状态')
setReturnItem(null)
setReturnReason('')
await refresh()
@@ -339,9 +339,13 @@ export default function ConsignmentInventoryPanel() {
<Text type="secondary" copyable style={{ fontSize: 12 }}>
{record.inventoryNo}
</Text>
{record.sourceType === 'collection_link' ? (
{record.sourceType === 'worker_portal' ? (
<Tag color="purple" style={{ fontSize: 11, padding: '0 4px' }}>
链接收集
打手端提报
</Tag>
) : record.sourceType === 'recycle' ? (
<Tag color="green" style={{ fontSize: 11, padding: '0 4px' }}>
回收
</Tag>
) : (
<Tag color="blue" style={{ fontSize: 11, padding: '0 4px' }}>
@@ -1249,7 +1253,7 @@ export default function ConsignmentInventoryPanel() {
退回复库吗?
</Text>
<div style={{ marginTop: 8, color: '#8c8c8c', fontSize: 12 }}>
回库后,物品状态将重新变为【在库】,允许匹配开关将自动开启,可重新匹配履约其他工单。
回库后,物品状态将重新变为【在库】。允许匹配开关保持原设置:若该物品此前被手动暂停匹配,回库后需在列表里手动开启才会重新参与匹配。
</div>
</div>
<Form layout="vertical">
@@ -141,7 +141,6 @@ export default function ConsignmentSubmissionsPanel() {
function resolveSubmissionSource(record: ConsignmentSubmission) {
const channel = (record.contentJson as { channel?: string } | undefined)?.channel
if (channel === 'worker_portal' || record.workerId) return '打手端提报'
if (record.linkTitle) return `旧收集链接: ${record.linkTitle}`
return '其他渠道'
}
@@ -167,6 +167,18 @@ function isInternalConsignmentOrder(row: WorkOrder) {
return Boolean(consignment.inventoryId) && !row.worker
}
/**
* 只有「未验收且未取消」的工单可以撤销寄售履约。
* 已验收工单撤销会造成同一件现货二次出货、并重新冻结已退还的押金,后端一律拒绝。
*/
const CONSIGNMENT_REVOCABLE_ORDER_STATUSES = ['in_progress', 'pending_acceptance', 'problem']
function isConsignmentRevocableOrder(row: WorkOrder) {
return (
isInternalConsignmentOrder(row) && CONSIGNMENT_REVOCABLE_ORDER_STATUSES.includes(row.status)
)
}
export default function WorkOrdersPanel() {
const { message, modal } = App.useApp()
const navigate = useNavigate()
@@ -216,6 +228,9 @@ export default function WorkOrdersPanel() {
const canDeductDeposit = sessionPermissions
? sessionPermissions.includes('worker_order.deduct_deposit')
: hasAdminPermission('worker_order.deduct_deposit')
const canFulfillConsignment = sessionPermissions
? sessionPermissions.includes('consignment.fulfill')
: hasAdminPermission('consignment.fulfill')
const canConfigureSharing = sessionPermissions
? sessionPermissions.includes('worker_order.configure_share')
: hasAdminPermission('worker_order.configure_share')
@@ -760,6 +775,10 @@ export default function WorkOrdersPanel() {
async function handleRevokeConsignmentSubmit() {
if (!revokeConsignmentModal.order) return
if (!revokeConsignmentModal.reason.trim()) {
message.warning('请填写撤单原因,便于后续对账与追溯')
return
}
setRevokeConsignmentModal((prev) => ({ ...prev, submitting: true }))
try {
await revokeWorkOrderConsignmentFulfillment({
@@ -1454,7 +1473,7 @@ export default function WorkOrdersPanel() {
扣押金
</Button>
) : null}
{isInternalConsignmentOrder(row) ? (
{isConsignmentRevocableOrder(row) && canFulfillConsignment ? (
<Button
icon={<RollbackOutlined />}
danger
@@ -2997,8 +3016,8 @@ export default function WorkOrdersPanel() {
<Alert
showIcon
type="warning"
message="撤销后,关联寄售物品将退单回库并重新进入待匹配池"
description="适用场景:买家退款、客服误选库存、账号密码错误失效或规格不符等情况。"
message="撤销后,关联寄售物品将退单回库;物品的【允许匹配】开关保持原设置不变"
description="只适用于【进行中 / 待验收 / 问题单】。已验收订单无法撤销(现货已交付、押金已结清),买家退款或纠纷请走售后流程。"
style={{ marginBottom: 16 }}
/>
@@ -1,577 +0,0 @@
import {
CheckCircleOutlined,
CloudUploadOutlined,
DollarOutlined,
SafetyCertificateOutlined,
TrophyOutlined,
} from '@ant-design/icons'
import {
Alert,
App,
Button,
Card,
Col,
Descriptions,
Form,
Image,
Input,
InputNumber,
Result,
Row,
Select,
Space,
Spin,
Tag,
Typography,
Upload,
} from 'antd'
import type { UploadFile } from 'antd'
import { useEffect, useState } from 'react'
import { useParams } from 'react-router'
import {
fetchPublicConsignmentLink,
submitPublicConsignment,
uploadConsignmentImage,
} from '@/services/consignment-public'
import type { ConsignmentCollectLink, ConsignmentSubmission } from '@/types/consignment'
const { Title, Paragraph, Text } = Typography
export default function ConsignmentCollectPage() {
const { message } = App.useApp()
const { token } = useParams<{ token: string }>()
const [loading, setLoading] = useState(true)
const [errorMsg, setErrorMsg] = useState('')
const [linkInfo, setLinkInfo] = useState<ConsignmentCollectLink | null>(null)
const [submittedResult, setSubmittedResult] = useState<ConsignmentSubmission | null>(null)
const [fileList, setFileList] = useState<UploadFile[]>([])
const [submitting, setSubmitting] = useState(false)
const [depositTouched, setDepositTouched] = useState(false)
const [form] = Form.useForm()
function updateAutoItemName(changedValues?: Record<string, unknown>) {
const sys = form.getFieldValue('system') || '苹果'
const zone = form.getFieldValue('serverZone') || '微信区'
const ser = form.getFieldValue('series') || '暗潮'
const dia = form.getFieldValue('diamondLevel') || 1
const bri = form.getFieldValue('brillianceCount') || 0
const briStr = bri > 0 ? `${bri}璀璨` : ''
const next: Record<string, unknown> = {
itemName: `${sys}${zone}${ser}${dia}钻${briStr} 苏尔南`,
}
if (changedValues && Object.prototype.hasOwnProperty.call(changedValues, 'depositAmount')) {
setDepositTouched(true)
}
if (
changedValues &&
Object.prototype.hasOwnProperty.call(changedValues, 'askingPrice') &&
!depositTouched &&
!Object.prototype.hasOwnProperty.call(changedValues, 'depositAmount')
) {
const price = Number(changedValues.askingPrice || 0)
if (price > 0) next.depositAmount = price
}
form.setFieldsValue(next)
}
useEffect(() => {
if (!token) {
setErrorMsg('缺少收集凭证')
setLoading(false)
return
}
fetchPublicConsignmentLink(token)
.then((res) => {
const data = res.data
setLinkInfo(data)
const briStr = data.brillianceCount > 0 ? `${data.brillianceCount}璀璨` : ''
form.setFieldsValue({
itemName:
data.title ||
`${data.system || '苹果'}${data.serverZone || '微信区'}${data.series || '暗潮'}${data.diamondLevel || 1}钻${briStr} 苏尔南`,
system: data.system || '苹果',
serverZone: data.serverZone || '微信区',
series: data.series || '暗潮',
diamondLevel: data.diamondLevel || 1,
brillianceCount: data.brillianceCount || 0,
askingPrice: data.presetPriceFen > 0 ? data.presetPriceFen / 100 : undefined,
depositAmount: data.presetPriceFen > 0 ? data.presetPriceFen / 100 : 100,
})
setDepositTouched(false)
})
.catch((err: any) => {
setErrorMsg(err.message || '获取收集信息失败或链接已失效')
})
.finally(() => {
setLoading(false)
})
}, [token, form])
async function handleSubmit(values: any) {
if (!token) return
const screenshots = fileList
.filter((f) => f.status === 'done' && (f.response?.url || f.url))
.map((f) => f.response?.url || f.url)
if (screenshots.length === 0) {
message.error('请上传游戏主页截图以供核验!')
return
}
setSubmitting(true)
try {
const askingPriceFen = Math.round(Number(values.askingPrice || 0) * 100)
const depositAmountFen = Math.round(
Number(values.depositAmount || values.askingPrice || 100) * 100,
)
if (askingPriceFen < 100) {
message.error('寄售价格不能少于1元')
return
}
if (depositAmountFen < 100) {
message.error('履约押金不能少于1元')
return
}
const payload = {
itemName:
values.itemName ||
`${values.system}${values.serverZone}${values.series}${values.diamondLevel}钻苏尔南`,
system: values.system,
serverZone: values.serverZone,
series: values.series,
diamondLevel: Number(values.diamondLevel || 1),
brillianceCount: Number(values.brillianceCount || 0),
sellerContact: values.sellerContact,
contactInfo: values.sellerContact,
sellerPhone: values.sellerPhone,
sellerGameId: `${values.characterName} (${values.characterId})`,
sellerPaymentInfo: '走接单平台钱包结算',
askingPriceFen,
depositAmountFen,
screenshotsJson: screenshots,
contentJson: {
characterName: values.characterName,
characterId: values.characterId,
sellerNote: values.sellerNote,
},
}
const res = await submitPublicConsignment(token, payload)
setSubmittedResult(res.data)
message.success('苏尔南手枪寄卖登记提交成功!')
} catch (err: any) {
message.error(err.message || '提交失败,请重试')
} finally {
setSubmitting(false)
}
}
if (loading) {
return (
<div style={{ padding: '80px 20px', textAlign: 'center' }}>
<Spin size="large" />
<div style={{ marginTop: 16, color: '#8c8c8c' }}>正在加载苏尔南手枪寄卖登记信息...</div>
</div>
)
}
if (errorMsg) {
return (
<div style={{ maxWidth: 600, margin: '60px auto', padding: '0 16px' }}>
<Result
status="warning"
title="链接无法访问"
subTitle={errorMsg}
extra={<Text type="secondary">如有疑问,请联系客服重新获取有效链接</Text>}
/>
</div>
)
}
if (submittedResult) {
return (
<div style={{ maxWidth: 640, margin: '40px auto', padding: '0 16px' }}>
<Card
bordered={false}
style={{ boxShadow: '0 4px 16px rgba(0,0,0,0.06)', borderRadius: 16 }}
>
<Result
status="success"
title="苏尔南手枪寄售登记成功!"
subTitle={
<div>
<div style={{ fontSize: 15, margin: '8px 0' }}>
登记单号:
<Text strong copyable style={{ color: '#1890ff' }}>
{submittedResult.submissionNo}
</Text>
</div>
<div style={{ marginTop: 12, color: '#595959', lineHeight: 1.7 }}>
您的寄售手枪已进入<strong>打手接单平台寄售池</strong>。
<br />
匹配到买家工单后,系统将通过接单平台指派您进行局内交付,并自动执行
<strong>押金保障机制</strong>。交付验收后货款将直达您的账户。
</div>
</div>
}
extra={[
<Button key="back" type="primary" onClick={() => window.location.reload()}>
返回重新填写
</Button>,
]}
/>
</Card>
</div>
)
}
return (
<div
style={{
minHeight: '100vh',
background: '#f0f2f5',
padding: '24px 16px 60px',
}}
>
<div style={{ maxWidth: 680, margin: '0 auto' }}>
{/* 顶部标题区 */}
<Card
bordered={false}
style={{
borderRadius: 16,
marginBottom: 16,
boxShadow: '0 4px 14px rgba(24, 144, 255, 0.15)',
background: 'linear-gradient(135deg, #096dd9 0%, #1890ff 50%, #40a9ff 100%)',
color: '#fff',
}}
>
<Space direction="vertical" size={8} style={{ width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<TrophyOutlined style={{ fontSize: 26, color: '#ffd666' }} />
<Title level={4} style={{ color: '#fff', margin: 0 }}>
{linkInfo?.title || '苏尔南手枪 · 走打手接单平台寄卖'}
</Title>
</div>
<Paragraph style={{ color: 'rgba(255,255,255,0.92)', margin: 0, fontSize: 13.5 }}>
平台保障 · 走接单平台押金履约 · 验货即打款 · 杜绝私下跑单
</Paragraph>
{linkInfo ? (
<div style={{ marginTop: 6, display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{linkInfo.system ? <Tag color="blue">{linkInfo.system}</Tag> : null}
{linkInfo.serverZone ? <Tag color="cyan">{linkInfo.serverZone}</Tag> : null}
{linkInfo.series ? <Tag color="orange">{linkInfo.series}</Tag> : null}
{linkInfo.diamondLevel ? <Tag color="gold">{linkInfo.diamondLevel} 钻</Tag> : null}
{linkInfo.brillianceCount > 0 ? (
<Tag color="magenta">{linkInfo.brillianceCount} 璀璨</Tag>
) : null}
{linkInfo.presetPriceFen > 0 ? (
<Tag color="volcano" style={{ fontWeight: 600 }}>
指导收购价 ¥{(linkInfo.presetPriceFen / 100).toFixed(0)}
</Tag>
) : null}
</div>
) : null}
</Space>
</Card>
{/* 押金保障卡片 */}
<Alert
showIcon
icon={<SafetyCertificateOutlined style={{ fontSize: 20, color: '#52c41a' }} />}
type="success"
message="走打手接单平台 · 押金保障机制"
description="寄售手枪统一通过接单平台派单交付。登记时可填写履约押金(默认跟寄售价一致);指派交枪时冻结押金,买家验货确认后解冻并结算货款。"
style={{ marginBottom: 16, borderRadius: 12, border: '1px solid #b7eb8f' }}
/>
{/* 表单填写区 */}
<Card
bordered={false}
style={{
borderRadius: 16,
boxShadow: '0 2px 10px rgba(0,0,0,0.04)',
}}
>
<Form
form={form}
layout="vertical"
initialValues={{ depositAmount: 100 }}
onFinish={handleSubmit}
onValuesChange={(changed) => updateAutoItemName(changed)}
>
{/* 1. 枪系与规格 */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
marginBottom: 16,
borderBottom: '1px solid #f0f0f0',
paddingBottom: 8,
}}
>
<TrophyOutlined style={{ color: '#1890ff', fontSize: 18 }} />
<Title level={5} style={{ margin: 0 }}>
1. 苏尔南手枪规格与价格
</Title>
</div>
<Row gutter={12}>
<Col span={12}>
<Form.Item
name="system"
label="操作系统"
rules={[{ required: true, message: '请选择操作系统' }]}
>
<Select
options={[
{ label: '苹果 (iOS)', value: '苹果' },
{ label: '安卓 (Android)', value: '安卓' },
{ label: '通用/双端', value: '通用' },
]}
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="serverZone"
label="游戏区服"
rules={[{ required: true, message: '请选择游戏区服' }]}
>
<Select
options={[
{ label: '微信区', value: '微信区' },
{ label: 'QQ区', value: 'QQ区' },
]}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={12}>
<Col span={8}>
<Form.Item
name="series"
label="系列/枪系"
rules={[{ required: true, message: '请选择枪系' }]}
>
<Select
options={[
{ label: '暗潮', value: '暗潮' },
{ label: '怒嚎', value: '怒嚎' },
{ label: '血宴', value: '血宴' },
]}
/>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
name="diamondLevel"
label="钻石等级 (1-7)"
rules={[{ required: true, message: '请选择钻石等级' }]}
>
<Select
options={[
{ label: '1 钻', value: 1 },
{ label: '2 钻', value: 2 },
{ label: '3 钻', value: 3 },
{ label: '4 钻', value: 4 },
{ label: '5 钻', value: 5 },
{ label: '6 钻', value: 6 },
{ label: '7 钻', value: 7 },
]}
/>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item name="brillianceCount" label="璀璨数 (0-7)">
<Select
options={[
{ label: '无璀璨 (0)', value: 0 },
{ label: '1 璀璨', value: 1 },
{ label: '2 璀璨', value: 2 },
{ label: '3 璀璨', value: 3 },
{ label: '4 璀璨', value: 4 },
{ label: '5 璀璨', value: 5 },
{ label: '6 璀璨', value: 6 },
{ label: '7 璀璨', value: 7 },
]}
/>
</Form.Item>
</Col>
</Row>
<Row gutter={12}>
<Col span={10}>
<Form.Item
name="itemName"
label="物品名称"
rules={[{ required: true, message: '请输入物品名称' }]}
>
<Input placeholder="如:苹果微信暗潮1钻 苏尔南" />
</Form.Item>
</Col>
<Col span={7}>
<Form.Item
name="askingPrice"
label="寄售价格 (元)"
rules={[{ required: true, message: '请填写寄售价格' }]}
>
<InputNumber
min={1}
step={1}
style={{ width: '100%' }}
placeholder="如 80"
addonBefore="¥"
/>
</Form.Item>
</Col>
<Col span={7}>
<Form.Item
name="depositAmount"
label="履约押金 (元)"
rules={[{ required: true, message: '请填写履约押金' }]}
tooltip="指派打手交枪时冻结;验货通过后解冻。建议不低于寄售价。"
extra="默认跟寄售价一致,也可手动调整"
>
<InputNumber
min={1}
step={10}
style={{ width: '100%' }}
placeholder="如 100"
addonBefore="¥"
/>
</Form.Item>
</Col>
</Row>
{/* 2. 卖家联系方式与游戏凭据 */}
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 8,
margin: '24px 0 16px',
borderBottom: '1px solid #f0f0f0',
paddingBottom: 8,
}}
>
<SafetyCertificateOutlined style={{ color: '#52c41a', fontSize: 18 }} />
<Title level={5} style={{ margin: 0 }}>
2. 卖家联系方式与游戏凭据
</Title>
</div>
<Row gutter={12}>
<Col span={12}>
<Form.Item
name="sellerContact"
label="您的微信号/QQ号"
rules={[{ required: true, message: '请填写微信号或QQ' }]}
>
<Input placeholder="方便客服核实与通知" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="sellerPhone"
label="联系电话 (用于打手平台关联)"
rules={[{ required: true, message: '请填写联系电话' }]}
tooltip="如您已是接单平台打手,请填写注册手机号,系统将自动识别并关联您的打手账户与押金额度"
>
<Input placeholder="手机号 (用于匹配打手)" />
</Form.Item>
</Col>
</Row>
<Row gutter={12}>
<Col span={12}>
<Form.Item
name="characterName"
label="游戏内角色名"
rules={[{ required: true, message: '请填写游戏内角色名' }]}
>
<Input placeholder="如:战神张三 (用于局内加好友丢枪)" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="characterId"
label="角色数字ID"
rules={[{ required: true, message: '请填写角色数字ID' }]}
>
<Input placeholder="如:1089238472 (核准号主身份)" />
</Form.Item>
</Col>
</Row>
<Form.Item
label="游戏主页截图 (必须包含角色名、ID及仓库/手枪详情)"
required
tooltip="请上传游戏个人主页或背包手枪品质截图,供客服审核真实性"
>
<Upload
listType="picture-card"
fileList={fileList}
maxCount={4}
customRequest={async ({ file, onSuccess, onError }) => {
try {
const res = await uploadConsignmentImage(file as File)
onSuccess?.(res)
} catch (err: any) {
onError?.(err)
message.error(err.message || '上传失败')
}
}}
onChange={({ fileList: nextList }) => setFileList(nextList)}
>
{fileList.length < 4 ? (
<div>
<CloudUploadOutlined style={{ fontSize: 22, color: '#1890ff' }} />
<div style={{ marginTop: 6, fontSize: 12 }}>上传主页截图</div>
</div>
) : null}
</Upload>
</Form.Item>
<Form.Item name="sellerNote" label="其他留言与补充">
<Input.TextArea
rows={2}
placeholder="如有手枪配件、子弹、涂装或在线时间段说明,可在此备注"
/>
</Form.Item>
<Button
type="primary"
htmlType="submit"
size="large"
block
loading={submitting}
icon={<DollarOutlined />}
style={{
marginTop: 16,
height: 48,
fontSize: 16,
borderRadius: 10,
background: 'linear-gradient(135deg, #1890ff 0%, #096dd9 100%)',
}}
>
提交苏尔南手枪寄售登记
</Button>
</Form>
</Card>
</div>
</div>
)
}
@@ -1,6 +1,5 @@
import { apiGet, apiPatch, apiPost, apiPut } from '@/lib/http'
import type {
ConsignmentCollectLink,
ConsignmentInventory,
ConsignmentStatsSummary,
ConsignmentSubmission,
@@ -62,25 +61,6 @@ export function returnAdminConsignmentInventory(id: number | string, payload: {
)
}
// --- 收集链接 ---
export function fetchAdminConsignmentLinks() {
return apiGet<{ items: ConsignmentCollectLink[] }>(
'/api/v1/admin/worker-platform/consignment/links',
)
}
export function createAdminConsignmentLink(payload: Record<string, unknown>) {
return apiPost<ConsignmentCollectLink>('/api/v1/admin/worker-platform/consignment/links', payload)
}
export function updateAdminConsignmentLink(id: number | string, payload: Record<string, unknown>) {
return apiPut<ConsignmentCollectLink>(
`/api/v1/admin/worker-platform/consignment/links/${id}`,
payload,
)
}
// --- 提报审核 ---
export function fetchAdminConsignmentSubmissions(params?: Record<string, unknown>) {
@@ -1,34 +0,0 @@
import { apiGet, apiPost } from '@/lib/http'
import type { ConsignmentCollectLink, ConsignmentSubmission } from '@/types/consignment'
export function fetchPublicConsignmentLink(token: string) {
return apiGet<ConsignmentCollectLink>(`/api/v1/consignment/${encodeURIComponent(token)}`)
}
export function submitPublicConsignment(token: string, payload: Record<string, unknown>) {
return apiPost<ConsignmentSubmission>(
`/api/v1/consignment/${encodeURIComponent(token)}/submit`,
payload,
)
}
export async function uploadConsignmentImage(
file: File,
): Promise<{ url: string; assetId?: string }> {
const formData = new FormData()
formData.append('file', file)
formData.append('scene', 'consignment-material')
const response = await fetch('/api/v1/consignment/upload', {
method: 'POST',
body: formData,
})
if (!response.ok) {
const errJson = await response.json().catch(() => ({}))
throw new Error(errJson?.message || '图片上传失败')
}
const result = await response.json()
return result?.data || result
}
+3 -28
View File
@@ -26,11 +26,13 @@ export type ConsignmentInventory = {
allocationStatus: string
boundWorkOrderId: number | null
boundPlatformOrderId: string
sourceType: 'manual' | 'collection_link' | 'recycle'
sourceType: 'manual' | 'worker_portal' | 'recycle'
sourceSubmissionId: number | null
workerId?: number | null
workerName?: string
workerPhone?: string
/** 被指派执行本次履约的打手(卖家之外的第三人);workerId 始终是卖家。 */
assignedWorkerId?: number | null
depositAmountFen?: number
inboundAt: string
consumedAt: string | null
@@ -38,36 +40,9 @@ export type ConsignmentInventory = {
updatedAt: string
}
export type ConsignmentCollectLink = {
id: number
token: string
title: string
gameName: string
categoryId: number | null
categoryName?: string
system: string
serverZone: string
series: string
diamondLevel: number
brillianceCount: number
presetPriceFen: number
inboundBatch: string
buyerName: string
templateJson: Record<string, unknown>
isMultiUse: boolean
expiresAt: string | null
enabled: boolean
createdBy: string
createdAt: string
updatedAt: string
submissionCount?: number
}
export type ConsignmentSubmission = {
id: number
submissionNo: string
linkId: number | null
linkTitle?: string
gameName: string
categoryId: number | null
categoryName?: string