diff --git a/YunYinSignPay.json b/YunYinSignPay.json new file mode 100644 index 0000000..f372f80 --- /dev/null +++ b/YunYinSignPay.json @@ -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": [] +} \ No newline at end of file diff --git a/app/main/api/desc/front/pay.api b/app/main/api/desc/front/pay.api index 382bfc0..04bb039 100644 --- a/app/main/api/desc/front/pay.api +++ b/app/main/api/desc/front/pay.api @@ -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"` } diff --git a/app/main/api/etc/main.dev.yaml b/app/main/api/etc/main.dev.yaml index 19febe5..b291734 100644 --- a/app/main/api/etc/main.dev.yaml +++ b/app/main/api/etc/main.dev.yaml @@ -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: "信息服务授权书" # 需要配置实际的模板名称 \ No newline at end of file diff --git a/app/main/api/etc/main.yaml b/app/main/api/etc/main.yaml index 51c7de1..51b6a0e 100644 --- a/app/main/api/etc/main.yaml +++ b/app/main/api/etc/main.yaml @@ -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: "信息服务授权书" # 需要配置实际的模板名称 \ No newline at end of file diff --git a/app/main/api/internal/config/config.go b/app/main/api/internal/config/config.go index cbb5e63..74ec3c3 100644 --- a/app/main/api/internal/config/config.go +++ b/app/main/api/internal/config/config.go @@ -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 // 模板名称 +} diff --git a/app/main/api/internal/logic/pay/paymentchecklogic.go b/app/main/api/internal/logic/pay/paymentchecklogic.go index e570fb9..20436c2 100644 --- a/app/main/api/internal/logic/pay/paymentchecklogic.go +++ b/app/main/api/internal/logic/pay/paymentchecklogic.go @@ -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, diff --git a/app/main/api/internal/logic/pay/paymentlogic.go b/app/main/api/internal/logic/pay/paymentlogic.go index 868450c..3304963 100644 --- a/app/main/api/internal/logic/pay/paymentlogic.go +++ b/app/main/api/internal/logic/pay/paymentlogic.go @@ -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, ¶ms); 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 } diff --git a/app/main/api/internal/service/yunyinSignPayService.go b/app/main/api/internal/service/yunyinSignPayService.go new file mode 100644 index 0000000..ba30b50 --- /dev/null +++ b/app/main/api/internal/service/yunyinSignPayService.go @@ -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 + + // 存储到Redis,2小时过期 + 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 + + // 存储到Redis,2小时过期 + 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 // 流程ID(flowId) +} + +// 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 +} diff --git a/app/main/api/internal/svc/servicecontext.go b/app/main/api/internal/svc/servicecontext.go index c01c4a3..7422133 100644 --- a/app/main/api/internal/svc/servicecontext.go +++ b/app/main/api/internal/svc/servicecontext.go @@ -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, diff --git a/app/main/api/internal/types/pay.go b/app/main/api/internal/types/pay.go index 57e9f44..5458ac4 100644 --- a/app/main/api/internal/types/pay.go +++ b/app/main/api/internal/types/pay.go @@ -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"` } diff --git a/app/main/model/yunyinSignPayOrderModel.go b/app/main/model/yunyinSignPayOrderModel.go new file mode 100644 index 0000000..32fdf51 --- /dev/null +++ b/app/main/model/yunyinSignPayOrderModel.go @@ -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) + }) +} diff --git a/app/main/model/yunyinSignPayOrderModel_gen.go b/app/main/model/yunyinSignPayOrderModel_gen.go new file mode 100644 index 0000000..e776a90 --- /dev/null +++ b/app/main/model/yunyinSignPayOrderModel_gen.go @@ -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"` // 主键ID(UUID) + OrderId string `db:"order_id"` // 订单ID(关联order表) + UserId string `db:"user_id"` // 用户ID(用于查询该用户是否有未完成的签署) + TaskId string `db:"task_id"` // 任务ID/流程ID(flowId) + 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 +} diff --git a/deploy/script/gen_models.ps1 b/deploy/script/gen_models.ps1 index 0aa1313..9793c18 100644 --- a/deploy/script/gen_models.ps1 +++ b/deploy/script/gen_models.ps1 @@ -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 "" diff --git a/deploy/script/model/vars.go b/deploy/script/model/vars.go new file mode 100644 index 0000000..69ca814 --- /dev/null +++ b/deploy/script/model/vars.go @@ -0,0 +1,5 @@ +package model + +import "github.com/zeromicro/go-zero/core/stores/sqlx" + +var ErrNotFound = sqlx.ErrNotFound diff --git a/deploy/sql/yunyin_sign_pay_order.sql b/deploy/sql/yunyin_sign_pay_order.sql new file mode 100644 index 0000000..8091b3f --- /dev/null +++ b/deploy/sql/yunyin_sign_pay_order.sql @@ -0,0 +1,33 @@ +-- ============================================ +-- 云印签支付订单表 +-- ============================================ +CREATE TABLE `yunyin_sign_pay_order` ( + `id` CHAR(36) NOT NULL COMMENT '主键ID(UUID)', + `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/流程ID(flowId)', + `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='云印签支付订单表'; diff --git a/云印签支付流程检查.md b/云印签支付流程检查.md new file mode 100644 index 0000000..f45b3d4 --- /dev/null +++ b/云印签支付流程检查.md @@ -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. **订单状态一致性**: 验证订单表和云印签订单表的状态是否同步