tokenFactory/model/distributor_withdrawal.go

457 lines
15 KiB
Go
Raw Permalink 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 model
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"time"
"github.com/QuantumNous/new-api/common"
"gorm.io/gorm"
)
var distWithdrawMonthRe = regexp.MustCompile(`^\d{4}-(0[1-9]|1[0-2])$`)
// 分销商线下提现申请状态
const (
DistWithdrawStatusPending = 1 // 提现中(待审核)
DistWithdrawStatusApproved = 2 // 提现成功
DistWithdrawStatusRejected = 3 // 提现失败(驳回)
DistWithdrawStatusCancelled = 4 // 已取消
)
// DistributorWithdrawalProfile 提现扩展资料(存 profile_data JSON
type DistributorWithdrawalProfile struct {
// 个人
IdCardNo string `json:"id_card_no,omitempty"`
IdCardExpiry string `json:"id_card_expiry,omitempty"`
Mobile string `json:"mobile,omitempty"`
BankReservedPhone string `json:"bank_reserved_phone,omitempty"`
IdCardFrontUrl string `json:"id_card_front_url,omitempty"`
IdCardBackUrl string `json:"id_card_back_url,omitempty"`
BankCardPhotoUrl string `json:"bank_card_photo_url,omitempty"`
// 企业
CreditCode string `json:"credit_code,omitempty"`
LegalPersonName string `json:"legal_person_name,omitempty"`
LegalPersonPhone string `json:"legal_person_phone,omitempty"`
BankBranchCode string `json:"bank_branch_code,omitempty"`
ContactPerson string `json:"contact_person,omitempty"`
BusinessLicenseUrl string `json:"business_license_url,omitempty"`
CorporateAccountProofUrl string `json:"corporate_account_proof_url,omitempty"`
InvoiceUrl string `json:"invoice_url,omitempty"`
}
// DistributorWithdrawal 分销商线下提现申请(提交后暂扣 aff_quota驳回/取消退回)
type DistributorWithdrawal struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
UserId int `json:"user_id" gorm:"not null;index:idx_dist_wd_user"`
AccountType int `json:"account_type" gorm:"type:int;not null;default:1;column:account_type"` // 1=个人 2=企业
RealName string `json:"real_name" gorm:"type:varchar(64);not null;column:real_name"`
BankName string `json:"bank_name" gorm:"type:varchar(128);not null;column:bank_name"`
BankAccount string `json:"bank_account" gorm:"type:varchar(64);not null;column:bank_account"`
ProfileData string `json:"profile_data" gorm:"type:text;column:profile_data"`
VoucherUrls string `json:"voucher_urls" gorm:"type:text;column:voucher_urls"` // 历史票据 JSON新单可为 []
WithdrawMonth string `json:"withdraw_month" gorm:"type:varchar(16);not null;column:withdraw_month"` // YYYY-MM
QuotaAmount int `json:"quota_amount" gorm:"not null;column:quota_amount"`
Status int `json:"status" gorm:"not null;default:1;index:idx_dist_wd_status"`
RejectReason string `json:"reject_reason" gorm:"type:varchar(512);column:reject_reason"`
ReviewerId int `json:"reviewer_id" gorm:"column:reviewer_id"`
ReviewedAt int64 `json:"reviewed_at" gorm:"column:reviewed_at"`
CancelledAt int64 `json:"cancelled_at" gorm:"column:cancelled_at"`
CreatedAt int64 `json:"created_at" gorm:"autoCreateTime;bigint;index"`
UpdatedAt int64 `json:"updated_at" gorm:"autoUpdateTime;bigint"`
}
func (DistributorWithdrawal) TableName() string {
return "distributor_withdrawals"
}
// GetDistributorWithdrawalByID 按主键查询提现记录。
func GetDistributorWithdrawalByID(id int) (*DistributorWithdrawal, error) {
if id <= 0 {
return nil, errors.New("invalid id")
}
var w DistributorWithdrawal
if err := DB.Where("id = ?", id).First(&w).Error; err != nil {
return nil, err
}
return &w, nil
}
// GetDistributorMinWithdrawQuota 最低提现额度(内部点数),未配置时与 QuotaPerUnit 一致(约等于展示 1 单位)
func GetDistributorMinWithdrawQuota() int {
common.OptionMapRWMutex.RLock()
raw := common.Interface2String(common.OptionMap["DistributorMinWithdrawQuota"])
common.OptionMapRWMutex.RUnlock()
raw = strings.TrimSpace(raw)
if raw == "" {
return int(common.QuotaPerUnit)
}
n, err := strconv.Atoi(raw)
if err != nil || n <= 0 {
return int(common.QuotaPerUnit)
}
return n
}
func distWithdrawRefundAffQuota(tx *gorm.DB, userId int, quota int) error {
if userId <= 0 || quota <= 0 {
return nil
}
res := tx.Model(&User{}).Where("id = ?", userId).UpdateColumn("aff_quota", gorm.Expr("aff_quota + ?", quota))
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return fmt.Errorf("user not found: %d", userId)
}
return nil
}
func distWithdrawRequireURL(label, u string) error {
if strings.TrimSpace(u) == "" {
return fmt.Errorf("请上传%s", label)
}
return nil
}
func normalizeDistributorWithdrawalProfile(accountType int, p *DistributorWithdrawalProfile) (string, error) {
if p == nil {
return "", errors.New("资料无效")
}
trim := func(s *string) {
*s = strings.TrimSpace(*s)
}
switch accountType {
case DistributorApplyTypePersonal:
trim(&p.IdCardNo)
trim(&p.IdCardExpiry)
trim(&p.Mobile)
trim(&p.BankReservedPhone)
trim(&p.IdCardFrontUrl)
trim(&p.IdCardBackUrl)
trim(&p.BankCardPhotoUrl)
trim(&p.InvoiceUrl)
if p.IdCardNo == "" {
return "", errors.New("请填写身份证号")
}
if p.IdCardExpiry == "" {
return "", errors.New("请填写身份证有效期")
}
if p.Mobile == "" {
return "", errors.New("请填写手机号")
}
if p.BankReservedPhone == "" {
return "", errors.New("请填写银行预留手机号")
}
for _, pair := range []struct {
label string
u string
}{
{"身份证正面", p.IdCardFrontUrl},
{"身份证反面", p.IdCardBackUrl},
{"银行卡照", p.BankCardPhotoUrl},
{"发票", p.InvoiceUrl},
} {
if err := distWithdrawRequireURL(pair.label, pair.u); err != nil {
return "", err
}
}
case DistributorApplyTypeEnterprise:
trim(&p.CreditCode)
trim(&p.LegalPersonName)
trim(&p.LegalPersonPhone)
trim(&p.BankBranchCode)
trim(&p.ContactPerson)
trim(&p.BusinessLicenseUrl)
trim(&p.CorporateAccountProofUrl)
trim(&p.InvoiceUrl)
if p.CreditCode == "" {
return "", errors.New("请填写统一社会信用代码")
}
if p.LegalPersonName == "" {
return "", errors.New("请填写法人姓名")
}
if p.LegalPersonPhone == "" {
return "", errors.New("请填写法人手机号")
}
if p.BankBranchCode == "" {
return "", errors.New("请填写联行号")
}
if p.ContactPerson == "" {
return "", errors.New("请填写联系人")
}
for _, pair := range []struct {
label string
u string
}{
{"营业执照", p.BusinessLicenseUrl},
{"对公账户证明", p.CorporateAccountProofUrl},
{"发票", p.InvoiceUrl},
} {
if err := distWithdrawRequireURL(pair.label, pair.u); err != nil {
return "", err
}
}
default:
return "", errors.New("账户类型无效")
}
b, err := common.Marshal(p)
if err != nil {
return "", err
}
return string(b), nil
}
// ParseDistributorWithdrawalProfile 解析 profile_data
func ParseDistributorWithdrawalProfile(raw string) DistributorWithdrawalProfile {
raw = strings.TrimSpace(raw)
if raw == "" {
return DistributorWithdrawalProfile{}
}
var p DistributorWithdrawalProfile
_ = common.UnmarshalJsonStr(raw, &p)
return p
}
// CreateDistributorWithdrawal 提交提现:校验最低额度与余额,暂扣 aff_quota
func CreateDistributorWithdrawal(userId, accountType int, realName, bankName, bankAccount, profileJSON, voucherUrlsJSON, withdrawMonth string, quotaAmount int) error {
if accountType != DistributorApplyTypePersonal && accountType != DistributorApplyTypeEnterprise {
return errors.New("账户类型无效")
}
realName = strings.TrimSpace(realName)
bankName = strings.TrimSpace(bankName)
bankAccount = strings.TrimSpace(bankAccount)
withdrawMonth = strings.TrimSpace(withdrawMonth)
profileJSON = strings.TrimSpace(profileJSON)
voucherUrlsJSON = strings.TrimSpace(voucherUrlsJSON)
if realName == "" {
if accountType == DistributorApplyTypeEnterprise {
return errors.New("请填写企业名称")
}
return errors.New("请填写姓名")
}
if bankName == "" {
return errors.New("请填写开户行")
}
if bankAccount == "" {
if accountType == DistributorApplyTypeEnterprise {
return errors.New("请填写对公卡号")
}
return errors.New("请填写银行卡号")
}
var profile DistributorWithdrawalProfile
if profileJSON != "" {
if err := common.UnmarshalJsonStr(profileJSON, &profile); err != nil {
return errors.New("资料格式无效")
}
}
normProfile, err := normalizeDistributorWithdrawalProfile(accountType, &profile)
if err != nil {
return err
}
profileJSON = normProfile
if voucherUrlsJSON == "" {
voucherUrlsJSON = "[]"
}
if withdrawMonth == "" {
withdrawMonth = time.Now().Format("2006-01")
} else if !distWithdrawMonthRe.MatchString(withdrawMonth) {
return errors.New("提现月份格式应为 YYYY-MM")
}
if quotaAmount <= 0 {
return errors.New("提现额度无效")
}
u, err := GetUserById(userId, false)
if err != nil {
return err
}
if !UserIsDistributor(u) {
return errors.New("仅分销商可申请提现")
}
if u.AffQuota < quotaAmount {
return errors.New("待使用收益不足")
}
minQ := GetDistributorMinWithdrawQuota()
// 余额达到系统最低提现额度时,不得低于该门槛;余额不足该门槛时,允许在 1当前余额之间提现
if u.AffQuota >= minQ {
if quotaAmount < minQ {
return fmt.Errorf("提现额度不能低于系统下限")
}
} else {
if quotaAmount < 1 || quotaAmount > u.AffQuota {
return errors.New("提现额度须在 1 与当前待使用余额之间")
}
}
ts := common.GetTimestamp()
return DB.Transaction(func(tx *gorm.DB) error {
res := tx.Model(&User{}).Where("id = ? AND aff_quota >= ?", userId, quotaAmount).
UpdateColumn("aff_quota", gorm.Expr("aff_quota - ?", quotaAmount))
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return errors.New("待使用收益不足")
}
w := DistributorWithdrawal{
UserId: userId,
AccountType: accountType,
RealName: realName,
BankName: bankName,
BankAccount: bankAccount,
ProfileData: profileJSON,
VoucherUrls: voucherUrlsJSON,
WithdrawMonth: withdrawMonth,
QuotaAmount: quotaAmount,
Status: DistWithdrawStatusPending,
CreatedAt: ts,
UpdatedAt: ts,
}
return tx.Create(&w).Error
})
}
// CancelDistributorWithdrawal 用户取消待审核申请,退回 aff_quota
func CancelDistributorWithdrawal(userId, withdrawalId int) error {
return DB.Transaction(func(tx *gorm.DB) error {
var w DistributorWithdrawal
if err := tx.Set("gorm:query_option", "FOR UPDATE").Where("id = ?", withdrawalId).First(&w).Error; err != nil {
return errors.New("记录不存在")
}
if w.UserId != userId {
return errors.New("无权操作")
}
if w.Status != DistWithdrawStatusPending {
return errors.New("当前状态不可取消")
}
if err := distWithdrawRefundAffQuota(tx, userId, w.QuotaAmount); err != nil {
return err
}
ts := common.GetTimestamp()
return tx.Model(&DistributorWithdrawal{}).Where("id = ?", withdrawalId).Updates(map[string]interface{}{
"status": DistWithdrawStatusCancelled,
"cancelled_at": ts,
"updated_at": ts,
}).Error
})
}
// ListDistributorWithdrawals 当前用户提现记录
func ListDistributorWithdrawals(userId int, pageInfo *common.PageInfo) ([]DistributorWithdrawal, int64, error) {
if userId <= 0 {
return nil, 0, errors.New("invalid user")
}
var total int64
base := DB.Model(&DistributorWithdrawal{}).Where("user_id = ?", userId)
if err := base.Count(&total).Error; err != nil {
return nil, 0, err
}
var rows []DistributorWithdrawal
err := base.Order("id DESC").
Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&rows).Error
if err != nil {
return nil, 0, err
}
if rows == nil {
rows = []DistributorWithdrawal{}
}
return rows, total, nil
}
type DistributorWithdrawalAdminRow struct {
DistributorWithdrawal
Username string `json:"username"`
}
// ListDistributorWithdrawalsAdmin 管理端列表
func ListDistributorWithdrawalsAdmin(status, accountType int, keyword string, pageInfo *common.PageInfo) ([]DistributorWithdrawalAdminRow, int64, error) {
base := DB.Model(&DistributorWithdrawal{})
if status > 0 {
base = base.Where("status = ?", status)
}
if accountType == DistributorApplyTypePersonal || accountType == DistributorApplyTypeEnterprise {
base = base.Where("account_type = ?", accountType)
}
if kw := strings.TrimSpace(keyword); kw != "" {
like := "%" + kw + "%"
base = base.Where(
"real_name LIKE ? OR bank_account LIKE ? OR profile_data LIKE ? OR user_id IN (SELECT id FROM users WHERE username LIKE ?)",
like, like, like, like,
)
}
var total int64
if err := base.Count(&total).Error; err != nil {
return nil, 0, err
}
var list []DistributorWithdrawal
err := base.Order("id DESC").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&list).Error
if err != nil {
return nil, 0, err
}
out := make([]DistributorWithdrawalAdminRow, 0, len(list))
for i := range list {
row := DistributorWithdrawalAdminRow{DistributorWithdrawal: list[i]}
var u User
if e := DB.Select("username").Where("id = ?", list[i].UserId).First(&u).Error; e == nil {
row.Username = u.Username
}
out = append(out, row)
}
return out, total, nil
}
// ApproveDistributorWithdrawalAdmin 审核通过(额度已在提交时扣除,此处仅改状态)
func ApproveDistributorWithdrawalAdmin(withdrawalId, reviewerId int) error {
return DB.Transaction(func(tx *gorm.DB) error {
var w DistributorWithdrawal
if err := tx.Set("gorm:query_option", "FOR UPDATE").Where("id = ?", withdrawalId).First(&w).Error; err != nil {
return errors.New("记录不存在")
}
if w.Status != DistWithdrawStatusPending {
return errors.New("当前状态不可审核")
}
ts := common.GetTimestamp()
return tx.Model(&DistributorWithdrawal{}).Where("id = ?", withdrawalId).Updates(map[string]interface{}{
"status": DistWithdrawStatusApproved,
"reviewer_id": reviewerId,
"reviewed_at": ts,
"updated_at": ts,
}).Error
})
}
// RejectDistributorWithdrawalAdmin 驳回:退回 aff_quota
func RejectDistributorWithdrawalAdmin(withdrawalId, reviewerId int, reason string) error {
reason = strings.TrimSpace(reason)
if reason == "" {
return errors.New("请填写驳回原因")
}
if len(reason) > 500 {
reason = reason[:500]
}
return DB.Transaction(func(tx *gorm.DB) error {
var w DistributorWithdrawal
if err := tx.Set("gorm:query_option", "FOR UPDATE").Where("id = ?", withdrawalId).First(&w).Error; err != nil {
return errors.New("记录不存在")
}
if w.Status != DistWithdrawStatusPending {
return errors.New("当前状态不可审核")
}
if err := distWithdrawRefundAffQuota(tx, w.UserId, w.QuotaAmount); err != nil {
return err
}
ts := common.GetTimestamp()
return tx.Model(&DistributorWithdrawal{}).Where("id = ?", withdrawalId).Updates(map[string]interface{}{
"status": DistWithdrawStatusRejected,
"reject_reason": reason,
"reviewer_id": reviewerId,
"reviewed_at": ts,
"updated_at": ts,
}).Error
})
}