refactor: subscribe module

This commit is contained in:
zchengo
2023-01-27 11:05:24 +08:00
parent a7d7ddb1bc
commit 9f944a34f6
7 changed files with 163 additions and 180 deletions
+6 -13
View File
@@ -1,6 +1,7 @@
package api package api
import ( import (
"crm/common"
"crm/models" "crm/models"
"crm/response" "crm/response"
"crm/service" "crm/service"
@@ -25,7 +26,7 @@ func NewSubscribeApi() *SubscribeApi {
func (s *SubscribeApi) Pay(context *gin.Context) { func (s *SubscribeApi) Pay(context *gin.Context) {
var param models.SubscribePayParam var param models.SubscribePayParam
uid, _ := strconv.Atoi(context.Request.Header.Get("uid")) uid, _ := strconv.Atoi(context.Request.Header.Get("uid"))
err := context.ShouldBind(&param); err := context.ShouldBind(&param)
if int64(uid) <= 0 || err != nil { if int64(uid) <= 0 || err != nil {
response.Result(response.ErrCodeParamInvalid, nil, context) response.Result(response.ErrCodeParamInvalid, nil, context)
return return
@@ -36,18 +37,10 @@ func (s *SubscribeApi) Pay(context *gin.Context) {
} }
// 支付成功回调 // 支付成功回调
func (s *SubscribeApi) Callback(context *gin.Context) { func (s *SubscribeApi) PayBack(context *gin.Context) {
context.Request.ParseForm() notifyReq := common.GetAlipay().VerifySign(context.Request)
var outTradeNo = context.Request.Form.Get("out_trade_no") errCode := s.subscribeService.PayBack(notifyReq.GetString("out_trade_no"))
paySuccessURL, _ := s.subscribeService.Callback(outTradeNo) context.String(http.StatusOK, "%s", "success")
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) response.Result(errCode, nil, context)
} }
+2 -2
View File
@@ -56,8 +56,8 @@ func (s *SubscribeDao) SetOrder(tradeNo string, param *models.SubscribePayOrder)
return global.Rdb.Set(ctx, tradeNo, string(order), time.Minute*30).Err() return global.Rdb.Set(ctx, tradeNo, string(order), time.Minute*30).Err()
} }
func (s *SubscribeDao) GetOrder(orderNo string) (string, error) { func (s *SubscribeDao) GetOrder(tradeNo string) (string, error) {
orderJson, err := global.Rdb.Get(ctx, orderNo).Result() orderJson, err := global.Rdb.Get(ctx, tradeNo).Result()
if err != nil { if err != nil {
return StringNull, err return StringNull, err
} }
+1 -1
View File
@@ -3,8 +3,8 @@ package global
import ( import (
"crm/config" "crm/config"
"github.com/go-pay/gopay/alipay"
"github.com/go-redis/redis/v9" "github.com/go-redis/redis/v9"
"github.com/smartwalle/alipay/v3"
"gorm.io/gorm" "gorm.io/gorm"
) )
+1 -2
View File
@@ -25,7 +25,7 @@ func Router() {
route.POST("/user/register", api.NewUserApi().Register) route.POST("/user/register", api.NewUserApi().Register)
route.POST("/user/pass", api.NewUserApi().ForgotPass) route.POST("/user/pass", api.NewUserApi().ForgotPass)
route.DELETE("/user/delete", api.NewUserApi().Delete) route.DELETE("/user/delete", api.NewUserApi().Delete)
route.GET("/subscribe/callback", api.NewSubscribeApi().Callback) route.POST("/subscribe/payback", api.NewSubscribeApi().PayBack)
// 初始化数据 // 初始化数据
route.POST("/init/data", api.InitData) route.POST("/init/data", api.InitData)
@@ -73,7 +73,6 @@ func Router() {
// 订阅模块 // 订阅模块
route.GET("/subscribe/info", api.NewSubscribeApi().GetInfo) route.GET("/subscribe/info", api.NewSubscribeApi().GetInfo)
route.POST("/subscribe/pay", api.NewSubscribeApi().Pay) route.POST("/subscribe/pay", api.NewSubscribeApi().Pay)
route.POST("/subscribe/notify", api.NewSubscribeApi().Notify)
// 通知模块 // 通知模块
route.GET("/notice/list", api.NewNoticeApi().GetList) route.GET("/notice/list", api.NewNoticeApi().GetList)
+3 -2
View File
@@ -24,12 +24,13 @@ type SubscribeUpdateParam struct {
type SubscribePayParam struct { type SubscribePayParam struct {
Uid int64 `json:"uid" binding:"-"` Uid int64 `json:"uid" binding:"-"`
Version int `json:"version" binding:"required,oneof=2 3"` Duration int64 `json:"duration" binding:"required,oneof=30 90 180 365 730"`
} }
type SubscribePayOrder struct { type SubscribePayOrder struct {
Uid int64 `json:"uid"` Uid int64 `json:"uid"`
Version int `json:"version"` TradeNo string `json:"tradeNo"`
Duration int64 `json:"duration"`
} }
type SubscribePayUrl struct { type SubscribePayUrl struct {
+24 -100
View File
@@ -1,23 +1,12 @@
package service package service
import ( import (
"crm/common"
"crm/dao" "crm/dao"
"crm/global"
"crm/models" "crm/models"
"crm/response" "crm/response"
"encoding/json" "encoding/json"
"fmt"
"log"
"net/url"
"time" "time"
"github.com/smartwalle/alipay/v3"
"github.com/smartwalle/xid"
)
const (
Paying = 1 // 支付中
Payed = 2 // 已支付
) )
type SubscribeService struct { type SubscribeService struct {
@@ -37,134 +26,69 @@ func NewSubscribeService() *SubscribeService {
func (s *SubscribeService) Pay(param models.SubscribePayParam) (*models.SubscribePayUrl, int) { func (s *SubscribeService) Pay(param models.SubscribePayParam) (*models.SubscribePayUrl, int) {
// 构建订单支付信息 // 构建订单支付信息
tradeNo := fmt.Sprintf("%d", xid.Next()) tradeNo := common.GetAlipay().GenTradeNo()
var p = alipay.TradePagePay{} totalAmount := float64(param.Duration) * 0.6
p.NotifyURL = global.Config.Alipay.NotifyURL payUrl, err := common.GetAlipay().PagePay(tradeNo, totalAmount)
p.ReturnURL = global.Config.Alipay.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 := &models.SubscribePayOrder{
Uid: param.Uid,
Version: param.Version,
}
if err := s.subscribeDao.SetOrder(tradeNo, order); err != nil {
return nil, response.ErrCodeFailed
}
// 返回支付链接
payUrl, err := global.Alipay.TradePagePay(p)
if err != nil { if err != nil {
return nil, response.ErrCodeFailed return nil, response.ErrCodeFailed
} }
order := models.SubscribePayOrder{
Uid: param.Uid,
TradeNo: tradeNo,
Duration: param.Duration,
}
if err := s.subscribeDao.SetOrder(tradeNo, &order); err != nil {
return nil, response.ErrCodeFailed
}
subscribePayUrl := models.SubscribePayUrl{ subscribePayUrl := models.SubscribePayUrl{
PayUrl: payUrl.String(), PayUrl: payUrl,
} }
return &subscribePayUrl, response.ErrCodeSuccess return &subscribePayUrl, response.ErrCodeSuccess
} }
// 支付成功回调 // 支付成功回调
func (s *SubscribeService) Callback(outTradeNo string) (string, int) { func (s *SubscribeService) PayBack(outTradeNo 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 var order models.SubscribePayOrder
orderJson, err := s.subscribeDao.GetOrder(outTradeNo) orderJson, err := s.subscribeDao.GetOrder(outTradeNo)
if err != nil { if err != nil {
return StringNull, response.ErrCodeFailed return response.ErrCodeFailed
} }
if err := json.Unmarshal([]byte(orderJson), &order); err != nil { if err := json.Unmarshal([]byte(orderJson), &order); err != nil {
return StringNull, response.ErrCodeFailed return response.ErrCodeFailed
} }
// 创建订阅信息 // 创建订阅信息
var expired int64 expired := order.Duration * 24 * 60 * 60
var content string
switch order.Version {
case 2:
expired = time.Now().Unix() + int64(2592000)
content = SUBSCRIBE_NOTICE_TEMPLATE1
case 3:
expired = time.Now().Unix() + int64(31104000)
content = SUBSCRIBE_NOTICE_TEMPLATE2
}
if !s.subscribeDao.IsExists(order.Uid) { if !s.subscribeDao.IsExists(order.Uid) {
subscribe := models.SubscribeCreateParam{ subscribe := models.SubscribeCreateParam{
Uid: order.Uid, Uid: order.Uid,
Version: order.Version, Version: 2,
Expired: expired, Expired: expired,
} }
if err := s.subscribeDao.Create(&subscribe); err != nil { if err := s.subscribeDao.Create(&subscribe); err != nil {
return StringNull, response.ErrCodeFailed return response.ErrCodeFailed
} }
} else { } else {
subscribe := models.SubscribeUpdateParam{ subscribe := models.SubscribeUpdateParam{
Uid: order.Uid, Uid: order.Uid,
Version: order.Version,
Expired: expired, Expired: expired,
} }
if err := s.subscribeDao.Update(&subscribe); err != nil { if err := s.subscribeDao.Update(&subscribe); err != nil {
return StringNull, response.ErrCodeFailed return response.ErrCodeFailed
} }
} }
// 订阅通知 // 订阅通知
s.noticeDao.Create(&models.NoticeCreateParam{ s.noticeDao.Create(&models.NoticeCreateParam{
Content: content, Content: SUBSCRIBE_NOTICE_TEMPLATE1,
Creator: order.Uid, Creator: order.Uid,
}) })
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 return response.ErrCodeSuccess
} }
+111 -45
View File
@@ -1,58 +1,90 @@
<template> <template>
<div :style="{ padding: '20px 20px 12px 20px' }"> <div :style="{ padding: '20px 20px 12px 20px' }">
<a-alert message="点击立即购买后,会跳转到支付宝支付页面(沙箱环境,账户名 emrpqt1589@sandbox.com 登录密码/支付密码 111111" type="info"
show-icon /><br />
<a-row :gutter="30"> <a-row :gutter="30">
<a-col :span="8"> <a-col :span="12">
<a-card :class="activedClass[0]" :bordered="false"> <a-card :class="activedClass[0]" :bordered="false">
<span class="member-tag" style="color: #33B9FE;">基础版</span> <span class="member-tag" style="color: #33B9FE;">基础版</span>
<h2 class="title">免费 <h2 class="title">免费
<check-circle-filled v-if="ver == 1" class="icon" /> <check-circle-filled v-if="ver == 1" class="icon" />
</h2> </h2>
<div class="content">满足基础功能需求永久免费</div><br /> <div class="content">满足基础功能需求永久免费</div>
<a-button size="large" class="btn-free" shape="round">免费使用</a-button> <a-divider />
<br />
<div class="subscribe-list" v-for="item in ['客户管理', '合同管理', '产品管理']"> <div class="subscribe-list" v-for="item in ['客户管理', '合同管理', '产品管理']">
<check-circle-filled style="color: #33B9FE;font-size: 20px;;" /> <check-circle-filled style="color: #33B9FE;font-size: 20px;;" />
<span style="margin-left: 10px;">{{ item }}</span> <span style="margin-left: 10px;">{{ item }}</span>
</div> </div>
<br />
<a-button size="large" class="btn-free" shape="round">免费使用</a-button><br />
</a-card> </a-card>
</a-col> </a-col>
<a-col :span="8"> <a-col :span="12">
<a-card :class="activedClass[1]" :bordered="false"> <a-card :class="activedClass[1]" :bordered="false">
<span class="member-tag" style="color: #3788FF;">专业版</span> <span class="member-tag" style="color: #3788FF;">专业版</span>
<h2 class="title">月付费每月18元</h2> <h2 class="title">订阅时长付费</h2>
<div class="content">能力不设限新功能优先体验</div><br /> <div class="content">能力不设限新功能优先体验</div>
<a-button v-if="version == 1 || version == 3" type="primary" size="large" class="btn-buy" <a-divider />
@click="onPay(2)" shape="round" :disabled="disabled">立即购买</a-button> <a-row :gutter="0">
<a-button v-if="version == 2" type="primary" size="large" class="btn-buy" shape="round">{{ <a-col :span="8">
expired <div class="subscribe-list" v-for="item in ['客户管理', '合同管理', '产品管理']">
}} 到期</a-button> <check-circle-filled style="color: #33B9FE;font-size: 20px;;" />
<br />
<div class="subscribe-list" v-for="item in ['客户管理', '合同管理', '产品管理', '仪表盘可体验30天']">
<check-circle-filled style="color: #476FFF;font-size: 20px;;" />
<span style="margin-left: 10px;">{{ item }}</span> <span style="margin-left: 10px;">{{ item }}</span>
</div> </div>
</a-card>
</a-col> </a-col>
<a-col :span="8"> <a-col :span="8">
<a-card :class="activedClass[2]" :bordered="false"> <div class="subscribe-list" v-for="item in ['仪表盘功能', '邮件服务配置', '邮件发送功能']">
<span class="member-tag" style="color: #3788FF;">高级版</span>
<h2 class="title">按年付费每年198元</h2>
<div class="content">能力不设限新功能优先体验</div><br />
<a-button v-if="version == 1 || version == 2" type="primary" size="large" class="btn-buy"
@click="onPay(3)" shape="round" :disabled="disabled">立即购买</a-button>
<a-button v-if="version == 3" type="primary" size="large" class="btn-buy" shape="round">{{
expired
}} 到期</a-button>
<br />
<div class="subscribe-list" v-for="item in ['客户管理', '合同管理', '产品管理', '仪表盘可体验365天']">
<check-circle-filled style="color: #476FFF;font-size: 20px;;" /> <check-circle-filled style="color: #476FFF;font-size: 20px;;" />
<span style="margin-left: 10px;">{{ item }}</span> <span style="margin-left: 10px;">{{ item }}</span>
</div> </div>
</a-col>
</a-row>
<br />
<a-button v-if="version == 1" type="primary" size="large" class="btn-buy" @click="onBuy"
shape="round" :disabled="disabled">立即购买</a-button>
<a-button v-if="version == 2" type="primary" size="large" class="btn-buy" shape="round"
@click="onBuy">
立即续费
</a-button><br />
<a-button v-if="version == 2" type="link" size="large" class="btn-buy" shape="round">专业版于 {{
expired
}} 到期</a-button>
</a-card> </a-card>
</a-col> </a-col>
</a-row> </a-row>
<a-modal v-model:visible="visible" title="选择订阅类型" @ok="onPay" @cancel="onCancel" cancelText="取消" okText="支付"
width="600px" :centered="true">
<div style="height: 45vh;overflow-y: scroll;padding: 0 15px;">
<a-alert message="支付提示"
description="点击支付后,会跳转到支付宝支付页面。您可以使用支付宝沙箱环境的账户名 emrpqt1589@sandbox.com 和 登录密码/支付密码 111111,完成支付。"
type="info" show-icon /><br />
<a-form ref="subscribeFormRef" :model="subscribe" name="subscribe" :rules="rules">
<a-row :gutter="16">
<a-col :span="24">
<a-form-item label="订阅时长" name="duration">
<a-select v-model:value="subscribe.duration" placeholder="请选择">
<a-select-option :value="30">1个月</a-select-option>
<a-select-option :value="90">3个月</a-select-option>
<a-select-option :value="180">6个月</a-select-option>
<a-select-option :value="365">一年</a-select-option>
<a-select-option :value="730">两年</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="支付方式" name="payMode">
<a-radio-group v-model:value="subscribe.payMode">
<a-radio :value="1">支付宝</a-radio>
<a-radio :value="2">微信</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="合计支付" name="payment">
<span style="color: #ff991f;font-size: 18px;font-weight: 550;">{{
subscribe.duration *
0.6
}}</span>
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
</a-modal>
</div> </div>
</template> </template>
@@ -60,41 +92,75 @@
import { ref, reactive, onBeforeMount } from 'vue'; import { ref, reactive, onBeforeMount } from 'vue';
import { CheckCircleFilled } from '@ant-design/icons-vue'; import { CheckCircleFilled } from '@ant-design/icons-vue';
import { subscribePay, getSubscribeInfo } from '../api/subscribe'; import { subscribePay, getSubscribeInfo } from '../api/subscribe';
import { useRouter } from 'vue-router' import moment from 'moment';
import moment from 'moment' import { message, Modal } from 'ant-design-vue';
const router = useRouter()
const version = ref(0) const version = ref(0)
const expired = ref(undefined) const expired = ref(undefined)
const payUrl = ref()
const visible = ref(false) const visible = ref(false)
const disabled = ref(false) const disabled = ref(false)
const activedClass = reactive(['card', 'card', 'card']) const activedClass = reactive(['card', 'card', 'card'])
const payResult = ref(false) const subscribe = reactive({
const title = ref('') duration: 30,
const buttonText = ref(undefined) payMode: 1,
payment: 0.00
})
const isClick = (index) => { // 表单校验
active.value = index const rules = {
} duration: [{
required: true,
message: '请选择订阅时长',
trigger: 'blur',
}],
payMode: [{
required: true,
message: '请选择支付方式',
trigger: 'blur',
}],
payment: [{
required: true,
}],
};
const payResult = ref(false)
const subscribeFormRef = ref()
// 初始化数据 // 初始化数据
onBeforeMount(() => { subscribeInfo() }) onBeforeMount(() => { subscribeInfo() })
// 点击购买
const onBuy = () => {
visible.value = true
}
// 点击支付 // 点击支付
const onPay = (ver) => { const onPay = () => {
if (subscribe.payMode == 2) {
message.error('暂不支持微信支付!')
return
}
subscribeFormRef.value.validateFields().then(() => {
let param = { let param = {
version: ver duration: subscribe.duration
} }
subscribePay(param).then((res) => { subscribePay(param).then((res) => {
if (res.data.code == 0) { if (res.data.code == 0) {
visible.value = false visible.value = false
payResult.value = true payResult.value = true
Modal.info({
title: '正在跳转到支付宝支付页面...',
centered: true,
okText: '返回',
onOk() {
window.stop()
},
});
window.open(res.data.data.payUrl, '_self') window.open(res.data.data.payUrl, '_self')
} }
}) })
})
} }
// 获取用户订阅信息 // 获取用户订阅信息
@@ -147,19 +213,19 @@ const subscribeInfo = () => {
} }
.card { .card {
min-height: 450px; min-height: 480px;
box-shadow: 0 1px 10px 0 rgb(33 41 48 / 5%); box-shadow: 0 1px 10px 0 rgb(33 41 48 / 5%);
} }
.selected-free-card { .selected-free-card {
min-height: 450px; min-height: 480px;
box-shadow: 0 1px 10px 0 rgb(33 41 48 / 5%); box-shadow: 0 1px 10px 0 rgb(33 41 48 / 5%);
border: 2px solid #ceebfa; border: 2px solid #ceebfa;
background: #f0faff; background: #f0faff;
} }
.selected-card { .selected-card {
min-height: 450px; min-height: 480px;
box-shadow: 0 1px 10px 0 rgb(33 41 48 / 5%); box-shadow: 0 1px 10px 0 rgb(33 41 48 / 5%);
border: 2px solid #d6ddf9; border: 2px solid #d6ddf9;
background: #f3f6fd; background: #f3f6fd;