This commit is contained in:
2026-01-16 03:33:02 +08:00
parent 3090cd62c8
commit 23ad0477b2
16 changed files with 1943 additions and 56 deletions

411
YunYinSignPay.json Normal file
View File

@@ -0,0 +1,411 @@
{
"openapi": "3.0.1",
"info": {
"title": "默认模块",
"description": "",
"version": "1.0.0"
},
"tags": [],
"paths": {
"/service/getAccessToken": {
"post": {
"summary": "获取token",
"deprecated": false,
"description": "",
"tags": [],
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"appId": {
"type": "string"
},
"appSecret": {
"type": "string"
}
},
"required": [
"appId",
"appSecret"
]
},
"example": {
"appId": "7226242043420468",
"appSecret": "ceac4dc740e3443bbb1433fbe9723326"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {}
}
}
},
"headers": {}
}
},
"security": []
}
},
"/member/infoByCorpId": {
"post": {
"summary": "获取操作id",
"deprecated": false,
"description": "",
"tags": [],
"parameters": [
{
"name": "appId",
"in": "header",
"description": "",
"required": true,
"example": "7226242043420468",
"schema": {
"type": "string"
}
},
{
"name": "accessToken",
"in": "header",
"description": "",
"required": true,
"example": "8629c8131fd83e116fd5b2fd30c2d9383f5af181",
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"mobile": {
"type": "string"
}
},
"required": [
"mobile"
]
},
"example": {
"mobile": "18566214578"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {}
}
}
},
"headers": {}
}
},
"security": []
}
},
"/signTask/startSignFlow": {
"post": {
"summary": "发起签署",
"deprecated": false,
"description": "",
"tags": [],
"parameters": [
{
"name": "appId",
"in": "header",
"description": "",
"required": true,
"example": " 7226242043420468",
"schema": {
"type": "string"
}
},
{
"name": "accessToken",
"in": "header",
"description": "",
"required": true,
"example": " 8629c8131fd83e116fd5b2fd30c2d9383f5af181",
"schema": {
"type": "string"
}
},
{
"name": "userId",
"in": "header",
"description": "",
"required": true,
"example": " 1460277348929680384",
"schema": {
"type": "string"
}
},
{
"name": "source",
"in": "header",
"description": "",
"required": true,
"example": " pc",
"schema": {
"type": "string"
}
},
{
"name": "Content-Type",
"in": "header",
"description": "",
"required": true,
"example": " application/json",
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"templateId": {
"type": "integer"
},
"templateName": {
"type": "string"
},
"autoFill": {
"type": "integer"
},
"sourceOrderCode": {
"type": "string"
},
"participantList": {
"type": "array",
"items": {
"type": "object",
"properties": {
"participantFlag": {
"type": "string"
},
"psnAccount": {
"type": "string"
},
"psnName": {
"type": "string"
},
"participantCorpName": {
"type": "string"
},
"participantType": {
"type": "integer"
}
},
"required": [
"participantFlag",
"psnAccount",
"psnName",
"participantType"
]
}
},
"fillComponents": {
"type": "array",
"items": {
"type": "object",
"properties": {
"componentKey": {
"type": "string"
},
"componentValue": {
"type": "string"
}
},
"required": [
"componentKey",
"componentValue"
]
}
}
},
"required": [
"templateId",
"templateName",
"autoFill",
"sourceOrderCode",
"participantList",
"fillComponents"
]
},
"example": {
"templateCode": "TP1461036991700317184",
"templateName": "信息服务授权书",
"autoFill": 1,
"sourceOrderCode": "Q_20260113123414384",
"participantList": [
{
"participantFlag": "签署方1",
"psnAccount": "18566214578",
"psnName": "陈立",
"participantCorpName": "海口开麦贸易有限公司",
"participantType": 1
},
{
"participantFlag": "签署方2",
"psnAccount": "18276151590",
"psnName": "张荣宏",
"participantType": 0,
"payeeContractFlag": 1,
"payee": {
"amount": 0.1,
"priority": "1"
}
}
]
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {}
}
}
},
"headers": {}
}
},
"security": []
}
},
"/signTask/contract/appletForParticipant": {
"post": {
"summary": "获取签署支付链接",
"deprecated": false,
"description": "",
"tags": [],
"parameters": [
{
"name": "appId",
"in": "header",
"description": "",
"required": true,
"example": " 7226242043420468",
"schema": {
"type": "string"
}
},
{
"name": "accessToken",
"in": "header",
"description": "",
"required": true,
"example": "8629c8131fd83e116fd5b2fd30c2d9383f5af181",
"schema": {
"type": "string"
}
},
{
"name": "userId",
"in": "header",
"description": "",
"required": true,
"example": " 1460277348929680384",
"schema": {
"type": "string"
}
},
{
"name": "source",
"in": "header",
"description": "",
"required": true,
"example": " pc",
"schema": {
"type": "string"
}
},
{
"name": "Content-Type",
"in": "header",
"description": "",
"required": true,
"example": " application/json",
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"participantId": {
"type": "string"
},
"type": {
"type": "integer"
}
},
"required": [
"participantId",
"type"
]
},
"example": {
"participantId": "1461532166079941632",
"type": 0
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {}
}
}
},
"headers": {}
}
},
"security": []
}
}
},
"components": {
"schemas": {},
"responses": {},
"securitySchemes": {}
},
"servers": [],
"security": []
}

View File

