- 新增 backend-module-architecture.md(10 章:模块分层/接口注入/Wire/日志/错误处理/批量查询/系统消息/WS 推送/Store 封装) - 从 frontend-backend-integration.md 拆分(621→360 行),符合 ≤500 行规范 - project-context.mdc 新增规则 #10 引用后端架构规范 Made-with: Cursor
8.6 KiB
8.6 KiB
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 的公开方法必须记录入口和出口日志:
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 错误处理标准
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 查询:
// ✅ 正确:批量查询
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 层触发)
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 事件路由表注册)
hub.RegisterEvent("im.message.send", handler.HandleSendMessage)
hub.RegisterEvent("im.message.read", handler.HandleReadMessage)
十、前端 Store 与 API 封装规范
10.1 API 封装标准
文件位置:frontend/src/api/{module}.js
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
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 设计原则:
- 单一职责:每个模块一个 Store,不混合不同模块数据
- WS 监听统一初始化:所有 Store 的 WS 事件监听在
App.vue的_initGlobalWS中统一调用 - 消息缓存:按 conversationId 键值对缓存,避免重复请求
- 乐观更新:发送消息时先本地插入,再等待服务端 ack 确认