# gen-login 生成器设计（登录链统一）

> 状态：**已实现（2026-08-19）**
> 关联文档：[token.md](./token.md)（token 令牌体系，底座）、[wechat.md](./wechat.md)（微信小程序登录体系，消费本设计）
> 定位：把 `gen admin-login`（仅 admin 账密）升级为 `gen login`——按 app 类型（admin / wxmini / mobile）分叉生成登录链，读取同一份 `login.config.ts`，每个 app 一份配置。

## 1. 背景与动机

现状 `gen admin-login` 只服务 admin 类型 app（`derive()` 硬校验 `app.type !== 'admin'` 报错），微信小程序登录链（wechat.md 形态 1 静默 / 形态 2 静默+账密）没有生成器。统一方案：**命令改名 + 配置按 app 类型分叉**。

## 2. 命令

```
pylonts gen admin-login   →   pylonts gen login
```

- cli：`gen-cli.ts` 注册名 `'admin-login'` → `'login'`；`cli/src/admin-login.ts` → `cli/src/login.ts`（runAdminLogin → runLogin）；
- gen：`gen-admin-login.ts` → `gen-login.ts`，导出 `generateAdminLogin` → `generateLogin`；
- **兼容**：`gen admin-login` 保留 alias（一个版本周期）；gen 包 re-export 旧符号 `AdminLoginConfig = AdminLoginAppConfig`、`generateAdminLogin = generateLogin`。

## 3. 配置类型（LoginConfig）

`LoginConfig = Record<string, LoginAppConfig>`——key = app/module 名（project.config.ts app name），结构与现状一致（记录式），值类型按 app 类型分叉。

### 3.1 类型定义（判别联合，password 分支 = Admin 超集）

```ts
/** 公共（所有登录形态） */
export interface LoginBaseConfig {
  table: TableSchema;        // 业务账号表（身份表，必须含 token/refresh_token/login_at 三列，决策 #13）
  token: TokenSchema;        // TokenSchema（identity 锚点，必须投影登录表 PK）
  bootstrapSecret: string;   // 初始密钥方案 2（登录入口无 token 验签）
  refreshKeySalt: string;    // refreshToken 派生密钥盐
}

/** 账密字段（admin 全部；wx password 模式复用——不复制） */
export interface PasswordFieldsConfig {
  usernameField: Field;
  passwordField: Field;
  statusField: Field;
  statusActiveValue: string | number;
  seedUsername?: string;
  seedPassword?: string;
}

/** admin 账密登录（与现状 AdminLoginConfig 字段完全一致） */
export type AdminLoginAppConfig = LoginBaseConfig & PasswordFieldsConfig;

/** wx 特有字段（@pylonts/wechat + {app}_wx 绑定表） */
export interface WxFieldsConfig {
  wxTable: TableSchema;   // {app}_wx 绑定表（appid + openid 联合主键，见 wechat.md §3.1）
  wechat: {
    appId: string;
    appSecret: string;
    mock?: boolean;
  };  // @pylonts/wechat 配置（getAccessToken 本地缓存，无需外部 store，见 wechat.md §4.2）
  sessionKeyEncKey: string;  // session_key 落库加密密钥（AES-256-GCM，merge 进 config.ts auth.token.sessionKeyEncKey）
}

/** wxmini 登录：silent 无账密；password = AdminLoginAppConfig + wx 字段（超集） */
export type WxLoginAppConfig =
  | (LoginBaseConfig & WxFieldsConfig & { mode: 'silent' })
  | (AdminLoginAppConfig & WxFieldsConfig & { mode: 'password' });

/** 匿名签名（MVP 2026-08-19）：只发安全材料 token（{ token, secret }），无身份、无 refreshToken
 *  （token.md #11）。无账户表——不继承 LoginBaseConfig。接口本身用 bootstrap secret 验签
 *  （初始密钥方案 2）。任意 app.type 可配（admin/mobile/wxmini）。 */
export interface SignAppConfig {
  mode: 'sign';
  bootstrapSecret: string;   // 初始密钥方案 2（登录入口无 token 验签）
}

/** 判别联合：derive 分支 switch 后类型自动收窄 */
export type LoginAppConfig = AdminLoginAppConfig | WxLoginAppConfig | SignAppConfig;
export type LoginConfig = Record<string, LoginAppConfig>;
```

**设计要点**：
- **password 分支 = `AdminLoginAppConfig & WxFieldsConfig & { mode }`**——字面即"Admin 的超集"，账密字段不复制、类型上强制完整（usernameField/passwordField/statusField 必填，不可能漏）；
- **silent 分支精确无账密**——静默登录（wechat.md 形态 1）没有用户名密码，类型层不允许填；
- **`mode` 是判别字段**——derive 里 `switch(app.type)` + `switch(mode)` 类型自动收窄，silent 分支访问 `config.usernameField` 直接编译报错；
- **字段存在性不用运行时判断**——判别联合把"哪种形态有哪些字段"钉在类型层。

### 3.2 兼容性

| 维度 | 结论 |
|------|------|
| 配置结构（admin） | **完全兼容**——字段一字不差，现有 `login.config.ts` 无需改动（`satisfies LoginConfig` 自动判定 admin 分支，不需加 mode） |
| `LoginConfig` 类型名 | 保留，`import type { LoginConfig }` 不破 |
| `AdminLoginConfig` 类型名 | 改 `AdminLoginAppConfig`——re-export alias 兼容（一个版本周期） |
| 生成产物（admin 形态） | DTO/Entity/DAO/Service/Controller/seed 结构与现状一致 |
| CLI 命令名 | `gen admin-login` → `gen login`（保留 alias 兼容） |

