adspecs 0.1.32 → 0.1.34

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.
Files changed (25) hide show
  1. package/.adspecs/paths.json +1 -1
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.codebuddy-plugin/plugin.json +1 -1
  5. package/.qoder-plugin/plugin.json +1 -1
  6. package/.workbuddy-plugin/plugin.json +1 -1
  7. package/CLAUDE.md +2 -2
  8. package/README.md +137 -108
  9. package/package.json +1 -1
  10. package/references/ant6-front-standard/02-/347/273/204/344/273/266/350/247/204/350/214/203.md +7 -0
  11. package/references/ant6-front-standard/03-/345/210/227/350/241/250/350/247/204/350/214/203.md +222 -147
  12. package/references/ant6-front-standard/04-/350/241/250/345/215/225/350/247/204/350/214/203.md +8 -0
  13. package/references/ant6-front-standard/09-/345/270/270/350/247/201/351/227/256/351/242/230/350/247/204/350/214/203.md +1 -1
  14. package/references/ant6-front-standard/index.md +100 -99
  15. package/references/yudaocloud-end-standard/01-Java/345/220/216/347/253/257/347/274/226/347/240/201/350/247/204/350/214/203.md +1166 -0
  16. package/references/yudaocloud-end-standard/02-/346/225/260/346/215/256/345/272/223/350/256/276/350/256/241/344/270/216/344/275/277/347/224/250/350/247/204/350/214/203.md +1023 -0
  17. package/references/yudaocloud-end-standard/03-/346/225/260/346/215/256/345/255/227/345/205/270/344/270/216/350/217/234/345/215/225/350/247/204/350/214/203.md +336 -0
  18. package/references/yudaocloud-end-standard/index.md +14 -3
  19. package/references/yudaocloud-end-standard/system_dict_type.sql +186 -0
  20. package/skills/adspecs-plan/SKILL.md +1 -1
  21. package/skills/adspecs-utest/SKILL.md +62 -40
  22. package/skills/project-init/SKILL.md +4 -4
  23. package/src/lib/paths-defaults.js +1 -1
  24. package/src/lib/readme-gen.js +1 -1
  25. package/references/ant6-front-standard/05-/345/211/215/347/253/257/347/274/226/347/240/201/350/247/204/350/214/203.md +0 -1443
