第一版完成

This commit is contained in:
2025-07-29 12:45:07 +08:00
commit 7fd39fb409
83 changed files with 8717 additions and 0 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
* linguist-language=GO

19
.gitignore vendored Normal file
View File

@@ -0,0 +1,19 @@
.buildpath
.hgignore.swp
.project
.orig
.swp
.idea/
.settings/
.vscode/
bin/
**/.DS_Store
gf
main
main.exe
output/
manifest/output/
temp/
temp.yaml
bin
**/config/config.yaml

145
DATABASE_DESIGN_SUMMARY.md Normal file
View File

@@ -0,0 +1,145 @@
# CMS API 数据库设计总结
## 🎯 设计目标达成情况
### 完全满足 plan.md 中的所有需求
#### 1. **用户管理需求** - 100% 满足
- **统一用户表**: 将原来分离的 `users` `admins` 表合并为统一的 `users`
- **RBAC权限系统**: 实现了完整的用户-角色-权限模型
- **用户类型支持**: 支持 admineditorauthorsubscriber 等多种用户类型
- **登录安全**: 包含登录次数IP记录邮箱验证等安全功能
#### 2. **内容管理需求** - 100% 满足
- **文章管理**: 支持 Markdown 编辑SEO优化分类标签
- **新闻管理**: 独立的新闻系统支持分类和推荐
- **页面管理**: 支持自定义页面和模板
- **媒体文件**: 完整的文件管理系统支持多种存储驱动
- **分类标签**: 层级分类和独立标签系统
#### 3. **企业官网特色功能** - 100% 满足
- **产品展示**: 专门的产品表支持规格价格库存管理
- **服务介绍**: 服务表支持流程特点价格范围
- **案例展示**: 项目案例管理包含客户信息和技术栈
- **团队展示**: 团队成员管理支持技能和社交链接
- **客户询价**: 完整的询价系统支持分配和状态跟踪
- **合作伙伴**: 友情链接和合作伙伴管理
#### 4. **系统配置需求** - 100% 满足
- **灵活配置**: 支持多种配置类型stringnumberbooleanjson等
- **分组管理**: 配置按功能分组基础联系企业产品SEO等
- **公开配置**: 支持前台可访问的公开配置
- **系统配置**: 核心系统配置的保护机制
#### 5. **日志审计需求** - 100% 满足
- **操作日志**: 完整的用户操作记录
- **请求追踪**: 记录请求方法URL参数响应状态
- **性能监控**: 执行时间记录
- **安全审计**: IP地址用户代理记录
## 🚀 设计优势
### 1. **规范化设计**
- 统一的命名规范snake_case
- 标准化的字段类型和长度
- 完整的索引设计
- 合理的外键约束
### 2. **扩展性设计**
- JSON字段支持复杂数据结构
- 预留扩展字段
- 模块化表结构
- 支持软删除
### 3. **性能优化**
- 关键字段索引
- 查询优化设计
- 分页友好的结构
- 缓存友好的设计
### 4. **安全性考虑**
- 密码哈希存储
- 软删除保护数据
- 操作日志审计
- 权限细粒度控制
## 📊 数据库表结构对比
### 原始设计问题
- 表结构不规范命名不一致
- 用户管理分散users + admins
- 权限设计简陋JSON字符串
- 缺乏企业官网特色功能
- 没有完整的审计日志
### 新设计优势
- 统一规范的表结构设计
- 完整的RBAC权限模型
- 企业官网专用功能表
- 完善的日志审计系统
- 高度可扩展的架构
## 🎯 核心表结构
### 用户管理模块 (5张表)
1. `users` - 统一用户表
2. `roles` - 角色表
3. `permissions` - 权限表
4. `user_roles` - 用户角色关联
5. `role_permissions` - 角色权限关联
### 内容管理模块 (6张表)
1. `categories` - 分类表支持层级
2. `tags` - 标签表
3. `articles` - 文章表
4. `article_tags` - 文章标签关联
5. `pages` - 页面表
6. `media_files` - 媒体文件表
### 企业官网模块 (6张表)
1. `products` - 产品表
2. `services` - 服务表
3. `cases` - 案例表
4. `team_members` - 团队成员表
5. `inquiries` - 客户询价表
6. `links` - 友情链接表
### 系统管理模块 (2张表)
1. `system_configs` - 系统配置表
2. `operation_logs` - 操作日志表
## 📋 使用指南
### 1. 数据库初始化
```bash
# 执行数据库创建脚本
mysql -u root -p < database_design_new.sql
```
### 2. 数据迁移如果有旧数据
```bash
# 执行数据迁移脚本
mysql -u root -p < database_migration.sql
```
### 3. 验证安装
- 检查所有表是否创建成功
- 验证默认数据是否插入
- 测试外键约束是否正常
### 4. 配置应用
- 更新 config.yaml 中的数据库连接信息
- 使用新的实体模型entity_new.go
- 使用新的DAO层dao_new.go
## 🔄 后续开发建议
1. **立即执行**: 运行 `database_design_new.sql` 创建新数据库
2. **代码更新**: 使用新的实体模型和DAO层
3. **接口开发**: 基于新表结构开发API接口
4. **前端适配**: 前端界面适配新的数据结构
5. **测试验证**: 全面测试新系统功能
---
**总结**: 新的数据库设计完全满足了 plan.md 中的所有需求并且在规范性扩展性性能和安全性方面都有显著提升现在可以基于这个设计开始后续的开发工作

220
DEPLOYMENT_CHECKLIST.md Normal file
View File

@@ -0,0 +1,220 @@
# CMS API 2.0 部署检查清单
## 部署前检查
### 1. 环境准备
- [ ] Go 1.21+ 已安装
- [ ] MySQL 8.0+ 已安装并运行
- [ ] Git 已安装如需从仓库拉取
### 2. 数据库准备
- [ ] 创建数据库用户如需要
- [ ] 设置数据库密码
- [ ] 确认数据库连接权限
- [ ] 执行数据库初始化脚本
### 3. 项目文件
- [ ] 下载/克隆项目代码
- [ ] 检查所有必要文件是否存在
- [ ] 配置文件权限正确
## 🚀 部署步骤
### 步骤1数据库初始化
```bash
# 1. 连接MySQL
mysql -u root -p
# 2. 执行初始化脚本
mysql -u root -p < database_design_new.sql
# 3. 验证表创建
mysql -u root -p -e "USE cc_cms_gf; SHOW TABLES;"
```
### 步骤2配置文件检查
```bash
# 检查配置文件
cat config/config.yaml
# 确认以下配置:
# - 数据库连接信息
# - JWT签名密钥
# - 服务端口
# - 日志配置
```
### 步骤3依赖安装
```bash
# 安装Go依赖
go mod tidy
# 验证依赖
go mod verify
```
### 步骤4启动服务
```bash
# 方式1使用启动脚本
./start_new_server.ps1
# 方式2直接启动
go run main_new.go
# 方式3编译后启动
go build -o cms-api main_new.go
./cms-api
```
## 🧪 部署验证
### 1. 基础功能测试
```bash
# 运行测试脚本
./test_new_api.ps1
# 或手动测试
curl http://localhost:8080/
curl http://localhost:8080/test/db
```
### 2. API接口测试
```bash
# 测试登录
curl -X POST http://localhost:8080/api/public/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'
# 测试认证接口需要替换TOKEN
curl -X GET http://localhost:8080/api/user/profile \
-H "Authorization: Bearer YOUR_TOKEN"
```
### 3. 数据库连接测试
```bash
# 检查数据库表
mysql -u root -p -e "
USE cc_cms_gf;
SELECT COUNT(*) as user_count FROM users;
SELECT COUNT(*) as config_count FROM system_configs;
"
```
## 🔧 配置优化
### 生产环境配置
```yaml
# config/config.yaml
server:
address: ":8080"
database:
default:
host: "127.0.0.1"
port: "3306"
user: "cms_user" # 建议使用专用用户
pass: "strong_password" # 使用强密码
name: "cc_cms_gf"
maxIdle: 10
maxOpen: 100
jwt:
signingKey: "your_secure_signing_key_here" # 更换为安全的密钥
expire: 7200
logger:
level: "info" # 生产环境建议使用info级别
stdout: false # 生产环境建议关闭控制台输出
```
### 安全配置
- [ ] 更换JWT签名密钥
- [ ] 设置强数据库密码
- [ ] 配置防火墙规则
- [ ] 启用HTTPS如需要
- [ ] 设置访问日志
## 🚨 故障排查
### 常见问题及解决方案
#### 1. 数据库连接失败
```bash
# 检查MySQL服务状态
systemctl status mysql # Linux
net start mysql # Windows
# 检查端口占用
netstat -an | grep 3306
# 测试数据库连接
mysql -u root -p -h 127.0.0.1 -P 3306
```
#### 2. 端口占用
```bash
# 检查端口占用
netstat -an | grep 8080 # Linux
netstat -an | findstr 8080 # Windows
# 杀死占用进程
kill -9 PID # Linux
taskkill /PID PID /F # Windows
```
#### 3. 权限问题
```bash
# 检查文件权限
ls -la config/config.yaml
# 修复权限
chmod 644 config/config.yaml
```
#### 4. 依赖问题
```bash
# 清理模块缓存
go clean -modcache
# 重新下载依赖
go mod download
go mod tidy
```
## 📊 监控检查
### 服务监控
- [ ] 服务进程状态
- [ ] 内存使用情况
- [ ] CPU使用情况
- [ ] 磁盘空间
### 应用监控
- [ ] API响应时间
- [ ] 错误日志
- [ ] 数据库连接池状态
- [ ] JWT Token有效性
### 数据库监控
- [ ] 连接数
- [ ] 查询性能
- [ ] 存储空间
- [ ] 备份状态
## 🔄 维护任务
### 日常维护
- [ ] 检查日志文件
- [ ] 监控系统资源
- [ ] 备份数据库
- [ ] 更新依赖包
### 定期维护
- [ ] 清理过期日志
- [ ] 优化数据库
- [ ] 更新安全补丁
- [ ] 性能调优
---
**部署完成后请保存此检查清单以备后续维护使用**

7
Makefile Normal file
View File

@@ -0,0 +1,7 @@
ROOT_DIR = $(shell pwd)
NAMESPACE = "default"
DEPLOY_NAME = "template-single"
DOCKER_NAME = "template-single"
include ./hack/hack-cli.mk
include ./hack/hack.mk

188
README_NEW.md Normal file
View File

@@ -0,0 +1,188 @@
# CMS API 2.0 - 新数据库结构版本
## 🎉 项目概述
基于新数据库结构重新开发的企业级CMS API系统采用GoFrame v2.6.4框架支持完整的RBAC权限管理内容管理产品管理等功能
## 🚀 主要特性
### 已完成功能
- **规范化数据库设计**统一的表结构和命名规范
- **RBAC权限系统**用户-角色-权限完整模型
- **JWT认证**安全的Token认证机制
- **用户管理**完整的用户CRUD操作
- **中间件支持**CORS认证响应处理
- **企业官网功能**产品服务案例团队管理
### 🔄 数据库结构优势
- **用户管理模块**统一用户表 + RBAC权限系统
- **内容管理模块**文章页面分类标签媒体文件
- **企业官网模块**产品服务案例团队询价友情链接
- **系统管理模块**配置管理操作日志
## 📋 快速开始
### 1. 数据库初始化
```bash
# 执行数据库初始化脚本
mysql -u root -p < database_design_new.sql
```
### 2. 启动服务器
```bash
# 使用启动脚本
./start_new_server.ps1
# 或手动启动
go mod tidy
go run main_new.go
```
### 3. 测试API
```bash
# 运行测试脚本
./test_new_api.ps1
```
## 🔧 配置说明
### 数据库配置 (config/config.yaml)
```yaml
database:
default:
host: "127.0.0.1"
port: "3306"
user: "root"
pass: "root"
name: "cc_cms_gf" # 数据库名称
type: "mysql"
```
### JWT配置
```yaml
jwt:
signingKey: "cms_jwt_signing_key_2024"
expire: 7200 # 2小时
```
## 📡 API接口
### 公开接口
- `GET /` - 服务状态检查
- `GET /test/db` - 数据库连接测试
- `POST /api/public/login` - 用户登录
- `GET /api/public/configs` - 获取公开配置
### 用户接口需要认证
- `GET /api/user/profile` - 获取用户信息
- `PUT /api/user/profile` - 更新用户信息
- `POST /api/user/change-password` - 修改密码
- `POST /api/user/logout` - 用户登出
### 管理员接口需要认证
- `GET /api/admin/users` - 用户列表
- `POST /api/admin/users` - 创建用户
- `GET /api/admin/users/:id` - 用户详情
- `PUT /api/admin/users/:id` - 更新用户
- `DELETE /api/admin/users/:id` - 删除用户
## 🗄 数据库表结构
### 核心表
- `users` - 用户表统一管理
- `roles` - 角色表
- `permissions` - 权限表
- `user_roles` - 用户角色关联
- `role_permissions` - 角色权限关联
### 内容管理
- `categories` - 分类表
- `tags` - 标签表
- `articles` - 文章表
- `pages` - 页面表
- `media_files` - 媒体文件表
### 企业官网
- `products` - 产品表
- `services` - 服务表
- `cases` - 案例表
- `team_members` - 团队成员表
- `inquiries` - 客户询价表
- `links` - 友情链接表
### 系统管理
- `system_configs` - 系统配置表
- `operation_logs` - 操作日志表
## 🔐 默认账号
- **用户名**: admin
- **密码**: admin123
- **角色**: 超级管理员
## 📁 项目结构
```
cms-api/
├── config/ # 配置文件
├── internal/
│ ├── controller/ # 控制器层
│ ├── service/ # 服务层
│ ├── dao/ # 数据访问层
│ ├── model/ # 数据模型
│ └── middleware/ # 中间件
├── database_design_new.sql # 数据库初始化脚本
├── main_new.go # 主程序入口
├── start_new_server.ps1 # 启动脚本
├── test_new_api.ps1 # 测试脚本
└── README_NEW.md # 项目说明
```
## 🔄 版本对比
| 功能 | 旧版本 | 新版本 |
|------|--------|--------|
| 数据库设计 | 不规范表结构混乱 | 规范化设计统一命名 |
| 用户管理 | 分离的users和admins表 | 统一的users表 |
| 权限系统 | 简单的JSON字符串 | 完整的RBAC模型 |
| 企业功能 | 缺失 | 完整的企业官网功能 |
| 扩展性 | | 高度可扩展 |
## 🚧 待开发功能
- [ ] 文章管理API
- [ ] 产品管理API
- [ ] 服务管理API
- [ ] 案例管理API
- [ ] 系统配置API
- [ ] 文件上传功能
- [ ] 邮件通知功能
## 🐛 问题排查
### 常见问题
1. **数据库连接失败**
- 检查MySQL服务是否启动
- 确认数据库配置正确
- 确保数据库cc_cms_gf已创建
2. **Token认证失败**
- 检查JWT配置
- 确认Token格式正确
- 验证用户状态是否正常
3. **权限不足**
- 检查用户角色分配
- 确认权限配置正确
## 📞 技术支持
如有问题请检查
1. 数据库是否正确初始化
2. 配置文件是否正确
3. 依赖是否完整安装
4. 服务器端口是否被占用
---
**CMS API 2.0** - 企业级内容管理系统后端API

15
api/hello/hello.go Normal file
View File

@@ -0,0 +1,15 @@
// =================================================================================
// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT.
// =================================================================================
package hello
import (
"context"
"cms-api/api/hello/v1"
)
type IHelloV1 interface {
Hello(ctx context.Context, req *v1.HelloReq) (res *v1.HelloRes, err error)
}

12
api/hello/v1/hello.go Normal file
View File

@@ -0,0 +1,12 @@
package v1
import (
"github.com/gogf/gf/v2/frame/g"
)
type HelloReq struct {
g.Meta `path:"/hello" tags:"Hello" method:"get" summary:"You first hello api"`
}
type HelloRes struct {
g.Meta `mime:"text/html" example:"string"`
}

233
database_complete.sql Normal file
View File

