diff --git a/app/main/api/desc/front/pay.api b/app/main/api/desc/front/pay.api index 04bb039..a4eb631 100644 --- a/app/main/api/desc/front/pay.api +++ b/app/main/api/desc/front/pay.api @@ -26,6 +26,10 @@ service main { // 易支付回调 @handler EasyPayCallback get /pay/easypay/callback + + // 云印签支付回调 + @handler YunYinSignPayCallback + post /pay/yunyinsign/callback } @server ( @@ -44,6 +48,10 @@ service main { @handler PaymentCheck post /pay/check (PaymentCheckReq) returns (PaymentCheckResp) + + // 云印签退款 + @handler YunYinSignPayRefund + post /pay/yunyinsign/refund (YunYinSignPayRefundReq) returns (YunYinSignPayRefundResp) } type ( @@ -72,5 +80,15 @@ type ( OrderID string `json:"order_id" validate:"required"` TransactionReceipt string `json:"transaction_receipt" validate:"required"` } + YunYinSignPayRefundReq { + OrderNo string `json:"order_no"` // 订单号(sourceOrderCode),与 participate_id 二选一 + ParticipateId int64 `json:"participate_id,omitempty"` // 参与方ID,与 order_no 二选一 + RefundAmount float64 `json:"refund_amount" validate:"required,gt=0"` // 退款金额,必须大于0 + RefundReason string `json:"refund_reason,omitempty"` // 退款原因 + } + YunYinSignPayRefundResp { + Success bool `json:"success"` + Message string `json:"message"` + } ) diff --git a/app/main/api/internal/handler/pay/yunyinsignpaycallbackhandler.go b/app/main/api/internal/handler/pay/yunyinsignpaycallbackhandler.go new file mode 100644 index 0000000..9e5c5b6 --- /dev/null +++ b/app/main/api/internal/handler/pay/yunyinsignpaycallbackhandler.go @@ -0,0 +1,17 @@ +package pay + +import ( + "net/http" + + "jnc-server/app/main/api/internal/logic/pay" + "jnc-server/app/main/api/internal/svc" + "jnc-server/common/result" +) + +func YunYinSignPayCallbackHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := pay.NewYunYinSignPayCallbackLogic(r.Context(), svcCtx) + err := l.YunYinSignPayCallback(w, r) + result.HttpResult(r, w, nil, err) + } +} diff --git a/app/main/api/internal/handler/pay/yunyinsignpayrefundhandler.go b/app/main/api/internal/handler/pay/yunyinsignpayrefundhandler.go new file mode 100644 index 0000000..5bdfdc5 --- /dev/null +++ b/app/main/api/internal/handler/pay/yunyinsignpayrefundhandler.go @@ -0,0 +1,30 @@ +package pay + +import ( + "net/http" + + "jnc-server/app/main/api/internal/logic/pay" + "jnc-server/app/main/api/internal/svc" + "jnc-server/app/main/api/internal/types" + "jnc-server/common/result" + "jnc-server/pkg/lzkit/validator" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +func YunYinSignPayRefundHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.YunYinSignPayRefundReq + if err := httpx.Parse(r, &req); err != nil { + result.ParamErrorResult(r, w, err) + return + } + if err := validator.Validate(req); err != nil { + result.ParamValidateErrorResult(r, w, err) + return + } + l := pay.NewYunYinSignPayRefundLogic(r.Context(), svcCtx) + resp, err := l.YunYinSignPayRefund(&req) + result.HttpResult(r, w, resp, err) + } +} diff --git a/app/main/api/internal/handler/routes.go b/app/main/api/internal/handler/routes.go index 8f6e62d..6b0be25 100644 --- a/app/main/api/internal/handler/routes.go +++ b/app/main/api/internal/handler/routes.go @@ -786,6 +786,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/pay/wechat/refund_callback", Handler: pay.WechatPayRefundCallbackHandler(serverCtx), }, + { + Method: http.MethodPost, + Path: "/pay/yunyinsign/callback", + Handler: pay.YunYinSignPayCallbackHandler(serverCtx), + }, }, rest.WithPrefix("/api/v1"), ) @@ -809,6 +814,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/pay/payment", Handler: pay.PaymentHandler(serverCtx), }, + { + Method: http.MethodPost, + Path: "/pay/yunyinsign/refund", + Handler: pay.YunYinSignPayRefundHandler(serverCtx), + }, }..., ), rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret), diff --git a/app/main/api/internal/logic/pay/yunyinsignpaycallbacklogic.go b/app/main/api/internal/logic/pay/yunyinsignpaycallbacklogic.go new file mode 100644 index 0000000..998fa6b --- /dev/null +++ b/app/main/api/internal/logic/pay/yunyinsignpaycallbacklogic.go @@ -0,0 +1,191 @@ +package pay + +import ( + "context" + "jnc-server/app/main/api/internal/svc" + "jnc-server/pkg/lzkit/lzUtils" + "net/http" + "strconv" + "time" + + "github.com/zeromicro/go-zero/core/logx" +) + +type YunYinSignPayCallbackLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewYunYinSignPayCallbackLogic(ctx context.Context, svcCtx *svc.ServiceContext) *YunYinSignPayCallbackLogic { + return &YunYinSignPayCallbackLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// YunYinSignPayCallback 云印签支付回调处理 +func (l *YunYinSignPayCallbackLogic) YunYinSignPayCallback(w http.ResponseWriter, r *http.Request) error { + // 检查云印签支付服务是否启用 + if l.svcCtx.YunYinSignPayService == nil { + logx.Errorf("云印签支付服务未启用") + w.WriteHeader(http.StatusOK) + w.Write([]byte("success")) + return nil + } + + // 解析表单数据 + if err := r.ParseForm(); err != nil { + logx.Errorf("云印签回调,解析表单数据失败: %v", err) + w.WriteHeader(http.StatusOK) + w.Write([]byte("success")) + return nil + } + + // 获取回调参数 + tradeState := r.FormValue("tradeState") + orderType := r.FormValue("orderType") + payOrderNo := r.FormValue("payOrderNo") + payAmount := r.FormValue("payAmount") + flowId := r.FormValue("flowId") + sourceOrderCode := r.FormValue("sourceOrderCode") + channelOrderNo := r.FormValue("channelOrderNo") + paySuccessTime := r.FormValue("paySuccessTime") + + // 记录回调日志 + logx.Infof("云印签回调通知,订单号: %s, 支付状态: %s, 订单类型: %s, 支付单号: %s, 金额: %s, 流程ID: %s", + sourceOrderCode, tradeState, orderType, payOrderNo, payAmount, flowId) + + // 只处理付款通知(orderType == "pay") + if orderType != "pay" { + logx.Infof("云印签回调,非付款通知,订单类型: %s, 订单号: %s", orderType, sourceOrderCode) + w.WriteHeader(http.StatusOK) + w.Write([]byte("success")) + return nil + } + + // 如果没有 sourceOrderCode,无法关联订单,直接返回成功 + if sourceOrderCode == "" { + logx.Errorf("云印签回调,缺少 sourceOrderCode 参数") + w.WriteHeader(http.StatusOK) + w.Write([]byte("success")) + return nil + } + + // 根据 sourceOrderCode 查找订单 + order, findOrderErr := l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, sourceOrderCode) + if findOrderErr != nil { + logx.Errorf("云印签回调,查找订单失败,订单号: %s, 错误: %v", sourceOrderCode, findOrderErr) + w.WriteHeader(http.StatusOK) + w.Write([]byte("success")) + return nil + } + + // 幂等性检查:如果订单已经不是 pending 状态,说明已经处理过,直接返回成功 + if order.Status != "pending" { + logx.Infof("云印签回调,订单已处理,订单号: %s, 当前状态: %s", sourceOrderCode, order.Status) + w.WriteHeader(http.StatusOK) + w.Write([]byte("success")) + return nil + } + + // 验证支付状态:tradeState == "00000" 表示支付成功 + if tradeState != "00000" { + logx.Infof("云印签回调,订单未支付成功,订单号: %s, 支付状态: %s", sourceOrderCode, tradeState) + w.WriteHeader(http.StatusOK) + w.Write([]byte("success")) + return nil + } + + // 验证金额 + payAmountFloat, parseAmountErr := strconv.ParseFloat(payAmount, 64) + if parseAmountErr != nil { + logx.Errorf("云印签回调,金额解析失败,订单号: %s, 金额: %s, 错误: %v", sourceOrderCode, payAmount, parseAmountErr) + w.WriteHeader(http.StatusOK) + w.Write([]byte("success")) + return nil + } + + // 金额比较(允许小数点误差) + amountDiff := payAmountFloat - order.Amount + if amountDiff < 0 { + amountDiff = -amountDiff + } + if amountDiff > 0.01 { // 允许1分钱的误差 + logx.Errorf("云印签回调,金额不一致,订单号: %s, 订单金额: %.2f, 回调金额: %.2f", sourceOrderCode, order.Amount, payAmountFloat) + w.WriteHeader(http.StatusOK) + w.Write([]byte("success")) + return nil + } + + // 解析支付成功时间 + var payTime time.Time + if paySuccessTime != "" { + // 尝试多种时间格式 + timeFormats := []string{ + "2006-01-02 15:04:05", + "2006-01-02T15:04:05", + "2006-01-02 15:04:05.000", + "2006-01-02T15:04:05.000Z", + } + parsed := false + for _, format := range timeFormats { + if t, err := time.Parse(format, paySuccessTime); err == nil { + payTime = t + parsed = true + break + } + } + if !parsed { + // 如果解析失败,使用当前时间 + payTime = time.Now() + logx.Infof("云印签回调,支付时间解析失败,使用当前时间,订单号: %s, 时间字符串: %s", sourceOrderCode, paySuccessTime) + } + } else { + payTime = time.Now() + } + + // 更新订单状态 + order.Status = "paid" + order.PayTime = lzUtils.TimeToNullTime(payTime) + if channelOrderNo != "" { + order.PlatformOrderId = lzUtils.StringToNullString(channelOrderNo) + } + + if updateErr := l.svcCtx.OrderModel.UpdateWithVersion(l.ctx, nil, order); updateErr != nil { + logx.Errorf("云印签回调,更新订单状态失败,订单号: %s, 错误: %v", sourceOrderCode, updateErr) + w.WriteHeader(http.StatusOK) + w.Write([]byte("success")) + return nil + } + + // 更新云印签订单表的支付状态 + yunyinOrder, findYunyinErr := l.svcCtx.YunyinSignPayOrderModel.FindOneByOrderId(l.ctx, order.Id) + if findYunyinErr == nil && yunyinOrder != nil { + // 更新支付状态为已支付(1) + if yunyinOrder.PayStatus != 1 { + yunyinOrder.PayStatus = 1 + if updateYunyinErr := l.svcCtx.YunyinSignPayOrderModel.Update(l.ctx, yunyinOrder); updateYunyinErr != nil { + logx.Errorf("云印签回调,更新云印签订单支付状态失败,订单ID: %s, 错误: %v", order.Id, updateYunyinErr) + } else { + logx.Infof("云印签回调,成功更新云印签订单支付状态,订单ID: %s", order.Id) + } + } + } else { + logx.Infof("云印签回调,未找到对应的云印签订单记录,订单ID: %s, 流程ID: %s", order.Id, flowId) + } + + // 发送异步任务处理后续流程 + if asyncErr := l.svcCtx.AsynqService.SendQueryTask(order.Id); asyncErr != nil { + logx.Errorf("云印签回调,异步任务调度失败,订单ID: %s, 错误: %v", order.Id, asyncErr) + // 不返回错误,因为订单已经更新成功 + } + + logx.Infof("云印签回调处理成功,订单号: %s, 支付单号: %s, 渠道单号: %s", sourceOrderCode, payOrderNo, channelOrderNo) + + // 返回 success,云印签要求返回纯字符串 success + w.WriteHeader(http.StatusOK) + w.Write([]byte("success")) + return nil +} diff --git a/app/main/api/internal/logic/pay/yunyinsignpayrefundlogic.go b/app/main/api/internal/logic/pay/yunyinsignpayrefundlogic.go new file mode 100644 index 0000000..5fae342 --- /dev/null +++ b/app/main/api/internal/logic/pay/yunyinsignpayrefundlogic.go @@ -0,0 +1,153 @@ +package pay + +import ( + "context" + "database/sql" + "fmt" + "jnc-server/app/main/api/internal/svc" + "jnc-server/app/main/api/internal/types" + "jnc-server/app/main/model" + "jnc-server/common/xerr" + "jnc-server/pkg/lzkit/lzUtils" + "time" + + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/zeromicro/go-zero/core/logx" + "github.com/zeromicro/go-zero/core/stores/sqlx" +) + +type YunYinSignPayRefundLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewYunYinSignPayRefundLogic(ctx context.Context, svcCtx *svc.ServiceContext) *YunYinSignPayRefundLogic { + return &YunYinSignPayRefundLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// YunYinSignPayRefund 云印签退款 +func (l *YunYinSignPayRefundLogic) YunYinSignPayRefund(req *types.YunYinSignPayRefundReq) (resp *types.YunYinSignPayRefundResp, err error) { + // 检查云印签支付服务是否启用 + if l.svcCtx.YunYinSignPayService == nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "云印签支付服务未启用") + } + + // 验证参数:order_no 和 participate_id 至少提供一个 + if req.OrderNo == "" && req.ParticipateId == 0 { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "订单号和参与方ID至少需要提供一个") + } + + // 查找订单(如果提供了订单号) + var order *model.Order + if req.OrderNo != "" { + order, err = l.svcCtx.OrderModel.FindOneByOrderNo(l.ctx, req.OrderNo) + if err != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找订单失败,订单号: %s, 错误: %v", req.OrderNo, err) + } + + // 验证订单支付平台 + if order.PaymentPlatform != "yunyinSignPay" && order.PaymentPlatform != "yunyinSignPay_wechat" && order.PaymentPlatform != "yunyinSignPay_alipay" { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "订单不是云印签支付订单,订单号: %s, 支付平台: %s", req.OrderNo, order.PaymentPlatform) + } + + // 验证订单状态 + if order.Status != "paid" { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "订单状态不允许退款,订单号: %s, 状态: %s", req.OrderNo, order.Status) + } + + // 验证退款金额 + if req.RefundAmount > order.Amount { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.REUQEST_PARAM_ERROR), "退款金额不能超过订单金额,订单号: %s, 订单金额: %.2f, 退款金额: %.2f", req.OrderNo, order.Amount, req.RefundAmount) + } + } + + // 调用云印签退款接口 + // RefundPayeeBill 方法会自动处理:如果只提供了 sourceOrderCode,会查询收款单获取 participateId + refundErr := l.svcCtx.YunYinSignPayService.RefundPayeeBill( + l.ctx, + req.OrderNo, + req.ParticipateId, + req.RefundAmount, + req.RefundReason, + ) + if refundErr != nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "云印签退款失败: %v", refundErr) + } + + // 如果提供了订单号,更新订单状态和创建退款记录 + if req.OrderNo != "" && order != nil { + // 检查是否全额退款 + isFullRefund := req.RefundAmount >= order.Amount-0.01 // 允许1分钱的误差 + + // 创建退款记录并更新订单状态 + err = l.createRefundRecordAndUpdateOrder(order, req, isFullRefund) + if err != nil { + logx.Errorf("创建退款记录或更新订单状态失败: %v", err) + // 不返回错误,因为退款已经成功 + } + + // 更新云印签订单表的支付状态 + yunyinOrder, findYunyinErr := l.svcCtx.YunyinSignPayOrderModel.FindOneByOrderId(l.ctx, order.Id) + if findYunyinErr == nil && yunyinOrder != nil { + if isFullRefund { + // 全额退款,更新为已退款状态(2) + yunyinOrder.PayStatus = 2 + } + // 部分退款保持已支付状态(1),但可以通过其他字段记录退款金额 + if updateYunyinErr := l.svcCtx.YunyinSignPayOrderModel.Update(l.ctx, yunyinOrder); updateYunyinErr != nil { + logx.Errorf("更新云印签订单状态失败: %v", updateYunyinErr) + } + } + } + + logx.Infof("云印签退款成功,订单号: %s, 参与方ID: %d, 退款金额: %.2f", req.OrderNo, req.ParticipateId, req.RefundAmount) + + return &types.YunYinSignPayRefundResp{ + Success: true, + Message: "退款申请已提交", + }, nil +} + +// createRefundRecordAndUpdateOrder 创建退款记录并更新订单状态 +func (l *YunYinSignPayRefundLogic) createRefundRecordAndUpdateOrder(order *model.Order, req *types.YunYinSignPayRefundReq, isFullRefund bool) error { + return l.svcCtx.OrderModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { + // 生成退款单号 + refundNo := fmt.Sprintf("refund-%s", order.OrderNo) + + // 创建退款记录 + refund := &model.OrderRefund{ + Id: uuid.NewString(), + RefundNo: refundNo, + PlatformRefundId: sql.NullString{String: "", Valid: false}, // 云印签退款没有平台退款单号 + OrderId: order.Id, + UserId: order.UserId, + ProductId: order.ProductId, + RefundAmount: req.RefundAmount, + RefundReason: lzUtils.StringToNullString(req.RefundReason), + Status: "success", // 云印签退款是异步的,这里标记为成功 + RefundTime: sql.NullTime{Time: time.Now(), Valid: true}, + } + + if _, err := l.svcCtx.OrderRefundModel.Insert(ctx, session, refund); err != nil { + return fmt.Errorf("创建退款记录失败: %v", err) + } + + // 更新订单状态 + if isFullRefund { + order.Status = "refunded" + } + // 部分退款保持 paid 状态 + + if _, err := l.svcCtx.OrderModel.Update(ctx, session, order); err != nil { + return fmt.Errorf("更新订单状态失败: %v", err) + } + + return nil + }) +} diff --git a/app/main/api/internal/service/yunyinSignPayService.go b/app/main/api/internal/service/yunyinSignPayService.go index ba30b50..4821082 100644 --- a/app/main/api/internal/service/yunyinSignPayService.go +++ b/app/main/api/internal/service/yunyinSignPayService.go @@ -683,3 +683,172 @@ func (y *YunYinSignPayService) QueryPayeeBill(ctx context.Context, sourceOrderCo 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 +} diff --git a/app/main/api/internal/types/pay.go b/app/main/api/internal/types/pay.go index 5458ac4..4b2704a 100644 --- a/app/main/api/internal/types/pay.go +++ b/app/main/api/internal/types/pay.go @@ -26,3 +26,15 @@ type PaymentResp struct { PrepayId string `json:"prepay_id"` OrderNo string `json:"order_no"` } + +type YunYinSignPayRefundReq struct { + OrderNo string `json:"order_no"` // 订单号(sourceOrderCode),与 participate_id 二选一 + ParticipateId int64 `json:"participate_id,omitempty"` // 参与方ID,与 order_no 二选一 + RefundAmount float64 `json:"refund_amount" validate:"required,gt=0"` // 退款金额,必须大于0 + RefundReason string `json:"refund_reason,omitempty"` // 退款原因 +} + +type YunYinSignPayRefundResp struct { + Success bool `json:"success"` + Message string `json:"message"` +}