hyapp-server/server/admin/internal/repository/withdrawal_application_repository.go

158 lines
5.9 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 repository
import (
"errors"
"strings"
"hyapp-admin-server/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
var ErrWithdrawalApplicationAlreadyAudited = errors.New("withdrawal application already audited")
type WithdrawalApplicationListOptions struct {
Page int
PageSize int
AppCode string
Keyword string
}
type WithdrawalApplicationAuditInput struct {
Decision string
ApproverUserID uint
ApproverName string
AuditRemark string
AuditImageURL string
AuditCommandID string
AuditTransactionID string
ApprovedAtMS int64
}
func (s *Store) GetWithdrawalApplication(id uint) (*model.UserWithdrawalApplication, error) {
var application model.UserWithdrawalApplication
if err := s.db.First(&application, id).Error; err != nil {
return nil, err
}
return &application, nil
}
func (s *Store) GetWithdrawalApplicationForApp(appCode string, id uint) (*model.UserWithdrawalApplication, error) {
var application model.UserWithdrawalApplication
if err := s.db.Where("app_code = ? AND id = ?", strings.TrimSpace(appCode), id).First(&application).Error; err != nil {
return nil, err
}
return &application, nil
}
func (s *Store) ListWithdrawalApplications(options WithdrawalApplicationListOptions) ([]model.UserWithdrawalApplication, int64, error) {
page, pageSize := normalizePage(options.Page, options.PageSize)
query := s.db.Model(&model.UserWithdrawalApplication{})
query = applyWithdrawalApplicationFilters(query, options)
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
var applications []model.UserWithdrawalApplication
// 提现申请是财务审核场景默认按最新申请优先展示id 作为同毫秒写入时的稳定次序,避免分页翻页时记录抖动。
err := query.
Order("created_at_ms DESC, id DESC").
Limit(pageSize).
Offset((page - 1) * pageSize).
Find(&applications).Error
return applications, total, err
}
func (s *Store) AuditWithdrawalApplication(id uint, input WithdrawalApplicationAuditInput) (*model.UserWithdrawalApplication, error) {
return s.AuditWithdrawalApplicationForApp("", id, input)
}
func (s *Store) AuditWithdrawalApplicationForApp(appCode string, id uint, input WithdrawalApplicationAuditInput) (*model.UserWithdrawalApplication, error) {
if id == 0 {
return nil, errors.New("withdrawal application id is required")
}
if input.Decision != model.WithdrawalApplicationStatusApproved && input.Decision != model.WithdrawalApplicationStatusRejected {
return nil, errors.New("withdrawal application decision is invalid")
}
var application model.UserWithdrawalApplication
err := s.db.Transaction(func(tx *gorm.DB) error {
// 审核只能从 pending 进入终态;行锁阻止两个财务同时覆盖同一张提现单的执行结果。
query := tx.Clauses(clause.Locking{Strength: "UPDATE"})
if strings.TrimSpace(appCode) != "" {
// 后台按当前 App 上下文审核,不能只凭全局自增 ID 跨 App 锁定和审核提现单。
query = query.Where("app_code = ?", strings.TrimSpace(appCode))
}
if err := query.First(&application, id).Error; err != nil {
return err
}
if application.Status != model.WithdrawalApplicationStatusPending {
return ErrWithdrawalApplicationAlreadyAudited
}
return tx.Model(&application).Updates(map[string]any{
"status": input.Decision,
"approver_user_id": input.ApproverUserID,
"approver_name": strings.TrimSpace(input.ApproverName),
"audit_remark": strings.TrimSpace(input.AuditRemark),
"audit_image_url": strings.TrimSpace(input.AuditImageURL),
"audit_command_id": strings.TrimSpace(input.AuditCommandID),
"audit_transaction_id": strings.TrimSpace(input.AuditTransactionID),
"approved_at_ms": input.ApprovedAtMS,
"updated_at_ms": input.ApprovedAtMS,
}).Error
})
if err != nil {
return nil, err
}
if strings.TrimSpace(appCode) != "" {
return s.GetWithdrawalApplicationForApp(appCode, id)
}
return s.GetWithdrawalApplication(id)
}
// ApprovedWithdrawalStats 按审批通过时间聚合用户提现申请的 USDT 金额。
// 旧工资提现用 withdraw_amount_minor 保存美分Huwaa POINT 提现该字段保存 gross points因此必须用 withdraw_amount 的 USD 展示值折算。
type ApprovedWithdrawalStats struct {
ApprovedCount int64
ApprovedUSDMinor int64
}
func (s *Store) ApprovedWithdrawalStats(appCode string, startAtMS int64, endAtMS int64) (ApprovedWithdrawalStats, error) {
query := s.db.Model(&model.UserWithdrawalApplication{}).
Where("status = ?", model.WithdrawalApplicationStatusApproved)
if appCode = strings.TrimSpace(appCode); appCode != "" {
query = query.Where("app_code = ?", appCode)
}
if startAtMS > 0 {
query = query.Where("approved_at_ms >= ?", startAtMS)
}
if endAtMS > 0 {
query = query.Where("approved_at_ms < ?", endAtMS)
}
var stats ApprovedWithdrawalStats
row := query.Select(`
COUNT(*),
COALESCE(SUM(CASE
WHEN salary_asset_type IN ('POINT', 'COIN_SELLER_POINT') THEN CAST(withdraw_amount * 100 AS SIGNED)
ELSE withdraw_amount_minor
END), 0)`).Row()
if err := row.Scan(&stats.ApprovedCount, &stats.ApprovedUSDMinor); err != nil {
return ApprovedWithdrawalStats{}, err
}
return stats, nil
}
func applyWithdrawalApplicationFilters(query *gorm.DB, options WithdrawalApplicationListOptions) *gorm.DB {
if appCode := strings.TrimSpace(options.AppCode); appCode != "" {
query = query.Where("app_code = ?", appCode)
}
if keyword := strings.TrimSpace(options.Keyword); keyword != "" {
like := "%" + keyword + "%"
// 关键词面向财务排查,覆盖用户、收款方式、收款地址和审批人;不做金额模糊,避免 decimal 隐式转换拖慢常规查询。
query = query.Where("user_id LIKE ? OR withdraw_method LIKE ? OR withdraw_address LIKE ? OR approver_name LIKE ? OR CAST(id AS CHAR) LIKE ?", like, like, like, like, like)
}
return query
}