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 (h *Handler) ListDistributionUsers(c *gin.Context) { siteID, err := h.resolveSiteID(c) if err != nil { writeError(c, err) return } adminID, _ := currentAdminID(c) var query ListDistributionUsersQuery if err := c.ShouldBindQuery(&query); err != nil { response.BadRequest(c, "查询参数格式错误") return } resp, err := h.service.ListDistributionUsers(c.Request.Context(), adminID, siteID, query) if err != nil { writeError(c, err) return } response.OK(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 { 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", "分销服务暂时不可用") } }