docs: 新增后端架构规范文档 + 拆分集成规范
- 新增 backend-module-architecture.md(10 章:模块分层/接口注入/Wire/日志/错误处理/批量查询/系统消息/WS 推送/Store 封装) - 从 frontend-backend-integration.md 拆分(621→360 行),符合 ≤500 行规范 - project-context.mdc 新增规则 #10 引用后端架构规范 Made-with: Cursor
This commit is contained in:
@@ -44,10 +44,11 @@ Phase 2c 待实现模块:group(群聊管理)、file(文件上传/MinIO
|
|||||||
7. **JWT 策略**:有状态 JWT,Token 存 Redis(按 clientType 隔离:`echo:auth:token:{frontend|admin}:{user_id}`),前后台互不影响
|
7. **JWT 策略**:有状态 JWT,Token 存 Redis(按 clientType 隔离:`echo:auth:token:{frontend|admin}:{user_id}`),前后台互不影响
|
||||||
8. **角色等级体系**:`auth_roles.level` 字段(值越小权限越高:1=超管, 10=管理员, 100=普通用户),所有管理操作强制执行层级权限校验
|
8. **角色等级体系**:`auth_roles.level` 字段(值越小权限越高:1=超管, 10=管理员, 100=普通用户),所有管理操作强制执行层级权限校验
|
||||||
9. **代码注释**:所有公开函数、组件、Store 必须有详细注释
|
9. **代码注释**:所有公开函数、组件、Store 必须有详细注释
|
||||||
10. **文档自动同步(强制)**:每个 Task 或功能开发完成后,必须自动执行文档同步,详见下方「文档自动同步规则」
|
10. **后端架构规范**:详见 `docs/conventions/backend-module-architecture.md`(模块分层/接口注入/日志/错误处理/批量查询/系统消息/Store 封装等)
|
||||||
11. **验证方式**:使用 Playwright MCP 进行页面自动化验证
|
11. **文档自动同步(强制)**:每个 Task 或功能开发完成后,必须自动执行文档同步,详见下方「文档自动同步规则」
|
||||||
12. **代码审查**:每个 Task 完成后,使用 `code-reviewer` 子代理进行结构化审查,确保代码质量和计划一致性
|
12. **验证方式**:使用 Playwright MCP 进行页面自动化验证
|
||||||
13. **完成验证**:使用 `verification-before-completion` 技能,在声称完成前运行验证命令并确认输出
|
13. **代码审查**:每个 Task 完成后,使用 `code-reviewer` 子代理进行结构化审查,确保代码质量和计划一致性
|
||||||
|
14. **完成验证**:使用 `verification-before-completion` 技能,在声称完成前运行验证命令并确认输出
|
||||||
|
|
||||||
## 文档自动同步规则(强制执行)
|
## 文档自动同步规则(强制执行)
|
||||||
|
|
||||||
|
|||||||
299
docs/conventions/backend-module-architecture.md
Normal file
299
docs/conventions/backend-module-architecture.md
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
# Go 后端模块架构规范
|
||||||
|
|
||||||
|
> **适用范围**:EchoChat Go 后端(`backend/go-service/`)
|
||||||
|
> **创建日期**:2026-03-04
|
||||||
|
> **最后更新**:2026-03-04(Phase 2c 设计阶段整理)
|
||||||
|
> **关联文档**:`docs/conventions/frontend-backend-integration.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、模块分层架构
|
||||||
|
|
||||||
|
每个业务模块采用四层架构,目录结构如下:
|
||||||
|
|
||||||
|
```
|
||||||
|
app/{module_name}/
|
||||||
|
├── controller/
|
||||||
|
│ └── {module}_controller.go # HTTP 请求处理(参数绑定/校验 → 调用 Service → 响应)
|
||||||
|
├── service/
|
||||||
|
│ └── {module}_service.go # 业务逻辑(事务协调、权限校验、多 DAO 编排)
|
||||||
|
├── dao/
|
||||||
|
│ └── {module}_dao.go # 数据访问(GORM 操作,不含业务逻辑)
|
||||||
|
├── model/
|
||||||
|
│ └── {module}.go # 数据模型(GORM 结构体,对应数据库表)
|
||||||
|
├── handler/
|
||||||
|
│ └── {module}_handler.go # [可选] WS 事件处理器
|
||||||
|
├── router.go # 路由注册
|
||||||
|
└── provider.go # Wire ProviderSet
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、层间调用规则
|
||||||
|
|
||||||
|
```
|
||||||
|
Controller → Service → DAO → 数据库
|
||||||
|
↓
|
||||||
|
其他模块接口(通过注入的 interface)
|
||||||
|
↓
|
||||||
|
PubSub / Redis
|
||||||
|
```
|
||||||
|
|
||||||
|
- Controller **只处理 HTTP 关注点**:参数绑定、调用 Service、返回响应
|
||||||
|
- Service **只处理业务逻辑**:权限校验、业务规则、事务编排、推送通知
|
||||||
|
- DAO **只处理数据存取**:GORM 查询、批量操作、不含业务逻辑
|
||||||
|
- **禁止跨层调用**:Controller 不能直接调用 DAO,DAO 不能调用 Service
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、跨模块接口注入模式
|
||||||
|
|
||||||
|
模块间通信通过 **interface injection** 实现,禁止直接 import 其他模块包。
|
||||||
|
|
||||||
|
### 3.1 标准流程
|
||||||
|
|
||||||
|
```
|
||||||
|
步骤1: 在消费方 Service 中定义 interface(如 im/service → GroupMemberChecker)
|
||||||
|
步骤2: 在提供方 DAO 中实现该接口(如 group/dao.GroupDAO)
|
||||||
|
步骤3: 在 app/provider/wire.go 中用 wire.Bind 绑定接口和实现
|
||||||
|
步骤4: 重新生成 wire_gen.go
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 接口命名约定
|
||||||
|
|
||||||
|
| 接口类型 | 命名模式 | 示例 |
|
||||||
|
|---------|---------|------|
|
||||||
|
| 数据查询 | `{Entity}{Action}er` | `FriendIDsGetter`, `GroupInfoGetter` |
|
||||||
|
| 状态检查 | `{Entity}{State}Checker` | `GroupMemberChecker`, `OnlineChecker` |
|
||||||
|
| 操作执行 | `{Entity}{Action}er` | `OfflineMessagePusher` |
|
||||||
|
|
||||||
|
### 3.3 已有接口注入清单
|
||||||
|
|
||||||
|
| 接口 | 定义方 | 实现方 | 用途 |
|
||||||
|
|------|--------|--------|------|
|
||||||
|
| FriendIDsGetter | ws/handler | contact/dao | 获取用户好友 ID 列表 |
|
||||||
|
| FriendChecker | im/service | contact/dao | 检查是否为好友 |
|
||||||
|
| UserInfoGetter | im/service | auth/dao | 获取用户信息 |
|
||||||
|
| OnlineChecker | contact/service | ws/service | 检查在线状态 |
|
||||||
|
| OfflineMessagePusher | im/service | ws/handler | 离线消息推送 |
|
||||||
|
| GroupMemberChecker | im/service | group/dao | 检查群成员身份(Phase 2c) |
|
||||||
|
| GroupInfoGetter | im/service | group/dao | 获取群信息(Phase 2c) |
|
||||||
|
| GroupRoleChecker | im/service | group/dao | 检查用户群角色(Phase 2c) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、Wire 依赖注入规范
|
||||||
|
|
||||||
|
- 每个模块在 `provider.go` 中导出 `ProviderSet`(`wire.NewSet(...)`)
|
||||||
|
- 接口绑定统一在 `app/provider/wire.go` 中声明
|
||||||
|
- 修改 wire.go 后必须重新运行 `wire gen ./app/provider/` 生成 wire_gen.go
|
||||||
|
- Wire 有过手动 patch 历史(Phase 2b),修改后需检查 wire_gen.go 一致性
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、日志记录标准
|
||||||
|
|
||||||
|
所有 DAO 和 Service 的公开方法必须记录入口和出口日志:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (d *SomeDAO) SomeMethod(ctx context.Context, param int64) (result *Model, err error) {
|
||||||
|
funcName := "dao.some_dao.SomeMethod"
|
||||||
|
|
||||||
|
logs.LogFunctionEntry(ctx, funcName, map[string]interface{}{
|
||||||
|
"param": param,
|
||||||
|
})
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
logs.LogFunctionExit(ctx, funcName, result, err)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 业务逻辑
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**funcName 命名规则:** `{层级}.{模块}_{文件}.{方法名}`
|
||||||
|
- DAO 层:`dao.group_dao.CreateGroup`
|
||||||
|
- Service 层:`service.group_service.CreateGroup`
|
||||||
|
- Controller 层:`controller.group_controller.CreateGroup`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、Controller 错误处理标准
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (ctrl *Controller) HandleAction(c *gin.Context) {
|
||||||
|
funcName := "controller.module.HandleAction"
|
||||||
|
|
||||||
|
// 参数绑定
|
||||||
|
var req dto.SomeRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用 Service
|
||||||
|
result, err := ctrl.service.DoAction(c.Request.Context(), &req)
|
||||||
|
if err != nil {
|
||||||
|
ctrl.handleError(c, funcName, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.ResponseOK(c, "操作成功", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleError 必须覆盖所有已知业务错误
|
||||||
|
func (ctrl *Controller) handleError(c *gin.Context, funcName string, err error) {
|
||||||
|
switch err {
|
||||||
|
case service.ErrNotFound:
|
||||||
|
utils.ResponseNotFound(c, err.Error())
|
||||||
|
case service.ErrPermission:
|
||||||
|
utils.ResponseForbidden(c, err.Error())
|
||||||
|
// ... 覆盖所有已知错误
|
||||||
|
default:
|
||||||
|
logs.Error(c.Request.Context(), funcName, "操作失败", zap.Error(err))
|
||||||
|
utils.ResponseError(c, "操作失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键要求:**
|
||||||
|
- `handleError` 必须覆盖所有已知业务错误,不能用 `default` 笼统处理
|
||||||
|
- 错误 message 使用中文,面向用户
|
||||||
|
- 未知错误必须记录日志
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、批量查询优化
|
||||||
|
|
||||||
|
获取关联信息时,**必须使用批量查询 + Map 映射,严禁 N+1 查询**:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ✅ 正确:批量查询
|
||||||
|
userIDs := extractUserIDs(members)
|
||||||
|
users, _ := userDAO.GetByIDs(ctx, userIDs)
|
||||||
|
userMap := make(map[int64]*User)
|
||||||
|
for _, u := range users {
|
||||||
|
userMap[u.ID] = u
|
||||||
|
}
|
||||||
|
for _, m := range members {
|
||||||
|
m.Nickname = userMap[m.UserID].Nickname
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ 错误:N+1 查询
|
||||||
|
for _, m := range members {
|
||||||
|
user, _ := userDAO.GetByID(ctx, m.UserID)
|
||||||
|
m.Nickname = user.Nickname
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、系统消息规范(Phase 2c+)
|
||||||
|
|
||||||
|
### 8.1 系统消息定义
|
||||||
|
|
||||||
|
系统消息是由服务端自动生成的提示类消息,写入 `im_messages` 表,type=10(`MessageTypeSystem`)。
|
||||||
|
|
||||||
|
### 8.2 内容格式
|
||||||
|
|
||||||
|
系统消息的 `content` 字段使用**纯文本格式**,不使用 JSON 结构。
|
||||||
|
|
||||||
|
### 8.3 前端渲染
|
||||||
|
|
||||||
|
系统消息在聊天页面中**居中显示,灰色小字体,无头像,无气泡**。
|
||||||
|
|
||||||
|
### 8.4 sender_id
|
||||||
|
|
||||||
|
系统消息的 `sender_id` 设为 0(表示系统),前端根据 sender_id=0 和 type=10 判断为系统消息。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、WS 事件推送模式
|
||||||
|
|
||||||
|
### 9.1 S→C 推送(Service 层触发)
|
||||||
|
|
||||||
|
```go
|
||||||
|
pubsub.PublishToUser(ctx, targetUserID, &ws.Message{
|
||||||
|
Event: "group.member.join",
|
||||||
|
Data: map[string]interface{}{...},
|
||||||
|
})
|
||||||
|
|
||||||
|
pubsub.PublishToUsers(ctx, memberIDs, &ws.Message{
|
||||||
|
Event: "im.message.new",
|
||||||
|
Data: messageDTO,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 C→S 事件处理(Hub 事件路由表注册)
|
||||||
|
|
||||||
|
```go
|
||||||
|
hub.RegisterEvent("im.message.send", handler.HandleSendMessage)
|
||||||
|
hub.RegisterEvent("im.message.read", handler.HandleReadMessage)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、前端 Store 与 API 封装规范
|
||||||
|
|
||||||
|
### 10.1 API 封装标准
|
||||||
|
|
||||||
|
文件位置:`frontend/src/api/{module}.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 获取群详情
|
||||||
|
export function getGroupDetail(groupId) {
|
||||||
|
return request.get(`/groups/${groupId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建群聊
|
||||||
|
export function createGroup(data) {
|
||||||
|
return request.post('/groups', data)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**命名规则:**
|
||||||
|
- GET 请求:`get{Entity}` / `get{Entity}List`
|
||||||
|
- POST 请求:`create{Entity}` / `{action}{Entity}`
|
||||||
|
- PUT 请求:`update{Entity}` / `set{Entity}{Field}`
|
||||||
|
- DELETE 请求:`delete{Entity}` / `remove{Entity}`
|
||||||
|
|
||||||
|
### 10.2 Pinia Store 标准结构
|
||||||
|
|
||||||
|
文件位置:`frontend/src/store/{module}.js`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useGroupStore = defineStore('group', {
|
||||||
|
state: () => ({
|
||||||
|
conversations: [], // 群会话列表
|
||||||
|
currentGroup: null, // 当前群详情
|
||||||
|
messages: {}, // {conversationId: Message[]}
|
||||||
|
members: {}, // {groupId: Member[]}
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
unreadTotal: (state) => { ... },
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
// 初始化 WS 事件监听(在 App.vue onLaunch 中调用)
|
||||||
|
initWsListeners() { ... },
|
||||||
|
|
||||||
|
// 加载群会话列表
|
||||||
|
async loadConversations() { ... },
|
||||||
|
|
||||||
|
// 发送群消息
|
||||||
|
async sendMessage(conversationId, content, atUserIds) { ... },
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Store 设计原则:**
|
||||||
|
1. **单一职责**:每个模块一个 Store,不混合不同模块数据
|
||||||
|
2. **WS 监听统一初始化**:所有 Store 的 WS 事件监听在 `App.vue` 的 `_initGlobalWS` 中统一调用
|
||||||
|
3. **消息缓存**:按 conversationId 键值对缓存,避免重复请求
|
||||||
|
4. **乐观更新**:发送消息时先本地插入,再等待服务端 ack 确认
|
||||||
@@ -315,20 +315,28 @@ JWT Token 的 Claims 中包含 `client_type` 字段,用于:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. WebSocket 事件联动规范
|
## 8. Go 后端架构与编码规范
|
||||||
|
|
||||||
### 8.1 适用范围
|
> 详细规范已拆分为独立文档,见 `docs/conventions/backend-module-architecture.md`
|
||||||
|
>
|
||||||
|
> 包含:模块分层架构、层间调用规则、跨模块接口注入模式、Wire 依赖注入、日志标准、Controller 错误处理、批量查询优化、系统消息规范、WS 事件推送模式、前端 Store/API 封装规范
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. WebSocket 事件联动规范
|
||||||
|
|
||||||
|
### 9.1 适用范围
|
||||||
|
|
||||||
仅前台用户端(frontend)使用 WebSocket 实时通讯,管理端(admin)使用 REST 轮询。
|
仅前台用户端(frontend)使用 WebSocket 实时通讯,管理端(admin)使用 REST 轮询。
|
||||||
|
|
||||||
### 8.2 连接管理
|
### 9.2 连接管理
|
||||||
|
|
||||||
- WebSocket 地址:`ws(s)://host/ws?token=xxx`
|
- WebSocket 地址:`ws(s)://host/ws?token=xxx`
|
||||||
- 认证方式:URL Query 参数传递 JWT Token(前台 frontend Token)
|
- 认证方式:URL Query 参数传递 JWT Token(前台 frontend Token)
|
||||||
- 心跳间隔:30 秒 ping/pong
|
- 心跳间隔:30 秒 ping/pong
|
||||||
- 断线重连:指数退避(1s → 2s → 4s → 8s → 30s max)
|
- 断线重连:指数退避(1s → 2s → 4s → 8s → 30s max)
|
||||||
|
|
||||||
### 8.3 事件命名规范
|
### 9.3 事件命名规范
|
||||||
|
|
||||||
格式:`{模块}.{对象}.{动作}`
|
格式:`{模块}.{对象}.{动作}`
|
||||||
|
|
||||||
@@ -340,12 +348,13 @@ JWT Token 的 Claims 中包含 `client_type` 字段,用于:
|
|||||||
| `user.status.online` | 服务端 → 客户端 | 好友上线 |
|
| `user.status.online` | 服务端 → 客户端 | 好友上线 |
|
||||||
| `user.status.offline` | 服务端 → 客户端 | 好友下线 |
|
| `user.status.offline` | 服务端 → 客户端 | 好友下线 |
|
||||||
|
|
||||||
### 8.4 前端事件处理原则
|
### 9.4 前端事件处理原则
|
||||||
|
|
||||||
1. **WebSocket Store 统一管理**:连接状态、事件监听、消息发送由 `store/websocket.js` 管理
|
1. **WebSocket Store 统一管理**:连接状态、事件监听、消息发送由 `store/websocket.js` 管理
|
||||||
2. **业务 Store 订阅事件**:各模块 Store(如 `contact.js`)通过 WebSocket Store 注册事件回调
|
2. **业务 Store 订阅事件**:各模块 Store(如 `contact.js`)通过 WebSocket Store 注册事件回调
|
||||||
3. **避免页面直接操作 WebSocket**:页面组件通过 Store 间接与 WebSocket 交互
|
3. **避免页面直接操作 WebSocket**:页面组件通过 Store 间接与 WebSocket 交互
|
||||||
|
|
||||||
### 8.5 WebSocket 详细协议
|
### 9.5 WebSocket 详细协议
|
||||||
|
|
||||||
> 完整事件协议见 `docs/api/frontend/websocket.md`
|
> 完整事件协议见 `docs/api/frontend/websocket.md`
|
||||||
|
> WS 事件推送模式和代码示例见 `docs/conventions/backend-module-architecture.md` 第九章
|
||||||
|
|||||||
Reference in New Issue
Block a user