draft
This commit is contained in:
373
backend/internal/api/handler/admin.go
Normal file
373
backend/internal/api/handler/admin.go
Normal file
@@ -0,0 +1,373 @@
|
||||
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=<binary>, 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)
|
||||
}
|
||||
49
backend/internal/api/handler/auth.go
Normal file
49
backend/internal/api/handler/auth.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type adminAuthSvc interface {
|
||||
Login(ctx context.Context, username, password string) (string, error)
|
||||
}
|
||||
|
||||
type AuthHandler struct {
|
||||
authSvc adminAuthSvc
|
||||
}
|
||||
|
||||
func NewAuthHandler(authSvc adminAuthSvc) *AuthHandler {
|
||||
return &AuthHandler{authSvc: authSvc}
|
||||
}
|
||||
|
||||
type loginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Login(c *gin.Context) {
|
||||
var req loginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": "400", "message": "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
token, err := h.authSvc.Login(c.Request.Context(), req.Username, req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": "UNAUTHORIZED", "message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": "0",
|
||||
"message": "ok",
|
||||
"data": gin.H{"token": token},
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Logout(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"code": "0", "message": "ok"})
|
||||
}
|
||||
188
backend/internal/api/handler/merchant.go
Normal file
188
backend/internal/api/handler/merchant.go
Normal 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)
|
||||
}
|
||||
216
backend/internal/api/handler/merchant_test.go
Normal file
216
backend/internal/api/handler/merchant_test.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"pay-bridge/internal/channel"
|
||||
"pay-bridge/internal/model"
|
||||
)
|
||||
|
||||
func init() {
|
||||
gin.SetMode(gin.TestMode)
|
||||
}
|
||||
|
||||
// mockMerchantSvc 实现 merchantService interface
|
||||
type mockMerchantSvc struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *mockMerchantSvc) CreateMerchantForApp(ctx context.Context, appID string, merchant *model.Merchant) error {
|
||||
return m.Called(ctx, appID, merchant).Error(0)
|
||||
}
|
||||
func (m *mockMerchantSvc) GetMerchantForApp(ctx context.Context, appID, merchantID string) (*model.Merchant, error) {
|
||||
args := m.Called(ctx, appID, merchantID)
|
||||
v, _ := args.Get(0).(*model.Merchant)
|
||||
return v, args.Error(1)
|
||||
}
|
||||
func (m *mockMerchantSvc) ListMerchantsForApp(ctx context.Context, appID string, status model.MerchantStatus, limit, offset int) ([]*model.Merchant, error) {
|
||||
args := m.Called(ctx, appID, status, limit, offset)
|
||||
return args.Get(0).([]*model.Merchant), args.Error(1)
|
||||
}
|
||||
func (m *mockMerchantSvc) ApplyForApp(ctx context.Context, appID, merchantID, channelCode string, bizContent map[string]any) (string, error) {
|
||||
args := m.Called(ctx, appID, merchantID, channelCode, bizContent)
|
||||
return args.String(0), args.Error(1)
|
||||
}
|
||||
func (m *mockMerchantSvc) QueryAuditStatusForApp(ctx context.Context, appID, merchantID string) (*model.MerchantApplication, error) {
|
||||
args := m.Called(ctx, appID, merchantID)
|
||||
v, _ := args.Get(0).(*model.MerchantApplication)
|
||||
return v, args.Error(1)
|
||||
}
|
||||
func (m *mockMerchantSvc) UploadFile(ctx context.Context, channelCode string, req *channel.UploadFileReq) (string, error) {
|
||||
args := m.Called(ctx, channelCode, req)
|
||||
return args.String(0), args.Error(1)
|
||||
}
|
||||
|
||||
// newMerchantTestRouter 构建测试路由,注入固定 app_id 模拟鉴权
|
||||
func newMerchantTestRouter(svc *mockMerchantSvc) *gin.Engine {
|
||||
r := gin.New()
|
||||
h := &MerchantHandler{merchantSvc: svc}
|
||||
|
||||
auth := func(c *gin.Context) {
|
||||
c.Set("app_id", "app_test")
|
||||
c.Next()
|
||||
}
|
||||
|
||||
g := r.Group("/api/v1/merchant", auth)
|
||||
g.POST("", h.CreateMerchant)
|
||||
g.GET("", h.ListMerchants)
|
||||
g.GET("/:merchantID", h.GetMerchant)
|
||||
g.POST("/:merchantID/apply", h.Apply)
|
||||
g.GET("/:merchantID/audit", h.QueryAuditStatus)
|
||||
return r
|
||||
}
|
||||
|
||||
// --- CreateMerchant ---
|
||||
|
||||
func TestCreateMerchant_OK(t *testing.T) {
|
||||
svc := new(mockMerchantSvc)
|
||||
svc.On("CreateMerchantForApp", mock.Anything, "app_test", mock.MatchedBy(func(m *model.Merchant) bool {
|
||||
return m.MerchantID == "m001" && m.MerchantName == "测试公司"
|
||||
})).Return(nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/merchant",
|
||||
strings.NewReader(`{"merchant_id":"m001","merchant_name":"测试公司"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
newMerchantTestRouter(svc).ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp map[string]any
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.Equal(t, "0", resp["code"])
|
||||
assert.Equal(t, "m001", resp["data"].(map[string]any)["merchant_id"])
|
||||
svc.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestCreateMerchant_MissingName(t *testing.T) {
|
||||
svc := new(mockMerchantSvc)
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/merchant",
|
||||
strings.NewReader(`{"merchant_id":"m001"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
newMerchantTestRouter(svc).ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
svc.AssertNotCalled(t, "CreateMerchantForApp")
|
||||
}
|
||||
|
||||
func TestCreateMerchant_MissingID(t *testing.T) {
|
||||
svc := new(mockMerchantSvc)
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/merchant",
|
||||
strings.NewReader(`{"merchant_name":"测试公司"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
newMerchantTestRouter(svc).ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
}
|
||||
|
||||
// --- GetMerchant ---
|
||||
|
||||
func TestGetMerchant_OK(t *testing.T) {
|
||||
svc := new(mockMerchantSvc)
|
||||
svc.On("GetMerchantForApp", mock.Anything, "app_test", "m001").
|
||||
Return(&model.Merchant{MerchantID: "m001", AppID: "app_test"}, nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
newMerchantTestRouter(svc).ServeHTTP(w,
|
||||
httptest.NewRequest(http.MethodGet, "/api/v1/merchant/m001", nil))
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
svc.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestGetMerchant_NotFound(t *testing.T) {
|
||||
svc := new(mockMerchantSvc)
|
||||
svc.On("GetMerchantForApp", mock.Anything, "app_test", "m999").
|
||||
Return((*model.Merchant)(nil), errors.New("30001"))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
newMerchantTestRouter(svc).ServeHTTP(w,
|
||||
httptest.NewRequest(http.MethodGet, "/api/v1/merchant/m999", nil))
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
func TestGetMerchant_WrongApp(t *testing.T) {
|
||||
svc := new(mockMerchantSvc)
|
||||
svc.On("GetMerchantForApp", mock.Anything, "app_test", "other_m").
|
||||
Return((*model.Merchant)(nil), errors.New("30001"))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
newMerchantTestRouter(svc).ServeHTTP(w,
|
||||
httptest.NewRequest(http.MethodGet, "/api/v1/merchant/other_m", nil))
|
||||
|
||||
// 跨 app 访问应返回 404,而不是 403,避免信息泄露
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
// --- ListMerchants ---
|
||||
|
||||
func TestListMerchants_DefaultPagination(t *testing.T) {
|
||||
svc := new(mockMerchantSvc)
|
||||
svc.On("ListMerchantsForApp", mock.Anything, "app_test", model.MerchantStatus(""), 20, 0).
|
||||
Return([]*model.Merchant{}, nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
newMerchantTestRouter(svc).ServeHTTP(w,
|
||||
httptest.NewRequest(http.MethodGet, "/api/v1/merchant", nil))
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
svc.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// --- Apply ---
|
||||
|
||||
func TestApply_OK(t *testing.T) {
|
||||
svc := new(mockMerchantSvc)
|
||||
svc.On("ApplyForApp", mock.Anything, "app_test", "m001", "HEEPAY", mock.Anything).
|
||||
Return("APP123", nil)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/merchant/m001/apply",
|
||||
strings.NewReader(`{"channel_code":"HEEPAY","submit_data":{"name":"测试公司"}}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
newMerchantTestRouter(svc).ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var resp map[string]any
|
||||
json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
assert.Equal(t, "APP123", resp["data"].(map[string]any)["application_id"])
|
||||
}
|
||||
|
||||
func TestApply_MissingChannelCode(t *testing.T) {
|
||||
svc := new(mockMerchantSvc)
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/merchant/m001/apply",
|
||||
strings.NewReader(`{"submit_data":{}}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
newMerchantTestRouter(svc).ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
svc.AssertNotCalled(t, "ApplyForApp")
|
||||
}
|
||||
|
||||
func TestApply_MerchantNotBelongToApp(t *testing.T) {
|
||||
svc := new(mockMerchantSvc)
|
||||
svc.On("ApplyForApp", mock.Anything, "app_test", "m_other", "HEEPAY", mock.Anything).
|
||||
Return("", errors.New("30001"))
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/merchant/m_other/apply",
|
||||
strings.NewReader(`{"channel_code":"HEEPAY"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
newMerchantTestRouter(svc).ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||
}
|
||||
242
backend/internal/api/handler/pay.go
Normal file
242
backend/internal/api/handler/pay.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"pay-bridge/internal/errcode"
|
||||
"pay-bridge/internal/model"
|
||||
"pay-bridge/internal/service"
|
||||
)
|
||||
|
||||
// PayHandler 支付相关 Handler
|
||||
type PayHandler struct {
|
||||
tradeSvc *service.TradeService
|
||||
refundSvc *service.RefundService
|
||||
}
|
||||
|
||||
func NewPayHandler(tradeSvc *service.TradeService, refundSvc *service.RefundService) *PayHandler {
|
||||
return &PayHandler{tradeSvc: tradeSvc, refundSvc: refundSvc}
|
||||
}
|
||||
|
||||
type unifiedOrderReq struct {
|
||||
ChannelCode string `json:"channel_code"`
|
||||
MerchantOrderNo string `json:"merchant_order_no" binding:"required"`
|
||||
PayMethod model.PayMethod `json:"pay_method" binding:"required"`
|
||||
Amount int64 `json:"amount" binding:"required,min=1"`
|
||||
ProfitSharingAmount int64 `json:"profit_sharing_amount"`
|
||||
Subject string `json:"subject" binding:"required"`
|
||||
NotifyURL string `json:"notify_url" binding:"required,url"`
|
||||
ExpireMinutes int `json:"expire_minutes"`
|
||||
Extra map[string]any `json:"extra"`
|
||||
MerchantID string `json:"merchant_id"` // 可选,指定收款商户
|
||||
}
|
||||
|
||||
type closeOrderReq struct {
|
||||
TradeNo string `json:"trade_no" binding:"required"`
|
||||
}
|
||||
|
||||
type refundReq struct {
|
||||
TradeNo string `json:"trade_no" binding:"required"`
|
||||
RefundAmount int64 `json:"refund_amount" binding:"required,min=1"`
|
||||
Reason string `json:"reason"`
|
||||
NotifyURL string `json:"notify_url"`
|
||||
}
|
||||
|
||||
// UnifiedOrder POST /api/v1/pay/unified-order
|
||||
func (h *PayHandler) UnifiedOrder(c *gin.Context) {
|
||||
var req unifiedOrderReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
BadRequest(c, errcode.ErrInvalidParam, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
appID := c.GetString("app_id")
|
||||
resp, err := h.tradeSvc.CreateOrder(c.Request.Context(), &service.CreateOrderReq{
|
||||
AppID: appID,
|
||||
ChannelCode: req.ChannelCode,
|
||||
MerchantOrderNo: req.MerchantOrderNo,
|
||||
PayMethod: req.PayMethod,
|
||||
Amount: req.Amount,
|
||||
ProfitSharingAmount: req.ProfitSharingAmount,
|
||||
Subject: req.Subject,
|
||||
NotifyURL: req.NotifyURL,
|
||||
ExpireMinutes: req.ExpireMinutes,
|
||||
Extra: req.Extra,
|
||||
MerchantID: req.MerchantID,
|
||||
})
|
||||
if err != nil {
|
||||
handleBizError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
OK(c, gin.H{
|
||||
"trade_no": resp.TradeNo,
|
||||
"pay_credential": resp.PayCredential,
|
||||
"is_idempotent": resp.IsIdempotent,
|
||||
})
|
||||
}
|
||||
|
||||
// QueryOrder GET /api/v1/pay/query/:tradeNo
|
||||
func (h *PayHandler) QueryOrder(c *gin.Context) {
|
||||
tradeNo := c.Param("tradeNo")
|
||||
if tradeNo == "" {
|
||||
BadRequest(c, errcode.ErrMissingParam, "trade_no is required")
|
||||
return
|
||||
}
|
||||
|
||||
appID := c.GetString("app_id")
|
||||
order, err := h.tradeSvc.QueryOrder(c.Request.Context(), appID, tradeNo)
|
||||
if err != nil {
|
||||
handleBizError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
OK(c, gin.H{
|
||||
"trade_no": order.TradeNo,
|
||||
"merchant_order_no": order.MerchantOrderNo,
|
||||
"pay_method": order.PayMethod,
|
||||
"amount": order.Amount,
|
||||
"status": order.Status,
|
||||
"channel_trade_no": order.ChannelTradeNo,
|
||||
"pay_time": order.PayTime,
|
||||
"created_at": order.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// CloseOrder POST /api/v1/pay/close
|
||||
func (h *PayHandler) CloseOrder(c *gin.Context) {
|
||||
var req closeOrderReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
BadRequest(c, errcode.ErrInvalidParam, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
appID := c.GetString("app_id")
|
||||
if err := h.tradeSvc.CloseOrder(c.Request.Context(), appID, req.TradeNo); err != nil {
|
||||
handleBizError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
OK(c, nil)
|
||||
}
|
||||
|
||||
// Refund POST /api/v1/pay/refund
|
||||
func (h *PayHandler) Refund(c *gin.Context) {
|
||||
var req refundReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
BadRequest(c, errcode.ErrInvalidParam, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
appID := c.GetString("app_id")
|
||||
refund, err := h.refundSvc.CreateRefund(c.Request.Context(), &service.CreateRefundReq{
|
||||
AppID: appID,
|
||||
TradeNo: req.TradeNo,
|
||||
RefundAmount: req.RefundAmount,
|
||||
Reason: req.Reason,
|
||||
NotifyURL: req.NotifyURL,
|
||||
})
|
||||
if err != nil {
|
||||
handleBizError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
OK(c, gin.H{
|
||||
"refund_no": refund.RefundNo,
|
||||
"trade_no": refund.TradeNo,
|
||||
"refund_amount": refund.RefundAmount,
|
||||
"status": refund.Status,
|
||||
"channel_refund_no": refund.ChannelRefundNo,
|
||||
})
|
||||
}
|
||||
|
||||
// QueryRefund GET /api/v1/pay/refund/query/:refundNo
|
||||
func (h *PayHandler) QueryRefund(c *gin.Context) {
|
||||
refundNo := c.Param("refundNo")
|
||||
if refundNo == "" {
|
||||
BadRequest(c, errcode.ErrMissingParam, "refund_no is required")
|
||||
return
|
||||
}
|
||||
|
||||
appID := c.GetString("app_id")
|
||||
refund, err := h.refundSvc.QueryRefund(c.Request.Context(), appID, refundNo)
|
||||
if err != nil {
|
||||
handleBizError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
OK(c, gin.H{
|
||||
"refund_no": refund.RefundNo,
|
||||
"trade_no": refund.TradeNo,
|
||||
"refund_amount": refund.RefundAmount,
|
||||
"status": refund.Status,
|
||||
"channel_refund_no": refund.ChannelRefundNo,
|
||||
"refund_time": refund.RefundTime,
|
||||
})
|
||||
}
|
||||
|
||||
// handleBizError 将业务错误映射到 HTTP 响应
|
||||
func handleBizError(c *gin.Context, err error) {
|
||||
code := err.Error()
|
||||
msg := errcode.Message(code)
|
||||
if msg == "未知错误" {
|
||||
msg = err.Error()
|
||||
code = errcode.ErrInternalSystem
|
||||
}
|
||||
|
||||
switch code {
|
||||
case errcode.ErrInvalidParam, errcode.ErrMissingParam, errcode.ErrInvalidPayMethod, errcode.ErrInvalidAmount:
|
||||
BadRequest(c, code, msg)
|
||||
case errcode.ErrUnauthorized, errcode.ErrAppNotFound:
|
||||
Unauthorized(c, code, msg)
|
||||
case errcode.ErrPermissionDenied:
|
||||
Forbidden(c, code, msg)
|
||||
case errcode.ErrOrderNotFound, errcode.ErrOrderAlreadyPaid, errcode.ErrOrderClosed,
|
||||
errcode.ErrRefundAmountExceed, errcode.ErrSharingAmountExceed, errcode.ErrOrderNotPaid,
|
||||
errcode.ErrSharingNotConfig, errcode.ErrSharingFeeExceed, errcode.ErrRefundNotFound:
|
||||
UnprocessableEntity(c, code, msg)
|
||||
case errcode.ErrChannelCreateFail, errcode.ErrChannelRefundFail,
|
||||
errcode.ErrChannelTimeout, errcode.ErrChannelNotSupport, errcode.ErrChannelVerifyFail:
|
||||
BadGateway(c, code, msg)
|
||||
default:
|
||||
_ = errors.New(code) // suppress unused
|
||||
InternalError(c, errcode.ErrInternalSystem, errcode.Message(errcode.ErrInternalSystem))
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyHandler 渠道回调 Handler
|
||||
type NotifyHandler struct {
|
||||
tradeSvc *service.TradeService
|
||||
}
|
||||
|
||||
func NewNotifyHandler(tradeSvc *service.TradeService) *NotifyHandler {
|
||||
return &NotifyHandler{tradeSvc: tradeSvc}
|
||||
}
|
||||
|
||||
// PaymentCallback POST /api/v1/notify/payment/:channelCode
|
||||
func (h *NotifyHandler) PaymentCallback(c *gin.Context) {
|
||||
channelCode := c.Param("channelCode")
|
||||
var rawBody []byte
|
||||
if v, exists := c.Get("raw_body"); exists {
|
||||
rawBody, _ = v.([]byte)
|
||||
}
|
||||
|
||||
result, err := h.tradeSvc.HandleUpstreamNotify(c.Request.Context(), channelCode, rawBody, headersMap(c))
|
||||
if err != nil {
|
||||
c.String(http.StatusOK, "fail")
|
||||
return
|
||||
}
|
||||
c.String(http.StatusOK, result)
|
||||
}
|
||||
|
||||
func headersMap(c *gin.Context) map[string]string {
|
||||
m := make(map[string]string)
|
||||
for k, v := range c.Request.Header {
|
||||
if len(v) > 0 {
|
||||
m[k] = v[0]
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
73
backend/internal/api/handler/response.go
Normal file
73
backend/internal/api/handler/response.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Response 统一响应格式
|
||||
type Response struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data any `json:"data,omitempty"`
|
||||
TraceID string `json:"trace_id,omitempty"`
|
||||
}
|
||||
|
||||
// OK 成功响应
|
||||
func OK(c *gin.Context, data any) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Code: "0",
|
||||
Message: "success",
|
||||
Data: data,
|
||||
TraceID: traceID(c),
|
||||
})
|
||||
}
|
||||
|
||||
// Fail 失败响应
|
||||
func Fail(c *gin.Context, httpStatus int, code, message string) {
|
||||
c.JSON(httpStatus, Response{
|
||||
Code: code,
|
||||
Message: message,
|
||||
TraceID: traceID(c),
|
||||
})
|
||||
}
|
||||
|
||||
// BadRequest 400
|
||||
func BadRequest(c *gin.Context, code, message string) {
|
||||
Fail(c, http.StatusBadRequest, code, message)
|
||||
}
|
||||
|
||||
// Unauthorized 401
|
||||
func Unauthorized(c *gin.Context, code, message string) {
|
||||
Fail(c, http.StatusUnauthorized, code, message)
|
||||
}
|
||||
|
||||
// Forbidden 403
|
||||
func Forbidden(c *gin.Context, code, message string) {
|
||||
Fail(c, http.StatusForbidden, code, message)
|
||||
}
|
||||
|
||||
// UnprocessableEntity 422(业务规则错误)
|
||||
func UnprocessableEntity(c *gin.Context, code, message string) {
|
||||
Fail(c, http.StatusUnprocessableEntity, code, message)
|
||||
}
|
||||
|
||||
// InternalError 500
|
||||
func InternalError(c *gin.Context, code, message string) {
|
||||
Fail(c, http.StatusInternalServerError, code, message)
|
||||
}
|
||||
|
||||
// BadGateway 502(渠道错误)
|
||||
func BadGateway(c *gin.Context, code, message string) {
|
||||
Fail(c, http.StatusBadGateway, code, message)
|
||||
}
|
||||
|
||||
func traceID(c *gin.Context) string {
|
||||
if id, exists := c.Get("trace_id"); exists {
|
||||
if s, ok := id.(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user