- 后端新增 GET /api/distribution/check-domain 作为 Caddy on_demand_tls ask 白名单端点 - 补充 CheckDomain 相关场景自动化测试 - Caddyfile 启用 on_demand_tls 并配置动态分销前台入口
918 lines
24 KiB
Go
918 lines
24 KiB
Go
package distribution
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"hfb_sys/backend/internal/middleware"
|
|
"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) 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 := strings.TrimSpace(c.Query("code"))
|
|
if code == "" {
|
|
code = strings.TrimSpace(c.Query("site_code"))
|
|
}
|
|
if code == "" {
|
|
code = strings.TrimSpace(c.GetHeader("X-Distribution-Site-Code"))
|
|
}
|
|
domain := strings.TrimSpace(c.Query("domain"))
|
|
if code == "" && domain == "" {
|
|
host := c.Request.Host
|
|
if colon := strings.Index(host, ":"); colon != -1 {
|
|
host = host[:colon]
|
|
}
|
|
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 := strings.ToLower(strings.TrimSpace(c.Query("domain")))
|
|
if domain == "" {
|
|
c.String(http.StatusBadRequest, "domain required")
|
|
return
|
|
}
|
|
if colon := strings.Index(domain, ":"); colon != -1 {
|
|
domain = domain[:colon]
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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", "分销服务暂时不可用")
|
|
}
|
|
}
|