feat:寄卖增加对接客服,db脚本迁移修复
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
-- 115_work_order_share_cancel_reason_comment.sql
|
-- 111_work_order_share_cancel_reason_comment.sql
|
||||||
-- 「取消部分」把被撤销的份额标记为 admin_partial,校准 cancelled_reason 的枚举注释。
|
-- 「取消部分」把被撤销的份额标记为 admin_partial,校准 cancelled_reason 的枚举注释。
|
||||||
-- 纯注释变更,无结构/数据改动;重复执行无副作用。
|
-- 纯注释变更,无结构/数据改动;重复执行无副作用。
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
-- 112_consignment_contact_supporter.sql
|
||||||
|
-- 原临时编号 116;108–115 已收敛为 108–111,本补丁顺延为 112。
|
||||||
|
-- 寄卖「对接客服」独立名单:发布寄卖时必选,后台按人统计提成与快速联系。
|
||||||
|
-- 该名单不复用 admin_users,避免后台账号变动影响历史寄卖归属。
|
||||||
|
-- 全部语句幂等,可重复执行。
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS consignment_contact_supporters (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
phone TEXT NOT NULL DEFAULT '',
|
||||||
|
wechat TEXT NOT NULL DEFAULT '',
|
||||||
|
note TEXT NOT NULL DEFAULT '',
|
||||||
|
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 打手端下拉只取启用项,按 sort_order、id 排序,正好走这条索引
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_consignment_contact_supporters_order
|
||||||
|
ON consignment_contact_supporters (enabled, sort_order, id);
|
||||||
|
|
||||||
|
ALTER TABLE consignment_submissions
|
||||||
|
ADD COLUMN IF NOT EXISTS contact_supporter_id BIGINT
|
||||||
|
REFERENCES consignment_contact_supporters(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
ALTER TABLE consignment_inventories
|
||||||
|
ADD COLUMN IF NOT EXISTS contact_supporter_id BIGINT
|
||||||
|
REFERENCES consignment_contact_supporters(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_consignment_submissions_contact_supporter
|
||||||
|
ON consignment_submissions (contact_supporter_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_consignment_inventories_contact_supporter
|
||||||
|
ON consignment_inventories (contact_supporter_id);
|
||||||
|
|
||||||
|
-- 首次部署给一个可用默认项:打手端「必填 + 默认选第一个」不会因名单为空而无法发布。
|
||||||
|
-- 后台可直接改名/补充联系方式,不会重复插入。
|
||||||
|
INSERT INTO consignment_contact_supporters (name, note, sort_order)
|
||||||
|
SELECT '默认客服', '系统初始化,请在「配置 - 对接客服」中修改', 0
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM consignment_contact_supporters);
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import { query } from '../../db/client.js'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 寄卖「对接客服」名单。
|
||||||
|
*
|
||||||
|
* 独立于后台账号表:客服离职或后台账号改角色,都不应影响历史寄卖归属;
|
||||||
|
* 提报单 / 现货只保存名单 id,展示时 join 出当时选择的客服资料。
|
||||||
|
*/
|
||||||
|
export type ConsignmentContactRow = {
|
||||||
|
id: string | number
|
||||||
|
name: string
|
||||||
|
phone: string
|
||||||
|
wechat: string
|
||||||
|
note: string
|
||||||
|
enabled: boolean
|
||||||
|
sort_order: number
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listConsignmentContacts(filters: {
|
||||||
|
keyword?: string | undefined
|
||||||
|
enabled?: boolean | undefined
|
||||||
|
}): Promise<ConsignmentContactRow[]> {
|
||||||
|
const conditions: string[] = []
|
||||||
|
const values: unknown[] = []
|
||||||
|
|
||||||
|
if (filters.enabled !== undefined) {
|
||||||
|
values.push(filters.enabled)
|
||||||
|
conditions.push(`enabled = $${values.length}`)
|
||||||
|
}
|
||||||
|
if (filters.keyword) {
|
||||||
|
values.push(`%${filters.keyword.trim()}%`)
|
||||||
|
conditions.push(
|
||||||
|
`(name ILIKE $${values.length} OR phone ILIKE $${values.length} OR wechat ILIKE $${values.length} OR note ILIKE $${values.length})`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
||||||
|
const res = await query<ConsignmentContactRow>(
|
||||||
|
`SELECT * FROM consignment_contact_supporters ${whereClause} ORDER BY sort_order ASC, id ASC`,
|
||||||
|
values,
|
||||||
|
)
|
||||||
|
return res.rows
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打手端发布下拉:只返回启用中的客服。 */
|
||||||
|
export async function listEnabledConsignmentContacts(): Promise<ConsignmentContactRow[]> {
|
||||||
|
return listConsignmentContacts({ enabled: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getConsignmentContactById(
|
||||||
|
id: number | string,
|
||||||
|
): Promise<ConsignmentContactRow | null> {
|
||||||
|
const res = await query<ConsignmentContactRow>(
|
||||||
|
'SELECT * FROM consignment_contact_supporters WHERE id = $1 LIMIT 1',
|
||||||
|
[id],
|
||||||
|
)
|
||||||
|
return res.rows[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重名检查:名单用于归属统计,同名会让运营无法判断到底是谁。 */
|
||||||
|
export async function findConsignmentContactByName(
|
||||||
|
name: string,
|
||||||
|
excludeId?: number | string,
|
||||||
|
): Promise<ConsignmentContactRow | null> {
|
||||||
|
const res = await query<ConsignmentContactRow>(
|
||||||
|
`SELECT * FROM consignment_contact_supporters
|
||||||
|
WHERE name = $1 ${excludeId !== undefined ? 'AND id <> $2' : ''}
|
||||||
|
ORDER BY id ASC
|
||||||
|
LIMIT 1`,
|
||||||
|
excludeId !== undefined ? [name, excludeId] : [name],
|
||||||
|
)
|
||||||
|
return res.rows[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createConsignmentContact(input: {
|
||||||
|
name: string
|
||||||
|
phone?: string | undefined
|
||||||
|
wechat?: string | undefined
|
||||||
|
note?: string | undefined
|
||||||
|
enabled?: boolean | undefined
|
||||||
|
sortOrder?: number | undefined
|
||||||
|
now: string
|
||||||
|
}): Promise<ConsignmentContactRow> {
|
||||||
|
const res = await query<ConsignmentContactRow>(
|
||||||
|
`INSERT INTO consignment_contact_supporters (
|
||||||
|
name, phone, wechat, note, enabled, sort_order, created_at, updated_at
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $7)
|
||||||
|
RETURNING *`,
|
||||||
|
[
|
||||||
|
input.name,
|
||||||
|
input.phone || '',
|
||||||
|
input.wechat || '',
|
||||||
|
input.note || '',
|
||||||
|
input.enabled !== false,
|
||||||
|
input.sortOrder || 0,
|
||||||
|
input.now,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
return res.rows[0]!
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateConsignmentContact(
|
||||||
|
id: number | string,
|
||||||
|
input: {
|
||||||
|
name?: string | undefined
|
||||||
|
phone?: string | undefined
|
||||||
|
wechat?: string | undefined
|
||||||
|
note?: string | undefined
|
||||||
|
sortOrder?: number | undefined
|
||||||
|
now: string
|
||||||
|
},
|
||||||
|
): Promise<ConsignmentContactRow | null> {
|
||||||
|
const updates: string[] = []
|
||||||
|
const values: unknown[] = []
|
||||||
|
|
||||||
|
if (input.name !== undefined) {
|
||||||
|
values.push(input.name)
|
||||||
|
updates.push(`name = $${values.length}`)
|
||||||
|
}
|
||||||
|
if (input.phone !== undefined) {
|
||||||
|
values.push(input.phone)
|
||||||
|
updates.push(`phone = $${values.length}`)
|
||||||
|
}
|
||||||
|
if (input.wechat !== undefined) {
|
||||||
|
values.push(input.wechat)
|
||||||
|
updates.push(`wechat = $${values.length}`)
|
||||||
|
}
|
||||||
|
if (input.note !== undefined) {
|
||||||
|
values.push(input.note)
|
||||||
|
updates.push(`note = $${values.length}`)
|
||||||
|
}
|
||||||
|
if (input.sortOrder !== undefined) {
|
||||||
|
values.push(input.sortOrder)
|
||||||
|
updates.push(`sort_order = $${values.length}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates.length === 0) {
|
||||||
|
return getConsignmentContactById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
values.push(input.now)
|
||||||
|
updates.push(`updated_at = $${values.length}`)
|
||||||
|
values.push(id)
|
||||||
|
|
||||||
|
const res = await query<ConsignmentContactRow>(
|
||||||
|
`UPDATE consignment_contact_supporters
|
||||||
|
SET ${updates.join(', ')}
|
||||||
|
WHERE id = $${values.length}
|
||||||
|
RETURNING *`,
|
||||||
|
values,
|
||||||
|
)
|
||||||
|
return res.rows[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setConsignmentContactEnabled(
|
||||||
|
id: number | string,
|
||||||
|
enabled: boolean,
|
||||||
|
now: string,
|
||||||
|
): Promise<ConsignmentContactRow | null> {
|
||||||
|
const res = await query<ConsignmentContactRow>(
|
||||||
|
`UPDATE consignment_contact_supporters
|
||||||
|
SET enabled = $1, updated_at = $2
|
||||||
|
WHERE id = $3
|
||||||
|
RETURNING *`,
|
||||||
|
[enabled, now, id],
|
||||||
|
)
|
||||||
|
return res.rows[0] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除前统计引用:有历史寄卖时禁止硬删,避免历史单据的对接客服变成空白。 */
|
||||||
|
export async function countConsignmentContactReferences(id: number | string): Promise<number> {
|
||||||
|
const res = await query<{ total: number }>(
|
||||||
|
`SELECT (
|
||||||
|
(SELECT COUNT(*) FROM consignment_submissions WHERE contact_supporter_id = $1) +
|
||||||
|
(SELECT COUNT(*) FROM consignment_inventories WHERE contact_supporter_id = $1)
|
||||||
|
)::integer AS total`,
|
||||||
|
[id],
|
||||||
|
)
|
||||||
|
return Number(res.rows[0]?.total || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteConsignmentContact(id: number | string): Promise<boolean> {
|
||||||
|
const res = await query<{ id: string | number }>(
|
||||||
|
'DELETE FROM consignment_contact_supporters WHERE id = $1 RETURNING id',
|
||||||
|
[id],
|
||||||
|
)
|
||||||
|
return res.rows.length > 0
|
||||||
|
}
|
||||||
@@ -50,6 +50,11 @@ export type ConsignmentInventoryRow = {
|
|||||||
worker_phone?: string
|
worker_phone?: string
|
||||||
assigned_worker_id?: string | number | null
|
assigned_worker_id?: string | number | null
|
||||||
deposit_amount_fen?: number
|
deposit_amount_fen?: number
|
||||||
|
/** 对接客服(发布/审核入库时选定,用于提成归属与联系) */
|
||||||
|
contact_supporter_id?: string | number | null
|
||||||
|
contact_supporter_name?: string
|
||||||
|
contact_supporter_phone?: string
|
||||||
|
contact_supporter_wechat?: string
|
||||||
inbound_at: string
|
inbound_at: string
|
||||||
consumed_at: string | null
|
consumed_at: string | null
|
||||||
created_at: string
|
created_at: string
|
||||||
@@ -79,6 +84,11 @@ export type ConsignmentSubmissionRow = {
|
|||||||
worker_name?: string
|
worker_name?: string
|
||||||
worker_phone?: string
|
worker_phone?: string
|
||||||
deposit_amount_fen?: number
|
deposit_amount_fen?: number
|
||||||
|
/** 对接客服(发布时选定,用于提成归属与联系) */
|
||||||
|
contact_supporter_id?: string | number | null
|
||||||
|
contact_supporter_name?: string
|
||||||
|
contact_supporter_phone?: string
|
||||||
|
contact_supporter_wechat?: string
|
||||||
status: 'pending_review' | 'approved' | 'rejected'
|
status: 'pending_review' | 'approved' | 'rejected'
|
||||||
review_note: string
|
review_note: string
|
||||||
reviewer_name: string
|
reviewer_name: string
|
||||||
@@ -175,13 +185,13 @@ export async function listConsignmentInventories(filters: {
|
|||||||
if (filters.keyword) {
|
if (filters.keyword) {
|
||||||
values.push(`%${filters.keyword.trim()}%`)
|
values.push(`%${filters.keyword.trim()}%`)
|
||||||
conditions.push(
|
conditions.push(
|
||||||
`(ci.item_name ILIKE $${values.length} OR ci.inventory_no ILIKE $${values.length} OR ci.bound_platform_order_id ILIKE $${values.length} OR ci.seller_contact ILIKE $${values.length} OR ci.seller_game_id ILIKE $${values.length} OR ci.seller_payment_info ILIKE $${values.length} OR ci.remark ILIKE $${values.length} OR cs.submission_no ILIKE $${values.length})`,
|
`(ci.item_name ILIKE $${values.length} OR ci.inventory_no ILIKE $${values.length} OR ci.bound_platform_order_id ILIKE $${values.length} OR ci.seller_contact ILIKE $${values.length} OR ci.seller_game_id ILIKE $${values.length} OR ci.seller_payment_info ILIKE $${values.length} OR ci.remark ILIKE $${values.length} OR cs.submission_no ILIKE $${values.length} OR ccs.name ILIKE $${values.length})`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
||||||
// 计数也要带上提报单 join:关键字现在包含 cs.submission_no(卖家端主标识就是它)
|
// 计数也要带上提报单 / 对接客服 join:关键字现在包含 cs.submission_no 与 ccs.name
|
||||||
const countSql = `SELECT COUNT(*)::integer AS total FROM consignment_inventories ci LEFT JOIN consignment_submissions cs ON cs.id = ci.source_submission_id ${whereClause}`
|
const countSql = `SELECT COUNT(*)::integer AS total FROM consignment_inventories ci LEFT JOIN consignment_submissions cs ON cs.id = ci.source_submission_id LEFT JOIN consignment_contact_supporters ccs ON ccs.id = ci.contact_supporter_id ${whereClause}`
|
||||||
const countRes = await query<{ total: number }>(countSql, values)
|
const countRes = await query<{ total: number }>(countSql, values)
|
||||||
const total = countRes.rows[0]?.total || 0
|
const total = countRes.rows[0]?.total || 0
|
||||||
|
|
||||||
@@ -202,10 +212,14 @@ export async function listConsignmentInventories(filters: {
|
|||||||
wo.gift_phase AS gift_phase,
|
wo.gift_phase AS gift_phase,
|
||||||
wo.friend_added_at AS gift_friend_added_at,
|
wo.friend_added_at AS gift_friend_added_at,
|
||||||
wo.material_json AS bound_work_order_material_json,
|
wo.material_json AS bound_work_order_material_json,
|
||||||
wo.requirement_json AS bound_work_order_requirement_json
|
wo.requirement_json AS bound_work_order_requirement_json,
|
||||||
|
ccs.name AS contact_supporter_name,
|
||||||
|
ccs.phone AS contact_supporter_phone,
|
||||||
|
ccs.wechat AS contact_supporter_wechat
|
||||||
FROM consignment_inventories ci
|
FROM consignment_inventories ci
|
||||||
LEFT JOIN work_categories wc ON wc.id = ci.category_id
|
LEFT JOIN work_categories wc ON wc.id = ci.category_id
|
||||||
LEFT JOIN consignment_submissions cs ON cs.id = ci.source_submission_id
|
LEFT JOIN consignment_submissions cs ON cs.id = ci.source_submission_id
|
||||||
|
LEFT JOIN consignment_contact_supporters ccs ON ccs.id = ci.contact_supporter_id
|
||||||
LEFT JOIN work_orders wo ON wo.id = ci.bound_work_order_id
|
LEFT JOIN work_orders wo ON wo.id = ci.bound_work_order_id
|
||||||
${whereClause}
|
${whereClause}
|
||||||
ORDER BY ci.id DESC
|
ORDER BY ci.id DESC
|
||||||
@@ -321,10 +335,14 @@ export async function getConsignmentInventoryById(
|
|||||||
wo.gift_phase AS gift_phase,
|
wo.gift_phase AS gift_phase,
|
||||||
wo.friend_added_at AS gift_friend_added_at,
|
wo.friend_added_at AS gift_friend_added_at,
|
||||||
wo.material_json AS bound_work_order_material_json,
|
wo.material_json AS bound_work_order_material_json,
|
||||||
wo.requirement_json AS bound_work_order_requirement_json
|
wo.requirement_json AS bound_work_order_requirement_json,
|
||||||
|
ccs.name AS contact_supporter_name,
|
||||||
|
ccs.phone AS contact_supporter_phone,
|
||||||
|
ccs.wechat AS contact_supporter_wechat
|
||||||
FROM consignment_inventories ci
|
FROM consignment_inventories ci
|
||||||
LEFT JOIN work_categories wc ON wc.id = ci.category_id
|
LEFT JOIN work_categories wc ON wc.id = ci.category_id
|
||||||
LEFT JOIN consignment_submissions cs ON cs.id = ci.source_submission_id
|
LEFT JOIN consignment_submissions cs ON cs.id = ci.source_submission_id
|
||||||
|
LEFT JOIN consignment_contact_supporters ccs ON ccs.id = ci.contact_supporter_id
|
||||||
LEFT JOIN work_orders wo ON wo.id = ci.bound_work_order_id
|
LEFT JOIN work_orders wo ON wo.id = ci.bound_work_order_id
|
||||||
WHERE ci.id = $1
|
WHERE ci.id = $1
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
@@ -344,10 +362,14 @@ export async function listConsignmentInventoriesByIds(
|
|||||||
ci.*,
|
ci.*,
|
||||||
wc.name AS category_name,
|
wc.name AS category_name,
|
||||||
cs.submission_no AS source_submission_no,
|
cs.submission_no AS source_submission_no,
|
||||||
cs.screenshots_json AS submission_screenshots_json
|
cs.screenshots_json AS submission_screenshots_json,
|
||||||
|
ccs.name AS contact_supporter_name,
|
||||||
|
ccs.phone AS contact_supporter_phone,
|
||||||
|
ccs.wechat AS contact_supporter_wechat
|
||||||
FROM consignment_inventories ci
|
FROM consignment_inventories ci
|
||||||
LEFT JOIN work_categories wc ON wc.id = ci.category_id
|
LEFT JOIN work_categories wc ON wc.id = ci.category_id
|
||||||
LEFT JOIN consignment_submissions cs ON cs.id = ci.source_submission_id
|
LEFT JOIN consignment_submissions cs ON cs.id = ci.source_submission_id
|
||||||
|
LEFT JOIN consignment_contact_supporters ccs ON ccs.id = ci.contact_supporter_id
|
||||||
WHERE ci.id = ANY($1::bigint[])
|
WHERE ci.id = ANY($1::bigint[])
|
||||||
`
|
`
|
||||||
const res = await query<ConsignmentInventoryRow>(sql, [numericIds])
|
const res = await query<ConsignmentInventoryRow>(sql, [numericIds])
|
||||||
@@ -379,6 +401,7 @@ export async function updateConsignmentInventory(
|
|||||||
workerName?: string | undefined
|
workerName?: string | undefined
|
||||||
workerPhone?: string | undefined
|
workerPhone?: string | undefined
|
||||||
depositAmountFen?: number | undefined
|
depositAmountFen?: number | undefined
|
||||||
|
contactSupporterId?: number | null | undefined
|
||||||
now: string
|
now: string
|
||||||
},
|
},
|
||||||
): Promise<ConsignmentInventoryRow | null> {
|
): Promise<ConsignmentInventoryRow | null> {
|
||||||
@@ -473,6 +496,10 @@ export async function updateConsignmentInventory(
|
|||||||
values.push(input.depositAmountFen)
|
values.push(input.depositAmountFen)
|
||||||
updates.push(`deposit_amount_fen = $${values.length}`)
|
updates.push(`deposit_amount_fen = $${values.length}`)
|
||||||
}
|
}
|
||||||
|
if (input.contactSupporterId !== undefined) {
|
||||||
|
values.push(input.contactSupporterId)
|
||||||
|
updates.push(`contact_supporter_id = $${values.length}`)
|
||||||
|
}
|
||||||
|
|
||||||
if (updates.length === 0) return getConsignmentInventoryById(id)
|
if (updates.length === 0) return getConsignmentInventoryById(id)
|
||||||
|
|
||||||
@@ -484,10 +511,13 @@ export async function updateConsignmentInventory(
|
|||||||
UPDATE consignment_inventories
|
UPDATE consignment_inventories
|
||||||
SET ${updates.join(', ')}
|
SET ${updates.join(', ')}
|
||||||
WHERE id = $${values.length}
|
WHERE id = $${values.length}
|
||||||
RETURNING *
|
RETURNING id
|
||||||
`
|
`
|
||||||
const res = await query<ConsignmentInventoryRow>(sql, values)
|
const res = await query<{ id: string | number }>(sql, values)
|
||||||
return res.rows[0] || null
|
const updatedId = res.rows[0]?.id
|
||||||
|
// 重新走带 join 的详情查询:RETURNING * 拿不到对接客服姓名,
|
||||||
|
// 编辑保存后的即时回显会变成空白(列表刷新前)。
|
||||||
|
return updatedId === undefined ? null : getConsignmentInventoryById(updatedId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -880,9 +910,13 @@ export async function listActiveConsignmentInventoriesForMatching(options?: {
|
|||||||
const sql = `
|
const sql = `
|
||||||
SELECT
|
SELECT
|
||||||
ci.*,
|
ci.*,
|
||||||
cs.submission_no AS source_submission_no
|
cs.submission_no AS source_submission_no,
|
||||||
|
ccs.name AS contact_supporter_name,
|
||||||
|
ccs.phone AS contact_supporter_phone,
|
||||||
|
ccs.wechat AS contact_supporter_wechat
|
||||||
FROM consignment_inventories ci
|
FROM consignment_inventories ci
|
||||||
LEFT JOIN consignment_submissions cs ON cs.id = ci.source_submission_id
|
LEFT JOIN consignment_submissions cs ON cs.id = ci.source_submission_id
|
||||||
|
LEFT JOIN consignment_contact_supporters ccs ON ccs.id = ci.contact_supporter_id
|
||||||
WHERE ${conditions.join(' AND ')}
|
WHERE ${conditions.join(' AND ')}
|
||||||
ORDER BY ci.id ASC
|
ORDER BY ci.id ASC
|
||||||
`
|
`
|
||||||
@@ -913,12 +947,12 @@ export async function listConsignmentSubmissions(filters: {
|
|||||||
if (filters.keyword) {
|
if (filters.keyword) {
|
||||||
values.push(`%${filters.keyword.trim()}%`)
|
values.push(`%${filters.keyword.trim()}%`)
|
||||||
conditions.push(
|
conditions.push(
|
||||||
`(cs.item_name ILIKE $${values.length} OR cs.submission_no ILIKE $${values.length} OR cs.contact_info ILIKE $${values.length} OR cs.seller_game_id ILIKE $${values.length} OR cs.seller_phone ILIKE $${values.length})`,
|
`(cs.item_name ILIKE $${values.length} OR cs.submission_no ILIKE $${values.length} OR cs.contact_info ILIKE $${values.length} OR cs.seller_game_id ILIKE $${values.length} OR cs.seller_phone ILIKE $${values.length} OR ccs.name ILIKE $${values.length})`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
||||||
const countSql = `SELECT COUNT(*)::integer AS total FROM consignment_submissions cs ${whereClause}`
|
const countSql = `SELECT COUNT(*)::integer AS total FROM consignment_submissions cs LEFT JOIN consignment_contact_supporters ccs ON ccs.id = cs.contact_supporter_id ${whereClause}`
|
||||||
const countRes = await query<{ total: number }>(countSql, values)
|
const countRes = await query<{ total: number }>(countSql, values)
|
||||||
const total = countRes.rows[0]?.total || 0
|
const total = countRes.rows[0]?.total || 0
|
||||||
|
|
||||||
@@ -934,9 +968,13 @@ export async function listConsignmentSubmissions(filters: {
|
|||||||
wc.name AS category_name,
|
wc.name AS category_name,
|
||||||
wo.acceptance_json AS gift_evidence_json,
|
wo.acceptance_json AS gift_evidence_json,
|
||||||
wo.work_order_no AS gift_work_order_no,
|
wo.work_order_no AS gift_work_order_no,
|
||||||
wo.status AS gift_work_order_status
|
wo.status AS gift_work_order_status,
|
||||||
|
ccs.name AS contact_supporter_name,
|
||||||
|
ccs.phone AS contact_supporter_phone,
|
||||||
|
ccs.wechat AS contact_supporter_wechat
|
||||||
FROM consignment_submissions cs
|
FROM consignment_submissions cs
|
||||||
LEFT JOIN work_categories wc ON wc.id = cs.category_id
|
LEFT JOIN work_categories wc ON wc.id = cs.category_id
|
||||||
|
LEFT JOIN consignment_contact_supporters ccs ON ccs.id = cs.contact_supporter_id
|
||||||
LEFT JOIN consignment_inventories rinv ON (rinv.id = cs.result_inventory_id OR rinv.source_submission_id = cs.id)
|
LEFT JOIN consignment_inventories rinv ON (rinv.id = cs.result_inventory_id OR rinv.source_submission_id = cs.id)
|
||||||
LEFT JOIN work_orders wo ON wo.id = rinv.bound_work_order_id
|
LEFT JOIN work_orders wo ON wo.id = rinv.bound_work_order_id
|
||||||
${whereClause}
|
${whereClause}
|
||||||
@@ -1152,9 +1190,13 @@ export async function listConsignmentSubmissionsByIds(
|
|||||||
wc.name AS category_name,
|
wc.name AS category_name,
|
||||||
wo.acceptance_json AS gift_evidence_json,
|
wo.acceptance_json AS gift_evidence_json,
|
||||||
wo.work_order_no AS gift_work_order_no,
|
wo.work_order_no AS gift_work_order_no,
|
||||||
wo.status AS gift_work_order_status
|
wo.status AS gift_work_order_status,
|
||||||
|
ccs.name AS contact_supporter_name,
|
||||||
|
ccs.phone AS contact_supporter_phone,
|
||||||
|
ccs.wechat AS contact_supporter_wechat
|
||||||
FROM consignment_submissions cs
|
FROM consignment_submissions cs
|
||||||
LEFT JOIN work_categories wc ON wc.id = cs.category_id
|
LEFT JOIN work_categories wc ON wc.id = cs.category_id
|
||||||
|
LEFT JOIN consignment_contact_supporters ccs ON ccs.id = cs.contact_supporter_id
|
||||||
LEFT JOIN consignment_inventories rinv
|
LEFT JOIN consignment_inventories rinv
|
||||||
ON (rinv.id = cs.result_inventory_id OR rinv.source_submission_id = cs.id)
|
ON (rinv.id = cs.result_inventory_id OR rinv.source_submission_id = cs.id)
|
||||||
LEFT JOIN work_orders wo ON wo.id = rinv.bound_work_order_id
|
LEFT JOIN work_orders wo ON wo.id = rinv.bound_work_order_id
|
||||||
@@ -1179,10 +1221,14 @@ export async function getConsignmentInventoryBySubmissionId(
|
|||||||
SELECT
|
SELECT
|
||||||
ci.*,
|
ci.*,
|
||||||
wc.name AS category_name,
|
wc.name AS category_name,
|
||||||
cs.submission_no AS source_submission_no
|
cs.submission_no AS source_submission_no,
|
||||||
|
ccs.name AS contact_supporter_name,
|
||||||
|
ccs.phone AS contact_supporter_phone,
|
||||||
|
ccs.wechat AS contact_supporter_wechat
|
||||||
FROM consignment_inventories ci
|
FROM consignment_inventories ci
|
||||||
LEFT JOIN work_categories wc ON wc.id = ci.category_id
|
LEFT JOIN work_categories wc ON wc.id = ci.category_id
|
||||||
LEFT JOIN consignment_submissions cs ON cs.id = ci.source_submission_id
|
LEFT JOIN consignment_submissions cs ON cs.id = ci.source_submission_id
|
||||||
|
LEFT JOIN consignment_contact_supporters ccs ON ccs.id = ci.contact_supporter_id
|
||||||
WHERE ci.source_submission_id = $1
|
WHERE ci.source_submission_id = $1
|
||||||
ORDER BY ci.id DESC
|
ORDER BY ci.id DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
@@ -1201,9 +1247,13 @@ export async function getConsignmentSubmissionById(
|
|||||||
wc.name AS category_name,
|
wc.name AS category_name,
|
||||||
wo.acceptance_json AS gift_evidence_json,
|
wo.acceptance_json AS gift_evidence_json,
|
||||||
wo.work_order_no AS gift_work_order_no,
|
wo.work_order_no AS gift_work_order_no,
|
||||||
wo.status AS gift_work_order_status
|
wo.status AS gift_work_order_status,
|
||||||
|
ccs.name AS contact_supporter_name,
|
||||||
|
ccs.phone AS contact_supporter_phone,
|
||||||
|
ccs.wechat AS contact_supporter_wechat
|
||||||
FROM consignment_submissions cs
|
FROM consignment_submissions cs
|
||||||
LEFT JOIN work_categories wc ON wc.id = cs.category_id
|
LEFT JOIN work_categories wc ON wc.id = cs.category_id
|
||||||
|
LEFT JOIN consignment_contact_supporters ccs ON ccs.id = cs.contact_supporter_id
|
||||||
LEFT JOIN consignment_inventories rinv ON (rinv.id = cs.result_inventory_id OR rinv.source_submission_id = cs.id)
|
LEFT JOIN consignment_inventories rinv ON (rinv.id = cs.result_inventory_id OR rinv.source_submission_id = cs.id)
|
||||||
LEFT JOIN work_orders wo ON wo.id = rinv.bound_work_order_id
|
LEFT JOIN work_orders wo ON wo.id = rinv.bound_work_order_id
|
||||||
WHERE cs.id = $1
|
WHERE cs.id = $1
|
||||||
@@ -1234,6 +1284,7 @@ export async function createConsignmentSubmission(input: {
|
|||||||
workerName?: string
|
workerName?: string
|
||||||
workerPhone?: string
|
workerPhone?: string
|
||||||
depositAmountFen?: number
|
depositAmountFen?: number
|
||||||
|
contactSupporterId?: number | null
|
||||||
now: string
|
now: string
|
||||||
}): Promise<ConsignmentSubmissionRow> {
|
}): Promise<ConsignmentSubmissionRow> {
|
||||||
return withTransaction((client) => createConsignmentSubmissionWithClient(client, input))
|
return withTransaction((client) => createConsignmentSubmissionWithClient(client, input))
|
||||||
@@ -1262,6 +1313,7 @@ export async function createConsignmentSubmissionWithClient(
|
|||||||
workerName?: string
|
workerName?: string
|
||||||
workerPhone?: string
|
workerPhone?: string
|
||||||
depositAmountFen?: number
|
depositAmountFen?: number
|
||||||
|
contactSupporterId?: number | null
|
||||||
now: string
|
now: string
|
||||||
},
|
},
|
||||||
): Promise<ConsignmentSubmissionRow> {
|
): Promise<ConsignmentSubmissionRow> {
|
||||||
@@ -1272,14 +1324,14 @@ export async function createConsignmentSubmissionWithClient(
|
|||||||
seller_game_id, seller_payment_info, seller_phone, contact_info,
|
seller_game_id, seller_payment_info, seller_phone, contact_info,
|
||||||
content_json, screenshots_json, asking_price_fen,
|
content_json, screenshots_json, asking_price_fen,
|
||||||
worker_id, worker_name, worker_phone, deposit_amount_fen,
|
worker_id, worker_name, worker_phone, deposit_amount_fen,
|
||||||
status, created_at, updated_at
|
contact_supporter_id, status, created_at, updated_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4,
|
$1, $2, $3, $4,
|
||||||
$5, $6, $7, $8, $9,
|
$5, $6, $7, $8, $9,
|
||||||
$10, $11, $12, $13,
|
$10, $11, $12, $13,
|
||||||
$14::jsonb, $15::jsonb, $16,
|
$14::jsonb, $15::jsonb, $16,
|
||||||
$17, $18, $19, $20,
|
$17, $18, $19, $20,
|
||||||
'pending_review', $21, $21
|
$21, 'pending_review', $22, $22
|
||||||
)
|
)
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`
|
`
|
||||||
@@ -1304,6 +1356,7 @@ export async function createConsignmentSubmissionWithClient(
|
|||||||
input.workerName || '',
|
input.workerName || '',
|
||||||
input.workerPhone || '',
|
input.workerPhone || '',
|
||||||
input.depositAmountFen || 0,
|
input.depositAmountFen || 0,
|
||||||
|
input.contactSupporterId || null,
|
||||||
input.now,
|
input.now,
|
||||||
])
|
])
|
||||||
return res.rows[0]!
|
return res.rows[0]!
|
||||||
@@ -1371,6 +1424,7 @@ export async function approveConsignmentSubmissionWithInbound(input: {
|
|||||||
workerName?: string | undefined
|
workerName?: string | undefined
|
||||||
workerPhone?: string | undefined
|
workerPhone?: string | undefined
|
||||||
depositAmountFen?: number | undefined
|
depositAmountFen?: number | undefined
|
||||||
|
contactSupporterId?: number | null | undefined
|
||||||
now: string
|
now: string
|
||||||
}): Promise<{ submission: ConsignmentSubmissionRow; inventory: ConsignmentInventoryRow }> {
|
}): Promise<{ submission: ConsignmentSubmissionRow; inventory: ConsignmentInventoryRow }> {
|
||||||
return withTransaction(async (client: PoolClient) => {
|
return withTransaction(async (client: PoolClient) => {
|
||||||
@@ -1391,6 +1445,10 @@ export async function approveConsignmentSubmissionWithInbound(input: {
|
|||||||
input.depositAmountFen !== undefined
|
input.depositAmountFen !== undefined
|
||||||
? input.depositAmountFen
|
? input.depositAmountFen
|
||||||
: submission.deposit_amount_fen || 0
|
: submission.deposit_amount_fen || 0
|
||||||
|
const contactSupporterId =
|
||||||
|
input.contactSupporterId !== undefined
|
||||||
|
? input.contactSupporterId
|
||||||
|
: (submission.contact_supporter_id ?? null)
|
||||||
|
|
||||||
const invSql = `
|
const invSql = `
|
||||||
INSERT INTO consignment_inventories (
|
INSERT INTO consignment_inventories (
|
||||||
@@ -1400,7 +1458,7 @@ export async function approveConsignmentSubmissionWithInbound(input: {
|
|||||||
seller_phone, credentials_json, screenshots_json, remark,
|
seller_phone, credentials_json, screenshots_json, remark,
|
||||||
allow_matching, status, allocation_status, source_type,
|
allow_matching, status, allocation_status, source_type,
|
||||||
source_submission_id, worker_id, worker_name, worker_phone, deposit_amount_fen,
|
source_submission_id, worker_id, worker_name, worker_phone, deposit_amount_fen,
|
||||||
inbound_at, created_at, updated_at
|
contact_supporter_id, inbound_at, created_at, updated_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4, $5, $6,
|
$1, $2, $3, $4, $5, $6,
|
||||||
$7, $8, $9, $10, $11,
|
$7, $8, $9, $10, $11,
|
||||||
@@ -1408,7 +1466,7 @@ export async function approveConsignmentSubmissionWithInbound(input: {
|
|||||||
$16, $17::jsonb, $18::jsonb, $19,
|
$16, $17::jsonb, $18::jsonb, $19,
|
||||||
TRUE, 'in_stock', '待安排', 'worker_portal',
|
TRUE, 'in_stock', '待安排', 'worker_portal',
|
||||||
$20, $21, $22, $23, $24,
|
$20, $21, $22, $23, $24,
|
||||||
$25, $25, $25
|
$25, $26, $26, $26
|
||||||
)
|
)
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`
|
`
|
||||||
@@ -1437,6 +1495,7 @@ export async function approveConsignmentSubmissionWithInbound(input: {
|
|||||||
workerName,
|
workerName,
|
||||||
workerPhone,
|
workerPhone,
|
||||||
depositAmountFen,
|
depositAmountFen,
|
||||||
|
contactSupporterId,
|
||||||
input.now,
|
input.now,
|
||||||
]
|
]
|
||||||
const invRes = await client.query<ConsignmentInventoryRow>(invSql, invValues)
|
const invRes = await client.query<ConsignmentInventoryRow>(invSql, invValues)
|
||||||
|
|||||||
@@ -26,3 +26,4 @@ export * from './after-sales-repo.js'
|
|||||||
export * from './product-match-repo.js'
|
export * from './product-match-repo.js'
|
||||||
export * from './sms-code-repo.js'
|
export * from './sms-code-repo.js'
|
||||||
export * from './consignment-repo.js'
|
export * from './consignment-repo.js'
|
||||||
|
export * from './consignment-contact-repo.js'
|
||||||
|
|||||||
@@ -13,6 +13,13 @@ import {
|
|||||||
toggleAdminConsignmentMatching,
|
toggleAdminConsignmentMatching,
|
||||||
updateAdminConsignmentInventory,
|
updateAdminConsignmentInventory,
|
||||||
} from '../../../services/worker-platform/consignment-admin-service.js'
|
} from '../../../services/worker-platform/consignment-admin-service.js'
|
||||||
|
import {
|
||||||
|
createAdminConsignmentContact,
|
||||||
|
deleteAdminConsignmentContact,
|
||||||
|
fetchAdminConsignmentContacts,
|
||||||
|
setAdminConsignmentContactEnabled,
|
||||||
|
updateAdminConsignmentContact,
|
||||||
|
} from '../../../services/worker-platform/consignment-contact-service.js'
|
||||||
import {
|
import {
|
||||||
assignWorkOrderWithConsignmentWorker,
|
assignWorkOrderWithConsignmentWorker,
|
||||||
checkWorkOrderInventoryMatches,
|
checkWorkOrderInventoryMatches,
|
||||||
@@ -54,6 +61,64 @@ router.get(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ---------------- 对接客服名单 ----------------
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/worker-platform/consignment/contacts',
|
||||||
|
requireAdminPermission('consignment.view'),
|
||||||
|
createJsonHandler((req) => fetchAdminConsignmentContacts(req.query), {
|
||||||
|
successMessage: 'ok',
|
||||||
|
errorMessage: '读取对接客服名单失败',
|
||||||
|
scope: '[admin/consignment/contacts]',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/worker-platform/consignment/contacts',
|
||||||
|
requireAdminPermission('consignment.manage'),
|
||||||
|
createJsonHandler((req) => createAdminConsignmentContact(req.body || {}), {
|
||||||
|
successMessage: '对接客服已新增',
|
||||||
|
errorMessage: '新增对接客服失败',
|
||||||
|
scope: '[admin/consignment/contacts/create]',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
router.put(
|
||||||
|
'/worker-platform/consignment/contacts/:id',
|
||||||
|
requireAdminPermission('consignment.manage'),
|
||||||
|
createJsonHandler(
|
||||||
|
(req) => updateAdminConsignmentContact(String(req.params.id || ''), req.body || {}),
|
||||||
|
{
|
||||||
|
successMessage: '对接客服已更新',
|
||||||
|
errorMessage: '更新对接客服失败',
|
||||||
|
scope: '[admin/consignment/contacts/update]',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
'/worker-platform/consignment/contacts/:id/enabled',
|
||||||
|
requireAdminPermission('consignment.manage'),
|
||||||
|
createJsonHandler(
|
||||||
|
(req) => setAdminConsignmentContactEnabled(String(req.params.id || ''), req.body?.enabled),
|
||||||
|
{
|
||||||
|
successMessage: '对接客服状态已更新',
|
||||||
|
errorMessage: '更新对接客服状态失败',
|
||||||
|
scope: '[admin/consignment/contacts/enabled]',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
'/worker-platform/consignment/contacts/:id',
|
||||||
|
requireAdminPermission('consignment.manage'),
|
||||||
|
createJsonHandler((req) => deleteAdminConsignmentContact(String(req.params.id || '')), {
|
||||||
|
successMessage: '对接客服已删除',
|
||||||
|
errorMessage: '删除对接客服失败',
|
||||||
|
scope: '[admin/consignment/contacts/delete]',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/worker-platform/consignment/inventories/:id',
|
'/worker-platform/consignment/inventories/:id',
|
||||||
requireAdminPermission('consignment.view'),
|
requireAdminPermission('consignment.view'),
|
||||||
|
|||||||
@@ -7,11 +7,25 @@ import {
|
|||||||
publishWorkerConsignment,
|
publishWorkerConsignment,
|
||||||
submitWorkerConsignmentGiftEvidence,
|
submitWorkerConsignmentGiftEvidence,
|
||||||
} from '../../services/worker-platform/consignment-worker-service.js'
|
} from '../../services/worker-platform/consignment-worker-service.js'
|
||||||
|
import { fetchWorkerConsignmentContacts } from '../../services/worker-platform/consignment-contact-service.js'
|
||||||
import { sendRouteError } from '../../utils/http.js'
|
import { sendRouteError } from '../../utils/http.js'
|
||||||
import { getRequiredWorkerSession, requireActiveWorker } from './session.js'
|
import { getRequiredWorkerSession, requireActiveWorker } from './session.js'
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v1/worker/consignment/contacts
|
||||||
|
* 打手发布寄卖时的「对接客服」下拉:只返回启用中的名单
|
||||||
|
*/
|
||||||
|
router.get('/consignment/contacts', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const result = await fetchWorkerConsignmentContacts()
|
||||||
|
res.json({ ok: true, data: result })
|
||||||
|
} catch (error) {
|
||||||
|
sendRouteError(res, error, '获取对接客服失败', '[worker/consignment/contacts]')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /api/v1/worker/consignment/publish
|
* POST /api/v1/worker/consignment/publish
|
||||||
* 打手在接单平台直接提报苏尔南手枪寄卖
|
* 打手在接单平台直接提报苏尔南手枪寄卖
|
||||||
|
|||||||
@@ -226,6 +226,10 @@ export function mapConsignmentInventory(row: ConsignmentInventoryRow) {
|
|||||||
workerPhone: row.worker_phone || '',
|
workerPhone: row.worker_phone || '',
|
||||||
assignedWorkerId: row.assigned_worker_id ? Number(row.assigned_worker_id) : null,
|
assignedWorkerId: row.assigned_worker_id ? Number(row.assigned_worker_id) : null,
|
||||||
depositAmountFen: Number(row.deposit_amount_fen || 0),
|
depositAmountFen: Number(row.deposit_amount_fen || 0),
|
||||||
|
contactSupporterId: row.contact_supporter_id ? Number(row.contact_supporter_id) : null,
|
||||||
|
contactSupporterName: row.contact_supporter_name || '',
|
||||||
|
contactSupporterPhone: row.contact_supporter_phone || '',
|
||||||
|
contactSupporterWechat: row.contact_supporter_wechat || '',
|
||||||
inboundAt: row.inbound_at || row.created_at,
|
inboundAt: row.inbound_at || row.created_at,
|
||||||
consumedAt: row.consumed_at,
|
consumedAt: row.consumed_at,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
@@ -266,6 +270,10 @@ export function mapConsignmentSubmission(row: ConsignmentSubmissionRow) {
|
|||||||
workerName: row.worker_name || '',
|
workerName: row.worker_name || '',
|
||||||
workerPhone: row.worker_phone || '',
|
workerPhone: row.worker_phone || '',
|
||||||
depositAmountFen: Number(row.deposit_amount_fen || 0),
|
depositAmountFen: Number(row.deposit_amount_fen || 0),
|
||||||
|
contactSupporterId: row.contact_supporter_id ? Number(row.contact_supporter_id) : null,
|
||||||
|
contactSupporterName: row.contact_supporter_name || '',
|
||||||
|
contactSupporterPhone: row.contact_supporter_phone || '',
|
||||||
|
contactSupporterWechat: row.contact_supporter_wechat || '',
|
||||||
status: row.status,
|
status: row.status,
|
||||||
reviewNote: row.review_note || '',
|
reviewNote: row.review_note || '',
|
||||||
reviewerName: row.reviewer_name || '',
|
reviewerName: row.reviewer_name || '',
|
||||||
@@ -407,6 +415,12 @@ export async function updateAdminConsignmentInventory(
|
|||||||
workerPhone: payload.workerPhone !== undefined ? String(payload.workerPhone).trim() : undefined,
|
workerPhone: payload.workerPhone !== undefined ? String(payload.workerPhone).trim() : undefined,
|
||||||
depositAmountFen:
|
depositAmountFen:
|
||||||
payload.depositAmountFen !== undefined ? Number(payload.depositAmountFen) : undefined,
|
payload.depositAmountFen !== undefined ? Number(payload.depositAmountFen) : undefined,
|
||||||
|
contactSupporterId:
|
||||||
|
payload.contactSupporterId !== undefined
|
||||||
|
? payload.contactSupporterId
|
||||||
|
? Number(payload.contactSupporterId)
|
||||||
|
: null
|
||||||
|
: undefined,
|
||||||
now,
|
now,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -655,12 +669,25 @@ export async function approveAdminConsignmentSubmission(
|
|||||||
workerName,
|
workerName,
|
||||||
workerPhone,
|
workerPhone,
|
||||||
depositAmountFen,
|
depositAmountFen,
|
||||||
|
contactSupporterId:
|
||||||
|
payload.contactSupporterId !== undefined
|
||||||
|
? payload.contactSupporterId
|
||||||
|
? Number(payload.contactSupporterId)
|
||||||
|
: null
|
||||||
|
: undefined,
|
||||||
now,
|
now,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 入库 INSERT 的 RETURNING * 不带 join 出来的对接客服姓名,这里按 id 重读一次再出参,
|
||||||
|
// 避免审核成功后前端立即刷新时「对接客服」短暂显示为空。
|
||||||
|
const [joinedInventory, joinedSubmission] = await Promise.all([
|
||||||
|
getConsignmentInventoryById(res.inventory.id),
|
||||||
|
getConsignmentSubmissionById(id),
|
||||||
|
])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
submission: mapConsignmentSubmission(res.submission),
|
submission: mapConsignmentSubmission(joinedSubmission || res.submission),
|
||||||
inventory: mapConsignmentInventory(res.inventory),
|
inventory: mapConsignmentInventory(joinedInventory || res.inventory),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
import type { JsonObject } from '../../types/json.js'
|
||||||
|
import { createHttpError } from '../../utils/http.js'
|
||||||
|
import { nowIso } from '../../utils/time.js'
|
||||||
|
import {
|
||||||
|
countConsignmentContactReferences,
|
||||||
|
createConsignmentContact,
|
||||||
|
deleteConsignmentContact,
|
||||||
|
findConsignmentContactByName,
|
||||||
|
getConsignmentContactById,
|
||||||
|
listConsignmentContacts,
|
||||||
|
listEnabledConsignmentContacts,
|
||||||
|
setConsignmentContactEnabled,
|
||||||
|
updateConsignmentContact,
|
||||||
|
type ConsignmentContactRow,
|
||||||
|
} from '../../repositories/worker-platform/consignment-contact-repo.js'
|
||||||
|
|
||||||
|
export function mapConsignmentContact(row: ConsignmentContactRow) {
|
||||||
|
return {
|
||||||
|
id: Number(row.id),
|
||||||
|
name: row.name,
|
||||||
|
phone: row.phone || '',
|
||||||
|
wechat: row.wechat || '',
|
||||||
|
note: row.note || '',
|
||||||
|
enabled: Boolean(row.enabled),
|
||||||
|
sortOrder: Number(row.sort_order || 0),
|
||||||
|
createdAt: row.created_at,
|
||||||
|
updatedAt: row.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeText(value: unknown): string {
|
||||||
|
return String(value ?? '').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSortOrder(value: unknown): number {
|
||||||
|
const num = Number(value)
|
||||||
|
return Number.isFinite(num) ? Math.round(num) : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireContactName(value: unknown): string {
|
||||||
|
const name = normalizeText(value)
|
||||||
|
if (!name) {
|
||||||
|
throw createHttpError('请填写对接客服姓名', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'consignment_contact_name_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (name.length > 60) {
|
||||||
|
throw createHttpError('对接客服姓名不能超过 60 个字符', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'consignment_contact_name_too_long',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertContactNameAvailable(name: string, excludeId?: number | string) {
|
||||||
|
const duplicated = await findConsignmentContactByName(name, excludeId)
|
||||||
|
if (duplicated) {
|
||||||
|
throw createHttpError(`已存在同名对接客服「${name}」,请换一个名字`, {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'consignment_contact_name_duplicated',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 管理端:完整名单(含禁用),用于配置页。 */
|
||||||
|
export async function fetchAdminConsignmentContacts(query: JsonObject = {}) {
|
||||||
|
const keyword = normalizeText(query.keyword)
|
||||||
|
const enabledRaw = normalizeText(query.enabled)
|
||||||
|
const enabled = enabledRaw === 'true' ? true : enabledRaw === 'false' ? false : undefined
|
||||||
|
const rows = await listConsignmentContacts({
|
||||||
|
keyword: keyword || undefined,
|
||||||
|
enabled,
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
items: rows.map(mapConsignmentContact),
|
||||||
|
total: rows.length,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打手端:发布下拉只取启用项。 */
|
||||||
|
export async function fetchWorkerConsignmentContacts() {
|
||||||
|
const rows = await listEnabledConsignmentContacts()
|
||||||
|
return {
|
||||||
|
items: rows.map(mapConsignmentContact),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAdminConsignmentContact(payload: JsonObject) {
|
||||||
|
const name = requireContactName(payload.name)
|
||||||
|
await assertContactNameAvailable(name)
|
||||||
|
const row = await createConsignmentContact({
|
||||||
|
name,
|
||||||
|
phone: normalizeText(payload.phone),
|
||||||
|
wechat: normalizeText(payload.wechat),
|
||||||
|
note: normalizeText(payload.note),
|
||||||
|
enabled: payload.enabled !== false,
|
||||||
|
sortOrder: normalizeSortOrder(payload.sortOrder),
|
||||||
|
now: nowIso(),
|
||||||
|
})
|
||||||
|
return mapConsignmentContact(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateAdminConsignmentContact(id: number | string, payload: JsonObject) {
|
||||||
|
const current = await getConsignmentContactById(id)
|
||||||
|
if (!current) {
|
||||||
|
throw createHttpError('对接客服不存在', {
|
||||||
|
statusCode: 404,
|
||||||
|
errorCode: 'consignment_contact_not_found',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = payload.name !== undefined ? requireContactName(payload.name) : undefined
|
||||||
|
if (name !== undefined) {
|
||||||
|
await assertContactNameAvailable(name, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await updateConsignmentContact(id, {
|
||||||
|
name,
|
||||||
|
phone: payload.phone !== undefined ? normalizeText(payload.phone) : undefined,
|
||||||
|
wechat: payload.wechat !== undefined ? normalizeText(payload.wechat) : undefined,
|
||||||
|
note: payload.note !== undefined ? normalizeText(payload.note) : undefined,
|
||||||
|
sortOrder: payload.sortOrder !== undefined ? normalizeSortOrder(payload.sortOrder) : undefined,
|
||||||
|
now: nowIso(),
|
||||||
|
})
|
||||||
|
if (!updated) {
|
||||||
|
throw createHttpError('对接客服不存在', {
|
||||||
|
statusCode: 404,
|
||||||
|
errorCode: 'consignment_contact_not_found',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return mapConsignmentContact(updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setAdminConsignmentContactEnabled(
|
||||||
|
id: number | string,
|
||||||
|
enabledValue: unknown,
|
||||||
|
) {
|
||||||
|
if (typeof enabledValue !== 'boolean') {
|
||||||
|
throw createHttpError('启用状态必须是布尔值', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'consignment_contact_enabled_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const enabled = enabledValue
|
||||||
|
const current = await getConsignmentContactById(id)
|
||||||
|
if (!current) {
|
||||||
|
throw createHttpError('对接客服不存在', {
|
||||||
|
statusCode: 404,
|
||||||
|
errorCode: 'consignment_contact_not_found',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!enabled && current.enabled) {
|
||||||
|
const enabledRows = await listEnabledConsignmentContacts()
|
||||||
|
if (enabledRows.length <= 1) {
|
||||||
|
throw createHttpError('至少保留一个启用中的对接客服,否则打手无法发布寄卖', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'consignment_contact_last_enabled',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const updated = await setConsignmentContactEnabled(id, enabled, nowIso())
|
||||||
|
if (!updated) {
|
||||||
|
throw createHttpError('对接客服不存在', {
|
||||||
|
statusCode: 404,
|
||||||
|
errorCode: 'consignment_contact_not_found',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return mapConsignmentContact(updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteAdminConsignmentContact(id: number | string) {
|
||||||
|
const current = await getConsignmentContactById(id)
|
||||||
|
if (!current) {
|
||||||
|
throw createHttpError('对接客服不存在', {
|
||||||
|
statusCode: 404,
|
||||||
|
errorCode: 'consignment_contact_not_found',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const referenceCount = await countConsignmentContactReferences(id)
|
||||||
|
if (referenceCount > 0) {
|
||||||
|
throw createHttpError(
|
||||||
|
`该对接客服已被 ${referenceCount} 条寄卖记录使用,不能删除;请改为「禁用」,历史记录仍会保留。`,
|
||||||
|
{ statusCode: 409, errorCode: 'consignment_contact_in_use' },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current.enabled) {
|
||||||
|
const enabledRows = await listEnabledConsignmentContacts()
|
||||||
|
if (enabledRows.length <= 1) {
|
||||||
|
throw createHttpError('至少保留一个启用中的对接客服,请先新增并启用其他客服', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'consignment_contact_last_enabled',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleted = await deleteConsignmentContact(id)
|
||||||
|
if (!deleted) {
|
||||||
|
throw createHttpError('对接客服不存在', {
|
||||||
|
statusCode: 404,
|
||||||
|
errorCode: 'consignment_contact_not_found',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return { deleted: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发布寄卖时校验「对接客服」:必填 + 必须存在于启用名单。
|
||||||
|
* 返回整行,调用方取 id 落库即可。
|
||||||
|
*/
|
||||||
|
export async function resolveEnabledConsignmentContact(
|
||||||
|
contactSupporterId: unknown,
|
||||||
|
): Promise<ConsignmentContactRow> {
|
||||||
|
const id = Number(contactSupporterId)
|
||||||
|
if (!Number.isFinite(id) || id <= 0) {
|
||||||
|
throw createHttpError('请选择对接客服', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'consignment_contact_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const row = await getConsignmentContactById(id)
|
||||||
|
if (!row || !row.enabled) {
|
||||||
|
throw createHttpError('对接客服不存在或已停用,请刷新后重新选择', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'consignment_contact_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return row
|
||||||
|
}
|
||||||
@@ -507,6 +507,12 @@ export async function fulfillWorkOrderWithConsignment(input: {
|
|||||||
sellerGameId: inv.seller_game_id || '',
|
sellerGameId: inv.seller_game_id || '',
|
||||||
sellerContact: inv.seller_contact || '',
|
sellerContact: inv.seller_contact || '',
|
||||||
sellerPhone: inv.seller_phone || '',
|
sellerPhone: inv.seller_phone || '',
|
||||||
|
contactSupporterId: inventory.contact_supporter_id
|
||||||
|
? Number(inventory.contact_supporter_id)
|
||||||
|
: null,
|
||||||
|
contactSupporterName: inventory.contact_supporter_name || '',
|
||||||
|
contactSupporterPhone: inventory.contact_supporter_phone || '',
|
||||||
|
contactSupporterWechat: inventory.contact_supporter_wechat || '',
|
||||||
workerName: inv.worker_name || '',
|
workerName: inv.worker_name || '',
|
||||||
workerPhone: inv.worker_phone || '',
|
workerPhone: inv.worker_phone || '',
|
||||||
remark: inv.remark || '',
|
remark: inv.remark || '',
|
||||||
@@ -552,6 +558,10 @@ export async function fulfillWorkOrderWithConsignment(input: {
|
|||||||
priceAdjustReason: '寄卖内部现货履约按现货结算成本价更新发单金额',
|
priceAdjustReason: '寄卖内部现货履约按现货结算成本价更新发单金额',
|
||||||
buyerName: inv.buyer_name,
|
buyerName: inv.buyer_name,
|
||||||
shopName,
|
shopName,
|
||||||
|
contactSupporterId: inventory.contact_supporter_id
|
||||||
|
? Number(inventory.contact_supporter_id)
|
||||||
|
: null,
|
||||||
|
contactSupporterName: inventory.contact_supporter_name || '',
|
||||||
matchType:
|
matchType:
|
||||||
specMatch?.matchType || (input.allowSpecMismatch === true ? 'conflict' : 'exact'),
|
specMatch?.matchType || (input.allowSpecMismatch === true ? 'conflict' : 'exact'),
|
||||||
manualMatch: Boolean(matchNote) || input.allowSpecMismatch === true,
|
manualMatch: Boolean(matchNote) || input.allowSpecMismatch === true,
|
||||||
@@ -757,6 +767,12 @@ export async function assignWorkOrderWithConsignmentWorker(input: {
|
|||||||
sellerGameId: claimed.seller_game_id || '',
|
sellerGameId: claimed.seller_game_id || '',
|
||||||
sellerContact: claimed.seller_contact || '',
|
sellerContact: claimed.seller_contact || '',
|
||||||
sellerPhone: claimed.seller_phone || '',
|
sellerPhone: claimed.seller_phone || '',
|
||||||
|
contactSupporterId: inventory.contact_supporter_id
|
||||||
|
? Number(inventory.contact_supporter_id)
|
||||||
|
: null,
|
||||||
|
contactSupporterName: inventory.contact_supporter_name || '',
|
||||||
|
contactSupporterPhone: inventory.contact_supporter_phone || '',
|
||||||
|
contactSupporterWechat: inventory.contact_supporter_wechat || '',
|
||||||
workerId: Number(targetWorkerId),
|
workerId: Number(targetWorkerId),
|
||||||
workerName: claimed.worker_name || '',
|
workerName: claimed.worker_name || '',
|
||||||
workerPhone: claimed.worker_phone || '',
|
workerPhone: claimed.worker_phone || '',
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { listWorkOrdersByIds } from '../../repositories/worker-platform/work-ord
|
|||||||
import type { WorkOrderRow } from '../../repositories/worker-platform/types.js'
|
import type { WorkOrderRow } from '../../repositories/worker-platform/types.js'
|
||||||
import { deriveConsignmentItemStage } from '../../domain/consignment-item-stage.js'
|
import { deriveConsignmentItemStage } from '../../domain/consignment-item-stage.js'
|
||||||
import { canWorkerPublishConsignment } from '../../domain/consignment-whitelist.js'
|
import { canWorkerPublishConsignment } from '../../domain/consignment-whitelist.js'
|
||||||
|
import { resolveEnabledConsignmentContact } from './consignment-contact-service.js'
|
||||||
import { suggestConsignmentPriceFromRules } from './consignment-matching-service.js'
|
import { suggestConsignmentPriceFromRules } from './consignment-matching-service.js'
|
||||||
import { hasInternalConsignmentFulfillment } from '../../repositories/worker-platform/work-order-gift-repo.js'
|
import { hasInternalConsignmentFulfillment } from '../../repositories/worker-platform/work-order-gift-repo.js'
|
||||||
import type { JsonObject } from '../../types/json.js'
|
import type { JsonObject } from '../../types/json.js'
|
||||||
@@ -43,6 +44,21 @@ function toRecord(value: unknown): Record<string, unknown> | null {
|
|||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 提报单 / 现货都可能带对接客服信息,统一成前端字段。 */
|
||||||
|
function mapConsignmentContactSupporter(source: {
|
||||||
|
contact_supporter_id?: string | number | null
|
||||||
|
contact_supporter_name?: string
|
||||||
|
contact_supporter_phone?: string
|
||||||
|
contact_supporter_wechat?: string
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
contactSupporterId: source.contact_supporter_id ? Number(source.contact_supporter_id) : null,
|
||||||
|
contactSupporterName: source.contact_supporter_name || '',
|
||||||
|
contactSupporterPhone: source.contact_supporter_phone || '',
|
||||||
|
contactSupporterWechat: source.contact_supporter_wechat || '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function extractConsignmentGiftEvidenceFromWorkOrder(boundWo?: WorkOrderRow | null) {
|
function extractConsignmentGiftEvidenceFromWorkOrder(boundWo?: WorkOrderRow | null) {
|
||||||
if (!boundWo) {
|
if (!boundWo) {
|
||||||
return {
|
return {
|
||||||
@@ -162,6 +178,9 @@ export async function publishWorkerConsignment(workerId: number, payload: JsonOb
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 对接客服必填:发布时选定,后续按人统计提成 / 方便联系。
|
||||||
|
const contactSupporter = await resolveEnabledConsignmentContact(payload.contactSupporterId)
|
||||||
|
|
||||||
const system = String(payload.system || '').trim()
|
const system = String(payload.system || '').trim()
|
||||||
const serverZone = String(payload.serverZone || '').trim()
|
const serverZone = String(payload.serverZone || '').trim()
|
||||||
const series = String(payload.series || '').trim()
|
const series = String(payload.series || '').trim()
|
||||||
@@ -298,6 +317,7 @@ export async function publishWorkerConsignment(workerId: number, payload: JsonOb
|
|||||||
workerName: worker.display_name || worker.username,
|
workerName: worker.display_name || worker.username,
|
||||||
workerPhone: sellerPhone || worker.phone || '',
|
workerPhone: sellerPhone || worker.phone || '',
|
||||||
depositAmountFen,
|
depositAmountFen,
|
||||||
|
contactSupporterId: Number(contactSupporter.id),
|
||||||
now,
|
now,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -330,6 +350,12 @@ export async function publishWorkerConsignment(workerId: number, payload: JsonOb
|
|||||||
workerName: submission.worker_name,
|
workerName: submission.worker_name,
|
||||||
workerPhone: submission.worker_phone,
|
workerPhone: submission.worker_phone,
|
||||||
depositAmountFen: submission.deposit_amount_fen,
|
depositAmountFen: submission.deposit_amount_fen,
|
||||||
|
contactSupporterId: submission.contact_supporter_id
|
||||||
|
? Number(submission.contact_supporter_id)
|
||||||
|
: null,
|
||||||
|
contactSupporterName: contactSupporter.name,
|
||||||
|
contactSupporterPhone: contactSupporter.phone || '',
|
||||||
|
contactSupporterWechat: contactSupporter.wechat || '',
|
||||||
createdAt: submission.created_at,
|
createdAt: submission.created_at,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -438,6 +464,8 @@ export async function listWorkerConsignments(
|
|||||||
const evidence = showingMatchedBuyer
|
const evidence = showingMatchedBuyer
|
||||||
? extractConsignmentGiftEvidenceFromWorkOrder(boundWo)
|
? extractConsignmentGiftEvidenceFromWorkOrder(boundWo)
|
||||||
: { giftEvidenceImages: [], giftEvidenceNote: '', giftEvidenceSubmittedAt: '' }
|
: { giftEvidenceImages: [], giftEvidenceNote: '', giftEvidenceSubmittedAt: '' }
|
||||||
|
// 现货上的对接客服优先级更高:客服可能在审核入库后改派对接人,卖家端应看到最新归属。
|
||||||
|
const contactSupporter = inv?.contact_supporter_id ? inv : sub
|
||||||
return {
|
return {
|
||||||
/** 唯一锚点:提报单号贯穿发布→审核→在库→匹配→冷却→赠送→结算 */
|
/** 唯一锚点:提报单号贯穿发布→审核→在库→匹配→冷却→赠送→结算 */
|
||||||
consignmentNo: sub.submission_no,
|
consignmentNo: sub.submission_no,
|
||||||
@@ -452,6 +480,7 @@ export async function listWorkerConsignments(
|
|||||||
brillianceCount: sub.brilliance_count,
|
brillianceCount: sub.brilliance_count,
|
||||||
askingPriceFen: sub.asking_price_fen,
|
askingPriceFen: sub.asking_price_fen,
|
||||||
depositAmountFen: sub.deposit_amount_fen,
|
depositAmountFen: sub.deposit_amount_fen,
|
||||||
|
...mapConsignmentContactSupporter(contactSupporter),
|
||||||
reviewNote: sub.review_note,
|
reviewNote: sub.review_note,
|
||||||
// 库里存的是入库时的签名链接(15 分钟有效),出参必须重签,否则卖家自己回头看也是裂图
|
// 库里存的是入库时的签名链接(15 分钟有效),出参必须重签,否则卖家自己回头看也是裂图
|
||||||
screenshotsJson: (Array.isArray(sub.screenshots_json) ? sub.screenshots_json : [])
|
screenshotsJson: (Array.isArray(sub.screenshots_json) ? sub.screenshots_json : [])
|
||||||
@@ -496,6 +525,7 @@ export async function listWorkerConsignments(
|
|||||||
brillianceCount: inv.brilliance_count,
|
brillianceCount: inv.brilliance_count,
|
||||||
askingPriceFen: inv.cost_price_fen,
|
askingPriceFen: inv.cost_price_fen,
|
||||||
depositAmountFen: inv.deposit_amount_fen,
|
depositAmountFen: inv.deposit_amount_fen,
|
||||||
|
...mapConsignmentContactSupporter(inv),
|
||||||
reviewNote: '',
|
reviewNote: '',
|
||||||
screenshotsJson: [],
|
screenshotsJson: [],
|
||||||
giftEvidenceImages: evidence.giftEvidenceImages,
|
giftEvidenceImages: evidence.giftEvidenceImages,
|
||||||
|
|||||||
@@ -62,3 +62,4 @@ export * from './worker-announcement-config-service.js'
|
|||||||
export * from './worker-hall-config-service.js'
|
export * from './worker-hall-config-service.js'
|
||||||
export * from './worker-acceptance-config-service.js'
|
export * from './worker-acceptance-config-service.js'
|
||||||
export * from './sync-worker-orders-service.js'
|
export * from './sync-worker-orders-service.js'
|
||||||
|
export * from './consignment-contact-service.js'
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ const AdminFinancePage = lazy(() => import('@/pages/admin/AdminFinancePage'))
|
|||||||
const AdminFinanceLedgerPage = lazy(() => import('@/pages/admin/AdminFinanceLedgerPage'))
|
const AdminFinanceLedgerPage = lazy(() => import('@/pages/admin/AdminFinanceLedgerPage'))
|
||||||
const AdminSalesProfitPage = lazy(() => import('@/pages/admin/AdminSalesProfitPage'))
|
const AdminSalesProfitPage = lazy(() => import('@/pages/admin/AdminSalesProfitPage'))
|
||||||
const AdminCostCenterPage = lazy(() => import('@/pages/admin/AdminCostCenterPage'))
|
const AdminCostCenterPage = lazy(() => import('@/pages/admin/AdminCostCenterPage'))
|
||||||
|
const AdminConsignmentContactsPage = lazy(
|
||||||
|
() => import('@/pages/admin/AdminConsignmentContactsPage'),
|
||||||
|
)
|
||||||
const AdminFundPage = lazy(() => import('@/pages/admin/AdminFundPage'))
|
const AdminFundPage = lazy(() => import('@/pages/admin/AdminFundPage'))
|
||||||
const AdminAuditLogsPage = lazy(() => import('@/pages/admin/AdminAuditLogsPage'))
|
const AdminAuditLogsPage = lazy(() => import('@/pages/admin/AdminAuditLogsPage'))
|
||||||
const AdminKuaishouIndustryPage = lazy(() => import('@/pages/admin/AdminKuaishouIndustryPage'))
|
const AdminKuaishouIndustryPage = lazy(() => import('@/pages/admin/AdminKuaishouIndustryPage'))
|
||||||
@@ -166,6 +169,7 @@ function AdminApplication() {
|
|||||||
</Route>
|
</Route>
|
||||||
<Route element={<RequireRole roles={['admin']} />}>
|
<Route element={<RequireRole roles={['admin']} />}>
|
||||||
<Route path="users" element={<AdminUsersPage />} />
|
<Route path="users" element={<AdminUsersPage />} />
|
||||||
|
<Route path="consignment-contacts" element={<AdminConsignmentContactsPage />} />
|
||||||
<Route path="platform-shops" element={<AdminPlatformShopsPage />} />
|
<Route path="platform-shops" element={<AdminPlatformShopsPage />} />
|
||||||
<Route path="platform-fulfillment" element={<AdminPlatformFulfillmentPage />} />
|
<Route path="platform-fulfillment" element={<AdminPlatformFulfillmentPage />} />
|
||||||
<Route path="audit-logs" element={<AdminAuditLogsPage />} />
|
<Route path="audit-logs" element={<AdminAuditLogsPage />} />
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
resolveManualMatchRequirement,
|
resolveManualMatchRequirement,
|
||||||
type ManualMatchType,
|
type ManualMatchType,
|
||||||
} from '@/utils/consignment-manual-match'
|
} from '@/utils/consignment-manual-match'
|
||||||
|
import { formatConsignmentContactSupporter } from '@/utils/consignment-contact'
|
||||||
import { formatDateTime } from '@/utils/date-time'
|
import { formatDateTime } from '@/utils/date-time'
|
||||||
import { notifyVoucherConsume } from '@/utils/voucher-consume'
|
import { notifyVoucherConsume } from '@/utils/voucher-consume'
|
||||||
|
|
||||||
@@ -45,6 +46,10 @@ type Selected = {
|
|||||||
/** 供货打手(用于回显供货来源) */
|
/** 供货打手(用于回显供货来源) */
|
||||||
workerId?: number | null
|
workerId?: number | null
|
||||||
workerName?: string
|
workerName?: string
|
||||||
|
/** 对接客服(用于履约前核对归属) */
|
||||||
|
contactSupporterName?: string
|
||||||
|
contactSupporterPhone?: string
|
||||||
|
contactSupporterWechat?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConsignmentManualMatchModalProps = {
|
export type ConsignmentManualMatchModalProps = {
|
||||||
@@ -79,6 +84,11 @@ export default function ConsignmentManualMatchModal({
|
|||||||
workerId: number | null
|
workerId: number | null
|
||||||
workerName: string
|
workerName: string
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
const [inventoryContact, setInventoryContact] = useState<{
|
||||||
|
contactSupporterName: string
|
||||||
|
contactSupporterPhone: string
|
||||||
|
contactSupporterWechat: string
|
||||||
|
} | null>(null)
|
||||||
const [selected, setSelected] = useState<Selected | null>(null)
|
const [selected, setSelected] = useState<Selected | null>(null)
|
||||||
const [confirmedPartial, setConfirmedPartial] = useState(false)
|
const [confirmedPartial, setConfirmedPartial] = useState(false)
|
||||||
const [confirmedMismatch, setConfirmedMismatch] = useState(false)
|
const [confirmedMismatch, setConfirmedMismatch] = useState(false)
|
||||||
@@ -94,6 +104,7 @@ export default function ConsignmentManualMatchModal({
|
|||||||
setConfirmedMismatch(false)
|
setConfirmedMismatch(false)
|
||||||
setMatchNote('')
|
setMatchNote('')
|
||||||
setInventoryInboundAt('')
|
setInventoryInboundAt('')
|
||||||
|
setInventoryContact(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
@@ -116,6 +127,11 @@ export default function ConsignmentManualMatchModal({
|
|||||||
workerId: res.data?.inventory?.workerId ?? null,
|
workerId: res.data?.inventory?.workerId ?? null,
|
||||||
workerName: res.data?.inventory?.workerName || '',
|
workerName: res.data?.inventory?.workerName || '',
|
||||||
})
|
})
|
||||||
|
setInventoryContact({
|
||||||
|
contactSupporterName: res.data?.inventory?.contactSupporterName || '',
|
||||||
|
contactSupporterPhone: res.data?.inventory?.contactSupporterPhone || '',
|
||||||
|
contactSupporterWechat: res.data?.inventory?.contactSupporterWechat || '',
|
||||||
|
})
|
||||||
} else if (workOrder) {
|
} else if (workOrder) {
|
||||||
const res = await fetchWorkOrderConsignmentMatchOptions(workOrder.workOrderId)
|
const res = await fetchWorkOrderConsignmentMatchOptions(workOrder.workOrderId)
|
||||||
setOptions(res.data?.options || [])
|
setOptions(res.data?.options || [])
|
||||||
@@ -319,7 +335,8 @@ export default function ConsignmentManualMatchModal({
|
|||||||
? `${option.inventory.diamondLevel}钻`
|
? `${option.inventory.diamondLevel}钻`
|
||||||
: ''}{' '}
|
: ''}{' '}
|
||||||
· 货源 {option.inventory.workerName || '未关联打手'} · 结算价 ¥
|
· 货源 {option.inventory.workerName || '未关联打手'} · 结算价 ¥
|
||||||
{(Number(option.inventory.costPriceFen || 0) / 100).toFixed(2)}
|
{(Number(option.inventory.costPriceFen || 0) / 100).toFixed(2)} ·
|
||||||
|
对接客服 {formatConsignmentContactSupporter(option.inventory).text}
|
||||||
{option.inventory.inboundAt || option.inventory.createdAt ? (
|
{option.inventory.inboundAt || option.inventory.createdAt ? (
|
||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
@@ -356,6 +373,9 @@ export default function ConsignmentManualMatchModal({
|
|||||||
conflicts: option.conflicts,
|
conflicts: option.conflicts,
|
||||||
workerId: option.inventory.workerId,
|
workerId: option.inventory.workerId,
|
||||||
workerName: option.inventory.workerName,
|
workerName: option.inventory.workerName,
|
||||||
|
contactSupporterName: option.inventory.contactSupporterName,
|
||||||
|
contactSupporterPhone: option.inventory.contactSupporterPhone,
|
||||||
|
contactSupporterWechat: option.inventory.contactSupporterWechat,
|
||||||
})
|
})
|
||||||
setConfirmedPartial(false)
|
setConfirmedPartial(false)
|
||||||
setConfirmedMismatch(false)
|
setConfirmedMismatch(false)
|
||||||
@@ -439,6 +459,9 @@ export default function ConsignmentManualMatchModal({
|
|||||||
conflicts: order.conflicts,
|
conflicts: order.conflicts,
|
||||||
workerId: inventoryWorker?.workerId ?? null,
|
workerId: inventoryWorker?.workerId ?? null,
|
||||||
workerName: inventoryWorker?.workerName || '',
|
workerName: inventoryWorker?.workerName || '',
|
||||||
|
contactSupporterName: inventoryContact?.contactSupporterName || '',
|
||||||
|
contactSupporterPhone: inventoryContact?.contactSupporterPhone || '',
|
||||||
|
contactSupporterWechat: inventoryContact?.contactSupporterWechat || '',
|
||||||
})
|
})
|
||||||
setConfirmedPartial(false)
|
setConfirmedPartial(false)
|
||||||
setConfirmedMismatch(false)
|
setConfirmedMismatch(false)
|
||||||
@@ -492,6 +515,9 @@ export default function ConsignmentManualMatchModal({
|
|||||||
<Tag color="default">内部库存现货(无打手关联)</Tag>
|
<Tag color="default">内部库存现货(无打手关联)</Tag>
|
||||||
)}
|
)}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="对接客服">
|
||||||
|
{formatConsignmentContactSupporter(selected).text}
|
||||||
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="匹配判定">
|
<Descriptions.Item label="匹配判定">
|
||||||
<Tag color={describeManualMatchType(selected.matchType).color}>
|
<Tag color={describeManualMatchType(selected.matchType).color}>
|
||||||
{describeManualMatchType(selected.matchType).label}
|
{describeManualMatchType(selected.matchType).label}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
ApartmentOutlined,
|
ApartmentOutlined,
|
||||||
BellOutlined,
|
BellOutlined,
|
||||||
BugOutlined,
|
BugOutlined,
|
||||||
|
CustomerServiceOutlined,
|
||||||
DashboardOutlined,
|
DashboardOutlined,
|
||||||
FileSearchOutlined,
|
FileSearchOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
@@ -282,6 +283,11 @@ export default function AdminLayout() {
|
|||||||
label: '配置',
|
label: '配置',
|
||||||
children: [
|
children: [
|
||||||
{ key: '/admin/users', icon: <TeamOutlined />, label: '后台用户' },
|
{ key: '/admin/users', icon: <TeamOutlined />, label: '后台用户' },
|
||||||
|
{
|
||||||
|
key: '/admin/consignment-contacts',
|
||||||
|
icon: <CustomerServiceOutlined />,
|
||||||
|
label: '对接客服',
|
||||||
|
},
|
||||||
{ key: '/admin/platform-shops', icon: <SettingOutlined />, label: '平台配置' },
|
{ key: '/admin/platform-shops', icon: <SettingOutlined />, label: '平台配置' },
|
||||||
{ key: '/admin/platform-fulfillment', icon: <ShopOutlined />, label: '履约配置' },
|
{ key: '/admin/platform-fulfillment', icon: <ShopOutlined />, label: '履约配置' },
|
||||||
],
|
],
|
||||||
@@ -850,6 +856,7 @@ function resolveSelectedKey(pathname: string) {
|
|||||||
if (pathname.startsWith('/admin/finance')) return '/admin/finance'
|
if (pathname.startsWith('/admin/finance')) return '/admin/finance'
|
||||||
if (pathname.startsWith('/admin/tasks')) return '/admin/tasks'
|
if (pathname.startsWith('/admin/tasks')) return '/admin/tasks'
|
||||||
if (pathname.startsWith('/admin/worker-platform')) return '/admin/worker-platform'
|
if (pathname.startsWith('/admin/worker-platform')) return '/admin/worker-platform'
|
||||||
|
if (pathname.startsWith('/admin/consignment-contacts')) return '/admin/consignment-contacts'
|
||||||
if (pathname.startsWith('/admin/kuaishou-industry')) return '/admin/kuaishou-industry'
|
if (pathname.startsWith('/admin/kuaishou-industry')) return '/admin/kuaishou-industry'
|
||||||
if (pathname === '/admin/platform-shops') return '/admin/platform-shops'
|
if (pathname === '/admin/platform-shops') return '/admin/platform-shops'
|
||||||
return pathname
|
return pathname
|
||||||
|
|||||||
@@ -0,0 +1,343 @@
|
|||||||
|
import { PlusOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons'
|
||||||
|
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import {
|
||||||
|
App,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
Modal,
|
||||||
|
Select,
|
||||||
|
Space,
|
||||||
|
Switch,
|
||||||
|
Tag,
|
||||||
|
Typography,
|
||||||
|
} from 'antd'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import PageHeader from '@/components/admin/PageHeader'
|
||||||
|
import PercentTable from '@/components/common/PercentTable'
|
||||||
|
import {
|
||||||
|
createAdminConsignmentContact,
|
||||||
|
deleteAdminConsignmentContact,
|
||||||
|
fetchAdminConsignmentContacts,
|
||||||
|
setAdminConsignmentContactEnabled,
|
||||||
|
updateAdminConsignmentContact,
|
||||||
|
} from '@/services/admin/consignment'
|
||||||
|
import type { ConsignmentContactSupporter } from '@/types/consignment'
|
||||||
|
import { formatDateTime } from '@/utils/date-time'
|
||||||
|
import type { TableColumnsType } from 'antd'
|
||||||
|
|
||||||
|
const { Text } = Typography
|
||||||
|
|
||||||
|
type ContactForm = {
|
||||||
|
name: string
|
||||||
|
phone?: string
|
||||||
|
wechat?: string
|
||||||
|
note?: string
|
||||||
|
sortOrder?: number
|
||||||
|
enabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const ENABLED_FILTER_OPTIONS = [
|
||||||
|
{ label: '全部状态', value: '' },
|
||||||
|
{ label: '启用中', value: 'true' },
|
||||||
|
{ label: '已禁用', value: 'false' },
|
||||||
|
]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置 → 对接客服。
|
||||||
|
*
|
||||||
|
* 独立名单(不复用后台账号):打手发布寄卖时必选,后台按人统计提成与快速联系。
|
||||||
|
* 已被寄卖记录引用的客服只能禁用,不能删除,保证历史单归属不丢。
|
||||||
|
*/
|
||||||
|
export default function AdminConsignmentContactsPage() {
|
||||||
|
const { message, modal } = App.useApp()
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const [form] = Form.useForm<ContactForm>()
|
||||||
|
|
||||||
|
const [keywordInput, setKeywordInput] = useState('')
|
||||||
|
const [keyword, setKeyword] = useState('')
|
||||||
|
const [enabledFilter, setEnabledFilter] = useState('')
|
||||||
|
const [editingContact, setEditingContact] = useState<ConsignmentContactSupporter | null>(null)
|
||||||
|
const [formOpen, setFormOpen] = useState(false)
|
||||||
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
const [actionId, setActionId] = useState<number | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = window.setTimeout(() => setKeyword(keywordInput.trim()), 300)
|
||||||
|
return () => window.clearTimeout(timer)
|
||||||
|
}, [keywordInput])
|
||||||
|
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: ['admin-consignment-contacts', keyword, enabledFilter],
|
||||||
|
queryFn: () =>
|
||||||
|
fetchAdminConsignmentContacts({
|
||||||
|
keyword: keyword || undefined,
|
||||||
|
enabled: enabledFilter || undefined,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const items = query.data?.data.items || []
|
||||||
|
const total = Number(query.data?.data.total || 0)
|
||||||
|
const enabledCount = items.filter((item) => item.enabled).length
|
||||||
|
|
||||||
|
async function reload() {
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['admin-consignment-contacts'] })
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreate() {
|
||||||
|
setEditingContact(null)
|
||||||
|
form.resetFields()
|
||||||
|
form.setFieldsValue({ enabled: true, sortOrder: 0 })
|
||||||
|
setFormOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEdit(record: ConsignmentContactSupporter) {
|
||||||
|
setEditingContact(record)
|
||||||
|
form.setFieldsValue({
|
||||||
|
name: record.name,
|
||||||
|
phone: record.phone,
|
||||||
|
wechat: record.wechat,
|
||||||
|
note: record.note,
|
||||||
|
sortOrder: record.sortOrder,
|
||||||
|
enabled: record.enabled,
|
||||||
|
})
|
||||||
|
setFormOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
try {
|
||||||
|
const values = await form.validateFields()
|
||||||
|
setSubmitting(true)
|
||||||
|
const payload = {
|
||||||
|
name: values.name,
|
||||||
|
phone: values.phone || '',
|
||||||
|
wechat: values.wechat || '',
|
||||||
|
note: values.note || '',
|
||||||
|
sortOrder: Number(values.sortOrder || 0),
|
||||||
|
enabled: values.enabled !== false,
|
||||||
|
}
|
||||||
|
if (editingContact) {
|
||||||
|
await updateAdminConsignmentContact(editingContact.id, payload)
|
||||||
|
message.success('对接客服已更新')
|
||||||
|
} else {
|
||||||
|
await createAdminConsignmentContact(payload)
|
||||||
|
message.success('对接客服已新增')
|
||||||
|
}
|
||||||
|
setFormOpen(false)
|
||||||
|
setEditingContact(null)
|
||||||
|
await reload()
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error?.errorFields) return
|
||||||
|
message.error(error?.message || '保存对接客服失败')
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleToggleEnabled(record: ConsignmentContactSupporter, enabled: boolean) {
|
||||||
|
setActionId(record.id)
|
||||||
|
try {
|
||||||
|
await setAdminConsignmentContactEnabled(record.id, enabled)
|
||||||
|
message.success(enabled ? '已启用该对接客服' : '已禁用该对接客服')
|
||||||
|
await reload()
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error?.message || '更新对接客服状态失败')
|
||||||
|
} finally {
|
||||||
|
setActionId(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete(record: ConsignmentContactSupporter) {
|
||||||
|
modal.confirm({
|
||||||
|
title: `删除对接客服「${record.name}」?`,
|
||||||
|
content:
|
||||||
|
'只有未被任何寄卖记录使用的客服可以删除。已被使用的请改为「禁用」,历史寄卖单仍会保留对接客服信息。',
|
||||||
|
okText: '删除',
|
||||||
|
okType: 'danger',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await deleteAdminConsignmentContact(record.id)
|
||||||
|
message.success('对接客服已删除')
|
||||||
|
await reload()
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(error?.message || '删除对接客服失败')
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: TableColumnsType<ConsignmentContactSupporter> = [
|
||||||
|
{
|
||||||
|
title: '姓名',
|
||||||
|
key: 'name',
|
||||||
|
width: 160,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space direction="vertical" size={2}>
|
||||||
|
<Text strong>{record.name}</Text>
|
||||||
|
{record.note ? (
|
||||||
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
{record.note}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '手机号',
|
||||||
|
dataIndex: 'phone',
|
||||||
|
width: 150,
|
||||||
|
render: (value: string) => value || <Text type="secondary">-</Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '微信号',
|
||||||
|
dataIndex: 'wechat',
|
||||||
|
width: 160,
|
||||||
|
render: (value: string) => value || <Text type="secondary">-</Text>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '排序',
|
||||||
|
dataIndex: 'sortOrder',
|
||||||
|
width: 80,
|
||||||
|
render: (value: number) => value || 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
key: 'enabled',
|
||||||
|
width: 110,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space size={6}>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={record.enabled}
|
||||||
|
loading={actionId === record.id}
|
||||||
|
checkedChildren="启用"
|
||||||
|
unCheckedChildren="禁用"
|
||||||
|
onChange={(checked) => void handleToggleEnabled(record, checked)}
|
||||||
|
/>
|
||||||
|
<Tag color={record.enabled ? 'green' : 'default'} style={{ marginInlineEnd: 0 }}>
|
||||||
|
{record.enabled ? '启用中' : '已禁用'}
|
||||||
|
</Tag>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '更新时间',
|
||||||
|
dataIndex: 'updatedAt',
|
||||||
|
width: 170,
|
||||||
|
render: (value: string) => formatDateTime(value),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'actions',
|
||||||
|
width: 140,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space size={4}>
|
||||||
|
<Button size="small" type="link" onClick={() => openEdit(record)}>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Button size="small" type="link" danger onClick={() => handleDelete(record)}>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PageHeader
|
||||||
|
title="对接客服"
|
||||||
|
description="打手发布寄卖时必须选择对接客服;本名单独立于后台账号,用于提成归属与联系。"
|
||||||
|
extra={
|
||||||
|
<Button type="primary" icon={<PlusOutlined />} onClick={openCreate}>
|
||||||
|
新增对接客服
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card size="small" style={{ marginBottom: 16 }}>
|
||||||
|
<Space wrap size={12}>
|
||||||
|
<Input
|
||||||
|
allowClear
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
placeholder="搜索姓名 / 手机 / 微信 / 备注"
|
||||||
|
value={keywordInput}
|
||||||
|
onChange={(event) => setKeywordInput(event.target.value)}
|
||||||
|
style={{ width: 280 }}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={enabledFilter}
|
||||||
|
options={ENABLED_FILTER_OPTIONS}
|
||||||
|
onChange={setEnabledFilter}
|
||||||
|
style={{ width: 140 }}
|
||||||
|
/>
|
||||||
|
<Button icon={<ReloadOutlined />} onClick={() => void query.refetch()}>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
<Text type="secondary">
|
||||||
|
共 {total} 个,本页启用 {enabledCount} 个
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<PercentTable<ConsignmentContactSupporter>
|
||||||
|
rowKey="id"
|
||||||
|
loading={query.isLoading}
|
||||||
|
columns={columns}
|
||||||
|
dataSource={items}
|
||||||
|
pagination={false}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={editingContact ? `编辑对接客服 · ${editingContact.name}` : '新增对接客服'}
|
||||||
|
open={formOpen}
|
||||||
|
onCancel={() => {
|
||||||
|
setFormOpen(false)
|
||||||
|
setEditingContact(null)
|
||||||
|
}}
|
||||||
|
onOk={() => void handleSubmit()}
|
||||||
|
confirmLoading={submitting}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
<Form.Item
|
||||||
|
name="name"
|
||||||
|
label="姓名"
|
||||||
|
rules={[{ required: true, message: '请填写对接客服姓名' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="如:小张" maxLength={60} />
|
||||||
|
</Form.Item>
|
||||||
|
<Space size={12} style={{ width: '100%' }} align="start">
|
||||||
|
<Form.Item name="phone" label="手机号" style={{ width: 240 }}>
|
||||||
|
<Input placeholder="选填" maxLength={30} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="wechat" label="微信号" style={{ width: 240 }}>
|
||||||
|
<Input placeholder="选填" maxLength={60} />
|
||||||
|
</Form.Item>
|
||||||
|
</Space>
|
||||||
|
<Form.Item name="note" label="备注">
|
||||||
|
<Input.TextArea rows={2} placeholder="如:负责苏尔南寄卖 / 白班" maxLength={200} />
|
||||||
|
</Form.Item>
|
||||||
|
<Space size={24} align="start">
|
||||||
|
<Form.Item
|
||||||
|
name="sortOrder"
|
||||||
|
label="排序"
|
||||||
|
tooltip="数字越小越靠前;打手端默认选中第一个启用项"
|
||||||
|
>
|
||||||
|
<InputNumber min={0} max={9999} style={{ width: 140 }} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item name="enabled" label="启用" valuePropName="checked">
|
||||||
|
<Switch checkedChildren="启用" unCheckedChildren="禁用" />
|
||||||
|
</Form.Item>
|
||||||
|
</Space>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ import { useState } from 'react'
|
|||||||
import ConsignmentEvidenceGallery from '@/components/admin/ConsignmentEvidenceGallery'
|
import ConsignmentEvidenceGallery from '@/components/admin/ConsignmentEvidenceGallery'
|
||||||
import {
|
import {
|
||||||
archiveAdminConsignmentInventory,
|
archiveAdminConsignmentInventory,
|
||||||
|
fetchAdminConsignmentContacts,
|
||||||
fetchAdminConsignmentInventories,
|
fetchAdminConsignmentInventories,
|
||||||
fetchAdminConsignmentStats,
|
fetchAdminConsignmentStats,
|
||||||
returnAdminConsignmentInventory,
|
returnAdminConsignmentInventory,
|
||||||
@@ -40,6 +41,7 @@ import ConsignmentManualMatchModal from '@/components/admin/ConsignmentManualMat
|
|||||||
import type { ConsignmentInventory, GiftEvidenceReviewStatus } from '@/types/consignment'
|
import type { ConsignmentInventory, GiftEvidenceReviewStatus } from '@/types/consignment'
|
||||||
import { fetchAdminSession } from '@/services/admin/auth'
|
import { fetchAdminSession } from '@/services/admin/auth'
|
||||||
import { resolveAdminPermission } from '@/utils/admin-auth'
|
import { resolveAdminPermission } from '@/utils/admin-auth'
|
||||||
|
import { formatConsignmentContactSupporter } from '@/utils/consignment-contact'
|
||||||
import { describeGiftEvidence, resolveGiftEvidenceBadge } from '@/utils/consignment-evidence'
|
import { describeGiftEvidence, resolveGiftEvidenceBadge } from '@/utils/consignment-evidence'
|
||||||
import { CONSIGNMENT_SYSTEM_OPTIONS, CONSIGNMENT_ZONE_OPTIONS } from '@/utils/consignment-spec'
|
import { CONSIGNMENT_SYSTEM_OPTIONS, CONSIGNMENT_ZONE_OPTIONS } from '@/utils/consignment-spec'
|
||||||
import { formatDateTime } from '@/utils/date-time'
|
import { formatDateTime } from '@/utils/date-time'
|
||||||
@@ -174,6 +176,18 @@ export default function ConsignmentInventoryPanel() {
|
|||||||
name: w.displayName || w.username,
|
name: w.displayName || w.username,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// 对接客服名单:编辑现货时可改派对接人(含已禁用项,历史数据需要能显示原值)
|
||||||
|
const contactsQuery = useQuery({
|
||||||
|
queryKey: ['admin-consignment-contacts'],
|
||||||
|
queryFn: () => fetchAdminConsignmentContacts(),
|
||||||
|
})
|
||||||
|
const contactSupporterOptions = (contactsQuery.data?.data.items || []).map((item) => ({
|
||||||
|
label: `${item.name}${item.enabled ? '' : '(已禁用)'}${
|
||||||
|
item.phone ? ` · ${item.phone}` : item.wechat ? ` · ${item.wechat}` : ''
|
||||||
|
}`,
|
||||||
|
value: item.id,
|
||||||
|
}))
|
||||||
|
|
||||||
// 数据查询
|
// 数据查询
|
||||||
const inventoriesQuery = useQuery({
|
const inventoriesQuery = useQuery({
|
||||||
queryKey: [
|
queryKey: [
|
||||||
@@ -255,6 +269,7 @@ export default function ConsignmentInventoryPanel() {
|
|||||||
workerId: values.workerId || undefined,
|
workerId: values.workerId || undefined,
|
||||||
workerName: selectedWorker?.name || editingItem.workerName || undefined,
|
workerName: selectedWorker?.name || editingItem.workerName || undefined,
|
||||||
workerPhone: selectedWorker?.phone || editingItem.workerPhone || undefined,
|
workerPhone: selectedWorker?.phone || editingItem.workerPhone || undefined,
|
||||||
|
contactSupporterId: values.contactSupporterId ?? null,
|
||||||
remark: values.remark,
|
remark: values.remark,
|
||||||
allowMatching: values.allowMatching !== false,
|
allowMatching: values.allowMatching !== false,
|
||||||
})
|
})
|
||||||
@@ -456,6 +471,22 @@ export default function ConsignmentInventoryPanel() {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '对接客服',
|
||||||
|
key: 'contactSupporter',
|
||||||
|
width: 150,
|
||||||
|
render: (_, record) => {
|
||||||
|
const contact = formatConsignmentContactSupporter(record)
|
||||||
|
return (
|
||||||
|
<div style={{ fontSize: 12 }}>
|
||||||
|
<div style={{ fontWeight: 600 }}>{contact.name || contact.text}</div>
|
||||||
|
{contact.contact ? (
|
||||||
|
<div style={{ color: '#8c8c8c', marginTop: 2 }}>{contact.contact}</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '凭证',
|
title: '凭证',
|
||||||
key: 'evidence',
|
key: 'evidence',
|
||||||
@@ -990,6 +1021,24 @@ export default function ConsignmentInventoryPanel() {
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={14}>
|
||||||
|
<Form.Item
|
||||||
|
name="contactSupporterId"
|
||||||
|
label="对接客服"
|
||||||
|
tooltip="用于提成归属与联系;发布寄卖时由卖家必选"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
allowClear
|
||||||
|
showSearch
|
||||||
|
optionFilterProp="label"
|
||||||
|
placeholder="选择对接客服(选填)"
|
||||||
|
options={contactSupporterOptions}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
<Form.Item name="sellerPaymentInfo" label="卖家收款信息">
|
<Form.Item name="sellerPaymentInfo" label="卖家收款信息">
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -1082,6 +1131,23 @@ export default function ConsignmentInventoryPanel() {
|
|||||||
<Text type="secondary">内部自营现货</Text>
|
<Text type="secondary">内部自营现货</Text>
|
||||||
)}
|
)}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="对接客服">
|
||||||
|
{(() => {
|
||||||
|
const contact = formatConsignmentContactSupporter(viewingItem)
|
||||||
|
return contact.name ? (
|
||||||
|
<Space direction="vertical" size={2}>
|
||||||
|
<Text strong>{contact.name}</Text>
|
||||||
|
{contact.contact ? (
|
||||||
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
{contact.contact}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
<Text type="secondary">未指定</Text>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="履约押金要求">
|
<Descriptions.Item label="履约押金要求">
|
||||||
{viewingItem.depositAmountFen ? (
|
{viewingItem.depositAmountFen ? (
|
||||||
<Tag color="gold">¥{(viewingItem.depositAmountFen / 100).toFixed(0)}</Tag>
|
<Tag color="gold">¥{(viewingItem.depositAmountFen / 100).toFixed(0)}</Tag>
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import { fetchAdminSession } from '@/services/admin/auth'
|
|||||||
import { fetchAdminWorkerUsers } from '@/services/admin/worker-platform'
|
import { fetchAdminWorkerUsers } from '@/services/admin/worker-platform'
|
||||||
import type { ConsignmentSubmission, GiftEvidenceReviewStatus } from '@/types/consignment'
|
import type { ConsignmentSubmission, GiftEvidenceReviewStatus } from '@/types/consignment'
|
||||||
import { resolveAdminPermission } from '@/utils/admin-auth'
|
import { resolveAdminPermission } from '@/utils/admin-auth'
|
||||||
|
import { formatConsignmentContactSupporter } from '@/utils/consignment-contact'
|
||||||
import { describeGiftEvidence, resolveGiftEvidenceBadge } from '@/utils/consignment-evidence'
|
import { describeGiftEvidence, resolveGiftEvidenceBadge } from '@/utils/consignment-evidence'
|
||||||
import { CONSIGNMENT_SYSTEM_OPTIONS, CONSIGNMENT_ZONE_OPTIONS } from '@/utils/consignment-spec'
|
import { CONSIGNMENT_SYSTEM_OPTIONS, CONSIGNMENT_ZONE_OPTIONS } from '@/utils/consignment-spec'
|
||||||
import { formatDateTime } from '@/utils/date-time'
|
import { formatDateTime } from '@/utils/date-time'
|
||||||
@@ -271,6 +272,22 @@ export default function ConsignmentSubmissionsPanel() {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '对接客服',
|
||||||
|
key: 'contactSupporter',
|
||||||
|
width: 150,
|
||||||
|
render: (_, record) => {
|
||||||
|
const contact = formatConsignmentContactSupporter(record)
|
||||||
|
return (
|
||||||
|
<div style={{ fontSize: 12 }}>
|
||||||
|
<div style={{ fontWeight: 600 }}>{contact.name || contact.text}</div>
|
||||||
|
{contact.contact ? (
|
||||||
|
<div style={{ color: '#8c8c8c', marginTop: 2 }}>{contact.contact}</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '提报凭证(游戏主页截图)',
|
title: '提报凭证(游戏主页截图)',
|
||||||
key: 'submissionScreenshots',
|
key: 'submissionScreenshots',
|
||||||
@@ -536,6 +553,10 @@ export default function ConsignmentSubmissionsPanel() {
|
|||||||
<span>
|
<span>
|
||||||
联系方式:<Text strong>{approveModalItem?.contactInfo || '-'}</Text>
|
联系方式:<Text strong>{approveModalItem?.contactInfo || '-'}</Text>
|
||||||
</span>
|
</span>
|
||||||
|
<span>
|
||||||
|
对接客服:
|
||||||
|
<Text strong>{formatConsignmentContactSupporter(approveModalItem).text}</Text>
|
||||||
|
</span>
|
||||||
<span>
|
<span>
|
||||||
游戏ID:<Text strong>{approveModalItem?.sellerGameId || '-'}</Text>
|
游戏ID:<Text strong>{approveModalItem?.sellerGameId || '-'}</Text>
|
||||||
</span>
|
</span>
|
||||||
@@ -852,6 +873,23 @@ export default function ConsignmentSubmissionsPanel() {
|
|||||||
<Text type="secondary">未关联打手账号</Text>
|
<Text type="secondary">未关联打手账号</Text>
|
||||||
)}
|
)}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
<Descriptions.Item label="对接客服">
|
||||||
|
{(() => {
|
||||||
|
const contact = formatConsignmentContactSupporter(viewingSubmission)
|
||||||
|
return contact.name ? (
|
||||||
|
<Space direction="vertical" size={2}>
|
||||||
|
<Text strong>{contact.name}</Text>
|
||||||
|
{contact.contact ? (
|
||||||
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
|
{contact.contact}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
<Text type="secondary">未指定</Text>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="履约押金要求">
|
<Descriptions.Item label="履约押金要求">
|
||||||
{viewingSubmission.depositAmountFen ? (
|
{viewingSubmission.depositAmountFen ? (
|
||||||
<Tag color="gold">¥{(viewingSubmission.depositAmountFen / 100).toFixed(0)}</Tag>
|
<Tag color="gold">¥{(viewingSubmission.depositAmountFen / 100).toFixed(0)}</Tag>
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ import { notifyVoucherConsume } from '@/utils/voucher-consume'
|
|||||||
import type { ConsignmentFulfillmentInfo, WorkOrderInventoryMatchCheck } from '@/types/consignment'
|
import type { ConsignmentFulfillmentInfo, WorkOrderInventoryMatchCheck } from '@/types/consignment'
|
||||||
import type { UploadedFile, WorkOrder, WorkOrderShare } from '@/types/worker-platform'
|
import type { UploadedFile, WorkOrder, WorkOrderShare } from '@/types/worker-platform'
|
||||||
import { canManuallyMatchConsignmentOrder } from '@/utils/consignment-manual-match'
|
import { canManuallyMatchConsignmentOrder } from '@/utils/consignment-manual-match'
|
||||||
|
import { formatConsignmentContactSupporter } from '@/utils/consignment-contact'
|
||||||
import {
|
import {
|
||||||
isConsignmentOrder,
|
isConsignmentOrder,
|
||||||
isInternalConsignmentOrder,
|
isInternalConsignmentOrder,
|
||||||
@@ -1644,10 +1645,15 @@ export default function WorkOrdersPanel() {
|
|||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
) : null}
|
) : null}
|
||||||
{consignment ? (
|
{consignment ? (
|
||||||
<Tag color="purple" style={{ marginTop: 2, width: 'fit-content' }}>
|
<Space size={[4, 4]} wrap>
|
||||||
寄卖单号: {resolveConsignmentNo(consignment) || '-'} (成本: ¥
|
<Tag color="purple" style={{ marginTop: 2, width: 'fit-content' }}>
|
||||||
{((Number(consignment.costPriceFen) || 0) / 100).toFixed(2)})
|
寄卖单号: {resolveConsignmentNo(consignment) || '-'} (成本: ¥
|
||||||
</Tag>
|
{((Number(consignment.costPriceFen) || 0) / 100).toFixed(2)})
|
||||||
|
</Tag>
|
||||||
|
<Tag color="cyan" style={{ marginTop: 2, width: 'fit-content' }}>
|
||||||
|
对接客服: {formatConsignmentContactSupporter(consignment).text}
|
||||||
|
</Tag>
|
||||||
|
</Space>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -2317,6 +2323,11 @@ export default function WorkOrdersPanel() {
|
|||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
{detailConsignment ? (
|
||||||
|
<Descriptions.Item label="对接客服">
|
||||||
|
{formatConsignmentContactSupporter(detailConsignment).text}
|
||||||
|
</Descriptions.Item>
|
||||||
|
) : null}
|
||||||
<Descriptions.Item label="状态">
|
<Descriptions.Item label="状态">
|
||||||
<Tag color={resolveStatusColor(detailOrder.status)}>
|
<Tag color={resolveStatusColor(detailOrder.status)}>
|
||||||
{formatStatus(detailOrder.status)}
|
{formatStatus(detailOrder.status)}
|
||||||
@@ -2655,6 +2666,13 @@ export default function WorkOrdersPanel() {
|
|||||||
</Space>
|
</Space>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
) : null}
|
) : null}
|
||||||
|
{isConsignment ? (
|
||||||
|
<Descriptions.Item label="对接客服" span={2}>
|
||||||
|
<span className="worker-order-detail-value">
|
||||||
|
{formatConsignmentContactSupporter(consignmentInfo).text}
|
||||||
|
</span>
|
||||||
|
</Descriptions.Item>
|
||||||
|
) : null}
|
||||||
<Descriptions.Item
|
<Descriptions.Item
|
||||||
label={isConsignment ? '卖家备注' : '打手备注'}
|
label={isConsignment ? '卖家备注' : '打手备注'}
|
||||||
span={2}
|
span={2}
|
||||||
@@ -4095,6 +4113,9 @@ export default function WorkOrdersPanel() {
|
|||||||
供货打手: {inv.workerName} ({inv.workerPhone || '无手机号'})
|
供货打手: {inv.workerName} ({inv.workerPhone || '无手机号'})
|
||||||
</Tag>
|
</Tag>
|
||||||
) : null}
|
) : null}
|
||||||
|
<Tag color="cyan">
|
||||||
|
对接客服: {formatConsignmentContactSupporter(inv).text}
|
||||||
|
</Tag>
|
||||||
{inv.depositAmountFen != null ? (
|
{inv.depositAmountFen != null ? (
|
||||||
<Tag color="warning">
|
<Tag color="warning">
|
||||||
履约押金: ¥{(inv.depositAmountFen / 100).toFixed(2)}
|
履约押金: ¥{(inv.depositAmountFen / 100).toFixed(2)}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
Pagination,
|
Pagination,
|
||||||
Radio,
|
Radio,
|
||||||
Row,
|
Row,
|
||||||
|
Select,
|
||||||
Space,
|
Space,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tag,
|
Tag,
|
||||||
@@ -43,6 +44,7 @@ import ImageUpload from '@/components/files/ImageUpload'
|
|||||||
import { WorkerContactModal } from '@/components/worker/WorkerContactModal'
|
import { WorkerContactModal } from '@/components/worker/WorkerContactModal'
|
||||||
import {
|
import {
|
||||||
confirmWorkerConsignmentFriendAdded,
|
confirmWorkerConsignmentFriendAdded,
|
||||||
|
fetchWorkerConsignmentContacts,
|
||||||
fetchWorkerConsignmentPriceSuggestion,
|
fetchWorkerConsignmentPriceSuggestion,
|
||||||
fetchWorkerConsignments,
|
fetchWorkerConsignments,
|
||||||
fetchWorkerProfile,
|
fetchWorkerProfile,
|
||||||
@@ -56,6 +58,7 @@ import {
|
|||||||
SURNAN_PUBLISH_SYSTEM_OPTIONS,
|
SURNAN_PUBLISH_SYSTEM_OPTIONS,
|
||||||
SURNAN_PUBLISH_ZONE_OPTIONS,
|
SURNAN_PUBLISH_ZONE_OPTIONS,
|
||||||
} from '@/utils/consignment-spec'
|
} from '@/utils/consignment-spec'
|
||||||
|
import { formatConsignmentContactSupporter } from '@/utils/consignment-contact'
|
||||||
import { formatGiftCooldownRemaining } from '@/utils/consignment-cooldown'
|
import { formatGiftCooldownRemaining } from '@/utils/consignment-cooldown'
|
||||||
import { canWorkerPublishConsignment } from '@/utils/consignment-whitelist'
|
import { canWorkerPublishConsignment } from '@/utils/consignment-whitelist'
|
||||||
import { formatDateTime } from '@/utils/date-time'
|
import { formatDateTime } from '@/utils/date-time'
|
||||||
@@ -199,6 +202,15 @@ export default function WorkerConsignmentPage() {
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 发布寄卖必填「对接客服」:只取启用名单,默认选第一个
|
||||||
|
const consignmentContactsQuery = useQuery({
|
||||||
|
queryKey: ['worker-consignment-contacts'],
|
||||||
|
queryFn: () => fetchWorkerConsignmentContacts(),
|
||||||
|
staleTime: 60_000,
|
||||||
|
})
|
||||||
|
const contactSupporters = consignmentContactsQuery.data?.data.items || []
|
||||||
|
const firstContactSupporterId = contactSupporters[0]?.id ?? null
|
||||||
|
|
||||||
// 切换标签页时自动刷新最新数据
|
// 切换标签页时自动刷新最新数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeTab === 'records') {
|
if (activeTab === 'records') {
|
||||||
@@ -261,6 +273,14 @@ export default function WorkerConsignmentPage() {
|
|||||||
}
|
}
|
||||||
}, [worker, form])
|
}, [worker, form])
|
||||||
|
|
||||||
|
// 名单异步加载完成后补默认值;用户已经选过则不覆盖
|
||||||
|
useEffect(() => {
|
||||||
|
if (firstContactSupporterId == null) return
|
||||||
|
if (form.getFieldValue('contactSupporterId') == null) {
|
||||||
|
form.setFieldsValue({ contactSupporterId: firstContactSupporterId })
|
||||||
|
}
|
||||||
|
}, [firstContactSupporterId, form])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (profileQuery.isLoading) return
|
if (profileQuery.isLoading) return
|
||||||
if (typeof window === 'undefined') return
|
if (typeof window === 'undefined') return
|
||||||
@@ -368,6 +388,15 @@ export default function WorkerConsignmentPage() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (contactSupporters.length === 0) {
|
||||||
|
message.error('暂无可选的对接客服,请联系管理员在「配置 - 对接客服」中添加后再发布')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (values.contactSupporterId == null) {
|
||||||
|
message.error('请选择对接客服')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const urls = screenshots.map((f) => f.url).filter(Boolean)
|
const urls = screenshots.map((f) => f.url).filter(Boolean)
|
||||||
if (urls.length === 0) {
|
if (urls.length === 0) {
|
||||||
message.error('请上传游戏主页截图以供核验')
|
message.error('请上传游戏主页截图以供核验')
|
||||||
@@ -412,10 +441,14 @@ export default function WorkerConsignmentPage() {
|
|||||||
screenshotsJson: urls,
|
screenshotsJson: urls,
|
||||||
sellerNote: values.sellerNote,
|
sellerNote: values.sellerNote,
|
||||||
depositAmountFen: publishDepositFen,
|
depositAmountFen: publishDepositFen,
|
||||||
|
contactSupporterId: Number(values.contactSupporterId),
|
||||||
})
|
})
|
||||||
|
|
||||||
message.success(`寄卖提报成功,已暂扣履约押金 ¥${publishDepositYuan.toFixed(0)}`)
|
message.success(`寄卖提报成功,已暂扣履约押金 ¥${publishDepositYuan.toFixed(0)}`)
|
||||||
form.resetFields()
|
form.resetFields()
|
||||||
|
if (firstContactSupporterId != null) {
|
||||||
|
form.setFieldsValue({ contactSupporterId: firstContactSupporterId })
|
||||||
|
}
|
||||||
setPriceState({ status: 'idle' })
|
setPriceState({ status: 'idle' })
|
||||||
lastPriceQueryKeyRef.current = ''
|
lastPriceQueryKeyRef.current = ''
|
||||||
setScreenshots([])
|
setScreenshots([])
|
||||||
@@ -662,6 +695,14 @@ export default function WorkerConsignmentPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{!consignmentContactsQuery.isLoading && contactSupporters.length === 0 ? (
|
||||||
|
<Alert
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
message="暂无可选的对接客服,请联系管理员在「配置 - 对接客服」中添加后再发布"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<Divider plain className="worker-consignment-divider">
|
<Divider plain className="worker-consignment-divider">
|
||||||
规格与价格
|
规格与价格
|
||||||
</Divider>
|
</Divider>
|
||||||
@@ -807,6 +848,35 @@ export default function WorkerConsignmentPage() {
|
|||||||
</Divider>
|
</Divider>
|
||||||
|
|
||||||
<Row gutter={[12, 0]}>
|
<Row gutter={[12, 0]}>
|
||||||
|
<Col xs={24} sm={12}>
|
||||||
|
<Form.Item
|
||||||
|
name="contactSupporterId"
|
||||||
|
label="对接客服"
|
||||||
|
rules={[{ required: true, message: '请选择对接客服' }]}
|
||||||
|
extra={
|
||||||
|
contactSupporters.length === 0
|
||||||
|
? '暂无可选客服,请联系管理员配置'
|
||||||
|
: '用于后续提成归属与联系'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
placeholder="请选择对接客服"
|
||||||
|
options={contactSupporters.map((item) => ({
|
||||||
|
value: item.id,
|
||||||
|
label: `${item.name}${
|
||||||
|
item.phone
|
||||||
|
? ` · ${item.phone}`
|
||||||
|
: item.wechat
|
||||||
|
? ` · ${item.wechat}`
|
||||||
|
: ''
|
||||||
|
}`,
|
||||||
|
}))}
|
||||||
|
notFoundContent="暂无启用中的对接客服"
|
||||||
|
showSearch
|
||||||
|
optionFilterProp="label"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Col>
|
||||||
<Col xs={24} sm={12}>
|
<Col xs={24} sm={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="sellerContact"
|
name="sellerContact"
|
||||||
@@ -1334,6 +1404,9 @@ function ConsignmentRecordCard({
|
|||||||
<span>
|
<span>
|
||||||
押金 <strong>¥{(Number(record.depositAmountFen || 0) / 100).toFixed(2)}</strong>
|
押金 <strong>¥{(Number(record.depositAmountFen || 0) / 100).toFixed(2)}</strong>
|
||||||
</span>
|
</span>
|
||||||
|
<span>
|
||||||
|
对接客服 <strong>{formatConsignmentContactSupporter(record).text}</strong>
|
||||||
|
</span>
|
||||||
{record.workOrderId ? <span>履约单 #{record.workOrderId}</span> : null}
|
{record.workOrderId ? <span>履约单 #{record.workOrderId}</span> : null}
|
||||||
</div>
|
</div>
|
||||||
{record.reviewNote ? (
|
{record.reviewNote ? (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { apiGet, apiPatch, apiPost, apiPut } from '@/lib/http'
|
import { apiDelete, apiGet, apiPatch, apiPost, apiPut } from '@/lib/http'
|
||||||
import type {
|
import type {
|
||||||
|
ConsignmentContactSupporter,
|
||||||
ConsignmentInventory,
|
ConsignmentInventory,
|
||||||
ConsignmentStatsSummary,
|
ConsignmentStatsSummary,
|
||||||
ConsignmentSubmission,
|
ConsignmentSubmission,
|
||||||
@@ -8,6 +9,43 @@ import type {
|
|||||||
WorkOrderInventoryMatchOptions,
|
WorkOrderInventoryMatchOptions,
|
||||||
} from '@/types/consignment'
|
} from '@/types/consignment'
|
||||||
|
|
||||||
|
// --- 对接客服名单 ---
|
||||||
|
|
||||||
|
export function fetchAdminConsignmentContacts(params?: { keyword?: string; enabled?: string }) {
|
||||||
|
return apiGet<{ items: ConsignmentContactSupporter[]; total: number }>(
|
||||||
|
'/api/v1/admin/worker-platform/consignment/contacts',
|
||||||
|
params,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAdminConsignmentContact(payload: Record<string, unknown>) {
|
||||||
|
return apiPost<ConsignmentContactSupporter>(
|
||||||
|
'/api/v1/admin/worker-platform/consignment/contacts',
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateAdminConsignmentContact(
|
||||||
|
id: number | string,
|
||||||
|
payload: Record<string, unknown>,
|
||||||
|
) {
|
||||||
|
return apiPut<ConsignmentContactSupporter>(
|
||||||
|
`/api/v1/admin/worker-platform/consignment/contacts/${id}`,
|
||||||
|
payload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAdminConsignmentContactEnabled(id: number | string, enabled: boolean) {
|
||||||
|
return apiPatch<ConsignmentContactSupporter>(
|
||||||
|
`/api/v1/admin/worker-platform/consignment/contacts/${id}/enabled`,
|
||||||
|
{ enabled },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteAdminConsignmentContact(id: number | string) {
|
||||||
|
return apiDelete<{ deleted: boolean }>(`/api/v1/admin/worker-platform/consignment/contacts/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
// --- 库存管理 ---
|
// --- 库存管理 ---
|
||||||
|
|
||||||
export function fetchAdminConsignmentInventories(params?: Record<string, unknown>) {
|
export function fetchAdminConsignmentInventories(params?: Record<string, unknown>) {
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ import type {
|
|||||||
WorkerUser,
|
WorkerUser,
|
||||||
WorkerWalletLedger,
|
WorkerWalletLedger,
|
||||||
} from '@/types/worker-platform'
|
} from '@/types/worker-platform'
|
||||||
import type { ConsignmentBuyerInfo, ConsignmentSellerInfo } from '@/types/consignment'
|
import type {
|
||||||
|
ConsignmentBuyerInfo,
|
||||||
|
ConsignmentContactSupporter,
|
||||||
|
ConsignmentSellerInfo,
|
||||||
|
} from '@/types/consignment'
|
||||||
|
|
||||||
import { getWorkerDeviceId } from '@/utils/worker-auth'
|
import { getWorkerDeviceId } from '@/utils/worker-auth'
|
||||||
|
|
||||||
@@ -392,6 +396,8 @@ export type WorkerConsignmentPublishPayload = {
|
|||||||
screenshotsJson: string[]
|
screenshotsJson: string[]
|
||||||
sellerNote?: string
|
sellerNote?: string
|
||||||
depositAmountFen?: number
|
depositAmountFen?: number
|
||||||
|
/** 对接客服名单 id(必填,发布时选定) */
|
||||||
|
contactSupporterId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkerConsignmentItem = {
|
export type WorkerConsignmentItem = {
|
||||||
@@ -409,6 +415,10 @@ export type WorkerConsignmentItem = {
|
|||||||
resultInventoryId?: number | string | null
|
resultInventoryId?: number | string | null
|
||||||
screenshotsJson: string[]
|
screenshotsJson: string[]
|
||||||
contentJson: Record<string, unknown>
|
contentJson: Record<string, unknown>
|
||||||
|
contactSupporterId?: number | null
|
||||||
|
contactSupporterName?: string
|
||||||
|
contactSupporterPhone?: string
|
||||||
|
contactSupporterWechat?: string
|
||||||
createdAt: string
|
createdAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,6 +460,10 @@ export type WorkerConsignmentRecord = {
|
|||||||
brillianceCount: number
|
brillianceCount: number
|
||||||
askingPriceFen: number
|
askingPriceFen: number
|
||||||
depositAmountFen: number
|
depositAmountFen: number
|
||||||
|
contactSupporterId?: number | null
|
||||||
|
contactSupporterName?: string
|
||||||
|
contactSupporterPhone?: string
|
||||||
|
contactSupporterWechat?: string
|
||||||
reviewNote: string
|
reviewNote: string
|
||||||
screenshotsJson: string[]
|
screenshotsJson: string[]
|
||||||
giftEvidenceImages?: string[]
|
giftEvidenceImages?: string[]
|
||||||
@@ -507,6 +521,11 @@ export function publishWorkerConsignment(payload: WorkerConsignmentPublishPayloa
|
|||||||
return apiPost<WorkerConsignmentItem>('/api/v1/worker/consignment/publish', payload)
|
return apiPost<WorkerConsignmentItem>('/api/v1/worker/consignment/publish', payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 发布寄卖时的「对接客服」下拉(仅启用中的名单)。 */
|
||||||
|
export function fetchWorkerConsignmentContacts() {
|
||||||
|
return apiGet<{ items: ConsignmentContactSupporter[] }>('/api/v1/worker/consignment/contacts')
|
||||||
|
}
|
||||||
|
|
||||||
export function fetchWorkerConsignments(params: WorkerConsignmentListParams = {}) {
|
export function fetchWorkerConsignments(params: WorkerConsignmentListParams = {}) {
|
||||||
return apiGet<WorkerConsignmentListResponse>(
|
return apiGet<WorkerConsignmentListResponse>(
|
||||||
'/api/v1/worker/consignment/my-items',
|
'/api/v1/worker/consignment/my-items',
|
||||||
|
|||||||
@@ -7,6 +7,31 @@
|
|||||||
*/
|
*/
|
||||||
export type GiftEvidenceReviewStatus = 'none' | 'pending' | 'accepted' | 'submitted'
|
export type GiftEvidenceReviewStatus = 'none' | 'pending' | 'accepted' | 'submitted'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 寄卖「对接客服」名单项。
|
||||||
|
*
|
||||||
|
* 独立名单(不是后台账号):发布寄卖时必选,后台用于统计提成与联系。
|
||||||
|
*/
|
||||||
|
export type ConsignmentContactSupporter = {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
phone: string
|
||||||
|
wechat: string
|
||||||
|
note: string
|
||||||
|
enabled: boolean
|
||||||
|
sortOrder: number
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提报单 / 现货 / 工单物料上的对接客服快照字段。 */
|
||||||
|
export type ConsignmentContactFields = {
|
||||||
|
contactSupporterId?: number | null
|
||||||
|
contactSupporterName?: string
|
||||||
|
contactSupporterPhone?: string
|
||||||
|
contactSupporterWechat?: string
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 客服列表 / 详情接口额外返回的寄卖凭证字段(提报列表与库存列表都有)。
|
* 客服列表 / 详情接口额外返回的寄卖凭证字段(提报列表与库存列表都有)。
|
||||||
*
|
*
|
||||||
@@ -34,51 +59,52 @@ export type ConsignmentEvidenceFields = {
|
|||||||
giftEvidenceReviewStatus?: GiftEvidenceReviewStatus
|
giftEvidenceReviewStatus?: GiftEvidenceReviewStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConsignmentInventory = ConsignmentEvidenceFields & {
|
export type ConsignmentInventory = ConsignmentEvidenceFields &
|
||||||
id: number
|
ConsignmentContactFields & {
|
||||||
inventoryNo: string
|
id: number
|
||||||
gameName: string
|
inventoryNo: string
|
||||||
categoryId: number | null
|
gameName: string
|
||||||
categoryName?: string
|
categoryId: number | null
|
||||||
itemName: string
|
categoryName?: string
|
||||||
system: string
|
itemName: string
|
||||||
serverZone: string
|
system: string
|
||||||
series: string
|
serverZone: string
|
||||||
diamondLevel: number
|
series: string
|
||||||
brillianceCount: number
|
diamondLevel: number
|
||||||
costPriceFen: number
|
brillianceCount: number
|
||||||
buyerName: string
|
costPriceFen: number
|
||||||
shopName: string
|
buyerName: string
|
||||||
inboundBatch: string
|
shopName: string
|
||||||
sellerContact: string
|
inboundBatch: string
|
||||||
sellerGameId: string
|
sellerContact: string
|
||||||
sellerPaymentInfo: string
|
sellerGameId: string
|
||||||
sellerPhone: string
|
sellerPaymentInfo: string
|
||||||
credentialsJson: Record<string, unknown>
|
sellerPhone: string
|
||||||
screenshotsJson: string[]
|
credentialsJson: Record<string, unknown>
|
||||||
remark: string
|
screenshotsJson: string[]
|
||||||
allowMatching: boolean
|
remark: string
|
||||||
status: 'in_stock' | 'consumed' | 'archived'
|
allowMatching: boolean
|
||||||
allocationStatus: string
|
status: 'in_stock' | 'consumed' | 'archived'
|
||||||
boundWorkOrderId: number | null
|
allocationStatus: string
|
||||||
boundPlatformOrderId: string
|
boundWorkOrderId: number | null
|
||||||
sourceType: 'manual' | 'worker_portal' | 'recycle'
|
boundPlatformOrderId: string
|
||||||
sourceSubmissionId: number | null
|
sourceType: 'manual' | 'worker_portal' | 'recycle'
|
||||||
/** 来源提报单号(提报单号即寄卖单号);客服手工入库的现货为空字符串。 */
|
sourceSubmissionId: number | null
|
||||||
sourceSubmissionNo?: string
|
/** 来源提报单号(提报单号即寄卖单号);客服手工入库的现货为空字符串。 */
|
||||||
workerId?: number | null
|
sourceSubmissionNo?: string
|
||||||
workerName?: string
|
workerId?: number | null
|
||||||
workerPhone?: string
|
workerName?: string
|
||||||
/** 被指派执行本次履约的打手(卖家之外的第三人);workerId 始终是卖家。 */
|
workerPhone?: string
|
||||||
assignedWorkerId?: number | null
|
/** 被指派执行本次履约的打手(卖家之外的第三人);workerId 始终是卖家。 */
|
||||||
depositAmountFen?: number
|
assignedWorkerId?: number | null
|
||||||
inboundAt: string
|
depositAmountFen?: number
|
||||||
consumedAt: string | null
|
inboundAt: string
|
||||||
createdAt: string
|
consumedAt: string | null
|
||||||
updatedAt: string
|
createdAt: string
|
||||||
/** 关联履约工单的买家资料(加好友及主页截图) */
|
updatedAt: string
|
||||||
buyerInfo?: ConsignmentBuyerInfo | null
|
/** 关联履约工单的买家资料(加好友及主页截图) */
|
||||||
}
|
buyerInfo?: ConsignmentBuyerInfo | null
|
||||||
|
}
|
||||||
|
|
||||||
export type ConsignmentSellerInfo = {
|
export type ConsignmentSellerInfo = {
|
||||||
characterName: string
|
characterName: string
|
||||||
@@ -112,40 +138,46 @@ export type ConsignmentFulfillmentInfo = {
|
|||||||
costPriceFen?: number
|
costPriceFen?: number
|
||||||
fulfillmentType?: string
|
fulfillmentType?: string
|
||||||
workerId?: number | null
|
workerId?: number | null
|
||||||
|
/** 对接客服(履约时从现货复制到工单物料,用于工单侧联系) */
|
||||||
|
contactSupporterId?: number | null
|
||||||
|
contactSupporterName?: string
|
||||||
|
contactSupporterPhone?: string
|
||||||
|
contactSupporterWechat?: string
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConsignmentSubmission = ConsignmentEvidenceFields & {
|
export type ConsignmentSubmission = ConsignmentEvidenceFields &
|
||||||
id: number
|
ConsignmentContactFields & {
|
||||||
submissionNo: string
|
id: number
|
||||||
gameName: string
|
submissionNo: string
|
||||||
categoryId: number | null
|
gameName: string
|
||||||
categoryName?: string
|
categoryId: number | null
|
||||||
itemName: string
|
categoryName?: string
|
||||||
system: string
|
itemName: string
|
||||||
serverZone: string
|
system: string
|
||||||
series: string
|
serverZone: string
|
||||||
diamondLevel: number
|
series: string
|
||||||
brillianceCount: number
|
diamondLevel: number
|
||||||
sellerGameId: string
|
brillianceCount: number
|
||||||
sellerPaymentInfo: string
|
sellerGameId: string
|
||||||
sellerPhone: string
|
sellerPaymentInfo: string
|
||||||
contactInfo: string
|
sellerPhone: string
|
||||||
contentJson: Record<string, unknown>
|
contactInfo: string
|
||||||
screenshotsJson: string[]
|
contentJson: Record<string, unknown>
|
||||||
askingPriceFen: number
|
screenshotsJson: string[]
|
||||||
workerId?: number | null
|
askingPriceFen: number
|
||||||
workerName?: string
|
workerId?: number | null
|
||||||
workerPhone?: string
|
workerName?: string
|
||||||
depositAmountFen?: number
|
workerPhone?: string
|
||||||
status: 'pending_review' | 'approved' | 'rejected'
|
depositAmountFen?: number
|
||||||
reviewNote: string
|
status: 'pending_review' | 'approved' | 'rejected'
|
||||||
reviewerName: string
|
reviewNote: string
|
||||||
reviewedAt: string | null
|
reviewerName: string
|
||||||
resultInventoryId: number | null
|
reviewedAt: string | null
|
||||||
createdAt: string
|
resultInventoryId: number | null
|
||||||
updatedAt: string
|
createdAt: string
|
||||||
}
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
export type ConsignmentStatsSummary = {
|
export type ConsignmentStatsSummary = {
|
||||||
totalInStockCount: number
|
totalInStockCount: number
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/** 寄卖「对接客服」展示工具:后台列表、详情、工单与打手端统一口径。 */
|
||||||
|
export type ConsignmentContactDisplay = {
|
||||||
|
/** 姓名;没有对接客服时为空串 */
|
||||||
|
name: string
|
||||||
|
/** 联系方式(电话 / 微信),没有时为空串 */
|
||||||
|
contact: string
|
||||||
|
/** 姓名 + 联系方式,列表里直接展示 */
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConsignmentContactLike =
|
||||||
|
| {
|
||||||
|
contactSupporterName?: string | null
|
||||||
|
contactSupporterPhone?: string | null
|
||||||
|
contactSupporterWechat?: string | null
|
||||||
|
}
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
|
||||||
|
export function formatConsignmentContactSupporter(
|
||||||
|
source: ConsignmentContactLike,
|
||||||
|
emptyText = '未指定',
|
||||||
|
): ConsignmentContactDisplay {
|
||||||
|
const name = String(source?.contactSupporterName || '').trim()
|
||||||
|
const phone = String(source?.contactSupporterPhone || '').trim()
|
||||||
|
const wechat = String(source?.contactSupporterWechat || '').trim()
|
||||||
|
|
||||||
|
const parts: string[] = []
|
||||||
|
if (phone) parts.push(`电话 ${phone}`)
|
||||||
|
if (wechat) parts.push(`微信 ${wechat}`)
|
||||||
|
const contact = parts.join(' · ')
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return { name: '', contact, text: contact || emptyText }
|
||||||
|
}
|
||||||
|
return { name, contact, text: contact ? `${name} · ${contact}` : name }
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
|
import { formatConsignmentContactSupporter } from '../src/utils/consignment-contact.ts'
|
||||||
|
|
||||||
|
test('对接客服:姓名 + 电话/微信一起展示', () => {
|
||||||
|
const display = formatConsignmentContactSupporter({
|
||||||
|
contactSupporterName: '小张',
|
||||||
|
contactSupporterPhone: '13800001111',
|
||||||
|
contactSupporterWechat: 'wx_zhang',
|
||||||
|
})
|
||||||
|
assert.equal(display.name, '小张')
|
||||||
|
assert.equal(display.contact, '电话 13800001111 · 微信 wx_zhang')
|
||||||
|
assert.equal(display.text, '小张 · 电话 13800001111 · 微信 wx_zhang')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('对接客服:只有姓名时不拼联系方式', () => {
|
||||||
|
const display = formatConsignmentContactSupporter({ contactSupporterName: '小李' })
|
||||||
|
assert.equal(display.text, '小李')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('对接客服:历史数据没有姓名时回退为未指定', () => {
|
||||||
|
assert.equal(formatConsignmentContactSupporter(null).text, '未指定')
|
||||||
|
assert.equal(formatConsignmentContactSupporter({}).text, '未指定')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('对接客服:自定义空值文案', () => {
|
||||||
|
assert.equal(formatConsignmentContactSupporter(undefined, '-').text, '-')
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user