219 lines
7.4 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 financewithdrawal
import (
"errors"
"strconv"
"strings"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/integration/activityclient"
"hyapp-admin-server/internal/integration/walletclient"
"hyapp-admin-server/internal/integration/withdrawalsource"
"hyapp-admin-server/internal/middleware"
"hyapp-admin-server/internal/modules/shared"
"hyapp-admin-server/internal/repository"
"hyapp-admin-server/internal/response"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type Handler struct {
service *Service
audit shared.OperationLogger
sources *withdrawalsource.Registry
}
func New(store *repository.Store, wallet walletclient.Client, activity activityclient.Client, audit shared.OperationLogger) *Handler {
return NewWithSources(store, wallet, activity, audit, nil)
}
func NewWithSources(store *repository.Store, wallet walletclient.Client, activity activityclient.Client, audit shared.OperationLogger, sources *withdrawalsource.Registry) *Handler {
return &Handler{service: NewServiceWithSources(store, wallet, activity, sources), audit: audit, sources: sources}
}
// CreateExternalApplication 是来源钱包的服务间入口,不使用后台 JWT。
// 每个 App 使用独立 submit token且业务唯一键保证来源端持久重试不会生成重复审核单。
func (h *Handler) CreateExternalApplication(c *gin.Context) {
var req struct {
AppCode string `json:"appCode" binding:"required"`
SourceSystem string `json:"sourceSystem" binding:"required"`
SourceApplicationID string `json:"sourceApplicationId" binding:"required"`
UserID string `json:"userId" binding:"required"`
SalaryAssetType string `json:"salaryAssetType" binding:"required"`
WithdrawAmount string `json:"withdrawAmount" binding:"required"`
WithdrawAmountMinor int64 `json:"withdrawAmountMinor" binding:"required"`
WithdrawMethod string `json:"withdrawMethod" binding:"required"`
WithdrawAddress string `json:"withdrawAddress" binding:"required"`
CreatedAtMS int64 `json:"createdAtMs"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "提现申请参数不正确")
return
}
if h.sources == nil || !h.sources.AuthenticateSubmit(req.AppCode, req.SourceSystem, c.GetHeader("Authorization")) {
response.Unauthorized(c, "提现来源鉴权失败")
return
}
item, err := h.service.CreateExternalApplication(externalWithdrawalApplicationInput{
AppCode: req.AppCode,
SourceSystem: req.SourceSystem,
SourceApplicationID: req.SourceApplicationID,
UserID: req.UserID,
SalaryAssetType: req.SalaryAssetType,
WithdrawAmount: req.WithdrawAmount,
WithdrawAmountMinor: req.WithdrawAmountMinor,
WithdrawMethod: req.WithdrawMethod,
WithdrawAddress: req.WithdrawAddress,
CreatedAtMS: req.CreatedAtMS,
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.Created(c, item)
}
func (h *Handler) ListApplications(c *gin.Context) {
h.listApplications(c, false)
}
func (h *Handler) ListOperationsApplications(c *gin.Context) {
h.listApplications(c, true)
}
func (h *Handler) listApplications(c *gin.Context, operations bool) {
options := shared.ListOptions(c)
listOptions := repository.WithdrawalApplicationListOptions{
Page: options.Page,
PageSize: options.PageSize,
AppCode: firstQuery(c, "app_code", "appCode"),
Keyword: options.Keyword,
}
var (
items []withdrawalApplicationDTO
total int64
err error
)
actor := shared.ActorFromContext(c)
if operations {
items, total, err = h.service.ListOperationsApplications(actor, listOptions)
} else {
items, total, err = h.service.ListApplications(actor, listOptions)
}
if err != nil {
if errors.Is(err, errWithdrawalMoneyScopeForbidden) {
response.Forbidden(c, err.Error())
return
}
response.ServerError(c, "获取用户提现申请列表失败")
return
}
response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: total})
}
func (h *Handler) ApproveApplication(c *gin.Context) {
h.auditApplication(c, "approved", false)
}
func (h *Handler) RejectApplication(c *gin.Context) {
h.auditApplication(c, "rejected", false)
}
func (h *Handler) ApproveOperationsApplication(c *gin.Context) {
h.auditApplication(c, "approved", true)
}
func (h *Handler) RejectOperationsApplication(c *gin.Context) {
h.auditApplication(c, "rejected", true)
}
func (h *Handler) auditApplication(c *gin.Context, decision string, operations bool) {
id, ok := shared.ParseID(c, "application_id")
if !ok {
return
}
if strings.TrimSpace(c.GetHeader(appctx.HeaderAppCode)) == "" {
// 财务工作台可跨 App 列表,审核时必须由前端显式选中目标行 App不能让 appctx 的 lalu 默认值静默决定资金数据域。
response.BadRequest(c, "缺少 "+appctx.HeaderAppCode)
return
}
var req struct {
AuditRemark string `json:"auditRemark"`
AuditImageURL string `json:"auditImageUrl"`
AuditImageURLSnake string `json:"audit_image_url"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "审核参数不正确")
return
}
var (
item *withdrawalApplicationDTO
err error
)
if operations && decision == "approved" {
item, err = h.service.ApproveOperationsApplication(c.Request.Context(), shared.ActorFromContext(c), id, req.AuditRemark, middleware.CurrentRequestID(c))
} else if operations {
item, err = h.service.RejectOperationsApplication(c.Request.Context(), shared.ActorFromContext(c), id, req.AuditRemark, middleware.CurrentRequestID(c))
} else if decision == "approved" {
item, err = h.service.ApproveApplication(c.Request.Context(), shared.ActorFromContext(c), id, req.AuditRemark, firstNonEmpty(req.AuditImageURL, req.AuditImageURLSnake), middleware.CurrentRequestID(c))
} else {
item, err = h.service.RejectApplication(c.Request.Context(), shared.ActorFromContext(c), id, req.AuditRemark, middleware.CurrentRequestID(c))
}
if err != nil {
writeWithdrawalServiceError(c, err)
return
}
action := "audit-user-withdrawal-finance"
if operations {
action = "audit-user-withdrawal-operations"
}
shared.OperationLogWithResourceID(c, h.audit, action, "admin_user_withdrawal_applications", strconv.FormatUint(uint64(item.ID), 10), "success", decision)
response.OK(c, item)
}
func writeWithdrawalServiceError(c *gin.Context, err error) {
if errors.Is(err, gorm.ErrRecordNotFound) {
response.NotFound(c, "用户提现申请不存在")
return
}
if errors.Is(err, repository.ErrWithdrawalApplicationAlreadyAudited) {
response.BadRequest(c, "提现申请已审核")
return
}
if errors.Is(err, repository.ErrWithdrawalApplicationStageNotReviewable) {
response.BadRequest(c, "提现申请尚未进入当前审核阶段")
return
}
if errors.Is(err, errWithdrawalMoneyScopeForbidden) {
response.Forbidden(c, err.Error())
return
}
switch strings.TrimSpace(err.Error()) {
case "没有操作权限":
response.Forbidden(c, err.Error())
case "admin finance withdrawal service is not configured":
response.ServerError(c, "审核用户提现申请失败")
default:
response.BadRequest(c, err.Error())
}
}
func firstQuery(c *gin.Context, keys ...string) string {
for _, key := range keys {
if value := strings.TrimSpace(c.Query(key)); value != "" {
return value
}
}
return ""
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if trimmed := strings.TrimSpace(value); trimmed != "" {
return trimmed
}
}
return ""
}