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)
|
||||
}
|
||||
Reference in New Issue
Block a user