From b81dee99ddc986020c21a958c1580f01c4166e98 Mon Sep 17 00:00:00 2001 From: zchengo <1933757688@qq.com> Date: Fri, 16 Dec 2022 20:39:36 +0800 Subject: [PATCH] feat: subscribe and alipay --- server/api/subscribe.go | 63 +++++++++++++ server/initialize/router.go | 10 +- server/models/subscribe.go | 29 ++++++ server/response/errcode.go | 2 + server/service/subscribe.go | 180 ++++++++++++++++++++++++++++++++++++ web/src/api/subscribe.js | 20 ++++ web/src/views/Dashboard.vue | 12 +-- web/src/views/Subscribe.vue | 178 +++++++++++++++++++++++++++-------- 8 files changed, 445 insertions(+), 49 deletions(-) create mode 100644 server/api/subscribe.go create mode 100644 server/models/subscribe.go create mode 100644 server/service/subscribe.go create mode 100644 web/src/api/subscribe.js diff --git a/server/api/subscribe.go b/server/api/subscribe.go new file mode 100644 index 0000000..0398442 --- /dev/null +++ b/server/api/subscribe.go @@ -0,0 +1,63 @@ +package api + +import ( + "crm/models" + "crm/response" + "crm/service" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" +) + +type SubscribeApi struct { + subscribeService *service.SubscribeService +} + +func NewSubscribeApi() *SubscribeApi { + subscribeApi := SubscribeApi{ + subscribeService: &service.SubscribeService{}, + } + return &subscribeApi +} + +// 订阅专业版,发起支付 +func (s *SubscribeApi) Pay(context *gin.Context) { + var param models.SubscribePayParam + uid, _ := strconv.Atoi(context.Request.Header.Get("uid")) + err := context.ShouldBind(¶m); + if int64(uid) <= 0 || err != nil { + response.Result(response.ErrCodeParamInvalid, nil, context) + return + } + param.Uid = int64(uid) + payUrl, errCode := s.subscribeService.Pay(param) + response.Result(errCode, payUrl, context) +} + +// 支付成功回调 +func (s *SubscribeApi) Callback(context *gin.Context) { + context.Request.ParseForm() + var outTradeNo = context.Request.Form.Get("out_trade_no") + paySuccessURL, _ := s.subscribeService.Callback(outTradeNo) + context.Redirect(http.StatusMovedPermanently, paySuccessURL) +} + +// 支付通知 +func (s *SubscribeApi) Notify(context *gin.Context) { + context.Request.ParseForm() + var outTradeNo = context.Request.Form.Get("out_trade_no") + errCode := s.subscribeService.Notify(context.Request.Form, outTradeNo) + response.Result(errCode, nil, context) +} + +// 获取订阅信息 +func (s *SubscribeApi) GetInfo(context *gin.Context) { + uid, _ := strconv.Atoi(context.Request.Header.Get("uid")) + if int64(uid) <= 0 { + response.Result(response.ErrCodeParamInvalid, nil, context) + return + } + subscribeInfo, errCode := s.subscribeService.GetInfo(int64(uid)) + response.Result(errCode, subscribeInfo, context) +} \ No newline at end of file diff --git a/server/initialize/router.go b/server/initialize/router.go index 7b2a60c..54165f5 100644 --- a/server/initialize/router.go +++ b/server/initialize/router.go @@ -18,16 +18,16 @@ func Router() { route := engine.Group("/api") { - // 用户模块 + // 用户模块,订阅模块 route.GET("/user/verifycode", api.NewUserApi().GetVerifyCode) route.GET("/user/info", api.NewUserApi().GetInfo) route.PUT("/user/mail", api.NewUserApi().UpdateMail) - route.PUT("/user/buy", api.NewUserApi().Buy) route.POST("/user/login", api.NewUserApi().Login) route.POST("/user/register", api.NewUserApi().Register) route.POST("/user/pass", api.NewUserApi().ForgotPass) route.DELETE("/user/logout", api.NewUserApi().Logout) route.DELETE("/user/delete", api.NewUserApi().Delete) + route.GET("/subscribe/callback", api.NewSubscribeApi().Callback) // Jwt中间件 route.Use(middleware.JwtAuth()) @@ -57,7 +57,11 @@ func Router() { // 仪表盘模块 route.GET("/dashboard/sum", api.NewDashboardApi().Summary) - + + // 订阅模块 + route.GET("/subscribe/info", api.NewSubscribeApi().GetInfo) + route.POST("/subscribe/pay", api.NewSubscribeApi().Pay) + route.POST("/subscribe/notify", api.NewSubscribeApi().Notify) } // 启动、监听端口 diff --git a/server/models/subscribe.go b/server/models/subscribe.go new file mode 100644 index 0000000..6f2c29b --- /dev/null +++ b/server/models/subscribe.go @@ -0,0 +1,29 @@ +package models + +type Subscribe struct { + Id int64 `gorm:"primaryKey"` + Uid int64 `gorm:"uid"` + Version int `gorm:"version"` + Expired int64 `gorm:"expired"` + Created int64 `gorm:"created"` + Updated int64 `gorm:"updated"` +} + +type SubscribePayParam struct { + Uid int64 `json:"uid" binding:"-"` + Version int `json:"version" binding:"required,oneof=2 3"` +} + +type SubscribePayOrder struct { + Uid int64 `json:"uid"` + Version int `json:"version"` +} + +type SubscribePayUrl struct { + PayUrl string `json:"payUrl"` +} + +type SubscribeInfo struct { + Version int `json:"version"` + Expired int64 `json:"expired"` +} diff --git a/server/response/errcode.go b/server/response/errcode.go index b503274..8e5bd52 100644 --- a/server/response/errcode.go +++ b/server/response/errcode.go @@ -16,6 +16,8 @@ const ( ErrCodeCompanyIdNotExist = 10007 // 企业编号不存在 ErrCodeEmailFormatInvalid = 10008 // 邮箱格式无效 ErrCodeUserPassResetFailed = 10009 // 用户密码重置失败 + + ErrCodePayFailed = 20001 // 支付宝支付失败 ) var msg = map[int]string{ diff --git a/server/service/subscribe.go b/server/service/subscribe.go new file mode 100644 index 0000000..e7947bf --- /dev/null +++ b/server/service/subscribe.go @@ -0,0 +1,180 @@ +package service + +import ( + "crm/global" + "crm/models" + "crm/response" + "encoding/json" + "fmt" + "log" + "net/url" + "time" + + "github.com/smartwalle/alipay/v3" + "github.com/smartwalle/xid" +) + +const ( + Paying = 1 // 支付中 + Payed = 2 // 已支付 +) + +type SubscribeService struct { +} + +// 订阅专业版,发起支付 +func (s *SubscribeService) Pay(param models.SubscribePayParam) (*models.SubscribePayUrl, int) { + + // 构建订单支付信息 + tradeNo := fmt.Sprintf("%d", xid.Next()) + var p = alipay.TradePagePay{} + p.NotifyURL = global.Config.Alipay.NotifyURL + p.ReturnURL = global.Config.Alipay.ReturnURL + fmt.Println(p.ReturnURL) + p.Subject = "支付测试:" + tradeNo + p.OutTradeNo = tradeNo + p.ProductCode = "FAST_INSTANT_TRADE_PAY" + switch param.Version { + case 2: + p.TotalAmount = "18.00" + case 3: + p.TotalAmount = "198.00" + } + + // 缓存订单信息 + order, _ := json.Marshal(&models.SubscribePayOrder{ + Uid: param.Uid, + Version: param.Version, + }) + err := global.Rdb.Set(ctx, tradeNo, string(order), time.Minute*30).Err() + if err != nil { + return nil, response.ErrCodeFailed + } + + // 设置支付状态 + key := fmt.Sprintf("uid:%v:pay:status", param.Uid) + if err := global.Rdb.Set(ctx, key, Paying, time.Minute*10).Err(); err != nil { + return nil, response.ErrCodeFailed + } + + // 返回支付链接 + payUrl, err := global.Alipay.TradePagePay(p) + if err != nil { + return nil, response.ErrCodeFailed + } + subscribePayUrl := models.SubscribePayUrl{ + PayUrl: payUrl.String(), + } + return &subscribePayUrl, response.ErrCodeSuccess +} + +// 支付成功回调 +func (s *SubscribeService) Callback(outTradeNo string) (string, int) { + + // 验证订单信息 + var p = alipay.TradeQuery{} + p.OutTradeNo = outTradeNo + rsp, err := global.Alipay.TradeQuery(p) + if err != nil { + log.Printf("验证订单 %s 信息发生错误: %s", outTradeNo, err.Error()) + return StringNull, response.ErrCodeFailed + } + if !rsp.IsSuccess() { + log.Printf("验证订单 %s 信息发生错误: %s-%s", outTradeNo, rsp.Content.Msg, rsp.Content.SubMsg) + return StringNull, response.ErrCodeFailed + } + + // 获取订单信息 + var order models.SubscribePayOrder + orderJson, err := global.Rdb.Get(ctx, outTradeNo).Result() + if err != nil { + return StringNull, response.ErrCodeFailed + } + if err := json.Unmarshal([]byte(orderJson), &order); err != nil { + return StringNull, response.ErrCodeFailed + } + + // 创建订阅信息 + var expired int64 + switch order.Version { + case 2: + expired = time.Now().Unix() + int64(2592000) + case 3: + expired = time.Now().Unix() + int64(31104000) + } + subscribe := models.Subscribe{ + Version: order.Version, + Expired: expired, + } + if global.Db.Table(SUBSCRIBE).Where("uid = ?", order.Uid).First(&models.Subscribe{}).RowsAffected == 0 { + subscribe.Uid = order.Uid + subscribe.Created = time.Now().Unix() + err := global.Db.Table(SUBSCRIBE).Create(&subscribe).Error + if err != nil { + return StringNull, response.ErrCodeFailed + } + } else { + subscribe.Updated = time.Now().Unix() + err = global.Db.Model(&models.Subscribe{}).Where("uid = ?", order.Uid).Updates(&subscribe).Error + if err != nil { + return StringNull, response.ErrCodeFailed + } + } + + // 修改支付状态 + key := fmt.Sprintf("uid:%v:pay:status", order.Uid) + if err := global.Rdb.Set(ctx, key, Payed, time.Hour*10).Err(); err != nil { + return StringNull, response.ErrCodeFailed + } + return global.Config.Alipay.PaySuccessURL, response.ErrCodeSuccess +} + +// 异步验签 +func (s *SubscribeService) Notify(data url.Values, outTradeNo string) int { + ok, err := global.Alipay.VerifySign(data) + if err != nil { + log.Println("异步通知验证签名发生错误", err) + return response.ErrCodeFailed + } + + if !ok { + log.Println("异步通知验证签名未通过") + return response.ErrCodeFailed + } + + log.Println("异步通知验证签名通过") + + var p = alipay.TradeQuery{} + p.OutTradeNo = outTradeNo + rsp, err := global.Alipay.TradeQuery(p) + if err != nil { + log.Printf("异步通知验证订单 %s 信息发生错误: %s \n", outTradeNo, err.Error()) + return response.ErrCodeFailed + } + if !rsp.IsSuccess() { + log.Printf("异步通知验证订单 %s 信息发生错误: %s-%s \n", outTradeNo, rsp.Content.Msg, rsp.Content.SubMsg) + return response.ErrCodeFailed + } + + log.Printf("订单 %s 支付成功 \n", outTradeNo) + return response.ErrCodeSuccess +} + +// 获取订阅信息 +func (s *SubscribeService) GetInfo(uid int64) (*models.SubscribeInfo, int) { + var si models.SubscribeInfo + if err := global.Db.Table(SUBSCRIBE).First(&si, "uid = ?", uid).Error; err != nil { + return nil, response.ErrCodeFailed + } + // 判断用户订阅是否过期 + if si.Version == 2 && time.Now().Unix() > int64(si.Expired) { + err := global.Db.Model(&models.Subscribe{}).Where("uid = ?", uid).Update("version", 1).Error + if err != nil { + return nil, response.ErrCodeFailed + } + } + if err := global.Db.Table(SUBSCRIBE).First(&si, "uid = ?", uid).Error; err != nil { + return nil, response.ErrCodeFailed + } + return &si, response.ErrCodeSuccess +} diff --git a/web/src/api/subscribe.js b/web/src/api/subscribe.js new file mode 100644 index 0000000..5540da6 --- /dev/null +++ b/web/src/api/subscribe.js @@ -0,0 +1,20 @@ +import request from '../axios/index' + +// 订阅专业版 +export function subscribePay(param) { + return request({ + url: '/subscribe/pay', + method: 'post', + data: param, + }) +} + +// 获取订阅信息 +export function getSubscribeInfo(param) { + return request({ + url: '/subscribe/info', + method: 'get', + params: param, + }) +} + diff --git a/web/src/views/Dashboard.vue b/web/src/views/Dashboard.vue index 6ff2c42..bd9e36f 100644 --- a/web/src/views/Dashboard.vue +++ b/web/src/views/Dashboard.vue @@ -93,7 +93,7 @@ import { QuestionCircleTwoTone } from '@ant-design/icons-vue' import * as echarts from "echarts"; import { reactive, ref, onMounted } from 'vue'; import { getSummary } from "../api/dashboard"; -import { getUserInfo } from "../api/user"; +import { getSubscribeInfo } from '../api/subscribe'; import { useRouter } from 'vue-router' export default { @@ -114,7 +114,7 @@ export default { }) onMounted(() => { - checkVersion(); + subscribeInfo(); initChart(); }); @@ -163,9 +163,9 @@ export default { }) } - // 查看用户系统版本 - const checkVersion = () => { - getUserInfo().then((res) => { + // 获取用户订阅信息 + const subscribeInfo = () => { + getSubscribeInfo().then((res) => { if (res.data.code == 0 && res.data.data.version == 1) { router.push('/result') } @@ -176,7 +176,7 @@ export default { data, daysRange, initChart, - checkVersion, + subscribeInfo, } } } diff --git a/web/src/views/Subscribe.vue b/web/src/views/Subscribe.vue index 3f33b8e..2c884e8 100644 --- a/web/src/views/Subscribe.vue +++ b/web/src/views/Subscribe.vue @@ -1,27 +1,53 @@