From 763f5034fda322bf198aa19063fefd8e1cafbfa3 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 10 Jul 2026 09:53:42 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=80=80=E6=AC=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/kuaishou-industry-admin-service.ts | 133 ++++++++- apps/backend/src/types/admin/write-inputs.ts | 2 + .../pages/admin/AdminKuaishouIndustryPage.tsx | 267 ++++++++++++++---- .../src/types/admin/kuaishou-industry.ts | 2 + 4 files changed, 342 insertions(+), 62 deletions(-) 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 c8bbb595..1b1c6138 100644 --- a/apps/backend/src/services/admin/kuaishou-industry-admin-service.ts +++ b/apps/backend/src/services/admin/kuaishou-industry-admin-service.ts @@ -11,6 +11,11 @@ import { getTaskById } from '../../repositories/task-repo.js' import { createHttpError } from '../../utils/http.js' import { maskSecret } from '../../utils/masking.js' import { checkKuaishouIndustryEticketAvailable } from '../platforms/kuaishou-industry/check-available-service.js' +import { + getKuaishouIndustrySourceConfig, + listKuaishouIndustryShopConfigs, + type KuaishouIndustrySourceConfig, +} from '../platforms/kuaishou-industry/source-config-service.js' import { resendKuaishouIndustryVoucherSendCallback } from '../platforms/kuaishou-industry/send-code-service.js' import { consumeKuaishouIndustryVoucher } from '../platforms/kuaishou-industry/voucher-service.js' import { @@ -43,14 +48,18 @@ export async function listAdminKuaishouIndustryRefunds(input: JsonObject = {}) { assertRequired(input.beginTime, '开始时间未填写') assertRequired(input.endTime, '结束时间未填写') - return mapOpenApiResult(await listKuaishouIndustryRefunds(input)) + const payload = await buildRefundOpenApiPayload(input) + + return mapOpenApiResult(await listKuaishouIndustryRefunds(payload)) } export async function approveAdminKuaishouIndustryRefund(input: JsonObject = {}) { assertRequired(input.refundId, '退款单编号未填写') assertRequired(input.refundAmount, '退款金额未填写') - return mapOpenApiResult(await approveKuaishouIndustryRefund(input)) + const payload = await buildRefundOpenApiPayload(input) + + return mapOpenApiResult(await approveKuaishouIndustryRefund(payload)) } export async function disagreeAdminKuaishouIndustryRefund(input: JsonObject = {}) { @@ -60,7 +69,9 @@ export async function disagreeAdminKuaishouIndustryRefund(input: JsonObject = {} assertRequired(input.status, '退款单当前状态未填写') assertRequired(input.negotiateStatus, '协商状态未填写') - return mapOpenApiResult(await disagreeKuaishouIndustryRefund(input)) + const payload = await buildRefundOpenApiPayload(input) + + return mapOpenApiResult(await disagreeKuaishouIndustryRefund(payload)) } export async function checkAdminKuaishouIndustryVoucherAvailable(input: JsonObject = {}) { @@ -167,21 +178,111 @@ function buildVoucherOpenApiPayload( orderId: String(input.orderId || input.oid || voucher?.oid || '').trim(), token: String(input.token || voucher?.token || '').trim(), serialNum: String(input.serialNum || voucher?.consume_serial_num || '').trim(), - etickets: etickets.length > 0 - ? etickets - : voucherCode - ? [{ id: voucherCode, code: voucherCode, num: 1 }] - : [], + etickets: + etickets.length > 0 + ? etickets + : voucherCode + ? [{ id: voucherCode, code: voucherCode, num: 1 }] + : [], } } -async function resolveOptionalVoucher(input: JsonObject): Promise { +async function buildRefundOpenApiPayload(input: JsonObject): Promise { + const source = getKuaishouIndustrySourceConfig() + const sellerId = + String(input.sellerId || '').trim() || + (await resolveRefundSellerIdFromLocalVoucher(input)) || + resolveSingleEnabledKuaishouIndustrySellerId(source) + + if (!sellerId && hasMultipleEnabledKuaishouIndustryShops(source)) { + throw createHttpError('存在多个快手售后退款店铺授权,请先选择 sellerId', { + statusCode: 400, + errorCode: 'admin_kuaishou_industry_refund_seller_id_required', + }) + } + + return { + ...input, + ...(sellerId ? { sellerId } : {}), + } +} + +async function resolveRefundSellerIdFromLocalVoucher(input: JsonObject): Promise { + const voucherCode = String(input.voucherCode || '').trim() + const oid = String(input.oid || input.orderId || '').trim() + if (voucherCode) { + const voucher = await findKuaishouIndustryVoucherByCode(voucherCode, oid) + return String(voucher?.seller_id || '').trim() + } + + const taskId = Number(input.taskId || 0) + if (Number.isFinite(taskId) && taskId > 0) { + return resolveSingleSellerIdFromVouchers( + await listKuaishouIndustryVouchersByTaskId(Math.trunc(taskId)), + '当前任务关联多个卖家,请指定 sellerId', + ) + } + + if (oid) { + return resolveSingleSellerIdFromVouchers( + await listKuaishouIndustryVouchersByOid(oid), + '当前订单关联多个卖家,请指定 sellerId', + ) + } + + return '' +} + +function resolveSingleSellerIdFromVouchers( + vouchers: KuaishouIndustryVoucherRow[], + multipleMessage: string, +): string { + const sellerIds = Array.from( + new Set(vouchers.map((voucher) => String(voucher.seller_id || '').trim()).filter(Boolean)), + ) + + if (sellerIds.length > 1) { + throw createHttpError(multipleMessage, { + statusCode: 409, + errorCode: 'admin_kuaishou_industry_refund_seller_id_ambiguous', + }) + } + + return sellerIds[0] || '' +} + +function resolveSingleEnabledKuaishouIndustrySellerId( + source: KuaishouIndustrySourceConfig, +): string { + const enabledShops = listEnabledKuaishouIndustryShops(source) + + return enabledShops.length === 1 + ? String(enabledShops[0]?.sellerId || enabledShops[0]?.shopId || '').trim() + : '' +} + +function hasMultipleEnabledKuaishouIndustryShops(source: KuaishouIndustrySourceConfig): boolean { + return listEnabledKuaishouIndustryShops(source).length > 1 +} + +function listEnabledKuaishouIndustryShops(source: KuaishouIndustrySourceConfig) { + return listKuaishouIndustryShopConfigs(source).filter( + (shop) => shop.enabled !== false && String(shop.sellerId || shop.shopId || '').trim(), + ) +} + +async function resolveOptionalVoucher( + input: JsonObject, +): Promise { const voucherCode = String(input.voucherCode || '').trim() if (!voucherCode) { return null } - const voucher = await findKuaishouIndustryVoucherByCode(voucherCode, String(input.oid || input.orderId || '').trim()) + const voucher = await findKuaishouIndustryVoucherByCode( + voucherCode, + String(input.oid || input.orderId || '').trim(), + ) if (!voucher) { throw createHttpError('电子凭证不存在', { statusCode: 404, @@ -201,13 +302,21 @@ async function resolveRequiredVoucher(input: JsonObject): Promise 0) { const vouchers = await listKuaishouIndustryVouchersByTaskId(Math.trunc(taskId)) - return resolveSingleVoucher(vouchers, '当前任务没有关联电子凭证', '当前任务关联多个电子凭证,请指定券码') + return resolveSingleVoucher( + vouchers, + '当前任务没有关联电子凭证', + '当前任务关联多个电子凭证,请指定券码', + ) } const oid = String(input.oid || input.orderId || '').trim() if (oid) { const vouchers = await listKuaishouIndustryVouchersByOid(oid) - return resolveSingleVoucher(vouchers, '当前订单没有关联电子凭证', '当前订单关联多个电子凭证,请指定券码') + return resolveSingleVoucher( + vouchers, + '当前订单没有关联电子凭证', + '当前订单关联多个电子凭证,请指定券码', + ) } throw createHttpError('请填写券码、任务 ID 或订单号', { diff --git a/apps/backend/src/types/admin/write-inputs.ts b/apps/backend/src/types/admin/write-inputs.ts index 866ffa32..3881ca88 100644 --- a/apps/backend/src/types/admin/write-inputs.ts +++ b/apps/backend/src/types/admin/write-inputs.ts @@ -262,6 +262,7 @@ export type AdminKuaishouIndustryRefundListInput = { export type AdminKuaishouIndustryRefundApproveInput = { sellerId?: string + orderId?: string refundId?: number | string desc?: string refundAmount?: number | string @@ -272,6 +273,7 @@ export type AdminKuaishouIndustryRefundApproveInput = { export type AdminKuaishouIndustryRefundDisagreeInput = { sellerId?: string + orderId?: string refundId?: number | string sellerDisagreeReason?: number | string sellerDisagreeDesc?: string diff --git a/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx b/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx index 235c8c37..43504a3b 100644 --- a/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx +++ b/apps/frontend/src/pages/admin/AdminKuaishouIndustryPage.tsx @@ -32,6 +32,7 @@ import { checkAdminKuaishouIndustryVoucherAvailable, consumeAdminKuaishouIndustryVoucher, disagreeAdminKuaishouIndustryRefund, + fetchAdminKuaishouIndustrySourceConfig, fetchAdminKuaishouIndustryVouchers, listAdminKuaishouIndustryRefunds, resendAdminKuaishouIndustryVoucherCode, @@ -42,6 +43,7 @@ import type { AdminKuaishouIndustryRefundApprovePayload, AdminKuaishouIndustryRefundDisagreePayload, AdminKuaishouIndustryRefundListPayload, + AdminKuaishouIndustryShopConfig, AdminKuaishouIndustryVoucher, AdminKuaishouIndustryVoucherToolPayload, } from '@/types/admin' @@ -124,6 +126,17 @@ export default function AdminKuaishouIndustryPage() { const [lastResultTitle, setLastResultTitle] = useState('接口结果') const [lastResult, setLastResult] = useState(null) const [refundRows, setRefundRows] = useState>>([]) + const [industryShops, setIndustryShops] = useState([]) + const refundShopOptions = useMemo( + () => + industryShops + .filter((shop) => shop.enabled !== false && shop.sellerId) + .map((shop) => ({ + label: formatShopOptionLabel(shop), + value: shop.sellerId, + })), + [industryShops], + ) const columns = useMemo>( () => [ @@ -206,6 +219,7 @@ export default function AdminKuaishouIndustryPage() { () => [ { title: '售后单', dataIndex: 'refundId', minWidth: 160 }, { title: '订单', dataIndex: 'oid', minWidth: 160 }, + { title: '卖家', dataIndex: 'sellerId', minWidth: 140 }, { title: '状态', dataIndex: 'status', width: 100 }, { title: '协商', dataIndex: 'negotiateStatus', width: 100 }, { title: '金额', dataIndex: 'refundFee', width: 100 }, @@ -230,8 +244,26 @@ export default function AdminKuaishouIndustryPage() { useEffect(() => { void loadVouchers() + void loadIndustryShops() }, []) + async function loadIndustryShops() { + try { + const response = await fetchAdminKuaishouIndustrySourceConfig() + const shops = response.data.source.shops || [] + setIndustryShops(shops) + + const enabledSellerIds = shops + .filter((shop) => shop.enabled !== false && shop.sellerId) + .map((shop) => shop.sellerId) + if (enabledSellerIds.length === 1) { + applyDefaultSellerId(enabledSellerIds[0] as string) + } + } catch (error) { + showError(error instanceof Error ? error.message : '读取快手授权店铺失败') + } + } + async function loadVouchers(nextFilters: Partial = {}) { const filters = { ...voucherFilters, ...nextFilters } setVoucherFilters(filters) @@ -323,12 +355,22 @@ export default function AdminKuaishouIndustryPage() { async function runRefundList() { const [begin, end] = refundDateRange || [] - await runOpenApiAction('售后单列表', 'refund-list', () => - listAdminKuaishouIndustryRefunds({ - ...refundListForm, - beginTime: begin?.valueOf(), - endTime: end?.valueOf(), - } satisfies AdminKuaishouIndustryRefundListPayload), + const sellerId = resolveRefundSellerId(refundListForm.sellerId) + if (!sellerId && refundShopOptions.length > 1) { + showError('请先选择快手售后退款店铺 sellerId') + return + } + + await runOpenApiAction( + '售后单列表', + 'refund-list', + () => + listAdminKuaishouIndustryRefunds({ + ...refundListForm, + sellerId, + beginTime: begin?.valueOf(), + endTime: end?.valueOf(), + } satisfies AdminKuaishouIndustryRefundListPayload), (result) => { const rows = extractRefundRows(result.response) setRefundRows(rows) @@ -337,19 +379,33 @@ export default function AdminKuaishouIndustryPage() { } async function runApproveRefund() { + const sellerId = resolveRefundSellerId(approveForm.sellerId) + if (!sellerId && refundShopOptions.length > 1) { + showError('请先选择快手售后退款店铺 sellerId') + return + } + await runOpenApiAction('同意退款', 'refund-approve', () => approveAdminKuaishouIndustryRefund({ ...approveForm, - sellerId: approveForm.sellerId || refundListForm.sellerId, + sellerId, + orderId: approveForm.orderId || refundListForm.orderId, }), ) } async function runDisagreeRefund() { + const sellerId = resolveRefundSellerId(disagreeForm.sellerId) + if (!sellerId && refundShopOptions.length > 1) { + showError('请先选择快手售后退款店铺 sellerId') + return + } + await runOpenApiAction('不同意退款', 'refund-disagree', () => disagreeAdminKuaishouIndustryRefund({ ...disagreeForm, - sellerId: disagreeForm.sellerId || refundListForm.sellerId, + sellerId, + orderId: disagreeForm.orderId || refundListForm.orderId, }), ) } @@ -389,21 +445,33 @@ export default function AdminKuaishouIndustryPage() { function fillRefundAction(row: Record) { const refundId = String(row.refundId || '').trim() + const sellerId = String(row.sellerId || refundListForm.sellerId || '').trim() + const orderId = String(row.oid || row.orderId || refundListForm.orderId || '').trim() const status = String(row.status || '').trim() const negotiateStatus = String(row.negotiateStatus || '').trim() setApproveForm((current) => ({ ...current, + sellerId: sellerId || current.sellerId, + orderId: orderId || current.orderId, refundId, status, negotiateStatus, refundAmount: String(row.refundFee || current.refundAmount || ''), + refundHandingWay: String(row.handlingWay || current.refundHandingWay || ''), })) setDisagreeForm((current) => ({ ...current, + sellerId: sellerId || current.sellerId, + orderId: orderId || current.orderId, refundId, status, negotiateStatus: negotiateStatus || current.negotiateStatus, })) + setRefundListForm((current) => ({ + ...current, + sellerId: sellerId || current.sellerId, + orderId: orderId || current.orderId, + })) showSuccess('已填入退款操作区') } @@ -444,14 +512,22 @@ export default function AdminKuaishouIndustryPage() { allowClear placeholder="订单号 oid" value={voucherFilters.oid} - onChange={(event) => setVoucherFilters({ ...voucherFilters, oid: event.target.value })} + onChange={(event) => + setVoucherFilters({ + ...voucherFilters, + oid: event.target.value, + }) + } /> - setVoucherFilters({ ...voucherFilters, voucherCode: event.target.value }) + setVoucherFilters({ + ...voucherFilters, + voucherCode: event.target.value, + }) } /> - setVoucherFilters({ ...voucherFilters, taskId: event.target.value }) + setVoucherFilters({ + ...voucherFilters, + taskId: event.target.value, + }) } /> - setVoucherFilters({ ...voucherFilters, sellerId: event.target.value }) + setVoucherFilters({ + ...voucherFilters, + sellerId: event.target.value, + }) } /> - updateRefundListForm({ sellerId: event.target.value })} - /> + {renderSellerIdControl(refundListForm.sellerId, (sellerId) => + updateRefundListForm({ sellerId }), + )} updateRefundListForm({ type: value })} /> - updateRefundListForm({ status: event.target.value })} /> - updateRefundListForm({ pcursor: event.target.value })} /> + updateRefundListForm({ status: event.target.value })} + /> + updateRefundListForm({ pcursor: event.target.value })} + /> String(row.refundId || row.oid || JSON.stringify(row))} columns={refundColumns} dataSource={refundRows} - scroll={{ x: 980 }} + scroll={{ x: 1120 }} pagination={{ pageSize: 10 }} style={{ marginTop: 16 }} /> @@ -663,21 +754,27 @@ export default function AdminKuaishouIndustryPage() {
- setApproveForm({ ...approveForm, sellerId: event.target.value })} - /> + {renderSellerIdControl(approveForm.sellerId || refundListForm.sellerId, (sellerId) => + setApproveForm({ ...approveForm, sellerId }), + )} setApproveForm({ ...approveForm, refundId: event.target.value })} + onChange={(event) => + setApproveForm({ + ...approveForm, + refundId: event.target.value, + }) + } /> - setApproveForm({ ...approveForm, refundAmount: event.target.value }) + setApproveForm({ + ...approveForm, + refundAmount: event.target.value, + }) } /> - setApproveForm({ ...approveForm, negotiateStatus: event.target.value }) + setApproveForm({ + ...approveForm, + negotiateStatus: event.target.value, + }) } /> - setApproveForm({ ...approveForm, refundHandingWay: event.target.value }) + setApproveForm({ + ...approveForm, + refundHandingWay: event.target.value, + }) } />
- - setDisagreeForm({ ...disagreeForm, sellerId: event.target.value }) - } - /> + {renderSellerIdControl(disagreeForm.sellerId || refundListForm.sellerId, (sellerId) => + setDisagreeForm({ ...disagreeForm, sellerId }), + )} - setDisagreeForm({ ...disagreeForm, refundId: event.target.value }) + setDisagreeForm({ + ...disagreeForm, + refundId: event.target.value, + }) } /> - setDisagreeForm({ ...disagreeForm, sellerDisagreeReason: event.target.value }) + setDisagreeForm({ + ...disagreeForm, + sellerDisagreeReason: event.target.value, + }) } /> - setDisagreeForm({ ...disagreeForm, sellerDisagreeDesc: event.target.value }) + setDisagreeForm({ + ...disagreeForm, + sellerDisagreeDesc: event.target.value, + }) } /> - setDisagreeForm({ ...disagreeForm, status: event.target.value }) + setDisagreeForm({ + ...disagreeForm, + status: event.target.value, + }) } /> - setDisagreeForm({ ...disagreeForm, negotiateStatus: event.target.value }) + setDisagreeForm({ + ...disagreeForm, + negotiateStatus: event.target.value, + }) } />
@@ -810,9 +924,52 @@ export default function AdminKuaishouIndustryPage() { function updateRefundListForm(patch: Partial) { setRefundListForm((current) => ({ ...current, ...patch })) } + + function applyDefaultSellerId(sellerId: string) { + setRefundListForm((current) => (current.sellerId ? current : { ...current, sellerId })) + setApproveForm((current) => (current.sellerId ? current : { ...current, sellerId })) + setDisagreeForm((current) => (current.sellerId ? current : { ...current, sellerId })) + setToolForm((current) => (current.sellerId ? current : { ...current, sellerId })) + } + + function resolveRefundSellerId(value?: unknown): string { + return ( + String(value || refundListForm.sellerId || '').trim() || + (refundShopOptions.length === 1 ? refundShopOptions[0]?.value || '' : '') + ) + } + + function renderSellerIdControl(value: string | undefined, onChange: (sellerId: string) => void) { + const normalizedValue = String(value || '').trim() + if (refundShopOptions.length > 0) { + return ( + onChange(event.target.value)} + /> + ) + } } -function extractRefundRows(response: Record | null): Array> { +function extractRefundRows( + response: Record | null, +): Array> { const data = response?.data if (!data || typeof data !== 'object' || Array.isArray(data)) { return [] @@ -827,12 +984,14 @@ function extractRefundRows(response: Record | null): Array - : {} - const response = result.response && typeof result.response === 'object' && !Array.isArray(result.response) - ? result.response as Record - : null + const result = + value && typeof value === 'object' && !Array.isArray(value) + ? (value as Record) + : {} + const response = + result.response && typeof result.response === 'object' && !Array.isArray(result.response) + ? (result.response as Record) + : null const success = Boolean(result.success) const code = String(response?.code || response?.sub_code || '').trim() const resultCode = String(response?.result || '').trim() @@ -856,7 +1015,8 @@ function resolveOpenApiResultSummary(value: unknown) { return { success, title: success ? '接口返回成功' : '接口返回失败', - description: [markers.join(' / '), message].filter(Boolean).join(',') || '请查看下方原始响应。', + description: + [markers.join(' / '), message].filter(Boolean).join(',') || '请查看下方原始响应。', } } @@ -875,6 +1035,13 @@ function getVoucherStatusLabel(status: string) { return status || '-' } +function formatShopOptionLabel(shop: AdminKuaishouIndustryShopConfig) { + const name = shop.customShopName || shop.shopName || '未命名店铺' + const tokenStatus = shop.hasAccessToken ? '' : ' / 未授权' + + return `${name} / ${shop.sellerId}${tokenStatus}` +} + function formatTimestamp(value: unknown) { const timestamp = Number(value || 0) if (!Number.isFinite(timestamp) || timestamp <= 0) { diff --git a/apps/frontend/src/types/admin/kuaishou-industry.ts b/apps/frontend/src/types/admin/kuaishou-industry.ts index df818755..cc9a924c 100644 --- a/apps/frontend/src/types/admin/kuaishou-industry.ts +++ b/apps/frontend/src/types/admin/kuaishou-industry.ts @@ -76,6 +76,7 @@ export interface AdminKuaishouIndustryRefundListPayload { export interface AdminKuaishouIndustryRefundApprovePayload { sellerId?: string + orderId?: string refundId?: number | string desc?: string refundAmount?: number | string @@ -86,6 +87,7 @@ export interface AdminKuaishouIndustryRefundApprovePayload { export interface AdminKuaishouIndustryRefundDisagreePayload { sellerId?: string + orderId?: string refundId?: number | string sellerDisagreeReason?: number | string sellerDisagreeDesc?: string