2026-08-09 10:43:45 +08:00
|
|
|
|
// Package models 定义钱包相关的数据模型和数据库操作
|
|
|
|
|
|
package models
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"errors"
|
2026-09-22 21:15:34 +08:00
|
|
|
|
"fmt"
|
|
|
|
|
|
"time"
|
2026-08-09 10:43:45 +08:00
|
|
|
|
|
2026-08-09 12:41:08 +08:00
|
|
|
|
"bsm/full/module/finance/wallet/internal/impl"
|
2026-09-22 21:15:34 +08:00
|
|
|
|
"git.apinb.com/bsm-sdk/core/errcode"
|
2026-08-09 10:43:45 +08:00
|
|
|
|
"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"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-09-22 21:15:34 +08:00
|
|
|
|
// 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()))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-09 10:43:45 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 21:15:34 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
})
|
2026-08-09 10:43:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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 为钱包充值
|
2026-09-22 21:15:34 +08:00
|
|
|
|
// 余额变更使用条件增量更新,流水写入与余额变更在同一事务内完成。
|
2026-08-09 10:43:45 +08:00
|
|
|
|
func ChargeWallet(passport_id uint, passport_identity, order_no string, paySource int32, amount int64) error {
|
2026-09-22 21:15:34 +08:00
|
|
|
|
if amount <= 0 {
|
|
|
|
|
|
return errors.New("amount is invalid")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-09 10:43:45 +08:00
|
|
|
|
wallet, err := GetWalletByPassportIdentity(passport_id, passport_identity)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 21:15:34 +08:00
|
|
|
|
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")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-09 10:43:45 +08:00
|
|
|
|
// 创建交易记录
|
|
|
|
|
|
record := &WalletRecord{
|
|
|
|
|
|
WalletIdentity: wallet.Identity,
|
2026-09-22 21:15:34 +08:00
|
|
|
|
TransType: 1, // 收入
|
2026-08-09 10:43:45 +08:00
|
|
|
|
InTradeNo: order_no,
|
|
|
|
|
|
Money: amount,
|
2026-09-22 21:15:34 +08:00
|
|
|
|
TradeType: 1, // 充值
|
2026-08-09 10:43:45 +08:00
|
|
|
|
PayChannel: int8(paySource),
|
2026-09-22 21:15:34 +08:00
|
|
|
|
PayType: "CHARGE",
|
2026-08-09 10:43:45 +08:00
|
|
|
|
}
|
2026-09-22 21:15:34 +08:00
|
|
|
|
record.Identity = utils.UUID()
|
2026-08-09 10:43:45 +08:00
|
|
|
|
record.PassportID = passport_id
|
|
|
|
|
|
record.PassportIdentity = passport_identity
|
2026-09-22 21:15:34 +08:00
|
|
|
|
FillRecordYmd(record)
|
|
|
|
|
|
return tx.Create(record).Error
|
|
|
|
|
|
})
|
2026-08-09 10:43:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// FindWalletRecords 查询钱包交易记录
|
|
|
|
|
|
func FindWalletRecords(passportIdentity, start, end string, transType, tradeType, page, pageSize int64) ([]*WalletRecord, int64, error) {
|
|
|
|
|
|
list := make([]*WalletRecord, 0)
|
2026-09-22 21:15:34 +08:00
|
|
|
|
tx := impl.DBService.Model(&WalletRecord{}).Where("passport_identity = ?", passportIdentity)
|
2026-08-09 10:43:45 +08:00
|
|
|
|
|
|
|
|
|
|
// 添加时间范围过滤
|
|
|
|
|
|
if start != "" {
|
2026-09-22 21:15:34 +08:00
|
|
|
|
tx = tx.Where("created_at >= ?", start)
|
2026-08-09 10:43:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
if end != "" {
|
2026-09-22 21:15:34 +08:00
|
|
|
|
tx = tx.Where("created_at <= ?", end)
|
2026-08-09 10:43:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加交易类型过滤
|
|
|
|
|
|
if transType != 0 {
|
2026-09-22 21:15:34 +08:00
|
|
|
|
tx = tx.Where("trans_type = ?", transType)
|
2026-08-09 10:43:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
if tradeType != 0 {
|
2026-09-22 21:15:34 +08:00
|
|
|
|
tx = tx.Where("trade_type = ?", tradeType)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 总数使用独立的 Count 查询,避免用当页条数充当总数
|
|
|
|
|
|
var total int64
|
|
|
|
|
|
if err := tx.Count(&total).Error; err != nil {
|
|
|
|
|
|
return nil, 0, err
|
2026-08-09 10:43:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 分页查询
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-09-22 21:15:34 +08:00
|
|
|
|
return list, total, nil
|
2026-08-09 10:43:45 +08:00
|
|
|
|
}
|