可联调 · 可回放 · 可回滚迁移实战工具包:让 Gauss 版真正“能跑、能验、能回退“前四章解决了“语法能编译“,这一章解决“业务能对账“。提供两套可落地的工程件:对外 API 适配包(HTT

发布时间:2026/11/4 7:20:01
可联调 · 可回放 · 可回滚迁移实战工具包:让 Gauss 版真正“能跑、能验、能回退“前四章解决了“语法能编译“,这一章解决“业务能对账“。提供两套可落地的工程件:对外 API 适配包(HTT

可联调 · 可回放 · 可回滚

迁移实战工具包:让 Gauss 版真正"能跑、能验、能回退"

前四章解决了"语法能编译",这一章解决"业务能对账"。提供两套可落地的工程件:对外 API 适配包(HTTP + 重试 + 死信队列)与oracle_fdw 双库并行校验框架,配合pg_cron自动回放,实现 Oracle 与 GaussDB 同参回放、逐笔对账、差异落表。

5.1 原 EBS API 的替代思路

原包的业务核心只有两次调用:ar_invoice_api_pub.create_single_invoice与ar_deposit_api_pub.Create_Deposit。这两者在 GaussDB 上不存在等价实现,是本次迁移最大的语义缺口。推荐采用"包签名不变、函数体可切换"的策略:

实现方式改动面一致性适用场景与代价
A. 纯 DB 直插(主包内置,当前默认)仅主包低无外部依赖,可编译、可单测;不推荐生产缺少单据编号、税码、SLA 会计分录、收款匹配,需 AR 产品方补齐规则。
B. 调用自研发票服务(HTTP)仅 API 包高推荐。主包零改动,切流只需在 API 包内切换实现;自带幂等键、指数退避重试、死信队列,语义最接近"一次提交、必达"。
C. 包装为 DB Procedure 供外部调用主包 + 服务中用postgres_fdw反向调用,或 GaussDB 外部表推数后由应用侧生成,适合服务尚未就绪时的过渡。

5.2 API 适配包:可联调函数体

SQLar_api.ar_invoice_api_pub — 对外 API 适配包(HTTP 版)复制代码

包签名与原 EBS API 对齐,内部完成 JSON 组装 → 出站调用 → 指数退避重试(3 次:0.5s/1s/2s)→ 失败入死信队列。主包xxx_ar_STDCreateDepoInv_pkg无需任何改动即可切换到真实发票服务。

1-- =============================================================== 2-- File : xxx_ar_api_adapters_gauss.sql 3-- Desc : GaussDB 侧 AR 对外接口包(原 EBS: ar_invoice_api_pub / 4-- ar_deposit_api_pub)。提供可联调函数体: 5-- 1) create_single_invoice -> 调用自研发票服务 (HTTP) 6-- 2) create_deposit -> 调用自研定金服务 (HTTP) 7-- · 含 指数退避重试 (3次) + 死信队列(dlx) + 幂等键 8-- · 纯 DB 直插版保留于 xxx_ar_STDCreateDepoInv_pkg 中, 9-- 生产切流时只需在此包内切换实现, 主包零改动 10-- Depend : postgres_fdw / http (可选, 见下文) 11-- =============================================================== 12 SCHEMA EXISTS ar_api; 13 14-- ---------- 1. 通用类型 (与 EBS API 入参对齐, 字段名保持同名) ---- 15 ar_api. ( 16 batch_source_id , 17 name (50), 18 status (1), 19 start_date , 20 end_date 21); 22 23 ar_api. ( 24 cust_trx_type_id , 25 trx_number (20), 26 bill_to_customer_id , 27 bill_to_site_use_id , 28 trx_date , 29 gl_date , 30 currency_code (15), 31 exchange_rate , 32 , 33 org_id , 34 doc_sequence_value , 35 interface_header_id , 36 trx_id , 37 term_id , 38 reference (240) 39); 40 41 ar_api. ( 42 interface_line_id , 43 line_number , 44 (20), 45 memo_line_id , 46 description (240), 47 quantity , 48 unit_selling_price , 49 amount , 50 tax_code (50), 51 tax_rate , 52 inventory_item_id , 53 org_id 54); 55 56 ar_api. ( 57 interface_distribution_id , 58 account_class (30), 59 code_combination_id , 60 amount , 61 org_id 62); 63 64 ar_api. ( 65 interface_salescredit_id , 66 salesrep_id , 67 sales_credit_type_id , 68 percentage , 69 org_id 70); 71 72-- ---------- 2. 发票 / 定金 API 包 (HTTP 远程调用版) -------------- 73 ar_api.ar_invoice_api_pub 74 create_single_invoice( 75 p_api_version , 76 p_init_msg_list 0, 77 p_commit 0, 78 p_validation_level 1, 79 , 80 , 81 , 82 , 83 , 84 x_return_status , 85 x_msg_count , 86 x_msg_data , 87 x_customer_trx_id , 88 x_trx_number ); 89 90 create_deposit( 91 p_api_version , 92 p_init_msg_list 0, 93 p_commit 0, 94 p_validation_level 1, 95 , 96 , 97 , 98 x_return_status , 99 x_msg_count , 100 x_msg_data , 101 x_customer_trx_id , 102 x_trx_number ); 103 ar_api.ar_invoice_api_pub; 104/ 105 106 ar_api.ar_invoice_api_pub 107 g_invoice_url (256) := sys_config_pkg.get_string(0,1,2); 108 g_deposit_url (256) := sys_config_pkg.get_string(0,1,2); 109 g_retry_max := 3; 110 g_idem_key (64); 111 112 -- 统一出站调用 + 重试 + 死信 113 call_remote( 114 p_url , 115 p_payload , 116 p_idem_key , 117 x_http_code , 118 x_response ) 119 l_attempt := 0; 120 l_code ; 121 l_resp (4000); 122 l_sqlerr (4000); 123 124 l_attempt < g_retry_max 125 l_attempt := l_attempt + 1; 126 127 -- 若未安装 http 扩展, 请改为: SELECT http_post(...) INTO l_code,l_resp ... 128 -- 以下为 ORA 兼容语法示例 129 l_code := 200; 130 l_resp := http_post(p_url, 131 p_payload || 0 || p_idem_key || 1, 132 0); 133 l_code BETWEEN 200 299 134 x_http_code := l_code; x_response := l_resp; ; 135 ; 136 LOG 0, l_attempt, p_url, l_code; 137 138 139 l_sqlerr := SQLERRM; 140 LOG 0, l_attempt, l_sqlerr; 141 ; 142 PERFORM pg_sleep(power(2, l_attempt - 1) * 0.5); -- 0.5s / 1s / 2s 143 ; 144 145 -- 全部失败 -> 入死信队列, 由 xxx_ar_dlx_replay_pkg 重放 146 147 (dlx_id, api_name, idem_key, payload, last_err, 148 retry_count, status, creation_date) 149 150 (.NEXTVAL, substr(p_url,1,100), p_idem_key, 151 p_payload, substr(coalesce(l_sqlerr,0||l_code),1,2000), 152 l_attempt, 0, now()); 153 x_http_code := -1; 154 x_response := 0 || l_sqlerr; 155 call_remote; 156 157 -- 响应 JSON 解析辅助 (无 jq, 纯 SQL 截取, 够用) 158 parse_json(p_json , p_key ) 159 l_pos ; l_end ; 160 161 l_pos := instr(p_json, 0||p_key||1); 162 l_pos = 0 ; ; 163 l_pos := l_pos + length(p_key) + 3; 164 l_end := instr(substr(p_json,l_pos), substr(p_json,l_pos,1)=0 1 2 ); 165 rtrim(ltrim(substr(p_json,l_pos,l_end-1),0),1); 166 parse_json; 167 168 -- ============ 发票创建 ============ 169 create_single_invoice( 170 p_api_version, p_init_msg_list, p_commit, p_validation_level, 171 , , , 172 , , 173 x_return_status, x_msg_count, x_msg_data, 174 x_customer_trx_id, x_trx_number) 175 l_payload (32767); 176 l_resp (4000); 177 l_code ; 178 l_err_msg (2000); 179 l_lines (32767); 180 l_dists (32767); 181 l_sc (32767); 182 183 x_return_status := 0; x_msg_count := 0; x_msg_data := ; 184 g_idem_key := 0 || .org_id || 1 || 185 .interface_header_id || 0 || 186 to_char(now(),0); 187 188 -- 行/分配/销售分成 -> JSON 数组 (遍历数组字段) 189 i 1..coalesce(.COUNT,0) 190 l_lines := l_lines || i>1 0 || 191 0||(i).line_number|| 192 0||(i).memo_line_id|| 193 0||(i).quantity|| 194 0||(i).unit_selling_price|| 195 0||(i).tax_code||1; 196 ; 197 i 1..coalesce(.COUNT,0) 198 l_dists := l_dists || i>1 0 || 199 0||(i).account_class|| 200 0||(i).code_combination_id|| 201 0||(i).amount||1; 202 ; 203 i 1..coalesce(.COUNT,0) 204 l_sc := l_sc || i>1 0 || 205 0||(i).salesrep_id|| 206 0||(i).percentage||1; 207 ; 208 209 l_payload := 210 0||.name|| 211 0||.cust_trx_type_id|| 212 0||to_char(.trx_date,1)|| 213 0||to_char(.gl_date,1)|| 214 0||.currency_code|| 215 0||.bill_to_customer_id|| 216 0||.bill_to_site_use_id|| 217 0||.term_id|| 218 0||.reference||1|| 219 0||l_lines||1||l_dists||2||l_sc||3; 220 221 call_remote(g_invoice_url, l_payload, g_idem_key, l_code, l_resp); 222 223 l_code BETWEEN 200 299 224 x_customer_trx_id := to_number(parse_json(l_resp,0)); 225 x_trx_number := parse_json(l_resp,0); 226 p_commit = 0 COMMIT; ; 227 228 x_return_status := 0; 229 x_msg_count := 1; 230 x_msg_data := substr(0||l_code||1||l_resp,1,2000); 231 LOG 0, x_msg_data; 232 ; 233 234 235 x_return_status := 0; x_msg_count := 1; 236 x_msg_data := substr(SQLERRM,1,2000); 237 LOG 0, SQLERRM; 238 create_single_invoice; 239 240 -- ============ 定金创建 (复用同一套出站/重试/DLX) ============ 241 create_deposit( 242 p_api_version, p_init_msg_list, p_commit, p_validation_level, 243 , , , 244 x_return_status, x_msg_count, x_msg_data, 245 x_customer_trx_id, x_trx_number) 246 l_payload (32767); 247 l_resp (4000); 248 l_code ; 249 250 x_return_status := 0; x_msg_count := 0; x_msg_data := ; 251 g_idem_key := 0 || .org_id || 1 || 252 .interface_header_id || 0 || 253 to_char(now(),0); 254 255 l_payload := 256 0||.cust_trx_type_id|| 257 0||to_char(.trx_date,1)|| 258 0||to_char(.gl_date,1)|| 259 0||.currency_code|| 260 0||.bill_to_customer_id|| 261 0||.bill_to_site_use_id|| 262 0||coalesce((1).amount,0)||1; 263 264 call_remote(g_deposit_url, l_payload, g_idem_key, l_code, l_resp); 265 l_code BETWEEN 200 299 266 x_customer_trx_id := to_number(parse_json(l_resp,0)); 267 x_trx_number := parse_json(l_resp,0); 268 p_commit = 0 COMMIT; ; 269 270 x_return_status := 0; x_msg_count := 1; 271 x_msg_data := substr(0||l_code||1||l_resp,1,2000); 272 ; 273 274 275 x_return_status := 0; x_msg_count := 1; 276 x_msg_data := substr(SQLERRM,1,2000); 277 create_deposit; 278 279 ar_api.ar_invoice_api_pub; 280/ 281SHOW ERRORS; 282

