← 返回 浏览器全选正文可复制到微信公众号;图片为公网链接。

MySQL → PostgreSQL 改造速查(权限 / 企康)

李福春 · 2026-07-20
图片 0 · Mermaid 2 · _shared/pgjr

老引擎 qvsu_backend_engine(非 engineV2)。目标:把企康、权限应用的 Groovy/SQL 从 MySQL 方言改成 PG。


0. 工程结论

项 结论
企康 QK_OPP(PC)/ MOBILE_QK_OPP(移动端),org 15N
权限 测试库见 A-ghD4QyCw1D(权限设置示例);另有 pinganOpt / pingan_test;系统侧可对照 operation
脚本位置 设计态 qvsu_meta_action.action_json;本测试库无 qvsu_run_action 表
执行入口 POST /action/run;Swagger /api/run/appName/actionName
必改框架 PermissionSqlProcessor / Ext.java 的 FIND_IN_SET
Upsert ON DUPLICATE KEY / new_row → ON CONFLICT / EXCLUDED

0.1 定范围(查 → 导 → 改)

1)查应用(已在测试库 qvsu_sys_dev 跑过)

SELECT DISTINCT app_name_en, app_name_cn, org_code
FROM qvsu_run_app
WHERE s_is_del = 1
  AND (app_name_cn LIKE '%权限%' OR app_name_cn LIKE '%企康%'
       OR app_name_en IN ('QK_OPP','operation','MOBILE_QK_OPP'));

实测结果(去重后):

app_name_en app_name_cn org_code
A-ghD4QyCw1D 权限设置示例 AB
QK_OPP 集团企康商机擎天系统 15N
MOBILE_QK_OPP 集团企康商机擎天系统移动端 15N
operation 组织管理 AB
pinganOpt 权限测试 AB
pingan_test 平安权限测试应用 AB

2)导出脚本(本环境用 meta 表)

SELECT action_name_en, plugin_type,
       JSON_UNQUOTE(JSON_EXTRACT(action_json,'$.actionConfiguration.body')) AS body
FROM qvsu_meta_action
WHERE app_name_en = 'QK_OPP' AND s_is_del = 1;

QK_OPP 实测:共 124 条 Action;DB=67 / CUS=35 / JS=21 / API=1。

DB 样例:

action_name_en body 开头
Mysql1 SELECT qudaoID as value,name as label from qudao where ...
Mysql1_7c669 SELECT branch_code, branch_name FROM pa_branch WHERE s_is_del = 1 ...
Mysql1_8bd32 SELECT company_code, institution_code ... FROM pa_institution_info ...

3)扫这些关键字再改