@@ -0,0 +1,1023 @@
1
+ # PostgreSQL 数据库设计与使用规范
2
+
3
+ > **版本**: v2.0
4
+ > **修订日期**: 2026-07-27
5
+ > **适用范围**: ultracloud 项目所有后端模块
6
+ > **目标数据库**: PostgreSQL 16+(主用),兼容 MySQL 8.0+(备用)
7
+
8
+ 本规范基于 PostgreSQL 特性编写,结合项目 MyBatis Plus 3.5.16 + Druid 1.2.28 + Spring Boot 4.1.0 技术栈,可直接落地。
9
+
10
+ ---
11
+
12
+ ## 1. SQL 建表规范
13
+
14
+ 生成的 `*-schema.sql` 文件必须遵循以下约定。
15
+
16
+ ### 1.1 文件头
17
+
18
+ ```sql
19
+ -- {spec标题} — 数据库表结构
20
+ -- 数据库: PostgreSQL 16+
21
+ -- 生成日期: YYYY-MM-DD
22
+ -- 模块: sie-module-{module}
23
+
24
+ SET client_encoding = 'UTF8';
25
+ SET standard_conforming_strings = ON;
26
+ ```
27
+
28
+ > **差异说明**:
29
+ > - PostgreSQL 无需 `SET FOREIGN_KEY_CHECKS`,无 `ENGINE`/`CHARSET`/`COLLATE` 表级选项
30
+ > - `standard_conforming_strings = ON` 为 PG 9.1+ 默认值,显式声明以确保反斜杠不被转义;若脚本需兼容旧版 PG,此项必写
31
+
32
+ ### 1.2 表命名
33
+
34
+ - 表名使用 `{module-prefix}_{entity_name}` 格式,如 `mdm_customer`、`mdm_ctct`
35
+ - 模块前缀约定:MDM=mdm, System=system, Infra=infra, BPM=bpm, AI=ai
36
+ - 表名全小写,单词间用下划线分隔
37
+ - **不使用双引号包裹表名**(PostgreSQL 中双引号区分大小写,保持一致的小写无引号风格)
38
+
39
+ ### 1.3 字段规范
40
+
41
+ | 类别 | MySQL 写法 | PostgreSQL 写法 | 说明 |
42
+ |------|-----------|----------------|------|
43
+ | 主键 | `BIGINT NOT NULL AUTO_INCREMENT` | `BIGSERIAL PRIMARY KEY` | 或使用 `GENERATED BY DEFAULT AS IDENTITY` |
44
+ | 布尔值 | `BIT(1) NOT NULL DEFAULT 0` | `BOOLEAN NOT NULL DEFAULT FALSE` | 使用原生 BOOLEAN(逻辑删除 `deleted` 例外,用 `SMALLINT`,见 §1.5) |
45
+ | 枚举/状态 | `VARCHAR` / `TINYINT` | `VARCHAR` / `SMALLINT` | 不使用 PostgreSQL ENUM 类型(不便修改) |
46
+ | 金额/精度 | `DECIMAL(M,N)` | `NUMERIC(M,N)` | `NUMERIC` 为 PG 标准写法(`DECIMAL` 为其别名) |
47
+ | JSON 字段 | `JSON` | `JSONB` | 使用 JSONB(二进制存储,支持索引与运算) |
48
+ | 时间 | `DATETIME` | `TIMESTAMP` | 与 Java `LocalDateTime` 对应;如需时区用 `TIMESTAMPTZ` |
49
+ | 文本 | `TEXT` / `VARCHAR` | `TEXT` / `VARCHAR` | 一致;PG 中 `TEXT` 与 `VARCHAR` 性能无差异 |
50
+ | 大文本 | `TEXT` / `LONGTEXT` | `TEXT` | PG 的 `TEXT` 无长度限制,无需区分 |
51
+ | 整数 | `INT` / `BIGINT` | `INTEGER` / `BIGINT` | PG 标准写法 |
52
+ | 短整数 | `TINYINT` | `SMALLINT` | PG 无 `TINYINT`,最小整数类型为 `SMALLINT`(2 字节) |
53
+ | 字节 | `BLOB` / `LONGBLOB` | `BYTEA` | 二进制数据存储 |
54
+
55
+ **通用字段规范**(MySQL / PostgreSQL 共同):
56
+
57
+ - **BIGINT**: 所有 ID/外键字段统一使用 `BIGINT`(Snowflake 主键约定)
58
+ - **每列必须带 `COMMENT`**,说明字段含义(PostgreSQL 通过独立的 `COMMENT ON` 语句实现)
59
+ - **不可空字段**: `NOT NULL DEFAULT` 给出合理默认值
60
+ - **可空字段**: `DEFAULT NULL`
61
+ - **JSON 字段**: 不允许有默认值,全部默认为 `NULL`
62
+
63
+ ### 1.4 注释规范(PostgreSQL 专属)
64
+
65
+ PostgreSQL 的列注释和表注释通过独立语句完成,**不内联在 CREATE TABLE 中**:
66
+
67
+ ```sql
68
+ -- 表注释
69
+ COMMENT ON TABLE mdm_customer IS '客户主数据表';
70
+
71
+ -- 列注释(建表语句之后逐一添加)
72
+ COMMENT ON COLUMN mdm_customer.id IS '主键ID';
73
+ COMMENT ON COLUMN mdm_customer.code IS '客户编码';
74
+ COMMENT ON COLUMN mdm_customer.name IS '客户名称';
75
+ COMMENT ON COLUMN mdm_customer.status IS '状态(0=草稿 1=已审核 2=已冻结)';
76
+ COMMENT ON COLUMN mdm_customer.ext_json IS '扩展属性(JSONB)';
77
+ ```
78
+
79
+ > **工具辅助**:可使用 `psql` 的 `\d` 命令查看注释;也可在 IDE(DBeaver/Navicat)中直接浏览。
80
+ > **批量生成建议**:建表脚本末尾集中写所有 `COMMENT ON` 语句,便于维护。
81
+
82
+ ### 1.5 审计字段(必须包含)
83
+
84
+ ```sql
85
+ -- TenantBaseDO 实体(绝大多数业务表):
86
+ tenant_id BIGINT NOT NULL DEFAULT 0,
87
+ creator VARCHAR(64) DEFAULT '',
88
+ create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
89
+ updater VARCHAR(64) DEFAULT '',
90
+ update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
91
+ deleted SMALLINT NOT NULL DEFAULT 0,
92
+ deleted_time TIMESTAMP DEFAULT NULL,
93
+
94
+ -- BaseDO 实体(关联表/版本表等非租户表): 去掉 tenant_id,其余相同
95
+ ```
96
+
97
+ **列注释**:
98
+
99
+ ```sql
100
+ COMMENT ON COLUMN {table}.tenant_id IS '租户ID';
101
+ COMMENT ON COLUMN {table}.creator IS '创建者';
102
+ COMMENT ON COLUMN {table}.create_time IS '创建时间';
103
+ COMMENT ON COLUMN {table}.updater IS '更新者';
104
+ COMMENT ON COLUMN {table}.update_time IS '更新时间';
105
+ COMMENT ON COLUMN {table}.deleted IS '逻辑删除标志(0=未删除 1=已删除)';
106
+ COMMENT ON COLUMN {table}.deleted_time IS '逻辑删除时间';
107
+ ```
108
+
109
+ > **与 MySQL 的关键差异**:
110
+ > - `deleted` 使用 `SMALLINT`(0=未删除,1=已删除),**禁止使用 `BOOLEAN`**。原因:`BaseDO.deleted` 为 `Boolean` 类型,MyBatis-Plus 全局配置 `logic-not-delete-value=0`(整数),生成 SQL `WHERE deleted = 0`。PostgreSQL 中 `smallint = integer` 可隐式转换(正常),但 `boolean = integer` 不可转换(报错"操作符不存在: boolean = integer")。系统表 `system_dict_type` 即用 `SMALLINT`,以此为统一标准。
111
+ > - 必须配套 `deleted_time TIMESTAMP DEFAULT NULL` 字段,记录逻辑删除发生时间
112
+ > - `update_time` 默认值仅设 `CURRENT_TIMESTAMP`(PG 无 `ON UPDATE CURRENT_TIMESTAMP`,由应用层 `DefaultDBFieldHandler` 自动填充,或可选启用数据库触发器——见 §1.10)
113
+ > - `TIMESTAMP` 对应 Java `LocalDateTime`;如需带时区使用 `TIMESTAMPTZ`
114
+ >
115
+ > ⚠️ **重要**:真布尔字段(如 `visible`/`keep_alive`/`always_show`/`is_primary`)仍使用 `BOOLEAN` 类型。仅逻辑删除标志 `deleted` 例外,使用 `SMALLINT`。
116
+
117
+ ### 1.6 主键与 ID 生成策略
118
+
119
+ **建表 SQL** 使用 `BIGSERIAL`(PostgreSQL 伪类型,自动创建 `SEQUENCE`):
120
+
121
+ ```sql
122
+ CREATE TABLE mdm_customer (
123
+ id BIGSERIAL PRIMARY KEY,
124
+ -- ...
125
+ );
126
+ ```
127
+
128
+ **Java DO 实体** 对应写法:
129
+
130
+ ```java
131
+ @TableId(type = IdType.AUTO)
132
+ private Long id;
133
+ ```
134
+
135
+ > **说明**:`BIGSERIAL` 等价于 `BIGINT NOT NULL DEFAULT nextval('table_id_seq')`,MyBatis Plus 的 `IdType.AUTO` 可正确识别并通过 JDBC `getGeneratedKeys` 取回自增值。
136
+ >
137
+ > **Snowflake 场景**:如使用雪花 ID(由应用层分配),建表改用普通 `BIGINT PRIMARY KEY`,不设 `BIGSERIAL`,`@TableId(type = IdType.ASSIGN_ID)`。
138
+
139
+ ### 1.7 N:N 关系表
140
+
141
+ - **无独立 ID 字段**,使用复合主键
142
+ - 不包含审计字段(纯关系映射表)
143
+ - 额外关联属性(如 `clause_code`、`is_primary`)作为主键组成部分或普通字段
144
+
145
+ ```sql
146
+ CREATE TABLE mdm_ctct_standard_ref (
147
+ ctct_id BIGINT NOT NULL,
148
+ standard_id BIGINT NOT NULL,
149
+ clause_code VARCHAR(64) NOT NULL DEFAULT '',
150
+ is_primary BOOLEAN NOT NULL DEFAULT FALSE,
151
+ PRIMARY KEY (ctct_id, standard_id, clause_code)
152
+ );
153
+
154
+ COMMENT ON TABLE mdm_ctct_standard_ref IS 'CTCT-标准引用关系表';
155
+ COMMENT ON COLUMN mdm_ctct_standard_ref.ctct_id IS 'CTCT ID';
156
+ COMMENT ON COLUMN mdm_ctct_standard_ref.standard_id IS '标准ID';
157
+ COMMENT ON COLUMN mdm_ctct_standard_ref.clause_code IS '条款号';
158
+ COMMENT ON COLUMN mdm_ctct_standard_ref.is_primary IS '是否主要引用';
159
+
160
+ -- 索引(见 §1.8 命名规范)
161
+ CREATE INDEX idx_ctct_ref_ctct_id ON mdm_ctct_standard_ref (ctct_id);
162
+ CREATE INDEX idx_ctct_ref_standard_id ON mdm_ctct_standard_ref (standard_id);
163
+ ```
164
+
165
+ ### 1.8 索引规范
166
+
167
+ #### 1.8.1 索引命名
168
+
169
+ | 索引类型 | 命名格式 | 示例 |
170
+ |---------|---------|------|
171
+ | 主键 | `pk_{table}` 或直接 `PRIMARY KEY` | 内建 |
172
+ | 唯一索引 | `uk_{table}_{field}` | `uk_customer_code` |
173
+ | 复合唯一 | `uk_{table}_{field1}_{field2}` | `uk_ctct_version` |
174
+ | 普通索引 | `idx_{table}_{field}` | `idx_customer_status` |
175
+
176
+ #### 1.8.2 索引创建语法
177
+
178
+ PostgreSQL 的索引通过独立 `CREATE INDEX` 语句创建(不在 CREATE TABLE 内部):
179
+
180
+ ```sql
181
+ -- 唯一索引(全表唯一,不含逻辑删除条件)
182
+ CREATE UNIQUE INDEX uk_customer_code ON mdm_customer (code);
183
+
184
+ -- 普通索引
185
+ CREATE INDEX idx_customer_status ON mdm_customer (status);
186
+
187
+ -- 复合索引
188
+ CREATE INDEX idx_customer_group_status ON mdm_customer (customer_group, status);
189
+
190
+ -- 降序索引
191
+ CREATE INDEX idx_customer_create_time ON mdm_customer (create_time DESC);
192
+ ```
193
+
194
+ > **注意**:上述 `uk_customer_code` 为全表唯一约束。逻辑删除场景下应改用部分索引(见 §1.8.3),避免已删除记录占用唯一空间。
195
+
196
+ #### 1.8.3 部分索引(PostgreSQL 特有)
197
+
198
+ 利用 `WHERE` 子句仅索引满足条件的行,大幅减小索引体积:
199
+
200
+ ```sql
201
+ -- 仅索引未删除的记录(逻辑删除场景首选)
202
+ CREATE UNIQUE INDEX uk_customer_code_active ON mdm_customer (code) WHERE deleted = 0;
203
+
204
+ -- 仅索引特定状态的记录
205
+ CREATE INDEX idx_order_pending ON biz_order (create_time) WHERE status = 'PENDING';
206
+ ```
207
+
208
+ > **最佳实践**:对逻辑删除的表,唯一约束应使用部分索引 `WHERE deleted = 0`,避免已删除记录的编码占用唯一空间。
209
+
210
+ #### 1.8.4 并发创建索引(生产环境必用)
211
+
212
+ ```sql
213
+ -- 生产环境在线加索引,避免锁表
214
+ CREATE INDEX CONCURRENTLY idx_customer_status ON mdm_customer (status);
215
+ ```
216
+
217
+ > **注意**:`CONCURRENTLY` 不能与 `CREATE INDEX` 放在事务块中,且不支持 `IF NOT EXISTS`。
218
+
219
+ #### 1.8.5 覆盖索引(INCLUDE)
220
+
221
+ PostgreSQL 11+ 支持 `INCLUDE` 子句,将非索引列包含进叶子节点以支持 Index-Only Scan:
222
+
223
+ ```sql
224
+ CREATE INDEX idx_order_status ON biz_order (status) INCLUDE (id, order_no, amount);
225
+ ```
226
+
227
+ ### 1.9 不使用外键约束
228
+
229
+ - 本项目不使用数据库 `FOREIGN KEY`,关联关系在代码层面维护
230
+ - PostgreSQL 中同样不添加外键约束,与 MySQL 规范保持一致
231
+
232
+ ### 1.10 表尾与 updateTime 自动更新
233
+
234
+ #### 1.10.1 应用层更新(默认方案)
235
+
236
+ 项目的 `DefaultDBFieldHandler`(MyBatis Plus `MetaObjectHandler` 实现)已在应用层自动填充 `update_time`,**无需数据库触发器**。此方案适用于所有环境,为默认选择。
237
+
238
+ #### 1.10.2 数据库触发器(可选方案)
239
+
240
+ 如需在直接操作数据库(如运维脚本、数据迁移)时也能自动更新 `update_time`,可创建触发器:
241
+
242
+ ```sql
243
+ -- 通用触发器函数(建一次,所有表复用)
244
+ CREATE OR REPLACE FUNCTION fn_update_timestamp()
245
+ RETURNS TRIGGER AS $$
246
+ BEGIN
247
+ NEW.update_time := CURRENT_TIMESTAMP;
248
+ RETURN NEW;
249
+ END;
250
+ $$ LANGUAGE plpgsql;
251
+
252
+ -- 在目标表上绑定触发器
253
+ CREATE TRIGGER trg_mdm_customer_update
254
+ BEFORE UPDATE ON mdm_customer
255
+ FOR EACH ROW
256
+ EXECUTE FUNCTION fn_update_timestamp();
257
+ ```
258
+
259
+ > **语法说明**:PL/pgSQL 中赋值使用 `:=`(PG 14+ 也支持 `=`,但 `:=` 为传统写法,可读性更好)。
260
+
261
+ > **使用建议**:开发/测试环境依赖应用层即可;生产环境如 DBA 要求数据库层面保障,可按需添加触发器。
262
+
263
+ ### 1.11 完整建表示例
264
+
265
+ ```sql
266
+ -- 客户主数据表 — 数据库表结构
267
+ -- 数据库: PostgreSQL 16+
268
+ -- 生成日期: 2026-07-27
269
+ -- 模块: sie-module-mdm
270
+
271
+ SET client_encoding = 'UTF8';
272
+ SET standard_conforming_strings = ON;
273
+
274
+ -- ========== 表结构 ==========
275
+ CREATE TABLE mdm_customer (
276
+ id BIGSERIAL PRIMARY KEY,
277
+ code VARCHAR(64) NOT NULL DEFAULT '',
278
+ name VARCHAR(255) NOT NULL DEFAULT '',
279
+ customer_group VARCHAR(32) NOT NULL DEFAULT '',
280
+ lifecycle_status VARCHAR(32) NOT NULL DEFAULT 'DRAFT',
281
+ ext_json JSONB,
282
+ tenant_id BIGINT NOT NULL DEFAULT 0,
283
+ creator VARCHAR(64) DEFAULT '',
284
+ create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
285
+ updater VARCHAR(64) DEFAULT '',
286
+ update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
287
+ deleted SMALLINT NOT NULL DEFAULT 0,
288
+ deleted_time TIMESTAMP DEFAULT NULL
289
+ );
290
+
291
+ -- ========== 注释 ==========
292
+ COMMENT ON TABLE mdm_customer IS '客户主数据表';
293
+ COMMENT ON COLUMN mdm_customer.id IS '主键ID';
294
+ COMMENT ON COLUMN mdm_customer.code IS '客户编码';
295
+ COMMENT ON COLUMN mdm_customer.name IS '客户名称';
296
+ COMMENT ON COLUMN mdm_customer.customer_group IS '客户分组';
297
+ COMMENT ON COLUMN mdm_customer.lifecycle_status IS '生命周期状态';
298
+ COMMENT ON COLUMN mdm_customer.ext_json IS '扩展属性(JSONB)';
299
+ COMMENT ON COLUMN mdm_customer.tenant_id IS '租户ID';
300
+ COMMENT ON COLUMN mdm_customer.creator IS '创建者';
301
+ COMMENT ON COLUMN mdm_customer.create_time IS '创建时间';
302
+ COMMENT ON COLUMN mdm_customer.updater IS '更新者';
303
+ COMMENT ON COLUMN mdm_customer.update_time IS '更新时间';
304
+ COMMENT ON COLUMN mdm_customer.deleted IS '逻辑删除标志';
305
+
306
+ -- ========== 索引 ==========
307
+ -- 部分唯一索引:仅未删除记录参与唯一约束(见 §1.8.3)
308
+ CREATE UNIQUE INDEX uk_customer_code_active ON mdm_customer (code) WHERE deleted = 0;
309
+ CREATE INDEX idx_customer_tenant_status ON mdm_customer (tenant_id, lifecycle_status);
310
+ CREATE INDEX idx_customer_group ON mdm_customer (customer_group);
311
+ CREATE INDEX idx_customer_create_time ON mdm_customer (create_time DESC);
312
+
313
+ -- ========== JSONB GIN 索引(对 ext_json 做键值查询加速,见 §2.3)==========
314
+ CREATE INDEX idx_customer_ext_json ON mdm_customer USING GIN (ext_json);
315
+ ```
316
+
317
+ ---
318
+
319
+ ## 2. JSONB 使用规范(PostgreSQL 专属)
320
+
321
+ PostgreSQL 的 `JSONB` 类型相比 MySQL 的 `JSON`,具备索引、键路径查询、包含运算等强大能力,应充分利用。
322
+
323
+ ### 2.1 类型选择
324
+
325
+ | 场景 | 类型 | 说明 |
326
+ |------|------|------|
327
+ | 结构化半透明数据 | `JSONB` | 支持索引与运算,写入稍慢但读取快 |
328
+ | 原始 JSON 文档(不查询) | `JSON` | 保留原始格式,仅存储用(极少使用) |
329
+ | **本项目统一选择** | **`JSONB`** | 全部替换 MySQL `JSON` |
330
+
331
+ ### 2.2 字段规范
332
+
333
+ - JSONB 字段**不允许设置默认值**,统一 `DEFAULT NULL`
334
+ - 列名建议使用 `ext_json`、`config_json`、`metadata` 等语义化命名
335
+ - 必须创建 GIN 索引(如需按 key 查询)
336
+
337
+ ### 2.3 GIN 索引
338
+
339
+ ```sql
340
+ -- 全局 GIN 索引:支持 @>(包含)、?(键存在)、?|、?& 运算
341
+ CREATE INDEX idx_{table}_{col}_gin ON {table} USING GIN ({column});
342
+
343
+ -- 路径_ops 索引:支持 jsonb_path_ops(仅支持 @>,索引更小更快)
344
+ CREATE INDEX idx_{table}_{col}_path ON {table} USING GIN ({column} jsonb_path_ops);
345
+ ```
346
+
347
+ ### 2.4 常用查询方式(MyBatis Plus / XML Mapper)
348
+
349
+ ```sql
350
+ -- 包含运算:ext_json 包含 {"industry": "AUTOMOTIVE"}
351
+ SELECT * FROM mdm_customer WHERE ext_json @> '{"industry": "AUTOMOTIVE"}';
352
+
353
+ -- 键存在判断(⚠️ 在 MyBatis XML Mapper 中 `?` 会与 JDBC 参数占位符冲突,改用函数形式)
354
+ -- 原生 SQL:SELECT * FROM mdm_customer WHERE ext_json ? 'certifications';
355
+ -- MyBatis XML 写法:
356
+ SELECT * FROM mdm_customer WHERE jsonb_exists(ext_json, 'certifications');
357
+
358
+ -- 提取值(-> 返回 JSONB,->> 返回 TEXT)
359
+ SELECT ext_json->>'industry' AS industry FROM mdm_customer;
360
+
361
+ -- 嵌套路径提取(#>> 取文本)
362
+ SELECT ext_json #>> '{address,city}' AS city FROM mdm_customer;
363
+
364
+ -- jsonb_path_query(SQL/JSON Path 语法,PG 12+)
365
+ SELECT * FROM mdm_customer
366
+ WHERE jsonb_path_match(ext_json, '$.tags[*] == "VIP"');
367
+ ```
368
+
369
+ > **MyBatis XML 中的 JSONB 运算符映射**:
370
+ >
371
+ > | 原生运算符 | MyBatis XML 函数形式 | 说明 |
372
+ > |-----------|---------------------|------|
373
+ > | `?` | `jsonb_exists(col, key)` | 键存在判断 |
374
+ > | `?\|` | `jsonb_exists_any(col, array)` | 任一键存在 |
375
+ > | `?&` | `jsonb_exists_all(col, array)` | 全部键存在 |
376
+ > | `@>` | `@>` 可直接使用 | 包含运算,不与 JDBC 冲突 |
377
+ > | `->>` | `->>` 可直接使用 | 文本提取 |
378
+
379
+ ### 2.5 Java 类型映射
380
+
381
+ 在 DO 实体中使用 `String` 或 `Map<String, Object>`,配合 MyBatis Plus 的 `JacksonTypeHandler`:
382
+
383
+ ```java
384
+ @TableName(value = "mdm_customer", autoResultMap = true)
385
+ public class MdmCustomerDO extends TenantBaseDO {
386
+
387
+ // 方式一:String(自行序列化/反序列化)
388
+ private String extJson;
389
+
390
+ // 方式二:Map(框架自动 Jackson 序列化,推荐)
391
+ @TableField(typeHandler = JacksonTypeHandler.class)
392
+ private Map<String, Object> extJson;
393
+ }
394
+ ```
395
+
396
+ > **重要**:使用 `typeHandler` 时,DO 类必须加 `@TableName(autoResultMap = true)`。
397
+
398
+ ---
399
+
400
+ ## 3. PostgreSQL 特有功能使用规范
401
+
402
+ ### 3.1 数组类型
403
+
404
+ 对于简单的标签、ID 列表等场景,可使用 PostgreSQL 原生数组(替代 MySQL 的逗号分隔字符串或关联表):
405
+
406
+ ```sql
407
+ -- 标签数组
408
+ tags TEXT[] NOT NULL DEFAULT '{}',
409
+
410
+ -- 整数数组
411
+ related_org_ids BIGINT[] NOT NULL DEFAULT '{}',
412
+ ```
413
+
414
+ Java 映射(MyBatis Plus 需自定义 TypeHandler 或使用框架内置 `ArrayTypeHandler`):
415
+
416
+ ```java
417
+ @TableField(typeHandler = ArrayTypeHandler.class)
418
+ private List<String> tags;
419
+ ```
420
+
421
+ > **使用边界**:仅在不需要对数组元素做独立索引/外键关联时使用。频繁按元素查询的场景应拆为子表。
422
+
423
+ ### 3.2 全文搜索(简要)
424
+
425
+ PostgreSQL 内建全文搜索能力,无需额外组件(轻量场景替代 Elasticsearch):
426
+
427
+ ```sql
428
+ -- 添加 tsvector 生成列
429
+ ALTER TABLE mdm_customer ADD COLUMN search_tsv TSVECTOR
430
+ GENERATED ALWAYS AS (
431
+ to_tsvector('simple', coalesce(name, '') || ' ' || coalesce(code, ''))
432
+ ) STORED;
433
+
434
+ -- GIN 索引加速搜索
435
+ CREATE INDEX idx_customer_search ON mdm_customer USING GIN (search_tsv);
436
+
437
+ -- 查询
438
+ SELECT * FROM mdm_customer WHERE search_tsv @@ plainto_tsquery('simple', '华东客户');
439
+ ```
440
+
441
+ > **适用场景**:中小型数据量(<500 万行)的模糊搜索;大数据量仍建议 Elasticsearch。
442
+
443
+ ### 3.3 RETURNING 子句
444
+
445
+ PostgreSQL 的 `INSERT ... RETURNING` / `UPDATE ... RETURNING` 可在写操作后直接返回任意列值,无需额外查询:
446
+
447
+ ```sql
448
+ -- 插入后返回生成的 ID 和创建时间
449
+ INSERT INTO mdm_customer (code, name) VALUES ('C001', '华东客户')
450
+ RETURNING id, create_time;
451
+
452
+ -- 更新后返回修改后的完整行
453
+ UPDATE mdm_customer SET name = '新客户' WHERE id = 1
454
+ RETURNING id, code, name, update_time;
455
+
456
+ -- 批量插入返回
457
+ INSERT INTO mdm_customer (code, name) VALUES ('C001', 'A'), ('C002', 'B')
458
+ RETURNING id, code;
459
+ ```
460
+
461
+ > **与 MySQL 的差异**:MySQL 的 JDBC `getGeneratedKeys()` 仅能获取自增主键;PostgreSQL 的 `RETURNING` 可返回表中任意列,省去回查开销。
462
+ >
463
+ > **MyBatis Plus 已内建支持**:`@TableId(type = IdType.AUTO)` + `BIGSERIAL` 通过 JDBC `getGeneratedKeys` 自动获取主键。手写 SQL 时可在 XML Mapper 中利用 `RETURNING` 返回非主键字段,进一步优化批量操作。
464
+
465
+ ### 3.4 UPSERT(ON CONFLICT)
466
+
467
+ PostgreSQL 的 `INSERT ... ON CONFLICT` 实现 upsert 语义:
468
+
469
+ ```sql
470
+ -- 场景一:唯一索引为全表唯一(普通唯一约束)
471
+ INSERT INTO mdm_customer (code, name, tenant_id)
472
+ VALUES ('C001', '华东客户', 1)
473
+ ON CONFLICT (code)
474
+ DO UPDATE SET name = EXCLUDED.name, update_time = CURRENT_TIMESTAMP;
475
+
476
+ -- 场景二:唯一索引为部分索引(WHERE deleted = 0)
477
+ -- 注意:ON CONFLICT 推断部分索引需要 PostgreSQL 15+(本项目 PG 16+ 已满足)
478
+ INSERT INTO mdm_customer (code, name, tenant_id)
479
+ VALUES ('C001', '华东客户', 1)
480
+ ON CONFLICT (code) WHERE deleted = 0
481
+ DO UPDATE SET name = EXCLUDED.name, update_time = CURRENT_TIMESTAMP;
482
+ ```
483
+
484
+ ### 3.5 部分索引与覆盖索引
485
+
486
+ 见 §1.8.3 和 §1.8.5。核心原则:
487
+
488
+ - **部分索引**:对逻辑删除表,唯一索引加 `WHERE deleted = 0`
489
+ - **覆盖索引**:高频查询的字段通过 `INCLUDE` 放入索引叶子,避免回表
490
+
491
+ ---
492
+
493
+ ## 4. 查询优化规范
494
+
495
+ ### 4.1 分页查询优化
496
+
497
+ **深分页问题**:`OFFSET N` 性能随偏移量线性下降,大偏移量分页使用 Keyset Pagination:
498
+
499
+ ```sql
500
+ -- 传统分页(偏移量大时慢)
501
+ SELECT * FROM mdm_customer ORDER BY id LIMIT 20 OFFSET 10000;
502
+
503
+ -- Keyset 分页(推荐,性能恒定)
504
+ SELECT * FROM mdm_customer WHERE id > {last_id} ORDER BY id LIMIT 20;
505
+ ```
506
+
507
+ Java 层实现:在 `PageReqVO` 中增加 `minId` 字段,Service 层根据上一页最后一条记录的 ID 构造查询条件。
508
+
509
+ ### 4.2 COUNT 优化
510
+
511
+ 对于逻辑删除表的全表 COUNT,可使用部分索引加速:
512
+
513
+ ```sql
514
+ -- 创建计数用部分索引
515
+ CREATE INDEX idx_customer_active ON mdm_customer (id) WHERE deleted = 0;
516
+
517
+ -- 查询活跃记录数(走部分索引)
518
+ SELECT count(*) FROM mdm_customer WHERE deleted = 0;
519
+ ```
520
+
521
+ ### 4.3 EXISTS 与 IN 的选择
522
+
523
+ 对于子查询判断存在性,优先使用 `EXISTS`(语义更清晰,大子查询集性能更稳定):
524
+
525
+ ```sql
526
+ -- 推荐:语义清晰,不受子查询结果集大小影响
527
+ SELECT * FROM mdm_customer c
528
+ WHERE EXISTS (SELECT 1 FROM mdm_ctct ct WHERE ct.customer_id = c.id);
529
+
530
+ -- 可接受:PG 优化器会自动转为 Semi-Join,性能通常与 EXISTS 等价
531
+ SELECT * FROM mdm_customer
532
+ WHERE id IN (SELECT customer_id FROM mdm_ctct);
533
+ ```
534
+
535
+ > **PG 优化器说明**:现代 PostgreSQL(12+)对 `IN (subquery)` 会自动转换为 Semi-Join,大多数场景下性能与 `EXISTS` 无差异。但当子查询结果集极大、且主表较小时,`EXISTS` 短路求值更稳定。建议统一使用 `EXISTS` 保持代码可读性。
536
+
537
+ ### 4.4 批量操作
538
+
539
+ 批量 INSERT 使用多行 VALUES 或 `COPY`(大数据量导入):
540
+
541
+ ```sql
542
+ -- 多行 VALUES(常规批量,建议单批 ≤ 1000 行)
543
+ INSERT INTO mdm_customer (code, name) VALUES
544
+ ('C001', '客户A'),
545
+ ('C002', '客户B'),
546
+ ('C003', '客户C');
547
+
548
+ -- COPY(大数据量导入,CSV 格式)
549
+ COPY mdm_customer (code, name, tenant_id) FROM '/data/import.csv' WITH (FORMAT csv, HEADER true);
550
+ ```
551
+
552
+ > **MyBatis Plus 的 `insertBatch`** 已自动使用多行 VALUES 方式,默认批量大小可通过配置调整。
553
+
554
+ ### 4.5 EXPLAIN 分析
555
+
556
+ 所有复杂查询必须用 `EXPLAIN (ANALYZE, BUFFERS)` 验证执行计划:
557
+
558
+ ```sql
559
+ EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
560
+ SELECT * FROM mdm_customer WHERE lifecycle_status = 'APPROVED' ORDER BY create_time DESC LIMIT 20;
561
+ ```
562
+
563
+ 关注指标:
564
+ - **Seq Scan**(全表扫描):大表出现时为性能问题
565
+ - **Index Scan / Index Only Scan**:理想执行方式
566
+ - **Buffers: shared hit**:缓存命中率,越高越好
567
+ - **Actual Time**:实际执行时间(ms)
568
+
569
+ ---
570
+
571
+ ## 5. 数据字典规范
572
+
573
+ ### 5.1 目标表
574
+
575
+ 数据字典由两张表组成:
576
+
577
+ **字典类型表** `system_dict_type`(字典分组/分类):
578
+ `id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`
579
+
580
+ **字典数据表** `system_dict_data`(字典具体选项):
581
+ `id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`
582
+
583
+ > **插入顺序要求**:必须先向 `system_dict_type` 插入字典类型记录,再向 `system_dict_data` 插入对应的字典数据。`system_dict_data.dict_type` 的值必须与 `system_dict_type.type` 的值一致。
584
+
585
+ ### 5.2 字典类型表(system_dict_type)
586
+
587
+ | 字段 | 类型 | 规则 |
588
+ |------|------|------|
589
+ | `id` | BIGINT | 自增主键 |
590
+ | `name` | VARCHAR | 字典类型的中文名称,如 "客户状态" |
591
+ | `type` | VARCHAR | 字典类型的英文标识,如 `customer_status`,必须全小写下划线分隔 |
592
+ | `status` | SMALLINT | `0`=启用,`1`=禁用(对应 `CommonStatusEnum`) |
593
+ | `remark` | VARCHAR | 备注说明 |
594
+
595
+ ### 5.3 字典数据表(system_dict_data)
596
+
597
+ | 字段 | 类型 | 规则 |
598
+ |------|------|------|
599
+ | `id` | BIGINT | 自增主键 |
600
+ | `sort` | INTEGER | 显示顺序,越小越靠前;实际数据中以 `1` 为步长递增 |
601
+ | `label` | VARCHAR | 显示标签,如 "待审核" |
602
+ | `value` | VARCHAR | 实际存储值,对应 Java Enum 的 `name()` 值或数值字符串 |
603
+ | `dict_type` | VARCHAR | 所属字典类型标识,必须与 `system_dict_type.type` 一致 |
604
+ | `status` | SMALLINT | `0`=启用,`1`=禁用 |
605
+ | `color_type` | VARCHAR | 标签颜色类型,见下方颜色约定 |
606
+ | `css_class` | VARCHAR | 自定义 CSS 类名,通常为空字符串 `''` |
607
+
608
+ ### 5.4 字典类型覆盖范围
609
+
610
+ 必须覆盖 spec 中定义的所有枚举类型,包括:
611
+ - 业务状态枚举(如 `customer_status`、`lifecycle_status`)
612
+ - 分类枚举(如 `customer_group`、`csr_type`、`pcr_type`)
613
+ - 行为/动作枚举(如 `csr_trigger_action`、`delivery_pattern`)
614
+ - 配置枚举(如 `ppap_level`、`contract_type`、`sync_scope`)
615
+
616
+ ### 5.5 颜色类型约定
617
+
618
+ | 场景 | color_type |
619
+ |------|-----------|
620
+ | 成功/启用/生效 | `success` |
621
+ | 警告/待处理 | `warning` |
622
+ | 危险/禁止/失效 | `danger` |
623
+ | 普通/信息 | `info` |
624
+ | 主要业务标识 | `primary` |
625
+ | 默认/无特殊含义 | `default` |
626
+
627
+ ### 5.6 字典值约定
628
+
629
+ - 布尔型枚举:`0`/`1`
630
+ - 枚举字符串值:使用 Java Enum 的 `name()` 值(如 `AUTOMOTIVE`、`DRAFT`、`INSPECT`)
631
+ - 数值型枚举:使用数值字符串(如 `1`~`5` 对应 PPAP 等级)
632
+
633
+ ### 5.7 示例 SQL
634
+
635
+ > **重要**:必须先插入 `system_dict_type`,再插入 `system_dict_data`。
636
+
637
+ ```sql
638
+ -- 步骤1: 先创建字典类型
639
+ INSERT INTO system_dict_type (name, type, status, remark, creator, create_time, updater, update_time, deleted)
640
+ VALUES ('客户状态', 'customer_status', 0, 'MDM 客户生命周期状态', '1', CURRENT_TIMESTAMP, '1', CURRENT_TIMESTAMP, 0);
641
+
642
+ INSERT INTO system_dict_type (name, type, status, remark, creator, create_time, updater, update_time, deleted)
643
+ VALUES ('客户分组', 'customer_group', 0, 'MDM 客户分类分组', '1', CURRENT_TIMESTAMP, '1', CURRENT_TIMESTAMP, 0);
644
+
645
+ -- 步骤2: 再插入字典数据
646
+ INSERT INTO system_dict_data (sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted)
647
+ VALUES (1, '草稿', 'DRAFT', 'customer_status', 0, 'info', '', '未提交审核', '1', CURRENT_TIMESTAMP, '1', CURRENT_TIMESTAMP, 0);
648
+
649
+ INSERT INTO system_dict_data (sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted)
650
+ VALUES (2, '已审核', 'APPROVED', 'customer_status', 0, 'success', '', '审核通过', '1', CURRENT_TIMESTAMP, '1', CURRENT_TIMESTAMP, 0);
651
+
652
+ INSERT INTO system_dict_data (sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted)
653
+ VALUES (3, '已冻结', 'FROZEN', 'customer_status', 0, 'danger', '', '禁止交易', '1', CURRENT_TIMESTAMP, '1', CURRENT_TIMESTAMP, 0);
654
+ ```
655
+
656
+ ### 5.8 注意事项
657
+
658
+ - **插入顺序**:必须先插入 `system_dict_type`,再插入 `system_dict_data`,否则字典数据无类型归属
659
+ - `system_dict_data.dict_type` 必须与 `system_dict_type.type` 严格一致
660
+ - 同一 `dict_type` 下的 `value` 值不可重复
661
+ - `dict_type` 命名采用全小写 + 下划线分隔格式,如 `customer_status`
662
+ - 字典数据表同样使用 `BaseDO` 审计字段,不含 `tenant_id`
663
+
664
+ ---
665
+
666
+ ## 6. 菜单数据规范
667
+
668
+ ### 6.1 菜单表结构
669
+
670
+ 数据写入 `system_menu` 表(系统内置菜单权限表),表结构为:
671
+ `id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`
672
+
673
+ 该表使用 `BaseDO` 审计字段,**不包含 `tenant_id`**(菜单为全局共享数据,通过 `@TenantIgnore` 注解忽略租户隔离)。
674
+
675
+ ### 6.2 菜单类型(type)
676
+
677
+ | type 值 | 含义 | 说明 | 必填字段 |
678
+ |---------|------|------|---------|
679
+ | 1 | 目录 | 一级/二级导航分组,无实际页面 | `name`, `path`, `icon`, `sort`, `parent_id` |
680
+ | 2 | 菜单 | 实际可访问的页面 | `name`, `path`, `icon`, `component`, `sort`, `parent_id` |
681
+ | 3 | 按钮 | 页面内的操作按钮,仅用于权限控制 | `name`, `permission`, `parent_id` |
682
+
683
+ ### 6.3 字段填写规则
684
+
685
+ | 字段 | 类型 | 规则 |
686
+ |------|------|------|
687
+ | `id` | BIGINT | 自增主键;新模块菜单 ID 从 `6000` 起始分配 |
688
+ | `name` | VARCHAR | 菜单/目录/按钮的中文显示名称 |
689
+ | `permission` | VARCHAR | 权限标识,格式 `${system}:${resource}:${action}` |
690
+ | `type` | SMALLINT | `1`=目录,`2`=菜单,`3`=按钮 |
691
+ | `sort` | INTEGER | 同级排序值,越小越靠前 |
692
+ | `parent_id` | BIGINT | 父菜单 ID,根目录为 `0` |
693
+ | `path` | VARCHAR | 路由路径;目录/菜单必填 |
694
+ | `icon` | VARCHAR | 图标名称,使用 `ep:` 或 `fa:` 前缀 |
695
+ | `component` | VARCHAR | 前端组件路径;仅菜单类型必填 |
696
+ | `component_name` | VARCHAR | 组件名称(用于 keep-alive 缓存) |
697
+ | `status` | SMALLINT | `0`=启用,`1`=禁用 |
698
+ | `visible` | BOOLEAN | `TRUE`=侧边栏可见,`FALSE`=隐藏 |
699
+ | `keep_alive` | BOOLEAN | `TRUE`=缓存页面状态,`FALSE`=不缓存;默认 `TRUE` |
700
+ | `always_show` | BOOLEAN | `TRUE`=始终显示父级,`FALSE`=单子菜单时折叠;默认 `TRUE` |
701
+
702
+ ### 6.4 目录结构层级
703
+
704
+ ```text
705
+ 目录 (type=1, parent_id=0) ← 一级导航,如 "主数据管理"
706
+ └─ 目录 (type=1, parent_id=父ID) ← 二级分组(可选),如 "客户管理"
707
+ └─ 菜单 (type=2, parent_id=父ID) ← 实际页面,如 "客户列表"
708
+ ├─ 按钮 (type=3, parent_id=菜单ID) ← "客户创建"
709
+ ├─ 按钮 (type=3, parent_id=菜单ID) ← "客户修改"
710
+ └─ 按钮 (type=3, parent_id=菜单ID) ← "客户删除"
711
+ ```
712
+
713
+ ### 6.5 权限标识命名规则
714
+
715
+ | 操作 | permission 示例 |
716
+ |------|----------------|
717
+ | 查询 | `mdm:customer:query` |
718
+ | 创建 | `mdm:customer:create` |
719
+ | 更新 | `mdm:customer:update` |
720
+ | 删除 | `mdm:customer:delete` |
721
+ | 导出 | `mdm:customer:export` |
722
+ | 导入 | `mdm:customer:import` |
723
+
724
+ ### 6.6 示例 SQL
725
+
726
+ ```sql
727
+ -- MDM 模块 - 主数据管理目录
728
+ INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
729
+ VALUES (5000, '主数据管理', '', 1, 10, 0, '/mdm', 'ep:menu', NULL, NULL, 0, TRUE, TRUE, TRUE, '1', CURRENT_TIMESTAMP, '1', CURRENT_TIMESTAMP, 0);
730
+
731
+ -- 客户管理菜单
732
+ INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
733
+ VALUES (5010, '客户管理', 'mdm:customer:query', 2, 10, 5000, 'customer', 'ep:user', 'mdm/customer/index', 'MdmCustomer', 0, TRUE, TRUE, TRUE, '1', CURRENT_TIMESTAMP, '1', CURRENT_TIMESTAMP, 0);
734
+
735
+ -- 客户创建按钮
736
+ INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, component_name, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
737
+ VALUES (5011, '客户创建', 'mdm:customer:create', 3, 1, 5010, '', '#', NULL, NULL, 0, TRUE, TRUE, TRUE, '1', CURRENT_TIMESTAMP, '1', CURRENT_TIMESTAMP, 0);
738
+ ```
739
+
740
+ ### 6.7 注意事项
741
+
742
+ - 新模块菜单 ID 范围需在模块设计文档中提前约定,避免与已有模块冲突
743
+ - 按钮类型(type=3)的 `path`、`icon`、`component` 字段必须为 NULL 或空字符串
744
+ - 目录类型(type=1)的 `component` 字段必须为 NULL
745
+ - 删除菜单时需先检查 `system_role_menu` 中是否有关联数据
746
+ - 菜单数据通过 `@TenantIgnore` 全局共享,不随租户隔离
747
+
748
+ ---
749
+
750
+ ## 7. 连接池与持久层配置规范
751
+
752
+ ### 7.1 Druid 连接池(PostgreSQL)
753
+
754
+ ```yaml
755
+ spring:
756
+ datasource:
757
+ druid:
758
+ driver-class-name: org.postgresql.Driver
759
+ url: jdbc:postgresql://localhost:5432/ultracloud?currentSchema=public&stringtype=unspecified
760
+ username: ultracloud
761
+ password: ${DB_PASSWORD}
762
+ # 连接池参数
763
+ initial-size: 5
764
+ min-idle: 10
765
+ max-active: 50
766
+ max-wait: 600000
767
+ # 检测配置
768
+ validation-query: SELECT 1
769
+ test-while-idle: true
770
+ test-on-borrow: false
771
+ test-on-return: false
772
+ time-between-eviction-runs-millis: 60000
773
+ min-evictable-idle-time-millis: 300000
774
+ ```
775
+
776
+ > **关键差异**:
777
+ > - `driver-class-name` 使用 `org.postgresql.Driver`
778
+ > - URL 协议为 `jdbc:postgresql://`
779
+ > - `validation-query` 使用 `SELECT 1`(与 MySQL 相同,PG 也支持)
780
+ > - `currentSchema=public` 指定默认 schema(多 schema 场景必配)
781
+ > - **`stringtype=unspecified`**(关键参数):MyBatis 向 PG 传递参数时,默认会将 `String` 类型标记为 `varchar`。当目标列类型为 `uuid`、`jsonb`、`timestamp` 等非字符串类型时,PG 严格模式下会报 `column is of type X but expression is of type varchar` 错误。设置 `stringtype=unspecified` 后,PG 会自动做隐式类型转换,避免此类问题。
782
+
783
+ ### 7.2 MyBatis Plus PostgreSQL 配置
784
+
785
+ ```yaml
786
+ mybatis-plus:
787
+ configuration:
788
+ # PostgreSQL 的 mapUnderscoreToCamelCase 默认开启
789
+ map-underscore-to-camel-case: true
790
+ # 日志(开发环境)
791
+ log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
792
+ global-config:
793
+ db-config:
794
+ # ID 策略:AUTO 对应 BIGSERIAL,ASSIGN_ID 对应雪花
795
+ id-type: auto
796
+ # 逻辑删除(SMALLINT 0/1 映射,与系统表 system_dict_type 一致)
797
+ # BaseDO.deleted 为 Boolean 类型,配合整数 0/1:
798
+ # 生成 SQL WHERE deleted = 0 / SET deleted = 1
799
+ # SMALLINT 列 smallint = integer 可隐式转换(PostgreSQL 支持)
800
+ # 禁止用 BOOLEAN 列,否则报错"操作符不存在: boolean = integer"
801
+ logic-delete-field: deleted
802
+ logic-delete-value: 1
803
+ logic-not-delete-value: 0
804
+ # 表前缀(按模块配置)
805
+ table-prefix: ''
806
+ # 类型处理器(JSONB 支持)
807
+ type-handlers-package: cn.ultracloud.com.framework.mybatis.core.handler
808
+ ```
809
+
810
+ > **逻辑删除配置**:`logic-delete-value: 1` / `logic-not-delete-value: 0` 对应 `SMALLINT` 类型(0/1 整数),与项目系统表 `system_dict_type` 一致。**禁止将 `deleted` 列定义为 `BOOLEAN`**——`BaseDO.deleted` 为 `Boolean` 类型但逻辑删除值为整数,PostgreSQL 不支持 `boolean = integer` 隐式转换,会报错"操作符不存在: boolean = integer"。
811
+
812
+ ### 7.3 application.yml 多环境 Profile
813
+
814
+ ```yaml
815
+ # application-dev.yml(开发环境)
816
+ spring:
817
+ datasource:
818
+ druid:
819
+ url: jdbc:postgresql://localhost:5432/ultracloud_dev?currentSchema=public&stringtype=unspecified
820
+
821
+ # application-unit-test.yml(单元测试环境)
822
+ # 使用 Testcontainers 动态启动 PostgreSQL 容器(见 §8)
823
+ ```
824
+
825
+ ---
826
+
827
+ ## 8. 单元测试数据库规范
828
+
829
+ ### 8.1 测试数据库选型
830
+
831
+ | 方案 | 说明 | 推荐场景 |
832
+ |------|------|---------|
833
+ | **Testcontainers PostgreSQL** | 真实 PG 容器,与生产一致 | **首选方案** |
834
+ | H2(PostgreSQL 兼容模式) | 内存运行,速度快但功能不全 | 简单单元测试 |
835
+ | 嵌入式 PostgreSQL(embedded-postgres) | 进程内 PG,无需 Docker | 无 Docker 环境 |
836
+
837
+ ### 8.2 Testcontainers 方案(推荐)
838
+
839
+ ```java
840
+ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
841
+ @ActiveProfiles("unit-test")
842
+ @Testcontainers
843
+ class WmsWarehouseServiceImplTest {
844
+
845
+ @Container
846
+ static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
847
+ .withDatabaseName("ultracloud_test")
848
+ .withUsername("test")
849
+ .withPassword("test");
850
+
851
+ @DynamicPropertySource
852
+ static void configureProperties(DynamicPropertyRegistry registry) {
853
+ registry.add("spring.datasource.url", postgres::getJdbcUrl);
854
+ registry.add("spring.datasource.username", postgres::getUsername);
855
+ registry.add("spring.datasource.password", postgres::getPassword);
856
+ }
857
+
858
+ @Resource
859
+ private WmsWarehouseServiceImpl warehouseService;
860
+
861
+ @Test
862
+ void testCreateWarehouse_success() {
863
+ // ...
864
+ }
865
+ }
866
+ ```
867
+
868
+ ### 8.3 测试 SQL 脚本
869
+
870
+ - 创建表脚本:`src/test/resources/sql/create_tables.sql`(使用 PostgreSQL 语法)
871
+ - 清理脚本:`src/test/resources/sql/clean.sql`(每个测试方法执行后运行)
872
+ - 脚本中使用 `BIGSERIAL`、`BOOLEAN`、`TIMESTAMP` 等 PG 类型
873
+
874
+ ---
875
+
876
+ ## 9. 性能优化与运维规范
877
+
878
+ ### 9.1 VACUUM 与 ANALYZE
879
+
880
+ PostgreSQL 使用 MVCC,更新/删除产生"死元组",需定期清理:
881
+
882
+ ```sql
883
+ -- 手动分析(更新统计信息,优化器使用)
884
+ ANALYZE mdm_customer;
885
+
886
+ -- 手动清理(回收死元组空间)
887
+ VACUUM ANALYZE mdm_customer;
888
+
889
+ -- 强制回收磁盘空间(会排他锁表,生产慎用)
890
+ VACUUM FULL mdm_customer;
891
+ ```
892
+
893
+ > **建议**:开启 `autovacuum`(默认开启),对高频更新表调整 `autovacuum_vacuum_scale_factor` 为更小值(如 `0.01`)。
894
+
895
+ ### 9.2 连接池监控
896
+
897
+ 通过 Druid Monitor 或 Spring Boot Actuator 监控:
898
+
899
+ | 指标 | 说明 | 告警阈值 |
900
+ |------|------|---------|
901
+ | `activeCount` | 活跃连接数 | > `max-active * 0.8` |
902
+ | `waitThreadCount` | 等待连接的线程数 | > 0 |
903
+ | `errorCount` | SQL 执行错误数 | > 0 |
904
+ | `executeMillisMax` | 最慢 SQL 执行时间(ms) | > 1000 |
905
+
906
+ ### 9.3 慢查询定位
907
+
908
+ ```sql
909
+ -- 查看当前正在执行的慢查询(> 1s)
910
+ SELECT pid, now() - pg_stat_activity.query_start AS duration, query, state
911
+ FROM pg_stat_activity
912
+ WHERE (now() - pg_stat_activity.query_start) > interval '1 second'
913
+ AND state != 'idle';
914
+
915
+ -- 取消慢查询
916
+ SELECT pg_cancel_backend(<pid>);
917
+ ```
918
+
919
+ ### 9.4 常用维护 SQL
920
+
921
+ ```sql
922
+ -- 查看表大小(含索引)
923
+ SELECT pg_size_pretty(pg_total_relation_size('mdm_customer'));
924
+
925
+ -- 查看表行数估算(比 COUNT 快得多)
926
+ SELECT reltuples::bigint AS estimated_rows
927
+ FROM pg_class WHERE relname = 'mdm_customer';
928
+
929
+ -- 查看索引使用情况
930
+ SELECT schemaname, tablename, indexname, idx_scan, idx_tup_read, idx_tup_fetch
931
+ FROM pg_stat_user_indexes
932
+ ORDER BY idx_scan ASC;
933
+
934
+ -- 查看未使用的索引(idx_scan = 0)
935
+ SELECT schemaname, tablename, indexname
936
+ FROM pg_stat_user_indexes
937
+ WHERE idx_scan = 0;
938
+ ```
939
+
940
+ ---
941
+
942
+ ## 10. 数据库迁移规范
943
+
944
+ ### 10.1 迁移工具
945
+
946
+ 推荐使用 **Flyway** 管理数据库版本迁移:
947
+
948
+ ```
949
+ src/main/resources/db/migration/
950
+ ├── V1.0.0__init_schema.sql
951
+ ├── V1.0.1__add_customer_ext_json.sql
952
+ ├── V1.1.0__create_mdm_ctct_table.sql
953
+ └── V1.1.1__add_idx_customer_status.sql
954
+ ```
955
+
956
+ ### 10.2 迁移脚本规范
957
+
958
+ - 文件名格式:`V{version}__{description}.sql`
959
+ - 每个脚本只做一件事(建表 / 加索引 / 改字段)
960
+ - **不修改已执行的脚本**,变更通过新版本脚本实现
961
+ - 所有迁移脚本必须幂等或使用 `IF NOT EXISTS` / `IF EXISTS`:
962
+
963
+ ```sql
964
+ -- 安全的表创建
965
+ CREATE TABLE IF NOT EXISTS mdm_customer (...);
966
+
967
+ -- 安全的索引创建
968
+ CREATE INDEX IF NOT EXISTS idx_customer_status ON mdm_customer (status);
969
+
970
+ -- 安全的列添加
971
+ ALTER TABLE mdm_customer ADD COLUMN IF NOT EXISTS ext_json JSONB;
972
+ ```
973
+
974
+ ### 10.3 版本与模块对应
975
+
976
+ | 版本号 | 含义 |
977
+ |--------|------|
978
+ | `V1.0.x` | MDM 模块初始化 |
979
+ | `V1.1.x` | MDM 模块增量变更 |
980
+ | `V2.0.x` | System/Infra 模块 |
981
+ | `V3.0.x` | BPM 模块 |
982
+ | `V4.0.x` | AI 模块 |
983
+
984
+ ---
985
+
986
+ ## 11. MySQL 与 PostgreSQL 对照速查表
987
+
988
+ | 特性 | MySQL 8.0 | PostgreSQL 16 |
989
+ |------|-----------|---------------|
990
+ | 自增主键 | `AUTO_INCREMENT` | `BIGSERIAL` 或 `GENERATED BY DEFAULT AS IDENTITY` |
991
+ | 布尔类型 | `BIT(1)` / `TINYINT(1)` | `BOOLEAN`(`TRUE`/`FALSE`) |
992
+ | 时间类型 | `DATETIME` | `TIMESTAMP`(无时区)/ `TIMESTAMPTZ`(带时区) |
993
+ | JSON 类型 | `JSON` | `JSONB`(二进制,可索引) |
994
+ | 文本大字段 | `TEXT` / `LONGTEXT` | `TEXT`(无长度限制) |
995
+ | 注释方式 | `COMMENT '...'`(内联) | `COMMENT ON TABLE/COLUMN`(独立语句) |
996
+ | 字符集 | `CHARSET=utf8mb4` | `SET client_encoding = 'UTF8'` |
997
+ | 表引擎 | `ENGINE=InnoDB` | 无需指定 |
998
+ | 排序规则 | `COLLATE=utf8mb4_unicode_ci` | 由数据库/列 Collation 控制 |
999
+ | 自动更新时间 | `ON UPDATE CURRENT_TIMESTAMP` | 触发器 / 应用层 |
1000
+ | 外键检查 | `SET FOREIGN_KEY_CHECKS = 0` | 无此概念(可不建 FK) |
1001
+ | 唯一约束(逻辑删除) | 不支持条件唯一 | 部分索引 `WHERE deleted = 0` |
1002
+ | 并发加索引 | 不支持 | `CREATE INDEX CONCURRENTLY` |
1003
+ | 覆盖索引 | 不支持 | `INCLUDE (col1, col2)` |
1004
+ | UPSERT | `INSERT ... ON DUPLICATE KEY UPDATE` | `INSERT ... ON CONFLICT DO UPDATE` |
1005
+ | 返回值 | 需额外 `SELECT` | `RETURNING` 子句 |
1006
+ | 数组类型 | 不支持 | `TEXT[]`、`BIGINT[]` 等 |
1007
+ | 全文搜索 | `FULLTEXT`(中文需 ngram) | `TSVECTOR` + `TSQUERY`(中文需 zhparser) |
1008
+
1009
+ ---
1010
+
1011
+ ## 12. 禁止事项汇总
1012
+
1013
+ 1. **禁止使用 MySQL ENUM 类型**(PostgreSQL 的 ENUM 修改不便,统一用 `VARCHAR` + 字典表)
1014
+ 2. **禁止使用 `JSON` 类型**,统一使用 `JSONB`
1015
+ 3. **禁止使用 `BIT(1)` 表示布尔值**,真布尔字段(如 `visible`/`keep_alive`/`always_show`/`is_primary`)统一使用 `BOOLEAN`;**逻辑删除字段 `deleted` 例外,统一使用 `SMALLINT`(0/1)+ `deleted_time TIMESTAMP`**
1016
+ 4. **禁止在表定义中使用 `ENGINE`/`CHARSET`/`COLLATE`**(MySQL 语法,PostgreSQL 不支持)
1017
+ 5. **禁止不使用 `COMMENT ON` 注释**,每个表和列必须有注释
1018
+ 6. **禁止在生产环境大表(> 10 万行)上直接 `CREATE INDEX` 不带 `CONCURRENTLY`**(会阻塞写操作)
1019
+ 7. **禁止唯一约束忽略逻辑删除**,逻辑删除表的唯一索引必须使用 `WHERE deleted = 0`
1020
+ 8. **禁止使用 `OFFSET` 深分页(OFFSET > 10000)**,大偏移量使用 Keyset Pagination
1021
+ 9. **禁止不使用 `BIGINT`** 作为主键/外键类型
1022
+ 10. **禁止在代码中拼接 SQL 字符串**,使用 MyBatis Plus `LambdaQueryWrapperX` 或参数化 XML Mapper
1023
+ 11. **禁止将 `deleted` 字段定义为 `BOOLEAN` 类型**,必须使用 `SMALLINT`(0=未删除,1=已删除)并配套 `deleted_time TIMESTAMP DEFAULT NULL`。原因:`BaseDO.deleted` 为 `Boolean` 但 MyBatis-Plus 逻辑删除配置为整数 `0/1`,生成 SQL `WHERE deleted = 0`,PostgreSQL 中 `boolean = integer` 不可隐式转换会报错。系统表 `system_dict_type` 即用 `SMALLINT`,以此为统一标准。