第一版完成
This commit is contained in:
0
internal/service/.gitkeep
Normal file
0
internal/service/.gitkeep
Normal file
251
internal/service/admin.go
Normal file
251
internal/service/admin.go
Normal file
@@ -0,0 +1,251 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"cms-api/internal/dao"
|
||||
"cms-api/internal/model"
|
||||
|
||||
"github.com/gogf/gf/v2/crypto/gmd5"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sAdmin struct{}
|
||||
|
||||
func Admin() *sAdmin {
|
||||
return &sAdmin{}
|
||||
}
|
||||
|
||||
// Login 管理员登录
|
||||
func (s *sAdmin) Login(ctx context.Context, req *model.LoginRequest) (*model.LoginResponse, error) {
|
||||
// 查询管理员
|
||||
admin := &model.Admin{}
|
||||
err := dao.Admin.Ctx(ctx).Where("username", req.Username).Where("deleted_at", 0).Scan(admin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if admin.Id == 0 {
|
||||
return nil, gerror.New("用户名或密码错误")
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if !s.VerifyPassword(req.Password, admin.Password) {
|
||||
return nil, gerror.New("用户名或密码错误")
|
||||
}
|
||||
|
||||
// 检查状态
|
||||
if admin.Status != 1 {
|
||||
return nil, gerror.New("账号已被禁用")
|
||||
}
|
||||
|
||||
// 更新登录信息
|
||||
_, err = dao.Admin.Ctx(ctx).Where("id", admin.Id).Update(g.Map{
|
||||
"last_login_at": gtime.Now().Unix(),
|
||||
"last_login_ip": g.RequestFromCtx(ctx).GetClientIp(),
|
||||
"updated_at": gtime.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
g.Log().Error(ctx, "更新登录信息失败:", err)
|
||||
}
|
||||
|
||||
// 生成token
|
||||
token, err := Auth.GenerateToken(ctx, uint64(admin.Id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 清除密码字段
|
||||
admin.Password = ""
|
||||
|
||||
return &model.LoginResponse{
|
||||
Token: token,
|
||||
Admin: admin,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetById 根据ID获取管理员信息
|
||||
func (s *sAdmin) GetById(ctx context.Context, id int) (*model.Admin, error) {
|
||||
admin := &model.Admin{}
|
||||
err := dao.Admin.Ctx(ctx).Where("id", id).Where("deleted_at", 0).Scan(admin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if admin.Id == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
// 清除密码字段
|
||||
admin.Password = ""
|
||||
return admin, nil
|
||||
}
|
||||
|
||||
// List 获取管理员列表
|
||||
func (s *sAdmin) List(ctx context.Context, req *model.ListRequest) (*model.PageResponse, error) {
|
||||
var (
|
||||
page = req.Page
|
||||
pageSize = req.PageSize
|
||||
keyword = req.Keyword
|
||||
)
|
||||
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
query := dao.Admin.Ctx(ctx).Where("deleted_at", 0)
|
||||
|
||||
// 关键词搜索
|
||||
if keyword != "" {
|
||||
query = query.WhereOr("username LIKE ?", "%"+keyword+"%").
|
||||
WhereOr("real_name LIKE ?", "%"+keyword+"%").
|
||||
WhereOr("email LIKE ?", "%"+keyword+"%")
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
if req.Status >= 0 {
|
||||
query = query.Where("status", req.Status)
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
total, err := query.Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取列表
|
||||
var admins []*model.Admin
|
||||
err = query.Page(page, pageSize).OrderDesc("id").Scan(&admins)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 清除密码字段
|
||||
for _, admin := range admins {
|
||||
admin.Password = ""
|
||||
}
|
||||
|
||||
return &model.PageResponse{
|
||||
List: admins,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: (total + pageSize - 1) / pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Create 创建管理员
|
||||
func (s *sAdmin) Create(ctx context.Context, req *model.CreateAdminRequest) error {
|
||||
// 检查用户名是否存在
|
||||
count, err := dao.Admin.Ctx(ctx).Where("username", req.Username).Where("deleted_at", 0).Count()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return gerror.New("用户名已存在")
|
||||
}
|
||||
|
||||
// 检查邮箱是否存在
|
||||
count, err = dao.Admin.Ctx(ctx).Where("email", req.Email).Where("deleted_at", 0).Count()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return gerror.New("邮箱已存在")
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
hashedPassword := s.HashPassword(req.Password)
|
||||
|
||||
// 创建管理员
|
||||
_, err = dao.Admin.Ctx(ctx).Insert(g.Map{
|
||||
"username": req.Username,
|
||||
"password": hashedPassword,
|
||||
"real_name": req.RealName,
|
||||
"email": req.Email,
|
||||
"phone": req.Phone,
|
||||
"role_id": req.RoleId,
|
||||
"status": 1,
|
||||
"created_at": gtime.Now(),
|
||||
"updated_at": gtime.Now(),
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Update 更新管理员
|
||||
func (s *sAdmin) Update(ctx context.Context, id int, req *model.UpdateAdminRequest) error {
|
||||
// 检查管理员是否存在
|
||||
admin, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if admin == nil {
|
||||
return gerror.New("管理员不存在")
|
||||
}
|
||||
|
||||
// 检查邮箱是否被其他管理员使用
|
||||
count, err := dao.Admin.Ctx(ctx).Where("email", req.Email).Where("id !=", id).Where("deleted_at", 0).Count()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return gerror.New("邮箱已被使用")
|
||||
}
|
||||
|
||||
// 更新管理员
|
||||
_, err = dao.Admin.Ctx(ctx).Where("id", id).Update(g.Map{
|
||||
"real_name": req.RealName,
|
||||
"email": req.Email,
|
||||
"phone": req.Phone,
|
||||
"role_id": req.RoleId,
|
||||
"status": req.Status,
|
||||
"updated_at": gtime.Now(),
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete 删除管理员
|
||||
func (s *sAdmin) Delete(ctx context.Context, id int) error {
|
||||
// 检查管理员是否存在
|
||||
admin, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if admin == nil {
|
||||
return gerror.New("管理员不存在")
|
||||
}
|
||||
|
||||
// 软删除
|
||||
_, err = dao.Admin.Ctx(ctx).Where("id", id).Update(g.Map{
|
||||
"deleted_at": gtime.Now().Unix(),
|
||||
"updated_at": gtime.Now(),
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// HashPassword 加密密码
|
||||
func (s *sAdmin) HashPassword(password string) string {
|
||||
return gmd5.MustEncrypt(password + "cms_salt_2024")
|
||||
}
|
||||
|
||||
// VerifyPassword 验证密码
|
||||
func (s *sAdmin) VerifyPassword(password, hashedPassword string) bool {
|
||||
return gmd5.MustEncrypt(password+"cms_salt_2024") == hashedPassword
|
||||
}
|
||||
|
||||
// Profile 获取当前管理员信息
|
||||
func (s *sAdmin) Profile(ctx context.Context) (*model.Admin, error) {
|
||||
adminId := gconv.Int(g.RequestFromCtx(ctx).GetCtxVar("admin_id"))
|
||||
return s.GetById(ctx, adminId)
|
||||
}
|
||||
|
||||
// UpdateProfile 更新当前管理员信息
|
||||
func (s *sAdmin) UpdateProfile(ctx context.Context, req *model.UpdateAdminRequest) error {
|
||||
adminId := gconv.Int(g.RequestFromCtx(ctx).GetCtxVar("admin_id"))
|
||||
return s.Update(ctx, adminId, req)
|
||||
}
|
||||
355
internal/service/article.go
Normal file
355
internal/service/article.go
Normal file
@@ -0,0 +1,355 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"cms-api/internal/dao"
|
||||
"cms-api/internal/model"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sArticle struct{}
|
||||
|
||||
func Article() *sArticle {
|
||||
return &sArticle{}
|
||||
}
|
||||
|
||||
// GetById 根据ID获取文章信息
|
||||
func (s *sArticle) GetById(ctx context.Context, id int) (*model.Article, error) {
|
||||
article, err := dao.Article.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if article != nil {
|
||||
// 增加浏览次数
|
||||
go func() {
|
||||
dao.Article.IncrementViewCount(context.Background(), id)
|
||||
}()
|
||||
}
|
||||
return article, nil
|
||||
}
|
||||
|
||||
// GetBySlug 根据URL别名获取文章
|
||||
func (s *sArticle) GetBySlug(ctx context.Context, slug string) (*model.Article, error) {
|
||||
article, err := dao.Article.GetBySlug(ctx, slug)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if article != nil {
|
||||
// 增加浏览次数
|
||||
go func() {
|
||||
dao.Article.IncrementViewCount(context.Background(), article.Id)
|
||||
}()
|
||||
}
|
||||
return article, nil
|
||||
}
|
||||
|
||||
// List 获取文章列表
|
||||
func (s *sArticle) List(ctx context.Context, req *model.ArticleListRequest) (*model.PageResponse, error) {
|
||||
var (
|
||||
page = req.Page
|
||||
pageSize = req.PageSize
|
||||
)
|
||||
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
// 设置分页参数
|
||||
req.Page = page
|
||||
req.PageSize = pageSize
|
||||
|
||||
// 获取列表
|
||||
articles, total, err := dao.Article.List(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.PageResponse{
|
||||
List: articles,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: (total + pageSize - 1) / pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Create 创建文章
|
||||
func (s *sArticle) Create(ctx context.Context, req *model.CreateArticleRequest) error {
|
||||
// 检查URL别名是否存在
|
||||
if req.Slug != "" {
|
||||
existArticle, err := dao.Article.GetBySlug(ctx, req.Slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existArticle != nil {
|
||||
return gerror.New("URL别名已存在")
|
||||
}
|
||||
} else {
|
||||
// 如果没有提供slug,则根据标题生成
|
||||
req.Slug = s.generateSlug(req.Title)
|
||||
}
|
||||
|
||||
// 获取当前用户ID
|
||||
authorId := gconv.Int(g.RequestFromCtx(ctx).GetCtxVar("admin_id"))
|
||||
if authorId == 0 {
|
||||
authorId = gconv.Int(g.RequestFromCtx(ctx).GetCtxVar("user_id"))
|
||||
}
|
||||
|
||||
// 处理HTML内容(如果是Markdown,需要转换)
|
||||
htmlContent := req.Content
|
||||
if req.Content != "" {
|
||||
// 这里可以添加Markdown到HTML的转换逻辑
|
||||
htmlContent = s.markdownToHTML(req.Content)
|
||||
}
|
||||
|
||||
// 创建文章
|
||||
article := &model.Article{
|
||||
Title: req.Title,
|
||||
Slug: req.Slug,
|
||||
Summary: req.Summary,
|
||||
Content: req.Content,
|
||||
HtmlContent: htmlContent,
|
||||
CoverImage: req.CoverImage,
|
||||
CategoryId: req.CategoryId,
|
||||
Tags: req.Tags,
|
||||
AuthorId: authorId,
|
||||
IsPublished: req.IsPublished,
|
||||
IsFeatured: req.IsFeatured,
|
||||
IsTop: req.IsTop,
|
||||
SeoTitle: req.SeoTitle,
|
||||
SeoDescription: req.SeoDescription,
|
||||
SeoKeywords: req.SeoKeywords,
|
||||
CreatedAt: gtime.Now(),
|
||||
UpdatedAt: gtime.Now(),
|
||||
}
|
||||
|
||||
if req.IsPublished == 1 {
|
||||
article.PublishedAt = gtime.Now()
|
||||
}
|
||||
|
||||
_, err := dao.Article.Create(ctx, article)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update 更新文章
|
||||
func (s *sArticle) Update(ctx context.Context, id int, req *model.UpdateArticleRequest) error {
|
||||
// 检查文章是否存在
|
||||
article, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if article == nil {
|
||||
return gerror.New("文章不存在")
|
||||
}
|
||||
|
||||
// 检查URL别名是否被其他文章使用
|
||||
if req.Slug != "" && req.Slug != article.Slug {
|
||||
existArticle, err := dao.Article.GetBySlug(ctx, req.Slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existArticle != nil && existArticle.Id != id {
|
||||
return gerror.New("URL别名已被使用")
|
||||
}
|
||||
}
|
||||
|
||||
// 处理HTML内容
|
||||
htmlContent := req.Content
|
||||
if req.Content != "" {
|
||||
htmlContent = s.markdownToHTML(req.Content)
|
||||
}
|
||||
|
||||
// 更新文章
|
||||
updateData := g.Map{
|
||||
"title": req.Title,
|
||||
"slug": req.Slug,
|
||||
"summary": req.Summary,
|
||||
"content": req.Content,
|
||||
"html_content": htmlContent,
|
||||
"cover_image": req.CoverImage,
|
||||
"category_id": req.CategoryId,
|
||||
"tags": req.Tags,
|
||||
"is_published": req.IsPublished,
|
||||
"is_featured": req.IsFeatured,
|
||||
"is_top": req.IsTop,
|
||||
"seo_title": req.SeoTitle,
|
||||
"seo_description": req.SeoDescription,
|
||||
"seo_keywords": req.SeoKeywords,
|
||||
"updated_at": gtime.Now(),
|
||||
}
|
||||
|
||||
// 如果从草稿变为发布状态,设置发布时间
|
||||
if req.IsPublished == 1 && article.IsPublished == 0 {
|
||||
updateData["published_at"] = gtime.Now()
|
||||
}
|
||||
|
||||
return dao.Article.Update(ctx, id, updateData)
|
||||
}
|
||||
|
||||
// Delete 删除文章
|
||||
func (s *sArticle) Delete(ctx context.Context, id int) error {
|
||||
// 检查文章是否存在
|
||||
article, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if article == nil {
|
||||
return gerror.New("文章不存在")
|
||||
}
|
||||
|
||||
return dao.Article.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// UpdateStatus 更新文章状态
|
||||
func (s *sArticle) UpdateStatus(ctx context.Context, id int, isPublished int) error {
|
||||
// 检查文章是否存在
|
||||
article, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if article == nil {
|
||||
return gerror.New("文章不存在")
|
||||
}
|
||||
|
||||
updateData := g.Map{
|
||||
"is_published": isPublished,
|
||||
"updated_at": gtime.Now(),
|
||||
}
|
||||
|
||||
// 如果是发布状态,设置发布时间
|
||||
if isPublished == 1 && article.IsPublished == 0 {
|
||||
updateData["published_at"] = gtime.Now()
|
||||
}
|
||||
|
||||
return dao.Article.Update(ctx, id, updateData)
|
||||
}
|
||||
|
||||
// SetFeatured 设置推荐状态
|
||||
func (s *sArticle) SetFeatured(ctx context.Context, id int, isFeatured int) error {
|
||||
// 检查文章是否存在
|
||||
article, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if article == nil {
|
||||
return gerror.New("文章不存在")
|
||||
}
|
||||
|
||||
return dao.Article.Update(ctx, id, g.Map{
|
||||
"is_featured": isFeatured,
|
||||
"updated_at": gtime.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// SetTop 设置置顶状态
|
||||
func (s *sArticle) SetTop(ctx context.Context, id int, isTop int) error {
|
||||
// 检查文章是否存在
|
||||
article, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if article == nil {
|
||||
return gerror.New("文章不存在")
|
||||
}
|
||||
|
||||
return dao.Article.Update(ctx, id, g.Map{
|
||||
"is_top": isTop,
|
||||
"updated_at": gtime.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// GetStats 获取文章统计信息
|
||||
func (s *sArticle) GetStats(ctx context.Context) (map[string]interface{}, error) {
|
||||
// 获取总文章数
|
||||
totalCount, err := dao.Article.GetCount(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取已发布文章数
|
||||
publishedCount, err := dao.Article.GetPublishedCount(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取草稿数
|
||||
draftCount := totalCount - publishedCount
|
||||
|
||||
// 获取推荐文章数
|
||||
featuredCount, err := dao.Article.GetFeaturedCount(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_count": totalCount,
|
||||
"published_count": publishedCount,
|
||||
"draft_count": draftCount,
|
||||
"featured_count": featuredCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetFeatured 获取推荐文章
|
||||
func (s *sArticle) GetFeatured(ctx context.Context, limit int) ([]*model.Article, error) {
|
||||
if limit <= 0 {
|
||||
limit = 5
|
||||
}
|
||||
return dao.Article.GetFeatured(ctx, limit)
|
||||
}
|
||||
|
||||
// GetLatest 获取最新文章
|
||||
func (s *sArticle) GetLatest(ctx context.Context, limit int) ([]*model.Article, error) {
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
return dao.Article.GetLatest(ctx, limit)
|
||||
}
|
||||
|
||||
// generateSlug 生成URL别名
|
||||
func (s *sArticle) generateSlug(title string) string {
|
||||
// 简单的slug生成逻辑,实际项目中可能需要更复杂的处理
|
||||
slug := strings.ToLower(title)
|
||||
slug = strings.ReplaceAll(slug, " ", "-")
|
||||
slug = strings.ReplaceAll(slug, " ", "-") // 全角空格
|
||||
// 移除特殊字符,只保留字母、数字、中文和连字符
|
||||
// 这里简化处理,实际项目中可能需要更完善的slug生成逻辑
|
||||
return slug
|
||||
}
|
||||
|
||||
// markdownToHTML 将Markdown转换为HTML
|
||||
func (s *sArticle) markdownToHTML(markdown string) string {
|
||||
// 这里应该使用Markdown解析库,如github.com/russross/blackfriday
|
||||
// 暂时直接返回原内容,实际项目中需要实现Markdown解析
|
||||
return markdown
|
||||
}
|
||||
|
||||
// Search 搜索文章
|
||||
func (s *sArticle) Search(ctx context.Context, keyword string, page, pageSize int) (*model.PageResponse, error) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
articles, total, err := dao.Article.Search(ctx, keyword, page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.PageResponse{
|
||||
List: articles,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: (total + pageSize - 1) / pageSize,
|
||||
}, nil
|
||||
}
|
||||
353
internal/service/attachment.go
Normal file
353
internal/service/attachment.go
Normal file
@@ -0,0 +1,353 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"cms-api/internal/dao"
|
||||
"cms-api/internal/model"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/crypto/gmd5"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"github.com/gogf/gf/v2/util/grand"
|
||||
)
|
||||
|
||||
type sAttachment struct{}
|
||||
|
||||
func Attachment() *sAttachment {
|
||||
return &sAttachment{}
|
||||
}
|
||||
|
||||
// Upload 上传文件
|
||||
func (s *sAttachment) Upload(ctx context.Context, file *ghttp.UploadFile, uploadType string) (*model.Attachment, error) {
|
||||
// 检查文件大小
|
||||
maxSize := int64(10 * 1024 * 1024) // 10MB
|
||||
if file.Size > maxSize {
|
||||
return nil, gerror.New("文件大小不能超过10MB")
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if !s.isAllowedFileType(file.Filename) {
|
||||
return nil, gerror.New("不支持的文件类型")
|
||||
}
|
||||
|
||||
// 生成文件名和路径
|
||||
fileName, filePath, fileUrl := s.generateFilePath(file.Filename, uploadType)
|
||||
|
||||
// 确保目录存在
|
||||
dir := filepath.Dir(filePath)
|
||||
if !gfile.Exists(dir) {
|
||||
if err := gfile.Mkdir(dir); err != nil {
|
||||
return nil, gerror.New("创建目录失败")
|
||||
}
|
||||
}
|
||||
|
||||
// 保存文件
|
||||
if _, err := file.Save(filePath, true); err != nil {
|
||||
return nil, gerror.New("保存文件失败: " + err.Error())
|
||||
}
|
||||
|
||||
// 获取上传者ID
|
||||
uploadedBy := gconv.Int(g.RequestFromCtx(ctx).GetCtxVar("admin_id"))
|
||||
if uploadedBy == 0 {
|
||||
uploadedBy = gconv.Int(g.RequestFromCtx(ctx).GetCtxVar("user_id"))
|
||||
}
|
||||
|
||||
// 获取客户端IP
|
||||
uploadIp := g.RequestFromCtx(ctx).GetClientIp()
|
||||
|
||||
// 创建附件记录
|
||||
attachment := &model.Attachment{
|
||||
OriginalName: file.Filename,
|
||||
FileName: fileName,
|
||||
FilePath: filePath,
|
||||
FileUrl: fileUrl,
|
||||
FileSize: file.Size,
|
||||
FileType: s.getFileType(file.Filename),
|
||||
MimeType: s.getMimeType(file.Filename),
|
||||
FileExt: s.getFileExt(file.Filename),
|
||||
StorageType: "local",
|
||||
UploadIp: uploadIp,
|
||||
UploadedBy: uploadedBy,
|
||||
UsageCount: 0,
|
||||
CreatedAt: gtime.Now(),
|
||||
UpdatedAt: gtime.Now(),
|
||||
}
|
||||
|
||||
id, err := dao.Attachment.Create(ctx, attachment)
|
||||
if err != nil {
|
||||
// 如果数据库保存失败,删除已上传的文件
|
||||
os.Remove(filePath)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
attachment.Id = int(id)
|
||||
return attachment, nil
|
||||
}
|
||||
|
||||
// GetById 根据ID获取附件信息
|
||||
func (s *sAttachment) GetById(ctx context.Context, id int) (*model.Attachment, error) {
|
||||
return dao.Attachment.GetById(ctx, id)
|
||||
}
|
||||
|
||||
// List 获取附件列表
|
||||
func (s *sAttachment) List(ctx context.Context, req *model.AttachmentListRequest) (*model.PageResponse, error) {
|
||||
var (
|
||||
page = req.Page
|
||||
pageSize = req.PageSize
|
||||
)
|
||||
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
// 设置分页参数
|
||||
req.Page = page
|
||||
req.PageSize = pageSize
|
||||
|
||||
// 获取列表
|
||||
attachments, total, err := dao.Attachment.List(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.PageResponse{
|
||||
List: attachments,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: (total + pageSize - 1) / pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Delete 删除附件
|
||||
func (s *sAttachment) Delete(ctx context.Context, id int) error {
|
||||
// 检查附件是否存在
|
||||
attachment, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if attachment == nil {
|
||||
return gerror.New("附件不存在")
|
||||
}
|
||||
|
||||
// 删除物理文件
|
||||
if gfile.Exists(attachment.FilePath) {
|
||||
if err := os.Remove(attachment.FilePath); err != nil {
|
||||
g.Log().Error(ctx, "删除物理文件失败:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除数据库记录
|
||||
return dao.Attachment.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// UpdateUsageCount 更新使用次数
|
||||
func (s *sAttachment) UpdateUsageCount(ctx context.Context, id int) error {
|
||||
return dao.Attachment.IncrementUsageCount(ctx, id)
|
||||
}
|
||||
|
||||
// GetStats 获取附件统计信息
|
||||
func (s *sAttachment) GetStats(ctx context.Context) (map[string]interface{}, error) {
|
||||
// 获取总附件数
|
||||
totalCount, err := dao.Attachment.GetCount(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取总文件大小
|
||||
totalSize, err := dao.Attachment.GetTotalSize(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取各类型文件数量
|
||||
imageCount, err := dao.Attachment.GetCountByType(ctx, "image")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
documentCount, err := dao.Attachment.GetCountByType(ctx, "document")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
videoCount, err := dao.Attachment.GetCountByType(ctx, "video")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_count": totalCount,
|
||||
"total_size": totalSize,
|
||||
"image_count": imageCount,
|
||||
"document_count": documentCount,
|
||||
"video_count": videoCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// isAllowedFileType 检查文件类型是否允许
|
||||
func (s *sAttachment) isAllowedFileType(filename string) bool {
|
||||
allowedExts := []string{
|
||||
".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", // 图片
|
||||
".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".txt", // 文档
|
||||
".mp4", ".avi", ".mov", ".wmv", ".flv", ".mkv", // 视频
|
||||
".mp3", ".wav", ".flac", ".aac", // 音频
|
||||
".zip", ".rar", ".7z", ".tar", ".gz", // 压缩包
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
for _, allowedExt := range allowedExts {
|
||||
if ext == allowedExt {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// generateFilePath 生成文件路径
|
||||
func (s *sAttachment) generateFilePath(originalName, uploadType string) (string, string, string) {
|
||||
// 获取文件扩展名
|
||||
ext := filepath.Ext(originalName)
|
||||
|
||||
// 生成唯一文件名
|
||||
hash := gmd5.MustEncrypt(fmt.Sprintf("%s_%d_%s", originalName, time.Now().UnixNano(), grand.S(8)))
|
||||
fileName := hash + ext
|
||||
|
||||
// 根据日期创建目录结构
|
||||
now := time.Now()
|
||||
dateDir := fmt.Sprintf("%d/%02d/%02d", now.Year(), now.Month(), now.Day())
|
||||
|
||||
// 根据上传类型创建子目录
|
||||
if uploadType == "" {
|
||||
uploadType = "general"
|
||||
}
|
||||
|
||||
// 构建完整路径
|
||||
relativePath := fmt.Sprintf("uploads/%s/%s/%s", uploadType, dateDir, fileName)
|
||||
fullPath := filepath.Join("storage", relativePath)
|
||||
fileUrl := "/" + strings.ReplaceAll(relativePath, "\\", "/")
|
||||
|
||||
return fileName, fullPath, fileUrl
|
||||
}
|
||||
|
||||
// getFileType 获取文件类型
|
||||
func (s *sAttachment) getFileType(filename string) string {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
|
||||
imageExts := []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"}
|
||||
documentExts := []string{".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".txt"}
|
||||
videoExts := []string{".mp4", ".avi", ".mov", ".wmv", ".flv", ".mkv"}
|
||||
audioExts := []string{".mp3", ".wav", ".flac", ".aac"}
|
||||
archiveExts := []string{".zip", ".rar", ".7z", ".tar", ".gz"}
|
||||
|
||||
for _, imageExt := range imageExts {
|
||||
if ext == imageExt {
|
||||
return "image"
|
||||
}
|
||||
}
|
||||
|
||||
for _, docExt := range documentExts {
|
||||
if ext == docExt {
|
||||
return "document"
|
||||
}
|
||||
}
|
||||
|
||||
for _, videoExt := range videoExts {
|
||||
if ext == videoExt {
|
||||
return "video"
|
||||
}
|
||||
}
|
||||
|
||||
for _, audioExt := range audioExts {
|
||||
if ext == audioExt {
|
||||
return "audio"
|
||||
}
|
||||
}
|
||||
|
||||
for _, archiveExt := range archiveExts {
|
||||
if ext == archiveExt {
|
||||
return "archive"
|
||||
}
|
||||
}
|
||||
|
||||
return "other"
|
||||
}
|
||||
|
||||
// getMimeType 获取MIME类型
|
||||
func (s *sAttachment) getMimeType(filename string) string {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
|
||||
mimeTypes := map[string]string{
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".gif": "image/gif",
|
||||
".bmp": "image/bmp",
|
||||
".webp": "image/webp",
|
||||
".pdf": "application/pdf",
|
||||
".doc": "application/msword",
|
||||
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
".xls": "application/vnd.ms-excel",
|
||||
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
".ppt": "application/vnd.ms-powerpoint",
|
||||
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
".txt": "text/plain",
|
||||
".mp4": "video/mp4",
|
||||
".avi": "video/x-msvideo",
|
||||
".mov": "video/quicktime",
|
||||
".wmv": "video/x-ms-wmv",
|
||||
".flv": "video/x-flv",
|
||||
".mkv": "video/x-matroska",
|
||||
".mp3": "audio/mpeg",
|
||||
".wav": "audio/wav",
|
||||
".flac": "audio/flac",
|
||||
".aac": "audio/aac",
|
||||
".zip": "application/zip",
|
||||
".rar": "application/x-rar-compressed",
|
||||
".7z": "application/x-7z-compressed",
|
||||
".tar": "application/x-tar",
|
||||
".gz": "application/gzip",
|
||||
}
|
||||
|
||||
if mimeType, exists := mimeTypes[ext]; exists {
|
||||
return mimeType
|
||||
}
|
||||
return "application/octet-stream"
|
||||
}
|
||||
|
||||
// getFileExt 获取文件扩展名
|
||||
func (s *sAttachment) getFileExt(filename string) string {
|
||||
ext := filepath.Ext(filename)
|
||||
if len(ext) > 0 {
|
||||
return ext[1:] // 去掉点号
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// BatchDelete 批量删除附件
|
||||
func (s *sAttachment) BatchDelete(ctx context.Context, ids []int) error {
|
||||
for _, id := range ids {
|
||||
if err := s.Delete(ctx, id); err != nil {
|
||||
g.Log().Error(ctx, "批量删除附件失败, ID:", id, "错误:", err)
|
||||
// 继续删除其他文件,不中断整个过程
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByIds 根据ID列表获取附件
|
||||
func (s *sAttachment) GetByIds(ctx context.Context, ids []int) ([]*model.Attachment, error) {
|
||||
return dao.Attachment.GetByIds(ctx, ids)
|
||||
}
|
||||
113
internal/service/auth.go
Normal file
113
internal/service/auth.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
type sAuth struct{}
|
||||
|
||||
var Auth = sAuth{}
|
||||
|
||||
// Claims JWT声明
|
||||
type Claims struct {
|
||||
UserId uint64 `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// GenerateToken 生成JWT Token
|
||||
func (s *sAuth) GenerateToken(ctx context.Context, userId uint64) (string, error) {
|
||||
// 获取JWT配置
|
||||
jwtConfig := g.Cfg().MustGet(ctx, "jwt")
|
||||
signingKey := jwtConfig.Map()["signingKey"].(string)
|
||||
expire := gconv.Int64(jwtConfig.Map()["expire"])
|
||||
|
||||
// 创建声明
|
||||
claims := Claims{
|
||||
UserId: userId,
|
||||
Username: "", // 将在后续获取
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expire) * time.Second)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: "cms-api",
|
||||
Subject: gconv.String(userId),
|
||||
},
|
||||
}
|
||||
|
||||
// 创建token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
// 签名token
|
||||
tokenString, err := token.SignedString([]byte(signingKey))
|
||||
if err != nil {
|
||||
return "", gerror.New("生成Token失败")
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// ParseToken 解析JWT Token
|
||||
func (s *sAuth) ParseToken(ctx context.Context, tokenString string) (*Claims, error) {
|
||||
// 获取JWT配置
|
||||
jwtConfig := g.Cfg().MustGet(ctx, "jwt")
|
||||
signingKey := jwtConfig.Map()["signingKey"].(string)
|
||||
|
||||
// 解析token
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(signingKey), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, gerror.New("Token解析失败")
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, gerror.New("Token无效")
|
||||
}
|
||||
|
||||
// ValidateToken 验证Token
|
||||
func (s *sAuth) ValidateToken(ctx context.Context, tokenString string) (uint64, error) {
|
||||
claims, err := s.ParseToken(ctx, tokenString)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// 检查管理员是否存在且状态正常
|
||||
admin, err := Admin().GetById(ctx, int(claims.UserId))
|
||||
if err != nil {
|
||||
return 0, gerror.New("管理员不存在")
|
||||
}
|
||||
|
||||
if admin == nil || admin.Status != 1 {
|
||||
return 0, gerror.New("管理员已被禁用")
|
||||
}
|
||||
|
||||
return claims.UserId, nil
|
||||
}
|
||||
|
||||
// RefreshToken 刷新Token
|
||||
func (s *sAuth) RefreshToken(ctx context.Context, tokenString string) (string, error) {
|
||||
claims, err := s.ParseToken(ctx, tokenString)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 生成新的Token
|
||||
return s.GenerateToken(ctx, claims.UserId)
|
||||
}
|
||||
|
||||
// GetTokenExpire 获取Token过期时间
|
||||
func (s *sAuth) GetTokenExpire(ctx context.Context) int {
|
||||
jwtConfig := g.Cfg().MustGet(ctx, "jwt")
|
||||
return gconv.Int(jwtConfig.Map()["expire"])
|
||||
}
|
||||
413
internal/service/config.go
Normal file
413
internal/service/config.go
Normal file
@@ -0,0 +1,413 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"cms-api/internal/dao"
|
||||
"cms-api/internal/model"
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sConfig struct{}
|
||||
|
||||
func Config() *sConfig {
|
||||
return &sConfig{}
|
||||
}
|
||||
|
||||
// GetByKey 根据配置键获取配置
|
||||
func (s *sConfig) GetByKey(ctx context.Context, key string) (*model.SiteConfig, error) {
|
||||
return dao.Config.GetByKey(ctx, key)
|
||||
}
|
||||
|
||||
// GetValue 获取配置值
|
||||
func (s *sConfig) GetValue(ctx context.Context, key string, defaultValue ...interface{}) interface{} {
|
||||
config, err := s.GetByKey(ctx, key)
|
||||
if err != nil || config == nil {
|
||||
if len(defaultValue) > 0 {
|
||||
return defaultValue[0]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 根据配置类型转换值
|
||||
switch config.ConfigType {
|
||||
case "number":
|
||||
return gconv.Int(config.ConfigValue)
|
||||
case "boolean":
|
||||
return gconv.Bool(config.ConfigValue)
|
||||
case "json", "array":
|
||||
var result interface{}
|
||||
if err := json.Unmarshal([]byte(config.ConfigValue), &result); err == nil {
|
||||
return result
|
||||
}
|
||||
return config.ConfigValue
|
||||
default:
|
||||
return config.ConfigValue
|
||||
}
|
||||
}
|
||||
|
||||
// GetString 获取字符串配置值
|
||||
func (s *sConfig) GetString(ctx context.Context, key string, defaultValue ...string) string {
|
||||
value := s.GetValue(ctx, key)
|
||||
if value == nil {
|
||||
if len(defaultValue) > 0 {
|
||||
return defaultValue[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return gconv.String(value)
|
||||
}
|
||||
|
||||
// GetInt 获取整数配置值
|
||||
func (s *sConfig) GetInt(ctx context.Context, key string, defaultValue ...int) int {
|
||||
value := s.GetValue(ctx, key)
|
||||
if value == nil {
|
||||
if len(defaultValue) > 0 {
|
||||
return defaultValue[0]
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return gconv.Int(value)
|
||||
}
|
||||
|
||||
// GetBool 获取布尔配置值
|
||||
func (s *sConfig) GetBool(ctx context.Context, key string, defaultValue ...bool) bool {
|
||||
value := s.GetValue(ctx, key)
|
||||
if value == nil {
|
||||
if len(defaultValue) > 0 {
|
||||
return defaultValue[0]
|
||||
}
|
||||
return false
|
||||
}
|
||||
return gconv.Bool(value)
|
||||
}
|
||||
|
||||
// List 获取配置列表
|
||||
func (s *sConfig) List(ctx context.Context, req *model.ConfigListRequest) (*model.PageResponse, error) {
|
||||
var (
|
||||
page = req.Page
|
||||
pageSize = req.PageSize
|
||||
)
|
||||
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
// 设置分页参数
|
||||
req.Page = page
|
||||
req.PageSize = pageSize
|
||||
|
||||
// 获取列表
|
||||
configs, total, err := dao.Config.List(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.PageResponse{
|
||||
List: configs,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: (total + pageSize - 1) / pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetByGroup 根据分组获取配置
|
||||
func (s *sConfig) GetByGroup(ctx context.Context, groupName string) ([]*model.SiteConfig, error) {
|
||||
return dao.Config.GetByGroup(ctx, groupName)
|
||||
}
|
||||
|
||||
// GetGroups 获取所有配置分组
|
||||
func (s *sConfig) GetGroups(ctx context.Context) ([]string, error) {
|
||||
return dao.Config.GetGroups(ctx)
|
||||
}
|
||||
|
||||
// Create 创建配置
|
||||
func (s *sConfig) Create(ctx context.Context, req *model.CreateConfigRequest) error {
|
||||
// 检查配置键是否存在
|
||||
existConfig, err := dao.Config.GetByKey(ctx, req.ConfigKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existConfig != nil {
|
||||
return gerror.New("配置键已存在")
|
||||
}
|
||||
|
||||
// 验证配置值格式
|
||||
if err := s.validateConfigValue(req.ConfigValue, req.ConfigType); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 创建配置
|
||||
config := &model.SiteConfig{
|
||||
ConfigKey: req.ConfigKey,
|
||||
ConfigValue: req.ConfigValue,
|
||||
ConfigType: req.ConfigType,
|
||||
GroupName: req.GroupName,
|
||||
Description: req.Description,
|
||||
SortOrder: req.SortOrder,
|
||||
IsSystem: req.IsSystem,
|
||||
CreatedAt: gtime.Now(),
|
||||
UpdatedAt: gtime.Now(),
|
||||
}
|
||||
|
||||
_, err = dao.Config.Create(ctx, config)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update 更新配置
|
||||
func (s *sConfig) Update(ctx context.Context, key string, req *model.UpdateConfigRequest) error {
|
||||
// 检查配置是否存在
|
||||
config, err := s.GetByKey(ctx, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if config == nil {
|
||||
return gerror.New("配置不存在")
|
||||
}
|
||||
|
||||
// 验证配置值格式
|
||||
if err := s.validateConfigValue(req.ConfigValue, req.ConfigType); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新配置
|
||||
updateData := g.Map{
|
||||
"config_value": req.ConfigValue,
|
||||
"config_type": req.ConfigType,
|
||||
"group_name": req.GroupName,
|
||||
"description": req.Description,
|
||||
"sort_order": req.SortOrder,
|
||||
"is_system": req.IsSystem,
|
||||
"updated_at": gtime.Now(),
|
||||
}
|
||||
|
||||
return dao.Config.UpdateByKey(ctx, key, updateData)
|
||||
}
|
||||
|
||||
// UpdateValue 更新配置值
|
||||
func (s *sConfig) UpdateValue(ctx context.Context, key string, value interface{}) error {
|
||||
// 检查配置是否存在
|
||||
config, err := s.GetByKey(ctx, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if config == nil {
|
||||
return gerror.New("配置不存在")
|
||||
}
|
||||
|
||||
// 转换值为字符串
|
||||
var valueStr string
|
||||
switch config.ConfigType {
|
||||
case "json", "array":
|
||||
if jsonBytes, err := json.Marshal(value); err == nil {
|
||||
valueStr = string(jsonBytes)
|
||||
} else {
|
||||
return gerror.New("配置值格式错误")
|
||||
}
|
||||
default:
|
||||
valueStr = gconv.String(value)
|
||||
}
|
||||
|
||||
// 验证配置值格式
|
||||
if err := s.validateConfigValue(valueStr, config.ConfigType); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新配置值
|
||||
return dao.Config.UpdateValue(ctx, key, valueStr)
|
||||
}
|
||||
|
||||
// BatchUpdate 批量更新配置
|
||||
func (s *sConfig) BatchUpdate(ctx context.Context, req *model.BatchUpdateConfigRequest) error {
|
||||
for key, value := range req.Configs {
|
||||
if err := s.UpdateValue(ctx, key, value); err != nil {
|
||||
g.Log().Error(ctx, "批量更新配置失败, 键:", key, "错误:", err)
|
||||
// 继续更新其他配置,不中断整个过程
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除配置
|
||||
func (s *sConfig) Delete(ctx context.Context, key string) error {
|
||||
// 检查配置是否存在
|
||||
config, err := s.GetByKey(ctx, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if config == nil {
|
||||
return gerror.New("配置不存在")
|
||||
}
|
||||
|
||||
// 检查是否为系统配置
|
||||
if config.IsSystem == 1 {
|
||||
return gerror.New("系统配置不能删除")
|
||||
}
|
||||
|
||||
return dao.Config.DeleteByKey(ctx, key)
|
||||
}
|
||||
|
||||
// InitDefaultConfigs 初始化默认配置
|
||||
func (s *sConfig) InitDefaultConfigs(ctx context.Context) error {
|
||||
defaultConfigs := []model.SiteConfig{
|
||||
{
|
||||
ConfigKey: "site_name",
|
||||
ConfigValue: "企业官网CMS系统",
|
||||
ConfigType: "string",
|
||||
GroupName: "基础设置",
|
||||
Description: "网站名称",
|
||||
SortOrder: 1,
|
||||
IsSystem: 1,
|
||||
},
|
||||
{
|
||||
ConfigKey: "site_title",
|
||||
ConfigValue: "企业官网CMS系统 - 专业的内容管理平台",
|
||||
ConfigType: "string",
|
||||
GroupName: "基础设置",
|
||||
Description: "网站标题",
|
||||
SortOrder: 2,
|
||||
IsSystem: 1,
|
||||
},
|
||||
{
|
||||
ConfigKey: "site_description",
|
||||
ConfigValue: "专业的企业官网内容管理系统,提供完整的内容发布和管理功能",
|
||||
ConfigType: "string",
|
||||
GroupName: "基础设置",
|
||||
Description: "网站描述",
|
||||
SortOrder: 3,
|
||||
IsSystem: 1,
|
||||
},
|
||||
{
|
||||
ConfigKey: "site_keywords",
|
||||
ConfigValue: "企业官网,CMS,内容管理,新闻发布",
|
||||
ConfigType: "string",
|
||||
GroupName: "基础设置",
|
||||
Description: "网站关键词",
|
||||
SortOrder: 4,
|
||||
IsSystem: 1,
|
||||
},
|
||||
{
|
||||
ConfigKey: "contact_phone",
|
||||
ConfigValue: "400-123-4567",
|
||||
ConfigType: "string",
|
||||
GroupName: "联系方式",
|
||||
Description: "联系电话",
|
||||
SortOrder: 1,
|
||||
IsSystem: 0,
|
||||
},
|
||||
{
|
||||
ConfigKey: "contact_email",
|
||||
ConfigValue: "contact@example.com",
|
||||
ConfigType: "string",
|
||||
GroupName: "联系方式",
|
||||
Description: "联系邮箱",
|
||||
SortOrder: 2,
|
||||
IsSystem: 0,
|
||||
},
|
||||
{
|
||||
ConfigKey: "contact_address",
|
||||
ConfigValue: "北京市朝阳区xxx大厦xxx室",
|
||||
ConfigType: "string",
|
||||
GroupName: "联系方式",
|
||||
Description: "联系地址",
|
||||
SortOrder: 3,
|
||||
IsSystem: 0,
|
||||
},
|
||||
{
|
||||
ConfigKey: "upload_max_size",
|
||||
ConfigValue: "10485760",
|
||||
ConfigType: "number",
|
||||
GroupName: "上传设置",
|
||||
Description: "最大上传文件大小(字节)",
|
||||
SortOrder: 1,
|
||||
IsSystem: 1,
|
||||
},
|
||||
{
|
||||
ConfigKey: "allowed_file_types",
|
||||
ConfigValue: `["jpg","jpeg","png","gif","pdf","doc","docx","xls","xlsx"]`,
|
||||
ConfigType: "array",
|
||||
GroupName: "上传设置",
|
||||
Description: "允许上传的文件类型",
|
||||
SortOrder: 2,
|
||||
IsSystem: 1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, config := range defaultConfigs {
|
||||
// 检查配置是否已存在
|
||||
existConfig, err := dao.Config.GetByKey(ctx, config.ConfigKey)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if existConfig != nil {
|
||||
continue // 跳过已存在的配置
|
||||
}
|
||||
|
||||
// 创建配置
|
||||
config.CreatedAt = gtime.Now()
|
||||
config.UpdatedAt = gtime.Now()
|
||||
dao.Config.Create(ctx, &config)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateConfigValue 验证配置值格式
|
||||
func (s *sConfig) validateConfigValue(value, configType string) error {
|
||||
switch configType {
|
||||
case "number":
|
||||
if _, err := strconv.ParseFloat(value, 64); err != nil {
|
||||
return gerror.New("配置值必须是数字")
|
||||
}
|
||||
case "boolean":
|
||||
if value != "true" && value != "false" && value != "1" && value != "0" {
|
||||
return gerror.New("配置值必须是布尔值")
|
||||
}
|
||||
case "json", "array":
|
||||
var temp interface{}
|
||||
if err := json.Unmarshal([]byte(value), &temp); err != nil {
|
||||
return gerror.New("配置值必须是有效的JSON格式")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isNumeric 检查字符串是否为数字
|
||||
func (s *sConfig) isNumeric(str string) bool {
|
||||
matched, _ := regexp.MatchString(`^-?\d+(\.\d+)?$`, str)
|
||||
return matched
|
||||
}
|
||||
|
||||
// GetPublicConfigs 获取公开配置(前台可访问)
|
||||
func (s *sConfig) GetPublicConfigs(ctx context.Context) (map[string]interface{}, error) {
|
||||
// 定义公开配置键
|
||||
publicKeys := []string{
|
||||
"site_name",
|
||||
"site_title",
|
||||
"site_description",
|
||||
"site_keywords",
|
||||
"contact_phone",
|
||||
"contact_email",
|
||||
"contact_address",
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
for _, key := range publicKeys {
|
||||
value := s.GetValue(ctx, key)
|
||||
if value != nil {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
286
internal/service/contact.go
Normal file
286
internal/service/contact.go
Normal file
@@ -0,0 +1,286 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"cms-api/internal/dao"
|
||||
"cms-api/internal/model"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sContact struct{}
|
||||
|
||||
func Contact() *sContact {
|
||||
return &sContact{}
|
||||
}
|
||||
|
||||
// GetById 根据ID获取联系信息
|
||||
func (s *sContact) GetById(ctx context.Context, id int) (*model.Contact, error) {
|
||||
return dao.Contact.GetById(ctx, id)
|
||||
}
|
||||
|
||||
// List 获取联系信息列表
|
||||
func (s *sContact) List(ctx context.Context, req *model.ContactListRequest) (*model.PageResponse, error) {
|
||||
var (
|
||||
page = req.Page
|
||||
pageSize = req.PageSize
|
||||
)
|
||||
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
// 设置分页参数
|
||||
req.Page = page
|
||||
req.PageSize = pageSize
|
||||
|
||||
// 获取列表
|
||||
contacts, total, err := dao.Contact.List(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.PageResponse{
|
||||
List: contacts,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: (total + pageSize - 1) / pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Create 创建联系信息(前台提交表单)
|
||||
func (s *sContact) Create(ctx context.Context, req *model.CreateContactRequest) error {
|
||||
// 获取客户端信息
|
||||
request := g.RequestFromCtx(ctx)
|
||||
ip := request.GetClientIp()
|
||||
userAgent := request.Header.Get("User-Agent")
|
||||
|
||||
// 创建联系信息
|
||||
contact := &model.Contact{
|
||||
Name: req.Name,
|
||||
Email: req.Email,
|
||||
Phone: req.Phone,
|
||||
Company: req.Company,
|
||||
Subject: req.Subject,
|
||||
Message: req.Message,
|
||||
Ip: ip,
|
||||
UserAgent: userAgent,
|
||||
Status: 0, // 未处理
|
||||
CreatedAt: gtime.Now(),
|
||||
UpdatedAt: gtime.Now(),
|
||||
}
|
||||
|
||||
_, err := dao.Contact.Create(ctx, contact)
|
||||
return err
|
||||
}
|
||||
|
||||
// Reply 回复联系信息
|
||||
func (s *sContact) Reply(ctx context.Context, id int, req *model.ReplyContactRequest) error {
|
||||
// 检查联系信息是否存在
|
||||
contact, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if contact == nil {
|
||||
return gerror.New("联系信息不存在")
|
||||
}
|
||||
|
||||
// 获取回复人ID
|
||||
repliedBy := gconv.Int(g.RequestFromCtx(ctx).GetCtxVar("admin_id"))
|
||||
if repliedBy == 0 {
|
||||
return gerror.New("未获取到管理员信息")
|
||||
}
|
||||
|
||||
// 更新回复信息
|
||||
updateData := g.Map{
|
||||
"reply": req.Reply,
|
||||
"status": 2, // 已回复
|
||||
"replied_at": gtime.Now(),
|
||||
"replied_by": repliedBy,
|
||||
"updated_at": gtime.Now(),
|
||||
}
|
||||
|
||||
return dao.Contact.Update(ctx, id, updateData)
|
||||
}
|
||||
|
||||
// UpdateStatus 更新处理状态
|
||||
func (s *sContact) UpdateStatus(ctx context.Context, id int, status int) error {
|
||||
// 检查联系信息是否存在
|
||||
contact, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if contact == nil {
|
||||
return gerror.New("联系信息不存在")
|
||||
}
|
||||
|
||||
updateData := g.Map{
|
||||
"status": status,
|
||||
"updated_at": gtime.Now(),
|
||||
}
|
||||
|
||||
// 如果状态改为已处理但没有回复,只更新状态
|
||||
if status == 1 && contact.Status == 0 {
|
||||
// 已处理状态
|
||||
}
|
||||
|
||||
return dao.Contact.Update(ctx, id, updateData)
|
||||
}
|
||||
|
||||
// Delete 删除联系信息
|
||||
func (s *sContact) Delete(ctx context.Context, id int) error {
|
||||
// 检查联系信息是否存在
|
||||
contact, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if contact == nil {
|
||||
return gerror.New("联系信息不存在")
|
||||
}
|
||||
|
||||
return dao.Contact.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// GetStats 获取联系信息统计
|
||||
func (s *sContact) GetStats(ctx context.Context) (map[string]interface{}, error) {
|
||||
// 获取总联系信息数
|
||||
totalCount, err := dao.Contact.GetCount(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取各状态联系信息数
|
||||
unprocessedCount, err := dao.Contact.GetCountByStatus(ctx, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
processedCount, err := dao.Contact.GetCountByStatus(ctx, 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
repliedCount, err := dao.Contact.GetCountByStatus(ctx, 2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取今日新增联系信息数
|
||||
todayCount, err := dao.Contact.GetTodayCount(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取本周新增联系信息数
|
||||
weekCount, err := dao.Contact.GetWeekCount(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取本月新增联系信息数
|
||||
monthCount, err := dao.Contact.GetMonthCount(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_count": totalCount,
|
||||
"unprocessed_count": unprocessedCount,
|
||||
"processed_count": processedCount,
|
||||
"replied_count": repliedCount,
|
||||
"today_count": todayCount,
|
||||
"week_count": weekCount,
|
||||
"month_count": monthCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetLatest 获取最新联系信息
|
||||
func (s *sContact) GetLatest(ctx context.Context, limit int) ([]*model.Contact, error) {
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
return dao.Contact.GetLatest(ctx, limit)
|
||||
}
|
||||
|
||||
// GetUnprocessed 获取未处理的联系信息
|
||||
func (s *sContact) GetUnprocessed(ctx context.Context, limit int) ([]*model.Contact, error) {
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
return dao.Contact.GetUnprocessed(ctx, limit)
|
||||
}
|
||||
|
||||
// BatchUpdateStatus 批量更新状态
|
||||
func (s *sContact) BatchUpdateStatus(ctx context.Context, ids []int, status int) error {
|
||||
for _, id := range ids {
|
||||
if err := s.UpdateStatus(ctx, id, status); err != nil {
|
||||
g.Log().Error(ctx, "批量更新联系信息状态失败, ID:", id, "错误:", err)
|
||||
// 继续更新其他记录,不中断整个过程
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchDelete 批量删除联系信息
|
||||
func (s *sContact) BatchDelete(ctx context.Context, ids []int) error {
|
||||
for _, id := range ids {
|
||||
if err := s.Delete(ctx, id); err != nil {
|
||||
g.Log().Error(ctx, "批量删除联系信息失败, ID:", id, "错误:", err)
|
||||
// 继续删除其他记录,不中断整个过程
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Search 搜索联系信息
|
||||
func (s *sContact) Search(ctx context.Context, keyword string, page, pageSize int) (*model.PageResponse, error) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
contacts, total, err := dao.Contact.Search(ctx, keyword, page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.PageResponse{
|
||||
List: contacts,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: (total + pageSize - 1) / pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Export 导出联系信息
|
||||
func (s *sContact) Export(ctx context.Context, req *model.ContactListRequest) ([]*model.Contact, error) {
|
||||
// 设置大的页面大小来获取所有数据
|
||||
req.Page = 1
|
||||
req.PageSize = 10000
|
||||
|
||||
contacts, _, err := dao.Contact.List(ctx, req)
|
||||
return contacts, err
|
||||
}
|
||||
|
||||
// GetContactTrends 获取联系信息趋势数据
|
||||
func (s *sContact) GetContactTrends(ctx context.Context, days int) ([]map[string]interface{}, error) {
|
||||
if days <= 0 {
|
||||
days = 7 // 默认7天
|
||||
}
|
||||
return dao.Contact.GetTrends(ctx, days)
|
||||
}
|
||||
|
||||
// GetTrends 获取联系信息趋势数据(兼容性方法)
|
||||
func (s *sContact) GetTrends(ctx context.Context, days int) ([]map[string]interface{}, error) {
|
||||
return s.GetContactTrends(ctx, days)
|
||||
}
|
||||
355
internal/service/news.go
Normal file
355
internal/service/news.go
Normal file
@@ -0,0 +1,355 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"cms-api/internal/dao"
|
||||
"cms-api/internal/model"
|
||||
"strings"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type sNews struct{}
|
||||
|
||||
func News() *sNews {
|
||||
return &sNews{}
|
||||
}
|
||||
|
||||
// GetById 根据ID获取新闻信息
|
||||
func (s *sNews) GetById(ctx context.Context, id int) (*model.News, error) {
|
||||
news, err := dao.News.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if news != nil {
|
||||
// 增加浏览次数
|
||||
go func() {
|
||||
dao.News.IncrementViewCount(context.Background(), id)
|
||||
}()
|
||||
}
|
||||
return news, nil
|
||||
}
|
||||
|
||||
// GetBySlug 根据URL别名获取新闻
|
||||
func (s *sNews) GetBySlug(ctx context.Context, slug string) (*model.News, error) {
|
||||
news, err := dao.News.GetBySlug(ctx, slug)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if news != nil {
|
||||
// 增加浏览次数
|
||||
go func() {
|
||||
dao.News.IncrementViewCount(context.Background(), news.Id)
|
||||
}()
|
||||
}
|
||||
return news, nil
|
||||
}
|
||||
|
||||
// List 获取新闻列表
|
||||
func (s *sNews) List(ctx context.Context, req *model.NewsListRequest) (*model.PageResponse, error) {
|
||||
var (
|
||||
page = req.Page
|
||||
pageSize = req.PageSize
|
||||
)
|
||||
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
// 设置分页参数
|
||||
req.Page = page
|
||||
req.PageSize = pageSize
|
||||
|
||||
// 获取列表
|
||||
newsList, total, err := dao.News.List(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.PageResponse{
|
||||
List: newsList,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: (total + pageSize - 1) / pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Create 创建新闻
|
||||
func (s *sNews) Create(ctx context.Context, req *model.CreateNewsRequest) error {
|
||||
// 检查URL别名是否存在
|
||||
if req.Slug != "" {
|
||||
existNews, err := dao.News.GetBySlug(ctx, req.Slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existNews != nil {
|
||||
return gerror.New("URL别名已存在")
|
||||
}
|
||||
} else {
|
||||
// 如果没有提供slug,则根据标题生成
|
||||
req.Slug = s.generateSlug(req.Title)
|
||||
}
|
||||
|
||||
// 创建新闻
|
||||
news := &model.News{
|
||||
Title: req.Title,
|
||||
Slug: req.Slug,
|
||||
Summary: req.Summary,
|
||||
Content: req.Content,
|
||||
CoverImage: req.CoverImage,
|
||||
Category: req.Category,
|
||||
Source: req.Source,
|
||||
Author: req.Author,
|
||||
IsPublished: req.IsPublished,
|
||||
IsFeatured: req.IsFeatured,
|
||||
IsTop: req.IsTop,
|
||||
CreatedAt: gtime.Now(),
|
||||
UpdatedAt: gtime.Now(),
|
||||
}
|
||||
|
||||
if req.IsPublished == 1 {
|
||||
news.PublishedAt = gtime.Now()
|
||||
}
|
||||
|
||||
_, err := dao.News.Create(ctx, news)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update 更新新闻
|
||||
func (s *sNews) Update(ctx context.Context, id int, req *model.UpdateNewsRequest) error {
|
||||
// 检查新闻是否存在
|
||||
news, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if news == nil {
|
||||
return gerror.New("新闻不存在")
|
||||
}
|
||||
|
||||
// 检查URL别名是否被其他新闻使用
|
||||
if req.Slug != "" && req.Slug != news.Slug {
|
||||
existNews, err := dao.News.GetBySlug(ctx, req.Slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existNews != nil && existNews.Id != id {
|
||||
return gerror.New("URL别名已被使用")
|
||||
}
|
||||
}
|
||||
|
||||
// 更新新闻
|
||||
updateData := g.Map{
|
||||
"title": req.Title,
|
||||
"slug": req.Slug,
|
||||
"summary": req.Summary,
|
||||
"content": req.Content,
|
||||
"cover_image": req.CoverImage,
|
||||
"category": req.Category,
|
||||
"source": req.Source,
|
||||
"author": req.Author,
|
||||
"is_published": req.IsPublished,
|
||||
"is_featured": req.IsFeatured,
|
||||
"is_top": req.IsTop,
|
||||
"updated_at": gtime.Now(),
|
||||
}
|
||||
|
||||
// 如果从草稿变为发布状态,设置发布时间
|
||||
if req.IsPublished == 1 && news.IsPublished == 0 {
|
||||
updateData["published_at"] = gtime.Now()
|
||||
}
|
||||
|
||||
return dao.News.Update(ctx, id, updateData)
|
||||
}
|
||||
|
||||
// Delete 删除新闻
|
||||
func (s *sNews) Delete(ctx context.Context, id int) error {
|
||||
// 检查新闻是否存在
|
||||
news, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if news == nil {
|
||||
return gerror.New("新闻不存在")
|
||||
}
|
||||
|
||||
return dao.News.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// UpdateStatus 更新新闻状态
|
||||
func (s *sNews) UpdateStatus(ctx context.Context, id int, isPublished int) error {
|
||||
// 检查新闻是否存在
|
||||
news, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if news == nil {
|
||||
return gerror.New("新闻不存在")
|
||||
}
|
||||
|
||||
updateData := g.Map{
|
||||
"is_published": isPublished,
|
||||
"updated_at": gtime.Now(),
|
||||
}
|
||||
|
||||
// 如果是发布状态,设置发布时间
|
||||
if isPublished == 1 && news.IsPublished == 0 {
|
||||
updateData["published_at"] = gtime.Now()
|
||||
}
|
||||
|
||||
return dao.News.Update(ctx, id, updateData)
|
||||
}
|
||||
|
||||
// SetFeatured 设置推荐状态
|
||||
func (s *sNews) SetFeatured(ctx context.Context, id int, isFeatured int) error {
|
||||
// 检查新闻是否存在
|
||||
news, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if news == nil {
|
||||
return gerror.New("新闻不存在")
|
||||
}
|
||||
|
||||
return dao.News.Update(ctx, id, g.Map{
|
||||
"is_featured": isFeatured,
|
||||
"updated_at": gtime.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// SetTop 设置置顶状态
|
||||
func (s *sNews) SetTop(ctx context.Context, id int, isTop int) error {
|
||||
// 检查新闻是否存在
|
||||
news, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if news == nil {
|
||||
return gerror.New("新闻不存在")
|
||||
}
|
||||
|
||||
return dao.News.Update(ctx, id, g.Map{
|
||||
"is_top": isTop,
|
||||
"updated_at": gtime.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// GetStats 获取新闻统计信息
|
||||
func (s *sNews) GetStats(ctx context.Context) (map[string]interface{}, error) {
|
||||
// 获取总新闻数
|
||||
totalCount, err := dao.News.GetCount(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取已发布新闻数
|
||||
publishedCount, err := dao.News.GetPublishedCount(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取草稿数
|
||||
draftCount := totalCount - publishedCount
|
||||
|
||||
// 获取推荐新闻数
|
||||
featuredCount, err := dao.News.GetFeaturedCount(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取各分类新闻数
|
||||
companyNewsCount, err := dao.News.GetCountByCategory(ctx, "company_news")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
industryNewsCount, err := dao.News.GetCountByCategory(ctx, "industry_news")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_count": totalCount,
|
||||
"published_count": publishedCount,
|
||||
"draft_count": draftCount,
|
||||
"featured_count": featuredCount,
|
||||
"company_news_count": companyNewsCount,
|
||||
"industry_news_count": industryNewsCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetFeatured 获取推荐新闻
|
||||
func (s *sNews) GetFeatured(ctx context.Context, limit int) ([]*model.News, error) {
|
||||
if limit <= 0 {
|
||||
limit = 5
|
||||
}
|
||||
return dao.News.GetFeatured(ctx, limit)
|
||||
}
|
||||
|
||||
// GetLatest 获取最新新闻
|
||||
func (s *sNews) GetLatest(ctx context.Context, limit int) ([]*model.News, error) {
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
return dao.News.GetLatest(ctx, limit)
|
||||
}
|
||||
|
||||
// GetByCategory 根据分类获取新闻
|
||||
func (s *sNews) GetByCategory(ctx context.Context, category string, page, pageSize int) (*model.PageResponse, error) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
newsList, total, err := dao.News.GetByCategory(ctx, category, page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.PageResponse{
|
||||
List: newsList,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: (total + pageSize - 1) / pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateSlug 生成URL别名
|
||||
func (s *sNews) generateSlug(title string) string {
|
||||
// 简单的slug生成逻辑
|
||||
slug := strings.ToLower(title)
|
||||
slug = strings.ReplaceAll(slug, " ", "-")
|
||||
slug = strings.ReplaceAll(slug, " ", "-") // 全角空格
|
||||
return slug
|
||||
}
|
||||
|
||||
// Search 搜索新闻
|
||||
func (s *sNews) Search(ctx context.Context, keyword string, page, pageSize int) (*model.PageResponse, error) {
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
newsList, total, err := dao.News.Search(ctx, keyword, page, pageSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.PageResponse{
|
||||
List: newsList,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: (total + pageSize - 1) / pageSize,
|
||||
}, nil
|
||||
}
|
||||
264
internal/service/partner.go
Normal file
264
internal/service/partner.go
Normal file
@@ -0,0 +1,264 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"cms-api/internal/dao"
|
||||
"cms-api/internal/model"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type sPartner struct{}
|
||||
|
||||
func Partner() *sPartner {
|
||||
return &sPartner{}
|
||||
}
|
||||
|
||||
// GetById 根据ID获取合作伙伴信息
|
||||
func (s *sPartner) GetById(ctx context.Context, id int) (*model.Partner, error) {
|
||||
return dao.Partner.GetById(ctx, id)
|
||||
}
|
||||
|
||||
// List 获取合作伙伴列表
|
||||
func (s *sPartner) List(ctx context.Context, req *model.PartnerListRequest) (*model.PageResponse, error) {
|
||||
var (
|
||||
page = req.Page
|
||||
pageSize = req.PageSize
|
||||
)
|
||||
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
// 设置分页参数
|
||||
req.Page = page
|
||||
req.PageSize = pageSize
|
||||
|
||||
// 获取列表
|
||||
partners, total, err := dao.Partner.List(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.PageResponse{
|
||||
List: partners,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: (total + pageSize - 1) / pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAll 获取所有启用的合作伙伴
|
||||
func (s *sPartner) GetAll(ctx context.Context) ([]*model.Partner, error) {
|
||||
return dao.Partner.GetAll(ctx)
|
||||
}
|
||||
|
||||
// GetFeatured 获取推荐合作伙伴
|
||||
func (s *sPartner) GetFeatured(ctx context.Context, limit int) ([]*model.Partner, error) {
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
return dao.Partner.GetFeatured(ctx, limit)
|
||||
}
|
||||
|
||||
// GetByCategory 根据分类获取合作伙伴
|
||||
func (s *sPartner) GetByCategory(ctx context.Context, category string) ([]*model.Partner, error) {
|
||||
return dao.Partner.GetByCategory(ctx, category)
|
||||
}
|
||||
|
||||
// Create 创建合作伙伴
|
||||
func (s *sPartner) Create(ctx context.Context, req *model.CreatePartnerRequest) error {
|
||||
// 检查合作伙伴名称是否存在
|
||||
existPartner, err := dao.Partner.GetByName(ctx, req.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existPartner != nil {
|
||||
return gerror.New("合作伙伴名称已存在")
|
||||
}
|
||||
|
||||
// 创建合作伙伴
|
||||
partner := &model.Partner{
|
||||
Name: req.Name,
|
||||
Logo: req.Logo,
|
||||
Website: req.Website,
|
||||
Description: req.Description,
|
||||
Category: req.Category,
|
||||
SortOrder: req.SortOrder,
|
||||
IsFeatured: req.IsFeatured,
|
||||
Status: req.Status,
|
||||
CreatedAt: gtime.Now(),
|
||||
UpdatedAt: gtime.Now(),
|
||||
}
|
||||
|
||||
_, err = dao.Partner.Create(ctx, partner)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update 更新合作伙伴
|
||||
func (s *sPartner) Update(ctx context.Context, id int, req *model.UpdatePartnerRequest) error {
|
||||
// 检查合作伙伴是否存在
|
||||
partner, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if partner == nil {
|
||||
return gerror.New("合作伙伴不存在")
|
||||
}
|
||||
|
||||
// 检查合作伙伴名称是否被其他记录使用
|
||||
existPartner, err := dao.Partner.GetByName(ctx, req.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existPartner != nil && existPartner.Id != id {
|
||||
return gerror.New("合作伙伴名称已被使用")
|
||||
}
|
||||
|
||||
// 更新合作伙伴
|
||||
updateData := g.Map{
|
||||
"name": req.Name,
|
||||
"logo": req.Logo,
|
||||
"website": req.Website,
|
||||
"description": req.Description,
|
||||
"category": req.Category,
|
||||
"sort_order": req.SortOrder,
|
||||
"is_featured": req.IsFeatured,
|
||||
"status": req.Status,
|
||||
"updated_at": gtime.Now(),
|
||||
}
|
||||
|
||||
return dao.Partner.Update(ctx, id, updateData)
|
||||
}
|
||||
|
||||
// Delete 删除合作伙伴
|
||||
func (s *sPartner) Delete(ctx context.Context, id int) error {
|
||||
// 检查合作伙伴是否存在
|
||||
partner, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if partner == nil {
|
||||
return gerror.New("合作伙伴不存在")
|
||||
}
|
||||
|
||||
return dao.Partner.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// UpdateStatus 更新合作伙伴状态
|
||||
func (s *sPartner) UpdateStatus(ctx context.Context, id int, status int) error {
|
||||
// 检查合作伙伴是否存在
|
||||
partner, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if partner == nil {
|
||||
return gerror.New("合作伙伴不存在")
|
||||
}
|
||||
|
||||
return dao.Partner.UpdateStatus(ctx, id, status)
|
||||
}
|
||||
|
||||
// SetFeatured 设置推荐状态
|
||||
func (s *sPartner) SetFeatured(ctx context.Context, id int, isFeatured int) error {
|
||||
// 检查合作伙伴是否存在
|
||||
partner, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if partner == nil {
|
||||
return gerror.New("合作伙伴不存在")
|
||||
}
|
||||
|
||||
return dao.Partner.Update(ctx, id, g.Map{
|
||||
"is_featured": isFeatured,
|
||||
"updated_at": gtime.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateSortOrder 更新排序
|
||||
func (s *sPartner) UpdateSortOrder(ctx context.Context, id int, sortOrder int) error {
|
||||
// 检查合作伙伴是否存在
|
||||
partner, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if partner == nil {
|
||||
return gerror.New("合作伙伴不存在")
|
||||
}
|
||||
|
||||
return dao.Partner.Update(ctx, id, g.Map{
|
||||
"sort_order": sortOrder,
|
||||
"updated_at": gtime.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
// GetStats 获取合作伙伴统计信息
|
||||
func (s *sPartner) GetStats(ctx context.Context) (map[string]interface{}, error) {
|
||||
// 获取总合作伙伴数
|
||||
totalCount, err := dao.Partner.GetCount(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取启用的合作伙伴数
|
||||
activeCount, err := dao.Partner.GetCountByStatus(ctx, 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取推荐合作伙伴数
|
||||
featuredCount, err := dao.Partner.GetFeaturedCount(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取各分类合作伙伴数
|
||||
categories, err := dao.Partner.GetCategories(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
categoryStats := make(map[string]int)
|
||||
for _, category := range categories {
|
||||
count, err := dao.Partner.GetCountByCategory(ctx, category)
|
||||
if err == nil {
|
||||
categoryStats[category] = count
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_count": totalCount,
|
||||
"active_count": activeCount,
|
||||
"featured_count": featuredCount,
|
||||
"category_stats": categoryStats,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BatchUpdateStatus 批量更新状态
|
||||
func (s *sPartner) BatchUpdateStatus(ctx context.Context, ids []int, status int) error {
|
||||
for _, id := range ids {
|
||||
if err := s.UpdateStatus(ctx, id, status); err != nil {
|
||||
g.Log().Error(ctx, "批量更新合作伙伴状态失败, ID:", id, "错误:", err)
|
||||
// 继续更新其他记录,不中断整个过程
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchDelete 批量删除合作伙伴
|
||||
func (s *sPartner) BatchDelete(ctx context.Context, ids []int) error {
|
||||
for _, id := range ids {
|
||||
if err := s.Delete(ctx, id); err != nil {
|
||||
g.Log().Error(ctx, "批量删除合作伙伴失败, ID:", id, "错误:", err)
|
||||
// 继续删除其他记录,不中断整个过程
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
227
internal/service/role.go
Normal file
227
internal/service/role.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"cms-api/internal/dao"
|
||||
"cms-api/internal/model"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type sRole struct{}
|
||||
|
||||
func Role() *sRole {
|
||||
return &sRole{}
|
||||
}
|
||||
|
||||
// GetById 根据ID获取角色信息
|
||||
func (s *sRole) GetById(ctx context.Context, id int) (*model.Role, error) {
|
||||
return dao.Role.GetById(ctx, id)
|
||||
}
|
||||
|
||||
// List 获取角色列表
|
||||
func (s *sRole) List(ctx context.Context, req *model.RoleListRequest) (*model.PageResponse, error) {
|
||||
var (
|
||||
page = req.Page
|
||||
pageSize = req.PageSize
|
||||
)
|
||||
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
// 设置分页参数
|
||||
req.Page = page
|
||||
req.PageSize = pageSize
|
||||
|
||||
// 获取列表
|
||||
roles, total, err := dao.Role.List(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.PageResponse{
|
||||
List: roles,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: (total + pageSize - 1) / pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAll 获取所有启用的角色
|
||||
func (s *sRole) GetAll(ctx context.Context) ([]*model.Role, error) {
|
||||
return dao.Role.GetAll(ctx)
|
||||
}
|
||||
|
||||
// Create 创建角色
|
||||
func (s *sRole) Create(ctx context.Context, req *model.CreateRoleRequest) error {
|
||||
// 检查角色名称是否存在
|
||||
existRole, err := dao.Role.GetByName(ctx, req.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existRole != nil {
|
||||
return gerror.New("角色名称已存在")
|
||||
}
|
||||
|
||||
// 序列化权限列表
|
||||
permissionsJson, err := json.Marshal(req.Permissions)
|
||||
if err != nil {
|
||||
return gerror.New("权限数据格式错误")
|
||||
}
|
||||
|
||||
// 创建角色
|
||||
role := &model.Role{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Permissions: string(permissionsJson),
|
||||
Status: req.Status,
|
||||
CreatedAt: gtime.Now(),
|
||||
UpdatedAt: gtime.Now(),
|
||||
}
|
||||
|
||||
_, err = dao.Role.Create(ctx, role)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update 更新角色
|
||||
func (s *sRole) Update(ctx context.Context, id int, req *model.UpdateRoleRequest) error {
|
||||
// 检查角色是否存在
|
||||
role, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if role == nil {
|
||||
return gerror.New("角色不存在")
|
||||
}
|
||||
|
||||
// 检查角色名称是否被其他角色使用
|
||||
existRole, err := dao.Role.GetByName(ctx, req.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existRole != nil && existRole.Id != id {
|
||||
return gerror.New("角色名称已被使用")
|
||||
}
|
||||
|
||||
// 序列化权限列表
|
||||
permissionsJson, err := json.Marshal(req.Permissions)
|
||||
if err != nil {
|
||||
return gerror.New("权限数据格式错误")
|
||||
}
|
||||
|
||||
// 更新角色
|
||||
updateData := g.Map{
|
||||
"name": req.Name,
|
||||
"description": req.Description,
|
||||
"permissions": string(permissionsJson),
|
||||
"status": req.Status,
|
||||
"updated_at": gtime.Now(),
|
||||
}
|
||||
|
||||
return dao.Role.Update(ctx, id, updateData)
|
||||
}
|
||||
|
||||
// Delete 删除角色
|
||||
func (s *sRole) Delete(ctx context.Context, id int) error {
|
||||
// 检查角色是否存在
|
||||
role, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if role == nil {
|
||||
return gerror.New("角色不存在")
|
||||
}
|
||||
|
||||
// TODO: 检查是否有用户使用该角色
|
||||
// 这里可以添加检查逻辑,防止删除正在使用的角色
|
||||
|
||||
return dao.Role.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// UpdateStatus 更新角色状态
|
||||
func (s *sRole) UpdateStatus(ctx context.Context, id int, status int) error {
|
||||
// 检查角色是否存在
|
||||
role, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if role == nil {
|
||||
return gerror.New("角色不存在")
|
||||
}
|
||||
|
||||
return dao.Role.UpdateStatus(ctx, id, status)
|
||||
}
|
||||
|
||||
// GetPermissions 获取所有可用权限
|
||||
func (s *sRole) GetPermissions(ctx context.Context) ([]model.PermissionGroup, error) {
|
||||
// 定义系统权限
|
||||
permissions := []model.PermissionGroup{
|
||||
{
|
||||
Name: "用户管理",
|
||||
Description: "用户相关权限",
|
||||
Permissions: []model.Permission{
|
||||
{Key: "user.list", Name: "查看用户列表", Description: "查看用户列表权限", Group: "用户管理"},
|
||||
{Key: "user.create", Name: "创建用户", Description: "创建用户权限", Group: "用户管理"},
|
||||
{Key: "user.update", Name: "编辑用户", Description: "编辑用户权限", Group: "用户管理"},
|
||||
{Key: "user.delete", Name: "删除用户", Description: "删除用户权限", Group: "用户管理"},
|
||||
{Key: "user.status", Name: "管理用户状态", Description: "管理用户状态权限", Group: "用户管理"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "角色管理",
|
||||
Description: "角色权限相关权限",
|
||||
Permissions: []model.Permission{
|
||||
{Key: "role.list", Name: "查看角色列表", Description: "查看角色列表权限", Group: "角色管理"},
|
||||
{Key: "role.create", Name: "创建角色", Description: "创建角色权限", Group: "角色管理"},
|
||||
{Key: "role.update", Name: "编辑角色", Description: "编辑角色权限", Group: "角色管理"},
|
||||
{Key: "role.delete", Name: "删除角色", Description: "删除角色权限", Group: "角色管理"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "内容管理",
|
||||
Description: "内容相关权限",
|
||||
Permissions: []model.Permission{
|
||||
{Key: "article.list", Name: "查看文章列表", Description: "查看文章列表权限", Group: "内容管理"},
|
||||
{Key: "article.create", Name: "创建文章", Description: "创建文章权限", Group: "内容管理"},
|
||||
{Key: "article.update", Name: "编辑文章", Description: "编辑文章权限", Group: "内容管理"},
|
||||
{Key: "article.delete", Name: "删除文章", Description: "删除文章权限", Group: "内容管理"},
|
||||
{Key: "news.list", Name: "查看新闻列表", Description: "查看新闻列表权限", Group: "内容管理"},
|
||||
{Key: "news.create", Name: "创建新闻", Description: "创建新闻权限", Group: "内容管理"},
|
||||
{Key: "news.update", Name: "编辑新闻", Description: "编辑新闻权限", Group: "内容管理"},
|
||||
{Key: "news.delete", Name: "删除新闻", Description: "删除新闻权限", Group: "内容管理"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "附件管理",
|
||||
Description: "附件相关权限",
|
||||
Permissions: []model.Permission{
|
||||
{Key: "attachment.list", Name: "查看附件列表", Description: "查看附件列表权限", Group: "附件管理"},
|
||||
{Key: "attachment.upload", Name: "上传附件", Description: "上传附件权限", Group: "附件管理"},
|
||||
{Key: "attachment.delete", Name: "删除附件", Description: "删除附件权限", Group: "附件管理"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "系统管理",
|
||||
Description: "系统相关权限",
|
||||
Permissions: []model.Permission{
|
||||
{Key: "config.list", Name: "查看系统配置", Description: "查看系统配置权限", Group: "系统管理"},
|
||||
{Key: "config.update", Name: "修改系统配置", Description: "修改系统配置权限", Group: "系统管理"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return permissions, nil
|
||||
}
|
||||
|
||||
// CheckPermission 检查权限
|
||||
func (s *sRole) CheckPermission(ctx context.Context, roleId int, permission string) (bool, error) {
|
||||
return dao.Role.CheckPermission(ctx, roleId, permission)
|
||||
}
|
||||
311
internal/service/user.go
Normal file
311
internal/service/user.go
Normal file
@@ -0,0 +1,311 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"cms-api/internal/dao"
|
||||
"cms-api/internal/model"
|
||||
|
||||
"github.com/gogf/gf/v2/crypto/gmd5"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
type sUser struct{}
|
||||
|
||||
func User() *sUser {
|
||||
return &sUser{}
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
func (s *sUser) Login(ctx context.Context, req *model.UserLoginRequest) (*model.UserLoginResponse, error) {
|
||||
// 查询用户
|
||||
user, err := dao.User.GetByAccount(ctx, req.Account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, gerror.New("用户名或密码错误")
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if !s.VerifyPassword(req.Password, user.Password) {
|
||||
return nil, gerror.New("用户名或密码错误")
|
||||
}
|
||||
|
||||
// 检查状态
|
||||
if user.Status != 0 {
|
||||
statusText := map[int]string{
|
||||
1: "账号已被冻结",
|
||||
2: "账号已被封号",
|
||||
3: "账号已注销",
|
||||
}
|
||||
return nil, gerror.New(statusText[user.Status])
|
||||
}
|
||||
|
||||
// 更新登录信息
|
||||
err = dao.User.Update(ctx, user.Id, g.Map{
|
||||
"ip": g.RequestFromCtx(ctx).GetClientIp(),
|
||||
"updated_at": gtime.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
g.Log().Error(ctx, "更新用户登录信息失败:", err)
|
||||
}
|
||||
|
||||
// 生成token (暂时使用简单的token生成)
|
||||
token := gmd5.MustEncrypt(gconv.String(user.Id) + "_" + gconv.String(gtime.Now().Unix()))
|
||||
|
||||
// 清除密码字段
|
||||
user.Password = ""
|
||||
|
||||
return &model.UserLoginResponse{
|
||||
Token: token,
|
||||
ExpiresIn: 7200, // 2小时
|
||||
User: user,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetById 根据ID获取用户信息
|
||||
func (s *sUser) GetById(ctx context.Context, id int) (*model.User, error) {
|
||||
user, err := dao.User.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user == nil {
|
||||
return nil, nil
|
||||
}
|
||||
// 清除密码字段
|
||||
user.Password = ""
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// List 获取用户列表
|
||||
func (s *sUser) List(ctx context.Context, req *model.UserListRequest) (*model.PageResponse, error) {
|
||||
var (
|
||||
page = req.Page
|
||||
pageSize = req.PageSize
|
||||
)
|
||||
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
// 设置分页参数
|
||||
req.Page = page
|
||||
req.PageSize = pageSize
|
||||
|
||||
// 获取列表
|
||||
users, total, err := dao.User.List(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 清除密码字段
|
||||
for _, user := range users {
|
||||
user.Password = ""
|
||||
}
|
||||
|
||||
return &model.PageResponse{
|
||||
List: users,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: (total + pageSize - 1) / pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Create 创建用户
|
||||
func (s *sUser) Create(ctx context.Context, req *model.CreateUserRequest) error {
|
||||
// 检查账号是否存在
|
||||
existUser, err := dao.User.GetByAccount(ctx, req.Account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existUser != nil {
|
||||
return gerror.New("账号已存在")
|
||||
}
|
||||
|
||||
// 检查邮箱是否存在
|
||||
existUser, err = dao.User.GetByEmail(ctx, req.Email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existUser != nil {
|
||||
return gerror.New("邮箱已存在")
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
hashedPassword := s.HashPassword(req.Password)
|
||||
|
||||
// 创建用户
|
||||
user := &model.User{
|
||||
Account: req.Account,
|
||||
NickName: req.NickName,
|
||||
Avatar: req.Avatar,
|
||||
Email: req.Email,
|
||||
Password: hashedPassword,
|
||||
Balance: req.Balance,
|
||||
RoleId: req.RoleId,
|
||||
IsSysNotifications: req.IsSysNotifications,
|
||||
IsCollectionNotifications: req.IsCollectionNotifications,
|
||||
IsMarketingNotifications: req.IsMarketingNotifications,
|
||||
Status: 0, // 默认正常状态
|
||||
CreatedAt: gtime.Now(),
|
||||
UpdatedAt: gtime.Now(),
|
||||
}
|
||||
|
||||
_, err = dao.User.Create(ctx, user)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update 更新用户
|
||||
func (s *sUser) Update(ctx context.Context, id int, req *model.UpdateUserRequest) error {
|
||||
// 检查用户是否存在
|
||||
user, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user == nil {
|
||||
return gerror.New("用户不存在")
|
||||
}
|
||||
|
||||
// 检查邮箱是否被其他用户使用
|
||||
existUser, err := dao.User.GetByEmail(ctx, req.Email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existUser != nil && existUser.Id != id {
|
||||
return gerror.New("邮箱已被使用")
|
||||
}
|
||||
|
||||
// 更新用户
|
||||
updateData := g.Map{
|
||||
"nick_name": req.NickName,
|
||||
"email": req.Email,
|
||||
"avatar": req.Avatar,
|
||||
"balance": req.Balance,
|
||||
"role_id": req.RoleId,
|
||||
"status": req.Status,
|
||||
"is_sys_notifications": req.IsSysNotifications,
|
||||
"is_collection_notifications": req.IsCollectionNotifications,
|
||||
"is_marketing_notifications": req.IsMarketingNotifications,
|
||||
"updated_at": gtime.Now(),
|
||||
}
|
||||
|
||||
return dao.User.Update(ctx, id, updateData)
|
||||
}
|
||||
|
||||
// Delete 删除用户
|
||||
func (s *sUser) Delete(ctx context.Context, id int) error {
|
||||
// 检查用户是否存在
|
||||
user, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user == nil {
|
||||
return gerror.New("用户不存在")
|
||||
}
|
||||
|
||||
return dao.User.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
func (s *sUser) ChangePassword(ctx context.Context, id int, req *model.ChangePasswordRequest) error {
|
||||
// 获取用户信息
|
||||
user, err := dao.User.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user == nil {
|
||||
return gerror.New("用户不存在")
|
||||
}
|
||||
|
||||
// 验证原密码
|
||||
if !s.VerifyPassword(req.OldPassword, user.Password) {
|
||||
return gerror.New("原密码错误")
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
hashedPassword := s.HashPassword(req.NewPassword)
|
||||
|
||||
// 更新密码
|
||||
updateData := g.Map{
|
||||
"password": hashedPassword,
|
||||
"last_reset_password_at": gtime.Now().Unix(),
|
||||
"updated_at": gtime.Now(),
|
||||
}
|
||||
|
||||
return dao.User.Update(ctx, id, updateData)
|
||||
}
|
||||
|
||||
// UpdateStatus 更新用户状态
|
||||
func (s *sUser) UpdateStatus(ctx context.Context, id int, status int) error {
|
||||
// 检查用户是否存在
|
||||
user, err := s.GetById(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if user == nil {
|
||||
return gerror.New("用户不存在")
|
||||
}
|
||||
|
||||
return dao.User.UpdateStatus(ctx, id, status)
|
||||
}
|
||||
|
||||
// HashPassword 加密密码
|
||||
func (s *sUser) HashPassword(password string) string {
|
||||
return gmd5.MustEncrypt(password + "cms_user_salt_2024")
|
||||
}
|
||||
|
||||
// VerifyPassword 验证密码
|
||||
func (s *sUser) VerifyPassword(password, hashedPassword string) bool {
|
||||
return gmd5.MustEncrypt(password+"cms_user_salt_2024") == hashedPassword
|
||||
}
|
||||
|
||||
// Profile 获取当前用户信息
|
||||
func (s *sUser) Profile(ctx context.Context) (*model.User, error) {
|
||||
userId := gconv.Int(g.RequestFromCtx(ctx).GetCtxVar("user_id"))
|
||||
return s.GetById(ctx, userId)
|
||||
}
|
||||
|
||||
// UpdateProfile 更新当前用户信息
|
||||
func (s *sUser) UpdateProfile(ctx context.Context, req *model.UpdateUserRequest) error {
|
||||
userId := gconv.Int(g.RequestFromCtx(ctx).GetCtxVar("user_id"))
|
||||
return s.Update(ctx, userId, req)
|
||||
}
|
||||
|
||||
// GetStats 获取用户统计信息
|
||||
func (s *sUser) GetStats(ctx context.Context) (map[string]interface{}, error) {
|
||||
// 获取总用户数
|
||||
totalCount, err := dao.User.GetCount(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取各状态用户数
|
||||
normalCount, err := dao.User.GetCountByStatus(ctx, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
frozenCount, err := dao.User.GetCountByStatus(ctx, 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bannedCount, err := dao.User.GetCountByStatus(ctx, 2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_count": totalCount,
|
||||
"normal_count": normalCount,
|
||||
"frozen_count": frozenCount,
|
||||
"banned_count": bannedCount,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user