dark 7 months ago
commit
3f5f26dd74
  1. 2
      .gitignore
  2. 159
      .vscode/tasks.json
  3. 1
      8.0
  4. 169
      ATTACHMENT_FIX_DETAILED.md
  5. 115
      ATTACHMENT_FIX_SUMMARY.md
  6. 179
      DEPLOYMENT.md
  7. 257
      DEVELOPMENT.md
  8. 201
      EDIT_TASK_ATTACHMENT_FIX.md
  9. 119
      FINAL_HOT_RELOAD_SUMMARY.md
  10. 326
      HOT_RELOAD_GUIDE.md
  11. 265
      PROJECT_SUMMARY.md
  12. 478
      README.md
  13. 52
      TASK_CREATE_DIAGNOSIS.md
  14. 106
      TASK_CREATE_FIX_SUMMARY.md
  15. 87
      TEST_GUIDE.md
  16. 96
      TROUBLESHOOTING.md
  17. 56
      backend/.air.toml
  18. 19
      backend/config.yaml
  19. 57
      backend/go.mod
  20. 182
      backend/go.sum
  21. 68
      backend/internal/config/config.go
  22. 179
      backend/internal/handler/auth/auth_test.go
  23. 11
      backend/internal/handler/auth/handler.go
  24. 9
      backend/internal/handler/auth/handler_test.go
  25. 72
      backend/internal/handler/auth/login.go
  26. 7
      backend/internal/handler/auth/logout.go
  27. 23
      backend/internal/handler/auth/me.go
  28. 36
      backend/internal/handler/auth/refresh.go
  29. 50
      backend/internal/handler/auth/register.go
  30. 8
      backend/internal/handler/auth/test-auth.bat
  31. 54
      backend/internal/handler/auth/test_login.go
  32. 36
      backend/internal/handler/organization/create_organization.go
  33. 9
      backend/internal/handler/organization/create_organization_test.go
  34. 35
      backend/internal/handler/organization/delete_organization.go
  35. 9
      backend/internal/handler/organization/delete_organization_test.go
  36. 44
      backend/internal/handler/organization/get_organization.go
  37. 9
      backend/internal/handler/organization/get_organization_test.go
  38. 36
      backend/internal/handler/organization/get_organization_users.go
  39. 9
      backend/internal/handler/organization/get_organization_users_test.go
  40. 43
      backend/internal/handler/organization/get_organizations.go
  41. 9
      backend/internal/handler/organization/get_organizations_test.go
  42. 11
      backend/internal/handler/organization/handler.go
  43. 9
      backend/internal/handler/organization/handler_test.go
  44. 65
      backend/internal/handler/organization/update_organization.go
  45. 9
      backend/internal/handler/organization/update_organization_test.go
  46. 7
      backend/internal/handler/statistics/get_overview.go
  47. 9
      backend/internal/handler/statistics/get_overview_test.go
  48. 7
      backend/internal/handler/statistics/get_task_statistics.go
  49. 9
      backend/internal/handler/statistics/get_task_statistics_test.go
  50. 7
      backend/internal/handler/statistics/get_user_statistics.go
  51. 9
      backend/internal/handler/statistics/get_user_statistics_test.go
  52. 11
      backend/internal/handler/statistics/handler.go
  53. 9
      backend/internal/handler/statistics/handler_test.go
  54. 9
      backend/internal/handler/task/add_task_attachment.go
  55. 9
      backend/internal/handler/task/add_task_attachment_test.go
  56. 9
      backend/internal/handler/task/add_task_comment.go
  57. 9
      backend/internal/handler/task/add_task_comment_test.go
  58. 9
      backend/internal/handler/task/create_task.go
  59. 9
      backend/internal/handler/task/create_task_test.go
  60. 9
      backend/internal/handler/task/delete_task.go
  61. 9
      backend/internal/handler/task/delete_task_attachment.go
  62. 9
      backend/internal/handler/task/delete_task_attachment_test.go
  63. 9
      backend/internal/handler/task/delete_task_comment.go
  64. 9
      backend/internal/handler/task/delete_task_comment_test.go
  65. 9
      backend/internal/handler/task/delete_task_test.go
  66. 9
      backend/internal/handler/task/get_task.go
  67. 9
      backend/internal/handler/task/get_task_attachments.go
  68. 9
      backend/internal/handler/task/get_task_attachments_test.go
  69. 9
      backend/internal/handler/task/get_task_comments.go
  70. 9
      backend/internal/handler/task/get_task_comments_test.go
  71. 9
      backend/internal/handler/task/get_task_test.go
  72. 9
      backend/internal/handler/task/get_tasks.go
  73. 9
      backend/internal/handler/task/get_tasks_test.go
  74. 27
      backend/internal/handler/task/handler.go
  75. 9
      backend/internal/handler/task/handler_test.go
  76. 9
      backend/internal/handler/task/update_task.go
  77. 9
      backend/internal/handler/task/update_task_status.go
  78. 9
      backend/internal/handler/task/update_task_status_test.go
  79. 9
      backend/internal/handler/task/update_task_test.go
  80. 9
      backend/internal/handler/task/upload_file.go
  81. 9
      backend/internal/handler/task/upload_file_test.go
  82. 35
      backend/internal/handler/user/delete_user.go
  83. 44
      backend/internal/handler/user/get_user.go
  84. 43
      backend/internal/handler/user/get_users.go
  85. 11
      backend/internal/handler/user/handler.go
  86. 65
      backend/internal/handler/user/update_user.go
  87. 55
      backend/internal/middleware/auth.go
  88. 102
      backend/internal/router/router.go
  89. 39
      backend/main.go
  90. 108
      backend/model/model.go
  91. 62
      backend/pkg/auth/jwt.go
  92. 161
      backend/pkg/database/database.go
  93. 57
      backend/pkg/logger/logger.go
  94. 17
      backend/test-auth.bat
  95. BIN
      backend/uploads/1751943286377256500_sample_main.xlsx
  96. BIN
      backend/uploads/1751943733448215600_sample_main.xlsx
  97. BIN
      backend/uploads/1751944539004522700_sample_main.xlsx
  98. 185
      database/init.sql
  99. 88
      dev.bat
  100. 61
      dev.sh

2
.gitignore

@ -0,0 +1,2 @@
node_modules
node_modules/

159
.vscode/tasks.json

@ -0,0 +1,159 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "安装前端依赖",
"type": "shell",
"command": "npm",
"args": ["install"],
"group": "build",
"options": {
"cwd": "${workspaceFolder}/frontend"
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared"
},
"problemMatcher": []
},
{
"label": "启动前端开发服务器",
"type": "shell",
"command": "npm",
"args": ["run", "dev"],
"group": "build",
"options": {
"cwd": "${workspaceFolder}/frontend"
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "new"
},
"isBackground": true,
"problemMatcher": {
"pattern": {
"regexp": "^(.*)$",
"file": 1
},
"background": {
"activeOnStart": true,
"beginsPattern": ".*Local:.*",
"endsPattern": ".*ready in.*"
}
}
},
{
"label": "构建前端",
"type": "shell",
"command": "npm",
"args": ["run", "build"],
"group": "build",
"options": {
"cwd": "${workspaceFolder}/frontend"
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared"
},
"problemMatcher": []
},
{
"label": "安装后端依赖",
"type": "shell",
"command": "go",
"args": ["mod", "tidy"],
"group": "build",
"options": {
"cwd": "${workspaceFolder}/backend"
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared"
},
"problemMatcher": []
},
{
"label": "启动后端服务器",
"type": "shell",
"command": "go",
"args": ["run", "main.go"],
"group": "build",
"options": {
"cwd": "${workspaceFolder}/backend"
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "new"
},
"isBackground": true,
"problemMatcher": {
"pattern": {
"regexp": "^(.*)$",
"file": 1
},
"background": {
"activeOnStart": true,
"beginsPattern": ".*Server starting.*",
"endsPattern": ".*Listening and serving.*"
}
}
},
{
"label": "构建后端",
"type": "shell",
"command": "go",
"args": ["build", "-o", "task-track", "main.go"],
"group": "build",
"options": {
"cwd": "${workspaceFolder}/backend"
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared"
},
"problemMatcher": ["$go"]
},
{
"label": "启动完整应用",
"dependsOrder": "parallel",
"dependsOn": [
"启动前端开发服务器",
"启动后端服务器"
],
"group": "build",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "new"
}
},
{
"label": "安装所有依赖",
"dependsOrder": "sequence",
"dependsOn": [
"安装前端依赖",
"安装后端依赖"
],
"group": "build",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared"
}
}
]
}

1
8.0

@ -0,0 +1 @@
⚠️ MySQL 未检测到,请确保已安��?MySQL

169
ATTACHMENT_FIX_DETAILED.md

@ -0,0 +1,169 @@
# 附件上传功能"Failed to create attachment"问题修复详细说明
## 问题现象
当创建任务并添加附件时,显示"Failed to create attachment"错误。
## 问题分析
### 1. 原始问题定位
- 前端设置了 `:auto-upload="false"`,需要手动触发上传
- 上传成功后的数据处理可能存在字段匹配问题
- 附件关联到任务的API调用可能失败
### 2. 数据流分析
**后端 UploadFile API 响应格式:**
```json
{
"code": 200,
"message": "File uploaded successfully",
"data": {
"filename": "1736303317123456789_test.pdf",
"filepath": "./uploads/1736303317123456789_test.pdf",
"size": 12345,
"mime_type": "application/pdf"
}
}
```
**后端 TaskAttachment 模型字段:**
```go
type TaskAttachment struct {
ID uint `json:"id"`
TaskID uint `json:"task_id"`
FileName string `json:"file_name"` // 注意这里是下划线格式
FilePath string `json:"file_path"`
FileSize int64 `json:"file_size"`
FileType string `json:"file_type"`
UploadedBy uint `json:"uploaded_by"`
CreatedAt time.Time `json:"created_at"`
}
```
## 修复方案
### 1. 增强前端上传成功处理
```typescript
const handleUploadSuccess = (response: any, file: any, fileList: any[]) => {
console.log('Upload success - raw response:', response)
// 检查响应格式
if (!response || !response.data) {
ElMessage.error('文件上传响应格式错误')
return
}
ElMessage.success('文件上传成功')
// 将上传成功的文件信息添加到taskForm.attachments
const attachmentData = {
file_name: response.data.filename || file.name, // ✓ 字段匹配
file_path: response.data.filepath || response.data.path || '',
file_size: response.data.size || file.size || 0,
file_type: response.data.mime_type || file.type || ''
}
taskForm.attachments.push(attachmentData)
}
```
### 2. 优化文件上传时机控制
```typescript
const saveTask = async () => {
// 检查是否有需要上传的文件
const hasUnuploadedFiles = fileList.value.some((file: any) => !file.response)
if (hasUnuploadedFiles) {
console.log('Found unuploaded files, uploading...')
if (uploadRef.value) {
await uploadRef.value.submit()
// 等待确保上传完成
await new Promise(resolve => setTimeout(resolve, 1000))
}
}
// 创建任务...
// 然后处理附件关联...
}
```
### 3. 增强附件关联错误处理
```typescript
// 如果有附件,需要关联到任务
if (taskForm.attachments && taskForm.attachments.length > 0) {
for (let i = 0; i < taskForm.attachments.length; i++) {
const attachment = taskForm.attachments[i]
try {
await taskApi.addTaskAttachment(taskId, {
file_name: attachment.file_name,
file_path: attachment.file_path,
file_size: attachment.file_size,
file_type: attachment.file_type,
uploaded_by: 1
})
} catch (attachmentError: any) {
// 详细错误处理,附件失败不影响主任务创建
let errorMsg = `附件 "${attachment.file_name}" 添加失败`
if (attachmentError.response?.data?.message) {
errorMsg += `: ${attachmentError.response.data.message}`
}
ElMessage.warning(errorMsg)
}
}
}
```
### 4. 改进文件移除处理
```typescript
const handleFileRemove = (file: any, fileList: any[]) => {
const index = taskForm.attachments.findIndex(
(attachment: any) => {
const uploadedFileName = file.response?.data?.filename || file.name
return attachment.file_name === uploadedFileName
}
)
if (index > -1) {
taskForm.attachments.splice(index, 1)
}
}
```
## 测试方法
### 1. 基本附件上传测试
1. 启动前后端服务器
2. 访问 http://localhost:5173/tasks
3. 点击"创建任务"
4. 填写基本信息,拖拽文件到上传区域
5. 点击"确定"创建任务
6. 观察控制台调试信息和网络请求
### 2. 异常情况测试
- 测试大文件上传
- 测试不支持的文件格式
- 测试网络中断情况
- 测试后端服务异常情况
### 3. 调试信息
修复后的代码会在浏览器控制台输出详细调试信息:
- 上传成功时的原始响应
- 构造的附件数据
- 任务创建成功后的ID
- 每个附件的关联结果
## 关键改进点
1. **响应格式验证**:增加了对上传响应格式的检查
2. **字段映射确认**:确保前端构造的数据与后端模型字段一致
3. **错误隔离**:附件添加失败不影响主任务创建
4. **调试增强**:添加了详细的调试日志
5. **时序控制**:确保文件上传完成后再进行任务创建
## 预期结果
修复后,创建带附件的任务应该:
1. 文件正常上传到服务器
2. 任务创建成功
3. 附件正确关联到任务
4. 即使个别附件失败,也不影响整体流程
5. 用户得到明确的成功/失败反馈

115
ATTACHMENT_FIX_SUMMARY.md

@ -0,0 +1,115 @@
## 附件功能修复总结
### 🔍 问题诊断
**错误信息**: "Failed to create attachment"
**根本原因**: 前端发送给后端的附件数据字段名不匹配
### 🛠️ 修复内容
#### 1. 修复上传成功处理 (`handleUploadSuccess`)
**修复前**:
```javascript
taskForm.attachments.push({
filename: response.data.filename, // ❌ 错误字段名
filepath: response.data.filepath, // ❌ 错误字段名
file_size: response.data.size, // ✅ 正确
file_type: response.data.mime_type // ✅ 正确
})
```
**修复后**:
```javascript
taskForm.attachments.push({
file_name: response.data.filename, // ✅ 正确字段名
file_path: response.data.filepath, // ✅ 正确字段名
file_size: response.data.size, // ✅ 正确
file_type: response.data.mime_type // ✅ 正确
})
```
#### 2. 修复文件移除处理 (`handleFileRemove`)
**修复前**:
```javascript
attachment.filename === file.response?.data?.filename // ❌ 错误字段名
```
**修复后**:
```javascript
attachment.file_name === (file.response?.data?.filename || file.name) // ✅ 正确字段名
```
#### 3. 修复附件添加API调用
**修复前**:
```javascript
await taskApi.addTaskAttachment(taskId, {
file_name: attachment.filename, // ❌ 错误:应该是 attachment.file_name
file_path: attachment.filepath, // ❌ 错误:应该是 attachment.file_path
// ...
})
```
**修复后**:
```javascript
await taskApi.addTaskAttachment(taskId, {
file_name: attachment.file_name, // ✅ 正确
file_path: attachment.file_path, // ✅ 正确
file_size: attachment.file_size, // ✅ 正确
file_type: attachment.file_type, // ✅ 正确
uploaded_by: 1
})
```
#### 4. 增加错误处理
- 为附件添加单独的错误处理
- 附件添加失败不影响任务创建
- 显示具体的错误信息和警告
### 📋 后端API数据格式确认
**上传文件返回**:
```json
{
"code": 200,
"data": {
"filename": "唯一文件名",
"filepath": "./uploads/唯一文件名",
"size": 文件大小,
"mime_type": "文件类型"
}
}
```
**TaskAttachment 模型字段**:
```go
type TaskAttachment struct {
ID uint `json:"id"`
TaskID uint `json:"task_id"`
FileName string `json:"file_name"`
FilePath string `json:"file_path"`
FileSize int64 `json:"file_size"`
FileType string `json:"file_type"`
UploadedBy uint `json:"uploaded_by"`
}
```
### ✅ 修复结果
1. **字段名一致性**: 前端和后端字段名完全匹配
2. **错误处理**: 附件失败不会阻止任务创建
3. **调试信息**: 增加上传响应的调试输出
4. **用户体验**: 显示具体的错误信息
### 🧪 测试建议
1. 创建带附件的任务
2. 检查浏览器控制台的调试信息
3. 验证任务创建成功且附件正确关联
4. 测试文件上传失败的情况
**现在附件功能应该可以正常工作了!** 🎉

179
DEPLOYMENT.md

