package handler import ( "io" "net/http" "path/filepath" "strconv" "github.com/gin-gonic/gin" "pay-bridge/internal/channel" "pay-bridge/internal/model" "pay-bridge/internal/service" ) // AdminHandler 管理后台接口处理器 type AdminHandler struct { matchSvc *service.PaymentMatchService merchantSvc *service.MerchantService reconSvc *service.ReconciliationService channelSvc *service.ChannelService appSvc *service.AppService } func NewAdminHandler( matchSvc *service.PaymentMatchService, merchantSvc *service.MerchantService, reconSvc *service.ReconciliationService, channelSvc *service.ChannelService, appSvc *service.AppService, ) *AdminHandler { return &AdminHandler{ matchSvc: matchSvc, merchantSvc: merchantSvc, reconSvc: reconSvc, channelSvc: channelSvc, appSvc: appSvc, } } // --- 请求结构体 --- type createAppReq struct { AppName string `json:"app_name" binding:"required"` } type manualBindOrderReq struct { MatchID uint64 `json:"match_id" binding:"required"` TradeNo string `json:"trade_no" binding:"required"` Operator string `json:"operator" binding:"required"` } type applyMerchantReq struct { ChannelCode string `json:"channel_code" binding:"required"` SubmitData map[string]any `json:"submit_data"` } // appVO 应用列表视图(不含加密 secret) type appVO struct { AppID string `json:"app_id"` AppName string `json:"app_name"` Status int8 `json:"status"` CreatedAt any `json:"created_at"` UpdatedAt any `json:"updated_at"` } // --- 应用管理 --- // CreateApp 创建下游接入应用 func (h *AdminHandler) CreateApp(c *gin.Context) { var req createAppReq if err := c.ShouldBindJSON(&req); err != nil { BadRequest(c, "10001", err.Error()) return } result, err := h.appSvc.CreateApp(c.Request.Context(), req.AppName) if err != nil { InternalError(c, "50001", err.Error()) return } // 明文 secret 仅在创建时返回一次,之后无法再查看 OK(c, gin.H{ "app_id": result.App.AppID, "app_name": result.App.AppName, "app_secret": result.PlainSecret, "status": result.App.Status, "created_at": result.App.CreatedAt, "secret_tip": "请妥善保存 app_secret,此后将无法再次查看", }) } // ListApps 查询应用列表 func (h *AdminHandler) ListApps(c *gin.Context) { limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) apps, err := h.appSvc.ListApps(c.Request.Context(), limit, offset) if err != nil { InternalError(c, "50001", err.Error()) return } list := make([]appVO, 0, len(apps)) for _, a := range apps { list = append(list, appVO{ AppID: a.AppID, AppName: a.AppName, Status: a.Status, CreatedAt: a.CreatedAt, UpdatedAt: a.UpdatedAt, }) } OK(c, gin.H{"list": list, "limit": limit, "offset": offset}) } // DisableApp 禁用应用 func (h *AdminHandler) DisableApp(c *gin.Context) { appID := c.Param("appID") if err := h.appSvc.DisableApp(c.Request.Context(), appID); err != nil { if err.Error() == "20002" { c.JSON(http.StatusNotFound, Response{Code: "20002", Message: "应用不存在"}) return } InternalError(c, "50001", err.Error()) return } OK(c, nil) } // EnableApp 启用应用 func (h *AdminHandler) EnableApp(c *gin.Context) { appID := c.Param("appID") if err := h.appSvc.EnableApp(c.Request.Context(), appID); err != nil { if err.Error() == "20002" { c.JSON(http.StatusNotFound, Response{Code: "20002", Message: "应用不存在"}) return } InternalError(c, "50001", err.Error()) return } OK(c, nil) } // ResetAppSecret 重置应用密钥 func (h *AdminHandler) ResetAppSecret(c *gin.Context) { appID := c.Param("appID") plainSecret, err := h.appSvc.ResetSecret(c.Request.Context(), appID) if err != nil { if err.Error() == "20002" { c.JSON(http.StatusNotFound, Response{Code: "20002", Message: "应用不存在"}) return } InternalError(c, "50001", err.Error()) return } OK(c, gin.H{ "app_id": appID, "app_secret": plainSecret, "secret_tip": "请妥善保存 app_secret,此后将无法再次查看", }) } // --- 收款匹配管理 --- // ListPendingMatches 查询待人工确认的收款记录 func (h *AdminHandler) ListPendingMatches(c *gin.Context) { appID := c.Query("app_id") limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) logs, err := h.matchSvc.ListPendingManual(c.Request.Context(), appID, limit, offset) if err != nil { InternalError(c, "50001", err.Error()) return } OK(c, gin.H{"list": logs, "limit": limit, "offset": offset}) } // ManualBindOrder 人工关联收款与订单 func (h *AdminHandler) ManualBindOrder(c *gin.Context) { var req manualBindOrderReq if err := c.ShouldBindJSON(&req); err != nil { BadRequest(c, "10001", err.Error()) return } if err := h.matchSvc.ManualBindOrder(c.Request.Context(), req.MatchID, req.TradeNo, req.Operator); err != nil { InternalError(c, "50001", err.Error()) return } OK(c, nil) } // --- 商户管理 --- // CreateMerchant 创建商户 func (h *AdminHandler) CreateMerchant(c *gin.Context) { var m model.Merchant if err := c.ShouldBindJSON(&m); err != nil { BadRequest(c, "10001", err.Error()) return } if err := h.merchantSvc.CreateMerchant(c.Request.Context(), &m); err != nil { InternalError(c, "50001", err.Error()) return } OK(c, m) } // GetMerchant 查询商户信息 func (h *AdminHandler) GetMerchant(c *gin.Context) { merchantID := c.Param("merchantID") m, err := h.merchantSvc.GetMerchant(c.Request.Context(), merchantID) if err != nil { InternalError(c, "50001", err.Error()) return } if m == nil { c.JSON(http.StatusNotFound, Response{Code: "30001", Message: "merchant not found"}) return } OK(c, m) } // ListMerchants 查询商户列表 func (h *AdminHandler) ListMerchants(c *gin.Context) { status := model.MerchantStatus(c.Query("status")) limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20")) offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0")) merchants, err := h.merchantSvc.ListMerchants(c.Request.Context(), status, limit, offset) if err != nil { InternalError(c, "50001", err.Error()) return } OK(c, gin.H{"list": merchants, "limit": limit, "offset": offset}) } // FreezeMerchant 冻结商户 func (h *AdminHandler) FreezeMerchant(c *gin.Context) { merchantID := c.Param("merchantID") if err := h.merchantSvc.FreezeMerchant(c.Request.Context(), merchantID); err != nil { InternalError(c, "50001", err.Error()) return } OK(c, nil) } // UnfreezeMerchant 解冻商户 func (h *AdminHandler) UnfreezeMerchant(c *gin.Context) { merchantID := c.Param("merchantID") if err := h.merchantSvc.UnfreezeMerchant(c.Request.Context(), merchantID); err != nil { InternalError(c, "50001", err.Error()) return } OK(c, nil) } // UploadMerchantFile 上传进件所需文件,返回 file_id // POST /api/v1/admin/merchant/upload-file // multipart/form-data: file=, channel_code=HEEPAY, file_media_type=01 func (h *AdminHandler) UploadMerchantFile(c *gin.Context) { channelCode := c.PostForm("channel_code") if channelCode == "" { channelCode = "HEEPAY" } fileMediaType := c.PostForm("file_media_type") if fileMediaType == "" { BadRequest(c, "10001", "file_media_type is required") return } fileHeader, err := c.FormFile("file") if err != nil { BadRequest(c, "10001", "file is required: "+err.Error()) return } f, err := fileHeader.Open() if err != nil { InternalError(c, "50001", "open file: "+err.Error()) return } defer f.Close() content, err := io.ReadAll(f) if err != nil { InternalError(c, "50001", "read file: "+err.Error()) return } fileID, err := h.merchantSvc.UploadFile(c.Request.Context(), channelCode, &channel.UploadFileReq{ FileContent: content, FileName: filepath.Base(fileHeader.Filename), FileMediaType: fileMediaType, }) if err != nil { InternalError(c, "50001", err.Error()) return } OK(c, gin.H{"file_id": fileID}) } // ApplyMerchant 商户进件申请 func (h *AdminHandler) ApplyMerchant(c *gin.Context) { merchantID := c.Param("merchantID") var req applyMerchantReq if err := c.ShouldBindJSON(&req); err != nil { BadRequest(c, "10001", err.Error()) return } applicationID, err := h.merchantSvc.Apply(c.Request.Context(), merchantID, req.ChannelCode, req.SubmitData) if err != nil { InternalError(c, "50001", err.Error()) return } OK(c, gin.H{"application_id": applicationID}) } // QueryAuditStatus 查询进件审核状态 func (h *AdminHandler) QueryAuditStatus(c *gin.Context) { merchantID := c.Param("merchantID") app, err := h.merchantSvc.QueryAuditStatus(c.Request.Context(), merchantID) if err != nil { InternalError(c, "50001", err.Error()) return } OK(c, app) } // --- 对账管理 --- // TriggerReconciliation 手动触发对账 func (h *AdminHandler) TriggerReconciliation(c *gin.Context) { if err := h.reconSvc.RunDailyReconciliation(c.Request.Context()); err != nil { InternalError(c, "50001", err.Error()) return } OK(c, nil) } // GetReconciliationReport 查询对账报告 func (h *AdminHandler) GetReconciliationReport(c *gin.Context) { appID := c.Query("app_id") billDate := c.Query("bill_date") channelCode := c.Query("channel_code") report, err := h.reconSvc.GetReport(c.Request.Context(), appID, billDate, channelCode) if err != nil { InternalError(c, "50001", err.Error()) return } OK(c, report) } // GetReconciliationExceptions 查询对账异常明细 func (h *AdminHandler) GetReconciliationExceptions(c *gin.Context) { reportIDStr := c.Param("reportID") reportID, err := strconv.ParseUint(reportIDStr, 10, 64) if err != nil { BadRequest(c, "10001", "invalid report_id") return } exs, err := h.reconSvc.GetExceptions(c.Request.Context(), reportID) if err != nil { InternalError(c, "50001", err.Error()) return } OK(c, exs) }