优化了一些文件 增加多店铺
This commit is contained in:
@@ -54,6 +54,25 @@ npm run start
|
||||
- OCR 子服务目录
|
||||
- 会话调试开关
|
||||
- 兑换证明模式 `full | basic | off`
|
||||
- Agiso 全局配置与后台店铺文件配置
|
||||
|
||||
### Agiso 多店铺
|
||||
|
||||
当前支持同一个 Agiso 应用下配置多个店铺的消息 token。
|
||||
|
||||
- 全局兜底配置仍走 `.env`:
|
||||
- `AGISO_APP_SECRET`
|
||||
- `AGISO_MESSAGING_ENABLED`
|
||||
- `AGISO_APP_ID`
|
||||
- `AGISO_ACCESS_TOKEN`
|
||||
- `AGISO_MESSAGE_APP_SECRET`
|
||||
- `AGISO_MESSAGE_TEMPLATE`
|
||||
- 店铺级覆盖配置改为文件:
|
||||
- `apps/backend/data/agiso-shops.json`
|
||||
- 后台维护入口:
|
||||
- `#/admin/platform-shops`
|
||||
|
||||
消息发送时会按 webhook 识别出的 `shop_id` 优先读取 `agiso-shops.json`;如果没有命中,再回退到全局 `AGISO_ACCESS_TOKEN`。
|
||||
|
||||
## 接口
|
||||
|
||||
|
||||
@@ -74,6 +74,17 @@ module.exports = {
|
||||
// Agiso 开放平台应用密钥,用于 webhook 验签。
|
||||
appSecret: '',
|
||||
|
||||
tradeDetail: {
|
||||
// 咸鱼 webhook 不带金额时,用这个接口按订单号补查订单详情。
|
||||
endpoint: 'https://gw-api.agiso.com/aldsIdle/Order/Detail',
|
||||
|
||||
// Header 中的 ApiVersion。
|
||||
apiVersion: '1',
|
||||
|
||||
// 请求超时,单位毫秒。
|
||||
timeoutMs: 5000,
|
||||
},
|
||||
|
||||
messaging: {
|
||||
// 是否启用 Agiso 站内消息发送。
|
||||
enabled: false,
|
||||
@@ -98,6 +109,10 @@ module.exports = {
|
||||
|
||||
// 发送给用户的默认消息模板。
|
||||
messageTemplate: '您的订单 {platformOrderId} 已创建领取链接,请在 {expiredAt} 前完成领取:{claimUrl}',
|
||||
|
||||
// 平台内多店铺配置,按 shopId 做覆盖。
|
||||
// 常见场景:同一个 Agiso 应用下,不同店铺使用不同 AccessToken。
|
||||
shops: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"693760716": {
|
||||
"shopName": "大锤商行",
|
||||
"accessToken": "AldsIdlefmykv7w6xdvgf9r2ymsxrmznux74tyaehc9pwygdnrgr4"
|
||||
},
|
||||
"2037495620": {
|
||||
"shopName": "羊小胖。",
|
||||
"accessToken": "AldsIdle4925bh8c9n3ka79fxauc9egk7uea6f6frfbbb5btvxpvu"
|
||||
}
|
||||
}
|
||||
@@ -167,6 +167,21 @@ function applyEnvOverrides(baseConfig) {
|
||||
nextConfig.platforms.agiso.appSecret = agisoAppSecret
|
||||
}
|
||||
|
||||
const agisoTradeDetailEndpoint = String(process.env.AGISO_TRADE_DETAIL_ENDPOINT || '').trim()
|
||||
if (agisoTradeDetailEndpoint) {
|
||||
nextConfig.platforms.agiso.tradeDetail.endpoint = agisoTradeDetailEndpoint
|
||||
}
|
||||
|
||||
const agisoTradeDetailApiVersion = String(process.env.AGISO_TRADE_DETAIL_API_VERSION || '').trim()
|
||||
if (agisoTradeDetailApiVersion) {
|
||||
nextConfig.platforms.agiso.tradeDetail.apiVersion = agisoTradeDetailApiVersion
|
||||
}
|
||||
|
||||
const agisoTradeDetailTimeoutMs = parseInteger(process.env.AGISO_TRADE_DETAIL_TIMEOUT_MS)
|
||||
if (agisoTradeDetailTimeoutMs !== null) {
|
||||
nextConfig.platforms.agiso.tradeDetail.timeoutMs = agisoTradeDetailTimeoutMs
|
||||
}
|
||||
|
||||
const agisoAppId = String(process.env.AGISO_APP_ID || '').trim()
|
||||
if (agisoAppId) {
|
||||
nextConfig.platforms.agiso.messaging.appId = agisoAppId
|
||||
@@ -202,6 +217,11 @@ function applyEnvOverrides(baseConfig) {
|
||||
nextConfig.platforms.agiso.messaging.messageTemplate = agisoMessageTemplate
|
||||
}
|
||||
|
||||
const agisoShops = parseJsonObject(process.env.AGISO_SHOPS_JSON)
|
||||
if (agisoShops) {
|
||||
nextConfig.platforms.agiso.messaging.shops = normalizeAgisoMessagingShops(agisoShops)
|
||||
}
|
||||
|
||||
return nextConfig
|
||||
}
|
||||
|
||||
@@ -308,3 +328,39 @@ function parseJsonArray(rawValue) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAgisoMessagingShops(rawValue) {
|
||||
const output = {}
|
||||
|
||||
for (const [shopId, config] of Object.entries(rawValue || {})) {
|
||||
const normalizedShopId = String(shopId || '').trim()
|
||||
if (!normalizedShopId || !isPlainObject(config)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const next = {}
|
||||
const enabled = normalizeBooleanLike(config.enabled)
|
||||
if (enabled !== null) {
|
||||
next.enabled = enabled
|
||||
}
|
||||
|
||||
for (const key of ['shopName', 'accessToken', 'messageTemplate', 'appSecret', 'apiVersion', 'sendMessageEndpoint']) {
|
||||
const value = String(config[key] || '').trim()
|
||||
if (value) {
|
||||
next[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
output[normalizedShopId] = next
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
function normalizeBooleanLike(value) {
|
||||
if (typeof value === 'boolean') {
|
||||
return value
|
||||
}
|
||||
|
||||
return parseBoolean(value)
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ import { runDatabaseMigrations } from './db/migrate.js'
|
||||
import adminRouter from './routes/admin.js'
|
||||
import claimsRouter from './routes/claims.js'
|
||||
import webhooksRouter from './routes/webhooks.js'
|
||||
import { ensureAdminUsersBootstrapped } from './services/admin-auth-service.js'
|
||||
import { closeLocalOcrWorker, warmupLocalOcrWorker } from './services/ocr.js'
|
||||
import { closeAllTencentBrowserSessions, warmupTencentBrowser } from './services/session.js'
|
||||
import { ensureAdminUsersBootstrapped } from './services/admin/admin-auth-service.js'
|
||||
import { closeLocalOcrWorker, warmupLocalOcrWorker } from './services/session/ocr.js'
|
||||
import { closeAllTencentBrowserSessions, warmupTencentBrowser } from './services/session/session.js'
|
||||
import { logError, logInfo, logWarn } from './utils/logger.js'
|
||||
import tencentRouter from './routes/tencent.js'
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Router } from 'express'
|
||||
import {
|
||||
closeAdminTask,
|
||||
createAdminCdk,
|
||||
getAdminAgisoShopConfigs,
|
||||
getAdminCdks,
|
||||
getAdminDashboardSummary,
|
||||
getAdminOrderDetail,
|
||||
@@ -20,7 +21,8 @@ import {
|
||||
regenerateAdminTaskClaimLink,
|
||||
releaseAdminTaskCdk,
|
||||
retryAdminTask,
|
||||
} from '../services/admin-service.js'
|
||||
updateAdminAgisoShopConfigs,
|
||||
} from '../services/admin/admin-service.js'
|
||||
import {
|
||||
createManagedAdminUser,
|
||||
getAdminUserList,
|
||||
@@ -31,8 +33,8 @@ import {
|
||||
updateManagedAdminUserRole,
|
||||
updateManagedAdminUserStatus,
|
||||
verifyAdminSessionToken,
|
||||
} from '../services/admin-auth-service.js'
|
||||
import { getAdminAuditLogs, writeAdminAuditLog } from '../services/admin-audit-service.js'
|
||||
} from '../services/admin/admin-auth-service.js'
|
||||
import { getAdminAuditLogs, writeAdminAuditLog } from '../services/admin/admin-audit-service.js'
|
||||
import { buildNotFoundPayload, buildSuccessPayload, createHttpError, sendRouteError } from '../utils/http.js'
|
||||
|
||||
const router = Router()
|
||||
@@ -173,6 +175,35 @@ router.get('/audit-logs', (req, res) => {
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/platform-config/agiso-shops', (req, res) => {
|
||||
try {
|
||||
requireAdminRole(req.adminSession, ['admin'])
|
||||
const data = getAdminAgisoShopConfigs()
|
||||
res.json(buildSuccessPayload(data, 'ok'))
|
||||
} catch (error) {
|
||||
sendRouteError(res, error, '读取 Agiso 店铺配置失败', '[admin/platform-config/agiso-shops]')
|
||||
}
|
||||
})
|
||||
|
||||
router.post('/platform-config/agiso-shops', (req, res) => {
|
||||
try {
|
||||
requireAdminRole(req.adminSession, ['admin'])
|
||||
const data = updateAdminAgisoShopConfigs(req.body)
|
||||
writeAdminAuditLog(req.adminSession, {
|
||||
action: 'platform_shop_config_updated',
|
||||
targetType: 'platform_config',
|
||||
targetId: 'agiso_shops',
|
||||
data: {
|
||||
shopCount: data.shops.length,
|
||||
filePath: data.filePath,
|
||||
},
|
||||
})
|
||||
res.json(buildSuccessPayload(data, 'Agiso 店铺配置已保存'))
|
||||
} catch (error) {
|
||||
sendRouteError(res, error, '保存 Agiso 店铺配置失败', '[admin/platform-config/agiso-shops]')
|
||||
}
|
||||
})
|
||||
|
||||
router.get('/orders', (req, res) => {
|
||||
try {
|
||||
const data = getAdminOrders(req.query)
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
getClaimScreenshotPath,
|
||||
getClaimSessionSummary,
|
||||
redeemClaimTask,
|
||||
} from '../services/claim-session-service.js'
|
||||
} from '../services/claim/claim-session-service.js'
|
||||
import { buildNotFoundPayload, buildSuccessPayload, sendRouteError } from '../utils/http.js'
|
||||
|
||||
const router = Router()
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
getTencentBrowserSessionScreenshotPath,
|
||||
reloadTencentBrowserSession,
|
||||
redeemTencentBrowserSession,
|
||||
} from '../services/session.js'
|
||||
} from '../services/session/session.js'
|
||||
import {
|
||||
buildNotFoundPayload,
|
||||
buildSuccessPayload,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Router } from 'express'
|
||||
|
||||
import { processAgisoTradeWebhook } from '../services/webhook-service.js'
|
||||
import { processAgisoTradeWebhook } from '../services/order/webhook-service.js'
|
||||
import { createRequestId, logWebhook } from '../utils/logger.js'
|
||||
import { buildNotFoundPayload, buildSuccessPayload, sendRouteError } from '../utils/http.js'
|
||||
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { createAdminAuditLog, listAdminAuditLogs } from '../repositories/admin-audit-log-repo.js'
|
||||
import { nowIso } from '../utils/time.js'
|
||||
import { createAdminAuditLog, listAdminAuditLogs } from '../../repositories/admin-audit-log-repo.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
|
||||
export function writeAdminAuditLog(session, payload = {}) {
|
||||
if (!session?.userId) {
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
import { runtimeConfig } from '../config/runtime.js'
|
||||
import { runtimeConfig } from '../../config/runtime.js'
|
||||
import {
|
||||
countActiveAdminUsers,
|
||||
createAdminUser,
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
getAdminUserByUsername,
|
||||
listAdminUsers,
|
||||
updateAdminUser,
|
||||
} from '../repositories/admin-user-repo.js'
|
||||
import { addHours, nowIso } from '../utils/time.js'
|
||||
import { createHttpError } from '../utils/http.js'
|
||||
} from '../../repositories/admin-user-repo.js'
|
||||
import { addHours, nowIso } from '../../utils/time.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
|
||||
export async function ensureAdminUsersBootstrapped() {
|
||||
ensureAdminAuthConfigured()
|
||||
+186
-24
@@ -1,16 +1,23 @@
|
||||
import { getDb } from '../db/client.js'
|
||||
import { createTaskClaimToken } from './claim-service.js'
|
||||
import { getClaimTokenById, updateClaimToken } from '../repositories/claim-token-repo.js'
|
||||
import { createCdks, getCdkById, invalidateCdk, listCdks, releaseReservedCdk } from '../repositories/cdk-repo.js'
|
||||
import { listOrderItemsByOrderId } from '../repositories/order-item-repo.js'
|
||||
import { getOrderById, listOrders } from '../repositories/order-repo.js'
|
||||
import { getTaskById, listTasks, listTasksByOrderId, updateTask } from '../repositories/task-repo.js'
|
||||
import { getWebhookEventById, listWebhookEvents, listWebhookEventsByOrderId } from '../repositories/webhook-event-repo.js'
|
||||
import { createHttpError } from '../utils/http.js'
|
||||
import { formatFenToAmount, normalizeFen, parseAmountToFen } from '../utils/money.js'
|
||||
import { nowIso } from '../utils/time.js'
|
||||
import { reserveCdkForTask } from './cdk-service.js'
|
||||
import { replayAgisoTradeWebhookEvent } from './webhook-service.js'
|
||||
import { runtimeConfig } from '../../config/runtime.js'
|
||||
import { getDb } from '../../db/client.js'
|
||||
import { createTaskClaimToken } from '../claim/claim-service.js'
|
||||
import {
|
||||
getAgisoShopConfig,
|
||||
getAgisoShopConfigMap,
|
||||
getAgisoShopsFilePath,
|
||||
saveAgisoShopConfigMap,
|
||||
} from '../platforms/agiso/shop-config-service.js'
|
||||
import { getClaimTokenById, updateClaimToken } from '../../repositories/claim-token-repo.js'
|
||||
import { createCdks, getCdkById, invalidateCdk, listCdks, releaseReservedCdk } from '../../repositories/cdk-repo.js'
|
||||
import { listOrderItemsByOrderId } from '../../repositories/order-item-repo.js'
|
||||
import { getOrderById, listOrders } from '../../repositories/order-repo.js'
|
||||
import { getTaskById, listTasks, listTasksByOrderId, updateTask } from '../../repositories/task-repo.js'
|
||||
import { getWebhookEventById, listWebhookEvents, listWebhookEventsByOrderId } from '../../repositories/webhook-event-repo.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { formatFenToAmount, normalizeFen, parseAmountToFen } from '../../utils/money.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { reserveCdkForTask } from '../order/cdk-service.js'
|
||||
import { replayAgisoTradeWebhookEvent } from '../order/webhook-service.js'
|
||||
|
||||
export function getAdminDashboardSummary() {
|
||||
const db = getDb()
|
||||
@@ -74,7 +81,7 @@ export function getAdminOrderDetail(orderId) {
|
||||
provider: order.provider || 'agiso',
|
||||
platform: order.platform,
|
||||
shopId: order.shop_id || '',
|
||||
shopName: order.shop_name || '',
|
||||
shopName: resolveDisplayShopName(order.provider, order.shop_id, order.shop_name),
|
||||
platformOrderId: order.platform_order_id,
|
||||
orderStatus: order.order_status,
|
||||
payStatus: order.pay_status,
|
||||
@@ -104,7 +111,7 @@ export function getAdminOrderDetail(orderId) {
|
||||
provider: event.provider || 'agiso',
|
||||
platform: event.platform,
|
||||
shopId: event.shop_id || '',
|
||||
shopName: event.shop_name || '',
|
||||
shopName: resolveDisplayShopName(event.provider, event.shop_id, event.shop_name),
|
||||
eventType: event.event_type,
|
||||
eventKey: event.event_key,
|
||||
signatureValid: Boolean(event.signature_valid),
|
||||
@@ -402,6 +409,113 @@ export async function replayAdminWebhookEvent(eventId) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getAdminAgisoShopConfigs() {
|
||||
const configMap = getAgisoShopConfigMap()
|
||||
const rows = getDb().prepare(`
|
||||
SELECT
|
||||
shop_id,
|
||||
MAX(CASE WHEN trim(shop_name) != '' THEN shop_name ELSE '' END) AS detected_shop_name,
|
||||
MAX(created_at) AS latest_seen_at,
|
||||
COUNT(*) AS webhook_event_count
|
||||
FROM webhook_events
|
||||
WHERE provider = 'agiso' AND trim(shop_id) != ''
|
||||
GROUP BY shop_id
|
||||
ORDER BY latest_seen_at DESC, shop_id DESC
|
||||
`).all()
|
||||
|
||||
return {
|
||||
filePath: getAgisoShopsFilePath(),
|
||||
shops: Object.entries(configMap)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([shopId, config]) => ({
|
||||
shopId,
|
||||
shopName: String(config.shopName || '').trim(),
|
||||
accessToken: String(config.accessToken || '').trim(),
|
||||
accessTokenMasked: maskSecret(config.accessToken),
|
||||
enabled: typeof config.enabled === 'boolean' ? config.enabled : null,
|
||||
messageTemplate: String(config.messageTemplate || '').trim(),
|
||||
appSecretConfigured: Boolean(String(config.appSecret || '').trim()),
|
||||
apiVersion: String(config.apiVersion || '').trim(),
|
||||
sendMessageEndpoint: String(config.sendMessageEndpoint || '').trim(),
|
||||
})),
|
||||
observedShops: rows.map((row) => ({
|
||||
shopId: String(row.shop_id || '').trim(),
|
||||
detectedShopName: String(row.detected_shop_name || '').trim(),
|
||||
displayShopName: resolveDisplayShopName('agiso', row.shop_id, row.detected_shop_name),
|
||||
latestSeenAt: row.latest_seen_at || null,
|
||||
webhookEventCount: Number(row.webhook_event_count || 0),
|
||||
configured: Boolean(configMap[String(row.shop_id || '').trim()]),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export function updateAdminAgisoShopConfigs(payload = {}) {
|
||||
const rawItems = Array.isArray(payload.shops) ? payload.shops : []
|
||||
const nextMap = {}
|
||||
|
||||
for (const item of rawItems) {
|
||||
const shopId = String(item?.shopId || '').trim()
|
||||
if (!shopId) {
|
||||
continue
|
||||
}
|
||||
|
||||
const next = {}
|
||||
const shopName = String(item?.shopName || '').trim()
|
||||
const accessToken = String(item?.accessToken || '').trim()
|
||||
const messageTemplate = String(item?.messageTemplate || '').trim()
|
||||
const appSecret = String(item?.appSecret || '').trim()
|
||||
const apiVersion = String(item?.apiVersion || '').trim()
|
||||
const sendMessageEndpoint = String(item?.sendMessageEndpoint || '').trim()
|
||||
|
||||
if (shopName) {
|
||||
next.shopName = shopName
|
||||
}
|
||||
if (accessToken) {
|
||||
next.accessToken = accessToken
|
||||
}
|
||||
if (messageTemplate) {
|
||||
next.messageTemplate = messageTemplate
|
||||
}
|
||||
if (appSecret) {
|
||||
next.appSecret = appSecret
|
||||
}
|
||||
if (apiVersion) {
|
||||
next.apiVersion = apiVersion
|
||||
}
|
||||
if (sendMessageEndpoint) {
|
||||
next.sendMessageEndpoint = sendMessageEndpoint
|
||||
}
|
||||
if (typeof item?.enabled === 'boolean') {
|
||||
next.enabled = item.enabled
|
||||
}
|
||||
|
||||
if (!next.accessToken) {
|
||||
continue
|
||||
}
|
||||
|
||||
nextMap[shopId] = next
|
||||
}
|
||||
|
||||
const saved = saveAgisoShopConfigMap(nextMap)
|
||||
|
||||
return {
|
||||
filePath: getAgisoShopsFilePath(),
|
||||
shops: Object.entries(saved)
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([shopId, config]) => ({
|
||||
shopId,
|
||||
shopName: String(config.shopName || '').trim(),
|
||||
accessToken: String(config.accessToken || '').trim(),
|
||||
accessTokenMasked: maskSecret(config.accessToken),
|
||||
enabled: typeof config.enabled === 'boolean' ? config.enabled : null,
|
||||
messageTemplate: String(config.messageTemplate || '').trim(),
|
||||
appSecretConfigured: Boolean(String(config.appSecret || '').trim()),
|
||||
apiVersion: String(config.apiVersion || '').trim(),
|
||||
sendMessageEndpoint: String(config.sendMessageEndpoint || '').trim(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export function retryAdminTask(taskId) {
|
||||
const task = getRequiredTask(taskId)
|
||||
const now = nowIso()
|
||||
@@ -626,14 +740,28 @@ function mapAdminWebhookEvent(item, { includeRaw = false } = {}) {
|
||||
payload.sellerNick,
|
||||
payload.SellerNick,
|
||||
]),
|
||||
shopName: item.shop_name || pickFirstNonEmpty([
|
||||
payload.shopName,
|
||||
payload.shop_name,
|
||||
payload.ShopName,
|
||||
payload.seller_nick,
|
||||
payload.sellerNick,
|
||||
payload.SellerNick,
|
||||
]),
|
||||
shopName: resolveDisplayShopName(
|
||||
item.provider,
|
||||
item.shop_id || pickFirstNonEmpty([
|
||||
payload.PlatformUserId,
|
||||
payload.platformUserId,
|
||||
payload.platform_user_id,
|
||||
payload.seller_id,
|
||||
payload.sellerId,
|
||||
payload.SellerId,
|
||||
payload.seller_nick,
|
||||
payload.sellerNick,
|
||||
payload.SellerNick,
|
||||
]),
|
||||
item.shop_name || pickFirstNonEmpty([
|
||||
payload.shopName,
|
||||
payload.shop_name,
|
||||
payload.ShopName,
|
||||
payload.seller_nick,
|
||||
payload.sellerNick,
|
||||
payload.SellerNick,
|
||||
]),
|
||||
),
|
||||
eventType: item.event_type,
|
||||
eventKey: item.event_key,
|
||||
signatureValid: Boolean(item.signature_valid),
|
||||
@@ -803,7 +931,7 @@ function mapAdminOrderListItem(item) {
|
||||
provider: item.provider || 'agiso',
|
||||
platform: item.platform,
|
||||
shopId: item.shop_id || '',
|
||||
shopName: item.shop_name || '',
|
||||
shopName: resolveDisplayShopName(item.provider, item.shop_id, item.shop_name),
|
||||
platformOrderId: item.platform_order_id,
|
||||
orderStatus: item.order_status,
|
||||
payStatus: item.pay_status,
|
||||
@@ -937,6 +1065,27 @@ function isTaskSystemBound(task) {
|
||||
return Boolean(task && (task.reserved_cdk_id || task.claim_token_id))
|
||||
}
|
||||
|
||||
function resolveDisplayShopName(provider, shopId, shopName) {
|
||||
const normalizedShopName = String(shopName || '').trim()
|
||||
if (normalizedShopName) {
|
||||
return normalizedShopName
|
||||
}
|
||||
|
||||
const normalizedShopId = String(shopId || '').trim()
|
||||
if (!normalizedShopId) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (String(provider || '').trim().toLowerCase() === 'agiso') {
|
||||
const configuredName = String(getAgisoShopConfig(normalizedShopId)?.shopName || '').trim()
|
||||
if (configuredName) {
|
||||
return configuredName
|
||||
}
|
||||
}
|
||||
|
||||
return normalizedShopId
|
||||
}
|
||||
|
||||
function normalizeCdkImportRows(payload) {
|
||||
const rows = []
|
||||
const directRows = Array.isArray(payload.rows) ? payload.rows : []
|
||||
@@ -1030,6 +1179,19 @@ function safeParseJson(rawText) {
|
||||
}
|
||||
}
|
||||
|
||||
function maskSecret(value) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (!normalized) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (normalized.length <= 10) {
|
||||
return `${normalized.slice(0, 2)}****${normalized.slice(-2)}`
|
||||
}
|
||||
|
||||
return `${normalized.slice(0, 6)}****${normalized.slice(-6)}`
|
||||
}
|
||||
|
||||
function normalizeRecord(value) {
|
||||
return value && typeof value === 'object' && !Array.isArray(value) ? value : {}
|
||||
}
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
import { runtimeConfig } from '../config/runtime.js'
|
||||
import { createClaimToken } from '../repositories/claim-token-repo.js'
|
||||
import { addHours, nowIso } from '../utils/time.js'
|
||||
import { randomToken } from '../utils/random.js'
|
||||
import { runtimeConfig } from '../../config/runtime.js'
|
||||
import { createClaimToken } from '../../repositories/claim-token-repo.js'
|
||||
import { addHours, nowIso } from '../../utils/time.js'
|
||||
import { randomToken } from '../../utils/random.js'
|
||||
|
||||
export function createTaskClaimToken(taskId) {
|
||||
const createdAt = nowIso()
|
||||
+9
-9
@@ -3,16 +3,16 @@ import {
|
||||
getTencentBrowserSession,
|
||||
getTencentBrowserSessionScreenshotPath,
|
||||
redeemTencentBrowserSession,
|
||||
} from './session.js'
|
||||
import { findClaimTokenByToken, updateClaimToken } from '../repositories/claim-token-repo.js'
|
||||
import { getOrderById } from '../repositories/order-repo.js'
|
||||
import { getOrderItemById } from '../repositories/order-item-repo.js'
|
||||
import { findTaskByClaimTokenId, updateTask } from '../repositories/task-repo.js'
|
||||
import { getCdkById, markCdkDelivered, releaseReservedCdk } from '../repositories/cdk-repo.js'
|
||||
} from '../session/session.js'
|
||||
import { findClaimTokenByToken, updateClaimToken } from '../../repositories/claim-token-repo.js'
|
||||
import { getOrderById } from '../../repositories/order-repo.js'
|
||||
import { getOrderItemById } from '../../repositories/order-item-repo.js'
|
||||
import { findTaskByClaimTokenId, updateTask } from '../../repositories/task-repo.js'
|
||||
import { getCdkById, markCdkDelivered, releaseReservedCdk } from '../../repositories/cdk-repo.js'
|
||||
import { buildClaimUrl } from './claim-service.js'
|
||||
import { createHttpError } from '../utils/http.js'
|
||||
import { formatFenToAmount, normalizeFen } from '../utils/money.js'
|
||||
import { nowIso } from '../utils/time.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { formatFenToAmount, normalizeFen } from '../../utils/money.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
|
||||
const CLAIM_TERMINAL_STATUSES = new Set(['expired', 'closed'])
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { assignReservedCdk, findFirstAvailableCdkBySkuCode } from '../repositories/cdk-repo.js'
|
||||
import { nowIso } from '../utils/time.js'
|
||||
import { assignReservedCdk, findFirstAvailableCdkBySkuCode } from '../../repositories/cdk-repo.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
|
||||
export function reserveCdkForTask(skuCode, taskId) {
|
||||
if (!skuCode) {
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
import { createTask, listTasksByOrderId, updateTask } from '../repositories/task-repo.js'
|
||||
import { createTask, listTasksByOrderId, updateTask } from '../../repositories/task-repo.js'
|
||||
import { reserveCdkForTask } from './cdk-service.js'
|
||||
import { createTaskClaimToken } from './claim-service.js'
|
||||
import { nowIso } from '../utils/time.js'
|
||||
import { randomId } from '../utils/random.js'
|
||||
import { createTaskClaimToken } from '../claim/claim-service.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { randomId } from '../../utils/random.js'
|
||||
|
||||
export function syncDeliveryTasksForOrder(order, orderItems) {
|
||||
const existingTasks = listTasksByOrderId(order.id)
|
||||
@@ -0,0 +1,177 @@
|
||||
import { getDb, runInTransaction } from '../../db/client.js'
|
||||
import { enrichAgisoXianyuTradeOrder } from '../platforms/agiso/xianyu/order-detail-service.js'
|
||||
import { logInfo } from '../../utils/logger.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { parseJsonObject } from '../../utils/json.js'
|
||||
|
||||
export async function repairAgisoOrderData() {
|
||||
const rows = getDb().prepare(`
|
||||
SELECT
|
||||
o.id,
|
||||
o.provider,
|
||||
o.platform,
|
||||
o.shop_id,
|
||||
o.shop_name,
|
||||
o.platform_order_id,
|
||||
o.total_amount,
|
||||
o.raw_payload_json,
|
||||
o.paid_at,
|
||||
o.buyer_id,
|
||||
o.buyer_name,
|
||||
o.receiver_contact,
|
||||
(
|
||||
SELECT we.body_json
|
||||
FROM webhook_events we
|
||||
WHERE we.related_order_id = o.id
|
||||
ORDER BY we.id DESC
|
||||
LIMIT 1
|
||||
) AS latest_body_json
|
||||
FROM orders o
|
||||
WHERE o.provider = 'agiso' AND o.platform = 'xianyu'
|
||||
ORDER BY o.id ASC
|
||||
`).all()
|
||||
|
||||
if (rows.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
let repairedOrderIdCount = 0
|
||||
let repairedAmountCount = 0
|
||||
|
||||
for (const row of rows) {
|
||||
const latestRequestBody = parseJsonObject(row.latest_body_json)
|
||||
const rawJson = String(latestRequestBody.json || '').trim()
|
||||
const webhookPayload = rawJson ? parseJsonObject(rawJson, { preserveLargeIntegers: true }) : {}
|
||||
const rawPayload = parseJsonObject(row.raw_payload_json, { preserveLargeIntegers: true })
|
||||
const expectedOrderId = pickFirstNonEmpty([
|
||||
webhookPayload.biz_order_id,
|
||||
webhookPayload.order_id,
|
||||
webhookPayload.orderId,
|
||||
rawPayload.biz_order_id,
|
||||
rawPayload.order_id,
|
||||
rawPayload.orderId,
|
||||
])
|
||||
|
||||
const parsed = {
|
||||
provider: row.provider,
|
||||
platform: row.platform,
|
||||
shopId: String(row.shop_id || '').trim(),
|
||||
shopName: String(row.shop_name || '').trim(),
|
||||
platformOrderId: expectedOrderId || String(row.platform_order_id || '').trim(),
|
||||
totalAmount: Number(row.total_amount || 0),
|
||||
buyerId: pickFirstNonEmpty([rawPayload.buyer_id, rawPayload.buyerId, rawPayload.BuyerId, row.buyer_id]),
|
||||
buyerName: pickFirstNonEmpty([rawPayload.buyer_name, rawPayload.buyerName, rawPayload.BuyerName, row.buyer_name]),
|
||||
receiverContact: pickFirstNonEmpty([
|
||||
rawPayload.receiver_contact,
|
||||
rawPayload.receiverContact,
|
||||
rawPayload.receiver_mobile,
|
||||
rawPayload.receiverMobile,
|
||||
rawPayload.mobile,
|
||||
rawPayload.phone,
|
||||
row.receiver_contact,
|
||||
]),
|
||||
paidAt: row.paid_at,
|
||||
rawPayload: Object.keys(webhookPayload).length > 0 ? webhookPayload : rawPayload,
|
||||
items: [],
|
||||
}
|
||||
|
||||
const detailResult = await enrichAgisoXianyuTradeOrder(parsed, {
|
||||
requestId: `repair-order-${row.id}`,
|
||||
})
|
||||
|
||||
const nextOrderId = pickFirstNonEmpty([
|
||||
detailResult.parsed.platformOrderId,
|
||||
expectedOrderId,
|
||||
row.platform_order_id,
|
||||
])
|
||||
const nextAmount = Number(detailResult.parsed.totalAmount || 0)
|
||||
const nextRawPayloadJson = JSON.stringify(detailResult.parsed.rawPayload || rawPayload || {})
|
||||
const nextShopName = pickFirstNonEmpty([detailResult.parsed.shopName, row.shop_name])
|
||||
const nextBuyerId = pickFirstNonEmpty([detailResult.parsed.buyerId, row.buyer_id])
|
||||
const nextBuyerName = pickFirstNonEmpty([detailResult.parsed.buyerName, row.buyer_name])
|
||||
const nextReceiverContact = pickFirstNonEmpty([detailResult.parsed.receiverContact, row.receiver_contact])
|
||||
const nextPaidAt = detailResult.parsed.paidAt || row.paid_at
|
||||
|
||||
const orderIdChanged = nextOrderId && nextOrderId !== String(row.platform_order_id || '')
|
||||
const amountChanged = nextAmount > 0 && nextAmount !== Number(row.total_amount || 0)
|
||||
const payloadChanged = nextRawPayloadJson !== String(row.raw_payload_json || '{}')
|
||||
const metadataChanged = nextShopName !== String(row.shop_name || '')
|
||||
|| nextBuyerId !== String(row.buyer_id || '')
|
||||
|| nextBuyerName !== String(row.buyer_name || '')
|
||||
|| nextReceiverContact !== String(row.receiver_contact || '')
|
||||
|| nextPaidAt !== row.paid_at
|
||||
|
||||
if (!orderIdChanged && !amountChanged && !payloadChanged && !metadataChanged) {
|
||||
continue
|
||||
}
|
||||
|
||||
runInTransaction((db) => {
|
||||
db.prepare(`
|
||||
UPDATE orders
|
||||
SET
|
||||
shop_name = ?,
|
||||
platform_order_id = ?,
|
||||
buyer_id = ?,
|
||||
buyer_name = ?,
|
||||
receiver_contact = ?,
|
||||
total_amount = ?,
|
||||
raw_payload_json = ?,
|
||||
paid_at = ?,
|
||||
updated_at = ?
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
nextShopName,
|
||||
nextOrderId,
|
||||
nextBuyerId,
|
||||
nextBuyerName,
|
||||
nextReceiverContact,
|
||||
nextAmount,
|
||||
nextRawPayloadJson,
|
||||
nextPaidAt,
|
||||
nowIso(),
|
||||
row.id,
|
||||
)
|
||||
|
||||
if (orderIdChanged) {
|
||||
db.prepare('UPDATE delivery_tasks SET platform_order_id = ? WHERE order_id = ?').run(nextOrderId, row.id)
|
||||
db.prepare(`
|
||||
UPDATE message_deliveries
|
||||
SET
|
||||
platform_order_id = ?,
|
||||
recipient_key = CASE
|
||||
WHEN recipient_key = ? THEN ?
|
||||
ELSE recipient_key
|
||||
END,
|
||||
updated_at = ?
|
||||
WHERE order_id = ?
|
||||
`).run(nextOrderId, row.platform_order_id, nextOrderId, nowIso(), row.id)
|
||||
}
|
||||
})
|
||||
|
||||
if (orderIdChanged) {
|
||||
repairedOrderIdCount += 1
|
||||
}
|
||||
|
||||
if (amountChanged) {
|
||||
repairedAmountCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
if (repairedOrderIdCount > 0 || repairedAmountCount > 0) {
|
||||
logInfo('[startup]', '已自动修复历史 Agiso 咸鱼订单数据', {
|
||||
repairedOrderIdCount,
|
||||
repairedAmountCount,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function pickFirstNonEmpty(values) {
|
||||
for (const value of values) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (normalized) {
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
+14
-9
@@ -1,11 +1,11 @@
|
||||
import { createOrder, findOrderByPlatformOrderId, updateOrder } from '../repositories/order-repo.js'
|
||||
import { replaceOrderItems } from '../repositories/order-item-repo.js'
|
||||
import { getClaimTokenById } from '../repositories/claim-token-repo.js'
|
||||
import { buildClaimUrl } from './claim-service.js'
|
||||
import { createOrder, findOrderByPlatformOrderId, updateOrder } from '../../repositories/order-repo.js'
|
||||
import { replaceOrderItems } from '../../repositories/order-item-repo.js'
|
||||
import { getClaimTokenById } from '../../repositories/claim-token-repo.js'
|
||||
import { buildClaimUrl } from '../claim/claim-service.js'
|
||||
import { syncDeliveryTasksForOrder } from './delivery-task-service.js'
|
||||
import { ensureAgisoClaimMessageDeliveredForTask } from './message-service.js'
|
||||
import { nowIso } from '../utils/time.js'
|
||||
import { logWebhook } from '../utils/logger.js'
|
||||
import { ensureAgisoXianyuClaimMessageDeliveredForTask } from '../platforms/agiso/xianyu/message-service.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { logWebhook } from '../../utils/logger.js'
|
||||
|
||||
export async function upsertOrderFromWebhook(event) {
|
||||
const now = nowIso()
|
||||
@@ -81,7 +81,12 @@ export async function upsertOrderFromWebhook(event) {
|
||||
})
|
||||
|
||||
for (const task of tasks) {
|
||||
if (event.provider !== 'agiso' || String(task.task_status || '') !== 'link_generated' || !task.claim_token_id) {
|
||||
if (
|
||||
event.provider !== 'agiso'
|
||||
|| event.platform !== 'xianyu'
|
||||
|| String(task.task_status || '') !== 'link_generated'
|
||||
|| !task.claim_token_id
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -90,7 +95,7 @@ export async function upsertOrderFromWebhook(event) {
|
||||
continue
|
||||
}
|
||||
|
||||
const result = await ensureAgisoClaimMessageDeliveredForTask({
|
||||
const result = await ensureAgisoXianyuClaimMessageDeliveredForTask({
|
||||
order,
|
||||
task,
|
||||
claimUrl: buildClaimUrl(claimToken.token),
|
||||
+6
-6
@@ -1,12 +1,12 @@
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
import { runtimeConfig } from '../config/runtime.js'
|
||||
import { createWebhookEvent, updateWebhookEvent } from '../repositories/webhook-event-repo.js'
|
||||
import { runtimeConfig } from '../../config/runtime.js'
|
||||
import { createWebhookEvent, updateWebhookEvent } from '../../repositories/webhook-event-repo.js'
|
||||
import { upsertOrderFromWebhook } from './order-service.js'
|
||||
import { createHttpError } from '../utils/http.js'
|
||||
import { logWebhook } from '../utils/logger.js'
|
||||
import { parseAmountToFen } from '../utils/money.js'
|
||||
import { nowIso } from '../utils/time.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { logWebhook } from '../../utils/logger.js'
|
||||
import { parseAmountToFen } from '../../utils/money.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
|
||||
export async function processAgisoTradeWebhook(requestLike) {
|
||||
const parsed = parseAgisoTradeRequest(requestLike)
|
||||
@@ -0,0 +1,9 @@
|
||||
## Agiso Platform Services
|
||||
|
||||
Keep Agiso-wide shared helpers in this directory.
|
||||
|
||||
Per-platform integrations live in subdirectories:
|
||||
|
||||
- `xianyu/`
|
||||
- `pdd/`
|
||||
- `taobao/`
|
||||
@@ -0,0 +1,3 @@
|
||||
## PDD
|
||||
|
||||
Put all Agiso PDD-specific service integrations here.
|
||||
@@ -0,0 +1,103 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { PROJECT_ROOT, runtimeConfig } from '../../../config/runtime.js'
|
||||
|
||||
const AGISO_SHOPS_FILE_PATH = path.join(PROJECT_ROOT, 'data', 'agiso-shops.json')
|
||||
|
||||
export function getAgisoShopsFilePath() {
|
||||
return AGISO_SHOPS_FILE_PATH
|
||||
}
|
||||
|
||||
export function getAgisoShopConfigMap() {
|
||||
const envConfig = normalizeAgisoShopConfigMap(runtimeConfig.platforms?.agiso?.messaging?.shops || {})
|
||||
const fileConfig = loadAgisoShopConfigMapFromFile()
|
||||
|
||||
return {
|
||||
...envConfig,
|
||||
...fileConfig,
|
||||
}
|
||||
}
|
||||
|
||||
export function getAgisoShopConfig(shopId) {
|
||||
const normalizedShopId = String(shopId || '').trim()
|
||||
if (!normalizedShopId) {
|
||||
return null
|
||||
}
|
||||
|
||||
return getAgisoShopConfigMap()[normalizedShopId] || null
|
||||
}
|
||||
|
||||
export function saveAgisoShopConfigMap(rawValue) {
|
||||
const normalized = normalizeAgisoShopConfigMap(rawValue)
|
||||
fs.mkdirSync(path.dirname(AGISO_SHOPS_FILE_PATH), { recursive: true })
|
||||
fs.writeFileSync(AGISO_SHOPS_FILE_PATH, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8')
|
||||
return normalized
|
||||
}
|
||||
|
||||
function loadAgisoShopConfigMapFromFile() {
|
||||
if (!fs.existsSync(AGISO_SHOPS_FILE_PATH)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
try {
|
||||
const rawText = fs.readFileSync(AGISO_SHOPS_FILE_PATH, 'utf8')
|
||||
const parsed = JSON.parse(rawText)
|
||||
return normalizeAgisoShopConfigMap(parsed)
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAgisoShopConfigMap(rawValue) {
|
||||
const output = {}
|
||||
|
||||
for (const [shopId, config] of Object.entries(rawValue || {})) {
|
||||
const normalizedShopId = String(shopId || '').trim()
|
||||
if (!normalizedShopId || !isPlainObject(config)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const next = {}
|
||||
const enabled = normalizeBooleanLike(config.enabled)
|
||||
if (enabled !== null) {
|
||||
next.enabled = enabled
|
||||
}
|
||||
|
||||
for (const key of ['shopName', 'accessToken', 'messageTemplate', 'appSecret', 'apiVersion', 'sendMessageEndpoint', 'tradeDetailEndpoint']) {
|
||||
const value = String(config[key] || '').trim()
|
||||
if (value) {
|
||||
next[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
output[normalizedShopId] = next
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
function normalizeBooleanLike(value) {
|
||||
if (typeof value === 'boolean') {
|
||||
return value
|
||||
}
|
||||
|
||||
const normalized = String(value || '').trim().toLowerCase()
|
||||
if (!normalized) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (['1', 'true', 'yes', 'on'].includes(normalized)) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (['0', 'false', 'no', 'off'].includes(normalized)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
## Taobao
|
||||
|
||||
Put all Agiso Taobao-specific service integrations here.
|
||||
@@ -0,0 +1,3 @@
|
||||
## Xianyu
|
||||
|
||||
Put all Agiso Xianyu-specific service integrations here.
|
||||
+39
-11
@@ -1,20 +1,22 @@
|
||||
import { runtimeConfig } from '../config/runtime.js'
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
import { runtimeConfig } from '../../../../config/runtime.js'
|
||||
import { getAgisoShopConfigMap } from '../shop-config-service.js'
|
||||
import {
|
||||
createMessageDelivery,
|
||||
findLatestSuccessfulMessageDeliveryByTask,
|
||||
updateMessageDelivery,
|
||||
} from '../repositories/message-delivery-repo.js'
|
||||
import { nowIso } from '../utils/time.js'
|
||||
} from '../../../../repositories/message-delivery-repo.js'
|
||||
import { nowIso } from '../../../../utils/time.js'
|
||||
|
||||
const AGISO_MESSAGE_CHANNEL = 'agiso_im'
|
||||
const AGISO_XIANYU_MESSAGE_CHANNEL = 'agiso_im'
|
||||
|
||||
export async function ensureAgisoClaimMessageDeliveredForTask({ order, task, claimUrl, expiredAt }) {
|
||||
export async function ensureAgisoXianyuClaimMessageDeliveredForTask({ order, task, claimUrl, expiredAt }) {
|
||||
if (!order || !task || !claimUrl) {
|
||||
return { sent: false, skipped: true, reason: 'missing_message_context' }
|
||||
}
|
||||
|
||||
const config = runtimeConfig.platforms?.agiso?.messaging || {}
|
||||
const config = resolveAgisoXianyuMessagingConfig(order)
|
||||
if (!config.enabled) {
|
||||
return { sent: false, skipped: true, reason: 'messaging_disabled' }
|
||||
}
|
||||
@@ -26,12 +28,19 @@ export async function ensureAgisoClaimMessageDeliveredForTask({ order, task, cla
|
||||
return { sent: false, skipped: true, reason: 'missing_endpoint_or_access_token_or_app_secret' }
|
||||
}
|
||||
|
||||
const successful = findLatestSuccessfulMessageDeliveryByTask(task.id, AGISO_MESSAGE_CHANNEL)
|
||||
const successful = findLatestSuccessfulMessageDeliveryByTask(task.id, AGISO_XIANYU_MESSAGE_CHANNEL)
|
||||
if (successful) {
|
||||
return { sent: false, skipped: true, reason: 'already_sent', deliveryId: successful.id }
|
||||
}
|
||||
|
||||
const messageContent = renderAgisoClaimMessage({ order, task, claimUrl, expiredAt, template: config.messageTemplate })
|
||||
const messageContent = renderAgisoClaimMessage({
|
||||
order,
|
||||
task,
|
||||
claimUrl,
|
||||
expiredAt,
|
||||
template: config.messageTemplate,
|
||||
shopName: config.shopName,
|
||||
})
|
||||
const url = buildRequestUrl(endpoint)
|
||||
const requestBody = buildRequestBody({
|
||||
tid: String(order.platform_order_id || ''),
|
||||
@@ -48,7 +57,7 @@ export async function ensureAgisoClaimMessageDeliveredForTask({ order, task, cla
|
||||
platform: String(order.platform || '').trim() || 'unknown',
|
||||
shopId: String(order.shop_id || '').trim(),
|
||||
shopName: String(order.shop_name || '').trim(),
|
||||
channel: AGISO_MESSAGE_CHANNEL,
|
||||
channel: AGISO_XIANYU_MESSAGE_CHANNEL,
|
||||
orderId: order.id,
|
||||
taskId: task.id,
|
||||
platformOrderId: order.platform_order_id,
|
||||
@@ -113,6 +122,18 @@ export async function ensureAgisoClaimMessageDeliveredForTask({ order, task, cla
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAgisoXianyuMessagingConfig(order) {
|
||||
const baseConfig = runtimeConfig.platforms?.agiso?.messaging || {}
|
||||
const shopId = String(order?.shop_id || '').trim()
|
||||
const shopConfigs = getAgisoShopConfigMap()
|
||||
const shopConfig = shopId && isPlainObject(shopConfigs[shopId]) ? shopConfigs[shopId] : {}
|
||||
|
||||
return {
|
||||
...baseConfig,
|
||||
...shopConfig,
|
||||
}
|
||||
}
|
||||
|
||||
function buildRequestUrl(endpoint) {
|
||||
return new URL(endpoint).toString()
|
||||
}
|
||||
@@ -151,12 +172,15 @@ function generateSign(params, appSecret) {
|
||||
return crypto.createHash('md5').update(raw, 'utf8').digest('hex').toLowerCase()
|
||||
}
|
||||
|
||||
function renderAgisoClaimMessage({ order, task, claimUrl, expiredAt, template }) {
|
||||
function renderAgisoClaimMessage({ order, task, claimUrl, expiredAt, template, shopName = '' }) {
|
||||
const source = String(template || '').trim() || '您的订单 {platformOrderId} 已创建领取链接,请在 {expiredAt} 前完成领取:{claimUrl}'
|
||||
const resolvedShopName = String(shopName || order.shop_name || order.shop_id || '').trim()
|
||||
|
||||
return source
|
||||
.replaceAll('{platformOrderId}', String(order.platform_order_id || ''))
|
||||
.replaceAll('{taskNo}', String(task.task_no || ''))
|
||||
.replaceAll('{shopName}', resolvedShopName)
|
||||
.replaceAll('{shopId}', String(order.shop_id || ''))
|
||||
.replaceAll('{claimUrl}', claimUrl)
|
||||
.replaceAll('{expiredAt}', String(expiredAt || '尽快'))
|
||||
}
|
||||
@@ -196,7 +220,7 @@ function resolveAgisoErrorMessage(payload, rawText, statusCode) {
|
||||
}
|
||||
|
||||
const text = String(rawText || '').trim()
|
||||
return text || `Agiso 发消息失败,HTTP ${statusCode}`
|
||||
return text || `Agiso 咸鱼发消息失败,HTTP ${statusCode}`
|
||||
}
|
||||
|
||||
function safeParseJson(rawText) {
|
||||
@@ -232,3 +256,7 @@ function maskBodyForStorage(body) {
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
import crypto from 'node:crypto'
|
||||
|
||||
import { runtimeConfig } from '../../../../config/runtime.js'
|
||||
import { getAgisoShopConfig } from '../shop-config-service.js'
|
||||
import { logWebhook } from '../../../../utils/logger.js'
|
||||
import { nowIso } from '../../../../utils/time.js'
|
||||
import { parseAmountToFen } from '../../../../utils/money.js'
|
||||
import { parseJsonObject } from '../../../../utils/json.js'
|
||||
|
||||
const DEFAULT_DETAIL_TIMEOUT_MS = 5000
|
||||
|
||||
export async function enrichAgisoXianyuTradeOrder(parsed, { requestId = '' } = {}) {
|
||||
if (!shouldHydrateAgisoXianyuTradeOrder(parsed)) {
|
||||
return { parsed, enriched: false, reason: 'not_needed' }
|
||||
}
|
||||
|
||||
const config = resolveAgisoXianyuTradeDetailConfig(parsed.shopId)
|
||||
if (!config.endpoint || !config.accessToken || !config.appSecret) {
|
||||
logWebhook('[agiso/xianyu/order-detail]', '跳过 Agiso 咸鱼订单详情补查:缺少必要配置', {
|
||||
requestId,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
hasEndpoint: Boolean(config.endpoint),
|
||||
hasAccessToken: Boolean(config.accessToken),
|
||||
hasAppSecret: Boolean(config.appSecret),
|
||||
}, { level: 'warn' })
|
||||
|
||||
return { parsed, enriched: false, reason: 'missing_config' }
|
||||
}
|
||||
|
||||
const requestBody = buildRequestBody({
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
appSecret: config.appSecret,
|
||||
})
|
||||
const requestHeaders = buildRequestHeaders({
|
||||
accessToken: config.accessToken,
|
||||
apiVersion: config.apiVersion,
|
||||
})
|
||||
|
||||
logWebhook('[agiso/xianyu/order-detail]', '开始补查 Agiso 咸鱼订单详情', {
|
||||
requestId,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
endpoint: config.endpoint,
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await fetchWithTimeout(config.endpoint, {
|
||||
method: 'POST',
|
||||
headers: requestHeaders,
|
||||
body: new URLSearchParams(requestBody).toString(),
|
||||
}, config.timeoutMs)
|
||||
const rawText = await response.text()
|
||||
const payload = parseJsonObject(rawText, { preserveLargeIntegers: true })
|
||||
const detailPayload = extractAgisoDetailPayload(payload)
|
||||
const detailAmountFen = resolveTotalAmountFen(detailPayload)
|
||||
|
||||
if (response.status < 200 || response.status >= 300) {
|
||||
const message = resolveAgisoDetailErrorMessage(payload, rawText, response.status)
|
||||
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查失败', {
|
||||
requestId,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
responseStatus: response.status,
|
||||
errorMessage: message,
|
||||
}, { level: 'warn' })
|
||||
|
||||
return { parsed, enriched: false, reason: 'http_error', errorMessage: message }
|
||||
}
|
||||
|
||||
if (!isAgisoDetailSuccess(payload)) {
|
||||
const message = resolveAgisoDetailErrorMessage(payload, rawText, response.status)
|
||||
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查返回业务失败', {
|
||||
requestId,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
responseStatus: response.status,
|
||||
errorMessage: message,
|
||||
response: payload,
|
||||
}, { level: 'warn' })
|
||||
|
||||
return { parsed, enriched: false, reason: 'business_error', errorMessage: message }
|
||||
}
|
||||
|
||||
if (!detailPayload || typeof detailPayload !== 'object' || Object.keys(detailPayload).length === 0) {
|
||||
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查未返回可用订单体', {
|
||||
requestId,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
responseStatus: response.status,
|
||||
}, { level: 'warn' })
|
||||
|
||||
return { parsed, enriched: false, reason: 'empty_detail_payload' }
|
||||
}
|
||||
|
||||
if (detailAmountFen <= 0) {
|
||||
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情已返回,但仍未解析出金额', {
|
||||
requestId,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
responseStatus: response.status,
|
||||
detailKeys: Object.keys(detailPayload),
|
||||
}, { level: 'warn' })
|
||||
|
||||
return {
|
||||
parsed: mergeEnrichedTrade(parsed, detailPayload, { keepOriginalAmount: true }),
|
||||
enriched: false,
|
||||
reason: 'amount_still_missing',
|
||||
}
|
||||
}
|
||||
|
||||
const merged = mergeEnrichedTrade(parsed, detailPayload)
|
||||
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查成功', {
|
||||
requestId,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
totalAmountFen: merged.totalAmount,
|
||||
paidAt: merged.paidAt,
|
||||
})
|
||||
|
||||
return {
|
||||
parsed: merged,
|
||||
enriched: true,
|
||||
totalAmountFen: merged.totalAmount,
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error || 'Agiso 咸鱼订单详情补查失败')
|
||||
logWebhook('[agiso/xianyu/order-detail]', 'Agiso 咸鱼订单详情补查异常', {
|
||||
requestId,
|
||||
platform: parsed.platform,
|
||||
shopId: parsed.shopId,
|
||||
platformOrderId: parsed.platformOrderId,
|
||||
errorMessage: message,
|
||||
}, { level: 'warn' })
|
||||
|
||||
return { parsed, enriched: false, reason: 'request_failed', errorMessage: message }
|
||||
}
|
||||
}
|
||||
|
||||
function shouldHydrateAgisoXianyuTradeOrder(parsed) {
|
||||
return parsed?.provider === 'agiso'
|
||||
&& parsed?.platform === 'xianyu'
|
||||
&& String(parsed?.platformOrderId || '').trim()
|
||||
&& Number(parsed?.totalAmount || 0) <= 0
|
||||
}
|
||||
|
||||
function resolveAgisoXianyuTradeDetailConfig(shopId) {
|
||||
const baseConfig = runtimeConfig.platforms?.agiso?.tradeDetail || {}
|
||||
const shopConfig = getAgisoShopConfig(shopId) || {}
|
||||
|
||||
return {
|
||||
endpoint: String(shopConfig.tradeDetailEndpoint || baseConfig.endpoint || '').trim(),
|
||||
apiVersion: String(shopConfig.tradeDetailApiVersion || baseConfig.apiVersion || '1').trim() || '1',
|
||||
accessToken: String(shopConfig.accessToken || runtimeConfig.platforms?.agiso?.messaging?.accessToken || '').trim(),
|
||||
appSecret: String(shopConfig.appSecret || runtimeConfig.platforms?.agiso?.appSecret || '').trim(),
|
||||
timeoutMs: normalizePositiveInteger(shopConfig.tradeDetailTimeoutMs || baseConfig.timeoutMs, DEFAULT_DETAIL_TIMEOUT_MS),
|
||||
}
|
||||
}
|
||||
|
||||
function buildRequestHeaders({ accessToken, apiVersion }) {
|
||||
return {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
ApiVersion: apiVersion,
|
||||
'content-type': 'application/x-www-form-urlencoded; charset=utf-8',
|
||||
}
|
||||
}
|
||||
|
||||
function buildRequestBody({ platformOrderId, appSecret }) {
|
||||
const timestamp = String(Math.floor(Date.now() / 1000))
|
||||
const payload = {
|
||||
tid: String(platformOrderId || '').trim(),
|
||||
timestamp,
|
||||
}
|
||||
|
||||
payload.sign = generateSign(payload, appSecret)
|
||||
return payload
|
||||
}
|
||||
|
||||
function generateSign(params, appSecret) {
|
||||
const sortedEntries = Object.entries(params).sort(([left], [right]) => left.localeCompare(right))
|
||||
let raw = appSecret
|
||||
|
||||
for (const [key, value] of sortedEntries) {
|
||||
raw += `${key}${value}`
|
||||
}
|
||||
|
||||
raw += appSecret
|
||||
|
||||
return crypto.createHash('md5').update(raw, 'utf8').digest('hex').toLowerCase()
|
||||
}
|
||||
|
||||
function extractAgisoDetailPayload(payload) {
|
||||
if (!isPlainObject(payload)) {
|
||||
return {}
|
||||
}
|
||||
|
||||
for (const key of ['Data', 'data', 'Result', 'result', 'Trade', 'trade', 'Order', 'order']) {
|
||||
if (isPlainObject(payload[key])) {
|
||||
return payload[key]
|
||||
}
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
function resolveTotalAmountFen(payload) {
|
||||
const fenAmount = normalizeFenInteger(pickFirstNonEmpty([
|
||||
payload.payment,
|
||||
payload.Payment,
|
||||
payload.post_fee,
|
||||
payload.postFee,
|
||||
payload.item?.price,
|
||||
]))
|
||||
|
||||
if (fenAmount > 0) {
|
||||
return fenAmount
|
||||
}
|
||||
|
||||
return parseAmountToFen(pickFirstNonEmpty([
|
||||
payload.total_fee,
|
||||
payload.totalFee,
|
||||
payload.TotalFee,
|
||||
payload.pay_fee,
|
||||
payload.payFee,
|
||||
payload.PayFee,
|
||||
payload.actual_fee,
|
||||
payload.actualFee,
|
||||
payload.ActualFee,
|
||||
payload.total_amount,
|
||||
payload.totalAmount,
|
||||
payload.Amount,
|
||||
payload.amount,
|
||||
]))
|
||||
}
|
||||
|
||||
function resolvePaidAt(payload, fallbackValue) {
|
||||
const providerPaidAt = normalizeProviderDateTime(
|
||||
pickFirstNonEmpty([
|
||||
payload.paid_at,
|
||||
payload.paidAt,
|
||||
payload.pay_time,
|
||||
payload.payTime,
|
||||
payload.PayTime,
|
||||
fallbackValue,
|
||||
]),
|
||||
)
|
||||
|
||||
if (providerPaidAt) {
|
||||
return providerPaidAt
|
||||
}
|
||||
|
||||
const rawStatus = normalizeInteger(
|
||||
pickFirstNonEmpty([payload.order_status, payload.orderStatus, payload.status]),
|
||||
)
|
||||
|
||||
if (rawStatus === 2 || rawStatus === 3 || rawStatus === 4) {
|
||||
return nowIso()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function mergeEnrichedTrade(parsed, detailPayload, { keepOriginalAmount = false } = {}) {
|
||||
const mergedPayload = {
|
||||
...parsed.rawPayload,
|
||||
_agisoTradeDetail: detailPayload,
|
||||
}
|
||||
const totalAmountFen = resolveTotalAmountFen(detailPayload)
|
||||
|
||||
return {
|
||||
...parsed,
|
||||
buyerId: pickFirstNonEmpty([
|
||||
parsed.buyerId,
|
||||
detailPayload.encryption_buyer_id,
|
||||
detailPayload.buyer_id,
|
||||
detailPayload.buyerId,
|
||||
detailPayload.BuyerId,
|
||||
detailPayload.BuyerOpenUid,
|
||||
detailPayload.buyer_open_uid,
|
||||
]),
|
||||
buyerName: pickFirstNonEmpty([
|
||||
parsed.buyerName,
|
||||
detailPayload.buyer_name,
|
||||
detailPayload.buyerName,
|
||||
detailPayload.BuyerName,
|
||||
detailPayload.buyer_nick,
|
||||
detailPayload.nick,
|
||||
detailPayload.BuyerNick,
|
||||
]),
|
||||
receiverContact: pickFirstNonEmpty([
|
||||
parsed.receiverContact,
|
||||
detailPayload.receiver_contact,
|
||||
detailPayload.receiverContact,
|
||||
detailPayload.receiver_mobile,
|
||||
detailPayload.receiverMobile,
|
||||
detailPayload.mobile,
|
||||
detailPayload.phone,
|
||||
]),
|
||||
shopName: pickFirstNonEmpty([
|
||||
parsed.shopName,
|
||||
detailPayload.shop_name,
|
||||
detailPayload.shopName,
|
||||
detailPayload.ShopName,
|
||||
detailPayload.seller_name,
|
||||
detailPayload.sellerName,
|
||||
detailPayload.seller_nick,
|
||||
detailPayload.sellerNick,
|
||||
detailPayload.SellerNick,
|
||||
]),
|
||||
totalAmount: keepOriginalAmount ? parsed.totalAmount : (totalAmountFen || parsed.totalAmount),
|
||||
paidAt: parsed.paidAt || resolvePaidAt(detailPayload, parsed.paidAt),
|
||||
rawPayload: mergedPayload,
|
||||
items: normalizeOrderItems(mergedPayload, parsed.items),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeOrderItems(payload, fallbackItems = []) {
|
||||
const candidates = [
|
||||
payload.item ? [payload.item] : null,
|
||||
payload.items,
|
||||
payload.Items,
|
||||
payload.orders,
|
||||
payload.Orders,
|
||||
payload.order_list,
|
||||
payload.OrderList,
|
||||
]
|
||||
const items = candidates.find((item) => Array.isArray(item) && item.length > 0)
|
||||
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
return Array.isArray(fallbackItems) ? fallbackItems : []
|
||||
}
|
||||
|
||||
return items.map((item) => {
|
||||
const source = isPlainObject(item) ? item : {}
|
||||
const skuCode = pickFirstNonEmpty([
|
||||
source.OuterSkuId,
|
||||
source.outerSkuId,
|
||||
source.outer_sku_id,
|
||||
source.OuterIid,
|
||||
source.outerIid,
|
||||
source.outer_iid,
|
||||
source.sku_code,
|
||||
source.skuCode,
|
||||
source.goods_sku,
|
||||
source.item_id,
|
||||
source.itemId,
|
||||
source.goods_id,
|
||||
source.goodsId,
|
||||
source.NumIid,
|
||||
source.num_iid,
|
||||
payload.item_id,
|
||||
payload.itemId,
|
||||
])
|
||||
|
||||
return {
|
||||
skuCode,
|
||||
skuName: pickFirstNonEmpty([
|
||||
source.Title,
|
||||
source.title,
|
||||
source.sku_name,
|
||||
source.skuName,
|
||||
source.goods_name,
|
||||
source.goodsName,
|
||||
skuCode,
|
||||
]),
|
||||
quantity: Math.max(
|
||||
1,
|
||||
normalizeInteger(
|
||||
pickFirstNonEmpty([
|
||||
source.quantity,
|
||||
source.num,
|
||||
source.Num,
|
||||
source.buy_amount,
|
||||
payload.quantity,
|
||||
payload.num,
|
||||
payload.Num,
|
||||
]),
|
||||
) || 1,
|
||||
),
|
||||
spec: source,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function isAgisoDetailSuccess(payload) {
|
||||
if (!payload || typeof payload !== 'object' || Object.keys(payload).length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (payload.IsSuccess === true) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (Number(payload.Error_Code) === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (Number(payload.code) === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (Number(payload.success) === 1 || payload.success === true) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function resolveAgisoDetailErrorMessage(payload, rawText, statusCode) {
|
||||
if (payload && typeof payload === 'object') {
|
||||
for (const value of [payload.Error_Msg, payload.msg, payload.message, payload.error]) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (normalized) {
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const text = String(rawText || '').trim()
|
||||
return text || `Agiso 咸鱼订单详情接口失败,HTTP ${statusCode}`
|
||||
}
|
||||
|
||||
async function fetchWithTimeout(url, options, timeoutMs) {
|
||||
const controller = new AbortController()
|
||||
const timer = setTimeout(() => controller.abort(new Error('Agiso 咸鱼订单详情请求超时')), timeoutMs)
|
||||
|
||||
try {
|
||||
return await fetch(url, {
|
||||
...options,
|
||||
signal: controller.signal,
|
||||
})
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeProviderDateTime(value) {
|
||||
const normalized = String(value || '').trim()
|
||||
|
||||
if (!normalized) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (/^\d{10,13}$/.test(normalized)) {
|
||||
const timestamp = normalized.length === 13 ? Number(normalized) : Number(normalized) * 1000
|
||||
|
||||
if (Number.isFinite(timestamp)) {
|
||||
return new Date(timestamp).toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
const isoLike = normalized.replace(' ', 'T')
|
||||
const parsed = Date.parse(isoLike)
|
||||
|
||||
if (Number.isNaN(parsed)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return new Date(parsed).toISOString()
|
||||
}
|
||||
|
||||
function normalizeInteger(value) {
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) ? Math.round(parsed) : 0
|
||||
}
|
||||
|
||||
function normalizePositiveInteger(value, fallbackValue) {
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : fallbackValue
|
||||
}
|
||||
|
||||
function normalizeFenInteger(value) {
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : 0
|
||||
}
|
||||
|
||||
function pickFirstNonEmpty(values) {
|
||||
for (const value of values) {
|
||||
const normalized = String(value || '').trim()
|
||||
if (normalized) {
|
||||
return normalized
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
|
||||
import { runtimeConfig } from '../config/runtime.js'
|
||||
import { runtimeConfig } from '../../config/runtime.js'
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 60_000
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { runtimeConfig } from '../config/runtime.js'
|
||||
import { logDebug } from '../utils/logger.js'
|
||||
import { runtimeConfig } from '../../config/runtime.js'
|
||||
import { logDebug } from '../../utils/logger.js'
|
||||
|
||||
const SESSION_DEBUG_ENABLED = Boolean(runtimeConfig.session.debug)
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
import { runtimeConfig } from '../config/runtime.js'
|
||||
import { runtimeConfig } from '../../config/runtime.js'
|
||||
|
||||
const BAIDU_BEIJING_TIME_URL = 'https://www.baidu.com/s?wd=%E5%8C%97%E4%BA%AC%E6%97%B6%E9%97%B4'
|
||||
const BEIJING_TIME_PROOF_VIEWPORT = { width: 1440, height: 860 }
|
||||
@@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { chromium } from 'playwright'
|
||||
|
||||
import { runtimeConfig } from '../config/runtime.js'
|
||||
import { runtimeConfig } from '../../config/runtime.js'
|
||||
import {
|
||||
isRetryableQrCaptureError,
|
||||
logBrowserSessionDebug,
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
import { recognizeTencentCaptcha } from './ocr.js'
|
||||
|
||||
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
|
||||
const PROJECT_ROOT = path.resolve(CURRENT_DIR, '../..')
|
||||
const PROJECT_ROOT = path.resolve(CURRENT_DIR, '../../..')
|
||||
const DATA_ROOT = path.resolve(PROJECT_ROOT, 'data/browser-sessions')
|
||||
const ACTIVITY_URL = 'https://df.qq.com/cp/a20240812cdk/index.html'
|
||||
const CHROME_PATH = String(runtimeConfig.browser.chromePath || '').trim()
|
||||
@@ -0,0 +1,60 @@
|
||||
import { createHttpError } from './http.js'
|
||||
|
||||
const LARGE_INTEGER_PATTERN = /(:\s*)(-?\d{16,})(\s*[,}\]])/g
|
||||
|
||||
export function parseJsonObject(rawText, { preserveLargeIntegers = false, throwOnError = false } = {}) {
|
||||
const normalized = String(rawText || '').trim()
|
||||
|
||||
if (!normalized) {
|
||||
return {}
|
||||
}
|
||||
|
||||
try {
|
||||
const prepared = preserveLargeIntegers ? wrapLargeIntegerLiterals(normalized) : normalized
|
||||
const parsed = JSON.parse(prepared)
|
||||
return isPlainObject(parsed) ? parsed : {}
|
||||
} catch (error) {
|
||||
if (!throwOnError) {
|
||||
return {}
|
||||
}
|
||||
|
||||
throw createHttpError('json 参数不是合法 JSON', {
|
||||
statusCode: 400,
|
||||
errorCode: 'invalid_json_payload',
|
||||
cause: error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function wrapLargeIntegerLiterals(rawText) {
|
||||
return String(rawText || '').replace(LARGE_INTEGER_PATTERN, (_match, prefix, digits, suffix) => {
|
||||
if (!shouldPreserveIntegerToken(digits)) {
|
||||
return `${prefix}${digits}${suffix}`
|
||||
}
|
||||
|
||||
return `${prefix}"${digits}"${suffix}`
|
||||
})
|
||||
}
|
||||
|
||||
function shouldPreserveIntegerToken(rawDigits) {
|
||||
const digits = String(rawDigits || '').trim()
|
||||
const unsigned = digits.startsWith('-') ? digits.slice(1) : digits
|
||||
|
||||
if (!/^\d+$/.test(unsigned)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (unsigned.length > 16) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (unsigned.length < 16) {
|
||||
return false
|
||||
}
|
||||
|
||||
return unsigned > '9007199254740991'
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
@@ -68,6 +68,10 @@ const router = createRouter({
|
||||
path: 'webhook-events',
|
||||
component: () => import('@/views/admin/AdminWebhookEventsView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'platform-shops',
|
||||
component: () => import('@/views/admin/AdminPlatformShopsView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'webhook-events/:eventId',
|
||||
component: () => import('@/views/admin/AdminWebhookEventDetailView.vue'),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { apiGet, apiGetBlob, apiPost } from '@/lib/http'
|
||||
import type {
|
||||
AdminAgisoObservedShopItem,
|
||||
AdminAgisoShopConfigItem,
|
||||
AdminAuditLogItem,
|
||||
AdminTaskActionResponse,
|
||||
AdminCdkListItem,
|
||||
@@ -57,6 +59,21 @@ export function fetchAdminAuditLogs(params?: Record<string, unknown>) {
|
||||
return apiGet<{ items: AdminAuditLogItem[]; pagination: AdminPagination }>('/api/v1/admin/audit-logs', params)
|
||||
}
|
||||
|
||||
export function fetchAdminAgisoShopConfigs() {
|
||||
return apiGet<{
|
||||
filePath: string
|
||||
shops: AdminAgisoShopConfigItem[]
|
||||
observedShops: AdminAgisoObservedShopItem[]
|
||||
}>('/api/v1/admin/platform-config/agiso-shops')
|
||||
}
|
||||
|
||||
export function saveAdminAgisoShopConfigs(payload: { shops: Array<Record<string, unknown>> }) {
|
||||
return apiPost<{
|
||||
filePath: string
|
||||
shops: AdminAgisoShopConfigItem[]
|
||||
}>('/api/v1/admin/platform-config/agiso-shops', payload)
|
||||
}
|
||||
|
||||
export function fetchAdminOrders(params?: Record<string, unknown>) {
|
||||
return apiGet<{ items: AdminOrderListItem[]; pagination: AdminPagination }>('/api/v1/admin/orders', params)
|
||||
}
|
||||
|
||||
@@ -48,6 +48,27 @@ export interface AdminAuditLogItem {
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface AdminAgisoShopConfigItem {
|
||||
shopId: string
|
||||
shopName: string
|
||||
accessToken: string
|
||||
accessTokenMasked: string
|
||||
enabled: boolean | null
|
||||
messageTemplate: string
|
||||
appSecretConfigured: boolean
|
||||
apiVersion: string
|
||||
sendMessageEndpoint: string
|
||||
}
|
||||
|
||||
export interface AdminAgisoObservedShopItem {
|
||||
shopId: string
|
||||
detectedShopName: string
|
||||
displayShopName: string
|
||||
latestSeenAt: string | null
|
||||
webhookEventCount: number
|
||||
configured: boolean
|
||||
}
|
||||
|
||||
export interface AdminDashboardSummary {
|
||||
todayOrders: number
|
||||
paidPendingClaim: number
|
||||
|
||||
@@ -28,6 +28,7 @@ export function formatAuditAction(action: string) {
|
||||
inventory_cdk_released: '释放库存 CDK',
|
||||
inventory_cdk_invalidated: '作废库存 CDK',
|
||||
webhook_replayed: '重放 Webhook',
|
||||
platform_shop_config_updated: '更新店铺配置',
|
||||
}
|
||||
|
||||
return formatStatusWithRaw(action, labelMap)
|
||||
@@ -39,6 +40,7 @@ export function formatAuditTargetType(targetType: string) {
|
||||
task: '交付任务',
|
||||
cdk: 'CDK 库存',
|
||||
webhook_event: 'Webhook 事件',
|
||||
platform_config: '平台配置',
|
||||
}
|
||||
|
||||
return formatStatusWithRaw(targetType, labelMap)
|
||||
|
||||
@@ -20,6 +20,7 @@ const navItems = computed(() => {
|
||||
|
||||
if (isAdmin.value) {
|
||||
baseItems.splice(1, 0, { to: '/admin/users', label: '用户' })
|
||||
baseItems.push({ to: '/admin/platform-shops', label: '店铺配置' })
|
||||
baseItems.push({ to: '/admin/audit-logs', label: '审计' })
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
import { fetchAdminAgisoShopConfigs, saveAdminAgisoShopConfigs } from '@/services/admin'
|
||||
import type { AdminAgisoObservedShopItem, AdminAgisoShopConfigItem } from '@/types/admin'
|
||||
import { hasAdminRole } from '@/utils/admin-auth'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
type EditableShop = {
|
||||
id: string
|
||||
shopId: string
|
||||
shopName: string
|
||||
accessToken: string
|
||||
enabled: boolean
|
||||
messageTemplate: string
|
||||
}
|
||||
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const filePath = ref('')
|
||||
const shops = ref<EditableShop[]>([])
|
||||
const observedShops = ref<AdminAgisoObservedShopItem[]>([])
|
||||
|
||||
function createEmptyShop(): EditableShop {
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
shopId: '',
|
||||
shopName: '',
|
||||
accessToken: '',
|
||||
enabled: true,
|
||||
messageTemplate: '',
|
||||
}
|
||||
}
|
||||
|
||||
function mapEditableShop(item: AdminAgisoShopConfigItem): EditableShop {
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
shopId: item.shopId,
|
||||
shopName: item.shopName,
|
||||
accessToken: item.accessToken,
|
||||
enabled: item.enabled !== false,
|
||||
messageTemplate: item.messageTemplate,
|
||||
}
|
||||
}
|
||||
|
||||
async function loadConfigs() {
|
||||
if (!hasAdminRole('admin')) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await fetchAdminAgisoShopConfigs()
|
||||
filePath.value = response.data.filePath
|
||||
shops.value = response.data.shops.map(mapEditableShop)
|
||||
observedShops.value = response.data.observedShops
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '读取店铺配置失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function addShop() {
|
||||
shops.value.unshift(createEmptyShop())
|
||||
}
|
||||
|
||||
function removeShop(id: string) {
|
||||
shops.value = shops.value.filter((item) => item.id !== id)
|
||||
}
|
||||
|
||||
function importObservedShop(item: AdminAgisoObservedShopItem) {
|
||||
shops.value.unshift({
|
||||
id: crypto.randomUUID(),
|
||||
shopId: item.shopId,
|
||||
shopName: item.detectedShopName || item.displayShopName || '',
|
||||
accessToken: '',
|
||||
enabled: true,
|
||||
messageTemplate: '',
|
||||
})
|
||||
}
|
||||
|
||||
async function saveConfigs() {
|
||||
const payload = shops.value
|
||||
.map((item) => ({
|
||||
shopId: item.shopId.trim(),
|
||||
shopName: item.shopName.trim(),
|
||||
accessToken: item.accessToken.trim(),
|
||||
enabled: item.enabled,
|
||||
messageTemplate: item.messageTemplate.trim(),
|
||||
}))
|
||||
.filter((item) => item.shopId && item.accessToken)
|
||||
|
||||
saving.value = true
|
||||
|
||||
try {
|
||||
const response = await saveAdminAgisoShopConfigs({ shops: payload })
|
||||
filePath.value = response.data.filePath
|
||||
shops.value = response.data.shops.map(mapEditableShop)
|
||||
ElMessage.success('店铺配置已保存')
|
||||
await loadConfigs()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '保存店铺配置失败')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadConfigs)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="admin-panel">
|
||||
<header class="panel-header">
|
||||
<div>
|
||||
<h1>店铺配置</h1>
|
||||
<p>Agiso 多店铺配置改为文件维护。保存后立即生效,不需要再手改 `.env`。</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button round @click="loadConfigs">刷新</el-button>
|
||||
<el-button round type="primary" @click="addShop">新增店铺</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div v-if="!hasAdminRole('admin')" class="empty-block">仅管理员可以维护店铺配置。</div>
|
||||
|
||||
<template v-else>
|
||||
<section class="meta-card">
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">配置文件</span>
|
||||
<code>{{ filePath || '-' }}</code>
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">说明</span>
|
||||
<span>店铺级 `accessToken` 优先于全局 `AGISO_ACCESS_TOKEN`。未配置的店铺仍会继续使用全局兜底。</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p v-if="errorMessage" class="error-copy">{{ errorMessage }}</p>
|
||||
<div v-if="loading" class="empty-block">店铺配置加载中</div>
|
||||
|
||||
<template v-else>
|
||||
<section class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>已配置店铺</h3>
|
||||
<p>至少填写 `shopId` 和 `accessToken`。`shopName` 为空时,界面会退回显示店铺 ID。</p>
|
||||
</div>
|
||||
<el-button :loading="saving" round type="primary" @click="saveConfigs">保存配置</el-button>
|
||||
</div>
|
||||
|
||||
<div v-if="shops.length === 0" class="empty-inline">当前还没有店铺配置,先新增一条。</div>
|
||||
|
||||
<div v-for="shop in shops" :key="shop.id" class="shop-card">
|
||||
<div class="shop-grid">
|
||||
<label class="field-block">
|
||||
<span>店铺 ID</span>
|
||||
<input v-model="shop.shopId" class="text-input" placeholder="例如 693760716" />
|
||||
</label>
|
||||
|
||||
<label class="field-block">
|
||||
<span>店铺名称</span>
|
||||
<input v-model="shop.shopName" class="text-input" placeholder="例如 大锤商行" />
|
||||
</label>
|
||||
|
||||
<label class="field-block field-wide">
|
||||
<span>AccessToken</span>
|
||||
<input v-model="shop.accessToken" class="text-input" placeholder="AldsIdle..." />
|
||||
</label>
|
||||
|
||||
<label class="field-block field-wide">
|
||||
<span>消息模板</span>
|
||||
<textarea
|
||||
v-model="shop.messageTemplate"
|
||||
class="text-area"
|
||||
placeholder="留空则沿用全局 AGISO_MESSAGE_TEMPLATE"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="shop-actions">
|
||||
<label class="checkbox-line">
|
||||
<input v-model="shop.enabled" type="checkbox" />
|
||||
<span>启用店铺级覆盖</span>
|
||||
</label>
|
||||
<el-button link type="danger" @click="removeShop(shop.id)">删除</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>近期识别到的店铺</h3>
|
||||
<p>从 webhook 自动识别出的店铺,可一键导入配置列表。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>店铺</th>
|
||||
<th>Webhook 次数</th>
|
||||
<th>最近出现时间</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in observedShops" :key="item.shopId">
|
||||
<td>
|
||||
<div class="cell-stack">
|
||||
<strong>{{ item.displayShopName || item.shopId }}</strong>
|
||||
<span class="cell-subtle">ID: {{ item.shopId }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ item.webhookEventCount }}</td>
|
||||
<td>{{ formatAdminDateTime(item.latestSeenAt) }}</td>
|
||||
<td>{{ item.configured ? '已配置' : '未配置' }}</td>
|
||||
<td>
|
||||
<el-button
|
||||
v-if="!item.configured"
|
||||
link
|
||||
type="primary"
|
||||
@click="importObservedShop(item)"
|
||||
>
|
||||
导入到配置
|
||||
</el-button>
|
||||
<span v-else class="cell-subtle">已存在</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="observedShops.length === 0">
|
||||
<td colspan="5" class="empty-inline">最近还没有识别到可用店铺。</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</template>
|
||||
</template>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.admin-panel {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.panel-header,
|
||||
.section-title-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.panel-header h1,
|
||||
.section-title-row h3 {
|
||||
margin: 0;
|
||||
color: #1d3555;
|
||||
}
|
||||
|
||||
.panel-header p,
|
||||
.section-title-row p {
|
||||
margin: 8px 0 0;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.meta-card,
|
||||
.table-card,
|
||||
.empty-block,
|
||||
.error-copy {
|
||||
padding: 18px;
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border: 1px solid rgba(86, 108, 138, 0.1);
|
||||
}
|
||||
|
||||
.meta-card {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.meta-line {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.shop-card {
|
||||
margin-top: 14px;
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
}
|
||||
|
||||
.shop-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.field-block {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.field-block span {
|
||||
color: #475467;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.field-wide {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.text-input,
|
||||
.text-area {
|
||||
width: 100%;
|
||||
min-height: 42px;
|
||||
padding: 0 14px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(86, 108, 138, 0.18);
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
|
||||
.text-area {
|
||||
min-height: 90px;
|
||||
padding: 12px 14px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.shop-actions {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.checkbox-line {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
color: #475467;
|
||||
}
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
padding: 12px 10px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid rgba(86, 108, 138, 0.08);
|
||||
color: #334155;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.cell-stack {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.cell-subtle {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.empty-block,
|
||||
.empty-inline {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.error-copy {
|
||||
color: #b42318;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.panel-header,
|
||||
.section-title-row,
|
||||
.shop-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.shop-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,4 +1,4 @@
|
||||
{$CADDY_SITE_ADDR:http://localhost} {
|
||||
{$CADDY_SITE_ADDR:http://localhost}, http://localhost, http://127.0.0.1 {
|
||||
route {
|
||||
handle /api/* {
|
||||
reverse_proxy backend:3000
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
order_cdk
|
||||
|
||||
15173678868
|
||||
778899
|
||||
|
||||
AppId: 2026040753219154857
|
||||
AppSecret: tccxk5c7ppy7xpr43rvastceyydskfha
|
||||
|
||||
@@ -13,6 +16,8 @@ TbAlds54ez66ateztecyprxtn6kb98w23ghy6r9c5yxt2zueknd
|
||||
AldsIdlefmykv7w6xdvgf9r2ymsxrmznux74tyaehc9pwygdnrgr4
|
||||
|
||||
|
||||
咸鱼店铺 羊小胖。
|
||||
AldsIdle4925bh8c9n3ka79fxauc9egk7uea6f6frfbbb5btvxpvu
|
||||
|
||||
|
||||
拼接用户授权需访问url ,示例及参数说明如下:
|
||||
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
订单查询
|
||||
|
||||
简要描述: 接口调用示例
|
||||
|
||||
¥高级
|
||||
根据订单号,查询订单信息
|
||||
|
||||
请求URL:
|
||||
|
||||
https://gw-api.agiso.com/aldsIdle/Order/Detail
|
||||
|
||||
请求方式:
|
||||
|
||||
POST
|
||||
|
||||
公共Header:
|
||||
Authorization
|
||||
|
||||
是
|
||||
|
||||
string
|
||||
|
||||
httpPost.addHeader("Authorization","Bearer "+ accessToken)
|
||||
ApiVersion
|
||||
|
||||
是
|
||||
|
||||
string
|
||||
|
||||
httpPost.addHeader("ApiVersion", "1")
|
||||
|
||||
公共参数:
|
||||
timestamp
|
||||
|
||||
是
|
||||
|
||||
Date
|
||||
|
||||
时间戳,例如:1468476350。API服务端允许客户端请求最大时间误差为10分钟。
|
||||
sign
|
||||
|
||||
是
|
||||
|
||||
string
|
||||
|
||||
API输入参数签名结果,签名算法参照下面的介绍。
|
||||
|
||||
参数:
|
||||
tid
|
||||
|
||||
是
|
||||
|
||||
Long
|
||||
|
||||
订单号
|
||||
|
||||
返回示例
|
||||
|
||||
{
|
||||
"IsSuccess": true,
|
||||
"Data": {
|
||||
"biz_order_id": 1520087523676842500, // 订单号
|
||||
"buy_amount": 1, // 商品购买数量
|
||||
"buyer_nick": "地铁吃火锅当真", // 买家昵称(不唯一且买家可以自己更改)
|
||||
"encryption_buyer_id": "RAzN8214YLDBs4AJxFJBYp5DdeHrBPKq4QsbFL", // 加密的买家id(唯一且不会改变)
|
||||
"create_time": 1647677835000, // 订单创建时间,时间戳,毫秒
|
||||
"end_time": 1647677858000, // 订单完结时间,时间戳,毫秒
|
||||
"item": { // 商品信息
|
||||
"item_id": 669513624774, // 商品ID
|
||||
"pic_url": "https://gw.alicdn.com/bao/uploaded/i4/O1CN01X6Nw841OaGN6dvZtx_!!0-fleamarket.jpg", // 商品图片,绝对途径
|
||||
"price": 1, // 商品价格,单位分
|
||||
"title": "测试宝贝5", // 商品标题
|
||||
"outer_id_sku": "XY02", // 商品外部编码(SKU维度)
|
||||
"outer_id_spu": "XY01", // 商品外部编码(SPU维度)
|
||||
},
|
||||
"order_status": 5, // 0:未知状态、1:订单已创建、2:订单已付款、3:已发货、4:交易成功、5:已退款、6:交易关闭
|
||||
"pay_time": 1647677841000, // 订单下单付款时间,时间戳,毫秒
|
||||
"payment": 1, // 实付金额, 单位分
|
||||
"post_fee": 0, // 邮费
|
||||
"seller_nick": "爱***网", // 卖家昵称(不唯一且买家可以自己更改)
|
||||
"ship_time": 0, // 订单发货时间,时间戳,毫秒
|
||||
"sku": 0, // sku信息(格式: skuId|属性名:属性值;属性名:属性值)
|
||||
"isRecharge":true, // 是否直冲标识
|
||||
"recharge_amount":"135xxxx", // 充值信息
|
||||
"isEticket":"true", // 是否囤囤券订单
|
||||
"coupon_eticket_info":[ // 核销码信息(囤囤券)
|
||||
{
|
||||
"code_id":"x222223", // 核销码
|
||||
"code_password":"123", // 核销码密码
|
||||
"code_status":"1", // 核销码状态
|
||||
"code_type":1, // 核销码类型
|
||||
"code_use_url":"http://localhost:26450/t/zbdjyu_9137A" // 核销码使用链接
|
||||
}
|
||||
]
|
||||
},
|
||||
"Error_Code": 0,
|
||||
"Error_Msg": ""
|
||||
"AllowRetry": null,
|
||||
"RequestId": "20220322142251106"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user