feat: 用户启停/注销、机构账号查询、手机号查记录、机构上传配置
1. 用户管理:新增启用(user/enable)/禁用(user/disable)/注销(user/admin_cancel)接口,
平台超管可操作全部用户,机构管理员限本机构;注销带分布式锁、禁自己/超管守卫
2. 禁用/注销立即踢下线:Redis黑名单(user:disabled:{uid}) + JWT过滤器拦截,
TTL与token有效期一致
3. 系统管理员查看各机构账号:GET user/page/org_account(机构/账号/手机号等筛选)
4. 按手机号查上传/发送/接收记录:GET user_file/record/page_by_phone,
发送/接收方向按部署侧 file.transfer.location 自动映射
5. 机构上传配置:新表 x_upload_config + upload_config/info|modify|check 接口,
上传总开关/后缀白名单/单文件大小限制,网盘落库时强制校验
6. 加固:UserInfoDTO.setPassword 缺省密码未配置时不再抛 rawPassword cannot be null
7. 新增 x_upload_config 建表脚本(upgrade-20260915-release.sql 为上线版)、
生产部署说明、前端接口文档×2、运维脚本 sfile-ctl.sh、AGENTS.md
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -30,4 +30,5 @@ build/
|
|||||||
!**/src/test/**/build/
|
!**/src/test/**/build/
|
||||||
|
|
||||||
### VS Code ###
|
### VS Code ###
|
||||||
.vscode/
|
.vscode/.DS_Store
|
||||||
|
.commandcode/
|
||||||
|
|||||||
74
AGENTS.md
Normal file
74
AGENTS.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# AGENTS.md — s-file(alita-bjy-uygur)编码代理指南
|
||||||
|
|
||||||
|
信创文件传输/网盘后端服务:内网与外网之间做文件安全传输("内穿外/外传内"),附带个人网盘、分享、机构与用户管理、通知等能力。单 Maven 模块,Spring Boot 可执行 jar,Docker 部署(`src/main/resources/Dockerfile`)。
|
||||||
|
|
||||||
|
## 技术栈(务必匹配,勿升级)
|
||||||
|
|
||||||
|
- **Java 8** + Spring Boot **2.4.2**(父 POM 管理)
|
||||||
|
- ORM:**MyBatis-Plus 3.4.0**(注解/BaseMapper)+ 自研 `com.senmesh.tkmybatis` 封装 + PageHelper 1.4.6 + 自定义分页拦截器
|
||||||
|
- 数据库:**达梦 DM8**(信创主力,另有 MySQL/PG 驱动),Druid 连接池,SQL 需多库兼容
|
||||||
|
- 安全:Spring Security + 自研 JWT(`com.senmesh.security`),Redisson 分布式锁,RabbitMQ
|
||||||
|
- 文档:springfox swagger2 2.7.0;Lombok + MapStruct 1.2.0
|
||||||
|
|
||||||
|
## 常用命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 编译(需 JDK 8,例如本机 Zulu 1.8)
|
||||||
|
export JAVA_HOME=/Users/leijiaping/Library/Java/JavaVirtualMachines/azul-1.8.0_452/Contents/Home
|
||||||
|
mvn compile -DskipTests
|
||||||
|
|
||||||
|
# 测试目录基本为空(仅 PwdTest),项目无自动化测试,验证靠编译 + 自查
|
||||||
|
```
|
||||||
|
|
||||||
|
**已知依赖问题(重要)**:商业依赖 `e-iceblue:spire.pdf:3.7.4` 在阿里云/e-iceblue 官方仓库均不存在(官方仓库只有 9.x+)。本地仓库 `~/.m2/repository/e-iceblue/spire.pdf/3.7.4/` 现已安装一个**手工编译级 stub jar**(仅为 `PdfUtils.java` 引用的类签名生成),使 `mvn package` 可以通过;**代价是运行 PDF 盖章功能(`PdfUtils.pdfStampPerPage`)会失败**。正式发布前需从公司私服获取真实 jar 覆盖。全量快速编译验证可跳过 PdfUtils.java(详见下方命令)。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn dependency:build-classpath -Dmdep.outputFile=/tmp/cp.txt # 需联网
|
||||||
|
$JAVA_HOME/bin/javac -encoding UTF-8 -cp "$(cat /tmp/cp.txt)" \
|
||||||
|
-sourcepath src/main/java -d /tmp/v \
|
||||||
|
$(find src/main/java -name "*.java" ! -name "PdfUtils.java")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 包结构地图(`com.senmesh`)
|
||||||
|
|
||||||
|
| 包 | 职责 |
|
||||||
|
|---|---|
|
||||||
|
| `usercenter` | 用户中心:`user`(用户/机构用户/注销)、`org`、`authority`(角色/权限)、`notice`(通知)、`auth` 等,子模块按「controller/service/impl/dto/entity/dao/mapper(XML)/constant/enums」分目录 |
|
||||||
|
| `sfile` | 文件业务:`UserFile`(上传/传输/分享记录,核心表 `x_user_file`)、`TransferUserFile`、`ShareUserFile`、`TransferFileMonitorJob`(定时扫描传输目录) |
|
||||||
|
| `component/file` | 物理文件上传组件(`xs_file_record`、`/file/upload`、`FileProperties`) |
|
||||||
|
| `common` | `AlitaResult`、`PageQueryDTO`、`BaseEntity`、`UserContextHolder`、异常体系 |
|
||||||
|
| `security` | JWT 过滤器、登录、`UserInfoDTO` |
|
||||||
|
| `frame/redis` | Redisson 封装:`RedissonUtils`、`@AutoUnLock`、`LockDTO` |
|
||||||
|
| `mybatis` / `mybatisplus` / `tkmybatis` | 分页拦截器、`@MapperScan`、`MyMapper<T>` 基类 |
|
||||||
|
| `mq`、`codesetting`、`config` 等 | 其他支撑模块 |
|
||||||
|
|
||||||
|
**注意**:源码树里有同包覆写的框架类(`org/springframework/...`、`com/senmesh/swagger`),改动前先确认是否为刻意覆写,不要删除。
|
||||||
|
|
||||||
|
## 新增一个接口的标准动作
|
||||||
|
|
||||||
|
1. **DTO**:查询 DTO 继承 `PageQueryDTO<XxxDTO>`(自带 pageNo/pageSize/needPage/pageResult);出参 DTO 继承 `BaseDTO`(用 `@SuperBuilder`);时间字段统一 `Date` + `@DateTimeFormat` + `@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss", timezone="GMT+8")`。
|
||||||
|
2. **Entity**:继承 `BaseEntity`(带机构的继承 `OrgBaseEntity`),`@TableName` 注解,逻辑删除字段 `deleted`。
|
||||||
|
3. **Mapper**:接口继承 `MyMapper<T>`(已有 `saveEntity`/`updateEntitySelective`/`getById`/`deleteEntity`,自动填充 createBy/updateBy),放 `**/dao/`;**自定义 SQL 的 XML 放同名 java 源码目录的 `mapper/` 下**(pom 已配置打包 java 目录下 `**/*.xml`),XML 中参数一律 `@Param("param")` + `param.xxx` 引用。
|
||||||
|
4. **Service**:接口 + `impl`。权限校验用命令式断言(项目无 @PreAuthorize):
|
||||||
|
- 平台超管:`Assert.isTrue(UserContextHolder.isSuperior(), "当前用户无操作权限")`,注意字段拼写就是 `supeiror`(历史笔误,勿"修复")
|
||||||
|
- 机构管理员:`UserContextHolder.isAdministrator()`;平台机构:`checkPlatformOrg()`
|
||||||
|
5. **Controller**:返回一律 `AlitaResult<T>`(`AlitaResult.success(data)`);`@Api(tags=...)` + `@ApiOperation`;分页接口返回 `AlitaResult<Page<T>>`,模式:Service 把 queryDTO 传给 mapper 即自动分页,Controller 返回 `queryDTO.getPageResult()`。查询类用 GET + query 参数绑定,操作类用 POST + `@Validated @RequestBody`。
|
||||||
|
6. **URL 前缀有语义**:`anon/**` 匿名(清单在配置 `security.anonymous`)、`unac/**` 已登录未激活可用、`authed/**` 已实名;普通前缀 = 登录即可。
|
||||||
|
|
||||||
|
## 关键业务概念
|
||||||
|
|
||||||
|
- **用户**(`uc_user`):`status` 1待激活/2已激活/3已注销 + `enabled` 启用开关 + 逻辑删除 `deleted`,三者独立。登录侧实时校验 `enabled=TRUE and status!=3`,禁用/注销后用户下次请求即失效。注销有两套:自助申请流(`UserCancelService`,`uc_user_cancel` 表)与管理员直接注销(`user/admin_cancel`)。同一手机号可对应多个账号(企业+个人)。
|
||||||
|
- **机构**:`xs_org`(`parent_id` 树)+ `uc_org_user` 中间表(含 `administrator` 标志,一个用户可在多个机构,登录时选定 orgId)。**用户表没有 org_id**。平台机构固定 `orgId=1`(`CommonConstants.PLATFORM_ORG_ID`)。
|
||||||
|
- **文件记录**(`x_user_file`):上传/发送/接收/分享全在这一张表,靠 `root_type` 区分:`1`普通(上传)、`2`内穿外、`3`外传内、`4`我的分享;`file_status`:1正常/2失效/11解析中/12待审核/13传输中/14失败。表里只有 `user_id`,无手机号,按手机号查需 JOIN `uc_user.phone`。
|
||||||
|
- **传输方向映射**(`file.transfer.location`:1内网/2外网):发送记录 = 内网端 rootType 2 / 外网端 rootType 3;接收记录取反。手机号只在传输 ZIP 包内 `transferMetadata.json`,接收端解包时靠它反查本地用户。
|
||||||
|
|
||||||
|
## 陷阱清单
|
||||||
|
|
||||||
|
- 只能写 **Java 8** 语法(无 `var`/`Set.of`/record/switch 表达式;集合用 guava `Sets.newHashSet` 等也行)。
|
||||||
|
- SQL 要兼容 DM8:布尔字面量用 `TRUE/FALSE`(bit 列),时间用 `now()`;避免 MySQL 专属函数;分页依赖拦截器,XML 里不要手写 limit。
|
||||||
|
- XML 放错位置不会被打包——必须放 java 源码目录(与现有 `UserMapper.xml`、`UserFileMapper.xml` 同级模式)。
|
||||||
|
- 驼峰映射由 MyBatis-Plus 默认开启,XML 直接 `SELECT u.real_name` 即可映射 `realName`;列别名规避同名冲突(如 `u.type AS user_type`)。
|
||||||
|
- 注销/禁用等敏感操作要防并发:参考 `userCancel` 的 `RedissonUtils.tryLockAutomatically(LockDTO...)` + `@AutoUnLock` 模式;禁止注销/禁用自己与超管。
|
||||||
|
- 数据库初始化脚本在 `db/`(`init-table.sql` MySQL 版、`init-table-postgres-full.sql` PG 版),改表结构需同步两个版本;增量变更另建 `db/upgrade-YYYYMMDD-*.sql`(现有:`upgrade-20260914-upload-config.sql`),历史上有过 DDL 只加脚本、不改 init 的情况,新环境安装依赖 init 脚本,**改表务必 init 与 upgrade 双写**。
|
||||||
|
- 给前端的接口变更请同步维护 `docs/` 下的接口文档(现有:`接口文档-用户管理与记录查询.md`、`接口文档-机构上传配置.md`)。
|
||||||
|
- `.commandcode/` 为本地工具产物,勿提交。
|
||||||
@@ -2018,5 +2018,31 @@ COMMENT ON COLUMN x_file_risk.update_by IS '修改人';
|
|||||||
COMMENT ON COLUMN x_file_risk.update_time IS '修改时间';
|
COMMENT ON COLUMN x_file_risk.update_time IS '修改时间';
|
||||||
COMMENT ON COLUMN x_file_risk.proxy_by IS '代理人';
|
COMMENT ON COLUMN x_file_risk.proxy_by IS '代理人';
|
||||||
|
|
||||||
|
CREATE TABLE x_upload_config (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
org_id BIGINT DEFAULT NULL,
|
||||||
|
upload_enabled BOOLEAN DEFAULT TRUE,
|
||||||
|
allow_all_type BOOLEAN DEFAULT TRUE,
|
||||||
|
allowed_suffixes VARCHAR(1024) DEFAULT NULL,
|
||||||
|
max_file_size BIGINT DEFAULT NULL,
|
||||||
|
deleted BOOLEAN DEFAULT FALSE,
|
||||||
|
create_by BIGINT DEFAULT NULL,
|
||||||
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_by BIGINT DEFAULT NULL,
|
||||||
|
update_time TIMESTAMP DEFAULT NULL,
|
||||||
|
proxy_by BIGINT DEFAULT NULL,
|
||||||
|
CONSTRAINT uk_upload_config_org_id UNIQUE (org_id)
|
||||||
|
);
|
||||||
|
COMMENT ON TABLE x_upload_config IS '机构上传配置';
|
||||||
|
COMMENT ON COLUMN x_upload_config.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN x_upload_config.org_id IS '机构id';
|
||||||
|
COMMENT ON COLUMN x_upload_config.upload_enabled IS '是否允许上传';
|
||||||
|
COMMENT ON COLUMN x_upload_config.allow_all_type IS '是否允许全部类型(false时按allowed_suffixes白名单校验)';
|
||||||
|
COMMENT ON COLUMN x_upload_config.allowed_suffixes IS '允许的文件后缀(逗号分隔,小写),allow_all_type=false时生效';
|
||||||
|
COMMENT ON COLUMN x_upload_config.max_file_size IS '单文件大小上限(MB),空或0不限制';
|
||||||
|
COMMENT ON COLUMN x_upload_config.deleted IS '是否删除';
|
||||||
|
COMMENT ON COLUMN x_upload_config.create_by IS '创建人';
|
||||||
|
COMMENT ON COLUMN x_upload_config.create_time IS '创建时间';
|
||||||
|
COMMENT ON COLUMN x_upload_config.update_by IS '修改人';
|
||||||
|
COMMENT ON COLUMN x_upload_config.update_time IS '修改时间';
|
||||||
|
COMMENT ON COLUMN x_upload_config.proxy_by IS '代理人';
|
||||||
|
|||||||
@@ -1260,4 +1260,22 @@ CREATE TABLE `x_file_risk` (
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件风险';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件风险';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE `x_upload_config` (
|
||||||
|
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`org_id` bigint(20) DEFAULT NULL COMMENT '机构id',
|
||||||
|
`upload_enabled` bit(1) DEFAULT b'1' COMMENT '是否允许上传',
|
||||||
|
`allow_all_type` bit(1) DEFAULT b'1' COMMENT '是否允许全部类型(false时按allowed_suffixes白名单校验)',
|
||||||
|
`allowed_suffixes` varchar(1024) DEFAULT NULL COMMENT '允许的文件后缀(逗号分隔,小写),allow_all_type=false时生效',
|
||||||
|
`max_file_size` bigint(20) DEFAULT NULL COMMENT '单文件大小上限(MB),空或0不限制',
|
||||||
|
`deleted` bit(1) DEFAULT b'0' COMMENT '是否删除',
|
||||||
|
`create_by` bigint(20) DEFAULT NULL COMMENT '创建人',
|
||||||
|
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_by` bigint(20) DEFAULT NULL COMMENT '修改人',
|
||||||
|
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
|
||||||
|
`proxy_by` bigint(20) DEFAULT NULL COMMENT '代理人',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE KEY `uk_org_id` (`org_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='机构上传配置';
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
62
db/upgrade-20260914-upload-config.sql
Normal file
62
db/upgrade-20260914-upload-config.sql
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
-- =============================================================
|
||||||
|
-- 升级脚本:机构上传配置(文件上传类型/大小限制)
|
||||||
|
-- 日期:2026-09-14
|
||||||
|
-- 说明:新增 x_upload_config 表(一行对应一个机构)。
|
||||||
|
-- 机构管理员可配置本机构允许上传的文件后缀白名单、
|
||||||
|
-- 单文件大小上限、上传总开关;未配置的机构默认允许全部。
|
||||||
|
-- 拦截点:网盘上传落库(normal_user_file/create)。
|
||||||
|
-- 执行方式:MySQL/达梦(DM8 MySQL兼容模式)执行第 1 节;
|
||||||
|
-- PostgreSQL 执行第 2 节。
|
||||||
|
-- =============================================================
|
||||||
|
|
||||||
|
-- -------------------------------------------------------------
|
||||||
|
-- 1. MySQL / 达梦 DM8
|
||||||
|
-- -------------------------------------------------------------
|
||||||
|
CREATE TABLE `x_upload_config` (
|
||||||
|
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`org_id` bigint(20) DEFAULT NULL COMMENT '机构id',
|
||||||
|
`upload_enabled` bit(1) DEFAULT b'1' COMMENT '是否允许上传',
|
||||||
|
`allow_all_type` bit(1) DEFAULT b'1' COMMENT '是否允许全部类型(false时按allowed_suffixes白名单校验)',
|
||||||
|
`allowed_suffixes` varchar(1024) DEFAULT NULL COMMENT '允许的文件后缀(逗号分隔,小写),allow_all_type=false时生效',
|
||||||
|
`max_file_size` bigint(20) DEFAULT NULL COMMENT '单文件大小上限(MB),空或0不限制',
|
||||||
|
`deleted` bit(1) DEFAULT b'0' COMMENT '是否删除',
|
||||||
|
`create_by` bigint(20) DEFAULT NULL COMMENT '创建人',
|
||||||
|
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`update_by` bigint(20) DEFAULT NULL COMMENT '修改人',
|
||||||
|
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
|
||||||
|
`proxy_by` bigint(20) DEFAULT NULL COMMENT '代理人',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE KEY `uk_org_id` (`org_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='机构上传配置';
|
||||||
|
|
||||||
|
-- -------------------------------------------------------------
|
||||||
|
-- 2. PostgreSQL
|
||||||
|
-- -------------------------------------------------------------
|
||||||
|
CREATE TABLE x_upload_config (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
org_id BIGINT DEFAULT NULL,
|
||||||
|
upload_enabled BOOLEAN DEFAULT TRUE,
|
||||||
|
allow_all_type BOOLEAN DEFAULT TRUE,
|
||||||
|
allowed_suffixes VARCHAR(1024) DEFAULT NULL,
|
||||||
|
max_file_size BIGINT DEFAULT NULL,
|
||||||
|
deleted BOOLEAN DEFAULT FALSE,
|
||||||
|
create_by BIGINT DEFAULT NULL,
|
||||||
|
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
update_by BIGINT DEFAULT NULL,
|
||||||
|
update_time TIMESTAMP DEFAULT NULL,
|
||||||
|
proxy_by BIGINT DEFAULT NULL,
|
||||||
|
CONSTRAINT uk_upload_config_org_id UNIQUE (org_id)
|
||||||
|
);
|
||||||
|
COMMENT ON TABLE x_upload_config IS '机构上传配置';
|
||||||
|
COMMENT ON COLUMN x_upload_config.id IS '主键ID';
|
||||||
|
COMMENT ON COLUMN x_upload_config.org_id IS '机构id';
|
||||||
|
COMMENT ON COLUMN x_upload_config.upload_enabled IS '是否允许上传';
|
||||||
|
COMMENT ON COLUMN x_upload_config.allow_all_type IS '是否允许全部类型(false时按allowed_suffixes白名单校验)';
|
||||||
|
COMMENT ON COLUMN x_upload_config.allowed_suffixes IS '允许的文件后缀(逗号分隔,小写),allow_all_type=false时生效';
|
||||||
|
COMMENT ON COLUMN x_upload_config.max_file_size IS '单文件大小上限(MB),空或0不限制';
|
||||||
|
COMMENT ON COLUMN x_upload_config.deleted IS '是否删除';
|
||||||
|
COMMENT ON COLUMN x_upload_config.create_by IS '创建人';
|
||||||
|
COMMENT ON COLUMN x_upload_config.create_time IS '创建时间';
|
||||||
|
COMMENT ON COLUMN x_upload_config.update_by IS '修改人';
|
||||||
|
COMMENT ON COLUMN x_upload_config.update_time IS '修改时间';
|
||||||
|
COMMENT ON COLUMN x_upload_config.proxy_by IS '代理人';
|
||||||
90
db/upgrade-20260915-release.sql
Normal file
90
db/upgrade-20260915-release.sql
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
-- =====================================================================
|
||||||
|
-- s-file 上线数据库脚本(2026-09-15 版本)
|
||||||
|
-- 适用场景:已有生产库升级(老版本 → 当前版本)
|
||||||
|
--
|
||||||
|
-- 本次版本(用户启停/注销、机构账号查询、手机号查记录、机构上传配置)
|
||||||
|
-- 相对老版本的数据库变更只有一项:
|
||||||
|
-- 新增表 x_upload_config(机构上传配置)
|
||||||
|
-- 其余功能(启停/注销/查询)全部复用已有表字段,无需任何变更。
|
||||||
|
--
|
||||||
|
-- 【执行说明】
|
||||||
|
-- * 生产主力库为达梦 DM8(MySQL 兼容模式)→ 只执行【第一部分】
|
||||||
|
-- * 若目标库是 PostgreSQL → 只执行【第二部分】
|
||||||
|
-- * 执行前建议备份;执行后核对【第三部分】的验证 SQL
|
||||||
|
-- * 可重复执行(幂等):表已存在时再执行会报"已存在"错误,属正常,
|
||||||
|
-- 说明该脚本已执行过,无需处理
|
||||||
|
-- =====================================================================
|
||||||
|
|
||||||
|
|
||||||
|
-- =====================================================================
|
||||||
|
-- 第一部分:达梦 DM8 / MySQL
|
||||||
|
-- =====================================================================
|
||||||
|
|
||||||
|
CREATE TABLE x_upload_config (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
org_id BIGINT DEFAULT NULL COMMENT '机构id',
|
||||||
|
upload_enabled BIT(1) DEFAULT b'1' COMMENT '是否允许上传',
|
||||||
|
allow_all_type BIT(1) DEFAULT b'1' COMMENT '是否允许全部类型(false时按allowed_suffixes白名单校验)',
|
||||||
|
allowed_suffixes VARCHAR(1024) DEFAULT NULL COMMENT '允许的文件后缀(逗号分隔,小写),allow_all_type=false时生效',
|
||||||
|
max_file_size BIGINT DEFAULT NULL COMMENT '单文件大小上限(MB),空或0不限制',
|
||||||
|
deleted BIT(1) DEFAULT b'0' COMMENT '是否删除',
|
||||||
|
create_by BIGINT DEFAULT NULL COMMENT '创建人',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
update_by BIGINT DEFAULT NULL COMMENT '修改人',
|
||||||
|
update_time DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
|
||||||
|
proxy_by BIGINT DEFAULT NULL COMMENT '代理人',
|
||||||
|
PRIMARY KEY (id) USING BTREE,
|
||||||
|
UNIQUE KEY uk_org_id (org_id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='机构上传配置';
|
||||||
|
|
||||||
|
-- 说明:不需要初始化数据。
|
||||||
|
-- 未配置的机构 = 默认策略(允许上传全部类型、不限大小),
|
||||||
|
-- 由各机构管理员在管理后台"上传配置"页面自行配置后落库。
|
||||||
|
|
||||||
|
|
||||||
|
-- =====================================================================
|
||||||
|
-- 第二部分:PostgreSQL
|
||||||
|
-- =====================================================================
|
||||||
|
|
||||||
|
-- CREATE TABLE x_upload_config (
|
||||||
|
-- id BIGSERIAL PRIMARY KEY,
|
||||||
|
-- org_id BIGINT DEFAULT NULL,
|
||||||
|
-- upload_enabled BOOLEAN DEFAULT TRUE,
|
||||||
|
-- allow_all_type BOOLEAN DEFAULT TRUE,
|
||||||
|
-- allowed_suffixes VARCHAR(1024) DEFAULT NULL,
|
||||||
|
-- max_file_size BIGINT DEFAULT NULL,
|
||||||
|
-- deleted BOOLEAN DEFAULT FALSE,
|
||||||
|
-- create_by BIGINT DEFAULT NULL,
|
||||||
|
-- create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
-- update_by BIGINT DEFAULT NULL,
|
||||||
|
-- update_time TIMESTAMP DEFAULT NULL,
|
||||||
|
-- proxy_by BIGINT DEFAULT NULL,
|
||||||
|
-- CONSTRAINT uk_upload_config_org_id UNIQUE (org_id)
|
||||||
|
-- );
|
||||||
|
-- COMMENT ON TABLE x_upload_config IS '机构上传配置';
|
||||||
|
-- COMMENT ON COLUMN x_upload_config.id IS '主键ID';
|
||||||
|
-- COMMENT ON COLUMN x_upload_config.org_id IS '机构id';
|
||||||
|
-- COMMENT ON COLUMN x_upload_config.upload_enabled IS '是否允许上传';
|
||||||
|
-- COMMENT ON COLUMN x_upload_config.allow_all_type IS '是否允许全部类型(false时按allowed_suffixes白名单校验)';
|
||||||
|
-- COMMENT ON COLUMN x_upload_config.allowed_suffixes IS '允许的文件后缀(逗号分隔,小写),allow_all_type=false时生效';
|
||||||
|
-- COMMENT ON COLUMN x_upload_config.max_file_size IS '单文件大小上限(MB),空或0不限制';
|
||||||
|
-- COMMENT ON COLUMN x_upload_config.deleted IS '是否删除';
|
||||||
|
-- COMMENT ON COLUMN x_upload_config.create_by IS '创建人';
|
||||||
|
-- COMMENT ON COLUMN x_upload_config.create_time IS '创建时间';
|
||||||
|
-- COMMENT ON COLUMN x_upload_config.update_by IS '修改人';
|
||||||
|
-- COMMENT ON COLUMN x_upload_config.update_time IS '修改时间';
|
||||||
|
-- COMMENT ON COLUMN x_upload_config.proxy_by IS '代理人';
|
||||||
|
|
||||||
|
|
||||||
|
-- =====================================================================
|
||||||
|
-- 第三部分:执行后验证(达梦/MySQL 语法)
|
||||||
|
-- =====================================================================
|
||||||
|
|
||||||
|
-- 1) 表存在且为空(0 行正常)
|
||||||
|
-- SELECT COUNT(*) FROM x_upload_config;
|
||||||
|
|
||||||
|
-- 2) 唯一约束存在(应返回 uk_org_id)
|
||||||
|
-- SELECT INDEX_NAME FROM USER_INDEXES WHERE TABLE_NAME = 'X_UPLOAD_CONFIG';
|
||||||
|
|
||||||
|
-- 3) (可选)回滚脚本 —— 如需回滚上线:
|
||||||
|
-- DROP TABLE x_upload_config;
|
||||||
96
docker-test/application.yml
Normal file
96
docker-test/application.yml
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
|
||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: s-file
|
||||||
|
datasource:
|
||||||
|
url: jdbc:postgresql://sfile-pg:5432/s_file
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
|
username: postgres
|
||||||
|
password: sfile123
|
||||||
|
druid:
|
||||||
|
initial-size: 2
|
||||||
|
min-idle: 2
|
||||||
|
max-active: 10
|
||||||
|
validation-query: SELECT 1
|
||||||
|
redis:
|
||||||
|
host: sfile-redis
|
||||||
|
port: 6379
|
||||||
|
timeout: 5000
|
||||||
|
lettuce:
|
||||||
|
pool:
|
||||||
|
max-active: 8
|
||||||
|
max-idle: 4
|
||||||
|
rabbitmq:
|
||||||
|
host: sfile-mq
|
||||||
|
port: 5672
|
||||||
|
username: guest
|
||||||
|
password: guest
|
||||||
|
servlet:
|
||||||
|
multipart:
|
||||||
|
max-file-size: 500MB
|
||||||
|
max-request-size: 500MB
|
||||||
|
main:
|
||||||
|
allow-bean-definition-overriding: true
|
||||||
|
|
||||||
|
mybatis-plus:
|
||||||
|
mapper-locations: classpath*:com/senmesh/**/mapper/*.xml
|
||||||
|
|
||||||
|
# 平台编码(init-data 中 uc_platform.platform_code=1)
|
||||||
|
system:
|
||||||
|
code: 1
|
||||||
|
|
||||||
|
# 发号器雪花 workerId(键格式: code.snowflake.workid.<容器IP>)
|
||||||
|
"[code.snowflake.workid.172.20.0.10]": 1
|
||||||
|
|
||||||
|
security:
|
||||||
|
jwt:
|
||||||
|
enable: true
|
||||||
|
secret: sfile-functional-test-secret-key-please-change-0123456789abcdef
|
||||||
|
expiration: 86400000
|
||||||
|
header: Authorization
|
||||||
|
ignoring: /error,/favicon.ico
|
||||||
|
login-page: /login
|
||||||
|
superior-only: false
|
||||||
|
use-local: true
|
||||||
|
allow-if-all-abstain-decisions: true
|
||||||
|
oauth2:
|
||||||
|
enable: false
|
||||||
|
not-logs: ""
|
||||||
|
|
||||||
|
# 新用户初始密码
|
||||||
|
user:
|
||||||
|
default:
|
||||||
|
password: Qaz123567!@#
|
||||||
|
|
||||||
|
file:
|
||||||
|
upload-dir: /data/upload
|
||||||
|
storage:
|
||||||
|
type: local
|
||||||
|
transfer:
|
||||||
|
# 内网侧
|
||||||
|
location: "1"
|
||||||
|
send-dir: /data/transfer/send
|
||||||
|
receive-dir: /data/transfer/receive
|
||||||
|
unpack-risk-check: false
|
||||||
|
|
||||||
|
dify:
|
||||||
|
risk:
|
||||||
|
rule:
|
||||||
|
api-key: test-rule-key
|
||||||
|
scan:
|
||||||
|
api-key: test-scan-key
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
custom:
|
||||||
|
topic-exchange-name: s-file-test-topic
|
||||||
|
delayed-exchange-name: s-file-test-delayed
|
||||||
|
broadcast-id: s-file-test-broadcast
|
||||||
|
|
||||||
|
swagger:
|
||||||
|
enable: false
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
com.senmesh: info
|
||||||
181
docs/接口文档-机构上传配置.md
Normal file
181
docs/接口文档-机构上传配置.md
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
# 前端接口文档 —— 机构上传配置(文件上传类型/大小限制)
|
||||||
|
|
||||||
|
> 版本:v1.0(2026-09-14)
|
||||||
|
> 适用后端:s-file(alita-bjy-uygur)
|
||||||
|
> Swagger 分组:「机构上传配置」
|
||||||
|
|
||||||
|
## 功能说明
|
||||||
|
|
||||||
|
机构管理员可以为本机构配置网盘上传策略:
|
||||||
|
|
||||||
|
- **上传总开关**:关闭后本机构用户不能上传任何文件到网盘;
|
||||||
|
- **类型白名单**:可限制只允许上传指定后缀的文件(如 `doc、docx、pdf、jpg`);
|
||||||
|
- **单文件大小上限**:单位 MB,超过即拒绝。
|
||||||
|
|
||||||
|
规则约定:
|
||||||
|
|
||||||
|
- 配置**按机构隔离**,作用于当前登录机构,管理的是本机构用户的**网盘上传**(`normal_user_file/create` 落库时强制校验);
|
||||||
|
- **未配置过的机构 = 默认策略**:允许上传、允许全部类型、不限大小(对现有用户完全兼容);
|
||||||
|
- 个人用户(无登录机构)不受机构配置限制;
|
||||||
|
- 后缀不区分大小写,配置时可带可不带前导点(`PDF`、`.pdf`、`pdf` 等价),服务端统一转小写存储;
|
||||||
|
- 文件夹创建不受类型/大小校验影响;
|
||||||
|
- 文件**传输(发送/接收)与分享不受此配置影响**,该配置只管上传。
|
||||||
|
|
||||||
|
## 通用约定
|
||||||
|
|
||||||
|
与既有接口一致:登录态调用;统一返回 `AlitaResult<T>`(`code="200"` 且 `success=true` 成功;业务失败 `success=false`,`msg` 为提示文案);时间格式 `yyyy-MM-dd HH:mm:ss`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 查询当前机构的上传配置
|
||||||
|
|
||||||
|
- **URL**:`GET /upload_config/info`
|
||||||
|
- **权限**:所有登录用户(前端可在上传前读取并据此过滤可选文件类型)
|
||||||
|
- **参数**:无(按当前登录机构查询)
|
||||||
|
- **返回**:`AlitaResult<UploadConfigDTO>`
|
||||||
|
|
||||||
|
**UploadConfigDTO 字段**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| orgId | Long | 机构 id |
|
||||||
|
| uploadEnabled | Boolean | 是否允许上传 |
|
||||||
|
| allowAllType | Boolean | 是否允许全部类型 |
|
||||||
|
| allowedSuffixes | List<String> | 允许的后缀(小写、不含点),`allowAllType=false` 时有值,否则为 null |
|
||||||
|
| maxFileSize | Long | 单文件大小上限(MB),null 或 0 表示不限制 |
|
||||||
|
| configured | Boolean | **机构是否已配置过**。`false` 表示当前是默认策略(允许上传全部类型、不限大小),前端此时可不限制文件选择器 |
|
||||||
|
|
||||||
|
**返回示例(已配置)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "200",
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"orgId": 12,
|
||||||
|
"uploadEnabled": true,
|
||||||
|
"allowAllType": false,
|
||||||
|
"allowedSuffixes": ["doc", "docx", "pdf", "jpg", "png"],
|
||||||
|
"maxFileSize": 200,
|
||||||
|
"configured": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回示例(未配置过)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "200",
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"orgId": 12,
|
||||||
|
"uploadEnabled": true,
|
||||||
|
"allowAllType": true,
|
||||||
|
"allowedSuffixes": null,
|
||||||
|
"maxFileSize": null,
|
||||||
|
"configured": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 保存机构的上传配置
|
||||||
|
|
||||||
|
Upsert:该机构无配置则新建,有则更新。
|
||||||
|
|
||||||
|
- **URL**:`POST /upload_config/modify`
|
||||||
|
- **权限**:**机构管理员** 或 平台超管(其他用户返回「当前用户无操作权限」)
|
||||||
|
- **Body**(application/json)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| uploadEnabled | Boolean | 是 | 是否允许上传 |
|
||||||
|
| allowAllType | Boolean | 是 | 是否允许全部类型 |
|
||||||
|
| allowedSuffixes | List<String> | 条件必填 | `allowAllType=false` 时必填且不能为空;后缀可带可不带点,自动小写去重 |
|
||||||
|
| maxFileSize | Long | 否 | 单文件上限(MB),null 或 0 不限制;传负数报错 |
|
||||||
|
|
||||||
|
**校验失败提示**
|
||||||
|
|
||||||
|
| 场景 | msg |
|
||||||
|
|---|---|
|
||||||
|
| 非机构管理员/超管 | 当前用户无操作权限 |
|
||||||
|
| 白名单为空 | 未允许全部类型时,允许的文件后缀不能为空 |
|
||||||
|
| maxFileSize ≤ 0 | 单文件大小上限必须大于0 |
|
||||||
|
| 并发修改 | 上传配置正在修改,请稍后重试 |
|
||||||
|
|
||||||
|
**请求示例(只允许文档和图片,单个最大 200MB)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
POST /upload_config/modify
|
||||||
|
{
|
||||||
|
"uploadEnabled": true,
|
||||||
|
"allowAllType": false,
|
||||||
|
"allowedSuffixes": ["doc", "docx", "xls", "xlsx", "pdf", "jpg", "png"],
|
||||||
|
"maxFileSize": 200
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求示例(禁止本机构上传)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
POST /upload_config/modify
|
||||||
|
{
|
||||||
|
"uploadEnabled": false,
|
||||||
|
"allowAllType": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "code": "200", "success": true, "data": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 上传预检(可选,提升体验)
|
||||||
|
|
||||||
|
按文件名/大小对当前机构配置做一次预检,用于**选择文件后、真正上传前**给出友好提示,避免白传一遍物理文件才被拒绝。
|
||||||
|
|
||||||
|
- **URL**:`GET /upload_config/check`
|
||||||
|
- **权限**:所有登录用户
|
||||||
|
- **参数**(URL query)
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| fileName | String | 是 | 完整文件名(含后缀) |
|
||||||
|
| fileSize | Long | 否 | 文件大小(字节),不传则跳过大小校验 |
|
||||||
|
|
||||||
|
- **返回**:通过时 `{"code":"200","success":true,"data":true}`;不通过时 `success=false`,`msg` 说明原因(与真实上传被拦截时的文案一致)。
|
||||||
|
|
||||||
|
**不通过返回示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "700",
|
||||||
|
"success": false,
|
||||||
|
"msg": "仅允许上传后缀为 doc、docx、pdf、jpg、png 的文件",
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 上传被拦截时的服务端行为
|
||||||
|
|
||||||
|
用户上传最终在网盘创建记录接口(`POST /normal_user_file/create`)被强制校验,不通过时接口返回 `success=false`,`msg` 为以下之一:
|
||||||
|
|
||||||
|
| 场景 | msg |
|
||||||
|
|---|---|
|
||||||
|
| 机构关闭上传 | 当前机构已禁止上传文件,请联系机构管理员 |
|
||||||
|
| 后缀不在白名单 | 仅允许上传后缀为 doc、docx、pdf 的文件(动态列出) |
|
||||||
|
| 机构白名单为空(异常配置) | 当前机构未配置可上传的文件类型,请联系机构管理员 |
|
||||||
|
| 超过大小上限 | 上传文件过大,单文件最大不得超过200MB(动态数值) |
|
||||||
|
|
||||||
|
**前端建议**:
|
||||||
|
|
||||||
|
1. 上传页加载时调 `GET /upload_config/info`:`uploadEnabled=false` 时直接隐藏/禁用上传入口;`allowAllType=false` 时设置文件选择器的 accept 过滤;
|
||||||
|
2. 选择文件后可先调 `GET /upload_config/check` 预检(可选);
|
||||||
|
3. 真实上传两步(`/file/upload` → `/normal_user_file/create`)中第二步失败时直接 toast `msg`。
|
||||||
407
docs/接口文档-用户管理与记录查询.md
Normal file
407
docs/接口文档-用户管理与记录查询.md
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
# 前端接口文档 —— 用户管理增强 & 机构账号查询 & 按手机号查询文件记录
|
||||||
|
|
||||||
|
> 版本:v1.0(2026-09-14)
|
||||||
|
> 适用后端:s-file(alita-bjy-uygur)
|
||||||
|
> Swagger 分组:「用户」「用户文件」
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 通用约定
|
||||||
|
|
||||||
|
### 0.1 请求与认证
|
||||||
|
|
||||||
|
- 所有接口均需**登录后调用**,认证 token 按现有方式携带(header 或 cookie,与系统其他接口一致)。
|
||||||
|
- 新增接口均要求**平台管理员(超管)身份**(`uc_user.supeiror = true`),非管理员调用统一返回失败,提示「当前用户无操作权限」。
|
||||||
|
|
||||||
|
### 0.2 统一返回结构 `AlitaResult<T>`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"requestId": "xxx",
|
||||||
|
"code": "200",
|
||||||
|
"success": true,
|
||||||
|
"msg": null,
|
||||||
|
"data": { ... }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 字段 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| code | `"200"` 成功;业务失败时 success=false,msg 为错误提示(如「不能禁用当前登录账号」) |
|
||||||
|
| success | 是否成功 |
|
||||||
|
| data | 业务数据,失败时为 null |
|
||||||
|
|
||||||
|
### 0.3 分页请求与返回
|
||||||
|
|
||||||
|
分页查询接口通过 **URL query 参数** 传递分页信息,返回结构 `Page<T>`:
|
||||||
|
|
||||||
|
```
|
||||||
|
pageNo 第几页,默认 1
|
||||||
|
pageSize 页宽,默认 20,上限 1000
|
||||||
|
needPage 是否分页,默认 true(无需传)
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pageNo": 1,
|
||||||
|
"pageSize": 20,
|
||||||
|
"pageCount": 5,
|
||||||
|
"totalCount": 88,
|
||||||
|
"entityList": [ ... ]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 0.4 通用枚举(前后端共用)
|
||||||
|
|
||||||
|
**用户状态 `status`**
|
||||||
|
|
||||||
|
| 值 | 含义 |
|
||||||
|
|---|---|
|
||||||
|
| 1 | 待激活 |
|
||||||
|
| 2 | 已激活 |
|
||||||
|
| 3 | 已注销 |
|
||||||
|
|
||||||
|
**是否启用 `enabled`**:`true` 启用,`false` 禁用。
|
||||||
|
|
||||||
|
**文件状态 `fileStatus`**
|
||||||
|
|
||||||
|
| 值 | 含义 |
|
||||||
|
|---|---|
|
||||||
|
| 1 | 正常 |
|
||||||
|
| 2 | 失效 |
|
||||||
|
| 11 | 解析中 |
|
||||||
|
| 12 | 待审核 |
|
||||||
|
| 13 | 传输中 |
|
||||||
|
| 14 | 失败 |
|
||||||
|
|
||||||
|
**记录类型 `recordType`**(本文档 3.4 节专用)
|
||||||
|
|
||||||
|
| 值 | 含义 |
|
||||||
|
|---|---|
|
||||||
|
| 1 | 上传记录 |
|
||||||
|
| 2 | 发送记录(本端发起文件传输) |
|
||||||
|
| 3 | 接收记录(本端接收文件传输) |
|
||||||
|
|
||||||
|
> 发送/接收与部署侧有关,但**前端无需关心**:传入 `recordType` 后,后端按部署配置(内网/外网)自动映射到对应的 `root_type`;返回列表中每条记录也已回填 `recordType` / `recordTypeDesc`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 用户管理:启用 / 禁用 / 注销
|
||||||
|
|
||||||
|
### 1.1 启用用户
|
||||||
|
|
||||||
|
将被禁用的账号重新启用(幂等:已启用的账号重复调用仍返回成功)。
|
||||||
|
|
||||||
|
- **URL**:`POST /user/enable`
|
||||||
|
- **权限**:平台管理员
|
||||||
|
- **Body**(application/json)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| userId | Long | 是 | 目标用户 id |
|
||||||
|
|
||||||
|
- **返回**:`AlitaResult<Boolean>`,data 为 `true`
|
||||||
|
- **失败场景**:用户不存在;账号已注销(提示「已注销账号不可启用」);非管理员
|
||||||
|
|
||||||
|
**请求示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
POST /user/enable
|
||||||
|
{ "userId": 10086 }
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "code": "200", "success": true, "data": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 禁用用户
|
||||||
|
|
||||||
|
禁用账号,禁用后该用户无法登录/使用系统(后端登录态校验会实时读取启用状态,被禁用用户下次请求即被拒绝)。幂等:已禁用的账号重复调用仍返回成功。
|
||||||
|
|
||||||
|
- **URL**:`POST /user/disable`
|
||||||
|
- **权限**:平台管理员
|
||||||
|
- **Body**(application/json)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| userId | Long | 是 | 目标用户 id |
|
||||||
|
|
||||||
|
- **返回**:`AlitaResult<Boolean>`,data 为 `true`
|
||||||
|
- **失败场景**:
|
||||||
|
- 用户不存在
|
||||||
|
- `userId` 为当前登录人(提示「不能禁用当前登录账号」)
|
||||||
|
- 目标是超级管理员(提示「不能禁用超级管理员账号」)
|
||||||
|
- 目标已注销(提示「已注销账号无需禁用」)
|
||||||
|
|
||||||
|
**请求示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
POST /user/disable
|
||||||
|
{ "userId": 10086 }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 注销用户(管理员直接注销)
|
||||||
|
|
||||||
|
管理员将账号直接置为**已注销**(status=3)并同步禁用(enabled=false),**不进入注销申请审批流程**。注销后账号不可恢复、不可登录。幂等:已注销账号重复调用返回成功。
|
||||||
|
|
||||||
|
- **URL**:`POST /user/admin_cancel`
|
||||||
|
- **权限**:平台管理员
|
||||||
|
- **Body**(application/json)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| userId | Long | 是 | 目标用户 id |
|
||||||
|
|
||||||
|
- **返回**:`AlitaResult<Void>`,data 为 null
|
||||||
|
- **失败场景**:
|
||||||
|
- 用户不存在
|
||||||
|
- `userId` 为当前登录人(提示「不能注销当前登录账号」)
|
||||||
|
- 目标是超级管理员(提示「不能注销超级管理员账号」)
|
||||||
|
- 并发重复操作(提示「该用户正在注销,请勿重复操作」)
|
||||||
|
|
||||||
|
**请求示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
POST /user/admin_cancel
|
||||||
|
{ "userId": 10086 }
|
||||||
|
```
|
||||||
|
|
||||||
|
> **与既有接口的区别**:既有 `POST /user/cancel` 为机构内注销(限定当前登录机构、且要求目标账号启用中),供原「注销申请/同意」流程使用,保持不变;本接口供系统管理员在用户管理页面跨机构直接注销。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 系统管理员:查看各机构下的账号信息
|
||||||
|
|
||||||
|
### 2.1 分页查询机构账号
|
||||||
|
|
||||||
|
按机构维度查看账号信息。一行 = 一个机构下的一个账号(同一账号属于多个机构时会出现多行)。不传 `orgId` 时查询全部机构,传 `orgId` 时查看指定机构。
|
||||||
|
|
||||||
|
- **URL**:`GET /user/page/org_account`
|
||||||
|
- **权限**:平台管理员
|
||||||
|
- **参数**(URL query)
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| orgId | Long | 否 | 机构 id,不传查全部机构 |
|
||||||
|
| orgNameLike | String | 否 | 机构名称,模糊匹配 |
|
||||||
|
| usernameLike | String | 否 | 账号,模糊匹配 |
|
||||||
|
| realNameLike | String | 否 | 真实姓名,模糊匹配 |
|
||||||
|
| phoneLike | String | 否 | 手机号,模糊匹配 |
|
||||||
|
| enabled | Boolean | 否 | 是否启用:true/false |
|
||||||
|
| status | Integer | 否 | 用户状态:1 待激活 / 2 已激活 / 3 已注销 |
|
||||||
|
| createTimeStart | String | 否 | 创建时间起,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||||
|
| createTimeEnd | String | 否 | 创建时间止,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||||
|
| pageNo / pageSize | Integer | 否 | 分页参数,默认 1 / 20 |
|
||||||
|
|
||||||
|
- **返回**:`AlitaResult<Page<OrgAccountDTO>>`
|
||||||
|
|
||||||
|
**OrgAccountDTO 字段**
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| orgId | Long | 机构 id |
|
||||||
|
| orgName | String | 机构名称 |
|
||||||
|
| administrator | Boolean | 是否该机构的管理员 |
|
||||||
|
| userId | Long | 用户 id(可传给 1.1/1.2/1.3 接口做启停/注销操作) |
|
||||||
|
| username | String | 账号 |
|
||||||
|
| nickname | String | 昵称 |
|
||||||
|
| realName | String | 真实姓名 |
|
||||||
|
| phone | String | 手机号 |
|
||||||
|
| email | String | 邮箱 |
|
||||||
|
| userType | Integer | 用户类型:1 企业用户 / 2 个人用户 |
|
||||||
|
| enabled | Boolean | 是否启用 |
|
||||||
|
| status | Integer | 状态:1 待激活 / 2 已激活 / 3 已注销 |
|
||||||
|
| createTime | String | 创建时间 `yyyy-MM-dd HH:mm:ss` |
|
||||||
|
|
||||||
|
- **排序规则**:机构 id 升序,机构管理员排前面,同机构内按用户 id 倒序。
|
||||||
|
|
||||||
|
**请求示例**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /user/page/org_account?orgNameLike=教育&phoneLike=138&enabled=false&pageNo=1&pageSize=20
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "200",
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"pageNo": 1,
|
||||||
|
"pageSize": 20,
|
||||||
|
"pageCount": 1,
|
||||||
|
"totalCount": 2,
|
||||||
|
"entityList": [
|
||||||
|
{
|
||||||
|
"orgId": 12,
|
||||||
|
"orgName": "XX市教育局",
|
||||||
|
"administrator": true,
|
||||||
|
"userId": 1001,
|
||||||
|
"username": "admin_xx",
|
||||||
|
"nickname": "张三",
|
||||||
|
"realName": "张三",
|
||||||
|
"phone": "13800000001",
|
||||||
|
"email": "zhang@xx.gov.cn",
|
||||||
|
"userType": 1,
|
||||||
|
"enabled": false,
|
||||||
|
"status": 2,
|
||||||
|
"createTime": "2026-08-01 10:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"orgId": 12,
|
||||||
|
"orgName": "XX市教育局",
|
||||||
|
"administrator": false,
|
||||||
|
"userId": 1002,
|
||||||
|
"username": "user_xx01",
|
||||||
|
"nickname": "李四",
|
||||||
|
"realName": "李四",
|
||||||
|
"phone": "13800000002",
|
||||||
|
"email": null,
|
||||||
|
"userType": 1,
|
||||||
|
"enabled": true,
|
||||||
|
"status": 2,
|
||||||
|
"createTime": "2026-08-03 14:30:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 按手机号查询文件记录(上传 / 发送 / 接收)
|
||||||
|
|
||||||
|
### 3.1 分页查询
|
||||||
|
|
||||||
|
输入手机号,精确匹配该手机号名下(可能是多个账号)的全部文件记录,可按记录类型筛选。
|
||||||
|
|
||||||
|
- **URL**:`GET /user_file/record/page_by_phone`
|
||||||
|
- **权限**:平台管理员
|
||||||
|
- **参数**(URL query)
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| phone | String | **是** | 用户手机号,精确匹配,同一手机号关联的所有账号均会命中 |
|
||||||
|
| recordType | Integer | 否 | 1 上传记录 / 2 发送记录 / 3 接收记录,**不传查全部三类** |
|
||||||
|
| fileNameLike | String | 否 | 文件名,模糊匹配 |
|
||||||
|
| fileType | String | 否 | 文件类型:`1` 文件 / `2` 文件夹 |
|
||||||
|
| fileStatus | String | 否 | 文件状态:1 正常 / 2 失效 / 11 解析中 / 12 待审核 / 13 传输中 / 14 失败 |
|
||||||
|
| createTimeBegin | String | 否 | 记录时间起,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||||
|
| createTimeEnd | String | 否 | 记录时间止,格式 `yyyy-MM-dd HH:mm:ss` |
|
||||||
|
| pageNo / pageSize | Integer | 否 | 分页参数,默认 1 / 20 |
|
||||||
|
|
||||||
|
- **返回**:`AlitaResult<Page<UserFileRecordDTO>>`
|
||||||
|
|
||||||
|
**UserFileRecordDTO 字段**(继承用户文件全部字段,以下为主要字段)
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| id | Long | 记录 id |
|
||||||
|
| phone | String | 归属手机号 |
|
||||||
|
| username | String | 归属账号 |
|
||||||
|
| realName | String | 归属真实姓名 |
|
||||||
|
| userType | Integer | 归属用户类型:1 企业用户 / 2 个人用户 |
|
||||||
|
| recordType | Integer | 记录类型:1 上传 / 2 发送 / 3 接收 |
|
||||||
|
| recordTypeDesc | String | 记录类型描述(「上传记录」「发送记录」「接收记录」) |
|
||||||
|
| userId | Long | 归属用户 id |
|
||||||
|
| rootType | String | 原始根类型:1 普通 / 2 内穿外 / 3 外传内(调试用,前端一般用 recordType 即可) |
|
||||||
|
| fileType | String | 文件大类:1 文件 / 2 文件夹 |
|
||||||
|
| fileName | String | 文件名 |
|
||||||
|
| fileCode | String | 物理文件编码 |
|
||||||
|
| fileSize | Long | 文件大小(字节) |
|
||||||
|
| fileStatus | String | 文件状态:1 正常 / 2 失效 / 11 解析中 / 12 待审核 / 13 传输中 / 14 失败 |
|
||||||
|
| createTime | String | 记录时间(上传/发送/接收发起时间)`yyyy-MM-dd HH:mm:ss` |
|
||||||
|
| endTime | String | 有效期(分享类记录使用),可能为 null |
|
||||||
|
| fileCategory | String | 文件种类(文档/图片/视频等,按后缀归类) |
|
||||||
|
| maxRiskLevel | String | 最高风险等级:1 重度 / 2 中度 / 3 轻度,可能为 null |
|
||||||
|
|
||||||
|
- **排序**:记录 id 倒序(即时间倒序)。
|
||||||
|
- **失败场景**:phone 为空;recordType 非法(提示「记录类型不合法,仅支持1上传/2发送/3接收」);非管理员。
|
||||||
|
|
||||||
|
**请求示例**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /user_file/record/page_by_phone?phone=13800000001&recordType=2&pageNo=1&pageSize=10
|
||||||
|
```
|
||||||
|
|
||||||
|
**返回示例**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "200",
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"pageNo": 1,
|
||||||
|
"pageSize": 10,
|
||||||
|
"pageCount": 1,
|
||||||
|
"totalCount": 2,
|
||||||
|
"entityList": [
|
||||||
|
{
|
||||||
|
"id": 9001,
|
||||||
|
"phone": "13800000001",
|
||||||
|
"username": "user_xx01",
|
||||||
|
"realName": "李四",
|
||||||
|
"userType": 1,
|
||||||
|
"recordType": 2,
|
||||||
|
"recordTypeDesc": "发送记录",
|
||||||
|
"userId": 1002,
|
||||||
|
"rootType": "2",
|
||||||
|
"fileType": "2",
|
||||||
|
"fileName": "2026年预算材料_20260912103000",
|
||||||
|
"fileCode": null,
|
||||||
|
"fileSize": null,
|
||||||
|
"fileStatus": "1",
|
||||||
|
"createTime": "2026-09-12 10:30:00",
|
||||||
|
"endTime": null,
|
||||||
|
"fileCategory": null,
|
||||||
|
"maxRiskLevel": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8802,
|
||||||
|
"phone": "13800000001",
|
||||||
|
"username": "user_xx01",
|
||||||
|
"realName": "李四",
|
||||||
|
"userType": 1,
|
||||||
|
"recordType": 1,
|
||||||
|
"recordTypeDesc": "上传记录",
|
||||||
|
"userId": 1002,
|
||||||
|
"rootType": "1",
|
||||||
|
"fileType": "1",
|
||||||
|
"fileName": "工作总结.docx",
|
||||||
|
"fileCode": "fc_3a7d92",
|
||||||
|
"fileSize": 1048576,
|
||||||
|
"fileStatus": "1",
|
||||||
|
"createTime": "2026-09-10 09:12:00",
|
||||||
|
"endTime": null,
|
||||||
|
"fileCategory": "文档",
|
||||||
|
"maxRiskLevel": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **记录类型口径说明**(便于前端理解,无需处理):
|
||||||
|
> - 上传记录:用户在网盘中上传的文件/文件夹(rootType=1)。
|
||||||
|
> - 发送记录:用户发起的文件传输(文件夹整体外发,传输中状态为 13,完成后变 1,失败为 14)。
|
||||||
|
> - 接收记录:对端传来的文件在本端解包后归属到该手机号用户的记录(若开启风险审查,先为 12 待审核,通过后为 1)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 错误处理建议
|
||||||
|
|
||||||
|
前端统一按 `success=false` 判定失败并 toast `msg`;特殊 code 沿用系统既有约定(`401` 未登录等,与现有接口一致)。
|
||||||
|
|
||||||
|
涉及本批接口的典型业务错误文案:
|
||||||
|
|
||||||
|
| 场景 | msg |
|
||||||
|
|---|---|
|
||||||
|
| 非平台管理员操作 | 当前用户无操作权限 |
|
||||||
|
| 启用已注销账号 | 已注销账号不可启用 |
|
||||||
|
| 禁用/注销自己 | 不能禁用当前登录账号 / 不能注销当前登录账号 |
|
||||||
|
| 禁用/注销超管 | 不能禁用超级管理员账号 / 不能注销超级管理员账号 |
|
||||||
|
| recordType 非法 | 记录类型不合法,仅支持1上传/2发送/3接收 |
|
||||||
|
| phone 为空 | 手机号不能为空 |
|
||||||
220
docs/生产部署说明.md
Normal file
220
docs/生产部署说明.md
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
# s-file 生产部署 / 上线说明
|
||||||
|
|
||||||
|
> 适用版本:2026-09-14(含用户启停/注销、机构账号查询、手机号查记录、机构上传配置、禁用踢下线黑名单)
|
||||||
|
> 构建产物:`target/s-file.jar`(Spring Boot 可执行 fat jar,需 JDK 8)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 上线前必须处理(阻断项)
|
||||||
|
|
||||||
|
### 0.1 替换 spire.pdf 占位 jar ⚠️
|
||||||
|
|
||||||
|
本机构开发机的 `~/.m2/repository/e-iceblue/spire.pdf/3.7.4/` 中目前安装的是**编译级 stub(空实现)jar**——它能保证编译打包通过,但 **PDF 盖章功能(`PdfUtils.pdfStampPerPage`)运行时必然失败**。
|
||||||
|
|
||||||
|
上线前必须:
|
||||||
|
|
||||||
|
1. 从公司私服获取真实的 `spire.pdf-3.7.4.jar`(阿里云与 e-iceblue 官方仓库均无此版本);
|
||||||
|
2. 覆盖 `~/.m2/repository/e-iceblue/spire.pdf/3.7.4/spire.pdf-3.7.4.jar`(pom 同步覆盖);
|
||||||
|
3. 重新 `mvn clean package -DskipTests`;
|
||||||
|
4. 验证:解包 jar 中 `com/spire/pdf/PdfDocument.class` 应为真实实现(stub 版仅 300~500 字节)。
|
||||||
|
|
||||||
|
> 若该业务暂时不用 PDF 盖章,可带 stub 上线,但需明确告知团队。
|
||||||
|
|
||||||
|
### 0.2 提供生产配置文件
|
||||||
|
|
||||||
|
**仓库内不含任何运行配置**(application.yml 是外置的)。启动时通过 `spring.config.additional-location` 挂载。生产模板如下(按实际环境修改):
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
|
||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: s-file
|
||||||
|
datasource:
|
||||||
|
# 生产为达梦 DM8;驱动 com.dameng.jdbc.driver(pom 已含依赖)
|
||||||
|
url: jdbc:dm://<DB_HOST>:5236/S_FILE
|
||||||
|
driver-class-name: com.dameng.jdbc.driver
|
||||||
|
username: <DB_USER>
|
||||||
|
password: <DB_PASSWORD>
|
||||||
|
druid:
|
||||||
|
initial-size: 5
|
||||||
|
min-idle: 5
|
||||||
|
max-active: 20
|
||||||
|
validation-query: SELECT 1
|
||||||
|
redis: # 必须可用:登录缓存 + 禁用/注销踢下线黑名单依赖 Redis
|
||||||
|
host: <REDIS_HOST>
|
||||||
|
port: 6379
|
||||||
|
timeout: 5000
|
||||||
|
rabbitmq:
|
||||||
|
host: <MQ_HOST>
|
||||||
|
port: 5672
|
||||||
|
username: <MQ_USER>
|
||||||
|
password: <MQ_PASSWORD>
|
||||||
|
servlet:
|
||||||
|
multipart:
|
||||||
|
max-file-size: 500MB # 按网盘需求调整
|
||||||
|
max-request-size: 500MB
|
||||||
|
main:
|
||||||
|
allow-bean-definition-overriding: true
|
||||||
|
|
||||||
|
mybatis-plus:
|
||||||
|
mapper-locations: classpath*:com/senmesh/**/mapper/*.xml # 必须有,否则自定义 XML 的接口全部报 Invalid bound statement
|
||||||
|
|
||||||
|
system:
|
||||||
|
code: 1
|
||||||
|
|
||||||
|
# 发号器雪花 workerId:键格式 code.snowflake.workid.<本机IP>
|
||||||
|
# 单机部署:直接写死本机 IP 的键;多机部署:每台机器一个键、值必须不同且 0~31
|
||||||
|
"[code.snowflake.workid.192.168.1.10]": 1
|
||||||
|
|
||||||
|
security:
|
||||||
|
jwt:
|
||||||
|
enable: true
|
||||||
|
secret: <上线前必须换成 64 字节以上随机串,勿用文档示例>
|
||||||
|
expiration: 86400 # token 有效期(秒)
|
||||||
|
header: Authorization
|
||||||
|
ignoring: /error,/favicon.ico,/v2/api-docs,/swagger-resources/**
|
||||||
|
login-page: /login
|
||||||
|
superior-only: false # true=仅平台机构用户可登录系统;按部署侧实际选择
|
||||||
|
use-local: true
|
||||||
|
allow-if-all-abstain-decisions: true
|
||||||
|
oauth2:
|
||||||
|
enable: false
|
||||||
|
anonymous: "" # 匿名白名单按需追加(逗号分隔,如 /file/upload)
|
||||||
|
|
||||||
|
user:
|
||||||
|
default:
|
||||||
|
password: <新用户初始密码>
|
||||||
|
|
||||||
|
file:
|
||||||
|
upload-dir: /data/upload # 容器/主机上需存在且可写
|
||||||
|
storage:
|
||||||
|
type: local # 阿里云 OSS 则改 ali 并补齐 oss 配置
|
||||||
|
transfer:
|
||||||
|
location: "1" # 内网侧 "1",外网侧 "2" —— 两侧必须相反!
|
||||||
|
send-dir: /data/transfer/send
|
||||||
|
receive-dir: /data/transfer/receive
|
||||||
|
unpack-risk-check: false
|
||||||
|
|
||||||
|
dify:
|
||||||
|
risk:
|
||||||
|
rule:
|
||||||
|
api-key: <真实key或占位>
|
||||||
|
scan:
|
||||||
|
api-key: <真实key或占位> # 必填(可为占位),否则启动失败
|
||||||
|
|
||||||
|
rabbitmq:
|
||||||
|
custom:
|
||||||
|
topic-exchange-name: s-file-topic
|
||||||
|
delayed-exchange-name: s-file-delayed
|
||||||
|
broadcast-id: s-file-broadcast
|
||||||
|
|
||||||
|
swagger:
|
||||||
|
enable: false # 生产必须 false
|
||||||
|
```
|
||||||
|
|
||||||
|
### 0.3 数据库变更
|
||||||
|
|
||||||
|
- **已有生产库**:执行 `db/upgrade-20260914-upload-config.sql`(达梦执行「1. MySQL / 达梦 DM8」一节,建 `x_upload_config` 表)。其余本次功能无表结构变更。
|
||||||
|
- **全新库**:先执行 `db/init-table.sql`(达梦按 MySQL 兼容模式)+ `db/init-data.sql`(平台、平台机构、admin 账号、文件种类字典)。
|
||||||
|
- 执行后验证:`select count(*) from x_upload_config;` 无报错即建表成功(0 行为正常,未配置的机构走默认策略)。
|
||||||
|
|
||||||
|
### 0.4 目录与权限
|
||||||
|
|
||||||
|
```
|
||||||
|
mkdir -p /data/upload /data/transfer/send /data/transfer/receive
|
||||||
|
chown -R <运行用户> /data
|
||||||
|
```
|
||||||
|
|
||||||
|
外网侧与内网侧通过 `file.transfer.location` 区分,`send-dir`/`receive-dir` 指向各自的摆渡目录(网闸/摆渡盘)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 启动方式
|
||||||
|
|
||||||
|
### 1.1 裸机 / 虚拟机(JDK 8)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 需 JDK 1.8(java -version 确认)
|
||||||
|
java -Xms1g -Xmx2g \
|
||||||
|
-Dspring.config.additional-location=file:/opt/sfile/config/application.yml \
|
||||||
|
-jar /opt/sfile/s-file.jar \
|
||||||
|
> /opt/sfile/logs/stdout.log 2>&1 &
|
||||||
|
```
|
||||||
|
|
||||||
|
多节点时按节点 IP 配置不同的 `code.snowflake.workid.<ip>`(可用启动参数 `-DCODE_SNOWFLAKE_WORKID_<IP大写>=<n>` 环境变量方式,`_` 映射 `.`)。
|
||||||
|
|
||||||
|
### 1.2 Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 构建(context 内需 s-file.jar 与 Dockerfile,Dockerfile 在 src/main/resources/)
|
||||||
|
cp target/s-file.jar <build-context>/ && docker build -t s-file:<版本> <build-context>
|
||||||
|
|
||||||
|
docker run -d --name sfile-app --restart unless-stopped -p 8080:8080 \
|
||||||
|
-v /opt/sfile/config/application.yml:/config/application.yml:ro \
|
||||||
|
-v /data/upload:/data/upload \
|
||||||
|
-v /data/transfer/send:/data/transfer/send \
|
||||||
|
-v /data/transfer/receive:/data/transfer/receive \
|
||||||
|
-e SPRING_CONFIG_ADDITIONAL_LOCATION=file:/config/application.yml \
|
||||||
|
-e CODE_SNOWFLAKE_WORKID_<容器IP大写下划线>=1 \
|
||||||
|
s-file:<版本>
|
||||||
|
```
|
||||||
|
|
||||||
|
> 注意:基础镜像 `freshworks/oraclejdk8` 为 amd64,arm64 主机需 `--platform linux/amd64`;workerId 键里的 IP 用容器固定 IP(自定义网络 `--ip`),容器重建 IP 不变。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 前端发布
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd senmesh-zc-legacy
|
||||||
|
bun install
|
||||||
|
bun run build # 产出 dist/(.env.production 已带 VITE_APP_TITLE 引号修复,规避 Bun env 解析 bug)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `dist/` 交由 nginx 托管;
|
||||||
|
- nginx 需将 `/api` 反代到后端并**去掉 `/api` 前缀**(与 vite dev 代理行为一致):
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://<后端IP>:8080/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
client_max_body_size 500m; # 与后端 multipart 上限匹配
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `.env.production` 当前值:`VITE_API_URL=`(空,走同域 /api)、`VITE_INIT_PASSWORD`(前端判断初始密码强制改密用)——如有调整需重新 build。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 本次版本功能与数据依赖清单(验收用)
|
||||||
|
|
||||||
|
| 功能 | 依赖 | 验收方法 |
|
||||||
|
|---|---|---|
|
||||||
|
| 用户启用/禁用/注销(管理员) | DB `uc_user.enabled/status`;**Redis**(禁用踢下线黑名单) | 禁用后该用户已登录 token 立即失效(302/401),重新登录被拒 |
|
||||||
|
| 机构管理员启停本机构用户 | 无额外依赖 | 机构管理员可启停本机构用户;跨机构返回「只能操作本机构的用户」 |
|
||||||
|
| 机构账号查询(超管) | 新增只读查询 | 管理后台-机构账号页正常出列表与筛选 |
|
||||||
|
| 按手机号查上传/发送/接收记录 | 无额外依赖 | 输入手机号出记录;发送/接收方向由部署侧 `file.transfer.location` 决定 |
|
||||||
|
| 机构上传配置 | 新表 `x_upload_config` | 管理后台-上传配置保存后,该机构网盘上传立即按策略拦截 |
|
||||||
|
| 禁用踢下线 | Redis key `user:disabled:{userId}` | `redis-cli keys 'user:disabled:*'` 可见禁用标记 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 上线步骤建议
|
||||||
|
|
||||||
|
1. 生产库执行 0.3 升级 SQL(执行前备份);
|
||||||
|
2. 替换真实 spire.pdf jar 并重新打包(0.1),核对 jar 时间戳;
|
||||||
|
3. 准备 application.yml(0.2 模板),**核对 JWT secret 已更换、dify api-key 已填、swagger.enable=false**;
|
||||||
|
4. 创建 /data 目录,确定内外网 location;
|
||||||
|
5. 部署后端(1.1 或 1.2),观察启动日志出现 `Started SFileApplication`;
|
||||||
|
6. 构建并发布前端(2),nginx reload;
|
||||||
|
7. 验收第 3 节清单(至少:admin 登录、上传一个非白名单文件被拒、禁用一个测试用户并确认其会话失效、记录查询出数据);
|
||||||
|
8. 观察日志无 `ERROR`,Redis 中 `user:disabled:*` 与登录缓存键正常。
|
||||||
|
|
||||||
|
## 5. 回滚
|
||||||
|
|
||||||
|
- 后端:保留上一版 jar/镜像,停新启旧即可(本次无破坏性 DDL/DML,`x_upload_config` 留存不影响旧版本运行);
|
||||||
|
- 前端:nginx 指回上一版 dist 目录;
|
||||||
|
- 数据库:无需回滚(仅新增表)。
|
||||||
209
scripts/sfile-ctl.sh
Executable file
209
scripts/sfile-ctl.sh
Executable file
@@ -0,0 +1,209 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ==============================================================================
|
||||||
|
# s-file 一键部署/运维脚本(生产环境)
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# ./sfile-ctl.sh start 启动服务
|
||||||
|
# ./sfile-ctl.sh stop 停止服务
|
||||||
|
# ./sfile-ctl.sh restart 重启服务
|
||||||
|
# ./sfile-ctl.sh status 查看状态
|
||||||
|
# ./sfile-ctl.sh deploy <jar路径> 发布新版 jar(自动备份旧版并重启)
|
||||||
|
# ./sfile-ctl.sh log [行数] 查看启动日志(默认 200 行)
|
||||||
|
# ./sfile-ctl.sh db-upgrade <sql路径> 在达梦库执行升级 SQL(需配置 disql)
|
||||||
|
# 上线用: db/upgrade-20260915-release.sql
|
||||||
|
#
|
||||||
|
# 部署目录结构(脚本首次 start 时自动创建,配置需手工放置):
|
||||||
|
# /opt/sfile/
|
||||||
|
# ├── s-file.jar 服务 jar
|
||||||
|
# ├── config/application.yml 生产配置(模板见 docs/生产部署说明.md)
|
||||||
|
# ├── backup/ 历史版本备份
|
||||||
|
# ├── logs/ 日志目录
|
||||||
|
# └── data/ 上传/摆渡目录(upload/transfer/send/transfer/receive)
|
||||||
|
# ==============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ------------------------- 环境变量(按服务器实际修改) -------------------------
|
||||||
|
APP_NAME="s-file"
|
||||||
|
INSTALL_DIR="${SFILE_INSTALL_DIR:-/opt/sfile}"
|
||||||
|
JAR_FILE="${INSTALL_DIR}/${APP_NAME}.jar"
|
||||||
|
CONFIG_FILE="${INSTALL_DIR}/config/application.yml"
|
||||||
|
LOG_DIR="${INSTALL_DIR}/logs"
|
||||||
|
BACKUP_DIR="${INSTALL_DIR}/backup"
|
||||||
|
STDOUT_LOG="${LOG_DIR}/stdout.log"
|
||||||
|
GC_LOG="${LOG_DIR}/gc.log"
|
||||||
|
|
||||||
|
# 运行用户(root 部署时可改为专用低权用户,如 sfile)
|
||||||
|
RUN_USER="${SFILE_RUN_USER:-$(whoami)}"
|
||||||
|
|
||||||
|
# JVM 参数:按机器内存调整 -Xmx;生产建议 2g 起
|
||||||
|
JAVA_OPTS="${SFILE_JAVA_OPTS:--Xms1g -Xmx2g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOG_DIR} -Xloggc:${GC_LOG} -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M}"
|
||||||
|
|
||||||
|
# 健康检查地址与超时
|
||||||
|
HEALTH_URL="http://127.0.0.1:8080/user/info"
|
||||||
|
START_TIMEOUT=120
|
||||||
|
|
||||||
|
# 达梦 disql(db-upgrade 用;达梦容器则为 docker exec -i <容器> /opt/dmdbms/bin/disql ...)
|
||||||
|
DISQL_CMD="${SFILE_DISQL:-disql <DB_USER>/<DB_PASSWORD>@<DB_HOST>:5236}"
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
GREEN='\033[32m'; RED='\033[31m'; YELLOW='\033[33m'; NC='\033[0m'
|
||||||
|
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||||
|
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||||
|
err() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||||
|
|
||||||
|
get_pid() {
|
||||||
|
pgrep -f "java .*${JAR_FILE}" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
require_env() {
|
||||||
|
[ -f "${JAR_FILE}" ] || { err "jar 不存在: ${JAR_FILE},先用 deploy 发布"; exit 1; }
|
||||||
|
[ -f "${CONFIG_FILE}" ] || { err "配置不存在: ${CONFIG_FILE}(模板见 docs/生产部署说明.md)"; exit 1; }
|
||||||
|
# 项目要求 JDK 8 运行(字节码 52.0),防护误用高版本 JDK
|
||||||
|
if command -v java >/dev/null 2>&1; then
|
||||||
|
local major
|
||||||
|
major=$(java -version 2>&1 | head -1 | sed -E 's/.*"([0-9]+)\..*"/\1/')
|
||||||
|
if [ "${major}" != "1" ] && [ "${major}" != "8" ]; then
|
||||||
|
err "检测到 java 版本为 ${major},本项目要求 JDK 8(请 export JAVA_HOME 指向 JDK8 并确认 java 命令)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
err "未找到 java 命令,请安装 JDK 8 并配置 PATH"; exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_dirs() {
|
||||||
|
mkdir -p "${INSTALL_DIR}/config" "${LOG_DIR}" "${BACKUP_DIR}" \
|
||||||
|
"${INSTALL_DIR}/data/upload" \
|
||||||
|
"${INSTALL_DIR}/data/transfer/send" \
|
||||||
|
"${INSTALL_DIR}/data/transfer/receive"
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
require_env
|
||||||
|
local pid
|
||||||
|
pid=$(get_pid)
|
||||||
|
if [ -n "${pid}" ]; then
|
||||||
|
warn "${APP_NAME} 已在运行 (PID ${pid}),如需重启请先 stop"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
prepare_dirs
|
||||||
|
info "启动 ${APP_NAME} ..."
|
||||||
|
# workerId 说明:多节点部署时每台机器配置不同的 code.snowflake.workid.<本机IP>,
|
||||||
|
# 最简单的方式是用环境变量(. 映射为 _),如:
|
||||||
|
# export CODE_SNOWFLAKE_WORKID_192_168_1_10=1
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
if [ "${RUN_USER}" = "$(whoami)" ]; then
|
||||||
|
(cd "${INSTALL_DIR}" && nohup java ${JAVA_OPTS} \
|
||||||
|
-Dspring.config.additional-location=file:${CONFIG_FILE} \
|
||||||
|
-jar ${JAR_FILE} >> ${STDOUT_LOG} 2>&1 & echo $! > "${INSTALL_DIR}/app.pid")
|
||||||
|
else
|
||||||
|
su -s /bin/bash "${RUN_USER}" -c "cd ${INSTALL_DIR} && nohup java ${JAVA_OPTS} \
|
||||||
|
-Dspring.config.additional-location=file:${CONFIG_FILE} \
|
||||||
|
-jar ${JAR_FILE} >> ${STDOUT_LOG} 2>&1 & echo \$! > ${INSTALL_DIR}/app.pid"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 启动健康检查轮询
|
||||||
|
info "等待服务就绪(最多 ${START_TIMEOUT}s)..."
|
||||||
|
local waited=0
|
||||||
|
while [ ${waited} -lt ${START_TIMEOUT} ]; do
|
||||||
|
if curl -sf -o /dev/null --max-time 3 "${HEALTH_URL}"; then
|
||||||
|
info "启动成功 (PID $(cat "${INSTALL_DIR}/app.pid")),耗时约 ${waited}s"
|
||||||
|
info "日志: tail -f ${STDOUT_LOG}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
# 进程若已退出则立即报错,不干等
|
||||||
|
if [ -z "$(get_pid)" ]; then
|
||||||
|
err "进程已退出,最近日志:"
|
||||||
|
tail -30 "${STDOUT_LOG}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 3; waited=$((waited + 3))
|
||||||
|
done
|
||||||
|
err "${START_TIMEOUT}s 内未就绪,请检查日志: tail -100 ${STDOUT_LOG}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
local pid
|
||||||
|
pid=$(get_pid)
|
||||||
|
if [ -z "${pid}" ]; then
|
||||||
|
info "${APP_NAME} 未在运行"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
info "停止 ${APP_NAME} (PID ${pid}) ..."
|
||||||
|
kill "${pid}"
|
||||||
|
local waited=0
|
||||||
|
while kill -0 "${pid}" 2>/dev/null && [ ${waited} -lt 60 ]; do
|
||||||
|
sleep 2; waited=$((waited + 2))
|
||||||
|
done
|
||||||
|
if kill -0 "${pid}" 2>/dev/null; then
|
||||||
|
warn "60s 未退出,强制 kill -9"
|
||||||
|
kill -9 "${pid}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
rm -f "${INSTALL_DIR}/app.pid"
|
||||||
|
info "已停止"
|
||||||
|
}
|
||||||
|
|
||||||
|
status() {
|
||||||
|
local pid
|
||||||
|
pid=$(get_pid)
|
||||||
|
if [ -z "${pid}" ]; then
|
||||||
|
info "${APP_NAME}: 未运行"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
info "${APP_NAME}: 运行中 (PID ${pid})"
|
||||||
|
if curl -sf -o /dev/null --max-time 3 "${HEALTH_URL}"; then
|
||||||
|
info "健康检查: 正常"
|
||||||
|
else
|
||||||
|
warn "健康检查: 8080 未响应(可能仍在启动)"
|
||||||
|
fi
|
||||||
|
info "内存: $(ps -o rss= -p "${pid}" | awk '{printf "%.0fMB", $1/1024}')"
|
||||||
|
}
|
||||||
|
|
||||||
|
deploy() {
|
||||||
|
local new_jar="$1"
|
||||||
|
[ -f "${new_jar}" ] || { err "找不到 jar: ${new_jar}"; exit 1; }
|
||||||
|
prepare_dirs
|
||||||
|
local ts; ts=$(date +%Y%m%d-%H%M%S)
|
||||||
|
if [ -f "${JAR_FILE}" ]; then
|
||||||
|
# 运行中先停
|
||||||
|
if [ -n "$(get_pid)" ]; then
|
||||||
|
stop
|
||||||
|
fi
|
||||||
|
cp "${JAR_FILE}" "${BACKUP_DIR}/${APP_NAME}-${ts}.jar"
|
||||||
|
info "旧版本已备份: ${BACKUP_DIR}/${APP_NAME}-${ts}.jar"
|
||||||
|
fi
|
||||||
|
cp "${new_jar}" "${JAR_FILE}"
|
||||||
|
# 保留最近 5 个备份
|
||||||
|
ls -1t "${BACKUP_DIR}/${APP_NAME}-"*.jar 2>/dev/null | tail -n +6 | xargs -r rm -f
|
||||||
|
info "新版本已就位: ${JAR_FILE} ($(du -h "${JAR_FILE}" | cut -f1))"
|
||||||
|
start
|
||||||
|
}
|
||||||
|
|
||||||
|
db_upgrade() {
|
||||||
|
local sql_file="$1"
|
||||||
|
[ -f "${sql_file}" ] || { err "找不到 SQL: ${sql_file}"; exit 1; }
|
||||||
|
case "${DISQL_CMD}" in
|
||||||
|
*"<DB_USER>"*) err "请先在脚本头部或环境变量 SFILE_DISQL 配置真实 disql 连接串"; exit 1;;
|
||||||
|
esac
|
||||||
|
info "执行升级 SQL: ${sql_file}"
|
||||||
|
# 达梦 disql 执行 SQL 文件语法: disql 用户/密码@host:port \`文件路径\`
|
||||||
|
${DISQL_CMD} \`"${sql_file}"
|
||||||
|
info "执行完成,请核对上方输出无 ERROR 后再继续部署"
|
||||||
|
}
|
||||||
|
|
||||||
|
log() {
|
||||||
|
local lines="${1:-200}"
|
||||||
|
tail -n "${lines}" "${STDOUT_LOG}" 2>/dev/null || warn "暂无日志: ${STDOUT_LOG}"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
start) start ;;
|
||||||
|
stop) stop ;;
|
||||||
|
restart) stop; start ;;
|
||||||
|
status) status ;;
|
||||||
|
deploy) [ $# -ge 2 ] || { err "用法: $0 deploy <jar路径>"; exit 1; }; deploy "$2" ;;
|
||||||
|
log) log "${2:-200}" ;;
|
||||||
|
db-upgrade) [ $# -ge 2 ] || { err "用法: $0 db-upgrade <sql路径>"; exit 1; }; db_upgrade "$2" ;;
|
||||||
|
*) sed -n '2,20p' "$0"; exit 1 ;;
|
||||||
|
esac
|
||||||
@@ -64,4 +64,7 @@ public interface CommonConstants {
|
|||||||
/** 平台机构id **/
|
/** 平台机构id **/
|
||||||
Long PLATFORM_ORG_ID = 1L;
|
Long PLATFORM_ORG_ID = 1L;
|
||||||
|
|
||||||
|
/** 用户禁用/注销登录态黑名单key前缀(存Redis,值存在期间该用户已发放的token视为无效) **/
|
||||||
|
String USER_DISABLED_KEY = "user:disabled:";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import com.senmesh.security.util.PasswordUtils;
|
|||||||
import com.senmesh.usercenter.user.util.UserUtils;
|
import com.senmesh.usercenter.user.util.UserUtils;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import io.swagger.annotations.ApiModel;
|
import io.swagger.annotations.ApiModel;
|
||||||
import io.swagger.annotations.ApiModelProperty;
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
@@ -75,7 +77,14 @@ public class UserInfoDTO implements Serializable{
|
|||||||
|
|
||||||
public void setPassword(String password){
|
public void setPassword(String password){
|
||||||
this.password = password;
|
this.password = password;
|
||||||
if (!PasswordUtils.match(UserUtils.getDefaultOrgUserPassword(), password)) {
|
// 未配置默认密码(user.default.password)或密码哈希为空时跳过比对,
|
||||||
|
// 视为非初始密码,避免缺配置导致登录链路抛 rawPassword cannot be null
|
||||||
|
String defaultPassword = UserUtils.getDefaultOrgUserPassword();
|
||||||
|
if (!StringUtils.isNotBlank(defaultPassword) || !StringUtils.isNotBlank(password)) {
|
||||||
|
this.originPassword = Boolean.FALSE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!PasswordUtils.match(defaultPassword, password)) {
|
||||||
this.originPassword = Boolean.FALSE;
|
this.originPassword = Boolean.FALSE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import com.senmesh.common.util.StrUtils;
|
|||||||
import com.senmesh.common.util.UserContextHolder;
|
import com.senmesh.common.util.UserContextHolder;
|
||||||
import com.senmesh.security.config.SecurityProperties;
|
import com.senmesh.security.config.SecurityProperties;
|
||||||
import com.senmesh.security.util.HostContextHolder;
|
import com.senmesh.security.util.HostContextHolder;
|
||||||
|
import com.senmesh.security.util.UserDisabledCache;
|
||||||
import com.senmesh.usercenter.constants.OrgConstants;
|
import com.senmesh.usercenter.constants.OrgConstants;
|
||||||
import com.senmesh.usercenter.dto.IUserDetail;
|
import com.senmesh.usercenter.dto.IUserDetail;
|
||||||
|
|
||||||
@@ -67,11 +68,16 @@ public class JwtAuthenticationTokenFilter extends GenericFilterBean {
|
|||||||
userDto = JwtTokenUtils.getUserLoginDtoFromToken(cookieToken);
|
userDto = JwtTokenUtils.getUserLoginDtoFromToken(cookieToken);
|
||||||
}
|
}
|
||||||
if (userDto != null) {
|
if (userDto != null) {
|
||||||
|
// 禁用/注销用户的已发放token立即失效(黑名单命中则不建立登录上下文)
|
||||||
|
if (UserDisabledCache.isMarked(userDto.getId())) {
|
||||||
|
log.debug("user is disabled or canceled, token rejected, userId:{}", userDto.getId());
|
||||||
|
} else {
|
||||||
SecurityContextHolder.getContext()
|
SecurityContextHolder.getContext()
|
||||||
.setAuthentication(JwtTokenUtils.getAuthentication(userDto));
|
.setAuthentication(JwtTokenUtils.getAuthentication(userDto));
|
||||||
UserContextHolder.setContext(userDto);
|
UserContextHolder.setContext(userDto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 登录用户只有平台用户能使用: 已登录 且 平台用户 且 非内部调用
|
// 登录用户只有平台用户能使用: 已登录 且 平台用户 且 非内部调用
|
||||||
//&& !StringUtils.equals(OrgConstants.ORG_TYPE_AGENT,userDto.getOrgType()) 开放代理机构登录中台判断条件
|
//&& !StringUtils.equals(OrgConstants.ORG_TYPE_AGENT,userDto.getOrgType()) 开放代理机构登录中台判断条件
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package com.senmesh.security.util;
|
||||||
|
|
||||||
|
import com.senmesh.common.constant.CommonConstants;
|
||||||
|
import com.senmesh.frame.redis.redisson.RedissonUtils;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户禁用/注销登录态黑名单
|
||||||
|
* 用户被禁用或注销后,其已发放且未过期的token需要立即失效:
|
||||||
|
* 写入黑名单标记后,JwtAuthenticationTokenFilter将对命中标记的用户不再建立登录上下文,
|
||||||
|
* 标记TTL与token有效期一致,token自然过期后标记自动清理
|
||||||
|
*
|
||||||
|
* @author liu on 2026-09-14
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class UserDisabledCache {
|
||||||
|
|
||||||
|
private UserDisabledCache() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String key(Long userId) {
|
||||||
|
return CommonConstants.USER_DISABLED_KEY + userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记用户登录态失效
|
||||||
|
*
|
||||||
|
* @param userId 用户id
|
||||||
|
* @param ttlSeconds 存活秒数(取token有效期)
|
||||||
|
*/
|
||||||
|
public static void mark(Long userId, long ttlSeconds) {
|
||||||
|
try {
|
||||||
|
RedissonUtils.getClient().getBucket(key(userId)).set(Boolean.TRUE, ttlSeconds, java.util.concurrent.TimeUnit.SECONDS);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("标记用户登录态失效失败,userId:{}", userId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解除标记(启用用户)
|
||||||
|
*/
|
||||||
|
public static void unmark(Long userId) {
|
||||||
|
try {
|
||||||
|
RedissonUtils.getClient().getBucket(key(userId)).delete();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("解除用户登录态标记失败,userId:{}", userId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否被标记(禁用/注销)
|
||||||
|
* Redis异常时降级放行,由数据库状态在后续链路兜底
|
||||||
|
*/
|
||||||
|
public static boolean isMarked(Long userId) {
|
||||||
|
try {
|
||||||
|
return Boolean.TRUE.equals(RedissonUtils.getClient().getBucket(key(userId)).get());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("查询用户登录态标记异常,降级放行,userId:{}", userId, e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.senmesh.sfile.constant;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件记录类型枚举(按手机号查询上传/发送/接收记录)
|
||||||
|
*/
|
||||||
|
public enum FileRecordTypeEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传记录
|
||||||
|
*/
|
||||||
|
UPLOAD(1, "上传记录"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送记录(本端发起文件传输)
|
||||||
|
*/
|
||||||
|
SEND(2, "发送记录"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收记录(本端接收文件传输)
|
||||||
|
*/
|
||||||
|
RECEIVE(3, "接收记录");
|
||||||
|
|
||||||
|
private final Integer type;
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
FileRecordTypeEnum(Integer type, String desc) {
|
||||||
|
this.type = type;
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDesc() {
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据类型获取枚举
|
||||||
|
*
|
||||||
|
* @param type 类型
|
||||||
|
* @return FileRecordTypeEnum
|
||||||
|
*/
|
||||||
|
public static FileRecordTypeEnum getByType(Integer type) {
|
||||||
|
for (FileRecordTypeEnum recordTypeEnum : FileRecordTypeEnum.values()) {
|
||||||
|
if (recordTypeEnum.getType().equals(type)) {
|
||||||
|
return recordTypeEnum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.senmesh.sfile.controller;
|
||||||
|
|
||||||
|
import com.senmesh.common.AlitaResult;
|
||||||
|
import com.senmesh.sfile.dto.UploadConfigDTO;
|
||||||
|
import com.senmesh.sfile.dto.UploadConfigModifyDTO;
|
||||||
|
import com.senmesh.sfile.service.UploadConfigService;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 机构上传配置Controller
|
||||||
|
* 机构管理员可配置本机构允许上传的文件类型/大小等,配置对网盘上传生效
|
||||||
|
*
|
||||||
|
* @author liu on 2026-09-14
|
||||||
|
*/
|
||||||
|
@Api(tags = "机构上传配置")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("upload_config")
|
||||||
|
public class UploadConfigController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UploadConfigService uploadConfigService;
|
||||||
|
|
||||||
|
@GetMapping("/info")
|
||||||
|
@ApiOperation("查询当前登录机构的上传配置(未配置返回默认策略)")
|
||||||
|
public AlitaResult<UploadConfigDTO> info() {
|
||||||
|
return AlitaResult.success(uploadConfigService.info());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/modify")
|
||||||
|
@ApiOperation("保存当前登录机构的上传配置(机构管理员/平台超管)")
|
||||||
|
public AlitaResult<Boolean> modify(@Validated @RequestBody UploadConfigModifyDTO modifyDTO) {
|
||||||
|
return AlitaResult.success(uploadConfigService.modify(modifyDTO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/check")
|
||||||
|
@ApiOperation("上传预检(按文件名校验当前机构配置,不通过返回业务异常)")
|
||||||
|
public AlitaResult<Boolean> check(@RequestParam("fileName") String fileName,
|
||||||
|
@RequestParam(value = "fileSize", required = false) Long fileSize) {
|
||||||
|
uploadConfigService.check(fileName, fileSize);
|
||||||
|
return AlitaResult.success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ import com.senmesh.sfile.dto.UserFileDTO;
|
|||||||
import com.senmesh.sfile.dto.UserFileModifyDTO;
|
import com.senmesh.sfile.dto.UserFileModifyDTO;
|
||||||
import com.senmesh.sfile.dto.UserFileModifyStatusDTO;
|
import com.senmesh.sfile.dto.UserFileModifyStatusDTO;
|
||||||
import com.senmesh.sfile.dto.UserFileQueryDTO;
|
import com.senmesh.sfile.dto.UserFileQueryDTO;
|
||||||
|
import com.senmesh.sfile.dto.UserFileRecordDTO;
|
||||||
|
import com.senmesh.sfile.dto.UserFileRecordQueryDTO;
|
||||||
import com.senmesh.sfile.dto.UserFileStatisticsDTO;
|
import com.senmesh.sfile.dto.UserFileStatisticsDTO;
|
||||||
import com.senmesh.sfile.service.UserFileService;
|
import com.senmesh.sfile.service.UserFileService;
|
||||||
import com.senmesh.tkmybatis.pojo.Page;
|
import com.senmesh.tkmybatis.pojo.Page;
|
||||||
@@ -115,6 +117,13 @@ public class UserFileController {
|
|||||||
return AlitaResult.success(userFileService.statisticsByCategory(queryDTO));
|
return AlitaResult.success(userFileService.statisticsByCategory(queryDTO));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/record/page_by_phone")
|
||||||
|
@ApiOperation("按手机号分页查询文件记录(上传/发送/接收,系统管理员)")
|
||||||
|
public AlitaResult<Page<UserFileRecordDTO>> pageRecordByPhone(UserFileRecordQueryDTO queryDTO) {
|
||||||
|
userFileService.pageRecordByPhone(queryDTO);
|
||||||
|
return AlitaResult.success(queryDTO.getPageResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
15
src/main/java/com/senmesh/sfile/dao/UploadConfigMapper.java
Normal file
15
src/main/java/com/senmesh/sfile/dao/UploadConfigMapper.java
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package com.senmesh.sfile.dao;
|
||||||
|
|
||||||
|
import com.senmesh.sfile.entity.UploadConfig;
|
||||||
|
import com.senmesh.tkmybatis.mybatis.MyMapper;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 机构上传配置Mapper
|
||||||
|
*
|
||||||
|
* @author liu on 2026-09-14
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface UploadConfigMapper extends MyMapper<UploadConfig> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ package com.senmesh.sfile.dao;
|
|||||||
|
|
||||||
import com.senmesh.sfile.dto.UserFileDTO;
|
import com.senmesh.sfile.dto.UserFileDTO;
|
||||||
import com.senmesh.sfile.dto.UserFileQueryDTO;
|
import com.senmesh.sfile.dto.UserFileQueryDTO;
|
||||||
|
import com.senmesh.sfile.dto.UserFileRecordDTO;
|
||||||
|
import com.senmesh.sfile.dto.UserFileRecordQueryDTO;
|
||||||
import com.senmesh.sfile.dto.UserFileStatisticsDTO;
|
import com.senmesh.sfile.dto.UserFileStatisticsDTO;
|
||||||
import com.senmesh.sfile.entity.UserFile;
|
import com.senmesh.sfile.entity.UserFile;
|
||||||
import com.senmesh.tkmybatis.mybatis.MyMapper;
|
import com.senmesh.tkmybatis.mybatis.MyMapper;
|
||||||
@@ -64,5 +66,13 @@ public interface UserFileMapper extends MyMapper<UserFile> {
|
|||||||
* @return List<UserFileStatisticsDTO>
|
* @return List<UserFileStatisticsDTO>
|
||||||
*/
|
*/
|
||||||
List<UserFileStatisticsDTO> statisticsByCategory(@Param("param") UserFileQueryDTO queryDTO);
|
List<UserFileStatisticsDTO> statisticsByCategory(@Param("param") UserFileQueryDTO queryDTO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按手机号查询文件记录(上传/发送/接收,PageHelper会根据queryDTO自动判断是否分页)
|
||||||
|
*
|
||||||
|
* @param queryDTO 查询DTO
|
||||||
|
* @return List<UserFileRecordDTO>
|
||||||
|
*/
|
||||||
|
List<UserFileRecordDTO> listRecordByPhone(@Param("param") UserFileRecordQueryDTO queryDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
46
src/main/java/com/senmesh/sfile/dto/UploadConfigDTO.java
Normal file
46
src/main/java/com/senmesh/sfile/dto/UploadConfigDTO.java
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package com.senmesh.sfile.dto;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 机构上传配置DTO
|
||||||
|
*
|
||||||
|
* @author liu on 2026-09-14
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@ApiModel("机构上传配置")
|
||||||
|
public class UploadConfigDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@ApiModelProperty("机构id")
|
||||||
|
private Long orgId;
|
||||||
|
|
||||||
|
@ApiModelProperty("是否允许上传")
|
||||||
|
private Boolean uploadEnabled;
|
||||||
|
|
||||||
|
@ApiModelProperty("是否允许全部类型")
|
||||||
|
private Boolean allowAllType;
|
||||||
|
|
||||||
|
@ApiModelProperty("允许的文件后缀(小写,不含点),allow_all_type=false时生效")
|
||||||
|
private List<String> allowedSuffixes;
|
||||||
|
|
||||||
|
@ApiModelProperty("单文件大小上限(MB),空或0不限制")
|
||||||
|
private Long maxFileSize;
|
||||||
|
|
||||||
|
@ApiModelProperty("机构是否已配置过(false表示使用默认策略:允许上传全部类型)")
|
||||||
|
private Boolean configured;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.senmesh.sfile.dto;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 机构上传配置修改DTO
|
||||||
|
*
|
||||||
|
* @author liu on 2026-09-14
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@ApiModel("机构上传配置修改")
|
||||||
|
public class UploadConfigModifyDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@ApiModelProperty("是否允许上传")
|
||||||
|
@NotNull(message = "是否允许上传不能为空")
|
||||||
|
private Boolean uploadEnabled;
|
||||||
|
|
||||||
|
@ApiModelProperty("是否允许全部类型")
|
||||||
|
@NotNull(message = "是否允许全部类型不能为空")
|
||||||
|
private Boolean allowAllType;
|
||||||
|
|
||||||
|
@ApiModelProperty("允许的文件后缀(不含点,自动忽略大小写),allow_all_type=false时必填")
|
||||||
|
private List<String> allowedSuffixes;
|
||||||
|
|
||||||
|
@ApiModelProperty("单文件大小上限(MB),空或0不限制")
|
||||||
|
private Long maxFileSize;
|
||||||
|
|
||||||
|
}
|
||||||
43
src/main/java/com/senmesh/sfile/dto/UserFileRecordDTO.java
Normal file
43
src/main/java/com/senmesh/sfile/dto/UserFileRecordDTO.java
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package com.senmesh.sfile.dto;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.SuperBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户文件记录DTO(按手机号查询上传/发送/接收记录)
|
||||||
|
*/
|
||||||
|
@ApiModel("用户文件记录")
|
||||||
|
@Data
|
||||||
|
@SuperBuilder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class UserFileRecordDTO extends UserFileDTO {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@ApiModelProperty("手机号")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@ApiModelProperty("账号")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@ApiModelProperty("真实姓名")
|
||||||
|
private String realName;
|
||||||
|
|
||||||
|
@ApiModelProperty("用户类型:1企业用户,2个人用户")
|
||||||
|
private Integer userType;
|
||||||
|
|
||||||
|
@ApiModelProperty("记录类型:1上传记录/2发送记录/3接收记录")
|
||||||
|
private Integer recordType;
|
||||||
|
|
||||||
|
@ApiModelProperty("记录类型描述")
|
||||||
|
private String recordTypeDesc;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.senmesh.sfile.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.senmesh.common.dto.PageQueryDTO;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户文件记录查询DTO(按手机号查询上传/发送/接收记录)
|
||||||
|
*/
|
||||||
|
@ApiModel("用户文件记录查询DTO(按手机号)")
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class UserFileRecordQueryDTO extends PageQueryDTO<UserFileRecordDTO> {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "手机号(必填,精确匹配)", required = true)
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@ApiModelProperty("记录类型:1上传记录/2发送记录/3接收记录(不传查全部)")
|
||||||
|
private Integer recordType;
|
||||||
|
|
||||||
|
@ApiModelProperty("文件名(模糊查询)")
|
||||||
|
private String fileNameLike;
|
||||||
|
|
||||||
|
@ApiModelProperty("文件状态:1正常2失效11解析中12待审核13传输中14失败")
|
||||||
|
private String fileStatus;
|
||||||
|
|
||||||
|
@ApiModelProperty("文件类型:1文件/2文件夹")
|
||||||
|
private String fileType;
|
||||||
|
|
||||||
|
@ApiModelProperty("创建时间起")
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@JsonFormat(
|
||||||
|
shape = JsonFormat.Shape.STRING,
|
||||||
|
pattern = "yyyy-MM-dd HH:mm:ss",
|
||||||
|
timezone = "GMT+8"
|
||||||
|
)
|
||||||
|
private Date createTimeBegin;
|
||||||
|
|
||||||
|
@ApiModelProperty("创建时间止")
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@JsonFormat(
|
||||||
|
shape = JsonFormat.Shape.STRING,
|
||||||
|
pattern = "yyyy-MM-dd HH:mm:ss",
|
||||||
|
timezone = "GMT+8"
|
||||||
|
)
|
||||||
|
private Date createTimeEnd;
|
||||||
|
|
||||||
|
@ApiModelProperty(hidden = true)
|
||||||
|
@JsonIgnore
|
||||||
|
private Set<String> rootTypeIn;
|
||||||
|
|
||||||
|
}
|
||||||
43
src/main/java/com/senmesh/sfile/entity/UploadConfig.java
Normal file
43
src/main/java/com/senmesh/sfile/entity/UploadConfig.java
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package com.senmesh.sfile.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import com.senmesh.common.entity.BaseEntity;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.experimental.SuperBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 机构上传配置
|
||||||
|
* 一行对应一个机构,用于限制该机构用户在网盘中可上传的文件类型/大小
|
||||||
|
*
|
||||||
|
* @author liu on 2026-09-14
|
||||||
|
*/
|
||||||
|
@TableName("x_upload_config")
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@SuperBuilder
|
||||||
|
@Data
|
||||||
|
public class UploadConfig extends BaseEntity {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/** 机构id(唯一) */
|
||||||
|
private Long orgId;
|
||||||
|
|
||||||
|
/** 是否允许上传 */
|
||||||
|
private Boolean uploadEnabled;
|
||||||
|
|
||||||
|
/** 是否允许全部类型(false时按allowed_suffixes白名单校验) */
|
||||||
|
private Boolean allowAllType;
|
||||||
|
|
||||||
|
/** 允许的文件后缀(逗号分隔,小写存储),allow_all_type=false时生效 */
|
||||||
|
private String allowedSuffixes;
|
||||||
|
|
||||||
|
/** 单文件大小上限(MB),空或0不限制 */
|
||||||
|
private Long maxFileSize;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -9,6 +9,44 @@
|
|||||||
ORDER BY t.id DESC
|
ORDER BY t.id DESC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="listRecordByPhone" resultType="com.senmesh.sfile.dto.UserFileRecordDTO">
|
||||||
|
SELECT
|
||||||
|
t.*,
|
||||||
|
fr.file_size,
|
||||||
|
u.phone,
|
||||||
|
u.username,
|
||||||
|
u.real_name,
|
||||||
|
u.type AS user_type
|
||||||
|
FROM x_user_file t
|
||||||
|
INNER JOIN uc_user u ON t.user_id = u.id AND u.deleted = FALSE
|
||||||
|
LEFT JOIN xs_file_record fr ON t.file_code = fr.file_code AND fr.deleted = FALSE
|
||||||
|
WHERE t.deleted = FALSE
|
||||||
|
AND u.phone = #{param.phone}
|
||||||
|
<if test='param.rootTypeIn != null and !param.rootTypeIn.isEmpty()'>
|
||||||
|
AND t.root_type IN
|
||||||
|
<foreach collection="param.rootTypeIn" item="item" open="(" separator="," close=")">
|
||||||
|
#{item}
|
||||||
|
</foreach>
|
||||||
|
</if>
|
||||||
|
<if test='param.fileType != null and param.fileType.trim() != ""'>
|
||||||
|
AND t.file_type = #{param.fileType}
|
||||||
|
</if>
|
||||||
|
<if test='param.fileNameLike != null and param.fileNameLike.trim() != ""'>
|
||||||
|
<bind name="fileNameLikeBind" value="'%' + param.fileNameLike + '%'"/>
|
||||||
|
AND t.file_name LIKE #{fileNameLikeBind}
|
||||||
|
</if>
|
||||||
|
<if test='param.fileStatus != null and param.fileStatus.trim() != ""'>
|
||||||
|
AND t.file_status = #{param.fileStatus}
|
||||||
|
</if>
|
||||||
|
<if test='param.createTimeBegin != null'>
|
||||||
|
AND t.create_time >= #{param.createTimeBegin}
|
||||||
|
</if>
|
||||||
|
<if test='param.createTimeEnd != null'>
|
||||||
|
AND t.create_time <= #{param.createTimeEnd}
|
||||||
|
</if>
|
||||||
|
ORDER BY t.id DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="statisticsByCategory" resultType="com.senmesh.sfile.dto.UserFileStatisticsDTO">
|
<select id="statisticsByCategory" resultType="com.senmesh.sfile.dto.UserFileStatisticsDTO">
|
||||||
SELECT
|
SELECT
|
||||||
fc.file_category AS file_category,
|
fc.file_category AS file_category,
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.senmesh.sfile.service;
|
||||||
|
|
||||||
|
import com.senmesh.sfile.dto.UploadConfigDTO;
|
||||||
|
import com.senmesh.sfile.dto.UploadConfigModifyDTO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 机构上传配置Service
|
||||||
|
*
|
||||||
|
* @author liu on 2026-09-14
|
||||||
|
*/
|
||||||
|
public interface UploadConfigService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前登录机构的上传配置
|
||||||
|
* 未配置时返回默认策略(允许上传全部类型,不限大小)
|
||||||
|
*
|
||||||
|
* @return UploadConfigDTO
|
||||||
|
*/
|
||||||
|
UploadConfigDTO info();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存当前登录机构的上传配置(机构管理员/平台超管)
|
||||||
|
*
|
||||||
|
* @param modifyDTO 修改DTO
|
||||||
|
* @return Boolean
|
||||||
|
*/
|
||||||
|
Boolean modify(UploadConfigModifyDTO modifyDTO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验文件是否满足当前登录机构的上传配置,不满足抛业务异常
|
||||||
|
* 无登录机构(个人用户)或机构未配置时不限制
|
||||||
|
*
|
||||||
|
* @param fileName 文件名
|
||||||
|
* @param fileSize 文件大小(字节),为空时跳过大小校验
|
||||||
|
*/
|
||||||
|
void check(String fileName, Long fileSize);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ import com.senmesh.sfile.dto.UserFileDTO;
|
|||||||
import com.senmesh.sfile.dto.UserFileModifyDTO;
|
import com.senmesh.sfile.dto.UserFileModifyDTO;
|
||||||
import com.senmesh.sfile.dto.UserFileModifyStatusDTO;
|
import com.senmesh.sfile.dto.UserFileModifyStatusDTO;
|
||||||
import com.senmesh.sfile.dto.UserFileQueryDTO;
|
import com.senmesh.sfile.dto.UserFileQueryDTO;
|
||||||
|
import com.senmesh.sfile.dto.UserFileRecordDTO;
|
||||||
|
import com.senmesh.sfile.dto.UserFileRecordQueryDTO;
|
||||||
import com.senmesh.sfile.dto.UserFileStatisticsDTO;
|
import com.senmesh.sfile.dto.UserFileStatisticsDTO;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -128,5 +130,13 @@ public interface UserFileService {
|
|||||||
* @return List<UserFileStatisticsDTO>
|
* @return List<UserFileStatisticsDTO>
|
||||||
*/
|
*/
|
||||||
List<UserFileStatisticsDTO> statisticsByCategory(UserFileQueryDTO queryDTO);
|
List<UserFileStatisticsDTO> statisticsByCategory(UserFileQueryDTO queryDTO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按手机号分页查询文件记录(上传/发送/接收,系统管理员)
|
||||||
|
* 记录类型与本端部署位置有关:内网侧发送为内穿外(2)、接收为外传内(3);外网侧相反
|
||||||
|
*
|
||||||
|
* @param queryDTO 查询DTO
|
||||||
|
*/
|
||||||
|
void pageRecordByPhone(UserFileRecordQueryDTO queryDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
package com.senmesh.sfile.service.impl;
|
package com.senmesh.sfile.service.impl;
|
||||||
|
|
||||||
import com.senmesh.common.util.UserContextHolder;
|
import com.senmesh.common.util.UserContextHolder;
|
||||||
|
import com.senmesh.component.file.entity.FileRecord;
|
||||||
|
import com.senmesh.component.file.service.FileRecordService;
|
||||||
import com.senmesh.sfile.constant.FileStatusEnum;
|
import com.senmesh.sfile.constant.FileStatusEnum;
|
||||||
|
import com.senmesh.sfile.constant.FileTypeEnum;
|
||||||
import com.senmesh.sfile.constant.RootTypeEnum;
|
import com.senmesh.sfile.constant.RootTypeEnum;
|
||||||
import com.senmesh.sfile.dto.UserFileCreateDTO;
|
import com.senmesh.sfile.dto.UserFileCreateDTO;
|
||||||
import com.senmesh.sfile.dto.UserFileDTO;
|
import com.senmesh.sfile.dto.UserFileDTO;
|
||||||
import com.senmesh.sfile.service.NormalUserFileService;
|
import com.senmesh.sfile.service.NormalUserFileService;
|
||||||
|
import com.senmesh.sfile.service.UploadConfigService;
|
||||||
import com.senmesh.sfile.service.UserFileService;
|
import com.senmesh.sfile.service.UserFileService;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -20,6 +25,12 @@ public class NormalUserFileServiceImpl implements NormalUserFileService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private UserFileService userFileService;
|
private UserFileService userFileService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UploadConfigService uploadConfigService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FileRecordService fileRecordService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public UserFileDTO create(UserFileCreateDTO createDTO) {
|
public UserFileDTO create(UserFileCreateDTO createDTO) {
|
||||||
@@ -28,6 +39,16 @@ public class NormalUserFileServiceImpl implements NormalUserFileService {
|
|||||||
createDTO.setRootType(RootTypeEnum.NORMAL.getType());
|
createDTO.setRootType(RootTypeEnum.NORMAL.getType());
|
||||||
// TODO 扫描文件审核
|
// TODO 扫描文件审核
|
||||||
createDTO.setFileStatus(FileStatusEnum.NORMAL.getStatus());
|
createDTO.setFileStatus(FileStatusEnum.NORMAL.getStatus());
|
||||||
|
// 机构上传配置校验(仅校验文件,文件夹无后缀/大小概念)
|
||||||
|
if (FileTypeEnum.isFile(createDTO.getFileType())) {
|
||||||
|
Long fileSize = null;
|
||||||
|
if (StringUtils.isNotBlank(createDTO.getFileCode())) {
|
||||||
|
FileRecord fileRecord = fileRecordService
|
||||||
|
.find(FileRecord.builder().fileCode(createDTO.getFileCode()).build());
|
||||||
|
fileSize = fileRecord == null ? null : fileRecord.getFileSize();
|
||||||
|
}
|
||||||
|
uploadConfigService.check(createDTO.getFileName(), fileSize);
|
||||||
|
}
|
||||||
// 创建用户文件
|
// 创建用户文件
|
||||||
return userFileService.create(createDTO);
|
return userFileService.create(createDTO);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package com.senmesh.sfile.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.senmesh.common.exception.BusinessException;
|
||||||
|
import com.senmesh.common.exception.util.Assert;
|
||||||
|
import com.senmesh.common.util.UserContextHolder;
|
||||||
|
import com.senmesh.frame.redis.aop.annotation.AutoUnLock;
|
||||||
|
import com.senmesh.frame.redis.dto.LockDTO;
|
||||||
|
import com.senmesh.frame.redis.redisson.RedissonUtils;
|
||||||
|
import com.senmesh.sfile.dao.UploadConfigMapper;
|
||||||
|
import com.senmesh.sfile.dto.UploadConfigDTO;
|
||||||
|
import com.senmesh.sfile.dto.UploadConfigModifyDTO;
|
||||||
|
import com.senmesh.sfile.entity.UploadConfig;
|
||||||
|
import com.senmesh.sfile.service.UploadConfigService;
|
||||||
|
import org.apache.commons.collections4.CollectionUtils;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 机构上传配置ServiceImpl
|
||||||
|
*
|
||||||
|
* @author liu on 2026-09-14
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class UploadConfigServiceImpl implements UploadConfigService {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UploadConfigMapper uploadConfigMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UploadConfigDTO info() {
|
||||||
|
Long orgId = UserContextHolder.getExistLoginOrgId();
|
||||||
|
UploadConfig config = getByOrgId(orgId);
|
||||||
|
if (config == null) {
|
||||||
|
// 未配置:默认允许上传全部类型,不限大小
|
||||||
|
return UploadConfigDTO.builder()
|
||||||
|
.orgId(orgId)
|
||||||
|
.uploadEnabled(Boolean.TRUE)
|
||||||
|
.allowAllType(Boolean.TRUE)
|
||||||
|
.configured(Boolean.FALSE)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
return UploadConfigDTO.builder()
|
||||||
|
.orgId(orgId)
|
||||||
|
.uploadEnabled(config.getUploadEnabled())
|
||||||
|
.allowAllType(config.getAllowAllType())
|
||||||
|
.allowedSuffixes(parseSuffixes(config.getAllowedSuffixes()))
|
||||||
|
.maxFileSize(config.getMaxFileSize())
|
||||||
|
.configured(Boolean.TRUE)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
@AutoUnLock
|
||||||
|
public Boolean modify(UploadConfigModifyDTO modifyDTO) {
|
||||||
|
Assert.isTrue(UserContextHolder.isSuperior() || UserContextHolder.isAdministrator(),
|
||||||
|
"当前用户无操作权限");
|
||||||
|
Long orgId = UserContextHolder.getExistLoginOrgId();
|
||||||
|
List<String> suffixes = normalizeSuffixes(modifyDTO.getAllowedSuffixes());
|
||||||
|
Assert.isTrue(Boolean.TRUE.equals(modifyDTO.getAllowAllType()) || CollectionUtils.isNotEmpty(suffixes),
|
||||||
|
"未允许全部类型时,允许的文件后缀不能为空");
|
||||||
|
if (modifyDTO.getMaxFileSize() != null) {
|
||||||
|
Assert.isTrue(modifyDTO.getMaxFileSize() > 0, "单文件大小上限必须大于0");
|
||||||
|
}
|
||||||
|
Assert.isTrue(RedissonUtils.tryLockAutomatically(
|
||||||
|
LockDTO.builder().methodName("uploadConfigModify").arg(String.valueOf(orgId)).build()),
|
||||||
|
"上传配置正在修改,请稍后重试");
|
||||||
|
|
||||||
|
UploadConfig config = getByOrgId(orgId);
|
||||||
|
boolean exists = config != null;
|
||||||
|
if (!exists) {
|
||||||
|
config = UploadConfig.builder().orgId(orgId).build();
|
||||||
|
}
|
||||||
|
config.setUploadEnabled(modifyDTO.getUploadEnabled());
|
||||||
|
config.setAllowAllType(modifyDTO.getAllowAllType());
|
||||||
|
config.setAllowedSuffixes(CollectionUtils.isEmpty(suffixes) ? null : String.join(",", suffixes));
|
||||||
|
config.setMaxFileSize(modifyDTO.getMaxFileSize());
|
||||||
|
if (exists) {
|
||||||
|
return uploadConfigMapper.updateEntitySelective(config, UserContextHolder.getExistLoginUserId()) == 1;
|
||||||
|
}
|
||||||
|
uploadConfigMapper.saveEntity(config);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void check(String fileName, Long fileSize) {
|
||||||
|
Long orgId = UserContextHolder.getLoginOrgId();
|
||||||
|
if (orgId == null) {
|
||||||
|
// 个人用户等无登录机构场景,不受机构配置限制
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UploadConfig config = getByOrgId(orgId);
|
||||||
|
if (config == null) {
|
||||||
|
// 未配置:默认允许
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Boolean.TRUE.equals(config.getUploadEnabled())) {
|
||||||
|
throw new BusinessException("当前机构已禁止上传文件,请联系机构管理员");
|
||||||
|
}
|
||||||
|
// 文件夹等无后缀场景由调用方跳过,此处后缀为空按白名单不含空处理
|
||||||
|
String suffix = extractSuffix(fileName);
|
||||||
|
if (!Boolean.TRUE.equals(config.getAllowAllType())) {
|
||||||
|
List<String> allowed = parseSuffixes(config.getAllowedSuffixes());
|
||||||
|
if (allowed.isEmpty()) {
|
||||||
|
throw new BusinessException("当前机构未配置可上传的文件类型,请联系机构管理员");
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(suffix) || !allowed.contains(suffix)) {
|
||||||
|
throw new BusinessException("仅允许上传后缀为 " + String.join("、", allowed) + " 的文件");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (config.getMaxFileSize() != null && config.getMaxFileSize() > 0
|
||||||
|
&& fileSize != null && fileSize > config.getMaxFileSize() * 1024 * 1024) {
|
||||||
|
throw new BusinessException("上传文件过大,单文件最大不得超过" + config.getMaxFileSize() + "MB");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private UploadConfig getByOrgId(Long orgId) {
|
||||||
|
return uploadConfigMapper.selectOne(new LambdaQueryWrapper<UploadConfig>()
|
||||||
|
.eq(UploadConfig::getOrgId, orgId)
|
||||||
|
.eq(UploadConfig::getDeleted, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后缀规范化:去空格/去前导点/转小写/去空元素
|
||||||
|
*/
|
||||||
|
private List<String> normalizeSuffixes(List<String> suffixes) {
|
||||||
|
if (CollectionUtils.isEmpty(suffixes)) {
|
||||||
|
return suffixes;
|
||||||
|
}
|
||||||
|
return suffixes.stream()
|
||||||
|
.filter(StringUtils::isNotBlank)
|
||||||
|
.map(s -> StringUtils.stripStart(s.trim(), ".").toLowerCase())
|
||||||
|
.filter(StringUtils::isNotBlank)
|
||||||
|
.distinct()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> parseSuffixes(String allowedSuffixes) {
|
||||||
|
if (StringUtils.isBlank(allowedSuffixes)) {
|
||||||
|
return java.util.Collections.emptyList();
|
||||||
|
}
|
||||||
|
return Arrays.stream(allowedSuffixes.split(","))
|
||||||
|
.map(String::trim)
|
||||||
|
.filter(StringUtils::isNotBlank)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取小写后缀(不含点),无后缀返回空串
|
||||||
|
*/
|
||||||
|
private String extractSuffix(String fileName) {
|
||||||
|
if (StringUtils.isBlank(fileName)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String name = fileName.trim();
|
||||||
|
int dot = name.lastIndexOf('.');
|
||||||
|
if (dot < 0 || dot == name.length() - 1) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return name.substring(dot + 1).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,8 +4,11 @@ import com.senmesh.common.constant.CommonConstants;
|
|||||||
import com.senmesh.common.exception.util.Assert;
|
import com.senmesh.common.exception.util.Assert;
|
||||||
import com.senmesh.common.util.ListUtil;
|
import com.senmesh.common.util.ListUtil;
|
||||||
import com.senmesh.common.util.UserContextHolder;
|
import com.senmesh.common.util.UserContextHolder;
|
||||||
|
import com.senmesh.component.file.config.FileProperties;
|
||||||
import com.senmesh.component.file.service.FileService;
|
import com.senmesh.component.file.service.FileService;
|
||||||
import com.senmesh.component.file.util.FileUtils;
|
import com.senmesh.component.file.util.FileUtils;
|
||||||
|
import com.senmesh.sfile.constant.FileLocationEnum;
|
||||||
|
import com.senmesh.sfile.constant.FileRecordTypeEnum;
|
||||||
import com.senmesh.sfile.constant.FileStatusEnum;
|
import com.senmesh.sfile.constant.FileStatusEnum;
|
||||||
import com.senmesh.sfile.constant.FileTypeEnum;
|
import com.senmesh.sfile.constant.FileTypeEnum;
|
||||||
import com.senmesh.sfile.constant.RootTypeEnum;
|
import com.senmesh.sfile.constant.RootTypeEnum;
|
||||||
@@ -18,9 +21,12 @@ import com.senmesh.sfile.dto.UserFileDTO;
|
|||||||
import com.senmesh.sfile.dto.UserFileModifyDTO;
|
import com.senmesh.sfile.dto.UserFileModifyDTO;
|
||||||
import com.senmesh.sfile.dto.UserFileModifyStatusDTO;
|
import com.senmesh.sfile.dto.UserFileModifyStatusDTO;
|
||||||
import com.senmesh.sfile.dto.UserFileQueryDTO;
|
import com.senmesh.sfile.dto.UserFileQueryDTO;
|
||||||
|
import com.senmesh.sfile.dto.UserFileRecordDTO;
|
||||||
|
import com.senmesh.sfile.dto.UserFileRecordQueryDTO;
|
||||||
import com.senmesh.sfile.dto.UserFileStatisticsDTO;
|
import com.senmesh.sfile.dto.UserFileStatisticsDTO;
|
||||||
import com.senmesh.sfile.entity.UserFile;
|
import com.senmesh.sfile.entity.UserFile;
|
||||||
import com.senmesh.sfile.service.UserFileService;
|
import com.senmesh.sfile.service.UserFileService;
|
||||||
|
import com.google.common.collect.Sets;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -30,9 +36,11 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 用户文件ServiceImpl
|
* 用户文件ServiceImpl
|
||||||
@@ -47,6 +55,9 @@ public class UserFileServiceImpl implements UserFileService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private FileService fileService;
|
private FileService fileService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FileProperties fileProperties;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public UserFileDTO create(UserFileCreateDTO createDTO) {
|
public UserFileDTO create(UserFileCreateDTO createDTO) {
|
||||||
@@ -346,5 +357,85 @@ public class UserFileServiceImpl implements UserFileService {
|
|||||||
queryDTO.disablePage();
|
queryDTO.disablePage();
|
||||||
return userFileMapper.statisticsByCategory(queryDTO);
|
return userFileMapper.statisticsByCategory(queryDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void pageRecordByPhone(UserFileRecordQueryDTO queryDTO) {
|
||||||
|
Assert.isTrue(UserContextHolder.isSuperior(), "当前用户无操作权限");
|
||||||
|
Assert.notBlank(queryDTO.getPhone(), "手机号不能为空");
|
||||||
|
queryDTO.setPhone(queryDTO.getPhone().trim());
|
||||||
|
if (queryDTO.getRecordType() != null) {
|
||||||
|
Assert.notNull(FileRecordTypeEnum.getByType(queryDTO.getRecordType()),
|
||||||
|
"记录类型不合法,仅支持1上传/2发送/3接收");
|
||||||
|
}
|
||||||
|
queryDTO.setRootTypeIn(resolveRootTypes(queryDTO.getRecordType()));
|
||||||
|
List<UserFileRecordDTO> records = userFileMapper.listRecordByPhone(queryDTO);
|
||||||
|
fillRecordType(records);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录类型 -> root_type 条件
|
||||||
|
* 上传记录固定为普通(1);发送/接收方向与本端部署位置有关:
|
||||||
|
* 内网侧发送为内穿外(2)、接收为外传内(3);外网侧相反
|
||||||
|
*
|
||||||
|
* @param recordType 记录类型,为空查全部
|
||||||
|
*/
|
||||||
|
private Set<String> resolveRootTypes(Integer recordType) {
|
||||||
|
if (recordType == null) {
|
||||||
|
return Sets.newHashSet(RootTypeEnum.NORMAL.getType(), getSendRootType(), getReceiveRootType());
|
||||||
|
}
|
||||||
|
switch (FileRecordTypeEnum.getByType(recordType)) {
|
||||||
|
case UPLOAD:
|
||||||
|
return Collections.singleton(RootTypeEnum.NORMAL.getType());
|
||||||
|
case SEND:
|
||||||
|
return Collections.singleton(getSendRootType());
|
||||||
|
case RECEIVE:
|
||||||
|
return Collections.singleton(getReceiveRootType());
|
||||||
|
default:
|
||||||
|
return resolveRootTypes(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本端发送方向:内网侧为内穿外(2),外网侧为外传内(3),与TransferUserFileServiceImpl保持一致
|
||||||
|
*/
|
||||||
|
private String getSendRootType() {
|
||||||
|
return FileLocationEnum.isInternal(fileProperties.getTransfer().getLocation())
|
||||||
|
? RootTypeEnum.INTERNAL_TO_EXTERNAL.getType()
|
||||||
|
: RootTypeEnum.EXTERNAL_TO_INTERNAL.getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本端接收方向:内网侧为外传内(3),外网侧为内穿外(2),与TransferFileMonitorServiceImpl保持一致
|
||||||
|
*/
|
||||||
|
private String getReceiveRootType() {
|
||||||
|
return FileLocationEnum.isInternal(fileProperties.getTransfer().getLocation())
|
||||||
|
? RootTypeEnum.EXTERNAL_TO_INTERNAL.getType()
|
||||||
|
: RootTypeEnum.INTERNAL_TO_EXTERNAL.getType();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按本端部署位置回填记录类型与描述
|
||||||
|
*/
|
||||||
|
private void fillRecordType(List<UserFileRecordDTO> records) {
|
||||||
|
if (records == null || records.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (UserFileRecordDTO record : records) {
|
||||||
|
String rootType = record.getRootType();
|
||||||
|
if (RootTypeEnum.NORMAL.getType().equals(rootType)) {
|
||||||
|
record.setRecordType(FileRecordTypeEnum.UPLOAD.getType());
|
||||||
|
} else if (getSendRootType().equals(rootType)) {
|
||||||
|
record.setRecordType(FileRecordTypeEnum.SEND.getType());
|
||||||
|
} else if (getReceiveRootType().equals(rootType)) {
|
||||||
|
record.setRecordType(FileRecordTypeEnum.RECEIVE.getType());
|
||||||
|
}
|
||||||
|
if (record.getRecordType() != null) {
|
||||||
|
record.setRecordTypeDesc(FileRecordTypeEnum.getByType(record.getRecordType()).getDesc());
|
||||||
|
} else {
|
||||||
|
RootTypeEnum rootTypeEnum = RootTypeEnum.getByType(rootType);
|
||||||
|
record.setRecordTypeDesc(rootTypeEnum == null ? rootType : rootTypeEnum.getDesc());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -275,4 +275,30 @@ public class UserController {
|
|||||||
public AlitaResult<List<UserDTO>> listByRealNameOrPhone(@RequestParam("realNames") Set<String> realNames, @RequestParam("phones") Set<String> phones) {
|
public AlitaResult<List<UserDTO>> listByRealNameOrPhone(@RequestParam("realNames") Set<String> realNames, @RequestParam("phones") Set<String> phones) {
|
||||||
return AlitaResult.success(userService.listByRealNameOrPhone(realNames,phones));
|
return AlitaResult.success(userService.listByRealNameOrPhone(realNames,phones));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("user/enable")
|
||||||
|
@ApiOperation("启用用户(系统管理员)")
|
||||||
|
public AlitaResult<Boolean> enableUser(@Validated @RequestBody UserStatusOperateDTO reqDTO) {
|
||||||
|
return AlitaResult.success(userService.enableUser(reqDTO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("user/disable")
|
||||||
|
@ApiOperation("禁用用户(系统管理员)")
|
||||||
|
public AlitaResult<Boolean> disableUser(@Validated @RequestBody UserStatusOperateDTO reqDTO) {
|
||||||
|
return AlitaResult.success(userService.disableUser(reqDTO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("user/admin_cancel")
|
||||||
|
@ApiOperation("注销用户(系统管理员,直接注销不进入注销申请流程)")
|
||||||
|
public AlitaResult<Void> adminCancelUser(@Validated @RequestBody UserStatusOperateDTO reqDTO) {
|
||||||
|
userService.adminCancelUser(reqDTO);
|
||||||
|
return AlitaResult.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("user/page/org_account")
|
||||||
|
@ApiOperation("分页查询机构账号信息(系统管理员)")
|
||||||
|
public AlitaResult<Page<OrgAccountDTO>> pageOrgAccount(OrgAccountQueryDTO queryDTO) {
|
||||||
|
userService.pageOrgAccount(queryDTO);
|
||||||
|
return AlitaResult.success(queryDTO.getPageResult());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,4 +190,21 @@ public interface UserMapper extends MyMapper<User> {
|
|||||||
* @return 用户列表
|
* @return 用户列表
|
||||||
*/
|
*/
|
||||||
List<UserDTO> listUncanceledByExactPhoneAndType(@Param("phone") String phone, @Param("type") Integer type);
|
List<UserDTO> listUncanceledByExactPhoneAndType(@Param("phone") String phone, @Param("type") Integer type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员注销用户(状态置为已注销,并禁用账号)
|
||||||
|
*
|
||||||
|
* @param userId 用户id
|
||||||
|
* @param operateUserId 操作用户id
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
int adminCancel(@Param("userId") Long userId, @Param("operateUserId") Long operateUserId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询机构账号信息(系统管理员)
|
||||||
|
*
|
||||||
|
* @param queryDTO 查询dto
|
||||||
|
* @return {@link List<OrgAccountDTO>}
|
||||||
|
*/
|
||||||
|
List<OrgAccountDTO> listOrgAccount(@Param("param") OrgAccountQueryDTO queryDTO);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package com.senmesh.usercenter.user.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 机构账号信息DTO
|
||||||
|
* 一行代表一个机构下的一个账号(用户可在多个机构,同一账号会在多个机构下各出现一行)
|
||||||
|
*
|
||||||
|
* @author liu on 2026-09-14
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@ApiModel(description = "机构账号信息")
|
||||||
|
public class OrgAccountDTO implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@ApiModelProperty("机构id")
|
||||||
|
private Long orgId;
|
||||||
|
|
||||||
|
@ApiModelProperty("机构名称")
|
||||||
|
private String orgName;
|
||||||
|
|
||||||
|
@ApiModelProperty("是否该机构的管理员")
|
||||||
|
private Boolean administrator;
|
||||||
|
|
||||||
|
@ApiModelProperty("用户id")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
@ApiModelProperty("账号")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@ApiModelProperty("昵称")
|
||||||
|
private String nickname;
|
||||||
|
|
||||||
|
@ApiModelProperty("真实姓名")
|
||||||
|
private String realName;
|
||||||
|
|
||||||
|
@ApiModelProperty("手机号")
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@ApiModelProperty("邮箱")
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@ApiModelProperty("用户类型:1企业用户,2个人用户")
|
||||||
|
private Integer userType;
|
||||||
|
|
||||||
|
@ApiModelProperty("是否启用")
|
||||||
|
private Boolean enabled;
|
||||||
|
|
||||||
|
@ApiModelProperty("状态:1待激活,2已激活,3已注销")
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
@ApiModelProperty("创建时间")
|
||||||
|
@JsonFormat(
|
||||||
|
shape = JsonFormat.Shape.STRING,
|
||||||
|
pattern = "yyyy-MM-dd HH:mm:ss",
|
||||||
|
timezone = "GMT+8"
|
||||||
|
)
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package com.senmesh.usercenter.user.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.senmesh.common.dto.PageQueryDTO;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 机构账号信息查询DTO
|
||||||
|
*
|
||||||
|
* @author liu on 2026-09-14
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@ApiModel(description = "机构账号信息查询")
|
||||||
|
public class OrgAccountQueryDTO extends PageQueryDTO<OrgAccountDTO> {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@ApiModelProperty("机构id(不传查全部机构)")
|
||||||
|
private Long orgId;
|
||||||
|
|
||||||
|
@ApiModelProperty("机构名称(模糊查询)")
|
||||||
|
private String orgNameLike;
|
||||||
|
|
||||||
|
@ApiModelProperty("账号(模糊查询)")
|
||||||
|
private String usernameLike;
|
||||||
|
|
||||||
|
@ApiModelProperty("真实姓名(模糊查询)")
|
||||||
|
private String realNameLike;
|
||||||
|
|
||||||
|
@ApiModelProperty("手机号(模糊查询)")
|
||||||
|
private String phoneLike;
|
||||||
|
|
||||||
|
@ApiModelProperty("是否启用")
|
||||||
|
private Boolean enabled;
|
||||||
|
|
||||||
|
@ApiModelProperty("状态:1待激活,2已激活,3已注销")
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
@ApiModelProperty("创建时间起")
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@JsonFormat(
|
||||||
|
shape = JsonFormat.Shape.STRING,
|
||||||
|
pattern = "yyyy-MM-dd HH:mm:ss",
|
||||||
|
timezone = "GMT+8"
|
||||||
|
)
|
||||||
|
private Date createTimeStart;
|
||||||
|
|
||||||
|
@ApiModelProperty("创建时间止")
|
||||||
|
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
@JsonFormat(
|
||||||
|
shape = JsonFormat.Shape.STRING,
|
||||||
|
pattern = "yyyy-MM-dd HH:mm:ss",
|
||||||
|
timezone = "GMT+8"
|
||||||
|
)
|
||||||
|
private Date createTimeEnd;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.senmesh.usercenter.user.dto;
|
||||||
|
|
||||||
|
import com.senmesh.common.dto.BaseOperateDTO;
|
||||||
|
|
||||||
|
import io.swagger.annotations.ApiModel;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户启用/禁用/注销操作DTO
|
||||||
|
*
|
||||||
|
* @author liu on 2026-09-14
|
||||||
|
*/
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
@ApiModel(description = "用户启用/禁用/注销操作")
|
||||||
|
public class UserStatusOperateDTO extends BaseOperateDTO {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@ApiModelProperty("用户id")
|
||||||
|
@NotNull(message = "用户id不能为空")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -584,5 +584,69 @@
|
|||||||
and status != 3
|
and status != 3
|
||||||
</update>
|
</update>
|
||||||
|
|
||||||
|
<update id="adminCancel">
|
||||||
|
update uc_user
|
||||||
|
set status = 3,
|
||||||
|
enabled = FALSE,
|
||||||
|
update_by = #{operateUserId},
|
||||||
|
update_time = now()
|
||||||
|
where id = #{userId}
|
||||||
|
and deleted = FALSE
|
||||||
|
and status != 3
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<select id="listOrgAccount" resultType="com.senmesh.usercenter.user.dto.OrgAccountDTO">
|
||||||
|
SELECT
|
||||||
|
ou.org_id,
|
||||||
|
o.org_name,
|
||||||
|
ou.administrator,
|
||||||
|
u.id AS user_id,
|
||||||
|
u.username,
|
||||||
|
u.nickname,
|
||||||
|
u.real_name,
|
||||||
|
u.phone,
|
||||||
|
u.email,
|
||||||
|
u.type AS user_type,
|
||||||
|
u.enabled,
|
||||||
|
u.status,
|
||||||
|
u.create_time
|
||||||
|
FROM uc_org_user ou
|
||||||
|
INNER JOIN uc_user u ON u.id = ou.user_id AND u.deleted = FALSE AND u.type != 3
|
||||||
|
LEFT JOIN xs_org o ON o.id = ou.org_id AND o.deleted = FALSE
|
||||||
|
WHERE ou.deleted = FALSE
|
||||||
|
<if test="param.orgId != null">
|
||||||
|
AND ou.org_id = #{param.orgId}
|
||||||
|
</if>
|
||||||
|
<if test="param.orgNameLike != null and param.orgNameLike != ''">
|
||||||
|
<bind name="orgNameLikeBind" value="'%' + param.orgNameLike + '%'"/>
|
||||||
|
AND o.org_name LIKE #{orgNameLikeBind}
|
||||||
|
</if>
|
||||||
|
<if test="param.usernameLike != null and param.usernameLike != ''">
|
||||||
|
<bind name="usernameLikeBind" value="'%' + param.usernameLike + '%'"/>
|
||||||
|
AND u.username LIKE #{usernameLikeBind}
|
||||||
|
</if>
|
||||||
|
<if test="param.realNameLike != null and param.realNameLike != ''">
|
||||||
|
<bind name="realNameLikeBind" value="'%' + param.realNameLike + '%'"/>
|
||||||
|
AND u.real_name LIKE #{realNameLikeBind}
|
||||||
|
</if>
|
||||||
|
<if test="param.phoneLike != null and param.phoneLike != ''">
|
||||||
|
<bind name="phoneLikeBind" value="'%' + param.phoneLike + '%'"/>
|
||||||
|
AND u.phone LIKE #{phoneLikeBind}
|
||||||
|
</if>
|
||||||
|
<if test="param.enabled != null">
|
||||||
|
AND u.enabled = #{param.enabled}
|
||||||
|
</if>
|
||||||
|
<if test="param.status != null">
|
||||||
|
AND u.status = #{param.status}
|
||||||
|
</if>
|
||||||
|
<if test="param.createTimeStart != null">
|
||||||
|
AND u.create_time >= #{param.createTimeStart}
|
||||||
|
</if>
|
||||||
|
<if test="param.createTimeEnd != null">
|
||||||
|
AND u.create_time <= #{param.createTimeEnd}
|
||||||
|
</if>
|
||||||
|
ORDER BY ou.org_id ASC, CASE WHEN ou.administrator THEN 1 ELSE 0 END DESC, u.id DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -335,4 +335,35 @@ public interface UserService {
|
|||||||
*/
|
*/
|
||||||
List<UserDTO> listByRealNameOrPhone(Set<String> realNames, Set<String> phones);
|
List<UserDTO> listByRealNameOrPhone(Set<String> realNames, Set<String> phones);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启用用户(系统管理员)
|
||||||
|
*
|
||||||
|
* @param reqDTO 操作dto
|
||||||
|
* @return {@link Boolean}
|
||||||
|
*/
|
||||||
|
Boolean enableUser(UserStatusOperateDTO reqDTO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 禁用用户(系统管理员)
|
||||||
|
*
|
||||||
|
* @param reqDTO 操作dto
|
||||||
|
* @return {@link Boolean}
|
||||||
|
*/
|
||||||
|
Boolean disableUser(UserStatusOperateDTO reqDTO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注销用户(系统管理员,直接注销不进入注销申请流程)
|
||||||
|
*
|
||||||
|
* @param reqDTO 操作dto
|
||||||
|
*/
|
||||||
|
void adminCancelUser(UserStatusOperateDTO reqDTO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询机构账号信息(系统管理员)
|
||||||
|
* 一行代表一个机构下的一个账号
|
||||||
|
*
|
||||||
|
* @param queryDTO 查询dto
|
||||||
|
*/
|
||||||
|
void pageOrgAccount(OrgAccountQueryDTO queryDTO);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import com.senmesh.mq.util.MqSendUtils;
|
|||||||
import com.senmesh.security.dto.UserInfoDTO;
|
import com.senmesh.security.dto.UserInfoDTO;
|
||||||
import com.senmesh.security.jwt.JwtTokenUtils;
|
import com.senmesh.security.jwt.JwtTokenUtils;
|
||||||
import com.senmesh.security.util.PasswordUtils;
|
import com.senmesh.security.util.PasswordUtils;
|
||||||
|
import com.senmesh.security.util.UserDisabledCache;
|
||||||
import com.senmesh.usercenter.auth.constant.AuthTypeEnum;
|
import com.senmesh.usercenter.auth.constant.AuthTypeEnum;
|
||||||
import com.senmesh.usercenter.auth.dto.AuthDetailBindDTO;
|
import com.senmesh.usercenter.auth.dto.AuthDetailBindDTO;
|
||||||
import com.senmesh.usercenter.auth.dto.AuthDetailLoginDTO;
|
import com.senmesh.usercenter.auth.dto.AuthDetailLoginDTO;
|
||||||
@@ -53,6 +54,8 @@ import com.senmesh.usercenter.user.dto.ChangePhoneMqDTO;
|
|||||||
import com.senmesh.usercenter.user.dto.CheckSecurityCodeReqDTO;
|
import com.senmesh.usercenter.user.dto.CheckSecurityCodeReqDTO;
|
||||||
import com.senmesh.usercenter.user.dto.InitUserPasswordDTO;
|
import com.senmesh.usercenter.user.dto.InitUserPasswordDTO;
|
||||||
import com.senmesh.usercenter.user.dto.ModifyPasswordDTO;
|
import com.senmesh.usercenter.user.dto.ModifyPasswordDTO;
|
||||||
|
import com.senmesh.usercenter.user.dto.OrgAccountDTO;
|
||||||
|
import com.senmesh.usercenter.user.dto.OrgAccountQueryDTO;
|
||||||
import com.senmesh.usercenter.user.dto.OrgUserCreateDTO;
|
import com.senmesh.usercenter.user.dto.OrgUserCreateDTO;
|
||||||
import com.senmesh.usercenter.user.dto.OrgUserModifyDTO;
|
import com.senmesh.usercenter.user.dto.OrgUserModifyDTO;
|
||||||
import com.senmesh.usercenter.user.dto.OrgUserOperateDTO;
|
import com.senmesh.usercenter.user.dto.OrgUserOperateDTO;
|
||||||
@@ -78,6 +81,7 @@ import com.senmesh.usercenter.user.dto.UserExtensionOperateDTO;
|
|||||||
import com.senmesh.usercenter.user.dto.UserOperateDTO;
|
import com.senmesh.usercenter.user.dto.UserOperateDTO;
|
||||||
import com.senmesh.usercenter.user.dto.UserQueryByRoleDTO;
|
import com.senmesh.usercenter.user.dto.UserQueryByRoleDTO;
|
||||||
import com.senmesh.usercenter.user.dto.UserQueryDTO;
|
import com.senmesh.usercenter.user.dto.UserQueryDTO;
|
||||||
|
import com.senmesh.usercenter.user.dto.UserStatusOperateDTO;
|
||||||
import com.senmesh.usercenter.user.entity.User;
|
import com.senmesh.usercenter.user.entity.User;
|
||||||
import com.senmesh.usercenter.user.login.dto.UserDetailDTO;
|
import com.senmesh.usercenter.user.login.dto.UserDetailDTO;
|
||||||
import com.senmesh.usercenter.user.mapstruct.UserMapMapper;
|
import com.senmesh.usercenter.user.mapstruct.UserMapMapper;
|
||||||
@@ -498,6 +502,8 @@ public class UserServiceImpl implements UserService {
|
|||||||
updateInfo.setStatus(UserStatusEnum.CLOSED.getStatus());
|
updateInfo.setStatus(UserStatusEnum.CLOSED.getStatus());
|
||||||
updateInfo.setEnabled(Boolean.FALSE);
|
updateInfo.setEnabled(Boolean.FALSE);
|
||||||
userMapper.updateEntitySelective(updateInfo, UserContextHolder.getLoginUserId());
|
userMapper.updateEntitySelective(updateInfo, UserContextHolder.getLoginUserId());
|
||||||
|
// 使该用户已发放的token立即失效
|
||||||
|
UserDisabledCache.mark(userDTO.getId(), JwtTokenUtils.getExpiration());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -991,4 +997,93 @@ public class UserServiceImpl implements UserService {
|
|||||||
}
|
}
|
||||||
return userMapper.listByRealNameOrPhone(UserContextHolder.getExistLoginOrgId(), realNames, phones);
|
return userMapper.listByRealNameOrPhone(UserContextHolder.getExistLoginOrgId(), realNames, phones);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public Boolean enableUser(UserStatusOperateDTO reqDTO) {
|
||||||
|
// 平台超管可操作全部用户,机构管理员仅可启用本机构用户
|
||||||
|
Assert.isTrue(UserContextHolder.isSuperior() || UserContextHolder.isAdministrator(),
|
||||||
|
"当前用户无操作权限");
|
||||||
|
this.checkOrgScopeOfOperateUser(reqDTO.getUserId());
|
||||||
|
User user = userMapper.getById(reqDTO.getUserId());
|
||||||
|
Assert.notNull(user, "用户不存在");
|
||||||
|
Assert.isTrue(!UserStatusEnum.CLOSED.getStatus().equals(user.getStatus()), "已注销账号不可启用");
|
||||||
|
// 幂等:已启用直接返回
|
||||||
|
if (Boolean.TRUE.equals(user.getEnabled())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
User updateInfo = User.builder().id(user.getId()).enabled(Boolean.TRUE).build();
|
||||||
|
boolean updated = userMapper.updateEntitySelective(updateInfo, UserContextHolder.getLoginUserId()) == 1;
|
||||||
|
if (updated) {
|
||||||
|
UserDisabledCache.unmark(user.getId());
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public Boolean disableUser(UserStatusOperateDTO reqDTO) {
|
||||||
|
// 平台超管可操作全部用户,机构管理员仅可禁用本机构用户
|
||||||
|
Assert.isTrue(UserContextHolder.isSuperior() || UserContextHolder.isAdministrator(),
|
||||||
|
"当前用户无操作权限");
|
||||||
|
Long operateUserId = UserContextHolder.getExistLoginUserId();
|
||||||
|
Long userId = reqDTO.getUserId();
|
||||||
|
Assert.isTrue(!userId.equals(operateUserId), "不能禁用当前登录账号");
|
||||||
|
this.checkOrgScopeOfOperateUser(userId);
|
||||||
|
User user = userMapper.getById(userId);
|
||||||
|
Assert.notNull(user, "用户不存在");
|
||||||
|
Assert.isTrue(!Boolean.TRUE.equals(user.getSupeiror()), "不能禁用超级管理员账号");
|
||||||
|
Assert.isTrue(!UserStatusEnum.CLOSED.getStatus().equals(user.getStatus()), "已注销账号无需禁用");
|
||||||
|
// 幂等:已禁用直接返回
|
||||||
|
if (Boolean.FALSE.equals(user.getEnabled())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
User updateInfo = User.builder().id(user.getId()).enabled(Boolean.FALSE).build();
|
||||||
|
boolean updated = userMapper.updateEntitySelective(updateInfo, operateUserId) == 1;
|
||||||
|
if (updated) {
|
||||||
|
// 使该用户已发放的token立即失效
|
||||||
|
UserDisabledCache.mark(user.getId(), JwtTokenUtils.getExpiration());
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 非平台超管(机构管理员)操作他人账号时,校验目标用户属于本机构
|
||||||
|
*/
|
||||||
|
private void checkOrgScopeOfOperateUser(Long userId) {
|
||||||
|
if (!UserContextHolder.isSuperior()) {
|
||||||
|
Assert.isTrue(
|
||||||
|
orgUserService.existOrgUser(UserContextHolder.getExistLoginOrgId(), userId),
|
||||||
|
"只能操作本机构的用户");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@AutoUnLock
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void adminCancelUser(UserStatusOperateDTO reqDTO) {
|
||||||
|
Assert.isTrue(UserContextHolder.isSuperior(), "当前用户无操作权限");
|
||||||
|
Long operateUserId = UserContextHolder.getExistLoginUserId();
|
||||||
|
Long userId = reqDTO.getUserId();
|
||||||
|
Assert.isTrue(!userId.equals(operateUserId), "不能注销当前登录账号");
|
||||||
|
Assert.isTrue(RedissonUtils.tryLockAutomatically(
|
||||||
|
LockDTO.builder().methodName("adminCancelUser").arg(String.valueOf(userId)).build()),
|
||||||
|
"该用户正在注销,请勿重复操作");
|
||||||
|
User user = userMapper.getById(userId);
|
||||||
|
Assert.notNull(user, "用户不存在");
|
||||||
|
Assert.isTrue(!Boolean.TRUE.equals(user.getSupeiror()), "不能注销超级管理员账号");
|
||||||
|
// 幂等:已注销直接返回
|
||||||
|
if (UserStatusEnum.CLOSED.getStatus().equals(user.getStatus())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Assert.isTrue(userMapper.adminCancel(userId, operateUserId) == 1, "注销失败,请刷新后重试");
|
||||||
|
// 使该用户已发放的token立即失效
|
||||||
|
UserDisabledCache.mark(userId, JwtTokenUtils.getExpiration());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void pageOrgAccount(OrgAccountQueryDTO queryDTO) {
|
||||||
|
Assert.isTrue(UserContextHolder.isSuperior(), "当前用户无操作权限");
|
||||||
|
userMapper.listOrgAccount(queryDTO);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user