feat(distribution): 分销站点消息中心与钱包提现多租户独立分支隔离
- 新增 000070 数据库迁移:支持 chat_conversations、wallet_accounts、wallet_ledger、withdrawal_requests 按 distribution_site_id 隔离 - 消息中心:会话列表、未读数统计、站点专属客服及订单群聊与主站实现双向完全隔离 - 资金账户与提现:独立按 (user_id, distribution_site_id) 建账,分销订单自动结算至对应分站钱包,提现与流水相互独立 - 前台界面:Axios 自动透传 X-Distribution-Site-Code,钱包、提现与消息页面呈现分站专属标牌与独立说明
This commit is contained in:
@@ -40,6 +40,7 @@ export interface ChatConversation {
|
||||
last_sender_role?: string
|
||||
unread_count: number
|
||||
needs_reply: boolean
|
||||
distribution_site_id?: number | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
@@ -92,17 +93,34 @@ export interface ChatMessage {
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export async function fetchChats(page = 1, pageSize = 20) {
|
||||
export async function fetchChats(page = 1, pageSize = 20, distributionSiteCode?: string) {
|
||||
const siteCode =
|
||||
distributionSiteCode ||
|
||||
(typeof window !== 'undefined' ? localStorage.getItem('hfb_site_code') : '') ||
|
||||
undefined
|
||||
const params: Record<string, unknown> = { page, page_size: pageSize }
|
||||
if (siteCode) {
|
||||
params.distribution_site_code = siteCode
|
||||
}
|
||||
const { data } = await apiClient.get<ApiResponse<PaginatedResult<ChatConversation>>>('/chats', {
|
||||
params: { page, page_size: pageSize },
|
||||
params,
|
||||
})
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function fetchUnreadChatCount() {
|
||||
export async function fetchUnreadChatCount(distributionSiteCode?: string) {
|
||||
const siteCode =
|
||||
distributionSiteCode ||
|
||||
(typeof window !== 'undefined' ? localStorage.getItem('hfb_site_code') : '') ||
|
||||
undefined
|
||||
const params: Record<string, unknown> = {}
|
||||
if (siteCode) {
|
||||
params.distribution_site_code = siteCode
|
||||
}
|
||||
const { data } = await apiClient.get<ApiResponse<{ unread_count: number }>>(
|
||||
'/chats/unread-count',
|
||||
{
|
||||
params,
|
||||
silent: true,
|
||||
}
|
||||
)
|
||||
@@ -119,10 +137,21 @@ export async function fetchOrderChat(orderId: number) {
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function ensureSupportChat(scene: SupportScene | string = 'general') {
|
||||
const { data } = await apiClient.post<ApiResponse<ChatConversation>>('/chats/support', {
|
||||
export async function ensureSupportChat(
|
||||
scene: SupportScene | string = 'general',
|
||||
distributionSiteCode?: string
|
||||
) {
|
||||
const siteCode =
|
||||
distributionSiteCode ||
|
||||
(typeof window !== 'undefined' ? localStorage.getItem('hfb_site_code') : '') ||
|
||||
undefined
|
||||
const payload: { scene: string; distribution_site_code?: string } = {
|
||||
scene: scene || 'general',
|
||||
})
|
||||
}
|
||||
if (siteCode) {
|
||||
payload.distribution_site_code = siteCode
|
||||
}
|
||||
const { data } = await apiClient.post<ApiResponse<ChatConversation>>('/chats/support', payload)
|
||||
return data.data
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,11 @@ import {
|
||||
useNotificationUnreadCount,
|
||||
} from '@/features/auth/composables/useNotificationUnreadCount'
|
||||
import { formatDateMinute, formatDateTime } from '@/shared/utils/time'
|
||||
import { useDistributionSiteStore } from '@/stores/distributionSite'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const distributionStore = useDistributionSiteStore()
|
||||
const currentUserId = Number(localStorage.getItem('user_id') || 0)
|
||||
|
||||
// ── 聊天状态 ──
|
||||
@@ -65,7 +67,11 @@ async function loadChats(isRefresh = false, showLoading = true) {
|
||||
if (loading.value) return
|
||||
if (showLoading) loading.value = true
|
||||
try {
|
||||
const res = await fetchChats(page.value, pageSize)
|
||||
const res = await fetchChats(
|
||||
page.value,
|
||||
pageSize,
|
||||
distributionStore.isDistribution ? distributionStore.siteCode : undefined
|
||||
)
|
||||
conversations.value = isRefresh ? res.items : [...conversations.value, ...res.items]
|
||||
total.value = res.total
|
||||
if (conversations.value.length < res.total && res.items.length > 0) {
|
||||
@@ -180,13 +186,30 @@ function handleTabChange(tab: string) {
|
||||
<section class="page messages-page">
|
||||
<div class="page-header-row">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Messages</p>
|
||||
<h1>消息</h1>
|
||||
<p class="eyebrow">
|
||||
Messages
|
||||
<el-tag
|
||||
v-if="distributionStore.isDistribution"
|
||||
type="warning"
|
||||
effect="light"
|
||||
size="small"
|
||||
class="distribution-site-tag"
|
||||
>
|
||||
{{ distributionStore.siteName }}专属分站
|
||||
</el-tag>
|
||||
</p>
|
||||
<h1>
|
||||
{{
|
||||
distributionStore.isDistribution ? `${distributionStore.siteName} · 消息中心` : '消息'
|
||||
}}
|
||||
</h1>
|
||||
<p>
|
||||
{{
|
||||
combinedUnread > 0
|
||||
? `${combinedUnread} 条未读`
|
||||
: '查看订单群聊、平台客服消息和系统通知。'
|
||||
distributionStore.isDistribution
|
||||
? `当前查看的是【${distributionStore.siteName}】分站专属客服会话与订单群聊。`
|
||||
: combinedUnread > 0
|
||||
? `${combinedUnread} 条未读`
|
||||
: '查看订单群聊、平台客服消息和系统通知。'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
@@ -352,6 +375,12 @@ function handleTabChange(tab: string) {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.distribution-site-tag {
|
||||
margin-left: 8px;
|
||||
vertical-align: middle;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
|
||||
@@ -5,8 +5,10 @@ import { showToast } from 'vant'
|
||||
import MobileBottomNav from '@/components/MobileBottomNav.vue'
|
||||
import { fetchChats, type ChatConversation } from '@/features/chats/api/chats'
|
||||
import { formatDateMinute } from '@/shared/utils/time'
|
||||
import { useDistributionSiteStore } from '@/stores/distributionSite'
|
||||
|
||||
const router = useRouter()
|
||||
const distributionStore = useDistributionSiteStore()
|
||||
const loading = ref(false)
|
||||
const refreshing = ref(false)
|
||||
const finished = ref(false)
|
||||
@@ -26,7 +28,11 @@ async function loadChats(isRefresh = false) {
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchChats(page.value, pageSize)
|
||||
const res = await fetchChats(
|
||||
page.value,
|
||||
pageSize,
|
||||
distributionStore.isDistribution ? distributionStore.siteCode : undefined
|
||||
)
|
||||
conversations.value = isRefresh ? res.items : [...conversations.value, ...res.items]
|
||||
total.value = res.total
|
||||
if (conversations.value.length >= res.total || res.items.length === 0) {
|
||||
@@ -85,7 +91,9 @@ const unreadTotal = computed(() =>
|
||||
<button class="back-btn" type="button" @click="router.back()">
|
||||
<van-icon name="arrow-left" :size="20" />
|
||||
</button>
|
||||
<h1>消息</h1>
|
||||
<h1>
|
||||
{{ distributionStore.isDistribution ? `${distributionStore.siteName} · 消息` : '消息' }}
|
||||
</h1>
|
||||
<span class="header-count">{{ unreadTotal > 0 ? `${unreadTotal} 未读` : '' }}</span>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface WalletAccount {
|
||||
available_balance_cent: number
|
||||
frozen_balance_cent: number
|
||||
status: WalletStatus
|
||||
distribution_site_id?: number
|
||||
}
|
||||
|
||||
export interface WalletLedger {
|
||||
@@ -22,19 +23,36 @@ export interface WalletLedger {
|
||||
biz_type: string
|
||||
biz_no: string
|
||||
remark: string
|
||||
distribution_site_id?: number
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export async function fetchWalletBalance() {
|
||||
const { data } = await apiClient.get<ApiResponse<WalletAccount>>('/wallet/balance')
|
||||
export async function fetchWalletBalance(distributionSiteCode?: string) {
|
||||
const siteCode =
|
||||
distributionSiteCode ||
|
||||
(typeof window !== 'undefined' ? localStorage.getItem('hfb_site_code') : '') ||
|
||||
undefined
|
||||
const params: Record<string, unknown> = {}
|
||||
if (siteCode) {
|
||||
params.distribution_site_code = siteCode
|
||||
}
|
||||
const { data } = await apiClient.get<ApiResponse<WalletAccount>>('/wallet/balance', { params })
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function fetchWalletLedger(page = 1, pageSize = 20) {
|
||||
export async function fetchWalletLedger(page = 1, pageSize = 20, distributionSiteCode?: string) {
|
||||
const siteCode =
|
||||
distributionSiteCode ||
|
||||
(typeof window !== 'undefined' ? localStorage.getItem('hfb_site_code') : '') ||
|
||||
undefined
|
||||
const params: Record<string, unknown> = { page, page_size: pageSize }
|
||||
if (siteCode) {
|
||||
params.distribution_site_code = siteCode
|
||||
}
|
||||
const { data } = await apiClient.get<ApiResponse<PaginatedResult<WalletLedger>>>(
|
||||
'/wallet/ledger',
|
||||
{
|
||||
params: { page, page_size: pageSize },
|
||||
params,
|
||||
}
|
||||
)
|
||||
return data.data
|
||||
|
||||
@@ -55,6 +55,7 @@ export interface WithdrawalRequest {
|
||||
bank_name: string
|
||||
status: WithdrawalStatus
|
||||
review_remark: string
|
||||
distribution_site_id?: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
reviewed_at: string | null
|
||||
@@ -65,6 +66,7 @@ export interface WithdrawalRequest {
|
||||
export interface CreateWithdrawalRequest {
|
||||
payment_account_id: number
|
||||
amount_cent: number
|
||||
distribution_site_code?: string
|
||||
}
|
||||
|
||||
// ========== 收款账号管理 API ==========
|
||||
@@ -110,16 +112,36 @@ export async function setDefaultPaymentAccount(id: number) {
|
||||
|
||||
// ========== 提现申请 API ==========
|
||||
|
||||
export async function createWithdrawal(req: CreateWithdrawalRequest) {
|
||||
const { data } = await apiClient.post<ApiResponse<WithdrawalRequest>>('/withdrawals', req)
|
||||
export async function createWithdrawal(
|
||||
req: CreateWithdrawalRequest,
|
||||
distributionSiteCode?: string
|
||||
) {
|
||||
const siteCode =
|
||||
distributionSiteCode ||
|
||||
req.distribution_site_code ||
|
||||
(typeof window !== 'undefined' ? localStorage.getItem('hfb_site_code') : '') ||
|
||||
undefined
|
||||
const payload: CreateWithdrawalRequest = { ...req }
|
||||
if (siteCode) {
|
||||
payload.distribution_site_code = siteCode
|
||||
}
|
||||
const { data } = await apiClient.post<ApiResponse<WithdrawalRequest>>('/withdrawals', payload)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function fetchWithdrawals(page = 1, pageSize = 20) {
|
||||
export async function fetchWithdrawals(page = 1, pageSize = 20, distributionSiteCode?: string) {
|
||||
const siteCode =
|
||||
distributionSiteCode ||
|
||||
(typeof window !== 'undefined' ? localStorage.getItem('hfb_site_code') : '') ||
|
||||
undefined
|
||||
const params: Record<string, unknown> = { page, page_size: pageSize }
|
||||
if (siteCode) {
|
||||
params.distribution_site_code = siteCode
|
||||
}
|
||||
const { data } = await apiClient.get<ApiResponse<PaginatedResult<WithdrawalRequest>>>(
|
||||
'/withdrawals',
|
||||
{
|
||||
params: { page, page_size: pageSize },
|
||||
params,
|
||||
}
|
||||
)
|
||||
return data.data
|
||||
|
||||
@@ -52,6 +52,19 @@
|
||||
box-shadow: 0 14px 28px rgba(255, 106, 0, 0.22);
|
||||
}
|
||||
|
||||
.site-account-tip {
|
||||
display: inline-block;
|
||||
align-self: start;
|
||||
width: fit-content;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.balance-panel span,
|
||||
.balance-panel small {
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
|
||||
@@ -23,8 +23,10 @@ import { formatCent, formatMoney, yuanToCent } from '@/shared/utils/money'
|
||||
import { formatDateMinute } from '@/shared/utils/time'
|
||||
import alipayReceiveQrExample from '@/assets/alipay-receive-qr-example.png'
|
||||
import wechatReceiveQrExample from '@/assets/wechat-receive-qr-example.png'
|
||||
import { useDistributionSiteStore } from '@/stores/distributionSite'
|
||||
|
||||
const router = useRouter()
|
||||
const distributionStore = useDistributionSiteStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
@@ -104,10 +106,11 @@ onMounted(loadData)
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const siteCode = distributionStore.isDistribution ? distributionStore.siteCode : undefined
|
||||
const [balanceData, accountsData, withdrawalsData] = await Promise.all([
|
||||
fetchWalletBalance(),
|
||||
fetchWalletBalance(siteCode),
|
||||
fetchPaymentAccounts(1, 100),
|
||||
fetchWithdrawals(1, 20),
|
||||
fetchWithdrawals(1, 20, siteCode),
|
||||
])
|
||||
account.value = balanceData
|
||||
paymentAccounts.value = accountsData.items
|
||||
@@ -142,10 +145,13 @@ async function handleSubmit() {
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
await createWithdrawal({
|
||||
payment_account_id: withdrawForm.payment_account_id,
|
||||
amount_cent: amountCent.value,
|
||||
})
|
||||
await createWithdrawal(
|
||||
{
|
||||
payment_account_id: withdrawForm.payment_account_id,
|
||||
amount_cent: amountCent.value,
|
||||
},
|
||||
distributionStore.isDistribution ? distributionStore.siteCode : undefined
|
||||
)
|
||||
showToast({ message: '提现申请已提交', icon: 'passed' })
|
||||
withdrawForm.amount = ''
|
||||
await loadData()
|
||||
@@ -386,13 +392,18 @@ function statusType(status: string): 'primary' | 'success' | 'danger' | 'warning
|
||||
<button class="back-btn" type="button" @click="router.back()">
|
||||
<van-icon name="arrow-left" :size="20" />
|
||||
</button>
|
||||
<h1>提现</h1>
|
||||
<h1>
|
||||
{{ distributionStore.isDistribution ? `${distributionStore.siteName} · 提现` : '提现' }}
|
||||
</h1>
|
||||
<button class="refresh-btn" type="button" @click="loadData">
|
||||
<van-icon name="replay" :size="18" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<section class="balance-panel">
|
||||
<div v-if="distributionStore.isDistribution" class="site-account-tip">
|
||||
{{ distributionStore.siteName }}独立账户
|
||||
</div>
|
||||
<span>可用余额</span>
|
||||
<strong>¥{{ formatCent(availableBalanceCent) }}</strong>
|
||||
<small>冻结余额 ¥{{ formatCent(account?.frozen_balance_cent) }}</small>
|
||||
|
||||
@@ -23,8 +23,10 @@ import {
|
||||
} from '@/shared/utils/statusLabels'
|
||||
import { formatDateTime } from '@/shared/utils/time'
|
||||
import { formatCentWithSymbol } from '@/shared/utils/money'
|
||||
import { useDistributionSiteStore } from '@/stores/distributionSite'
|
||||
|
||||
const router = useRouter()
|
||||
const distributionStore = useDistributionSiteStore()
|
||||
const loading = ref(false)
|
||||
const account = ref<WalletAccount | null>(null)
|
||||
const ledger = ref<WalletLedger[]>([])
|
||||
@@ -66,9 +68,10 @@ onMounted(loadWallet)
|
||||
async function loadWallet() {
|
||||
loading.value = true
|
||||
try {
|
||||
const siteCode = distributionStore.isDistribution ? distributionStore.siteCode : undefined
|
||||
const [balance, result] = await Promise.all([
|
||||
fetchWalletBalance(),
|
||||
fetchWalletLedger(currentPage.value, currentPageSize.value),
|
||||
fetchWalletBalance(siteCode),
|
||||
fetchWalletLedger(currentPage.value, currentPageSize.value, siteCode),
|
||||
])
|
||||
account.value = balance
|
||||
ledger.value = result.items
|
||||
@@ -138,9 +141,32 @@ function amountPrefix(direction: string) {
|
||||
<section class="page wallet-page" v-loading="loading">
|
||||
<div class="wallet-hero">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">我的钱包</p>
|
||||
<h1>资金账户</h1>
|
||||
<p>查看卖家结算收入、可提现余额和每一笔资金变化。</p>
|
||||
<p class="eyebrow">
|
||||
我的钱包
|
||||
<el-tag
|
||||
v-if="distributionStore.isDistribution"
|
||||
type="warning"
|
||||
effect="light"
|
||||
size="small"
|
||||
class="distribution-site-tag"
|
||||
>
|
||||
{{ distributionStore.siteName }}专属分站
|
||||
</el-tag>
|
||||
</p>
|
||||
<h1>
|
||||
{{
|
||||
distributionStore.isDistribution
|
||||
? `${distributionStore.siteName} · 资金账户`
|
||||
: '资金账户'
|
||||
}}
|
||||
</h1>
|
||||
<p>
|
||||
{{
|
||||
distributionStore.isDistribution
|
||||
? `当前查看的是【${distributionStore.siteName}】专属分站钱包与流水明细,资金及流水与主站完全独立。`
|
||||
: '查看卖家结算收入、可提现余额和每一笔资金变化。'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="wallet-hero-action">
|
||||
<span>当前可用</span>
|
||||
@@ -258,6 +284,12 @@ function amountPrefix(direction: string) {
|
||||
max-width: 780px;
|
||||
}
|
||||
|
||||
.distribution-site-tag {
|
||||
margin-left: 8px;
|
||||
vertical-align: middle;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.wallet-hero-action {
|
||||
min-width: 220px;
|
||||
padding: 16px 18px;
|
||||
|
||||
@@ -14,8 +14,10 @@ import {
|
||||
} from '../api/withdrawal'
|
||||
import { fetchWalletBalance, type WalletAccount } from '../api/wallet'
|
||||
import { formatMoney, formatMoneyWithSymbol, formatCent, yuanToCent } from '@/shared/utils/money'
|
||||
import { useDistributionSiteStore } from '@/stores/distributionSite'
|
||||
|
||||
const router = useRouter()
|
||||
const distributionStore = useDistributionSiteStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
@@ -42,10 +44,11 @@ onMounted(() => {
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const siteCode = distributionStore.isDistribution ? distributionStore.siteCode : undefined
|
||||
const [balanceData, accountsData, withdrawalsData] = await Promise.all([
|
||||
fetchWalletBalance(),
|
||||
fetchWalletBalance(siteCode),
|
||||
fetchPaymentAccounts(1, 100),
|
||||
fetchWithdrawals(currentPage.value, pageSize.value),
|
||||
fetchWithdrawals(currentPage.value, pageSize.value, siteCode),
|
||||
])
|
||||
account.value = balanceData
|
||||
paymentAccounts.value = accountsData.items
|
||||
@@ -95,10 +98,13 @@ async function handleSubmit() {
|
||||
)
|
||||
|
||||
submitting.value = true
|
||||
await createWithdrawal({
|
||||
payment_account_id: withdrawForm.value.payment_account_id,
|
||||
amount_cent: yuanToCent(withdrawForm.value.amount),
|
||||
})
|
||||
await createWithdrawal(
|
||||
{
|
||||
payment_account_id: withdrawForm.value.payment_account_id,
|
||||
amount_cent: yuanToCent(withdrawForm.value.amount),
|
||||
},
|
||||
distributionStore.isDistribution ? distributionStore.siteCode : undefined
|
||||
)
|
||||
ElMessage.success('提现申请已提交')
|
||||
withdrawForm.value.amount = 0
|
||||
await loadData()
|
||||
@@ -167,8 +173,25 @@ function accountTypeLabel(type: string) {
|
||||
<template>
|
||||
<div class="withdrawal-page">
|
||||
<div class="page-header">
|
||||
<h2>提现</h2>
|
||||
<p class="page-desc">将钱包余额提现到您的收款账号</p>
|
||||
<h2>
|
||||
提现
|
||||
<el-tag
|
||||
v-if="distributionStore.isDistribution"
|
||||
type="warning"
|
||||
effect="light"
|
||||
size="small"
|
||||
class="distribution-site-tag"
|
||||
>
|
||||
{{ distributionStore.siteName }}专属分站
|
||||
</el-tag>
|
||||
</h2>
|
||||
<p class="page-desc">
|
||||
{{
|
||||
distributionStore.isDistribution
|
||||
? `将【${distributionStore.siteName}】专属分站钱包余额提现到您的收款账号`
|
||||
: '将钱包余额提现到您的收款账号'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 余额卡片 -->
|
||||
@@ -376,6 +399,12 @@ function accountTypeLabel(type: string) {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.distribution-site-tag {
|
||||
margin-left: 8px;
|
||||
vertical-align: middle;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 24px;
|
||||
|
||||
Reference in New Issue
Block a user