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

@@ -1,10 +1,9 @@
Service: "{ServiceKey}"
Port: 12238
Databases:
Databases:
Driver: postgres
Source:
Source:
- postgres://postgres:CHANGE_ME@127.0.0.1:5432/bsm_dev?sslmode=disable&TimeZone=Asia/Shanghai
# cache DB的选择请在后面直接带参数,不带会自动HASH计算选择DB库。
@@ -12,41 +11,27 @@ Databases:
Cache: redis://null:CHANGE_ME@127.0.0.1:6379/
Etcd:
Hosts:
- "127.0.0.1:2379"
Key: service.{Workspace}.{ServiceKey}.rpc
Endpoints:
- "127.0.0.1:2379"
Gateway:
Enable: true
Port: 12239
## 服务调用
#Notify:
# Etcd:
# Hosts:
# - 127.0.0.1:2379
# Key: service.{Workspace}.notify.rpc
#
#Material:
# Etcd:
# Hosts:
# - 127.0.0.1:2379
# Key: service.{Workspace}.material.rpc
MicroService:
MicroService:
Enable: false
Anonymous:
# 渠道回调按渠道验签放行;支付结果不再接受调用方自行提交
- wallet.wechat.WxCallback
- wallet.payment.Callback
- wallet.payment.Hello
# 日志记录
Log:
ServiceName: {ServiceKey}
Mode: file
Path: logs/{ServiceKey}
Stat: false
Name: wallet
Level: debug
Dir: logs/wallet
Console: true
File: true
# 性能监控
Prometheus:
@@ -54,26 +39,36 @@ Prometheus:
Port: 22238
Path: /metrics
# 链路追踪
# Telemetry:
# Name: {Workspace}.{ServiceKey}.{RuntimeMode}
# Endpoint: http://121.37.208.46:14268/api/traces
# Sampler: 1.0
# Batcher: jaeger
# MQ Pulsar
#Pulsar:
# Endpoints: 127.0.0.1
# Token: 9091
# Namespaces: {Workspace}
WeChat:
AppID: wx9a2f163207219776
AppSecret:
SerialNo: 209F629E63E56D2366361969B51FBA0ADCF69F55
APIV3Key: MakeSupplyChainFinance1123581321
PrivateKey: CHANGE_ME
NotifyURL:
AppID: ${BSM_WECHAT_APPID}
AppSecret: ${BSM_WECHAT_APPSECRET}
SerialNo: ${BSM_WECHAT_SERIALNO}
APIV3Key: ${BSM_WECHAT_APIV3KEY}
PrivateKey: ${BSM_WECHAT_PRIVATEKEY}
NotifyURL: ${BSM_WECHAT_NOTIFYURL}
Currency: CNY
MchID: 1670561983
MchID: ${BSM_WECHAT_MCHID}
SubMchID:
OpenID:
OpenID:
Alipay:
AppID: ${BSM_ALIPAY_APPID}
AppSecret: ${BSM_ALIPAY_APPSECRET}
PrivateKey: ${BSM_ALIPAY_PRIVATEKEY}
IsProd: false
NotifyURL: ${BSM_ALIPAY_NOTIFYURL}
ReturnURL: ${BSM_ALIPAY_RETURNURL}
QuitURL: ${BSM_ALIPAY_QUITURL}
PublicKeyRsa2Byte: ${BSM_ALIPAY_PUBLICKEYRSA2BYTE}
CertPublicKey: ${BSM_ALIPAY_CERTPUBLICKEY}
CertRoot: ${BSM_ALIPAY_CERTROOT}
CertPublicKeyRsa2: ${BSM_ALIPAY_CERTPUBLICKEYRSA2}
Wallet:
Name: ${BSM_WALLET_NAME}
URL: ${BSM_WALLET_URL}
YMDHMS: "2006-01-02 15:04:05"
YMD: "20060102"
AlipyIsOpen: false
WalletIsOpen: true
WechatpayIsOpen: false

View File

