优化店铺配置相关
This commit is contained in:
@@ -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` 兜底。
|
||||
|
||||
## 接口
|
||||
|
||||
|
||||
@@ -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} 已完成自动发货,请注意查收。',
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -86,8 +86,6 @@ export {}
|
||||
* }
|
||||
* messaging: {
|
||||
* enabled: boolean
|
||||
* appId: string
|
||||
* accessToken: string
|
||||
* appSecret: string
|
||||
* apiVersion: string
|
||||
* sendMessageEndpoint: string
|
||||
|
||||
@@ -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<string, unknown>) {
|
||||
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<Record<string, unknown>> }) {
|
||||
export function saveAdminAgisoShopConfigs(payload: {
|
||||
defaults: AdminAgisoMessagingDefaults
|
||||
shops: Array<Record<string, unknown>>
|
||||
}) {
|
||||
return apiPost<{
|
||||
filePath: string
|
||||
defaults: AdminAgisoMessagingDefaults
|
||||
shops: AdminAgisoShopConfigItem[]
|
||||
}>('/api/v1/admin/platform-config/agiso-shops', payload)
|
||||
}
|
||||
|
||||
@@ -61,6 +61,11 @@ export interface AdminAgisoShopConfigItem {
|
||||
sendMessageEndpoint: string
|
||||
}
|
||||
|
||||
export interface AdminAgisoMessagingDefaults {
|
||||
messageTemplate: string
|
||||
autoDeliveryMessageTemplate: string
|
||||
}
|
||||
|
||||
export interface AdminAgisoObservedShopItem {
|
||||
shopId: string
|
||||
detectedShopName: string
|
||||
|
||||
@@ -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<EditableDefaults>(createEmptyDefaults())
|
||||
const shops = ref<EditableShop[]>([])
|
||||
const observedShops = ref<AdminAgisoObservedShopItem[]>([])
|
||||
const expandedShopIds = ref<string[]>([])
|
||||
|
||||
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)
|
||||
<header class="panel-header">
|
||||
<div>
|
||||
<h1>Agiso 店铺配置</h1>
|
||||
<p>Agiso 多店铺配置改为文件维护。保存后立即生效,不需要再手改 `.env`。</p>
|
||||
<p>Agiso 默认消息模板和店铺覆盖配置都改为文件维护。保存后立即生效,不需要再手改 `.env`。</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button round @click="loadConfigs">刷新</el-button>
|
||||
@@ -142,7 +233,7 @@ onMounted(loadConfigs)
|
||||
</div>
|
||||
<div class="meta-line">
|
||||
<span class="meta-label">说明</span>
|
||||
<span>店铺级 `accessToken` 优先于全局 `AGISO_ACCESS_TOKEN`。未配置的店铺仍会继续使用全局兜底。</span>
|
||||
<span>全局默认消息模板与店铺级覆盖统一保存在此文件中。店铺留空时会回退到上面的默认模板与全局兜底配置。</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -153,56 +244,105 @@ onMounted(loadConfigs)
|
||||
<section class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>默认消息模板</h3>
|
||||
<p>留空时会回退到系统内置模板。店铺未单独配置时,默认使用这里的内容。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shop-grid">
|
||||
<label class="field-block field-wide">
|
||||
<span>默认领取链接消息模板</span>
|
||||
<textarea
|
||||
v-model="defaults.messageTemplate"
|
||||
class="text-area"
|
||||
placeholder="例如:您的订单 {platformOrderId} 已创建领取链接,请尽快打开并完成领取:{claimUrl}"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="field-block field-wide">
|
||||
<span>默认自动发货消息模板</span>
|
||||
<textarea
|
||||
v-model="defaults.autoDeliveryMessageTemplate"
|
||||
class="text-area"
|
||||
placeholder="例如:您的订单 {platformOrderId} 已完成自动发货,请注意查收。"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="table-card">
|
||||
<div class="section-title-row">
|
||||
<div>
|
||||
<h3>已配置店铺</h3>
|
||||
<p>至少填写 `shopId` 和 `accessToken`。`shopName` 为空时,界面会退回显示店铺 ID。</p>
|
||||
<p>默认折叠显示。至少填写 `shopId` 和 `accessToken`;`shopName` 为空时,界面会退回显示店铺 ID。</p>
|
||||
</div>
|
||||
<div class="section-actions">
|
||||
<el-button :disabled="shops.length === 0" text @click="expandAllShops">全部展开</el-button>
|
||||
<el-button :disabled="shops.length === 0" text @click="collapseAllShops">全部折叠</el-button>
|
||||
<el-button :loading="saving" round type="primary" @click="saveConfigs">保存配置</el-button>
|
||||
</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>
|
||||
<button type="button" class="shop-summary" @click="toggleShop(shop.id)">
|
||||
<div class="shop-summary-main">
|
||||
<strong>{{ shop.shopName || shop.shopId || '未命名店铺' }}</strong>
|
||||
<span class="cell-subtle">ID: {{ shop.shopId || '未填写' }}</span>
|
||||
</div>
|
||||
<div class="shop-summary-side">
|
||||
<span class="shop-status" :class="{ 'is-disabled': !shop.enabled }">
|
||||
{{ shop.enabled ? '已启用' : '已停用' }}
|
||||
</span>
|
||||
<span class="shop-summary-copy">{{ describeShop(shop) }}</span>
|
||||
<span class="shop-toggle">{{ isShopExpanded(shop.id) ? '收起' : '展开' }}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<label class="field-block">
|
||||
<span>店铺名称</span>
|
||||
<input v-model="shop.shopName" class="text-input" placeholder="例如 大锤商行" />
|
||||
</label>
|
||||
<div v-if="isShopExpanded(shop.id)" class="shop-body">
|
||||
<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 field-wide">
|
||||
<span>AccessToken</span>
|
||||
<input v-model="shop.accessToken" class="text-input" placeholder="AldsIdle..." />
|
||||
</label>
|
||||
<label class="field-block">
|
||||
<span>店铺名称</span>
|
||||
<input v-model="shop.shopName" class="text-input" placeholder="例如 大锤商行" />
|
||||
</label>
|
||||
|
||||
<label class="field-block field-wide">
|
||||
<span>领取链接消息模板</span>
|
||||
<textarea
|
||||
v-model="shop.messageTemplate"
|
||||
class="text-area"
|
||||
placeholder="留空则沿用全局 AGISO_MESSAGE_TEMPLATE"
|
||||
/>
|
||||
</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.autoDeliveryMessageTemplate"
|
||||
class="text-area"
|
||||
placeholder="留空则沿用全局 AGISO_AUTO_DELIVERY_MESSAGE_TEMPLATE"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<label class="field-block field-wide">
|
||||
<span>领取链接消息模板</span>
|
||||
<textarea
|
||||
v-model="shop.messageTemplate"
|
||||
class="text-area"
|
||||
placeholder="留空则沿用上面的默认领取链接模板"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<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>
|
||||
<label class="field-block field-wide">
|
||||
<span>自动发货消息模板</span>
|
||||
<textarea
|
||||
v-model="shop.autoDeliveryMessageTemplate"
|
||||
class="text-area"
|
||||
placeholder="留空则沿用上面的默认自动发货模板"
|
||||
/>
|
||||
</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>
|
||||
</div>
|
||||
</section>
|
||||
@@ -290,6 +430,13 @@ onMounted(loadConfigs)
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.section-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.meta-card,
|
||||
.table-card,
|
||||
.empty-block,
|
||||
@@ -318,10 +465,66 @@ onMounted(loadConfigs)
|
||||
|
||||
.shop-card {
|
||||
margin-top: 14px;
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.shop-summary {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
padding: 16px;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.shop-summary-main,
|
||||
.shop-summary-side {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.shop-summary-side {
|
||||
justify-items: end;
|
||||
}
|
||||
|
||||
.shop-summary-main strong,
|
||||
.shop-toggle {
|
||||
color: #1d3555;
|
||||
}
|
||||
|
||||
.shop-summary-copy {
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.shop-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(22, 163, 74, 0.12);
|
||||
color: #166534;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.shop-status.is-disabled {
|
||||
background: rgba(148, 163, 184, 0.14);
|
||||
color: #475467;
|
||||
}
|
||||
|
||||
.shop-body {
|
||||
padding: 0 16px 16px;
|
||||
border-top: 1px solid rgba(148, 163, 184, 0.18);
|
||||
}
|
||||
|
||||
.shop-grid {
|
||||
@@ -426,5 +629,14 @@ onMounted(loadConfigs)
|
||||
.shop-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.shop-summary {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.shop-summary-side {
|
||||
justify-items: start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user