Files
jnc-server/app/main/api/internal/service/yunyinSignPayService.go
2026-01-16 17:01:36 +08:00

855 lines
27 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

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

package 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
}
// RefundPayeeBillRequest 退款请求
type RefundPayeeBillRequest struct {
SourceOrderCode string `json:"sourceOrderCode,omitempty"` // 来源订单号(与 participateId 二选一)
ParticipateId int64 `json:"participateId,omitempty"` // 参与方ID与 sourceOrderCode 二选一)
RefundAmount float64 `json:"refundAmount"` // 退款金额(必填)
RefundReason string `json:"refundReason,omitempty"` // 退款原因(可选)
}
// RefundPayeeBillResponse 退款响应
type RefundPayeeBillResponse struct {
Code interface{} `json:"code"` // 返回码,可能是字符串"200"或数字200
Msg string `json:"msg"` // 返回码的描述信息
Data interface{} `json:"data,omitempty"`
}
// GetCodeInt 获取 code 的 int 值
func (r *RefundPayeeBillResponse) 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
}
}
// RefundPayeeBill 发起退款
func (y *YunYinSignPayService) RefundPayeeBill(ctx context.Context, sourceOrderCode string, participateId int64, refundAmount float64, refundReason string) error {
// 1. 获取token和操作ID带缓存
accessToken, err := y.GetAccessToken(ctx)
if err != nil {
return fmt.Errorf("获取云印签token失败: %v", err)
}
operationUserId, err := y.GetUserId(ctx, accessToken)
if err != nil {
return fmt.Errorf("获取云印签操作ID失败: %v", err)
}
// 2. 如果只提供了 sourceOrderCode需要先查询收款单获取 participateId
if participateId == 0 && sourceOrderCode != "" {
// 查询收款单列表获取 participateId
reqBody := QueryPayeeBillRequest{
SourceOrderCode: sourceOrderCode,
ListPageNo: 1,
ListPageSize: 10,
}
jsonData, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("序列化查询请求失败: %v", err)
}
url := fmt.Sprintf("%s/signFlowBill/payeeBillList", 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", operationUserId)
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 queryResp QueryPayeeBillResponse
if err := json.Unmarshal(body, &queryResp); err != nil {
return fmt.Errorf("解析查询响应失败: %v, 响应内容: %s", err, string(body))
}
codeInt := queryResp.GetCodeInt()
if codeInt != 200 {
return fmt.Errorf("查询收款单失败: %s", queryResp.Msg)
}
if len(queryResp.Data) == 0 {
return fmt.Errorf("未找到匹配的收款单记录,订单号: %s", sourceOrderCode)
}
// 获取第一条记录的 participateId
billItem := queryResp.Data[0]
participateId = billItem.ParticipateID
logx.Infof("通过订单号查询到参与方ID: %d", participateId)
}
// 3. 验证参数
if participateId == 0 {
return fmt.Errorf("参与方ID不能为空请提供 participateId 或 sourceOrderCode")
}
if refundAmount <= 0 {
return fmt.Errorf("退款金额必须大于0")
}
// 4. 构建退款请求
refundReq := RefundPayeeBillRequest{
RefundAmount: refundAmount,
RefundReason: refundReason,
}
// 优先使用 participateId
if participateId > 0 {
refundReq.ParticipateId = participateId
} else if sourceOrderCode != "" {
// 如果 participateId 仍然为0使用 sourceOrderCode
refundReq.SourceOrderCode = sourceOrderCode
}
jsonData, err := json.Marshal(refundReq)
if err != nil {
return fmt.Errorf("序列化退款请求失败: %v", err)
}
// 5. 调用退款API
url := fmt.Sprintf("%s/signFlowBill/refund", 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", operationUserId)
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 refundResp RefundPayeeBillResponse
if err := json.Unmarshal(body, &refundResp); err != nil {
return fmt.Errorf("解析退款响应失败: %v, 响应内容: %s", err, string(body))
}
// 6. 检查响应码
codeInt := refundResp.GetCodeInt()
if codeInt != 200 {
return fmt.Errorf("退款失败: %s", refundResp.Msg)
}
logx.Infof("云印签退款成功参与方ID: %d, 退款金额: %.2f, 退款原因: %s", participateId, refundAmount, refundReason)
return nil
}