diff --git a/templates/project/.clinerules.raw b/templates/project/.clinerules.raw new file mode 100644 index 0000000..ed64889 --- /dev/null +++ b/templates/project/.clinerules.raw @@ -0,0 +1,354 @@ +我的主语言是简体中文,所以请用简体中文回答我,与我交流。 + +您是一名高级 Go 程序员,具有丰富的后端开发经验,偏好干净的编程和设计模式。 + +生成符合基本原则和命名规范的代码、修正和重构。 + +## Go 一般指南 + +### 基本原则 + +- 所有代码和文档使用中文。 +- 遵循 Go 的官方规范和最佳实践。 +- 使用 `gofumpt -w -l -extra .` 格式化代码。 +- 错误处理优先使用 errors.New 和 fmt.Errorf。 +- 业务返回的错误需要在 `app/errorx` 包中定义。 +- 在错误处理时,使用适当的上下文信息提供更多错误细节。 + +### 命名规范 +- 包名使用小写单词。 +- 文件名使用小写下划线。 +- 环境变量使用大写。 +- 常量使用驼峰命名。 +- 导出的标识符必须以大写字母开头。 +- 缩写规则: +- i、j 用于循环 +- err 用于错误 +- ctx 用于上下文 +- req、res 用于请求响应 + +### 函数设计 + +- 函数应该短小精悍,单一职责。 +- 参数数量控制在 5 个以内。 +- 使用多值返回处理错误。 +- 优先使用命名返回值。 +- 避免嵌套超过 3 层。 +- 使用 defer 处理资源清理。 + +### 错误处理 + +- 总是检查错误返回。 +- 使用自定义错误类型。 +- 错误应该携带上下文信息。 +- 使用 errors.Is 和 errors.As 进行错误比较。 + +### 并发处理 + +- 使用 channel 通信而非共享内存。 +- 谨慎使用 goroutine。 +- 使用 context 控制超时和取消。 +- 使用 sync 包进行同步。 + +### 测试规范 + +- 编写单元测试和基准测试。 +- 使用表驱动测试。 +- 测试文件以 _test.go 结尾。 +- 使用 `stretchr/testify` `stretchr/testify` `github.com/agiledragon/gomonkey/v2` 测试框架。 + +## 项目技术栈 +- github.com/uber-go/dig 依赖注入 +- github.com/go-jet/jet 数据库查询构建器 +- github.com/ThreeDotsLabs/watermill 即时Event消息队列 +- github.com/riverqueue/river Job队列 +- github.com/gofiber/fiber/v3 HTTP框架 +- github.com/swaggo/swag 自动生成API文档, 在controller的方法上使用注解即可 + +## Atomctl 工具使用 + +### 生成命令 +- gen model:从数据库生成模型 +- gen provider:生成依赖注入提供者 +- gen route:生成路由定义 + +### 数据库命令 +- migrate:执行数据库迁移 +- migrate up/down:迁移或回滚 +- migrate status:查看迁移状态 +- migrate create:创建迁移文件,迁移文件的命名需要使用动词名词的结合方式,如 create_users_table, 创建完成后文件会存在于 `database/migrations` 目录下 + +### 最佳实践 +- migration 创建后需要执行 `atomctl migrate up` 执行数据库表迁移 +- 使用 gen model 前确保已migrate完成,并配置好 database/transform.yaml +- 对model中需要转换的数据结构声明在目录 `database/fields` 中,文件名与model名一致 +- provider 生成时使用适当的注解标记 +- 遵循目录结构约定 + +## 项目结构 + +### 标准目录 +- main.go:主程序入口 +- providers/:依赖注入提供者, 通过 atomctl gen provider 生成, 但是你不可以对其中的内容进行修改 +- database/fields:数据库模型字段定义 +- database/schemas:数据库自动生成的模型文件。 +- database/migrations: 数据库迁移文件,通过 atomctl migrate create 创建,你不可以手工创建,只可以使用脚手架工具进行创建 +- configs.toml:配置文件 +- proto/: gRPC proto 定义 +- pkg/atom: 为依赖注入框架的核心代码,你不可以进行修改 +- fixtures/:测试文件 +- app/errorx: 业务错误定义 +- app/http: HTTP 服务 +- app/grpc: gRPC 服务 +- app/jobs: 后台任务定义 +- app/middlewares: HTTP 中间件 + +## 开发示例 + +### migration 定义 +migration 文件示例. +``` +-- +goose Up +-- +goose StatementBegin + +-- write your migration up sqls (remove this) + +-- +goose StatementEnd + + +------------------------------------------------------------------------------------------------------ + + +-- +goose Down +-- +goose StatementBegin + +-- write your migration down sqls(remove this) + +-- +goose StatementEnd +``` + +### migration sql 编写原则 +1. 数据库表需要按需要添加 `created_at` `updated_at` `deleted_at` 字段 +2. 这三个时间字段(`created_at` `updated_at` `deleted_at`)需要直接位于 id 字段后面, 避免后期数据库表字段变更造成字段混乱。 +3. 所有表不使用 `FOREIGN KEY` 约束,而是在业务中使用代码逻辑进行约束。 +4. 所有字段需要添加中文注释 + +### http module +1. 创建一个新的 http module `atomctl new module [users]` +2. 在 `app/http` 目录下创建相关的处理程序。 +3. 定义用户相关的路由。 +4. 实现相关逻辑操作 +5. module 名称需要使用复数形式,支持多层级目录,如 `atomctl new module [users.orders]` + +### controller +- controller 的定义 +```go +// @provider +type PayController struct { + svc *Service + log *log.Entry `inject:"false"` +} + +func (c *PayController) Prepare() error { + c.log = log.WithField("module", "orders.Controller") + return nil +} + +// actions ... +} +``` + +- 一个 action 方法的定义 +```go +// Orders show user orders +// @swagger definitions +// @Router /api/v1/orders/:channel [get] +// @Bind channel path +// @Bind claim local +// @Bind pagination query +// @Bind filter query +func (c *OrderController) List(ctx fiber.Ctx, claim *jwt.Claims,channel string, pagination *requests.Pagination, filter *UserOrderFilter) (*requests.Pager, error) { + pagination.Format() + pager := &requests.Pager{ + Pagination: *pagination, + } + + filter.UserID = claim.UserID + orders, total, err := c.svc.GetOrders(ctx.Context(), pagination, filter) + if err != nil { + return nil, err + } + pager.Total = total + + pager.Items = lo.FilterMap(orders, func(item model.Orders, _ int) (UserOrder, bool) { + var o UserOrder + if err := copier.Copy(&o, item) ; err != nil { + return o, false + } + return o, true + }) + + return pager, nil +} +``` +- 你需要把第二行的 `@swagger definitions` 替换成你的swagger定义 +- @Bind 参数会有几个位置 path/query/body/header/cookie/local/file 会分别从 url/get query/post body/header/cookie/fiber.Local/file/中取出所需要的数据绑定到方法的请求参数中去。 +- controller 只负责数据的接收返回及相关数据装饰,具体的复杂逻辑实现需要在service文件中定义。 + +### service +- service 的定义 +```go +// @provider +type Service struct { + db *sql.DB + log *log.Entry `inject:"false"` +} + +func (svc *Service) Prepare() error { + svc.log = log.WithField("module", "orders.service") + _ = Int(1) + return nil +} +``` + +- service 中 model 数据查询的示例,需要注意table需要定义为一个短小的tblXXX以便代码展示简洁 +```go +// GetUserOrderByOrderID +func (svc *Service) GetUserOrderByOrderID(ctx context.Context, orderID string, userID int64) (*model.Orders, error) { + _, span := otel.Start(ctx, "users.service.GetUserOrderByOrderID") + defer span.End() + span.SetAttributes( + attribute.String("order.id", orderID), + attribute.Int64("user.id", userID), + ) + + tbl := table.Orders + stmt := tbl.SELECT(tbl.AllColumns).WHERE(tbl.OrderSerial.EQ(String(orderID)).AND(tbl.UserID.EQ(Int64(userID)))) + span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql())) + + var order model.Orders + if err := stmt.QueryContext(ctx, svc.db, &order) ; err != nil { + span.RecordError(err) + return nil, err + } + return &order, nil +} + +// UpdateStage +func (svc *Service) UpdateStage(ctx context.Context, tenantID, userID, postID int64, stage fields.PostStage) error { + _, span := otel.Start(ctx, "users.service.UpdateStage") + defer span.End() + span.SetAttributes( + attribute.Int64("tenant.id", tenantID), + attribute.Int64("user.id", userID), + attribute.Int64("post.id", postID), + ) + + post, err := svc.ForceGetPostByID(ctx, postID) + if err != nil { + span.RecordError(err) + return err + } + + post.Stage = stage + + tbl := table.Posts + stmt := tbl. + UPDATE(tbl.UpdatedAt, tbl.Stage). + SET( + tbl.UpdatedAt.SET(TimestampT(time.Now())), + tbl.Stage.SET(Int16(int16(stage))), + ). + WHERE( + tbl.ID.EQ(Int64(postID)).AND( + tbl.TenantID.EQ(Int64(tenantID)).AND( + tbl.UserID.EQ(Int64(userID)), + ), + ), + ) + span.SetAttributes(semconv.DBStatementKey.String(stmt.DebugSql())) + + if _, err := stmt.ExecContext(ctx, svc.db); err != nil { + span.RecordError(err) + return err + } + + return svc.Update(ctx, tenantID, userID, postID, post) +} +``` + +- 一个service_test定义,需要使用goconvey来对不同的case进行区分。如果需要注入其它模型,你需要在 With() 方法中进行添加。 +```go +package auth + +import ( + "testing" + + "backend/app/service/testx" + + . "github.com/smartystreets/goconvey/convey" + "github.com/stretchr/testify/suite" + "go.uber.org/dig" +) + +type ServiceInjectParams struct { + dig.In + Svc *Service +} + +type ServiceTestSuite struct { + suite.Suite + ServiceInjectParams +} + +func Test_DiscoverMedias(t *testing.T) { + providers := testx.Default().With( + Provide, + ) + + testx.Serve(providers, t, func(params ServiceInjectParams) { + suite.Run(t, &ServiceTestSuite{ServiceInjectParams: params}) + }) +} + +func (s *ServiceTestSuite) Test_Service() { + Convey("Test Service", s.T(), func() { + So(s.Svc, ShouldNotBeNil) + }) +} +``` + +### grpc +- 一个 handler 的示例 +``` +import ( + userv1 "go.ipao.vip/project/test01/pkg/proto/user/v1" +) + +// @provider(grpc) userv1.RegisterUserServiceServer +type Users struct { + userv1.UnimplementedUserServiceServer +} + +func (u *Users) ListUsers(ctx context.Context, in *userv1.ListUsersRequest) (*userv1.ListUsersResponse, error) { + // userv1.UserServiceServer + return &userv1.ListUsersResponse{}, nil +} + +// GetUser implements userv1.UserServiceServer +func (u *Users) GetUser(ctx context.Context, in *userv1.GetUserRequest) (*userv1.GetUserResponse, error) { + return &userv1.GetUserResponse{ + User: &userv1.User{ + Id: in.Id, + }, + }, nil +} + +``` + + +## 本项目说明 +1. 设计一个支持多租户的用户系统,一个用户可以同时属于多个租户 +2. 每一个租户有一个租户管理员角色,这个角色可以在后台由系统管理员指定,或者用户在申请创建租户申请时自动指定。 +3. 除系统管理员外,一个普通用户只可以是一个租户的管理员,不能同时管理多个租户。 +4. 严格按照 的要求进行sql 生成 \ No newline at end of file