积分充值全流程落地:申请审核入账、凭证上传压缩转webp、管理员调账收权
- 新增积分充值申请与审核:商户提交充值申请(凭证图片必填,1元=100积分), 管理员审核通过后事务内自动入账,幂等键防重复 - 新增通用图片上传组件 ImageUploader:canvas 压缩转 webp、预览、删除, 后端按魔数校验图片格式并持久化到 data/uploads - 管理员钱包调整改为按商户维度:移除商户侧调账接口,新增管理员 平台商户积分调整与钱包流水查看 - 低余额 Webhook 告警配置持久化,充值入账/调账后自动检查推送 - 平台商户操作列移除进入/成员,积分充值页接入真实接口并修复金额单位换算
This commit is contained in:
@@ -17,14 +17,16 @@ type MerchantHandler struct {
|
||||
fulfillmentSvc *service.FulfillmentService
|
||||
callbackSvc *service.CallbackService
|
||||
deliverySvc *service.DeliveryService
|
||||
rechargeSvc *service.RechargeService
|
||||
}
|
||||
|
||||
func NewMerchantHandler(merchantSvc *service.MerchantService, fulfillmentSvc *service.FulfillmentService, callbackSvc *service.CallbackService, deliverySvc *service.DeliveryService) *MerchantHandler {
|
||||
func NewMerchantHandler(merchantSvc *service.MerchantService, fulfillmentSvc *service.FulfillmentService, callbackSvc *service.CallbackService, deliverySvc *service.DeliveryService, rechargeSvc *service.RechargeService) *MerchantHandler {
|
||||
return &MerchantHandler{
|
||||
merchantSvc: merchantSvc,
|
||||
fulfillmentSvc: fulfillmentSvc,
|
||||
callbackSvc: callbackSvc,
|
||||
deliverySvc: deliverySvc,
|
||||
rechargeSvc: rechargeSvc,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,6 +276,7 @@ func (h *MerchantHandler) AdminAdjustWallet(c *gin.Context) {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
h.rechargeSvc.CheckLowBalanceAndNotify(uint(id))
|
||||
response.OK(c, wallet)
|
||||
}
|
||||
|
||||
@@ -359,10 +362,10 @@ func (h *MerchantHandler) ListCallbacks(c *gin.Context) {
|
||||
}
|
||||
|
||||
type callbackReq struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url" binding:"required"`
|
||||
Events string `json:"events" binding:"required"`
|
||||
Status string `json:"status"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url" binding:"required"`
|
||||
Events string `json:"events" binding:"required"`
|
||||
Status string `json:"status"`
|
||||
RotateSecret bool `json:"rotate_secret"`
|
||||
}
|
||||
|
||||
@@ -373,10 +376,10 @@ func (h *MerchantHandler) CreateCallback(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
credential, err := h.callbackSvc.CreateSubscription(middleware.GetMerchantID(c), service.CreateCallbackInput{
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
Events: req.Events,
|
||||
Status: req.Status,
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
Events: req.Events,
|
||||
Status: req.Status,
|
||||
RotateSecret: req.RotateSecret,
|
||||
}, middleware.GetUserID(c))
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"affiliate_dash/internal/middleware"
|
||||
"affiliate_dash/internal/pkg/response"
|
||||
"affiliate_dash/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const uploadMaxBytes = 5 * 1024 * 1024 // 单张图片最大 5MB
|
||||
|
||||
// RechargeHandler 提供积分充值申请(商户侧)与审核入账(管理员侧)能力。
|
||||
type RechargeHandler struct {
|
||||
rechargeSvc *service.RechargeService
|
||||
}
|
||||
|
||||
func NewRechargeHandler(rechargeSvc *service.RechargeService) *RechargeHandler {
|
||||
return &RechargeHandler{rechargeSvc: rechargeSvc}
|
||||
}
|
||||
|
||||
type rechargeCreateReq struct {
|
||||
AmountCNY int64 `json:"amount_cny" binding:"required"` // 人民币,单位:分
|
||||
Vouchers []string `json:"vouchers" binding:"required"`
|
||||
Note string `json:"note"`
|
||||
}
|
||||
|
||||
// CreateApplication POST /api/merchant/recharge/applications
|
||||
func (h *RechargeHandler) CreateApplication(c *gin.Context) {
|
||||
var req rechargeCreateReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "参数错误:amount_cny 与 vouchers 必填")
|
||||
return
|
||||
}
|
||||
app, err := h.rechargeSvc.CreateRecharge(service.CreateRechargeInput{
|
||||
MerchantID: middleware.GetMerchantID(c),
|
||||
ActorUserID: middleware.GetUserID(c),
|
||||
AmountCNY: req.AmountCNY,
|
||||
Vouchers: req.Vouchers,
|
||||
Note: req.Note,
|
||||
})
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.OK(c, app)
|
||||
}
|
||||
|
||||
// ListApplications GET /api/merchant/recharge/applications(当前商户)
|
||||
func (h *RechargeHandler) ListApplications(c *gin.Context) {
|
||||
page, size := pageParams(c)
|
||||
list, total, err := h.rechargeSvc.ListRechargeApplications(middleware.GetMerchantID(c), page, size, c.Query("status"))
|
||||
if err != nil {
|
||||
response.ServerError(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.Page(c, list, total, page, size)
|
||||
}
|
||||
|
||||
// AdminListApplications GET /api/platform/recharge/applications(全部商户)
|
||||
func (h *RechargeHandler) AdminListApplications(c *gin.Context) {
|
||||
page, size := pageParams(c)
|
||||
list, total, err := h.rechargeSvc.ListAllRechargeApplications(page, size, c.Query("status"))
|
||||
if err != nil {
|
||||
response.ServerError(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.Page(c, list, total, page, size)
|
||||
}
|
||||
|
||||
type rechargeReviewReq struct {
|
||||
Approved bool `json:"approved" binding:"required"`
|
||||
ReviewNote string `json:"review_note"`
|
||||
}
|
||||
|
||||
// AdminReviewApplication POST /api/platform/recharge/applications/:id/review
|
||||
// 通过后由后端按 1 元 = 100 积分自动入账到商户钱包,幂等键为申请单号。
|
||||
func (h *RechargeHandler) AdminReviewApplication(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if id == 0 {
|
||||
response.BadRequest(c, "申请 ID 无效")
|
||||
return
|
||||
}
|
||||
var req rechargeReviewReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "参数错误:approved 必填")
|
||||
return
|
||||
}
|
||||
app, err := h.rechargeSvc.ReviewRecharge(service.ReviewRechargeInput{
|
||||
ApplicationID: uint(id),
|
||||
Approved: req.Approved,
|
||||
ReviewNote: req.ReviewNote,
|
||||
ActorUserID: middleware.GetUserID(c),
|
||||
})
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
h.rechargeSvc.CheckLowBalanceAndNotify(app.MerchantID)
|
||||
response.OK(c, app)
|
||||
}
|
||||
|
||||
// GetAlertConfig GET /api/merchant/recharge/alert-config
|
||||
func (h *RechargeHandler) GetAlertConfig(c *gin.Context) {
|
||||
cfg, err := h.rechargeSvc.GetAlertConfig(middleware.GetMerchantID(c))
|
||||
if err != nil {
|
||||
response.ServerError(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.OK(c, cfg)
|
||||
}
|
||||
|
||||
type alertConfigReq struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
ThresholdPoints int64 `json:"threshold_points"`
|
||||
WebhookURL string `json:"webhook_url"`
|
||||
}
|
||||
|
||||
// SaveAlertConfig PUT /api/merchant/recharge/alert-config
|
||||
func (h *RechargeHandler) SaveAlertConfig(c *gin.Context) {
|
||||
var req alertConfigReq
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
cfg, err := h.rechargeSvc.SaveAlertConfig(middleware.GetMerchantID(c), service.SaveAlertInput{
|
||||
Enabled: req.Enabled,
|
||||
ThresholdPoints: req.ThresholdPoints,
|
||||
WebhookURL: req.WebhookURL,
|
||||
})
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.OK(c, cfg)
|
||||
}
|
||||
|
||||
// Upload POST /api/upload 上传凭证图片(multipart 字段名 file)。
|
||||
func (h *RechargeHandler) Upload(c *gin.Context) {
|
||||
fileHeader, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
response.BadRequest(c, "请选择要上传的图片")
|
||||
return
|
||||
}
|
||||
file, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
response.BadRequest(c, "读取上传文件失败")
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
url, err := h.rechargeSvc.SaveUploadFile(file, fileHeader.Filename, uploadMaxBytes)
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"url": url})
|
||||
}
|
||||
Reference in New Issue
Block a user