@@ -49,7 +49,7 @@ service main {
type (
PaymentReq {
Id string `json:"id"`
PayMethod string `json:"pay_method"` // 支付方式: wechat, alipay, appleiap, test(仅开发环境), test_empty(仅开发环境-空报告模式)
PayMethod string `json:"pay_method"` // 支付方式: wechat, alipay, easypay_alipay, appleiap, yunyinSignPay, yunyinSignPay_wechat, yunyinSignPay_alipay, test(仅开发环境), test_empty(仅开发环境-空报告模式)
// 系统简化:移除 agent_vip, agent_upgrade 支付类型
PayType string `json:"pay_type" validate:"required,oneof=query"`
}

View File

@@ -90,5 +90,16 @@ EasyPay:
CIDs: ["12200"] # 支付渠道ID数组可配置一个或多个多个时会按RotateDays轮询
# CIDs: ["12200", "12201", "12202"] # 多个渠道示例会按RotateDays天数轮询
RotateDays: 3 # 渠道轮询天数默认3天仅在配置了多个CIDs时生效
RotateMode: "count" # 轮询模式day天数轮询或 count次数轮询默认count
NotifyUrl: "https://www.dsjcq168.cn/api/v1/pay/easypay/callback"
ReturnUrl: "https://www.dsjcq168.cn/payment/result"
YunYinSignPay:
Enabled: true
ApiURL: "https://openapi.yunyinsign.com"
AppID: "7226242043420468"
AppSecret: "ceac4dc740e3443bbb1433fbe9723326"
Mobile: "18566214578"
Name: "陈立"
CorpName: "海口开麦贸易有限公司"
TemplateCode: "TP1461036991700317184" # 需要配置实际的模板ID
TemplateName: "信息服务授权书" # 需要配置实际的模板名称

View File

@@ -92,3 +92,13 @@ EasyPay:
RotateDays: 3 # 渠道轮询天数默认3天仅在RotateMode=day时生效
NotifyUrl: "https://www.dsjcq168.cn/api/v1/pay/easypay/callback"
ReturnUrl: "https://www.dsjcq168.cn/payment/result"
YunYinSignPay:
Enabled: true
ApiURL: "https://openapi.yunyinsign.com"
AppID: "7226242043420468"
AppSecret: "ceac4dc740e3443bbb1433fbe9723326"
Mobile: "18566214578"
Name: "陈立"
CorpName: "海口开麦贸易有限公司"
TemplateCode: "TP1461036991700317184" # 需要配置实际的模板ID
TemplateName: "信息服务授权书" # 需要配置实际的模板名称

View File

@@ -16,6 +16,7 @@ type Config struct {
Wxpay WxpayConfig
Applepay ApplepayConfig
EasyPay EasyPayConfig
YunYinSignPay YunYinSignPayConfig
Tianyuanapi TianyuanapiConfig
SystemConfig SystemConfig
WechatH5 WechatH5Config
@@ -135,3 +136,16 @@ type EasyPayConfig struct {
NotifyUrl string // 异步通知地址
ReturnUrl string // 页面跳转地址
}
// YunYinSignPayConfig 云印签支付配置
type YunYinSignPayConfig struct {
Enabled bool // 是否启用云印签支付
ApiURL string // 接口地址
AppID string // 应用ID
AppSecret string // 应用密钥
Mobile string // 我方手机号
Name string // 我方姓名
CorpName string // 我方公司名称
TemplateCode string // 模板代码
TemplateName string // 模板名称
}

View File

@@ -95,6 +95,112 @@ func (l *PaymentCheckLogic) PaymentCheck(req *types.PaymentCheckReq) (resp *type
}
}
// 如果订单状态是 pending 且支付平台是云印签支付,主动查询云印签订单状态
if order.Status == "pending" && (order.PaymentPlatform == "yunyinSignPay" || order.PaymentPlatform == "yunyinSignPay_wechat" || order.PaymentPlatform == "yunyinSignPay_alipay") {
// 检查云印签支付服务是否启用
if l.svcCtx.YunYinSignPayService != nil {
// 主动查询云印签订单状态
queryResp, queryErr := l.svcCtx.YunYinSignPayService.QueryPayeeBill(l.ctx, req.OrderNo)
if queryErr != nil {
logx.Errorf("主动查询云印签订单状态失败,订单号: %s, 错误: %v", req.OrderNo, queryErr)
// 查询失败不影响返回,继续返回当前订单状态
} else {
// 根据云印签返回的支付状态更新本地订单状态
// 云印签 payStatus: 0-订单生成, 1-支付中, 2-支付成功, 3-支付失败, 4-已退款
// 我们的订单状态: pending, paid, failed, refunded
var newOrderStatus string
var shouldUpdate bool
switch queryResp.PayStatus {
case 2: // 支付成功
newOrderStatus = "paid"
shouldUpdate = true
case 3: // 支付失败
newOrderStatus = "failed"
shouldUpdate = true
case 4: // 已退款
newOrderStatus = "refunded"
shouldUpdate = true
case 0, 1: // 订单生成或支付中,保持 pending
// 不更新,继续返回 pending
}
if shouldUpdate {
logx.Infof("主动查询发现云印签订单状态已变更,订单号: %s, 支付状态: %d, 新订单状态: %s", req.OrderNo, queryResp.PayStatus, newOrderStatus)
// 重新查询订单(获取最新版本号)
order, err = l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo)
if err != nil {
logx.Errorf("更新订单状态前重新查询订单失败: %v", err)
} else {
// 更新云印签订单表的支付状态(无论订单状态如何,都要同步云印签的状态)
yunyinOrder, findYunyinErr := l.svcCtx.YunyinSignPayOrderModel.FindOneByOrderId(l.ctx, order.Id)
if findYunyinErr == nil && yunyinOrder != nil {
// 更新支付状态
// 云印签 payStatus: 0-订单生成, 1-支付中, 2-支付成功, 3-支付失败, 4-已退款
// 我们的 payStatus: 0-待支付, 1-已支付, 2-已退款
var newPayStatus int64
if queryResp.PayStatus == 2 {
newPayStatus = 1 // 已支付
} else if queryResp.PayStatus == 4 {
newPayStatus = 2 // 已退款
} else if queryResp.PayStatus == 3 {
// 支付失败,保持待支付状态
newPayStatus = 0
}
if newPayStatus != yunyinOrder.PayStatus {
yunyinOrder.PayStatus = newPayStatus
if updateYunyinErr := l.svcCtx.YunyinSignPayOrderModel.Update(l.ctx, yunyinOrder); updateYunyinErr != nil {
logx.Errorf("更新云印签订单支付状态失败: %v", updateYunyinErr)
} else {
logx.Infof("成功更新云印签订单支付状态订单ID: %s, 新支付状态: %d", order.Id, newPayStatus)
}
}
}
// 只有在订单状态是 pending 时才更新订单状态
if order.Status == "pending" {
// 更新订单状态
order.Status = newOrderStatus
if newOrderStatus == "paid" {
order.PayTime = lzUtils.TimeToNullTime(time.Now())
if queryResp.ChannelOrderNo != "" {
order.PlatformOrderId = lzUtils.StringToNullString(queryResp.ChannelOrderNo)
}
} else if newOrderStatus == "refunded" {
order.RefundTime = lzUtils.TimeToNullTime(time.Now())
}
if updateErr := l.svcCtx.OrderModel.UpdateWithVersion(l.ctx, nil, order); updateErr != nil {
logx.Errorf("主动查询后更新订单状态失败: %v", updateErr)
} else {
logx.Infof("主动查询后成功更新订单状态,订单号: %s, 新状态: %s", req.OrderNo, newOrderStatus)
// 如果订单已支付,发送异步任务处理后续流程
if newOrderStatus == "paid" {
if asyncErr := l.svcCtx.AsynqService.SendQueryTask(order.Id); asyncErr != nil {
logx.Errorf("主动查询后发送异步任务失败: %v", asyncErr)
}
}
// 返回更新后的状态
return &types.PaymentCheckResp{
Type: "query",
Status: newOrderStatus,
}, nil
}
} else {
// 订单状态已经不是 pending说明可能已经被其他流程处理
// 但仍然返回当前状态
logx.Infof("订单状态已不是 pending当前状态: %s跳过更新", order.Status)
}
}
}
}
}
}
return &types.PaymentCheckResp{
Type: "query",
Status: order.Status,

View File

@@ -3,13 +3,16 @@ 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"
@@ -31,6 +34,8 @@ type PaymentTypeResp struct {
outTradeNo string
description string
orderID string // 订单ID用于开发环境测试支付模式
userName string // 用户姓名(从查询缓存中获取)
userMobile string // 用户手机号(从查询缓存中获取)
}
func NewPaymentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PaymentLogic {
@@ -111,6 +116,83 @@ func (l *PaymentLogic) Payment(req *types.PaymentReq) (resp *types.PaymentResp,
}
} 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)
@@ -204,6 +286,27 @@ func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Ses
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)
@@ -252,6 +355,40 @@ func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Ses
}
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 {
// 获取产品配置(必须存在)
@@ -302,5 +439,12 @@ func (l *PaymentLogic) QueryOrderPayment(req *types.PaymentReq, session sqlx.Ses
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "生成订单, 保存代理订单失败: %+v", agentOrderInsert)
}
}
return &PaymentTypeResp{amount: amount, outTradeNo: outTradeNo, description: product.ProductName, orderID: orderID}, nil
return &PaymentTypeResp{
amount: amount,
outTradeNo: outTradeNo,
description: product.ProductName,
orderID: orderID,
userName: userName,
userMobile: userMobile,
}, nil
}

View File

