diff --git a/constants/order.go b/constants/order.go index 79fa6497..a22fa9f3 100644 --- a/constants/order.go +++ b/constants/order.go @@ -126,7 +126,8 @@ type Day int64 // 订单取消时间 const ( - CANCLE_ORDER_EXPIRE Day = 48 * 3600 + CANCLE_ORDER_EXPIRE Day = 48 * 3600 + CANCLE_ORDER_EXPIRE_DAY Day = 2 // 2天 ) // 订单时间配置 diff --git a/model/gmodel/fs_cart_logic.go b/model/gmodel/fs_cart_logic.go index b18f7672..d86b18be 100755 --- a/model/gmodel/fs_cart_logic.go +++ b/model/gmodel/fs_cart_logic.go @@ -92,5 +92,5 @@ func (c *FsCartModel) DeleteCartsByIds(ctx context.Context, ids []int64) ( err e if len(ids) == 0 { return } - return c.db.WithContext(ctx).Model(&FsCart{}).Where("`id` in (?)", ids).Delete(&FsCart{}).Error + return c.db.Table(c.name).WithContext(ctx).Model(&FsCart{}).Where("`id` in (?)", ids).Update("status", 0).Error } diff --git a/model/gmodel/fs_pay_event_gen.go b/model/gmodel/fs_pay_event_gen.go new file mode 100644 index 00000000..38d6c56e --- /dev/null +++ b/model/gmodel/fs_pay_event_gen.go @@ -0,0 +1,25 @@ +package gmodel + +import ( + "gorm.io/gorm" +) + +// fs_pay_event 支付回调事件日志 +type FsPayEvent struct { + Id int64 `gorm:"primary_key;default:0;auto_increment;" json:"id"` // + PayMethod *int64 `gorm:"default:0;" json:"pay_method"` // 支付方式 1 stripe 2 paypal + EventId *string `gorm:"default:'';" json:"event_id"` // 事件ID + EventType *string `gorm:"default:'';" json:"event_type"` // 事件类型 + EventData *string `gorm:"default:'';" json:"event_data"` // 事件数据 + EventCreated *int64 `gorm:"default:0;" json:"event_created"` // 事件时间 + Ip *string `gorm:"default:'';" json:"ip"` // 请求IP + CreatedAt *int64 `gorm:"default:0;" json:"created_at"` // 创建时间 +} +type FsPayEventModel struct { + db *gorm.DB + name string +} + +func NewFsPayEventModel(db *gorm.DB) *FsPayEventModel { + return &FsPayEventModel{db: db, name: "fs_pay_event"} +} diff --git a/model/gmodel/fs_pay_event_logic.go b/model/gmodel/fs_pay_event_logic.go new file mode 100644 index 00000000..8af36582 --- /dev/null +++ b/model/gmodel/fs_pay_event_logic.go @@ -0,0 +1,15 @@ +package gmodel + +import "context" + +// TODO: 使用model的属性做你想做的 + +func (p *FsPayEventModel) CreateOrUpdate(ctx context.Context, req *FsPayEvent) (resp *FsPayEvent, err error) { + rowBuilder := p.db.Table(p.name).WithContext(ctx) + if req.Id > 0 { + err = rowBuilder.Save(req).Error + } else { + err = rowBuilder.Create(req).Error + } + return req, err +} diff --git a/model/gmodel/fs_pay_gen.go b/model/gmodel/fs_pay_gen.go index e30685b6..0ee65802 100644 --- a/model/gmodel/fs_pay_gen.go +++ b/model/gmodel/fs_pay_gen.go @@ -7,7 +7,6 @@ import ( // fs_pay 支付记录 type FsPay struct { Id int64 `gorm:"primary_key;default:0;auto_increment;" json:"id"` // - PayNo *string `gorm:"default:'';" json:"pay_no"` // 支付编号 UserId *int64 `gorm:"index;default:0;" json:"user_id"` // 用户id OrderNumber *string `gorm:"default:'';" json:"order_number"` // 订单编号 TradeNo *string `gorm:"index;default:'';" json:"trade_no"` // 第三方支付编号 diff --git a/model/gmodel/fs_pay_logic.go b/model/gmodel/fs_pay_logic.go index d470f2e9..fa4a6093 100644 --- a/model/gmodel/fs_pay_logic.go +++ b/model/gmodel/fs_pay_logic.go @@ -3,6 +3,7 @@ package gmodel import ( "context" "fusenapi/utils/handler" + "reflect" "gorm.io/gorm" ) @@ -80,6 +81,31 @@ func (m *FsPayModel) FindOneByQuery(ctx context.Context, rowBuilder *gorm.DB, fi } } +func (m *FsPayModel) FindAll(ctx context.Context, rowBuilder *gorm.DB, filterMap map[string]string, orderBy string) ([]*FsPay, error) { + var resp []*FsPay + // 过滤 + if filterMap != nil { + rowBuilder = rowBuilder.Scopes(handler.FilterData(filterMap)) + } + + // 排序 + if orderBy != "" { + var fieldsMap = make(map[string]struct{}) + s := reflect.TypeOf(&FsOrder{}).Elem() //通过反射获取type定义 + for i := 0; i < s.NumField(); i++ { + fieldsMap[s.Field(i).Tag.Get("json")] = struct{}{} + } + rowBuilder = rowBuilder.Scopes(handler.OrderCheck(orderBy, fieldsMap)) + } + + result := rowBuilder.WithContext(ctx).Find(&resp) + if result.Error != nil { + return nil, result.Error + } else { + return resp, nil + } +} + // 事务 func (m *FsPayModel) Trans(ctx context.Context, fn func(ctx context.Context, connGorm *gorm.DB) error) error { tx := m.db.Table(m.name).WithContext(ctx).Begin() diff --git a/model/gmodel/fs_refund_reason_logic.go b/model/gmodel/fs_refund_reason_logic.go index 88644fc4..48e67d51 100644 --- a/model/gmodel/fs_refund_reason_logic.go +++ b/model/gmodel/fs_refund_reason_logic.go @@ -2,6 +2,7 @@ package gmodel import ( "context" + "fusenapi/utils/handler" "gorm.io/gorm" ) @@ -24,3 +25,43 @@ func (m *FsRefundReasonModel) Update(ctx context.Context, obj *FsRefundReason) e func (m *FsRefundReasonModel) UpdateByRefundReasonId(ctx context.Context, obj *FsRefundReason) error { return m.db.WithContext(ctx).Model(obj).Where("`refund_reason_id` = ?", obj.RefundReasonId).Updates(obj).Error } + +func (m *FsRefundReasonModel) CreateOrUpdate(ctx context.Context, req *FsRefundReason) (resp *FsRefundReason, err error) { + rowBuilder := m.db.Table(m.name).WithContext(ctx) + if req.Id > 0 { + err = rowBuilder.Save(req).Error + } else { + err = rowBuilder.Create(req).Error + } + return req, err +} + +func (m *FsRefundReasonModel) FindOneByQuery(ctx context.Context, rowBuilder *gorm.DB, filterMap map[string]string) (*FsRefundReason, error) { + var resp FsRefundReason + + if filterMap != nil { + rowBuilder = rowBuilder.Scopes(handler.FilterData(filterMap)) + } + + result := rowBuilder.WithContext(ctx).Limit(1).Find(&resp) + if result.Error != nil { + return nil, result.Error + } else { + return &resp, nil + } +} + +func (m *FsRefundReasonModel) RowSelectBuilder(selectData []string) *gorm.DB { + var rowBuilder = m.db.Table(m.name) + + if selectData != nil { + rowBuilder = rowBuilder.Select(selectData) + } else { + rowBuilder = rowBuilder.Select("*") + } + return rowBuilder +} + +func (m *FsRefundReasonModel) TableName() string { + return m.name +} diff --git a/model/gmodel/var_gen.go b/model/gmodel/var_gen.go index 191a9898..a90a6f7c 100644 --- a/model/gmodel/var_gen.go +++ b/model/gmodel/var_gen.go @@ -52,6 +52,7 @@ type AllModelsGen struct { FsOrderDetailTemplate *FsOrderDetailTemplateModel // fs_order_detail_template 订单模板详细表 FsOrderRemark *FsOrderRemarkModel // fs_order_remark 订单备注表 FsPay *FsPayModel // fs_pay 支付记录 + FsPayEvent *FsPayEventModel // fs_pay_event 支付回调事件日志 FsProduct *FsProductModel // fs_product 产品表 FsProductCopy1 *FsProductCopy1Model // fs_product_copy1 产品表 FsProductDesign *FsProductDesignModel // fs_product_design 产品设计表 @@ -144,6 +145,7 @@ func NewAllModels(gdb *gorm.DB) *AllModelsGen { FsOrderDetailTemplate: NewFsOrderDetailTemplateModel(gdb), FsOrderRemark: NewFsOrderRemarkModel(gdb), FsPay: NewFsPayModel(gdb), + FsPayEvent: NewFsPayEventModel(gdb), FsProduct: NewFsProductModel(gdb), FsProductCopy1: NewFsProductCopy1Model(gdb), FsProductDesign: NewFsProductDesignModel(gdb), diff --git a/server/home-user-auth/etc/home-user-auth.yaml b/server/home-user-auth/etc/home-user-auth.yaml index 7ef5a2c9..31722119 100644 --- a/server/home-user-auth/etc/home-user-auth.yaml +++ b/server/home-user-auth/etc/home-user-auth.yaml @@ -20,3 +20,10 @@ OAuth: Stripe: SK: "sk_test_51IisojHygnIJZeghPVSBhkwySfcyDV4SoAduIxu3J7bvSJ9cZMD96LY1LO6SpdbYquLJX5oKvgEBB67KT9pecfCy00iEC4pp9y" + +PayConfig: + Stripe: + Key: "sk_test_51IisojHygnIJZeghPVSBhkwySfcyDV4SoAduIxu3J7bvSJ9cZMD96LY1LO6SpdbYquLJX5oKvgEBB67KT9pecfCy00iEC4pp9y" + EndpointSecret: "whsec_f5f9a121d43af3789db7459352f08cf523eb9e0fbf3381f91ba6c97c324c174d" + SuccessURL: "http://www.baidu.com" + CancelURL: "http://www.baidu.com" diff --git a/server/home-user-auth/internal/config/config.go b/server/home-user-auth/internal/config/config.go index 19c515b1..fa7c3bf3 100644 --- a/server/home-user-auth/internal/config/config.go +++ b/server/home-user-auth/internal/config/config.go @@ -28,4 +28,13 @@ type Config struct { Stripe struct { SK string } + + PayConfig struct { + Stripe struct { + EndpointSecret string + Key string + CancelURL string + SuccessURL string + } + } } diff --git a/server/home-user-auth/internal/logic/userordercancellogic.go b/server/home-user-auth/internal/logic/userordercancellogic.go index 56a9ab4a..ce64a5b2 100644 --- a/server/home-user-auth/internal/logic/userordercancellogic.go +++ b/server/home-user-auth/internal/logic/userordercancellogic.go @@ -6,11 +6,13 @@ import ( "fusenapi/model/gmodel" "fusenapi/utils/auth" "fusenapi/utils/basic" + "time" "context" "fusenapi/server/home-user-auth/internal/svc" "fusenapi/server/home-user-auth/internal/types" + handlerUtils "fusenapi/utils/handler" "github.com/zeromicro/go-zero/core/logx" "gorm.io/gorm" @@ -50,11 +52,76 @@ func (l *UserOrderCancelLogic) UserOrderCancel(req *types.UserOrderCancelReq, us } // 判断订单状态 - if *orderInfo.Status == int64(constants.STATUS_NEW_NOT_PAY) { - - } else { + var notCancelStatusMap = make(map[int64]struct{}, 3) + notCancelStatusMap[int64(constants.STATUS_NEW_NOT_PAY)] = struct{}{} + notCancelStatusMap[int64(constants.STATUS_NEW_PART_PAY)] = struct{}{} + notCancelStatusMap[int64(constants.STATUS_NEW_PAY_COMPLETED)] = struct{}{} + _, ok := notCancelStatusMap[int64(*orderInfo.Status)] + if !ok { return resp.SetStatusWithMessage(basic.CodeOrderNotCancelledErr, "the order status not cancle") } + var cancelTime int64 = time.Now().Unix() - (*orderInfo.Ctime + int64(constants.CANCLE_ORDER_EXPIRE)) + // 第一次支付成功后48小时后不能进行取消操作 + if *orderInfo.IsPayCompleted == 1 && cancelTime > 0 { + return resp.SetStatusWithMessage(basic.CodeOrderNotCancelledErr, "The current order cannot be cancelled") + } + + // 修改订单--取消状态和取消原因 + *orderInfo.Status = int64(constants.STATUS_NEW_CANCEL) + *orderInfo.IsCancel = 1 + orderInfo.RefundReasonId = &req.RefundReasonId + orderInfo.RefundReason = &req.RefundReason + + var nowTime = time.Now().Unix() + var payList []handlerUtils.PayInfo + // 事务处理 + err = orderModel.Trans(l.ctx, func(ctx context.Context, connGorm *gorm.DB) (err error) { + // 修改订单信息 + orderModelTS := gmodel.NewFsOrderModel(connGorm) + err = orderModelTS.Update(ctx, orderInfo) + if err != nil { + return err + } + // 新增退款记录 + var isRefund int64 = 0 + refundReasonModelTS := gmodel.NewFsRefundReasonModel(connGorm) + refundReasonModelTS.CreateOrUpdate(ctx, &gmodel.FsRefundReason{ + IsRefund: &isRefund, + RefundReasonId: &req.RefundReasonId, + RefundReason: &req.RefundReason, + OrderId: &orderInfo.Id, + CreatedAt: &nowTime, + }) + // 退款申请 + // 退款申请--查询支付信息 + fsPayModelTS := gmodel.NewFsPayModel(connGorm) + rbFsPay := fsPayModelTS.RowSelectBuilder(nil).Where("order_number = ?", orderInfo.Sn).Where("pay_status =?", constants.PAYSTATUS_SUCCESS).Where("is_refund =?", 0) + payInfoList, err := fsPayModelTS.FindAll(ctx, rbFsPay, nil, "") + if err != nil { + return err + } + for _, payInfo := range payInfoList { + var key string + if *payInfo.PaymentMethod == int64(constants.PAYMETHOD_STRIPE) { + key = l.svcCtx.Config.PayConfig.Stripe.Key + } + payList = append(payList, handlerUtils.PayInfo{ + TradeNo: *payInfo.TradeNo, + PaymentMethod: *payInfo.PaymentMethod, + Key: key, + }) + } + return nil + }) + // 退款申请--调取第三方接口发起退款 + handlerUtils.PayRefundHandler(&handlerUtils.PayRefundHandlerReq{ + PayInfoList: payList, + }) + if err != nil { + logx.Error(err) + return resp.SetStatusWithMessage(basic.CodeOrderCancelledNotOk, "the order cancle failed") + } + return resp.SetStatus(basic.CodeOK) } diff --git a/server/pay/etc/pay.yaml b/server/pay/etc/pay.yaml index 841753e3..831292a9 100644 --- a/server/pay/etc/pay.yaml +++ b/server/pay/etc/pay.yaml @@ -1,7 +1,6 @@ Name: pay Host: 0.0.0.0 Port: 9915 -Timeout: 15000 SourceMysql: fusentest:XErSYmLELKMnf3Dh@tcp(110.41.19.98:3306)/fusentest Auth: AccessSecret: fusen2023 diff --git a/server/pay/internal/handler/orderrefundhandler.go b/server/pay/internal/handler/orderrefundhandler.go new file mode 100644 index 00000000..3d7998ff --- /dev/null +++ b/server/pay/internal/handler/orderrefundhandler.go @@ -0,0 +1,35 @@ +package handler + +import ( + "net/http" + "reflect" + + "fusenapi/utils/basic" + + "fusenapi/server/pay/internal/logic" + "fusenapi/server/pay/internal/svc" + "fusenapi/server/pay/internal/types" +) + +func OrderRefundHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + var req types.OrderRefundReq + userinfo, err := basic.RequestParse(w, r, svcCtx, &req) + if err != nil { + return + } + + // 创建一个业务逻辑层实例 + l := logic.NewOrderRefundLogic(r.Context(), svcCtx) + + rl := reflect.ValueOf(l) + basic.BeforeLogic(w, r, rl) + + resp := l.OrderRefund(&req, userinfo) + + if !basic.AfterLogic(w, r, rl, resp) { + basic.NormalAfterLogic(w, r, resp) + } + } +} diff --git a/server/pay/internal/handler/routes.go b/server/pay/internal/handler/routes.go index 28292f5f..f1295e35 100644 --- a/server/pay/internal/handler/routes.go +++ b/server/pay/internal/handler/routes.go @@ -17,6 +17,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { Path: "/api/pay/payment-intent", Handler: OrderPaymentIntentHandler(serverCtx), }, + { + Method: http.MethodPost, + Path: "/api/pay/refund", + Handler: OrderRefundHandler(serverCtx), + }, { Method: http.MethodPost, Path: "/api/pay/stripe-webhook", diff --git a/server/pay/internal/handler/stripewebhookhandler.go b/server/pay/internal/handler/stripewebhookhandler.go index b4447ba7..ca7689d0 100644 --- a/server/pay/internal/handler/stripewebhookhandler.go +++ b/server/pay/internal/handler/stripewebhookhandler.go @@ -30,6 +30,15 @@ func StripeWebhookHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { // return // } + IPAddress := r.Header.Get("X-Real-Ip") + if IPAddress == "" { + IPAddress = r.Header.Get("X-Forwarded-For") + } + if IPAddress == "" { + IPAddress = r.RemoteAddr + } + + req.RemoteAddr = IPAddress req.Payload = payload req.StripeSignature = r.Header.Get("Stripe-Signature") diff --git a/server/pay/internal/logic/orderrefundlogic.go b/server/pay/internal/logic/orderrefundlogic.go new file mode 100644 index 00000000..5f9e11db --- /dev/null +++ b/server/pay/internal/logic/orderrefundlogic.go @@ -0,0 +1,43 @@ +package logic + +import ( + "fusenapi/utils/auth" + "fusenapi/utils/basic" + + "context" + + "fusenapi/server/pay/internal/svc" + "fusenapi/server/pay/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type OrderRefundLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewOrderRefundLogic(ctx context.Context, svcCtx *svc.ServiceContext) *OrderRefundLogic { + return &OrderRefundLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +// 处理进入前逻辑w,r +// func (l *OrderRefundLogic) BeforeLogic(w http.ResponseWriter, r *http.Request) { +// } + +// 处理逻辑后 w,r 如:重定向, resp 必须重新处理 +// func (l *OrderRefundLogic) AfterLogic(w http.ResponseWriter, r *http.Request, resp *basic.Response) { +// // httpx.OkJsonCtx(r.Context(), w, resp) +// } + +func (l *OrderRefundLogic) OrderRefund(req *types.OrderRefundReq, userinfo *auth.UserInfo) (resp *basic.Response) { + // 返回值必须调用Set重新返回, resp可以空指针调用 resp.SetStatus(basic.CodeOK, data) + // userinfo 传入值时, 一定不为null + + return resp.SetStatus(basic.CodeOK) +} diff --git a/server/pay/internal/logic/stripewebhooklogic.go b/server/pay/internal/logic/stripewebhooklogic.go index 048b78e7..038c2bce 100644 --- a/server/pay/internal/logic/stripewebhooklogic.go +++ b/server/pay/internal/logic/stripewebhooklogic.go @@ -64,6 +64,21 @@ func (l *StripeWebhookLogic) StripeWebhook(req *types.StripeWebhookReq, userinfo return resp.SetStatusWithMessage(basic.CodeAesCbcDecryptionErr, "Webhook signature verification failed") } + // 新增支付回调事件日志 + var payMethod = int64(constants.PAYMETHOD_STRIPE) + var nowTime = time.Now().Unix() + var eventData = string(event.Data.Raw) + var fsPayEvent = &gmodel.FsPayEvent{ + PayMethod: &payMethod, + EventId: &event.ID, + EventType: &event.Type, + EventData: &eventData, + EventCreated: &event.Created, + Ip: &req.RemoteAddr, + CreatedAt: &nowTime, + } + l.HandlePayEventCreate(fsPayEvent) + // Unmarshal the event data into an appropriate struct depending on its Type switch event.Type { case "charge.succeeded": @@ -91,12 +106,13 @@ func (l *StripeWebhookLogic) StripeWebhook(req *types.StripeWebhookReq, userinfo var paymentIntent stripe.PaymentIntent err := json.Unmarshal(event.Data.Raw, &paymentIntent) if err != nil { - logx.Error(err) + logx.Errorf("err:%+v,desc:%s", err, "pay notify Unmarshal fail event.Type payment_intent.succeeded") return resp.SetStatusWithMessage(basic.CodeAesCbcDecryptionErr, "pay notify Unmarshal fail event.Type payment_intent.succeeded") } - err = l.handlePaymentIntentSucceeded(&paymentIntent) + err = l.HandlePaymentIntentSucceeded(&paymentIntent) if err != nil { - return resp.SetStatusWithMessage(basic.CodeAesCbcDecryptionErr, "pay notify Unmarshal fail event.Type Unhandled") + logx.Errorf("err:%+v,desc:%s", err, "pay notify handle payment_intent.succeeded") + return resp.SetStatusWithMessage(basic.CodePaybackNotOk, "pay notify handle payment_intent.succeeded") } case "payment_method.attached": var paymentMethod stripe.PaymentMethod @@ -105,6 +121,19 @@ func (l *StripeWebhookLogic) StripeWebhook(req *types.StripeWebhookReq, userinfo logx.Error(err) return resp.SetStatusWithMessage(basic.CodeAesCbcDecryptionErr, "pay notify Unmarshal fail event.Type payment_method.attached") } + case "charge.refunded": + var chargeRefunded stripe.Charge + err := json.Unmarshal(event.Data.Raw, &chargeRefunded) + if err != nil { + logx.Errorf("err:%+v,desc:%s", err, "pay notify Unmarshal fail event.Type charge.refunded") + return resp.SetStatusWithMessage(basic.CodeAesCbcDecryptionErr, "pay notify Unmarshal fail event.Type charge.refunded") + } + err = l.HandleChargeRefunded(&chargeRefunded) + if err != nil { + logx.Errorf("err:%+v,desc:%s", err, "pay notify handle charge.refunded") + return resp.SetStatusWithMessage(basic.CodeAesCbcDecryptionErr, "pay notify handle charge.refunded") + } + // ... handle other event types default: logx.Error("Unhandled event") @@ -114,6 +143,72 @@ func (l *StripeWebhookLogic) StripeWebhook(req *types.StripeWebhookReq, userinfo return resp.SetStatus(basic.CodeOK) } +// 回调事件日志 +func (l *StripeWebhookLogic) HandlePayEventCreate(fsPayEvent *gmodel.FsPayEvent) error { + _, err := gmodel.NewFsPayEventModel(l.svcCtx.MysqlConn).CreateOrUpdate(l.ctx, fsPayEvent) + return err +} + +// 退款成功 +func (l *StripeWebhookLogic) HandleChargeRefunded(chargeRefunded *stripe.Charge) (err error) { + // 退款成功 + if chargeRefunded.Status == "succeeded" { + orderModel := gmodel.NewFsOrderModel(l.svcCtx.MysqlConn) + err = orderModel.Trans(l.ctx, func(ctx context.Context, connGorm *gorm.DB) (err error) { + // 查询支付记录 + payModelT := gmodel.NewFsPayModel(connGorm) + payModelTRSB := payModelT.RowSelectBuilder(nil) + payModelTRSB1 := payModelTRSB.Where("trade_no = ?", chargeRefunded.PaymentIntent.ID).Where("pay_status = ?", constants.PAYSTATUS_SUCCESS).Where("is_refund = ?", 0) + payInfo, err := payModelT.FindOneByQuery(l.ctx, payModelTRSB1, nil) + if err != nil { + return err + } + // 更新支付记录 + *payInfo.IsRefund = 1 + _, err = payModelT.CreateOrUpdate(ctx, payInfo) + if err != nil { + return err + } + // 获取是否还有未退款的数据 + payModelTRSB2 := payModelTRSB.Where("order_number = ?", payInfo.OrderNumber).Where("pay_status = ?", constants.PAYSTATUS_SUCCESS).Where("is_refund = ?", 0) + count, err := payModelT.FindCount(l.ctx, payModelTRSB2, nil) + if count == 0 { + // 退款完成更新订单状态 + orderModelT := gmodel.NewFsOrderModel(connGorm) + orderModelTRSB := orderModelT.RowSelectBuilder(nil).Where("sn =?", payInfo.OrderNumber) + orderInfoRel, err := orderModelT.FindOneByQuery(ctx, orderModelTRSB, nil) + if err != nil { + return err + } + var isRefunded int64 = 1 + var isRefunding int64 = 1 + var orderStatus int64 = int64(constants.STATUS_NEW_REFUNDED) + var orderInfo = &gmodel.FsOrder{} + orderInfo.Id = orderInfoRel.Id + orderInfo.IsRefunded = &isRefunded + orderInfo.IsRefunding = &isRefunding + orderInfo.Status = &orderStatus + orderModelT.Update(ctx, orderInfo) + + // 记录退款原因 + refundReasonModelT := gmodel.NewFsRefundReasonModel(connGorm) + refundReasonModelTRSB := refundReasonModelT.RowSelectBuilder(nil).Where("order_id =?", orderInfoRel.Id) + refundReasonInfo, err := refundReasonModelT.FindOneByQuery(ctx, refundReasonModelTRSB, nil) + if err != nil { + return err + } + *refundReasonInfo.IsRefund = 1 + _, err = refundReasonModelT.CreateOrUpdate(ctx, refundReasonInfo) + if err != nil { + return err + } + } + return err + }) + } + return err +} + // session完成 // func (l *StripeWebhookLogic) handlePaymentSessionCompleted(sessionId string, tradeNo string) (err error) { // // 查询支付记录 @@ -136,8 +231,8 @@ func (l *StripeWebhookLogic) StripeWebhook(req *types.StripeWebhookReq, userinfo // return err // } -// 成功的付款 -func (l *StripeWebhookLogic) handlePaymentIntentSucceeded(paymentIntent *stripe.PaymentIntent) error { +// 付款成功 +func (l *StripeWebhookLogic) HandlePaymentIntentSucceeded(paymentIntent *stripe.PaymentIntent) error { orderSn, ok := paymentIntent.Metadata["order_sn"] if !ok || orderSn == "" { return errors.New("order_sn not found") @@ -146,14 +241,12 @@ func (l *StripeWebhookLogic) handlePaymentIntentSucceeded(paymentIntent *stripe. // 查询支付记录 payModel := gmodel.NewFsPayModel(l.svcCtx.MysqlConn) rsbPay := payModel.RowSelectBuilder(nil) - rsbPay = rsbPay.Where("order_number = ?", orderSn) + rsbPay = rsbPay.Where("order_number = ?", orderSn).Where("pay_status = ?", constants.PAYSTATUS_UNSUCCESS) payInfo, err := payModel.FindOneByQuery(l.ctx, rsbPay, nil) if err != nil { return err } - if *payInfo.PayStatus == 1 { - return errors.New("pay status 1") - } + //订单信息 orderDetailTemplateModel := gmodel.NewFsOrderDetailTemplateModel(l.svcCtx.MysqlConn) orderModel := gmodel.NewFsOrderModel(l.svcCtx.MysqlConn) @@ -209,6 +302,7 @@ func (l *StripeWebhookLogic) handlePaymentIntentSucceeded(paymentIntent *stripe. *payInfo.PayTime = nowTime *payInfo.CardNo = card *payInfo.Brand = brand + *payInfo.TradeNo = paymentIntent.ID _, err = payModelT.CreateOrUpdate(ctx, payInfo) if err != nil { return err @@ -244,12 +338,12 @@ func (l *StripeWebhookLogic) handlePaymentIntentSucceeded(paymentIntent *stripe. // 支付记录是尾款 if *payInfo.PayStage == int64(constants.PAYSTAGE_REMAINING) { - if *orderInfo.Status < int64(constants.STATUS_NEW_PAY_COMPLETED) { + if *fsOrderRelInfo.Status < int64(constants.STATUS_NEW_PAY_COMPLETED) { orderStatus = int64(constants.STATUS_NEW_PAY_COMPLETED) } orderIsPayCompleted = 1 orderInfo.IsPayCompleted = &orderIsPayCompleted - orderPayedAmount = *orderInfo.PayedAmount + paymentIntent.Amount + orderPayedAmount = *fsOrderRelInfo.PayedAmount + paymentIntent.Amount } // 更新订单信息 diff --git a/server/pay/internal/types/types.go b/server/pay/internal/types/types.go index 76c0341e..fab57728 100644 --- a/server/pay/internal/types/types.go +++ b/server/pay/internal/types/types.go @@ -5,6 +5,12 @@ import ( "fusenapi/utils/basic" ) +type OrderRefundReq struct { +} + +type OrderRefundRes struct { +} + type OrderPaymentIntentReq struct { Sn string `form:"sn"` //订单编号 DeliveryMethod int64 `form:"delivery_method"` //发货方式 @@ -20,6 +26,7 @@ type OrderPaymentIntentRes struct { type StripeWebhookReq struct { Payload []byte `json:"base_byte_slice,optional"` StripeSignature string `json:"Stripe-Signature"` + RemoteAddr string `json:"remote_addr"` } type Request struct { diff --git a/server_api/pay.api b/server_api/pay.api index b7a1b0d1..4044c1e3 100644 --- a/server_api/pay.api +++ b/server_api/pay.api @@ -14,10 +14,20 @@ service pay { @handler OrderPaymentIntentHandler post /api/pay/payment-intent(OrderPaymentIntentReq) returns (response); + @handler OrderRefundHandler + post /api/pay/refund(OrderRefundReq) returns (response); + @handler StripeWebhookHandler post /api/pay/stripe-webhook(StripeWebhookReq) returns (response); + } +// 退款 +type ( + OrderRefundReq struct{} + OrderRefundRes struct{} +) + // 生成预付款 type ( OrderPaymentIntentReq { @@ -37,5 +47,6 @@ type ( StripeWebhookReq { Payload []byte `json:"base_byte_slice,optional"` StripeSignature string `json:"Stripe-Signature"` + RemoteAddr string `json:"remote_addr"` } ) \ No newline at end of file diff --git a/utils/basic/basic.go b/utils/basic/basic.go index 7ec3d02f..c8a5f370 100644 --- a/utils/basic/basic.go +++ b/utils/basic/basic.go @@ -52,14 +52,18 @@ var ( CodeSafeValueRangeErr = &StatusResponse{5040, "value not in range"} // 值不在范围内 CodeTemplateErr = &StatusResponse{5040, "template parsed error"} // 模板解析错误 - CodeOrderNotFoundErr = &StatusResponse{5030, "order not found"} //未找到订单 - CodeCloudOrderNotFoundErr = &StatusResponse{5031, "cloud order not found"} //未找到云仓订单 - CodeOrderNotCancelledErr = &StatusResponse{5032, "current order cannot be cancelled"} // 当前订单无法取消 + CodeOrderNotFoundErr = &StatusResponse{5030, "order not found"} //未找到订单 + CodeCloudOrderNotFoundErr = &StatusResponse{5031, "cloud order not found"} //未找到云仓订单 + CodeOrderNotCancelledErr = &StatusResponse{5032, "current order cannot be cancelled"} // 当前订单无法取消 + CodeOrderCancelledNotOk = &StatusResponse{5033, "current order cancelled failed"} // 当前订单取消失败 + CodeOrderCancelledOk = &StatusResponse{5034, "current order cancelled successful"} // 当前订单取消成功 CodePayNotFoundErr = &StatusResponse{5020, "pay info not found"} // 支付信息无法查询 CodePayCancelOk = &StatusResponse{5021, "cancellation successful"} // 支付取消成功 CodePayCancelNotOk = &StatusResponse{5022, "cancellation failed"} // 支付取消失败 + CodePaybackNotOk = &StatusResponse{5023, "pay back failed"} // 支付回调处理失败 + CodeGuestDupErr = &StatusResponse{5010, "user is already guest and does not need to reapply"} // 用户已经是访客用户,无需重复申请 CodeGuestGenErr = &StatusResponse{5011, "serialization failed for guest ID"} // 访客ID序列化失败 diff --git a/utils/handler/payHandler.go b/utils/handler/payHandler.go new file mode 100644 index 00000000..f69412b7 --- /dev/null +++ b/utils/handler/payHandler.go @@ -0,0 +1,64 @@ +package handler + +import ( + "fusenapi/constants" + "fusenapi/utils/pay" + + "github.com/zeromicro/go-zero/core/mr" +) + +type ( + PayInfo struct { + TradeNo string `json:"trade_no"` + PaymentMethod int64 `json:"payment_method"` + Key string `json:"key"` + } + PayRefundHandlerReq struct { + PayInfoList []PayInfo + } + PayRefundHandlerRes struct { + } +) + +// 申请第三方退款 +func PayRefundHandler(req *PayRefundHandlerReq) (res PayRefundHandlerRes, err error) { + + _, err = mr.MapReduce(func(source chan<- interface{}) { + for _, payInfo := range req.PayInfoList { + source <- payInfo + } + }, func(item interface{}, writer mr.Writer[interface{}], cancel func(error)) { + payConfig := new(pay.Config) + payInfo := item.(PayInfo) + switch payInfo.PaymentMethod { + case int64(constants.PAYMETHOD_STRIPE): + // stripe 支付 + payConfig.Stripe.Key = payInfo.Key + } + payDriver := pay.NewPayDriver(payInfo.PaymentMethod, payConfig) + _, err = payDriver.PayRefund(&pay.PayRefundReq{ + TradeNo: payInfo.TradeNo, + }) + + if err != nil { + // Notice 如果不加 cancel(err),会返回校验成功的id; 如果加上cancel(err),返回的结果会是一个空列表 + // Notice 实际上,如果这里返回错误,其他协程直接就退出了! + // Notice 看实际中业务的需求情况来定了... + cancel(err) + } + // Notice 这个必须加! + writer.Write(payInfo) + }, func(pipe <-chan interface{}, writer mr.Writer[interface{}], cancel func(error)) { + var payInfoList []PayInfo + for p := range pipe { + payInfoList = append(payInfoList, p.(PayInfo)) + } + // Notice 这个必须加! + writer.Write(payInfoList) + }) + if err != nil { + return res, err + } + + return res, nil +} diff --git a/utils/pay/pay.go b/utils/pay/pay.go index a378e4a8..e982139e 100644 --- a/utils/pay/pay.go +++ b/utils/pay/pay.go @@ -20,7 +20,12 @@ func NewPayDriver(PayMethod int64, config *Config) Pay { // Pay 支付集成接口 type Pay interface { + + // 支付预处理 GeneratePrepayment(req *GeneratePrepaymentReq) (res *GeneratePrepaymentRes, err error) + + // 支付退款申请 + PayRefund(req *PayRefundReq) (res *PayRefundRes, err error) } type GeneratePrepaymentReq struct { @@ -41,3 +46,10 @@ type GeneratePrepaymentRes struct { ClientSecret string `json:"clientSecret"` //交易密钥 SessionId string `json:"session_id"` //SessionId } + +type PayRefundReq struct { + TradeNo string `json:"trade_no"` // 交易编号 +} + +type PayRefundRes struct { +} diff --git a/utils/pay/stripe.go b/utils/pay/stripe.go index cc8127f7..e2e13799 100644 --- a/utils/pay/stripe.go +++ b/utils/pay/stripe.go @@ -3,12 +3,25 @@ package pay import ( "github.com/stripe/stripe-go/v74" "github.com/stripe/stripe-go/v74/checkout/session" + "github.com/stripe/stripe-go/v74/refund" + "github.com/zeromicro/go-zero/core/logx" ) type Stripe struct { Key string `json:"key"` } +// 生成退款 +func (stripePay *Stripe) PayRefund(req *PayRefundReq) (res *PayRefundRes, err error) { + stripe.Key = stripePay.Key + params := &stripe.RefundParams{PaymentIntent: stripe.String(req.TradeNo)} + _, err = refund.New(params) + if err != nil { + logx.Error(err) + } + return res, err +} + // 生成预付款 func (stripePay *Stripe) GeneratePrepayment(req *GeneratePrepaymentReq) (res *GeneratePrepaymentRes, err error) { var productData stripe.CheckoutSessionLineItemPriceDataProductDataParams