2ed5dfa6a1
iotlcm-app:小程序端 + 用户端服务。 - app/:uni-app 小程序前端(基于 unibest,MIT) - 微信 AppID → YOUR_WX_APPID,uni-app AppID → __UNI__YOURAPPID - 内网后端地址 → admin.iotlcm.example.com - 应用标题与 unibest 模板 README 重写(保留上游致谢与 MIT 声明) - server/:GoFrame 用户端服务(基于 XiuAdmin,Apache-2.0) - module xiuadmin → git.5iot.com/iotlcm-public/iotlcm-app(360 文件) - 支付回调地址 → app.iotlcm.example.com,商户名与应用名泛化 - 真实手机号 → 合法 11 位编造号(保留号段前 3 位 + 后 4 位) - 依赖号码尾部的断言同步更新 - 补 server/LICENSE(Apache-2.0) - 移除内部资产:CI 触发器 - 补根 README,LF 行尾 + .gitattributes 已知:TestSysDept_RefreshDeptAncestors 在源仓同一提交上同样失败,属既有缺陷。
357 lines
12 KiB
Go
357 lines
12 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
|
|
"git.5iot.com/iotlcm-public/iotlcm-app/internal/consts"
|
|
"git.5iot.com/iotlcm-public/iotlcm-app/internal/dao"
|
|
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model"
|
|
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/do"
|
|
"git.5iot.com/iotlcm-public/iotlcm-app/internal/model/entity"
|
|
"git.5iot.com/iotlcm-public/iotlcm-app/internal/service"
|
|
|
|
"github.com/gogf/gf/v2/database/gdb"
|
|
"github.com/gogf/gf/v2/errors/gerror"
|
|
"github.com/gogf/gf/v2/os/gtime"
|
|
)
|
|
|
|
type appBalanceRechargePaymentResult struct {
|
|
Order *entity.TOrder
|
|
BalanceAfter float64
|
|
PayTradeNo string
|
|
PayProvider string
|
|
}
|
|
|
|
func (s *sAppCard) completeAppBalanceRechargePayment(ctx context.Context, orderSn string, loginUser *entity.SysUser, payTradeNo string, payProvider string, transactionChannel int, expectedAmount float64, paidAt *gtime.Time) (*appBalanceRechargePaymentResult, error) {
|
|
orderSn = strings.TrimSpace(orderSn)
|
|
if orderSn == "" {
|
|
return nil, gerror.New("订单号不能为空")
|
|
}
|
|
payTradeNo = strings.TrimSpace(payTradeNo)
|
|
payProvider = strings.TrimSpace(payProvider)
|
|
if payProvider == "" {
|
|
payProvider = "online"
|
|
}
|
|
if paidAt == nil {
|
|
paidAt = gtime.Now()
|
|
}
|
|
|
|
var result *appBalanceRechargePaymentResult
|
|
err := dao.TOrder.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
|
query := dao.TOrder.Ctx(ctx).TX(tx).
|
|
Where(dao.TOrder.Columns().OrderSn, orderSn).
|
|
LockUpdate().
|
|
Limit(1)
|
|
if loginUser != nil {
|
|
query = query.Where(dao.TOrder.Columns().TenantId, loginUser.TenantId)
|
|
}
|
|
|
|
var lockedOrder *entity.TOrder
|
|
if err := query.Scan(&lockedOrder); err != nil {
|
|
return gerror.Wrap(err, "锁定充值订单失败")
|
|
}
|
|
if lockedOrder == nil || lockedOrder.Id == 0 {
|
|
return gerror.New("充值订单不存在")
|
|
}
|
|
if lockedOrder.OrderType != consts.OrderTypeBalanceRecharge {
|
|
return gerror.New("订单不是余额充值订单")
|
|
}
|
|
|
|
user := loginUser
|
|
if user == nil {
|
|
var err error
|
|
user, err = s.loadAppBalanceRechargeOrderUser(ctx, tx, lockedOrder)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else if err := s.validateAppBalanceRechargeOrderOwner(ctx, user, lockedOrder); err != nil {
|
|
return err
|
|
}
|
|
|
|
amount := appRoundAmount(lockedOrder.TotalAmount)
|
|
if amount <= 0 {
|
|
return gerror.New("充值订单金额不正确")
|
|
}
|
|
if expectedAmount > 0 && math.Abs(appRoundAmount(expectedAmount)-amount) >= 0.01 {
|
|
return gerror.Newf("支付金额与订单金额不一致,订单 %.2f 元,支付 %.2f 元", amount, expectedAmount)
|
|
}
|
|
|
|
account, err := s.lockAppBalanceAccount(ctx, tx, user)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if lockedOrder.PayStatus == consts.PayStatusPaid {
|
|
if payTradeNo == "" {
|
|
payTradeNo = strings.TrimSpace(lockedOrder.PayOrderSn)
|
|
}
|
|
result = &appBalanceRechargePaymentResult{
|
|
Order: lockedOrder,
|
|
BalanceAfter: appRoundAmount(account.Balance),
|
|
PayTradeNo: payTradeNo,
|
|
PayProvider: payProvider,
|
|
}
|
|
return nil
|
|
}
|
|
if lockedOrder.PayStatus != consts.PayStatusUnpaid || lockedOrder.OrderStatus != consts.OrderStatusPendingPay {
|
|
return gerror.New("充值订单状态不允许支付")
|
|
}
|
|
|
|
creditedAccount, err := s.creditAppBalanceWithChannel(ctx, tx, user, lockedOrder.OrderSn, amount, fmt.Sprintf("%s充值", payProvider), transactionChannel, payTradeNo, paidAt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err = dao.TOrder.Ctx(ctx).TX(tx).
|
|
Where(dao.TOrder.Columns().Id, lockedOrder.Id).
|
|
Data(do.TOrder{
|
|
OrderStatus: consts.OrderStatusCompleted,
|
|
PayType: consts.PayTypeOnline,
|
|
PayStatus: consts.PayStatusPaid,
|
|
PayOrderSn: payTradeNo,
|
|
PayTime: paidAt,
|
|
PaidAmount: amount,
|
|
OnlinePaidAmount: amount,
|
|
UnpaidAmount: 0,
|
|
Remark: fmt.Sprintf("小程序余额充值%s支付成功", payProvider),
|
|
UpdatedBy: user.UserId,
|
|
UpdatedAt: paidAt,
|
|
}).
|
|
Update(); err != nil {
|
|
return gerror.Wrap(err, "更新充值订单支付状态失败")
|
|
}
|
|
if err = s.updateAppBalanceRechargeOrderItemPayment(ctx, tx, user, lockedOrder, creditedAccount, payProvider, payTradeNo, paidAt); err != nil {
|
|
return err
|
|
}
|
|
|
|
lockedOrder.OrderStatus = consts.OrderStatusCompleted
|
|
lockedOrder.PayType = consts.PayTypeOnline
|
|
lockedOrder.PayStatus = consts.PayStatusPaid
|
|
lockedOrder.PayOrderSn = payTradeNo
|
|
lockedOrder.PayTime = paidAt
|
|
lockedOrder.PaidAmount = amount
|
|
lockedOrder.OnlinePaidAmount = amount
|
|
lockedOrder.UnpaidAmount = 0
|
|
result = &appBalanceRechargePaymentResult{
|
|
Order: lockedOrder,
|
|
BalanceAfter: appRoundAmount(creditedAccount.Balance),
|
|
PayTradeNo: payTradeNo,
|
|
PayProvider: payProvider,
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if result == nil || result.Order == nil {
|
|
return nil, gerror.New("支付结果不完整")
|
|
}
|
|
_ = s.publishAppBusinessAsyncEvent(ctx, appBusinessAsyncEvent{
|
|
EventType: appAsyncEventBalanceRechargePaid,
|
|
OrderSn: result.Order.OrderSn,
|
|
PayProvider: result.PayProvider,
|
|
PayTradeNo: result.PayTradeNo,
|
|
TenantId: result.Order.TenantId,
|
|
UserId: result.Order.CreatedBy,
|
|
DeptId: result.Order.CreatedDept,
|
|
Trigger: "payment_complete",
|
|
})
|
|
return result, nil
|
|
}
|
|
|
|
func (s *sAppCard) BalanceRechargeMockPay(ctx context.Context, param *model.AppBalanceRechargeMockPayParam) (*model.AppBalanceRechargeStatusModel, error) {
|
|
if param == nil {
|
|
param = &model.AppBalanceRechargeMockPayParam{}
|
|
}
|
|
user, err := appCurrentUser(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !appMockPaymentEnabled(ctx) {
|
|
return nil, gerror.New("模拟支付未启用")
|
|
}
|
|
orderSn := strings.TrimSpace(param.OrderSn)
|
|
if orderSn == "" {
|
|
return nil, gerror.New("订单号不能为空")
|
|
}
|
|
now := gtime.Now()
|
|
payTradeNo := appCardBatchNo("MOCK", user.UserId, now)
|
|
if _, err = s.completeAppBalanceRechargePayment(ctx, orderSn, user, payTradeNo, "mock", consts.BalanceTransactionChannelMock, 0, now); err != nil {
|
|
return nil, err
|
|
}
|
|
return s.BalanceRechargeStatus(ctx, &model.AppBalanceRechargeStatusParam{OrderSn: orderSn})
|
|
}
|
|
|
|
func (s *sAppCard) loadAppBalanceRechargeOrderUser(ctx context.Context, tx gdb.TX, order *entity.TOrder) (*entity.SysUser, error) {
|
|
if order == nil || order.CreatedBy <= 0 {
|
|
return nil, gerror.New("充值订单创建人信息不完整")
|
|
}
|
|
var user *entity.SysUser
|
|
if err := dao.SysUser.Ctx(ctx).TX(tx).
|
|
Where(dao.SysUser.Columns().TenantId, order.TenantId).
|
|
Where(dao.SysUser.Columns().UserId, order.CreatedBy).
|
|
Limit(1).
|
|
Scan(&user); err != nil {
|
|
return nil, gerror.Wrap(err, "查询充值订单用户失败")
|
|
}
|
|
if user == nil || user.UserId == 0 {
|
|
return nil, gerror.New("充值订单用户不存在")
|
|
}
|
|
if err := s.validateAppBalanceRechargeOrderOwner(ctx, user, order); err != nil {
|
|
return nil, err
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
func (s *sAppCard) validateAppBalanceRechargeOrderOwner(ctx context.Context, user *entity.SysUser, order *entity.TOrder) error {
|
|
if user == nil || order == nil {
|
|
return gerror.New("充值订单信息不完整")
|
|
}
|
|
if order.CreatedBy != user.UserId {
|
|
if order.CustomerType != consts.CustomerTypeTerminal || order.EndUserId != user.UserId {
|
|
return gerror.New("无权操作该充值订单")
|
|
}
|
|
}
|
|
if s.isTerminalUser(ctx, user) {
|
|
if order.CustomerType != consts.CustomerTypeTerminal || order.EndUserId != user.UserId {
|
|
return gerror.New("无权操作该充值订单")
|
|
}
|
|
return nil
|
|
}
|
|
if user.DeptId <= 0 {
|
|
return gerror.New("当前用户未绑定机构,无法操作充值订单")
|
|
}
|
|
if order.CustomerType != consts.CustomerTypeAgent || order.CustomerAgentId != user.DeptId {
|
|
return gerror.New("无权操作该充值订单")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *sAppCard) updateAppBalanceRechargeOrderItemPayment(ctx context.Context, tx gdb.TX, user *entity.SysUser, order *entity.TOrder, account *appBalanceAccount, payProvider string, payTradeNo string, paidAt *gtime.Time) error {
|
|
if user == nil || order == nil || account == nil {
|
|
return gerror.New("充值订单项支付快照信息不完整")
|
|
}
|
|
extraSkuData := appRenewMarshalJSON(map[string]any{
|
|
"business": "miniapp_balance_recharge",
|
|
"accountType": account.AccountLabel,
|
|
"accountId": account.AccountId,
|
|
"amount": appRoundAmount(order.TotalAmount),
|
|
"payChannel": payProvider,
|
|
"payTradeNo": payTradeNo,
|
|
"paidAt": appRenewFormatTime(paidAt),
|
|
})
|
|
if _, err := dao.TOrderItem.Ctx(ctx).TX(tx).
|
|
Where(dao.TOrderItem.Columns().OrderSn, order.OrderSn).
|
|
Data(do.TOrderItem{
|
|
ExtraSkuData: extraSkuData,
|
|
UpdatedBy: user.UserId,
|
|
UpdatedAt: paidAt,
|
|
}).
|
|
Update(); err != nil {
|
|
return gerror.Wrap(err, "更新充值订单项支付快照失败")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *sAppCard) BalanceRechargeStatus(ctx context.Context, param *model.AppBalanceRechargeStatusParam) (*model.AppBalanceRechargeStatusModel, error) {
|
|
if param == nil {
|
|
param = &model.AppBalanceRechargeStatusParam{}
|
|
}
|
|
orderSn := strings.TrimSpace(param.OrderSn)
|
|
if orderSn == "" {
|
|
return nil, gerror.New("订单号不能为空")
|
|
}
|
|
|
|
user, err := appCurrentUser(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var order *entity.TOrder
|
|
var balanceAfter float64
|
|
err = dao.TOrder.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
|
if err := dao.TOrder.Ctx(ctx).TX(tx).
|
|
Where(dao.TOrder.Columns().TenantId, user.TenantId).
|
|
Where(dao.TOrder.Columns().OrderSn, orderSn).
|
|
Limit(1).
|
|
Scan(&order); err != nil {
|
|
return gerror.Wrap(err, "查询充值订单失败")
|
|
}
|
|
if order == nil || order.Id == 0 {
|
|
return gerror.New("充值订单不存在")
|
|
}
|
|
if err := s.validateAppBalanceRechargeOrderOwner(ctx, user, order); err != nil {
|
|
return err
|
|
}
|
|
account, err := s.lockAppBalanceAccount(ctx, tx, user)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
balanceAfter = appRoundAmount(account.Balance)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
status := "pending_pay"
|
|
if order.PayStatus == consts.PayStatusPaid {
|
|
status = "paid"
|
|
}
|
|
paymentExpireAt, paymentRemainingSeconds, paymentExpired := appBalanceRechargePaymentTiming(ctx, order)
|
|
if paymentExpired {
|
|
status = "expired"
|
|
}
|
|
var payRequest *appWechatPayRequest
|
|
payProvider := appBalancePayProviderFromTradeNo(order.PayOrderSn)
|
|
if order.PayStatus == consts.PayStatusUnpaid && !paymentExpired && appWechatEnabled(ctx) {
|
|
payProvider = "wechat"
|
|
if appNormalizeWechatPayScene(param.PayScene) == "mini" && strings.TrimSpace(param.WxCode) != "" {
|
|
if err = service.AppAuth().BindWechatOpenIdByLoginCode(ctx, user, param.WxCode); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
payRequest, err = appBuildWechatPayRequest(ctx, order, param.PayScene, user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return &model.AppBalanceRechargeStatusModel{
|
|
OrderId: order.Id,
|
|
OrderSn: order.OrderSn,
|
|
TotalAmount: appRoundAmount(order.TotalAmount),
|
|
PriceText: appFormatMoney(order.TotalAmount),
|
|
OrderStatus: order.OrderStatus,
|
|
PayStatus: order.PayStatus,
|
|
Status: status,
|
|
PaymentExpireAt: paymentExpireAt,
|
|
PaymentRemainingSeconds: paymentRemainingSeconds,
|
|
PaymentExpired: paymentExpired,
|
|
BalanceAfter: balanceAfter,
|
|
BalanceAfterText: appFormatMoney(balanceAfter),
|
|
PayProvider: payProvider,
|
|
PayTradeNo: order.PayOrderSn,
|
|
PayUrl: appWechatPayUrl(payRequest),
|
|
PayMethod: appWechatPayMethod(payRequest),
|
|
PayGateway: "",
|
|
PayParams: appWechatPayParams(payRequest),
|
|
}, nil
|
|
}
|
|
|
|
func appBalancePayProviderFromTradeNo(tradeNo string) string {
|
|
if strings.TrimSpace(tradeNo) == "" {
|
|
return ""
|
|
}
|
|
upper := strings.ToUpper(tradeNo)
|
|
if strings.HasPrefix(upper, "MOCK") {
|
|
return "mock"
|
|
}
|
|
if strings.HasPrefix(upper, "WX") {
|
|
return "wechat"
|
|
}
|
|
return "wechat"
|
|
}
|