参考 u-desk 项目结构,按编号分类: - 01-设计文档/ (功能设计 + 数据库设计) - 05-代码审查/ (代码审查报告) - 07-项目管理/ (工作报告) - INDEX.md 索引入口
424 lines
13 KiB
Markdown
424 lines
13 KiB
Markdown
# 审查报告: 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` 是裸 map,gin 并发读写会触发 `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
|
||
|
||
**质量评级: 需重构**
|