From 36fb994acd2b4ba00f88fff7ae4104b7373b9cf1 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 10 Jul 2026 17:46:40 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=BF=AB=E6=89=8B=E7=94=B5?= =?UTF-8?q?=E5=AD=90=E5=87=AD=E8=AF=81=EF=BC=9A=E5=AE=A2=E6=9C=8D=E6=A0=B8?= =?UTF-8?q?=E9=94=80=E9=A1=B5=E3=80=81=E5=BA=97=E9=93=BA=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E4=B8=8E=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 客服角色仅展示「券码核销」Tab,本地列表直接核销 - 卖家列改为店铺名+ID 展示,并返回 shopName - 新增轻量店铺列表接口,支持按店铺下拉筛选 - 修复 asRecord 空值导致的前端构建类型错误 --- .../src/routes/admin/kuaishou-industry.ts | 11 + .../admin/kuaishou-industry-admin-service.ts | 58 ++- .../pages/admin/AdminKuaishouIndustryPage.tsx | 463 +++++++++++++----- .../src/services/admin/kuaishou-industry.ts | 5 + apps/frontend/src/types/admin/index.ts | 2 + .../src/types/admin/kuaishou-industry.ts | 14 + 6 files changed, 414 insertions(+), 139 deletions(-) diff --git a/apps/backend/src/routes/admin/kuaishou-industry.ts b/apps/backend/src/routes/admin/kuaishou-industry.ts index 0879dfde..c9bd2b2d 100644 --- a/apps/backend/src/routes/admin/kuaishou-industry.ts +++ b/apps/backend/src/routes/admin/kuaishou-industry.ts @@ -5,6 +5,7 @@ import { consumeAdminKuaishouIndustryVoucherByCode, disagreeAdminKuaishouIndustryRefund, listAdminKuaishouIndustryRefunds, + listAdminKuaishouIndustryShops, listAdminKuaishouIndustryVouchers, resendAdminKuaishouIndustryVoucherCode, reverseAdminKuaishouIndustryVoucher, @@ -35,6 +36,16 @@ router.get( ), ) +router.get( + '/kuaishou-industry/shops', + requireAdminRoles(['admin', 'operator', 'support']), + createJsonHandler(() => listAdminKuaishouIndustryShops(), { + successMessage: 'ok', + errorMessage: '查询快手行业店铺失败', + scope: '[admin/kuaishou-industry/shops]', + }), +) + router.post( '/kuaishou-industry/refunds/list', requireAdminRoles(['admin', 'operator']), diff --git a/apps/backend/src/services/admin/kuaishou-industry-admin-service.ts b/apps/backend/src/services/admin/kuaishou-industry-admin-service.ts index 8a2d801a..71b6fa06 100644 --- a/apps/backend/src/services/admin/kuaishou-industry-admin-service.ts +++ b/apps/backend/src/services/admin/kuaishou-industry-admin-service.ts @@ -32,9 +32,12 @@ export async function listAdminKuaishouIndustryVouchers( input: KuaishouIndustryVoucherAdminListQuery = {}, ) { const result = await listKuaishouIndustryVouchersForAdmin(input) + const shopNameBySellerId = buildKuaishouIndustryShopNameMap() return { - items: result.items.map(mapAdminKuaishouIndustryVoucher), + items: result.items.map((voucher) => + mapAdminKuaishouIndustryVoucher(voucher, shopNameBySellerId), + ), pagination: { page: result.page, pageSize: result.pageSize, @@ -43,6 +46,27 @@ export async function listAdminKuaishouIndustryVouchers( } } +/** 轻量店铺选项:仅展示名与 ID,供筛选/下拉使用(不含 token 等敏感字段) */ +export function listAdminKuaishouIndustryShops() { + const shops = listKuaishouIndustryShopConfigs(getKuaishouIndustrySourceConfig()) + + return { + shops: shops + .map((shop) => { + const sellerId = String(shop.sellerId || '').trim() + const shopId = String(shop.shopId || '').trim() + const shopName = String(shop.customShopName || shop.shopName || '').trim() + return { + sellerId, + shopId, + shopName, + enabled: shop.enabled !== false, + } + }) + .filter((shop) => shop.sellerId || shop.shopId), + } +} + export async function listAdminKuaishouIndustryRefunds(input: JsonObject = {}) { assertRequired(input.beginTime, '开始时间未填写') assertRequired(input.endTime, '结束时间未填写') @@ -370,7 +394,11 @@ function sanitizeOpenApiRequest(request: JsonObject | undefined): JsonObject | n } } -function mapAdminKuaishouIndustryVoucher(voucher: KuaishouIndustryVoucherRow) { +function mapAdminKuaishouIndustryVoucher( + voucher: KuaishouIndustryVoucherRow, + shopNameBySellerId: Map = buildKuaishouIndustryShopNameMap(), +) { + const sellerId = String(voucher.seller_id || '').trim() return { id: voucher.id, voucherCode: voucher.voucher_code, @@ -378,7 +406,8 @@ function mapAdminKuaishouIndustryVoucher(voucher: KuaishouIndustryVoucherRow) { orderId: voucher.order_id, taskId: voucher.task_id, unitIndex: voucher.unit_index, - sellerId: voucher.seller_id, + sellerId, + shopName: shopNameBySellerId.get(sellerId) || '', tokenMasked: maskSecret(voucher.token), status: voucher.status, validStartTime: Number(voucher.valid_start_time || 0) || 0, @@ -397,6 +426,29 @@ function mapAdminKuaishouIndustryVoucher(voucher: KuaishouIndustryVoucherRow) { } } +function buildKuaishouIndustryShopNameMap(): Map { + const map = new Map() + const shops = listKuaishouIndustryShopConfigs(getKuaishouIndustrySourceConfig()) + + for (const shop of shops) { + const name = String(shop.customShopName || shop.shopName || '').trim() + if (!name) { + continue + } + + const sellerId = String(shop.sellerId || '').trim() + const shopId = String(shop.shopId || '').trim() + if (sellerId) { + map.set(sellerId, name) + } + if (shopId && shopId !== sellerId) { + map.set(shopId, name) + } + } + + return map +} + function mapTaskReference(task: TaskRow) { return { taskId: task.id, diff --git a/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx b/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx index 9c7328ca..cc6b374e 100644 --- a/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx +++ b/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx @@ -14,6 +14,7 @@ import { Descriptions, Input, InputNumber, + Popconfirm, Select, Space, Table, @@ -33,7 +34,7 @@ import { checkAdminKuaishouIndustryVoucherAvailable, consumeAdminKuaishouIndustryVoucher, disagreeAdminKuaishouIndustryRefund, - fetchAdminKuaishouIndustrySourceConfig, + fetchAdminKuaishouIndustryShops, fetchAdminKuaishouIndustryVouchers, listAdminKuaishouIndustryRefunds, resendAdminKuaishouIndustryVoucherCode, @@ -44,11 +45,11 @@ import type { AdminKuaishouIndustryRefundApprovePayload, AdminKuaishouIndustryRefundDisagreePayload, AdminKuaishouIndustryRefundListPayload, - AdminKuaishouIndustryShopConfig, + AdminKuaishouIndustryShopOption, AdminKuaishouIndustryVoucher, AdminKuaishouIndustryVoucherToolPayload, } from '@/types/admin' -import { hasAdminRole } from '@/utils/admin-auth' +import { getAdminRole, hasAdminRole } from '@/utils/admin-auth' import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminLocalTablePagination, @@ -58,7 +59,7 @@ import { formatAdminDateTime } from '@/utils/admin-time' import { stringifyDisplayJson } from '@/utils/date-time' type DateRangeValue = [Dayjs | null, Dayjs | null] | null -type IndustryTab = 'vouchers' | 'refunds' | 'result' +type IndustryTab = 'support-consume' | 'vouchers' | 'refunds' | 'result' type IndustryActionScope = 'vouchers' | 'refunds' type IndustryActionKind = | 'check' @@ -109,10 +110,14 @@ type RefundListState = { const DEFAULT_ETICKET_TYPE = 'DINING_OPEN_TICKET' export default function AdminKuaishouIndustryPage() { - const [activeTab, setActiveTab] = useState('vouchers') + const isSupportOnly = getAdminRole() === 'support' const canManageRefund = hasAdminRole('operator') + const [activeTab, setActiveTab] = useState( + isSupportOnly ? 'support-consume' : 'vouchers', + ) const [voucherLoading, setVoucherLoading] = useState(false) const [actionLoading, setActionLoading] = useState('') + const [consumingVoucherId, setConsumingVoucherId] = useState(null) const [vouchers, setVouchers] = useState([]) const [voucherTotal, setVoucherTotal] = useState(0) const [voucherFilters, setVoucherFilters] = useState({ @@ -158,7 +163,17 @@ export default function AdminKuaishouIndustryPage() { }) const [lastActionResult, setLastActionResult] = useState(null) const [refundRows, setRefundRows] = useState>>([]) - const [industryShops, setIndustryShops] = useState([]) + const [industryShops, setIndustryShops] = useState([]) + const shopFilterOptions = useMemo( + () => + industryShops + .filter((shop) => shop.sellerId) + .map((shop) => ({ + label: formatShopOptionLabel(shop), + value: shop.sellerId, + })), + [industryShops], + ) const refundShopOptions = useMemo( () => industryShops @@ -169,6 +184,24 @@ export default function AdminKuaishouIndustryPage() { })), [industryShops], ) + const shopNameBySellerId = useMemo(() => { + const map = new Map() + for (const shop of industryShops) { + const name = String(shop.shopName || '').trim() + if (!name) { + continue + } + const sellerId = String(shop.sellerId || '').trim() + const shopId = String(shop.shopId || '').trim() + if (sellerId) { + map.set(sellerId, name) + } + if (shopId) { + map.set(shopId, name) + } + } + return map + }, [industryShops]) const columns = useMemo>( () => [ @@ -209,14 +242,14 @@ export default function AdminKuaishouIndustryPage() { ), }, { - title: '卖家', - minWidth: 160, - render: (_, row) => ( -
- {row.sellerId || '-'} - Token:{row.tokenMasked || '-'} -
- ), + title: '店铺', + minWidth: 180, + render: (_, row) => + renderShopCell( + row.shopName || shopNameBySellerId.get(String(row.sellerId || '').trim()) || '', + row.sellerId, + isSupportOnly ? '' : row.tokenMasked, + ), }, { title: '核销', @@ -228,30 +261,69 @@ export default function AdminKuaishouIndustryPage() { ), }, - { - title: '更新时间', - width: 170, - render: (_, row) => formatAdminDateTime(row.updatedAt), - }, + ...(isSupportOnly + ? [] + : [ + { + title: '更新时间', + width: 170, + render: (_: unknown, row: AdminKuaishouIndustryVoucher) => + formatAdminDateTime(row.updatedAt), + }, + ]), { title: '操作', - fixed: 'right', - width: 120, - render: (_, row) => ( - - ), + fixed: 'right' as const, + width: isSupportOnly ? 100 : 120, + render: (_: unknown, row: AdminKuaishouIndustryVoucher) => { + if (isSupportOnly) { + const canConsume = row.status === 'UNUSED' + return ( + consumeVoucherRow(row)} + > + + + ) + } + + return ( + + ) + }, }, ], - [], + [consumingVoucherId, isSupportOnly, shopNameBySellerId], ) const refundColumns = useMemo>>( () => [ { title: '售后单', dataIndex: 'refundId', minWidth: 160 }, { title: '订单', dataIndex: 'oid', minWidth: 160 }, - { title: '卖家', dataIndex: 'sellerId', minWidth: 140 }, + { + title: '店铺', + dataIndex: 'sellerId', + minWidth: 180, + render: (_, row) => { + const sellerId = String(row.sellerId || '').trim() + return renderShopCell(shopNameBySellerId.get(sellerId) || '', sellerId) + }, + }, { title: '状态', dataIndex: 'status', width: 100 }, { title: '协商', dataIndex: 'negotiateStatus', width: 100 }, { title: '金额', dataIndex: 'refundFee', width: 100 }, @@ -271,18 +343,18 @@ export default function AdminKuaishouIndustryPage() { ), }, ], - [], + [shopNameBySellerId], ) useEffect(() => { - void loadVouchers() void loadIndustryShops() + void loadVouchers() }, []) async function loadIndustryShops() { try { - const response = await fetchAdminKuaishouIndustrySourceConfig() - const shops = response.data.source.shops || [] + const response = await fetchAdminKuaishouIndustryShops() + const shops = response.data.shops || [] setIndustryShops(shops) const enabledSellerIds = shops @@ -292,7 +364,8 @@ export default function AdminKuaishouIndustryPage() { applyDefaultSellerId(enabledSellerIds[0] as string) } } catch (error) { - showError(error instanceof Error ? error.message : '读取快手授权店铺失败') + // 店铺下拉失败不阻断列表,仅回退为手动输入店铺 ID + console.warn(error instanceof Error ? error.message : '读取店铺列表失败') } } @@ -372,6 +445,50 @@ export default function AdminKuaishouIndustryPage() { } } + async function consumeVoucherRow(row: AdminKuaishouIndustryVoucher) { + if (row.status !== 'UNUSED') { + showError('仅未使用券码可核销') + return + } + + setConsumingVoucherId(row.id) + setActionLoading('consume') + try { + const response = await consumeAdminKuaishouIndustryVoucher({ + sellerId: row.sellerId, + oid: row.oid, + orderId: row.oid, + taskId: row.taskId || '', + voucherCode: row.voucherCode, + eticketType: DEFAULT_ETICKET_TYPE, + consumeType: 'consume', + serialNum: row.consumeSerialNum || undefined, + etickets: [{ id: row.voucherCode, code: row.voucherCode, num: 1 }], + }) + if (!isSupportOnly) { + publishActionResult( + buildIndustryActionResultView('consume', '手动核销', response.data), + ) + } + showSuccess(`券码 ${row.voucherCode} 已核销`) + await loadVouchers() + } catch (error) { + const message = error instanceof Error ? error.message : '核销失败' + if (!isSupportOnly) { + publishActionResult( + buildIndustryActionResultView('consume', '手动核销', { + success: false, + error: message, + }), + ) + } + showError(message) + } finally { + setConsumingVoucherId(null) + setActionLoading('') + } + } + async function runResendCode() { setActionLoading('resend') try { @@ -403,7 +520,7 @@ export default function AdminKuaishouIndustryPage() { const [begin, end] = refundDateRange || [] const sellerId = resolveRefundSellerId(refundListForm.sellerId) if (!sellerId && refundShopOptions.length > 1) { - showError('请先选择快手售后退款店铺 sellerId') + showError('请先选择快手售后退款店铺') return } @@ -429,7 +546,7 @@ export default function AdminKuaishouIndustryPage() { async function runApproveRefund() { const sellerId = resolveRefundSellerId(approveForm.sellerId) if (!sellerId && refundShopOptions.length > 1) { - showError('请先选择快手售后退款店铺 sellerId') + showError('请先选择快手售后退款店铺') return } @@ -445,7 +562,7 @@ export default function AdminKuaishouIndustryPage() { async function runDisagreeRefund() { const sellerId = resolveRefundSellerId(disagreeForm.sellerId) if (!sellerId && refundShopOptions.length > 1) { - showError('请先选择快手售后退款店铺 sellerId') + showError('请先选择快手售后退款店铺') return } @@ -537,33 +654,47 @@ export default function AdminKuaishouIndustryPage() { showSuccess('已填入退款操作区') } - const tabItems: TabsProps['items'] = [ - { - key: 'vouchers', - label: '券码操作', - children: renderVoucherTab(), - }, - ...(canManageRefund - ? [ - { - key: 'refunds', - label: '售后退款', - children: renderRefundTab(), - }, - ] - : []), - { - key: 'result', - label: lastActionResult ? `接口结果 · ${lastActionResult.title}` : '接口结果', - children: renderResultTab(), - }, - ] + const tabItems: TabsProps['items'] = isSupportOnly + ? [ + { + key: 'support-consume', + label: '券码核销', + children: renderSupportConsumeTab(), + }, + ] + : [ + { + key: 'vouchers', + label: '券码操作', + children: renderVoucherTab(), + }, + ...(canManageRefund + ? [ + { + key: 'refunds', + label: '售后退款', + children: renderRefundTab(), + }, + ] + : []), + { + key: 'result', + label: lastActionResult ? `接口结果 · ${lastActionResult.title}` : '接口结果', + children: renderResultTab(), + }, + ] return (
) - function renderVoucherTab() { + function renderVoucherFilters() { return ( -
- - - + + + setVoucherFilters({ + ...voucherFilters, + oid: event.target.value, + }) + } + /> + + setVoucherFilters({ + ...voucherFilters, + voucherCode: event.target.value, + }) + } + /> + + setVoucherFilters({ + ...voucherFilters, + taskId: event.target.value, + }) + } + /> + {shopFilterOptions.length > 0 ? ( + - setVoucherFilters({ - ...voucherFilters, - voucherCode: event.target.value, - }) - } - /> - - setVoucherFilters({ - ...voucherFilters, - taskId: event.target.value, - }) - } - /> - setVoucherFilters({ @@ -623,50 +770,71 @@ export default function AdminKuaishouIndustryPage() { }) } /> - setVoucherFilters({ ...voucherFilters, status: value || '' })} + /> + + + + ) + } + + function renderVoucherTable(scrollX = 1180) { + return ( + loadVouchers({ page, pageSize }), + })} + /> + ) + } + + function renderSupportConsumeTab() { + return ( +
+ {renderVoucherFilters()} + {renderVoucherTable(1080)} +
+ ) + } + + function renderVoucherTab() { + return ( +
+ {renderVoucherFilters()}
- -
loadVouchers({ page, pageSize }), - })} - /> - + {renderVoucherTable()}
updateToolForm({ sellerId: event.target.value })} /> @@ -1115,7 +1283,7 @@ export default function AdminKuaishouIndustryPage() { return ( onChange(event.target.value)} /> @@ -1123,6 +1291,28 @@ export default function AdminKuaishouIndustryPage() { } } +function renderShopCell( + shopName?: string | null, + sellerId?: string | null, + tokenMasked?: string | null, +) { + const name = String(shopName || '').trim() + const id = String(sellerId || '').trim() + const token = String(tokenMasked || '').trim() + + if (!name && !id) { + return - + } + + return ( +
+ {name || id} + {name && id ? ID:{id} : null} + {token ? Token:{token} : null} +
+ ) +} + function extractRefundRows( response: Record | null, ): Array> { @@ -1409,11 +1599,12 @@ function getVoucherStatusLabel(status: string) { return status || '-' } -function formatShopOptionLabel(shop: AdminKuaishouIndustryShopConfig) { - const name = shop.customShopName || shop.shopName || '未命名店铺' - const tokenStatus = shop.hasAccessToken ? '' : ' / 未授权' +function formatShopOptionLabel(shop: AdminKuaishouIndustryShopOption) { + const name = String(shop.shopName || '').trim() || '未命名店铺' + const id = shop.sellerId || shop.shopId || '' + const disabledSuffix = shop.enabled === false ? ' · 已停用' : '' - return `${name} / ${shop.sellerId}${tokenStatus}` + return id ? `${name}(${id})${disabledSuffix}` : `${name}${disabledSuffix}` } function formatTimestamp(value: unknown) { diff --git a/apps/frontend/src/services/admin/kuaishou-industry.ts b/apps/frontend/src/services/admin/kuaishou-industry.ts index ab45c787..7b34f1f8 100644 --- a/apps/frontend/src/services/admin/kuaishou-industry.ts +++ b/apps/frontend/src/services/admin/kuaishou-industry.ts @@ -4,6 +4,7 @@ import type { AdminKuaishouIndustryRefundApprovePayload, AdminKuaishouIndustryRefundDisagreePayload, AdminKuaishouIndustryRefundListPayload, + AdminKuaishouIndustryShopListResult, AdminKuaishouIndustryVoucherConsumeResult, AdminKuaishouIndustryVoucherListResult, AdminKuaishouIndustryVoucherResendResult, @@ -17,6 +18,10 @@ export function fetchAdminKuaishouIndustryVouchers(params?: Record('/api/v1/admin/kuaishou-industry/shops') +} + export function listAdminKuaishouIndustryRefunds( payload: AdminKuaishouIndustryRefundListPayload, ) { diff --git a/apps/frontend/src/types/admin/index.ts b/apps/frontend/src/types/admin/index.ts index 53383859..f6418129 100644 --- a/apps/frontend/src/types/admin/index.ts +++ b/apps/frontend/src/types/admin/index.ts @@ -32,6 +32,8 @@ export type { AdminKuaishouIndustryOpenApiResult, AdminKuaishouIndustryVoucher, AdminKuaishouIndustryVoucherListResult, + AdminKuaishouIndustryShopOption, + AdminKuaishouIndustryShopListResult, AdminKuaishouIndustryVoucherConsumeResult, AdminKuaishouIndustryVoucherResendResult, AdminKuaishouIndustryRefundListPayload, diff --git a/apps/frontend/src/types/admin/kuaishou-industry.ts b/apps/frontend/src/types/admin/kuaishou-industry.ts index cc9a924c..4944d646 100644 --- a/apps/frontend/src/types/admin/kuaishou-industry.ts +++ b/apps/frontend/src/types/admin/kuaishou-industry.ts @@ -19,6 +19,8 @@ export interface AdminKuaishouIndustryVoucher { taskId: number | null unitIndex: number sellerId: string + /** 来自行业店铺配置的展示名(customShopName 优先) */ + shopName: string tokenMasked: string status: string validStartTime: number @@ -41,6 +43,18 @@ export interface AdminKuaishouIndustryVoucherListResult { pagination: AdminPagination } +/** 电子凭证页筛选用的轻量店铺选项(无 token 等敏感字段) */ +export interface AdminKuaishouIndustryShopOption { + sellerId: string + shopId: string + shopName: string + enabled: boolean +} + +export interface AdminKuaishouIndustryShopListResult { + shops: AdminKuaishouIndustryShopOption[] +} + export interface AdminKuaishouIndustryVoucherConsumeResult { success: boolean voucher: AdminKuaishouIndustryVoucher