@@ -0,0 +1,685 @@
package service
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"jnc-server/app/main/api/internal/config"
"net/http"
"time"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/stores/redis"
)
const (
// Redis key 前缀
YunYinSignPayTokenKey = "yunyin_sign_pay:token"
YunYinSignPayUserIdKey = "yunyin_sign_pay:user_id"
TokenCacheExpire = 2 * time.Hour // token 缓存2小时
UserIdCacheExpire = 2 * time.Hour // 操作ID缓存2小时
)
// YunYinSignPayService 云印签支付服务
type YunYinSignPayService struct {
config config.YunYinSignPayConfig
client *http.Client
redis *redis.Redis
}
// NewYunYinSignPayService 创建云印签支付服务实例
func NewYunYinSignPayService(c config.Config, redisClient *redis.Redis) *YunYinSignPayService {
return &YunYinSignPayService{
config: c.YunYinSignPay,
client: &http.Client{
Timeout: 30 * time.Second,
},
redis: redisClient,
}
}
// GetAccessTokenRequest 获取token请求
type GetAccessTokenRequest struct {
AppID string `json:"appId"`
AppSecret string `json:"appSecret"`
}
// GetAccessTokenResponse 获取token响应
type GetAccessTokenResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *AccessTokenData `json:"data,omitempty"`
}
// AccessTokenData token数据
type AccessTokenData struct {
AccessToken string `json:"accessToken"`
ExpireTime int64 `json:"expireTime,omitempty"`
}
// GetAccessToken 获取访问token带缓存
func (y *YunYinSignPayService) GetAccessToken(ctx context.Context) (string, error) {
// 先从Redis获取
token, err := y.redis.GetCtx(ctx, YunYinSignPayTokenKey)
if err == nil && token != "" {
logx.Infof("从Redis获取云印签token成功")
return token, nil
}
// Redis中没有调用API获取
logx.Infof("从API获取云印签token")
reqBody := GetAccessTokenRequest{
AppID: y.config.AppID,
AppSecret: y.config.AppSecret,
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("序列化请求参数失败: %v", err)
}
url := fmt.Sprintf("%s/service/getAccessToken", y.config.ApiURL)
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonData))
if err != nil {
return "", fmt.Errorf("创建请求失败: %v", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := y.client.Do(req)
if err != nil {
return "", fmt.Errorf("请求失败: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取响应失败: %v", err)
}
var tokenResp GetAccessTokenResponse
if err := json.Unmarshal(body, &tokenResp); err != nil {
return "", fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, string(body))
}
if tokenResp.Code != 0 {
return "", fmt.Errorf("获取token失败: %s", tokenResp.Msg)
}
if tokenResp.Data == nil || tokenResp.Data.AccessToken == "" {
return "", fmt.Errorf("token数据为空")
}
accessToken := tokenResp.Data.AccessToken
// 存储到Redis2小时过期
err = y.redis.SetexCtx(ctx, YunYinSignPayTokenKey, accessToken, int(TokenCacheExpire.Seconds()))
if err != nil {
logx.Errorf("存储token到Redis失败: %v", err)
// 不返回错误因为token已经获取成功
}
logx.Infof("获取云印签token成功已缓存到Redis")
return accessToken, nil
}
// GetUserIdRequest 获取操作ID请求
type GetUserIdRequest struct {
Mobile string `json:"mobile"`
}
// GetUserIdResponse 获取操作ID响应
type GetUserIdResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *UserIdData `json:"data,omitempty"`
}
// UserIdData 操作ID数据
type UserIdData struct {
UserId string `json:"userId"`
}
// GetUserId 获取操作ID带缓存
func (y *YunYinSignPayService) GetUserId(ctx context.Context, accessToken string) (string, error) {
// 先从Redis获取
userId, err := y.redis.GetCtx(ctx, YunYinSignPayUserIdKey)
if err == nil && userId != "" {
logx.Infof("从Redis获取云印签操作ID成功")
return userId, nil
}
// Redis中没有调用API获取
logx.Infof("从API获取云印签操作ID")
reqBody := GetUserIdRequest{
Mobile: y.config.Mobile,
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("序列化请求参数失败: %v", err)
}
url := fmt.Sprintf("%s/member/infoByCorpId", y.config.ApiURL)
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonData))
if err != nil {
return "", fmt.Errorf("创建请求失败: %v", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("appId", y.config.AppID)
req.Header.Set("accessToken", accessToken)
resp, err := y.client.Do(req)
if err != nil {
return "", fmt.Errorf("请求失败: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取响应失败: %v", err)
}
var userIdResp GetUserIdResponse
if err := json.Unmarshal(body, &userIdResp); err != nil {
return "", fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, string(body))
}
if userIdResp.Code != 0 {
return "", fmt.Errorf("获取操作ID失败: %s", userIdResp.Msg)
}
if userIdResp.Data == nil || userIdResp.Data.UserId == "" {
return "", fmt.Errorf("操作ID数据为空")
}
// 获取操作ID
operationUserId := userIdResp.Data.UserId
// 存储到Redis2小时过期
err = y.redis.SetexCtx(ctx, YunYinSignPayUserIdKey, operationUserId, int(UserIdCacheExpire.Seconds()))
if err != nil {
logx.Errorf("存储操作ID到Redis失败: %v", err)
// 不返回错误因为操作ID已经获取成功
}
logx.Infof("获取云印签操作ID成功已缓存到Redis")
return operationUserId, nil
}
// ParticipantInfo 参与者信息
type ParticipantInfo struct {
ParticipantFlag string `json:"participantFlag"`
PsnAccount string `json:"psnAccount"`
PsnName string `json:"psnName"`
ParticipantCorpName string `json:"participantCorpName,omitempty"`
ParticipantType int `json:"participantType"`
PayeeContractFlag int `json:"payeeContractFlag,omitempty"`
Payee *PayeeInfo `json:"payee,omitempty"`
}
// PayeeInfo 收款方信息
type PayeeInfo struct {
Amount float64 `json:"amount"`
Priority string `json:"priority"`
}
// FillComponent 填充组件
type FillComponent struct {
ComponentKey string `json:"componentKey"`
ComponentValue string `json:"componentValue"`
}
// StartSignFlowRequest 发起签署请求
type StartSignFlowRequest struct {
TemplateCode string `json:"templateCode"`
TemplateName string `json:"templateName"`
AutoFill int `json:"autoFill"`
SourceOrderCode string `json:"sourceOrderCode"`
ParticipantList []ParticipantInfo `json:"participantList"`
FillComponents []FillComponent `json:"fillComponents"`
}
// StartSignFlowResponse 发起签署响应
type StartSignFlowResponse struct {
Code interface{} `json:"code"` // 可能是 int 或 string
Msg string `json:"msg"`
Data *StartSignFlowData `json:"data,omitempty"`
}
// StartSignFlowData 发起签署数据API响应
type StartSignFlowData struct {
FlowID string `json:"flowId,omitempty"`
FlowStatus int `json:"flowStatus,omitempty"`
FlowDesc string `json:"flowDesc,omitempty"`
FlowTitle string `json:"flowTitle,omitempty"`
NextStatus int `json:"nextStatus,omitempty"`
ResultStatus interface{} `json:"resultStatus,omitempty"`
ErrorMessage interface{} `json:"errorMessage,omitempty"`
SourceOrderCode string `json:"sourceOrderCode,omitempty"`
ParticipantList []ParticipantItem `json:"participantList,omitempty"`
}
// StartSignFlowResult 发起签署流程返回结果
type StartSignFlowResult struct {
ParticipantID string // 签署方2的参与者ID
TaskID string // 流程IDflowId
}
// ParticipantItem 参与者项(响应中的)
type ParticipantItem struct {
ParticipantID string `json:"participantId"`
ParticipantFlag string `json:"participantFlag"`
SignStatus interface{} `json:"signStatus,omitempty"`
ParticipantCorpID string `json:"participantCorpId,omitempty"`
ParticipantCorpName string `json:"participantCorpName,omitempty"`
ParticipantType int `json:"participantType"`
ParticipateBizType []string `json:"participateBizType,omitempty"`
PsnID string `json:"psnId,omitempty"`
PsnName string `json:"psnName,omitempty"`
PayeeContractFlag interface{} `json:"payeeContractFlag,omitempty"`
Payee interface{} `json:"payee,omitempty"`
}
// GetCodeInt 获取 code 的 int 值
func (r *StartSignFlowResponse) GetCodeInt() int {
switch v := r.Code.(type) {
case int:
return v
case float64:
return int(v)
case string:
if v == "200" {
return 200
}
return 0
default:
return 0
}
}
// StartSignFlow 发起签署流程
func (y *YunYinSignPayService) StartSignFlow(ctx context.Context, accessToken, userId string, req *StartSignFlowRequest) (*StartSignFlowResult, error) {
jsonData, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("序列化请求参数失败: %v", err)
}
url := fmt.Sprintf("%s/signTask/startSignFlow", y.config.ApiURL)
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonData))
if err != nil {
return nil, fmt.Errorf("创建请求失败: %v", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("appId", y.config.AppID)
httpReq.Header.Set("accessToken", accessToken)
httpReq.Header.Set("userId", userId)
httpReq.Header.Set("source", "pc")
resp, err := y.client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("请求失败: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %v", err)
}
var signFlowResp StartSignFlowResponse
if err := json.Unmarshal(body, &signFlowResp); err != nil {
return nil, fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, string(body))
}
// 检查响应码(可能是字符串"200"或数字200
codeInt := signFlowResp.GetCodeInt()
if codeInt != 200 {
return nil, fmt.Errorf("发起签署失败: %s", signFlowResp.Msg)
}
if signFlowResp.Data == nil {
return nil, fmt.Errorf("签署数据为空")
}
// 从 participantList 中找到签署方2的 participantId
var participantID2 string
for _, participant := range signFlowResp.Data.ParticipantList {
if participant.ParticipantFlag == "签署方2" {
participantID2 = participant.ParticipantID
break
}
}
if participantID2 == "" {
return nil, fmt.Errorf("未找到签署方2的参与者ID")
}
logx.Infof("发起云印签签署流程成功,订单号: %s, 流程ID: %s, 签署方2参与者ID: %s", req.SourceOrderCode, signFlowResp.Data.FlowID, participantID2)
// 返回结果包含签署方2的参与者ID和流程ID
return &StartSignFlowResult{
ParticipantID: participantID2,
TaskID: signFlowResp.Data.FlowID, // 使用 flowId 作为 taskId
}, nil
}
// GetPaymentURLRequest 获取支付链接请求
type GetPaymentURLRequest struct {
ParticipantID string `json:"participantId"`
Type int `json:"type"` // 0=微信支付1=支付宝支付
}
// GetPaymentURLResponse 获取支付链接响应
type GetPaymentURLResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data *PaymentURLData `json:"data,omitempty"`
}
// PaymentURLData 支付链接数据
type PaymentURLData struct {
PayURL string `json:"payUrl,omitempty"`
URL string `json:"url,omitempty"`
}
// GetPaymentURL 获取支付链接
func (y *YunYinSignPayService) GetPaymentURL(ctx context.Context, accessToken, userId string, participantID string, payType int) (string, error) {
reqBody := GetPaymentURLRequest{
ParticipantID: participantID,
Type: payType,
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return "", fmt.Errorf("序列化请求参数失败: %v", err)
}
url := fmt.Sprintf("%s/signTask/contract/appletForParticipant", y.config.ApiURL)
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonData))
if err != nil {
return "", fmt.Errorf("创建请求失败: %v", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("appId", y.config.AppID)
httpReq.Header.Set("accessToken", accessToken)
httpReq.Header.Set("userId", userId)
httpReq.Header.Set("source", "pc")
resp, err := y.client.Do(httpReq)
if err != nil {
return "", fmt.Errorf("请求失败: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取响应失败: %v", err)
}
var paymentURLResp GetPaymentURLResponse
if err := json.Unmarshal(body, &paymentURLResp); err != nil {
return "", fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, string(body))
}
if paymentURLResp.Code != 0 {
return "", fmt.Errorf("获取支付链接失败: %s", paymentURLResp.Msg)
}
if paymentURLResp.Data == nil {
return "", fmt.Errorf("支付链接数据为空")
}
// 优先返回 PayURL如果没有则返回 URL
payURL := paymentURLResp.Data.PayURL
if payURL == "" {
payURL = paymentURLResp.Data.URL
}
if payURL == "" {
return "", fmt.Errorf("支付链接为空")
}
logx.Infof("获取云印签支付链接成功参与者ID: %s", participantID)
return payURL, nil
}
// CreateYunYinSignPayOrderResult 创建云印签支付订单结果
type CreateYunYinSignPayOrderResult struct {
PayURL string // 支付链接
ParticipantID string // 参与者ID
TaskID string // 任务ID
}
// CreateYunYinSignPayOrder 创建云印签支付订单(封装完整流程)
func (y *YunYinSignPayService) CreateYunYinSignPayOrder(ctx context.Context, userMobile, userName string, amount float64, outTradeNo string, payType int) (*CreateYunYinSignPayOrderResult, error) {
// 1. 获取token和操作ID带缓存
accessToken, err := y.GetAccessToken(ctx)
if err != nil {
return nil, fmt.Errorf("获取云印签token失败: %v", err)
}
operationUserId, err := y.GetUserId(ctx, accessToken)
if err != nil {
return nil, fmt.Errorf("获取云印签操作ID失败: %v", err)
}
// 2. 构建参与者列表
participantList := []ParticipantInfo{
// 签署方1我方
{
ParticipantFlag: "签署方1",
PsnAccount: y.config.Mobile,
PsnName: y.config.Name,
ParticipantCorpName: y.config.CorpName,
ParticipantType: 1, // 1表示企业
},
// 签署方2用户支付方
{
ParticipantFlag: "签署方2",
PsnAccount: userMobile,
PsnName: func() string {
if userName != "" {
return userName
}
return "用户"
}(),
ParticipantType: 0, // 0表示个人
PayeeContractFlag: 1,
Payee: &PayeeInfo{
Amount: amount,
Priority: "1",
},
},
}
// 3. 发起签署流程
startSignFlowReq := &StartSignFlowRequest{
TemplateCode: y.config.TemplateCode,
TemplateName: y.config.TemplateName,
AutoFill: 1,
SourceOrderCode: outTradeNo,
ParticipantList: participantList,
FillComponents: []FillComponent{}, // 可以根据需要填充
}
signFlowData, err := y.StartSignFlow(ctx, accessToken, operationUserId, startSignFlowReq)
if err != nil {
return nil, fmt.Errorf("发起云印签签署失败: %v", err)
}
if signFlowData.ParticipantID == "" {
return nil, fmt.Errorf("签署流程返回的参与者ID为空")
}
// 4. 获取支付链接
payURL, err := y.GetPaymentURL(ctx, accessToken, operationUserId, signFlowData.ParticipantID, payType)
if err != nil {
return nil, fmt.Errorf("获取云印签支付链接失败: %v", err)
}
return &CreateYunYinSignPayOrderResult{
PayURL: payURL,
ParticipantID: signFlowData.ParticipantID,
TaskID: signFlowData.TaskID,
}, nil
}
// QueryPayeeBillRequest 查询收款单请求
type QueryPayeeBillRequest struct {
SourceOrderCode string `json:"sourceOrderCode,omitempty"` // 来源订单号
PayOrderCode string `json:"payOrderCode,omitempty"` // 支付单号
ContractCode string `json:"contractCode,omitempty"` // 合同号
PayStatus int `json:"payStatus,omitempty"` // 支付状态: 1-待支付, 2-支付成功, 3-退款成功
ChannelOrderNo string `json:"channelOrderNo,omitempty"` // 渠道单号
PayName string `json:"payName,omitempty"` // 付款方名称
ParticipantType int `json:"participantType,omitempty"` // 付款方类型: 1-企业, 0-个人
PayeeName string `json:"payeeName,omitempty"` // 收款方名称
PayeeCorpName string `json:"payeeCorpName,omitempty"` // 收款方企业名称
FlowStartTime string `json:"flowStartTime,omitempty"` // 查询范围的开始时间
FlowFinishTime string `json:"flowFinishTime,omitempty"` // 查询范围的结束时间
ListPageNo int `json:"listPageNo,omitempty"` // 当前页码从1开始
ListPageSize int `json:"listPageSize,omitempty"` // 每页返回的记录数量
}
// QueryPayeeBillResponse 查询收款单响应
type QueryPayeeBillResponse struct {
Code interface{} `json:"code"` // 返回码,可能是字符串"200"或数字200
Msg string `json:"msg"` // 返回码的描述信息
TotalCount int `json:"totalCount"` // 满足查询条件的总记录数
ListPageCount int `json:"listPageCount"` // 总页数
Data []PayeeBillItem `json:"data"` // 收款单记录列表
}
// PayeeBillItem 收款单记录
type PayeeBillItem struct {
ID int64 `json:"id"` // 消息ID
ContractName string `json:"contractName"` // 合同名称
ContractCode string `json:"contractCode"` // 合同号
FlowCode string `json:"flowCode"` // 签署流程编码
PayOrderCode string `json:"payOrderCode"` // 支付单号
Amount float64 `json:"amount"` // 支付金额
PayStatus int `json:"payStatus"` // 支付状态: 0-订单生成, 1-支付中, 2-支付成功, 3-支付失败, 4-已退款
RefundAmount float64 `json:"refundAmount"` // 退款金额
ParticipateID int64 `json:"participateId"` // 付款参与方ID
PsnName string `json:"psnName"` // 付款方经办人/个人名称
PsnAccount string `json:"psnAccount"` // 付款方经办人手机
ParticipantType int `json:"participantType"` // 付款方类型 (1-企业, 2-个人)
ParticipantCorpName string `json:"participantCorpName"` // 付款方企业名称
ParticipateBizType string `json:"participateBizType"` // 付款方参与方式
PayeeCorpName string `json:"payeeCorpName"` // 收款方企业名称
CreateTime string `json:"createTime"` // 订单创建时间
ChannelOrderNo string `json:"channelOrderNo"` // 渠道单号 (如微信或支付宝的交易号)
ProviderCode string `json:"providerCode"` // 支付方式编码
CallbackStatus int `json:"callbackStatus"` // 异步回调通知状态
}
// QueryPayeeBillResult 查询收款单结果
type QueryPayeeBillResult struct {
PayStatus int // 支付状态: 0-订单生成, 1-支付中, 2-支付成功, 3-支付失败, 4-已退款
PayOrderCode string // 支付单号
ChannelOrderNo string // 渠道单号
Amount float64 // 支付金额
RefundAmount float64 // 退款金额
}
// GetCodeInt 获取 code 的 int 值
func (r *QueryPayeeBillResponse) GetCodeInt() int {
switch v := r.Code.(type) {
case int:
return v
case float64:
return int(v)
case string:
if v == "200" {
return 200
}
return 0
default:
return 0
}
}
// QueryPayeeBill 查询收款单根据sourceOrderCode查询
func (y *YunYinSignPayService) QueryPayeeBill(ctx context.Context, sourceOrderCode string) (*QueryPayeeBillResult, error) {
// 1. 获取token和操作ID带缓存
accessToken, err := y.GetAccessToken(ctx)
if err != nil {
return nil, fmt.Errorf("获取云印签token失败: %v", err)
}
operationUserId, err := y.GetUserId(ctx, accessToken)
if err != nil {
return nil, fmt.Errorf("获取云印签操作ID失败: %v", err)
}
// 2. 构建查询请求
reqBody := QueryPayeeBillRequest{
SourceOrderCode: sourceOrderCode,
ListPageNo: 1,
ListPageSize: 10, // 只需要查询第一条匹配的记录
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("序列化请求参数失败: %v", err)
}
// 3. 调用查询API
url := fmt.Sprintf("%s/signFlowBill/payeeBillList", y.config.ApiURL)
httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(jsonData))
if err != nil {
return nil, fmt.Errorf("创建请求失败: %v", err)
}
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("appId", y.config.AppID)
httpReq.Header.Set("accessToken", accessToken)
httpReq.Header.Set("userId", operationUserId)
httpReq.Header.Set("source", "pc")
resp, err := y.client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("请求失败: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %v", err)
}
var queryResp QueryPayeeBillResponse
if err := json.Unmarshal(body, &queryResp); err != nil {
return nil, fmt.Errorf("解析响应失败: %v, 响应内容: %s", err, string(body))
}
// 4. 检查响应码
codeInt := queryResp.GetCodeInt()
if codeInt != 200 {
return nil, fmt.Errorf("查询收款单失败: %s", queryResp.Msg)
}
// 5. 查找匹配的记录根据sourceOrderCode
if len(queryResp.Data) == 0 {
return nil, fmt.Errorf("未找到匹配的收款单记录")
}
// 取第一条记录因为我们已经用sourceOrderCode精确查询
billItem := queryResp.Data[0]
logx.Infof("查询云印签收款单成功,订单号: %s, 支付状态: %d", sourceOrderCode, billItem.PayStatus)
return &QueryPayeeBillResult{
PayStatus: billItem.PayStatus,
PayOrderCode: billItem.PayOrderCode,
ChannelOrderNo: billItem.ChannelOrderNo,
Amount: billItem.Amount,
RefundAmount: billItem.RefundAmount,
}, nil
}

