@pylonts/dsl 1.1.16 → 1.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/convert.d.ts +4 -5
- package/dist/dto.d.ts +13 -0
- package/dist/dto.js +28 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mysql-driver.js +3 -3
- package/dist/service.js +3 -0
- package/dist/third-service.d.ts +58 -2
- package/dist/third-service.js +28 -0
- package/dist/token.d.ts +39 -0
- package/dist/token.js +96 -0
- package/dist/typebox-driver.d.ts +13 -0
- package/dist/typebox-driver.js +120 -5
- package/docs/curd.md +150 -146
- package/docs/gen-login.md +136 -0
- package/docs/third-service.md +201 -151
- package/docs/token-migration.md +60 -0
- package/docs/token.md +341 -327
- package/docs/wechat.md +235 -0
- package/package.json +2 -2
- package/src/convert.ts +9 -9
- package/src/dto.ts +364 -331
- package/src/index.ts +1 -0
- package/src/mysql-driver.ts +108 -108
- package/src/service.ts +5 -0
- package/src/third-service.ts +86 -2
- package/src/token.ts +139 -0
- package/src/typebox-driver.ts +128 -6
package/docs/curd.md
CHANGED
|
@@ -1,146 +1,150 @@
|
|
|
1
|
-
# 管理端 CRUD 页面标准(CurdSchema)
|
|
2
|
-
|
|
3
|
-
CurdSchema 是**管理端专用**(`FrontAppSchema.type === 'admin'`)的 CRUD 页面标准:绑定一张实体表 + 一个管理端 app,描述列表页与新增/编辑/详情动作页生成所需的全部页面语义。一条 CurdSchema = 列表页(+ 动作页)的生成规格。
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
**CurdSchema 只依赖 table schema(`Field` 实例),不挂钩 DTO(`DtoMessage`)**——DTO 由生成器按标准从 `columns` 推导。
|
|
8
|
-
|
|
9
|
-
## 定义
|
|
10
|
-
|
|
11
|
-
```ts
|
|
12
|
-
import { defineCurd } from '@pylonts/dsl';
|
|
13
|
-
import { admin } from '../project.config';
|
|
14
|
-
import { order } from '../schema/order.table';
|
|
15
|
-
import { merchant } from '../schema/merchant.table';
|
|
16
|
-
import { orderListFilter } from '../filter_schema/api/admin/filter/order-list.filter';
|
|
17
|
-
|
|
18
|
-
export const orderCurd = defineCurd('order', { // name = table.name 的 kebab(即 admin 路由路径)
|
|
19
|
-
description: '订单管理',
|
|
20
|
-
app: admin, // 所属管理端(project.config.ts 的 FrontAppSchema 共享实例)
|
|
21
|
-
table: order, // 绑定实体表(共享实例)
|
|
22
|
-
title: '订单管理',
|
|
23
|
-
section: '订单管理', // 必填:sidebar 分组名
|
|
24
|
-
actions: [defineAction('EXPORT', '导出订单')], // 额外操作按钮
|
|
25
|
-
actionPages: {
|
|
26
|
-
add: { mode: 'modal', columns: [order.columns.order_no, order.columns.mer_id] },
|
|
27
|
-
update: { mode: 'modal', columns: [order.columns.id, order.columns.order_no] },
|
|
28
|
-
detail: { mode: 'route', columns: [order.columns.id, order.columns.order_no, order.columns.amount] },
|
|
29
|
-
},
|
|
30
|
-
list: {
|
|
31
|
-
columns: [order.columns.id, order.columns.order_no, merchant.columns.name], // 可跨表
|
|
32
|
-
filter: orderListFilter, // 搜索表单 + keyword(FilterSchema 引用,可选)
|
|
33
|
-
orderBy: { column: order.columns.id, direction: 'desc' },
|
|
34
|
-
columnTitles: { order_no: '订单号', name: '商户名称' }, // Field.name → 文案
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
搜索条件**不内联在 list 里**,而是独立的 **FilterSchema**(`defineFilter`)声明,存放在 `filter_schema/{api.name}/{app.name}/filter/`(机器校验:一文件一 filter,文件名 = 名字去 Filter 后缀转 kebab):
|
|
40
|
-
|
|
41
|
-
```ts
|
|
42
|
-
// filter_schema/api/admin/filter/order-list.filter.ts
|
|
43
|
-
import { defineFilter } from '@pylonts/dsl';
|
|
44
|
-
import { admin, api } from '../../../project.config';
|
|
45
|
-
import { order } from '../../../schema/order.table';
|
|
46
|
-
|
|
47
|
-
export const orderListFilter = defineFilter({
|
|
48
|
-
name: 'OrderListFilter',
|
|
49
|
-
api,
|
|
50
|
-
app: admin,
|
|
51
|
-
conditions: [
|
|
52
|
-
{ field: order.columns.status, optional: true }, // op 默认 eq;optional = 有值才加 WHERE
|
|
53
|
-
{ field: order.columns.order_no, op: 'like', optional: true },
|
|
54
|
-
],
|
|
55
|
-
keyword: { columns: [order.columns.order_no] }, // 单输入值多列 OR 模糊
|
|
56
|
-
});
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
生成物为 `{api}/src/modules/{app}/filter/{FilterName}.ts`(两段柯里化 WHERE 拼装方法,DAO/Service 列表查询共用)。
|
|
60
|
-
|
|
61
|
-
## 字段
|
|
62
|
-
|
|
63
|
-
| 字段 | 类型 | 说明 |
|
|
64
|
-
|---|---|---|
|
|
65
|
-
| `app` | `FrontAppSchema` | 所属管理端(共享实例,`type` 必须为 `'admin'`) |
|
|
66
|
-
| `table` | `TableSchema` | 绑定实体表(共享实例) |
|
|
67
|
-
| `title` | `string` | 列表页中文标题 |
|
|
68
|
-
| `section` | `string` | **必填**:sidebar 分组名 |
|
|
69
|
-
| `actions?` | `ActionSchema[]` | 页面额外可执行动作(标准 CRUD 之外,如导出、审核) |
|
|
70
|
-
| `actionPages?` | `{ add? / update? / detail? }` | 动作页:`{ mode: 'modal' \| 'route'; columns: Field[] }` |
|
|
71
|
-
| `list` | `CurdListConfig` | 列表页配置(必填) |
|
|
72
|
-
|
|
73
|
-
### ActionPage
|
|
74
|
-
|
|
75
|
-
| 字段 | 类型 | 说明 |
|
|
76
|
-
|---|---|---|
|
|
77
|
-
| `mode` | `'modal' \| 'route'` | 弹窗或独立路由 |
|
|
78
|
-
| `columns` | `Field[]` | 该页面渲染的字段,**必填非空**——前端要显示的字段必须全部显式列出 |
|
|
79
|
-
|
|
80
|
-
### CurdListConfig
|
|
81
|
-
|
|
82
|
-
| 字段 | 类型 | 说明 |
|
|
83
|
-
|---|---|---|
|
|
84
|
-
| `columns` | `Field[]` | 列表列,**必填非空**;可含跨表字段 |
|
|
85
|
-
| `filter?` | `FilterSchema` | 页面过滤器引用:搜索表单(AND 条件)+ keyword(多列 OR 模糊);缺省 = 无搜索表单 |
|
|
86
|
-
| `orderBy` | `{ column: Field; direction: 'asc' \| 'desc' }` | 默认排序,**必填**,column 与 direction 都必填;column 必须是**本表字段实例** |
|
|
87
|
-
| `columnTitles?` | `Record<string, string>` | 列标题覆盖:`Field.name` → 中文文案 |
|
|
88
|
-
|
|
89
|
-
### FilterSchema(`defineFilter`)
|
|
90
|
-
|
|
91
|
-
| 字段 | 类型 | 说明 |
|
|
92
|
-
|---|---|---|
|
|
93
|
-
| `name` | `string` | PascalCase、`Filter` 结尾;导出名 = name 首字母小写 |
|
|
94
|
-
| `api` | `ProjectApiSchema` | 所属后端 api(project.config.ts 共享实例);`api.apps` 必须包含 `app` |
|
|
95
|
-
| `app` | `FrontAppSchema` | 所属前端 app(共享实例);必须与引用它的 curd 同 app |
|
|
96
|
-
| `conditions?` | `FilterCondition[]` | AND 组合条件:`{ field, op?='eq', right?, optional? }`;`optional: true` = 有值才加 WHERE(页面搜索场景) |
|
|
97
|
-
| `keyword?` | `{ columns: Field[] }` | 单输入值对多列 OR like 模糊;配置后驱动「关键词查询」端点(`query({ keyword })`,供 Select/AutoComplete 搜索) |
|
|
98
|
-
|
|
99
|
-
## 跨表字段
|
|
100
|
-
|
|
101
|
-
`list.columns` 与 filter `conditions` 里的 `Field` 实例可指向**本表或其他表**的列——列表列与搜索条件因此可以显示/过滤关联表字段(如订单列表显示商户名称、按商户名称过滤)。
|
|
102
|
-
|
|
103
|
-
## 默认与校验
|
|
104
|
-
|
|
105
|
-
- `list.columns` / `actionPages.*.columns` **必填非空**(不允许省略、不允许空数组)
|
|
106
|
-
- `list.orderBy` **必填**,`column` 与 `direction` 都必填(规格:默认主键 desc 由定义方显式写出)
|
|
107
|
-
- 运行时校验(`defineCurd`,仿 `defineTable` 强校验风格):
|
|
108
|
-
- `app.type` 必须为 `'admin'`,否则抛错
|
|
109
|
-
- **`name` 必须是 `table.name` 的 kebab 形式**(name 即 admin 路由路径,不允许与所服务的表漂移)
|
|
110
|
-
- `section` 必填
|
|
111
|
-
- 所有 `columns` 非空,否则抛错
|
|
112
|
-
- `list.filter.app` 必须 === `curd.app`,否则抛错
|
|
113
|
-
- `list.orderBy.column` 必须属于 `table`,否则抛错
|
|
114
|
-
- `list.columns` 允许跨表,**不校验归属**
|
|
115
|
-
-
|
|
116
|
-
-
|
|
117
|
-
-
|
|
118
|
-
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
|
129
|
-
|
|
130
|
-
|
|
|
131
|
-
|
|
|
132
|
-
|
|
|
133
|
-
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
|
142
|
-
|
|
143
|
-
| `
|
|
144
|
-
| `
|
|
145
|
-
| `
|
|
146
|
-
|
|
|
1
|
+
# 管理端 CRUD 页面标准(CurdSchema)
|
|
2
|
+
|
|
3
|
+
CurdSchema 是**管理端专用**(`FrontAppSchema.type === 'admin'`)的 CRUD 页面标准:绑定一张实体表 + 一个管理端 app,描述列表页与新增/编辑/详情动作页生成所需的全部页面语义。一条 CurdSchema = 列表页(+ 动作页)的生成规格。
|
|
4
|
+
|
|
5
|
+
页面定义文件按**管理端 app + 实体表**组织:`{project}/curd_schema/{app}/{table}.curd.ts`(一文件一 curd;同一张表可在不同 admin app 下分别 CRUD——curd 的身份是 **(app, table)**,生成物落在 `dto_schema/{app}/` 也按 app 隔离,不会互相撞名)。
|
|
6
|
+
|
|
7
|
+
**CurdSchema 只依赖 table schema(`Field` 实例),不挂钩 DTO(`DtoMessage`)**——DTO 由生成器按标准从 `columns` 推导。
|
|
8
|
+
|
|
9
|
+
## 定义
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { defineCurd } from '@pylonts/dsl';
|
|
13
|
+
import { admin } from '../project.config';
|
|
14
|
+
import { order } from '../schema/order.table';
|
|
15
|
+
import { merchant } from '../schema/merchant.table';
|
|
16
|
+
import { orderListFilter } from '../filter_schema/api/admin/filter/order-list.filter';
|
|
17
|
+
|
|
18
|
+
export const orderCurd = defineCurd('order', { // name = table.name 的 kebab(即 admin 路由路径)
|
|
19
|
+
description: '订单管理',
|
|
20
|
+
app: admin, // 所属管理端(project.config.ts 的 FrontAppSchema 共享实例)
|
|
21
|
+
table: order, // 绑定实体表(共享实例)
|
|
22
|
+
title: '订单管理',
|
|
23
|
+
section: '订单管理', // 必填:sidebar 分组名
|
|
24
|
+
actions: [defineAction('EXPORT', '导出订单')], // 额外操作按钮
|
|
25
|
+
actionPages: {
|
|
26
|
+
add: { mode: 'modal', columns: [order.columns.order_no, order.columns.mer_id] },
|
|
27
|
+
update: { mode: 'modal', columns: [order.columns.id, order.columns.order_no] },
|
|
28
|
+
detail: { mode: 'route', columns: [order.columns.id, order.columns.order_no, order.columns.amount] },
|
|
29
|
+
},
|
|
30
|
+
list: {
|
|
31
|
+
columns: [order.columns.id, order.columns.order_no, merchant.columns.name], // 可跨表
|
|
32
|
+
filter: orderListFilter, // 搜索表单 + keyword(FilterSchema 引用,可选)
|
|
33
|
+
orderBy: { column: order.columns.id, direction: 'desc' },
|
|
34
|
+
columnTitles: { order_no: '订单号', name: '商户名称' }, // Field.name → 文案
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
搜索条件**不内联在 list 里**,而是独立的 **FilterSchema**(`defineFilter`)声明,存放在 `filter_schema/{api.name}/{app.name}/filter/`(机器校验:一文件一 filter,文件名 = 名字去 Filter 后缀转 kebab):
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
// filter_schema/api/admin/filter/order-list.filter.ts
|
|
43
|
+
import { defineFilter } from '@pylonts/dsl';
|
|
44
|
+
import { admin, api } from '../../../project.config';
|
|
45
|
+
import { order } from '../../../schema/order.table';
|
|
46
|
+
|
|
47
|
+
export const orderListFilter = defineFilter({
|
|
48
|
+
name: 'OrderListFilter',
|
|
49
|
+
api,
|
|
50
|
+
app: admin,
|
|
51
|
+
conditions: [
|
|
52
|
+
{ field: order.columns.status, optional: true }, // op 默认 eq;optional = 有值才加 WHERE
|
|
53
|
+
{ field: order.columns.order_no, op: 'like', optional: true },
|
|
54
|
+
],
|
|
55
|
+
keyword: { columns: [order.columns.order_no] }, // 单输入值多列 OR 模糊
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
生成物为 `{api}/src/modules/{app}/filter/{FilterName}.ts`(两段柯里化 WHERE 拼装方法,DAO/Service 列表查询共用)。
|
|
60
|
+
|
|
61
|
+
## 字段
|
|
62
|
+
|
|
63
|
+
| 字段 | 类型 | 说明 |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `app` | `FrontAppSchema` | 所属管理端(共享实例,`type` 必须为 `'admin'`) |
|
|
66
|
+
| `table` | `TableSchema` | 绑定实体表(共享实例) |
|
|
67
|
+
| `title` | `string` | 列表页中文标题 |
|
|
68
|
+
| `section` | `string` | **必填**:sidebar 分组名 |
|
|
69
|
+
| `actions?` | `ActionSchema[]` | 页面额外可执行动作(标准 CRUD 之外,如导出、审核) |
|
|
70
|
+
| `actionPages?` | `{ add? / update? / detail? }` | 动作页:`{ mode: 'modal' \| 'route'; columns: Field[] }` |
|
|
71
|
+
| `list` | `CurdListConfig` | 列表页配置(必填) |
|
|
72
|
+
|
|
73
|
+
### ActionPage
|
|
74
|
+
|
|
75
|
+
| 字段 | 类型 | 说明 |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `mode` | `'modal' \| 'route'` | 弹窗或独立路由 |
|
|
78
|
+
| `columns` | `Field[]` | 该页面渲染的字段,**必填非空**——前端要显示的字段必须全部显式列出 |
|
|
79
|
+
|
|
80
|
+
### CurdListConfig
|
|
81
|
+
|
|
82
|
+
| 字段 | 类型 | 说明 |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `columns` | `Field[]` | 列表列,**必填非空**;可含跨表字段 |
|
|
85
|
+
| `filter?` | `FilterSchema` | 页面过滤器引用:搜索表单(AND 条件)+ keyword(多列 OR 模糊);缺省 = 无搜索表单 |
|
|
86
|
+
| `orderBy` | `{ column: Field; direction: 'asc' \| 'desc' }` | 默认排序,**必填**,column 与 direction 都必填;column 必须是**本表字段实例** |
|
|
87
|
+
| `columnTitles?` | `Record<string, string>` | 列标题覆盖:`Field.name` → 中文文案 |
|
|
88
|
+
|
|
89
|
+
### FilterSchema(`defineFilter`)
|
|
90
|
+
|
|
91
|
+
| 字段 | 类型 | 说明 |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `name` | `string` | PascalCase、`Filter` 结尾;导出名 = name 首字母小写 |
|
|
94
|
+
| `api` | `ProjectApiSchema` | 所属后端 api(project.config.ts 共享实例);`api.apps` 必须包含 `app` |
|
|
95
|
+
| `app` | `FrontAppSchema` | 所属前端 app(共享实例);必须与引用它的 curd 同 app |
|
|
96
|
+
| `conditions?` | `FilterCondition[]` | AND 组合条件:`{ field, op?='eq', right?, optional? }`;`optional: true` = 有值才加 WHERE(页面搜索场景) |
|
|
97
|
+
| `keyword?` | `{ columns: Field[] }` | 单输入值对多列 OR like 模糊;配置后驱动「关键词查询」端点(`query({ keyword })`,供 Select/AutoComplete 搜索) |
|
|
98
|
+
|
|
99
|
+
## 跨表字段
|
|
100
|
+
|
|
101
|
+
`list.columns` 与 filter `conditions` 里的 `Field` 实例可指向**本表或其他表**的列——列表列与搜索条件因此可以显示/过滤关联表字段(如订单列表显示商户名称、按商户名称过滤)。
|
|
102
|
+
|
|
103
|
+
## 默认与校验
|
|
104
|
+
|
|
105
|
+
- `list.columns` / `actionPages.*.columns` **必填非空**(不允许省略、不允许空数组)
|
|
106
|
+
- `list.orderBy` **必填**,`column` 与 `direction` 都必填(规格:默认主键 desc 由定义方显式写出)
|
|
107
|
+
- 运行时校验(`defineCurd`,仿 `defineTable` 强校验风格):
|
|
108
|
+
- `app.type` 必须为 `'admin'`,否则抛错
|
|
109
|
+
- **`name` 必须是 `table.name` 的 kebab 形式**(name 即 admin 路由路径,不允许与所服务的表漂移)
|
|
110
|
+
- `section` 必填
|
|
111
|
+
- 所有 `columns` 非空,否则抛错
|
|
112
|
+
- `list.filter.app` 必须 === `curd.app`,否则抛错
|
|
113
|
+
- `list.orderBy.column` 必须属于 `table`,否则抛错
|
|
114
|
+
- `list.columns` 允许跨表,**不校验归属**
|
|
115
|
+
- 存储校验(`loadCurds`,仿 `loadDaos`/`loadEntities` 机器校验风格):
|
|
116
|
+
- 唯一合法目录是 `curd_schema/{app.name}/`(一级,app 名);`curd.app` 必须是 project.config.ts 共享实例(`type === 'admin'`),且与所在目录一致
|
|
117
|
+
- **文件名 = `{table}.curd.ts`(表名 verbatim**,与 `{table}.entity.ts` / `{table}.dao.ts` 同规)——(app, table) 两维决定 curd 身份,同一张表可在不同 admin app 下合法共存
|
|
118
|
+
- 一文件一 curd(多导出/零导出报错)
|
|
119
|
+
- 运行时校验(`defineFilter`):`api.apps` 包含 `app`;conditions 与 keyword 不能同时为空;keyword.columns 非空
|
|
120
|
+
- 生成时校验(curd 生成器,`DtoSchemaGen.add` / `update`):
|
|
121
|
+
- 表配置 `autoIncrement` 或 `generator`(主键由服务端生成)时,`actionPages.add.columns` **不允许包含主键字段**,否则抛错——AddRequest 不携带服务端生成的主键
|
|
122
|
+
- `actionPages.update.columns` **必须包含主键字段**,否则抛错——UpdateRequest 靠主键定位记录
|
|
123
|
+
|
|
124
|
+
## DTO 推导(生成器约定)
|
|
125
|
+
|
|
126
|
+
DTO 由 curd 生成器从 `CurdSchema` 按标准命名推导,页面语义不持有 DTO 实例:
|
|
127
|
+
|
|
128
|
+
| DTO | 命名 | 字段来源 |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| Row | `{Pascal}Row` | `list.columns` |
|
|
131
|
+
| ListRequest | `{Pascal}ListRequest` | filter 的 conditions(camelCase + op)与 keyword + 分页参数(`PageRequest`,仅 paginated 表) |
|
|
132
|
+
| QueryRequest | `{Pascal}QueryRequest` | filter 的 conditions + keyword,无分页——keyword 查询端点专用(仅配置 keyword 时生成) |
|
|
133
|
+
| ListResponse | `{Pascal}ListResponse` | `PageResult(Row)`(仅 paginated 表;非分页表列表接口直接返回 `Row[]`,不生成 ListResponse) |
|
|
134
|
+
| AddRequest | `{Pascal}AddRequest` | `actionPages.add.columns` |
|
|
135
|
+
| UpdateRequest | `{Pascal}UpdateRequest` | `actionPages.update.columns` |
|
|
136
|
+
| DetailRequest | `{Pascal}DetailRequest` | 主键 |
|
|
137
|
+
| DetailResponse | `{Pascal}DetailResponse` | `actionPages.detail.columns` |
|
|
138
|
+
|
|
139
|
+
## 与旧 PageConfig 的差异
|
|
140
|
+
|
|
141
|
+
| PageConfig(旧方案,已废弃) | CurdSchema |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `module: string` | 由 `app` 推导(后端模块 == app 1:1) |
|
|
144
|
+
| `schema: 'bd'` 字符串 | `table: TableSchema` 实例(类型安全) |
|
|
145
|
+
| `operations: { label, action }` | `actions: ActionSchema[]` |
|
|
146
|
+
| `detail.mode` 单例 | `actionPages.detail.mode` |
|
|
147
|
+
| `forms.add / forms.update` | `actionPages.add / actionPages.update` |
|
|
148
|
+
| `keyword` / `orderBy` / `columnTitles` | `list.filter`(FilterSchema)/ `list.orderBy` / `list.columnTitles` |
|
|
149
|
+
| `naming` | 去掉(DTO 命名是生成器约定,非页面语义) |
|
|
150
|
+
| DTO 引用(`request` / `fields` / `DtoFields`) | 去掉(DTO 由生成器推导,页面只依赖 table) |
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# gen-login 生成器设计(登录链统一)
|
|
2
|
+
|
|
3
|
+
> 状态:**已实现(2026-08-19)**
|
|
4
|
+
> 关联文档:[token.md](./token.md)(token 令牌体系,底座)、[wechat.md](./wechat.md)(微信小程序登录体系,消费本设计)
|
|
5
|
+
> 定位:把 `gen admin-login`(仅 admin 账密)升级为 `gen login`——按 app 类型(admin / wxmini / mobile)分叉生成登录链,读取同一份 `login.config.ts`,每个 app 一份配置。
|
|
6
|
+
|
|
7
|
+
## 1. 背景与动机
|
|
8
|
+
|
|
9
|
+
现状 `gen admin-login` 只服务 admin 类型 app(`derive()` 硬校验 `app.type !== 'admin'` 报错),微信小程序登录链(wechat.md 形态 1 静默 / 形态 2 静默+账密)没有生成器。统一方案:**命令改名 + 配置按 app 类型分叉**。
|
|
10
|
+
|
|
11
|
+
## 2. 命令
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
pylonts gen admin-login → pylonts gen login
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- cli:`gen-cli.ts` 注册名 `'admin-login'` → `'login'`;`cli/src/admin-login.ts` → `cli/src/login.ts`(runAdminLogin → runLogin);
|
|
18
|
+
- gen:`gen-admin-login.ts` → `gen-login.ts`,导出 `generateAdminLogin` → `generateLogin`;
|
|
19
|
+
- **兼容**:`gen admin-login` 保留 alias(一个版本周期);gen 包 re-export 旧符号 `AdminLoginConfig = AdminLoginAppConfig`、`generateAdminLogin = generateLogin`。
|
|
20
|
+
|
|
21
|
+
## 3. 配置类型(LoginConfig)
|
|
22
|
+
|
|
23
|
+
`LoginConfig = Record<string, LoginAppConfig>`——key = app/module 名(project.config.ts app name),结构与现状一致(记录式),值类型按 app 类型分叉。
|
|
24
|
+
|
|
25
|
+
### 3.1 类型定义(判别联合,password 分支 = Admin 超集)
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
/** 公共(所有登录形态) */
|
|
29
|
+
export interface LoginBaseConfig {
|
|
30
|
+
table: TableSchema; // 业务账号表(身份表,必须含 token/refresh_token/login_at 三列,决策 #13)
|
|
31
|
+
token: TokenSchema; // TokenSchema(identity 锚点,必须投影登录表 PK)
|
|
32
|
+
bootstrapSecret: string; // 初始密钥方案 2(登录入口无 token 验签)
|
|
33
|
+
refreshKeySalt: string; // refreshToken 派生密钥盐
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 账密字段(admin 全部;wx password 模式复用——不复制) */
|
|
37
|
+
export interface PasswordFieldsConfig {
|
|
38
|
+
usernameField: Field;
|
|
39
|
+
passwordField: Field;
|
|
40
|
+
statusField: Field;
|
|
41
|
+
statusActiveValue: string | number;
|
|
42
|
+
seedUsername?: string;
|
|
43
|
+
seedPassword?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** admin 账密登录(与现状 AdminLoginConfig 字段完全一致) */
|
|
47
|
+
export type AdminLoginAppConfig = LoginBaseConfig & PasswordFieldsConfig;
|
|
48
|
+
|
|
49
|
+
/** wx 特有字段(@pylonts/wechat + {app}_wx 绑定表) */
|
|
50
|
+
export interface WxFieldsConfig {
|
|
51
|
+
wxTable: TableSchema; // {app}_wx 绑定表(appid + openid 联合主键,见 wechat.md §3.1)
|
|
52
|
+
wechat: {
|
|
53
|
+
appId: string;
|
|
54
|
+
appSecret: string;
|
|
55
|
+
mock?: boolean;
|
|
56
|
+
}; // @pylonts/wechat 配置(getAccessToken 本地缓存,无需外部 store,见 wechat.md §4.2)
|
|
57
|
+
sessionKeyEncKey: string; // session_key 落库加密密钥(AES-256-GCM,merge 进 config.ts auth.token.sessionKeyEncKey)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** wxmini 登录:silent 无账密;password = AdminLoginAppConfig + wx 字段(超集) */
|
|
61
|
+
export type WxLoginAppConfig =
|
|
62
|
+
| (LoginBaseConfig & WxFieldsConfig & { mode: 'silent' })
|
|
63
|
+
| (AdminLoginAppConfig & WxFieldsConfig & { mode: 'password' });
|
|
64
|
+
|
|
65
|
+
/** 匿名签名(MVP 2026-08-19):只发安全材料 token({ token, secret }),无身份、无 refreshToken
|
|
66
|
+
* (token.md #11)。无账户表——不继承 LoginBaseConfig。接口本身用 bootstrap secret 验签
|
|
67
|
+
* (初始密钥方案 2)。任意 app.type 可配(admin/mobile/wxmini)。 */
|
|
68
|
+
export interface SignAppConfig {
|
|
69
|
+
mode: 'sign';
|
|
70
|
+
bootstrapSecret: string; // 初始密钥方案 2(登录入口无 token 验签)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 判别联合:derive 分支 switch 后类型自动收窄 */
|
|
74
|
+
export type LoginAppConfig = AdminLoginAppConfig | WxLoginAppConfig | SignAppConfig;
|
|
75
|
+
export type LoginConfig = Record<string, LoginAppConfig>;
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**设计要点**:
|
|
79
|
+
- **password 分支 = `AdminLoginAppConfig & WxFieldsConfig & { mode }`**——字面即"Admin 的超集",账密字段不复制、类型上强制完整(usernameField/passwordField/statusField 必填,不可能漏);
|
|
80
|
+
- **silent 分支精确无账密**——静默登录(wechat.md 形态 1)没有用户名密码,类型层不允许填;
|
|
81
|
+
- **`mode` 是判别字段**——derive 里 `switch(app.type)` + `switch(mode)` 类型自动收窄,silent 分支访问 `config.usernameField` 直接编译报错;
|
|
82
|
+
- **字段存在性不用运行时判断**——判别联合把"哪种形态有哪些字段"钉在类型层。
|
|
83
|
+
|
|
84
|
+
### 3.2 兼容性
|
|
85
|
+
|
|
86
|
+
| 维度 | 结论 |
|
|
87
|
+
|------|------|
|
|
88
|
+
| 配置结构(admin) | **完全兼容**——字段一字不差,现有 `login.config.ts` 无需改动(`satisfies LoginConfig` 自动判定 admin 分支,不需加 mode) |
|
|
89
|
+
| `LoginConfig` 类型名 | 保留,`import type { LoginConfig }` 不破 |
|
|
90
|
+
| `AdminLoginConfig` 类型名 | 改 `AdminLoginAppConfig`——re-export alias 兼容(一个版本周期) |
|
|
91
|
+
| 生成产物(admin 形态) | DTO/Entity/DAO/Service/Controller/seed 结构与现状一致 |
|
|
92
|
+
| CLI 命令名 | `gen admin-login` → `gen login`(保留 alias 兼容) |
|
|
93
|
+
|
|
94
|
+
## 4. 验证规则(按 app 类型分支)
|
|
95
|
+
|
|
96
|
+
`derive()` 现在硬校验 `app.type !== 'admin'`——改为按 app.type 分支:
|
|
97
|
+
|
|
98
|
+
| app.type | 允许形态 | 验证要点 |
|
|
99
|
+
|----------|---------|---------|
|
|
100
|
+
| `admin` | 账密(`AdminLoginAppConfig`)或 `sign` | 账密:现有全量校验(username/password NOT NULL、status enum、statusActiveValue ∈ enum、bootstrapSecret/refreshKeySalt 非空、seed 可选);sign:仅 bootstrapSecret 非空 |
|
|
101
|
+
| `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 非空) |
|
|
102
|
+
| `mobile` | 账密(同 admin,待定)或 `sign` | 同 admin;sign 同上 |
|
|
103
|
+
|
|
104
|
+
**配置与 app 类型不符 → 定义期报错**(如 wxmini app 配了账密但没 wxTable、admin app 配了 wxTable)。sign 是唯一 app.type 无关形态(`resolveLoginKind` 最先短路返回 `'sign'`)。
|
|
105
|
+
|
|
106
|
+
## 5. 生成产物差异
|
|
107
|
+
|
|
108
|
+
| 产物 | admin 账密 | wxmini silent | wxmini password | sign |
|
|
109
|
+
|------|-----------|---------------|-----------------|------|
|
|
110
|
+
| DTO | LoginRequest{username,password} / RefreshRequest{refreshToken} / LoginResponse{token,refreshToken,secret,user} | LoginRequest{code} | LoginRequest{code,username,password} | SignResponse{token,secret}(无请求体,无 refreshToken) |
|
|
111
|
+
| Service.login | 账密校验 + 单点确认 + 发 token(现有逻辑) | code2Session → 查/建 {app}_wx 行 → 单点确认 + 发 token | code2Session + 账密校验 + bind wx + 单点确认 + 发 token | —(只有 sign()) |
|
|
112
|
+
| Service.sign | — | — | — | `generateSecret + generateCipher → createToken(app, {secret, cipher}) → { token, secret }`(Redis 对象含 cipher,响应只外发 secret;不 attachIdentity) |
|
|
113
|
+
| Service 内部 | 无微信 | `@pylonts/wechat` code2Session + session_key 加密落库(AES-256-GCM) | 同 silent + bcrypt | 无表无 DAO |
|
|
114
|
+
| Controller | `@Rpc('login') + @LoginEntry('{app}')` login/refresh | 同 | 同 | `@Rpc('login') + @LoginEntry('{app}')` 仅 sign(无 @Body) |
|
|
115
|
+
| Entity / DAO | ✅ | ✅ | ✅ | ❌(无账户表) |
|
|
116
|
+
| seed SQL | ✅(初始账号) | 不需要 | 可选 | ❌ |
|
|
117
|
+
| config merge | bootstrapSecrets + refreshKeySalt | + sessionKeyEncKey | + sessionKeyEncKey | 仅 bootstrapSecrets(无 refresh/session 配置) |
|
|
118
|
+
|
|
119
|
+
refresh 三种账密/静默形态一致(token 决策 #11:请求体上送 refreshToken、7 天滑动窗口、每次轮换);sign 无 refresh(决策 #11:获取 token 接口不返回 refreshToken)。
|
|
120
|
+
|
|
121
|
+
## 6. 落地步骤(已完成)
|
|
122
|
+
|
|
123
|
+
| 步骤 | 内容 | 状态 |
|
|
124
|
+
|------|------|------|
|
|
125
|
+
| 1 | `gen/src/gen-login.ts`:类型重构(LoginBaseConfig / PasswordFieldsConfig / AdminLoginAppConfig / WxFieldsConfig / WxLoginAppConfig / LoginConfig)+ `derive()` 按 app.type 分支验证 | ✅ |
|
|
126
|
+
| 2 | 生成器分叉:wxmini 形态产物(LoginRequest{code}/code2Session/session_key 加密落库/bind wx)——消费 `@pylonts/wechat` | ✅ |
|
|
127
|
+
| 3 | cli:`gen-cli.ts` 命令改名 + alias;`cli/src/login.ts` | ✅ |
|
|
128
|
+
| 4 | 文档同步:onboarding.md 阶段 2(`gen admin-login` → `gen login`)、guide/login.md、wechat.md §9/§10 | ✅ |
|
|
129
|
+
| 5 | 测试:gen-login 测试(admin 兼容 / wx silent / wx password / 类型不符报错) | ✅(221 全绿) |
|
|
130
|
+
| 6 | sign 形态(MVP 2026-08-19):SignAppConfig + signDriver(无表无 DAO 无 seed,service 只 createToken + 响应 {token, secret})+ mergeConfigToken 引号 key 修复 | ✅(225 全绿,含 sign 3 个 + 重跑不重复 key 回归) |
|
|
131
|
+
|
|
132
|
+
## 7. 边界
|
|
133
|
+
|
|
134
|
+
- 不做 mobile 静默登录(native app 场景待定,先账密);
|
|
135
|
+
- sign 只做匿名签发(无身份 token),**真实登录(手机号)/ 身份升级 / 静默登录分离** 未做(规划中:真实登录 = 静默身份 + getPhoneNumber 手机号 → 升级 token,`rotateToken` + `attachIdentity` 已具备);
|
|
136
|
+
- 不做 wx 前端客户端生成(gen-client 已支持 loginPath/refreshPath,wx 会话管理由前端模板负责)。
|