LIMIT 数字, · 反引号 · IFNULL · FIND_IN_SET · GROUP_CONCAT · DATE_FORMAT · ON DUPLICATE · new_row. · INSERT IGNORE · JSON_EXTRACT · CONVERT( · ->'$.


1. MySQL → PG 语法对照(Action SQL 为主)

只收录写法不同或语义不同;同写法不占条;红字 = 现场必盯;参考掘金 https://juejin.cn/post/7356108146632163339

Action Tran 范围:仅查询 / 插入 / 更新 / 删除相关规则。建表与字段类型映射由其它流程处理(见下方 DDL 说明)。

标识符与字面量

规则 1 · 标识符引用

MySQL

SELECT `s_id`, `staff_name` FROM t_staff WHERE `s_is_del` = 1;

PostgreSQL

SELECT s_id, staff_name FROM t_staff WHERE s_is_del = 1;

反引号在 PG 语法错误。无保留字就别加引号;保留字用双引号,如 "order"。

规则 2 · 字符串引号

MySQL

SELECT * FROM t_dept WHERE dept_id = "D1";

PostgreSQL

SELECT * FROM t_dept WHERE dept_id = 'D1';

PG 里双引号是标识符,字符串只用单引号。WHERE name = "jay" 必改 'jay'。

规则 3 · 大小写比较

MySQL

SELECT COUNT(*) FROM t_staff WHERE staff_name = 'john';

PostgreSQL

SELECT COUNT(*) FROM t_staff WHERE staff_name ILIKE 'john';
-- 或 LOWER(staff_name) = LOWER('john')

MySQL 默认 ci;PG 区分大小写。模糊匹配用 ILIKE。

规则 4 · LIKE / ILIKE

MySQL

SELECT * FROM t_staff WHERE staff_name LIKE '%jo%';

PostgreSQL

-- 大小写敏感(与 MySQL 默认 ci 不同)
SELECT * FROM t_staff WHERE staff_name LIKE '%jo%';
-- 要忽略大小写,改用 ILIKE
SELECT * FROM t_staff WHERE staff_name ILIKE '%jo%';

语义差异:MySQL 默认不区分大小写;PG 的 LIKE 区分,忽略大小写必须用 ILIKE。

规则 5 · 注释

MySQL

SELECT 1 AS c # mysql hash comment

PostgreSQL

SELECT 1 AS c -- pg comment

删掉 # 注释。

规则 6 · 布尔比较

MySQL

SELECT * FROM t_staff WHERE is_active = 1;

PostgreSQL

-- 列已是 BOOLEAN
SELECT * FROM t_staff WHERE is_active = TRUE;
-- 列仍是 SMALLINT/INT,必须继续写整数,不能写 TRUE
SELECT * FROM t_staff WHERE is_active = 1;

PG 强类型:SMALLINT/INT 与 BOOLEAN 不能混比(见「smallint = boolean」规则)。先看列类型再决定 TRUE 还是 1。

规则 7 · 空串与 NULL

MySQL

SELECT * FROM t_staff WHERE IFNULL(note, '') = '';

PostgreSQL

SELECT * FROM t_staff WHERE COALESCE(note, '') = '';

条件里统一 COALESCE。

规则 8 · 库名.表名

MySQL

SELECT * FROM `qvsu_sys1`.`qvsu_api_log`;

PostgreSQL

SELECT * FROM qvsu_sys1.qvsu_api_log;

PG 是 schema.table,不是跨库同义词。

DDL 与类型映射(Action Tran 不覆盖)

说明:建表 / 列类型 / ENGINE / 字符集 / AUTO_INCREMENT 等由其它流程处理。
Action Tran 只改写业务 SQL(查询 / 插入 / 更新 / 删除)。下列仅保留会在 DML 字面量中出现的「零日期」。

规则 9 · 零日期

MySQL

-- MySQL 允许
INSERT INTO t(d) VALUES ('0000-00-00');

PostgreSQL

-- PG 拒绝,迁成 NULL
INSERT INTO t(d) VALUES (NULL);

迁数前清零日期。

分页与排序

规则 10 · LIMIT offset,count

MySQL

SELECT * FROM t_order WHERE s_is_del=1
ORDER BY s_id LIMIT 0,100;

PostgreSQL

SELECT * FROM t_order WHERE s_is_del=1
ORDER BY s_id LIMIT 100 OFFSET 0;

权限脚本里大量 LIMIT 0,100,必改。

规则 11 · FETCH FIRST

MySQL

SELECT * FROM t_order ORDER BY s_id LIMIT 1;

PostgreSQL

SELECT * FROM t_order ORDER BY s_id LIMIT 1;
SELECT * FROM t_order ORDER BY s_id FETCH FIRST 1 ROWS ONLY;

对应 MySQL 的 LIMIT n:PG 可用 LIMIT n 或标准写法 FETCH FIRST n ROWS ONLY,任选其一。

规则 12 · 随机排序

MySQL

SELECT * FROM t_staff ORDER BY RAND() LIMIT 2;

PostgreSQL

SELECT * FROM t_staff ORDER BY random() LIMIT 2;

大表都慢。

规则 13 · FORCE INDEX

MySQL

SELECT * FROM t_staff FORCE INDEX (idx_org) WHERE s_org_code='15Q';

PostgreSQL

SELECT * FROM t_staff WHERE s_org_code='15Q';

PG 无 FORCE INDEX,删掉整段提示;靠索引/统计信息与 SQL 改写调优。

条件与逻辑函数

规则 14 · IFNULL

MySQL

SELECT IFNULL(email, 'none') FROM t_staff WHERE staff_no='S002';

PostgreSQL

SELECT COALESCE(email, 'none') FROM t_staff WHERE staff_no='S002';

PG 无 IFNULL,高频替换为 COALESCE。

规则 15 · IF()

MySQL

SELECT IF(amount > 0, 1, 0) FROM t_staff WHERE staff_no='S001';

PostgreSQL

SELECT CASE WHEN amount > 0 THEN 1 ELSE 0 END
FROM t_staff WHERE staff_no='S001';

PG 没有 IF()。

规则 16 · CASE

MySQL

SELECT CASE WHEN is_active=1 THEN 'Y' ELSE 'N' END FROM t_staff;

PostgreSQL

-- 列仍是整型时,写法可与 MySQL 相同
SELECT CASE WHEN is_active=1 THEN 'Y' ELSE 'N' END FROM t_staff;
-- 列已改 BOOLEAN 时,用真值判断
SELECT CASE WHEN is_active THEN 'Y' ELSE 'N' END FROM t_staff;

语义差异:整型 =1/=0 与 BOOLEAN 真值判断不能混用,先看列类型再选写法。

规则 17 · 空安全等于

MySQL

SELECT COUNT(*) FROM t_staff a
JOIN t_staff b ON a.phone <=> b.phone AND a.s_id < b.s_id;

PostgreSQL

SELECT COUNT(*) FROM t_staff a
JOIN t_staff b ON a.phone IS NOT DISTINCT FROM b.phone
 AND a.s_id < b.s_id;

含 NULL 比较用 IS NOT DISTINCT FROM。

规则 18 · XOR

MySQL

SELECT (1 XOR 0) AS x;

PostgreSQL

SELECT ((TRUE AND NOT FALSE) OR (NOT TRUE AND FALSE))::int AS x;

少见。

规则 19 · 布尔转 0/1

MySQL

SELECT (amount > 0) AS b FROM t_staff WHERE staff_no='S001';

PostgreSQL

SELECT CASE WHEN amount > 0 THEN 1 ELSE 0 END AS b
FROM t_staff WHERE staff_no='S001';

需要整数结果就显式 CASE。

字符串函数

规则 20 · CONCAT / || 与 NULL

MySQL

SELECT CONCAT(dept_name, NULL) FROM t_dept WHERE dept_id='D2';
-- 任一参数为 NULL → 整段结果 NULL

PostgreSQL

SELECT CONCAT(dept_name, NULL) FROM t_dept WHERE dept_id='D2';
-- PG 的 CONCAT 会忽略 NULL,结果仍是 dept_name
SELECT dept_name || NULL FROM t_dept WHERE dept_id='D2';
-- || 遇 NULL → 整段结果 NULL(与 MySQL CONCAT 更接近)

语义差异:PG 的 CONCAT 忽略 NULL;|| 遇 NULL 整段变 NULL。迁移时别混用。

规则 21 · GROUP_CONCAT

MySQL

SELECT GROUP_CONCAT(staff_no ORDER BY staff_no SEPARATOR ',')
FROM t_role_staff WHERE role_id=1;

PostgreSQL

SELECT STRING_AGG(staff_no, ',' ORDER BY staff_no)
FROM t_role_staff WHERE role_id=1;

SEPARATOR 变成第二个参数。

规则 22 · FIND_IN_SET

MySQL

SELECT staff_no FROM t_staff
WHERE FIND_IN_SET('S001', s_create_by);

PostgreSQL

SELECT staff_no FROM t_staff
WHERE 'S001' = ANY (string_to_array(s_create_by, ','));

权限数据范围核心。框架 PermissionSqlProcessor 现写 FIND_IN_SET。

规则 23 · SUBSTRING_INDEX

MySQL

SELECT SUBSTRING_INDEX(ancestors, ',', 1) FROM t_staff WHERE staff_no='S001';
-- 正数:从左取第 1 段
SELECT SUBSTRING_INDEX(ancestors, ',', -1) FROM t_staff WHERE staff_no='S001';
-- 负数:从右取最后 1 段

PostgreSQL

SELECT split_part(ancestors, ',', 1) FROM t_staff WHERE staff_no='S001';
-- 正数:与 SUBSTRING_INDEX(..., 1) 对应
SELECT reverse(split_part(reverse(ancestors), ',', 1))
FROM t_staff WHERE staff_no='S001';
-- 负索引无直接等价;取最后一段用上面写法(或用正则/数组)

语义差异:split_part 不支持负索引;SUBSTRING_INDEX(..., -n) 必须改写成从右取段的写法。

规则 24 · LOCATE

MySQL

SELECT LOCATE('li', staff_name) FROM t_staff WHERE staff_no='S001';

PostgreSQL

SELECT STRPOS(staff_name, 'li') FROM t_staff WHERE staff_no='S001';

参数顺序反过来。

规则 25 · LENGTH / CHAR_LENGTH

MySQL

SELECT LENGTH(staff_name), CHAR_LENGTH(staff_name)
FROM t_staff WHERE staff_no='S001';
-- LENGTH = 字节数;CHAR_LENGTH = 字符数

PostgreSQL

SELECT OCTET_LENGTH(staff_name), CHAR_LENGTH(staff_name)
FROM t_staff WHERE staff_no='S001';
-- LENGTH()/CHAR_LENGTH() = 字符数;字节数用 OCTET_LENGTH()

语义差异:MySQL 的 LENGTH 是字节;PG 的 LENGTH 是字符。要字节数用 OCTET_LENGTH。

规则 26 · 正则

MySQL

SELECT staff_no FROM t_staff WHERE staff_no REGEXP '^S00[1-3]$';

PostgreSQL

SELECT staff_no FROM t_staff WHERE staff_no ~ '^S00[1-3]$';

~* 忽略大小写。

规则 27 · CAST UNSIGNED

MySQL

SELECT CAST('12' AS UNSIGNED);

PostgreSQL

SELECT CAST('12' AS BIGINT);

无 UNSIGNED。

规则 28 · CONVERT → CAST

MySQL

SELECT CONVERT('12.30', DECIMAL(10,2)) AS v;

PostgreSQL

SELECT CAST('12.30' AS DECIMAL(10,2)) AS v;

PG 无 CONVERT,统一改 CAST(... AS ...)。

规则 29 · UUID

MySQL

SELECT UUID();

PostgreSQL

CREATE EXTENSION IF NOT EXISTS pgcrypto;
SELECT gen_random_uuid();

需要 pgcrypto。

日期时间函数

规则 30 · NOW

MySQL

SELECT NOW(), SYSDATE();

PostgreSQL

SELECT NOW(), CURRENT_TIMESTAMP;

SYSDATE() 改 NOW()。

规则 31 · DATE_FORMAT

MySQL

SELECT DATE_FORMAT(s_created_time, '%Y-%m-%d %H:%i:%s') FROM t_staff;

PostgreSQL

SELECT TO_CHAR(s_created_time, 'YYYY-MM-DD HH24:MI:SS') FROM t_staff;

PG 无 DATE_FORMAT。%Y→YYYY,%m→MM,%d→DD,%H→HH24,%i→MI,%s→SS。

规则 32 · STR_TO_DATE

MySQL

SELECT STR_TO_DATE('2026-07-18', '%Y-%m-%d');

PostgreSQL

SELECT TO_DATE('2026-07-18', 'YYYY-MM-DD');

格式必须匹配。

规则 33 · UNIX_TIMESTAMP

MySQL

SELECT UNIX_TIMESTAMP('2026-07-18 00:00:00');

PostgreSQL

SELECT EXTRACT(EPOCH FROM TIMESTAMP '2026-07-18 00:00:00')::bigint;

返回秒。

规则 34 · FROM_UNIXTIME

MySQL

SELECT FROM_UNIXTIME(1721260800);

PostgreSQL

SELECT TO_TIMESTAMP(1721260800);

注意时区。

规则 35 · DATE_ADD

MySQL

SELECT DATE_ADD('2026-07-18', INTERVAL 1 DAY);

PostgreSQL

SELECT DATE '2026-07-18' + INTERVAL '1 day';

单位带引号。

规则 36 · DATEDIFF

MySQL

SELECT DATEDIFF('2026-07-20', '2026-07-18');
-- 参数顺序:结束日 - 起始日,返回整数天

PostgreSQL

SELECT DATE '2026-07-20' - DATE '2026-07-18';
-- 日期相减,返回整数天(与上面参数顺序对应)

语义差异:MySQL 是函数调用 DATEDIFF(后, 前);PG 是日期算术 后 - 前。参数顺序必须前后对应,写反会变号。

规则 37 · DATE()

MySQL

SELECT DATE('2026-07-18 12:00:00');

PostgreSQL

SELECT (TIMESTAMP '2026-07-18 12:00:00')::date;

规则 38 · YEAR/MONTH

MySQL

SELECT YEAR('2026-07-18'), MONTH('2026-07-18');

PostgreSQL

SELECT EXTRACT(YEAR FROM DATE '2026-07-18')::int,
       EXTRACT(MONTH FROM DATE '2026-07-18')::int;

规则 39 · CURDATE

MySQL

SELECT CURDATE();

PostgreSQL

SELECT CURRENT_DATE;

JSON(列类型另流程;Action Tran 只改 path)

说明:JSON → JSONB 列类型由其它流程处理。本工具只改写查询侧 path 表达式。

规则 40 · JSON path

MySQL

SELECT JSON_UNQUOTE(JSON_EXTRACT('{"name":"jay"}', '$.name')) AS v;
-- 或列路径:keywords_json->'$.name'
SELECT keywords_json->'$.name' FROM t WHERE keywords_json->'$.name' LIKE CONCAT('%', ?, '%');

PostgreSQL

SELECT '{"name":"jay"}'::jsonb->>'name' AS v;
-- 文本取值用 ->>;要 json/jsonb 值用 ->
SELECT keywords_json->>'name' FROM t WHERE keywords_json->>'name' LIKE CONCAT('%', ?, '%');

MySQL JSON_EXTRACT / ->'$.name';PG 用 jsonb->>'name'(Docker 实测)。

DML(插入/更新/删除/Upsert)

规则 41 · ON DUPLICATE KEY

MySQL

INSERT INTO t_saas_app(app_name_en, a_version)
VALUES ('QK_OPP', '2.0')
ON DUPLICATE KEY UPDATE a_version = VALUES(a_version);

PostgreSQL

INSERT INTO t_saas_app(app_name_en, a_version)
VALUES ('QK_OPP', '2.0')
ON CONFLICT (app_name_en) DO UPDATE
SET a_version = EXCLUDED.a_version;

必须写冲突目标(唯一键)。

规则 42 · new_row 别名

MySQL

ON DUPLICATE KEY UPDATE A_version = new_row.A_version

PostgreSQL

ON CONFLICT (app_name_en) DO UPDATE
SET a_version = EXCLUDED.a_version

new_row → EXCLUDED。Groovy 里常见。

规则 43 · INSERT IGNORE

MySQL

INSERT IGNORE INTO t_saas_app(app_name_en, a_version)
VALUES ('QK_OPP', '9.9');

PostgreSQL

INSERT INTO t_saas_app(app_name_en, a_version)
VALUES ('QK_OPP', '9.9')
ON CONFLICT DO NOTHING;

规则 44 · REPLACE INTO

MySQL

REPLACE INTO t_saas_app(app_name_en, a_version) VALUES ('QK_OPP','3.0');
-- 冲突时先 DELETE 再 INSERT,自增/触发器/外键可能被连带影响

PostgreSQL

INSERT INTO t_saas_app(app_name_en, a_version) VALUES ('QK_OPP','3.0')
ON CONFLICT (app_name_en) DO UPDATE
SET a_version = EXCLUDED.a_version;
-- 冲突时原地 UPDATE,不删行

语义差异:REPLACE = 删再建;ON CONFLICT DO UPDATE = 原地更新。副作用(自增、级联、触发器)不同,不能当纯语法替换。

规则 45 · UPDATE JOIN

MySQL

UPDATE t_staff s
JOIN t_dept d ON d.dept_id = 'D2'
SET s.note = CONCAT('dept:', d.dept_name)
WHERE s.staff_no = 'S002';

PostgreSQL

UPDATE t_staff s
SET note = CONCAT('dept:', d.dept_name)
FROM t_dept d
WHERE d.dept_id = 'D2' AND s.staff_no = 'S002';

JOIN 改写成 FROM。

规则 46 · DELETE JOIN

MySQL

DELETE s FROM t_role_staff s
JOIN t_staff t ON s.staff_no = t.staff_no
WHERE t.staff_no = 'S003' AND s.role_id = 2;

PostgreSQL

DELETE FROM t_role_staff s
USING t_staff t
WHERE s.staff_no = t.staff_no
  AND t.staff_no = 'S003' AND s.role_id = 2;

规则 47 · 多表 DELETE

MySQL

-- MySQL 多表 DELETE 特有写法,建议拆开

PostgreSQL

-- PG:拆成多条 DELETE,或 DELETE ... USING ...

拆语句最省事。

规则 48 · 取自增 ID

MySQL

INSERT INTO t_dept(dept_id, dept_name) VALUES ('DX','临时');
SELECT LAST_INSERT_ID();

PostgreSQL

INSERT INTO t_dept(dept_id, dept_name)
VALUES ('DX','临时')
RETURNING s_id;

Groovy 优先 RETURNING。

查询语义与事务

规则 49 · 松散 GROUP BY

MySQL

-- MySQL 宽松模式可能放过(非聚合列随机取值)
SELECT role_id, staff_no FROM t_role_staff GROUP BY role_id;

PostgreSQL

-- PG 报错:staff_no 必须聚合或进 GROUP BY
SELECT role_id, COUNT(*) FROM t_role_staff GROUP BY role_id;
-- 或把非聚合列补进 GROUP BY / 用 MIN(staff_no) 等

迁移后最常见报错之一。SELECT 列必须在 GROUP BY 中或使用聚合函数。

规则 50 · 隐式类型转换

MySQL

SELECT CAST('1' AS SIGNED) + 1;

PostgreSQL

SELECT CAST('1' AS INT) + 1;

PG 更严,显式 CAST。

规则 51 · 除法

MySQL

SELECT CAST(1 AS DECIMAL(10,2)) / 2;

PostgreSQL

SELECT (1::numeric) / 2;

金额用 numeric。

规则 52 · ROUND 需 numeric

MySQL

SELECT ROUND(1.234, 2) AS v;

PostgreSQL

SELECT ROUND(CAST(1.234 AS numeric), 2) AS v;

PG 上对 double 的 ROUND 常需先 CAST(... AS numeric)(Docker 实测)。

规则 53 · 事务中止 25P02

MySQL

START TRANSACTION;
SELECT IFNULL(1, 2) AS v;   -- 即使某句失败,同事务后续仍可能继续
SELECT 1 AS ok;
ROLLBACK;

PostgreSQL

BEGIN;
SELECT IFNULL(1, 2) AS v;   -- 出错后事务进入 aborted
SELECT 1 AS ok;             -- SQLSTATE 25P02: current transaction is aborted
ROLLBACK;                   -- 必须先结束事务块才能继续

语义差异:PG 同事务一旦出错,后续语句一律忽略直至 ROLLBACK/COMMIT;勿靠捕获 SQL 异常后继续写库,改为业务侧先判断。

规则 54 · smallint = boolean

MySQL

-- TINYINT/SMALLINT 列可与 0/1 比较;部分驱动也容忍布尔语义
SELECT COUNT(*) AS c FROM t_bool_demo WHERE is_mobile = 1;

PostgreSQL

-- 列是 SMALLINT 时写 TRUE 直接失败:operator does not exist: smallint = boolean
-- 错误写法:WHERE s_is_del = TRUE
SELECT COUNT(*) AS c FROM t_staff WHERE s_is_del = 1;
-- 列是 BOOLEAN 时才用 TRUE/FALSE

语义/类型差异:PG 强类型,SMALLINT 与 BOOLEAN 不可混比;UPDATE/INSERT 赋值同理。列类型与字面量必须一致。

2. 怎么测(开发视角)

先写用例,再改脚本。同一入参:MySQL 基线 vs PG 结果对等,才算过。

flowchart 示意图

四步就够:

  1. 定范围:上面 0.1
  2. FT + 基线:每个待改 Action 至少一条;POST /action/run 落盘
  3. 改并发布:改 body → 数据源 plugin 改 PostgreSQL → 清缓存/重启
  4. AB:同一 JSON 复跑;行数和关键字段一致才打勾;再用 JMeter 扫语法错误

接口:

POST /action/run
Content-Type: application/json

{
  "appNameEn": "QK_OPP",
  "actionNameEn": "Mysql1",
  "organizationCode": "15N",
  "viewMode": false,
  "params": {}
}

本地语法夹具:D:\src\cqs\pgjr-verify\verify.py(含 JSON/CONVERT/ROUND/25P02 等新增用例;Docker pgjr_mysql/pgjr_pg)


3. 路径

用途 路径
老引擎 D:\src\cqs\qvsu_backend_engine
PG profile core-server/.../application-dev-jvda-pg.yml
权限 SQL 展开 .../PermissionSqlProcessor.java
部署 POST /deploy/engineDeployApp
语法夹具 D:\src\cqs\pgjr-verify\
Action 迁移工具 20260719/gr/(版本 3.0.25)
需求原文 20260719/pgt.md
工具设计稿 docs/superpowers/specs/2026-07-19-groovy-action-tran-design.md
OpenSpec openspec/changes/groovy-action-tran/

4. 概要设计(Action 迁移工具)

闭环实现文档:规则对照(§1)→ 本概要/接口(§4–§5)→ 代码 20260719/gr → Docker 实测。需求见 pgt.md。

4.1 目标

用 Maven + Spring Boot 2.7 + Groovy 3.0.25(全 Groovy 实现)提供 HTTP 服务:读 MySQL qvsu_meta_action(DB/CUS)→ 按本文件 Action SQL 规则子集(不含建表/字段 DDL)匹配/改写 → PostgreSQL 检测 → 写入 MySQL qvsu_action_tran。

4.2 框架选型结论(已定)

版本核对(core-server)

来源 值
D:\src\cqs\qvsu_backend_engine\core-server\pom.xml groovy-all:**3.0.7**(对照);编译属性曾标 JDK 11
内网运行 Java 8(本工具以此为准)
本工具 Groovy 3.0.25;JDK 8;Spring Boot 2.7.x;构建 Maven
工程 artifact 1.0.0(与语言版本分离);/health 同时返回 version 与 groovy

选型表

项 选定
构建 Maven + gmavenplus(不用 Gradle)
运行时 Spring Boot 2.7.x + JDK 8,内嵌 Tomcat
语言 Groovy 3.0.25(groovy-all)
实现语言 一接口一类(HealthApi / ActionTranRunApi / ActionTranResultsApi),无 Service 分层
HTTP Spring MVC @RestController(Groovy)
数据访问 JDBC:Hikari + groovy.sql.Sql 双 Bean;不用 JPA/MyBatis
规则改写 逻辑在 ActionTranRunApi 内;catalog 覆盖 Action SQL 子集(无建表/字段 DDL)
代码目录 20260719/gr/(本地运行见 README.md,Groovy SDK 3.0.25)

依赖要点:spring-boot-starter-web、spring-boot-starter-jdbc、org.codehaus.groovy:groovy-all:3.0.25(type=pom)、mysql-connector-java、postgresql。

4.3 双数据源与表

4.4 流水线闭环

flowchart 示意图

闭环要点:参数配置可改;规则多命中 → JSON 数组;命中打日志(原 SQL / 规则 / 新 SQL / PG 结果);检测失败不计入 upserted;不回写 qvsu_meta_action。


5. 接口逻辑设计

默认监听:http://127.0.0.1:3025(server.port=3025)。基于 Spring MVC。响应壳:{"code":0,"message":"ok","data":{}}。

5.1 GET /health

返回工程 version、groovy=3.0.25,以及 mysql/postgres ping。

5.2 POST /api/v1/action-tran/run

无 HTTP 入参。参数写死在 run() 方法内局部变量:appNameEn=COC、pluginTypes=[DB,CUS]、limit=0(全量)、skipPgCheck=true。

源查询(应用内解析 action_json,不用 MySQL JSON_EXTRACT):

SELECT action_name_en, plugin_type, action_json
FROM qvsu_meta_action
WHERE app_name_en = 'COC' AND s_is_del = 1 AND plugin_type IN ('DB','CUS');

逻辑:

  1. DB:直接对 body 做规则匹配/替换
  2. CUS:类内抽 SQL(围栏/"""),再同 DB 处理;抽不出不整段当 SQL
  3. match_rule:仅 title/category/notes;未命中 []
  4. need_update:命中=1,否则=0
  5. skipPgCheck=true 时跳过 EXPLAIN;否则 PG 失败记 failed 不 upsert
  6. 响应 data:scanned / upserted / hitRules / failed[]

5.3 GET /api/v1/action-tran/results

无 HTTP 入参。参数写死在 results() 方法内:appNameEn=COC、limit=50。返回已落库记录供核对。

5.4 配置示例(库连接;业务入参在 handler 方法局部变量)

app:
  version: "1.0.0"
  groovyVersion: "3.0.25"
mysql:
  jdbcUrl: "jdbc:mysql://127.0.0.1:13306/pgjr?useSSL=false&characterEncoding=utf8"
postgres:
  jdbcUrl: "jdbc:postgresql://127.0.0.1:15432/pgjr"

规则文件 rules/auto-rules.json 覆盖本文 Action SQL 相关条目(去掉建表/字段 DDL;每条 auto:true + detect + rewrite;语义难改用 annotate/template)。

5.5 测试闭环

  1. 从 qvsu_backend_engine 测试环境抽样复制 qvsu_meta_action → 本地 MySQL
  2. 启动 20260719/gr
  3. webapp-testing:/health、/run(小 limit)、/results

测试库:qvsu_sys_dev(2026-07-19 实测)。勿把口令写进文档。

复制提示:选中上方正文(含表格与图片)→ 复制 → 粘贴到微信公众号编辑器。 图片为公网链接,部署到 public.joysky.cloud 后可正常显示。