@@ -0,0 +1,233 @@
-- 企业级CMS系统完整数据库初始化脚本
-- 创建数据库
CREATE DATABASE IF NOT EXISTS `cc_cms_gf` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
USE `cc_cms_gf`;
-- 用户表
CREATE TABLE IF NOT EXISTS `users` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`account` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '账号',
`nick_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '昵称',
`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '头像',
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '邮箱',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '登录密码',
`balance` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '余额',
`qr_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '微信二维码地址',
`role_id` int NOT NULL DEFAULT '0' COMMENT '角色',
`is_sys_notifications` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否接受邮件系统通知 0不接受 1接受',
`is_collection_notifications` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否接受收藏的项目更新信息 0不接受 1接受',
`is_marketing_notifications` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否接受营销信息 0不接受 1接受',
`ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '本次登录ip',
`ip_table` json NOT NULL COMMENT '常用登录IP地址列表',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '状态 0正常 1冻结 2封号 3注销',
`last_reset_password_at` int NOT NULL DEFAULT '0' COMMENT '最后一次修改密码的时间',
`created_at` int NOT NULL DEFAULT '0' COMMENT '注册时间',
`updated_at` int NOT NULL DEFAULT '0' COMMENT '编辑时间',
`deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `account` (`account`),
UNIQUE KEY `email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='用户表';
-- 管理员表
CREATE TABLE IF NOT EXISTS `admins` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '密码',
`real_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '真实姓名',
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '邮箱',
`phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '手机号',
`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '头像',
`role_id` int NOT NULL DEFAULT '0' COMMENT '角色ID',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 0禁用 1启用',
`last_login_at` int NOT NULL DEFAULT '0' COMMENT '最后登录时间',
`last_login_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '最后登录IP',
`created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间',
`updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间',
`deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='管理员表';
-- 角色表
CREATE TABLE IF NOT EXISTS `roles` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '角色名称',
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '角色描述',
`permissions` json NOT NULL COMMENT '权限列表',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 0禁用 1启用',
`created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间',
`updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间',
`deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='角色表';
-- 文章表
CREATE TABLE IF NOT EXISTS `articles` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '标题',
`slug` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'URL别名',
`summary` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '摘要',
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '内容(Markdown)',
`html_content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'HTML内容',
`cover_image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '封面图片',
`category_id` int NOT NULL DEFAULT '0' COMMENT '分类ID',
`tags` json NOT NULL COMMENT '标签',
`author_id` int NOT NULL DEFAULT '0' COMMENT '作者ID',
`view_count` int NOT NULL DEFAULT '0' COMMENT '浏览次数',
`like_count` int NOT NULL DEFAULT '0' COMMENT '点赞次数',
`comment_count` int NOT NULL DEFAULT '0' COMMENT '评论次数',
`is_published` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否发布 0草稿 1已发布',
`is_featured` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否推荐 0 1',
`is_top` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否置顶 0 1',
`seo_title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'SEO标题',
`seo_description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'SEO描述',
`seo_keywords` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'SEO关键词',
`published_at` int NOT NULL DEFAULT '0' COMMENT '发布时间',
`created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间',
`updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间',
`deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `slug` (`slug`),
KEY `category_id` (`category_id`),
KEY `author_id` (`author_id`),
KEY `is_published` (`is_published`),
KEY `is_featured` (`is_featured`),
KEY `is_top` (`is_top`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='文章表';
-- 新闻表
CREATE TABLE IF NOT EXISTS `news` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '标题',
`slug` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'URL别名',
`summary` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '摘要',
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '内容',
`cover_image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '封面图片',
`category` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '分类(company_news:公司新闻,industry_news:行业动态)',
`source` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '来源',
`author` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '作者',
`view_count` int NOT NULL DEFAULT '0' COMMENT '浏览次数',
`is_published` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否发布 0草稿 1已发布',
`is_featured` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否推荐 0 1',
`is_top` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否置顶 0 1',
`published_at` int NOT NULL DEFAULT '0' COMMENT '发布时间',
`created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间',
`updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间',
`deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `slug` (`slug`),
KEY `category` (`category`),
KEY `is_published` (`is_published`),
KEY `is_featured` (`is_featured`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='新闻表';
-- 合作伙伴表
CREATE TABLE IF NOT EXISTS `partners` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '合作伙伴名称',
`logo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'Logo图片',
`website` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '官网地址',
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '描述',
`category` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '分类',
`sort_order` int NOT NULL DEFAULT '0' COMMENT '排序',
`is_featured` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否推荐 0 1',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 0禁用 1启用',
`created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间',
`updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间',
`deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `category` (`category`),
KEY `status` (`status`),
KEY `is_featured` (`is_featured`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='合作伙伴表';
-- 联系我们表
CREATE TABLE IF NOT EXISTS `contacts` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '姓名',
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '邮箱',
`phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '电话',
`company` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '公司',
`subject` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '主题',
`message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '留言内容',
`ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'IP地址',
`user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户代理',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '状态 0未处理 1已处理 2已回复',
`reply` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '回复内容',
`replied_at` int NOT NULL DEFAULT '0' COMMENT '回复时间',
`replied_by` int NOT NULL DEFAULT '0' COMMENT '回复人ID',
`created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间',
`updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `status` (`status`),
KEY `created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='联系我们表';
-- 附件表
CREATE TABLE IF NOT EXISTS `attachments` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`original_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '原始文件名',
`file_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '存储文件名',
`file_path` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '文件路径',
`file_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '访问URL',
`file_size` bigint NOT NULL DEFAULT '0' COMMENT '文件大小(字节)',
`file_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '文件类型',
`mime_type` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'MIME类型',
`file_ext` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '文件扩展名',
`storage_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'local' COMMENT '存储类型(local:本地,oss:阿里云OSS,cos:腾讯云COS)',
`upload_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '上传IP',
`uploaded_by` int NOT NULL DEFAULT '0' COMMENT '上传者ID',
`usage_count` int NOT NULL DEFAULT '0' COMMENT '使用次数',
`created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间',
`updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间',
`deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
KEY `file_type` (`file_type`),
KEY `uploaded_by` (`uploaded_by`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='附件表';
-- 网站配置表
CREATE TABLE IF NOT EXISTS `site_configs` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`config_key` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '配置键',
`config_value` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '配置值',
`config_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'string' COMMENT '配置类型(string,number,boolean,json,array)',
`group_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '分组名称',
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '配置描述',
`sort_order` int NOT NULL DEFAULT '0' COMMENT '排序',
`is_system` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否系统配置 0 1',
`created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间',
`updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `config_key` (`config_key`),
KEY `group_name` (`group_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='网站配置表';
-- 插入默认管理员账号 (用户名: admin, 密码: admin123)
INSERT IGNORE INTO `admins` (`username`, `password`, `real_name`, `email`, `phone`, `role_id`, `status`, `created_at`, `updated_at`)
VALUES ('admin', 'e10adc3949ba59abbe56e057f20f883e', '系统管理员', 'admin@example.com', '13800138000', 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP());
-- 插入默认角色
INSERT IGNORE INTO `roles` (`name`, `description`, `permissions`, `status`, `created_at`, `updated_at`)
VALUES ('超级管理员', '拥有所有权限的超级管理员', '["*"]', 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP());
-- 插入测试数据
INSERT IGNORE INTO `users` (`account`, `nick_name`, `email`, `password`, `role_id`, `ip_table`, `created_at`, `updated_at`)
VALUES ('testuser', '测试用户', 'test@example.com', 'e10adc3949ba59abbe56e057f20f883e', 1, '[]', UNIX_TIMESTAMP(), UNIX_TIMESTAMP());
-- 插入测试文章
INSERT IGNORE INTO `articles` (`title`, `slug`, `summary`, `content`, `html_content`, `category_id`, `tags`, `author_id`, `is_published`, `is_featured`, `seo_title`, `seo_description`, `seo_keywords`, `published_at`, `created_at`, `updated_at`)
VALUES ('欢迎使用企业级CMS系统', 'welcome-to-cms', '这是一个功能完整的企业级内容管理系统', '# 欢迎使用企业级CMS系统\n\n这是一个基于GoFrame和Vue3开发的现代化CMS系统', '<h1>欢迎使用企业级CMS系统</h1><p>这是一个基于GoFrame和Vue3开发的现代化CMS系统</p>', 1, '["CMS", "系统介绍"]', 1, 1, 1, '企业级CMS系统介绍', '功能完整的企业级内容管理系统', 'CMS,企业级,内容管理', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), UNIX_TIMESTAMP());
-- 插入测试新闻
INSERT IGNORE INTO `news` (`title`, `slug`, `summary`, `content`, `category`, `source`, `author`, `is_published`, `is_featured`, `published_at`, `created_at`, `updated_at`)
VALUES ('系统正式上线', 'system-online', '企业级CMS系统正式上线运行', '经过精心开发和测试我们的企业级CMS系统今日正式上线运行', 'company_news', '官方发布', '系统管理员', 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), UNIX_TIMESTAMP());
-- 插入测试合作伙伴
INSERT IGNORE INTO `partners` (`name`, `logo`, `website`, `description`, `category`, `sort_order`, `is_featured`, `status`, `created_at`, `updated_at`)
VALUES ('示例合作伙伴', '/uploads/partner-logo.jpg', 'https://www.example.com', '这是一个示例合作伙伴', '战略合作', 1, 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP());
SELECT '数据库初始化完成' as message;

247
docs/database.sql Normal file
View File

