ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Oracle EBS API注册本质:权限、调度与可执行单元的三层缝合

Oracle EBS API注册本质:权限、调度与可执行单元的三层缝合

1. 这不是“点几下就完事”的配置——EBS中API注册的本质是权限链路的精准缝合

在某公司上线新财务对账模块时,开发团队写好了完整的PL/SQL API包,封装了凭证校验、余额比对、差异标记三大核心逻辑。他们信心满满地把包部署到测试环境,调用测试脚本返回“ORA-00942: table or view does not exist”——可表明明存在,且所有者权限也已授予。折腾两天后才发现:这个API根本没被注册为“可执行程序”,更未绑定到任何并发管理器(Concurrent Manager)队列。系统压根不认它是个“合法服务”,自然拒绝一切调用请求。

这就是EBS里最常被低估的环节:API注册不是发布一个函数,而是把一段代码嵌入整套安全与调度体系的神经末梢。它不像Web API那样暴露一个URL就能调用,而必须完成三重身份认证:

  • 代码身份:确认PL/SQL包体(Package Body)已编译通过,且包含标准的main过程签名(p_errbuf OUT VARCHAR2, p_retcode OUT NUMBER);
  • 系统身份:在Application Object Library( AOL)中创建“可执行程序”(Executable)记录,明确指定其类型(如“PL/SQL Stored Procedure”)、执行文件路径(即包名+过程名);
  • 调度身份:将该可执行程序绑定到“并发程序”(Concurrent Program),再分配给特定的并发管理器(如“Standard Manager”或自定义队列),最终才能被用户提交、被后台进程抓取执行。

关键词里虽未明示,但实际操作中绕不开的硬性依赖有三个:并发管理器(Concurrent Manager)、应用对象库(AOL)、数据库权限模型(GRANT EXECUTE ON package TO apps)。这三者构成EBS并发架构的铁三角——缺一不可。比如,即使你把可执行程序注册得完美无瑕,若目标并发管理器处于“禁用”状态,或者未配置足够线程数,任务永远卡在“Pending”队列里,连日志都不会生成一行。

我见过太多团队把精力全耗在PL/SQL逻辑优化上,却在注册环节栽跟头。最典型的是忽略“应用产品”(Application)上下文绑定:EBS要求每个并发程序必须归属于某个应用产品(如“General Ledger”或“Payables”),而这个应用产品必须与调用它的责任(Responsibility)权限域严格匹配。曾有个项目因误将财务API注册到“System Administrator”应用下,导致业务用户始终提示“您无权访问此程序”,排查三天才发现是应用产品ID填错了两位数字。

提示:EBS的并发程序注册不是独立动作,它本质是把数据库层的PL/SQL能力,翻译成应用层可识别、可授权、可调度的服务单元。理解这一点,才能避开80%的“注册成功但无法调用”的伪故障。

2. 可执行程序注册的四个致命细节——漏掉任意一项,API永远在黑盒里沉睡

注册可执行程序(Executable)看似只是在“系统管理员→并发→可执行”菜单里填几个字段,但每个字段背后都藏着EBS底层调度引擎的硬性校验逻辑。我整理了过去三年在多个EBS R12.2.x环境踩过的坑,把最关键的四个细节拆解如下:

2.1 “执行方法”(Execution Method)必须与代码类型严格对应,不存在“兼容模式”

EBS预置了十几种执行方法,但真正用于PL/SQL API的只有两类:

  • “PL/SQL Stored Procedure”:适用于标准包过程,要求过程签名必须为p_errbuf OUT VARCHAR2, p_retcode OUT NUMBER,且过程名需与可执行程序定义中的“执行文件”字段完全一致(如XXFIN_PKG.validate_voucher);
  • “Host”:用于调用操作系统命令,与PL/SQL无关,切勿混淆。

曾有团队尝试用“SQLPlus”执行方法去跑PL/SQL包,结果日志里只显示“SQLPlus exited with status 1”,没有任何错误堆栈。原因在于:SQL*Plus执行方法会启动一个独立的sqlplus进程,它无法捕获PL/SQL包内部的异常,只会返回进程退出码。而“PL/SQL Stored Procedure”执行方法由FNDLIBR进程直接调用,能完整传递p_errbuf中的错误信息。

