Compare commits

...

5 Commits

Author SHA1 Message Date
bda654ec5e 🎨 新增打包docker相关配置 2025-09-17 12:09:35 +08:00
9070e40f5c 🎨 优化文章和讲师接口 2025-09-17 12:09:18 +08:00
914513b5da 🐛 修复分成金额错误计算bug 2025-09-17 12:08:45 +08:00
7f066800a8 🎨 新增批量修改包月服务价格功能&新增讲师管理相关功能 2025-09-15 20:26:32 +08:00
19eeb6f0a3 🎨 获取文章列表新增排序&优化批量上传文章 2025-09-15 20:25:13 +08:00
18 changed files with 526 additions and 90 deletions

View File

@@ -12,7 +12,6 @@ RUN go env -w GO111MODULE=on \
FROM alpine:latest
LABEL MAINTAINER="SliverHorn@sliver_horn@qq.com"
# 设置时区
ENV TZ=Asia/Shanghai
RUN apk update && apk add --no-cache tzdata openntpd \

View File

@@ -110,3 +110,21 @@ func (a *TeacherVip) GetTeacherVip(context *gin.Context) {
}
r.OkWithDetailed(vip, "获取讲师VIP成功", context)
}
// UpdatePriceBatch 批量更新价格
func (a *TeacherVip) UpdatePriceBatch(context *gin.Context) {
var p request.UpdateTeacherVipPriceBatch
if err := context.ShouldBind(&p); err != nil {
global.GVA_LOG.Error("参数错误,批量更新价格失败", zap.Error(err))
r.FailWithMessage("参数错误,批量更新价格失败", context)
return
}
err := teacherVipService.UpdatePriceBatch(p)
if err != nil {
r.FailWithMessage("批量更新价格失败", context)
return
}
r.OkWithMessage("批量更新价格成功", context)
}

View File

@@ -236,3 +236,39 @@ func (a *UserApi) GetUserVipList(context *gin.Context) {
PageSize: p.PageSize,
}, "获取用户会员列表成功", context)
}
// SetTeacherWeight 设置讲师权重
func (a *UserApi) SetTeacherWeight(context *gin.Context) {
var p request.SetTeacherInfo
if err := context.ShouldBind(&p); err != nil {
global.GVA_LOG.Error("参数错误,修改讲师信息失败", zap.Error(err))
r.FailWithMessage("参数错误,修改讲师信息失败", context)
return
}
if err := userService.SetTeacherWeight(p); err != nil {
global.GVA_LOG.Error("修改讲师信息失败", zap.Error(err))
r.FailWithMessage("修改讲师信息失败", context)
return
}
r.OkWithMessage("修改讲师信息成功", context)
}
// SetTeacherExpectRate 设置讲师分成比例
func (a *UserApi) SetTeacherExpectRate(context *gin.Context) {
var p request.SetTeacherInfo
if err := context.ShouldBind(&p); err != nil {
global.GVA_LOG.Error("参数错误,修改讲师信息失败", zap.Error(err))
r.FailWithMessage("参数错误,修改讲师信息失败", context)
return
}
if err := userService.SetTeacherExpectRate(p); err != nil {
global.GVA_LOG.Error("修改讲师信息失败", zap.Error(err))
r.FailWithMessage("修改讲师信息失败", context)
return
}
r.OkWithMessage("修改讲师信息成功", context)
}

View File

