Merge branch 'dev'

This commit is contained in:
PIGCLOUD committed 2026-01-03 23:06:51 +08:00
commit b9268e4393
525 files changed
+7448 -5836

No files matched your search

+3 -3
View File
@@ -5,7 +5,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
PigX is a proprietary enterprise-grade microservices platform built on Spring Cloud Alibaba, focusing on AI knowledge
management, document processing, and intelligent Q&A capabilities. The current version is 5.10.0.
management, document processing, and intelligent Q&A capabilities. The current version is 5.11.0.
**Important**: This is NOT an open-source project. All rights reserved by the author. Commercial use requires proper
licensing.
@@ -13,8 +13,8 @@ licensing.
## Technology Stack
- **Java Version**: Java 17
- **Spring Boot**: 3.5.6
- **Spring Cloud**: 2025.0.0
- **Spring Boot**: 3.5.9
- **Spring Cloud**: 2025.0.1
- **Spring Cloud Alibaba**: 2023.0.3.3
- **AI/ML Libraries**:
- LangChain4j: 1.6.0
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2018-2025, lengleng All rights reserved.
Copyright (c) 2018-2026, lengleng All rights reserved.
协议和授权
pigX并非一个开源软件,作者保留全部的权利。
+4 -3
View File
@@ -591,7 +591,7 @@ INSERT INTO `sys_menu` VALUES (2210,'参数管理',NULL,'/admin/param/index',NUL
INSERT INTO `sys_menu` VALUES (2211,'参数新增','sys_syspublicparam_add',NULL,NULL,2210,NULL,'1',1,'0',NULL,'1',' ','2019-04-29 22:17:36',' ','2020-03-24 08:57:11','0',1);
INSERT INTO `sys_menu` VALUES (2212,'参数删除','sys_syspublicparam_del',NULL,NULL,2210,NULL,'1',1,'0',NULL,'1',' ','2019-04-29 22:17:55',' ','2020-03-24 08:57:12','0',1);
INSERT INTO `sys_menu` VALUES (2213,'参数编辑','sys_syspublicparam_edit',NULL,NULL,2210,NULL,'1',1,'0',NULL,'1',' ','2019-04-29 22:18:14',' ','2020-03-24 08:57:13','0',1);
INSERT INTO `sys_menu` VALUES (2214,'参数查看','sys_syspublicparam_edit',NULL,NULL,2210,NULL,'1',1,'0',NULL,'1',' ','2019-04-29 22:18:14',' ','2020-03-24 08:57:13','0',1);
INSERT INTO `sys_menu` VALUES (2214,'参数查看','sys_syspublicparam_view',NULL,NULL,2210,NULL,'1',1,'0',NULL,'1',' ','2019-04-29 22:18:14',' ','2020-03-24 08:57:13','0',1);
INSERT INTO `sys_menu` VALUES (2300,'代码生成',NULL,'/gen/table/index',NULL,9000,'iconfont icon-daimashengcheng','1',2,'0','0','0','','2018-01-20 13:17:19','admin','2023-02-20 13:54:35','0',1);
INSERT INTO `sys_menu` VALUES (2400,'终端管理',NULL,'/admin/client/index',NULL,2000,'iconfont icon-shouji','1',9,'1',NULL,'0','','2018-01-20 13:17:19','admin','2023-02-16 15:25:28','0',1);
INSERT INTO `sys_menu` VALUES (2401,'客户端新增','sys_client_add',NULL,NULL,2400,'1','1',1,'0',NULL,'1',' ','2018-05-15 21:35:18',' ','2021-05-25 03:12:55','0',1);
@@ -1311,7 +1311,7 @@ CREATE TABLE `sys_tenant` (
-- Records of sys_tenant
-- ----------------------------
BEGIN;
INSERT INTO `sys_tenant` VALUES (1, '北京分公司', '1', '', NULL, NULL, NULL, NULL, NULL, '2019-05-15 00:00:00', '2099-12-31 00:00:00', '0', '0', '', 'admin', '2019-05-15 15:44:57', '2023-07-30 14:52:57', 1642752536722997250);
INSERT INTO `sys_tenant` VALUES (1, 'PIGX ADMIN', '1', '', NULL, NULL, NULL, NULL, NULL, '2019-05-15 00:00:00', '2099-12-31 00:00:00', '0', '0', '', 'admin', '2019-05-15 15:44:57', '2023-07-30 14:52:57', 1642752536722997250);
COMMIT;
@@ -1344,6 +1344,7 @@ CREATE TABLE `sys_user` (
`osc_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '开源中国标识',
`wx_cp_userid` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '企业微信唯一ID',
`wx_ding_userid` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '钉钉唯一ID',
`dept_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '主部门ID',
`tenant_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '所属租户ID',
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户表';
@@ -1352,7 +1353,7 @@ CREATE TABLE `sys_user` (
-- Records of sys_user
-- ----------------------------
BEGIN;
INSERT INTO `sys_user` VALUES (1, 'admin', '$2a$10$c/Ae0pRjJtMZg3BnvVpO.eIK6WYWVbKTzqgdy3afR7w.vd.xi3Mgy', '', '17338122125', '/admin/sys-file/local/2a14ae08150e483c93e12ac8934173e2.png', '管理员666777', '管理员', 'sw@mail.pigxl.vip', ' ', 'admin', '2018-04-20 07:15:18', '2023-04-03 14:00:06', '0', '0', NULL, '0', NULL, 'oBxPy5E-v82xWGsfzZVzkD3wEX64', NULL, 'log4j', NULL, NULL,NULL,1);
INSERT INTO `sys_user` VALUES (1, 'admin', '$2a$10$c/Ae0pRjJtMZg3BnvVpO.eIK6WYWVbKTzqgdy3afR7w.vd.xi3Mgy', '', '17338122125', '/admin/sys-file/local/2a14ae08150e483c93e12ac8934173e2.png', '管理员666777', '管理员', 'sw@mail.pigxl.vip', ' ', 'admin', '2018-04-20 07:15:18', '2023-04-03 14:00:06', '0', '0', NULL, '0', NULL, 'oBxPy5E-v82xWGsfzZVzkD3wEX64', NULL, 'log4j', NULL, NULL,NULL,1,1);
COMMIT;
-- ----------------------------
+4 -3
View File
@@ -573,7 +573,7 @@ INSERT INTO `sys_menu` VALUES (2210,'参数管理',NULL,'/admin/param/index',NUL
INSERT INTO `sys_menu` VALUES (2211,'参数新增','sys_syspublicparam_add',NULL,NULL,2210,NULL,'1',1,'0',NULL,'1',' ','2019-04-29 22:17:36',' ','2020-03-24 08:57:11','0',1);
INSERT INTO `sys_menu` VALUES (2212,'参数删除','sys_syspublicparam_del',NULL,NULL,2210,NULL,'1',1,'0',NULL,'1',' ','2019-04-29 22:17:55',' ','2020-03-24 08:57:12','0',1);
INSERT INTO `sys_menu` VALUES (2213,'参数编辑','sys_syspublicparam_edit',NULL,NULL,2210,NULL,'1',1,'0',NULL,'1',' ','2019-04-29 22:18:14',' ','2020-03-24 08:57:13','0',1);
INSERT INTO `sys_menu` VALUES (2214,'参数查看','sys_syspublicparam_edit',NULL,NULL,2210,NULL,'1',1,'0',NULL,'1',' ','2019-04-29 22:18:14',' ','2020-03-24 08:57:13','0',1);
INSERT INTO `sys_menu` VALUES (2214,'参数查看','sys_syspublicparam_view',NULL,NULL,2210,NULL,'1',1,'0',NULL,'1',' ','2019-04-29 22:18:14',' ','2020-03-24 08:57:13','0',1);
INSERT INTO `sys_menu` VALUES (2300,'代码生成',NULL,'/gen/table/index',NULL,9000,'iconfont icon-daimashengcheng','1',2,'0','0','0','','2018-01-20 13:17:19','admin','2023-02-20 13:54:35','0',1);
INSERT INTO `sys_menu` VALUES (2400,'终端管理',NULL,'/admin/client/index',NULL,2000,'iconfont icon-shouji','1',9,'1',NULL,'0','','2018-01-20 13:17:19','admin','2023-02-16 15:25:28','0',1);
INSERT INTO `sys_menu` VALUES (2401,'客户端新增','sys_client_add',NULL,NULL,2400,'1','1',1,'0',NULL,'1',' ','2018-05-15 21:35:18',' ','2021-05-25 03:12:55','0',1);
@@ -1295,7 +1295,7 @@ CREATE TABLE `sys_tenant` (
-- Records of sys_tenant
-- ----------------------------
BEGIN;
INSERT INTO `sys_tenant` VALUES (1, '北京分公司', '1', '', NULL, NULL, NULL, NULL, NULL, '2019-05-15 00:00:00', '2099-12-31 00:00:00', '0', '0', '', 'admin', '2019-05-15 15:44:57', '2023-07-30 14:52:57', 1642752536722997250);
INSERT INTO `sys_tenant` VALUES (1, 'PIGX ADMIN', '1', '', NULL, NULL, NULL, NULL, NULL, '2019-05-15 00:00:00', '2099-12-31 00:00:00', '0', '0', '', 'admin', '2019-05-15 15:44:57', '2023-07-30 14:52:57', 1642752536722997250);
COMMIT;
@@ -1328,6 +1328,7 @@ CREATE TABLE `sys_user` (
`osc_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '开源中国标识',
`wx_cp_userid` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '企业微信唯一ID',
`wx_ding_userid` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '钉钉唯一ID',
`dept_id` bigint(20) DEFAULT NULL COMMENT '主部门ID',
`tenant_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '所属租户ID',
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户表';
@@ -1336,7 +1337,7 @@ CREATE TABLE `sys_user` (
-- Records of sys_user
-- ----------------------------
BEGIN;
INSERT INTO `sys_user` VALUES (1, 'admin', '$2a$10$c/Ae0pRjJtMZg3BnvVpO.eIK6WYWVbKTzqgdy3afR7w.vd.xi3Mgy', '', '17338122125', '/admin/sys-file/local/2a14ae08150e483c93e12ac8934173e2.png', '管理员666777', '管理员', 'sw@mail.pigxl.vip', ' ', 'admin', '2018-04-20 07:15:18', '2023-04-03 14:00:06', '0', '0', NULL, '0', NULL, 'oBxPy5E-v82xWGsfzZVzkD3wEX64', NULL, 'log4j', NULL, NULL,NULL,1);
INSERT INTO `sys_user` VALUES (1, 'admin', '$2a$10$c/Ae0pRjJtMZg3BnvVpO.eIK6WYWVbKTzqgdy3afR7w.vd.xi3Mgy', '', '17338122125', '/admin/sys-file/local/2a14ae08150e483c93e12ac8934173e2.png', '管理员666777', '管理员', 'sw@mail.pigxl.vip', ' ', 'admin', '2018-04-20 07:15:18', '2023-04-03 14:00:06', '0', '0', NULL, '0', NULL, 'oBxPy5E-v82xWGsfzZVzkD3wEX64', NULL, 'log4j', NULL, NULL,NULL,1,1);
COMMIT;
-- ----------------------------
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-app-server</artifactId>
<version>5.10.0</version>
<version>5.11.0</version>
</parent>
<artifactId>pigx-app-server-api</artifactId>
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -0,0 +1,28 @@
package com.pig4cloud.pigx.app.api.enums;
/**
* App Server 模块错误编码
*
* @author lengleng
* @date 2022/3/30
*/
public interface AppErrorCodes {
// === App 用户相关 ===
String APP_USER_USERINFO_EMPTY = "app.user.userInfo.empty";
String APP_USER_USERNAME_EXISTING = "app.user.username.existing";
String APP_USER_IMPORT_SUCCEED = "app.user.import.succeed";
String APP_USER_QUERY_ERROR = "app.user.query.error";
// === App 角色相关 ===
String APP_ROLE_ROLENAME_INEXISTENCE = "app.role.roleName.inexistence";
String APP_ROLE_NAMEORCODE_EXISTING = "app.role.nameOrCode.existing";
// === App 短信相关 ===
String APP_SMS_OFTEN = "app.sms.often";
}
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -0,0 +1,14 @@
# App Server Module i18n Messages - English
# App user related
app.user.userInfo.empty=App user information is empty for {0}
app.user.username.existing=Username {0} already exists
app.user.import.succeed=User import succeeded; default password is the phone number
app.user.query.error=Failed to fetch current user information
# App role related
app.role.roleName.inexistence=Role name {0} does not exist
app.role.nameOrCode.existing=Role name or code already exists ({0} {1})
# App SMS related
app.sms.often=Verification code sent too frequently
@@ -0,0 +1,14 @@
# App Server 模块国际化消息 - 中文
# App 用户相关
app.user.userInfo.empty={0} \u7528\u6237\u4FE1\u606F\u4E3A\u7A7A
app.user.username.existing={0} \u7528\u6237\u540D\u5DF2\u5B58\u5728
app.user.import.succeed=\u7528\u6237\u5BFC\u5165\u6210\u529F\uFF0C\u9ED8\u8BA4\u5BC6\u7801\u4E3A\u624B\u673A\u53F7
app.user.query.error=\u83B7\u53D6\u5F53\u524D\u7528\u6237\u4FE1\u606F\u5931\u8D25
# App 角色相关
app.role.roleName.inexistence={0} \u89D2\u8272\u540D\u79F0\u4E0D\u5B58\u5728
app.role.nameOrCode.existing={0} {1} \u89D2\u8272\u540D\u6216\u89D2\u8272\u7F16\u7801\u5DF2\u7ECF\u5B58\u5728
# App 短信相关
app.sms.often=\u9A8C\u8BC1\u7801\u53D1\u9001\u8FC7\u9891\u7E41
+1 -4
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-app-server</artifactId>
<version>5.10.0</version>
<version>5.11.0</version>
</parent>
<artifactId>pigx-app-server-biz</artifactId>
@@ -112,9 +112,6 @@
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<loaderImplementation>CLASSIC</loaderImplementation>
</configuration>
</execution>
</executions>
</plugin>
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -21,11 +21,11 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.pig4cloud.pigx.app.api.dto.AppUserDTO;
import com.pig4cloud.pigx.app.api.entity.AppUser;
import com.pig4cloud.pigx.app.api.enums.AppErrorCodes;
import com.pig4cloud.pigx.app.api.vo.AppUserExcelVO;
import com.pig4cloud.pigx.app.service.AppUserService;
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
import com.pig4cloud.pigx.common.core.constant.enums.UserTypeEnum;
import com.pig4cloud.pigx.common.core.exception.ErrorCodes;
import com.pig4cloud.pigx.common.core.util.MsgUtils;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.excel.annotation.RequestExcel;
@@ -58,150 +58,158 @@ import java.util.List;
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
public class AppUserController {
private final AppUserService appUserService;
private final AppUserService appUserService;
@Inner
@GetMapping("/info/{username}")
public R info(@PathVariable String username) {
AppUser user = appUserService.getOne(Wrappers.<AppUser>query()
.lambda()
.eq(AppUser::getUsername, username)
.or()
.eq(AppUser::getPhone, username));
if (user == null) {
return R.failed(MsgUtils.getMessage(ErrorCodes.APP_USER_USERINFO_EMPTY, username));
}
return R.ok(appUserService.findUserInfo(user));
}
@Inner
@GetMapping("/info/{username}")
public R info(@PathVariable String username) {
AppUser user = appUserService.getOne(Wrappers.<AppUser>query()
.lambda()
.eq(AppUser::getUsername, username)
.or()
.eq(AppUser::getPhone, username));
if (user == null) {
return R.failed(MsgUtils.getMessage(AppErrorCodes.APP_USER_USERINFO_EMPTY, username));
}
return R.ok(appUserService.findUserInfo(user));
}
/**
* 获取当前用户全部信息
* @return 用户信息
*/
@GetMapping(value = { "/info" })
public R info() {
String username = SecurityUtils.getUser().getUsername();
AppUser user = appUserService.getOne(Wrappers.<AppUser>query().lambda().eq(AppUser::getUsername, username));
if (user == null) {
return R.failed(MsgUtils.getMessage(ErrorCodes.SYS_USER_QUERY_ERROR));
}
return R.ok(appUserService.findUserInfo(user));
}
/**
* 获取当前用户全部信息
*
* @return 用户信息
*/
@GetMapping(value = {"/info"})
public R info() {
String username = SecurityUtils.getUser().getUsername();
AppUser user = appUserService.getOne(Wrappers.<AppUser>query().lambda().eq(AppUser::getUsername, username));
if (user == null) {
return R.failed(MsgUtils.getMessage(AppErrorCodes.APP_USER_QUERY_ERROR));
}
return R.ok(appUserService.findUserInfo(user));
}
/**
* 分页查询
* @param page 分页对象
* @param appUserDTO app用户表
* @return
*/
@Operation(summary = "分页查询", description = "分页查询")
@GetMapping("/page")
public R getAppUserPage(Page page, AppUserDTO appUserDTO) {
return R.ok(appUserService.getUsersWithRolePage(page, appUserDTO));
}
/**
* 分页查询
*
* @param page 分页对象
* @param appUserDTO app用户表
* @return
*/
@Operation(summary = "分页查询", description = "分页查询")
@GetMapping("/page")
public R getAppUserPage(Page page, AppUserDTO appUserDTO) {
return R.ok(appUserService.getUsersWithRolePage(page, appUserDTO));
}
/**
* 通过id查询app用户表
* @param userId id
* @return R
*/
@Operation(summary = "通过id查询", description = "通过id查询")
@GetMapping("/details/{userId}")
public R getById(@PathVariable("userId") Long userId) {
return R.ok(appUserService.selectUserVoById(userId));
}
/**
* 通过id查询app用户表
*
* @param userId id
* @return R
*/
@Operation(summary = "通过id查询", description = "通过id查询")
@GetMapping("/details/{userId}")
public R getById(@PathVariable("userId") Long userId) {
return R.ok(appUserService.selectUserVoById(userId));
}
/**
* @return R
*/
@Operation(summary = "通过userName查询", description = "通过userName查询")
@GetMapping("/details")
public R getByUserName(AppUser user) {
AppUser one = appUserService.getOne(Wrappers.query(user), false);
return R.ok(one == null ? null : CommonConstants.SUCCESS);
}
/**
* @return R
*/
@Operation(summary = "通过userName查询", description = "通过userName查询")
@GetMapping("/details")
public R getByUserName(AppUser user) {
AppUser one = appUserService.getOne(Wrappers.query(user), false);
return R.ok(one == null ? null : CommonConstants.SUCCESS);
}
/**
* 新增app用户表
* @param appUser app用户表
* @return R
*/
@Operation(summary = "新增app用户表", description = "新增app用户表")
@SysLog("新增app用户表")
@PostMapping
@HasPermission("app_appuser_add")
public R save(@RequestBody AppUserDTO appUser) {
appUserService.saveUser(appUser);
return R.ok();
}
/**
* 新增app用户表
*
* @param appUser app用户表
* @return R
*/
@Operation(summary = "新增app用户表", description = "新增app用户表")
@SysLog("新增app用户表")
@PostMapping
@HasPermission("app_appuser_add")
public R save(@RequestBody AppUserDTO appUser) {
appUserService.saveUser(appUser);
return R.ok();
}
/**
* 修改app用户表
* @param appUser app用户表
* @return R
*/
@Operation(summary = "修改app用户表", description = "修改app用户表")
@SysLog("修改app用户表")
@PutMapping
@HasPermission("app_appuser_edit")
public R updateById(@RequestBody AppUserDTO appUser) {
return R.ok(appUserService.updateUser(appUser));
}
/**
* 修改app用户表
*
* @param appUser app用户表
* @return R
*/
@Operation(summary = "修改app用户表", description = "修改app用户表")
@SysLog("修改app用户表")
@PutMapping
@HasPermission("app_appuser_edit")
public R updateById(@RequestBody AppUserDTO appUser) {
return R.ok(appUserService.updateUser(appUser));
}
/**
* 通过id删除app用户表
* @param ids userIds
* @return R
*/
@Operation(summary = "通过ids删除app用户表", description = "通过ids删除app用户表")
@SysLog("通过id删除app用户表")
@DeleteMapping
@HasPermission("app_appuser_del")
public R removeById(@RequestBody Long[] ids) {
return R.ok(appUserService.deleteAppUserByIds(ids));
}
/**
* 通过id删除app用户表
*
* @param ids userIds
* @return R
*/
@Operation(summary = "通过ids删除app用户表", description = "通过ids删除app用户表")
@SysLog("通过id删除app用户表")
@DeleteMapping
@HasPermission("app_appuser_del")
public R removeById(@RequestBody Long[] ids) {
return R.ok(appUserService.deleteAppUserByIds(ids));
}
/**
* 导出excel 表格
* @param appUser 查询条件
* @return excel 文件流
*/
@ResponseExcel
@GetMapping("/export")
@HasPermission("app_appuser_export")
public List<AppUserExcelVO> export(AppUserDTO appUser) {
return appUserService.listUser(appUser);
}
/**
* 导出excel 表格
*
* @param appUser 查询条件
* @return excel 文件流
*/
@ResponseExcel
@GetMapping("/export")
@HasPermission("app_appuser_export")
public List<AppUserExcelVO> export(AppUserDTO appUser) {
return appUserService.listUser(appUser);
}
@SysLog("修改个人信息")
@PutMapping("/edit")
public R updateUserInfo(@Valid @RequestBody AppUserDTO userDto) {
if (UserTypeEnum.TOC.getStatus().equals(SecurityUtils.getUser().getUserType())) {
userDto.setUserId(SecurityUtils.getUser().getId());
// TOC 以手机号为key
userDto.setUsername(SecurityUtils.getUser().getUsername());
}
return appUserService.updateUserInfo(userDto);
}
@SysLog("修改个人信息")
@PutMapping("/edit")
public R updateUserInfo(@Valid @RequestBody AppUserDTO userDto) {
if (UserTypeEnum.TOC.getStatus().equals(SecurityUtils.getUser().getUserType())) {
userDto.setUserId(SecurityUtils.getUser().getId());
// TOC 以手机号为key
userDto.setUsername(SecurityUtils.getUser().getUsername());
}
return appUserService.updateUserInfo(userDto);
}
/**
* 导入用户
* @param excelVOList 用户列表
* @param bindingResult 错误信息列表
* @return R
*/
@PostMapping("/import")
@HasPermission("app_appuser_export")
public R importUser(@RequestExcel List<AppUserExcelVO> excelVOList, BindingResult bindingResult) {
return appUserService.importUser(excelVOList, bindingResult);
}
/**
* 导入用户
*
* @param excelVOList 用户列表
* @param bindingResult 错误信息列表
* @return R
*/
@PostMapping("/import")
@HasPermission("app_appuser_export")
public R importUser(@RequestExcel List<AppUserExcelVO> excelVOList, BindingResult bindingResult) {
return appUserService.importUser(excelVOList, bindingResult);
}
@Operation(summary = "注册app用户表", description = "注册app用户表")
@SysLog("注册app用户表")
@Inner(value = false)
@PostMapping("/register")
public R register(@RequestBody AppUserDTO appUser) {
return appUserService.registerAppUser(appUser);
}
@Operation(summary = "注册app用户表", description = "注册app用户表")
@SysLog("注册app用户表")
@Inner(value = false)
@PostMapping("/register")
public R register(@RequestBody AppUserDTO appUser) {
return appUserService.registerAppUser(appUser);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -20,11 +20,11 @@ package com.pig4cloud.pigx.app.service.impl;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.pig4cloud.pigx.app.api.enums.AppErrorCodes;
import com.pig4cloud.pigx.app.service.AppMobileService;
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
import com.pig4cloud.pigx.common.core.constant.enums.LoginTypeEnum;
import com.pig4cloud.pigx.common.core.exception.ErrorCodes;
import com.pig4cloud.pigx.common.core.util.MsgUtils;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.data.cache.RedisUtils;
@@ -43,24 +43,25 @@ import org.springframework.stereotype.Service;
@AllArgsConstructor
public class AppMobileServiceImpl implements AppMobileService {
/**
* 发送手机验证码 TODO: 调用短信网关发送验证码,测试返回前端
* @param mobile mobile
* @return code
*/
@Override
public R<Boolean> sendSmsCode(String mobile) {
String codeObj = RedisUtils.get(CacheConstants.DEFAULT_CODE_KEY + LoginTypeEnum.APPSMS.getType() + StringPool.AT + mobile);
/**
* 发送手机验证码 TODO: 调用短信网关发送验证码,测试返回前端
*
* @param mobile mobile
* @return code
*/
@Override
public R<Boolean> sendSmsCode(String mobile) {
String codeObj = RedisUtils.get(CacheConstants.DEFAULT_CODE_KEY + LoginTypeEnum.APPSMS.getType() + StringPool.AT + mobile);
if (StrUtil.isNotBlank(codeObj)) {
log.info("手机号验证码未过期:{},{}", mobile, codeObj);
return R.ok(Boolean.FALSE, MsgUtils.getMessage(ErrorCodes.SYS_APP_SMS_OFTEN));
}
if (StrUtil.isNotBlank(codeObj)) {
log.info("手机号验证码未过期:{},{}", mobile, codeObj);
return R.ok(Boolean.FALSE, MsgUtils.getMessage(AppErrorCodes.APP_SMS_OFTEN));
}
String code = RandomUtil.randomNumbers(Integer.parseInt(SecurityConstants.CODE_SIZE));
log.debug("手机号生成验证码成功:{},{}", mobile, code);
RedisUtils.set(CacheConstants.DEFAULT_CODE_KEY + LoginTypeEnum.APPSMS.getType() + StringPool.AT + mobile, code, SecurityConstants.CODE_TIME);
return R.ok(Boolean.TRUE);
}
String code = RandomUtil.randomNumbers(Integer.parseInt(SecurityConstants.CODE_SIZE));
log.debug("手机号生成验证码成功:{},{}", mobile, code);
RedisUtils.set(CacheConstants.DEFAULT_CODE_KEY + LoginTypeEnum.APPSMS.getType() + StringPool.AT + mobile, code, SecurityConstants.CODE_TIME);
return R.ok(Boolean.TRUE);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -17,16 +17,20 @@
package com.pig4cloud.pigx.app.service.impl;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pig4cloud.pigx.app.api.entity.AppPageEntity;
import com.pig4cloud.pigx.app.api.entity.AppRole;
import com.pig4cloud.pigx.app.api.enums.AppErrorCodes;
import com.pig4cloud.pigx.app.api.enums.PageTypeEnums;
import com.pig4cloud.pigx.app.api.vo.AppRoleExcelVO;
import com.pig4cloud.pigx.app.mapper.AppPageMapper;
import com.pig4cloud.pigx.app.mapper.AppRoleMapper;
import com.pig4cloud.pigx.app.service.AppRoleService;
import com.pig4cloud.pigx.common.core.exception.ErrorCodes;
import com.pig4cloud.pigx.common.core.util.MsgUtils;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.excel.vo.ErrorMessage;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.validation.BindingResult;
@@ -35,79 +39,86 @@ import java.util.List;
import java.util.Set;
/**
* app角色表
* APP角色服务实现类
* <p>
* 提供对APP角色相关的业务操作实现。
*
* @author aeizzz
* @date 2022-12-07 09:52:03
* @author lengleng
* @date 2025/05/29
*/
@Service
@AllArgsConstructor
@RequiredArgsConstructor
public class AppRoleServiceImpl extends ServiceImpl<AppRoleMapper, AppRole> implements AppRoleService {
@Override
public List<AppRole> findRolesByUserId(Long userId) {
return baseMapper.listRolesByUserId(userId);
}
private final AppPageMapper appPageMapper;
/**
* 删除用户的同时,把role_menu关系删除
* @param ids roleIds
*/
@Override
public Boolean deleteRoleByIds(Long[] ids) {
this.removeBatchByIds(CollUtil.toList(ids));
return Boolean.TRUE;
}
private final AppRoleMapper appRoleMapper;
/**
* 导入角色
* @param excelVOList
* @param bindingResult
* @return
*/
@Override
public R importRole(List<AppRoleExcelVO> excelVOList, BindingResult bindingResult) {
// 通用校验获取失败的数据
List<ErrorMessage> errorMessageList = (List<ErrorMessage>) bindingResult.getTarget();
@Override
public List<AppRole> findRolesByUserId(Long userId) {
return baseMapper.listRolesByUserId(userId);
}
// 个性化校验逻辑
List<AppRole> roleList = this.list();
/**
* 删除用户的同时,把role_menu关系删除
*
* @param ids roleIds
*/
@Override
public Boolean deleteRoleByIds(Long[] ids) {
this.removeBatchByIds(CollUtil.toList(ids));
return Boolean.TRUE;
}
// 执行数据插入操作 组装 RoleDto
for (AppRoleExcelVO excel : excelVOList) {
Set<String> errorMsg = new HashSet<>();
// 检验角色名称或者角色编码是否存在
boolean existRole = roleList.stream()
.anyMatch(appRole -> excel.getRoleName().equals(appRole.getRoleName())
|| excel.getRoleCode().equals(appRole.getRoleCode()));
/**
* 导入角色
*
* @param excelVOList
* @param bindingResult
* @return
*/
@Override
public R importRole(List<AppRoleExcelVO> excelVOList, BindingResult bindingResult) {
// 通用校验获取失败的数据
List<ErrorMessage> errorMessageList = (List<ErrorMessage>) bindingResult.getTarget();
if (existRole) {
errorMsg.add(MsgUtils.getMessage(ErrorCodes.SYS_ROLE_NAMEORCODE_EXISTING, excel.getRoleName(),
excel.getRoleCode()));
}
// 个性化校验逻辑
List<AppRole> roleList = this.list();
// 数据合法情况
if (CollUtil.isEmpty(errorMsg)) {
insertExcelRole(excel);
}
else {
// 数据不合法情况
errorMessageList.add(new ErrorMessage(excel.getLineNum(), errorMsg));
}
}
if (CollUtil.isNotEmpty(errorMessageList)) {
return R.failed(errorMessageList);
}
return R.ok();
// 执行数据插入操作 组装 RoleDto
for (AppRoleExcelVO excel : excelVOList) {
Set<String> errorMsg = new HashSet<>();
// 检验角色名称或者角色编码是否存在
boolean existRole = roleList.stream()
.anyMatch(appRole -> excel.getRoleName().equals(appRole.getRoleName())
|| excel.getRoleCode().equals(appRole.getRoleCode()));
}
if (existRole) {
errorMsg.add(MsgUtils.getMessage(AppErrorCodes.APP_ROLE_NAMEORCODE_EXISTING, excel.getRoleName(),
excel.getRoleCode()));
}
private void insertExcelRole(AppRoleExcelVO excel) {
AppRole appRole = new AppRole();
appRole.setRoleName(excel.getRoleName());
appRole.setRoleDesc(excel.getRoleDesc());
appRole.setRoleCode(excel.getRoleCode());
this.save(appRole);
}
// 数据合法情况
if (CollUtil.isEmpty(errorMsg)) {
insertExcelRole(excel);
} else {
// 数据不合法情况
errorMessageList.add(new ErrorMessage(excel.getLineNum(), errorMsg));
}
}
if (CollUtil.isNotEmpty(errorMessageList)) {
return R.failed(errorMessageList);
}
return R.ok();
}
private void insertExcelRole(AppRoleExcelVO excel) {
AppRole appRole = new AppRole();
appRole.setRoleName(excel.getRoleName());
appRole.setRoleDesc(excel.getRoleDesc());
appRole.setRoleCode(excel.getRoleCode());
this.save(appRole);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -29,6 +29,7 @@ import com.pig4cloud.pigx.app.api.dto.AppUserInfo;
import com.pig4cloud.pigx.app.api.entity.AppRole;
import com.pig4cloud.pigx.app.api.entity.AppUser;
import com.pig4cloud.pigx.app.api.entity.AppUserRole;
import com.pig4cloud.pigx.app.api.enums.AppErrorCodes;
import com.pig4cloud.pigx.app.api.vo.AppUserExcelVO;
import com.pig4cloud.pigx.app.api.vo.AppUserVO;
import com.pig4cloud.pigx.app.mapper.AppUserMapper;
@@ -39,7 +40,6 @@ import com.pig4cloud.pigx.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
import com.pig4cloud.pigx.common.core.constant.enums.LoginTypeEnum;
import com.pig4cloud.pigx.common.core.constant.enums.UserTypeEnum;
import com.pig4cloud.pigx.common.core.exception.ErrorCodes;
import com.pig4cloud.pigx.common.core.util.MsgUtils;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.data.cache.RedisUtils;
@@ -72,328 +72,338 @@ import java.util.Set;
@Slf4j
public class AppUserServiceImpl extends ServiceImpl<AppUserMapper, AppUser> implements AppUserService {
/**
* 密码编码器,用于密码加密
*/
private static final PasswordEncoder ENCODER = new BCryptPasswordEncoder();
/**
* 密码编码器,用于密码加密
*/
private static final PasswordEncoder ENCODER = new BCryptPasswordEncoder();
/**
* 用户角色关联服务
*/
private final AppUserRoleService appUserRoleService;
/**
* 用户角色关联服务
*/
private final AppUserRoleService appUserRoleService;
/**
* 角色服务
*/
private final AppRoleService appRoleService;
/**
* 角色服务
*/
private final AppRoleService appRoleService;
/**
* 缓存管理器
*/
private final CacheManager cacheManager;
/**
* 缓存管理器
*/
private final CacheManager cacheManager;
/**
* 更新用户信息
* @param userDTO 用户数据传输对象
* @return 更新结果
*/
@Override
@CacheEvict(value = CacheConstants.USER_DETAILS_MINI, key = "#userDTO.username")
public Boolean updateUser(AppUserDTO userDTO) {
// 创建用户实体并复制属性
AppUser appUser = new AppUser();
BeanUtils.copyProperties(userDTO, appUser);
/**
* 更新用户信息
*
* @param userDTO 用户数据传输对象
* @return 更新结果
*/
@Override
@CacheEvict(value = CacheConstants.USER_DETAILS_MINI, key = "#userDTO.username")
public Boolean updateUser(AppUserDTO userDTO) {
// 创建用户实体并复制属性
AppUser appUser = new AppUser();
BeanUtils.copyProperties(userDTO, appUser);
// 如果密码不为空,进行加密处理
if (StrUtil.isNotBlank(userDTO.getPassword())) {
appUser.setPassword(ENCODER.encode(userDTO.getPassword()));
}
// 更新用户基本信息
this.updateById(appUser);
// 如果密码不为空,进行加密处理
if (StrUtil.isNotBlank(userDTO.getPassword())) {
appUser.setPassword(ENCODER.encode(userDTO.getPassword()));
}
// 更新用户基本信息
this.updateById(appUser);
// 删除用户与角色的关联关系
appUserRoleService.remove(Wrappers.<AppUserRole>lambdaQuery().eq(AppUserRole::getUserId, appUser.getUserId()));
// 重新建立用户与角色的关联关系
userDTO.getRole().forEach(roleId -> {
AppUserRole appUserRole = new AppUserRole();
appUserRole.setRoleId(roleId);
appUserRole.setUserId(userDTO.getUserId());
appUserRole.insert();
});
// 删除用户与角色的关联关系
appUserRoleService.remove(Wrappers.<AppUserRole>lambdaQuery().eq(AppUserRole::getUserId, appUser.getUserId()));
// 重新建立用户与角色的关联关系
userDTO.getRole().forEach(roleId -> {
AppUserRole appUserRole = new AppUserRole();
appUserRole.setRoleId(roleId);
appUserRole.setUserId(userDTO.getUserId());
appUserRole.insert();
});
// 清除以手机号为key的用户缓存
AppUser appUserInfo = baseMapper.selectById(userDTO.getUserId());
cacheManager.getCache(CacheConstants.USER_DETAILS_MINI).evict(appUserInfo.getPhone());
return Boolean.TRUE;
}
// 清除以手机号为key的用户缓存
AppUser appUserInfo = baseMapper.selectById(userDTO.getUserId());
cacheManager.getCache(CacheConstants.USER_DETAILS_MINI).evict(appUserInfo.getPhone());
return Boolean.TRUE;
}
/**
* 新增用户
* @param userDTO 用户数据传输对象
* @return 保存结果
*/
@Override
public Boolean saveUser(AppUserDTO userDTO) {
// 创建用户实体并复制属性
AppUser appUser = new AppUser();
BeanUtils.copyProperties(userDTO, appUser);
// 设置删除标志为正常状态
appUser.setDelFlag(CommonConstants.STATUS_NORMAL);
// 对密码进行加密
appUser.setPassword(ENCODER.encode(userDTO.getPassword()));
// 插入用户记录
baseMapper.insert(appUser);
/**
* 新增用户
*
* @param userDTO 用户数据传输对象
* @return 保存结果
*/
@Override
public Boolean saveUser(AppUserDTO userDTO) {
// 创建用户实体并复制属性
AppUser appUser = new AppUser();
BeanUtils.copyProperties(userDTO, appUser);
// 设置删除标志为正常状态
appUser.setDelFlag(CommonConstants.STATUS_NORMAL);
// 对密码进行加密
appUser.setPassword(ENCODER.encode(userDTO.getPassword()));
// 插入用户记录
baseMapper.insert(appUser);
// 如果角色列表不为空,建立用户与角色的关联关系
if (CollUtil.isNotEmpty(userDTO.getRole())) {
List<AppUserRole> userRoleList = userDTO.getRole().stream().map(roleId -> {
AppUserRole userRole = new AppUserRole();
userRole.setUserId(appUser.getUserId());
userRole.setRoleId(roleId);
return userRole;
}).toList();
// 批量保存用户角色关联
appUserRoleService.saveBatch(userRoleList);
}
return Boolean.TRUE;
}
// 如果角色列表不为空,建立用户与角色的关联关系
if (CollUtil.isNotEmpty(userDTO.getRole())) {
List<AppUserRole> userRoleList = userDTO.getRole().stream().map(roleId -> {
AppUserRole userRole = new AppUserRole();
userRole.setUserId(appUser.getUserId());
userRole.setRoleId(roleId);
return userRole;
}).toList();
// 批量保存用户角色关联
appUserRoleService.saveBatch(userRoleList);
}
return Boolean.TRUE;
}
/**
* 查询全部用户(用于Excel导出)
* @param appUser 查询条件
* @return 用户Excel视图对象列表
*/
@Override
public List<AppUserExcelVO> listUser(AppUserDTO appUser) {
// 查询所有用户
List<AppUser> appUsers = baseMapper.selectList(null);
// 将用户实体转换为Excel视图对象
List<AppUserExcelVO> appUserExcelVOS = appUsers.stream().map(item -> {
AppUserExcelVO appUserExcelVO = new AppUserExcelVO();
BeanUtils.copyProperties(item, appUserExcelVO);
return appUserExcelVO;
}).toList();
return appUserExcelVOS;
}
/**
* 查询全部用户(用于Excel导出)
*
* @param appUser 查询条件
* @return 用户Excel视图对象列表
*/
@Override
public List<AppUserExcelVO> listUser(AppUserDTO appUser) {
// 查询所有用户
List<AppUser> appUsers = baseMapper.selectList(null);
// 将用户实体转换为Excel视图对象
List<AppUserExcelVO> appUserExcelVOS = appUsers.stream().map(item -> {
AppUserExcelVO appUserExcelVO = new AppUserExcelVO();
BeanUtils.copyProperties(item, appUserExcelVO);
return appUserExcelVO;
}).toList();
return appUserExcelVOS;
}
/**
* 分页查询用户(包含角色信息)
* @param page 分页参数
* @param appUserDTO 查询条件
* @return 分页结果
*/
@Override
public IPage getUsersWithRolePage(Page page, AppUserDTO appUserDTO) {
return baseMapper.getUserVosPage(page, appUserDTO);
}
/**
* 分页查询用户(包含角色信息)
*
* @param page 分页参数
* @param appUserDTO 查询条件
* @return 分页结果
*/
@Override
public IPage getUsersWithRolePage(Page page, AppUserDTO appUserDTO) {
return baseMapper.getUserVosPage(page, appUserDTO);
}
/**
* 查询用户详细信息(包含角色和权限)
* @param user 用户对象
* @return 用户信息对象
*/
@Override
public AppUserInfo findUserInfo(AppUser user) {
// 创建用户信息对象
AppUserInfo info = new AppUserInfo();
info.setAppUser(user);
/**
* 查询用户详细信息(包含角色和权限)
*
* @param user 用户对象
* @return 用户信息对象
*/
@Override
public AppUserInfo findUserInfo(AppUser user) {
// 创建用户信息对象
AppUserInfo info = new AppUserInfo();
info.setAppUser(user);
// 设置角色列表(ID数组)
List<Long> roleIds = appRoleService.findRolesByUserId(user.getUserId())
.stream()
.map(AppRole::getRoleId)
.toList();
info.setRoles(ArrayUtil.toArray(roleIds, Long.class));
// 设置角色列表(ID数组)
List<Long> roleIds = appRoleService.findRolesByUserId(user.getUserId())
.stream()
.map(AppRole::getRoleId)
.toList();
info.setRoles(ArrayUtil.toArray(roleIds, Long.class));
// 设置权限列表(暂为空)
Set<String> permissions = new HashSet<>();
info.setPermissions(ArrayUtil.toArray(permissions, String.class));
return info;
}
// 设置权限列表(暂为空)
Set<String> permissions = new HashSet<>();
info.setPermissions(ArrayUtil.toArray(permissions, String.class));
return info;
}
/**
* 更新用户个人信息
* @param userDTO 用户数据传输对象
* @return 操作结果
*/
@Override
@CacheEvict(value = CacheConstants.USER_DETAILS_MINI, key = "#userDTO.username")
public R updateUserInfo(AppUserDTO userDTO) {
// 获取当前登录用户
PigxUser user = SecurityUtils.getUser();
/**
* 更新用户个人信息
*
* @param userDTO 用户数据传输对象
* @return 操作结果
*/
@Override
@CacheEvict(value = CacheConstants.USER_DETAILS_MINI, key = "#userDTO.username")
public R updateUserInfo(AppUserDTO userDTO) {
// 获取当前登录用户
PigxUser user = SecurityUtils.getUser();
// C端客户修改手机号需要验证短信验证码
if (UserTypeEnum.TOC.getStatus().equals(user.getUserType()) && StrUtil.isNotBlank(userDTO.getPhone())) {
// 构建验证码缓存key
String key = CacheConstants.DEFAULT_CODE_KEY + LoginTypeEnum.APPSMS.getType() + StringPool.AT
+ userDTO.getPhone();
// 获取缓存中的验证码
String codeObj = RedisUtils.get(key);
// C端客户修改手机号需要验证短信验证码
if (UserTypeEnum.TOC.getStatus().equals(user.getUserType()) && StrUtil.isNotBlank(userDTO.getPhone())) {
// 构建验证码缓存key
String key = CacheConstants.DEFAULT_CODE_KEY + LoginTypeEnum.APPSMS.getType() + StringPool.AT
+ userDTO.getPhone();
// 获取缓存中的验证码
String codeObj = RedisUtils.get(key);
// 验证码不匹配,返回错误
if (!userDTO.getMobileCode().equals(codeObj)) {
return R.failed("验证码错误");
}
}
// 验证码不匹配,返回错误
if (!userDTO.getMobileCode().equals(codeObj)) {
return R.failed("验证码错误");
}
}
// 更新密码(如果提供了新密码)
if (StrUtil.isNotBlank(userDTO.getPassword())) {
userDTO.setPassword(ENCODER.encode(userDTO.getPassword()));
}
// 更新密码(如果提供了新密码)
if (StrUtil.isNotBlank(userDTO.getPassword())) {
userDTO.setPassword(ENCODER.encode(userDTO.getPassword()));
}
// 获取原用户信息并更新
AppUser appUser = baseMapper.selectById(userDTO.getUserId());
BeanUtils.copyProperties(userDTO, appUser);
// 获取原用户信息并更新
AppUser appUser = baseMapper.selectById(userDTO.getUserId());
BeanUtils.copyProperties(userDTO, appUser);
// 清除以手机号为key的用户缓存
cacheManager.getCache(CacheConstants.USER_DETAILS_MINI).evict(user.getPhone());
return R.ok(this.updateById(appUser));
}
// 清除以手机号为key的用户缓存
cacheManager.getCache(CacheConstants.USER_DETAILS_MINI).evict(user.getPhone());
return R.ok(this.updateById(appUser));
}
/**
* 根据用户ID查询用户视图对象
* @param userId 用户ID
* @return 用户视图对象
*/
@Override
public AppUserVO selectUserVoById(Long userId) {
return baseMapper.getUserVoById(userId);
}
/**
* 根据用户ID查询用户视图对象
*
* @param userId 用户ID
* @return 用户视图对象
*/
@Override
public AppUserVO selectUserVoById(Long userId) {
return baseMapper.getUserVoById(userId);
}
/**
* 批量删除用户
* @param ids 用户ID数组
* @return 删除结果
*/
@Override
public Boolean deleteAppUserByIds(Long[] ids) {
// 获取缓存对象
Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS_MINI);
// 清除用户缓存
for (AppUser appUser : baseMapper.selectByIds(CollUtil.toList(ids))) {
cache.evict(appUser.getUsername());
}
/**
* 批量删除用户
*
* @param ids 用户ID数组
* @return 删除结果
*/
@Override
public Boolean deleteAppUserByIds(Long[] ids) {
// 获取缓存对象
Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS_MINI);
// 清除用户缓存
for (AppUser appUser : baseMapper.selectByIds(CollUtil.toList(ids))) {
cache.evict(appUser.getUsername());
}
// 删除用户角色关联关系
this.appUserRoleService
.remove(Wrappers.<AppUserRole>lambdaQuery().in(AppUserRole::getUserId, CollUtil.toList(ids)));
// 删除用户角色关联关系
this.appUserRoleService
.remove(Wrappers.<AppUserRole>lambdaQuery().in(AppUserRole::getUserId, CollUtil.toList(ids)));
// 删除用户记录
this.removeBatchByIds(CollUtil.toList(ids));
// 删除用户记录
this.removeBatchByIds(CollUtil.toList(ids));
return Boolean.TRUE;
}
return Boolean.TRUE;
}
/**
* 注册APP用户
* @param appUser 用户数据传输对象
* @return 操作结果
*/
@Override
public R registerAppUser(AppUserDTO appUser) {
// 检查手机号是否已被注册
List<AppUser> appUserList = baseMapper
.selectList(Wrappers.<AppUser>lambdaQuery().eq(AppUser::getPhone, appUser.getPhone()));
/**
* 注册APP用户
*
* @param appUser 用户数据传输对象
* @return 操作结果
*/
@Override
public R registerAppUser(AppUserDTO appUser) {
// 检查手机号是否已被注册
List<AppUser> appUserList = baseMapper
.selectList(Wrappers.<AppUser>lambdaQuery().eq(AppUser::getPhone, appUser.getPhone()));
if (CollUtil.isNotEmpty(appUserList)) {
return R.failed("手机号已注册,请使用验证码直接登录");
}
if (CollUtil.isNotEmpty(appUserList)) {
return R.failed("手机号已注册,请使用验证码直接登录");
}
// 验证短信验证码
String key = CacheConstants.DEFAULT_CODE_KEY + LoginTypeEnum.APPSMS.getType() + StringPool.AT
+ appUser.getPhone();
String codeObj = RedisUtils.get(key);
// 验证短信验证码
String key = CacheConstants.DEFAULT_CODE_KEY + LoginTypeEnum.APPSMS.getType() + StringPool.AT
+ appUser.getPhone();
String codeObj = RedisUtils.get(key);
if (!appUser.getMobileCode().equals(codeObj)) {
return R.failed("验证码错误");
}
if (!appUser.getMobileCode().equals(codeObj)) {
return R.failed("验证码错误");
}
// 创建用户并保存
AppUser app = new AppUser();
BeanUtils.copyProperties(appUser, app);
appUser.setUsername(app.getPhone()); // 默认使用手机号作为用户名
appUser.setDelFlag(CommonConstants.STATUS_NORMAL);
appUser.setPassword(ENCODER.encode(appUser.getPassword()));
baseMapper.insert(appUser);
return null;
}
// 创建用户并保存
AppUser app = new AppUser();
BeanUtils.copyProperties(appUser, app);
appUser.setUsername(app.getPhone()); // 默认使用手机号作为用户名
appUser.setDelFlag(CommonConstants.STATUS_NORMAL);
appUser.setPassword(ENCODER.encode(appUser.getPassword()));
baseMapper.insert(appUser);
return null;
}
/**
* 导入用户(从Excel导入)
* @param excelVOList Excel数据列表
* @param bindingResult 数据校验结果
* @return 导入结果
*/
@Override
public R importUser(List<AppUserExcelVO> excelVOList, BindingResult bindingResult) {
// 获取通用校验失败的数据
List<ErrorMessage> errorMessageList = (List<ErrorMessage>) bindingResult.getTarget();
/**
* 导入用户(从Excel导入)
*
* @param excelVOList Excel数据列表
* @param bindingResult 数据校验结果
* @return 导入结果
*/
@Override
public R importUser(List<AppUserExcelVO> excelVOList, BindingResult bindingResult) {
// 获取通用校验失败的数据
List<ErrorMessage> errorMessageList = (List<ErrorMessage>) bindingResult.getTarget();
// 获取现有用户和角色列表,用于校验
List<AppUser> userList = this.list();
List<AppRole> roleList = appRoleService.list();
// 获取现有用户和角色列表,用于校验
List<AppUser> userList = this.list();
List<AppRole> roleList = appRoleService.list();
// 遍历Excel数据进行处理
for (AppUserExcelVO excel : excelVOList) {
Set<String> errorMsg = new HashSet<>();
// 遍历Excel数据进行处理
for (AppUserExcelVO excel : excelVOList) {
Set<String> errorMsg = new HashSet<>();
// 校验用户名是否已存在
boolean exsitUserName = userList.stream()
.anyMatch(sysUser -> excel.getUsername().equals(sysUser.getUsername()));
// 校验用户名是否已存在
boolean exsitUserName = userList.stream()
.anyMatch(sysUser -> excel.getUsername().equals(sysUser.getUsername()));
if (exsitUserName) {
errorMsg.add(MsgUtils.getMessage(ErrorCodes.SYS_USER_USERNAME_EXISTING, excel.getUsername()));
}
if (exsitUserName) {
errorMsg.add(MsgUtils.getMessage(AppErrorCodes.APP_USER_USERNAME_EXISTING, excel.getUsername()));
}
// 校验角色名称是否合法
List<String> roleNameList = StrUtil.split(excel.getRoleNameList(), StrUtil.COMMA);
List<AppRole> roleCollList = roleList.stream()
.filter(role -> roleNameList.stream().anyMatch(name -> role.getRoleName().equals(name)))
.toList();
// 校验角色名称是否合法
List<String> roleNameList = StrUtil.split(excel.getRoleNameList(), StrUtil.COMMA);
List<AppRole> roleCollList = roleList.stream()
.filter(role -> roleNameList.stream().anyMatch(name -> role.getRoleName().equals(name)))
.toList();
if (roleCollList.size() != roleNameList.size()) {
errorMsg.add(MsgUtils.getMessage(ErrorCodes.SYS_ROLE_ROLENAME_INEXISTENCE, excel.getRoleNameList()));
}
if (roleCollList.size() != roleNameList.size()) {
errorMsg.add(MsgUtils.getMessage(AppErrorCodes.APP_ROLE_ROLENAME_INEXISTENCE, excel.getRoleNameList()));
}
// 数据合法时执行插入
if (CollUtil.isEmpty(errorMsg)) {
insertExcelUser(excel, roleCollList);
}
else {
// 记录不合法数据的错误信息
errorMessageList.add(new ErrorMessage(excel.getLineNum(), errorMsg));
}
}
// 数据合法时执行插入
if (CollUtil.isEmpty(errorMsg)) {
insertExcelUser(excel, roleCollList);
} else {
// 记录不合法数据的错误信息
errorMessageList.add(new ErrorMessage(excel.getLineNum(), errorMsg));
}
}
// 存在错误信息时返回失败
if (CollUtil.isNotEmpty(errorMessageList)) {
return R.failed(errorMessageList);
}
return R.ok(null, MsgUtils.getMessage(ErrorCodes.SYS_USER_IMPORT_SUCCEED));
}
// 存在错误信息时返回失败
if (CollUtil.isNotEmpty(errorMessageList)) {
return R.failed(errorMessageList);
}
return R.ok(null, MsgUtils.getMessage(AppErrorCodes.APP_USER_IMPORT_SUCCEED));
}
/**
* 插入Excel导入的用户数据
* @param excel Excel用户数据
* @param roleCollList 角色列表
*/
private void insertExcelUser(AppUserExcelVO excel, List<AppRole> roleCollList) {
// 创建用户DTO并设置基本信息
AppUserDTO userDTO = new AppUserDTO();
userDTO.setUsername(excel.getUsername());
userDTO.setPhone(excel.getPhone());
userDTO.setNickname(excel.getNickname());
userDTO.setName(excel.getName());
userDTO.setEmail(excel.getEmail());
// 批量导入时初始密码设置为手机号
userDTO.setPassword(userDTO.getPhone());
/**
* 插入Excel导入的用户数据
*
* @param excel Excel用户数据
* @param roleCollList 角色列表
*/
private void insertExcelUser(AppUserExcelVO excel, List<AppRole> roleCollList) {
// 创建用户DTO并设置基本信息
AppUserDTO userDTO = new AppUserDTO();
userDTO.setUsername(excel.getUsername());
userDTO.setPhone(excel.getPhone());
userDTO.setNickname(excel.getNickname());
userDTO.setName(excel.getName());
userDTO.setEmail(excel.getEmail());
// 批量导入时初始密码设置为手机号
userDTO.setPassword(userDTO.getPhone());
// 获取角色ID列表
List<Long> roleIdList = roleCollList.stream().map(AppRole::getRoleId).toList();
userDTO.setRole(roleIdList);
// 获取角色ID列表
List<Long> roleIdList = roleCollList.stream().map(AppRole::getRoleId).toList();
userDTO.setRole(roleIdList);
// 保存用户
this.saveUser(userDTO);
}
// 保存用户
this.saveUser(userDTO);
}
}
@@ -2,7 +2,7 @@
<!--
~
~ Copyright (c) 2018-2025, lengleng All rights reserved.
~ Copyright (c) 2018-2026, lengleng All rights reserved.
~
~ Redistribution and use in source and binary forms, with or without
~ modification, are permitted provided that the following conditions are met:
@@ -2,7 +2,7 @@
<!--
~
~ Copyright (c) 2018-2025, lengleng All rights reserved.
~ Copyright (c) 2018-2026, lengleng All rights reserved.
~
~ Redistribution and use in source and binary forms, with or without
~ modification, are permitted provided that the following conditions are met:
@@ -2,7 +2,7 @@
<!--
~
~ Copyright (c) 2018-2025, lengleng All rights reserved.
~ Copyright (c) 2018-2026, lengleng All rights reserved.
~
~ Redistribution and use in source and binary forms, with or without
~ modification, are permitted provided that the following conditions are met:
+1 -1
View File
@@ -7,7 +7,7 @@
<parent>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx</artifactId>
<version>5.10.0</version>
<version>5.11.0</version>
</parent>
<artifactId>pigx-app-server</artifactId>
+2 -5
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~
~ Copyright (c) 2018-2025, lengleng All rights reserved.
~ Copyright (c) 2018-2026, lengleng All rights reserved.
~
~ Redistribution and use in source and binary forms, with or without
~ modification, are permitted provided that the following conditions are met:
@@ -24,7 +24,7 @@
<parent>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx</artifactId>
<version>5.10.0</version>
<version>5.11.0</version>
</parent>
<artifactId>pigx-auth</artifactId>
@@ -128,9 +128,6 @@
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<loaderImplementation>CLASSIC</loaderImplementation>
</configuration>
</execution>
</executions>
</plugin>
@@ -30,6 +30,7 @@ import com.pig4cloud.pigx.auth.support.password.OAuth2ResourceOwnerPasswordAuthe
import com.pig4cloud.pigx.auth.support.sms.OAuth2ResourceOwnerSmsAuthenticationConverter;
import com.pig4cloud.pigx.auth.support.sms.OAuth2ResourceOwnerSmsAuthenticationProvider;
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
import com.pig4cloud.pigx.common.security.component.PigxBootCorsProperties;
import com.pig4cloud.pigx.common.security.handler.FormAuthenticationFailureHandler;
import com.pig4cloud.pigx.common.security.handler.SsoLogoutSuccessHandler;
import lombok.RequiredArgsConstructor;
@@ -56,6 +57,8 @@ import org.springframework.security.web.authentication.AuthenticationFailureHand
import org.springframework.security.web.authentication.DelegatingAuthenticationConverter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@@ -69,114 +72,131 @@ import java.util.Arrays;
@RequiredArgsConstructor
public class AuthorizationServerConfiguration {
private final OAuth2AuthorizationService authorizationService;
private final OAuth2AuthorizationService authorizationService;
private final PasswordDecoderFilter passwordDecoderFilter;
private final PasswordDecoderFilter passwordDecoderFilter;
private final ValidateCodeFilter validateCodeFilter;
private final ValidateCodeFilter validateCodeFilter;
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,
PigxAuthenticationSuccessEventHandler successEventHandler,
PigxAuthenticationFailureEventHandler failureEventHandler) throws Exception {
// 配置授权服务器的安全策略,只有/oauth2/**的请求才会走如下的配置
http.securityMatcher("/oauth2/**");
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();
private final PigxBootCorsProperties corsProperties;
// 增加验证码过滤器
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
// 增加密码解密过滤器
http.addFilterBefore(passwordDecoderFilter, UsernamePasswordAuthenticationFilter.class);
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http,
PigxAuthenticationSuccessEventHandler successEventHandler,
PigxAuthenticationFailureEventHandler failureEventHandler) throws Exception {
// 配置授权服务器的安全策略,只有/oauth2/**的请求才会走如下的配置
http.securityMatcher("/oauth2/**");
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();
http.with(authorizationServerConfigurer.tokenEndpoint((tokenEndpoint) -> {// 个性化认证授权端点
tokenEndpoint.accessTokenRequestConverter(accessTokenRequestConverter()) // 注入自定义的授权认证Converter
.accessTokenResponseHandler(successEventHandler) // 登录成功处理器
.errorResponseHandler(failureEventHandler);// 登录失败处理器
}).clientAuthentication(oAuth2ClientAuthenticationConfigurer -> // 个性化客户端认证
oAuth2ClientAuthenticationConfigurer.errorResponseHandler(failureEventHandler))// 处理客户端认证异常
.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint// 授权码端点个性化confirm页面
.consentPage(SecurityConstants.CUSTOM_CONSENT_PAGE_URI)), Customizer.withDefaults())
.authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated());
// 增加验证码过滤器
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
// 增加密码解密过滤器
http.addFilterBefore(passwordDecoderFilter, UsernamePasswordAuthenticationFilter.class);
// 设置 Token 存储的策略
http.with(authorizationServerConfigurer.authorizationService(authorizationService)// redis存储token的实现
.authorizationServerSettings(
AuthorizationServerSettings.builder().issuer(SecurityConstants.PIGX_LICENSE).build()),
Customizer.withDefaults());
http.with(authorizationServerConfigurer.tokenEndpoint((tokenEndpoint) -> {// 个性化认证授权端点
tokenEndpoint.accessTokenRequestConverter(accessTokenRequestConverter()) // 注入自定义的授权认证Converter
.accessTokenResponseHandler(successEventHandler) // 登录成功处理器
.errorResponseHandler(failureEventHandler);// 登录失败处理器
}).clientAuthentication(oAuth2ClientAuthenticationConfigurer -> // 个性化客户端认证
oAuth2ClientAuthenticationConfigurer.errorResponseHandler(failureEventHandler))// 处理客户端认证异常
.authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint// 授权码端点个性化confirm页面
.consentPage(SecurityConstants.CUSTOM_CONSENT_PAGE_URI)), Customizer.withDefaults())
.authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated());
// 设置授权码模式登录页面
http.with(new FormIdentityLoginConfigurer(), Customizer.withDefaults());
DefaultSecurityFilterChain securityFilterChain = http.build();
// 设置 Token 存储的策略
http.with(authorizationServerConfigurer.authorizationService(authorizationService)// redis存储token的实现
.authorizationServerSettings(
AuthorizationServerSettings.builder().issuer(SecurityConstants.PIGX_LICENSE).build()),
Customizer.withDefaults());
// 注入自定义授权模式实现
addCustomOAuth2GrantAuthenticationProvider(http);
return securityFilterChain;
}
// 设置授权码模式登录页面
http.with(new FormIdentityLoginConfigurer(), Customizer.withDefaults());
/**
* 令牌生成规则实现 </br>
* client:username:uuid
*
* @return OAuth2TokenGenerator
*/
@Bean
public OAuth2TokenGenerator oAuth2TokenGenerator() {
CustomeOAuth2AccessTokenGenerator accessTokenGenerator = new CustomeOAuth2AccessTokenGenerator();
// 注入Token 增加关联用户信息
accessTokenGenerator.setAccessTokenCustomizer(new CustomeOAuth2TokenCustomizer());
return new DelegatingOAuth2TokenGenerator(accessTokenGenerator, new OAuth2RefreshTokenGenerator());
}
// 单体版本:根据配置启用CORS跨域支持(仅在security.cors.enabled=true时生效)
if (Boolean.TRUE.equals(corsProperties.getEnabled())) {
http.cors(cors -> cors.configurationSource(request -> {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsProperties.getAllowedOriginPatterns().forEach(corsConfiguration::addAllowedOriginPattern);
corsProperties.getAllowedHeaders().forEach(corsConfiguration::addAllowedHeader);
corsProperties.getAllowedMethods().forEach(corsConfiguration::addAllowedMethod);
corsConfiguration.setAllowCredentials(corsProperties.getAllowCredentials());
source.registerCorsConfiguration(corsProperties.getPathPattern(), corsConfiguration);
return corsConfiguration;
}));
}
@Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return new FormAuthenticationFailureHandler();
}
DefaultSecurityFilterChain securityFilterChain = http.build();
@Bean
public LogoutSuccessHandler logoutSuccessHandler() {
return new SsoLogoutSuccessHandler();
}
// 注入自定义授权模式实现
addCustomOAuth2GrantAuthenticationProvider(http);
return securityFilterChain;
}
/**
* request -> xToken 注入请求转换器
*
* @return DelegatingAuthenticationConverter
*/
@Bean
public AuthenticationConverter accessTokenRequestConverter() {
return new DelegatingAuthenticationConverter(
Arrays.asList(new OAuth2ResourceOwnerPasswordAuthenticationConverter(),
new OAuth2ResourceOwnerSmsAuthenticationConverter(),
new PigxOAuth2RefreshTokenAuthenticationConverter(),
new OAuth2ClientCredentialsAuthenticationConverter(),
new OAuth2AuthorizationCodeAuthenticationConverter(),
new OAuth2AuthorizationCodeRequestAuthenticationConverter()));
}
/**
* 令牌生成规则实现 </br>
* client:username:uuid
*
* @return OAuth2TokenGenerator
*/
@Bean
public OAuth2TokenGenerator oAuth2TokenGenerator() {
CustomeOAuth2AccessTokenGenerator accessTokenGenerator = new CustomeOAuth2AccessTokenGenerator();
// 注入Token 增加关联用户信息
accessTokenGenerator.setAccessTokenCustomizer(new CustomeOAuth2TokenCustomizer());
return new DelegatingOAuth2TokenGenerator(accessTokenGenerator, new OAuth2RefreshTokenGenerator());
}
/**
* 注入授权模式实现提供方
* <p>
* 1. 密码模式 </br>
* 2. 短信登录 </br>
*/
@SuppressWarnings("unchecked")
private void addCustomOAuth2GrantAuthenticationProvider(HttpSecurity http) {
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
OAuth2AuthorizationService authorizationService = http.getSharedObject(OAuth2AuthorizationService.class);
@Bean
public AuthenticationFailureHandler authenticationFailureHandler() {
return new FormAuthenticationFailureHandler();
}
OAuth2ResourceOwnerPasswordAuthenticationProvider resourceOwnerPasswordAuthenticationProvider = new OAuth2ResourceOwnerPasswordAuthenticationProvider(
authenticationManager, authorizationService, oAuth2TokenGenerator());
@Bean
public LogoutSuccessHandler logoutSuccessHandler() {
return new SsoLogoutSuccessHandler();
}
OAuth2ResourceOwnerSmsAuthenticationProvider resourceOwnerSmsAuthenticationProvider = new OAuth2ResourceOwnerSmsAuthenticationProvider(
authenticationManager, authorizationService, oAuth2TokenGenerator());
/**
* request -> xToken 注入请求转换器
*
* @return DelegatingAuthenticationConverter
*/
@Bean
public AuthenticationConverter accessTokenRequestConverter() {
return new DelegatingAuthenticationConverter(
Arrays.asList(new OAuth2ResourceOwnerPasswordAuthenticationConverter(),
new OAuth2ResourceOwnerSmsAuthenticationConverter(),
new PigxOAuth2RefreshTokenAuthenticationConverter(),
new OAuth2ClientCredentialsAuthenticationConverter(),
new OAuth2AuthorizationCodeAuthenticationConverter(),
new OAuth2AuthorizationCodeRequestAuthenticationConverter()));
}
// 处理 UsernamePasswordAuthenticationToken
http.authenticationProvider(new PigxDaoAuthenticationProvider());
// 处理 OAuth2ResourceOwnerPasswordAuthenticationToken
http.authenticationProvider(resourceOwnerPasswordAuthenticationProvider);
// 处理 OAuth2ResourceOwnerSmsAuthenticationToken
http.authenticationProvider(resourceOwnerSmsAuthenticationProvider);
}
/**
* 注入授权模式实现提供方
* <p>
* 1. 密码模式 </br>
* 2. 短信登录 </br>
*/
@SuppressWarnings("unchecked")
private void addCustomOAuth2GrantAuthenticationProvider(HttpSecurity http) {
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
OAuth2AuthorizationService authorizationService = http.getSharedObject(OAuth2AuthorizationService.class);
OAuth2ResourceOwnerPasswordAuthenticationProvider resourceOwnerPasswordAuthenticationProvider = new OAuth2ResourceOwnerPasswordAuthenticationProvider(
authenticationManager, authorizationService, oAuth2TokenGenerator());
OAuth2ResourceOwnerSmsAuthenticationProvider resourceOwnerSmsAuthenticationProvider = new OAuth2ResourceOwnerSmsAuthenticationProvider(
authenticationManager, authorizationService, oAuth2TokenGenerator());
// 处理 UsernamePasswordAuthenticationToken
http.authenticationProvider(new PigxDaoAuthenticationProvider());
// 处理 OAuth2ResourceOwnerPasswordAuthenticationToken
http.authenticationProvider(resourceOwnerPasswordAuthenticationProvider);
// 处理 OAuth2ResourceOwnerSmsAuthenticationToken
http.authenticationProvider(resourceOwnerSmsAuthenticationProvider);
}
}
@@ -4,7 +4,6 @@ import cn.hutool.core.lang.Validator;
import com.anji.captcha.model.vo.CaptchaVO;
import com.anji.captcha.service.CaptchaService;
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.core.util.SpringContextHolder;
@@ -59,9 +58,9 @@ public class ImageCodeEndpoint {
*/
@SneakyThrows
@GetMapping("/create")
public R behavior() {
public R behavior(String captchaType) {
CaptchaVO vo = new CaptchaVO();
vo.setCaptchaType(CommonConstants.IMAGE_CODE_TYPE);
vo.setCaptchaType(captchaType);
CaptchaService captchaService = SpringContextHolder.getBean(CaptchaService.class);
return R.ok(captchaService.get(vo));
}
@@ -75,7 +74,7 @@ public class ImageCodeEndpoint {
CaptchaVO vo = new CaptchaVO();
vo.setPointJson(request.getParameter("pointJson"));
vo.setToken(request.getParameter("token"));
vo.setCaptchaType(CommonConstants.IMAGE_CODE_TYPE);
vo.setCaptchaType(request.getParameter("captchaType"));
CaptchaService captchaService = SpringContextHolder.getBean(CaptchaService.class);
return R.ok(captchaService.check(vo));
@@ -26,9 +26,11 @@ import com.pig4cloud.pigx.admin.api.entity.SysTenant;
import com.pig4cloud.pigx.admin.api.feign.RemoteClientDetailsService;
import com.pig4cloud.pigx.admin.api.feign.RemoteTenantService;
import com.pig4cloud.pigx.admin.api.vo.TokenVO;
import com.pig4cloud.pigx.auth.support.core.AuthErrorCodes;
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
import com.pig4cloud.pigx.common.core.util.MsgUtils;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.core.util.RetOps;
import com.pig4cloud.pigx.common.core.util.SpringContextHolder;
@@ -48,6 +50,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.authentication.event.LogoutSuccessEvent;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
@@ -91,6 +94,7 @@ public class PigxTokenEndpoint {
@GetMapping("/token/login")
public ModelAndView require(ModelAndView modelAndView, @RequestParam(required = false) String error) {
modelAndView.setViewName("ftl/login");
// Note: XSS prevention is handled by FreeMarker template using ?html directive
modelAndView.addObject("error", error);
R<List<SysTenant>> tenantList = tenantService.list();
@@ -110,9 +114,11 @@ public class PigxTokenEndpoint {
@RequestParam(OAuth2ParameterNames.STATE) String state) {
SysOauthClientDetails clientDetails = RetOps.of(clientDetailsService.getClientDetailsById(clientId))
.getData()
.orElseThrow(() -> new OAuthClientException("clientId 不合法"));
.orElseThrow(() -> new OAuthClientException(OAuth2ErrorCodes.INVALID_CLIENT,
MsgUtils.getMessage(AuthErrorCodes.AUTH_CLIENT_INVALID)));
Set<String> authorizedScopes = StringUtils.commaDelimitedListToSet(clientDetails.getScope());
// Note: XSS prevention is handled by FreeMarker template using ?html directive
modelAndView.addObject("clientId", clientId);
modelAndView.addObject("state", state);
modelAndView.addObject("scopeList", authorizedScopes);
@@ -0,0 +1,18 @@
package com.pig4cloud.pigx.auth.support.core;
/**
* Auth 模块错误编码
*
* @author lengleng
* @date 2022/3/30
*/
public interface AuthErrorCodes {
// === 验证码相关 ===
String AUTH_CAPTCHA_EMPTY = "auth.captcha.empty";
String AUTH_CAPTCHA_INVALID = "auth.captcha.invalid";
// === 客户端相关 ===
String AUTH_CLIENT_INVALID = "auth.client.invalid";
}
@@ -1,8 +1,8 @@
package com.pig4cloud.pigx.auth.support.core;
import com.pig4cloud.pigx.auth.support.handler.FormAuthenticationFailureHandler;
import com.pig4cloud.pigx.auth.support.handler.SsoLogoutSuccessHandler;
import com.pig4cloud.pigx.auth.support.handler.TenantSavedRequestAwareAuthenticationSuccessHandler;
import com.pig4cloud.pigx.common.security.handler.SsoLogoutSuccessHandler;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
@@ -12,11 +12,13 @@ import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.anji.captcha.model.vo.CaptchaVO;
import com.anji.captcha.service.CaptchaService;
import com.pig4cloud.pigx.auth.support.core.AuthErrorCodes;
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
import com.pig4cloud.pigx.common.core.constant.enums.CaptchaFlagTypeEnum;
import com.pig4cloud.pigx.common.core.exception.ValidateCodeException;
import com.pig4cloud.pigx.common.core.util.MsgUtils;
import com.pig4cloud.pigx.common.core.util.SpringContextHolder;
import com.pig4cloud.pigx.common.core.util.WebUtils;
import com.pig4cloud.pigx.common.data.cache.RedisUtils;
@@ -45,129 +47,130 @@ import java.io.IOException;
@RequiredArgsConstructor
public class ValidateCodeFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String requestUrl = request.getServletPath();
String requestUrl = request.getServletPath();
// 不是登录URL 请求直接跳过
if (!SecurityConstants.OAUTH_TOKEN_URL.equals(requestUrl)) {
filterChain.doFilter(request, response);
return;
}
// 不是登录URL 请求直接跳过
if (!SecurityConstants.OAUTH_TOKEN_URL.equals(requestUrl)) {
filterChain.doFilter(request, response);
return;
}
// 如果登录URL 但是刷新token的请求,直接向下执行
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (StrUtil.containsAny(grantType, SecurityConstants.REFRESH_TOKEN)) {
filterChain.doFilter(request, response);
return;
}
// 如果登录URL 但是刷新token的请求,直接向下执行
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (StrUtil.containsAny(grantType, SecurityConstants.REFRESH_TOKEN)) {
filterChain.doFilter(request, response);
return;
}
// mobile模式, 如果请求不包含mobile 参数直接
String mobile = request.getParameter(SecurityConstants.GRANT_MOBILE);
if (StrUtil.equals(SecurityConstants.GRANT_MOBILE, grantType) && StrUtil.isBlank(mobile)) {
throw new OAuth2AuthenticationException(SecurityConstants.GRANT_MOBILE);
}
// mobile模式, 如果请求不包含mobile 参数直接
String mobile = request.getParameter(SecurityConstants.GRANT_MOBILE);
if (StrUtil.equals(SecurityConstants.GRANT_MOBILE, grantType) && StrUtil.isBlank(mobile)) {
throw new OAuth2AuthenticationException(SecurityConstants.GRANT_MOBILE);
}
// mobile模式, 社交登录模式不校验验证码直接跳过
if (StrUtil.equals(SecurityConstants.GRANT_MOBILE, grantType) && !StrUtil.contains(mobile, "SMS")) {
filterChain.doFilter(request, response);
return;
}
// mobile模式, 社交登录模式不校验验证码直接跳过
if (StrUtil.equals(SecurityConstants.GRANT_MOBILE, grantType) && !StrUtil.contains(mobile, "SMS")) {
filterChain.doFilter(request, response);
return;
}
// 判断客户端是否跳过检验
if (!isCheckCaptchaClient(request)) {
filterChain.doFilter(request, response);
return;
}
// 判断客户端是否跳过检验
if (!isCheckCaptchaClient(request)) {
filterChain.doFilter(request, response);
return;
}
// 校验验证码 1. 客户端开启验证码 2. 短信模式
try {
checkCode();
filterChain.doFilter(request, response);
// 校验验证码 1. 客户端开启验证码 2. 短信模式
try {
checkCode();
filterChain.doFilter(request, response);
} catch (ValidateCodeException validateCodeException) {
throw new OAuth2AuthenticationException(validateCodeException.getMessage());
}
}
throw new OAuth2AuthenticationException(validateCodeException.getMessage());
}
}
/**
* 校验验证码
*/
private void checkCode() throws ValidateCodeException {
HttpServletRequest request = WebUtils.getRequest();
String code = request.getParameter("code");
/**
* 校验验证码
*/
private void checkCode() throws ValidateCodeException {
HttpServletRequest request = WebUtils.getRequest();
String code = request.getParameter("code");
if (StrUtil.isBlank(code)) {
throw new ValidateCodeException("验证码不能为空");
}
if (StrUtil.isBlank(code)) {
throw new ValidateCodeException(MsgUtils.getMessage(AuthErrorCodes.AUTH_CAPTCHA_EMPTY));
}
String randomStr = request.getParameter("randomStr");
String randomStr = request.getParameter("randomStr");
// https://gitee.com/log4j/pig/issues/IWA0D
String mobile = request.getParameter("mobile");
if (StrUtil.isNotBlank(mobile)) {
randomStr = mobile;
}
// https://gitee.com/log4j/pig/issues/IWA0D
String mobile = request.getParameter("mobile");
if (StrUtil.isNotBlank(mobile)) {
randomStr = mobile;
}
// 若是滑块登录
if (CommonConstants.IMAGE_CODE_TYPE.equalsIgnoreCase(randomStr)) {
CaptchaService captchaService = SpringContextHolder.getBean(CaptchaService.class);
CaptchaVO vo = new CaptchaVO();
vo.setCaptchaVerification(code);
vo.setCaptchaType(CommonConstants.IMAGE_CODE_TYPE);
if (!captchaService.verification(vo).isSuccess()) {
throw new ValidateCodeException("验证码不能为空");
}
return;
}
// 若是滑块登录
if (StrUtil.equalsAnyIgnoreCase(randomStr, CommonConstants.IMAGE_CODE_BLOCK_PUZZLE, CommonConstants.IMAGE_CODE_CLICK_WORD)) {
CaptchaService captchaService = SpringContextHolder.getBean(CaptchaService.class);
CaptchaVO vo = new CaptchaVO();
vo.setCaptchaVerification(code);
vo.setCaptchaType(randomStr);
if (!captchaService.verification(vo).isSuccess()) {
throw new ValidateCodeException(MsgUtils.getMessage(AuthErrorCodes.AUTH_CAPTCHA_EMPTY));
}
return;
}
String key = CacheConstants.DEFAULT_CODE_KEY + randomStr;
if (!RedisUtils.hasKey(key)) {
throw new ValidateCodeException("验证码不合法");
}
String key = CacheConstants.DEFAULT_CODE_KEY + randomStr;
if (!RedisUtils.hasKey(key)) {
throw new ValidateCodeException(MsgUtils.getMessage(AuthErrorCodes.AUTH_CAPTCHA_INVALID));
}
String codeObj = RedisUtils.get(key);
String codeObj = RedisUtils.get(key);
if (codeObj == null) {
throw new ValidateCodeException("验证码不合法");
}
if (codeObj == null) {
throw new ValidateCodeException(MsgUtils.getMessage(AuthErrorCodes.AUTH_CAPTCHA_INVALID));
}
if (StrUtil.isBlank(codeObj)) {
RedisUtils.delete(key);
throw new ValidateCodeException("验证码不合法");
}
if (StrUtil.isBlank(codeObj)) {
RedisUtils.delete(key);
throw new ValidateCodeException(MsgUtils.getMessage(AuthErrorCodes.AUTH_CAPTCHA_INVALID));
}
if (!StrUtil.equals(codeObj, code)) {
RedisUtils.delete(key);
throw new ValidateCodeException("验证码不合法");
}
if (!StrUtil.equals(codeObj, code)) {
RedisUtils.delete(key);
throw new ValidateCodeException(MsgUtils.getMessage(AuthErrorCodes.AUTH_CAPTCHA_INVALID));
}
RedisUtils.delete(key);
}
RedisUtils.delete(key);
}
/**
* 是否需要校验客户端,根据client 查询客户端配置
* @param request 请求
* @return true 需要校验, false 不需要校验
*/
private boolean isCheckCaptchaClient(HttpServletRequest request) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
String clientId = WebUtils.extractClientId(header).orElse(null);
// 获取租户拼接区分租户的key
String tenantId = request.getHeader(CommonConstants.TENANT_ID);
String key = String.format("%s:%s:%s", StrUtil.isBlank(tenantId) ? CommonConstants.TENANT_ID_1 : tenantId,
CacheConstants.CLIENT_FLAG, clientId);
/**
* 是否需要校验客户端,根据client 查询客户端配置
*
* @param request 请求
* @return true 需要校验, false 不需要校验
*/
private boolean isCheckCaptchaClient(HttpServletRequest request) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
String clientId = WebUtils.extractClientId(header).orElse(null);
// 获取租户拼接区分租户的key
String tenantId = request.getHeader(CommonConstants.TENANT_ID);
String key = String.format("%s:%s:%s", StrUtil.isBlank(tenantId) ? CommonConstants.TENANT_ID_1 : tenantId,
CacheConstants.CLIENT_FLAG, clientId);
String val = RedisUtils.get(key);
String val = RedisUtils.get(key);
// 当配置不存在时,不用校验
if (val == null) {
return false;
}
// 当配置不存在时,不用校验
if (val == null) {
return false;
}
JSONObject information = JSONUtil.parseObj(val);
return !StrUtil.equals(CaptchaFlagTypeEnum.OFF.getType(), information.getStr(CommonConstants.CAPTCHA_FLAG));
}
JSONObject information = JSONUtil.parseObj(val);
return !StrUtil.equals(CaptchaFlagTypeEnum.OFF.getType(), information.getStr(CommonConstants.CAPTCHA_FLAG));
}
}
@@ -1,41 +0,0 @@
package com.pig4cloud.pigx.auth.support.handler;
import cn.hutool.core.util.StrUtil;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @author lengleng
* @date 2022-06-02
* <p>
* sso 退出功能 ,根据客户端传入跳转
*/
public class SsoLogoutSuccessHandler implements LogoutSuccessHandler {
private static final String REDIRECT_URL = "redirect_url";
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
if (response == null) {
return;
}
// 获取请求参数中是否包含 回调地址
String redirectUrl = request.getParameter(REDIRECT_URL);
if (StrUtil.isNotBlank(redirectUrl)) {
response.sendRedirect(redirectUrl);
}
else if (StrUtil.isNotBlank(request.getHeader(HttpHeaders.REFERER))) {
// 默认跳转referer 地址
String referer = request.getHeader(HttpHeaders.REFERER);
response.sendRedirect(referer);
}
}
}
@@ -0,0 +1,5 @@
# Auth Module i18n Messages - English
# Captcha related
auth.captcha.empty=Captcha cannot be empty
auth.captcha.invalid=Invalid captcha
auth.client.invalid=Invalid client
@@ -0,0 +1,5 @@
# Auth \u6A21\u5757\u56FD\u9645\u5316\u6D88\u606F - \u4E2D\u6587
# \u9A8C\u8BC1\u7801\u76F8\u5173
auth.captcha.empty=\u9A8C\u8BC1\u7801\u4E0D\u80FD\u4E3A\u7A7A
auth.captcha.invalid=\u9A8C\u8BC1\u7801\u4E0D\u5408\u6CD5
auth.client.invalid=\u65E0\u6548\u7684\u5BA2\u6237\u7AEF
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2018-2025, lengleng All rights reserved.
~ Copyright (c) 2018-2026, lengleng All rights reserved.
~
~ Redistribution and use in source and binary forms, with or without
~ modification, are permitted provided that the following conditions are met:
@@ -10,7 +10,7 @@
<#else>
<a href="https://pig4cloud.com"
class="text-purple-600 dark:text-purple-400 hover:text-purple-700 dark:hover:text-purple-300 font-medium transition-colors">
${principalName}
${principalName?html}
</a>
</#if>
</div>
@@ -20,8 +20,8 @@
<form id='confirmationForm' name='confirmationForm'
action="<#if request??>${request.contextPath!}</#if>/oauth2/authorize" method='post'>
<input type="hidden" name="client_id" value="${clientId}">
<input type="hidden" name="state" value="${state}">
<input type="hidden" name="client_id" value="${clientId?html}">
<input type="hidden" name="state" value="${state?html}">
<div class="space-y-6">
<div class="mb-4">
@@ -29,9 +29,9 @@
<div class="space-y-3 bg-gray-50 dark:bg-gray-800 p-4 rounded-lg border border-gray-200 dark:border-gray-700 transition-all">
<#list scopeList as scope>
<div class="flex items-center">
<input type="checkbox" checked="checked" name="scope" value="${scope}"
<input type="checkbox" checked="checked" name="scope" value="${scope?html}"
class="h-4 w-4 text-purple-600 focus:ring-purple-500 dark:focus:ring-purple-400 border-gray-300 dark:border-gray-600 rounded transition-colors">
<label class="ml-3 text-gray-600 dark:text-gray-400 transition-colors">${scope}</label>
<label class="ml-3 text-gray-600 dark:text-gray-400 transition-colors">${scope?html}</label>
</div>
</#list>
</div>
@@ -22,7 +22,7 @@
<select class="w-full text-sm px-4 py-3 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:border-purple-400 dark:focus:border-purple-500 dark:text-gray-300 transition-colors"
name="TENANT-ID">
<#list tenantList as tenant>
<option value="${tenant.id}">${tenant.name}</option>
<option value="${tenant.id?c}">${tenant.name?html}</option>
</#list>
</select>
</#if>
@@ -40,7 +40,7 @@
<#if error??>
<div class="relative text-center">
<span class="text-red-600 dark:text-red-400">${error}</span>
<span class="text-red-600 dark:text-red-400">${error?html}</span>
</div>
</#if>
+1 -1
View File
@@ -21,7 +21,7 @@
<parent>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx</artifactId>
<version>5.10.0</version>
<version>5.11.0</version>
</parent>
<artifactId>pigx-boot</artifactId>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2018-2025, lengleng All rights reserved.
~ Copyright (c) 2018-2026, lengleng All rights reserved.
~
~ Redistribution and use in source and binary forms, with or without
~ modification, are permitted provided that the following conditions are met:
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-common</artifactId>
<version>5.10.0</version>
<version>5.11.0</version>
</parent>
<artifactId>pigx-common-audit</artifactId>
+34 -10
View File
@@ -11,14 +11,14 @@
<description>pigx 公共版本控制,在此pom定义的依赖,全局业务微服务不需要指定版本。</description>
<properties>
<pigx.version>5.10.0</pigx.version>
<mybatis-plus.version>3.5.14</mybatis-plus.version>
<mybatis-plus-join.version>1.5.4</mybatis-plus-join.version>
<dynamic-ds.version>4.3.1</dynamic-ds.version>
<pigx.version>5.11.0</pigx.version>
<mybatis-plus.version>3.5.15</mybatis-plus.version>
<mybatis-plus-join.version>1.5.5</mybatis-plus-join.version>
<dynamic-ds.version>4.5.0</dynamic-ds.version>
<nacos.client.version>3.1.0</nacos.client.version>
<druid.version>1.2.23</druid.version>
<hutool.version>5.8.41</hutool.version>
<mysql.connector.version>8.3.0</mysql.connector.version>
<hutool.version>5.8.42</hutool.version>
<mysql.connector.version>9.5.0</mysql.connector.version>
<oracle.version>21.3.0.0</oracle.version>
<sqlserver.version>12.10.0.jre8</sqlserver.version>
<dm.version>8.1.3.140</dm.version>
@@ -26,8 +26,10 @@
<knife4j.version>3.0.5</knife4j.version>
<springdoc.version>2.8.5</springdoc.version>
<swagger.core.version>2.2.28</swagger.core.version>
<mp.weixin.version>4.6.7.B</mp.weixin.version>
<ijpay.version>2.9.10-17</ijpay.version>
<!-- FIX Quartz https://github.com/spring-projects/spring-boot/issues/48012 -->
<quartz.version>2.5.2</quartz.version>
<mp.weixin.version>4.7.8-20251028.120007</mp.weixin.version>
<ijpay.version>2.9.12.1-17</ijpay.version>
<groovy.version>3.0.3</groovy.version>
<javax.version>4.0.1</javax.version>
<jsoup.version>1.15.3</jsoup.version>
@@ -42,7 +44,7 @@
<seata.version>1.7.0</seata.version>
<asm.version>7.1</asm.version>
<sensitive.word.version>0.21.0</sensitive.word.version>
<log4j2.version>2.17.1</log4j2.version>
<log4j2.version>2.25.2</log4j2.version>
<grpc.version>1.59.1</grpc.version>
<docker.plugin.version>0.33.0</docker.plugin.version>
<cloud.plugin.version>1.0.0</cloud.plugin.version>
@@ -52,7 +54,7 @@
<deepseek.version>1.4.7</deepseek.version>
<jakarta.mail.version>2.0.1</jakarta.mail.version>
<dashscope.version>2.20.1</dashscope.version>
<velocity.version>2.3</velocity.version>
<velocity.version>2.4.1</velocity.version>
<common.io.version>2.16.1</common.io.version>
<velocity.tool.version>3.1</velocity.tool.version>
</properties>
@@ -186,6 +188,22 @@
<artifactId>pigx-flow-api</artifactId>
<version>${pigx.version}</version>
</dependency>
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-common-milvus</artifactId>
<version>${pigx.version}</version>
</dependency>
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-common-mcp</artifactId>
<version>${pigx.version}</version>
</dependency>
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-common-neo4j</artifactId>
<version>${pigx.version}</version>
</dependency>
<!-- chat ai -->
<dependency>
<groupId>io.github.pig-mesh.ai</groupId>
@@ -463,6 +481,12 @@
<artifactId>nacos-client</artifactId>
<version>${nacos.client.version}</version>
</dependency>
<!-- fix springboot 3.5.8 bug,后续版本注意移除-->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>${quartz.version}</version>
</dependency>
<!-- 指定 log4j 版本-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
+2 -2
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~
~ Copyright (c) 2018-2025, lengleng All rights reserved.
~ Copyright (c) 2018-2026, lengleng All rights reserved.
~
~ Redistribution and use in source and binary forms, with or without
~ modification, are permitted provided that the following conditions are met:
@@ -24,7 +24,7 @@
<parent>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-common</artifactId>
<version>5.10.0</version>
<version>5.11.0</version>
</parent>
<artifactId>pigx-common-core</artifactId>
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -17,25 +17,149 @@
package com.pig4cloud.pigx.common.core.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.ResourceUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
/**
* @author lengleng
* @date 2018/11/14
* @author Hccake
* @date 2025-12-12
* <p>
* 国际化配置
* 国际化配置 - 支持多 JAR 包扫描
*/
@Slf4j
@Configuration
public class MessageSourceConfiguration {
@Bean
public MessageSource pigxMessageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:i18n/messages");
return messageSource;
}
/**
* 创建并配置国际化消息源Bean
*
* @return 配置好的消息源实例,支持通配符资源加载
*/
@Bean
public MessageSource pigxMessageSource() {
WildcardReloadableResourceBundleMessageSource messageSource = new WildcardReloadableResourceBundleMessageSource();
// 设置基础包名,支持通配符扫描多个 JAR
messageSource.setBasename("i18n/messages");
messageSource.setDefaultEncoding("UTF-8");
messageSource.setCacheSeconds(3600);
return messageSource;
}
/**
* 支持 classpath*: 通配符的 MessageSource 实现 能够扫描所有 JAR 包中的 i18n 资源文件
*/
@Slf4j
static class WildcardReloadableResourceBundleMessageSource extends ReloadableResourceBundleMessageSource {
private static final String PROPERTIES_SUFFIX = ".properties";
private final PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
private static final Pattern LOCALE_PROPERTIES_FILE_NAME_PATTERN = Pattern.compile(".*_([a-z]{2}(_[A-Z]{2})?(_[A-Z]+)?)\\.properties$");
public WildcardReloadableResourceBundleMessageSource() {
super.setResourceLoader(this.resolver);
}
/**
* 计算给定bundle基名和Locale的所有文件名。会计算指定Locale、系统Locale(如果适用)和默认文件的文件名。
* 当基名包含通配符时,会移除基名并添加通配符匹配到的资源文件路径。
*
* @param basename bundle的基名
* @param locale 区域设置
* @return 要检查的文件名列表
* @see #setFallbackToSystemLocale
* @see #calculateFilenamesForLocale
*/
@Override
protected List<String> calculateAllFilenames(String basename, Locale locale) {
// 父类默认的方法会将 basename 也放入 filenames 列表
List<String> filenames = super.calculateAllFilenames(basename, locale);
// 当 basename 有匹配符时,从 filenames 中移除,否则扫描文件将抛出 Illegal char <*> 的异常
if (containsWildcard(basename)) {
filenames.remove(basename);
try {
Resource[] resources = getResources(basename);
for (Resource resource : resources) {
String resourceUriStr = resource.getURI().toString();
// 根据通配符匹配到的多个 basename 对应的文件添加到文件列表末尾,作为兜底匹配
if (!LOCALE_PROPERTIES_FILE_NAME_PATTERN.matcher(resourceUriStr).matches()) {
String sourcePath = resourceUriStr.replace(PROPERTIES_SUFFIX, "");
filenames.add(sourcePath);
}
}
} catch (IOException ex) {
log.error("读取国际化信息文件异常", ex);
}
}
return filenames;
}
/**
* 计算指定基名和区域设置的资源文件名列表
*
* @param basename 资源文件基名
* @param locale 区域设置
* @return 存在的资源文件名列表(不包含.properties后缀)
*/
@Override
protected List<String> calculateFilenamesForLocale(String basename, Locale locale) {
// 资源文件名
List<String> fileNames = new ArrayList<>();
// 获取到待匹配的国际化信息文件名集合
List<String> matchFilenames = super.calculateFilenamesForLocale(basename, locale);
for (String matchFilename : matchFilenames) {
try {
Resource[] resources = getResources(matchFilename);
for (Resource resource : resources) {
if (resource.exists()) {
String sourcePath = resource.getURI().toString().replace(PROPERTIES_SUFFIX, "");
fileNames.add(sourcePath);
}
}
} catch (IOException ex) {
log.error("读取国际化信息文件异常", ex);
}
}
return fileNames;
}
/**
* 根据资源路径模式获取资源数组
*
* @param resourceLocationPattern 资源路径模式(支持用"."表示文件层级)
* @return 匹配的资源数组
* @throws IOException 获取资源过程中发生IO异常时抛出
*/
private Resource[] getResources(String resourceLocationPattern) throws IOException {
// 支持用 . 表示文件层级
resourceLocationPattern = resourceLocationPattern.replace(".", "/");
return this.resolver.getResources("classpath*:" + resourceLocationPattern + PROPERTIES_SUFFIX);
}
/**
* 检查字符串是否包含通配符
*
* @param str 要检查的字符串
* @return 如果字符串包含通配符返回true,否则返回false
*/
private boolean containsWildcard(String str) {
if (str.startsWith(ResourceUtils.CLASSPATH_URL_PREFIX)) {
str = str.substring(ResourceUtils.CLASSPATH_URL_PREFIX.length());
}
return str.contains("*");
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -25,114 +25,119 @@ package com.pig4cloud.pigx.common.core.constant;
*/
public interface CommonConstants {
/**
* header 中租户ID
*/
String TENANT_ID = "TENANT-ID";
/**
* header 中租户ID
*/
String TENANT_ID = "TENANT-ID";
/**
* header 中版本信息
*/
String VERSION = "VERSION";
/**
* header 中版本信息
*/
String VERSION = "VERSION";
/**
* 租户ID
*/
Long TENANT_ID_1 = 1L;
/**
* 租户ID
*/
Long TENANT_ID_1 = 1L;
/**
* 删除
*/
String STATUS_DEL = "1";
/**
* 删除
*/
String STATUS_DEL = "1";
/**
* 正常
*/
String STATUS_NORMAL = "0";
/**
* 正常
*/
String STATUS_NORMAL = "0";
/**
* 锁定
*/
String STATUS_LOCK = "9";
/**
* 锁定
*/
String STATUS_LOCK = "9";
/**
* 菜单树根节点
*/
Long MENU_TREE_ROOT_ID = -1L;
/**
* 菜单树根节点
*/
Long MENU_TREE_ROOT_ID = -1L;
/**
* 编码
*/
String UTF8 = "UTF-8";
/**
* 编码
*/
String UTF8 = "UTF-8";
/**
* 前端工程名
*/
String FRONT_END_PROJECT = "pigx-ui";
/**
* 前端工程名
*/
String FRONT_END_PROJECT = "pigx-ui";
/**
* 移动端工程名
*/
String UNI_END_PROJECT = "pigx-app";
/**
* 移动端工程名
*/
String UNI_END_PROJECT = "pigx-app";
/**
* 后端工程名
*/
String BACK_END_PROJECT = "pigx";
/**
* 后端工程名
*/
String BACK_END_PROJECT = "pigx";
/**
* 公共参数
*/
String PIG_PUBLIC_PARAM_KEY = "PIG_PUBLIC_PARAM_KEY";
/**
* 公共参数
*/
String PIG_PUBLIC_PARAM_KEY = "PIG_PUBLIC_PARAM_KEY";
/**
* 成功标记
*/
Integer SUCCESS = 0;
/**
* 成功标记
*/
Integer SUCCESS = 0;
/**
* 失败标记
*/
Integer FAIL = 1;
/**
* 失败标记
*/
Integer FAIL = 1;
/**
* 默认存储bucket
*/
String BUCKET_NAME = "lengleng";
/**
* 默认存储bucket
*/
String BUCKET_NAME = "lengleng";
/**
* 滑块验证码
*/
String IMAGE_CODE_TYPE = "blockPuzzle";
/**
* 滑块验证码
*/
String IMAGE_CODE_BLOCK_PUZZLE = "blockPuzzle";
/**
* 验证码开关
*/
String CAPTCHA_FLAG = "captcha_flag";
/**
* 点击文字验证码类型
*/
String IMAGE_CODE_CLICK_WORD = "clickWord";
/**
* 密码传输是否加密
*/
String ENC_FLAG = "enc_flag";
/**
* 验证码开关
*/
String CAPTCHA_FLAG = "captcha_flag";
/**
* 客户端允许同时在线数量
*/
String ONLINE_QUANTITY = "online_quantity";
/**
* 密码传输是否加密
*/
String ENC_FLAG = "enc_flag";
/**
* 请求开始时间
*/
String REQUEST_START_TIME = "REQUEST-START-TIME";
/**
* 客户端允许同时在线数量
*/
String ONLINE_QUANTITY = "online_quantity";
/**
* 当前页
*/
String CURRENT = "current";
/**
* 请求开始时间
*/
String REQUEST_START_TIME = "REQUEST-START-TIME";
/**
* size
*/
String SIZE = "size";
/**
* 当前页
*/
String CURRENT = "current";
/**
* size
*/
String SIZE = "size";
}
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,113 +0,0 @@
package com.pig4cloud.pigx.common.core.exception;
/**
* 错误编码
*
* @author lengleng
* @date 2022/3/30
*/
public interface ErrorCodes {
/**
* 系统编码错误
*/
String SYS_PARAM_CONFIG_ERROR = "sys.param.config.error";
/**
* 系统内置参数不能删除
*/
String SYS_PARAM_DELETE_SYSTEM = "sys.param.delete.system";
/**
* 用户已存在
*/
String SYS_USER_USERNAME_EXISTING = "sys.user.username.existing";
/**
* 用户已存在
*/
String SYS_USER_PHONE_EXISTING = "sys.user.phone.existing";
/**
* 用户原密码错误,修改失败
*/
String SYS_USER_UPDATE_PASSWORDERROR = "sys.user.update.passwordError";
/**
* 用户信息为空
*/
String SYS_USER_USERINFO_EMPTY = "sys.user.userInfo.empty";
/**
* 获取当前用户信息失败
*/
String SYS_USER_QUERY_ERROR = "sys.user.query.error";
String SYS_USER_IMPORT_SUCCEED = "sys.user.import.succeed";
/**
* 部门名称不存在
*/
String SYS_DEPT_DEPTNAME_INEXISTENCE = "sys.dept.deptName.inexistence";
/**
* 岗位名称不存在
*/
String SYS_POST_POSTNAME_INEXISTENCE = "sys.post.postName.inexistence";
/**
* 岗位名称或编码已经存在
*/
String SYS_POST_NAMEORCODE_EXISTING = "sys.post.nameOrCode.existing";
/**
* 角色名称不存在
*/
String SYS_ROLE_ROLENAME_INEXISTENCE = "sys.role.roleName.inexistence";
/**
* 角色名或角色编码已经存在
*/
String SYS_ROLE_NAMEORCODE_EXISTING = "sys.role.nameOrCode.existing";
/**
* 菜单存在下级节点 删除失败
*/
String SYS_MENU_DELETE_EXISTING = "sys.menu.delete.existing";
/**
* 系统内置字典不允许删除
*/
String SYS_DICT_DELETE_SYSTEM = "sys.dict.delete.system";
/**
* 系统内置字典不能修改
*/
String SYS_DICT_UPDATE_SYSTEM = "sys.dict.update.system";
/**
* 验证码发送频繁
*/
String SYS_APP_SMS_OFTEN = "sys.app.sms.often";
/**
* 手机号未注册
*/
String SYS_APP_PHONE_UNREGISTERED = "sys.app.phone.unregistered";
/**
* 企微调用接口错误
*/
String SYS_CONNECT_CP_DEPT_SYNC_ERROR = "sys.connect.cp.dept.sync.error";
/**
* 企微调用接口错误
*/
String SYS_CONNECT_CP_USER_SYNC_ERROR = "sys.connect.cp.user.sync.error";
/**
* 用户信息为空
*/
String APP_USER_USERINFO_EMPTY = "app.user.userInfo.empty";
}
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -2,6 +2,8 @@ package com.pig4cloud.pigx.common.core.util;
import lombok.experimental.UtilityClass;
import org.springframework.context.MessageSource;
import org.springframework.context.NoSuchMessageException;
import org.springframework.context.i18n.LocaleContextHolder;
import java.util.Locale;
@@ -14,34 +16,55 @@ import java.util.Locale;
@UtilityClass
public class MsgUtils {
/**
* 通过code 获取中文错误信息
* @param code
* @return
*/
public String getMessage(String code) {
MessageSource messageSource = SpringContextHolder.getBean("pigxMessageSource");
return messageSource.getMessage(code, null, Locale.CHINA);
}
/**
* 通过code 获取中文错误信息
*
* @param code
* @return
*/
public String getMessage(String code) {
MessageSource messageSource = SpringContextHolder.getBean("pigxMessageSource");
return resolveMessage(messageSource, code, null);
}
/**
* 通过code 和参数获取中文错误信息
* @param code
* @return
*/
public String getMessage(String code, Object... objects) {
MessageSource messageSource = SpringContextHolder.getBean("pigxMessageSource");
return messageSource.getMessage(code, objects, Locale.CHINA);
}
/**
* 通过code 和参数获取中文错误信息
*
* @param code
* @return
*/
public String getMessage(String code, Object... objects) {
MessageSource messageSource = SpringContextHolder.getBean("pigxMessageSource");
return resolveMessage(messageSource, code, objects);
}
/**
* security 通过code 和参数获取中文错误信息
* @param code
* @return
*/
public String getSecurityMessage(String code, Object... objects) {
MessageSource messageSource = SpringContextHolder.getBean("securityMessageSource");
return messageSource.getMessage(code, objects, Locale.CHINA);
}
/**
* 通过错误码和参数获取安全相关的中文错误信息
*
* @param code 错误码
* @param objects 格式化参数
* @return 格式化后的错误信息
*/
public String getSecurityMessage(String code, Object... objects) {
MessageSource messageSource = SpringContextHolder.getBean("securityMessageSource");
return resolveMessage(messageSource, code, objects);
}
/**
* 解析消息,根据当前区域设置获取消息,如果找不到消息则使用中国区域设置重试
*
* @param messageSource 消息源
* @param code 消息代码
* @param objects 消息参数
* @return 解析后的消息
*/
private String resolveMessage(MessageSource messageSource, String code, Object[] objects) {
Locale locale = LocaleContextHolder.getLocale();
try {
return messageSource.getMessage(code, objects, locale);
} catch (NoSuchMessageException ex) {
return messageSource.getMessage(code, objects, Locale.CHINA);
}
}
}
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
+2 -2
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~
~ Copyright (c) 2018-2025, lengleng All rights reserved.
~ Copyright (c) 2018-2026, lengleng All rights reserved.
~
~ Redistribution and use in source and binary forms, with or without
~ modification, are permitted provided that the following conditions are met:
@@ -24,7 +24,7 @@
<parent>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-common</artifactId>
<version>5.10.0</version>
<version>5.11.0</version>
</parent>
<artifactId>pigx-common-data</artifactId>
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -71,6 +71,16 @@ public class DataScope extends HashMap {
*/
private DataScopeFuncEnum func = DataScopeFuncEnum.ALL;
/**
* 逻辑组合模式,默认OR(向后兼容)
* 当同时有本人权限(username)和部门权限(deptList)时的逻辑组合方式
* <ul>
* <li>OR: 满足任一条件即可(默认)</li>
* <li>AND: 需同时满足所有条件</li>
* </ul>
*/
private DataScopeLogicEnum logicMode = DataScopeLogicEnum.OR;
/**
* of 获取实例
*/
@@ -93,4 +103,14 @@ public class DataScope extends HashMap {
return this;
}
/**
* 设置逻辑组合模式
* @param logicMode ��辑模式(AND/OR)
* @return DataScope
*/
public DataScope logic(DataScopeLogicEnum logicMode) {
this.logicMode = logicMode;
return this;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -63,8 +63,13 @@ public class DataScopeInnerInterceptor implements DataScopeInterceptor {
// 2.如果为本人权限 + 部门权限控制
else if (StrUtil.isNotBlank(dataScope.getUsername()) && CollUtil.isNotEmpty(deptIds)) {
String join = CollectionUtil.join(deptIds, ",");
originalSql = String.format("SELECT %s FROM (%s) temp_data_scope WHERE temp_data_scope.%s = '%s' OR temp_data_scope.%s IN (%s)",
dataScope.getFunc().getType(), originalSql, dataScope.getScopeUserName(), dataScope.getUsername(), dataScope.getScopeDeptName(), join);
// 使用配置的组合模式(AND/OR),默认OR保持向后兼容
String logicKeyword = dataScope.getLogicMode() != null ? dataScope.getLogicMode().getKeyword()
: DataScopeLogicEnum.OR.getKeyword();
originalSql = String.format(
"SELECT %s FROM (%s) temp_data_scope WHERE temp_data_scope.%s = '%s' %s temp_data_scope.%s IN (%s)",
dataScope.getFunc().getType(), originalSql, dataScope.getScopeUserName(), dataScope.getUsername(),
logicKeyword, dataScope.getScopeDeptName(), join);
}
// 3. 如果为本人
else if (StrUtil.isNotBlank(dataScope.getUsername())) {
@@ -0,0 +1,53 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.common.data.datascope;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 数据权限逻辑组合类型
*
* @author lengleng
* @date 2025-12-10
*/
@Getter
@AllArgsConstructor
public enum DataScopeLogicEnum {
/**
* OR逻辑 - 满足任一条件即可(默认) 示例: WHERE create_by='user' OR dept_id IN (1,2,3)
*/
OR("OR", "或"),
/**
* AND逻辑 - 需同时满足所有条件 示例: WHERE create_by='user' AND dept_id IN (1,2,3)
*/
AND("AND", "且");
/**
* SQL关键字
*/
private final String keyword;
/**
* 描述
*/
private final String description;
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -22,11 +22,13 @@ import cn.hutool.core.util.StrUtil;
import com.pig4cloud.pigx.admin.api.entity.SysDept;
import com.pig4cloud.pigx.admin.api.entity.SysRole;
import com.pig4cloud.pigx.admin.api.feign.RemoteDataScopeService;
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
import com.pig4cloud.pigx.common.core.constant.enums.UserTypeEnum;
import com.pig4cloud.pigx.common.core.util.RetOps;
import com.pig4cloud.pigx.common.security.service.PigxUser;
import com.pig4cloud.pigx.common.security.util.SecurityUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import java.util.Arrays;
import java.util.Collections;
@@ -65,7 +67,12 @@ public class PigxDefaultDataScopeHandle implements DataScopeHandle {
}
// 获取用户角色ID列表
List<Long> roleIdList = user.getRoleIds();
List<String> roleIdList = user.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.filter(authority -> authority.startsWith(SecurityConstants.ROLE))
.map(authority -> authority.split(StrUtil.UNDERLINE)[1])
.toList();
if (CollUtil.isEmpty(roleIdList)) {
return false;
}
@@ -128,24 +135,20 @@ public class PigxDefaultDataScopeHandle implements DataScopeHandle {
* 处理本级及下级数据权限
*/
private void handleOwnChildLevelScope(PigxUser user, List<Long> deptList) {
for (Long deptId : user.getDeptIds()) {
List<Long> descendantDeptIds = RetOps.of(dataScopeService.getDescendantList(deptId))
.getData()
.orElseGet(Collections::emptyList)
.stream()
.map(SysDept::getDeptId)
.toList();
deptList.addAll(descendantDeptIds);
}
List<Long> descendantDeptIds = RetOps.of(dataScopeService.getDescendantList(user.getDeptId()))
.getData()
.orElseGet(Collections::emptyList)
.stream()
.map(SysDept::getDeptId)
.toList();
deptList.addAll(descendantDeptIds);
}
/**
* 处理本级数据权限
*/
private void handleOwnLevelScope(PigxUser user, List<Long> deptList) {
deptList.addAll(user.getDeptIds());
deptList.add(user.getDeptId());
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
* Copyright (c) 2018-2026, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -48,13 +48,15 @@ import java.util.List;
import java.util.Objects;
/**
* 租户上下文过滤器:处理租户ID的提取、验证和上下文设置
*
* @author lengleng
* @date 2018/9/13
* @date 2025/12/30
*/
@Slf4j
@Order(-104) // OrderedRequestContextFilter 之后执行
@Component
@RequiredArgsConstructor
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TenantContextHolderFilter extends GenericFilterBean {
private final static String UNDEFINED_STR = "undefined";
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<artifactId>pigx-common</artifactId>
<groupId>com.pig4cloud</groupId>
<version>5.10.0</version>
<version>5.11.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
Loaded 100 of 525 files, more files were not shown because too many files have changed in this diff. Show more