@@ -0,0 +1,155 @@
< template >
< section class = "payments-page" >
< el-alert v-if = "lastError" :title="lastError" type="error" show-icon closable @close="lastError = ''" / >
< el-alert v-if = "lastResult" :title="lastResult" type="success" show-icon closable @close="lastResult = ''" / >
< section class = "payment-metrics" >
< span > < small > 收款账户 < / small > < strong > { { snapshot . accounts . length } } < / strong > < / span >
< span > < small > 已授权账户 < / small > < strong > { { authorizedAccounts } } < / strong > < / span >
< span > < small > 分账接收方 < / small > < strong > { { snapshot . receivers . length } } < / strong > < / span >
< span > < small > 近百笔分账金额 < / small > < strong > { { money ( sharedAmountCents ) } } < / strong > < / span >
< / section >
< section class = "panel" >
< header class = "panel-toolbar payment-toolbar" >
< div > < p class = "section-kicker" > M08 - D · 资金运营 < / p > < h3 > 支付 、 退款与分账 < / h3 > < / div >
< div class = "toolbar-actions" >
< el-select v-model = "storeId" class="filter-select" filterable placeholder="全部账户范围" @change="loadSnapshot" >
< el -option label = "全部账户范围" value = "" / >
< el-option v-for = "store in stores" :key="store.id" :label="store.name" :value="store.id" / >
< / el-select >
< el-button :icon = "RefreshCw" :loading = "loading" @click ="loadSnapshot" > 刷新 < / el -button >
< / div >
< / header >
< el-tabs v-model = "activeTab" class="payment-tabs" >
< el -tab -pane label = "分账记录" name = "shares" >
< el-table v-loading = "loading" :data="snapshot.shares" class="data-table" row-key="id" >
< el -table -column label = "分账单" min -width = " 180 " > < template # default = "{ row }" > < div class = "stack" > < strong > { { row . shareNo } } < / strong > < span > 支付 # { { row . paymentId } } · 订单 # { { row . orderId } } < / span > < / div > < / template > < / el-table-column >
< el-table-column label = "接收方" min -width = " 160 " > < template # default = "{ row }" > < div class = "stack" > < strong > { { row . receiverMasked } } < / strong > < span > { { row . receiverType } } · { { percent ( row . percentageBps ) } } < / span > < / div > < / template > < / el-table-column >
< el-table-column label = "金额" width = "130" > < template # default = "{ row }" > < strong > { { money ( row . amountCents ) } } < / strong > < / template > < / el-table-column >
< el-table-column label = "状态" width = "130" > < template # default = "{ row }" > < el-tag :type = "shareStatusType(row.status)" > { { row . status } } < / el-tag > < / template > < / el-table-column >
< el-table-column label = "门店 / 时间" min -width = " 180 " > < template # default = "{ row }" > < div class = "stack" > < strong > { { storeName ( row . storeId ) } } < / strong > < span > { { formatDate ( row . createdAt ) } } < / span > < span v-if = "row.failureCode" > {{ row.failureCode }} < / span > < / div > < / template > < / el -table -column >
< / el-table >
< / el-tab-pane >
< el-tab-pane label = "收款账户" name = "accounts" >
< div class = "tab-action-row" > < span > 凭据只保存环境引用 , 页面不读取或回显私钥 。 < / span > < el-button type = "primary" :icon = "Plus" @click ="openAccountForm" > 配置账户 < / el -button > < / div >
< el-table :data = "snapshot.accounts" class = "data-table" row -key = " id " >
< el-table-column prop = "id" label = "账户 ID" width = "100" / >
< el-table-column label = "范围" min -width = " 150 " > < template # default = "{ row }" > { { row . storeId ? storeName ( row . storeId ) : '租户默认' } } < / template > < / el-table-column >
< el-table-column prop = "merchantId" label = "商户号" min -width = " 160 " / >
< el-table-column label = "授权" width = "130" > < template # default = "{ row }" > < el-tag :type = "authorizationType(row.authorizationStatus)" > { { authorizationLabel ( row . authorizationStatus ) } } < / el-tag > < / template > < / el-table-column >
< el-table-column label = "分账 / 启用" min -width = " 150 " > < template # default = "{ row }" > < div class = "profile-badges" > < el-tag : type = "truthy(row.profitSharingEnabled) ? 'success' : 'info'" size = "small" > { { truthy ( row . profitSharingEnabled ) ? '分账开启' : '分账关闭' } } < / el-tag > < el-tag : type = "truthy(row.enabled) ? 'success' : 'info'" size = "small" > { { truthy ( row . enabled ) ? '已启用' : '已停用' } } < / el-tag > < / div > < / template > < / el-table-column >
< / el-table >
< / el-tab-pane >
< el-tab-pane label = "接收方与策略" name = "policies" >
< div class = "tab-action-row" > < span > 分账比例由服务端校验 , 总和不得超过 100 % 。 < / span > < div class = "row-actions" > < el-button :icon = "Plus" @click ="openReceiverForm" > 新增接收方 < / el -button > < el-button type = "primary" :icon = "Plus" @click ="openPolicyForm" > 新增策略 < / el -button > < / div > < / div >
< el-table :data = "snapshot.receivers" class = "data-table" row -key = " id " >
< el-table-column prop = "name" label = "接收方" min -width = " 150 " / >
< el-table-column prop = "receiverMasked" label = "脱敏标识" min -width = " 170 " / >
< el-table-column prop = "receiverType" label = "类型" min -width = " 150 " / >
< el-table-column prop = "relationType" label = "关系" min -width = " 120 " / >
< el-table-column label = "授权" width = "120" > < template # default = "{ row }" > < el-tag :type = "authorizationType(row.authorizationStatus)" > { { authorizationLabel ( row . authorizationStatus ) } } < / el-tag > < / template > < / el-table-column >
< / el-table >
< el-table :data = "snapshot.policies" class = "data-table policy-table" row -key = " id " >
< el-table-column prop = "id" label = "策略 ID" width = "100" / >
< el-table-column label = "账户" min -width = " 120 " > < template # default = "{ row }" > # { { row . collectionAccountId } } < / template > < / el-table-column >
< el-table-column label = "范围" min -width = " 160 " > < template # default = "{ row }" > { { row . storeId ? storeName ( row . storeId ) : '账户默认范围' } } < / template > < / el-table-column >
< el-table-column label = "接收方" min -width = " 160 " > < template # default = "{ row }" > { { receiverName ( row . receiverId ) } } < / template > < / el-table-column >
< el-table-column label = "比例" width = "120" > < template # default = "{ row }" > < strong > { { percent ( row . percentageBps ) } } < / strong > < / template > < / el-table-column >
< / el-table >
< / el-tab-pane >
< el-tab-pane label = "资金工具" name = "tools" >
< div class = "payment-tools-grid" >
< article > < header > < RotateCcw :size = "20" / > < div > < strong > 微信退款 < / strong > < span > 金额由操作人明确填写 , 服务端校验可退余额与幂等键 。 < / span > < / div > < / header > < el-button type = "primary" plain @click ="toolDialog = 'refund'" > 发起退款 < / el -button > < / article >
< article > < header > < ReceiptText :size = "20" / > < div > < strong > 微信对账单 < / strong > < span > 按门店 、 账单日期与类型请求官方账单下载地址 。 < / span > < / div > < / header > < el-button type = "primary" plain @click ="toolDialog = 'reconciliation'" > 请求对账 < / el -button > < / article >
< article > < header > < Split :size = "20" / > < div > < strong > 执行分账 < / strong > < span > 生产 API 模式需账户与接收方均授权 ; Mock 仅非生产可用 。 < / span > < / div > < / header > < el-button type = "primary" plain @click ="toolDialog = 'share'" > 执行分账 < / el -button > < / article >
< / div >
< / el-tab-pane >
< / el-tabs >
< / section >
< el-dialog v-model = "accountDialog" title="配置收款账户" width="min(620px, 94vw)" >
< el -form label -position = " top " class = "asset-form" > < el-form-item label = "门店范围" > < el-select v-model = "accountForm.storeId" clearable><el-option label="租户默认" value="" /><el-option v-for="store in stores" :key="store.id" :label="store.name" :value="store.id" /></el-select></el-form-item><el-form-item label="商户号"><el-input v-model="accountForm.merchantId" /></el-form-item><el-form-item label="凭据环境引用" class="form-span-2"><el-input v-model="accountForm.credentialRef" placeholder="env:WECHAT_PAY_STORE_001" /></el-form-item><el-form-item label="授权状态"><el-select v-model="accountForm.authorizationStatus"><el-option v-for="item in authorizationOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item><el-form-item label="开关"><div class="row-actions"><el-switch v-model="accountForm.profitSharingEnabled" active-text="分账" /><el-switch v-model="accountForm.enabled" active-text="启用" / > < / div > < / el-form-item > < / el-form >
< template # footer > < el-button @click ="accountDialog = false" > 取消 < / el -button > < el-button type = "primary" :loading = "saving" @click ="saveAccount" > 保存账户 < / el -button > < / template >
< / el-dialog >
< el-dialog v-model = "receiverDialog" title="新增或更新分账接收方" width="min(620px, 94vw)" >
< el -form label -position = " top " class = "asset-form" > < el-form-item label = "收款账户" > < el-select v-model = "receiverForm.collectionAccountId"><el-option v-for="item in snapshot.accounts" :key="item.id" :label="`#${item.id} ${item.merchantId}`" :value="item.id" /></el-select></el-form-item><el-form-item label="接收方类型"><el-select v-model="receiverForm.receiverType"><el-option label="商户号" value="MERCHANT_ID" /><el-option label="个人 OpenID" value="PERSONAL_OPENID" /></el-select></el-form-item><el-form-item label="接收方账户"><el-input v-model="receiverForm.receiverAccount" type="password" show-password /></el-form-item><el-form-item label="接收方凭据引用"><el-input v-model="receiverForm.receiverCredentialRef" /></el-form-item><el-form-item label="名称"><el-input v-model="receiverForm.name" /></el-form-item><el-form-item label="关系类型"><el-input v-model="receiverForm.relationType" placeholder="SERVICE_PROVIDER" /></el-form-item><el-form-item label="授权状态"><el-select v-model="receiverForm.authorizationStatus"><el-option v-for="item in authorizationOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item><el-form-item label="启用"><el-switch v-model="receiverForm.enabled" / > < / el-form-item > < / el-form >
< template # footer > < el-button @click ="receiverDialog = false" > 取消 < / el -button > < el-button type = "primary" :loading = "saving" @click ="saveReceiver" > 保存接收方 < / el -button > < / template >
< / el-dialog >
< el-dialog v-model = "policyDialog" title="新增或更新分账策略" width="min(560px, 94vw)" >
< el -form label -position = " top " > < el-form-item label = "收款账户" > < el-select v-model = "policyForm.collectionAccountId"><el-option v-for="item in snapshot.accounts" :key="item.id" :label="`#${item.id} ${item.merchantId}`" :value="item.id" /></el-select></el-form-item><el-form-item label="门店范围"><el-select v-model="policyForm.storeId" clearable><el-option label="账户默认范围" value="" /><el-option v-for="store in stores" :key="store.id" :label="store.name" :value="store.id" /></el-select></el-form-item><el-form-item label="接收方"><el-select v-model="policyForm.receiverId"><el-option v-for="item in snapshot.receivers" :key="item.id" :label="`${item.name} · ${item.receiverMasked}`" :value="item.id" /></el-select></el-form-item><el-form-item label="分账比例(%) "><el-input-number v-model="policyForm.percentage" :min="0.01" :max="100" :precision="2" /></el-form-item><el-form-item label="启用"><el-switch v-model="policyForm.enabled" / > < / el-form-item > < / el-form >
< template # footer > < el-button @click ="policyDialog = false" > 取消 < / el -button > < el-button type = "primary" :loading = "saving" @click ="savePolicy" > 保存策略 < / el -button > < / template >
< / el-dialog >
< el-dialog : model -value = " toolDialog ! = = ' ' " :title = "toolTitle" width = "min(560px, 94vw)" @close ="toolDialog = ''" >
< el -form v-if = "toolDialog === 'refund'" label-position="top"><el-form-item label="支付 ID"><el-input v-model="refundForm.paymentId" /></el-form-item><el-form-item label="退款金额(元)"><el-input-number v-model="refundForm.amountYuan" :min="0.01" :precision="2" /></el-form-item><el-form-item label="退款原因"><el-input v-model="refundForm.reason" type="textarea" / > < / el-form-item > < / el-form >
< el-form v-else-if = "toolDialog === 'reconciliation'" label-position="top"><el-form-item label="门店"><el-select v-model="reconciliationForm.storeId"><el-option v-for="store in stores" :key="store.id" :label="store.name" :value="store.id" /></el-select></el-form-item><el-form-item label="账单日期"><el-date-picker v-model="reconciliationForm.billDate" type="date" value-format="YYYY-MM-DD" /></el-form-item><el-form-item label="账单类型"><el-radio-group v-model="reconciliationForm.billType"><el-radio value="ALL">全部</el-radio><el-radio value="SUCCESS">支付</el-radio><el-radio value="REFUND" > 退款 < / el -radio > < / el-radio-group > < / el-form-item > < / el-form >
< el-form v-else label -position = " top " > < el-form-item label = "支付 ID" > < el-input v-model = "shareForm.paymentId" /></el-form-item><el-form-item label="执行模式"><el-radio-group v-model="shareForm.mode"><el-radio value="API">生产 API</el-radio><el-radio value="MOCK" > 非生产 Mock < / el -radio > < / el-radio-group > < / el-form-item > < / el-form >
< template # footer > < el-button @click ="toolDialog = ''" > 取消 < / el -button > < el-button type = "primary" :loading = "saving" @click ="executeTool" > 确认执行 < / el -button > < / template >
< / el-dialog >
< / section >
< / template >
< script setup lang = "ts" >
import { computed , reactive , ref , watch } from 'vue' ;
import { ElMessage } from 'element-plus' ;
import { Plus , ReceiptText , RefreshCw , RotateCcw , Split } from '@lucide/vue' ;
import {
ApiError , createWechatRefund , executeProfitSharing , getProfitSharingSnapshot ,
listManagedStores , requestWechatReconciliation , saveCollectionAccount ,
saveProfitSharePolicy , saveProfitShareReceiver , type ApiSession
} from '../api' ;
import { money } from '../format' ;
import type { ManagedStore , PaymentAuthorizationStatus , ProfitSharingSnapshot } from '../types' ;
const props = defineProps < { session : ApiSession } > ( ) ;
const stores = ref < ManagedStore [ ] > ( [ ] ) ;
const snapshot = reactive < ProfitSharingSnapshot > ( { accounts : [ ] , receivers : [ ] , policies : [ ] , shares : [ ] } ) ;
const storeId = ref ( '' ) ;
const activeTab = ref ( 'shares' ) ;
const loading = ref ( false ) ;
const saving = ref ( false ) ;
const lastError = ref ( '' ) ;
const lastResult = ref ( '' ) ;
const accountDialog = ref ( false ) ;
const receiverDialog = ref ( false ) ;
const policyDialog = ref ( false ) ;
const toolDialog = ref < '' | 'refund' | 'reconciliation' | 'share' > ( '' ) ;
const accountForm = reactive ( { storeId : '' , merchantId : '' , credentialRef : '' , authorizationStatus : 'UNAUTHORIZED' as PaymentAuthorizationStatus , profitSharingEnabled : false , enabled : true } ) ;
const receiverForm = reactive ( { collectionAccountId : '' , receiverType : 'MERCHANT_ID' as 'MERCHANT_ID' | 'PERSONAL_OPENID' , receiverAccount : '' , receiverCredentialRef : '' , relationType : 'SERVICE_PROVIDER' , name : '' , authorizationStatus : 'UNAUTHORIZED' as PaymentAuthorizationStatus , enabled : true } ) ;
const policyForm = reactive ( { collectionAccountId : '' , storeId : '' , receiverId : '' , percentage : 1 , enabled : true } ) ;
const refundForm = reactive ( { paymentId : '' , amountYuan : 0 , reason : '' } ) ;
const reconciliationForm = reactive ( { storeId : '' , billDate : '' , billType : 'ALL' as 'ALL' | 'SUCCESS' | 'REFUND' } ) ;
const shareForm = reactive ( { paymentId : '' , mode : 'API' as 'API' | 'MOCK' } ) ;
const authorizationOptions : Array < { value : PaymentAuthorizationStatus ; label : string } > = [ { value : 'UNAUTHORIZED' , label : '未授权' } , { value : 'PENDING' , label : '授权中' } , { value : 'AUTHORIZED' , label : '已授权' } , { value : 'REVOKED' , label : '已撤销' } ] ;
const authorizedAccounts = computed ( ( ) => snapshot . accounts . filter ( ( item ) => item . authorizationStatus === 'AUTHORIZED' ) . length ) ;
const sharedAmountCents = computed ( ( ) => snapshot . shares . reduce ( ( sum , item ) => sum + Number ( item . amountCents || 0 ) , 0 ) ) ;
const toolTitle = computed ( ( ) => toolDialog . value === 'refund' ? '发起微信退款' : toolDialog . value === 'reconciliation' ? '请求微信对账单' : '执行微信分账' ) ;
async function capture < T > ( work : ( ) => Promise < T > ) { lastError . value = '' ; try { return await work ( ) ; } catch ( error ) { lastError . value = error instanceof ApiError ? ` ${ error . code } ${ error . traceId ? ` · ${ error . traceId } ` : '' } ` : error instanceof Error ? error . message : '操作失败' ; throw error ; } }
async function loadWorkspace ( ) { try { stores . value = await capture ( ( ) => listManagedStores ( props . session ) ) ; await loadSnapshot ( ) ; } catch { /* displayed */ } }
async function loadSnapshot ( ) { if ( ! props . session . token ) return ; loading . value = true ; try { Object . assign ( snapshot , await capture ( ( ) => getProfitSharingSnapshot ( props . session , storeId . value || undefined ) ) ) ; } catch { /* displayed */ } finally { loading . value = false ; } }
function openAccountForm ( ) { Object . assign ( accountForm , { storeId : storeId . value , merchantId : '' , credentialRef : '' , authorizationStatus : 'UNAUTHORIZED' , profitSharingEnabled : false , enabled : true } ) ; accountDialog . value = true ; }
function openReceiverForm ( ) { Object . assign ( receiverForm , { collectionAccountId : snapshot . accounts [ 0 ] ? . id || '' , receiverType : 'MERCHANT_ID' , receiverAccount : '' , receiverCredentialRef : '' , relationType : 'SERVICE_PROVIDER' , name : '' , authorizationStatus : 'UNAUTHORIZED' , enabled : true } ) ; receiverDialog . value = true ; }
function openPolicyForm ( ) { Object . assign ( policyForm , { collectionAccountId : snapshot . accounts [ 0 ] ? . id || '' , storeId : storeId . value , receiverId : snapshot . receivers [ 0 ] ? . id || '' , percentage : 1 , enabled : true } ) ; policyDialog . value = true ; }
async function saveAccount ( ) { if ( ! accountForm . merchantId || ! accountForm . credentialRef ) return ElMessage . warning ( '请填写商户号和凭据引用' ) ; saving . value = true ; try { await capture ( ( ) => saveCollectionAccount ( props . session , { ... accountForm , storeId : accountForm . storeId || null } ) ) ; accountDialog . value = false ; ElMessage . success ( '收款账户已保存' ) ; await loadSnapshot ( ) ; } catch { /* displayed */ } finally { saving . value = false ; } }
async function saveReceiver ( ) { if ( ! receiverForm . collectionAccountId || ! receiverForm . receiverAccount || ! receiverForm . receiverCredentialRef || ! receiverForm . name ) return ElMessage . warning ( '请完整填写接收方资料' ) ; saving . value = true ; try { await capture ( ( ) => saveProfitShareReceiver ( props . session , receiverForm ) ) ; receiverDialog . value = false ; receiverForm . receiverAccount = '' ; ElMessage . success ( '分账接收方已保存' ) ; await loadSnapshot ( ) ; } catch { /* displayed */ } finally { saving . value = false ; } }
async function savePolicy ( ) { if ( ! policyForm . collectionAccountId || ! policyForm . receiverId ) return ElMessage . warning ( '请选择账户和接收方' ) ; saving . value = true ; try { await capture ( ( ) => saveProfitSharePolicy ( props . session , { collectionAccountId : policyForm . collectionAccountId , storeId : policyForm . storeId || null , receiverId : policyForm . receiverId , percentageBps : Math . round ( policyForm . percentage * 100 ) , enabled : policyForm . enabled } ) ) ; policyDialog . value = false ; ElMessage . success ( '分账策略已保存' ) ; await loadSnapshot ( ) ; } catch { /* displayed */ } finally { saving . value = false ; } }
async function executeTool ( ) { saving . value = true ; lastResult . value = '' ; try { if ( toolDialog . value === 'refund' ) { if ( ! refundForm . paymentId || ! refundForm . reason || refundForm . amountYuan <= 0 ) return ElMessage . warning ( '请完整填写退款信息' ) ; const result = await capture ( ( ) => createWechatRefund ( props . session , { paymentId : refundForm . paymentId , amountCents : Math . round ( refundForm . amountYuan * 100 ) , reason : refundForm . reason , clientRequestId : requestId ( 'refund' ) } ) ) ; lastResult . value = ` 退款 ${ result . refundNo } 已提交,状态 ${ result . status } ,剩余可退 ${ money ( result . refundableCents ) } ` ; } else if ( toolDialog . value === 'reconciliation' ) { if ( ! reconciliationForm . storeId || ! reconciliationForm . billDate ) return ElMessage . warning ( '请选择门店和账单日期' ) ; const result = await capture ( ( ) => requestWechatReconciliation ( props . session , reconciliationForm ) ) ; lastResult . value = ` 对账请求 # ${ result . id } 状态 ${ result . status } ${ result . downloadUrl ? ',下载地址已由服务端返回' : '' } ` ; } else { if ( ! shareForm . paymentId ) return ElMessage . warning ( '请填写支付 ID' ) ; const result = await capture ( ( ) => executeProfitSharing ( props . session , { paymentId : shareForm . paymentId , clientRequestId : requestId ( 'share' ) , mode : shareForm . mode } ) ) ; lastResult . value = ` 分账已提交,共 ${ result . shares . length } 条 ${ result . idempotent ? '(幂等复用)' : '' } ` ; await loadSnapshot ( ) ; } toolDialog . value = '' ; } catch { /* displayed */ } finally { saving . value = false ; } }
function requestId ( prefix : string ) { return ` ${ prefix } - ${ Date . now ( ) } - ${ Math . random ( ) . toString ( 16 ) . slice ( 2 , 10 ) } ` ; }
function storeName ( id : string ) { return stores . value . find ( ( item ) => item . id === String ( id ) ) ? . name || ` 门店 # ${ id } ` ; }
function receiverName ( id : string ) { const item = snapshot . receivers . find ( ( receiver ) => receiver . id === String ( id ) ) ; return item ? ` ${ item . name } · ${ item . receiverMasked } ` : ` 接收方 # ${ id } ` ; }
function authorizationLabel ( value : PaymentAuthorizationStatus ) { return authorizationOptions . find ( ( item ) => item . value === value ) ? . label || value ; }
function authorizationType ( value : PaymentAuthorizationStatus ) { return value === 'AUTHORIZED' ? 'success' : value === 'PENDING' ? 'warning' : 'info' ; }
function shareStatusType ( value : string ) { return value === 'SUCCEEDED' ? 'success' : value === 'FAILED' ? 'danger' : 'warning' ; }
function truthy ( value : boolean | number ) { return value === true || Number ( value ) === 1 ; }
function percent ( bps : number ) { return ` ${ ( Number ( bps || 0 ) / 100 ) . toFixed ( 2 ) } % ` ; }
function formatDate ( value : string ) { const date = new Date ( value ) ; return Number . isNaN ( date . getTime ( ) ) ? value : new Intl . DateTimeFormat ( 'zh-CN' , { month : '2-digit' , day : '2-digit' , hour : '2-digit' , minute : '2-digit' , hour12 : false } ) . format ( date ) ; }
watch ( ( ) => props . session . token , ( token ) => { if ( token ) void loadWorkspace ( ) ; } , { immediate : true } ) ;
< / script >