注意:执行方法一旦保存,无法修改。若选错,必须删除重建——因为EBS底层表fnd_executables中execution_method_code字段为不可更新字段。

2.2 “执行文件”(Execution File)字段不是路径,而是“可执行单元”的唯一标识符

这个字段常被误填为/usr/bin/sqlplus或$AU_TOP/bin/xxfin_pkg.sql,这是绝对错误的。对于PL/SQL类型,它必须是**“包名.过程名”** 的纯文本格式,且区分大小写。例如:

  • 正确:XXFIN_VALIDATION_PKG.validate_invoice
  • 错误:xxfin_validation_pkg.validate_invoice(小写包名,而数据库中实际为大写)
  • 错误:XXFIN_VALIDATION_PKG.VALIDATE_INVOICE@PROD(带数据库链接,EBS不支持)

验证方法极简单:登录数据库,以APPS用户执行SELECT text FROM all_source WHERE name = 'XXFIN_VALIDATION_PKG' AND type = 'PACKAGE BODY' AND line = 1;,确认包名拼写与大小写完全一致。我建议在开发阶段就强制约定:所有EBS集成包名统一为大写,避免大小写陷阱。

2.3 “应用产品”(Application)字段决定权限边界,不是随便选一个就行

这个下拉框里的选项,来自fnd_application_tl表中enabled_flag = 'Y'的应用产品列表。关键点在于:并发程序的“应用产品”必须与调用它的“责任”(Responsibility)所关联的应用产品权限集完全匹配。例如:

  • 若责任“AP Clerk”被赋予“Payables”和“Financials”两个应用产品权限;
  • 而你把API注册在“System Administrator”应用下;
  • 那么该责任下的用户永远看不到这个程序,更无法提交。

解决方案不是“把程序挪到Payables下”,而是检查责任的权限配置。我通常会导出该责任的fnd_responsibility记录,核对其application_id是否包含目标应用产品的ID。一个快速验证技巧:在“系统管理员→责任→定义”中,点击目标责任,查看“应用产品”标签页里勾选的项——这里列出的,才是该责任真正拥有的应用产品权限。

2.4 “输出类型”(Output Type)影响日志生成机制,与调试强相关

这个字段常被设为“Text”,但它决定了并发管理器如何处理程序输出:

  • “Text”:输出内容直接写入并发请求日志(log file),适合调试;
  • “XML”:输出必须符合XDO模板规范,否则解析失败;
  • “PDF”/“Excel”:需配置相应格式转换器,否则报错“Unable to generate output”。

曾有个项目因设为“PDF”但未安装Adobe PDF printer,导致所有请求卡在“Running”状态。后来发现,EBS在生成PDF时会调用adspdf工具,而该工具依赖系统级打印机驱动。对于纯API调用(无需生成报表),务必选“Text”,确保p_errbuf中的错误信息能原样输出到日志,这是排错的第一手资料。

3. 并发程序(Concurrent Program)绑定的三重校验——为什么“注册成功”不等于“能被调用”

完成可执行程序注册后,下一步是在“系统管理员→并发→程序”中创建并发程序,并将其与刚注册的可执行程序绑定。这步操作看似简单,但EBS在此处设置了三道隐形关卡,每一道都会让API失效于无声无息之间。

3.1 “可执行程序”字段的下拉值,只显示“已启用”且“应用产品匹配”的条目

当你在新建并发程序界面点击“可执行程序”下拉框时,看到的列表并非所有已注册的可执行程序,而是经过双重过滤的结果:

  1. fnd_executables.enabled_flag = 'Y'(启用状态);
  2. fnd_executables.application_id = :current_app_id(当前责任所属应用产品的ID)。

这意味着:如果你在“System Administrator”责任下注册了一个可执行程序,但在“Payables”责任下创建并发程序,该可执行程序根本不会出现在下拉列表中。解决方案只有两个:要么切换到同一应用产品的责任下操作,要么在注册可执行程序时,将“应用产品”字段明确设为“Payables”(ID=200)。

我习惯在注册前先查表确认:SELECT application_id, application_name FROM fnd_application_tl WHERE UPPER(application_name) LIKE '%PAY%';获取准确的应用产品ID,避免凭记忆填写。

3.2 “参数”(Parameters)定义必须与PL/SQL过程的IN参数严格一致,包括顺序、名称、数据类型

