Files
hfb_sys/backend/internal/modules/distribution/handler.go
T
yml2213 2a76d59a30 feat(distribution): 公告中心支持分站自定义
公告中心(前台公告列表/详情)此前完全没有站点概念,分站访客只能看到主站公告。
本次按「默认继承主站、站长可切仅本站」的口径支持分站自定义。

数据与口径:
- 新增 announcements.distribution_site_id(0=主站),前台按站点隔离
- 新增 distribution_sites.announcement_mode:inherit(默认,主站+本站)/ own_only(仅本站)

后端:
- 公告模块新增 ResolvePublicSiteScope:主站 [0]、inherit [0, siteID]、own_only [siteID],
  站点编码无效/已停用回落主站;列表与详情都按该范围过滤,详情不再允许跨站按 ID 读取
- 管理端公告列表支持按站点筛选(site_id:不传=全部,0=主站,>0=分站)并带出站点名
- 分销模块新增本站公告 CRUD(distribution/announcement.go):强制 distribution_site_id=本站,
  先过 ensureSiteAccess,跨站更新/删除/上下架一律 ErrAnnouncementNotFound
- UpdateSiteBranding 支持 announcement_mode 切换(非法值报错,读侧非法历史值回落 inherit)
- 新增权限 distribution:announcement_view|manage 并授予 super_admin/ops/finance,
  站长开通时继承 ops 角色即自动获得
- 路由:站长侧 /api/distribution/announcements*,平台侧 /api/admin/distribution/announcements*

前端:
- 分销后台新增「公告中心」页:来源开关(继承主站+本站 / 仅本站)、本站公告列表与
  新建/编辑/发布/下架/归档/恢复/删除,空态按来源模式给出引导
- 平台后台公告页新增「站点」列与筛选,新建公告可选择归属站点(主站或指定分站代发)
- 前台 PC/移动无需改动:请求已带 X-Distribution-Site-Code,后端按站点隔离,页面已有空态

测试:
- 公告模块:主站/继承/仅本站三种可见范围、跨站详情拦截、无效站点回落、管理端站点筛选
- 分销模块:本站公告 CRUD 与跨站防护、公告来源开关校验与非法值容错
2026-10-01 10:02:31 +08:00

1065 lines
28 KiB
Go