@@ -1,68 +1,71 @@
Name: {ServiceKey}
ListenOn: 0.0.0.0:12238
Service: "{ServiceKey}"
Port: 12238
BindIP: 0.0.0.0
Dsn: postgres://postgres:CHANGE_ME@127.0.0.1:5432/{Workspace}_prod?sslmode=disable&TimeZone=Asia/Shanghai
Databases:
Driver: postgres
Source:
- ${BSM_WALLET_DB_DSN}
# cache DB的选择请在后面直接带参数,不带会自动HASH计算选择DB库。
Cache: redis://null:CHANGE_ME@127.0.0.1:6379/
Cache: ${BSM_WALLET_CACHE}
Etcd:
Hosts:
- 192.168.0.83:2379
Key: service.{Workspace}.{ServiceKey}.rpc
Endpoints:
- ${BSM_ETCD_ENDPOINT}
# 服务调用
Notify:
Etcd:
Hosts:
- 192.168.5.161:2379
Key: service.{Workspace}.notify.rpc
Gateway:
Enable: true
Port: 12239
# 匿名访问清单,自动注入网关层
# 匿名访问清单,自动注入网关层
Anonymous:
Key: anonymous.{Workspace}
Urls:
- wallet.Check.Hello
- wallet.Check.Updates
- wallet.Data.Configure
- wallet.Data.Areas
- wallet.Data.Tags
MicroService:
Enable: true
Anonymous:
# 渠道回调按渠道验签放行;支付结果不再接受调用方自行提交
- wallet.wechat.WxCallback
# 日志记录
Log:
ServiceName: {ServiceKey}
Mode: file
Path: logs/{ServiceKey}
Stat: false
Name: wallet
Level: info
Dir: /usr/local/bsm/logs/wallet
Console: false
File: true
# 性能监控
Prometheus:
Host: 127.0.0.1
Port: 22238
Path: /metrics
# 链路追踪
Telemetry:
Name: {Workspace}.{ServiceKey}.{RuntimeMode}
Endpoint: http://192.168.0.85:14268/api/traces
Sampler: 1.0
Batcher: jaeger
WeChat:
AppID: wx9a2f163207219776
AppSecret:
SerialNo: 209F629E63E56D2366361969B51FBA0ADCF69F55
APIV3Key: MakeSupplyChainFinance1123581321
PrivateKey: /usr/local/cert/wechat/apiclient_key.pem
NotifyURL: https://srv.make-w.com/wallet.Payment.Callback
AppID: ${BSM_WECHAT_APPID}
AppSecret: ${BSM_WECHAT_APPSECRET}
SerialNo: ${BSM_WECHAT_SERIALNO}
APIV3Key: ${BSM_WECHAT_APIV3KEY}
PrivateKey: ${BSM_WECHAT_PRIVATEKEY}
NotifyURL: ${BSM_WECHAT_NOTIFYURL}
Currency: CNY
MchID: 1670561983
MchID: ${BSM_WECHAT_MCHID}
SubMchID:
OpenID:
# MQ Pulsar
#Pulsar:
# Endpoints: 127.0.0.1
# Token: 9091
# Namespaces: {Workspace}
Alipay:
AppID: ${BSM_ALIPAY_APPID}
AppSecret: ${BSM_ALIPAY_APPSECRET}
PrivateKey: ${BSM_ALIPAY_PRIVATEKEY}
IsProd: true
NotifyURL: ${BSM_ALIPAY_NOTIFYURL}
ReturnURL: ${BSM_ALIPAY_RETURNURL}
QuitURL: ${BSM_ALIPAY_QUITURL}
PublicKeyRsa2Byte: ${BSM_ALIPAY_PUBLICKEYRSA2BYTE}
CertPublicKey: ${BSM_ALIPAY_CERTPUBLICKEY}
CertRoot: ${BSM_ALIPAY_CERTROOT}
CertPublicKeyRsa2: ${BSM_ALIPAY_CERTPUBLICKEYRSA2}
Wallet:
Name: ${BSM_WALLET_NAME}
URL: ${BSM_WALLET_URL}
YMDHMS: "2006-01-02 15:04:05"
YMD: "20060102"
AlipyIsOpen: false
WalletIsOpen: true
WechatpayIsOpen: false

View File