EBS并发框架在调用PL/SQL过程前,会将用户在提交界面输入的参数值,按定义顺序拼装成一个字符串,再通过DBMS_SQL动态执行。因此,参数定义的任何偏差都会导致运行时错误。常见错误有:

  • 顺序错位:PL/SQL过程定义为(p_invoice_id IN NUMBER, p_period IN VARCHAR2),但并发程序参数定义为先p_period后p_invoice_id,则传入值会错乱;
  • 名称不匹配:并发程序参数名为invoice_num,而过程期望p_invoice_id,EBS不会做映射,直接报“PLS-00306: wrong number or types of arguments”;
  • 数据类型失配:过程参数为NUMBER,但并发程序定义为“字符型”,EBS会尝试TO_NUMBER()转换,若用户输入非数字则崩溃。

实操中,我坚持“代码先行”原则:先写好PL/SQL过程,再用以下SQL自动生成参数定义草稿:

SELECT argument_name AS "参数名", data_type AS "数据类型", in_out AS "方向", position AS "位置" FROM all_arguments WHERE object_name = 'XXFIN_VALIDATION_PKG' AND package_name = 'XXFIN_VALIDATION_PKG' AND owner = 'APPS' ORDER BY position;

然后逐条复制到并发程序参数界面,确保零误差。

3.3 “专用”(Special)和“阶段”(Phase)字段控制调度优先级,影响高并发场景下的响应

这两个字段极少被关注,但在生产环境至关重要:

  • “专用”:若勾选,该并发程序只能由指定的并发管理器执行(需在“管理器”标签页指定);若不勾选,则可由任何满足条件的管理器抓取;
  • “阶段”:决定程序在并发请求生命周期中的执行时机。“Normal”表示常规执行,“Restart”表示仅在请求重启时运行。

曾有个批处理API因未勾选“专用”,被误分配到“Report Manager”(专为报表设计)而非“Standard Manager”,导致执行超时被强制终止。原因是Report Manager默认内存限制较低,而该API需加载大量缓存数据。解决方案是:为关键业务API创建专用管理器(如“XXFIN_Manager”),并在并发程序中勾选“专用”,再绑定到该管理器。

提示:专用管理器的配置在“系统管理员→并发→管理器→定义”。需设置“最大进程数”、“内存限制(MB)”、“睡眠时间(秒)”等参数。我通常将业务API管理器的内存限制设为512MB,远高于Report Manager的128MB默认值。

4. 权限分发与责任绑定——让API从“系统可见”变成“用户可用”的最后一步

即使可执行程序和并发程序全部注册成功,普通用户依然看不到、无法提交该程序——因为EBS的权限模型是四层嵌套结构:数据库对象权限 → 应用产品权限 → 责任权限 → 用户责任分配。任何一层缺失,API就形同虚设。

4.1 数据库层:APPS用户必须拥有EXECUTE权限,且不能依赖角色继承

这是最基础也最容易被忽略的一环。很多团队在开发库中用GRANT EXECUTE ON XXFIN_PKG TO APPS;,但忘了在生产库执行。更隐蔽的坑是:如果XXFIN_PKG依赖其他schema的表,而这些表的权限是通过角色(如SELECT_CATALOG_ROLE)授予APPS的,那么EBS并发框架将无法使用该角色权限——因为FNDLIBR进程以APPS用户直接连接,不激活角色。

验证方法:以APPS用户登录数据库,执行SELECT * FROM session_roles;,确认所需角色已激活。若未激活,必须显式授予对象权限:

-- 不要依赖角色 GRANT SELECT ON fin_schema.gl_balances TO APPS; GRANT EXECUTE ON xxfin_schema.xxfin_pkg TO APPS;

4.2 应用产品层:必须将并发程序添加到目标应用产品的“功能”(Function)列表中

EBS中,所有可访问的功能(包括并发程序)都必须注册为“功能”(Function),并归属于某个应用产品。这步在“系统管理员→应用→功能”中完成:

  • 创建新功能,类型选“Concurrent Program”;
  • “功能名称”填一个易读名(如“XXFIN Validate Invoice”);
  • “功能代码”填唯一编码(如“XXFIN_VALIDATE_INV”);
  • “应用产品”必须与并发程序一致;
  • “Web HTML Call”字段留空(并发程序无需HTML调用)。