View File

@@ -41,6 +41,7 @@ type ServiceContext struct {
QueryCleanupLogModel model.QueryCleanupLogModel
QueryCleanupDetailModel model.QueryCleanupDetailModel
QueryCleanupConfigModel model.QueryCleanupConfigModel
YunyinSignPayOrderModel model.YunyinSignPayOrderModel
// 代理相关模型(新系统)
AgentModel model.AgentModel
@@ -75,6 +76,7 @@ type ServiceContext struct {
WechatPayService *service.WechatPayService
ApplePayService *service.ApplePayService
EasyPayService *service.EasyPayService
YunYinSignPayService *service.YunYinSignPayService
ApiRequestService *service.ApiRequestService
AsynqServer *asynq.Server
AsynqService *service.AsynqService
@@ -116,6 +118,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
queryCleanupLogModel := model.NewQueryCleanupLogModel(db, cacheConf)
queryCleanupDetailModel := model.NewQueryCleanupDetailModel(db, cacheConf)
queryCleanupConfigModel := model.NewQueryCleanupConfigModel(db, cacheConf)
yunyinSignPayOrderModel := model.NewYunyinSignPayOrderModel(db, cacheConf)
// ============================== 代理相关模型(新系统) ==============================
agentModel := model.NewAgentModel(db, cacheConf)
@@ -191,6 +194,16 @@ func NewServiceContext(c config.Config) *ServiceContext {
} else {
logx.Info("易支付服务已禁用")
}
// 根据配置决定是否初始化云印签支付服务
var yunYinSignPayService *service.YunYinSignPayService
if c.YunYinSignPay.Enabled {
yunYinSignPayService = service.NewYunYinSignPayService(c, redisClient)
logx.Info("云印签支付服务已启用")
} else {
logx.Info("云印签支付服务已禁用")
}
apiRequestService := service.NewApiRequestService(c, featureModel, productFeatureModel, tianyuanapi)
verificationService := service.NewVerificationService(c, tianyuanapi, apiRequestService)
asynqService := service.NewAsynqService(c)
@@ -238,6 +251,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
QueryCleanupLogModel: queryCleanupLogModel,
QueryCleanupDetailModel: queryCleanupDetailModel,
QueryCleanupConfigModel: queryCleanupConfigModel,
YunyinSignPayOrderModel: yunyinSignPayOrderModel,
// 代理相关模型(简化版 - 移除团队关系、返佣、升级、提现、实名认证、邀请码)
AgentModel: agentModel,
@@ -272,6 +286,7 @@ func NewServiceContext(c config.Config) *ServiceContext {
WechatPayService: wechatPayService,
ApplePayService: applePayService,
EasyPayService: easyPayService,
YunYinSignPayService: yunYinSignPayService,
ApiRequestService: apiRequestService,
AsynqServer: asynqServer,
AsynqService: asynqService,

View File

@@ -17,7 +17,7 @@ type PaymentCheckResp struct {
type PaymentReq struct {
Id string `json:"id"`
PayMethod string `json:"pay_method"` // 支付方式: wechat, alipay, appleiap, test(仅开发环境), test_empty(仅开发环境-空报告模式)
PayMethod string `json:"pay_method"` // 支付方式: wechat, alipay, easypay_alipay, appleiap, yunyinSignPay, yunyinSignPay_wechat, yunyinSignPay_alipay, test(仅开发环境), test_empty(仅开发环境-空报告模式)
PayType string `json:"pay_type" validate:"required,oneof=query"`
}

View File

@@ -0,0 +1,60 @@
package model
import (
"context"
"database/sql"
"fmt"
"github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
var _ YunyinSignPayOrderModel = (*customYunyinSignPayOrderModel)(nil)
type (
// YunyinSignPayOrderModel is an interface to be customized, add more methods here,
// and implement the added methods in customYunyinSignPayOrderModel.
YunyinSignPayOrderModel interface {
yunyinSignPayOrderModel
FindUnfinishedByUserId(ctx context.Context, userId string) (*YunyinSignPayOrder, error)
InsertWithSession(ctx context.Context, session sqlx.Session, data *YunyinSignPayOrder) (sql.Result, error)
}
customYunyinSignPayOrderModel struct {
*defaultYunyinSignPayOrderModel
}
)
// NewYunyinSignPayOrderModel returns a model for the database table.
func NewYunyinSignPayOrderModel(conn sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) YunyinSignPayOrderModel {
return &customYunyinSignPayOrderModel{
defaultYunyinSignPayOrderModel: newYunyinSignPayOrderModel(conn, c, opts...),
}
}
// FindUnfinishedByUserId 查找用户未完成的签署(待签署且待支付)
func (m *customYunyinSignPayOrderModel) FindUnfinishedByUserId(ctx context.Context, userId string) (*YunyinSignPayOrder, error) {
query := fmt.Sprintf("select %s from %s where `user_id` = ? and `sign_status` = ? and `pay_status` = ? and `del_state` = ? order by `create_time` desc limit 1", yunyinSignPayOrderRows, m.table)
var resp YunyinSignPayOrder
err := m.QueryRowNoCacheCtx(ctx, &resp, query, userId, 0, 0, 0)
switch err {
case nil:
return &resp, nil
case sqlx.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}
// InsertWithSession 在事务中插入数据
func (m *customYunyinSignPayOrderModel) InsertWithSession(ctx context.Context, session sqlx.Session, data *YunyinSignPayOrder) (sql.Result, error) {
query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, yunyinSignPayOrderRowsExpectAutoSet)
if session != nil {
return session.ExecCtx(ctx, query, data.Id, data.OrderId, data.UserId, data.TaskId, data.ParticipantId, data.Amount, data.PayType, data.SignStatus, data.PayStatus, data.SourceOrderCode, data.UserMobile, data.UserName, data.DeleteTime, data.DelState, data.Version)
}
return m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
return conn.ExecCtx(ctx, query, data.Id, data.OrderId, data.UserId, data.TaskId, data.ParticipantId, data.Amount, data.PayType, data.SignStatus, data.PayStatus, data.SourceOrderCode, data.UserMobile, data.UserName, data.DeleteTime, data.DelState, data.Version)
})
}

View File

@@ -0,0 +1,186 @@
// Code generated by goctl. DO NOT EDIT.
// versions:
// goctl version: 1.8.3
package model
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
"github.com/zeromicro/go-zero/core/stores/builder"
"github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/core/stores/sqlc"
"github.com/zeromicro/go-zero/core/stores/sqlx"
"github.com/zeromicro/go-zero/core/stringx"
)
var (
yunyinSignPayOrderFieldNames = builder.RawFieldNames(&YunyinSignPayOrder{})
yunyinSignPayOrderRows = strings.Join(yunyinSignPayOrderFieldNames, ",")
yunyinSignPayOrderRowsExpectAutoSet = strings.Join(stringx.Remove(yunyinSignPayOrderFieldNames, "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), ",")
yunyinSignPayOrderRowsWithPlaceHolder = strings.Join(stringx.Remove(yunyinSignPayOrderFieldNames, "`id`", "`create_at`", "`create_time`", "`created_at`", "`update_at`", "`update_time`", "`updated_at`"), "=?,") + "=?"
cacheJncYunyinSignPayOrderIdPrefix = "cache:jnc:yunyinSignPayOrder:id:"
cacheJncYunyinSignPayOrderOrderIdPrefix = "cache:jnc:yunyinSignPayOrder:orderId:"
cacheJncYunyinSignPayOrderTaskIdPrefix = "cache:jnc:yunyinSignPayOrder:taskId:"
)
type (
yunyinSignPayOrderModel interface {
Insert(ctx context.Context, data *YunyinSignPayOrder) (sql.Result, error)
FindOne(ctx context.Context, id string) (*YunyinSignPayOrder, error)
FindOneByOrderId(ctx context.Context, orderId string) (*YunyinSignPayOrder, error)
FindOneByTaskId(ctx context.Context, taskId string) (*YunyinSignPayOrder, error)
Update(ctx context.Context, data *YunyinSignPayOrder) error
Delete(ctx context.Context, id string) error
}
defaultYunyinSignPayOrderModel struct {
sqlc.CachedConn
table string
}
YunyinSignPayOrder struct {
Id string `db:"id"` // 主键IDUUID
OrderId string `db:"order_id"` // 订单ID关联order表
UserId string `db:"user_id"` // 用户ID用于查询该用户是否有未完成的签署
TaskId string `db:"task_id"` // 任务ID/流程IDflowId
ParticipantId string `db:"participant_id"` // 参与者ID签署方2的participantId
Amount float64 `db:"amount"` // 支付金额
PayType int64 `db:"pay_type"` // 支付类型0=微信支付1=支付宝支付
SignStatus int64 `db:"sign_status"` // 签署状态0=待签署1=已签署2=已取消
PayStatus int64 `db:"pay_status"` // 支付状态0=待支付1=已支付2=已退款
SourceOrderCode string `db:"source_order_code"` // 源订单号(我们平台的订单号,用于关联)
UserMobile sql.NullString `db:"user_mobile"` // 用户手机号(冗余字段,方便查询)
UserName sql.NullString `db:"user_name"` // 用户姓名(冗余字段,方便查询)
CreateTime time.Time `db:"create_time"` // 创建时间
UpdateTime time.Time `db:"update_time"` // 更新时间
DeleteTime sql.NullTime `db:"delete_time"` // 删除时间
DelState int64 `db:"del_state"` // 删除状态0=未删除1=已删除
Version int64 `db:"version"` // 版本号(乐观锁)
}
)
func newYunyinSignPayOrderModel(conn sqlx.SqlConn, c cache.CacheConf, opts ...cache.Option) *defaultYunyinSignPayOrderModel {
return &defaultYunyinSignPayOrderModel{
CachedConn: sqlc.NewConn(conn, c, opts...),
table: "`yunyin_sign_pay_order`",
}
}
func (m *defaultYunyinSignPayOrderModel) Delete(ctx context.Context, id string) error {
data, err := m.FindOne(ctx, id)
if err != nil {
return err
}
jncYunyinSignPayOrderIdKey := fmt.Sprintf("%s%v", cacheJncYunyinSignPayOrderIdPrefix, id)
jncYunyinSignPayOrderOrderIdKey := fmt.Sprintf("%s%v", cacheJncYunyinSignPayOrderOrderIdPrefix, data.OrderId)
jncYunyinSignPayOrderTaskIdKey := fmt.Sprintf("%s%v", cacheJncYunyinSignPayOrderTaskIdPrefix, data.TaskId)
_, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
query := fmt.Sprintf("delete from %s where `id` = ?", m.table)
return conn.ExecCtx(ctx, query, id)
}, jncYunyinSignPayOrderIdKey, jncYunyinSignPayOrderOrderIdKey, jncYunyinSignPayOrderTaskIdKey)
return err
}
func (m *defaultYunyinSignPayOrderModel) FindOne(ctx context.Context, id string) (*YunyinSignPayOrder, error) {
jncYunyinSignPayOrderIdKey := fmt.Sprintf("%s%v", cacheJncYunyinSignPayOrderIdPrefix, id)
var resp YunyinSignPayOrder
err := m.QueryRowCtx(ctx, &resp, jncYunyinSignPayOrderIdKey, func(ctx context.Context, conn sqlx.SqlConn, v any) error {
query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", yunyinSignPayOrderRows, m.table)
return conn.QueryRowCtx(ctx, v, query, id)
})
switch err {
case nil:
return &resp, nil
case sqlc.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}
func (m *defaultYunyinSignPayOrderModel) FindOneByOrderId(ctx context.Context, orderId string) (*YunyinSignPayOrder, error) {
jncYunyinSignPayOrderOrderIdKey := fmt.Sprintf("%s%v", cacheJncYunyinSignPayOrderOrderIdPrefix, orderId)
var resp YunyinSignPayOrder
err := m.QueryRowIndexCtx(ctx, &resp, jncYunyinSignPayOrderOrderIdKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v any) (i any, e error) {
query := fmt.Sprintf("select %s from %s where `order_id` = ? limit 1", yunyinSignPayOrderRows, m.table)
if err := conn.QueryRowCtx(ctx, &resp, query, orderId); err != nil {
return nil, err
}
return resp.Id, nil
}, m.queryPrimary)
switch err {
case nil:
return &resp, nil
case sqlc.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}
func (m *defaultYunyinSignPayOrderModel) FindOneByTaskId(ctx context.Context, taskId string) (*YunyinSignPayOrder, error) {
jncYunyinSignPayOrderTaskIdKey := fmt.Sprintf("%s%v", cacheJncYunyinSignPayOrderTaskIdPrefix, taskId)
var resp YunyinSignPayOrder
err := m.QueryRowIndexCtx(ctx, &resp, jncYunyinSignPayOrderTaskIdKey, m.formatPrimary, func(ctx context.Context, conn sqlx.SqlConn, v any) (i any, e error) {
query := fmt.Sprintf("select %s from %s where `task_id` = ? limit 1", yunyinSignPayOrderRows, m.table)
if err := conn.QueryRowCtx(ctx, &resp, query, taskId); err != nil {
return nil, err
}
return resp.Id, nil
}, m.queryPrimary)
switch err {
case nil:
return &resp, nil
case sqlc.ErrNotFound:
return nil, ErrNotFound
default:
return nil, err
}
}
func (m *defaultYunyinSignPayOrderModel) Insert(ctx context.Context, data *YunyinSignPayOrder) (sql.Result, error) {
jncYunyinSignPayOrderIdKey := fmt.Sprintf("%s%v", cacheJncYunyinSignPayOrderIdPrefix, data.Id)
jncYunyinSignPayOrderOrderIdKey := fmt.Sprintf("%s%v", cacheJncYunyinSignPayOrderOrderIdPrefix, data.OrderId)
jncYunyinSignPayOrderTaskIdKey := fmt.Sprintf("%s%v", cacheJncYunyinSignPayOrderTaskIdPrefix, data.TaskId)
ret, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
query := fmt.Sprintf("insert into %s (%s) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", m.table, yunyinSignPayOrderRowsExpectAutoSet)
return conn.ExecCtx(ctx, query, data.Id, data.OrderId, data.UserId, data.TaskId, data.ParticipantId, data.Amount, data.PayType, data.SignStatus, data.PayStatus, data.SourceOrderCode, data.UserMobile, data.UserName, data.DeleteTime, data.DelState, data.Version)
}, jncYunyinSignPayOrderIdKey, jncYunyinSignPayOrderOrderIdKey, jncYunyinSignPayOrderTaskIdKey)
return ret, err
}
func (m *defaultYunyinSignPayOrderModel) Update(ctx context.Context, newData *YunyinSignPayOrder) error {
data, err := m.FindOne(ctx, newData.Id)
if err != nil {
return err
}
jncYunyinSignPayOrderIdKey := fmt.Sprintf("%s%v", cacheJncYunyinSignPayOrderIdPrefix, data.Id)
jncYunyinSignPayOrderOrderIdKey := fmt.Sprintf("%s%v", cacheJncYunyinSignPayOrderOrderIdPrefix, data.OrderId)
jncYunyinSignPayOrderTaskIdKey := fmt.Sprintf("%s%v", cacheJncYunyinSignPayOrderTaskIdPrefix, data.TaskId)
_, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, yunyinSignPayOrderRowsWithPlaceHolder)
return conn.ExecCtx(ctx, query, newData.OrderId, newData.UserId, newData.TaskId, newData.ParticipantId, newData.Amount, newData.PayType, newData.SignStatus, newData.PayStatus, newData.SourceOrderCode, newData.UserMobile, newData.UserName, newData.DeleteTime, newData.DelState, newData.Version, newData.Id)
}, jncYunyinSignPayOrderIdKey, jncYunyinSignPayOrderOrderIdKey, jncYunyinSignPayOrderTaskIdKey)
return err
}
func (m *defaultYunyinSignPayOrderModel) formatPrimary(primary any) string {
return fmt.Sprintf("%s%v", cacheJncYunyinSignPayOrderIdPrefix, primary)
}
func (m *defaultYunyinSignPayOrderModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary any) error {
query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", yunyinSignPayOrderRows, m.table)
return conn.QueryRowCtx(ctx, v, query, primary)
}
func (m *defaultYunyinSignPayOrderModel) tableName() string {
return m.table
}

