快手小店-核销--ok
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"syncFromCreatedAt": "2026-05-02T10:53:28.550Z",
|
||||
"lastRunStartedAt": "2026-05-02T16:37:16.806Z",
|
||||
"lastRunFinishedAt": "2026-05-02T16:37:17.707Z",
|
||||
"lastRunStartedAt": "2026-05-03T08:34:17.211Z",
|
||||
"lastRunFinishedAt": "2026-05-03T08:34:17.844Z",
|
||||
"lastRunStatus": "success",
|
||||
"lastErrorMessage": "",
|
||||
"fetchedCount": 40,
|
||||
"syncedCount": 0,
|
||||
"ignoredCount": 40,
|
||||
"lastOrderCreatedAt": "2026-05-02T16:36:46.000Z"
|
||||
"lastOrderCreatedAt": "2026-05-03T08:33:37.000Z"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"enabled": true,
|
||||
"baseUrl": "https://s.kwaixiaodian.com",
|
||||
"shops": [
|
||||
{
|
||||
"shopId": "140000112682050",
|
||||
"kshopName": "洋洋运营",
|
||||
"cookie": "_did=web_412376696871A458; did=web_pdd5bx2mm4sdx78w7zg3oj1uya9cqs59; bUserId=1000598999789; userId=140000112682050; merchantSellerId=4269276762; sid=kshop.api; kshop.api_st=Cgxrc2hvcC5hcGkuc3QSoAEHQpSWH5NWp5ReO9nWzyqJIZ9NsB7YhqywKcjVzUAf1YBJODvE9HHSA5jNrO1kyoYtQmK7hlUmD7FCkDjvzTNoDJCd5mGiu_oFbUr-JYJ2--q_QNhHlY13he_NQSPQ7EYDf54NBMk1jKQNarlUnB-1WosXNgV7G5YNeZXr8CDMWkDwKUaDnG-2JGmPTsouINryXjteKsj4YZEzwWHR2GTZGhJex1zz60qNCRAHidwVN1PhX4wiIN3oIIsuly3Zub__D_EnvOhbCP_Ma363nNk4hmpKm_NpKAUwAQ; kshop.api_ph=fabd3673d2238257e37fc892abf2e8e73fd4; KS-CSRF-Token=179C60C8C34FB066A71221C9EE64B5CE; merchantSessionKey=1777795509898_209a40cd7ad55a7e842151ee83de41bb",
|
||||
"userAvatar": "https://p2-pro.a.yximgs.com/uhead/AB/2024/07/23/17/BMjAyNDA3MjMxNzA1MThfNDI2OTI3Njc2Ml8yX2hkNDg1XzgxNQ==_s.jpg",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"shopId": "4269276762",
|
||||
"kshopName": "稚嫩游戏交易店",
|
||||
"cookie": "_did=web_1426300268274CD9; did=web_ozkrrsevhb6yuztyk6m63vqqi5h2pzfl; __risk_web_device_id=cd52e12c1773406609757e71; sid=kuaishou.shop.b; sellerId=4269276762; bUserId=1000492186791; userId=4269276762; merchantSellerId=4269276762; merchantSessionKey=1777621574120_fbd47024d23e73fc4ad1a157ffece433; kuaishou.shop.b_st=ChJrdWFpc2hvdS5zaG9wLmIuc3QSoAFzGnewF-wCpt4rHi-D-pp8zPUp7HByKY0MlHkw-WV676zG7z09Y5i7d7URwmGC8CL13jN0fOGV7d3GfaH2XLH8d4p60z0Ms-dIX0bipkI0yUWHfgnWqv9VPa8HqVRqMLs2VGD82X9tAR3eN9PXu38AYg0a5kpawchGPb2tjcYgLgH5b5DnJgjipHqzWb8Ma8AcLOmryqCSg4ZHwO5dbVJHGhIdwFBlxRpZgyh6i0Do-DSRsroiIIayd1xA628OsvCY2qV1HjCrTvMVt-3LmXcU6rLKSzytKAUwAQ; kuaishou.shop.b_ph=38925ccb882ec5456dd1c6bcee3f87b7e740",
|
||||
"userAvatar": "https://p2-pro.a.yximgs.com/uhead/AB/2024/07/23/17/BMjAyNDA3MjMxNzA1MThfNDI2OTI3Njc2Ml8yX2hkNDg1XzgxNQ==_s.jpg",
|
||||
"enabled": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -10,20 +10,25 @@ import {
|
||||
getAdminCloudtentaclesAsset,
|
||||
getAdminCloudtentaclesBindUrl,
|
||||
getAdminCloudtentaclesCategories,
|
||||
getAdminKuaishouEticketSourceConfig,
|
||||
getAdminCloudtentaclesKnapsack,
|
||||
getAdminCloudtentaclesSourceConfig,
|
||||
getAdminCloudtentaclesSkuList,
|
||||
getAdminAgisoShopConfigs,
|
||||
getAdminFulfillmentBindingConfigs,
|
||||
getAdminKhhaoSourceConfig,
|
||||
consumeAdminKuaishouEticket,
|
||||
listAdminCloudtentaclesVirtualNumbers,
|
||||
lookupAdminFulfillmentBindingOrder,
|
||||
queryAdminKuaishouEticketShopInfo,
|
||||
queryAdminKuaishouEticketDetail,
|
||||
queryAdminKhhaoOrders,
|
||||
runAdminCloudtentaclesFullFlow,
|
||||
sendAdminCloudtentaclesSmsCode,
|
||||
syncAdminKhhaoOrders,
|
||||
testAdminCloudtentaclesLogin,
|
||||
testAdminKhhaoLogin,
|
||||
updateAdminKuaishouEticketSourceConfig,
|
||||
updateAdminCloudtentaclesSourceConfig,
|
||||
updateAdminAgisoShopConfigs,
|
||||
updateAdminKhhaoSourceConfig,
|
||||
@@ -39,6 +44,10 @@ import { createJsonHandler, requireAdminRoles } from './shared.js'
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminKhhaoOrderQueryRouteBody} AdminKhhaoOrderQueryRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminKhhaoSourceConfigRouteBody} AdminKhhaoSourceConfigRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminKhhaoTestLoginRouteBody} AdminKhhaoTestLoginRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminKuaishouEticketSourceConfigRouteBody} AdminKuaishouEticketSourceConfigRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminKuaishouEticketDetailQueryRouteBody} AdminKuaishouEticketDetailQueryRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminKuaishouEticketConsumeRouteBody} AdminKuaishouEticketConsumeRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminKuaishouEticketShopInfoRouteBody} AdminKuaishouEticketShopInfoRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesSourceConfigRouteBody} AdminCloudtentaclesSourceConfigRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesSendSmsCodeRouteBody} AdminCloudtentaclesSendSmsCodeRouteBody */
|
||||
/** @typedef {import('../../types/admin-route-inputs.js').AdminCloudtentaclesTestLoginRouteBody} AdminCloudtentaclesTestLoginRouteBody */
|
||||
@@ -83,6 +92,37 @@ router.post('/platform-config/agiso-shops', createJsonHandler(
|
||||
},
|
||||
))
|
||||
|
||||
router.get('/platform-config/kuaishou-eticket-source', createJsonHandler(
|
||||
() => getAdminKuaishouEticketSourceConfig(),
|
||||
{
|
||||
successMessage: 'ok',
|
||||
errorMessage: '读取快手小店核销配置失败',
|
||||
scope: '[admin/platform-config/kuaishou-eticket-source]',
|
||||
},
|
||||
))
|
||||
|
||||
router.post('/platform-config/kuaishou-eticket-source', createJsonHandler(
|
||||
(req) => updateAdminKuaishouEticketSourceConfig(/** @type {AdminKuaishouEticketSourceConfigRouteBody} */ (req.body)),
|
||||
{
|
||||
successMessage: '快手小店核销配置已保存',
|
||||
errorMessage: '保存快手小店核销配置失败',
|
||||
scope: '[admin/platform-config/kuaishou-eticket-source]',
|
||||
audit: (_req, data) => {
|
||||
const result = /** @type {{ filePath?: string, source?: { enabled?: boolean, shops?: unknown[] } }} */ (data)
|
||||
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.get('/platform-config/khhao-source', createJsonHandler(
|
||||
() => getAdminKhhaoSourceConfig(),
|
||||
{
|
||||
@@ -188,6 +228,75 @@ router.post('/platform-config/khhao/sync-orders', createJsonHandler(
|
||||
},
|
||||
))
|
||||
|
||||
router.post('/platform-config/kuaishou-eticket/query-detail', createJsonHandler(
|
||||
(req) => queryAdminKuaishouEticketDetail(/** @type {AdminKuaishouEticketDetailQueryRouteBody} */ (req.body)),
|
||||
{
|
||||
successMessage: '快手小店核销信息查询成功',
|
||||
errorMessage: '查询快手小店核销信息失败',
|
||||
scope: '[admin/platform-config/kuaishou-eticket/query-detail]',
|
||||
audit: (req, data) => {
|
||||
const body = /** @type {AdminKuaishouEticketDetailQueryRouteBody} */ (req.body)
|
||||
const result = /** @type {{ eTicketId?: string, result?: number, ok?: boolean, alreadyConsumed?: boolean }} */ (data)
|
||||
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('/platform-config/kuaishou-eticket/query-shop-info', createJsonHandler(
|
||||
(req) => queryAdminKuaishouEticketShopInfo(/** @type {AdminKuaishouEticketShopInfoRouteBody} */ (req.body)),
|
||||
{
|
||||
successMessage: '快手小店店铺信息查询成功',
|
||||
errorMessage: '查询快手小店店铺信息失败',
|
||||
scope: '[admin/platform-config/kuaishou-eticket/query-shop-info]',
|
||||
audit: (req, data) => {
|
||||
const body = /** @type {AdminKuaishouEticketShopInfoRouteBody} */ (req.body)
|
||||
const result = /** @type {{ shop?: { shopId?: string, kshopName?: string }, result?: number, ok?: boolean }} */ (data)
|
||||
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('/platform-config/kuaishou-eticket/consume', createJsonHandler(
|
||||
(req) => consumeAdminKuaishouEticket(/** @type {AdminKuaishouEticketConsumeRouteBody} */ (req.body)),
|
||||
{
|
||||
successMessage: '快手小店核销请求已执行',
|
||||
errorMessage: '执行快手小店核销失败',
|
||||
scope: '[admin/platform-config/kuaishou-eticket/consume]',
|
||||
audit: (req, data) => {
|
||||
const body = /** @type {AdminKuaishouEticketConsumeRouteBody} */ (req.body)
|
||||
const result = /** @type {{ eTicketId?: string, result?: number, consumed?: boolean, alreadyConsumed?: boolean }} */ (data)
|
||||
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),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
router.get('/platform-config/cloudtentacles-source', createJsonHandler(
|
||||
() => getAdminCloudtentaclesSourceConfig(),
|
||||
{
|
||||
|
||||
@@ -15,6 +15,19 @@ import { syncKhhaoOrders } from '../platforms/khhao/order-sync-service.js'
|
||||
import { getKhhaoSourceConfig, getKhhaoSourcesFilePath, saveKhhaoSourceConfig } from '../platforms/khhao/source-config-service.js'
|
||||
import { ensureKhhaoSyncBaseline, getKhhaoSyncState, getKhhaoSyncStateFilePath } from '../platforms/khhao/sync-state-service.js'
|
||||
import { loginKhhaoSession } from '../platforms/khhao/session-service.js'
|
||||
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 {
|
||||
getCloudtentaclesSourceConfig,
|
||||
getCloudtentaclesSourcesFilePath,
|
||||
@@ -62,6 +75,10 @@ import { resolveDisplayShopName } from './admin-read-shared-helpers.js'
|
||||
|
||||
/** @typedef {import('../../types/admin-write-inputs.js').AdminAgisoShopConfigSaveInput} AdminAgisoShopConfigSaveInput */
|
||||
/** @typedef {import('../../types/admin-write-inputs.js').AdminKhhaoSourceConfigInput} AdminKhhaoSourceConfigInput */
|
||||
/** @typedef {import('../../types/admin-write-inputs.js').AdminKuaishouEticketSourceConfigInput} AdminKuaishouEticketSourceConfigInput */
|
||||
/** @typedef {import('../../types/admin-write-inputs.js').AdminKuaishouEticketShopInfoInput} AdminKuaishouEticketShopInfoInput */
|
||||
/** @typedef {import('../../types/admin-write-inputs.js').AdminKuaishouEticketDetailQueryInput} AdminKuaishouEticketDetailQueryInput */
|
||||
/** @typedef {import('../../types/admin-write-inputs.js').AdminKuaishouEticketConsumeInput} AdminKuaishouEticketConsumeInput */
|
||||
/** @typedef {import('../../types/admin-write-inputs.js').AdminKhhaoOrderQueryInput} AdminKhhaoOrderQueryInput */
|
||||
/** @typedef {import('../../types/admin-write-inputs.js').AdminKhhaoTestLoginInput} AdminKhhaoTestLoginInput */
|
||||
/** @typedef {import('../../types/admin-write-inputs.js').AdminCloudtentaclesSourceConfigInput} AdminCloudtentaclesSourceConfigInput */
|
||||
@@ -258,6 +275,37 @@ export function getAdminKhhaoSourceConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
export function getAdminKuaishouEticketSourceConfig() {
|
||||
const config = getKuaishouEticketSourceConfig()
|
||||
|
||||
return {
|
||||
filePath: getKuaishouEticketSourceFilePath(),
|
||||
source: {
|
||||
enabled: config.enabled !== false,
|
||||
baseUrl: String(config.baseUrl || '').trim(),
|
||||
shops: listKuaishouEticketShopConfigs(config).map((item) => mapAdminKuaishouEticketShopItem(item)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {AdminKuaishouEticketSourceConfigInput} [payload] */
|
||||
export function updateAdminKuaishouEticketSourceConfig(payload = /** @type {AdminKuaishouEticketSourceConfigInput} */ ({})) {
|
||||
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: {
|
||||
enabled: saved.enabled !== false,
|
||||
baseUrl: String(saved.baseUrl || '').trim(),
|
||||
shops: listKuaishouEticketShopConfigs(saved).map((item) => mapAdminKuaishouEticketShopItem(item)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {AdminKhhaoSourceConfigInput} [payload] */
|
||||
export function updateAdminKhhaoSourceConfig(payload = /** @type {AdminKhhaoSourceConfigInput} */ ({})) {
|
||||
const saved = saveKhhaoSourceConfig({
|
||||
@@ -376,6 +424,41 @@ export async function syncAdminKhhaoOrders(payload = /** @type {AdminKhhaoOrderQ
|
||||
})
|
||||
}
|
||||
|
||||
/** @param {AdminKuaishouEticketDetailQueryInput} [payload] */
|
||||
export async function queryAdminKuaishouEticketDetail(payload = /** @type {AdminKuaishouEticketDetailQueryInput} */ ({})) {
|
||||
const context = resolveKuaishouEticketAdminContext(payload)
|
||||
|
||||
return queryKuaishouEticketConsumeDetail({
|
||||
baseUrl: context.baseUrl,
|
||||
cookie: context.cookie,
|
||||
eTicketId: String(payload.eTicketId || '').trim(),
|
||||
})
|
||||
}
|
||||
|
||||
/** @param {AdminKuaishouEticketShopInfoInput} [payload] */
|
||||
export async function queryAdminKuaishouEticketShopInfo(payload = /** @type {AdminKuaishouEticketShopInfoInput} */ ({})) {
|
||||
const context = resolveKuaishouEticketAdminContext(payload)
|
||||
return queryKuaishouEticketStableInfo({
|
||||
baseUrl: context.baseUrl,
|
||||
cookie: context.cookie,
|
||||
})
|
||||
}
|
||||
|
||||
/** @param {AdminKuaishouEticketConsumeInput} [payload] */
|
||||
export async function consumeAdminKuaishouEticket(payload = /** @type {AdminKuaishouEticketConsumeInput} */ ({})) {
|
||||
const context = resolveKuaishouEticketAdminContext(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(),
|
||||
})
|
||||
}
|
||||
|
||||
export function getAdminCloudtentaclesSourceConfig() {
|
||||
const config = getCloudtentaclesSourceConfig()
|
||||
const session = getCloudtentaclesSessionState()
|
||||
@@ -984,6 +1067,33 @@ function maskPhone(value) {
|
||||
return `${normalized.slice(0, 3)}****${normalized.slice(-4)}`
|
||||
}
|
||||
|
||||
function mapAdminKuaishouEticketShopItem(item = {}) {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
function resolveKuaishouEticketAdminContext(payload = {}) {
|
||||
const savedSource = getKuaishouEticketSourceConfig()
|
||||
const requestedShopId = String(payload.shopId || '').trim()
|
||||
const configuredShop = requestedShopId
|
||||
? findKuaishouEticketShopConfig(requestedShopId, savedSource)
|
||||
: getFirstAvailableKuaishouEticketShop(savedSource)
|
||||
|
||||
return {
|
||||
baseUrl: pickFirstNonEmpty([payload.baseUrl, savedSource.baseUrl, 'https://s.kwaixiaodian.com']),
|
||||
cookie: pickFirstNonEmpty([payload.cookie, configuredShop?.cookie]),
|
||||
shopId: pickFirstNonEmpty([requestedShopId, configuredShop?.shopId]),
|
||||
shop: configuredShop,
|
||||
}
|
||||
}
|
||||
|
||||
function resolveCloudtentaclesAdminContext(payload = {}) {
|
||||
const savedSource = getCloudtentaclesSourceConfig()
|
||||
const persistedSession = getCloudtentaclesSessionState()
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
// @ts-check
|
||||
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import { kuaishouEticketRequest } from './http-client.js'
|
||||
import { resolveKuaishouEticketConfig } from './shared.js'
|
||||
|
||||
export async function queryKuaishouEticketConsumeDetail(payload = {}) {
|
||||
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 = {}) {
|
||||
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 = {}, context = {}) {
|
||||
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 = {}, context = {}) {
|
||||
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 = {}, detailResult = 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 = {}) {
|
||||
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 = {}) {
|
||||
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 = {}) {
|
||||
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 = {}) {
|
||||
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 = {}) {
|
||||
return !String(payload.oid || '').trim() || !String(payload.formToken || '').trim()
|
||||
}
|
||||
|
||||
function pickFirstNonEmpty(values) {
|
||||
for (const value of values) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (normalized) {
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
function normalizePositiveInteger(value, fallback) {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback
|
||||
}
|
||||
|
||||
function normalizeInteger(value, fallback) {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) ? parsed : fallback
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
mapKuaishouEticketDetailResult,
|
||||
normalizeKuaishouEticketApiResult,
|
||||
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',
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,170 @@
|
||||
// @ts-check
|
||||
|
||||
import http from 'node:http'
|
||||
import https from 'node:https'
|
||||
|
||||
import { parseJsonObject } from '../../../utils/json.js'
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import { logInfo } from '../../../utils/logger.js'
|
||||
import { buildKuaishouEticketHeaders, buildKuaishouEticketUrl, resolveKuaishouEticketConfig } from './shared.js'
|
||||
|
||||
export async function kuaishouEticketRequest(pathname, options = {}) {
|
||||
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),
|
||||
extra: options.headers,
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await requestViaNodeHttp(url, {
|
||||
method,
|
||||
headers,
|
||||
body: normalizeRequestBody(options.body, headers['content-type']),
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
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) {
|
||||
if (body == null) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (typeof body === 'string') {
|
||||
return 'application/json'
|
||||
}
|
||||
|
||||
return 'application/json'
|
||||
}
|
||||
|
||||
function normalizeRequestBody(body, contentType) {
|
||||
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, { method, headers, body, signal }) {
|
||||
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 = []
|
||||
|
||||
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) {
|
||||
const normalized = new Map()
|
||||
|
||||
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) {
|
||||
return normalized.get(String(name || '').toLowerCase()) || null
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// @ts-check
|
||||
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import { kuaishouEticketRequest } from './http-client.js'
|
||||
import { resolveKuaishouEticketConfig } from './shared.js'
|
||||
|
||||
export async function queryKuaishouEticketStableInfo(payload = {}) {
|
||||
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 = {}, context = {}) {
|
||||
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, fallback) {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) ? parsed : fallback
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
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)
|
||||
})
|
||||
@@ -0,0 +1,75 @@
|
||||
// @ts-check
|
||||
|
||||
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 function resolveKuaishouEticketConfig(overrides = {}) {
|
||||
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, pathname) {
|
||||
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 = {},
|
||||
} = {}) {
|
||||
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) {
|
||||
return String(value || '').trim().replace(/\/+$/, '')
|
||||
}
|
||||
|
||||
function normalizePath(value, fallback) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (!normalized) {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return normalized.startsWith('/') ? normalized : `/${normalized}`
|
||||
}
|
||||
|
||||
function normalizePositiveInteger(value, fallback) {
|
||||
const parsed = Number(value)
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback
|
||||
}
|
||||
|
||||
function normalizeHeaderMap(extra) {
|
||||
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),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// @ts-check
|
||||
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { PROJECT_ROOT } from '../../../config/runtime.js'
|
||||
|
||||
const KUAISHOU_ETICKET_SOURCE_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'kuaishou-eticket-source.json')
|
||||
const DEFAULT_BASE_URL = 'https://s.kwaixiaodian.com'
|
||||
|
||||
export function getKuaishouEticketSourceFilePath() {
|
||||
return KUAISHOU_ETICKET_SOURCE_FILE_PATH
|
||||
}
|
||||
|
||||
export function getKuaishouEticketSourceConfig() {
|
||||
return loadKuaishouEticketSourceConfigFromFile()
|
||||
}
|
||||
|
||||
export function saveKuaishouEticketSourceConfig(rawValue) {
|
||||
const normalized = normalizeKuaishouEticketSourceConfig(rawValue)
|
||||
fs.mkdirSync(path.dirname(KUAISHOU_ETICKET_SOURCE_FILE_PATH), { recursive: true })
|
||||
fs.writeFileSync(KUAISHOU_ETICKET_SOURCE_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
|
||||
return normalized
|
||||
}
|
||||
|
||||
export function listKuaishouEticketShopConfigs(source = getKuaishouEticketSourceConfig()) {
|
||||
return Array.isArray(source?.shops) ? source.shops : []
|
||||
}
|
||||
|
||||
export function findKuaishouEticketShopConfig(shopId, source = getKuaishouEticketSourceConfig()) {
|
||||
const normalizedShopId = String(shopId || '').trim()
|
||||
if (!normalizedShopId) {
|
||||
return null
|
||||
}
|
||||
|
||||
return listKuaishouEticketShopConfigs(source).find((item) => String(item.shopId || '').trim() === normalizedShopId) || null
|
||||
}
|
||||
|
||||
export function getFirstAvailableKuaishouEticketShop(source = getKuaishouEticketSourceConfig()) {
|
||||
return listKuaishouEticketShopConfigs(source).find((item) => item.enabled !== false && String(item.cookie || '').trim()) || null
|
||||
}
|
||||
|
||||
function loadKuaishouEticketSourceConfigFromFile() {
|
||||
if (!fs.existsSync(KUAISHOU_ETICKET_SOURCE_FILE_PATH)) {
|
||||
return createDefaultKuaishouEticketSourceConfig()
|
||||
}
|
||||
|
||||
try {
|
||||
const rawText = fs.readFileSync(KUAISHOU_ETICKET_SOURCE_FILE_PATH, 'utf8')
|
||||
return normalizeKuaishouEticketSourceConfig(JSON.parse(rawText))
|
||||
} catch {
|
||||
return createDefaultKuaishouEticketSourceConfig()
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeKuaishouEticketSourceConfig(rawValue) {
|
||||
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(Boolean),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeKuaishouEticketShopConfig(rawValue) {
|
||||
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) {
|
||||
const cookie = String(source.cookie || '').trim()
|
||||
if (!cookie) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
shopId: '',
|
||||
kshopName: '',
|
||||
cookie,
|
||||
userAvatar: '',
|
||||
enabled: source.enabled !== false,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function createDefaultKuaishouEticketSourceConfig() {
|
||||
return {
|
||||
enabled: true,
|
||||
baseUrl: DEFAULT_BASE_URL,
|
||||
shops: [],
|
||||
}
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
@@ -34,10 +34,26 @@ export {}
|
||||
* @typedef {import('./admin-write-inputs.js').AdminKhhaoSourceConfigInput} AdminKhhaoSourceConfigRouteBody
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./admin-write-inputs.js').AdminKuaishouEticketSourceConfigInput} AdminKuaishouEticketSourceConfigRouteBody
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./admin-write-inputs.js').AdminAgisoShopConfigSaveInput} AdminAgisoShopConfigRouteBody
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./admin-write-inputs.js').AdminKuaishouEticketDetailQueryInput} AdminKuaishouEticketDetailQueryRouteBody
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./admin-write-inputs.js').AdminKuaishouEticketConsumeInput} AdminKuaishouEticketConsumeRouteBody
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./admin-write-inputs.js').AdminKuaishouEticketShopInfoInput} AdminKuaishouEticketShopInfoRouteBody
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {import('./admin-write-inputs.js').AdminKhhaoTestLoginInput} AdminKhhaoTestLoginRouteBody
|
||||
*/
|
||||
|
||||
@@ -83,6 +83,54 @@ export {}
|
||||
* }} AdminKhhaoSourceConfigInput
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* shopId?: string
|
||||
* kshopName?: string
|
||||
* cookie?: string
|
||||
* userAvatar?: string
|
||||
* enabled?: boolean
|
||||
* }} AdminKuaishouEticketShopConfigWriteItemInput
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* enabled?: boolean
|
||||
* baseUrl?: string
|
||||
* shops?: AdminKuaishouEticketShopConfigWriteItemInput[]
|
||||
* }} AdminKuaishouEticketSourceConfigInput
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* baseUrl?: string
|
||||
* shopId?: string
|
||||
* cookie?: string
|
||||
* }} AdminKuaishouEticketShopInfoInput
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* baseUrl?: string
|
||||
* shopId?: string
|
||||
* cookie?: string
|
||||
* eTicketId?: string
|
||||
* }} AdminKuaishouEticketDetailQueryInput
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* baseUrl?: string
|
||||
* shopId?: string
|
||||
* cookie?: string
|
||||
* eTicketId?: string
|
||||
* oid?: string
|
||||
* formToken?: string
|
||||
* num?: number | string
|
||||
* storeId?: string
|
||||
* }} AdminKuaishouEticketConsumeInput
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* baseUrl?: string
|
||||
|
||||
@@ -16,6 +16,10 @@ import type {
|
||||
AdminKhhaoSourceConfig,
|
||||
AdminKhhaoSyncState,
|
||||
AdminKhhaoOrderSyncResult,
|
||||
AdminKuaishouEticketConsumeResult,
|
||||
AdminKuaishouEticketDetailResult,
|
||||
AdminKuaishouEticketShopInfoResult,
|
||||
AdminKuaishouEticketSourceConfig,
|
||||
AdminInventoryItemListItem,
|
||||
AdminInventorySkuSuggestion,
|
||||
AdminMessageDeliveryListItem,
|
||||
@@ -109,6 +113,50 @@ export function fetchAdminKhhaoSourceConfig() {
|
||||
}>('/api/v1/admin/platform-config/khhao-source')
|
||||
}
|
||||
|
||||
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 as unknown as Record<string, unknown>)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
export function saveAdminKhhaoSourceConfig(payload: AdminKhhaoSourceConfig) {
|
||||
return apiPost<{
|
||||
filePath: string
|
||||
|
||||
@@ -111,6 +111,90 @@ export interface AdminKhhaoSourceConfig {
|
||||
}
|
||||
}
|
||||
|
||||
export interface AdminKuaishouEticketSourceConfig {
|
||||
enabled: boolean
|
||||
baseUrl: string
|
||||
shops: AdminKuaishouEticketShopConfigItem[]
|
||||
}
|
||||
|
||||
export interface AdminKuaishouEticketShopConfigItem {
|
||||
shopId: string
|
||||
kshopName: string
|
||||
cookie: string
|
||||
cookieMasked: string
|
||||
hasCookie: boolean
|
||||
userAvatar: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
export interface AdminKhhaoSyncState {
|
||||
syncFromCreatedAt: string
|
||||
lastRunStartedAt: string
|
||||
|
||||
@@ -5,21 +5,26 @@ import { showError, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
appointAdminCloudtentaclesVn,
|
||||
buyAdminCloudtentaclesSku,
|
||||
consumeAdminKuaishouEticket,
|
||||
fetchAdminCloudtentaclesAsset,
|
||||
fetchAdminCloudtentaclesBindUrl,
|
||||
fetchAdminCloudtentaclesCategories,
|
||||
fetchAdminCloudtentaclesKnapsack,
|
||||
fetchAdminAgisoShopConfigs,
|
||||
fetchAdminCloudtentaclesSourceConfig,
|
||||
fetchAdminKuaishouEticketSourceConfig,
|
||||
fetchAdminCloudtentaclesSkuList,
|
||||
fetchAdminCloudtentaclesVnCode,
|
||||
fetchAdminCloudtentaclesVnList,
|
||||
fetchAdminKhhaoSourceConfig,
|
||||
generateAdminCloudtentaclesVnLoginCode,
|
||||
queryAdminKuaishouEticketDetail,
|
||||
queryAdminKuaishouEticketShopInfo,
|
||||
queryAdminKhhaoOrders,
|
||||
runAdminCloudtentaclesFullFlow,
|
||||
saveAdminCloudtentaclesSourceConfig,
|
||||
saveAdminAgisoShopConfigs,
|
||||
saveAdminKuaishouEticketSourceConfig,
|
||||
saveAdminKhhaoSourceConfig,
|
||||
sendAdminCloudtentaclesSmsCode,
|
||||
syncAdminKhhaoOrders,
|
||||
@@ -41,11 +46,16 @@ import type {
|
||||
AdminKhhaoOrderQueryResult,
|
||||
AdminKhhaoSyncState,
|
||||
AdminKhhaoOrderSyncResult,
|
||||
AdminKuaishouEticketShopConfigItem,
|
||||
AdminKuaishouEticketShopInfoResult,
|
||||
AdminKuaishouEticketConsumeResult,
|
||||
AdminKuaishouEticketDetailResult,
|
||||
AdminKuaishouEticketSourceConfig,
|
||||
} from '@/types/admin'
|
||||
import { hasAdminRole } from '@/utils/admin-auth'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
type PlatformTab = 'agiso' | 'khhao' | 'cloudtentacles'
|
||||
type PlatformTab = 'agiso' | 'khhao' | 'kuaishouEticket' | 'cloudtentacles'
|
||||
|
||||
type EditableDefaults = {
|
||||
messageTemplate: string
|
||||
@@ -62,6 +72,15 @@ type EditableShop = {
|
||||
autoDeliveryMessageTemplate: string
|
||||
}
|
||||
|
||||
type EditableKuaishouEticketShop = {
|
||||
id: string
|
||||
shopId: string
|
||||
kshopName: string
|
||||
cookie: string
|
||||
userAvatar: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
const loading = ref(true)
|
||||
const isDevMode = import.meta.env.DEV
|
||||
const saving = ref(false)
|
||||
@@ -73,6 +92,7 @@ const activePlatform = ref<PlatformTab>('agiso')
|
||||
const agisoFilePath = ref('')
|
||||
const khhaoFilePath = ref('')
|
||||
const khhaoStateFilePath = ref('')
|
||||
const kuaishouEticketFilePath = ref('')
|
||||
const cloudtentaclesFilePath = ref('')
|
||||
const cloudtentaclesSessionFilePath = ref('')
|
||||
const defaults = ref<EditableDefaults>(createEmptyDefaults())
|
||||
@@ -100,6 +120,26 @@ const khhaoLoginResult = ref<AdminKhhaoLoginTestResult | null>(null)
|
||||
const khhaoQueryResult = ref<AdminKhhaoOrderQueryResult | null>(null)
|
||||
const khhaoSyncResult = ref<AdminKhhaoOrderSyncResult | null>(null)
|
||||
const khhaoAutoSyncState = ref<AdminKhhaoSyncState | null>(null)
|
||||
const kuaishouEticketShops = ref<EditableKuaishouEticketShop[]>([])
|
||||
const expandedKuaishouEticketShopIds = ref<string[]>([])
|
||||
const kuaishouEticketForm = ref({
|
||||
baseUrl: 'https://s.kwaixiaodian.com',
|
||||
selectedShopId: '',
|
||||
queryTicketCode: '',
|
||||
eTicketId: '',
|
||||
oid: '',
|
||||
formToken: '',
|
||||
num: 1,
|
||||
storeId: '0',
|
||||
})
|
||||
const kuaishouEticketSaving = ref(false)
|
||||
const kuaishouEticketResolvingShopId = ref('')
|
||||
const kuaishouEticketDetailing = ref(false)
|
||||
const kuaishouEticketConsuming = ref(false)
|
||||
const kuaishouEticketResultError = ref('')
|
||||
const kuaishouEticketShopInfoResult = ref<AdminKuaishouEticketShopInfoResult | null>(null)
|
||||
const kuaishouEticketDetailResult = ref<AdminKuaishouEticketDetailResult | null>(null)
|
||||
const kuaishouEticketConsumeResult = ref<AdminKuaishouEticketConsumeResult | null>(null)
|
||||
const cloudtentaclesForm = ref({
|
||||
baseUrl: 'https://123.207.217.176',
|
||||
username: '',
|
||||
@@ -138,6 +178,18 @@ const khhaoStats = computed(() => ({
|
||||
ignoredCount: khhaoAutoSyncState.value?.ignoredCount || khhaoSyncResult.value?.ignoredCount || 0,
|
||||
}))
|
||||
|
||||
const activeKuaishouEticketShop = computed(() => (
|
||||
kuaishouEticketShops.value.find((item) => item.id === kuaishouEticketForm.value.selectedShopId) || null
|
||||
))
|
||||
|
||||
const kuaishouEticketStats = computed(() => ({
|
||||
configuredShopCount: kuaishouEticketShops.value.length,
|
||||
cookieReadyCount: kuaishouEticketShops.value.filter((item) => item.cookie.trim()).length,
|
||||
selectedShopReady: Boolean(activeKuaishouEticketShop.value?.cookie.trim()),
|
||||
detailReady: Boolean(kuaishouEticketDetailResult.value?.detail),
|
||||
consumed: Boolean(kuaishouEticketConsumeResult.value?.consumed),
|
||||
}))
|
||||
|
||||
const cloudtentaclesStats = computed(() => ({
|
||||
hasCredential: Boolean(
|
||||
cloudtentaclesForm.value.username.trim()
|
||||
@@ -186,6 +238,28 @@ function mapEditableShop(item: AdminAgisoShopConfigItem): EditableShop {
|
||||
}
|
||||
}
|
||||
|
||||
function createEmptyKuaishouEticketShop(): EditableKuaishouEticketShop {
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
shopId: '',
|
||||
kshopName: '',
|
||||
cookie: '',
|
||||
userAvatar: '',
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
|
||||
function mapEditableKuaishouEticketShop(item: AdminKuaishouEticketShopConfigItem): EditableKuaishouEticketShop {
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
shopId: item.shopId,
|
||||
kshopName: item.kshopName,
|
||||
cookie: item.cookie,
|
||||
userAvatar: item.userAvatar,
|
||||
enabled: item.enabled !== false,
|
||||
}
|
||||
}
|
||||
|
||||
async function loadConfigs() {
|
||||
if (!hasAdminRole('admin')) {
|
||||
loading.value = false
|
||||
@@ -196,9 +270,10 @@ async function loadConfigs() {
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const [agisoResponse, khhaoResponse, cloudtentaclesResponse] = await Promise.all([
|
||||
const [agisoResponse, khhaoResponse, kuaishouEticketResponse, cloudtentaclesResponse] = await Promise.all([
|
||||
fetchAdminAgisoShopConfigs(),
|
||||
fetchAdminKhhaoSourceConfig(),
|
||||
fetchAdminKuaishouEticketSourceConfig(),
|
||||
fetchAdminCloudtentaclesSourceConfig(),
|
||||
])
|
||||
|
||||
@@ -224,6 +299,21 @@ async function loadConfigs() {
|
||||
autoSyncPageSize: khhaoResponse.data.source.autoSync?.pageSize || 20,
|
||||
}
|
||||
|
||||
kuaishouEticketFilePath.value = kuaishouEticketResponse.data.filePath
|
||||
kuaishouEticketShops.value = kuaishouEticketResponse.data.source.shops.map(mapEditableKuaishouEticketShop)
|
||||
kuaishouEticketForm.value = {
|
||||
baseUrl: kuaishouEticketResponse.data.source.baseUrl || 'https://s.kwaixiaodian.com',
|
||||
selectedShopId: '',
|
||||
queryTicketCode: '',
|
||||
eTicketId: '',
|
||||
oid: '',
|
||||
formToken: '',
|
||||
num: 1,
|
||||
storeId: '0',
|
||||
}
|
||||
kuaishouEticketForm.value.selectedShopId = kuaishouEticketShops.value[0]?.id || ''
|
||||
expandedKuaishouEticketShopIds.value = kuaishouEticketShops.value[0]?.id ? [kuaishouEticketShops.value[0].id] : []
|
||||
|
||||
cloudtentaclesFilePath.value = cloudtentaclesResponse.data.filePath
|
||||
cloudtentaclesSessionFilePath.value = cloudtentaclesResponse.data.sessionFilePath
|
||||
cloudtentaclesPersistedSession.value = cloudtentaclesResponse.data.session
|
||||
@@ -468,6 +558,256 @@ async function handleKhhaoSyncOrders() {
|
||||
}
|
||||
}
|
||||
|
||||
function buildKuaishouEticketConfigPayload(): AdminKuaishouEticketSourceConfig {
|
||||
return {
|
||||
enabled: true,
|
||||
baseUrl: kuaishouEticketForm.value.baseUrl.trim() || 'https://s.kwaixiaodian.com',
|
||||
shops: kuaishouEticketShops.value.map((item) => ({
|
||||
shopId: item.shopId.trim(),
|
||||
kshopName: item.kshopName.trim(),
|
||||
cookie: item.cookie.trim(),
|
||||
cookieMasked: '',
|
||||
hasCookie: Boolean(item.cookie.trim()),
|
||||
userAvatar: item.userAvatar.trim(),
|
||||
enabled: item.enabled,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
function addKuaishouEticketShop() {
|
||||
const shop = createEmptyKuaishouEticketShop()
|
||||
kuaishouEticketShops.value.unshift(shop)
|
||||
setKuaishouEticketDebugShop(shop.id)
|
||||
}
|
||||
|
||||
function removeKuaishouEticketShop(id: string) {
|
||||
kuaishouEticketShops.value = kuaishouEticketShops.value.filter((item) => item.id !== id)
|
||||
collapseKuaishouEticketShop(id)
|
||||
if (kuaishouEticketForm.value.selectedShopId === id) {
|
||||
kuaishouEticketForm.value.selectedShopId = kuaishouEticketShops.value[0]?.id || ''
|
||||
}
|
||||
}
|
||||
|
||||
function isKuaishouEticketShopExpanded(id: string) {
|
||||
return expandedKuaishouEticketShopIds.value.includes(id)
|
||||
}
|
||||
|
||||
function expandKuaishouEticketShop(id: string) {
|
||||
if (expandedKuaishouEticketShopIds.value.includes(id)) {
|
||||
return
|
||||
}
|
||||
expandedKuaishouEticketShopIds.value = [id, ...expandedKuaishouEticketShopIds.value]
|
||||
}
|
||||
|
||||
function collapseKuaishouEticketShop(id: string) {
|
||||
expandedKuaishouEticketShopIds.value = expandedKuaishouEticketShopIds.value.filter((item) => item !== id)
|
||||
}
|
||||
|
||||
function toggleKuaishouEticketShop(id: string) {
|
||||
if (isKuaishouEticketShopExpanded(id)) {
|
||||
collapseKuaishouEticketShop(id)
|
||||
return
|
||||
}
|
||||
expandKuaishouEticketShop(id)
|
||||
}
|
||||
|
||||
function expandAllKuaishouEticketShops() {
|
||||
expandedKuaishouEticketShopIds.value = kuaishouEticketShops.value.map((item) => item.id)
|
||||
}
|
||||
|
||||
function collapseAllKuaishouEticketShops() {
|
||||
expandedKuaishouEticketShopIds.value = []
|
||||
}
|
||||
|
||||
function setKuaishouEticketDebugShop(id: string) {
|
||||
kuaishouEticketForm.value.selectedShopId = id
|
||||
expandKuaishouEticketShop(id)
|
||||
}
|
||||
|
||||
function describeKuaishouEticketShop(shop: EditableKuaishouEticketShop) {
|
||||
const status = shop.cookie.trim() ? 'Cookie 已就绪' : '待补 Cookie'
|
||||
const identity = shop.shopId.trim() ? '已识别店铺' : '待读取信息'
|
||||
return `${status} · ${identity}`
|
||||
}
|
||||
|
||||
function getKuaishouEticketShopInitial(shop: EditableKuaishouEticketShop) {
|
||||
const label = shop.kshopName.trim() || shop.shopId.trim() || '店'
|
||||
return label.slice(0, 1).toUpperCase()
|
||||
}
|
||||
|
||||
function ensureSelectedKuaishouEticketShop() {
|
||||
const selected = activeKuaishouEticketShop.value
|
||||
|
||||
if (!selected) {
|
||||
kuaishouEticketResultError.value = '请先新增并选择一个店铺配置'
|
||||
return null
|
||||
}
|
||||
|
||||
if (!selected.cookie.trim()) {
|
||||
kuaishouEticketResultError.value = '当前店铺缺少 Cookie,请先补全后再操作'
|
||||
return null
|
||||
}
|
||||
|
||||
return selected
|
||||
}
|
||||
|
||||
function ensureKuaishouEticketQueryFields() {
|
||||
const selected = ensureSelectedKuaishouEticketShop()
|
||||
if (!selected) return false
|
||||
|
||||
if (!kuaishouEticketForm.value.queryTicketCode.trim()) {
|
||||
kuaishouEticketResultError.value = '请先填写查询券码'
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function ensureKuaishouEticketConsumeFields() {
|
||||
const selected = ensureSelectedKuaishouEticketShop()
|
||||
if (!selected) return false
|
||||
|
||||
if (!kuaishouEticketDetailResult.value?.detail) {
|
||||
kuaishouEticketResultError.value = '请先查询核销信息,系统会自动带出 eTicketId、oid 和 formToken'
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function applyKuaishouEticketDetailToForm(result: AdminKuaishouEticketDetailResult | null) {
|
||||
if (!result?.detail) {
|
||||
return
|
||||
}
|
||||
|
||||
kuaishouEticketForm.value.eTicketId = result.detail.eTicketId || kuaishouEticketForm.value.eTicketId
|
||||
kuaishouEticketForm.value.oid = result.detail.oid || kuaishouEticketForm.value.oid
|
||||
kuaishouEticketForm.value.formToken = result.detail.formToken || kuaishouEticketForm.value.formToken
|
||||
|
||||
const leftCount = Number(result.detail.leftCount || 0)
|
||||
if (leftCount > 0) {
|
||||
kuaishouEticketForm.value.num = leftCount
|
||||
}
|
||||
}
|
||||
|
||||
async function handleKuaishouEticketResolveShopInfo(shop: EditableKuaishouEticketShop) {
|
||||
if (!shop.cookie.trim()) {
|
||||
kuaishouEticketResultError.value = '请先为该店铺粘贴 Cookie'
|
||||
showError(kuaishouEticketResultError.value)
|
||||
return
|
||||
}
|
||||
|
||||
kuaishouEticketResolvingShopId.value = shop.id
|
||||
kuaishouEticketResultError.value = ''
|
||||
|
||||
try {
|
||||
const response = await queryAdminKuaishouEticketShopInfo({
|
||||
baseUrl: kuaishouEticketForm.value.baseUrl.trim() || 'https://s.kwaixiaodian.com',
|
||||
shopId: shop.shopId.trim(),
|
||||
cookie: shop.cookie.trim(),
|
||||
})
|
||||
kuaishouEticketShopInfoResult.value = response.data
|
||||
shop.shopId = response.data.shop.shopId || shop.shopId
|
||||
shop.kshopName = response.data.shop.kshopName || shop.kshopName
|
||||
shop.userAvatar = response.data.shop.userAvatar || shop.userAvatar
|
||||
if (!kuaishouEticketForm.value.selectedShopId) {
|
||||
setKuaishouEticketDebugShop(shop.id)
|
||||
}
|
||||
showSuccess(`已识别店铺:${response.data.shop.kshopName || response.data.shop.shopId || '未知店铺'}`)
|
||||
} catch (error) {
|
||||
kuaishouEticketResultError.value = error instanceof Error ? error.message : '读取快手小店店铺信息失败'
|
||||
showError(kuaishouEticketResultError.value)
|
||||
} finally {
|
||||
kuaishouEticketResolvingShopId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function handleKuaishouEticketSaveSource() {
|
||||
kuaishouEticketSaving.value = true
|
||||
kuaishouEticketResultError.value = ''
|
||||
|
||||
try {
|
||||
const response = await saveAdminKuaishouEticketSourceConfig(buildKuaishouEticketConfigPayload())
|
||||
kuaishouEticketFilePath.value = response.data.filePath
|
||||
kuaishouEticketForm.value.baseUrl = response.data.source.baseUrl || 'https://s.kwaixiaodian.com'
|
||||
const previousSelectedShopId = activeKuaishouEticketShop.value?.shopId || ''
|
||||
kuaishouEticketShops.value = response.data.source.shops.map(mapEditableKuaishouEticketShop)
|
||||
kuaishouEticketForm.value.selectedShopId = (
|
||||
kuaishouEticketShops.value.find((item) => item.shopId && item.shopId === previousSelectedShopId)?.id
|
||||
|| kuaishouEticketShops.value[0]?.id
|
||||
|| ''
|
||||
)
|
||||
expandedKuaishouEticketShopIds.value = kuaishouEticketForm.value.selectedShopId
|
||||
? [kuaishouEticketForm.value.selectedShopId]
|
||||
: []
|
||||
showSuccess('快手小店核销配置已保存')
|
||||
} catch (error) {
|
||||
kuaishouEticketResultError.value = error instanceof Error ? error.message : '快手小店核销配置保存失败'
|
||||
showError(kuaishouEticketResultError.value)
|
||||
} finally {
|
||||
kuaishouEticketSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleKuaishouEticketQueryDetail() {
|
||||
if (!ensureKuaishouEticketQueryFields()) {
|
||||
return
|
||||
}
|
||||
|
||||
const selected = activeKuaishouEticketShop.value
|
||||
kuaishouEticketDetailing.value = true
|
||||
kuaishouEticketResultError.value = ''
|
||||
kuaishouEticketConsumeResult.value = null
|
||||
|
||||
try {
|
||||
const response = await queryAdminKuaishouEticketDetail({
|
||||
baseUrl: kuaishouEticketForm.value.baseUrl.trim() || 'https://s.kwaixiaodian.com',
|
||||
shopId: selected?.shopId.trim(),
|
||||
cookie: selected?.cookie.trim(),
|
||||
eTicketId: kuaishouEticketForm.value.queryTicketCode.trim(),
|
||||
})
|
||||
kuaishouEticketDetailResult.value = response.data
|
||||
applyKuaishouEticketDetailToForm(response.data)
|
||||
showSuccess(response.data.ok ? '核销信息查询成功' : (response.data.errorMessage || '核销信息已返回'))
|
||||
} catch (error) {
|
||||
kuaishouEticketResultError.value = error instanceof Error ? error.message : '查询快手小店核销信息失败'
|
||||
showError(kuaishouEticketResultError.value)
|
||||
} finally {
|
||||
kuaishouEticketDetailing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleKuaishouEticketConsume() {
|
||||
if (!ensureKuaishouEticketConsumeFields()) {
|
||||
return
|
||||
}
|
||||
|
||||
const selected = activeKuaishouEticketShop.value
|
||||
kuaishouEticketConsuming.value = true
|
||||
kuaishouEticketResultError.value = ''
|
||||
|
||||
try {
|
||||
const detail = kuaishouEticketDetailResult.value?.detail
|
||||
const response = await consumeAdminKuaishouEticket({
|
||||
baseUrl: kuaishouEticketForm.value.baseUrl.trim() || 'https://s.kwaixiaodian.com',
|
||||
shopId: selected?.shopId.trim(),
|
||||
cookie: selected?.cookie.trim(),
|
||||
eTicketId: String(detail?.eTicketId || kuaishouEticketForm.value.eTicketId || '').trim(),
|
||||
oid: String(detail?.oid || kuaishouEticketForm.value.oid || '').trim(),
|
||||
formToken: String(detail?.formToken || kuaishouEticketForm.value.formToken || '').trim(),
|
||||
num: Number(kuaishouEticketForm.value.num || 1),
|
||||
storeId: kuaishouEticketForm.value.storeId.trim() || '0',
|
||||
})
|
||||
kuaishouEticketConsumeResult.value = response.data
|
||||
showSuccess(response.data.ok ? '快手小店核销成功' : (response.data.errorMessage || '核销请求已执行'))
|
||||
} catch (error) {
|
||||
kuaishouEticketResultError.value = error instanceof Error ? error.message : '执行快手小店核销失败'
|
||||
showError(kuaishouEticketResultError.value)
|
||||
} finally {
|
||||
kuaishouEticketConsuming.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function buildCloudtentaclesConfigPayload(): AdminCloudtentaclesSourceConfig {
|
||||
return {
|
||||
enabled: true,
|
||||
@@ -776,7 +1116,7 @@ onMounted(loadConfigs)
|
||||
<header class="hero-card">
|
||||
<div class="hero-copy">
|
||||
<h1>平台配置</h1>
|
||||
<p>按平台分别管理来源接入、凭据与店铺能力。Agiso 负责 webhook / 店铺消息配置,khhao 负责来源拉单,cloudtentacles 负责外部履约平台登录。</p>
|
||||
<p>按平台分别管理来源接入、凭据与店铺能力。Agiso 负责 webhook / 店铺消息配置,khhao 负责来源拉单,快手小店核销负责独立券核销调试,cloudtentacles 负责外部履约平台登录。</p>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<el-button round @click="loadConfigs">刷新全部配置</el-button>
|
||||
@@ -826,6 +1166,23 @@ onMounted(loadConfigs)
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
:class="['platform-overview-card', { 'is-active': activePlatform === 'kuaishouEticket' }]"
|
||||
@click="switchPlatform('kuaishouEticket')"
|
||||
>
|
||||
<div class="platform-head">
|
||||
<span class="platform-badge">快手小店核销</span>
|
||||
<span class="platform-tag">Cookie / 券核销</span>
|
||||
</div>
|
||||
<strong>{{ kuaishouEticketStats.configuredShopCount }}</strong>
|
||||
<span>已配置店铺</span>
|
||||
<div class="platform-metrics">
|
||||
<span>Cookie 就绪 {{ kuaishouEticketStats.cookieReadyCount }} 家</span>
|
||||
<span>当前店铺 {{ kuaishouEticketStats.selectedShopReady ? '可调试' : '待补全' }}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
:class="['platform-overview-card', { 'is-active': activePlatform === 'cloudtentacles' }]"
|
||||
@@ -860,6 +1217,13 @@ onMounted(loadConfigs)
|
||||
>
|
||||
khhao 来源与同步
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="['switch-chip', { 'is-active': activePlatform === 'kuaishouEticket' }]"
|
||||
@click="switchPlatform('kuaishouEticket')"
|
||||
>
|
||||
快手小店核销
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="['switch-chip', { 'is-active': activePlatform === 'cloudtentacles' }]"
|
||||
@@ -1248,6 +1612,285 @@ onMounted(loadConfigs)
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else-if="activePlatform === 'kuaishouEticket'">
|
||||
<section class="meta-card">
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">配置文件</span>
|
||||
<code>{{ kuaishouEticketFilePath || '-' }}</code>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">职责</span>
|
||||
<span>这是独立的新流程,只负责快手小店券核销调试与配置,不接入当前已完成的订单来源和履约主链路。</span>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">建议流程</span>
|
||||
<span>先为每个店铺录入 `kshopName + Cookie`,可用 info 接口自动识别店铺名与店铺 ID;保存后选择目标店铺,再查询券码并执行核销。</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>快手小店店铺配置</h3>
|
||||
<p>支持多店铺配置,建议先粘贴 Cookie,再用 info 接口自动识别 `kshopName / shopId`,最后统一保存到文件。</p>
|
||||
</div>
|
||||
<div class="section-actions">
|
||||
<el-button :disabled="kuaishouEticketShops.length === 0" text @click="expandAllKuaishouEticketShops">全部展开</el-button>
|
||||
<el-button :disabled="kuaishouEticketShops.length === 0" text @click="collapseAllKuaishouEticketShops">全部折叠</el-button>
|
||||
<el-button round @click="addKuaishouEticketShop">新增店铺</el-button>
|
||||
<el-button :loading="kuaishouEticketSaving" round type="primary" @click="handleKuaishouEticketSaveSource">保存配置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kuaishou-workspace">
|
||||
<div class="kuaishou-shop-stack">
|
||||
<div class="form-grid kuaishou-base-grid">
|
||||
<label class="field-block field-wide">
|
||||
<span>Base URL</span>
|
||||
<input v-model="kuaishouEticketForm.baseUrl" class="text-input" placeholder="https://s.kwaixiaodian.com" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="kuaishouEticketShops.length === 0" class="empty-inline">当前还没有快手小店店铺配置,先新增一条。</div>
|
||||
|
||||
<article
|
||||
v-for="shop in kuaishouEticketShops"
|
||||
:key="shop.id"
|
||||
:class="['shop-card', 'kuaishou-shop-card', {
|
||||
'is-active': kuaishouEticketForm.selectedShopId === shop.id,
|
||||
'is-expanded': isKuaishouEticketShopExpanded(shop.id),
|
||||
}]"
|
||||
>
|
||||
<button type="button" class="shop-summary kuaishou-shop-summary" @click="toggleKuaishouEticketShop(shop.id)">
|
||||
<div class="kuaishou-shop-head">
|
||||
<div class="shop-avatar">
|
||||
<img v-if="shop.userAvatar" :src="shop.userAvatar" :alt="shop.kshopName || shop.shopId || '店铺头像'" />
|
||||
<span v-else>{{ getKuaishouEticketShopInitial(shop) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="shop-summary-main">
|
||||
<div class="shop-title-row">
|
||||
<strong>{{ shop.kshopName || shop.shopId || '未命名店铺' }}</strong>
|
||||
<span class="shop-status" :class="{ 'is-disabled': !shop.enabled }">
|
||||
{{ shop.enabled ? '已启用' : '已停用' }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="cell-subtle">店铺 ID:{{ shop.shopId || '等待 info 自动识别' }}</span>
|
||||
<div class="shop-tag-row">
|
||||
<span class="shop-mini-tag" :class="{ 'is-ready': Boolean(shop.cookie.trim()) }">
|
||||
{{ shop.cookie.trim() ? 'Cookie 已粘贴' : '待补 Cookie' }}
|
||||
</span>
|
||||
<span class="shop-mini-tag" :class="{ 'is-ready': Boolean(shop.userAvatar || shop.shopId.trim()) }">
|
||||
{{ shop.userAvatar || shop.shopId.trim() ? '店铺信息已识别' : '未读取店铺信息' }}
|
||||
</span>
|
||||
<span v-if="kuaishouEticketForm.selectedShopId === shop.id" class="shop-mini-tag is-highlight">当前调试店铺</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shop-summary-side">
|
||||
<span class="shop-summary-copy">{{ describeKuaishouEticketShop(shop) }}</span>
|
||||
<span class="shop-toggle">{{ isKuaishouEticketShopExpanded(shop.id) ? '收起详情' : '展开配置' }}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div v-if="isKuaishouEticketShopExpanded(shop.id)" class="shop-body kuaishou-shop-body">
|
||||
<div class="form-grid">
|
||||
<label class="field-block">
|
||||
<span>kshopName</span>
|
||||
<input v-model="shop.kshopName" class="text-input" placeholder="例如 稚嫩游戏交易店" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>店铺 ID</span>
|
||||
<input v-model="shop.shopId" class="text-input" readonly placeholder="通过 info 接口自动识别" />
|
||||
</label>
|
||||
|
||||
<label class="field-block field-wide">
|
||||
<span>Cookie</span>
|
||||
<textarea
|
||||
v-model="shop.cookie"
|
||||
class="text-area kuaishou-cookie-area"
|
||||
placeholder="直接粘贴该店铺请求头中的完整 Cookie"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="shop-actions kuaishou-shop-actions">
|
||||
<label class="checkbox-line">
|
||||
<input v-model="shop.enabled" type="checkbox" />
|
||||
<span>启用该店铺</span>
|
||||
</label>
|
||||
<span class="cell-subtle">{{ shop.userAvatar ? '已识别头像,可直接作为调试店铺' : '建议先读取店铺信息确认 Cookie 归属' }}</span>
|
||||
<el-button
|
||||
:loading="kuaishouEticketResolvingShopId === shop.id"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleKuaishouEticketResolveShopInfo(shop)"
|
||||
>
|
||||
读取店铺信息
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
:type="kuaishouEticketForm.selectedShopId === shop.id ? 'success' : 'primary'"
|
||||
@click="setKuaishouEticketDebugShop(shop.id)"
|
||||
>
|
||||
{{ kuaishouEticketForm.selectedShopId === shop.id ? '当前调试店铺' : '设为调试店铺' }}
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="removeKuaishouEticketShop(shop.id)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<aside class="kuaishou-side-panel">
|
||||
<article class="result-card kuaishou-side-card">
|
||||
<span class="result-label">店铺总数</span>
|
||||
<strong>{{ kuaishouEticketStats.configuredShopCount }}</strong>
|
||||
<p>其中 Cookie 就绪 {{ kuaishouEticketStats.cookieReadyCount }} 家。</p>
|
||||
</article>
|
||||
|
||||
<article class="result-card kuaishou-side-card">
|
||||
<span class="result-label">当前调试店铺</span>
|
||||
<strong>{{ activeKuaishouEticketShop?.kshopName || activeKuaishouEticketShop?.shopId || '未选择' }}</strong>
|
||||
<p>{{ kuaishouEticketStats.selectedShopReady ? '已具备调试条件,可直接查询核销信息。' : '请选择店铺并补全 Cookie。' }}</p>
|
||||
</article>
|
||||
|
||||
<article class="result-card kuaishou-side-card">
|
||||
<span class="result-label">最近识别结果</span>
|
||||
<strong>{{ kuaishouEticketShopInfoResult?.shop.kshopName || kuaishouEticketShopInfoResult?.shop.shopId || '未执行' }}</strong>
|
||||
<p v-if="kuaishouEticketShopInfoResult">结算状态 {{ kuaishouEticketShopInfoResult.shop.settleStatus }}</p>
|
||||
<p v-else>可用“读取店铺信息”校验 Cookie 是否属于预期店铺。</p>
|
||||
</article>
|
||||
|
||||
<article class="result-card kuaishou-side-card kuaishou-guide-card">
|
||||
<span class="result-label">建议顺序</span>
|
||||
<strong>先配置,再调试</strong>
|
||||
<p>1. 新增店铺并粘贴 Cookie</p>
|
||||
<p>2. 读取店铺信息确认 kshopName / shopId</p>
|
||||
<p>3. 设为调试店铺后查询券码并执行核销</p>
|
||||
</article>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<p v-if="kuaishouEticketResultError" class="error-copy">{{ kuaishouEticketResultError }}</p>
|
||||
</section>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>快手小店核销调试</h3>
|
||||
<p>这里使用“当前调试店铺”的 Cookie 执行查询与核销;`eTicketId / oid / formToken` 都来自查询结果。</p>
|
||||
</div>
|
||||
<div class="section-actions">
|
||||
<el-button :loading="kuaishouEticketDetailing" round @click="handleKuaishouEticketQueryDetail">查询核销信息</el-button>
|
||||
<el-button :loading="kuaishouEticketConsuming" round type="primary" @click="handleKuaishouEticketConsume">执行核销</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kuaishou-debug-layout">
|
||||
<div class="kuaishou-debug-panel">
|
||||
<div class="form-grid">
|
||||
<label class="field-block">
|
||||
<span>调试店铺</span>
|
||||
<select v-model="kuaishouEticketForm.selectedShopId" class="text-input">
|
||||
<option value="">请选择店铺</option>
|
||||
<option v-for="shop in kuaishouEticketShops" :key="shop.id" :value="shop.id">
|
||||
{{ shop.kshopName || shop.shopId || '未命名店铺' }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>查询券码</span>
|
||||
<input v-model="kuaishouEticketForm.queryTicketCode" class="text-input" placeholder="用于查询核销信息" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>核销数量</span>
|
||||
<input v-model.number="kuaishouEticketForm.num" class="text-input" type="number" min="1" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>storeId</span>
|
||||
<input v-model="kuaishouEticketForm.storeId" class="text-input" placeholder="默认 0" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kuaishou-debug-panel kuaishou-debug-panel-soft">
|
||||
<div class="kuaishou-debug-head">
|
||||
<div>
|
||||
<span class="result-label">自动回填参数</span>
|
||||
<strong>查询后生成</strong>
|
||||
</div>
|
||||
<span class="cell-subtle">这些字段不需要手动填写</span>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<label class="field-block">
|
||||
<span>eTicketId</span>
|
||||
<input :value="kuaishouEticketForm.eTicketId" class="text-input" readonly placeholder="查询后自动回填" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>oid</span>
|
||||
<input :value="kuaishouEticketForm.oid" class="text-input" readonly placeholder="查询后自动回填" />
|
||||
</label>
|
||||
|
||||
<label class="field-block field-wide">
|
||||
<span>formToken</span>
|
||||
<input :value="kuaishouEticketForm.formToken" class="text-input" readonly placeholder="查询后自动回填" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-grid">
|
||||
<article class="result-card">
|
||||
<span class="result-label">详情查询</span>
|
||||
<strong>{{ kuaishouEticketDetailResult ? (kuaishouEticketDetailResult.ok ? '成功' : '已返回') : '未执行' }}</strong>
|
||||
<p v-if="kuaishouEticketDetailResult?.detail">
|
||||
{{ kuaishouEticketDetailResult.goods?.itemTitle || kuaishouEticketDetailResult.goods?.skuDesc || '-' }},
|
||||
剩余 {{ kuaishouEticketDetailResult.detail.leftCount }} / {{ kuaishouEticketDetailResult.detail.totalCount }}
|
||||
</p>
|
||||
<p v-else>先通过查询券码获取核销详情并自动带出核销参数。</p>
|
||||
</article>
|
||||
|
||||
<article class="result-card">
|
||||
<span class="result-label">核销执行</span>
|
||||
<strong>{{ kuaishouEticketConsumeResult ? (kuaishouEticketConsumeResult.consumed ? '已处理' : '已返回') : '未执行' }}</strong>
|
||||
<p v-if="kuaishouEticketConsumeResult">
|
||||
{{ kuaishouEticketConsumeResult.errorMessage || (kuaishouEticketConsumeResult.ok ? '核销成功' : '接口已返回结果') }}
|
||||
</p>
|
||||
<p v-else>查询详情后再执行核销会更稳妥。</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="kuaishouEticketDetailResult" class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>核销详情返回</h3>
|
||||
<p>成功查询后会自动回填 `eTicketId`、`oid`、`formToken` 和建议核销数量。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pre class="debug-result">{{ JSON.stringify(kuaishouEticketDetailResult.raw, null, 2) }}</pre>
|
||||
</section>
|
||||
|
||||
<section v-if="kuaishouEticketConsumeResult" class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>核销执行返回</h3>
|
||||
<p>这里保留接口原始响应,方便核对 `result`、`error_msg` 和 `requestId`。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<pre class="debug-result">{{ JSON.stringify(kuaishouEticketConsumeResult.raw, null, 2) }}</pre>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else-if="isDevMode">
|
||||
<section class="meta-card">
|
||||
<div class="meta-line">
|
||||
@@ -1497,7 +2140,7 @@ onMounted(loadConfigs)
|
||||
|
||||
.overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
@@ -1618,6 +2261,29 @@ onMounted(loadConfigs)
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.kuaishou-workspace,
|
||||
.kuaishou-debug-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.7fr) minmax(280px, 0.9fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.kuaishou-shop-stack,
|
||||
.kuaishou-side-panel,
|
||||
.kuaishou-debug-panel {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.kuaishou-base-grid {
|
||||
padding: 14px;
|
||||
border-radius: 18px;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(59, 130, 246, 0.08), transparent 38%),
|
||||
linear-gradient(180deg, rgba(239, 246, 255, 0.86), rgba(248, 250, 252, 0.96));
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
}
|
||||
|
||||
.text-input,
|
||||
.text-area {
|
||||
width: 100%;
|
||||
@@ -1642,6 +2308,10 @@ onMounted(loadConfigs)
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shop-card:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.shop-summary {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
@@ -1672,6 +2342,152 @@ onMounted(loadConfigs)
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.kuaishou-shop-card {
|
||||
margin-top: 0;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.98));
|
||||
border-color: rgba(96, 165, 250, 0.18);
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.kuaishou-shop-card.is-active {
|
||||
border-color: rgba(37, 99, 235, 0.34);
|
||||
box-shadow: 0 18px 44px rgba(37, 99, 235, 0.12);
|
||||
}
|
||||
|
||||
.kuaishou-shop-card.is-expanded {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.kuaishou-shop-summary {
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.kuaishou-shop-head,
|
||||
.shop-title-row,
|
||||
.shop-tag-row,
|
||||
.kuaishou-debug-head {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.kuaishou-shop-head {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.shop-title-row,
|
||||
.shop-tag-row,
|
||||
.kuaishou-debug-head {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.shop-title-row {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.shop-avatar {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #dbeafe, #eff6ff);
|
||||
color: #1d4ed8;
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid rgba(37, 99, 235, 0.14);
|
||||
}
|
||||
|
||||
.shop-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.shop-mini-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 26px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(15, 23, 42, 0.05);
|
||||
color: #475467;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.shop-mini-tag.is-ready {
|
||||
background: rgba(22, 163, 74, 0.12);
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.shop-mini-tag.is-highlight {
|
||||
background: rgba(37, 99, 235, 0.12);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.kuaishou-shop-body {
|
||||
padding-top: 16px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.74), rgba(248, 250, 252, 0.96));
|
||||
}
|
||||
|
||||
.kuaishou-shop-actions {
|
||||
margin-top: 14px;
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
.kuaishou-cookie-area {
|
||||
min-height: 128px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.kuaishou-side-panel {
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.kuaishou-side-card {
|
||||
min-height: 132px;
|
||||
}
|
||||
|
||||
.kuaishou-guide-card {
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(37, 99, 235, 0.08), transparent 35%),
|
||||
linear-gradient(180deg, rgba(239, 246, 255, 0.9), rgba(248, 250, 252, 0.98));
|
||||
}
|
||||
|
||||
.kuaishou-guide-card p {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.kuaishou-debug-panel {
|
||||
padding: 16px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.16);
|
||||
background: rgba(248, 250, 252, 0.76);
|
||||
}
|
||||
|
||||
.kuaishou-debug-panel-soft {
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(59, 130, 246, 0.08), transparent 34%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.98));
|
||||
}
|
||||
|
||||
.kuaishou-debug-head {
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.kuaishou-debug-head strong {
|
||||
display: block;
|
||||
margin-top: 4px;
|
||||
color: #1d3555;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.shop-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1791,7 +2607,9 @@ onMounted(loadConfigs)
|
||||
@media (max-width: 1100px) {
|
||||
.overview-grid,
|
||||
.result-grid,
|
||||
.form-grid {
|
||||
.form-grid,
|
||||
.kuaishou-workspace,
|
||||
.kuaishou-debug-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -1805,6 +2623,14 @@ onMounted(loadConfigs)
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.kuaishou-shop-head,
|
||||
.shop-title-row,
|
||||
.shop-tag-row,
|
||||
.kuaishou-debug-head {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.shop-summary-side {
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user