中http 扩展可用性

示例中http_post来自http扩展;若环境未装,改为postgres_fdw调用或应用侧 outbound。部署前执行CREATE EXTENSION IF NOT EXISTS http;验证。

中幂等键设计

g_idem_key = INV-|DEP- <org_id>-<interface_header_id>-<timestamp>,保证同一笔业务在发票服务侧只落一次;服务端须按此键做唯一约束,否则重试会产生重复单据。

高敏感配置外置

服务 URL 通过sys_config_pkg.get_string('AR','INVOICE_SVC_URL',...)读取,禁止明文写进包体;密钥走 KMS / 配置中心,包内只持有脱敏占位。

5.3 死信队列与自动重放

网络抖动或服务短暂不可用时,call_remote将请求体连同错误原因写入xxx_ar_api_dlx,由xxx_ar_dlx_replay_pkg.replay按SKIP LOCKED并发安全重放,累计失败达 5 次即标记DLQ转入人工处理。

-- 手工触发重放(亦可交给 pg_cron 每 2 分钟自动执行) BEGIN xxx_ar_dlx_replay_pkg.replay(p_batch_size => 200); END; / -- 查看积压 SELECT status, COUNT(*), MAX(last_update) FROM xxx_ar_api_dlx GROUP BY status; -- 人工认领 / 关闭 BEGIN xxx_ar_dlx_replay_pkg.mark_dlq(101, '上游服务返回不可逆业务错误'); END; /

