From bb47d168a93ae1856fea7a7e5c4e5e1742a9ff6f Mon Sep 17 00:00:00 2001 From: yml Date: Tue, 14 Apr 2026 16:15:17 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=BA=97=E9=93=BA=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E7=9B=B8=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development.example | 5 - .env.production.example | 5 - apps/backend/README.md | 7 +- apps/backend/config/default.cjs | 3 - apps/backend/data/agiso-shops.json | 32 +- apps/backend/src/config/runtime.js | 71 ---- .../admin/admin-platform-config-service.js | 80 +++-- .../platforms/agiso/shop-config-service.js | 67 +++- .../agiso/xianyu/auto-delivery-service.js | 2 +- .../platforms/agiso/xianyu/message-service.js | 4 +- .../agiso/xianyu/order-detail-service.js | 2 +- apps/backend/src/types/admin-write-inputs.js | 8 + apps/backend/src/types/admin-write-models.js | 10 +- apps/backend/src/types/runtime-config.js | 2 - apps/frontend/src/services/admin.ts | 8 +- apps/frontend/src/types/admin.ts | 5 + .../views/admin/AdminPlatformShopsView.vue | 304 +++++++++++++++--- docker-compose.dev.yml | 3 - docker-compose.yml | 3 - docs/商品识别与履约SKU配置.md | 2 +- 20 files changed, 425 insertions(+), 198 deletions(-) diff --git a/.env.development.example b/.env.development.example index 104ad215..dde86db2 100644 --- a/.env.development.example +++ b/.env.development.example @@ -23,11 +23,6 @@ TENCENT_SESSION_DEBUG=false ADMIN_SESSION_SECRET=dev-local-session-secret ADMIN_DEFAULT_USERS_JSON=[{"username":"admin","password":"dev-admin-123456","role":"admin"},{"username":"operator","password":"dev-operator-123456","role":"operator"}] -ORDER_FULFILLMENT_BINDINGS_JSON=[] - AGISO_APP_SECRET= AGISO_MESSAGING_ENABLED=false -AGISO_APP_ID= -AGISO_ACCESS_TOKEN= AGISO_MESSAGE_APP_SECRET= -AGISO_MESSAGE_TEMPLATE=您的订单 {platformOrderId} 已创建领取链接,请尽快打开并完成领取:{claimUrl} diff --git a/.env.production.example b/.env.production.example index 0f359cae..0d60c5fd 100644 --- a/.env.production.example +++ b/.env.production.example @@ -20,11 +20,6 @@ TENCENT_SESSION_DEBUG=false ADMIN_SESSION_SECRET=replace-with-a-long-random-secret ADMIN_DEFAULT_USERS_JSON=[{"username":"admin","password":"replace-with-strong-admin-password","role":"admin"},{"username":"operator","password":"replace-with-strong-operator-password","role":"operator"}] -ORDER_FULFILLMENT_BINDINGS_JSON=[] - AGISO_APP_SECRET= AGISO_MESSAGING_ENABLED=false -AGISO_APP_ID= -AGISO_ACCESS_TOKEN= AGISO_MESSAGE_APP_SECRET= -AGISO_MESSAGE_TEMPLATE=您的订单 {platformOrderId} 已创建领取链接,请尽快打开并完成领取:{claimUrl} diff --git a/apps/backend/README.md b/apps/backend/README.md index 0d14b996..8d690eda 100644 --- a/apps/backend/README.md +++ b/apps/backend/README.md @@ -76,16 +76,13 @@ npm run typecheck - 全局兜底配置仍走 `.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`。 +消息发送时会按 webhook 识别出的 `shop_id` 读取 `agiso-shops.json` 中的店铺配置;模板未命中时,会先回退到文件里的 `defaults`,再回退到系统内置默认值。店铺的 `accessToken` 也需要在文件中维护,不再使用全局 `.env` 兜底。 ## 接口 diff --git a/apps/backend/config/default.cjs b/apps/backend/config/default.cjs index 4fde9a9d..280dc3bd 100644 --- a/apps/backend/config/default.cjs +++ b/apps/backend/config/default.cjs @@ -67,9 +67,6 @@ module.exports = { enabled: false, sendMessageEndpoint: 'https://gw-api.agiso.com/aldsIdle/ImMsg/SendMsg', apiVersion: '1', - authMode: 'bearer', - appId: '', - accessToken: '', appSecret: '', messageTemplate: '您的订单 {platformOrderId} 已创建领取链接,请在 {expiredAt} 前完成领取:{claimUrl}', autoDeliveryMessageTemplate: '您的订单 {platformOrderId} 已完成自动发货,请注意查收。', diff --git a/apps/backend/data/agiso-shops.json b/apps/backend/data/agiso-shops.json index 810ba8fe..9162ebe4 100644 --- a/apps/backend/data/agiso-shops.json +++ b/apps/backend/data/agiso-shops.json @@ -1,17 +1,23 @@ { - "693760716": { - "enabled": true, - "shopName": "大锤商行", - "accessToken": "AldsIdlefmykv7w6xdvgf9r2ymsxrmznux74tyaehc9pwygdnrgr4" + "defaults": { + "messageTemplate": "您的订单 {platformOrderId} 已创建领取链接,请尽快打开并完成领取:{claimUrl}", + "autoDeliveryMessageTemplate": "亲亲,您购买的兑换码已自行兑换成功,请在游戏中邮件领取,感谢您的支持" }, - "2037495620": { - "enabled": true, - "shopName": "羊小胖。", - "accessToken": "AldsIdle4925bh8c9n3ka79fxauc9egk7uea6f6frfbbb5btvxpvu" - }, - "2209880145223": { - "enabled": true, - "shopName": "大锤号商", - "accessToken": "AldsIdleu5rzb68g8gh32sk9e8rbunes6gwyg59aph2nn5em6fp4r" + "shops": { + "693760716": { + "enabled": true, + "shopName": "大锤商行", + "accessToken": "AldsIdlefmykv7w6xdvgf9r2ymsxrmznux74tyaehc9pwygdnrgr4" + }, + "2037495620": { + "enabled": true, + "shopName": "羊小胖。", + "accessToken": "AldsIdle4925bh8c9n3ka79fxauc9egk7uea6f6frfbbb5btvxpvu" + }, + "2209880145223": { + "enabled": true, + "shopName": "大锤号商", + "accessToken": "AldsIdleu5rzb68g8gh32sk9e8rbunes6gwyg59aph2nn5em6fp4r" + } } } diff --git a/apps/backend/src/config/runtime.js b/apps/backend/src/config/runtime.js index 3598ab3d..8720338f 100644 --- a/apps/backend/src/config/runtime.js +++ b/apps/backend/src/config/runtime.js @@ -6,7 +6,6 @@ import process from 'node:process' import { createRequire } from 'node:module' import { fileURLToPath } from 'node:url' -/** @typedef {import('../types/runtime-config.js').AgisoMessagingShopsConfig} AgisoMessagingShopsConfig */ /** @typedef {import('../types/runtime-config.js').RuntimeConfig} RuntimeConfig */ const require = createRequire(import.meta.url) @@ -235,16 +234,6 @@ function applyEnvOverrides(baseConfig) { } } - const agisoAppId = String(process.env.AGISO_APP_ID || '').trim() - if (agisoAppId) { - nextConfig.platforms.agiso.messaging.appId = agisoAppId - } - - const agisoAccessToken = String(process.env.AGISO_ACCESS_TOKEN || '').trim() - if (agisoAccessToken) { - nextConfig.platforms.agiso.messaging.accessToken = agisoAccessToken - } - const agisoMessageAppSecret = String(process.env.AGISO_MESSAGE_APP_SECRET || '').trim() if (agisoMessageAppSecret) { nextConfig.platforms.agiso.messaging.appSecret = agisoMessageAppSecret @@ -265,21 +254,6 @@ function applyEnvOverrides(baseConfig) { nextConfig.platforms.agiso.messaging.enabled = agisoMessagingEnabled } - const agisoMessageTemplate = String(process.env.AGISO_MESSAGE_TEMPLATE || '').trim() - if (agisoMessageTemplate) { - nextConfig.platforms.agiso.messaging.messageTemplate = agisoMessageTemplate - } - - const agisoAutoDeliveryMessageTemplate = String(process.env.AGISO_AUTO_DELIVERY_MESSAGE_TEMPLATE || '').trim() - if (agisoAutoDeliveryMessageTemplate) { - nextConfig.platforms.agiso.messaging.autoDeliveryMessageTemplate = agisoAutoDeliveryMessageTemplate - } - - const agisoShops = parseJsonObject(process.env.AGISO_SHOPS_JSON) - if (agisoShops) { - nextConfig.platforms.agiso.messaging.shops = normalizeAgisoMessagingShops(agisoShops) - } - const proofMode = String(process.env.TENCENT_REDEEM_PROOF_MODE || '').trim() if (proofMode) { nextConfig.redeem.proofMode = proofMode @@ -360,21 +334,6 @@ function parseInteger(rawValue) { return Number.isFinite(parsed) ? parsed : null } -function parseJsonObject(rawValue) { - const normalized = String(rawValue || '').trim() - - if (!normalized) { - return null - } - - try { - const parsed = JSON.parse(normalized) - return isPlainObject(parsed) ? parsed : null - } catch { - return null - } -} - function parseJsonArray(rawValue) { const normalized = String(rawValue || '').trim() @@ -390,36 +349,6 @@ function parseJsonArray(rawValue) { } } -function normalizeAgisoMessagingShops(rawValue) { - /** @type {AgisoMessagingShopsConfig} */ - const output = {} - - for (const [shopId, config] of Object.entries(rawValue || {})) { - const normalizedShopId = String(shopId || '').trim() - if (!normalizedShopId || !isPlainObject(config)) { - continue - } - - /** @type {AgisoMessagingShopsConfig[string]} */ - const next = {} - const enabled = normalizeBooleanLike(config.enabled) - if (enabled !== null) { - next.enabled = enabled - } - - for (const key of ['shopName', 'accessToken', 'messageTemplate', 'autoDeliveryMessageTemplate', '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 diff --git a/apps/backend/src/services/admin/admin-platform-config-service.js b/apps/backend/src/services/admin/admin-platform-config-service.js index 0597fd02..e3320154 100644 --- a/apps/backend/src/services/admin/admin-platform-config-service.js +++ b/apps/backend/src/services/admin/admin-platform-config-service.js @@ -2,10 +2,11 @@ import { query } from '../../db/client.js' import { + getAgisoMessagingDefaults, getAgisoShopConfig, getAgisoShopConfigMap, getAgisoShopsFilePath, - saveAgisoShopConfigMap, + saveAgisoMessagingConfig, } from '../platforms/agiso/shop-config-service.js' import { enrichAgisoXianyuTradeOrder } from '../platforms/agiso/xianyu/order-detail-service.js' import { @@ -26,6 +27,7 @@ import { resolveDisplayShopName } from './admin-read-shared-helpers.js' export async function getAdminAgisoShopConfigs() { const configMap = getAgisoShopConfigMap() + const defaults = getAgisoMessagingDefaults() const rowsResult = await query( ` SELECT @@ -43,20 +45,10 @@ export async function getAdminAgisoShopConfigs() { return { filePath: getAgisoShopsFilePath(), + defaults: mapAdminAgisoMessagingDefaults(defaults), 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(), - autoDeliveryMessageTemplate: String(config.autoDeliveryMessageTemplate || '').trim(), - appSecretConfigured: Boolean(String(config.appSecret || '').trim()), - apiVersion: String(config.apiVersion || '').trim(), - sendMessageEndpoint: String(config.sendMessageEndpoint || '').trim(), - })), + .map(([shopId, config]) => mapAdminAgisoShopConfigItem(shopId, config)), observedShops: rows.map((row) => ({ shopId: String(row.shop_id || '').trim(), detectedShopName: String(row.detected_shop_name || '').trim(), @@ -71,9 +63,14 @@ export async function getAdminAgisoShopConfigs() { /** @param {AdminAgisoShopConfigSaveInput} [payload] */ export function updateAdminAgisoShopConfigs(payload = /** @type {AdminAgisoShopConfigSaveInput} */ ({})) { const rawItems = Array.isArray(payload.shops) ? payload.shops : [] + const currentDefaults = getAgisoMessagingDefaults() const currentMap = getAgisoShopConfigMap() + const nextDefaults = { ...currentDefaults } const nextMap = {} + applyOptionalStringField(nextDefaults, 'messageTemplate', payload.defaults) + applyOptionalStringField(nextDefaults, 'autoDeliveryMessageTemplate', payload.defaults) + for (const item of rawItems) { const shopId = String(item?.shopId || '').trim() if (!shopId) { @@ -128,27 +125,56 @@ export function updateAdminAgisoShopConfigs(payload = /** @type {AdminAgisoShopC nextMap[shopId] = next } - const saved = saveAgisoShopConfigMap(nextMap) + const saved = saveAgisoMessagingConfig({ + defaults: nextDefaults, + shops: nextMap, + }) return { filePath: getAgisoShopsFilePath(), - shops: Object.entries(saved) + defaults: mapAdminAgisoMessagingDefaults(saved.defaults), + shops: Object.entries(saved.shops) .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(), - autoDeliveryMessageTemplate: String(config.autoDeliveryMessageTemplate || '').trim(), - appSecretConfigured: Boolean(String(config.appSecret || '').trim()), - apiVersion: String(config.apiVersion || '').trim(), - sendMessageEndpoint: String(config.sendMessageEndpoint || '').trim(), - })), + .map(([shopId, config]) => mapAdminAgisoShopConfigItem(shopId, config)), } } +function mapAdminAgisoMessagingDefaults(defaults = {}) { + return { + messageTemplate: String(defaults.messageTemplate || '').trim(), + autoDeliveryMessageTemplate: String(defaults.autoDeliveryMessageTemplate || '').trim(), + } +} + +function mapAdminAgisoShopConfigItem(shopId, config = {}) { + return { + 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(), + autoDeliveryMessageTemplate: String(config.autoDeliveryMessageTemplate || '').trim(), + appSecretConfigured: Boolean(String(config.appSecret || '').trim()), + apiVersion: String(config.apiVersion || '').trim(), + sendMessageEndpoint: String(config.sendMessageEndpoint || '').trim(), + } +} + +function applyOptionalStringField(target, key, source) { + if (!source || typeof source[key] !== 'string') { + return + } + + const value = String(source[key] || '').trim() + if (value) { + target[key] = value + return + } + + delete target[key] +} + export async function getAdminFulfillmentBindingConfigs() { const bindings = getOrderFulfillmentBindingConfigs() const rowsResult = await query( diff --git a/apps/backend/src/services/platforms/agiso/shop-config-service.js b/apps/backend/src/services/platforms/agiso/shop-config-service.js index b52a83d8..b43c7267 100644 --- a/apps/backend/src/services/platforms/agiso/shop-config-service.js +++ b/apps/backend/src/services/platforms/agiso/shop-config-service.js @@ -4,6 +4,17 @@ 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') +const AGISO_MESSAGING_DEFAULT_KEYS = ['messageTemplate', 'autoDeliveryMessageTemplate'] +const AGISO_SHOP_CONFIG_KEYS = [ + 'shopName', + 'accessToken', + 'messageTemplate', + 'autoDeliveryMessageTemplate', + 'appSecret', + 'apiVersion', + 'sendMessageEndpoint', + 'tradeDetailEndpoint', +] export function getAgisoShopsFilePath() { return AGISO_SHOPS_FILE_PATH @@ -11,11 +22,11 @@ export function getAgisoShopsFilePath() { export function getAgisoShopConfigMap() { const envConfig = normalizeAgisoShopConfigMap(runtimeConfig.platforms?.agiso?.messaging?.shops || {}) - const fileConfig = loadAgisoShopConfigMapFromFile() + const fileConfig = loadAgisoMessagingConfigDocumentFromFile() return { ...envConfig, - ...fileConfig, + ...fileConfig.shops, } } @@ -28,24 +39,34 @@ export function getAgisoShopConfig(shopId) { return getAgisoShopConfigMap()[normalizedShopId] || null } -export function saveAgisoShopConfigMap(rawValue) { - const normalized = normalizeAgisoShopConfigMap(rawValue) +export function getAgisoMessagingDefaults() { + return loadAgisoMessagingConfigDocumentFromFile().defaults +} + +export function saveAgisoMessagingConfig(rawValue) { + const normalized = normalizeAgisoMessagingConfigDocument(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() { +function loadAgisoMessagingConfigDocumentFromFile() { if (!fs.existsSync(AGISO_SHOPS_FILE_PATH)) { - return {} + return { + defaults: {}, + shops: {}, + } } try { const rawText = fs.readFileSync(AGISO_SHOPS_FILE_PATH, 'utf8') const parsed = JSON.parse(rawText) - return normalizeAgisoShopConfigMap(parsed) + return normalizeAgisoMessagingConfigDocument(parsed) } catch { - return {} + return { + defaults: {}, + shops: {}, + } } } @@ -64,7 +85,7 @@ function normalizeAgisoShopConfigMap(rawValue) { next.enabled = enabled } - for (const key of ['shopName', 'accessToken', 'messageTemplate', 'autoDeliveryMessageTemplate', 'appSecret', 'apiVersion', 'sendMessageEndpoint', 'tradeDetailEndpoint']) { + for (const key of AGISO_SHOP_CONFIG_KEYS) { const value = String(config[key] || '').trim() if (value) { next[key] = value @@ -77,6 +98,34 @@ function normalizeAgisoShopConfigMap(rawValue) { return output } +function normalizeAgisoMessagingDefaults(rawValue) { + const output = {} + + if (!isPlainObject(rawValue)) { + return output + } + + for (const key of AGISO_MESSAGING_DEFAULT_KEYS) { + const value = String(rawValue[key] || '').trim() + if (value) { + output[key] = value + } + } + + return output +} + +function normalizeAgisoMessagingConfigDocument(rawValue) { + const normalizedValue = isPlainObject(rawValue) ? rawValue : {} + const hasStructuredShape = Object.prototype.hasOwnProperty.call(normalizedValue, 'defaults') + || Object.prototype.hasOwnProperty.call(normalizedValue, 'shops') + + return { + defaults: normalizeAgisoMessagingDefaults(hasStructuredShape ? normalizedValue.defaults : {}), + shops: normalizeAgisoShopConfigMap(hasStructuredShape ? normalizedValue.shops : normalizedValue), + } +} + function normalizeBooleanLike(value) { if (typeof value === 'boolean') { return value diff --git a/apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.js b/apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.js index 1c1fda80..684ef7c0 100644 --- a/apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.js +++ b/apps/backend/src/services/platforms/agiso/xianyu/auto-delivery-service.js @@ -253,7 +253,7 @@ function resolveAgisoXianyuAutoDeliveryConfig(order) { endpoint: String(baseConfig.endpoint || '').trim(), apiVersion: String(baseConfig.apiVersion || shopConfig.apiVersion || '1').trim() || '1', appSecret: String(shopConfig.appSecret || runtimeConfig.platforms?.agiso?.appSecret || '').trim(), - accessToken: String(shopConfig.accessToken || runtimeConfig.platforms?.agiso?.messaging?.accessToken || '').trim(), + accessToken: String(shopConfig.accessToken || '').trim(), aldsType: normalizePositiveInteger(baseConfig.aldsType, 1), ignoreAldsLog: normalizeBooleanLike(baseConfig.ignoreAldsLog, false), ignoreBlackList: normalizeBooleanLike(baseConfig.ignoreBlackList, false), diff --git a/apps/backend/src/services/platforms/agiso/xianyu/message-service.js b/apps/backend/src/services/platforms/agiso/xianyu/message-service.js index 7f4e39c6..d6ffda63 100644 --- a/apps/backend/src/services/platforms/agiso/xianyu/message-service.js +++ b/apps/backend/src/services/platforms/agiso/xianyu/message-service.js @@ -1,7 +1,7 @@ import crypto from 'node:crypto' import { runtimeConfig } from '../../../../config/runtime.js' -import { getAgisoShopConfigMap } from '../shop-config-service.js' +import { getAgisoMessagingDefaults, getAgisoShopConfigMap } from '../shop-config-service.js' import { createMessageDelivery, findLatestSuccessfulMessageDeliveryByTask, @@ -161,12 +161,14 @@ async function deliverAgisoXianyuMessageForTask({ function resolveAgisoXianyuMessagingConfig(order) { const baseConfig = runtimeConfig.platforms?.agiso?.messaging || {} + const fileDefaults = getAgisoMessagingDefaults() const shopId = String(order?.shop_id || '').trim() const shopConfigs = getAgisoShopConfigMap() const shopConfig = shopId && isPlainObject(shopConfigs[shopId]) ? shopConfigs[shopId] : {} return { ...baseConfig, + ...fileDefaults, ...shopConfig, } } diff --git a/apps/backend/src/services/platforms/agiso/xianyu/order-detail-service.js b/apps/backend/src/services/platforms/agiso/xianyu/order-detail-service.js index d2736750..2a3f7354 100644 --- a/apps/backend/src/services/platforms/agiso/xianyu/order-detail-service.js +++ b/apps/backend/src/services/platforms/agiso/xianyu/order-detail-service.js @@ -214,7 +214,7 @@ function resolveAgisoXianyuTradeDetailConfig(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(), + accessToken: String(shopConfig.accessToken || '').trim(), appSecret: String(shopConfig.appSecret || runtimeConfig.platforms?.agiso?.appSecret || '').trim(), timeoutMs: normalizePositiveInteger(shopConfig.tradeDetailTimeoutMs || baseConfig.timeoutMs, DEFAULT_DETAIL_TIMEOUT_MS), } diff --git a/apps/backend/src/types/admin-write-inputs.js b/apps/backend/src/types/admin-write-inputs.js index 5dcf101d..1ef4622e 100644 --- a/apps/backend/src/types/admin-write-inputs.js +++ b/apps/backend/src/types/admin-write-inputs.js @@ -30,6 +30,13 @@ export {} * }} AdminInventoryImportInput */ +/** + * @typedef {{ + * messageTemplate?: string + * autoDeliveryMessageTemplate?: string + * }} AdminAgisoMessagingDefaultsInput + */ + /** * @typedef {{ * shopId?: string @@ -46,6 +53,7 @@ export {} /** * @typedef {{ + * defaults?: AdminAgisoMessagingDefaultsInput * shops?: AdminAgisoShopConfigWriteItemInput[] * }} AdminAgisoShopConfigSaveInput */ diff --git a/apps/backend/src/types/admin-write-models.js b/apps/backend/src/types/admin-write-models.js index 7373f7bb..4bf83600 100644 --- a/apps/backend/src/types/admin-write-models.js +++ b/apps/backend/src/types/admin-write-models.js @@ -20,10 +20,17 @@ export {} * }} AdminInventoryImportResponse */ +/** + * @typedef {{ + * messageTemplate: string + * autoDeliveryMessageTemplate: string + * }} AdminAgisoMessagingDefaults + */ + /** * @typedef {{ * shopId: string - * shopName: string + * shopName: string * accessToken: string * accessTokenMasked: string * enabled: boolean | null @@ -38,6 +45,7 @@ export {} /** * @typedef {{ * filePath: string + * defaults: AdminAgisoMessagingDefaults * shops: AdminAgisoShopConfigItem[] * }} AdminAgisoShopConfigSaveResponse */ diff --git a/apps/backend/src/types/runtime-config.js b/apps/backend/src/types/runtime-config.js index 7b373515..ea38db69 100644 --- a/apps/backend/src/types/runtime-config.js +++ b/apps/backend/src/types/runtime-config.js @@ -86,8 +86,6 @@ export {} * } * messaging: { * enabled: boolean - * appId: string - * accessToken: string * appSecret: string * apiVersion: string * sendMessageEndpoint: string diff --git a/apps/frontend/src/services/admin.ts b/apps/frontend/src/services/admin.ts index fbaac6fb..53ca5949 100644 --- a/apps/frontend/src/services/admin.ts +++ b/apps/frontend/src/services/admin.ts @@ -1,5 +1,6 @@ import { apiGet, apiGetBlob, apiPost } from '@/lib/http' import type { + AdminAgisoMessagingDefaults, AdminAgisoObservedShopItem, AdminAgisoShopConfigItem, AdminAuditLogItem, @@ -67,14 +68,19 @@ export function fetchAdminAuditLogs(params?: Record) { export function fetchAdminAgisoShopConfigs() { return apiGet<{ filePath: string + defaults: AdminAgisoMessagingDefaults shops: AdminAgisoShopConfigItem[] observedShops: AdminAgisoObservedShopItem[] }>('/api/v1/admin/platform-config/agiso-shops') } -export function saveAdminAgisoShopConfigs(payload: { shops: Array> }) { +export function saveAdminAgisoShopConfigs(payload: { + defaults: AdminAgisoMessagingDefaults + shops: Array> +}) { return apiPost<{ filePath: string + defaults: AdminAgisoMessagingDefaults shops: AdminAgisoShopConfigItem[] }>('/api/v1/admin/platform-config/agiso-shops', payload) } diff --git a/apps/frontend/src/types/admin.ts b/apps/frontend/src/types/admin.ts index d2d45c03..7d01b2c1 100644 --- a/apps/frontend/src/types/admin.ts +++ b/apps/frontend/src/types/admin.ts @@ -61,6 +61,11 @@ export interface AdminAgisoShopConfigItem { sendMessageEndpoint: string } +export interface AdminAgisoMessagingDefaults { + messageTemplate: string + autoDeliveryMessageTemplate: string +} + export interface AdminAgisoObservedShopItem { shopId: string detectedShopName: string diff --git a/apps/frontend/src/views/admin/AdminPlatformShopsView.vue b/apps/frontend/src/views/admin/AdminPlatformShopsView.vue index e7948cf3..87e83540 100644 --- a/apps/frontend/src/views/admin/AdminPlatformShopsView.vue +++ b/apps/frontend/src/views/admin/AdminPlatformShopsView.vue @@ -3,10 +3,19 @@ import { onMounted, ref } from 'vue' import { showError, showSuccess } from '@/lib/feedback' import { fetchAdminAgisoShopConfigs, saveAdminAgisoShopConfigs } from '@/services/admin' -import type { AdminAgisoObservedShopItem, AdminAgisoShopConfigItem } from '@/types/admin' +import type { + AdminAgisoMessagingDefaults, + AdminAgisoObservedShopItem, + AdminAgisoShopConfigItem, +} from '@/types/admin' import { hasAdminRole } from '@/utils/admin-auth' import { formatAdminDateTime } from '@/utils/admin-time' +type EditableDefaults = { + messageTemplate: string + autoDeliveryMessageTemplate: string +} + type EditableShop = { id: string shopId: string @@ -21,8 +30,17 @@ const loading = ref(true) const saving = ref(false) const errorMessage = ref('') const filePath = ref('') +const defaults = ref(createEmptyDefaults()) const shops = ref([]) const observedShops = ref([]) +const expandedShopIds = ref([]) + +function createEmptyDefaults(): EditableDefaults { + return { + messageTemplate: '', + autoDeliveryMessageTemplate: '', + } +} function createEmptyShop(): EditableShop { return { @@ -36,6 +54,13 @@ function createEmptyShop(): EditableShop { } } +function mapEditableDefaults(item?: AdminAgisoMessagingDefaults): EditableDefaults { + return { + messageTemplate: item?.messageTemplate ?? '', + autoDeliveryMessageTemplate: item?.autoDeliveryMessageTemplate ?? '', + } +} + function mapEditableShop(item: AdminAgisoShopConfigItem): EditableShop { return { id: crypto.randomUUID(), @@ -60,8 +85,10 @@ async function loadConfigs() { try { const response = await fetchAdminAgisoShopConfigs() filePath.value = response.data.filePath + defaults.value = mapEditableDefaults(response.data.defaults) shops.value = response.data.shops.map(mapEditableShop) observedShops.value = response.data.observedShops + expandedShopIds.value = [] } catch (error) { errorMessage.value = error instanceof Error ? error.message : '读取店铺配置失败' } finally { @@ -70,15 +97,18 @@ async function loadConfigs() { } function addShop() { - shops.value.unshift(createEmptyShop()) + const shop = createEmptyShop() + shops.value.unshift(shop) + expandShop(shop.id) } function removeShop(id: string) { shops.value = shops.value.filter((item) => item.id !== id) + collapseShop(id) } function importObservedShop(item: AdminAgisoObservedShopItem) { - shops.value.unshift({ + const shop: EditableShop = { id: crypto.randomUUID(), shopId: item.shopId, shopName: item.detectedShopName || item.displayShopName || '', @@ -86,10 +116,67 @@ function importObservedShop(item: AdminAgisoObservedShopItem) { enabled: true, messageTemplate: '', autoDeliveryMessageTemplate: '', - }) + } + shops.value.unshift(shop) + expandShop(shop.id) +} + +function isShopExpanded(id: string) { + return expandedShopIds.value.includes(id) +} + +function expandShop(id: string) { + if (expandedShopIds.value.includes(id)) { + return + } + + expandedShopIds.value = [id, ...expandedShopIds.value] +} + +function collapseShop(id: string) { + expandedShopIds.value = expandedShopIds.value.filter((item) => item !== id) +} + +function toggleShop(id: string) { + if (isShopExpanded(id)) { + collapseShop(id) + return + } + + expandShop(id) +} + +function expandAllShops() { + expandedShopIds.value = shops.value.map((item) => item.id) +} + +function collapseAllShops() { + expandedShopIds.value = [] +} + +function describeShop(shop: EditableShop) { + const overrides: string[] = [] + + if (shop.messageTemplate.trim()) { + overrides.push('领取模板') + } + + if (shop.autoDeliveryMessageTemplate.trim()) { + overrides.push('发货模板') + } + + if (overrides.length === 0) { + return '使用默认模板' + } + + return `已覆盖 ${overrides.join('、')}` } async function saveConfigs() { + const payloadDefaults = { + messageTemplate: defaults.value.messageTemplate.trim(), + autoDeliveryMessageTemplate: defaults.value.autoDeliveryMessageTemplate.trim(), + } const payload = shops.value .map((item) => ({ shopId: item.shopId.trim(), @@ -104,8 +191,12 @@ async function saveConfigs() { saving.value = true try { - const response = await saveAdminAgisoShopConfigs({ shops: payload }) + const response = await saveAdminAgisoShopConfigs({ + defaults: payloadDefaults, + shops: payload, + }) filePath.value = response.data.filePath + defaults.value = mapEditableDefaults(response.data.defaults) shops.value = response.data.shops.map(mapEditableShop) showSuccess('店铺配置已保存') await loadConfigs() @@ -124,7 +215,7 @@ onMounted(loadConfigs)

Agiso 店铺配置

-

Agiso 多店铺配置改为文件维护。保存后立即生效,不需要再手改 `.env`。

+

Agiso 默认消息模板和店铺覆盖配置都改为文件维护。保存后立即生效,不需要再手改 `.env`。

刷新 @@ -142,7 +233,7 @@ onMounted(loadConfigs)
说明 - 店铺级 `accessToken` 优先于全局 `AGISO_ACCESS_TOKEN`。未配置的店铺仍会继续使用全局兜底。 + 全局默认消息模板与店铺级覆盖统一保存在此文件中。店铺留空时会回退到上面的默认模板与全局兜底配置。
@@ -153,56 +244,105 @@ onMounted(loadConfigs)
+

默认消息模板

+

留空时会回退到系统内置模板。店铺未单独配置时,默认使用这里的内容。

+
+
+ +
+