// Package models 定义钱包相关的数据模型和数据库操作 package models import ( "errors" "fmt" "time" "bsm/full/module/finance/wallet/internal/impl" "git.apinb.com/bsm-sdk/core/errcode" "git.apinb.com/bsm-sdk/core/types" "git.apinb.com/bsm-sdk/core/utils" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "gorm.io/gorm" ) // FillRecordYmd 填充流水的年月日与年月,供当日/当月统计使用 func FillRecordYmd(record *WalletRecord) { now := time.Now() record.Ymd = int32(now.Year()*10000 + int(now.Month())*100 + now.Day()) record.Ym = int32(now.Year()*100 + int(now.Month())) } // InitData 初始化默认钱包数据 func InitData() error { var ( cnt int64 err error userIdentity string ) // 获取默认用户唯一标识 err = impl.DBService.Table("passport_account").Select("identity").Where("account=?", "demo").Find(&userIdentity).Error if err != nil { return err } if userIdentity == "" { return errors.New("默认用户未找到") } // 检查是否已经创建了钱包信息 err = impl.DBService.Model(&WalletBasic{}).Where("passport_identity=?", userIdentity).Count(&cnt).Error if err != nil { return err } // 如果钱包不存在,则创建默认钱包 if cnt == 0 { pa := WalletBasic{ Balance: 0, WithdrawalBalance: 0, Std_Identity: types.Std_Identity{Identity: utils.ULID()}, Std_Passport: types.Std_Passport{PassportIdentity: userIdentity}, } err = impl.DBService.Create(&pa).Error if err != nil { return err } } return nil } // GetWalletBalanceByPassportIdentity 根据用户身份获取钱包余额 func GetWalletBalanceByPassportIdentity(passport_identity string) float64 { wallet := new(WalletBasic) err := impl.DBService.Where("passport_identity = ?", passport_identity).First(wallet).Error if err != nil { return 0.00 } return float64(wallet.Balance / 100) } // WalletExists 检查钱包是否存在 func WalletExists(passportIdentity string) *WalletBasic { wallet := new(WalletBasic) err := impl.DBService.Where("passport_identity = ?", passportIdentity).First(wallet).Error if err != nil { return nil } return wallet } // GetWalletByPassportIdentity 根据用户身份获取钱包,如果不存在则创建 func GetWalletByPassportIdentity(passport_id uint, passport_identity string) (*WalletBasic, error) { wallet := new(WalletBasic) err := impl.DBService.Where("passport_identity = ?", passport_identity).First(wallet).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { // 钱包不存在,创建新钱包 wallet.Identity = utils.UUID() wallet.PassportID = passport_id wallet.PassportIdentity = passport_identity impl.DBService.Create(wallet) return wallet, nil } else { return nil, err } } return wallet, nil } // GetWalletByPassportIdentityT 根据用户身份获取钱包(不创建) func GetWalletByPassportIdentityT(passport_id int64, passport_identity string) (*WalletBasic, error) { wallet := &WalletBasic{} err := impl.DBService.Where("passport_identity = ?", passport_identity).First(wallet).Error return wallet, err } // GetTransPriceByOrderNo 根据订单号获取交易价格 func GetTransPriceByOrderNo(no string) (int64, error) { var amount int64 impl.DBService.Select("trans_price").Table("order_summary").Where("order_no=? and status=1", no).Scan(&amount) if amount == 0 { return 0, status.Error(codes.Internal, "Order is close.") } return amount, nil } // GetWalletPaymentByIdentity 根据身份和支付ID获取钱包支付记录 func GetWalletPaymentByIdentity(passport_identity, identity string) (*WalletPayment, error) { fp := new(WalletPayment) err := impl.DBService.Where("passport_identity=? and identity=?", passport_identity, identity).First(&fp).Error return fp, err } // MarkPaymentFailed 将钱包支付单标记为支付失败 // 仅允许从「创建(0)/支付中(1)」流转到「失败(-1)」,并限定支付单归属,保证幂等且不可越权。 func MarkPaymentFailed(passport_identity, identity, msg string) error { res := impl.DBService.Model(&WalletPayment{}). Where("passport_identity = ? AND identity = ? AND status IN (0,1)", passport_identity, identity). UpdateColumns(map[string]interface{}{"status": -1, "callback_msg": msg}) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return errcode.ErrNotFound(404, "payment") } return nil } // SettlePaymentSuccess 渠道支付成功结算 // 按商户订单号反查支付单 → 核对渠道金额 → 条件更新为成功(幂等)→ // 同一事务内完成余额入账与流水写入。仅充值单(type=1)增加余额。 // 只有经过验签解密的渠道回调才允许调用本函数。 func SettlePaymentSuccess(outTradeNo, tradeNo string, amount int64) error { if outTradeNo == "" { return errors.New("out_trade_no is empty") } if amount <= 0 { return errors.New("amount is invalid") } return impl.DBService.Transaction(func(tx *gorm.DB) error { payment := new(WalletPayment) if err := tx.Where("order_no = ?", outTradeNo).First(payment).Error; err != nil { return err } // 金额核对:渠道返回金额必须与本地下单金额一致,防止金额篡改 if payment.Amount != amount { return fmt.Errorf("payment amount mismatch: local=%d channel=%d", payment.Amount, amount) } // 条件更新:重复回调时 RowsAffected 为 0,直接返回保证幂等 res := tx.Model(&WalletPayment{}). Where("identity = ? AND status IN (0,1)", payment.Identity). UpdateColumns(map[string]interface{}{ "status": 2, // 支付成功 "trade_no": tradeNo, "callback_msg": "SUCCESS", }) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return nil } // 仅充值单需要入账 if payment.Type != 1 { return nil } res = tx.Model(&WalletBasic{}). Where("identity = ? AND status <> -1", payment.WalletIdentity). UpdateColumns(map[string]interface{}{ "balance": gorm.Expr("balance + ?", payment.Amount), "withdrawal_balance": gorm.Expr("withdrawal_balance + ?", payment.Amount), }) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return errors.New("wallet not found") } record := &WalletRecord{ WalletIdentity: payment.WalletIdentity, TransType: 1, // 收入 InTradeNo: outTradeNo, OutTradeNo: tradeNo, Money: payment.Amount, TradeType: 1, // 充值 PayChannel: payment.PayChannel, PayType: payment.PayType, } record.Identity = utils.UUID() record.PassportID = payment.PassportID record.PassportIdentity = payment.PassportIdentity FillRecordYmd(record) return tx.Create(record).Error }) } // CreatePaymentRecord 创建支付记录 func CreatePaymentRecord(fp *WalletPayment) error { return impl.DBService.Create(&fp).Error } // CreateTradeRecord 创建交易记录 func CreateTradeRecord(fr *WalletRecord) error { return impl.DBService.Create(&fr).Error } // ChargeWallet 为钱包充值 // 余额变更使用条件增量更新,流水写入与余额变更在同一事务内完成。 func ChargeWallet(passport_id uint, passport_identity, order_no string, paySource int32, amount int64) error { if amount <= 0 { return errors.New("amount is invalid") } wallet, err := GetWalletByPassportIdentity(passport_id, passport_identity) if err != nil { return err } return impl.DBService.Transaction(func(tx *gorm.DB) error { res := tx.Model(&WalletBasic{}). Where("identity = ? AND status <> -1", wallet.Identity). UpdateColumns(map[string]interface{}{ "balance": gorm.Expr("balance + ?", amount), "withdrawal_balance": gorm.Expr("withdrawal_balance + ?", amount), }) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return errors.New("wallet not found") } // 创建交易记录 record := &WalletRecord{ WalletIdentity: wallet.Identity, TransType: 1, // 收入 InTradeNo: order_no, Money: amount, TradeType: 1, // 充值 PayChannel: int8(paySource), PayType: "CHARGE", } record.Identity = utils.UUID() record.PassportID = passport_id record.PassportIdentity = passport_identity FillRecordYmd(record) return tx.Create(record).Error }) } // FindWalletRecords 查询钱包交易记录 func FindWalletRecords(passportIdentity, start, end string, transType, tradeType, page, pageSize int64) ([]*WalletRecord, int64, error) { list := make([]*WalletRecord, 0) tx := impl.DBService.Model(&WalletRecord{}).Where("passport_identity = ?", passportIdentity) // 添加时间范围过滤 if start != "" { tx = tx.Where("created_at >= ?", start) } if end != "" { tx = tx.Where("created_at <= ?", end) } // 添加交易类型过滤 if transType != 0 { tx = tx.Where("trans_type = ?", transType) } if tradeType != 0 { tx = tx.Where("trade_type = ?", tradeType) } // 总数使用独立的 Count 查询,避免用当页条数充当总数 var total int64 if err := tx.Count(&total).Error; err != nil { return nil, 0, err } // 分页查询 res := tx.Order("created_at desc").Limit(int(pageSize)).Offset(int((page - 1) * pageSize)).Find(&list) if res.Error != nil { if !errors.Is(res.Error, gorm.ErrRecordNotFound) { return nil, 0, res.Error } return nil, 0, nil } return list, total, nil }