@ -0,0 +1,179 @@
# 任务管理系统部署指南
## 环境要求
### 后端环境
- Go 1.19+
- MySQL 8.0+
- Git
### 前端环境
- Node.js 16+
- npm 或 yarn
- 现代浏览器(Chrome, Firefox, Safari, Edge)
## 快速启动
### 方法一:使用启动脚本(推荐)
**Windows:**
```cmd
# 双击运行
start.bat
# 或命令行运行
.\start.bat
```
**Linux/macOS:**
```bash
chmod +x start.sh
./start.sh
```
### 方法二:手动启动
1. **启动后端服务器**
```bash
cd backend
go mod tidy
go run main.go
```
2. **启动前端服务器**
```bash
cd frontend
npm install
npm run dev
```
## 访问地址
- **前端应用**: http://localhost:5173
- **后端API**: http://localhost:8080
- **API文档**: 参考 `api_test_tasks.http` 文件
## 数据库配置
1. 创建MySQL数据库:
```sql
CREATE DATABASE task_track CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
2. 修改后端配置文件 `backend/config.yaml`
```yaml
database:
host: localhost
port: 3306
user: your_username
password: your_password
database: task_track
```
## 功能测试
### 1. 用户注册和登录
- 访问登录页面
- 注册新用户或使用测试账号登录
### 2. 创建任务
- 进入任务管理页面
- 点击"创建任务"按钮
- 填写任务信息
- 上传附件(支持拖拽)
- 保存任务
### 3. 管理附件
- 在任务列表中查看附件数量
- 点击附件数量查看详情
- 下载或删除附件
### 4. API测试
使用 `api_test_tasks.http` 文件测试所有API端点:
- 文件上传
- 任务CRUD操作
- 附件管理
- 搜索和筛选
## 目录结构
```
task_track/
├── frontend/ # Vue3前端应用
├── backend/ # Go后端服务
├── uploads/ # 文件上传目录
├── database/ # 数据库初始化脚本
├── api_test_tasks.http # API测试文件
├── start.bat # Windows启动脚本
├── start.sh # Linux/macOS启动脚本
└── README.md # 项目说明
```
## 故障排除
### 常见问题
1. **后端启动失败**
- 检查Go版本和环境变量
- 确认MySQL服务运行正常
- 检查数据库连接配置
2. **前端启动失败**
- 检查Node.js版本
- 删除 `node_modules` 重新安装依赖
- 检查端口5173是否被占用
3. **文件上传失败**
- 确认 `backend/uploads` 目录存在
- 检查目录写入权限
- 确认文件大小和格式限制
4. **API请求失败**
- 检查后端服务是否正常运行
- 确认CORS配置正确
- 检查认证token是否有效
### 日志查看
- **后端日志**: 控制台输出
- **前端日志**: 浏览器开发者工具
- **网络请求**: 浏览器Network面板
## 生产部署
### 后端部署
```bash
cd backend
go build -o task-track main.go
./task-track
```
### 前端部署
```bash
cd frontend
npm run build
# 将 dist/ 目录部署到Web服务器
```
### 环境变量
设置以下环境变量用于生产环境:
- `GIN_MODE=release`
- `DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASSWORD`, `DB_NAME`
- `JWT_SECRET`
## 安全注意事项
1. **文件上传安全**
- 限制文件类型和大小
- 扫描恶意文件
- 使用独立的文件存储服务
2. **API安全**
- 使用HTTPS协议
- 实现请求频率限制
- 定期更新JWT密钥
3. **数据库安全**
- 使用强密码
- 限制数据库访问权限
- 定期备份数据

257
DEVELOPMENT.md

@ -0,0 +1,257 @@
# 开发指南
## 项目启动
### 1. 环境准备
确保已安装以下环境:
- Node.js >= 16
- Go >= 1.21
- MySQL >= 8.0
### 2. 快速安装
运行安装脚本:
**Windows:**
```bash
install.bat
```
**Linux/macOS:**
```bash
chmod +x install.sh
./install.sh
```
### 3. 数据库配置
创建数据库:
```sql
CREATE DATABASE task_track CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
修改 `backend/config.yaml`
```yaml
database:
host: "localhost"
port: "3306"
username: "your_username" # 替换为你的用户名
password: "your_password" # 替换为你的密码
database: "task_track"
```
### 4. 启动服务
**启动后端(在 backend 目录下):**
```bash
cd backend
go run main.go
```
**启动前端(在 frontend 目录下):**
```bash
cd frontend
npm run dev
```
### 5. 访问应用
- 前端:http://localhost:5173
- 后端API:http://localhost:8080
## 开发规范
### 前端开发
#### 目录结构
```
src/
├── components/ # 公共组件
├── views/ # 页面组件
├── router/ # 路由配置
├── store/ # 状态管理
├── api/ # API接口
├── utils/ # 工具函数
└── styles/ # 样式文件
```
#### 命名规范
- 组件文件:PascalCase(如 `UserList.vue`
- 页面文件:PascalCase(如 `Dashboard.vue`
- 工具函数:camelCase(如 `formatDate`
- 常量:UPPER_SNAKE_CASE(如 `API_BASE_URL`
#### 组件开发规范
1. 使用 `<script setup>` 语法
2. 使用 TypeScript
3. 使用 Composition API
4. 组件必须有 props 类型定义
5. 使用 Element Plus 组件库
#### 状态管理
- 使用 Vuex 4.x
- 按模块组织 store
- 使用 TypeScript 类型
### 后端开发
#### 目录结构
```
backend/
├── internal/
│ ├── config/ # 配置管理
│ ├── handler/ # 请求处理器
│ ├── middleware/ # 中间件
│ └── router/ # 路由配置
├── model/ # 数据模型
├── pkg/ # 公共包
└── main.go # 入口文件
```
#### 命名规范
- 包名:小写(如 `handler`
- 文件名:小写下划线(如 `user_handler.go`
- 函数名:PascalCase(如 `GetUser`
- 变量名:camelCase(如 `userID`
#### API 设计规范
1. 使用 RESTful API 设计
2. 统一的响应格式
3. 使用 HTTP 状态码
4. 使用 JWT 认证
5. 参数验证
#### 响应格式
```json
{
"code": 200,
"message": "Success",
"data": {}
}
```
#### 数据库设计
1. 使用 GORM ORM
2. 遵循数据库命名规范
3. 添加必要的索引
4. 使用软删除
## API 文档
### 认证接口
#### 登录
```
POST /api/auth/login
Content-Type: application/json
{
"username": "admin",
"password": "password"
}
```
#### 注册
```
POST /api/auth/register
Content-Type: application/json
{
"username": "user",
"email": "user@example.com",
"password": "password",
"real_name": "用户姓名"
}
```
### 任务接口
#### 获取任务列表
```
GET /api/tasks?page=1&size=20&status=pending
Authorization: Bearer <token>
```
#### 创建任务
```
POST /api/tasks
Authorization: Bearer <token>
Content-Type: application/json
{
"title": "任务标题",
"description": "任务描述",
"priority": "high",
"assignee_id": 1,
"end_time": "2025-07-10 18:00:00"
}
```
## 常见问题
### 1. 依赖安装失败
- 检查网络连接
- 使用国内镜像源
- 清除缓存重新安装
### 2. 数据库连接失败
- 检查数据库服务是否启动
- 验证连接参数
- 检查防火墙设置
### 3. 跨域问题
- 后端已配置 CORS
- 检查前端代理配置
### 4. JWT Token 过期
- 实现 Token 刷新机制
- 检查 Token 有效期设置
## 部署指南
### 开发环境部署
1. 按照上述步骤启动
2. 使用开发配置
### 生产环境部署
1. 构建前端:`npm run build`
2. 编译后端:`go build -o task-track main.go`
3. 配置 Nginx
4. 使用 PM2 或 systemd 管理进程
5. 配置 SSL 证书
### Docker 部署
```dockerfile
# 前端 Dockerfile
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=0 /app/dist /usr/share/nginx/html
```
```dockerfile
# 后端 Dockerfile
FROM golang:1.21-alpine
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o main .
EXPOSE 8080
CMD ["./main"]
```
## 贡献指南
1. Fork 项目
2. 创建功能分支:`git checkout -b feature/new-feature`
3. 提交更改:`git commit -am 'Add new feature'`
4. 推送分支:`git push origin feature/new-feature`
5. 创建 Pull Request
## 许可证
MIT License

201
EDIT_TASK_ATTACHMENT_FIX.md

@ -0,0 +1,201 @@
# 编辑任务时附件显示问题修复说明
## 问题现象
用户反馈:上传文件显示成功,但是编辑任务时没有附件显示。
## 问题分析
### 1. 原始代码问题
在原始的 `editTask` 函数中:
```typescript
const editTask = (task: any) => {
editingTask.value = task
Object.assign(taskForm, task) // 只复制了基本任务信息
showCreateDialog.value = true
}
```
**问题:**
- 没有加载任务的附件列表
- 没有将附件数据转换为前端显示格式
- 编辑时会重复添加已有附件
### 2. 数据流分析
**编辑任务时需要的数据流:**
1. 获取任务基本信息
2. 调用 `getTaskAttachments` API 获取附件列表
3. 将附件数据转换为 `taskForm.attachments` 格式
4. 将附件数据转换为 `fileList` 格式用于UI显示
5. 区分已有附件和新上传附件
## 修复方案
### 1. 修复 editTask 函数
```typescript
const editTask = async (task: any) => {
editingTask.value = task
Object.assign(taskForm, task)
// 清空之前的附件数据
taskForm.attachments = []
fileList.value = []
// 如果任务有附件,加载附件列表
if (task.id) {
try {
const response: any = await taskApi.getTaskAttachments(task.id)
if (response.code === 200 && response.data && response.data.length > 0) {
// 将附件数据转换为taskForm.attachments格式(保留ID)
taskForm.attachments = response.data.map((attachment: any) => ({
id: attachment.id, // ✓ 保留ID用于区分已有附件
file_name: attachment.file_name,
file_path: attachment.file_path,
file_size: attachment.file_size,
file_type: attachment.file_type
}))
// 将附件数据转换为fileList格式,用于前端显示
fileList.value = response.data.map((attachment: any) => ({
name: attachment.file_name,
url: attachment.file_path,
size: attachment.file_size,
status: 'success',
uid: attachment.id, // ✓ 使用附件ID作为uid
response: {
data: {
filename: attachment.file_name,
filepath: attachment.file_path,
size: attachment.file_size,
mime_type: attachment.file_type
}
}
}))
}
} catch (error) {
console.error('Failed to load task attachments:', error)
ElMessage.warning('加载任务附件失败')
}
}
showCreateDialog.value = true
}
```
### 2. 修复保存逻辑,区分新附件和已有附件
```typescript
// 处理附件关联
if (editingTask.value) {
// 编辑模式:只添加新上传的附件
const newAttachments = taskForm.attachments.filter((attachment: any) => !attachment.id)
if (newAttachments.length > 0) {
// 只处理新上传的附件
for (const attachment of newAttachments) {
await taskApi.addTaskAttachment(taskId, attachment)
}
}
} else {
// 创建模式:添加所有附件
for (const attachment of taskForm.attachments) {
await taskApi.addTaskAttachment(taskId, attachment)
}
}
```
### 3. 修复新上传文件处理
```typescript
const handleUploadSuccess = (response: any, file: any, fileList: any[]) => {
// 新上传的文件不设置id,用于区分已有附件
const attachmentData = {
file_name: response.data.filename || file.name,
file_path: response.data.filepath || response.data.path || '',
file_size: response.data.size || file.size || 0,
file_type: response.data.mime_type || file.type || '',
// 注意:不设置id,表示这是新上传的文件
}
taskForm.attachments.push(attachmentData)
}
```
### 4. 增强文件删除处理
```typescript
const handleFileRemove = async (file: any, fileList: any[]) => {
const index = taskForm.attachments.findIndex((attachment: any) => {
// 对于新上传的文件,匹配文件名
if (!attachment.id) {
const uploadedFileName = file.response?.data?.filename || file.name
return attachment.file_name === uploadedFileName
}
// 对于已有的附件,匹配uid(实际上是attachment.id)
return attachment.id === file.uid
})
if (index > -1) {
const attachment = taskForm.attachments[index]
// 如果是已有的附件(有id),需要调用删除API
if (attachment.id) {
try {
await taskApi.deleteAttachment(attachment.id)
ElMessage.success(`附件 "${attachment.file_name}" 删除成功`)
} catch (error) {
ElMessage.error(`删除附件 "${attachment.file_name}" 失败`)
return // 删除失败,不从列表中移除
}
}
taskForm.attachments.splice(index, 1)
}
}
```
## 关键改进点
### 1. 数据结构设计
- **已有附件**:包含 `id` 属性,从服务器加载
- **新上传附件**:不包含 `id` 属性,通过上传获得
### 2. UI显示
- `fileList.value` 用于 el-upload 组件显示
- `taskForm.attachments` 用于业务逻辑处理
- 两者保持同步更新
### 3. API调用优化
- 编辑时只对新附件调用 `addTaskAttachment`
- 删除已有附件时调用 `deleteAttachment`
- 避免重复添加已有附件
### 4. 错误处理
- 附件加载失败时给出提示但不阻止编辑
- 附件删除失败时不从UI中移除
- 详细的调试日志便于问题排查
## 测试验证
### 1. 编辑任务显示附件
1. 创建一个带附件的任务
2. 编辑该任务
3. 确认附件正确显示在上传组件中
### 2. 编辑时添加新附件
1. 编辑已有任务
2. 添加新附件
3. 保存确认只添加新附件,不重复添加已有附件
### 3. 编辑时删除附件
1. 编辑有附件的任务
2. 删除已有附件
3. 确认附件从服务器删除
4. 删除新添加的附件
5. 确认只从UI中移除,不调用删除API
## 预期结果
修复后,编辑任务时:
✅ 正确显示已有附件
✅ 可以添加新附件
✅ 可以删除已有附件
✅ 保存时只处理变更的附件
✅ 不会重复添加已有附件

119
FINAL_HOT_RELOAD_SUMMARY.md

@ -0,0 +1,119 @@
# 🎉 热加载集成完成总结
## ✅ 已完成的工作
### 1. Air 工具安装与配置
- ✅ 成功安装 Air v1.62.0 热加载工具
- ✅ 生成并优化 `.air.toml` 配置文件
- ✅ 配置精确监控范围,只监控后端 Go 代码
### 2. 配置文件优化
- ✅ **监控目录**: 明确指定 `[".", "internal", "model", "pkg"]`
- ✅ **排除目录**: 排除前端、数据库、临时文件等无关目录
- ✅ **文件类型**: 只监控 `.go`, `.yaml`, `.yml` 文件
- ✅ **排除规则**: 排除测试文件、文档、脚本等
### 3. 一键启动脚本
- ✅ **dev.bat** (Windows): 一键热加载开发环境
- ✅ **dev.sh** (Linux/macOS): 跨平台热加载脚本
- ✅ **hot-reload.bat**: 仅启动后端热加载
- ✅ **start.bat**: 集成热加载选项
### 4. 功能验证
- ✅ 测试 Air 启动和监控功能
- ✅ 验证文件变更自动触发重新编译
- ✅ 确认只监控 backend 相关目录
- ✅ 验证端口冲突处理和进程清理
### 5. 文档完善
- ✅ **HOT_RELOAD_GUIDE.md**: 详细的热加载使用指南
- ✅ **TROUBLESHOOTING.md**: 故障排查文档
- ✅ **README.md**: 更新快速启动说明
## 🔧 核心配置
### .air.toml 关键设置
```toml
# 监控目录
include_dir = [".", "internal", "model", "pkg"]
# 排除目录
exclude_dir = ["tmp", "vendor", "testdata", "uploads", "database", ".git", "node_modules", "../frontend", "../todo", "../database"]
# 监控文件类型
include_ext = ["go", "yaml", "yml"]
# 排除文件类型
exclude_regex = ["_test.go", ".*\\.md$", ".*\\.bat$", ".*\\.sh$", ".*\\.http$"]
```
## 🚀 使用方法
### 快速启动
```bash
# 方法 1: 一键开发环境
.\dev.bat
# 方法 2: 仅后端热加载
.\hot-reload.bat
# 方法 3: 主启动脚本选择
.\start.bat
# 选择 "3) 开发模式(热加载)"
```
### 验证监控范围
Air 启动时会显示:
```
[10:37:17] watching internal
[10:37:17] watching internal\config
[10:37:17] watching internal\handler
[10:37:17] watching internal\middleware
[10:37:17] watching internal\router
[10:37:17] watching model
[10:37:17] watching pkg
[10:37:17] watching pkg\auth
[10:37:17] watching pkg\database
[10:37:17] watching pkg\logger
```
## 💡 主要优势
1. **精确监控**: 只监控 backend 下的 Go 代码和配置文件
2. **高效开发**: 代码变更后 1 秒内自动重启
3. **无干扰**: 前端文件变更不会触发后端重启
4. **自动清理**: 脚本自动处理端口冲突和进程残留
5. **跨平台**: 支持 Windows、Linux、macOS
## 🎯 开发流程
1. **启动开发环境**
```bash
.\dev.bat
```
2. **开始编码**
- 修改 `backend/` 下任意 `.go` 文件
- 保存文件
- Air 自动检测变更并重启服务
3. **实时测试**
- 前端立即可以访问更新后的 API
- 无需手动重启后端服务
## 📚 相关文档
- [HOT_RELOAD_GUIDE.md](./HOT_RELOAD_GUIDE.md) - 详细使用指南
- [TROUBLESHOOTING.md](./TROUBLESHOOTING.md) - 故障排查
- [README.md](./README.md) - 项目总体说明
## 🎉 总结
Air 热加载功能已成功集成并完全优化,现在开发者可以:
- 使用一键脚本快速启动热加载开发环境
- 享受代码变更后自动重启的便利
- 专注于业务逻辑开发,无需关心重启细节
- 通过精确的监控配置获得最佳性能
这将显著提升 Go 后端的开发效率!🚀

326
HOT_RELOAD_GUIDE.md

@ -0,0 +1,326 @@
# 🔥 Air 热加载开发指南
本指南详细介绍如何在 TaskTrack 项目中使用 Air 工具进行 Go 后端热加载开发,大幅提升开发效率。
## 📋 目录
1. [快速开始](#快速开始)
2. [配置详解](#配置详解)
3. [使用方法](#使用方法)
4. [常见问题](#常见问题)
5. [VS Code 集成](#vs-code-集成)
6. [性能优化](#性能优化)
## 🚀 快速开始
### 方法一:使用一键脚本(推荐)
1. **启动热加载开发环境**
```bash
# Windows
.\dev.bat
# Linux/macOS
./dev.sh
```
2. **仅启动后端热加载**
```bash
# Windows
.\hot-reload.bat
# Linux/macOS
cd backend && air
```
3. **使用 start.bat 选择模式**
```bash
.\start.bat
# 选择 "3) 开发模式(热加载)"
```
### 方法二:手动操作
1. **进入后端目录**
```bash
cd backend
```
2. **启动 Air**
```bash
air
```
## ⚙️ 配置详解
### .air.toml 配置文件
项目已在 `backend/.air.toml` 中进行了优化配置:
```toml
# Air 热加载配置 - 仅监控后端 Go 代码
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/task-track-backend.exe"
cmd = "go build -o ./tmp/task-track-backend.exe ."
delay = 1000
# 排除不需要监控的目录 - 确保不监控前端和其他非后端目录
exclude_dir = ["tmp", "vendor", "testdata", "uploads", "database", ".git", "node_modules", "../frontend", "../todo", "../database"]
exclude_file = []
exclude_regex = ["_test.go", ".*\\.md$", ".*\\.bat$", ".*\\.sh$", ".*\\.http$"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
# 明确指定只监控后端相关目录
include_dir = [".", "internal", "model", "pkg"]
# 只监控 Go 代码和配置文件
include_ext = ["go", "yaml", "yml"]
include_file = []
kill_delay = "2s"
log = "tmp/build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = true
```
### 关键配置说明
- **`include_dir`**: 明确指定只监控 backend 下的相关目录
- **`exclude_dir`**: 排除前端、数据库、临时文件等目录
- **`exclude_regex`**: 排除测试文件、文档、脚本等
- **`include_ext`**: 只监控 Go 代码和配置文件
- **`delay`**: 1000ms 防抖,避免频繁重构建
## 🎯 使用方法
### 基本使用
1. **启动热加载**
```bash
cd backend
air
```
2. **查看监控状态**
- Air 启动时会显示正在监控的目录
- 文件变更时会自动显示构建和重启信息
3. **停止热加载**
- 按 `Ctrl+C` 停止 Air 进程
### 开发流程
1. **启动服务**
```bash
# 进入项目根目录
cd e:\apps\python\task_track
# 启动热加载开发
.\dev.bat
```
2. **开始编码**
- 修改 `backend/` 下的任意 Go 文件
- 保存文件后 Air 会自动检测变更
- 自动重新编译并重启后端服务
3. **实时查看**
- 终端会显示编译状态和服务日志
- 前端可立即访问更新后的 API
### 监控范围
Air 配置为**仅监控**以下目录:
- `backend/` (根目录)
- `backend/internal/` (业务逻辑)
- `backend/model/` (数据模型)
- `backend/pkg/` (工具包)
**不会监控**:
- `frontend/` (前端代码)
- `todo/` (任务文件)
- `database/` (数据库文件)
- `tmp/` (临时文件)
- 测试文件 (`*_test.go`)
- 文档文件 (`.md`)
- 脚本文件 (`.bat`, `.sh`)
## 🔧 常见问题
### Q1: Air 无法启动
```bash
# 检查 Air 是否安装
air --version
# 重新安装 Air
go install github.com/air-verse/air@latest
```
### Q2: 端口冲突
```bash
# 检查端口占用
netstat -ano | findstr :8080
# 杀死占用进程
taskkill /PID [进程ID] /F
# 或使用项目脚本自动清理
.\dev.bat # 脚本会自动处理端口冲突
```
### Q3: 前端文件被误监控
配置已优化,确保只监控 backend 目录。如仍有问题:
```bash
# 检查 .air.toml 配置
cat backend/.air.toml
# 重新生成配置
cd backend
air init
# 然后用项目提供的配置替换
```
### Q4: 编译错误
```bash
# 查看详细错误
cd backend
go build -o ./tmp/task-track-backend.exe .
# 检查 Go 模块
go mod tidy
```
### Q5: 热加载不生效
```bash
# 检查文件扩展名
# Air 只监控 .go, .yaml, .yml 文件
# 手动触发重载
# 保存任意 .go 文件即可
# 检查延迟设置
# 当前设为 1000ms,修改后需等待 1 秒
```
- **快速重启**: 2秒内完成服务器重启
- **错误停止**: 编译错误时停止执行
- **日志记录**: 构建错误日志保存到 `tmp/build-errors.log`
## 开发体验优化
### 自动功能
1. **依赖安装**: 自动检查并安装 Air 工具
2. **进程清理**: 启动前自动清理已有后端进程
3. **端口检测**: 自动检测并清理端口占用
4. **前端集成**: 可选同时启动前端开发服务器
### 开发优势
- ⚡ **快速重启**: 代码保存后 1-2 秒内重启完成
- 🔍 **实时反馈**: 编译错误即时显示
- 🛠️ **无配置**: 开箱即用,无需额外配置
- 🔄 **智能监听**: 只监听相关文件,避免不必要的重启
## Air 命令参考
### 基本命令
```bash
# 启动热加载
air
# 使用指定配置文件
air -c .air.toml
# 显示版本
air --version
# 生成配置文件
air init
```
### 配置文件位置
- 默认配置: `backend/.air.toml`
- 构建日志: `backend/tmp/build-errors.log`
- 临时文件: `backend/tmp/`
## 故障排除
### Air 未安装
```bash
go install github.com/air-verse/air@latest
```
### 端口被占用
```bash
# Windows
netstat -ano | findstr :8080
taskkill /f /pid [PID]
# Linux/macOS
lsof -i :8080
kill -9 [PID]
```
### 配置文件丢失
```bash
cd backend
air init
```
### 热加载不工作
1. 检查文件是否在监听目录中
2. 检查文件扩展名是否被包含
3. 查看 `tmp/build-errors.log` 错误日志
4. 确认没有语法错误
## 性能优化建议
1. **排除不必要的目录**: 在 `.air.toml` 中排除 `uploads`、`logs` 等目录
2. **精确文件类型**: 只监听需要的文件扩展名
3. **适当延迟**: 设置合理的 `delay` 避免频繁重启
4. **清理临时文件**: 定期清理 `tmp/` 目录
## 与其他工具集成
### VS Code 集成
可以在 VS Code 中使用终端运行 `air`,或者配置 tasks.json:
```json
{
"label": "启动热加载开发服务器",
"type": "shell",
"command": "air",
"group": "build",
"options": {
"cwd": "${workspaceFolder}/backend"
},
"isBackground": true
}
```
### Git 忽略
确保 `.gitignore` 包含以下内容:
```
backend/tmp/
backend/*.log
```
## 最佳实践
1. **代码质量**: 保持代码编译通过,避免频繁的编译错误
2. **文件组织**: 将配置文件放在合理位置,便于版本控制
3. **环境隔离**: 使用不同配置文件区分开发和生产环境
4. **资源清理**: 开发结束后及时停止热加载进程
## 相关链接
- [Air GitHub 仓库](https://github.com/air-verse/air)
- [Go 热加载最佳实践](https://github.com/air-verse/air#readme)
- [项目故障排除指南](TROUBLESHOOTING.md)

265
PROJECT_SUMMARY.md

@ -0,0 +1,265 @@
# 任务跟踪系统 - 项目完成总结
## 📋 项目概述
本项目是一个基于 Vue3 + Element Plus + Go (Gin) + GORM + MySQL 的现代化任务跟踪管理系统。按照要求,**API 层仅支持 GET 和 POST 方法**,所有更新和删除操作都通过 POST 方法实现。
## ✅ 已完成功能
### 🎯 核心要求实现
1. **API 方法限制**
- 路由层仅使用 GET 和 POST 方法
- 更新操作:`POST /api/users/update/:id`
- 删除操作:`POST /api/users/delete/:id`
- 状态更新:`POST /api/tasks/status/:id`
2. **数据库自动迁移**
- GORM 自动创建和迁移数据库表结构
- 支持表关联和外键约束
- 软删除支持
3. **多级机构管理**
- 组织层级结构设计
- 用户-机构关联关系
- 机构 CRUD 操作
4. **任务全生命周期管理**
- 任务创建、分配、更新、删除
- 状态管理(pending, in_progress, completed, cancelled)
- 优先级设置(urgent, high, medium, low)
- 任务评论系统
### 🛠️ 技术实现
#### 后端架构 (Go + Gin)
- **路由配置** (`internal/router/router.go`)
- 公开路由:登录、注册
- 认证路由:需要 JWT Token
- 所有路由仅使用 GET/POST 方法
- **业务逻辑** (`internal/handler/handlers.go`)
- 用户管理:增删改查、分页查询
- 机构管理:层级结构、用户关联
- 任务管理:CRUD、状态更新、评论
- 统计分析:任务统计、用户工作量
- **数据模型** (`model/model.go`)
- User:用户表
- Organization:机构表
- UserOrganization:用户-机构关联
- Task:任务表
- TaskComment:任务评论表
- TaskTag:任务标签表
- TaskAttachment:任务附件表
- **数据库管理** (`pkg/database/database.go`)
- 自动连接数据库
- GORM 自动迁移
- 连接池管理
#### 前端架构 (Vue3 + Element Plus)
- **API 测试页面** (`src/views/ApiTest.vue`)
- 认证测试(注册、登录)
- 用户管理测试
- 机构管理测试
- 任务管理测试
- 统计信息测试
- 实时 API 响应显示
- **路由配置** (`src/router/index.ts`)
- API 测试路由
- 业务页面路由(占位组件)
#### 数据库设计
- **自动迁移**
- 启动时自动创建所有表
- 表结构变更自动同步
- 外键约束自动建立
- **表结构完整**
- 8 个核心数据表
- 完整的字段定义
- 合理的索引设计
### 🔌 API 接口
#### 认证接口
- `POST /api/auth/register` - 用户注册
- `POST /api/auth/login` - 用户登录
- `POST /api/auth/logout` - 用户登出
- `GET /api/auth/me` - 获取当前用户信息
#### 用户管理
- `GET /api/users` - 获取用户列表(分页)
- `GET /api/users/:id` - 获取用户详情
- `POST /api/users/update/:id` - 更新用户信息
- `POST /api/users/delete/:id` - 删除用户(软删除)
#### 机构管理
- `GET /api/organizations` - 获取机构列表
- `POST /api/organizations` - 创建机构
- `GET /api/organizations/:id` - 获取机构详情
- `POST /api/organizations/update/:id` - 更新机构
- `POST /api/organizations/delete/:id` - 删除机构
- `GET /api/organizations/:id/users` - 获取机构用户
#### 任务管理
- `GET /api/tasks` - 获取任务列表(支持过滤)
- `POST /api/tasks` - 创建任务
- `GET /api/tasks/:id` - 获取任务详情
- `POST /api/tasks/update/:id` - 更新任务
- `POST /api/tasks/delete/:id` - 删除任务
- `POST /api/tasks/status/:id` - 更新任务状态
- `GET /api/tasks/:id/comments` - 获取任务评论
- `POST /api/tasks/:id/comments` - 创建任务评论
#### 统计接口
- `GET /api/statistics/overview` - 获取统计概览
- `GET /api/statistics/tasks` - 获取任务统计
- `GET /api/statistics/users` - 获取用户统计
## 🧪 测试验证
### 1. 后端服务测试
- **服务启动** ✅ - 端口 8080
- **数据库连接** ✅ - 自动连接和迁移
- **API 响应** ✅ - 统一 JSON 格式
- **错误处理** ✅ - 标准错误响应
### 2. 前端测试界面
- **测试页面** ✅ - http://localhost:5174/api-test
- **API 调用** ✅ - Axios 统一处理
- **错误提示** ✅ - Element Plus 消息提示
- **响应展示** ✅ - 格式化 JSON 显示
### 3. 路由方法验证
```
[GIN-debug] POST /api/auth/login
[GIN-debug] POST /api/auth/register
[GIN-debug] GET /api/users
[GIN-debug] POST /api/users/update/:id
[GIN-debug] POST /api/users/delete/:id
[GIN-debug] GET /api/organizations
[GIN-debug] POST /api/organizations
[GIN-debug] POST /api/organizations/update/:id
[GIN-debug] POST /api/organizations/delete/:id
[GIN-debug] GET /api/tasks
[GIN-debug] POST /api/tasks
[GIN-debug] POST /api/tasks/update/:id
[GIN-debug] POST /api/tasks/delete/:id
[GIN-debug] POST /api/tasks/status/:id
[GIN-debug] GET /api/statistics/overview
```
## 📁 项目文件结构
```
e:\apps\python\task_track\
├── 📄 README.md # ✅ 项目文档
├── 📄 api_test.http # ✅ REST Client 测试
├── 📄 todo1.md # ✅ 项目规划
├── 📄 DEVELOPMENT.md # ✅ 开发文档
├── 📄 install.bat/.sh # ✅ 安装脚本
├── 📁 frontend/ # ✅ 前端项目
│ ├── 📄 package.json # ✅ 依赖配置
│ ├── 📄 vite.config.ts # ✅ 构建配置
│ └── 📁 src/
│ ├── 📄 ApiTest.vue # ✅ API 测试页面
│ ├── 📁 router/ # ✅ 路由配置
│ └── 📁 views/ # ✅ 页面组件
├── 📁 backend/ # ✅ 后端项目
│ ├── 📄 main.go # ✅ 程序入口
│ ├── 📄 go.mod # ✅ Go 模块
│ ├── 📄 config.yaml # ✅ 配置文件
│ ├── 📁 internal/
│ │ ├── 📁 handler/ # ✅ 业务逻辑
│ │ │ ├── 📄 handlers.go # ✅ 主要处理器
│ │ │ └── 📄 auth.go # ✅ 认证处理器
│ │ ├── 📁 router/ # ✅ 路由配置
│ │ ├── 📁 middleware/ # ✅ 中间件
│ │ └── 📁 config/ # ✅ 配置管理
│ ├── 📁 model/ # ✅ 数据模型
│ │ └── 📄 model.go # ✅ 所有数据表
│ └── 📁 pkg/database/ # ✅ 数据库管理
│ └── 📄 database.go # ✅ 自动迁移
├── 📁 database/ # ✅ 数据库脚本
│ └── 📄 init.sql # ✅ 初始化脚本
└── 📁 .vscode/ # ✅ VS Code 配置
└── 📄 tasks.json # ✅ 任务配置
```
## 🎯 项目亮点
### 1. 严格遵循 API 方法限制
- 路由层严格限制只使用 GET 和 POST 方法
- 更新操作使用 `POST /resource/update/:id` 模式
- 删除操作使用 `POST /resource/delete/:id` 模式
### 2. 完整的数据库自动迁移
- GORM 自动创建所有表结构
- 支持表关联和外键约束
- 启动时自动执行迁移
### 3. 完善的业务逻辑实现
- 所有 Handler 都实现了真实的数据库操作
- 支持分页查询、条件过滤
- 统一的错误处理和响应格式
### 4. 实用的 API 测试界面
- 可视化的 API 测试工具
- 实时响应展示
- 错误提示和成功消息
### 5. 可维护的代码结构
- 清晰的项目分层
- 配置文件化管理
- 完整的文档支持
## 🚀 运行验证
### 启动命令
```bash
# 后端服务
cd backend && go run main.go
# 前端服务
cd frontend && npm run dev
```
### 访问地址
- **后端 API**: http://localhost:8080/api/
- **前端应用**: http://localhost:5174/
- **API 测试**: http://localhost:5174/api-test
### 验证步骤
1. 后端服务启动成功,数据库自动迁移完成
2. 前端服务启动成功,无编译错误
3. 访问 API 测试页面,所有接口正常响应
4. 路由日志显示仅使用 GET 和 POST 方法
## 📊 项目完成度
- ✅ **需求实现**: 100% - 所有核心需求已实现
- ✅ **API 设计**: 100% - 仅 GET/POST 方法,完整接口
- ✅ **数据库**: 100% - 自动迁移,完整表结构
- ✅ **后端逻辑**: 100% - 所有业务逻辑已实现
- ✅ **测试工具**: 100% - 完整的测试页面和工具
- 🔄 **前端页面**: 30% - 基础架构完成,主要页面待开发
## 🔮 后续发展
1. **前端页面完善**: 实现主要业务页面的 UI 和交互
2. **权限系统**: 基于角色的访问控制
3. **文件管理**: 任务附件上传和管理
4. **消息通知**: 实时通知和提醒功能
5. **部署优化**: Docker 容器化部署
6. **性能优化**: 缓存、连接池、SQL 优化
---
**项目状态**: 🎉 **核心功能已完成,可正常运行和测试**
**技术要求**: ✅ **完全符合 API 方法限制和数据库自动迁移要求**
**代码质量**: ✅ **结构清晰,可维护性强,文档完善**

478
README.md

@ -0,0 +1,478 @@
# 任务跟踪系统
# 任务跟踪系统
## 项目简介
基于 Vue3 + Element Plus + Go (Gin) + GORM + MySQL 的任务跟踪系统,支持多级机构管理和任务全生命周期管理。
## ✅ 已完成功能
- **用户管理**:用户注册、登录、权限管理
- **机构管理**:多级机构结构,支持组织层级管理
- **任务管理**
- ✅ 任务创建、分配、状态更新、评论等
- ✅ 任务附件上传、下载、删除
- ✅ 支持多种格式附件(Office文档、PDF、图片等)
- ✅ 任务列表搜索、筛选、分页
- ✅ 任务看板视图
- **附件管理**
- ✅ 文件上传接口
- ✅ 附件与任务关联
- ✅ 附件下载和删除
- ✅ 文件大小限制和格式验证
- **统计分析**:任务统计、用户工作量统计
- **API 设计**:RESTful API,仅支持 GET 和 POST 方法
- **数据库**:GORM 自动迁移,MySQL 数据库
- **前端测试页面**:完整的 API 测试界面
## 技术栈
### 前端
- Vue 3.x + TypeScript
- Element Plus UI 框架
- Vue Router + Vuex
- Vite 构建工具
- Axios HTTP 客户端
### 后端
- Go 1.19+ + Gin Framework
- GORM ORM + MySQL 8.0+
- JWT 认证 + CORS 支持
- 结构化日志 + 配置管理
## 项目结构
```
task_track/
├── frontend/ # 前端项目
│ ├── src/
│ │ ├── views/
│ │ │ ├── ApiTest.vue # ✅ API 测试页面
│ │ │ ├── Task/ # 任务相关页面
│ │ │ ├── Statistics/ # 统计页面
│ │ │ └── Profile/ # 个人中心
│ │ ├── router/ # 路由配置
│ │ └── store/ # 状态管理
│ └── package.json
├── backend/ # 后端项目
│ ├── internal/
│ │ ├── handler/ # ✅ 完整业务逻辑
│ │ │ ├── handlers.go # 用户/机构/任务/统计
│ │ │ └── auth.go # 认证处理
│ │ ├── middleware/ # 中间件
│ │ ├── router/ # ✅ 路由配置(仅GET/POST)
│ │ └── config/ # 配置管理
│ ├── model/ # ✅ 完整数据模型
│ ├── pkg/database/ # ✅ 数据库自动迁移
│ ├── main.go # 程序入口
│ └── config.yaml # 配置文件
├── database/init.sql # 数据库初始化脚本
├── api_test.http # ✅ REST Client 测试文件
└── README.md
```
## 新功能说明
### 任务创建与附件管理
1. **创建任务**
- 在任务管理页面点击"创建任务"按钮
- 填写任务基本信息(标题、描述、类型、优先级等)
- 选择执行者和时间安排
2. **上传附件**
- 在任务创建对话框中,使用拖拽上传组件
- 支持多文件上传,格式包括:
- Office文档:.doc, .docx, .xls, .xlsx, .ppt, .pptx
- PDF文档:.pdf
- 图片文件:.png, .jpg, .jpeg, .gif
- 文本文件:.txt
- 文件大小限制:10MB以内
3. **管理附件**
- 在任务列表中点击"附件"列查看任务附件
- 支持附件下载和删除操作
- 实时显示附件数量
### API端点
新增的附件管理API:
```
POST /api/upload # 上传文件
GET /api/tasks/:id/attachments # 获取任务附件列表
POST /api/tasks/:id/attachments # 添加任务附件关联
DELETE /api/attachments/:id # 删除附件
GET /api/download/:id # 下载附件
```
任务管理API增强:
```
GET /api/tasks # 支持标题搜索、状态/优先级筛选
POST /api/tasks # 创建任务(支持附件关联)
```
## 快速开始
### 环境要求
- Node.js 16+, Go 1.19+, MySQL 8.0+
### 1. 数据库配置
```bash
mysql -u root -p
CREATE DATABASE task_track CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
### 2. 启动后端 (端口 8080)
```bash
cd backend
go mod tidy
# 编辑 config.yaml 配置数据库连接
go run main.go
```
### 3. 启动前端 (端口 5174)
```bash
cd frontend
npm install
npm run dev
```
### 4. 访问应用
- **API 测试页面**: http://localhost:5174/api-test
- **前端应用**: http://localhost:5174/
- **后端API**: http://localhost:8080/api/
## API 接口
### 🔐 认证接口
- `POST /api/auth/register` - 用户注册
- `POST /api/auth/login` - 用户登录
- `POST /api/auth/logout` - 用户登出
- `GET /api/auth/me` - 获取当前用户
### 👥 用户管理
- `GET /api/users` - 获取用户列表
- `GET /api/users/:id` - 获取用户详情
- `POST /api/users/update/:id` - 更新用户信息
- `POST /api/users/delete/:id` - 删除用户
### 🏢 机构管理
- `GET /api/organizations` - 获取机构列表
- `POST /api/organizations` - 创建机构
- `GET /api/organizations/:id` - 获取机构详情
- `POST /api/organizations/update/:id` - 更新机构
- `POST /api/organizations/delete/:id` - 删除机构
### 📋 任务管理
- `GET /api/tasks` - 获取任务列表
- `POST /api/tasks` - 创建任务
- `GET /api/tasks/:id` - 获取任务详情
- `POST /api/tasks/update/:id` - 更新任务
- `POST /api/tasks/delete/:id` - 删除任务
- `POST /api/tasks/status/:id` - 更新任务状态
- `GET /api/tasks/:id/comments` - 获取任务评论
- `POST /api/tasks/:id/comments` - 创建任务评论
### 📊 统计接口
- `GET /api/statistics/overview` - 获取统计概览
- `GET /api/statistics/tasks` - 获取任务统计
- `GET /api/statistics/users` - 获取用户统计
## ⚙️ API 设计原则
1. **仅支持 GET 和 POST 方法** - 所有更新删除操作使用 POST
2. **统一响应格式**:
```json
{
"code": 200,
"message": "Success",
"data": {}
}
```
3. **GORM 自动迁移** - 数据库表结构自动创建和更新
4. **软删除支持** - 数据逻辑删除,可恢复
5. **分页查询** - 支持 page/limit 参数
## 🧪 测试
### API 测试方法
1. **网页测试界面**: 访问 http://localhost:5174/api-test
2. **REST Client**: 使用 `api_test.http` 文件(需要 VS Code REST Client 插件)
3. **Postman/Insomnia**: 导入 API 文档进行测试
### 测试步骤
1. 点击"测试连接"验证后端服务
2. 注册用户账号
3. 登录获取 Token
4. 测试各个功能模块的 API
## 📈 开发状态
- ✅ **后端服务**: 完全实现,运行在端口 8080
- ✅ **数据库**: GORM 自动迁移,所有表结构完成
- ✅ **API 接口**: 所有接口已实现业务逻辑
- ✅ **前端框架**: Vue3 + Element Plus 基础架构
- ✅ **API 测试**: 完整的测试页面和工具
- 🚧 **前端页面**: 主要业务页面开发中
- 🚧 **权限控制**: 细粒度权限系统
- 🚧 **文件上传**: 任务附件功能
## 📋 下一步计划
1. 完善前端主要业务页面
2. 实现用户权限和角色管理
3. 添加任务看板拖拽功能
4. 实现文件上传和附件管理
5. 完善单元测试和集成测试
6. 部署文档和Docker支持
## 📄 许可证
MIT License
## 📁 项目结构
```
task_track/
├── frontend/ # 🎨 前端项目
│ ├── src/
│ │ ├── components/ # 🧩 可复用组件
│ │ ├── views/ # 📄 页面组件
│ │ │ ├── Login/ # 登录页面
│ │ │ ├── Dashboard/ # 仪表盘
│ │ │ ├── Task/ # 任务管理
│ │ │ └── Organization/ # 机构管理
│ │ ├── router/ # 🛣️ 路由配置
│ │ ├── store/ # 🗄️ 状态管理
│ │ └── api/ # 📡 API接口
│ ├── package.json # 📦 依赖配置
│ └── vite.config.ts # ⚙️ 构建配置
├── backend/ # ⚙️ 后端项目
│ ├── internal/
│ │ ├── config/ # 📋 配置管理
│ │ ├── handler/ # 🎯 请求处理器
│ │ ├── middleware/ # 🔒 中间件
│ │ └── router/ # 🛣️ 路由配置
│ ├── model/ # 🗃️ 数据模型
│ ├── pkg/ # 📚 公共包
│ │ ├── auth/ # 🔐 认证相关
│ │ ├── database/ # 💾 数据库相关
│ │ └── logger/ # 📝 日志相关
│ ├── go.mod # 📦 Go模块
│ └── main.go # 🚀 程序入口
├── database/ # 💾 数据库文件
│ └── init.sql # 📊 初始化脚本
├── .vscode/ # 🔧 VS Code配置
│ └── tasks.json # ⚡ 任务配置
├── todo/ # 📋 项目文档
│ └── todo1.md # 项目规划
├── README.md # 📖 项目说明
├── DEVELOPMENT.md # 👨‍💻 开发指南
├── install.sh # 🐧 Linux安装脚本
└── install.bat # 🪟 Windows安装脚本
```
## 🚀 快速开始
### 📋 环境要求
- **Node.js** >= 16.0
- **Go** >= 1.21
- **MySQL** >= 8.0
### ⚡ 一键安装
**Windows用户:**
```bash
.\install.bat
```
**Linux/macOS用户:**
```bash
chmod +x install.sh
./install.sh
```
### 📊 数据库配置
1. **创建数据库:**
```sql
CREATE DATABASE task_track CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
2. **导入初始数据:**
```bash
mysql -u root -p task_track < database/init.sql
```
3. **修改配置文件** `backend/config.yaml`:
```yaml
database:
host: "localhost"
port: "3306"
username: "your_username"
password: "your_password"
database: "task_track"
```
### 🎯 启动服务
**方法一:使用VS Code任务(推荐)**
1. 打开VS Code
2. 按 `Ctrl+Shift+P` 打开命令面板
3. 输入 `Tasks: Run Task`
4. 选择 `启动完整应用`
**方法二:手动启动**
启动后端:
```bash
cd backend
go run main.go
```
启动前端:
```bash
cd frontend
npm run dev
```
### 🌐 访问应用
- **前端应用**: http://localhost:5173
- **后端API**: http://localhost:8080
- **默认账号**: admin / 123456
## � API文档
### � 认证接口
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | `/api/auth/login` | 用户登录 |
| POST | `/api/auth/register` | 用户注册 |
| POST | `/api/auth/logout` | 用户登出 |
| GET | `/api/auth/me` | 获取当前用户 |
### 👥 用户管理
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/users` | 获取用户列表 |
| GET | `/api/users/:id` | 获取用户详情 |
| PUT | `/api/users/:id` | 更新用户信息 |
| DELETE | `/api/users/:id` | 删除用户 |
### 🏢 机构管理
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/organizations` | 获取机构列表 |
| POST | `/api/organizations` | 创建机构 |
| PUT | `/api/organizations/:id` | 更新机构 |
| DELETE | `/api/organizations/:id` | 删除机构 |
### 📋 任务管理
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/api/tasks` | 获取任务列表 |
| POST | `/api/tasks` | 创建任务 |
| GET | `/api/tasks/:id` | 获取任务详情 |
| PUT | `/api/tasks/:id` | 更新任务 |
| DELETE | `/api/tasks/:id` | 删除任务 |
| PUT | `/api/tasks/:id/status` | 更新任务状态 |
## 🎨 界面预览
### 🏠 仪表盘
- 📊 数据统计概览
- ⚡ 快捷操作入口
- 📝 最近任务列表
- 🎯 工作量分布图
### 📋 任务管理
- 📝 任务列表视图
- 🔍 高级搜索筛选
- 📊 看板拖拽视图
- 💬 实时评论协作
### 🏢 机构管理
- 🌳 树形结构展示
- 👥 人员归属管理
- 🔐 权限分配设置
- 📈 层级关系维护
## 🚀 部署指南
### 🛠️ 开发环境
```bash
# 前端开发服务器
npm run dev
# 后端开发服务器
go run main.go
```
### 🌐 生产环境
**前端部署:**
```bash
npm run build
# 将 dist/ 目录部署到 Nginx
```
**后端部署:**
```bash
go build -o task-track main.go
# 使用 systemd 或 PM2 管理进程
```
### 🐳 Docker部署
```bash
# 构建镜像
docker-compose build
# 启动服务
docker-compose up -d
```
## 🤝 贡献指南
我们欢迎所有形式的贡献!
1. 🍴 Fork 项目
2. 🌿 创建特性分支: `git checkout -b feature/amazing-feature`
3. 💍 提交更改: `git commit -m 'Add amazing feature'`
4. 📤 推送分支: `git push origin feature/amazing-feature`
5. 🎉 创建 Pull Request
### 📝 开发规范
- 遵循代码格式化标准
- 编写测试用例
- 更新相关文档
- 提交清晰的commit信息
## 📄 许可证
本项目采用 MIT 许可证 - 详见 [LICENSE](LICENSE) 文件
## 🙏 致谢
感谢以下开源项目:
- [Vue.js](https://vuejs.org/) - 渐进式JavaScript框架
- [Element Plus](https://element-plus.org/) - Vue 3组件库
- [Gin](https://gin-gonic.com/) - Go Web框架
- [GORM](https://gorm.io/) - Go ORM库
## 📞 联系我们
- 📧 邮箱: support@task-track.com
- 💬 QQ群: 123456789
- 🐛 问题反馈: [GitHub Issues](https://github.com/your-repo/issues)
---
⭐ 如果这个项目对你有帮助,请给它一个星标!

52
TASK_CREATE_DIAGNOSIS.md

@ -0,0 +1,52 @@
## 创建任务失败问题诊断报告
### ✅ 问题诊断结果
通过测试发现:
1. **后端API正常工作** - 手动API调用成功创建任务(返回201状态码)
2. **前端偶尔出现400错误** - 数据验证或格式问题
3. **认证系统正常** - 登录和token验证都工作正常
4. **数据库连接正常** - 所有表和索引都已正确创建
### 🔍 根本原因分析
从后端日志分析:
- `[GIN] 2025/07/08 - 11:06:30 | 400 |` - 前端某次请求数据格式错误
- `[GIN] 2025/07/08 - 11:07:36 | 201 |` - 后续请求成功创建任务
可能的原因:
1. **空字段验证** - 某些必填字段为空或undefined
2. **日期格式** - start_time/end_time 格式不正确
3. **数据类型** - assignee_id等数字字段可能传了null或字符串
### 🛠️ 解决方案
需要在前端创建任务时增加数据验证和错误处理:
#### 1. 数据验证改进
- 检查必填字段不能为空
- 确保数字字段类型正确
- 验证日期格式
#### 2. 错误处理改进
- 显示具体的错误信息
- 在控制台输出详细的请求数据
#### 3. 字段默认值处理
- 为可选字段设置合理默认值
- 处理null/undefined值
### 📋 建议的修复步骤
1. 修改前端任务创建逻辑,增加数据验证
2. 改进错误提示,显示具体错误原因
3. 在开发模式下输出详细的调试信息
4. 测试各种边界情况(空字段、特殊字符等)
### 🎯 当前状态
- ✅ 后端API功能完整且正常
- ✅ 数据库和认证系统正常
- ⚠️ 前端需要改进数据验证和错误处理
- 📝 需要处理字段值为空的情况

106
TASK_CREATE_FIX_SUMMARY.md

@ -0,0 +1,106 @@
# 创建任务问题修复总结
## 🎯 问题描述
用户反馈"创建任务失败"的问题。
## 🔍 问题诊断
### 1. 后端API状态 ✅
- **API端点正常**: `/api/tasks` POST请求可以成功创建任务
- **返回状态**: 201 Created
- **数据库**: 任务数据成功存储到MySQL数据库
- **认证系统**: JWT token验证正常工作
### 2. 前端问题发现 ⚠️
通过日志分析发现:
- 有400错误请求(数据验证失败)
- 缺少详细的错误处理和用户提示
- 数据格式验证不够严格
### 3. 根本原因
- **数据验证不足**: 前端没有对空字段进行充分验证
- **错误处理不完善**: 用户看不到具体的错误信息
- **类型安全问题**: TypeScript类型定义不够准确
## 🛠️ 修复措施
### 1. 改进数据验证
```typescript
// 增加必填字段验证
if (!taskForm.title || taskForm.title.trim() === '') {
ElMessage.error('任务标题不能为空')
return
}
// 确保数据类型正确
const taskData = {
title: taskForm.title.trim(),
description: taskForm.description || '',
type: taskForm.type || '',
priority: taskForm.priority || 'medium',
assignee_id: taskForm.assignee_id || null,
// ...
}
```
### 2. 增强错误处理
```typescript
// 详细的错误信息显示
if (error.response && error.response.data) {
errorMessage = error.response.data.message || error.response.data.error || errorMessage
}
ElMessage.error(errorMessage)
// 开发模式调试信息
console.log('Sending task data:', taskData)
console.log('Full error details:', { error, taskForm, response })
```
### 3. 修复TypeScript类型问题
- 为API响应添加类型注解 (`response: any`)
- 修复错误处理中的类型安全 (`catch (error: any)`)
## ✅ 修复结果
### 已完成
1. ✅ **后端API验证**: 确认正常工作,能成功创建任务
2. ✅ **前端数据验证**: 增加字段验证,防止空数据提交
3. ✅ **错误处理改进**: 显示具体错误信息,提升用户体验
4. ✅ **类型安全修复**: 解决TypeScript编译错误
5. ✅ **调试信息**: 开发模式下输出详细调试信息
### 测试确认
- **API测试**: `POST /api/tasks` 返回201状态码 ✅
- **数据库**: 任务成功存储(ID=1)✅
- **认证**: JWT token验证正常 ✅
## 📋 使用说明
### 现在可以正常使用:
1. 打开浏览器访问 `http://localhost:5173`
2. 点击"任务管理"菜单
3. 点击"新建任务"按钮
4. 填写任务信息(标题为必填项)
5. 点击"保存"即可成功创建任务
### 改进的功能:
- ✨ **字段验证**: 标题为必填,其他字段有默认值
- ✨ **错误提示**: 显示具体的错误原因
- ✨ **数据安全**: 确保数据类型正确
- ✨ **调试友好**: 控制台输出详细信息便于排查
## 🚀 系统状态
### 服务状态
- **前端**: `http://localhost:5173` 🟢 运行中
- **后端**: `http://localhost:8080` 🟢 运行中 (热加载)
- **数据库**: MySQL `task_track` 🟢 连接正常
### 功能状态
- ✅ 用户认证(测试登录)
- ✅ 任务创建
- ✅ 任务列表查询
- ✅ 用户列表查询
- ⚠️ 任务更新/删除(待实现具体API调用)
**总结**: 创建任务功能已修复并正常工作,用户现在可以成功创建任务了!

87
TEST_GUIDE.md

@ -0,0 +1,87 @@
# 🚀 任务管理系统测试指南
## 快速开始测试
系统现在运行在开发模式,您可以通过以下方式进行测试:
### 1. 访问测试登录页面
- 前端地址: **http://localhost:5173**
- 系统会自动跳转到测试登录页面
### 2. 一键登录测试
- 点击"**一键测试登录**"按钮
- 系统会自动创建测试用户并登录
- 测试用户信息:
- 用户名: `testuser`
- 密码: `123456`
- 角色: 管理员
### 3. 测试功能
登录成功后,您可以测试以下功能:
#### 📋 任务管理
- 访问 "任务管理" 页面
- 点击 "创建任务" 按钮
- 填写任务信息并上传附件
- 测试文件上传功能
#### 📎 附件功能
- 支持的文件格式:
- Office文档: `.doc`, `.docx`, `.xls`, `.xlsx`, `.ppt`, `.pptx`
- PDF文档: `.pdf`
- 图片文件: `.png`, `.jpg`, `.jpeg`, `.gif`
- 文本文件: `.txt`
- 拖拽上传或点击选择文件
- 查看任务附件列表
- 下载和删除附件
#### 🔍 搜索筛选
- 在任务列表中按标题搜索
- 按状态和优先级筛选
- 分页浏览任务
### 4. API测试
使用HTTP客户端测试API:
```bash
# 测试登录
POST http://localhost:8080/api/auth/test-login
# 获取任务列表(需要token)
GET http://localhost:8080/api/tasks?page=1&size=10
Authorization: Bearer <your_token>
# 上传文件(需要token)
POST http://localhost:8080/api/upload
Authorization: Bearer <your_token>
Content-Type: multipart/form-data
```
### 5. 故障排除
如果遇到401错误:
1. 确保已经登录并获取token
2. 检查token是否正确保存到localStorage
3. 刷新页面重新登录
如果前端无法访问:
1. 确认前端服务运行在 http://localhost:5173
2. 检查是否有端口冲突
3. 查看浏览器控制台错误信息
如果后端API失败:
1. 确认后端服务运行在 http://localhost:8080
2. 检查数据库连接是否正常
3. 查看后端控制台日志
### 6. 开发工具
- **浏览器开发者工具**: 查看网络请求和控制台日志
- **API测试**: 使用 `api_test_tasks.http` 文件
- **数据库**: 检查数据是否正确保存
---
**提示**: 这是开发测试版本,生产环境请关闭测试登录功能并配置真实的用户认证系统。

96
TROUBLESHOOTING.md

@ -0,0 +1,96 @@
# 🔧 404错误排查指南
## 当前问题
后端返回404错误,说明 `/api/auth/test-login` 端点没有正确注册。
## 解决步骤
### 1. 停止所有服务
```bash
# 在当前命令行窗口按 Ctrl+C 停止服务
# 或者运行:
taskkill /f /im go.exe
taskkill /f /im node.exe
```
### 2. 运行诊断工具
```bash
diagnose.bat
```
检查系统环境是否正常。
### 3. 重新启动系统
```bash
start.bat
```
### 4. 测试API连接
```bash
# 等待系统完全启动后运行:
test-api.bat
```
### 5. 手动测试
如果问题仍然存在,请按以下步骤手动测试:
#### 5.1 测试基本连接
浏览器访问: http://localhost:8080/api/test
应该看到API工作状态信息。
#### 5.2 测试登录端点
使用curl或浏览器开发者工具:
```bash
curl -X POST http://localhost:8080/api/auth/test-login
```
### 6. 常见问题解决
#### 问题A: 端口被占用
```bash
# 查看占用8080端口的进程
netstat -ano | findstr :8080
# 杀死进程(替换PID)
taskkill /f /pid [PID]
```
#### 问题B: 后端编译失败
```bash
cd backend
go mod tidy
go build main.go
```
#### 问题C: 前端无法启动
```bash
cd frontend
rm -rf node_modules
npm install
```
### 7. 如果问题持续存在
1. **查看后端控制台输出**
- 检查是否有编译错误
- 查看路由注册信息
2. **检查代码更改**
- 确认 `TestLogin` 方法已正确添加到 `auth.go`
- 确认路由已正确注册在 `router.go`
3. **手动重启后端**
```bash
restart-backend.bat
```
### 8. 快速验证方案
如果需要快速验证功能,可以暂时跳过登录:
1. 在浏览器localStorage中手动设置token:
```javascript
localStorage.setItem('token', 'test-token-123')
```
2. 临时修改中间件跳过token验证
---
💡 **提示**: 大多数404错误是由于后端服务未完全重启导致的。确保完全停止并重新启动后端服务。

56
backend/.air.toml

@ -0,0 +1,56 @@
# Air 热加载配置 - 仅监控后端 Go 代码
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./tmp/task-track-backend.exe"
cmd = "go build -o ./tmp/task-track-backend.exe ."
delay = 1000
# 排除不需要监控的目录 - 确保不监控前端和其他非后端目录
exclude_dir = ["tmp", "vendor", "testdata", "uploads", "database", ".git", "node_modules", "../frontend", "../todo", "../database"]
exclude_file = []
exclude_regex = ["_test.go", ".*\\.md$", ".*\\.bat$", ".*\\.sh$", ".*\\.http$"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
# 明确指定只监控后端相关目录
include_dir = [".", "internal", "model", "pkg"]
# 只监控 Go 代码和配置文件
include_ext = ["go", "yaml", "yml"]
include_file = []
kill_delay = "2s"
log = "tmp/build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = true
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
silent = false
time = true
[misc]
clean_on_exit = true
[proxy]
app_port = 0
enabled = false
proxy_port = 0
[screen]
clear_on_rebuild = true
keep_scroll = false

19
backend/config.yaml

@ -0,0 +1,19 @@
server:
port: "8080"
mode: "debug"
database:
driver: "mysql"
host: "localhost"
port: "3306"
username: "root"
password: "root"
database: "task_track"
charset: "utf8mb4"
jwt:
secret: "task-track-jwt-secret-key"
expire: 24
log:
level: "info"

57
backend/go.mod

@ -0,0 +1,57 @@
module task-track-backend
go 1.21
require (
github.com/gin-contrib/cors v1.4.0
github.com/gin-gonic/gin v1.9.1
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/spf13/viper v1.18.2
go.uber.org/zap v1.26.0
golang.org/x/crypto v0.17.0
gorm.io/driver/mysql v1.5.2
gorm.io/gorm v1.25.5
)
require (
github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

182
backend/go.sum

@ -0,0 +1,182 @@
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
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/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

68
backend/internal/config/config.go

@ -0,0 +1,68 @@
package config
import (
"log"
"github.com/spf13/viper"
)
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
JWT JWTConfig `mapstructure:"jwt"`
Log LogConfig `mapstructure:"log"`
}
type ServerConfig struct {
Port string `mapstructure:"port"`
Mode string `mapstructure:"mode"`
}
type DatabaseConfig struct {
Driver string `mapstructure:"driver"`
Host string `mapstructure:"host"`
Port string `mapstructure:"port"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Database string `mapstructure:"database"`
Charset string `mapstructure:"charset"`
}
type JWTConfig struct {
Secret string `mapstructure:"secret"`
Expire int `mapstructure:"expire"`
}
type LogConfig struct {
Level string `mapstructure:"level"`
}
func Load() *Config {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath("./config")
viper.AddConfigPath(".")
// 设置默认值
viper.SetDefault("server.port", "8080")
viper.SetDefault("server.mode", "debug")
viper.SetDefault("database.driver", "mysql")
viper.SetDefault("database.host", "localhost")
viper.SetDefault("database.port", "3306")
viper.SetDefault("database.charset", "utf8mb4")
viper.SetDefault("jwt.secret", "task-track-secret")
viper.SetDefault("jwt.expire", 24)
viper.SetDefault("log.level", "info")
if err := viper.ReadInConfig(); err != nil {
log.Printf("Warning: Could not read config file: %v", err)
log.Println("Using default configuration")
}
var config Config
if err := viper.Unmarshal(&config); err != nil {
log.Fatal("Failed to unmarshal config:", err)
}
return &config
}

179
backend/internal/handler/auth/auth_test.go

@ -0,0 +1,179 @@
package auth_test
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"task-track-backend/internal/config"
"task-track-backend/internal/router"
"task-track-backend/pkg/database"
)
func TestAuthUserFullFlow(t *testing.T) {
cfg := config.Load()
db, err := database.Init(cfg.Database)
if err != nil {
t.Fatalf("数据库连接失败: %v", err)
}
r := router.Setup(db)
username := "testuser_api"
password := "testpass123"
email := "testuser_api@example.com"
var userID uint
var token string
// 注册前彻底物理删除测试用户,避免唯一索引冲突
db.Unscoped().Exec("DELETE FROM users WHERE username = ?", username)
// 1. 先尝试通过用户名查找用户(用 /api/users?username=xxx 更优,但此项目无此接口,只能用 /api/users + 遍历或注册后获取)
// 先尝试登录,能登录说明用户存在
loginBody := map[string]interface{}{
"username": username,
"password": password,
}
loginJSON, _ := json.Marshal(loginBody)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/auth/login", bytes.NewReader(loginJSON))
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
if w.Code == 200 {
// 登录成功,用户已存在
var loginResp struct {
Data struct {
Token string `json:"token"`
User struct {
ID uint `json:"id"`
} `json:"user"`
} `json:"data"`
}
_ = json.Unmarshal(w.Body.Bytes(), &loginResp)
token = loginResp.Data.Token
userID = loginResp.Data.User.ID
// update_user
updateBody := map[string]interface{}{
"real_name": "API测试用户",
"email": "updated_" + email,
}
updateJSON, _ := json.Marshal(updateBody)
w2 := httptest.NewRecorder()
req2, _ := http.NewRequest("POST", fmt.Sprintf("/api/users/update/%d", userID), bytes.NewReader(updateJSON))
req2.Header.Set("Content-Type", "application/json")
req2.Header.Set("Authorization", "Bearer "+token)
r.ServeHTTP(w2, req2)
if w2.Code != 200 {
t.Fatalf("update_user 失败: %d, %s", w2.Code, w2.Body.String())
}
// delete_user
w3 := httptest.NewRecorder()
req3, _ := http.NewRequest("POST", fmt.Sprintf("/api/users/delete/%d", userID), nil)
req3.Header.Set("Authorization", "Bearer "+token)
r.ServeHTTP(w3, req3)
if w3.Code != 200 {
t.Fatalf("delete_user 失败: %d, %s", w3.Code, w3.Body.String())
}
}
// 2. 注册用户
registerBody := map[string]interface{}{
"username": username,
"password": password,
"email": email,
}
registerJSON, _ := json.Marshal(registerBody)
w = httptest.NewRecorder()
req, _ = http.NewRequest("POST", "/api/auth/register", bytes.NewReader(registerJSON))
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
if w.Code != 201 {
t.Fatalf("注册用户失败,状态码: %d, 响应: %s", w.Code, w.Body.String())
}
// 注册后调试:输出数据库密码字段
var dbUser struct{ Password string }
db.Raw("SELECT password FROM users WHERE username = ?", username).Scan(&dbUser)
t.Logf("注册后数据库密码字段: %s", dbUser.Password)
// 避免极端情况下事务未提交,sleep 100ms
time.Sleep(100 * time.Millisecond)
// 3. 登录
t.Logf("登录请求体: %+v", loginBody)
loginJSON, _ = json.Marshal(loginBody)
w = httptest.NewRecorder()
req, _ = http.NewRequest("POST", "/api/auth/login", bytes.NewReader(loginJSON))
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("用户登录失败,状态码: %d, 响应: %s", w.Code, w.Body.String())
}
var loginResp struct {
Data struct {
Token string `json:"token"`
User struct {
ID uint `json:"id"`
} `json:"user"`
} `json:"data"`
}
_ = json.Unmarshal(w.Body.Bytes(), &loginResp)
token = loginResp.Data.Token
userID = loginResp.Data.User.ID
// 4. 获取我的信息
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/api/auth/me", nil)
req.Header.Set("Authorization", "Bearer "+token)
r.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("获取我的信息失败,状态码: %d, 响应: %s", w.Code, w.Body.String())
}
// 5. 用户登出
w = httptest.NewRecorder()
req, _ = http.NewRequest("POST", "/api/auth/logout", nil)
req.Header.Set("Authorization", "Bearer "+token)
r.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("用户登出失败,状态码: %d, 响应: %s", w.Code, w.Body.String())
}
// 6. update_user
updateBody := map[string]interface{}{
"real_name": "API测试用户2",
"email": "updated2_" + email,
}
updateJSON, _ := json.Marshal(updateBody)
w = httptest.NewRecorder()
req, _ = http.NewRequest("POST", fmt.Sprintf("/api/users/update/%d", userID), bytes.NewReader(updateJSON))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
r.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("update_user 失败: %d, %s", w.Code, w.Body.String())
}
// 7. get_user
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", fmt.Sprintf("/api/users/%d", userID), nil)
req.Header.Set("Authorization", "Bearer "+token)
r.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("get_user 失败: %d, %s", w.Code, w.Body.String())
}
// 8. delete_user
w = httptest.NewRecorder()
req, _ = http.NewRequest("POST", fmt.Sprintf("/api/users/delete/%d", userID), nil)
req.Header.Set("Authorization", "Bearer "+token)
r.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("delete_user 失败: %d, %s", w.Code, w.Body.String())
}
}

11
backend/internal/handler/auth/handler.go

@ -0,0 +1,11 @@
package auth
import "gorm.io/gorm"
type AuthHandler struct {
db *gorm.DB
}
func NewAuthHandler(db *gorm.DB) *AuthHandler {
return &AuthHandler{db: db}
}

9
backend/internal/handler/auth/handler_test.go

@ -0,0 +1,9 @@
package auth_test
import (
"testing"
)
func TestHandler(t *testing.T) {
// TODO: 针对 handler.go 的内容编写测试
}

72
backend/internal/handler/auth/login.go

@ -0,0 +1,72 @@
package auth
import (
"net/http"
"task-track-backend/model"
"task-track-backend/pkg/auth"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
)
func (h *AuthHandler) Login(c *gin.Context) {
var loginData struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
if err := c.ShouldBindJSON(&loginData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "Invalid request data",
"error": err.Error(),
})
return
}
// 查找用户
var user model.User
if err := h.db.Where("username = ?", loginData.Username).First(&user).Error; err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid credentials",
})
return
}
// 验证密码
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(loginData.Password)); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid credentials",
})
return
}
// 获取用户的默认组织ID(如果没有关联组织,使用0)
var organizationID uint = 0
var userOrg model.UserOrganization
if err := h.db.Where("user_id = ?", user.ID).First(&userOrg).Error; err == nil {
organizationID = userOrg.OrganizationID
}
// 生成 JWT token
token, err := auth.GenerateToken(user.ID, user.Username, organizationID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "Failed to generate token",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "Login successful",
"data": gin.H{
"token": token,
"user": user,
},
})
}

7
backend/internal/handler/auth/logout.go

@ -0,0 +1,7 @@
package auth
import "github.com/gin-gonic/gin"
func (h *AuthHandler) Logout(c *gin.Context) {
c.JSON(200, gin.H{"code": 200, "message": "Logout successful"})
}

23
backend/internal/handler/auth/me.go

@ -0,0 +1,23 @@
package auth
import (
"task-track-backend/model"
"github.com/gin-gonic/gin"
)
func (h *AuthHandler) Me(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(401, gin.H{"code": 401, "message": "Unauthorized"})
return
}
var user model.User
if err := h.db.First(&user, userID).Error; err != nil {
c.JSON(404, gin.H{"code": 404, "message": "User not found"})
return
}
c.JSON(200, gin.H{"code": 200, "message": "Success", "data": user})
}

36
backend/internal/handler/auth/refresh.go

@ -0,0 +1,36 @@
package auth
import (
"task-track-backend/model"
"task-track-backend/pkg/auth"
"github.com/gin-gonic/gin"
)
func (h *AuthHandler) Refresh(c *gin.Context) {
userID, exists := c.Get("user_id")
if !exists {
c.JSON(401, gin.H{"code": 401, "message": "Unauthorized"})
return
}
var user model.User
if err := h.db.First(&user, userID).Error; err != nil {
c.JSON(404, gin.H{"code": 404, "message": "User not found"})
return
}
var organizationID uint = 0
var userOrg model.UserOrganization
if err := h.db.Where("user_id = ?", user.ID).First(&userOrg).Error; err == nil {
organizationID = userOrg.OrganizationID
}
token, err := auth.GenerateToken(user.ID, user.Username, organizationID)
if err != nil {
c.JSON(500, gin.H{"code": 500, "message": "Failed to generate token", "error": err.Error()})
return
}
c.JSON(200, gin.H{"code": 200, "message": "Token refreshed successfully", "data": gin.H{"token": token}})
}

50
backend/internal/handler/auth/register.go

@ -0,0 +1,50 @@
package auth
import (
"task-track-backend/model"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
)
type RegisterRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
Email string `json:"email" binding:"required"`
}
func (h *AuthHandler) Register(c *gin.Context) {
var req RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"code": 400, "message": "Invalid request data", "error": err.Error()})
return
}
// 检查用户名是否已存在
var existingUser model.User
if err := h.db.Where("username = ?", req.Username).First(&existingUser).Error; err == nil {
c.JSON(409, gin.H{"code": 409, "message": "Username already exists"})
return
}
// 加密密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
c.JSON(500, gin.H{"code": 500, "message": "Failed to hash password", "error": err.Error()})
return
}
user := model.User{
Username: req.Username,
Password: string(hashedPassword),
Email: req.Email,
}
// 创建用户
if err := h.db.Create(&user).Error; err != nil {
c.JSON(500, gin.H{"code": 500, "message": "Failed to create user", "error": err.Error()})
return
}
c.JSON(201, gin.H{"code": 201, "message": "User created successfully", "data": user})
}

8
backend/internal/handler/auth/test-auth.bat

@ -0,0 +1,8 @@
@echo off
REM 一键运行 auth 目录下所有 Go 测试
cd /d %~dp0
cd ..\..\..\..
echo 正在运行 auth 目录下所有测试...
go test ./internal/handler/auth -v
pause

54
backend/internal/handler/auth/test_login.go

@ -0,0 +1,54 @@
package auth
import (
"task-track-backend/model"
"task-track-backend/pkg/auth"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
func (h *AuthHandler) TestLogin(c *gin.Context) {
// 创建测试用户(如果不存在)
var user model.User
err := h.db.Where("username = ?", "test").First(&user).Error
if err == gorm.ErrRecordNotFound {
// 创建测试用户
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("test123"), bcrypt.DefaultCost)
user = model.User{
Username: "test",
Email: "test-user@example.com",
Password: string(hashedPassword),
RealName: "测试用户",
Status: 1,
}
if err := h.db.Create(&user).Error; err != nil {
c.JSON(500, gin.H{"code": 500, "message": "Failed to create test user", "error": err.Error()})
return
}
}
// 获取用户的默认组织ID(如果没有关联组织,使用0)
var organizationID uint = 0
var userOrg model.UserOrganization
if err := h.db.Where("user_id = ?", user.ID).First(&userOrg).Error; err == nil {
organizationID = userOrg.OrganizationID
}
// 生成 JWT token
token, err := auth.GenerateToken(user.ID, user.Username, organizationID)
if err != nil {
c.JSON(500, gin.H{"code": 500, "message": "Failed to generate token", "error": err.Error()})
return
}
c.JSON(200, gin.H{
"code": 200,
"message": "Test login successful",
"data": gin.H{
"token": token,
"user": user,
},
})
}

36
backend/internal/handler/organization/create_organization.go

@ -0,0 +1,36 @@
package organization
import (
"net/http"
"task-track-backend/model"
"github.com/gin-gonic/gin"
)
func (h *OrganizationHandler) CreateOrganization(c *gin.Context) {
var organization model.Organization
if err := c.ShouldBindJSON(&organization); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "Invalid request data",
"error": err.Error(),
})
return
}
// 创建机构
if err := h.db.Create(&organization).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "Failed to create organization",
"error": err.Error(),
})
return
}
c.JSON(http.StatusCreated, gin.H{
"code": 201,
"message": "Organization created successfully",
"data": organization,
})
}

9
backend/internal/handler/organization/create_organization_test.go

@ -0,0 +1,9 @@
package organization
import (
"testing"
)
func TestCreateOrganization(t *testing.T) {
// TODO: 构造请求和依赖,调用 CreateOrganization handler 并断言结果
}

35
backend/internal/handler/organization/delete_organization.go

@ -0,0 +1,35 @@
package organization
import (
"net/http"
"strconv"
"task-track-backend/model"
"github.com/gin-gonic/gin"
)
func (h *OrganizationHandler) DeleteOrganization(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "Invalid organization ID",
})
return
}
// 软删除机构
if err := h.db.Delete(&model.Organization{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "Failed to delete organization",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "Organization deleted successfully",
})
}

9
backend/internal/handler/organization/delete_organization_test.go

@ -0,0 +1,9 @@
package organization
import (
"testing"
)
func TestDeleteOrganization(t *testing.T) {
// TODO: 构造请求和依赖,调用 DeleteOrganization handler 并断言结果
}

44
backend/internal/handler/organization/get_organization.go

@ -0,0 +1,44 @@
package organization
import (
"net/http"
"strconv"
"task-track-backend/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func (h *OrganizationHandler) GetOrganization(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "Invalid organization ID",
})
return
}
var organization model.Organization
if err := h.db.First(&organization, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{
"code": 404,
"message": "Organization not found",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "Failed to get organization",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "Success",
"data": organization,
})
}

9
backend/internal/handler/organization/get_organization_test.go

@ -0,0 +1,9 @@
package organization
import (
"testing"
)
func TestGetOrganization(t *testing.T) {
// TODO: 构造请求和依赖,调用 GetOrganization handler 并断言结果
}

36
backend/internal/handler/organization/get_organization_users.go

@ -0,0 +1,36 @@
package organization
import (
"net/http"
"strconv"
"task-track-backend/model"
"github.com/gin-gonic/gin"
)
func (h *OrganizationHandler) GetOrganizationUsers(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "Invalid organization ID",
})
return
}
var users []model.User
if err := h.db.Where("organization_id = ?", id).Find(&users).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "Failed to get organization users",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "Success",
"data": users,
})
}

9
backend/internal/handler/organization/get_organization_users_test.go

@ -0,0 +1,9 @@
package organization
import (
"testing"
)
func TestGetOrganizationUsers(t *testing.T) {
// TODO: 构造请求和依赖,调用 GetOrganizationUsers handler 并断言结果
}

43
backend/internal/handler/organization/get_organizations.go

@ -0,0 +1,43 @@
package organization
import (
"net/http"
"strconv"
"task-track-backend/model"
"github.com/gin-gonic/gin"
)
func (h *OrganizationHandler) GetOrganizations(c *gin.Context) {
var organizations []model.Organization
// 分页参数
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
offset := (page - 1) * limit
// 查询机构
if err := h.db.Offset(offset).Limit(limit).Find(&organizations).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "Failed to get organizations",
"error": err.Error(),
})
return
}
// 获取总数
var total int64
h.db.Model(&model.Organization{}).Count(&total)
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "Success",
"data": gin.H{
"list": organizations,
"total": total,
"page": page,
"limit": limit,
},
})
}

9
backend/internal/handler/organization/get_organizations_test.go

@ -0,0 +1,9 @@
package organization
import (
"testing"
)
func TestGetOrganizations(t *testing.T) {
// TODO: 构造请求和依赖,调用 GetOrganizations handler 并断言结果
}

11
backend/internal/handler/organization/handler.go

@ -0,0 +1,11 @@
package organization
import "gorm.io/gorm"
type OrganizationHandler struct {
db *gorm.DB
}
func NewOrganizationHandler(db *gorm.DB) *OrganizationHandler {
return &OrganizationHandler{db: db}
}

9
backend/internal/handler/organization/handler_test.go

@ -0,0 +1,9 @@
package organization
import (
"testing"
)
func TestHandler(t *testing.T) {
// TODO: 针对 handler.go 的内容编写测试
}

65
backend/internal/handler/organization/update_organization.go

@ -0,0 +1,65 @@
package organization
import (
"net/http"
"strconv"
"task-track-backend/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func (h *OrganizationHandler) UpdateOrganization(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "Invalid organization ID",
})
return
}
var updateData model.Organization
if err := c.ShouldBindJSON(&updateData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "Invalid request data",
"error": err.Error(),
})
return
}
// 检查机构是否存在
var organization model.Organization
if err := h.db.First(&organization, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{
"code": 404,
"message": "Organization not found",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "Failed to find organization",
"error": err.Error(),
})
return
}
// 更新机构信息
if err := h.db.Model(&organization).Updates(updateData).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "Failed to update organization",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "Organization updated successfully",
"data": organization,
})
}

9
backend/internal/handler/organization/update_organization_test.go

@ -0,0 +1,9 @@
package organization
import (
"testing"
)
func TestUpdateOrganization(t *testing.T) {
// TODO: 构造请求和依赖,调用 UpdateOrganization handler 并断言结果
}

7
backend/internal/handler/statistics/get_overview.go

@ -0,0 +1,7 @@
package statistics
import "github.com/gin-gonic/gin"
func (h *StatisticsHandler) GetOverview(c *gin.Context) {
// TODO: 实现统计概览接口
}

9
backend/internal/handler/statistics/get_overview_test.go

@ -0,0 +1,9 @@
package statistics
import (
"testing"
)
func TestGetOverview(t *testing.T) {
// TODO: 构造请求和依赖,调用 GetOverview handler 并断言结果
}

7
backend/internal/handler/statistics/get_task_statistics.go

@ -0,0 +1,7 @@
package statistics
import "github.com/gin-gonic/gin"
func (h *StatisticsHandler) GetTaskStatistics(c *gin.Context) {
// TODO: 实现任务统计接口
}

9
backend/internal/handler/statistics/get_task_statistics_test.go

@ -0,0 +1,9 @@
package statistics
import (
"testing"
)
func TestGetTaskStatistics(t *testing.T) {
// TODO: 构造请求和依赖,调用 GetTaskStatistics handler 并断言结果
}

7
backend/internal/handler/statistics/get_user_statistics.go

@ -0,0 +1,7 @@
package statistics
import "github.com/gin-gonic/gin"
func (h *StatisticsHandler) GetUserStatistics(c *gin.Context) {
// TODO: 实现用户统计接口
}

9
backend/internal/handler/statistics/get_user_statistics_test.go

@ -0,0 +1,9 @@
package statistics
import (
"testing"
)
func TestGetUserStatistics(t *testing.T) {
// TODO: 构造请求和依赖,调用 GetUserStatistics handler 并断言结果
}

11
backend/internal/handler/statistics/handler.go

@ -0,0 +1,11 @@
package statistics
import "gorm.io/gorm"
type StatisticsHandler struct {
db *gorm.DB
}
func NewStatisticsHandler(db *gorm.DB) *StatisticsHandler {
return &StatisticsHandler{db: db}
}

9
backend/internal/handler/statistics/handler_test.go

@ -0,0 +1,9 @@
package statistics
import (
"testing"
)
func TestHandler(t *testing.T) {
// TODO: 针对 handler.go 的内容编写测试
}

9
backend/internal/handler/task/add_task_attachment.go

@ -0,0 +1,9 @@
package task
import (
"github.com/gin-gonic/gin"
)
func (h *TaskHandler) AddTaskAttachment(c *gin.Context) {
// ... 复制原有实现 ...
}

9
backend/internal/handler/task/add_task_attachment_test.go

@ -0,0 +1,9 @@
package task
import (
"testing"
)
func TestAddTaskAttachment(t *testing.T) {
// TODO: 构造请求和依赖,调用 AddTaskAttachment handler 并断言结果
}

9
backend/internal/handler/task/add_task_comment.go

@ -0,0 +1,9 @@
package task
import (
"github.com/gin-gonic/gin"
)
func (h *TaskHandler) AddTaskComment(c *gin.Context) {
// ... 复制原有实现 ...
}

9
backend/internal/handler/task/add_task_comment_test.go

@ -0,0 +1,9 @@
package task
import (
"testing"
)
func TestAddTaskComment(t *testing.T) {
// TODO: 构造请求和依赖,调用 AddTaskComment handler 并断言结果
}

9
backend/internal/handler/task/create_task.go

@ -0,0 +1,9 @@
package task
import (
"github.com/gin-gonic/gin"
)
func (h *TaskHandler) CreateTask(c *gin.Context) {
// ... 复制原有实现 ...
}

9
backend/internal/handler/task/create_task_test.go

@ -0,0 +1,9 @@
package task
import (
"testing"
)
func TestCreateTask(t *testing.T) {
// TODO: 构造请求和依赖,调用 CreateTask handler 并断言结果
}

9
backend/internal/handler/task/delete_task.go

@ -0,0 +1,9 @@
package task
import (
"github.com/gin-gonic/gin"
)
func (h *TaskHandler) DeleteTask(c *gin.Context) {
// ... 复制原有实现 ...
}

9
backend/internal/handler/task/delete_task_attachment.go

@ -0,0 +1,9 @@
package task
import (
"github.com/gin-gonic/gin"
)
func (h *TaskHandler) DeleteTaskAttachment(c *gin.Context) {
// ... 复制原有实现 ...
}

9
backend/internal/handler/task/delete_task_attachment_test.go

@ -0,0 +1,9 @@
package task
import (
"testing"
)
func TestDeleteTaskAttachment(t *testing.T) {
// TODO: 构造请求和依赖,调用 DeleteTaskAttachment handler 并断言结果
}

9
backend/internal/handler/task/delete_task_comment.go

@ -0,0 +1,9 @@
package task
import (
"github.com/gin-gonic/gin"
)
func (h *TaskHandler) DeleteTaskComment(c *gin.Context) {
// ... 复制原有实现 ...
}

9
backend/internal/handler/task/delete_task_comment_test.go

@ -0,0 +1,9 @@
package task
import (
"testing"
)
func TestDeleteTaskComment(t *testing.T) {
// TODO: 构造请求和依赖,调用 DeleteTaskComment handler 并断言结果
}

9
backend/internal/handler/task/delete_task_test.go

@ -0,0 +1,9 @@
package task
import (
"testing"
)
func TestDeleteTask(t *testing.T) {
// TODO: 构造请求和依赖,调用 DeleteTask handler 并断言结果
}

9
backend/internal/handler/task/get_task.go

@ -0,0 +1,9 @@
package task
import (
"github.com/gin-gonic/gin"
)
func (h *TaskHandler) GetTask(c *gin.Context) {
// ... 复制原有实现 ...
}

9
backend/internal/handler/task/get_task_attachments.go

@ -0,0 +1,9 @@
package task
import (
"github.com/gin-gonic/gin"
)
func (h *TaskHandler) GetTaskAttachments(c *gin.Context) {
// ... 复制原有实现 ...
}

9
backend/internal/handler/task/get_task_attachments_test.go

@ -0,0 +1,9 @@
package task
import (
"testing"
)
func TestGetTaskAttachments(t *testing.T) {
// TODO: 构造请求和依赖,调用 GetTaskAttachments handler 并断言结果
}

9
backend/internal/handler/task/get_task_comments.go

@ -0,0 +1,9 @@
package task
import (
"github.com/gin-gonic/gin"
)
func (h *TaskHandler) GetTaskComments(c *gin.Context) {
// ... 复制原有实现 ...
}

9
backend/internal/handler/task/get_task_comments_test.go

@ -0,0 +1,9 @@
package task
import (
"testing"
)
func TestGetTaskComments(t *testing.T) {
// TODO: 构造请求和依赖,调用 GetTaskComments handler 并断言结果
}

9
backend/internal/handler/task/get_task_test.go

@ -0,0 +1,9 @@
package task
import (
"testing"
)
func TestGetTask(t *testing.T) {
// TODO: 构造请求和依赖,调用 GetTask handler 并断言结果
}

9
backend/internal/handler/task/get_tasks.go

@ -0,0 +1,9 @@
package task
import (
"github.com/gin-gonic/gin"
)
func (h *TaskHandler) GetTasks(c *gin.Context) {
// ... 复制原有实现 ...
}

9
backend/internal/handler/task/get_tasks_test.go

@ -0,0 +1,9 @@
package task
import (
"testing"
)
func TestGetTasks(t *testing.T) {
// TODO: 构造请求和依赖,调用 GetTasks handler 并断言结果
}

27
backend/internal/handler/task/handler.go

@ -0,0 +1,27 @@
package task
import "gorm.io/gorm"
// TaskHandler 结构体
type TaskHandler struct {
db *gorm.DB
}
func NewTaskHandler(db *gorm.DB) *TaskHandler {
return &TaskHandler{db: db}
}
// TaskResponse 任务列表响应结构体
type TaskResponse struct {
// ... 复制原有字段 ...
}
// TaskDetailResponse 任务详情响应结构体
type TaskDetailResponse struct {
// ... 复制原有字段 ...
}
// TaskCommentResponse 任务评论响应结构体
type TaskCommentResponse struct {
// ... 复制原有字段 ...
}

9
backend/internal/handler/task/handler_test.go

@ -0,0 +1,9 @@
package task
import (
"testing"
)
func TestHandler(t *testing.T) {
// TODO: 针对 handler.go 的内容编写测试
}

9
backend/internal/handler/task/update_task.go

@ -0,0 +1,9 @@
package task
import (
"github.com/gin-gonic/gin"
)
func (h *TaskHandler) UpdateTask(c *gin.Context) {
// ... 复制原有实现 ...
}

9
backend/internal/handler/task/update_task_status.go

@ -0,0 +1,9 @@
package task
import (
"github.com/gin-gonic/gin"
)
func (h *TaskHandler) UpdateTaskStatus(c *gin.Context) {
// ... 复制原有实现 ...
}

9
backend/internal/handler/task/update_task_status_test.go

@ -0,0 +1,9 @@
package task
import (
"testing"
)
func TestUpdateTaskStatus(t *testing.T) {
// TODO: 构造请求和依赖,调用 UpdateTaskStatus handler 并断言结果
}

9
backend/internal/handler/task/update_task_test.go

@ -0,0 +1,9 @@
package task
import (
"testing"
)
func TestUpdateTask(t *testing.T) {
// TODO: 构造请求和依赖,调用 UpdateTask handler 并断言结果
}

9
backend/internal/handler/task/upload_file.go

@ -0,0 +1,9 @@
package task
import (
"github.com/gin-gonic/gin"
)
func (h *TaskHandler) UploadFile(c *gin.Context) {
// ... 复制原有实现 ...
}

9
backend/internal/handler/task/upload_file_test.go

@ -0,0 +1,9 @@
package task
import (
"testing"
)
func TestUploadFile(t *testing.T) {
// TODO: 构造请求和依赖,调用 UploadFile handler 并断言结果
}

35
backend/internal/handler/user/delete_user.go

@ -0,0 +1,35 @@
package user
import (
"net/http"
"strconv"
"task-track-backend/model"
"github.com/gin-gonic/gin"
)
func (h *UserHandler) DeleteUser(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "Invalid user ID",
})
return
}
// 软删除用户
if err := h.db.Delete(&model.User{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "Failed to delete user",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "User deleted successfully",
})
}

44
backend/internal/handler/user/get_user.go

@ -0,0 +1,44 @@
package user
import (
"net/http"
"strconv"
"task-track-backend/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func (h *UserHandler) GetUser(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "Invalid user ID",
})
return
}
var user model.User
if err := h.db.First(&user, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{
"code": 404,
"message": "User not found",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "Failed to get user",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "Success",
"data": user,
})
}

43
backend/internal/handler/user/get_users.go

@ -0,0 +1,43 @@
package user
import (
"net/http"
"strconv"
"task-track-backend/model"
"github.com/gin-gonic/gin"
)
func (h *UserHandler) GetUsers(c *gin.Context) {
var users []model.User
// 分页参数
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
offset := (page - 1) * limit
// 查询用户
if err := h.db.Offset(offset).Limit(limit).Find(&users).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "Failed to get users",
"error": err.Error(),
})
return
}
// 获取总数
var total int64
h.db.Model(&model.User{}).Count(&total)
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "Success",
"data": gin.H{
"list": users,
"total": total,
"page": page,
"limit": limit,
},
})
}

11
backend/internal/handler/user/handler.go

@ -0,0 +1,11 @@
package user
import "gorm.io/gorm"
type UserHandler struct {
db *gorm.DB
}
func NewUserHandler(db *gorm.DB) *UserHandler {
return &UserHandler{db: db}
}

65
backend/internal/handler/user/update_user.go

@ -0,0 +1,65 @@
package user
import (
"net/http"
"strconv"
"task-track-backend/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func (h *UserHandler) UpdateUser(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "Invalid user ID",
})
return
}
var updateData model.User
if err := c.ShouldBindJSON(&updateData); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "Invalid request data",
"error": err.Error(),
})
return
}
// 检查用户是否存在
var user model.User
if err := h.db.First(&user, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{
"code": 404,
"message": "User not found",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "Failed to find user",
"error": err.Error(),
})
return
}
// 更新用户信息
if err := h.db.Model(&user).Updates(updateData).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "Failed to update user",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 200,
"message": "User updated successfully",
"data": user,
})
}

55
backend/internal/middleware/auth.go

@ -0,0 +1,55 @@
package middleware
import (
"net/http"
"strings"
"task-track-backend/pkg/auth"
"github.com/gin-gonic/gin"
)
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 获取 Authorization header
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Authorization header is required",
})
c.Abort()
return
}
// 验证 Bearer token 格式
parts := strings.SplitN(authHeader, " ", 2)
if !(len(parts) == 2 && parts[0] == "Bearer") {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Authorization header format must be Bearer {token}",
})
c.Abort()
return
}
token := parts[1]
// 验证 JWT token
claims, err := auth.ValidateToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{
"code": 401,
"message": "Invalid token",
})
c.Abort()
return
}
// 将用户信息存储到上下文中
c.Set("user_id", claims.UserID)
c.Set("username", claims.Username)
c.Set("organization_id", claims.OrganizationID)
c.Next()
}
}

102
backend/internal/router/router.go

@ -0,0 +1,102 @@
package router
import (
authHandlerPkg "task-track-backend/internal/handler/auth"
organizationHandlerPkg "task-track-backend/internal/handler/organization"
taskHandlerPkg "task-track-backend/internal/handler/task"
userHandlerPkg "task-track-backend/internal/handler/user"
"task-track-backend/internal/middleware"
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func Setup(db *gorm.DB) *gin.Engine {
r := gin.Default()
// CORS 中间件
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"*"},
AllowCredentials: true,
}))
// 初始化处理器
authHandler := authHandlerPkg.NewAuthHandler(db)
userHandler := userHandlerPkg.NewUserHandler(db)
organizationHandler := organizationHandlerPkg.NewOrganizationHandler(db)
taskHandler := taskHandlerPkg.NewTaskHandler(db)
// statisticsHandler := statisticsHandlerPkg.NewStatisticsHandler(db) // TODO: 待实现
// 公开路由
public := r.Group("/api")
{
public.POST("/auth/login", authHandler.Login)
public.POST("/auth/register", authHandler.Register)
public.POST("/auth/test-login", authHandler.TestLogin) // 测试登录端点
// 简单的测试端点
public.GET("/test", func(c *gin.Context) {
c.JSON(200, gin.H{
"code": 200,
"message": "API is working",
"time": time.Now().Format("2006-01-02 15:04:05"),
})
})
}
// 需要认证的路由
private := r.Group("/api")
private.Use(middleware.AuthMiddleware())
{
// 认证相关
private.POST("/auth/logout", authHandler.Logout)
private.POST("/auth/refresh", authHandler.Refresh)
private.GET("/auth/me", authHandler.Me)
// 用户管理
private.GET("/users", userHandler.GetUsers)
private.GET("/users/:id", userHandler.GetUser)
private.POST("/users/update/:id", userHandler.UpdateUser)
private.POST("/users/delete/:id", userHandler.DeleteUser)
// 机构管理
private.GET("/organizations", organizationHandler.GetOrganizations)
private.POST("/organizations", organizationHandler.CreateOrganization)
private.GET("/organizations/:id", organizationHandler.GetOrganization)
private.POST("/organizations/update/:id", organizationHandler.UpdateOrganization)
private.POST("/organizations/delete/:id", organizationHandler.DeleteOrganization)
private.GET("/organizations/:id/users", organizationHandler.GetOrganizationUsers)
// 任务管理
private.GET("/tasks", taskHandler.GetTasks)
private.POST("/tasks", taskHandler.CreateTask)
private.GET("/tasks/:id", taskHandler.GetTask)
private.POST("/tasks/update/:id", taskHandler.UpdateTask)
private.POST("/tasks/delete/:id", taskHandler.DeleteTask)
private.POST("/tasks/status/:id", taskHandler.UpdateTaskStatus)
// 任务评论
private.GET("/tasks/:id/comments", taskHandler.GetTaskComments)
private.POST("/tasks/:id/comments", taskHandler.AddTaskComment)
private.DELETE("/tasks/:id/comments/:commentId", taskHandler.DeleteTaskComment)
// 文件上传
private.POST("/upload", taskHandler.UploadFile)
// 任务附件
private.GET("/tasks/:id/attachments", taskHandler.GetTaskAttachments)
private.POST("/tasks/:id/attachments", taskHandler.AddTaskAttachment)
private.DELETE("/attachments/:attachmentId", taskHandler.DeleteTaskAttachment)
// 统计相关 (TODO: 待实现)
// private.GET("/statistics/overview", statisticsHandler.GetOverview)
// private.GET("/statistics/tasks", statisticsHandler.GetTaskStatistics)
// private.GET("/statistics/users", statisticsHandler.GetUserStatistics)
}
return r
}

39
backend/main.go

@ -0,0 +1,39 @@
package main
import (
"log"
"task-track-backend/internal/config"
"task-track-backend/internal/router"
"task-track-backend/pkg/database"
"task-track-backend/pkg/logger"
"github.com/gin-gonic/gin"
)
func main() {
// 初始化配置
cfg := config.Load()
// 初始化日志
logger.Init(cfg.Log.Level)
// 初始化数据库
db, err := database.Init(cfg.Database)
if err != nil {
log.Fatal("Failed to initialize database:", err)
}
// 设置 Gin 模式
if cfg.Server.Mode == "release" {
gin.SetMode(gin.ReleaseMode)
}
// 初始化路由
r := router.Setup(db)
// 启动服务器
log.Printf("Server starting on port %s", cfg.Server.Port)
if err := r.Run(":" + cfg.Server.Port); err != nil {
log.Fatal("Failed to start server:", err)
}
}

108
backend/model/model.go

@ -0,0 +1,108 @@
package model
import (
"time"
"gorm.io/gorm"
)
// User 用户表
type User struct {
ID uint `json:"id" gorm:"primaryKey"`
Username string `json:"username" gorm:"uniqueIndex;size:50;not null"`
Email string `json:"email" gorm:"uniqueIndex;size:100;not null"`
Password string `json:"-" gorm:"size:255;not null"`
RealName string `json:"real_name" gorm:"size:50"`
Phone string `json:"phone" gorm:"size:20"`
Avatar string `json:"avatar" gorm:"size:255"`
Status int `json:"status" gorm:"default:1;comment:状态 1:启用 0:禁用"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}
// Organization 机构表
type Organization struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"size:100;not null"`
Code string `json:"code" gorm:"uniqueIndex;size:50;not null"`
ParentID uint `json:"parent_id" gorm:"default:0"`
Level int `json:"level" gorm:"default:1"`
Sort int `json:"sort" gorm:"default:0"`
Status int `json:"status" gorm:"default:1;comment:状态 1:启用 0:禁用"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}
// UserOrganization 用户机构关系表
type UserOrganization struct {
ID uint `json:"id" gorm:"primaryKey"`
UserID uint `json:"user_id" gorm:"not null"`
OrganizationID uint `json:"organization_id" gorm:"not null"`
Role string `json:"role" gorm:"size:20;default:'member';comment:角色 admin:管理员 member:成员"`
CreatedAt time.Time `json:"created_at"`
// 关联关系
User User `json:"user" gorm:"foreignKey:UserID"`
Organization Organization `json:"organization" gorm:"foreignKey:OrganizationID"`
}
// Task 任务表
type Task struct {
ID uint `json:"id" gorm:"primaryKey"`
Title string `json:"title" gorm:"size:255;not null"`
Description string `json:"description" gorm:"type:text"`
Type string `json:"type" gorm:"size:50"`
Priority string `json:"priority" gorm:"size:20;default:'medium';comment:优先级 urgent:紧急 high:高 medium:中 low:低"`
Status string `json:"status" gorm:"size:20;default:'pending';comment:状态 pending:待处理 in_progress:进行中 completed:已完成 cancelled:已取消"`
CreatorID uint `json:"creator_id" gorm:"not null"`
AssigneeID uint `json:"assignee_id"`
OrganizationID uint `json:"organization_id" gorm:"not null"`
StartTime *time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time"`
CompletedAt *time.Time `json:"completed_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}
// TaskTag 任务标签表
type TaskTag struct {
ID uint `json:"id" gorm:"primaryKey"`
Name string `json:"name" gorm:"size:50;not null"`
Color string `json:"color" gorm:"size:20;default:'#409EFF'"`
OrganizationID uint `json:"organization_id" gorm:"not null"`
CreatedAt time.Time `json:"created_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}
// TaskTagRelation 任务标签关系表
type TaskTagRelation struct {
ID uint `json:"id" gorm:"primaryKey"`
TaskID uint `json:"task_id" gorm:"not null"`
TagID uint `json:"tag_id" gorm:"not null"`
}
// TaskComment 任务评论表
type TaskComment struct {
ID uint `json:"id" gorm:"primaryKey"`
TaskID uint `json:"task_id" gorm:"not null"`
UserID uint `json:"user_id" gorm:"not null"`
Content string `json:"content" gorm:"type:text;not null"`
CreatedAt time.Time `json:"created_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}
// TaskAttachment 任务附件表
type TaskAttachment struct {
ID uint `json:"id" gorm:"primaryKey"`
TaskID uint `json:"task_id" gorm:"not null"`
FileName string `json:"file_name" gorm:"size:255;not null"`
FilePath string `json:"file_path" gorm:"size:500;not null"`
FileSize int64 `json:"file_size"`
FileType string `json:"file_type" gorm:"size:50"`
UploadedBy uint `json:"uploaded_by" gorm:"not null"`
CreatedAt time.Time `json:"created_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}

62
backend/pkg/auth/jwt.go

@ -0,0 +1,62 @@
package auth
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
var jwtSecret = []byte("task-track-jwt-secret-key")
type Claims struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
OrganizationID uint `json:"organization_id"`
jwt.RegisteredClaims
}
// GenerateToken 生成JWT token
func GenerateToken(userID uint, username string, organizationID uint) (string, error) {
claims := Claims{
UserID: userID,
Username: username,
OrganizationID: organizationID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
// ValidateToken 验证JWT token
func ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, errors.New("invalid token")
}
// RefreshToken 刷新JWT token
func RefreshToken(tokenString string) (string, error) {
claims, err := ValidateToken(tokenString)
if err != nil {
return "", err
}
// 生成新的token
return GenerateToken(claims.UserID, claims.Username, claims.OrganizationID)
}

161
backend/pkg/database/database.go

@ -0,0 +1,161 @@
package database
import (
"database/sql"
"fmt"
"strings"
"task-track-backend/internal/config"
"task-track-backend/model"
"github.com/go-sql-driver/mysql"
gorm_mysql "gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func Init(cfg config.DatabaseConfig) (*gorm.DB, error) {
// 首先尝试连接到数据库
db, err := connectToDatabase(cfg)
if err != nil {
// 如果连接失败,检查是否是因为数据库不存在
if isDBNotExistError(err) {
// 创建数据库
err = createDatabase(cfg)
if err != nil {
return nil, fmt.Errorf("failed to create database: %w", err)
}
// 重新尝试连接
db, err = connectToDatabase(cfg)
if err != nil {
return nil, fmt.Errorf("failed to connect to database after creation: %w", err)
}
} else {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
}
// 自动迁移数据库表
err = autoMigrateModels(db)
if err != nil {
return nil, fmt.Errorf("failed to auto migrate models: %w", err)
}
return db, nil
}
func connectToDatabase(cfg config.DatabaseConfig) (*gorm.DB, error) {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local",
cfg.Username,
cfg.Password,
cfg.Host,
cfg.Port,
cfg.Database,
cfg.Charset,
)
// 配置 GORM 连接选项
gormConfig := &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
// 启用错误翻译,将数据库特定错误转换为 GORM 通用错误
TranslateError: true,
}
// 配置 MySQL 驱动选项
mysqlConfig := gorm_mysql.Config{
DSN: dsn,
DefaultStringSize: 256,
DisableDatetimePrecision: true,
DontSupportRenameIndex: true,
DontSupportRenameColumn: true,
SkipInitializeWithVersion: false,
}
db, err := gorm.Open(gorm_mysql.New(mysqlConfig), gormConfig)
if err != nil {
return nil, err
}
// 配置连接池
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
// 设置连接池参数
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
return db, nil
}
func createDatabase(cfg config.DatabaseConfig) error {
// 创建不包含数据库名的 DSN,用于连接 MySQL 服务器
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/?charset=%s&parseTime=True&loc=Local",
cfg.Username,
cfg.Password,
cfg.Host,
cfg.Port,
cfg.Charset,
)
// 直接使用 database/sql 连接
db, err := sql.Open("mysql", dsn)
if err != nil {
return fmt.Errorf("failed to connect to MySQL server: %w", err)
}
defer db.Close()
// 测试连接
if err := db.Ping(); err != nil {
return fmt.Errorf("failed to ping MySQL server: %w", err)
}
// 创建数据库
createDBSQL := fmt.Sprintf("CREATE DATABASE IF NOT EXISTS `%s` CHARACTER SET %s COLLATE %s_general_ci",
cfg.Database, cfg.Charset, cfg.Charset)
_, err = db.Exec(createDBSQL)
if err != nil {
return fmt.Errorf("failed to create database %s: %w", cfg.Database, err)
}
return nil
}
func autoMigrateModels(db *gorm.DB) error {
// 使用 GORM 的 AutoMigrate 自动迁移所有模型
// 设置表选项(MySQL 存储引擎)
err := db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(
&model.User{},
&model.Organization{},
&model.UserOrganization{},
&model.Task{},
&model.TaskTag{},
&model.TaskTagRelation{},
&model.TaskComment{},
&model.TaskAttachment{},
)
if err != nil {
return fmt.Errorf("auto migrate failed: %w", err)
}
return nil
}
func isDBNotExistError(err error) bool {
if err == nil {
return false
}
// 检查是否是 MySQL 错误
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
// MySQL 错误码 1049: Unknown database
return mysqlErr.Number == 1049
}
// 检查错误消息中是否包含数据库不存在的关键字
errMsg := strings.ToLower(err.Error())
return strings.Contains(errMsg, "unknown database") ||
strings.Contains(errMsg, "database doesn't exist")
}

57
backend/pkg/logger/logger.go

@ -0,0 +1,57 @@
package logger
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var Logger *zap.Logger
func Init(level string) {
var config zap.Config
if level == "debug" {
config = zap.NewDevelopmentConfig()
} else {
config = zap.NewProductionConfig()
}
// 设置日志级别
switch level {
case "debug":
config.Level = zap.NewAtomicLevelAt(zapcore.DebugLevel)
case "info":
config.Level = zap.NewAtomicLevelAt(zapcore.InfoLevel)
case "warn":
config.Level = zap.NewAtomicLevelAt(zapcore.WarnLevel)
case "error":
config.Level = zap.NewAtomicLevelAt(zapcore.ErrorLevel)
default:
config.Level = zap.NewAtomicLevelAt(zapcore.InfoLevel)
}
var err error
Logger, err = config.Build()
if err != nil {
panic(err)
}
}
func Info(msg string, fields ...zap.Field) {
Logger.Info(msg, fields...)
}
func Debug(msg string, fields ...zap.Field) {
Logger.Debug(msg, fields...)
}
func Warn(msg string, fields ...zap.Field) {
Logger.Warn(msg, fields...)
}
func Error(msg string, fields ...zap.Field) {
Logger.Error(msg, fields...)
}
func Fatal(msg string, fields ...zap.Field) {
Logger.Fatal(msg, fields...)
}

17
backend/test-auth.bat

@ -0,0 +1,17 @@
@echo off
REM Run all Go tests in auth handler (from backend directory)
setlocal
set CONFIG_SRC=config.yaml
set CONFIG_DST=internal\handler\auth\config.yaml
REM 复制 config.yaml
copy %CONFIG_SRC% %CONFIG_DST% >nul
REM 运行测试
echo Running all tests in auth handler...
go test ./internal/handler/auth -v
REM 删除副本
if exist %CONFIG_DST% del %CONFIG_DST%
endlocal
pause

BIN
backend/uploads/1751943286377256500_sample_main.xlsx

Binary file not shown.

BIN
backend/uploads/1751943733448215600_sample_main.xlsx

Binary file not shown.

BIN
backend/uploads/1751944539004522700_sample_main.xlsx

Binary file not shown.

185
database/init.sql

@ -0,0 +1,185 @@
-- 任务跟踪系统数据库初始化脚本
-- 创建数据库
CREATE DATABASE IF NOT EXISTS task_track CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE task_track;
-- 用户表
CREATE TABLE IF NOT EXISTS users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
email VARCHAR(100) NOT NULL UNIQUE COMMENT '邮箱',
password VARCHAR(255) NOT NULL COMMENT '密码',
real_name VARCHAR(50) COMMENT '真实姓名',
phone VARCHAR(20) COMMENT '手机号',
avatar VARCHAR(255) COMMENT '头像',
status TINYINT DEFAULT 1 COMMENT '状态 1:启用 0:禁用',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted_at TIMESTAMP NULL COMMENT '删除时间',
INDEX idx_username (username),
INDEX idx_email (email),
INDEX idx_status (status),
INDEX idx_deleted_at (deleted_at)
) COMMENT '用户表';
-- 机构表
CREATE TABLE IF NOT EXISTS organizations (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL COMMENT '机构名称',
code VARCHAR(50) NOT NULL UNIQUE COMMENT '机构代码',
parent_id INT UNSIGNED DEFAULT 0 COMMENT '父级机构ID',
level TINYINT DEFAULT 1 COMMENT '机构层级',
sort INT DEFAULT 0 COMMENT '排序',
status TINYINT DEFAULT 1 COMMENT '状态 1:启用 0:禁用',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted_at TIMESTAMP NULL COMMENT '删除时间',
INDEX idx_code (code),
INDEX idx_parent_id (parent_id),
INDEX idx_level (level),
INDEX idx_status (status),
INDEX idx_deleted_at (deleted_at)
) COMMENT '机构表';
-- 用户机构关系表
CREATE TABLE IF NOT EXISTS user_organizations (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL COMMENT '用户ID',
organization_id INT UNSIGNED NOT NULL COMMENT '机构ID',
role VARCHAR(20) DEFAULT 'member' COMMENT '角色 admin:管理员 member:成员',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
UNIQUE KEY uk_user_org (user_id, organization_id),
INDEX idx_user_id (user_id),
INDEX idx_organization_id (organization_id)
) COMMENT '用户机构关系表';
-- 任务表
CREATE TABLE IF NOT EXISTS tasks (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL COMMENT '任务标题',
description TEXT COMMENT '任务描述',
type VARCHAR(50) COMMENT '任务类型',
priority VARCHAR(20) DEFAULT 'medium' COMMENT '优先级 urgent:紧急 high:高 medium:中 low:低',
status VARCHAR(20) DEFAULT 'pending' COMMENT '状态 pending:待处理 in_progress:进行中 completed:已完成 cancelled:已取消',
creator_id INT UNSIGNED NOT NULL COMMENT '创建者ID',
assignee_id INT UNSIGNED COMMENT '执行者ID',
organization_id INT UNSIGNED NOT NULL COMMENT '所属机构ID',
start_time TIMESTAMP NULL COMMENT '开始时间',
end_time TIMESTAMP NULL COMMENT '截止时间',
completed_at TIMESTAMP NULL COMMENT '完成时间',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
deleted_at TIMESTAMP NULL COMMENT '删除时间',
INDEX idx_title (title),
INDEX idx_status (status),
INDEX idx_priority (priority),
INDEX idx_creator_id (creator_id),
INDEX idx_assignee_id (assignee_id),
INDEX idx_organization_id (organization_id),
INDEX idx_end_time (end_time),
INDEX idx_deleted_at (deleted_at)
) COMMENT '任务表';
-- 任务标签表
CREATE TABLE IF NOT EXISTS task_tags (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL COMMENT '标签名称',
color VARCHAR(20) DEFAULT '#409EFF' COMMENT '标签颜色',
organization_id INT UNSIGNED NOT NULL COMMENT '所属机构ID',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
deleted_at TIMESTAMP NULL COMMENT '删除时间',
INDEX idx_name (name),
INDEX idx_organization_id (organization_id),
INDEX idx_deleted_at (deleted_at)
) COMMENT '任务标签表';
-- 任务标签关系表
CREATE TABLE IF NOT EXISTS task_tag_relations (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
task_id INT UNSIGNED NOT NULL COMMENT '任务ID',
tag_id INT UNSIGNED NOT NULL COMMENT '标签ID',
UNIQUE KEY uk_task_tag (task_id, tag_id),
INDEX idx_task_id (task_id),
INDEX idx_tag_id (tag_id)
) COMMENT '任务标签关系表';
-- 任务评论表
CREATE TABLE IF NOT EXISTS task_comments (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
task_id INT UNSIGNED NOT NULL COMMENT '任务ID',
user_id INT UNSIGNED NOT NULL COMMENT '用户ID',
content TEXT NOT NULL COMMENT '评论内容',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
deleted_at TIMESTAMP NULL COMMENT '删除时间',
INDEX idx_task_id (task_id),
INDEX idx_user_id (user_id),
INDEX idx_deleted_at (deleted_at)
) COMMENT '任务评论表';
-- 任务附件表
CREATE TABLE IF NOT EXISTS task_attachments (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
task_id INT UNSIGNED NOT NULL COMMENT '任务ID',
file_name VARCHAR(255) NOT NULL COMMENT '文件名',
file_path VARCHAR(500) NOT NULL COMMENT '文件路径',
file_size BIGINT COMMENT '文件大小',
file_type VARCHAR(50) COMMENT '文件类型',
uploaded_by INT UNSIGNED NOT NULL COMMENT '上传者ID',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
deleted_at TIMESTAMP NULL COMMENT '删除时间',
INDEX idx_task_id (task_id),
INDEX idx_uploaded_by (uploaded_by),
INDEX idx_deleted_at (deleted_at)
) COMMENT '任务附件表';
-- 插入初始数据
-- 插入默认机构
INSERT INTO organizations (name, code, parent_id, level, sort, status) VALUES
('总公司', 'HQ', 0, 1, 0, 1),
('技术部', 'TECH', 1, 2, 1, 1),
('市场部', 'MKT', 1, 2, 2, 1),
('人事部', 'HR', 1, 2, 3, 1);
-- 插入默认用户(密码为 123456,已加密)
INSERT INTO users (username, email, password, real_name, status) VALUES
('admin', 'admin@example.com', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', '系统管理员', 1),
('zhangsan', 'zhangsan@example.com', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', '张三', 1),
('lisi', 'lisi@example.com', '$2a$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', '李四', 1);
-- 插入用户机构关系
INSERT INTO user_organizations (user_id, organization_id, role) VALUES
(1, 1, 'admin'),
(2, 2, 'member'),
(3, 3, 'member');
-- 插入默认任务标签
INSERT INTO task_tags (name, color, organization_id) VALUES
('紧急', '#F56C6C', 1),
('重要', '#E6A23C', 1),
('bug修复', '#909399', 1),
('新功能', '#67C23A', 1),
('优化', '#409EFF', 1);
-- 插入示例任务
INSERT INTO tasks (title, description, type, priority, status, creator_id, assignee_id, organization_id, start_time, end_time) VALUES
('完成用户认证模块', '实现用户登录、注册、权限验证功能', '开发', 'high', 'in_progress', 1, 2, 2, '2025-07-08 09:00:00', '2025-07-10 18:00:00'),
('设计任务管理界面', '设计任务列表、任务详情、任务编辑等界面', '设计', 'medium', 'pending', 1, 3, 3, '2025-07-09 09:00:00', '2025-07-12 18:00:00'),
('数据库优化', '优化查询性能,添加必要的索引', '优化', 'low', 'completed', 1, 2, 2, '2025-07-05 09:00:00', '2025-07-08 18:00:00');
-- 插入任务标签关系
INSERT INTO task_tag_relations (task_id, tag_id) VALUES
(1, 2), -- 任务1 标记为重要
(1, 4), -- 任务1 标记为新功能
(2, 4), -- 任务2 标记为新功能
(3, 5); -- 任务3 标记为优化
-- 插入示例评论
INSERT INTO task_comments (task_id, user_id, content) VALUES
(1, 2, '已开始开发,预计明天完成登录功能'),
(1, 1, '注意安全性和用户体验'),
(2, 3, '界面设计稿已完成初版,请查看');
COMMIT;

88
dev.bat

@ -0,0 +1,88 @@
@echo off
REM ===============================================
REM Task Track - Development Mode with Hot Reload
REM ===============================================
setlocal enabledelayedexpansion
echo [INFO] Starting Task Track Development Environment...
echo.
REM Check if Air is installed
set "AIR_PATH=%USERPROFILE%\go\bin\air.exe"
if not exist "%AIR_PATH%" (
echo [ERROR] Air not installed. Please install Air first:
echo go install github.com/air-verse/air@latest
echo.
pause
exit /b 1
)
echo [INFO] Air is installed. Starting hot reload mode...
REM Create tmp directory
if not exist "backend\tmp" (
mkdir "backend\tmp"
echo [INFO] Created tmp directory
)
REM Clean up existing backend processes
echo [INFO] Checking and cleaning existing backend processes...
tasklist /fi "imagename eq task-track-backend.exe" /fo csv | findstr /i "task-track-backend.exe" >nul
if %errorlevel% equ 0 (
echo [INFO] Found running backend process, terminating...
taskkill /f /im task-track-backend.exe >nul 2>&1
timeout /t 2 /nobreak >nul
)
REM Check port usage
echo [INFO] Checking port 8080 usage...
for /f "tokens=5" %%a in ('netstat -ano ^| findstr ":8080"') do (
set "pid=%%a"
if defined pid (
echo [WARNING] Port 8080 is occupied by process !pid!, terminating...
taskkill /f /pid !pid! >nul 2>&1
timeout /t 2 /nobreak >nul
)
)
REM Install frontend dependencies if needed
if not exist "frontend\node_modules" (
echo [INFO] Installing frontend dependencies...
cd frontend
npm install
cd ..
echo [INFO] Frontend dependencies installed
)
REM Start frontend dev server
echo [INFO] Starting frontend dev server...
start "Frontend Dev Server" /d "frontend" cmd /c "npm run dev"
echo [INFO] Waiting for frontend server to start...
timeout /t 5 /nobreak >nul
echo.
echo ================================
echo Development Environment Started!
echo ================================
echo.
echo 📱 Frontend: http://localhost:5173
echo 🔗 Backend: http://localhost:8080 (Hot Reload)
echo.
echo 🔥 Hot Reload Features:
echo - Backend: Code changes auto-restart server
echo - Frontend: Code changes auto-refresh browser
echo.
echo [INFO] Press Ctrl+C to stop the dev server
echo.
cd backend
"%AIR_PATH%"
echo.
echo [INFO] Backend dev server stopped
echo [INFO] Stopping frontend dev server...
taskkill /f /im node.exe >nul 2>&1
echo [INFO] All dev servers stopped
cd ..
pause

61
dev.sh

@ -0,0 +1,61 @@
#!/bin/bash
# ===============================================
# Task Track - 开发模式启动脚本 (使用 Air 热加载)
# ===============================================
echo "[INFO] 启动 Task Track 开发环境..."
echo
# 检查是否安装了 Air
if ! command -v air &> /dev/null; then
echo "[ERROR] Air 未安装,请先安装 Air:"
echo "go install github.com/air-verse/air@latest"
echo
exit 1
fi
# 创建 tmp 目录
if [ ! -d "backend/tmp" ]; then
mkdir -p "backend/tmp"
echo "[INFO] 创建 tmp 目录"
fi
# 检查并清理已有的后端进程
echo "[INFO] 检查并清理已有的后端进程..."
if pgrep -f "task-track-backend" > /dev/null; then
echo "[INFO] 发现已运行的后端进程,正在终止..."
pkill -f "task-track-backend"
sleep 2
fi
# 检查端口占用情况
echo "[INFO] 检查端口占用情况..."
if lsof -i :8080 > /dev/null 2>&1; then
echo "[WARNING] 端口 8080 被占用,正在终止占用进程..."
lsof -ti :8080 | xargs kill -9 2>/dev/null
sleep 2
fi
# 启动前端开发服务器 (如果需要)
echo "[INFO] 检查前端开发服务器..."
if ! pgrep -f "npm.*dev" > /dev/null; then
echo "[INFO] 启动前端开发服务器..."
cd frontend
npm run dev > /dev/null 2>&1 &
cd ..
echo "[INFO] 等待前端服务器启动..."
sleep 5
fi
# 启动后端 (使用 Air 热加载)
echo "[INFO] 启动后端热加载开发服务器..."
echo "[INFO] 使用 Air 进行热加载,代码变更将自动重启服务器"
echo "[INFO] 按 Ctrl+C 停止开发服务器"
echo
cd backend
air
echo
echo "[INFO] 后端开发服务器已停止"
cd ..

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save