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:
@@ -63,5 +63,8 @@ public interface CommonConstants {
|
||||
|
||||
/** 平台机构id **/
|
||||
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 java.io.Serializable;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -75,7 +77,14 @@ public class UserInfoDTO implements Serializable{
|
||||
|
||||
public void setPassword(String 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.senmesh.common.util.StrUtils;
|
||||
import com.senmesh.common.util.UserContextHolder;
|
||||
import com.senmesh.security.config.SecurityProperties;
|
||||
import com.senmesh.security.util.HostContextHolder;
|
||||
import com.senmesh.security.util.UserDisabledCache;
|
||||
import com.senmesh.usercenter.constants.OrgConstants;
|
||||
import com.senmesh.usercenter.dto.IUserDetail;
|
||||
|
||||
@@ -67,9 +68,14 @@ public class JwtAuthenticationTokenFilter extends GenericFilterBean {
|
||||
userDto = JwtTokenUtils.getUserLoginDtoFromToken(cookieToken);
|
||||
}
|
||||
if (userDto != null) {
|
||||
SecurityContextHolder.getContext()
|
||||
.setAuthentication(JwtTokenUtils.getAuthentication(userDto));
|
||||
UserContextHolder.setContext(userDto);
|
||||
// 禁用/注销用户的已发放token立即失效(黑名单命中则不建立登录上下文)
|
||||
if (UserDisabledCache.isMarked(userDto.getId())) {
|
||||
log.debug("user is disabled or canceled, token rejected, userId:{}", userDto.getId());
|
||||
} else {
|
||||
SecurityContextHolder.getContext()
|
||||
.setAuthentication(JwtTokenUtils.getAuthentication(userDto));
|
||||
UserContextHolder.setContext(userDto);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.UserFileModifyStatusDTO;
|
||||
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.service.UserFileService;
|
||||
import com.senmesh.tkmybatis.pojo.Page;
|
||||
@@ -115,6 +117,13 @@ public class UserFileController {
|
||||
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.UserFileQueryDTO;
|
||||
import com.senmesh.sfile.dto.UserFileRecordDTO;
|
||||
import com.senmesh.sfile.dto.UserFileRecordQueryDTO;
|
||||
import com.senmesh.sfile.dto.UserFileStatisticsDTO;
|
||||
import com.senmesh.sfile.entity.UserFile;
|
||||
import com.senmesh.tkmybatis.mybatis.MyMapper;
|
||||
@@ -64,5 +66,13 @@ public interface UserFileMapper extends MyMapper<UserFile> {
|
||||
* @return List<UserFileStatisticsDTO>
|
||||
*/
|
||||
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
|
||||
</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
|
||||
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.UserFileModifyStatusDTO;
|
||||
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 java.util.List;
|
||||
@@ -128,5 +130,13 @@ public interface UserFileService {
|
||||
* @return List<UserFileStatisticsDTO>
|
||||
*/
|
||||
List<UserFileStatisticsDTO> statisticsByCategory(UserFileQueryDTO queryDTO);
|
||||
|
||||
/**
|
||||
* 按手机号分页查询文件记录(上传/发送/接收,系统管理员)
|
||||
* 记录类型与本端部署位置有关:内网侧发送为内穿外(2)、接收为外传内(3);外网侧相反
|
||||
*
|
||||
* @param queryDTO 查询DTO
|
||||
*/
|
||||
void pageRecordByPhone(UserFileRecordQueryDTO queryDTO);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
package com.senmesh.sfile.service.impl;
|
||||
|
||||
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.FileTypeEnum;
|
||||
import com.senmesh.sfile.constant.RootTypeEnum;
|
||||
import com.senmesh.sfile.dto.UserFileCreateDTO;
|
||||
import com.senmesh.sfile.dto.UserFileDTO;
|
||||
import com.senmesh.sfile.service.NormalUserFileService;
|
||||
import com.senmesh.sfile.service.UploadConfigService;
|
||||
import com.senmesh.sfile.service.UserFileService;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -20,6 +25,12 @@ public class NormalUserFileServiceImpl implements NormalUserFileService {
|
||||
@Autowired
|
||||
private UserFileService userFileService;
|
||||
|
||||
@Autowired
|
||||
private UploadConfigService uploadConfigService;
|
||||
|
||||
@Autowired
|
||||
private FileRecordService fileRecordService;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public UserFileDTO create(UserFileCreateDTO createDTO) {
|
||||
@@ -28,6 +39,16 @@ public class NormalUserFileServiceImpl implements NormalUserFileService {
|
||||
createDTO.setRootType(RootTypeEnum.NORMAL.getType());
|
||||
// TODO 扫描文件审核
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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.util.ListUtil;
|
||||
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.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.FileTypeEnum;
|
||||
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.UserFileModifyStatusDTO;
|
||||
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.entity.UserFile;
|
||||
import com.senmesh.sfile.service.UserFileService;
|
||||
import com.google.common.collect.Sets;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -30,9 +36,11 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import java.io.InputStream;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 用户文件ServiceImpl
|
||||
@@ -47,6 +55,9 @@ public class UserFileServiceImpl implements UserFileService {
|
||||
@Autowired
|
||||
private FileService fileService;
|
||||
|
||||
@Autowired
|
||||
private FileProperties fileProperties;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public UserFileDTO create(UserFileCreateDTO createDTO) {
|
||||
@@ -346,5 +357,85 @@ public class UserFileServiceImpl implements UserFileService {
|
||||
queryDTO.disablePage();
|
||||
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) {
|
||||
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 用户列表
|
||||
*/
|
||||
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
|
||||
</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>
|
||||
|
||||
@@ -335,4 +335,35 @@ public interface UserService {
|
||||
*/
|
||||
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.jwt.JwtTokenUtils;
|
||||
import com.senmesh.security.util.PasswordUtils;
|
||||
import com.senmesh.security.util.UserDisabledCache;
|
||||
import com.senmesh.usercenter.auth.constant.AuthTypeEnum;
|
||||
import com.senmesh.usercenter.auth.dto.AuthDetailBindDTO;
|
||||
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.InitUserPasswordDTO;
|
||||
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.OrgUserModifyDTO;
|
||||
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.UserQueryByRoleDTO;
|
||||
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.login.dto.UserDetailDTO;
|
||||
import com.senmesh.usercenter.user.mapstruct.UserMapMapper;
|
||||
@@ -498,6 +502,8 @@ public class UserServiceImpl implements UserService {
|
||||
updateInfo.setStatus(UserStatusEnum.CLOSED.getStatus());
|
||||
updateInfo.setEnabled(Boolean.FALSE);
|
||||
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);
|
||||
}
|
||||
|
||||
@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