新增快手电子凭证后台工具
This commit is contained in:
@@ -13,6 +13,16 @@ type VoucherPatchColumn = {
|
||||
cast?: string
|
||||
}
|
||||
|
||||
export type KuaishouIndustryVoucherAdminListQuery = {
|
||||
oid?: string
|
||||
voucherCode?: string
|
||||
taskId?: number | string
|
||||
sellerId?: string
|
||||
status?: string
|
||||
page?: number | string
|
||||
pageSize?: number | string
|
||||
}
|
||||
|
||||
const VOUCHER_CODE_PREFIX = 'KSV'
|
||||
const VOUCHER_CODE_RANDOM_BYTES = 10
|
||||
const VOUCHER_CODE_MAX_GENERATE_ATTEMPTS = 5
|
||||
@@ -217,6 +227,75 @@ export async function listKuaishouIndustryVouchersByTaskId(
|
||||
return result.rows
|
||||
}
|
||||
|
||||
export async function listKuaishouIndustryVouchersForAdmin(
|
||||
input: KuaishouIndustryVoucherAdminListQuery = {},
|
||||
): Promise<{ items: KuaishouIndustryVoucherRow[]; total: number; page: number; pageSize: number }> {
|
||||
const page = normalizePositiveLimit(input.page, 1)
|
||||
const pageSize = Math.min(normalizePositiveLimit(input.pageSize, 50), 200)
|
||||
const where: string[] = []
|
||||
const params: unknown[] = []
|
||||
|
||||
const oid = String(input.oid || '').trim()
|
||||
if (oid) {
|
||||
params.push(oid)
|
||||
where.push(`oid = $${params.length}`)
|
||||
}
|
||||
|
||||
const voucherCode = String(input.voucherCode || '').trim()
|
||||
if (voucherCode) {
|
||||
params.push(Array.from(new Set([voucherCode, voucherCode.toUpperCase()])))
|
||||
where.push(`voucher_code = ANY($${params.length}::text[])`)
|
||||
}
|
||||
|
||||
const taskId = Number(input.taskId || 0)
|
||||
if (Number.isFinite(taskId) && taskId > 0) {
|
||||
params.push(Math.trunc(taskId))
|
||||
where.push(`task_id = $${params.length}`)
|
||||
}
|
||||
|
||||
const sellerId = String(input.sellerId || '').trim()
|
||||
if (sellerId) {
|
||||
params.push(sellerId)
|
||||
where.push(`seller_id = $${params.length}`)
|
||||
}
|
||||
|
||||
const status = String(input.status || '').trim().toUpperCase()
|
||||
if (status) {
|
||||
params.push(status)
|
||||
where.push(`status = $${params.length}`)
|
||||
}
|
||||
|
||||
const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : ''
|
||||
const totalResult = await query<{ total: string | number }>(
|
||||
`
|
||||
SELECT COUNT(*) AS total
|
||||
FROM kuaishou_industry_vouchers
|
||||
${whereSql}
|
||||
`,
|
||||
params,
|
||||
)
|
||||
|
||||
const listParams = [...params, pageSize, (page - 1) * pageSize]
|
||||
const listResult = await query<KuaishouIndustryVoucherRow>(
|
||||
`
|
||||
SELECT *
|
||||
FROM kuaishou_industry_vouchers
|
||||
${whereSql}
|
||||
ORDER BY updated_at DESC, id DESC
|
||||
LIMIT $${params.length + 1}
|
||||
OFFSET $${params.length + 2}
|
||||
`,
|
||||
listParams,
|
||||
)
|
||||
|
||||
return {
|
||||
items: listResult.rows,
|
||||
total: Number(totalResult.rows[0]?.total || 0) || 0,
|
||||
page,
|
||||
pageSize,
|
||||
}
|
||||
}
|
||||
|
||||
export async function findKuaishouIndustryVoucherByCode(
|
||||
voucherCode: string,
|
||||
oid = '',
|
||||
|
||||
@@ -4,6 +4,7 @@ import authRouter from "./admin/auth.js";
|
||||
import auditLogsRouter from "./admin/audit-logs.js";
|
||||
import cloudtentaclesRecordsRouter from "./admin/cloudtentacles-records.js";
|
||||
import dashboardRouter from "./admin/dashboard.js";
|
||||
import kuaishouIndustryRouter from "./admin/kuaishou-industry.js";
|
||||
import ordersRouter from "./admin/orders.js";
|
||||
import platformConfigRouter from "./admin/platform-config.js";
|
||||
import { requireAdminSession } from "./admin/session.js";
|
||||
@@ -19,6 +20,7 @@ router.use(dashboardRouter);
|
||||
router.use(usersRouter);
|
||||
router.use(auditLogsRouter);
|
||||
router.use(platformConfigRouter);
|
||||
router.use(kuaishouIndustryRouter);
|
||||
router.use(ordersRouter);
|
||||
router.use(tasksRouter);
|
||||
router.use(cloudtentaclesRecordsRouter);
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import { Router } from 'express'
|
||||
import {
|
||||
approveAdminKuaishouIndustryRefund,
|
||||
checkAdminKuaishouIndustryVoucherAvailable,
|
||||
consumeAdminKuaishouIndustryVoucherByCode,
|
||||
disagreeAdminKuaishouIndustryRefund,
|
||||
listAdminKuaishouIndustryRefunds,
|
||||
listAdminKuaishouIndustryVouchers,
|
||||
resendAdminKuaishouIndustryVoucherCode,
|
||||
reverseAdminKuaishouIndustryVoucher,
|
||||
} from '../../services/admin/kuaishou-industry-admin-service.js'
|
||||
import { createJsonHandler, requireAdminRoles } from './session.js'
|
||||
import type {
|
||||
AdminKuaishouIndustryRefundApproveRouteBody,
|
||||
AdminKuaishouIndustryRefundDisagreeRouteBody,
|
||||
AdminKuaishouIndustryRefundListRouteBody,
|
||||
AdminKuaishouIndustryVoucherCheckAvailableRouteBody,
|
||||
AdminKuaishouIndustryVoucherConsumeRouteBody,
|
||||
AdminKuaishouIndustryVoucherResendRouteBody,
|
||||
AdminKuaishouIndustryVoucherReverseRouteBody,
|
||||
} from '../../types/admin/route-inputs.js'
|
||||
import type { KuaishouIndustryVoucherAdminListQuery } from '../../repositories/kuaishou-industry-voucher-repo.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get(
|
||||
'/kuaishou-industry/vouchers',
|
||||
createJsonHandler(
|
||||
(req) => listAdminKuaishouIndustryVouchers(req.query as KuaishouIndustryVoucherAdminListQuery),
|
||||
{
|
||||
successMessage: 'ok',
|
||||
errorMessage: '查询快手行业电子凭证失败',
|
||||
scope: '[admin/kuaishou-industry/vouchers]',
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/kuaishou-industry/refunds/list',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
createJsonHandler(
|
||||
(req) => listAdminKuaishouIndustryRefunds(req.body as AdminKuaishouIndustryRefundListRouteBody),
|
||||
{
|
||||
successMessage: '售后单列表已查询',
|
||||
errorMessage: '查询快手售后单列表失败',
|
||||
scope: '[admin/kuaishou-industry/refunds/list]',
|
||||
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_refund_list', req.body, data),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/kuaishou-industry/refunds/approve',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
approveAdminKuaishouIndustryRefund(req.body as AdminKuaishouIndustryRefundApproveRouteBody),
|
||||
{
|
||||
successMessage: '同意退款接口已执行',
|
||||
errorMessage: '执行同意退款失败',
|
||||
scope: '[admin/kuaishou-industry/refunds/approve]',
|
||||
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_refund_approve', req.body, data),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/kuaishou-industry/refunds/disagree',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
disagreeAdminKuaishouIndustryRefund(req.body as AdminKuaishouIndustryRefundDisagreeRouteBody),
|
||||
{
|
||||
successMessage: '不同意退款接口已执行',
|
||||
errorMessage: '执行不同意退款失败',
|
||||
scope: '[admin/kuaishou-industry/refunds/disagree]',
|
||||
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_refund_disagree', req.body, data),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/kuaishou-industry/vouchers/check-available',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
checkAdminKuaishouIndustryVoucherAvailable(
|
||||
req.body as AdminKuaishouIndustryVoucherCheckAvailableRouteBody,
|
||||
),
|
||||
{
|
||||
successMessage: '电子凭证有效性已检查',
|
||||
errorMessage: '检查电子凭证有效性失败',
|
||||
scope: '[admin/kuaishou-industry/vouchers/check-available]',
|
||||
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_voucher_check_available', req.body, data),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/kuaishou-industry/vouchers/reverse',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
reverseAdminKuaishouIndustryVoucher(req.body as AdminKuaishouIndustryVoucherReverseRouteBody),
|
||||
{
|
||||
successMessage: '电子凭证冲正回调已执行',
|
||||
errorMessage: '执行电子凭证冲正失败',
|
||||
scope: '[admin/kuaishou-industry/vouchers/reverse]',
|
||||
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_voucher_reverse', req.body, data),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/kuaishou-industry/vouchers/consume',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
consumeAdminKuaishouIndustryVoucherByCode(
|
||||
req.body as AdminKuaishouIndustryVoucherConsumeRouteBody,
|
||||
),
|
||||
{
|
||||
successMessage: '电子凭证已手动核销',
|
||||
errorMessage: '手动核销电子凭证失败',
|
||||
scope: '[admin/kuaishou-industry/vouchers/consume]',
|
||||
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_voucher_consume', req.body, data),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/kuaishou-industry/vouchers/resend-code',
|
||||
requireAdminRoles(['admin', 'operator']),
|
||||
createJsonHandler(
|
||||
(req) =>
|
||||
resendAdminKuaishouIndustryVoucherCode(
|
||||
req.body as AdminKuaishouIndustryVoucherResendRouteBody,
|
||||
),
|
||||
{
|
||||
successMessage: '电子凭证发码回调已重发',
|
||||
errorMessage: '重发电子凭证发码回调失败',
|
||||
scope: '[admin/kuaishou-industry/vouchers/resend-code]',
|
||||
audit: (req, data) => buildKuaishouIndustryAudit('kuaishou_industry_voucher_resend_code', req.body, data),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
function buildKuaishouIndustryAudit(action: string, body: unknown, data: unknown) {
|
||||
const payload = body && typeof body === 'object' ? body as Record<string, unknown> : {}
|
||||
const result = data && typeof data === 'object' ? data as Record<string, unknown> : {}
|
||||
|
||||
return {
|
||||
action,
|
||||
targetType: 'kuaishou_industry',
|
||||
targetId: resolveAuditTargetId(payload),
|
||||
data: {
|
||||
sellerId: String(payload.sellerId || ''),
|
||||
oid: String(payload.oid || payload.orderId || ''),
|
||||
voucherCode: String(payload.voucherCode || ''),
|
||||
refundId: String(payload.refundId || ''),
|
||||
success: Boolean(result.success),
|
||||
httpStatus: Number(result.httpStatus || 0) || 0,
|
||||
error: String(result.error || ''),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAuditTargetId(payload: Record<string, unknown>): string {
|
||||
return String(
|
||||
payload.voucherCode ||
|
||||
payload.refundId ||
|
||||
payload.oid ||
|
||||
payload.orderId ||
|
||||
payload.sellerId ||
|
||||
'kuaishou-industry',
|
||||
)
|
||||
}
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,379 @@
|
||||
import {
|
||||
findKuaishouIndustryVoucherByCode,
|
||||
listKuaishouIndustryVouchersByOid,
|
||||
listKuaishouIndustryVouchersByTaskId,
|
||||
listKuaishouIndustryVouchersForAdmin,
|
||||
updateKuaishouIndustryVoucherByCode,
|
||||
type KuaishouIndustryVoucherAdminListQuery,
|
||||
} from '../../repositories/kuaishou-industry-voucher-repo.js'
|
||||
import { getOrderById } from '../../repositories/order-repo.js'
|
||||
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 { resendKuaishouIndustryVoucherSendCallback } from '../platforms/kuaishou-industry/send-code-service.js'
|
||||
import { consumeKuaishouIndustryVoucher } from '../platforms/kuaishou-industry/voucher-service.js'
|
||||
import {
|
||||
approveKuaishouIndustryRefund,
|
||||
disagreeKuaishouIndustryRefund,
|
||||
listKuaishouIndustryRefunds,
|
||||
} from '../platforms/kuaishou-industry/refund-service.js'
|
||||
import { reverseKuaishouIndustryCallback } from '../platforms/kuaishou-industry/reverse-callback-service.js'
|
||||
import type { KuaishouIndustryOpenApiCallResult } from '../platforms/kuaishou-industry/openapi-client.js'
|
||||
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../types/repository/rows.js'
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
|
||||
export async function listAdminKuaishouIndustryVouchers(
|
||||
input: KuaishouIndustryVoucherAdminListQuery = {},
|
||||
) {
|
||||
const result = await listKuaishouIndustryVouchersForAdmin(input)
|
||||
|
||||
return {
|
||||
items: result.items.map(mapAdminKuaishouIndustryVoucher),
|
||||
pagination: {
|
||||
page: result.page,
|
||||
pageSize: result.pageSize,
|
||||
total: result.total,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function listAdminKuaishouIndustryRefunds(input: JsonObject = {}) {
|
||||
assertRequired(input.beginTime, '开始时间未填写')
|
||||
assertRequired(input.endTime, '结束时间未填写')
|
||||
|
||||
return mapOpenApiResult(await listKuaishouIndustryRefunds(input))
|
||||
}
|
||||
|
||||
export async function approveAdminKuaishouIndustryRefund(input: JsonObject = {}) {
|
||||
assertRequired(input.refundId, '退款单编号未填写')
|
||||
assertRequired(input.refundAmount, '退款金额未填写')
|
||||
|
||||
return mapOpenApiResult(await approveKuaishouIndustryRefund(input))
|
||||
}
|
||||
|
||||
export async function disagreeAdminKuaishouIndustryRefund(input: JsonObject = {}) {
|
||||
assertRequired(input.refundId, '退款单编号未填写')
|
||||
assertRequired(input.sellerDisagreeReason, '拒绝原因未填写')
|
||||
assertRequired(input.sellerDisagreeDesc, '拒绝说明未填写')
|
||||
assertRequired(input.status, '退款单当前状态未填写')
|
||||
assertRequired(input.negotiateStatus, '协商状态未填写')
|
||||
|
||||
return mapOpenApiResult(await disagreeKuaishouIndustryRefund(input))
|
||||
}
|
||||
|
||||
export async function checkAdminKuaishouIndustryVoucherAvailable(input: JsonObject = {}) {
|
||||
const voucher = await resolveOptionalVoucher(input)
|
||||
const payload = buildVoucherOpenApiPayload(input, voucher)
|
||||
|
||||
assertRequired(payload.sellerId, '卖家编号未填写')
|
||||
assertRequired(payload.eticketType || payload.bizTypeCode, '电子凭证类型未填写')
|
||||
assertEtickets(payload.etickets, '电子凭证列表未填写')
|
||||
|
||||
return mapOpenApiResult(await checkKuaishouIndustryEticketAvailable(payload))
|
||||
}
|
||||
|
||||
export async function reverseAdminKuaishouIndustryVoucher(input: JsonObject = {}) {
|
||||
const voucher = await resolveOptionalVoucher(input)
|
||||
const payload = buildVoucherOpenApiPayload(input, voucher)
|
||||
|
||||
assertRequired(payload.oid, '订单号未填写')
|
||||
assertEtickets(payload.etickets, '冲正券码列表未填写')
|
||||
|
||||
const result = await reverseKuaishouIndustryCallback(payload)
|
||||
let updatedVoucher: KuaishouIndustryVoucherRow | null = null
|
||||
|
||||
if (result.success && voucher) {
|
||||
updatedVoucher = await updateKuaishouIndustryVoucherByCode(voucher.voucher_code, {
|
||||
status: 'UNUSED',
|
||||
consumeSerialNum: '',
|
||||
consumeDetailsJson: [],
|
||||
consumedAt: null,
|
||||
destroyedAt: null,
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...mapOpenApiResult(result),
|
||||
voucher: updatedVoucher ? mapAdminKuaishouIndustryVoucher(updatedVoucher) : null,
|
||||
}
|
||||
}
|
||||
|
||||
export async function consumeAdminKuaishouIndustryVoucherByCode(input: JsonObject = {}) {
|
||||
const voucher = await resolveRequiredVoucher(input)
|
||||
const task = voucher.task_id ? await getTaskById(voucher.task_id) : null
|
||||
const consumeInput: Parameters<typeof consumeKuaishouIndustryVoucher>[1] = {
|
||||
source: 'admin_tool_manual_consume',
|
||||
token: String(input.token || voucher.token || '').trim(),
|
||||
consumeType: String(input.consumeType || 'delivery').trim() || 'delivery',
|
||||
consumeTime: Date.now(),
|
||||
...(input.serialNum ? { serialNum: String(input.serialNum).trim() } : {}),
|
||||
...(input.eticketType ? { eticketType: String(input.eticketType).trim() } : {}),
|
||||
...(input.storeName ? { storeName: String(input.storeName).trim() } : {}),
|
||||
...(input.storeAddress ? { storeAddress: String(input.storeAddress).trim() } : {}),
|
||||
...(input.expressCode ? { expressCode: String(input.expressCode).trim() } : {}),
|
||||
...(input.expressNo ? { expressNo: String(input.expressNo).trim() } : {}),
|
||||
}
|
||||
if (task) {
|
||||
consumeInput.task = task
|
||||
}
|
||||
|
||||
const result = await consumeKuaishouIndustryVoucher(voucher, consumeInput)
|
||||
|
||||
if (!result.ok || !result.voucher) {
|
||||
throw createHttpError(result.errorMessage || '电子凭证核销失败', {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_kuaishou_industry_consume_failed',
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
voucher: mapAdminKuaishouIndustryVoucher(result.voucher),
|
||||
task: task ? mapTaskReference(task) : null,
|
||||
}
|
||||
}
|
||||
|
||||
export async function resendAdminKuaishouIndustryVoucherCode(input: JsonObject = {}) {
|
||||
const voucher = await resolveRequiredVoucher(input)
|
||||
const order = voucher.order_id ? await getOrderById(voucher.order_id) : null
|
||||
const result = await resendKuaishouIndustryVoucherSendCallback({
|
||||
voucherCode: voucher.voucher_code,
|
||||
oid: voucher.oid,
|
||||
preferredTotalGoodsValue: Number(order?.total_amount || 0) || 0,
|
||||
})
|
||||
|
||||
return {
|
||||
success: result.success,
|
||||
response: 'response' in result ? result.response || null : null,
|
||||
error: result.error || '',
|
||||
voucher: result.voucher ? mapAdminKuaishouIndustryVoucher(result.voucher) : null,
|
||||
}
|
||||
}
|
||||
|
||||
function buildVoucherOpenApiPayload(
|
||||
input: JsonObject,
|
||||
voucher: KuaishouIndustryVoucherRow | null,
|
||||
): JsonObject {
|
||||
const etickets = normalizeAdminEtickets(input.etickets)
|
||||
const voucherCode = String(voucher?.voucher_code || input.voucherCode || '').trim()
|
||||
|
||||
return {
|
||||
...input,
|
||||
sellerId: String(input.sellerId || voucher?.seller_id || '').trim(),
|
||||
oid: String(input.oid || input.orderId || voucher?.oid || '').trim(),
|
||||
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 }]
|
||||
: [],
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveOptionalVoucher(input: JsonObject): Promise<KuaishouIndustryVoucherRow | null> {
|
||||
const voucherCode = String(input.voucherCode || '').trim()
|
||||
if (!voucherCode) {
|
||||
return null
|
||||
}
|
||||
|
||||
const voucher = await findKuaishouIndustryVoucherByCode(voucherCode, String(input.oid || input.orderId || '').trim())
|
||||
if (!voucher) {
|
||||
throw createHttpError('电子凭证不存在', {
|
||||
statusCode: 404,
|
||||
errorCode: 'admin_kuaishou_industry_voucher_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
return voucher
|
||||
}
|
||||
|
||||
async function resolveRequiredVoucher(input: JsonObject): Promise<KuaishouIndustryVoucherRow> {
|
||||
const voucher = await resolveOptionalVoucher(input)
|
||||
if (voucher) {
|
||||
return voucher
|
||||
}
|
||||
|
||||
const taskId = Number(input.taskId || 0)
|
||||
if (Number.isFinite(taskId) && taskId > 0) {
|
||||
const vouchers = await listKuaishouIndustryVouchersByTaskId(Math.trunc(taskId))
|
||||
return resolveSingleVoucher(vouchers, '当前任务没有关联电子凭证', '当前任务关联多个电子凭证,请指定券码')
|
||||
}
|
||||
|
||||
const oid = String(input.oid || input.orderId || '').trim()
|
||||
if (oid) {
|
||||
const vouchers = await listKuaishouIndustryVouchersByOid(oid)
|
||||
return resolveSingleVoucher(vouchers, '当前订单没有关联电子凭证', '当前订单关联多个电子凭证,请指定券码')
|
||||
}
|
||||
|
||||
throw createHttpError('请填写券码、任务 ID 或订单号', {
|
||||
statusCode: 400,
|
||||
errorCode: 'admin_kuaishou_industry_voucher_selector_missing',
|
||||
})
|
||||
}
|
||||
|
||||
function resolveSingleVoucher(
|
||||
vouchers: KuaishouIndustryVoucherRow[],
|
||||
missingMessage: string,
|
||||
multipleMessage: string,
|
||||
): KuaishouIndustryVoucherRow {
|
||||
if (vouchers.length === 0) {
|
||||
throw createHttpError(missingMessage, {
|
||||
statusCode: 404,
|
||||
errorCode: 'admin_kuaishou_industry_voucher_not_found',
|
||||
})
|
||||
}
|
||||
|
||||
if (vouchers.length > 1) {
|
||||
throw createHttpError(multipleMessage, {
|
||||
statusCode: 409,
|
||||
errorCode: 'admin_kuaishou_industry_voucher_ambiguous',
|
||||
})
|
||||
}
|
||||
|
||||
return vouchers[0] as KuaishouIndustryVoucherRow
|
||||
}
|
||||
|
||||
function mapOpenApiResult(result: KuaishouIndustryOpenApiCallResult) {
|
||||
return {
|
||||
success: result.success,
|
||||
response: result.response || null,
|
||||
error: result.error || '',
|
||||
request: sanitizeOpenApiRequest(result.request),
|
||||
durationMs: result.durationMs || 0,
|
||||
httpStatus: result.httpStatus || 0,
|
||||
skippedReason: result.skippedReason || '',
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeOpenApiRequest(request: JsonObject | undefined): JsonObject | null {
|
||||
if (!request) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
...request,
|
||||
access_token: maskSecret(request.access_token),
|
||||
sign: maskSecret(request.sign),
|
||||
}
|
||||
}
|
||||
|
||||
function mapAdminKuaishouIndustryVoucher(voucher: KuaishouIndustryVoucherRow) {
|
||||
return {
|
||||
id: voucher.id,
|
||||
voucherCode: voucher.voucher_code,
|
||||
oid: voucher.oid,
|
||||
orderId: voucher.order_id,
|
||||
taskId: voucher.task_id,
|
||||
unitIndex: voucher.unit_index,
|
||||
sellerId: voucher.seller_id,
|
||||
tokenMasked: maskSecret(voucher.token),
|
||||
status: voucher.status,
|
||||
validStartTime: Number(voucher.valid_start_time || 0) || 0,
|
||||
validEndTime: Number(voucher.valid_end_time || 0) || 0,
|
||||
consumeSerialNum: voucher.consume_serial_num,
|
||||
consumeDetails: parseJsonArray(voucher.consume_details_json),
|
||||
consumedAt: voucher.consumed_at,
|
||||
destroyedAt: voucher.destroyed_at,
|
||||
sendCallbackStatus: voucher.send_callback_status,
|
||||
sendCallbackAttemptCount: voucher.send_callback_attempt_count,
|
||||
sendCallbackLastError: voucher.send_callback_last_error,
|
||||
sendCallbackSentAt: voucher.send_callback_sent_at,
|
||||
rawPayload: parseJsonObject(voucher.raw_payload_json),
|
||||
createdAt: voucher.created_at,
|
||||
updatedAt: voucher.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
function mapTaskReference(task: TaskRow) {
|
||||
return {
|
||||
taskId: task.id,
|
||||
taskNo: task.task_no,
|
||||
status: task.task_status,
|
||||
deliveryStatus: task.delivery_status,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAdminEtickets(value: unknown): JsonObject[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const items: JsonObject[] = []
|
||||
for (const item of value) {
|
||||
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const current = item as JsonObject
|
||||
const id = String(current.id || '').trim()
|
||||
const code = String(current.code || '').trim()
|
||||
const num = Number(current.num || 1)
|
||||
|
||||
if (!id && !code) {
|
||||
continue
|
||||
}
|
||||
|
||||
items.push({
|
||||
...(id ? { id } : {}),
|
||||
...(code ? { code } : {}),
|
||||
num: Number.isInteger(num) && num > 0 ? num : 1,
|
||||
})
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
function assertRequired(value: unknown, message: string) {
|
||||
if (value === undefined || value === null || String(value).trim() === '') {
|
||||
throw createHttpError(message, {
|
||||
statusCode: 400,
|
||||
errorCode: 'admin_kuaishou_industry_missing_required_field',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function assertEtickets(value: unknown, message: string) {
|
||||
if (!Array.isArray(value) || value.length === 0) {
|
||||
throw createHttpError(message, {
|
||||
statusCode: 400,
|
||||
errorCode: 'admin_kuaishou_industry_missing_etickets',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function parseJsonObject(value: unknown): JsonObject {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
return value as JsonObject
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(String(value || '{}'))
|
||||
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function parseJsonArray(value: unknown): JsonObject[] {
|
||||
if (Array.isArray(value)) {
|
||||
return value.filter((item): item is JsonObject =>
|
||||
Boolean(item && typeof item === 'object' && !Array.isArray(item)),
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(String(value || '[]'))
|
||||
return Array.isArray(parsed)
|
||||
? parsed.filter((item): item is JsonObject =>
|
||||
Boolean(item && typeof item === 'object' && !Array.isArray(item)),
|
||||
)
|
||||
: []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { requestKuaishouIndustryOpenApi } from './openapi-client.js'
|
||||
import {
|
||||
normalizeOpenApiLong,
|
||||
normalizeOpenApiPositiveInteger,
|
||||
pickDefinedBizParams,
|
||||
type JsonObject,
|
||||
} from './openapi-values.js'
|
||||
|
||||
export type KuaishouIndustryAvailableEticketInput = {
|
||||
id?: unknown
|
||||
code?: unknown
|
||||
num?: unknown
|
||||
}
|
||||
|
||||
export type KuaishouIndustryCheckAvailableInput = {
|
||||
sellerId?: unknown
|
||||
buyerId?: unknown
|
||||
orderId?: unknown
|
||||
eticketType?: unknown
|
||||
bizTypeCode?: unknown
|
||||
etickets?: unknown
|
||||
eTicketList?: unknown
|
||||
}
|
||||
|
||||
export function checkKuaishouIndustryEticketAvailable(
|
||||
input: KuaishouIndustryCheckAvailableInput = {},
|
||||
) {
|
||||
const eticketType = String(input.eticketType || input.bizTypeCode || '').trim()
|
||||
const etickets = normalizeAvailableEtickets(input.etickets || input.eTicketList)
|
||||
const sellerId = normalizeOpenApiLong(input.sellerId)
|
||||
const bizParams = pickDefinedBizParams({
|
||||
buyerId: normalizeOpenApiLong(input.buyerId),
|
||||
eticketType,
|
||||
etickets,
|
||||
orderId: normalizeOpenApiLong(input.orderId),
|
||||
sellerId,
|
||||
})
|
||||
|
||||
return requestKuaishouIndustryOpenApi({
|
||||
apiMethod: 'open.virtual.eticket.checkavailable',
|
||||
path: '/open/virtual/eticket/checkavailable',
|
||||
bizParams,
|
||||
...(sellerId ? { sellerId: String(sellerId) } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeAvailableEtickets(value: unknown): JsonObject[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return value
|
||||
.map((item) => normalizeAvailableEticket(item))
|
||||
.filter((item): item is JsonObject => Boolean(item))
|
||||
}
|
||||
|
||||
function normalizeAvailableEticket(value: unknown): JsonObject | null {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const current = value as KuaishouIndustryAvailableEticketInput
|
||||
const id = String(current.id || '').trim()
|
||||
const code = String(current.code || '').trim()
|
||||
const num = normalizeOpenApiPositiveInteger(current.num, 1)
|
||||
|
||||
if (!id && !code) {
|
||||
return null
|
||||
}
|
||||
|
||||
return pickDefinedBizParams({ id, code, num })
|
||||
}
|
||||
+46
-140
@@ -1,12 +1,5 @@
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
import { logInfo, logWarn } from '../../../utils/logger.js'
|
||||
import { getKuaishouIndustryConfig } from './config.js'
|
||||
import { ensureKuaishouIndustryAccessToken } from './token-service.js'
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
|
||||
const DEFAULT_KUAISHOU_OPEN_API = 'https://openapi.kwaixiaodian.com'
|
||||
import { requestKuaishouIndustryOpenApi, type JsonObject } from './openapi-client.js'
|
||||
|
||||
type ConsumeCallbackInput = {
|
||||
oid: string
|
||||
@@ -33,38 +26,9 @@ type ConsumeCallbackInput = {
|
||||
consumePoiId?: number
|
||||
}
|
||||
|
||||
export async function consumeCallback(input: ConsumeCallbackInput): Promise<{ success: boolean; response?: JsonObject; error?: string }> {
|
||||
let config = getKuaishouIndustryConfig()
|
||||
|
||||
if (!config.enabled) {
|
||||
logInfo('[kuaishou-industry/consume-callback]', '行业电子凭证配置未启用,跳过核销回调')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
try {
|
||||
const tokenResult = await ensureKuaishouIndustryAccessToken({
|
||||
config,
|
||||
...(input.sellerId ? { sellerId: input.sellerId } : {}),
|
||||
})
|
||||
config = {
|
||||
...config,
|
||||
...tokenResult.config,
|
||||
accessToken: tokenResult.accessToken,
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logWarn('[kuaishou-industry/consume-callback]', 'accessToken 刷新失败,无法发起核销回调', {
|
||||
error: message,
|
||||
})
|
||||
return { success: false, error: message }
|
||||
}
|
||||
|
||||
if (!config.accessToken) {
|
||||
const message = 'accessToken 未配置,无法发起核销回调'
|
||||
logWarn('[kuaishou-industry/consume-callback]', message)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
|
||||
export async function consumeCallback(
|
||||
input: ConsumeCallbackInput,
|
||||
): Promise<{ success: boolean; response?: JsonObject; error?: string }> {
|
||||
const bizParams: JsonObject = {
|
||||
oid: input.oid,
|
||||
etickets: input.etickets.map((e) => {
|
||||
@@ -90,121 +54,63 @@ export async function consumeCallback(input: ConsumeCallbackInput): Promise<{ su
|
||||
if (input.seriallNum) bizParams.seriallNum = input.seriallNum
|
||||
if (input.consumePoiId != null) bizParams.consumePoiId = input.consumePoiId
|
||||
|
||||
const paramStr = JSON.stringify(bizParams)
|
||||
|
||||
const signParams: JsonObject = {
|
||||
method: 'integration.callback.virtual.eticket.consume',
|
||||
appkey: config.appKey,
|
||||
access_token: config.accessToken,
|
||||
version: '1',
|
||||
timestamp: Date.now(),
|
||||
signMethod: 'MD5',
|
||||
param: paramStr,
|
||||
}
|
||||
|
||||
const sign = signPayload(signParams, config.signSecret)
|
||||
signParams.sign = sign
|
||||
|
||||
const body = new URLSearchParams()
|
||||
for (const [key, value] of Object.entries(signParams)) {
|
||||
body.append(key, String(value))
|
||||
}
|
||||
|
||||
const url = `${resolveKuaishouOpenApiBaseUrl(config.baseUrl)}/integration/callback/virtual/eticket/consume`
|
||||
|
||||
const result = await requestKuaishouIndustryOpenApi({
|
||||
apiMethod: 'integration.callback.virtual.eticket.consume',
|
||||
path: '/integration/callback/virtual/eticket/consume',
|
||||
bizParams,
|
||||
...(input.sellerId ? { sellerId: input.sellerId } : {}),
|
||||
onRequest: (request) => {
|
||||
logInfo('[kuaishou-industry/consume-callback]', `发起核销回调 oid=${input.oid}`, {
|
||||
url,
|
||||
...request,
|
||||
oid: input.oid,
|
||||
sellerId: input.sellerId || '',
|
||||
status: input.status,
|
||||
consumeType: input.consumeType,
|
||||
})
|
||||
|
||||
try {
|
||||
const startedAt = Date.now()
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
||||
body: body.toString(),
|
||||
},
|
||||
})
|
||||
const text = await res.text()
|
||||
let json: JsonObject = {}
|
||||
try { json = JSON.parse(text) } catch { json = { raw: text } }
|
||||
|
||||
const durationMs = Date.now() - startedAt
|
||||
const ok = res.ok && Number(json.result) === 1
|
||||
|
||||
if (ok) {
|
||||
logInfo('[kuaishou-industry/consume-callback]', `核销回调成功 oid=${input.oid}`, {
|
||||
durationMs,
|
||||
response: json,
|
||||
})
|
||||
} else {
|
||||
logWarn('[kuaishou-industry/consume-callback]', `核销回调失败 oid=${input.oid}`, {
|
||||
durationMs,
|
||||
status: res.status,
|
||||
response: json,
|
||||
})
|
||||
if (result.skippedReason === 'disabled') {
|
||||
logInfo('[kuaishou-industry/consume-callback]', '行业电子凭证配置未启用,跳过核销回调')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
return { success: ok, response: json }
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
logWarn('[kuaishou-industry/consume-callback]', `核销回调异常 oid=${input.oid}`, resolveCallbackErrorDetail(err))
|
||||
if (result.skippedReason === 'token_error') {
|
||||
const message = result.error || 'accessToken 刷新失败'
|
||||
logWarn('[kuaishou-industry/consume-callback]', 'accessToken 刷新失败,无法发起核销回调', {
|
||||
error: message,
|
||||
})
|
||||
return { success: false, error: message }
|
||||
}
|
||||
|
||||
if (result.skippedReason === 'missing_access_token') {
|
||||
const message = 'accessToken 未配置,无法发起核销回调'
|
||||
logWarn('[kuaishou-industry/consume-callback]', message)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
|
||||
function signPayload(params: JsonObject, signSecret: string): string {
|
||||
const entries = Object.entries(params)
|
||||
.filter(([key]) => key !== 'sign' && key !== 'signSecret')
|
||||
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
||||
|
||||
const queryString = entries
|
||||
.map(([key, value]) => `${key}=${stringifySignValue(value)}`)
|
||||
.join('&')
|
||||
|
||||
const source = `${queryString}&signSecret=${signSecret}`
|
||||
|
||||
return crypto.createHash('md5').update(source, 'utf8').digest('hex').toLowerCase()
|
||||
if (result.success) {
|
||||
logInfo('[kuaishou-industry/consume-callback]', `核销回调成功 oid=${input.oid}`, {
|
||||
durationMs: result.durationMs,
|
||||
response: result.response || null,
|
||||
})
|
||||
} else if (result.error) {
|
||||
logWarn(
|
||||
'[kuaishou-industry/consume-callback]',
|
||||
`核销回调异常 oid=${input.oid}`,
|
||||
result.errorDetail || { error: result.error },
|
||||
)
|
||||
} else {
|
||||
logWarn('[kuaishou-industry/consume-callback]', `核销回调失败 oid=${input.oid}`, {
|
||||
durationMs: result.durationMs,
|
||||
status: result.httpStatus,
|
||||
response: result.response || null,
|
||||
})
|
||||
}
|
||||
|
||||
function stringifySignValue(value: unknown): string {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return String(value)
|
||||
if (typeof value === 'boolean') return value ? 'true' : 'false'
|
||||
if (value == null) return ''
|
||||
if (typeof value === 'object') return JSON.stringify(value, Object.keys(value as Record<string, unknown>).sort())
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function resolveCallbackErrorDetail(error: unknown): JsonObject {
|
||||
const detail: JsonObject = {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}
|
||||
const cause = error && typeof error === 'object' && 'cause' in error
|
||||
? (error as { cause?: unknown }).cause
|
||||
: null
|
||||
if (!cause || typeof cause !== 'object') {
|
||||
return detail
|
||||
}
|
||||
|
||||
const current = cause as Record<string, unknown>
|
||||
const fields: Array<[string, unknown]> = [
|
||||
['causeMessage', current.message],
|
||||
['causeCode', current.code],
|
||||
['causeSyscall', current.syscall],
|
||||
['causeHostname', current.hostname],
|
||||
]
|
||||
for (const [key, value] of fields) {
|
||||
const text = String(value || '').trim()
|
||||
if (text) {
|
||||
detail[key] = text
|
||||
return {
|
||||
success: result.success,
|
||||
...(result.response ? { response: result.response } : {}),
|
||||
...(result.error ? { error: result.error } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
return detail
|
||||
}
|
||||
|
||||
function resolveKuaishouOpenApiBaseUrl(value: unknown): string {
|
||||
return String(value || DEFAULT_KUAISHOU_OPEN_API).trim().replace(/\/+$/, '') || DEFAULT_KUAISHOU_OPEN_API
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { assertKuaishouIndustryConfig } from './config.js'
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
|
||||
type SignMethod = 'MD5' | 'HMAC_SHA256'
|
||||
export type SignMethod = 'MD5' | 'HMAC_SHA256'
|
||||
|
||||
export function buildKuaishouIndustrySignSource(params: JsonObject = {}, { signSecret } = assertKuaishouIndustryConfig()) {
|
||||
const entries = Object.entries(params)
|
||||
|
||||
+46
-140
@@ -1,12 +1,5 @@
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
import { logInfo, logWarn } from '../../../utils/logger.js'
|
||||
import { getKuaishouIndustryConfig } from './config.js'
|
||||
import { ensureKuaishouIndustryAccessToken } from './token-service.js'
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
|
||||
const DEFAULT_KUAISHOU_OPEN_API = 'https://openapi.kwaixiaodian.com'
|
||||
import { requestKuaishouIndustryOpenApi, type JsonObject } from './openapi-client.js'
|
||||
|
||||
type DestroyCallbackInput = {
|
||||
oid: string
|
||||
@@ -23,38 +16,9 @@ type DestroyCallbackInput = {
|
||||
token?: string
|
||||
}
|
||||
|
||||
export async function destroyCallback(input: DestroyCallbackInput): Promise<{ success: boolean; response?: JsonObject; error?: string }> {
|
||||
let config = getKuaishouIndustryConfig()
|
||||
|
||||
if (!config.enabled) {
|
||||
logInfo('[kuaishou-industry/destroy-callback]', '行业电子凭证配置未启用,跳过销毁回调')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
try {
|
||||
const tokenResult = await ensureKuaishouIndustryAccessToken({
|
||||
config,
|
||||
...(input.sellerId ? { sellerId: input.sellerId } : {}),
|
||||
})
|
||||
config = {
|
||||
...config,
|
||||
...tokenResult.config,
|
||||
accessToken: tokenResult.accessToken,
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logWarn('[kuaishou-industry/destroy-callback]', 'accessToken 刷新失败,无法发起销毁回调', {
|
||||
error: message,
|
||||
})
|
||||
return { success: false, error: message }
|
||||
}
|
||||
|
||||
if (!config.accessToken) {
|
||||
const message = 'accessToken 未配置,无法发起销毁回调'
|
||||
logWarn('[kuaishou-industry/destroy-callback]', message)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
|
||||
export async function destroyCallback(
|
||||
input: DestroyCallbackInput,
|
||||
): Promise<{ success: boolean; response?: JsonObject; error?: string }> {
|
||||
const bizParams: JsonObject = {
|
||||
oid: input.oid,
|
||||
reason: input.reason,
|
||||
@@ -73,121 +37,63 @@ export async function destroyCallback(input: DestroyCallbackInput): Promise<{ su
|
||||
if (input.ext) bizParams.ext = input.ext
|
||||
if (input.token) bizParams.token = input.token
|
||||
|
||||
const paramStr = JSON.stringify(bizParams)
|
||||
|
||||
const signParams: JsonObject = {
|
||||
method: 'integration.callback.virtual.eticket.destroy',
|
||||
appkey: config.appKey,
|
||||
access_token: config.accessToken,
|
||||
version: '1',
|
||||
timestamp: Date.now(),
|
||||
signMethod: 'MD5',
|
||||
param: paramStr,
|
||||
}
|
||||
|
||||
const sign = signPayload(signParams, config.signSecret)
|
||||
signParams.sign = sign
|
||||
|
||||
const body = new URLSearchParams()
|
||||
for (const [key, value] of Object.entries(signParams)) {
|
||||
body.append(key, String(value))
|
||||
}
|
||||
|
||||
const url = `${resolveKuaishouOpenApiBaseUrl(config.baseUrl)}/integration/callback/virtual/eticket/destroy`
|
||||
|
||||
const result = await requestKuaishouIndustryOpenApi({
|
||||
apiMethod: 'integration.callback.virtual.eticket.destroy',
|
||||
path: '/integration/callback/virtual/eticket/destroy',
|
||||
bizParams,
|
||||
...(input.sellerId ? { sellerId: input.sellerId } : {}),
|
||||
onRequest: (request) => {
|
||||
logInfo('[kuaishou-industry/destroy-callback]', `发起销毁回调 oid=${input.oid}`, {
|
||||
url,
|
||||
...request,
|
||||
oid: input.oid,
|
||||
sellerId: input.sellerId || '',
|
||||
reason: input.reason,
|
||||
hasToken: Boolean(input.token),
|
||||
})
|
||||
|
||||
try {
|
||||
const startedAt = Date.now()
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
||||
body: body.toString(),
|
||||
},
|
||||
})
|
||||
const text = await res.text()
|
||||
let json: JsonObject = {}
|
||||
try { json = JSON.parse(text) } catch { json = { raw: text } }
|
||||
|
||||
const durationMs = Date.now() - startedAt
|
||||
const ok = res.ok && Number(json.result) === 1
|
||||
|
||||
if (ok) {
|
||||
logInfo('[kuaishou-industry/destroy-callback]', `销毁回调成功 oid=${input.oid}`, {
|
||||
durationMs,
|
||||
response: json,
|
||||
})
|
||||
} else {
|
||||
logWarn('[kuaishou-industry/destroy-callback]', `销毁回调失败 oid=${input.oid}`, {
|
||||
durationMs,
|
||||
status: res.status,
|
||||
response: json,
|
||||
})
|
||||
if (result.skippedReason === 'disabled') {
|
||||
logInfo('[kuaishou-industry/destroy-callback]', '行业电子凭证配置未启用,跳过销毁回调')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
return { success: ok, response: json }
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
logWarn('[kuaishou-industry/destroy-callback]', `销毁回调异常 oid=${input.oid}`, resolveCallbackErrorDetail(err))
|
||||
if (result.skippedReason === 'token_error') {
|
||||
const message = result.error || 'accessToken 刷新失败'
|
||||
logWarn('[kuaishou-industry/destroy-callback]', 'accessToken 刷新失败,无法发起销毁回调', {
|
||||
error: message,
|
||||
})
|
||||
return { success: false, error: message }
|
||||
}
|
||||
|
||||
if (result.skippedReason === 'missing_access_token') {
|
||||
const message = 'accessToken 未配置,无法发起销毁回调'
|
||||
logWarn('[kuaishou-industry/destroy-callback]', message)
|
||||
return { success: false, error: message }
|
||||
}
|
||||
|
||||
function signPayload(params: JsonObject, signSecret: string): string {
|
||||
const entries = Object.entries(params)
|
||||
.filter(([key]) => key !== 'sign' && key !== 'signSecret')
|
||||
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
||||
|
||||
const queryString = entries
|
||||
.map(([key, value]) => `${key}=${stringifySignValue(value)}`)
|
||||
.join('&')
|
||||
|
||||
const source = `${queryString}&signSecret=${signSecret}`
|
||||
|
||||
return crypto.createHash('md5').update(source, 'utf8').digest('hex').toLowerCase()
|
||||
if (result.success) {
|
||||
logInfo('[kuaishou-industry/destroy-callback]', `销毁回调成功 oid=${input.oid}`, {
|
||||
durationMs: result.durationMs,
|
||||
response: result.response || null,
|
||||
})
|
||||
} else if (result.error) {
|
||||
logWarn(
|
||||
'[kuaishou-industry/destroy-callback]',
|
||||
`销毁回调异常 oid=${input.oid}`,
|
||||
result.errorDetail || { error: result.error },
|
||||
)
|
||||
} else {
|
||||
logWarn('[kuaishou-industry/destroy-callback]', `销毁回调失败 oid=${input.oid}`, {
|
||||
durationMs: result.durationMs,
|
||||
status: result.httpStatus,
|
||||
response: result.response || null,
|
||||
})
|
||||
}
|
||||
|
||||
function stringifySignValue(value: unknown): string {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return String(value)
|
||||
if (typeof value === 'boolean') return value ? 'true' : 'false'
|
||||
if (value == null) return ''
|
||||
if (typeof value === 'object') return JSON.stringify(value, Object.keys(value as Record<string, unknown>).sort())
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function resolveCallbackErrorDetail(error: unknown): JsonObject {
|
||||
const detail: JsonObject = {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}
|
||||
const cause = error && typeof error === 'object' && 'cause' in error
|
||||
? (error as { cause?: unknown }).cause
|
||||
: null
|
||||
if (!cause || typeof cause !== 'object') {
|
||||
return detail
|
||||
}
|
||||
|
||||
const current = cause as Record<string, unknown>
|
||||
const fields: Array<[string, unknown]> = [
|
||||
['causeMessage', current.message],
|
||||
['causeCode', current.code],
|
||||
['causeSyscall', current.syscall],
|
||||
['causeHostname', current.hostname],
|
||||
]
|
||||
for (const [key, value] of fields) {
|
||||
const text = String(value || '').trim()
|
||||
if (text) {
|
||||
detail[key] = text
|
||||
return {
|
||||
success: result.success,
|
||||
...(result.response ? { response: result.response } : {}),
|
||||
...(result.error ? { error: result.error } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
return detail
|
||||
}
|
||||
|
||||
function resolveKuaishouOpenApiBaseUrl(value: unknown): string {
|
||||
return String(value || DEFAULT_KUAISHOU_OPEN_API).trim().replace(/\/+$/, '') || DEFAULT_KUAISHOU_OPEN_API
|
||||
}
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import { getKuaishouIndustryConfig } from './config.js'
|
||||
import { signKuaishouIndustryPayload, type SignMethod } from './crypto.js'
|
||||
import { ensureKuaishouIndustryAccessToken } from './token-service.js'
|
||||
|
||||
export type JsonObject = Record<string, any>
|
||||
|
||||
type KuaishouIndustryOpenApiCallInput = {
|
||||
apiMethod: string
|
||||
path: string
|
||||
bizParams: JsonObject
|
||||
httpMethod?: 'GET' | 'POST'
|
||||
sellerId?: string
|
||||
signMethod?: SignMethod
|
||||
onRequest?: (request: JsonObject) => void
|
||||
}
|
||||
|
||||
export type KuaishouIndustryOpenApiCallResult = {
|
||||
success: boolean
|
||||
response?: JsonObject
|
||||
error?: string
|
||||
request?: JsonObject
|
||||
durationMs?: number
|
||||
httpStatus?: number
|
||||
skippedReason?: 'disabled' | 'missing_access_token' | 'token_error'
|
||||
errorDetail?: JsonObject
|
||||
}
|
||||
|
||||
const DEFAULT_KUAISHOU_OPEN_API = 'https://openapi.kwaixiaodian.com'
|
||||
|
||||
export async function requestKuaishouIndustryOpenApi(
|
||||
input: KuaishouIndustryOpenApiCallInput,
|
||||
): Promise<KuaishouIndustryOpenApiCallResult> {
|
||||
let config = getKuaishouIndustryConfig()
|
||||
|
||||
if (!config.enabled) {
|
||||
return { success: true, skippedReason: 'disabled' }
|
||||
}
|
||||
|
||||
try {
|
||||
const tokenResult = await ensureKuaishouIndustryAccessToken({
|
||||
config,
|
||||
...(input.sellerId ? { sellerId: input.sellerId } : {}),
|
||||
})
|
||||
config = {
|
||||
...config,
|
||||
...tokenResult.config,
|
||||
accessToken: tokenResult.accessToken,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
skippedReason: 'token_error',
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
errorDetail: resolveKuaishouOpenApiErrorDetail(error),
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.accessToken) {
|
||||
return {
|
||||
success: false,
|
||||
skippedReason: 'missing_access_token',
|
||||
error: 'accessToken 未配置',
|
||||
}
|
||||
}
|
||||
|
||||
const signMethod = input.signMethod || 'MD5'
|
||||
const paramStr = JSON.stringify(input.bizParams)
|
||||
const signParams: JsonObject = {
|
||||
method: input.apiMethod,
|
||||
appkey: config.appKey,
|
||||
access_token: config.accessToken,
|
||||
version: config.version || '1',
|
||||
timestamp: Date.now(),
|
||||
signMethod,
|
||||
param: paramStr,
|
||||
}
|
||||
signParams.sign = signKuaishouIndustryPayload(signParams, signMethod, config)
|
||||
|
||||
const body = new URLSearchParams()
|
||||
for (const [key, value] of Object.entries(signParams)) {
|
||||
body.append(key, String(value))
|
||||
}
|
||||
|
||||
const httpMethod = input.httpMethod || 'POST'
|
||||
const url = `${resolveKuaishouOpenApiBaseUrl(config.baseUrl)}${normalizeOpenApiPath(input.path)}`
|
||||
const requestLog = buildKuaishouOpenApiRequestLog({
|
||||
url,
|
||||
signParams,
|
||||
bizParams: input.bizParams,
|
||||
httpMethod,
|
||||
})
|
||||
|
||||
input.onRequest?.(requestLog)
|
||||
|
||||
try {
|
||||
const startedAt = Date.now()
|
||||
const res = httpMethod === 'GET'
|
||||
? await fetch(`${url}?${body.toString()}`, { method: 'GET' })
|
||||
: await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
||||
body: body.toString(),
|
||||
})
|
||||
const text = await res.text()
|
||||
let json: JsonObject = {}
|
||||
try {
|
||||
json = JSON.parse(text)
|
||||
} catch {
|
||||
json = { raw: text }
|
||||
}
|
||||
|
||||
const durationMs = Date.now() - startedAt
|
||||
return {
|
||||
success: res.ok && Number(json.result) === 1,
|
||||
response: json,
|
||||
request: requestLog,
|
||||
durationMs,
|
||||
httpStatus: res.status,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
request: requestLog,
|
||||
errorDetail: resolveKuaishouOpenApiErrorDetail(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function buildKuaishouOpenApiRequestLog({
|
||||
url,
|
||||
signParams,
|
||||
bizParams,
|
||||
httpMethod,
|
||||
}: {
|
||||
url: string
|
||||
signParams: JsonObject
|
||||
bizParams: JsonObject
|
||||
httpMethod?: 'GET' | 'POST'
|
||||
}): JsonObject {
|
||||
return {
|
||||
url,
|
||||
method: httpMethod || 'POST',
|
||||
contentType: 'application/x-www-form-urlencoded',
|
||||
apiMethod: signParams.method,
|
||||
appkey: signParams.appkey,
|
||||
access_token: signParams.access_token,
|
||||
version: signParams.version,
|
||||
timestamp: signParams.timestamp,
|
||||
signMethod: signParams.signMethod,
|
||||
sign: signParams.sign,
|
||||
param: bizParams,
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveKuaishouOpenApiBaseUrl(value: unknown): string {
|
||||
return String(value || DEFAULT_KUAISHOU_OPEN_API).trim().replace(/\/+$/, '') ||
|
||||
DEFAULT_KUAISHOU_OPEN_API
|
||||
}
|
||||
|
||||
export function resolveKuaishouOpenApiErrorDetail(error: unknown): JsonObject {
|
||||
const detail: JsonObject = {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}
|
||||
const cause = error && typeof error === 'object' && 'cause' in error
|
||||
? (error as { cause?: unknown }).cause
|
||||
: null
|
||||
if (!cause || typeof cause !== 'object') {
|
||||
return detail
|
||||
}
|
||||
|
||||
const current = cause as Record<string, unknown>
|
||||
const fields: Array<[string, unknown]> = [
|
||||
['causeMessage', current.message],
|
||||
['causeCode', current.code],
|
||||
['causeSyscall', current.syscall],
|
||||
['causeHostname', current.hostname],
|
||||
]
|
||||
for (const [key, value] of fields) {
|
||||
const text = String(value || '').trim()
|
||||
if (text) {
|
||||
detail[key] = text
|
||||
}
|
||||
}
|
||||
|
||||
return detail
|
||||
}
|
||||
|
||||
function normalizeOpenApiPath(value: unknown): string {
|
||||
const path = String(value || '').trim()
|
||||
if (!path) {
|
||||
return '/'
|
||||
}
|
||||
return path.startsWith('/') ? path : `/${path}`
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
export type JsonObject = Record<string, any>
|
||||
|
||||
export function pickDefinedBizParams(input: JsonObject): JsonObject {
|
||||
const output: JsonObject = {}
|
||||
|
||||
for (const [key, value] of Object.entries(input)) {
|
||||
if (value === undefined || value === null) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (typeof value === 'string' && value.trim() === '') {
|
||||
continue
|
||||
}
|
||||
|
||||
output[key] = value
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
export function normalizeOpenApiLong(value: unknown): string | number {
|
||||
const text = String(value ?? '').trim()
|
||||
if (!text) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (!/^\d+$/.test(text)) {
|
||||
return text
|
||||
}
|
||||
|
||||
const parsed = Number(text)
|
||||
return Number.isSafeInteger(parsed) ? parsed : text
|
||||
}
|
||||
|
||||
export function normalizeOpenApiInteger(value: unknown, fallback = 0): number {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) ? parsed : fallback
|
||||
}
|
||||
|
||||
export function normalizeOpenApiPositiveInteger(value: unknown, fallback = 1): number {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback
|
||||
}
|
||||
|
||||
export function normalizeStringList(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return value
|
||||
.map((item) => String(item || '').trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { requestKuaishouIndustryOpenApi } from './openapi-client.js'
|
||||
import {
|
||||
normalizeOpenApiInteger,
|
||||
normalizeOpenApiLong,
|
||||
normalizeOpenApiPositiveInteger,
|
||||
normalizeStringList,
|
||||
pickDefinedBizParams,
|
||||
type JsonObject,
|
||||
} from './openapi-values.js'
|
||||
|
||||
export type KuaishouIndustryRefundListInput = {
|
||||
sellerId?: string
|
||||
beginTime?: unknown
|
||||
endTime?: unknown
|
||||
type?: unknown
|
||||
pageSize?: unknown
|
||||
currentPage?: unknown
|
||||
sort?: unknown
|
||||
queryType?: unknown
|
||||
negotiateStatus?: unknown
|
||||
pcursor?: unknown
|
||||
status?: unknown
|
||||
option?: JsonObject
|
||||
orderId?: unknown
|
||||
}
|
||||
|
||||
export type KuaishouIndustryRefundApproveInput = {
|
||||
sellerId?: string
|
||||
refundId?: unknown
|
||||
desc?: unknown
|
||||
refundAmount?: unknown
|
||||
status?: unknown
|
||||
negotiateStatus?: unknown
|
||||
refundHandingWay?: unknown
|
||||
}
|
||||
|
||||
export type KuaishouIndustryRefundDisagreeInput = {
|
||||
sellerId?: string
|
||||
refundId?: unknown
|
||||
sellerDisagreeReason?: unknown
|
||||
sellerDisagreeDesc?: unknown
|
||||
sellerDisagreeImages?: unknown
|
||||
status?: unknown
|
||||
negotiateStatus?: unknown
|
||||
}
|
||||
|
||||
export function listKuaishouIndustryRefunds(input: KuaishouIndustryRefundListInput = {}) {
|
||||
const bizParams = pickDefinedBizParams({
|
||||
beginTime: normalizeOpenApiLong(input.beginTime),
|
||||
endTime: normalizeOpenApiLong(input.endTime),
|
||||
type: normalizeOpenApiInteger(input.type, 8),
|
||||
pageSize: normalizeOpenApiPositiveInteger(input.pageSize, 50),
|
||||
currentPage: normalizeOpenApiPositiveInteger(input.currentPage, 1),
|
||||
sort: input.sort === undefined || input.sort === '' ? undefined : normalizeOpenApiInteger(input.sort, 1),
|
||||
queryType: input.queryType === undefined || input.queryType === ''
|
||||
? undefined
|
||||
: normalizeOpenApiInteger(input.queryType, 1),
|
||||
negotiateStatus: input.negotiateStatus === undefined || input.negotiateStatus === ''
|
||||
? undefined
|
||||
: normalizeOpenApiInteger(input.negotiateStatus, 0),
|
||||
pcursor: String(input.pcursor ?? ''),
|
||||
status: input.status === undefined || input.status === '' ? undefined : normalizeOpenApiInteger(input.status, 0),
|
||||
option: input.option && typeof input.option === 'object' ? input.option : undefined,
|
||||
orderId: normalizeOpenApiLong(input.orderId),
|
||||
})
|
||||
|
||||
return requestKuaishouIndustryOpenApi({
|
||||
apiMethod: 'open.seller.order.refund.pcursor.list',
|
||||
path: '/open/seller/order/refund/pcursor/list',
|
||||
httpMethod: 'GET',
|
||||
bizParams,
|
||||
...(input.sellerId ? { sellerId: String(input.sellerId).trim() } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
export function approveKuaishouIndustryRefund(input: KuaishouIndustryRefundApproveInput = {}) {
|
||||
const bizParams = pickDefinedBizParams({
|
||||
refundId: normalizeOpenApiLong(input.refundId),
|
||||
desc: String(input.desc ?? '').trim(),
|
||||
refundAmount: normalizeOpenApiLong(input.refundAmount),
|
||||
status: input.status === undefined || input.status === '' ? undefined : normalizeOpenApiInteger(input.status, 0),
|
||||
negotiateStatus: input.negotiateStatus === undefined || input.negotiateStatus === ''
|
||||
? undefined
|
||||
: normalizeOpenApiInteger(input.negotiateStatus, 0),
|
||||
refundHandingWay: input.refundHandingWay === undefined || input.refundHandingWay === ''
|
||||
? undefined
|
||||
: normalizeOpenApiInteger(input.refundHandingWay, 0),
|
||||
})
|
||||
|
||||
return requestKuaishouIndustryOpenApi({
|
||||
apiMethod: 'open.seller.order.refund.approve',
|
||||
path: '/open/seller/order/refund/approve',
|
||||
bizParams,
|
||||
...(input.sellerId ? { sellerId: String(input.sellerId).trim() } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
export function disagreeKuaishouIndustryRefund(input: KuaishouIndustryRefundDisagreeInput = {}) {
|
||||
const bizParams = pickDefinedBizParams({
|
||||
refundId: normalizeOpenApiLong(input.refundId),
|
||||
sellerDisagreeReason: normalizeOpenApiInteger(input.sellerDisagreeReason, 100),
|
||||
sellerDisagreeDesc: String(input.sellerDisagreeDesc ?? '').trim(),
|
||||
sellerDisagreeImages: normalizeStringList(input.sellerDisagreeImages),
|
||||
status: normalizeOpenApiInteger(input.status, 10),
|
||||
negotiateStatus: normalizeOpenApiInteger(input.negotiateStatus, 1),
|
||||
})
|
||||
|
||||
return requestKuaishouIndustryOpenApi({
|
||||
apiMethod: 'open.seller.order.refund.disagree.refund',
|
||||
path: '/open/seller/order/refund/disagree/refund',
|
||||
bizParams,
|
||||
...(input.sellerId ? { sellerId: String(input.sellerId).trim() } : {}),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { requestKuaishouIndustryOpenApi } from './openapi-client.js'
|
||||
import {
|
||||
normalizeOpenApiPositiveInteger,
|
||||
pickDefinedBizParams,
|
||||
type JsonObject,
|
||||
} from './openapi-values.js'
|
||||
|
||||
export type KuaishouIndustryReverseEticketInput = {
|
||||
id?: unknown
|
||||
code?: unknown
|
||||
num?: unknown
|
||||
}
|
||||
|
||||
export type KuaishouIndustryReverseCallbackInput = {
|
||||
sellerId?: string
|
||||
oid?: unknown
|
||||
eticketType?: unknown
|
||||
etickets?: unknown
|
||||
serialNum?: unknown
|
||||
reason?: unknown
|
||||
ext?: unknown
|
||||
token?: unknown
|
||||
}
|
||||
|
||||
export function reverseKuaishouIndustryCallback(input: KuaishouIndustryReverseCallbackInput = {}) {
|
||||
const bizParams = pickDefinedBizParams({
|
||||
oid: String(input.oid || '').trim(),
|
||||
eticketType: String(input.eticketType || '').trim(),
|
||||
etickets: normalizeReverseEtickets(input.etickets),
|
||||
serialNum: String(input.serialNum || '').trim(),
|
||||
reason: String(input.reason || '').trim(),
|
||||
ext: normalizeReverseExt(input.ext),
|
||||
token: String(input.token || '').trim(),
|
||||
})
|
||||
|
||||
return requestKuaishouIndustryOpenApi({
|
||||
apiMethod: 'integration.callback.virtual.eticket.reverse',
|
||||
path: '/integration/callback/virtual/eticket/reverse',
|
||||
bizParams,
|
||||
...(input.sellerId ? { sellerId: String(input.sellerId).trim() } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
function normalizeReverseEtickets(value: unknown): JsonObject[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return value
|
||||
.map((item) => normalizeReverseEticket(item))
|
||||
.filter((item): item is JsonObject => Boolean(item))
|
||||
}
|
||||
|
||||
function normalizeReverseEticket(value: unknown): JsonObject | null {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const current = value as KuaishouIndustryReverseEticketInput
|
||||
const id = String(current.id || '').trim()
|
||||
const code = String(current.code || '').trim()
|
||||
const num = normalizeOpenApiPositiveInteger(current.num, 1)
|
||||
|
||||
if (!id && !code) {
|
||||
return null
|
||||
}
|
||||
|
||||
return pickDefinedBizParams({ id, code, num })
|
||||
}
|
||||
|
||||
function normalizeReverseExt(value: unknown): unknown {
|
||||
if (!value) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
const text = value.trim()
|
||||
return text || undefined
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
@@ -1,12 +1,5 @@
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
import { logIntegration } from '../../../utils/logger.js'
|
||||
import { getKuaishouIndustryConfig } from './config.js'
|
||||
import { ensureKuaishouIndustryAccessToken } from './token-service.js'
|
||||
|
||||
type JsonObject = Record<string, any>
|
||||
|
||||
const DEFAULT_KUAISHOU_OPEN_API = 'https://openapi.kwaixiaodian.com'
|
||||
import { requestKuaishouIndustryOpenApi, type JsonObject } from './openapi-client.js'
|
||||
|
||||
type SendCallbackInput = {
|
||||
oid: string
|
||||
@@ -29,37 +22,6 @@ type SendCallbackInput = {
|
||||
}
|
||||
|
||||
export async function sendCallback(input: SendCallbackInput): Promise<{ success: boolean; response?: JsonObject; error?: string }> {
|
||||
let config = getKuaishouIndustryConfig()
|
||||
|
||||
if (!config.enabled) {
|
||||
logIntegration('[kuaishou-industry/send-callback]', '行业电子凭证配置未启用,跳过发货回调')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
try {
|
||||
const tokenResult = await ensureKuaishouIndustryAccessToken({
|
||||
config,
|
||||
...(input.sellerId ? { sellerId: input.sellerId } : {}),
|
||||
})
|
||||
config = {
|
||||
...config,
|
||||
...tokenResult.config,
|
||||
accessToken: tokenResult.accessToken,
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
logIntegration('[kuaishou-industry/send-callback]', 'accessToken 刷新失败,无法发起发货回调', {
|
||||
error: message,
|
||||
}, { level: 'warn' })
|
||||
return { success: false, error: message }
|
||||
}
|
||||
|
||||
if (!config.accessToken) {
|
||||
const message = 'accessToken 未配置,无法发起发货回调'
|
||||
logIntegration('[kuaishou-industry/send-callback]', message, undefined, { level: 'warn' })
|
||||
return { success: false, error: message }
|
||||
}
|
||||
|
||||
const bizParams: JsonObject = {
|
||||
oid: input.oid,
|
||||
sendType: input.sendType,
|
||||
@@ -83,153 +45,70 @@ export async function sendCallback(input: SendCallbackInput): Promise<{ success:
|
||||
if (input.expressCode) bizParams.expressCode = input.expressCode
|
||||
if (input.expressNo) bizParams.expressNo = input.expressNo
|
||||
|
||||
const paramStr = JSON.stringify(bizParams)
|
||||
|
||||
const signParams: JsonObject = {
|
||||
method: 'integration.callback.virtual.eticket.send',
|
||||
appkey: config.appKey,
|
||||
access_token: config.accessToken,
|
||||
version: '1',
|
||||
timestamp: Date.now(),
|
||||
signMethod: 'MD5',
|
||||
param: paramStr,
|
||||
}
|
||||
|
||||
const sign = signPayload(signParams, config.signSecret)
|
||||
signParams.sign = sign
|
||||
|
||||
const body = new URLSearchParams()
|
||||
for (const [key, value] of Object.entries(signParams)) {
|
||||
body.append(key, String(value))
|
||||
}
|
||||
|
||||
const url = `${resolveKuaishouOpenApiBaseUrl(config.baseUrl)}/integration/callback/virtual/eticket/send`
|
||||
const result = await requestKuaishouIndustryOpenApi({
|
||||
apiMethod: 'integration.callback.virtual.eticket.send',
|
||||
path: '/integration/callback/virtual/eticket/send',
|
||||
bizParams,
|
||||
...(input.sellerId ? { sellerId: input.sellerId } : {}),
|
||||
onRequest: (request) => {
|
||||
const requestLog = {
|
||||
sellerId: input.sellerId || '',
|
||||
...buildSendCallbackRequestLog({
|
||||
url,
|
||||
signParams,
|
||||
bizParams,
|
||||
}),
|
||||
...request,
|
||||
}
|
||||
|
||||
logIntegration('[kuaishou-industry/send-callback]', `发起电子凭证发货回调 oid=${input.oid}`, requestLog)
|
||||
|
||||
try {
|
||||
const startedAt = Date.now()
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
||||
body: body.toString(),
|
||||
},
|
||||
})
|
||||
const text = await res.text()
|
||||
let json: JsonObject = {}
|
||||
try { json = JSON.parse(text) } catch { json = { raw: text } }
|
||||
|
||||
const durationMs = Date.now() - startedAt
|
||||
const ok = res.ok && Number(json.result) === 1
|
||||
|
||||
if (ok) {
|
||||
logIntegration('[kuaishou-industry/send-callback]', `电子凭证发货回调成功 oid=${input.oid}`, {
|
||||
durationMs,
|
||||
status: res.status,
|
||||
request: requestLog,
|
||||
response: json,
|
||||
})
|
||||
} else {
|
||||
logIntegration('[kuaishou-industry/send-callback]', `电子凭证发货回调失败 oid=${input.oid}`, {
|
||||
durationMs,
|
||||
status: res.status,
|
||||
request: requestLog,
|
||||
response: json,
|
||||
}, { level: 'warn' })
|
||||
if (result.skippedReason === 'disabled') {
|
||||
logIntegration('[kuaishou-industry/send-callback]', '行业电子凭证配置未启用,跳过发货回调')
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
return { success: ok, response: json }
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
logIntegration('[kuaishou-industry/send-callback]', `电子凭证发货回调异常 oid=${input.oid}`, {
|
||||
...resolveCallbackErrorDetail(err),
|
||||
request: requestLog,
|
||||
if (result.skippedReason === 'token_error') {
|
||||
const message = result.error || 'accessToken 刷新失败'
|
||||
logIntegration('[kuaishou-industry/send-callback]', 'accessToken 刷新失败,无法发起发货回调', {
|
||||
error: message,
|
||||
}, { level: 'warn' })
|
||||
return { success: false, error: message }
|
||||
}
|
||||
|
||||
if (result.skippedReason === 'missing_access_token') {
|
||||
const message = 'accessToken 未配置,无法发起发货回调'
|
||||
logIntegration('[kuaishou-industry/send-callback]', message, undefined, { level: 'warn' })
|
||||
return { success: false, error: message }
|
||||
}
|
||||
|
||||
const requestLog = result.request
|
||||
? {
|
||||
sellerId: input.sellerId || '',
|
||||
...result.request,
|
||||
}
|
||||
: undefined
|
||||
|
||||
if (result.success) {
|
||||
logIntegration('[kuaishou-industry/send-callback]', `电子凭证发货回调成功 oid=${input.oid}`, {
|
||||
durationMs: result.durationMs,
|
||||
status: result.httpStatus,
|
||||
request: requestLog,
|
||||
response: result.response || null,
|
||||
})
|
||||
} else if (result.error) {
|
||||
logIntegration('[kuaishou-industry/send-callback]', `电子凭证发货回调异常 oid=${input.oid}`, {
|
||||
...(result.errorDetail || { error: result.error }),
|
||||
request: requestLog,
|
||||
}, { level: 'warn' })
|
||||
} else {
|
||||
logIntegration('[kuaishou-industry/send-callback]', `电子凭证发货回调失败 oid=${input.oid}`, {
|
||||
durationMs: result.durationMs,
|
||||
status: result.httpStatus,
|
||||
request: requestLog,
|
||||
response: result.response || null,
|
||||
}, { level: 'warn' })
|
||||
}
|
||||
|
||||
function buildSendCallbackRequestLog({
|
||||
url,
|
||||
signParams,
|
||||
bizParams,
|
||||
}: {
|
||||
url: string
|
||||
signParams: JsonObject
|
||||
bizParams: JsonObject
|
||||
}): JsonObject {
|
||||
return {
|
||||
url,
|
||||
method: 'POST',
|
||||
contentType: 'application/x-www-form-urlencoded',
|
||||
apiMethod: signParams.method,
|
||||
appkey: signParams.appkey,
|
||||
access_token: signParams.access_token,
|
||||
version: signParams.version,
|
||||
timestamp: signParams.timestamp,
|
||||
signMethod: signParams.signMethod,
|
||||
sign: signParams.sign,
|
||||
param: bizParams,
|
||||
success: result.success,
|
||||
...(result.response ? { response: result.response } : {}),
|
||||
...(result.error ? { error: result.error } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
function signPayload(params: JsonObject, signSecret: string): string {
|
||||
const entries = Object.entries(params)
|
||||
.filter(([key]) => key !== 'sign' && key !== 'signSecret')
|
||||
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
||||
|
||||
const queryString = entries
|
||||
.map(([key, value]) => `${key}=${stringifySignValue(value)}`)
|
||||
.join('&')
|
||||
|
||||
const source = `${queryString}&signSecret=${signSecret}`
|
||||
|
||||
return crypto.createHash('md5').update(source, 'utf8').digest('hex').toLowerCase()
|
||||
}
|
||||
|
||||
function stringifySignValue(value: unknown): string {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return String(value)
|
||||
if (typeof value === 'boolean') return value ? 'true' : 'false'
|
||||
if (value == null) return ''
|
||||
if (typeof value === 'object') return JSON.stringify(value, Object.keys(value as Record<string, unknown>).sort())
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function resolveCallbackErrorDetail(error: unknown): JsonObject {
|
||||
const detail: JsonObject = {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}
|
||||
const cause = error && typeof error === 'object' && 'cause' in error
|
||||
? (error as { cause?: unknown }).cause
|
||||
: null
|
||||
if (!cause || typeof cause !== 'object') {
|
||||
return detail
|
||||
}
|
||||
|
||||
const current = cause as Record<string, unknown>
|
||||
const fields: Array<[string, unknown]> = [
|
||||
['causeMessage', current.message],
|
||||
['causeCode', current.code],
|
||||
['causeSyscall', current.syscall],
|
||||
['causeHostname', current.hostname],
|
||||
]
|
||||
for (const [key, value] of fields) {
|
||||
const text = String(value || '').trim()
|
||||
if (text) {
|
||||
detail[key] = text
|
||||
}
|
||||
}
|
||||
|
||||
return detail
|
||||
}
|
||||
|
||||
function resolveKuaishouOpenApiBaseUrl(value: unknown): string {
|
||||
return String(value || DEFAULT_KUAISHOU_OPEN_API).trim().replace(/\/+$/, '') || DEFAULT_KUAISHOU_OPEN_API
|
||||
}
|
||||
|
||||
@@ -12,8 +12,15 @@ import type {
|
||||
AdminCloudtentaclesSkuBuyInput,
|
||||
AdminCloudtentaclesSkuUseInput,
|
||||
AdminCloudtentaclesSourceConfigInput,
|
||||
AdminKuaishouIndustryRefundApproveInput,
|
||||
AdminKuaishouIndustryRefundDisagreeInput,
|
||||
AdminKuaishouIndustryRefundListInput,
|
||||
AdminKuaishouIndustryAuthorizationCodeInput,
|
||||
AdminKuaishouIndustrySourceConfigInput,
|
||||
AdminKuaishouIndustryVoucherCheckAvailableInput,
|
||||
AdminKuaishouIndustryVoucherConsumeInput,
|
||||
AdminKuaishouIndustryVoucherResendInput,
|
||||
AdminKuaishouIndustryVoucherReverseInput,
|
||||
AdminCloudtentaclesTestLoginInput,
|
||||
AdminCloudtentaclesValidateSessionInput,
|
||||
AdminCloudtentaclesVirtualNumberInput,
|
||||
@@ -36,6 +43,13 @@ export type AdminRouteAdminSession = AdminViewerSessionInput
|
||||
export type AdminKuaishouEticketSourceConfigRouteBody = AdminKuaishouEticketSourceConfigInput
|
||||
export type AdminKuaishouIndustrySourceConfigRouteBody = AdminKuaishouIndustrySourceConfigInput
|
||||
export type AdminKuaishouIndustryAuthorizationCodeRouteBody = AdminKuaishouIndustryAuthorizationCodeInput
|
||||
export type AdminKuaishouIndustryRefundListRouteBody = AdminKuaishouIndustryRefundListInput
|
||||
export type AdminKuaishouIndustryRefundApproveRouteBody = AdminKuaishouIndustryRefundApproveInput
|
||||
export type AdminKuaishouIndustryRefundDisagreeRouteBody = AdminKuaishouIndustryRefundDisagreeInput
|
||||
export type AdminKuaishouIndustryVoucherCheckAvailableRouteBody = AdminKuaishouIndustryVoucherCheckAvailableInput
|
||||
export type AdminKuaishouIndustryVoucherReverseRouteBody = AdminKuaishouIndustryVoucherReverseInput
|
||||
export type AdminKuaishouIndustryVoucherConsumeRouteBody = AdminKuaishouIndustryVoucherConsumeInput
|
||||
export type AdminKuaishouIndustryVoucherResendRouteBody = AdminKuaishouIndustryVoucherResendInput
|
||||
export type AdminNotificationConfigRouteBody = AdminNotificationConfigInput
|
||||
export type AdminNotificationTestRouteBody = AdminNotificationTestInput
|
||||
export type AdminScheduledJobsConfigRouteBody = AdminScheduledJobsConfigInput
|
||||
|
||||
@@ -285,3 +285,85 @@ export type AdminTaskKuaishouIndustryConsumeInput = {
|
||||
expressCode?: string
|
||||
expressNo?: string
|
||||
}
|
||||
|
||||
export type AdminKuaishouIndustryRefundListInput = {
|
||||
sellerId?: string
|
||||
beginTime?: number | string
|
||||
endTime?: number | string
|
||||
type?: number | string
|
||||
pageSize?: number | string
|
||||
currentPage?: number | string
|
||||
sort?: number | string
|
||||
queryType?: number | string
|
||||
negotiateStatus?: number | string
|
||||
pcursor?: string
|
||||
status?: number | string
|
||||
orderId?: string
|
||||
option?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type AdminKuaishouIndustryRefundApproveInput = {
|
||||
sellerId?: string
|
||||
refundId?: number | string
|
||||
desc?: string
|
||||
refundAmount?: number | string
|
||||
status?: number | string
|
||||
negotiateStatus?: number | string
|
||||
refundHandingWay?: number | string
|
||||
}
|
||||
|
||||
export type AdminKuaishouIndustryRefundDisagreeInput = {
|
||||
sellerId?: string
|
||||
refundId?: number | string
|
||||
sellerDisagreeReason?: number | string
|
||||
sellerDisagreeDesc?: string
|
||||
sellerDisagreeImages?: string[]
|
||||
status?: number | string
|
||||
negotiateStatus?: number | string
|
||||
}
|
||||
|
||||
export type AdminKuaishouIndustryVoucherCheckAvailableInput = {
|
||||
sellerId?: string
|
||||
buyerId?: string
|
||||
orderId?: string
|
||||
oid?: string
|
||||
voucherCode?: string
|
||||
eticketType?: string
|
||||
bizTypeCode?: string
|
||||
etickets?: Array<{ id?: string; code?: string; num?: number | string }>
|
||||
}
|
||||
|
||||
export type AdminKuaishouIndustryVoucherReverseInput = {
|
||||
sellerId?: string
|
||||
oid?: string
|
||||
orderId?: string
|
||||
voucherCode?: string
|
||||
eticketType?: string
|
||||
etickets?: Array<{ id?: string; code?: string; num?: number | string }>
|
||||
serialNum?: string
|
||||
reason?: string
|
||||
ext?: string | Record<string, unknown>
|
||||
token?: string
|
||||
}
|
||||
|
||||
export type AdminKuaishouIndustryVoucherConsumeInput = {
|
||||
oid?: string
|
||||
orderId?: string
|
||||
taskId?: number | string
|
||||
voucherCode?: string
|
||||
token?: string
|
||||
eticketType?: string
|
||||
consumeType?: string
|
||||
serialNum?: string
|
||||
storeName?: string
|
||||
storeAddress?: string
|
||||
expressCode?: string
|
||||
expressNo?: string
|
||||
}
|
||||
|
||||
export type AdminKuaishouIndustryVoucherResendInput = {
|
||||
oid?: string
|
||||
orderId?: string
|
||||
taskId?: number | string
|
||||
voucherCode?: string
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ const AdminAuditLogsPage = lazy(() => import('@/pages/admin/AdminAuditLogsPage')
|
||||
const AdminCloudtentaclesRecordsPage = lazy(
|
||||
() => import('@/pages/admin/AdminCloudtentaclesRecordsPage'),
|
||||
)
|
||||
const AdminKuaishouIndustryPage = lazy(() => import('@/pages/admin/AdminKuaishouIndustryPage'))
|
||||
const AdminLoginPage = lazy(() => import('@/pages/admin/AdminLoginPage'))
|
||||
const AdminOrderDetailPage = lazy(() => import('@/pages/admin/AdminOrderDetailPage'))
|
||||
const AdminOrdersPage = lazy(() => import('@/pages/admin/AdminOrdersPage'))
|
||||
@@ -64,6 +65,7 @@ export default function App() {
|
||||
<Route path="orders/:orderId" element={<AdminOrderDetailPage />} />
|
||||
<Route path="tasks" element={<AdminTasksPage />} />
|
||||
<Route path="tasks/:taskId" element={<AdminTaskDetailPage />} />
|
||||
<Route path="kuaishou-industry" element={<AdminKuaishouIndustryPage />} />
|
||||
<Route path="cloudtentacles-records" element={<AdminCloudtentaclesRecordsPage />} />
|
||||
<Route element={<RequireRole roles={['admin']} />}>
|
||||
<Route path="users" element={<AdminUsersPage />} />
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
OrderedListOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
SettingOutlined,
|
||||
ShopOutlined,
|
||||
TeamOutlined,
|
||||
@@ -44,6 +45,7 @@ export default function AdminLayout() {
|
||||
{ key: '/admin/dashboard', icon: <DashboardOutlined />, label: '概览' },
|
||||
{ key: '/admin/orders', icon: <OrderedListOutlined />, label: '订单' },
|
||||
{ key: '/admin/tasks', icon: <UnorderedListOutlined />, label: '任务' },
|
||||
{ key: '/admin/kuaishou-industry', icon: <SafetyCertificateOutlined />, label: '电子凭证' },
|
||||
{ key: '/admin/cloudtentacles-records', icon: <FileSearchOutlined />, label: '发货记录' },
|
||||
]
|
||||
|
||||
@@ -181,6 +183,7 @@ export default function AdminLayout() {
|
||||
function resolveSelectedKey(pathname: string) {
|
||||
if (pathname.startsWith('/admin/orders')) return '/admin/orders'
|
||||
if (pathname.startsWith('/admin/tasks')) return '/admin/tasks'
|
||||
if (pathname.startsWith('/admin/kuaishou-industry')) return '/admin/kuaishou-industry'
|
||||
if (pathname === '/admin/platform-shops') return '/admin/platform-shops'
|
||||
return pathname
|
||||
}
|
||||
|
||||
@@ -0,0 +1,834 @@
|
||||
import {
|
||||
CheckCircleOutlined,
|
||||
ReloadOutlined,
|
||||
RollbackOutlined,
|
||||
SearchOutlined,
|
||||
SendOutlined,
|
||||
SyncOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
DatePicker,
|
||||
Input,
|
||||
InputNumber,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import type { TableColumnsType } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import type { Dayjs } from 'dayjs'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import PageHeader from '@/components/admin/PageHeader'
|
||||
import { showError, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
approveAdminKuaishouIndustryRefund,
|
||||
checkAdminKuaishouIndustryVoucherAvailable,
|
||||
consumeAdminKuaishouIndustryVoucher,
|
||||
disagreeAdminKuaishouIndustryRefund,
|
||||
fetchAdminKuaishouIndustryVouchers,
|
||||
listAdminKuaishouIndustryRefunds,
|
||||
resendAdminKuaishouIndustryVoucherCode,
|
||||
reverseAdminKuaishouIndustryVoucher,
|
||||
} from '@/services/admin'
|
||||
import type {
|
||||
AdminKuaishouIndustryOpenApiResult,
|
||||
AdminKuaishouIndustryRefundApprovePayload,
|
||||
AdminKuaishouIndustryRefundDisagreePayload,
|
||||
AdminKuaishouIndustryRefundListPayload,
|
||||
AdminKuaishouIndustryVoucher,
|
||||
AdminKuaishouIndustryVoucherToolPayload,
|
||||
} from '@/types/admin'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
import { stringifyDisplayJson } from '@/utils/date-time'
|
||||
|
||||
type DateRangeValue = [Dayjs | null, Dayjs | null] | null
|
||||
|
||||
type VoucherFilterState = {
|
||||
oid: string
|
||||
voucherCode: string
|
||||
taskId: string
|
||||
sellerId: string
|
||||
status: string
|
||||
page: number
|
||||
pageSize: number
|
||||
}
|
||||
|
||||
type RefundListState = {
|
||||
sellerId: string
|
||||
orderId: string
|
||||
type: string
|
||||
pageSize: string
|
||||
currentPage: string
|
||||
pcursor: string
|
||||
status: string
|
||||
negotiateStatus: string
|
||||
}
|
||||
|
||||
const DEFAULT_ETICKET_TYPE = 'DINING_OPEN_TICKET'
|
||||
|
||||
export default function AdminKuaishouIndustryPage() {
|
||||
const [voucherLoading, setVoucherLoading] = useState(false)
|
||||
const [actionLoading, setActionLoading] = useState('')
|
||||
const [vouchers, setVouchers] = useState<AdminKuaishouIndustryVoucher[]>([])
|
||||
const [voucherTotal, setVoucherTotal] = useState(0)
|
||||
const [voucherFilters, setVoucherFilters] = useState<VoucherFilterState>({
|
||||
oid: '',
|
||||
voucherCode: '',
|
||||
taskId: '',
|
||||
sellerId: '',
|
||||
status: '',
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
})
|
||||
const [toolForm, setToolForm] = useState<AdminKuaishouIndustryVoucherToolPayload>({
|
||||
eticketType: DEFAULT_ETICKET_TYPE,
|
||||
consumeType: 'delivery',
|
||||
})
|
||||
const [refundDateRange, setRefundDateRange] = useState<DateRangeValue>(() => [
|
||||
dayjs().subtract(1, 'day'),
|
||||
dayjs(),
|
||||
])
|
||||
const [refundListForm, setRefundListForm] = useState<RefundListState>({
|
||||
sellerId: '',
|
||||
orderId: '',
|
||||
type: '8',
|
||||
pageSize: '50',
|
||||
currentPage: '1',
|
||||
pcursor: '',
|
||||
status: '10',
|
||||
negotiateStatus: '',
|
||||
})
|
||||
const [approveForm, setApproveForm] = useState<AdminKuaishouIndustryRefundApprovePayload>({
|
||||
refundId: '',
|
||||
refundAmount: '',
|
||||
status: '',
|
||||
negotiateStatus: '',
|
||||
refundHandingWay: '10',
|
||||
})
|
||||
const [disagreeForm, setDisagreeForm] = useState<AdminKuaishouIndustryRefundDisagreePayload>({
|
||||
refundId: '',
|
||||
sellerDisagreeReason: '100',
|
||||
sellerDisagreeDesc: '',
|
||||
status: '10',
|
||||
negotiateStatus: '1',
|
||||
})
|
||||
const [lastResultTitle, setLastResultTitle] = useState('接口结果')
|
||||
const [lastResult, setLastResult] = useState<unknown>(null)
|
||||
const [refundRows, setRefundRows] = useState<Array<Record<string, unknown>>>([])
|
||||
|
||||
const columns = useMemo<TableColumnsType<AdminKuaishouIndustryVoucher>>(
|
||||
() => [
|
||||
{
|
||||
title: '券码',
|
||||
minWidth: 220,
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text strong copyable>
|
||||
{row.voucherCode}
|
||||
</Typography.Text>
|
||||
<span className="muted">序号:{row.unitIndex}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '订单 / 任务',
|
||||
minWidth: 220,
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text copyable>{row.oid || '-'}</Typography.Text>
|
||||
<span className="muted">
|
||||
任务:{row.taskId || '-'} / 订单:{row.orderId || '-'}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
width: 150,
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<Tag color={getVoucherStatusColor(row.status)}>{getVoucherStatusLabel(row.status)}</Tag>
|
||||
<Tag color={row.sendCallbackStatus === 'success' ? 'green' : 'orange'}>
|
||||
发码:{row.sendCallbackStatus || '-'}
|
||||
</Tag>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '卖家',
|
||||
minWidth: 160,
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<span>{row.sellerId || '-'}</span>
|
||||
<span className="muted">Token:{row.tokenMasked || '-'}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '核销',
|
||||
minWidth: 180,
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<span>{row.consumeSerialNum || '-'}</span>
|
||||
<span className="muted">{formatAdminDateTime(row.consumedAt)}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
width: 170,
|
||||
render: (_, row) => formatAdminDateTime(row.updatedAt),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
fixed: 'right',
|
||||
width: 120,
|
||||
render: (_, row) => (
|
||||
<Button size="small" type="primary" ghost onClick={() => selectVoucher(row)}>
|
||||
选中
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
],
|
||||
[],
|
||||
)
|
||||
|
||||
const refundColumns = useMemo<TableColumnsType<Record<string, unknown>>>(
|
||||
() => [
|
||||
{ title: '售后单', dataIndex: 'refundId', minWidth: 160 },
|
||||
{ title: '订单', dataIndex: 'oid', minWidth: 160 },
|
||||
{ title: '状态', dataIndex: 'status', width: 100 },
|
||||
{ title: '协商', dataIndex: 'negotiateStatus', width: 100 },
|
||||
{ title: '金额', dataIndex: 'refundFee', width: 100 },
|
||||
{
|
||||
title: '提交时间',
|
||||
minWidth: 170,
|
||||
render: (_, row) => formatTimestamp(row.submitTime || row.createTime),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
fixed: 'right',
|
||||
width: 120,
|
||||
render: (_, row) => (
|
||||
<Button size="small" onClick={() => fillRefundAction(row)}>
|
||||
填入
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
],
|
||||
[],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
void loadVouchers()
|
||||
}, [])
|
||||
|
||||
async function loadVouchers(nextFilters: Partial<VoucherFilterState> = {}) {
|
||||
const filters = { ...voucherFilters, ...nextFilters }
|
||||
setVoucherFilters(filters)
|
||||
setVoucherLoading(true)
|
||||
|
||||
try {
|
||||
const response = await fetchAdminKuaishouIndustryVouchers({
|
||||
oid: filters.oid.trim(),
|
||||
voucherCode: filters.voucherCode.trim(),
|
||||
taskId: filters.taskId.trim(),
|
||||
sellerId: filters.sellerId.trim(),
|
||||
status: filters.status,
|
||||
page: filters.page,
|
||||
pageSize: filters.pageSize,
|
||||
})
|
||||
setVouchers(response.data.items)
|
||||
setVoucherTotal(response.data.pagination.total)
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '查询电子凭证失败')
|
||||
} finally {
|
||||
setVoucherLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
function selectVoucher(row: AdminKuaishouIndustryVoucher) {
|
||||
setToolForm((current) => ({
|
||||
...current,
|
||||
sellerId: row.sellerId,
|
||||
oid: row.oid,
|
||||
orderId: row.oid,
|
||||
taskId: row.taskId || '',
|
||||
voucherCode: row.voucherCode,
|
||||
serialNum: row.consumeSerialNum || current.serialNum,
|
||||
}))
|
||||
showSuccess('已填入券码操作区')
|
||||
}
|
||||
|
||||
async function runCheckAvailable() {
|
||||
await runOpenApiAction('检查电子凭证有效性', 'check', () =>
|
||||
checkAdminKuaishouIndustryVoucherAvailable(buildVoucherToolPayload()),
|
||||
)
|
||||
}
|
||||
|
||||
async function runReverse() {
|
||||
await runOpenApiAction('电子凭证冲正回调', 'reverse', () =>
|
||||
reverseAdminKuaishouIndustryVoucher({
|
||||
...buildVoucherToolPayload(),
|
||||
reason: toolForm.reason || '后台手动冲正',
|
||||
}),
|
||||
)
|
||||
await loadVouchers()
|
||||
}
|
||||
|
||||
async function runConsume() {
|
||||
setActionLoading('consume')
|
||||
try {
|
||||
const response = await consumeAdminKuaishouIndustryVoucher(buildVoucherToolPayload())
|
||||
setLastResultTitle('手动核销')
|
||||
setLastResult(response.data)
|
||||
showSuccess('手动核销已完成')
|
||||
await loadVouchers()
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '手动核销失败')
|
||||
} finally {
|
||||
setActionLoading('')
|
||||
}
|
||||
}
|
||||
|
||||
async function runResendCode() {
|
||||
setActionLoading('resend')
|
||||
try {
|
||||
const response = await resendAdminKuaishouIndustryVoucherCode(buildVoucherToolPayload())
|
||||
setLastResultTitle('重发发码回调')
|
||||
setLastResult(response.data)
|
||||
if (response.data.success) {
|
||||
showSuccess('发码回调已重发')
|
||||
} else {
|
||||
showError(response.data.error || '发码回调重发失败')
|
||||
}
|
||||
await loadVouchers()
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : '重发发码回调失败')
|
||||
} finally {
|
||||
setActionLoading('')
|
||||
}
|
||||
}
|
||||
|
||||
async function runRefundList() {
|
||||
const [begin, end] = refundDateRange || []
|
||||
await runOpenApiAction('售后单列表', 'refund-list', () =>
|
||||
listAdminKuaishouIndustryRefunds({
|
||||
...refundListForm,
|
||||
beginTime: begin?.valueOf(),
|
||||
endTime: end?.valueOf(),
|
||||
} satisfies AdminKuaishouIndustryRefundListPayload),
|
||||
(result) => {
|
||||
const rows = extractRefundRows(result.response)
|
||||
setRefundRows(rows)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
async function runApproveRefund() {
|
||||
await runOpenApiAction('同意退款', 'refund-approve', () =>
|
||||
approveAdminKuaishouIndustryRefund({
|
||||
...approveForm,
|
||||
sellerId: approveForm.sellerId || refundListForm.sellerId,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function runDisagreeRefund() {
|
||||
await runOpenApiAction('不同意退款', 'refund-disagree', () =>
|
||||
disagreeAdminKuaishouIndustryRefund({
|
||||
...disagreeForm,
|
||||
sellerId: disagreeForm.sellerId || refundListForm.sellerId,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async function runOpenApiAction(
|
||||
title: string,
|
||||
loadingKey: string,
|
||||
action: () => Promise<{ data: AdminKuaishouIndustryOpenApiResult }>,
|
||||
afterSuccess?: (result: AdminKuaishouIndustryOpenApiResult) => void,
|
||||
) {
|
||||
setActionLoading(loadingKey)
|
||||
try {
|
||||
const response = await action()
|
||||
setLastResultTitle(title)
|
||||
setLastResult(response.data)
|
||||
afterSuccess?.(response.data)
|
||||
if (response.data.success) {
|
||||
showSuccess(`${title}已执行`)
|
||||
} else {
|
||||
showError(response.data.error || `${title}返回失败`)
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : `${title}失败`)
|
||||
} finally {
|
||||
setActionLoading('')
|
||||
}
|
||||
}
|
||||
|
||||
function buildVoucherToolPayload(): AdminKuaishouIndustryVoucherToolPayload {
|
||||
const voucherCode = String(toolForm.voucherCode || '').trim()
|
||||
return {
|
||||
...toolForm,
|
||||
etickets: voucherCode ? [{ id: voucherCode, code: voucherCode, num: 1 }] : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
function fillRefundAction(row: Record<string, unknown>) {
|
||||
const refundId = String(row.refundId || '').trim()
|
||||
const status = String(row.status || '').trim()
|
||||
const negotiateStatus = String(row.negotiateStatus || '').trim()
|
||||
setApproveForm((current) => ({
|
||||
...current,
|
||||
refundId,
|
||||
status,
|
||||
negotiateStatus,
|
||||
refundAmount: String(row.refundFee || current.refundAmount || ''),
|
||||
}))
|
||||
setDisagreeForm((current) => ({
|
||||
...current,
|
||||
refundId,
|
||||
status,
|
||||
negotiateStatus: negotiateStatus || current.negotiateStatus,
|
||||
}))
|
||||
showSuccess('已填入退款操作区')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page-stack kuaishou-industry-page">
|
||||
<PageHeader title="快手电子凭证" description="行业电子凭证接口工具与售后处理" />
|
||||
|
||||
<Tabs
|
||||
items={[
|
||||
{
|
||||
key: 'vouchers',
|
||||
label: '券码操作',
|
||||
children: renderVoucherTab(),
|
||||
},
|
||||
{
|
||||
key: 'refunds',
|
||||
label: '售后退款',
|
||||
children: renderRefundTab(),
|
||||
},
|
||||
{
|
||||
key: 'result',
|
||||
label: '接口结果',
|
||||
children: renderResultTab(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
function renderVoucherTab() {
|
||||
return (
|
||||
<div className="page-stack">
|
||||
<Card>
|
||||
<Space wrap className="filter-form">
|
||||
<Input
|
||||
allowClear
|
||||
placeholder="订单号 oid"
|
||||
value={voucherFilters.oid}
|
||||
onChange={(event) => setVoucherFilters({ ...voucherFilters, oid: event.target.value })}
|
||||
/>
|
||||
<Input
|
||||
allowClear
|
||||
placeholder="券码"
|
||||
value={voucherFilters.voucherCode}
|
||||
onChange={(event) =>
|
||||
setVoucherFilters({ ...voucherFilters, voucherCode: event.target.value })
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
allowClear
|
||||
placeholder="任务 ID"
|
||||
value={voucherFilters.taskId}
|
||||
onChange={(event) =>
|
||||
setVoucherFilters({ ...voucherFilters, taskId: event.target.value })
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
allowClear
|
||||
placeholder="卖家 ID"
|
||||
value={voucherFilters.sellerId}
|
||||
onChange={(event) =>
|
||||
setVoucherFilters({ ...voucherFilters, sellerId: event.target.value })
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
allowClear
|
||||
placeholder="券码状态"
|
||||
style={{ width: 132 }}
|
||||
value={voucherFilters.status || undefined}
|
||||
options={[
|
||||
{ label: '未使用', value: 'UNUSED' },
|
||||
{ label: '已核销', value: 'CONSUMED' },
|
||||
{ label: '已销毁', value: 'DESTROYED' },
|
||||
]}
|
||||
onChange={(value) => setVoucherFilters({ ...voucherFilters, status: value || '' })}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SearchOutlined />}
|
||||
loading={voucherLoading}
|
||||
onClick={() => loadVouchers({ page: 1 })}
|
||||
>
|
||||
查询
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<div className="kuaishou-industry-split">
|
||||
<Card title="本地券码">
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={vouchers}
|
||||
loading={voucherLoading}
|
||||
scroll={{ x: 1180 }}
|
||||
pagination={{
|
||||
current: voucherFilters.page,
|
||||
pageSize: voucherFilters.pageSize,
|
||||
total: voucherTotal,
|
||||
showSizeChanger: true,
|
||||
onChange: (page, pageSize) => loadVouchers({ page, pageSize }),
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card title="接口操作">
|
||||
<div className="kuaishou-industry-tool-grid">
|
||||
<Input
|
||||
placeholder="卖家 ID"
|
||||
value={toolForm.sellerId}
|
||||
onChange={(event) => updateToolForm({ sellerId: event.target.value })}
|
||||
/>
|
||||
<Input
|
||||
placeholder="订单号 oid"
|
||||
value={toolForm.oid}
|
||||
onChange={(event) =>
|
||||
updateToolForm({ oid: event.target.value, orderId: event.target.value })
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder="任务 ID"
|
||||
value={String(toolForm.taskId || '')}
|
||||
onChange={(event) => updateToolForm({ taskId: event.target.value })}
|
||||
/>
|
||||
<Input
|
||||
placeholder="券码"
|
||||
value={toolForm.voucherCode}
|
||||
onChange={(event) => updateToolForm({ voucherCode: event.target.value })}
|
||||
/>
|
||||
<Input
|
||||
placeholder="电子凭证类型"
|
||||
value={toolForm.eticketType}
|
||||
onChange={(event) => updateToolForm({ eticketType: event.target.value })}
|
||||
/>
|
||||
<Input
|
||||
placeholder="核销序列号"
|
||||
value={toolForm.serialNum}
|
||||
onChange={(event) => updateToolForm({ serialNum: event.target.value })}
|
||||
/>
|
||||
<Input
|
||||
placeholder="核销类型"
|
||||
value={toolForm.consumeType}
|
||||
onChange={(event) => updateToolForm({ consumeType: event.target.value })}
|
||||
/>
|
||||
<Input
|
||||
placeholder="冲正原因"
|
||||
value={toolForm.reason}
|
||||
onChange={(event) => updateToolForm({ reason: event.target.value })}
|
||||
/>
|
||||
<Input
|
||||
placeholder="门店名称"
|
||||
value={toolForm.storeName}
|
||||
onChange={(event) => updateToolForm({ storeName: event.target.value })}
|
||||
/>
|
||||
<Input
|
||||
placeholder="门店地址"
|
||||
value={toolForm.storeAddress}
|
||||
onChange={(event) => updateToolForm({ storeAddress: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<Space wrap className="kuaishou-industry-actions">
|
||||
<Button
|
||||
icon={<SyncOutlined />}
|
||||
loading={actionLoading === 'check'}
|
||||
onClick={runCheckAvailable}
|
||||
>
|
||||
检查有效性
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<CheckCircleOutlined />}
|
||||
loading={actionLoading === 'consume'}
|
||||
onClick={runConsume}
|
||||
>
|
||||
手动核销
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
icon={<RollbackOutlined />}
|
||||
loading={actionLoading === 'reverse'}
|
||||
onClick={runReverse}
|
||||
>
|
||||
冲正
|
||||
</Button>
|
||||
<Button
|
||||
icon={<SendOutlined />}
|
||||
loading={actionLoading === 'resend'}
|
||||
onClick={runResendCode}
|
||||
>
|
||||
重发发码
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function renderRefundTab() {
|
||||
return (
|
||||
<div className="page-stack">
|
||||
<Card title="售后单列表">
|
||||
<Space wrap className="filter-form">
|
||||
<Input
|
||||
placeholder="卖家 ID"
|
||||
value={refundListForm.sellerId}
|
||||
onChange={(event) => updateRefundListForm({ sellerId: event.target.value })}
|
||||
/>
|
||||
<Input
|
||||
placeholder="订单号"
|
||||
value={refundListForm.orderId}
|
||||
onChange={(event) => updateRefundListForm({ orderId: event.target.value })}
|
||||
/>
|
||||
<DatePicker.RangePicker
|
||||
showTime
|
||||
value={refundDateRange}
|
||||
onChange={(value) => setRefundDateRange(value)}
|
||||
/>
|
||||
<Select
|
||||
style={{ width: 140 }}
|
||||
value={refundListForm.type}
|
||||
options={[
|
||||
{ label: '等待退款', value: '8' },
|
||||
{ label: '全部退款', value: '9' },
|
||||
]}
|
||||
onChange={(value) => updateRefundListForm({ type: value })}
|
||||
/>
|
||||
<Input placeholder="状态" value={refundListForm.status} onChange={(event) => updateRefundListForm({ status: event.target.value })} />
|
||||
<Input placeholder="游标" value={refundListForm.pcursor} onChange={(event) => updateRefundListForm({ pcursor: event.target.value })} />
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={100}
|
||||
value={Number(refundListForm.pageSize)}
|
||||
onChange={(value) => updateRefundListForm({ pageSize: String(value || 50) })}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SearchOutlined />}
|
||||
loading={actionLoading === 'refund-list'}
|
||||
onClick={runRefundList}
|
||||
>
|
||||
查询售后
|
||||
</Button>
|
||||
</Space>
|
||||
<Table
|
||||
rowKey={(row) => String(row.refundId || row.oid || JSON.stringify(row))}
|
||||
columns={refundColumns}
|
||||
dataSource={refundRows}
|
||||
scroll={{ x: 980 }}
|
||||
pagination={{ pageSize: 10 }}
|
||||
style={{ marginTop: 16 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<div className="kuaishou-industry-split">
|
||||
<Card title="同意退款">
|
||||
<div className="kuaishou-industry-tool-grid">
|
||||
<Input
|
||||
placeholder="卖家 ID"
|
||||
value={approveForm.sellerId || refundListForm.sellerId}
|
||||
onChange={(event) => setApproveForm({ ...approveForm, sellerId: event.target.value })}
|
||||
/>
|
||||
<Input
|
||||
placeholder="退款单编号"
|
||||
value={String(approveForm.refundId || '')}
|
||||
onChange={(event) => setApproveForm({ ...approveForm, refundId: event.target.value })}
|
||||
/>
|
||||
<Input
|
||||
placeholder="退款金额(分)"
|
||||
value={String(approveForm.refundAmount || '')}
|
||||
onChange={(event) =>
|
||||
setApproveForm({ ...approveForm, refundAmount: event.target.value })
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder="退款单状态"
|
||||
value={String(approveForm.status || '')}
|
||||
onChange={(event) => setApproveForm({ ...approveForm, status: event.target.value })}
|
||||
/>
|
||||
<Input
|
||||
placeholder="协商状态"
|
||||
value={String(approveForm.negotiateStatus || '')}
|
||||
onChange={(event) =>
|
||||
setApproveForm({ ...approveForm, negotiateStatus: event.target.value })
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder="退款方式"
|
||||
value={String(approveForm.refundHandingWay || '')}
|
||||
onChange={(event) =>
|
||||
setApproveForm({ ...approveForm, refundHandingWay: event.target.value })
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder="说明"
|
||||
value={approveForm.desc}
|
||||
onChange={(event) => setApproveForm({ ...approveForm, desc: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<CheckCircleOutlined />}
|
||||
loading={actionLoading === 'refund-approve'}
|
||||
onClick={runApproveRefund}
|
||||
>
|
||||
同意退款
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Card title="不同意退款">
|
||||
<div className="kuaishou-industry-tool-grid">
|
||||
<Input
|
||||
placeholder="卖家 ID"
|
||||
value={disagreeForm.sellerId || refundListForm.sellerId}
|
||||
onChange={(event) =>
|
||||
setDisagreeForm({ ...disagreeForm, sellerId: event.target.value })
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder="退款单编号"
|
||||
value={String(disagreeForm.refundId || '')}
|
||||
onChange={(event) =>
|
||||
setDisagreeForm({ ...disagreeForm, refundId: event.target.value })
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder="拒绝原因枚举"
|
||||
value={String(disagreeForm.sellerDisagreeReason || '')}
|
||||
onChange={(event) =>
|
||||
setDisagreeForm({ ...disagreeForm, sellerDisagreeReason: event.target.value })
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder="拒绝说明"
|
||||
value={disagreeForm.sellerDisagreeDesc}
|
||||
onChange={(event) =>
|
||||
setDisagreeForm({ ...disagreeForm, sellerDisagreeDesc: event.target.value })
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder="退款单状态"
|
||||
value={String(disagreeForm.status || '')}
|
||||
onChange={(event) =>
|
||||
setDisagreeForm({ ...disagreeForm, status: event.target.value })
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder="协商状态"
|
||||
value={String(disagreeForm.negotiateStatus || '')}
|
||||
onChange={(event) =>
|
||||
setDisagreeForm({ ...disagreeForm, negotiateStatus: event.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
danger
|
||||
icon={<RollbackOutlined />}
|
||||
loading={actionLoading === 'refund-disagree'}
|
||||
onClick={runDisagreeRefund}
|
||||
>
|
||||
不同意退款
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function renderResultTab() {
|
||||
return (
|
||||
<Card
|
||||
title={lastResultTitle}
|
||||
extra={
|
||||
<Button icon={<ReloadOutlined />} onClick={() => setLastResult(null)}>
|
||||
清空
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
{lastResult ? (
|
||||
<pre className="json-preview">{stringifyDisplayJson(lastResult)}</pre>
|
||||
) : (
|
||||
<Alert type="info" showIcon message="暂无接口结果" />
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function updateToolForm(patch: Partial<AdminKuaishouIndustryVoucherToolPayload>) {
|
||||
setToolForm((current) => ({ ...current, ...patch }))
|
||||
}
|
||||
|
||||
function updateRefundListForm(patch: Partial<RefundListState>) {
|
||||
setRefundListForm((current) => ({ ...current, ...patch }))
|
||||
}
|
||||
}
|
||||
|
||||
function extractRefundRows(response: Record<string, unknown> | null): Array<Record<string, unknown>> {
|
||||
const data = response?.data
|
||||
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const rows = (data as { refundOrderInfoList?: unknown }).refundOrderInfoList
|
||||
return Array.isArray(rows)
|
||||
? rows.filter((item): item is Record<string, unknown> =>
|
||||
Boolean(item && typeof item === 'object' && !Array.isArray(item)),
|
||||
)
|
||||
: []
|
||||
}
|
||||
|
||||
function getVoucherStatusColor(status: string) {
|
||||
const normalized = String(status || '').toUpperCase()
|
||||
if (normalized === 'CONSUMED') return 'green'
|
||||
if (normalized === 'DESTROYED') return 'red'
|
||||
return 'blue'
|
||||
}
|
||||
|
||||
function getVoucherStatusLabel(status: string) {
|
||||
const normalized = String(status || '').toUpperCase()
|
||||
if (normalized === 'CONSUMED') return '已核销'
|
||||
if (normalized === 'DESTROYED') return '已销毁'
|
||||
if (normalized === 'UNUSED') return '未使用'
|
||||
return status || '-'
|
||||
}
|
||||
|
||||
function formatTimestamp(value: unknown) {
|
||||
const timestamp = Number(value || 0)
|
||||
if (!Number.isFinite(timestamp) || timestamp <= 0) {
|
||||
return '-'
|
||||
}
|
||||
|
||||
return formatAdminDateTime(new Date(timestamp).toISOString())
|
||||
}
|
||||
@@ -3,5 +3,6 @@ export * from './dashboard'
|
||||
export * from './users'
|
||||
export * from './audit-logs'
|
||||
export * from './platform-config'
|
||||
export * from './kuaishou-industry'
|
||||
export * from './orders'
|
||||
export * from './tasks'
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { apiGet, apiPost } from '@/lib/http'
|
||||
import type {
|
||||
AdminKuaishouIndustryOpenApiResult,
|
||||
AdminKuaishouIndustryRefundApprovePayload,
|
||||
AdminKuaishouIndustryRefundDisagreePayload,
|
||||
AdminKuaishouIndustryRefundListPayload,
|
||||
AdminKuaishouIndustryVoucherConsumeResult,
|
||||
AdminKuaishouIndustryVoucherListResult,
|
||||
AdminKuaishouIndustryVoucherResendResult,
|
||||
AdminKuaishouIndustryVoucherToolPayload,
|
||||
} from '@/types/admin'
|
||||
|
||||
export function fetchAdminKuaishouIndustryVouchers(params?: Record<string, unknown>) {
|
||||
return apiGet<AdminKuaishouIndustryVoucherListResult>(
|
||||
'/api/v1/admin/kuaishou-industry/vouchers',
|
||||
params,
|
||||
)
|
||||
}
|
||||
|
||||
export function listAdminKuaishouIndustryRefunds(
|
||||
payload: AdminKuaishouIndustryRefundListPayload,
|
||||
) {
|
||||
return apiPost<AdminKuaishouIndustryOpenApiResult>(
|
||||
'/api/v1/admin/kuaishou-industry/refunds/list',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function approveAdminKuaishouIndustryRefund(
|
||||
payload: AdminKuaishouIndustryRefundApprovePayload,
|
||||
) {
|
||||
return apiPost<AdminKuaishouIndustryOpenApiResult>(
|
||||
'/api/v1/admin/kuaishou-industry/refunds/approve',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function disagreeAdminKuaishouIndustryRefund(
|
||||
payload: AdminKuaishouIndustryRefundDisagreePayload,
|
||||
) {
|
||||
return apiPost<AdminKuaishouIndustryOpenApiResult>(
|
||||
'/api/v1/admin/kuaishou-industry/refunds/disagree',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function checkAdminKuaishouIndustryVoucherAvailable(
|
||||
payload: AdminKuaishouIndustryVoucherToolPayload,
|
||||
) {
|
||||
return apiPost<AdminKuaishouIndustryOpenApiResult>(
|
||||
'/api/v1/admin/kuaishou-industry/vouchers/check-available',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function reverseAdminKuaishouIndustryVoucher(
|
||||
payload: AdminKuaishouIndustryVoucherToolPayload,
|
||||
) {
|
||||
return apiPost<AdminKuaishouIndustryOpenApiResult>(
|
||||
'/api/v1/admin/kuaishou-industry/vouchers/reverse',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function consumeAdminKuaishouIndustryVoucher(
|
||||
payload: AdminKuaishouIndustryVoucherToolPayload,
|
||||
) {
|
||||
return apiPost<AdminKuaishouIndustryVoucherConsumeResult>(
|
||||
'/api/v1/admin/kuaishou-industry/vouchers/consume',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
export function resendAdminKuaishouIndustryVoucherCode(
|
||||
payload: AdminKuaishouIndustryVoucherToolPayload,
|
||||
) {
|
||||
return apiPost<AdminKuaishouIndustryVoucherResendResult>(
|
||||
'/api/v1/admin/kuaishou-industry/vouchers/resend-code',
|
||||
payload,
|
||||
)
|
||||
}
|
||||
@@ -306,6 +306,24 @@ select {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.kuaishou-industry-split {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.45fr) minmax(360px, 0.75fr);
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.kuaishou-industry-tool-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.kuaishou-industry-actions {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.task-action-hint {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
@@ -1083,6 +1101,10 @@ select {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.kuaishou-industry-split {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.manual-dispatch-grid,
|
||||
.fulfillment-overview-grid,
|
||||
.task-flow-grid {
|
||||
|
||||
@@ -28,6 +28,18 @@ export type {
|
||||
AdminTaskDetail,
|
||||
} from './tasks'
|
||||
|
||||
export type {
|
||||
AdminKuaishouIndustryOpenApiResult,
|
||||
AdminKuaishouIndustryVoucher,
|
||||
AdminKuaishouIndustryVoucherListResult,
|
||||
AdminKuaishouIndustryVoucherConsumeResult,
|
||||
AdminKuaishouIndustryVoucherResendResult,
|
||||
AdminKuaishouIndustryRefundListPayload,
|
||||
AdminKuaishouIndustryRefundApprovePayload,
|
||||
AdminKuaishouIndustryRefundDisagreePayload,
|
||||
AdminKuaishouIndustryVoucherToolPayload,
|
||||
} from './kuaishou-industry'
|
||||
|
||||
// Platform config types
|
||||
export type {
|
||||
AdminNotificationBarkRecipient,
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import type { AdminPagination } from './common'
|
||||
|
||||
export interface AdminKuaishouIndustryOpenApiResult {
|
||||
success: boolean
|
||||
response: Record<string, unknown> | null
|
||||
error: string
|
||||
request: Record<string, unknown> | null
|
||||
durationMs: number
|
||||
httpStatus: number
|
||||
skippedReason: string
|
||||
voucher?: AdminKuaishouIndustryVoucher | null
|
||||
}
|
||||
|
||||
export interface AdminKuaishouIndustryVoucher {
|
||||
id: number
|
||||
voucherCode: string
|
||||
oid: string
|
||||
orderId: number | null
|
||||
taskId: number | null
|
||||
unitIndex: number
|
||||
sellerId: string
|
||||
tokenMasked: string
|
||||
status: string
|
||||
validStartTime: number
|
||||
validEndTime: number
|
||||
consumeSerialNum: string
|
||||
consumeDetails: Array<Record<string, unknown>>
|
||||
consumedAt: string | null
|
||||
destroyedAt: string | null
|
||||
sendCallbackStatus: string
|
||||
sendCallbackAttemptCount: number
|
||||
sendCallbackLastError: string
|
||||
sendCallbackSentAt: string | null
|
||||
rawPayload: Record<string, unknown>
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface AdminKuaishouIndustryVoucherListResult {
|
||||
items: AdminKuaishouIndustryVoucher[]
|
||||
pagination: AdminPagination
|
||||
}
|
||||
|
||||
export interface AdminKuaishouIndustryVoucherConsumeResult {
|
||||
success: boolean
|
||||
voucher: AdminKuaishouIndustryVoucher
|
||||
task: null | {
|
||||
taskId: number
|
||||
taskNo: string
|
||||
status: string
|
||||
deliveryStatus: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface AdminKuaishouIndustryVoucherResendResult {
|
||||
success: boolean
|
||||
response: Record<string, unknown> | null
|
||||
error: string
|
||||
voucher: AdminKuaishouIndustryVoucher | null
|
||||
}
|
||||
|
||||
export interface AdminKuaishouIndustryRefundListPayload {
|
||||
sellerId?: string
|
||||
beginTime?: number | string
|
||||
endTime?: number | string
|
||||
type?: number | string
|
||||
pageSize?: number | string
|
||||
currentPage?: number | string
|
||||
sort?: number | string
|
||||
queryType?: number | string
|
||||
negotiateStatus?: number | string
|
||||
pcursor?: string
|
||||
status?: number | string
|
||||
orderId?: string
|
||||
}
|
||||
|
||||
export interface AdminKuaishouIndustryRefundApprovePayload {
|
||||
sellerId?: string
|
||||
refundId?: number | string
|
||||
desc?: string
|
||||
refundAmount?: number | string
|
||||
status?: number | string
|
||||
negotiateStatus?: number | string
|
||||
refundHandingWay?: number | string
|
||||
}
|
||||
|
||||
export interface AdminKuaishouIndustryRefundDisagreePayload {
|
||||
sellerId?: string
|
||||
refundId?: number | string
|
||||
sellerDisagreeReason?: number | string
|
||||
sellerDisagreeDesc?: string
|
||||
sellerDisagreeImages?: string[]
|
||||
status?: number | string
|
||||
negotiateStatus?: number | string
|
||||
}
|
||||
|
||||
export interface AdminKuaishouIndustryVoucherToolPayload {
|
||||
sellerId?: string
|
||||
buyerId?: string
|
||||
orderId?: string
|
||||
oid?: string
|
||||
taskId?: number | string
|
||||
voucherCode?: string
|
||||
eticketType?: string
|
||||
consumeType?: string
|
||||
serialNum?: string
|
||||
reason?: string
|
||||
token?: string
|
||||
storeName?: string
|
||||
storeAddress?: string
|
||||
expressCode?: string
|
||||
expressNo?: string
|
||||
etickets?: Array<{ id?: string; code?: string; num?: number | string }>
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
kwaishop_aftersales_addRefund
|
||||
需商家授权
|
||||
售后单新增消息
|
||||
更新时间: 2024-04-22 14:25:16
|
||||
有新增售后单时触发消息发送
|
||||
系统参数
|
||||
参数名 类型 详情描述
|
||||
eventId String 消息唯一id
|
||||
msgId String 业务消息内容唯一id
|
||||
bizId Long 业务id如订单id、退款单id、商品id
|
||||
userId Long 授权用户id
|
||||
openId String 授权用户openId
|
||||
appKey String 应用id
|
||||
event String 消息标示
|
||||
info String 消息内容,业务内容Json串,详见消息文档参数
|
||||
status Integer 状态 0未知 1发送中 2发送成功 3发送失败
|
||||
createTime Long 创建时间
|
||||
updateTime Long 更新时间
|
||||
业务参数
|
||||
参数名 类型 示例值 详情描述
|
||||
orderId Long 2015612346452419 订单id
|
||||
refundId Long 32142132 售后单id
|
||||
handlingWay Integer 3 退款方式,枚举: [1, "退货退款"] [10, "仅退款"] [3, "换货"][4, "补寄"][5, "维修"]
|
||||
specialRefundType Integer 3 特殊退款类型[0, "非特殊退款"] [1, "价保"]
|
||||
status Integer 3 订单退款状态[10, "买家已经申请退款,等待卖家同意"] [12, "卖家已拒绝,等待买家处理"] [20, "协商纠纷,等待平台处理"] [30, "卖家已经同意退款,等待买家退货"] [40, "买家已经退货,等待卖家确认收货"] [45, "卖家已经发货,等待买家确认收货"] [50, "卖家已经同意退款,等待系统执行退款"] [60, "退款成功"] [70, "退款关闭"]
|
||||
createTime Long 1679035606000 售后单创建时间
|
||||
消息示例
|
||||
JSON
|
||||
{
|
||||
"eventId": "237_6341_60079475",
|
||||
"msgId": "470_60079469",
|
||||
"bizId": 23666666666666,
|
||||
"userId": 2066666666,
|
||||
"openId": "f19666666666666666666666666666666",
|
||||
"appKey": "ks66666666666666666",
|
||||
"event": "kwaishop_aftersales_addRefund",
|
||||
"info": {
|
||||
"orderId": 2015612346452419,
|
||||
"refundId": 32142132,
|
||||
"handlingWay": 3,
|
||||
"specialRefundType": 3,
|
||||
"status": 3,
|
||||
"createTime": 1679035606000
|
||||
},
|
||||
"status": 2,
|
||||
"createTime": 1691563032872,
|
||||
"updateTime": 1691567756044
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
open.seller.order.refund.pcursor.list
|
||||
需商家授权
|
||||
GET
|
||||
获取售后单列表
|
||||
更新时间: 2024-04-22 16:28:28
|
||||
查询商家售后单列表(游标方式),可根据订单id查询关联的全量售后单列表,handlingway为售后方式,status为售后状态
|
||||
系统环境
|
||||
环境 域名
|
||||
线上环境(推荐) https://openapi.kwaixiaodian.com
|
||||
线上环境(备用) https://open.kwaixiaodian.com
|
||||
系统参数
|
||||
参数名 类型 必须 详情描述
|
||||
appkey String 是 平台分配的appId
|
||||
timestamp Number 是 发起请求的Unix时间戳,单位为毫秒
|
||||
access_token String 是 访问token,所有需用户授权API使用code模式获取,不需要用户授权API使用client_credentials获取,详情参考《授权说明》文档
|
||||
version Number 是 请求的API版本号,目前版本为1
|
||||
param JSON 是 业务参数,详见下方的请求入参
|
||||
method String 是 请求的API英文名,详见各API定义
|
||||
sign String 是 API入参的签名结果参数
|
||||
signMethod String 是 签名算法,支持HMAC_SHA256和MD5,推荐使用HMAC_SHA256
|
||||
请求参数
|
||||
参数名 类型 必须 示例值 详情描述
|
||||
beginTime Long 是 1543817629000 订单生成的开始时间(单位毫秒),在当前时间的近90天内,不能大于90天,且小于截止时间
|
||||
endTime Long 是 1543817629000 订单生成的截止时间(单位毫秒)在当前时间的近90天内,不能大于90天,且大于开始时间,且与开始时间的时间范围不大于1天 (与开始时间的时间范围建议做成随时可配置,该范围可能在活动期间随时变化,比如变成小时级或者分钟级)。
|
||||
type Integer 是 8 退款单请求类型,8 等待退款 9 全部退款订单
|
||||
pageSize Integer 是 50 每次请求数量,最多一页100条
|
||||
currentPage Long 是 1 当前页码
|
||||
sort Integer 否 1 排序方式,1时间降序 2时间升序 ,默认降序
|
||||
queryType Integer 否 1 查找方式,1按创建时间查找 2按更新时间查找 ,默认创建时间
|
||||
negotiateStatus Integer 否 1 协商状态,1待商家处理 2 商家同意 3商家驳回,等待买家修改 默认返回所有数据
|
||||
pcursor String 是 1543843735000_1 游标内容,第一次传空串,之后传上一次的pcursor返回值,若返回“nomore”则标识到底
|
||||
status Integer 否 10 退款状态,枚举:[10, "买家已经申请退款,等待卖家同意"] [12, "卖家已拒绝,等待买家处理"] [20, "协商纠纷,等待平台处理"] [30, "卖家已经同意退款,等待买家退货"] [40, "买家已经退货,等待卖家确认收货"] [45, "卖家已经发货,等待买家确认收货"] [50, "卖家已经同意退款,等待系统执行退款"] [60, "退款成功"] [70, "退款关闭"]
|
||||
option RefundPageOption 否 {} 选项
|
||||
orderId Long 否 2412314120000 订单id,可根据订单id查询关联的所有售后单列表
|
||||
返回参数
|
||||
参数名 类型 示例值 详情描述
|
||||
code String 1 主返回码
|
||||
msg String success 主返回信息
|
||||
sub_code String 1 子返回码
|
||||
sub_msg String SUCCESS 子返回信息
|
||||
result Integer 1 返回码,1正确,其他返回码表示接口未执行成功
|
||||
data MerchantRefundListDataView {} 售后单列表信息
|
||||
error_msg String SUCCESS 返回码描述
|
||||
错误码
|
||||
错误码 错误类型 错误描述 解决方法
|
||||
13 参数非法 参数不合法 分页参数不符合‘nomore’ 或 ‘数字_数字’的格式,请检查
|
||||
16 系统错误 请求过于频繁 请求过于频繁,请稍后重试
|
||||
51 参数非法 pageSize 应为20 pageSize超过了100
|
||||
53 参数非法 page number 过大 page number 过大,分页参数为nomore报错
|
||||
300002 业务错误 退款单类型错误 退款单类型错误
|
||||
300003 业务错误 结束时间必须大于开始时间 结束时间必须大于开始时间
|
||||
300005 业务错误 开始时间不能大于90天前 开始时间不能大于90天前
|
||||
300019 业务错误 时间范围超限 时间范围超限
|
||||
300107 业务错误 状态无效 status参数不在RefundStatusEnum里
|
||||
300109 业务错误 退款单请求类型为等待退款时,退款单状态列表中不能包含非等待退款的状态 退款单请求类型为等待退款时,退款单状态列表中不能包含非等待退款的状态
|
||||
请求示例
|
||||
JAVA
|
||||
CURL
|
||||
package com.kuaishou.merchant.open.api.sdk.demo;
|
||||
import com.kuaishou.merchant.open.api.common.utils.GsonUtils;
|
||||
import com.kuaishou.merchant.open.api.request.refund.OpenSellerOrderRefundPcursorListRequest;
|
||||
import com.kuaishou.merchant.open.api.response.refund.OpenSellerOrderRefundPcursorListResponse;
|
||||
import com.kuaishou.merchant.open.api.client.AccessTokenKsMerchantClient;
|
||||
|
||||
public class AccessTokenKsMerchantClientDemo {
|
||||
|
||||
public static void main(String[] args) {
|
||||
String url = "https://openapi.kwaixiaodian.com";
|
||||
String appKey = "your appKey";
|
||||
String signSecret = "your app signSecret";
|
||||
String accessToken = "your accessToken";
|
||||
|
||||
AccessTokenKsMerchantClient client = new AccessTokenKsMerchantClient(url,appKey,signSecret);
|
||||
|
||||
OpenSellerOrderRefundPcursorListRequest request = new OpenSellerOrderRefundPcursorListRequest();
|
||||
request.setAccessToken(accessToken);
|
||||
request.setApiMethodVersion(1L);
|
||||
|
||||
request.setBeginTime(1543817629000);
|
||||
request.setEndTime(1543817629000);
|
||||
request.setType(8);
|
||||
request.setPageSize(50);
|
||||
request.setCurrentPage(1);
|
||||
request.setSort(1);
|
||||
request.setQueryType(1);
|
||||
request.setNegotiateStatus(1);
|
||||
request.setPcursor("1543843735000_1");
|
||||
request.setStatus(10);
|
||||
RefundPageOption obj1 = new RefundPageOption();
|
||||
obj1.setNeedExchange(false);
|
||||
request.setOption(obj1);
|
||||
request.setOrderId(2412314120000);
|
||||
|
||||
OpenSellerOrderRefundPcursorListResponse response = client.execute(request);
|
||||
|
||||
System.out.println(GsonUtils.toJSON(response));
|
||||
}
|
||||
}
|
||||
|
||||
响应示例
|
||||
JSON
|
||||
{
|
||||
"result": 1,
|
||||
"data": {
|
||||
"currentPage": 1,
|
||||
"pageSize": 100,
|
||||
"totalPage": 10,
|
||||
"totalSize": 100,
|
||||
"beginTime": 1597248000000,
|
||||
"endTime": 1597276800000,
|
||||
"pcursor": "nomore",
|
||||
"refundOrderInfoList": [
|
||||
{
|
||||
"oid": 12020384,
|
||||
"refundId": 2016300011212478,
|
||||
"handlingWay": 1,
|
||||
"negotiateStatus": 1,
|
||||
"refundFee": 890,
|
||||
"skuId": 92381223,
|
||||
"refundReason": 1,
|
||||
"status": 10,
|
||||
"buyerId": 63523209,
|
||||
"refundType": 1298723,
|
||||
"sellerId": 92371012,
|
||||
"refundDesc": "描述",
|
||||
"submitTime": 1543843735000,
|
||||
"relItemId": 111,
|
||||
"negotiateUpdateTime": 1543843735000,
|
||||
"updateTime": 1543843735000,
|
||||
"createTime": 1543843735000,
|
||||
"relSkuId": 120823876,
|
||||
"skuNick": "111",
|
||||
"logisticsId": 1234567,
|
||||
"endTime": 1543843735000,
|
||||
"itemId": 123456,
|
||||
"receiptStatus": 1,
|
||||
"refundReasonDesc": "拍多、拍错",
|
||||
"expireTime": 1543843735000
|
||||
}
|
||||
]
|
||||
},
|
||||
"error_msg": "SUCCESS"
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
open.seller.order.refund.approve
|
||||
需商家授权
|
||||
POST
|
||||
商家同意退款
|
||||
更新时间: 2023-11-01 14:45:53
|
||||
1.售后单类型为仅退款时,调用该API完成退款操作
|
||||
2.售后单类型为退货退款时,使用open.seller.order.refund.returngoods.approve(商家同意退货API)完成同意退货操作
|
||||
系统环境
|
||||
环境 域名
|
||||
线上环境(推荐) https://openapi.kwaixiaodian.com
|
||||
线上环境(备用) https://open.kwaixiaodian.com
|
||||
系统参数
|
||||
参数名 类型 必须 详情描述
|
||||
appkey String 是 平台分配的appId
|
||||
timestamp Number 是 发起请求的Unix时间戳,单位为毫秒
|
||||
access_token String 是 访问token,所有需用户授权API使用code模式获取,不需要用户授权API使用client_credentials获取,详情参考《授权说明》文档
|
||||
version Number 是 请求的API版本号,目前版本为1
|
||||
param JSON 是 业务参数,详见下方的请求入参
|
||||
method String 是 请求的API英文名,详见各API定义
|
||||
sign String 是 API入参的签名结果参数
|
||||
signMethod String 是 签名算法,支持HMAC_SHA256和MD5,推荐使用HMAC_SHA256
|
||||
请求参数
|
||||
参数名 类型 必须 示例值 详情描述
|
||||
refundId Long 是 129230087 退款单编号
|
||||
desc String 否 说明 退款说明(预计4月中旬下线)
|
||||
refundAmount Long 是 1000 退款金额 单位:分
|
||||
status Integer 否 1 退款单当前状态
|
||||
negotiateStatus Integer 否 1 协商状态,枚举: [1, "待商家处理"] [2, "商家同意"] [3, "商家驳回,等待买家修改"]
|
||||
refundHandingWay Integer 否 1 退款方式,枚举:[0, "未知"] [1, "退货退款"] [10,"仅退款"]
|
||||
返回参数
|
||||
参数名 类型 示例值 详情描述
|
||||
code String 1 主返回码
|
||||
msg String success 主返回信息
|
||||
sub_code String 1 子返回码
|
||||
sub_msg String SUCCESS 子返回信息
|
||||
result Integer 1 返回码,1正确,其他返回码表示接口未执行成功
|
||||
error_msg String SUCCESS 返回码描述
|
||||
错误码
|
||||
错误码 错误类型 错误描述 解决方法
|
||||
16 系统错误 请求过于频繁 请求过于频繁,请稍后重试
|
||||
21 业务错误 当前退款单状态已更新 当前退款单状态已更新,请刷新后重试
|
||||
1004 系统错误 服务繁忙 服务繁忙,请稍后重试
|
||||
3616 业务错误 当前退款单状态已更新 当前退款单状态已更新,请刷新后重试
|
||||
6002 业务错误 保证金余额不足 保证金余额不足,请检查
|
||||
300009 业务错误 退款金额为0 退款金额为0,请检查
|
||||
300011 业务错误 退款单不存在 退款单不存在,请检查
|
||||
300013 业务错误 买家取消退款 买家取消退款
|
||||
300014 业务错误 退款单状态已更新 退款单状态已更新,请刷新重试
|
||||
300108 业务错误 输入的退款金额与退款单实际金额不匹配 输入的退款金额与退款单实际金额不匹配,请检查
|
||||
请求示例
|
||||
JAVA
|
||||
CURL
|
||||
package com.kuaishou.merchant.open.api.sdk.demo;
|
||||
import com.kuaishou.merchant.open.api.common.utils.GsonUtils;
|
||||
import com.kuaishou.merchant.open.api.request.refund.OpenSellerOrderRefundApproveRequest;
|
||||
import com.kuaishou.merchant.open.api.response.refund.OpenSellerOrderRefundApproveResponse;
|
||||
import com.kuaishou.merchant.open.api.client.AccessTokenKsMerchantClient;
|
||||
|
||||
public class AccessTokenKsMerchantClientDemo {
|
||||
|
||||
public static void main(String[] args) {
|
||||
String url = "https://openapi.kwaixiaodian.com";
|
||||
String appKey = "your appKey";
|
||||
String signSecret = "your app signSecret";
|
||||
String accessToken = "your accessToken";
|
||||
|
||||
AccessTokenKsMerchantClient client = new AccessTokenKsMerchantClient(url,appKey,signSecret);
|
||||
|
||||
OpenSellerOrderRefundApproveRequest request = new OpenSellerOrderRefundApproveRequest();
|
||||
request.setAccessToken(accessToken);
|
||||
request.setApiMethodVersion(1L);
|
||||
|
||||
request.setRefundId(129230087);
|
||||
request.setDesc("说明");
|
||||
request.setRefundAmount(1000);
|
||||
request.setStatus(1);
|
||||
request.setNegotiateStatus(1);
|
||||
request.setRefundHandingWay(1);
|
||||
|
||||
OpenSellerOrderRefundApproveResponse response = client.execute(request);
|
||||
|
||||
System.out.println(GsonUtils.toJSON(response));
|
||||
}
|
||||
}
|
||||
|
||||
响应示例
|
||||
JSON
|
||||
{
|
||||
"result": 1,
|
||||
"error_msg": "SUCCESS"
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
open.seller.order.refund.disagree.refund
|
||||
需商家授权
|
||||
POST
|
||||
商家不同意退款
|
||||
更新时间: 2021-12-21 11:35:17
|
||||
商家不同意退款
|
||||
系统环境
|
||||
环境 域名
|
||||
线上环境(推荐) https://openapi.kwaixiaodian.com
|
||||
线上环境(备用) https://open.kwaixiaodian.com
|
||||
系统参数
|
||||
参数名 类型 必须 详情描述
|
||||
appkey String 是 平台分配的appId
|
||||
timestamp Number 是 发起请求的Unix时间戳,单位为毫秒
|
||||
access_token String 是 访问token,所有需用户授权API使用code模式获取,不需要用户授权API使用client_credentials获取,详情参考《授权说明》文档
|
||||
version Number 是 请求的API版本号,目前版本为1
|
||||
param JSON 是 业务参数,详见下方的请求入参
|
||||
method String 是 请求的API英文名,详见各API定义
|
||||
sign String 是 API入参的签名结果参数
|
||||
signMethod String 是 签名算法,支持HMAC_SHA256和MD5,推荐使用HMAC_SHA256
|
||||
请求参数
|
||||
参数名 类型 必须 示例值 详情描述
|
||||
refundId Long 是 129230087 退款单id
|
||||
sellerDisagreeReason Integer 是 1 商家拒绝原因,枚举:[1 商品退回后才能退款] [2 家已签收] [3 买家未举证/举证无效] [4 已发货,请买家承担运费] [5 已履行约定"] [100 其他]
|
||||
sellerDisagreeDesc String 是 不同意 商家拒绝原因说明
|
||||
sellerDisagreeImages List<String> 否 [] 拒绝图片,最多6张
|
||||
status Integer 是 10 退款单当前状态,枚举:[10, "买家仅退款申请"] [11, "买家退货退款申请"] [20, "平台介入-买家仅退款申请"] [21, "平台介入-买家退货退款申请"] [22, "平台介入-已确认退货退款"] [30, "商品回寄信息待买家更新"] [40, "商品回寄信息待卖家确认"] [50, "退款执行中"] [60, "退款成功"] [70, "退款失败"]
|
||||
negotiateStatus Integer 是 1 协商状态(以退款单信息里返回值为入参),枚举:[1,"待商家处理"] [3, "商家驳回,等待买家修改"]
|
||||
返回参数
|
||||
参数名 类型 示例值 详情描述
|
||||
code String 1 主返回码
|
||||
msg String success 主返回信息
|
||||
sub_code String 1 子返回码
|
||||
sub_msg String SUCCESS 子返回信息
|
||||
result Integer 1 返回码,1正确,其他返回码表示接口未执行成功
|
||||
error_msg String SUCCESS 返回码描述
|
||||
错误码
|
||||
错误码 错误类型 错误描述 解决方法
|
||||
暂无数据
|
||||
请求示例
|
||||
JAVA
|
||||
CURL
|
||||
package com.kuaishou.merchant.open.api.sdk.demo;
|
||||
import com.kuaishou.merchant.open.api.common.utils.GsonUtils;
|
||||
import com.kuaishou.merchant.open.api.request.refund.OpenSellerOrderRefundDisagreeRefundRequest;
|
||||
import com.kuaishou.merchant.open.api.response.refund.OpenSellerOrderRefundDisagreeRefundResponse;
|
||||
import com.kuaishou.merchant.open.api.client.AccessTokenKsMerchantClient;
|
||||
|
||||
public class AccessTokenKsMerchantClientDemo {
|
||||
|
||||
public static void main(String[] args) {
|
||||
String url = "https://openapi.kwaixiaodian.com";
|
||||
String appKey = "your appKey";
|
||||
String signSecret = "your app signSecret";
|
||||
String accessToken = "your accessToken";
|
||||
|
||||
AccessTokenKsMerchantClient client = new AccessTokenKsMerchantClient(url,appKey,signSecret);
|
||||
|
||||
OpenSellerOrderRefundDisagreeRefundRequest request = new OpenSellerOrderRefundDisagreeRefundRequest();
|
||||
request.setAccessToken(accessToken);
|
||||
request.setApiMethodVersion(1L);
|
||||
|
||||
request.setRefundId(129230087);
|
||||
request.setSellerDisagreeReason(1);
|
||||
request.setSellerDisagreeDesc("不同意");
|
||||
List<String> list1 = new ArrayList<>();
|
||||
list1.add("");
|
||||
request.setSellerDisagreeImages(list1);
|
||||
request.setStatus(10);
|
||||
request.setNegotiateStatus(1);
|
||||
|
||||
OpenSellerOrderRefundDisagreeRefundResponse response = client.execute(request);
|
||||
|
||||
System.out.println(GsonUtils.toJSON(response));
|
||||
}
|
||||
}
|
||||
|
||||
响应示例
|
||||
JSON
|
||||
{
|
||||
"result": 1,
|
||||
"error_msg": "SUCCESS"
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
integration.callback.virtual.eticket.reverse
|
||||
需用户授权
|
||||
POST
|
||||
电子凭证冲正回调接口
|
||||
更新时间: 2022-05-11 16:25:25
|
||||
电子凭证冲正回调,后续再开放主动通知冲正接口
|
||||
系统环境
|
||||
环境 域名
|
||||
线上环境(推荐) https://openapi.kwaixiaodian.com
|
||||
线上环境(备用) https://open.kwaixiaodian.com
|
||||
系统参数
|
||||
参数名 类型 必须 详情描述
|
||||
appkey String 是 平台分配的appId
|
||||
timestamp Number 是 发起请求的Unix时间戳,单位为毫秒
|
||||
access_token String 是 访问token,所有需用户授权API使用code模式获取,不需要用户授权API使用client_credentials获取,详情参考《授权说明》文档
|
||||
version Number 是 请求的API版本号,目前版本为1
|
||||
param JSON 是 业务参数,详见下方的请求入参
|
||||
method String 是 请求的API英文名,详见各API定义
|
||||
sign String 是 API入参的签名结果参数
|
||||
signMethod String 是 签名算法,支持HMAC_SHA256和MD5,推荐使用HMAC_SHA256
|
||||
请求参数
|
||||
参数名 类型 必须 示例值 详情描述
|
||||
oid String 是 21111 订单id
|
||||
eticketType String 否 DINING_OPEN_TICKET/DEFAULT_MEDICAL_TICKET 电子凭证类型,跟商家想要入驻的类目相关联,非必传,默认DINING_OPEN_TICKET
|
||||
etickets List<ReverseETicket> 是 [] 冲正的卡券列表
|
||||
serialNum String 否 fffassaass 核销时的序列号,冲正时必填!
|
||||
reason String 否 消费者投诉 冲正原因
|
||||
ext String 否 {} 扩展信息
|
||||
token String 否 fasddda 鉴权Token,订单操作维度
|
||||
返回参数
|
||||
参数名 类型 示例值 详情描述
|
||||
code String 1 主返回码
|
||||
msg String success 主返回信息
|
||||
sub_code String 1 子返回码
|
||||
sub_msg String "success" 子返回信息
|
||||
commonResult KwaishopPlatformDigitalBaseRespInfo {} 返回码,1成功
|
||||
result Integer 1 返回码,1成功
|
||||
error_msg String "success" 错误信息
|
||||
错误码
|
||||
错误码 错误类型 错误描述 解决方法
|
||||
暂无数据
|
||||
请求示例
|
||||
JAVA
|
||||
CURL
|
||||
package com.kuaishou.merchant.open.api.sdk.demo;
|
||||
import com.kuaishou.merchant.open.api.common.utils.GsonUtils;
|
||||
import com.kuaishou.merchant.open.api.request.virtual.IntegrationCallbackVirtualEticketReverseRequest;
|
||||
import com.kuaishou.merchant.open.api.response.virtual.IntegrationCallbackVirtualEticketReverseResponse;
|
||||
import com.kuaishou.merchant.open.api.client.AccessTokenKsMerchantClient;
|
||||
|
||||
public class AccessTokenKsMerchantClientDemo {
|
||||
|
||||
public static void main(String[] args) {
|
||||
String url = "https://openapi.kwaixiaodian.com";
|
||||
String appKey = "your appKey";
|
||||
String signSecret = "your app signSecret";
|
||||
String accessToken = "your accessToken";
|
||||
|
||||
AccessTokenKsMerchantClient client = new AccessTokenKsMerchantClient(url,appKey,signSecret);
|
||||
|
||||
IntegrationCallbackVirtualEticketReverseRequest request = new IntegrationCallbackVirtualEticketReverseRequest();
|
||||
request.setAccessToken(accessToken);
|
||||
request.setApiMethodVersion(1L);
|
||||
|
||||
request.setOid("21111");
|
||||
request.setEticketType("KFC");
|
||||
List<ReverseETicket> list1 = new ArrayList<>();
|
||||
ReverseETicket obj1 = new ReverseETicket();
|
||||
obj1.setId("123");
|
||||
list1.add(obj1);
|
||||
request.setEtickets(list1);
|
||||
request.setSerialNum("fffassaass");
|
||||
request.setReason("消费者投诉");
|
||||
request.setExt("{}");
|
||||
request.setToken("fasddda");
|
||||
|
||||
IntegrationCallbackVirtualEticketReverseResponse response = client.execute(request);
|
||||
|
||||
System.out.println(GsonUtils.toJSON(response));
|
||||
}
|
||||
}
|
||||
|
||||
响应示例
|
||||
JSON
|
||||
{
|
||||
"commonResult": {
|
||||
"code": 1,
|
||||
"message": "success"
|
||||
},
|
||||
"result": 1,
|
||||
"error_msg": "\"success\""
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
open.virtual.eticket.checkavailable
|
||||
需用户授权
|
||||
POST
|
||||
查询电子凭证校验结果
|
||||
更新时间: 2023-11-01 20:02:42
|
||||
检查电子凭证是否有效
|
||||
系统环境
|
||||
环境 域名
|
||||
线上环境(推荐) https://openapi.kwaixiaodian.com
|
||||
线上环境(备用) https://open.kwaixiaodian.com
|
||||
系统参数
|
||||
参数名 类型 必须 详情描述
|
||||
appkey String 是 平台分配的appId
|
||||
timestamp Number 是 发起请求的Unix时间戳,单位为毫秒
|
||||
access_token String 是 访问token,所有需用户授权API使用code模式获取,不需要用户授权API使用client_credentials获取,详情参考《授权说明》文档
|
||||
version Number 是 请求的API版本号,目前版本为1
|
||||
param JSON 是 业务参数,详见下方的请求入参
|
||||
method String 是 请求的API英文名,详见各API定义
|
||||
sign String 是 API入参的签名结果参数
|
||||
signMethod String 是 签名算法,支持HMAC_SHA256和MD5,推荐使用HMAC_SHA256
|
||||
请求参数
|
||||
参数名 类型 必须 示例值 详情描述
|
||||
buyerId Long 否 211 买家编号
|
||||
eticketType String 是 22 电子凭证类型
|
||||
etickets List<AvailableEticket> 是 [] 电子凭证列表
|
||||
orderId Long 否 1 订单编号
|
||||
sellerId Long 是 211 卖家编号
|
||||
返回参数
|
||||
参数名 类型 示例值 详情描述
|
||||
code String 1 主返回码
|
||||
msg String success 主返回信息
|
||||
sub_code String 1 子返回码
|
||||
sub_msg String 1 子返回信息
|
||||
etickets List<AvailableEticketDetail> [] 电子凭证校验结果
|
||||
result Integer 1 是否成功
|
||||
error_msg String 1 错误信息
|
||||
错误码
|
||||
错误码 错误类型 错误描述 解决方法
|
||||
3009 业务错误 电子凭证核销时,可用数量和平台不一致,其中一部分券不可用 电子凭证核销时,可用数量和平台不一致,其中一部分券不可用
|
||||
请求示例
|
||||
JAVA
|
||||
CURL
|
||||
package com.kuaishou.merchant.open.api.sdk.demo;
|
||||
import com.kuaishou.merchant.open.api.common.utils.GsonUtils;
|
||||
import com.kuaishou.merchant.open.api.request.industry.OpenVirtualEticketCheckavailableRequest;
|
||||
import com.kuaishou.merchant.open.api.response.industry.OpenVirtualEticketCheckavailableResponse;
|
||||
import com.kuaishou.merchant.open.api.client.AccessTokenKsMerchantClient;
|
||||
|
||||
public class AccessTokenKsMerchantClientDemo {
|
||||
|
||||
public static void main(String[] args) {
|
||||
String url = "https://gw-merchant-staging.test.gifshow.com";
|
||||
String appKey = "your appKey";
|
||||
String signSecret = "your app signSecret";
|
||||
String accessToken = "your accessToken";
|
||||
|
||||
AccessTokenKsMerchantClient client = new AccessTokenKsMerchantClient(url,appKey,signSecret);
|
||||
|
||||
OpenVirtualEticketCheckavailableRequest request = new OpenVirtualEticketCheckavailableRequest();
|
||||
request.setAccessToken(accessToken);
|
||||
request.setApiMethodVersion(1L);
|
||||
|
||||
request.setSellerId(211);
|
||||
request.setBuyerId(211);
|
||||
request.setOrderId(112);
|
||||
request.setBizTypeCode("22");
|
||||
List<AvailableETicketReq> list1 = new ArrayList<>();
|
||||
AvailableETicketReq obj1 = new AvailableETicketReq();
|
||||
obj1.setId("111");
|
||||
obj1.setCode("112");
|
||||
obj1.setNum(2);
|
||||
list1.add(obj1);
|
||||
request.setETicketList(list1);
|
||||
|
||||
OpenVirtualEticketCheckavailableResponse response = client.execute(request);
|
||||
|
||||
System.out.println(GsonUtils.toJSON(response));
|
||||
}
|
||||
}
|
||||
|
||||
响应示例
|
||||
JSON
|
||||
{
|
||||
"result": 1,
|
||||
"error_msg": "success",
|
||||
"data": {
|
||||
"sellerId": 1,
|
||||
"buyerId": 1,
|
||||
"orderId": 1,
|
||||
"eticketType": "2"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user