# mujun
**Repository Path**: mujunhub/mujun
## Basic Information
- **Project Name**: mujun
- **Description**: No description available
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-05-21
- **Last Updated**: 2026-09-23
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# mujun
面向 **Spring Boot 3** 的后端基础库(不是可独立启动的业务应用)。业务项目按需引入 Maven 模块,即可获得统一响应、CRUD 基类、认证鉴权、字典、文件、日志、OSS、微信、流媒体等能力。
| 项 | 内容 |
|---|---|
| Maven 坐标 | `cn.mujunhub:mujun` |
| 当前版本 | `1.7.2` |
| Java 包根 | `cn.mujunhub` |
| 许可证 | MIT(见 `pom.xml`) |
| 仓库 | https://gitee.com/mujunhub/mujun |
| 作者 | mujun / dxiaolong@outlook.com |
## 目录
- [技术栈](#技术栈)
- [架构](#架构)
- [快速接入](#快速接入)
- [宿主 SPI 速查](#5-宿主-spi-速查)
- [约定](#约定)
- [模块一览](#模块一览)
- [soil 基础设施](#soil-基础设施)
- [soil-base](#soil-base)
- [soil-tool](#soil-tool)
- [soil-biz](#soil-biz)
- [soil-oss](#soil-oss)
- [soil-log](#soil-log)
- [soil-poi](#soil-poi)
- [soil-sms](#soil-sms)
- [soil-wechat](#soil-wechat)
- [soil-ai](#soil-ai)
- [plugin 业务插件](#plugin-业务插件)
- [plugin-api](#plugin-api)
- [plugin-auth](#plugin-auth)
- [plugin-security](#plugin-security)
- [plugin-dict](#plugin-dict)
- [plugin-file](#plugin-file)
- [plugin-log](#plugin-log)
- [plugin-zlmk](#plugin-zlmk)
- [配置参考](#配置参考)
- [本地构建](#本地构建)
## 技术栈
- 语言:Java 21
- 构建:Maven 多模块(`${revision}` + flatten)
- 框架:Spring Boot 3.2.0、Spring WebMVC、Spring Data Redis、Spring Data JPA
- ORM:Hibernate 6.3 / JPA;MyBatis-Plus 3.5.14
- 数据库适配:MySQL、Oracle;日志可选 ClickHouse
- 缓存:Redis(`StringRedisTemplate`)、线程上下文用 Alibaba TTL
- 对象存储:MinIO、移动云 S3(AWS SDK)、本地文件系统
- 文档:springdoc-openapi 2.3.0
- 其它:Hutool、Lombok、EasyExcel 4.0.3、poi-tl、LangChain4j 1.16.3、Happy-Captcha、BouncyCastle、Redisson
## 架构
分层:
- **soil(土壤)**:基础设施与通用能力,一般不单独对外暴露 HTTP
- **plugin(插件)**:可按需引入的业务能力,多数带 REST 接口
- **dependencies**:BOM,统一管理各模块版本
```
业务 Spring Boot App
│ Header: Authorization = token
▼
┌─────────────────────────────────────────┐
│ plugin-security 拦截链(order 1→3) │
│ 1. SafetyCertificateInterceptor │
│ 2. TokenPreHandlerInterceptor (Redis) │
│ 3. UserPermissionInterceptor │
└─────────────────────────────────────────┘
│
▼
┌──────────────┐ ┌──────────────┐ ┌─────────────┐
│ Auth 接口 │ │ Dict/File/... │ │ Biz RestQLP* │
└──────┬───────┘ └──────┬───────┘ └──────┬──────┘
│ │ │
▼ ▼ ▼
Redis Token MySQL/Oracle MySQL/Oracle
OSS / 微信 API ClickHouse(log)
```
宿主应用必须:
1. 扫描 `cn.mujunhub`(库内无 `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`)
2. 提供 Redis(认证、验证码、会话都走 Redis)
3. 按引入的插件实现对应 SPI(见 [宿主 SPI 速查](#5-宿主-spi-速查))
4. 需要持久化时自行配置数据源(JPA 或 MyBatis-Plus)
## 快速接入
### 1. 用 BOM 统一版本
```xml
cn.mujunhub
dependencies
1.7.2
pom
import
```
再按需引入具体模块。最小可用组合(密码登录 + 拦截):
```xml
cn.mujunhub
plugin-security
cn.mujunhub
plugin-auth-pwd
```
`plugin-security` 的 Java 默认是 `web.certificateEnable=true`、`web.permissionsEnable=true`。本地开发请在 yaml 里设 `certificateEnable: false`(否则须实现 `SafetyCertificateAdapter` 并配置 `web.safetyCertificate`;拦截器把**配置值**传给适配器,不读请求头)。权限拦截开启时须实现 `LoginOptMenus`。
### 2. 扫描包
```java
@SpringBootApplication(scanBasePackages = {"com.example", "cn.mujunhub"})
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
```
### 3. 实现登录所需 SPI(密码登录示例)
```java
@Service
public class PwdAuthNeedUserApiImpl implements PwdAuthNeedUserApi {
@Override
public SysUserApiBo getByAccountAndPassword(String account, String password) {
// 按账号+密码查用户;找不到返回 null(插件会提示「账号或密码错误」)
return sysUser;
}
@Override
public boolean authCurrUserPassword(String password) {
// 校验当前登录用户密码(/auth/currUserPwdCheck 使用)
return true;
}
}
```
权限拦截开启时(`web.permissionsEnable=true`,默认开启),建议同时实现:
```java
@Service
public class LoginOptMenusImpl implements LoginOptMenus {
@Override
public List listUserPermissionsByUserId(String userId) {
// 返回 Ant 风格路径,如 "/dict/**"、"/file/uploadSingle"
return List.of("/dict/**", "/file/**");
}
}
```
超管(`SysUserApiBo.superAdmin=true`)跳过权限匹配。
### 4. 选型矩阵(ORM × 数据库)
字典等带持久化的模块按组合 **四选一**,不要同时引入冲突变体:
| ORM | 数据库 | soil 实体 | soil CRUD | 字典插件 |
|---|---|---|---|---|
| JPA | MySQL | `soil-biz-entity-jpa-mysql` | `soil-biz-jpa` | `plugin-dict-jpa-mysql` |
| JPA | Oracle | `soil-biz-entity-jpa-oracle` | `soil-biz-jpa` | `plugin-dict-jpa-oracle` |
| MyBatis-Plus | MySQL | `soil-biz-entity-mybatis-mysql` | `soil-biz-mybatis` | `plugin-dict-mybatis-mysql` |
| MyBatis-Plus | Oracle | `soil-biz-entity-mybatis-oracle` | `soil-biz-mybatis` | `plugin-dict-mybatis-oracle` |
### 5. 宿主 SPI 速查
按引入的模块实现对应接口(缺 Bean 时启动失败或运行时报业务错误)。`plugin-auth` 是**聚合 POM**,不要把它当依赖引入。
| 引入模块 | 必须实现 | 按需 / 开启后必须 |
|---|---|---|
| `plugin-security` | — | `certificateEnable=true`(默认)时实现 `SafetyCertificateAdapter`;`permissionsEnable=true`(默认)时实现 `LoginOptMenus`,否则非超管只能走白名单 |
| `plugin-auth-pwd` | `PwdAuthNeedUserApi` | `LoginBeforeOrAfterOpt`、`LoginOptRoles` |
| `plugin-auth-sms` | `AuthNeedUserPubApi`、`SmsAuthNeedApi`、`SmsService` | 同上登录钩子 |
| `plugin-auth-ewm` | `EwmAuthNeedUserApi` | 同上登录钩子 |
| `plugin-auth-wechat` | `WechatAuthNeedUserApi`,并提供 `WechatBusiness`(`wechat.enable=true`) | 同上登录钩子 |
| `plugin-dict-*` | `UserApi`(审计人姓名回填) | — |
| `plugin-file` | 配置 `oss.*` | — |
| `plugin-zlmk` | `RtspUrlBuild` | — |
## 约定
### 统一响应 `R`
```json
{ "code": 200, "msg": "操作成功", "data": {} }
```
| 字段 | 说明 |
|---|---|
| `code` | 状态码,成功为 `200` |
| `msg` | 提示信息 |
| `data` | 业务数据 |
工厂方法:`R.data(data)`、`R.data(data, msg)`、`R.data(code, data, msg)`。
常用 `ResCode`:
| 枚举 | code | 含义 |
|---|---|---|
| `SUCCESS` | 200 | 操作成功 |
| `UN_AUTHORIZED` | 401 | 认证错误/失效(未登录或 Token 过期) |
| `FAILURE` | 412 | 业务异常 |
| `NO_CERTIFICATE` | 501 | 安全证书缺失 |
| `CERTIFICATE_ERROR` | 502 | 安全证书错误 |
| `CERTIFICATE_EXPIRE` | 503 | 安全证书过期 |
全局异常由 `plugin-security` 的 `ExceptionAdvice` 处理:`BizException` 原样返回;参数校验失败返回 `412`;未捕获异常返回「系统繁忙,请稍后重试」。
### 分页
请求 `PageReq`:
| 字段 | 说明 |
|---|---|
| `current` | 当前页,分页接口必填 |
| `size` | 每页条数,分页接口必填 |
| `orders` | 排序列表 |
`OrderItem`:`column`(字段名)、`asc`(是否升序)。便捷工厂:`asc` / `desc`、`crtAsc` / `crtDesc`(`createTime`)、`uptAsc` / `uptDesc`(`updateTime`)。
响应 `PageResp`:`pageSize`、`pageNumber`、`totalPage`、`totalNumber`、`dataList`。
### 用户上下文
登录后由 Token 拦截器写入 `UserContextHolder`(Alibaba TTL,子线程可传递)。请求结束在 `afterCompletion` 里 `clear()`。
`UserContext` 字段:`userId`、`userName`、`userType`、`superAdmin`、`token`、`clientType`、`roleCodes`、`permissions`、`extMap`。
同模块还有通用 `ContextHolder`(TTL `Map`),可按 key 存取请求级数据;**不会**随 Token 拦截器自动清理,用完请自行 `remove`。
后续请求在 Header 中携带 Token,键名默认 `Authorization`(`web.authHeaderKey` 可改)。
### 审计与软删
继承 `DataAuditEntity` 的实体自动填充:
| Java 字段 | MySQL 列 | Oracle 列 | 填充时机 |
|---|---|---|---|
| `createUser` | `create_user` | `CREATE_USER` | INSERT(取当前 `UserContext.userId`) |
| `createTime` | `create_time` | `CREATE_TIME` | INSERT |
| `updateUser` | `update_user` | `UPDATE_USER` | INSERT / UPDATE |
| `updateTime` | `update_time` | `UPDATE_TIME` | INSERT / UPDATE |
| `delete` | `_delete` | `DELETE_STATUS` | 默认 `false` |
MyBatis 由 `EntityMetaObjectHandler` 填充;JPA 由 `DataAuditEntityListener`(`@PrePersist` / `@PreUpdate`)填充。
框架默认删除是软删(`deleteDealMark()` 后 `updateById`)。**列表是否过滤已删除数据,需业务在查询条件里自己加。** 字典插件覆盖为**物理删除**。
## 模块一览
### soil
| artifactId | 功能 |
|---|---|
| `soil-base` | 统一响应、异常、用户上下文、校验注解、工厂、日志注解 |
| `soil-tool` | RSA/AES/HMAC/签名、JSON、分页、身份证、IP、下载、密码等工具 |
| `soil-biz-base` | CRUD 公共接口(增/改/实体/软删) |
| `soil-biz-jpa` | JPA 版 QLP / QLPD / QLPDAM |
| `soil-biz-mybatis` | MyBatis-Plus 版 QLP / QLPD / QLPDAM |
| `soil-biz-entity-*` | 审计实体,按 ORM × DB 拆分 |
| `soil-oss` | 对象存储抽象:MinIO / 移动云 / 本地 |
| `soil-log` | `@OptLogger` AOP,ClickHouse 或控制台 |
| `soil-poi` | Excel 导入导出模板、Word 模板 |
| `soil-sms` | 短信发送接口(无默认实现) |
| `soil-wechat` | 微信小程序 token / openId / 手机号 / 订阅消息 |
| `soil-ai` | LangChain4j Redis 对话记忆 |
### plugin
| artifactId | HTTP 前缀 | 功能 |
|---|---|---|
| `plugin-api` | 无 | 跨插件 SPI:`UserApi` / `FileApi` / `DictApi` |
| `plugin-auth-base` | 无 | Token/Redis、登录钩子、认证配置 |
| `plugin-auth-pwd` | `/auth` | 图形验证码、RSA 公钥、密码登录、改密校验、登出 |
| `plugin-auth-sms` | `/smsAuth` | 短信验证码登录 |
| `plugin-auth-ewm` | `/ewmAuth` | 扫码登录 |
| `plugin-auth-wechat` | `/wechatAuth` | 微信小程序登录 |
| `plugin-security` | 无 | 证书 / Token / 权限拦截 + 全局异常 |
| `plugin-dict-*` | `/dict`、`/dictItem` | 字典与字典项 CRUD |
| `plugin-file` | `/file` | 上传、下载、URL 头 |
| `plugin-log` | `/log` | 操作日志分页查询 |
| `plugin-zlmk` | `/zlmk` | ZLMK 拉流 / 关流 |
---
## soil 基础设施
### soil-base
最底层模块。提供统一 API 形态、异常、上下文和通用注解,无 HTTP 接口。
**核心类型**
| 类型 | 作用 |
|---|---|
| `R` | 统一响应 |
| `PageReq` / `PageResp` | 分页入参 / 出参 |
| `OrderItem` | 排序项 |
| `BizException` | 业务异常,绑定 `ResCode` |
| `SignException` | 签名异常 |
| `UserContext` / `UserContextHolder` | 当前登录用户(TTL) |
| `ContextHolder` | 通用 TTL 上下文(`set` / `get` / `remove`),不随请求自动清理 |
| `RestTop` | Controller 基类,注入 `business` |
| `KV` / `TreeNode` | 通用键值、树节点 |
| `BTBizEx` | `throwBiz` / `obtainBiz`;支持 `{}` 占位(Hutool `StrUtil.format`)和 `cause` |
| `BFactory` / `BaseSF` | 普通工厂 / 策略工厂基类 |
| `Gender` / `DatePattern` / `Regex` / `Symbol` | 性别、日期格式、正则、分隔符 |
**注解**
| 注解 | 作用 |
|---|---|
| `@OptLogger` | 方法级操作日志。属性:`value`(必填)、`type`(默认 `"BIZ"`)、`hideArgs`、`hideRes` |
| `@LogTitle` | 类级日志标题,与 `@OptLogger.value` 拼成 `【标题】操作说明` |
| `@BizFactory` | 标记业务工厂(元注解含 `@Service`) |
| `@StrategyFactory` | 策略工厂 |
| `@IPV4` / `@Port` / `@LngLat` / `@Phone` | 参数校验;空串/`null` 视为通过。`@Phone` 接受 `1[3-9]` 开头的 11 位,或中间四位为 `*` 的脱敏号(如 `138****1234`)。严格 11 位请用 `Regex.MOBILE_PHONE` |
`@OptLogger` 用法:
```java
@LogTitle("订单")
@RestController
@RequestMapping("/order")
public class OrderController {
@PostMapping("/save")
@OptLogger(value = "新增订单", type = "BIZ", hideArgs = false, hideRes = false)
public R save(@RequestBody OrderAddCmd cmd) { ... }
}
```
无 `LogService` Bean 时切面直接放行,不记日志。有 Bean 时在 `finally` 里用线程池 **异步** 写入。
### soil-tool
通用静态工具,依赖 `soil-base`。无 REST 接口。
| 类 | 能力 |
|---|---|
| `RSATool` | `getKey()` 默认 2048 位;`getKey(KeyLength.KEY_2048/KEY_4096)`;`publicEnc` / `privateDec` |
| `AESTool` | `getInstance(String)` / `getInstance(byte[])` 后 `encrypt` / `decrypt`;AES-GCM;密钥须刚好 16/24/32 字节 |
| `HMACTool` | `getInstance(String)` / `getInstance(byte[])` 后 `encode` / `verify`;HmacSHA256;密钥至少 32 字节,输出小写 Hex |
| `SignTool` | `sign` / `verify` / `signWithInclude` / `verifyAndGet` |
| `JsonTool` | `toJson` / `parseObj` / `parseList` / 按路径取 `Str/Int/Bool` |
| `PwdTool` | 随机密码、强度校验、加密 `pwdEnc`、比对 `authPwdCorrect` |
| `IdCardTool` | 身份证性别、生日、年龄 |
| `IPTool` | IP 字符串与 Long 互转 |
| `WebTool` | 客户端 IP、本机地址、请求头 |
| `DownloadTool` | 浏览器下载响应头 |
| `TimeTool` | 日/月/年起止、格式化、解析、Date ↔ LocalDateTime;`formatBLDT` / `formatBLD` 用默认 `yyyy-MM-dd HH:mm:ss` / `yyyy-MM-dd` |
| `PageTool` | 总页数、内存分页 |
| `ListTool` | listToMap / group / filter / distinct |
| `EmptyTool` | 空判断 |
| `DSTool` | UUID36、随机数、经纬度距离、`trueThrow`、文件扩展名、`secureRandom` / `secureRandomA2Z`、`objGet` / `objGetOrDefault`、`mapGet` / `mapGetOrDefault`、`mapObjGetField` / `mapObjGetFieldOrDefault`、`hypPhone` 等 |
| `ExTool` | 抛出 `BizException` / `SignException`;同样支持 `{}` 占位与 `cause` |
| `IOTool` | 流拷贝与关闭 |
| `ClassTool` | 反射取全部字段 |
`getInstance(String)` 用 `secretKey.getBytes(UTF_8)`,校验的是 **UTF-8 字节长度**,不是字符数。`DSTool.secureRandom(n)` 得到 n 字节的 **URL-safe Base64(无 padding)** 文本(字符串会变长,不能当 AES 字符串密钥)。`DSTool.secureRandomA2Z(n)` 得到 n 个大小写英文字母(A–Z / a–z),`getBytes(UTF_8)` 仍是 n 字节。二进制密钥请走 `getInstance(byte[])`,不要先转成 String。
**AESTool**(`AES/GCM/NoPadding`):每次加密随机 12 字节 nonce,输出为 Base64(nonce 在前,密文和 tag 在后)。`encrypt`/`decrypt` 对 `null` 和空串返回 `null`。密钥必须刚好 16 / 24 / 32 字节(AES-128/192/256)。字符串密钥用 `secureRandomA2Z`;原始密钥用 `getInstance(byte[])`。
```java
AESTool aes = AESTool.getInstance(DSTool.secureRandomA2Z(32));
String cipher = aes.encrypt("hello");
String plain = aes.decrypt(cipher);
```
二进制密钥走 `AESTool.getInstance(byte[])`,数组长度必须刚好是 16 / 24 / 32。
**HMACTool**(HmacSHA256):密钥至少 32 字节。`encode` 对 `null`/空串返回 `null`,成功为 64 位小写 Hex。`verify` 忽略 Hex 大小写,空串或畸形签名返回 `false`。同一密钥同一明文结果固定;不同明文在实用中不会碰撞,不必为此写业务分支。
```java
HMACTool hmac = HMACTool.getInstance(DSTool.secureRandomA2Z(32));
String sign = hmac.encode("hello");
boolean ok = hmac.verify("hello", sign);
```
二进制密钥走 `HMACTool.getInstance(byte[])`,数组长度至少 32。
常量 `Cons.DICT_DEFAULT_TYPE = "SYSTEM"`(字典未传 `type` 时的默认类型)。
### soil-biz
通用 CRUD 框架,JPA 与 MyBatis 双实现,HTTP 路径对称。业务 Controller 继承对应 `Rest*` 后,自动拥有下列接口(前缀由子类 `@RequestMapping` 决定)。
**继承链**
```
BaseTop → BaseGet → BaseQLP → BaseQLPD → BaseQLPDAM
RestTop → RestQLP → RestQLPD → RestQLPDAM
```
| 基类 | 能力 | HTTP |
|---|---|---|
| `RestQLP` | 查 / 列表 / 分页 | `GET /query/{id}`、`POST /list`、`POST /page` |
| `RestQLPD` | + 删除 | `GET /remove/{id}` |
| `RestQLPDAM` | + 新增 / 修改 | `POST /save`、`POST /modify` |
`/list`、`/page` 会先走 `beforeQuery(cmd, isList, isPage)`,默认原样返回;`/query/{id}` 不经过此钩子。`/page` 缺少 `current` 或 `size` 会抛「缺少分页参数」。
**宿主侧扩展步骤**
1. Entity 实现 `EntityBase`,并继承 `DataAuditEntity`(实现 `DeleteBase`)
2. `AddCmd implements AddBase`,实现 `createNewEntityObj()`
3. `ModifyCmd implements ModifyBase`,实现 `obtainPrimaryKey()` / `modifyIntoOldEntityObj()`
4. `ListCmd extends PageReq`
5. `XxxBusiness extends BaseQLPDAM<...>`:实现查询条件与 `entityToVo`
6. `XxxController extends RestQLPDAM<...>` + `@RequestMapping`
**Business 钩子(可覆盖)**
| 方法 | 时机 |
|---|---|
| `authExist(EN obj)` | 新增/修改前校验(如编码唯一) |
| `afterAddInTran(EN, ADD_CMD)` | 插入后、事务内 |
| `afterAddOutTran(EN, ADD_CMD)` | 事务提交后 |
| `afterModifyInTran` / `afterModifyOutTran` | 修改同理 |
| `dealDelete(EN)` / `dealDelete(List)` | 默认软删;可覆盖为物理删 |
| MyBatis:`cmdIntoWrapper(cmd, wrapper)` | 组装查询条件 |
| JPA:`cmdToPredicate(...)` | 组装查询条件 |
**编程接口**
| 接口 | 方法 |
|---|---|
| `AddBase` | `EN createNewEntityObj()` |
| `ModifyBase` | `ID obtainPrimaryKey()`、`EN modifyIntoOldEntityObj(EN)` |
| `DeleteBase` | `void deleteDealMark()` |
| `EntityBase` | `void newEntityObjSetPrimaryKey()`(可空实现;字典里用 UUID36) |
完整示例见 [plugin-dict](#plugin-dict)。
**JPA 原生 SQL 拼装 `SelectBo`**(`soil-biz-jpa`)
链式拼 `SELECT` 语句与命名参数,`sql()` 取出完整 SQL(开头已带 `SELECT `),`param()` 取出参数 Map。`judgeFlag=false` 的重载不追加。
```java
SelectBo bo = SelectBo.newInstance()
.addSQL("u.id, u.name FROM sys_user u WHERE 1=1")
.judgeAdd(EmptyTool.isNotEmpty(name), "AND u.name = :name", "name", name)
.addParam("deptId", deptId);
String sql = bo.sql();
Map params = bo.param();
```
| 方法 | 作用 |
|---|---|
| `addSQL(sql)` / `addSQL(flag, sql)` | 追加 SQL 片段 |
| `addParam(key, value)` / `addParam(flag, key, value)` | 只加参数 |
| `addSqlParam(sql, key, value)` / `addSqlParam(sql, map)` | 同时追加 SQL 与参数 |
| `judgeAdd(flag, sql, key, value)` / `judgeAdd(flag, sql, map)` | 条件为真时同时追加 |
### soil-oss
对象存储抽象。无 REST 接口,由 `plugin-file` 封装对外。
配置前缀 `oss`,`oss.name` 决定实现:
| `oss.name` | 实现 | 说明 |
|---|---|---|
| `minio` | `MinioServiceImpl` | 需 `endpointUrl` / `accessKey` / `secretKey` |
| `yd` | `YdServiceImpl` | 移动云,AWS S3 SDK |
| `local` | `LocalServiceImpl` | `endpointUrl` 作为本地根路径;物理目录 `{pathUrl}{bucketName}/`;存盘名为 `UUID.扩展名` |
**`OssService`**
```java
OssFile putFile(String bucketName, String fileName, InputStream iStream);
InputStream getFile(String bucketName, String fileName);
void deleteFile(String bucketName, String fileName);
```
`OssFile`:`relUrl`(相对路径/对象键)、`fullUrl`、`fullName`(原始文件名)。
其它配置:`oss.endpointUrl`、`oss.defaultBucket`、`oss.accessKey`、`oss.secretKey`。
### soil-log
基于 `@OptLogger` 的 AOP 操作日志。无 REST 接口,查询走 `plugin-log`。
配置前缀 `logging`:
| 配置 | 说明 |
|---|---|
| `logging.logName` | `ClickHouse` 或 `Print` |
| `logging.clickHouseUrl` / `clickHouseUser` / `clickHousePwd` | ClickHouse 连接 |
**注意:** 此前缀与 Spring Boot 原生 `logging` 可能冲突,宿主侧需自行核对。
**`LogService`**
```java
void addLog(LogApi logApi);
PageResp pageLog(PageLog pageLog);
```
切面捕获字段:`id`(雪花)、`serverIp`、`remoteIp`、`userAgent`、`requestUri`、`method`、`methodClass`、`methodName`、`params`、`result`、`time`(毫秒)、`type`、`title`、`createBy`、`createByName`、`createTime`。`hideArgs=true` 时 `params` 置空;`hideRes=true` 时清空返回 `R.data`。ClickHouse 写入表 `logs`。
### soil-poi
Excel / Word 能力。本身不注册固定路径,业务 Controller **实现下列接口** 后,会带上对应 HTTP(路径仍由业务 `@RequestMapping` 决定)。
**导入(`RestIEDI` + `IEDI`)**
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/template` | 下载导入模板(空表头 Excel) |
| POST | `/dataImport` | `multipart` 字段名 `file` |
业务需实现:
```java
ImEx importImEx(); // clazz、fileName、sheetName
void excelDataIntoDatabase(List dataList);
```
表头不匹配时提示「Excel文件表头错误,请重新下载导入模板」。
**导出(`RestIEDE` + `IEDE`)**
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | `/dataExport` | body 为查询命令 |
业务需实现:`ImEx exportImEx()`、`List cmdToExcel(QUERY_CMD)`。
**工具类**
- `ExcelRW.reader(...)` / `writer(...)`:EasyExcel 读写、多 Sheet、单元格合并、插图
- `WordTool.writerDocxByTemplate(...)`:poi-tl 按模板生成 docx,可注册 HTML / 循环行表 / 循环列表策略
### soil-sms
仅定义短信发送接口,无默认实现、无 REST。
```java
boolean sendMsg(List aimPhones, String msgOrParamJson);
```
`plugin-auth-sms` 发验证码时调用该 Bean;缺失则报「系统未提供短信服务」。`msgOrParamJson` 内容由宿主 `SmsAuthNeedApi.obtainSmsVerCodeMsgOrParamJson` 拼出(可以是纯文本,也可以是模板参数 JSON)。
### soil-wechat
微信小程序基础能力。`wechat.enable=true` 且配置 `appId` / `appSecret` 后装配 `WechatBusiness`。无 REST 接口。
**`WechatBusiness`**
| 方法 | 说明 |
|---|---|
| `AccessToken wechatToken()` | `cgi-bin/token` |
| `String obtainOpenIdByCode(String codeStr)` | `jscode2session` 换 openId |
| `String obtainPhoneByCodeAndOpenId(String codeStr, String openId)` | 手机号 code 换手机号 |
| `boolean sendSubscribeMsg(SubMsgParam param)` | 订阅消息;`param` 含 `touser`、`template_id`、`page`、`data` 等 |
`plugin-auth-wechat` 依赖本 Bean;缺失报「系统未提供微信服务」。
### soil-ai
LangChain4j `ChatMemoryStore` 的 Redis 实现,无 REST。
```java
@Bean
public ChatMemoryStore chatMemoryStore(StringRedisTemplate redis) {
return RedisChatMemoryStore.builder()
.stringRedisTemplate(redis)
.keyPrefix("chat:memory:") // 可选,默认该前缀
.ttl(Duration.ofHours(24)) // 可选;不设则不过期
.build();
}
```
实现 `getMessages` / `updateMessages` / `deleteMessages`。JSON 反序列化失败会删掉该 key 并返回空列表。
---
## plugin 业务插件
### plugin-api
跨插件 SPI,无 REST。字典、文件等模块会提供实现;其它业务也可注入使用。
**`UserApi`**(宿主实现,字典审计回填姓名)
```java
Map userNameMapByIds(List userIds);
```
**`FileApi`**(`plugin-file` 已实现,业务可直接注入)
```java
String fileUrlHead(String fileBizType);
OssFileBo uploadSingle(MultipartFile file);
OssFileBo uploadSingle(String fileBizType, MultipartFile file);
List uploadList(MultipartFile[] fileList);
List uploadList(String fileBizType, MultipartFile[] fileList);
OssFileBo uploadFileByStream(String fileName, InputStream iStream);
OssFileBo uploadFileByStream(String fileBizType, String fileName, InputStream iStream);
InputStream getFileStreamByRelUrl(String relUrl);
InputStream getFileStreamByRelUrl(String fileBizType, String relUrl);
```
`OssFileBo`:`relUrl`、`fullUrl`、`fullName`。
**`DictApi`**(字典插件已实现)
```java
Map itemMapByDictCode(String type, String dictCode);
```
认证相关 SPI 不在本模块,见下面 `plugin-auth-base`。
### plugin-auth
父聚合 `plugin-auth`(`packaging=pom`,**不能**当依赖引入),实际按需引入子模块。公共能力在 `plugin-auth-base`:Token 写入 Redis、按客户端 TTL、登录前后钩子、内置白名单。
#### 登录返回 `LoginVo`
| 字段 | 说明 |
|---|---|
| `id` / `name` / `account` / `userType` | 用户基本信息 |
| `superAdmin` | 是否超管 |
| `pwdChangeStatus` | `pwdChangeTime != null` 即为已改过密码 |
| `headPicture` | 头像(来自 `SysUserApiBo.avatar`) |
| `token` | `DSTool.secureRandom(32)`:32 字节的 URL-safe Base64(约 43 字符),后续放 Header |
宿主回填用户时使用 `SysUserApiBo`:`id`、`name`、`account`、`superAdmin`、`pwdChangeTime`、`avatar`、`userType`、`extMap`。
`clientType` 是 **自由字符串**(无枚举),密码/短信登录必传;须与 `auth.authTtl[].clientType` 精确匹配才会套用对应超时。微信登录写死 `"WECHAT"`。
`LoginType`:`PWD`、`SMS`、`EWM`、`WECHAT`,传给登录钩子。
#### Redis Key
| Key | 结构 | field / 完整 key | 值 |
|---|---|---|---|
| `VER_CODE_KEY_` + codeId | String | — | 图形验证码明文,TTL **5 分钟**,一次性读取 |
| `CACHE_TOKEN_KEY` | Hash | token | `LoginCacheBo` JSON |
| `CACHE_USER_TOKEN_KEY` | Hash | `{clientType}_{userId}` | 该端当前 token(仅 `authAllowMultipleOnline=false` 时写入,用于踢掉旧会话) |
| `SMS_AUTH_CACHE_KEY` | Hash | phone | 短信验证码缓存 |
| `SMS_AUTH_ERROR_CACHE_KEY` | Hash | phone | 短信错误次数 / 锁定 |
| `EWM_CACHE_KEY` | Hash | codeStr | 二维码状态 |
| `IP_ERROR_CACHE_KEY` | Hash | `{ip下划线}_{account}` | 密码登录 IP 错误次数 / 锁定 |
Token Hash **没有 Redis 级 TTL**,过期看 `LoginCacheBo.tokenTimeout`。`authTimeout` 为空或 `0` 表示永久有效。
请求续期(`web.requestRefreshAuthTime=true`,默认开):剩余时间 ≤ `min(15, authTimeout)` 分钟时,把 `tokenTimeout` 再延长 `authTimeout` 分钟。
`web.restartClearAuthCache=true` 时,启动会删除上表除图形验证码外的 Hash。
#### 可选钩子
| SPI | 方法 | 何时实现 |
|---|---|---|
| `LoginBeforeOrAfterOpt`(抽象类,须 `extends`) | `beforeCheckUserCanLogin(user, clientType, loginType)`、`afterLoginSuccessOpt(...)` | 登录前禁用校验、登录后写日志等 |
| `LoginOptRoles` | `listUserRoleCodesByUserId` | 需要角色码 |
| `LoginOptMenus` | `listUserPermissionsByUserId` | 权限拦截开启时建议实现,返回 Ant 路径 |
登录时非超管会把角色/权限写入 Redis;Token 拦截时若缓存里为空会再调 SPI 补全。
#### 内置白名单
启动时内置:`/error`、`/doc.html`、`/v3/api-docs/**`、`/`、`/webjars/**`。
各认证模块 `CommandLineRunner` 再追加自己的登录接口(见下)。业务额外放行写 `web.authIgnores` 或调用 `AuthIgnores.addAuthIgnores(...)`。
---
#### plugin-auth-pwd(`/auth`)
密码登录。白名单:`/auth/verCode`、`/auth/publicKey`、`/auth/pwdLogin`、`/auth/loginOut`。`/auth/currUserPwdCheck` **需要已登录**。
| 方法 | 路径 | 入参 | 返回 | 说明 |
|---|---|---|---|---|
| GET | `/auth/verCode` | 无 | `VerCodeVo` | 4 位数字图形验证码;`codeId` + `imgStr`(`data:image/png;base64,...`);Redis TTL **5 分钟**,校验时一次性读取 |
| GET | `/auth/publicKey` | 无 | `String` | 返回 `auth.pubKey` |
| POST | `/auth/pwdLogin` | body `PwdLoginCmd` | `LoginVo` | 账号密码登录(`@OptLogger` 隐藏入参和返回) |
| POST | `/auth/currUserPwdCheck` | body `{ "password": "..." }` | `Boolean` | 校验当前用户密码;需已登录;`auth.rsa=true` 时 `password` 同样为公钥密文 |
| GET | `/auth/loginOut` | 无 | `Boolean` | 删除 Redis 中当前 Token |
`PwdLoginCmd`:
```json
{
"codeId": "验证码ID,auth.ver=true 时必填",
"codeStr": "验证码",
"account": "账号",
"password": "密码,auth.rsa=true 时为公钥密文",
"clientType": "WEB"
}
```
登录顺序:验证码(可关)→ RSA 解密账号密码(可关)→ IP+账号锁定检查 → 查用户 → `beforeCheckUserCanLogin` → 写 Token。
常见错误:「传入验证ID为空」「验证码已失效」「验证码错误」「您被锁定至【...】」「账号或密码错误」;连续失败达 `pwdIpErrorMaxCount` 后锁定 `pwdIpLockTime` 分钟。
**宿主必须实现 `PwdAuthNeedUserApi`**
```java
SysUserApiBo getByAccountAndPassword(String account, String password);
boolean authCurrUserPassword(String password);
```
相关配置:`auth.ver`、`auth.rsa`、`auth.pubKey`、`auth.priKey`、`auth.pwdIpErrorMaxCount`、`auth.pwdIpLockTime`、`auth.authTtl`、`auth.authAllowMultipleOnline`。
---
#### plugin-auth-sms(`/smsAuth`)
白名单:`/smsAuth/**`。
| 方法 | 路径 | 入参 | 返回 | 说明 |
|---|---|---|---|---|
| GET | `/smsAuth/sendVerCode` | query `phone` | `Boolean` | 发送短信验证码 |
| POST | `/smsAuth/verCodeLogin` | body `SmsLoginCmd` | `LoginVo` | 验证码登录 |
```json
{ "phone": "13800000000", "verCode": "123456", "clientType": "APP" }
```
发送规则:手机号格式校验;锁定中不可发;距上次发送不足 60 秒报「请求太频繁」;验证码为 **6 位数字**;有效期内重发会复用同一验证码。校验错误累计达 `smsErrorMaxCount` 后锁定手机 `smsPhoneLockTime` 分钟。用户查不到报「手机号未注册」。
**宿主必须实现**
```java
// AuthNeedUserPubApi
SysUserApiBo loginGetByPhone(String phone);
// SmsAuthNeedApi:拼短信正文或模板参数 JSON
String obtainSmsVerCodeMsgOrParamJson(String verCode, Integer smsVerCodeEffTime);
```
另需提供 `SmsService` Bean。插件内部的 `SmsAuthProvideOpt` 由插件自己实现,不是宿主必做项。
---
#### plugin-auth-ewm(`/ewmAuth`)
扫码登录。白名单仅生成与轮询;扫码、确认接口需 **已登录端** 带 Token。
| 方法 | 路径 | 入参 | 返回 | 说明 |
|---|---|---|---|---|
| GET | `/ewmAuth/getCodeStrByClientType` | query `clientType` | `String` | 生成二维码字符串(待扫) |
| GET | `/ewmAuth/getSmResByCodeStr` | query `codeStr` | `EwmLoginVo` | 待确认端轮询;`CONFIRM_OK` 时带 `loginVo` |
| GET | `/ewmAuth/smLoginByCodeStr` | query `codeStr` | `String` | 已登录端扫码,进入待确认,返回 clientType |
| GET | `/ewmAuth/confirmSmLoginByCodeStr` | query `codeStr`、`confirmStatus` | `Boolean` | `true` 确认 / `false` 拒绝 |
状态 `EwmSmRes`:`WAIT_SM` → `SM_WAIT_CONFIRM` → `CONFIRM_OK` 或 `REFUSE_CONFIRM`;过期/无效为 `YSX`。二维码有效期 `auth.ewmEffTime` 分钟。
典型流程:待登录端取 `codeStr` 展示二维码并轮询 → 已登录端扫码 → 已登录端确认 → 待登录端轮询拿到 `loginVo.token`。
**宿主必须实现 `EwmAuthNeedUserApi`**
```java
SysUserApiBo getByUserId(String userId);
```
---
#### plugin-auth-wechat(`/wechatAuth`)
白名单:`/wechatAuth/**`。`clientType` 固定为 `"WECHAT"`。需 `WechatBusiness` Bean。
| 方法 | 路径 | 入参 | 返回 | 说明 |
|---|---|---|---|---|
| GET | `/wechatAuth/codeLogin` | query `code` | `WechatLoginVo` | 临时 code 换 openId 登录 |
| POST | `/wechatAuth/phoneCodeAndOpenIdLogin` | body | `WechatLoginVo` | 手机号 code + openId 登录 / 绑号 / 建用户 |
`WechatLoginVo`:`success`、`openId`、`loginVo`。
`codeLogin`:已绑定用户则 `success=true` 并发 Token;未绑定则 `success=false`、`loginVo=null`,仍返回 `openId` 供下一步绑定。
`phoneCodeAndOpenIdLogin`:
```json
{ "phoneCode": "微信手机号code", "openId": "上一步拿到的openId" }
```
已有手机号用户:可选更新 openId 后登录;没有则 `wechatLoginCreateUserByPhoneAndOpenId` 自动建用户(创建路径不走 `beforeCheckUserCanLogin`)。
**宿主必须实现 `WechatAuthNeedUserApi`**(继承 `AuthNeedUserPubApi`)
```java
SysUserApiBo wechatLoginGetByOpenId(String openId);
void wechatLoginModifyOpenIdById(String userId, String openId);
SysUserApiBo wechatLoginCreateUserByPhoneAndOpenId(String phone, String openId);
SysUserApiBo loginGetByPhone(String phone);
```
### plugin-security
安全网关:拦截器链 + 全局异常。无业务 REST,对 `/**` 生效。依赖 `plugin-auth-base`。
**拦截顺序**
| order | 类 | 职责 |
|---|---|---|
| 1 | `SafetyCertificateInterceptor` | 安全证书。**Java 默认 `web.certificateEnable=true`**。`false` 时整段跳过。开启时**不读取请求头**,把 yaml 里的 `web.safetyCertificate` 传给宿主 `SafetyCertificateAdapter`;配置为空 → `501`;未提供适配器 Bean → NPE(落入 `412`「系统繁忙」)。要比对客户端证书时,适配器须自行从 Request 取头 |
| 2 | `TokenPreHandlerInterceptor` | 读 Header Token → Redis 恢复 `UserContext`;校验/刷新超时;非超管时懒加载角色权限;`afterCompletion` 清理上下文并打耗时日志 |
| 3 | `UserPermissionInterceptor` | 白名单路径 / 白名单 Token / 未登录 401 / 超管放行 / Ant 路径权限 |
白名单 Token(`web.whiteTokenEnable=true` 且 Header 等于 `web.whiteToken`):Token 拦截器直接放行且 **不建 UserContext**;权限拦截器也视为放行。
权限匹配:`LoginOptMenus` 返回的字符串按 **Ant 风格** 匹配 `request.getServletPath()`,例如 `/dict/**`。不匹配抛「无权访问:{path}」。`web.permissionsEnable=false` 时不校验路径。
静态资源(`ResourceHttpRequestHandler`)直接放行。
**证书开启时宿主必须实现**
```java
@Service
public class SafetyCertificateAdapterImpl extends SafetyCertificateAdapter {
@Override
public void verifySafetyCertificate(String safetyCertificate) {
// 参数来自 yaml 的 web.safetyCertificate,不是请求头
// 若要比对客户端传入的证书,请自行从 Request 取头
// 校验失败请 throwBiz 或抛带 ResCode.CERTIFICATE_ERROR / CERTIFICATE_EXPIRE 的 BizException
}
}
```
**全局异常 `ExceptionAdvice`(最高优先级)**
| 异常 | 响应 |
|---|---|
| `BizException` | 对应 `ResCode` + 消息 |
| `MethodArgumentNotValidException` | `412` + 校验消息拼接 |
| `HttpMessageNotReadableException` / `MethodArgumentTypeMismatchException` | `412` +「参数错误」 |
| 其它 `Exception` | `412` +「系统繁忙,请稍后重试」 |
| 未登录 / Token 过期 | `401` |
**按需装配的 `HMACTool` / `AESTool` Bean**
`plugin-security` **不会**无条件装配这两个 Bean。Java 无默认密钥。yaml **配置了对应项且有值** 才注册(`@ConditionalOnProperty`,键名 kebab-case):
| 配置(yaml 规范键) | 条件 | 密钥约束 | Bean |
|---|---|---|---|
| `web.hmac-secret-key` | 该项存在 | UTF-8 **至少 32 字节** | `HMACTool` |
| `web.aes-secret-key` | 该项存在 | UTF-8 **刚好 16 / 24 / 32 字节** | `AESTool` |
宽松绑定也认 `hmacSecretKey` / `aesSecretKey`。**不要写成空串**(属性算存在,随后 `getInstance` 会因长度失败)。不需要时直接不配。字符串密钥请用 `DSTool.secureRandomA2Z(32)`(HMAC)或 `secureRandomA2Z(16/24/32)`(AES),不要用口令或仓库示例串。
宿主按需注入:
```java
@Autowired(required = false)
private HMACTool hmacTool;
@Autowired(required = false)
private AESTool aesTool;
```
配置前缀 `web`,见 [配置参考](#配置参考)。
### plugin-dict
数据字典。四选一引入(jpa/mybatis × mysql/oracle),HTTP 一致。
| 变体 | 表名 | 说明 |
|---|---|---|
| MySQL | `dict` / `dict_item` | 字典列:`_id` `_type` `_name` `_code` `_remark`;字典项:`_id` `_name` `_code` `_sort` `_remark`、`dict_id` |
| Oracle | `DICT` / `DICT_ITEM` | 字典列:`DICT_ID` `DICT_TYPE` `DICT_NAME` `DICT_CODE` `DICT_REMARK`;字典项:`DICT_ITEM_ID` `DICT_ITEM_NAME` `DICT_ITEM_CODE` `DICT_ITEM_SORT` `DICT_ITEM_REMARK` `DICT_ITEM_DICTID`。`DICT_ITEM_REMARK` 为 `NVARCHAR2(64)`,短于 MySQL 的 `varchar(255)` |
主键:UUID36。删除为**物理删除**(覆盖了框架软删)。宿主需实现 `UserApi`(审计人姓名回填)。未传 `type` 时默认为 `"SYSTEM"`。
#### `/dict`
| 方法 | 路径 | 入参 | 返回 |
|---|---|---|---|
| GET | `/dict/query/{id}` | path `id` | `DictVo` |
| POST | `/dict/list` | body `DictListCmd` | `List` |
| POST | `/dict/page` | body `DictListCmd`(需 `current`、`size`) | `PageResp` |
| GET | `/dict/remove/{id}` | path `id` | `DictVo` |
| POST | `/dict/save` | body `DictAddCmd` | `DictVo` |
| POST | `/dict/modify` | body `DictModifyCmd` | `DictVo` |
新增示例:
```json
{ "type": "SYSTEM", "name": "性别", "code": "GENDER", "remark": "" }
```
修改示例:
```json
{ "id": "...", "name": "性别", "code": "GENDER", "remark": "" }
```
列表/分页可筛 `type`、`name`、`code`。`DictVo`:`id`、`name`、`code`、`remark` + 审计字段(`createUser` / `createUserName` / `createTime` / `updateUser` / `updateUserName` / `updateTime`)。返回 VO **不含** `type`。
#### `/dictItem`
| 方法 | 路径 | 入参 | 返回 |
|---|---|---|---|
| GET | `/dictItem/query/{id}` | path `id` | `DictItemVo` |
| POST | `/dictItem/list` | body `DictItemListCmd` | `List` |
| POST | `/dictItem/page` | body `DictItemListCmd` | `PageResp` |
| GET | `/dictItem/remove/{id}` | path `id` | `DictItemVo` |
| POST | `/dictItem/save` | body `DictItemAddCmd` | `DictItemVo` |
| POST | `/dictItem/modify` | body `DictItemModifyCmd` | `DictItemVo` |
| GET | `/dictItem/listItemByDictCode/{dictCode}` | path `dictCode`,query `type`(可空,默认 SYSTEM) | `List` |
新增字典项:
```json
{ "name": "男", "code": "M", "sort": 1, "remark": "", "dictId": "所属字典ID" }
```
`listItemByDictCode` 返回 `[{ "key": "编码", "value": "名称" }, ...]`(`KV` 字段为 `key` / `value` / `flag`)。编程接口:`DictApi.itemMapByDictCode(type, dictCode)`。
`UserApi` 示例:
```java
@Service
public class UserApiImpl implements UserApi {
@Override
public Map userNameMapByIds(List userIds) {
return userIds.stream().collect(Collectors.toMap(id -> id, this::findName));
}
}
```
### plugin-file
文件上传下载,封装 `soil-oss`。插件自身实现 `FileApi`。需配置 `oss.*`。
| 方法 | 路径 | 入参 | 返回 | 说明 |
|---|---|---|---|---|
| GET | `/file/fileUrlHead` | 无 | `FileUrlHeadVo` | `defaultFileUrlHead` + `fileUrlHeadMap`(type → 头) |
| POST | `/file/uploadSingle` | query `fileBizType`(可空),part **`file`** | `OssFile` | 单文件 |
| POST | `/file/uploadList` | query `fileBizType`(可空),part **`fileList`** | `List` | 多文件 |
| POST | `/file/download` | body `FileDownloadCmd` | 文件流(非 `R`) | 下载 |
桶解析:`fileBizType` 在 `file.bizTypeExt` 里按 `type` 找 `bucketName`,找不到用 `oss.defaultBucket`。
```json
{ "fileBizType": "avatar", "relUrl": "上传返回的 relUrl" }
```
`OssFile`:`relUrl`、`fullUrl`、`fullName`。配置:`file.defaultFileUrlHead`;`file.bizTypeExt[].type` / `bucketName` / `fileUrlHead`。
业务代码上传(不走 HTTP):
```java
@Autowired
private FileApi fileApi;
OssFileBo bo = fileApi.uploadSingle("avatar", multipartFile);
```
### plugin-log
操作日志查询,依赖 `soil-log`(需已配置 `logging.logName` 以产生 `LogService`)。
| 方法 | 路径 | 入参 | 返回 |
|---|---|---|---|
| POST | `/log/page` | body `LogListCmd`(需 `current`、`size`) | `PageResp` |
```json
{
"current": 1,
"size": 20,
"type": "LOGIN",
"title": "",
"createBy": "",
"createByName": "",
"startTime": "2026-01-01 00:00:00",
"endTime": "2026-12-31 23:59:59"
}
```
`LogVo`:`id`、`serverIp`、`remoteIp`、`userAgent`、`requestUri`、`method`、`methodClass`、`methodName`、`params`、`result`、`time`、`createBy`、`createByName`、`type`、`createTime`、`title`。
`type` 来自 `@OptLogger.type`,框架不限定取值(登录接口用 `"LOGIN"`,业务默认 `"BIZ"`)。
### plugin-zlmk
对接 ZLM(ZLMediaKit)拉流。配置前缀 `zlmk`(`ZlmkConfig` 已自动装配)。宿主仍需实现 `RtspUrlBuild`。
| 方法 | 路径 | 入参 | 返回 | 说明 |
|---|---|---|---|---|
| POST | `/zlmk/obtainStream` | body `ZlmkCmd` | `ZlmkVo` | 调用 ZLM `/index/api/addStreamProxy` |
| GET | `/zlmk/closeStream/{streamId}` | path `streamId` | `Boolean` | 调用 `/index/api/close_streams` |
```json
{
"ip": "192.168.1.64",
"type": "海康",
"user": "admin",
"password": "...",
"channel": 1,
"stream": 0
}
```
`ZlmkVo`:`success`、`streamId`、`rtmpUrl`、`flvUrl`、`hlsUrl`、`wsFlvUrl`。`ip` 使用 `@IPV4` 校验。
**宿主必须实现**
```java
@Service
public class RtspUrlBuildImpl extends RtspUrlBuild {
@Override
public String buildRtspUrl(String ip, Integer port, String type,
String user, String password, Integer channel, Integer stream) {
return "rtsp://" + user + ":" + password + "@" + ip + ":" + port + "/...";
}
}
```
`port` 来自配置 `zlmk.rtspPort`。其它配置:`zlmk.zlmkServer`(如 `http://127.0.0.1:80`)、`zlmk.zlmkSecret`。
---
## 配置参考
宿主 `application.yml` 示例(按实际引入的模块裁剪)。另需自行配置 Redis、数据源。
```yaml
auth:
authAllowMultipleOnline: true # Java 默认 true;false 时同一 clientType+userId 只保留最新 Token
ver: false # Java 默认 false;密码登录图形验证码
rsa: false # Java 默认 false;账号密码 RSA 加密传输
pubKey: ""
priKey: ""
# 以下 Integer 项 Java 无默认值(null)。不配则对应锁定逻辑跳过;短信登录必须配 smsVerCodeEffTime,否则发码 NPE
smsErrorMaxCount: 5 # 短信验证码最大错误次数
smsPhoneLockTime: 30 # 手机锁定分钟数
smsVerCodeEffTime: 5 # 短信验证码有效分钟数
ewmEffTime: 5 # 二维码有效分钟数
pwdIpErrorMaxCount: 5 # 密码登录 IP+账号 最大错误次数
pwdIpLockTime: 30 # IP 锁定分钟数
authTtl: # 按 clientType 精确匹配;空或 0 表示永久
- clientType: WEB
authTimeout: 120
- clientType: APP
authTimeout: 43200
- clientType: WECHAT
authTimeout: 43200
web:
authHeaderKey: Authorization # Java 默认 Authorization
requestRefreshAuthTime: true # Java 默认 true;临近过期自动续期
restartClearAuthCache: false # Java 默认 false;启动时清空认证相关 Redis Hash
whiteTokenEnable: false # Java 默认 false
whiteToken: ""
permissionsEnable: true # Java 默认 true;按用户 permissions 做 Ant 路径校验
certificateEnable: false # Java 默认 true;本地开发建议 false,否则须实现 SafetyCertificateAdapter
safetyCertificate: ""
# 下列为可选。不配则不注册对应 Bean;不要写成空串
# hmac-secret-key: <至少32字节随机串> # 有值才注册 HMACTool
# aes-secret-key: <16或24或32字节随机串> # 有值才注册 AESTool
authIgnores: # 额外白名单,Ant 风格
- /actuator/**
oss:
name: minio # minio | yd | local
endpointUrl: http://127.0.0.1:9000
defaultBucket: default
accessKey: ""
secretKey: ""
file:
defaultFileUrlHead: https://cdn.example.com/
bizTypeExt:
- type: avatar
bucketName: avatar
fileUrlHead: https://cdn.example.com/avatar/
logging:
logName: Print # ClickHouse | Print
clickHouseUrl: ""
clickHouseUser: ""
clickHousePwd: ""
wechat:
enable: false
appId: ""
appSecret: ""
zlmk:
rtspPort: 554
zlmkServer: http://127.0.0.1:80
zlmkSecret: ""
```
ZLMK 除上述配置外,宿主还需实现 `RtspUrlBuild`。
## 本地构建
需要 JDK 21 与 Maven:
```bash
git clone https://gitee.com/mujunhub/mujun.git
cd mujun
mvn clean install -DskipTests
```
本仓库是库项目,不能 `java -jar` 直接运行。发布形态为 Maven 构件(Maven Central,`cn.mujunhub`)。宿主侧 import `cn.mujunhub:dependencies:1.7.2` 后按需引入子模块即可。