feat(consignment): 寄售匹配成功后向卖家端与客服后台透出买家加好友资料
This commit is contained in:
@@ -41,6 +41,8 @@ export type ConsignmentInventoryRow = {
|
||||
gift_evidence_json?: string | Record<string, unknown> | null
|
||||
gift_work_order_no?: string | null
|
||||
gift_work_order_status?: string | null
|
||||
bound_work_order_material_json?: string | Record<string, unknown> | null
|
||||
bound_work_order_requirement_json?: string | Record<string, unknown> | null
|
||||
worker_id?: string | number | null
|
||||
worker_name?: string
|
||||
worker_phone?: string
|
||||
@@ -194,7 +196,9 @@ export async function listConsignmentInventories(filters: {
|
||||
cs.screenshots_json AS submission_screenshots_json,
|
||||
wo.acceptance_json AS gift_evidence_json,
|
||||
wo.work_order_no AS gift_work_order_no,
|
||||
wo.status AS gift_work_order_status
|
||||
wo.status AS gift_work_order_status,
|
||||
wo.material_json AS bound_work_order_material_json,
|
||||
wo.requirement_json AS bound_work_order_requirement_json
|
||||
FROM consignment_inventories ci
|
||||
LEFT JOIN work_categories wc ON wc.id = ci.category_id
|
||||
LEFT JOIN consignment_submissions cs ON cs.id = ci.source_submission_id
|
||||
@@ -309,7 +313,9 @@ export async function getConsignmentInventoryById(
|
||||
cs.screenshots_json AS submission_screenshots_json,
|
||||
wo.acceptance_json AS gift_evidence_json,
|
||||
wo.work_order_no AS gift_work_order_no,
|
||||
wo.status AS gift_work_order_status
|
||||
wo.status AS gift_work_order_status,
|
||||
wo.material_json AS bound_work_order_material_json,
|
||||
wo.requirement_json AS bound_work_order_requirement_json
|
||||
FROM consignment_inventories ci
|
||||
LEFT JOIN work_categories wc ON wc.id = ci.category_id
|
||||
LEFT JOIN consignment_submissions cs ON cs.id = ci.source_submission_id
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
releaseConsignmentPublishDepositBySubmissionWithClient,
|
||||
} from '../../repositories/worker-platform/consignment-deposit-repo.js'
|
||||
import { publishWorkerWalletRealtimeChange } from '../realtime/realtime-event-service.js'
|
||||
import { extractConsignmentBuyerInfo } from './work-order-material-mappers.js'
|
||||
|
||||
function throwConsignmentDepositHttpError(error: unknown, restock: boolean): never {
|
||||
const code = getConsignmentDepositErrorCode(error)
|
||||
@@ -192,6 +193,10 @@ export function mapConsignmentInventory(row: ConsignmentInventoryRow) {
|
||||
/** 唯一锚点:卖家端看到的「寄卖单号」就是它,客服端也用同一个号 */
|
||||
sourceSubmissionNo: row.source_submission_no || '',
|
||||
...mapConsignmentEvidence(row),
|
||||
buyerInfo: extractConsignmentBuyerInfo({
|
||||
materialJson: row.bound_work_order_material_json,
|
||||
requirementJson: row.bound_work_order_requirement_json,
|
||||
}),
|
||||
workerId: row.worker_id ? Number(row.worker_id) : null,
|
||||
workerName: row.worker_name || '',
|
||||
workerPhone: row.worker_phone || '',
|
||||
|
||||
@@ -23,6 +23,7 @@ import { getConsignmentPublishDepositFen as getConfiguredConsignmentPublishDepos
|
||||
import { confirmAdminWorkOrderFriendAdded } from './admin-work-order-gift-service.js'
|
||||
import { submitWorkerOrderAcceptance } from './worker-acceptance-service.js'
|
||||
import type { WorkerSession } from './worker-session-context-service.js'
|
||||
import { extractConsignmentBuyerInfoFromWorkOrder } from './work-order-material-mappers.js'
|
||||
|
||||
export function getConsignmentPublishDepositFen() {
|
||||
return getConfiguredConsignmentPublishDepositFen()
|
||||
@@ -276,6 +277,9 @@ export async function listWorkerConsignments(workerId: number) {
|
||||
const items = [
|
||||
...submissionsRes.items.map((sub) => {
|
||||
const inv = inventoryBySubmission.get(Number(sub.id))
|
||||
const boundWo = inv?.bound_work_order_id
|
||||
? workOrderById.get(Number(inv.bound_work_order_id))
|
||||
: undefined
|
||||
const stage = resolveStage(String(sub.status || ''), inv)
|
||||
return {
|
||||
/** 唯一锚点:提报单号贯穿发布→审核→在库→匹配→冷却→赠送→结算 */
|
||||
@@ -305,10 +309,14 @@ export async function listWorkerConsignments(workerId: number) {
|
||||
friendAddedAt: inv?.bound_work_order_id
|
||||
? (workOrderById.get(Number(inv.bound_work_order_id))?.friend_added_at ?? null)
|
||||
: null,
|
||||
buyerInfo: extractConsignmentBuyerInfoFromWorkOrder(boundWo),
|
||||
}
|
||||
}),
|
||||
...orphanInventories.map((inv) => {
|
||||
// 手动入库已停用:没有提报单的货不该再出现,这里只做兜底展示(不显示内部仓库号、不给操作能力)
|
||||
const boundWo = inv.bound_work_order_id
|
||||
? workOrderById.get(Number(inv.bound_work_order_id))
|
||||
: undefined
|
||||
const stage = resolveStage('approved', inv)
|
||||
return {
|
||||
consignmentNo: '客服手工入库',
|
||||
@@ -336,6 +344,7 @@ export async function listWorkerConsignments(workerId: number) {
|
||||
friendAddedAt: inv.bound_work_order_id
|
||||
? (workOrderById.get(Number(inv.bound_work_order_id))?.friend_added_at ?? null)
|
||||
: null,
|
||||
buyerInfo: extractConsignmentBuyerInfoFromWorkOrder(boundWo),
|
||||
}
|
||||
}),
|
||||
].sort((a, b) => Date.parse(String(b.createdAt || '')) - Date.parse(String(a.createdAt || '')))
|
||||
|
||||
@@ -9,6 +9,87 @@ import {
|
||||
normalizeSubmittedFields,
|
||||
normalizeUploadedFiles,
|
||||
} from './worker-input-normalizers.js'
|
||||
import { normalizeRequirementFields } from './work-order-requirement-fields.js'
|
||||
|
||||
export interface ConsignmentBuyerInfo {
|
||||
gameNickname: string
|
||||
gameId: string
|
||||
serverZone?: string
|
||||
system?: string
|
||||
fieldItems: Array<{ key: string; label: string; value: string }>
|
||||
screenshots: string[]
|
||||
}
|
||||
|
||||
export function extractConsignmentBuyerInfo(input: {
|
||||
materialJson: unknown
|
||||
requirementJson?: unknown
|
||||
}): ConsignmentBuyerInfo | null {
|
||||
const material = safeParseJson(input.materialJson)
|
||||
const collect = safeParseJson(material.collect)
|
||||
const fields = normalizeSubmittedFields(collect.fields || material.fields)
|
||||
const requirementFields = normalizeRequirementFields(safeParseJson(input.requirementJson).fields)
|
||||
|
||||
const gameNickname = String(
|
||||
fields.gameNickname || fields.roleName || fields.nickname || fields.game_nickname || '',
|
||||
).trim()
|
||||
const gameId = String(
|
||||
fields.gameId || fields.characterId || fields.uid || fields.game_id || '',
|
||||
).trim()
|
||||
const serverZone = String(fields.serverZone || fields.zone || fields.server_zone || '').trim()
|
||||
const system = String(fields.system || fields.os || '').trim()
|
||||
|
||||
const fieldItems: Array<{ key: string; label: string; value: string }> = []
|
||||
const usedKeys = new Set<string>()
|
||||
for (const req of requirementFields) {
|
||||
usedKeys.add(req.key)
|
||||
const val = String(fields[req.key] || '').trim()
|
||||
if (val) {
|
||||
fieldItems.push({ key: req.key, label: req.label, value: val })
|
||||
}
|
||||
}
|
||||
for (const [k, v] of Object.entries(fields)) {
|
||||
if (!usedKeys.has(k) && String(v || '').trim()) {
|
||||
fieldItems.push({ key: k, label: k, value: String(v).trim() })
|
||||
}
|
||||
}
|
||||
|
||||
const rawScreenshots = Array.isArray(collect.screenshots) ? collect.screenshots : []
|
||||
const screenshots = rawScreenshots
|
||||
.map((item) => {
|
||||
if (typeof item === 'string' && item.trim()) {
|
||||
return refreshStoredFileUrl(item.trim())
|
||||
}
|
||||
if (item && typeof item === 'object') {
|
||||
const norm = normalizeUploadedFiles([item])[0]
|
||||
return norm ? refreshUploadedFileUrls(norm)?.url : (item as { url?: string }).url || null
|
||||
}
|
||||
return null
|
||||
})
|
||||
.filter((url): url is string => typeof url === 'string' && url.length > 0)
|
||||
|
||||
if (!gameNickname && !gameId && fieldItems.length === 0 && screenshots.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
gameNickname,
|
||||
gameId,
|
||||
serverZone,
|
||||
system,
|
||||
fieldItems,
|
||||
screenshots,
|
||||
}
|
||||
}
|
||||
|
||||
export function extractConsignmentBuyerInfoFromWorkOrder(
|
||||
workOrder: WorkOrderRow | undefined | null,
|
||||
): ConsignmentBuyerInfo | null {
|
||||
if (!workOrder) return null
|
||||
return extractConsignmentBuyerInfo({
|
||||
materialJson: workOrder.material_json,
|
||||
requirementJson: workOrder.requirement_json,
|
||||
})
|
||||
}
|
||||
|
||||
export function mapAcceptanceForResponse(value: unknown) {
|
||||
const acceptance = safeParseJson(value)
|
||||
@@ -29,3 +110,4 @@ export function resolveCollectSubmittedFieldValues(workOrder: WorkOrderRow) {
|
||||
: {}
|
||||
return normalizeSubmittedFields(collect.fields)
|
||||
}
|
||||
|
||||
|
||||
@@ -1107,6 +1107,52 @@ export default function ConsignmentInventoryPanel() {
|
||||
<Text type="secondary">未匹配工单</Text>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
{viewingItem.buyerInfo ? (
|
||||
<Descriptions.Item label="匹配买家资料" span={2}>
|
||||
<Space direction="vertical" size={8} style={{ width: '100%' }}>
|
||||
<Space size={16} wrap>
|
||||
{viewingItem.buyerInfo.gameNickname ? (
|
||||
<span>
|
||||
买家角色名:
|
||||
<Text strong copyable>
|
||||
{viewingItem.buyerInfo.gameNickname}
|
||||
</Text>
|
||||
</span>
|
||||
) : null}
|
||||
{viewingItem.buyerInfo.gameId ? (
|
||||
<span>
|
||||
角色数字ID:
|
||||
<Text strong copyable>
|
||||
{viewingItem.buyerInfo.gameId}
|
||||
</Text>
|
||||
</span>
|
||||
) : null}
|
||||
{viewingItem.buyerInfo.serverZone ? (
|
||||
<Tag color="blue">{viewingItem.buyerInfo.serverZone}</Tag>
|
||||
) : null}
|
||||
{viewingItem.buyerInfo.system ? (
|
||||
<Tag color="cyan">{viewingItem.buyerInfo.system}</Tag>
|
||||
) : null}
|
||||
</Space>
|
||||
{viewingItem.buyerInfo.screenshots &&
|
||||
viewingItem.buyerInfo.screenshots.length > 0 ? (
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
买家主页截图(加好友核对用):
|
||||
</Text>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<ConsignmentEvidenceGallery
|
||||
images={viewingItem.buyerInfo.screenshots}
|
||||
emptyText="无买家截图"
|
||||
max={6}
|
||||
size={70}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
) : null}
|
||||
<Descriptions.Item label="登录方式">
|
||||
{(viewingItem.credentialsJson as any)?.loginMethod || '-'}
|
||||
</Descriptions.Item>
|
||||
|
||||
@@ -2,12 +2,15 @@ import {
|
||||
CheckCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloudUploadOutlined,
|
||||
CopyOutlined,
|
||||
DollarCircleOutlined,
|
||||
HistoryOutlined,
|
||||
InboxOutlined,
|
||||
PictureOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
@@ -912,6 +915,115 @@ function SpecTags({
|
||||
)
|
||||
}
|
||||
|
||||
function ConsignmentBuyerInfoBlock({
|
||||
buyerInfo,
|
||||
}: {
|
||||
buyerInfo: NonNullable<WorkerConsignmentRecord['buyerInfo']>
|
||||
}) {
|
||||
const { message } = App.useApp()
|
||||
|
||||
const copyText = (text: string, label: string) => {
|
||||
if (!text) return
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
.then(() => message.success(`已复制${label}:${text}`))
|
||||
.catch(() => message.error('复制失败,请手动长按复制'))
|
||||
}
|
||||
|
||||
const copyAll = () => {
|
||||
const parts: string[] = []
|
||||
if (buyerInfo.gameNickname) parts.push(`角色名: ${buyerInfo.gameNickname}`)
|
||||
if (buyerInfo.gameId) parts.push(`游戏ID: ${buyerInfo.gameId}`)
|
||||
if (buyerInfo.serverZone) parts.push(`区服: ${buyerInfo.serverZone}`)
|
||||
if (buyerInfo.system) parts.push(`系统: ${buyerInfo.system}`)
|
||||
copyText(parts.join(' | '), '买家加好友资料')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="worker-consignment-buyer-box">
|
||||
<div className="worker-consignment-buyer-header">
|
||||
<Space size={6}>
|
||||
<UserOutlined style={{ color: '#1677ff' }} />
|
||||
<Typography.Text strong style={{ color: '#1677ff', fontSize: 13 }}>
|
||||
买家加好友资料
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
style={{ padding: 0, height: 'auto', fontSize: 12 }}
|
||||
onClick={copyAll}
|
||||
>
|
||||
一键复制资料
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="worker-consignment-buyer-grid">
|
||||
{buyerInfo.gameNickname ? (
|
||||
<div className="buyer-field-cell">
|
||||
<span className="buyer-field-label">买家角色名</span>
|
||||
<Typography.Text
|
||||
strong
|
||||
copyable={{ text: buyerInfo.gameNickname }}
|
||||
className="buyer-field-highlight"
|
||||
>
|
||||
{buyerInfo.gameNickname}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{buyerInfo.gameId ? (
|
||||
<div className="buyer-field-cell">
|
||||
<span className="buyer-field-label">角色数字ID</span>
|
||||
<Typography.Text
|
||||
strong
|
||||
copyable={{ text: buyerInfo.gameId }}
|
||||
className="buyer-field-highlight"
|
||||
>
|
||||
{buyerInfo.gameId}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{buyerInfo.serverZone || buyerInfo.system ? (
|
||||
<div className="buyer-field-cell">
|
||||
<span className="buyer-field-label">区服与系统</span>
|
||||
<Space size={4} style={{ marginTop: 2 }}>
|
||||
{buyerInfo.system ? <Tag color="blue">{buyerInfo.system}</Tag> : null}
|
||||
{buyerInfo.serverZone ? <Tag color="cyan">{buyerInfo.serverZone}</Tag> : null}
|
||||
</Space>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{buyerInfo.screenshots && buyerInfo.screenshots.length > 0 ? (
|
||||
<div className="worker-consignment-buyer-shots-wrap">
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
<PictureOutlined style={{ marginRight: 4 }} />
|
||||
买家主页截图(在游戏内添加好友时核对头像/段位/等级,防加错人):
|
||||
</Typography.Text>
|
||||
<div style={{ marginTop: 6 }}>
|
||||
<Image.PreviewGroup>
|
||||
<Space size={8} wrap>
|
||||
{buyerInfo.screenshots.map((src: string, i: number) => (
|
||||
<Image
|
||||
key={`buyer-shot-${i}`}
|
||||
src={src}
|
||||
width={48}
|
||||
height={48}
|
||||
style={{ objectFit: 'cover', borderRadius: 6, border: '1px solid #bfdbfe' }}
|
||||
/>
|
||||
))}
|
||||
</Space>
|
||||
</Image.PreviewGroup>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 「我的寄卖」记录卡片:一件货一张卡。
|
||||
*
|
||||
@@ -968,6 +1080,7 @@ function ConsignmentRecordCard({
|
||||
{record.reviewNote ? (
|
||||
<Typography.Text type="danger">审核说明:{record.reviewNote}</Typography.Text>
|
||||
) : null}
|
||||
{record.buyerInfo ? <ConsignmentBuyerInfoBlock buyerInfo={record.buyerInfo} /> : null}
|
||||
{record.hint ? (
|
||||
<Typography.Text type="secondary" className="worker-consignment-record-hint">
|
||||
{record.hint}
|
||||
|
||||
@@ -23,6 +23,8 @@ import type {
|
||||
WorkerUser,
|
||||
WorkerWalletLedger,
|
||||
} from '@/types/worker-platform'
|
||||
import type { ConsignmentBuyerInfo } from '@/types/consignment'
|
||||
|
||||
import { getWorkerDeviceId } from '@/utils/worker-auth'
|
||||
|
||||
export function sendWorkerSmsCode(payload: { phone: string }) {
|
||||
@@ -445,8 +447,12 @@ export type WorkerConsignmentRecord = {
|
||||
/** 赠送冷却截止时间,前端拿它做倒计时 */
|
||||
giftAvailableAt: string | null
|
||||
friendAddedAt: string | null
|
||||
/** 匹配工单的买家资料(用于加好友) */
|
||||
buyerInfo?: ConsignmentBuyerInfo | null
|
||||
}
|
||||
|
||||
export type { ConsignmentBuyerInfo } from '@/types/consignment'
|
||||
|
||||
export type WorkerConsignmentListResponse = {
|
||||
publishDepositFen: number
|
||||
items: WorkerConsignmentRecord[]
|
||||
|
||||
@@ -4466,3 +4466,49 @@ body {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.worker-consignment-buyer-box {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: #eff6ff;
|
||||
border: 1px solid #bfdbfe;
|
||||
}
|
||||
|
||||
.worker-consignment-buyer-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.worker-consignment-buyer-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px 20px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.buyer-field-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.buyer-field-label {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.buyer-field-highlight {
|
||||
font-size: 14px;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.worker-consignment-buyer-shots-wrap {
|
||||
margin-top: 6px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px dashed #cbd5e1;
|
||||
}
|
||||
|
||||
|
||||
@@ -72,8 +72,20 @@ export type ConsignmentInventory = ConsignmentEvidenceFields & {
|
||||
consumedAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
/** 关联履约工单的买家资料(加好友及主页截图) */
|
||||
buyerInfo?: ConsignmentBuyerInfo | null
|
||||
}
|
||||
|
||||
export type ConsignmentBuyerInfo = {
|
||||
gameNickname: string
|
||||
gameId: string
|
||||
serverZone?: string
|
||||
system?: string
|
||||
fieldItems: Array<{ key: string; label: string; value: string }>
|
||||
screenshots: string[]
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 工单 `material.consignment` 里的寄售履约信息(履约/指派时后端写入)。
|
||||
* 历史老单没有 sourceSubmissionNo,展示时必须回退到 inventoryNo。
|
||||
|
||||
Reference in New Issue
Block a user