## 4. 验证规则（按 app 类型分支）

`derive()` 现在硬校验 `app.type !== 'admin'`——改为按 app.type 分支：

| app.type | 允许形态 | 验证要点 |
|----------|---------|---------|
| `admin` | 账密（`AdminLoginAppConfig`）或 `sign` | 账密：现有全量校验（username/password NOT NULL、status enum、statusActiveValue ∈ enum、bootstrapSecret/refreshKeySalt 非空、seed 可选）；sign：仅 bootstrapSecret 非空 |
| `wxmini` | `mode: 'silent'` / `'password'` / `'sign'` | **账密/静默公共**：业务账号表三列（token/refresh_token/login_at）、wxTable 必须为 `{app.name}_wx`（表名 = app name + `_wx`，snake）且含 `appid`+`openid` 联合主键、wechat 配置非空；**password**：复用 admin 账密全量校验（NOT NULL/enum/seed）；**silent**：无账密校验、无 seed；**sign**：无表无 wx 约束（仅 bootstrapSecret 非空） |
| `mobile` | 账密（同 admin，待定）或 `sign` | 同 admin；sign 同上 |

**配置与 app 类型不符 → 定义期报错**（如 wxmini app 配了账密但没 wxTable、admin app 配了 wxTable）。sign 是唯一 app.type 无关形态（`resolveLoginKind` 最先短路返回 `'sign'`）。

## 5. 生成产物差异

| 产物 | admin 账密 | wxmini silent | wxmini password | sign |
|------|-----------|---------------|-----------------|------|
| DTO | LoginRequest{username,password} / RefreshRequest{refreshToken} / LoginResponse{token,refreshToken,secret,user} | LoginRequest{code} | LoginRequest{code,username,password} | SignResponse{token,secret}（无请求体，无 refreshToken） |
| Service.login | 账密校验 + 单点确认 + 发 token（现有逻辑） | code2Session → 查/建 {app}_wx 行 → 单点确认 + 发 token | code2Session + 账密校验 + bind wx + 单点确认 + 发 token | —（只有 sign()） |
| Service.sign | — | — | — | `generateSecret + generateCipher → createToken(app, {secret, cipher}) → { token, secret }`（Redis 对象含 cipher，响应只外发 secret；不 attachIdentity） |
| Service 内部 | 无微信 | `@pylonts/wechat` code2Session + session_key 加密落库（AES-256-GCM） | 同 silent + bcrypt | 无表无 DAO |
| Controller | `@Rpc('login') + @LoginEntry('{app}')` login/refresh | 同 | 同 | `@Rpc('login') + @LoginEntry('{app}')` 仅 sign（无 @Body） |
| Entity / DAO | ✅ | ✅ | ✅ | ❌（无账户表） |
| seed SQL | ✅（初始账号） | 不需要 | 可选 | ❌ |
| config merge | bootstrapSecrets + refreshKeySalt | + sessionKeyEncKey | + sessionKeyEncKey | 仅 bootstrapSecrets（无 refresh/session 配置） |

refresh 三种账密/静默形态一致（token 决策 #11：请求体上送 refreshToken、7 天滑动窗口、每次轮换）；sign 无 refresh（决策 #11：获取 token 接口不返回 refreshToken）。

## 6. 落地步骤（已完成）

| 步骤 | 内容 | 状态 |
|------|------|------|
| 1 | `gen/src/gen-login.ts`：类型重构（LoginBaseConfig / PasswordFieldsConfig / AdminLoginAppConfig / WxFieldsConfig / WxLoginAppConfig / LoginConfig）+ `derive()` 按 app.type 分支验证 | ✅ |
| 2 | 生成器分叉：wxmini 形态产物（LoginRequest{code}/code2Session/session_key 加密落库/bind wx）——消费 `@pylonts/wechat` | ✅ |
| 3 | cli：`gen-cli.ts` 命令改名 + alias；`cli/src/login.ts` | ✅ |
| 4 | 文档同步：onboarding.md 阶段 2（`gen admin-login` → `gen login`）、guide/login.md、wechat.md §9/§10 | ✅ |
| 5 | 测试：gen-login 测试（admin 兼容 / wx silent / wx password / 类型不符报错） | ✅（221 全绿） |
| 6 | sign 形态（MVP 2026-08-19）：SignAppConfig + signDriver（无表无 DAO 无 seed，service 只 createToken + 响应 {token, secret}）+ mergeConfigToken 引号 key 修复 | ✅（225 全绿，含 sign 3 个 + 重跑不重复 key 回归） |

## 7. 边界

- 不做 mobile 静默登录（native app 场景待定，先账密）；
- sign 只做匿名签发（无身份 token），**真实登录（手机号）/ 身份升级 / 静默登录分离** 未做（规划中：真实登录 = 静默身份 + getPhoneNumber 手机号 → 升级 token，`rotateToken` + `attachIdentity` 已具备）；
- 不做 wx 前端客户端生成（gen-client 已支持 loginPath/refreshPath，wx 会话管理由前端模板负责）。