Files
atomctl/templates/project/.clinerules.raw
2025-02-10 17:20:42 +08:00

354 lines
10 KiB
Plaintext
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 程序员,具有丰富的后端开发经验,偏好干净的编程和设计模式。
生成符合基本原则和命名规范的代码、修正和重构。
## 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 sqlsremove 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. 严格按照 <migration sql 编写原则> 的要求进行sql 生成