5.4 oracle_fdw 双库并行校验框架

SQLxxx_oracle_fdw_conf_pkg + xxx_ar_dual_validation_pkg — 双库同参回放与差异落表复制代码

在 GaussDB 侧建立ora_ebs外部服务器与ar_val.ora_*外部表,读取 Oracle 已生成的结果(避免重复开票);再用同一组入参在 GaussDB 侧执行,逐笔比对金额、行数、状态,差异写入xxx_ar_dual_diff。

1-- =============================================================== 2-- File : xxx_ar_validation_gauss.sql 3-- Desc : (1) 死信队列重放包 xxx_ar_dlx_replay_pkg 4-- (2) oracle_fdw 双库并行校验框架 5-- xxx_oracle_fdw_conf_pkg - 外部服务器/映射 DDL 6-- xxx_ar_dual_validation_pkg - 同参回放 + 差异落表 7-- Usage : 迁移上线后, 用同一组入参同时调 Oracle 与 GaussDB, 8-- 比对 x_customer_trx_id / 金额 / 行数, 方差=0 即通过 9-- =============================================================== 10 11-- ==================== (1) 死信队列重放 ========================== 12 EXISTS ( 13 dlx_id GENERATED BY IDENTITY PRIMARY KEY, 14 api_name (100), 15 idem_key (64) UNIQUE, 16 payload CLOB, 17 last_err (2000), 18 retry_count 0, 19 status (20) 0, -- NEW/RETRYING/DONE/DLQ 20 creation_date now(), 21 last_update now() 22); 23COMMENT ON 0; 24 25 26 g_max_retry := 5; 27 replay(p_batch_size 100); 28 mark_dlq(p_dlx_id , p_reason ); 29 ; 30/ 31 32 33 replay(p_batch_size 100) 34 CURSOR c_dlx 35 dlx_id, api_name, idem_key, payload 36 37 status = 0 retry_count < g_max_retry 38 ORDER BY creation_date 39 LIMIT p_batch_size 40 SKIP LOCKED; 41 l_code ; 42 l_resp (4000); 43 l_url (256); 44 45 r c_dlx 46 47 status=0, last_update=now() 48 dlx_id = r.dlx_id; 49 50 l_url := 51 r.api_name LIKE 0 ar_api.g_invoice_url 52 ar_api.g_deposit_url ; 53 ar_api.ar_invoice_api_pub.call_remote(l_url, r.payload, 54 r.idem_key, l_code, l_resp); 55 l_code BETWEEN 200 299 56 status=0, last_update=now() 57 dlx_id = r.dlx_id; 58 59 60 retry_count = retry_count + 1, 61 status = retry_count+1 >= g_max_retry 0 1 , 62 last_err = substr(0||l_code||1||l_resp,1,2000), 63 last_update = now() 64 dlx_id = r.dlx_id; 65 ; 66 COMMIT; 67 68 69 LOG 0, r.dlx_id, SQLERRM; 70 ROLLBACK; 71 ; 72 ; 73 replay; 74 75 mark_dlq(p_dlx_id , p_reason ) 76 77 78 status=0, last_err=substr(p_reason,1,2000), last_update=now() 79 dlx_id = p_dlx_id; 80 COMMIT; 81 mark_dlq; 82 ; 83/ 84SHOW ERRORS; 85 86-- ==================== (2) oracle_fdw 配置 ======================== 87 88 -- 创建外部服务器 + 用户映射 + 导入 Oracle 侧同名表 89 -- 需在 GaussDB 节点安装 oracle_fdw (dnf install oracle_fdw_$PN 并放到 90 -- shared_preload_libraries), 且 LD_LIBRARY_PATH 含 Oracle Client 91 setup_fdw( 92 p_srv_name 0, 93 p_host , 94 p_port 0, 95 p_service , 96 p_user , 97 p_passwd ); 98 import_table(p_ora_schema , p_ora_table ); 99 ; 100/ 101 102 103 setup_fdw(p_srv_name,p_host,p_port,p_service,p_user,p_passwd) 104 l_ddl (1000); 105 106 EXTENSION EXISTS oracle_fdw; 107 l_ddl := 0||p_srv_name||1|| 108 0||p_host||1||p_port||2||p_service||3; 109 IMMEDIATE l_ddl; ; ; 110 111 IMMEDIATE 0||p_srv_name|| 112 0||p_user||1||p_passwd||2; 113 ; ; 114 LOG 0, p_srv_name; 115 setup_fdw; 116 117 import_table(p_ora_schema , p_ora_table ) 118 119 -- 外部表落到 ar_val schema, 表名加 ora_ 前缀避免冲突 120 IMMEDIATE 121 0||p_ora_table||1|| 122 0||p_ora_schema||1||p_ora_table||2; 123 import_table; 124 ; 125/ 126 127-- ==================== (3) 双库并行校验 ========================== 128 SCHEMA EXISTS ar_val; 129 130 ar_val. ( 131 run_id GENERATED BY IDENTITY, 132 run_date now(), 133 org_id , 134 group_id , 135 (20), 136 db_side (10), -- ORACLE / GAUSS 137 customer_trx_id , 138 trx_number (20), 139 total_amount , 140 line_cnt , 141 return_status (1), 142 msg_data (2000), 143 PRIMARY KEY (run_id) 144); 145COMMENT ON ar_val. 0; 146 147 ar_val. ( 148 diff_id GENERATED BY IDENTITY PRIMARY KEY, 149 run_id , 150 (30), -- AMOUNT / LINE_CNT / STATUS / MISSING 151 gauss_value (4000), 152 oracle_value (4000), 153 diff_ratio , 154 is_critical (1) 0 155); 156COMMENT ON ar_val. 0; 157 158 159 g_run_id ; 160 -- 同参回放: 入参与生产完全一致, 结果写入 xxx_ar_dual_result 161 run_ora(p_org_id , p_group_id , 162 p_tpname , ); 163 run_gauss(p_org_id , p_group_id , 164 p_tpname , ); 165 -- 比对: 金额方差=0 且行数一致 即 PASS 166 diff ; -- OK / DIFF / ERROR 167 report(p_run_id ); 168 ; 169/ 170 171 172 run_ora(p_org_id,p_group_id,p_tpname,) 173 l_cnt ; 174 ; 175 176 -- 通过 oracle_fdw 读取 Oracle 侧"同源输入"产生的单据, 而非再调一次 EBS 177 -- (避免重复开票), 直接对比 Oracle 已生成的结果表 178 ar_val. 179 (org_id, group_id, , db_side, customer_trx_id, 180 trx_number, total_amount, line_cnt, return_status) 181 p_org_id, p_group_id, , 0, 182 ct.customer_trx_id, ct.trx_number, 183 SUM(ca.extended_amount), COUNT(ca.customer_trx_line_id), 184 0 185 ar_val.ora_ra_customer_trx ct 186 JOIN ar_val.ora_ra_customer_trx_lines ca 187 ON ca.customer_trx_id = ct.customer_trx_id 188 ct.org_id = p_org_id 189 ct.interface_header_id = p_group_id 190 GROUP BY ct.customer_trx_id, ct.trx_number; 191 COMMIT; 192 193 194 ar_val. 195 (org_id,group_id,,db_side,return_status,msg_data) 196 (p_org_id,p_group_id,,0,1,substr(SQLERRM,1,2000)); 197 COMMIT; 198 run_ora; 199 200 run_gauss(p_org_id,p_group_id,p_tpname,) 201 l_msg (32767); 202 l_cnt ; 203 l_trx ; 204 l_no (20); 205 206 .Auto_Created_STD( 207 p_msg_Data => l_msg, 208 p_msg_Count => l_cnt, 209 p_Org_ID => p_org_id, 210 p_Group_ID => p_group_id, 211 p_TPName => p_tpname, 212 p_trx_Type => ); 213 214 r ( 215 customer_trx_id, trx_number, 216 SUM(extended_amount) amt, COUNT(*) lc 217 ra_customer_trx_lines_all 218 org_id = p_org_id interface_header_id = p_group_id 219 GROUP BY customer_trx_id, trx_number 220 ) 221 ar_val. 222 (org_id,group_id,,db_side,customer_trx_id, 223 trx_number,total_amount,line_cnt,return_status) 224 (p_org_id,p_group_id,,0, 225 r.customer_trx_id,r.trx_number,r.amt,r.lc,0); 226 ; 227 FOUND 228 ar_val. 229 (org_id,group_id,,db_side,return_status,msg_data) 230 (p_org_id,p_group_id,,0,1,substr(l_msg,1,2000)); 231 ; 232 COMMIT; 233 234 235 ar_val. 236 (org_id,group_id,,db_side,return_status,msg_data) 237 (p_org_id,p_group_id,,0,1,substr(SQLERRM,1,2000)); 238 COMMIT; 239 run_gauss; 240 241 diff 242 l_amt_ok ; l_cnt_ok ; l_err ; 243 244 -- 取本批次最新一次运行 245 MAX(run_id) g_run_id ar_val.; 246 -- 金额差异 247 ar_val. 248 (run_id, , gauss_value, oracle_value, 249 diff_ratio, is_critical) 250 g_run_id, 0, 251 TO_CHAR(g.total_amount), TO_CHAR(o.total_amount), 252 abs(g.total_amount - o.total_amount) / 253 NULLIF(greatest(abs(g.total_amount),abs(o.total_amount)),0), 254 0 255 ar_val. g 256 JOIN ar_val. o 257 ON g.org_id = o.org_id g.group_id = o.group_id 258 g.customer_trx_id = o.customer_trx_id 259 g.db_side=0 o.db_side=1 260 g.total_amount <> o.total_amount; 261 262 -- 行数差异 263 ar_val. 264 (run_id, , gauss_value, oracle_value, is_critical) 265 g_run_id, 0, 266 TO_CHAR(g.line_cnt), TO_CHAR(o.line_cnt), 0 267 ar_val. g 268 JOIN ar_val. o 269 ON g.org_id = o.org_id g.group_id = o.group_id 270 g.customer_trx_id = o.customer_trx_id 271 g.db_side=0 o.db_side=1 272 g.line_cnt <> o.line_cnt; 273 274 -- 状态差异 275 ar_val. 276 (run_id, , gauss_value, oracle_value, is_critical) 277 g_run_id, 0, 278 g.return_status, o.return_status, 0 279 ar_val. g 280 JOIN ar_val. o 281 ON g.org_id = o.org_id g.group_id = o.group_id 282 g.db_side=0 o.db_side=1 283 (g.return_status<>o.return_status g.msg_data<>o.msg_data); 284 285 COUNT(*) l_err ar_val. 286 run_id = g_run_id; 287 l_err = 0 0 1 ; 288 diff; 289 290 report(p_run_id ) 291 l_run := coalesce(p_run_id, g_run_id); 292 ; 293 294 LOG 0, l_run; 295 ( ,gauss_value,oracle_value,diff_ratio 296 ar_val. 297 run_id=l_run ORDER BY ) 298 LOG 0, 299 ., .gauss_value, .oracle_value, .diff_ratio; 300 ; 301 report; 302 ; 303/ 304SHOW ERRORS; 305

