fix version 1
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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$")
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user