这步完成后,该功能才真正成为应用产品权限体系的一部分。若跳过,后续无论怎么赋权,用户都无法看到该程序。

4.3 责任层:必须将功能添加到责任的“请求组”(Request Group)中

这才是用户能否看到API的决定性步骤。请求组是责任的权限容器,它定义了该责任下用户可提交哪些并发程序。操作路径:“系统管理员→安全→责任→请求组”:

  • 找到目标责任对应的请求组(如“Payables Full Access”);
  • 点击“阶段”标签页,选择“并发程序”;
  • 在“可用的并发程序”列表中,找到刚注册的程序,点击“>”加入右侧“选定的并发程序”列表;
  • 保存。

关键细节:请求组有“标准”和“自定义”之分。若目标责任使用的是标准请求组(如“All Reports”),则不能直接修改,必须先复制为自定义请求组,再添加程序。否则保存时会提示“无法修改标准请求组”。

4.4 用户层:必须将责任分配给具体用户,并验证责任激活状态

最后一步是将配置好的责任分配给用户:“系统管理员→安全→用户→定义”,找到目标用户,在“责任”标签页中添加该责任。但注意:

  • 用户可能拥有多个责任,需确认当前登录时激活的是正确责任;
  • 责任的“开始日期”和“结束日期”必须覆盖当前日期,否则不生效;
  • 若用户是通过LDAP同步的,需确认同步策略是否包含责任字段。

验证方法:以该用户登录,进入“工作台→请求→提交请求”,在“程序名称”下拉框中搜索程序名。若出现,说明全流程打通;若无,则按上述四层逐级回溯。

注意:EBS权限缓存机制可能导致修改后延迟生效。若配置无误但用户仍看不到,执行“系统管理员→并发→管理器→管理员”中的“清理缓存”请求,或等待15分钟自动刷新。

5. 实战排错链路——当API“注册成功却无法调用”时,我如何30分钟内定位根因

在某次紧急上线中,财务团队反馈新API“XXFIN Validate Invoice”在测试环境注册后,提交请求始终卡在“Pending”状态,日志为空。以下是我在现场执行的标准化排查流程,全程32分钟,最终定位到并发管理器配置问题。

5.1 第一层:确认请求是否真正进入调度队列(耗时3分钟)

登录数据库,查询并发请求状态:

SELECT request_id, phase_code, status_code, hold_flag, requested_start_date, actual_start_date FROM fnd_concurrent_requests WHERE program_application_id = 200 -- Payables应用ID AND concurrent_program_id = (SELECT concurrent_program_id FROM fnd_concurrent_programs WHERE concurrent_program_name = 'XXFIN_VALIDATE_INV') ORDER BY request_date DESC;

结果发现phase_code = 'P'(Pending),status_code = 'I'(Inactive),且hold_flag = 'N'。这说明请求已提交,但未被任何管理器抓取,问题出在调度层。

5.2 第二层:检查并发管理器状态与负载(耗时5分钟)

查询管理器状态:

SELECT manager_type, running_processes, max_processes, actual_start_date, enabled_flag FROM fnd_concurrent_queues WHERE queue_application_id = 0 -- Standard Manager AND queue_name = 'STANDARD';

发现running_processes = 0,enabled_flag = 'N'!原来运维同事在前一天维护时,误将Standard Manager设为禁用。立即执行:

UPDATE fnd_concurrent_queues SET enabled_flag = 'Y', running_processes = 5 WHERE queue_name = 'STANDARD'; COMMIT;

但请求仍未变化——因为管理器需要重启才能生效。

5.3 第三层:验证管理器重启与进程拉起(耗时12分钟)

在操作系统层面,执行EBS管理脚本重启Standard Manager:

# 切换到应用用户 su - applmgr # 进入管理脚本目录 cd $ADMIN_SCRIPTS_HOME # 停止Standard Manager ./adcmctl.sh stop apps/apps @=STANDARD # 启动Standard Manager ./adcmctl.sh start apps/apps @=STANDARD

等待脚本返回“Manager started successfully”,再查数据库:

SELECT running_processes FROM fnd_concurrent_queues WHERE queue_name = 'STANDARD';

