feat(distribution): 独立分销后台账号体系、多租户隔离与主站后台分销管理GUI
- 独立账号鉴权:分销后台支持独立登录页面、Bearer+Cookie 双模鉴权与租户资料接口 - 多租户隔离:分销商成员强制锁定自身站点数据范围,防止横向越权;平台超管享有全局视界 - 一键开通分销商:平台端支持一键录入站点、管理员账号、分配角色并原子化初始化佣金规则 - 主后台分销分组:在主站后台增加「分销管理」导航分组,提供站点管理、规则配置、订单监控与结算管理 GUI - 门禁与联调:补充完整单测,全量代码门禁 check.sh 100% 通过,dev.sh 支持前后端与分销后台一键同启
This commit is contained in:
+114
-12
@@ -1,28 +1,60 @@
|
||||
import axios from 'axios'
|
||||
import { Modal } from 'antd'
|
||||
|
||||
const TOKEN_KEY = 'hfb_dist_access_token'
|
||||
|
||||
export function getStoredToken(): string | null {
|
||||
try {
|
||||
return localStorage.getItem(TOKEN_KEY)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function setStoredToken(token: string): void {
|
||||
try {
|
||||
localStorage.setItem(TOKEN_KEY, token)
|
||||
} catch {
|
||||
// ignore local storage errors
|
||||
}
|
||||
}
|
||||
|
||||
export function clearStoredToken(): void {
|
||||
try {
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
} catch {
|
||||
// ignore local storage errors
|
||||
}
|
||||
}
|
||||
|
||||
const api = axios.create({ baseURL: '/api', withCredentials: true })
|
||||
|
||||
interface ApiResponse<T> { data: T; code?: number; message?: string }
|
||||
|
||||
let authNoticeShown = false
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = getStoredToken()
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
api.interceptors.response.use(
|
||||
(resp) => resp,
|
||||
(error) => {
|
||||
// 后台鉴权依赖主站 HttpOnly cookie;401 说明未登录或会话过期。
|
||||
if (error?.response?.status === 401 && !authNoticeShown) {
|
||||
authNoticeShown = true
|
||||
Modal.warning({
|
||||
title: '登录状态失效',
|
||||
content: '请先在主站后台登录管理员账号,再返回本页面。',
|
||||
onOk: () => { authNoticeShown = false },
|
||||
})
|
||||
if (error?.response?.status === 401) {
|
||||
clearStoredToken()
|
||||
if (typeof window !== 'undefined' && !window.location.pathname.startsWith('/login')) {
|
||||
window.location.href = '/login'
|
||||
}
|
||||
}
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
data: T
|
||||
code?: string | number
|
||||
message?: string
|
||||
}
|
||||
|
||||
export function extract<T>(resp: { data: ApiResponse<T> }): T {
|
||||
return resp.data.data
|
||||
}
|
||||
@@ -31,6 +63,49 @@ export function formatMoney(cent: number | undefined | null): string {
|
||||
return `¥${((cent ?? 0) / 100).toFixed(2)}`
|
||||
}
|
||||
|
||||
export interface CurrentMember {
|
||||
admin_id: number
|
||||
username: string
|
||||
nickname: string
|
||||
is_superadmin: boolean
|
||||
site_id?: number
|
||||
site_code?: string
|
||||
site_name?: string
|
||||
domain?: string
|
||||
member_type?: string
|
||||
}
|
||||
|
||||
export interface LoginResult {
|
||||
token: string
|
||||
refresh_token?: string
|
||||
member: CurrentMember
|
||||
}
|
||||
|
||||
export async function login(username: string, password: string): Promise<LoginResult> {
|
||||
const resp = await api.post<ApiResponse<LoginResult>>('/distribution/auth/login', {
|
||||
username,
|
||||
password,
|
||||
})
|
||||
const res = extract(resp)
|
||||
if (res.token) {
|
||||
setStoredToken(res.token)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
export async function getMe(): Promise<CurrentMember> {
|
||||
const resp = await api.get<ApiResponse<CurrentMember>>('/distribution/auth/me')
|
||||
return extract(resp)
|
||||
}
|
||||
|
||||
export async function logout(): Promise<void> {
|
||||
try {
|
||||
await api.post('/distribution/auth/logout')
|
||||
} finally {
|
||||
clearStoredToken()
|
||||
}
|
||||
}
|
||||
|
||||
export interface Site {
|
||||
id: number
|
||||
site_code: string
|
||||
@@ -123,6 +198,33 @@ export interface SettlementItem {
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface OnboardSitePayload {
|
||||
site_code: string
|
||||
site_name: string
|
||||
domain: string
|
||||
admin_username: string
|
||||
admin_password: string
|
||||
admin_nickname?: string
|
||||
member_type?: string
|
||||
display_account_enabled?: boolean
|
||||
tax_enabled?: boolean
|
||||
tax_rate_bps?: number
|
||||
distributor_receive_ratio?: number
|
||||
platform_sell_ratio?: number
|
||||
service_fee_value?: number
|
||||
}
|
||||
|
||||
export interface OnboardSiteResult {
|
||||
site: Site
|
||||
rule: Rule
|
||||
member: CurrentMember
|
||||
}
|
||||
|
||||
export async function onboardSite(payload: OnboardSitePayload): Promise<OnboardSiteResult> {
|
||||
const resp = await api.post<ApiResponse<OnboardSiteResult>>('/admin/distribution/sites/onboard', payload)
|
||||
return extract(resp)
|
||||
}
|
||||
|
||||
export async function listSites(): Promise<Site[]> {
|
||||
const resp = await api.get<ApiResponse<{ items: Site[] }>>('/admin/distribution/sites')
|
||||
return extract(resp).items ?? []
|
||||
|
||||
Reference in New Issue
Block a user