@@ -1,61 +1,71 @@
Name: {ServiceKey}
ListenOn: 0.0.0.0:12238
Service: "{ServiceKey}"
Port: 12238
Dsn: postgres://postgres:CHANGE_ME@127.0.0.1:5432/{Workspace}_test?sslmode=disable&TimeZone=Asia/Shanghai
Databases:
Driver: postgres
Source:
- postgres://postgres:CHANGE_ME@127.0.0.1:5432/{Workspace}_test?sslmode=disable&TimeZone=Asia/Shanghai
# cache DB的选择请在后面直接带参数,不带会自动HASH计算选择DB库。
# DB库设置例:redis://null:Weidong1214@127.0.0.1:6379/12,代表DB12
Cache: redis://null:CHANGE_ME@127.0.0.1:6379/
Etcd:
Hosts:
- 127.0.0.1:2379
Key: service.{Workspace}.{ServiceKey}.rpc
Endpoints:
- "127.0.0.1:2379"
# 服务调用
Notify:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: service.{Workspace}.notify.rpc
Gateway:
Enable: true
Port: 12239
Material:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: service.{Workspace}.material.rpc
# 匿名访问清单,自动注入网关层
Anonymous:
Key: anonymous.{Workspace}
Urls:
MicroService:
Enable: false
Anonymous:
# 渠道回调按渠道验签放行;支付结果不再接受调用方自行提交
- wallet.wechat.WxCallback
- wallet.payment.Callback
- wallet.payment.Hello
# 日志记录
Log:
ServiceName: {ServiceKey}
Mode: file
Path: logs/{ServiceKey}
Stat: false
Name: wallet
Level: debug
Dir: logs/wallet
Console: true
File: true
# 性能监控
Prometheus:
Host: 127.0.0.1
Port: 22238
Path: /metrics
# 链路追踪
Telemetry:
Name: {Workspace}.{ServiceKey}.{RuntimeMode}
Endpoint: http://192.168.0.85:14268/api/traces
Sampler: 1.0
Batcher: jaeger
WeChat:
AppID: ${BSM_WECHAT_APPID}
AppSecret: ${BSM_WECHAT_APPSECRET}
SerialNo: ${BSM_WECHAT_SERIALNO}
APIV3Key: ${BSM_WECHAT_APIV3KEY}
PrivateKey: ${BSM_WECHAT_PRIVATEKEY}
NotifyURL: ${BSM_WECHAT_NOTIFYURL}
Currency: CNY
MchID: ${BSM_WECHAT_MCHID}
SubMchID:
OpenID:
# MQ Pulsar
#Pulsar:
# Endpoints: 127.0.0.1
# Token: 9091
# Namespaces: {Workspace}
Alipay:
AppID: ${BSM_ALIPAY_APPID}
AppSecret: ${BSM_ALIPAY_APPSECRET}
PrivateKey: ${BSM_ALIPAY_PRIVATEKEY}
IsProd: false
NotifyURL: ${BSM_ALIPAY_NOTIFYURL}
ReturnURL: ${BSM_ALIPAY_RETURNURL}
QuitURL: ${BSM_ALIPAY_QUITURL}
PublicKeyRsa2Byte: ${BSM_ALIPAY_PUBLICKEYRSA2BYTE}
CertPublicKey: ${BSM_ALIPAY_CERTPUBLICKEY}
CertRoot: ${BSM_ALIPAY_CERTROOT}
CertPublicKeyRsa2: ${BSM_ALIPAY_CERTPUBLICKEYRSA2}
Wallet:
Name: ${BSM_WALLET_NAME}
URL: ${BSM_WALLET_URL}
YMDHMS: "2006-01-02 15:04:05"
YMD: "20060102"
AlipyIsOpen: false
WalletIsOpen: true
WechatpayIsOpen: false

View File

