🎨 新增兑换码功能,新增vip过期检测定时任务
This commit is contained in:
218
README.md
218
README.md
@@ -1,54 +1,174 @@
|
|||||||
## server项目结构
|
# LCKT 后端服务
|
||||||
|
|
||||||
|
基于 Gin-Vue-Admin 框架开发的全栈开发基础平台后端服务,提供完整的用户管理、权限控制、内容管理等功能。
|
||||||
|
|
||||||
|
## 🚀 项目特性
|
||||||
|
|
||||||
|
- **现代化技术栈**: 基于 Go 1.23 + Gin + GORM + Redis + MySQL
|
||||||
|
- **完整的权限系统**: 基于 Casbin 的 RBAC 权限控制
|
||||||
|
- **丰富的业务模块**: 用户管理、订单系统、内容管理、通知系统等
|
||||||
|
- **多数据库支持**: MySQL、PostgreSQL、SQLite、SQL Server、Oracle
|
||||||
|
- **多种存储方案**: 阿里云OSS、腾讯云COS、七牛云、MinIO等
|
||||||
|
- **微信生态集成**: 微信支付、微信公众号
|
||||||
|
- **API文档**: 集成 Swagger 自动生成API文档
|
||||||
|
- **容器化部署**: 支持 Docker 部署
|
||||||
|
|
||||||
|
## 📋 功能模块
|
||||||
|
|
||||||
|
### 系统管理
|
||||||
|
- 用户管理 (User Management)
|
||||||
|
- 角色权限 (Role & Permission)
|
||||||
|
- 菜单管理 (Menu Management)
|
||||||
|
- API管理 (API Management)
|
||||||
|
- 操作日志 (Operation Log)
|
||||||
|
- 字典管理 (Dictionary)
|
||||||
|
|
||||||
|
### 业务功能
|
||||||
|
- 用户系统 (App User System)
|
||||||
|
- 订单管理 (Order Management)
|
||||||
|
- 兑换码系统 (Redeem Code)
|
||||||
|
- 文章管理 (Article Management)
|
||||||
|
- 分类管理 (Category Management)
|
||||||
|
- 横幅管理 (Banner Management)
|
||||||
|
- 通知系统 (Notice System)
|
||||||
|
- VIP会员 (VIP System)
|
||||||
|
- 机器人管理 (Bot Management)
|
||||||
|
|
||||||
|
### 工具功能
|
||||||
|
- 文件上传下载 (File Upload/Download)
|
||||||
|
- 代码生成器 (Code Generator)
|
||||||
|
- 表单生成器 (Form Builder)
|
||||||
|
- Excel导入导出 (Excel Import/Export)
|
||||||
|
- 验证码 (Captcha)
|
||||||
|
## 🏗️ 项
|
||||||
|
目结构
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
├── api
|
├── api # API接口层
|
||||||
│ └── v1
|
│ └── v1 # v1版本API
|
||||||
├── config
|
├── config # 配置文件结构体
|
||||||
├── core
|
├── core # 核心组件初始化
|
||||||
├── docs
|
├── docs # Swagger文档
|
||||||
├── global
|
├── global # 全局变量
|
||||||
├── initialize
|
├── initialize # 系统初始化
|
||||||
│ └── internal
|
├── middleware # 中间件
|
||||||
├── middleware
|
├── model # 数据模型
|
||||||
├── model
|
│ ├── request # 请求结构体
|
||||||
│ ├── request
|
│ └── response # 响应结构体
|
||||||
│ └── response
|
├── resource # 静态资源
|
||||||
├── packfile
|
│ ├── excel # Excel文件
|
||||||
├── resource
|
│ ├── page # 页面模板
|
||||||
│ ├── excel
|
│ └── template # 代码模板
|
||||||
│ ├── page
|
├── router # 路由
|
||||||
│ └── template
|
├── service # 业务逻辑层
|
||||||
├── router
|
├── source # 初始化数据
|
||||||
├── service
|
├── task # 定时任务
|
||||||
├── source
|
└── utils # 工具函数
|
||||||
└── utils
|
├── timer # 定时器
|
||||||
├── timer
|
└── upload # 文件上传
|
||||||
└── upload
|
|
||||||
```
|
```
|
||||||
|
|
||||||
| 文件夹 | 说明 | 描述 |
|
## 🛠️ 技术栈
|
||||||
| ------------ | ----------------------- | --------------------------- |
|
|
||||||
| `api` | api层 | api层 |
|
|
||||||
| `--v1` | v1版本接口 | v1版本接口 |
|
|
||||||
| `config` | 配置包 | config.yaml对应的配置结构体 |
|
|
||||||
| `core` | 核心文件 | 核心组件(zap, viper, server)的初始化 |
|
|
||||||
| `docs` | swagger文档目录 | swagger文档目录 |
|
|
||||||
| `global` | 全局对象 | 全局对象 |
|
|
||||||
| `initialize` | 初始化 | router,redis,gorm,validator, timer的初始化 |
|
|
||||||
| `--internal` | 初始化内部函数 | gorm 的 longger 自定义,在此文件夹的函数只能由 `initialize` 层进行调用 |
|
|
||||||
| `middleware` | 中间件层 | 用于存放 `gin` 中间件代码 |
|
|
||||||
| `model` | 模型层 | 模型对应数据表 |
|
|
||||||
| `--request` | 入参结构体 | 接收前端发送到后端的数据。 |
|
|
||||||
| `--response` | 出参结构体 | 返回给前端的数据结构体 |
|
|
||||||
| `packfile` | 静态文件打包 | 静态文件打包 |
|
|
||||||
| `resource` | 静态资源文件夹 | 负责存放静态文件 |
|
|
||||||
| `--excel` | excel导入导出默认路径 | excel导入导出默认路径 |
|
|
||||||
| `--page` | 表单生成器 | 表单生成器 打包后的dist |
|
|
||||||
| `--template` | 模板 | 模板文件夹,存放的是代码生成器的模板 |
|
|
||||||
| `router` | 路由层 | 路由层 |
|
|
||||||
| `service` | service层 | 存放业务逻辑问题 |
|
|
||||||
| `source` | source层 | 存放初始化数据的函数 |
|
|
||||||
| `utils` | 工具包 | 工具函数封装 |
|
|
||||||
| `--timer` | timer | 定时器接口封装 |
|
|
||||||
| `--upload` | oss | oss接口封装 |
|
|
||||||
|
|
||||||
|
### 后端框架
|
||||||
|
- **Gin**: 高性能的Go Web框架
|
||||||
|
- **GORM**: Go语言ORM库
|
||||||
|
- **Casbin**: 权限管理库
|
||||||
|
- **Viper**: 配置管理
|
||||||
|
- **Zap**: 高性能日志库
|
||||||
|
- **JWT**: 身份认证
|
||||||
|
- **Redis**: 缓存和会话存储
|
||||||
|
|
||||||
|
### 数据库支持
|
||||||
|
- MySQL
|
||||||
|
- PostgreSQL
|
||||||
|
- SQLite
|
||||||
|
- SQL Server
|
||||||
|
- Oracle
|
||||||
|
- MongoDB
|
||||||
|
|
||||||
|
### 对象存储
|
||||||
|
- 阿里云OSS
|
||||||
|
- 腾讯云COS
|
||||||
|
- 七牛云
|
||||||
|
- MinIO
|
||||||
|
- AWS S3
|
||||||
|
- Cloudflare R2
|
||||||
|
- 华为云OBS
|
||||||
|
|
||||||
|
### 第三方集成
|
||||||
|
- 微信支付
|
||||||
|
- 微信公众号
|
||||||
|
- 阿里云短信
|
||||||
|
- 邮件发送#
|
||||||
|
# 🚀 快速开始
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
- Go 1.23+
|
||||||
|
- MySQL 5.7+
|
||||||
|
- Redis 5.0+
|
||||||
|
|
||||||
|
### 安装依赖
|
||||||
|
```bash
|
||||||
|
go mod tidy
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置文件
|
||||||
|
复制配置文件并修改相关配置:
|
||||||
|
```bash
|
||||||
|
cp config.yaml.example config.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
主要配置项:
|
||||||
|
- 数据库连接信息
|
||||||
|
- Redis连接信息
|
||||||
|
- JWT密钥
|
||||||
|
- 对象存储配置
|
||||||
|
- 微信相关配置
|
||||||
|
|
||||||
|
### 运行项目
|
||||||
|
```bash
|
||||||
|
# 开发环境
|
||||||
|
go run main.go
|
||||||
|
|
||||||
|
# 生产环境
|
||||||
|
go build -o server main.go
|
||||||
|
./server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker 部署
|
||||||
|
```bash
|
||||||
|
# 构建镜像
|
||||||
|
docker build -t lckt-server .
|
||||||
|
|
||||||
|
# 运行容器
|
||||||
|
docker run -d -p 8888:8888 --name lckt-server lckt-server
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📖 API 文档
|
||||||
|
|
||||||
|
项目集成了 Swagger 自动生成API文档,启动项目后访问:
|
||||||
|
```
|
||||||
|
http://localhost:8888/swagger/index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 开发指南
|
||||||
|
|
||||||
|
### 代码生成
|
||||||
|
项目内置代码生成器,可以快速生成CRUD代码:
|
||||||
|
1. 配置数据表结构
|
||||||
|
2. 使用代码生成器生成相关文件
|
||||||
|
3. 根据业务需求调整生成的代码
|
||||||
|
|
||||||
|
### 权限控制
|
||||||
|
基于 Casbin 实现的 RBAC 权限控制:
|
||||||
|
- 用户 (User)
|
||||||
|
- 角色 (Role)
|
||||||
|
- 权限 (Permission)
|
||||||
|
- 菜单 (Menu)
|
||||||
|
|
||||||
|
### 中间件
|
||||||
|
- JWT认证中间件
|
||||||
|
- 跨域处理中间件
|
||||||
|
- 操作日志中间件
|
||||||
|
- 限流中间件
|
@@ -1,9 +1,11 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"git.echol.cn/loser/lckt/global"
|
||||||
"git.echol.cn/loser/lckt/model/app"
|
"git.echol.cn/loser/lckt/model/app"
|
||||||
common "git.echol.cn/loser/lckt/model/common/request"
|
common "git.echol.cn/loser/lckt/model/common/request"
|
||||||
"git.echol.cn/loser/lckt/model/common/response"
|
"git.echol.cn/loser/lckt/model/common/response"
|
||||||
|
"git.echol.cn/loser/lckt/task"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -94,3 +96,13 @@ func (b *BannerApi) GetIndexBanners(context *gin.Context) {
|
|||||||
}
|
}
|
||||||
response.OkWithData(list, context)
|
response.OkWithData(list, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BannerApi) GetVIPBanners(context *gin.Context) {
|
||||||
|
err := task.CheckVip(global.GVA_DB)
|
||||||
|
if err != nil {
|
||||||
|
response.FailWithMessage("获取VIPBanner失败: "+err.Error(), context)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.OkWithMessage("检测vip成功", context)
|
||||||
|
}
|
||||||
|
@@ -7,6 +7,7 @@ type ApiGroup struct {
|
|||||||
BannerApi
|
BannerApi
|
||||||
OrderApi
|
OrderApi
|
||||||
TeacherVip
|
TeacherVip
|
||||||
|
RedeemCodeApi
|
||||||
}
|
}
|
||||||
|
|
||||||
var userService = service.ServiceGroupApp.UserServiceGroup.UserService
|
var userService = service.ServiceGroupApp.UserServiceGroup.UserService
|
||||||
@@ -14,3 +15,4 @@ var appUserService = service.ServiceGroupApp.AppServiceGroup.AppUserService
|
|||||||
var bannerService = service.ServiceGroupApp.AppServiceGroup.BannerService
|
var bannerService = service.ServiceGroupApp.AppServiceGroup.BannerService
|
||||||
var orderService = service.ServiceGroupApp.AppServiceGroup.OrderService
|
var orderService = service.ServiceGroupApp.AppServiceGroup.OrderService
|
||||||
var teacherVipService = service.ServiceGroupApp.AppServiceGroup.TeacherVipService
|
var teacherVipService = service.ServiceGroupApp.AppServiceGroup.TeacherVipService
|
||||||
|
var redeemCodeService = service.ServiceGroupApp.AppServiceGroup.RedeemCodeService
|
||||||
|
188
api/v1/app/redeem_code.go
Normal file
188
api/v1/app/redeem_code.go
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.echol.cn/loser/lckt/global"
|
||||||
|
"git.echol.cn/loser/lckt/model/app"
|
||||||
|
"git.echol.cn/loser/lckt/model/app/request"
|
||||||
|
common "git.echol.cn/loser/lckt/model/common/request"
|
||||||
|
r "git.echol.cn/loser/lckt/model/common/response"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RedeemCodeApi struct{}
|
||||||
|
|
||||||
|
// Create 创建兑换码库
|
||||||
|
func (rc *RedeemCodeApi) Create(ctx *gin.Context) {
|
||||||
|
var p app.RedeemCode
|
||||||
|
if err := ctx.ShouldBind(&p); err != nil {
|
||||||
|
r.FailWithMessage("创建兑换码库失败:"+err.Error(), ctx)
|
||||||
|
global.GVA_LOG.Error("创建兑换码库失败", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := redeemCodeService.Create(p)
|
||||||
|
if err != nil {
|
||||||
|
r.FailWithMessage("创建兑换码库失败:"+err.Error(), ctx)
|
||||||
|
global.GVA_LOG.Error("创建兑换码库失败", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.OkWithMessage("创建兑换码库成功", ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除兑换码库
|
||||||
|
func (rc *RedeemCodeApi) Delete(ctx *gin.Context) {
|
||||||
|
var p app.RedeemCode
|
||||||
|
if err := ctx.ShouldBind(&p); err != nil {
|
||||||
|
r.FailWithMessage("删除兑换码库失败:"+err.Error(), ctx)
|
||||||
|
global.GVA_LOG.Error("删除兑换码库失败", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := redeemCodeService.Delete(p)
|
||||||
|
if err != nil {
|
||||||
|
r.FailWithMessage("删除兑换码库失败:"+err.Error(), ctx)
|
||||||
|
global.GVA_LOG.Error("删除兑换码库失败", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.OkWithMessage("删除兑换码库成功", ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新兑换码库
|
||||||
|
func (rc *RedeemCodeApi) Update(ctx *gin.Context) {
|
||||||
|
var p app.RedeemCode
|
||||||
|
if err := ctx.ShouldBind(&p); err != nil {
|
||||||
|
r.FailWithMessage("更新兑换码库失败:"+err.Error(), ctx)
|
||||||
|
global.GVA_LOG.Error("更新兑换码库失败", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := redeemCodeService.Update(p)
|
||||||
|
if err != nil {
|
||||||
|
r.FailWithMessage("更新兑换码库失败:"+err.Error(), ctx)
|
||||||
|
global.GVA_LOG.Error("更新兑换码库失败", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.OkWithMessage("更新兑换码库成功", ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetList 获取兑换码库列表
|
||||||
|
func (rc *RedeemCodeApi) GetList(ctx *gin.Context) {
|
||||||
|
var p common.PageInfo
|
||||||
|
if err := ctx.ShouldBind(&p); err != nil {
|
||||||
|
r.FailWithMessage("获取兑换码库列表失败:"+err.Error(), ctx)
|
||||||
|
global.GVA_LOG.Error("获取兑换码库列表失败", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list, total, err := redeemCodeService.GetList(p)
|
||||||
|
if err != nil {
|
||||||
|
r.FailWithMessage("获取兑换码库列表失败:"+err.Error(), ctx)
|
||||||
|
global.GVA_LOG.Error("获取兑换码库列表失败", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.OkWithDetailed(r.PageResult{
|
||||||
|
List: list,
|
||||||
|
Total: total,
|
||||||
|
Page: p.Page,
|
||||||
|
PageSize: p.PageSize,
|
||||||
|
}, "获取兑换码库列表成功", ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetById 根据ID获取兑换码库
|
||||||
|
func (rc *RedeemCodeApi) GetById(ctx *gin.Context) {
|
||||||
|
var p common.GetById
|
||||||
|
if err := ctx.ShouldBind(&p); err != nil {
|
||||||
|
r.FailWithMessage("获取兑换码库失败:"+err.Error(), ctx)
|
||||||
|
global.GVA_LOG.Error("获取兑换码库失败", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
redeem, err := redeemCodeService.GetById(p.ID)
|
||||||
|
if err != nil {
|
||||||
|
r.FailWithMessage("获取兑换码库失败:"+err.Error(), ctx)
|
||||||
|
global.GVA_LOG.Error("获取兑换码库失败", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.OkWithDetailed(redeem, "获取兑换码库成功", ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================CDK相关========================
|
||||||
|
|
||||||
|
// CreateCDK 生成兑换码
|
||||||
|
func (rc *RedeemCodeApi) CreateCDK(ctx *gin.Context) {
|
||||||
|
var p request.GenerateCDK
|
||||||
|
if err := ctx.ShouldBind(&p); err != nil {
|
||||||
|
r.FailWithMessage("生成兑换码失败:"+err.Error(), ctx)
|
||||||
|
global.GVA_LOG.Error("生成兑换码失败", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cdkVo, err := redeemCodeService.CreateCDK(p)
|
||||||
|
if err != nil {
|
||||||
|
r.FailWithMessage("生成兑换码失败:"+err.Error(), ctx)
|
||||||
|
global.GVA_LOG.Error("生成兑换码失败", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.OkWithDetailed(cdkVo, "生成兑换码成功", ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCDKList 获取兑换码列表
|
||||||
|
func (rc *RedeemCodeApi) GetCDKList(ctx *gin.Context) {
|
||||||
|
var p request.GetCDKList
|
||||||
|
if err := ctx.ShouldBind(&p); err != nil {
|
||||||
|
r.FailWithMessage("获取兑换码列表失败:"+err.Error(), ctx)
|
||||||
|
global.GVA_LOG.Error("获取兑换码列表失败", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list, total, err := redeemCodeService.GetCDKList(p)
|
||||||
|
if err != nil {
|
||||||
|
r.FailWithMessage("获取兑换码列表失败:"+err.Error(), ctx)
|
||||||
|
global.GVA_LOG.Error("获取兑换码列表失败", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.OkWithDetailed(r.PageResult{
|
||||||
|
List: list,
|
||||||
|
Total: total,
|
||||||
|
Page: p.Page,
|
||||||
|
PageSize: p.PageSize,
|
||||||
|
}, "获取兑换码列表成功", ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCDK 删除兑换码
|
||||||
|
func (rc *RedeemCodeApi) DeleteCDK(ctx *gin.Context) {
|
||||||
|
var p app.CDK
|
||||||
|
if err := ctx.ShouldBind(&p); err != nil {
|
||||||
|
r.FailWithMessage("删除兑换码失败:"+err.Error(), ctx)
|
||||||
|
global.GVA_LOG.Error("删除兑换码失败", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := redeemCodeService.DeleteCDK(p)
|
||||||
|
if err != nil {
|
||||||
|
r.FailWithMessage("删除兑换码失败:"+err.Error(), ctx)
|
||||||
|
global.GVA_LOG.Error("删除兑换码失败", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.OkWithMessage("删除兑换码成功", ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *RedeemCodeApi) Redeem(context *gin.Context) {
|
||||||
|
var p request.RedeemCDK
|
||||||
|
if err := context.ShouldBind(&p); err != nil {
|
||||||
|
r.FailWithMessage("兑换失败:"+err.Error(), context)
|
||||||
|
global.GVA_LOG.Error("兑换失败", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := redeemCodeService.Redeem(p, context)
|
||||||
|
if err != nil {
|
||||||
|
r.FailWithMessage(err.Error(), context)
|
||||||
|
global.GVA_LOG.Error("兑换失败", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.OkWithMessage("兑换成功", context)
|
||||||
|
}
|
@@ -84,6 +84,8 @@ func RegisterTables() {
|
|||||||
app.TeacherVip{},
|
app.TeacherVip{},
|
||||||
app.UserTeacherVip{},
|
app.UserTeacherVip{},
|
||||||
user.LoginLog{},
|
user.LoginLog{},
|
||||||
|
app.RedeemCode{},
|
||||||
|
app.CDK{},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.GVA_LOG.Error("register table failed", zap.Error(err))
|
global.GVA_LOG.Error("register table failed", zap.Error(err))
|
||||||
|
@@ -117,6 +117,7 @@ func Routers() *gin.Engine {
|
|||||||
appRouter.InitAppUserRouter(AppAuthGroup, PublicGroup)
|
appRouter.InitAppUserRouter(AppAuthGroup, PublicGroup)
|
||||||
appRouter.InitBannerRouter(PrivateGroup, PublicGroup) // Banner相关路由
|
appRouter.InitBannerRouter(PrivateGroup, PublicGroup) // Banner相关路由
|
||||||
appRouter.InitOrderRouter(AppAuthGroup, PrivateGroup, PublicGroup) // 订单相关路由
|
appRouter.InitOrderRouter(AppAuthGroup, PrivateGroup, PublicGroup) // 订单相关路由
|
||||||
|
appRouter.InitRedeemCodeRouter(AppAuthGroup, PrivateGroup) // 兑换码相关路由
|
||||||
}
|
}
|
||||||
|
|
||||||
//插件路由安装
|
//插件路由安装
|
||||||
|
@@ -26,12 +26,14 @@ func Timer() {
|
|||||||
|
|
||||||
// 其他定时任务定在这里 参考上方使用方法
|
// 其他定时任务定在这里 参考上方使用方法
|
||||||
|
|
||||||
//_, err := global.GVA_Timer.AddTaskByFunc("定时任务标识", "corn表达式", func() {
|
_, err = global.GVA_Timer.AddTaskByFunc("ClearVip", "@daily", func() {
|
||||||
// 具体执行内容...
|
err2 := task.CheckVip(global.GVA_DB)
|
||||||
// ......
|
if err2 != nil {
|
||||||
//}, option...)
|
fmt.Println("清理过期VIP定时任务失败:", err2)
|
||||||
//if err != nil {
|
}
|
||||||
// fmt.Println("add timer error:", err)
|
}, "定时清理过期VIP日志内容:", option...)
|
||||||
//}
|
if err != nil {
|
||||||
|
fmt.Println("add timer error:", err)
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
3
main.go
3
main.go
@@ -7,6 +7,8 @@ import (
|
|||||||
"git.echol.cn/loser/lckt/utils/wechat"
|
"git.echol.cn/loser/lckt/utils/wechat"
|
||||||
_ "go.uber.org/automaxprocs"
|
_ "go.uber.org/automaxprocs"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate go env -w GO111MODULE=on
|
//go:generate go env -w GO111MODULE=on
|
||||||
@@ -48,6 +50,7 @@ func initializeSystem() {
|
|||||||
|
|
||||||
wechat.InitWechatPay()
|
wechat.InitWechatPay()
|
||||||
wechat.InitWeOfficial()
|
wechat.InitWeOfficial()
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
|
||||||
initialize.SetupHandlers() // 注册全局函数
|
initialize.SetupHandlers() // 注册全局函数
|
||||||
if global.GVA_DB != nil {
|
if global.GVA_DB != nil {
|
||||||
|
34
model/app/redeem_code.go
Normal file
34
model/app/redeem_code.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import "git.echol.cn/loser/lckt/global"
|
||||||
|
|
||||||
|
type RedeemCode struct {
|
||||||
|
global.GVA_MODEL
|
||||||
|
CodeName string `json:"codeName" gorm:"comment:兑换码名称"`
|
||||||
|
Type int `json:"type" gorm:"comment:类型,1-VIP,2-讲师VIP,3-课程"`
|
||||||
|
Item uint `json:"item" gorm:"comment:对应类型的ID"`
|
||||||
|
Num int `json:"num" gorm:"comment:兑换码数量"`
|
||||||
|
No int `json:"no" gorm:"comment:已使用数量"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (RedeemCode) TableName() string {
|
||||||
|
return "app_redeem_code"
|
||||||
|
}
|
||||||
|
|
||||||
|
type CDK struct {
|
||||||
|
global.GVA_MODEL
|
||||||
|
RedeemId uint `json:"redeemId" gorm:"comment:兑换码库ID"`
|
||||||
|
Code string `json:"code" gorm:"unique;comment:兑换码"`
|
||||||
|
Status int `json:"status" gorm:"default:1;comment:状态,1-未使用,2-已使用,3-已过期"`
|
||||||
|
UseId uint `json:"useId" gorm:"comment:使用用户ID"`
|
||||||
|
UseName string `json:"useName" gorm:"comment:使用用户名"`
|
||||||
|
UseAt string `json:"useAt" gorm:"comment:使用时间"`
|
||||||
|
// 有效期
|
||||||
|
ValidDay int `json:"validDay" gorm:"comment:有效期,单位天 0表示永久有效"`
|
||||||
|
// 过期时间
|
||||||
|
ExpireAt string `json:"expireAt" gorm:"comment:过期时间"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (CDK) TableName() string {
|
||||||
|
return "app_cdk"
|
||||||
|
}
|
23
model/app/request/cdk.go
Normal file
23
model/app/request/cdk.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package request
|
||||||
|
|
||||||
|
import common "git.echol.cn/loser/lckt/model/common/request"
|
||||||
|
|
||||||
|
type GenerateCDK struct {
|
||||||
|
Eid uint `json:"eid" form:"eid" binding:"required"` // 兑换码库ID
|
||||||
|
Number int `json:"number" from:"number" binding:"required,min=1"` // 生成数量
|
||||||
|
Expirer int `json:"expirer" form:"expirer"` // 有效期,单位天 0表示永久有效
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetCDKList struct {
|
||||||
|
common.PageInfo
|
||||||
|
Eid uint `json:"eid" form:"eid" binding:"required"`
|
||||||
|
Code string `json:"code" form:"code"` // 兑换码
|
||||||
|
UseName string `json:"useName" form:"useName"` // 使用人
|
||||||
|
Status int `json:"status" form:"status"` // 状态
|
||||||
|
}
|
||||||
|
|
||||||
|
type RedeemCDK struct {
|
||||||
|
Code string `json:"code" form:"code" binding:"required"` // 兑换码
|
||||||
|
UseName string `json:"useName" form:"useName"` // 使用人名称
|
||||||
|
UserId uint `json:"userId" form:"userId"` // 用户ID
|
||||||
|
}
|
8
model/app/vo/cdk.go
Normal file
8
model/app/vo/cdk.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package vo
|
||||||
|
|
||||||
|
import "git.echol.cn/loser/lckt/model/app"
|
||||||
|
|
||||||
|
type CDKVo struct {
|
||||||
|
Eid uint `json:"eid"` // 兑换码库ID
|
||||||
|
CDK []app.CDK `json:"cdk"` // 生成的兑换码
|
||||||
|
}
|
@@ -18,5 +18,6 @@ func (b *BannerRouter) InitBannerRouter(Router, PublicRouter *gin.RouterGroup) {
|
|||||||
appRouter.GET("/list", bannerApi.GetList) // 获取Banner列表
|
appRouter.GET("/list", bannerApi.GetList) // 获取Banner列表
|
||||||
appRouter.GET("", bannerApi.GetByID) // Banner公开接口
|
appRouter.GET("", bannerApi.GetByID) // Banner公开接口
|
||||||
appRouter.GET("/index", bannerApi.GetIndexBanners) // 获取首页Banner
|
appRouter.GET("/index", bannerApi.GetIndexBanners) // 获取首页Banner
|
||||||
|
appRouter.GET("vip", bannerApi.GetVIPBanners) // 获取VIP Banner
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,9 +6,11 @@ type RouterGroup struct {
|
|||||||
UserRouter
|
UserRouter
|
||||||
BannerRouter
|
BannerRouter
|
||||||
OrderRouter
|
OrderRouter
|
||||||
|
RedeemCodeRouter
|
||||||
}
|
}
|
||||||
|
|
||||||
var userApi = api.ApiGroupApp.AppApiGroup.AppUserApi
|
var userApi = api.ApiGroupApp.AppApiGroup.AppUserApi
|
||||||
var bannerApi = api.ApiGroupApp.AppApiGroup.BannerApi
|
var bannerApi = api.ApiGroupApp.AppApiGroup.BannerApi
|
||||||
var orderApi = api.ApiGroupApp.AppApiGroup.OrderApi
|
var orderApi = api.ApiGroupApp.AppApiGroup.OrderApi
|
||||||
var teacherVipApi = api.ApiGroupApp.AppApiGroup.TeacherVip
|
var teacherVipApi = api.ApiGroupApp.AppApiGroup.TeacherVip
|
||||||
|
var redeemCodeApi = api.ApiGroupApp.AppApiGroup.RedeemCodeApi
|
||||||
|
31
router/app/redeem_code.go
Normal file
31
router/app/redeem_code.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import "github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
type RedeemCodeRouter struct{}
|
||||||
|
|
||||||
|
func (rcr *RedeemCodeRouter) InitRedeemCodeRouter(AppRouter, SysteamRouter *gin.RouterGroup) {
|
||||||
|
AppCDKRouter := AppRouter.Group("/cdk")
|
||||||
|
SysCDKRouter := SysteamRouter.Group("/cdk")
|
||||||
|
|
||||||
|
{
|
||||||
|
// 兑换码库
|
||||||
|
SysCDKRouter.POST("mk", redeemCodeApi.Create) // 创建兑换码库
|
||||||
|
SysCDKRouter.DELETE("mk", redeemCodeApi.Delete) // 删除兑换码库
|
||||||
|
SysCDKRouter.PUT("mk", redeemCodeApi.Update) // 更新兑换码库
|
||||||
|
SysCDKRouter.GET("/mk/list", redeemCodeApi.GetList) // 分页获取兑换码库列表
|
||||||
|
SysCDKRouter.GET("mk/:id", redeemCodeApi.GetById) // 获取单个兑换码库信息
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// 兑换码
|
||||||
|
SysCDKRouter.POST("/generate", redeemCodeApi.CreateCDK) // 生成兑换码
|
||||||
|
SysCDKRouter.GET("/list", redeemCodeApi.GetCDKList) // 分页获取兑换码列表
|
||||||
|
SysCDKRouter.DELETE("", redeemCodeApi.DeleteCDK) // 删除兑换码
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// 用户兑换码
|
||||||
|
AppCDKRouter.POST("/redeem", redeemCodeApi.Redeem) // 兑换码
|
||||||
|
}
|
||||||
|
}
|
@@ -5,4 +5,5 @@ type ServiceGroup struct {
|
|||||||
BannerService
|
BannerService
|
||||||
OrderService
|
OrderService
|
||||||
TeacherVipService
|
TeacherVipService
|
||||||
|
RedeemCodeService
|
||||||
}
|
}
|
||||||
|
348
service/app/redeem_code.go
Normal file
348
service/app/redeem_code.go
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"git.echol.cn/loser/lckt/global"
|
||||||
|
"git.echol.cn/loser/lckt/model/app"
|
||||||
|
request2 "git.echol.cn/loser/lckt/model/app/request"
|
||||||
|
"git.echol.cn/loser/lckt/model/article"
|
||||||
|
"git.echol.cn/loser/lckt/model/common/request"
|
||||||
|
"git.echol.cn/loser/lckt/model/user"
|
||||||
|
"git.echol.cn/loser/lckt/model/vip"
|
||||||
|
"git.echol.cn/loser/lckt/utils"
|
||||||
|
"git.echol.cn/loser/lckt/utils/wechat"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RedeemCodeService struct{}
|
||||||
|
|
||||||
|
// Create 创建兑换码库
|
||||||
|
func (s RedeemCodeService) Create(p app.RedeemCode) (err error) {
|
||||||
|
return global.GVA_DB.Create(&p).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除兑换码库
|
||||||
|
func (s RedeemCodeService) Delete(p app.RedeemCode) (err error) {
|
||||||
|
return global.GVA_DB.Delete(&p).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新兑换码库
|
||||||
|
func (s RedeemCodeService) Update(p app.RedeemCode) (err error) {
|
||||||
|
return global.GVA_DB.Save(&p).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetList 分页获取兑换码库列表
|
||||||
|
func (s RedeemCodeService) GetList(p request.PageInfo) (list []app.RedeemCode, total int64, err error) {
|
||||||
|
limit := p.PageSize
|
||||||
|
offset := p.PageSize * (p.Page - 1)
|
||||||
|
db := global.GVA_DB.Model(&app.RedeemCode{})
|
||||||
|
|
||||||
|
if p.Keyword != "" {
|
||||||
|
db = db.Where("code_name LIKE ?", "%"+p.Keyword+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Count(&total).Error
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = db.Limit(limit).Offset(offset).Find(&list).Error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetById 根据ID获取兑换码库
|
||||||
|
func (s RedeemCodeService) GetById(id int) (redeem app.RedeemCode, err error) {
|
||||||
|
err = global.GVA_DB.Where("id = ?", id).First(&redeem).Error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========================CDK相关========================
|
||||||
|
|
||||||
|
// CreateCDK 生成兑换码
|
||||||
|
func (s RedeemCodeService) CreateCDK(p request2.GenerateCDK) ([]app.CDK, error) {
|
||||||
|
codes := utils.GenerateRedeemCodes(p.Number)
|
||||||
|
var cdks []app.CDK
|
||||||
|
for _, code := range codes {
|
||||||
|
var cdk app.CDK
|
||||||
|
cdk.Code = code
|
||||||
|
cdk.RedeemId = p.Eid
|
||||||
|
cdk.ValidDay = p.Expirer
|
||||||
|
cdk.Status = 1
|
||||||
|
if p.Expirer == 0 {
|
||||||
|
cdk.ExpireAt = "永久有效"
|
||||||
|
} else {
|
||||||
|
cdk.ExpireAt = time.Now().AddDate(0, 0, p.Expirer).Format("2006-01-02")
|
||||||
|
}
|
||||||
|
|
||||||
|
cdks = append(cdks, cdk)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := global.GVA_DB.Create(&cdks).Error
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("生成兑换码失败", zap.Error(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新兑换码库的数量
|
||||||
|
err = global.GVA_DB.Model(&app.RedeemCode{}).Where("id = ?", p.Eid).Update("num", gorm.Expr("num + ?", p.Number)).Error
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("更新兑换码库数量失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return cdks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCDKList 获取兑换码列表
|
||||||
|
func (s RedeemCodeService) GetCDKList(p request2.GetCDKList) (list []app.CDK, total int64, err error) {
|
||||||
|
limit := p.PageSize
|
||||||
|
offset := p.PageSize * (p.Page - 1)
|
||||||
|
db := global.GVA_DB.Model(&app.CDK{}).Where("redeem_id = ?", p.Eid)
|
||||||
|
|
||||||
|
if p.Code != "" {
|
||||||
|
db = db.Where("code LIKE ?", "%"+p.Code+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.UseName != "" {
|
||||||
|
db = db.Where("use_name LIKE ?", "%"+p.UseName+"%")
|
||||||
|
}
|
||||||
|
if p.Status != 0 {
|
||||||
|
db = db.Where("status = ?", p.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Count(&total).Error
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = db.Limit(limit).Offset(offset).Find(&list).Error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteCDK 删除兑换码
|
||||||
|
func (s RedeemCodeService) DeleteCDK(p app.CDK) (err error) {
|
||||||
|
return global.GVA_DB.Delete(&p).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redeem 用户兑换码
|
||||||
|
func (s RedeemCodeService) Redeem(p request2.RedeemCDK, ctx *gin.Context) (err error) {
|
||||||
|
var cdk app.CDK
|
||||||
|
err = global.GVA_DB.Where("code = ?", p.Code).First(&cdk).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断状态
|
||||||
|
if cdk.Status != 1 {
|
||||||
|
return errors.New("兑换码不可用")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否过期
|
||||||
|
if cdk.ValidDay != 0 {
|
||||||
|
expireAt, _ := time.Parse("2006-01-02", cdk.ExpireAt)
|
||||||
|
if time.Now().After(expireAt) {
|
||||||
|
// 更新状态为已过期
|
||||||
|
cdk.Status = 3
|
||||||
|
_ = global.GVA_DB.Save(&cdk).Error
|
||||||
|
return errors.New("兑换码已过期")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取兑换码库信息
|
||||||
|
var redeem app.RedeemCode
|
||||||
|
err = global.GVA_DB.Where("id = ?", cdk.RedeemId).First(&redeem).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
userId := utils.GetUserID(ctx)
|
||||||
|
var userInfo user.User
|
||||||
|
err = global.GVA_DB.Where("id = ?", userId).First(&userInfo).Error
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("获取用户信息出错", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新兑换码状态
|
||||||
|
cdk.Status = 2
|
||||||
|
cdk.UseId = userInfo.ID
|
||||||
|
cdk.UseName = userInfo.NickName
|
||||||
|
cdk.UseAt = time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
err = global.GVA_DB.Save(&cdk).Error
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("更新兑换码状态失败", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新兑换码库已使用数量
|
||||||
|
err = global.GVA_DB.Model(&app.RedeemCode{}).Where("id = ?", cdk.RedeemId).Update("no", gorm.Expr("no + ?", 1)).Error
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("更新兑换码库已使用数量失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发放对应的权益
|
||||||
|
switch redeem.Type {
|
||||||
|
case 1:
|
||||||
|
// VIP
|
||||||
|
var vipInfo vip.Vip
|
||||||
|
err = global.GVA_DB.Where("id = ?", redeem.Item).First(&vipInfo).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
vipLevel := 1
|
||||||
|
if vipInfo.Level == 1 {
|
||||||
|
vipLevel = 2
|
||||||
|
} else if vipInfo.Level == 2 {
|
||||||
|
vipLevel = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断用户是否已经是VIP
|
||||||
|
if userInfo.IsVip == 1 {
|
||||||
|
// 是VIP,判断会员等级
|
||||||
|
if userInfo.UserLabel == int64(vipLevel) {
|
||||||
|
// 等级相同,延长会员时间
|
||||||
|
if userInfo.VipExpireTime != "" {
|
||||||
|
expireTime, _ := time.Parse("2006-01-02", userInfo.VipExpireTime)
|
||||||
|
if expireTime.After(time.Now()) {
|
||||||
|
// 如果会员未过期,则在原有的基础上增加时间
|
||||||
|
userInfo.VipExpireTime = expireTime.AddDate(0, 0, int(vipInfo.Expiration)).Format("2006-01-02")
|
||||||
|
} else {
|
||||||
|
// 如果会员已过期,则从当前时间开始计算
|
||||||
|
userInfo.VipExpireTime = time.Now().AddDate(0, 0, int(vipInfo.Expiration)).Format("2006-01-02")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果没有会员时间,则从当前时间开始计算
|
||||||
|
userInfo.VipExpireTime = time.Now().AddDate(0, 0, int(vipInfo.Expiration)).Format("2006-01-02")
|
||||||
|
}
|
||||||
|
err = global.GVA_DB.Save(&userInfo).Error
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("更新用户VIP信息失败", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if userInfo.UserLabel < int64(vipLevel) {
|
||||||
|
// 等级更高,直接覆盖
|
||||||
|
userInfo.UserLabel = int64(vipLevel)
|
||||||
|
userInfo.VipExpireTime = time.Now().AddDate(0, 0, int(vipInfo.Expiration)).Format("2006-01-02")
|
||||||
|
err = global.GVA_DB.Save(&userInfo).Error
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("更新用户VIP信息失败", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 不是VIP,直接设置为VIP
|
||||||
|
userInfo.IsVip = 1
|
||||||
|
userInfo.UserLabel = int64(vipLevel)
|
||||||
|
userInfo.VipExpireTime = time.Now().AddDate(0, 0, int(vipInfo.Expiration)).Format("2006-01-02")
|
||||||
|
err = global.GVA_DB.Save(&userInfo).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
// 讲师VIP
|
||||||
|
var teacherVip app.TeacherVip
|
||||||
|
err = global.GVA_DB.Where("id = ?", redeem.Item).First(&teacherVip).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否已经拥有该讲师VIP
|
||||||
|
var existingTeacherVip app.UserTeacherVip
|
||||||
|
err = global.GVA_DB.Where("user_id = ? AND teacher_vip_id = ?", userInfo.ID, teacherVip.ID).First(&existingTeacherVip).Error
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
global.GVA_LOG.Error("获取用户讲师VIP信息出错", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否为空
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
userTeacherVip := app.UserTeacherVip{
|
||||||
|
UserId: userInfo.ID,
|
||||||
|
TeacherVipId: teacherVip.ID,
|
||||||
|
TeacherId: teacherVip.TeacherId,
|
||||||
|
IsExpire: 1,
|
||||||
|
ExpireAt: time.Now().AddDate(0, 0, 30).Format("2006-01-02"),
|
||||||
|
}
|
||||||
|
err = global.GVA_DB.Create(&userTeacherVip).Error
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("兑换讲师包月服务失败", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认未过期,获取过期时间+30天
|
||||||
|
// 将 existingTeacherVip.ExpireAt转为time.Time类型
|
||||||
|
parse, err := time.Parse("2006-01-02", existingTeacherVip.ExpireAt)
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("转换过期时间出错", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已过期 当前时间开始计算+30天
|
||||||
|
if existingTeacherVip.IsExpire == 2 && time.Now().After(parse) {
|
||||||
|
existingTeacherVip.IsExpire = 1
|
||||||
|
existingTeacherVip.ExpireAt = time.Now().AddDate(0, 0, 30).Format("2006-01-02")
|
||||||
|
} else {
|
||||||
|
// 未过期 在原有时间上+30天
|
||||||
|
existingTeacherVip.ExpireAt = parse.AddDate(0, 0, 30).Format("2006-01-02")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = global.GVA_DB.Save(&existingTeacherVip).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("兑换讲师包月服务失败", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case 3:
|
||||||
|
// 课程
|
||||||
|
var course article.Article
|
||||||
|
err = global.GVA_DB.Where("id = ?", redeem.Item).First(&course).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断用户是否购买过该课程
|
||||||
|
var count int64
|
||||||
|
err = global.GVA_DB.Model(&app.Order{}).Where("article_id = ? AND user_id = ? AND status = 2", course.ID, userInfo.ID).Count(&count).Error
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("查询用户购买记录失败", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
// 未购买,新增购买记录
|
||||||
|
order := app.Order{
|
||||||
|
UserId: uint64(userInfo.ID),
|
||||||
|
Name: userInfo.NickName,
|
||||||
|
OpenId: userInfo.OpenId,
|
||||||
|
Phone: userInfo.Phone,
|
||||||
|
TeacherId: uint64(course.TeacherId),
|
||||||
|
ArticleId: course.ID,
|
||||||
|
Price: 0,
|
||||||
|
Status: 2,
|
||||||
|
OrderNo: wechat.GenerateOrderNum(),
|
||||||
|
OrderType: 1,
|
||||||
|
Title: course.Title,
|
||||||
|
Desc: "兑换码兑换" + course.Title,
|
||||||
|
PayType: 4,
|
||||||
|
}
|
||||||
|
err = global.GVA_DB.Create(&order).Error
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("兑换课程失败", zap.Error(err))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 已购买,直接返回
|
||||||
|
return errors.New("您已购买过该课程,无需重复兑换")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
23
task/checkVip.go
Normal file
23
task/checkVip.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package task
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.echol.cn/loser/lckt/model/user"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CheckVip 检查用户VIP是否过期
|
||||||
|
func CheckVip(db *gorm.DB) error {
|
||||||
|
var users []user.User
|
||||||
|
// 根据当前时间和vip_expire_time对比 查看是否到过期时间
|
||||||
|
db.Where("vip_expire_time < ? AND vip_expire_time IS NOT NULL", gorm.Expr("NOW()")).Find(&users)
|
||||||
|
for _, u := range users {
|
||||||
|
u.VipExpireTime = ""
|
||||||
|
u.IsVip = 0
|
||||||
|
u.UserLabel = 4
|
||||||
|
err := db.Save(&u).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
@@ -3,6 +3,11 @@ package test
|
|||||||
import (
|
import (
|
||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"git.echol.cn/loser/lckt/core"
|
||||||
|
"git.echol.cn/loser/lckt/global"
|
||||||
|
"git.echol.cn/loser/lckt/initialize"
|
||||||
|
"git.echol.cn/loser/lckt/task"
|
||||||
|
"go.uber.org/zap"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -71,3 +76,29 @@ func sendCode(code string) {
|
|||||||
fmt.Println("请求失败,状态码:", resp.StatusCode)
|
fmt.Println("请求失败,状态码:", resp.StatusCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTime(t *testing.T) {
|
||||||
|
// 默认未过期,获取过期时间+30天
|
||||||
|
// 将 existingTeacherVip.ExpireAt转为time.Time类型
|
||||||
|
parse, err := time.Parse("2006-01-02", "2025-09-10")
|
||||||
|
if err != nil {
|
||||||
|
global.GVA_LOG.Error("转换过期时间出错", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
after := time.Now().After(parse)
|
||||||
|
fmt.Println(after)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTask(t *testing.T) {
|
||||||
|
global.GVA_VP = core.Viper() // 初始化Viper
|
||||||
|
global.GVA_LOG = core.Zap() // 初始化zap日志库
|
||||||
|
zap.ReplaceGlobals(global.GVA_LOG)
|
||||||
|
global.GVA_DB = initialize.Gorm() // gorm连接数据库
|
||||||
|
initialize.DBList()
|
||||||
|
err := task.CheckVip(global.GVA_DB)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("清理表失败", err.Error())
|
||||||
|
} else {
|
||||||
|
fmt.Println("清理表成功")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
|
||||||
|
// GenerateInviteCode 生成邀请码,基于用户ID和随机数的MD5哈希
|
||||||
func GenerateInviteCode(userID uint) string {
|
func GenerateInviteCode(userID uint) string {
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
// 拼接用户ID和随机数
|
// 拼接用户ID和随机数
|
||||||
@@ -34,3 +35,28 @@ func GenerateRandomString(length int) string {
|
|||||||
}
|
}
|
||||||
return code
|
return code
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateRedeemCode 生成单个兑换码,格式为N7DY4kcf5z37hwz,随机大小写字母+数字
|
||||||
|
func GenerateRedeemCode() string {
|
||||||
|
const codeCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
|
codeLen := 15
|
||||||
|
b := make([]byte, codeLen)
|
||||||
|
for i := range b {
|
||||||
|
b[i] = codeCharset[rand.Intn(len(codeCharset))]
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateRedeemCodes 批量生成唯一兑换码,number为生成数量
|
||||||
|
func GenerateRedeemCodes(number int) []string {
|
||||||
|
codesMap := make(map[string]struct{}, number)
|
||||||
|
for len(codesMap) < number {
|
||||||
|
code := GenerateRedeemCode()
|
||||||
|
codesMap[code] = struct{}{}
|
||||||
|
}
|
||||||
|
codes := make([]string, 0, number)
|
||||||
|
for code := range codesMap {
|
||||||
|
codes = append(codes, code)
|
||||||
|
}
|
||||||
|
return codes
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user