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

8.6 KiB
Raw Blame History

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 中导出 ProviderSetwire.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_messagestype=10MessageTypeSystem)。

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 设计原则:

  1. 单一职责:每个模块一个 Store不混合不同模块数据
  2. WS 监听统一初始化:所有 Store 的 WS 事件监听在 App.vue_initGlobalWS 中统一调用
  3. 消息缓存:按 conversationId 键值对缓存,避免重复请求
  4. 乐观更新:发送消息时先本地插入,再等待服务端 ack 确认