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

374 lines
9.8 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 (
"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)
}