package distribution
import (
"errors"
"net/http"
"strconv"
"strings"
"hfb_sys/backend/internal/middleware"
"hfb_sys/backend/internal/sitecode"
"hfb_sys/backend/pkg/response"
"github.com/gin-gonic/gin"
)
type Handler struct{ service *Service }
func NewHandler(service *Service) *Handler { return &Handler{service: service} }
func (h *Handler) ListSites(c *gin.Context) {
adminID, _ := currentAdminID(c)
items, err := h.service.ListSitesForAdmin(c.Request.Context(), adminID)
if err != nil {
writeError(c, err)
return
}
response.OK(c, gin.H{"items": items})
}
func (h *Handler) GetSite(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
adminID, _ := currentAdminID(c)
item, err := h.service.FindSite(c.Request.Context(), adminID, id)
if err != nil {
writeError(c, err)
return
}
response.OK(c, item)
}
func (h *Handler) CreateSite(c *gin.Context) {
var req CreateSiteRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "分销网站参数不正确")
return
}
item, err := h.service.CreateSite(c.Request.Context(), req)
if err != nil {
writeError(c, err)
return
}
response.Created(c, item)
}
func (h *Handler) GetSiteDefaults(c *gin.Context) {
holdDays, err := h.service.DefaultSettlementHoldDays(c.Request.Context())
if err != nil {
writeError(c, err)
return
}
response.OK(c, gin.H{"default_settlement_hold_days": holdDays})
}
func (h *Handler) UpdateSite(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
var req UpdateSiteRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "分销网站参数不正确")
return
}
adminID, _ := currentAdminID(c)
item, err := h.service.UpdateSite(c.Request.Context(), adminID, id, req)
if err != nil {
writeError(c, err)
return
}
response.OK(c, item)
}
func (h *Handler) DeleteSite(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
adminID, _ := currentAdminID(c)
if err := h.service.DeleteSite(c.Request.Context(), adminID, id); err != nil {
writeError(c, err)
return
}
response.OK(c, gin.H{"deleted": true})
}
func (h *Handler) ResetSiteAdminPassword(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
var req ResetSiteAdminPasswordRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "密码格式不正确(至少6位)")
return
}
adminID, _ := currentAdminID(c)
if err := h.service.ResetSiteAdminPassword(c.Request.Context(), adminID, id, req.NewPassword); err != nil {
writeError(c, err)
return
}
response.OK(c, gin.H{"message": "分销商密码重置成功"})
}
func (h *Handler) GetSiteSMSConfig(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
adminID, _ := currentAdminID(c)
item, err := h.service.GetSiteSMSConfig(c.Request.Context(), adminID, id)
if err != nil {
writeError(c, err)
return
}
response.OK(c, item)
}
func (h *Handler) UpdateSiteSMSConfig(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
var req UpdateSiteSMSConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "短信配置参数不正确")
return
}
adminID, _ := currentAdminID(c)
item, err := h.service.UpdateSiteSMSConfig(c.Request.Context(), adminID, id, req)
if err != nil {
writeError(c, err)
return
}
response.OK(c, item)
}
func (h *Handler) TestSiteSMSConfig(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
var req TestSiteSMSConfigRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "测试参数不正确,手机号必填")
return
}
adminID, _ := currentAdminID(c)
if err := h.service.TestSiteSMSConfig(c.Request.Context(), adminID, id, req); err != nil {
writeError(c, err)
return
}
response.OK(c, gin.H{"message": "测试短信发送成功"})
}
func (h *Handler) ListRules(c *gin.Context) {
var siteID *uint64
if raw := strings.TrimSpace(c.Query("site_id")); raw != "" {
id, err := strconv.ParseUint(raw, 10, 64)
if err != nil || id == 0 {
response.BadRequest(c, "site_id 不正确")
return
}
siteID = &id
}
adminID, _ := currentAdminID(c)
items, err := h.service.ListRules(c.Request.Context(), adminID, siteID, c.Query("mode"))
if err != nil {
writeError(c, err)
return
}
response.OK(c, gin.H{"items": items})
}
func (h *Handler) CreateRule(c *gin.Context) {
adminID, ok := currentAdminID(c)
if !ok {
response.Unauthorized(c, "缺少管理员上下文")
return
}
var req CreateRuleRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "分销规则参数不正确")
return
}
item, err := h.service.CreateRule(c.Request.Context(), adminID, req)
if err != nil {
writeError(c, err)
return
}
response.Created(c, item)
}
func (h *Handler) UpdateRuleStatus(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
var req UpdateRuleStatusRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "规则状态不正确")
return
}
adminID, _ := currentAdminID(c)
item, err := h.service.UpdateRuleStatus(c.Request.Context(), adminID, id, req.Status)
if err != nil {
writeError(c, err)
return
}
response.OK(c, item)
}
func (h *Handler) ListOrders(c *gin.Context) {
siteID := parseQueryUint(c, "site_id")
if siteID < 0 {
return
}
adminID, _ := currentAdminID(c)
scoped, err := h.service.ScopeSiteID(c.Request.Context(), adminID, uint64(siteID))
if err != nil {
writeError(c, err)
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
result, err := h.service.ListOrders(c.Request.Context(), DistributionOrderListQuery{
SiteID: scoped,
Kind: c.Query("kind"),
Role: c.Query("role"),
Status: c.Query("status"),
Mode: c.Query("mode"),
FinancialStatus: c.Query("financial_status"),
Keyword: c.Query("keyword"),
Page: page,
Size: size,
})
if err != nil {
writeError(c, err)
return
}
response.OK(c, result)
}
func (h *Handler) FindOrder(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
adminID, _ := currentAdminID(c)
item, err := h.service.FindOrder(c.Request.Context(), adminID, id)
if err != nil {
writeError(c, err)
return
}
response.OK(c, item)
}
func (h *Handler) CreateFinancialSnapshot(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
var req CreateFinancialSnapshotRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "分销财务快照参数不正确")
return
}
adminID, _ := currentAdminID(c)
item, err := h.service.CreateFinancialSnapshot(c.Request.Context(), adminID, id, req)
if err != nil {
writeError(c, err)
return
}
response.Created(c, item)
}
func (h *Handler) ListAccountDisplays(c *gin.Context) {
siteID := parseQueryUint(c, "site_id")
if siteID < 0 {
return
}
adminID, _ := currentAdminID(c)
scoped, err := h.service.ScopeSiteID(c.Request.Context(), adminID, uint64(siteID))
if err != nil {
writeError(c, err)
return
}
if scoped == 0 && siteID == 0 {
response.BadRequest(c, "site_id 不正确")
return
}
items, err := h.service.ListAccountDisplays(c.Request.Context(), scoped, c.Query("status"))
if err != nil {
writeError(c, err)
return
}
response.OK(c, gin.H{"items": items})
}
func (h *Handler) UpdateAccountDisplay(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
var req UpdateAccountDisplayRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "账号展示状态不正确")
return
}
adminID, _ := currentAdminID(c)
item, err := h.service.UpdateAccountDisplay(c.Request.Context(), adminID, id, req.DisplayStatus)
if err != nil {
writeError(c, err)
return
}
response.OK(c, item)
}
func (h *Handler) SyncSiteAccounts(c *gin.Context) {
var siteID uint64
id, ok := parseID(c)
if ok && id > 0 {
siteID = id
} else {
qID := parseQueryUint(c, "site_id")
if qID > 0 {
siteID = uint64(qID)
}
}
var req SyncSiteAccountsRequest
if c.Request.ContentLength != 0 {
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "同步参数不正确")
return
}
}
adminID, _ := currentAdminID(c)
scoped, err := h.service.ScopeSiteID(c.Request.Context(), adminID, siteID)
if err != nil {
writeError(c, err)
return
}
if scoped == 0 {
response.BadRequest(c, "site_id 不正确")
return
}
result, err := h.service.SyncSiteAccounts(c.Request.Context(), adminID, scoped, req)
if err != nil {
writeError(c, err)
return
}
response.OK(c, result)
}
func (h *Handler) ListPublicSiteListings(c *gin.Context) {
siteCode := strings.TrimSpace(c.Param("site_code"))
if siteCode == "" {
siteCode = strings.TrimSpace(c.Param("id"))
}
if siteCode == "" {
siteCode = strings.TrimSpace(c.Query("site_code"))
}
if siteCode == "" {
response.BadRequest(c, "site_code 不能为空")
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
result, err := h.service.ListPublicSiteListings(c.Request.Context(), siteCode, page, pageSize)
if err != nil {
writeError(c, err)
return
}
response.OK(c, result)
}
func (h *Handler) GetPublicSiteInfo(c *gin.Context) {
code := sitecode.FromQueryThenHeader(c)
domain := strings.TrimSpace(c.Query("domain"))
if code == "" && domain == "" {
host := sitecode.ExtractHost(c.Request.Host)
if host != "" && !sitecode.IsLocalHost(host) {
domain = host
}
}
info, err := h.service.GetPublicSiteInfo(c.Request.Context(), code, domain)
if err != nil {
writeError(c, err)
return
}
response.OK(c, info)
}
// CheckDomain 供网关(如 Caddy on_demand_tls ask)在按需申请 SSL 证书时校验域名白名单。
// 仅当域名属于系统中正常运行(active)的分销站点时返回 200,其余情况返回 404,
// 防止任意外部未授权域名解析到该服务器刷爆 Let's Encrypt 证书申请配额。
func (h *Handler) CheckDomain(c *gin.Context) {
domain := sitecode.ExtractHost(c.Query("domain"))
if domain == "" {
c.String(http.StatusBadRequest, "domain required")
return
}
info, err := h.service.GetPublicSiteInfo(c.Request.Context(), "", domain)
if err != nil || info == nil || info.Status != "active" {
c.String(http.StatusNotFound, "domain not allowed")
return
}
c.String(http.StatusOK, "ok")
}
func (h *Handler) ListPaymentChannels(c *gin.Context) {
siteID := parseQueryUint(c, "site_id")
if siteID <= 0 {
response.BadRequest(c, "site_id 不正确")
return
}
adminID, _ := currentAdminID(c)
scoped, err := h.service.ScopeSiteID(c.Request.Context(), adminID, uint64(siteID))
if err != nil {
writeError(c, err)
return
}
items, err := h.service.ListPaymentChannels(c.Request.Context(), scoped)
if err != nil {
writeError(c, err)
return
}
response.OK(c, gin.H{"items": items})
}
func (h *Handler) BindPaymentChannel(c *gin.Context) {
siteID := parseQueryUint(c, "site_id")
if siteID <= 0 {
response.BadRequest(c, "site_id 不正确")
return
}
adminID, _ := currentAdminID(c)
scoped, err := h.service.ScopeSiteID(c.Request.Context(), adminID, uint64(siteID))
if err != nil {
writeError(c, err)
return
}
var req BindPaymentChannelRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "支付通道参数不正确")
return
}
item, err := h.service.BindPaymentChannel(c.Request.Context(), scoped, req)
if err != nil {
writeError(c, err)
return
}
response.Created(c, item)
}
func (h *Handler) UnbindPaymentChannel(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
adminID, _ := currentAdminID(c)
if err := h.service.UnbindPaymentChannel(c.Request.Context(), adminID, id); err != nil {
writeError(c, err)
return
}
response.OK(c, gin.H{"deleted": true})
}
func (h *Handler) ListSettlementBatches(c *gin.Context) {
siteID := parseQueryUint(c, "site_id")
if siteID <= 0 {
response.BadRequest(c, "site_id 不正确")
return
}
adminID, _ := currentAdminID(c)
scoped, err := h.service.ScopeSiteID(c.Request.Context(), adminID, uint64(siteID))
if err != nil {
writeError(c, err)
return
}
items, err := h.service.ListSettlementBatches(c.Request.Context(), scoped)
if err != nil {
writeError(c, err)
return
}
response.OK(c, gin.H{"items": items})
}
func (h *Handler) CreateSettlementBatch(c *gin.Context) {
adminID, ok := currentAdminID(c)
if !ok {
response.Unauthorized(c, "缺少管理员上下文")
return
}
var req CreateSettlementBatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "结算周期参数不正确")
return
}
item, err := h.service.CreateSettlementBatch(c.Request.Context(), adminID, req)
if err != nil {
writeError(c, err)
return
}
response.Created(c, item)
}
func (h *Handler) ListSettlementItems(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
adminID, _ := currentAdminID(c)
items, err := h.service.ListSettlementItems(c.Request.Context(), adminID, id)
if err != nil {
writeError(c, err)
return
}
response.OK(c, gin.H{"items": items})
}
func (h *Handler) MarkSettlementBatch(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
adminID, _ := currentAdminID(c)
item, err := h.service.MarkSettlementBatch(c.Request.Context(), adminID, id)
if err != nil {
writeError(c, err)
return
}
response.OK(c, item)
}
func (h *Handler) CancelSettlementBatch(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
adminID, _ := currentAdminID(c)
item, err := h.service.CancelSettlementBatch(c.Request.Context(), adminID, id)
if err != nil {
writeError(c, err)
return
}
response.OK(c, item)
}
// parseQueryUint 返回 -1 表示格式错误,0 表示未传入。
func parseQueryUint(c *gin.Context, key string) int64 {
raw := strings.TrimSpace(c.Query(key))
if raw == "" {
return 0
}
id, err := strconv.ParseUint(raw, 10, 64)
if err != nil || id == 0 {
response.BadRequest(c, key+" 不正确")
return -1
}
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 (h *Handler) resolveSiteID(c *gin.Context) (uint64, error) {
adminID, _ := currentAdminID(c)
var requested uint64
if raw := c.Param("id"); raw != "" {
if id, err := strconv.ParseUint(raw, 10, 64); err == nil && id > 0 {
requested = id
}
}
if requested == 0 {
if q := parseQueryUint(c, "site_id"); q > 0 {
requested = uint64(q)
}
}
return h.service.ScopeSiteID(c.Request.Context(), adminID, requested)
}
// 分站公告中心(站长自助维护本站公告)。站点由 ?site_id= 解析并校验访问权。
func (h *Handler) ListSiteAnnouncements(c *gin.Context) {
siteID, adminID, ok := h.resolveAnnouncementSite(c)
if !ok {
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
resp, err := h.service.ListSiteAnnouncements(c.Request.Context(), adminID, siteID, c.Query("status"), page, pageSize)
if err != nil {
writeError(c, err)
return
}
response.OK(c, resp)
}
func (h *Handler) CreateSiteAnnouncement(c *gin.Context) {
siteID, adminID, ok := h.resolveAnnouncementSite(c)
if !ok {
return
}
var req CreateSiteAnnouncementRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "公告参数格式不正确")
return
}
dto, err := h.service.CreateSiteAnnouncement(c.Request.Context(), adminID, siteID, req)
if err != nil {
writeError(c, err)
return
}
response.Created(c, dto)
}
func (h *Handler) UpdateSiteAnnouncement(c *gin.Context) {
siteID, adminID, ok := h.resolveAnnouncementSite(c)
if !ok {
return
}
id, ok := parseSiteAnnouncementID(c)
if !ok {
return
}
var req UpdateSiteAnnouncementRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "公告参数格式不正确")
return
}
dto, err := h.service.UpdateSiteAnnouncement(c.Request.Context(), adminID, siteID, id, req)
if err != nil {
writeError(c, err)
return
}
response.OK(c, dto)
}
func (h *Handler) PublishSiteAnnouncement(c *gin.Context) {
h.updateSiteAnnouncementStatus(c, "publish")
}
func (h *Handler) ArchiveSiteAnnouncement(c *gin.Context) {
h.updateSiteAnnouncementStatus(c, "archive")
}
func (h *Handler) UnarchiveSiteAnnouncement(c *gin.Context) {
h.updateSiteAnnouncementStatus(c, "unarchive")
}
func (h *Handler) updateSiteAnnouncementStatus(c *gin.Context, action string) {
siteID, adminID, ok := h.resolveAnnouncementSite(c)
if !ok {
return
}
id, ok := parseSiteAnnouncementID(c)
if !ok {
return
}
dto, err := h.service.UpdateSiteAnnouncementStatus(c.Request.Context(), adminID, siteID, id, action)
if err != nil {
writeError(c, err)
return
}
response.OK(c, dto)
}
func (h *Handler) DeleteSiteAnnouncement(c *gin.Context) {
siteID, adminID, ok := h.resolveAnnouncementSite(c)
if !ok {
return
}
id, ok := parseSiteAnnouncementID(c)
if !ok {
return
}
if err := h.service.DeleteSiteAnnouncement(c.Request.Context(), adminID, siteID, id); err != nil {
writeError(c, err)
return
}
response.OK(c, gin.H{"deleted": true})
}
// resolveAnnouncementSite 解析公告所属站点:公告路由的 :id 是公告 ID,
// 因此站点只能来自 ?site_id=(不能复用会优先读 :id 的 resolveSiteID)。
func (h *Handler) resolveAnnouncementSite(c *gin.Context) (uint64, uint64, bool) {
adminID, _ := currentAdminID(c)
requested := uint64(parseQueryUint(c, "site_id"))
siteID, err := h.service.ScopeSiteID(c.Request.Context(), adminID, requested)
if err != nil {
writeError(c, err)
return 0, 0, false
}
if siteID == 0 {
response.BadRequest(c, "未指定分销站点")
return 0, 0, false
}
return siteID, adminID, true
}
func parseSiteAnnouncementID(c *gin.Context) (uint64, bool) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil || id == 0 {
response.BadRequest(c, "公告ID不正确")
return 0, false
}
return id, true
}
func (h *Handler) UpdateSiteBranding(c *gin.Context) {
siteID, err := h.resolveSiteID(c)
if err != nil {
writeError(c, err)
return
}
if siteID == 0 {
response.BadRequest(c, "未指定分销站点")
return
}
var req UpdateSiteBrandingRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "品牌装修参数格式不正确")
return
}
adminID, _ := currentAdminID(c)
dto, err := h.service.UpdateSiteBranding(c.Request.Context(), adminID, siteID, req)
if err != nil {
writeError(c, err)
return
}
response.OK(c, dto)
}
func (h *Handler) UpdateAccountMarkup(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
var req UpdateAccountMarkupRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "加价参数格式不正确")
return
}
adminID, _ := currentAdminID(c)
dto, err := h.service.UpdateAccountMarkup(c.Request.Context(), adminID, id, req.ShelfRatioDelta)
if err != nil {
writeError(c, err)
return
}
response.OK(c, dto)
}
func (h *Handler) BatchUpdateAccountMarkup(c *gin.Context) {
siteID, err := h.resolveSiteID(c)
if err != nil {
writeError(c, err)
return
}
var req BatchUpdateAccountMarkupRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "批量加价参数格式不正确")
return
}
if siteID == 0 {
response.BadRequest(c, "未指定分销站点")
return
}
adminID, _ := currentAdminID(c)
if err := h.service.BatchUpdateAccountMarkup(c.Request.Context(), adminID, siteID, req); err != nil {
writeError(c, err)
return
}
response.OK(c, gin.H{"updated": true})
}
func (h *Handler) BatchUpdateAccountDisplay(c *gin.Context) {
siteID, err := h.resolveSiteID(c)
if err != nil {
writeError(c, err)
return
}
var req BatchUpdateAccountDisplayRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "批量启停参数格式不正确")
return
}
if siteID == 0 {
response.BadRequest(c, "未指定分销站点")
return
}
adminID, _ := currentAdminID(c)
if err := h.service.BatchUpdateAccountDisplay(c.Request.Context(), adminID, siteID, req); err != nil {
writeError(c, err)
return
}
response.OK(c, gin.H{"updated": true})
}
func (h *Handler) ListReceivedListings(c *gin.Context) {
siteID, err := h.resolveSiteID(c)
if err != nil {
writeError(c, err)
return
}
if siteID == 0 {
response.BadRequest(c, "未指定分销站点")
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
q := ReceivedListingQuery{
Page: page,
PageSize: pageSize,
Status: c.Query("status"),
ReviewStatus: c.Query("review_status"),
GameName: c.Query("game_name"),
Keyword: c.Query("keyword"),
}
adminID, _ := currentAdminID(c)
resp, err := h.service.ListReceivedListings(c.Request.Context(), adminID, siteID, q)
if err != nil {
writeError(c, err)
return
}
response.OK(c, resp)
}
func (h *Handler) UpdateReceivedListingStatus(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
var req UpdateReceivedListingStatusRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "状态参数格式不正确")
return
}
adminID, _ := currentAdminID(c)
if err := h.service.UpdateReceivedListingStatus(c.Request.Context(), adminID, id, req.Status); err != nil {
writeError(c, err)
return
}
response.OK(c, gin.H{"updated": true})
}
func (h *Handler) GetSiteWallet(c *gin.Context) {
siteID, err := h.resolveSiteID(c)
if err != nil {
writeError(c, err)
return
}
if siteID == 0 {
response.BadRequest(c, "未指定分销站点")
return
}
adminID, _ := currentAdminID(c)
walletDTO, err := h.service.GetSiteWallet(c.Request.Context(), adminID, siteID)
if err != nil {
writeError(c, err)
return
}
response.OK(c, walletDTO)
}
func (h *Handler) ApplySiteWithdrawal(c *gin.Context) {
siteID, err := h.resolveSiteID(c)
if err != nil {
writeError(c, err)
return
}
if siteID == 0 {
response.BadRequest(c, "未指定分销站点")
return
}
var req ApplySiteWithdrawalRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "提现申请参数格式不正确")
return
}
adminID, _ := currentAdminID(c)
dto, err := h.service.ApplySiteWithdrawal(c.Request.Context(), adminID, siteID, req)
if err != nil {
writeError(c, err)
return
}
response.Created(c, dto)
}
func (h *Handler) ListSiteWithdrawals(c *gin.Context) {
siteID, err := h.resolveSiteID(c)
if err != nil {
writeError(c, err)
return
}
if siteID == 0 {
response.BadRequest(c, "未指定分销站点")
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
adminID, _ := currentAdminID(c)
pageResult, err := h.service.ListSiteWithdrawals(c.Request.Context(), adminID, siteID, page, pageSize)
if err != nil {
writeError(c, err)
return
}
response.OK(c, pageResult)
}
func (h *Handler) GetDashboardAnalytics(c *gin.Context) {
siteID, err := h.resolveSiteID(c)
if err != nil {
writeError(c, err)
return
}
if siteID == 0 {
response.BadRequest(c, "未指定分销站点")
return
}
adminID, _ := currentAdminID(c)
analytics, err := h.service.GetDashboardAnalytics(c.Request.Context(), adminID, siteID)
if err != nil {
writeError(c, err)
return
}
response.OK(c, analytics)
}
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 {
return 0, false
}
id, ok := value.(uint64)
return id, ok && id > 0
}
func parseID(c *gin.Context) (uint64, bool) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil || id == 0 {
response.BadRequest(c, "ID 不正确")
return 0, false
}
return id, true
}
func writeError(c *gin.Context, err error) {
switch {
case errors.Is(err, ErrSiteNotFound), errors.Is(err, ErrRuleNotFound), errors.Is(err, ErrOrderNotFound), errors.Is(err, ErrAccountDisplayNotFound), errors.Is(err, ErrPaymentChannelNotFound), errors.Is(err, ErrSettlementBatchNotFound):
response.NotFound(c, "分销资源不存在")
case errors.Is(err, ErrPaymentConfigNotFound):
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, ErrInsufficientBalance):
response.BadRequest(c, "可提现余额不足")
case errors.Is(err, ErrSettlementBatchStale):
response.BadRequest(c, "批次内订单发生退款变动或维权,该批次已失效,请作废后重新生成")
case errors.Is(err, ErrInvalidSettlementHold):
response.BadRequest(c, "结算冷却天数须在 0 到 365 之间")
case errors.Is(err, ErrBatchNotCancellable):
response.BadRequest(c, "该结算批次无法作废")
case errors.Is(err, ErrTooManySyncListings):
response.BadRequest(c, "一次勾选同步的商品过多")
case errors.Is(err, ErrSyncInProgress):
response.BadRequest(c, "当前分销站点商品同步正在进行中,请勿重复操作")
case errors.Is(err, ErrSyncListingNotFound):
response.BadRequest(c, "勾选的商品不存在")
case errors.Is(err, ErrEmptyListingIDs):
response.BadRequest(c, "请选择要加价的商品")
case errors.Is(err, ErrInvalidListingStatus):
response.BadRequest(c, "商品展示状态不正确")
case errors.Is(err, ErrSMSConfigInvalid):
response.BadRequest(c, "分销短信配置不完整,启用时必须配置 AccessKey、签名与模板")
case errors.Is(err, ErrSMSTestRateLimited):
response.Error(c, http.StatusTooManyRequests, "rate_limited", "测试短信发送过于频繁,请稍后再试")
case errors.Is(err, ErrSMSSendFailed):
response.Error(c, http.StatusBadGateway, "sms_send_failed", "短信测试发送失败,请检查配置与通道额度")
case errors.Is(err, ErrSiteAccessDenied):
response.Error(c, http.StatusForbidden, "forbidden", "无权访问该分销站点")
case errors.Is(err, ErrDependencyUnavailable):
response.ServiceUnavailable(c, "分销服务暂时不可用")
default:
response.RecordError(c, err)
response.Error(c, http.StatusInternalServerError, "distribution_error", "分销服务暂时不可用")
}
}