# 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` 后按需引入子模块即可。