移除快手小店以及 api
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
快手轻量后端服务。
|
快手轻量后端服务。
|
||||||
|
|
||||||
后端负责订单入库、履约任务编排、kuaishou-lewan / 91 卡券 / 快手小店核销配置、后台管理接口和数据库迁移。默认入口就是快手轻量后端。
|
后端负责订单入库、履约任务编排、kuaishou-lewan / 91 卡券 / 行业电子凭证配置、后台管理接口和数据库迁移。默认入口就是快手轻量后端。
|
||||||
|
|
||||||
## 快速启动
|
## 快速启动
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"baseUrl": "https://s.kwaixiaodian.com",
|
|
||||||
"shops": [
|
|
||||||
{
|
|
||||||
"shopId": "",
|
|
||||||
"kshopName": "",
|
|
||||||
"cookie": "",
|
|
||||||
"userAvatar": "",
|
|
||||||
"enabled": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ import { requireAdminRoles } from "./session.js";
|
|||||||
import cloudtentaclesRouter from "./platform-config/cloudtentacles.js";
|
import cloudtentaclesRouter from "./platform-config/cloudtentacles.js";
|
||||||
import kuaishouIndustryRouter from "./platform-config/kuaishou-industry.js";
|
import kuaishouIndustryRouter from "./platform-config/kuaishou-industry.js";
|
||||||
import kuaishouFeifeiRouter from "./platform-config/kuaishou-feifei.js";
|
import kuaishouFeifeiRouter from "./platform-config/kuaishou-feifei.js";
|
||||||
import kuaishouEticketRouter from "./platform-config/kuaishou-eticket.js";
|
|
||||||
import ninetyoneRouter from "./platform-config/ninetyone.js";
|
import ninetyoneRouter from "./platform-config/ninetyone.js";
|
||||||
import notificationsRouter from "./platform-config/notifications.js";
|
import notificationsRouter from "./platform-config/notifications.js";
|
||||||
import fulfillmentRoutingRouter from "./platform-config/fulfillment-routing.js";
|
import fulfillmentRoutingRouter from "./platform-config/fulfillment-routing.js";
|
||||||
@@ -14,7 +13,6 @@ const router = Router();
|
|||||||
router.use("/platform-config", requireAdminRoles(["admin"]));
|
router.use("/platform-config", requireAdminRoles(["admin"]));
|
||||||
router.use("/platform-config", notificationsRouter);
|
router.use("/platform-config", notificationsRouter);
|
||||||
router.use("/platform-config", kuaishouIndustryRouter);
|
router.use("/platform-config", kuaishouIndustryRouter);
|
||||||
router.use("/platform-config", kuaishouEticketRouter);
|
|
||||||
router.use("/platform-config", kuaishouFeifeiRouter);
|
router.use("/platform-config", kuaishouFeifeiRouter);
|
||||||
router.use("/platform-config", ninetyoneRouter);
|
router.use("/platform-config", ninetyoneRouter);
|
||||||
router.use("/platform-config", fulfillmentRoutingRouter);
|
router.use("/platform-config", fulfillmentRoutingRouter);
|
||||||
|
|||||||
@@ -1,153 +0,0 @@
|
|||||||
import { Router } from "express";
|
|
||||||
|
|
||||||
import {
|
|
||||||
consumeAdminKuaishouEticket,
|
|
||||||
getAdminKuaishouEticketSourceConfig,
|
|
||||||
queryAdminKuaishouEticketDetail,
|
|
||||||
queryAdminKuaishouEticketShopInfo,
|
|
||||||
updateAdminKuaishouEticketSourceConfig,
|
|
||||||
} from "../../../services/admin/platform-config/kuaishou-eticket-service.js";
|
|
||||||
import type {
|
|
||||||
AdminKuaishouEticketConsumeRouteBody,
|
|
||||||
AdminKuaishouEticketDetailQueryRouteBody,
|
|
||||||
AdminKuaishouEticketShopInfoRouteBody,
|
|
||||||
AdminKuaishouEticketSourceConfigRouteBody,
|
|
||||||
} from "../../../types/admin/route-inputs.js";
|
|
||||||
import { createJsonHandler } from "../session.js";
|
|
||||||
import type { JsonRecord } from "../../../types/json.js";
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
router.get(
|
|
||||||
"/kuaishou-eticket-source",
|
|
||||||
createJsonHandler(() => getAdminKuaishouEticketSourceConfig(), {
|
|
||||||
successMessage: "ok",
|
|
||||||
errorMessage: "读取快手小店核销配置失败",
|
|
||||||
scope: "[admin/platform-config/kuaishou-eticket-source]",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
"/kuaishou-eticket-source",
|
|
||||||
createJsonHandler(
|
|
||||||
(req) =>
|
|
||||||
updateAdminKuaishouEticketSourceConfig(
|
|
||||||
req.body as AdminKuaishouEticketSourceConfigRouteBody
|
|
||||||
),
|
|
||||||
{
|
|
||||||
successMessage: "快手小店核销配置已保存",
|
|
||||||
errorMessage: "保存快手小店核销配置失败",
|
|
||||||
scope: "[admin/platform-config/kuaishou-eticket-source]",
|
|
||||||
audit: (_req, data) => {
|
|
||||||
const result = data as JsonRecord;
|
|
||||||
return {
|
|
||||||
action: "platform_kuaishou_eticket_source_updated",
|
|
||||||
targetType: "platform_config",
|
|
||||||
targetId: "kuaishou_eticket_source",
|
|
||||||
data: {
|
|
||||||
filePath: String(result.filePath || "").trim(),
|
|
||||||
enabled: Boolean(result.source?.enabled),
|
|
||||||
shopCount: Array.isArray(result.source?.shops)
|
|
||||||
? result.source.shops.length
|
|
||||||
: 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
"/kuaishou-eticket/query-detail",
|
|
||||||
createJsonHandler(
|
|
||||||
(req) =>
|
|
||||||
queryAdminKuaishouEticketDetail(
|
|
||||||
req.body as AdminKuaishouEticketDetailQueryRouteBody
|
|
||||||
),
|
|
||||||
{
|
|
||||||
successMessage: "快手小店核销信息查询成功",
|
|
||||||
errorMessage: "查询快手小店核销信息失败",
|
|
||||||
scope: "[admin/platform-config/kuaishou-eticket/query-detail]",
|
|
||||||
audit: (req, data) => {
|
|
||||||
const body = req.body as AdminKuaishouEticketDetailQueryRouteBody;
|
|
||||||
const result = data as JsonRecord;
|
|
||||||
return {
|
|
||||||
action: "platform_kuaishou_eticket_query_detail",
|
|
||||||
targetType: "platform_config",
|
|
||||||
targetId:
|
|
||||||
String(result.eTicketId || body.eTicketId || "").trim() ||
|
|
||||||
"kuaishou_eticket",
|
|
||||||
data: {
|
|
||||||
result: Number(result.result || 0),
|
|
||||||
ok: Boolean(result.ok),
|
|
||||||
alreadyConsumed: Boolean(result.alreadyConsumed),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
"/kuaishou-eticket/query-shop-info",
|
|
||||||
createJsonHandler(
|
|
||||||
(req) =>
|
|
||||||
queryAdminKuaishouEticketShopInfo(
|
|
||||||
req.body as AdminKuaishouEticketShopInfoRouteBody
|
|
||||||
),
|
|
||||||
{
|
|
||||||
successMessage: "快手小店店铺信息查询成功",
|
|
||||||
errorMessage: "查询快手小店店铺信息失败",
|
|
||||||
scope: "[admin/platform-config/kuaishou-eticket/query-shop-info]",
|
|
||||||
audit: (req, data) => {
|
|
||||||
const body = req.body as AdminKuaishouEticketShopInfoRouteBody;
|
|
||||||
const result = data as JsonRecord;
|
|
||||||
return {
|
|
||||||
action: "platform_kuaishou_eticket_query_shop_info",
|
|
||||||
targetType: "platform_config",
|
|
||||||
targetId:
|
|
||||||
String(result.shop?.shopId || body.shopId || "").trim() ||
|
|
||||||
"kuaishou_eticket_shop_info",
|
|
||||||
data: {
|
|
||||||
result: Number(result.result || 0),
|
|
||||||
ok: Boolean(result.ok),
|
|
||||||
kshopName: String(result.shop?.kshopName || "").trim(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
router.post(
|
|
||||||
"/kuaishou-eticket/consume",
|
|
||||||
createJsonHandler(
|
|
||||||
(req) =>
|
|
||||||
consumeAdminKuaishouEticket(
|
|
||||||
req.body as AdminKuaishouEticketConsumeRouteBody
|
|
||||||
),
|
|
||||||
{
|
|
||||||
successMessage: "快手小店核销请求已执行",
|
|
||||||
errorMessage: "执行快手小店核销失败",
|
|
||||||
scope: "[admin/platform-config/kuaishou-eticket/consume]",
|
|
||||||
audit: (req, data) => {
|
|
||||||
const body = req.body as AdminKuaishouEticketConsumeRouteBody;
|
|
||||||
const result = data as JsonRecord;
|
|
||||||
return {
|
|
||||||
action: "platform_kuaishou_eticket_consume",
|
|
||||||
targetType: "platform_config",
|
|
||||||
targetId:
|
|
||||||
String(result.eTicketId || body.eTicketId || "").trim() ||
|
|
||||||
"kuaishou_eticket",
|
|
||||||
data: {
|
|
||||||
result: Number(result.result || 0),
|
|
||||||
consumed: Boolean(result.consumed),
|
|
||||||
alreadyConsumed: Boolean(result.alreadyConsumed),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
} from '../../services/admin/write/kuaishou-cloud-actions.js'
|
} from '../../services/admin/write/kuaishou-cloud-actions.js'
|
||||||
import { createJsonHandler, requireAdminRoles } from './session.js'
|
import { createJsonHandler, requireAdminRoles } from './session.js'
|
||||||
import type {
|
import type {
|
||||||
AdminTaskKuaishouCloudDispatchRouteBody,
|
|
||||||
AdminTaskKuaishouIndustryConsumeRouteBody,
|
AdminTaskKuaishouIndustryConsumeRouteBody,
|
||||||
AdminTaskRouteParams,
|
AdminTaskRouteParams,
|
||||||
AdminTaskRouteQuery,
|
AdminTaskRouteQuery,
|
||||||
@@ -108,7 +107,6 @@ router.post(
|
|||||||
(req) =>
|
(req) =>
|
||||||
dispatchAdminTaskKuaishouCloudFulfillment(
|
dispatchAdminTaskKuaishouCloudFulfillment(
|
||||||
getTaskId(req),
|
getTaskId(req),
|
||||||
req.body as AdminTaskKuaishouCloudDispatchRouteBody,
|
|
||||||
req.adminSession || null,
|
req.adminSession || null,
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
@@ -117,7 +115,6 @@ router.post(
|
|||||||
scope: '[admin/tasks/:taskId/kuaishou-cloud/dispatch]',
|
scope: '[admin/tasks/:taskId/kuaishou-cloud/dispatch]',
|
||||||
audit: (req, data) => {
|
audit: (req, data) => {
|
||||||
const result = data as AdminTaskActionResponse
|
const result = data as AdminTaskActionResponse
|
||||||
const body = req.body as AdminTaskKuaishouCloudDispatchRouteBody
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
action: 'task_kuaishou_cloud_dispatch',
|
action: 'task_kuaishou_cloud_dispatch',
|
||||||
@@ -128,7 +125,6 @@ router.post(
|
|||||||
taskNo: result.task.taskNo,
|
taskNo: result.task.taskNo,
|
||||||
status: result.task.status,
|
status: result.task.status,
|
||||||
deliveryStatus: result.task.deliveryStatus,
|
deliveryStatus: result.task.deliveryStatus,
|
||||||
ticketCodeProvided: Boolean(String(body.ticketCode || '').trim()),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,12 +4,10 @@ import { createRateLimitMiddleware, getParamRateLimitKey } from '../middleware/r
|
|||||||
import {
|
import {
|
||||||
confirmKuaishouCloudClaimRole,
|
confirmKuaishouCloudClaimRole,
|
||||||
getKuaishouCloudClaimDetail,
|
getKuaishouCloudClaimDetail,
|
||||||
getKuaishouCloudClaimGuideAssetPath,
|
|
||||||
rebindKuaishouCloudClaimRole,
|
rebindKuaishouCloudClaimRole,
|
||||||
redeemKuaishouCloudClaim,
|
redeemKuaishouCloudClaim,
|
||||||
verifyKuaishouCloudClaimTicket,
|
|
||||||
} from '../services/claim/kuaishou-cloud-claim-service.js'
|
} from '../services/claim/kuaishou-cloud-claim-service.js'
|
||||||
import { buildNotFoundPayload, createRouteFileHandler, createRouteHandler } from '../utils/http.js'
|
import { buildNotFoundPayload, createRouteHandler } from '../utils/http.js'
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
const claimReadRateLimit = createRateLimitMiddleware({
|
const claimReadRateLimit = createRateLimitMiddleware({
|
||||||
@@ -34,16 +32,6 @@ router.get(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
router.post(
|
|
||||||
'/:token/kuaishou-cloud/verify-ticket',
|
|
||||||
claimWriteRateLimit,
|
|
||||||
createRouteHandler((req) => verifyKuaishouCloudClaimTicket(req.params.token, req.body), {
|
|
||||||
successMessage: '核销码提交并核销成功',
|
|
||||||
errorMessage: '提交核销码失败',
|
|
||||||
scope: '[claims/:token/kuaishou-cloud/verify-ticket]',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/:token/kuaishou-cloud/confirm-role',
|
'/:token/kuaishou-cloud/confirm-role',
|
||||||
claimWriteRateLimit,
|
claimWriteRateLimit,
|
||||||
@@ -74,14 +62,6 @@ router.post(
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
router.get(
|
|
||||||
'/assets/kuaishou-cloud/:filename',
|
|
||||||
createRouteFileHandler((req) => getKuaishouCloudClaimGuideAssetPath(req.params.filename), {
|
|
||||||
errorMessage: '读取指引图片失败',
|
|
||||||
scope: '[claims/assets/kuaishou-cloud/:filename]',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
router.use((req, res) => {
|
router.use((req, res) => {
|
||||||
res.status(404).json(buildNotFoundPayload(req))
|
res.status(404).json(buildNotFoundPayload(req))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
isPlainObject,
|
isPlainObject,
|
||||||
pickFirstNonEmpty,
|
pickFirstNonEmpty,
|
||||||
resolveCloudtentaclesAdminContext,
|
resolveCloudtentaclesAdminContext,
|
||||||
resolveKuaishouEticketAdminContext,
|
|
||||||
} from './context.js'
|
} from './context.js'
|
||||||
|
|
||||||
test('pickFirstNonEmpty returns first trimmed non-empty string value', () => {
|
test('pickFirstNonEmpty returns first trimmed non-empty string value', () => {
|
||||||
@@ -21,52 +20,6 @@ test('isPlainObject only accepts non-array objects', () => {
|
|||||||
assert.equal(isPlainObject('x'), false)
|
assert.equal(isPlainObject('x'), false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('resolveKuaishouEticketAdminContext prefers explicit payload values', () => {
|
|
||||||
const context = resolveKuaishouEticketAdminContext(
|
|
||||||
{
|
|
||||||
baseUrl: ' https://override.example.com ',
|
|
||||||
shopId: ' shop-2 ',
|
|
||||||
cookie: ' cookie-2 ',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
savedSource: { baseUrl: 'https://saved.example.com' },
|
|
||||||
findShopConfig(requestedShopId) {
|
|
||||||
assert.equal(requestedShopId, 'shop-2')
|
|
||||||
return { shopId: 'shop-2', cookie: 'saved-cookie-2' }
|
|
||||||
},
|
|
||||||
getFirstAvailableShop() {
|
|
||||||
throw new Error('should not use first available shop')
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.deepEqual(context, {
|
|
||||||
baseUrl: 'https://override.example.com',
|
|
||||||
cookie: 'cookie-2',
|
|
||||||
shopId: 'shop-2',
|
|
||||||
shop: { shopId: 'shop-2', cookie: 'saved-cookie-2' },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('resolveKuaishouEticketAdminContext falls back to first available shop', () => {
|
|
||||||
const context = resolveKuaishouEticketAdminContext(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
savedSource: { baseUrl: ' https://saved.example.com ' },
|
|
||||||
getFirstAvailableShop() {
|
|
||||||
return { shopId: 'shop-1', cookie: 'cookie-1' }
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.deepEqual(context, {
|
|
||||||
baseUrl: 'https://saved.example.com',
|
|
||||||
cookie: 'cookie-1',
|
|
||||||
shopId: 'shop-1',
|
|
||||||
shop: { shopId: 'shop-1', cookie: 'cookie-1' },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('resolveCloudtentaclesAdminContext merges payload source and persisted session', () => {
|
test('resolveCloudtentaclesAdminContext merges payload source and persisted session', () => {
|
||||||
const context = resolveCloudtentaclesAdminContext(
|
const context = resolveCloudtentaclesAdminContext(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,32 +15,6 @@ export function isPlainObject(value: unknown): value is JsonObject {
|
|||||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveKuaishouEticketAdminContext(payload: JsonObject = {}, options: JsonObject = {}) {
|
|
||||||
const savedSource = options.savedSource || {};
|
|
||||||
const findShopConfig =
|
|
||||||
(options.findShopConfig || (() => null)) as (shopId: string, source: JsonObject) => unknown;
|
|
||||||
const getFirstAvailableShop =
|
|
||||||
(options.getFirstAvailableShop || (() => null)) as (source: JsonObject) => unknown;
|
|
||||||
const defaultBaseUrl = String(
|
|
||||||
options.defaultBaseUrl || "https://s.kwaixiaodian.com"
|
|
||||||
).trim();
|
|
||||||
const requestedShopId = String(payload.shopId || "").trim();
|
|
||||||
const configuredShop = (requestedShopId
|
|
||||||
? findShopConfig(requestedShopId, savedSource)
|
|
||||||
: getFirstAvailableShop(savedSource)) as JsonObject | null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
baseUrl: pickFirstNonEmpty([
|
|
||||||
payload.baseUrl,
|
|
||||||
savedSource.baseUrl,
|
|
||||||
defaultBaseUrl,
|
|
||||||
]),
|
|
||||||
cookie: pickFirstNonEmpty([payload.cookie, configuredShop?.cookie]),
|
|
||||||
shopId: pickFirstNonEmpty([requestedShopId, configuredShop?.shopId]),
|
|
||||||
shop: configuredShop,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
import { getCloudtentaclesSourceByKey } from "../../../platforms/cloudtentacles/source-config-service.js";
|
import { getCloudtentaclesSourceByKey } from "../../../platforms/cloudtentacles/source-config-service.js";
|
||||||
import { getCloudtentaclesSessionStateByKey } from "../../../platforms/cloudtentacles/session-state-service.js";
|
import { getCloudtentaclesSessionStateByKey } from "../../../platforms/cloudtentacles/session-state-service.js";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import assert from 'node:assert/strict'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
mapAdminCloudtentaclesSession,
|
mapAdminCloudtentaclesSession,
|
||||||
mapAdminKuaishouEticketSourceConfig,
|
|
||||||
maskPhone,
|
maskPhone,
|
||||||
maskSecret,
|
maskSecret,
|
||||||
} from './mappers.js'
|
} from './mappers.js'
|
||||||
@@ -20,30 +19,6 @@ test('maskPhone hides middle digits for mobile numbers', () => {
|
|||||||
assert.equal(maskPhone(''), '')
|
assert.equal(maskPhone(''), '')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('mapAdminKuaishouEticketSourceConfig maps shops and masks cookies', () => {
|
|
||||||
assert.deepEqual(
|
|
||||||
mapAdminKuaishouEticketSourceConfig(
|
|
||||||
{ enabled: true, baseUrl: ' https://s.kwaixiaodian.com ' },
|
|
||||||
[{ shopId: ' 1 ', kshopName: ' A店 ', cookie: 'cookie-abcdef123456', enabled: true }],
|
|
||||||
),
|
|
||||||
{
|
|
||||||
enabled: true,
|
|
||||||
baseUrl: 'https://s.kwaixiaodian.com',
|
|
||||||
shops: [
|
|
||||||
{
|
|
||||||
shopId: '1',
|
|
||||||
kshopName: 'A店',
|
|
||||||
cookie: 'cookie-abcdef123456',
|
|
||||||
cookieMasked: 'cookie****123456',
|
|
||||||
hasCookie: true,
|
|
||||||
userAvatar: '',
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('mapAdminCloudtentaclesSession derives masked fields and token presence', () => {
|
test('mapAdminCloudtentaclesSession derives masked fields and token presence', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
mapAdminCloudtentaclesSession({
|
mapAdminCloudtentaclesSession({
|
||||||
|
|||||||
@@ -17,26 +17,6 @@ export function maskPhone(value: unknown) {
|
|||||||
return maskPhoneValue(value, { maskShort: false });
|
return maskPhoneValue(value, { maskShort: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapAdminKuaishouEticketShopItem(item: JsonObject = {}) {
|
|
||||||
return {
|
|
||||||
shopId: String(item.shopId || "").trim(),
|
|
||||||
kshopName: String(item.kshopName || "").trim(),
|
|
||||||
cookie: String(item.cookie || "").trim(),
|
|
||||||
cookieMasked: maskSecret(item.cookie),
|
|
||||||
hasCookie: Boolean(String(item.cookie || "").trim()),
|
|
||||||
userAvatar: String(item.userAvatar || "").trim(),
|
|
||||||
enabled: item.enabled !== false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapAdminKuaishouEticketSourceConfig(config: JsonObject = {}, shops: JsonObject[] = []) {
|
|
||||||
return {
|
|
||||||
enabled: config.enabled !== false,
|
|
||||||
baseUrl: String(config.baseUrl || "").trim(),
|
|
||||||
shops: shops.map((item) => mapAdminKuaishouEticketShopItem(item)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapAdminCloudtentaclesSourceConfig(config: JsonObject = {}) {
|
export function mapAdminCloudtentaclesSourceConfig(config: JsonObject = {}) {
|
||||||
return {
|
return {
|
||||||
key: String(config.key || "").trim(),
|
key: String(config.key || "").trim(),
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
import {
|
|
||||||
consumeKuaishouEticket,
|
|
||||||
queryKuaishouEticketConsumeDetail,
|
|
||||||
} from '../../platforms/kuaishou-eticket/consume-service.js'
|
|
||||||
import {
|
|
||||||
findKuaishouEticketShopConfig,
|
|
||||||
getFirstAvailableKuaishouEticketShop,
|
|
||||||
getKuaishouEticketSourceConfig,
|
|
||||||
getKuaishouEticketSourceFilePath,
|
|
||||||
listKuaishouEticketShopConfigs,
|
|
||||||
saveKuaishouEticketSourceConfig,
|
|
||||||
} from '../../platforms/kuaishou-eticket/source-config-service.js'
|
|
||||||
import { queryKuaishouEticketStableInfo } from '../../platforms/kuaishou-eticket/info-service.js'
|
|
||||||
import { resolveKuaishouEticketAdminContext } from './cloudtentacles/context.js'
|
|
||||||
import { mapAdminKuaishouEticketSourceConfig } from './cloudtentacles/mappers.js'
|
|
||||||
|
|
||||||
type JsonObject = Record<string, any>
|
|
||||||
|
|
||||||
function resolveAdminKuaishouEticketContext(payload: JsonObject = {}) {
|
|
||||||
return resolveKuaishouEticketAdminContext(payload, {
|
|
||||||
savedSource: getKuaishouEticketSourceConfig(),
|
|
||||||
findShopConfig: findKuaishouEticketShopConfig,
|
|
||||||
getFirstAvailableShop: getFirstAvailableKuaishouEticketShop,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getAdminKuaishouEticketSourceConfig() {
|
|
||||||
const config = getKuaishouEticketSourceConfig()
|
|
||||||
|
|
||||||
return {
|
|
||||||
filePath: getKuaishouEticketSourceFilePath(),
|
|
||||||
source: mapAdminKuaishouEticketSourceConfig(config, listKuaishouEticketShopConfigs(config)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateAdminKuaishouEticketSourceConfig(payload: JsonObject = {}) {
|
|
||||||
const saved = saveKuaishouEticketSourceConfig({
|
|
||||||
enabled: payload.enabled !== false,
|
|
||||||
baseUrl: String(payload.baseUrl || '').trim() || 'https://s.kwaixiaodian.com',
|
|
||||||
shops: Array.isArray(payload.shops) ? payload.shops : [],
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
filePath: getKuaishouEticketSourceFilePath(),
|
|
||||||
source: mapAdminKuaishouEticketSourceConfig(saved, listKuaishouEticketShopConfigs(saved)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function queryAdminKuaishouEticketDetail(payload: JsonObject = {}) {
|
|
||||||
const context = resolveAdminKuaishouEticketContext(payload)
|
|
||||||
|
|
||||||
return queryKuaishouEticketConsumeDetail({
|
|
||||||
baseUrl: context.baseUrl,
|
|
||||||
cookie: context.cookie,
|
|
||||||
eTicketId: String(payload.eTicketId || '').trim(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function queryAdminKuaishouEticketShopInfo(payload: JsonObject = {}) {
|
|
||||||
const context = resolveAdminKuaishouEticketContext(payload)
|
|
||||||
|
|
||||||
return queryKuaishouEticketStableInfo({
|
|
||||||
baseUrl: context.baseUrl,
|
|
||||||
cookie: context.cookie,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function consumeAdminKuaishouEticket(payload: JsonObject = {}) {
|
|
||||||
const context = resolveAdminKuaishouEticketContext(payload)
|
|
||||||
|
|
||||||
return consumeKuaishouEticket({
|
|
||||||
baseUrl: context.baseUrl,
|
|
||||||
cookie: context.cookie,
|
|
||||||
eTicketId: String(payload.eTicketId || '').trim(),
|
|
||||||
oid: String(payload.oid || '').trim(),
|
|
||||||
formToken: String(payload.formToken || '').trim(),
|
|
||||||
num: payload.num,
|
|
||||||
storeId: String(payload.storeId || '').trim(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -17,18 +17,12 @@ import {
|
|||||||
backCloudtentaclesVirtualNumber,
|
backCloudtentaclesVirtualNumber,
|
||||||
getCloudtentaclesBindInfo,
|
getCloudtentaclesBindInfo,
|
||||||
} from '../../platforms/cloudtentacles/virtual-number-service.js'
|
} from '../../platforms/cloudtentacles/virtual-number-service.js'
|
||||||
import { consumeKuaishouEticket } from '../../platforms/kuaishou-eticket/consume-service.js'
|
|
||||||
import { isKuaishouEticketMockTicketCode } from '../../platforms/kuaishou-eticket/mock-ticket-service.js'
|
|
||||||
import { resendKuaishouIndustryVoucherSendCallback } from '../../platforms/kuaishou-industry/send-code-service.js'
|
import { resendKuaishouIndustryVoucherSendCallback } from '../../platforms/kuaishou-industry/send-code-service.js'
|
||||||
import { consumeKuaishouIndustryVoucher } from '../../platforms/kuaishou-industry/voucher-service.js'
|
import { consumeKuaishouIndustryVoucher } from '../../platforms/kuaishou-industry/voucher-service.js'
|
||||||
import {
|
import {
|
||||||
attachKuaishouIndustryVoucherToTask,
|
attachKuaishouIndustryVoucherToTask,
|
||||||
bindKuaishouIndustryVouchersToOrderTasks,
|
bindKuaishouIndustryVouchersToOrderTasks,
|
||||||
} from '../../platforms/kuaishou-industry/voucher-binding-service.js'
|
} from '../../platforms/kuaishou-industry/voucher-binding-service.js'
|
||||||
import {
|
|
||||||
getKuaishouEticketSourceConfig,
|
|
||||||
resolveKuaishouEticketShopConfig,
|
|
||||||
} from '../../platforms/kuaishou-eticket/source-config-service.js'
|
|
||||||
import { createHttpError } from '../../../utils/http.js'
|
import { createHttpError } from '../../../utils/http.js'
|
||||||
import { nowIso } from '../../../utils/time.js'
|
import { nowIso } from '../../../utils/time.js'
|
||||||
import { TASK_STATUS } from '../../../domain/task-status.js'
|
import { TASK_STATUS } from '../../../domain/task-status.js'
|
||||||
@@ -58,7 +52,6 @@ import type {
|
|||||||
AdminViewerSessionInput,
|
AdminViewerSessionInput,
|
||||||
} from '../../../types/admin/read-inputs.js'
|
} from '../../../types/admin/read-inputs.js'
|
||||||
import type {
|
import type {
|
||||||
AdminTaskKuaishouCloudDispatchInput,
|
|
||||||
AdminTaskKuaishouIndustryConsumeInput,
|
AdminTaskKuaishouIndustryConsumeInput,
|
||||||
} from '../../../types/admin/write-inputs.js'
|
} from '../../../types/admin/write-inputs.js'
|
||||||
import type { AdminTaskActionResponse } from '../../../types/admin/write-models.js'
|
import type { AdminTaskActionResponse } from '../../../types/admin/write-models.js'
|
||||||
@@ -246,7 +239,6 @@ export async function prepareAdminTaskKuaishouCloudFulfillment(
|
|||||||
|
|
||||||
export async function dispatchAdminTaskKuaishouCloudFulfillment(
|
export async function dispatchAdminTaskKuaishouCloudFulfillment(
|
||||||
taskId: AdminEntityIdInput,
|
taskId: AdminEntityIdInput,
|
||||||
payload: AdminTaskKuaishouCloudDispatchInput = {},
|
|
||||||
session: AdminViewerSessionInput | null = null,
|
session: AdminViewerSessionInput | null = null,
|
||||||
): Promise<AdminTaskActionResponse> {
|
): Promise<AdminTaskActionResponse> {
|
||||||
const task = await getRequiredTask(taskId)
|
const task = await getRequiredTask(taskId)
|
||||||
@@ -281,11 +273,15 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const ticketCode = String(payload.ticketCode || '').trim()
|
|
||||||
const persistedTicketCode = String(flow.ticket.code || '').trim()
|
const persistedTicketCode = String(flow.ticket.code || '').trim()
|
||||||
|
const voucherContext = normalizeAdminTaskIndustryVoucherContext(taskContext.kuaishouIndustryVoucher)
|
||||||
|
const industryVoucherCode = String(
|
||||||
|
voucherContext.voucherCode || voucherContext.eticketId || '',
|
||||||
|
).trim()
|
||||||
|
const resolvedTicketCode = persistedTicketCode || industryVoucherCode
|
||||||
|
|
||||||
if (!persistedTicketCode && !ticketCode) {
|
if (!resolvedTicketCode) {
|
||||||
throw createHttpError('客户还没有在领取页提交核销码,暂时不能直接发货', {
|
throw createHttpError('旧快手小店核销流程已停用,请改用行业电子凭证处理', {
|
||||||
statusCode: 409,
|
statusCode: 409,
|
||||||
errorCode: 'admin_task_kuaishou_cloud_missing_ticket_code',
|
errorCode: 'admin_task_kuaishou_cloud_missing_ticket_code',
|
||||||
})
|
})
|
||||||
@@ -322,16 +318,9 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(
|
|||||||
...syncedFlow,
|
...syncedFlow,
|
||||||
ticket: {
|
ticket: {
|
||||||
...syncedFlow.ticket,
|
...syncedFlow.ticket,
|
||||||
code: ticketCode || persistedTicketCode,
|
code: resolvedTicketCode,
|
||||||
capturedAt: ticketCode ? now : syncedFlow.ticket.capturedAt,
|
capturedAt: resolvedTicketCode ? (syncedFlow.ticket.capturedAt || now) : syncedFlow.ticket.capturedAt,
|
||||||
capturedBy:
|
capturedBy: syncedFlow.ticket.capturedBy,
|
||||||
ticketCode && session
|
|
||||||
? {
|
|
||||||
userId: Number(session.userId || 0) || 0,
|
|
||||||
username: String(session.username || '').trim(),
|
|
||||||
role: String(session.role || '').trim(),
|
|
||||||
}
|
|
||||||
: syncedFlow.ticket.capturedBy,
|
|
||||||
},
|
},
|
||||||
dispatch: {
|
dispatch: {
|
||||||
...syncedFlow.dispatch,
|
...syncedFlow.dispatch,
|
||||||
@@ -366,7 +355,7 @@ export async function dispatchAdminTaskKuaishouCloudFulfillment(
|
|||||||
task.id,
|
task.id,
|
||||||
'kuaishou_cloud_dispatched',
|
'kuaishou_cloud_dispatched',
|
||||||
{
|
{
|
||||||
ticketCodeMasked: maskCode(ticketCode || persistedTicketCode),
|
ticketCodeMasked: maskCode(resolvedTicketCode),
|
||||||
skuId: syncedFlow.binding.skuId,
|
skuId: syncedFlow.binding.skuId,
|
||||||
vnId: syncedFlow.binding.vnId,
|
vnId: syncedFlow.binding.vnId,
|
||||||
vnPhoneMasked: maskPhone(syncedFlow.binding.vnPhone),
|
vnPhoneMasked: maskPhone(syncedFlow.binding.vnPhone),
|
||||||
@@ -548,77 +537,18 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(
|
|||||||
id: flow.binding.vnId,
|
id: flow.binding.vnId,
|
||||||
})
|
})
|
||||||
|
|
||||||
const order = await getOrderById(task.order_id)
|
|
||||||
const ticketCode = String(flow.ticket.code || '').trim()
|
const ticketCode = String(flow.ticket.code || '').trim()
|
||||||
const shopId = String(flow.consume.shopId || order?.shop_id || '').trim()
|
|
||||||
const shopName = String(flow.consume.shopName || order?.shop_name || '').trim()
|
|
||||||
const mockTicketCode = isKuaishouEticketMockTicketCode(ticketCode)
|
|
||||||
const eticketSource = getKuaishouEticketSourceConfig()
|
|
||||||
const shopConfig = resolveKuaishouEticketShopConfig({
|
|
||||||
shopId,
|
|
||||||
shopName,
|
|
||||||
})
|
|
||||||
let consumeStatus = 'pending'
|
|
||||||
let consumeErrorMessage = ''
|
|
||||||
let consumedAt = null
|
|
||||||
let nextTaskStatus = 'completed'
|
|
||||||
let nextResultCode = 'kuaishou_cloud_completed'
|
|
||||||
let nextResultMessage = 'cloudtentacles 发货、退号并完成快手核销'
|
|
||||||
const consumeAlreadyCompleted = flow.consume.status === 'success'
|
const consumeAlreadyCompleted = flow.consume.status === 'success'
|
||||||
|
const consumeStatus = consumeAlreadyCompleted ? 'success' : 'skipped'
|
||||||
if (consumeAlreadyCompleted) {
|
let consumeErrorMessage = ''
|
||||||
consumeStatus = 'success'
|
const consumedAt = consumeAlreadyCompleted ? flow.consume.consumedAt || now : null
|
||||||
consumedAt = flow.consume.consumedAt || now
|
const nextTaskStatus = 'completed'
|
||||||
} else if (!order) {
|
const nextResultCode = consumeAlreadyCompleted
|
||||||
consumeStatus = 'failed'
|
? 'kuaishou_cloud_completed'
|
||||||
consumeErrorMessage = '任务关联订单不存在,无法执行快手核销'
|
: 'kuaishou_cloud_completed_without_eticket_consume'
|
||||||
} else if (!ticketCode) {
|
const nextResultMessage = consumeAlreadyCompleted
|
||||||
consumeStatus = 'failed'
|
? 'cloudtentacles 发货、退号并完成电子凭证核销'
|
||||||
consumeErrorMessage = '客户未提交有效核销码,无法执行快手核销'
|
: 'cloudtentacles 发货、退号并收口'
|
||||||
} else if (mockTicketCode) {
|
|
||||||
const consumeResult = await consumeKuaishouEticket({
|
|
||||||
eTicketId: ticketCode,
|
|
||||||
oid: String(flow.ticket.oid || '').trim(),
|
|
||||||
formToken: String(flow.ticket.formToken || '').trim(),
|
|
||||||
})
|
|
||||||
consumeStatus = consumeResult.consumed ? 'success' : 'failed'
|
|
||||||
consumedAt = consumeResult.consumed ? now : null
|
|
||||||
consumeErrorMessage = String(consumeResult.errorMessage || '').trim()
|
|
||||||
} else if (
|
|
||||||
!shopConfig ||
|
|
||||||
shopConfig.enabled === false ||
|
|
||||||
!String(shopConfig.cookie || '').trim()
|
|
||||||
) {
|
|
||||||
consumeStatus = 'failed'
|
|
||||||
consumeErrorMessage = '订单对应快手小店缺少可用 Cookie,无法执行快手核销'
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
const consumeResult = await consumeKuaishouEticket({
|
|
||||||
baseUrl: eticketSource.baseUrl,
|
|
||||||
cookie: shopConfig.cookie,
|
|
||||||
eTicketId: ticketCode,
|
|
||||||
oid: String(flow.ticket.oid || '').trim(),
|
|
||||||
formToken: String(flow.ticket.formToken || '').trim(),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (consumeResult.consumed) {
|
|
||||||
consumeStatus = 'success'
|
|
||||||
consumedAt = now
|
|
||||||
} else {
|
|
||||||
consumeStatus = 'failed'
|
|
||||||
consumeErrorMessage = String(consumeResult.errorMessage || '快手核销失败').trim()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
consumeStatus = 'failed'
|
|
||||||
consumeErrorMessage = error instanceof Error ? error.message : '快手核销失败'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (consumeStatus !== 'success') {
|
|
||||||
nextTaskStatus = 'manual_review'
|
|
||||||
nextResultCode = 'kuaishou_cloud_consume_failed'
|
|
||||||
nextResultMessage = consumeErrorMessage || '号码已退还,但快手核销未完成,请人工处理'
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextContext = {
|
const nextContext = {
|
||||||
...taskContext,
|
...taskContext,
|
||||||
@@ -639,8 +569,8 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(
|
|||||||
consume: {
|
consume: {
|
||||||
...flow.consume,
|
...flow.consume,
|
||||||
status: consumeStatus,
|
status: consumeStatus,
|
||||||
shopId: shopId || flow.consume.shopId,
|
shopId: flow.consume.shopId,
|
||||||
shopName,
|
shopName: flow.consume.shopName,
|
||||||
autoConsumeEnabled: flow.consume.autoConsumeEnabled === true,
|
autoConsumeEnabled: flow.consume.autoConsumeEnabled === true,
|
||||||
consumedAt,
|
consumedAt,
|
||||||
errorMessage: consumeErrorMessage,
|
errorMessage: consumeErrorMessage,
|
||||||
@@ -653,7 +583,7 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(
|
|||||||
delivery_status: 'delivered',
|
delivery_status: 'delivered',
|
||||||
result_code: nextResultCode,
|
result_code: nextResultCode,
|
||||||
result_message: nextResultMessage,
|
result_message: nextResultMessage,
|
||||||
redeemed_at: consumeStatus === 'success' ? now : task.redeemed_at,
|
redeemed_at: now,
|
||||||
last_error: consumeErrorMessage,
|
last_error: consumeErrorMessage,
|
||||||
context_json: JSON.stringify(nextContext),
|
context_json: JSON.stringify(nextContext),
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
@@ -669,18 +599,19 @@ export async function returnNumberAdminTaskKuaishouCloudFulfillment(
|
|||||||
now,
|
now,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const consumeEventType = consumeAlreadyCompleted
|
||||||
|
? 'kuaishou_cloud_consume_already_completed'
|
||||||
|
: 'kuaishou_cloud_consume_skipped'
|
||||||
|
|
||||||
await createTaskEvent(
|
await createTaskEvent(
|
||||||
task.id,
|
task.id,
|
||||||
consumeAlreadyCompleted
|
consumeEventType,
|
||||||
? 'kuaishou_cloud_consume_already_completed'
|
|
||||||
: consumeStatus === 'success'
|
|
||||||
? 'kuaishou_cloud_consumed'
|
|
||||||
: 'kuaishou_cloud_consume_failed',
|
|
||||||
{
|
{
|
||||||
ticketCodeMasked: maskCode(ticketCode),
|
ticketCodeMasked: maskCode(ticketCode),
|
||||||
shopId,
|
shopId: flow.consume.shopId,
|
||||||
shopName,
|
shopName: flow.consume.shopName,
|
||||||
consumeStatus,
|
consumeStatus,
|
||||||
|
consumeMode: 'legacy_writeoff_disabled',
|
||||||
errorMessage: consumeErrorMessage,
|
errorMessage: consumeErrorMessage,
|
||||||
},
|
},
|
||||||
now,
|
now,
|
||||||
@@ -961,7 +892,7 @@ async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise<Kuaisho
|
|||||||
}
|
}
|
||||||
|
|
||||||
const taskContext = parseTaskContext(task)
|
const taskContext = parseTaskContext(task)
|
||||||
const voucherContext = taskContext.kuaishouIndustryVoucher || {}
|
const voucherContext = normalizeAdminTaskIndustryVoucherContext(taskContext.kuaishouIndustryVoucher)
|
||||||
const voucherCode = String(voucherContext.voucherCode || voucherContext.eticketId || '').trim()
|
const voucherCode = String(voucherContext.voucherCode || voucherContext.eticketId || '').trim()
|
||||||
if (voucherCode) {
|
if (voucherCode) {
|
||||||
const voucher = await findKuaishouIndustryVoucherByCode(voucherCode, task.platform_order_id)
|
const voucher = await findKuaishouIndustryVoucherByCode(voucherCode, task.platform_order_id)
|
||||||
@@ -1000,6 +931,12 @@ async function getRequiredIndustryVoucherForTask(task: TaskRow): Promise<Kuaisho
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeAdminTaskIndustryVoucherContext(value: unknown): Record<string, any> {
|
||||||
|
return value && typeof value === 'object' && !Array.isArray(value)
|
||||||
|
? value as Record<string, any>
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
|
||||||
async function resolveIndustryVouchersForOrder(platformOrderId: string): Promise<KuaishouIndustryVoucherRow[]> {
|
async function resolveIndustryVouchersForOrder(platformOrderId: string): Promise<KuaishouIndustryVoucherRow[]> {
|
||||||
const normalizedOid = String(platformOrderId || '').trim()
|
const normalizedOid = String(platformOrderId || '').trim()
|
||||||
if (!normalizedOid) {
|
if (!normalizedOid) {
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ import { buildClaimUrl } from './claim-service.js'
|
|||||||
import type { ClaimTokenRow, OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
|
import type { ClaimTokenRow, OrderItemRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
|
||||||
|
|
||||||
export const CLAIM_TERMINAL_STATUSES = new Set([TASK_STATUS.EXPIRED, TASK_STATUS.CLOSED])
|
export const CLAIM_TERMINAL_STATUSES = new Set([TASK_STATUS.EXPIRED, TASK_STATUS.CLOSED])
|
||||||
export const KUAISHOU_CLOUD_CLAIM_GUIDE_BASE_PATH = '/kuaishou-cloud-guide'
|
|
||||||
|
|
||||||
type JsonObject = Record<string, any>
|
type JsonObject = Record<string, any>
|
||||||
type ClaimContext = {
|
type ClaimContext = {
|
||||||
claimToken: ClaimTokenRow
|
claimToken: ClaimTokenRow
|
||||||
@@ -355,11 +353,7 @@ export function mapClaimKuaishouCloudFulfillment(task: TaskRow, order: OrderRow)
|
|||||||
flowType: 'kuaishou_cloud',
|
flowType: 'kuaishou_cloud',
|
||||||
shopId: String(order?.shop_id || '').trim(),
|
shopId: String(order?.shop_id || '').trim(),
|
||||||
shopName: String(order?.shop_name || '').trim(),
|
shopName: String(order?.shop_name || '').trim(),
|
||||||
guideImages: [
|
guideImages: [],
|
||||||
`${KUAISHOU_CLOUD_CLAIM_GUIDE_BASE_PATH}/1.png`,
|
|
||||||
`${KUAISHOU_CLOUD_CLAIM_GUIDE_BASE_PATH}/2.png`,
|
|
||||||
`${KUAISHOU_CLOUD_CLAIM_GUIDE_BASE_PATH}/3.png`,
|
|
||||||
],
|
|
||||||
ticket: {
|
ticket: {
|
||||||
code: String(ticket.code || '').trim(),
|
code: String(ticket.code || '').trim(),
|
||||||
status: String(ticket.status || 'pending').trim() || 'pending',
|
status: String(ticket.status || 'pending').trim() || 'pending',
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
import fs from 'node:fs'
|
|
||||||
import path from 'node:path'
|
|
||||||
|
|
||||||
import { PROJECT_ROOT } from '../../config/runtime.js'
|
|
||||||
import { createTaskEvent } from '../../repositories/task-event-repo.js'
|
import { createTaskEvent } from '../../repositories/task-event-repo.js'
|
||||||
import { getTaskById, updateTask, updateTaskStatusIfCurrent } from '../../repositories/task-repo.js'
|
import { getTaskById, updateTask, updateTaskStatusIfCurrent } from '../../repositories/task-repo.js'
|
||||||
import { createHttpError } from '../../utils/http.js'
|
import { createHttpError } from '../../utils/http.js'
|
||||||
@@ -13,22 +9,10 @@ import {
|
|||||||
isKuaishouCloudRoleConfirmSettledStatus,
|
isKuaishouCloudRoleConfirmSettledStatus,
|
||||||
normalizeTaskStatus,
|
normalizeTaskStatus,
|
||||||
} from '../../domain/task-status.js'
|
} from '../../domain/task-status.js'
|
||||||
import {
|
|
||||||
getKuaishouEticketSourceConfig,
|
|
||||||
listKuaishouEticketShopConfigs,
|
|
||||||
resolveKuaishouEticketShopConfig,
|
|
||||||
type KuaishouEticketShopConfig,
|
|
||||||
} from '../platforms/kuaishou-eticket/source-config-service.js'
|
|
||||||
import {
|
|
||||||
consumeKuaishouEticket,
|
|
||||||
queryKuaishouEticketConsumeDetail,
|
|
||||||
} from '../platforms/kuaishou-eticket/consume-service.js'
|
|
||||||
import { isKuaishouEticketMockTicketCode } from '../platforms/kuaishou-eticket/mock-ticket-service.js'
|
|
||||||
import {
|
import {
|
||||||
dispatchKuaishouCloudFulfillmentTask,
|
dispatchKuaishouCloudFulfillmentTask,
|
||||||
hasKuaishouCloudCustomerRole,
|
hasKuaishouCloudCustomerRole,
|
||||||
hasKuaishouCloudDefaultRoleSnapshot,
|
hasKuaishouCloudDefaultRoleSnapshot,
|
||||||
maskCode,
|
|
||||||
normalizeKuaishouCloudFlow,
|
normalizeKuaishouCloudFlow,
|
||||||
prepareKuaishouCloudFulfillmentTask,
|
prepareKuaishouCloudFulfillmentTask,
|
||||||
rebindKuaishouCloudTaskRole,
|
rebindKuaishouCloudTaskRole,
|
||||||
@@ -39,215 +23,9 @@ import { buildClaimDetailPayload, getClaimContext } from './kuaishou-cloud-claim
|
|||||||
import { syncKuaishouCloudRoleInfo } from './kuaishou-cloud-sync-service.js'
|
import { syncKuaishouCloudRoleInfo } from './kuaishou-cloud-sync-service.js'
|
||||||
import type { TaskRow } from '../../types/repository/rows.js'
|
import type { TaskRow } from '../../types/repository/rows.js'
|
||||||
|
|
||||||
const KUAISHOU_CLOUD_GUIDE_DIR = path.resolve(PROJECT_ROOT, '../../tems/imgs')
|
|
||||||
const ALLOWED_GUIDE_FILES = new Set(['1.png', '2.png', '3.png'])
|
|
||||||
|
|
||||||
type JsonObject = Record<string, any>
|
type JsonObject = Record<string, any>
|
||||||
type ClaimDetailPayload = ReturnType<typeof buildClaimDetailPayload>
|
type ClaimDetailPayload = ReturnType<typeof buildClaimDetailPayload>
|
||||||
|
|
||||||
export async function verifyKuaishouCloudClaimTicket(token: unknown, payload: JsonObject = {}) {
|
|
||||||
const context = await getClaimContext(token)
|
|
||||||
const now = nowIso()
|
|
||||||
|
|
||||||
const executorKey = String(context.task.executor_key || '').trim()
|
|
||||||
|
|
||||||
if (executorKey === 'kuaishou-industry') {
|
|
||||||
return verifyIndustryVoucherTicket(context, now)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (executorKey !== 'kuaishou_ct_assisted') {
|
|
||||||
throw createHttpError('当前领取链接不是 kuaishou-lewan 客户领取流程', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'claim_not_kuaishou_cloud',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const flow = normalizeKuaishouCloudFlow(parseTaskContext(context.task).kuaishouCloudFulfillment)
|
|
||||||
if (hasUsableIndustryVoucher(parseTaskContext(context.task))) {
|
|
||||||
return verifyIndustryVoucherTicket(context, now)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ticketCode = String(payload.ticketCode || payload.eTicketId || '').trim()
|
|
||||||
if (!ticketCode) {
|
|
||||||
throw createHttpError('请先粘贴快手小店核销码', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'claim_kuaishou_cloud_missing_ticket_code',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const persistedTicketCode = String(flow.ticket.code || '').trim()
|
|
||||||
if (flow.consume.status === 'success') {
|
|
||||||
if (!persistedTicketCode || persistedTicketCode === ticketCode) {
|
|
||||||
return getKuaishouCloudClaimDetail(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
throw createHttpError('当前领取链接已完成核销,不能更换核销码', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'claim_kuaishou_cloud_ticket_already_consumed',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockTicketCode = isKuaishouEticketMockTicketCode(ticketCode)
|
|
||||||
let shopId = String(
|
|
||||||
flow.consume.shopId || context.order.shop_id || (mockTicketCode ? 'mock' : ''),
|
|
||||||
).trim()
|
|
||||||
let shopName = String(flow.consume.shopName || context.order.shop_name || '').trim()
|
|
||||||
let detailResult
|
|
||||||
let shopCookie = ''
|
|
||||||
let consumeBaseUrl = ''
|
|
||||||
|
|
||||||
if (mockTicketCode) {
|
|
||||||
detailResult = await queryKuaishouEticketConsumeDetail({
|
|
||||||
eTicketId: ticketCode,
|
|
||||||
goodsTitle: context.orderItem.sku_name || context.orderItem.sku_code,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const matched = await queryKuaishouEticketConsumeDetailWithShopFallback({
|
|
||||||
ticketCode,
|
|
||||||
shopId,
|
|
||||||
shopName,
|
|
||||||
fallbackShopName: String(context.order.shop_name || '').trim(),
|
|
||||||
})
|
|
||||||
|
|
||||||
detailResult = matched.detailResult
|
|
||||||
shopId = matched.shopConfig.shopId || shopId
|
|
||||||
shopName = matched.shopConfig.kshopName || shopName
|
|
||||||
shopCookie = matched.shopConfig.cookie
|
|
||||||
consumeBaseUrl = getKuaishouEticketSourceConfig().baseUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!detailResult.ok || detailResult.alreadyConsumed || !detailResult.detail) {
|
|
||||||
throw createHttpError(detailResult.errorMessage || '核销码校验失败,请确认是否复制完整', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: detailResult.alreadyConsumed
|
|
||||||
? 'claim_kuaishou_cloud_ticket_already_consumed'
|
|
||||||
: 'claim_kuaishou_cloud_ticket_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const consumePayload = {
|
|
||||||
baseUrl: consumeBaseUrl,
|
|
||||||
cookie: shopCookie,
|
|
||||||
eTicketId: detailResult.eTicketId || ticketCode,
|
|
||||||
oid: String(detailResult.detail.oid || '').trim(),
|
|
||||||
formToken: String(detailResult.detail.formToken || '').trim(),
|
|
||||||
num: Number(detailResult.detail.leftCount || 0) || 1,
|
|
||||||
}
|
|
||||||
const consumeResult = await consumeKuaishouEticket(consumePayload)
|
|
||||||
if (!consumeResult.consumed) {
|
|
||||||
throw createHttpError(consumeResult.errorMessage || '核销失败,请确认核销码状态后重试', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'claim_kuaishou_cloud_consume_failed',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const taskContext = parseTaskContext(context.task)
|
|
||||||
const nextContext = {
|
|
||||||
...taskContext,
|
|
||||||
kuaishouCloudFulfillment: {
|
|
||||||
...flow,
|
|
||||||
ticket: {
|
|
||||||
...flow.ticket,
|
|
||||||
code: detailResult.eTicketId || ticketCode,
|
|
||||||
status: 'verified',
|
|
||||||
capturedAt: flow.ticket.capturedAt || now,
|
|
||||||
capturedBy: flow.ticket.capturedBy || {
|
|
||||||
source: 'claim_page',
|
|
||||||
},
|
|
||||||
verifiedAt: now,
|
|
||||||
oid: String(detailResult.detail.oid || '').trim(),
|
|
||||||
formToken: String(detailResult.detail.formToken || '').trim(),
|
|
||||||
leftCount: Number(detailResult.detail.leftCount || 0) || 0,
|
|
||||||
goodsTitle: String(detailResult.goods?.itemTitle || '').trim(),
|
|
||||||
},
|
|
||||||
consume: {
|
|
||||||
...flow.consume,
|
|
||||||
status: 'success',
|
|
||||||
shopId,
|
|
||||||
shopName,
|
|
||||||
consumedAt: now,
|
|
||||||
errorMessage: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isKuaishouCloudMockContext(nextContext)) {
|
|
||||||
const mockFlow = buildMockVerifiedKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment, now)
|
|
||||||
await updateTask(context.task.id, {
|
|
||||||
claim_token: context.claimToken.token,
|
|
||||||
claim_expires_at: context.claimToken.expired_at,
|
|
||||||
context_json: JSON.stringify({
|
|
||||||
...nextContext,
|
|
||||||
kuaishouCloudFulfillment: mockFlow,
|
|
||||||
}),
|
|
||||||
claimed_at: context.task.claimed_at || now,
|
|
||||||
task_status: TASK_STATUS.WAITING_BINDING,
|
|
||||||
user_action_status: 'pending_claim',
|
|
||||||
last_error: '',
|
|
||||||
updated_at: now,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const preparedFlow = normalizeKuaishouCloudFlow(nextContext.kuaishouCloudFulfillment)
|
|
||||||
const taskWithConsumedTicket = (await updateTask(context.task.id, {
|
|
||||||
claim_token: context.claimToken.token,
|
|
||||||
claim_expires_at: context.claimToken.expired_at,
|
|
||||||
context_json: JSON.stringify(nextContext),
|
|
||||||
claimed_at: context.task.claimed_at || now,
|
|
||||||
task_status: TASK_STATUS.WAITING_BINDING,
|
|
||||||
user_action_status: 'pending_claim',
|
|
||||||
last_error: '',
|
|
||||||
updated_at: now,
|
|
||||||
})) || {
|
|
||||||
...context.task,
|
|
||||||
context_json: JSON.stringify(nextContext),
|
|
||||||
claimed_at: context.task.claimed_at || now,
|
|
||||||
task_status: TASK_STATUS.WAITING_BINDING,
|
|
||||||
user_action_status: 'pending_claim',
|
|
||||||
last_error: '',
|
|
||||||
updated_at: now,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preparedFlow.binding.prepareStatus !== 'ready' || !preparedFlow.binding.bindUrl) {
|
|
||||||
await prepareKuaishouCloudFulfillmentTask(taskWithConsumedTicket, {
|
|
||||||
source: 'claim_page_ticket_verified',
|
|
||||||
actor: { source: 'claim_page' },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await createTaskEvent(
|
|
||||||
context.task.id,
|
|
||||||
'kuaishou_cloud_ticket_verified',
|
|
||||||
{
|
|
||||||
ticketCodeMasked: maskCode(detailResult.eTicketId || ticketCode),
|
|
||||||
shopId,
|
|
||||||
shopName,
|
|
||||||
goodsTitle: String(detailResult.goods?.itemTitle || '').trim(),
|
|
||||||
mock: mockTicketCode,
|
|
||||||
consumedAt: now,
|
|
||||||
consumeAlreadyCompleted: consumeResult.alreadyConsumed === true,
|
|
||||||
},
|
|
||||||
now,
|
|
||||||
)
|
|
||||||
|
|
||||||
await createTaskEvent(
|
|
||||||
context.task.id,
|
|
||||||
'kuaishou_cloud_consumed',
|
|
||||||
{
|
|
||||||
source: 'claim_page_ticket_submit',
|
|
||||||
ticketCodeMasked: maskCode(detailResult.eTicketId || ticketCode),
|
|
||||||
shopId,
|
|
||||||
shopName,
|
|
||||||
consumeStatus: 'success',
|
|
||||||
consumedAt: now,
|
|
||||||
mock: mockTicketCode,
|
|
||||||
},
|
|
||||||
now,
|
|
||||||
)
|
|
||||||
|
|
||||||
return getKuaishouCloudClaimDetail(token)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function verifyIndustryVoucherTicket(
|
async function verifyIndustryVoucherTicket(
|
||||||
context: Awaited<ReturnType<typeof getClaimContext>>,
|
context: Awaited<ReturnType<typeof getClaimContext>>,
|
||||||
now: string,
|
now: string,
|
||||||
@@ -363,26 +141,6 @@ async function verifyIndustryVoucherTicket(
|
|||||||
return getKuaishouCloudClaimDetail(context.claimToken.token)
|
return getKuaishouCloudClaimDetail(context.claimToken.token)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getKuaishouCloudClaimGuideAssetPath(filename: unknown) {
|
|
||||||
const normalized = String(filename || '').trim()
|
|
||||||
if (!ALLOWED_GUIDE_FILES.has(normalized)) {
|
|
||||||
throw createHttpError('指引图片不存在', {
|
|
||||||
statusCode: 404,
|
|
||||||
errorCode: 'claim_kuaishou_cloud_asset_not_found',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const filePath = path.resolve(KUAISHOU_CLOUD_GUIDE_DIR, normalized)
|
|
||||||
if (!filePath.startsWith(KUAISHOU_CLOUD_GUIDE_DIR) || !fs.existsSync(filePath)) {
|
|
||||||
throw createHttpError('指引图片不存在', {
|
|
||||||
statusCode: 404,
|
|
||||||
errorCode: 'claim_kuaishou_cloud_asset_not_found',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return filePath
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getKuaishouCloudClaimDetail(token: unknown): Promise<ClaimDetailPayload> {
|
export async function getKuaishouCloudClaimDetail(token: unknown): Promise<ClaimDetailPayload> {
|
||||||
const context = await getClaimContext(token)
|
const context = await getClaimContext(token)
|
||||||
let task = context.task
|
let task = context.task
|
||||||
@@ -500,134 +258,6 @@ function normalizeIndustryVoucherContext(value: unknown): JsonObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function queryKuaishouEticketConsumeDetailWithShopFallback({
|
|
||||||
ticketCode,
|
|
||||||
shopId,
|
|
||||||
shopName,
|
|
||||||
fallbackShopName = '',
|
|
||||||
}: {
|
|
||||||
ticketCode: string
|
|
||||||
shopId: string
|
|
||||||
shopName: string
|
|
||||||
fallbackShopName?: string
|
|
||||||
}) {
|
|
||||||
const eticketSource = getKuaishouEticketSourceConfig()
|
|
||||||
const shopConfigs = resolveKuaishouEticketDetailCandidateShops({
|
|
||||||
shopId,
|
|
||||||
shopName,
|
|
||||||
fallbackShopName,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (shopConfigs.length === 0) {
|
|
||||||
throw createHttpError('还没有配置可用的快手小店 Cookie,请联系客服处理', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'claim_kuaishou_cloud_shop_cookie_missing',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastResult: JsonObject | null = null
|
|
||||||
let lastError: unknown = null
|
|
||||||
|
|
||||||
for (const shopConfig of shopConfigs) {
|
|
||||||
try {
|
|
||||||
const detailResult = await queryKuaishouEticketConsumeDetail({
|
|
||||||
baseUrl: eticketSource.baseUrl,
|
|
||||||
cookie: shopConfig.cookie,
|
|
||||||
eTicketId: ticketCode,
|
|
||||||
})
|
|
||||||
|
|
||||||
lastResult = detailResult
|
|
||||||
if (detailResult.ok || detailResult.alreadyConsumed) {
|
|
||||||
return {
|
|
||||||
shopConfig,
|
|
||||||
detailResult,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
lastError = error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastResult) {
|
|
||||||
const fallbackShopConfig = shopConfigs[0]
|
|
||||||
if (!fallbackShopConfig) {
|
|
||||||
throw createHttpError('还没有配置可用的快手小店 Cookie,请联系客服处理', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'claim_kuaishou_cloud_shop_cookie_missing',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
shopConfig: fallbackShopConfig,
|
|
||||||
detailResult: lastResult,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw createHttpError(
|
|
||||||
lastError instanceof Error ? lastError.message : '核销码校验失败,请确认是否复制完整',
|
|
||||||
{
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'claim_kuaishou_cloud_ticket_invalid',
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveKuaishouEticketDetailCandidateShops({
|
|
||||||
shopId,
|
|
||||||
shopName,
|
|
||||||
fallbackShopName = '',
|
|
||||||
}: {
|
|
||||||
shopId: string
|
|
||||||
shopName: string
|
|
||||||
fallbackShopName?: string
|
|
||||||
}) {
|
|
||||||
const candidates: KuaishouEticketShopConfig[] = []
|
|
||||||
const configuredShop = resolveKuaishouEticketShopConfig({
|
|
||||||
shopId,
|
|
||||||
shopName: shopName || fallbackShopName,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (isUsableKuaishouEticketShopConfig(configuredShop)) {
|
|
||||||
candidates.push(configuredShop)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const shopConfig of listKuaishouEticketShopConfigs()) {
|
|
||||||
if (!isUsableKuaishouEticketShopConfig(shopConfig)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (candidates.some((item) => isSameKuaishouEticketShopConfig(item, shopConfig))) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
candidates.push(shopConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
return candidates
|
|
||||||
}
|
|
||||||
|
|
||||||
function isUsableKuaishouEticketShopConfig(
|
|
||||||
shopConfig: KuaishouEticketShopConfig | null | undefined,
|
|
||||||
): shopConfig is KuaishouEticketShopConfig {
|
|
||||||
return Boolean(
|
|
||||||
shopConfig && shopConfig.enabled !== false && String(shopConfig.cookie || '').trim(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSameKuaishouEticketShopConfig(
|
|
||||||
left: KuaishouEticketShopConfig,
|
|
||||||
right: KuaishouEticketShopConfig,
|
|
||||||
) {
|
|
||||||
const leftShopId = String(left.shopId || '').trim()
|
|
||||||
const rightShopId = String(right.shopId || '').trim()
|
|
||||||
|
|
||||||
if (leftShopId && rightShopId) {
|
|
||||||
return leftShopId === rightShopId
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(left.kshopName || '').trim() === String(right.kshopName || '').trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function confirmKuaishouCloudClaimRole(token: unknown) {
|
export async function confirmKuaishouCloudClaimRole(token: unknown) {
|
||||||
const context = await getClaimContext(token)
|
const context = await getClaimContext(token)
|
||||||
const now = nowIso()
|
const now = nowIso()
|
||||||
@@ -656,7 +286,7 @@ export async function confirmKuaishouCloudClaimRole(token: unknown) {
|
|||||||
const flow = normalizeKuaishouCloudFlow(parseTaskContext(refreshed.task).kuaishouCloudFulfillment)
|
const flow = normalizeKuaishouCloudFlow(parseTaskContext(refreshed.task).kuaishouCloudFulfillment)
|
||||||
|
|
||||||
if (flow.ticket.status !== 'verified') {
|
if (flow.ticket.status !== 'verified') {
|
||||||
throw createHttpError('请先验证核销码', {
|
throw createHttpError('请先完成电子凭证确认', {
|
||||||
statusCode: 409,
|
statusCode: 409,
|
||||||
errorCode: 'claim_kuaishou_cloud_ticket_not_verified',
|
errorCode: 'claim_kuaishou_cloud_ticket_not_verified',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ async function preparePaidTask(
|
|||||||
claim_token: claimToken.token,
|
claim_token: claimToken.token,
|
||||||
claim_expires_at: claimToken.expired_at,
|
claim_expires_at: claimToken.expired_at,
|
||||||
user_action_status: 'pending_claim',
|
user_action_status: 'pending_claim',
|
||||||
last_error: task.last_error || '领取链接已生成,等待客户提交核销码',
|
last_error: task.last_error || '领取链接已生成,等待电子凭证流程推进',
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { getOrderById } from "../../../repositories/order-repo.js";
|
|
||||||
import { createTaskEvent } from "../../../repositories/task-event-repo.js";
|
import { createTaskEvent } from "../../../repositories/task-event-repo.js";
|
||||||
import { updateTask } from "../../../repositories/task-repo.js";
|
import { updateTask } from "../../../repositories/task-repo.js";
|
||||||
import { createHttpError } from "../../../utils/http.js";
|
import { createHttpError } from "../../../utils/http.js";
|
||||||
import { nowIso } from "../../../utils/time.js";
|
import { nowIso } from "../../../utils/time.js";
|
||||||
import { TASK_STATUS, normalizeTaskStatus, type TaskStatus } from "../../../domain/task-status.js";
|
import { TASK_STATUS, normalizeTaskStatus, type TaskStatus } from "../../../domain/task-status.js";
|
||||||
import {
|
import { notifyKuaishouCloudAssetNotEnough } from "../../notification/domain-notifications.js";
|
||||||
notifyKuaishouCloudAssetNotEnough,
|
|
||||||
notifyKuaishouCloudConsumeFailed,
|
|
||||||
} from "../../notification/domain-notifications.js";
|
|
||||||
import {
|
import {
|
||||||
buyCloudtentaclesSku,
|
buyCloudtentaclesSku,
|
||||||
getCloudtentaclesAsset,
|
getCloudtentaclesAsset,
|
||||||
@@ -16,13 +12,7 @@ import {
|
|||||||
} from "../../platforms/cloudtentacles/catalog-service.js";
|
} from "../../platforms/cloudtentacles/catalog-service.js";
|
||||||
import { getCloudtentaclesKnapsack } from "../../platforms/cloudtentacles/knapsack-service.js";
|
import { getCloudtentaclesKnapsack } from "../../platforms/cloudtentacles/knapsack-service.js";
|
||||||
import { backCloudtentaclesVirtualNumber } from "../../platforms/cloudtentacles/virtual-number-service.js";
|
import { backCloudtentaclesVirtualNumber } from "../../platforms/cloudtentacles/virtual-number-service.js";
|
||||||
import { consumeKuaishouEticket } from "../../platforms/kuaishou-eticket/consume-service.js";
|
|
||||||
import { isKuaishouEticketMockTicketCode } from "../../platforms/kuaishou-eticket/mock-ticket-service.js";
|
|
||||||
import { consumeKuaishouIndustryVouchersForTask } from "../../platforms/kuaishou-industry/voucher-service.js";
|
import { consumeKuaishouIndustryVouchersForTask } from "../../platforms/kuaishou-industry/voucher-service.js";
|
||||||
import {
|
|
||||||
getKuaishouEticketSourceConfig,
|
|
||||||
resolveKuaishouEticketShopConfig,
|
|
||||||
} from "../../platforms/kuaishou-eticket/source-config-service.js";
|
|
||||||
import {
|
import {
|
||||||
isKuaishouCloudTask,
|
isKuaishouCloudTask,
|
||||||
isIndustryEVoucherTask,
|
isIndustryEVoucherTask,
|
||||||
@@ -95,10 +85,19 @@ export async function dispatchKuaishouCloudFulfillmentTask(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ticketCode = String(options.ticketCode || "").trim();
|
|
||||||
const persistedTicketCode = String(flow.ticket.code || "").trim();
|
const persistedTicketCode = String(flow.ticket.code || "").trim();
|
||||||
if (!persistedTicketCode && !ticketCode) {
|
const voucherContext = isPlainObject(taskContext.kuaishouIndustryVoucher)
|
||||||
throw createHttpError("客户还没有提交有效核销码,暂时不能继续兑换", {
|
? taskContext.kuaishouIndustryVoucher
|
||||||
|
: {};
|
||||||
|
const industryVoucherCode = String(
|
||||||
|
voucherContext.voucherCode || voucherContext.eticketId || ""
|
||||||
|
).trim();
|
||||||
|
const hasIndustryVoucherForDispatch =
|
||||||
|
Boolean(industryVoucherCode) || isIndustryEVoucherTask(task);
|
||||||
|
const resolvedTicketCode = persistedTicketCode || industryVoucherCode;
|
||||||
|
|
||||||
|
if (!resolvedTicketCode && !hasIndustryVoucherForDispatch) {
|
||||||
|
throw createHttpError("旧快手小店核销流程已停用,请改用行业电子凭证处理", {
|
||||||
statusCode: 409,
|
statusCode: 409,
|
||||||
errorCode: "kuaishou_cloud_missing_ticket_code",
|
errorCode: "kuaishou_cloud_missing_ticket_code",
|
||||||
});
|
});
|
||||||
@@ -173,9 +172,11 @@ export async function dispatchKuaishouCloudFulfillmentTask(
|
|||||||
...syncedFlow,
|
...syncedFlow,
|
||||||
ticket: {
|
ticket: {
|
||||||
...syncedFlow.ticket,
|
...syncedFlow.ticket,
|
||||||
code: ticketCode || persistedTicketCode,
|
code: resolvedTicketCode,
|
||||||
capturedAt: ticketCode ? now : syncedFlow.ticket.capturedAt,
|
capturedAt: resolvedTicketCode
|
||||||
capturedBy: ticketCode && actor ? actor : syncedFlow.ticket.capturedBy,
|
? syncedFlow.ticket.capturedAt || now
|
||||||
|
: syncedFlow.ticket.capturedAt,
|
||||||
|
capturedBy: syncedFlow.ticket.capturedBy,
|
||||||
},
|
},
|
||||||
dispatch: {
|
dispatch: {
|
||||||
...syncedFlow.dispatch,
|
...syncedFlow.dispatch,
|
||||||
@@ -222,7 +223,7 @@ export async function dispatchKuaishouCloudFulfillmentTask(
|
|||||||
"kuaishou_cloud_dispatched",
|
"kuaishou_cloud_dispatched",
|
||||||
{
|
{
|
||||||
source: String(options.source || "system").trim() || "system",
|
source: String(options.source || "system").trim() || "system",
|
||||||
ticketCodeMasked: maskCode(ticketCode || persistedTicketCode),
|
ticketCodeMasked: maskCode(resolvedTicketCode),
|
||||||
skuId: syncedFlow.binding.skuId,
|
skuId: syncedFlow.binding.skuId,
|
||||||
vnId: syncedFlow.binding.vnId,
|
vnId: syncedFlow.binding.vnId,
|
||||||
vnPhoneMasked: maskPhone(syncedFlow.binding.vnPhone),
|
vnPhoneMasked: maskPhone(syncedFlow.binding.vnPhone),
|
||||||
@@ -785,34 +786,25 @@ export async function returnKuaishouCloudFulfillmentTask(
|
|||||||
id: flow.binding.vnId,
|
id: flow.binding.vnId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const order = await getOrderById(task.order_id);
|
|
||||||
const ticketCode = String(flow.ticket.code || "").trim();
|
const ticketCode = String(flow.ticket.code || "").trim();
|
||||||
const shopId = String(flow.consume.shopId || order?.shop_id || "").trim();
|
|
||||||
const shopName = String(
|
|
||||||
flow.consume.shopName || order?.shop_name || ""
|
|
||||||
).trim();
|
|
||||||
const mockTicketCode = isKuaishouEticketMockTicketCode(ticketCode);
|
|
||||||
const eticketSource = getKuaishouEticketSourceConfig();
|
|
||||||
const shopConfig = resolveKuaishouEticketShopConfig({
|
|
||||||
shopId,
|
|
||||||
shopName,
|
|
||||||
});
|
|
||||||
|
|
||||||
let consumeStatus = "pending";
|
let consumeStatus = "pending";
|
||||||
let consumeErrorMessage = "";
|
let consumeErrorMessage = "";
|
||||||
let consumedAt = null;
|
let consumedAt = null;
|
||||||
let nextTaskStatus: TaskStatus = TASK_STATUS.COMPLETED;
|
let nextTaskStatus: TaskStatus = TASK_STATUS.COMPLETED;
|
||||||
let nextResultCode = "kuaishou_cloud_completed";
|
let nextResultCode = "kuaishou_cloud_completed";
|
||||||
let nextResultMessage = "cloudtentacles 发货、退号并完成快手核销";
|
|
||||||
const consumeAlreadyCompleted = flow.consume.status === "success";
|
const consumeAlreadyCompleted = flow.consume.status === "success";
|
||||||
const hasIndustryVoucher = hasKuaishouIndustryVoucherContext(taskContext);
|
const hasIndustryVoucher = hasKuaishouIndustryVoucherContext(taskContext);
|
||||||
const isIndustryTask = isIndustryEVoucherTask(task);
|
const isIndustryTask = isIndustryEVoucherTask(task);
|
||||||
|
const shouldConsumeIndustryVoucher = hasIndustryVoucher || isIndustryTask;
|
||||||
|
let nextResultMessage = shouldConsumeIndustryVoucher
|
||||||
|
? "cloudtentacles 发货、退号并完成电子凭证核销"
|
||||||
|
: "cloudtentacles 发货、退号并收口";
|
||||||
let industryVoucherContextPatch: JsonObject | null = null;
|
let industryVoucherContextPatch: JsonObject | null = null;
|
||||||
|
|
||||||
if (consumeAlreadyCompleted) {
|
if (consumeAlreadyCompleted) {
|
||||||
consumeStatus = "success";
|
consumeStatus = "success";
|
||||||
consumedAt = flow.consume.consumedAt || now;
|
consumedAt = flow.consume.consumedAt || now;
|
||||||
} else if (hasIndustryVoucher || isIndustryTask) {
|
} else if (shouldConsumeIndustryVoucher) {
|
||||||
const industryResult = hasIndustryVoucher
|
const industryResult = hasIndustryVoucher
|
||||||
? await consumeKuaishouIndustryVouchersForTask(task, {
|
? await consumeKuaishouIndustryVouchersForTask(task, {
|
||||||
source: String(options.source || "system_auto_finalize").trim() || "system_auto_finalize",
|
source: String(options.source || "system_auto_finalize").trim() || "system_auto_finalize",
|
||||||
@@ -844,59 +836,20 @@ export async function returnKuaishouCloudFulfillmentTask(
|
|||||||
consumeErrorMessage =
|
consumeErrorMessage =
|
||||||
industryResult.failed[0]?.errorMessage || "电子凭证核销回调失败,请人工处理";
|
industryResult.failed[0]?.errorMessage || "电子凭证核销回调失败,请人工处理";
|
||||||
}
|
}
|
||||||
} else if (!order) {
|
|
||||||
consumeStatus = "failed";
|
|
||||||
consumeErrorMessage = "任务关联订单不存在,无法执行快手核销";
|
|
||||||
} else if (!ticketCode) {
|
|
||||||
consumeStatus = "failed";
|
|
||||||
consumeErrorMessage = "客户未提交有效核销码,无法执行快手核销";
|
|
||||||
} else if (mockTicketCode) {
|
|
||||||
const consumeResult = await consumeKuaishouEticket({
|
|
||||||
eTicketId: ticketCode,
|
|
||||||
oid: String(flow.ticket.oid || "").trim(),
|
|
||||||
formToken: String(flow.ticket.formToken || "").trim(),
|
|
||||||
});
|
|
||||||
consumeStatus = consumeResult.consumed ? "success" : "failed";
|
|
||||||
consumedAt = consumeResult.consumed ? now : null;
|
|
||||||
consumeErrorMessage = String(consumeResult.errorMessage || "").trim();
|
|
||||||
} else if (
|
|
||||||
!shopConfig ||
|
|
||||||
shopConfig.enabled === false ||
|
|
||||||
!String(shopConfig.cookie || "").trim()
|
|
||||||
) {
|
|
||||||
consumeStatus = "failed";
|
|
||||||
consumeErrorMessage = "订单对应快手小店缺少可用 Cookie,无法执行快手核销";
|
|
||||||
} else {
|
} else {
|
||||||
try {
|
consumeStatus = "skipped";
|
||||||
const consumeResult = await consumeKuaishouEticket({
|
consumeErrorMessage = "";
|
||||||
baseUrl: eticketSource.baseUrl,
|
|
||||||
cookie: shopConfig.cookie,
|
|
||||||
eTicketId: ticketCode,
|
|
||||||
oid: String(flow.ticket.oid || "").trim(),
|
|
||||||
formToken: String(flow.ticket.formToken || "").trim(),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (consumeResult.consumed) {
|
|
||||||
consumeStatus = "success";
|
|
||||||
consumedAt = now;
|
|
||||||
} else {
|
|
||||||
consumeStatus = "failed";
|
|
||||||
consumeErrorMessage = String(
|
|
||||||
consumeResult.errorMessage || "快手核销失败"
|
|
||||||
).trim();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
consumeStatus = "failed";
|
|
||||||
consumeErrorMessage =
|
|
||||||
error instanceof Error ? error.message : "快手核销失败";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (consumeStatus !== "success") {
|
if (consumeStatus !== "success") {
|
||||||
|
if (shouldConsumeIndustryVoucher) {
|
||||||
nextTaskStatus = TASK_STATUS.MANUAL_REVIEW;
|
nextTaskStatus = TASK_STATUS.MANUAL_REVIEW;
|
||||||
nextResultCode = "kuaishou_cloud_consume_failed";
|
nextResultCode = "kuaishou_cloud_consume_failed";
|
||||||
nextResultMessage =
|
nextResultMessage =
|
||||||
consumeErrorMessage || "号码已退还,但快手核销未完成,请人工处理";
|
consumeErrorMessage || "号码已退还,但电子凭证核销未完成,请人工处理";
|
||||||
|
} else {
|
||||||
|
nextResultCode = "kuaishou_cloud_completed_without_eticket_consume";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextContext = {
|
const nextContext = {
|
||||||
@@ -922,8 +875,8 @@ export async function returnKuaishouCloudFulfillmentTask(
|
|||||||
consume: {
|
consume: {
|
||||||
...flow.consume,
|
...flow.consume,
|
||||||
status: consumeStatus,
|
status: consumeStatus,
|
||||||
shopId: shopId || flow.consume.shopId,
|
shopId: flow.consume.shopId,
|
||||||
shopName,
|
shopName: flow.consume.shopName,
|
||||||
autoConsumeEnabled: flow.consume.autoConsumeEnabled === true,
|
autoConsumeEnabled: flow.consume.autoConsumeEnabled === true,
|
||||||
consumedAt,
|
consumedAt,
|
||||||
errorMessage: consumeErrorMessage,
|
errorMessage: consumeErrorMessage,
|
||||||
@@ -936,7 +889,7 @@ export async function returnKuaishouCloudFulfillmentTask(
|
|||||||
delivery_status: "delivered",
|
delivery_status: "delivered",
|
||||||
result_code: nextResultCode,
|
result_code: nextResultCode,
|
||||||
result_message: nextResultMessage,
|
result_message: nextResultMessage,
|
||||||
redeemed_at: consumeStatus === "success" ? now : task.redeemed_at,
|
redeemed_at: consumeStatus === "failed" ? task.redeemed_at : now,
|
||||||
last_error: consumeErrorMessage,
|
last_error: consumeErrorMessage,
|
||||||
context_json: JSON.stringify(nextContext),
|
context_json: JSON.stringify(nextContext),
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
@@ -954,36 +907,30 @@ export async function returnKuaishouCloudFulfillmentTask(
|
|||||||
now
|
now
|
||||||
);
|
);
|
||||||
|
|
||||||
await createTaskEvent(
|
const consumeEventType = consumeAlreadyCompleted
|
||||||
task.id,
|
|
||||||
consumeAlreadyCompleted
|
|
||||||
? "kuaishou_cloud_consume_already_completed"
|
? "kuaishou_cloud_consume_already_completed"
|
||||||
: consumeStatus === "success"
|
: consumeStatus === "success"
|
||||||
? "kuaishou_cloud_consumed"
|
? "kuaishou_cloud_consumed"
|
||||||
: "kuaishou_cloud_consume_failed",
|
: consumeStatus === "skipped"
|
||||||
|
? "kuaishou_cloud_consume_skipped"
|
||||||
|
: "kuaishou_cloud_consume_failed";
|
||||||
|
|
||||||
|
await createTaskEvent(
|
||||||
|
task.id,
|
||||||
|
consumeEventType,
|
||||||
{
|
{
|
||||||
source: String(options.source || "system").trim() || "system",
|
source: String(options.source || "system").trim() || "system",
|
||||||
ticketCodeMasked: maskCode(ticketCode),
|
ticketCodeMasked: maskCode(ticketCode),
|
||||||
shopId,
|
shopId: flow.consume.shopId,
|
||||||
shopName,
|
shopName: flow.consume.shopName,
|
||||||
consumeStatus,
|
consumeStatus,
|
||||||
|
consumeMode: shouldConsumeIndustryVoucher ? "industry_voucher" : "legacy_writeoff_disabled",
|
||||||
errorMessage: consumeErrorMessage,
|
errorMessage: consumeErrorMessage,
|
||||||
actor,
|
actor,
|
||||||
},
|
},
|
||||||
now
|
now
|
||||||
);
|
);
|
||||||
|
|
||||||
if (consumeStatus !== "success") {
|
|
||||||
await notifyKuaishouCloudConsumeFailed({
|
|
||||||
task: updatedTask,
|
|
||||||
order,
|
|
||||||
ticketCodeMasked: maskCode(ticketCode),
|
|
||||||
shopId,
|
|
||||||
shopName,
|
|
||||||
errorMessage: consumeErrorMessage,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
task: updatedTask,
|
task: updatedTask,
|
||||||
flow: normalizeKuaishouCloudFlow(
|
flow: normalizeKuaishouCloudFlow(
|
||||||
|
|||||||
@@ -48,14 +48,6 @@ type TaskNotificationPayload = {
|
|||||||
source?: unknown
|
source?: unknown
|
||||||
errorMessage?: unknown
|
errorMessage?: unknown
|
||||||
}
|
}
|
||||||
type KuaishouCloudConsumeFailedPayload = {
|
|
||||||
task?: unknown
|
|
||||||
order?: unknown
|
|
||||||
ticketCodeMasked?: unknown
|
|
||||||
shopId?: unknown
|
|
||||||
shopName?: unknown
|
|
||||||
errorMessage?: unknown
|
|
||||||
}
|
|
||||||
type ClaimRedeemNeedsAttentionPayload = {
|
type ClaimRedeemNeedsAttentionPayload = {
|
||||||
task?: unknown
|
task?: unknown
|
||||||
order?: unknown
|
order?: unknown
|
||||||
@@ -216,31 +208,6 @@ export function notifyKuaishouCloudBindUrlRefreshFailed({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function notifyKuaishouCloudConsumeFailed({
|
|
||||||
task = {},
|
|
||||||
order = {},
|
|
||||||
ticketCodeMasked = '',
|
|
||||||
shopId = '',
|
|
||||||
shopName = '',
|
|
||||||
errorMessage = '',
|
|
||||||
}: KuaishouCloudConsumeFailedPayload = {}) {
|
|
||||||
const taskRecord = toRecord(task)
|
|
||||||
const orderRecord = toRecord(order)
|
|
||||||
|
|
||||||
return notifyInternalSafely({
|
|
||||||
title: '快手核销失败,需人工处理',
|
|
||||||
body: [
|
|
||||||
formatTaskLine(taskRecord),
|
|
||||||
`订单:${String(orderRecord.platform_order_id || taskRecord.platform_order_id || '').trim() || '-'}`,
|
|
||||||
`券码:${String(ticketCodeMasked || '').trim() || '-'}`,
|
|
||||||
`店铺:${String(shopName || shopId || '').trim() || '-'}`,
|
|
||||||
`原因:${String(errorMessage || taskRecord.last_error || '').trim() || '-'}`,
|
|
||||||
].join('\n'),
|
|
||||||
category: 'kuaishou_cloud_consume_failed',
|
|
||||||
cooldownKey: ['kuaishou_cloud_consume_failed', taskRecord.id || ''].join(':'),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function notifyTaskAutoManualReview({
|
export function notifyTaskAutoManualReview({
|
||||||
task = {},
|
task = {},
|
||||||
reason = '',
|
reason = '',
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
import test from 'node:test'
|
|
||||||
import assert from 'node:assert/strict'
|
|
||||||
|
|
||||||
import {
|
|
||||||
consumeKuaishouEticket,
|
|
||||||
mapKuaishouEticketDetailResult,
|
|
||||||
normalizeKuaishouEticketApiResult,
|
|
||||||
queryKuaishouEticketConsumeDetail,
|
|
||||||
resolveKuaishouEticketConsumePayload,
|
|
||||||
} from './consume-service.js'
|
|
||||||
|
|
||||||
test('normalizeKuaishouEticketApiResult recognizes already consumed response', () => {
|
|
||||||
const result = normalizeKuaishouEticketApiResult({
|
|
||||||
result: 21,
|
|
||||||
error_msg: '券码已核销无法再次核销,请核实后重试',
|
|
||||||
requestId: 'abc',
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.equal(result.ok, false)
|
|
||||||
assert.equal(result.result, 21)
|
|
||||||
assert.equal(result.errorMessage, '券码已核销无法再次核销,请核实后重试')
|
|
||||||
assert.equal(result.requestId, 'abc')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('mapKuaishouEticketDetailResult maps detail payload fields', () => {
|
|
||||||
const result = mapKuaishouEticketDetailResult({
|
|
||||||
result: 1,
|
|
||||||
error_msg: '成功',
|
|
||||||
data: {
|
|
||||||
eTicket: {
|
|
||||||
eTicketId: 'F2E886B6CBF8E5F5',
|
|
||||||
oid: '2612200246413561',
|
|
||||||
formToken: '1777716306154',
|
|
||||||
leftCount: 1,
|
|
||||||
totalCount: 1,
|
|
||||||
status: 'CONSUME_ORDER_CREATED',
|
|
||||||
},
|
|
||||||
goods: {
|
|
||||||
itemTitle: '测试链接五排',
|
|
||||||
skuDesc: '测试1',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
baseUrl: 'https://s.kwaixiaodian.com',
|
|
||||||
eTicketId: 'F2E886B6CBF8E5F5',
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.equal(result.ok, true)
|
|
||||||
assert.equal(result.detail?.oid, '2612200246413561')
|
|
||||||
assert.equal(result.detail?.formToken, '1777716306154')
|
|
||||||
assert.equal(result.detail?.leftCount, 1)
|
|
||||||
assert.equal(result.goods?.itemTitle, '测试链接五排')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('resolveKuaishouEticketConsumePayload falls back to detail result', () => {
|
|
||||||
const payload = resolveKuaishouEticketConsumePayload({
|
|
||||||
eTicketId: 'F2E886B6CBF8E5F5',
|
|
||||||
}, {
|
|
||||||
detail: {
|
|
||||||
eTicketId: 'F2E886B6CBF8E5F5',
|
|
||||||
oid: '2612200246413561',
|
|
||||||
formToken: '1777716306154',
|
|
||||||
leftCount: 2,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.deepEqual(payload, {
|
|
||||||
eTicketId: 'F2E886B6CBF8E5F5',
|
|
||||||
num: 2,
|
|
||||||
storeId: '0',
|
|
||||||
oid: '2612200246413561',
|
|
||||||
formToken: '1777716306154',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('queryKuaishouEticketConsumeDetail accepts mock ticket code outside production', async () => {
|
|
||||||
const originalNodeEnv = process.env.NODE_ENV
|
|
||||||
process.env.NODE_ENV = 'development'
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await queryKuaishouEticketConsumeDetail({
|
|
||||||
eTicketId: 'MOCK-CLAIM-001',
|
|
||||||
goodsTitle: '套装-浪漫天命',
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.equal(result.ok, true)
|
|
||||||
assert.equal(result.eTicketId, 'MOCK-CLAIM-001')
|
|
||||||
assert.equal(result.detail?.leftCount, 1)
|
|
||||||
assert.equal(result.goods?.itemTitle, '套装-浪漫天命')
|
|
||||||
} finally {
|
|
||||||
if (originalNodeEnv === undefined) {
|
|
||||||
delete process.env.NODE_ENV
|
|
||||||
} else {
|
|
||||||
process.env.NODE_ENV = originalNodeEnv
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
test('consumeKuaishouEticket consumes mock ticket code outside production', async () => {
|
|
||||||
const originalNodeEnv = process.env.NODE_ENV
|
|
||||||
process.env.NODE_ENV = 'development'
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await consumeKuaishouEticket({
|
|
||||||
eTicketId: 'MOCK-CLAIM-002',
|
|
||||||
oid: 'MOCK-OID-002',
|
|
||||||
formToken: 'MOCK-FORM-002',
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.equal(result.consumed, true)
|
|
||||||
assert.equal(result.eTicketId, 'MOCK-CLAIM-002')
|
|
||||||
assert.equal(result.oid, 'MOCK-OID-002')
|
|
||||||
assert.equal(result.formToken, 'MOCK-FORM-002')
|
|
||||||
} finally {
|
|
||||||
if (originalNodeEnv === undefined) {
|
|
||||||
delete process.env.NODE_ENV
|
|
||||||
} else {
|
|
||||||
process.env.NODE_ENV = originalNodeEnv
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
import { createHttpError } from '../../../utils/http.js'
|
|
||||||
import { kuaishouEticketRequest } from './http-client.js'
|
|
||||||
import { resolveKuaishouEticketConfig } from './helpers.js'
|
|
||||||
import {
|
|
||||||
buildMockKuaishouEticketConsumeResult,
|
|
||||||
buildMockKuaishouEticketDetailResult,
|
|
||||||
isKuaishouEticketMockTicketCode,
|
|
||||||
} from './mock-ticket-service.js'
|
|
||||||
|
|
||||||
type JsonObject = Record<string, any>
|
|
||||||
|
|
||||||
type KuaishouEticketDetailPayload = {
|
|
||||||
eTicketId: string
|
|
||||||
oid: string
|
|
||||||
formToken: string
|
|
||||||
leftCount: number
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
type KuaishouEticketDetailResult = {
|
|
||||||
detail: KuaishouEticketDetailPayload | null
|
|
||||||
[key: string]: any
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function queryKuaishouEticketConsumeDetail(payload: JsonObject = {}) {
|
|
||||||
if (isKuaishouEticketMockTicketCode(payload.eTicketId)) {
|
|
||||||
return buildMockKuaishouEticketDetailResult({
|
|
||||||
baseUrl: String(payload.baseUrl || '').trim(),
|
|
||||||
eTicketId: String(payload.eTicketId || '').trim(),
|
|
||||||
goodsTitle: String(payload.goodsTitle || '').trim(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const context = resolveKuaishouEticketActionContext(payload)
|
|
||||||
const response = await kuaishouEticketRequest(context.detailPath, {
|
|
||||||
baseUrl: context.baseUrl,
|
|
||||||
cookie: context.cookie,
|
|
||||||
timeoutMs: context.timeoutMs,
|
|
||||||
body: {
|
|
||||||
eTicketId: context.eTicketId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return mapKuaishouEticketDetailResult(response.payload, {
|
|
||||||
baseUrl: context.baseUrl,
|
|
||||||
eTicketId: context.eTicketId,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function consumeKuaishouEticket(payload: JsonObject = {}) {
|
|
||||||
if (isKuaishouEticketMockTicketCode(payload.eTicketId)) {
|
|
||||||
return buildMockKuaishouEticketConsumeResult({
|
|
||||||
baseUrl: String(payload.baseUrl || '').trim(),
|
|
||||||
eTicketId: String(payload.eTicketId || '').trim(),
|
|
||||||
oid: String(payload.oid || '').trim(),
|
|
||||||
formToken: String(payload.formToken || '').trim(),
|
|
||||||
num: payload.num,
|
|
||||||
storeId: String(payload.storeId || '').trim(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const context = resolveKuaishouEticketActionContext(payload)
|
|
||||||
const detailResult = shouldResolveConsumeDetail(payload)
|
|
||||||
? await queryKuaishouEticketConsumeDetail({
|
|
||||||
baseUrl: context.baseUrl,
|
|
||||||
cookie: context.cookie,
|
|
||||||
eTicketId: context.eTicketId,
|
|
||||||
timeoutMs: context.timeoutMs,
|
|
||||||
})
|
|
||||||
: null
|
|
||||||
const consumePayload = resolveKuaishouEticketConsumePayload(payload, detailResult)
|
|
||||||
|
|
||||||
const response = await kuaishouEticketRequest(context.consumePath, {
|
|
||||||
baseUrl: context.baseUrl,
|
|
||||||
cookie: context.cookie,
|
|
||||||
timeoutMs: context.timeoutMs,
|
|
||||||
body: consumePayload,
|
|
||||||
})
|
|
||||||
|
|
||||||
return mapKuaishouEticketConsumeResult(response.payload, {
|
|
||||||
baseUrl: context.baseUrl,
|
|
||||||
eTicketId: consumePayload.eTicketId,
|
|
||||||
oid: consumePayload.oid,
|
|
||||||
formToken: consumePayload.formToken,
|
|
||||||
num: consumePayload.num,
|
|
||||||
storeId: consumePayload.storeId,
|
|
||||||
detailResult,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapKuaishouEticketDetailResult(payload: JsonObject = {}, context: JsonObject = {}) {
|
|
||||||
const normalized = normalizeKuaishouEticketApiResult(payload)
|
|
||||||
const detail = mapKuaishouEticketDetailPayload(payload?.data?.eTicket)
|
|
||||||
const goods = mapKuaishouEticketGoodsPayload(payload?.data?.goods)
|
|
||||||
|
|
||||||
return {
|
|
||||||
baseUrl: String(context.baseUrl || '').trim(),
|
|
||||||
eTicketId: String(context.eTicketId || detail?.eTicketId || '').trim(),
|
|
||||||
ok: normalized.ok,
|
|
||||||
alreadyConsumed: normalized.result === 21,
|
|
||||||
result: normalized.result,
|
|
||||||
errorMessage: normalized.errorMessage,
|
|
||||||
requestId: normalized.requestId,
|
|
||||||
serverTimestamp: normalized.serverTimestamp,
|
|
||||||
detail,
|
|
||||||
goods,
|
|
||||||
raw: payload && typeof payload === 'object' ? payload : {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapKuaishouEticketConsumeResult(payload: JsonObject = {}, context: JsonObject = {}) {
|
|
||||||
const normalized = normalizeKuaishouEticketApiResult(payload)
|
|
||||||
|
|
||||||
return {
|
|
||||||
baseUrl: String(context.baseUrl || '').trim(),
|
|
||||||
eTicketId: String(context.eTicketId || '').trim(),
|
|
||||||
oid: String(context.oid || '').trim(),
|
|
||||||
formToken: String(context.formToken || '').trim(),
|
|
||||||
num: normalizePositiveInteger(context.num, 1),
|
|
||||||
storeId: String(context.storeId || '0').trim() || '0',
|
|
||||||
ok: normalized.ok,
|
|
||||||
consumed: normalized.result === 1 || normalized.result === 21,
|
|
||||||
alreadyConsumed: normalized.result === 21,
|
|
||||||
result: normalized.result,
|
|
||||||
errorMessage: normalized.errorMessage,
|
|
||||||
requestId: normalized.requestId,
|
|
||||||
serverTimestamp: normalized.serverTimestamp,
|
|
||||||
detail: context.detailResult?.detail || null,
|
|
||||||
raw: payload && typeof payload === 'object' ? payload : {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveKuaishouEticketConsumePayload(
|
|
||||||
payload: JsonObject = {},
|
|
||||||
detailResult: KuaishouEticketDetailResult | null = null,
|
|
||||||
) {
|
|
||||||
const detail = detailResult?.detail || null
|
|
||||||
const eTicketId = pickFirstNonEmpty([payload.eTicketId, detail?.eTicketId])
|
|
||||||
const oid = pickFirstNonEmpty([payload.oid, detail?.oid])
|
|
||||||
const formToken = pickFirstNonEmpty([payload.formToken, detail?.formToken])
|
|
||||||
const storeId = pickFirstNonEmpty([payload.storeId, '0'])
|
|
||||||
const num = normalizePositiveInteger(payload.num, normalizePositiveInteger(detail?.leftCount, 1))
|
|
||||||
|
|
||||||
if (!eTicketId) {
|
|
||||||
throw createHttpError('请先填写 eTicketId', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'kuaishou_eticket_missing_eticket_id',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!oid) {
|
|
||||||
throw createHttpError('缺少核销所需 oid,请先查询核销信息', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'kuaishou_eticket_missing_oid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!formToken) {
|
|
||||||
throw createHttpError('缺少核销所需 formToken,请先查询核销信息', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'kuaishou_eticket_missing_form_token',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
eTicketId,
|
|
||||||
num,
|
|
||||||
storeId,
|
|
||||||
oid,
|
|
||||||
formToken,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeKuaishouEticketApiResult(payload: JsonObject = {}) {
|
|
||||||
const result = Number(payload?.result || 0)
|
|
||||||
|
|
||||||
return {
|
|
||||||
ok: result === 1,
|
|
||||||
result,
|
|
||||||
errorMessage: String(payload?.error_msg || '').trim(),
|
|
||||||
requestId: String(payload?.requestId || '').trim(),
|
|
||||||
serverTimestamp: String(payload?.serverTimestamp || '').trim(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapKuaishouEticketDetailPayload(value: unknown): KuaishouEticketDetailPayload | null {
|
|
||||||
if (!isPlainObject(value)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
uid: String(value.uid || '').trim(),
|
|
||||||
fulfillDetailId: String(value.fulfillDetailId || '').trim(),
|
|
||||||
sellerId: String(value.sellerId || '').trim(),
|
|
||||||
formToken: String(value.formToken || '').trim(),
|
|
||||||
validEndTime: String(value.validEndTime || '').trim(),
|
|
||||||
validStartTime: String(value.validStartTime || '').trim(),
|
|
||||||
leftReverseCount: normalizeInteger(value.leftReverseCount, 0),
|
|
||||||
eTicketId: String(value.eTicketId || '').trim(),
|
|
||||||
oid: String(value.oid || '').trim(),
|
|
||||||
totalCount: normalizeInteger(value.totalCount, 0),
|
|
||||||
leftCount: normalizeInteger(value.leftCount, 0),
|
|
||||||
status: String(value.status || '').trim(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapKuaishouEticketGoodsPayload(value: unknown) {
|
|
||||||
if (!isPlainObject(value)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
itemId: String(value.itemId || '').trim(),
|
|
||||||
itemPicUrl: String(value.itemPicUrl || '').trim(),
|
|
||||||
itemTitle: String(value.itemTitle || '').trim(),
|
|
||||||
price: String(value.price || '').trim(),
|
|
||||||
skuDesc: String(value.skuDesc || '').trim(),
|
|
||||||
skuId: String(value.skuId || '').trim(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveKuaishouEticketActionContext(payload: JsonObject = {}) {
|
|
||||||
const config = resolveKuaishouEticketConfig(payload)
|
|
||||||
const eTicketId = String(payload.eTicketId || '').trim()
|
|
||||||
|
|
||||||
if (!config.cookie) {
|
|
||||||
throw createHttpError('请先配置快手小店 Cookie', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'kuaishou_eticket_missing_cookie',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!eTicketId) {
|
|
||||||
throw createHttpError('请先填写 eTicketId', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'kuaishou_eticket_missing_eticket_id',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...config,
|
|
||||||
eTicketId,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldResolveConsumeDetail(payload: JsonObject = {}): boolean {
|
|
||||||
return !String(payload.oid || '').trim() || !String(payload.formToken || '').trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
function pickFirstNonEmpty(values: unknown[]): string {
|
|
||||||
for (const value of values) {
|
|
||||||
const normalized = String(value || '').trim()
|
|
||||||
if (normalized) {
|
|
||||||
return normalized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizePositiveInteger(value: unknown, fallback: number): number {
|
|
||||||
const parsed = Number(value)
|
|
||||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeInteger(value: unknown, fallback: number): number {
|
|
||||||
const parsed = Number(value)
|
|
||||||
return Number.isInteger(parsed) ? parsed : fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPlainObject(value: unknown): value is JsonObject {
|
|
||||||
return Object.prototype.toString.call(value) === '[object Object]'
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
const DEFAULT_BASE_URL = 'https://s.kwaixiaodian.com'
|
|
||||||
const DEFAULT_REFERER_PATH = '/zone/industry/voucher/verify-list'
|
|
||||||
const DEFAULT_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36'
|
|
||||||
|
|
||||||
export type KuaishouEticketConfigInput = {
|
|
||||||
enabled?: boolean
|
|
||||||
baseUrl?: string
|
|
||||||
cookie?: string
|
|
||||||
timeoutMs?: number | string
|
|
||||||
infoPath?: string
|
|
||||||
detailPath?: string
|
|
||||||
consumePath?: string
|
|
||||||
refererPath?: string
|
|
||||||
userAgent?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type KuaishouEticketConfig = {
|
|
||||||
enabled: boolean
|
|
||||||
baseUrl: string
|
|
||||||
cookie: string
|
|
||||||
timeoutMs: number
|
|
||||||
infoPath: string
|
|
||||||
detailPath: string
|
|
||||||
consumePath: string
|
|
||||||
refererPath: string
|
|
||||||
userAgent: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type HeaderInput = {
|
|
||||||
baseUrl?: string
|
|
||||||
refererPath?: string
|
|
||||||
userAgent?: string
|
|
||||||
cookie?: string
|
|
||||||
contentType?: string
|
|
||||||
extra?: Record<string, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveKuaishouEticketConfig(overrides: KuaishouEticketConfigInput = {}): KuaishouEticketConfig {
|
|
||||||
return {
|
|
||||||
enabled: overrides.enabled !== false,
|
|
||||||
baseUrl: normalizeBaseUrl(overrides.baseUrl || DEFAULT_BASE_URL) || DEFAULT_BASE_URL,
|
|
||||||
cookie: String(overrides.cookie || '').trim(),
|
|
||||||
timeoutMs: normalizePositiveInteger(overrides.timeoutMs, 8000),
|
|
||||||
infoPath: normalizePath(overrides.infoPath, '/gateway/pc/workbench/service/stable/info'),
|
|
||||||
detailPath: normalizePath(overrides.detailPath, '/gateway/industry/eticket/consume/detail'),
|
|
||||||
consumePath: normalizePath(overrides.consumePath, '/gateway/industry/eticket/consume'),
|
|
||||||
refererPath: normalizePath(overrides.refererPath, DEFAULT_REFERER_PATH),
|
|
||||||
userAgent: String(overrides.userAgent || DEFAULT_USER_AGENT).trim() || DEFAULT_USER_AGENT,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildKuaishouEticketUrl(baseUrl: unknown, pathname: unknown): URL {
|
|
||||||
return new URL(normalizePath(pathname, '/'), normalizeBaseUrl(baseUrl) || DEFAULT_BASE_URL)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildKuaishouEticketHeaders({
|
|
||||||
baseUrl = DEFAULT_BASE_URL,
|
|
||||||
refererPath = DEFAULT_REFERER_PATH,
|
|
||||||
userAgent = DEFAULT_USER_AGENT,
|
|
||||||
cookie = '',
|
|
||||||
contentType = 'application/json',
|
|
||||||
extra = {},
|
|
||||||
}: HeaderInput = {}): Record<string, string> {
|
|
||||||
return {
|
|
||||||
accept: 'application/json',
|
|
||||||
'accept-language': 'zh-CN,zh;q=0.9',
|
|
||||||
...(contentType ? { 'content-type': contentType } : {}),
|
|
||||||
kpf: 'PC_WEB',
|
|
||||||
kpn: 'KWAIXIAODIAN',
|
|
||||||
origin: normalizeBaseUrl(baseUrl) || DEFAULT_BASE_URL,
|
|
||||||
referer: buildKuaishouEticketUrl(baseUrl, refererPath).toString(),
|
|
||||||
'user-agent': String(userAgent || DEFAULT_USER_AGENT).trim() || DEFAULT_USER_AGENT,
|
|
||||||
...(String(cookie || '').trim() ? { cookie: String(cookie || '').trim() } : {}),
|
|
||||||
...normalizeHeaderMap(extra),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeBaseUrl(value: unknown): string {
|
|
||||||
return String(value || '').trim().replace(/\/+$/, '')
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizePath(value: unknown, fallback: string): string {
|
|
||||||
const normalized = String(value || '').trim()
|
|
||||||
if (!normalized) {
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalized.startsWith('/') ? normalized : `/${normalized}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizePositiveInteger(value: unknown, fallback: number): number {
|
|
||||||
const parsed = Number(value)
|
|
||||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeHeaderMap(extra: unknown): Record<string, string> {
|
|
||||||
if (!extra || typeof extra !== 'object') {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(extra)
|
|
||||||
.map(([key, value]) => [String(key || '').trim().toLowerCase(), String(value || '').trim()])
|
|
||||||
.filter(([key, value]) => key && value),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
import http from 'node:http'
|
|
||||||
import https from 'node:https'
|
|
||||||
import type { IncomingHttpHeaders } from 'node:http'
|
|
||||||
|
|
||||||
import { parseJsonObject } from '../../../utils/json.js'
|
|
||||||
import { createHttpError } from '../../../utils/http.js'
|
|
||||||
import { logInfo } from '../../../utils/logger.js'
|
|
||||||
import { buildKuaishouEticketHeaders, buildKuaishouEticketUrl, resolveKuaishouEticketConfig } from './helpers.js'
|
|
||||||
|
|
||||||
type JsonObject = Record<string, any>
|
|
||||||
|
|
||||||
type KuaishouEticketRequestOptions = JsonObject & {
|
|
||||||
method?: string
|
|
||||||
body?: unknown
|
|
||||||
headers?: Record<string, unknown>
|
|
||||||
timeoutMs?: number | string
|
|
||||||
cookie?: string
|
|
||||||
contentType?: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
type HeaderAdapter = {
|
|
||||||
get(name: string): string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
type NodeHttpResponse = {
|
|
||||||
ok: boolean
|
|
||||||
status: number
|
|
||||||
headers: HeaderAdapter
|
|
||||||
bodyText: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function kuaishouEticketRequest(pathname: string, options: KuaishouEticketRequestOptions = {}) {
|
|
||||||
const config = resolveKuaishouEticketConfig(options)
|
|
||||||
const url = buildKuaishouEticketUrl(config.baseUrl, pathname)
|
|
||||||
const timeoutMs = Number(options.timeoutMs || config.timeoutMs || 8000)
|
|
||||||
const controller = new AbortController()
|
|
||||||
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
||||||
const method = String(options.method || 'POST').trim().toUpperCase()
|
|
||||||
const headers = buildKuaishouEticketHeaders({
|
|
||||||
baseUrl: config.baseUrl,
|
|
||||||
refererPath: config.refererPath,
|
|
||||||
userAgent: config.userAgent,
|
|
||||||
cookie: options.cookie ?? config.cookie,
|
|
||||||
contentType: options.contentType === null ? '' : options.contentType || inferContentType(options.body),
|
|
||||||
...(options.headers !== undefined ? { extra: options.headers } : {}),
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
|
||||||
const body = normalizeRequestBody(options.body, headers['content-type'])
|
|
||||||
const response = await requestViaNodeHttp(url, {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
signal: controller.signal,
|
|
||||||
...(body !== undefined ? { body } : {}),
|
|
||||||
})
|
|
||||||
|
|
||||||
const rawText = response.bodyText
|
|
||||||
const payload = parseJsonObject(rawText, { preserveLargeIntegers: true })
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw createHttpError(`快手小店核销请求失败,HTTP ${response.status}`, {
|
|
||||||
statusCode: 502,
|
|
||||||
errorCode: 'kuaishou_eticket_http_error',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
logInfo('[kuaishou-eticket/http]', '请求完成', {
|
|
||||||
method,
|
|
||||||
pathname,
|
|
||||||
status: response.status,
|
|
||||||
result: Number(payload?.result || 0),
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
url: url.toString(),
|
|
||||||
status: response.status,
|
|
||||||
headers: response.headers,
|
|
||||||
payload,
|
|
||||||
rawText,
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error && error.name === 'AbortError') {
|
|
||||||
throw createHttpError(`快手小店核销请求超时(${timeoutMs}ms)`, {
|
|
||||||
statusCode: 504,
|
|
||||||
errorCode: 'kuaishou_eticket_request_timeout',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
throw error
|
|
||||||
} finally {
|
|
||||||
clearTimeout(timer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function inferContentType(body: unknown): string {
|
|
||||||
if (body == null) {
|
|
||||||
return ''
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof body === 'string') {
|
|
||||||
return 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeRequestBody(body: unknown, contentType: unknown): string | undefined {
|
|
||||||
if (body == null) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof body === 'string') {
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
|
|
||||||
if (String(contentType || '').includes('application/json')) {
|
|
||||||
return JSON.stringify(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function requestViaNodeHttp(
|
|
||||||
url: URL,
|
|
||||||
{ method, headers, body, signal }: {
|
|
||||||
method: string
|
|
||||||
headers: Record<string, string>
|
|
||||||
body?: string
|
|
||||||
signal: AbortSignal
|
|
||||||
},
|
|
||||||
): Promise<NodeHttpResponse> {
|
|
||||||
const isHttps = url.protocol === 'https:'
|
|
||||||
const transport = isHttps ? https : http
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const request = transport.request(url, {
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
rejectUnauthorized: false,
|
|
||||||
}, (response) => {
|
|
||||||
const chunks: Buffer[] = []
|
|
||||||
|
|
||||||
response.on('data', (chunk) => {
|
|
||||||
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
|
|
||||||
})
|
|
||||||
|
|
||||||
response.on('end', () => {
|
|
||||||
const bodyText = Buffer.concat(chunks).toString('utf8')
|
|
||||||
resolve({
|
|
||||||
ok: Number(response.statusCode || 0) >= 200 && Number(response.statusCode || 0) < 300,
|
|
||||||
status: Number(response.statusCode || 0),
|
|
||||||
headers: createHeaderAdapter(response.headers),
|
|
||||||
bodyText,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
request.on('error', reject)
|
|
||||||
|
|
||||||
if (signal) {
|
|
||||||
if (signal.aborted) {
|
|
||||||
const error = new Error('Request aborted')
|
|
||||||
error.name = 'AbortError'
|
|
||||||
request.destroy(error)
|
|
||||||
} else {
|
|
||||||
signal.addEventListener('abort', () => {
|
|
||||||
const error = new Error('Request aborted')
|
|
||||||
error.name = 'AbortError'
|
|
||||||
request.destroy(error)
|
|
||||||
}, { once: true })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof body !== 'undefined') {
|
|
||||||
request.write(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
request.end()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function createHeaderAdapter(headers: IncomingHttpHeaders): HeaderAdapter {
|
|
||||||
const normalized = new Map<string, string>()
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(headers || {})) {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
normalized.set(String(key || '').toLowerCase(), value.join(', '))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
normalized.set(String(key || '').toLowerCase(), value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
get(name: string) {
|
|
||||||
return normalized.get(String(name || '').toLowerCase()) || null
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import test from 'node:test'
|
|
||||||
import assert from 'node:assert/strict'
|
|
||||||
|
|
||||||
import { mapKuaishouEticketStableInfoResult } from './info-service.js'
|
|
||||||
|
|
||||||
test('mapKuaishouEticketStableInfoResult extracts shop info from payload', () => {
|
|
||||||
const result = mapKuaishouEticketStableInfoResult({
|
|
||||||
result: 1,
|
|
||||||
error_msg: '成功',
|
|
||||||
requestId: '777795601162094230',
|
|
||||||
data: {
|
|
||||||
userInfo: {
|
|
||||||
userId: 4269276762,
|
|
||||||
userName: '稚嫩游戏交易店',
|
|
||||||
userAvatar: 'https://example.com/avatar.jpg',
|
|
||||||
},
|
|
||||||
bizStatus: {
|
|
||||||
settleStatus: 200,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
baseUrl: 'https://s.kwaixiaodian.com',
|
|
||||||
cookie: 'sid=abc',
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.equal(result.ok, true)
|
|
||||||
assert.equal(result.shop.shopId, '4269276762')
|
|
||||||
assert.equal(result.shop.kshopName, '稚嫩游戏交易店')
|
|
||||||
assert.equal(result.shop.userAvatar, 'https://example.com/avatar.jpg')
|
|
||||||
assert.equal(result.shop.settleStatus, 200)
|
|
||||||
assert.equal(result.shop.hasCookie, true)
|
|
||||||
})
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { createHttpError } from '../../../utils/http.js'
|
|
||||||
import { kuaishouEticketRequest } from './http-client.js'
|
|
||||||
import { resolveKuaishouEticketConfig } from './helpers.js'
|
|
||||||
|
|
||||||
type JsonObject = Record<string, any>
|
|
||||||
|
|
||||||
export async function queryKuaishouEticketStableInfo(payload: JsonObject = {}) {
|
|
||||||
const config = resolveKuaishouEticketConfig(payload)
|
|
||||||
const cookie = String(payload.cookie || config.cookie || '').trim()
|
|
||||||
|
|
||||||
if (!cookie) {
|
|
||||||
throw createHttpError('请先提供店铺 Cookie', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'kuaishou_eticket_missing_cookie',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await kuaishouEticketRequest(config.infoPath, {
|
|
||||||
method: 'GET',
|
|
||||||
baseUrl: config.baseUrl,
|
|
||||||
cookie,
|
|
||||||
timeoutMs: config.timeoutMs,
|
|
||||||
contentType: null,
|
|
||||||
})
|
|
||||||
|
|
||||||
return mapKuaishouEticketStableInfoResult(response.payload, {
|
|
||||||
baseUrl: config.baseUrl,
|
|
||||||
cookie,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mapKuaishouEticketStableInfoResult(payload: JsonObject = {}, context: JsonObject = {}) {
|
|
||||||
const result = Number(payload?.result || 0)
|
|
||||||
const userInfo = isPlainObject(payload?.data?.userInfo) ? payload.data.userInfo : {}
|
|
||||||
const bizStatus = isPlainObject(payload?.data?.bizStatus) ? payload.data.bizStatus : {}
|
|
||||||
|
|
||||||
return {
|
|
||||||
baseUrl: String(context.baseUrl || '').trim(),
|
|
||||||
ok: result === 1,
|
|
||||||
result,
|
|
||||||
errorMessage: String(payload?.error_msg || '').trim(),
|
|
||||||
requestId: String(payload?.requestId || '').trim(),
|
|
||||||
shop: {
|
|
||||||
shopId: String(userInfo.userId || '').trim(),
|
|
||||||
kshopName: String(userInfo.userName || '').trim(),
|
|
||||||
userAvatar: String(userInfo.userAvatar || '').trim(),
|
|
||||||
settleStatus: normalizeInteger(bizStatus.settleStatus, 0),
|
|
||||||
hasCookie: Boolean(String(context.cookie || '').trim()),
|
|
||||||
},
|
|
||||||
raw: payload && typeof payload === 'object' ? payload : {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeInteger(value: unknown, fallback: number): number {
|
|
||||||
const parsed = Number(value)
|
|
||||||
return Number.isInteger(parsed) ? parsed : fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPlainObject(value: unknown): value is JsonObject {
|
|
||||||
return Object.prototype.toString.call(value) === '[object Object]'
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
import process from 'node:process'
|
|
||||||
|
|
||||||
import { isProductionLike } from '../../../config/runtime-validation.js'
|
|
||||||
|
|
||||||
type RuntimeEnvironment = {
|
|
||||||
NODE_ENV?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type MockTicketDetailOptions = {
|
|
||||||
eTicketId: string
|
|
||||||
baseUrl?: string
|
|
||||||
oid?: string
|
|
||||||
formToken?: string
|
|
||||||
leftCount?: number
|
|
||||||
goodsTitle?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type MockTicketConsumeOptions = {
|
|
||||||
eTicketId: string
|
|
||||||
baseUrl?: string
|
|
||||||
oid?: string
|
|
||||||
formToken?: string
|
|
||||||
num?: number
|
|
||||||
storeId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isKuaishouEticketMockTicketCode(
|
|
||||||
ticketCode: unknown,
|
|
||||||
env: RuntimeEnvironment = process.env,
|
|
||||||
): boolean {
|
|
||||||
const normalized = String(ticketCode || '').trim().toUpperCase()
|
|
||||||
|
|
||||||
if (!normalized || isProductionLike(env)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalized === 'MOCK' || normalized.startsWith('MOCK-') || normalized.startsWith('MOCK_')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildMockKuaishouEticketDetailResult(options: MockTicketDetailOptions) {
|
|
||||||
const eTicketId = String(options.eTicketId || '').trim()
|
|
||||||
const oid = String(options.oid || `MOCK-OID-${eTicketId}`).trim()
|
|
||||||
const formToken = String(options.formToken || `MOCK-FORM-${eTicketId}`).trim()
|
|
||||||
const leftCount = normalizePositiveInteger(options.leftCount, 1)
|
|
||||||
const goodsTitle = String(options.goodsTitle || 'MOCK 快手商品').trim()
|
|
||||||
|
|
||||||
return {
|
|
||||||
baseUrl: String(options.baseUrl || 'mock://kuaishou-eticket').trim(),
|
|
||||||
eTicketId,
|
|
||||||
ok: true,
|
|
||||||
alreadyConsumed: false,
|
|
||||||
result: 1,
|
|
||||||
errorMessage: '',
|
|
||||||
requestId: `mock-${Date.now()}`,
|
|
||||||
serverTimestamp: new Date().toISOString(),
|
|
||||||
detail: {
|
|
||||||
uid: 'mock',
|
|
||||||
fulfillDetailId: `MOCK-FULFILL-${eTicketId}`,
|
|
||||||
sellerId: 'mock',
|
|
||||||
formToken,
|
|
||||||
validEndTime: '',
|
|
||||||
validStartTime: '',
|
|
||||||
leftReverseCount: 0,
|
|
||||||
eTicketId,
|
|
||||||
oid,
|
|
||||||
totalCount: leftCount,
|
|
||||||
leftCount,
|
|
||||||
status: 'MOCK',
|
|
||||||
},
|
|
||||||
goods: {
|
|
||||||
itemId: 'mock',
|
|
||||||
itemPicUrl: '',
|
|
||||||
itemTitle: goodsTitle,
|
|
||||||
price: '',
|
|
||||||
skuDesc: goodsTitle,
|
|
||||||
skuId: 'mock',
|
|
||||||
},
|
|
||||||
raw: {
|
|
||||||
mock: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildMockKuaishouEticketConsumeResult(options: MockTicketConsumeOptions) {
|
|
||||||
const eTicketId = String(options.eTicketId || '').trim()
|
|
||||||
const oid = String(options.oid || `MOCK-OID-${eTicketId}`).trim()
|
|
||||||
const formToken = String(options.formToken || `MOCK-FORM-${eTicketId}`).trim()
|
|
||||||
const num = normalizePositiveInteger(options.num, 1)
|
|
||||||
|
|
||||||
return {
|
|
||||||
baseUrl: String(options.baseUrl || 'mock://kuaishou-eticket').trim(),
|
|
||||||
eTicketId,
|
|
||||||
oid,
|
|
||||||
formToken,
|
|
||||||
num,
|
|
||||||
storeId: String(options.storeId || '0').trim() || '0',
|
|
||||||
ok: true,
|
|
||||||
consumed: true,
|
|
||||||
alreadyConsumed: false,
|
|
||||||
result: 1,
|
|
||||||
errorMessage: '',
|
|
||||||
requestId: `mock-${Date.now()}`,
|
|
||||||
serverTimestamp: new Date().toISOString(),
|
|
||||||
detail: {
|
|
||||||
eTicketId,
|
|
||||||
oid,
|
|
||||||
formToken,
|
|
||||||
leftCount: num,
|
|
||||||
},
|
|
||||||
raw: {
|
|
||||||
mock: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizePositiveInteger(value: unknown, fallback: number) {
|
|
||||||
const normalized = Number(value)
|
|
||||||
if (!Number.isFinite(normalized) || normalized <= 0) {
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.floor(normalized)
|
|
||||||
}
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
import path from 'node:path'
|
|
||||||
|
|
||||||
import { PROJECT_ROOT } from '../../../config/runtime.js'
|
|
||||||
import { readJsonFile, writeJsonFile } from '../../../utils/json-file-store.js'
|
|
||||||
|
|
||||||
const KUAISHOU_ETICKET_SOURCE_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'kuaishou-eticket-source.json')
|
|
||||||
const DEFAULT_BASE_URL = 'https://s.kwaixiaodian.com'
|
|
||||||
|
|
||||||
type JsonObject = Record<string, any>
|
|
||||||
|
|
||||||
export type KuaishouEticketShopConfig = {
|
|
||||||
shopId: string
|
|
||||||
kshopName: string
|
|
||||||
cookie: string
|
|
||||||
userAvatar: string
|
|
||||||
enabled: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type KuaishouEticketSourceConfig = {
|
|
||||||
enabled: boolean
|
|
||||||
baseUrl: string
|
|
||||||
shops: KuaishouEticketShopConfig[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getKuaishouEticketSourceFilePath(): string {
|
|
||||||
return KUAISHOU_ETICKET_SOURCE_FILE_PATH
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getKuaishouEticketSourceConfig(): KuaishouEticketSourceConfig {
|
|
||||||
return loadKuaishouEticketSourceConfigFromFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveKuaishouEticketSourceConfig(rawValue: unknown): KuaishouEticketSourceConfig {
|
|
||||||
return writeJsonFile(
|
|
||||||
KUAISHOU_ETICKET_SOURCE_FILE_PATH,
|
|
||||||
rawValue,
|
|
||||||
normalizeKuaishouEticketSourceConfig,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function listKuaishouEticketShopConfigs(source: KuaishouEticketSourceConfig = getKuaishouEticketSourceConfig()): KuaishouEticketShopConfig[] {
|
|
||||||
return Array.isArray(source?.shops) ? source.shops : []
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findKuaishouEticketShopConfig(shopId: unknown, source: KuaishouEticketSourceConfig = getKuaishouEticketSourceConfig()): KuaishouEticketShopConfig | null {
|
|
||||||
const normalizedShopId = String(shopId || '').trim()
|
|
||||||
if (!normalizedShopId) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return listKuaishouEticketShopConfigs(source).find((item) => String(item.shopId || '').trim() === normalizedShopId) || null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findKuaishouEticketShopConfigByName(kshopName: unknown, source: KuaishouEticketSourceConfig = getKuaishouEticketSourceConfig()): KuaishouEticketShopConfig | null {
|
|
||||||
const normalizedName = String(kshopName || '').trim()
|
|
||||||
if (!normalizedName) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return listKuaishouEticketShopConfigs(source).find((item) => String(item.kshopName || '').trim() === normalizedName) || null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resolveKuaishouEticketShopConfig(
|
|
||||||
{ shopId = '', kshopName = '', shopName = '' }: { shopId?: unknown, kshopName?: unknown, shopName?: unknown } = {},
|
|
||||||
source: KuaishouEticketSourceConfig = getKuaishouEticketSourceConfig(),
|
|
||||||
): KuaishouEticketShopConfig | null {
|
|
||||||
const byId = findKuaishouEticketShopConfig(shopId, source)
|
|
||||||
if (byId) {
|
|
||||||
return byId
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedName = String(kshopName || shopName || '').trim()
|
|
||||||
if (!normalizedName) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return findKuaishouEticketShopConfigByName(normalizedName, source)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getFirstAvailableKuaishouEticketShop(source: KuaishouEticketSourceConfig = getKuaishouEticketSourceConfig()): KuaishouEticketShopConfig | null {
|
|
||||||
return listKuaishouEticketShopConfigs(source).find((item) => item.enabled !== false && String(item.cookie || '').trim()) || null
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadKuaishouEticketSourceConfigFromFile(): KuaishouEticketSourceConfig {
|
|
||||||
return readJsonFile(
|
|
||||||
KUAISHOU_ETICKET_SOURCE_FILE_PATH,
|
|
||||||
createDefaultKuaishouEticketSourceConfig,
|
|
||||||
normalizeKuaishouEticketSourceConfig,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeKuaishouEticketSourceConfig(rawValue: unknown): KuaishouEticketSourceConfig {
|
|
||||||
const source = isPlainObject(rawValue) ? rawValue : {}
|
|
||||||
const shops = Array.isArray(source.shops)
|
|
||||||
? source.shops
|
|
||||||
: buildLegacySingleShopList(source)
|
|
||||||
|
|
||||||
return {
|
|
||||||
enabled: typeof source.enabled === 'boolean' ? source.enabled : true,
|
|
||||||
baseUrl: String(source.baseUrl || DEFAULT_BASE_URL).trim() || DEFAULT_BASE_URL,
|
|
||||||
shops: shops
|
|
||||||
.map((item) => normalizeKuaishouEticketShopConfig(item))
|
|
||||||
.filter((item): item is KuaishouEticketShopConfig => Boolean(item)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeKuaishouEticketShopConfig(rawValue: unknown): KuaishouEticketShopConfig | null {
|
|
||||||
if (!isPlainObject(rawValue)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const shopId = String(rawValue.shopId || rawValue.userId || '').trim()
|
|
||||||
const kshopName = String(rawValue.kshopName || rawValue.shopName || rawValue.userName || '').trim()
|
|
||||||
const cookie = String(rawValue.cookie || '').trim()
|
|
||||||
const userAvatar = String(rawValue.userAvatar || '').trim()
|
|
||||||
const enabled = typeof rawValue.enabled === 'boolean' ? rawValue.enabled : true
|
|
||||||
|
|
||||||
if (!shopId && !kshopName && !cookie) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
shopId,
|
|
||||||
kshopName,
|
|
||||||
cookie,
|
|
||||||
userAvatar,
|
|
||||||
enabled,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildLegacySingleShopList(source: JsonObject): JsonObject[] {
|
|
||||||
const cookie = String(source.cookie || '').trim()
|
|
||||||
if (!cookie) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
shopId: '',
|
|
||||||
kshopName: '',
|
|
||||||
cookie,
|
|
||||||
userAvatar: '',
|
|
||||||
enabled: source.enabled !== false,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDefaultKuaishouEticketSourceConfig(): KuaishouEticketSourceConfig {
|
|
||||||
return {
|
|
||||||
enabled: true,
|
|
||||||
baseUrl: DEFAULT_BASE_URL,
|
|
||||||
shops: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPlainObject(value: unknown): value is JsonObject {
|
|
||||||
return Object.prototype.toString.call(value) === '[object Object]'
|
|
||||||
}
|
|
||||||
@@ -24,15 +24,10 @@ import type {
|
|||||||
AdminCloudtentaclesTestLoginInput,
|
AdminCloudtentaclesTestLoginInput,
|
||||||
AdminCloudtentaclesValidateSessionInput,
|
AdminCloudtentaclesValidateSessionInput,
|
||||||
AdminCloudtentaclesVirtualNumberInput,
|
AdminCloudtentaclesVirtualNumberInput,
|
||||||
AdminKuaishouEticketConsumeInput,
|
|
||||||
AdminKuaishouEticketDetailQueryInput,
|
|
||||||
AdminKuaishouEticketShopInfoInput,
|
|
||||||
AdminKuaishouEticketSourceConfigInput,
|
|
||||||
AdminNotificationConfigInput,
|
AdminNotificationConfigInput,
|
||||||
AdminNotificationTestInput,
|
AdminNotificationTestInput,
|
||||||
AdminScheduledJobsConfigInput,
|
AdminScheduledJobsConfigInput,
|
||||||
AdminTaskKuaishouIndustryConsumeInput,
|
AdminTaskKuaishouIndustryConsumeInput,
|
||||||
AdminTaskKuaishouCloudDispatchInput,
|
|
||||||
AdminTaskManualDispatchInput,
|
AdminTaskManualDispatchInput,
|
||||||
} from './write-inputs.js'
|
} from './write-inputs.js'
|
||||||
|
|
||||||
@@ -40,7 +35,6 @@ export type AdminOrderRouteQuery = AdminOrderListQueryInput
|
|||||||
export type AdminTaskRouteQuery = AdminTaskListQueryInput
|
export type AdminTaskRouteQuery = AdminTaskListQueryInput
|
||||||
export type AdminRouteAdminSession = AdminViewerSessionInput
|
export type AdminRouteAdminSession = AdminViewerSessionInput
|
||||||
|
|
||||||
export type AdminKuaishouEticketSourceConfigRouteBody = AdminKuaishouEticketSourceConfigInput
|
|
||||||
export type AdminKuaishouIndustrySourceConfigRouteBody = AdminKuaishouIndustrySourceConfigInput
|
export type AdminKuaishouIndustrySourceConfigRouteBody = AdminKuaishouIndustrySourceConfigInput
|
||||||
export type AdminKuaishouIndustryAuthorizationCodeRouteBody = AdminKuaishouIndustryAuthorizationCodeInput
|
export type AdminKuaishouIndustryAuthorizationCodeRouteBody = AdminKuaishouIndustryAuthorizationCodeInput
|
||||||
export type AdminKuaishouIndustryRefundListRouteBody = AdminKuaishouIndustryRefundListInput
|
export type AdminKuaishouIndustryRefundListRouteBody = AdminKuaishouIndustryRefundListInput
|
||||||
@@ -53,9 +47,6 @@ export type AdminKuaishouIndustryVoucherResendRouteBody = AdminKuaishouIndustryV
|
|||||||
export type AdminNotificationConfigRouteBody = AdminNotificationConfigInput
|
export type AdminNotificationConfigRouteBody = AdminNotificationConfigInput
|
||||||
export type AdminNotificationTestRouteBody = AdminNotificationTestInput
|
export type AdminNotificationTestRouteBody = AdminNotificationTestInput
|
||||||
export type AdminScheduledJobsConfigRouteBody = AdminScheduledJobsConfigInput
|
export type AdminScheduledJobsConfigRouteBody = AdminScheduledJobsConfigInput
|
||||||
export type AdminKuaishouEticketDetailQueryRouteBody = AdminKuaishouEticketDetailQueryInput
|
|
||||||
export type AdminKuaishouEticketConsumeRouteBody = AdminKuaishouEticketConsumeInput
|
|
||||||
export type AdminKuaishouEticketShopInfoRouteBody = AdminKuaishouEticketShopInfoInput
|
|
||||||
export type AdminCloudtentaclesSourceConfigRouteBody = AdminCloudtentaclesSourceConfigInput
|
export type AdminCloudtentaclesSourceConfigRouteBody = AdminCloudtentaclesSourceConfigInput
|
||||||
export type AdminCloudtentaclesOverrideRuleConfigRouteBody = AdminCloudtentaclesOverrideRuleConfigInput
|
export type AdminCloudtentaclesOverrideRuleConfigRouteBody = AdminCloudtentaclesOverrideRuleConfigInput
|
||||||
export type AdminCloudtentaclesSendSmsCodeRouteBody = AdminCloudtentaclesSendSmsCodeInput
|
export type AdminCloudtentaclesSendSmsCodeRouteBody = AdminCloudtentaclesSendSmsCodeInput
|
||||||
@@ -68,7 +59,6 @@ export type AdminCloudtentaclesSkuUseRouteBody = AdminCloudtentaclesSkuUseInput
|
|||||||
export type AdminCloudtentaclesVirtualNumberRouteBody = AdminCloudtentaclesVirtualNumberInput
|
export type AdminCloudtentaclesVirtualNumberRouteBody = AdminCloudtentaclesVirtualNumberInput
|
||||||
export type AdminCloudtentaclesFullFlowRouteBody = AdminCloudtentaclesFullFlowInput
|
export type AdminCloudtentaclesFullFlowRouteBody = AdminCloudtentaclesFullFlowInput
|
||||||
export type AdminTaskManualDispatchRouteBody = AdminTaskManualDispatchInput
|
export type AdminTaskManualDispatchRouteBody = AdminTaskManualDispatchInput
|
||||||
export type AdminTaskKuaishouCloudDispatchRouteBody = AdminTaskKuaishouCloudDispatchInput
|
|
||||||
export type AdminTaskKuaishouIndustryConsumeRouteBody = AdminTaskKuaishouIndustryConsumeInput
|
export type AdminTaskKuaishouIndustryConsumeRouteBody = AdminTaskKuaishouIndustryConsumeInput
|
||||||
|
|
||||||
export type AdminOrderRouteParams = { orderId?: string }
|
export type AdminOrderRouteParams = { orderId?: string }
|
||||||
|
|||||||
@@ -1,17 +1,3 @@
|
|||||||
export type AdminKuaishouEticketShopConfigWriteItemInput = {
|
|
||||||
shopId?: string
|
|
||||||
kshopName?: string
|
|
||||||
cookie?: string
|
|
||||||
userAvatar?: string
|
|
||||||
enabled?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AdminKuaishouEticketSourceConfigInput = {
|
|
||||||
enabled?: boolean
|
|
||||||
baseUrl?: string
|
|
||||||
shops?: AdminKuaishouEticketShopConfigWriteItemInput[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AdminKuaishouIndustryShopConfigInput = {
|
export type AdminKuaishouIndustryShopConfigInput = {
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
sellerId?: string
|
sellerId?: string
|
||||||
@@ -122,30 +108,6 @@ export type AdminScheduledJobsConfigInput = {
|
|||||||
jobs?: AdminScheduledJobInput[]
|
jobs?: AdminScheduledJobInput[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AdminKuaishouEticketShopInfoInput = {
|
|
||||||
baseUrl?: string
|
|
||||||
shopId?: string
|
|
||||||
cookie?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AdminKuaishouEticketDetailQueryInput = {
|
|
||||||
baseUrl?: string
|
|
||||||
shopId?: string
|
|
||||||
cookie?: string
|
|
||||||
eTicketId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AdminKuaishouEticketConsumeInput = {
|
|
||||||
baseUrl?: string
|
|
||||||
shopId?: string
|
|
||||||
cookie?: string
|
|
||||||
eTicketId?: string
|
|
||||||
oid?: string
|
|
||||||
formToken?: string
|
|
||||||
num?: number | string
|
|
||||||
storeId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AdminCloudtentaclesSourceConfigInput = {
|
export type AdminCloudtentaclesSourceConfigInput = {
|
||||||
sourceKey?: string
|
sourceKey?: string
|
||||||
enabled?: boolean
|
enabled?: boolean
|
||||||
@@ -274,10 +236,6 @@ export type AdminTaskManualDispatchInput = {
|
|||||||
deliveredCredential?: string
|
deliveredCredential?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AdminTaskKuaishouCloudDispatchInput = {
|
|
||||||
ticketCode?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AdminTaskKuaishouIndustryConsumeInput = {
|
export type AdminTaskKuaishouIndustryConsumeInput = {
|
||||||
consumeType?: string
|
consumeType?: string
|
||||||
storeName?: string
|
storeName?: string
|
||||||
|
|||||||
@@ -1088,11 +1088,11 @@ function KuaishouCloudPanel({
|
|||||||
const checklist = [
|
const checklist = [
|
||||||
{
|
{
|
||||||
key: 'ticket',
|
key: 'ticket',
|
||||||
label: '核销码校验',
|
label: '凭证确认',
|
||||||
status: flow.ticket.status || 'pending',
|
status: flow.ticket.status || 'pending',
|
||||||
detail: flow.ticket.verifiedAt
|
detail: flow.ticket.verifiedAt
|
||||||
? `已于 ${formatAdminDateTime(flow.ticket.verifiedAt)} 校验`
|
? `已于 ${formatAdminDateTime(flow.ticket.verifiedAt)} 确认`
|
||||||
: '等待客户提交并校验核销码',
|
: '等待电子凭证同步确认',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'bind',
|
key: 'bind',
|
||||||
@@ -1129,11 +1129,11 @@ function KuaishouCloudPanel({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'consume',
|
key: 'consume',
|
||||||
label: '快手核销',
|
label: '电子凭证收口',
|
||||||
status: flow.consume.status || 'pending',
|
status: flow.consume.status || 'pending',
|
||||||
detail: flow.consume.consumedAt
|
detail: flow.consume.consumedAt
|
||||||
? `已于 ${formatAdminDateTime(flow.consume.consumedAt)} 完成核销`
|
? `已于 ${formatAdminDateTime(flow.consume.consumedAt)} 完成收口`
|
||||||
: flow.consume.errorMessage || '等待退号后核销收口',
|
: flow.consume.errorMessage || '等待退号后收口',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Avatar,
|
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Empty,
|
Empty,
|
||||||
@@ -39,20 +38,16 @@ import {
|
|||||||
fetchAdminCloudtentaclesAsset,
|
fetchAdminCloudtentaclesAsset,
|
||||||
fetchAdminCloudtentaclesSkuList,
|
fetchAdminCloudtentaclesSkuList,
|
||||||
fetchAdminCloudtentaclesSourceConfig,
|
fetchAdminCloudtentaclesSourceConfig,
|
||||||
fetchAdminKuaishouEticketSourceConfig,
|
|
||||||
fetchAdminKuaishouFeifeiConfig,
|
fetchAdminKuaishouFeifeiConfig,
|
||||||
fetchAdminKuaishouIndustrySourceConfig,
|
fetchAdminKuaishouIndustrySourceConfig,
|
||||||
fetchAdminNinetyoneOrders,
|
fetchAdminNinetyoneOrders,
|
||||||
fetchAdminNotificationConfig,
|
fetchAdminNotificationConfig,
|
||||||
fetchAdminScheduledJobsConfig,
|
fetchAdminScheduledJobsConfig,
|
||||||
matchAdminKuaishouFeifeiProduct,
|
matchAdminKuaishouFeifeiProduct,
|
||||||
queryAdminKuaishouEticketDetail,
|
|
||||||
queryAdminKuaishouEticketShopInfo,
|
|
||||||
refreshAdminKuaishouIndustryAccessToken,
|
refreshAdminKuaishouIndustryAccessToken,
|
||||||
retryAdminNinetyoneOrder,
|
retryAdminNinetyoneOrder,
|
||||||
runAdminScheduledJob,
|
runAdminScheduledJob,
|
||||||
saveAdminCloudtentaclesSourceConfig,
|
saveAdminCloudtentaclesSourceConfig,
|
||||||
saveAdminKuaishouEticketSourceConfig,
|
|
||||||
saveAdminKuaishouFeifeiConfig,
|
saveAdminKuaishouFeifeiConfig,
|
||||||
saveAdminKuaishouIndustrySourceConfig,
|
saveAdminKuaishouIndustrySourceConfig,
|
||||||
saveAdminNotificationConfig,
|
saveAdminNotificationConfig,
|
||||||
@@ -66,10 +61,6 @@ import {
|
|||||||
import type {
|
import type {
|
||||||
AdminCloudtentaclesSourceConfigResponse,
|
AdminCloudtentaclesSourceConfigResponse,
|
||||||
AdminCloudtentaclesSourceItem,
|
AdminCloudtentaclesSourceItem,
|
||||||
AdminKuaishouEticketDetailResult,
|
|
||||||
AdminKuaishouEticketShopConfigItem,
|
|
||||||
AdminKuaishouEticketShopInfoResult,
|
|
||||||
AdminKuaishouEticketSourceConfig,
|
|
||||||
AdminKuaishouIndustryConfigResponse,
|
AdminKuaishouIndustryConfigResponse,
|
||||||
AdminKuaishouIndustryShopConfig,
|
AdminKuaishouIndustryShopConfig,
|
||||||
AdminKuaishouIndustrySourceConfig,
|
AdminKuaishouIndustrySourceConfig,
|
||||||
@@ -91,7 +82,6 @@ import { formatAdminDateTime } from '@/utils/admin-time'
|
|||||||
type PlatformTab =
|
type PlatformTab =
|
||||||
| 'ninetyone'
|
| 'ninetyone'
|
||||||
| 'notifications'
|
| 'notifications'
|
||||||
| 'kuaishouEticket'
|
|
||||||
| 'kuaishouIndustry'
|
| 'kuaishouIndustry'
|
||||||
| 'kuaishouFeifei'
|
| 'kuaishouFeifei'
|
||||||
| 'cloudtentacles'
|
| 'cloudtentacles'
|
||||||
@@ -119,7 +109,6 @@ type ScheduledJobsState = {
|
|||||||
const platformTabs: PlatformTab[] = [
|
const platformTabs: PlatformTab[] = [
|
||||||
'ninetyone',
|
'ninetyone',
|
||||||
'notifications',
|
'notifications',
|
||||||
'kuaishouEticket',
|
|
||||||
'kuaishouIndustry',
|
'kuaishouIndustry',
|
||||||
'kuaishouFeifei',
|
'kuaishouFeifei',
|
||||||
'cloudtentacles',
|
'cloudtentacles',
|
||||||
@@ -132,10 +121,6 @@ export default function AdminPlatformShopsPage() {
|
|||||||
const [errorMessage, setErrorMessage] = useState('')
|
const [errorMessage, setErrorMessage] = useState('')
|
||||||
const [notificationConfig, setNotificationConfig] = useState<NotificationConfigResponse | null>(null)
|
const [notificationConfig, setNotificationConfig] = useState<NotificationConfigResponse | null>(null)
|
||||||
const [scheduledJobs, setScheduledJobs] = useState<ScheduledJobsState | null>(null)
|
const [scheduledJobs, setScheduledJobs] = useState<ScheduledJobsState | null>(null)
|
||||||
const [eticketConfig, setEticketConfig] = useState<{
|
|
||||||
filePath: string
|
|
||||||
source: AdminKuaishouEticketSourceConfig
|
|
||||||
} | null>(null)
|
|
||||||
const [industryConfig, setIndustryConfig] = useState<AdminKuaishouIndustryConfigResponse | null>(null)
|
const [industryConfig, setIndustryConfig] = useState<AdminKuaishouIndustryConfigResponse | null>(null)
|
||||||
const [feifeiConfig, setFeifeiConfig] = useState<AdminKuaishouFeifeiConfigResponse | null>(null)
|
const [feifeiConfig, setFeifeiConfig] = useState<AdminKuaishouFeifeiConfigResponse | null>(null)
|
||||||
const [cloudtentaclesConfig, setCloudtentaclesConfig] =
|
const [cloudtentaclesConfig, setCloudtentaclesConfig] =
|
||||||
@@ -159,7 +144,6 @@ export default function AdminPlatformShopsPage() {
|
|||||||
const [
|
const [
|
||||||
notificationResponse,
|
notificationResponse,
|
||||||
scheduledJobsResponse,
|
scheduledJobsResponse,
|
||||||
eticketResponse,
|
|
||||||
industryResponse,
|
industryResponse,
|
||||||
feifeiResponse,
|
feifeiResponse,
|
||||||
cloudtentaclesResponse,
|
cloudtentaclesResponse,
|
||||||
@@ -167,7 +151,6 @@ export default function AdminPlatformShopsPage() {
|
|||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
fetchAdminNotificationConfig(),
|
fetchAdminNotificationConfig(),
|
||||||
fetchAdminScheduledJobsConfig(),
|
fetchAdminScheduledJobsConfig(),
|
||||||
fetchAdminKuaishouEticketSourceConfig(),
|
|
||||||
fetchAdminKuaishouIndustrySourceConfig(),
|
fetchAdminKuaishouIndustrySourceConfig(),
|
||||||
fetchAdminKuaishouFeifeiConfig(),
|
fetchAdminKuaishouFeifeiConfig(),
|
||||||
fetchAdminCloudtentaclesSourceConfig(),
|
fetchAdminCloudtentaclesSourceConfig(),
|
||||||
@@ -176,7 +159,6 @@ export default function AdminPlatformShopsPage() {
|
|||||||
|
|
||||||
setNotificationConfig(notificationResponse.data)
|
setNotificationConfig(notificationResponse.data)
|
||||||
setScheduledJobs(scheduledJobsResponse.data)
|
setScheduledJobs(scheduledJobsResponse.data)
|
||||||
setEticketConfig(eticketResponse.data)
|
|
||||||
setIndustryConfig(industryResponse.data)
|
setIndustryConfig(industryResponse.data)
|
||||||
setFeifeiConfig(feifeiResponse.data)
|
setFeifeiConfig(feifeiResponse.data)
|
||||||
setCloudtentaclesConfig(cloudtentaclesResponse.data)
|
setCloudtentaclesConfig(cloudtentaclesResponse.data)
|
||||||
@@ -246,18 +228,6 @@ export default function AdminPlatformShopsPage() {
|
|||||||
<Empty description="通知配置未加载" />
|
<Empty description="通知配置未加载" />
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'kuaishouEticket',
|
|
||||||
label: '快手小店核销',
|
|
||||||
children: eticketConfig ? (
|
|
||||||
<KuaishouEticketPanel
|
|
||||||
config={eticketConfig}
|
|
||||||
onChange={setEticketConfig}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Empty description="快手小店配置未加载" />
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: 'kuaishouIndustry',
|
key: 'kuaishouIndustry',
|
||||||
label: '行业电子凭证',
|
label: '行业电子凭证',
|
||||||
@@ -838,232 +808,6 @@ function ScheduledJobCard({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function KuaishouEticketPanel({
|
|
||||||
config,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
config: { filePath: string; source: AdminKuaishouEticketSourceConfig }
|
|
||||||
onChange: (config: { filePath: string; source: AdminKuaishouEticketSourceConfig }) => void
|
|
||||||
}) {
|
|
||||||
const [saving, setSaving] = useState(false)
|
|
||||||
const [querying, setQuerying] = useState(false)
|
|
||||||
const [selectedShopId, setSelectedShopId] = useState(config.source.shops[0]?.shopId || '')
|
|
||||||
const [ticketCode, setTicketCode] = useState('')
|
|
||||||
const [shopInfoResult, setShopInfoResult] = useState<AdminKuaishouEticketShopInfoResult | null>(null)
|
|
||||||
const [detailResult, setDetailResult] = useState<AdminKuaishouEticketDetailResult | null>(null)
|
|
||||||
const source = config.source
|
|
||||||
const selectedShop = source.shops.find((shop) => shop.shopId === selectedShopId) || source.shops[0] || null
|
|
||||||
|
|
||||||
async function saveConfig() {
|
|
||||||
setSaving(true)
|
|
||||||
try {
|
|
||||||
const response = await saveAdminKuaishouEticketSourceConfig(source)
|
|
||||||
onChange(response.data)
|
|
||||||
showSuccess('快手小店核销配置已保存')
|
|
||||||
} catch (error) {
|
|
||||||
showError(error instanceof Error ? error.message : '保存快手小店核销配置失败')
|
|
||||||
} finally {
|
|
||||||
setSaving(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function queryShopInfo(shop = selectedShop) {
|
|
||||||
if (!shop?.cookie) {
|
|
||||||
showError('请先选择并填写店铺 Cookie')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setQuerying(true)
|
|
||||||
try {
|
|
||||||
const response = await queryAdminKuaishouEticketShopInfo({
|
|
||||||
baseUrl: source.baseUrl,
|
|
||||||
shopId: shop.shopId,
|
|
||||||
cookie: shop.cookie,
|
|
||||||
})
|
|
||||||
setShopInfoResult(response.data)
|
|
||||||
updateShop(shop.shopId, {
|
|
||||||
shopId: response.data.shop.shopId || shop.shopId,
|
|
||||||
kshopName: response.data.shop.kshopName || shop.kshopName,
|
|
||||||
userAvatar: response.data.shop.userAvatar || shop.userAvatar,
|
|
||||||
})
|
|
||||||
showSuccess(`已识别店铺:${response.data.shop.kshopName || response.data.shop.shopId}`)
|
|
||||||
} catch (error) {
|
|
||||||
showError(error instanceof Error ? error.message : '读取快手小店店铺信息失败')
|
|
||||||
} finally {
|
|
||||||
setQuerying(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function queryDetail() {
|
|
||||||
if (!selectedShop?.cookie) {
|
|
||||||
showError('请先选择并填写店铺 Cookie')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!ticketCode.trim()) {
|
|
||||||
showError('请输入查询券码')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setQuerying(true)
|
|
||||||
try {
|
|
||||||
const response = await queryAdminKuaishouEticketDetail({
|
|
||||||
baseUrl: source.baseUrl,
|
|
||||||
shopId: selectedShop.shopId,
|
|
||||||
cookie: selectedShop.cookie,
|
|
||||||
eTicketId: ticketCode.trim(),
|
|
||||||
})
|
|
||||||
setDetailResult(response.data)
|
|
||||||
showSuccess('核销券详情查询完成')
|
|
||||||
} catch (error) {
|
|
||||||
showError(error instanceof Error ? error.message : '查询核销券详情失败')
|
|
||||||
} finally {
|
|
||||||
setQuerying(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSource(nextSource: AdminKuaishouEticketSourceConfig) {
|
|
||||||
onChange({ ...config, source: nextSource })
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateShop(shopId: string, patch: Partial<AdminKuaishouEticketShopConfigItem>) {
|
|
||||||
updateSource({
|
|
||||||
...source,
|
|
||||||
shops: source.shops.map((shop) => (shop.shopId === shopId ? { ...shop, ...patch } : shop)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function addShop() {
|
|
||||||
const shopId = `shop_${Date.now()}`
|
|
||||||
updateSource({
|
|
||||||
...source,
|
|
||||||
shops: [
|
|
||||||
...source.shops,
|
|
||||||
{
|
|
||||||
shopId,
|
|
||||||
kshopName: '',
|
|
||||||
cookie: '',
|
|
||||||
cookieMasked: '',
|
|
||||||
hasCookie: false,
|
|
||||||
userAvatar: '',
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
setSelectedShopId(shopId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="platform-panel-stack">
|
|
||||||
<Card
|
|
||||||
title="快手小店核销源"
|
|
||||||
extra={
|
|
||||||
<Space wrap>
|
|
||||||
<Typography.Text type="secondary">{config.filePath || '默认配置'}</Typography.Text>
|
|
||||||
<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={saveConfig}>
|
|
||||||
保存配置
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="platform-form-grid">
|
|
||||||
<FieldSwitch
|
|
||||||
label="启用"
|
|
||||||
checked={source.enabled}
|
|
||||||
onChange={(enabled) => updateSource({ ...source, enabled })}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<Typography.Text type="secondary">接口地址</Typography.Text>
|
|
||||||
<Input
|
|
||||||
value={source.baseUrl}
|
|
||||||
onChange={(event) => updateSource({ ...source, baseUrl: event.target.value })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card size="small" title="店铺凭据" className="platform-section-gap" extra={<Button icon={<PlusOutlined />} onClick={addShop}>新增店铺</Button>}>
|
|
||||||
{source.shops.length === 0 ? (
|
|
||||||
<Empty description="暂无店铺配置" />
|
|
||||||
) : (
|
|
||||||
<Space direction="vertical" className="full-width" size={12}>
|
|
||||||
{source.shops.map((shop) => (
|
|
||||||
<div key={shop.shopId} className="platform-shop-row">
|
|
||||||
<Switch checked={shop.enabled !== false} onChange={(enabled) => updateShop(shop.shopId, { enabled })} />
|
|
||||||
<Avatar src={shop.userAvatar}>{shop.kshopName?.slice(0, 1) || '店'}</Avatar>
|
|
||||||
<Input
|
|
||||||
value={shop.shopId}
|
|
||||||
placeholder="shopId"
|
|
||||||
onChange={(event) => updateShop(shop.shopId, { shopId: event.target.value })}
|
|
||||||
onFocus={() => setSelectedShopId(shop.shopId)}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
value={shop.kshopName}
|
|
||||||
placeholder="店铺名称"
|
|
||||||
onChange={(event) => updateShop(shop.shopId, { kshopName: event.target.value })}
|
|
||||||
/>
|
|
||||||
<Input.Password
|
|
||||||
value={shop.cookie}
|
|
||||||
placeholder={shop.cookieMasked || 'Cookie'}
|
|
||||||
onChange={(event) =>
|
|
||||||
updateShop(shop.shopId, {
|
|
||||||
cookie: event.target.value,
|
|
||||||
hasCookie: Boolean(event.target.value.trim()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Button loading={querying && selectedShopId === shop.shopId} onClick={() => queryShopInfo(shop)}>
|
|
||||||
识别
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
danger
|
|
||||||
icon={<DeleteOutlined />}
|
|
||||||
onClick={() =>
|
|
||||||
updateSource({
|
|
||||||
...source,
|
|
||||||
shops: source.shops.filter((item) => item.shopId !== shop.shopId),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Space>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card title="核销调试">
|
|
||||||
<Space wrap className="full-width">
|
|
||||||
<Select
|
|
||||||
value={selectedShop?.shopId}
|
|
||||||
style={{ minWidth: 220 }}
|
|
||||||
placeholder="选择店铺"
|
|
||||||
options={source.shops.map((shop) => ({
|
|
||||||
label: shop.kshopName || shop.shopId,
|
|
||||||
value: shop.shopId,
|
|
||||||
}))}
|
|
||||||
onChange={setSelectedShopId}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
value={ticketCode}
|
|
||||||
placeholder="券码 / eTicketId"
|
|
||||||
style={{ width: 260 }}
|
|
||||||
onChange={(event) => setTicketCode(event.target.value)}
|
|
||||||
onPressEnter={queryDetail}
|
|
||||||
/>
|
|
||||||
<Button icon={<SearchOutlined />} loading={querying} onClick={queryDetail}>
|
|
||||||
查询详情
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
|
|
||||||
{shopInfoResult || detailResult ? (
|
|
||||||
<pre className="json-preview platform-section-gap">
|
|
||||||
{JSON.stringify(detailResult || shopInfoResult, null, 2)}
|
|
||||||
</pre>
|
|
||||||
) : null}
|
|
||||||
</Card>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function KuaishouIndustryPanel({
|
function KuaishouIndustryPanel({
|
||||||
config,
|
config,
|
||||||
onChange,
|
onChange,
|
||||||
|
|||||||
@@ -1,21 +1,16 @@
|
|||||||
import {
|
import {
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
ClockCircleOutlined,
|
|
||||||
ExclamationCircleOutlined,
|
ExclamationCircleOutlined,
|
||||||
LinkOutlined,
|
LinkOutlined,
|
||||||
LoadingOutlined,
|
LoadingOutlined,
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
SendOutlined,
|
|
||||||
SwapOutlined,
|
SwapOutlined,
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Collapse,
|
|
||||||
Empty,
|
Empty,
|
||||||
Image,
|
|
||||||
Input,
|
|
||||||
Space,
|
Space,
|
||||||
Spin,
|
Spin,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -39,7 +34,6 @@ import {
|
|||||||
fetchClaimDetail,
|
fetchClaimDetail,
|
||||||
rebindKuaishouCloudClaimRole,
|
rebindKuaishouCloudClaimRole,
|
||||||
redeemKuaishouCloudClaim,
|
redeemKuaishouCloudClaim,
|
||||||
verifyKuaishouCloudClaimTicket,
|
|
||||||
} from '@/services/claim'
|
} from '@/services/claim'
|
||||||
import type {
|
import type {
|
||||||
ClaimDetailData,
|
ClaimDetailData,
|
||||||
@@ -66,14 +60,12 @@ export default function ClaimPage() {
|
|||||||
const token = String(routeToken || '').trim()
|
const token = String(routeToken || '').trim()
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [submitting, setSubmitting] = useState(false)
|
|
||||||
const [refreshingRole, setRefreshingRole] = useState(false)
|
const [refreshingRole, setRefreshingRole] = useState(false)
|
||||||
const [confirmingRole, setConfirmingRole] = useState(false)
|
const [confirmingRole, setConfirmingRole] = useState(false)
|
||||||
const [rebindingRole, setRebindingRole] = useState(false)
|
const [rebindingRole, setRebindingRole] = useState(false)
|
||||||
const [redeeming, setRedeeming] = useState(false)
|
const [redeeming, setRedeeming] = useState(false)
|
||||||
const [errorMessage, setErrorMessage] = useState('')
|
const [errorMessage, setErrorMessage] = useState('')
|
||||||
const [detail, setDetail] = useState<ClaimDetailData | null>(null)
|
const [detail, setDetail] = useState<ClaimDetailData | null>(null)
|
||||||
const [ticketCode, setTicketCode] = useState('')
|
|
||||||
const [qrCodeDataUrl, setQrCodeDataUrl] = useState('')
|
const [qrCodeDataUrl, setQrCodeDataUrl] = useState('')
|
||||||
|
|
||||||
const pollTimerRef = useRef<number>(0)
|
const pollTimerRef = useRef<number>(0)
|
||||||
@@ -117,7 +109,6 @@ export default function ClaimPage() {
|
|||||||
const applyDetail = useCallback(
|
const applyDetail = useCallback(
|
||||||
async (nextDetail: ClaimDetailData) => {
|
async (nextDetail: ClaimDetailData) => {
|
||||||
setDetail(nextDetail)
|
setDetail(nextDetail)
|
||||||
setTicketCode((current) => current || nextDetail.kuaishouCloudFulfillment?.ticket.code || '')
|
|
||||||
await generateQRCode(
|
await generateQRCode(
|
||||||
String(nextDetail.kuaishouCloudFulfillment?.binding.bindUrl || '').trim(),
|
String(nextDetail.kuaishouCloudFulfillment?.binding.bindUrl || '').trim(),
|
||||||
)
|
)
|
||||||
@@ -208,7 +199,6 @@ export default function ClaimPage() {
|
|||||||
rolePollBaselineAtRef.current = 0
|
rolePollBaselineAtRef.current = 0
|
||||||
rolePollBaselineKeyRef.current = ''
|
rolePollBaselineKeyRef.current = ''
|
||||||
setDetail(null)
|
setDetail(null)
|
||||||
setTicketCode('')
|
|
||||||
setQrCodeDataUrl('')
|
setQrCodeDataUrl('')
|
||||||
void loadDetail()
|
void loadDetail()
|
||||||
|
|
||||||
@@ -234,27 +224,6 @@ export default function ClaimPage() {
|
|||||||
}
|
}
|
||||||
}, [loadDetail, resolveNextPollDelay, snapshot, stopPolling])
|
}, [loadDetail, resolveNextPollDelay, snapshot, stopPolling])
|
||||||
|
|
||||||
async function submitTicket() {
|
|
||||||
const normalizedTicketCode = ticketCode.trim()
|
|
||||||
if (!normalizedTicketCode) {
|
|
||||||
showError('请输入核销码')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setSubmitting(true)
|
|
||||||
try {
|
|
||||||
const response = await verifyKuaishouCloudClaimTicket(token, {
|
|
||||||
ticketCode: normalizedTicketCode,
|
|
||||||
})
|
|
||||||
await applyDetail(response.data)
|
|
||||||
showSuccess('核销已完成,系统已开始准备绑定资源')
|
|
||||||
} catch (error) {
|
|
||||||
showError(error instanceof Error ? error.message : '核销码验证失败')
|
|
||||||
} finally {
|
|
||||||
setSubmitting(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshRole() {
|
async function refreshRole() {
|
||||||
setRefreshingRole(true)
|
setRefreshingRole(true)
|
||||||
try {
|
try {
|
||||||
@@ -318,7 +287,7 @@ export default function ClaimPage() {
|
|||||||
async function rebindRole() {
|
async function rebindRole() {
|
||||||
try {
|
try {
|
||||||
await showConfirm(
|
await showConfirm(
|
||||||
'换绑后当前绑定链接会失效,系统会退还当前虚拟号并生成新的绑定二维码。核销码和领取商品不会改变,但需要重新扫码绑定角色。',
|
'换绑后当前绑定链接会失效,系统会退还当前虚拟号并生成新的绑定二维码。领取商品不会改变,但需要重新扫码绑定角色。',
|
||||||
'确认换绑角色',
|
'确认换绑角色',
|
||||||
{
|
{
|
||||||
okText: '确认换绑',
|
okText: '确认换绑',
|
||||||
@@ -407,15 +376,11 @@ export default function ClaimPage() {
|
|||||||
) : detail && snapshot.flow ? (
|
) : detail && snapshot.flow ? (
|
||||||
<KuaishouCloudClaimSteps
|
<KuaishouCloudClaimSteps
|
||||||
snapshot={snapshot}
|
snapshot={snapshot}
|
||||||
ticketCode={ticketCode}
|
|
||||||
qrCodeDataUrl={qrCodeDataUrl}
|
qrCodeDataUrl={qrCodeDataUrl}
|
||||||
submitting={submitting}
|
|
||||||
refreshingRole={refreshingRole}
|
refreshingRole={refreshingRole}
|
||||||
confirmingRole={confirmingRole}
|
confirmingRole={confirmingRole}
|
||||||
rebindingRole={rebindingRole}
|
rebindingRole={rebindingRole}
|
||||||
redeeming={redeeming}
|
redeeming={redeeming}
|
||||||
onTicketCodeChange={setTicketCode}
|
|
||||||
onSubmitTicket={submitTicket}
|
|
||||||
onOpenBindUrl={openBindUrl}
|
onOpenBindUrl={openBindUrl}
|
||||||
onRefreshRole={refreshRole}
|
onRefreshRole={refreshRole}
|
||||||
onRebindRole={rebindRole}
|
onRebindRole={rebindRole}
|
||||||
@@ -474,8 +439,6 @@ function createClaimSnapshot(
|
|||||||
String(flow?.dispatch.status || '').trim() === 'failed'
|
String(flow?.dispatch.status || '').trim() === 'failed'
|
||||||
const isCompleted = isKuaishouCloudCompletedStatus(task?.status)
|
const isCompleted = isKuaishouCloudCompletedStatus(task?.status)
|
||||||
const hasRedeemResult = isDispatched || hasKuaishouCloudRedeemResultStatus(task?.status)
|
const hasRedeemResult = isDispatched || hasKuaishouCloudRedeemResultStatus(task?.status)
|
||||||
const canSubmitTicket = !isClaimInactiveTaskStatus(task?.status)
|
|
||||||
|
|
||||||
const currentStep = resolveCurrentStep({
|
const currentStep = resolveCurrentStep({
|
||||||
isFeifeiFlow,
|
isFeifeiFlow,
|
||||||
isCompleted,
|
isCompleted,
|
||||||
@@ -524,7 +487,6 @@ function createClaimSnapshot(
|
|||||||
isRedeemFailed,
|
isRedeemFailed,
|
||||||
isCompleted,
|
isCompleted,
|
||||||
hasRedeemResult,
|
hasRedeemResult,
|
||||||
canSubmitTicket,
|
|
||||||
currentStep,
|
currentStep,
|
||||||
progressText,
|
progressText,
|
||||||
resultTitle,
|
resultTitle,
|
||||||
@@ -691,15 +653,11 @@ function ClaimHeaderCard({
|
|||||||
|
|
||||||
function KuaishouCloudClaimSteps({
|
function KuaishouCloudClaimSteps({
|
||||||
snapshot,
|
snapshot,
|
||||||
ticketCode,
|
|
||||||
qrCodeDataUrl,
|
qrCodeDataUrl,
|
||||||
submitting,
|
|
||||||
refreshingRole,
|
refreshingRole,
|
||||||
confirmingRole,
|
confirmingRole,
|
||||||
rebindingRole,
|
rebindingRole,
|
||||||
redeeming,
|
redeeming,
|
||||||
onTicketCodeChange,
|
|
||||||
onSubmitTicket,
|
|
||||||
onOpenBindUrl,
|
onOpenBindUrl,
|
||||||
onRefreshRole,
|
onRefreshRole,
|
||||||
onRebindRole,
|
onRebindRole,
|
||||||
@@ -707,15 +665,11 @@ function KuaishouCloudClaimSteps({
|
|||||||
onConfirmRedeem,
|
onConfirmRedeem,
|
||||||
}: {
|
}: {
|
||||||
snapshot: ClaimSnapshot
|
snapshot: ClaimSnapshot
|
||||||
ticketCode: string
|
|
||||||
qrCodeDataUrl: string
|
qrCodeDataUrl: string
|
||||||
submitting: boolean
|
|
||||||
refreshingRole: boolean
|
refreshingRole: boolean
|
||||||
confirmingRole: boolean
|
confirmingRole: boolean
|
||||||
rebindingRole: boolean
|
rebindingRole: boolean
|
||||||
redeeming: boolean
|
redeeming: boolean
|
||||||
onTicketCodeChange: (value: string) => void
|
|
||||||
onSubmitTicket: () => void
|
|
||||||
onOpenBindUrl: (useCurrentPage?: boolean) => void
|
onOpenBindUrl: (useCurrentPage?: boolean) => void
|
||||||
onRefreshRole: () => void
|
onRefreshRole: () => void
|
||||||
onRebindRole: () => void
|
onRebindRole: () => void
|
||||||
@@ -727,17 +681,7 @@ function KuaishouCloudClaimSteps({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot.currentStep === 1) {
|
if (snapshot.currentStep === 1) {
|
||||||
return (
|
return <ClaimDeprecatedTicketStep />
|
||||||
<ClaimTicketStep
|
|
||||||
ticketCode={ticketCode}
|
|
||||||
canSubmitTicket={snapshot.canSubmitTicket && !submitting}
|
|
||||||
submitting={submitting}
|
|
||||||
isBindingPreparing={snapshot.isBindingPreparing}
|
|
||||||
flow={snapshot.flow}
|
|
||||||
onTicketCodeChange={onTicketCodeChange}
|
|
||||||
onSubmitTicket={onSubmitTicket}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot.currentStep === 2) {
|
if (snapshot.currentStep === 2) {
|
||||||
@@ -773,84 +717,21 @@ function KuaishouCloudClaimSteps({
|
|||||||
return <ClaimResultStep snapshot={snapshot} />
|
return <ClaimResultStep snapshot={snapshot} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function ClaimTicketStep({
|
function ClaimDeprecatedTicketStep() {
|
||||||
ticketCode,
|
|
||||||
canSubmitTicket,
|
|
||||||
submitting,
|
|
||||||
isBindingPreparing,
|
|
||||||
flow,
|
|
||||||
onTicketCodeChange,
|
|
||||||
onSubmitTicket,
|
|
||||||
}: {
|
|
||||||
ticketCode: string
|
|
||||||
canSubmitTicket: boolean
|
|
||||||
submitting: boolean
|
|
||||||
isBindingPreparing: boolean
|
|
||||||
flow: ClaimKuaishouCloudFlowInfo
|
|
||||||
onTicketCodeChange: (value: string) => void
|
|
||||||
onSubmitTicket: () => void
|
|
||||||
}) {
|
|
||||||
return (
|
return (
|
||||||
<Card className="claim-content-card">
|
<Card className="claim-content-card">
|
||||||
<Typography.Title level={2}>第 1 步:提交核销码</Typography.Title>
|
<Typography.Title level={2}>旧领取流程已停用</Typography.Title>
|
||||||
<p className="claim-muted">
|
<p className="claim-muted">
|
||||||
请您先从快手小店复制核销码。提交后会立即核销,核销成功后系统会自动准备绑定资源。
|
当前订单已改用行业电子凭证方案处理,不再支持快手小店核销码提交。请联系商家确认新的领取方式。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Input
|
|
||||||
size="large"
|
|
||||||
value={ticketCode}
|
|
||||||
placeholder="粘贴快手核销码"
|
|
||||||
disabled={!canSubmitTicket}
|
|
||||||
onChange={(event) => onTicketCodeChange(event.target.value)}
|
|
||||||
onPressEnter={onSubmitTicket}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
icon={<SendOutlined />}
|
|
||||||
loading={submitting}
|
|
||||||
disabled={!canSubmitTicket}
|
|
||||||
onClick={onSubmitTicket}
|
|
||||||
>
|
|
||||||
提交核销码并继续
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Alert
|
<Alert
|
||||||
type="info"
|
type="warning"
|
||||||
showIcon
|
showIcon
|
||||||
icon={<ClockCircleOutlined />}
|
icon={<ExclamationCircleOutlined />}
|
||||||
message={
|
message="快手小店核销入口已下线"
|
||||||
isBindingPreparing
|
description="电子凭证发码、查询和核销请以新的电子凭证流程为准。"
|
||||||
? '核销完成后会自动准备绑定资源。'
|
|
||||||
: '核销完成后会自动进入扫码绑定步骤。'
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{flow.guideImages.length > 0 ? (
|
|
||||||
<Collapse
|
|
||||||
defaultActiveKey={['guide']}
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
key: 'guide',
|
|
||||||
label: '查看核销码图文指引',
|
|
||||||
children: (
|
|
||||||
<Image.PreviewGroup>
|
|
||||||
<div className="claim-guide-grid">
|
|
||||||
{flow.guideImages.map((imageUrl, index) => (
|
|
||||||
<figure key={imageUrl} className="claim-guide-figure">
|
|
||||||
<Image src={imageUrl} alt={`步骤 ${index + 1}`} />
|
|
||||||
<figcaption>步骤 {index + 1}</figcaption>
|
|
||||||
</figure>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Image.PreviewGroup>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
export * from './notifications'
|
export * from './notifications'
|
||||||
export * from './scheduled-jobs'
|
export * from './scheduled-jobs'
|
||||||
export * from './ninetyone'
|
export * from './ninetyone'
|
||||||
export * from './kuaishou-eticket'
|
|
||||||
export * from './kuaishou-industry'
|
export * from './kuaishou-industry'
|
||||||
export * from './kuaishou-feifei'
|
export * from './kuaishou-feifei'
|
||||||
export * from './cloudtentacles'
|
export * from './cloudtentacles'
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
import { apiGet, apiPost } from '@/lib/http'
|
|
||||||
import type {
|
|
||||||
AdminKuaishouEticketConsumeResult,
|
|
||||||
AdminKuaishouEticketDetailResult,
|
|
||||||
AdminKuaishouEticketShopInfoResult,
|
|
||||||
AdminKuaishouEticketSourceConfig,
|
|
||||||
} from '@/types/admin'
|
|
||||||
|
|
||||||
export function fetchAdminKuaishouEticketSourceConfig() {
|
|
||||||
return apiGet<{
|
|
||||||
filePath: string
|
|
||||||
source: AdminKuaishouEticketSourceConfig
|
|
||||||
}>('/api/v1/admin/platform-config/kuaishou-eticket-source')
|
|
||||||
}
|
|
||||||
|
|
||||||
export function saveAdminKuaishouEticketSourceConfig(payload: AdminKuaishouEticketSourceConfig) {
|
|
||||||
return apiPost<{
|
|
||||||
filePath: string
|
|
||||||
source: AdminKuaishouEticketSourceConfig
|
|
||||||
}>('/api/v1/admin/platform-config/kuaishou-eticket-source', payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function queryAdminKuaishouEticketDetail(payload: {
|
|
||||||
baseUrl?: string
|
|
||||||
shopId?: string
|
|
||||||
cookie?: string
|
|
||||||
eTicketId: string
|
|
||||||
}) {
|
|
||||||
return apiPost<AdminKuaishouEticketDetailResult>(
|
|
||||||
'/api/v1/admin/platform-config/kuaishou-eticket/query-detail',
|
|
||||||
payload,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function queryAdminKuaishouEticketShopInfo(payload: {
|
|
||||||
baseUrl?: string
|
|
||||||
shopId?: string
|
|
||||||
cookie?: string
|
|
||||||
}) {
|
|
||||||
return apiPost<AdminKuaishouEticketShopInfoResult>(
|
|
||||||
'/api/v1/admin/platform-config/kuaishou-eticket/query-shop-info',
|
|
||||||
payload,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function consumeAdminKuaishouEticket(payload: {
|
|
||||||
baseUrl?: string
|
|
||||||
shopId?: string
|
|
||||||
cookie?: string
|
|
||||||
eTicketId: string
|
|
||||||
oid?: string
|
|
||||||
formToken?: string
|
|
||||||
num?: number
|
|
||||||
storeId?: string
|
|
||||||
}) {
|
|
||||||
return apiPost<AdminKuaishouEticketConsumeResult>(
|
|
||||||
'/api/v1/admin/platform-config/kuaishou-eticket/consume',
|
|
||||||
payload,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -73,15 +73,10 @@ export function rebindAdminTaskKuaishouCloudRole(taskId: number | string) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dispatchAdminTaskKuaishouCloudFulfillment(
|
export function dispatchAdminTaskKuaishouCloudFulfillment(taskId: number | string) {
|
||||||
taskId: number | string,
|
|
||||||
payload: {
|
|
||||||
ticketCode?: string
|
|
||||||
} = {},
|
|
||||||
) {
|
|
||||||
return apiPost<AdminTaskActionResponse>(
|
return apiPost<AdminTaskActionResponse>(
|
||||||
`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/dispatch`,
|
`/api/v1/admin/tasks/${taskId}/kuaishou-cloud/dispatch`,
|
||||||
payload,
|
{},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,6 @@ export function fetchClaimDetail(token: string) {
|
|||||||
return apiGet<ClaimDetailData>(`/api/v1/claim/${token}`)
|
return apiGet<ClaimDetailData>(`/api/v1/claim/${token}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function verifyKuaishouCloudClaimTicket(
|
|
||||||
token: string,
|
|
||||||
payload: {
|
|
||||||
ticketCode: string
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/kuaishou-cloud/verify-ticket`, payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function confirmKuaishouCloudClaimRole(token: string) {
|
export function confirmKuaishouCloudClaimRole(token: string) {
|
||||||
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/kuaishou-cloud/confirm-role`, {})
|
return apiPost<ClaimDetailData>(`/api/v1/claim/${token}/kuaishou-cloud/confirm-role`, {})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -580,32 +580,6 @@ select {
|
|||||||
color: #dc2626;
|
color: #dc2626;
|
||||||
}
|
}
|
||||||
|
|
||||||
.claim-guide-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.claim-guide-figure {
|
|
||||||
margin: 0;
|
|
||||||
background: #f8fafc;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.claim-guide-figure .ant-image,
|
|
||||||
.claim-guide-figure img {
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.claim-guide-figure figcaption {
|
|
||||||
padding: 10px 12px 14px;
|
|
||||||
color: #475569;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.claim-qr-block {
|
.claim-qr-block {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -1209,7 +1183,6 @@ select {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.claim-guide-grid,
|
|
||||||
.claim-info-grid {
|
.claim-info-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,11 +57,6 @@ export type {
|
|||||||
AdminNinetyoneOrderItem,
|
AdminNinetyoneOrderItem,
|
||||||
AdminNinetyoneOrderListResult,
|
AdminNinetyoneOrderListResult,
|
||||||
AdminNinetyoneOrderActionResult,
|
AdminNinetyoneOrderActionResult,
|
||||||
AdminKuaishouEticketSourceConfig,
|
|
||||||
AdminKuaishouEticketShopConfigItem,
|
|
||||||
AdminKuaishouEticketShopInfoResult,
|
|
||||||
AdminKuaishouEticketDetailResult,
|
|
||||||
AdminKuaishouEticketConsumeResult,
|
|
||||||
AdminKuaishouIndustryAccessTokenStatus,
|
AdminKuaishouIndustryAccessTokenStatus,
|
||||||
AdminKuaishouIndustryShopConfig,
|
AdminKuaishouIndustryShopConfig,
|
||||||
AdminKuaishouIndustrySourceConfig,
|
AdminKuaishouIndustrySourceConfig,
|
||||||
|
|||||||
@@ -22,14 +22,6 @@ export type {
|
|||||||
AdminNinetyoneOrderActionResult,
|
AdminNinetyoneOrderActionResult,
|
||||||
} from './ninetyone'
|
} from './ninetyone'
|
||||||
|
|
||||||
export type {
|
|
||||||
AdminKuaishouEticketSourceConfig,
|
|
||||||
AdminKuaishouEticketShopConfigItem,
|
|
||||||
AdminKuaishouEticketShopInfoResult,
|
|
||||||
AdminKuaishouEticketDetailResult,
|
|
||||||
AdminKuaishouEticketConsumeResult,
|
|
||||||
} from './kuaishou-eticket'
|
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
AdminKuaishouIndustryAccessTokenStatus,
|
AdminKuaishouIndustryAccessTokenStatus,
|
||||||
AdminKuaishouIndustryShopConfig,
|
AdminKuaishouIndustryShopConfig,
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
export interface AdminKuaishouEticketShopConfigItem {
|
|
||||||
shopId: string
|
|
||||||
kshopName: string
|
|
||||||
cookie: string
|
|
||||||
cookieMasked: string
|
|
||||||
hasCookie: boolean
|
|
||||||
userAvatar: string
|
|
||||||
enabled: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminKuaishouEticketSourceConfig {
|
|
||||||
enabled: boolean
|
|
||||||
baseUrl: string
|
|
||||||
shops: AdminKuaishouEticketShopConfigItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminKuaishouEticketShopInfoResult {
|
|
||||||
baseUrl: string
|
|
||||||
ok: boolean
|
|
||||||
result: number
|
|
||||||
errorMessage: string
|
|
||||||
requestId: string
|
|
||||||
shop: {
|
|
||||||
shopId: string
|
|
||||||
kshopName: string
|
|
||||||
userAvatar: string
|
|
||||||
settleStatus: number
|
|
||||||
hasCookie: boolean
|
|
||||||
}
|
|
||||||
raw: Record<string, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminKuaishouEticketDetailResult {
|
|
||||||
baseUrl: string
|
|
||||||
eTicketId: string
|
|
||||||
ok: boolean
|
|
||||||
alreadyConsumed: boolean
|
|
||||||
result: number
|
|
||||||
errorMessage: string
|
|
||||||
requestId: string
|
|
||||||
serverTimestamp: string
|
|
||||||
detail: {
|
|
||||||
uid: string
|
|
||||||
fulfillDetailId: string
|
|
||||||
sellerId: string
|
|
||||||
formToken: string
|
|
||||||
validEndTime: string
|
|
||||||
validStartTime: string
|
|
||||||
leftReverseCount: number
|
|
||||||
eTicketId: string
|
|
||||||
oid: string
|
|
||||||
totalCount: number
|
|
||||||
leftCount: number
|
|
||||||
status: string
|
|
||||||
} | null
|
|
||||||
goods: {
|
|
||||||
itemId: string
|
|
||||||
itemPicUrl: string
|
|
||||||
itemTitle: string
|
|
||||||
price: string
|
|
||||||
skuDesc: string
|
|
||||||
skuId: string
|
|
||||||
} | null
|
|
||||||
raw: Record<string, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AdminKuaishouEticketConsumeResult {
|
|
||||||
baseUrl: string
|
|
||||||
eTicketId: string
|
|
||||||
oid: string
|
|
||||||
formToken: string
|
|
||||||
num: number
|
|
||||||
storeId: string
|
|
||||||
ok: boolean
|
|
||||||
consumed: boolean
|
|
||||||
alreadyConsumed: boolean
|
|
||||||
result: number
|
|
||||||
errorMessage: string
|
|
||||||
requestId: string
|
|
||||||
serverTimestamp: string
|
|
||||||
detail: AdminKuaishouEticketDetailResult['detail']
|
|
||||||
raw: Record<string, unknown>
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user