Files
jnc-server/app/main/api/internal/logic/pay/paymentlogic.go
2026-01-16 03:33:02 +08:00

451 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package pay
import (
"context"
"database/sql"
"encoding/hex"
"encoding/json"
"fmt"
"jnc-server/app/main/api/internal/service"
"jnc-server/app/main/api/internal/svc"
"jnc-server/app/main/api/internal/types"
"jnc-server/app/main/model"
"jnc-server/common/ctxdata"
"jnc-server/common/xerr"
"jnc-server/pkg/lzkit/crypto"
"os"
"strings"
"time"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/redis/go-redis/v9"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
type PaymentLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
type PaymentTypeResp struct {
amount float64
outTradeNo string
description string
orderID string // 订单ID用于开发环境测试支付模式
userName string // 用户姓名(从查询缓存中获取)
userMobile string // 用户手机号(从查询缓存中获取)
}
func NewPaymentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PaymentLogic {
return &PaymentLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp, err error) {
var paymentTypeResp *PaymentTypeResp
var prepayData interface{}
var orderID string
// 检查是否为开发环境的测试支付模式
env := os.Getenv("ENV")
isDevTestPayment := env == "development" && (req.PayMethod == "test" || req.PayMethod == "test_empty")
isEmptyReportMode := env == "development" && req.PayMethod == "test_empty"
l.svcCtx.OrderModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error {
switch req.PayType {
case "agent_vip":
// 系统简化:已废弃会员充值功能
return errors.Wrapf(xerr.NewErrMsg("该功能已废弃"), "")
case "query":
paymentTypeResp, err = l.QueryOrderPayment(req, session)
if err != nil {
return err
}
case "agent_upgrade":
// 系统简化:已废弃升级功能
return errors.Wrapf(xerr.NewErrMsg("该功能已废弃"), "")
}
// 开发环境测试支付模式:跳过实际支付流程
// 注意:订单状态更新在事务外进行,避免在事务中查询不到订单的问题
if isDevTestPayment {
// 获取订单ID从 QueryOrderPayment 返回的 orderID
if paymentTypeResp.orderID == "" {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "开发测试模式订单ID无效")
}
orderID = paymentTypeResp.orderID
// 在事务中只记录订单ID不更新订单状态
// 订单状态的更新和后续流程在事务提交后处理
logx.Infof("开发环境测试支付模式:订单 %s (ID: %s) 将在事务提交后更新状态", paymentTypeResp.outTradeNo, orderID)
// 返回测试支付标识
prepayData = "test_payment_success"
return nil
}
// 正常支付流程
var createOrderErr error
if req.PayMethod == "wechat" {
prepayData, createOrderErr = l.svcCtx.WechatPayService.CreateWechatOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo)
} else if req.PayMethod == "alipay" {
prepayData, createOrderErr = l.svcCtx.AlipayService.CreateAlipayOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo)
} else if req.PayMethod == "easypay_alipay" {
if l.svcCtx.EasyPayService == nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "易支付服务未启用")
}
// 获取用户ID用于次数轮询模式
userID, getUidErr := ctxdata.GetUidFromCtx(l.ctx)
if getUidErr != nil {
// 如果获取用户ID失败使用空字符串会回退到天数轮询
userID = ""
}
easyPayResult, createOrderErr := l.svcCtx.EasyPayService.CreateEasyPayOrder(l.ctx, paymentTypeResp.amount, paymentTypeResp.description, paymentTypeResp.outTradeNo, userID)
if createOrderErr == nil && easyPayResult != nil {
prepayData = easyPayResult.PayURL
// 将渠道ID存储到context中后续创建订单时使用
ctx = context.WithValue(ctx, "easypay_cid", easyPayResult.CID)
l.ctx = ctx
}
} else if req.PayMethod == "appleiap" {
prepayData = l.svcCtx.ApplePayService.GetIappayAppID(paymentTypeResp.outTradeNo)
} else if req.PayMethod == "yunyinSignPay" || req.PayMethod == "yunyinSignPay_wechat" || req.PayMethod == "yunyinSignPay_alipay" {
if l.svcCtx.YunYinSignPayService == nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "云印签支付服务未启用")
}
// 从查询缓存中获取用户姓名和手机号
if paymentTypeResp.userMobile == "" {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 查询缓存中未找到用户手机号,无法使用云印签支付")
}
// 获取用户ID
userID, getUidErr := ctxdata.GetUidFromCtx(l.ctx)
if getUidErr != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 获取用户ID失败: %+v", getUidErr)
}
// 根据支付方式确定支付类型
payType := 0 // 默认微信支付
if req.PayMethod == "yunyinSignPay_alipay" {
payType = 1 // 支付宝支付
}
// 查询用户是否有未完成的签署(待签署且待支付)
var yunYinSignPayResult *service.CreateYunYinSignPayOrderResult
unfinishedOrder, findUnfinishedErr := l.svcCtx.YunyinSignPayOrderModel.FindUnfinishedByUserId(l.ctx, userID)
if findUnfinishedErr == nil && unfinishedOrder != nil {
// 复用未完成的签署,只获取新的支付链接
logx.Infof("复用未完成的云印签签署任务ID: %s, 参与者ID: %s", unfinishedOrder.TaskId, unfinishedOrder.ParticipantId)
// 获取token和操作ID带缓存
accessToken, tokenErr := l.svcCtx.YunYinSignPayService.GetAccessToken(l.ctx)
if tokenErr != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 获取云印签token失败: %+v", tokenErr)
}
operationUserId, userIdErr := l.svcCtx.YunYinSignPayService.GetUserId(l.ctx, accessToken)
if userIdErr != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 获取云印签操作ID失败: %+v", userIdErr)
}
// 获取新的支付链接
payURL, payURLErr := l.svcCtx.YunYinSignPayService.GetPaymentURL(l.ctx, accessToken, operationUserId, unfinishedOrder.ParticipantId, payType)
if payURLErr != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 获取云印签支付链接失败: %+v", payURLErr)
}
yunYinSignPayResult = &service.CreateYunYinSignPayOrderResult{
PayURL: payURL,
ParticipantID: unfinishedOrder.ParticipantId,
TaskID: unfinishedOrder.TaskId,
}
} else {
// 没有未完成的签署,创建新的签署流程
var createOrderErr error
yunYinSignPayResult, createOrderErr = l.svcCtx.YunYinSignPayService.CreateYunYinSignPayOrder(
l.ctx,
paymentTypeResp.userMobile,
paymentTypeResp.userName,
paymentTypeResp.amount,
paymentTypeResp.outTradeNo,
payType,
)
if createOrderErr != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 创建云印签支付订单失败: %+v", createOrderErr)
}
}
prepayData = yunYinSignPayResult.PayURL
// 将云印签信息存储到context中后续创建订单和云印签订单记录时使用
ctx = context.WithValue(ctx, "yunyin_sign_pay_result", yunYinSignPayResult)
ctx = context.WithValue(ctx, "yunyin_sign_pay_user_id", userID)
ctx = context.WithValue(ctx, "yunyin_sign_pay_user_mobile", paymentTypeResp.userMobile)
ctx = context.WithValue(ctx, "yunyin_sign_pay_user_name", paymentTypeResp.userName)
ctx = context.WithValue(ctx, "yunyin_sign_pay_amount", paymentTypeResp.amount)
ctx = context.WithValue(ctx, "yunyin_sign_pay_pay_type", payType)
l.ctx = ctx
}
if createOrderErr != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 创建支付订单失败: %+v", createOrderErr)
}
return nil
})
if err != nil {
return nil, err
}
// 开发环境测试支付模式:事务提交后处理订单状态更新和后续流程
if isDevTestPayment && paymentTypeResp != nil && paymentTypeResp.orderID != "" {
// 使用 goroutine 异步处理,确保事务已完全提交
go func() {
// 短暂延迟,确保事务已完全提交到数据库
time.Sleep(200 * time.Millisecond)
finalOrderID := paymentTypeResp.orderID
// 查找订单并更新状态为已支付
order, findOrderErr := l.svcCtx.OrderModel.FindOne(context.Background(), finalOrderID)
if findOrderErr != nil {
logx.Errorf("开发测试模式查找订单失败订单ID: %s, 错误: %v", finalOrderID, findOrderErr)
return
}
// 更新订单状态为已支付
order.Status = "paid"
now := time.Now()
order.PayTime = sql.NullTime{Time: now, Valid: true}
// 空报告模式:在 PaymentPlatform 字段中标记,用于后续生成空报告
if isEmptyReportMode {
order.PaymentPlatform = "test_empty"
logx.Infof("开发环境空报告模式:订单 %s (ID: %s) 已标记为空报告模式", paymentTypeResp.outTradeNo, finalOrderID)
}
// 更新订单状态(在事务外执行)
updateErr := l.svcCtx.OrderModel.UpdateWithVersion(context.Background(), nil, order)
if updateErr != nil {
logx.Errorf("开发测试模式更新订单状态失败订单ID: %s, 错误: %+v", finalOrderID, updateErr)
return
}
logx.Infof("开发环境测试支付模式:订单 %s (ID: %s) 已自动标记为已支付", paymentTypeResp.outTradeNo, finalOrderID)
// 再次短暂延迟,确保订单状态更新已提交
time.Sleep(100 * time.Millisecond)
// 根据订单类型处理后续流程
if strings.HasPrefix(paymentTypeResp.outTradeNo, "U_") {
// 系统简化:升级订单已废弃,跳过处理
logx.Infof("开发测试模式,升级订单已废弃,订单号: %s", paymentTypeResp.outTradeNo)
} else {
// 查询订单:发送支付成功通知任务,触发后续流程(生成报告和代理处理)
if sendErr := l.svcCtx.AsynqService.SendQueryTask(finalOrderID); sendErr != nil {
logx.Errorf("开发测试模式发送支付成功通知任务失败订单ID: %s, 错误: %+v", finalOrderID, sendErr)
} else {
logx.Infof("开发测试模式已发送支付成功通知任务订单ID: %s", finalOrderID)
}
}
}()
}
switch v := prepayData.(type) {
case string:
// 如果 prepayData 是字符串类型,直接返回
return &types.PaymentResp{PrepayId: v, OrderNo: paymentTypeResp.outTradeNo}, nil
default:
return &types.PaymentResp{PrepayData: prepayData, OrderNo: paymentTypeResp.outTradeNo}, nil
}
}
func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Session) (resp *PaymentTypeResp, err error) {
userID, getUidErr := ctxdata.GetUidFromCtx(l.ctx)
if getUidErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 获取用户信息失败, %+v", getUidErr)
}
outTradeNo := req.Id
redisKey := fmt.Sprintf(types.QueryCacheKey, userID, outTradeNo)
cache, cacheErr := l.svcCtx.Redis.GetCtx(l.ctx, redisKey)
if cacheErr != nil {
if cacheErr == redis.Nil {
return nil, errors.Wrapf(xerr.NewErrMsg("订单已过期"), "生成订单, 缓存不存在, %+v", cacheErr)
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 获取缓存失败, %+v", cacheErr)
}
var data types.QueryCacheLoad
err = json.Unmarshal([]byte(cache), &data)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 解析缓存内容失败, %v", err)
}
// 解析查询参数,获取用户姓名和手机号(用于云印签支付)
var userName, userMobile string
if data.Params != "" {
secretKey := l.svcCtx.Config.Encrypt.SecretKey
key, decodeErr := hex.DecodeString(secretKey)
if decodeErr == nil {
decryptedData, decryptErr := crypto.AesDecrypt(data.Params, key)
if decryptErr == nil {
var params map[string]interface{}
if unmarshalErr := json.Unmarshal(decryptedData, &params); unmarshalErr == nil {
if name, ok := params["name"].(string); ok {
userName = name
}
if mobile, ok := params["mobile"].(string); ok {
userMobile = mobile
}
}
}
}
}
product, err := l.svcCtx.ProductModel.FindOneByProductEn(l.ctx, data.Product)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成订单, 查找产品错误: %v", err)
}
var amount float64
user, err := l.svcCtx.UserModel.FindOne(l.ctx, userID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 获取用户信息失败: %v", err)
}
var agentLinkModel *model.AgentLink
if data.AgentIdentifier != "" {
var findAgentLinkErr error
agentLinkModel, findAgentLinkErr = l.svcCtx.AgentLinkModel.FindOneByLinkIdentifier(l.ctx, data.AgentIdentifier)
if findAgentLinkErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 获取代理链接失败: %+v", findAgentLinkErr)
}
amount = agentLinkModel.SetPrice
} else {
amount = product.SellPrice
}
if user.Inside == 1 {
amount = 0.01
}
order := model.Order{
Id: uuid.NewString(),
OrderNo: outTradeNo,
UserId: userID,
ProductId: product.Id,
PaymentPlatform: req.PayMethod,
PaymentScene: "app",
Amount: amount,
Status: "pending",
}
// 如果是易支付记录使用的渠道ID到备注字段
if req.PayMethod == "easypay_alipay" {
if cid, ok := l.ctx.Value("easypay_cid").(string); ok && cid != "" {
order.Remark = sql.NullString{String: fmt.Sprintf("易支付渠道号: %s", cid), Valid: true}
}
}
_, insertOrderErr := l.svcCtx.OrderModel.Insert(l.ctx, session, &order)
if insertOrderErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 保存订单失败: %+v", insertOrderErr)
}
orderID := order.Id
// 如果是云印签支付,创建云印签订单记录
if req.PayMethod == "yunyinSignPay" || req.PayMethod == "yunyinSignPay_wechat" || req.PayMethod == "yunyinSignPay_alipay" {
yunYinSignPayResult, ok := l.ctx.Value("yunyin_sign_pay_result").(*service.CreateYunYinSignPayOrderResult)
if ok && yunYinSignPayResult != nil {
userID, _ := l.ctx.Value("yunyin_sign_pay_user_id").(string)
userMobile, _ := l.ctx.Value("yunyin_sign_pay_user_mobile").(string)
userName, _ := l.ctx.Value("yunyin_sign_pay_user_name").(string)
amount, _ := l.ctx.Value("yunyin_sign_pay_amount").(float64)
payType, _ := l.ctx.Value("yunyin_sign_pay_pay_type").(int)
yunyinSignPayOrder := model.YunyinSignPayOrder{
Id: uuid.NewString(),
OrderId: orderID,
UserId: userID,
TaskId: yunYinSignPayResult.TaskID,
ParticipantId: yunYinSignPayResult.ParticipantID,
Amount: amount,
PayType: int64(payType),
SignStatus: 0, // 待签署
PayStatus: 0, // 待支付
SourceOrderCode: outTradeNo,
UserMobile: sql.NullString{String: userMobile, Valid: userMobile != ""},
UserName: sql.NullString{String: userName, Valid: userName != ""},
DelState: 0,
Version: 0,
}
_, insertYunYinErr := l.svcCtx.YunyinSignPayOrderModel.InsertWithSession(l.ctx, session, &yunyinSignPayOrder)
if insertYunYinErr != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 保存云印签订单失败: %+v", insertYunYinErr)
}
}
}
// 如果是代理推广订单,创建完整的代理订单记录
if data.AgentIdentifier != "" && agentLinkModel != nil {
// 获取产品配置(必须存在)
productConfig, err := l.svcCtx.AgentProductConfigModel.FindOneByProductId(l.ctx, product.Id)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单失败,产品配置不存在, productId: %s请先在后台配置产品价格参数", product.Id)
}
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 查询产品配置失败: %+v", err)
}
// 系统简化:移除等级加成,直接使用底价
actualBasePrice := productConfig.BasePrice
// 计算提价成本(使用产品配置)
priceThreshold := 0.0
priceFeeRate := 0.0
if productConfig.PriceThreshold.Valid {
priceThreshold = productConfig.PriceThreshold.Float64
}
if productConfig.PriceFeeRate.Valid {
priceFeeRate = productConfig.PriceFeeRate.Float64
}
priceCost := 0.0
if agentLinkModel.SetPrice > priceThreshold {
priceCost = (agentLinkModel.SetPrice - priceThreshold) * priceFeeRate
}
// 计算代理收益
agentProfit := agentLinkModel.SetPrice - actualBasePrice - priceCost
// 创建代理订单记录
agentOrder := model.AgentOrder{
Id: uuid.NewString(),
AgentId: agentLinkModel.AgentId,
OrderId: orderID,
ProductId: product.Id,
OrderAmount: amount,
SetPrice: agentLinkModel.SetPrice,
ActualBasePrice: actualBasePrice,
PriceCost: priceCost,
AgentProfit: agentProfit,
ProcessStatus: 0, // 待处理
}
_, agentOrderInsert := l.svcCtx.AgentOrderModel.Insert(l.ctx, session, &agentOrder)
if agentOrderInsert != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 保存代理订单失败: %+v", agentOrderInsert)
}
}
return &PaymentTypeResp{
amount: amount,
outTradeNo: outTradeNo,
description: product.ProductName,
orderID: orderID,
userName: userName,
userMobile: userMobile,
}, nil
}