Wujies 无界平台
API 手册

平台 SDK 契约

本文是 1.x 的归档文档(冻结于 1.x)。该分支只接收勘误修正, 内容基本不再变动;文中的源码路径、配置项与版本号均以 1.x 为准,不建议用于新项目。

新项目请查看 5.x 最新文档。已在 1.x 上运行的项目,本文仍是可靠依据。

平台 SDK 是宿主与插件之间唯一的稳定面:插件要宿主的能力,不直接注入宿主的 Mapper / Service, 而是依赖这里的一个接口;宿主提供一个实现。

项值
坐标cn.wujies:wujies-common-plugin-sdk(版本随平台,当前 5.6.2)
源码wujies-platform/wujies-common/wujies-common-plugin-sdk/src/main/java/cn/wujies/common/plugin/sdk/
内容18 个类:6 个能力接口、9 个值对象、3 个定位器
依赖零依赖(纯 JDK)。它被所有插件共同依赖,一旦引入 Spring 或 JSON 库就不再是稳定契约

为什么必须是零依赖

这不是洁癖,是几条具体设计后果的来源:

  • PlatformOssApi.upload 收的是 PlatformUploadFile 而不是 Spring 的 MultipartFile —— 插件侧只多一行 PlatformUploadFile.of(mf.getOriginalFilename(), mf.getContentType(), mf.getBytes()), 换来契约不绑框架;
  • 分页自己给一个 PlatformMessagePage(纯 JDK),不引平台分页类 —— 插件要转成自己的分页结构自己转;
  • PlatformOssExt 只给类型、不负责解析 —— 解析交给调用方自己的 JSON 工具, SDK 不因为一个 parseObject 就把 Jackson 拉进来。

一、能力接口(宿主实现,插件使用)

插件侧一律用 ObjectProvider<X> 取,不要强注入:宿主缺少该能力时应当降级,而不是启动失败。

接口用途方法数
PlatformUserApi读用户信息(昵称 / 头像 / 状态)与按部门取人8
PlatformDeptApi把部门展开成"它 + 全部子部门"1
PlatformMessageApi发站内消息、查公开公告2
PlatformOssApi对象存储:查元信息 / 上传 / 批量删5
FollowRelationApi查"某用户关注了谁"(图床的关注流要用社交模块的数据)1
PlatformMemberApi读 / 改会员资料(宿主读会员数据用,见定位器)2

PlatformUserApi

方法说明
PlatformUserView findById(Long userId)取单个用户;不存在返回 null,不抛异常
Map<Long, PlatformUserView> findByIds(Collection<Long> userIds)批量取;入参为空返回空 Map,查不到的 ID 不出现在结果里
List<Long> listEnabledUserIds()全部启用中的用户 ID
List<Long> searchEnabledUserIds(String keyword)按关键词模糊搜索启用中的用户(昵称或账号);关键词为空等价于上一个方法
long countEnabled()启用中的用户总数
long countEnabledByIds(Collection<Long> userIds)给定 ID 里启用中的有多少(考试用它算有效考生)
List<Long> listUserIdsByDeptIds(Collection<Long> deptIds, boolean includeChildren)按部门范围取启用中的用户;展开子部门的逻辑由宿主负责
int updateStatus(Collection<Long> userIds, String status)批量改账号状态(会员冻结时要连带停用系统账号)。刻意只改状态,不做通用更新

PlatformDeptApi

方法说明
Set<Long> deptAndChildIds(Long deptId)某部门及其全部子部门的 ID(含自己);入参 null 返回空集合

只有一个方法,是刻意的:插件对部门表的需求实测就这么一件事。一个 1 方法的接口评审成本接近零, 而一个"按任意条件查部门"的接口则不然。

PlatformMessageApi

方法说明
Long send(PlatformMessageCommand command)发一条站内消息(落库 + 投递)。失败返回 null,不抛异常 —— 通知发不出去不该让业务事务回滚
PlatformMessagePage queryPublicNotices(int pageNum, int pageSize)分页查公开公告(@SaIgnore 的开放接口用)

PlatformOssApi

方法说明
PlatformOssView findById(Long ossId)按 ID 取单个对象元信息,不存在返回 null
Map<Long, PlatformOssView> findByIds(Collection<Long> ossIds)批量取(列表页一次补齐 URL,避免 N+1)
PlatformOssView upload(PlatformUploadFile file)上传"请求里带上来的字节"
PlatformOssView upload(File file)上传本地文件(走流式,适合大文件与插件自己产生的文件)
boolean deleteByIds(Collection<Long> ossIds)批量删除(宿主按软删除处理)