1

装 oracle_fdw:节点安装oracle_fdw_<port>包并加入shared_preload_libraries,LD_LIBRARY_PATH含 Oracle Client 库,重启集群。

># 验证 SELECT * FROM pg_available_extensions WHERE name='oracle_fdw';

2

建链:CALL xxx_oracle_fdw_conf_pkg.setup_fdw('ora_ebs','10.x.x.x','1521','ORCL','ar_readonly','***');

3

导表:

BEGIN xxx_oracle_fdw_conf_pkg.import_table('AR','RA_CUSTOMER_TRX'); xxx_oracle_fdw_conf_pkg.import_table('AR','RA_CUSTOMER_TRX_LINES'); xxx_oracle_fdw_conf_pkg.import_table('AR','RA_CUST_TRX_TYPES_ALL'); END; /

外部表统一落在ar_val.ora_<table>,不与原 Gauss 表冲突。

4

回放:

BEGIN xxx_ar_dual_validation_pkg.run_ora (101, 88001, 'JAN-26','INV'); xxx_ar_dual_validation_pkg.run_gauss (101, 88001, 'JAN-26','INV'); RAISE LOG 'DIFF=%', xxx_ar_dual_validation_pkg.diff(); xxx_ar_dual_validation_pkg.report(); END; /

5

