Merge branch 'master' of https://gitea.zianxin.com/zianxin/pj_service_cloud
# Conflicts: # pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysClientController.java
This commit is contained in:
commit
2c1bf9b967
14 files changed
+880
-271
No files matched your search
+57
@@ -0,0 +1,57 @@
|
||||
package com.bsd.pjcloud.common.core.util;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
|
||||
/**
|
||||
* 弱口令校验:禁止密码与手机号/用户名相同等低强度规则。
|
||||
*/
|
||||
public final class PasswordWeakCheckUtil {
|
||||
|
||||
private PasswordWeakCheckUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param rawPassword 明文密码
|
||||
* @param phone 手机号(可空)
|
||||
* @param username 用户名(可空)
|
||||
* @return 不通过时的中文原因;通过返回 null
|
||||
*/
|
||||
public static String validate(String rawPassword, String phone, String username) {
|
||||
if (StrUtil.isBlank(rawPassword)) {
|
||||
return "密码不能为空";
|
||||
}
|
||||
String pwd = StrUtil.trim(rawPassword);
|
||||
String phoneTrim = StrUtil.trim(phone);
|
||||
String userTrim = StrUtil.trim(username);
|
||||
if (StrUtil.isNotBlank(phoneTrim) && pwd.equals(phoneTrim)) {
|
||||
return "密码不能与手机号相同";
|
||||
}
|
||||
if (StrUtil.isNotBlank(userTrim) && pwd.equalsIgnoreCase(userTrim)) {
|
||||
return "密码不能与用户名相同";
|
||||
}
|
||||
if (pwd.length() < 8) {
|
||||
return "密码长度不能少于8位";
|
||||
}
|
||||
boolean hasLetter = false;
|
||||
boolean hasDigit = false;
|
||||
for (int i = 0; i < pwd.length(); i++) {
|
||||
char c = pwd.charAt(i);
|
||||
if (Character.isLetter(c)) {
|
||||
hasLetter = true;
|
||||
} else if (Character.isDigit(c)) {
|
||||
hasDigit = true;
|
||||
}
|
||||
}
|
||||
if (!hasLetter || !hasDigit) {
|
||||
return "密码必须同时包含字母和数字";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void assertStrong(String rawPassword, String phone, String username) {
|
||||
String err = validate(rawPassword, phone, username);
|
||||
if (err != null) {
|
||||
throw new IllegalArgumentException(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
sys.user.update.passwordError=\u539F\u5BC6\u7801\u9519\u8BEF\uFF0C\u4FEE\u6539\u5931\u8D25
|
||||
sys.user.query.error=\u83B7\u53D6\u5F53\u524D\u7528\u6237\u4FE1\u606F\u5931\u8D25
|
||||
sys.user.import.succeed=\u7528\u6237\u5BFC\u5165\u6210\u529F\uFF0C\u9ED8\u8BA4\u5BC6\u7801\u4E3A\u624B\u673A\u53F7
|
||||
sys.user.import.succeed=\u7528\u6237\u5BFC\u5165\u6210\u529F\uFF0C\u9ED8\u8BA4\u5BC6\u7801\u89C1\u7CFB\u7EDF\u53C2\u6570USER_DEFAULT_PASSWORD
|
||||
sys.user.username.existing={0} \u7528\u6237\u540D\u5DF2\u5B58\u5728
|
||||
sys.user.phone.existing={0} \u624b\u673a\u53f7\u5df2\u5b58\u5728
|
||||
sys.user.userInfo.empty={0} \u7528\u6237\u4FE1\u606F\u4E3A\u7A7A
|
||||
|
||||
+84
-53
@@ -35,76 +35,107 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author pjcloud
|
||||
* @date 2019-06-24
|
||||
* <p>
|
||||
* 解决Mybatis Plus Order By SQL注入问题
|
||||
* 解决Mybatis Plus Order By SQL注入问题:ascs/descs 仅允许简单列名白名单,禁止表达式/函数注入。
|
||||
*/
|
||||
@Slf4j
|
||||
public class SqlFilterArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
/**
|
||||
* 排序字段白名单:仅允许标识符(可选表别名.列名),禁止空格、括号、逗号、注释等。
|
||||
* 例:create_time、user_id、u.createTime
|
||||
*/
|
||||
private static final Pattern SAFE_ORDER_COLUMN = Pattern.compile(
|
||||
"^[a-zA-Z_][a-zA-Z0-9_]{0,63}(\\.[a-zA-Z_][a-zA-Z0-9_]{0,63})?$");
|
||||
|
||||
/**
|
||||
* 判断Controller是否包含page 参数
|
||||
*
|
||||
* @param parameter 参数
|
||||
* @return 是否过滤
|
||||
*/
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return parameter.getParameterType().equals(Page.class);
|
||||
}
|
||||
/** 单次请求最多允许的排序列数量,防止恶意堆叠。 */
|
||||
private static final int MAX_ORDER_COLUMNS = 5;
|
||||
|
||||
/**
|
||||
* @param parameter 入参集合
|
||||
* @param mavContainer model 和 view
|
||||
* @param webRequest web相关
|
||||
* @param binderFactory 入参解析
|
||||
* @return 检查后新的page对象
|
||||
* <p>
|
||||
* page 只支持查询 GET .如需解析POST获取请求报文体处理
|
||||
*/
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
|
||||
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
|
||||
/**
|
||||
* 判断Controller是否包含page 参数
|
||||
*
|
||||
* @param parameter 参数
|
||||
* @return 是否过滤
|
||||
*/
|
||||
@Override
|
||||
public boolean supportsParameter(MethodParameter parameter) {
|
||||
return parameter.getParameterType().equals(Page.class);
|
||||
}
|
||||
|
||||
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
|
||||
/**
|
||||
* @param parameter 入参集合
|
||||
* @param mavContainer model 和 view
|
||||
* @param webRequest web相关
|
||||
* @param binderFactory 入参解析
|
||||
* @return 检查后新的page对象
|
||||
* <p>
|
||||
* page 只支持查询 GET .如需解析POST获取请求报文体处理
|
||||
*/
|
||||
@Override
|
||||
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
|
||||
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
|
||||
|
||||
String ascs = request.getParameter("ascs");
|
||||
String descs = request.getParameter("descs");
|
||||
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
|
||||
|
||||
String current = request.getParameter("current");
|
||||
String size = request.getParameter("size");
|
||||
String ascs = request.getParameter("ascs");
|
||||
String descs = request.getParameter("descs");
|
||||
|
||||
Page page = new Page();
|
||||
if (StrUtil.isNotBlank(current)) {
|
||||
// 如果current page 小于零 视为不合法数据
|
||||
if (CompareUtil.compare(Long.parseLong(current), 0L) < 0) {
|
||||
throw new CheckedException("current page error");
|
||||
}
|
||||
page.setCurrent(Long.parseLong(current));
|
||||
}
|
||||
String current = request.getParameter("current");
|
||||
String size = request.getParameter("size");
|
||||
|
||||
if (StrUtil.isNotBlank(size)) {
|
||||
page.setSize(Long.parseLong(size));
|
||||
}
|
||||
Page page = new Page();
|
||||
if (StrUtil.isNotBlank(current)) {
|
||||
// 如果current page 小于零 视为不合法数据
|
||||
if (CompareUtil.compare(Long.parseLong(current), 0L) < 0) {
|
||||
throw new CheckedException("current page error");
|
||||
}
|
||||
page.setCurrent(Long.parseLong(current));
|
||||
}
|
||||
|
||||
List<OrderItem> orderItemList = new ArrayList<>();
|
||||
Optional.ofNullable(ascs)
|
||||
.ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
|
||||
.filter(asc -> !SqlInjectionUtils.check(asc))
|
||||
.map(OrderItem::asc)
|
||||
.collect(Collectors.toList())));
|
||||
Optional.ofNullable(descs)
|
||||
.ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
|
||||
.filter(desc -> !SqlInjectionUtils.check(desc))
|
||||
.map(OrderItem::desc)
|
||||
.collect(Collectors.toList())));
|
||||
page.addOrder(orderItemList);
|
||||
if (StrUtil.isNotBlank(size)) {
|
||||
page.setSize(Long.parseLong(size));
|
||||
}
|
||||
|
||||
List<OrderItem> orderItemList = new ArrayList<>();
|
||||
Optional.ofNullable(ascs)
|
||||
.ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
|
||||
.map(String::trim)
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.map(col -> toSafeOrderColumn(col, true))
|
||||
.collect(Collectors.toList())));
|
||||
Optional.ofNullable(descs)
|
||||
.ifPresent(s -> orderItemList.addAll(Arrays.stream(s.split(StrUtil.COMMA))
|
||||
.map(String::trim)
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.map(col -> toSafeOrderColumn(col, false))
|
||||
.collect(Collectors.toList())));
|
||||
if (orderItemList.size() > MAX_ORDER_COLUMNS) {
|
||||
throw new CheckedException("排序字段数量超限");
|
||||
}
|
||||
page.addOrder(orderItemList);
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将前端排序字段转为安全的 OrderItem:必须通过标识符白名单,且通过 SqlInjectionUtils 黑名单兜底。
|
||||
*/
|
||||
private static OrderItem toSafeOrderColumn(String column, boolean asc) {
|
||||
if (!SAFE_ORDER_COLUMN.matcher(column).matches()) {
|
||||
log.warn("拒绝非法排序字段: {}", column);
|
||||
throw new CheckedException("排序字段不合法");
|
||||
}
|
||||
if (SqlInjectionUtils.check(column)) {
|
||||
log.warn("排序字段命中注入检测: {}", column);
|
||||
throw new CheckedException("排序字段不合法");
|
||||
}
|
||||
return asc ? OrderItem.asc(column) : OrderItem.desc(column);
|
||||
}
|
||||
|
||||
return page;
|
||||
}
|
||||
}
|
||||
+131
-95
@@ -18,8 +18,10 @@
|
||||
package com.bsd.pjcloud.common.sentinel.handle;
|
||||
|
||||
import com.alibaba.csp.sentinel.Tracer;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.bsd.pjcloud.common.core.exception.CheckedException;
|
||||
import com.bsd.pjcloud.common.core.exception.ServiceException;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import feign.FeignException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -40,111 +42,145 @@ import java.util.List;
|
||||
@RestControllerAdvice
|
||||
public class GlobalBizExceptionHandler {
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 全局异常.
|
||||
*
|
||||
* @param e the e
|
||||
* @return R
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public R handleGlobalException(Exception e) {
|
||||
/**
|
||||
* 业务可控异常:允许把明确的业务文案返回前端。
|
||||
*/
|
||||
@ExceptionHandler({CheckedException.class, ServiceException.class, IllegalArgumentException.class})
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public R handleBizException(RuntimeException e) {
|
||||
log.warn("业务异常 ex={}", e.getMessage());
|
||||
return R.failed(e.getMessage());
|
||||
}
|
||||
|
||||
log.error("全局异常信息 ex={}", e.getMessage(), e);
|
||||
/**
|
||||
* SQLException:禁止把完整 SQL / 表结构回显给前端。
|
||||
*/
|
||||
@ExceptionHandler({SQLException.class})
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public R handleSQLException(SQLException exception) {
|
||||
log.error("数据库调用异常 ex={}", exception.getMessage(), exception);
|
||||
return R.failed("数据库调用异常,请联系管理员处理");
|
||||
}
|
||||
|
||||
// 业务异常交由 sentinel 记录
|
||||
Tracer.trace(e);
|
||||
return R.failed(e.getLocalizedMessage());
|
||||
}
|
||||
/**
|
||||
* 全局异常:默认不回显底层异常原文(MyBatis 常把 SQL、mapper 路径塞进 message)。
|
||||
*
|
||||
* @param e the e
|
||||
* @return R
|
||||
*/
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public R handleGlobalException(Exception e) {
|
||||
log.error("全局异常信息 ex={}", e.getMessage(), e);
|
||||
Tracer.trace(e);
|
||||
if (isDataAccessRelated(e)) {
|
||||
return R.failed("数据库调用异常,请联系管理员处理");
|
||||
}
|
||||
return R.failed("系统繁忙,请稍后重试");
|
||||
}
|
||||
|
||||
/**
|
||||
* SQLException Exception
|
||||
*
|
||||
* @param exception 数据库调用异常
|
||||
* @return R
|
||||
*/
|
||||
@ExceptionHandler({SQLException.class})
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public R handleSQLException(SQLException exception) {
|
||||
log.error("数据库调用异常 ex={}", exception.getMessage(), exception);
|
||||
return R.failed("数据库调用异常,请联系管理员处理");
|
||||
}
|
||||
/**
|
||||
* 识别 Spring DAO / MyBatis / JDBC 相关异常(本模块不直接依赖 mybatis,用类名与特征串判断)。
|
||||
*/
|
||||
private static boolean isDataAccessRelated(Throwable e) {
|
||||
Throwable t = e;
|
||||
while (t != null) {
|
||||
if (t instanceof SQLException) {
|
||||
return true;
|
||||
}
|
||||
String name = t.getClass().getName();
|
||||
if (name.startsWith("org.springframework.dao.")
|
||||
|| name.startsWith("org.springframework.jdbc.")
|
||||
|| name.contains("MyBatis")
|
||||
|| name.contains("PersistenceException")
|
||||
|| name.contains("BadSqlGrammar")) {
|
||||
return true;
|
||||
}
|
||||
String msg = t.getMessage();
|
||||
if (msg != null && (msg.contains("### Error") || msg.contains("### SQL")
|
||||
|| msg.contains("SQL:") || msg.contains("Cause: java.sql")
|
||||
|| msg.contains(" bad SQL grammar"))) {
|
||||
return true;
|
||||
}
|
||||
t = t.getCause();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@ExceptionHandler(FeignException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public R handleGlobalException(FeignException e) {
|
||||
log.error("全局异常信息 ex={}", e.getMessage(), e);
|
||||
@ExceptionHandler(FeignException.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public R handleGlobalException(FeignException e) {
|
||||
log.error("全局异常信息 ex={}", e.getMessage(), e);
|
||||
|
||||
// 业务异常交由 sentinel 记录
|
||||
Tracer.trace(e);
|
||||
// 业务异常交由 sentinel 记录
|
||||
Tracer.trace(e);
|
||||
|
||||
if (e.responseBody().isPresent()) {
|
||||
try {
|
||||
return objectMapper.readValue(e.responseBody().get().array(), R.class);
|
||||
} catch (Exception parseEx) {
|
||||
// 下游返回非 JSON(或 JDK 序列化魔数 0xAC 等)时,勿把 Jackson 原文抛给前端
|
||||
log.warn("Feign 错误响应非 JSON,status={}", e.status());
|
||||
return R.failed("下游服务异常,请稍后重试");
|
||||
}
|
||||
}
|
||||
if (e.responseBody().isPresent()) {
|
||||
try {
|
||||
return objectMapper.readValue(e.responseBody().get().array(), R.class);
|
||||
} catch (Exception parseEx) {
|
||||
// 下游返回非 JSON(或 JDK 序列化魔数 0xAC 等)时,勿把 Jackson 原文抛给前端
|
||||
log.warn("Feign 错误响应非 JSON,status={}", e.status());
|
||||
return R.failed("下游服务异常,请稍后重试");
|
||||
}
|
||||
}
|
||||
|
||||
return R.failed("服务调用失败,请稍后重试");
|
||||
}
|
||||
return R.failed("服务调用失败,请稍后重试");
|
||||
}
|
||||
|
||||
/**
|
||||
* AccessDeniedException
|
||||
*
|
||||
* @param e the e
|
||||
* @return R
|
||||
*/
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public R handleAccessDeniedException(AccessDeniedException e) {
|
||||
log.error("拒绝授权异常信息 ex={}", e.getMessage());
|
||||
return R.failed("权限不足,不允许访问");
|
||||
}
|
||||
/**
|
||||
* AccessDeniedException
|
||||
*
|
||||
* @param e the e
|
||||
* @return R
|
||||
*/
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public R handleAccessDeniedException(AccessDeniedException e) {
|
||||
log.error("拒绝授权异常信息 ex={}", e.getMessage());
|
||||
return R.failed("权限不足,不允许访问");
|
||||
}
|
||||
|
||||
/**
|
||||
* validation Exception
|
||||
*
|
||||
* @param exception
|
||||
* @return R
|
||||
*/
|
||||
@ExceptionHandler({BindException.class})
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public R handleBodyValidException(BindException exception) {
|
||||
List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors();
|
||||
// 插入log 的逻辑
|
||||
return R.failed(String.format("%s %s", fieldErrors.get(0).getField(), fieldErrors.get(0).getDefaultMessage()));
|
||||
}
|
||||
/**
|
||||
* validation Exception
|
||||
*
|
||||
* @param exception
|
||||
* @return R
|
||||
*/
|
||||
@ExceptionHandler({BindException.class})
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public R handleBodyValidException(BindException exception) {
|
||||
List<FieldError> fieldErrors = exception.getBindingResult().getFieldErrors();
|
||||
return R.failed(String.format("%s %s", fieldErrors.get(0).getField(), fieldErrors.get(0).getDefaultMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 避免 404 重定向到 /error 导致NPE ,ignore-url 需要配置对应端点
|
||||
*
|
||||
* @return R
|
||||
*/
|
||||
@DeleteMapping("/error")
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public R noHandlerFoundException() {
|
||||
return R.failed(HttpStatus.NOT_FOUND.getReasonPhrase());
|
||||
}
|
||||
/**
|
||||
* 避免 404 重定向到 /error 导致NPE ,ignore-url 需要配置对应端点
|
||||
*
|
||||
* @return R
|
||||
*/
|
||||
@DeleteMapping("/error")
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public R noHandlerFoundException() {
|
||||
return R.failed(HttpStatus.NOT_FOUND.getReasonPhrase());
|
||||
}
|
||||
|
||||
/**
|
||||
* 保持和低版本请求路径不存在的行为一致
|
||||
* <p>
|
||||
* <a href="https://github.com/spring-projects/spring-boot/issues/38733">[Spring Boot
|
||||
* 3.2.0] 404 Not Found behavior #38733</a>
|
||||
*
|
||||
* @param exception
|
||||
* @return R
|
||||
*/
|
||||
@ExceptionHandler({NoResourceFoundException.class})
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public R noResourceFoundException(NoResourceFoundException exception) {
|
||||
log.debug("请求路径 404 {}", exception.getMessage());
|
||||
return R.failed(exception.getMessage());
|
||||
}
|
||||
/**
|
||||
* 保持和低版本请求路径不存在的行为一致
|
||||
* <p>
|
||||
* <a href="https://github.com/spring-projects/spring-boot/issues/38733">[Spring Boot
|
||||
* 3.2.0] 404 Not Found behavior #38733</a>
|
||||
*
|
||||
* @param exception
|
||||
* @return R
|
||||
*/
|
||||
@ExceptionHandler({NoResourceFoundException.class})
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public R noResourceFoundException(NoResourceFoundException exception) {
|
||||
log.debug("请求路径 404 {}", exception.getMessage());
|
||||
return R.failed(exception.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package com.bsd.pjcloud.thirdparty.controller;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.log.annotation.SysLog;
|
||||
import com.bsd.pjcloud.thirdparty.entity.ThirdPartyZzdPhoneBindEntity;
|
||||
import com.bsd.pjcloud.thirdparty.service.ThirdPartyZzdPhoneBindService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 浙政钉短信核验手机号绑定:后台查询/改号(绑错时可修正)
|
||||
*/
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/zzdPhoneBind")
|
||||
@Tag(description = "zzdPhoneBind", name = "浙政钉手机号绑定管理")
|
||||
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
|
||||
public class ThirdPartyZzdPhoneBindController {
|
||||
|
||||
private final ThirdPartyZzdPhoneBindService thirdPartyZzdPhoneBindService;
|
||||
|
||||
@Operation(summary = "分页查询")
|
||||
@GetMapping("/page")
|
||||
@PreAuthorize("@pms.hasPermission('thirdparty_zzdPhoneBind_view')")
|
||||
public R page(@ParameterObject Page page,
|
||||
@RequestParam(value = "name", required = false) String name,
|
||||
@RequestParam(value = "verifiedPhone", required = false) String verifiedPhone,
|
||||
@RequestParam(value = "employeeCode", required = false) String employeeCode) {
|
||||
LambdaQueryWrapper<ThirdPartyZzdPhoneBindEntity> wrapper = Wrappers.lambdaQuery();
|
||||
wrapper.eq(ThirdPartyZzdPhoneBindEntity::getDelFlag, "0");
|
||||
wrapper.like(StringUtils.isNotBlank(verifiedPhone), ThirdPartyZzdPhoneBindEntity::getVerifiedPhone, verifiedPhone);
|
||||
wrapper.like(StringUtils.isNotBlank(employeeCode), ThirdPartyZzdPhoneBindEntity::getEmployeeCode, employeeCode);
|
||||
// 名称在 zzd_profile_json(nickName/username)里,模糊匹配 JSON 文本
|
||||
wrapper.like(StringUtils.isNotBlank(name), ThirdPartyZzdPhoneBindEntity::getZzdProfileJson, name);
|
||||
wrapper.orderByDesc(ThirdPartyZzdPhoneBindEntity::getUpdateTime)
|
||||
.orderByDesc(ThirdPartyZzdPhoneBindEntity::getCreateTime);
|
||||
Page<ThirdPartyZzdPhoneBindEntity> result = thirdPartyZzdPhoneBindService.page(page, wrapper);
|
||||
List<Map<String, Object>> records = result.getRecords().stream().map(this::toRow).collect(Collectors.toList());
|
||||
Page<Map<String, Object>> voPage = new Page<>(result.getCurrent(), result.getSize(), result.getTotal());
|
||||
voPage.setRecords(records);
|
||||
return R.ok(voPage);
|
||||
}
|
||||
|
||||
@Operation(summary = "详情")
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("@pms.hasPermission('thirdparty_zzdPhoneBind_view')")
|
||||
public R getById(@PathVariable("id") Long id) {
|
||||
ThirdPartyZzdPhoneBindEntity e = thirdPartyZzdPhoneBindService.getById(id);
|
||||
if (e == null || "1".equals(e.getDelFlag())) {
|
||||
return R.failed("记录不存在");
|
||||
}
|
||||
return R.ok(toRow(e));
|
||||
}
|
||||
|
||||
@Operation(summary = "修改核验手机号")
|
||||
@SysLog("修改浙政钉绑定手机号")
|
||||
@PutMapping("/phone")
|
||||
@PreAuthorize("@pms.hasPermission('thirdparty_zzdPhoneBind_edit')")
|
||||
public R updatePhone(@RequestBody UpdatePhoneBody body) {
|
||||
try {
|
||||
thirdPartyZzdPhoneBindService.updateVerifiedPhone(body.getId(), body.getVerifiedPhone());
|
||||
return R.ok(Boolean.TRUE, "手机号已更新");
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
return R.failed(ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class UpdatePhoneBody {
|
||||
private Long id;
|
||||
private String verifiedPhone;
|
||||
}
|
||||
|
||||
private Map<String, Object> toRow(ThirdPartyZzdPhoneBindEntity e) {
|
||||
Map<String, Object> m = new HashMap<>(12);
|
||||
m.put("id", e.getId());
|
||||
m.put("employeeCode", e.getEmployeeCode());
|
||||
m.put("openId", e.getOpenId());
|
||||
m.put("verifiedPhone", e.getVerifiedPhone());
|
||||
m.put("zzdProfileJson", e.getZzdProfileJson());
|
||||
m.put("createTime", e.getCreateTime());
|
||||
m.put("updateTime", e.getUpdateTime());
|
||||
m.put("createBy", e.getCreateBy());
|
||||
m.put("updateBy", e.getUpdateBy());
|
||||
String displayName = parseDisplayName(e.getZzdProfileJson());
|
||||
m.put("displayName", displayName);
|
||||
return m;
|
||||
}
|
||||
|
||||
private static String parseDisplayName(String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
JSONObject o = JSON.parseObject(json);
|
||||
if (o == null) {
|
||||
return "";
|
||||
}
|
||||
String nick = o.getString("nickName");
|
||||
if (StringUtils.isNotBlank(nick)) {
|
||||
return nick;
|
||||
}
|
||||
String user = o.getString("username");
|
||||
return StringUtils.defaultString(user);
|
||||
}
|
||||
catch (Exception ignored) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.bsd.pjcloud.thirdparty.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.bsd.pjcloud.thirdparty.entity.ThirdPartyZzdPhoneBindEntity;
|
||||
|
||||
public interface ThirdPartyZzdPhoneBindService extends IService<ThirdPartyZzdPhoneBindEntity> {
|
||||
|
||||
/**
|
||||
* 修正短信核验手机号(绑错号时后台改)
|
||||
*/
|
||||
boolean updateVerifiedPhone(Long id, String verifiedPhone);
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.bsd.pjcloud.thirdparty.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bsd.pjcloud.thirdparty.entity.ThirdPartyZzdPhoneBindEntity;
|
||||
import com.bsd.pjcloud.thirdparty.mapper.ThirdPartyZzdPhoneBindMapper;
|
||||
import com.bsd.pjcloud.thirdparty.service.ThirdPartyZzdPhoneBindService;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class ThirdPartyZzdPhoneBindServiceImpl
|
||||
extends ServiceImpl<ThirdPartyZzdPhoneBindMapper, ThirdPartyZzdPhoneBindEntity>
|
||||
implements ThirdPartyZzdPhoneBindService {
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean updateVerifiedPhone(Long id, String verifiedPhone) {
|
||||
if (id == null || StringUtils.isBlank(verifiedPhone)) {
|
||||
throw new IllegalArgumentException("id与手机号不能为空");
|
||||
}
|
||||
String phone = verifiedPhone.trim();
|
||||
if (!phone.matches("^1\\d{10}$")) {
|
||||
throw new IllegalArgumentException("手机号格式不正确,需为11位大陆手机号");
|
||||
}
|
||||
ThirdPartyZzdPhoneBindEntity exist = getById(id);
|
||||
if (exist == null || "1".equals(exist.getDelFlag())) {
|
||||
throw new IllegalArgumentException("绑定记录不存在");
|
||||
}
|
||||
ThirdPartyZzdPhoneBindEntity upd = new ThirdPartyZzdPhoneBindEntity();
|
||||
upd.setId(id);
|
||||
upd.setVerifiedPhone(phone);
|
||||
return updateById(upd);
|
||||
}
|
||||
}
|
||||
+109
-113
@@ -62,127 +62,123 @@ import java.util.List;
|
||||
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
|
||||
public class SysClientController {
|
||||
|
||||
private final SysOauthClientDetailsService clientDetailsService;
|
||||
private final SysOauthClientDetailsService clientDetailsService;
|
||||
|
||||
/**
|
||||
* 通过ID查询
|
||||
*
|
||||
* @param clientId clientId
|
||||
* @return SysOauthClientDetails
|
||||
*/
|
||||
@GetMapping("/{clientId}")
|
||||
public R getByClientId(@PathVariable String clientId) {
|
||||
SysOauthClientDetails details = clientDetailsService
|
||||
.getOne(Wrappers.<SysOauthClientDetails>lambdaQuery().eq(SysOauthClientDetails::getClientId, clientId));
|
||||
String information = details.getAdditionalInformation();
|
||||
String captchaFlag = JSONUtil.parseObj(information).getStr(CommonConstants.CAPTCHA_FLAG);
|
||||
String encFlag = JSONUtil.parseObj(information).getStr(CommonConstants.ENC_FLAG);
|
||||
String onlineQuantity = JSONUtil.parseObj(information).getStr(CommonConstants.ONLINE_QUANTITY);
|
||||
SysOauthClientDetailsDTO dto = new SysOauthClientDetailsDTO();
|
||||
BeanUtils.copyProperties(details, dto);
|
||||
dto.setCaptchaFlag(captchaFlag);
|
||||
dto.setEncFlag(encFlag);
|
||||
dto.setOnlineQuantity(onlineQuantity);
|
||||
return R.ok(dto);
|
||||
}
|
||||
/**
|
||||
* 通过ID查询
|
||||
* @param clientId clientId
|
||||
* @return SysOauthClientDetails
|
||||
*/
|
||||
@GetMapping("/{clientId}")
|
||||
public R getByClientId(@PathVariable String clientId) {
|
||||
SysOauthClientDetails details = clientDetailsService
|
||||
.getOne(Wrappers.<SysOauthClientDetails>lambdaQuery().eq(SysOauthClientDetails::getClientId, clientId));
|
||||
String information = details.getAdditionalInformation();
|
||||
String captchaFlag = JSONUtil.parseObj(information).getStr(CommonConstants.CAPTCHA_FLAG);
|
||||
String encFlag = JSONUtil.parseObj(information).getStr(CommonConstants.ENC_FLAG);
|
||||
String onlineQuantity = JSONUtil.parseObj(information).getStr(CommonConstants.ONLINE_QUANTITY);
|
||||
SysOauthClientDetailsDTO dto = new SysOauthClientDetailsDTO();
|
||||
BeanUtils.copyProperties(details, dto);
|
||||
dto.setCaptchaFlag(captchaFlag);
|
||||
dto.setEncFlag(encFlag);
|
||||
dto.setOnlineQuantity(onlineQuantity);
|
||||
return R.ok(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单分页查询
|
||||
*
|
||||
* @param page 分页对象
|
||||
* @param sysOauthClientDetails 系统终端
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/page")
|
||||
@HasPermission("sys_client_page")
|
||||
public R getOauthClientDetailsPage(@ParameterObject Page page, @ParameterObject SysOauthClientDetails sysOauthClientDetails) {
|
||||
LambdaQueryWrapper<SysOauthClientDetails> wrapper = Wrappers.<SysOauthClientDetails>lambdaQuery()
|
||||
.like(StrUtil.isNotBlank(sysOauthClientDetails.getClientId()), SysOauthClientDetails::getClientId,
|
||||
sysOauthClientDetails.getClientId())
|
||||
.like(StrUtil.isNotBlank(sysOauthClientDetails.getClientSecret()), SysOauthClientDetails::getClientSecret,
|
||||
sysOauthClientDetails.getClientSecret());
|
||||
return R.ok(clientDetailsService.page(page, wrapper));
|
||||
}
|
||||
/**
|
||||
* 简单分页查询
|
||||
* @param page 分页对象
|
||||
* @param sysOauthClientDetails 系统终端
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/page")
|
||||
public R getOauthClientDetailsPage(@ParameterObject Page page,
|
||||
@ParameterObject SysOauthClientDetails sysOauthClientDetails) {
|
||||
LambdaQueryWrapper<SysOauthClientDetails> wrapper = Wrappers.<SysOauthClientDetails>lambdaQuery()
|
||||
.like(StrUtil.isNotBlank(sysOauthClientDetails.getClientId()), SysOauthClientDetails::getClientId,
|
||||
sysOauthClientDetails.getClientId())
|
||||
.like(StrUtil.isNotBlank(sysOauthClientDetails.getClientSecret()), SysOauthClientDetails::getClientSecret,
|
||||
sysOauthClientDetails.getClientSecret());
|
||||
return R.ok(clientDetailsService.page(page, wrapper));
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加
|
||||
*
|
||||
* @param clientDetailsDTO 实体
|
||||
* @return success/false
|
||||
*/
|
||||
@SysLog("添加终端")
|
||||
@PostMapping
|
||||
@HasPermission("sys_client_add")
|
||||
public R add(@Valid @RequestBody SysOauthClientDetailsDTO clientDetailsDTO) {
|
||||
return R.ok(clientDetailsService.saveClient(clientDetailsDTO));
|
||||
}
|
||||
/**
|
||||
* 添加
|
||||
* @param clientDetailsDTO 实体
|
||||
* @return success/false
|
||||
*/
|
||||
@SysLog("添加终端")
|
||||
@PostMapping
|
||||
@HasPermission("sys_client_add")
|
||||
public R add(@Valid @RequestBody SysOauthClientDetailsDTO clientDetailsDTO) {
|
||||
return R.ok(clientDetailsService.saveClient(clientDetailsDTO));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*
|
||||
* @param ids ID 列表
|
||||
* @return success/false
|
||||
*/
|
||||
@SysLog("删除终端")
|
||||
@DeleteMapping
|
||||
@HasPermission("sys_client_del")
|
||||
public R removeById(@RequestBody Long[] ids) {
|
||||
clientDetailsService.removeBatchByIds(CollUtil.toList(ids));
|
||||
SpringContextHolder.publishEvent(new ClientDetailsInitRunner.ClientDetailsInitEvent(ids));
|
||||
return R.ok();
|
||||
}
|
||||
/**
|
||||
* 删除
|
||||
* @param ids ID 列表
|
||||
* @return success/false
|
||||
*/
|
||||
@SysLog("删除终端")
|
||||
@DeleteMapping
|
||||
@HasPermission("sys_client_del")
|
||||
public R removeById(@RequestBody Long[] ids) {
|
||||
clientDetailsService.removeBatchByIds(CollUtil.toList(ids));
|
||||
SpringContextHolder.publishEvent(new ClientDetailsInitRunner.ClientDetailsInitEvent(ids));
|
||||
return R.ok();
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*
|
||||
* @param clientDetailsDTO 实体
|
||||
* @return success/false
|
||||
*/
|
||||
@SysLog("编辑终端")
|
||||
@PutMapping
|
||||
@HasPermission("sys_client_edit")
|
||||
public R update(@Valid @RequestBody SysOauthClientDetailsDTO clientDetailsDTO) {
|
||||
return R.ok(clientDetailsService.updateClientById(clientDetailsDTO));
|
||||
}
|
||||
/**
|
||||
* 编辑
|
||||
* @param clientDetailsDTO 实体
|
||||
* @return success/false
|
||||
*/
|
||||
@SysLog("编辑终端")
|
||||
@PutMapping
|
||||
@HasPermission("sys_client_edit")
|
||||
public R update(@Valid @RequestBody SysOauthClientDetailsDTO clientDetailsDTO) {
|
||||
return R.ok(clientDetailsService.updateClientById(clientDetailsDTO));
|
||||
}
|
||||
|
||||
@Inner(false)
|
||||
@GetMapping("/getClientDetailsById/{clientId}")
|
||||
public R getClientDetailsById(@PathVariable String clientId) {
|
||||
return R.ok(clientDetailsService.getOne(
|
||||
Wrappers.<SysOauthClientDetails>lambdaQuery().eq(SysOauthClientDetails::getClientId, clientId), false));
|
||||
}
|
||||
/**
|
||||
* 仅服务间调用(需 FROM 头),禁止匿名公网读取客户端密钥。
|
||||
*/
|
||||
@Inner
|
||||
@GetMapping("/getClientDetailsById/{clientId}")
|
||||
public R getClientDetailsById(@PathVariable String clientId) {
|
||||
return R.ok(clientDetailsService.getOne(
|
||||
Wrappers.<SysOauthClientDetails>lambdaQuery().eq(SysOauthClientDetails::getClientId, clientId), false));
|
||||
}
|
||||
|
||||
// /**
|
||||
// * 查询全部客户端
|
||||
// * @return
|
||||
// */
|
||||
// @Inner(false)
|
||||
// @GetMapping("/list")
|
||||
// public R listClients() {
|
||||
// return R.ok(clientDetailsService.list());
|
||||
// }
|
||||
/**
|
||||
* 查询全部客户端(仅服务间调用,需 FROM 头)
|
||||
* @return
|
||||
*/
|
||||
@Inner
|
||||
@GetMapping("/list")
|
||||
public R listClients() {
|
||||
return R.ok(clientDetailsService.list());
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步缓存字典
|
||||
*
|
||||
* @return R
|
||||
*/
|
||||
@SysLog("同步终端")
|
||||
@PutMapping("/sync")
|
||||
public R sync() {
|
||||
return clientDetailsService.syncClientCache();
|
||||
}
|
||||
/**
|
||||
* 同步缓存字典
|
||||
* @return R
|
||||
*/
|
||||
@SysLog("同步终端")
|
||||
@PutMapping("/sync")
|
||||
public R sync() {
|
||||
return clientDetailsService.syncClientCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出所有客户端
|
||||
*
|
||||
* @return excel
|
||||
*/
|
||||
@ResponseExcel
|
||||
@SysLog("导出excel")
|
||||
@GetMapping("/export")
|
||||
public List<SysOauthClientDetails> export(SysOauthClientDetails sysOauthClientDetails) {
|
||||
return clientDetailsService.list(Wrappers.query(sysOauthClientDetails));
|
||||
}
|
||||
/**
|
||||
* 导出所有客户端
|
||||
* @return excel
|
||||
*/
|
||||
@ResponseExcel
|
||||
@SysLog("导出excel")
|
||||
@GetMapping("/export")
|
||||
public List<SysOauthClientDetails> export(SysOauthClientDetails sysOauthClientDetails) {
|
||||
return clientDetailsService.list(Wrappers.query(sysOauthClientDetails));
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -146,7 +146,7 @@ public class SysI18nController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统I18N配置
|
||||
* 获取系统I18N配置(登录页初始化需要匿名可读;仅文案映射,无密钥/账号数据)
|
||||
* @return I18N 配置
|
||||
*/
|
||||
@Operation(summary = "获取系统配置-国际化", description = "获取系统配置-国际化")
|
||||
|
||||
+18
-1
@@ -137,7 +137,7 @@ public class SysTenantController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询全部有效的租户
|
||||
* 查询全部有效的租户(登录页匿名可用,仅返回展示字段,不含菜单等内部配置)
|
||||
* @return
|
||||
*/
|
||||
@Inner(value = false)
|
||||
@@ -147,10 +147,27 @@ public class SysTenantController {
|
||||
.stream()
|
||||
.filter(tenant -> tenant.getStartTime().isBefore(LocalDateTime.now()))
|
||||
.filter(tenant -> tenant.getEndTime().isAfter(LocalDateTime.now()))
|
||||
.map(this::toPublicTenant)
|
||||
.collect(Collectors.toList());
|
||||
return R.ok(tenants);
|
||||
}
|
||||
|
||||
/** 登录页/对外列表脱敏:只保留选租户与品牌展示所需字段。 */
|
||||
private SysTenant toPublicTenant(SysTenant src) {
|
||||
SysTenant t = new SysTenant();
|
||||
t.setId(src.getId());
|
||||
t.setName(src.getName());
|
||||
t.setCode(src.getCode());
|
||||
t.setTenantDomain(src.getTenantDomain());
|
||||
t.setWebsiteName(src.getWebsiteName());
|
||||
t.setLogo(src.getLogo());
|
||||
t.setFooter(src.getFooter());
|
||||
t.setMiniQr(src.getMiniQr());
|
||||
t.setBackground(src.getBackground());
|
||||
t.setStatus(src.getStatus());
|
||||
return t;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel 表格
|
||||
* @return
|
||||
|
||||
+18
@@ -96,6 +96,9 @@ public class SysUserController {
|
||||
/** 复用原 UserExcelVO 的分批导出。 */
|
||||
private final com.bsd.pjcloud.admin.service.impl.SysUserBatchExportService batchExportService;
|
||||
|
||||
/** 异步修复「密码=手机号」弱口令。 */
|
||||
private final com.bsd.pjcloud.admin.service.impl.SysUserFixPhonePasswordService fixPhonePasswordService;
|
||||
|
||||
private final SysUserService userService;
|
||||
|
||||
private final RemoterVisitorService remoterVisitorService;
|
||||
@@ -430,6 +433,21 @@ public class SysUserController {
|
||||
return userService.checkPassword(SecurityUtils.getUser().getUsername(), password);
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步修复「密码=手机号」弱口令:立即返回任务进度,前端轮询;完成后可下载名单 txt。
|
||||
* body: { "password": "PjHq@2024Fix!" }
|
||||
*/
|
||||
@SysLog("修复密码等于手机号的弱口令")
|
||||
@PostMapping("/fix-phone-as-password")
|
||||
@HasPermission("sys_user_edit")
|
||||
public R<com.bsd.pjcloud.common.asynctask.vo.AsyncTaskProgressVO> fixPhoneAsPassword(@RequestBody UserDTO body) {
|
||||
String newPassword = body != null ? body.getPassword() : null;
|
||||
if (newPassword == null && body != null) {
|
||||
newPassword = body.getNewpassword1();
|
||||
}
|
||||
return R.ok(fixPhonePasswordService.submit(newPassword), "弱口令修复任务已提交");
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据角色ID列表获取用户ID列表接口
|
||||
*
|
||||
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
package com.bsd.pjcloud.admin.service.impl;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.bsd.pjcloud.admin.api.entity.SysUser;
|
||||
import com.bsd.pjcloud.admin.mapper.SysUserMapper;
|
||||
import com.bsd.pjcloud.admin.service.SysFileService;
|
||||
import com.bsd.pjcloud.common.asynctask.config.AsyncTaskAutoConfiguration;
|
||||
import com.bsd.pjcloud.common.asynctask.service.AsyncTaskException;
|
||||
import com.bsd.pjcloud.common.asynctask.service.AsyncTaskProgressService;
|
||||
import com.bsd.pjcloud.common.asynctask.service.AsyncTaskResult;
|
||||
import com.bsd.pjcloud.common.asynctask.vo.AsyncTaskProgressVO;
|
||||
import com.bsd.pjcloud.common.core.constant.CacheConstants;
|
||||
import com.bsd.pjcloud.common.core.util.PasswordWeakCheckUtil;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.data.tenant.TenantContextHolder;
|
||||
import com.bsd.pjcloud.common.security.util.NonWebTokenContextHolder;
|
||||
import com.bsd.pjcloud.common.security.util.SecurityUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.core.task.TaskExecutor;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.context.SecurityContextHolderStrategy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 异步扫描并修复「密码=手机号」弱口令:分页处理,避免 HTTP 超时;逐条提交,避免超长事务。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SysUserFixPhonePasswordService {
|
||||
|
||||
public static final String REDIS_KEY = "admin:fix:phone-password";
|
||||
|
||||
private static final String TASK_NAME = "修复弱口令";
|
||||
|
||||
private static final int PAGE_SIZE = 200;
|
||||
|
||||
private static final PasswordEncoder ENCODER = new BCryptPasswordEncoder();
|
||||
|
||||
private final SysUserMapper sysUserMapper;
|
||||
|
||||
private final SysFileService sysFileService;
|
||||
|
||||
private final CacheManager cacheManager;
|
||||
|
||||
private final AsyncTaskProgressService asyncTaskProgressService;
|
||||
|
||||
private final TaskExecutor asyncTaskWorkerExecutor;
|
||||
|
||||
public SysUserFixPhonePasswordService(SysUserMapper sysUserMapper, SysFileService sysFileService,
|
||||
CacheManager cacheManager, AsyncTaskProgressService asyncTaskProgressService,
|
||||
@Qualifier(AsyncTaskAutoConfiguration.ASYNC_TASK_WORKER_EXECUTOR_BEAN_NAME) TaskExecutor asyncTaskWorkerExecutor) {
|
||||
this.sysUserMapper = sysUserMapper;
|
||||
this.sysFileService = sysFileService;
|
||||
this.cacheManager = cacheManager;
|
||||
this.asyncTaskProgressService = asyncTaskProgressService;
|
||||
this.asyncTaskWorkerExecutor = asyncTaskWorkerExecutor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交异步修复任务,立即返回进度对象供前端轮询。
|
||||
*/
|
||||
public AsyncTaskProgressVO submit(String newPassword) {
|
||||
if (StrUtil.isBlank(newPassword)) {
|
||||
throw new AsyncTaskException("新密码不能为空");
|
||||
}
|
||||
String pwd = newPassword.trim();
|
||||
String strength = PasswordWeakCheckUtil.validate(pwd, null, null);
|
||||
if (strength != null) {
|
||||
throw new AsyncTaskException(strength);
|
||||
}
|
||||
AsyncTaskProgressVO progress = asyncTaskProgressService.create(REDIS_KEY, TASK_NAME, 1);
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
Authentication authentication = SecurityUtils.getAuthentication();
|
||||
String token = SecurityUtils.getToken();
|
||||
SecurityContextHolderStrategy strategy = SecurityContextHolder.getContextHolderStrategy();
|
||||
SecurityContext securityContext = strategy.createEmptyContext();
|
||||
securityContext.setAuthentication(authentication);
|
||||
try {
|
||||
asyncTaskWorkerExecutor.execute(() -> run(progress, pwd, tenantId, token, strategy, securityContext));
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
log.error("|修复弱口令|任务提交失败,taskId:{}", progress.getTaskId(), ex);
|
||||
asyncTaskProgressService.fail(progress.getRedisKey(), progress.getTaskId(), "任务提交失败,请稍后重试");
|
||||
throw new AsyncTaskException("任务提交失败,请稍后重试");
|
||||
}
|
||||
return progress;
|
||||
}
|
||||
|
||||
private void run(AsyncTaskProgressVO progress, String pwd, Long tenantId, String token,
|
||||
SecurityContextHolderStrategy strategy, SecurityContext securityContext) {
|
||||
try {
|
||||
TenantContextHolder.setTenantId(tenantId);
|
||||
strategy.setContext(securityContext);
|
||||
NonWebTokenContextHolder.setToken(token);
|
||||
asyncTaskProgressService.start(progress.getRedisKey(), progress.getTaskId());
|
||||
|
||||
long totalLong = sysUserMapper.selectCount(Wrappers.<SysUser>lambdaQuery()
|
||||
.isNotNull(SysUser::getPhone)
|
||||
.ne(SysUser::getPhone, ""));
|
||||
if (totalLong <= 0) {
|
||||
asyncTaskProgressService.success(progress.getRedisKey(), progress.getTaskId(),
|
||||
"没有带手机号的用户需要扫描", AsyncTaskResult.empty());
|
||||
return;
|
||||
}
|
||||
int total = (int) Math.min(totalLong, Integer.MAX_VALUE);
|
||||
asyncTaskProgressService.updateTotal(progress.getRedisKey(), progress.getTaskId(), total,
|
||||
"共 " + total + " 个有手机号账号,开始扫描");
|
||||
|
||||
String encoded = ENCODER.encode(pwd);
|
||||
List<String> lines = new ArrayList<>();
|
||||
int scanned = 0;
|
||||
int fixed = 0;
|
||||
long current = 1;
|
||||
while (true) {
|
||||
Page<SysUser> page = sysUserMapper.selectPage(new Page<>(current, PAGE_SIZE, false),
|
||||
Wrappers.<SysUser>lambdaQuery()
|
||||
.select(SysUser::getUserId, SysUser::getUsername, SysUser::getName,
|
||||
SysUser::getPhone, SysUser::getPassword)
|
||||
.isNotNull(SysUser::getPhone)
|
||||
.ne(SysUser::getPhone, "")
|
||||
.orderByAsc(SysUser::getUserId));
|
||||
List<SysUser> records = page.getRecords();
|
||||
if (records == null || records.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
for (SysUser u : records) {
|
||||
scanned++;
|
||||
if (u == null || StrUtil.isBlank(u.getPassword()) || StrUtil.isBlank(u.getPhone())) {
|
||||
continue;
|
||||
}
|
||||
String phone = StrUtil.trim(u.getPhone());
|
||||
if (!ENCODER.matches(phone, u.getPassword())) {
|
||||
continue;
|
||||
}
|
||||
String weak = PasswordWeakCheckUtil.validate(pwd, phone, u.getUsername());
|
||||
if (weak != null) {
|
||||
throw new AsyncTaskException("新密码不满足强度要求(用户 " + u.getUsername() + "):" + weak
|
||||
+ ";请换一个不与任何用户手机号/用户名相同的强密码");
|
||||
}
|
||||
sysUserMapper.update(null, Wrappers.<SysUser>lambdaUpdate()
|
||||
.set(SysUser::getPassword, encoded)
|
||||
.set(SysUser::getPasswordModifyTime, LocalDateTime.now())
|
||||
.eq(SysUser::getUserId, u.getUserId()));
|
||||
Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS);
|
||||
if (cache != null && StrUtil.isNotBlank(u.getUsername())) {
|
||||
cache.evict(u.getUsername());
|
||||
}
|
||||
fixed++;
|
||||
lines.add(fixed + ". " + StrUtil.blankToDefault(u.getName(), "-")
|
||||
+ " / " + StrUtil.blankToDefault(u.getUsername(), "-")
|
||||
+ " / " + phone);
|
||||
}
|
||||
asyncTaskProgressService.update(progress.getRedisKey(), progress.getTaskId(),
|
||||
Math.min(scanned, total),
|
||||
"已扫描 " + scanned + "/" + total + ",已重置 " + fixed + " 人");
|
||||
if (records.size() < PAGE_SIZE) {
|
||||
break;
|
||||
}
|
||||
current++;
|
||||
}
|
||||
|
||||
String message = "已将 " + fixed + " 个「密码=手机号」账号重置为统一新密码";
|
||||
if (fixed == 0) {
|
||||
asyncTaskProgressService.success(progress.getRedisKey(), progress.getTaskId(),
|
||||
"扫描完成,未发现「密码=手机号」账号", AsyncTaskResult.empty());
|
||||
return;
|
||||
}
|
||||
AsyncTaskResult result = uploadResultFile(fixed, lines);
|
||||
asyncTaskProgressService.success(progress.getRedisKey(), progress.getTaskId(), message, result);
|
||||
}
|
||||
catch (AsyncTaskException ex) {
|
||||
log.warn("|修复弱口令|业务失败,taskId:{},msg:{}", progress.getTaskId(), ex.getMessage());
|
||||
asyncTaskProgressService.fail(progress.getRedisKey(), progress.getTaskId(), ex.getMessage());
|
||||
}
|
||||
catch (Exception ex) {
|
||||
log.error("|修复弱口令|执行失败,taskId:{}", progress.getTaskId(), ex);
|
||||
asyncTaskProgressService.fail(progress.getRedisKey(), progress.getTaskId(), "修复弱口令任务执行失败,请重试");
|
||||
}
|
||||
finally {
|
||||
NonWebTokenContextHolder.clear();
|
||||
strategy.clearContext();
|
||||
TenantContextHolder.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private AsyncTaskResult uploadResultFile(int fixed, List<String> lines) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("弱口令(密码=手机号)账号已重置,共 ").append(fixed).append(" 人。\n");
|
||||
sb.append("新密码请按单位通知另行获取,原「手机号当密码」将无法登录。\n\n");
|
||||
sb.append("涉及人员名单(姓名 / 用户名 / 手机号):\n");
|
||||
for (String line : lines) {
|
||||
sb.append(line).append('\n');
|
||||
}
|
||||
byte[] bytes = sb.toString().getBytes(StandardCharsets.UTF_8);
|
||||
String fileName = "弱口令修复名单.txt";
|
||||
try {
|
||||
MockMultipartFile multipartFile = new MockMultipartFile("file", fileName, "text/plain", bytes);
|
||||
R<?> upload = sysFileService.uploadFile(multipartFile, "export", null, "txt");
|
||||
Object data = upload != null ? upload.getData() : null;
|
||||
if (!(data instanceof Map)) {
|
||||
throw new AsyncTaskException("名单文件保存失败,请重试");
|
||||
}
|
||||
Object url = ((Map<?, ?>) data).get("url");
|
||||
if (url == null || StrUtil.isBlank(String.valueOf(url))) {
|
||||
throw new AsyncTaskException("名单文件保存失败,请重试");
|
||||
}
|
||||
return AsyncTaskResult.file(String.valueOf(url), fileName);
|
||||
}
|
||||
catch (AsyncTaskException ex) {
|
||||
throw ex;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
log.error("|修复弱口令|上传名单失败", ex);
|
||||
throw new AsyncTaskException("名单文件保存失败,请重试");
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
-4
@@ -59,6 +59,7 @@ import com.bsd.pjcloud.common.core.constant.CommonRoleConstant;
|
||||
import com.bsd.pjcloud.common.core.exception.ErrorCodes;
|
||||
import com.bsd.pjcloud.common.core.util.CollectionUtils;
|
||||
import com.bsd.pjcloud.common.core.util.MsgUtils;
|
||||
import com.bsd.pjcloud.common.core.util.PasswordWeakCheckUtil;
|
||||
import com.bsd.pjcloud.common.core.util.R;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScope;
|
||||
import com.bsd.pjcloud.common.data.resolver.ParamResolver;
|
||||
@@ -178,6 +179,7 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
||||
sysUser.setLockFlag(StrUtil.isBlank(userDto.getLockFlag()) ? UserStateEnum.NORMAL.getCode() : userDto.getLockFlag());
|
||||
sysUser.setCreateBy(userDto.getUsername());
|
||||
sysUser.setUpdateBy(userDto.getUsername());
|
||||
PasswordWeakCheckUtil.assertStrong(userDto.getPassword(), userDto.getPhone(), userDto.getUsername());
|
||||
sysUser.setPassword(ENCODER.encode(userDto.getPassword()));
|
||||
sysUser.setPasswordModifyTime(LocalDateTime.now());
|
||||
baseMapper.insert(sysUser);
|
||||
@@ -392,6 +394,7 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
||||
sysUser.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
if (StrUtil.isNotBlank(userDto.getPassword())) {
|
||||
PasswordWeakCheckUtil.assertStrong(userDto.getPassword(), userDto.getPhone(), userDto.getUsername());
|
||||
sysUser.setPassword(ENCODER.encode(userDto.getPassword()));
|
||||
}
|
||||
|
||||
@@ -688,9 +691,9 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
||||
userDTO.setEmail(excel.getEmail());
|
||||
userDTO.setLockFlag(excel.getLockFlag());
|
||||
userDTO.setStaffingType(excel.getStaffingTypeParse());
|
||||
// 批量导入初始密码为手机号
|
||||
// 批量导入:默认密码取系统参数 USER_DEFAULT_PASSWORD(禁止再用手机号当密码)
|
||||
userDTO.setPasswordModifyTime(LocalDateTime.now());
|
||||
userDTO.setPassword(userDTO.getPhone());
|
||||
userDTO.setPassword(resolveImportDefaultPassword());
|
||||
// 根据部门名称查询部门ID
|
||||
userDTO.setDeptId(deptOptional.get().getDeptId());
|
||||
// 插入岗位名称
|
||||
@@ -712,9 +715,9 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
||||
userDTO.setPhone(excel.getMobile().replaceAll("\\s+", ""));
|
||||
userDTO.setDeptId(deptId);
|
||||
userDTO.setStaffingType(excel.getStaffingType());
|
||||
// 批量导入初始密码为手机号
|
||||
// 批量导入:默认密码取系统参数 USER_DEFAULT_PASSWORD(禁止再用手机号当密码)
|
||||
userDTO.setPasswordModifyTime(LocalDateTime.now());
|
||||
userDTO.setPassword(userDTO.getPhone().replaceAll("\\s+", ""));
|
||||
userDTO.setPassword(resolveImportDefaultPassword());
|
||||
// 根据角色名称查询角色ID
|
||||
userDTO.setRole(roleList);
|
||||
// 插入用户
|
||||
@@ -794,6 +797,7 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
||||
sysUser.setLockFlag(UserStateEnum.NORMAL.getCode());
|
||||
sysUser.setCreateBy(userDto.getUsername());
|
||||
sysUser.setUpdateBy(userDto.getUsername());
|
||||
PasswordWeakCheckUtil.assertStrong(userDto.getPassword(), userDto.getPhone(), userDto.getUsername());
|
||||
sysUser.setPassword(ENCODER.encode(userDto.getPassword()));
|
||||
sysUser.setPasswordModifyTime(LocalDateTime.now());
|
||||
sysUser.setProcessStatus(TaskCheckStatusEnum.RESULT.getCode());
|
||||
@@ -868,6 +872,10 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
||||
if (StrUtil.isEmpty(userDto.getNewpassword1())) {
|
||||
return R.failed("新密码不能为空");
|
||||
}
|
||||
String weak = PasswordWeakCheckUtil.validate(userDto.getNewpassword1(), user.getPhone(), user.getUsername());
|
||||
if (weak != null) {
|
||||
return R.failed(weak);
|
||||
}
|
||||
String password = ENCODER.encode(userDto.getNewpassword1());
|
||||
|
||||
this.update(Wrappers.<SysUser>lambdaUpdate().set(SysUser::getPassword, password).set(SysUser::getPasswordModifyTime, LocalDateTime.now()).eq(SysUser::getUserId, user.getUserId()));
|
||||
@@ -931,6 +939,12 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
||||
if (StrUtil.equals(userDto.getPassword(), userDto.getNewpassword1())) {
|
||||
return R.failed("新旧密码不能相同");
|
||||
}
|
||||
SysUser sysUser = this.getOne(Wrappers.<SysUser>lambdaQuery().eq(SysUser::getUsername, userDto.getUsername()));
|
||||
String weak = PasswordWeakCheckUtil.validate(userDto.getNewpassword1(),
|
||||
sysUser != null ? sysUser.getPhone() : null, userDto.getUsername());
|
||||
if (weak != null) {
|
||||
return R.failed(weak);
|
||||
}
|
||||
|
||||
// 重置密码
|
||||
String password = ENCODER.encode(userDto.getNewpassword1());
|
||||
@@ -1086,6 +1100,19 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Excel 导入默认密码:系统参数 USER_DEFAULT_PASSWORD,未配置则用内置强口令。
|
||||
*/
|
||||
private String resolveImportDefaultPassword() {
|
||||
String def = ParamResolver.getStr("USER_DEFAULT_PASSWORD");
|
||||
if (StrUtil.isBlank(def)) {
|
||||
def = "PjHq@2024Init!";
|
||||
}
|
||||
// 兜底:导入默认密码也走同一套强度规则(字母+数字、大于8位)
|
||||
PasswordWeakCheckUtil.assertStrong(def.trim(), null, null);
|
||||
return def.trim();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteDiscountsCacheByUserId(Long userId) {
|
||||
if (ObjectUtil.isEmpty(userId)) return;
|
||||
|
||||
+20
-3
@@ -1,15 +1,21 @@
|
||||
package com.bsd.pjcloud.wallet.utils.page;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.metadata.OrderItem;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
public class PaginationOrderSqlExtractor {
|
||||
|
||||
/** 与 SqlFilterArgumentResolver 一致:仅允许简单标识符,防止 ORDER BY 拼接注入。 */
|
||||
private static final Pattern SAFE_ORDER_COLUMN = Pattern.compile(
|
||||
"^[a-zA-Z_][a-zA-Z0-9_]{0,63}(\\.[a-zA-Z_][a-zA-Z0-9_]{0,63})?$");
|
||||
|
||||
public static String getOrderSql(Page page) {
|
||||
return getOrderSql(page, null);
|
||||
}
|
||||
@@ -18,14 +24,25 @@ public class PaginationOrderSqlExtractor {
|
||||
List<OrderItem> orderItems = page.orders();
|
||||
if (ObjectUtil.isEmpty(orderItems)) return null;
|
||||
if (ObjectUtil.isEmpty(alias)) alias = "";
|
||||
if (alias.length() > 0) alias += ".";
|
||||
if (alias.length() > 0) {
|
||||
if (!SAFE_ORDER_COLUMN.matcher(alias).matches()) {
|
||||
throw new IllegalArgumentException("排序表别名不合法");
|
||||
}
|
||||
alias += ".";
|
||||
}
|
||||
String finalAlias = alias;
|
||||
return orderItems.stream()
|
||||
.map(item -> {
|
||||
String column = item.getColumn();
|
||||
if (StrUtil.isBlank(column) || !SAFE_ORDER_COLUMN.matcher(column).matches()) {
|
||||
throw new IllegalArgumentException("排序字段不合法");
|
||||
}
|
||||
// 若列名已带别名(a.col),不再重复拼 alias
|
||||
String prefix = column.contains(".") ? "" : finalAlias;
|
||||
if (item.isAsc()) {
|
||||
return finalAlias + item.getColumn() + " asc";
|
||||
return prefix + column + " asc";
|
||||
} else {
|
||||
return finalAlias + item.getColumn() + " desc";
|
||||
return prefix + column + " desc";
|
||||
}
|
||||
})
|
||||
.collect(Collectors.joining(", "));
|
||||
|
||||
Reference in new issue
Block a user