主题
后端工程
这一页讲一个应用的后端部分:工程放哪、依赖怎么声明、怎么构建,哪些代码必须自己写、 哪些由宿主在运行期替你注册,以及「用宿主能力 / 跨插件调用 / 开放接口 / 定时任务 / 事件」 这五件最容易做错的事。
写法以范例应用 wujies-hello 为准(后端 <插件仓库>/wujies-hello/,前端 frontend/hello-app/)。 它的源码就是"已经这么做并且能跑"的最小实例;涉及具体写法时先打开它对照,再回来看本页。 按主题逐文件索引见 跟着 hello 读源码。
0. 三个仓库与三条被推翻的旧规则
| 仓库 | 内容 | 本页涉及的路径 |
|---|---|---|
wujies-platform/ | 后端(Java / Spring Boot) | 宿主:wujies-admin/、wujies-common/、wujies-modules/ |
wujies/wujies-plugins/ | 插件本体 + 插件前端工程 + 打包器 | wujies-<app>/、frontend/<app>-app/、package.ps1 |
plus-ui/ | 宿主前端(Vue 3 / Vite) | ⚠️ 它的 package.json 里 name 写的是 wujies-admin,别被名字骗了 |
先纠正三条旧规则
- 平台里没有
*-api契约模块(数量为 0)。每个应用是自包含的一个工程:实体 / BO / VO / Mapper 接口 与 Mapper XML / Service 接口与实现 / controller / open / listener / task /app.json/migrations/全在插件仓库自己的目录下。 - Mapper XML 不放平台、也不靠
mapperLocations扫出来 —— 它随插件 jar 走, 由PluginMapperRegistrar在加载时动态注册(见 §4)。 wujies-platform的在线代码对cn.wujies.<app>.*的 Java 引用为 0; 只有归档目录wujies-platform/docs/removed/里还留着历史残留。
一句话:新增一个业务模块不需要动平台一行,只写插件。这是整套外置架构的全部意义。
1. 工程位置与目录结构
一个应用 = 插件仓库里的一个工程,它不在平台的 Maven reactor 里:
wujies/wujies-plugins/ # 插件仓库:独立 git 仓库、独立父 pom
├── pom.xml # cn.wujies.plugins:wujies-plugins-parent
├── package.ps1 # 打包 + 签名
├── sandbox/ # 本地调试用的宿主沙箱(H2 内存库)
└── wujies-hello/
├── app.json # 清单:编码 / 版本 / 产物路径 / 菜单树
├── pom.xml
├── migrations/ # 建表、种子数据、字典、站点配置(随包走)
└── src/main/
├── java/cn/wujies/hello/
│ ├── domain/ # 实体(另有 bo/ 入参、vo/ 出参)
│ ├── mapper/ # Mapper 接口
│ ├── service/ # I*Service 接口
│ │ └── impl/ # 实现
│ ├── controller/ # 管理端接口
│ ├── open/ # 公开接口(/open/<app>/**)
│ └── task/ # 定时任务
└── resources/mapper/hello/ # 有自定义 SQL 时才需要放 Mapper XML各目录的职责与真实文件对照:
目录(包名 cn.wujies.<app>.) | 放什么 | wujies-hello 里的对应文件 |
|---|---|---|
domain/、domain/bo/、domain/vo/ | 实体、入参、出参 | domain/HelloItem.java |
mapper/ | Mapper 接口 | mapper/HelloItemMapper.java |
src/main/resources/mapper/<app>/ | 自定义 SQL 的 Mapper XML | hello 只用 MyBatis-Plus 通用方法,没有 XML;现成例子见 wujies-gallery/src/main/resources/mapper/gallery/*.xml |
service/、service/impl/ | 服务接口与实现 | service/HelloService.java、service/impl/HelloServiceImpl.java |
controller/ | 管理端接口 | controller/HelloController.java(GET /hello/item/list) |
open/ | 公开接口 /open/<app>/** + 类上 @SaIgnore | open/HelloDemoOpenController.java |
task/ | 定时任务(两种写法) | task/HelloDemoScheduledTask.java、task/HelloDemoSnailJobTask.java |
listener/、event/、message/ | 消息消费者与事件 | hello 没有;见 wujies-gallery/src/main/java/cn/wujies/gallery/listener/ |
enums/、constant/ | 枚举与常量 | wujies-gallery 的 constant/ |
包名是硬约定:cn.wujies.<app>.
PluginMapperRegistrar 在重新注册前会按包名前缀扫除上一轮留下的语句、ResultMap 与"已加载"标记, 前缀直接由 appCode 推出(cn.wujies.<app>.)。包名跑偏,清理就会漏 —— 症状是热加载后 Invalid bound statement (not found)。
2. 依赖:平台件一律 provided
wujies-hello/pom.xml 的父 POM 与依赖声明(节选):
xml
<parent>
<groupId>cn.wujies.plugins</groupId>
<artifactId>wujies-plugins-parent</artifactId>
<version>1.0.0</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>wujies-hello-plugin</artifactId>按用途声明平台依赖(父 POM 的 dependencyManagement 里已定好版本与 scope):
| 用途 | artifactId |
|---|---|
R / 基础工具 / ConfigService / OssService | wujies-common-core |
Web(BaseController) | wujies-common-web |
| MyBatis-Plus 注解与通用方法 | wujies-common-mybatis |
登录态(LoginHelper) | wujies-common-satoken |
| 跨插件契约与定位器 | wujies-common-plugin-sdk |
| MyBatis 之外的常用能力 | wujies-common-redis、wujies-common-tenant、wujies-common-oss、wujies-common-log、wujies-common-rabbitmq 等 |
| 兄弟插件的契约 | wujies-<sibling>-plugin(需要时显式写 provided,见 §6) |
| SnailJob 注解与 DTO | snail-job-client-starter、snail-job-client-job-core(版本由父 POM 管,不写 version) |
子模块里通常不用逐条写 <scope>provided</scope>
插件仓库父 POM 的 dependencyManagement 已经把平台件统一设成 provided。 但你必须知道它得是 provided:一旦有人改成 compile,平台类会被打进插件 jar, 出现「同一个接口两个 Class 对象」—— instanceof 莫名返回 false、按类型注入找不到 bean, 而且只在运行期暴露。
- 不要依赖
wujies-system:父 POM 的dependencyManagement里刻意没有它。 宿主能力走wujies-common-*的接口(见 §5)。 验收方式很直接:若还有插件引用cn.wujies.system.*,编译会cannot find symbol。 - 第三方 SDK 也是
provided:支付宝 / 微信支付 / 微信 MP 由宿主的wujies-admin显式引入, 插件只编译期引用(打进插件要 shade,会撞 BouncyCastle 的签名校验,包也会从几十 KB 涨到几十 MB)。
别动 plugin.contract.artifact
父 POM 里这个属性默认是哨兵值 none,机制保留但当前无人在用。 把它设成空串会让 Maven 解包全部依赖到 target/contract,再被 antrun 的 **/mapper/**/*.class 挑进插件 jar —— 实测把 BaseMapperPlus 之类的平台类打进了插件, 直接违反"平台件必须 provided"。
3. 构建、打包、跑起来
powershell
# ① 平台构件先装进本地仓库(插件以"已发布的平台构件"形式引用平台,不拉平台源码)
cd wujies-platform
& $mvn -pl wujies-admin -am -B install -DskipTests
# ② 构建插件(在插件仓库根,用 -pl 指定应用)
cd ..\wujies\wujies-plugins
& $mvn -B -pl wujies-hello package -DskipTests
# 产物:wujies-hello/target/wujies-hello-plugin-1.0.0.jar
# ③ 打成应用包(默认取该应用的 migrations/,按 app.json 组包,可选签名)
.\package.ps1 -Plugin wujies-hello -SigningKey keys/private.pem| 环节 | 要点 |
|---|---|
平台 install | 平台版本由父 POM 的 platform.version 统一控制;构件解析不到时报 dependencies.dependency.version is missing,回去补第 ① 步 |
| 插件构建 | 只编译这一个应用;构建前必须已 install 过平台构件 |
| 打包 | package.ps1 的信息全部来自 app.json,路径按 code 推导;发布与签名见 打包与发布、包签名 |
新插件必须挂进插件仓库根 pom 的 <modules>
漏了这一步的表现是"改了代码没生效",很难第一时间想到。wujies-hello 就在列表最前面。
改了后端代码必须先重新出 jar
只跑 mvn compile(哪怕成功)不会更新可运行 jar,你会拿着旧代码得出"功能没生效"的结论, 然后去改本来是对的实现。另外:不要在正在运行的 JVM 上跑 mvn install —— compiler 插件会先清空输出目录再全编,正在运行、按需懒解析类的 JVM 会抛 NoClassDefFoundError。
本地跑起来不需要 MySQL / Redis / RabbitMQ —— 沙箱用 H2 内存库:
powershell
& $mvn -B -pl sandbox spring-boot:run `
"-Dspring-boot.run.arguments=--sandbox.plugin-jar=../wujies-hello/target/wujies-hello-plugin-1.0.0.jar"先打 GET /hello/greet(验装载与依赖注入,不碰库),再打 GET /hello/item/list (验建表脚本、Mapper 与 SQL);/sandbox 看插件装上了没有、挂了几条路由。 两条都通说明链路完整,只有第二条不通说明问题在数据侧(建表脚本 / Mapper / 表名)。 清单里的应用则要在控制台部署后用真实宿主联调,见 本地联调。
4. 你不需要写的注册代码
| 东西 | 谁注册 | 约定 |
|---|---|---|
| Spring 组件 | PluginRuntimeManager.scanComponents | 枚举 jar 条目、按注解收集;@Service / @Repository / @RestController / @Configuration 都元注解了 @Component,一次判断就够;跳过接口、抽象类、内部类($) |
| Mapper 接口 | PluginMapperRegistrar | 带 @Mapper,或包名含 .mapper |
| Mapper XML | 同上 | jar 内路径以 mapper/ 开头、以 .xml 结尾 |
| Mapper 的 Spring bean | PluginRuntimeManager | 注册进宿主 bean factory,兄弟插件才能注入(见 §6) |
| HTTP 路由 | PluginHandlerMapping | 从子上下文取 @Controller bean 注册,并复制宿主的拦截器链 |
@Scheduled / @RabbitListener | 子上下文基础设施 | PluginRuntimeManager 给子上下文直接注册 BeanPostProcessor(消息监听还配一个子上下文自己的 RabbitListenerEndpointRegistry);不要自己写 @EnableScheduling / @EnableRabbit |
不需要写:@ComponentScan、@MapperScan、spring.factories —— 插件运行时是按注解扫 jar 条目注册 bean 的,不看 spring.factories。
@Async 不在这张表里
子上下文只补了 @Scheduled 与 @RabbitListener 这两类基础设施, 别默认插件里的 @Async 会自动生效 —— 要用异步就先确认宿主那一侧到底注册了什么。
关于 Mapper XML,四件事必须记住:
- 路径就是
src/main/resources/mapper/<app>/*.xml(打进 jar 后是mapper/...xml)。 - 它不是靠
mapperLocations扫出来的。宿主那份mapperLocations: classpath*:mapper/**/*Mapper.xml(见wujies-admin/src/main/resources/application.yml) 用的是宿主类加载器,扫不到插件里的 XML;而拿它去扫会把宿主的映射文件再注册一遍, 直接撞Result Maps collection already contains value,宿主起不来。 - 注册是代码做的:
PluginMapperRegistrar只读传入的那个 jar,先解析 XML、再addMapper(MyBatis 的既有约定,顺序不能反)。 - XML 里的
namespace与type写全限定名,例如cn.wujies.gallery.mapper.GalleryImageMapper、cn.wujies.gallery.domain.vo.GalleryImageVo—— 不要依赖宿主的typeAliasesPackage。
一个写坏的 XML 不会拖垮整个插件
每个 XML 独立 try/catch,失败会 WARN 并继续(只影响那个 Mapper 的方法), 但异常仍向上抛 —— 这是刻意保留的"硬失败",避免出现"插件装上了、所有 SQL 报不存在"的假成功。
依赖不在宿主类路径上的类会被跳过
scanComponents 在注册前先做一次"能不能链接"的检查:连不上就跳过并 WARN(列出缺哪个包)。 所以带 @JobExecutor / @RabbitListener 的可选能力,在没引对应库的宿主上只是不生效, 不会让整个插件装不上。
队列 / 交换机 / 绑定要声明在宿主侧
子上下文里声明的 Queue / Exchange / Binding,宿主的 RabbitAdmin 看不见、也不会声明到 broker —— 表现是"插件加载成功、消息发不出去 / 消费者起不来"。本项目的做法是放在宿主 wujies-common-rabbitmq 的 RabbitMQConfig;插件里的 @RabbitListener 本身没问题。
5. 插件怎么用宿主能力
插件上下文是宿主上下文的子上下文(宿主是 parent),所以插件能注入宿主 bean,反之不行 (Spring 父子关系是单向的,见 §10)。
wujies-hello 演示了最常用的三个,源码在 wujies-hello/src/main/java/cn/wujies/hello/controller/HelloDemoHostController.java:
| 能力 | 接口 | 演示端点 |
|---|---|---|
读平台配置表 sys_config | ConfigService | GET /hello/demo/host/config |
把 sys_oss 里的行 id 换成可访问 URL | OssService | GET /hello/demo/host/oss-url |
| 读当前登录用户(宿主建立的登录态) | LoginHelper | GET /hello/demo/host/whoami |
- 用构造注入(
@RequiredArgsConstructor),不要字段注入:缺哪个宿主能力会在启动期暴露, 而不是某次调用才 NPE。 - 可选能力用
ObjectProvider<T>取,宿主没提供就降级,而不是启动失败。 - 插件不要自己拼 OSS 地址、也不要自己解析 token —— 这两件事都由宿主能力负责。
平台 SPI 的实现必须留在宿主侧
如果某个 SPI 的消费方可能是另一个应用,实现要放在宿主侧的平台公共模块(wujies-common-*), 不能放在插件里。原因:插件只看得见父上下文(宿主),看不见兄弟插件 —— 实现放在插件 A 里,插件 B 通过 ObjectProvider 取到的是 null,功能静默降级。
6. 跨插件调用:类可见 ≠ bean 可见
这是本项目最容易被做错的一处。文档里两句话看着矛盾,其实都对:
| 层面 | 能不能跨插件 | 为什么 |
|---|---|---|
| 类(编译期 / 类加载期) | 能 | PluginClassLoader.findClass 在"从自己的 URL 定义"之前,先按包名把类委托给拥有它的那个插件加载器(cn.wujies.member.* 一律交给 member 的加载器)—— 所以 JVM 里同一个 FQCN 只有一个 Class 对象,instanceof 成立 |
| Spring bean(运行期) | 不能 | 每个插件有自己的子上下文(parent 是宿主),你的上下文里没有兄弟的 bean |
所以在 pom 里依赖兄弟插件构件买到的只是类型可见,不是 bean 可见。落地成两条:
| 要用的东西 | 能直接注入吗 | 怎么做 |
|---|---|---|
| 兄弟的 Mapper 接口 | 能 | PluginMapperRegistrar 把它注册进宿主 bean factory ⇒ 宿主 bean 对子上下文可见 |
| 兄弟的 Service bean | 不能 | 用平台 SDK 的 XxxServiceLocator(每次调用都反查,装卸、升级都跟得上) |
| 兄弟的 DTO / 事件类 | —(不是 bean) | provided 依赖引用类型即可 |
定位器有两种用法:
| 场景 | 写法 |
|---|---|
| 可选依赖(没装也能跑,只是少个功能) | 先 locator.available(IMemberService.class) 判在不在,不在就优雅降级 |
| 强依赖(没装这功能没意义) | 直接 locator.get(IMemberService.class),让它抛「请到应用市场安装 XX」,比 NPE 清楚 |
现成的三个定位器住在平台 SDK wujies-common-plugin-sdk: MemberServiceLocator / PayServiceLocator / ImServiceLocator, 由该模块的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 装配。 两种做法都能跑的对照代码见 wujies-hello/src/main/java/cn/wujies/hello/controller/HelloDemoCrossPluginController.java (/hello/demo/cross/mapper 用兄弟的 Mapper,/hello/demo/cross/service 走定位器并降级)。
跨插件共享的类必须住在平台段
定位器曾经被放在插件自己的 fallback 包里,结果宿主与别的插件都拿不到它, 报 No qualifying bean of type '...MemberServiceLocator'。判断标准是「谁会引用它」, 而不是"它看起来更像谁的一部分"。平台段 = wujies-common-* / wujies-common-plugin-sdk。
依赖兄弟插件构件之前想清楚
兄弟插件没装时,引用它的类解析不到,scanComponents 会跳过相关类并 WARN「缺 cn.wujies.x.*」。 所以"可选依赖"请走定位器,别在字段类型上强绑。
7. 从插件里开放 /open 接口
这是"应用外置"之后最常被问的一件事:接口写在插件里、宿主管不着它,C 端 / 第三方怎么调? 答案:写一个 /open/<app>/** 的控制器,宿主一行都不用改。
| # | 做什么 | 为什么 / 谁负责 |
|---|---|---|
| 1 | 控制器放 /open/<app>/**,类上加 @SaIgnore | 宿主路由统一过 Sa-Token 的登录检查(路径清单来自 AllUrlHandler,插件路由也在其中),@SaIgnore 就是"这条不要登录态" |
| 2 | 什么都不用注册 | 宿主的 AllUrlHandler 会同时收集「宿主 mapping + 插件 mapping」,PluginRuntimeManager 在 load / unload 之后调 refresh() —— 装了就进清单、卸了就出清单 |
| 3 | 租户自动就有 | 宿主 open-api.tenant.path-patterns 的默认值含 /open/**,按请求头把租户设进上下文;不设的话带租户过滤的表查不到数据 |
| 4 | 站点配置不用自己写接口 | 把 <code>.* 写进 sys_config(config_category = 应用编码,见 种子数据与站点配置),宿主的泛化端点 GET /open/<code>/config 直接能读 |
第 4 条多说一句:那个泛化端点实现在宿主 wujies-system 的 OpenWebsiteConfigController, 它只认 config_category、不认识任何具体应用 —— 返回里 8 个基础字段 (logo / favicon / name / nameEn / backgroundImage / description / copyright / icp) 各占一个属性,其余 <code>.* 去掉前缀后原样放进 settings 透传给前端。 也就是说,"新应用要有一个公开的站点配置接口"这件事你什么都不用做。
别自己再包一层站点配置接口
HelloDemoOpenController 里的 GET /open/hello/config-via-plugin 是反面教材: 同样的数据用"插件自己读 sys_config 再包一层"又实现了一遍。别照它写 —— 它的存在只是为了让你看到"这件事宿主已经替所有应用做完了"。
这些路由只在装载时存在
没装该应用的部署,/open/<app>/** 就是 404 —— 不会留下"能路由、一调就报没装"的空壳。
8. 定时任务:两种写法都合法
wujies-hello 两个都写了,因为两种都是真实存在的做法(不是二选一的历史遗留):
@Scheduled(HelloDemoScheduledTask) | SnailJob @JobExecutor(HelloDemoSnailJobTask) | |
|---|---|---|
| cron 写在哪 | 代码里(注解参数) | 控制台里(任务调度中心) |
| 改了 cron 要重启吗 | 要(重新打包 + 装载) | 不要 |
| 执行历史 / 重试 / 告警 | 只有日志 | 有(控制台能看到每次执行) |
| 多实例部署 | 每台都跑(要自己加分布式锁) | 由调度中心分片 / 路由 |
| 额外依赖 | 不需要(Spring 自带) | 需要 SnailJob 客户端 |
| 适合什么 | 本机内务、轻量高频、失败重跑就行 | 业务任务:要对账、要可观测、要能临时改时间 |
三条只有真做过才知道的事:
@Scheduled在插件里能用,因为PluginRuntimeManager在建立插件上下文时直接注册了ScheduledAnnotationBeanPostProcessor(源码注释写明:"刻意不注册带@EnableScheduling的@Configuration类")。你不要自己写@EnableScheduling。@JobExecutor光有注解不会跑 —— 还要在调度中心建一条任务,执行器名必须与@JobExecutor(name=...)完全一致;方法签名固定为public ExecuteResult jobExecute(JobArgs jobArgs)。只把包传上去、不建任务,是不会有任何执行的 —— 排查"任务没跑"的第一件事是这个,不是权限、也不是插件没装。- SnailJob 的日志要用
SnailJobLog.REMOTE:普通log.info只进本机文件, 在控制台的任务日志里看不到。
9. 事件:先想清楚方向
| 方向 | 成立? | 机制 |
|---|---|---|
| 插件 → 宿主 | 成立 | 插件 publishEvent 会冒泡到父上下文(Spring 自带行为) |
| 宿主 / 兄弟插件 → 插件 | 由中继转发 | PluginEventRelay(宿主侧)把宿主收到的事件转发进各插件子上下文 |
事件类必须在父加载器上,否则静默失效
订阅方的 @EventListener(PaySuccessEvent.class) 认的是自己类加载器里的那个 Class 对象。 事件类若跟着发布方插件的 jar 走,订阅方就拿到第二个 Class → 永远不匹配,而且不报任何错。 所以事件类统一放 wujies-common-core 的 cn.wujies.common.core.domain.event (现有 UserRegisteredEvent / PaySuccessEvent / PayRefundSuccessEvent / ProcessEvent 等)。
事件里只放标量(扁平 POJO):wujies-common-core 不能依赖 wujies-common-tenant(会成环), 所以不要塞继承 TenantEntity 的实体 —— 需要实体就带 ID / 单号,由接收方自己重查。
10. 宿主侧要用插件的服务
宿主注入不到插件(Spring 父子关系单向),只有两条路:
| 方式 | 用在哪 |
|---|---|
PluginServiceProvider#getPluginBean(appCode, type)(SPI,推荐) | 按需取一次 bean |
XxxServiceLocator#proxy(Class)(返回每次调用都反查的代理) | 宿主里的长期消费方,装卸 / 升级都跟得上 |
定位器本身是平台段的薄包装(wujies-common-plugin-sdk 里二十来行一个类), 逻辑统一在 wujies-common-core 的 PluginServiceLocator。
绝对不要在宿主上下文里注册与插件实现同类型的 bean
哪怕是"转发代理"也不行。Spring 按类型注入走 beanNamesForTypeIncludingAncestors, 会把父上下文的同类型 bean 一起算作候选 → 插件的控制器启动即报 expected single matching bean but found 2。
宿主代码注入不了插件的 Mapper
Mapper bean 确实注册在宿主 bean factory 里,但那些接口的类型来自插件 —— 平台对 cn.wujies.<app>.* 的 Java 引用为 0,宿主代码编译期拿不到这些类型。 宿主想要插件的数据,走定位器调服务,或走事件;只有同为插件的消费方才能直接注入(§6)。
11. 一个应用长什么样:真实文件清单
wujies-hello(最小范例,后端全部文件):
| 文件 | 作用 |
|---|---|
app.json | 清单(编码 / 版本 / assets / 菜单树,含 type:"F" 按钮) |
pom.xml | 5 个平台依赖 + 兄弟插件构件 + SnailJob;没有自己的契约模块 |
migrations/01-create-hello-tables.sql | 建表(同时是"彻底删除"时删哪些表的唯一依据) |
migrations/02-create-hello-seed-data.sql | 示例数据 |
migrations/03-create-hello-website-config.sql | 站点配置(sys_config,config_category='hello') |
controller/HelloController.java | 管理端接口:/hello/greet、/hello/item/list |
controller/HelloDemoHostController.java | 演示点 1:调用宿主系统服务 |
controller/HelloDemoCrossPluginController.java | 演示点 2:跨插件调用(Mapper vs 定位器) |
open/HelloDemoOpenController.java | 演示点 3:开放 /open/hello/** |
task/HelloDemoScheduledTask.java | 演示点 4a:@Scheduled |
task/HelloDemoSnailJobTask.java | 演示点 4b:SnailJob @JobExecutor |
domain/HelloItem.java、mapper/HelloItemMapper.java、service/HelloService.java、service/impl/HelloServiceImpl.java | 一个实体的完整纵向切片 |
wujies-gallery(规模参考,src/main/java/cn/wujies/gallery/ 共 47 个 Java 文件, 另有 src/main/resources/mapper/gallery/ 下 5 个 Mapper XML):
| 目录 | 文件数 | 说明 |
|---|---|---|
controller/ | 5 | 管理端接口 |
open/ | 3 | 对外开放接口 |
listener/ | 2 | 消息消费者 |
service/ | 12 | 服务接口与实现 |
mapper/ | 5 | Mapper 接口(与上面的 XML 一一对应) |
domain/ | 17 | 实体 / BO / VO |
constant/、fallback/ | 1 + 2 | 常量与定位器(fallback/ 是历史位置;新建应用的定位器放平台段,见 §6) |
(数字按当前仓库统计,用来判断"一个真实应用有多大";不要把它当验收标准。)
12. 写完之后的验收
| 检查项 | 怎么查 |
|---|---|
| 插件被装载 | 部署响应 hotLoaded: true;hotLoadNote 里写「插件 X 已加载:组件 N 个,接口 M 个」 |
| Mapper 注册成功 | 宿主日志:插件 X 的 Mapper 已注册:接口 N 个,XML M 个,语句 K 条 |
| 菜单没有重复建 | 安装 / 升级前后 sys_menu 行数不变(菜单按 menu_key 幂等复用既有 menu_id) |
| 接口真的通了 | 管理端接口带 token 打一次;同类插件路由不带 token 应 401(除非你要的是 @SaIgnore) |
| 宿主消费方通了 | 走定位器的那个宿主接口调一次,拿到真实数据说明注入点都对;报「应用『xxx』未安装或未启用」则是定位器或加载有问题 |
| 任务真的跑了 | SnailJob 控制台看执行记录;本地调度看服务端日志 |
排查入口:故障排查、本地联调、 任务中心(安装 / 升级 / 卸载的每一步都在那里落库,比翻服务端日志快)。
相关阅读
- 必须遵守的不变量 —— 改代码前扫一遍
- 跟着 hello 读源码 —— 本页所有"演示点"的逐文件索引
- 应用清单 app.json
- 数据库迁移 / 种子数据与站点配置
- 菜单与权限
- 前端远程应用
- 插件运行时 / 应用市场架构
- 从宿主模块抽成应用(SOP)
- 打包与发布 / 包签名