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 在源仓同一提交上同样失败,属既有缺陷。
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
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"
|
||||
}
|
||||
Reference in New Issue
Block a user