bindbox-game/docs/开发规范.md
邹方成 2a89a1ab9d
Some checks failed
Build docker and publish / linux (1.24.5) (push) Failing after 39s
feat(admin): 更新前端资源文件及修复相关功能
refactor(service): 修改banner和guild删除逻辑为软删除
fix(service): 修复删除操作使用软删除而非物理删除

build: 添加SQLite测试仓库实现
docs: 新增奖励管理字段拆分和批量抽奖UI改造文档

ci: 更新CI忽略文件
style: 清理无用资源文件
2025-11-19 01:35:55 +08:00

566 lines
24 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 API 开发规范
## 1. 概述
### 1.1 目的
本文档旨在为 `bindbox-game` 项目制定一套统一、清晰的 Go API 开发规范。通过标准化代码结构、命名约定、错误处理和文档注释,我们致力于提高代码的可读性、可维护性和团队协作效率,并从根本上避免之前开发中遇到的问题(如包导入不一致、特殊字符使用不当等)。
### 1.2 适用范围
本规范主要适用于项目 `internal/api/` 目录下的所有 API 层的开发工作。所有新的 API 模块以及对现有模块的修改,都必须严格遵守本规范。
### 1.3 核心原则
* **一致性**:所有代码都应遵循相同的模式和风格。
* **清晰性**:代码应易于阅读和理解,避免复杂的或晦涩的实现。
* **可维护性**:代码结构应清晰,便于未来扩展和重构。
* **api/service**代码结构应清晰api 和 service 应该保持一致的命名规范。必须: 一个 api 对应一个 service。一个函数功能对应一个 service 方法。单独一个文件
* **接口文档注释**:所有 API 端点都必须包含详细的 Swagger 注释,描述请求参数、响应体、可能的错误码等。
* **测试功能**:所有 API 端点都必须包含详细的测试用例,确保功能的正确性和稳定性。(cmd/testchain/ 中写)
---
## 2. 目录与文件结构
规范的目录结构是项目可维护性的基础。
* **模块化目录**:每一个独立的业务功能模块都应在 `internal/api/` 下创建一个专属目录。
```
internal/api/
├── admin/
├── activity/
└── ... (其他模块)
```
* **Handler 结构体定义**:在每个模块目录下,应有一个与模块同名的 `go` 文件(如 `admin.go`),用于定义该模块的 `handler` 结构体和 `New` 初始化函数Handler 不直接进行数据库读写)。
```go
// internal/api/admin/admin.go
package admin
type handler struct {
logger logger.CustomLogger
writeDB *dao.Query // 仅用于注入(避免在 Handler 直接使用)
readDB *dao.Query // 仅用于注入(避免在 Handler 直接使用)
svc adminsvc.Service
}
func New(logger logger.CustomLogger, db mysql.Repo) *handler {
return &handler{
logger: logger,
writeDB: dao.Use(db.GetDbW()),
readDB: dao.Use(db.GetDbR()),
svc: adminsvc.New(logger, db),
}
}
```
* **端点文件分离**:每个 API 端点Endpoint的实现应放在一个独立的 `go` 文件中,文件名应清晰描述其功能,示例对齐当前 `admin` 模块:
```
internal/api/admin/
├── admin.go // Handler 定义
├── login.go // 管理员登录
├── admin_create.go // 新增客服
├── admin_modify.go // 编辑客服(含路径参数)
├── admin_delete.go // 删除客服(批量)
└── admin_list.go // 客服列表(分页查询)
```
---
## 3. 命名规范
统一的命名规范能极大提升代码的可读性。
* **Go 文件**:使用小写蛇形命名法 (`snake_case`),如 `admin_create.go`。
* **Handler 函数**:使用大驼峰命名法 (`PascalCase`),且函数名应清晰表达其动作,如 `CreateAdmin()`。
* **请求/响应结构体**:使用小驼峰命名法 (`camelCase`),并以 `Request`/`Response` 作为后缀,如 `createAdminRequest`, `createAdminResponse`。
---
## 4. API Handler 规范
API Handler 是业务逻辑的入口,其规范性至关重要。
### 4.1 函数签名
所有 API Handler 函数都必须返回 `core.HandlerFunc` 类型。这是框架的核心设计,用于统一处理请求上下文。
```go
// CreateAdmin 新增客服
func (h *handler) CreateAdmin() core.HandlerFunc {
return func(ctx core.Context) {
// ... 实现
}
}
```
### 9.7 权限矩阵与中间件
- 管理端所有接口统一要求携带 `Authorization`,经 JWT 校验后方可访问;未登录或令牌无效返回 `401`。
- 管理端认证组统一前置 `RequireAdminRole` 校验:非超管必须至少绑定一个角色,否则返回 `403`。
- 敏感接口增加动作级权限校验 `RequireAdminAction(action_mark)`
- 工会:
- `POST /api/admin/guilds` → `guild:create`
- `PUT /api/admin/guilds/:guild_id` → `guild:modify`
- `DELETE /api/admin/guilds/:guild_id` → `guild:delete`
- `GET /api/admin/guilds/:guild_id` → `guild:view`
- `GET /api/admin/guilds/:guild_id/members` → `guild:view`
- `GET /api/admin/guilds/:guild_id/applications` → `guild:view`
- `POST /api/admin/guilds/:guild_id/applications/:member_id/approve` → `guild:member:approve`
- `POST /api/admin/guilds/:guild_id/applications/:member_id/reject` → `guild:member:reject`
- `DELETE /api/admin/guilds/:guild_id/members/:user_id` → `guild:member:delete`
- 商品:`product:create`、`product:modify`、`product:batch:modify`、`product:delete`、`product:view`
- 轮播图:`banner:create`、`banner:modify`、`banner:delete`、`banner:view`
- 系统称号与效果:`title:view`、`title:create`、`title:modify`、`title:delete`、`title:effect:create`、`title:effect:modify`、`title:effect:delete`、`title:assign`
- 道具卡:`itemcard:create`、`itemcard:modify`、`itemcard:delete`、`itemcard:view`
- 优惠券:`coupon:create`、`coupon:modify`、`coupon:delete`、`coupon:view`
- 订单与退款:`order:view`、`order:modify`、`order:cancel`、`order:consume`、`order:export`、`refund:create`、`refund:view`
- 发货统计:`ops:shipping:view`、`ops:shipping:write`
- 角色-动作分配通过现有接口维护:
- `POST /api/role/:role_id/actions` 分配动作
- `GET /api/menu/:menu_id/actions` 查看动作
- 权限校验失败统一返回 `403`,响应结构为标准 `code.Failure`。
### 4.2 参数绑定与校验
* **绑定JSON**POST/PUT 等带请求体的接口使用 `ctx.ShouldBindJSON(req)` 绑定到预定义的 `Request` 结构体。
* **绑定Query**GET 查询接口使用 `ctx.ShouldBindForm(req)` 绑定到预定义的 `Request` 结构体(字段使用 `form:"..."` tag
* **路径参数**:通过 `ctx.Param("id")` 等读取路径参数并进行类型转换与校验。
* **校验**:绑定方法会自动执行 `binding` tag 定义的校验规则。如果校验失败,必须立即中断请求并返回错误。
```go
req := new(createAdminRequest)
res := new(createAdminResponse)
if err := ctx.ShouldBindJSON(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
```
```go
// GET 查询绑定示例(与 admin_list 对齐)
req := new(listRequest)
res := new(listResponse)
if err := ctx.ShouldBindForm(req); err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
validation.Error(err)),
)
return
}
```
```go
// 路径参数读取示例(与 admin_modify 对齐)
id, err := strconv.Atoi(ctx.Param("id"))
if err != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.ParamBindError,
"未传递编号ID"),
)
return
}
```
### 4.3 响应处理
* **成功响应**:当业务逻辑成功执行后,必须使用 `ctx.Payload(res)` 方法来返回标准的成功响应。`res` 是预定义的 `Response` 结构体实例。
```go
res.Message = "操作成功"
ctx.Payload(res)
```
* **错误响应**:见第 6 章“错误处理规范”。
---
## 5. 数据传输对象 (DTO) 规范
DTO (Data Transfer Object) 是 API 契约的直接体现。
* **结构体定义**:每个 API 都应定义清晰的 `Request` 和 `Response` 结构体。
* **字段注释**:所有结构体字段都必须有清晰的中文注释,解释其含义和用途。
* **JSON Tag**:所有字段都必须包含 `json:"..."` tag以确保与前端交互的 JSON 字段名一致。
* **Validation Tag**:对于 `Request` 结构体中需要校验的字段,必须添加 `binding:"..."` tag 来定义校验规则(如 `required`)。
```go
// internal/api/admin/admin_create.go
type createAdminRequest struct {
UserName string `json:"username" binding:"required"` // 用户名
NickName string `json:"nickname" binding:"required"` // 昵称
Password string `json:"password" binding:"required"` // 密码
Mobile string `json:"mobile"` // 手机号 (非必填)
}
type createAdminResponse struct {
Message string `json:"message"` // 提示信息
}
```
```go
// internal/api/admin/admin_list.go
// GET 查询示例(字段使用 form 标签)
type listRequest struct {
Username string `form:"username"` // 用户名
Nickname string `form:"nickname"` // 昵称
Page int `form:"page"` // 当前页码,默认 1
PageSize int `form:"page_size"` // 每页返回的数据量,最多 100 条
}
type listData struct {
ID int32 `json:"id"`
UserName string `json:"username"`
NickName string `json:"nickname"`
Mobile string `json:"mobile"`
Avatar string `json:"avatar"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type listResponse struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
List []listData `json:"list"`
}
```
---
## 6. 错误处理规范
统一的错误处理是保证 API 健壮性和可预测性的关键。
* **统一入口**:所有在 Handler 中捕获的错误(参数校验失败、数据库操作失败、权限不足等)都必须通过 `ctx.AbortWithError()` 函数来处理。
* **`core.Error` 结构**`ctx.AbortWithError` 接收一个 `core.Error` 对象,该对象封装了错误的三个核心要素:
1. **HTTP 状态码**:如 `http.StatusBadRequest`。
2. **业务错误码**:在 `internal/code/` 中预定义的 `code.Code`,如 `code.CreateAdminError`。
3. **详细错误信息**:一个描述错误具体原因的字符串。
* **未找到记录**:当数据库查询返回 `gorm.ErrRecordNotFound` 时,需要按具体业务错误码返回明确的提示信息(如“账号不存在”)。
* **权限校验失败**:当 `ctx.SessionUserInfo().IsSuper != 1` 时,禁止操作,返回对应业务错误码和提示“禁止操作”。
```go
// 示例:用户已存在
if info != nil {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateAdminError,
fmt.Sprintf("%s: %s", code.Text(code.CreateAdminError), "该账号已存在")),
)
return
}
```
```go
// 示例:未找到记录(与 admin_modify/login 对齐)
if err == gorm.ErrRecordNotFound {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.AdminLoginError,
fmt.Sprintf("%s: %s", code.Text(code.AdminLoginError), "账号不存在,请联系管理员。")),
)
return
}
```
```go
// 示例:权限校验失败(与 create/modify/delete/list 对齐)
if ctx.SessionUserInfo().IsSuper != 1 {
ctx.AbortWithError(core.Error(
http.StatusBadRequest,
code.CreateAdminError,
fmt.Sprintf("%s: %s", code.Text(code.CreateAdminError), "禁止操作")),
)
return
}
```
---
### 6.1 数据库上下文与读写分离Service 层)
* **上下文传递**:所有数据库操作必须调用 `WithContext(ctx)`Service 层接收 `context.Context`)。
* **读写分离**:读取使用 `s.readDB`,写入使用 `s.writeDB`。
* **分页查询会话**:分页列表与计数需要分别创建独立会话,避免互相影响。
```go
query := s.readDB.Admin.WithContext(ctx)
listQueryDB := query.Session(&gorm.Session{})
countQueryDB := query.Session(&gorm.Session{})
resultData, err := listQueryDB.Order(s.readDB.Admin.ID.Desc()).Limit(in.PageSize).Offset((in.Page-1)*in.PageSize).Find()
if err != nil { /* 错误处理 */ }
count, err := countQueryDB.Count()
if err != nil { /* 错误处理 */ }
```
---
## 7. 日志记录规范
虽然在 `admin` 模块的示例中未显式大量使用,但规范的日志记录对于问题排查至关重要。
* **使用注入的 Logger**:应通过 `handler` 结构体中注入的 `h.logger` 实例来记录日志。
* **记录关键信息**:在关键业务节点或错误发生时,记录必要的上下文信息,如请求参数、用户 ID 等。
```go
// 推荐实践 (示例)
h.logger.Error("创建管理员失败", zap.Error(err), zap.String("username", req.UserName))
```
---
## 8. 注释与文档 (Swagger)
API 的可发现性和易用性依赖于完善的文档。
* **强制 Swagger 注解**:每个 API Handler 函数上方都必须添加完整的 Swagger 注解块。
* **注解内容**:必须包含以下核心注解:
* `@Summary`:一句话功能简介。
* `@Description`:更详细的功能描述。
* `@Tags`API 分组标签,便于在 Swagger UI 中分类。
* `@Accept` / `@Produce`:通常为 `json`。
* `@Param`:描述请求参数,包括参数位置(`body`)、类型、是否必需和说明。
* `@Success`:描述成功响应的结构。
* `@Failure`:描述可能发生的错误响应。
* `@Router`API 的路由路径和 HTTP 方法。
* `@Security`:如果接口需要认证,需添加此注解。
```go
// internal/api/admin/admin_create.go
// CreateAdmin 新增客服
// @Summary 新增客服
// @Description 新增客服
// @Tags 管理端.客服管理
// @Accept json
// @Produce json
// @Param RequestBody body createAdminRequest true "请求参数"
// @Success 200 {object} createAdminResponse
// @Failure 400 {object} code.Failure
// @Router /api/admin/create [post]
// @Security LoginVerifyToken
func (h *handler) CreateAdmin() core.HandlerFunc { /* ... */ }
// internal/api/admin/admin_list.goGET 查询示例)
// @Param username query string false "用户名"
// @Param nickname query string false "昵称"
// @Param page query int true "当前页码" default(1)
// @Param page_size query int true "每页返回的数据量,最多 100 条" default(20)
// @Router /api/admin/list [get]
// @Security LoginVerifyToken
// internal/api/admin/admin_modify.go路径参数示例
// @Param id path string true "编号ID"
// @Router /api/admin/{id} [put]
// @Security LoginVerifyToken
// internal/api/admin/login.go登录接口不加 Security
// @Router /api/admin/login [post]
```
---
## 9. 路由与中间件规范
路由是 API 对外的入口,必须与模块职责、认证策略保持一致,并在 `internal/router/router.go` 统一注册。
### 9.1 分组与路径前缀
- **管理端**:统一前缀 `"/api/admin"`
- 非认证组:用于登录、系统状态等不需要鉴权的接口。
- 认证组:使用鉴权中间件保护,适用于敏感操作(新增、编辑、删除、列表)。
- **用户端APP**:统一前缀 `"/api/app"`,用于对外业务接口(如活动相关)。
### 9.2 鉴权中间件
- **封装方式**:通过 `core.WrapAuthHandler(interceptor.AdminTokenAuthVerify)` 将会话信息写入上下文,认证失败统一返回业务错误。
- **接口签名**`internal/router/interceptor/interceptor.go:12-18` 定义了鉴权接口:
`AdminTokenAuthVerify(ctx core.Context) (sessionUserInfo proposal.SessionUserInfo, err core.BusinessError)`。
- **实现参考**:管理端鉴权实现见 `internal/router/interceptor/admin_auth.go:1-80`。
### 9.3 路由注册示例(与当前 admin 模块对齐)
```go
// internal/router/router.go
// 管理端非认证接口路由组(登录)
adminNonAuthApiRouter := mux.Group("/api/admin")
{
adminNonAuthApiRouter.GET("/license/status", func(ctx core.Context) { /* ... */ })
adminNonAuthApiRouter.POST("/login", adminHandler.Login())
}
// 管理端认证接口路由组(需要 Authorization
adminAuthApiRouter := mux.Group("/api/admin", core.WrapAuthHandler(intc.AdminTokenAuthVerify))
{
adminAuthApiRouter.POST("/create", adminHandler.CreateAdmin())
adminAuthApiRouter.PUT("/:id", adminHandler.ModifyAdmin())
adminAuthApiRouter.POST("/delete", adminHandler.DeleteAdmin())
adminAuthApiRouter.GET("/list", adminHandler.PageList())
}
// APP 端接口路由组
appApiRouter := mux.Group("/api/app")
{
appHandler := app.New(logger, db)
appApiRouter.GET("/activities", appHandler.ListActivities())
// ... 其他 APP 端接口
}
```
### 9.4 Swagger 安全定义与使用
- **安全定义位置**`main.go:23-26` 定义认证方案 `LoginVerifyToken`,从请求头读取 `Authorization`。
- **注解使用规范**
- 管理端认证组接口均需添加 `@Security LoginVerifyToken`。
- 登录接口不需要添加 `@Security` 注解。
### 9.5 路径命名与语义
- **资源语义**
- `POST /api/admin/create`:创建资源(支持复杂校验与默认值设置)。
- `PUT /api/admin/:id`:更新单个资源,路径参数 `id` 必须有效。
- `POST /api/admin/delete`:批量删除,参数 `ids` 以逗号分隔。
- `GET /api/admin/list`:分页检索,`page`/`page_size` 合理限制(最大 100
- **用户端**:保持 RESTful 风格,如 `GET /api/app/activities`、`GET /api/app/activities/:activity_id`。
### 9.6 指标与别名(可选)
- 为避免路径参数导致指标维度爆炸,可在 Handler 前添加别名记录指标:
```go
// 例如:为 /api/app/activities/:activity_id 设置指标记录别名
appApiRouter.GET("/activities/:activity_id",
core.AliasForRecordMetrics("/api/app/activities/:activity_id"),
appHandler.GetActivityDetail(),
)
```
---
## 10. 服务层分离规范
为避免 Handler 同时承担传输与业务职责,必须在 `internal/service/{module}` 引入服务层,统一承载业务规则、数据协作与事务处理。
### 10.1 目录与命名
- 目录:`internal/service/admin`、`internal/service/activity` 等按模块划分。
- 包名与模块名一致Handler 导入时使用别名避免冲突(如 `svc "bindbox-game/internal/service/admin"`)。
### 10.1.1 文件拆分(与 API 端点保持一致)
- 每个端点在服务层对应一个实现文件,避免所有方法堆叠在单文件:
```
internal/service/admin/
├── service.go // 接口、构造
├── login.go // 登录
├── create.go // 新增客服
├── modify.go // 编辑客服
├── delete.go // 删除客服
└── list.go // 客服列表
```
- 规则:每个端点的输入/输出类型在对应文件中定义(例如 `LoginInput`/`LoginResult` 在 `login.go`)。
---
## 11. 流程规范Router → API → Service
### 11.1 职责边界
- Router负责分组、认证拦截与端点注册不承载业务逻辑。
- 管理端非认证路由:`internal/router/router.go:44-51`
- 管理端认证路由(鉴权中间件):`internal/router/router.go:53-60`
- 用户端 APP 路由:`internal/router/router.go:62-72`
- APIHandler负责参数绑定、权限校验、错误码映射与响应输出不直接操作数据库。
- 绑定参数并校验:如登录 `internal/api/admin/login.go:35-45`、列表 `internal/api/admin/admin_list.go:57-64`
- 权限校验:如列表 `internal/api/admin/admin_list.go:83-90`、创建 `internal/api/admin/admin_create.go:49-56`
- 错误码映射与响应:如登录 `internal/api/admin/login.go:53-59,61-64`
- Service承载领域逻辑、数据访问与事务输入/输出类型在对应端点文件中定义。
- 登录实现:`internal/service/admin/login.go:16-52`
- 创建实现:`internal/service/admin/admin_create.go:14-42`
- 编辑实现:`internal/service/admin/admin_modify.go:13-50`
- 删除实现:`internal/service/admin/admin_delete.go:9-15`
- 列表实现:`internal/service/admin/admin_list.go:12-38`
### 11.2 端到端流程(以管理端为例)
- Router 接收请求并路由到对应分组:非认证或认证(携带 `Authorization`)。
- 若为认证接口,鉴权中间件注入会话信息:`internal/router/interceptor/admin_auth.go:1-80`。
- Handler
- `ShouldBindJSON` 或 `ShouldBindForm` 解析请求(含路径参数 `ctx.Param`)。
- 权限检查(如 `ctx.SessionUserInfo().IsSuper`)。
- 将 DTO 转换为 Service 输入,调用 `svc.*(ctx.RequestContext(), input)`。
- 根据 Service 返回的错误映射业务错误码,或输出成功响应 `ctx.Payload(res)`。
- Service
- 使用 `dao.Query.WithContext` 与读写分离实现查询或写入。
- 必要场景内使用事务保障原子性(参考 `dao.gen.go` 的 `Query.Transaction`)。
### 11.3 错误处理策略
- Service 返回语义化错误信息(`error`);不直接决定 HTTP 或业务码。
- Handler 统一映射到 `core.Error(httpCode, code.* , message)` 并 `AbortWithError`。
- 登录错误映射:`internal/api/admin/login.go:53-59`
- 创建错误映射:`internal/api/admin/admin_create.go:66-71`
- 编辑错误映射:`internal/api/admin/admin_modify.go:87-93`
- 删除错误映射:`internal/api/admin/admin_delete.go:95-102`
- 列表错误映射:`internal/api/admin/admin_list.go:98-105`
### 11.4 数据绑定与类型转换
- 登录:`loginRequest` → `LoginInput` 映射:`internal/api/admin/login.go:47-51`
- 创建:`createAdminRequest` → `CreateInput` 映射:`internal/api/admin/admin_create.go:58-65`
- 编辑:`modifyAdminRequest` → `ModifyInput` 映射:`internal/api/admin/admin_modify.go:79-86`
- 列表:`listRequest(form)` → `ListInput` 映射:`internal/api/admin/admin_list.go:92-97`
- 删除:解析 `ids` 列表后直接传入 `svc.Delete``internal/api/admin/admin_delete.go:95`
### 11.5 事务与读写分离
- 读操作统一使用 `readDB`,写操作统一使用 `writeDB`。
- 所有 DAO 调用必须附带上下文:`WithContext(ctx)`。
- 多步骤写操作建议在 Service 层使用事务(`Query.Transaction``internal/repository/mysql/dao/gen.go:211-213`。
### 11.6 鉴权与会话
- 路由层通过 `core.WrapAuthHandler(intc.AdminTokenAuthVerify)` 进行会话注入:`internal/router/router.go:53-60`。
- 鉴权逻辑参考:`internal/router/interceptor/admin_auth.go:1-80`。
- Handler 使用 `ctx.SessionUserInfo()` 获取操作者信息(如 `CreatedBy`、`UpdatedBy`)。
### 11.7 命名与拆分
- API 与 Service 均按端点拆分文件,命名对齐,便于定位与维护。
- Service 的输入/输出类型在对应端点文件定义(如 `login.go` 定义 `LoginInput/LoginResult`)。
---
## 12. Repository 规范
### 12.1 层职责
- 负责数据访问CRUD、分页、事务不包含业务规则与 HTTP 细节。
- 仅依赖底层 DAO/Model向 Service 层提供稳定的数据操作能力。
### 12.2 目录与组件
- 目录:`internal/repository/mysql`
- `dao/`:由 `gorm/gen` 生成的查询对象与读写分离封装,参考:
- 读写分离方法:`internal/repository/mysql/dao/gen.go:135-143`
- 事务封装:`internal/repository/mysql/dao/gen.go:211-213`
- 上下文绑定:`internal/repository/mysql/dao/gen.go:188-209`
- `model/`:数据库对应的实体结构体(字段、标签)。
### 12.3 使用规范(在 Service 层)
- 初始化查询对象:
- 读库:`readDB := dao.Use(db.GetDbR())`
- 写库:`writeDB := dao.Use(db.GetDbW())`
- 所有操作必须绑定上下文:`WithContext(ctx)`,示例(查询管理员):
```go
info, err := s.readDB.Admin.WithContext(ctx).
Where(s.readDB.Admin.Username.Eq(in.Username)).
First()
```
- 分页与计数使用独立会话,避免互相影响:
```go
listQueryDB := query.Session(&gorm.Session{})
countQueryDB := query.Session(&gorm.Session{})
items, _ := listQueryDB.Order(...).Limit(...).Offset(...).Find()
total, _ := countQueryDB.Count()
```
- 多步骤写操作建议在事务中完成:
```go
_ = s.writeDB.Transaction(func(tx *dao.Query) error {
// 在 tx 上执行写操作
return nil
})
```
### 12.4 分层建议
- 如需进一步解耦 DAO可在 `internal/repository/{module}` 定义模块级仓库接口(如 `adminrepo`),内部封装 DAO 细节Service 只依赖仓库接口,便于单元测试与替换数据源。
- 目前项目直接在 Service 层使用 DAO符合轻量项目场景当跨表协作复杂时可引入仓库接口以降低耦合。