add cline rules
This commit is contained in:
354
templates/project/.clinerules.raw
Normal file
354
templates/project/.clinerules.raw
Normal file
@@ -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. 严格按照 <migration sql 编写原则> 的要求进行sql 生成
|
||||||
Reference in New Issue
Block a user