158 lines
5.9 KiB
Go
158 lines
5.9 KiB
Go
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
|
||
}
|