@@ -19,6 +19,7 @@ require (
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0
github.com/patrickmn/go-cache v2.1.0+incompatible
go.etcd.io/etcd/client/v3 v3.7.1
golang.org/x/crypto v0.57.0
google.golang.org/grpc v1.84.0
google.golang.org/protobuf v1.36.12
gorm.io/gorm v1.31.2

View File

@@ -102,6 +102,8 @@ go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo=
go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues=

View File

@@ -1,6 +1,7 @@
package config
import (
"log"
"net"
"git.apinb.com/bsm-sdk/core/conf"
@@ -82,6 +83,16 @@ func New(srvKey string) {
// 配置校验 服务名称地址及监听地址不能为空
conf.NotNil(Spec.Service, Spec.Cache)
// 数据库配置缺失时 with.Databases 会直接 panic,这里提前给出明确错误
if Spec.Databases == nil || len(Spec.Databases.Source) == 0 {
log.Fatalln("ERROR: Databases Not Nil")
}
// 渠道与钱包配置缺失会导致下单时解引用空指针,这里提前拦截
if Spec.WeChat == nil || Spec.Alipay == nil || Spec.Wallet == nil {
log.Fatalln("ERROR: WeChat/Alipay/Wallet Config Not Nil")
}
// 初始化加密SecretKey
encipher.New(env.Runtime.JwtSecretKey)

View File

@@ -2,6 +2,7 @@ package alipay
import (
"context"
"strconv"
"bsm/full/module/finance/wallet/internal/config"
"github.com/go-pay/gopay"
@@ -23,8 +24,8 @@ func NewAlipay() (*AliPay, error) {
if err != nil {
return nil, err
}
// 打开Debug开关,输出日志,默认关闭
client.DebugSwitch = gopay.DebugOn
// 调试开关默认关闭,避免渠道请求/响应(含金额、订单号)写入进程日志
client.DebugSwitch = gopay.DebugOff
// 设置支付宝请求 公共参数
client.SetLocation(alipay.LocationShanghai) // 设置时区,不设置或出错均为默认服务器时间
@@ -44,17 +45,22 @@ func NewAlipay() (*AliPay, error) {
}
return &AliPay{
ctx: context.Background(),
Client: client,
// ctx: context.Background(),
Body: &gopay.BodyMap{},
}, nil
}
func (srv *AliPay) SetBody(body, orderNo string, amount int64) {
// SetBody 组装支付宝下单公共参数
// productCode:WAP 场景为 QUICK_WAP_WAY,APP 场景为 QUICK_MSECURITY_PAY
func (srv *AliPay) SetBody(body, orderNo string, amount int64, productCode string) {
// 支付宝要求 total_amount 为两位小数的元字符串,不能用整数除法截断
totalAmount := strconv.FormatFloat(float64(amount)/100, 'f', 2, 64)
srv.Body.Set("subject", config.Spec.Wallet.Name).
Set("body", body). //finance_payment表的identity
Set("product_code", "QUICK_WAP_WAY").
Set("product_code", productCode).
Set("out_trade_no", orderNo).
Set("total_amount", amount/100).
Set("total_amount", totalAmount).
Set("timeout_express", "60m").
Set("quit_url", config.Spec.Alipay.QuitURL)
}

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
}

View File

@@ -23,26 +23,27 @@ func ByCharge(ctx context.Context, in *pb.ChargeRequest) (reply *pb.PaymentReply
return nil, ok
}
if in.Amount == 0 || in.Desc == "" {
if in.Amount <= 0 || in.Desc == "" {
return nil, errcode.ErrInvalidArgument
}
var (
myWallet, wErr = models.GetWalletByPassportIdentity(auth.ID, auth.Identity)
orderNo = utils.RandomString(32)
paymentRecrod = models.WalletPayment{
WalletIdentity: myWallet.Identity,
Type: 1, //1充值,2为订单
OrderNo: orderNo,
PayChannel: int8(in.PayChannel),
PayType: in.PayType,
Amount: in.Amount,
}
)
if wErr != nil || myWallet.Identity == "" {
if wErr != nil || myWallet == nil || myWallet.Identity == "" {
return nil, errcode.ErrNotFound(404, "wallet")
}
paymentRecrod := models.WalletPayment{
WalletIdentity: myWallet.Identity,
Type: 1, //1充值,2为订单
OrderNo: orderNo,
PayChannel: int8(in.PayChannel),
PayType: in.PayType,
Amount: in.Amount,
}
paymentRecrod.Identity = utils.UUID()
paymentRecrod.PassportID = auth.ID
paymentRecrod.PassportIdentity = auth.Identity
@@ -93,30 +94,34 @@ func ByCharge(ctx context.Context, in *pb.ChargeRequest) (reply *pb.PaymentReply
}
case 2:
var result string
var (
result string
productCode string
)
switch in.PayType {
case "WAP":
productCode = "QUICK_WAP_WAY"
case "APP":
productCode = "QUICK_MSECURITY_PAY"
default:
return nil, excode.ErrPayType
}
alipay, err := alipay.NewAlipay()
if err != nil {
printer.Error(err.Error())
return nil, excode.ErrAlipayInit
}
alipay.SetBody(payBodyAttach.String(), orderNo, in.Amount)
alipay.SetBody(payBodyAttach.String(), orderNo, in.Amount, productCode)
switch in.PayType {
case "WAP":
result, err = alipay.TradeWapPay()
if err != nil {
printer.Error(err.Error())
return nil, excode.ErrAlipayGetResp
}
case "APP":
if productCode == "QUICK_MSECURITY_PAY" {
result, err = alipay.TradeAppPay()
if err != nil {
printer.Error(err.Error())
return nil, excode.ErrAlipayGetResp
}
default:
return nil, excode.ErrPayType
} else {
result, err = alipay.TradeWapPay()
}
if err != nil {
printer.Error(err.Error())
return nil, excode.ErrAlipayGetResp
}
replyResult["payUrl"] = result

View File

@@ -35,19 +35,20 @@ func ByOrder(ctx context.Context, in *pb.OrderRequest) (reply *pb.PaymentReply,
var (
myWallet, wErr = models.GetWalletByPassportIdentity(auth.ID, auth.Identity)
paymentRecrod = models.WalletPayment{
WalletIdentity: myWallet.Identity,
Type: 2, //1充值,2为订单
OrderNo: in.OrderNo,
PayChannel: int8(in.PayChannel),
PayType: in.PayType,
Amount: amount,
}
)
if wErr != nil || myWallet.Identity == "" {
if wErr != nil || myWallet == nil || myWallet.Identity == "" {
return nil, errcode.ErrNotFound(404, "wallet")
}
paymentRecrod := models.WalletPayment{
WalletIdentity: myWallet.Identity,
Type: 2, //1充值,2为订单
OrderNo: in.OrderNo,
PayChannel: int8(in.PayChannel),
PayType: in.PayType,
Amount: amount,
}
paymentRecrod.Identity = utils.UUID()
paymentRecrod.PassportID = auth.ID
paymentRecrod.PassportIdentity = auth.Identity
@@ -109,28 +110,34 @@ func ByOrder(ctx context.Context, in *pb.OrderRequest) (reply *pb.PaymentReply,
replyResult["prepay_id"] = res.PrepayId
case 2:
var result string
var (
result string
productCode string
)
switch in.PayType {
case "WAP":
productCode = "QUICK_WAP_WAY"
case "APP":
productCode = "QUICK_MSECURITY_PAY"
default:
return nil, excode.ErrPayType
}
alipay, err := alipay.NewAlipay()
if err != nil {
printer.Error(err.Error())
return nil, excode.ErrAlipayInit
}
alipay.SetBody(payBodyAttach.String(), in.OrderNo, amount)
switch in.PayType {
case "WAP":
result, err = alipay.TradeWapPay()
if err != nil {
printer.Error(err.Error())
return nil, excode.ErrAlipayGetResp
}
case "APP":
alipay.SetBody(payBodyAttach.String(), in.OrderNo, amount, productCode)
if productCode == "QUICK_MSECURITY_PAY" {
result, err = alipay.TradeAppPay()
if err != nil {
printer.Error(err.Error())
return nil, excode.ErrAlipayGetResp
}
default:
return nil, excode.ErrPayType
} else {
result, err = alipay.TradeWapPay()
}
if err != nil {
printer.Error(err.Error())
return nil, excode.ErrAlipayGetResp
}
replyResult["payUrl"] = result
@@ -138,11 +145,12 @@ func ByOrder(ctx context.Context, in *pb.OrderRequest) (reply *pb.PaymentReply,
case 3:
walletSrv, err := basic.NewWallet(auth.Identity, in.Password, amount)
if err != nil {
return nil, errcode.ErrDB
printer.Error(err.Error())
return nil, err
}
err = walletSrv.TradeConsum(amount)
if err != nil {
return nil, excode.ErrBalanceNotEnough
if err = walletSrv.TradeConsum(amount); err != nil {
printer.Error(err.Error())
return nil, err
}
default:
return nil, excode.ErrPayChannel

View File

@@ -12,6 +12,9 @@ import (
)
// 回调更新支付的结果和状态
// 注意:支付成功只能由渠道回调(如 Wechat.WxCallback 验签解密后)写入。
// 本接口仅允许调用方把自己名下的待支付单标记为取消/失败,
// 不再接受调用方传入的成功状态,避免客户端自证支付成功。
func Callback(ctx context.Context, in *pb.CallbackRequest) (reply *pb.StatusReply, err error) {
auth, ok := service.ParseMetaCtx(ctx, nil)
if ok != nil {
@@ -22,19 +25,19 @@ func Callback(ctx context.Context, in *pb.CallbackRequest) (reply *pb.StatusRepl
return nil, errcode.ErrInvalidArgument
}
var status int8 = 0
// 拒绝由调用方直接把支付单置为支付成功
if in.CallbackStatus {
status = 2
} else {
status = -1
return nil, errcode.ErrPermissionDenied
}
err = models.UpsetWalletPaymentByIdentity(auth.Identity, in.Identity, status, in.CallbackMsg)
// 仅允许「创建(0)/支付中(1)」流转到「失败(-1)」,重复调用保持幂等
err = models.MarkPaymentFailed(auth.Identity, in.Identity, in.CallbackMsg)
if err != nil {
return nil, err
}
return &pb.StatusReply{
Message: vars.OK,
Timeseq: time.Now().UnixMilli(),
}, nil
}

View File

@@ -3,11 +3,19 @@ package wechat
import (
"context"
"bsm/full/module/finance/wallet/internal/excode"
pb "bsm/full/module/finance/wallet/pb"
"git.apinb.com/bsm-sdk/core/printer"
"git.apinb.com/bsm-sdk/core/service"
)
// 微信JSAPI下单
func JsapiPreOrder(ctx context.Context, in *pb.WxpayJSAPIPreOrderRequest) (reply *pb.WxpayJSAPIPreOrderReply, err error) {
// 下单前必须先通过身份认证
if _, ok := service.ParseMetaCtx(ctx, nil); ok != nil {
return nil, ok
}
if err := CheckParam(in.Amount, in.AuthCode, in.UserIdentification, in.OrderNo); err != nil {
return nil, err
}
@@ -19,10 +27,15 @@ func JsapiPreOrder(ctx context.Context, in *pb.WxpayJSAPIPreOrderRequest) (reply
openid = in.UserIdentification // 用户微信唯一标识
amount = in.Amount // 订单金额
)
wx, _ := NewWechat()
wx, err := NewWechat()
if err != nil {
printer.Error(err.Error())
return nil, excode.ErrWechatInit
}
res, err := wx.GetResponse(attach, orderNo, desc, openid, amount)
if err != nil {
return nil, err
printer.Error(err.Error())
return nil, excode.ErrWechatGetResp
}
return &pb.WxpayJSAPIPreOrderReply{PrepayId: res.PrepayId}, nil
}

View File

@@ -180,7 +180,7 @@ func struct2map(in any) (map[string]string, error) {
}
func CheckParam(amount int64, auth_code string, identity string, order_no string) (err error) {
if amount == 0 {
if amount <= 0 {
return excode.ErrAmount
}
if auth_code == "" {

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"bsm/full/module/finance/wallet/internal/config"
"bsm/full/module/finance/wallet/internal/models"
pb "bsm/full/module/finance/wallet/pb"
"git.apinb.com/bsm-sdk/core/errcode"
"git.apinb.com/bsm-sdk/core/printer"
@@ -12,6 +13,8 @@ import (
)
// 微信支付回调
// 完成验签与解密后,按商户订单号反查本地支付单、核对金额,
// 条件更新为支付成功并在同一事务内入账写流水。
func WxCallback(ctx context.Context, in *pb.WxCallBackRequest) (reply *pb.CallBackReply, err error) {
var notifyReq = &wechat.V3NotifyReq{}
data, err := json.Marshal(in)
@@ -23,7 +26,11 @@ func WxCallback(ctx context.Context, in *pb.WxCallBackRequest) (reply *pb.CallBa
return nil, errcode.ErrJsonUnmarshal
}
wx, _ := NewWechat()
wx, err := NewWechat()
if err != nil {
printer.Error(err.Error())
return &pb.CallBackReply{Code: "FAIL", Message: "支付客户端初始化失败"}, nil
}
pubKey := wx.Client.WxPublicKeyMap()
err = notifyReq.VerifySignByPKMap(pubKey)
if err != nil {
@@ -33,9 +40,22 @@ func WxCallback(ctx context.Context, in *pb.WxCallBackRequest) (reply *pb.CallBa
res, err := notifyReq.DecryptPayCipherText(config.Spec.WeChat.APIV3Key)
if err != nil {
printer.Error(err.Error())
return &pb.CallBackReply{Code: "FAIL", Message: "验签失败"}, err
return &pb.CallBackReply{Code: "FAIL", Message: "解密失败"}, err
}
// Todo: 业务逻辑
payResult, _ := json.Marshal(res)
return &pb.CallBackReply{Code: "SUCCESS", Message: string(payResult)}, nil
// 仅处理支付成功通知,其余状态直接应答,避免渠道重复推送
if res.TradeState != "SUCCESS" {
return &pb.CallBackReply{Code: "SUCCESS", Message: res.TradeState}, nil
}
if res.Amount == nil {
return &pb.CallBackReply{Code: "FAIL", Message: "金额缺失"}, nil
}
// 结算:金额核对 + 条件更新为成功 + 事务内入账写流水(幂等)
if err := models.SettlePaymentSuccess(res.OutTradeNo, res.TransactionId, int64(res.Amount.Total)); err != nil {
printer.Error(err.Error())
return &pb.CallBackReply{Code: "FAIL", Message: "结算失败"}, nil
}
return &pb.CallBackReply{Code: "SUCCESS", Message: "SUCCESS"}, nil
}

View File

@@ -3,8 +3,11 @@ 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"
@@ -12,6 +15,13 @@ import (
"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 (
@@ -115,10 +125,93 @@ func GetWalletPaymentByIdentity(passport_identity, identity string) (*WalletPaym
return fp, err
}
// UpsetWalletPaymentByIdentity 更新钱包支付记录状态
func UpsetWalletPaymentByIdentity(passport_identity, identity string, CallbackStatus int8, CallbackMsg string) error {
err := impl.DBService.Model(&WalletPayment{}).Where("passport_identity=? and identity=?", passport_identity, identity).Updates(map[string]interface{}{"status": CallbackStatus, "call_back_msg": CallbackMsg}).Error
return 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 创建支付记录
@@ -132,57 +225,74 @@ func CreateTradeRecord(fr *WalletRecord) 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
}
// 更新钱包余额
data := map[string]interface{}{
"balance": wallet.Balance + amount,
"withdrawal_balance": wallet.WithdrawalBalance + amount,
}
err = impl.DBService.Model(&WalletBasic{}).Where("identity=?", wallet.Identity).UpdateColumns(data).Error
if err != nil {
return err
} else {
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, // 充值类型
TransType: 1, // 收入
InTradeNo: order_no,
Money: amount,
TradeType: 1, // 收入类型
TradeType: 1, // 充值
PayChannel: int8(paySource),
PayType: "CHARGE",
}
record.Identity = utils.UUID()
record.PassportID = passport_id
record.PassportIdentity = passport_identity
CreateTradeRecord(record)
}
return nil
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.Where("passport_identity = ?", passportIdentity)
tx := impl.DBService.Model(&WalletRecord{}).Where("passport_identity = ?", passportIdentity)
// 添加时间范围过滤
if start != "" {
tx.Where("created_at >= ?", start)
tx = tx.Where("created_at >= ?", start)
}
if end != "" {
tx.Where("created_at <= ?", end)
tx = tx.Where("created_at <= ?", end)
}
// 添加交易类型过滤
if transType != 0 {
tx.Where("trans_type = ?", transType)
tx = tx.Where("trans_type = ?", transType)
}
if tradeType != 0 {
tx.Where("trade_type = ?", tradeType)
tx = tx.Where("trade_type = ?", tradeType)
}
// 总数使用独立的 Count 查询,避免用当页条数充当总数
var total int64
if err := tx.Count(&total).Error; err != nil {
return nil, 0, err
}
// 分页查询
@@ -193,5 +303,5 @@ func FindWalletRecords(passportIdentity, start, end string, transType, tradeType
}
return nil, 0, nil
}
return list, res.RowsAffected, nil
return list, total, nil
}