2026-07-12 17:38:48 +08:00

163 lines
6.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 wallet
import (
"context"
"strings"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
"hyapp/services/wallet-service/internal/domain/ledger"
)
const (
defaultPointWithdrawalPointsPerUSD int64 = 100000
minPointWithdrawalGrossPoints int64 = 1000000
)
// FreezePointWithdrawal 把已启用 App 的收益积分从 available 冻结到 frozengateway 必须先读取已发布政策快照。
func (s *Service) FreezePointWithdrawal(ctx context.Context, command ledger.PointWithdrawalCommand) (ledger.PointWithdrawalReceipt, error) {
if err := normalizePointWithdrawalCommand(&command, false); err != nil {
return ledger.PointWithdrawalReceipt{}, err
}
if command.Reason == "" {
command.Reason = "point withdrawal freeze"
}
if s.repository == nil {
return ledger.PointWithdrawalReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
}
command.AppCode = appcode.Normalize(command.AppCode)
ctx = appcode.WithContext(ctx, command.AppCode)
receipt, err := s.repository.FreezeSalaryWithdrawal(ctx, pointWithdrawalAsSalaryCommand(command))
if err != nil {
return ledger.PointWithdrawalReceipt{}, err
}
return pointWithdrawalReceiptFromSalary(receipt), nil
}
// SettlePointWithdrawal 在财务审核通过时扣掉 frozen 积分;冻结时已扣 available因此这里不能再次扣 available。
func (s *Service) SettlePointWithdrawal(ctx context.Context, command ledger.PointWithdrawalCommand) (ledger.PointWithdrawalReceipt, error) {
if err := normalizePointWithdrawalCommand(&command, true); err != nil {
return ledger.PointWithdrawalReceipt{}, err
}
if command.Reason == "" {
command.Reason = "point withdrawal approved"
}
if s.repository == nil {
return ledger.PointWithdrawalReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
}
command.AppCode = appcode.Normalize(command.AppCode)
ctx = appcode.WithContext(ctx, command.AppCode)
receipt, err := s.repository.SettleSalaryWithdrawal(ctx, pointWithdrawalAsSalaryCommand(command))
if err != nil {
return ledger.PointWithdrawalReceipt{}, err
}
return pointWithdrawalReceiptFromSalary(receipt), nil
}
// ReleasePointWithdrawal 在审核拒绝或申请落库失败时把 frozen 积分返还 available保证用户不会被永久冻结。
func (s *Service) ReleasePointWithdrawal(ctx context.Context, command ledger.PointWithdrawalCommand) (ledger.PointWithdrawalReceipt, error) {
if err := normalizePointWithdrawalCommand(&command, false); err != nil {
return ledger.PointWithdrawalReceipt{}, err
}
if command.Reason == "" {
command.Reason = "point withdrawal released"
}
if s.repository == nil {
return ledger.PointWithdrawalReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
}
command.AppCode = appcode.Normalize(command.AppCode)
ctx = appcode.WithContext(ctx, command.AppCode)
receipt, err := s.repository.ReleaseSalaryWithdrawal(ctx, pointWithdrawalAsSalaryCommand(command))
if err != nil {
return ledger.PointWithdrawalReceipt{}, err
}
return pointWithdrawalReceiptFromSalary(receipt), nil
}
func normalizePointWithdrawalCommand(command *ledger.PointWithdrawalCommand, requireOperator bool) error {
if command == nil || strings.TrimSpace(command.CommandID) == "" || command.UserID <= 0 || command.GrossPointAmount <= 0 {
return xerr.New(xerr.InvalidArgument, "point withdrawal command is incomplete")
}
if requireOperator && command.OperatorUserID <= 0 {
return xerr.New(xerr.InvalidArgument, "operator_user_id is required")
}
command.CommandID = strings.TrimSpace(command.CommandID)
if strings.TrimSpace(command.AppCode) == "" {
return xerr.New(xerr.InvalidArgument, "app_code is required")
}
command.AppCode = appcode.Normalize(command.AppCode)
if command.AppCode != "huwaa" && command.AppCode != "fami" {
// Lalu 继续使用旧公会工资提现链路;只有显式迁移到 POINT 钱包的 App 才能调用本接口。
return xerr.New(xerr.InvalidArgument, "point withdrawal is not enabled for app")
}
command.AssetType = strings.ToUpper(strings.TrimSpace(command.AssetType))
if !ledger.ValidPointWithdrawalAssetType(command.AssetType) {
return xerr.New(xerr.InvalidArgument, "point asset_type is invalid")
}
if command.GrossPointAmount < minPointWithdrawalGrossPoints {
return xerr.New(xerr.InvalidArgument, "point withdrawal amount is below minimum")
}
if command.PointsPerUSD <= 0 {
command.PointsPerUSD = defaultPointWithdrawalPointsPerUSD
}
if command.FeeBPS < 0 || command.FeeBPS > 10000 {
return xerr.New(xerr.InvalidArgument, "point withdrawal fee_bps is invalid")
}
expectedFee := command.GrossPointAmount * command.FeeBPS / 10000
if command.FeePointAmount == 0 {
command.FeePointAmount = expectedFee
}
if command.FeePointAmount != expectedFee {
return xerr.New(xerr.InvalidArgument, "point withdrawal fee is invalid")
}
expectedNet := command.GrossPointAmount - command.FeePointAmount
if command.NetPointAmount == 0 {
command.NetPointAmount = expectedNet
}
if command.NetPointAmount != expectedNet || command.NetPointAmount < 0 {
return xerr.New(xerr.InvalidArgument, "point withdrawal net amount is invalid")
}
command.Reason = strings.TrimSpace(command.Reason)
command.WithdrawalRef = strings.TrimSpace(command.WithdrawalRef)
command.WithdrawalApplicationID = strings.TrimSpace(command.WithdrawalApplicationID)
if len(command.CommandID) > 128 || len(command.Reason) > 512 || len(command.WithdrawalRef) > 128 || len(command.WithdrawalApplicationID) > 64 {
return xerr.New(xerr.InvalidArgument, "point withdrawal text fields are too long")
}
return nil
}
func pointWithdrawalAsSalaryCommand(command ledger.PointWithdrawalCommand) ledger.SalaryWithdrawalCommand {
return ledger.SalaryWithdrawalCommand{
AppCode: command.AppCode,
CommandID: command.CommandID,
UserID: command.UserID,
SalaryAssetType: command.AssetType,
SalaryUSDMinor: command.GrossPointAmount,
PointFeeAmount: command.FeePointAmount,
PointNetAmount: command.NetPointAmount,
PointsPerUSD: command.PointsPerUSD,
PointWithdrawFeeBPS: command.FeeBPS,
OperatorUserID: command.OperatorUserID,
Reason: command.Reason,
WithdrawalRef: command.WithdrawalRef,
WithdrawalApplicationID: command.WithdrawalApplicationID,
}
}
func pointWithdrawalReceiptFromSalary(receipt ledger.SalaryWithdrawalReceipt) ledger.PointWithdrawalReceipt {
return ledger.PointWithdrawalReceipt{
TransactionID: receipt.TransactionID,
UserID: receipt.UserID,
AssetType: receipt.SalaryAssetType,
GrossPointAmount: receipt.SalaryUSDMinor,
FeePointAmount: receipt.PointFeeAmount,
NetPointAmount: receipt.PointNetAmount,
PointsPerUSD: receipt.PointsPerUSD,
FeeBPS: receipt.PointWithdrawFeeBPS,
AvailableAfter: receipt.AvailableAfter,
FrozenAfter: receipt.FrozenAfter,
Version: receipt.Version,
CreatedAtMS: receipt.CreatedAtMS,
}
}