退款
This commit is contained in:
		
							parent
							
								
									ae717f932e
								
							
						
					
					
						commit
						1225a4efdc
					
				| @ -127,6 +127,7 @@ type Day int64 | |||||||
| // 订单取消时间 | // 订单取消时间 | ||||||
| const ( | const ( | ||||||
| 	CANCLE_ORDER_EXPIRE     Day = 48 * 3600 | 	CANCLE_ORDER_EXPIRE     Day = 48 * 3600 | ||||||
|  | 	CANCLE_ORDER_EXPIRE_DAY Day = 2 // 2天 | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // 订单时间配置 | // 订单时间配置 | ||||||
|  | |||||||
| @ -92,5 +92,5 @@ func (c *FsCartModel) DeleteCartsByIds(ctx context.Context, ids []int64) ( err e | |||||||
| 	if len(ids) == 0 { | 	if len(ids) == 0 { | ||||||
| 		return | 		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 | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										25
									
								
								model/gmodel/fs_pay_event_gen.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								model/gmodel/fs_pay_event_gen.go
									
									
									
									
									
										Normal file
									
								
							| @ -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"} | ||||||
|  | } | ||||||
							
								
								
									
										15
									
								
								model/gmodel/fs_pay_event_logic.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								model/gmodel/fs_pay_event_logic.go
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||||
|  | } | ||||||
| @ -7,7 +7,6 @@ import ( | |||||||
| // fs_pay 支付记录 | // fs_pay 支付记录 | ||||||
| type FsPay struct { | type FsPay struct { | ||||||
| 	Id            int64   `gorm:"primary_key;default:0;auto_increment;" json:"id"` // | 	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 | 	UserId        *int64  `gorm:"index;default:0;" json:"user_id"`                 // 用户id | ||||||
| 	OrderNumber   *string `gorm:"default:'';" json:"order_number"`                 // 订单编号 | 	OrderNumber   *string `gorm:"default:'';" json:"order_number"`                 // 订单编号 | ||||||
| 	TradeNo       *string `gorm:"index;default:'';" json:"trade_no"`               // 第三方支付编号 | 	TradeNo       *string `gorm:"index;default:'';" json:"trade_no"`               // 第三方支付编号 | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ package gmodel | |||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"fusenapi/utils/handler" | 	"fusenapi/utils/handler" | ||||||
|  | 	"reflect" | ||||||
| 
 | 
 | ||||||
| 	"gorm.io/gorm" | 	"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 { | 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() | 	tx := m.db.Table(m.name).WithContext(ctx).Begin() | ||||||
|  | |||||||
| @ -24,3 +24,17 @@ func (m *FsRefundReasonModel) Update(ctx context.Context, obj *FsRefundReason) e | |||||||
| func (m *FsRefundReasonModel) UpdateByRefundReasonId(ctx context.Context, obj *FsRefundReason) error { | 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 | 	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) TableName() string { | ||||||
|  | 	return m.name | ||||||
|  | } | ||||||
|  | |||||||
| @ -52,6 +52,7 @@ type AllModelsGen struct { | |||||||
| 	FsOrderDetailTemplate     *FsOrderDetailTemplateModel     // fs_order_detail_template 订单模板详细表 | 	FsOrderDetailTemplate     *FsOrderDetailTemplateModel     // fs_order_detail_template 订单模板详细表 | ||||||
| 	FsOrderRemark             *FsOrderRemarkModel             // fs_order_remark 订单备注表 | 	FsOrderRemark             *FsOrderRemarkModel             // fs_order_remark 订单备注表 | ||||||
| 	FsPay                     *FsPayModel                     // fs_pay 支付记录 | 	FsPay                     *FsPayModel                     // fs_pay 支付记录 | ||||||
|  | 	FsPayEvent                *FsPayEventModel                // fs_pay_event 支付回调事件日志 | ||||||
| 	FsProduct                 *FsProductModel                 // fs_product 产品表 | 	FsProduct                 *FsProductModel                 // fs_product 产品表 | ||||||
| 	FsProductCopy1            *FsProductCopy1Model            // fs_product_copy1 产品表 | 	FsProductCopy1            *FsProductCopy1Model            // fs_product_copy1 产品表 | ||||||
| 	FsProductDesign           *FsProductDesignModel           // fs_product_design 产品设计表 | 	FsProductDesign           *FsProductDesignModel           // fs_product_design 产品设计表 | ||||||
| @ -144,6 +145,7 @@ func NewAllModels(gdb *gorm.DB) *AllModelsGen { | |||||||
| 		FsOrderDetailTemplate:     NewFsOrderDetailTemplateModel(gdb), | 		FsOrderDetailTemplate:     NewFsOrderDetailTemplateModel(gdb), | ||||||
| 		FsOrderRemark:             NewFsOrderRemarkModel(gdb), | 		FsOrderRemark:             NewFsOrderRemarkModel(gdb), | ||||||
| 		FsPay:                     NewFsPayModel(gdb), | 		FsPay:                     NewFsPayModel(gdb), | ||||||
|  | 		FsPayEvent:                NewFsPayEventModel(gdb), | ||||||
| 		FsProduct:                 NewFsProductModel(gdb), | 		FsProduct:                 NewFsProductModel(gdb), | ||||||
| 		FsProductCopy1:            NewFsProductCopy1Model(gdb), | 		FsProductCopy1:            NewFsProductCopy1Model(gdb), | ||||||
| 		FsProductDesign:           NewFsProductDesignModel(gdb), | 		FsProductDesign:           NewFsProductDesignModel(gdb), | ||||||
|  | |||||||
| @ -20,3 +20,10 @@ OAuth: | |||||||
| 
 | 
 | ||||||
| Stripe: | Stripe: | ||||||
|   SK: "sk_test_51IisojHygnIJZeghPVSBhkwySfcyDV4SoAduIxu3J7bvSJ9cZMD96LY1LO6SpdbYquLJX5oKvgEBB67KT9pecfCy00iEC4pp9y" |   SK: "sk_test_51IisojHygnIJZeghPVSBhkwySfcyDV4SoAduIxu3J7bvSJ9cZMD96LY1LO6SpdbYquLJX5oKvgEBB67KT9pecfCy00iEC4pp9y" | ||||||
|  | 
 | ||||||
|  | PayConfig: | ||||||
|  |   Stripe: | ||||||
|  |     Key: "sk_test_51IisojHygnIJZeghPVSBhkwySfcyDV4SoAduIxu3J7bvSJ9cZMD96LY1LO6SpdbYquLJX5oKvgEBB67KT9pecfCy00iEC4pp9y" | ||||||
|  |     EndpointSecret: "whsec_f5f9a121d43af3789db7459352f08cf523eb9e0fbf3381f91ba6c97c324c174d" | ||||||
|  |     SuccessURL: "http://www.baidu.com" | ||||||
|  |     CancelURL: "http://www.baidu.com" | ||||||
|  | |||||||
| @ -28,4 +28,13 @@ type Config struct { | |||||||
| 	Stripe struct { | 	Stripe struct { | ||||||
| 		SK string | 		SK string | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	PayConfig struct { | ||||||
|  | 		Stripe struct { | ||||||
|  | 			EndpointSecret string | ||||||
|  | 			Key            string | ||||||
|  | 			CancelURL      string | ||||||
|  | 			SuccessURL     string | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  | |||||||
| @ -6,11 +6,13 @@ import ( | |||||||
| 	"fusenapi/model/gmodel" | 	"fusenapi/model/gmodel" | ||||||
| 	"fusenapi/utils/auth" | 	"fusenapi/utils/auth" | ||||||
| 	"fusenapi/utils/basic" | 	"fusenapi/utils/basic" | ||||||
|  | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"context" | 	"context" | ||||||
| 
 | 
 | ||||||
| 	"fusenapi/server/home-user-auth/internal/svc" | 	"fusenapi/server/home-user-auth/internal/svc" | ||||||
| 	"fusenapi/server/home-user-auth/internal/types" | 	"fusenapi/server/home-user-auth/internal/types" | ||||||
|  | 	handlerUtils "fusenapi/utils/handler" | ||||||
| 
 | 
 | ||||||
| 	"github.com/zeromicro/go-zero/core/logx" | 	"github.com/zeromicro/go-zero/core/logx" | ||||||
| 	"gorm.io/gorm" | 	"gorm.io/gorm" | ||||||
| @ -50,11 +52,76 @@ func (l *UserOrderCancelLogic) UserOrderCancel(req *types.UserOrderCancelReq, us | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// 判断订单状态 | 	// 判断订单状态 | ||||||
| 	if *orderInfo.Status == int64(constants.STATUS_NEW_NOT_PAY) { | 	var notCancelStatusMap = make(map[constants.Order]struct{}, 3) | ||||||
| 
 | 	notCancelStatusMap[constants.STATUS_NEW_NOT_PAY] = struct{}{} | ||||||
| 	} else { | 	notCancelStatusMap[constants.STATUS_NEW_PART_PAY] = struct{}{} | ||||||
|  | 	notCancelStatusMap[constants.STATUS_NEW_PAY_COMPLETED] = struct{}{} | ||||||
|  | 	_, ok := notCancelStatusMap[constants.Order(*orderInfo.Status)] | ||||||
|  | 	if !ok { | ||||||
| 		return resp.SetStatusWithMessage(basic.CodeOrderNotCancelledErr, "the order status not cancle") | 		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 != nil && 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) | 	return resp.SetStatus(basic.CodeOK) | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| Name: pay | Name: pay | ||||||
| Host: 0.0.0.0 | Host: 0.0.0.0 | ||||||
| Port: 9915 | Port: 9915 | ||||||
| Timeout: 15000 |  | ||||||
| SourceMysql: fusentest:XErSYmLELKMnf3Dh@tcp(110.41.19.98:3306)/fusentest | SourceMysql: fusentest:XErSYmLELKMnf3Dh@tcp(110.41.19.98:3306)/fusentest | ||||||
| Auth: | Auth: | ||||||
|     AccessSecret: fusen2023 |     AccessSecret: fusen2023 | ||||||
|  | |||||||
							
								
								
									
										35
									
								
								server/pay/internal/handler/orderrefundhandler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								server/pay/internal/handler/orderrefundhandler.go
									
									
									
									
									
										Normal file
									
								
							| @ -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) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -17,6 +17,11 @@ func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { | |||||||
| 				Path:    "/api/pay/payment-intent", | 				Path:    "/api/pay/payment-intent", | ||||||
| 				Handler: OrderPaymentIntentHandler(serverCtx), | 				Handler: OrderPaymentIntentHandler(serverCtx), | ||||||
| 			}, | 			}, | ||||||
|  | 			{ | ||||||
|  | 				Method:  http.MethodPost, | ||||||
|  | 				Path:    "/api/pay/refund", | ||||||
|  | 				Handler: OrderRefundHandler(serverCtx), | ||||||
|  | 			}, | ||||||
| 			{ | 			{ | ||||||
| 				Method:  http.MethodPost, | 				Method:  http.MethodPost, | ||||||
| 				Path:    "/api/pay/stripe-webhook", | 				Path:    "/api/pay/stripe-webhook", | ||||||
|  | |||||||
| @ -30,6 +30,15 @@ func StripeWebhookHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { | |||||||
| 		// 	return | 		// 	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.Payload = payload | ||||||
| 		req.StripeSignature = r.Header.Get("Stripe-Signature") | 		req.StripeSignature = r.Header.Get("Stripe-Signature") | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										43
									
								
								server/pay/internal/logic/orderrefundlogic.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								server/pay/internal/logic/orderrefundlogic.go
									
									
									
									
									
										Normal file
									
								
							| @ -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) | ||||||
|  | } | ||||||
| @ -64,6 +64,21 @@ func (l *StripeWebhookLogic) StripeWebhook(req *types.StripeWebhookReq, userinfo | |||||||
| 		return resp.SetStatusWithMessage(basic.CodeAesCbcDecryptionErr, "Webhook signature verification failed") | 		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 | 	// Unmarshal the event data into an appropriate struct depending on its Type | ||||||
| 	switch event.Type { | 	switch event.Type { | ||||||
| 	case "charge.succeeded": | 	case "charge.succeeded": | ||||||
| @ -94,7 +109,7 @@ func (l *StripeWebhookLogic) StripeWebhook(req *types.StripeWebhookReq, userinfo | |||||||
| 			logx.Error(err) | 			logx.Error(err) | ||||||
| 			return resp.SetStatusWithMessage(basic.CodeAesCbcDecryptionErr, "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 { | 		if err != nil { | ||||||
| 			return resp.SetStatusWithMessage(basic.CodeAesCbcDecryptionErr, "pay notify Unmarshal fail event.Type Unhandled") | 			return resp.SetStatusWithMessage(basic.CodeAesCbcDecryptionErr, "pay notify Unmarshal fail event.Type Unhandled") | ||||||
| 		} | 		} | ||||||
| @ -114,6 +129,12 @@ func (l *StripeWebhookLogic) StripeWebhook(req *types.StripeWebhookReq, userinfo | |||||||
| 	return resp.SetStatus(basic.CodeOK) | 	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 | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // session完成 | // session完成 | ||||||
| // func (l *StripeWebhookLogic) handlePaymentSessionCompleted(sessionId string, tradeNo string) (err error) { | // func (l *StripeWebhookLogic) handlePaymentSessionCompleted(sessionId string, tradeNo string) (err error) { | ||||||
| // 	// 查询支付记录 | // 	// 查询支付记录 | ||||||
| @ -137,7 +158,7 @@ func (l *StripeWebhookLogic) StripeWebhook(req *types.StripeWebhookReq, userinfo | |||||||
| // } | // } | ||||||
| 
 | 
 | ||||||
| // 成功的付款 | // 成功的付款 | ||||||
| func (l *StripeWebhookLogic) handlePaymentIntentSucceeded(paymentIntent *stripe.PaymentIntent) error { | func (l *StripeWebhookLogic) HandlePaymentIntentSucceeded(paymentIntent *stripe.PaymentIntent) error { | ||||||
| 	orderSn, ok := paymentIntent.Metadata["order_sn"] | 	orderSn, ok := paymentIntent.Metadata["order_sn"] | ||||||
| 	if !ok || orderSn == "" { | 	if !ok || orderSn == "" { | ||||||
| 		return errors.New("order_sn not found") | 		return errors.New("order_sn not found") | ||||||
| @ -146,14 +167,12 @@ func (l *StripeWebhookLogic) handlePaymentIntentSucceeded(paymentIntent *stripe. | |||||||
| 	// 查询支付记录 | 	// 查询支付记录 | ||||||
| 	payModel := gmodel.NewFsPayModel(l.svcCtx.MysqlConn) | 	payModel := gmodel.NewFsPayModel(l.svcCtx.MysqlConn) | ||||||
| 	rsbPay := payModel.RowSelectBuilder(nil) | 	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) | 	payInfo, err := payModel.FindOneByQuery(l.ctx, rsbPay, nil) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	if *payInfo.PayStatus == 1 { | 
 | ||||||
| 		return errors.New("pay status 1") |  | ||||||
| 	} |  | ||||||
| 	//订单信息 | 	//订单信息 | ||||||
| 	orderDetailTemplateModel := gmodel.NewFsOrderDetailTemplateModel(l.svcCtx.MysqlConn) | 	orderDetailTemplateModel := gmodel.NewFsOrderDetailTemplateModel(l.svcCtx.MysqlConn) | ||||||
| 	orderModel := gmodel.NewFsOrderModel(l.svcCtx.MysqlConn) | 	orderModel := gmodel.NewFsOrderModel(l.svcCtx.MysqlConn) | ||||||
| @ -209,6 +228,7 @@ func (l *StripeWebhookLogic) handlePaymentIntentSucceeded(paymentIntent *stripe. | |||||||
| 			*payInfo.PayTime = nowTime | 			*payInfo.PayTime = nowTime | ||||||
| 			*payInfo.CardNo = card | 			*payInfo.CardNo = card | ||||||
| 			*payInfo.Brand = brand | 			*payInfo.Brand = brand | ||||||
|  | 			*payInfo.TradeNo = paymentIntent.ID | ||||||
| 			_, err = payModelT.CreateOrUpdate(ctx, payInfo) | 			_, err = payModelT.CreateOrUpdate(ctx, payInfo) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return err | 				return err | ||||||
| @ -244,12 +264,12 @@ func (l *StripeWebhookLogic) handlePaymentIntentSucceeded(paymentIntent *stripe. | |||||||
| 
 | 
 | ||||||
| 			// 支付记录是尾款 | 			// 支付记录是尾款 | ||||||
| 			if *payInfo.PayStage == int64(constants.PAYSTAGE_REMAINING) { | 			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) | 					orderStatus = int64(constants.STATUS_NEW_PAY_COMPLETED) | ||||||
| 				} | 				} | ||||||
| 				orderIsPayCompleted = 1 | 				orderIsPayCompleted = 1 | ||||||
| 				orderInfo.IsPayCompleted = &orderIsPayCompleted | 				orderInfo.IsPayCompleted = &orderIsPayCompleted | ||||||
| 				orderPayedAmount = *orderInfo.PayedAmount + paymentIntent.Amount | 				orderPayedAmount = *fsOrderRelInfo.PayedAmount + paymentIntent.Amount | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			// 更新订单信息 | 			// 更新订单信息 | ||||||
|  | |||||||
| @ -5,6 +5,12 @@ import ( | |||||||
| 	"fusenapi/utils/basic" | 	"fusenapi/utils/basic" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | type OrderRefundReq struct { | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type OrderRefundRes struct { | ||||||
|  | } | ||||||
|  | 
 | ||||||
| type OrderPaymentIntentReq struct { | type OrderPaymentIntentReq struct { | ||||||
| 	Sn             string `form:"sn"`              //订单编号 | 	Sn             string `form:"sn"`              //订单编号 | ||||||
| 	DeliveryMethod int64  `form:"delivery_method"` //发货方式 | 	DeliveryMethod int64  `form:"delivery_method"` //发货方式 | ||||||
| @ -20,6 +26,7 @@ type OrderPaymentIntentRes struct { | |||||||
| type StripeWebhookReq struct { | type StripeWebhookReq struct { | ||||||
| 	Payload         []byte `json:"base_byte_slice,optional"` | 	Payload         []byte `json:"base_byte_slice,optional"` | ||||||
| 	StripeSignature string `json:"Stripe-Signature"` | 	StripeSignature string `json:"Stripe-Signature"` | ||||||
|  | 	RemoteAddr      string `json:"remote_addr"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type Request struct { | type Request struct { | ||||||
|  | |||||||
| @ -14,10 +14,20 @@ service pay { | |||||||
| 	@handler OrderPaymentIntentHandler | 	@handler OrderPaymentIntentHandler | ||||||
| 	post /api/pay/payment-intent(OrderPaymentIntentReq) returns (response); | 	post /api/pay/payment-intent(OrderPaymentIntentReq) returns (response); | ||||||
| 
 | 
 | ||||||
|  | 	@handler OrderRefundHandler | ||||||
|  | 	post /api/pay/refund(OrderRefundReq) returns (response); | ||||||
|  | 
 | ||||||
| 	@handler StripeWebhookHandler | 	@handler StripeWebhookHandler | ||||||
| 	post /api/pay/stripe-webhook(StripeWebhookReq) returns (response); | 	post /api/pay/stripe-webhook(StripeWebhookReq) returns (response); | ||||||
|  | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // 退款 | ||||||
|  | type ( | ||||||
|  | 	OrderRefundReq struct{} | ||||||
|  | 	OrderRefundRes struct{} | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| // 生成预付款 | // 生成预付款 | ||||||
| type ( | type ( | ||||||
| 	OrderPaymentIntentReq { | 	OrderPaymentIntentReq { | ||||||
| @ -37,5 +47,6 @@ type ( | |||||||
| 	StripeWebhookReq { | 	StripeWebhookReq { | ||||||
| 		Payload         []byte `json:"base_byte_slice,optional"` | 		Payload         []byte `json:"base_byte_slice,optional"` | ||||||
| 		StripeSignature string `json:"Stripe-Signature"` | 		StripeSignature string `json:"Stripe-Signature"` | ||||||
|  | 		RemoteAddr      string `json:"remote_addr"` | ||||||
| 	} | 	} | ||||||
| ) | ) | ||||||
| @ -55,6 +55,8 @@ var ( | |||||||
| 	CodeOrderNotFoundErr      = &StatusResponse{5030, "order not found"}                    //未找到订单 | 	CodeOrderNotFoundErr      = &StatusResponse{5030, "order not found"}                    //未找到订单 | ||||||
| 	CodeCloudOrderNotFoundErr = &StatusResponse{5031, "cloud order not found"}              //未找到云仓订单 | 	CodeCloudOrderNotFoundErr = &StatusResponse{5031, "cloud order not found"}              //未找到云仓订单 | ||||||
| 	CodeOrderNotCancelledErr  = &StatusResponse{5032, "current order cannot be cancelled"}  // 当前订单无法取消 | 	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"}      // 支付信息无法查询 | 	CodePayNotFoundErr = &StatusResponse{5020, "pay info not found"}      // 支付信息无法查询 | ||||||
| 	CodePayCancelOk    = &StatusResponse{5021, "cancellation successful"} // 支付取消成功 | 	CodePayCancelOk    = &StatusResponse{5021, "cancellation successful"} // 支付取消成功 | ||||||
|  | |||||||
							
								
								
									
										64
									
								
								utils/handler/payHandler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								utils/handler/payHandler.go
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||||
|  | } | ||||||
| @ -20,7 +20,12 @@ func NewPayDriver(PayMethod int64, config *Config) Pay { | |||||||
| 
 | 
 | ||||||
| // Pay 支付集成接口 | // Pay 支付集成接口 | ||||||
| type Pay interface { | type Pay interface { | ||||||
|  | 
 | ||||||
|  | 	// 支付预处理 | ||||||
| 	GeneratePrepayment(req *GeneratePrepaymentReq) (res *GeneratePrepaymentRes, err error) | 	GeneratePrepayment(req *GeneratePrepaymentReq) (res *GeneratePrepaymentRes, err error) | ||||||
|  | 
 | ||||||
|  | 	// 支付退款申请 | ||||||
|  | 	PayRefund(req *PayRefundReq) (res *PayRefundRes, err error) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type GeneratePrepaymentReq struct { | type GeneratePrepaymentReq struct { | ||||||
| @ -41,3 +46,10 @@ type GeneratePrepaymentRes struct { | |||||||
| 	ClientSecret string `json:"clientSecret"` //交易密钥 | 	ClientSecret string `json:"clientSecret"` //交易密钥 | ||||||
| 	SessionId    string `json:"session_id"`   //SessionId | 	SessionId    string `json:"session_id"`   //SessionId | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | type PayRefundReq struct { | ||||||
|  | 	TradeNo string `json:"trade_no"` // 交易编号 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type PayRefundRes struct { | ||||||
|  | } | ||||||
|  | |||||||
| @ -3,12 +3,25 @@ package pay | |||||||
| import ( | import ( | ||||||
| 	"github.com/stripe/stripe-go/v74" | 	"github.com/stripe/stripe-go/v74" | ||||||
| 	"github.com/stripe/stripe-go/v74/checkout/session" | 	"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 { | type Stripe struct { | ||||||
| 	Key string `json:"key"` | 	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) { | func (stripePay *Stripe) GeneratePrepayment(req *GeneratePrepaymentReq) (res *GeneratePrepaymentRes, err error) { | ||||||
| 	var productData stripe.CheckoutSessionLineItemPriceDataProductDataParams | 	var productData stripe.CheckoutSessionLineItemPriceDataProductDataParams | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user