简介这份补丁包面向使用 Oracle 19c19.3的数据库管理员与运维人员用于为数据库补充 TSTZ 35 版本时区缓解因时区版本不匹配触发的 ORA-39405 错误。需要特别留意它仅适用于 19.3 版本其他版本执行会报错可结合 v$timezone_file 查询结果判断当前时区版本与适用性。包内共 10 个文件以 6 个 dat 数据文件为主辅以 2 个 xml 配置文件和 2 个 txt 说明文档压缩包约 394KB体积虽小但结构完整便于核对补丁内容与版本信息。已有 2570 人学习下载。对于遇到 TSTZ 版本不一致、需要将数据库时区升级至 35 版本的读者可借助其中的数据文件与说明快速确认补丁用途减少因版本误用造成的安装失败并为后续时区补丁维护提供参考。1. 8MB 的补丁包为什么要单独停机先看清时区版本 35 到底改了什么如果你手上有一套跑在 Linux 上的 Oracle 19c库里有TIMESTAMP WITH TIME ZONE列跨时区业务在夏令时切换那天对不上账先别怀疑 SQL——大概率是数据库内置的时区规则表停在了旧版本。p31335037_190000_Linux-x86-64.zip就是时区版本 35 的补丁包文件名里的190000对应 19.0.0Linux-x86-64是平台标识。它替换$ORACLE_HOME/oracore/zoneinfo下的时区数据文件和配套库文件让数据库认识最新一轮夏令时切换规则。适合还在维护 19c、库里有 TSTZ 数据、或者客户端与服务器时间对不齐的 DBA。注意它不是一个装完就完事的二进制补丁文件换掉之后数据字典里那套时区数据还得单独升一次这一步才是真正要排停机窗口的地方。2. 打补丁前的四项体检home、OPatch、时区版本与 TSTZ 数据分布时区补丁翻车的案例里有一半不是补丁本身的问题而是打之前没搞清楚“打哪个 home、谁来打、打完影响哪些表”。19c 的部署形态通常是 Grid Infrastructure home 加 RDBMS home时区补丁只跟数据库内核和客户端库相关所以第一步要确认你当前 shell 里的ORACLE_HOME到底指向谁而不是习惯性地cd $ORACLE_HOME就开干。2.1 先确认目标 home别把补丁打到 Grid 的家里Grid home 里也有 OPatch也有 inventory直接opatch apply也能跑起来但时区数据文件在 RDBMS home 的oracore/zoneinfo目录下打到 Grid home 等于白打。先看环境变量和 inventory 登记了哪些 home。# 当前 shell 指向的 ORACLE_HOME echo $ORACLE_HOME # 看 PATH 里排在前面的 oracle 相关路径确认没有旧 home 插队 echo $PATH | tr : \n | grep -i oracle # 中央 inventory 的位置后面查多 home 时要用 cat /etc/oraInst.loc 2/dev/null || cat /var/opt/oracle/oraInst.loc 2/dev/null # 列出当前 home 已安装的补丁和组件 $ORACLE_HOME/OPatch/opatch lsinventory -detail | head -60oraInst.loc里的inventory_loc指向中央 inventoryopatch lsinventory -detail只列当前 home 的内容。如果机器上有多个 RDBMS home最稳妥的办法是把每个 home 的opatch lsinventory都跑一遍记下哪个 home 挂着正在用的实例。RAC 共享 home 的场景下所有节点看到的是同一份文件只在一个节点操作即可但前提是所有实例都已经停掉。2.2 OPatch 版本与补丁 README两个必须先看的东西OPatch 版本不够新表现通常是Prereq checkOPatchVersion失败或者解压后opatch apply直接报不认识的补丁格式。19c 环境一般要求 12.2.0.1.17 以上但不同补丁包的门槛不完全一样以补丁自带的 README 为准。# 当前 OPatch 版本 $ORACLE_HOME/OPatch/opatch version # 解压后先看 README重点看 Prerequisites、Pre-installation、Post-installation、Rollback unzip -q p31335037_190000_Linux-x86-64.zip -d /u01/media/patch ls -l /u01/media/patch/31335037 sed -n 1,120p /u01/media/patch/31335037/README.html 2/dev/null | sed s/[^]*//gREADME 里会写清楚这个补丁是否要求先装某个前置补丁、是否与某些补丁冲突、以及打完之后的必做动作。时区补丁的 README 里通常会明确提到utltz_upg_check.sql和utltz_upg_apply.sql这两个脚本是后面第 4 章的主角。如果 OPatch 版本确实低先用 OPatch 的独立升级包把$ORACLE_HOME/OPatch目录整体替换掉再回来打时区补丁这一步不要跳过版本不匹配时 OPatch 的报错信息往往很含糊。2.3 查时区版本文件版本和字典版本要分开看时区这件事有两个版本一个是$ORACLE_HOME/oracore/zoneinfo下数据文件的版本另一个是数据库数据字典里记录的版本。打补丁只解决前者后者要靠脚本升级。所以打之前先把两个版本都记下来作为后面验证的基线。-- 以 sysdba 登录 -- sqlplus / as sysdba -- 1) 当前 home 下的时区文件版本FILENAME 一般是 timezlrg_XX.dat SELECT * FROM v$timezone_file; -- 2) 数据文件里支持的最新版本 SELECT DBMS_DST.GET_LATEST_TIMEZONE_VERSION FROM dual; -- 3) 数据库当前的升级状态和库时区 SELECT property_name, property_value FROM database_properties WHERE property_name IN (DST_UPGRADE_STATE, DBTIMEZONE);V$TIMEZONE_FILE的VERSION列就是文件版本打补丁前看到 31、32 这类数字都正常DBMS_DST.GET_LATEST_TIMEZONE_VERSION返回的是文件里的最新版本通常和前者一致。DST_UPGRADE_STATE为NONE表示没有进行中的升级如果看到PREPARE或UPGRADE说明之前有人开过升级窗口但没收尾这种情况要先处理状态再打补丁否则utltz_upg_apply.sql会直接拒绝执行。DBTIMEZONE这一项容易被忽略。库时区如果是00:00或UTC升级过程最顺如果建库时选了个带夏令时的地区名升级脚本在检查阶段就会提示风险。常见做法是业务库统一用UTC存库时区展示层再按会话时区转换这样时区补丁升级对业务的影响面最小。2.4 TSTZ 数据摸底与备份时区升级真正耗时的部分是重写所有TIMESTAMP WITH TIME ZONE列相关的数据。表越多、数据量越大UPGRADE模式跑的时间越长。打补丁前先摸清楚有哪些表受影响既是为了估时间也是为了确认回滚表空间够不够。-- 直接查数据字典列出所有带时区类型的列 SELECT owner, table_name, column_name, data_type FROM dba_tab_columns WHERE data_type LIKE TIMESTAMP%WITH TIME ZONE ORDER BY owner, table_name; -- 用 DBMS_DST 生成受影响表清单第一个参数是输出表名 SET SERVEROUTPUT ON DECLARE v_fail NUMBER : 0; BEGIN DBMS_DST.FIND_AFFECTED_TABLES(DST_AFFECTED_TABLES, TRUE, v_fail); DBMS_OUTPUT.PUT_LINE(fail count: || v_fail); END; / SELECT * FROM DST_AFFECTED_TABLES;FIND_AFFECTED_TABLES的第一个参数是结果表名第二个参数传TRUE表示表已存在时先删再建第三个参数是输出参数返回处理失败的对象数。这个清单在升级完成后还可以再跑一次对比前后是否一致。如果清单是空的说明库里没有 TSTZ 数据升级会快很多只需要更新字典如果清单里有大表停机窗口就要按“重写这些表”的量级来排。备份这一步没有捷径。时区升级改的是数据字典一旦中途失败靠回滚补丁是回不去的。# 先生成一份 pfile 备份防止 spfile 出问题时无法启动 sqlplus / as sysdba EOF CREATE PFILE/backup/pfile_before_dst35.ora FROM SPFILE; EXIT EOF # RMAN 全备加归档控制文件单独再备一份 rman target / EOF BACKUP DATABASE PLUS ARCHIVELOG; BACKUP CURRENT CONTROLFILE; EXIT EOF # 顺手把时区文件目录也备一份方便对比 cp -r $ORACLE_HOME/oracore/zoneinfo /backup/zoneinfo_before_dst35备份完成后把V$TIMEZONE_FILE、DST_UPGRADE_STATE、DST_AFFECTED_TABLES行数这三组值记在变更单里。后面验证时区 35 是否真的生效靠的就是这三组值的对比而不是凭感觉说“时间对了”。3. OPatch 落地从解压到 apply 的六步与回滚边界前面四项体检做完补丁本身的操作其实不复杂复杂的是边界条件什么时候能回滚、RAC 怎么处理、多 home 要不要都打。这一章按实际操作顺序走一遍每一步都给出可抄的命令和判断依据。3.1 解压与目录结构核对补丁包解压后是一个以补丁号命名的目录里面通常分etc/、files/和 README 三部分。files/下就是实际要被替换的文件包括oracore/zoneinfo下的时区数据和lib/下的客户端库。mkdir -p /u01/media/patch cd /u01/media/patch unzip -q p31335037_190000_Linux-x86-64.zip cd 31335037 ls -l # 典型结构README.html、etc/、files/ # 看 files 下到底换了哪些东西心里有数 find files -type f | head -30不要图省事手动把files/下的文件cp到$ORACLE_HOME。时区补丁除了替换数据文件还会更新 inventory 记录和部分二进制库手动拷贝会导致opatch lsinventory看不到这个补丁后面回滚和补丁冲突检查都会出问题。所有文件变更都交给opatch apply完成。3.2 冲突预检先出报告再决定要不要停库OPatch 支持只分析不应用正式停库之前先跑一遍报告把冲突和前置条件问题提前暴露出来。这一步在测试库上尤其值得做因为报告里会列出与现有补丁的冲突关系。# 生成分析报告不实际修改文件 $ORACLE_HOME/OPatch/opatch apply -report /tmp/dst35_report.txt # 看报告里有没有冲突、前置条件失败 grep -i -E conflict|prereq|fail|warning /tmp/dst35_report.txt如果报告里出现与其他补丁的冲突先确认那个补丁是不是必须保留。时区补丁和普通 RU/CPU 的冲突处理方式不完全一样有的冲突只是文件重叠、顺序问题有的则是真正的不兼容。报告里没有明确结论时以补丁 README 的说明为准不要凭经验猜。3.3 停实例与打补丁opatch apply要替换lib/下的共享库实例运行期间这些库被进程占用补丁会报CheckActiveFilesAndExecutables失败。所以标准动作是先停实例监听可以不停但保险起见把监听和监控 agent 一起停掉更省事。# 单实例环境 sqlplus / as sysdba EOF SHUTDOWN IMMEDIATE; EXIT EOF # 确认没有进程还占着 ORACLE_HOME 下的库 fuser -v $ORACLE_HOME/lib/libclntsh.so 2/dev/null ps -ef | grep -i ora_ | grep -v grep # 正式打补丁交互式会提示确认 cd /u01/media/patch/31335037 $ORACLE_HOME/OPatch/opatch apply # 无人值守场景可以加 -silent但首次建议交互式方便看提示 # $ORACLE_HOME/OPatch/opatch apply -silentRAC 环境稍有不同共享 ORACLE_HOME 只需要在一个节点执行opatch apply但执行前必须把所有节点的实例都停掉。常见做法是srvctl stop database -d db_unique_name在一个节点打完补丁再用srvctl start database统一起库。如果 home 不是共享的那每个节点都要单独打别只打一个节点就以为完事。3.4 核对 inventory 与文件版本补丁打完先别看数据库先确认补丁确实进了当前 home 的 inventory并且时区数据文件已经换成了版本 35。# inventory 里应该能看到 31335037 $ORACLE_HOME/OPatch/opatch lspatches | grep -i 31335037 # 文件层面确认版本 35 的数据文件存在 ls -l $ORACLE_HOME/oracore/zoneinfo/timezlrg_35.dat ls -l $ORACLE_HOME/oracore/zoneinfo/timezone_35.dat 2/dev/nullopatch lspatches的输出里这一行通常显示补丁号和补丁描述描述里会带TIMEZONE VERSION 35字样。如果 inventory 里有、文件也在说明二进制层面已经就位。此时数据库字典还是旧的V$TIMEZONE_FILE要等实例重启后才会反映新文件所以下一步是启动实例然后进入第 4 章的字典升级。3.5 回滚的正确姿势与边界opatch rollback -id 31335037可以把文件层面的变更退回去但它有一个硬边界如果已经跑过utltz_upg_apply.sql数据字典里的时区数据已经升级回滚补丁文件不会把字典降回去结果就是文件和字典版本不一致数据库可能起不来或者时间计算继续出错。# 仅在还没执行 utltz 字典升级时回滚才有完整意义 $ORACLE_HOME/OPatch/opatch rollback -id 31335037所以回滚窗口应该卡在“补丁打完、实例已启动、但utltz_upg_apply.sql还没跑”这个区间。一旦字典升级开始出问题的恢复手段就只剩从升级前的全备恢复。这也是为什么第 2 章反复强调备份时区升级不是那种“大不了回滚补丁”的操作。3.6 多 home 与客户端 home 的处理服务器端 RDBMS home 打完只解决了数据库自己识别新时区的问题。如果应用通过 Oracle Client 连接客户端 home 里的时区文件还是旧的某些驱动层做时区转换时仍会按旧规则算。判断要不要给客户端打看应用有没有在客户端侧做TIMESTAMP WITH TIME ZONE的转换有的话客户端 home 也要用同一个补丁包打一遍。同一台机器上既有 RDBMS home 又有 Client home 时两个 home 的opatch lsinventory要分别确认。RAC 共享 home 只打一次所有节点生效非共享 home 每个节点单独打。这些边界条件在变更前写成检查清单比在窗口里临时判断靠谱得多。4. 字典里的时区数据升级utltz_upg_check 到 utltz_upg_apply 的完整走法补丁文件就位只是前半程。数据库数据字典里存着一套时区规则业务表里的 TSTZ 数据也是按旧规则写入的这两部分要靠$ORACLE_HOME/rdbms/admin下的两个脚本升级。这一章把脚本的执行顺序、两阶段升级的触发条件、以及中途卡住时看哪里按实际操作讲清楚。4.1 启动实例确认文件版本已经切换补丁打完先正常启动实例确认数据库读到的时区文件已经是 35。STARTUP; -- 文件版本应该是 35 SELECT * FROM v$timezone_file; -- 数据文件支持的最新版本 SELECT DBMS_DST.GET_LATEST_TIMEZONE_VERSION FROM dual;如果V$TIMEZONE_FILE里FILENAME还是旧的timezlrg_31.dat先别往下走。要么补丁打到了错误的 home要么实例启动时读的还是旧路径。用ls -l $ORACLE_HOME/oracore/zoneinfo/确认文件存在再用opatch lsinventory确认补丁在当前 home两个都对还显示旧版本检查是不是有多个ORACLE_HOME环境变量在 profile 里被覆盖了。4.2 utltz_upg_check.sql先做一次只读检查检查脚本不改数据只输出当前状态和建议的升级路径。养成先跑 check、把日志 spool 下来的习惯出问题时这份日志就是排查依据。cd $ORACLE_HOME/rdbms/admin sqlplus / as sysdba EOF SPOOL /tmp/utltz_check.log ?/rdbms/admin/utltz_upg_check.sql SPOOL OFF EXIT EOF # 看检查结果里的关键行 grep -i -E timezone|version|error|warning|TSTZ /tmp/utltz_check.log脚本输出里会写明当前数据库时区版本、文件版本、是否检测到 TSTZ 数据、以及建议的升级方式。?在 sqlplus 里代表ORACLE_HOME如果当前 shell 的ORACLE_HOME没设对?会找不到文件这时候用绝对路径$ORACLE_HOME/rdbms/admin/utltz_upg_check.sql更稳。检查通过后再跑 apply。如果 check 阶段就报错常见原因是DST_UPGRADE_STATE不是NONE说明之前有未完成的升级或者DBTIMEZONE设置不合法。这两类问题都要先处理状态不能硬跑 apply。4.3 utltz_upg_apply.sql有 TSTZ 数据时的两阶段升级apply 脚本的行为取决于库里有没有 TSTZ 数据。没有 TSTZ 数据时脚本直接更新字典然后提示重启有 TSTZ 数据时脚本第一次运行只会做准备工作然后要求你把库重启到UPGRADE模式再跑第二次才真正重写数据。# 第一次执行 sqlplus / as sysdba EOF SPOOL /tmp/utltz_apply_round1.log ?/rdbms/admin/utltz_upg_apply.sql SPOOL OFF EXIT EOF如果日志里出现类似“Please shutdown the database and restart it in UPGRADE mode”的提示按下面的顺序走-- 第一阶段收尾进入升级模式 SHUTDOWN IMMEDIATE; STARTUP UPGRADE; -- 在 UPGRADE 模式下再跑一次 apply这次才是真正重写 TSTZ 数据 ?/rdbms/admin/utltz_upg_apply.sql -- 升级完成后正常重启 SHUTDOWN IMMEDIATE; STARTUP;第二次执行的时间取决于受影响表的数量和数据量。执行期间不要中断不要在这时候做其他 DDL。如果日志里出现 ORA 报错先看报错前的最后一个对象名再看DST_UPGRADE_STATE停在哪个状态不要急着重跑重跑之前必须确认上一轮没有留下半完成状态。4.4 DST_UPGRADE_STATE 的状态机与卡住时的处理DST_UPGRADE_STATE是判断升级进行到哪一步最直接的依据。正常结束后应该是NONE如果停在PREPARE说明准备阶段开了窗口但没关停在UPGRADE说明升级模式下的重写没完成。SELECT property_name, property_value FROM database_properties WHERE property_name DST_UPGRADE_STATE;停在PREPARE时常见做法是用DBMS_DST.END_PREPARE收尾把窗口关掉再重新走 check 和 apply。停在UPGRADE时不要直接重启到正常模式先看utltz_upg_apply的日志里最后处理到哪张表确认是回滚段不足还是对象锁等待处理完再重新进入UPGRADE模式续跑。时区升级的状态机不像普通 DDL 那样可以随意重试每次重试前都要确认上一轮的状态已经清理干净。4.5 升级期间的资源预留与时间预估TSTZ 数据重写会产生大量 undo 和 redo。受影响表多、单表数据量大时最容易翻车的地方是回滚表空间不够报ORA-30036: unable to extend segment by 8 in undo tablespace。打补丁前用第 2 章生成的DST_AFFECTED_TABLES清单结合DBA_TABLES.NUM_ROWS粗略估一下量级。-- 受影响表的行数分布用来判断升级窗口和 undo 需求 SELECT t.owner, t.table_name, t.num_rows FROM dba_tables t WHERE (t.owner, t.table_name) IN ( SELECT owner, table_name FROM DST_AFFECTED_TABLES ) ORDER BY t.num_rows DESC NULLS LAST;NUM_ROWS是统计信息里的值统计信息旧的话只能作为参考但用来判断“有没有百万级以上的大表”足够了。有大表就提前扩 undo 表空间或者在升级窗口前把业务写入停掉避免升级期间 undo 被其他事务抢占。redo 方面升级期间归档空间消耗会比平时快确认归档目录和 FRA 有足够余量。4.6 升级完成后的第一轮确认utltz_upg_apply.sql跑完并正常重启后先做一轮只读确认再放业务进来。-- 文件版本应为 35 SELECT * FROM v$timezone_file; -- 升级状态应回到 NONE SELECT property_name, property_value FROM database_properties WHERE property_name IN (DST_UPGRADE_STATE, DBTIMEZONE); -- CATPROC 组件状态应为 VALID SELECT comp_name, status, version FROM dba_registry WHERE comp_id CATPROC; -- 再跑一次受影响表清单和升级前对比 SET SERVEROUTPUT ON DECLARE v_fail NUMBER : 0; BEGIN DBMS_DST.FIND_AFFECTED_TABLES(DST_AFFECTED_TABLES_AFTER, TRUE, v_fail); DBMS_OUTPUT.PUT_LINE(fail count: || v_fail); END; /如果DST_AFFECTED_TABLES_AFTER是空的说明字典层面已经没有待升级的 TSTZ 数据如果还有记录说明上一轮升级没有覆盖到这些表需要回头看 apply 日志里有没有跳过的对象。这一步做完二进制和字典两层才算真正对齐。5. 避坑清单五个让升级卡死或时间继续偏移的现场时区补丁的坑集中在两个地方补丁应用阶段的环境问题和字典升级阶段的状态问题。下面五条按“现象 → 原因 → 解决”写都是实际窗口里容易遇到的。5.1 opatch apply 报 CheckActiveFilesAndExecutables 失败现象opatch apply在预检查阶段退出提示某个lib/*.so文件被占用补丁没有应用。原因实例虽然停了但监听、监控 agent、或者残留的 sqlplus 进程还开着$ORACLE_HOME/lib下的共享库。RAC 环境下如果只停了一个节点其他节点的实例还在跑也会触发同样的报错。解决先ps -ef | grep ora_确认没有实例进程再fuser -v查具体是哪个进程占着文件。把监听和 agent 一起停掉再打补丁。RAC 必须所有节点实例都停共享 home 只在一个节点执行 apply。5.2 打完补丁 V$TIMEZONE_FILE 还是旧版本现象opatch lspatches能看到 31335037但启动实例后V$TIMEZONE_FILE仍显示旧的文件名和版本号。原因最常见的是补丁打到了错误的 home比如打到了 Grid home 或者机器上另一个不用的 RDBMS home其次是实例启动时环境变量被 profile 覆盖读到了别的ORACLE_HOME。还有一种情况是实例根本没重启V$TIMEZONE_FILE读的是启动时加载的文件信息。解决用ls -l $ORACLE_HOME/oracore/zoneinfo/timezlrg_35.dat确认文件在用opatch lsinventory确认补丁在当前 home然后确认sqlplus / as sysdba连的就是这个实例最后完整重启一次实例再看。5.3 utltz_upg_apply 报 ORA-30036 回滚段不足现象在UPGRADE模式下跑 apply 脚本中途报ORA-30036: unable to extend segment by 8 in undo tablespace脚本中断。原因受影响表的数据量超过 undo 表空间的承载能力升级事务写满了回滚空间。大表多、或者升级期间还有别的写入事务在抢 undo都会触发。解决先确认DST_UPGRADE_STATE的状态把中断的事务清理干净给 undo 表空间加数据文件或扩大自动扩展上限停掉业务写入后再重新进入UPGRADE模式续跑。不要在有业务写入的时间段做 TSTZ 升级。5.4 升级后业务时间仍然差一小时现象数据库层面V$TIMEZONE_FILE显示 35DST_UPGRADE_STATE也是NONE但应用查出来的本地时间还是和预期差一小时。原因只打了服务器端补丁、没跑字典升级或者反过来跑了脚本但客户端 home 没打补丁。还有一种情况是应用连接串里显式设置了会话时区覆盖了数据库的默认转换逻辑。解决服务器端先按第 4 章确认字典升级完成客户端 home 用同一个补丁包打一遍再检查应用连接池初始化时有没有执行ALTER SESSION SET TIME_ZONE把会话时区和业务预期对一遍。三层都确认后再判断是不是业务 SQL 本身的转换写法问题。5.5 RAC 环境每个节点都跑了一遍 utltz 脚本现象RAC 环境里有人在每个节点上都执行了utltz_upg_apply.sql第二个节点开始报错或者状态变成UPGRADE后无法继续。原因时区字典升级是数据库级操作不是节点级操作。共享 home 的 RAC 只需要在一个节点、以数据库级别执行一次其他节点不需要重复跑。解决停掉所有实例用一个节点STARTUP UPGRADE执行脚本完成后SHUTDOWN IMMEDIATE再用srvctl start database统一起库。已经重复执行导致状态异常的先查DST_UPGRADE_STATE用DBMS_DST的相关过程清理状态后再重走流程。6. 验证与回退用三条查询确认时区 35 真的生效时区补丁打完最忌讳的验证方式是“看业务时间好像对了”。时间对不对受会话时区、客户端转换、应用逻辑多层影响必须回到数据库层面拿硬指标。我一般用三条查询做第一轮确认再用一个具体切换点做第二轮抽样。-- 第一条文件版本FILENAME 应为 timezlrg_35.datVERSION 为 35 SELECT * FROM v$timezone_file; -- 第二条字典升级状态和库时区状态应为 NONE SELECT property_name, property_value FROM database_properties WHERE property_name IN (DST_UPGRADE_STATE, DBTIMEZONE p a hrefhttps://download.csdn.net/download/leo52066/13105124 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p SEO 优化官网定制响应式建站教育培训建站