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
@@ -364,6 +364,65 @@ func parseQueryUint(c *gin.Context, key string) int64 {
return int64(id)
}
func (h *Handler) Login(c *gin.Context) {
var req DistributionLoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "用户名和密码不能为空")
return
}
resp, err := h.service.Login(c.Request.Context(), req, c.ClientIP())
if err != nil {
writeError(c, err)
return
}
secure := isHTTPSRequest(c)
c.SetSameSite(http.SameSiteStrictMode)
c.SetCookie(middleware.AdminAccessCookieName, resp.Token, 2*3600, "/api", "", secure, true)
response.OK(c, resp)
}
func (h *Handler) Me(c *gin.Context) {
adminID, ok := currentAdminID(c)
if !ok {
response.Unauthorized(c, "缺少管理员上下文")
return
}
profile, err := h.service.Me(c.Request.Context(), adminID)
if err != nil {
writeError(c, err)
return
}
response.OK(c, profile)
}
func (h *Handler) Logout(c *gin.Context) {
secure := isHTTPSRequest(c)
c.SetSameSite(http.SameSiteStrictMode)
c.SetCookie(middleware.AdminAccessCookieName, "", -1, "/api", "", secure, true)
response.OK(c, gin.H{"logged_out": true})
}
func (h *Handler) OnboardSite(c *gin.Context) {
var req OnboardSiteRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "开通分销商参数格式不正确")
return
}
resp, err := h.service.OnboardSite(c.Request.Context(), req)
if err != nil {
writeError(c, err)
return
}
response.Created(c, resp)
}
func isHTTPSRequest(c *gin.Context) bool {
if c.Request.TLS != nil {
return true
}
return strings.EqualFold(c.GetHeader("X-Forwarded-Proto"), "https")
}
func currentAdminID(c *gin.Context) (uint64, bool) {
value, ok := c.Get(middleware.ContextAdminID)
if !ok {
@@ -390,6 +449,14 @@ func writeError(c *gin.Context, err error) {
response.BadRequest(c, "支付商户配置不存在")
case errors.Is(err, ErrOrderSnapshotExists):
response.Error(c, http.StatusConflict, "snapshot_exists", "该订单已存在财务快照,不能重复补数")
case errors.Is(err, ErrDuplicateSiteCodeOrDomain):
response.Error(c, http.StatusConflict, "duplicate_site", "分销站点标识或域名已被占用")
case errors.Is(err, ErrDuplicateUsername):
response.Error(c, http.StatusConflict, "duplicate_username", "管理员用户名已被占用")
case errors.Is(err, ErrInvalidCredential):
response.BadRequest(c, "用户名或密码错误")
case errors.Is(err, ErrAdminDisabled):
response.Error(c, http.StatusForbidden, "admin_disabled", "分销管理员账号已被禁用")
case errors.Is(err, ErrInvalidAmount), errors.Is(err, ErrInvalidRate), errors.Is(err, ErrInvalidServiceUnit), errors.Is(err, ErrUnsupportedMode), errors.Is(err, ErrInvalidOrderKind), errors.Is(err, ErrInvalidPeriod), errors.Is(err, ErrBatchAlreadySettled), errors.Is(err, ErrSettlementBatchEmpty), errors.Is(err, ErrSettlementTotalMismatch):
response.BadRequest(c, "分销规则参数不正确")
case errors.Is(err, ErrSiteAccessDenied):