Files
full/module/finance/wallet/internal/models/query.go
2026-09-22 21:15:34 +08:00

308 lines
9.3 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 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
}