Files
EchoChat/docs/conventions/backend-module-architecture.md
bujinyuan 1d9f7a7432 docs: 新增后端架构规范文档 + 拆分集成规范
- 新增 backend-module-architecture.md(10 章:模块分层/接口注入/Wire/日志/错误处理/批量查询/系统消息/WS 推送/Store 封装)
- 从 frontend-backend-integration.md 拆分(621→360 行),符合 ≤500 行规范
- project-context.mdc 新增规则 #10 引用后端架构规范

Made-with: Cursor
2026-03-04 10:51:38 +08:00

300 lines
8.6 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.

# Go 后端模块架构规范
> **适用范围**EchoChat Go 后端(`backend/go-service/`
> **创建日期**2026-03-04
> **最后更新**2026-03-04Phase 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 不能直接调用 DAODAO 不能调用 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 确认