@@ -16,12 +16,29 @@ zap:
encode-level: LowercaseColorLevelEncoder
stacktrace-key: stacktrace
log-in-console: true
retention-day: -1
# redis configuration
redis:
name: "hw"
addr: 120.46.165.63:6379
password: "loser765911"
db: 0
addr: 177.7.0.14:6379
password: ""
useCluster: false
clusterAddrs:
- 172.21.0.3:7000
- 172.21.0.4:7001
- 172.21.0.2:7002
redis-list:
- name: cache
addr: 120.46.165.63:6379
password: "loser765911"
db: 1
useCluster: false
clusterAddrs:
- 172.21.0.3:7000
- 172.21.0.4:7001
- 172.21.0.2:7002
# mongo configuration
mongo:
@@ -30,6 +47,7 @@ mongo:
database: ''
username: ''
password: ''
auth-source: ''
min-pool-size: 0
max-pool-size: 100
socket-timeout-ms: 0
@@ -51,21 +69,25 @@ email:
# system configuration
system:
env: public # Change to "develop" to skip authentication for development mode
env: local # 修改为public可以关闭路由日志输出
addr: 8888
db-type: mysql
oss-type: local # 控制oss选择走本地还是 七牛等其他仓 自行增加其他oss仓可以在 server/utils/upload/upload.go 中 NewOss函数配置
use-redis: false # 使用redis
oss-type: aliyun-oss # 控制oss选择走本地还是 七牛等其他仓 自行增加其他oss仓可以在 server/utils/upload/upload.go 中 NewOss函数配置
use-redis: true # 使用redis
use-mongo: false # 使用mongo
use-multipoint: false
# IP限制次数 一个小时15000次
iplimit-count: 15000
# IP限制一个小时
iplimit-time: 3600
# 路由全局前缀
router-prefix: ""
# 严格角色模式 打开后权限将会存在上下级关系
use-strict-auth: false
# captcha configuration
captcha:
key-long: 6
key-long: 4
img-width: 240
img-height: 80
open-captcha: 0 # 0代表一直开启大于0代表限制次数
@@ -74,16 +96,19 @@ captcha:
# mysql connect configuration
# 未初始化之前请勿手动修改数据库信息如果一定要手动初始化请看https://gin-vue-admin.com/docs/first_master
mysql:
path: ""
port: ""
config: ""
db-name: ""
username: ""
password: ""
prefix: ""
port: "3366"
config: charset=utf8mb4&parseTime=True&loc=Local
db-name: lckt
username: lckt
password: loser765911.
path: 219.152.55.29
engine: ""
log-mode: info
max-idle-conns: 10
max-open-conns: 100
log-mode: ""
log-zap: false
singular: false
log-zap: true
# pgsql connect configuration
# 未初始化之前请勿手动修改数据库信息如果一定要手动初始化请看https://gin-vue-admin.com/docs/first_master
@@ -98,10 +123,42 @@ pgsql:
max-open-conns: 100
log-mode: ""
log-zap: false
oracle:
path: ""
port: ""
config: ""
db-name: ""
username: ""
password: ""
max-idle-conns: 10
max-open-conns: 100
log-mode: ""
log-zap: false
mssql:
path: ""
port: ""
config: ""
db-name: ""
username: ""
password: ""
max-idle-conns: 10
max-open-conns: 100
log-mode: ""
log-zap: false
sqlite:
path: ""
port: ""
config: ""
db-name: ""
username: ""
password: ""
max-idle-conns: 10
max-open-conns: 100
log-mode: ""
log-zap: false
db-list:
- disable: true # 是否禁用
type: "" # 数据库的类型,目前支持mysql、pgsql
type: "" # 数据库的类型,目前支持mysql、pgsql、mssql、oracle
alias-name: "" # 数据库的名称,注意: alias-name 需要在db-list中唯一
path: ""
port: ""
@@ -114,7 +171,6 @@ db-list:
log-mode: ""
log-zap: false
# local configuration
local:
path: uploads/file
@@ -122,22 +178,11 @@ local:
# autocode configuration
autocode:
transfer-restart: true
# root 自动适配项目根目录
# 请不要手动配置,他会在项目加载的时候识别出根路径
root: ""
server: /server
server-plug: /plugin/%s
server-api: /api/v1/%s
server-initialize: /initialize
server-model: /model/%s
server-request: /model/%s/request/
server-router: /router/%s
server-service: /service/%s
web: /web/src
web-api: /api
web-form: /view
web-table: /view
web: web/src
root: "" # root 自动适配项目根目录, 请不要手动配置,他会在项目加载的时候识别出根路径
server: /lckt-server
module: 'git.echol.cn/loser/lckt'
ai-path: "https://ai.gin-vue-admin.com/{FUNC}/loser7659/c178e970-6a59-497d-96ed-86fee6b3285a" # AI服务路径
# qiniu configuration (请自行七牛申请对应的 公钥 私钥 bucket 和 域名地址)
qiniu:
@@ -149,14 +194,24 @@ qiniu:
secret-key: ""
use-cdn-domains: false
# aliyun oss configuration
aliyun-oss:
# minio oss configuration
minio:
endpoint: yourEndpoint
access-key-id: yourAccessKeyId
access-key-secret: yourAccessKeySecret
bucket-name: yourBucketName
bucket-url: yourBucketUrl
base-path: yourBasePath
use-ssl: false
base-path: ""
bucket-url: "http://host:9000/yourBucketName"
# aliyun oss configuration
aliyun-oss:
endpoint: oss-cn-hangzhou.aliyuncs.com
access-key-id: LTAI5tB3Mn5Y7mVo8h3zkf46
access-key-secret: FtuHdFy4NFdVItEiNBnTun3Ddi8BMK
bucket-name: lckt
bucket-url: https://lckt.oss-cn-hangzhou.aliyuncs.com
base-path: lckt
# tencent cos configuration
tencent-cos:
@@ -179,6 +234,15 @@ aws-s3:
base-url: https://gin.vue.admin
path-prefix: git.echol.cn/loser/lckt
# cloudflare r2 configuration
cloudflare-r2:
bucket: xxxx0bucket
base-url: https://gin.vue.admin.com
path: uploads
account-id: xxx_account_id
access-key-id: xxx_key_id
secret-access-key: xxx_secret_key
# huawei obs configuration
hua-wei-obs:
path: you-path
@@ -191,30 +255,55 @@ hua-wei-obs:
excel:
dir: ./resource/excel/
# timer task db clear table
Timer:
start: true
spec: "@daily" # 定时任务详细配置参考 https://pkg.go.dev/github.com/robfig/cron/v3
detail:
- tableName: sys_operation_records
compareField: created_at
interval: 2160h
- tableName: jwt_blacklists
compareField: created_at
interval: 168h
# disk usage configuration
disk-list:
- mount-point: "/"
# 跨域配置
# 需要配合 server/initialize/router.go -> `Router.Use(middleware.CorsByRules())` 使用
cors:
mode: whitelist # 放行模式: allow-all, 放行全部; whitelist, 白名单模式, 来自白名单内域名的请求添加 cors 头; strict-whitelist 严格白名单模式, 白名单外的请求一律拒绝
mode: allow-all # 放行模式: allow-all, 放行全部; whitelist, 白名单模式, 来自白名单内域名的请求添加 cors 头; strict-whitelist 严格白名单模式, 白名单外的请求一律拒绝
whitelist:
- allow-origin: example1.com
allow-headers: content-type
allow-methods: GET, POST
allow-headers: Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id
allow-methods: POST, GET
expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type
allow-credentials: true # 布尔值
- allow-origin: example2.com
allow-headers: content-type
allow-methods: GET, POST
expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type
allow-credentials: true # 布尔值
allow-credentials: true # 布尔值
mcp:
name: GVA_MCP
version: v1.0.0
sse_path: /sse
message_path: /message
url_prefix: ''
wechat:
app-id: wx3d21df18d7f8f9fc
app-secret: 3ab19e9b6a5e155c25ac6457be650047
token: kjeldcsdz2phfwfxnevsajnzsboho1ev
aes-key: PiqqlGdEblw5Gv1RJ5qcTnhKUjFw9YNkBMAX6CIw6Me
callback: https://api.gin-vue-admin.com/wechat/callback
pay-list:
- type: wxpay
alias-name: wxpay-1
app-id: wx3d21df18d7f8f9fc
mch-id: 1646874753
v3-key: 1a3sd8561d5179Df152D4789aD38wG9s
cert-path: /resource/cert/apiclient_cert.pem
key-path: /resource/cert/apiclient_key.pem
notify-url: http://lckt.hnlc5588.cn/app_order/notify
serial-no: 59A891FB403EC7A1CF2090DB9C8EC704BD43B101
- type: wxpay2
alias-name: wxpay-2
app-id: 2
mch-id: 2
v3-key: 2
cert-path: 2
key-path: 2
notify-url: 2
serial-no: 2