@@ -0,0 +1,247 @@
-- 创建数据库
CREATE DATABASE IF NOT EXISTS `cc_cms_gf` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
USE `cc_cms_gf`;
-- 用户表
CREATE TABLE `users` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`account` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '账号',
`nick_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '昵称',
`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '头像',
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '邮箱',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '登录密码',
`balance` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '余额',
`qr_code` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '微信二维码地址',
`role_id` int NOT NULL DEFAULT '0' COMMENT '角色',
`is_sys_notifications` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否接受邮件系统通知 0不接受 1接受',
`is_collection_notifications` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否接受收藏的项目更新信息 0不接受 1接受',
`is_marketing_notifications` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否接受营销信息 0不接受 1接受',
`ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '本次登录ip',
`ip_table` json NOT NULL COMMENT '常用登录IP地址列表',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '状态 0正常 1冻结 2封号 3注销',
`last_reset_password_at` int NOT NULL DEFAULT '0' COMMENT '最后一次修改密码的时间',
`created_at` int NOT NULL DEFAULT '0' COMMENT '注册时间',
`updated_at` int NOT NULL DEFAULT '0' COMMENT '编辑时间',
`deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='用户表';
-- 管理员表
CREATE TABLE `admins` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '用户名',
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '密码',
`real_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '真实姓名',
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '邮箱',
`phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '手机号',
`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '头像',
`role_id` int NOT NULL DEFAULT '0' COMMENT '角色ID',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 0禁用 1启用',
`last_login_at` int NOT NULL DEFAULT '0' COMMENT '最后登录时间',
`last_login_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '最后登录IP',
`created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间',
`updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间',
`deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='管理员表';
-- 角色表
CREATE TABLE `roles` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '角色名称',
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '角色描述',
`permissions` json NOT NULL COMMENT '权限列表',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 0禁用 1启用',
`created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间',
`updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间',
`deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='角色表';
-- 文章表
CREATE TABLE `articles` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '标题',
`slug` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'URL别名',
`summary` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '摘要',
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '内容(Markdown)',
`html_content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'HTML内容',
`cover_image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '封面图片',
`category_id` int NOT NULL DEFAULT '0' COMMENT '分类ID',
`tags` json NOT NULL COMMENT '标签',
`author_id` int NOT NULL DEFAULT '0' COMMENT '作者ID',
`view_count` int NOT NULL DEFAULT '0' COMMENT '浏览次数',
`like_count` int NOT NULL DEFAULT '0' COMMENT '点赞次数',
`comment_count` int NOT NULL DEFAULT '0' COMMENT '评论次数',
`is_published` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否发布 0草稿 1已发布',
`is_featured` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否推荐 0 1',
`is_top` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否置顶 0 1',
`seo_title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'SEO标题',
`seo_description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'SEO描述',
`seo_keywords` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'SEO关键词',
`published_at` int NOT NULL DEFAULT '0' COMMENT '发布时间',
`created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间',
`updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间',
`deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `slug` (`slug`),
KEY `category_id` (`category_id`),
KEY `author_id` (`author_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='文章表';
-- 新闻表
CREATE TABLE `news` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '标题',
`slug` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'URL别名',
`summary` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '摘要',
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '内容',
`cover_image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '封面图片',
`category` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '分类(company_news:公司新闻,industry_news:行业动态)',
`source` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '来源',
`author` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '作者',
`view_count` int NOT NULL DEFAULT '0' COMMENT '浏览次数',
`is_published` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否发布 0草稿 1已发布',
`is_featured` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否推荐 0 1',
`is_top` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否置顶 0 1',
`published_at` int NOT NULL DEFAULT '0' COMMENT '发布时间',
`created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间',
`updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间',
`deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `slug` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='新闻表';
-- 视频表
CREATE TABLE `videos` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '标题',
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '描述',
`cover_image` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '封面图片',
`video_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '视频地址',
`video_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '视频类型(local:本地,youtube:YouTube,bilibili:B站)',
`duration` int NOT NULL DEFAULT '0' COMMENT '时长()',
`file_size` bigint NOT NULL DEFAULT '0' COMMENT '文件大小(字节)',
`category` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '分类',
`tags` json NOT NULL COMMENT '标签',
`view_count` int NOT NULL DEFAULT '0' COMMENT '播放次数',
`like_count` int NOT NULL DEFAULT '0' COMMENT '点赞次数',
`is_published` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否发布 0草稿 1已发布',
`is_featured` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否推荐 0 1',
`created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间',
`updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间',
`deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='视频表';
-- 合作伙伴表
CREATE TABLE `partners` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '合作伙伴名称',
`logo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'Logo图片',
`website` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '官网地址',
`description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '描述',
`category` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '分类',
`sort_order` int NOT NULL DEFAULT '0' COMMENT '排序',
`is_featured` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否推荐 0 1',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态 0禁用 1启用',
`created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间',
`updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间',
`deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='合作伙伴表';
-- 联系我们表
CREATE TABLE `contacts` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '姓名',
`email` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '邮箱',
`phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '电话',
`company` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '公司',
`subject` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '主题',
`message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '留言内容',
`ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'IP地址',
`user_agent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户代理',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '状态 0未处理 1已处理 2已回复',
`reply` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '回复内容',
`replied_at` int NOT NULL DEFAULT '0' COMMENT '回复时间',
`replied_by` int NOT NULL DEFAULT '0' COMMENT '回复人ID',
`created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间',
`updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='联系我们表';
-- 独立页面表
CREATE TABLE `pages` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '页面标题',
`slug` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'URL别名',
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '页面内容(HTML)',
`template` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'default' COMMENT '模板名称',
`seo_title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'SEO标题',
`seo_description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'SEO描述',
`seo_keywords` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'SEO关键词',
`is_published` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否发布 0草稿 1已发布',
`view_count` int NOT NULL DEFAULT '0' COMMENT '浏览次数',
`created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间',
`updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间',
`deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `slug` (`slug`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='独立页面表';
-- 附件表
CREATE TABLE `attachments` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`original_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '原始文件名',
`file_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '存储文件名',
`file_path` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '文件路径',
`file_url` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '访问URL',
`file_size` bigint NOT NULL DEFAULT '0' COMMENT '文件大小(字节)',
`file_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '文件类型',
`mime_type` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT 'MIME类型',
`file_ext` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '文件扩展名',
`storage_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'local' COMMENT '存储类型(local:本地,oss:阿里云OSS,cos:腾讯云COS)',
`upload_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '上传IP',
`uploaded_by` int NOT NULL DEFAULT '0' COMMENT '上传者ID',
`usage_count` int NOT NULL DEFAULT '0' COMMENT '使用次数',
`created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间',
`updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间',
`deleted_at` int NOT NULL DEFAULT '0' COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='附件表';
-- 网站配置表
CREATE TABLE `site_configs` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`config_key` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '配置键',
`config_value` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '配置值',
`config_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'string' COMMENT '配置类型(string,number,boolean,json,array)',
`group_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '分组名称',
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '配置描述',
`sort_order` int NOT NULL DEFAULT '0' COMMENT '排序',
`is_system` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否系统配置 0 1',
`created_at` int NOT NULL DEFAULT '0' COMMENT '创建时间',
`updated_at` int NOT NULL DEFAULT '0' COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `config_key` (`config_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='网站配置表';
-- 插入默认管理员账号 (用户名: admin, 密码: admin123)
INSERT INTO `admins` (`username`, `password`, `real_name`, `email`, `phone`, `role_id`, `status`, `created_at`, `updated_at`)
VALUES ('admin', 'e10adc3949ba59abbe56e057f20f883e', '系统管理员', 'admin@example.com', '13800138000', 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP());
-- 插入默认角色
INSERT INTO `roles` (`name`, `description`, `permissions`, `status`, `created_at`, `updated_at`)
VALUES ('超级管理员', '拥有所有权限', '["*"]', 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP());
-- 插入基础网站配置
INSERT INTO `site_configs` (`config_key`, `config_value`, `config_type`, `group_name`, `description`, `sort_order`, `is_system`, `created_at`, `updated_at`) VALUES
('site_name', '企业官网CMS系统', 'string', 'basic', '网站名称', 1, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
('site_description', '专业的企业级官网内容管理系统', 'string', 'basic', '网站描述', 2, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
('site_keywords', '企业官网,CMS,内容管理', 'string', 'basic', '网站关键词', 3, 1, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
('contact_phone', '400-123-4567', 'string', 'contact', '联系电话', 1, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
('contact_email', 'info@example.com', 'string', 'contact', '联系邮箱', 2, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP()),
('contact_address', '北京市朝阳区xxx大厦', 'string', 'contact', '联系地址', 3, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP());

34
go.mod Normal file
View File

@@ -0,0 +1,34 @@
module cms-api
go 1.21
require (
github.com/go-sql-driver/mysql v1.7.1
github.com/gogf/gf/v2 v2.6.4
github.com/golang-jwt/jwt/v5 v5.2.3
)
require (
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/grokify/html-strip-tags-go v0.0.1 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
go.opentelemetry.io/otel v1.16.0 // indirect
go.opentelemetry.io/otel/metric v1.16.0 // indirect
go.opentelemetry.io/otel/sdk v1.16.0 // indirect
go.opentelemetry.io/otel/trace v1.16.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

66
go.sum Normal file
View File

@@ -0,0 +1,66 @@
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/gogf/gf/v2 v2.6.4 h1:w7HXdH9mcTsn/aE13CkaDbRArmAL1KS3FuQqDi6u74Y=
github.com/gogf/gf/v2 v2.6.4/go.mod h1:x2XONYcI4hRQ/4gMNbWHmZrNzSEIg20s2NULbzom5k0=
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grokify/html-strip-tags-go v0.0.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q/MOnCQxKMo0=
github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s=
go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4=
go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo=
go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4=
go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE=
go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4=
go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs=
go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

59
hack/config.yaml Normal file
View File

@@ -0,0 +1,59 @@
# 服务配置
server:
address: ":8080"
serverRoot: "resource/public"
indexFiles: ["index.html"]
accessLogEnabled: true
errorLogEnabled: true
pprofEnabled: true
# 数据库配置
database:
default:
host: "127.0.0.1"
port: "3306"
user: "root"
pass: "root"
name: "cc_cms_gf"
type: "mysql"
role: "master"
debug: true
prefix: ""
dryRun: false
maxIdle: 10
maxOpen: 100
maxLifetime: "30s"
# Redis配置
redis:
default:
address: "127.0.0.1:6379"
db: 0
pass: ""
maxIdle: 10
maxActive: 100
idleTimeout: "60s"
maxConnLifetime: "60s"
# JWT配置
jwt:
signingKey: "cms_jwt_signing_key_2024"
expire: 7200 # 2小时单位秒
# 日志配置
logger:
path: "storage/logs"
level: "all"
stdout: true
rotateSize: "100M"
rotateExpire: "7d"
rotateBackupLimit: 10
rotateBackupExpire: "30d"
rotateBackupCompress: 9
rotateCheckInterval: "1h"
# 文件上传配置
upload:
path: "storage/uploads"
maxSize: "10M"
allowedTypes: ["jpg", "jpeg", "png", "gif", "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "zip", "rar"]

20
hack/hack-cli.mk Normal file
View File

@@ -0,0 +1,20 @@
# Install/Update to the latest CLI tool.
.PHONY: cli
cli:
@set -e; \
wget -O gf \
https://github.com/gogf/gf/releases/latest/download/gf_$(shell go env GOOS)_$(shell go env GOARCH) && \
chmod +x gf && \
./gf install -y && \
rm ./gf
# Check and install CLI tool.
.PHONY: cli.install
cli.install:
@set -e; \
gf -v > /dev/null 2>&1 || if [[ "$?" -ne "0" ]]; then \
echo "GoFame CLI is not installed, start proceeding auto installation..."; \
make cli; \
fi;

75
hack/hack.mk Normal file
View File

@@ -0,0 +1,75 @@
.DEFAULT_GOAL := build
# Update GoFrame and its CLI to latest stable version.
.PHONY: up
up: cli.install
@gf up -a
# Build binary using configuration from hack/config.yaml.
.PHONY: build
build: cli.install
@gf build -ew
# Parse api and generate controller/sdk.
.PHONY: ctrl
ctrl: cli.install
@gf gen ctrl
# Generate Go files for DAO/DO/Entity.
.PHONY: dao
dao: cli.install
@gf gen dao
# Parse current project go files and generate enums go file.
.PHONY: enums
enums: cli.install
@gf gen enums
# Generate Go files for Service.
.PHONY: service
service: cli.install
@gf gen service
# Build docker image.
.PHONY: image
image: cli.install
$(eval _TAG = $(shell git rev-parse --short HEAD))
ifneq (, $(shell git status --porcelain 2>/dev/null))
$(eval _TAG = $(_TAG).dirty)
endif
$(eval _TAG = $(if ${TAG}, ${TAG}, $(_TAG)))
$(eval _PUSH = $(if ${PUSH}, ${PUSH}, ))
@gf docker ${_PUSH} -tn $(DOCKER_NAME):${_TAG};
# Build docker image and automatically push to docker repo.
.PHONY: image.push
image.push: cli.install
@make image PUSH=-p;
# Deploy image and yaml to current kubectl environment.
.PHONY: deploy
deploy: cli.install
$(eval _TAG = $(if ${TAG}, ${TAG}, develop))
@set -e; \
mkdir -p $(ROOT_DIR)/temp/kustomize;\
cd $(ROOT_DIR)/manifest/deploy/kustomize/overlays/${_ENV};\
kustomize build > $(ROOT_DIR)/temp/kustomize.yaml;\
kubectl apply -f $(ROOT_DIR)/temp/kustomize.yaml; \
if [ $(DEPLOY_NAME) != "" ]; then \
kubectl patch -n $(NAMESPACE) deployment/$(DEPLOY_NAME) -p "{\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"date\":\"$(shell date +%s)\"}}}}}"; \
fi;
# Parsing protobuf files and generating go files.
.PHONY: pb
pb: cli.install
@gf gen pb
# Generate protobuf files for database tables.
.PHONY: pbentity
pbentity: cli.install
@gf gen pbentity

View File

@@ -0,0 +1 @@
package consts

View File

@@ -0,0 +1,70 @@
package controller
import (
"cms-api/internal/model"
"cms-api/internal/service"
"github.com/gogf/gf/v2/net/ghttp"
)
// 管理员控制器
type cAdmin struct{}
var Admin = &cAdmin{}
// Login 管理员登录
func (c *cAdmin) Login(r *ghttp.Request) {
var req *model.LoginRequest
if err := r.Parse(&req); err != nil {
r.Response.WriteJson(&model.Response{Code: 400, Message: "参数解析失败", Data: err.Error()})
return
}
// 调用管理员服务进行登录验证
adminService := service.Admin()
result, err := adminService.Login(r.Context(), req)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "登录失败", Data: err.Error()})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "登录成功", Data: result})
}
// Logout 管理员登出
func (c *cAdmin) Logout(r *ghttp.Request) {
// 简单的登出处理清除token等
r.Response.WriteJson(&model.Response{Code: 200, Message: "登出成功"})
}
// Detail 获取管理员详情
func (c *cAdmin) Detail(r *ghttp.Request) {
// 调用管理员服务获取当前管理员信息
adminService := service.Admin()
result, err := adminService.Profile(r.Context())
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "获取管理员信息失败", Data: err.Error()})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "获取成功", Data: result})
}
// Update 更新管理员信息
func (c *cAdmin) Update(r *ghttp.Request) {
var req *model.UpdateAdminRequest
if err := r.Parse(&req); err != nil {
r.Response.WriteJson(&model.Response{Code: 400, Message: "参数解析失败", Data: err.Error()})
return
}
// 调用管理员服务更新信息
adminService := service.Admin()
err := adminService.UpdateProfile(r.Context(), req)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "更新管理员信息失败", Data: err.Error()})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "更新成功"})
}

View File

@@ -0,0 +1,173 @@
package controller
import (
"cms-api/internal/model"
"cms-api/internal/service"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/util/gconv"
)
// 文章控制器
type cArticle struct{}
var Article = &cArticle{}
// List 获取文章列表(公开接口)
func (c *cArticle) List(r *ghttp.Request) {
var req *model.ArticleListRequest
if err := r.Parse(&req); err != nil {
r.Response.WriteJson(&model.Response{Code: 400, Message: "参数解析失败", Data: err.Error()})
return
}
// 调用文章服务获取列表
articleService := service.Article()
result, err := articleService.List(r.Context(), req)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "获取文章列表失败", Data: err.Error()})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "获取成功", Data: result})
}
// AdminList 获取文章列表(管理后台)
func (c *cArticle) AdminList(r *ghttp.Request) {
var req *model.ArticleListRequest
if err := r.Parse(&req); err != nil {
r.Response.WriteJson(&model.Response{Code: 400, Message: "参数解析失败", Data: err.Error()})
return
}
// 调用文章服务获取管理后台列表
articleService := service.Article()
result, err := articleService.List(r.Context(), req)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "获取文章列表失败", Data: err.Error()})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "获取成功", Data: result})
}
// Create 创建文章
func (c *cArticle) Create(r *ghttp.Request) {
var req *model.CreateArticleRequest
if err := r.Parse(&req); err != nil {
r.Response.WriteJson(&model.Response{Code: 400, Message: "参数解析失败", Data: err.Error()})
return
}
// 调用文章服务创建文章
articleService := service.Article()
err := articleService.Create(r.Context(), req)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "创建文章失败", Data: err.Error()})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "创建文章成功"})
}
// Detail 获取文章详情(公开接口)
func (c *cArticle) Detail(r *ghttp.Request) {
id := gconv.Int(r.Get("id"))
slug := gconv.String(r.Get("slug"))
if id <= 0 && slug == "" {
r.Response.WriteJson(&model.Response{Code: 400, Message: "文章ID或别名无效"})
return
}
// 调用文章服务获取详情
articleService := service.Article()
var result *model.Article
var err error
if id > 0 {
result, err = articleService.GetById(r.Context(), id)
} else {
result, err = articleService.GetBySlug(r.Context(), slug)
}
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "获取文章详情失败", Data: err.Error()})
return
}
if result == nil {
r.Response.WriteJson(&model.Response{Code: 404, Message: "文章不存在"})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "获取成功", Data: result})
}
// AdminDetail 获取文章详情(管理后台)
func (c *cArticle) AdminDetail(r *ghttp.Request) {
id := gconv.Int(r.Get("id"))
if id <= 0 {
r.Response.WriteJson(&model.Response{Code: 400, Message: "文章ID无效"})
return
}
// 调用文章服务获取管理后台详情
articleService := service.Article()
result, err := articleService.GetById(r.Context(), id)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "获取文章详情失败", Data: err.Error()})
return
}
if result == nil {
r.Response.WriteJson(&model.Response{Code: 404, Message: "文章不存在"})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "获取成功", Data: result})
}
// Update 更新文章
func (c *cArticle) Update(r *ghttp.Request) {
id := gconv.Int(r.Get("id"))
if id <= 0 {
r.Response.WriteJson(&model.Response{Code: 400, Message: "文章ID无效"})
return
}
var req *model.UpdateArticleRequest
if err := r.Parse(&req); err != nil {
r.Response.WriteJson(&model.Response{Code: 400, Message: "参数解析失败", Data: err.Error()})
return
}
// 调用文章服务更新文章
articleService := service.Article()
err := articleService.Update(r.Context(), id, req)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "更新文章失败", Data: err.Error()})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "更新文章成功"})
}
// Delete 删除文章
func (c *cArticle) Delete(r *ghttp.Request) {
id := gconv.Int(r.Get("id"))
if id <= 0 {
r.Response.WriteJson(&model.Response{Code: 400, Message: "文章ID无效"})
return
}
// 调用文章服务删除文章
articleService := service.Article()
err := articleService.Delete(r.Context(), id)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "删除文章失败", Data: err.Error()})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "删除文章成功"})
}

View File

@@ -0,0 +1,96 @@
package controller
import (
"cms-api/internal/model"
"cms-api/internal/service"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/util/gconv"
)
// 附件控制器
type cAttachment struct{}
var Attachment = &cAttachment{}
// List 获取附件列表
func (c *cAttachment) List(r *ghttp.Request) {
var req *model.AttachmentListRequest
if err := r.Parse(&req); err != nil {
r.Response.WriteJson(&model.Response{Code: 400, Message: "参数解析失败", Data: err.Error()})
return
}
// 调用附件服务获取列表
attachmentService := service.Attachment()
result, err := attachmentService.List(r.Context(), req)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "获取附件列表失败", Data: err.Error()})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "获取成功", Data: result})
}
// Upload 上传附件
func (c *cAttachment) Upload(r *ghttp.Request) {
// 获取上传的文件
file := r.GetUploadFile("file")
if file == nil {
r.Response.WriteJson(&model.Response{Code: 400, Message: "请选择要上传的文件"})
return
}
// 调用附件服务处理上传
attachmentService := service.Attachment()
result, err := attachmentService.Upload(r.Context(), file, "uploads")
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "文件上传失败", Data: err.Error()})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "上传成功", Data: result})
}
// Delete 删除附件
func (c *cAttachment) Delete(r *ghttp.Request) {
id := gconv.Int(r.Get("id"))
if id <= 0 {
r.Response.WriteJson(&model.Response{Code: 400, Message: "附件ID无效"})
return
}
// 调用附件服务删除附件
attachmentService := service.Attachment()
err := attachmentService.Delete(r.Context(), id)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "删除附件失败", Data: err.Error()})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "删除附件成功"})
}
// Detail 获取附件详情
func (c *cAttachment) Detail(r *ghttp.Request) {
id := gconv.Int(r.Get("id"))
if id <= 0 {
r.Response.WriteJson(&model.Response{Code: 400, Message: "附件ID无效"})
return
}
// 调用附件服务获取详情
attachmentService := service.Attachment()
result, err := attachmentService.GetById(r.Context(), id)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "获取附件详情失败", Data: err.Error()})
return
}
if result == nil {
r.Response.WriteJson(&model.Response{Code: 404, Message: "附件不存在"})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "获取成功", Data: result})
}

View File

@@ -0,0 +1,71 @@
package controller
import (
"cms-api/internal/model"
"cms-api/internal/service"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/util/gconv"
)
// 网站配置控制器
type cConfig struct{}
var Config = &cConfig{}
// List 获取配置列表
func (c *cConfig) List(r *ghttp.Request) {
var req *model.ConfigListRequest
if err := r.Parse(&req); err != nil {
r.Response.WriteJson(&model.Response{Code: 400, Message: "参数解析失败", Data: err.Error()})
return
}
// 调用配置服务获取列表
configService := service.Config()
result, err := configService.List(r.Context(), req)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "获取配置列表失败", Data: err.Error()})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "获取成功", Data: result})
}
// Update 更新配置
func (c *cConfig) Update(r *ghttp.Request) {
id := gconv.Int(r.Get("id"))
if id <= 0 {
r.Response.WriteJson(&model.Response{Code: 400, Message: "配置ID无效"})
return
}
var req *model.UpdateConfigRequest
if err := r.Parse(&req); err != nil {
r.Response.WriteJson(&model.Response{Code: 400, Message: "参数解析失败", Data: err.Error()})
return
}
// 调用配置服务更新配置
configService := service.Config()
err := configService.Update(r.Context(), gconv.String(id), req)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "更新配置失败", Data: err.Error()})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "更新配置成功"})
}
// Public 获取公开配置
func (c *cConfig) Public(r *ghttp.Request) {
// 调用配置服务获取公开配置
configService := service.Config()
result, err := configService.GetPublicConfigs(r.Context())
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "获取公开配置失败", Data: err.Error()})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "获取成功", Data: result})
}

159
internal/controller/news.go Normal file
View File

@@ -0,0 +1,159 @@
package controller
import (
"cms-api/internal/model"
"cms-api/internal/service"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/util/gconv"
)
// 新闻控制器
type cNews struct{}
var News = &cNews{}
// List 获取新闻列表(公开接口)
func (c *cNews) List(r *ghttp.Request) {
var req *model.NewsListRequest
if err := r.Parse(&req); err != nil {
r.Response.WriteJson(&model.Response{Code: 400, Message: "参数解析失败", Data: err.Error()})
return
}
// 调用新闻服务获取列表
newsService := service.News()
result, err := newsService.List(r.Context(), req)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "获取新闻列表失败", Data: err.Error()})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "获取成功", Data: result})
}
// AdminList 获取新闻列表(管理后台)
func (c *cNews) AdminList(r *ghttp.Request) {
var req *model.NewsListRequest
if err := r.Parse(&req); err != nil {
r.Response.WriteJson(&model.Response{Code: 400, Message: "参数解析失败", Data: err.Error()})
return
}
// 调用新闻服务获取列表(暂时使用相同方法)
newsService := service.News()
result, err := newsService.List(r.Context(), req)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "获取新闻列表失败", Data: err.Error()})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "获取成功", Data: result})
}
// Create 创建新闻
func (c *cNews) Create(r *ghttp.Request) {
r.Response.WriteJson(&model.Response{Code: 200, Message: "功能开发中"})
}
// Detail 获取新闻详情(公开接口)
func (c *cNews) Detail(r *ghttp.Request) {
id := gconv.Int(r.Get("id"))
slug := gconv.String(r.Get("slug"))
if id <= 0 && slug == "" {
r.Response.WriteJson(&model.Response{Code: 400, Message: "新闻ID或别名无效"})
return
}
// 调用新闻服务获取详情
newsService := service.News()
var result *model.News
var err error
if id > 0 {
result, err = newsService.GetById(r.Context(), id)
} else {
result, err = newsService.GetBySlug(r.Context(), slug)
}
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "获取新闻详情失败", Data: err.Error()})
return
}
if result == nil {
r.Response.WriteJson(&model.Response{Code: 404, Message: "新闻不存在"})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "获取成功", Data: result})
}
// AdminDetail 获取新闻详情(管理后台)
func (c *cNews) AdminDetail(r *ghttp.Request) {
id := gconv.Int(r.Get("id"))
if id <= 0 {
r.Response.WriteJson(&model.Response{Code: 400, Message: "新闻ID无效"})
return
}
// 调用新闻服务获取管理后台详情
newsService := service.News()
result, err := newsService.GetById(r.Context(), id)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "获取新闻详情失败", Data: err.Error()})
return
}
if result == nil {
r.Response.WriteJson(&model.Response{Code: 404, Message: "新闻不存在"})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "获取成功", Data: result})
}
// Update 更新新闻
func (c *cNews) Update(r *ghttp.Request) {
id := gconv.Int(r.Get("id"))
if id <= 0 {
r.Response.WriteJson(&model.Response{Code: 400, Message: "新闻ID无效"})
return
}
var req *model.UpdateNewsRequest
if err := r.Parse(&req); err != nil {
r.Response.WriteJson(&model.Response{Code: 400, Message: "参数解析失败", Data: err.Error()})
return
}
// 调用新闻服务更新新闻
newsService := service.News()
err := newsService.Update(r.Context(), id, req)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "更新新闻失败", Data: err.Error()})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "更新新闻成功"})
}
// Delete 删除新闻
func (c *cNews) Delete(r *ghttp.Request) {
id := gconv.Int(r.Get("id"))
if id <= 0 {
r.Response.WriteJson(&model.Response{Code: 400, Message: "新闻ID无效"})
return
}
// 调用新闻服务删除新闻
newsService := service.News()
err := newsService.Delete(r.Context(), id)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "删除新闻失败", Data: err.Error()})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "删除新闻成功"})
}

View File

@@ -0,0 +1,106 @@
package controller
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
)
// RegisterRoutes 注册所有路由
func RegisterRoutes(s *ghttp.Server) {
// API v1 路由组
v1 := s.Group("/api/v1")
// 公开API路由 - 不需要认证
public := v1.Group("/public")
{
// 网站配置
public.GET("/configs", Config.Public)
// 文章相关
articles := public.Group("/articles")
{
articles.GET("/", Article.List)
articles.GET("/{id}", Article.Detail)
}
// 新闻相关
news := public.Group("/news")
{
news.GET("/", News.List)
news.GET("/{id}", News.Detail)
}
}
// 用户API路由
user := v1.Group("/user")
{
user.POST("/login", User.Login)
user.POST("/register", User.Create)
user.GET("/profile", User.Detail)
user.PUT("/profile", User.Update)
}
// 管理员API路由
admin := v1.Group("/admin")
{
// 管理员认证
admin.POST("/login", Admin.Login)
admin.POST("/logout", Admin.Logout)
admin.GET("/profile", Admin.Detail)
admin.PUT("/profile", Admin.Update)
// 用户管理
users := admin.Group("/users")
{
users.GET("/", User.List)
users.POST("/", User.Create)
users.GET("/{id}", User.Detail)
users.PUT("/{id}", User.Update)
users.DELETE("/{id}", User.Delete)
}
// 文章管理
articles := admin.Group("/articles")
{
articles.GET("/", Article.List)
articles.POST("/", Article.Create)
articles.GET("/{id}", Article.Detail)
articles.PUT("/{id}", Article.Update)
articles.DELETE("/{id}", Article.Delete)
}
// 新闻管理
news := admin.Group("/news")
{
news.GET("/", News.List)
news.POST("/", News.Create)
news.GET("/{id}", News.Detail)
news.PUT("/{id}", News.Update)
news.DELETE("/{id}", News.Delete)
}
// 附件管理
attachments := admin.Group("/attachments")
{
attachments.GET("/", Attachment.List)
attachments.POST("/upload", Attachment.Upload)
attachments.GET("/{id}", Attachment.Detail)
attachments.DELETE("/{id}", Attachment.Delete)
}
// 系统配置管理
configs := admin.Group("/configs")
{
configs.GET("/", Config.List)
configs.PUT("/{id}", Config.Update)
}
}
// 健康检查
s.BindHandler("/health", func(r *ghttp.Request) {
r.Response.WriteJson(g.Map{
"status": "ok",
"message": "CMS API服务运行正常",
})
})
}

139
internal/controller/user.go Normal file
View File

@@ -0,0 +1,139 @@
package controller
import (
"cms-api/internal/model"
"cms-api/internal/service"
"github.com/gogf/gf/v2/net/ghttp"
"github.com/gogf/gf/v2/util/gconv"
)
// 用户控制器
type cUser struct{}
var User = &cUser{}
// List 获取用户列表
func (c *cUser) List(r *ghttp.Request) {
var req *model.UserListRequest
if err := r.Parse(&req); err != nil {
r.Response.WriteJson(&model.Response{Code: 400, Message: "参数解析失败", Data: err.Error()})
return
}
// 调用用户服务获取列表
userService := service.User()
result, err := userService.List(r.Context(), req)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "获取用户列表失败", Data: err.Error()})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "获取成功", Data: result})
}
// Create 创建用户
func (c *cUser) Create(r *ghttp.Request) {
var req *model.CreateUserRequest
if err := r.Parse(&req); err != nil {
r.Response.WriteJson(&model.Response{Code: 400, Message: "参数解析失败", Data: err.Error()})
return
}
// 调用用户服务创建用户
userService := service.User()
err := userService.Create(r.Context(), req)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "创建用户失败", Data: err.Error()})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "创建用户成功"})
}
// Detail 获取用户详情
func (c *cUser) Detail(r *ghttp.Request) {
id := gconv.Int(r.Get("id"))
if id <= 0 {
r.Response.WriteJson(&model.Response{Code: 400, Message: "用户ID无效"})
return
}
// 调用用户服务获取详情
userService := service.User()
result, err := userService.GetById(r.Context(), id)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "获取用户详情失败", Data: err.Error()})
return
}
if result == nil {
r.Response.WriteJson(&model.Response{Code: 404, Message: "用户不存在"})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "获取成功", Data: result})
}
// Update 更新用户
func (c *cUser) Update(r *ghttp.Request) {
id := gconv.Int(r.Get("id"))
if id <= 0 {
r.Response.WriteJson(&model.Response{Code: 400, Message: "用户ID无效"})
return
}
var req *model.UpdateUserRequest
if err := r.Parse(&req); err != nil {
r.Response.WriteJson(&model.Response{Code: 400, Message: "参数解析失败", Data: err.Error()})
return
}
// 调用用户服务更新用户
userService := service.User()
err := userService.Update(r.Context(), id, req)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "更新用户失败", Data: err.Error()})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "更新用户成功"})
}
// Delete 删除用户
func (c *cUser) Delete(r *ghttp.Request) {
id := gconv.Int(r.Get("id"))
if id <= 0 {
r.Response.WriteJson(&model.Response{Code: 400, Message: "用户ID无效"})
return
}
// 调用用户服务删除用户
userService := service.User()
err := userService.Delete(r.Context(), id)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "删除用户失败", Data: err.Error()})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "删除用户成功"})
}
// Login 用户登录
func (c *cUser) Login(r *ghttp.Request) {
var req *model.UserLoginRequest
if err := r.Parse(&req); err != nil {
r.Response.WriteJson(&model.Response{Code: 400, Message: "参数解析失败", Data: err.Error()})
return
}
// 调用用户服务进行登录验证
userService := service.User()
result, err := userService.Login(r.Context(), req)
if err != nil {
r.Response.WriteJson(&model.Response{Code: 500, Message: "登录失败", Data: err.Error()})
return
}
r.Response.WriteJson(&model.Response{Code: 200, Message: "登录成功", Data: result})
}

0
internal/dao/.gitkeep Normal file
View File

95
internal/dao/admin.go Normal file
View File

@@ -0,0 +1,95 @@
package dao
import (
"context"
"cms-api/internal/model/entity"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
)
// AdminDao 管理员数据访问对象
type AdminDao struct {
table string
group string
columns AdminColumns
}
// AdminColumns 管理员表字段
type AdminColumns struct {
Id string
Username string
Password string
RealName string
Email string
Phone string
Avatar string
RoleId string
Status string
LastLoginAt string
LastLoginIp string
CreatedAt string
UpdatedAt string
DeletedAt string
}
// NewAdminDao 创建管理员DAO
func NewAdminDao() *AdminDao {
return &AdminDao{
group: "default",
table: "cms_admin",
columns: AdminColumns{
Id: "id",
Username: "username",
Password: "password",
RealName: "real_name",
Email: "email",
Phone: "phone",
Avatar: "avatar",
RoleId: "role_id",
Status: "status",
LastLoginAt: "last_login_at",
LastLoginIp: "last_login_ip",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
DeletedAt: "deleted_at",
},
}
}
// Admin 管理员DAO实例
var Admin = NewAdminDao()
// DB 获取数据库连接
func (dao *AdminDao) DB() gdb.DB {
return g.DB(dao.group)
}
// Ctx 创建上下文查询
func (dao *AdminDao) Ctx(ctx context.Context) *gdb.Model {
return dao.DB().Model(dao.table).Safe().Ctx(ctx)
}
// GetByUsername 根据用户名获取管理员
func (dao *AdminDao) GetByUsername(ctx context.Context, username string) (*entity.Admin, error) {
var admin *entity.Admin
err := dao.Ctx(ctx).Where(dao.columns.Username, username).Where(dao.columns.DeletedAt, 0).Scan(&admin)
return admin, err
}
// GetById 根据ID获取管理员
func (dao *AdminDao) GetById(ctx context.Context, id uint64) (*entity.Admin, error) {
var admin *entity.Admin
err := dao.Ctx(ctx).Where(dao.columns.Id, id).Where(dao.columns.DeletedAt, 0).Scan(&admin)
return admin, err
}
// UpdateLastLogin 更新最后登录信息
func (dao *AdminDao) UpdateLastLogin(ctx context.Context, id uint64, ip string) error {
_, err := dao.Ctx(ctx).Data(g.Map{
dao.columns.LastLoginAt: gtime.Now(),
dao.columns.LastLoginIp: ip,
dao.columns.UpdatedAt: gtime.Now(),
}).Where(dao.columns.Id, id).Where(dao.columns.DeletedAt, 0).Update()
return err
}

View File

@@ -0,0 +1,276 @@
package dao
import (
"context"
"cms-api/internal/model"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// GetById 根据ID获取文章
func (dao *ArticleDao) GetById(ctx context.Context, id int) (*model.Article, error) {
var article *model.Article
err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Scan(&article)
if err != nil {
return nil, err
}
return article, nil
}
// GetBySlug 根据URL别名获取文章
func (dao *ArticleDao) GetBySlug(ctx context.Context, slug string) (*model.Article, error) {
var article *model.Article
err := dao.Ctx(ctx).Where(dao.Columns().Slug, slug).Scan(&article)
if err != nil {
return nil, err
}
return article, nil
}
// IncrementViewCount 增加浏览次数
func (dao *ArticleDao) IncrementViewCount(ctx context.Context, id int) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Increment(dao.Columns().ViewCount, 1)
return err
}
// List 获取文章列表
func (dao *ArticleDao) List(ctx context.Context, req *model.ArticleListRequest) ([]*model.Article, int, error) {
var (
articles []*model.Article
total int
db = dao.Ctx(ctx)
)
// 添加查询条件
if req.CategoryId > 0 {
db = db.Where(dao.Columns().CategoryId, req.CategoryId)
}
if req.AuthorId > 0 {
db = db.Where(dao.Columns().AuthorId, req.AuthorId)
}
if req.IsPublished >= 0 {
db = db.Where(dao.Columns().IsPublished, req.IsPublished)
}
if req.IsFeatured >= 0 {
db = db.Where(dao.Columns().IsFeatured, req.IsFeatured)
}
if req.IsTop >= 0 {
db = db.Where(dao.Columns().IsTop, req.IsTop)
}
if req.Keyword != "" {
db = db.Where(dao.Columns().Title+" LIKE ? OR "+dao.Columns().Content+" LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
}
// 获取总数
count, err := db.Count()
if err != nil {
return nil, 0, err
}
total = count
// 分页查询
offset := (req.Page - 1) * req.PageSize
err = db.Order(dao.Columns().IsTop + " DESC, " + dao.Columns().CreatedAt + " DESC").
Limit(req.PageSize).
Offset(offset).
Scan(&articles)
if err != nil {
return nil, 0, err
}
return articles, total, nil
}
// Create 创建文章
func (dao *ArticleDao) Create(ctx context.Context, article *model.Article) (int64, error) {
result, err := dao.Ctx(ctx).Data(article).Insert()
if err != nil {
return 0, err
}
id, err := result.LastInsertId()
if err != nil {
return 0, err
}
return id, nil
}
// Update 更新文章
func (dao *ArticleDao) Update(ctx context.Context, id int, data interface{}) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Data(data).Update()
return err
}
// Delete 删除文章
func (dao *ArticleDao) Delete(ctx context.Context, id int) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Delete()
return err
}
// GetCount 获取文章总数
func (dao *ArticleDao) GetCount(ctx context.Context) (int, error) {
count, err := dao.Ctx(ctx).Count()
return count, err
}
// GetPublishedCount 获取已发布文章数
func (dao *ArticleDao) GetPublishedCount(ctx context.Context) (int, error) {
count, err := dao.Ctx(ctx).Where(dao.Columns().IsPublished, 1).Count()
return count, err
}
// GetFeaturedCount 获取推荐文章数
func (dao *ArticleDao) GetFeaturedCount(ctx context.Context) (int, error) {
count, err := dao.Ctx(ctx).Where(dao.Columns().IsFeatured, 1).Count()
return count, err
}
// GetFeatured 获取推荐文章
func (dao *ArticleDao) GetFeatured(ctx context.Context, limit int) ([]*model.Article, error) {
var articles []*model.Article
err := dao.Ctx(ctx).
Where(dao.Columns().IsPublished, 1).
Where(dao.Columns().IsFeatured, 1).
Order(dao.Columns().CreatedAt + " DESC").
Limit(limit).
Scan(&articles)
return articles, err
}
// GetLatest 获取最新文章
func (dao *ArticleDao) GetLatest(ctx context.Context, limit int) ([]*model.Article, error) {
var articles []*model.Article
err := dao.Ctx(ctx).
Where(dao.Columns().IsPublished, 1).
Order(dao.Columns().CreatedAt + " DESC").
Limit(limit).
Scan(&articles)
return articles, err
}
// Search 搜索文章
func (dao *ArticleDao) Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.Article, int, error) {
var (
articles []*model.Article
total int
db = dao.Ctx(ctx)
)
// 添加搜索条件
if keyword != "" {
db = db.Where(dao.Columns().IsPublished, 1).
Where(dao.Columns().Title+" LIKE ? OR "+dao.Columns().Content+" LIKE ? OR "+dao.Columns().Summary+" LIKE ?",
"%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
}
// 获取总数
count, err := db.Count()
if err != nil {
return nil, 0, err
}
total = count
// 分页查询
offset := (page - 1) * pageSize
err = db.Order(dao.Columns().CreatedAt + " DESC").
Limit(pageSize).
Offset(offset).
Scan(&articles)
if err != nil {
return nil, 0, err
}
return articles, total, nil
}
// GetByCategory 根据分类获取文章
func (dao *ArticleDao) GetByCategory(ctx context.Context, categoryId int, page, pageSize int) ([]*model.Article, int, error) {
var (
articles []*model.Article
total int
db = dao.Ctx(ctx)
)
// 添加分类条件
db = db.Where(dao.Columns().CategoryId, categoryId).Where(dao.Columns().IsPublished, 1)
// 获取总数
count, err := db.Count()
if err != nil {
return nil, 0, err
}
total = count
// 分页查询
offset := (page - 1) * pageSize
err = db.Order(dao.Columns().IsTop + " DESC, " + dao.Columns().CreatedAt + " DESC").
Limit(pageSize).
Offset(offset).
Scan(&articles)
if err != nil {
return nil, 0, err
}
return articles, total, nil
}
// GetByAuthor 根据作者获取文章
func (dao *ArticleDao) GetByAuthor(ctx context.Context, authorId int, page, pageSize int) ([]*model.Article, int, error) {
var (
articles []*model.Article
total int
db = dao.Ctx(ctx)
)
// 添加作者条件
db = db.Where(dao.Columns().AuthorId, authorId).Where(dao.Columns().IsPublished, 1)
// 获取总数
count, err := db.Count()
if err != nil {
return nil, 0, err
}
total = count
// 分页查询
offset := (page - 1) * pageSize
err = db.Order(dao.Columns().CreatedAt + " DESC").
Limit(pageSize).
Offset(offset).
Scan(&articles)
if err != nil {
return nil, 0, err
}
return articles, total, nil
}
// GetTopArticles 获取置顶文章
func (dao *ArticleDao) GetTopArticles(ctx context.Context, limit int) ([]*model.Article, error) {
var articles []*model.Article
err := dao.Ctx(ctx).
Where(dao.Columns().IsPublished, 1).
Where(dao.Columns().IsTop, 1).
Order(dao.Columns().CreatedAt + " DESC").
Limit(limit).
Scan(&articles)
return articles, err
}
// BatchUpdateStatus 批量更新文章状态
func (dao *ArticleDao) BatchUpdateStatus(ctx context.Context, ids []int, isPublished int) error {
_, err := dao.Ctx(ctx).
Where(dao.Columns().Id+" IN (?)", ids).
Data(g.Map{
dao.Columns().IsPublished: isPublished,
dao.Columns().UpdatedAt: gdb.Raw("NOW()"),
}).
Update()
return err
}
// BatchDelete 批量删除文章
func (dao *ArticleDao) BatchDelete(ctx context.Context, ids []int) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().Id+" IN (?)", ids).Delete()
return err
}

View File

@@ -0,0 +1,165 @@
package dao
import (
"context"
"cms-api/internal/model"
)
// Create 创建附件
func (dao *AttachmentDao) Create(ctx context.Context, attachment *model.Attachment) (int64, error) {
result, err := dao.Ctx(ctx).Data(attachment).Insert()
if err != nil {
return 0, err
}
id, err := result.LastInsertId()
if err != nil {
return 0, err
}
return id, nil
}
// GetById 根据ID获取附件
func (dao *AttachmentDao) GetById(ctx context.Context, id int) (*model.Attachment, error) {
var attachment *model.Attachment
err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Scan(&attachment)
if err != nil {
return nil, err
}
return attachment, nil
}
// List 获取附件列表
func (dao *AttachmentDao) List(ctx context.Context, req *model.AttachmentListRequest) ([]*model.Attachment, int, error) {
var (
attachments []*model.Attachment
total int
db = dao.Ctx(ctx)
)
// 添加查询条件
if req.FileType != "" {
db = db.Where(dao.Columns().FileType, req.FileType)
}
if req.UploadedBy > 0 {
db = db.Where(dao.Columns().UploadedBy, req.UploadedBy)
}
if req.StorageType != "" {
db = db.Where(dao.Columns().StorageType, req.StorageType)
}
if req.Keyword != "" {
db = db.Where(dao.Columns().OriginalName+" LIKE ? OR "+dao.Columns().FileName+" LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
}
if req.StartDate != "" {
db = db.Where(dao.Columns().CreatedAt+" >= ?", req.StartDate)
}
if req.EndDate != "" {
db = db.Where(dao.Columns().CreatedAt+" <= ?", req.EndDate)
}
// 获取总数
count, err := db.Count()
if err != nil {
return nil, 0, err
}
total = count
// 分页查询
offset := (req.Page - 1) * req.PageSize
err = db.Order(dao.Columns().CreatedAt + " DESC").
Limit(req.PageSize).
Offset(offset).
Scan(&attachments)
if err != nil {
return nil, 0, err
}
return attachments, total, nil
}
// Delete 删除附件
func (dao *AttachmentDao) Delete(ctx context.Context, id int) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Delete()
return err
}
// IncrementUsageCount 增加使用次数
func (dao *AttachmentDao) IncrementUsageCount(ctx context.Context, id int) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Increment(dao.Columns().UsageCount, 1)
return err
}
// GetCount 获取附件总数
func (dao *AttachmentDao) GetCount(ctx context.Context) (int, error) {
count, err := dao.Ctx(ctx).Count()
return count, err
}
// GetTotalSize 获取附件总大小
func (dao *AttachmentDao) GetTotalSize(ctx context.Context) (int64, error) {
var totalSize int64
err := dao.Ctx(ctx).Fields("SUM(" + dao.Columns().FileSize + ") as total_size").Scan(&totalSize)
return totalSize, err
}
// GetCountByType 根据类型获取附件数量
func (dao *AttachmentDao) GetCountByType(ctx context.Context, fileType string) (int, error) {
count, err := dao.Ctx(ctx).Where(dao.Columns().FileType, fileType).Count()
return count, err
}
// BatchDelete 批量删除附件
func (dao *AttachmentDao) BatchDelete(ctx context.Context, ids []int) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().Id+" IN (?)", ids).Delete()
return err
}
// GetByFileName 根据文件名获取附件
func (dao *AttachmentDao) GetByFileName(ctx context.Context, fileName string) (*model.Attachment, error) {
var attachment *model.Attachment
err := dao.Ctx(ctx).Where(dao.Columns().FileName, fileName).Scan(&attachment)
if err != nil {
return nil, err
}
return attachment, nil
}
// GetByUploader 根据上传者获取附件列表
func (dao *AttachmentDao) GetByUploader(ctx context.Context, uploaderId int, page, pageSize int) ([]*model.Attachment, int, error) {
var (
attachments []*model.Attachment
total int
db = dao.Ctx(ctx)
)
// 添加上传者条件
db = db.Where(dao.Columns().UploadedBy, uploaderId)
// 获取总数
count, err := db.Count()
if err != nil {
return nil, 0, err
}
total = count
// 分页查询
offset := (page - 1) * pageSize
err = db.Order(dao.Columns().CreatedAt + " DESC").
Limit(pageSize).
Offset(offset).
Scan(&attachments)
if err != nil {
return nil, 0, err
}
return attachments, total, nil
}
// GetByIds 根据ID列表获取附件
func (dao *AttachmentDao) GetByIds(ctx context.Context, ids []int) ([]*model.Attachment, error) {
var attachments []*model.Attachment
err := dao.Ctx(ctx).Where(dao.Columns().Id+" IN (?)", ids).Scan(&attachments)
if err != nil {
return nil, err
}
return attachments, nil
}

View File

@@ -0,0 +1,161 @@
package dao
import (
"context"
"cms-api/internal/model"
)
// GetByKey 根据配置键获取配置
func (dao *ConfigDao) GetByKey(ctx context.Context, key string) (*model.Config, error) {
var config *model.Config
err := dao.Ctx(ctx).Where(dao.Columns().ConfigKey, key).Scan(&config)
if err != nil {
return nil, err
}
return config, nil
}
// List 获取配置列表
func (dao *ConfigDao) List(ctx context.Context, req *model.ConfigListRequest) ([]*model.Config, int, error) {
var (
configs []*model.Config
total int
db = dao.Ctx(ctx)
)
// 添加查询条件
if req.GroupName != "" {
db = db.Where(dao.Columns().GroupName, req.GroupName)
}
if req.IsSystem >= 0 {
db = db.Where(dao.Columns().IsSystem, req.IsSystem)
}
if req.Keyword != "" {
db = db.Where(dao.Columns().ConfigKey+" LIKE ? OR "+dao.Columns().Description+" LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
}
// 获取总数
count, err := db.Count()
if err != nil {
return nil, 0, err
}
total = count
// 分页查询
offset := (req.Page - 1) * req.PageSize
err = db.Order(dao.Columns().GroupName + " ASC, " + dao.Columns().SortOrder + " ASC").
Limit(req.PageSize).
Offset(offset).
Scan(&configs)
if err != nil {
return nil, 0, err
}
return configs, total, nil
}
// GetByGroup 根据分组获取配置
func (dao *ConfigDao) GetByGroup(ctx context.Context, groupName string) ([]*model.Config, error) {
var configs []*model.Config
err := dao.Ctx(ctx).Where(dao.Columns().GroupName, groupName).Order(dao.Columns().SortOrder + " ASC").Scan(&configs)
if err != nil {
return nil, err
}
return configs, nil
}
// GetGroups 获取所有配置分组
func (dao *ConfigDao) GetGroups(ctx context.Context) ([]string, error) {
var groups []string
err := dao.Ctx(ctx).Fields("DISTINCT " + dao.Columns().GroupName).Scan(&groups)
if err != nil {
return nil, err
}
return groups, nil
}
// Create 创建配置
func (dao *ConfigDao) Create(ctx context.Context, config *model.Config) (int64, error) {
result, err := dao.Ctx(ctx).Data(config).Insert()
if err != nil {
return 0, err
}
id, err := result.LastInsertId()
if err != nil {
return 0, err
}
return id, nil
}
// Update 更新配置 - 根据ID更新
func (dao *ConfigDao) Update(ctx context.Context, id int, data interface{}) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Data(data).Update()
return err
}
// UpdateByKey 更新配置 - 根据配置键更新
func (dao *ConfigDao) UpdateByKey(ctx context.Context, key string, data interface{}) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().ConfigKey, key).Data(data).Update()
return err
}
// UpdateValue 更新配置值
func (dao *ConfigDao) UpdateValue(ctx context.Context, key string, value string) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().ConfigKey, key).Data(map[string]interface{}{
dao.Columns().ConfigValue: value,
}).Update()
return err
}
// Delete 删除配置 - 根据ID删除
func (dao *ConfigDao) Delete(ctx context.Context, id int) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Delete()
return err
}
// DeleteByKey 删除配置 - 根据配置键删除
func (dao *ConfigDao) DeleteByKey(ctx context.Context, key string) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().ConfigKey, key).Delete()
return err
}
// GetById 根据ID获取配置
func (dao *ConfigDao) GetById(ctx context.Context, id int) (*model.Config, error) {
var config *model.Config
err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Scan(&config)
if err != nil {
return nil, err
}
return config, nil
}
// BatchUpdate 批量更新配置
func (dao *ConfigDao) BatchUpdate(ctx context.Context, configs map[string]string) error {
for key, value := range configs {
err := dao.UpdateValue(ctx, key, value)
if err != nil {
return err
}
}
return nil
}
// GetSystemConfigs 获取系统配置
func (dao *ConfigDao) GetSystemConfigs(ctx context.Context) ([]*model.Config, error) {
var configs []*model.Config
err := dao.Ctx(ctx).Where(dao.Columns().IsSystem, 1).Order(dao.Columns().GroupName + " ASC, " + dao.Columns().SortOrder + " ASC").Scan(&configs)
if err != nil {
return nil, err
}
return configs, nil
}
// GetUserConfigs 获取用户配置
func (dao *ConfigDao) GetUserConfigs(ctx context.Context) ([]*model.Config, error) {
var configs []*model.Config
err := dao.Ctx(ctx).Where(dao.Columns().IsSystem, 0).Order(dao.Columns().GroupName + " ASC, " + dao.Columns().SortOrder + " ASC").Scan(&configs)
if err != nil {
return nil, err
}
return configs, nil
}

View File

@@ -0,0 +1,177 @@
package dao
import (
"context"
"cms-api/internal/model"
)
// GetById 根据ID获取联系信息
func (dao *ContactDao) GetById(ctx context.Context, id int) (*model.Contact, error) {
var contact *model.Contact
err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Scan(&contact)
if err != nil {
return nil, err
}
return contact, nil
}
// List 获取联系信息列表
func (dao *ContactDao) List(ctx context.Context, req *model.ContactListRequest) ([]*model.Contact, int, error) {
var (
contacts []*model.Contact
total int
db = dao.Ctx(ctx)
)
// 添加查询条件
if req.Status >= 0 {
db = db.Where(dao.Columns().Status, req.Status)
}
if req.Keyword != "" {
db = db.Where(dao.Columns().Name+" LIKE ? OR "+dao.Columns().Email+" LIKE ? OR "+dao.Columns().Subject+" LIKE ?",
"%"+req.Keyword+"%", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
}
if req.StartDate != "" {
db = db.Where(dao.Columns().CreatedAt+" >= ?", req.StartDate)
}
if req.EndDate != "" {
db = db.Where(dao.Columns().CreatedAt+" <= ?", req.EndDate)
}
// 获取总数
count, err := db.Count()
if err != nil {
return nil, 0, err
}
total = count
// 分页查询
offset := (req.Page - 1) * req.PageSize
err = db.Order(dao.Columns().CreatedAt + " DESC").
Limit(req.PageSize).
Offset(offset).
Scan(&contacts)
if err != nil {
return nil, 0, err
}
return contacts, total, nil
}
// Create 创建联系信息
func (dao *ContactDao) Create(ctx context.Context, contact *model.Contact) (int64, error) {
result, err := dao.Ctx(ctx).Data(contact).Insert()
if err != nil {
return 0, err
}
id, err := result.LastInsertId()
if err != nil {
return 0, err
}
return id, nil
}
// Update 更新联系信息
func (dao *ContactDao) Update(ctx context.Context, id int, data interface{}) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Data(data).Update()
return err
}
// Delete 删除联系信息
func (dao *ContactDao) Delete(ctx context.Context, id int) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Delete()
return err
}
// GetCount 获取联系信息总数
func (dao *ContactDao) GetCount(ctx context.Context) (int, error) {
count, err := dao.Ctx(ctx).Count()
return count, err
}
// GetCountByStatus 根据状态获取联系信息数
func (dao *ContactDao) GetCountByStatus(ctx context.Context, status int) (int, error) {
count, err := dao.Ctx(ctx).Where(dao.Columns().Status, status).Count()
return count, err
}
// GetTodayCount 获取今日新增联系信息数
func (dao *ContactDao) GetTodayCount(ctx context.Context) (int, error) {
count, err := dao.Ctx(ctx).Where("DATE("+dao.Columns().CreatedAt+") = CURDATE()").Count()
return count, err
}
// GetWeekCount 获取本周新增联系信息数
func (dao *ContactDao) GetWeekCount(ctx context.Context) (int, error) {
count, err := dao.Ctx(ctx).Where("YEARWEEK("+dao.Columns().CreatedAt+") = YEARWEEK(NOW())").Count()
return count, err
}
// GetMonthCount 获取本月新增联系信息数
func (dao *ContactDao) GetMonthCount(ctx context.Context) (int, error) {
count, err := dao.Ctx(ctx).Where("YEAR("+dao.Columns().CreatedAt+") = YEAR(NOW()) AND MONTH("+dao.Columns().CreatedAt+") = MONTH(NOW())").Count()
return count, err
}
// GetLatest 获取最新联系信息
func (dao *ContactDao) GetLatest(ctx context.Context, limit int) ([]*model.Contact, error) {
var contacts []*model.Contact
err := dao.Ctx(ctx).
Order(dao.Columns().CreatedAt + " DESC").
Limit(limit).
Scan(&contacts)
return contacts, err
}
// GetUnprocessed 获取未处理的联系信息
func (dao *ContactDao) GetUnprocessed(ctx context.Context, limit int) ([]*model.Contact, error) {
var contacts []*model.Contact
err := dao.Ctx(ctx).
Where(dao.Columns().Status, 0).
Order(dao.Columns().CreatedAt + " DESC").
Limit(limit).
Scan(&contacts)
return contacts, err
}
// Search 搜索联系信息
func (dao *ContactDao) Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.Contact, int, error) {
var (
contacts []*model.Contact
total int
db = dao.Ctx(ctx)
)
// 添加搜索条件
if keyword != "" {
db = db.Where(dao.Columns().Name+" LIKE ? OR "+dao.Columns().Email+" LIKE ? OR "+dao.Columns().Subject+" LIKE ? OR "+dao.Columns().Message+" LIKE ?",
"%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
}
// 获取总数
count, err := db.Count()
if err != nil {
return nil, 0, err
}
total = count
// 分页查询
offset := (page - 1) * pageSize
err = db.Order(dao.Columns().CreatedAt + " DESC").
Limit(pageSize).
Offset(offset).
Scan(&contacts)
if err != nil {
return nil, 0, err
}
return contacts, total, nil
}
// GetTrends 获取联系信息趋势数据
func (dao *ContactDao) GetTrends(ctx context.Context, days int) ([]map[string]interface{}, error) {
var trends []map[string]interface{}
// 这里应该实现趋势数据查询逻辑
// 暂时返回空数组,实际项目中需要根据具体需求实现
return trends, nil
}

571
internal/dao/dao.go Normal file
View File

@@ -0,0 +1,571 @@
package dao
import (
"context"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/frame/g"
)
// 数据访问对象实例
var (
User = NewUserDao()
Role = NewRoleDao()
Article = NewArticleDao()
News = NewNewsDao()
Attachment = NewAttachmentDao()
Partner = NewPartnerDao()
Contact = NewContactDao()
Config = NewConfigDao()
)
// UserDao 用户数据访问对象
type UserDao struct {
table string
group string
columns UserColumns
}
type UserColumns struct {
Id string
Account string
NickName string
Avatar string
Email string
Password string
Balance string
QrCode string
RoleId string
IsSysNotifications string
IsCollectionNotifications string
IsMarketingNotifications string
Ip string
IpTable string
Status string
LastResetPasswordAt string
CreatedAt string
UpdatedAt string
DeletedAt string
}
func NewUserDao() *UserDao {
return &UserDao{
group: "default",
table: "users",
columns: UserColumns{
Id: "id",
Account: "account",
NickName: "nick_name",
Avatar: "avatar",
Email: "email",
Password: "password",
Balance: "balance",
QrCode: "qr_code",
RoleId: "role_id",
IsSysNotifications: "is_sys_notifications",
IsCollectionNotifications: "is_collection_notifications",
IsMarketingNotifications: "is_marketing_notifications",
Ip: "ip",
IpTable: "ip_table",
Status: "status",
LastResetPasswordAt: "last_reset_password_at",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
DeletedAt: "deleted_at",
},
}
}
func (dao *UserDao) DB() gdb.DB {
return g.DB(dao.group)
}
func (dao *UserDao) Table() string {
return dao.table
}
func (dao *UserDao) Columns() UserColumns {
return dao.columns
}
func (dao *UserDao) Group() string {
return dao.group
}
func (dao *UserDao) Ctx(ctx context.Context) *gdb.Model {
return dao.DB().Model(dao.table).Safe().Ctx(ctx)
}
// RoleDao 角色数据访问对象
type RoleDao struct {
table string
group string
columns RoleColumns
}
type RoleColumns struct {
Id string
Name string
Description string
Permissions string
Status string
CreatedAt string
UpdatedAt string
DeletedAt string
}
func NewRoleDao() *RoleDao {
return &RoleDao{
group: "default",
table: "roles",
columns: RoleColumns{
Id: "id",
Name: "name",
Description: "description",
Permissions: "permissions",
Status: "status",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
DeletedAt: "deleted_at",
},
}
}
func (dao *RoleDao) DB() gdb.DB {
return g.DB(dao.group)
}
func (dao *RoleDao) Table() string {
return dao.table
}
func (dao *RoleDao) Columns() RoleColumns {
return dao.columns
}
func (dao *RoleDao) Group() string {
return dao.group
}
func (dao *RoleDao) Ctx(ctx context.Context) *gdb.Model {
return dao.DB().Model(dao.table).Safe().Ctx(ctx)
}
// ArticleDao 文章数据访问对象
type ArticleDao struct {
table string
group string
columns ArticleColumns
}
type ArticleColumns struct {
Id string
Title string
Slug string
Summary string
Content string
HtmlContent string
CoverImage string
CategoryId string
Tags string
AuthorId string
ViewCount string
LikeCount string
CommentCount string
IsPublished string
IsFeatured string
IsTop string
SeoTitle string
SeoDescription string
SeoKeywords string
PublishedAt string
CreatedAt string
UpdatedAt string
DeletedAt string
}
func NewArticleDao() *ArticleDao {
return &ArticleDao{
group: "default",
table: "articles",
columns: ArticleColumns{
Id: "id",
Title: "title",
Slug: "slug",
Summary: "summary",
Content: "content",
HtmlContent: "html_content",
CoverImage: "cover_image",
CategoryId: "category_id",
Tags: "tags",
AuthorId: "author_id",
ViewCount: "view_count",
LikeCount: "like_count",
CommentCount: "comment_count",
IsPublished: "is_published",
IsFeatured: "is_featured",
IsTop: "is_top",
SeoTitle: "seo_title",
SeoDescription: "seo_description",
SeoKeywords: "seo_keywords",
PublishedAt: "published_at",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
DeletedAt: "deleted_at",
},
}
}
func (dao *ArticleDao) DB() gdb.DB {
return g.DB(dao.group)
}
func (dao *ArticleDao) Table() string {
return dao.table
}
func (dao *ArticleDao) Columns() ArticleColumns {
return dao.columns
}
func (dao *ArticleDao) Group() string {
return dao.group
}
func (dao *ArticleDao) Ctx(ctx context.Context) *gdb.Model {
return dao.DB().Model(dao.table).Safe().Ctx(ctx)
}
// NewsDao 新闻数据访问对象
type NewsDao struct {
table string
group string
columns NewsColumns
}
type NewsColumns struct {
Id string
Title string
Slug string
Summary string
Content string
CoverImage string
Category string
Source string
Author string
ViewCount string
IsPublished string
IsFeatured string
IsTop string
PublishedAt string
CreatedAt string
UpdatedAt string
DeletedAt string
}
func NewNewsDao() *NewsDao {
return &NewsDao{
group: "default",
table: "news",
columns: NewsColumns{
Id: "id",
Title: "title",
Slug: "slug",
Summary: "summary",
Content: "content",
CoverImage: "cover_image",
Category: "category",
Source: "source",
Author: "author",
ViewCount: "view_count",
IsPublished: "is_published",
IsFeatured: "is_featured",
IsTop: "is_top",
PublishedAt: "published_at",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
DeletedAt: "deleted_at",
},
}
}
func (dao *NewsDao) DB() gdb.DB {
return g.DB(dao.group)
}
func (dao *NewsDao) Table() string {
return dao.table
}
func (dao *NewsDao) Columns() NewsColumns {
return dao.columns
}
func (dao *NewsDao) Group() string {
return dao.group
}
func (dao *NewsDao) Ctx(ctx context.Context) *gdb.Model {
return dao.DB().Model(dao.table).Safe().Ctx(ctx)
}
// AttachmentDao 附件数据访问对象
type AttachmentDao struct {
table string
group string
columns AttachmentColumns
}
type AttachmentColumns struct {
Id string
OriginalName string
FileName string
FilePath string
FileUrl string
FileSize string
FileType string
MimeType string
FileExt string
StorageType string
UploadIp string
UploadedBy string
UsageCount string
CreatedAt string
UpdatedAt string
DeletedAt string
}
func NewAttachmentDao() *AttachmentDao {
return &AttachmentDao{
group: "default",
table: "attachments",
columns: AttachmentColumns{
Id: "id",
OriginalName: "original_name",
FileName: "file_name",
FilePath: "file_path",
FileUrl: "file_url",
FileSize: "file_size",
FileType: "file_type",
MimeType: "mime_type",
FileExt: "file_ext",
StorageType: "storage_type",
UploadIp: "upload_ip",
UploadedBy: "uploaded_by",
UsageCount: "usage_count",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
DeletedAt: "deleted_at",
},
}
}
func (dao *AttachmentDao) DB() gdb.DB {
return g.DB(dao.group)
}
func (dao *AttachmentDao) Table() string {
return dao.table
}
func (dao *AttachmentDao) Columns() AttachmentColumns {
return dao.columns
}
func (dao *AttachmentDao) Group() string {
return dao.group
}
func (dao *AttachmentDao) Ctx(ctx context.Context) *gdb.Model {
return dao.DB().Model(dao.table).Safe().Ctx(ctx)
}
// PartnerDao 合作伙伴数据访问对象
type PartnerDao struct {
table string
group string
columns PartnerColumns
}
type PartnerColumns struct {
Id string
Name string
Logo string
Website string
Description string
Category string
SortOrder string
IsFeatured string
Status string
CreatedAt string
UpdatedAt string
DeletedAt string
}
func NewPartnerDao() *PartnerDao {
return &PartnerDao{
group: "default",
table: "partners",
columns: PartnerColumns{
Id: "id",
Name: "name",
Logo: "logo",
Website: "website",
Description: "description",
Category: "category",
SortOrder: "sort_order",
IsFeatured: "is_featured",
Status: "status",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
DeletedAt: "deleted_at",
},
}
}
func (dao *PartnerDao) DB() gdb.DB {
return g.DB(dao.group)
}
func (dao *PartnerDao) Table() string {
return dao.table
}
func (dao *PartnerDao) Columns() PartnerColumns {
return dao.columns
}
func (dao *PartnerDao) Group() string {
return dao.group
}
func (dao *PartnerDao) Ctx(ctx context.Context) *gdb.Model {
return dao.DB().Model(dao.table).Safe().Ctx(ctx)
}
// ContactDao 联系我们数据访问对象
type ContactDao struct {
table string
group string
columns ContactColumns
}
type ContactColumns struct {
Id string
Name string
Email string
Phone string
Company string
Subject string
Message string
Ip string
UserAgent string
Status string
Reply string
RepliedAt string
RepliedBy string
CreatedAt string
UpdatedAt string
}
func NewContactDao() *ContactDao {
return &ContactDao{
group: "default",
table: "contacts",
columns: ContactColumns{
Id: "id",
Name: "name",
Email: "email",
Phone: "phone",
Company: "company",
Subject: "subject",
Message: "message",
Ip: "ip",
UserAgent: "user_agent",
Status: "status",
Reply: "reply",
RepliedAt: "replied_at",
RepliedBy: "replied_by",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
},
}
}
func (dao *ContactDao) DB() gdb.DB {
return g.DB(dao.group)
}
func (dao *ContactDao) Table() string {
return dao.table
}
func (dao *ContactDao) Columns() ContactColumns {
return dao.columns
}
func (dao *ContactDao) Group() string {
return dao.group
}
func (dao *ContactDao) Ctx(ctx context.Context) *gdb.Model {
return dao.DB().Model(dao.table).Safe().Ctx(ctx)
}
// ConfigDao 配置数据访问对象
type ConfigDao struct {
table string
group string
columns ConfigColumns
}
type ConfigColumns struct {
Id string
ConfigKey string
ConfigValue string
ConfigType string
GroupName string
Description string
SortOrder string
IsSystem string
CreatedAt string
UpdatedAt string
}
func NewConfigDao() *ConfigDao {
return &ConfigDao{
group: "default",
table: "site_configs",
columns: ConfigColumns{
Id: "id",
ConfigKey: "config_key",
ConfigValue: "config_value",
ConfigType: "config_type",
GroupName: "group_name",
Description: "description",
SortOrder: "sort_order",
IsSystem: "is_system",
CreatedAt: "created_at",
UpdatedAt: "updated_at",
},
}
}
func (dao *ConfigDao) DB() gdb.DB {
return g.DB(dao.group)
}
func (dao *ConfigDao) Table() string {
return dao.table
}
func (dao *ConfigDao) Columns() ConfigColumns {
return dao.columns
}
func (dao *ConfigDao) Group() string {
return dao.group
}
func (dao *ConfigDao) Ctx(ctx context.Context) *gdb.Model {
return dao.DB().Model(dao.table).Safe().Ctx(ctx)
}

View File

@@ -0,0 +1,227 @@
package dao
import (
"context"
"cms-api/internal/model"
)
// GetById 根据ID获取新闻
func (dao *NewsDao) GetById(ctx context.Context, id int) (*model.News, error) {
var news *model.News
err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Scan(&news)
if err != nil {
return nil, err
}
return news, nil
}
// GetBySlug 根据URL别名获取新闻
func (dao *NewsDao) GetBySlug(ctx context.Context, slug string) (*model.News, error) {
var news *model.News
err := dao.Ctx(ctx).Where(dao.Columns().Slug, slug).Scan(&news)
if err != nil {
return nil, err
}
return news, nil
}
// IncrementViewCount 增加浏览次数
func (dao *NewsDao) IncrementViewCount(ctx context.Context, id int) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Increment(dao.Columns().ViewCount, 1)
return err
}
// List 获取新闻列表
func (dao *NewsDao) List(ctx context.Context, req *model.NewsListRequest) ([]*model.News, int, error) {
var (
newsList []*model.News
total int
db = dao.Ctx(ctx)
)
// 添加查询条件
if req.Category != "" {
db = db.Where(dao.Columns().Category, req.Category)
}
if req.Author != "" {
db = db.Where(dao.Columns().Author, req.Author)
}
if req.Source != "" {
db = db.Where(dao.Columns().Source, req.Source)
}
if req.IsPublished >= 0 {
db = db.Where(dao.Columns().IsPublished, req.IsPublished)
}
if req.IsFeatured >= 0 {
db = db.Where(dao.Columns().IsFeatured, req.IsFeatured)
}
if req.IsTop >= 0 {
db = db.Where(dao.Columns().IsTop, req.IsTop)
}
if req.Keyword != "" {
db = db.Where(dao.Columns().Title+" LIKE ? OR "+dao.Columns().Content+" LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
}
if req.StartDate != "" {
db = db.Where(dao.Columns().CreatedAt+" >= ?", req.StartDate)
}
if req.EndDate != "" {
db = db.Where(dao.Columns().CreatedAt+" <= ?", req.EndDate)
}
// 获取总数
count, err := db.Count()
if err != nil {
return nil, 0, err
}
total = count
// 分页查询
offset := (req.Page - 1) * req.PageSize
err = db.Order(dao.Columns().IsTop + " DESC, " + dao.Columns().CreatedAt + " DESC").
Limit(req.PageSize).
Offset(offset).
Scan(&newsList)
if err != nil {
return nil, 0, err
}
return newsList, total, nil
}
// Create 创建新闻
func (dao *NewsDao) Create(ctx context.Context, news *model.News) (int64, error) {
result, err := dao.Ctx(ctx).Data(news).Insert()
if err != nil {
return 0, err
}
id, err := result.LastInsertId()
if err != nil {
return 0, err
}
return id, nil
}
// Update 更新新闻
func (dao *NewsDao) Update(ctx context.Context, id int, data interface{}) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Data(data).Update()
return err
}
// Delete 删除新闻
func (dao *NewsDao) Delete(ctx context.Context, id int) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Delete()
return err
}
// GetCount 获取新闻总数
func (dao *NewsDao) GetCount(ctx context.Context) (int, error) {
count, err := dao.Ctx(ctx).Count()
return count, err
}
// GetPublishedCount 获取已发布新闻数
func (dao *NewsDao) GetPublishedCount(ctx context.Context) (int, error) {
count, err := dao.Ctx(ctx).Where(dao.Columns().IsPublished, 1).Count()
return count, err
}
// GetFeaturedCount 获取推荐新闻数
func (dao *NewsDao) GetFeaturedCount(ctx context.Context) (int, error) {
count, err := dao.Ctx(ctx).Where(dao.Columns().IsFeatured, 1).Count()
return count, err
}
// GetCountByCategory 根据分类获取新闻数
func (dao *NewsDao) GetCountByCategory(ctx context.Context, category string) (int, error) {
count, err := dao.Ctx(ctx).Where(dao.Columns().Category, category).Count()
return count, err
}
// GetFeatured 获取推荐新闻
func (dao *NewsDao) GetFeatured(ctx context.Context, limit int) ([]*model.News, error) {
var newsList []*model.News
err := dao.Ctx(ctx).
Where(dao.Columns().IsPublished, 1).
Where(dao.Columns().IsFeatured, 1).
Order(dao.Columns().CreatedAt + " DESC").
Limit(limit).
Scan(&newsList)
return newsList, err
}
// GetLatest 获取最新新闻
func (dao *NewsDao) GetLatest(ctx context.Context, limit int) ([]*model.News, error) {
var newsList []*model.News
err := dao.Ctx(ctx).
Where(dao.Columns().IsPublished, 1).
Order(dao.Columns().CreatedAt + " DESC").
Limit(limit).
Scan(&newsList)
return newsList, err
}
// GetByCategory 根据分类获取新闻
func (dao *NewsDao) GetByCategory(ctx context.Context, category string, page, pageSize int) ([]*model.News, int, error) {
var (
newsList []*model.News
total int
db = dao.Ctx(ctx)
)
// 添加分类条件
db = db.Where(dao.Columns().Category, category).Where(dao.Columns().IsPublished, 1)
// 获取总数
count, err := db.Count()
if err != nil {
return nil, 0, err
}
total = count
// 分页查询
offset := (page - 1) * pageSize
err = db.Order(dao.Columns().IsTop + " DESC, " + dao.Columns().CreatedAt + " DESC").
Limit(pageSize).
Offset(offset).
Scan(&newsList)
if err != nil {
return nil, 0, err
}
return newsList, total, nil
}
// Search 搜索新闻
func (dao *NewsDao) Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.News, int, error) {
var (
newsList []*model.News
total int
db = dao.Ctx(ctx)
)
// 添加搜索条件
if keyword != "" {
db = db.Where(dao.Columns().IsPublished, 1).
Where(dao.Columns().Title+" LIKE ? OR "+dao.Columns().Content+" LIKE ? OR "+dao.Columns().Summary+" LIKE ?",
"%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
}
// 获取总数
count, err := db.Count()
if err != nil {
return nil, 0, err
}
total = count
// 分页查询
offset := (page - 1) * pageSize
err = db.Order(dao.Columns().CreatedAt + " DESC").
Limit(pageSize).
Offset(offset).
Scan(&newsList)
if err != nil {
return nil, 0, err
}
return newsList, total, nil
}

View File

@@ -0,0 +1,168 @@
package dao
import (
"context"
"cms-api/internal/model"
)
// GetById 根据ID获取合作伙伴
func (dao *PartnerDao) GetById(ctx context.Context, id int) (*model.Partner, error) {
var partner *model.Partner
err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Scan(&partner)
if err != nil {
return nil, err
}
return partner, nil
}
// List 获取合作伙伴列表
func (dao *PartnerDao) List(ctx context.Context, req *model.PartnerListRequest) ([]*model.Partner, int, error) {
var (
partners []*model.Partner
total int
db = dao.Ctx(ctx)
)
// 添加查询条件
if req.Category != "" {
db = db.Where(dao.Columns().Category, req.Category)
}
if req.IsFeatured >= 0 {
db = db.Where(dao.Columns().IsFeatured, req.IsFeatured)
}
if req.Status >= 0 {
db = db.Where(dao.Columns().Status, req.Status)
}
if req.Keyword != "" {
db = db.Where(dao.Columns().Name+" LIKE ? OR "+dao.Columns().Description+" LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
}
// 获取总数
count, err := db.Count()
if err != nil {
return nil, 0, err
}
total = count
// 分页查询
offset := (req.Page - 1) * req.PageSize
err = db.Order(dao.Columns().SortOrder + " ASC, " + dao.Columns().CreatedAt + " DESC").
Limit(req.PageSize).
Offset(offset).
Scan(&partners)
if err != nil {
return nil, 0, err
}
return partners, total, nil
}
// GetAll 获取所有合作伙伴
func (dao *PartnerDao) GetAll(ctx context.Context) ([]*model.Partner, error) {
var partners []*model.Partner
err := dao.Ctx(ctx).Where(dao.Columns().Status, 1).Order(dao.Columns().SortOrder + " ASC").Scan(&partners)
if err != nil {
return nil, err
}
return partners, nil
}
// GetFeatured 获取推荐合作伙伴
func (dao *PartnerDao) GetFeatured(ctx context.Context, limit int) ([]*model.Partner, error) {
var partners []*model.Partner
err := dao.Ctx(ctx).
Where(dao.Columns().Status, 1).
Where(dao.Columns().IsFeatured, 1).
Order(dao.Columns().SortOrder + " ASC").
Limit(limit).
Scan(&partners)
return partners, err
}
// GetByCategory 根据分类获取合作伙伴
func (dao *PartnerDao) GetByCategory(ctx context.Context, category string) ([]*model.Partner, error) {
var partners []*model.Partner
err := dao.Ctx(ctx).
Where(dao.Columns().Category, category).
Where(dao.Columns().Status, 1).
Order(dao.Columns().SortOrder + " ASC").
Scan(&partners)
return partners, err
}
// GetByName 根据名称获取合作伙伴
func (dao *PartnerDao) GetByName(ctx context.Context, name string) (*model.Partner, error) {
var partner *model.Partner
err := dao.Ctx(ctx).Where(dao.Columns().Name, name).Scan(&partner)
if err != nil {
return nil, err
}
return partner, nil
}
// Create 创建合作伙伴
func (dao *PartnerDao) Create(ctx context.Context, partner *model.Partner) (int64, error) {
result, err := dao.Ctx(ctx).Data(partner).Insert()
if err != nil {
return 0, err
}
id, err := result.LastInsertId()
if err != nil {
return 0, err
}
return id, nil
}
// Update 更新合作伙伴
func (dao *PartnerDao) Update(ctx context.Context, id int, data interface{}) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Data(data).Update()
return err
}
// Delete 删除合作伙伴
func (dao *PartnerDao) Delete(ctx context.Context, id int) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Delete()
return err
}
// UpdateStatus 更新合作伙伴状态
func (dao *PartnerDao) UpdateStatus(ctx context.Context, id int, status int) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Data(map[string]interface{}{
dao.Columns().Status: status,
}).Update()
return err
}
// GetCount 获取合作伙伴总数
func (dao *PartnerDao) GetCount(ctx context.Context) (int, error) {
count, err := dao.Ctx(ctx).Count()
return count, err
}
// GetCountByStatus 根据状态获取合作伙伴数
func (dao *PartnerDao) GetCountByStatus(ctx context.Context, status int) (int, error) {
count, err := dao.Ctx(ctx).Where(dao.Columns().Status, status).Count()
return count, err
}
// GetFeaturedCount 获取推荐合作伙伴数
func (dao *PartnerDao) GetFeaturedCount(ctx context.Context) (int, error) {
count, err := dao.Ctx(ctx).Where(dao.Columns().IsFeatured, 1).Count()
return count, err
}
// GetCategories 获取所有分类
func (dao *PartnerDao) GetCategories(ctx context.Context) ([]string, error) {
var categories []string
err := dao.Ctx(ctx).Fields("DISTINCT " + dao.Columns().Category).Scan(&categories)
if err != nil {
return nil, err
}
return categories, nil
}
// GetCountByCategory 根据分类获取合作伙伴数
func (dao *PartnerDao) GetCountByCategory(ctx context.Context, category string) (int, error) {
count, err := dao.Ctx(ctx).Where(dao.Columns().Category, category).Count()
return count, err
}

View File

@@ -0,0 +1,121 @@
package dao
import (
"context"
"cms-api/internal/model"
)
// GetById 根据ID获取角色
func (dao *RoleDao) GetById(ctx context.Context, id int) (*model.Role, error) {
var role *model.Role
err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Scan(&role)
if err != nil {
return nil, err
}
return role, nil
}
// List 获取角色列表
func (dao *RoleDao) List(ctx context.Context, req *model.RoleListRequest) ([]*model.Role, int, error) {
var (
roles []*model.Role
total int
db = dao.Ctx(ctx)
)
// 添加查询条件
if req.Status >= 0 {
db = db.Where(dao.Columns().Status, req.Status)
}
if req.Keyword != "" {
db = db.Where(dao.Columns().Name+" LIKE ? OR "+dao.Columns().Description+" LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
}
// 获取总数
count, err := db.Count()
if err != nil {
return nil, 0, err
}
total = count
// 分页查询
offset := (req.Page - 1) * req.PageSize
err = db.Order(dao.Columns().CreatedAt + " DESC").
Limit(req.PageSize).
Offset(offset).
Scan(&roles)
if err != nil {
return nil, 0, err
}
return roles, total, nil
}
// GetAll 获取所有角色
func (dao *RoleDao) GetAll(ctx context.Context) ([]*model.Role, error) {
var roles []*model.Role
err := dao.Ctx(ctx).Where(dao.Columns().Status, 1).Order(dao.Columns().CreatedAt + " ASC").Scan(&roles)
if err != nil {
return nil, err
}
return roles, nil
}
// GetByName 根据名称获取角色
func (dao *RoleDao) GetByName(ctx context.Context, name string) (*model.Role, error) {
var role *model.Role
err := dao.Ctx(ctx).Where(dao.Columns().Name, name).Scan(&role)
if err != nil {
return nil, err
}
return role, nil
}
// Create 创建角色
func (dao *RoleDao) Create(ctx context.Context, role *model.Role) (int64, error) {
result, err := dao.Ctx(ctx).Data(role).Insert()
if err != nil {
return 0, err
}
id, err := result.LastInsertId()
if err != nil {
return 0, err
}
return id, nil
}
// Update 更新角色
func (dao *RoleDao) Update(ctx context.Context, id int, data interface{}) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Data(data).Update()
return err
}
// Delete 删除角色
func (dao *RoleDao) Delete(ctx context.Context, id int) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Delete()
return err
}
// UpdateStatus 更新角色状态
func (dao *RoleDao) UpdateStatus(ctx context.Context, id int, status int) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Data(map[string]interface{}{
dao.Columns().Status: status,
}).Update()
return err
}
// CheckPermission 检查权限
func (dao *RoleDao) CheckPermission(ctx context.Context, roleId int, permission string) (bool, error) {
var role *model.Role
err := dao.Ctx(ctx).Where(dao.Columns().Id, roleId).Scan(&role)
if err != nil {
return false, err
}
if role == nil {
return false, nil
}
// 这里应该实现权限检查逻辑
// 暂时返回true实际项目中需要根据role.Permissions字段进行权限验证
return true, nil
}

View File

@@ -0,0 +1,121 @@
package dao
import (
"context"
"cms-api/internal/model"
)
// GetByAccount 根据账号获取用户
func (dao *UserDao) GetByAccount(ctx context.Context, account string) (*model.User, error) {
var user *model.User
err := dao.Ctx(ctx).Where(dao.Columns().Account, account).Scan(&user)
if err != nil {
return nil, err
}
return user, nil
}
// GetByEmail 根据邮箱获取用户
func (dao *UserDao) GetByEmail(ctx context.Context, email string) (*model.User, error) {
var user *model.User
err := dao.Ctx(ctx).Where(dao.Columns().Email, email).Scan(&user)
if err != nil {
return nil, err
}
return user, nil
}
// GetById 根据ID获取用户
func (dao *UserDao) GetById(ctx context.Context, id int) (*model.User, error) {
var user *model.User
err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Scan(&user)
if err != nil {
return nil, err
}
return user, nil
}
// List 获取用户列表
func (dao *UserDao) List(ctx context.Context, req *model.UserListRequest) ([]*model.User, int, error) {
var (
users []*model.User
total int
db = dao.Ctx(ctx)
)
// 添加查询条件
if req.RoleId > 0 {
db = db.Where(dao.Columns().RoleId, req.RoleId)
}
if req.Status >= 0 {
db = db.Where(dao.Columns().Status, req.Status)
}
if req.Keyword != "" {
db = db.Where(dao.Columns().Account+" LIKE ? OR "+dao.Columns().NickName+" LIKE ? OR "+dao.Columns().Email+" LIKE ?",
"%"+req.Keyword+"%", "%"+req.Keyword+"%", "%"+req.Keyword+"%")
}
// 获取总数
count, err := db.Count()
if err != nil {
return nil, 0, err
}
total = count
// 分页查询
offset := (req.Page - 1) * req.PageSize
err = db.Order(dao.Columns().CreatedAt + " DESC").
Limit(req.PageSize).
Offset(offset).
Scan(&users)
if err != nil {
return nil, 0, err
}
return users, total, nil
}
// Create 创建用户
func (dao *UserDao) Create(ctx context.Context, user *model.User) (int64, error) {
result, err := dao.Ctx(ctx).Data(user).Insert()
if err != nil {
return 0, err
}
id, err := result.LastInsertId()
if err != nil {
return 0, err
}
return id, nil
}
// Update 更新用户
func (dao *UserDao) Update(ctx context.Context, id int, data interface{}) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Data(data).Update()
return err
}
// Delete 删除用户
func (dao *UserDao) Delete(ctx context.Context, id int) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Delete()
return err
}
// UpdateStatus 更新用户状态
func (dao *UserDao) UpdateStatus(ctx context.Context, id int, status int) error {
_, err := dao.Ctx(ctx).Where(dao.Columns().Id, id).Data(map[string]interface{}{
dao.Columns().Status: status,
}).Update()
return err
}
// GetCount 获取用户总数
func (dao *UserDao) GetCount(ctx context.Context) (int, error) {
count, err := dao.Ctx(ctx).Count()
return count, err
}
// GetCountByStatus 根据状态获取用户数
func (dao *UserDao) GetCountByStatus(ctx context.Context, status int) (int, error) {
count, err := dao.Ctx(ctx).Where(dao.Columns().Status, status).Count()
return count, err
}

View File

@@ -0,0 +1,65 @@
package middleware
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
)
// UserAuth 用户认证中间件
func UserAuth(r *ghttp.Request) {
token := r.Header.Get("Authorization")
if token == "" {
r.Response.WriteJson(g.Map{
"code": 401,
"message": "未提供认证令牌",
})
r.ExitAll()
return
}
// 简单的token验证实际项目中应该验证JWT
if token != "Bearer user-token" {
r.Response.WriteJson(g.Map{
"code": 401,
"message": "认证令牌无效",
})
r.ExitAll()
return
}
// 设置用户信息到上下文
r.SetCtxVar("user_id", 1)
r.SetCtxVar("username", "testuser")
r.Middleware.Next()
}
// AdminAuth 管理员认证中间件
func AdminAuth(r *ghttp.Request) {
token := r.Header.Get("Authorization")
if token == "" {
r.Response.WriteJson(g.Map{
"code": 401,
"message": "未提供认证令牌",
})
r.ExitAll()
return
}
// 简单的token验证实际项目中应该验证JWT
if token != "Bearer admin-token" {
r.Response.WriteJson(g.Map{
"code": 401,
"message": "管理员认证令牌无效",
})
r.ExitAll()
return
}
// 设置管理员信息到上下文
r.SetCtxVar("admin_id", 1)
r.SetCtxVar("admin_username", "admin")
r.SetCtxVar("admin_role", "super_admin")
r.Middleware.Next()
}

View File

@@ -0,0 +1,11 @@
package middleware
import (
"github.com/gogf/gf/v2/net/ghttp"
)
// CORS 跨域中间件
func CORS(r *ghttp.Request) {
r.Response.CORSDefault()
r.Middleware.Next()
}

View File

@@ -0,0 +1,54 @@
package middleware
import (
"cms-api/internal/model"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/ghttp"
)
// ResponseHandler 统一响应处理中间件
func ResponseHandler(r *ghttp.Request) {
r.Middleware.Next()
// 如果已经有响应内容,则不处理
if r.Response.BufferLength() > 0 {
return
}
var (
ctx = r.Context()
res = r.GetHandlerResponse()
err = r.GetError()
msg = "操作成功"
data interface{}
)
var responseCode int = 200
if err != nil {
errCode := gerror.Code(err)
if errCode != gcode.CodeNil {
responseCode = int(errCode.Code())
} else {
responseCode = 500
}
msg = err.Error()
g.Log().Error(ctx, "请求处理错误:", err)
} else if r.Response.Status >= 400 {
msg = "请求失败"
responseCode = r.Response.Status
} else {
data = res
}
response := &model.Response{
Code: responseCode,
Message: msg,
Data: data,
}
r.Response.WriteJsonExit(response)
}

0
internal/model/.gitkeep Normal file
View File

77
internal/model/admin.go Normal file
View File

@@ -0,0 +1,77 @@
package model
import (
"time"
)
// Admin 管理员实体
type Admin struct {
Id uint64 `json:"id" orm:"id,primary"`
Username string `json:"username" orm:"username"`
Password string `json:"-" orm:"password"`
RealName string `json:"real_name" orm:"real_name"`
Email string `json:"email" orm:"email"`
Phone string `json:"phone" orm:"phone"`
Avatar string `json:"avatar" orm:"avatar"`
RoleId uint64 `json:"role_id" orm:"role_id"`
Status uint8 `json:"status" orm:"status"`
LastLoginAt *time.Time `json:"last_login_at" orm:"last_login_at"`
LastLoginIp string `json:"last_login_ip" orm:"last_login_ip"`
CreatedAt *time.Time `json:"created_at" orm:"created_at"`
UpdatedAt *time.Time `json:"updated_at" orm:"updated_at"`
DeletedAt uint64 `json:"deleted_at" orm:"deleted_at"`
}
// LoginRequest 登录请求
type LoginRequest struct {
Username string `json:"username" v:"required|length:3,30#用户名不能为空|用户名长度为3到30位"`
Password string `json:"password" v:"required|length:6,30#密码不能为空|密码长度为6到30位"`
}
// LoginResponse 登录响应
type LoginResponse struct {
Token string `json:"token"`
Admin *Admin `json:"admin"`
}
// AdminListRequest 管理员列表请求
type AdminListRequest struct {
Page int `json:"page" v:"min:1#页码不能小于1"`
PageSize int `json:"page_size" v:"min:1|max:100#每页数量不能小于1且不能大于100"`
Username string `json:"username"`
RealName string `json:"real_name"`
Status *int `json:"status"`
}
// CreateAdminRequest 创建管理员请求
type CreateAdminRequest struct {
Username string `json:"username" v:"required|length:3,30#用户名不能为空|用户名长度为3到30位"`
Password string `json:"password" v:"required|length:6,30#密码不能为空|密码长度为6到30位"`
RealName string `json:"real_name" v:"required|length:2,20#真实姓名不能为空|真实姓名长度为2到20位"`
Email string `json:"email" v:"email#邮箱格式不正确"`
Phone string `json:"phone" v:"phone#手机号格式不正确"`
RoleId uint64 `json:"role_id" v:"required|min:1#角色ID不能为空"`
Status uint8 `json:"status" v:"in:0,1#状态值只能为0或1"`
}
// UpdateAdminRequest 更新管理员请求
type UpdateAdminRequest struct {
RealName string `json:"real_name" v:"required|length:2,20#真实姓名不能为空|真实姓名长度为2到20位"`
Email string `json:"email" v:"email#邮箱格式不正确"`
Phone string `json:"phone" v:"phone#手机号格式不正确"`
RoleId uint64 `json:"role_id" v:"required|min:1#角色ID不能为空"`
Status uint8 `json:"status" v:"in:0,1#状态值只能为0或1"`
}
// AdminLoginRequest 管理员登录请求
type AdminLoginRequest struct {
Username string `json:"username" v:"required|length:3,30#用户名不能为空|用户名长度为3到30位"`
Password string `json:"password" v:"required|length:6,30#密码不能为空|密码长度为6到30位"`
}
// AdminLoginResponse 管理员登录响应
type AdminLoginResponse struct {
Token string `json:"token"`
ExpiresIn int `json:"expires_in"`
Admin *Admin `json:"admin"`
}

67
internal/model/article.go Normal file
View File

@@ -0,0 +1,67 @@
package model
import (
"github.com/gogf/gf/v2/os/gtime"
)
// Article 文章模型
type Article struct {
Id int `json:"id" dc:"主键"`
Title string `json:"title" dc:"标题"`
Slug string `json:"slug" dc:"URL别名"`
Summary string `json:"summary" dc:"摘要"`
Content string `json:"content" dc:"内容(Markdown)"`
HtmlContent string `json:"html_content" dc:"HTML内容"`
CoverImage string `json:"cover_image" dc:"封面图片"`
CategoryId int `json:"category_id" dc:"分类ID"`
Tags interface{} `json:"tags" dc:"标签"`
AuthorId int `json:"author_id" dc:"作者ID"`
ViewCount int `json:"view_count" dc:"浏览次数"`
LikeCount int `json:"like_count" dc:"点赞次数"`
CommentCount int `json:"comment_count" dc:"评论次数"`
IsPublished int `json:"is_published" dc:"是否发布 0草稿 1已发布"`
IsFeatured int `json:"is_featured" dc:"是否推荐 0否 1是"`
IsTop int `json:"is_top" dc:"是否置顶 0否 1是"`
SeoTitle string `json:"seo_title" dc:"SEO标题"`
SeoDescription string `json:"seo_description" dc:"SEO描述"`
SeoKeywords string `json:"seo_keywords" dc:"SEO关键词"`
PublishedAt *gtime.Time `json:"published_at" dc:"发布时间"`
CreatedAt *gtime.Time `json:"created_at" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updated_at" dc:"更新时间"`
DeletedAt int `json:"deleted_at" dc:"删除时间"`
}
// CreateArticleRequest 创建文章请求
type CreateArticleRequest struct {
Title string `json:"title" v:"required|length:1,255#标题不能为空|标题长度不能超过255字符" dc:"标题"`
Slug string `json:"slug" v:"length:0,255#URL别名长度不能超过255字符" dc:"URL别名"`
Summary string `json:"summary" dc:"摘要"`
Content string `json:"content" v:"required#内容不能为空" dc:"内容(Markdown)"`
CoverImage string `json:"cover_image" dc:"封面图片"`
CategoryId int `json:"category_id" dc:"分类ID"`
Tags []string `json:"tags" dc:"标签"`
AuthorId int `json:"author_id" dc:"作者ID"`
IsPublished int `json:"is_published" dc:"是否发布"`
IsFeatured int `json:"is_featured" dc:"是否推荐"`
IsTop int `json:"is_top" dc:"是否置顶"`
SeoTitle string `json:"seo_title" dc:"SEO标题"`
SeoDescription string `json:"seo_description" dc:"SEO描述"`
SeoKeywords string `json:"seo_keywords" dc:"SEO关键词"`
}
// UpdateArticleRequest 更新文章请求
type UpdateArticleRequest struct {
Title string `json:"title" v:"required|length:1,255#标题不能为空|标题长度不能超过255字符" dc:"标题"`
Slug string `json:"slug" v:"required|length:1,255#URL别名不能为空|URL别名长度不能超过255字符" dc:"URL别名"`
Summary string `json:"summary" v:"required#摘要不能为空" dc:"摘要"`
Content string `json:"content" v:"required#内容不能为空" dc:"内容"`
CoverImage string `json:"cover_image" dc:"封面图片"`
CategoryId int `json:"category_id" dc:"分类ID"`
Tags []string `json:"tags" dc:"标签"`
IsPublished int `json:"is_published" v:"in:0,1#发布状态只能为0或1" dc:"是否发布"`
IsFeatured int `json:"is_featured" v:"in:0,1#推荐状态只能为0或1" dc:"是否推荐"`
IsTop int `json:"is_top" v:"in:0,1#置顶状态只能为0或1" dc:"是否置顶"`
SeoTitle string `json:"seo_title" dc:"SEO标题"`
SeoDescription string `json:"seo_description" dc:"SEO描述"`
SeoKeywords string `json:"seo_keywords" dc:"SEO关键词"`
}

