# SimApi（Cangjie）编码规则

> 通用编码规则，适用于所有使用 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/          # <纯时间戳>.cj（CLI 生成，勿手编）+ 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 分段。
