feat(distribution): 独立分销后台账号体系、多租户隔离与主站后台分销管理GUI

- 独立账号鉴权:分销后台支持独立登录页面、Bearer+Cookie 双模鉴权与租户资料接口
- 多租户隔离:分销商成员强制锁定自身站点数据范围,防止横向越权;平台超管享有全局视界
- 一键开通分销商:平台端支持一键录入站点、管理员账号、分配角色并原子化初始化佣金规则
- 主后台分销分组:在主站后台增加「分销管理」导航分组,提供站点管理、规则配置、订单监控与结算管理 GUI
- 门禁与联调:补充完整单测,全量代码门禁 check.sh 100% 通过,dev.sh 支持前后端与分销后台一键同启
This commit is contained in:
yml2213
2026-09-07 15:45:53 +08:00
parent 0c37c7e659
commit 550b42eb22
28 changed files with 3192 additions and 117 deletions
@@ -0,0 +1,406 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { Plus, Shop, Link, DocumentCopy } from '@element-plus/icons-vue'
import {
fetchDistributionSites,
onboardDistributionSite,
type DistributionSite,
type OnboardDistributionSitePayload,
type OnboardDistributionSiteResult,
} from '@/features/admin/api/adminDistribution'
import { formatDateTime } from '@/shared/utils/time'
import { readError } from '@/shared/utils/error'
const loading = ref(false)
const sites = ref<DistributionSite[]>([])
const onboardDialogVisible = ref(false)
const submitting = ref(false)
const onboardFormRef = ref<FormInstance>()
const successDialogVisible = ref(false)
const onboardResult = ref<OnboardDistributionSiteResult | null>(null)
const onboardForm = reactive({
site_name: '',
site_code: '',
domain: '',
admin_username: '',
admin_password: '',
admin_nickname: '',
member_type: 'operator',
distributor_receive_ratio: 3.5,
platform_sell_ratio: 4.0,
service_fee_value: 2.5,
display_account_enabled: true,
tax_enabled: false,
tax_rate_bps: 100,
})
const onboardRules: FormRules = {
site_name: [{ required: true, message: '请输入分销站点名称', trigger: 'blur' }],
site_code: [
{ required: true, message: '请输入站点唯一标识', trigger: 'blur' },
{ pattern: /^[a-zA-Z0-9_-]+$/, message: '仅支持英文字母、数字及下划线', trigger: 'blur' },
],
domain: [{ required: true, message: '请输入分销专属域名', trigger: 'blur' }],
admin_username: [{ required: true, message: '请输入分销管理员账号', trigger: 'blur' }],
admin_password: [
{ required: true, message: '请输入初始密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' },
],
}
async function loadSites() {
loading.value = true
try {
sites.value = await fetchDistributionSites()
} catch (err) {
ElMessage.error(readError(err, '获取分销站点列表失败'))
} finally {
loading.value = false
}
}
function handleOpenOnboard() {
onboardForm.site_name = ''
onboardForm.site_code = ''
onboardForm.domain = ''
onboardForm.admin_username = ''
onboardForm.admin_password = ''
onboardForm.admin_nickname = ''
onboardForm.member_type = 'operator'
onboardForm.distributor_receive_ratio = 3.5
onboardForm.platform_sell_ratio = 4.0
onboardForm.service_fee_value = 2.5
onboardForm.display_account_enabled = true
onboardForm.tax_enabled = false
onboardForm.tax_rate_bps = 100
onboardDialogVisible.value = true
}
async function handleOnboardSubmit() {
if (!onboardFormRef.value) return
await onboardFormRef.value.validate(async valid => {
if (!valid) return
submitting.value = true
try {
const payload: OnboardDistributionSitePayload = {
site_name: onboardForm.site_name.trim(),
site_code: onboardForm.site_code.trim(),
domain: onboardForm.domain.trim(),
admin_username: onboardForm.admin_username.trim(),
admin_password: onboardForm.admin_password,
admin_nickname: onboardForm.admin_nickname.trim() || undefined,
member_type: onboardForm.member_type,
distributor_receive_ratio: Number(onboardForm.distributor_receive_ratio),
platform_sell_ratio: Number(onboardForm.platform_sell_ratio),
service_fee_value: Number(onboardForm.service_fee_value),
display_account_enabled: onboardForm.display_account_enabled,
tax_enabled: onboardForm.tax_enabled,
tax_rate_bps: onboardForm.tax_enabled ? onboardForm.tax_rate_bps : 0,
}
const res = await onboardDistributionSite(payload)
ElMessage.success('分销站点与管理员账号已成功开通!')
onboardDialogVisible.value = false
onboardResult.value = res
successDialogVisible.value = true
await loadSites()
} catch (err) {
ElMessage.error(readError(err, '开通分销商失败'))
} finally {
submitting.value = false
}
})
}
function handleCopyInfo() {
if (!onboardResult.value) return
const text = `分销站点:${onboardResult.value.site.site_name} (${onboardResult.value.site.site_code})\n分销域名:${onboardResult.value.site.domain}\n后台登录地址:http://${window.location.hostname}:5190/login\n管理员账号:${onboardResult.value.member.username}`
navigator.clipboard?.writeText(text)
ElMessage.success('已复制分销商开通凭证到剪贴板')
}
function openDistributionAdmin() {
window.open(`http://${window.location.hostname}:5190`, '_blank')
}
onMounted(() => {
loadSites()
})
</script>
<template>
<div class="distribution-sites-view">
<div class="page-header">
<div>
<h2 class="page-title">分销站点管理</h2>
<p class="page-subtitle">
统一管理全平台合作分销站点,支持一键开通分销商、初始化管理员账号及专属佣金费率。
</p>
</div>
<div class="header-actions">
<el-button :icon="Link" @click="openDistributionAdmin"> 打开独立分销后台 </el-button>
<el-button type="primary" :icon="Plus" @click="handleOpenOnboard">
一键开通分销商
</el-button>
</div>
</div>
<el-card shadow="never">
<el-table v-loading="loading" :data="sites" stripe style="width: 100%">
<el-table-column label="站点名称" prop="site_name" min-width="160">
<template #default="{ row }">
<div class="site-name-cell">
<el-icon class="site-icon"><Shop /></el-icon>
<strong>{{ row.site_name }}</strong>
</div>
</template>
</el-table-column>
<el-table-column label="站点标识" prop="site_code" min-width="120">
<template #default="{ row }">
<el-tag size="small">{{ row.site_code }}</el-tag>
</template>
</el-table-column>
<el-table-column label="绑定域名" prop="domain" min-width="180" />
<el-table-column label="运行状态" prop="status" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 'active' ? 'success' : 'info'" size="small">
{{ row.status === 'active' ? '正常' : '已停用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="展示主站账号" prop="display_account_enabled" width="130">
<template #default="{ row }">
<el-tag :type="row.display_account_enabled ? 'success' : 'info'" size="small">
{{ row.display_account_enabled ? '已开启' : '关闭' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="税费设定" width="120">
<template #default="{ row }">
<el-tag v-if="row.tax_enabled" type="warning" size="small">
{{ (row.tax_rate_bps / 100).toFixed(1) }}%
</el-tag>
<el-tag v-else type="info" size="small">免税</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" prop="created_at" min-width="160">
<template #default="{ row }">
{{ formatDateTime(row.created_at) }}
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 一键开通分销商对话框 -->
<el-dialog v-model="onboardDialogVisible" title="一键开通分销商" width="680px" destroy-on-close>
<el-form ref="onboardFormRef" :model="onboardForm" :rules="onboardRules" label-width="140px">
<div class="form-section-title">1. 分销站点基础信息</div>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="站点名称" prop="site_name">
<el-input v-model="onboardForm.site_name" placeholder="例如:广州游戏分销站" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="站点唯一标识" prop="site_code">
<el-input v-model="onboardForm.site_code" placeholder="例如:gz_dist" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="分销专属域名" prop="domain">
<el-input v-model="onboardForm.domain" placeholder="例如:gz.distribution.hao858.com" />
</el-form-item>
<div class="form-section-title">2. 分销商管理员账号</div>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="登录用户名" prop="admin_username">
<el-input v-model="onboardForm.admin_username" placeholder="例如:gz_admin" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="初始登录密码" prop="admin_password">
<el-input
v-model="onboardForm.admin_password"
type="password"
show-password
placeholder="至少6位"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="站长姓名/昵称" prop="admin_nickname">
<el-input v-model="onboardForm.admin_nickname" placeholder="例如:张站长" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="初始角色权限" prop="member_type">
<el-select v-model="onboardForm.member_type" style="width: 100%">
<el-option label="分销运营 (operator)" value="operator" />
<el-option label="分销财务 (finance)" value="finance" />
<el-option label="客服人员 (customer_service)" value="customer_service" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<div class="form-section-title">3. 佣金比例与费率初始化</div>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="收号分成比例 (%)">
<el-input-number
v-model="onboardForm.distributor_receive_ratio"
:min="0"
:max="100"
:step="0.1"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="平台卖号分成 (%)">
<el-input-number
v-model="onboardForm.platform_sell_ratio"
:min="0"
:max="100"
:step="0.1"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="单笔服务费 (元)">
<el-input-number
v-model="onboardForm.service_fee_value"
:min="0"
:step="0.5"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="展示主站账号">
<el-switch v-model="onboardForm.display_account_enabled" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="是否征税">
<el-switch v-model="onboardForm.tax_enabled" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="onboardDialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleOnboardSubmit">
立即开通并生成分销商
</el-button>
</template>
</el-dialog>
<!-- 开通成功凭证对话框 -->
<el-dialog v-model="successDialogVisible" title="分销商开通成功" width="560px">
<div v-if="onboardResult">
<el-alert
type="success"
:closable="false"
show-icon
title="分销站点与管理员账号已成功初始化,请妥善保管交付凭证:"
style="margin-bottom: 16px"
/>
<el-descriptions :column="1" border size="small">
<el-descriptions-item label="分销站点">
<strong>{{ onboardResult.site.site_name }}</strong> ({{ onboardResult.site.site_code }})
</el-descriptions-item>
<el-descriptions-item label="分销专属域名">
{{ onboardResult.site.domain }}
</el-descriptions-item>
<el-descriptions-item label="分销后台登录账号">
<el-tag type="success">{{ onboardResult.member.username }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="分销后台地址">
<code>http://{{ windowHost }}:5190/login</code>
</el-descriptions-item>
<el-descriptions-item label="初始规则">
收号分成: {{ onboardResult.rule.distributor_receive_ratio }}% · 卖号分成:
{{ onboardResult.rule.platform_sell_ratio }}% · 服务费: ¥{{
onboardResult.rule.service_fee_value
}}
</el-descriptions-item>
</el-descriptions>
</div>
<template #footer>
<el-button :icon="DocumentCopy" type="primary" @click="handleCopyInfo">
一键复制交付信息
</el-button>
<el-button @click="successDialogVisible = false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
export default {
data() {
return {
windowHost: typeof window !== 'undefined' ? window.location.hostname : 'localhost',
}
},
}
</script>
<style scoped>
.distribution-sites-view {
padding: 20px;
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.page-title {
font-size: 20px;
font-weight: 600;
margin: 0 0 6px 0;
color: #1f2937;
}
.page-subtitle {
font-size: 13px;
color: #6b7280;
margin: 0;
}
.header-actions {
display: flex;
gap: 12px;
}
.site-name-cell {
display: flex;
align-items: center;
gap: 8px;
}
.site-icon {
color: #409eff;
font-size: 16px;
}
.form-section-title {
font-size: 14px;
font-weight: 600;
color: #374151;
margin: 16px 0 12px 0;
padding-bottom: 6px;
border-bottom: 1px dashed #e5e7eb;
}
</style>