Files
pay-bridge/backend/internal/api/handler/merchant.go
2026-03-13 15:51:59 +08:00

189 lines
5.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}