fix version 1

This commit is contained in:
2026-09-22 21:15:34 +08:00
parent 9f86366638
commit d63d7e8b3a
277 changed files with 9959 additions and 1514 deletions

View File

@@ -13,18 +13,25 @@ import (
"git.apinb.com/bsm-sdk/core/service"
"git.apinb.com/bsm-sdk/core/utils"
"git.apinb.com/bsm-sdk/core/vars"
"gorm.io/gorm"
)
// 申请提现
// 在同一事务内先原子扣减余额/可提现余额,再写入提现单与流水,
// 并通过条件更新balance >= amount保证余额不足时不会生成提现单避免重复提交超额提现。
func ApplyCash(ctx context.Context, in *pb.ApplyCashRequest) (reply *pb.StatusReply, err error) {
auth, ok := service.ParseMetaCtx(ctx, nil)
if ok != nil {
return nil, ok
}
if in.Amount == 0 || in.Channel == 0 {
if in.Amount <= 0 || in.Channel == 0 {
return nil, errcode.ErrInvalidArgument
}
// 提现渠道仅支持 1:微信 2:支付宝 3:银行卡
if in.Channel != 1 && in.Channel != 2 && in.Channel != 3 {
return nil, excode.ErrPayChannel
}
myWallet, err := models.GetWalletByPassportIdentity(auth.ID, auth.Identity)
if err != nil {
@@ -32,27 +39,64 @@ func ApplyCash(ctx context.Context, in *pb.ApplyCashRequest) (reply *pb.StatusRe
return nil, errcode.ErrDB
}
if in.Amount > myWallet.Balance {
return nil, excode.ErrBalanceNotEnough
}
// cashNo 同时作为提现单幂等键与内部流水号
cashNo := utils.UUID()
data := &models.WalletApplyCash{
WalletIdentity: myWallet.Identity,
Amount: in.Amount,
Channel: int8(in.Channel),
Remark: in.Remark,
}
data.Identity = utils.UUID()
data.PassportID = auth.ID
data.PassportIdentity = auth.Identity
err = impl.DBService.Transaction(func(tx *gorm.DB) error {
// 条件更新:余额与可提现余额均充足且钱包未禁用时才扣减
res := tx.Model(&models.WalletBasic{}).
Where("identity = ? AND status <> -1 AND balance >= ? AND withdrawal_balance >= ?", myWallet.Identity, in.Amount, in.Amount).
UpdateColumns(map[string]interface{}{
"balance": gorm.Expr("balance - ?", in.Amount),
"withdrawal_balance": gorm.Expr("withdrawal_balance - ?", in.Amount),
})
if res.Error != nil {
return res.Error
}
if res.RowsAffected == 0 {
return excode.ErrBalanceNotEnough
}
err = impl.DBService.Create(&data).Error
// 生成提现单
data := &models.WalletApplyCash{
WalletIdentity: myWallet.Identity,
CashNo: cashNo,
Amount: in.Amount,
Channel: int8(in.Channel),
Remark: in.Remark,
}
data.Identity = utils.UUID()
data.PassportID = auth.ID
data.PassportIdentity = auth.Identity
if err := tx.Create(data).Error; err != nil {
return err
}
// 同一事务内写入提现流水
record := &models.WalletRecord{
WalletIdentity: myWallet.Identity,
TransType: -1, // 支出
InTradeNo: cashNo, // 内部流水号
Money: in.Amount,
TradeType: 2, // 提现
PayChannel: int8(in.Channel),
PayType: "CASH", // 提现
Remark: in.Remark,
}
record.Identity = utils.UUID()
record.PassportID = auth.ID
record.PassportIdentity = auth.Identity
models.FillRecordYmd(record)
return tx.Create(record).Error
})
if err != nil {
if err == excode.ErrBalanceNotEnough {
return nil, excode.ErrBalanceNotEnough
}
printer.Error(err.Error())
return nil, errcode.ErrDB
}
//生成提现单
return &pb.StatusReply{
Message: vars.OK,
Timeseq: time.Now().UnixMilli(),

View File

@@ -33,45 +33,65 @@ func GetWallet(ctx context.Context, in *pb.GetWalletRequest) (reply *pb.GetWalle
ymd, _ := strconv.Atoi(time.Now().Format(vars.YYYYMMDD))
ym := time.Now().Year()*100 + int(time.Now().Month())
var totalMoney int64
total := make(map[string]int64)
var (
total = make(map[string]int64)
stat int64
)
// 统计当日收入
if in.IsTotalTodayIn {
impl.DBService.Model(&models.WalletRecord{}).Select("sum(money)").Where("passport_id=? AND trans_type=? AND ymd=?", auth.ID, transIn, ymd).Scan(&totalMoney)
total["TodayIn"] = totalMoney
stat = 0
if err = impl.DBService.Model(&models.WalletRecord{}).Select("sum(money)").Where("passport_id=? AND trans_type=? AND ymd=?", auth.ID, transIn, ymd).Scan(&stat).Error; err != nil {
printer.Error(err.Error())
}
total["TodayIn"] = stat
}
// 统计当日支出
if in.IsTotalTodayOut {
impl.DBService.Model(&models.WalletRecord{}).Select("sum(money)").Where("passport_id=? AND trans_type=? AND ymd=?", auth.ID, transOut, ymd).Scan(&totalMoney)
total["TodayOut"] = totalMoney
stat = 0
if err = impl.DBService.Model(&models.WalletRecord{}).Select("sum(money)").Where("passport_id=? AND trans_type=? AND ymd=?", auth.ID, transOut, ymd).Scan(&stat).Error; err != nil {
printer.Error(err.Error())
}
total["TodayOut"] = stat
}
// 统计本月收入
if in.IsTotalMonthIn {
impl.DBService.Model(&models.WalletRecord{}).Select("sum(money)").Where("passport_id=? AND trans_type=? AND ym=?", auth.ID, transIn, ym).Scan(&totalMoney)
total["MonthIn"] = totalMoney
stat = 0
if err = impl.DBService.Model(&models.WalletRecord{}).Select("sum(money)").Where("passport_id=? AND trans_type=? AND ym=?", auth.ID, transIn, ym).Scan(&stat).Error; err != nil {
printer.Error(err.Error())
}
total["MonthIn"] = stat
}
// 统计本月支出
if in.IsTotalMonthOut {
impl.DBService.Model(&models.WalletRecord{}).Select("sum(money)").Where("passport_id=? AND trans_type=? AND ym=?", auth.ID, transOut, ym).Scan(&totalMoney)
total["MonthOut"] = totalMoney
stat = 0
if err = impl.DBService.Model(&models.WalletRecord{}).Select("sum(money)").Where("passport_id=? AND trans_type=? AND ym=?", auth.ID, transOut, ym).Scan(&stat).Error; err != nil {
printer.Error(err.Error())
}
total["MonthOut"] = stat
}
// 统计全部收入
if in.IsTotalAllIn {
impl.DBService.Model(&models.WalletRecord{}).Select("sum(money)").Where("passport_id=? AND trans_type=? ", auth.ID, transIn).Scan(&totalMoney)
total["AllIn"] = totalMoney
stat = 0
if err = impl.DBService.Model(&models.WalletRecord{}).Select("sum(money)").Where("passport_id=? AND trans_type=? ", auth.ID, transIn).Scan(&stat).Error; err != nil {
printer.Error(err.Error())
}
total["AllIn"] = stat
}
// 统计全部支出
if in.IsTotalAllOut {
impl.DBService.Model(&models.WalletRecord{}).Select("sum(money)").Where("passport_id=? AND trans_type=? ", auth.ID, transOut).Scan(&totalMoney)
total["AllOut"] = totalMoney
stat = 0
if err = impl.DBService.Model(&models.WalletRecord{}).Select("sum(money)").Where("passport_id=? AND trans_type=? ", auth.ID, transOut).Scan(&stat).Error; err != nil {
printer.Error(err.Error())
}
total["AllOut"] = stat
}
// remove useless fmt.Sprint side-effect-free call
return &pb.GetWalletReply{
PassportIdentity: myWallet.PassportIdentity,
WalletIdentity: myWallet.Identity,

View File

@@ -2,8 +2,10 @@ package basic
import (
"context"
"strings"
"time"
"bsm/full/module/finance/wallet/internal/excode"
"bsm/full/module/finance/wallet/internal/impl"
"bsm/full/module/finance/wallet/internal/models"
pb "bsm/full/module/finance/wallet/pb"
@@ -14,19 +16,37 @@ import (
)
// 设置支付密码
// 首次设置(库中尚无 bcrypt 支付密码)允许直接写入;
// 已设置时不得由任意登录态直接覆盖,否则令牌泄露即可改密并盗刷钱包余额。
func SetPayPassword(ctx context.Context, in *pb.SetPayPasswordRequest) (reply *pb.StatusReply, err error) {
auth, ok := service.ParseMetaCtx(ctx, nil)
if ok != nil {
return nil, ok
}
if in.Password == "" {
return nil, errcode.ErrPassword
// 支付密码为 6~32 位数字
if !isValidPayPassword(in.Password) {
return nil, excode.ErrPasswordArgument
}
encPassword := EncodePassword(in.Password, auth.Identity)
myWallet, err := models.GetWalletByPassportIdentity(auth.ID, auth.Identity)
if err != nil {
printer.Error(err.Error())
return nil, errcode.ErrDB
}
err = impl.DBService.Model(&models.WalletBasic{}).Where("passport_identity", auth.Identity).Update("pay_password", encPassword).Error
// 已存在 bcrypt 支付密码时拒绝覆盖(当前接口没有旧密码字段,需走安全重置流程)
if isBcryptHash(myWallet.PayPassword) {
return nil, errcode.ErrPermissionDenied
}
encPassword, err := EncodePassword(in.Password)
if err != nil {
printer.Error(err.Error())
return nil, errcode.ErrDB
}
err = impl.DBService.Model(&models.WalletBasic{}).Where("passport_identity = ?", auth.Identity).Update("pay_password", encPassword).Error
if err != nil {
printer.Error(err.Error())
return nil, errcode.ErrDB
@@ -37,3 +57,21 @@ func SetPayPassword(ctx context.Context, in *pb.SetPayPasswordRequest) (reply *p
Timeseq: time.Now().UnixMilli(),
}, nil
}
// isValidPayPassword 校验支付密码格式6~32 位纯数字
func isValidPayPassword(pwd string) bool {
if len(pwd) < 6 || len(pwd) > 32 {
return false
}
for _, c := range pwd {
if c < '0' || c > '9' {
return false
}
}
return true
}
// isBcryptHash 判断是否已是 bcrypt 哈希串
func isBcryptHash(hashed string) bool {
return strings.HasPrefix(hashed, "$2a$") || strings.HasPrefix(hashed, "$2b$") || strings.HasPrefix(hashed, "$2y$")
}

View File

@@ -18,12 +18,16 @@ func Transactions(ctx context.Context, in *pb.TransactionsRequest) (reply *pb.Tr
return nil, err
}
if in.Page <= 1 {
if in.Page <= 0 {
in.Page = 1
}
if in.PageSize <= 1 {
if in.PageSize <= 0 {
in.PageSize = 20
}
// 单页上限,避免一次拉取整表流水
if in.PageSize > 100 {
in.PageSize = 100
}
list, total, err := models.FindWalletRecords(auth.Identity, in.Start, in.End, in.TransType, in.TradeType, in.Page, in.PageSize)
if err != nil {
printer.Error(err.Error())

View File

@@ -4,12 +4,13 @@ package basic
import (
"context"
"bsm/full/module/finance/wallet/internal/config"
"bsm/full/module/finance/wallet/internal/excode"
"bsm/full/module/finance/wallet/internal/impl"
"bsm/full/module/finance/wallet/internal/models"
"git.apinb.com/bsm-sdk/core/errcode"
"git.apinb.com/bsm-sdk/core/utils"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
// WalletPay 钱包支付结构体
@@ -32,12 +33,16 @@ func NewWallet(passportIdentity, pwd string, amount int64) (*WalletPay, error) {
return nil, errcode.ErrPermissionDenied
}
// 验支付密码
encPassword := EncodePassword(pwd, passportIdentity)
if encPassword != wallet.PayPassword {
// 验支付密码
if !VerifyPayPassword(wallet.PayPassword, pwd) {
return nil, errcode.ErrPassword
}
// 检查金额合法性
if amount <= 0 {
return nil, excode.ErrAmount
}
// 检查余额是否足够
if amount > wallet.Balance {
return nil, excode.ErrBalanceNotEnough
@@ -49,21 +54,60 @@ func NewWallet(passportIdentity, pwd string, amount int64) (*WalletPay, error) {
}
// TradeConsum 执行交易消费
// 扣除钱包余额可提现余额
// 使用单条条件更新原子扣减余额可提现余额,避免"读后写"造成的并发双花;
// 余额变更与消费流水写入放在同一事务内,保证账实一致。
func (srv *WalletPay) TradeConsum(amount int64) error {
data := map[string]interface{}{
"balance": srv.Body.Balance - amount,
"withdrawal_balance": srv.Body.WithdrawalBalance - amount,
if amount <= 0 {
return excode.ErrAmount
}
err := impl.DBService.Model(&models.WalletBasic{}).Where("identity=?", srv.Body.Identity).UpdateColumns(data).Error
if err != nil {
return err
}
return nil
return impl.DBService.Transaction(func(tx *gorm.DB) error {
// 条件更新:余额与可提现余额均充足且钱包未禁用时才扣减
res := tx.Model(&models.WalletBasic{}).
Where("identity = ? AND status <> -1 AND balance >= ? AND withdrawal_balance >= ?", srv.Body.Identity, amount, amount).
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 excode.ErrBalanceNotEnough
}
// 同一事务内写入消费流水
record := &models.WalletRecord{
WalletIdentity: srv.Body.Identity,
TransType: -1, // 支出
InTradeNo: utils.UUID(),
Money: amount,
TradeType: 3, // 消费
PayChannel: 3, // 钱包余额
PayType: "BALANCE", // 余额支付
}
record.Identity = utils.UUID()
record.PassportID = srv.Body.PassportID
record.PassportIdentity = srv.Body.PassportIdentity
models.FillRecordYmd(record)
return tx.Create(record).Error
})
}
// EncodePassword 加密支付密码
// 使用SHA256算法对密码进行加密
func EncodePassword(pwd, passportIdentity string) string {
return utils.Sha256(pwd, passportIdentity+config.Spec.Wallet.PublicKey)
// EncodePassword 使用 bcrypt 生成支付密码哈希
func EncodePassword(pwd string) (string, error) {
hashed, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hashed), nil
}
// VerifyPayPassword 校验支付密码
// 仅接受 bcrypt 哈希;历史 SHA256 摘要无法校验,需通过重置流程重新设置。
func VerifyPayPassword(hashed, pwd string) bool {
if hashed == "" || pwd == "" {
return false
}
return bcrypt.CompareHashAndPassword([]byte(hashed), []byte(pwd)) == nil
}