feat: 支付商户
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* API 服务模块统一导出
|
||||
*/
|
||||
|
||||
@@ -23,6 +23,7 @@ export { OrderService } from './order'
|
||||
export { AssetService } from './asset'
|
||||
export { AgentRechargeService } from './agentRecharge'
|
||||
export { PaymentSettingsService } from './paymentSettings'
|
||||
export { PaymentMerchantPoolsService } from './paymentMerchantPools'
|
||||
export { SystemConfigService } from './systemConfig'
|
||||
export { ExchangeService } from './exchange'
|
||||
export { RefundService } from './refund'
|
||||
@@ -42,3 +43,4 @@ export { WecomService } from './wecom'
|
||||
|
||||
// TODO: 按需添加其他业务模块
|
||||
// export { SettingService } from './setting'
|
||||
|
||||
|
||||
171
src/api/modules/paymentMerchantPools.ts
Normal file
171
src/api/modules/paymentMerchantPools.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* 支付商户、商户池及微信授权配置 API
|
||||
*/
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import type { BaseResponse } from '@/types/api'
|
||||
import type {
|
||||
CreatePaymentMerchantRequest,
|
||||
PaymentMerchant,
|
||||
PaymentMerchantPageResponse,
|
||||
PaymentMerchantPageResult,
|
||||
PaymentMerchantPool,
|
||||
PaymentMerchantPoolPageResponse,
|
||||
PaymentMerchantPoolPayload,
|
||||
PaymentMerchantPoolQueryParams,
|
||||
PaymentMerchantQueryParams,
|
||||
PaymentMerchantResponse,
|
||||
UpdatePaymentMerchantRequest,
|
||||
UpdateWechatAuthorizationRequest,
|
||||
WechatAuthorizationConfig,
|
||||
WechatAuthorizationResponse
|
||||
} from '@/types/api/paymentMerchantPools'
|
||||
|
||||
const PAYMENT_MERCHANTS_BASE_URL = '/api/admin/payment-merchants'
|
||||
const PAYMENT_MERCHANT_POOLS_BASE_URL = '/api/admin/payment-merchant-pools'
|
||||
const WECHAT_AUTHORIZATIONS_BASE_URL = '/api/admin/wechat-authorizations'
|
||||
|
||||
type RawPaymentMerchant = PaymentMerchant & { credentials?: unknown }
|
||||
|
||||
const sanitizeMerchant = (merchant: RawPaymentMerchant): PaymentMerchant => {
|
||||
// 仅通过解构丢弃 credentials 字段,避免在前端页面/状态中保留支付凭证明文
|
||||
const { credentials: _ignoredCredentials, ...safeMerchant } = merchant
|
||||
void _ignoredCredentials
|
||||
return safeMerchant
|
||||
}
|
||||
|
||||
const sanitizeMerchantPage = (page: PaymentMerchantPageResult): PaymentMerchantPageResult => ({
|
||||
...page,
|
||||
items: (page.items || []).map((item) => sanitizeMerchant(item as RawPaymentMerchant))
|
||||
})
|
||||
|
||||
const sanitizeWechatAuthorization = (
|
||||
config: WechatAuthorizationConfig & Record<string, unknown>
|
||||
): WechatAuthorizationConfig => {
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
// 仅通过解构丢弃敏感字段,禁止在前端响应中保留 AppSecret/Token/AES Key 等明文
|
||||
const {
|
||||
miniapp_app_secret: _miniappAppSecret,
|
||||
oa_app_secret: _oaAppSecret,
|
||||
oa_token: _oaToken,
|
||||
oa_aes_key: _oaAesKey,
|
||||
...safeConfig
|
||||
} = config
|
||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||
|
||||
return safeConfig as WechatAuthorizationConfig
|
||||
}
|
||||
|
||||
export class PaymentMerchantPoolsService extends BaseService {
|
||||
static getPaymentMerchants(
|
||||
params?: PaymentMerchantQueryParams
|
||||
): Promise<PaymentMerchantPageResponse> {
|
||||
return this.get<PaymentMerchantPageResponse>(PAYMENT_MERCHANTS_BASE_URL, params).then(
|
||||
(response) => ({
|
||||
...response,
|
||||
data: sanitizeMerchantPage(response.data)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
static getPaymentMerchantById(id: number): Promise<PaymentMerchantResponse> {
|
||||
return this.get<PaymentMerchantResponse>(`${PAYMENT_MERCHANTS_BASE_URL}/${id}`).then(
|
||||
(response) => ({
|
||||
...response,
|
||||
data: sanitizeMerchant(response.data as RawPaymentMerchant)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
static createPaymentMerchant(
|
||||
data: CreatePaymentMerchantRequest
|
||||
): Promise<PaymentMerchantResponse> {
|
||||
return this.post<PaymentMerchantResponse>(PAYMENT_MERCHANTS_BASE_URL, data).then(
|
||||
(response) => ({
|
||||
...response,
|
||||
data: sanitizeMerchant(response.data as RawPaymentMerchant)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
static updatePaymentMerchant(
|
||||
id: number,
|
||||
data: UpdatePaymentMerchantRequest
|
||||
): Promise<PaymentMerchantResponse> {
|
||||
return this.put<PaymentMerchantResponse>(`${PAYMENT_MERCHANTS_BASE_URL}/${id}`, data).then(
|
||||
(response) => ({
|
||||
...response,
|
||||
data: sanitizeMerchant(response.data as RawPaymentMerchant)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
static deletePaymentMerchant(id: number): Promise<BaseResponse<void>> {
|
||||
return this.delete<BaseResponse<void>>(`${PAYMENT_MERCHANTS_BASE_URL}/${id}`, undefined, {
|
||||
data: { confirm: true }
|
||||
})
|
||||
}
|
||||
|
||||
static getPaymentMerchantPools(
|
||||
params?: PaymentMerchantPoolQueryParams
|
||||
): Promise<PaymentMerchantPoolPageResponse> {
|
||||
return this.get<PaymentMerchantPoolPageResponse>(PAYMENT_MERCHANT_POOLS_BASE_URL, params)
|
||||
}
|
||||
|
||||
static getPaymentMerchantPoolById(id: number): Promise<BaseResponse<PaymentMerchantPool>> {
|
||||
return this.get<BaseResponse<PaymentMerchantPool>>(`${PAYMENT_MERCHANT_POOLS_BASE_URL}/${id}`)
|
||||
}
|
||||
|
||||
static createPaymentMerchantPool(
|
||||
data: PaymentMerchantPoolPayload
|
||||
): Promise<BaseResponse<PaymentMerchantPool>> {
|
||||
return this.post<BaseResponse<PaymentMerchantPool>>(PAYMENT_MERCHANT_POOLS_BASE_URL, data)
|
||||
}
|
||||
|
||||
static updatePaymentMerchantPool(
|
||||
id: number,
|
||||
data: PaymentMerchantPoolPayload
|
||||
): Promise<BaseResponse<PaymentMerchantPool>> {
|
||||
return this.put<BaseResponse<PaymentMerchantPool>>(
|
||||
`${PAYMENT_MERCHANT_POOLS_BASE_URL}/${id}`,
|
||||
data
|
||||
)
|
||||
}
|
||||
|
||||
static enablePaymentMerchantPool(id: number): Promise<BaseResponse<PaymentMerchantPool>> {
|
||||
return this.post<BaseResponse<PaymentMerchantPool>>(
|
||||
`${PAYMENT_MERCHANT_POOLS_BASE_URL}/${id}/enable`
|
||||
)
|
||||
}
|
||||
|
||||
static disablePaymentMerchantPool(id: number): Promise<BaseResponse<PaymentMerchantPool>> {
|
||||
return this.post<BaseResponse<PaymentMerchantPool>>(
|
||||
`${PAYMENT_MERCHANT_POOLS_BASE_URL}/${id}/disable`
|
||||
)
|
||||
}
|
||||
|
||||
static getWechatAuthorization(): Promise<WechatAuthorizationResponse> {
|
||||
return this.get<WechatAuthorizationResponse>(WECHAT_AUTHORIZATIONS_BASE_URL).then(
|
||||
(response) => ({
|
||||
...response,
|
||||
data: sanitizeWechatAuthorization(
|
||||
response.data as WechatAuthorizationConfig & Record<string, unknown>
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
static updateWechatAuthorization(
|
||||
data: UpdateWechatAuthorizationRequest
|
||||
): Promise<WechatAuthorizationResponse> {
|
||||
return this.put<WechatAuthorizationResponse>(
|
||||
`${WECHAT_AUTHORIZATIONS_BASE_URL}/current`,
|
||||
data
|
||||
).then((response) => ({
|
||||
...response,
|
||||
data: sanitizeWechatAuthorization(
|
||||
response.data as WechatAuthorizationConfig & Record<string, unknown>
|
||||
)
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* 权限检查 Composable
|
||||
* 用于在模板或脚本中检查用户权限
|
||||
*/
|
||||
@@ -18,6 +18,9 @@ export function usePermission() {
|
||||
// 是否是超级管理员
|
||||
const isSuperAdmin = computed(() => userStore.isSuperAdmin)
|
||||
|
||||
// 是否是超级管理员或平台用户
|
||||
const isPlatformAccount = computed(() => [1, 2].includes(Number(userStore.info.user_type)))
|
||||
|
||||
/**
|
||||
* 检查是否有指定权限
|
||||
* @param permission 权限码
|
||||
@@ -76,6 +79,7 @@ export function usePermission() {
|
||||
permissions,
|
||||
buttons,
|
||||
isSuperAdmin,
|
||||
isPlatformAccount,
|
||||
hasPermission,
|
||||
hasAnyPermission,
|
||||
hasAllPermissions,
|
||||
@@ -84,3 +88,4 @@ export function usePermission() {
|
||||
hasButton
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,11 +26,20 @@
|
||||
"setting": {
|
||||
"menuType": {
|
||||
"title": "Menu Layout",
|
||||
"list": ["Vertical", "Horizontal", "Mixed", "Dual"]
|
||||
"list": [
|
||||
"Vertical",
|
||||
"Horizontal",
|
||||
"Mixed",
|
||||
"Dual"
|
||||
]
|
||||
},
|
||||
"theme": {
|
||||
"title": "Theme Style",
|
||||
"list": ["Light", "Dark", "System"]
|
||||
"list": [
|
||||
"Light",
|
||||
"Dark",
|
||||
"System"
|
||||
]
|
||||
},
|
||||
"menu": {
|
||||
"title": "Menu Style"
|
||||
@@ -40,11 +49,17 @@
|
||||
},
|
||||
"box": {
|
||||
"title": "Box Style",
|
||||
"list": ["Border", "Shadow"]
|
||||
"list": [
|
||||
"Border",
|
||||
"Shadow"
|
||||
]
|
||||
},
|
||||
"container": {
|
||||
"title": "Container Width",
|
||||
"list": ["Full", "Boxed"]
|
||||
"list": [
|
||||
"Full",
|
||||
"Boxed"
|
||||
]
|
||||
},
|
||||
"basics": {
|
||||
"title": "Basic Config",
|
||||
@@ -82,8 +97,14 @@
|
||||
"notice": {
|
||||
"title": "Notice",
|
||||
"btnRead": "Mark as read",
|
||||
"bar": ["Notice", "Message", "Todo"],
|
||||
"text": ["No"],
|
||||
"bar": [
|
||||
"Notice",
|
||||
"Message",
|
||||
"Todo"
|
||||
],
|
||||
"text": [
|
||||
"No"
|
||||
],
|
||||
"viewAll": "View all"
|
||||
},
|
||||
"worktab": {
|
||||
@@ -275,10 +296,10 @@
|
||||
"evening": "Good evening!"
|
||||
},
|
||||
"exceptionPage": {
|
||||
"gohome": "Go Home",
|
||||
"403": "Sorry, you do not have permission to access this page",
|
||||
"404": "Sorry, the page you are trying to access does not exist",
|
||||
"500": "Sorry, there was an error on the server"
|
||||
"500": "Sorry, there was an error on the server",
|
||||
"gohome": "Go Home"
|
||||
},
|
||||
"menus": {
|
||||
"login": {
|
||||
@@ -478,7 +499,11 @@
|
||||
"detailsOfPaymentConfiguration": "Payment Configuration Details",
|
||||
"paymentMerchant": "Payment Merchant",
|
||||
"developerApi": "Developer API",
|
||||
"commissionTemplate": "Commission Template"
|
||||
"commissionTemplate": "Commission Template",
|
||||
"paymentMerchantPools": "Merchant Pool Management",
|
||||
"paymentMerchantPoolsTabMerchants": "Payment Merchants",
|
||||
"paymentMerchantPoolsTabPools": "Merchant Pools",
|
||||
"paymentMerchantPoolsTabWechatAuth": "WeChat Authorization"
|
||||
},
|
||||
"batch": {
|
||||
"title": "Batch Operations",
|
||||
|
||||
@@ -27,11 +27,20 @@
|
||||
"setting": {
|
||||
"menuType": {
|
||||
"title": "菜单布局",
|
||||
"list": ["垂直", "水平", "混合", "双列"]
|
||||
"list": [
|
||||
"垂直",
|
||||
"水平",
|
||||
"混合",
|
||||
"双列"
|
||||
]
|
||||
},
|
||||
"theme": {
|
||||
"title": "主题风格",
|
||||
"list": ["浅色", "深色", "系统"]
|
||||
"list": [
|
||||
"浅色",
|
||||
"深色",
|
||||
"系统"
|
||||
]
|
||||
},
|
||||
"menu": {
|
||||
"title": "菜单风格"
|
||||
@@ -41,11 +50,17 @@
|
||||
},
|
||||
"box": {
|
||||
"title": "盒子样式",
|
||||
"list": ["边框", "阴影"]
|
||||
"list": [
|
||||
"边框",
|
||||
"阴影"
|
||||
]
|
||||
},
|
||||
"container": {
|
||||
"title": "容器宽度",
|
||||
"list": ["铺满", "定宽"]
|
||||
"list": [
|
||||
"铺满",
|
||||
"定宽"
|
||||
]
|
||||
},
|
||||
"basics": {
|
||||
"title": "基础配置",
|
||||
@@ -83,8 +98,14 @@
|
||||
"notice": {
|
||||
"title": "通知",
|
||||
"btnRead": "标为已读",
|
||||
"bar": ["通知", "消息", "代办"],
|
||||
"text": ["暂无"],
|
||||
"bar": [
|
||||
"通知",
|
||||
"消息",
|
||||
"代办"
|
||||
],
|
||||
"text": [
|
||||
"暂无"
|
||||
],
|
||||
"viewAll": "查看全部"
|
||||
},
|
||||
"worktab": {
|
||||
@@ -110,7 +131,11 @@
|
||||
"admin": "管理员",
|
||||
"user": "普通用户"
|
||||
},
|
||||
"placeholder": ["请输入手机号", "请输入密码", "请拖动滑块完成验证"],
|
||||
"placeholder": [
|
||||
"请输入手机号",
|
||||
"请输入密码",
|
||||
"请拖动滑块完成验证"
|
||||
],
|
||||
"sliderText": "按住滑块拖动",
|
||||
"sliderSuccessText": "验证成功",
|
||||
"rememberPwd": "记住密码",
|
||||
@@ -153,7 +178,11 @@
|
||||
"register": {
|
||||
"title": "创建账号",
|
||||
"subTitle": "欢迎加入我们,请填写以下信息完成注册",
|
||||
"placeholder": ["请输入账号", "请输入密码", "请再次输入密码"],
|
||||
"placeholder": [
|
||||
"请输入账号",
|
||||
"请输入密码",
|
||||
"请再次输入密码"
|
||||
],
|
||||
"rule": [
|
||||
"请再次输入密码",
|
||||
"两次输入密码不一致!",
|
||||
@@ -288,10 +317,10 @@
|
||||
"evening": "晚上好!"
|
||||
},
|
||||
"exceptionPage": {
|
||||
"gohome": "返回首页",
|
||||
"403": "抱歉,您无权访问该页面",
|
||||
"404": "抱歉,您访问的页面不存在",
|
||||
"500": "抱歉,服务器出错了"
|
||||
"500": "抱歉,服务器出错了",
|
||||
"gohome": "返回首页"
|
||||
},
|
||||
"menus": {
|
||||
"login": {
|
||||
@@ -421,7 +450,11 @@
|
||||
"paymentSettings": "支付设置",
|
||||
"detailsOfPaymentConfiguration": "支付配置详情",
|
||||
"withdrawalSettings": "提现配置",
|
||||
"passwordSettings": "密码设置"
|
||||
"passwordSettings": "密码设置",
|
||||
"paymentMerchantPools": "商户池管理",
|
||||
"paymentMerchantPoolsTabMerchants": "支付商户",
|
||||
"paymentMerchantPoolsTabPools": "商户池",
|
||||
"paymentMerchantPoolsTabWechatAuth": "微信授权配置"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* 权限验证相关工具函数
|
||||
*/
|
||||
|
||||
@@ -63,8 +63,17 @@ export const hasRoutePermission = (
|
||||
}
|
||||
}
|
||||
|
||||
// 检查允许访问的用户类型
|
||||
if (route.meta?.allowedUserTypes) {
|
||||
const allowedUserTypes = route.meta.allowedUserTypes as number[]
|
||||
const userType = Number(userInfo.user_type)
|
||||
if (!allowedUserTypes.includes(userType)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 如果路由没有设置额外的权限要求,直接通过
|
||||
if (!route.meta?.roles && !route.meta?.permissions) {
|
||||
if (!route.meta?.roles && !route.meta?.permissions && !route.meta?.allowedUserTypes) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -195,3 +204,4 @@ export const buildLoginRedirect = (currentPath: string): string => {
|
||||
}
|
||||
return `/auth/login?redirect=${encodeURIComponent(currentPath)}`
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { RoutesAlias } from '../routesAlias'
|
||||
import { RoutesAlias } from '../routesAlias'
|
||||
import { AppRouteRecord } from '@/types/router'
|
||||
import { BULK_PURCHASE_PERMISSIONS } from '@/config/constants/bulkPurchase'
|
||||
import { JULY_PERMISSIONS } from '@/config/constants/julyIteration'
|
||||
@@ -836,6 +836,17 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
roles: ['R_SUPER', 'R_ADMIN']
|
||||
}
|
||||
},
|
||||
// 支付商户与商户池管理
|
||||
{
|
||||
path: 'payment-merchant-pools',
|
||||
name: 'PaymentMerchantPools',
|
||||
component: RoutesAlias.PaymentMerchantPools,
|
||||
meta: {
|
||||
title: 'menus.settings.paymentMerchantPools',
|
||||
keepAlive: true,
|
||||
allowedUserTypes: [1, 2]
|
||||
}
|
||||
},
|
||||
// 支付设置详情
|
||||
{
|
||||
path: 'payment-settings/detail/:id',
|
||||
@@ -1096,3 +1107,5 @@ export const asyncRoutes: AppRouteRecord[] = [
|
||||
// ]
|
||||
// },
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* 路由别名,方便快速找到页面,同时可以用作路由跳转
|
||||
*/
|
||||
export enum RoutesAlias {
|
||||
@@ -102,6 +102,7 @@ export enum RoutesAlias {
|
||||
WithdrawalSettings = '/settings/withdrawal-settings', // 提现配置
|
||||
PaymentSettings = '/settings/payment-settings', // 支付设置
|
||||
PaymentSettingsDetail = '/settings/payment-settings/detail', // 支付设置详情
|
||||
PaymentMerchantPools = '/settings/payment-merchant-pools', // 支付商户与商户池管理
|
||||
OperationPasswordSettings = '/settings/operation-password', // 操作密码设置
|
||||
SystemConfigs = '/settings/system-configs', // 系统配置
|
||||
WecomSettings = '/settings/wecom', // 企业微信配置兼容入口
|
||||
@@ -133,3 +134,4 @@ export enum RoutesAlias {
|
||||
|
||||
// 主页路由 - 修改为资产信息页面
|
||||
export const HOME_PAGE = RoutesAlias.AssetInformation
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* API 类型统一导出
|
||||
*/
|
||||
|
||||
@@ -78,6 +78,9 @@ export * from './agentRecharge'
|
||||
// 支付设置相关
|
||||
export * from './paymentSettings'
|
||||
|
||||
// 支付商户与商户池相关
|
||||
export * from './paymentMerchantPools'
|
||||
|
||||
// 系统配置相关
|
||||
export * from './systemConfig'
|
||||
|
||||
@@ -131,3 +134,5 @@ export * from './audit'
|
||||
|
||||
// 企业微信审批配置相关
|
||||
export * from './wecom'
|
||||
|
||||
|
||||
|
||||
207
src/types/api/paymentMerchantPools.ts
Normal file
207
src/types/api/paymentMerchantPools.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* 支付商户与商户池相关类型定义
|
||||
*/
|
||||
|
||||
import type { BaseResponse, PaginationData, PaginationParams } from './common'
|
||||
|
||||
export type PaymentMerchantMethod = 'wechat' | 'alipay'
|
||||
|
||||
export type PaymentMerchantProviderType = 'wechat' | 'wechat_v2' | 'fuiou' | 'alipay'
|
||||
|
||||
export type PaymentPoolStrategy = 'amount' | 'count' | 'time'
|
||||
|
||||
export type PaymentStatisticCycle = 'round' | 'day' | 'month'
|
||||
|
||||
export type PaymentTimePeriodUnit = 'minute' | 'hour' | 'day'
|
||||
|
||||
export type PaymentCredentialValue = string | number | boolean | null
|
||||
|
||||
export type PaymentCredentials = Record<
|
||||
string,
|
||||
PaymentCredentialValue | PaymentCredentialValue[] | Record<string, PaymentCredentialValue>
|
||||
>
|
||||
|
||||
/**
|
||||
* 仅用于前端表单编辑场景:携带本地 ID 用于稳定 v-for 渲染与局部删除。
|
||||
* 提交时会去除 localId,仅保留后端契约的 key / value 字段。
|
||||
*/
|
||||
export interface PaymentCredentialEntry {
|
||||
key: string
|
||||
value: string
|
||||
/** 仅前端使用,提交时丢弃 */
|
||||
localId?: string
|
||||
}
|
||||
|
||||
export interface PaymentMerchant {
|
||||
id: number
|
||||
name: string
|
||||
payment_method: PaymentMerchantMethod
|
||||
provider_type: PaymentMerchantProviderType
|
||||
merchant_identity: string
|
||||
enabled: boolean
|
||||
remark: string
|
||||
credential_version: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface PaymentMerchantQueryParams extends PaginationParams {
|
||||
/** 可选:按名称模糊筛选 */
|
||||
name?: string
|
||||
payment_method?: PaymentMerchantMethod
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export type PaymentMerchantPageResult = PaginationData<PaymentMerchant>
|
||||
|
||||
export interface CreatePaymentMerchantRequest {
|
||||
name: string
|
||||
payment_method: PaymentMerchantMethod
|
||||
provider_type: PaymentMerchantProviderType
|
||||
merchant_identity: string
|
||||
credentials: PaymentCredentials
|
||||
enabled: boolean
|
||||
remark?: string
|
||||
}
|
||||
|
||||
export interface UpdatePaymentMerchantRequest {
|
||||
name?: string
|
||||
enabled?: boolean
|
||||
remark?: string
|
||||
/** 仅在显式更换凭证时填写;未填写时后端保持原值 */
|
||||
credentials?: PaymentCredentials
|
||||
}
|
||||
|
||||
export interface PaymentMerchantPool {
|
||||
id: number
|
||||
name: string
|
||||
payment_method: PaymentMerchantMethod
|
||||
member_ids: number[]
|
||||
enabled: boolean
|
||||
strategy: PaymentPoolStrategy
|
||||
statistic_cycle: PaymentStatisticCycle
|
||||
threshold_amount: number
|
||||
threshold_count: number
|
||||
time_period_started_at: string
|
||||
time_period_unit: PaymentTimePeriodUnit
|
||||
time_period_value: number
|
||||
routing_epoch: number
|
||||
remark: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface PaymentMerchantPoolQueryParams extends PaginationParams {
|
||||
payment_method?: PaymentMerchantMethod
|
||||
}
|
||||
|
||||
export type PaymentMerchantPoolPageResult = PaginationData<PaymentMerchantPool>
|
||||
|
||||
export interface PaymentMerchantPoolPayload {
|
||||
name: string
|
||||
payment_method: PaymentMerchantMethod
|
||||
member_ids: number[]
|
||||
enabled: boolean
|
||||
strategy: PaymentPoolStrategy
|
||||
statistic_cycle?: PaymentStatisticCycle
|
||||
threshold_amount?: number
|
||||
threshold_count?: number
|
||||
time_period_started_at?: string
|
||||
time_period_unit?: PaymentTimePeriodUnit
|
||||
time_period_value?: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
export interface WechatAuthorizationConfig {
|
||||
enabled: boolean
|
||||
miniapp_app_id: string
|
||||
oa_app_id: string
|
||||
oa_oauth_redirect_url: string
|
||||
}
|
||||
|
||||
export interface UpdateWechatAuthorizationRequest {
|
||||
enabled: boolean
|
||||
miniapp_app_id?: string
|
||||
miniapp_app_secret?: string
|
||||
oa_app_id?: string
|
||||
oa_app_secret?: string
|
||||
oa_token?: string
|
||||
oa_aes_key?: string
|
||||
oa_oauth_redirect_url?: string
|
||||
}
|
||||
|
||||
export type PaymentMerchantResponse = BaseResponse<PaymentMerchant>
|
||||
export type PaymentMerchantPageResponse = BaseResponse<PaymentMerchantPageResult>
|
||||
export type PaymentMerchantPoolResponse = BaseResponse<PaymentMerchantPool>
|
||||
export type PaymentMerchantPoolPageResponse = BaseResponse<PaymentMerchantPoolPageResult>
|
||||
export type WechatAuthorizationResponse = BaseResponse<WechatAuthorizationConfig>
|
||||
|
||||
export const PAYMENT_METHOD_OPTIONS: Array<{ label: string; value: PaymentMerchantMethod }> = [
|
||||
{ label: '微信支付', value: 'wechat' },
|
||||
{ label: '支付宝', value: 'alipay' }
|
||||
]
|
||||
|
||||
export const PAYMENT_PROVIDER_OPTIONS: Array<{
|
||||
label: string
|
||||
value: PaymentMerchantProviderType
|
||||
paymentMethod: PaymentMerchantMethod
|
||||
}> = [
|
||||
{ label: '微信直连', value: 'wechat', paymentMethod: 'wechat' },
|
||||
{ label: '微信直连 V2', value: 'wechat_v2', paymentMethod: 'wechat' },
|
||||
{ label: '富友支付', value: 'fuiou', paymentMethod: 'wechat' },
|
||||
{ label: '支付宝', value: 'alipay', paymentMethod: 'alipay' }
|
||||
]
|
||||
|
||||
export const PAYMENT_POOL_STRATEGY_OPTIONS: Array<{ label: string; value: PaymentPoolStrategy }> = [
|
||||
{ label: '按金额轮换', value: 'amount' },
|
||||
{ label: '按笔数轮换', value: 'count' },
|
||||
{ label: '按时间轮换', value: 'time' }
|
||||
]
|
||||
|
||||
export const PAYMENT_STATISTIC_CYCLE_OPTIONS: Array<{
|
||||
label: string
|
||||
value: PaymentStatisticCycle
|
||||
}> = [
|
||||
{ label: '每轮', value: 'round' },
|
||||
{ label: '每天', value: 'day' },
|
||||
{ label: '每月', value: 'month' }
|
||||
]
|
||||
|
||||
export const PAYMENT_TIME_PERIOD_UNIT_OPTIONS: Array<{
|
||||
label: string
|
||||
value: PaymentTimePeriodUnit
|
||||
}> = [
|
||||
{ label: '分钟', value: 'minute' },
|
||||
{ label: '小时', value: 'hour' },
|
||||
{ label: '天', value: 'day' }
|
||||
]
|
||||
|
||||
export function getPaymentMethodLabel(value?: PaymentMerchantMethod): string {
|
||||
return PAYMENT_METHOD_OPTIONS.find((item) => item.value === value)?.label || '-'
|
||||
}
|
||||
|
||||
export function getPaymentProviderLabel(value?: PaymentMerchantProviderType): string {
|
||||
return PAYMENT_PROVIDER_OPTIONS.find((item) => item.value === value)?.label || '-'
|
||||
}
|
||||
|
||||
export function getPaymentPoolStrategyLabel(value?: PaymentPoolStrategy): string {
|
||||
return PAYMENT_POOL_STRATEGY_OPTIONS.find((item) => item.value === value)?.label || '-'
|
||||
}
|
||||
|
||||
export function getPaymentStatisticCycleLabel(value?: PaymentStatisticCycle): string {
|
||||
return PAYMENT_STATISTIC_CYCLE_OPTIONS.find((item) => item.value === value)?.label || '-'
|
||||
}
|
||||
|
||||
export function getPaymentTimePeriodUnitLabel(value?: PaymentTimePeriodUnit): string {
|
||||
return PAYMENT_TIME_PERIOD_UNIT_OPTIONS.find((item) => item.value === value)?.label || '-'
|
||||
}
|
||||
|
||||
/**
|
||||
* 商户池在编辑场景下可选成员:来自同支付方式且已启用的支付商户
|
||||
*/
|
||||
export interface PaymentMerchantPoolMemberOption {
|
||||
id: number
|
||||
name: string
|
||||
payment_method: PaymentMerchantMethod
|
||||
enabled: boolean
|
||||
}
|
||||
@@ -40,6 +40,8 @@ export interface RouteMeta extends Record<string | number | symbol, unknown> {
|
||||
exportTaskScene?: ExportTaskScene
|
||||
/** 是否固定标签页 */
|
||||
fixedTab?: boolean
|
||||
/** 仅允许指定 user_type 访问(1=超级管理员,2=平台用户),为空时不做用户类型限制 */
|
||||
allowedUserTypes?: number[]
|
||||
}
|
||||
|
||||
// 扩展路由记录
|
||||
|
||||
108
src/utils/business/paymentMerchantPool.ts
Normal file
108
src/utils/business/paymentMerchantPool.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import type {
|
||||
PaymentCredentialEntry,
|
||||
PaymentCredentials,
|
||||
PaymentMerchant,
|
||||
PaymentMerchantMethod,
|
||||
PaymentMerchantPoolPayload,
|
||||
PaymentPoolStrategy
|
||||
} from '@/types/api/paymentMerchantPools'
|
||||
|
||||
export const PAYMENT_NO_AVAILABLE_MERCHANT_MESSAGE = '暂无可用商户'
|
||||
export const PAYMENT_FAILED_REISSUE_MESSAGE = '支付失败,请重新发起支付'
|
||||
|
||||
export interface PaymentPoolFormModel {
|
||||
name: string
|
||||
payment_method: PaymentMerchantMethod
|
||||
member_ids: number[]
|
||||
enabled: boolean
|
||||
strategy: PaymentPoolStrategy
|
||||
statistic_cycle: PaymentMerchantPoolPayload['statistic_cycle']
|
||||
threshold_amount_yuan: number | undefined
|
||||
threshold_count: number | undefined
|
||||
time_period_started_at: string
|
||||
time_period_unit: PaymentMerchantPoolPayload['time_period_unit']
|
||||
time_period_value: number | undefined
|
||||
remark: string
|
||||
}
|
||||
|
||||
export const isPlatformUserType = (userType?: number | string | null): boolean =>
|
||||
[1, 2].includes(Number(userType))
|
||||
|
||||
export const credentialEntriesToObject = (
|
||||
entries: PaymentCredentialEntry[]
|
||||
): PaymentCredentials => {
|
||||
return entries.reduce<PaymentCredentials>((credentials, entry) => {
|
||||
const key = entry.key.trim()
|
||||
if (!key || !entry.value) return credentials
|
||||
credentials[key] = entry.value
|
||||
return credentials
|
||||
}, {})
|
||||
}
|
||||
|
||||
export const hasCredentialEntries = (entries: PaymentCredentialEntry[]): boolean =>
|
||||
entries.some((entry) => entry.key.trim() && entry.value)
|
||||
|
||||
export const sortMerchantsByMemberIds = (
|
||||
merchants: PaymentMerchant[],
|
||||
memberIds: number[]
|
||||
): PaymentMerchant[] => {
|
||||
const merchantMap = new Map(merchants.map((merchant) => [Number(merchant.id), merchant]))
|
||||
return memberIds.map((id) => merchantMap.get(Number(id))).filter(Boolean) as PaymentMerchant[]
|
||||
}
|
||||
|
||||
export const yuanToFen = (value?: number | null): number | undefined => {
|
||||
if (value === undefined || value === null || Number.isNaN(Number(value))) return undefined
|
||||
return Math.round(Number(value) * 100)
|
||||
}
|
||||
|
||||
export const buildPaymentPoolPayload = (form: PaymentPoolFormModel): PaymentMerchantPoolPayload => {
|
||||
const payload: PaymentMerchantPoolPayload = {
|
||||
name: form.name.trim(),
|
||||
payment_method: form.payment_method,
|
||||
member_ids: [...form.member_ids],
|
||||
enabled: form.enabled,
|
||||
strategy: form.strategy,
|
||||
remark: form.remark.trim()
|
||||
}
|
||||
|
||||
if (form.strategy === 'amount') {
|
||||
payload.statistic_cycle = form.statistic_cycle
|
||||
payload.threshold_amount = yuanToFen(form.threshold_amount_yuan)
|
||||
} else if (form.strategy === 'count') {
|
||||
payload.statistic_cycle = form.statistic_cycle
|
||||
payload.threshold_count = form.threshold_count
|
||||
} else {
|
||||
payload.time_period_started_at = form.time_period_started_at
|
||||
payload.time_period_unit = form.time_period_unit
|
||||
payload.time_period_value = form.time_period_value
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
export const resolvePaymentFailureMessage = (
|
||||
errorCode: string | number | undefined | null,
|
||||
noAvailableMerchantCode: string | number | undefined | null
|
||||
): string => {
|
||||
if (
|
||||
noAvailableMerchantCode !== undefined &&
|
||||
noAvailableMerchantCode !== null &&
|
||||
errorCode !== undefined &&
|
||||
errorCode !== null &&
|
||||
String(errorCode) === String(noAvailableMerchantCode)
|
||||
) {
|
||||
return PAYMENT_NO_AVAILABLE_MERCHANT_MESSAGE
|
||||
}
|
||||
|
||||
return PAYMENT_FAILED_REISSUE_MESSAGE
|
||||
}
|
||||
|
||||
export const isNoAvailableMerchantError = (
|
||||
errorCode: string | number | undefined | null,
|
||||
noAvailableMerchantCode: string | number | undefined | null
|
||||
): boolean =>
|
||||
noAvailableMerchantCode !== undefined &&
|
||||
noAvailableMerchantCode !== null &&
|
||||
errorCode !== undefined &&
|
||||
errorCode !== null &&
|
||||
String(errorCode) === String(noAvailableMerchantCode)
|
||||
@@ -0,0 +1,717 @@
|
||||
<template>
|
||||
<div class="merchant-management">
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchItems"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="loadMerchants"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton v-if="canManage" type="primary" :icon="Plus" @click="showCreateDrawer">
|
||||
新增支付商户
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="merchants"
|
||||
:currentPage="pagination.page"
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:actions="getActions"
|
||||
:actionsWidth="180"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<ElDrawer
|
||||
v-model="formDrawerVisible"
|
||||
:title="formMode === 'create' ? '新增支付商户' : '编辑支付商户'"
|
||||
size="680px"
|
||||
destroy-on-close
|
||||
@closed="clearSensitiveForm"
|
||||
>
|
||||
<ElAlert
|
||||
title="支付凭证仅用于本次写入,页面不会回显、缓存或记录凭证内容。"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
|
||||
<ElForm ref="formRef" :model="form" :rules="formRules" label-width="120px">
|
||||
<ElFormItem label="商户名称" prop="name">
|
||||
<ElInput v-model="form.name" maxlength="100" placeholder="请输入商户名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="支付方式" prop="payment_method">
|
||||
<ElSelect
|
||||
v-model="form.payment_method"
|
||||
:disabled="formMode === 'edit'"
|
||||
style="width: 100%"
|
||||
@change="handlePaymentMethodChange"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in PAYMENT_METHOD_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="服务商类型" prop="provider_type">
|
||||
<ElSelect
|
||||
v-model="form.provider_type"
|
||||
:disabled="formMode === 'edit'"
|
||||
style="width: 100%"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in providerOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="商户标识" prop="merchant_identity">
|
||||
<ElInput
|
||||
v-model="form.merchant_identity"
|
||||
:disabled="formMode === 'edit'"
|
||||
maxlength="128"
|
||||
placeholder="请输入商户号或应用标识"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="启停状态">
|
||||
<ElSwitch v-model="form.enabled" active-text="启用" inactive-text="停用" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="备注">
|
||||
<ElInput
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-if="formMode === 'edit'" label="凭证状态">
|
||||
<ElTag :type="credentialConfigured ? 'success' : 'info'">
|
||||
{{ credentialConfigured ? '已配置' : '未配置' }}
|
||||
</ElTag>
|
||||
<span class="credential-version">版本:{{ form.credential_version || '-' }}</span>
|
||||
<ElButton type="primary" link @click="startCredentialReplacement">更换凭证</ElButton>
|
||||
</ElFormItem>
|
||||
<template v-if="showCredentialEditor">
|
||||
<ElDivider content-position="left">写入支付凭证</ElDivider>
|
||||
<ElAlert
|
||||
title="凭证值以密码方式输入,提交后立即清空。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<div class="credential-list">
|
||||
<div
|
||||
v-for="(entry, index) in form.credentials"
|
||||
:key="entry.localId"
|
||||
class="credential-row"
|
||||
>
|
||||
<ElInput
|
||||
v-model="entry.key"
|
||||
placeholder="凭证字段名"
|
||||
maxlength="100"
|
||||
aria-label="凭证字段名"
|
||||
/>
|
||||
<ElInput
|
||||
v-model="entry.value"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="凭证值(不可回显)"
|
||||
autocomplete="new-password"
|
||||
aria-label="凭证值"
|
||||
/>
|
||||
<ElButton
|
||||
type="danger"
|
||||
link
|
||||
:icon="Delete"
|
||||
aria-label="删除凭证字段"
|
||||
@click="removeCredential(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ElButton type="primary" plain :icon="Plus" @click="addCredential">添加凭证字段</ElButton>
|
||||
<div v-if="credentialError" class="field-error" role="alert">
|
||||
{{ credentialError }}
|
||||
</div>
|
||||
</template>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="formDrawerVisible = false">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
:disabled="!canManage"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDrawer>
|
||||
|
||||
<ElDrawer v-model="detailDrawerVisible" title="支付商户详情" size="560px">
|
||||
<ElDescriptions v-if="detail" :column="1" border>
|
||||
<ElDescriptionsItem label="商户名称">{{ detail.name }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="支付方式">{{
|
||||
getPaymentMethodLabel(detail.payment_method)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="服务商类型">{{
|
||||
getPaymentProviderLabel(detail.provider_type)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="商户标识">{{ detail.merchant_identity }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="启停状态">{{
|
||||
detail.enabled ? '启用' : '停用'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="支付凭证">{{
|
||||
Number(detail.credential_version) > 0 ? '已配置' : '未配置'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="凭证版本">{{
|
||||
detail.credential_version || '-'
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="备注">{{ detail.remark || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="更新时间">{{
|
||||
formatDateTime(detail.updated_at)
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElDrawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
import { Delete, Plus } from '@element-plus/icons-vue'
|
||||
import { ElButton, ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { PaymentMerchantPoolsService } from '@/api/modules'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { usePermission } from '@/composables/usePermission'
|
||||
import type { SearchFormItem } from '@/types/component'
|
||||
import {
|
||||
PAYMENT_METHOD_OPTIONS,
|
||||
PAYMENT_PROVIDER_OPTIONS,
|
||||
getPaymentMethodLabel,
|
||||
getPaymentProviderLabel,
|
||||
type PaymentCredentialEntry,
|
||||
type PaymentCredentials,
|
||||
type PaymentMerchant,
|
||||
type PaymentMerchantMethod,
|
||||
type PaymentMerchantPageResult,
|
||||
type PaymentMerchantProviderType,
|
||||
type PaymentMerchantQueryParams
|
||||
} from '@/types/api/paymentMerchantPools'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
|
||||
defineOptions({ name: 'PaymentMerchantManagement' })
|
||||
|
||||
type FilterVo = string | number | undefined | null | unknown[]
|
||||
|
||||
const { isPlatformAccount } = usePermission()
|
||||
const canManage = computed(() => isPlatformAccount.value)
|
||||
|
||||
// 列表查询
|
||||
const searchForm = reactive<Record<string, FilterVo>>({
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
name: '',
|
||||
payment_method: null,
|
||||
enabled: null
|
||||
})
|
||||
|
||||
const merchants = ref<PaymentMerchant[]>([])
|
||||
const pagination = reactive({ page: 1, page_size: 10, total: 0 })
|
||||
const loading = ref(false)
|
||||
|
||||
const searchItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '商户名称',
|
||||
prop: 'name',
|
||||
type: 'input',
|
||||
config: { clearable: true, placeholder: '请输入商户名称' }
|
||||
},
|
||||
{
|
||||
label: '支付方式',
|
||||
prop: 'payment_method',
|
||||
type: 'select',
|
||||
options: PAYMENT_METHOD_OPTIONS.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value
|
||||
})),
|
||||
config: { clearable: true }
|
||||
},
|
||||
{
|
||||
label: '启停状态',
|
||||
prop: 'enabled',
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: '启用', value: true },
|
||||
{ label: '停用', value: false }
|
||||
],
|
||||
config: { clearable: true }
|
||||
}
|
||||
]
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '商户名称', prop: 'name', minWidth: 180 },
|
||||
{
|
||||
label: '支付方式',
|
||||
prop: 'payment_method',
|
||||
width: 110,
|
||||
formatter: (row: PaymentMerchant) => getPaymentMethodLabel(row.payment_method)
|
||||
},
|
||||
{
|
||||
label: '服务商类型',
|
||||
prop: 'provider_type',
|
||||
width: 140,
|
||||
formatter: (row: PaymentMerchant) => getPaymentProviderLabel(row.provider_type)
|
||||
},
|
||||
{ label: '商户标识', prop: 'merchant_identity', minWidth: 180 },
|
||||
{
|
||||
label: '启停状态',
|
||||
prop: 'enabled',
|
||||
width: 110,
|
||||
formatter: (row: PaymentMerchant) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ type: row.enabled ? 'success' : 'info' },
|
||||
{ default: () => (row.enabled ? '启用' : '停用') }
|
||||
)
|
||||
},
|
||||
{
|
||||
label: '凭证状态',
|
||||
prop: 'credential_version',
|
||||
width: 110,
|
||||
formatter: (row: PaymentMerchant) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ type: Number(row.credential_version) > 0 ? 'success' : 'info' },
|
||||
{ default: () => (Number(row.credential_version) > 0 ? '已配置' : '未配置') }
|
||||
)
|
||||
},
|
||||
{
|
||||
label: '更新时间',
|
||||
prop: 'updated_at',
|
||||
width: 170,
|
||||
formatter: (row: PaymentMerchant) => formatDateTime(row.updated_at)
|
||||
},
|
||||
{ label: '备注', prop: 'remark', minWidth: 160 }
|
||||
]
|
||||
|
||||
const { columnChecks, columns } = useCheckedColumns(() => columnOptions)
|
||||
|
||||
// 列表加载
|
||||
const loadMerchants = async () => {
|
||||
if (!canManage.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const params: PaymentMerchantQueryParams = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size
|
||||
}
|
||||
const name = searchForm.name as string | undefined
|
||||
if (name) params.name = name
|
||||
if (searchForm.payment_method)
|
||||
params.payment_method = searchForm.payment_method as PaymentMerchantMethod
|
||||
if (typeof searchForm.enabled === 'boolean') params.enabled = searchForm.enabled
|
||||
|
||||
const res = await PaymentMerchantPoolsService.getPaymentMerchants(params)
|
||||
if (res.code === 0) {
|
||||
const data = (res.data as PaymentMerchantPageResult) || {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
size: 10
|
||||
}
|
||||
merchants.value = data.items || []
|
||||
pagination.total = data.total || 0
|
||||
pagination.page = data.page || pagination.page
|
||||
pagination.page_size = data.size || pagination.page_size
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载支付商户失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
loadMerchants()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.name = ''
|
||||
searchForm.payment_method = null
|
||||
searchForm.enabled = null
|
||||
pagination.page = 1
|
||||
loadMerchants()
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.page_size = size
|
||||
pagination.page = 1
|
||||
loadMerchants()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
loadMerchants()
|
||||
}
|
||||
|
||||
// 表单
|
||||
type FormMode = 'create' | 'edit'
|
||||
|
||||
const formDrawerVisible = ref(false)
|
||||
const formMode = ref<FormMode>('create')
|
||||
const submitLoading = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const showCredentialEditor = ref(false)
|
||||
const credentialError = ref('')
|
||||
|
||||
const initialFormState = () => ({
|
||||
id: 0,
|
||||
name: '',
|
||||
payment_method: 'wechat' as PaymentMerchantMethod,
|
||||
provider_type: 'wechat' as PaymentMerchantProviderType,
|
||||
merchant_identity: '',
|
||||
enabled: true,
|
||||
remark: '',
|
||||
credential_version: 0,
|
||||
credentials: [] as PaymentCredentialEntry[]
|
||||
})
|
||||
|
||||
const form = reactive(initialFormState())
|
||||
|
||||
const providerOptions = computed(() =>
|
||||
PAYMENT_PROVIDER_OPTIONS.filter((option) => option.paymentMethod === form.payment_method)
|
||||
)
|
||||
|
||||
const credentialConfigured = computed(() => Number(form.credential_version) > 0)
|
||||
|
||||
const formRules = reactive<FormRules>({
|
||||
name: [
|
||||
{ required: true, message: '请输入商户名称', trigger: 'blur' },
|
||||
{ max: 100, message: '商户名称不超过 100 个字符', trigger: 'blur' }
|
||||
],
|
||||
payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }],
|
||||
provider_type: [{ required: true, message: '请选择服务商类型', trigger: 'change' }],
|
||||
merchant_identity: [
|
||||
{ required: true, message: '请输入商户标识', trigger: 'blur' },
|
||||
{ max: 128, message: '商户标识不超过 128 个字符', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const generateLocalId = () =>
|
||||
`cred-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
|
||||
|
||||
const resetSensitiveForm = () => {
|
||||
form.credentials = []
|
||||
showCredentialEditor.value = false
|
||||
credentialError.value = ''
|
||||
}
|
||||
|
||||
const clearSensitiveForm = () => {
|
||||
resetSensitiveForm()
|
||||
form.id = 0
|
||||
form.credential_version = 0
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
const buildCredentialPayload = (): PaymentCredentials | null => {
|
||||
const credentials: Record<string, string> = {}
|
||||
for (const entry of form.credentials) {
|
||||
const key = entry.key.trim()
|
||||
if (!key) {
|
||||
credentialError.value = '请填写凭证字段名'
|
||||
return null
|
||||
}
|
||||
if (!entry.value) {
|
||||
credentialError.value = `请填写凭证字段 ${key} 的值`
|
||||
return null
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(credentials, key)) {
|
||||
credentialError.value = `凭证字段 ${key} 重复`
|
||||
return null
|
||||
}
|
||||
credentials[key] = entry.value
|
||||
}
|
||||
if (form.credentials.length === 0) {
|
||||
credentialError.value = '请至少添加一个凭证字段'
|
||||
return null
|
||||
}
|
||||
credentialError.value = ''
|
||||
return credentials
|
||||
}
|
||||
|
||||
const showCreateDrawer = () => {
|
||||
if (!canManage.value) return
|
||||
Object.assign(form, initialFormState())
|
||||
formDrawerVisible.value = true
|
||||
formMode.value = 'create'
|
||||
showCredentialEditor.value = true
|
||||
addCredential()
|
||||
}
|
||||
|
||||
const startCredentialReplacement = () => {
|
||||
if (!canManage.value) return
|
||||
form.credentials = []
|
||||
showCredentialEditor.value = true
|
||||
addCredential()
|
||||
}
|
||||
|
||||
const addCredential = () => {
|
||||
form.credentials.push({ localId: generateLocalId(), key: '', value: '' })
|
||||
}
|
||||
|
||||
const removeCredential = (index: number) => {
|
||||
form.credentials.splice(index, 1)
|
||||
}
|
||||
|
||||
const handlePaymentMethodChange = (value: PaymentMerchantMethod) => {
|
||||
form.payment_method = value
|
||||
const firstMatch = PAYMENT_PROVIDER_OPTIONS.find((option) => option.paymentMethod === value)
|
||||
if (firstMatch) {
|
||||
form.provider_type = firstMatch.value
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!canManage.value) return
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
if (formMode.value === 'create' || showCredentialEditor.value) {
|
||||
const credentials = buildCredentialPayload()
|
||||
if (!credentials) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (formMode.value === 'create') {
|
||||
await PaymentMerchantPoolsService.createPaymentMerchant({
|
||||
name: form.name.trim(),
|
||||
payment_method: form.payment_method,
|
||||
provider_type: form.provider_type,
|
||||
merchant_identity: form.merchant_identity.trim(),
|
||||
credentials,
|
||||
enabled: form.enabled,
|
||||
remark: form.remark?.trim() || ''
|
||||
})
|
||||
ElMessage.success('支付商户创建成功')
|
||||
} else {
|
||||
await PaymentMerchantPoolsService.updatePaymentMerchant(form.id, {
|
||||
name: form.name.trim(),
|
||||
enabled: form.enabled,
|
||||
remark: form.remark?.trim() || '',
|
||||
credentials
|
||||
})
|
||||
ElMessage.success('支付凭证更新成功')
|
||||
}
|
||||
formDrawerVisible.value = false
|
||||
await loadMerchants()
|
||||
} catch (error) {
|
||||
console.error('提交支付商户失败:', error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
await PaymentMerchantPoolsService.updatePaymentMerchant(form.id, {
|
||||
name: form.name.trim(),
|
||||
enabled: form.enabled,
|
||||
remark: form.remark?.trim() || ''
|
||||
})
|
||||
ElMessage.success('支付商户已更新')
|
||||
formDrawerVisible.value = false
|
||||
await loadMerchants()
|
||||
} catch (error) {
|
||||
console.error('更新支付商户失败:', error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 详情 / 启停 / 删除
|
||||
const detailDrawerVisible = ref(false)
|
||||
const detail = ref<PaymentMerchant | null>(null)
|
||||
const tableRef = ref()
|
||||
|
||||
const showDetail = async (id: number) => {
|
||||
try {
|
||||
const res = await PaymentMerchantPoolsService.getPaymentMerchantById(id)
|
||||
if (res.code === 0) {
|
||||
detail.value = res.data
|
||||
detailDrawerVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载支付商户详情失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const openEditDrawer = async (id: number) => {
|
||||
if (!canManage.value) return
|
||||
try {
|
||||
const res = await PaymentMerchantPoolsService.getPaymentMerchantById(id)
|
||||
if (res.code !== 0) return
|
||||
const merchant = res.data
|
||||
Object.assign(form, initialFormState(), {
|
||||
id: merchant.id,
|
||||
name: merchant.name,
|
||||
payment_method: merchant.payment_method,
|
||||
provider_type: merchant.provider_type,
|
||||
merchant_identity: merchant.merchant_identity,
|
||||
enabled: merchant.enabled,
|
||||
remark: merchant.remark || '',
|
||||
credential_version: merchant.credential_version
|
||||
})
|
||||
formMode.value = 'edit'
|
||||
showCredentialEditor.value = false
|
||||
formDrawerVisible.value = true
|
||||
} catch (error) {
|
||||
console.error('加载支付商户详情失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleEnabled = async (merchant: PaymentMerchant) => {
|
||||
if (!canManage.value) return
|
||||
const target = !merchant.enabled
|
||||
const action = target ? '启用' : '停用'
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认${action}商户 “${merchant.name}”?`, '操作确认', {
|
||||
type: 'warning'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await PaymentMerchantPoolsService.updatePaymentMerchant(merchant.id, {
|
||||
enabled: target
|
||||
})
|
||||
if (res.code === 0) {
|
||||
ElMessage.success(`已${action}`)
|
||||
await loadMerchants()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${action}支付商户失败:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDeleteMerchant = async (merchant: PaymentMerchant) => {
|
||||
if (!canManage.value) return
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认删除支付商户 “${merchant.name}”?该操作不可恢复。`,
|
||||
'删除确认',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await PaymentMerchantPoolsService.deletePaymentMerchant(merchant.id)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('已删除')
|
||||
if (pagination.total > 1 && merchants.value.length === 1 && pagination.page > 1) {
|
||||
pagination.page -= 1
|
||||
}
|
||||
await loadMerchants()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除支付商户失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getActions = (row: PaymentMerchant) => [
|
||||
{
|
||||
label: '详情',
|
||||
type: 'primary' as const,
|
||||
handler: () => showDetail(row.id)
|
||||
},
|
||||
{
|
||||
label: '编辑',
|
||||
type: 'primary' as const,
|
||||
handler: () => openEditDrawer(row.id),
|
||||
permission: canManage.value ? '' : 'hidden'
|
||||
},
|
||||
{
|
||||
label: row.enabled ? '停用' : '启用',
|
||||
type: 'primary' as const,
|
||||
handler: () => toggleEnabled(row),
|
||||
permission: canManage.value ? '' : 'hidden'
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
type: 'danger' as const,
|
||||
handler: () => confirmDeleteMerchant(row),
|
||||
permission: canManage.value ? '' : 'hidden'
|
||||
}
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
if (canManage.value) {
|
||||
loadMerchants()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
resetSensitiveForm()
|
||||
merchants.value = []
|
||||
detail.value = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.merchant-management {
|
||||
.credential-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.credential-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr auto;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.credential-version {
|
||||
margin: 0 12px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.field-error {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,871 @@
|
||||
<template>
|
||||
<div class="pool-management">
|
||||
<ArtSearchBar
|
||||
v-model:filter="searchForm"
|
||||
:items="searchItems"
|
||||
:show-expand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
|
||||
<ElCard shadow="never" class="art-table-card">
|
||||
<ArtTableHeader
|
||||
:columnList="columnOptions"
|
||||
v-model:columns="columnChecks"
|
||||
@refresh="loadPools"
|
||||
>
|
||||
<template #left>
|
||||
<ElButton v-if="canManage" type="primary" :icon="Plus" @click="showCreateDrawer">
|
||||
新增商户池
|
||||
</ElButton>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
:data="pools"
|
||||
:currentPage="pagination.page"
|
||||
:pageSize="pagination.page_size"
|
||||
:total="pagination.total"
|
||||
:marginTop="10"
|
||||
:actions="getActions"
|
||||
:actionsWidth="220"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
>
|
||||
<template #default>
|
||||
<ElTableColumn v-for="col in columns" :key="col.prop || col.type" v-bind="col" />
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<ElDrawer
|
||||
v-model="formDrawerVisible"
|
||||
:title="formMode === 'create' ? '新增商户池' : '编辑商户池'"
|
||||
size="780px"
|
||||
destroy-on-close
|
||||
@closed="resetForm"
|
||||
>
|
||||
<ElForm ref="formRef" :model="form" :rules="formRules" label-width="120px">
|
||||
<ElFormItem label="商户池名称" prop="name">
|
||||
<ElInput v-model="form.name" maxlength="100" placeholder="请输入商户池名称" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="支付方式" prop="payment_method">
|
||||
<ElSelect
|
||||
v-model="form.payment_method"
|
||||
:disabled="formMode === 'edit'"
|
||||
style="width: 100%"
|
||||
@change="handlePaymentMethodChange"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in PAYMENT_METHOD_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="成员商户" prop="member_ids">
|
||||
<VueDraggable
|
||||
v-if="orderedMemberIds.length"
|
||||
v-model="orderedMemberIds"
|
||||
:animation="150"
|
||||
handle=".drag-handle"
|
||||
class="pool-member-list"
|
||||
>
|
||||
<div v-for="memberId in orderedMemberIds" :key="memberId" class="pool-member-row">
|
||||
<ElIcon class="drag-handle"><Rank /></ElIcon>
|
||||
<span class="member-name">{{ getMemberName(memberId) }}</span>
|
||||
<ElButton type="danger" link :icon="Delete" @click="removeMember(memberId)" />
|
||||
</div>
|
||||
</VueDraggable>
|
||||
<div v-else class="pool-member-empty">尚未选择成员,请从下方选择</div>
|
||||
<ElDivider />
|
||||
<ElSelect
|
||||
v-model="pendingMemberId"
|
||||
filterable
|
||||
placeholder="选择同支付方式的商户"
|
||||
style="width: 100%"
|
||||
:disabled="!form.payment_method"
|
||||
@change="appendMember"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in availableMerchantOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
:disabled="form.member_ids.includes(item.id)"
|
||||
/>
|
||||
</ElSelect>
|
||||
<div v-if="memberError" class="field-error" role="alert">{{ memberError }}</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="轮询策略" prop="strategy">
|
||||
<ElSelect v-model="form.strategy" style="width: 100%">
|
||||
<ElOption
|
||||
v-for="item in PAYMENT_POOL_STRATEGY_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="form.strategy === 'amount' || form.strategy === 'count'"
|
||||
label="统计周期"
|
||||
prop="statistic_cycle"
|
||||
>
|
||||
<ElSelect v-model="form.statistic_cycle" style="width: 100%">
|
||||
<ElOption
|
||||
v-for="item in PAYMENT_STATISTIC_CYCLE_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-if="form.strategy === 'amount'"
|
||||
label="金额阈值(元)"
|
||||
prop="threshold_amount_yuan"
|
||||
>
|
||||
<ElInputNumber
|
||||
v-model="form.threshold_amount_yuan"
|
||||
:min="0.01"
|
||||
:precision="2"
|
||||
:step="100"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem v-else-if="form.strategy === 'count'" label="笔数阈值" prop="threshold_count">
|
||||
<ElInputNumber
|
||||
v-model="form.threshold_count"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
:step="1"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<template v-else-if="form.strategy === 'time'">
|
||||
<ElFormItem label="时间单位" prop="time_period_unit">
|
||||
<ElSelect v-model="form.time_period_unit" style="width: 100%">
|
||||
<ElOption
|
||||
v-for="item in PAYMENT_TIME_PERIOD_UNIT_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="时间长度" prop="time_period_value">
|
||||
<ElInputNumber
|
||||
v-model="form.time_period_value"
|
||||
:min="1"
|
||||
:precision="0"
|
||||
:step="1"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="时间起点" prop="time_period_started_at">
|
||||
<ElDatePicker
|
||||
v-model="form.time_period_started_at"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="请选择时间起点"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<ElFormItem label="启停状态">
|
||||
<ElSwitch v-model="form.enabled" active-text="启用" inactive-text="停用" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="备注">
|
||||
<ElInput
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="formDrawerVisible = false">取消</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:loading="submitLoading"
|
||||
:disabled="!canManage"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
保存
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDrawer>
|
||||
|
||||
<ElDrawer v-model="detailDrawerVisible" title="商户池详情" size="640px">
|
||||
<ElDescriptions v-if="detail" :column="1" border>
|
||||
<ElDescriptionsItem label="商户池名称">{{ detail.name }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="支付方式">{{
|
||||
getPaymentMethodLabel(detail.payment_method)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="成员数量">{{ detail.member_ids.length }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="轮询策略">{{
|
||||
getPaymentPoolStrategyLabel(detail.strategy)
|
||||
}}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem
|
||||
v-if="detail.strategy === 'amount' || detail.strategy === 'count'"
|
||||
label="统计周期"
|
||||
>
|
||||
{{ getPaymentStatisticCycleLabel(detail.statistic_cycle) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="detail.strategy === 'amount'" label="金额阈值">
|
||||
{{ (Number(detail.threshold_amount) / 100).toFixed(2) }} 元
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="detail.strategy === 'count'" label="笔数阈值">
|
||||
{{ detail.threshold_count }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem v-if="detail.strategy === 'time'" label="时间周期">
|
||||
{{ detail.time_period_value }}
|
||||
{{ getPaymentTimePeriodUnitLabel(detail.time_period_unit) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="启停状态">
|
||||
{{ detail.enabled ? '启用' : '停用' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="路由世代">v{{ detail.routing_epoch }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="备注">{{ detail.remark || '-' }}</ElDescriptionsItem>
|
||||
<ElDescriptionsItem label="更新时间">{{
|
||||
formatDateTime(detail.updated_at)
|
||||
}}</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</ElDrawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, h, onMounted, onUnmounted, reactive, ref, watch } from 'vue'
|
||||
import { Delete, Plus, Rank } from '@element-plus/icons-vue'
|
||||
import { VueDraggable } from 'vue-draggable-plus'
|
||||
import { ElButton, ElMessage, ElMessageBox, ElTag } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { PaymentMerchantPoolsService } from '@/api/modules'
|
||||
import { useCheckedColumns } from '@/composables/useCheckedColumns'
|
||||
import { usePermission } from '@/composables/usePermission'
|
||||
import type { SearchFormItem } from '@/types/component'
|
||||
import {
|
||||
PAYMENT_METHOD_OPTIONS,
|
||||
PAYMENT_POOL_STRATEGY_OPTIONS,
|
||||
PAYMENT_STATISTIC_CYCLE_OPTIONS,
|
||||
PAYMENT_TIME_PERIOD_UNIT_OPTIONS,
|
||||
getPaymentMethodLabel,
|
||||
getPaymentPoolStrategyLabel,
|
||||
getPaymentStatisticCycleLabel,
|
||||
getPaymentTimePeriodUnitLabel,
|
||||
type PaymentMerchantMethod,
|
||||
type PaymentMerchantPool,
|
||||
type PaymentMerchantPoolPageResult,
|
||||
type PaymentMerchantPoolPayload,
|
||||
type PaymentMerchantPoolMemberOption,
|
||||
type PaymentPoolStrategy
|
||||
} from '@/types/api/paymentMerchantPools'
|
||||
import { formatDateTime } from '@/utils/business/format'
|
||||
|
||||
defineOptions({ name: 'PaymentMerchantPoolManagement' })
|
||||
|
||||
type FilterVo = string | number | undefined | null | unknown[]
|
||||
|
||||
const { isPlatformAccount } = usePermission()
|
||||
const canManage = computed(() => isPlatformAccount.value)
|
||||
|
||||
const searchForm = reactive<Record<string, FilterVo>>({
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
payment_method: null
|
||||
})
|
||||
|
||||
const pools = ref<PaymentMerchantPool[]>([])
|
||||
const pagination = reactive({ page: 1, page_size: 10, total: 0 })
|
||||
const loading = ref(false)
|
||||
|
||||
const searchItems: SearchFormItem[] = [
|
||||
{
|
||||
label: '支付方式',
|
||||
prop: 'payment_method',
|
||||
type: 'select',
|
||||
options: PAYMENT_METHOD_OPTIONS.map((item) => ({
|
||||
label: item.label,
|
||||
value: item.value
|
||||
})),
|
||||
config: { clearable: true }
|
||||
}
|
||||
]
|
||||
|
||||
const columnOptions = [
|
||||
{ label: '商户池名称', prop: 'name', minWidth: 180 },
|
||||
{
|
||||
label: '支付方式',
|
||||
prop: 'payment_method',
|
||||
width: 110,
|
||||
formatter: (row: PaymentMerchantPool) => getPaymentMethodLabel(row.payment_method)
|
||||
},
|
||||
{
|
||||
label: '成员数量',
|
||||
prop: 'member_ids',
|
||||
width: 110,
|
||||
formatter: (row: PaymentMerchantPool) => `${row.member_ids.length} 个`
|
||||
},
|
||||
{
|
||||
label: '启停状态',
|
||||
prop: 'enabled',
|
||||
width: 110,
|
||||
formatter: (row: PaymentMerchantPool) =>
|
||||
h(
|
||||
ElTag,
|
||||
{ type: row.enabled ? 'success' : 'info' },
|
||||
{ default: () => (row.enabled ? '启用' : '停用') }
|
||||
)
|
||||
},
|
||||
{
|
||||
label: '轮询策略',
|
||||
prop: 'strategy',
|
||||
width: 130,
|
||||
formatter: (row: PaymentMerchantPool) => getPaymentPoolStrategyLabel(row.strategy)
|
||||
},
|
||||
{
|
||||
label: '统计周期',
|
||||
prop: 'statistic_cycle',
|
||||
width: 110,
|
||||
formatter: (row: PaymentMerchantPool) =>
|
||||
row.strategy === 'time' ? '-' : getPaymentStatisticCycleLabel(row.statistic_cycle)
|
||||
},
|
||||
{
|
||||
label: '阈值',
|
||||
prop: 'threshold_summary',
|
||||
width: 150,
|
||||
formatter: (row: PaymentMerchantPool) => describeThreshold(row)
|
||||
},
|
||||
{
|
||||
label: '更新时间',
|
||||
prop: 'updated_at',
|
||||
width: 170,
|
||||
formatter: (row: PaymentMerchantPool) => formatDateTime(row.updated_at)
|
||||
}
|
||||
]
|
||||
|
||||
const { columnChecks, columns } = useCheckedColumns(() => columnOptions)
|
||||
|
||||
const describeThreshold = (row: PaymentMerchantPool): string => {
|
||||
if (row.strategy === 'amount') {
|
||||
return `${(Number(row.threshold_amount) / 100).toFixed(2)} 元`
|
||||
}
|
||||
if (row.strategy === 'count') {
|
||||
return `${row.threshold_count} 笔`
|
||||
}
|
||||
return `${row.time_period_value} ${getPaymentTimePeriodUnitLabel(row.time_period_unit)}`
|
||||
}
|
||||
|
||||
const loadPools = async () => {
|
||||
if (!canManage.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.page,
|
||||
page_size: pagination.page_size,
|
||||
payment_method: searchForm.payment_method as PaymentMerchantMethod | undefined
|
||||
}
|
||||
|
||||
const res = await PaymentMerchantPoolsService.getPaymentMerchantPools(params)
|
||||
if (res.code === 0) {
|
||||
const data = (res.data as PaymentMerchantPoolPageResult) || {
|
||||
items: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
size: 10
|
||||
}
|
||||
pools.value = data.items || []
|
||||
pagination.total = data.total || 0
|
||||
pagination.page = data.page || pagination.page
|
||||
pagination.page_size = data.size || pagination.page_size
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载商户池失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
loadPools()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.payment_method = null
|
||||
pagination.page = 1
|
||||
loadPools()
|
||||
}
|
||||
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.page_size = size
|
||||
pagination.page = 1
|
||||
loadPools()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.page = page
|
||||
loadPools()
|
||||
}
|
||||
|
||||
// 候选成员
|
||||
const availableMerchants = ref<PaymentMerchantPoolMemberOption[]>([])
|
||||
|
||||
const loadAvailableMerchants = async (paymentMethod: PaymentMerchantMethod) => {
|
||||
try {
|
||||
const res = await PaymentMerchantPoolsService.getPaymentMerchants({
|
||||
page: 1,
|
||||
page_size: 100,
|
||||
payment_method: paymentMethod
|
||||
})
|
||||
if (res.code === 0) {
|
||||
const items = res.data?.items || []
|
||||
availableMerchants.value = items.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
payment_method: item.payment_method,
|
||||
enabled: item.enabled
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载候选商户失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const availableMerchantOptions = computed(() =>
|
||||
availableMerchants.value.filter((m) => m.payment_method === form.payment_method)
|
||||
)
|
||||
|
||||
const getMemberName = (id: number) =>
|
||||
availableMerchants.value.find((m) => m.id === id)?.name || `#${id}`
|
||||
|
||||
// 表单
|
||||
type FormMode = 'create' | 'edit'
|
||||
|
||||
const formDrawerVisible = ref(false)
|
||||
const formMode = ref<FormMode>('create')
|
||||
const submitLoading = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const memberError = ref('')
|
||||
const pendingMemberId = ref<number | undefined>(undefined)
|
||||
|
||||
const initialFormState = () => ({
|
||||
id: 0,
|
||||
name: '',
|
||||
payment_method: 'wechat' as PaymentMerchantMethod,
|
||||
member_ids: [] as number[],
|
||||
enabled: true,
|
||||
strategy: 'amount' as PaymentPoolStrategy,
|
||||
statistic_cycle: 'round' as PaymentMerchantPoolPayload['statistic_cycle'],
|
||||
threshold_amount_yuan: 0,
|
||||
threshold_count: 1,
|
||||
time_period_unit: 'hour' as PaymentMerchantPoolPayload['time_period_unit'],
|
||||
time_period_value: 1,
|
||||
time_period_started_at: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const form = reactive(initialFormState())
|
||||
|
||||
const orderedMemberIds = ref<number[]>([])
|
||||
watch(
|
||||
() => form.member_ids,
|
||||
(val) => {
|
||||
orderedMemberIds.value = [...val]
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
watch(orderedMemberIds, (val) => {
|
||||
form.member_ids = [...val]
|
||||
})
|
||||
|
||||
const formRules = reactive<FormRules>({
|
||||
name: [
|
||||
{ required: true, message: '请输入商户池名称', trigger: 'blur' },
|
||||
{ max: 100, message: '商户池名称不超过 100 个字符', trigger: 'blur' }
|
||||
],
|
||||
payment_method: [{ required: true, message: '请选择支付方式', trigger: 'change' }],
|
||||
member_ids: [
|
||||
{
|
||||
validator: (_rule, value: number[], callback) => {
|
||||
if (!value || value.length === 0) {
|
||||
callback(new Error('请至少选择一个成员'))
|
||||
return
|
||||
}
|
||||
const unique = new Set(value)
|
||||
if (unique.size !== value.length) {
|
||||
callback(new Error('成员不可重复'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
strategy: [{ required: true, message: '请选择轮询策略', trigger: 'change' }],
|
||||
statistic_cycle: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if ((form.strategy === 'amount' || form.strategy === 'count') && !value) {
|
||||
callback(new Error('请选择统计周期'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
threshold_amount_yuan: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (form.strategy !== 'amount') {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
const num = Number(value)
|
||||
if (!Number.isFinite(num) || num <= 0) {
|
||||
callback(new Error('金额阈值必须大于 0'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
threshold_count: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (form.strategy !== 'count') {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
const num = Number(value)
|
||||
if (!Number.isFinite(num) || !Number.isInteger(num) || num <= 0) {
|
||||
callback(new Error('笔数阈值必须为正整数'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
time_period_unit: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (form.strategy !== 'time') {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
if (!value) {
|
||||
callback(new Error('请选择时间单位'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
time_period_value: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (form.strategy !== 'time') {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
const num = Number(value)
|
||||
if (!Number.isFinite(num) || !Number.isInteger(num) || num <= 0) {
|
||||
callback(new Error('时间长度必须为正整数'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
time_period_started_at: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (form.strategy !== 'time') {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
if (!value) {
|
||||
callback(new Error('请选择时间起点'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const handlePaymentMethodChange = (value: PaymentMerchantMethod) => {
|
||||
form.payment_method = value
|
||||
form.member_ids = []
|
||||
orderedMemberIds.value = []
|
||||
availableMerchants.value = []
|
||||
memberError.value = ''
|
||||
loadAvailableMerchants(value)
|
||||
}
|
||||
|
||||
const appendMember = (id?: number | string) => {
|
||||
const memberId = Number(id)
|
||||
if (!memberId) {
|
||||
pendingMemberId.value = undefined
|
||||
return
|
||||
}
|
||||
if (form.member_ids.includes(memberId)) {
|
||||
memberError.value = '成员不可重复'
|
||||
} else {
|
||||
form.member_ids = [...form.member_ids, memberId]
|
||||
memberError.value = ''
|
||||
}
|
||||
pendingMemberId.value = undefined
|
||||
}
|
||||
|
||||
const removeMember = (id: number) => {
|
||||
form.member_ids = form.member_ids.filter((memberId) => memberId !== id)
|
||||
}
|
||||
|
||||
const buildPayload = (): PaymentMerchantPoolPayload | null => {
|
||||
if (!form.payment_method) {
|
||||
memberError.value = '请选择支付方式'
|
||||
return null
|
||||
}
|
||||
if (form.member_ids.length === 0) {
|
||||
memberError.value = '请至少选择一个成员'
|
||||
return null
|
||||
}
|
||||
if (new Set(form.member_ids).size !== form.member_ids.length) {
|
||||
memberError.value = '成员不可重复'
|
||||
return null
|
||||
}
|
||||
memberError.value = ''
|
||||
|
||||
const payload: PaymentMerchantPoolPayload = {
|
||||
name: form.name.trim(),
|
||||
payment_method: form.payment_method,
|
||||
member_ids: [...form.member_ids],
|
||||
enabled: form.enabled,
|
||||
strategy: form.strategy,
|
||||
remark: form.remark?.trim() || ''
|
||||
}
|
||||
|
||||
if (form.strategy === 'amount') {
|
||||
if (!form.statistic_cycle) return null
|
||||
const yuan = Number(form.threshold_amount_yuan)
|
||||
if (!Number.isFinite(yuan) || yuan <= 0) return null
|
||||
payload.statistic_cycle = form.statistic_cycle
|
||||
payload.threshold_amount = Math.round(yuan * 100)
|
||||
} else if (form.strategy === 'count') {
|
||||
if (!form.statistic_cycle) return null
|
||||
const count = Number(form.threshold_count)
|
||||
if (!Number.isInteger(count) || count <= 0) return null
|
||||
payload.statistic_cycle = form.statistic_cycle
|
||||
payload.threshold_count = count
|
||||
} else {
|
||||
if (!form.time_period_unit) return null
|
||||
const value = Number(form.time_period_value)
|
||||
if (!Number.isInteger(value) || value <= 0) return null
|
||||
payload.time_period_unit = form.time_period_unit
|
||||
payload.time_period_value = value
|
||||
if (form.time_period_started_at) {
|
||||
payload.time_period_started_at = form.time_period_started_at
|
||||
}
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
const showCreateDrawer = async () => {
|
||||
if (!canManage.value) return
|
||||
Object.assign(form, initialFormState())
|
||||
orderedMemberIds.value = []
|
||||
formDrawerVisible.value = true
|
||||
formMode.value = 'create'
|
||||
await loadAvailableMerchants(form.payment_method)
|
||||
}
|
||||
|
||||
const openEditDrawer = async (pool: PaymentMerchantPool) => {
|
||||
if (!canManage.value) return
|
||||
await loadAvailableMerchants(pool.payment_method)
|
||||
Object.assign(form, initialFormState(), {
|
||||
id: pool.id,
|
||||
name: pool.name,
|
||||
payment_method: pool.payment_method,
|
||||
member_ids: [...pool.member_ids],
|
||||
enabled: pool.enabled,
|
||||
strategy: pool.strategy,
|
||||
statistic_cycle: pool.statistic_cycle,
|
||||
threshold_amount_yuan: Number(pool.threshold_amount) / 100,
|
||||
threshold_count: pool.threshold_count,
|
||||
time_period_unit: pool.time_period_unit,
|
||||
time_period_value: pool.time_period_value,
|
||||
time_period_started_at: pool.time_period_started_at || '',
|
||||
remark: pool.remark || ''
|
||||
})
|
||||
orderedMemberIds.value = [...pool.member_ids]
|
||||
formDrawerVisible.value = true
|
||||
formMode.value = 'edit'
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(form, initialFormState())
|
||||
orderedMemberIds.value = []
|
||||
memberError.value = ''
|
||||
pendingMemberId.value = undefined
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!canManage.value) return
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
const payload = buildPayload()
|
||||
if (!payload) return
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (formMode.value === 'create') {
|
||||
await PaymentMerchantPoolsService.createPaymentMerchantPool(payload)
|
||||
ElMessage.success('商户池创建成功')
|
||||
} else {
|
||||
await PaymentMerchantPoolsService.updatePaymentMerchantPool(form.id, payload)
|
||||
ElMessage.success('商户池已更新')
|
||||
}
|
||||
formDrawerVisible.value = false
|
||||
await loadPools()
|
||||
} catch (error) {
|
||||
console.error('提交商户池失败:', error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 详情
|
||||
const detailDrawerVisible = ref(false)
|
||||
const detail = ref<PaymentMerchantPool | null>(null)
|
||||
const tableRef = ref()
|
||||
|
||||
const showDetail = async (id: number) => {
|
||||
try {
|
||||
const res = await PaymentMerchantPoolsService.getPaymentMerchantPoolById(id)
|
||||
if (res.code === 0) {
|
||||
detail.value = res.data
|
||||
detailDrawerVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载商户池详情失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleEnabled = async (pool: PaymentMerchantPool) => {
|
||||
if (!canManage.value) return
|
||||
const target = !pool.enabled
|
||||
const action = target ? '启用' : '停用'
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认${action}商户池 “${pool.name}”?`, '操作确认', {
|
||||
type: 'warning'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = target
|
||||
? await PaymentMerchantPoolsService.enablePaymentMerchantPool(pool.id)
|
||||
: await PaymentMerchantPoolsService.disablePaymentMerchantPool(pool.id)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success(`已${action}`)
|
||||
await loadPools()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${action}商户池失败:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
const getActions = (row: PaymentMerchantPool) => [
|
||||
{
|
||||
label: '详情',
|
||||
type: 'primary' as const,
|
||||
handler: () => showDetail(row.id)
|
||||
},
|
||||
{
|
||||
label: '编辑',
|
||||
type: 'primary' as const,
|
||||
handler: () => openEditDrawer(row),
|
||||
permission: canManage.value ? '' : 'hidden'
|
||||
},
|
||||
{
|
||||
label: row.enabled ? '停用' : '启用',
|
||||
type: 'primary' as const,
|
||||
handler: () => toggleEnabled(row),
|
||||
permission: canManage.value ? '' : 'hidden'
|
||||
}
|
||||
]
|
||||
|
||||
onMounted(() => {
|
||||
if (canManage.value) {
|
||||
loadPools()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
pools.value = []
|
||||
detail.value = null
|
||||
availableMerchants.value = []
|
||||
orderedMemberIds.value = []
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pool-management {
|
||||
.pool-member-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pool-member-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background-color: rgba(var(--art-gray-200-rgb), 0.6);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.pool-member-row :deep(.drag-handle) {
|
||||
color: var(--el-text-color-secondary);
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.pool-member-empty {
|
||||
padding: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
text-align: center;
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.field-error {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<div class="wechat-authorization-management">
|
||||
<ElCard v-if="canManage" shadow="never" class="art-table-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">微信授权配置</span>
|
||||
<ElTag :type="form.enabled ? 'success' : 'info'">
|
||||
{{ form.enabled ? '已启用' : '已停用' }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ElAlert
|
||||
v-if="!form.enabled"
|
||||
title="微信授权已停用。停用状态下不会影响现有客户端的支付,但新的授权流程将被拦截。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
|
||||
<ElForm ref="formRef" :model="form" :rules="formRules" label-width="140px">
|
||||
<ElFormItem label="启用微信授权">
|
||||
<ElSwitch v-model="form.enabled" active-text="启用" inactive-text="停用" />
|
||||
</ElFormItem>
|
||||
<ElDivider content-position="left">小程序授权</ElDivider>
|
||||
<ElFormItem label="小程序 AppID" prop="miniapp_app_id">
|
||||
<ElInput v-model="form.miniapp_app_id" maxlength="64" placeholder="请输入小程序 AppID" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="小程序 AppSecret">
|
||||
<ElInput
|
||||
v-model="sensitive.miniapp_app_secret"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="如需更换请输入新的 AppSecret,留空表示保持原值"
|
||||
autocomplete="new-password"
|
||||
aria-label="小程序 AppSecret"
|
||||
/>
|
||||
<div class="form-tip">仅在显式更换时填写,提交后立即清空输入。</div>
|
||||
</ElFormItem>
|
||||
<ElDivider content-position="left">公众号授权</ElDivider>
|
||||
<ElFormItem label="公众号 AppID" prop="oa_app_id">
|
||||
<ElInput v-model="form.oa_app_id" maxlength="64" placeholder="请输入公众号 AppID" />
|
||||
</ElFormItem>
|
||||
<ElFormItem label="公众号 AppSecret">
|
||||
<ElInput
|
||||
v-model="sensitive.oa_app_secret"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="如需更换请输入新的 AppSecret,留空表示保持原值"
|
||||
autocomplete="new-password"
|
||||
aria-label="公众号 AppSecret"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="公众号 Token">
|
||||
<ElInput
|
||||
v-model="sensitive.oa_token"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="如需更换请输入新的 Token"
|
||||
autocomplete="new-password"
|
||||
aria-label="公众号 Token"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="公众号 AES Key">
|
||||
<ElInput
|
||||
v-model="sensitive.oa_aes_key"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="如需更换请输入新的 EncodingAESKey"
|
||||
autocomplete="new-password"
|
||||
aria-label="公众号 AES Key"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="OAuth 回调地址" prop="oa_oauth_redirect_url">
|
||||
<ElInput
|
||||
v-model="form.oa_oauth_redirect_url"
|
||||
maxlength="512"
|
||||
placeholder="请输入 OAuth 回调地址"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem>
|
||||
<ElButton type="primary" :loading="submitLoading" @click="handleSubmit"> 保存 </ElButton>
|
||||
<ElButton @click="handleCancel">重置</ElButton>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</ElCard>
|
||||
<ElEmpty v-else description="无访问权限" :image-size="120" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { PaymentMerchantPoolsService } from '@/api/modules'
|
||||
import { usePermission } from '@/composables/usePermission'
|
||||
import type {
|
||||
UpdateWechatAuthorizationRequest,
|
||||
WechatAuthorizationConfig
|
||||
} from '@/types/api/paymentMerchantPools'
|
||||
|
||||
defineOptions({ name: 'WechatAuthorizationManagement' })
|
||||
|
||||
const { isPlatformAccount } = usePermission()
|
||||
const canManage = computed(() => isPlatformAccount.value)
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const submitLoading = ref(false)
|
||||
|
||||
const initialFormState = (): WechatAuthorizationConfig => ({
|
||||
enabled: false,
|
||||
miniapp_app_id: '',
|
||||
oa_app_id: '',
|
||||
oa_oauth_redirect_url: ''
|
||||
})
|
||||
|
||||
const form = reactive<WechatAuthorizationConfig>(initialFormState())
|
||||
|
||||
const initialSensitive = () => ({
|
||||
miniapp_app_secret: '',
|
||||
oa_app_secret: '',
|
||||
oa_token: '',
|
||||
oa_aes_key: ''
|
||||
})
|
||||
|
||||
const sensitive = reactive(initialSensitive())
|
||||
|
||||
const clearSensitive = () => {
|
||||
sensitive.miniapp_app_secret = ''
|
||||
sensitive.oa_app_secret = ''
|
||||
sensitive.oa_token = ''
|
||||
sensitive.oa_aes_key = ''
|
||||
}
|
||||
|
||||
const formRules = reactive<FormRules>({
|
||||
miniapp_app_id: [{ max: 64, message: 'AppID 长度不超过 64 个字符', trigger: 'blur' }],
|
||||
oa_app_id: [{ max: 64, message: 'AppID 长度不超过 64 个字符', trigger: 'blur' }],
|
||||
oa_oauth_redirect_url: [{ max: 512, message: '回调地址长度不超过 512 个字符', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const loadConfig = async () => {
|
||||
if (!canManage.value) return
|
||||
try {
|
||||
const res = await PaymentMerchantPoolsService.getWechatAuthorization()
|
||||
if (res.code === 0) {
|
||||
Object.assign(form, initialFormState(), res.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载微信授权配置失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!canManage.value) return
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
const payload: UpdateWechatAuthorizationRequest = {
|
||||
enabled: form.enabled,
|
||||
miniapp_app_id: form.miniapp_app_id?.trim() || '',
|
||||
oa_app_id: form.oa_app_id?.trim() || '',
|
||||
oa_oauth_redirect_url: form.oa_oauth_redirect_url?.trim() || ''
|
||||
}
|
||||
|
||||
if (sensitive.miniapp_app_secret) {
|
||||
payload.miniapp_app_secret = sensitive.miniapp_app_secret
|
||||
}
|
||||
if (sensitive.oa_app_secret) {
|
||||
payload.oa_app_secret = sensitive.oa_app_secret
|
||||
}
|
||||
if (sensitive.oa_token) {
|
||||
payload.oa_token = sensitive.oa_token
|
||||
}
|
||||
if (sensitive.oa_aes_key) {
|
||||
payload.oa_aes_key = sensitive.oa_aes_key
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const res = await PaymentMerchantPoolsService.updateWechatAuthorization(payload)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('微信授权配置已保存')
|
||||
Object.assign(form, initialFormState(), res.data)
|
||||
clearSensitive()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存微信授权配置失败:', error)
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = async () => {
|
||||
await loadConfig()
|
||||
clearSensitive()
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (canManage.value) {
|
||||
loadConfig()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
clearSensitive()
|
||||
Object.assign(form, initialFormState())
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wechat-authorization-management {
|
||||
.card-header {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
56
src/views/settings/payment-merchant-pools/index.vue
Normal file
56
src/views/settings/payment-merchant-pools/index.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<ArtTableFullScreen>
|
||||
<div class="payment-merchant-pools-page" id="table-full-screen">
|
||||
<ElCard v-if="canAccess" shadow="never" class="art-table-card">
|
||||
<ElTabs v-model="activeTab" type="card" class="payment-merchant-pools-tabs">
|
||||
<ElTabPane :label="merchantsLabel" name="merchants">
|
||||
<MerchantManagement />
|
||||
</ElTabPane>
|
||||
<ElTabPane :label="poolsLabel" name="pools">
|
||||
<PoolManagement />
|
||||
</ElTabPane>
|
||||
<ElTabPane :label="wechatAuthLabel" name="wechat-auth">
|
||||
<WechatAuthorizationManagement />
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</ElCard>
|
||||
<ElEmpty v-else description="无访问权限" :image-size="120" />
|
||||
</div>
|
||||
</ArtTableFullScreen>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import MerchantManagement from './components/MerchantManagement.vue'
|
||||
import PoolManagement from './components/PoolManagement.vue'
|
||||
import WechatAuthorizationManagement from './components/WechatAuthorizationManagement.vue'
|
||||
import { usePermission } from '@/composables/usePermission'
|
||||
|
||||
defineOptions({ name: 'PaymentMerchantPools' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const { isPlatformAccount } = usePermission()
|
||||
const canAccess = computed(() => isPlatformAccount.value)
|
||||
|
||||
const activeTab = ref<'merchants' | 'pools' | 'wechat-auth'>('merchants')
|
||||
|
||||
const merchantsLabel = computed(() => t('menus.settings.paymentMerchantPoolsTabMerchants'))
|
||||
const poolsLabel = computed(() => t('menus.settings.paymentMerchantPoolsTabPools'))
|
||||
const wechatAuthLabel = computed(() => t('menus.settings.paymentMerchantPoolsTabWechatAuth'))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.payment-merchant-pools-page {
|
||||
height: 100%;
|
||||
|
||||
:deep(.payment-merchant-pools-tabs) {
|
||||
.el-tabs__header {
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user