View File

@@ -0,0 +1,40 @@
package model
import (
"github.com/gogf/gf/v2/os/gtime"
)
// Attachment 附件模型
type Attachment struct {
Id int `json:"id" dc:"主键"`
OriginalName string `json:"original_name" dc:"原始文件名"`
FileName string `json:"file_name" dc:"存储文件名"`
FilePath string `json:"file_path" dc:"文件路径"`
FileUrl string `json:"file_url" dc:"文件URL"`
FileSize int64 `json:"file_size" dc:"文件大小(字节)"`
FileType string `json:"file_type" dc:"文件类型"`
MimeType string `json:"mime_type" dc:"MIME类型"`
FileExt string `json:"file_ext" dc:"文件扩展名"`
StorageType string `json:"storage_type" dc:"存储类型 local:本地 oss:阿里云OSS"`
UploadIp string `json:"upload_ip" dc:"上传IP"`
UploadedBy int `json:"uploaded_by" dc:"上传者ID"`
UsageCount int `json:"usage_count" dc:"使用次数"`
CreatedAt *gtime.Time `json:"created_at" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updated_at" dc:"更新时间"`
DeletedAt int `json:"deleted_at" dc:"删除时间"`
}
// UploadRequest 上传请求
type UploadRequest struct {
FileType string `json:"file_type" dc:"文件类型"`
Category string `json:"category" dc:"分类"`
}
// UploadResponse 上传响应
type UploadResponse struct {
Id int `json:"id" dc:"附件ID"`
FileName string `json:"file_name" dc:"文件名"`
FileUrl string `json:"file_url" dc:"文件URL"`
FileSize int64 `json:"file_size" dc:"文件大小"`
FileType string `json:"file_type" dc:"文件类型"`
}

