Files
iotlcm/server/internal/logic/app/app_balance_recharge_complete.go
T
MiFaZhan 2ed5dfa6a1 chore: 初始开源发布
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 在源仓同一提交上同样失败,属既有缺陷。
2026-10-26 14:01:25 +08:00

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"
}