Files
ticket-workbench/docs/05-代码审查/代码审查报告.md
绝尘 633ba27710 优化: 文档整理到 docs 目录
参考 u-desk 项目结构,按编号分类:
- 01-设计文档/ (功能设计 + 数据库设计)
- 05-代码审查/ (代码审查报告)
- 07-项目管理/ (工作报告)
- INDEX.md 索引入口
2026-05-14 01:05:25 +08:00

424 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 审查报告: ticket-workbench (Go + Vue)
语言: Go 1.24 / Vue 3 + Arco Design | 后端 19 文件 | 前端 14 文件
---
## 🔴 必须修复 (10)
### ① [detail.vue:7,17,22,117] 前端字段名与后端 API 返回不匹配 — 页面无法正常渲染
**改什么:** 后端返回 `ticketid`/`contactname`/`contactphone`/`createtime` (snake_case),前端用 `ticket.id`/`ticket.contactName`/`ticket.contactPhone`/`ticket.createdAt` (camelCase)。同样 note 用 `note.id`/`note.createdAt` 而后端返回 `noteid`/`createtime`
**怎么改:**
```diff
- <a-descriptions-item label="工单编号">{{ ticket.id }}</a-descriptions-item>
+ <a-descriptions-item label="工单编号">{{ ticket.ticketid }}</a-descriptions-item>
- <a-descriptions-item label="联系人">{{ ticket.contactName }}</a-descriptions-item>
+ <a-descriptions-item label="联系人">{{ ticket.contactname }}</a-descriptions-item>
- <a-descriptions-item label="联系电话">{{ ticket.contactPhone || '-' }}</a-descriptions-item>
+ <a-descriptions-item label="联系电话">{{ ticket.contactphone || '-' }}</a-descriptions-item>
- <a-descriptions-item label="创建时间">{{ ticket.createdAt }}</a-descriptions-item>
+ <a-descriptions-item label="创建时间">{{ ticket.createtime }}</a-descriptions-item>
- <a-timeline-item v-for="note in notes" :key="note.id">
+ <a-timeline-item v-for="note in notes" :key="note.noteid">
- <div class="note-time">{{ note.createdAt }}</div>
+ <div class="note-time">{{ note.createtime }}</div>
```
---
### ② [ticket_service.go:23,29] 工单列表默认过滤 bug — 不传 status/priority 时只查 status=0 且 priority=0 的记录
**改什么:** int16 零值是 0`if status >= 0` 对零值成立。用户不传筛选参数时SQL 自动追加 `WHERE status=0 AND priority=0`,查不到其他数据。
**怎么改:** DTO 改为指针类型service 判 nil
```diff
// dto/ticket.go
type TicketListQuery struct {
- Status int16 `form:"status"`
+ Status *int16 `form:"status"`
Category string `form:"category"`
- Priority int16 `form:"priority"`
+ Priority *int16 `form:"priority"`
Keyword string `form:"keyword"`
Page int `form:"page"`
PageSize int `form:"pageSize"`
}
// ticket_service.go
- if status >= 0 {
- query = query.Where("status = ?", status)
+ if status != nil {
+ query = query.Where("status = ?", *status)
}
- if priority >= 0 {
- query = query.Where("priority = ?", priority)
+ if priority != nil {
+ query = query.Where("priority = ?", *priority)
}
```
handler 传参也要同步调整:
```diff
// ticket_handler.go
- result, err := service.ListTickets(db, query.Status, query.Category, query.Priority, query.Keyword, query.Page, query.PageSize)
+ result, err := service.ListTickets(db, query.Status, query.Category, query.Priority, query.Keyword, query.Page, query.PageSize)
```
---
### ③ [auth_service.go:13] session map 无并发保护 — 并发登录/请求会 panic
**改什么:** `sessions` 是裸 mapgin 并发读写会触发 `fatal error: concurrent map writes`
**怎么改:** 加 sync.RWMutex
```diff
+ import "sync"
- var sessions = make(map[string]*model.TicketUser)
+ var (
+ sessions = make(map[string]*model.TicketUser)
+ sessionMu sync.RWMutex
+ )
// Login
+ sessionMu.Lock()
sessions[sessionID] = &user
+ sessionMu.Unlock()
// Logout
+ sessionMu.Lock()
delete(sessions, sessionID)
+ sessionMu.Unlock()
// GetUserBySession
+ sessionMu.RLock()
+ defer sessionMu.RUnlock()
return sessions[sessionID]
```
---
### ④ [config.yaml:4-11] 数据库密码和 AI API Key 硬编码在配置文件中
**改什么:** `Lake@2019` 和 GLM API Key 明文写在版本控制的 config.yaml 中。
**怎么改:** config.yaml 加入 `.gitignore`,创建 `config.example.yaml` 作为模板。程序支持环境变量覆盖:
```go
// config.go Load 函数中加入
v.AutomaticEnv()
v.SetEnvPrefix("TKT")
```
---
### ⑤ [detail.vue:238-241] 状态更新调错 API — 用了 updateTicket 而非 updateTicketStatus
**改什么:** `handleUpdateStatus` 调用 `updateTicket(id, { status })`,对应 `PUT /tickets/:id`,但该接口不处理 status 字段变更。
**怎么改:**
```diff
async function handleUpdateStatus(status: number) {
try {
- await updateTicket(ticketId, { status })
+ await updateTicketStatus(ticketId, status)
Message.success('状态更新成功')
fetchDetail()
```
---
### ⑥ [detail.vue:214-236] AI 确认调错 API — 用 updateTicket 替代了 confirmAnalysis
**改什么:** `handleConfirmAnalysis` 调用 `updateTicket``aiAnalysis``suggestedRole` 字段,但后端 `UpdateTicket` 不处理这些字段。
**怎么改:**
```diff
async function handleConfirmAnalysis() {
confirming.value = true
try {
- const aiAnalysis = JSON.stringify({
- category: analysisForm.category,
- priority: analysisForm.priority,
- summary: analysisForm.summary,
- suggestedRole: analysisForm.suggestedRole
- })
- await updateTicket(ticketId, {
- category: analysisForm.category,
- priority: analysisForm.priority,
- aiAnalysis,
- suggestedRole: analysisForm.suggestedRole
- })
+ await confirmAnalysis(ticketId, {
+ category: analysisForm.category,
+ priority: analysisForm.priority,
+ summary: analysisForm.summary
+ })
Message.success('确认成功')
fetchDetail()
```
同时需要在 import 中引入 `confirmAnalysis`
```diff
import {
getTicketDetail,
- updateTicket,
analyzeTicket,
+ confirmAnalysis,
getTicketNotes,
createNote
} from '@/api/ticket'
```
---
### ⑦ [auth_handler.go:52] `/auth/me` 返回空 account — middleware 未设置 account 到 context
**改什么:** `c.GetString("account")` 取不到值Auth 中间件只设置了 `userid`/`username`/`role`/`team`,没有 `account`
**怎么改:**
```diff
// middleware/auth.go
c.Set("userid", user.Userid)
c.Set("username", user.Username)
+ c.Set("account", user.Account)
c.Set("role", user.Role)
c.Set("team", user.Team)
```
---
### ⑧ [detail.vue:178,180] `ticketData.assignee` 和 `ticketData.aiAnalysis` 字段不存在
**改什么:** Ticket model 没有 `assignee``aiAnalysis` 字段,访问 undefined 对象导致运行时问题。AI 分析需通过 `getAnalysis(ticketId)` 单独获取。
**怎么改:**
```diff
async function fetchDetail() {
loading.value = true
try {
- const [ticketData, notesData] = await Promise.all([
+ const [ticketData, notesData, analysisData] = await Promise.all([
getTicketDetail(ticketId),
- getTicketNotes(ticketId)
+ getTicketNotes(ticketId),
+ getAnalysis(ticketId).catch(() => [])
])
ticket.value = ticketData
notes.value = notesData
- assignee.value = ticketData.assignee || ''
- if (ticketData.aiAnalysis) {
- try {
- const analysis = JSON.parse(ticketData.aiAnalysis)
+ if (analysisData && analysisData.length > 0) {
+ const latest = analysisData[0]
analysisForm.category = latest.category || ''
analysisForm.priority = latest.priority || 0
analysisForm.summary = latest.summary || ''
- analysisForm.suggestedRole = analysis.suggested_role || analysis.suggestedRole || ''
+ analysisForm.suggestedRole = latest.suggestrole || ''
- } catch {
- analysisForm.summary = ticketData.aiAnalysis
- }
}
```
同时需要在 import 中引入 `getAnalysis`
```diff
import {
getTicketDetail,
updateTicketStatus,
analyzeTicket,
+ getAnalysis,
+ confirmAnalysis,
getTicketNotes,
createNote
} from '@/api/ticket'
```
---
### ⑨ [analysis_service.go:93] AI 返回 JSON 解析过于脆弱 — 没处理 markdown 代码块包裹
**改什么:** AI 模型经常返回 `` ```json {...} ``` `` 格式,直接 `json.Unmarshal` 会失败。
**怎么改:**
```diff
+ import "strings"
content := glmResp.Choices[0].Message.Content
+ // 去除 markdown 代码块包裹
+ content = strings.TrimSpace(content)
+ if strings.HasPrefix(content, "```") {
+ if idx := strings.Index(content, "\n"); idx >= 0 {
+ content = content[idx+1:]
+ }
+ if idx := strings.LastIndex(content, "```"); idx >= 0 {
+ content = content[:idx]
+ }
+ content = strings.TrimSpace(content)
+ }
var result AnalysisResult
- if err := json.Unmarshal([]byte(glmResp.Choices[0].Message.Content), &result); err != nil {
+ if err := json.Unmarshal([]byte(content), &result); err != nil {
```
---
### ⑩ [main.go] 缺少 AutoMigrate — 首次启动表不存在会报错
**改什么:** 没有 `db.AutoMigrate(...)` 调用,数据库表需手动创建。
**怎么改:**
```diff
sqlDB.SetMaxOpenConns(100)
+ if err := db.AutoMigrate(
+ &model.TicketUser{},
+ &model.TicketInfo{},
+ &model.TicketAiAnalysis{},
+ &model.TicketNote{},
+ &model.TicketOperationLog{},
+ ); err != nil {
+ log.Fatalf("Failed to auto migrate: %v", err)
+ }
gin.SetMode(gin.ReleaseMode)
```
---
## 🟡 建议改进 (7)
### ⑪ [auth_service.go:15-18] MD5 哈希密码不安全
**说明:** MD5 可快速碰撞/彩虹表。生产环境应使用 bcrypt。
---
### ⑫ [analysis_service.go:41-54] AI prompt 注入风险
**说明:** 用户提交的工单内容直接拼入 prompt恶意用户可构造内容操控 AI 输出格式/内容。应对用户输入做转义或用 structured prompt 模式。
---
### ⑬ [ticket_service.go:111] UpdateTicket 不检查记录是否存在
**说明:** 传入不存在的 ticketid 时 `Updates` 不报错0 rows affected返回成功。应检查 `RowsAffected` 或先用 `First` 确认存在。
---
### ⑭ [analysis_service.go:64,81] json.Marshal 和 io.ReadAll 错误被忽略
**说明:** `jsonData, _ := json.Marshal(reqBody)` 和 `body, _ := io.ReadAll(resp.Body)` 都丢弃了 error。
---
### ⑮ [detail.vue:89-99] 处理人下拉硬编码 admin/user1/user2
**说明:** 应从后端获取用户列表,或至少与数据库用户数据一致。当前硬编码值 `admin`/`user1`/`user2` 与后端不对应。
---
### ⑯ [create.vue:66] form 提交 `submitterid: 1` 硬编码
**说明:** 后端已从 Auth 中间件获取真实 userid此字段多余且误导。应移除。
---
### ⑰ [store/user.ts:21] login 存 username 用的是 account 而非实际 username
**说明:** `setUsername(account)` 存的是登录账号,不是用户真实姓名。应使用 `res.user.username`。
```diff
async function login(account: string, password: string) {
const res = await loginApi({ account, password })
setToken(res.token)
- setUsername(account)
+ setUsername(res.user.username)
}
```
---
## ⚪ 可选优化 (3)
### ⑱ [ticket_service.go:104] `if priority >= 0` 导致 priority=0 无法区分"未传"和"P0紧急"
**说明:** 与问题②同类UpdateTicket 的 priority 参数也应改为指针类型。
---
### ⑲ [ticket_service.go:58] ticketno 用 UUID 前 8 位有碰撞风险
**说明:** `uuid.New().String()[:8]` 理论上可能重复。可加时间戳或用更长前缀。
---
### ⑳ [analysis_service.go:30] AnalyzeTicket 参数过多
**说明:** 8 个参数的函数签名过长,建议传入结构体。
---
## ✅ 亮点
- 后端分层清晰 (handler/service/model/dto)
- 前端类型定义完整API 层封装规范
- 操作日志设计完善,关键动作都有记录
- CORS 中间件处理周全
---
## 📊 摘要
| # | 等级 | 文件:行 | 修改内容 |
|---|------|---------|----------|
| ① | 🔴 | detail.vue:7,17,22 | 前端字段名与后端不匹配,页面无法渲染 |
| ② | 🔴 | ticket_service.go:23 | 列表默认过滤 bug不传参只查 status=0 |
| ③ | 🔴 | auth_service.go:13 | session map 无锁,并发 panic |
| ④ | 🔴 | config.yaml:4-11 | 密码和 API Key 硬编码 |
| ⑤ | 🔴 | detail.vue:238 | 状态更新调错 API |
| ⑥ | 🔴 | detail.vue:214 | AI 确认调错 API |
| ⑦ | 🔴 | auth_handler.go:52 | /auth/me 返回空 account |
| ⑧ | 🔴 | detail.vue:178 | 访问不存在的 ticket.assignee/aiAnalysis 字段 |
| ⑨ | 🔴 | analysis_service.go:93 | AI JSON 解析未处理 markdown 包裹 |
| ⑩ | 🔴 | main.go | 缺少 AutoMigrate |
| ⑪ | 🟡 | auth_service.go:15 | MD5 密码哈希不安全 |
| ⑫ | 🟡 | analysis_service.go:41 | AI prompt 注入风险 |
| ⑬ | 🟡 | ticket_service.go:111 | 更新不检查记录是否存在 |
| ⑭ | 🟡 | analysis_service.go:64,81 | 错误被忽略 |
| ⑮ | 🟡 | detail.vue:89 | 处理人下拉硬编码 |
| ⑯ | 🟡 | create.vue:66 | submitterid 硬编码 |
| ⑰ | 🟡 | store/user.ts:21 | username 存为 account |
| ⑱ | ⚪ | ticket_service.go:104 | priority 零值歧义 |
| ⑲ | ⚪ | ticket_service.go:58 | ticketno 碰撞风险 |
| ⑳ | ⚪ | analysis_service.go:30 | 函数参数过多 |
**总计:** 🔴10 🟡7 ⚪3
**总体评价:** 前端 detail.vue 与后端严重脱节,核心流程跑不通;后端有并发安全隐患和过滤逻辑 bug
**质量评级: 需重构**