View File

@@ -6,3 +6,8 @@ type GetTeacherVipList struct {
common.PageInfo
TeacherId uint `json:"teacher_id" form:"teacher_id"` // 讲师ID
}
type UpdateTeacherVipPriceBatch struct {
Ids []uint `json:"ids" form:"ids" vd:"@:len($)>0; msg:'请选择要修改的讲师VIP'"`
Price float64 `json:"price" form:"price" vd:"@:len($)>0; msg:'请输入讲师VIP价格'"` // 讲师VIP价格
}

View File

@@ -4,12 +4,13 @@ import "git.echol.cn/loser/lckt/global"
type TeacherVip struct {
global.GVA_MODEL
Title string `json:"title" form:"title" gorm:"comment:VIP标题;size:128"`
TeacherId uint `json:"teacher_id" gorm:"comment:讲师ID;"`
TeacherName string `json:"teacher_name" gorm:"comment:讲师名称"` // 讲师名称
Avatar string `json:"avatar" gorm:"comment:讲师头像"`
Price int `json:"price" gorm:"comment:VIP价格(单位为分)"`
Desc string `json:"desc" gorm:"comment:VIP描述;type:longtext"`
Title string `json:"title" form:"title" gorm:"comment:VIP标题;size:128"`
TeacherId uint `json:"teacher_id" gorm:"comment:讲师ID;"`
TeacherName string `json:"teacher_name" gorm:"comment:讲师名称"` // 讲师名称
Avatar string `json:"avatar" gorm:"comment:讲师头像"`
Price float64 `json:"price" gorm:"comment:VIP价格(单位为分)"`
Desc string `json:"desc" gorm:"comment:VIP描述;type:longtext"`
CategoryId uint `json:"category_id" gorm:"comment:分类ID;"`
}
func (TeacherVip) TableName() string {

View File

@@ -22,4 +22,5 @@ type TeacherInfo struct {
Avatar string `json:"avatar" form:"avatar"`
Des string `json:"des" form:"des"`
Follow int64 `json:"follow" form:"follow"` // 粉丝数
Weight int `json:"weight" form:"weight"` // 权重
}

View File

@@ -31,6 +31,7 @@ type ArticleVo struct {
TeacherAvatar string `json:"teacherAvatar" gorm:"comment:讲师头像"`
IsFree int `json:"isFree" gorm:"comment:是否免费;default:0"` // 是否免费 0-否 1-是
IsBuy int `json:"isBuy" gorm:"comment:是否购买;default:0"` // 是否购买 0-否 1-是
CategoryId uint `json:"categoryId" gorm:"comment:分类ID"`
}
type ArticleTeacherVo struct {

View File

@@ -90,3 +90,9 @@ type SetUserVipReq struct {
VipExpireTime string `json:"vip_expire_time" form:"vip_expire_time" vd:"@:len($)>0; msg:'会员过期时间不能为空'"`
UserLabel int64 `json:"user_label" form:"user_label" vd:"@:len($)>0; msg:'用户标签不能为空'"` // 1 普通用户 2 Vip 3 Svip
}
type SetTeacherInfo struct {
Id int `json:"id" form:"id" vd:"@:len($)>0; msg:'用户ID不能为空'"`
ExpectRate int `json:"expect_rate" form:"expect_rate"`
Weight int `json:"weight" form:"weight" `
}

View File

@@ -23,12 +23,15 @@ func (s *UserRouter) InitUserRouter(Router *gin.RouterGroup, PublicRouter *gin.R
userRouter.GET("/login/log", userApi.GetLoginLog) // 获取用户登录日志
userRouter.DELETE("/vip", userApi.RemoveUserVip) // 删除用户会员
userRouter.GET("/vip/list", userApi.GetUserVipList) // 获取用户会员列表
userRouter.PUT("/rate", userApi.SetTeacherExpectRate) // 设置讲师分成比例
userRouter.PUT("/weight", userApi.SetTeacherWeight) // 设置讲师权重
}
{
userRouter.GET("teacher_vips", teacherVipApi.GetTeacherVipList) // 获取讲师VIP列表
userRouter.GET("teacher_vip", teacherVipApi.GetTeacherVip) // 获取讲师VIP详情
userRouter.POST("teacher_vip", teacherVipApi.CreateTeacherVip) // 创建讲师VIP
userRouter.PUT("teacher_vip", teacherVipApi.Update) // 更新讲师VIP
userRouter.DELETE("teacher_vip", teacherVipApi.DeleteTeacherVip) // 删除讲师VIP
userRouter.GET("teacher_vips", teacherVipApi.GetTeacherVipList) // 获取讲师VIP列表
userRouter.GET("teacher_vip", teacherVipApi.GetTeacherVip) // 获取讲师VIP详情
userRouter.POST("teacher_vip", teacherVipApi.CreateTeacherVip) // 创建讲师VIP
userRouter.PUT("teacher_vip", teacherVipApi.Update) // 更新讲师VIP
userRouter.DELETE("teacher_vip", teacherVipApi.DeleteTeacherVip) // 删除讲师VIP
userRouter.PUT("/teacher_vip/price", teacherVipApi.UpdatePriceBatch) // 批量设置讲师课程价格
}
}

View File

@@ -10,7 +10,7 @@ type VipRouter struct{}
// InitVipRouter 初始化会员路由
func (s *VipRouter) InitVipRouter(Router *gin.RouterGroup, PublicRouter *gin.RouterGroup) {
vipRouter := Router.Group("vip").Use(middleware.OperationRecord())
vipNoAuthRouter := PublicRouter.Group("vip").Use(middleware.OperationRecord())
vipNoAuthRouter := PublicRouter.Group("vip")
{
vipRouter.POST("", vipApi.Create) // 获取用户列表
vipRouter.DELETE("", vipApi.Delete) // 更新用户余额

View File

@@ -167,14 +167,12 @@ func (s *OrderService) BalancePay(p request.BalancePay) error {
// 全站Vip
if order.OrderType == 2 {
// 更新用户的会员状态
user.IsVip = 1
// 查询用户购买的会员信息
vipInfo := vip.Vip{}
err = global.GVA_DB.Model(&vip.Vip{}).Where("id = ?", order.VipId).First(&vipInfo).Error
if err != nil {
global.GVA_LOG.Error("查询会员信息失败", zap.Error(err))
return nil
return err
}
// 计算会员的过期时间
if user.VipExpireTime != "" {
@@ -190,11 +188,22 @@ func (s *OrderService) BalancePay(p request.BalancePay) error {
// 如果没有会员时间,则从当前时间开始计算
user.VipExpireTime = time.Now().AddDate(0, 0, int(vipInfo.Expiration)).Format("2006-01-02")
}
// 更新用户的会员状态
user.IsVip = 1
if vipInfo.Level == 1 {
user.UserLabel = 2 // VIP
}
if vipInfo.Level == 2 {
user.UserLabel = 3 // SVIP
}
err = global.GVA_DB.Save(&user).Error
if err != nil {
global.GVA_LOG.Error("更新用户会员状态失败", zap.Error(err))
return nil
return err
}
return nil
}
// 讲师包月
if order.OrderType == 3 {
@@ -223,13 +232,14 @@ func (s *OrderService) BalancePay(p request.BalancePay) error {
return err
}
// 计算分成金额
amount := float64(order.Price) * float64(teacher.ExpectRate) / 100.0
amount := float64(order.Price/100) * float64(teacher.ExpectRate) / 100.0
teacher.Balance = teacher.Balance + amount
err = global.GVA_DB.Save(&teacher).Error
if err != nil {
global.GVA_LOG.Error("更新讲师余额失败", zap.Error(err))
return err
}
return nil
}
// 计算分成比例,按比例增加讲师余额
teacher := userM.User{}
@@ -239,7 +249,7 @@ func (s *OrderService) BalancePay(p request.BalancePay) error {
return err
}
// 计算分成金额
amount := float64(order.Price) * float64(teacher.ExpectRate) / 100.0
amount := float64(order.Price/100) * float64(teacher.ExpectRate) / 100.0
teacher.Balance = teacher.Balance + amount
err = global.GVA_DB.Save(&teacher).Error
if err != nil {

View File

@@ -97,4 +97,13 @@ func (u *TeacherVipService) GetTeacherVip(id int) (vip app.TeacherVip, err error
return vip, nil
}
func (u *TeacherVipService) UpdatePriceBatch(p request.UpdateTeacherVipPriceBatch) (err error) {
err = global.GVA_DB.Model(&app.TeacherVip{}).Where("id IN ?", p.Ids).Update("price", p.Price).Error
if err != nil {
global.GVA_LOG.Error("批量更新讲师VIP价格失败", zap.Error(err))
return
}
return
}
// ===========================管理后台接口===========================

View File

@@ -244,20 +244,40 @@ func (u *AppUserService) GetTeacherList(p common.PageInfo) (list []vo.TeacherInf
global.GVA_LOG.Error("查询教师总数失败", zap.Error(err))
return nil, 0, err
}
err = db.Limit(limit).Offset(offset).Select("id, nick_name, avatar,des").Find(&list).Error
err = db.Limit(limit).Offset(offset).Select("id, nick_name, avatar,des,weight").Find(&list).Error
if err != nil {
global.GVA_LOG.Error("查询教师列表失败", zap.Error(err))
return nil, 0, err
}
// 获取每个教师的粉丝数
for i := range list {
followCount, err := u.GetTeacherFansCount(list[i].ID)
// 批量查询所有教师的粉丝数
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))
global.GVA_LOG.Error("批量查询教师粉丝数失败", zap.Error(err))
return nil, 0, err
}
list[i].Follow = followCount
}
// 映射粉丝数
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

View File

@@ -55,7 +55,7 @@ func (ArticleService) GetArticleList(pageInfo request.GetList) (list []article.A
if err != nil {
return
}
err = db.Limit(limit).Offset(offset).Find(&list).Error
err = db.Limit(limit).Offset(offset).Order("created_at desc").Find(&list).Error
return
}
@@ -64,11 +64,8 @@ func (s ArticleService) APPGetArticleList(pageInfo request.GetList) (list []vo.A
offset := pageInfo.PageSize * (pageInfo.Page - 1)
db := global.GVA_DB.Model(&article.Article{}).Where("status = 1") // 只查询已发布的文章
err = db.Count(&total).Error
if err != nil {
return
}
// 优化:先构建所有筛选条件再查询总数和数据
if pageInfo.Title != "" {
db = db.Where("title LIKE ?", "%"+pageInfo.Title+"%")
}
@@ -78,14 +75,41 @@ func (s ArticleService) APPGetArticleList(pageInfo request.GetList) (list []vo.A
if pageInfo.TeacherId != 0 {
db = db.Where("teacher_id = ?", pageInfo.TeacherId)
}
if pageInfo.Keyword != "" {
db = db.Where("title LIKE ? OR article.desc LIKE ? OR teacher_name LIKE ?", "%"+pageInfo.Keyword+"%", "%"+pageInfo.Keyword+"%", "%"+pageInfo.Keyword+"%")
}
// 先查总数
err = db.Count(&total).Error
if err != nil {
return
}
// 批量查出teacher_id
err = db.Limit(limit).Offset(offset).Omit("teacher_avatar").Order("created_at desc").Find(&list).Error
if err != nil {
return
}
// 优化批量查询讲师头像避免N+1查询
teacherIds := make([]int, 0, len(list))
for _, a := range list {
teacherIds = append(teacherIds, a.TeacherId)
}
avatars := make(map[int]string)
if len(teacherIds) > 0 {
type avatarResult struct {
ID int
Avatar string
}
var results []avatarResult
global.GVA_DB.Table("app_user").Select("id, avatar").Where("id IN ?", teacherIds).Find(&results)
for _, r := range results {
avatars[r.ID] = r.Avatar
}
}
for i, a := range list {
global.GVA_DB.Table("app_user").Select("avatar").Where("id = ?", a.TeacherId).Scan(&list[i].TeacherAvatar)
list[i].TeacherAvatar = avatars[a.TeacherId]
}
return
@@ -113,9 +137,37 @@ func (s ArticleService) APPGetArticle(id string, userId int) (article *vo.Articl
// 如果count = 0 或者 TeacherId 不等于 userId表示用户没有购买过该文章
if count == 0 && article.TeacherId != userId {
// 用户没有购买过隐藏Content
article.Content = ""
article.IsBuy = 0 // 设置为未购买
// 判断用户是否已经包月该讲师和是否是讲师本人
if userId != 0 && userId != article.TeacherId {
var userTeacherVip []app.UserTeacherVip
err = global.GVA_DB.Model(&app.UserTeacherVip{}).Where("teacher_id = ? AND user_id = ? AND is_expire = 1", article.TeacherId, userId).Find(&userTeacherVip).Count(&count).Error
if err != nil {
global.GVA_LOG.Error("查询用户包月记录失败", zap.Error(err))
return nil, err
}
for _, vip := range userTeacherVip {
// 获取包月的信息 判断文章是否在包月范围内
var teacherVipInfo app.TeacherVip
err = global.GVA_DB.Model(&app.TeacherVip{}).Where("id = ?", vip.TeacherVipId).First(&teacherVipInfo).Error
if err != nil {
global.GVA_LOG.Error("查询讲师包月信息失败", zap.Error(err))
return nil, err
}
// 用户包月了,判断文章是否在包月范围内
if count != 0 {
if teacherVipInfo.CategoryId == article.CategoryId {
return
}
}
}
// 用户没有购买过隐藏Content
article.Content = ""
article.IsBuy = 0 // 设置为未购买
}
}
}
@@ -223,7 +275,7 @@ func (s ArticleService) BulkUpload(p request.BulkUpload) (err error) {
publishTime, _ := time.ParseInLocation("2006-01-02 15:04:05", p.PublishTime, loc)
articles = append(articles, article.Article{
Title: p.Title,
Title: teacher.NickName + "--" + p.Title,
Desc: p.Desc,
CategoryId: p.CategoryId,
TeacherId: int(teacher.ID),

View File

@@ -363,3 +363,23 @@ func (u *UserService) GetUserVipList(p request.GetUserListReq) (list []vo.UserIn
}
return
}
// SetTeacherWeight 设置讲师权重
func (u *UserService) SetTeacherWeight(p request.SetTeacherInfo) (err error) {
err = global.GVA_DB.Model(&user.User{}).Where("id = ?", p.Id).Update("weight", p.Weight).Error
if err != nil {
global.GVA_LOG.Error("设置讲师权重失败", zap.Error(err))
return
}
return
}
// SetTeacherExpectRate 设置讲师分成
func (u *UserService) SetTeacherExpectRate(p request.SetTeacherInfo) (err error) {
err = global.GVA_DB.Model(&user.User{}).Where("id = ?", p.Id).Update("expect_rate", p.ExpectRate).Error
if err != nil {
global.GVA_LOG.Error("设置讲师权重失败", zap.Error(err))
return
}
return
}

View File

@@ -0,0 +1,157 @@
package upload
// UploadOSS OSS对象存储接口
import (
"errors"
"fmt"
"github.com/aliyun/aliyun-oss-go-sdk/oss"
"io/ioutil"
"os"
"strings"
)
type Worker struct {
in chan string
done chan bool
}
type Uploads struct {
}
const endpoint = "oss-cn-hangzhou.aliyuncs.com" // oss endpoint
const accessKeyId = "LTAI5tB3Mn5Y7mVo8h3zkf46" // oss key
const accessKeySecret = "FtuHdFy4NFdVItEiNBnTun3Ddi8BMK" //oss secret
const bucketName = "lckt" //oss bucket名称
const objectName = "avatar/" //oss远程目标地址
const workerCount = 100 //设置最大并发数
const suffix = "" //筛选目录下需要上传的格式
// 上传单个文件
func (u *Uploads) UploadOne(path string) {
// 创建OSSClient实例。
localFileName := path
split := strings.Split(localFileName, `\`)
length := len(split)
fileName := split[length-1 : length]
client, err := oss.New(endpoint, accessKeyId, accessKeySecret)
if err != nil {
handleError(err)
}
// 获取存储空间。
bucket, err := client.Bucket(bucketName)
if err != nil {
handleError(err)
}
up(bucket, objectName+fileName[0], localFileName)
}
func createWorker(bucket *oss.Bucket, objectName string, id int) Worker {
w := Worker{
in: make(chan string),
done: make(chan bool),
}
go doWork(id, w.in, w.done, bucket, objectName)
return w
}
// 并发上传目录下所有文件
func (u *Uploads) UploadMany(path string) {
files, err := getAllFiles(path)
if err != nil {
handleError(err)
}
fileCount := len(files)
if fileCount == 0 {
handleError(errors.New("目录下没有指定文件,请重试!"))
}
client, err := oss.New(endpoint, accessKeyId, accessKeySecret)
if err != nil {
handleError(err)
}
bucket, err := client.Bucket(bucketName)
if err != nil {
handleError(err)
}
var workers [workerCount]Worker
for i := 0; i < fileCount; i++ {
workers[i] = createWorker(bucket, objectName, i)
}
for i := 0; i < fileCount; i++ {
workers[i].in <- files[i]
}
for i := 0; i < fileCount; i++ {
<-workers[i].done
}
}
func handleError(err error) {
fmt.Println("Error:", err)
os.Exit(-1)
}
func up(bucket *oss.Bucket, objectName string, localFileName string) {
// 上传文件。
err := bucket.PutObjectFromFile(objectName, localFileName)
if err != nil {
handleError(err)
}
}
func doWork(id int, c chan string, done chan bool, bucket *oss.Bucket, objectName string) {
for n := range c {
split := strings.Split(n, `\`)
length := len(split)
fileName := split[length-1 : length]
fmt.Printf("worker : %d, object: %s, uploading file %v \n", id, objectName, n)
up(bucket, objectName+fileName[0], n)
go func() { done <- true }()
}
}
// 获取指定目录下的所有文件,包含子目录下的文件
func getAllFiles(dirPth string) (files []string, err error) {
var dirs []string
dir, err := ioutil.ReadDir(dirPth)
if err != nil {
return nil, err
}
PthSep := string(os.PathSeparator)
for _, fi := range dir {
if fi.IsDir() { // 目录, 递归遍历
dirs = append(dirs, dirPth+PthSep+fi.Name())
getAllFiles(dirPth + PthSep + fi.Name())
} else {
// 过滤指定格式
if suffix != "" {
ok := strings.HasSuffix(fi.Name(), suffix)
if ok {
files = append(files, dirPth+PthSep+fi.Name())
}
} else {
files = append(files, dirPth+PthSep+fi.Name())
}
}
}
return files, nil
}
func InitOss() (*oss.Bucket, error) {
// 创建OSSClient实例。
AccessKeyId := "LTAI5tB3Mn5Y7mVo8h3zkf46"
AccessKeySecret := "FtuHdFy4NFdVItEiNBnTun3Ddi8BMK"
Endpoint := "oss-cn-hangzhou.aliyuncs.com"
BucketName := "lckt"
client, err := oss.New(Endpoint, AccessKeyId, AccessKeySecret)
if err != nil {
return nil, err
}
// 获取存储空间。
bucket, err := client.Bucket(BucketName)
if err != nil {
return nil, err
}
return bucket, nil
}

View File

@@ -219,7 +219,7 @@ func NotifyHandle(ctx *gin.Context) error {
return err
}
// 计算分成金额
amount := float64(order.Price) * float64(teacher.ExpectRate) / 100.0
amount := float64(order.Price/100) * float64(teacher.ExpectRate) / 100.0
teacher.Balance = teacher.Balance + amount
err = global.GVA_DB.Save(&teacher).Error
if err != nil {
@@ -256,6 +256,15 @@ func NotifyHandle(ctx *gin.Context) error {
// 如果没有会员时间,则从当前时间开始计算
userInfo.VipExpireTime = time.Now().AddDate(0, 0, int(vipInfo.Expiration)).Format("2006-01-02")
}
if vipInfo.Level == 1 {
userInfo.UserLabel = 2 // SVIP
}
if vipInfo.Level == 2 {
userInfo.UserLabel = 3 // SVIP
}
err = global.GVA_DB.Save(&userInfo).Error
if err != nil {
global.GVA_LOG.Error("更新用户会员状态失败", zap.Error(err))
@@ -287,7 +296,7 @@ func NotifyHandle(ctx *gin.Context) error {
return err
}
// 计算分成金额
amount := float64(order.Price) * float64(teacher.ExpectRate) / 100.0
amount := float64(order.Price/100) * float64(teacher.ExpectRate) / 100.0
teacher.Balance = teacher.Balance + amount
err = global.GVA_DB.Save(&teacher).Error
if err != nil {