189 lines
5.7 KiB
Go
189 lines
5.7 KiB
Go
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)
|
||
}
|