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,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)
}