View File

@@ -44,7 +44,7 @@ $tables = @(
# "example", # 示例
# "feature", # 功能
# "global_notifications", # 全局通知
"order" # 订单
# "order" # 订单
# "order_refund", # 订单退款
# "product", # 产品
# "product_feature", # 产品功能
@@ -55,7 +55,7 @@ $tables = @(
# "user", # 用户
# "user_auth", # 用户认证
# "user_temp" # 临时用户
"yunyin_sign_pay_order" # 云印签签署流程
)
# 为每个表生成模型
@@ -64,63 +64,63 @@ foreach ($table in $tables) {
goctl model mysql datasource -url="jnc:5vg67b3UNHu8@tcp(127.0.0.1:21301)/jnc" -table="$table" -dir="./model" --home="$HOME_DIR" -cache=true --style=goZero
# 移动生成的文件到目标目录
if (Test-Path $OUTPUT_DIR) {
$sourceFiles = Get-ChildItem -Path $OUTPUT_DIR -File
# if (Test-Path $OUTPUT_DIR) {
# $sourceFiles = Get-ChildItem -Path $OUTPUT_DIR -File
foreach ($file in $sourceFiles) {
$fileName = $file.Name
$targetPath = Join-Path $TARGET_DIR $fileName
$sourcePath = $file.FullName
# foreach ($file in $sourceFiles) {
# $fileName = $file.Name
# $targetPath = Join-Path $TARGET_DIR $fileName
# $sourcePath = $file.FullName
# 检查文件类型并决定是否移动
$shouldMove = $false
$shouldOverwrite = $false
# # 检查文件类型并决定是否移动
# $shouldMove = $false
# $shouldOverwrite = $false
if ($fileName -eq "vars.go") {
# vars.go: 如果目标目录不存在才移动
if (-not (Test-Path $targetPath)) {
$shouldMove = $true
Write-Host " 移动 $fileName (vars.go 不存在于目标目录)" -ForegroundColor Yellow
}
else {
Write-Host " 跳过 $fileName (vars.go 已存在于目标目录,防止覆盖)" -ForegroundColor Cyan
}
}
elseif ($fileName -match "_gen\.go$") {
# 带 _gen 后缀的文件: 直接覆盖
$shouldMove = $true
$shouldOverwrite = $true
Write-Host " 移动 $fileName (覆盖 _gen 文件)" -ForegroundColor Yellow
}
else {
# 不带 _gen 后缀的文件: 如果目标目录不存在才移动
if (-not (Test-Path $targetPath)) {
$shouldMove = $true
Write-Host " 移动 $fileName (非 _gen 文件不存在于目标目录)" -ForegroundColor Yellow
}
else {
Write-Host " 跳过 $fileName (非 _gen 文件已存在于目标目录,防止覆盖)" -ForegroundColor Cyan
}
}
# if ($fileName -eq "vars.go") {
# # vars.go: 如果目标目录不存在才移动
# if (-not (Test-Path $targetPath)) {
# $shouldMove = $true
# Write-Host " 移动 $fileName (vars.go 不存在于目标目录)" -ForegroundColor Yellow
# }
# else {
# Write-Host " 跳过 $fileName (vars.go 已存在于目标目录,防止覆盖)" -ForegroundColor Cyan
# }
# }
# elseif ($fileName -match "_gen\.go$") {
# # 带 _gen 后缀的文件: 直接覆盖
# $shouldMove = $true
# $shouldOverwrite = $true
# Write-Host " 移动 $fileName (覆盖 _gen 文件)" -ForegroundColor Yellow
# }
# else {
# # 不带 _gen 后缀的文件: 如果目标目录不存在才移动
# if (-not (Test-Path $targetPath)) {
# $shouldMove = $true
# Write-Host " 移动 $fileName (非 _gen 文件不存在于目标目录)" -ForegroundColor Yellow
# }
# else {
# Write-Host " 跳过 $fileName (非 _gen 文件已存在于目标目录,防止覆盖)" -ForegroundColor Cyan
# }
# }
# 执行移动操作
if ($shouldMove) {
# 确保目标目录存在
if (-not (Test-Path $TARGET_DIR)) {
New-Item -ItemType Directory -Path $TARGET_DIR -Force | Out-Null
}
# # 执行移动操作
# if ($shouldMove) {
# # 确保目标目录存在
# if (-not (Test-Path $TARGET_DIR)) {
# New-Item -ItemType Directory -Path $TARGET_DIR -Force | Out-Null
# }
# 如果目标文件存在且需要覆盖,先删除
if ($shouldOverwrite -and (Test-Path $targetPath)) {
Remove-Item -Path $targetPath -Force
}
# # 如果目标文件存在且需要覆盖,先删除
# if ($shouldOverwrite -and (Test-Path $targetPath)) {
# Remove-Item -Path $targetPath -Force
# }
# 移动文件
Move-Item -Path $sourcePath -Destination $targetPath -Force
Write-Host " ✓ 已移动到: $targetPath" -ForegroundColor Green
}
}
}
# # 移动文件
# Move-Item -Path $sourcePath -Destination $targetPath -Force
# Write-Host " ✓ 已移动到: $targetPath" -ForegroundColor Green
# }
# }
# }
}
Write-Host ""

View File

@@ -0,0 +1,5 @@
package model
import "github.com/zeromicro/go-zero/core/stores/sqlx"
var ErrNotFound = sqlx.ErrNotFound

View File

@@ -0,0 +1,33 @@
-- ============================================
-- 云印签支付订单表
-- ============================================
CREATE TABLE `yunyin_sign_pay_order` (
`id` CHAR(36) NOT NULL COMMENT '主键IDUUID',
`order_id` CHAR(36) NOT NULL COMMENT '订单ID关联order表',
`user_id` CHAR(36) NOT NULL COMMENT '用户ID用于查询该用户是否有未完成的签署',
`task_id` VARCHAR(100) NOT NULL COMMENT '任务ID/流程IDflowId',
`participant_id` VARCHAR(100) NOT NULL COMMENT '参与者ID签署方2的participantId',
`amount` DECIMAL(10,2) NOT NULL COMMENT '支付金额',
`pay_type` TINYINT NOT NULL COMMENT '支付类型0=微信支付1=支付宝支付',
`sign_status` TINYINT NOT NULL DEFAULT 0 COMMENT '签署状态0=待签署1=已签署2=已取消',
`pay_status` TINYINT NOT NULL DEFAULT 0 COMMENT '支付状态0=待支付1=已支付2=已退款',
`source_order_code` VARCHAR(100) NOT NULL COMMENT '源订单号(我们平台的订单号,用于关联)',
`user_mobile` VARCHAR(20) DEFAULT NULL COMMENT '用户手机号(冗余字段,方便查询)',
`user_name` VARCHAR(100) DEFAULT NULL COMMENT '用户姓名(冗余字段,方便查询)',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`delete_time` DATETIME DEFAULT NULL COMMENT '删除时间',
`del_state` TINYINT NOT NULL DEFAULT 0 COMMENT '删除状态0=未删除1=已删除',
`version` BIGINT NOT NULL DEFAULT 0 COMMENT '版本号(乐观锁)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_id` (`order_id`),
UNIQUE KEY `uk_task_id` (`task_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_participant_id` (`participant_id`),
KEY `idx_source_order_code` (`source_order_code`),
KEY `idx_sign_status` (`sign_status`),
KEY `idx_pay_status` (`pay_status`),
KEY `idx_user_mobile` (`user_mobile`),
KEY `idx_create_time` (`create_time`),
KEY `idx_del_state` (`del_state`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='云印签支付订单表';

View File

@@ -0,0 +1,207 @@
# 云印签支付流程完整性检查
## 一、支付创建流程
### 1. 前端流程 ✅
- **位置**: `jnc-webview/src/components/Payment.vue`
- **步骤**:
1. 用户选择 `yunyinSignPay_wechat``yunyinSignPay_alipay`
2. 调用 `/pay/payment` 接口
3. 获取 `prepay_id``prepay_data`(支付链接)
4. 直接跳转到支付链接:`window.location.href = prepayUrl`
### 2. 后端流程 ✅
- **位置**: `jnc-server/app/main/api/internal/logic/pay/paymentlogic.go`
- **步骤**:
1. 接收支付请求
2. 从查询缓存获取用户姓名和手机号
3. **查询是否有未完成的签署**(复用逻辑)
4. 如果有未完成签署:
- 复用 `task_id``participant_id`
- 获取新的支付链接
5. 如果没有未完成签署:
- 获取 token 和操作ID带缓存
- 发起签署流程
- 获取支付链接
6. 创建订单记录(`order` 表)
7. 创建云印签订单记录(`yunyin_sign_pay_order` 表)
8. 返回支付链接(字符串类型,使用 `PrepayId` 字段)
### 3. 数据存储 ✅
- **订单表** (`order`):
- `order_no`: 订单号
- `payment_platform`: `yunyinSignPay_wechat``yunyinSignPay_alipay`
- `status`: `pending`
- **注意**: 不再在 `remark` 中存储云印签信息
- **云印签订单表** (`yunyin_sign_pay_order`):
- `order_id`: 关联订单ID
- `user_id`: 用户ID用于查询未完成签署
- `task_id`: 任务ID/流程ID唯一索引
- `participant_id`: 参与者ID
- `source_order_code`: 源订单号(用于查询支付状态)
- `amount`: 支付金额
- `pay_type`: 支付类型0=微信1=支付宝)
- `sign_status`: 签署状态0=待签署)
- `pay_status`: 支付状态0=待支付)
- `user_mobile`: 用户手机号(冗余字段)
- `user_name`: 用户姓名(冗余字段)
## 二、支付状态查询流程
### 1. 前端轮询 ✅
- **位置**: `jnc-webview/src/views/PaymentResult.vue`
- **步骤**:
1. 用户从支付页面返回后,进入支付结果页面
2. 从 URL 参数获取 `order_no``out_trade_no`
3. 首次调用 `/pay/check` 接口
4. 如果状态是 `pending`开始轮询每3秒一次最多30次
5. 如果状态变为 `paid``refunded`,停止轮询并跳转到结果页面
### 2. 后端主动查询 ✅
- **位置**: `jnc-server/app/main/api/internal/logic/pay/paymentchecklogic.go`
- **步骤**:
1. 查询订单状态
2. 如果订单状态是 `pending` 且支付平台是云印签:
- 调用 `QueryPayeeBill` 查询云印签平台
- 使用 `sourceOrderCode`(订单号)查询
3. 根据云印签返回的 `payStatus` 更新订单状态:
- `2` (支付成功) → `paid`
- `3` (支付失败) → `failed`
- `4` (已退款) → `refunded`
- `0, 1` (订单生成/支付中) → 保持 `pending`
4. 同步更新 `yunyin_sign_pay_order` 表的支付状态
5. 如果支付成功,发送异步任务处理后续流程
### 3. 状态映射 ✅
- **云印签 payStatus**:
- `0`: 订单生成
- `1`: 支付中
- `2`: 支付成功
- `3`: 支付失败
- `4`: 已退款
- **我们的订单状态**:
- `pending`: 待支付
- `paid`: 已支付
- `failed`: 支付失败
- `refunded`: 已退款
- **我们的支付状态** (`yunyin_sign_pay_order.pay_status`):
- `0`: 待支付
- `1`: 已支付
- `2`: 已退款
## 三、前后端数据字段一致性 ✅
### 1. 支付创建接口 (`/pay/payment`)
- **请求字段**: `pay_method` (支持 `yunyinSignPay_wechat`, `yunyinSignPay_alipay`)
- **响应字段**:
- `prepay_id`: 支付链接(字符串类型)
- `prepay_data`: 备用字段(对象类型)
- `order_no`: 订单号
- **前端读取**: `data.value.data.prepay_id || data.value.data.prepay_data`
### 2. 支付状态查询接口 (`/pay/check`)
- **请求字段**: `order_no`
- **响应字段**:
- `type`: `"query"`
- `status`: `"pending"` | `"paid"` | `"failed"` | `"refunded"`
- **前端处理**: 根据 `status` 判断是否继续轮询 ✅
## 四、潜在问题和改进建议
### ⚠️ 问题1: 支付完成后用户可能不会自动跳转
**现状**: 云印签支付完成后,用户可能不会自动跳转回我们的页面
**解决方案**:
- ✅ 已实现:前端通过轮询主动查询支付状态
- ✅ 已实现:用户手动返回后,支付结果页面会自动轮询
### ⚠️ 问题2: 订单状态更新需要检查并发
**现状**: 使用乐观锁更新订单状态,但需要确保不会重复更新
**检查**: ✅ 已实现:使用 `UpdateWithVersion` 和检查 `order.Status == "pending"`
### ⚠️ 问题3: 查询失败时的处理
**现状**: 如果查询云印签平台失败,会记录错误但继续返回当前状态
**检查**: ✅ 已实现:错误处理完善,不影响正常流程
### ⚠️ 问题4: 未找到收款单记录的处理
**现状**: 如果订单刚创建,云印签平台可能还没有记录
**检查**: ✅ 已实现:返回错误但不影响,继续返回 `pending` 状态,前端会继续轮询
## 五、完整流程验证
### 场景1: 首次支付(创建新签署流程)✅
1. 用户选择云印签支付
2. 后端查询无未完成签署
3. 创建新签署流程
4. 创建订单和云印签订单记录
5. 返回支付链接
6. 前端跳转到支付页面
7. 用户完成支付
8. 前端轮询查询状态
9. 后端主动查询云印签平台
10. 更新订单状态为 `paid`
11. 发送异步任务处理后续流程
12. 前端跳转到结果页面
### 场景2: 复用未完成签署 ✅
1. 用户之前创建了签署但未支付
2. 用户再次选择云印签支付
3. 后端查询到未完成签署
4. 复用 `task_id``participant_id`
5. 获取新的支付链接
6. 创建新订单和云印签订单记录(关联新的订单)
7. 返回支付链接
8. 后续流程同场景1
### 场景3: 支付失败 ✅
1. 用户支付失败
2. 前端轮询查询状态
3. 后端查询到 `payStatus = 3`
4. 更新订单状态为 `failed`
5. 前端显示支付失败提示
### 场景4: 退款 ✅
1. 订单已支付
2. 发生退款
3. 前端轮询查询状态
4. 后端查询到 `payStatus = 4`
5. 更新订单状态为 `refunded`
6. 更新退款时间
7. 前端显示退款状态
## 六、总结
### ✅ 已完成的流程
1. 支付创建流程(包括复用逻辑)
2. 支付状态主动查询
3. 订单状态自动更新
4. 云印签订单表数据同步
5. 前后端数据字段一致性
6. 错误处理和边界情况
### ✅ 代码质量
- 使用了事务保证数据一致性
- 使用了乐观锁避免并发问题
- 错误处理完善
- 日志记录详细
### ✅ 前后端联调
- API 接口定义清晰
- 数据字段命名一致
- 状态映射正确
- 轮询机制完善
## 七、建议测试点
1. **首次支付流程**: 验证创建新签署流程是否正常
2. **复用签署流程**: 验证复用逻辑是否正常工作
3. **支付成功**: 验证状态更新和后续流程是否正常
4. **支付失败**: 验证失败状态是否正确处理
5. **退款**: 验证退款状态是否正确处理
6. **网络异常**: 验证查询失败时的容错处理
7. **并发支付**: 验证同一用户多次支付的场景
8. **订单状态一致性**: 验证订单表和云印签订单表的状态是否同步