Compare commits

...

10 Commits

Author SHA1 Message Date
99779e6415 🎨 优化讲师包月模块 2025-10-11 16:16:42 +08:00
ed962c26b9 🎨 优化鉴权中间件 2025-10-11 16:16:24 +08:00
86c7d443cb 🎨 优化支付回调 2025-10-11 16:16:12 +08:00
91ec3a2601 🎨 新增清理机器人和定时设置免费定时任务 2025-10-11 16:15:02 +08:00
481635d333 🎨 优化机器人查询 2025-10-11 16:14:37 +08:00
4fc979aaad 🎨 优化文章上传接口 2025-10-11 16:14:30 +08:00
d593476c51 🎨 优化文章和支付回调,新增返回分享用接口 2025-10-08 16:41:50 +08:00
f0ea189553 🎨 优化机器人关键词匹配 2025-09-27 17:22:32 +08:00
0ec44fad2c 🎨 新增兑换码库导出excel功能 2025-09-27 17:17:47 +08:00
013b7af7e3 🎨 机器人新增vip权限判断 2025-09-27 17:17:20 +08:00
27 changed files with 575 additions and 133 deletions

View File

@@ -1,6 +1,7 @@
package app
import (
"fmt"
"git.echol.cn/loser/lckt/global"
"git.echol.cn/loser/lckt/model/app"
"git.echol.cn/loser/lckt/model/app/request"
@@ -8,6 +9,7 @@ import (
r "git.echol.cn/loser/lckt/model/common/response"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"time"
)
type RedeemCodeApi struct{}
@@ -169,6 +171,7 @@ func (rc *RedeemCodeApi) DeleteCDK(ctx *gin.Context) {
r.OkWithMessage("删除兑换码成功", ctx)
}
// Redeem 兑换码兑换
func (rc *RedeemCodeApi) Redeem(context *gin.Context) {
var p request.RedeemCDK
if err := context.ShouldBind(&p); err != nil {
@@ -186,3 +189,29 @@ func (rc *RedeemCodeApi) Redeem(context *gin.Context) {
r.OkWithMessage("兑换成功", context)
}
// ExportCDK 导出Excel
func (rc *RedeemCodeApi) ExportCDK(ctx *gin.Context) {
var p request.ExportCDK
if err := ctx.ShouldBind(&p); err != nil {
r.FailWithMessage("导出Excel失败"+err.Error(), ctx)
global.GVA_LOG.Error("导出Excel失败", zap.Error(err))
return
}
f, err := redeemCodeService.ExportCDK(p)
if err != nil {
r.FailWithMessage("导出Excel失败"+err.Error(), ctx)
global.GVA_LOG.Error("导出Excel失败", zap.Error(err))
return
}
// Return the excel file.
ctx.Header("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
filename := fmt.Sprintf("好运助手兑换码%v.xlsx", time.Now().Format("2006-01-02T15:04:05"))
ctx.Header("Content-Disposition", "attachment; filename="+filename)
if err := f.Write(ctx.Writer); err != nil {
r.FailWithMessage("导出Excel失败"+err.Error(), ctx)
return
}
r.OkWithMessage("导出Excel成功", ctx)
}

View File

@@ -7,6 +7,7 @@ import (
common "git.echol.cn/loser/lckt/model/common/request"
r "git.echol.cn/loser/lckt/model/common/response"
"git.echol.cn/loser/lckt/utils"
"git.echol.cn/loser/lckt/utils/user_jwt"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
@@ -78,7 +79,9 @@ func (a *TeacherVip) GetTeacherVipList(context *gin.Context) {
return
}
vips, total, err := teacherVipService.GetTeacherVipList(p)
userId := user_jwt.GetUserID(context)
vips, total, err := teacherVipService.GetTeacherVipList(p, userId)
if err != nil {
global.GVA_LOG.Error("获取讲师VIP列表失败", zap.Error(err))
r.FailWithMessage("获取讲师VIP列表失败", context)

View File

@@ -6,6 +6,7 @@ import (
common "git.echol.cn/loser/lckt/model/common/request"
user2 "git.echol.cn/loser/lckt/model/user"
"gorm.io/gorm"
"net/http"
"strconv"
"time"
@@ -586,3 +587,18 @@ func (a *AppUserApi) GetVipTeacherList(context *gin.Context) {
PageSize: p.PageSize,
}, "获取包月讲师列表成功", context)
}
func (a *AppUserApi) GetWechatJSSDKSign(context *gin.Context) {
//接收前端传递的url参数
url := context.Query("url")
jsapiList := []string{"chooseImage"}
debug := false
beta := false
openTagList := []string{"updateAppMessageShareData", "updateTimelineShareData", "onMenuShareAppMessage", "onMenuShareTimeline"}
data, err := wechat.WeOfficial.JSSDK.BuildConfig(context, jsapiList, debug, beta, openTagList, url)
if err != nil {
context.String(http.StatusBadRequest, err.Error())
return
}
context.JSON(http.StatusOK, data)
}

View File

@@ -14,7 +14,7 @@ import (
type ArticleApi struct{}
func (ArticleApi) Create(ctx *gin.Context) {
var p article.Article
var p request.CreateArticle
if err := ctx.ShouldBind(&p); err != nil {
r.FailWithMessage(err.Error(), ctx)
global.GVA_LOG.Error("参数有误!", zap.Error(err))

View File

@@ -1,13 +1,17 @@
package bot
import (
"errors"
"git.echol.cn/loser/lckt/global"
"git.echol.cn/loser/lckt/model/bot"
botReq "git.echol.cn/loser/lckt/model/bot/request"
"git.echol.cn/loser/lckt/model/common/response"
"git.echol.cn/loser/lckt/utils"
"git.echol.cn/loser/lckt/utils/user_jwt"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"gorm.io/gorm"
"strings"
)
type BotApi struct{}
@@ -183,7 +187,16 @@ func (btApi *BotApi) FindKey(c *gin.Context) {
return
}
bt, err := btService.GetBotPublic(req)
userId := user_jwt.GetUserID(c)
bt, err := btService.GetBotPublic(req, userId)
// 判断是否查询为空
if errors.Is(err, gorm.ErrRecordNotFound) {
// 没有找到记录的处理逻辑
response.OkWithData(bt, c)
return
}
if err != nil {
global.GVA_LOG.Error("获取失败!", zap.Error(err))
response.FailWithMessage("获取失败:"+err.Error(), c)
@@ -205,8 +218,9 @@ func (btApi *BotApi) BulkBot(c *gin.Context) {
err := btService.BulkBot(p, name)
if err != nil {
global.GVA_LOG.Error("批量创建失败!", zap.Error(err))
response.FailWithMessage("批量创建失败:"+err.Error(), c)
global.GVA_LOG.Error("批量上传失败!", zap.Error(err))
// 只要返回部分失败的文件列表 删除前面的"部分文件上传失败: "即可
response.FailWithDetailed(strings.TrimPrefix(err.Error(), "部分文件上传失败: "), "部分文件上传失败:"+strings.TrimPrefix(err.Error(), "部分文件上传失败: "), c)
return
}
response.OkWithMessage("批量创建成功", c)

View File

@@ -26,7 +26,7 @@ func initBizRouter(routers ...*gin.RouterGroup) {
}
{
articleRouter := router.RouterGroupApp.Article
articleRouter.InitBotRouter(privateGroup, publicGroup, appGroup)
articleRouter.InitBotRouter(privateGroup, appGroup)
}
{
userRouter := router.RouterGroupApp.User

View File

@@ -58,5 +58,38 @@ func Timer() {
if err != nil {
fmt.Println("add timer error:", err)
}
// 定时检查讲师VIP是否过期
_, err = global.GVA_Timer.AddTaskByFunc("CheckTeacherVip", "0 0/5 * * * ?", func() {
err5 := task.CheckTeacherVip(global.GVA_DB)
if err5 != nil {
fmt.Println("清理过期讲师VIP定时任务失败:", err5)
}
}, "定时清理过期讲师VIP日志内容", option...)
if err != nil {
fmt.Println("add timer error:", err)
}
// 清理机器人
_, err = global.GVA_Timer.AddTaskByFunc("ClearBot", "0 0 22 * * ?", func() {
err6 := task.ClearBot(global.GVA_DB)
if err6 != nil {
fmt.Println("清理机器人定时任务失败:", err6)
}
}, "定时清理机器人日志内容:", option...)
if err != nil {
fmt.Println("add timer error:", err)
}
// 每天21:20 将所有文章设为免费
_, err = global.GVA_Timer.AddTaskByFunc("SetArticleFree", "0 20 21 * * ?", func() {
err7 := task.SetArticleFree(global.GVA_DB)
if err7 != nil {
fmt.Println("将文章设为免费定时任务失败:", err7)
}
}, "将所有文章设为免费", option...)
if err != nil {
fmt.Println("add timer error:", err)
}
}()
}

View File

@@ -3,9 +3,11 @@ package middleware
import (
"errors"
"git.echol.cn/loser/lckt/global"
"git.echol.cn/loser/lckt/model/user"
"git.echol.cn/loser/lckt/utils"
"git.echol.cn/loser/lckt/utils/user_jwt"
"github.com/golang-jwt/jwt/v5"
"go.uber.org/zap"
"strconv"
"time"
@@ -23,7 +25,7 @@ func UserJWTAuth() gin.HandlerFunc {
return
}
j := utils.NewJWT()
j := user_jwt.NewUserJWT()
// parseToken 解析token包含的信息
claims, err := j.ParseToken(token)
if err != nil {
@@ -39,6 +41,23 @@ func UserJWTAuth() gin.HandlerFunc {
return
}
// 查询用户是否被禁用
status := 1
err = global.GVA_DB.Model(&user.User{}).Where("id = ?", claims.BaseClaims.ID).Select("status").Scan(&status).Error
if err != nil {
global.GVA_LOG.Error("中间件查询用户状态失败", zap.Error(err))
response.FailWithMessage(err.Error(), c)
c.Abort()
return
}
if status == 0 {
response.Banned("用户已被禁用", c)
user_jwt.ClearToken(c)
c.Abort()
return
}
c.Set("claims", claims)
if claims.ExpiresAt.Unix()-time.Now().Unix() < claims.BufferTime {
dr, _ := utils.ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime)
@@ -50,7 +69,7 @@ func UserJWTAuth() gin.HandlerFunc {
user_jwt.SetToken(c, newToken, int(dr.Seconds()))
if global.GVA_CONFIG.System.UseMultipoint {
// 记录新的活跃jwt
_ = utils.SetRedisJWT(newToken, newClaims.Username)
_ = utils.SetRedisJWT(newToken, newClaims.NickName)
}
}
c.Next()

View File

@@ -21,3 +21,8 @@ type RedeemCDK struct {
UseName string `json:"useName" form:"useName"` // 使用人名称
UserId uint `json:"userId" form:"userId"` // 用户ID
}
type ExportCDK struct {
Eid uint `json:"eid" form:"eid" binding:"required"`
Domain string `json:"domain" form:"domain"` // 域名
}

View File

@@ -1,5 +1,7 @@
package vo
import "git.echol.cn/loser/lckt/model/app"
type UserInfo struct {
ID uint `json:"id" form:"id"`
NickName string `json:"nick_name" form:"nick_name"`
@@ -17,10 +19,24 @@ type UserInfo struct {
}
type TeacherInfo struct {
ID uint `json:"id" form:"id"`
NickName string `json:"nick_name" form:"nick_name"`
Avatar string `json:"avatar" form:"avatar"`
Des string `json:"des" form:"des"`
Follow int64 `json:"follow" form:"follow"` // 粉丝数
Weight int `json:"weight" form:"weight"` // 权重
ID uint `json:"id" form:"id"`
NickName string `json:"nick_name" form:"nick_name"`
Avatar string `json:"avatar" form:"avatar"`
Des string `json:"des" form:"des"`
Weight int `json:"weight" form:"weight"` // 权重
VIPInfo []TeacherVipInfo `json:"vip_info" form:"vip_info"`
}
type TeacherVipInfo struct {
TeacherId uint `json:"teacher_id" form:"teacher_id"`
Title string `json:"title" form:"title"`
ExpireAt string `json:"expire_at" form:"expire_at"`
}
type TeacherVipList struct {
app.TeacherVip
IsBuy int `json:"is_buy" form:"is_buy"` //是否购买 0 否 1 是
ExpireAt string `json:"expire_at" form:"expire_at"`
//是否过期
IsExpire int `json:"is_expire" form:"is_expire"` //是否过期 1 未过期 2 已过期
}

View File

@@ -1,6 +1,8 @@
package request
import "git.echol.cn/loser/lckt/model/common/request"
import (
"git.echol.cn/loser/lckt/model/common/request"
)
type GetList struct {
request.PageInfo
@@ -26,4 +28,19 @@ type BulkUpload struct {
// 发布时间
PublishTime string `json:"publishTime" form:"publishTime"` // 发布时间
IsFree *int `json:"isFree" form:"isFree"` // 是否免费
FreeTime string `json:"freeTime" form:"freeTime"` // 设置为免费时,免费时间段
}
type CreateArticle struct {
Title string `json:"title" form:"title" binding:"required"`
Desc string `json:"desc" form:"desc" binding:"required"`
Content string `json:"content" form:"content" binding:"required"`
CoverImg string `json:"coverImg" form:"coverImg" binding:"required"`
TeacherId int `json:"teacherId" form:"teacherId" binding:"required"`
TeacherName string `json:"teacherName" form:"teacherName" binding:"required"`
Price int64 `json:"price" form:"price" binding:"required"` // 价格,单位分
IsFree *int `json:"isFree" form:"isFree"` // 是否免费 0-否 1-是
// 分类ID
CategoryId int `json:"categoryId" form:"categoryId" binding:"required"` // 分类ID
PublishTime string `json:"publishTime" form:"publishTime"` // 发布时间
}

View File

@@ -6,6 +6,7 @@ type BaseClaims struct {
NickName string `json:"nickName"`
ID uint `json:"id"`
Phone string `json:"phone"`
Status int8 `json:"status"`
}
type CustomClaims struct {

View File

@@ -10,11 +10,12 @@ func (rcr *RedeemCodeRouter) InitRedeemCodeRouter(AppRouter, SysteamRouter *gin.
{
// 兑换码库
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("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.GET("/mk/excel", redeemCodeApi.ExportCDK) // 导出兑换码
}
{

View File

@@ -22,13 +22,14 @@ func (s *UserRouter) InitAppUserRouter(AppAuthGroup, PublicRouter *gin.RouterGro
appUserRouter.GET("/balanceLog", userApi.GetBalanceLog) // 获取余额变动日志
}
{
publicRouter.POST("wxLogin", userApi.WechatLogin) // 微信登录
publicRouter.POST("bindWX", userApi.BindWechat) // 绑定微信
publicRouter.POST("bindPhone", userApi.BindPhone) // 获取用户信息
publicRouter.POST("pwdlogin", userApi.PwdLogin) // 密码登录
publicRouter.POST("sms/send", userApi.SendCode) // 发送短信验证码
publicRouter.POST("login", userApi.Login) // 短信验证码登录
publicRouter.POST("register", userApi.Register) // 注册
publicRouter.POST("wxLogin", userApi.WechatLogin) // 微信登录
publicRouter.POST("bindWX", userApi.BindWechat) // 绑定微信
publicRouter.POST("bindPhone", userApi.BindPhone) // 获取用户信息
publicRouter.POST("pwdlogin", userApi.PwdLogin) // 密码登录
publicRouter.POST("sms/send", userApi.SendCode) // 发送短信验证码
publicRouter.POST("login", userApi.Login) // 短信验证码登录
publicRouter.POST("register", userApi.Register) // 注册
publicRouter.GET("/wechat/js/sign", userApi.GetWechatJSSDKSign) // 获取微信JSSDK签名
}
// 讲师包月相关接口
{

View File

@@ -8,10 +8,9 @@ import (
type ArticleRouter struct{}
// InitBotRouter 初始化 文章 路由信息
func (s *ArticleRouter) InitBotRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup, AppRouter *gin.RouterGroup) {
func (s *ArticleRouter) InitBotRouter(Router *gin.RouterGroup, AppRouter *gin.RouterGroup) {
articleRouter := Router.Group("article").Use(middleware.OperationRecord())
articleRouterWithoutRecord := Router.Group("article")
articleRouterWithoutAuth := PublicRouter.Group("article")
appRouter := AppRouter.Group("article")
{
articleRouter.POST("", artApi.Create) // 新建文章
@@ -26,8 +25,8 @@ func (s *ArticleRouter) InitBotRouter(Router *gin.RouterGroup, PublicRouter *gin
}
{
articleRouterWithoutAuth.GET("app/list", artApi.APPGetList) // 文章公开接口
articleRouterWithoutAuth.GET("app/:id", artApi.AppById) // 文章开放接口
appRouter.GET("app/list", artApi.APPGetList) // 文章公开接口
appRouter.GET("app/:id", artApi.AppById) // 文章开放接口
}
{
// App端文章相关接口

View File

@@ -176,17 +176,17 @@ func (s *OrderService) BalancePay(p request.BalancePay) error {
}
// 计算会员的过期时间
if user.VipExpireTime != "" {
expireTime, _ := time.Parse("2006-01-02", user.VipExpireTime)
expireTime, _ := time.Parse("2006-01-02 15:04:05", user.VipExpireTime)
if expireTime.After(time.Now()) {
// 如果会员未过期,则在原有的基础上增加时间
user.VipExpireTime = expireTime.AddDate(0, 0, int(vipInfo.Expiration)).Format("2006-01-02")
user.VipExpireTime = expireTime.AddDate(0, 0, int(vipInfo.Expiration)).Format("2006-01-02 15:04:05")
} else {
// 如果会员已过期,则从当前时间开始计算
user.VipExpireTime = time.Now().AddDate(0, 0, int(vipInfo.Expiration)).Format("2006-01-02")
user.VipExpireTime = time.Now().AddDate(0, 0, int(vipInfo.Expiration)).Format("2006-01-02 15:04:05")
}
} else {
// 如果没有会员时间,则从当前时间开始计算
user.VipExpireTime = time.Now().AddDate(0, 0, int(vipInfo.Expiration)).Format("2006-01-02")
user.VipExpireTime = time.Now().AddDate(0, 0, int(vipInfo.Expiration)).Format("2006-01-02 15:04:05")
}
// 更新用户的会员状态
@@ -211,17 +211,45 @@ func (s *OrderService) BalancePay(p request.BalancePay) error {
ids := strings.Split(order.TeacherVipId, ",")
for _, id := range ids {
teacherVip := app.UserTeacherVip{}
teacherVip.TeacherId = uint(order.TeacherId)
// 将id转为uint
teacherVipId, _ := strconv.ParseUint(id, 10, 64)
teacherVip.TeacherVipId = uint(teacherVipId)
teacherVip.UserId = uint(order.UserId)
teacherVip.ExpireAt = time.Now().AddDate(0, 1, 0).Format("2006-01-02") // 会员有效期一个月
teacherVip.IsExpire = 1 // 设置为未过期
err = global.GVA_DB.Create(&teacherVip).Error
if err != nil {
global.GVA_LOG.Error("购买讲师会员回调处理失败:", zap.Error(err))
return err
err = global.GVA_DB.Model(&app.UserTeacherVip{}).
Where("teacher_id = ? AND user_id = ? AND teacher_vip_id = ?", order.TeacherId, order.UserId, id).
Order("id desc"). // 取最新一条
First(&teacherVip).Error
now := time.Now()
var newExpireAt time.Time
if err == nil {
// 找到记录,判断是否过期
expireTime, _ := time.Parse("2006-01-02 15:04:05", teacherVip.ExpireAt)
if teacherVip.IsExpire == 1 && expireTime.After(now) {
// 未过期,在原有基础上加一个月
newExpireAt = expireTime.AddDate(0, 1, 0)
} else {
// 已过期,从当前时间加一个月
newExpireAt = now.AddDate(0, 1, 0)
}
teacherVip.ExpireAt = newExpireAt.Format("2006-01-02 15:04:05")
teacherVip.IsExpire = 1 // 设置为未过期
err = global.GVA_DB.Save(&teacherVip).Error
if err != nil {
global.GVA_LOG.Error("更新用户讲师会员信息失败", zap.Error(err))
return err
}
} else {
// 没有购买过,直接新建
teacherVip := app.UserTeacherVip{
TeacherId: uint(order.TeacherId),
UserId: uint(order.UserId),
TeacherVipId: func() uint { v, _ := strconv.ParseUint(id, 10, 64); return uint(v) }(),
ExpireAt: now.AddDate(0, 1, 0).Format("2006-01-02 15:04:05"),
IsExpire: 1,
}
err = global.GVA_DB.Create(&teacherVip).Error
if err != nil {
global.GVA_LOG.Error("购买讲师会员回调处理失败:", zap.Error(err))
return err
}
}
}
// 计算分成比例,按比例增加讲师余额

View File

@@ -2,6 +2,7 @@ package app
import (
"errors"
"fmt"
"git.echol.cn/loser/lckt/global"
"git.echol.cn/loser/lckt/model/app"
request2 "git.echol.cn/loser/lckt/model/app/request"
@@ -12,8 +13,10 @@ import (
"git.echol.cn/loser/lckt/utils"
"git.echol.cn/loser/lckt/utils/wechat"
"github.com/gin-gonic/gin"
"github.com/xuri/excelize/v2"
"go.uber.org/zap"
"gorm.io/gorm"
"os"
"time"
)
@@ -346,3 +349,90 @@ func (s RedeemCodeService) Redeem(p request2.RedeemCDK, ctx *gin.Context) (err e
}
return
}
func (s RedeemCodeService) ExportCDK(p request2.ExportCDK) (f *excelize.File, err error) {
var cdks []app.CDK
err = global.GVA_DB.Where("redeem_id = ?", p.Eid).Find(&cdks).Error
if err != nil {
global.GVA_LOG.Error("导出兑换码失败", zap.Error(err))
return
}
var redeem app.RedeemCode
err = global.GVA_DB.Where("id = ?", p.Eid).First(&redeem).Error
if err != nil {
global.GVA_LOG.Error("读取码库信息失败", zap.Error(err))
return
}
// 导出Excel
f = excelize.NewFile()
temp, err := os.CreateTemp("", "cdk.xlsx")
if err != nil {
global.GVA_LOG.Error("创建临时文件失败", zap.Error(err))
return
}
defer os.Remove(temp.Name())
defer temp.Close()
// 创建一个工作表
index, err := f.NewSheet("Sheet1")
// 设置单元格的值
f.SetCellValue("Sheet1", "A1", "赠送码标题")
f.SetCellValue("Sheet1", "B1", "CDK")
f.SetCellValue("Sheet1", "C1", "状态")
// 兑换链接
f.SetCellValue("Sheet1", "D1", "兑换链接")
f.SetCellValue("Sheet1", "E1", "使用人")
for i, cdk := range cdks {
f.SetCellValue("Sheet1", fmt.Sprintf("A%d", i+2), redeem.CodeName)
f.SetCellValue("Sheet1", fmt.Sprintf("B%d", i+2), cdk.Code)
if cdk.Status == 2 {
f.SetCellValue("Sheet1", fmt.Sprintf("C%d", i+2), "已使用")
} else if cdk.Status == 1 {
f.SetCellValue("Sheet1", fmt.Sprintf("C%d", i+2), "未使用")
}
f.SetCellValue("Sheet1", fmt.Sprintf("D%d", i+2), p.Domain+"pages/user/cdk/index?dhm="+cdk.Code)
f.SetCellValue("Sheet1", fmt.Sprintf("E%d", i+2), cdk.UseName)
}
// 设置工作簿的默认工作表
f.SetActiveSheet(index)
// 设置列宽
if err := f.SetColWidth("Sheet1", "A", "A", 20); err != nil {
global.GVA_LOG.Error("设置列宽失败", zap.Error(err))
}
if err := f.SetColWidth("Sheet1", "B", "B", 20); err != nil {
global.GVA_LOG.Error("设置列宽失败", zap.Error(err))
}
if err := f.SetColWidth("Sheet1", "C", "C", 10); err != nil {
global.GVA_LOG.Error("设置列宽失败", zap.Error(err))
}
if err := f.SetColWidth("Sheet1", "D", "D", 100); err != nil {
global.GVA_LOG.Error("设置列宽失败", zap.Error(err))
}
if err := f.SetColWidth("Sheet1", "E", "E", 20); err != nil {
global.GVA_LOG.Error("设置列宽失败", zap.Error(err))
}
// 设置单元格样式 居中
style, err := f.NewStyle(&excelize.Style{
Alignment: &excelize.Alignment{
Horizontal: "center",
Vertical: "center",
},
})
if err != nil {
global.GVA_LOG.Error("设置单元格样式失败", zap.Error(err))
}
if err := f.SetCellStyle("Sheet1", "A1", fmt.Sprintf("E%d", len(cdks)+1), style); err != nil {
global.GVA_LOG.Error("设置单元格样式失败", zap.Error(err))
}
if err = f.Write(temp); err != nil {
global.GVA_LOG.Error("写入临时文件失败", zap.Error(err))
return
}
return
}

View File

@@ -5,20 +5,22 @@ import (
"git.echol.cn/loser/lckt/global"
"git.echol.cn/loser/lckt/model/app"
"git.echol.cn/loser/lckt/model/app/request"
"git.echol.cn/loser/lckt/model/app/vo"
user2 "git.echol.cn/loser/lckt/model/user"
"go.uber.org/zap"
"gorm.io/gorm"
)
type TeacherVipService struct{}
// GetTeacherVipList 获取讲师包月列表
func (u *TeacherVipService) GetTeacherVipList(p request.GetTeacherVipList) (list []app.TeacherVip, total int64, err error) {
func (u *TeacherVipService) GetTeacherVipList(p request.GetTeacherVipList, userId uint) (list []vo.TeacherVipList, total int64, err error) {
limit := p.PageSize
offset := (p.Page - 1) * p.PageSize
db := global.GVA_DB.Model(&app.TeacherVip{})
if p.TeacherId != 0 {
db.Where("teacher_id = ? ", p.TeacherId)
db = db.Where("teacher_id = ? ", p.TeacherId)
}
if p.Keyword != "" {
@@ -30,11 +32,37 @@ func (u *TeacherVipService) GetTeacherVipList(p request.GetTeacherVipList) (list
global.GVA_LOG.Error("查询讲师包月总数失败", zap.Error(err))
return nil, 0, err
}
err = db.Limit(limit).Offset(offset).Find(&list).Error
var teacherVips []app.TeacherVip
err = db.Limit(limit).Offset(offset).Find(&teacherVips).Error
if err != nil {
global.GVA_LOG.Error("查询讲师包月列表失败", zap.Error(err))
return nil, 0, err
}
list = make([]vo.TeacherVipList, len(teacherVips))
for i, vip := range teacherVips {
list[i] = vo.TeacherVipList{
TeacherVip: vip,
}
}
for i := range list {
var userVip app.UserTeacherVip
err = global.GVA_DB.Where("user_id = ? AND teacher_vip_id = ?", userId, list[i].ID).First(&userVip).Error
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
global.GVA_LOG.Error("查询用户讲师VIP失败", zap.Error(err))
return
}
if errors.Is(err, gorm.ErrRecordNotFound) {
err = nil
list[i].IsBuy = 0
continue
}
list[i].IsBuy = 1
list[i].ExpireAt = userVip.ExpireAt
list[i].IsExpire = userVip.IsExpire
}
return
}

View File

@@ -250,36 +250,6 @@ func (u *AppUserService) GetTeacherList(p common.PageInfo) (list []vo.TeacherInf
return nil, 0, err
}
// 批量查询所有教师的粉丝数
var teacherIDs []uint
for _, t := range list {
teacherIDs = append(teacherIDs, t.ID)
}
type FansCount struct {
TeacherId uint
Count int64
}
var fansCounts []FansCount
if len(teacherIDs) > 0 {
err = global.GVA_DB.Model(&app.Follow{}).
Select("teacher_id, count(*) as count").
Where("teacher_id IN ?", teacherIDs).
Group("teacher_id").
Scan(&fansCounts).Error
if err != nil {
global.GVA_LOG.Error("批量查询教师粉丝数失败", zap.Error(err))
return nil, 0, err
}
}
// 映射粉丝数
fansMap := make(map[uint]int64)
for _, fc := range fansCounts {
fansMap[fc.TeacherId] = fc.Count
}
for i := range list {
list[i].Follow = fansMap[list[i].ID]
}
return
}
@@ -320,16 +290,6 @@ func (u *AppUserService) GetFollowTeacherList(id uint, p common.PageInfo) (list
return
}
// 获取每个教师的粉丝数
for i := range list {
followCount, err := u.GetTeacherFansCount(list[i].ID)
if err != nil {
global.GVA_LOG.Error("查询教师粉丝数失败", zap.Error(err))
return nil, 0, err
}
list[i].Follow = followCount
}
return
}
@@ -407,6 +367,12 @@ func (u *AppUserService) GetVipTeacherList(p common.PageInfo, userId uint) (list
global.GVA_LOG.Error("获取用户讲师包月信息失败:", zap.Error(err))
return nil, 0, err
}
var TeacherVipIds []uint
err = global.GVA_DB.Model(&app.UserTeacherVip{}).Where("user_id = ? and is_expire = 1", userId).Select("teacher_vip_id").Scan(&TeacherVipIds).Error
if len(vipTeacherIds) == 0 {
global.GVA_LOG.Error("获取讲师包月信息失败:", zap.Error(err))
return nil, 0, nil
}
db := global.GVA_DB.Model(&user.User{}).Where("user_type = ? and id in ?", 2, vipTeacherIds)
@@ -425,14 +391,26 @@ func (u *AppUserService) GetVipTeacherList(p common.PageInfo, userId uint) (list
return nil, 0, err
}
// 获取每个教师的粉丝数
// 获取用户包月信息
var vipInfos []vo.TeacherVipInfo
err = global.GVA_DB.Table("user_teacher_vip AS u").
Select("u.teacher_id, a.title, u.expire_at").
Joins("LEFT JOIN app_teacher_vip AS a ON u.teacher_vip_id = a.id").
Where("u.user_id = ?", userId).
Scan(&vipInfos).Error
if err != nil {
global.GVA_LOG.Error("查询用户包月信息失败", zap.Error(err))
return nil, 0, err
}
// 1. 按TeacherId分组
vipInfoMap := make(map[uint][]vo.TeacherVipInfo)
for _, v := range vipInfos {
vipInfoMap[v.TeacherId] = append(vipInfoMap[v.TeacherId], v)
}
// 2. 给每个讲师赋值自己的VIPInfo
for i := range list {
followCount, err := u.GetTeacherFansCount(list[i].ID)
if err != nil {
global.GVA_LOG.Error("查询教师粉丝数失败", zap.Error(err))
return nil, 0, err
}
list[i].Follow = followCount
list[i].VIPInfo = vipInfoMap[list[i].ID]
}
return

View File

@@ -15,8 +15,31 @@ import (
type ArticleService struct{}
func (ArticleService) CreateArticle(req article.Article) (err error) {
err = global.GVA_DB.Create(&req).Error
func (ArticleService) CreateArticle(req request.CreateArticle) (err error) {
// 将 p.PublishTime转为time.time类型
loc, _ := time.LoadLocation("Asia/Shanghai")
publishTime, _ := time.ParseInLocation("2006-01-02 15:04:05", req.PublishTime, loc)
status := 2
if req.PublishTime == "" {
status = 1 // 如果没有设置发布时间,默认立即发布
}
model := article.Article{
Title: req.Title,
Desc: req.Desc,
CategoryId: req.CategoryId,
TeacherId: req.TeacherId,
TeacherName: req.TeacherName,
CoverImg: req.CoverImg,
Content: req.Content,
IsFree: req.IsFree,
Price: req.Price,
PublishTime: &publishTime,
Status: status,
}
err = global.GVA_DB.Create(&model).Error
return
}
@@ -125,6 +148,19 @@ func (s ArticleService) APPGetArticle(id string, userId int) (article *vo.Articl
global.GVA_DB.Table("app_user").Select("avatar").Where("id = ?", article.TeacherId).Scan(&article.TeacherAvatar)
// 判断用户是否为SVIP
if userId != 0 {
var userInfo user.User
err = global.GVA_DB.Model(&user.User{}).Where("id = ?", userId).First(&userInfo).Error
if err != nil {
global.GVA_LOG.Error("查询用户信息失败", zap.Error(err))
return nil, err
}
if userInfo.IsVip == 1 && userInfo.UserLabel == 3 {
return article, nil
}
}
// 判断是否免费
if article.IsFree == 0 {
// 如果不是免费文章,判断用户是否购买过
@@ -274,6 +310,11 @@ func (s ArticleService) BulkUpload(p request.BulkUpload) (err error) {
loc, _ := time.LoadLocation("Asia/Shanghai")
publishTime, _ := time.ParseInLocation("2006-01-02 15:04:05", p.PublishTime, loc)
status := 2
if p.PublishTime == "" {
status = 1 // 如果没有设置发布时间,默认立即发布
}
articles = append(articles, article.Article{
Title: teacher.NickName + "--" + p.Title,
Desc: p.Desc,
@@ -285,6 +326,7 @@ func (s ArticleService) BulkUpload(p request.BulkUpload) (err error) {
IsFree: p.IsFree,
Price: int64(p.Price),
PublishTime: &publishTime,
Status: status,
})
}

View File

@@ -2,6 +2,7 @@ package bot
import (
"context"
"fmt"
"git.echol.cn/loser/lckt/global"
"git.echol.cn/loser/lckt/model/bot"
botReq "git.echol.cn/loser/lckt/model/bot/request"
@@ -73,7 +74,24 @@ func (btService *BotService) GetBotInfoList(ctx context.Context, info botReq.Bot
}
// GetBotPublic 模糊搜索公开机器人
func (btService *BotService) GetBotPublic(keyWord botReq.FindKey) (bt bot.Bot, err error) {
func (btService *BotService) GetBotPublic(keyWord botReq.FindKey, userId uint) (bt bot.Bot, err error) {
// 判断用户是否为VIP或者SVIP
var userCount int64
if userId != 0 {
err = global.GVA_DB.Table("app_user").Where("id = ? AND is_vip = 1", userId).Count(&userCount).Error
if err != nil {
global.GVA_LOG.Error("查询用户VIP状态失败", zap.Error(err))
return
}
}
if userCount == 0 {
text := "抱歉该内容仅对VIP用户开放升级VIP后即可查看完整内容。"
// 非VIP直接返回空内容
bt.Content = &text
return
}
userInput := strings.TrimSpace(keyWord.KeyWord)
// 生成用户输入的所有变体
@@ -109,6 +127,21 @@ func (btService *BotService) GetBotPublic(keyWord botReq.FindKey) (bt bot.Bot, e
whereClause := strings.Join(conditions, " OR ")
err = global.GVA_DB.Where(whereClause, args...).First(&bt).Error
// 如果没有查到进行2字一组分词模糊匹配
if err == gorm.ErrRecordNotFound {
segments := splitByTwoWords(userInput)
if len(segments) > 0 {
var segConds []string
var segArgs []interface{}
for _, seg := range segments {
segConds = append(segConds, "keyword LIKE ?")
segArgs = append(segArgs, "%"+seg+"%")
}
segWhere := strings.Join(segConds, " OR ")
err = global.GVA_DB.Where(segWhere, segArgs...).First(&bt).Error
}
}
go func() {
// 更新查询次数
if err == nil && bt.ID != 0 {
@@ -118,6 +151,16 @@ func (btService *BotService) GetBotPublic(keyWord botReq.FindKey) (bt bot.Bot, e
return
}
// splitByTwoWords 将字符串按2字一组分割
func splitByTwoWords(s string) []string {
runes := []rune(s)
var result []string
for i := 0; i < len(runes)-1; i++ {
result = append(result, string(runes[i:i+2]))
}
return result
}
// generateKeywordVariants 生成关键词的所有可能变体
func (btService *BotService) generateKeywordVariants(input string) []string {
variants := make(map[string]bool)
@@ -168,22 +211,29 @@ func (btService *BotService) generateKeywordVariants(input string) []string {
}
func (btService *BotService) BulkBot(p botReq.BulkBot, userName string) (err error) {
var bots []bot.Bot
// 上传失败的图片
var failFiles []string
for _, a := range p.Files {
content := "<p><img src=" + a + " alt=\"" + a + "\" data-href=\"\" style=\"width: 100%;height: auto;\"/></p>"
bots = append(bots, bot.Bot{
content := "<img src=" + a + " alt=\"" + a + "\" data-href=\"\" style=\"width: 100%;height: auto;\"/>"
bots := bot.Bot{
Keyword: getBotKeyWorld(a),
Content: &content,
CreateBy: userName,
})
}
err = global.GVA_DB.Create(&bots).Error
if err != nil {
global.GVA_LOG.Error("创建机器人失败", zap.Error(err))
// 记录上传失败的文件
failFiles = append(failFiles, a)
continue
}
}
if len(bots) > 0 {
if dbErr := global.GVA_DB.Create(&bots).Error; dbErr != nil {
global.GVA_LOG.Error("批量上传文章失败", zap.Error(dbErr))
return dbErr
}
if len(failFiles) > 0 {
global.GVA_LOG.Error("部分文件上传失败", zap.Strings("failedFiles", failFiles))
return fmt.Errorf("部分文件上传失败: %v", failFiles)
}
return nil
}

24
task/checkTeacherVip.go Normal file
View File

@@ -0,0 +1,24 @@
package task
import (
"git.echol.cn/loser/lckt/global"
"git.echol.cn/loser/lckt/model/app"
"gorm.io/gorm"
)
// CheckTeacherVip 检查讲师VIP是否过期
func CheckTeacherVip(db *gorm.DB) error {
global.GVA_LOG.Info("开始检查用户讲师包月是否过期...")
var userTeacherVips []app.UserTeacherVip
// 根据当前时间和expire_at对比 查看是否到过期时间
db.Where("expire_at < ? AND is_expire = 1", gorm.Expr("NOW()")).Find(&userTeacherVips)
for _, u := range userTeacherVips {
u.IsExpire = 2
err := db.Save(&u).Error
if err != nil {
return err
}
}
global.GVA_LOG.Info("检查用户讲师包月是否过期完成...")
return nil
}

14
task/clearBot.go Normal file
View File

@@ -0,0 +1,14 @@
package task
import (
"gorm.io/gorm"
)
// ClearBot 清理机器人消息
func ClearBot(db *gorm.DB) error {
// 删除所有机器人
if err := db.Exec("TRUNCATE TABLE bots").Error; err != nil {
return err
}
return nil
}

8
task/setArticleFree.go Normal file
View File

@@ -0,0 +1,8 @@
package task
import "gorm.io/gorm"
// SetArticleFree 定时将到期的付费文章设置为免费
func SetArticleFree(db *gorm.DB) error {
return db.Exec("UPDATE `article` SET is_free = 1, price = 0 WHERE is_free = 0 AND `status` = 1").Error
}

View File

@@ -3,15 +3,13 @@ package test
import (
"crypto/md5"
"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"
"math/rand"
"net/http"
"net/url"
"strings"
"testing"
"time"
)
@@ -90,15 +88,14 @@ func TestTime(t *testing.T) {
}
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("清理表成功")
}
fmt.Println(GetTeacherName("https://lckt.oss-cn-hangzhou.aliyuncs.com/lckt/uploads/2025-09-12/阿弟_6CO8KB8HCSJL_1757684981.jpg"))
}
func GetTeacherName(url string) string {
lastSlash := strings.LastIndex(url, "/")
underscore := strings.Index(url[lastSlash+1:], "_")
if lastSlash == -1 || underscore == -1 {
return ""
}
return url[lastSlash+1 : lastSlash+1+underscore]
}

View File

@@ -94,6 +94,7 @@ func LoginToken(user user.User) (token string, claims request.CustomClaims, err
ID: user.ID,
NickName: user.NickName,
Phone: user.Phone,
Status: user.Status,
})
token, err = j.CreateToken(claims)
return

View File

@@ -274,17 +274,45 @@ func NotifyHandle(ctx *gin.Context) error {
ids := strings.Split(order.TeacherVipId, ",")
for _, id := range ids {
teacherVip := app.UserTeacherVip{}
teacherVip.TeacherId = uint(order.TeacherId)
// 将id转为uint
teacherVipId, _ := strconv.ParseUint(id, 10, 64)
teacherVip.TeacherVipId = uint(teacherVipId)
teacherVip.UserId = uint(order.UserId)
teacherVip.ExpireAt = time.Now().AddDate(0, 1, 0).Format("2006-01-02") // 会员有效期一个月
teacherVip.IsExpire = 1 // 设置为未过期
err = global.GVA_DB.Create(&teacherVip).Error
if err != nil {
global.GVA_LOG.Error("购买讲师会员回调处理失败:", zap.Error(err))
return err
err = global.GVA_DB.Model(&app.UserTeacherVip{}).
Where("teacher_id = ? AND user_id = ? AND teacher_vip_id = ?", order.TeacherId, order.UserId, id).
Order("id desc"). // 取最新一条
First(&teacherVip).Error
now := time.Now()
var newExpireAt time.Time
if err == nil {
// 找到记录,判断是否过期
expireTime, _ := time.Parse("2006-01-02 15:04:05", teacherVip.ExpireAt)
if teacherVip.IsExpire == 1 && expireTime.After(now) {
// 未过期,在原有基础上加一个月
newExpireAt = expireTime.AddDate(0, 1, 0)
} else {
// 已过期,从当前时间加一个月
newExpireAt = now.AddDate(0, 1, 0)
}
teacherVip.ExpireAt = newExpireAt.Format("2006-01-02 15:04:05")
teacherVip.IsExpire = 1 // 设置为未过期
err = global.GVA_DB.Save(&teacherVip).Error
if err != nil {
global.GVA_LOG.Error("更新用户讲师会员信息失败", zap.Error(err))
return err
}
} else {
// 没有购买过,直接新建
teacherVip := app.UserTeacherVip{
TeacherId: uint(order.TeacherId),
UserId: uint(order.UserId),
TeacherVipId: func() uint { v, _ := strconv.ParseUint(id, 10, 64); return uint(v) }(),
ExpireAt: now.AddDate(0, 1, 0).Format("2006-01-02 15:04:05"),
IsExpire: 1,
}
err = global.GVA_DB.Create(&teacherVip).Error
if err != nil {
global.GVA_LOG.Error("购买讲师会员回调处理失败:", zap.Error(err))
return err
}
}
}