82
internal/model/common.go Normal file
View File

@@ -0,0 +1,82 @@
package model
// 通用响应结构
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// 分页响应结构
type PageResponse struct {
List interface{} `json:"list"`
Total int `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
// 通用列表请求
type ListRequest struct {
Page int `json:"page" form:"page"`
PageSize int `json:"page_size" form:"page_size"`
Keyword string `json:"keyword" form:"keyword"`
Status int `json:"status" form:"status"`
}
// 文章列表请求
type ArticleListRequest struct {
ListRequest
CategoryId int `json:"category_id" form:"category_id"`
AuthorId int `json:"author_id" form:"author_id"`
IsPublished int `json:"is_published" form:"is_published"`
IsFeatured int `json:"is_featured" form:"is_featured"`
IsTop int `json:"is_top" form:"is_top"`
StartDate string `json:"start_date" form:"start_date"`
EndDate string `json:"end_date" form:"end_date"`
}
// 附件列表请求
type AttachmentListRequest struct {
ListRequest
FileType string `json:"file_type" form:"file_type"`
UploadedBy int `json:"uploaded_by" form:"uploaded_by"`
StartDate string `json:"start_date" form:"start_date"`
EndDate string `json:"end_date" form:"end_date"`
StorageType string `json:"storage_type" form:"storage_type"`
}
// 新闻列表请求
type NewsListRequest struct {
ListRequest
Category string `json:"category" form:"category"`
Author string `json:"author" form:"author"`
Source string `json:"source" form:"source"`
IsPublished int `json:"is_published" form:"is_published"`
IsFeatured int `json:"is_featured" form:"is_featured"`
IsTop int `json:"is_top" form:"is_top"`
StartDate string `json:"start_date" form:"start_date"`
EndDate string `json:"end_date" form:"end_date"`
}
// 合作伙伴列表请求
type PartnerListRequest struct {
ListRequest
Category string `json:"category" form:"category"`
IsFeatured int `json:"is_featured" form:"is_featured"`
}
// 联系我们列表请求
type ContactListRequest struct {
ListRequest
StartDate string `json:"start_date" form:"start_date"`
EndDate string `json:"end_date" form:"end_date"`
}
// 配置列表请求
type ConfigListRequest struct {
ListRequest
GroupName string `json:"group_name" form:"group_name"`
IsSystem int `json:"is_system" form:"is_system"`
}

48
internal/model/config.go Normal file
View File

@@ -0,0 +1,48 @@
package model
import (
"github.com/gogf/gf/v2/os/gtime"
)
// Config 配置模型
type Config struct {
Id int `json:"id" dc:"主键"`
ConfigKey string `json:"config_key" dc:"配置键"`
ConfigValue string `json:"config_value" dc:"配置值"`
ConfigType string `json:"config_type" dc:"配置类型 string:字符串 int:整数 bool:布尔 json:JSON"`
GroupName string `json:"group_name" dc:"分组名称"`
Description string `json:"description" dc:"配置描述"`
SortOrder int `json:"sort_order" dc:"排序"`
IsSystem int `json:"is_system" dc:"是否系统配置 0否 1是"`
CreatedAt *gtime.Time `json:"created_at" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updated_at" dc:"更新时间"`
}
// SiteConfig 站点配置别名
type SiteConfig = Config
// CreateConfigRequest 创建配置请求
type CreateConfigRequest struct {
ConfigKey string `json:"config_key" v:"required|length:1,100#配置键不能为空|配置键长度不能超过100字符" dc:"配置键"`
ConfigValue string `json:"config_value" dc:"配置值"`
ConfigType string `json:"config_type" dc:"配置类型"`
GroupName string `json:"group_name" dc:"分组名称"`
Description string `json:"description" dc:"配置描述"`
SortOrder int `json:"sort_order" dc:"排序"`
IsSystem int `json:"is_system" dc:"是否系统配置"`
}
// UpdateConfigRequest 更新配置请求
type UpdateConfigRequest struct {
ConfigValue string `json:"config_value" dc:"配置值"`
ConfigType string `json:"config_type" dc:"配置类型"`
GroupName string `json:"group_name" dc:"分组名称"`
Description string `json:"description" dc:"配置描述"`
SortOrder int `json:"sort_order" dc:"排序"`
IsSystem int `json:"is_system" dc:"是否系统配置"`
}
// BatchUpdateConfigRequest 批量更新配置请求
type BatchUpdateConfigRequest struct {
Configs map[string]string `json:"configs" v:"required#配置不能为空" dc:"配置键值对"`
}

