This commit is contained in:
2026-03-13 15:51:59 +08:00
parent 4db2386bbf
commit 4e91f4cede
133 changed files with 19502 additions and 37 deletions

View File

@@ -0,0 +1,188 @@
package handler
import (
"context"
"io"
"net/http"
"path/filepath"
"strconv"
"github.com/gin-gonic/gin"
"pay-bridge/internal/channel"
"pay-bridge/internal/errcode"
"pay-bridge/internal/model"
)
// merchantService 定义 MerchantHandler 依赖的 service 方法,便于测试时注入 mock
type merchantService interface {
CreateMerchantForApp(ctx context.Context, appID string, m *model.Merchant) error
GetMerchantForApp(ctx context.Context, appID, merchantID string) (*model.Merchant, error)
ListMerchantsForApp(ctx context.Context, appID string, status model.MerchantStatus, limit, offset int) ([]*model.Merchant, error)
ApplyForApp(ctx context.Context, appID, merchantID, channelCode string, bizContent map[string]any) (string, error)
QueryAuditStatusForApp(ctx context.Context, appID, merchantID string) (*model.MerchantApplication, error)
UploadFile(ctx context.Context, channelCode string, req *channel.UploadFileReq) (string, error)
}
// MerchantHandler 业务侧商户进件接口处理器HMAC 鉴权appID 隔离)
type MerchantHandler struct {
merchantSvc merchantService
}
func NewMerchantHandler(svc merchantService) *MerchantHandler {
return &MerchantHandler{merchantSvc: svc}
}
// --- 请求结构体 ---
type createMerchantReq struct {
MerchantID string `json:"merchant_id" binding:"required"`
MerchantName string `json:"merchant_name" binding:"required"`
LicenseNo string `json:"license_no"`
LegalPerson string `json:"legal_person"`
BankAccount string `json:"bank_account"`
}
type merchantApplyReq struct {
ChannelCode string `json:"channel_code" binding:"required"`
SubmitData map[string]any `json:"submit_data"`
}
// --- Handlers ---
// CreateMerchant POST /api/v1/merchant
func (h *MerchantHandler) CreateMerchant(c *gin.Context) {
var req createMerchantReq
if err := c.ShouldBindJSON(&req); err != nil {
BadRequest(c, errcode.ErrInvalidParam, err.Error())
return
}
appID := c.GetString("app_id")
m := &model.Merchant{
MerchantID: req.MerchantID,
MerchantName: req.MerchantName,
LicenseNo: req.LicenseNo,
LegalPerson: req.LegalPerson,
BankAccount: req.BankAccount,
}
if err := h.merchantSvc.CreateMerchantForApp(c.Request.Context(), appID, m); err != nil {
InternalError(c, errcode.ErrInternalDB, err.Error())
return
}
OK(c, gin.H{"merchant_id": m.MerchantID})
}
// ListMerchants GET /api/v1/merchant
func (h *MerchantHandler) ListMerchants(c *gin.Context) {
appID := c.GetString("app_id")
status := model.MerchantStatus(c.Query("status"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
offset, _ := strconv.Atoi(c.DefaultQuery("offset", "0"))
merchants, err := h.merchantSvc.ListMerchantsForApp(c.Request.Context(), appID, status, limit, offset)
if err != nil {
InternalError(c, errcode.ErrInternalDB, err.Error())
return
}
OK(c, gin.H{"list": merchants, "limit": limit, "offset": offset})
}
// GetMerchant GET /api/v1/merchant/:merchantID
func (h *MerchantHandler) GetMerchant(c *gin.Context) {
appID := c.GetString("app_id")
merchantID := c.Param("merchantID")
m, err := h.merchantSvc.GetMerchantForApp(c.Request.Context(), appID, merchantID)
if err != nil {
if err.Error() == errcode.ErrOrderNotFound {
c.JSON(http.StatusNotFound, Response{Code: errcode.ErrOrderNotFound, Message: "merchant not found"})
return
}
InternalError(c, errcode.ErrInternalDB, err.Error())
return
}
OK(c, m)
}
// UploadFile POST /api/v1/merchant/upload-file
func (h *MerchantHandler) UploadFile(c *gin.Context) {
channelCode := c.PostForm("channel_code")
if channelCode == "" {
channelCode = "HEEPAY"
}
fileMediaType := c.PostForm("file_media_type")
if fileMediaType == "" {
BadRequest(c, errcode.ErrInvalidParam, "file_media_type is required")
return
}
fileHeader, err := c.FormFile("file")
if err != nil {
BadRequest(c, errcode.ErrInvalidParam, "file is required: "+err.Error())
return
}
f, err := fileHeader.Open()
if err != nil {
InternalError(c, errcode.ErrInternalSystem, "open file: "+err.Error())
return
}
defer f.Close()
content, err := io.ReadAll(f)
if err != nil {
InternalError(c, errcode.ErrInternalSystem, "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, errcode.ErrInternalSystem, err.Error())
return
}
OK(c, gin.H{"file_id": fileID})
}
// Apply POST /api/v1/merchant/:merchantID/apply
func (h *MerchantHandler) Apply(c *gin.Context) {
appID := c.GetString("app_id")
merchantID := c.Param("merchantID")
var req merchantApplyReq
if err := c.ShouldBindJSON(&req); err != nil {
BadRequest(c, errcode.ErrInvalidParam, err.Error())
return
}
applicationID, err := h.merchantSvc.ApplyForApp(c.Request.Context(), appID, merchantID, req.ChannelCode, req.SubmitData)
if err != nil {
if err.Error() == errcode.ErrOrderNotFound {
c.JSON(http.StatusNotFound, Response{Code: errcode.ErrOrderNotFound, Message: "merchant not found"})
return
}
InternalError(c, errcode.ErrInternalSystem, err.Error())
return
}
OK(c, gin.H{"application_id": applicationID})
}
// QueryAuditStatus GET /api/v1/merchant/:merchantID/audit
func (h *MerchantHandler) QueryAuditStatus(c *gin.Context) {
appID := c.GetString("app_id")
merchantID := c.Param("merchantID")
app, err := h.merchantSvc.QueryAuditStatusForApp(c.Request.Context(), appID, merchantID)
if err != nil {
if err.Error() == errcode.ErrOrderNotFound {
c.JSON(http.StatusNotFound, Response{Code: errcode.ErrOrderNotFound, Message: "merchant not found"})
return
}
InternalError(c, errcode.ErrInternalSystem, err.Error())
return
}
OK(c, app)
}