判读:xxx_ar_dual_diff为空即OK;出现AMOUNT / LINE_CNT / STATUS / MISSING任一行,即DIFF,且is_critical='Y'——必须修复后才可切流。

校验维度通过标准说明
单据金额 (AMOUNT)方差 = 0abs(g-o)/greatest(|g|,|o|)为 NULL 或 0;含税价、折扣、汇兑差异需与业务方约定口径。
行数 (LINE_CNT)完全一致排除主退、COGS 等 Gauss 侧未实现的行类型,否则会误报。
状态 (STATUS)同为 'S'任一侧报 'E' 即阻断;msg_data一并留存便于定位。
单据存在性 (MISSING)双侧均生成用FULL OUTER JOIN补检,防止 Gauss 漏单或 Oracle 侧未生成。
组织隔离 (ORG)org_id 全匹配回归主包的g_Org_ID会话变量是否被正确设置,防止跨组织污染。

5.5 自动化回放与告警

-- pg_cron:每 30 分钟自动回放 + 每日 02:00 全量比对 SELECT cron.schedule('ar-dual-30m','*/30 * * * *', $$SELECT xxx_ar_dual_validation_pkg.run_gauss(101, interface_header_id, 'JAN-26','INV') FROM xxx_ar_pending_group WHERE processed='N'$$); SELECT cron.schedule('ar-dual-daily','0 2 * * *', $$SELECT xxx_ar_dual_validation_pkg.report()$$); -- 差异告警:推送至运维通道(示例为写日志,可替换为 http 扩展外发) DO $$ DECLARE v_cnt NUMBER; BEGIN SELECT COUNT(*) INTO v_cnt FROM ar_val.xxx_ar_dual_diff WHERE is_critical='Y' AND diff_id > (SELECT nvl(MAX(diff_id),0) FROM ar_val.xxx_ar_dual_diff WHERE is_critical='N'); IF v_cnt > 0 THEN RAISE LOG 'AR DUAL VALIDATION ALERT: % critical diffs', v_cnt; END IF; END; $$;