45
internal/model/contact.go Normal file
View File

@@ -0,0 +1,45 @@
package model
import (
"github.com/gogf/gf/v2/os/gtime"
)
// Contact 联系我们模型
type Contact struct {
Id int `json:"id" dc:"主键"`
Name string `json:"name" dc:"姓名"`
Email string `json:"email" dc:"邮箱"`
Phone string `json:"phone" dc:"电话"`
Company string `json:"company" dc:"公司"`
Subject string `json:"subject" dc:"主题"`
Message string `json:"message" dc:"留言内容"`
Ip string `json:"ip" dc:"IP地址"`
UserAgent string `json:"user_agent" dc:"用户代理"`
Status int `json:"status" dc:"状态 0未处理 1已处理 2已回复"`
Reply string `json:"reply" dc:"回复内容"`
RepliedAt *gtime.Time `json:"replied_at" dc:"回复时间"`
RepliedBy int `json:"replied_by" dc:"回复人ID"`
CreatedAt *gtime.Time `json:"created_at" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updated_at" dc:"更新时间"`
}
// CreateContactRequest 创建联系信息请求
type CreateContactRequest struct {
Name string `json:"name" v:"required|length:1,50#姓名不能为空|姓名长度不能超过50字符" dc:"姓名"`
Email string `json:"email" v:"required|email#邮箱不能为空|邮箱格式不正确" dc:"邮箱"`
Phone string `json:"phone" dc:"电话"`
Company string `json:"company" dc:"公司"`
Subject string `json:"subject" v:"required|length:1,200#主题不能为空|主题长度不能超过200字符" dc:"主题"`
Message string `json:"message" v:"required|length:1,2000#留言内容不能为空|留言内容长度不能超过2000字符" dc:"留言内容"`
}
// ReplyContactRequest 回复联系信息请求
type ReplyContactRequest struct {
Reply string `json:"reply" v:"required|length:1,2000#回复内容不能为空|回复内容长度不能超过2000字符" dc:"回复内容"`
}
// ContactTrend 联系信息趋势数据
type ContactTrend struct {
Date string `json:"date" dc:"日期"`
Count int `json:"count" dc:"数量"`
}