确认running_processes > 0。此时再查请求表,phase_code已变为'R'(Running),说明管理器开始处理。

5.4 第四层:捕获运行时错误并修正PL/SQL逻辑(耗时12分钟)

请求状态变为“Running”后,很快又变成'C'(Completed)但status_code = 'E'(Error)。查日志:

SELECT request_id, logfile_name, outfile_name FROM fnd_concurrent_requests WHERE request_id = 123456789;

日志文件路径为$APPLCSF/$APPLLOG/l123456789.log,查看内容发现关键错误:
ORA-01422: exact fetch returns more than requested number of rows
定位到PL/SQL包中SELECT ... INTO ...语句,因数据异常返回多行。修复逻辑后重新编译包,再次提交请求,status_code = 'C'(Normal),日志显示“Validation completed successfully”。

整个过程没有一句“重启试试看”,而是基于EBS并发架构的分层模型,从请求状态→管理器状态→进程状态→代码逻辑,逐层下沉,每一步都有明确的验证SQL或命令。这种结构化排错能力,比记住一百个报错代码更有价值。

6. 生产环境黄金配置清单——让API注册一次,稳定运行三年的12个实操要点

基于在金融、制造、零售行业多个EBS R12.2.x生产环境的运维经验,我总结出一套经实战检验的API注册配置清单。这些不是文档里的“应该”,而是“必须”,漏掉任意一条,都可能在某个业务高峰时刻引发雪崩。

6.1 可执行程序配置要点(4项)

  1. 执行方法锁定为“PL/SQL Stored Procedure”:绝不使用“SQL*Plus”或“Host”,避免进程隔离导致的错误屏蔽;
  2. 执行文件严格使用大写包名+过程名:如XXFIN_PKG.VALIDATE_INVOICE,并在注册前用ALL_SOURCE表确认拼写;
  3. 应用产品ID必须与业务责任一致:通过SELECT application_id FROM fnd_application_tl WHERE application_short_name = 'SQLGL';获取准确ID;
  4. 输出类型强制设为“Text”:确保所有p_errbuf内容原样输出到日志,为排错保留第一手证据。

6.2 并发程序配置要点(4项)

  1. 参数定义必须与PL/SQL过程IN参数100%一致:包括顺序、名称、数据类型,用ALL_ARGUMENTS视图自动生成草稿;
  2. 勾选“专用”并绑定到业务专用管理器:为财务API创建XXFIN_MANAGER,内存限制设为512MB,避免与报表任务争抢资源;
  3. “启用”标志必须为“Yes”:fnd_concurrent_programs.enabled_flag = 'Y',这是EBS前端界面的硬性开关;
  4. “可提交”标志必须为“Yes”:fnd_concurrent_programs.submit_flag = 'Y',否则用户界面不显示提交按钮。

6.3 权限与分发要点(4项)

  1. 数据库权限必须显式授予,禁用角色依赖:GRANT EXECUTE ON XXFIN_PKG TO APPS;,且所有依赖对象权限同样显式授予;
  2. 功能(Function)必须注册到正确应用产品下:功能代码命名规范为XX<APP>_XXX(如XXGL_BALANCE_CHECK),便于审计;
  3. 请求组必须为自定义,禁止修改标准组:复制“All Reports”为“XXFIN Custom Requests”,再添加程序;
  4. 用户责任分配后,必须执行“清理缓存”请求:在“系统管理员→并发→管理器→管理员”中提交“Purge Concurrent Manager Data”,强制刷新权限缓存。

这些要点看似琐碎,但每一条都对应一个真实故障场景。比如第2条“大写包名”,曾让我们在跨环境迁移时避免了一次上线失败;第6条“专用管理器”,在双十一大促期间保障了订单对账API的毫秒级响应。它们不是教条,而是用时间和故障换来的肌肉记忆。

7. 从注册到监控——构建API全生命周期管理的最小可行方案

注册完成只是起点。在某银行核心系统中,我们曾因忽视API的长期可观测性,导致一次批量对账失败未被及时发现,造成T+1日账务延迟。为此,我们构建了一套轻量但有效的API全生命周期管理方案,仅用3个脚本+1个视图,就实现了从注册到告警的闭环。

7.1 注册即建档:自动化元数据采集脚本

