@@ -274,6 +284,7 @@ import CleaningFieldHandoffPanel from './components/CleaningFieldHandoffPanel.vu
import OperationsOverviewPanel from './components/OperationsOverviewPanel.vue';
import StoresRoomsPanel from './components/StoresRoomsPanel.vue';
import OrdersPanel from './components/OrdersPanel.vue';
+import PaymentsPanel from './components/PaymentsPanel.vue';
import {
ApiError,
assignCleaningTask,
@@ -310,7 +321,7 @@ import { money } from './format';
const savedToken = ref(localStorage.getItem('qipai.admin.token') || '');
const tokenDraft = ref(savedToken.value);
-const activeModule = ref<'overview' | 'stores' | 'orders' | 'cleaning'>('overview');
+const activeModule = ref<'overview' | 'stores' | 'orders' | 'payments' | 'cleaning'>('overview');
const activeTab = ref('tasks');
const lastError = ref('');
const lastMessage = ref('');
@@ -323,6 +334,7 @@ const activeModuleMeta = computed(() => ({
overview: { stage: 'M08-D', title: '平台运营总览' },
stores: { stage: 'M08-D', title: '门店与房间管理' },
orders: { stage: 'M08-D', title: '订单筛选与运营处置' },
+ payments: { stage: 'M08-D', title: '支付、退款与分账' },
cleaning: { stage: 'M08-B', title: '保洁任务与结算' }
})[activeModule.value]);
const loading = reactive({ tasks: false, settlements: false, statistics: false, cleaners: false });
diff --git a/admin/src/api.ts b/admin/src/api.ts
index bdba352..e3cd227 100644
--- a/admin/src/api.ts
+++ b/admin/src/api.ts
@@ -14,6 +14,8 @@ import type {
OrderAction,
OrderHistoryItem,
OrderStatus,
+ PaymentAuthorizationStatus,
+ ProfitSharingSnapshot,
PayoutStateFilter,
StaffRole,
SettlementStatus,
@@ -170,6 +172,63 @@ export function changeManagedOrderRoom(
});
}
+export function getProfitSharingSnapshot(session: ApiSession, storeId?: string) {
+ const query = storeId ? `?${new URLSearchParams({ storeId })}` : '';
+ return request
(session, `/pay/profit-shares${query}`);
+}
+
+export function saveCollectionAccount(session: ApiSession, input: {
+ storeId: string | null; merchantId: string; credentialRef: string;
+ authorizationStatus: PaymentAuthorizationStatus; profitSharingEnabled: boolean; enabled: boolean;
+}) {
+ return request<{ collectionAccountId: string }>(session, '/pay/collection-account', {
+ method: 'PUT', body: JSON.stringify(input)
+ });
+}
+
+export function saveProfitShareReceiver(session: ApiSession, input: {
+ collectionAccountId: string; receiverType: 'MERCHANT_ID' | 'PERSONAL_OPENID';
+ receiverAccount: string; receiverCredentialRef: string; relationType: string; name: string;
+ authorizationStatus: PaymentAuthorizationStatus; enabled: boolean;
+}) {
+ return request<{ receiverId: string }>(session, '/pay/profit-share-receiver', {
+ method: 'PUT', body: JSON.stringify(input)
+ });
+}
+
+export function saveProfitSharePolicy(session: ApiSession, input: {
+ collectionAccountId: string; storeId: string | null; receiverId: string;
+ percentageBps: number; enabled: boolean;
+}) {
+ return request<{ policyId: string }>(session, '/pay/profit-share-policy', {
+ method: 'PUT', body: JSON.stringify(input)
+ });
+}
+
+export function executeProfitSharing(
+ session: ApiSession, input: { paymentId: string; clientRequestId: string; mode: 'API' | 'MOCK' }
+) {
+ return request<{ shares: unknown[]; idempotent: boolean }>(session, '/pay/profit-shares', {
+ method: 'POST', body: JSON.stringify(input)
+ });
+}
+
+export function createWechatRefund(session: ApiSession, input: {
+ paymentId: string; amountCents: number; reason: string; clientRequestId: string;
+}) {
+ return request<{ refundId: string; refundNo: string; status: string; refundableCents: number }>(
+ session, '/pay/refund', { method: 'POST', body: JSON.stringify(input) }
+ );
+}
+
+export function requestWechatReconciliation(session: ApiSession, input: {
+ storeId: string; billDate: string; billType: 'ALL' | 'SUCCESS' | 'REFUND';
+}) {
+ return request<{ id: string; status: string; downloadUrl: string; idempotent: boolean }>(
+ session, '/pay/reconciliation', { method: 'POST', body: JSON.stringify(input) }
+ );
+}
+
export function getBusinessStatistics(
session: ApiSession,
input: { storeId: string; from: string; to: string }
diff --git a/admin/src/components/PaymentsPanel.vue b/admin/src/components/PaymentsPanel.vue
new file mode 100644
index 0000000..5c20523
--- /dev/null
+++ b/admin/src/components/PaymentsPanel.vue
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+ 收款账户{{ snapshot.accounts.length }}
+ 已授权账户{{ authorizedAccounts }}
+ 分账接收方{{ snapshot.receivers.length }}
+ 近百笔分账金额{{ money(sharedAmountCents) }}
+
+
+
+
+
+
+
+
+ {{ row.shareNo }}支付 #{{ row.paymentId }} · 订单 #{{ row.orderId }}
+ {{ row.receiverMasked }}{{ row.receiverType }} · {{ percent(row.percentageBps) }}
+ {{ money(row.amountCents) }}
+ {{ row.status }}
+ {{ storeName(row.storeId) }}{{ formatDate(row.createdAt) }}{{ row.failureCode }}
+
+
+
+
+ 凭据只保存环境引用,页面不读取或回显私钥。配置账户
+
+
+ {{ row.storeId ? storeName(row.storeId) : '租户默认' }}
+
+ {{ authorizationLabel(row.authorizationStatus) }}
+ {{ truthy(row.profitSharingEnabled) ? '分账开启' : '分账关闭' }}{{ truthy(row.enabled) ? '已启用' : '已停用' }}
+
+
+
+
+ 分账比例由服务端校验,总和不得超过 100%。新增接收方新增策略
+
+
+
+
+
+ {{ authorizationLabel(row.authorizationStatus) }}
+
+
+
+ #{{ row.collectionAccountId }}
+ {{ row.storeId ? storeName(row.storeId) : '账户默认范围' }}
+ {{ receiverName(row.receiverId) }}
+ {{ percent(row.percentageBps) }}
+
+
+
+
+
+
+
+
+
+
+
+ 取消保存账户
+
+
+
+
+ 取消保存接收方
+
+
+
+
+ 取消保存策略
+
+
+
+
+ 全部支付退款
+ 生产 API非生产 Mock
+ 取消确认执行
+
+
+
+
+
diff --git a/admin/src/styles.css b/admin/src/styles.css
index 74f0c3a..0c01041 100644
--- a/admin/src/styles.css
+++ b/admin/src/styles.css
@@ -1396,6 +1396,97 @@ textarea {
white-space: nowrap;
}
+.payments-page {
+ display: grid;
+ gap: 14px;
+}
+
+.payment-metrics {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 12px;
+}
+
+.payment-metrics span {
+ display: grid;
+ gap: 6px;
+ padding: 14px;
+ background: #fff;
+ border: 1px solid #d8e0ea;
+ border-radius: 8px;
+}
+
+.payment-metrics small {
+ color: #69788c;
+}
+
+.payment-metrics strong {
+ color: #172033;
+ font-size: 22px;
+}
+
+.payment-toolbar h3 {
+ margin: 0;
+}
+
+.payment-tabs {
+ padding: 0 14px 14px;
+}
+
+.tab-action-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ min-height: 54px;
+ color: #69788c;
+ font-size: 13px;
+}
+
+.policy-table {
+ margin-top: 16px;
+ border-top: 1px solid #e6ebf2;
+}
+
+.payment-tools-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 14px;
+ padding: 8px 0;
+}
+
+.payment-tools-grid article {
+ display: grid;
+ gap: 18px;
+ align-content: space-between;
+ min-height: 180px;
+ padding: 16px;
+ border: 1px solid #dfe6ee;
+ border-radius: 9px;
+ background: #f8fafc;
+}
+
+.payment-tools-grid header {
+ display: flex;
+ gap: 10px;
+ color: #2878ff;
+}
+
+.payment-tools-grid header div {
+ display: grid;
+ gap: 6px;
+}
+
+.payment-tools-grid header strong {
+ color: #172033;
+}
+
+.payment-tools-grid header span {
+ color: #69788c;
+ font-size: 12px;
+ line-height: 1.6;
+}
+
.el-button {
border-radius: 8px;
}
@@ -1499,6 +1590,11 @@ textarea {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
+ .payment-metrics,
+ .payment-tools-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
.trend-row {
grid-template-columns: 92px minmax(80px, 1fr);
}
@@ -1603,6 +1699,16 @@ textarea {
grid-template-columns: 1fr;
}
+ .payment-metrics,
+ .payment-tools-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .tab-action-row {
+ align-items: stretch;
+ flex-direction: column;
+ }
+
.toolbar-actions {
justify-content: flex-end;
}
diff --git a/admin/src/types.ts b/admin/src/types.ts
index c61794f..69691a5 100644
--- a/admin/src/types.ts
+++ b/admin/src/types.ts
@@ -151,6 +151,61 @@ export interface OrderHistoryItem {
createdAt: string;
}
+export type PaymentAuthorizationStatus = 'UNAUTHORIZED' | 'PENDING' | 'AUTHORIZED' | 'REVOKED';
+
+export interface CollectionAccount {
+ id: string;
+ platformAppId: string;
+ storeId: string | null;
+ provider: string;
+ merchantId: string;
+ authorizationStatus: PaymentAuthorizationStatus;
+ profitSharingEnabled: boolean | number;
+ enabled: boolean | number;
+}
+
+export interface ProfitShareReceiver {
+ id: string;
+ collectionAccountId: string;
+ receiverType: 'MERCHANT_ID' | 'PERSONAL_OPENID';
+ receiverMasked: string;
+ relationType: string;
+ name: string;
+ authorizationStatus: PaymentAuthorizationStatus;
+ enabled: boolean | number;
+}
+
+export interface ProfitSharePolicy {
+ id: string;
+ collectionAccountId: string;
+ storeId: string | null;
+ receiverId: string;
+ percentageBps: number;
+ enabled: boolean | number;
+}
+
+export interface ProfitShareRecord {
+ id: string;
+ paymentId: string;
+ orderId: string;
+ storeId: string;
+ shareNo: string;
+ receiverType: string;
+ receiverMasked: string;
+ percentageBps: number;
+ amountCents: number;
+ status: string;
+ failureCode: string;
+ createdAt: string;
+}
+
+export interface ProfitSharingSnapshot {
+ accounts: CollectionAccount[];
+ receivers: ProfitShareReceiver[];
+ policies: ProfitSharePolicy[];
+ shares: ProfitShareRecord[];
+}
+
export interface BusinessStatistics {
storeId: string;
from: string;
diff --git a/scripts/check-admin-m08-d.mjs b/scripts/check-admin-m08-d.mjs
index 54cd2cd..7073189 100644
--- a/scripts/check-admin-m08-d.mjs
+++ b/scripts/check-admin-m08-d.mjs
@@ -12,9 +12,11 @@ for (const pattern of [
"activeModule = 'cleaning'",
"activeModule = 'stores'",
"activeModule = 'orders'",
+ "activeModule = 'payments'",
'平台运营总览',
'StoresRoomsPanel',
'OrdersPanel',
+ 'PaymentsPanel',
'运营总览',
'savedToken',
'loadCleaningWorkspace'
@@ -100,6 +102,24 @@ for (const pattern of [
assert.match(orders, new RegExp(pattern));
}
+const payments = read('admin/src/components/PaymentsPanel.vue');
+for (const pattern of [
+ '支付、退款与分账',
+ 'getProfitSharingSnapshot',
+ 'saveCollectionAccount',
+ 'saveProfitShareReceiver',
+ 'saveProfitSharePolicy',
+ 'createWechatRefund',
+ 'requestWechatReconciliation',
+ 'executeProfitSharing',
+ '凭据只保存环境引用',
+ 'clientRequestId',
+ '生产 API',
+ '非生产 Mock'
+]) {
+ assert.match(payments, new RegExp(pattern));
+}
+
const routes = read('backend/src/routes/business-statistics.ts');
assert.match(routes, /'\/app-api\/management\/statistics', '\/admin-api\/statistics'/);
@@ -129,6 +149,8 @@ for (const pattern of [
'.hours-editor',
'.order-page-summary',
'.history-card',
+ '.payment-metrics',
+ '.payment-tools-grid',
'@media (max-width: 980px)',
'@media (max-width: 560px)'
]) {