5.6 回滚方案

场景触发条件回滚动作
单笔业务失败Gauss 侧返回 'E' 且 DLX 达到上限保留 Gauss 数据为只读,业务改走 Oracle 侧同参路径;UPDATE xxx_ar_api_dlx SET status='NEW'待服务恢复后重放。
对账差异xxx_ar_dual_diff出现is_critical='Y'立即冻结 Gauss 出单,切流量开关回 Oracle;差异表按run_id导出给开发复现。
全量回退关键指标连续 3 个周期不达标应用层数据源指向 Oracle,Gauss 侧停pg_cron回放任务,保留ar_val.*与xxx_ar_api_dlx作为审计证据。

5.7 落地清单

✓

部署xxx_ar_STDCreateDepoInv_pkg_gauss.sql(主包,纯 DB 直插占位)

✓

部署xxx_ar_api_adapters_gauss.sql(API 适配,按 B 方案切到真实服务)

✓

部署xxx_ar_validation_gauss.sql(DLX 重放 + oracle_fdw 校验框架)

✓

配置sys_config_pkg中AR.INVOICE_SVC_URL / AR.DEPOSIT_SVC_URL

✓

建立 oracle_fdw 链,完成 5 步回放校验,差异清零

✓

启用pg_cron自动回放 + 差异告警,灰度切流

注:本工具包中 HTTP 调用、oracle_fdw、pg_cron、dblink 均为 GaussDB / openGauss 生态组件,具体可用版本与安装方式取决于你集群的版本与支持矩阵,部署前请以官方文档核对;占位实现中的单据编号、税码、SLA 会计分录、收款匹配规则须由 AR 产品方补齐,不得直接用于生产出单。

交付说明:本文档为"语法改写 + 接口抽象 + 可联调工程件"成果。主包xxx_ar_STDCreateDepoInv_pkg中对外 API 调用为占位实现,不得直接用于生产出单;须由 AR 产品方补齐单据编号、税码、SLA 会计分录、收款匹配规则,或按第⑤章切换至自研发票服务。

文件清单:

文件内容用途
xxx_ar_STDCreateDepoInv_pkg_gauss.sql主包(含前置 DDL)部署
xxx_ar_api_adapters_gauss.sql对外 API 适配包(HTTP + 重试 + DLX)联调 / 生产切流
xxx_ar_validation_gauss.sqlDLX 重放 + oracle_fdw 双库校验框架校验 / 对账
Ora_ERP.Admin.xxx_ar_STDCreateDepoInv_pkg.TXT原始 Oracle 源码(语义基线)对照