每次注册新API,必须运行以下SQL脚本,将关键元数据存入自定义表xxfin_api_registry:

INSERT INTO xxfin_api_registry ( api_code, executable_name, concurrent_program_name, application_short_name, created_by, creation_date, last_updated_by, last_update_date ) SELECT 'XXFIN_VAL_INV' AS api_code, 'XXFIN_VALIDATION_PKG.VALIDATE_INVOICE' AS executable_name, 'XXFIN_VALIDATE_INV' AS concurrent_program_name, 'SQLGL' AS application_short_name, fnd_global.user_id, SYSDATE, fnd_global.user_id, SYSDATE FROM dual;

该表成为所有API的唯一真相源,后续监控、审计、文档均从此表抽取。

7.2 运行即监控:并发请求健康度视图

创建视图xxfin_api_health_v,实时聚合关键指标:

CREATE OR REPLACE VIEW xxfin_api_health_v AS SELECT r.concurrent_program_id, p.concurrent_program_name, COUNT(*) AS total_requests, SUM(CASE WHEN r.status_code = 'C' THEN 1 ELSE 0 END) AS success_count, SUM(CASE WHEN r.status_code = 'E' THEN 1 ELSE 0 END) AS error_count, ROUND(AVG(r.actual_completion_date - r.actual_start_date) * 24 * 60, 2) AS avg_duration_min, MAX(r.request_date) AS last_run_time FROM fnd_concurrent_requests r JOIN fnd_concurrent_programs p ON r.concurrent_program_id = p.concurrent_program_id WHERE p.concurrent_program_name LIKE 'XXFIN%' AND r.request_date > SYSDATE - 7 GROUP BY r.concurrent_program_id, p.concurrent_program_name;

每天早会,DBA只需运行SELECT * FROM xxfin_api_health_v;,一眼看清各API的周成功率、平均耗时、最近运行时间。

7.3 异常即告警:失败请求自动邮件脚本

编写Shell脚本check_xxfin_api_failures.sh,每日凌晨2点执行:

#!/bin/bash # 查询过去24小时失败的XXFIN API请求 sqlplus -s apps/apps <<EOF SET PAGESIZE 0 FEEDBACK OFF VERIFY OFF HEADING OFF ECHO OFF SPOOL /tmp/xxfin_api_failures.txt SELECT 'Failed API: ' || p.concurrent_program_name || ', Request ID: ' || r.request_id || ', Error: ' || r.logfile_name FROM fnd_concurrent_requests r JOIN fnd_concurrent_programs p ON r.concurrent_program_id = p.concurrent_program_id WHERE p.concurrent_program_name LIKE 'XXFIN%' AND r.status_code = 'E' AND r.request_date > SYSDATE - 1; SPOOL OFF EXIT EOF # 若有失败,发送邮件 if [ -s /tmp/xxfin_api_failures.txt ]; then mailx -s "ALERT: XXFIN API Failure Detected" devops@company.com < /tmp/xxfin_api_failures.txt fi

该脚本接入公司邮件网关,确保任何失败在1小时内触达负责人。

7.4 演进即归档:版本变更追踪机制

每次PL/SQL包升级,必须更新xxfin_api_registry表的last_update_date字段,并在comments列记录变更摘要:

UPDATE xxfin_api_registry SET last_updated_by = fnd_global.user_id, last_update_date = SYSDATE, comments = 'v2.1: Fixed ORA-01422 by adding BULK COLLECT' WHERE api_code = 'XXFIN_VAL_INV';

配合数据库ALL_SOURCE的历史版本查询,形成完整的API演进地图。

这套方案不依赖昂贵的APM工具,全部基于EBS原生能力构建。它让API从“注册即遗忘”的黑盒,变成“可观测、可预警、可追溯”的生产资产。在我参与的最后一个项目中,这套机制帮助团队将API平均故障恢复时间(MTTR)从4.2小时压缩到18分钟。

我个人在实际操作中的体会是:EBS的API注册,表面是填几个字段,实质是把一段代码编织进企业级应用的权限、调度、监控三张大网。填错一个字段,就像织网时打错一个结,平时无感,风来即断。真正的资深,不在于知道所有字段含义,而在于理解每个字段在整张网中的承重位置——以及,当网断了,如何顺着丝线,一秒找到那个松动的结。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进