刻意没有"按任意条件查询""透传 SqlWrapper"这类口子 —— 一旦开了,契约就退化成"给插件一个 Mapper"。

FollowRelationApi

方法说明
Set<Long> followedUserIds(Long userId, Collection<Long> candidateUserIds)该用户关注了哪些人;candidateUserIds 为空表示不限制。返回集合可能为空,不会为 null

它存在的理由:图床的"关注流"要读社交模块的数据,原实现让图床直接依赖 SocialFollowMapper, 图床就无法脱离社交模块单独安装。现在反转成"插件声明需要什么能力,宿主提供实现"。

PlatformMemberApi

方法说明
MemberProfileView queryProfileByUserId(Long userId)按登录用户 id 查会员资料;不是会员返回 null
boolean updateProfileByUserId(Long userId, MemberProfileCommand command)按登录用户 id 更新会员资料;不是会员返回 false

它与 MemberServiceLocator 的分工见定位器:这个接口是宿主读会员用的, 定位器是插件之间用的。

二、值对象(跨边界的传输形态)

类形态用途规模
PlatformUserViewrecord插件能看到的全部用户信息。不含 password / roleIds / postIds10 字段 + 2 常量 + displayName() / enabled()
PlatformMessageViewrecord站内消息只读视图(公开公告列表实际要展示的那些字段)8 字段
PlatformMessagePagerecord与分页框架无关的最小分页载体(records / total)2 字段 + empty()
PlatformMessageCommandrecord发消息的请求。替代插件自己"先 insert、再从回填的 bo 取 ID、再 publish"两步9 字段 + 2 静态工厂 + 3 链式方法
PlatformOssViewrecord对象存储元信息只读视图8 字段 + extJson() / local() / toMap()
PlatformOssExt类附件扩展信息(存在 OSS 记录 ext1 字段里的 JSON 结构)。刻意不是 record —— 属性名即 JSON 键名,改 record 会改序列化行为13 字段
PlatformUploadFilerecord与 Web 框架无关的待上传文件。content 是整块字节数组,不适合大文件3 字段 + 2 个 of + size() / isEmpty()
MemberProfileView类(@Data)会员资料视图,只带标量字段(badgeList 与嵌套 user 刻意不带)19 字段
MemberProfileCommand类(@Data)会员资料更新指令;字段全可为 null,null 表示这次不改这一项。昵称 / 头像 / 手机号属于宿主 sys_user,不走这个契约5 字段

三、定位器

三个定位器都标了 @AutoConfiguration,是 PluginServiceLocator(在 wujies-common-core,不在本 SDK) 的薄包装。它们让宿主侧代码拿到插件上下文里的服务实现。

类应用编码应用名(仅用于报错文案)
MemberServiceLocatormember会员管理
PayServiceLocatorpay支付管理
ImServiceLocatorim即时通讯

三者结构相同:构造时收 ObjectProvider<PluginServiceProvider> 与宿主 ApplicationContext, 对外暴露 get(Class<T>) / available(Class<?>) / proxy(Class<T>)。 机制、回落与降级规则见定位器。

四、自动装配

src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 的内容:

cn.wujies.common.plugin.sdk.MemberServiceLocator
cn.wujies.common.plugin.sdk.PayServiceLocator
cn.wujies.common.plugin.sdk.ImServiceLocator

只有这三个定位器需要自动装配 —— 能力接口与值对象都不是 bean。

五、两侧各要做什么

插件侧(消费能力)
private final ObjectProvider<PlatformUserApi> userApiProvider;

void doSomething(Long userId) {
    PlatformUserApi api = userApiProvider.getIfAvailable();
    if (api == null) {
        // 宿主没提供这个能力 → 降级。**不要**让插件启动失败
        return;
    }
    PlatformUserView user = api.findById(userId);
}
宿主侧(提供实现)

实现在 wujies-system 等拿得到 Mapper 的模块里,用 @ConditionalOnMissingBean 声明 —— 这样沙箱或测试可以覆盖成替身。不要把实现放进某个插件:消费方可能是另一个插件, 而插件看不见兄弟插件,对方会拿到 null 并静默降级。

相关阅读

目录