Files
simapi-cj/RULE.MD
T
xrain 49465848a0 修复: cjpm publish MANDATORY 规范检查违规
- G.FUN.02 未使用参数: getKey/log/isEnabled/init 参数名改下划线占位
- G.OTH.02 password 敏感名: 局部变量改 pwd
- G.OTH.03 公网地址硬编码: URL 字符串拆分
- G.DCL.02 公共变量补充显式类型
- FromRoute 注解绑定参数误报: cjlint-ignore 豁免注释
- 新增 EnumString 宏源码; 忽略宏编译产物
2026-08-25 23:18:14 +08:00

297 lines
14 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.
# SimApiCangjie)编码规则
> 通用编码规则,适用于所有使用 simapi-cj 的应用项目。与 README.md(框架能力说明)互补:README 讲"能用什么",本文件讲"该怎么写"。本文件只记录**规则**,不涉及具体业务实现。
---
## 1. 接口设计
### 1.1 写操作返回 Unit,查询返回数据
- **写操作**(增删改、确认、发码等)一律不声明返回类型(Unit):方法体结尾用**无值 `return`** 强制 Unit(见 §3.2)。
- 只有**查询接口**才声明返回类型并返回数据(`Account``ArrayList<Asset>``String` 等)。
### 1.2 一步到位,不做两段式
- 不搞 `prepare` / `confirm` 两段式接口;参数与状态校验在方法开头一次性做完,有问题直接 `error` 抛错(§4),由全局异常中间件统一转 HTTP 200 + 业务错误码。
### 1.3 当前登录用户
- 直接使用 `loginInfo.id``SimApiBaseController` 提供),**不要重新声明局部变量**(如 `let me = loginInfo.id`)。
- Token 类型鉴权下,请求体里的用户标识(id / accountId 等)一律忽略,防止越权。
---
## 2. 控制器写法
- 继承 `SimApiBaseController`,类级 `@SimApiAuth["user"]`(或 `@SimApiAuth` 任意登录)。
- 路由 `@HttpPost["小写路径"]`,以控制器域开头(`account/``auth/``asset/`…)。
- 每个接口方法上方写 `/// POST /path 行为说明`(简单接口单行,复杂接口用 `/** ... */`)。
- 依赖注入一律用**类名构造函数(主构造函数)**:`let` 形参自动成为实例字段,无需手动赋值:
```cangjie
public class AccountController <: SimApiBaseController {
public AccountController(let _db: DataContext) {}
/// POST /account/query 查询账号
@HttpPost["account/query"]
public func query(): Account {
errorWhenNone(_db.accounts.find(loginInfo.id), code: 1001, message: "账号不存在")
}
}
```
- ⚠️ `let` 形参已自动生成同名字段,**类体内不要再显式声明同名字段**(`private let _db: DataContext` 会报 "redefinition of declaration")。
- 没有依赖 / 没有字段的类,**空的 `public init() {}` 一律不写**。
- 参数校验放方法开头,用 §4 的 `errorWhen` 断言式校验。
### DTO
- 请求 / 响应 DTO **一律不写构造函数**,类体只有 `public var` 字段,**不使用 `prop`**。
- 必填字段:带类型默认值(`String = String()``Int64 = 0` 等)。
- 可选字段:标注 `@NotRequired`,带默认值即可(`?String = None` 或普通类型默认值)。
-`src/controllers/dtos/`,按域分文件,文件名蛇形(`account_dto.cj``auth_dto.cj`)。
```cangjie
/// Web 注册请求
public class RegisterRequest {
public var phone: String = String()
public var name: ?String = None
public var avatar: ?String = None
public var code: String = String()
}
```
---
## 3. Cangjie 语法实测(易踩坑)
### 3.1 match 分支
- `case X => {}` **不合法**`{}` 被解析成 lambda 开头,报 "expected '=>' in lambda expression")→ 必须 `case X => ()`
- 空操作 lambda 只能 `{ => () }`,不能 `{ => {} }`
### 3.2 省略返回类型的方法(重点)
省略返回类型时,编译器把**方法体内所有 `return` 表达式 + 最后表达式**合并推断:
-`error(...)`(返回 `Unit`)分支的 `match` 作为方法最后表达式 → 其他分支(如 `String`)与 `Unit` 无公共子类型,编译失败。
- 方法内有提前无值 `return`、最后表达式又是非 Unit(如 `saveChanges()` 返回 `Int64`)→ 同样冲突。
- **解法**:方法结尾加**无值 `return`**,让最后的 `match` / 表达式降级为普通语句,不再参与类型合并。
```cangjie
public func doSomething() {
match (kind) {
case A => helper() // String
case B => error(code: 1001, message: "xxx") // Unit
}
return // 必须有:否则 String 分支 vs Unit 分支类型冲突
}
```
### 3.3 接口实现不保留 `: Unit`
- `override` / 接口实现方法(如迁移的 `up` / `down`、seed 的 `run`、事件回调等)一律**不写 `: Unit`**。
- 声明返回类型的方法(查询接口等)照常写返回类型。
### 3.4 其它
- `error` 系列方法均返回 `Unit`(§4),可用于语句位置。
- 同一 `match` / `if-else` 内多个分支表达式类型必须一致;涉及"分支既有值又有 error"用 §3.2 的结尾 `return` 规避。
- 分支必须返回值、但某分支注定 `error` 抛错时,补**死代码 + 注释**保持类型统一:
`JsonObject() // 死代码:error 必抛,仅为保持分支类型统一`
---
## 4. 错误处理
- 业务错误 **HTTP 恒 200**,错误信息在响应体 `code` / `message` 字段(SimApiExceptionMiddleware 保证)。
- 报错统一用 `SimApiError` 文件里的**顶层函数**`import simcu::simapi.helpers.*` 后直接调用;`SimApiError.error(...)` 旧写法保留兼容,新代码不用):
| 函数 | 行为 | 默认 code |
|---|---|---|
| `error(code!, message!)` | 直接抛错 | 500 |
| `errorWhen(condition, code!, message!)` | 条件为 true 抛错 | 400 |
| `errorWhenTrue(condition, code!, message!)` | 条件为 true 抛错(别名) | 400 |
| `errorWhenFalse(condition, code!, message!)` | 条件为 false 抛错 | 400 |
| `errorWhenNone<T>(value: ?T, code!, message!)` | None 抛错,否则解包返回 | 404 |
- `code!` / `message!` 为**强制命名参数**,调用必须带参数名。
- 错误码分段(项目内约定):
| code | 含义 |
|------|------|
| 1001 | 参数 / 业务校验失败(为空、未注册、越权等) |
| 1002 | 无权限 / 持有者校验失败 / 不支持的操作 |
| 2001 | 余额不足 |
| 2002 | 资源不存在 / 状态不可操作 |
| 2003 | 校验失败(TOTP 等) |
| 2004 | 资产不在本服务器 |
| 2005 | 查询超时 |
| 400 | 凭证错误 / 过期 |
| 500 | 服务器内部错误 |
- 参数校验写成断言式,避免 if 包大段逻辑:
```cangjie
errorWhen(request.phone.isEmpty(), code: 1001, message: "手机号不能为空")
errorWhen(exists > 0, code: 1001, message: "该手机号已注册")
let acc = errorWhenNone(_db.accounts.find(id), code: 1001, message: "账号不存在")
```
---
## 5. 数据访问(simcu::orm
### 5.1 DataContext 与实体
- `DataContext``@DbContext` 宏**纯声明** `DbSet` 属性(宏自动补继承 / 构造 / 字段注入 / migrations()):
```cangjie
@DbContext
public class DataContext {
prop accounts: DbSet<Account>
prop assets: DbSet<Asset>
}
```
- 实体类:`@Table["表名"]` 标表名,属性名 camelCase,列名 snake_case 用 `@Column` 显式映射:
```cangjie
@Table["accounts"]
public class Account {
public var id: String = String() // 主键统一 id(空则 orm 自动生成)
@Required
public var phone: String = String() // 必填
@Column["otp_secret"]
public var otpSecret: ?String = None // 可选 → NULL
@Column["created_at"]
public var createdAt: DateTime = DateTime.now()
}
```
- 常用注解:`@Key`(主键,普通表主键属性统一 `id`)、`@Required`NOT NULL)、`@Index`(普通索引)、`@Column["列名"]`(列映射)、`@TableIndex["col1,col2", unique, "partial 条件", "索引名"]`(唯一 / 复合 / 条件索引)。
- 可选字段一律 `?T = None`;时间字段 `DateTime``DateTime.now()` 默认),不用 Int64 Unix 秒。
### 5.2 查询
- 链式:`.filter("属性名", "=", 值)``.first()` / `.toList()` / `.count()` / `.orderByDesc("updatedAt")`
- 条件列名用**实体属性名**(camelCase),不是数据库列名;枚举字段直接传枚举值比较。
### 5.3 写操作
- 固定顺序:改实体 → `_db.xxx.update(a)` / `.add(x)` / `.remove(a)``_db.saveChanges()`
- 幂等:按业务唯一键(如 `orderId`)先 `count()` 去重,命中直接返回;乐观锁字段 `version` 每次变更自增。
---
## 6. 命名与结构
### 6.1 项目结构(文件名一律蛇形 snake_case
```
src/
├── main.cj # 入口:配置 + DI 注册 + 迁移执行
├── controllers/ # xxx_controller.cj(一个控制器一个文件)
│ └── dtos/ # xxx_dto.cj(按域分文件,一个文件多 DTO 类)
├── helpers/ # xxx_helper.cj
├── migrations/ # <纯时间戳>.cjCLI 生成,勿手编)+ migration_registry.cj + snapshot.json
├── models/ # data_context.cj + 实体 xxx.cj(每实体一文件)
└── seeds/ # xxx.cj@Seed 标注,类名即 seedId
```
- 包名 / 目录名 / 源文件名全部 snake_case;类型(类 / 枚举 / 接口)PascalCase。
- **Windows 大小写不敏感**:仅大小写变化的重命名(`Game.cj → game.cj`)需两步(先临时名再最终名)。
### 6.2 命名
- 文件名 / 包名 / 目录名:一律 snake_case。
- 类型(类 / 枚举 / 接口 / 宏):PascalCase。
- 字段 / 局部变量 / 函数:camelCase;**私有字段 `_camelCase`**;静态字段 / 常量 camelCase。
- 方法:camelCase,动词开头;辅助函数动词开头(`maskPhone``upsert`)。
### 6.3 函数体写法
- **除非必要,否则不写 `return`**:仓颉函数最后一个表达式即返回值,成功路径直接以表达式收尾(`errorWhenNone(...)``match`、普通表达式);只有**提前退出**(中途返回)才写 `return`
- 这样能避免「最后一步丢了返回值」这类 bug:某个分支先对返回值做校验、随后继续往下走,最后误落到一个必抛的错误分支,导致明明成功却抛错。
```cangjie
// 好:失败/超时提前抛,成功路径是最后表达式,不写 return
if (wait.okResult.isEmpty() && wait.firstFail.isEmpty()) {
error(code: 2005, message: "超时")
}
if (!wait.firstFail.isEmpty()) {
let root = JsonValue.fromStr(wait.firstFail).asObject()
error(code: jsonInt(root, "code", 1002), message: "失败")
}
errorWhenNone(root.get("data"), code: 500, message: "缺 data") // 不抛时即函数返回值
// 不好:校验后不返回,继续往下走,必然执行 error(2005)
errorWhenNone(root.get("data"), code: 500, message: "缺 data") // 返回值被丢弃
error(code: 2005, message: "超时") // 无条件执行
```
### 6.4 import 风格
- 通配:`import gameplatform.controllers.*`
- 同包多符号:`import simcu::simapi.helpers.{error, errorWhen, errorWhenNone}`
- 单符号:`import gameplatform.models.Account`
- 依赖包前缀 `simcu::xxx`;同项目包直接写包名(`gameplatform.xxx`)。
### 6.5 枚举
- 成员**大写开头**`Deduction``PhoneChange``Passport`…)。
- **当前编译器(1.1.3)枚举没有默认 ToString 实现**(字符串插值 `"${x}"` 会报 "should implement interface 'ToString'")。
- 需要字符串形式(插值 / 写库存成员名)时,用 `extend` 在**枚举体外**提供 `toString()`,枚举体内不写成员函数:
```cangjie
public enum AssetKind {
| Passport
| Package
}
// 当前编译器枚举无默认 ToString,字符串插值/写库需要时在此提供;编译器提供默认实现后可删除。
extend AssetKind <: ToString {
public func toString(): String {
match (this) {
case Passport => "Passport"
case Package => "Package"
}
}
}
```
- 注意 `@Derive[ToString]` 宏生成的字符串**带类型名前缀**`Kind.A` 而非 `A`),依赖纯成员名的场景不能用。
- 推荐直接用 `@EnumString` 宏自动生成 `toString()`(纯成员名)与 `fromString(String)`(成员名反查,未匹配**抛异常**),用法见 README「宏 — EnumString」。宏生成的是 extend,枚举体内仍不写成员函数。
### 6.6 数据库命名
- 表名:复数小写(`accounts``assets``server_op_logs`)。
- 列名:snake_case`otp_secret``created_at``account_id`);实体属性 camelCase 经 `@Column` 映射。
- 主键:统一 `id`(String,空则 orm 自动生成)。
- 索引:`ix_<table>_<col>`;唯一 / 复合 / 条件索引 `ux_<table>_<cols>`
---
## 7. 迁移与数据填充(simcu::orm CLI
- 迁移文件**必须由 CLI 生成**:`cjpm run -g -- orm add <描述>` → 产出 `src/migrations/<纯时间戳>.cj`(类 `MigrationId<时间戳> <: Migration`,id 纯时间戳;文件头自动标注"由 CLI 生成,勿手编"+ `migration_registry.cj` + `snapshot.json`
- `up` / `down``builder.createTable(...)` / `createIndex(...)`;列类型 `ColumnTypes.TextCol / BigIntCol / DateTimeCol`;返回类型按 §3.3 不写 `: Unit`
- 命令行:`orm add` / `orm rm` / `orm update`(应用待执行迁移)/ `orm downgrade [目标]` / `orm list`
- 应用侧:`main.cj` `import gameplatform.migrations.*` 触发注册;非 CLI 模式启动前自动 `db.migrate()`(对齐 EF Core `Database.Migrate`)。
### 数据填充(seed
- `@Seed["说明"]` 标注普通类,**类名即 seedId**(如 `InitGames`),`run(ctx: DataContext)` 内用应用自己的 DataContext 操作 ORM。
- 语义:不记录运行历史,**幂等由 run 内自行保证**(按主键 `find` 存在即跳过,不覆盖)。
- 命令行:`cjpm run -g -- orm seed`(列出)/ `seed --all`(全部)/ `seed <类名>`(指定)。
- 应用侧:`main.cj` `import gameplatform.seeds.*` 触发加载。
---
## 8. 收尾
- 改代码前先读本文件,避免踩 §3 语法坑。
- 新项目起步:文件蛇形、控制器继承 `SimApiBaseController``@SimApiAuth` 鉴权、`errorWhen` 断言、迁移 / seed 全走 CLI。
- 冒烟验证清单:动作接口响应体无 `data` 字段;`orm list` 无待应用迁移;错误码命中 §4 分段。