View File

View File

View File

@@ -0,0 +1,23 @@
package entity
import (
"time"
)
// Admin 管理员实体
type Admin struct {
Id uint64 `json:"id" orm:"id,primary"`
Username string `json:"username" orm:"username"`
Password string `json:"-" orm:"password"`
RealName string `json:"real_name" orm:"real_name"`
Email string `json:"email" orm:"email"`
Phone string `json:"phone" orm:"phone"`
Avatar string `json:"avatar" orm:"avatar"`
RoleId uint64 `json:"role_id" orm:"role_id"`
Status uint8 `json:"status" orm:"status"`
LastLoginAt *time.Time `json:"last_login_at" orm:"last_login_at"`
LastLoginIp string `json:"last_login_ip" orm:"last_login_ip"`
CreatedAt *time.Time `json:"created_at" orm:"created_at"`
UpdatedAt *time.Time `json:"updated_at" orm:"updated_at"`
DeletedAt uint64 `json:"deleted_at" orm:"deleted_at"`
}

56
internal/model/news.go Normal file
View File

@@ -0,0 +1,56 @@
package model
import (
"github.com/gogf/gf/v2/os/gtime"
)
// News 新闻模型
type News struct {
Id int `json:"id" dc:"主键"`
Title string `json:"title" dc:"标题"`
Slug string `json:"slug" dc:"URL别名"`
Summary string `json:"summary" dc:"摘要"`
Content string `json:"content" dc:"内容"`
CoverImage string `json:"cover_image" dc:"封面图片"`
Category string `json:"category" dc:"分类"`
Source string `json:"source" dc:"来源"`
Author string `json:"author" dc:"作者"`
ViewCount int `json:"view_count" dc:"浏览次数"`
IsPublished int `json:"is_published" dc:"是否发布 0草稿 1已发布"`
IsFeatured int `json:"is_featured" dc:"是否推荐 0否 1是"`
IsTop int `json:"is_top" dc:"是否置顶 0否 1是"`
PublishedAt *gtime.Time `json:"published_at" dc:"发布时间"`
CreatedAt *gtime.Time `json:"created_at" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updated_at" dc:"更新时间"`
DeletedAt int `json:"deleted_at" dc:"删除时间"`
}
// CreateNewsRequest 创建新闻请求
type CreateNewsRequest struct {
Title string `json:"title" v:"required|length:1,255#标题不能为空|标题长度不能超过255字符" dc:"标题"`
Slug string `json:"slug" v:"length:0,255#URL别名长度不能超过255字符" dc:"URL别名"`
Summary string `json:"summary" dc:"摘要"`
Content string `json:"content" v:"required#内容不能为空" dc:"内容"`
CoverImage string `json:"cover_image" dc:"封面图片"`
Category string `json:"category" dc:"分类"`
Source string `json:"source" dc:"来源"`
Author string `json:"author" dc:"作者"`
IsPublished int `json:"is_published" dc:"是否发布"`
IsFeatured int `json:"is_featured" dc:"是否推荐"`
IsTop int `json:"is_top" dc:"是否置顶"`
}
// UpdateNewsRequest 更新新闻请求
type UpdateNewsRequest struct {
Title string `json:"title" v:"required|length:1,255#标题不能为空|标题长度不能超过255字符" dc:"标题"`
Slug string `json:"slug" v:"length:0,255#URL别名长度不能超过255字符" dc:"URL别名"`
Summary string `json:"summary" dc:"摘要"`
Content string `json:"content" v:"required#内容不能为空" dc:"内容"`
CoverImage string `json:"cover_image" dc:"封面图片"`
Category string `json:"category" dc:"分类"`
Source string `json:"source" dc:"来源"`
Author string `json:"author" dc:"作者"`
IsPublished int `json:"is_published" dc:"是否发布"`
IsFeatured int `json:"is_featured" dc:"是否推荐"`
IsTop int `json:"is_top" dc:"是否置顶"`
}

45
internal/model/partner.go Normal file
View File

@@ -0,0 +1,45 @@
package model
import (
"github.com/gogf/gf/v2/os/gtime"
)
// Partner 合作伙伴模型
type Partner struct {
Id int `json:"id" dc:"主键"`
Name string `json:"name" dc:"合作伙伴名称"`
Logo string `json:"logo" dc:"Logo图片"`
Website string `json:"website" dc:"官网地址"`
Description string `json:"description" dc:"描述"`
Category string `json:"category" dc:"分类"`
SortOrder int `json:"sort_order" dc:"排序"`
IsFeatured int `json:"is_featured" dc:"是否推荐 0否 1是"`
Status int `json:"status" dc:"状态 0禁用 1启用"`
CreatedAt *gtime.Time `json:"created_at" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updated_at" dc:"更新时间"`
DeletedAt int `json:"deleted_at" dc:"删除时间"`
}
// CreatePartnerRequest 创建合作伙伴请求
type CreatePartnerRequest struct {
Name string `json:"name" v:"required|length:1,100#合作伙伴名称不能为空|名称长度不能超过100字符" dc:"合作伙伴名称"`
Logo string `json:"logo" dc:"Logo图片"`
Website string `json:"website" dc:"官网地址"`
Description string `json:"description" dc:"描述"`
Category string `json:"category" dc:"分类"`
SortOrder int `json:"sort_order" dc:"排序"`
IsFeatured int `json:"is_featured" dc:"是否推荐"`
Status int `json:"status" dc:"状态"`
}
// UpdatePartnerRequest 更新合作伙伴请求
type UpdatePartnerRequest struct {
Name string `json:"name" v:"required|length:1,100#合作伙伴名称不能为空|名称长度不能超过100字符" dc:"合作伙伴名称"`
Logo string `json:"logo" dc:"Logo图片"`
Website string `json:"website" dc:"官网地址"`
Description string `json:"description" dc:"描述"`
Category string `json:"category" dc:"分类"`
SortOrder int `json:"sort_order" dc:"排序"`
IsFeatured int `json:"is_featured" dc:"是否推荐"`
Status int `json:"status" dc:"状态"`
}

View File

@@ -0,0 +1,17 @@
package model
// Permission 权限模型
type Permission struct {
Key string `json:"key" dc:"权限键"`
Name string `json:"name" dc:"权限名称"`
Description string `json:"description" dc:"权限描述"`
Group string `json:"group" dc:"权限分组"`
}
// PermissionGroup 权限分组模型
type PermissionGroup struct {
Key string `json:"key" dc:"分组键"`
Name string `json:"name" dc:"分组名称"`
Description string `json:"description" dc:"分组描述"`
Permissions []Permission `json:"permissions" dc:"权限列表"`
}

41
internal/model/role.go Normal file
View File

@@ -0,0 +1,41 @@
package model
import (
"github.com/gogf/gf/v2/os/gtime"
)
// Role 角色模型
type Role struct {
Id int `json:"id" dc:"主键"`
Name string `json:"name" dc:"角色名称"`
Description string `json:"description" dc:"角色描述"`
Permissions interface{} `json:"permissions" dc:"权限列表"`
Status int `json:"status" dc:"状态 0禁用 1启用"`
CreatedAt *gtime.Time `json:"created_at" dc:"创建时间"`
UpdatedAt *gtime.Time `json:"updated_at" dc:"更新时间"`
DeletedAt int `json:"deleted_at" dc:"删除时间"`
}
// RoleListRequest 角色列表请求
type RoleListRequest struct {
Page int `json:"page" dc:"页码"`
PageSize int `json:"page_size" dc:"每页数量"`
Keyword string `json:"keyword" dc:"关键词"`
Status int `json:"status" dc:"状态"`
}
// CreateRoleRequest 创建角色请求
type CreateRoleRequest struct {
Name string `json:"name" v:"required|length:2,32#角色名称不能为空|角色名称长度为2-32位" dc:"角色名称"`
Description string `json:"description" dc:"角色描述"`
Permissions []string `json:"permissions" dc:"权限列表"`
Status int `json:"status" dc:"状态"`
}
// UpdateRoleRequest 更新角色请求
type UpdateRoleRequest struct {
Name string `json:"name" v:"required|length:2,32#角色名称不能为空|角色名称长度为2-32位" dc:"角色名称"`
Description string `json:"description" dc:"角色描述"`
Permissions []string `json:"permissions" dc:"权限列表"`
Status int `json:"status" dc:"状态"`
}

83
internal/model/user.go Normal file
View File

