--- name: crud-repository description: PowerX CRUD Repository 规则(BaseRepository、tenant 约束、分页)。 --- # PowerX CRUD Repository ## 步骤 1) 打开 `本文件内嵌规则`。 2) 按规则执行实现/校对。 3) 完成后按核对清单验收。 ## 核对点 - 与 PowerX 当前代码结构、路径与命名一致。 - 仅在传输层/契约层做职责内改动,不跨层越界。 ## 规则(内嵌) ### repository.yaml ````yaml kind: ruleset name: crud_repository version: 1.0.0 owner: powerx status: stable meta: intent: > 统一 Repository(持久化)层的接口与实现规范:只负责数据访问与映射, 不承载业务逻辑;严格多租户隔离、软删、分页筛选与一致的错误/返回语义。 CoreX 模块仓储统一采用 BaseRepository 泛型封装,便于事务注入与测试。 references: - constitution.md - crud_model.yaml - crud_migration.yaml - crud_dto.yaml - crud_di.yaml scope: codebase: repo_globs: - "internal/repository/**/**_repo.go" - "pkg/corex/db/persistence/repository/**/**_repo.go" model_globs: - "pkg/corex/db/persistence/model/**/**.go" principles: - 目录规范:CoreX 仓库实现放置于 `pkg/corex/db/persistence/repository//`,目录名使用 snake_case,对应的模型位于同层级 `model/`。 - 职责单一:Repository 只做数据访问(SQL/GORM),禁止业务分支与权限判断。 - 多租户强约束:所有查询均带 TenantID 条件;默认忽略软删(GORM 默认生效)。 - 事务注入:方法接受 *gorm.DB(可为 tx),由 Service 层控制 tx 生命周期。 - 上下文贯穿:方法首参为 context.Context,用于 tracing/超时/审计钩子。 - 错误语义:不存在返回 gorm.ErrRecordNotFound(由 Service 翻译成 404);唯一冲突返回 ErrConflict(由 Service 翻译 409)。 - 可测试性:提供接口与实现;实现命名 `XxxRepoImpl`,禁止全局单例。 - 性能:列表查询必须显式选择字段/建立索引,禁止 N+1(注意 Preload 选择性)。 checks: # 目录/文件命名 - id: repo.file.naming level: error when: { glob: "internal/repository/**/**_repo.go" } assert: - must_package_suffix: "repo" # BaseRepository 形态 - id: repo.base_repository.pattern level: error when: { glob: "internal/repository/**/**_repo.go" } assert: - must_contain_regex: "type [A-Za-z0-9]+Repository struct {" - must_contain_regex: "\\*repository.BaseRepository" - must_contain_regex: "func New[A-Za-z0-9]+Repository\\(db \\*gorm\\.DB\\)" - must_not_import: ["github.com/gin-gonic/gin"] # 不允许直接耦合 HTTP # 方法签名(ctx + db) - id: repo.method.signature level: error when: { glob: "internal/repository/**/**_repo.go" } assert: - must_contain_regex: "func \\(r \\*.*Repository\\) .*\\(ctx context\\.Context, db \\*gorm\\.DB" - must_import: ["context","gorm.io/gorm"] # 多租户条件 - id: repo.must.tenant.scope level: error when: { glob: "internal/repository/**/**_repo.go" } assert: - contains_any: - "Where(\"tenant_id = ?\"" - "Scopes(TenantScope(" - "WithTenant(" message: "所有 CRUD 查询必须带 TenantID(直写 Where 或统一 Scopes/TenantScope)" # 列表与分页(limit/offset 或 cursor) - id: repo.list.pagination level: error when: { glob: "internal/repository/**/**_repo.go" } assert: - contains_any: ["Limit(","Offset(","Scopes(Paginate("] # 冲突与不存在的错误 - id: repo.error.mapping level: warn when: { glob: "internal/repository/**/**_repo.go" } assert: - contains_any: ["gorm.ErrRecordNotFound","ErrDuplicate","unique"] message: "建议把 gorm.ErrRecordNotFound 原样返回,由 Service 统一翻译为 404;唯一冲突包装为 ErrConflict" # 软删(默认开启),如实现硬删需标注 - id: repo.soft_delete level: warn when: { glob: "internal/repository/**/**_repo.go" } assert: - should_contain_any: ["Delete(&m)","Unscoped().Delete("] # 性能/索引使用(建议) - id: repo.index.hint level: warn when: { glob: "internal/repository/**/**_repo.go" } assert: - should_contain_any: ["Select(","Omit("] acceptance: checklist: - "[ ] 每个仓库文件包含接口 + 实现 + New 构造函数" - "[ ] 方法签名统一为 (ctx context.Context, db *gorm.DB, ...)" - "[ ] 所有查询带 TenantID 条件(Where/Scopes/TenantScope)" - "[ ] 列表查询有分页(Limit/Offset 或 Cursor)" - "[ ] 不做业务鉴权/分支;错误语义由 Service 统一翻译" - "[ ] 使用软删;如需硬删必须在 Service 授权并清晰标注" - "[ ] 可被 DI 装配(由 Deps 提供 *gorm.DB 注入)" templates: repo_interface_impl_go: | // pkg/corex/db/persistence/repository/{{domain}}/{{resource}}_repo.go package {{domain}}repo import ( "context" "gorm.io/gorm" m "{{module_path}}/pkg/corex/db/persistence/model/{{domain}}" ) // {{Entity}}Repo 数据访问接口 type {{Entity}}Repo interface { Create(ctx context.Context, db *gorm.DB, tenantID uint64, in *m.{{Entity}}) error GetByID(ctx context.Context, db *gorm.DB, tenantID uint64, id string) (*m.{{Entity}}, error) List(ctx context.Context, db *gorm.DB, tenantID uint64, pg Page, filters map[string]string) ([]*m.{{Entity}}, int64, error) Update(ctx context.Context, db *gorm.DB, tenantID uint64, in *m.{{Entity}}) error Delete(ctx context.Context, db *gorm.DB, tenantID uint64, id string, hard bool) error // Tx 透传辅助(可选) WithTx(ctx context.Context, db *gorm.DB, fn func(tx *gorm.DB) error) error } // 实现 type {{Entity}}RepoImpl struct { db *gorm.DB } func New{{Entity}}Repo(db *gorm.DB) {{Entity}}Repo { return &{{Entity}}RepoImpl{db: db} } // 分页结构(按项目实际放置到共用包) type Page struct { Page int PageSize int SortBy string SortOrder string } func (r *{{Entity}}RepoImpl) Create(ctx context.Context, db *gorm.DB, tenantID uint64, in *m.{{Entity}}) error { in.TenantID = tenantID return db.WithContext(ctx).Create(in).Error } func (r *{{Entity}}RepoImpl) GetByID(ctx context.Context, db *gorm.DB, tenantID uint64, id string) (*m.{{Entity}}, error) { var out m.{{Entity}} err := db.WithContext(ctx). Where("tenant_id = ? AND id = ?", tenantID, id). First(&out).Error if err != nil { return nil, err // 由 Service 翻译为 404 } return &out, nil } // 可抽成 Scopes:TenantScope / Filters / Paginate func (r *{{Entity}}RepoImpl) List(ctx context.Context, db *gorm.DB, tenantID uint64, pg Page, filters map[string]string) ([]*m.{{Entity}}, int64, error) { q := db.WithContext(ctx).Model(&m.{{Entity}}{}).Where("tenant_id = ?", tenantID) // 简化:按 filters 动态拼装(生产中建议固定白名单) if v, ok := filters["status"]; ok { q = q.Where("status = ?", v) } if v, ok := filters["q"]; ok { q = q.Where("name ILIKE ?", "%"+v+"%") } // 计数 var total int64 if err := q.Count(&total).Error; err != nil { return nil, 0, err } // 排序/分页 orderBy := "created_at desc" if pg.SortBy != "" { if pg.SortOrder == "" { pg.SortOrder = "desc" } orderBy = pg.SortBy + " " + pg.SortOrder } if pg.Page <= 0 { pg.Page = 1 } if pg.PageSize <= 0 { pg.PageSize = 20 } var items []*m.{{Entity}} err := q.Order(orderBy). Limit(pg.PageSize). Offset((pg.Page-1)*pg.PageSize). Find(&items).Error if err != nil { return nil, 0, err } return items, total, nil } func (r *{{Entity}}RepoImpl) Update(ctx context.Context, db *gorm.DB, tenantID uint64, in *m.{{Entity}}) error { return db.WithContext(ctx). Model(&m.{{Entity}}{}). Where("tenant_id = ? AND id = ?", tenantID, in.ID). Updates(map[string]any{ "name": in.Name, "meta": in.Meta, "status": in.Status, }).Error } func (r *{{Entity}}RepoImpl) Delete(ctx context.Context, db *gorm.DB, tenantID uint64, id string, hard bool) error { q := db.WithContext(ctx).Where("tenant_id = ? AND id = ?", tenantID, id) if hard { return q.Unscoped().Delete(&m.{{Entity}}{}).Error } return q.Delete(&m.{{Entity}}{}).Error } func (r *{{Entity}}RepoImpl) WithTx(ctx context.Context, db *gorm.DB, fn func(tx *gorm.DB) error) error { return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { return fn(tx) }) } scopes_go: | // internal/repository/scopes/scopes.go (可选:集中放置) package scopes import "gorm.io/gorm" func TenantScope(tenantID uint64) func(*gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { return db.Where("tenant_id = ?", tenantID) } } func Paginate(page, pageSize int) func(*gorm.DB) *gorm.DB { if page <= 0 { page = 1 } if pageSize <= 0 { pageSize = 20 } return func(db *gorm.DB) *gorm.DB { return db.Limit(pageSize).Offset((page-1)*pageSize) } } ````