#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ P2 第一步:把老工程 zteits-clouds 的核心模块搬进 parking-cloud。 ## 设计原则(对应 AGENTS.md §3「搬迁存量代码的顺序」) 1. **只搬不改**——包名、类名、mapper XML 的 namespace / resultMap type 全部原样保留。 老 XML 里 `namespace="com.zteits.park.mapper.TpPRegionAreaMapper"`、 `type="com.zteits.park.domain.TpPRegionArea"` 用的是全限定名,改包名会牵动 上千个 XML 与实体,收益为零、风险极高。因此新模块**沿用老包名** (`com.zteits.park.*` / `com.zteits.order.*` / `com.clouds.common.*` / `com.zteits.clouds.api.*`),搬迁动作只是「把文件挪到另一个 Maven 模块」。 2. **COPY 而非 MOVE**——老仓库保持可运行、可回溯,直到 P8 下线。 3. **不搬**:启动类(会与 ParkingServerApplication 抢 @SpringBootApplication)、 Dubbo 过滤器、代码生成器、`application*.properties` / `logback-spring.xml` (环境配置以新工程为准,见红线第 9 条)、`context/*.xml`(里面是 DataSource/SqlSessionFactory 的旧装配,新工程由 parking-spring-boot-starter-mybatis 接管)、 `dubbo/*.xml`(要删的东西)、`generator/`、证书文件(红线第 11 条:不提交真实密钥)。 ## ⚠️ 一次真实事故,以及本脚本现在的删除策略 本脚本原先为了「可重复执行」会 `shutil.rmtree(目标目录)`。但 `parking-module-system` 和 `parking-module-api` 这两个模块**同时装着 P1 手写的 新代码和迁移进来的老代码**,于是那一次执行把 P1 写的 96 个类(`com.zteits.parking.*` 的 system 域 + apibase 契约)连同老代码一起删了,只剩 `target/classes` 里的 .class 文件(后由 `tools/recover/` 从会话记录恢复,详见 AGENTS.md)。 因此现在的规则是: * **默认只增不删**——直接覆盖同名文件,不删除任何东西。这是安全默认值。 * `--prune` 才会删,且**只删「本次导入会写出去的同一批相对路径」**,也就是 目的目录里确实由老工程镜像过来的文件;凡是老工程里没有对应源的(= 手写的), 一律留着。这样既保住了「改名/删除后不留残骸」,又不可能再误删新代码。 * 另有一层兜底:任何落在 PROTECTED_PREFIXES 下的路径,即使理论上算「待删」也拒绝删。 用法: python3 tools/p2_import_legacy.py [--dry-run] [--prune] """ import argparse import os import shutil import sys LEGACY = "/Users/wangbiao/IdeaProjects/parking/zteits-clouds" CLOUD = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # --------------------------------------------------------------------------- # 模块映射:新模块 → (老模块, 要搬的相对路径列表) # --------------------------------------------------------------------------- SOURCES = [ # 契约层:416 个 Dubbo 接口 + 947 个 DTO,包名 com.zteits.clouds.api.* 原样保留 ("parking-module-api", "zteits-api", ["src/main/java"]), # 框架层:com.clouds.common.*,单独一个 Maven 模块,不往 parking-common 里揉 ("parking-framework/parking-legacy-common", "zteits-framework", ["src/main/java"]), # 业务模块 ("parking-module-park", "zteits-park", ["src/main/java", "src/main/resources/mybatis"]), ("parking-module-order", "zteits-order", ["src/main/java", "src/main/resources/mybatis"]), ("parking-module-pay", "zteits-pay", ["src/main/java", "src/main/resources/mybatis"]), ("parking-module-bill", "zteits-bill", ["src/main/java", "src/main/resources/mybatis"]), ("parking-module-charge", "zteits-charge", ["src/main/java", "src/main/resources/mybatis"]), ("parking-module-business", "zteits-business", ["src/main/java", "src/main/resources/mybatis"]), # 系统域:老 zteits-sys 并进已存在的 parking-module-system ("parking-module-system", "zteits-sys", ["src/main/java", "src/main/resources/mybatis"]), ] # 老工程里 com/clouds/common 下需要额外搬的资源(PDF 字体、二维码底图等运行时资源) FRAMEWORK_RESOURCES = [ "fdfs_client.conf", "fonts.xml", "images", "reportpdf", ] # --------------------------------------------------------------------------- # 按文件精选的随迁清单(不是整目录搬) # --------------------------------------------------------------------------- # 用于「老模块整体不搬,但其中有一小撮类被核心模块在运行期调用」的场景。 # 每条 = (目标模块, 老模块, [相对 src/main/java 的文件路径...])。 # # 目前只有企业微信一处。老工程 zteits-wxcp-portal 是**独立 JVM**(端口 18031), # 通过 Dubbo 向 park / order / pay / bill 提供 # `com.zteits.clouds.api.service.wxcp.SendMessageService`(收费员越界、设备故障、 # 优惠券到期等告警)。合成单 Jar、Dubbo 移除后这些调用变成同进程注入, # 实现必须搬进来 —— 否则启动即 # `No qualifying bean of type ...SendMessageService`(有 4 处是裸 @Autowired 的调用方)。 # # 为什么不整目录搬:这个模块里大部分是**门户 Web 层** # (`controller/`、`SwaggerConfig`、`FilterRegistrationConfig` + `filter/`、启动类), # 属于方案 §4.1「portal 不整体搬迁」的范畴;且 `SwaggerConfig` 走 springfox, # 而新栈是 springdoc,搬过来只会多一个编译不过的类。 # 所以只搬「企业微信服务子系统」:config + handler + builder + service + utils。 EXPLICIT_SOURCES = [ ("parking-module-system", "zteits-wxcp-portal", [ # 配置:WxCpProperties 是 @ConfigurationProperties(wechat.cp); # WxCpConfiguration 负责建 WxCpService,并对外提供静态 # getCpService(agentId) —— 4 个发送实现全靠它拿 service。 "com/zteits/wxcp/config/WxCpConfiguration.java", "com/zteits/wxcp/config/WxCpProperties.java", # 消息路由处理器:WxCpConfiguration 构造器注入其中 7 个(都带 @Component), # EnterAgentHandler 由路由直接 new。它们服务的是**接收**侧(消息回调), # 但路由本身挂在 getCpService 的同一个类里,属于同一子系统,故一并搬。 "com/zteits/wxcp/handler/AbstractHandler.java", "com/zteits/wxcp/handler/ContactChangeHandler.java", "com/zteits/wxcp/handler/EnterAgentHandler.java", "com/zteits/wxcp/handler/LocationHandler.java", "com/zteits/wxcp/handler/LogHandler.java", "com/zteits/wxcp/handler/MenuHandler.java", "com/zteits/wxcp/handler/MsgHandler.java", "com/zteits/wxcp/handler/NullHandler.java", "com/zteits/wxcp/handler/ScanHandler.java", "com/zteits/wxcp/handler/SubscribeHandler.java", "com/zteits/wxcp/handler/UnsubscribeHandler.java", # MsgHandler 回复消息时用到的响应构造器 "com/zteits/wxcp/builder/AbstractBuilder.java", "com/zteits/wxcp/builder/ImageBuilder.java", "com/zteits/wxcp/builder/TextBuilder.java", # 发送实现:SendMessageService 的实现类 + 按消息类型分派的 4 个内部分支 "com/zteits/wxcp/service/impl/SendMessageServiceImpl.java", "com/zteits/wxcp/service/impl/inner/InnerMessageSendService.java", "com/zteits/wxcp/service/impl/inner/impl/NewsMSendMessageServiceImpl.java", "com/zteits/wxcp/service/impl/inner/impl/PnewsMSendMessageServiceImpl.java", "com/zteits/wxcp/service/impl/inner/impl/TextCardMsgSendServiceImpl.java", "com/zteits/wxcp/service/impl/inner/impl/TextSendMessageServiceImpl.java", # 工具与常量:JsonUtils 被 WxCpProperties.toString() 用,走 gson(已在 classpath) "com/zteits/wxcp/utils/JsonUtils.java", "com/zteits/wxcp/utils/constants/InnerMessageSendRoutes.java", ]), ] # --------------------------------------------------------------------------- # 排除规则 # --------------------------------------------------------------------------- # 整文件排除(按文件名) SKIP_FILENAMES = { # 启动类:与 ParkingServerApplication 冲突 "ParkApplication.java", "OrderApplication.java", "PayApplication.java", "BillApplication.java", "ChargeApplication.java", "BusinessApplication.java", "Application.java", # zteits-sys/src/main/java/com/zteits/sys/Application.java "RefreshCacheApplication.java", "FrameworkMain.java", # 代码生成器:新工程不再用(P6 统一进 parking-module-infra) "MybatisGenerateRun.java", "MybatisGenerateRun2.java", # 上面两个入口类的伴生类(mybatis-generator-core 的 PluginAdapter / IntrospectedTable 等)。 # 入口类不搬之后这些成了孤儿:全仓 grep 过,除了彼此没有别的引用。 # 留着会把 mybatis-generator-core 拖进单 Jar(只为一段跑不到的代码),所以一并排除。 "MybatisGeneratorCommon.java", "ServiceAndControllerGeneratorPlugin.java", "OverWriteIsMergeablePlugin.java", # Zookeeper 分布式锁客户端:唯一使用者是 park 的 RefreshCache, # 后者已改用 Redisson(去掉 Dubbo/ZK 的必然动作),此类随之作废。 "ZkClientFactory.java", # 错放在 src/main/java 里的 JUnit 测试类(两个 @Test 方法、全仓 0 引用、 # 类注释自己写着 TEST-JOB)。老 zteits-order/pom.xml 里 starter-test 是 # test,所以它在老仓库的 main 编译里**也编译不过**, # 只在 IDE 里跑得起来。搬过来会逼着把 junit 提成 compile 依赖、进而混进单 Jar, # 只为两个跑不到的调试方法。测试代码的迁移是 P4/P5 的事(按页面与用例重建)。 "CloudsJobTest.java", # Dubbo 过滤器:Dubbo 整体废弃 "RPCConsumerFilter.java", "RPCProviderFilter.java", # 老 mybatis 装配(DataSource/SqlSessionFactory),新工程由 starter 接管 "core-context-sys.xml", # 老 Redis 装配:jedis 版 RedisConnectionFactory + RedisTemplate, # 全仓无人引用,新工程由 parking-spring-boot-starter-redis 提供(且新栈是 # Lettuce/Redisson,不是 jedis)。留着会多出一个 RedisConnectionFactory Bean, # 且它头上还挂着 @EnableAutoConfiguration。 "RedisConfig.java", # 老 Redisson 装配:类上 @Configuration,但 redissonClient() **没挂 @Bean**, # 即注册不出任何 Bean;老仓库 + 新仓库双向 grep `RedissonConfig` / `redissonClient()` # 均为零引用(`RedissonClient` 靠 redisson-spring-boot-starter 的自动配置提供, # 存量 20+ 处 @Autowired RedissonClient 与它无关)。 # ⚠️ 它同时是 `spring.redis.password` 这个占位符的**唯一**索取者,而本地 Redis 无密码, # 必须"整行不定义"才能避免 Redisson 发 AUTH ""(见 application-local.yaml 的注释), # 两者不可兼得 —— 删掉这个死类,占位符需求随之消失。详见 AGENTS.md 坑 15。 "RedissonConfig.java", # Boot 1.x 专有错误处理:继承 org.springframework.boot.autoconfigure.web.AbstractErrorController、 # 用 autoconfigure.web.ErrorAttributes —— 这两个类在 Boot 2 已挪到 # org.springframework.boot.web.servlet.error.*。全仓无人引用,异常统一由 # starter-web 的 GlobalExceptionHandler + GlobalResponseBodyHandler 承担 # (与方案 §4.5「响应语义收敛到 CommonResult」一致),故不再搬运。 "ControllerExceptionHandler.java", # 老 MVC 装配:@Configuration + **@EnableWebMvc** + 继承 WebMvcConfigurerAdapter。 # @EnableWebMvc 的语义就是「关掉 Boot 的 MVC 自动配置」,而新框架的 # GlobalExceptionHandler / Jackson 配置 / Knife4j / URL 前缀映射全靠 Boot 那套, # 扫进来等于把整个 web 层拆了。老工程的 portal 启动类也是靠 # excludeFilters={@Filter(ASSIGNABLE_TYPE, value=WebMvcConfigurer.class)} 把它排除的。 # 引用者只有未搬迁的 portal / data-collection / glock / lz 的启动类, # 7 个业务模块全程不引用(已全仓 + 老仓库双向 grep 核对)。 "WebMvcConfigurer.java", # 老数据源装配(每个存量模块各一份,老仓库共 10 份)。在单 Jar 里就是 # **7 个同名 @Configuration + 7 个同名 dataSource() Bean**,直接 # ConflictingBeanDefinitionException。且新工程的数据源由 # druid-spring-boot-starter(application-local.yaml 的 spring.datasource.druid) # + parking-spring-boot-starter-mybatis 接管,这些类里 @AutoConfigureBefore( # DataSourceAutoConfiguration.class) 的老写法在 Boot 2.7 也已无意义 # (@AutoConfigureBefore 只在 spring.factories/AutoConfiguration.imports 里才生效)。 # ⚠️ 代价:老 DruidAutoConfiguration 里手工设的池参数(initialSize/maxActive/ # validationQuery/filters=stat,wall 等)不再生效,需在 P3 按新 yaml 对齐, # 已记入 AGENTS.md 待办。 "DruidAutoConfiguration.java", "DruidProperties.java", # 老 xxl-job 装配(老仓库:XxlJobConfig ×4、XxlJobConfiguration ×6、XxlJobCondition ×6)。 # 单 Jar 只允许**一个**执行器,而这 4 份 XxlJobConfig 逐字相同、只差包名, # 且各自 @Value 取的是**各模块专属**的部署参数(xxl.job.executor.appname=zteits-park / # zteits-order、port=9991/9992、logpath=/home/zteits/...)。 # 合并后 appname/port/logpath 该取什么值是**新部署的决策**,不是搬迁能定的: # * 保留任意一份 → 必须新造一套 xxl.job.* 配置(还要知道新环境的 admin 地址), # 且会把「park 的执行器」当成整个平台的执行器,语义也不对; # * 全不保留 → 105 个 @JobHandler 照常编译、只是没有执行器去拉起它们。 # 方案里 xxl-job → Quartz 的收敛是 P5,届时统一落一份平台级配置,故此处全部排除。 # ⚠️ 代价:这 105 个定时任务在 P5 之前**不会运行**,已记入 AGENTS.md 待办。 "XxlJobConfig.java", "XxlJobConfiguration.java", "XxlJobCondition.java", } # 整目录排除(相对 src/main/java 或 src/main/resources 的路径片段) SKIP_DIR_PARTS = { "META-INF", # zteits-order/src/main/java/META-INF/MANIFEST.MF } # 非 Java/XML 的资源后缀一律不搬(证书、私钥) SKIP_SUFFIXES = (".p12", ".jks", ".pkcs12", ".keystore") # 手写代码所在路径:这些目录下的文件**永远**不删(--prune 也不例外)。 # com/zteits/parking 是新工程的包名,老工程里根本不存在,所以「老工程里没有对应源」 # 这条规则本身就足以保护它;这里再列一遍是为了防住「万一以后有人往老工程也放了同名包」。 PROTECTED_PREFIXES = ( "com/zteits/parking", ) def is_protected(rel): return any(rel.replace(os.sep, "/").startswith(p) for p in PROTECTED_PREFIXES) def should_skip(path): name = os.path.basename(path) if name in SKIP_FILENAMES: return True if name.endswith(SKIP_SUFFIXES): return True parts = set(path.split(os.sep)) if parts & SKIP_DIR_PARTS: return True return False def source_rels(src): """老工程这一份源码对应的全部相对路径(应用排除规则后),即「本次导入管得着的文件」。""" out = set() for root, dirs, files in os.walk(src): dirs[:] = [d for d in dirs if d not in SKIP_DIR_PARTS] for f in files: rel = os.path.relpath(os.path.join(root, f), src) if not should_skip(rel): out.add(rel) return out def prune_mirror(src, dst, dry, stats): """删掉目的目录里「老工程镜像过来的、但本次不再产出」的文件(改名/删除的残骸)。 判据是**相对路径是否在老工程源码里**: * 在 → 这个文件由导入负责,老工程里已没了,可以删 * 不在 → 手写文件(如 com/zteits/parking/**),绝不删 """ keep = source_rels(src) stale = [] for root, _, files in os.walk(dst): for f in files: rel = os.path.relpath(os.path.join(root, f), dst) if rel in keep or should_skip(rel): continue # 本次会重新产出的路径不删;排除掉的文件也不管(本来就不该由导入管理) if is_protected(rel): stats["protected"] += 1 continue stale.append(os.path.join(root, f)) for p in stale: stats["pruned"] += 1 if dry: print(f" prune {os.path.relpath(p, dst)}") else: os.remove(p) # 删空目录(老代码改名后可能留下空包目录) if not dry: for root, dirs, _ in os.walk(dst, topdown=False): for d in dirs: p = os.path.join(root, d) if is_protected(os.path.relpath(p, dst)): continue if not os.listdir(p): os.rmdir(p) def copy_tree(src, dst, dry, stats): """拷贝 src → dst,逐文件应用排除规则。""" for root, dirs, files in os.walk(src): dirs[:] = [d for d in dirs if d not in SKIP_DIR_PARTS] for f in files: s = os.path.join(root, f) rel = os.path.relpath(s, src) t = os.path.join(dst, rel) if should_skip(rel): stats["skipped"] += 1 if dry: print(f" skip {rel}") continue stats["copied"] += 1 if not dry: os.makedirs(os.path.dirname(t), exist_ok=True) shutil.copy2(s, t) def copy_explicit(dry, stats): """按 EXPLICIT_SOURCES 逐文件拷贝(老模块不整体搬,只挑若干类)。 与 copy_tree 的区别:只搬列出来的文件,**不碰**目的目录里其它任何东西; 因此一个源文件被删掉时这里只会「少拷贝一个」,不会清理残骸 —— 需要清残骸时 仍走 copy_tree + --prune 的那一套。少而明确,所以够用。 """ for dst_mod, src_mod, rels in EXPLICIT_SOURCES: print(f"\n=== {src_mod} -> {dst_mod}(按文件精选)===") base = os.path.join(LEGACY, src_mod, "src/main/java") for rel in rels: s = os.path.join(base, rel) if not os.path.isfile(s): print(f" !! 源文件不存在: {src_mod}/{rel}") continue if should_skip(rel): stats["skipped"] += 1 print(f" skip {rel}") continue stats["copied"] += 1 # 与 SOURCES 的落位保持一致:老模块的 src/main/java 对应新模块的 src/main/java t = os.path.join(CLOUD, dst_mod, "src/main/java", rel) print(f" {'(dry)' if dry else 'copy'} {rel}") if not dry: os.makedirs(os.path.dirname(t), exist_ok=True) shutil.copy2(s, t) def main(): ap = argparse.ArgumentParser() ap.add_argument("--dry-run", action="store_true") ap.add_argument( "--prune", action="store_true", help="删除老工程镜像过来、但本次已不产出的文件(只删这一类,手写代码不动)", ) ap.add_argument( "--only-explicit", action="store_true", help="只跑 EXPLICIT_SOURCES 的按文件精选项,不重新覆盖 SOURCES 的整目录镜像", ) args = ap.parse_args() if not os.path.isdir(LEGACY): sys.exit(f"老工程目录不存在: {LEGACY}") grand = {"copied": 0, "skipped": 0, "pruned": 0, "protected": 0} # ⚠️ --only-explicit 存在的理由:整目录镜像(SOURCES)是**覆盖式**的, # 凡是在镜像目录里手改过的文件(已发生过的例子:com/zteits/charge/config/ # CallKeLinServiceProxyConfiguration.java 删掉无参 @ComponentScan、9 个 mapper 的 # 显式 bean 名)再跑一次就会被老源码原样盖回去。只想补跑按文件精选项时用它。 if args.only_explicit: copy_explicit(args.dry_run, grand) print(f"\n合计:拷贝 {grand['copied']} 个文件,排除 {grand['skipped']} 个。") if args.dry_run: print("(--dry-run,未落盘)") return for dst_mod, src_mod, rels in SOURCES: dst_root = os.path.join(CLOUD, dst_mod) src_root = os.path.join(LEGACY, src_mod) if not os.path.isdir(src_root): print(f"!! 跳过 {src_mod}(不存在)") continue print(f"\n=== {src_mod} -> {dst_mod} ===") for rel in rels: s = os.path.join(src_root, rel) if not os.path.isdir(s): continue d = os.path.join(dst_root, rel) stats = {"copied": 0, "skipped": 0, "pruned": 0, "protected": 0} # 先清残骸再拷(--prune 才清;默认只增不删) if args.prune and os.path.isdir(d): prune_mirror(s, d, args.dry_run, stats) copy_tree(s, d, args.dry_run, stats) msg = f" {rel:<34} 拷贝 {stats['copied']:>5} 排除 {stats['skipped']:>3}" if args.prune: msg += f" 清理残骸 {stats['pruned']:>4}" if stats["protected"]: msg += f" 保护未删 {stats['protected']}" print(msg) for k in grand: grand[k] += stats[k] # 按文件精选的随迁(见 EXPLICIT_SOURCES 的注释) copy_explicit(args.dry_run, grand) # 框架资源 print("\n=== zteits-framework 运行时资源 ===") for r in FRAMEWORK_RESOURCES: s = os.path.join(LEGACY, "zteits-framework", "src/main/resources", r) if not os.path.exists(s): continue d = os.path.join(CLOUD, "parking-framework/parking-legacy-common/src/main/resources", r) if not args.dry_run: shutil.rmtree(d, ignore_errors=True) if os.path.isdir(d) else None os.makedirs(os.path.dirname(d), exist_ok=True) if os.path.isdir(s): shutil.copytree(s, d) else: shutil.copy2(s, d) print(f" {r}") print(f"\n合计:拷贝 {grand['copied']} 个文件,排除 {grand['skipped']} 个。") if args.prune: print( f"清理老代码残骸 {grand['pruned']} 个;" f"因属手写代码而保护未删 {grand['protected']} 个。" ) else: print("(未传 --prune:只增不删,不会删除任何现有文件)") if args.dry_run: print("(--dry-run,未落盘)") if __name__ == "__main__": main()