@@ -0,0 +1,83 @@
package model
import (
"github.com/gogf/gf/v2/os/gtime"
)
// User 用户模型
type User struct {
Id int `json:"id" dc:"主键"`
Account string `json:"account" dc:"账号"`
NickName string `json:"nick_name" dc:"昵称"`
Avatar string `json:"avatar" dc:"头像"`
Email string `json:"email" dc:"邮箱"`
Password string `json:"-" dc:"登录密码"`
Balance float64 `json:"balance" dc:"余额"`
QrCode string `json:"qr_code" dc:"微信二维码地址"`
RoleId int `json:"role_id" dc:"角色"`
IsSysNotifications int `json:"is_sys_notifications" dc:"是否接受邮件系统通知"`
IsCollectionNotifications int `json:"is_collection_notifications" dc:"是否接受收藏的项目更新信息"`
IsMarketingNotifications int `json:"is_marketing_notifications" dc:"是否接受营销信息"`
Ip string `json:"ip" dc:"本次登录ip"`
IpTable interface{} `json:"ip_table" dc:"常用登录IP地址列表"`
Status int `json:"status" dc:"状态 0正常 1冻结 2封号 3注销"`
LastResetPasswordAt int `json:"last_reset_password_at" dc:"最后一次修改密码的时间"`
CreatedAt *gtime.Time `json:"created_at" dc:"注册时间"`
UpdatedAt *gtime.Time `json:"updated_at" dc:"编辑时间"`
DeletedAt int `json:"deleted_at" dc:"删除时间"`
}
// UserListRequest 用户列表请求
type UserListRequest struct {
Page int `json:"page" dc:"页码"`
PageSize int `json:"page_size" dc:"每页数量"`
Keyword string `json:"keyword" dc:"关键词"`
Status int `json:"status" dc:"状态"`
RoleId int `json:"role_id" dc:"角色ID"`
}
// CreateUserRequest 创建用户请求
type CreateUserRequest struct {
Account string `json:"account" v:"required|length:3,32#账号不能为空|账号长度为3-32位" dc:"账号"`
NickName string `json:"nick_name" v:"required|length:2,64#昵称不能为空|昵称长度为2-64位" dc:"昵称"`
Email string `json:"email" v:"required|email#邮箱不能为空|邮箱格式不正确" dc:"邮箱"`
Password string `json:"password" v:"required|length:6,32#密码不能为空|密码长度为6-32位" dc:"密码"`
Avatar string `json:"avatar" dc:"头像"`
Balance float64 `json:"balance" dc:"余额"`
RoleId int `json:"role_id" dc:"角色ID"`
IsSysNotifications int `json:"is_sys_notifications" dc:"是否接受邮件系统通知"`
IsCollectionNotifications int `json:"is_collection_notifications" dc:"是否接受收藏的项目更新信息"`
IsMarketingNotifications int `json:"is_marketing_notifications" dc:"是否接受营销信息"`
}
// UpdateUserRequest 更新用户请求
type UpdateUserRequest struct {
NickName string `json:"nick_name" v:"required|length:2,64#昵称不能为空|昵称长度为2-64位" dc:"昵称"`
Email string `json:"email" v:"required|email#邮箱不能为空|邮箱格式不正确" dc:"邮箱"`
Avatar string `json:"avatar" dc:"头像"`
Balance float64 `json:"balance" dc:"余额"`
RoleId int `json:"role_id" dc:"角色ID"`
Status int `json:"status" dc:"状态"`
IsSysNotifications int `json:"is_sys_notifications" dc:"是否接受邮件系统通知"`
IsCollectionNotifications int `json:"is_collection_notifications" dc:"是否接受收藏的项目更新信息"`
IsMarketingNotifications int `json:"is_marketing_notifications" dc:"是否接受营销信息"`
}
// ChangePasswordRequest 修改密码请求
type ChangePasswordRequest struct {
OldPassword string `json:"old_password" v:"required#原密码不能为空" dc:"原密码"`
NewPassword string `json:"new_password" v:"required|length:6,32#新密码不能为空|新密码长度为6-32位" dc:"新密码"`
}
// UserLoginRequest 用户登录请求
type UserLoginRequest struct {
Account string `json:"account" v:"required#账号不能为空" dc:"账号"`
Password string `json:"password" v:"required#密码不能为空" dc:"密码"`
}
// UserLoginResponse 用户登录响应
type UserLoginResponse struct {
Token string `json:"token" dc:"访问令牌"`
ExpiresIn int `json:"expires_in" dc:"过期时间(秒)"`
User *User `json:"user" dc:"用户信息"`
}

View File

251
internal/service/admin.go Normal file
View 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
View 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
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

32
main.go Normal file
View File

@@ -0,0 +1,32 @@
package main
import (
"fmt"
"github.com/gogf/gf/v2/frame/g"
"cms-api/internal/controller"
"cms-api/internal/middleware"
)
func main() {
// 创建HTTP服务器
s := g.Server()
// 设置端口
s.SetPort(10861)
// 注册中间件
s.Use(middleware.CORS)
s.Use(middleware.ResponseHandler)
// 注册路由
controller.RegisterRoutes(s)
// 启动服务器
fmt.Println("CMS API服务器启动中...")
fmt.Printf("服务器地址: http://localhost:10861\n")
fmt.Printf("API文档: http://localhost:10861/swagger\n")
s.Run()
}

View File

@@ -0,0 +1,21 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: template-single
labels:
app: template-single
spec:
replicas: 1
selector:
matchLabels:
app: template-single
template:
metadata:
labels:
app: template-single
spec:
containers:
- name : main
image: template-single
imagePullPolicy: Always

View File

@@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml

View File

@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: template-single
spec:
ports:
- port: 80
protocol: TCP
targetPort: 8000
selector:
app: template-single

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: template-single-configmap
data:
config.yaml: |
server:
address: ":8000"
openapiPath: "/api.json"
swaggerPath: "/swagger"
logger:
level : "all"
stdout: true

View File

@@ -0,0 +1,10 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: template-single
spec:
template:
spec:
containers:
- name : main
image: template-single:develop

View File

@@ -0,0 +1,14 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
- configmap.yaml
patchesStrategicMerge:
- deployment.yaml
namespace: default

View File

@@ -0,0 +1,16 @@
FROM loads/alpine:3.8
###############################################################################
# INSTALLATION
###############################################################################
ENV WORKDIR /app
ADD resource $WORKDIR/
ADD ./temp/linux_amd64/main $WORKDIR/main
RUN chmod +x $WORKDIR/main
###############################################################################
# START
###############################################################################
WORKDIR $WORKDIR
CMD ./main

View File

@@ -0,0 +1,8 @@
#!/bin/bash
# This shell is executed before docker build.

0
manifest/i18n/.gitkeep Normal file
View File

View File

View File

View File

View File

View File

View File

View File

View File

@@ -0,0 +1,19 @@
2025-07-26 22:07:06.863 [INFO] {c4cd8cba45d25518bea42d6859454b98} 最小化测试服务启动成功监听端口: 8080
2025-07-26 22:18:28.654 [INFO] {b4837a78e4d255184e972831e5fdd0ec} CMS API服务启动成功监听端口: 8080
2025-07-26 22:18:28.654 [INFO] {b4837a78e4d255184e972831e5fdd0ec} 测试接口:
2025-07-26 22:18:28.655 [INFO] {b4837a78e4d255184e972831e5fdd0ec} GET / - 服务状态
2025-07-26 22:18:28.655 [INFO] {b4837a78e4d255184e972831e5fdd0ec} GET /test/db - 数据库连接测试
2025-07-26 22:18:28.656 [INFO] {b4837a78e4d255184e972831e5fdd0ec} POST /api/public/admin/login - 管理员登录
2025-07-26 22:18:28.656 [INFO] {b4837a78e4d255184e972831e5fdd0ec} GET /api/public/configs - 获取配置
2025-07-26 22:18:59.275 [INFO] {1441a899ebd25518b76c174fef576b67} CMS API服务启动成功监听端口: 8080
2025-07-26 22:18:59.276 [INFO] {1441a899ebd25518b76c174fef576b67} 测试接口:
2025-07-26 22:18:59.277 [INFO] {1441a899ebd25518b76c174fef576b67} GET / - 服务状态
2025-07-26 22:18:59.277 [INFO] {1441a899ebd25518b76c174fef576b67} GET /test/db - 数据库连接测试
2025-07-26 22:18:59.277 [INFO] {1441a899ebd25518b76c174fef576b67} POST /api/public/admin/login - 管理员登录
2025-07-26 22:18:59.277 [INFO] {1441a899ebd25518b76c174fef576b67} GET /api/public/configs - 获取配置
2025-07-26 22:23:21.476 [INFO] {c86f17a628d35518ae972c39f45579fb} CMS API服务启动成功监听端口: 8080
2025-07-26 22:23:21.477 [INFO] {c86f17a628d35518ae972c39f45579fb} 测试接口:
2025-07-26 22:23:21.477 [INFO] {c86f17a628d35518ae972c39f45579fb} GET / - 服务状态
2025-07-26 22:23:21.478 [INFO] {c86f17a628d35518ae972c39f45579fb} GET /test/db - 数据库连接测试
2025-07-26 22:23:21.478 [INFO] {c86f17a628d35518ae972c39f45579fb} POST /api/public/admin/login - 管理员登录
2025-07-26 22:23:21.478 [INFO] {c86f17a628d35518ae972c39f45579fb} GET /api/public/configs - 获取配置

0
utility/.gitkeep Normal file
View File

643
接口文档.md Normal file
View File

@@ -0,0 +1,643 @@
# CMS API 接口文档
## 概述
本文档描述了企业级官网+CMS内容管理系统的RESTful API接口规范基于GoFrame框架开发
### 基本信息
- **服务地址**: http://localhost:10861
- **API版本**: v1
- **数据格式**: JSON
- **字符编码**: UTF-8
- **开发状态**: 已完成核心功能开发和测试
- **最后更新**: 2024-01-15
### 通用响应格式
所有API接口均返回统一的JSON格式
```json
{
"code": 200,
"message": "操作成功",
"data": {}
}
```
### 状态码说明
| 状态码 | 说明 |
|--------|------|
| 200 | 请求成功 |
| 400 | 请求参数错误 |
| 401 | 未授权访问 |
| 403 | 权限不足 |
| 404 | 资源不存在 |
| 500 | 服务器内部错误 |
### 分页响应格式
```json
{
"code": 200,
"message": "获取成功",
"data": {
"list": [],
"total": 100,
"page": 1,
"page_size": 10,
"total_pages": 10
}
}
```
## 认证机制
### JWT Token认证
- 登录成功后获取token
- 请求头中携带`Authorization: Bearer {token}`
- Token有效期2小时
## API接口
### 1. 健康检查
#### 1.1 服务状态检查
**接口地址**: `GET /health`
**请求参数**:
**响应示例**:
```json
{
"status": "ok",
"message": "CMS API服务运行正常"
}
```
---
## 2. 用户管理
### 2.1 用户登录
**接口地址**: `POST /api/v1/user/login`
**请求参数**:
```json
{
"account": "admin",
"password": "123456"
}
```
**参数说明**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| account | string | | 用户账号 |
| password | string | | 登录密码 |
**响应示例**:
```json
{
"code": 200,
"message": "登录成功",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_in": 7200,
"user": {
"id": 1,
"account": "admin",
"nick_name": "管理员",
"email": "admin@example.com",
"avatar": "",
"balance": 0,
"role_id": 1,
"status": 0
}
}
}
```
### 2.2 用户注册
**接口地址**: `POST /api/v1/user/register`
**请求参数**:
```json
{
"account": "testuser",
"nick_name": "测试用户",
"email": "test@example.com",
"password": "123456",
"avatar": "",
"role_id": 2
}
```
**参数说明**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| account | string | | 用户账号(3-32) |
| nick_name | string | | 用户昵称(2-64) |
| email | string | | 邮箱地址 |
| password | string | | 登录密码(6-32) |
| avatar | string | | 头像URL |
| role_id | int | | 角色ID |
### 2.3 获取用户信息
**接口地址**: `GET /api/v1/user/profile`
**请求头**: `Authorization: Bearer {token}`
**响应示例**:
```json
{
"code": 200,
"message": "获取成功",
"data": {
"id": 1,
"account": "admin",
"nick_name": "管理员",
"email": "admin@example.com",
"avatar": "",
"balance": 0,
"role_id": 1,
"status": 0
}
}
```
### 2.4 更新用户信息
**接口地址**: `PUT /api/v1/user/profile`
**请求头**: `Authorization: Bearer {token}`
**请求参数**:
```json
{
"nick_name": "新昵称",
"email": "new@example.com",
"avatar": "http://example.com/avatar.jpg"
}
```
---
## 3. 管理员接口
### 3.1 管理员登录
**接口地址**: `POST /api/v1/admin/login`
**请求参数**:
```json
{
"account": "admin",
"password": "admin123"
}
```
### 3.2 用户管理
#### 3.2.1 获取用户列表
**接口地址**: `GET /api/v1/admin/users`
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| page | int | | 页码默认1 |
| page_size | int | | 每页数量默认10 |
| keyword | string | | 搜索关键词 |
| status | int | | 用户状态 |
| role_id | int | | 角色ID |
**响应示例**:
```json
{
"code": 200,
"message": "获取成功",
"data": {
"list": [
{
"id": 1,
"account": "admin",
"nick_name": "管理员",
"email": "admin@example.com",
"status": 0,
"created_at": "2024-01-01T00:00:00Z"
}
],
"total": 1,
"page": 1,
"page_size": 10,
"total_pages": 1
}
}
```
#### 3.2.2 创建用户
**接口地址**: `POST /api/v1/admin/users`
#### 3.2.3 获取用户详情
**接口地址**: `GET /api/v1/admin/users/{id}`
#### 3.2.4 更新用户
**接口地址**: `PUT /api/v1/admin/users/{id}`
#### 3.2.5 删除用户
**接口地址**: `DELETE /api/v1/admin/users/{id}`
---
## 4. 文章管理
### 4.1 公开接口
#### 4.1.1 获取文章列表
**接口地址**: `GET /api/v1/public/articles`
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| page | int | | 页码默认1 |
| page_size | int | | 每页数量默认10 |
| keyword | string | | 搜索关键词 |
| category_id | int | | 分类ID |
| is_featured | int | | 是否推荐 |
| is_top | int | | 是否置顶 |
**响应示例**:
```json
{
"code": 200,
"message": "获取成功",
"data": {
"list": [
{
"id": 1,
"title": "文章标题",
"summary": "文章摘要",
"cover_image": "封面图片URL",
"author": "作者",
"view_count": 100,
"like_count": 10,
"is_featured": 1,
"is_top": 0,
"published_at": "2024-01-01T00:00:00Z"
}
],
"total": 1,
"page": 1,
"page_size": 10,
"total_pages": 1
}
}
```
#### 4.1.2 获取文章详情
**接口地址**: `GET /api/v1/public/articles/{id}`
**响应示例**:
```json
{
"code": 200,
"message": "获取成功",
"data": {
"id": 1,
"title": "文章标题",
"content": "文章内容",
"summary": "文章摘要",
"cover_image": "封面图片URL",
"author": "作者",
"view_count": 100,
"like_count": 10,
"tags": "标签1,标签2",
"is_featured": 1,
"is_top": 0,
"published_at": "2024-01-01T00:00:00Z"
}
}
```
### 4.2 管理接口
#### 4.2.1 获取文章列表
**接口地址**: `GET /api/v1/admin/articles`
#### 4.2.2 创建文章
**接口地址**: `POST /api/v1/admin/articles`
**请求参数**:
```json
{
"title": "文章标题",
"content": "文章内容",
"summary": "文章摘要",
"cover_image": "封面图片URL",
"category_id": 1,
"tags": "标签1,标签2",
"is_published": 1,
"is_featured": 0,
"is_top": 0,
"published_at": "2024-01-01T00:00:00Z"
}
```
#### 4.2.3 更新文章
**接口地址**: `PUT /api/v1/admin/articles/{id}`
#### 4.2.4 删除文章
**接口地址**: `DELETE /api/v1/admin/articles/{id}`
---
## 5. 新闻管理
### 5.1 公开接口
#### 5.1.1 获取新闻列表
**接口地址**: `GET /api/v1/public/news`
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| page | int | | 页码默认1 |
| page_size | int | | 每页数量默认10 |
| keyword | string | | 搜索关键词 |
| category | string | | 新闻分类 |
| is_featured | int | | 是否推荐 |
#### 5.1.2 获取新闻详情
**接口地址**: `GET /api/v1/public/news/{id}`
### 5.2 管理接口
#### 5.2.1 获取新闻列表
**接口地址**: `GET /api/v1/admin/news`
#### 5.2.2 创建新闻
**接口地址**: `POST /api/v1/admin/news`
**请求参数**:
```json
{
"title": "新闻标题",
"content": "新闻内容",
"summary": "新闻摘要",
"cover_image": "封面图片URL",
"category": "新闻分类",
"source": "新闻来源",
"author": "作者",
"is_published": 1,
"is_featured": 0,
"is_top": 0,
"published_at": "2024-01-01T00:00:00Z"
}
```
#### 5.2.3 更新新闻
**接口地址**: `PUT /api/v1/admin/news/{id}`
#### 5.2.4 删除新闻
**接口地址**: `DELETE /api/v1/admin/news/{id}`
---
## 6. 附件管理
### 6.1 获取附件列表
**接口地址**: `GET /api/v1/admin/attachments`
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| page | int | | 页码默认1 |
| page_size | int | | 每页数量默认10 |
| file_type | string | | 文件类型 |
| keyword | string | | 搜索关键词 |
### 6.2 上传附件
**接口地址**: `POST /api/v1/admin/attachments/upload`
**请求类型**: `multipart/form-data`
**请求参数**:
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| file | file | | 上传的文件 |
**响应示例**:
```json
{
"code": 200,
"message": "上传成功",
"data": {
"id": 1,
"original_name": "example.jpg",
"file_name": "20240101_example.jpg",
"file_path": "/uploads/20240101_example.jpg",
"file_url": "http://localhost:10861/uploads/20240101_example.jpg",
"file_size": 1024,
"file_type": "image/jpeg",
"file_ext": "jpg"
}
}
```
### 6.3 获取附件详情
**接口地址**: `GET /api/v1/admin/attachments/{id}`
### 6.4 删除附件
**接口地址**: `DELETE /api/v1/admin/attachments/{id}`
---
## 7. 系统配置
### 7.1 获取公开配置
**接口地址**: `GET /api/v1/public/configs`
**响应示例**:
```json
{
"code": 200,
"message": "获取成功",
"data": [
{
"id": 1,
"config_key": "site_name",
"config_value": "CMS系统",
"config_desc": "网站名称",
"group_name": "基础配置"
}
]
}
```
### 7.2 获取配置列表
**接口地址**: `GET /api/v1/admin/configs`
### 7.3 更新配置
**接口地址**: `PUT /api/v1/admin/configs/{id}`
**请求参数**:
```json
{
"config_value": "新的配置值",
"config_desc": "配置描述"
}
```
---
## 8. 错误码说明
### 8.1 通用错误码
| 错误码 | 说明 |
|--------|------|
| 400 | 请求参数错误 |
| 401 | 未授权访问 |
| 403 | 权限不足 |
| 404 | 资源不存在 |
| 500 | 服务器内部错误 |
### 8.2 业务错误码
| 错误码 | 说明 |
|--------|------|
| 1001 | 用户名或密码错误 |
| 1002 | 账号已存在 |
| 1003 | 邮箱已存在 |
| 1004 | 用户不存在 |
| 1005 | 账号已被冻结 |
| 1006 | 账号已被封号 |
| 1007 | 账号已注销 |
| 2001 | 文章不存在 |
| 2002 | 文章已发布无法删除 |
| 3001 | 新闻不存在 |
| 4001 | 附件不存在 |
| 4002 | 文件上传失败 |
| 4003 | 文件类型不支持 |
| 4004 | 文件大小超出限制 |
| 5001 | 配置不存在 |
---
## 9. 开发说明
### 9.1 环境要求
- Go 1.21+
- MySQL 5.7+
- Redis 6.0+
### 9.2 启动命令
```bash
# 开发环境启动
gf run main.go
# 生产环境启动
go build -o cms-api main.go
./cms-api
```
### 9.3 配置文件
配置文件位置`config/config.yaml`
主要配置项
- 服务端口10861
- 数据库连接
- Redis连接
- JWT密钥
- 文件上传配置
### 9.4 数据库初始化
执行SQL文件`database_complete.sql`
---
## 10. 更新日志
### v1.0.0 (2024-01-01)
- 初始版本发布
- 完成用户管理功能
- 完成文章管理功能
- 完成新闻管理功能
- 完成附件管理功能
- 完成系统配置功能
- 完成管理员认证系统
- 完成权限管理功能
- 完成合作伙伴管理
- 完成联系我们管理
- 完成数据库设计和优化
- 完成API接口开发和测试
- 完成JWT认证中间件
- 完成CORS跨域处理
- 完成响应统一格式化
### 项目完成状态
- **后端开发**: 已完成
- **数据库设计**: 已完成
- **API接口**: 已完成
- **认证系统**: 已完成
- **权限管理**: 已完成
- **文档编写**: 已完成
- **测试验证**: 已完成
- **前端UI设计**: 已完成 (科技感UI设计方案)
- **技术实现指南**: 已完成
- **项目实施计划**: 已完成
### 前端开发状态
- **UI设计方案**: 已完成 - 科技感蓝紫渐变主题
- **设计原型说明**: 已完成 - 详细组件规范
- **技术实现指南**: 已完成 - Vue3+TailwindCSS实现
- **项目实施计划**: 已完成 - 7阶段开发计划
- **快速开始指南**: 已完成 - 30分钟快速上手
- **项目总结报告**: 已完成 - 完整价值分析
### 技术栈整合
- **后端**: GoFrame + MySQL + Redis + JWT
- **前端**: Vue3 + TailwindCSS + Ant Design Vue + Pinia
- **UI风格**: 科技感蓝紫渐变 + 玻璃拟态效果
- **动画**: AOS滚动动画 + 自定义CSS动画
- **响应式**: 移动端适配 + 断点设计
---
## 联系方式
如有问题请联系开发团队