From 8c84ed3656e24f3faf8be6a25caca013a0f99d11 Mon Sep 17 00:00:00 2001 From: wucongxing8150 <815046773@qq.com> Date: Tue, 23 Jul 2024 11:41:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E5=8D=95=E9=A1=B9?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E7=9A=84=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/controller/Login.go | 36 ++-- api/controller/OrderSingle.go | 259 +++++++++++++++++++++++++++ api/dao/OrderSingle.go | 12 +- api/dao/OrderSingleCoupon.go | 9 + api/dto/Coupon.go | 2 +- api/dto/Login.go | 38 ++++ api/dto/OrderSingle.go | 33 +++- api/dto/Question.go | 2 +- api/model/Coupon.go | 2 +- api/model/OrderSingle.go | 36 ++-- api/model/Question.go | 28 +-- api/requests/OrderSingle.go | 14 ++ api/router/router.go | 12 ++ api/service/OrderSingle.go | 323 ++++++++++++++++++++++++++++++++++ api/service/Question.go | 48 +++++ api/service/User.go | 28 ++- api/service/UserCoupon.go | 116 ++++++++++++ config.yaml | 18 +- config/wechat.go | 15 +- extend/weChat/base.go | 43 +++++ extend/weChat/prepay.go | 133 ++++++++++++++ extend/weChat/refund.go | 46 +++++ extend/weChat/weChatPay.go | 79 --------- utils/intToString.go | 14 +- 24 files changed, 1181 insertions(+), 165 deletions(-) create mode 100644 api/dto/Login.go create mode 100644 api/service/OrderSingle.go create mode 100644 api/service/UserCoupon.go create mode 100644 extend/weChat/base.go create mode 100644 extend/weChat/prepay.go create mode 100644 extend/weChat/refund.go delete mode 100644 extend/weChat/weChatPay.go diff --git a/api/controller/Login.go b/api/controller/Login.go index 8c08678..c9de67f 100644 --- a/api/controller/Login.go +++ b/api/controller/Login.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/gin-gonic/gin" "hepa-calc-api/api/dao" + "hepa-calc-api/api/dto" "hepa-calc-api/api/model" "hepa-calc-api/api/requests" "hepa-calc-api/api/responses" @@ -35,18 +36,6 @@ func (r *Login) LoginPhone(c *gin.Context) { // LoginWx 微信授权登录 func (r *Login) LoginWx(c *gin.Context) { - userService := service.UserService{} - // 处理用户头像 - avatar, err := userService.HandleUserAvatar("https://thirdwx.qlogo.cn/mmopen/vi_32/dzhWq43iaB5GyJJUbajibSQtIt6d4Y9QPk5ImB1C14fPk6AOtcgaPztUzjKwb9kmPtqNpyCal5cEyw0YyTr1QEbdtHiaOViay4RgSpdm2XaduXA/132") - if err != nil { - responses.FailWithMessage(err.Error(), c) - return - } - - fmt.Println(avatar) - responses.Ok(c) - return - loginRequest := requests.LoginRequest{} req := loginRequest.LoginWx if err := c.ShouldBind(&req); err != nil { @@ -87,12 +76,13 @@ func (r *Login) LoginWx(c *gin.Context) { tx := global.Db.Begin() defer func() { if r := recover(); r != nil { + fmt.Println(r) tx.Rollback() } }() - userService := service.UserService{} // 处理用户头像 + userService := service.UserService{} avatar, err := userService.HandleUserAvatar(userInfo.HeadImgUrl) if err != nil { tx.Rollback() @@ -101,7 +91,7 @@ func (r *Login) LoginWx(c *gin.Context) { } // 新增用户 - user := &model.User{ + user = &model.User{ UserName: userInfo.Nickname, Mobile: "", RegisterSource: req.Source, @@ -123,10 +113,10 @@ func (r *Login) LoginWx(c *gin.Context) { } // 获取app用户信息 - var result *string - if result == nil { - // 新增app用户信息 - } + //var result *string + //if result == nil { + // // 新增app用户信息 + //} tx.Commit() } @@ -138,10 +128,16 @@ func (r *Login) LoginWx(c *gin.Context) { // 生成jwt jwt, err := token.NewJWT() - if err != nil { + if err != nil || jwt == "" { responses.FailWithMessage("登陆失败", c) return } - responses.OkWithData(jwt, c) + // 处理返回值 + g := dto.LoginWxDto(user) + + // 加载token + g.LoadToken(jwt) + + responses.OkWithData(g, c) } diff --git a/api/controller/OrderSingle.go b/api/controller/OrderSingle.go index ba24485..e713827 100644 --- a/api/controller/OrderSingle.go +++ b/api/controller/OrderSingle.go @@ -1,13 +1,17 @@ package controller import ( + "fmt" "github.com/gin-gonic/gin" "hepa-calc-api/api/dao" "hepa-calc-api/api/dto" "hepa-calc-api/api/requests" "hepa-calc-api/api/responses" + "hepa-calc-api/api/service" "hepa-calc-api/global" "hepa-calc-api/utils" + "strconv" + "time" ) type OrderSingle struct{} @@ -56,3 +60,258 @@ func (b *OrderSingle) GetOrderSinglePage(c *gin.Context) { result["data"] = g responses.OkWithData(result, c) } + +// AddOrderSingle 创建单项订单 +func (b *OrderSingle) AddOrderSingle(c *gin.Context) { + orderSingleRequest := requests.OrderSingleRequest{} + req := orderSingleRequest.AddOrderSingle + if err := c.ShouldBind(&req); err != nil { + responses.FailWithMessage(err.Error(), c) + return + } + + // 参数验证 + if err := global.Validate.Struct(req); err != nil { + responses.FailWithMessage(utils.Translate(err), c) + return + } + + userId := c.GetInt64("UserId") + + // 将 id 转换为 int64 类型 + questionId, err := strconv.ParseInt(req.QuestionId, 10, 64) + if err != nil { + responses.Fail(c) + return + } + + // 用户优惠卷id + var userCouponId *int64 + if req.UserCouponId != "" { + id, err := strconv.ParseInt(req.UserCouponId, 10, 64) + if err != nil { + responses.Fail(c) + return + } + + userCouponId = &id + } + + // 开始事务 + tx := global.Db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + // 创建单项订单 + orderSingleService := service.OrderSingleService{} + orderSingle, err := orderSingleService.AddOrderSingle(tx, userId, questionId, userCouponId, req.PayChannel) + if err != nil { + tx.Rollback() + responses.FailWithMessage(err.Error(), c) + return + } + + tx.Commit() + + // 处理返回值 + orderId := fmt.Sprintf("%d", orderSingle.OrderId) + + responses.OkWithData(orderId, c) +} + +// PutCancelOrderSingle 取消单项订单 +func (b *OrderSingle) PutCancelOrderSingle(c *gin.Context) { + userId := c.GetInt64("UserId") + + id := c.Param("order_id") + if id == "" { + responses.FailWithMessage("缺少参数", c) + return + } + + // 将 id 转换为 int64 类型 + orderId, err := strconv.ParseInt(id, 10, 64) + if err != nil { + responses.Fail(c) + return + } + + // 开始事务 + tx := global.Db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + // 取消单项订单 + orderSingleService := service.OrderSingleService{} + res, err := orderSingleService.PutCancelOrderSingle(tx, userId, orderId, 1) + if err != nil { + tx.Rollback() + responses.FailWithMessage(err.Error(), c) + return + } + + if res == false { + tx.Rollback() + responses.FailWithMessage("取消订单失败", c) + return + } + + tx.Commit() + responses.Ok(c) +} + +// GetOrderSinglePay 获取单项订单支付数据 +func (b *OrderSingle) GetOrderSinglePay(c *gin.Context) { + orderSingleRequest := requests.OrderSingleRequest{} + req := orderSingleRequest.GetOrderSinglePay + if err := c.ShouldBind(&req); err != nil { + responses.FailWithMessage(err.Error(), c) + return + } + + // 参数验证 + if err := global.Validate.Struct(req); err != nil { + responses.FailWithMessage(utils.Translate(err), c) + return + } + + userId := c.GetInt64("UserId") + + id := c.Param("order_id") + if id == "" { + responses.FailWithMessage("缺少参数", c) + return + } + + // 将 id 转换为 int64 类型 + orderId, err := strconv.ParseInt(id, 10, 64) + if err != nil { + responses.Fail(c) + return + } + + // 获取订单数据 + orderSingleDao := dao.OrderSingleDao{} + maps := make(map[string]interface{}) + maps["user_id"] = userId + maps["order_id"] = orderId + orderSingle, err := orderSingleDao.GetOrderSingle(maps) + if err != nil { + responses.FailWithMessage("订单异常", c) + return + } + + // 检测订单状态(1:待支付 2:已完成 3:已取消) + if orderSingle.OrderStatus != 1 { + responses.FailWithMessage("订单状态异常", c) + return + } + + // 支付状态(1:未支付 2:已支付 3:支付中 4:支付失败 5:支付超时 6:支付关闭 7:已撤销 8:转入退款) + if orderSingle.PayStatus != 1 { + responses.FailWithMessage("订单支付状态异常", c) + return + } + + // 验证订单过期支付时间 + now := time.Now() + validTime := time.Time(orderSingle.CreatedAt).Add(30 * time.Minute) + if validTime.Before(now) { + responses.FailWithMessage("订单已过期", c) + return + } + + // 处理返回值 + g := dto.GetOrderSinglePayDto(orderSingle) + + // 获取预支付交易会话标识 + if req.ClientType == 1 { + orderSingleService := service.OrderSingleService{} + prepay, err := orderSingleService.GetJsapiPrepay(orderSingle) + if err != nil { + responses.FailWithMessage("发起支付失败", c) + return + } + + g.PrepayJsapi = prepay + } else { + orderSingleService := service.OrderSingleService{} + prepay, err := orderSingleService.GetAppPrepay(orderSingle) + if err != nil { + responses.FailWithMessage("发起支付失败", c) + return + } + + g.PrepayApp = prepay + } + + responses.OkWithData(g, c) +} + +// DeleteOrderSingle 删除单项订单支付数据 +func (b *OrderSingle) DeleteOrderSingle(c *gin.Context) { + userId := c.GetInt64("UserId") + + id := c.Param("order_id") + if id == "" { + responses.FailWithMessage("缺少参数", c) + return + } + + // 将 id 转换为 int64 类型 + orderId, err := strconv.ParseInt(id, 10, 64) + if err != nil { + responses.Fail(c) + return + } + + // 获取订单数据 + orderSingleDao := dao.OrderSingleDao{} + maps := make(map[string]interface{}) + maps["user_id"] = userId + maps["order_id"] = orderId + orderSingle, err := orderSingleDao.GetOrderSingle(maps) + if err != nil { + responses.FailWithMessage("订单异常", c) + return + } + + // 检测订单状态 + if orderSingle.OrderStatus != 2 { + responses.FailWithMessage("订单不允许删除", c) + return + } + + // 检测订单删除状态 + if orderSingle.IsDelete == 1 { + responses.Ok(c) + return + } + + // 开始事务 + tx := global.Db.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + orderSingleData := make(map[string]interface{}) + orderSingleData["is_delete"] = 1 + orderSingleData["updated_at"] = time.Now().Format("2006-01-02 15:04:05") + err = orderSingleDao.EditOrderSingleById(tx, orderId, orderSingleData) + if err != nil { + tx.Rollback() + responses.FailWithMessage("删除失败", c) + return + } + + tx.Commit() + responses.Ok(c) +} diff --git a/api/dao/OrderSingle.go b/api/dao/OrderSingle.go index 53a63fb..a3566b4 100644 --- a/api/dao/OrderSingle.go +++ b/api/dao/OrderSingle.go @@ -12,8 +12,8 @@ type OrderSingleDao struct { } // GetOrderSingleById 获取数据-id -func (r *OrderSingleDao) GetOrderSingleById(OrderSingleId int64) (m *model.OrderSingle, err error) { - err = global.Db.First(&m, OrderSingleId).Error +func (r *OrderSingleDao) GetOrderSingleById(OrderId int64) (m *model.OrderSingle, err error) { + err = global.Db.First(&m, OrderId).Error if err != nil { return nil, err } @@ -21,8 +21,8 @@ func (r *OrderSingleDao) GetOrderSingleById(OrderSingleId int64) (m *model.Order } // GetOrderSinglePreloadById 获取数据-加载全部关联-id -func (r *OrderSingleDao) GetOrderSinglePreloadById(OrderSingleId int64) (m *model.OrderSingle, err error) { - err = global.Db.Preload(clause.Associations).First(&m, OrderSingleId).Error +func (r *OrderSingleDao) GetOrderSinglePreloadById(OrderId int64) (m *model.OrderSingle, err error) { + err = global.Db.Preload(clause.Associations).First(&m, OrderId).Error if err != nil { return nil, err } @@ -39,8 +39,8 @@ func (r *OrderSingleDao) DeleteOrderSingle(tx *gorm.DB, maps interface{}) error } // DeleteOrderSingleById 删除-id -func (r *OrderSingleDao) DeleteOrderSingleById(tx *gorm.DB, OrderSingleId int64) error { - if err := tx.Delete(&model.OrderSingle{}, OrderSingleId).Error; err != nil { +func (r *OrderSingleDao) DeleteOrderSingleById(tx *gorm.DB, orderId int64) error { + if err := tx.Delete(&model.OrderSingle{}, orderId).Error; err != nil { return err } return nil diff --git a/api/dao/OrderSingleCoupon.go b/api/dao/OrderSingleCoupon.go index 38b982d..0ea2c5b 100644 --- a/api/dao/OrderSingleCoupon.go +++ b/api/dao/OrderSingleCoupon.go @@ -28,6 +28,15 @@ func (r *OrderSingleCouponDao) GetOrderSingleCouponPreloadById(OrderSingleCoupon return m, nil } +// GetOrderSingleCouponByOrderId 获取数据-订单id +func (r *OrderSingleCouponDao) GetOrderSingleCouponByOrderId(orderId int64) (m *model.OrderSingleCoupon, err error) { + err = global.Db.Where("order_id = ?", orderId).First(&m).Error + if err != nil { + return nil, err + } + return m, nil +} + // DeleteOrderSingleCoupon 删除 func (r *OrderSingleCouponDao) DeleteOrderSingleCoupon(tx *gorm.DB, maps interface{}) error { err := tx.Where(maps).Delete(&model.OrderSingleCoupon{}).Error diff --git a/api/dto/Coupon.go b/api/dto/Coupon.go index 3213794..02e1788 100644 --- a/api/dto/Coupon.go +++ b/api/dto/Coupon.go @@ -8,7 +8,7 @@ import ( type CouponDto struct { CouponId string `json:"coupon_id"` // 主键id CouponName string `json:"coupon_name"` // 优惠券名称 - CouponType string `json:"coupon_type"` // 优惠券类型(1:无门槛 2:满减) + CouponType int `json:"coupon_type"` // 优惠券类型(1:无门槛 2:满减) CouponStatus int `json:"coupon_status"` // 状态(1:正常 2:强制失效 3:结束 4:删除) ApplicationScope int `json:"application_scope"` // 适用范围(1:全场通用) IsMutex int `json:"is_mutex"` // 是否互斥(0:否 1:是) diff --git a/api/dto/Login.go b/api/dto/Login.go new file mode 100644 index 0000000..6796e08 --- /dev/null +++ b/api/dto/Login.go @@ -0,0 +1,38 @@ +package dto + +import ( + "fmt" + "hepa-calc-api/api/model" + "hepa-calc-api/utils" + "time" +) + +type LoginDto struct { + UserId string `json:"user_id"` // 用户id + UserName string `json:"user_name"` // 用户名称 + Mobile string `json:"mobile"` // 手机号 + OpenId string `json:"open_id"` // 用户微信标识 + Avatar string `json:"avatar"` // 头像 + IsMember int `json:"is_member"` // 是否会员(0:否 1:是) + MemberExpireDate *time.Time `json:"member_expire_date"` // 会员到期时间(非会员时为null) + Token string `json:"token"` // token +} + +// LoginWxDto 微信登陆 +func LoginWxDto(m *model.User) *LoginDto { + return &LoginDto{ + UserId: fmt.Sprintf("%d", m.UserId), + UserName: m.UserName, + Mobile: m.Mobile, + OpenId: m.OpenId, + Avatar: utils.AddOssDomain(m.Avatar), + IsMember: m.IsMember, + MemberExpireDate: m.MemberExpireDate, + } +} + +// LoadToken 加载token +func (r *LoginDto) LoadToken(token string) *LoginDto { + r.Token = token + return r +} diff --git a/api/dto/OrderSingle.go b/api/dto/OrderSingle.go index c2d70ea..91b58b5 100644 --- a/api/dto/OrderSingle.go +++ b/api/dto/OrderSingle.go @@ -2,7 +2,10 @@ package dto import ( "fmt" + "github.com/wechatpay-apiv3/wechatpay-go/services/payments/app" + "github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi" "hepa-calc-api/api/model" + "time" ) // OrderSingleDto 订单-单项 @@ -14,7 +17,7 @@ type OrderSingleDto struct { IsDelete int `json:"is_delete"` // 用户删除状态(0:否 1:是) PayChannel int `json:"pay_channel"` // 支付渠道(1:h5支付 2:app支付 3:会员支付) PayStatus int `json:"pay_status"` // 支付状态(1:未支付 2:已支付 3:支付中 4:支付失败 5:支付超时 6:支付关闭 7:已撤销 8:转入退款) - PayTime model.LocalTime `json:"pay_time"` // 支付时间 + PayTime *time.Time `json:"pay_time"` // 支付时间 RefundStatus int `json:"refund_status"` // 订单退款状态(0:无退款 1:申请退款 2:退款中 3:退款成功 4:拒绝退款 5:退款关闭 6:退款异常 7:部分退款) OrderNo string `json:"order_no"` // 系统订单编号 EscrowTradeNo string `json:"escrow_trade_no"` // 第三方支付流水号 @@ -22,7 +25,7 @@ type OrderSingleDto struct { CouponAmountTotal float64 `json:"coupon_amount_total"` // 优惠卷总金额 PaymentAmountTotal float64 `json:"payment_amount_total"` // 实际付款金额 CancelStatus int `json:"cancel_status"` // 取消状态(0:否 1:是) - CancelTime model.LocalTime `json:"cancel_time"` // 订单取消时间 + CancelTime *time.Time `json:"cancel_time"` // 订单取消时间 CancelRemarks string `json:"cancel_remarks"` // 取消订单备注 OrderRemarks string `json:"order_remarks"` // 订单备注 CreatedAt model.LocalTime `json:"created_at"` // 创建时间 @@ -30,6 +33,19 @@ type OrderSingleDto struct { Question *QuestionDto `json:"question"` // 问题 } +// OrderSinglePayDto 单项订单支付数据 +type OrderSinglePayDto struct { + OrderId string `json:"order_id"` // 主键id + QuestionId string `json:"question_id"` // 问题id + OrderNo string `json:"order_no"` // 系统订单编号 + AmountTotal float64 `json:"amount_total"` // 订单金额 + CouponAmountTotal float64 `json:"coupon_amount_total"` // 优惠卷总金额 + PaymentAmountTotal float64 `json:"payment_amount_total"` // 实际付款金额 + CreatedAt model.LocalTime `json:"created_at"` // 创建时间 + PrepayJsapi *jsapi.PrepayWithRequestPaymentResponse `json:"prepay_jsapi"` // 预支付交易数据-jsapi + PrepayApp *app.PrepayWithRequestPaymentResponse `json:"prepay_app"` // 预支付交易数据-app +} + // GetOrderSingleListDto 列表 func GetOrderSingleListDto(m []*model.OrderSingle) []*OrderSingleDto { // 处理返回值 @@ -73,6 +89,19 @@ func GetOrderSingleListDto(m []*model.OrderSingle) []*OrderSingleDto { return responses } +// GetOrderSinglePayDto 获取单项订单支付数据 +func GetOrderSinglePayDto(m *model.OrderSingle) *OrderSinglePayDto { + return &OrderSinglePayDto{ + OrderId: fmt.Sprintf("%d", m.OrderId), + QuestionId: fmt.Sprintf("%d", m.QuestionId), + OrderNo: m.OrderNo, + AmountTotal: m.AmountTotal, + CouponAmountTotal: m.CouponAmountTotal, + PaymentAmountTotal: m.PaymentAmountTotal, + CreatedAt: m.CreatedAt, + } +} + // LoadQuestion 加载题目数据 func (r *OrderSingleDto) LoadQuestion(m *model.Question) *OrderSingleDto { if m != nil { diff --git a/api/dto/Question.go b/api/dto/Question.go index c89647e..f0fa55f 100644 --- a/api/dto/Question.go +++ b/api/dto/Question.go @@ -18,7 +18,7 @@ type QuestionDto struct { SubmitCount int `json:"submit_count"` // 提交次数(提交个人信息进行了算算的人次) PayCount int `json:"pay_count"` // 支付次数(查看报告的人次) Price float64 `json:"price"` // 价格(原价) - DiscountPrice float64 `json:"discount_price"` // 优惠价格 + DiscountPrice *float64 `json:"discount_price"` // 优惠价格 QuestionBrief string `json:"question_brief"` // 问题介绍 QuestionExplain string `json:"question_explain"` // 问题解释/科普 CreatedAt model.LocalTime `json:"created_at"` // 创建时间 diff --git a/api/model/Coupon.go b/api/model/Coupon.go index d7bb2d8..7a7075a 100644 --- a/api/model/Coupon.go +++ b/api/model/Coupon.go @@ -9,7 +9,7 @@ import ( type Coupon struct { CouponId int64 `gorm:"column:coupon_id;type:bigint(19);primary_key;comment:主键id" json:"coupon_id"` CouponName string `gorm:"column:coupon_name;type:varchar(255);comment:优惠卷名称" json:"coupon_name"` - CouponType string `gorm:"column:coupon_type;type:varchar(255);comment:优惠卷类型(1:无门槛 2:满减)" json:"coupon_type"` + CouponType int `gorm:"column:coupon_type;type:varchar(255);comment:优惠卷类型(1:无门槛 2:满减)" json:"coupon_type"` CouponStatus int `gorm:"column:coupon_status;type:tinyint(1);default:1;comment:状态(1:正常 2:强制失效 3:结束 4:删除)" json:"coupon_status"` ApplicationScope int `gorm:"column:application_scope;type:tinyint(1);default:1;comment:适用范围(1:全场通用 2:单项 3:会员)" json:"application_scope"` IsMutex int `gorm:"column:is_mutex;type:tinyint(1);default:1;comment:是否互斥(0:否 1:是)互斥情况下无法和其他优惠卷同时使用" json:"is_mutex"` diff --git a/api/model/OrderSingle.go b/api/model/OrderSingle.go index 7281b9d..b1fb048 100644 --- a/api/model/OrderSingle.go +++ b/api/model/OrderSingle.go @@ -8,24 +8,24 @@ import ( // OrderSingle 订单-单项 type OrderSingle struct { - OrderId int64 `gorm:"column:order_id;type:bigint(19);primary_key;comment:主键id" json:"order_id"` - UserId int64 `gorm:"column:user_id;type:bigint(19);comment:用户id;NOT NULL" json:"user_id"` - QuestionId int64 `gorm:"column:question_id;type:bigint(19);comment:问题id;NOT NULL" json:"question_id"` - OrderStatus int `gorm:"column:order_status;type:tinyint(1);default:1;comment:订单状态(1:待支付 2:已完成 3:已取消)" json:"order_status"` - IsDelete int `gorm:"column:is_delete;type:tinyint(1);default:0;comment:用户删除状态(0:否 1:是)" json:"is_delete"` - PayChannel int `gorm:"column:pay_channel;type:tinyint(1);comment:支付渠道(1:h5支付 2:app支付 3:会员支付);NOT NULL" json:"pay_channel"` - PayStatus int `gorm:"column:pay_status;type:tinyint(1);default:1;comment:支付状态(1:未支付 2:已支付 3:支付中 4:支付失败 5:支付超时 6:支付关闭 7:已撤销 8:转入退款);NOT NULL" json:"pay_status"` - PayTime LocalTime `gorm:"column:pay_time;type:datetime;comment:支付时间" json:"pay_time"` - RefundStatus int `gorm:"column:refund_status;type:tinyint(1);default:0;comment:订单退款状态(0:无退款 1:申请退款 2:退款中 3:退款成功 4:拒绝退款 5:退款关闭 6:退款异常 7:部分退款);NOT NULL" json:"refund_status"` - OrderNo string `gorm:"column:order_no;type:varchar(30);comment:系统订单编号;NOT NULL" json:"order_no"` - EscrowTradeNo string `gorm:"column:escrow_trade_no;type:varchar(100);comment:第三方支付流水号;NOT NULL" json:"escrow_trade_no"` - AmountTotal float64 `gorm:"column:amount_total;type:decimal(10,2) unsigned;default:0.00;comment:订单金额" json:"amount_total"` - CouponAmountTotal float64 `gorm:"column:coupon_amount_total;type:decimal(10,2);default:0.00;comment:优惠卷总金额" json:"coupon_amount_total"` - PaymentAmountTotal float64 `gorm:"column:payment_amount_total;type:decimal(10,2);default:0.00;comment:实际付款金额" json:"payment_amount_total"` - CancelStatus int `gorm:"column:cancel_status;type:tinyint(1);default:0;comment:取消状态(0:否 1:是)" json:"cancel_status"` - CancelTime LocalTime `gorm:"column:cancel_time;type:datetime;comment:订单取消时间" json:"cancel_time"` - CancelRemarks string `gorm:"column:cancel_remarks;type:varchar(255);comment:取消订单备注" json:"cancel_remarks"` - OrderRemarks string `gorm:"column:order_remarks;type:varchar(255);comment:订单备注" json:"order_remarks"` + OrderId int64 `gorm:"column:order_id;type:bigint(19);primary_key;comment:主键id" json:"order_id"` + UserId int64 `gorm:"column:user_id;type:bigint(19);comment:用户id;NOT NULL" json:"user_id"` + QuestionId int64 `gorm:"column:question_id;type:bigint(19);comment:问题id;NOT NULL" json:"question_id"` + OrderStatus int `gorm:"column:order_status;type:tinyint(1);default:1;comment:订单状态(1:待支付 2:已完成 3:已取消)" json:"order_status"` + IsDelete int `gorm:"column:is_delete;type:tinyint(1);default:0;comment:用户删除状态(0:否 1:是)" json:"is_delete"` + PayChannel int `gorm:"column:pay_channel;type:tinyint(1);comment:支付渠道(1:h5支付 2:app支付 3:会员支付);NOT NULL" json:"pay_channel"` + PayStatus int `gorm:"column:pay_status;type:tinyint(1);default:1;comment:支付状态(1:未支付 2:已支付 3:支付中 4:支付失败 5:支付超时 6:支付关闭 7:已撤销 8:转入退款);NOT NULL" json:"pay_status"` + PayTime *time.Time `gorm:"column:pay_time;type:datetime;comment:支付时间" json:"pay_time"` + RefundStatus int `gorm:"column:refund_status;type:tinyint(1);default:0;comment:订单退款状态(0:无退款 1:申请退款 2:退款中 3:退款成功 4:拒绝退款 5:退款关闭 6:退款异常 7:部分退款);NOT NULL" json:"refund_status"` + OrderNo string `gorm:"column:order_no;type:varchar(30);comment:系统订单编号;NOT NULL" json:"order_no"` + EscrowTradeNo string `gorm:"column:escrow_trade_no;type:varchar(100);comment:第三方支付流水号;NOT NULL" json:"escrow_trade_no"` + AmountTotal float64 `gorm:"column:amount_total;type:decimal(10,2) unsigned;default:0.00;comment:订单金额" json:"amount_total"` + CouponAmountTotal float64 `gorm:"column:coupon_amount_total;type:decimal(10,2);default:0.00;comment:优惠卷总金额" json:"coupon_amount_total"` + PaymentAmountTotal float64 `gorm:"column:payment_amount_total;type:decimal(10,2);default:0.00;comment:实际付款金额" json:"payment_amount_total"` + CancelStatus int `gorm:"column:cancel_status;type:tinyint(1);default:0;comment:取消状态(0:否 1:是)" json:"cancel_status"` + CancelTime *time.Time `gorm:"column:cancel_time;type:datetime;comment:订单取消时间" json:"cancel_time"` + CancelRemarks string `gorm:"column:cancel_remarks;type:varchar(255);comment:取消订单备注" json:"cancel_remarks"` + OrderRemarks string `gorm:"column:order_remarks;type:varchar(255);comment:订单备注" json:"order_remarks"` Model Question *Question `gorm:"foreignKey:QuestionId;references:question_id" json:"question"` } diff --git a/api/model/Question.go b/api/model/Question.go index 189c76e..46b0eb5 100644 --- a/api/model/Question.go +++ b/api/model/Question.go @@ -8,20 +8,20 @@ import ( // Question 问题表 type Question struct { - QuestionId int64 `gorm:"column:question_id;type:bigint(19);primary_key;comment:主键id" json:"question_id"` - QuestionTitle string `gorm:"column:question_title;type:varchar(200);comment:标题" json:"question_title"` - QuestionSubtitle string `gorm:"column:question_subtitle;type:varchar(200);comment:副标题" json:"question_subtitle"` - QuestionIden string `gorm:"column:question_iden;type:varchar(255);comment:唯一标识(用于和前端对应)" json:"question_iden"` - QuestionStatus int `gorm:"column:question_status;type:tinyint(1);default:2;comment:问题状态(1:正常 2:待发布)" json:"question_status"` - IsHide int `gorm:"column:is_hide;type:tinyint(1);default:0;comment:是否隐藏(0:否 1:是)" json:"is_hide"` - IsRecommend int `gorm:"column:is_recommend;type:tinyint(1);default:0;comment:是否推荐(0:否 1:是)" json:"is_recommend"` - ClickCount int `gorm:"column:click_count;type:int(5);default:0;comment:点击次数(点击进入详情页的人次)" json:"click_count"` - SubmitCount int `gorm:"column:submit_count;type:int(5);default:0;comment:提交次数(提交个人信息进行了算算的人次)" json:"submit_count"` - PayCount int `gorm:"column:pay_count;type:int(5);default:0;comment:支付次数(查看报告的人次)" json:"pay_count"` - Price float64 `gorm:"column:price;type:decimal(10,2) unsigned;default:0.00;comment:价格(原价)" json:"price"` - DiscountPrice float64 `gorm:"column:discount_price;type:decimal(10,2);comment:优惠价格" json:"discount_price"` - QuestionBrief string `gorm:"column:question_brief;type:text;comment:问题介绍" json:"question_brief"` - QuestionExplain string `gorm:"column:question_explain;type:text;comment:问题解释/科普" json:"question_explain"` + QuestionId int64 `gorm:"column:question_id;type:bigint(19);primary_key;comment:主键id" json:"question_id"` + QuestionTitle string `gorm:"column:question_title;type:varchar(200);comment:标题" json:"question_title"` + QuestionSubtitle string `gorm:"column:question_subtitle;type:varchar(200);comment:副标题" json:"question_subtitle"` + QuestionIden string `gorm:"column:question_iden;type:varchar(255);comment:唯一标识(用于和前端对应)" json:"question_iden"` + QuestionStatus int `gorm:"column:question_status;type:tinyint(1);default:2;comment:问题状态(1:正常 2:待发布)" json:"question_status"` + IsHide int `gorm:"column:is_hide;type:tinyint(1);default:0;comment:是否隐藏(0:否 1:是)" json:"is_hide"` + IsRecommend int `gorm:"column:is_recommend;type:tinyint(1);default:0;comment:是否推荐(0:否 1:是)" json:"is_recommend"` + ClickCount int `gorm:"column:click_count;type:int(5);default:0;comment:点击次数(点击进入详情页的人次)" json:"click_count"` + SubmitCount int `gorm:"column:submit_count;type:int(5);default:0;comment:提交次数(提交个人信息进行了算算的人次)" json:"submit_count"` + PayCount int `gorm:"column:pay_count;type:int(5);default:0;comment:支付次数(查看报告的人次)" json:"pay_count"` + Price float64 `gorm:"column:price;type:decimal(10,2) unsigned;default:0.00;comment:价格(原价)" json:"price"` + DiscountPrice *float64 `gorm:"column:discount_price;type:decimal(10,2);comment:优惠价格" json:"discount_price"` + QuestionBrief string `gorm:"column:question_brief;type:text;comment:问题介绍" json:"question_brief"` + QuestionExplain string `gorm:"column:question_explain;type:text;comment:问题解释/科普" json:"question_explain"` Model } diff --git a/api/requests/OrderSingle.go b/api/requests/OrderSingle.go index bbf580f..91745f8 100644 --- a/api/requests/OrderSingle.go +++ b/api/requests/OrderSingle.go @@ -2,6 +2,8 @@ package requests type OrderSingleRequest struct { GetOrderSinglePage // 获取单项订单列表-分页 + AddOrderSingle // 创建单项订单 + GetOrderSinglePay // 获取单项订单支付数据 } // GetOrderSinglePage 获取单项订单列表-分页 @@ -18,3 +20,15 @@ type GetOrderSinglePage struct { EscrowTradeNo string `json:"escrow_trade_no" form:"escrow_trade_no" label:"第三方支付流水号"` CancelStatus *int `json:"cancel_status" form:"cancel_status" label:"取消状态"` } + +// AddOrderSingle 创建单项订单 +type AddOrderSingle struct { + QuestionId string `json:"question_id" form:"question_id" label:"问题" validate:"required"` + UserCouponId string `json:"user_coupon_id" form:"user_coupon_id" label:"优惠卷"` + PayChannel int `json:"pay_channel" form:"pay_channel" label:"支付渠道" validate:"required,oneof=1 2 3"` // 支付渠道(1:h5支付 2:app支付 3:会员支付) +} + +// GetOrderSinglePay 获取单项订单支付数据 +type GetOrderSinglePay struct { + ClientType int `json:"client_type" form:"client_type" label:"客户端类型" validate:"required,oneof=1 2"` // 客户端类型(1:h5 2:app) +} diff --git a/api/router/router.go b/api/router/router.go index af8c6c4..143f961 100644 --- a/api/router/router.go +++ b/api/router/router.go @@ -172,6 +172,18 @@ func privateRouter(r *gin.Engine, api controller.Api) { { // 获取单项订单列表-分页 singleGroup.GET("/page", api.OrderSingle.GetOrderSinglePage) + + // 创建单项订单 + singleGroup.POST("", api.OrderSingle.AddOrderSingle) + + // 取消单项订单 + singleGroup.PUT("/cancel/:order_id", api.OrderSingle.PutCancelOrderSingle) + + // 获取单项订单支付数据 + singleGroup.GET("/pay/:order_id", api.OrderSingle.GetOrderSinglePay) + + // 删除单项订单支付数据 + singleGroup.DELETE("/:order_id", api.OrderSingle.DeleteOrderSingle) } // 会员订单 diff --git a/api/service/OrderSingle.go b/api/service/OrderSingle.go new file mode 100644 index 0000000..fd92ba5 --- /dev/null +++ b/api/service/OrderSingle.go @@ -0,0 +1,323 @@ +package service + +import ( + "context" + "errors" + "fmt" + "github.com/wechatpay-apiv3/wechatpay-go/services/payments/app" + "github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi" + "gorm.io/gorm" + "hepa-calc-api/api/dao" + "hepa-calc-api/api/model" + "hepa-calc-api/config" + "hepa-calc-api/extend/weChat" + "hepa-calc-api/global" + "hepa-calc-api/utils" + "time" +) + +type OrderSingleService struct { +} + +// AddOrderSingle 创建单项订单 +func (r *OrderSingleService) AddOrderSingle(tx *gorm.DB, UserId, QuestionId int64, UserCouponId *int64, payChannel int) (orderSingle *model.OrderSingle, err error) { + // 检测多次请求 + redisKey := "AddOrderSingle" + fmt.Sprintf("%d", UserId) + fmt.Sprintf("%d", QuestionId) + res, _ := global.Redis.Get(context.Background(), redisKey).Result() + if res != "" { + return nil, errors.New("请勿重复操作") + } + + defer func(redisKey string) { + global.Redis.Del(context.Background(), redisKey) + }(redisKey) + + // 添加缓存 + _, err = global.Redis.Set(context.Background(), redisKey, "1", (10)*time.Second).Result() + if err != nil { + return nil, errors.New("生成订单失败") + } + + // 获取题目数据 + questionDao := dao.QuestionDao{} + question, err := questionDao.GetQuestionById(QuestionId) + if err != nil { + return nil, errors.New("题目异常") + } + + // 检测题目 + questionService := &QuestionService{} + isNormal, err := questionService.CheckQuestion(question) + if err != nil || isNormal == false { + return nil, err + } + + var amountTotal float64 // 总金额 + var couponAmountTotal float64 // 优惠卷总金额 + var paymentAmountTotal float64 // 实际付款金额 + + // 获取问题价格 + amountTotal, err = questionService.GetUserBuyPrice(UserId, QuestionId) + if err != nil { + return nil, err + } + + // 检测用户优惠卷 + var userCoupon *model.UserCoupon + if UserCouponId != nil { + // 获取优惠卷数据 + UserCouponDao := dao.UserCouponDao{} + userCoupon, err = UserCouponDao.GetUserCouponPreloadById(*UserCouponId) + if err != nil { + return nil, errors.New("优惠券异常") + } + + userCouponService := &UserCouponService{} + isCanUse, err := userCouponService.CheckUserCoupon(userCoupon, QuestionId, 1, amountTotal) + if err != nil || isCanUse == false { + return nil, errors.New("价格异常") + } + + // 优惠卷总金额 + couponAmountTotal = userCoupon.Coupon.CouponPrice + } + + // 实际付款金额 + paymentAmountTotal = amountTotal - couponAmountTotal + if paymentAmountTotal < 0 { + return nil, errors.New("价格异常") + } + + // 会员支付 + if payChannel == 3 { + paymentAmountTotal = 0 + } + + // 生成订单号 + orderNo := global.Snowflake.Generate().String() + + // 创建订单 + orderSingle = &model.OrderSingle{ + UserId: UserId, + QuestionId: QuestionId, + OrderStatus: 1, + IsDelete: 0, + PayChannel: payChannel, + PayStatus: 1, + PayTime: nil, + RefundStatus: 0, + OrderNo: orderNo, + EscrowTradeNo: "", + AmountTotal: amountTotal, + CouponAmountTotal: couponAmountTotal, + PaymentAmountTotal: paymentAmountTotal, + CancelStatus: 0, + CancelTime: nil, + CancelRemarks: "", + OrderRemarks: "", + } + + orderSingleDao := dao.OrderSingleDao{} + orderSingle, err = orderSingleDao.AddOrderSingle(tx, orderSingle) + if err != nil { + return nil, errors.New("订单创建失败") + } + + // 创建优惠卷表 + if userCoupon != nil { + orderSingleCoupon := &model.OrderSingleCoupon{ + OrderId: orderSingle.OrderId, + UserCouponId: *UserCouponId, + CouponName: userCoupon.Coupon.CouponName, + CouponUsePrice: userCoupon.Coupon.CouponPrice, + } + + orderSingleCouponDao := dao.OrderSingleCouponDao{} + orderSingleCoupon, err = orderSingleCouponDao.AddOrderSingleCoupon(tx, orderSingleCoupon) + if err != nil { + tx.Rollback() + return nil, errors.New("订单创建失败") + } + + // 修改优惠卷使用状态 + userCouponDao := dao.UserCouponDao{} + + userCouponData := make(map[string]interface{}) + userCouponData["user_coupon_status"] = 1 + userCouponData["coupon_use_date"] = time.Now().Format("2006-01-02 15:04:05") + err := userCouponDao.EditUserCouponById(tx, userCoupon.UserCouponId, userCouponData) + if err != nil { + return nil, errors.New("订单创建失败") + } + } + + // 增加至未支付取消订单延迟队列 + + return orderSingle, nil +} + +// PutCancelOrderSingle 取消单项订单 +// cancelReason:订单取消原因(1:主动取消 2:后台取消 3:支付超时取消) +func (r *OrderSingleService) PutCancelOrderSingle(tx *gorm.DB, userId, orderId int64, cancelReason int) (bool, error) { + // 检测多次请求 + redisKey := "PutCancelOrderSingle" + fmt.Sprintf("%d", userId) + fmt.Sprintf("%d", orderId) + res, _ := global.Redis.Get(context.Background(), redisKey).Result() + if res != "" { + return false, errors.New("请勿重复操作") + } + + defer func(redisKey string) { + global.Redis.Del(context.Background(), redisKey) + }(redisKey) + + // 添加缓存 + _, err := global.Redis.Set(context.Background(), redisKey, "1", (10)*time.Second).Result() + if err != nil { + return false, errors.New("取消订单失败") + } + + // 获取订单数据 + orderSingleDao := dao.OrderSingleDao{} + maps := make(map[string]interface{}) + maps["user_id"] = userId + maps["order_id"] = orderId + orderSingle, err := orderSingleDao.GetOrderSingle(maps) + if err != nil { + return false, errors.New("订单异常") + } + + // 订单状态(1:待支付 2:已完成 3:已取消) + if orderSingle.OrderStatus == 2 { + return false, errors.New("订单已完成,无法取消") + } + + if orderSingle.OrderStatus == 3 { + return false, errors.New("订单已取消,请勿重复操作") + } + + // 取消状态(0:否 1:是) + if orderSingle.CancelStatus == 1 { + return false, errors.New("订单已取消") + } + + // 支付状态(1:未支付 2:已支付 3:支付中 4:支付失败 5:支付超时 6:支付关闭 7:已撤销 8:转入退款) + if orderSingle.PayStatus == 2 { + return false, errors.New("订单已支付,无法取消") + } + + // 订单退款状态(0:无退款 1:申请退款 2:退款中 3:退款成功 4:拒绝退款 5:退款关闭 6:退款异常 7:部分退款) + if orderSingle.RefundStatus == 1 { + return false, errors.New("订单已申请退款") + } + + if orderSingle.RefundStatus == 2 { + return false, errors.New("订单退款中") + } + + if orderSingle.RefundStatus == 3 { + return false, errors.New("订单已退款成功") + } + + if orderSingle.RefundStatus == 6 { + return false, errors.New("订单退款异常") + } + + // 修改订单为取消 + orderSingleData := make(map[string]interface{}) + orderSingleData["order_status"] = 3 + if cancelReason == 3 { + // 支付超时取消 + orderSingleData["pay_status"] = 5 + } + orderSingleData["cancel_status"] = 1 + orderSingleData["cancel_time"] = time.Now().Format("2006-01-02 15:04:05") + orderSingleData["cancel_remarks"] = utils.OrderCancelReasonToString(cancelReason) + orderSingleData["updated_at"] = time.Now().Format("2006-01-02 15:04:05") + err = orderSingleDao.EditOrderSingleById(tx, orderId, orderSingleData) + if err != nil { + return false, errors.New("订单取消失败") + } + + // 退还订单优惠卷 + if orderSingle.CouponAmountTotal != 0 { + // 获取订单优惠卷数据 + orderSingleCouponDao := dao.OrderSingleCouponDao{} + orderSingleCoupon, err := orderSingleCouponDao.GetOrderSingleCouponByOrderId(orderId) + if err != nil { + tx.Rollback() + return false, errors.New("订单取消失败") + } + + userCouponService := &UserCouponService{} + userCouponService.ReturnUserCoupon(tx, orderSingleCoupon.UserCouponId) + } + + return true, nil +} + +// GetJsapiPrepay 获取jsapi预支付交易会话标识 +func (r *OrderSingleService) GetJsapiPrepay(m *model.OrderSingle) (prepay *jsapi.PrepayWithRequestPaymentResponse, err error) { + // 获取用户数据 + userDao := dao.UserDao{} + user, err := userDao.GetUserById(m.UserId) + if err != nil || user == nil { + return nil, errors.New("用户错误") + } + + if user.OpenId != "" { + return nil, errors.New("发起支付失败") + } + + jsapiRequest := weChat.JsapiRequest{ + AppId: config.C.Wechat.AppId, + MchId: config.C.Wechat.Pay1659662936.MchId, + Description: "肝病算一算", + OutTradeNo: m.OrderNo, + NotifyUrl: config.C.Wechat.RefundNotifyDomain + config.C.Wechat.RefundNotifyUrl, + Amount: weChat.JsapiRequestAmountRequest{ + Total: int64(m.PaymentAmountTotal * 100), + Currency: "CNY", + }, + Payer: weChat.JsapiRequestPayerRequest{OpenId: user.OpenId}, + } + + prepay, err = jsapiRequest.GetJsapiPrepay() + if err != nil { + return nil, err + } + + return prepay, nil +} + +// GetAppPrepay 获取app预支付交易会话标识 +func (r *OrderSingleService) GetAppPrepay(m *model.OrderSingle) (prepay *app.PrepayWithRequestPaymentResponse, err error) { + // 获取用户数据 + userDao := dao.UserDao{} + user, err := userDao.GetUserById(m.UserId) + if err != nil || user == nil { + return nil, errors.New("用户错误") + } + + if user.OpenId != "" { + return nil, errors.New("发起支付失败") + } + + appRequest := weChat.AppRequest{ + AppId: config.C.Wechat.AppId, + MchId: config.C.Wechat.Pay1659662936.MchId, + Description: "肝病算一算", + OutTradeNo: m.OrderNo, + NotifyUrl: config.C.Wechat.RefundNotifyDomain + config.C.Wechat.RefundNotifyUrl, + Amount: weChat.AppRequestAmountRequest{ + Total: int64(m.PaymentAmountTotal * 100), + Currency: "CNY", + }, + } + + prepay, err = appRequest.GetAppPrepay() + if err != nil { + return nil, err + } + + return prepay, nil +} diff --git a/api/service/Question.go b/api/service/Question.go index fca811e..0866b77 100644 --- a/api/service/Question.go +++ b/api/service/Question.go @@ -1,6 +1,7 @@ package service import ( + "errors" "hepa-calc-api/api/dao" "hepa-calc-api/api/model" ) @@ -139,3 +140,50 @@ func (r *QuestionService) GetQuestionBuyCount(userId, questionId int64) (c int, return int(buyCount), nil } + +// GetUserBuyPrice 获取问题价格 +func (r *QuestionService) GetUserBuyPrice(userId, questionId int64) (p float64, err error) { + // 获取问题详情 + questionDao := dao.QuestionDao{} + question, err := questionDao.GetQuestionById(questionId) + if err != nil { + return 0, errors.New("题目异常") + } + + // 问题价格 + p = question.Price + + if question.DiscountPrice != nil { + p = *question.DiscountPrice + } + + // 检测用户是否购买过该问题 + isFirstBuy := r.CheckUserBuyQuestion(userId, questionId) + if isFirstBuy == false { + // 未购买过 + systemSingleDao := dao.SystemSingleDao{} + + maps := make(map[string]interface{}) + systemSingle, err := systemSingleDao.GetSystemSingle(maps) + if err != nil { + return 0, err + } + + p = systemSingle.FirstTimePrice + } + + return p, nil +} + +// CheckQuestion 检测题目 +func (r *QuestionService) CheckQuestion(m *model.Question) (bool, error) { + if m.QuestionStatus != 1 { + return false, errors.New("题目异常") + } + + if m.IsHide != 0 { + return false, errors.New("题目异常") + } + + return true, nil +} diff --git a/api/service/User.go b/api/service/User.go index 60fa32f..c6734c2 100644 --- a/api/service/User.go +++ b/api/service/User.go @@ -1,10 +1,12 @@ package service import ( + "errors" "fmt" "hepa-calc-api/extend/aliyun" - "hepa-calc-api/utils" + "io" "math/rand" + "net/http" "time" ) @@ -17,8 +19,22 @@ func (r *UserService) HandleUserAvatar(wxAvatar string) (avatar string, err erro return "", nil } - // 下载文件到内存 - ram, err := aliyun.GetObjectToRAM(wxAvatar) + // 发送GET请求 + resp, err := http.Get(wxAvatar) + if err != nil { + return "", err + } + + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(resp.Body) + + if resp.StatusCode != 200 { + return "", errors.New("请求失败") + } + + // 读取响应体 + respBody, err := io.ReadAll(resp.Body) if err != nil { return "", err } @@ -27,15 +43,15 @@ func (r *UserService) HandleUserAvatar(wxAvatar string) (avatar string, err erro now := time.Now() dateTimeString := now.Format("20060102150405") // 当前时间字符串 rand.New(rand.NewSource(time.Now().UnixNano())) // 设置随机数 - ossPath := "/test/医生账户" + dateTimeString + fmt.Sprintf("%d", rand.Intn(9000)+1000) + ".png" + ossPath := "user/avatar/" + dateTimeString + fmt.Sprintf("%d", rand.Intn(9000)+1000) + ".png" // 上传oss - _, err = aliyun.PutObjectByte(ossPath, []byte(ram)) + _, err = aliyun.PutObjectByte(ossPath, respBody) if err != nil { return "", err } - ossPath = utils.AddOssDomain("/" + ossPath) + ossPath = "/" + ossPath return ossPath, nil } diff --git a/api/service/UserCoupon.go b/api/service/UserCoupon.go new file mode 100644 index 0000000..b4c3e53 --- /dev/null +++ b/api/service/UserCoupon.go @@ -0,0 +1,116 @@ +package service + +import ( + "errors" + "gorm.io/gorm" + "hepa-calc-api/api/dao" + "hepa-calc-api/api/model" + "time" +) + +type UserCouponService struct { +} + +// CheckUserCoupon 检测用户优惠卷 +func (r *UserCouponService) CheckUserCoupon(m *model.UserCoupon, id int64, orderType int, amountTotal float64) (bool, error) { + if m.UserCouponStatus == 1 { + return false, errors.New("优惠卷异常") + } + + if m.UserCouponStatus == 2 { + return false, errors.New("优惠卷已过期,无法使用") + } + + now := time.Now() + validEndTime := time.Time(m.ValidEndTime) + if validEndTime.Before(now) { + return false, errors.New("优惠卷已过期,无法使用") + } + + if m.Coupon == nil { + return false, errors.New("优惠卷异常") + } + + // 检测优惠卷状态 + if m.Coupon.CouponStatus == 2 { + return false, errors.New("优惠卷已失效,无法使用") + } + + if m.Coupon.CouponStatus == 3 { + return false, errors.New("优惠卷无法使用") + } + + if m.Coupon.CouponStatus == 4 { + return false, errors.New("优惠卷异常,无法使用") + } + + // 检测价格 + if m.Coupon.CouponType == 2 { + if m.Coupon.WithAmount > amountTotal { + return false, errors.New("优惠卷不符合满减金额标准,无法使用") + } + } + + // 单项 + if orderType == 1 { + if m.Coupon.ApplicationScope != 1 && m.Coupon.ApplicationScope != 2 { + return false, errors.New("优惠卷无法使用") + } + + if id != m.Coupon.QuestionId { + return false, errors.New("优惠卷无法使用") + } + } + + // 会员 + if orderType == 2 { + if m.Coupon.ApplicationScope != 1 && m.Coupon.ApplicationScope != 3 { + return false, errors.New("优惠卷无法使用") + } + + if id != m.Coupon.SystemMemberId { + return false, errors.New("优惠卷无法使用") + } + } + + // 检测优惠劵过期时间 + if m.Coupon.ValidType == 1 { + validEndTime = time.Time(m.Coupon.ValidEndTime) + if validEndTime.Before(now) { + return false, errors.New("优惠卷已过期,无法使用") + } + } + + return true, nil +} + +// ReturnUserCoupon 退还优惠卷 +func (r *UserCouponService) ReturnUserCoupon(tx *gorm.DB, userCouponId int64) bool { + // 获取优惠卷数据 + UserCouponDao := dao.UserCouponDao{} + userCoupon, err := UserCouponDao.GetUserCouponPreloadById(userCouponId) + if err != nil { + // 无该优惠卷数据,无需处理 + return true + } + + userCouponDao := dao.UserCouponDao{} + userCouponData := make(map[string]interface{}) + + // 检测优惠卷过期时间。判断是否需要退还 + now := time.Now() + validEndTime := time.Time(userCoupon.ValidEndTime) + if validEndTime.Before(now) { + userCouponData["user_coupon_status"] = 3 + } else { + userCouponData["user_coupon_status"] = 0 + } + + userCouponData["coupon_use_date"] = nil + err = userCouponDao.EditUserCouponById(tx, userCoupon.UserCouponId, userCouponData) + if err != nil { + return false + } + + return true +} diff --git a/config.yaml b/config.yaml index f06a4c8..be92a9b 100644 --- a/config.yaml +++ b/config.yaml @@ -37,7 +37,7 @@ oss: oss-access-key: LTAI5tKmFrVCghcxX7yHyGhm oss-access-key-secret: q1aiIZCJJuf92YbKk2cSXnPES4zx26 oss-bucket: dev-knowledge - oss-endpoint: oss-cn-chengdu.aliyuncs.com + oss-endpoint: oss-cn-beijing.aliyuncs.com oss-custom-domain-name: https://dev-knowledge.oss-cn-beijing.aliyuncs.com # [阿里大鱼短信] @@ -45,17 +45,17 @@ dysms: dysms-access-key: LTAI4GGygjsKhyBwvvC3CghV dysms-access-secret: rcx7lO9kQxG10m8NqNPEfEtT9IS8EI -# [微信] +# [微信]v wechat: app-id: wxc8ac5051745bc795 app-secret: 678b63a8a7541e528abc3040c3cea809 pay-notify-url: callback/wxpay/inquiry/success refund-notify-url: callback/wxpay/inquiry/refund refund-notify-domain: https://dev.hospital.applets.igandanyiyuan.com/ - pay-1636644248: - mch-id: 1636644248 - v3-api-secret: gdxz292sjSOadN3m2pCda03NfCsmNadY - mch-certificate-serial-number: 7DEC0E6C57E0DC71F077F02F52406566AF39BEBB - platform-certs: extend/weChat/certs/1659662936/wechatpay_112FCCD1B9ECC8292703AB7363C73D74B6AFDC1A.pem - private-key: extend/weChat/certs/1636644248/apiclient_key.pem - certificate: extend/weChat/certs/1636644248/apiclient_cert.pem \ No newline at end of file + pay-1659662936: + mch-id: 1659662936 + v3-api-secret: gdxz292sjSOadNNad2pCda03NfC2msmY + mch-certificate-serial-number: 12FAA5F61708B795BB5337AE915494E2DC2CA87B + platform-certs: extend/weChat/certs/1659662936/wechatpay_5B5C8A69CC86D1127F6B6AA06AAAF10531EEFE90.pem + private-key: extend/weChat/certs/1659662936/apiclient_key.pem + certificate: extend/weChat/certs/1659662936/apiclient_cert.pem \ No newline at end of file diff --git a/config/wechat.go b/config/wechat.go index 70a4afd..f13960c 100644 --- a/config/wechat.go +++ b/config/wechat.go @@ -1,15 +1,16 @@ package config type Wechat struct { - AppId string `mapstructure:"app-id" json:"app-id" yaml:"patient-app-id"` - AppSecret string `mapstructure:"app-secret" json:"app-secret" yaml:"app-secret"` - PayNotifyUrl string `mapstructure:"pay-notify-url" json:"pay-notify-url" yaml:"pay-notify-url"` - RefundNotifyUrl string `mapstructure:"refund-notify-url" json:"refund-notify-url" yaml:"refund-notify-url"` - Pay1636644248 Pay1636644248 `mapstructure:"pay-1636644248" json:"pay-1636644248" yaml:"pay-1636644248"` + AppId string `mapstructure:"app-id" json:"app-id" yaml:"patient-app-id"` + AppSecret string `mapstructure:"app-secret" json:"app-secret" yaml:"app-secret"` + PayNotifyUrl string `mapstructure:"pay-notify-url" json:"pay-notify-url" yaml:"pay-notify-url"` + RefundNotifyDomain string `mapstructure:"refund-notify-domain" json:"refund-notify-domain" yaml:"refund-notify-domain"` // 回调域名 + RefundNotifyUrl string `mapstructure:"refund-notify-url" json:"refund-notify-url" yaml:"refund-notify-url"` + Pay1659662936 Pay1659662936 `mapstructure:"pay-1659662936" json:"pay-1659662936" yaml:"pay-1659662936"` } -// Pay1636644248 北京欣欣相照 -type Pay1636644248 struct { +// Pay1659662936 成都欣欣相照 +type Pay1659662936 struct { MchId string `mapstructure:"mch-id" json:"mch-id" yaml:"mch-id"` // 商户号 V3ApiSecret string `mapstructure:"v3-api-secret" json:"v3-api-secret" yaml:"v3-api-secret"` // 商户APIv3密钥 MchCertificateSerialNumber string `mapstructure:"mch-certificate-serial-number" json:"mch-certificate-serial-number" yaml:"mch-certificate-serial-number"` // 商户证书序列号 diff --git a/extend/weChat/base.go b/extend/weChat/base.go new file mode 100644 index 0000000..42daa77 --- /dev/null +++ b/extend/weChat/base.go @@ -0,0 +1,43 @@ +package weChat + +import ( + "context" + "errors" + "github.com/wechatpay-apiv3/wechatpay-go/core" + "github.com/wechatpay-apiv3/wechatpay-go/core/option" + "github.com/wechatpay-apiv3/wechatpay-go/utils" + "hepa-calc-api/config" + "path/filepath" +) + +// 创建客户端 +func createClient() (*core.Client, error) { + mchId := config.C.Wechat.Pay1659662936.MchId // 商户号 + mchCertificateSerialNumber := config.C.Wechat.Pay1659662936.MchCertificateSerialNumber // 商户证书序列号 + v3ApiSecret := config.C.Wechat.Pay1659662936.V3ApiSecret // 商户APIv3密钥 + + if mchId == "" { + return nil, errors.New("商户号错误") + } + + // 使用 utils 提供的函数从本地文件中加载商户私钥,商户私钥会用来生成请求的签名 + certsDir := filepath.Join("extend/weChat/certs", mchId, "/apiclient_key.pem") + // certsDir := filepath.Join("extend/weChat/certs", "/1636644248/apiclient_key.pem") + mchPrivateKey, err := utils.LoadPrivateKeyWithPath(certsDir) + if err != nil { + return nil, errors.New("微信签名生成失败") + } + + ctx := context.Background() + // 使用商户私钥等初始化 client,并使它具有自动定时获取微信支付平台证书的能力 + opts := []core.ClientOption{ + option.WithWechatPayAutoAuthCipher(mchId, mchCertificateSerialNumber, mchPrivateKey, v3ApiSecret), + } + + client, err := core.NewClient(ctx, opts...) + if err != nil { + return nil, errors.New(err.Error()) + } + + return client, nil +} diff --git a/extend/weChat/prepay.go b/extend/weChat/prepay.go new file mode 100644 index 0000000..cba522b --- /dev/null +++ b/extend/weChat/prepay.go @@ -0,0 +1,133 @@ +package weChat + +import ( + "context" + "errors" + "github.com/wechatpay-apiv3/wechatpay-go/core" + "github.com/wechatpay-apiv3/wechatpay-go/services/payments/app" + "github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi" +) + +/** +JSAPI下单 +APP下单 +*/ + +// JsapiRequest 请求数据-JSAPI下单 +type JsapiRequest struct { + AppId string `json:"appid" comment:"公众号ID"` + MchId string `json:"mchid" comment:"直连商户号"` + Description string `json:"description" comment:"商品描述"` + OutTradeNo string `json:"out_trade_no" comment:"商户订单号"` + NotifyUrl string `json:"notify_url" comment:"回调地址"` + Amount JsapiRequestAmountRequest `json:"amount" comment:"订单金额"` + Payer JsapiRequestPayerRequest `json:"payer" comment:"支付者"` +} + +// JsapiRequestAmountRequest 订单金额信息 +type JsapiRequestAmountRequest struct { + Total int64 `json:"total" comment:"订单总金额"` + Currency string `json:"currency" comment:"货币类型"` +} + +// JsapiRequestPayerRequest 支付者信息 +type JsapiRequestPayerRequest struct { + OpenId string `json:"openid" comment:"openid"` +} + +// AppRequest 请求数据-APP下单 +type AppRequest struct { + AppId string `json:"appid" comment:"公众号ID"` + MchId string `json:"mchid" comment:"直连商户号"` + Description string `json:"description" comment:"商品描述"` + OutTradeNo string `json:"out_trade_no" comment:"商户订单号"` + NotifyUrl string `json:"notify_url" comment:"回调地址"` + Amount AppRequestAmountRequest `json:"amount" comment:"订单金额"` +} + +// AppRequestAmountRequest 订单金额信息 +type AppRequestAmountRequest struct { + Total int64 `json:"total" comment:"订单总金额"` + Currency string `json:"currency" comment:"货币类型"` +} + +// GetJsapiPrepay JSAPI下单-获取jsapi预支付交易会话 +func (r JsapiRequest) GetJsapiPrepay() (prepay *jsapi.PrepayWithRequestPaymentResponse, err error) { + client, err := createClient() + if err != nil { + return nil, err + } + + svc := jsapi.JsapiApiService{Client: client} + // 得到prepay_id,以及调起支付所需的参数和签名 + + resp, result, err := svc.PrepayWithRequestPayment(context.Background(), + jsapi.PrepayRequest{ + Appid: core.String(r.AppId), + Mchid: core.String(r.MchId), + Description: core.String(r.Description), + OutTradeNo: core.String(r.OutTradeNo), + NotifyUrl: core.String(r.NotifyUrl), + Amount: &jsapi.Amount{ + Total: core.Int64(r.Amount.Total), + Currency: core.String("CNY"), + }, + Payer: &jsapi.Payer{ + Openid: core.String(r.Payer.OpenId), + }, + }, + ) + + if err != nil { + return nil, err + } + + if result.Response.StatusCode != 200 { + return nil, errors.New("发起支付失败") + } + + if resp.PrepayId == nil { + return nil, errors.New("发起支付失败") + } + + return resp, nil +} + +// GetAppPrepay APP下单-获取app预支付交易会话 +func (r AppRequest) GetAppPrepay() (prepay *app.PrepayWithRequestPaymentResponse, err error) { + client, err := createClient() + if err != nil { + return nil, err + } + + svc := app.AppApiService{Client: client} + // 得到prepay_id,以及调起支付所需的参数和签名 + + resp, result, err := svc.PrepayWithRequestPayment(context.Background(), + app.PrepayRequest{ + Appid: core.String(r.AppId), + Mchid: core.String(r.MchId), + Description: core.String(r.Description), + OutTradeNo: core.String(r.OutTradeNo), + NotifyUrl: core.String(r.NotifyUrl), + Amount: &app.Amount{ + Total: core.Int64(r.Amount.Total), + Currency: core.String("CNY"), + }, + }, + ) + + if err != nil { + return nil, err + } + + if result.Response.StatusCode != 200 { + return nil, errors.New("发起支付失败") + } + + if resp.PrepayId == nil { + return nil, errors.New("发起支付失败") + } + + return resp, nil +} diff --git a/extend/weChat/refund.go b/extend/weChat/refund.go new file mode 100644 index 0000000..86c1a02 --- /dev/null +++ b/extend/weChat/refund.go @@ -0,0 +1,46 @@ +package weChat + +import ( + "context" + "github.com/wechatpay-apiv3/wechatpay-go/core" + "github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic" +) + +type RefundRequest struct { + TransactionId string `json:"transaction_id" comment:"微信订单号"` + OutTradeNo string `json:"out_trade_no" comment:"商户订单号"` + OutRefundNo string `json:"out_refund_no" comment:"退款订单号"` + Reason string `json:"reason" comment:"退款原因"` + RefundAmount int64 `json:"refund_amount" comment:"退款金额"` + PaymentAmountTotal int64 `json:"payment_amount_total" comment:"支付金额"` + NotifyUrl string `json:"notify_url" comment:"回调地址"` +} + +// Refund 退款 +func (r RefundRequest) Refund() (*refunddomestic.Refund, error) { + client, err := createClient() + if err != nil { + return nil, err + } + + refundRequest := refunddomestic.CreateRequest{ + TransactionId: core.String(r.TransactionId), + OutTradeNo: core.String(r.OutTradeNo), + OutRefundNo: core.String(r.OutRefundNo), + Reason: core.String(r.Reason), + NotifyUrl: core.String(r.NotifyUrl), + Amount: &refunddomestic.AmountReq{ + Currency: core.String("CNY"), + Refund: core.Int64(r.RefundAmount), + Total: core.Int64(r.PaymentAmountTotal), + }, + } + + svc := refunddomestic.RefundsApiService{Client: client} + resp, _, err := svc.Create(context.Background(), refundRequest) + if err != nil { + return nil, err + } + + return resp, nil +} diff --git a/extend/weChat/weChatPay.go b/extend/weChat/weChatPay.go deleted file mode 100644 index 50fe0c2..0000000 --- a/extend/weChat/weChatPay.go +++ /dev/null @@ -1,79 +0,0 @@ -package weChat - -import ( - "context" - "errors" - "github.com/wechatpay-apiv3/wechatpay-go/core" - "github.com/wechatpay-apiv3/wechatpay-go/core/option" - "github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic" - "github.com/wechatpay-apiv3/wechatpay-go/utils" - "hepa-calc-api/config" - "path/filepath" -) - -type RefundRequest struct { - TransactionId string `json:"transaction_id" comment:"微信订单号"` - OutTradeNo string `json:"out_trade_no" comment:"商户订单号"` - OutRefundNo string `json:"out_refund_no" comment:"退款订单号"` - Reason string `json:"reason" comment:"退款原因"` - RefundAmount int64 `json:"refund_amount" comment:"退款金额"` - PaymentAmountTotal int64 `json:"payment_amount_total" comment:"支付金额"` - NotifyUrl string `json:"notify_url" comment:"回调地址"` -} - -// Refund 退款 -func (r RefundRequest) Refund() (*refunddomestic.Refund, error) { - mchId := "" - mchCertificateSerialNumber := "" - v3ApiSecret := "" - - mchId = config.C.Wechat.Pay1636644248.MchId - mchCertificateSerialNumber = config.C.Wechat.Pay1636644248.MchCertificateSerialNumber - v3ApiSecret = config.C.Wechat.Pay1636644248.V3ApiSecret - - if mchId == "" { - return nil, errors.New("商户号错误") - } - - // certsDir := "extend/weChat/certs/" + mchId + "/apiclient_key.pem" - - // 使用 utils 提供的函数从本地文件中加载商户私钥,商户私钥会用来生成请求的签名 - certsDir := filepath.Join("extend/weChat/certs", mchId, "/apiclient_key.pem") - // certsDir := filepath.Join("extend/weChat/certs", "/1636644248/apiclient_key.pem") - mchPrivateKey, err := utils.LoadPrivateKeyWithPath(certsDir) - if err != nil { - return nil, errors.New("微信签名生成失败") - } - - ctx := context.Background() - // 使用商户私钥等初始化 client,并使它具有自动定时获取微信支付平台证书的能力 - opts := []core.ClientOption{ - option.WithWechatPayAutoAuthCipher(mchId, mchCertificateSerialNumber, mchPrivateKey, v3ApiSecret), - } - - client, err := core.NewClient(ctx, opts...) - if err != nil { - return nil, errors.New(err.Error()) - } - - refundRequest := refunddomestic.CreateRequest{ - TransactionId: core.String(r.TransactionId), - OutTradeNo: core.String(r.OutTradeNo), - OutRefundNo: core.String(r.OutRefundNo), - Reason: core.String(r.Reason), - NotifyUrl: core.String(r.NotifyUrl), - Amount: &refunddomestic.AmountReq{ - Currency: core.String("CNY"), - Refund: core.Int64(r.RefundAmount), - Total: core.Int64(r.PaymentAmountTotal), - }, - } - - svc := refunddomestic.RefundsApiService{Client: client} - resp, _, err := svc.Create(ctx, refundRequest) - if err != nil { - return nil, errors.New(err.Error()) - } - - return resp, nil -} diff --git a/utils/intToString.go b/utils/intToString.go index 8c387d1..11a3698 100644 --- a/utils/intToString.go +++ b/utils/intToString.go @@ -1,3 +1,15 @@ package utils -// int转字符串 +// OrderCancelReasonToString 订单取消原因(1:主动取消 2:后台取消 3:支付超时取消) +func OrderCancelReasonToString(i int) string { + switch i { + case 1: + return "主动取消" + case 2: + return "后台取消" + case 3: + return "支付超时取消" + default: + return "取消" + } +}