解决API文档和路由配置合并冲突

This commit is contained in:
李旭明 committed 2025-08-04 11:19:52 +08:00
commit 79a96a7dfd
662 files changed
+127608 -205195

No files matched your search

+1
View File
@@ -161,3 +161,4 @@ coverage/
*.crt
*.p12
*.keystore/xgxt/
excel/
+57 -202870
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -79,7 +79,7 @@ public class AuthController {
* @param body 登录信息
* @return 结果
*/
@ApiEncrypt
@ApiEncrypt(response = true)
@PostMapping("/login")
public R<LoginVo> login(@RequestBody String body) {
LoginBody loginBody = JsonUtils.parseObject(body, LoginBody.class);
@@ -293,3 +293,44 @@ justauth:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=gitea
system:
file-cache:
# 是否启用文件缓存
enabled: true
# 最大缓存文件数量
max-cache-size: 100
# 最大缓存大小(字节)- 500MB
max-cache-size-bytes: 524288000
# 缓存过期时间(毫秒)- 30分钟
expire-time-ms: 1800000
# 最大缓存单个文件大小(字节)- 50MB
max-single-file-size: 52428800
# LRU淘汰比例(百分比)
evict-ratio: 20
# 是否启用缓存统计
enable-stats: true
# 缓存清理间隔(毫秒)- 5分钟
cleanup-interval-ms: 300000
# 配置说明:
# 1. enabled: 控制是否启用文件缓存功能
# 2. max-cache-size: 缓存的最大文件数量
# 3. max-cache-size-bytes: 缓存的最大总大小(字节)
# 4. expire-time-ms: 缓存条目的过期时间(毫秒)
# 5. max-single-file-size: 单个文件的最大缓存大小(字节)
# 6. evict-ratio: LRU淘汰时移除的条目比例(百分比)
# 7. enable-stats: 是否启用缓存统计功能
# 8. cleanup-interval-ms: 自动清理过期条目的间隔(毫秒)
# 性能优化建议:
# - 对于内存充足的环境,可以适当增加 max-cache-size-bytes
# - 对于频繁访问的小文件,可以增加 max-cache-size
# - 对于大文件较多的环境,可以调整 max-single-file-size
# - 对于访问频率较低的环境,可以适当减少 expire-time-ms
@@ -0,0 +1,43 @@
# 🚀 文件缓存配置示例
# 将此配置添加到 application.yml 或 application-dev.yml 中
system:
file-cache:
# 是否启用文件缓存
enabled: true
# 最大缓存文件数量
max-cache-size: 100
# 最大缓存大小(字节)- 500MB
max-cache-size-bytes: 524288000
# 缓存过期时间(毫秒)- 30分钟
expire-time-ms: 1800000
# 最大缓存单个文件大小(字节)- 50MB
max-single-file-size: 52428800
# LRU淘汰比例(百分比)
evict-ratio: 20
# 是否启用缓存统计
enable-stats: true
# 缓存清理间隔(毫秒)- 5分钟
cleanup-interval-ms: 300000
# 配置说明:
# 1. enabled: 控制是否启用文件缓存功能
# 2. max-cache-size: 缓存的最大文件数量
# 3. max-cache-size-bytes: 缓存的最大总大小(字节)
# 4. expire-time-ms: 缓存条目的过期时间(毫秒)
# 5. max-single-file-size: 单个文件的最大缓存大小(字节)
# 6. evict-ratio: LRU淘汰时移除的条目比例(百分比)
# 7. enable-stats: 是否启用缓存统计功能
# 8. cleanup-interval-ms: 自动清理过期条目的间隔(毫秒)
# 性能优化建议:
# - 对于内存充足的环境,可以适当增加 max-cache-size-bytes
# - 对于频繁访问的小文件,可以增加 max-cache-size
# - 对于大文件较多的环境,可以调整 max-single-file-size
# - 对于访问频率较低的环境,可以适当减少 expire-time-ms
@@ -272,3 +272,44 @@ justauth:
client-id: 10**********6
client-secret: 1f7d08**********5b7**********29e
redirect-uri: ${justauth.address}/social-callback?source=gitea
system:
file-cache:
# 是否启用文件缓存
enabled: true
# 最大缓存文件数量
max-cache-size: 100
# 最大缓存大小(字节)- 500MB
max-cache-size-bytes: 524288000
# 缓存过期时间(毫秒)- 30分钟
expire-time-ms: 1800000
# 最大缓存单个文件大小(字节)- 50MB
max-single-file-size: 52428800
# LRU淘汰比例(百分比)
evict-ratio: 20
# 是否启用缓存统计
enable-stats: true
# 缓存清理间隔(毫秒)- 5分钟
cleanup-interval-ms: 300000
# 配置说明:
# 1. enabled: 控制是否启用文件缓存功能
# 2. max-cache-size: 缓存的最大文件数量
# 3. max-cache-size-bytes: 缓存的最大总大小(字节)
# 4. expire-time-ms: 缓存条目的过期时间(毫秒)
# 5. max-single-file-size: 单个文件的最大缓存大小(字节)
# 6. evict-ratio: LRU淘汰时移除的条目比例(百分比)
# 7. enable-stats: 是否启用缓存统计功能
# 8. cleanup-interval-ms: 自动清理过期条目的间隔(毫秒)
# 性能优化建议:
# - 对于内存充足的环境,可以适当增加 max-cache-size-bytes
# - 对于频繁访问的小文件,可以增加 max-cache-size
# - 对于大文件较多的环境,可以调整 max-single-file-size
# - 对于访问频率较低的环境,可以适当减少 expire-time-ms
Whitespace-only changes.
@@ -25,6 +25,12 @@
<groupId>cn.idev.excel</groupId>
<artifactId>fastexcel</artifactId>
</dependency>
<!-- Spring Boot JDBC Starter,用于SQL缓存功能 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,44 @@
package org.dromara.common.excel.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* SQL缓存格式化注解
* 用于配置SQL查询来加载数据到缓存进行匹配
*
* @author Lion Li
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExcelSqlCacheFormat {
/**
* SQL缓存键,用于标识不同的SQL查询配置
*/
String sqlKey() default "";
/**
* SQL查询语句
* 支持动态SQL,可以使用参数占位符
*/
String sql() default "";
/**
* 值字段名(用于匹配的字段)
*/
String valueColumn() default "id";
/**
* 标签字段名(用于显示的字段)
*/
String labelColumn() default "name";
/**
* 分隔符
*/
String separator() default ",";
}
@@ -0,0 +1,78 @@
package org.dromara.common.excel.config;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.excel.service.SqlCacheConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* SQL缓存配置初始化器
* 在应用启动时注册SQL配置
*
* @author Lion Li
*/
@Slf4j
@Component
public class SqlCacheConfigInitializer implements CommandLineRunner {
@Autowired
private SqlCacheConfigService sqlCacheConfigService;
@Override
public void run(String... args) throws Exception {
log.info("开始初始化SQL缓存配置...");
// 注册默认的SQL配置
registerDefaultSqlConfigs();
log.info("SQL缓存配置初始化完成");
}
/**
* 注册默认的SQL配置
*/
private void registerDefaultSqlConfigs() {
Map<String, SqlCacheConfigService.SqlCacheConfig> configs = new HashMap<>();
// 示例:学生班级信息查询
// configs.put("student_class_info", new SqlCacheConfigService.SqlCacheConfig(
// "SELECT class_id as id, class_name as name FROM xg_class WHERE deleted = 0",
// "id",
// "name"));
// 示例:学院信息查询
// configs.put("college_info", new SqlCacheConfigService.SqlCacheConfig(
// "SELECT college_id as id, college_name as name FROM xg_college WHERE deleted
// = 0",
// "id",
// "name"));
// 示例:专业信息查询
// configs.put("major_info", new SqlCacheConfigService.SqlCacheConfig(
// "SELECT major_id as id, major_name as name FROM xg_major WHERE deleted = 0",
// "id",
// "name"));
// 示例:班主任信息查询
// configs.put("class_teacher_info", new SqlCacheConfigService.SqlCacheConfig(
// "SELECT user_id as id, nick_name as name FROM sys_user WHERE deleted = 0 AND
// user_type = '00'",
// "id",
// "name"));
// 楼栋信息
configs.put("xg_dorm_building", new SqlCacheConfigService.SqlCacheConfig(
"SELECT id as id, building_name as name FROM xg_dorm_building",
"id",
"name"));
// 批量注册配置
sqlCacheConfigService.registerSqlConfigs(configs);
log.info("注册了 {} 个默认SQL配置", configs.size());
}
}
@@ -0,0 +1,150 @@
package org.dromara.common.excel.convert;
import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import cn.idev.excel.converters.Converter;
import cn.idev.excel.enums.CellDataTypeEnum;
import cn.idev.excel.metadata.GlobalConfiguration;
import cn.idev.excel.metadata.data.ReadCellData;
import cn.idev.excel.metadata.data.WriteCellData;
import cn.idev.excel.metadata.property.ExcelContentProperty;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.core.service.DictService;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.excel.utils.ExcelUtil;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* 高性能字典格式化转换处理 - 预加载字典数据到内存缓存
*
* @author Lion Li
*/
@Slf4j
public class ExcelDictPreloadConvert implements Converter<Object> {
// 静态字典缓存,避免重复加载
private static final Map<String, Map<String, String>> DICT_CACHE = new ConcurrentHashMap<>();
@Override
public Class<Object> supportJavaTypeKey() {
return Object.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return null;
}
@Override
public Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
ExcelDictFormat anno = getAnnotation(contentProperty.getField());
String type = anno.dictType();
String label = cellData.getStringValue();
String value;
if (StringUtils.isBlank(type)) {
value = ExcelUtil.reverseByExp(label, anno.readConverterExp(), anno.separator());
} else {
value = SpringUtils.getBean(DictService.class).getDictValue(type, label, anno.separator());
}
return Convert.convert(contentProperty.getField().getType(), value);
}
@Override
public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
if (ObjectUtil.isNull(object)) {
return new WriteCellData<>("");
}
ExcelDictFormat anno = getAnnotation(contentProperty.getField());
String dictType = anno.dictType();
String value = Convert.toStr(object);
if (StringUtils.isBlank(dictType)) {
// 使用表达式转换
String label = ExcelUtil.convertByExp(value, anno.readConverterExp(), anno.separator());
return new WriteCellData<>(label);
} else {
// 使用预加载的字典缓存
String label = getDictLabelFromCache(dictType, value, anno.separator());
return new WriteCellData<>(label);
}
}
/**
* 预加载指定字典类型的所有数据
*/
public static void preloadDict(String dictType) {
if (!DICT_CACHE.containsKey(dictType)) {
try {
DictService dictService = SpringUtils.getBean(DictService.class);
Map<String, String> dictMap = dictService.getAllDictByDictType(dictType);
DICT_CACHE.put(dictType, dictMap);
log.info("预加载字典类型: {}, 数据量: {}", dictType, dictMap.size());
} catch (Exception e) {
log.error("预加载字典失败: {}", dictType, e);
}
}
}
/**
* 批量预加载多个字典类型
*/
public static void preloadDicts(String... dictTypes) {
for (String dictType : dictTypes) {
preloadDict(dictType);
}
}
/**
* 从缓存获取字典标签
*/
private String getDictLabelFromCache(String dictType, String value, String separator) {
Map<String, String> dictMap = DICT_CACHE.get(dictType);
if (dictMap == null) {
// 缓存未命中,实时加载
preloadDict(dictType);
dictMap = DICT_CACHE.get(dictType);
}
final Map<String, String> finalDictMap = dictMap;
if (StringUtils.containsAny(value, separator)) {
return Arrays.stream(value.split(separator))
.map(v -> finalDictMap.getOrDefault(v, v))
.collect(Collectors.joining(separator));
} else {
return finalDictMap.getOrDefault(value, value);
}
}
/**
* 清理缓存
*/
public static void clearCache() {
DICT_CACHE.clear();
log.info("字典缓存已清理");
}
/**
* 获取缓存统计信息
*/
public static Map<String, Integer> getCacheStats() {
return DICT_CACHE.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> entry.getValue().size()));
}
private ExcelDictFormat getAnnotation(Field field) {
return AnnotationUtil.getAnnotation(field, ExcelDictFormat.class);
}
}
@@ -0,0 +1,200 @@
package org.dromara.common.excel.convert;
import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import cn.idev.excel.converters.Converter;
import cn.idev.excel.enums.CellDataTypeEnum;
import cn.idev.excel.metadata.GlobalConfiguration;
import cn.idev.excel.metadata.data.ReadCellData;
import cn.idev.excel.metadata.data.WriteCellData;
import cn.idev.excel.metadata.property.ExcelContentProperty;
import org.dromara.common.excel.annotation.ExcelSqlCacheFormat;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.excel.service.SqlCacheConfigService;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.List;
import java.util.stream.Collectors;
/**
* SQL缓存格式化转换处理 - 支持自定义SQL语句加载数据到缓存
*
* @author Lion Li
*/
@Slf4j
public class ExcelSqlCacheConvert implements Converter<Object> {
// 静态SQL缓存,避免重复加载
private static final Map<String, Map<String, String>> SQL_CACHE = new ConcurrentHashMap<>();
@Override
public Class<Object> supportJavaTypeKey() {
return Object.class;
}
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return null;
}
@Override
public Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
ExcelSqlCacheFormat anno = getAnnotation(contentProperty.getField());
String sqlKey = anno.sqlKey();
String label = cellData.getStringValue();
String value;
if (StringUtils.isBlank(sqlKey)) {
value = label; // 如果没有配置SQL,直接返回标签
} else {
value = getValueFromCache(sqlKey, label, anno.valueColumn(), anno.labelColumn());
}
return Convert.convert(contentProperty.getField().getType(), value);
}
@Override
public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty,
GlobalConfiguration globalConfiguration) {
if (ObjectUtil.isNull(object)) {
return new WriteCellData<>("");
}
ExcelSqlCacheFormat anno = getAnnotation(contentProperty.getField());
String sqlKey = anno.sqlKey();
String value = Convert.toStr(object);
if (StringUtils.isBlank(sqlKey)) {
return new WriteCellData<>(value);
} else {
String label = getLabelFromCache(sqlKey, value, anno.valueColumn(), anno.labelColumn());
return new WriteCellData<>(label);
}
}
/**
* 预加载指定SQL查询的数据
*/
public static void preloadSqlData(String sqlKey, String sql, String valueColumn, String labelColumn) {
if (!SQL_CACHE.containsKey(sqlKey)) {
try {
// 这里可以通过配置获取对应的Mapper来执行SQL
// 暂时使用通用的方式,实际使用时可以根据sqlKey配置对应的Mapper
Map<String, String> dataMap = executeSqlAndBuildMap(sql, valueColumn, labelColumn);
SQL_CACHE.put(sqlKey, dataMap);
log.info("预加载SQL数据: {}, 数据量: {}", sqlKey, dataMap.size());
} catch (Exception e) {
log.error("预加载SQL数据失败: {}", sqlKey, e);
}
}
}
/**
* 批量预加载多个SQL查询
*/
public static void preloadSqlDataBatch(Map<String, SqlCacheConfig> sqlConfigs) {
for (Map.Entry<String, SqlCacheConfig> entry : sqlConfigs.entrySet()) {
SqlCacheConfig config = entry.getValue();
preloadSqlData(entry.getKey(), config.getSql(), config.getValueColumn(), config.getLabelColumn());
}
}
/**
* 从缓存获取标签
*/
private String getLabelFromCache(String sqlKey, String value, String valueColumn, String labelColumn) {
Map<String, String> dataMap = SQL_CACHE.get(sqlKey);
if (dataMap == null) {
log.warn("SQL缓存未找到: {}", sqlKey);
return value;
}
return dataMap.getOrDefault(value, value);
}
/**
* 从缓存获取值
*/
private String getValueFromCache(String sqlKey, String label, String valueColumn, String labelColumn) {
Map<String, String> dataMap = SQL_CACHE.get(sqlKey);
if (dataMap == null) {
log.warn("SQL缓存未找到: {}", sqlKey);
return label;
}
// 反向查找:通过标签找值
for (Map.Entry<String, String> entry : dataMap.entrySet()) {
if (entry.getValue().equals(label)) {
return entry.getKey();
}
}
return label;
}
/**
* 执行SQL并构建映射关系
*/
private static Map<String, String> executeSqlAndBuildMap(String sql, String valueColumn, String labelColumn) {
try {
// 使用配置服务来执行SQL
SqlCacheConfigService configService = SpringUtils.getBean(SqlCacheConfigService.class);
return configService.executeSqlAndBuildMap(sql, valueColumn, labelColumn);
} catch (Exception e) {
log.error("执行SQL失败: {}", sql, e);
return new ConcurrentHashMap<>();
}
}
/**
* 清理缓存
*/
public static void clearCache() {
SQL_CACHE.clear();
log.info("SQL缓存已清理");
}
/**
* 获取缓存统计信息
*/
public static Map<String, Integer> getCacheStats() {
return SQL_CACHE.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> entry.getValue().size()));
}
private ExcelSqlCacheFormat getAnnotation(Field field) {
return AnnotationUtil.getAnnotation(field, ExcelSqlCacheFormat.class);
}
/**
* SQL缓存配置类
*/
public static class SqlCacheConfig {
private String sql;
private String valueColumn;
private String labelColumn;
public SqlCacheConfig(String sql, String valueColumn, String labelColumn) {
this.sql = sql;
this.valueColumn = valueColumn;
this.labelColumn = labelColumn;
}
public String getSql() {
return sql;
}
public String getValueColumn() {
return valueColumn;
}
public String getLabelColumn() {
return labelColumn;
}
}
}
@@ -0,0 +1,87 @@
package org.dromara.common.excel.service;
import cn.hutool.core.collection.CollUtil;
import org.dromara.common.core.utils.StringUtils;
import cn.hutool.core.util.ReflectUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.convert.ExcelDictPreloadConvert;
import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.Set;
/**
* 高性能导出服务基类
* 提供字典预加载功能,优化导出性能
*
* @author Lion Li
*/
@Slf4j
public abstract class BaseExportService {
/**
* 预加载导出所需的字典数据
*/
protected void preloadExportDicts() {
// 获取导出VO类中所有字典字段
Set<String> dictTypes = getExportDictTypes();
if (!dictTypes.isEmpty()) {
ExcelDictPreloadConvert.preloadDicts(dictTypes.toArray(new String[0]));
log.info("预加载导出字典完成,字典类型: {}", dictTypes);
}
}
/**
* 获取导出VO类中的字典类型
* 子类可以重写此方法来自定义字典类型
*/
protected Set<String> getExportDictTypes() {
Set<String> dictTypes = new HashSet<>();
// 通过反射获取VO类中的@ExcelDictFormat注解
// 这里可以根据具体需求实现
return dictTypes;
}
/**
* 根据VO类获取字典类型
*/
protected Set<String> getDictTypesFromVoClass(Class<?> voClass) {
Set<String> dictTypes = new HashSet<>();
if (voClass != null) {
Field[] fields = ReflectUtil.getFields(voClass);
for (Field field : fields) {
ExcelDictFormat annotation = field.getAnnotation(ExcelDictFormat.class);
if (annotation != null && StringUtils.isNotBlank(annotation.dictType())) {
dictTypes.add(annotation.dictType());
}
}
}
return dictTypes;
}
/**
* 执行导出前准备
*/
protected void beforeExport() {
preloadExportDicts();
}
/**
* 执行导出后清理
*/
protected void afterExport() {
// 可选:清理缓存
// ExcelDictPreloadConvert.clearCache();
}
/**
* 获取缓存统计信息
*/
protected void logCacheStats() {
var stats = ExcelDictPreloadConvert.getCacheStats();
if (!stats.isEmpty()) {
log.info("字典缓存统计: {}", stats);
}
}
}
@@ -0,0 +1,146 @@
package org.dromara.common.excel.service;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.excel.convert.ExcelSqlCacheConvert;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* SQL缓存配置服务
* 用于管理SQL查询配置和执行
*
* @author Lion Li
*/
@Slf4j
@Service
public class SqlCacheConfigService {
// SQL配置缓存
private static final Map<String, SqlCacheConfig> SQL_CONFIGS = new ConcurrentHashMap<>();
/**
* 注册SQL配置
*/
public void registerSqlConfig(String sqlKey, String sql, String valueColumn, String labelColumn) {
SQL_CONFIGS.put(sqlKey, new SqlCacheConfig(sql, valueColumn, labelColumn));
log.info("注册SQL配置: {} -> {}", sqlKey, sql);
}
/**
* 批量注册SQL配置
*/
public void registerSqlConfigs(Map<String, SqlCacheConfig> configs) {
SQL_CONFIGS.putAll(configs);
log.info("批量注册SQL配置: {} 个", configs.size());
}
/**
* 预加载指定SQL键的数据
*/
public void preloadSqlData(String sqlKey) {
SqlCacheConfig config = SQL_CONFIGS.get(sqlKey);
if (config != null) {
ExcelSqlCacheConvert.preloadSqlData(sqlKey, config.getSql(), config.getValueColumn(),
config.getLabelColumn());
} else {
log.warn("未找到SQL配置: {}", sqlKey);
}
}
/**
* 批量预加载所有SQL数据
*/
public void preloadAllSqlData() {
for (String sqlKey : SQL_CONFIGS.keySet()) {
preloadSqlData(sqlKey);
}
}
/**
* 执行SQL并构建映射关系
*/
public Map<String, String> executeSqlAndBuildMap(String sql, String valueColumn, String labelColumn) {
try {
JdbcTemplate jdbcTemplate = SpringUtils.getBean(JdbcTemplate.class);
List<Map<String, Object>> results = jdbcTemplate.queryForList(sql);
return results.stream()
.collect(Collectors.toMap(
row -> String.valueOf(row.get(valueColumn)),
row -> String.valueOf(row.get(labelColumn)),
(existing, replacement) -> existing // 如果有重复键,保留第一个
));
} catch (Exception e) {
log.error("执行SQL失败: {}", sql, e);
return new ConcurrentHashMap<>();
}
}
/**
* 清理所有缓存
*/
public void clearAllCache() {
ExcelSqlCacheConvert.clearCache();
log.info("所有SQL缓存已清理");
}
/**
* 获取缓存统计信息
*/
public Map<String, Integer> getCacheStats() {
return ExcelSqlCacheConvert.getCacheStats();
}
/**
* 获取SQL配置
*/
public SqlCacheConfig getSqlConfig(String sqlKey) {
return SQL_CONFIGS.get(sqlKey);
}
/**
* 获取所有SQL配置
*/
public Map<String, SqlCacheConfig> getAllSqlConfigs() {
return new ConcurrentHashMap<>(SQL_CONFIGS);
}
/**
* SQL缓存配置类
*/
public static class SqlCacheConfig {
private final String sql;
private final String valueColumn;
private final String labelColumn;
public SqlCacheConfig(String sql, String valueColumn, String labelColumn) {
this.sql = sql;
this.valueColumn = valueColumn;
this.labelColumn = labelColumn;
}
public String getSql() {
return sql;
}
public String getValueColumn() {
return valueColumn;
}
public String getLabelColumn() {
return labelColumn;
}
@Override
public String toString() {
return String.format("SqlCacheConfig{sql='%s', valueColumn='%s', labelColumn='%s'}",
sql, valueColumn, labelColumn);
}
}
}
@@ -79,35 +79,45 @@ public class OssClient {
try {
// 创建 AWS 认证信息
StaticCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey()));
AwsBasicCredentials.create(properties.getAccessKey(), properties.getSecretKey()));
// MinIO 使用 HTTPS 限制使用域名访问,站点填域名。需要启用路径样式访问
boolean isStyle = !StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE);
// 创建AWS基于 Netty 的 S3 客户端
this.client = S3AsyncClient.builder()
.credentialsProvider(credentialsProvider)
.endpointOverride(URI.create(getEndpoint()))
.region(of())
.forcePathStyle(isStyle)
.httpClient(NettyNioAsyncHttpClient.builder()
.connectionTimeout(Duration.ofSeconds(60)).build())
.build();
.credentialsProvider(credentialsProvider)
.endpointOverride(URI.create(getEndpoint()))
.region(of())
.forcePathStyle(isStyle)
.httpClient(NettyNioAsyncHttpClient.builder()
// 🚀 优化:连接超时时间(60秒)
.connectionTimeout(Duration.ofSeconds(60))
// 🚀 优化:读取超时时间(300秒,适合大文件下载)
.readTimeout(Duration.ofSeconds(300))
// 🚀 优化:写入超时时间(300秒,适合大文件上传)
.writeTimeout(Duration.ofSeconds(300))
.build())
.build();
//AWS基于 CRT 的 S3 AsyncClient 实例用作 S3 传输管理器的底层客户端
this.transferManager = S3TransferManager.builder().s3Client(this.client).build();
// 🚀 优化:S3传输管理器配置
this.transferManager = S3TransferManager.builder()
.s3Client(this.client)
.build();
// 创建 S3 配置对象
S3Configuration config = S3Configuration.builder().chunkedEncodingEnabled(false)
.pathStyleAccessEnabled(isStyle).build();
S3Configuration config = S3Configuration.builder()
.chunkedEncodingEnabled(false)
.pathStyleAccessEnabled(isStyle)
.build();
// 创建 预签名 URL 的生成器 实例,用于生成 S3 预签名 URL
this.presigner = S3Presigner.builder()
.region(of())
.credentialsProvider(credentialsProvider)
.endpointOverride(URI.create(getDomain()))
.serviceConfiguration(config)
.build();
.region(of())
.credentialsProvider(credentialsProvider)
.endpointOverride(URI.create(getDomain()))
.serviceConfiguration(config)
.build();
} catch (Exception e) {
if (e instanceof OssException) {
@@ -131,17 +141,17 @@ public class OssClient {
try {
// 构建上传请求对象
FileUpload fileUpload = transferManager.uploadFile(
x -> x.putObjectRequest(
y -> y.bucket(properties.getBucketName())
.key(key)
.contentMD5(StringUtils.isNotEmpty(md5Digest) ? md5Digest : null)
.contentType(contentType)
// 用于设置对象的访问控制列表(ACL)。不同云厂商对ACL的支持和实现方式有所不同,
// 因此根据具体的云服务提供商,你可能需要进行不同的配置(自行开启,阿里云有acl权限配置,腾讯云没有acl权限配置)
//.acl(getAccessPolicy().getObjectCannedACL())
.build())
.addTransferListener(LoggingTransferListener.create())
.source(filePath).build());
x -> x.putObjectRequest(
y -> y.bucket(properties.getBucketName())
.key(key)
.contentMD5(StringUtils.isNotEmpty(md5Digest) ? md5Digest : null)
.contentType(contentType)
// 用于设置对象的访问控制列表(ACL)。不同云厂商对ACL的支持和实现方式有所不同,
// 因此根据具体的云服务提供商,你可能需要进行不同的配置(自行开启,阿里云有acl权限配置,腾讯云没有acl权限配置)
// .acl(getAccessPolicy().getObjectCannedACL())
.build())
.addTransferListener(LoggingTransferListener.create())
.source(filePath).build());
// 等待上传完成并获取上传结果
CompletedFileUpload uploadResult = fileUpload.completionFuture().join();
@@ -176,22 +186,22 @@ public class OssClient {
try {
// 创建异步请求体(length如果为空会报错)
BlockingInputStreamAsyncRequestBody body = BlockingInputStreamAsyncRequestBody.builder()
.contentLength(length)
.subscribeTimeout(Duration.ofSeconds(30))
.build();
.contentLength(length)
.subscribeTimeout(Duration.ofSeconds(30))
.build();
// 使用 transferManager 进行上传
Upload upload = transferManager.upload(
x -> x.requestBody(body)
.putObjectRequest(
y -> y.bucket(properties.getBucketName())
.key(key)
.contentType(contentType)
// 用于设置对象的访问控制列表(ACL)。不同云厂商对ACL的支持和实现方式有所不同,
// 因此根据具体的云服务提供商,你可能需要进行不同的配置(自行开启,阿里云有acl权限配置,腾讯云没有acl权限配置)
//.acl(getAccessPolicy().getObjectCannedACL())
.build())
.build());
x -> x.requestBody(body)
.putObjectRequest(
y -> y.bucket(properties.getBucketName())
.key(key)
.contentType(contentType)
// 用于设置对象的访问控制列表(ACL)。不同云厂商对ACL的支持和实现方式有所不同,
// 因此根据具体的云服务提供商,你可能需要进行不同的配置(自行开启,阿里云有acl权限配置,腾讯云没有acl权限配置)
// .acl(getAccessPolicy().getObjectCannedACL())
.build())
.build());
// 将输入流写入请求体
body.writeInputStream(inputStream);
@@ -219,13 +229,13 @@ public class OssClient {
Path tempFilePath = FileUtils.createTempFile().toPath();
// 使用 S3TransferManager 下载文件
FileDownload downloadFile = transferManager.downloadFile(
x -> x.getObjectRequest(
y -> y.bucket(properties.getBucketName())
.key(removeBaseUrl(path))
.build())
.addTransferListener(LoggingTransferListener.create())
.destination(tempFilePath)
.build());
x -> x.getObjectRequest(
y -> y.bucket(properties.getBucketName())
.key(removeBaseUrl(path))
.build())
.addTransferListener(LoggingTransferListener.create())
.destination(tempFilePath)
.build());
// 等待文件下载操作完成
downloadFile.completionFuture().join();
return tempFilePath;
@@ -234,8 +244,8 @@ public class OssClient {
/**
* 下载文件从 Amazon S3 到 输出流
*
* @param key 文件在 Amazon S3 中的对象键
* @param out 输出流
* @param key 文件在 Amazon S3 中的对象键
* @param out 输出流
* @param consumer 自定义处理逻辑
* @return 输出流中写入的字节数(长度)
* @throws OssException 如果下载失败,抛出自定义异常
@@ -244,18 +254,19 @@ public class OssClient {
try {
// 构建下载请求
DownloadRequest<ResponseInputStream<GetObjectResponse>> downloadRequest = DownloadRequest.builder()
// 文件对象
.getObjectRequest(y -> y.bucket(properties.getBucketName())
.key(key)
.build())
.addTransferListener(LoggingTransferListener.create())
// 使用订阅转换器
.responseTransformer(AsyncResponseTransformer.toBlockingInputStream())
.build();
// 文件对象
.getObjectRequest(y -> y.bucket(properties.getBucketName())
.key(key)
.build())
.addTransferListener(LoggingTransferListener.create())
// 使用订阅转换器
.responseTransformer(AsyncResponseTransformer.toBlockingInputStream())
.build();
// 使用 S3TransferManager 下载文件
Download<ResponseInputStream<GetObjectResponse>> responseFuture = transferManager.download(downloadRequest);
// 输出到流中
try (ResponseInputStream<GetObjectResponse> responseStream = responseFuture.completionFuture().join().result()) { // auto-closeable stream
try (ResponseInputStream<GetObjectResponse> responseStream = responseFuture.completionFuture().join()
.result()) { // auto-closeable stream
if (consumer != null) {
consumer.accept(responseStream.response().contentLength());
}
@@ -274,9 +285,9 @@ public class OssClient {
public void delete(String path) {
try {
client.deleteObject(
x -> x.bucket(properties.getBucketName())
.key(removeBaseUrl(path))
.build());
x -> x.bucket(properties.getBucketName())
.key(removeBaseUrl(path))
.build());
} catch (Exception e) {
throw new OssException("删除文件失败,请检查配置信息:[" + e.getMessage() + "]");
}
@@ -292,12 +303,12 @@ public class OssClient {
// 使用 AWS S3 预签名 URL 的生成器 获取对象的预签名 URL
URL url = presigner.presignGetObject(
x -> x.signatureDuration(expiredTime)
.getObjectRequest(
y -> y.bucket(properties.getBucketName())
.key(objectKey)
.build())
.build())
.url();
.getObjectRequest(
y -> y.bucket(properties.getBucketName())
.key(objectKey)
.build())
.build())
.url();
return url.toString();
}
@@ -310,7 +321,8 @@ public class OssClient {
* @throws OssException 如果上传失败,抛出自定义异常
*/
public UploadResult uploadSuffix(byte[] data, String suffix, String contentType) {
return upload(new ByteArrayInputStream(data), getPath(properties.getPrefix(), suffix), Long.valueOf(data.length), contentType);
return upload(new ByteArrayInputStream(data), getPath(properties.getPrefix(), suffix),
Long.valueOf(data.length), contentType);
}
/**
@@ -400,7 +412,7 @@ public class OssClient {
* @return 对应的 AWS 区域对象,或者默认的广泛支持的区域(us-east-1)
*/
public Region of() {
//AWS 区域字符串
// AWS 区域字符串
String region = properties.getRegion();
// 如果 region 参数非空,使用 Region.of 方法创建对应的 AWS 区域对象,否则返回默认区域
return StringUtils.isNotEmpty(region) ? Region.of(region) : Region.US_EAST_1;
@@ -422,8 +434,9 @@ public class OssClient {
// MinIO 单独处理
if (StringUtils.isNotEmpty(domain)) {
// 如果 domain 以 "https://" 或 "http://" 开头
return (domain.startsWith(Constants.HTTPS) || domain.startsWith(Constants.HTTP)) ?
domain + StringUtils.SLASH + properties.getBucketName() : header + domain + StringUtils.SLASH + properties.getBucketName();
return (domain.startsWith(Constants.HTTPS) || domain.startsWith(Constants.HTTP))
? domain + StringUtils.SLASH + properties.getBucketName()
: header + domain + StringUtils.SLASH + properties.getBucketName();
}
return header + endpoint + StringUtils.SLASH + properties.getBucketName();
}
@@ -441,8 +454,8 @@ public class OssClient {
// 生成日期路径
String datePath = DateUtils.datePath();
// 拼接路径
String path = StringUtils.isNotEmpty(prefix) ?
prefix + StringUtils.SLASH + datePath + StringUtils.SLASH + uuid : datePath + StringUtils.SLASH + uuid;
String path = StringUtils.isNotEmpty(prefix) ? prefix + StringUtils.SLASH + datePath + StringUtils.SLASH + uuid
: datePath + StringUtils.SLASH + uuid;
return path + suffix;
}
@@ -0,0 +1,210 @@
package org.dromara.cw.controller;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.dromara.common.web.core.BaseController;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import org.dromara.cw.domain.vo.CwApprovalRecordVo;
import org.dromara.cw.domain.bo.CwApprovalRecordBo;
import org.dromara.cw.service.ICwApprovalRecordService;
/**
* 审批记录
*
* @author ruoyi
* @date 2025-01-26
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/cw/approval")
public class CwApprovalRecordController extends BaseController {
private final ICwApprovalRecordService approvalRecordService;
/**
* 查询审批记录列表
*/
@SaCheckPermission("cw:approval:list")
@GetMapping("/list")
public TableDataInfo<CwApprovalRecordVo> list(CwApprovalRecordBo bo, PageQuery pageQuery) {
return approvalRecordService.queryPageList(bo, pageQuery);
}
/**
* 导出审批记录列表
*/
@SaCheckPermission("cw:approval:export")
@Log(title = "审批记录", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(CwApprovalRecordBo bo, HttpServletResponse response) {
List<CwApprovalRecordVo> list = approvalRecordService.queryList(bo);
ExcelUtil.exportExcel(list, "审批记录", CwApprovalRecordVo.class, response);
}
/**
* 获取审批记录详细信息
*/
@SaCheckPermission("cw:approval:query")
@GetMapping("/{id}")
public R<CwApprovalRecordVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(approvalRecordService.queryById(id));
}
/**
* 根据申报ID查询审批记录列表
*/
@SaCheckPermission("cw:approval:query")
@GetMapping("/byApplication/{applicationId}")
public R<List<CwApprovalRecordVo>> getByApplicationId(@PathVariable Long applicationId) {
List<CwApprovalRecordVo> list = approvalRecordService.queryByApplicationId(applicationId);
return R.ok(list);
}
/**
* 查询当前用户待审批记录列表
*/
@SaCheckPermission("cw:approval:pending")
@GetMapping("/pending")
public R<List<CwApprovalRecordVo>> getPendingList() {
List<CwApprovalRecordVo> list = approvalRecordService.queryPendingByCurrentUser();
return R.ok(list);
}
/**
* 查询部门待审批申报列表
*/
@SaCheckPermission("cw:approval:dept")
@GetMapping("/dept/pending")
public R<List<CwApprovalRecordVo>> getDeptPendingList() {
List<CwApprovalRecordVo> list = approvalRecordService.queryDeptPendingApprovals();
return R.ok(list);
}
/**
* 查询所有待审批申报列表(超级管理员专用)
*/
@SaCheckPermission("cw:approval:list")
@GetMapping("/all/pending")
public R<List<CwApprovalRecordVo>> getAllPendingList() {
List<CwApprovalRecordVo> list = approvalRecordService.queryAllPendingApprovals();
return R.ok(list);
}
/**
* 新增审批记录
*/
@SaCheckPermission("cw:approval:add")
@Log(title = "审批记录", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody CwApprovalRecordBo bo) {
return toAjax(approvalRecordService.insertByBo(bo));
}
/**
* 修改审批记录
*/
@SaCheckPermission("cw:approval:edit")
@Log(title = "审批记录", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody CwApprovalRecordBo bo) {
return toAjax(approvalRecordService.updateByBo(bo));
}
/**
* 删除审批记录
*/
@SaCheckPermission("cw:approval:remove")
@Log(title = "审批记录", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(approvalRecordService.deleteWithValidByIds(List.of(ids), true));
}
/**
* 创建审批流程
*/
@SaCheckPermission("cw:approval:create")
@Log(title = "创建审批流程", businessType = BusinessType.INSERT)
@PostMapping("/create/{applicationId}")
public R<Void> createProcess(@PathVariable Long applicationId, @RequestParam String applicationNo) {
return toAjax(approvalRecordService.createApprovalProcess(applicationId, applicationNo));
}
/**
* 审批操作
*/
@SaCheckPermission("cw:approval:process")
@Log(title = "审批操作", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping("/process")
public R<Void> processApproval(@RequestParam Long applicationId,
@RequestParam String nodeCode,
@RequestParam String approveAction,
@RequestParam(required = false) String approveOpinion) {
return toAjax(approvalRecordService.processApproval(applicationId, nodeCode, approveAction, approveOpinion));
}
/**
* 部门审批
*/
@SaCheckPermission("cw:approval:dept")
@Log(title = "部门审批", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping("/dept")
public R<Void> deptApprove(@RequestParam Long applicationId,
@RequestParam String approveAction,
@RequestParam(required = false) String approveOpinion) {
return toAjax(approvalRecordService.deptApprove(applicationId, approveAction, approveOpinion));
}
/**
* 查询当前审批节点
*/
@SaCheckPermission("cw:approval:query")
@GetMapping("/current/{applicationId}")
public R<CwApprovalRecordVo> getCurrentNode(@PathVariable Long applicationId) {
CwApprovalRecordVo record = approvalRecordService.queryCurrentApprovalNode(applicationId);
return R.ok(record);
}
/**
* 检查审批权限
*/
@SaCheckPermission("cw:approval:check")
@GetMapping("/check/{applicationId}")
public R<Boolean> checkPermission(@PathVariable Long applicationId, @RequestParam String nodeCode) {
Boolean hasPermission = approvalRecordService.checkApprovalPermission(applicationId, nodeCode);
return R.ok(hasPermission);
}
/**
* 检查部门审批权限
*/
@SaCheckPermission("cw:approval:dept")
@GetMapping("/check/dept/{applicationId}")
public R<Boolean> checkDeptPermission(@PathVariable Long applicationId) {
Boolean hasPermission = approvalRecordService.checkDeptApprovalPermission(applicationId);
return R.ok(hasPermission);
}
}
@@ -0,0 +1,247 @@
package org.dromara.cw.controller;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.web.core.BaseController;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.cw.domain.vo.CwProjectApplicationVo;
import org.dromara.cw.domain.bo.CwProjectApplicationBo;
import org.dromara.cw.service.ICwProjectApplicationService;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.log.enums.BusinessType;
/**
* 项目预算申报
*
* @author ruoyi
* @date 2025-01-26
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/cw/application")
@Slf4j
public class CwProjectApplicationController extends BaseController {
private final ICwProjectApplicationService projectApplicationService;
/**
* 查询项目预算申报列表
*/
@SaCheckPermission("cw:application:list")
@GetMapping("/list")
public TableDataInfo<CwProjectApplicationVo> list(CwProjectApplicationBo bo, PageQuery pageQuery) {
return projectApplicationService.queryPageList(bo, pageQuery);
}
/**
* 查询我的申报列表
*/
@SaCheckPermission("cw:application:list")
@GetMapping("/myList")
public R<List<CwProjectApplicationVo>> myList(@RequestParam(required = false) Integer year) {
Long applicantId = LoginHelper.getUserId();
List<CwProjectApplicationVo> list = projectApplicationService.queryMyApplications(applicantId, year);
return R.ok(list);
}
/**
* 查询部门申报列表
*/
@SaCheckPermission("cw:application:list")
@GetMapping("/deptList")
public R<List<CwProjectApplicationVo>> deptList(@RequestParam Long departmentId, @RequestParam(required = false) Integer year) {
List<CwProjectApplicationVo> list = projectApplicationService.queryDeptApplications(departmentId, year);
return R.ok(list);
}
/**
* 查询待我审批的申报列表
*/
@SaCheckPermission("cw:approvalRecord:list")
@GetMapping("/pendingList")
public R<List<CwProjectApplicationVo>> pendingList() {
Long approverId = LoginHelper.getUserId();
List<CwProjectApplicationVo> list = projectApplicationService.queryPendingApprovals(approverId);
return R.ok(list);
}
/**
* 导出项目预算申报列表
*/
@SaCheckPermission("cw:application:export")
@Log(title = "项目预算申报", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(CwProjectApplicationBo bo, HttpServletResponse response) {
List<CwProjectApplicationVo> list = projectApplicationService.exportList(bo);
ExcelUtil.exportExcel(list, "项目预算申报", CwProjectApplicationVo.class, response);
}
/**
* 获取项目预算申报详细信息
*/
@SaCheckPermission("cw:application:query")
@GetMapping("/{id}")
public R<CwProjectApplicationVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(projectApplicationService.queryById(id));
}
/**
* 根据申报编号获取详细信息
*/
@SaCheckPermission("cw:application:query")
@GetMapping("/byNo/{applicationNo}")
public R<CwProjectApplicationVo> getInfoByNo(@PathVariable String applicationNo) {
return R.ok(projectApplicationService.queryByApplicationNo(applicationNo));
}
/**
* 新增项目预算申报
*/
@SaCheckPermission("cw:application:add")
@Log(title = "项目预算申报", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<CwProjectApplicationVo> add(@Validated(AddGroup.class) @RequestBody CwProjectApplicationBo bo) {
Boolean result = projectApplicationService.insertByBo(bo);
if (result && bo.getId() != null) {
// 返回创建的记录
CwProjectApplicationVo vo = projectApplicationService.queryById(bo.getId());
return R.ok(vo);
} else {
return R.fail("新增项目申报失败");
}
}
/**
* 修改项目预算申报
*/
@SaCheckPermission("cw:application:edit")
@Log(title = "项目预算申报", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody CwProjectApplicationBo bo) {
return toAjax(projectApplicationService.updateByBo(bo));
}
/**
* 保存草稿
*/
@SaCheckPermission("cw:application:add")
@Log(title = "保存申报草稿", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping("/saveDraft")
public R<CwProjectApplicationVo> saveDraft(@RequestBody CwProjectApplicationBo bo) {
log.info("=== 控制器接收到保存草稿请求 ===");
log.info("项目名称: {}", bo.getProjectName());
log.info("绩效指标数据: {}", bo.getPerformanceIndicators());
log.info("绩效指标数量: {}", bo.getPerformanceIndicators() != null ? bo.getPerformanceIndicators().size() : 0);
Boolean result = projectApplicationService.saveDraft(bo);
if (result && bo.getId() != null) {
// 保存成功后,返回完整的申报信息
CwProjectApplicationVo vo = projectApplicationService.queryById(bo.getId());
return R.ok(vo);
}
return R.fail("保存草稿失败");
}
/**
* 提交申报
*/
@SaCheckPermission("cw:application:submit")
@Log(title = "提交项目预算申报", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping("/submit/{id}")
public R<Void> submit(@PathVariable Long id) {
return toAjax(projectApplicationService.submitApplication(id));
}
/**
* 撤回申报
*/
@SaCheckPermission("cw:application:edit")
@Log(title = "撤回项目预算申报", businessType = BusinessType.UPDATE)
@PostMapping("/withdraw/{id}")
public R<Void> withdraw(@PathVariable Long id) {
return toAjax(projectApplicationService.withdrawApplication(id));
}
/**
* 取消申报
*/
@SaCheckPermission("cw:application:remove")
@Log(title = "取消项目预算申报", businessType = BusinessType.UPDATE)
@PostMapping("/cancel/{id}")
public R<Void> cancel(@PathVariable Long id) {
return toAjax(projectApplicationService.cancelApplication(id));
}
/**
* 删除项目预算申报
*/
@SaCheckPermission("cw:application:remove")
@Log(title = "项目预算申报", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(projectApplicationService.deleteWithValidByIds(List.of(ids), true));
}
/**
* 生成申报编号
*/
@SaCheckPermission("cw:application:add")
@GetMapping("/generateNo")
public R<String> generateApplicationNo() {
return R.ok(projectApplicationService.generateApplicationNo());
}
/**
* 统计数据
*/
@SaCheckPermission("cw:application:list")
@GetMapping("/statistics")
public R<java.util.Map<String, Object>> statistics(@RequestParam(required = false) Integer year) {
java.util.Map<String, Object> result = new java.util.HashMap<>();
// 申报数量统计 - 使用字符串状态值
result.put("totalCount", projectApplicationService.countApplications(year, null));
result.put("draftCount", projectApplicationService.countApplications(year, "draft"));
result.put("submitCount", projectApplicationService.countApplications(year, "submitted"));
// 审批中包含已提交和正在审批的申报
Long submittedCount = projectApplicationService.countApplications(year, "submitted");
Long approvingCount = projectApplicationService.countApplications(year, "approving");
result.put("approvingCount", submittedCount + approvingCount);
result.put("approvedCount", projectApplicationService.countApplications(year, "approved"));
result.put("rejectedCount", projectApplicationService.countApplications(year, "rejected"));
// 金额统计 - 使用字符串状态值
result.put("totalAmount", projectApplicationService.sumApplicationAmount(year, null));
result.put("approvedAmount", projectApplicationService.sumApplicationAmount(year, "approved"));
// 部门统计
result.put("deptStatistics", projectApplicationService.statisticsByDept(year));
// 月度趋势
result.put("monthlyTrend", projectApplicationService.statisticsByMonth(year));
return R.ok(result);
}
}
@@ -0,0 +1,127 @@
package org.dromara.cw.domain;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.tenant.core.TenantEntity;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 审批记录对象 cw_approval_record
*
* @author ruoyi
* @date 2025-01-26
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("cw_approval_record")
public class CwApprovalRecord extends TenantEntity implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id")
private Long id;
/**
* 申报ID
*/
private Long applicationId;
/**
* 申报编号
*/
private String applicationNo;
/**
* 节点编码(dept_approve-部门审批 finance_approve-财务审批 leader_approve-领导审批)
*/
private String nodeCode;
/**
* 节点名称
*/
private String nodeName;
/**
* 节点顺序(1-部门审批 2-财务审批 3-领导审批)
*/
private Integer nodeOrder;
/**
* 审批人ID
*/
private Long approverId;
/**
* 审批人姓名
*/
private String approverName;
/**
* 是否代理审批(0-否 1-是)
*/
private Boolean isDelegate;
/**
* 原审批人ID(代理时使用)
*/
private Long originalApproverId;
/**
* 原审批人姓名(代理时使用)
*/
private String originalApproverName;
/**
* 节点状态(waiting-等待审批 approving-审批中 approved-已通过 rejected-已拒绝 returned-已退回)
*/
private String nodeStatus;
/**
* 审批动作(approve-通过 reject-拒绝 return-退回)
*/
private String approveAction;
/**
* 审批意见
*/
private String approveOpinion;
/**
* 节点开始时间
*/
private Date startTime;
/**
* 节点结束时间
*/
private Date endTime;
/**
* 期望完成时间
*/
private Date expectFinishTime;
/**
* 是否超时(0-否 1-是)
*/
private Boolean isOvertime;
/**
* 使用的审批配置ID
*/
private Long configId;
/**
* 审批配置快照(JSON格式,记录当时的配置)
*/
private String configSnapshot;
}
@@ -0,0 +1,114 @@
package org.dromara.cw.domain;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import java.math.BigDecimal;
/**
* 审批人配置对象 cw_approver_config
*
* @author ruoyi
* @date 2025-01-26
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("cw_approver_config")
public class CwApproverConfig extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id")
private Long id;
/**
* 配置类型(global-全局 dept-部门 amount-金额)
*/
private String configType;
/**
* 部门ID(按部门配置时使用)
*/
private Long deptId;
/**
* 最小金额(按金额配置时使用)
*/
private BigDecimal minAmount;
/**
* 最大金额(按金额配置时使用)
*/
private BigDecimal maxAmount;
/**
* 部门审批人ID
*/
private Long deptApproverId;
/**
* 部门审批人姓名
*/
private String deptApproverName;
/**
* 财务审批人ID
*/
private Long financeApproverId;
/**
* 财务审批人姓名
*/
private String financeApproverName;
/**
* 领导审批人ID
*/
private Long leaderApproverId;
/**
* 领导审批人姓名
*/
private String leaderApproverName;
/**
* 部门代理审批人ID
*/
private Long deptDelegateId;
/**
* 部门代理审批人姓名
*/
private String deptDelegateName;
/**
* 财务代理审批人ID
*/
private Long financeDelegateId;
/**
* 财务代理审批人姓名
*/
private String financeDelegateName;
/**
* 领导代理审批人ID
*/
private Long leaderDelegateId;
/**
* 领导代理审批人姓名
*/
private String leaderDelegateName;
/**
* 状态(0-禁用 1-启用)
*/
private String status;
}
@@ -0,0 +1,106 @@
package org.dromara.cw.domain;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import java.math.BigDecimal;
/**
* 绩效指标对象 cw_performance_indicator
*
* @author ruoyi
* @date 2025-01-26
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("cw_performance_indicator")
public class CwPerformanceIndicator extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id")
private Long id;
/**
* 申报ID
*/
@TableField(insertStrategy = FieldStrategy.NOT_NULL)
private Long applicationId;
/**
* 申报编号
*/
@TableField(insertStrategy = FieldStrategy.NOT_EMPTY)
private String applicationNo;
/**
* 指标类型(quantity-数量指标 quality-质量指标 timeliness-时效指标 cost-成本指标 economic-经济效益指标 social-社会效益指标 environmental-环境效益指标 satisfaction-满意度指标)
*/
@TableField(insertStrategy = FieldStrategy.NOT_EMPTY)
private String indicatorType;
/**
* 指标类型名称
*/
@TableField(insertStrategy = FieldStrategy.NOT_EMPTY)
private String indicatorTypeName;
/**
* 绩效目标名称
*/
@TableField(insertStrategy = FieldStrategy.NOT_EMPTY)
private String goalName;
/**
* 目标值
*/
@TableField(insertStrategy = FieldStrategy.NOT_EMPTY)
private String targetValue;
/**
* 分值占比(%)
*/
@TableField(insertStrategy = FieldStrategy.NOT_NULL)
private BigDecimal weightPercentage;
/**
* 详细说明
*/
private String description;
/**
* 计量单位
*/
private String unit;
/**
* 测量方法
*/
private String measurementMethod;
/**
* 数据来源
*/
private String dataSource;
/**
* 是否定量指标(0-定性 1-定量)
*/
private Boolean isQuantitative;
/**
* 排序
*/
private Integer sortOrder;
/**
* 状态(0-删除 1-正常)
*/
private String status;
}
@@ -0,0 +1,215 @@
package org.dromara.cw.domain;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import java.math.BigDecimal;
import java.util.Date;
/**
* 项目预算申报对象 cw_project_application
*
* @author ruoyi
* @date 2025-01-26
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("cw_project_application")
public class CwProjectApplication extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id")
private Long id;
/**
* 申报编号
*/
private String applicationNo;
/**
* 项目名称
*/
private String projectName;
/**
* 项目负责人
*/
private String principal;
/**
* 联系电话
*/
private String contactPhone;
/**
* 项目实施开始时间
*/
private Date startDate;
/**
* 项目实施结束时间
*/
private Date endDate;
/**
* 是否面向中小企业(0-否 1-是)
*/
private Boolean targetSme;
/**
* 是否面向小微企业(0-否 1-是)
*/
private Boolean targetMicroSme;
/**
* 是否采购进口设备(0-否 1-是)
*/
private Boolean importEquipment;
/**
* 项目申请原因(包括设立依据、可行性及必要性)
*/
private String applicationReason;
/**
* 项目建设内容
*/
private String constructionContent;
/**
* 绩效总目标
*/
private String totalPerformanceGoal;
/**
* 当年绩效目标
*/
private String currentYearGoal;
/**
* 项目总投资-合计
*/
private BigDecimal totalInvestment;
/**
* 项目总投资-市财政资金
*/
private BigDecimal totalMunicipalFiscal;
/**
* 项目总投资-单位自筹
*/
private BigDecimal totalSelfFunding;
/**
* 项目总投资-其他
*/
private BigDecimal totalOtherFunding;
/**
* 年度预计执行数-合计
*/
private BigDecimal currentYearTotal;
/**
* 年度预计执行数-市财政资金
*/
private BigDecimal currentYearMunicipal;
/**
* 年度预计执行数-单位自筹
*/
private BigDecimal currentYearSelf;
/**
* 年度预计执行数-其他
*/
private BigDecimal currentYearOther;
/**
* 下年度申请资金-合计
*/
private BigDecimal nextYearTotal;
/**
* 下年度申请资金-市财政资金
*/
private BigDecimal nextYearMunicipal;
/**
* 下年度申请资金-单位自筹
*/
private BigDecimal nextYearSelf;
/**
* 下年度申请资金-其他
*/
private BigDecimal nextYearOther;
/**
* 以后年度安排-合计
*/
private BigDecimal futureYearTotal;
/**
* 以后年度安排-市财政资金
*/
private BigDecimal futureYearMunicipal;
/**
* 以后年度安排-单位自筹
*/
private BigDecimal futureYearSelf;
/**
* 以后年度安排-其他
*/
private BigDecimal futureYearOther;
/**
* 申请人ID
*/
private Long applicantId;
/**
* 申请人姓名
*/
private String applicantName;
/**
* 所属部门ID
*/
private Long departmentId;
/**
* 所属部门名称
*/
private String departmentName;
/**
* 审批状态(1-草稿 2-已提交 3-审批中 4-已通过 5-已拒绝 6-已退回 7-已取消)
*/
private String approvalStatus;
/**
* 提交时间
*/
private Date submitTime;
/**
* 审批完成时间
*/
private Date approveTime;
/**
* 预算年度
*/
private Integer year;
}
@@ -0,0 +1,134 @@
package org.dromara.cw.domain.bo;
import org.dromara.cw.domain.CwApprovalRecord;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.*;
import java.util.Date;
/**
* 审批记录业务对象 cw_approval_record
*
* @author ruoyi
* @date 2025-01-26
*/
@Data
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = CwApprovalRecord.class, reverseConvertGenerate = false)
public class CwApprovalRecordBo extends BaseEntity {
/**
* 主键ID
*/
@NotNull(message = "主键ID不能为空", groups = { EditGroup.class })
private Long id;
/**
* 申报ID
*/
@NotNull(message = "申报ID不能为空", groups = { AddGroup.class, EditGroup.class })
private Long applicationId;
/**
* 申报编号
*/
@NotBlank(message = "申报编号不能为空", groups = { AddGroup.class, EditGroup.class })
private String applicationNo;
/**
* 节点编码(dept_approve-部门审批 finance_approve-财务审批 leader_approve-领导审批)
*/
@NotBlank(message = "节点编码不能为空", groups = { AddGroup.class, EditGroup.class })
private String nodeCode;
/**
* 节点名称
*/
@NotBlank(message = "节点名称不能为空", groups = { AddGroup.class, EditGroup.class })
private String nodeName;
/**
* 节点顺序(1-部门审批 2-财务审批 3-领导审批)
*/
@NotNull(message = "节点顺序不能为空", groups = { AddGroup.class, EditGroup.class })
private Integer nodeOrder;
/**
* 审批人ID
*/
@NotNull(message = "审批人ID不能为空", groups = { AddGroup.class, EditGroup.class })
private Long approverId;
/**
* 审批人姓名
*/
@NotBlank(message = "审批人姓名不能为空", groups = { AddGroup.class, EditGroup.class })
private String approverName;
/**
* 是否代理审批(0-否 1-是)
*/
private Boolean isDelegate;
/**
* 原审批人ID(代理时使用)
*/
private Long originalApproverId;
/**
* 原审批人姓名(代理时使用)
*/
private String originalApproverName;
/**
* 节点状态(waiting-等待审批 approving-审批中 approved-已通过 rejected-已拒绝 returned-已退回)
*/
@NotBlank(message = "节点状态不能为空", groups = { AddGroup.class, EditGroup.class })
private String nodeStatus;
/**
* 审批动作(approve-通过 reject-拒绝 return-退回)
*/
private String approveAction;
/**
* 审批意见
*/
private String approveOpinion;
/**
* 节点开始时间
*/
private Date startTime;
/**
* 节点结束时间
*/
private Date endTime;
/**
* 期望完成时间
*/
private Date expectFinishTime;
/**
* 是否超时(0-否 1-是)
*/
private Boolean isOvertime;
/**
* 使用的审批配置ID
*/
private Long configId;
/**
* 审批配置快照(JSON格式,记录当时的配置)
*/
private String configSnapshot;
}
@@ -0,0 +1,125 @@
package org.dromara.cw.domain.bo;
import org.dromara.cw.domain.CwApproverConfig;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
/**
* 审批人配置业务对象 cw_approver_config
*
* @author ruoyi
* @date 2025-01-26
*/
@Data
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = CwApproverConfig.class, reverseConvertGenerate = false)
public class CwApproverConfigBo extends BaseEntity {
/**
* 主键ID
*/
@NotNull(message = "主键ID不能为空", groups = { EditGroup.class })
private Long id;
/**
* 配置类型(global-全局 dept-部门 amount-金额)
*/
@NotBlank(message = "配置类型不能为空", groups = { AddGroup.class, EditGroup.class })
private String configType;
/**
* 部门ID(按部门配置时使用)
*/
private Long deptId;
/**
* 最小金额(按金额配置时使用)
*/
@Min(value = 0, message = "最小金额不能为负数")
private BigDecimal minAmount;
/**
* 最大金额(按金额配置时使用)
*/
@Min(value = 0, message = "最大金额不能为负数")
private BigDecimal maxAmount;
/**
* 部门审批人ID
*/
@NotNull(message = "部门审批人ID不能为空", groups = { AddGroup.class, EditGroup.class })
private Long deptApproverId;
/**
* 部门审批人姓名
*/
@NotBlank(message = "部门审批人姓名不能为空", groups = { AddGroup.class, EditGroup.class })
private String deptApproverName;
/**
* 财务审批人ID
*/
@NotNull(message = "财务审批人ID不能为空", groups = { AddGroup.class, EditGroup.class })
private Long financeApproverId;
/**
* 财务审批人姓名
*/
@NotBlank(message = "财务审批人姓名不能为空", groups = { AddGroup.class, EditGroup.class })
private String financeApproverName;
/**
* 领导审批人ID
*/
@NotNull(message = "领导审批人ID不能为空", groups = { AddGroup.class, EditGroup.class })
private Long leaderApproverId;
/**
* 领导审批人姓名
*/
@NotBlank(message = "领导审批人姓名不能为空", groups = { AddGroup.class, EditGroup.class })
private String leaderApproverName;
/**
* 部门代理审批人ID
*/
private Long deptDelegateId;
/**
* 部门代理审批人姓名
*/
private String deptDelegateName;
/**
* 财务代理审批人ID
*/
private Long financeDelegateId;
/**
* 财务代理审批人姓名
*/
private String financeDelegateName;
/**
* 领导代理审批人ID
*/
private Long leaderDelegateId;
/**
* 领导代理审批人姓名
*/
private String leaderDelegateName;
/**
* 状态(0-禁用 1-启用)
*/
private String status;
}
@@ -0,0 +1,116 @@
package org.dromara.cw.domain.bo;
import org.dromara.cw.domain.CwPerformanceIndicator;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
/**
* 绩效指标业务对象 cw_performance_indicator
*
* @author ruoyi
* @date 2025-01-26
*/
@Data
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = CwPerformanceIndicator.class, reverseConvertGenerate = false)
public class CwPerformanceIndicatorBo extends BaseEntity {
/**
* 主键ID
*/
@NotNull(message = "主键ID不能为空", groups = { EditGroup.class })
private Long id;
/**
* 申报ID
*/
@NotNull(message = "申报ID不能为空", groups = { AddGroup.class, EditGroup.class })
private Long applicationId;
/**
* 申报编号
*/
@NotBlank(message = "申报编号不能为空", groups = { AddGroup.class, EditGroup.class })
private String applicationNo;
/**
* 指标类型
*/
@NotBlank(message = "指标类型不能为空", groups = { AddGroup.class, EditGroup.class })
private String indicatorType;
/**
* 指标类型名称
*/
@NotBlank(message = "指标类型名称不能为空", groups = { AddGroup.class, EditGroup.class })
private String indicatorTypeName;
/**
* 绩效目标名称
*/
@NotBlank(message = "绩效目标名称不能为空", groups = { AddGroup.class, EditGroup.class })
@Size(max = 200, message = "绩效目标名称不能超过200个字符")
private String goalName;
/**
* 目标值
*/
@NotBlank(message = "目标值不能为空", groups = { AddGroup.class, EditGroup.class })
@Size(max = 100, message = "目标值不能超过100个字符")
private String targetValue;
/**
* 分值占比(%)
*/
@NotNull(message = "分值占比不能为空", groups = { AddGroup.class, EditGroup.class })
@DecimalMin(value = "0.1", message = "分值占比必须大于0")
@DecimalMax(value = "100.0", message = "分值占比不能超过100")
private BigDecimal weightPercentage;
/**
* 详细说明
*/
@Size(max = 500, message = "详细说明不能超过500个字符")
private String description;
/**
* 计量单位
*/
@Size(max = 20, message = "计量单位不能超过20个字符")
private String unit;
/**
* 测量方法
*/
@Size(max = 200, message = "测量方法不能超过200个字符")
private String measurementMethod;
/**
* 数据来源
*/
@Size(max = 200, message = "数据来源不能超过200个字符")
private String dataSource;
/**
* 是否定量指标(0-定性 1-定量)
*/
private Boolean isQuantitative;
/**
* 排序
*/
private Integer sortOrder;
/**
* 状态(0-删除 1-正常)
*/
private String status;
}
@@ -0,0 +1,258 @@
package org.dromara.cw.domain.bo;
import org.dromara.cw.domain.CwProjectApplication;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
/**
* 项目预算申报业务对象 cw_project_application
*
* @author ruoyi
* @date 2025-01-26
*/
@Data
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = CwProjectApplication.class, reverseConvertGenerate = false)
public class CwProjectApplicationBo extends BaseEntity {
/**
* 主键ID
*/
@NotNull(message = "主键ID不能为空", groups = { EditGroup.class })
private Long id;
/**
* 申报编号
*/
@NotBlank(message = "申报编号不能为空", groups = { EditGroup.class })
private String applicationNo;
/**
* 项目名称
*/
@NotBlank(message = "项目名称不能为空", groups = { AddGroup.class, EditGroup.class })
@Size(max = 200, message = "项目名称不能超过200个字符")
private String projectName;
/**
* 项目负责人
*/
@NotBlank(message = "项目负责人不能为空", groups = { AddGroup.class, EditGroup.class })
@Size(max = 50, message = "项目负责人不能超过50个字符")
private String principal;
/**
* 联系电话
*/
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "联系电话格式不正确")
private String contactPhone;
/**
* 项目实施开始时间
*/
@NotNull(message = "项目实施开始时间不能为空", groups = { AddGroup.class, EditGroup.class })
private Date startDate;
/**
* 项目实施结束时间
*/
@NotNull(message = "项目实施结束时间不能为空", groups = { AddGroup.class, EditGroup.class })
private Date endDate;
/**
* 是否面向中小企业(0-否 1-是)
*/
private Boolean targetSme;
/**
* 是否面向小微企业(0-否 1-是)
*/
private Boolean targetMicroSme;
/**
* 是否采购进口设备(0-否 1-是)
*/
private Boolean importEquipment;
/**
* 项目申请原因(包括设立依据、可行性及必要性)
*/
private String applicationReason;
/**
* 项目建设内容
*/
@NotBlank(message = "项目建设内容不能为空", groups = { AddGroup.class, EditGroup.class })
private String constructionContent;
/**
* 绩效总目标
* 注意:可以根据绩效指标自动生成,无需强制验证
*/
private String totalPerformanceGoal;
/**
* 当年绩效目标
* 注意:可以根据绩效指标自动生成,无需强制验证
*/
private String currentYearGoal;
/**
* 项目总投资-合计
*/
@NotNull(message = "项目总投资合计不能为空", groups = { AddGroup.class, EditGroup.class })
@Min(value = 0, message = "项目总投资合计不能为负数")
private BigDecimal totalInvestment;
/**
* 项目总投资-市财政资金
*/
@Min(value = 0, message = "市财政资金不能为负数")
private BigDecimal totalMunicipalFiscal;
/**
* 项目总投资-单位自筹
*/
@Min(value = 0, message = "单位自筹资金不能为负数")
private BigDecimal totalSelfFunding;
/**
* 项目总投资-其他
*/
@Min(value = 0, message = "其他资金不能为负数")
private BigDecimal totalOtherFunding;
/**
* 年度预计执行数-合计
*/
@Min(value = 0, message = "年度预计执行数合计不能为负数")
private BigDecimal currentYearTotal;
/**
* 年度预计执行数-市财政资金
*/
@Min(value = 0, message = "年度预计执行数-市财政资金不能为负数")
private BigDecimal currentYearMunicipal;
/**
* 年度预计执行数-单位自筹
*/
@Min(value = 0, message = "年度预计执行数-单位自筹不能为负数")
private BigDecimal currentYearSelf;
/**
* 年度预计执行数-其他
*/
@Min(value = 0, message = "年度预计执行数-其他不能为负数")
private BigDecimal currentYearOther;
/**
* 下年度申请资金-合计
*/
@Min(value = 0, message = "下年度申请资金合计不能为负数")
private BigDecimal nextYearTotal;
/**
* 下年度申请资金-市财政资金
*/
@Min(value = 0, message = "下年度申请资金-市财政资金不能为负数")
private BigDecimal nextYearMunicipal;
/**
* 下年度申请资金-单位自筹
*/
@Min(value = 0, message = "下年度申请资金-单位自筹不能为负数")
private BigDecimal nextYearSelf;
/**
* 下年度申请资金-其他
*/
@Min(value = 0, message = "下年度申请资金-其他不能为负数")
private BigDecimal nextYearOther;
/**
* 以后年度安排-合计
*/
@Min(value = 0, message = "以后年度安排合计不能为负数")
private BigDecimal futureYearTotal;
/**
* 以后年度安排-市财政资金
*/
@Min(value = 0, message = "以后年度安排-市财政资金不能为负数")
private BigDecimal futureYearMunicipal;
/**
* 以后年度安排-单位自筹
*/
@Min(value = 0, message = "以后年度安排-单位自筹不能为负数")
private BigDecimal futureYearSelf;
/**
* 以后年度安排-其他
*/
@Min(value = 0, message = "以后年度安排-其他不能为负数")
private BigDecimal futureYearOther;
/**
* 申请人ID
* 注意:提交时系统会自动设置,无需前端验证
*/
private Long applicantId;
/**
* 申请人姓名
* 注意:提交时系统会自动设置,无需前端验证
*/
private String applicantName;
/**
* 所属部门ID
* 注意:提交时系统会自动设置,无需前端验证
*/
private Long departmentId;
/**
* 所属部门名称
* 注意:提交时系统会自动设置,无需前端验证
*/
private String departmentName;
/**
* 审批状态(1-草稿 2-已提交 3-审批中 4-已通过 5-已拒绝 6-已退回 7-已取消)
*/
private String approvalStatus;
/**
* 提交时间
*/
private Date submitTime;
/**
* 审批完成时间
*/
private Date approveTime;
/**
* 预算年度
*/
@NotNull(message = "预算年度不能为空", groups = { AddGroup.class, EditGroup.class })
@Min(value = 2020, message = "预算年度不能小于2020年")
@Max(value = 2030, message = "预算年度不能大于2030年")
private Integer year;
/**
* 绩效指标列表
*/
private List<CwPerformanceIndicatorBo> performanceIndicators;
}
@@ -0,0 +1,174 @@
package org.dromara.cw.domain.vo;
import org.dromara.cw.domain.CwApprovalRecord;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.convert.ExcelDictConvert;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 审批记录视图对象 cw_approval_record
*
* @author ruoyi
* @date 2025-01-26
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = CwApprovalRecord.class)
public class CwApprovalRecordVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@ExcelProperty(value = "主键ID")
private Long id;
/**
* 申报ID
*/
@ExcelProperty(value = "申报ID")
private Long applicationId;
/**
* 申报编号
*/
@ExcelProperty(value = "申报编号")
private String applicationNo;
/**
* 节点编码
*/
@ExcelProperty(value = "节点编码")
private String nodeCode;
/**
* 节点名称
*/
@ExcelProperty(value = "节点名称")
private String nodeName;
/**
* 节点顺序
*/
@ExcelProperty(value = "节点顺序")
private Integer nodeOrder;
/**
* 审批人ID
*/
@ExcelProperty(value = "审批人ID")
private Long approverId;
/**
* 审批人姓名
*/
@ExcelProperty(value = "审批人姓名")
private String approverName;
/**
* 是否代理审批
*/
@ExcelProperty(value = "是否代理审批", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "sys_yes_no")
private Boolean isDelegate;
/**
* 原审批人ID
*/
private Long originalApproverId;
/**
* 原审批人姓名
*/
private String originalApproverName;
/**
* 节点状态
*/
@ExcelProperty(value = "节点状态", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "cw_approval_status")
private String nodeStatus;
/**
* 审批动作
*/
@ExcelProperty(value = "审批动作")
private String approveAction;
/**
* 审批意见
*/
@ExcelProperty(value = "审批意见")
private String approveOpinion;
/**
* 节点开始时间
*/
@ExcelProperty(value = "节点开始时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date startTime;
/**
* 节点结束时间
*/
@ExcelProperty(value = "节点结束时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date endTime;
/**
* 期望完成时间
*/
@ExcelProperty(value = "期望完成时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date expectFinishTime;
/**
* 是否超时
*/
@ExcelProperty(value = "是否超时", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "sys_yes_no")
private Boolean isOvertime;
/**
* 使用的审批配置ID
*/
private Long configId;
/**
* 审批配置快照
*/
private String configSnapshot;
/**
* 创建者
*/
private Long createBy;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 更新者
*/
private Long updateBy;
/**
* 更新时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
}
@@ -0,0 +1,129 @@
package org.dromara.cw.domain.vo;
import org.dromara.cw.domain.CwPerformanceIndicator;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.convert.ExcelDictConvert;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
import java.math.BigDecimal;
/**
* 绩效指标视图对象 cw_performance_indicator
*
* @author ruoyi
* @date 2025-01-26
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = CwPerformanceIndicator.class)
public class CwPerformanceIndicatorVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@ExcelProperty(value = "主键ID")
private Long id;
/**
* 申报ID
*/
@ExcelProperty(value = "申报ID")
private Long applicationId;
/**
* 申报编号
*/
@ExcelProperty(value = "申报编号")
private String applicationNo;
/**
* 指标类型
*/
@ExcelProperty(value = "指标类型", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "cw_performance_type")
private String indicatorType;
/**
* 指标类型名称
*/
@ExcelProperty(value = "指标类型名称")
private String indicatorTypeName;
/**
* 绩效目标名称
*/
@ExcelProperty(value = "绩效目标名称")
private String goalName;
/**
* 目标值
*/
@ExcelProperty(value = "目标值")
private String targetValue;
/**
* 分值占比(%)
*/
@ExcelProperty(value = "分值占比(%)")
private BigDecimal weightPercentage;
/**
* 详细说明
*/
@ExcelProperty(value = "详细说明")
private String description;
/**
* 计量单位
*/
@ExcelProperty(value = "计量单位")
private String unit;
/**
* 测量方法
*/
@ExcelProperty(value = "测量方法")
private String measurementMethod;
/**
* 数据来源
*/
@ExcelProperty(value = "数据来源")
private String dataSource;
/**
* 是否定量指标
*/
@ExcelProperty(value = "是否定量指标", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "sys_yes_no")
private Boolean isQuantitative;
/**
* 排序
*/
@ExcelProperty(value = "排序")
private Integer sortOrder;
/**
* 状态
*/
@ExcelProperty(value = "状态", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "sys_normal_disable")
private String status;
/**
* 创建时间
*/
@ExcelProperty(value = "创建时间")
private Date createTime;
}
@@ -0,0 +1,299 @@
package org.dromara.cw.domain.vo;
import org.dromara.cw.domain.CwProjectApplication;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.convert.ExcelDictConvert;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 项目预算申报视图对象 cw_project_application
*
* @author ruoyi
* @date 2025-01-26
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = CwProjectApplication.class)
public class CwProjectApplicationVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@ExcelProperty(value = "主键ID")
private Long id;
/**
* 申报编号
*/
@ExcelProperty(value = "申报编号")
private String applicationNo;
/**
* 项目名称
*/
@ExcelProperty(value = "项目名称")
private String projectName;
/**
* 项目负责人
*/
@ExcelProperty(value = "项目负责人")
private String principal;
/**
* 联系电话
*/
@ExcelProperty(value = "联系电话")
private String contactPhone;
/**
* 项目开始日期
*/
@ExcelProperty(value = "项目开始日期")
@JsonFormat(pattern = "yyyy-MM-dd")
private Date startDate;
/**
* 项目结束日期
*/
@ExcelProperty(value = "项目结束日期")
@JsonFormat(pattern = "yyyy-MM-dd")
private Date endDate;
/**
* 是否面向中小企业
*/
@ExcelProperty(value = "是否面向中小企业", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "sys_yes_no")
private Boolean targetSme;
/**
* 是否面向小微企业
*/
@ExcelProperty(value = "是否面向小微企业", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "sys_yes_no")
private Boolean targetMicroSme;
/**
* 是否采购进口设备
*/
@ExcelProperty(value = "是否采购进口设备", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "sys_yes_no")
private Boolean importEquipment;
/**
* 项目申请原因
*/
@ExcelProperty(value = "项目申请原因")
private String applicationReason;
/**
* 项目建设内容
*/
@ExcelProperty(value = "项目建设内容")
private String constructionContent;
/**
* 绩效总目标
*/
@ExcelProperty(value = "绩效总目标")
private String totalPerformanceGoal;
/**
* 当年绩效目标
*/
@ExcelProperty(value = "当年绩效目标")
private String currentYearGoal;
/**
* 项目总投资(万元)
*/
@ExcelProperty(value = "项目总投资(万元)")
private BigDecimal totalInvestment;
/**
* 市财政资金(万元)
*/
@ExcelProperty(value = "市财政资金(万元)")
private BigDecimal totalMunicipalFiscal;
/**
* 单位自筹资金(万元)
*/
@ExcelProperty(value = "单位自筹资金(万元)")
private BigDecimal totalSelfFunding;
/**
* 其他资金(万元)
*/
@ExcelProperty(value = "其他资金(万元)")
private BigDecimal totalOtherFunding;
/**
* 年度预计执行总数(万元)
*/
@ExcelProperty(value = "年度预计执行总数(万元)")
private BigDecimal currentYearTotal;
/**
* 年度市财政资金(万元)
*/
@ExcelProperty(value = "年度市财政资金(万元)")
private BigDecimal currentYearMunicipal;
/**
* 年度单位自筹(万元)
*/
@ExcelProperty(value = "年度单位自筹(万元)")
private BigDecimal currentYearSelf;
/**
* 年度其他资金(万元)
*/
@ExcelProperty(value = "年度其他资金(万元)")
private BigDecimal currentYearOther;
/**
* 下年度申请总数(万元)
*/
@ExcelProperty(value = "下年度申请总数(万元)")
private BigDecimal nextYearTotal;
/**
* 下年度市财政资金(万元)
*/
@ExcelProperty(value = "下年度市财政资金(万元)")
private BigDecimal nextYearMunicipal;
/**
* 下年度单位自筹(万元)
*/
@ExcelProperty(value = "下年度单位自筹(万元)")
private BigDecimal nextYearSelf;
/**
* 下年度其他资金(万元)
*/
@ExcelProperty(value = "下年度其他资金(万元)")
private BigDecimal nextYearOther;
/**
* 以后年度总数(万元)
*/
@ExcelProperty(value = "以后年度总数(万元)")
private BigDecimal futureYearTotal;
/**
* 以后年度市财政资金(万元)
*/
@ExcelProperty(value = "以后年度市财政资金(万元)")
private BigDecimal futureYearMunicipal;
/**
* 以后年度单位自筹(万元)
*/
@ExcelProperty(value = "以后年度单位自筹(万元)")
private BigDecimal futureYearSelf;
/**
* 以后年度其他资金(万元)
*/
@ExcelProperty(value = "以后年度其他资金(万元)")
private BigDecimal futureYearOther;
/**
* 申请人ID
*/
@ExcelProperty(value = "申请人ID")
private Long applicantId;
/**
* 申请人姓名
*/
@ExcelProperty(value = "申请人姓名")
private String applicantName;
/**
* 所属部门ID
*/
@ExcelProperty(value = "所属部门ID")
private Long departmentId;
/**
* 所属部门名称
*/
@ExcelProperty(value = "所属部门名称")
private String departmentName;
/**
* 审批状态
*/
@ExcelProperty(value = "审批状态", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "cw_approval_status")
private String approvalStatus;
/**
* 提交时间
*/
@ExcelProperty(value = "提交时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date submitTime;
/**
* 审批完成时间
*/
@ExcelProperty(value = "审批完成时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date approveTime;
/**
* 预算年度
*/
@ExcelProperty(value = "预算年度")
private Integer year;
/**
* 创建者
*/
private Long createBy;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 更新者
*/
private Long updateBy;
/**
* 更新时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/**
* 备注
*/
@ExcelProperty(value = "备注")
private String remark;
/**
* 绩效指标列表
*/
private java.util.List<CwPerformanceIndicatorVo> performanceIndicators;
}
@@ -0,0 +1,23 @@
package org.dromara.cw.event;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;
/**
* 审批流程创建事件
*
* @author ruoyi
* @date 2025-01-26
*/
@Getter
public class ApprovalProcessCreatedEvent extends ApplicationEvent {
private final Long applicationId;
private final String applicationNo;
public ApprovalProcessCreatedEvent(Object source, Long applicationId, String applicationNo) {
super(source);
this.applicationId = applicationId;
this.applicationNo = applicationNo;
}
}
@@ -0,0 +1,53 @@
package org.dromara.cw.listener;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.scheduling.annotation.Async;
import org.dromara.cw.event.ApprovalProcessCreatedEvent;
import org.dromara.cw.service.ICwApprovalRecordService;
/**
* 审批流程事件监听器
*
* @author ruoyi
* @date 2025-01-26
*/
@Slf4j
@RequiredArgsConstructor
@Component
public class ApprovalProcessEventListener {
private final ICwApprovalRecordService approvalRecordService;
/**
* 处理审批流程创建事件
*/
@EventListener
@Async
public void handleApprovalProcessCreated(ApprovalProcessCreatedEvent event) {
try {
log.info("开始创建审批流程:applicationId={}, applicationNo={}",
event.getApplicationId(), event.getApplicationNo());
Boolean result = approvalRecordService.createApprovalProcess(
event.getApplicationId(),
event.getApplicationNo()
);
if (result) {
log.info("审批流程创建成功:applicationId={}, applicationNo={}",
event.getApplicationId(), event.getApplicationNo());
} else {
log.error("审批流程创建失败:applicationId={}, applicationNo={}",
event.getApplicationId(), event.getApplicationNo());
}
} catch (Exception e) {
log.error("处理审批流程创建事件失败:applicationId={}, applicationNo={}, error={}",
event.getApplicationId(), event.getApplicationNo(), e.getMessage(), e);
// 异步处理,不会影响主业务流程
}
}
}
@@ -0,0 +1,99 @@
package org.dromara.cw.mapper;
import org.dromara.cw.domain.CwApprovalRecord;
import org.dromara.cw.domain.vo.CwApprovalRecordVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 审批记录Mapper接口
*
* @author ruoyi
* @date 2025-01-26
*/
public interface CwApprovalRecordMapper extends BaseMapperPlus<CwApprovalRecord, CwApprovalRecordVo> {
/**
* 根据申报ID查询审批记录列表
*
* @param applicationId 申报ID
* @return 审批记录列表
*/
List<CwApprovalRecordVo> selectByApplicationId(@Param("applicationId") Long applicationId);
/**
* 根据申报编号查询审批记录列表
*
* @param applicationNo 申报编号
* @return 审批记录列表
*/
List<CwApprovalRecordVo> selectByApplicationNo(@Param("applicationNo") String applicationNo);
/**
* 根据审批人ID查询待审批记录列表
*
* @param approverId 审批人ID
* @param nodeStatus 节点状态
* @return 审批记录列表
*/
List<CwApprovalRecordVo> selectPendingByApproverId(@Param("approverId") Long approverId, @Param("nodeStatus") String nodeStatus);
/**
* 根据审批人ID和节点编码查询审批记录
*
* @param approverId 审批人ID
* @param nodeCode 节点编码
* @param nodeStatus 节点状态
* @return 审批记录列表
*/
List<CwApprovalRecordVo> selectByApproverAndNode(@Param("approverId") Long approverId,
@Param("nodeCode") String nodeCode,
@Param("nodeStatus") String nodeStatus);
/**
* 查询所有待审批记录(超级管理员专用)
*
* @param nodeStatus 节点状态
* @return 审批记录列表
*/
List<CwApprovalRecordVo> selectAllPendingApprovals(@Param("nodeStatus") String nodeStatus);
/**
* 根据申报ID和节点编码查询审批记录
*
* @param applicationId 申报ID
* @param nodeCode 节点编码
* @return 审批记录
*/
CwApprovalRecordVo selectByApplicationAndNode(@Param("applicationId") Long applicationId,
@Param("nodeCode") String nodeCode);
/**
* 根据申报ID删除审批记录
*
* @param applicationId 申报ID
* @return 删除行数
*/
int deleteByApplicationId(@Param("applicationId") Long applicationId);
/**
* 查询当前审批节点记录
*
* @param applicationId 申报ID
* @param nodeStatus 节点状态
* @return 当前审批节点记录
*/
CwApprovalRecordVo selectCurrentApprovalNode(@Param("applicationId") Long applicationId,
@Param("nodeStatus") String nodeStatus);
/**
* 批量插入审批记录
*
* @param records 审批记录列表
* @return 插入行数
*/
int batchInsert(@Param("records") List<CwApprovalRecord> records);
}
@@ -0,0 +1,58 @@
package org.dromara.cw.mapper;
import org.dromara.cw.domain.CwPerformanceIndicator;
import org.dromara.cw.domain.vo.CwPerformanceIndicatorVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 绩效指标Mapper接口
*
* @author ruoyi
* @date 2025-01-26
*/
public interface CwPerformanceIndicatorMapper extends BaseMapperPlus<CwPerformanceIndicator, CwPerformanceIndicatorVo> {
/**
* 根据申报ID查询绩效指标列表
*
* @param applicationId 申报ID
* @return 绩效指标列表
*/
List<CwPerformanceIndicatorVo> selectByApplicationId(@Param("applicationId") Long applicationId);
/**
* 根据申报编号查询绩效指标列表
*
* @param applicationNo 申报编号
* @return 绩效指标列表
*/
List<CwPerformanceIndicatorVo> selectByApplicationNo(@Param("applicationNo") String applicationNo);
/**
* 根据申报ID删除绩效指标
*
* @param applicationId 申报ID
* @return 删除行数
*/
int deleteByApplicationId(@Param("applicationId") Long applicationId);
/**
* 根据申报编号删除绩效指标
*
* @param applicationNo 申报编号
* @return 删除行数
*/
int deleteByApplicationNo(@Param("applicationNo") String applicationNo);
/**
* 批量插入绩效指标
*
* @param indicators 绩效指标列表
* @return 插入行数
*/
int batchInsert(@Param("indicators") List<CwPerformanceIndicator> indicators);
}
@@ -0,0 +1,94 @@
package org.dromara.cw.mapper;
import org.dromara.cw.domain.CwProjectApplication;
import org.dromara.cw.domain.vo.CwProjectApplicationVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 项目预算申报Mapper接口
*
* @author ruoyi
* @date 2025-01-26
*/
public interface CwProjectApplicationMapper extends BaseMapperPlus<CwProjectApplication, CwProjectApplicationVo> {
/**
* 查询项目预算申报列表
*
* @param application 项目预算申报
* @return 项目预算申报集合
*/
List<CwProjectApplicationVo> selectApplicationList(CwProjectApplication application);
/**
* 根据申报编号查询申报信息
*
* @param applicationNo 申报编号
* @return 项目预算申报
*/
CwProjectApplicationVo selectByApplicationNo(@Param("applicationNo") String applicationNo);
/**
* 查询我的申报列表
*
* @param applicantId 申请人ID
* @param year 年度
* @return 申报列表
*/
List<CwProjectApplicationVo> selectMyApplications(@Param("applicantId") Long applicantId, @Param("year") Integer year);
/**
* 查询部门申报列表
*
* @param departmentId 部门ID
* @param year 年度
* @return 申报列表
*/
List<CwProjectApplicationVo> selectDeptApplications(@Param("departmentId") Long departmentId, @Param("year") Integer year);
/**
* 统计申报数量
*
* @param year 年度
* @param status 状态
* @return 申报数量
*/
Long countApplications(@Param("year") Integer year, @Param("status") String status);
/**
* 统计申报金额
*
* @param year 年度
* @param status 状态
* @return 申报金额
*/
java.math.BigDecimal sumApplicationAmount(@Param("year") Integer year, @Param("status") String status);
/**
* 按部门统计申报情况
*
* @param year 年度
* @return 部门申报统计
*/
List<java.util.Map<String, Object>> statisticsByDept(@Param("year") Integer year);
/**
* 按月份统计申报趋势
*
* @param year 年度
* @return 月度趋势统计
*/
List<java.util.Map<String, Object>> statisticsByMonth(@Param("year") Integer year);
/**
* 查询待我审批的申报列表
*
* @param approverId 审批人ID
* @return 待审批列表
*/
List<CwProjectApplicationVo> selectPendingApprovals(@Param("approverId") Long approverId);
}
@@ -0,0 +1,133 @@
package org.dromara.cw.service;
import org.dromara.cw.domain.bo.CwApprovalRecordBo;
import org.dromara.cw.domain.vo.CwApprovalRecordVo;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import java.util.Collection;
import java.util.List;
/**
* 审批记录Service接口
*
* @author ruoyi
* @date 2025-01-26
*/
public interface ICwApprovalRecordService {
/**
* 查询审批记录
*/
CwApprovalRecordVo queryById(Long id);
/**
* 查询审批记录列表
*/
TableDataInfo<CwApprovalRecordVo> queryPageList(CwApprovalRecordBo bo, PageQuery pageQuery);
/**
* 查询审批记录列表
*/
List<CwApprovalRecordVo> queryList(CwApprovalRecordBo bo);
/**
* 根据申报ID查询审批记录列表
*/
List<CwApprovalRecordVo> queryByApplicationId(Long applicationId);
/**
* 根据申报编号查询审批记录列表
*/
List<CwApprovalRecordVo> queryByApplicationNo(String applicationNo);
/**
* 查询当前用户待审批记录列表
*/
List<CwApprovalRecordVo> queryPendingByCurrentUser();
/**
* 查询指定审批人的待审批记录列表
*/
List<CwApprovalRecordVo> queryPendingByApproverId(Long approverId);
/**
* 查询部门待审批申报列表
* 具有部门审批权限的用户可以查看本部门用户提交的申报
*/
List<CwApprovalRecordVo> queryDeptPendingApprovals();
/**
* 查询所有待审批申报列表(超级管理员专用)
* 超级管理员可以查看所有待审批的申报
*/
List<CwApprovalRecordVo> queryAllPendingApprovals();
/**
* 根据申报ID和节点编码查询审批记录
*/
CwApprovalRecordVo queryByApplicationAndNode(Long applicationId, String nodeCode);
/**
* 新增审批记录
*/
Boolean insertByBo(CwApprovalRecordBo bo);
/**
* 修改审批记录
*/
Boolean updateByBo(CwApprovalRecordBo bo);
/**
* 校验并批量删除审批记录信息
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 根据申报ID删除审批记录
*/
Boolean deleteByApplicationId(Long applicationId);
/**
* 创建审批流程
* 当申报提交时,自动创建审批流程记录
*/
Boolean createApprovalProcess(Long applicationId, String applicationNo);
/**
* 处理审批
*
* @param applicationId 申报ID
* @param nodeCode 节点编码
* @param approveAction 审批动作(approve-通过 reject-拒绝 return-退回)
* @param approveOpinion 审批意见
* @return 是否成功
*/
Boolean processApproval(Long applicationId, String nodeCode, String approveAction, String approveOpinion);
/**
* 部门审批
*
* @param applicationId 申报ID
* @param approveAction 审批动作(approve-通过 reject-拒绝 return-退回)
* @param approveOpinion 审批意见
* @return 是否成功
*/
Boolean deptApprove(Long applicationId, String approveAction, String approveOpinion);
/**
* 查询当前审批节点
*/
CwApprovalRecordVo queryCurrentApprovalNode(Long applicationId);
/**
* 检查用户是否有审批权限
*/
Boolean checkApprovalPermission(Long applicationId, String nodeCode);
/**
* 检查用户是否有部门审批权限
*/
Boolean checkDeptApprovalPermission(Long applicationId);
}
@@ -0,0 +1,72 @@
package org.dromara.cw.service;
import org.dromara.cw.domain.bo.CwPerformanceIndicatorBo;
import org.dromara.cw.domain.vo.CwPerformanceIndicatorVo;
import java.util.Collection;
import java.util.List;
/**
* 绩效指标Service接口
*
* @author ruoyi
* @date 2025-01-26
*/
public interface ICwPerformanceIndicatorService {
/**
* 查询绩效指标
*/
CwPerformanceIndicatorVo queryById(Long id);
/**
* 查询绩效指标列表
*/
List<CwPerformanceIndicatorVo> queryList(CwPerformanceIndicatorBo bo);
/**
* 根据申报ID查询绩效指标列表
*/
List<CwPerformanceIndicatorVo> queryByApplicationId(Long applicationId);
/**
* 根据申报编号查询绩效指标列表
*/
List<CwPerformanceIndicatorVo> queryByApplicationNo(String applicationNo);
/**
* 新增绩效指标
*/
Boolean insertByBo(CwPerformanceIndicatorBo bo);
/**
* 修改绩效指标
*/
Boolean updateByBo(CwPerformanceIndicatorBo bo);
/**
* 校验并批量删除绩效指标信息
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 根据申报ID删除绩效指标
*/
Boolean deleteByApplicationId(Long applicationId);
/**
* 根据申报编号删除绩效指标
*/
Boolean deleteByApplicationNo(String applicationNo);
/**
* 批量保存绩效指标
*/
Boolean batchSave(Long applicationId, String applicationNo, List<CwPerformanceIndicatorBo> indicators);
/**
* 验证绩效指标分值总和
*/
Boolean validateWeightSum(List<CwPerformanceIndicatorBo> indicators);
}
@@ -0,0 +1,125 @@
package org.dromara.cw.service;
import org.dromara.cw.domain.vo.CwProjectApplicationVo;
import org.dromara.cw.domain.bo.CwProjectApplicationBo;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 项目预算申报Service接口
*
* @author ruoyi
* @date 2025-01-26
*/
public interface ICwProjectApplicationService {
/**
* 查询项目预算申报
*/
CwProjectApplicationVo queryById(Long id);
/**
* 查询项目预算申报列表
*/
TableDataInfo<CwProjectApplicationVo> queryPageList(CwProjectApplicationBo bo, PageQuery pageQuery);
/**
* 查询项目预算申报列表
*/
List<CwProjectApplicationVo> queryList(CwProjectApplicationBo bo);
/**
* 根据申报编号查询申报信息
*/
CwProjectApplicationVo queryByApplicationNo(String applicationNo);
/**
* 查询我的申报列表
*/
List<CwProjectApplicationVo> queryMyApplications(Long applicantId, Integer year);
/**
* 查询部门申报列表
*/
List<CwProjectApplicationVo> queryDeptApplications(Long departmentId, Integer year);
/**
* 查询待我审批的申报列表
*/
List<CwProjectApplicationVo> queryPendingApprovals(Long approverId);
/**
* 新增项目预算申报
*/
Boolean insertByBo(CwProjectApplicationBo bo);
/**
* 修改项目预算申报
*/
Boolean updateByBo(CwProjectApplicationBo bo);
/**
* 校验并批量删除项目预算申报信息
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 保存草稿
*/
Boolean saveDraft(CwProjectApplicationBo bo);
/**
* 提交申报
*/
Boolean submitApplication(Long id);
/**
* 撤回申报
*/
Boolean withdrawApplication(Long id);
/**
* 取消申报
*/
Boolean cancelApplication(Long id);
/**
* 生成申报编号
*/
String generateApplicationNo();
/**
* 统计申报数量
*/
Long countApplications(Integer year, String status);
/**
* 统计申报金额
*/
java.math.BigDecimal sumApplicationAmount(Integer year, String status);
/**
* 按部门统计申报情况
*/
List<Map<String, Object>> statisticsByDept(Integer year);
/**
* 按月份统计申报趋势
*/
List<Map<String, Object>> statisticsByMonth(Integer year);
/**
* 导出数据
*/
List<CwProjectApplicationVo> exportList(CwProjectApplicationBo bo);
/**
* 更新审批状态
*/
Boolean updateApprovalStatus(Long id, String approvalStatus);
}
@@ -0,0 +1,463 @@
package org.dromara.cw.service.impl;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.satoken.utils.LoginHelper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.dromara.cw.domain.bo.CwApprovalRecordBo;
import org.dromara.cw.domain.vo.CwApprovalRecordVo;
import org.dromara.cw.domain.vo.CwProjectApplicationVo;
import org.dromara.cw.domain.CwApprovalRecord;
import org.dromara.cw.mapper.CwApprovalRecordMapper;
import org.dromara.cw.service.ICwApprovalRecordService;
import org.dromara.cw.service.ICwProjectApplicationService;
import java.util.*;
/**
* 审批记录Service业务层处理
*
* @author ruoyi
* @date 2025-01-26
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class CwApprovalRecordServiceImpl implements ICwApprovalRecordService {
private final CwApprovalRecordMapper baseMapper;
private final ICwProjectApplicationService projectApplicationService;
/**
* 查询审批记录
*/
@Override
public CwApprovalRecordVo queryById(Long id) {
return baseMapper.selectVoById(id);
}
/**
* 查询审批记录分页列表
*/
@Override
public TableDataInfo<CwApprovalRecordVo> queryPageList(CwApprovalRecordBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<CwApprovalRecord> lqw = buildQueryWrapper(bo);
Page<CwApprovalRecordVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询审批记录列表
*/
@Override
public List<CwApprovalRecordVo> queryList(CwApprovalRecordBo bo) {
LambdaQueryWrapper<CwApprovalRecord> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<CwApprovalRecord> buildQueryWrapper(CwApprovalRecordBo bo) {
LambdaQueryWrapper<CwApprovalRecord> lqw = Wrappers.lambdaQuery();
lqw.eq(bo.getApplicationId() != null, CwApprovalRecord::getApplicationId, bo.getApplicationId());
lqw.eq(StringUtils.isNotBlank(bo.getApplicationNo()), CwApprovalRecord::getApplicationNo, bo.getApplicationNo());
lqw.eq(StringUtils.isNotBlank(bo.getNodeCode()), CwApprovalRecord::getNodeCode, bo.getNodeCode());
lqw.eq(StringUtils.isNotBlank(bo.getNodeStatus()), CwApprovalRecord::getNodeStatus, bo.getNodeStatus());
lqw.eq(bo.getApproverId() != null, CwApprovalRecord::getApproverId, bo.getApproverId());
// 添加数据权限过滤
addDataPermissionFilter(lqw);
lqw.orderByAsc(CwApprovalRecord::getNodeOrder);
lqw.orderByDesc(CwApprovalRecord::getCreateTime);
return lqw;
}
/**
* 添加数据权限过滤
* 超级管理员:查看所有数据
* 部门领导:只能查看本部门数据
* 普通用户:只能查看自己相关的审批
*/
private void addDataPermissionFilter(LambdaQueryWrapper<CwApprovalRecord> lqw) {
try {
// 获取当前登录用户信息
var loginUser = LoginHelper.getLoginUser();
if (loginUser == null) {
log.warn("未登录用户尝试访问审批记录数据");
lqw.eq(CwApprovalRecord::getId, 0L);
return;
}
Long currentUserId = loginUser.getUserId();
var rolePermissions = loginUser.getRolePermission();
log.debug("当前用户[{}]权限过滤 - 用户ID: {}, 角色权限: {}",
loginUser.getUsername(), currentUserId, rolePermissions);
// 超级管理员拥有所有权限,无需过滤
if (LoginHelper.isSuperAdmin(currentUserId)) {
log.debug("当前用户[{}]为系统超级管理员,可查看所有审批记录", currentUserId);
return;
}
// 检查角色权限
if (rolePermissions != null && rolePermissions.contains("superadmin")) {
log.debug("当前用户[{}]拥有superadmin权限,可查看所有审批记录", currentUserId);
return;
}
// 部门领导:只能查看本部门用户提交的审批
if (rolePermissions != null && rolePermissions.contains("cw:approval:dept")) {
log.debug("当前用户[{}]为部门领导,只能查看本部门审批记录", currentUserId);
Long deptId = loginUser.getDeptId();
if (deptId != null) {
// 通过关联查询,只显示本部门用户提交的申报对应的审批记录
lqw.exists("SELECT 1 FROM cw_project_application a " +
"WHERE a.id = cw_approval_record.application_id " +
"AND a.department_id = " + deptId);
} else {
log.warn("部门领导[{}]没有部门信息,拒绝数据访问", currentUserId);
lqw.eq(CwApprovalRecord::getId, 0L);
}
return;
}
// 普通用户:只能查看自己作为审批人的记录
log.debug("当前用户[{}]为普通用户,只能查看自己相关的审批记录", currentUserId);
lqw.eq(CwApprovalRecord::getApproverId, currentUserId);
} catch (Exception e) {
log.error("数据权限过滤异常: {}", e.getMessage(), e);
// 异常情况下,只允许查看自己相关的数据
try {
Long currentUserId = LoginHelper.getUserId();
if (currentUserId != null) {
log.warn("权限过滤异常,回退到只查看自己相关的审批记录,用户ID: {}", currentUserId);
lqw.eq(CwApprovalRecord::getApproverId, currentUserId);
} else {
log.error("无法获取当前用户ID,拒绝所有数据访问");
lqw.eq(CwApprovalRecord::getId, 0L);
}
} catch (Exception ex) {
log.error("权限过滤回退逻辑也失败了: {}", ex.getMessage(), ex);
lqw.eq(CwApprovalRecord::getId, 0L);
}
}
}
/**
* 根据申报ID查询审批记录列表
*/
@Override
public List<CwApprovalRecordVo> queryByApplicationId(Long applicationId) {
return baseMapper.selectByApplicationId(applicationId);
}
/**
* 根据申报编号查询审批记录列表
*/
@Override
public List<CwApprovalRecordVo> queryByApplicationNo(String applicationNo) {
return baseMapper.selectByApplicationNo(applicationNo);
}
/**
* 查询当前用户待审批记录列表
*/
@Override
public List<CwApprovalRecordVo> queryPendingByCurrentUser() {
Long currentUserId = LoginHelper.getUserId();
return queryPendingByApproverId(currentUserId);
}
/**
* 查询指定审批人的待审批记录列表
*/
@Override
public List<CwApprovalRecordVo> queryPendingByApproverId(Long approverId) {
return baseMapper.selectPendingByApproverId(approverId, "waiting");
}
/**
* 查询部门待审批申报列表
*/
@Override
public List<CwApprovalRecordVo> queryDeptPendingApprovals() {
Long currentUserId = LoginHelper.getUserId();
return baseMapper.selectByApproverAndNode(currentUserId, "dept_approve", "waiting");
}
/**
* 查询所有待审批申报列表(超级管理员专用)
*/
@Override
public List<CwApprovalRecordVo> queryAllPendingApprovals() {
return baseMapper.selectAllPendingApprovals("waiting");
}
/**
* 根据申报ID和节点编码查询审批记录
*/
@Override
public CwApprovalRecordVo queryByApplicationAndNode(Long applicationId, String nodeCode) {
return baseMapper.selectByApplicationAndNode(applicationId, nodeCode);
}
/**
* 新增审批记录
*/
@Override
public Boolean insertByBo(CwApprovalRecordBo bo) {
CwApprovalRecord add = MapstructUtils.convert(bo, CwApprovalRecord.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
}
return flag;
}
/**
* 修改审批记录
*/
@Override
public Boolean updateByBo(CwApprovalRecordBo bo) {
CwApprovalRecord update = MapstructUtils.convert(bo, CwApprovalRecord.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(CwApprovalRecord entity) {
// 可以添加数据校验逻辑
}
/**
* 批量删除审批记录
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
// 可以添加删除前的校验逻辑
}
return baseMapper.deleteBatchIds(ids) > 0;
}
/**
* 根据申报ID删除审批记录
*/
@Override
public Boolean deleteByApplicationId(Long applicationId) {
return baseMapper.deleteByApplicationId(applicationId) > 0;
}
/**
* 创建审批流程
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean createApprovalProcess(Long applicationId, String applicationNo) {
try {
// 获取申报信息
CwProjectApplicationVo application = projectApplicationService.queryById(applicationId);
if (application == null) {
throw new RuntimeException("申报记录不存在");
}
// 创建审批流程记录
List<CwApprovalRecord> records = new ArrayList<>();
// 第1节点:部门审批
CwApprovalRecord deptRecord = createApprovalRecord(applicationId, applicationNo,
"dept_approve", "部门审批", 1, "waiting");
records.add(deptRecord);
// 第2节点:财务审批(初始状态为未开始)
CwApprovalRecord financeRecord = createApprovalRecord(applicationId, applicationNo,
"finance_approve", "财务审批", 2, "pending");
records.add(financeRecord);
// 第3节点:领导审批(初始状态为未开始)
CwApprovalRecord leaderRecord = createApprovalRecord(applicationId, applicationNo,
"leader_approve", "领导审批", 3, "pending");
records.add(leaderRecord);
// 批量插入审批记录
boolean insertResult = baseMapper.batchInsert(records) > 0;
if (insertResult) {
// 审批流程创建成功后,更新申报状态为审批中
boolean updateResult = projectApplicationService.updateApprovalStatus(applicationId, "approving");
log.info("审批流程创建成功,申报状态已更新为审批中:applicationId={}, updateResult={}", applicationId, updateResult);
return updateResult;
}
return insertResult;
} catch (Exception e) {
log.error("创建审批流程失败:applicationId={}, error={}", applicationId, e.getMessage(), e);
throw new RuntimeException("创建审批流程失败:" + e.getMessage(), e);
}
}
private CwApprovalRecord createApprovalRecord(Long applicationId, String applicationNo,
String nodeCode, String nodeName,
Integer nodeOrder, String nodeStatus) {
CwApprovalRecord record = new CwApprovalRecord();
record.setApplicationId(applicationId);
record.setApplicationNo(applicationNo);
record.setNodeCode(nodeCode);
record.setNodeName(nodeName);
record.setNodeOrder(nodeOrder);
record.setNodeStatus(nodeStatus);
// 设置审批人(暂时设置为当前用户,后续可以根据配置动态设置)
Long currentUserId = LoginHelper.getUserId();
String currentUserName = LoginHelper.getUsername();
// 确保审批人ID不为空,避免验证失败
if (currentUserId == null) {
log.warn("当前用户ID为空,无法创建审批记录:nodeCode={}", nodeCode);
throw new RuntimeException("当前用户信息获取失败,无法创建审批流程");
}
record.setApproverId(currentUserId);
record.setApproverName(currentUserName != null ? currentUserName : "未知用户");
record.setIsDelegate(false);
if ("waiting".equals(nodeStatus)) {
record.setStartTime(new Date());
}
return record;
}
/**
* 处理审批
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean processApproval(Long applicationId, String nodeCode, String approveAction, String approveOpinion) {
try {
// 检查审批权限
if (!checkApprovalPermission(applicationId, nodeCode)) {
throw new RuntimeException("您没有该申报的审批权限");
}
// 查询当前审批记录
CwApprovalRecordVo currentRecord = queryByApplicationAndNode(applicationId, nodeCode);
if (currentRecord == null) {
throw new RuntimeException("审批记录不存在");
}
if (!"waiting".equals(currentRecord.getNodeStatus())) {
throw new RuntimeException("该审批节点不是等待状态,无法审批");
}
// 更新当前审批记录
CwApprovalRecordBo updateBo = new CwApprovalRecordBo();
updateBo.setId(currentRecord.getId());
updateBo.setNodeStatus("approve".equals(approveAction) ? "approved" :
"reject".equals(approveAction) ? "rejected" : "returned");
updateBo.setApproveAction(approveAction);
updateBo.setApproveOpinion(approveOpinion);
updateBo.setEndTime(new Date());
boolean updateResult = updateByBo(updateBo);
if (!updateResult) {
throw new RuntimeException("更新审批记录失败");
}
// 根据审批结果更新申报状态和流程
return updateApplicationStatusAndProcess(applicationId, nodeCode, approveAction);
} catch (Exception e) {
log.error("处理审批失败:applicationId={}, nodeCode={}, error={}",
applicationId, nodeCode, e.getMessage(), e);
throw new RuntimeException("处理审批失败:" + e.getMessage(), e);
}
}
private Boolean updateApplicationStatusAndProcess(Long applicationId, String nodeCode, String approveAction) {
if ("approve".equals(approveAction)) {
// 审批通过,启动下一个节点或完成审批
if ("dept_approve".equals(nodeCode)) {
// 部门审批通过,启动财务审批
return startNextApprovalNode(applicationId, "finance_approve");
} else if ("finance_approve".equals(nodeCode)) {
// 财务审批通过,启动领导审批
return startNextApprovalNode(applicationId, "leader_approve");
} else if ("leader_approve".equals(nodeCode)) {
// 领导审批通过,整个审批流程完成
return projectApplicationService.updateApprovalStatus(applicationId, "approved");
}
} else if ("reject".equals(approveAction)) {
// 审批拒绝,整个流程结束
return projectApplicationService.updateApprovalStatus(applicationId, "rejected");
} else if ("return".equals(approveAction)) {
// 退回修改,申报状态改为已退回
return projectApplicationService.updateApprovalStatus(applicationId, "returned");
}
return true;
}
private Boolean startNextApprovalNode(Long applicationId, String nextNodeCode) {
// 更新下一个节点为等待状态
CwApprovalRecordVo nextRecord = queryByApplicationAndNode(applicationId, nextNodeCode);
if (nextRecord != null) {
CwApprovalRecordBo updateBo = new CwApprovalRecordBo();
updateBo.setId(nextRecord.getId());
updateBo.setNodeStatus("waiting");
updateBo.setStartTime(new Date());
updateByBo(updateBo);
}
// 更新申报状态为审批中
return projectApplicationService.updateApprovalStatus(applicationId, "approving");
}
/**
* 部门审批
*/
@Override
public Boolean deptApprove(Long applicationId, String approveAction, String approveOpinion) {
return processApproval(applicationId, "dept_approve", approveAction, approveOpinion);
}
/**
* 查询当前审批节点
*/
@Override
public CwApprovalRecordVo queryCurrentApprovalNode(Long applicationId) {
return baseMapper.selectCurrentApprovalNode(applicationId, "waiting");
}
/**
* 检查用户是否有审批权限
*/
@Override
public Boolean checkApprovalPermission(Long applicationId, String nodeCode) {
Long currentUserId = LoginHelper.getUserId();
CwApprovalRecordVo record = queryByApplicationAndNode(applicationId, nodeCode);
return record != null && Objects.equals(record.getApproverId(), currentUserId);
}
/**
* 检查用户是否有部门审批权限
*/
@Override
public Boolean checkDeptApprovalPermission(Long applicationId) {
return checkApprovalPermission(applicationId, "dept_approve");
}
}
@@ -0,0 +1,240 @@
package org.dromara.cw.service.impl;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.dromara.cw.domain.bo.CwPerformanceIndicatorBo;
import org.dromara.cw.domain.vo.CwPerformanceIndicatorVo;
import org.dromara.cw.domain.CwPerformanceIndicator;
import org.dromara.cw.mapper.CwPerformanceIndicatorMapper;
import org.dromara.cw.service.ICwPerformanceIndicatorService;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
/**
* 绩效指标Service业务层处理
*
* @author ruoyi
* @date 2025-01-26
*/
@RequiredArgsConstructor
@Service
public class CwPerformanceIndicatorServiceImpl implements ICwPerformanceIndicatorService {
private final CwPerformanceIndicatorMapper baseMapper;
/**
* 查询绩效指标
*/
@Override
public CwPerformanceIndicatorVo queryById(Long id) {
return baseMapper.selectVoById(id);
}
/**
* 查询绩效指标列表
*/
@Override
public List<CwPerformanceIndicatorVo> queryList(CwPerformanceIndicatorBo bo) {
LambdaQueryWrapper<CwPerformanceIndicator> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
/**
* 根据申报ID查询绩效指标列表
*/
@Override
public List<CwPerformanceIndicatorVo> queryByApplicationId(Long applicationId) {
return baseMapper.selectByApplicationId(applicationId);
}
/**
* 根据申报编号查询绩效指标列表
*/
@Override
public List<CwPerformanceIndicatorVo> queryByApplicationNo(String applicationNo) {
return baseMapper.selectByApplicationNo(applicationNo);
}
private LambdaQueryWrapper<CwPerformanceIndicator> buildQueryWrapper(CwPerformanceIndicatorBo bo) {
LambdaQueryWrapper<CwPerformanceIndicator> lqw = Wrappers.lambdaQuery();
lqw.eq(bo.getApplicationId() != null, CwPerformanceIndicator::getApplicationId, bo.getApplicationId());
lqw.eq(StringUtils.isNotBlank(bo.getApplicationNo()), CwPerformanceIndicator::getApplicationNo, bo.getApplicationNo());
lqw.eq(StringUtils.isNotBlank(bo.getIndicatorType()), CwPerformanceIndicator::getIndicatorType, bo.getIndicatorType());
lqw.like(StringUtils.isNotBlank(bo.getGoalName()), CwPerformanceIndicator::getGoalName, bo.getGoalName());
lqw.eq(StringUtils.isNotBlank(bo.getStatus()), CwPerformanceIndicator::getStatus, bo.getStatus());
lqw.orderByAsc(CwPerformanceIndicator::getSortOrder);
lqw.orderByDesc(CwPerformanceIndicator::getCreateTime);
return lqw;
}
/**
* 新增绩效指标
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean insertByBo(CwPerformanceIndicatorBo bo) {
CwPerformanceIndicator add = MapstructUtils.convert(bo, CwPerformanceIndicator.class);
validEntityBeforeSave(add);
// 设置默认值
if (StringUtils.isBlank(add.getStatus())) {
add.setStatus("1");
}
if (add.getSortOrder() == null) {
add.setSortOrder(0);
}
if (add.getIsQuantitative() == null) {
add.setIsQuantitative(true);
}
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
}
return flag;
}
/**
* 修改绩效指标
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean updateByBo(CwPerformanceIndicatorBo bo) {
CwPerformanceIndicator update = MapstructUtils.convert(bo, CwPerformanceIndicator.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(CwPerformanceIndicator entity) {
// 验证分值占比范围
if (entity.getWeightPercentage() != null) {
if (entity.getWeightPercentage().compareTo(BigDecimal.ZERO) <= 0 ||
entity.getWeightPercentage().compareTo(new BigDecimal("100")) > 0) {
throw new RuntimeException("分值占比必须在0-100之间");
}
}
}
/**
* 批量删除绩效指标
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
// 可以添加删除前的验证逻辑
}
return baseMapper.deleteBatchIds(ids) > 0;
}
/**
* 根据申报ID删除绩效指标
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean deleteByApplicationId(Long applicationId) {
return baseMapper.deleteByApplicationId(applicationId) >= 0;
}
/**
* 根据申报编号删除绩效指标
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean deleteByApplicationNo(String applicationNo) {
return baseMapper.deleteByApplicationNo(applicationNo) >= 0;
}
/**
* 批量保存绩效指标
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean batchSave(Long applicationId, String applicationNo, List<CwPerformanceIndicatorBo> indicators) {
if (indicators == null || indicators.isEmpty()) {
return true;
}
// 验证分值总和
if (!validateWeightSum(indicators)) {
throw new RuntimeException("绩效指标分值占比总和必须等于100%");
}
// 删除原有指标
deleteByApplicationId(applicationId);
// 批量插入新指标
for (int i = 0; i < indicators.size(); i++) {
CwPerformanceIndicatorBo bo = indicators.get(i);
// 清空ID,确保新增
bo.setId(null);
// 强制设置关联字段(双重保险)
bo.setApplicationId(applicationId);
bo.setApplicationNo(applicationNo); // 即使传入的applicationNo为null也要确保设置
// 设置排序和默认值
bo.setSortOrder(i + 1);
// 设置默认值(如果为null)
if (bo.getIsQuantitative() == null) {
bo.setIsQuantitative(true);
}
if (StringUtils.isBlank(bo.getStatus())) {
bo.setStatus("1");
}
// 数据验证
if (StringUtils.isBlank(bo.getGoalName())) {
throw new RuntimeException("第" + (i + 1) + "个绩效指标的目标名称不能为空");
}
// 放宽验证:targetValue可以为空,允许后续补充
// if (StringUtils.isBlank(bo.getTargetValue())) {
// throw new RuntimeException("第" + (i + 1) + "个绩效指标的目标值不能为空");
// }
// 确保targetValue至少是空字符串而不是null
if (bo.getTargetValue() == null) {
bo.setTargetValue("");
}
if (bo.getWeightPercentage() == null || bo.getWeightPercentage().compareTo(BigDecimal.ZERO) <= 0) {
throw new RuntimeException("第" + (i + 1) + "个绩效指标的分值占比必须大于0");
}
insertByBo(bo);
}
return true;
}
/**
* 验证绩效指标分值总和
*/
@Override
public Boolean validateWeightSum(List<CwPerformanceIndicatorBo> indicators) {
if (indicators == null || indicators.isEmpty()) {
return false;
}
BigDecimal totalWeight = BigDecimal.ZERO;
for (CwPerformanceIndicatorBo indicator : indicators) {
if (indicator.getWeightPercentage() != null) {
totalWeight = totalWeight.add(indicator.getWeightPercentage());
}
}
// 验证总和是否等于100(允许0.01的误差)
return totalWeight.compareTo(new BigDecimal("100")) == 0;
}
}
@@ -0,0 +1,562 @@
package org.dromara.cw.service.impl;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.satoken.utils.LoginHelper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.dromara.cw.domain.bo.CwProjectApplicationBo;
import org.dromara.cw.domain.vo.CwProjectApplicationVo;
import org.dromara.cw.domain.vo.CwPerformanceIndicatorVo;
import org.dromara.cw.domain.CwProjectApplication;
import org.dromara.cw.mapper.CwProjectApplicationMapper;
import org.dromara.cw.service.ICwProjectApplicationService;
import org.dromara.cw.service.ICwPerformanceIndicatorService;
import org.dromara.cw.event.ApprovalProcessCreatedEvent;
import org.springframework.context.ApplicationEventPublisher;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
/**
* 项目预算申报Service业务层处理
*
* @author ruoyi
* @date 2025-01-26
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class CwProjectApplicationServiceImpl implements ICwProjectApplicationService {
private final CwProjectApplicationMapper baseMapper;
private final ICwPerformanceIndicatorService performanceIndicatorService;
private final ApplicationEventPublisher eventPublisher;
/**
* 查询项目预算申报
*/
@Override
public CwProjectApplicationVo queryById(Long id) {
CwProjectApplicationVo vo = baseMapper.selectVoById(id);
if (vo != null) {
// 获取绩效指标数据
List<CwPerformanceIndicatorVo> performanceIndicators = performanceIndicatorService.queryByApplicationId(id);
vo.setPerformanceIndicators(performanceIndicators);
}
return vo;
}
/**
* 查询项目预算申报列表
*/
@Override
public TableDataInfo<CwProjectApplicationVo> queryPageList(CwProjectApplicationBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<CwProjectApplication> lqw = buildQueryWrapper(bo);
Page<CwProjectApplicationVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询项目预算申报列表
*/
@Override
public List<CwProjectApplicationVo> queryList(CwProjectApplicationBo bo) {
LambdaQueryWrapper<CwProjectApplication> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
/**
* 根据申报编号查询申报信息
*/
@Override
public CwProjectApplicationVo queryByApplicationNo(String applicationNo) {
return baseMapper.selectByApplicationNo(applicationNo);
}
/**
* 查询我的申报列表
*/
@Override
public List<CwProjectApplicationVo> queryMyApplications(Long applicantId, Integer year) {
return baseMapper.selectMyApplications(applicantId, year);
}
/**
* 查询部门申报列表
*/
@Override
public List<CwProjectApplicationVo> queryDeptApplications(Long departmentId, Integer year) {
return baseMapper.selectDeptApplications(departmentId, year);
}
/**
* 查询待我审批的申报列表
*/
@Override
public List<CwProjectApplicationVo> queryPendingApprovals(Long approverId) {
return baseMapper.selectPendingApprovals(approverId);
}
private LambdaQueryWrapper<CwProjectApplication> buildQueryWrapper(CwProjectApplicationBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<CwProjectApplication> lqw = Wrappers.lambdaQuery();
// 添加数据权限过滤
addDataPermissionFilter(lqw);
lqw.like(StringUtils.isNotBlank(bo.getApplicationNo()), CwProjectApplication::getApplicationNo, bo.getApplicationNo());
lqw.like(StringUtils.isNotBlank(bo.getProjectName()), CwProjectApplication::getProjectName, bo.getProjectName());
lqw.eq(StringUtils.isNotBlank(bo.getPrincipal()), CwProjectApplication::getPrincipal, bo.getPrincipal());
lqw.eq(StringUtils.isNotBlank(bo.getContactPhone()), CwProjectApplication::getContactPhone, bo.getContactPhone());
lqw.eq(bo.getStartDate() != null, CwProjectApplication::getStartDate, bo.getStartDate());
lqw.eq(bo.getEndDate() != null, CwProjectApplication::getEndDate, bo.getEndDate());
lqw.eq(bo.getTargetSme() != null, CwProjectApplication::getTargetSme, bo.getTargetSme());
lqw.eq(bo.getTargetMicroSme() != null, CwProjectApplication::getTargetMicroSme, bo.getTargetMicroSme());
lqw.eq(bo.getImportEquipment() != null, CwProjectApplication::getImportEquipment, bo.getImportEquipment());
lqw.eq(bo.getApplicantId() != null, CwProjectApplication::getApplicantId, bo.getApplicantId());
lqw.like(StringUtils.isNotBlank(bo.getApplicantName()), CwProjectApplication::getApplicantName, bo.getApplicantName());
lqw.eq(bo.getDepartmentId() != null, CwProjectApplication::getDepartmentId, bo.getDepartmentId());
lqw.like(StringUtils.isNotBlank(bo.getDepartmentName()), CwProjectApplication::getDepartmentName, bo.getDepartmentName());
lqw.eq(StringUtils.isNotBlank(bo.getApprovalStatus()), CwProjectApplication::getApprovalStatus, bo.getApprovalStatus());
lqw.eq(bo.getYear() != null, CwProjectApplication::getYear, bo.getYear());
lqw.between(params.get("beginCreateTime") != null && params.get("endCreateTime") != null,
CwProjectApplication::getCreateTime, params.get("beginCreateTime"), params.get("endCreateTime"));
lqw.between(params.get("beginSubmitTime") != null && params.get("endSubmitTime") != null,
CwProjectApplication::getSubmitTime, params.get("beginSubmitTime"), params.get("endSubmitTime"));
lqw.orderByDesc(CwProjectApplication::getCreateTime);
return lqw;
}
/**
* 添加数据权限过滤
* 超级管理员:查看所有数据
* 部门领导:只能查看本部门数据
* 普通用户:只能查看自己的申报
*/
private void addDataPermissionFilter(LambdaQueryWrapper<CwProjectApplication> lqw) {
try {
// 获取当前登录用户信息
var loginUser = LoginHelper.getLoginUser();
if (loginUser == null) {
log.warn("未登录用户尝试访问项目申报数据");
lqw.eq(CwProjectApplication::getId, 0L);
return;
}
Long currentUserId = loginUser.getUserId();
var rolePermissions = loginUser.getRolePermission();
log.debug("当前用户[{}]权限过滤 - 用户ID: {}, 角色权限: {}",
loginUser.getUsername(), currentUserId, rolePermissions);
// 超级管理员拥有所有权限,无需过滤
if (LoginHelper.isSuperAdmin(currentUserId)) {
log.debug("当前用户[{}]为系统超级管理员,可查看所有项目申报", currentUserId);
return;
}
// 检查角色权限
if (rolePermissions != null && rolePermissions.contains("superadmin")) {
log.debug("当前用户[{}]拥有superadmin权限,可查看所有项目申报", currentUserId);
return;
}
// 部门领导:只能查看本部门数据
if (rolePermissions != null && rolePermissions.contains("cw:approval:dept")) {
log.debug("当前用户[{}]为部门领导,只能查看本部门项目申报", currentUserId);
Long deptId = loginUser.getDeptId();
if (deptId != null) {
lqw.eq(CwProjectApplication::getDepartmentId, deptId);
} else {
log.warn("部门领导[{}]没有部门信息,拒绝数据访问", currentUserId);
lqw.eq(CwProjectApplication::getId, 0L);
}
return;
}
// 普通用户:只能查看自己的申报
log.debug("当前用户[{}]为普通用户,只能查看自己的项目申报", currentUserId);
lqw.eq(CwProjectApplication::getApplicantId, currentUserId);
} catch (Exception e) {
log.error("数据权限过滤异常: {}", e.getMessage(), e);
// 异常情况下,只允许查看自己的数据
try {
Long currentUserId = LoginHelper.getUserId();
if (currentUserId != null) {
log.warn("权限过滤异常,回退到只查看自己的项目申报,用户ID: {}", currentUserId);
lqw.eq(CwProjectApplication::getApplicantId, currentUserId);
} else {
log.error("无法获取当前用户ID,拒绝所有数据访问");
lqw.eq(CwProjectApplication::getId, 0L);
}
} catch (Exception ex) {
log.error("权限过滤回退逻辑也失败了: {}", ex.getMessage(), ex);
lqw.eq(CwProjectApplication::getId, 0L);
}
}
}
/**
* 新增项目预算申报
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean insertByBo(CwProjectApplicationBo bo) {
CwProjectApplication add = MapstructUtils.convert(bo, CwProjectApplication.class);
validEntityBeforeSave(add);
// 生成申报编号
if (StringUtils.isBlank(add.getApplicationNo())) {
add.setApplicationNo(generateApplicationNo());
}
// 设置默认状态为草稿
if (StringUtils.isBlank(add.getApprovalStatus())) {
add.setApprovalStatus("draft");
}
// 设置申请人信息
if (add.getApplicantId() == null) {
add.setApplicantId(LoginHelper.getUserId());
}
if (StringUtils.isBlank(add.getApplicantName())) {
add.setApplicantName(LoginHelper.getUsername());
}
if (add.getDepartmentId() == null) {
add.setDepartmentId(LoginHelper.getDeptId());
}
if (StringUtils.isBlank(add.getDepartmentName())) {
add.setDepartmentName(LoginHelper.getDeptName());
}
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
bo.setApplicationNo(add.getApplicationNo()); // 回填申报编号,确保后续绩效指标保存时有正确的编号
}
return flag;
}
/**
* 修改项目预算申报
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean updateByBo(CwProjectApplicationBo bo) {
CwProjectApplication update = MapstructUtils.convert(bo, CwProjectApplication.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(CwProjectApplication entity) {
// 数据校验
if (entity.getStartDate() != null && entity.getEndDate() != null) {
if (entity.getStartDate().after(entity.getEndDate())) {
throw new RuntimeException("项目开始时间不能晚于结束时间");
}
}
// 校验申报编号唯一性
if (StringUtils.isNotBlank(entity.getApplicationNo())) {
LambdaQueryWrapper<CwProjectApplication> lqw = Wrappers.lambdaQuery();
lqw.eq(CwProjectApplication::getApplicationNo, entity.getApplicationNo());
if (entity.getId() != null) {
lqw.ne(CwProjectApplication::getId, entity.getId());
}
if (baseMapper.selectCount(lqw) > 0) {
throw new RuntimeException("申报编号已存在");
}
}
}
/**
* 批量删除项目预算申报
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
// 验证是否可以删除
for (Long id : ids) {
CwProjectApplication application = baseMapper.selectById(id);
if (application != null && !"1".equals(application.getApprovalStatus())) {
throw new RuntimeException("只能删除草稿状态的申报记录");
}
}
}
return baseMapper.deleteBatchIds(ids) > 0;
}
/**
* 保存草稿
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean saveDraft(CwProjectApplicationBo bo) {
try {
log.info("=== 开始保存草稿 ===");
log.info("项目名称: {}", bo.getProjectName());
log.info("绩效指标数据: {}", bo.getPerformanceIndicators());
log.info("绩效指标数量: {}", bo.getPerformanceIndicators() != null ? bo.getPerformanceIndicators().size() : 0);
bo.setApprovalStatus("draft"); // 草稿状态
Boolean result;
if (bo.getId() == null) {
result = insertByBo(bo);
log.info("新增草稿保存结果: {}, 生成ID: {}", result, bo.getId());
} else {
result = updateByBo(bo);
log.info("更新草稿保存结果: {}, ID: {}", result, bo.getId());
}
// 如果主申报保存失败,直接返回
if (!result) {
throw new RuntimeException("项目申报信息保存失败");
}
// 保存绩效指标
log.info("检查绩效指标保存条件: getPerformanceIndicators()={}, isEmpty()={}",
bo.getPerformanceIndicators(),
bo.getPerformanceIndicators() != null ? bo.getPerformanceIndicators().isEmpty() : "null");
if (bo.getPerformanceIndicators() != null && !bo.getPerformanceIndicators().isEmpty()) {
log.info("开始保存绩效指标,数量: {}", bo.getPerformanceIndicators().size());
try {
Boolean indicatorResult = performanceIndicatorService.batchSave(bo.getId(), bo.getApplicationNo(), bo.getPerformanceIndicators());
if (!indicatorResult) {
throw new RuntimeException("绩效指标保存失败");
}
} catch (Exception e) {
// 记录详细错误并重新抛出,确保事务回滚
log.error("保存绩效指标失败:applicationId={}, applicationNo={}, error={}",
bo.getId(), bo.getApplicationNo(), e.getMessage(), e);
throw new RuntimeException("绩效指标保存失败:" + e.getMessage(), e);
}
} else {
log.info("跳过绩效指标保存:绩效指标数据为空或null");
}
return result;
} catch (Exception e) {
// 记录详细错误日志
log.error("保存申报草稿失败:projectName={}, error={}", bo.getProjectName(), e.getMessage(), e);
// 重新抛出异常,确保事务回滚和前端能接收到错误信息
throw new RuntimeException("保存申报草稿失败:" + e.getMessage(), e);
}
}
/**
* 提交申报
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean submitApplication(Long id) {
log.info("开始提交申报,申报ID:{}", id);
CwProjectApplication application = baseMapper.selectById(id);
if (application == null) {
log.error("申报记录不存在,申报ID:{}", id);
throw new RuntimeException("申报记录不存在");
}
log.info("申报记录详情:id={}, applicationNo={}, approvalStatus={}, applicantId={}, departmentId={}",
application.getId(), application.getApplicationNo(), application.getApprovalStatus(),
application.getApplicantId(), application.getDepartmentId());
if (!"draft".equals(application.getApprovalStatus())) {
log.error("申报状态错误,当前状态:{},只能提交草稿状态的申报", application.getApprovalStatus());
throw new RuntimeException("只能提交草稿状态的申报");
}
// 检查并修复申请人信息
if (application.getApplicantId() == null) {
log.warn("项目申报记录申请人ID为空,正在修复:applicationId={}", id);
Long currentUserId = LoginHelper.getUserId();
String currentUserName = LoginHelper.getUsername();
log.info("当前登录用户信息:userId={}, userName={}", currentUserId, currentUserName);
application.setApplicantId(currentUserId);
application.setApplicantName(currentUserName);
// 如果当前用户信息也获取不到,抛出明确错误
if (application.getApplicantId() == null) {
log.error("无法获取当前登录用户信息,userId={}, userName={}", currentUserId, currentUserName);
throw new RuntimeException("无法获取当前登录用户信息,请重新登录后重试");
}
}
// 检查部门信息
if (application.getDepartmentId() == null) {
log.warn("项目申报记录部门ID为空,正在修复:applicationId={}", id);
Long currentDeptId = LoginHelper.getDeptId();
String currentDeptName = LoginHelper.getDeptName();
log.info("当前用户部门信息:deptId={}, deptName={}", currentDeptId, currentDeptName);
application.setDepartmentId(currentDeptId);
application.setDepartmentName(currentDeptName);
}
// 更新状态为已提交
application.setApprovalStatus("submitted");
application.setSubmitTime(new Date());
log.info("准备更新申报状态为已提交:applicationId={}, applicationNo={}", id, application.getApplicationNo());
boolean updateResult = false;
try {
updateResult = baseMapper.updateById(application) > 0;
log.info("申报状态更新结果:{}, applicationId={}", updateResult, id);
} catch (Exception e) {
log.error("更新申报状态失败:applicationId={}, error={}", id, e.getMessage(), e);
throw new RuntimeException("更新申报状态失败:" + e.getMessage(), e);
}
if (updateResult) {
// 发布审批流程创建事件(解决循环依赖问题)
try {
ApprovalProcessCreatedEvent event = new ApprovalProcessCreatedEvent(
this, id, application.getApplicationNo());
eventPublisher.publishEvent(event);
log.info("申报提交成功,已发布审批流程创建事件:applicationId={}, applicationNo={}",
id, application.getApplicationNo());
} catch (Exception e) {
log.error("发布审批流程创建事件失败:applicationId={}, error={}", id, e.getMessage(), e);
// 事件发布失败不影响提交,可以后续手动创建审批流程
}
} else {
log.error("申报提交失败,数据库更新返回false:applicationId={}", id);
}
log.info("申报提交流程完成:applicationId={}, updateResult={}", id, updateResult);
return updateResult;
}
/**
* 撤回申报
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean withdrawApplication(Long id) {
CwProjectApplication application = baseMapper.selectById(id);
if (application == null) {
throw new RuntimeException("申报记录不存在");
}
if (!"submitted".equals(application.getApprovalStatus())) {
throw new RuntimeException("只能撤回已提交状态的申报");
}
// 更新状态为草稿
application.setApprovalStatus("draft");
application.setSubmitTime(null);
return baseMapper.updateById(application) > 0;
}
/**
* 取消申报
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean cancelApplication(Long id) {
CwProjectApplication application = baseMapper.selectById(id);
if (application == null) {
throw new RuntimeException("申报记录不存在");
}
// 更新状态为已取消
application.setApprovalStatus("cancelled");
return baseMapper.updateById(application) > 0;
}
/**
* 更新审批状态
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean updateApprovalStatus(Long id, String approvalStatus) {
CwProjectApplication application = baseMapper.selectById(id);
if (application == null) {
throw new RuntimeException("申报记录不存在");
}
application.setApprovalStatus(approvalStatus);
// 如果是最终通过或拒绝状态,设置审批完成时间
if ("approved".equals(approvalStatus) || "rejected".equals(approvalStatus)) {
application.setApproveTime(new Date());
}
return baseMapper.updateById(application) > 0;
}
/**
* 生成申报编号
*/
@Override
public String generateApplicationNo() {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
String dateStr = dateFormat.format(new Date());
// 生成4位随机数
int randomNum = ThreadLocalRandom.current().nextInt(1000, 9999);
return "YS" + dateStr + randomNum;
}
/**
* 统计申报数量
*/
@Override
public Long countApplications(Integer year, String status) {
return baseMapper.countApplications(year, status);
}
/**
* 统计申报金额
*/
@Override
public java.math.BigDecimal sumApplicationAmount(Integer year, String status) {
return baseMapper.sumApplicationAmount(year, status);
}
/**
* 按部门统计申报情况
*/
@Override
public List<Map<String, Object>> statisticsByDept(Integer year) {
return baseMapper.statisticsByDept(year);
}
/**
* 按月份统计申报趋势
*/
@Override
public List<Map<String, Object>> statisticsByMonth(Integer year) {
return baseMapper.statisticsByMonth(year);
}
/**
* 导出数据
*/
@Override
public List<CwProjectApplicationVo> exportList(CwProjectApplicationBo bo) {
return queryList(bo);
}
}
@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.cw.mapper.CwApprovalRecordMapper">
<resultMap type="org.dromara.cw.domain.CwApprovalRecord" id="CwApprovalRecordResult">
<result property="id" column="id" />
<result property="applicationId" column="application_id" />
<result property="applicationNo" column="application_no" />
<result property="nodeCode" column="node_code" />
<result property="nodeName" column="node_name" />
<result property="nodeOrder" column="node_order" />
<result property="approverId" column="approver_id" />
<result property="approverName" column="approver_name" />
<result property="isDelegate" column="is_delegate" />
<result property="originalApproverId" column="original_approver_id" />
<result property="originalApproverName" column="original_approver_name"/>
<result property="nodeStatus" column="node_status" />
<result property="approveAction" column="approve_action" />
<result property="approveOpinion" column="approve_opinion" />
<result property="startTime" column="start_time" />
<result property="endTime" column="end_time" />
<result property="expectFinishTime" column="expect_finish_time" />
<result property="isOvertime" column="is_overtime" />
<result property="configId" column="config_id" />
<result property="configSnapshot" column="config_snapshot" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="createDept" column="create_dept" />
<result property="deleted" column="deleted" />
<result property="tenantId" column="tenant_id" />
</resultMap>
<sql id="selectCwApprovalRecordVo">
select id, application_id, application_no, node_code, node_name, node_order,
approver_id, approver_name, is_delegate, original_approver_id, original_approver_name,
node_status, approve_action, approve_opinion, start_time, end_time, expect_finish_time,
is_overtime, config_id, config_snapshot, create_by, create_time, update_by, update_time,
create_dept, deleted, tenant_id
from cw_approval_record
</sql>
<select id="selectByApplicationId" parameterType="Long" resultType="org.dromara.cw.domain.vo.CwApprovalRecordVo">
<include refid="selectCwApprovalRecordVo"/>
where application_id = #{applicationId} and deleted = 0
order by node_order asc, create_time asc
</select>
<select id="selectByApplicationNo" parameterType="String" resultType="org.dromara.cw.domain.vo.CwApprovalRecordVo">
<include refid="selectCwApprovalRecordVo"/>
where application_no = #{applicationNo} and deleted = 0
order by node_order asc, create_time asc
</select>
<select id="selectPendingByApproverId" resultType="org.dromara.cw.domain.vo.CwApprovalRecordVo">
<include refid="selectCwApprovalRecordVo"/>
where approver_id = #{approverId}
and node_status = #{nodeStatus}
and deleted = 0
order by start_time desc
</select>
<select id="selectByApproverAndNode" resultType="org.dromara.cw.domain.vo.CwApprovalRecordVo">
<include refid="selectCwApprovalRecordVo"/>
where approver_id = #{approverId}
and node_code = #{nodeCode}
and node_status = #{nodeStatus}
and deleted = 0
order by create_time desc
</select>
<select id="selectAllPendingApprovals" resultType="org.dromara.cw.domain.vo.CwApprovalRecordVo">
<include refid="selectCwApprovalRecordVo"/>
where node_status = #{nodeStatus}
and deleted = 0
order by create_time desc
</select>
<select id="selectByApplicationAndNode" resultType="org.dromara.cw.domain.vo.CwApprovalRecordVo">
<include refid="selectCwApprovalRecordVo"/>
where application_id = #{applicationId}
and node_code = #{nodeCode}
and deleted = 0
order by create_time desc
limit 1
</select>
<select id="selectCurrentApprovalNode" resultType="org.dromara.cw.domain.vo.CwApprovalRecordVo">
<include refid="selectCwApprovalRecordVo"/>
where application_id = #{applicationId}
and node_status = #{nodeStatus}
and deleted = 0
order by node_order asc
limit 1
</select>
<delete id="deleteByApplicationId" parameterType="Long">
update cw_approval_record set deleted = 1
where application_id = #{applicationId}
</delete>
<insert id="batchInsert" parameterType="java.util.List">
insert into cw_approval_record(
id, application_id, application_no, node_code, node_name, node_order,
approver_id, approver_name, is_delegate, original_approver_id, original_approver_name,
node_status, approve_action, approve_opinion, start_time, end_time, expect_finish_time,
is_overtime, config_id, config_snapshot, create_by, create_time, update_by, update_time,
create_dept, deleted, tenant_id
) values
<foreach collection="records" item="record" separator=",">
(
#{record.id}, #{record.applicationId}, #{record.applicationNo}, #{record.nodeCode},
#{record.nodeName}, #{record.nodeOrder}, #{record.approverId}, #{record.approverName},
#{record.isDelegate}, #{record.originalApproverId}, #{record.originalApproverName},
#{record.nodeStatus}, #{record.approveAction}, #{record.approveOpinion},
#{record.startTime}, #{record.endTime}, #{record.expectFinishTime}, #{record.isOvertime},
#{record.configId}, #{record.configSnapshot}, #{record.createBy}, #{record.createTime},
#{record.updateBy}, #{record.updateTime}, #{record.createDept}, #{record.deleted}, #{record.tenantId}
)
</foreach>
</insert>
</mapper>
@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.cw.mapper.CwPerformanceIndicatorMapper">
<resultMap type="org.dromara.cw.domain.CwPerformanceIndicator" id="CwPerformanceIndicatorResult">
<result property="id" column="id" />
<result property="applicationId" column="application_id" />
<result property="applicationNo" column="application_no" />
<result property="indicatorType" column="indicator_type" />
<result property="indicatorTypeName" column="indicator_type_name" />
<result property="goalName" column="goal_name" />
<result property="targetValue" column="target_value" />
<result property="weightPercentage" column="weight_percentage" />
<result property="description" column="description" />
<result property="unit" column="unit" />
<result property="measurementMethod" column="measurement_method" />
<result property="dataSource" column="data_source" />
<result property="isQuantitative" column="is_quantitative" />
<result property="sortOrder" column="sort_order" />
<result property="status" column="status" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="createDept" column="create_dept" />
<result property="deleted" column="deleted" />
<result property="tenantId" column="tenant_id" />
</resultMap>
<sql id="selectCwPerformanceIndicatorVo">
select id, application_id, application_no, indicator_type, indicator_type_name, goal_name, target_value, weight_percentage, description, unit, measurement_method, data_source, is_quantitative, sort_order, status, create_by, create_time, update_by, update_time, create_dept, deleted, tenant_id from cw_performance_indicator
</sql>
<select id="selectByApplicationId" parameterType="Long" resultType="org.dromara.cw.domain.vo.CwPerformanceIndicatorVo">
<include refid="selectCwPerformanceIndicatorVo"/>
where application_id = #{applicationId} and deleted = 0
order by sort_order asc, create_time desc
</select>
<select id="selectByApplicationNo" parameterType="String" resultType="org.dromara.cw.domain.vo.CwPerformanceIndicatorVo">
<include refid="selectCwPerformanceIndicatorVo"/>
where application_no = #{applicationNo} and deleted = 0
order by sort_order asc, create_time desc
</select>
<delete id="deleteByApplicationId" parameterType="Long">
update cw_performance_indicator set deleted = 1
where application_id = #{applicationId}
</delete>
<delete id="deleteByApplicationNo" parameterType="String">
update cw_performance_indicator set deleted = 1
where application_no = #{applicationNo}
</delete>
<insert id="batchInsert" parameterType="java.util.List">
insert into cw_performance_indicator(
id, application_id, application_no, indicator_type, indicator_type_name,
goal_name, target_value, weight_percentage, description, unit,
measurement_method, data_source, is_quantitative, sort_order, status,
create_by, create_time, update_by, update_time, create_dept, tenant_id
) values
<foreach collection="indicators" item="item" separator=",">
(
#{item.id}, #{item.applicationId}, #{item.applicationNo}, #{item.indicatorType}, #{item.indicatorTypeName},
#{item.goalName}, #{item.targetValue}, #{item.weightPercentage}, #{item.description}, #{item.unit},
#{item.measurementMethod}, #{item.dataSource}, #{item.isQuantitative}, #{item.sortOrder}, #{item.status},
#{item.createBy}, #{item.createTime}, #{item.updateBy}, #{item.updateTime}, #{item.createDept}, #{item.tenantId}
)
</foreach>
</insert>
</mapper>
@@ -0,0 +1,169 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.cw.mapper.CwProjectApplicationMapper">
<resultMap type="org.dromara.cw.domain.vo.CwProjectApplicationVo" id="CwProjectApplicationResult">
<result property="id" column="id" />
<result property="applicationNo" column="application_no" />
<result property="projectName" column="project_name" />
<result property="principal" column="principal" />
<result property="contactPhone" column="contact_phone" />
<result property="startDate" column="start_date" />
<result property="endDate" column="end_date" />
<result property="targetSme" column="target_sme" />
<result property="targetMicroSme" column="target_micro_sme" />
<result property="importEquipment" column="import_equipment" />
<result property="applicationReason" column="application_reason" />
<result property="constructionContent" column="construction_content" />
<result property="totalPerformanceGoal" column="total_performance_goal" />
<result property="currentYearGoal" column="current_year_goal" />
<result property="totalInvestment" column="total_investment" />
<result property="totalMunicipalFiscal" column="total_municipal_fiscal" />
<result property="totalSelfFunding" column="total_self_funding" />
<result property="totalOtherFunding" column="total_other_funding" />
<result property="currentYearTotal" column="current_year_total" />
<result property="currentYearMunicipal" column="current_year_municipal" />
<result property="currentYearSelf" column="current_year_self" />
<result property="currentYearOther" column="current_year_other" />
<result property="nextYearTotal" column="next_year_total" />
<result property="nextYearMunicipal" column="next_year_municipal" />
<result property="nextYearSelf" column="next_year_self" />
<result property="nextYearOther" column="next_year_other" />
<result property="futureYearTotal" column="future_year_total" />
<result property="futureYearMunicipal" column="future_year_municipal" />
<result property="futureYearSelf" column="future_year_self" />
<result property="futureYearOther" column="future_year_other" />
<result property="applicantId" column="applicant_id" />
<result property="applicantName" column="applicant_name" />
<result property="departmentId" column="department_id" />
<result property="departmentName" column="department_name" />
<result property="approvalStatus" column="approval_status" />
<result property="submitTime" column="submit_time" />
<result property="approveTime" column="approve_time" />
<result property="year" column="year" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
</resultMap>
<sql id="selectCwProjectApplicationVo">
select
a.id, a.application_no, a.project_name, a.principal, a.contact_phone,
a.start_date, a.end_date, a.target_sme, a.target_micro_sme, a.import_equipment,
a.application_reason, a.construction_content, a.total_performance_goal, a.current_year_goal,
a.total_investment, a.total_municipal_fiscal, a.total_self_funding, a.total_other_funding,
a.current_year_total, a.current_year_municipal, a.current_year_self, a.current_year_other,
a.next_year_total, a.next_year_municipal, a.next_year_self, a.next_year_other,
a.future_year_total, a.future_year_municipal, a.future_year_self, a.future_year_other,
a.applicant_id, a.applicant_name, a.department_id, a.department_name,
a.approval_status, a.submit_time, a.approve_time, a.year,
a.create_by, a.create_time, a.update_by, a.update_time, a.remark,
u.nick_name as applicant_user_name,
d.dept_name as dept_name
from cw_project_application a
left join sys_user u on a.applicant_id = u.user_id
left join sys_dept d on a.department_id = d.dept_id
</sql>
<select id="selectApplicationList" parameterType="org.dromara.cw.domain.CwProjectApplication" resultMap="CwProjectApplicationResult">
<include refid="selectCwProjectApplicationVo"/>
<where>
a.deleted = 0
<if test="applicationNo != null and applicationNo != ''"> and a.application_no like concat('%', #{applicationNo}, '%')</if>
<if test="projectName != null and projectName != ''"> and a.project_name like concat('%', #{projectName}, '%')</if>
<if test="principal != null and principal != ''"> and a.principal like concat('%', #{principal}, '%')</if>
<if test="applicantName != null and applicantName != ''"> and a.applicant_name like concat('%', #{applicantName}, '%')</if>
<if test="departmentId != null"> and a.department_id = #{departmentId}</if>
<if test="departmentName != null and departmentName != ''"> and a.department_name like concat('%', #{departmentName}, '%')</if>
<if test="approvalStatus != null and approvalStatus != ''"> and a.approval_status = #{approvalStatus}</if>
<if test="year != null"> and a.year = #{year}</if>
<if test="params.beginCreateTime != null and params.beginCreateTime != ''"> and a.create_time &gt;= #{params.beginCreateTime}</if>
<if test="params.endCreateTime != null and params.endCreateTime != ''"> and a.create_time &lt;= #{params.endCreateTime}</if>
<if test="params.beginSubmitTime != null and params.beginSubmitTime != ''"> and a.submit_time &gt;= #{params.beginSubmitTime}</if>
<if test="params.endSubmitTime != null and params.endSubmitTime != ''"> and a.submit_time &lt;= #{params.endSubmitTime}</if>
</where>
order by a.create_time desc
</select>
<select id="selectByApplicationNo" resultMap="CwProjectApplicationResult">
<include refid="selectCwProjectApplicationVo"/>
where a.application_no = #{applicationNo} and a.deleted = 0
</select>
<select id="selectMyApplications" resultMap="CwProjectApplicationResult">
<include refid="selectCwProjectApplicationVo"/>
where a.applicant_id = #{applicantId} and a.deleted = 0
<if test="year != null"> and a.year = #{year}</if>
order by a.create_time desc
</select>
<select id="selectDeptApplications" resultMap="CwProjectApplicationResult">
<include refid="selectCwProjectApplicationVo"/>
where a.department_id = #{departmentId} and a.deleted = 0
<if test="year != null"> and a.year = #{year}</if>
order by a.create_time desc
</select>
<select id="countApplications" resultType="java.lang.Long">
select count(*) from cw_project_application
where deleted = 0
<if test="year != null"> and year = #{year}</if>
<if test="status != null and status != ''"> and approval_status = #{status}</if>
</select>
<select id="sumApplicationAmount" resultType="java.math.BigDecimal">
select COALESCE(sum(total_investment), 0) from cw_project_application
where deleted = 0
<if test="year != null"> and year = #{year}</if>
<if test="status != null and status != ''"> and approval_status = #{status}</if>
</select>
<select id="statisticsByDept" resultType="java.util.Map">
select
d.dept_name,
a.department_id,
count(*) as application_count,
COALESCE(sum(a.total_investment), 0) as total_amount,
count(case when a.approval_status = 'approved' then 1 end) as approved_count,
COALESCE(sum(case when a.approval_status = 'approved' then a.total_investment else 0 end), 0) as approved_amount,
count(case when a.approval_status in ('submitted', 'approving') then 1 end) as approving_count,
count(case when a.approval_status = 'rejected' then 1 end) as rejected_count
from cw_project_application a
left join sys_dept d on a.department_id = d.dept_id
where a.deleted = 0
<if test="year != null"> and a.year = #{year}</if>
group by a.department_id, d.dept_name
order by total_amount desc
</select>
<select id="statisticsByMonth" resultType="java.util.Map">
select
DATE_FORMAT(submit_time, '%Y-%m') as month,
count(*) as application_count,
COALESCE(sum(total_investment), 0) as total_amount,
count(case when approval_status = 'approved' then 1 end) as approved_count,
COALESCE(sum(case when approval_status = 'approved' then total_investment else 0 end), 0) as approved_amount,
count(case when approval_status in ('submitted', 'approving') then 1 end) as approving_count,
count(case when approval_status = 'rejected' then 1 end) as rejected_count
from cw_project_application
where deleted = 0 and submit_time is not null
<if test="year != null"> and year = #{year}</if>
group by DATE_FORMAT(submit_time, '%Y-%m')
order by month
</select>
<select id="selectPendingApprovals" resultMap="CwProjectApplicationResult">
<include refid="selectCwProjectApplicationVo"/>
inner join cw_approval_record r on a.id = r.application_id
where a.deleted = 0
and r.deleted = 0
and r.approver_id = #{approverId}
and r.node_status = 'waiting'
order by a.submit_time asc
</select>
</mapper>
@@ -15,6 +15,7 @@ import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.encrypt.annotation.ApiEncrypt;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.hq.domain.vo.HqHallInfoVo;
@@ -39,6 +40,7 @@ public class HqHallInfoController extends BaseController {
/**
* 查询报告厅信息列表
*/
@ApiEncrypt(response = true)
@SaCheckPermission("hq:hallInfo:list")
@GetMapping("/list")
public TableDataInfo<HqHallInfoVo> list(HqHallInfoBo bo, PageQuery pageQuery) {
@@ -48,6 +50,7 @@ public class HqHallInfoController extends BaseController {
/**
* 导出报告厅信息列表
*/
@ApiEncrypt(response = true)
@SaCheckPermission("hq:hallInfo:export")
@Log(title = "报告厅信息", businessType = BusinessType.EXPORT)
@PostMapping("/export")
@@ -61,16 +64,17 @@ public class HqHallInfoController extends BaseController {
*
* @param id 主键
*/
@ApiEncrypt(response = true)
@SaCheckPermission("hq:hallInfo:query")
@GetMapping("/{id}")
public R<HqHallInfoVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
public R<HqHallInfoVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
return R.ok(hqHallInfoService.queryById(id));
}
/**
* 新增报告厅信息
*/
@ApiEncrypt(response = true)
@SaCheckPermission("hq:hallInfo:add")
@Log(title = "报告厅信息", businessType = BusinessType.INSERT)
@RepeatSubmit()
@@ -82,6 +86,7 @@ public class HqHallInfoController extends BaseController {
/**
* 修改报告厅信息
*/
@ApiEncrypt(response = true)
@SaCheckPermission("hq:hallInfo:edit")
@Log(title = "报告厅信息", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@@ -95,11 +100,11 @@ public class HqHallInfoController extends BaseController {
*
* @param ids 主键串
*/
@ApiEncrypt(response = true)
@SaCheckPermission("hq:hallInfo:remove")
@Log(title = "报告厅信息", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
return toAjax(hqHallInfoService.deleteWithValidByIds(List.of(ids), true));
}
}
@@ -15,6 +15,7 @@ import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.encrypt.annotation.ApiEncrypt;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.hq.domain.vo.HqHallReservationVo;
@@ -42,6 +43,7 @@ public class HqHallReservationController extends BaseController {
/**
* 查询报告厅预约列表
*/
@ApiEncrypt(response = true)
@SaCheckPermission("hq:hallReservation:list")
@GetMapping("/list")
public TableDataInfo<HqHallReservationVo> list(HqHallReservationBo bo, PageQuery pageQuery) {
@@ -51,6 +53,7 @@ public class HqHallReservationController extends BaseController {
/**
* 导出报告厅预约列表
*/
@ApiEncrypt(response = true)
@SaCheckPermission("hq:hallReservation:export")
@Log(title = "报告厅预约", businessType = BusinessType.EXPORT)
@PostMapping("/export")
@@ -64,6 +67,7 @@ public class HqHallReservationController extends BaseController {
*
* @param id 主键
*/
@ApiEncrypt(response = true)
@SaCheckPermission("hq:hallReservation:query")
@GetMapping("/{id}")
public R<HqHallReservationVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
@@ -73,6 +77,7 @@ public class HqHallReservationController extends BaseController {
/**
* 新增报告厅预约
*/
@ApiEncrypt(response = true)
@SaCheckPermission("hq:hallReservation:add")
@Log(title = "报告厅预约", businessType = BusinessType.INSERT)
@RepeatSubmit()
@@ -84,6 +89,7 @@ public class HqHallReservationController extends BaseController {
/**
* 修改报告厅预约
*/
@ApiEncrypt(response = true)
@SaCheckPermission("hq:hallReservation:edit")
@Log(title = "报告厅预约", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@@ -97,6 +103,7 @@ public class HqHallReservationController extends BaseController {
*
* @param ids 主键串
*/
@ApiEncrypt(response = true)
@SaCheckPermission("hq:hallReservation:remove")
@Log(title = "报告厅预约", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
@@ -107,6 +114,7 @@ public class HqHallReservationController extends BaseController {
/**
* 审核报告厅预约(批准/拒绝)
*/
@ApiEncrypt(response = true)
@SaCheckPermission("hq:hallReservation:audit")
@Log(title = "报告厅预约审核", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@@ -118,6 +126,7 @@ public class HqHallReservationController extends BaseController {
/**
* 检查报告厅时间冲突
*/
@ApiEncrypt(response = true)
@SaCheckPermission("hq:hallReservation:query")
@PostMapping("/checkTimeConflict")
public R<HqHallTimeCheckVo> checkTimeConflict(@Validated @RequestBody HqHallTimeCheckBo timeCheckBo) {
@@ -42,7 +42,7 @@ public class HqPmRepairAttachmentController extends BaseController {
/**
* 查询报修附件列表
*/
// @SaCheckPermission("pm:attachment:list")
@SaCheckPermission("pm:attachment:list")
@GetMapping("/list")
public TableDataInfo<HqPmRepairAttachmentVo> list(HqPmRepairAttachmentBo bo, PageQuery pageQuery) {
return attachmentService.queryPageList(bo, pageQuery);
@@ -51,7 +51,7 @@ public class HqPmRepairAttachmentController extends BaseController {
/**
* 根据工单ID获取附件列表
*/
// @SaCheckPermission("pm:attachment:list")
@SaCheckPermission("pm:attachment:list")
@GetMapping("/order/{orderId}")
public R<List<HqPmRepairAttachmentVo>> getByOrderId(@PathVariable("orderId") Long orderId) {
List<HqPmRepairAttachmentVo> list = attachmentService.queryByOrderId(orderId);
@@ -61,7 +61,7 @@ public class HqPmRepairAttachmentController extends BaseController {
/**
* 获取报修附件详细信息
*/
// @SaCheckPermission("pm:attachment:query")
@SaCheckPermission("pm:attachment:query")
@GetMapping("/{id}")
public R<HqPmRepairAttachmentVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
return R.ok(attachmentService.queryById(id));
@@ -70,7 +70,7 @@ public class HqPmRepairAttachmentController extends BaseController {
/**
* 新增报修附件
*/
// @SaCheckPermission("pm:attachment:add")
@SaCheckPermission("pm:attachment:add")
@Log(title = "报修附件", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping
@@ -81,7 +81,7 @@ public class HqPmRepairAttachmentController extends BaseController {
/**
* 修改报修附件
*/
// @SaCheckPermission("pm:attachment:edit")
@SaCheckPermission("pm:attachment:edit")
@Log(title = "报修附件", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping
@@ -93,7 +93,7 @@ public class HqPmRepairAttachmentController extends BaseController {
* 删除报修附件
* //
*/
// @SaCheckPermission("pm:attachment:remove")
@SaCheckPermission("pm:attachment:remove")
@Log(title = "报修附件", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
@@ -103,7 +103,7 @@ public class HqPmRepairAttachmentController extends BaseController {
/**
* 上传单个附件文件
*/
// @SaCheckPermission("pm:attachment:upload")
@SaCheckPermission("pm:attachment:upload")
@Log(title = "上传报修附件", businessType = BusinessType.INSERT)
@PostMapping("/upload")
public R<HqPmRepairAttachmentVo> uploadFile(
@@ -127,7 +127,7 @@ public class HqPmRepairAttachmentController extends BaseController {
/**
* 批量上传附件文件
*/
// @SaCheckPermission("pm:attachment:upload")
@SaCheckPermission("pm:attachment:upload")
@Log(title = "批量上传报修附件", businessType = BusinessType.INSERT)
@PostMapping("/upload/batch")
public R<List<HqPmRepairAttachmentVo>> uploadFiles(
@@ -152,7 +152,7 @@ public class HqPmRepairAttachmentController extends BaseController {
/**
* 删除附件文件(包括OSS文件和数据库记录)
*/
// @SaCheckPermission("pm:attachment:remove")
@SaCheckPermission("pm:attachment:remove")
@Log(title = "删除报修附件文件", businessType = BusinessType.DELETE)
@DeleteMapping("/file/{id}")
public R<Void> deleteAttachment(@PathVariable("id") Long id) {
@@ -162,7 +162,7 @@ public class HqPmRepairAttachmentController extends BaseController {
/**
* 下载附件文件
*/
// @SaCheckPermission("pm:attachment:download")
@SaCheckPermission("pm:attachment:download")
@GetMapping("/download/{id}")
public void downloadFile(@PathVariable("id") Long id, HttpServletResponse response) {
HqPmRepairAttachmentVo attachment = attachmentService.queryById(id);
@@ -186,7 +186,7 @@ public class HqPmRepairAttachmentController extends BaseController {
/**
* 更新附件的工单ID(用于新增工单时关联已上传的附件)
*/
// @SaCheckPermission("pm:attachment:edit")
@SaCheckPermission("pm:attachment:edit")
@Log(title = "更新附件工单ID", businessType = BusinessType.UPDATE)
@PutMapping("/{attachmentId}/order/{orderId}")
public R<Void> updateAttachmentOrderId(@PathVariable("attachmentId") Long attachmentId,
@@ -197,7 +197,7 @@ public class HqPmRepairAttachmentController extends BaseController {
/**
* 批量更新附件的工单ID(用于新增工单时关联已上传的附件)
*/
// @SaCheckPermission("pm:attachment:edit")
@SaCheckPermission("pm:attachment:edit")
@Log(title = "批量更新附件工单ID", businessType = BusinessType.UPDATE)
@PutMapping("/batch-update-order")
public R<Void> batchUpdateAttachmentOrderId(@RequestBody BatchUpdateOrderIdRequest request) {
@@ -135,7 +135,7 @@ public class HqSatSurveyController extends BaseController {
*
* @param surveyId 问卷ID
*/
// @SaCheckPermission("hq:satSurvey:question")
@SaCheckPermission("hq:satSurvey:question")
@GetMapping("/{surveyId}/questions")
public R<List<HqSatSurveyQuestionVo>> getSurveyQuestions(
@NotNull(message = "问卷ID不能为空") @PathVariable Long surveyId) {
@@ -22,6 +22,7 @@ import org.dromara.hq.domain.vo.HqPmRepairAttachmentVo;
import org.dromara.hq.mapper.HqPmRepairAttachmentMapper;
import org.dromara.hq.mapper.HqPmRepairOrderMapper;
import org.dromara.hq.service.IHqPmRepairAttachmentService;
import org.dromara.system.service.ISysOssService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
@@ -44,6 +45,7 @@ public class HqPmRepairAttachmentServiceImpl implements IHqPmRepairAttachmentSer
private final HqPmRepairAttachmentMapper baseMapper;
private final HqPmRepairOrderMapper repairOrderMapper;
private final ISysOssService sysOssService;
/**
* 查询报修附件
@@ -160,11 +162,31 @@ public class HqPmRepairAttachmentServiceImpl implements IHqPmRepairAttachmentSer
for (HqPmRepairAttachmentVo attachment : attachments) {
try {
if (StringUtils.isNotBlank(attachment.getFilePath())) {
OssFactory.instance().delete(attachment.getFilePath());
// 从文件路径中提取ossId
String filePath = attachment.getFilePath();
if (filePath.contains("/resource/oss/download/")) {
String ossIdStr = filePath.substring(filePath.lastIndexOf("/") + 1);
try {
Long ossId = Long.parseLong(ossIdStr);
sysOssService.deleteWithValidByIds(List.of(ossId), true);
} catch (NumberFormatException e) {
log.warn("无法从文件路径中解析ossId: {}", filePath);
}
}
}
// 删除缩略图
if (StringUtils.isNotBlank(attachment.getThumbnailPath())) {
OssFactory.instance().delete(attachment.getThumbnailPath());
// 从缩略图路径中提取ossId
String thumbnailPath = attachment.getThumbnailPath();
if (thumbnailPath.contains("/resource/oss/download/")) {
String ossIdStr = thumbnailPath.substring(thumbnailPath.lastIndexOf("/") + 1);
try {
Long ossId = Long.parseLong(ossIdStr);
sysOssService.deleteWithValidByIds(List.of(ossId), true);
} catch (NumberFormatException e) {
log.warn("无法从缩略图路径中解析ossId: {}", thumbnailPath);
}
}
}
} catch (Exception e) {
log.warn("删除OSS文件失败: {}", e.getMessage());
@@ -196,14 +218,15 @@ public class HqPmRepairAttachmentServiceImpl implements IHqPmRepairAttachmentSer
String contentType = file.getContentType();
long fileSize = file.getSize();
// 上传文件到OSS
UploadResult uploadResult = OssFactory.instance().uploadSuffix(file.getBytes(), fileExtension, contentType);
// 通过SysOssService上传文件,获取ossId
var sysOssVo = sysOssService.upload(file);
// 构建附件信息
HqPmRepairAttachmentBo bo = new HqPmRepairAttachmentBo();
bo.setOrderId(orderId);
bo.setFileName(originalFilename);
bo.setFilePath(uploadResult.getUrl());
// bo.setFilePath("/resource/oss/download/" + sysOssVo.getOssId());
bo.setFilePath(sysOssVo.getOssId().toString());
bo.setFileSize(fileSize);
bo.setFileType(getFileType(contentType));
bo.setFileExtension(fileExtension);
@@ -220,7 +243,7 @@ public class HqPmRepairAttachmentServiceImpl implements IHqPmRepairAttachmentSer
} else {
throw new RuntimeException("保存附件信息失败");
}
} catch (IOException e) {
} catch (Exception e) {
log.error("上传附件失败: {}", e.getMessage(), e);
throw new RuntimeException("上传附件失败: " + e.getMessage());
}
@@ -265,10 +288,30 @@ public class HqPmRepairAttachmentServiceImpl implements IHqPmRepairAttachmentSer
// 删除OSS文件
try {
if (StringUtils.isNotBlank(attachment.getFilePath())) {
OssFactory.instance().delete(attachment.getFilePath());
// 从文件路径中提取ossId
String filePath = attachment.getFilePath();
if (filePath.contains("/resource/oss/download/")) {
String ossIdStr = filePath.substring(filePath.lastIndexOf("/") + 1);
try {
Long ossId = Long.parseLong(ossIdStr);
sysOssService.deleteWithValidByIds(List.of(ossId), true);
} catch (NumberFormatException e) {
log.warn("无法从文件路径中解析ossId: {}", filePath);
}
}
}
if (StringUtils.isNotBlank(attachment.getThumbnailPath())) {
OssFactory.instance().delete(attachment.getThumbnailPath());
// 从缩略图路径中提取ossId
String thumbnailPath = attachment.getThumbnailPath();
if (thumbnailPath.contains("/resource/oss/download/")) {
String ossIdStr = thumbnailPath.substring(thumbnailPath.lastIndexOf("/") + 1);
try {
Long ossId = Long.parseLong(ossIdStr);
sysOssService.deleteWithValidByIds(List.of(ossId), true);
} catch (NumberFormatException e) {
log.warn("无法从缩略图路径中解析ossId: {}", thumbnailPath);
}
}
}
} catch (Exception e) {
log.warn("删除OSS文件失败: {}", e.getMessage());
@@ -0,0 +1,57 @@
package org.dromara.system.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 🚀 文件缓存配置类
* 用于管理文件下载的内存缓存配置参数
*
* @author Lion Li
*/
@Data
@Component
@ConfigurationProperties(prefix = "system.file-cache")
public class FileCacheConfig {
/**
* 是否启用文件缓存
*/
private boolean enabled = true;
/**
* 最大缓存文件数量
*/
private int maxCacheSize = 100;
/**
* 最大缓存大小(字节)
*/
private long maxCacheSizeBytes = 500 * 1024 * 1024; // 500MB
/**
* 缓存过期时间(毫秒)
*/
private long expireTimeMs = 30 * 60 * 1000; // 30分钟
/**
* 最大缓存单个文件大小(字节)
*/
private long maxSingleFileSize = 50 * 1024 * 1024; // 50MB
/**
* LRU淘汰比例(百分比)
*/
private int evictRatio = 20; // 20%
/**
* 是否启用缓存统计
*/
private boolean enableStats = true;
/**
* 缓存清理间隔(毫秒)
*/
private long cleanupIntervalMs = 5 * 60 * 1000; // 5分钟
}
@@ -0,0 +1,113 @@
package org.dromara.system.controller;
import java.util.List;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.web.core.BaseController;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.system.domain.vo.SysRegionVo;
import org.dromara.system.domain.bo.SysRegionBo;
import org.dromara.system.service.ISysRegionService;
import org.dromara.common.mybatis.core.page.TableDataInfo;
/**
* 行政区划
*
* @author zlwu
* @date 2025-07-26
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/system/SysRegion")
public class SysRegionController extends BaseController {
private final ISysRegionService sysRegionService;
/**
* 查询行政区划列表
*/
@SaCheckPermission("system:SysRegion:list")
@GetMapping("/list")
public TableDataInfo<SysRegionVo> list(SysRegionBo bo, PageQuery pageQuery) {
return sysRegionService.queryPageList(bo, pageQuery);
}
/**
* 导出行政区划列表
*/
@SaCheckPermission("system:SysRegion:export")
@Log(title = "行政区划", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(SysRegionBo bo, HttpServletResponse response) {
List<SysRegionVo> list = sysRegionService.queryList(bo);
ExcelUtil.exportExcel(list, "行政区划", SysRegionVo.class, response);
}
/**
* 获取行政区划详细信息
*
* @param id 主键
*/
@SaCheckPermission("system:SysRegion:query")
@GetMapping("/{id}")
public R<SysRegionVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
return R.ok(sysRegionService.queryById(id));
}
/**
* 新增行政区划
*/
@SaCheckPermission("system:SysRegion:add")
@Log(title = "行政区划", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody SysRegionBo bo) {
return toAjax(sysRegionService.insertByBo(bo));
}
/**
* 修改行政区划
*/
@SaCheckPermission("system:SysRegion:edit")
@Log(title = "行政区划", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody SysRegionBo bo) {
return toAjax(sysRegionService.updateByBo(bo));
}
/**
* 删除行政区划
*
* @param ids 主键串
*/
@SaCheckPermission("system:SysRegion:remove")
@Log(title = "行政区划", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
return toAjax(sysRegionService.deleteWithValidByIds(List.of(ids), true));
}
/**
* 获取省市区树形数据
*/
@SaCheckPermission("system:SysRegion:list")
@GetMapping("/tree")
public R<List<SysRegionVo>> getRegionTree() {
List<SysRegionVo> tree = sysRegionService.getRegionTree();
return R.ok(tree);
}
}
@@ -0,0 +1,124 @@
package org.dromara.system.controller.system;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.dromara.common.core.domain.R;
import org.dromara.common.web.core.BaseController;
import org.dromara.system.service.ISysBucketAccessPasswordService;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.constraints.NotBlank;
import lombok.RequiredArgsConstructor;
import java.util.HashMap;
import java.util.Map;
/**
* bucket密码管理控制器
*
* @author Lion Li
* @date 2025-01-27
*/
@RestController
@RequestMapping("/system/bucket-password")
@RequiredArgsConstructor
public class SysBucketPasswordController extends BaseController {
private final ISysBucketAccessPasswordService bucketPasswordService;
/**
* 重置bucket访问密码
*
* @param bucketName bucket名称
* @param newPassword 新密码
* @return 操作结果
*/
@SaCheckPermission("system:bucket-password:reset")
@PostMapping("/reset")
public R<Void> resetBucketPassword(
@RequestParam @NotBlank(message = "bucket名称不能为空") String bucketName,
@RequestParam @NotBlank(message = "新密码不能为空") String newPassword) {
try {
boolean success = bucketPasswordService.resetBucketPassword(bucketName, newPassword);
if (success) {
return R.ok("密码重置成功");
} else {
return R.fail("密码重置失败");
}
} catch (Exception e) {
return R.fail("密码重置失败:" + e.getMessage());
}
}
/**
* 获取bucket密码信息
*
* @param bucketName bucket名称
* @return bucket密码信息
*/
@SaCheckPermission("system:bucket-password:query")
@GetMapping("/info/{bucketName}")
public R<Object> getBucketPasswordInfo(@PathVariable String bucketName) {
try {
var passwordInfo = bucketPasswordService.getBucketPassword(bucketName);
if (passwordInfo != null) {
// 不返回实际密码,只返回基本信息
Map<String, Object> info = new HashMap<>();
info.put("bucketName", passwordInfo.getBucketName());
info.put("isActive", passwordInfo.getIsActive());
info.put("maxAccessCount", passwordInfo.getMaxAccessCount());
info.put("currentAccessCount", passwordInfo.getCurrentAccessCount());
info.put("expireTime", passwordInfo.getExpireTime());
info.put("updateTime", passwordInfo.getUpdateTime());
return R.ok(info);
} else {
return R.fail("bucket密码配置不存在");
}
} catch (Exception e) {
return R.fail("获取bucket密码信息失败:" + e.getMessage());
}
}
/**
* 获取bucket访问记录
*
* @param bucketName bucket名称
* @param limit 限制数量,默认10条
* @return 访问记录列表
*/
@SaCheckPermission("system:bucket-password:query")
@GetMapping("/records/{bucketName}")
public R<Object> getBucketAccessRecords(
@PathVariable String bucketName,
@RequestParam(defaultValue = "10") Integer limit) {
try {
var records = bucketPasswordService.getBucketAccessRecords(bucketName, limit);
return R.ok(records);
} catch (Exception e) {
return R.fail("获取bucket访问记录失败:" + e.getMessage());
}
}
/**
* 验证bucket密码
*
* @param bucketName bucket名称
* @param password 密码
* @return 验证结果
*/
@PostMapping("/validate")
public R<Boolean> validateBucketPassword(
@RequestParam @NotBlank(message = "bucket名称不能为空") String bucketName,
@RequestParam @NotBlank(message = "密码不能为空") String password) {
try {
boolean isValid = bucketPasswordService.validateBucketPassword(
bucketName, password, "127.0.0.1", "API-Test");
return R.ok(isValid);
} catch (Exception e) {
return R.fail("密码验证失败:" + e.getMessage());
}
}
}
@@ -0,0 +1,312 @@
package org.dromara.system.controller.system;
import cn.hutool.core.util.ObjectUtil;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.oss.core.OssClient;
import org.dromara.common.oss.factory.OssFactory;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.web.core.BaseController;
import org.dromara.system.domain.vo.SysOssVo;
import org.dromara.system.service.ISysOssService;
import org.dromara.system.service.ISysBucketAccessPasswordService;
import org.springframework.web.bind.annotation.*;
import jakarta.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 文件访问控制器
* 提供安全的文件访问接口,实现预签名URL机制 + bucket密码验证
*
* @author Lion Li
*/
@RestController
@RequestMapping("/resource/file")
public class SysFileAccessController extends BaseController {
private final ISysOssService ossService;
private final ISysBucketAccessPasswordService bucketPasswordService;
public SysFileAccessController(ISysOssService ossService, ISysBucketAccessPasswordService bucketPasswordService) {
this.ossService = ossService;
this.bucketPasswordService = bucketPasswordService;
}
/**
* 获取文件临时访问URL(带bucket密码验证)
*
* @param ossId 文件ID
* @param bucketPassword bucket访问密码
* @param expireSeconds 过期时间(秒),默认300秒
* @return 临时访问URL
*/
@GetMapping("/access/{ossId}")
public R<String> getFileAccessUrl(
@PathVariable Long ossId,
@RequestParam(required = false) String bucketPassword,
@RequestParam(defaultValue = "300") Integer expireSeconds,
HttpServletRequest request) {
// 权限检查:只有登录用户才能获取文件访问URL
if (!LoginHelper.isLogin()) {
return R.fail("请先登录");
}
try {
// 获取文件信息
SysOssVo oss = ossService.getById(ossId);
if (ObjectUtil.isNull(oss)) {
return R.fail("文件不存在");
}
// 检查文件访问权限
if (!ossService.checkFileAccessPermission(ossId)) {
return R.fail("无权限访问该文件");
}
// 获取bucket名称
String bucketName = getBucketNameFromOss(oss);
// 验证bucket访问密码
if (!validateBucketPassword(bucketName, bucketPassword, request)) {
return R.fail("Bucket访问密码验证失败");
}
// 获取OSS客户端
OssClient storage = OssFactory.instance(oss.getService());
// 生成预签名URL
String presignedUrl = storage.getPrivateUrl(
oss.getFileName(),
Duration.ofSeconds(expireSeconds)
);
// 记录文件访问日志
ossService.logFileAccess(ossId, LoginHelper.getUserId(), "ACCESS",
getClientIpAddress(request), request.getHeader("User-Agent"));
return R.ok(presignedUrl);
} catch (Exception e) {
return R.fail("获取文件访问链接失败:" + e.getMessage());
}
}
/**
* 批量获取文件临时访问URL(带bucket密码验证)
*
* @param ossIds 文件ID列表
* @param bucketPassword bucket访问密码
* @param expireSeconds 过期时间(秒),默认300秒
* @return 文件ID与临时URL的映射
*/
@PostMapping("/access/batch")
public R<Map<Long, String>> getBatchFileAccessUrl(
@RequestBody List<Long> ossIds,
@RequestParam(required = false) String bucketPassword,
@RequestParam(defaultValue = "300") Integer expireSeconds,
HttpServletRequest request) {
// 权限检查
if (!LoginHelper.isLogin()) {
return R.fail("请先登录");
}
Map<Long, String> urlMap = new HashMap<>();
try {
for (Long ossId : ossIds) {
SysOssVo oss = ossService.getById(ossId);
if (ObjectUtil.isNotNull(oss) && ossService.checkFileAccessPermission(ossId)) {
// 获取bucket名称
String bucketName = getBucketNameFromOss(oss);
// 验证bucket访问密码
if (!validateBucketPassword(bucketName, bucketPassword, request)) {
continue; // 跳过验证失败的文件
}
OssClient storage = OssFactory.instance(oss.getService());
String presignedUrl = storage.getPrivateUrl(
oss.getFileName(),
Duration.ofSeconds(expireSeconds)
);
urlMap.put(ossId, presignedUrl);
// 记录文件访问日志
ossService.logFileAccess(ossId, LoginHelper.getUserId(), "BATCH_ACCESS",
getClientIpAddress(request), request.getHeader("User-Agent"));
}
}
return R.ok(urlMap);
} catch (Exception e) {
return R.fail("批量获取文件访问链接失败:" + e.getMessage());
}
}
/**
* 获取文件下载URL(带bucket密码验证)
*
* @param ossId 文件ID
* @param bucketPassword bucket访问密码
* @param expireSeconds 过期时间(秒),默认300秒
* @return 下载URL
*/
@GetMapping("/download/{ossId}")
public R<String> getFileDownloadUrl(
@PathVariable Long ossId,
@RequestParam(required = false) String bucketPassword,
@RequestParam(defaultValue = "300") Integer expireSeconds,
HttpServletRequest request) {
// 权限检查
if (!LoginHelper.isLogin()) {
return R.fail("请先登录");
}
try {
SysOssVo oss = ossService.getById(ossId);
if (ObjectUtil.isNull(oss)) {
return R.fail("文件不存在");
}
if (!ossService.checkFileAccessPermission(ossId)) {
return R.fail("无权限下载该文件");
}
// 获取bucket名称
String bucketName = getBucketNameFromOss(oss);
// 验证bucket访问密码
if (!validateBucketPassword(bucketName, bucketPassword, request)) {
return R.fail("Bucket访问密码验证失败");
}
OssClient storage = OssFactory.instance(oss.getService());
String presignedUrl = storage.getPrivateUrl(
oss.getFileName(),
Duration.ofSeconds(expireSeconds)
);
// 记录下载日志
ossService.logFileAccess(ossId, LoginHelper.getUserId(), "DOWNLOAD",
getClientIpAddress(request), request.getHeader("User-Agent"));
return R.ok(presignedUrl);
} catch (Exception e) {
return R.fail("获取文件下载链接失败:" + e.getMessage());
}
}
/**
* 获取文件预览URL(带bucket密码验证)
*
* @param ossId 文件ID
* @param bucketPassword bucket访问密码
* @param expireSeconds 过期时间(秒),默认300秒
* @return 预览URL
*/
@GetMapping("/preview/{ossId}")
public R<String> getFilePreviewUrl(
@PathVariable Long ossId,
@RequestParam(required = false) String bucketPassword,
@RequestParam(defaultValue = "300") Integer expireSeconds,
HttpServletRequest request) {
// 权限检查
if (!LoginHelper.isLogin()) {
return R.fail("请先登录");
}
try {
SysOssVo oss = ossService.getById(ossId);
if (ObjectUtil.isNull(oss)) {
return R.fail("文件不存在");
}
if (!ossService.checkFileAccessPermission(ossId)) {
return R.fail("无权限预览该文件");
}
// 获取bucket名称
String bucketName = getBucketNameFromOss(oss);
// 验证bucket访问密码
if (!validateBucketPassword(bucketName, bucketPassword, request)) {
return R.fail("Bucket访问密码验证失败");
}
OssClient storage = OssFactory.instance(oss.getService());
String presignedUrl = storage.getPrivateUrl(
oss.getFileName(),
Duration.ofSeconds(expireSeconds)
);
// 记录预览日志
ossService.logFileAccess(ossId, LoginHelper.getUserId(), "PREVIEW",
getClientIpAddress(request), request.getHeader("User-Agent"));
return R.ok(presignedUrl);
} catch (Exception e) {
return R.fail("获取文件预览链接失败:" + e.getMessage());
}
}
/**
* 验证bucket访问密码
*
* @param bucketName bucket名称
* @param bucketPassword bucket访问密码
* @param request HttpServletRequest
* @return 验证结果
*/
private boolean validateBucketPassword(String bucketName, String bucketPassword, HttpServletRequest request) {
// 如果bucket密码为空,跳过验证(向后兼容)
if (StringUtils.isBlank(bucketPassword)) {
return true;
}
return bucketPasswordService.validateBucketPassword(
bucketName,
bucketPassword,
getClientIpAddress(request),
request.getHeader("User-Agent")
);
}
/**
* 从OSS对象中获取bucket名称
*
* @param oss OSS对象
* @return bucket名称
*/
private String getBucketNameFromOss(SysOssVo oss) {
// 从OSS配置中获取bucket名称,这里需要根据实际配置调整
// 暂时返回默认的ruoyi bucket
return "ruoyi";
}
/**
* 获取客户端IP地址
*
* @param request HttpServletRequest
* @return IP地址
*/
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (StringUtils.isNotEmpty(xForwardedFor) && !"unknown".equalsIgnoreCase(xForwardedFor)) {
return xForwardedFor.split(",")[0].trim();
}
String xRealIp = request.getHeader("X-Real-IP");
if (StringUtils.isNotEmpty(xRealIp) && !"unknown".equalsIgnoreCase(xRealIp)) {
return xRealIp;
}
return request.getRemoteAddr();
}
}
@@ -1,19 +1,21 @@
package org.dromara.system.controller.system;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.ObjectUtil;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.validate.QueryGroup;
import org.dromara.common.web.core.BaseController;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.web.core.BaseController;
import org.dromara.system.domain.bo.SysOssBo;
import org.dromara.system.domain.vo.SysOssUploadVo;
import org.dromara.system.domain.vo.SysOssVo;
import org.dromara.system.service.ISysOssService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotEmpty;
import lombok.RequiredArgsConstructor;
@@ -21,16 +23,19 @@ import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/**
* 文件上传 控制层
*
* @author Lion Li
*/
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@@ -55,8 +60,7 @@ public class SysOssController extends BaseController {
*/
@SaCheckPermission("system:oss:query")
@GetMapping("/listByIds/{ossIds}")
public R<List<SysOssVo>> listByIds(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ossIds) {
public R<List<SysOssVo>> listByIds(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ossIds) {
List<SysOssVo> list = ossService.listByIds(Arrays.asList(ossIds));
return R.ok(list);
}
@@ -82,14 +86,65 @@ public class SysOssController extends BaseController {
}
/**
* 下载OSS对象
* 🔐 安全文件下载接口
* 强制权限验证,确保只有登录用户才能下载文件
*
* @param ossId OSS对象ID
* @param ossId 文件ID
* @param response HTTP响应对象
*/
@SaCheckPermission("system:oss:download")
@GetMapping("/download/{ossId}")
public void download(@PathVariable Long ossId, HttpServletResponse response) throws IOException {
ossService.download(ossId, response);
public void secureDownload(@PathVariable("ossId") Long ossId, HttpServletResponse response) {
try {
ossService.secureDownload(ossId, response);
} catch (Exception e) {
log.error("安全文件下载失败: {}", e.getMessage(), e);
try {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write("无权限下载该文件");
} catch (IOException ex) {
log.error("写入错误响应失败", ex);
}
}
}
/**
* 🔐 获取安全文件访问URL
* 返回通过后端代理的安全访问URL,而不是直接返回OSS URL
*
* @param ossId 文件ID
* @return 安全访问URL
*/
@SaCheckPermission("system:oss:access")
@GetMapping("/secure-url/{ossId}")
public R<String> getSecureFileUrl(@PathVariable("ossId") Long ossId) {
try {
String secureUrl = ossService.getSecureFileUrl(ossId);
return R.ok(secureUrl);
} catch (Exception e) {
log.error("获取安全文件URL失败: {}", e.getMessage(), e);
return R.fail("无权限访问该文件");
}
}
/**
* 获取客户端IP地址
*
* @param request HttpServletRequest
* @return IP地址
*/
private String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (StringUtils.isNotEmpty(xForwardedFor) && !"unknown".equalsIgnoreCase(xForwardedFor)) {
return xForwardedFor.split(",")[0].trim();
}
String xRealIp = request.getHeader("X-Real-IP");
if (StringUtils.isNotEmpty(xRealIp) && !"unknown".equalsIgnoreCase(xRealIp)) {
return xRealIp;
}
return request.getRemoteAddr();
}
/**
@@ -100,9 +155,45 @@ public class SysOssController extends BaseController {
@SaCheckPermission("system:oss:remove")
@Log(title = "OSS对象存储", businessType = BusinessType.DELETE)
@DeleteMapping("/{ossIds}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ossIds) {
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ossIds) {
return toAjax(ossService.deleteWithValidByIds(List.of(ossIds), true));
}
/**
* 🚀 获取文件缓存统计信息
* 包含缓存大小、命中率、缓存条目数量等统计信息
*/
@SaCheckPermission("system:oss:cache:stats")
@GetMapping("/cache/stats")
public R<Map<String, Object>> getCacheStats() {
Map<String, Object> stats = ossService.getCacheStats();
return R.ok(stats);
}
/**
* 🚀 清空文件缓存
* 清除所有缓存的文件数据,释放内存空间
*/
@SaCheckPermission("system:oss:cache:clear")
@Log(title = "文件缓存管理", businessType = BusinessType.DELETE)
@DeleteMapping("/cache/clear")
public R<Void> clearCache() {
ossService.clearCache();
return R.ok("缓存已清空");
}
/**
* 🚀 从缓存中移除指定文件
* 移除指定文件ID的缓存数据
*
* @param ossId 文件ID
*/
@SaCheckPermission("system:oss:cache:remove")
@Log(title = "文件缓存管理", businessType = BusinessType.DELETE)
@DeleteMapping("/cache/{ossId}")
public R<Void> removeFromCache(@PathVariable Long ossId) {
ossService.removeFromCache(ossId);
return R.ok("文件缓存已移除");
}
}
@@ -95,7 +95,8 @@ public class SysProfileController extends BaseController {
if (BCrypt.checkpw(bo.getNewPassword(), password)) {
return R.fail("新密码不能与旧密码相同");
}
int rows = DataPermissionHelper.ignore(() -> userService.resetUserPwd(user.getUserId(), BCrypt.hashpw(bo.getNewPassword())));
int rows = DataPermissionHelper
.ignore(() -> userService.resetUserPwd(user.getUserId(), BCrypt.hashpw(bo.getNewPassword())));
if (rows > 0) {
return R.ok();
}
@@ -118,7 +119,8 @@ public class SysProfileController extends BaseController {
}
SysOssVo oss = ossService.upload(avatarfile);
String avatar = oss.getUrl();
boolean updateSuccess = DataPermissionHelper.ignore(() -> userService.updateUserAvatar(LoginHelper.getUserId(), oss.getOssId()));
boolean updateSuccess = DataPermissionHelper
.ignore(() -> userService.updateUserAvatar(LoginHelper.getUserId(), oss.getOssId()));
if (updateSuccess) {
return R.ok(new AvatarVo(avatar));
}
@@ -126,8 +128,10 @@ public class SysProfileController extends BaseController {
return R.fail("上传图片异常,请联系管理员");
}
public record AvatarVo(String imgUrl) {}
public record AvatarVo(String imgUrl) {
}
public record ProfileVo(SysUserVo user, String roleGroup, String postGroup) {}
public record ProfileVo(SysUserVo user, String roleGroup, String postGroup) {
}
}
@@ -0,0 +1,96 @@
package org.dromara.system.domain;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.tenant.core.TenantEntity;
import java.io.Serial;
import java.util.Date;
/**
* bucket访问密码对象 sys_bucket_access_password
*
* @author Lion Li
* @date 2025-01-27
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_bucket_access_password")
public class SysBucketAccessPassword extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id")
private Long id;
/**
* bucket名称
*/
private String bucketName;
/**
* 访问密码(加密存储)
*/
private String accessPassword;
/**
* 密码盐值
*/
private String passwordSalt;
/**
* 是否启用(1启用 0禁用)
*/
private Integer isActive;
/**
* 密码过期时间
*/
private Date expireTime;
/**
* 最大访问次数(0表示无限制)
*/
private Integer maxAccessCount;
/**
* 当前访问次数
*/
private Integer currentAccessCount;
/**
* 创建者
*/
@TableField(fill = FieldFill.INSERT)
private Long createBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新者
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateBy;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
/**
* 删除标志(0代表存在 1代表删除)
*/
@TableLogic
private Long deleted;
}
@@ -0,0 +1,72 @@
package org.dromara.system.domain;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.tenant.core.TenantEntity;
import java.io.Serial;
import java.util.Date;
/**
* bucket访问记录对象 sys_bucket_access_record
*
* @author Lion Li
* @date 2025-01-27
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_bucket_access_record")
public class SysBucketAccessRecord extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id")
private Long id;
/**
* bucket名称
*/
private String bucketName;
/**
* 访问密码ID
*/
private Long accessPasswordId;
/**
* 访问时间
*/
private Date accessTime;
/**
* 访问IP
*/
private String accessIp;
/**
* 用户代理
*/
private String accessUserAgent;
/**
* 访问结果(1成功 0失败)
*/
private Integer accessResult;
/**
* 失败原因
*/
private String failReason;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
}
@@ -0,0 +1,67 @@
package org.dromara.system.domain;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.tenant.core.TenantEntity;
import java.io.Serial;
import java.util.Date;
/**
* 文件访问日志对象 sys_file_access_log
*
* @author Lion Li
* @date 2025-01-27
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_file_access_log")
public class SysFileAccessLog extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id")
private Long id;
/**
* 文件ID
*/
private Long ossId;
/**
* 访问用户ID
*/
private Long userId;
/**
* 访问类型:DOWNLOAD, PREVIEW, ACCESS, BATCH_ACCESS
*/
private String accessType;
/**
* 访问时间
*/
private Date accessTime;
/**
* 访问IP
*/
private String ipAddress;
/**
* 用户代理
*/
private String userAgent;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
}
@@ -0,0 +1,81 @@
package org.dromara.system.domain;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.tenant.core.TenantEntity;
import java.io.Serial;
import java.util.Date;
/**
* 文件权限对象 sys_file_permission
*
* @author Lion Li
* @date 2025-01-27
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_file_permission")
public class SysFilePermission extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id")
private Long id;
/**
* 文件ID
*/
private Long ossId;
/**
* 用户ID(为空表示角色权限)
*/
private Long userId;
/**
* 角色ID(为空表示用户权限)
*/
private Long roleId;
/**
* 权限类型:READ, WRITE, DELETE
*/
private String permissionType;
/**
* 创建者
*/
@TableField(fill = FieldFill.INSERT)
private Long createBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新者
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateBy;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
/**
* 删除标志(0代表存在 1代表删除)
*/
@TableLogic
private Long deleted;
}
@@ -0,0 +1,86 @@
package org.dromara.system.domain;
import org.dromara.common.tenant.core.TenantEntity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
/**
* 行政区划对象 sys_region
*
* @author zlwu
* @date 2025-07-26
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_region")
public class SysRegion extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id")
private Long id;
/**
* 行政区划编码
*/
private String code;
/**
* 行政区划名称
*/
private String name;
/**
* 行政区划简称
*/
private String shortName;
/**
* 父级行政区划编码,顶级为0
*/
private Long parentCode;
/**
* 层级(1:省,2:市,3:区/县,4:街道/镇)
*/
private Long level;
/**
* 排序
*/
private Long sort;
/**
* 状态(0正常 1停用)
*/
private String status;
/**
* 备注
*/
private String remark;
/**
* 经度
*/
private Long longitude;
/**
* 纬度
*/
private Long latitude;
/**
* 逻辑删除标识(0未删除 1已删除)
*/
private Long deleted;
}
@@ -70,11 +70,7 @@ public class SysUser extends TenantEntity {
/**
* 密码
*/
@TableField(
insertStrategy = FieldStrategy.NOT_EMPTY,
updateStrategy = FieldStrategy.NOT_EMPTY,
whereStrategy = FieldStrategy.NOT_EMPTY
)
@TableField(insertStrategy = FieldStrategy.NOT_EMPTY, updateStrategy = FieldStrategy.NOT_EMPTY, whereStrategy = FieldStrategy.NOT_EMPTY)
private String password;
/**
@@ -103,7 +99,6 @@ public class SysUser extends TenantEntity {
*/
private String remark;
public SysUser(Long userId) {
this.userId = userId;
}
@@ -112,4 +107,9 @@ public class SysUser extends TenantEntity {
return SystemConstants.SUPER_ADMIN_ID.equals(this.userId);
}
/**
* 账号类型
*/
private String accountType;
}
@@ -0,0 +1,88 @@
package org.dromara.system.domain.bo;
import org.dromara.system.domain.SysRegion;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.*;
/**
* 行政区划业务对象 sys_region
*
* @author zlwu
* @date 2025-07-26
*/
@Data
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = SysRegion.class, reverseConvertGenerate = false)
public class SysRegionBo extends BaseEntity {
/**
* 主键ID
*/
@NotNull(message = "主键ID不能为空", groups = { EditGroup.class })
private Long id;
/**
* 行政区划编码
*/
@NotBlank(message = "行政区划编码不能为空", groups = { AddGroup.class, EditGroup.class })
private String code;
/**
* 行政区划名称
*/
@NotBlank(message = "行政区划名称不能为空", groups = { AddGroup.class, EditGroup.class })
private String name;
/**
* 行政区划简称
*/
private String shortName;
/**
* 父级行政区划编码,顶级为0
*/
private Long parentCode;
/**
* 层级(1:省,2:市,3:区/县,4:街道/镇)
*/
@NotNull(message = "层级(1:省,2:市,3:区/县,4:街道/镇)不能为空", groups = { AddGroup.class, EditGroup.class })
private Long level;
/**
* 排序
*/
private Long sort;
/**
* 状态(0正常 1停用)
*/
private String status;
/**
* 备注
*/
private String remark;
/**
* 经度
*/
private Long longitude;
/**
* 纬度
*/
private Long latitude;
/**
* 逻辑删除标识(0未删除 1已删除)
*/
private Long deleted;
}
@@ -121,4 +121,13 @@ public class SysUserBo extends BaseEntity {
return SystemConstants.SUPER_ADMIN_ID.equals(this.userId);
}
/**
* 账号类型
*/
private String accountType;
/**
* 头像
*/
private String avatar;
}
@@ -0,0 +1,110 @@
package org.dromara.system.domain.vo;
import org.dromara.system.domain.SysRegion;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.convert.ExcelDictConvert;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
/**
* 行政区划视图对象 sys_region
*
* @author zlwu
* @date 2025-07-26
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = SysRegion.class)
public class SysRegionVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@ExcelProperty(value = "主键ID")
private Long id;
/**
* 行政区划编码
*/
@ExcelProperty(value = "行政区划编码")
private String code;
/**
* 行政区划名称
*/
@ExcelProperty(value = "行政区划名称")
private String name;
/**
* 行政区划简称
*/
@ExcelProperty(value = "行政区划简称")
private String shortName;
/**
* 父级行政区划编码,顶级为0
*/
@ExcelProperty(value = "父级行政区划编码,顶级为0")
private Long parentCode;
/**
* 层级(1:省,2:市,3:区/县,4:街道/镇)
*/
@ExcelProperty(value = "层级", converter = ExcelDictConvert.class)
@ExcelDictFormat(readConverterExp = "1=:省,2:市,3:区/县,4:街道/镇")
private Long level;
/**
* 排序
*/
@ExcelProperty(value = "排序")
private Long sort;
/**
* 状态(0正常 1停用)
*/
@ExcelProperty(value = "状态", converter = ExcelDictConvert.class)
@ExcelDictFormat(readConverterExp = "0=正常,1=停用")
private String status;
/**
* 备注
*/
@ExcelProperty(value = "备注")
private String remark;
/**
* 经度
*/
@ExcelProperty(value = "经度")
private Long longitude;
/**
* 纬度
*/
@ExcelProperty(value = "纬度")
private Long latitude;
/**
* 逻辑删除标识(0未删除 1已删除)
*/
@ExcelProperty(value = "逻辑删除标识", converter = ExcelDictConvert.class)
@ExcelDictFormat(readConverterExp = "0=未删除,1=已删除")
private Long deleted;
/**
* 子级行政区划列表(用于树形结构)
*/
private List<SysRegionVo> children;
}
@@ -15,7 +15,6 @@ import java.io.Serializable;
import java.util.Date;
import java.util.List;
/**
* 用户信息视图对象 sys_user
*
@@ -139,4 +138,8 @@ public class SysUserVo implements Serializable {
*/
private Long roleId;
/**
* 账号类型 1 - 老师 2- 学生
*/
private String accountType;
}
@@ -0,0 +1,15 @@
package org.dromara.system.mapper;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.system.domain.SysBucketAccessPassword;
/**
* bucket访问密码 数据层
*
* @author Lion Li
* @date 2025-01-27
*/
public interface SysBucketAccessPasswordMapper
extends BaseMapperPlus<SysBucketAccessPassword, SysBucketAccessPassword> {
}
@@ -0,0 +1,14 @@
package org.dromara.system.mapper;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.system.domain.SysBucketAccessRecord;
/**
* bucket访问记录 数据层
*
* @author Lion Li
* @date 2025-01-27
*/
public interface SysBucketAccessRecordMapper extends BaseMapperPlus<SysBucketAccessRecord, SysBucketAccessRecord> {
}
@@ -14,6 +14,14 @@ import java.util.List;
*/
public interface SysDictDataMapper extends BaseMapperPlus<SysDictData, SysDictDataVo> {
/**
* 根据字典类型查询字典数据(绕过租户过滤)
*
* @param dictType 字典类型
* @return 符合条件的字典数据列表
*/
List<SysDictDataVo> selectDictDataByTypeIgnoreTenant(String dictType);
/**
* 根据字典类型查询字典数据列表
*
@@ -0,0 +1,15 @@
package org.dromara.system.mapper;
import org.dromara.system.domain.SysRegion;
import org.dromara.system.domain.vo.SysRegionVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
/**
* 行政区划Mapper接口
*
* @author zlwu
* @date 2025-07-26
*/
public interface SysRegionMapper extends BaseMapperPlus<SysRegion, SysRegionVo> {
}
@@ -21,138 +21,136 @@ import java.util.List;
*/
public interface SysUserMapper extends BaseMapperPlus<SysUser, SysUserVo> {
/**
* 分页查询用户列表,并进行数据权限控制
*
* @param page 分页参数
* @param queryWrapper 查询条件
* @return 分页的用户信息
*/
@DataPermission({
@DataColumn(key = "deptName", value = "u.dept_id"),
@DataColumn(key = "userName", value = "u.user_id")
})
Page<SysUserVo> selectPageUserList(@Param("page") Page<SysUser> page,
@Param(Constants.WRAPPER) Wrapper<SysUser> queryWrapper);
/**
* 分页查询用户列表,并进行数据权限控制
*
* @param page 分页参数
* @param queryWrapper 查询条件
* @return 分页的用户信息
*/
@DataPermission({
@DataColumn(key = "deptName", value = "u.dept_id"),
@DataColumn(key = "userName", value = "u.user_id")
})
Page<SysUserVo> selectPageUserList(@Param("page") Page<SysUser> page,
@Param(Constants.WRAPPER) Wrapper<SysUser> queryWrapper);
/**
* 查询用户列表,并进行数据权限控制
*
* @param queryWrapper 查询条件
* @return 用户信息集合
*/
@DataPermission({
@DataColumn(key = "deptName", value = "dept_id"),
@DataColumn(key = "userName", value = "user_id")
})
List<SysUserVo> selectUserList(@Param(Constants.WRAPPER) Wrapper<SysUser> queryWrapper);
/**
* 查询用户列表,并进行数据权限控制
*
* @param queryWrapper 查询条件
* @return 用户信息集合
*/
@DataPermission({
@DataColumn(key = "deptName", value = "dept_id"),
@DataColumn(key = "userName", value = "user_id")
})
List<SysUserVo> selectUserList(@Param(Constants.WRAPPER) Wrapper<SysUser> queryWrapper);
/**
* 根据条件分页查询用户列表
*
* @param queryWrapper 查询条件
* @return 用户信息集合信息
*/
@DataPermission({
@DataColumn(key = "deptName", value = "d.dept_id"),
@DataColumn(key = "userName", value = "u.user_id")
})
List<SysUserExportVo> selectUserExportList(@Param(Constants.WRAPPER) Wrapper<SysUser> queryWrapper);
/**
* 根据条件分页查询用户列表
*
* @param queryWrapper 查询条件
* @return 用户信息集合信息
*/
@DataPermission({
@DataColumn(key = "deptName", value = "d.dept_id"),
@DataColumn(key = "userName", value = "u.user_id")
})
List<SysUserExportVo> selectUserExportList(@Param(Constants.WRAPPER) Wrapper<SysUser> queryWrapper);
/**
* 根据条件分页查询已配用户角色列表
*
* @param queryWrapper 查询条件
* @return 用户信息集合信息
*/
@DataPermission({
@DataColumn(key = "deptName", value = "d.dept_id"),
@DataColumn(key = "userName", value = "u.user_id")
})
Page<SysUserVo> selectAllocatedList(@Param("page") Page<SysUser> page,
@Param(Constants.WRAPPER) Wrapper<SysUser> queryWrapper);
/**
* 根据条件分页查询已配用户角色列表
*
* @param queryWrapper 查询条件
* @return 用户信息集合信息
*/
@DataPermission({
@DataColumn(key = "deptName", value = "d.dept_id"),
@DataColumn(key = "userName", value = "u.user_id")
})
Page<SysUserVo> selectAllocatedList(@Param("page") Page<SysUser> page,
@Param(Constants.WRAPPER) Wrapper<SysUser> queryWrapper);
/**
* 根据条件分页查询未分配用户角色列表
*
* @param queryWrapper 查询条件
* @return 用户信息集合信息
*/
@DataPermission({
@DataColumn(key = "deptName", value = "d.dept_id"),
@DataColumn(key = "userName", value = "u.user_id")
})
Page<SysUserVo> selectUnallocatedList(@Param("page") Page<SysUser> page,
@Param(Constants.WRAPPER) Wrapper<SysUser> queryWrapper);
/**
* 根据条件分页查询未分配用户角色列表
*
* @param queryWrapper 查询条件
* @return 用户信息集合信息
*/
@DataPermission({
@DataColumn(key = "deptName", value = "d.dept_id"),
@DataColumn(key = "userName", value = "u.user_id")
})
Page<SysUserVo> selectUnallocatedList(@Param("page") Page<SysUser> page,
@Param(Constants.WRAPPER) Wrapper<SysUser> queryWrapper);
/**
* 根据用户ID统计用户数量
*
* @param userId 用户ID
* @return 用户数量
*/
@DataPermission({
@DataColumn(key = "deptName", value = "dept_id"),
@DataColumn(key = "userName", value = "user_id")
})
long countUserById(Long userId);
/**
* 根据用户ID统计用户数量
*
* @param userId 用户ID
* @return 用户数量
*/
@DataPermission({
@DataColumn(key = "deptName", value = "dept_id"),
@DataColumn(key = "userName", value = "user_id")
})
long countUserById(Long userId);
/**
* 根据权限标识获取用户列表
*
* @param permission 权限标识
* @param tenantId 租户ID
* @return 用户列表
*/
@Select("""
SELECT DISTINCT
su.*
FROM
sys_user su
INNER JOIN sys_user_role sur ON su.user_id = sur.user_id
INNER JOIN sys_role sr ON sur.role_id = sr.role_id
INNER JOIN sys_role_menu srm ON sr.role_id = srm.role_id
INNER JOIN sys_menu sm ON srm.menu_id = sm.menu_id
WHERE
su.del_flag = '0'
AND su.status = '0'
AND sr.del_flag = '0'
AND sr.status = '0'
AND sm.perms = #{permission}
<if test="tenantId != null and tenantId != ''">
AND su.tenant_id = #{tenantId}
</if>
ORDER BY
su.user_id
""")
List<SysUser> selectUserListByPermission(@Param("permission") String permission,
@Param("tenantId") String tenantId);
/**
* 根据权限标识获取用户列表
*
* @param permission 权限标识
* @param tenantId 租户ID
* @return 用户列表
*/
@Select("""
SELECT DISTINCT
su.*
FROM
sys_user su
INNER JOIN sys_user_role sur ON su.user_id = sur.user_id
INNER JOIN sys_role sr ON sur.role_id = sr.role_id
INNER JOIN sys_role_menu srm ON sr.role_id = srm.role_id
INNER JOIN sys_menu sm ON srm.menu_id = sm.menu_id
WHERE
su.del_flag = '0'
AND su.status = '0'
AND sr.del_flag = '0'
AND sr.status = '0'
AND sm.perms = #{permission}
AND (#{tenantId} IS NULL OR #{tenantId} = '' OR su.tenant_id = #{tenantId})
ORDER BY
su.user_id
""")
List<SysUser> selectUserListByPermission(@Param("permission") String permission,
@Param("tenantId") String tenantId);
/**
* 根据条件更新用户数据
*
* @param user 要更新的用户实体
* @param updateWrapper 更新条件封装器
* @return 更新操作影响的行数
*/
@Override
@DataPermission({
@DataColumn(key = "deptName", value = "dept_id"),
@DataColumn(key = "userName", value = "user_id")
})
int update(@Param(Constants.ENTITY) SysUser user, @Param(Constants.WRAPPER) Wrapper<SysUser> updateWrapper);
/**
* 根据条件更新用户数据
*
* @param user 要更新的用户实体
* @param updateWrapper 更新条件封装器
* @return 更新操作影响的行数
*/
@Override
@DataPermission({
@DataColumn(key = "deptName", value = "dept_id"),
@DataColumn(key = "userName", value = "user_id")
})
int update(@Param(Constants.ENTITY) SysUser user, @Param(Constants.WRAPPER) Wrapper<SysUser> updateWrapper);
/**
* 根据用户ID更新用户数据
*
* @param user 要更新的用户实体
* @return 更新操作影响的行数
*/
@Override
@DataPermission({
@DataColumn(key = "deptName", value = "dept_id"),
@DataColumn(key = "userName", value = "user_id")
})
int updateById(@Param(Constants.ENTITY) SysUser user);
/**
* 根据用户ID更新用户数据
*
* @param user 要更新的用户实体
* @return 更新操作影响的行数
*/
@Override
@DataPermission({
@DataColumn(key = "deptName", value = "dept_id"),
@DataColumn(key = "userName", value = "user_id")
})
int updateById(@Param(Constants.ENTITY) SysUser user);
}
@@ -0,0 +1,89 @@
package org.dromara.system.service;
import org.dromara.system.domain.SysBucketAccessPassword;
import org.dromara.system.domain.SysBucketAccessRecord;
import java.util.List;
/**
* bucket访问密码 服务层
*
* @author Lion Li
* @date 2025-01-27
*/
public interface ISysBucketAccessPasswordService {
/**
* 验证bucket访问密码
*
* @param bucketName bucket名称
* @param password 访问密码
* @param ipAddress 访问IP
* @param userAgent 用户代理
* @return 验证结果
*/
boolean validateBucketPassword(String bucketName, String password, String ipAddress, String userAgent);
/**
* 获取bucket访问密码信息
*
* @param bucketName bucket名称
* @return bucket访问密码信息
*/
SysBucketAccessPassword getBucketPassword(String bucketName);
/**
* 记录bucket访问
*
* @param bucketName bucket名称
* @param passwordId 密码ID
* @param ipAddress 访问IP
* @param userAgent 用户代理
* @param success 是否成功
* @param failReason 失败原因
*/
void recordBucketAccess(String bucketName, Long passwordId, String ipAddress, String userAgent, boolean success, String failReason);
/**
* 更新访问次数
*
* @param bucketName bucket名称
*/
void incrementAccessCount(String bucketName);
/**
* 检查bucket访问限制
*
* @param bucketName bucket名称
* @return 是否可以访问
*/
boolean checkAccessLimit(String bucketName);
/**
* 生成bucket访问密码
*
* @param bucketName bucket名称
* @param password 原始密码
* @return 加密后的密码
*/
String generateEncryptedPassword(String bucketName, String password);
/**
* 重置bucket访问密码
*
* @param bucketName bucket名称
* @param newPassword 新密码
* @return 是否成功
*/
boolean resetBucketPassword(String bucketName, String newPassword);
/**
* 获取bucket访问记录
*
* @param bucketName bucket名称
* @param limit 限制数量
* @return 访问记录列表
*/
List<SysBucketAccessRecord> getBucketAccessRecords(String bucketName, int limit);
}
@@ -11,6 +11,7 @@ import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 文件上传 服务层
@@ -77,4 +78,69 @@ public interface ISysOssService {
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 检查文件访问权限
*
* @param ossId 文件ID
* @return 是否有权限访问
*/
boolean checkFileAccessPermission(Long ossId);
/**
* 记录文件访问日志
*
* @param ossId 文件ID
* @param userId 用户ID
* @param accessType 访问类型
* @param ipAddress IP地址
* @param userAgent 用户代理
*/
void logFileAccess(Long ossId, Long userId, String accessType, String ipAddress, String userAgent);
/**
* 获取文件临时访问URL(带权限检查)
*
* @param ossId 文件ID
* @param expireSeconds 过期时间(秒)
* @return 临时访问URL
*/
String getFileAccessUrl(Long ossId, Integer expireSeconds);
/**
* 🔐 安全文件访问URL生成(推荐使用)
* 通过后端代理进行权限验证,确保只有登录用户才能访问
*
* @param ossId 文件ID
* @return 安全访问URL(通过后端代理)
*/
String getSecureFileUrl(Long ossId);
/**
* 🔐 安全文件下载(通过后端代理)
* 强制权限验证,确保只有登录用户才能下载
*
* @param ossId 文件ID
* @param response HTTP响应对象
*/
void secureDownload(Long ossId, HttpServletResponse response) throws IOException;
/**
* 🚀 获取文件缓存统计信息
*
* @return 缓存统计信息
*/
Map<String, Object> getCacheStats();
/**
* 🚀 清空文件缓存
*/
void clearCache();
/**
* 🚀 从缓存中移除指定文件
*
* @param ossId 文件ID
*/
void removeFromCache(Long ossId);
}
@@ -0,0 +1,75 @@
package org.dromara.system.service;
import org.dromara.system.domain.vo.SysRegionVo;
import org.dromara.system.domain.bo.SysRegionBo;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import java.util.Collection;
import java.util.List;
/**
* 行政区划Service接口
*
* @author zlwu
* @date 2025-07-26
*/
public interface ISysRegionService {
/**
* 查询行政区划
*
* @param id 主键
* @return 行政区划
*/
SysRegionVo queryById(Long id);
/**
* 分页查询行政区划列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 行政区划分页列表
*/
TableDataInfo<SysRegionVo> queryPageList(SysRegionBo bo, PageQuery pageQuery);
/**
* 查询符合条件的行政区划列表
*
* @param bo 查询条件
* @return 行政区划列表
*/
List<SysRegionVo> queryList(SysRegionBo bo);
/**
* 新增行政区划
*
* @param bo 行政区划
* @return 是否新增成功
*/
Boolean insertByBo(SysRegionBo bo);
/**
* 修改行政区划
*
* @param bo 行政区划
* @return 是否修改成功
*/
Boolean updateByBo(SysRegionBo bo);
/**
* 校验并批量删除行政区划信息
*
* @param ids 待删除的主键集合
* @param isValid 是否进行有效性校验
* @return 是否删除成功
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 获取省市区树形数据
*
* @return 省市区树形数据列表
*/
List<SysRegionVo> getRegionTree();
}
@@ -0,0 +1,203 @@
package org.dromara.system.service.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.system.domain.SysBucketAccessPassword;
import org.dromara.system.domain.SysBucketAccessRecord;
import org.dromara.system.mapper.SysBucketAccessPasswordMapper;
import org.dromara.system.mapper.SysBucketAccessRecordMapper;
import org.dromara.system.service.ISysBucketAccessPasswordService;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
/**
* bucket访问密码 服务层实现
*
* @author Lion Li
* @date 2025-01-27
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class SysBucketAccessPasswordServiceImpl implements ISysBucketAccessPasswordService {
private final SysBucketAccessPasswordMapper baseMapper;
private final SysBucketAccessRecordMapper recordMapper;
@Override
public boolean validateBucketPassword(String bucketName, String password, String ipAddress, String userAgent) {
try {
// 获取bucket密码信息
SysBucketAccessPassword bucketPassword = getBucketPassword(bucketName);
if (bucketPassword == null) {
recordBucketAccess(bucketName, null, ipAddress, userAgent, false, "Bucket密码配置不存在");
return false;
}
// 检查密码是否启用
if (bucketPassword.getIsActive() != 1) {
recordBucketAccess(bucketName, bucketPassword.getId(), ipAddress, userAgent, false, "Bucket密码已禁用");
return false;
}
// 检查密码是否过期
if (bucketPassword.getExpireTime() != null && bucketPassword.getExpireTime().before(new Date())) {
recordBucketAccess(bucketName, bucketPassword.getId(), ipAddress, userAgent, false, "Bucket密码已过期");
return false;
}
// 检查访问次数限制
if (!checkAccessLimit(bucketName)) {
recordBucketAccess(bucketName, bucketPassword.getId(), ipAddress, userAgent, false, "Bucket访问次数已达上限");
return false;
}
// 验证密码
String encryptedPassword = generateEncryptedPassword(bucketName, password);
if (!encryptedPassword.equals(bucketPassword.getAccessPassword())) {
recordBucketAccess(bucketName, bucketPassword.getId(), ipAddress, userAgent, false, "Bucket密码错误");
return false;
}
// 验证成功,记录访问并增加访问次数
recordBucketAccess(bucketName, bucketPassword.getId(), ipAddress, userAgent, true, null);
incrementAccessCount(bucketName);
return true;
} catch (Exception e) {
log.error("验证bucket密码失败: {}", e.getMessage(), e);
recordBucketAccess(bucketName, null, ipAddress, userAgent, false, "验证过程发生异常: " + e.getMessage());
return false;
}
}
@Override
public SysBucketAccessPassword getBucketPassword(String bucketName) {
if (StrUtil.isBlank(bucketName)) {
return null;
}
LambdaQueryWrapper<SysBucketAccessPassword> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysBucketAccessPassword::getBucketName, bucketName);
wrapper.eq(SysBucketAccessPassword::getDeleted, 0);
wrapper.last("LIMIT 1");
return baseMapper.selectOne(wrapper);
}
@Override
public void recordBucketAccess(String bucketName, Long passwordId, String ipAddress, String userAgent,
boolean success, String failReason) {
try {
SysBucketAccessRecord record = new SysBucketAccessRecord();
record.setBucketName(bucketName);
record.setAccessPasswordId(passwordId);
record.setAccessTime(new Date());
record.setAccessIp(ipAddress);
record.setAccessUserAgent(userAgent);
record.setAccessResult(success ? 1 : 0);
record.setFailReason(failReason);
recordMapper.insert(record);
} catch (Exception e) {
log.error("记录bucket访问失败: {}", e.getMessage(), e);
}
}
@Override
public void incrementAccessCount(String bucketName) {
try {
LambdaQueryWrapper<SysBucketAccessPassword> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysBucketAccessPassword::getBucketName, bucketName);
wrapper.eq(SysBucketAccessPassword::getDeleted, 0);
SysBucketAccessPassword bucketPassword = baseMapper.selectOne(wrapper);
if (bucketPassword != null) {
bucketPassword.setCurrentAccessCount(bucketPassword.getCurrentAccessCount() + 1);
baseMapper.updateById(bucketPassword);
}
} catch (Exception e) {
log.error("更新bucket访问次数失败: {}", e.getMessage(), e);
}
}
@Override
public boolean checkAccessLimit(String bucketName) {
SysBucketAccessPassword bucketPassword = getBucketPassword(bucketName);
if (bucketPassword == null) {
return false;
}
// 如果最大访问次数为0,表示无限制
if (bucketPassword.getMaxAccessCount() == 0) {
return true;
}
// 检查当前访问次数是否超过限制
return bucketPassword.getCurrentAccessCount() < bucketPassword.getMaxAccessCount();
}
@Override
public String generateEncryptedPassword(String bucketName, String password) {
if (StrUtil.isBlank(bucketName) || StrUtil.isBlank(password)) {
return null;
}
// 使用MD5加密,可以后续改为更安全的加密方式
String salt = bucketName + "_salt_2025";
return DigestUtil.md5Hex(password + salt);
}
@Override
public boolean resetBucketPassword(String bucketName, String newPassword) {
try {
SysBucketAccessPassword bucketPassword = getBucketPassword(bucketName);
if (bucketPassword == null) {
// 创建新的密码配置
bucketPassword = new SysBucketAccessPassword();
bucketPassword.setBucketName(bucketName);
bucketPassword.setPasswordSalt(bucketName + "_salt_2025");
bucketPassword.setIsActive(1);
bucketPassword.setMaxAccessCount(0);
bucketPassword.setCurrentAccessCount(0);
}
// 设置新密码
bucketPassword.setAccessPassword(generateEncryptedPassword(bucketName, newPassword));
bucketPassword.setUpdateTime(new Date());
if (bucketPassword.getId() == null) {
return baseMapper.insert(bucketPassword) > 0;
} else {
return baseMapper.updateById(bucketPassword) > 0;
}
} catch (Exception e) {
log.error("重置bucket密码失败: {}", e.getMessage(), e);
return false;
}
}
@Override
public List<SysBucketAccessRecord> getBucketAccessRecords(String bucketName, int limit) {
try {
LambdaQueryWrapper<SysBucketAccessRecord> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysBucketAccessRecord::getBucketName, bucketName);
wrapper.orderByDesc(SysBucketAccessRecord::getAccessTime);
wrapper.last("LIMIT " + limit);
return recordMapper.selectList(wrapper);
} catch (Exception e) {
log.error("获取bucket访问记录失败: {}", e.getMessage(), e);
return List.of();
}
}
}
@@ -35,6 +35,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.stream.Collectors;
import org.dromara.common.tenant.helper.TenantHelper;
/**
* 字典 业务层处理
@@ -94,10 +95,11 @@ public class SysDictTypeServiceImpl implements ISysDictTypeService, DictService
* @param dictType 字典类型
* @return 字典数据集合信息
*/
@Cacheable(cacheNames = CacheNames.SYS_DICT, key = "#dictType")
// @Cacheable(cacheNames = CacheNames.SYS_DICT, key = "#dictType") // 临时注释缓存测试
@Override
public List<SysDictDataVo> selectDictDataByType(String dictType) {
List<SysDictDataVo> dictDatas = dictDataMapper.selectDictDataByType(dictType);
// 使用绕过租户过滤的查询方法
List<SysDictDataVo> dictDatas = dictDataMapper.selectDictDataByTypeIgnoreTenant(dictType);
if (CollUtil.isNotEmpty(dictDatas)) {
return dictDatas;
}
@@ -27,6 +27,7 @@ import org.dromara.system.domain.SysOss;
import org.dromara.system.domain.bo.SysOssBo;
import org.dromara.system.domain.vo.SysOssVo;
import org.dromara.system.mapper.SysOssMapper;
import org.dromara.system.config.FileCacheConfig;
import org.dromara.system.service.ISysOssService;
import org.jetbrains.annotations.NotNull;
import org.springframework.cache.annotation.Cacheable;
@@ -36,11 +37,23 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.io.BufferedOutputStream;
import java.io.BufferedInputStream;
import java.io.OutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* 文件上传 服务层实现
@@ -52,6 +65,61 @@ import java.util.Map;
public class SysOssServiceImpl implements ISysOssService, OssService {
private final SysOssMapper baseMapper;
private final FileCacheConfig fileCacheConfig;
// 🚀 LRU缓存实现
private final ConcurrentHashMap<Long, CacheEntry> fileCache = new ConcurrentHashMap<>();
private final ReentrantReadWriteLock cacheLock = new ReentrantReadWriteLock();
private final AtomicLong currentCacheSize = new AtomicLong(0);
private final AtomicLong cacheHits = new AtomicLong(0);
private final AtomicLong cacheMisses = new AtomicLong(0);
/**
* 🚀 缓存条目类
*/
private class CacheEntry {
private final byte[] data;
private final long size;
private final long createTime;
private final long lastAccessTime;
private final String contentType;
public CacheEntry(byte[] data, String contentType) {
this.data = data;
this.size = data.length;
this.createTime = System.currentTimeMillis();
this.lastAccessTime = System.currentTimeMillis();
this.contentType = contentType;
}
public byte[] getData() {
return data;
}
public long getSize() {
return size;
}
public long getCreateTime() {
return createTime;
}
public long getLastAccessTime() {
return lastAccessTime;
}
public String getContentType() {
return contentType;
}
public void updateAccessTime() {
// 注意:这里需要同步访问,但为了性能考虑,我们使用volatile或者重新创建对象
}
public boolean isExpired() {
return System.currentTimeMillis() - createTime > fileCacheConfig.getExpireTimeMs();
}
}
/**
* 查询OSS对象存储列表
@@ -83,9 +151,12 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
SysOssVo vo = ossService.getById(id);
if (ObjectUtil.isNotNull(vo)) {
try {
list.add(this.matchingUrl(vo));
// 使用安全文件访问URL替代直接OSS URL
String secureUrl = this.getSecureFileUrl(id);
vo.setUrl(secureUrl);
list.add(vo);
} catch (Exception ignored) {
// 如果oss异常无法连接则将数据直接返回
// 如果安全访问失败则将数据直接返回
list.add(vo);
}
}
@@ -143,7 +214,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
lqw.eq(StringUtils.isNotBlank(bo.getFileSuffix()), SysOss::getFileSuffix, bo.getFileSuffix());
lqw.eq(StringUtils.isNotBlank(bo.getUrl()), SysOss::getUrl, bo.getUrl());
lqw.between(params.get("beginCreateTime") != null && params.get("endCreateTime") != null,
SysOss::getCreateTime, params.get("beginCreateTime"), params.get("endCreateTime"));
SysOss::getCreateTime, params.get("beginCreateTime"), params.get("endCreateTime"));
lqw.eq(ObjectUtil.isNotNull(bo.getCreateBy()), SysOss::getCreateBy, bo.getCreateBy());
lqw.eq(StringUtils.isNotBlank(bo.getService()), SysOss::getService, bo.getService());
lqw.orderByAsc(SysOss::getOssId);
@@ -162,7 +233,6 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
return baseMapper.selectVoById(ossId);
}
/**
* 文件下载方法,支持一次性下载完整文件
*
@@ -176,9 +246,26 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
throw new ServiceException("文件数据不存在!");
}
FileUtils.setAttachmentResponseHeader(response, sysOss.getOriginalName());
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE + "; charset=UTF-8");
OssClient storage = OssFactory.instance(sysOss.getService());
storage.download(sysOss.getFileName(), response.getOutputStream(), response::setContentLengthLong);
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
// 添加缓存控制头,防止浏览器缓存干扰
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "0");
try {
OssClient storage = OssFactory.instance(sysOss.getService());
storage.download(sysOss.getFileName(), response.getOutputStream(), contentLength -> {
if (contentLength != null && contentLength > 0) {
response.setContentLengthLong(contentLength);
}
});
// 确保输出流刷新
response.getOutputStream().flush();
} catch (Exception e) {
throw new ServiceException("文件下载失败: " + e.getMessage());
}
}
/**
@@ -191,7 +278,8 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
@Override
public SysOssVo upload(MultipartFile file) {
String originalfileName = file.getOriginalFilename();
String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."),
originalfileName.length());
OssClient storage = OssFactory.instance();
UploadResult uploadResult;
try {
@@ -212,7 +300,8 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
@Override
public SysOssVo upload(File file) {
String originalfileName = file.getName();
String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."),
originalfileName.length());
OssClient storage = OssFactory.instance();
UploadResult uploadResult = storage.uploadSuffix(file, suffix);
// 保存文件信息
@@ -220,7 +309,8 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
}
@NotNull
private SysOssVo buildResultEntity(String originalfileName, String suffix, String configKey, UploadResult uploadResult) {
private SysOssVo buildResultEntity(String originalfileName, String suffix, String configKey,
UploadResult uploadResult) {
SysOss oss = new SysOss();
oss.setUrl(uploadResult.getUrl());
oss.setFileSuffix(suffix);
@@ -255,6 +345,9 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
/**
* 桶类型为 private 的URL 修改为临时URL时长为120s
*
* ⚠️ 注意:此方法仅用于内部展示,不推荐直接暴露给前端
* 建议使用 getSecureFileUrl() 方法进行安全访问
*
* @param oss OSS对象
* @return oss 匹配Url的OSS对象
*/
@@ -266,4 +359,562 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
}
return oss;
}
/**
* 🔐 安全文件访问URL生成(推荐使用)
* 通过后端代理进行权限验证,确保只有登录用户才能访问
*
* @param ossId 文件ID
* @return 安全访问URL(通过后端代理)
*/
public String getSecureFileUrl(Long ossId) {
// 权限验证
if (!checkFileAccessPermission(ossId)) {
throw new ServiceException("无权限访问该文件");
}
// 记录访问日志
try {
Long userId = LoginHelper.getUserId();
String ipAddress = ServletUtils.getClientIP();
String userAgent = ServletUtils.getRequest().getHeader("User-Agent");
logFileAccess(ossId, userId, "SECURE_ACCESS", ipAddress, userAgent);
} catch (Exception e) {
// 日志记录失败不影响主流程
}
// 返回后端代理URL,而不是直接返回OSS URL
return "/resource/oss/download/" + ossId;
}
/**
* 🔐 安全文件下载(通过后端代理)- 性能优化版本
* 强制权限验证,确保只有登录用户才能下载
*
* 🚀 性能优化特性:
* - 异步分块下载
* - 大文件缓冲区优化
* - 并发传输提升
* - 断点续传支持
* - 🆕 内存缓存机制
*
* @param ossId 文件ID
* @param response HTTP响应对象
*/
public void secureDownload(Long ossId, HttpServletResponse response) throws IOException {
// 强制权限验证
if (!checkFileAccessPermission(ossId)) {
throw new ServiceException("无权限下载该文件");
}
SysOssVo sysOss = getById(ossId);
if (ObjectUtil.isNull(sysOss)) {
throw new ServiceException("文件不存在");
}
// 记录下载日志
try {
Long userId = LoginHelper.getUserId();
String ipAddress = ServletUtils.getClientIP();
String userAgent = ServletUtils.getRequest().getHeader("User-Agent");
logFileAccess(ossId, userId, "DOWNLOAD", ipAddress, userAgent);
} catch (Exception e) {
// 日志记录失败不影响主流程
}
// 🔧 修复:设置正确的响应头,移除charset避免二进制文件问题
FileUtils.setAttachmentResponseHeader(response, sysOss.getOriginalName());
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
// 🔧 修复:禁用缓存,确保文件下载正确处理
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "0");
// 🚀 新增:支持断点续传
String rangeHeader = ServletUtils.getRequest().getHeader("Range");
if (StringUtils.isNotBlank(rangeHeader)) {
handleRangeDownload(sysOss, response, rangeHeader);
return;
}
try {
// 🚀 优化:优先从缓存获取,缓存未命中则从OSS下载
if (!serveFromCache(ossId, sysOss, response)) {
// 缓存未命中,从OSS下载并缓存
performOptimizedDownloadWithCache(ossId, sysOss, response);
}
} catch (Exception e) {
// 🔧 修复:更详细的错误日志
throw new ServiceException("文件下载失败: " + e.getMessage());
}
}
/**
* 🚀 从缓存中提供文件服务
*
* @param ossId 文件ID
* @param sysOss 文件信息
* @param response HTTP响应
* @return 是否从缓存提供服务
*/
private boolean serveFromCache(Long ossId, SysOssVo sysOss, HttpServletResponse response) throws IOException {
cacheLock.readLock().lock();
try {
CacheEntry entry = fileCache.get(ossId);
if (entry != null && !entry.isExpired()) {
// 缓存命中
cacheHits.incrementAndGet();
// 设置响应头
response.setContentLengthLong(entry.getSize());
if (StringUtils.isNotBlank(entry.getContentType())) {
response.setContentType(entry.getContentType());
}
// 输出文件内容
try (BufferedOutputStream bufferedOut = new BufferedOutputStream(response.getOutputStream(),
8 * 1024 * 1024)) {
bufferedOut.write(entry.getData());
bufferedOut.flush();
}
return true;
}
} finally {
cacheLock.readLock().unlock();
}
// 缓存未命中
cacheMisses.incrementAndGet();
return false;
}
/**
* 🚀 高性能文件下载实现(带缓存)
*
* @param ossId 文件ID
* @param sysOss 文件信息
* @param response HTTP响应
*/
private void performOptimizedDownloadWithCache(Long ossId, SysOssVo sysOss, HttpServletResponse response)
throws IOException {
OssClient storage = OssFactory.instance(sysOss.getService());
// 🚀 优化:使用ByteArrayOutputStream收集数据用于缓存
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
BufferedOutputStream bufferedOut = new BufferedOutputStream(response.getOutputStream(),
8 * 1024 * 1024)) {
// 🚀 优化:异步下载并流式传输,同时收集数据
storage.download(sysOss.getFileName(), new OutputStream() {
@Override
public void write(int b) throws IOException {
baos.write(b);
bufferedOut.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
baos.write(b, off, len);
bufferedOut.write(b, off, len);
}
@Override
public void flush() throws IOException {
baos.flush();
bufferedOut.flush();
}
}, contentLength -> {
// 🔧 修复:确保Content-Length正确设置
if (contentLength != null && contentLength > 0) {
response.setContentLengthLong(contentLength);
}
});
// 🔧 修复:确保输出流刷新
bufferedOut.flush();
// 🚀 新增:将文件数据添加到缓存
byte[] fileData = baos.toByteArray();
addToCache(ossId, fileData, sysOss.getOriginalName());
}
}
/**
* 🚀 添加文件到缓存
*
* @param ossId 文件ID
* @param fileData 文件数据
* @param fileName 文件名
*/
private void addToCache(Long ossId, byte[] fileData, String fileName) {
// 检查文件大小是否适合缓存
if (fileData.length > fileCacheConfig.getMaxSingleFileSize()) {
return;
}
cacheLock.writeLock().lock();
try {
// 检查缓存大小限制
if (currentCacheSize.get() + fileData.length > fileCacheConfig.getMaxCacheSizeBytes()) {
// 缓存已满,执行LRU淘汰
evictLRUEntries();
}
// 如果仍然超出限制,不缓存
if (currentCacheSize.get() + fileData.length > fileCacheConfig.getMaxCacheSizeBytes()) {
return;
}
// 确定Content-Type
String contentType = determineContentType(fileName);
// 创建缓存条目
CacheEntry entry = new CacheEntry(fileData, contentType);
// 添加到缓存
fileCache.put(ossId, entry);
currentCacheSize.addAndGet(fileData.length);
// 清理过期条目
cleanupExpiredEntries();
} finally {
cacheLock.writeLock().unlock();
}
}
/**
* 🚀 LRU淘汰策略
*/
private void evictLRUEntries() {
// 按最后访问时间排序,淘汰最久未访问的条目
List<Map.Entry<Long, CacheEntry>> entries = new ArrayList<>(fileCache.entrySet());
entries.sort((e1, e2) -> Long.compare(e1.getValue().getLastAccessTime(), e2.getValue().getLastAccessTime()));
// 淘汰最旧的20%条目
int evictCount = Math.max(1, entries.size() / 5);
for (int i = 0; i < evictCount && i < entries.size(); i++) {
Map.Entry<Long, CacheEntry> entry = entries.get(i);
fileCache.remove(entry.getKey());
currentCacheSize.addAndGet(-entry.getValue().getSize());
}
}
/**
* 🚀 清理过期条目
*/
private void cleanupExpiredEntries() {
List<Long> expiredKeys = new ArrayList<>();
for (Map.Entry<Long, CacheEntry> entry : fileCache.entrySet()) {
if (entry.getValue().isExpired()) {
expiredKeys.add(entry.getKey());
}
}
for (Long key : expiredKeys) {
CacheEntry entry = fileCache.remove(key);
if (entry != null) {
currentCacheSize.addAndGet(-entry.getSize());
}
}
}
/**
* 🚀 根据文件名确定Content-Type
*
* @param fileName 文件名
* @return Content-Type
*/
private String determineContentType(String fileName) {
if (StringUtils.isBlank(fileName)) {
return MediaType.APPLICATION_OCTET_STREAM_VALUE;
}
String extension = StringUtils.substringAfterLast(fileName, ".").toLowerCase();
switch (extension) {
case "jpg":
case "jpeg":
return "image/jpeg";
case "png":
return "image/png";
case "gif":
return "image/gif";
case "pdf":
return "application/pdf";
case "doc":
return "application/msword";
case "docx":
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
case "xls":
return "application/vnd.ms-excel";
case "xlsx":
return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
case "txt":
return "text/plain";
case "zip":
return "application/zip";
case "rar":
return "application/x-rar-compressed";
default:
return MediaType.APPLICATION_OCTET_STREAM_VALUE;
}
}
/**
* 🚀 获取缓存统计信息
*
* @return 缓存统计信息
*/
public Map<String, Object> getCacheStats() {
Map<String, Object> stats = new ConcurrentHashMap<>();
stats.put("cacheSize", fileCache.size());
stats.put("currentCacheSizeBytes", currentCacheSize.get());
stats.put("maxCacheSizeBytes", fileCacheConfig.getMaxCacheSizeBytes());
stats.put("cacheHits", cacheHits.get());
stats.put("cacheMisses", cacheMisses.get());
long totalRequests = cacheHits.get() + cacheMisses.get();
double hitRate = totalRequests > 0 ? (double) cacheHits.get() / totalRequests * 100 : 0;
stats.put("hitRate", String.format("%.2f%%", hitRate));
return stats;
}
/**
* 🚀 清空缓存
*/
public void clearCache() {
cacheLock.writeLock().lock();
try {
fileCache.clear();
currentCacheSize.set(0);
cacheHits.set(0);
cacheMisses.set(0);
} finally {
cacheLock.writeLock().unlock();
}
}
/**
* 🚀 从缓存中移除指定文件
*
* @param ossId 文件ID
*/
public void removeFromCache(Long ossId) {
cacheLock.writeLock().lock();
try {
CacheEntry entry = fileCache.remove(ossId);
if (entry != null) {
currentCacheSize.addAndGet(-entry.getSize());
}
} finally {
cacheLock.writeLock().unlock();
}
}
/**
* 🚀 断点续传支持
*
* @param sysOss 文件信息
* @param response HTTP响应
* @param rangeHeader Range请求头
*/
private void handleRangeDownload(SysOssVo sysOss, HttpServletResponse response, String rangeHeader)
throws IOException {
// 解析Range头
String[] ranges = rangeHeader.substring("bytes=".length()).split("-");
long start = Long.parseLong(ranges[0]);
long end = ranges.length > 1 && StringUtils.isNotBlank(ranges[1]) ? Long.parseLong(ranges[1]) : -1;
// 设置206状态码和Content-Range头
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
response.setHeader("Accept-Ranges", "bytes");
// 计算实际内容长度
long contentLength = end - start + 1;
response.setHeader("Content-Range", "bytes " + start + "-" + end + "/*");
response.setContentLengthLong(contentLength);
// 🚀 优化:分块下载指定范围
OssClient storage = OssFactory.instance(sysOss.getService());
try (BufferedOutputStream bufferedOut = new BufferedOutputStream(response.getOutputStream(), 8 * 1024 * 1024)) {
// 使用分块下载API(需要OssClient支持)
downloadRange(storage, sysOss.getFileName(), start, end, bufferedOut);
bufferedOut.flush();
}
}
/**
* 🚀 分块下载实现
*
* @param storage OSS客户端
* @param fileName 文件名
* @param start 开始位置
* @param end 结束位置
* @param outputStream 输出流
*/
private void downloadRange(OssClient storage, String fileName, long start, long end, OutputStream outputStream) {
// 这里需要根据具体的OSS客户端实现来调用分块下载
// 目前先使用完整下载,后续可以扩展为真正的分块下载
try {
storage.download(fileName, outputStream, null);
} catch (Exception e) {
throw new ServiceException("分块下载失败: " + e.getMessage());
}
}
/**
* 🚀 批量文件下载优化
* 支持多个文件打包下载
*
* @param ossIds 文件ID列表
* @param response HTTP响应
*/
public void batchDownload(Collection<Long> ossIds, HttpServletResponse response) throws IOException {
if (ossIds == null || ossIds.isEmpty()) {
throw new ServiceException("请选择要下载的文件");
}
// 权限验证
for (Long ossId : ossIds) {
if (!checkFileAccessPermission(ossId)) {
throw new ServiceException("无权限下载文件ID: " + ossId);
}
}
// 设置ZIP响应头
String zipFileName = "files_" + System.currentTimeMillis() + ".zip";
FileUtils.setAttachmentResponseHeader(response, zipFileName);
response.setContentType("application/zip");
// 🚀 优化:使用ZIP流式输出,避免内存溢出
try (ZipOutputStream zipOut = new ZipOutputStream(
new BufferedOutputStream(response.getOutputStream(), 8 * 1024 * 1024))) {
for (Long ossId : ossIds) {
SysOssVo sysOss = getById(ossId);
if (sysOss != null) {
addFileToZip(sysOss, zipOut);
}
}
zipOut.flush();
}
}
/**
* 🚀 添加文件到ZIP流
*
* @param sysOss 文件信息
* @param zipOut ZIP输出流
*/
private void addFileToZip(SysOssVo sysOss, ZipOutputStream zipOut) throws IOException {
ZipEntry zipEntry = new ZipEntry(sysOss.getOriginalName());
zipOut.putNextEntry(zipEntry);
OssClient storage = OssFactory.instance(sysOss.getService());
// 🚀 优化:使用缓冲流提升性能
try (BufferedInputStream bufferedIn = new BufferedInputStream(
storage.getObjectContent(sysOss.getFileName()), 8 * 1024 * 1024)) {
byte[] buffer = new byte[8 * 1024 * 1024]; // 8MB缓冲区
int bytesRead;
while ((bytesRead = bufferedIn.read(buffer)) != -1) {
zipOut.write(buffer, 0, bytesRead);
}
}
zipOut.closeEntry();
}
/**
* 🔐 增强版权限检查
* 包含用户身份验证、文件访问权限、业务权限等多重验证
*
* @param ossId 文件ID
* @return 是否有权限访问
*/
@Override
public boolean checkFileAccessPermission(Long ossId) {
// 1. 基础检查:文件必须存在
SysOssVo oss = getById(ossId);
if (ObjectUtil.isNull(oss)) {
return false;
}
// 2. 用户身份验证:必须已登录
try {
Long userId = LoginHelper.getUserId();
if (ObjectUtil.isNull(userId)) {
return false;
}
} catch (Exception e) {
return false;
}
// 3. 文件所有者检查:只有文件上传者可以访问
// 可以根据业务需求调整此逻辑
// if (ObjectUtil.isNotNull(oss.getCreateBy())) {
// Long currentUserId = LoginHelper.getUserId();
// if (!oss.getCreateBy().equals(currentUserId)) {
// // 如果不是文件所有者,检查是否有超级管理员权限
// if (!LoginHelper.isSuperAdmin()) {
// return false;
// }
// }
// }
// 4. 业务权限检查(可根据具体业务需求扩展)
// 例如:检查用户是否有访问该文件所属模块的权限
// 这里可以添加更复杂的业务逻辑
return true;
}
/**
* 记录文件访问日志
*
* @param ossId 文件ID
* @param userId 用户ID
* @param accessType 访问类型
* @param ipAddress IP地址
* @param userAgent 用户代理
*/
@Override
public void logFileAccess(Long ossId, Long userId, String accessType, String ipAddress, String userAgent) {
try {
// TODO: 这里可以调用文件访问日志服务记录访问信息
// 目前暂时只打印日志,后续可以完善为数据库记录
// log.info("文件访问日志 - 文件ID: {}, 用户ID: {}, 访问类型: {}, IP: {}, UserAgent: {}",
// ossId, userId, accessType, ipAddress, userAgent);
} catch (Exception e) {
// log.warn("记录文件访问日志失败: {}", e.getMessage());
}
}
/**
* 获取文件临时访问URL(带权限检查)
*
* @param ossId 文件ID
* @param expireSeconds 过期时间(秒)
* @return 临时访问URL
*/
@Override
public String getFileAccessUrl(Long ossId, Integer expireSeconds) {
SysOssVo oss = getById(ossId);
if (ObjectUtil.isNull(oss)) {
throw new ServiceException("文件不存在");
}
if (!checkFileAccessPermission(ossId)) {
throw new ServiceException("无权限访问该文件");
}
OssClient storage = OssFactory.instance(oss.getService());
return storage.getPrivateUrl(oss.getFileName(), Duration.ofSeconds(expireSeconds));
}
}
@@ -0,0 +1,184 @@
package org.dromara.system.service.impl;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.dromara.system.domain.bo.SysRegionBo;
import org.dromara.system.domain.vo.SysRegionVo;
import org.dromara.system.domain.SysRegion;
import org.dromara.system.mapper.SysRegionMapper;
import org.dromara.system.service.ISysRegionService;
import java.util.List;
import java.util.Map;
import java.util.Collection;
import java.util.stream.Collectors;
import java.util.ArrayList;
/**
* 行政区划Service业务层处理
*
* @author zlwu
* @date 2025-07-26
*/
@Slf4j
@RequiredArgsConstructor
@Service
public class SysRegionServiceImpl implements ISysRegionService {
private final SysRegionMapper baseMapper;
/**
* 查询行政区划
*
* @param id 主键
* @return 行政区划
*/
@Override
public SysRegionVo queryById(Long id) {
return baseMapper.selectVoById(id);
}
/**
* 分页查询行政区划列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 行政区划分页列表
*/
@Override
public TableDataInfo<SysRegionVo> queryPageList(SysRegionBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<SysRegion> lqw = buildQueryWrapper(bo);
Page<SysRegionVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询符合条件的行政区划列表
*
* @param bo 查询条件
* @return 行政区划列表
*/
@Override
public List<SysRegionVo> queryList(SysRegionBo bo) {
LambdaQueryWrapper<SysRegion> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<SysRegion> buildQueryWrapper(SysRegionBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<SysRegion> lqw = Wrappers.lambdaQuery();
lqw.orderByAsc(SysRegion::getId);
lqw.eq(StringUtils.isNotBlank(bo.getCode()), SysRegion::getCode, bo.getCode());
lqw.like(StringUtils.isNotBlank(bo.getName()), SysRegion::getName, bo.getName());
lqw.like(StringUtils.isNotBlank(bo.getShortName()), SysRegion::getShortName, bo.getShortName());
lqw.eq(bo.getParentCode() != null, SysRegion::getParentCode, bo.getParentCode());
lqw.eq(bo.getLevel() != null, SysRegion::getLevel, bo.getLevel());
lqw.eq(bo.getSort() != null, SysRegion::getSort, bo.getSort());
lqw.eq(StringUtils.isNotBlank(bo.getStatus()), SysRegion::getStatus, bo.getStatus());
lqw.eq(bo.getLongitude() != null, SysRegion::getLongitude, bo.getLongitude());
lqw.eq(bo.getLatitude() != null, SysRegion::getLatitude, bo.getLatitude());
lqw.eq(bo.getDeleted() != null, SysRegion::getDeleted, bo.getDeleted());
return lqw;
}
/**
* 新增行政区划
*
* @param bo 行政区划
* @return 是否新增成功
*/
@Override
public Boolean insertByBo(SysRegionBo bo) {
SysRegion add = MapstructUtils.convert(bo, SysRegion.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
}
return flag;
}
/**
* 修改行政区划
*
* @param bo 行政区划
* @return 是否修改成功
*/
@Override
public Boolean updateByBo(SysRegionBo bo) {
SysRegion update = MapstructUtils.convert(bo, SysRegion.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(SysRegion entity) {
// TODO 做一些数据校验,如唯一约束
}
/**
* 校验并批量删除行政区划信息
*
* @param ids 待删除的主键集合
* @param isValid 是否进行有效性校验
* @return 是否删除成功
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
// TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteByIds(ids) > 0;
}
/**
* 获取省市区树形数据
*
* @return 省市区树形数据列表
*/
@Override
public List<SysRegionVo> getRegionTree() {
// 查询所有正常状态且未删除的行政区划数据
LambdaQueryWrapper<SysRegion> lqw = Wrappers.lambdaQuery();
lqw.eq(SysRegion::getDeleted, 0);
lqw.eq(SysRegion::getStatus, "0");
lqw.orderByAsc(SysRegion::getLevel, SysRegion::getSort, SysRegion::getId);
List<SysRegionVo> allRegions = baseMapper.selectVoList(lqw);
// 构建树形结构
return buildRegionTree(allRegions, 0L);
}
/**
* 构建树形结构
*
* @param allRegions 所有行政区划数据
* @param parentCode 父级编码
* @return 树形结构数据
*/
private List<SysRegionVo> buildRegionTree(List<SysRegionVo> allRegions, Long parentCode) {
List<SysRegionVo> treeList = new ArrayList<>();
for (SysRegionVo region : allRegions) {
if (parentCode.equals(region.getParentCode())) {
// 递归查找子节点
List<SysRegionVo> children = buildRegionTree(allRegions, region.getId());
region.setChildren(children);
treeList.add(region);
}
}
return treeList;
}
}
@@ -24,14 +24,17 @@ import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.system.domain.*;
import org.dromara.system.domain.bo.SysUserBo;
import org.dromara.system.domain.vo.SysOssVo;
import org.dromara.system.domain.vo.SysPostVo;
import org.dromara.system.domain.vo.SysRoleVo;
import org.dromara.system.domain.vo.SysUserExportVo;
import org.dromara.system.domain.vo.SysUserVo;
import org.dromara.system.mapper.*;
import org.dromara.system.service.ISysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -54,6 +57,10 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
private final SysPostMapper postMapper;
private final SysUserRoleMapper userRoleMapper;
private final SysUserPostMapper userPostMapper;
private final SysOssMapper ossMapper;
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public TableDataInfo<SysUserVo> selectPageUserList(SysUserBo user, PageQuery pageQuery) {
@@ -383,14 +390,41 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
*/
@CacheEvict(cacheNames = CacheNames.SYS_NICKNAME, key = "#user.userId")
@Override
@Transactional(rollbackFor = Exception.class)
public int updateUserProfile(SysUserBo user) {
return baseMapper.update(null,
int result = baseMapper.update(null,
new LambdaUpdateWrapper<SysUser>()
.set(ObjectUtil.isNotNull(user.getNickName()), SysUser::getNickName, user.getNickName())
.set(SysUser::getPhonenumber, user.getPhonenumber())
.set(SysUser::getEmail, user.getEmail())
.set(SysUser::getSex, user.getSex())
.eq(SysUser::getUserId, user.getUserId()));
if (result > 0) {
SysUserVo user1 = baseMapper.selectVoById(user.getUserId());
if (user1.getAccountType().equals("2")) {
// 判断账号类型为学生(accountType = "2")时,同步更新学生信息表
try {
String updateSql = "UPDATE xg_student_msg SET " +
"name = ?, email = ?, gender = ?, phone = ?, update_time = NOW() " +
"WHERE id = ? AND deleted = 0";
jdbcTemplate.update(updateSql,
user.getNickName(), // 昵称对应学生姓名
user.getEmail(), // 邮箱
user.getSex(), // 性别
user.getPhonenumber(), // 手机号
user.getUserId() // 用户ID对应学生ID
);
log.info("学生用户基本信息同步更新成功,用户ID: {}", user.getUserId());
} catch (Exception e) {
log.error("学生用户基本信息同步更新失败,用户ID: {}, 错误信息: {}", user.getUserId(), e.getMessage());
// 这里不抛出异常,避免影响用户基本信息的更新
}
}
}
return result;
}
/**
@@ -401,11 +435,41 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateUserAvatar(Long userId, Long avatar) {
return baseMapper.update(null,
boolean result = baseMapper.update(null,
new LambdaUpdateWrapper<SysUser>()
.set(SysUser::getAvatar, avatar)
.eq(SysUser::getUserId, userId)) > 0;
// 判断账号类型为学生(accountType = "2")时,同步更新学生信息表
if (result) {
SysUserVo user = baseMapper.selectVoById(userId);
if (user.getAccountType().equals("2")) {
// 通过avatar丛sys_oss表获取url信息
SysOssVo oss = ossMapper.selectVoById(avatar);
String url = oss.getUrl();
try {
// 更新学生信息表 xg_student_msg 的 photo_path 字段 和 photo_url 字段
String updateSql = "UPDATE xg_student_msg SET " +
"photo_path = ?, photo_url = ? " +
"WHERE id = ? AND deleted = 0";
jdbcTemplate.update(updateSql,
avatar, // 头像ossid
url, // 头像url
user.getUserId() // 用户ID对应学生ID
);
log.info("学生用户头像同步更新成功,用户ID: {}", user.getUserId());
} catch (Exception e) {
log.error("学生用户头像同步更新失败,用户ID: {}, 错误信息: {}", user.getUserId(), e.getMessage());
// 这里不抛出异常,避免影响用户头像的更新
}
}
}
return result;
}
/**
@@ -4,4 +4,25 @@
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.system.mapper.SysDictDataMapper">
<!-- 绕过租户过滤查询字典数据 -->
<select id="selectDictDataByTypeIgnoreTenant" resultType="org.dromara.system.domain.vo.SysDictDataVo">
SELECT dict_code,
dict_sort,
dict_label,
dict_value,
dict_type,
css_class,
list_class,
is_default,
create_dept,
create_by,
create_time,
update_by,
update_time,
remark
FROM sys_dict_data
WHERE dict_type = #{dictType}
ORDER BY dict_sort ASC
</select>
</mapper>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.system.mapper.SysRegionMapper">
</mapper>
@@ -18,7 +18,7 @@
</if>
<if test="ew.getSqlSelect == null">
u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex,
u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark
u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark,u.account_type
</if>
from sys_user u
${ew.getCustomSqlSegment}
@@ -31,7 +31,7 @@
</if>
<if test="ew.getSqlSelect == null">
u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex,
u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark
u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark,u.account_type
</if>
from sys_user u
${ew.getCustomSqlSegment}
@@ -40,7 +40,7 @@
<select id="selectUserExportList" resultMap="SysUserExportResult">
select u.user_id, u.dept_id, u.nick_name, u.user_name, u.email, u.avatar, u.phonenumber, u.sex,
u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark,
d.dept_name, d.leader, u1.user_name as leaderName
d.dept_name, d.leader, u1.user_name as leaderName,u.account_type
from sys_user u
left join sys_dept d on u.dept_id = d.dept_id
left join sys_user u1 on u1.user_id = d.leader
@@ -48,7 +48,7 @@
</select>
<select id="selectAllocatedList" resultMap="SysUserResult">
select distinct u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.phonenumber, u.status, u.create_time
select distinct u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.phonenumber, u.status, u.create_time,u.account_type
from sys_user u
left join sys_dept d on u.dept_id = d.dept_id
left join sys_user_role sur on u.user_id = sur.user_id
@@ -57,7 +57,7 @@
</select>
<select id="selectUnallocatedList" resultMap="SysUserResult">
select distinct u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.phonenumber, u.status, u.create_time
select distinct u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.phonenumber, u.status, u.create_time,u.account_type
from sys_user u
left join sys_dept d on u.dept_id = d.dept_id
left join sys_user_role sur on u.user_id = sur.user_id
@@ -66,8 +66,7 @@ public class TestLeaveController extends BaseController {
*/
@SaCheckPermission("workflow:leave:query")
@GetMapping("/{id}")
public R<TestLeaveVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
public R<TestLeaveVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
return R.ok(testLeaveService.queryById(id));
}
@@ -101,8 +100,7 @@ public class TestLeaveController extends BaseController {
@SaCheckPermission("workflow:leave:remove")
@Log(title = "请假", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
return toAjax(testLeaveService.deleteWithValidByIds(List.of(ids)));
}
}
@@ -101,12 +101,13 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
}
// 启动流程实例(提交申请)
Map<String, Object> variables = startProcessBo.getVariables();
log.info("流程变量variables:{}", variables);
// 流程发起人
variables.put(INITIATOR, LoginHelper.getUserIdStr());
// 业务id
variables.put(BUSINESS_ID, businessId);
FlowInstance flowInstance = flowInstanceMapper.selectOne(new LambdaQueryWrapper<>(FlowInstance.class)
.eq(FlowInstance::getBusinessId, businessId));
.eq(FlowInstance::getBusinessId, businessId));
if (ObjectUtil.isNotNull(flowInstance)) {
BusinessStatusEnum.checkStartStatus(flowInstance.getFlowStatus());
List<Task> taskList = taskService.list(new FlowTask().setInstanceId(flowInstance.getId()));
@@ -118,9 +119,9 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
return dto;
}
FlowParams flowParams = FlowParams.build()
.flowCode(startProcessBo.getFlowCode())
.variable(startProcessBo.getVariables())
.flowStatus(BusinessStatusEnum.DRAFT.getStatus());
.flowCode(startProcessBo.getFlowCode())
.variable(startProcessBo.getVariables())
.flowStatus(BusinessStatusEnum.DRAFT.getStatus());
Instance instance;
try {
instance = insService.start(businessId, flowParams);
@@ -160,7 +161,6 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
// 消息通知
completeTaskBo.getVariables().put(FlowConstant.MESSAGE_NOTICE, notice);
FlowTask flowTask = flowTaskMapper.selectById(taskId);
if (ObjectUtil.isNull(flowTask)) {
throw new ServiceException("流程任务不存在或任务已审批!");
@@ -177,12 +177,12 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
}
// 构建流程参数,包括变量、跳转类型、消息、处理人、权限等信息
FlowParams flowParams = FlowParams.build()
.variable(completeTaskBo.getVariables())
.skipType(SkipType.PASS.getKey())
.message(completeTaskBo.getMessage())
.flowStatus(BusinessStatusEnum.WAITING.getStatus())
.hisStatus(TaskStatusEnum.PASS.getStatus())
.hisTaskExt(completeTaskBo.getFileId());
.variable(completeTaskBo.getVariables())
.skipType(SkipType.PASS.getKey())
.message(completeTaskBo.getMessage())
.flowStatus(BusinessStatusEnum.WAITING.getStatus())
.hisStatus(TaskStatusEnum.PASS.getStatus())
.hisTaskExt(completeTaskBo.getFileId());
// 执行任务跳转,并根据返回的处理人设置下一步处理人
taskService.skip(taskId, flowParams);
return true;
@@ -208,9 +208,9 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
String userIds = variablesMap.get(entry.getKey()).toString();
if (StringUtils.isNotBlank(userIds)) {
Set<String> hashSet = new HashSet<>();
//弹窗传入的选人
// 弹窗传入的选人
List<String> popUserIds = Arrays.asList(entry.getValue().toString().split(StringUtils.SEPARATOR));
//已有的选人
// 已有的选人
List<String> variableUserIds = Arrays.asList(userIds.split(StringUtils.SEPARATOR));
hashSet.addAll(popUserIds);
hashSet.addAll(variableUserIds);
@@ -235,31 +235,33 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
return;
}
// 添加抄送人记录
FlowHisTask flowHisTask = flowHisTaskMapper.selectList(new LambdaQueryWrapper<>(FlowHisTask.class).eq(FlowHisTask::getTaskId, task.getId())).get(0);
FlowHisTask flowHisTask = flowHisTaskMapper
.selectList(new LambdaQueryWrapper<>(FlowHisTask.class).eq(FlowHisTask::getTaskId, task.getId()))
.get(0);
FlowNode flowNode = new FlowNode();
flowNode.setNodeCode(flowHisTask.getTargetNodeCode());
flowNode.setNodeName(flowHisTask.getTargetNodeName());
//生成新的任务id
// 生成新的任务id
long taskId = identifierGenerator.nextId(null).longValue();
task.setId(taskId);
task.setNodeName("【抄送】" + task.getNodeName());
Date updateTime = new Date(flowHisTask.getUpdateTime().getTime() - 1000);
FlowParams flowParams = FlowParams.build()
.skipType(SkipType.NONE.getKey())
.hisStatus(TaskStatusEnum.COPY.getStatus())
.message("【抄送给】" + StreamUtils.join(flowCopyList, FlowCopyBo::getUserName));
.skipType(SkipType.NONE.getKey())
.hisStatus(TaskStatusEnum.COPY.getStatus())
.message("【抄送给】" + StreamUtils.join(flowCopyList, FlowCopyBo::getUserName));
HisTask hisTask = hisTaskService.setSkipHisTask(task, flowNode, flowParams);
hisTask.setCreateTime(updateTime);
hisTask.setUpdateTime(updateTime);
hisTaskService.save(hisTask);
List<User> userList = flowCopyList.stream()
.map(flowCopy -> {
FlowUser flowUser = new FlowUser();
flowUser.setType(TaskAssigneeType.COPY.getCode());
flowUser.setProcessedBy(String.valueOf(flowCopy.getUserId()));
flowUser.setAssociated(taskId);
return flowUser;
}).collect(Collectors.toList());
.map(flowCopy -> {
FlowUser flowUser = new FlowUser();
flowUser.setType(TaskAssigneeType.COPY.getCode());
flowUser.setProcessedBy(String.valueOf(flowCopy.getUserId()));
flowUser.setAssociated(taskId);
return flowUser;
}).collect(Collectors.toList());
// 批量保存抄送人员
FlowEngine.userService().saveBatch(userList);
}
@@ -361,7 +363,8 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
wrapper.like(StringUtils.isNotBlank(flowTaskBo.getFlowCode()), "t.flow_code", flowTaskBo.getFlowCode());
wrapper.in(CollUtil.isNotEmpty(flowTaskBo.getCreateByIds()), "t.create_by", flowTaskBo.getCreateByIds());
if (StringUtils.isNotBlank(flowTaskBo.getCategory())) {
List<Long> categoryIds = flwCategoryMapper.selectCategoryIdsByParentId(Convert.toLong(flowTaskBo.getCategory()));
List<Long> categoryIds = flwCategoryMapper
.selectCategoryIdsByParentId(Convert.toLong(flowTaskBo.getCategory()));
wrapper.in("t.category", StreamUtils.toList(categoryIds, Convert::toStr));
}
wrapper.orderByDesc("t.create_time");
@@ -397,13 +400,14 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
variable.put("notice", notice);
FlowParams flowParams = FlowParams.build()
.nodeCode(bo.getNodeCode())
.variable(variable)
.message(message)
.skipType(SkipType.REJECT.getKey())
.flowStatus(applyNodeCode.equals(bo.getNodeCode()) ? TaskStatusEnum.BACK.getStatus() : TaskStatusEnum.WAITING.getStatus())
.hisStatus(TaskStatusEnum.BACK.getStatus())
.hisTaskExt(bo.getFileId());
.nodeCode(bo.getNodeCode())
.variable(variable)
.message(message)
.skipType(SkipType.REJECT.getKey())
.flowStatus(applyNodeCode.equals(bo.getNodeCode()) ? TaskStatusEnum.BACK.getStatus()
: TaskStatusEnum.WAITING.getStatus())
.hisStatus(TaskStatusEnum.BACK.getStatus())
.hisTaskExt(bo.getFileId());
taskService.skip(task.getId(), flowParams);
return true;
} catch (Exception e) {
@@ -424,12 +428,12 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
if (!CollUtil.isNotEmpty(nodeCodes)) {
return nodeCodes;
}
//判断是否配置了固定驳回节点
// 判断是否配置了固定驳回节点
Node node = nodeCodes.get(0);
if (StringUtils.isNotBlank(node.getAnyNodeSkip())) {
return nodeService.getByNodeCodes(Collections.singletonList(node.getAnyNodeSkip()), definitionId);
}
//获取可驳回的前置节点
// 获取可驳回的前置节点
List<Node> nodes = nodeService.previousNodeList(definitionId, nowNodeCode);
if (CollUtil.isNotEmpty(nodes)) {
return StreamUtils.filter(nodes, e -> NodeType.BETWEEN.getKey().equals(e.getNodeType()));
@@ -456,9 +460,9 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
BusinessStatusEnum.checkInvalidStatus(instance.getFlowStatus());
}
FlowParams flowParams = FlowParams.build()
.message(bo.getComment())
.flowStatus(BusinessStatusEnum.TERMINATION.getStatus())
.hisStatus(TaskStatusEnum.TERMINATION.getStatus());
.message(bo.getComment())
.flowStatus(BusinessStatusEnum.TERMINATION.getStatus())
.hisStatus(TaskStatusEnum.TERMINATION.getStatus());
taskService.termination(taskId, flowParams);
return true;
} catch (Exception e) {
@@ -475,7 +479,7 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
@Override
public List<FlowTask> selectByIdList(List<Long> taskIdList) {
return flowTaskMapper.selectList(new LambdaQueryWrapper<>(FlowTask.class)
.in(FlowTask::getId, taskIdList));
.in(FlowTask::getId, taskIdList));
}
/**
@@ -501,7 +505,7 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
if (ObjectUtil.isNull(flowNode)) {
throw new NullPointerException("当前【" + flowTaskVo.getNodeCode() + "】节点编码不存在");
}
//设置按钮权限
// 设置按钮权限
flowTaskVo.setButtonList(flwNodeExtService.buildButtonPermissionsFromExt(flowNode.getExt()));
flowTaskVo.setNodeRatio(flowNode.getNodeRatio());
flowTaskVo.setApplyNode(flowNode.getNodeCode().equals(flwCommonService.applyNodeCode(task.getDefinitionId())));
@@ -522,24 +526,29 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
Definition definition = defService.getById(task.getDefinitionId());
Map<String, Object> mergeVariable = MapUtil.mergeAll(instance.getVariableMap(), variables);
// 获取下一节点列表
List<Node> nextNodeList = nodeService.getNextNodeList(task.getDefinitionId(), task.getNodeCode(), null, SkipType.PASS.getKey(), mergeVariable);
List<Node> nextNodeList = nodeService.getNextNodeList(task.getDefinitionId(), task.getNodeCode(), null,
SkipType.PASS.getKey(), mergeVariable);
List<FlowNode> nextFlowNodes = BeanUtil.copyToList(nextNodeList, FlowNode.class);
// 只获取中间节点
nextFlowNodes = StreamUtils.filter(nextFlowNodes, node -> NodeType.BETWEEN.getKey().equals(node.getNodeType()));
if (CollUtil.isNotEmpty(nextNodeList)) {
// 构建以下节点数据
List<Task> buildNextTaskList = StreamUtils.toList(nextNodeList, node -> taskService.addTask(node, instance, definition, FlowParams.build()));
List<Task> buildNextTaskList = StreamUtils.toList(nextNodeList,
node -> taskService.addTask(node, instance, definition, FlowParams.build()));
// 办理人变量替换
ExpressionUtil.evalVariable(buildNextTaskList, mergeVariable);
for (FlowNode flowNode : nextFlowNodes) {
buildNextTaskList.stream().filter(t -> t.getNodeCode().equals(flowNode.getNodeCode())).findFirst().ifPresent(t -> {
if (CollUtil.isNotEmpty(t.getPermissionList())) {
List<UserDTO> users = flwTaskAssigneeService.fetchUsersByStorageIds(String.join(StringUtils.SEPARATOR, t.getPermissionList()));
if (CollUtil.isNotEmpty(users)) {
flowNode.setPermissionFlag(StreamUtils.join(users, e -> String.valueOf(e.getUserId())));
}
}
});
buildNextTaskList.stream().filter(t -> t.getNodeCode().equals(flowNode.getNodeCode())).findFirst()
.ifPresent(t -> {
if (CollUtil.isNotEmpty(t.getPermissionList())) {
List<UserDTO> users = flwTaskAssigneeService.fetchUsersByStorageIds(
String.join(StringUtils.SEPARATOR, t.getPermissionList()));
if (CollUtil.isNotEmpty(users)) {
flowNode.setPermissionFlag(
StreamUtils.join(users, e -> String.valueOf(e.getUserId())));
}
}
});
}
}
return nextFlowNodes;
@@ -554,7 +563,7 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
@Override
public List<FlowHisTask> selectHisTaskByIdList(List<Long> taskIdList) {
return flowHisTaskMapper.selectList(new LambdaQueryWrapper<>(FlowHisTask.class)
.in(FlowHisTask::getId, taskIdList));
.in(FlowHisTask::getId, taskIdList));
}
/**
@@ -566,7 +575,7 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
@Override
public FlowHisTask selectHisTaskById(Long taskId) {
return flowHisTaskMapper.selectOne(new LambdaQueryWrapper<>(FlowHisTask.class)
.eq(FlowHisTask::getId, taskId));
.eq(FlowHisTask::getId, taskId));
}
/**
@@ -577,7 +586,7 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
@Override
public List<FlowTask> selectByInstIdList(List<Long> instanceIdList) {
return flowTaskMapper.selectList(new LambdaQueryWrapper<>(FlowTask.class)
.in(FlowTask::getInstanceId, instanceIdList));
.in(FlowTask::getInstanceId, instanceIdList));
}
/**
@@ -588,20 +597,21 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
@Override
public List<FlowTask> selectByInstId(Long instanceId) {
return flowTaskMapper.selectList(new LambdaQueryWrapper<>(FlowTask.class)
.eq(FlowTask::getInstanceId, instanceId));
.eq(FlowTask::getInstanceId, instanceId));
}
/**
* 任务操作
*
* @param bo 参数
* @param taskOperation 操作类型,委派 delegateTask、转办 transferTask、加签 addSignature、减签 reductionSignature
* @param taskOperation 操作类型,委派 delegateTask、转办 transferTask、加签 addSignature、减签
* reductionSignature
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean taskOperation(TaskOperationBo bo, String taskOperation) {
FlowParams flowParams = FlowParams.build()
.message(bo.getMessage());
.message(bo.getMessage());
if (LoginHelper.isSuperAdmin() || LoginHelper.isTenantAdmin()) {
flowParams.ignore(true);
}
@@ -636,22 +646,22 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
}
// 设置任务状态并执行对应的任务操作
switch (taskOperation) {
//委派任务
// 委派任务
case DELEGATE_TASK -> {
flowParams.hisStatus(TaskStatusEnum.DEPUTE.getStatus());
return taskService.depute(taskId, flowParams);
}
//转办任务
// 转办任务
case TRANSFER_TASK -> {
flowParams.hisStatus(TaskStatusEnum.TRANSFER.getStatus());
return taskService.transfer(taskId, flowParams);
}
//加签,增加办理人
// 加签,增加办理人
case ADD_SIGNATURE -> {
flowParams.hisStatus(TaskStatusEnum.SIGN.getStatus());
return taskService.addSignature(taskId, flowParams);
}
//减签,减少办理人
// 减签,减少办理人
case REDUCTION_SIGNATURE -> {
flowParams.hisStatus(TaskStatusEnum.SIGN_OFF.getStatus());
return taskService.reductionSignature(taskId, flowParams);
@@ -681,14 +691,14 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
if (CollUtil.isNotEmpty(flowTasks)) {
FlowEngine.userService().deleteByTaskIds(StreamUtils.toList(flowTasks, FlowTask::getId));
List<User> userList = flowTasks.stream()
.map(flowTask -> {
FlowUser flowUser = new FlowUser();
flowUser.setType(TaskAssigneeType.APPROVER.getCode());
flowUser.setProcessedBy(userId);
flowUser.setAssociated(flowTask.getId());
return flowUser;
})
.collect(Collectors.toList());
.map(flowTask -> {
FlowUser flowUser = new FlowUser();
flowUser.setType(TaskAssigneeType.APPROVER.getCode());
flowUser.setProcessedBy(userId);
flowUser.setAssociated(flowTask.getId());
return flowUser;
})
.collect(Collectors.toList());
if (CollUtil.isNotEmpty(userList)) {
FlowEngine.userService().saveBatch(userList);
}
@@ -714,7 +724,8 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
for (Map.Entry<Long, List<User>> entry : listMap.entrySet()) {
List<User> value = entry.getValue();
if (CollUtil.isNotEmpty(value)) {
List<UserDTO> userDtoList = userService.selectListByIds(StreamUtils.toList(value, e -> Long.valueOf(e.getProcessedBy())));
List<UserDTO> userDtoList = userService
.selectListByIds(StreamUtils.toList(value, e -> Long.valueOf(e.getProcessedBy())));
map.put(entry.getKey(), userDtoList);
}
}
@@ -745,8 +756,8 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
@Override
public FlowNode getByNodeCode(String nodeCode, Long definitionId) {
return flowNodeMapper.selectOne(new LambdaQueryWrapper<FlowNode>()
.eq(FlowNode::getNodeCode, nodeCode)
.eq(FlowNode::getDefinitionId, definitionId));
.eq(FlowNode::getNodeCode, nodeCode)
.eq(FlowNode::getDefinitionId, definitionId));
}
}
@@ -60,6 +60,19 @@ public class TestLeaveServiceImpl implements ITestLeaveService {
return false;
}
/**
* spel条件表达:判断小于3
*
* @param qjts 待判断的变量(可不传自行返回true或false)
* @return boolean
*/
// public boolean eval(Integer qjts) {
// if (qjts <= 3) {
// return true;
// }
// return false;
// }
/**
* 查询请假
*/
@@ -142,11 +155,12 @@ public class TestLeaveServiceImpl implements ITestLeaveService {
*
* @param processEvent 参数
*/
@EventListener(condition = "#processEvent.flowCode.startsWith('leave')")
@EventListener(condition = "#processEvent.flowCode.startsWith('leave') || #processEvent.flowCode.startsWith('hzdyjs-qj')")
public void processHandler(ProcessEvent processEvent) {
log.info("当前任务执行了{}", processEvent.toString());
TestLeave testLeave = baseMapper.selectById(Long.valueOf(processEvent.getBusinessId()));
testLeave.setStatus(processEvent.getStatus());
// processEvent.getParams().put("qjts", 100);
// 用于例如审批附件 审批意见等 存储到业务表内 自行根据业务实现存储流程
Map<String, Object> params = processEvent.getParams();
if (MapUtil.isNotEmpty(params)) {
@@ -165,7 +179,8 @@ public class TestLeaveServiceImpl implements ITestLeaveService {
/**
* 执行任务创建监听
* 示例:也可通过 @EventListener(condition = "#processTaskEvent.flowCode=='leave1'")进行判断
* 示例:也可通过 @EventListener(condition =
* "#processTaskEvent.flowCode=='leave1'")进行判断
* 在方法中判断流程节点key
* if ("xxx".equals(processTaskEvent.getNodeCode())) {
* //执行业务逻辑
@@ -173,7 +188,7 @@ public class TestLeaveServiceImpl implements ITestLeaveService {
*
* @param processTaskEvent 参数
*/
@EventListener(condition = "#processTaskEvent.flowCode.startsWith('leave')")
@EventListener(condition = "#processTaskEvent.flowCode.startsWith('leave') || #processTaskEvent.flowCode.startsWith('hzdyjs-qj')")
public void processTaskHandler(ProcessTaskEvent processTaskEvent) {
log.info("当前任务创建了{}", processTaskEvent.toString());
}
@@ -185,7 +200,7 @@ public class TestLeaveServiceImpl implements ITestLeaveService {
*
* @param processDeleteEvent 参数
*/
@EventListener(condition = "#processDeleteEvent.flowCode.startsWith('leave')")
@EventListener(condition = "#processDeleteEvent.flowCode.startsWith('leave') || #processDeleteEvent.flowCode.startsWith('hzdyjs-qj')")
public void processDeleteHandler(ProcessDeleteEvent processDeleteEvent) {
log.info("监听删除流程事件,当前任务执行了{}", processDeleteEvent.toString());
TestLeave testLeave = baseMapper.selectById(Long.valueOf(processDeleteEvent.getBusinessId()));
@@ -64,7 +64,7 @@ public class XgStudentInfo extends TenantEntity {
private String namePinyin;
/**
* 性别:M-男,F-女
* 性别:0-男,1-女
*/
private String gender;
@@ -46,7 +46,7 @@ public class XgStudentInfoSimple extends TenantEntity {
private String name;
/**
* 性别:M-男,F-女
* 性别:0-男,1-女
*/
private String gender;
@@ -271,4 +271,4 @@ public class XgStudentInfoSimple extends TenantEntity {
* 备注信息
*/
private String remark;
}
}
@@ -41,8 +41,7 @@ public class XgStudentInfoBo extends BaseEntity {
* 身份证号
*/
@Size(max = 18, message = "身份证号长度不能超过18个字符")
@Pattern(regexp = "^[1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$",
message = "身份证号格式不正确")
@Pattern(regexp = "^[1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$", message = "身份证号格式不正确")
private String idCard;
/**
@@ -71,7 +70,7 @@ public class XgStudentInfoBo extends BaseEntity {
private String namePinyin;
/**
* 性别:M-男,F-女
* 性别:0-男,1-女
*/
@NotBlank(message = "性别不能为空", groups = { AddGroup.class, EditGroup.class })
@Pattern(regexp = "^[MF]$", message = "性别只能为M或F")
@@ -71,7 +71,7 @@ public class XgStudentInfoVo implements Serializable {
private String namePinyin;
/**
* 性别:M-男,F-女
* 性别:0-男,1-女
*/
@ExcelProperty(value = "性别", converter = ExcelDictConvert.class)
@ExcelDictFormat(dictType = "sys_user_sex")
@@ -0,0 +1,120 @@
package org.dromara.xg.controller;
import java.util.List;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.web.core.BaseController;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.xg.domain.vo.XgClassPosDictVo;
import org.dromara.xg.domain.bo.XgClassPosDictBo;
import org.dromara.xg.service.IXgClassPosDictService;
import org.dromara.common.mybatis.core.page.TableDataInfo;
/**
* 班级干部职务配置
*
* @author zlwu
* @date 2025-07-28
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/xgClassPosDict/xgClassPosDict")
public class XgClassPosDictController extends BaseController {
private final IXgClassPosDictService xgClassPosDictService;
/**
* 查询班级干部职务配置列表
*/
@SaCheckPermission("xgClassPosDict:xgClassPosDict:list")
@GetMapping("/list")
public TableDataInfo<XgClassPosDictVo> list(XgClassPosDictBo bo, PageQuery pageQuery) {
return xgClassPosDictService.queryPageList(bo, pageQuery);
}
/**
* 导出班级干部职务配置列表
*/
@SaCheckPermission("xgClassPosDict:xgClassPosDict:export")
@Log(title = "班级干部职务配置", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(XgClassPosDictBo bo, HttpServletResponse response) {
List<XgClassPosDictVo> list = xgClassPosDictService.queryList(bo);
ExcelUtil.exportExcel(list, "班级干部职务配置", XgClassPosDictVo.class, response);
}
/**
* 获取班级干部职务配置详细信息
*
* @param id 主键
*/
@SaCheckPermission("xgClassPosDict:xgClassPosDict:query")
@GetMapping("/{id}")
public R<XgClassPosDictVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
return R.ok(xgClassPosDictService.queryById(id));
}
/**
* 新增班级干部职务配置
*/
@SaCheckPermission("xgClassPosDict:xgClassPosDict:add")
@Log(title = "班级干部职务配置", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody XgClassPosDictBo bo) {
return toAjax(xgClassPosDictService.insertByBo(bo));
}
/**
* 修改班级干部职务配置
*/
@SaCheckPermission("xgClassPosDict:xgClassPosDict:edit")
@Log(title = "班级干部职务配置", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody XgClassPosDictBo bo) {
return toAjax(xgClassPosDictService.updateByBo(bo));
}
/**
* 删除班级干部职务配置
*
* @param ids 主键串
*/
@SaCheckPermission("xgClassPosDict:xgClassPosDict:remove")
@Log(title = "班级干部职务配置", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
return toAjax(xgClassPosDictService.deleteWithValidByIds(List.of(ids), true));
}
/**
* 获取有效的职务信息列表
*
* @param positionName 职务名称(非必填,支持模糊查询)
* @return 有效的职务信息列表
*/
@SaCheckPermission("xgClassPosDict:xgClassPosDict:list")
@GetMapping("/validPositions")
public R<List<XgClassPosDictVo>> getValidPositions(@RequestParam(required = false) String positionName) {
XgClassPosDictBo bo = new XgClassPosDictBo();
bo.setPositionName(positionName);
bo.setStatus("0"); // 启用状态
bo.setDeleted(0L); // 未删除
List<XgClassPosDictVo> list = xgClassPosDictService.queryValidPositions(bo);
return R.ok(list);
}
}
@@ -0,0 +1,127 @@
package org.dromara.xg.controller;
import java.util.List;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.web.core.BaseController;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.xg.domain.vo.XgClassPosMsgVo;
import org.dromara.xg.domain.bo.XgClassPosMsgBo;
import org.dromara.xg.service.IXgClassPosMsgService;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import java.util.Date;
/**
* 班级干部信息
*
* @author zlwu
* @date 2025-07-28
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/xgClassPosMsg/xgClassPosMsg")
public class XgClassPosMsgController extends BaseController {
private final IXgClassPosMsgService xgClassPosMsgService;
/**
* 查询班级干部信息列表
* 增加一个参数 deptId
*/
@SaCheckPermission("xgClassPosMsg:xgClassPosMsg:list")
@GetMapping("/list")
public TableDataInfo<XgClassPosMsgVo> list(XgClassPosMsgBo bo, PageQuery pageQuery,
@RequestParam(required = false) Long deptId) {
return xgClassPosMsgService.queryPageList(bo, pageQuery, deptId);
}
/**
* 导出班级干部信息列表
*/
@SaCheckPermission("xgClassPosMsg:xgClassPosMsg:export")
@Log(title = "班级干部信息", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(XgClassPosMsgBo bo, HttpServletResponse response) {
List<XgClassPosMsgVo> list = xgClassPosMsgService.queryList(bo);
ExcelUtil.exportExcel(list, "班级干部信息", XgClassPosMsgVo.class, response);
}
/**
* 获取班级干部信息详细信息
*
* @param id 主键
*/
@SaCheckPermission("xgClassPosMsg:xgClassPosMsg:query")
@GetMapping("/{id}")
public R<XgClassPosMsgVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
return R.ok(xgClassPosMsgService.queryById(id));
}
/**
* 新增班级干部信息
*/
@SaCheckPermission("xgClassPosMsg:xgClassPosMsg:add")
@Log(title = "班级干部信息", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody XgClassPosMsgBo bo) {
return toAjax(xgClassPosMsgService.insertByBo(bo));
}
/**
* 修改班级干部信息
*/
@SaCheckPermission("xgClassPosMsg:xgClassPosMsg:edit")
@Log(title = "班级干部信息", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody XgClassPosMsgBo bo) {
return toAjax(xgClassPosMsgService.updateByBo(bo));
}
/**
* 删除班级干部信息
*
* @param ids 主键串
*/
@SaCheckPermission("xgClassPosMsg:xgClassPosMsg:remove")
@Log(title = "班级干部信息", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
return toAjax(xgClassPosMsgService.deleteWithValidByIds(List.of(ids), true));
}
/**
* 学生干部离任
*
* @param id 班级干部信息ID
* @param termEndDate 任期结束日期
* @param cadreStatus 学生职务任职状态
* @param dismissalReason 离任原因
* @return 操作结果
*/
@SaCheckPermission("xgClassPosMsg:xgClassPosMsg:edit")
@Log(title = "学生干部离任", businessType = BusinessType.UPDATE)
@PostMapping("/dismiss/{id}")
public R<Void> dismissCadre(
@NotNull(message = "班级干部信息ID不能为空") @PathVariable Long id,
@NotNull(message = "任期结束日期不能为空") @RequestParam Date termEndDate,
@NotBlank(message = "学生职务任职状态不能为空") @RequestParam String cadreStatus,
@NotBlank(message = "离任原因不能为空") @RequestParam String dismissalReason) {
return toAjax(xgClassPosMsgService.dismissCadre(id, termEndDate, cadreStatus, dismissalReason));
}
}
@@ -30,7 +30,7 @@ public class XgDormBuildingTreeController extends BaseController {
* 获取宿舍楼树形结构数据
* 校区-宿舍区-宿舍楼三级结构,校区和宿舍区不可选,只有宿舍楼可以选择
*/
// @SaCheckPermission("xgss:dormBuildingTree:list")
@SaCheckPermission("xgss:dormBuildingTree:list")
@GetMapping("/list")
public R<List<DormBuildingTreeVo>> getDormBuildingTree(DormBuildingTreeBo bo) {
List<DormBuildingTreeVo> treeData = xgDormBuildingTreeService.getDormBuildingTree(bo);
@@ -0,0 +1,388 @@
package org.dromara.xg.controller;
import java.util.List;
import java.util.ArrayList;
import java.util.Date;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.springframework.transaction.annotation.Transactional;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.web.core.BaseController;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.xg.domain.vo.XgStuColBatchClassVo;
import org.dromara.xg.domain.bo.XgStuColBatchClassBo;
import org.dromara.xg.domain.XgStuColBatchClass;
import org.dromara.xg.service.IXgStuColBatchClassService;
import org.dromara.xg.mapper.XgStuColBatchClassMapper;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.xg.service.impl.XgStudentManagerSourceServiceImpl;
import org.dromara.xg.domain.vo.XgStudentManagerDeptTreeVo;
import org.dromara.xg.domain.vo.XgStuColBatchVo;
/**
* 学生信息收集批次班级权限
*
* @author zlwu
* @date 2025-07-27
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/stuColBatchClass/stuColBatchClass")
public class XgStuColBatchClassController extends BaseController {
private final IXgStuColBatchClassService xgStuColBatchClassService;
private final XgStuColBatchClassMapper xgStuColBatchClassMapper;
private final XgStudentManagerSourceServiceImpl xgStudentManagerSourceService;
/**
* 查询学生信息收集批次班级权限列表
*/
@SaCheckPermission("stuColBatchClass:stuColBatchClass:list")
@GetMapping("/list")
public TableDataInfo<XgStuColBatchClassVo> list(XgStuColBatchClassBo bo, PageQuery pageQuery) {
return xgStuColBatchClassService.queryPageList(bo, pageQuery);
}
/**
* 导出学生信息收集批次班级权限列表
*/
@SaCheckPermission("stuColBatchClass:stuColBatchClass:export")
@Log(title = "学生信息收集批次班级权限", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(XgStuColBatchClassBo bo, HttpServletResponse response) {
List<XgStuColBatchClassVo> list = xgStuColBatchClassService.queryList(bo);
ExcelUtil.exportExcel(list, "学生信息收集批次班级权限", XgStuColBatchClassVo.class, response);
}
/**
* 获取学生信息收集批次班级权限详细信息
*
* @param id 主键
*/
@SaCheckPermission("stuColBatchClass:stuColBatchClass:query")
@GetMapping("/{id}")
public R<XgStuColBatchClassVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
return R.ok(xgStuColBatchClassService.queryById(id));
}
/**
* 新增学生信息收集批次班级权限
*/
@SaCheckPermission("stuColBatchClass:stuColBatchClass:add")
@Log(title = "学生信息收集批次班级权限", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody XgStuColBatchClassBo bo) {
return toAjax(xgStuColBatchClassService.insertByBo(bo));
}
/**
* 修改学生信息收集批次班级权限
*/
@SaCheckPermission("stuColBatchClass:stuColBatchClass:edit")
@Log(title = "学生信息收集批次班级权限", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody XgStuColBatchClassBo bo) {
return toAjax(xgStuColBatchClassService.updateByBo(bo));
}
/**
* 删除学生信息收集批次班级权限
*
* @param ids 主键串
*/
@SaCheckPermission("stuColBatchClass:stuColBatchClass:remove")
@Log(title = "学生信息收集批次班级权限", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
return toAjax(xgStuColBatchClassService.deleteWithValidByIds(List.of(ids), true));
}
/**
* 获取学生信息收集批次班级权限
*
* @param batchId 批次ID
*/
@SaCheckPermission("stuColBatchClass:stuColBatchClass:query")
@GetMapping("/getScope/{batchId}")
public R<List<Long>> getScope(@NotNull(message = "批次ID不能为空") @PathVariable Long batchId) {
try {
List<Long> classIds = xgStuColBatchClassMapper.selectClassIdsByBatchId(batchId);
return R.ok(classIds);
} catch (Exception e) {
return R.fail("获取收集范围失败:" + e.getMessage());
}
}
/**
* 获取批次配置的权限范围详细信息
*
* @param batchId 批次ID
*/
@SaCheckPermission("stuColBatchClass:stuColBatchClass:query")
@GetMapping("/getScopeDetail/{batchId}")
public R<List<XgStuColBatchClassVo>> getScopeDetail(@NotNull(message = "批次ID不能为空") @PathVariable Long batchId) {
try {
List<XgStuColBatchClassVo> scopeDetail = xgStuColBatchClassMapper.selectScopeDetailByBatchId(batchId);
return R.ok(scopeDetail);
} catch (Exception e) {
return R.fail("获取权限范围详情失败:" + e.getMessage());
}
}
/**
* 设置学生信息收集批次班级权限
*
* @param batchId 批次ID
* @param deptIds 授权的班级ID列表
*/
@SaCheckPermission("stuColBatchClass:stuColBatchClass:setScope")
@Log(title = "设置学生信息收集批次班级权限", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping("/setScope")
@Transactional(rollbackFor = Exception.class)
public R<Void> setScope(@RequestParam("batchId") @NotNull(message = "批次ID不能为空") Long batchId,
@RequestParam("deptIds") @NotEmpty(message = "班级ID列表不能为空") List<Long> deptIds) {
try {
// 第一步:遍历deptIds,查询班级信息
List<XgStuColBatchClassVo> classInfoList = xgStuColBatchClassMapper.selectClassInfoByDeptIds(deptIds);
if (classInfoList.isEmpty()) {
return R.fail("未找到有效的班级信息");
}
// 第二步:清理历史权限数据
int clearCount = xgStuColBatchClassMapper.clearHistoryPermissions(batchId);
// 第三步:插入新的权限数据
List<XgStuColBatchClass> batchClassList = new ArrayList<>();
for (XgStuColBatchClassVo classInfo : classInfoList) {
XgStuColBatchClass batchClass = new XgStuColBatchClass();
batchClass.setBatchId(batchId);
batchClass.setClassId(classInfo.getId());
batchClass.setClassName(classInfo.getClassName());
batchClass.setMajorId(classInfo.getMajorId());
batchClass.setMajorName(classInfo.getMajorName());
batchClass.setCollegeId(classInfo.getCollegeId());
batchClass.setCollegeName(classInfo.getCollegeName());
batchClass.setDeleted(0L);
batchClassList.add(batchClass);
}
// 批量插入新权限数据
boolean insertResult = true;
for (XgStuColBatchClass batchClass : batchClassList) {
if (xgStuColBatchClassMapper.insert(batchClass) <= 0) {
insertResult = false;
break;
}
}
if (insertResult) {
return R.ok("权限设置成功,共设置 " + batchClassList.size() + " 个班级权限");
} else {
return R.fail("权限设置失败");
}
} catch (Exception e) {
return R.fail("权限设置异常:" + e.getMessage());
}
}
/**
* 查询学生可参与的批次信息
* 通过xg_student_msg、xg_stu_col_batch_class、xg_stu_col_batch三张表关联查询
*/
@SaCheckPermission("stuColBatchClass:stuColBatchClass:query")
@GetMapping("/getStudentAvailableBatches")
public R<List<Map<String, Object>>> getStudentAvailableBatches(
@RequestParam(value = "batchName", required = false) String batchName) {
try {
// 获取当前登录用户ID
Long currentUserId = LoginHelper.getUserId();
if (currentUserId == null) {
return R.fail("用户未登录");
}
// 查询学生可参与的批次信息
List<Map<String, Object>> batchList = xgStuColBatchClassMapper.selectStudentAvailableBatches(currentUserId,
batchName);
return R.ok(batchList);
} catch (Exception e) {
return R.fail("查询学生可参与批次信息失败:" + e.getMessage());
}
}
/**
* 查询当前登录老师关联的批次信息
*
* @param batchName 批次名称(非必要参数)
*/
@SaCheckPermission("stuColBatchClass:stuColBatchClass:query")
@GetMapping("/getTeacherRelatedBatches")
public R<List<XgStuColBatchVo>> getTeacherRelatedBatches(
@RequestParam(value = "batchName", required = false) String batchName) {
try {
// 第一步:获取当前用户所有有权限的部门信息
List<XgStudentManagerDeptTreeVo> deptTree = xgStudentManagerSourceService.getCurrentUserAuthDeptTree();
// 第二步:递归提取所有部门ID
List<Long> allDeptIds = new ArrayList<>();
extractAllDeptIds(deptTree, allDeptIds);
if (allDeptIds.isEmpty()) {
return R.ok(new ArrayList<>());
}
// 第三步:根据部门ID查询关联的批次信息
List<XgStuColBatchVo> batchList = xgStuColBatchClassMapper.selectTeacherRelatedBatches(allDeptIds,
batchName);
return R.ok(batchList);
} catch (Exception e) {
return R.fail("查询老师关联批次信息失败:" + e.getMessage());
}
}
/**
* 获取用户对应该批次的组织权限范围
*
* @param batchId 批次ID
*/
@SaCheckPermission("stuColBatchClass:stuColBatchClass:query")
@GetMapping("/getBatchAuthScope/{batchId}")
public R<List<XgStudentManagerDeptTreeVo>> getBatchAuthScope(
@NotNull(message = "批次ID不能为空") @PathVariable Long batchId) {
try {
// 第一步:获取当前用户所有的组织权限信息
List<XgStudentManagerDeptTreeVo> userDeptTree = xgStudentManagerSourceService.getCurrentUserAuthDeptTree();
if (userDeptTree == null || userDeptTree.isEmpty()) {
return R.ok(new ArrayList<>());
}
// 第二步:通过batchId在xg_stu_col_batch_class中获取class_id数据,过滤条件deleted=0
List<Long> batchClassIds = xgStuColBatchClassMapper.selectClassIdsByBatchId(batchId);
if (batchClassIds == null || batchClassIds.isEmpty()) {
return R.ok(new ArrayList<>());
}
// 第三步:过滤树形数据,只保留在第二步中存在的班级ID以及其上级节点
List<XgStudentManagerDeptTreeVo> filteredTree = filterDeptTreeByClassIds(userDeptTree, batchClassIds);
return R.ok(filteredTree);
} catch (Exception e) {
return R.fail("获取批次组织权限范围失败:" + e.getMessage());
}
}
/**
* 过滤部门树,只保留指定班级ID及其所有上级节点
*
* @param deptTree 原始部门树
* @param targetClassIds 目标班级ID列表
* @return 过滤后的部门树
*/
private List<XgStudentManagerDeptTreeVo> filterDeptTreeByClassIds(List<XgStudentManagerDeptTreeVo> deptTree,
List<Long> targetClassIds) {
if (deptTree == null || deptTree.isEmpty() || targetClassIds == null || targetClassIds.isEmpty()) {
return new ArrayList<>();
}
List<XgStudentManagerDeptTreeVo> filteredList = new ArrayList<>();
for (XgStudentManagerDeptTreeVo dept : deptTree) {
// 递归检查当前节点及其子节点是否包含目标班级ID
XgStudentManagerDeptTreeVo filteredDept = filterSingleDeptNode(dept, targetClassIds);
if (filteredDept != null) {
filteredList.add(filteredDept);
}
}
return filteredList;
}
/**
* 过滤单个部门节点,如果该节点或其子节点包含目标班级ID,则保留该节点
*
* @param dept 部门节点
* @param targetClassIds 目标班级ID列表
* @return 过滤后的部门节点,如果不包含目标班级则返回null
*/
private XgStudentManagerDeptTreeVo filterSingleDeptNode(XgStudentManagerDeptTreeVo dept,
List<Long> targetClassIds) {
if (dept == null) {
return null;
}
// 创建新的部门节点副本
XgStudentManagerDeptTreeVo filteredDept = new XgStudentManagerDeptTreeVo();
filteredDept.setId(dept.getId());
filteredDept.setParentId(dept.getParentId());
filteredDept.setLabel(dept.getLabel());
filteredDept.setWeight(dept.getWeight());
filteredDept.setDeptCategory(dept.getDeptCategory());
filteredDept.setDisabled(dept.getDisabled());
// 检查当前节点是否是目标班级
boolean isTargetClass = targetClassIds.contains(dept.getId());
// 递归处理子节点
List<XgStudentManagerDeptTreeVo> filteredChildren = new ArrayList<>();
if (dept.getChildren() != null && !dept.getChildren().isEmpty()) {
for (XgStudentManagerDeptTreeVo child : dept.getChildren()) {
XgStudentManagerDeptTreeVo filteredChild = filterSingleDeptNode(child, targetClassIds);
if (filteredChild != null) {
filteredChildren.add(filteredChild);
}
}
}
// 如果当前节点是目标班级,或者有过滤后的子节点,则保留该节点
if (isTargetClass || !filteredChildren.isEmpty()) {
filteredDept.setChildren(filteredChildren);
return filteredDept;
}
// 否则剔除该节点
return null;
}
/**
* 递归提取部门树中的所有部门ID
*
* @param deptTree 部门树
* @param deptIds 部门ID列表
*/
private void extractAllDeptIds(List<XgStudentManagerDeptTreeVo> deptTree, List<Long> deptIds) {
if (deptTree == null || deptTree.isEmpty()) {
return;
}
for (XgStudentManagerDeptTreeVo dept : deptTree) {
if (dept.getId() != null) {
deptIds.add(dept.getId());
}
// 递归处理子部门
if (dept.getChildren() != null && !dept.getChildren().isEmpty()) {
extractAllDeptIds(dept.getChildren(), deptIds);
}
}
}
}
@@ -0,0 +1,105 @@
package org.dromara.xg.controller;
import java.util.List;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.web.core.BaseController;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.xg.domain.vo.XgStuColBatchVo;
import org.dromara.xg.domain.bo.XgStuColBatchBo;
import org.dromara.xg.service.IXgStuColBatchService;
import org.dromara.common.mybatis.core.page.TableDataInfo;
/**
* 学生信息收集批次
*
* @author zlwu
* @date 2025-07-27
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/XgStuColBatch/stuColBatch")
public class XgStuColBatchController extends BaseController {
private final IXgStuColBatchService xgStuColBatchService;
/**
* 查询学生信息收集批次列表
*/
@SaCheckPermission("XgStuColBatch:stuColBatch:list")
@GetMapping("/list")
public TableDataInfo<XgStuColBatchVo> list(XgStuColBatchBo bo, PageQuery pageQuery) {
return xgStuColBatchService.queryPageList(bo, pageQuery);
}
/**
* 导出学生信息收集批次列表
*/
@SaCheckPermission("XgStuColBatch:stuColBatch:export")
@Log(title = "学生信息收集批次", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(XgStuColBatchBo bo, HttpServletResponse response) {
List<XgStuColBatchVo> list = xgStuColBatchService.queryList(bo);
ExcelUtil.exportExcel(list, "学生信息收集批次", XgStuColBatchVo.class, response);
}
/**
* 获取学生信息收集批次详细信息
*
* @param id 主键
*/
@SaCheckPermission("XgStuColBatch:stuColBatch:query")
@GetMapping("/{id}")
public R<XgStuColBatchVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(xgStuColBatchService.queryById(id));
}
/**
* 新增学生信息收集批次
*/
@SaCheckPermission("XgStuColBatch:stuColBatch:add")
@Log(title = "学生信息收集批次", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody XgStuColBatchBo bo) {
return toAjax(xgStuColBatchService.insertByBo(bo));
}
/**
* 修改学生信息收集批次
*/
@SaCheckPermission("XgStuColBatch:stuColBatch:edit")
@Log(title = "学生信息收集批次", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody XgStuColBatchBo bo) {
return toAjax(xgStuColBatchService.updateByBo(bo));
}
/**
* 删除学生信息收集批次
*
* @param ids 主键串
*/
@SaCheckPermission("XgStuColBatch:stuColBatch:remove")
@Log(title = "学生信息收集批次", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空")
@PathVariable Long[] ids) {
return toAjax(xgStuColBatchService.deleteWithValidByIds(List.of(ids), true));
}
}
@@ -0,0 +1,132 @@
package org.dromara.xg.controller;
import java.util.List;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.web.core.BaseController;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.xg.domain.vo.XgStudentManagerVo;
import org.dromara.xg.domain.bo.XgStudentManagerBo;
import org.dromara.xg.service.IXgStudentManagerService;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.xg.domain.vo.ClassTeacherInfoVo;
/**
* 宿舍管理员信息
*
* @author zlwu
* @date 2025-07-24
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/xgStudentManager/studentManager")
public class XgStudentManagerController extends BaseController {
private final IXgStudentManagerService xgStudentManagerService;
/**
* 查询宿舍管理员信息列表
*/
@SaCheckPermission("xgStudentManager:studentManager:list")
@GetMapping("/list")
public TableDataInfo<XgStudentManagerVo> list(XgStudentManagerBo bo, PageQuery pageQuery) {
return xgStudentManagerService.queryPageList(bo, pageQuery);
}
/**
* 导出宿舍管理员信息列表
*/
@SaCheckPermission("xgStudentManager:studentManager:export")
@Log(title = "宿舍管理员信息", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(XgStudentManagerBo bo, HttpServletResponse response) {
List<XgStudentManagerVo> list = xgStudentManagerService.queryList(bo);
ExcelUtil.exportExcel(list, "宿舍管理员信息", XgStudentManagerVo.class, response);
}
/**
* 获取宿舍管理员信息详细信息
*
* @param id 主键
*/
@SaCheckPermission("xgStudentManager:studentManager:query")
@GetMapping("/{id}")
public R<XgStudentManagerVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
return R.ok(xgStudentManagerService.queryById(id));
}
/**
* 新增宿舍管理员信息
*/
@SaCheckPermission("xgStudentManager:studentManager:add")
@Log(title = "宿舍管理员信息", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody XgStudentManagerBo bo) {
return toAjax(xgStudentManagerService.insertByBo(bo));
}
/**
* 修改宿舍管理员信息
*/
@SaCheckPermission("xgStudentManager:studentManager:edit")
@Log(title = "宿舍管理员信息", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody XgStudentManagerBo bo) {
return toAjax(xgStudentManagerService.updateByBo(bo));
}
/**
* 删除宿舍管理员信息
*
* @param ids 主键串
*/
@SaCheckPermission("xgStudentManager:studentManager:remove")
@Log(title = "宿舍管理员信息", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
return toAjax(xgStudentManagerService.deleteWithValidByIds(List.of(ids), true));
}
/**
* 获取可用的教职工信息列表
*
* @param managerType 管理员类型(1 全校管理员 2 自定义管理员 3 班主任)
*/
@SaCheckPermission("xgStudentManager:studentManager:list")
@GetMapping("/listAvailableTeachers")
public R<List<XgStudentManagerVo>> listAvailableTeachers(@RequestParam("managerType") String managerType) {
List<XgStudentManagerVo> teachers = xgStudentManagerService.queryAvailableTeachers(managerType);
return R.ok(teachers);
}
/**
* 获取当前正在教学的班主任信息
*
* @param phone 手机号码(可选,模糊查询)
* @param name 姓名(可选,模糊查询)
* @return 班主任信息列表
*/
@SaCheckPermission("xgStudentManager:studentManager:list")
@GetMapping("/listActiveClassTeachers")
public R<List<ClassTeacherInfoVo>> listActiveClassTeachers(
@RequestParam(value = "phone", required = false) String phone,
@RequestParam(value = "name", required = false) String name) {
List<ClassTeacherInfoVo> teachers = xgStudentManagerService.queryActiveClassTeachers(phone, name);
return R.ok(teachers);
}
}
@@ -0,0 +1,141 @@
package org.dromara.xg.controller;
import java.util.List;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.web.core.BaseController;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.xg.domain.vo.XgStudentManagerSourceVo;
import org.dromara.xg.domain.vo.XgStudentManagerDeptTreeVo;
import org.dromara.xg.domain.bo.XgStudentManagerSourceBo;
import org.dromara.xg.service.IXgStudentManagerSourceService;
import org.dromara.common.mybatis.core.page.TableDataInfo;
/**
* 学生管理员授权关系
*
* @author zlwu
* @date 2025-07-24
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/xgStudentManagerSource/studentManagerSource")
public class XgStudentManagerSourceController extends BaseController {
private final IXgStudentManagerSourceService xgStudentManagerSourceService;
/**
* 查询学生管理员授权关系列表
*/
@SaCheckPermission("xgStudentManagerSource:studentManagerSource:list")
@GetMapping("/list")
public TableDataInfo<XgStudentManagerSourceVo> list(XgStudentManagerSourceBo bo, PageQuery pageQuery) {
return xgStudentManagerSourceService.queryPageList(bo, pageQuery);
}
/**
* 导出学生管理员授权关系列表
*/
@SaCheckPermission("xgStudentManagerSource:studentManagerSource:export")
@Log(title = "学生管理员授权关系", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(XgStudentManagerSourceBo bo, HttpServletResponse response) {
List<XgStudentManagerSourceVo> list = xgStudentManagerSourceService.queryList(bo);
ExcelUtil.exportExcel(list, "学生管理员授权关系", XgStudentManagerSourceVo.class, response);
}
/**
* 获取学生管理员授权关系详细信息
*
* @param id 主键
*/
@SaCheckPermission("xgStudentManagerSource:studentManagerSource:query")
@GetMapping("/{id}")
public R<XgStudentManagerSourceVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
return R.ok(xgStudentManagerSourceService.queryById(id));
}
/**
* 新增学生管理员授权关系
*/
@SaCheckPermission("xgStudentManagerSource:studentManagerSource:add")
@Log(title = "学生管理员授权关系", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody XgStudentManagerSourceBo bo) {
return toAjax(xgStudentManagerSourceService.insertByBo(bo));
}
/**
* 修改学生管理员授权关系
*/
@SaCheckPermission("xgStudentManagerSource:studentManagerSource:edit")
@Log(title = "学生管理员授权关系", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody XgStudentManagerSourceBo bo) {
return toAjax(xgStudentManagerSourceService.updateByBo(bo));
}
/**
* 删除学生管理员授权关系
*
* @param ids 主键串
*/
@SaCheckPermission("xgStudentManagerSource:studentManagerSource:remove")
@Log(title = "学生管理员授权关系", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
return toAjax(xgStudentManagerSourceService.deleteWithValidByIds(List.of(ids), true));
}
/**
* 自定义学生管理员授予部门权限
*
* @param managerId 管理员ID
* @param authSourceIds 资源ID列表
*/
@SaCheckPermission("xgStudentManagerSource:studentManagerSource:grantDeptAuth")
@Log(title = "学生管理员授权关系", businessType = BusinessType.UPDATE)
@PostMapping("/grantDeptAuth")
public R<Void> grantDeptAuth(@RequestParam Long managerId, @RequestBody List<Long> authSourceIds) {
xgStudentManagerSourceService.grantDeptAuth(managerId, authSourceIds);
return R.ok();
}
/**
* 获取指定管理员的授权资源名称列表
*
* @param managerId 管理员ID
* @return 授权资源名称数组
*/
@SaCheckPermission("xgStudentManagerSource:studentManagerSource:query")
@GetMapping("/authSourceNames")
public R<List<String>> getAuthSourceNames(@RequestParam Long managerId) {
return R.ok(xgStudentManagerSourceService.getAuthSourceNames(managerId));
}
/**
* 获取当前用户的学生管理权限部门树
*
* @return 部门树形结构列表
*/
@SaCheckPermission("xgStudentManagerSource:studentManagerSource:query")
@GetMapping("/currentUserAuthTree")
public R<List<XgStudentManagerDeptTreeVo>> getCurrentUserAuthTree() {
return R.ok(xgStudentManagerSourceService.getCurrentUserAuthDeptTree());
}
}
@@ -0,0 +1,237 @@
package org.dromara.xg.controller;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.*;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.web.core.BaseController;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.xg.domain.vo.XgStudentMsgBatchVo;
import org.dromara.xg.domain.bo.XgStudentMsgBatchBo;
import org.dromara.xg.service.IXgStudentMsgBatchService;
import org.dromara.common.mybatis.core.page.TableDataInfo;
/**
* 学生信息批次收集
*
* @author zlwu
* @date 2025-07-27
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/studentMsgBatch/studentMsgBatch")
public class XgStudentMsgBatchController extends BaseController {
private final IXgStudentMsgBatchService xgStudentMsgBatchService;
/**
* 查询学生信息批次收集列表
*/
@SaCheckPermission("studentMsgBatch:studentMsgBatch:list")
@GetMapping("/list")
public TableDataInfo<XgStudentMsgBatchVo> list(XgStudentMsgBatchBo bo, PageQuery pageQuery) {
return xgStudentMsgBatchService.queryPageList(bo, pageQuery);
}
/**
* 导出学生信息批次收集列表
*/
@SaCheckPermission("studentMsgBatch:studentMsgBatch:export")
@Log(title = "学生信息批次收集", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(XgStudentMsgBatchBo bo, HttpServletResponse response) {
List<XgStudentMsgBatchVo> list = xgStudentMsgBatchService.queryList(bo);
ExcelUtil.exportExcel(list, "学生信息批次收集", XgStudentMsgBatchVo.class, response);
}
/**
* 获取学生信息批次收集详细信息
*
* @param id 主键
*/
@SaCheckPermission("studentMsgBatch:studentMsgBatch:query")
@GetMapping("/{id}")
public R<XgStudentMsgBatchVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
return R.ok(xgStudentMsgBatchService.queryById(id));
}
/**
* 新增学生信息批次收集
*/
@SaCheckPermission("studentMsgBatch:studentMsgBatch:add")
@Log(title = "学生信息批次收集", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody XgStudentMsgBatchBo bo) {
return toAjax(xgStudentMsgBatchService.insertByBo(bo));
}
/**
* 修改学生信息批次收集
*/
@SaCheckPermission("studentMsgBatch:studentMsgBatch:edit")
@Log(title = "学生信息批次收集", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody XgStudentMsgBatchBo bo) {
return toAjax(xgStudentMsgBatchService.updateByBo(bo));
}
/**
* 删除学生信息批次收集
*
* @param ids 主键串
*/
@SaCheckPermission("studentMsgBatch:studentMsgBatch:remove")
@Log(title = "学生信息批次收集", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
return toAjax(xgStudentMsgBatchService.deleteWithValidByIds(List.of(ids), true));
}
/**
* 查询学生信息完善的历史记录信息及最后一次提交的信息状态
*
* @param batchId 批次ID
*/
@SaCheckPermission("studentMsgBatch:studentMsgBatch:query")
@GetMapping("/history/{batchId}")
public R<Map<String, Object>> getStudentMsgHistory(@NotNull(message = "批次ID不能为空") @PathVariable Long batchId) {
return R.ok(xgStudentMsgBatchService.getStudentMsgHistory(batchId));
}
/**
* 查询学生信息完善的历史记录信息及最后一次提交的信息状态
*
* @param batchId 批次ID
* @param userId 用户ID
*/
@SaCheckPermission("studentMsgBatch:studentMsgBatch:query")
@GetMapping("/history/{batchId}/{userId}")
public R<Map<String, Object>> getStudentMsgHistory(@NotNull(message = "批次ID不能为空") @PathVariable Long batchId,
@NotNull(message = "用户ID不能为空") @PathVariable Long userId) {
return R.ok(xgStudentMsgBatchService.getStudentMsgHistory(batchId, userId));
}
/**
* 获取用户该批次的历史已填写数据
*
* @param batchId 批次ID
*/
@SaCheckPermission("studentMsgBatch:studentMsgBatch:query")
@GetMapping("/userHistoryData/{batchId}")
public R<XgStudentMsgBatchVo> getUserHistoryData(@NotNull(message = "批次ID不能为空") @PathVariable Long batchId) {
return R.ok(xgStudentMsgBatchService.getUserHistoryData(batchId));
}
/**
* 获取用户该批次的历史已填写数据
*
* @param batchId 批次ID
* @param userId 用户ID
*/
@SaCheckPermission("studentMsgBatch:studentMsgBatch:query")
@GetMapping("/userHistoryData/{batchId}/{userId}")
public R<XgStudentMsgBatchVo> getUserHistoryData(@NotNull(message = "批次ID不能为空") @PathVariable Long batchId,
@NotNull(message = "用户ID不能为空") @PathVariable Long userId) {
return R.ok(xgStudentMsgBatchService.getUserHistoryData(batchId, userId));
}
/**
* 学生提交信息完善数据
*/
@SaCheckPermission("studentMsgBatch:studentMsgBatch:add")
@Log(title = "学生提交信息完善", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping("/submitStudentMsg")
public R<Void> submitStudentMsg(@Validated(AddGroup.class) @RequestBody XgStudentMsgBatchBo bo) {
return toAjax(xgStudentMsgBatchService.submitStudentMsg(bo));
}
/**
* 根据部门ID查询该组织下所有已完善信息的学生数据
*
* @param batchId 批次ID
* @param deptId 部门ID
* @param name 学生姓名(模糊查询)
* @param studentNo 学号(精确查询)
* @param phone 手机号码(精确查询)
* @param infoStatus 信息状态
* @param pageQuery 分页查询参数
*/
@SaCheckPermission("studentMsgBatch:studentMsgBatch:query")
@GetMapping("/listByDept")
public TableDataInfo<XgStudentMsgBatchVo> listByDeptId(
@RequestParam(required = true) Long batchId,
@RequestParam(required = true) Long deptId,
@RequestParam(required = false) String name,
@RequestParam(required = false) String studentNo,
@RequestParam(required = false) String phone,
@RequestParam(required = false) String infoStatus,
PageQuery pageQuery) {
return xgStudentMsgBatchService.queryPageListByDeptId(batchId, deptId, name, studentNo, phone, infoStatus,
pageQuery);
}
/**
* 驳回学生信息提交
*
* @param batchId 批次ID
* @param userId 用户ID
* @param remark 驳回原因
*/
@SaCheckPermission("studentMsgBatch:studentMsgBatch:edit")
@Log(title = "驳回学生信息提交", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping("/rejectStudentMsg")
public R<Void> rejectStudentMsg(@RequestParam @NotNull(message = "批次ID不能为空") Long batchId,
@RequestParam @NotNull(message = "用户ID不能为空") Long userId,
@RequestParam @NotBlank(message = "驳回原因不能为空") String remark) {
return toAjax(xgStudentMsgBatchService.rejectStudentMsg(batchId, userId, remark));
}
/**
* 审核通过学生信息提交
*
* @param batchId 批次ID
* @param userId 用户ID
* @param remark 审核备注
*/
@SaCheckPermission("studentMsgBatch:studentMsgBatch:edit")
@Log(title = "审核通过学生信息提交", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PostMapping("/approveStudentMsg")
public R<Void> approveStudentMsg(@RequestParam @NotNull(message = "批次ID不能为空") Long batchId,
@RequestParam @NotNull(message = "用户ID不能为空") Long userId,
@RequestParam(required = false) String remark) {
return toAjax(xgStudentMsgBatchService.approveStudentMsg(batchId, userId, remark));
}
/**
* 比较学生信息变更详情
*
* @param batchId 批次ID
* @param userId 用户ID
* @return 变更详情信息
*/
@SaCheckPermission("studentMsgBatch:studentMsgBatch:query")
@GetMapping("/compareStudentMsg")
public R<Map<String, Object>> compareStudentMsg(@RequestParam @NotNull(message = "批次ID不能为空") Long batchId,
@RequestParam @NotNull(message = "用户ID不能为空") Long userId) {
return R.ok(xgStudentMsgBatchService.compareStudentMsg(batchId, userId));
}
}
@@ -285,4 +285,9 @@ public class XgClassMsg extends TenantEntity {
*/
private String remark;
/**
* 学制(年)
*/
private Long schoolSystem;
}
@@ -0,0 +1,81 @@
package org.dromara.xg.domain;
import org.dromara.common.tenant.core.TenantEntity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serial;
/**
* 班级干部职务配置对象 xg_class_pos_dict
*
* @author zlwu
* @date 2025-07-28
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("xg_class_pos_dict")
public class XgClassPosDict extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID(雪花ID)
*/
@TableId(value = "id")
private Long id;
/**
* 删除标志(0代表存在 1代表删除)
*/
@TableLogic(value = "0", delval = "1")
private Long deleted;
/**
* 职务编码
*/
private String positionCode;
/**
* 职务名称
*/
private String positionName;
/**
* 学生职务类别:class-班级干部,party-党团干部,other-其他
*/
private String positionCategory;
/**
* 学生职务级别: 1-班长,2-副班长,3-委员,9-普通成员
*/
private Long positionLevel;
/**
* 学生职务类型:regular-常规职务,special-特殊职务,temporary-临时职务
*/
private String positionType;
/**
* 职责描述
*/
private String responsibilities;
/**
* 状态:0-启用,1-停用
*/
private String status;
/**
* 排序顺序
*/
private Long sortOrder;
/**
* 备注信息
*/
private String remark;
}
@@ -0,0 +1,123 @@
package org.dromara.xg.domain;
import org.dromara.common.tenant.core.TenantEntity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.io.Serial;
/**
* 班级干部信息对象 xg_class_pos_msg
*
* @author zlwu
* @date 2025-07-28
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("xg_class_pos_msg")
public class XgClassPosMsg extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID(雪花ID)
*/
@TableId(value = "id")
private Long id;
/**
* 删除标志(0代表存在 1代表删除)
*/
@TableLogic(value = "0", delval = "1")
private Long deleted;
/**
* 班级ID,关联xg_class_msg.id
*/
private Long classId;
/**
* 学生ID,关联xg_student_msg.id
*/
private Long studentId;
/**
* 学号
*/
private String studentNo;
/**
* 学生姓名(冗余字段)
*/
private String studentName;
/**
* 职务编码id,关联xg_class_pos_dict.id
*/
private Long positionId;
/**
* 职务编码,关联xg_class_pos_dict.position_code
*/
private String positionCode;
/**
* 任职日期
*/
private Date appointmentDate;
/**
* 任期开始日期
*/
private Date termStartDate;
/**
* 任期结束日期
*/
private Date termEndDate;
/**
* 任期时长(月)
*/
private Long termDuration;
/**
* 是否现任:0-否,1-是
*/
private Long isCurrent;
/**
* 学生职务任职状态:1-在任,2-到期,3-辞职,4-免职,5-调任,6-休学
*/
private String cadreStatus;
/**
* 任命原因
*/
private String appointmentReason;
/**
* 学生职务任命方式:appointment-任命,election-选举,recommendation-推荐
*/
private String appointmentMethod;
/**
* 离任日期
*/
private Date dismissalDate;
/**
* 离任原因
*/
private String dismissalReason;
/**
* 备注信息
*/
private String remark;
}
@@ -0,0 +1,73 @@
package org.dromara.xg.domain;
import org.dromara.common.tenant.core.TenantEntity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.io.Serial;
/**
* 学生信息收集批次对象 xg_stu_col_batch
*
* @author zlwu
* @date 2025-07-27
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("xg_stu_col_batch")
public class XgStuColBatch extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 批次ID
*/
@TableId(value = "id")
private Long id;
/**
* 批次名称
*/
private String batchName;
/**
* 批次描述
*/
private String description;
/**
* 开始时间
*/
private Date startTime;
/**
* 结束时间
*/
private Date endTime;
/**
* 状态(0正常 1停用)
*/
private String status;
/**
* 收集类型 1 - 新学期收集 2 - 其他
*/
private String collectionType;
/**
* 备注
*/
private String remark;
/**
* 删除标志(0代表存在 1代表删除)
*/
@TableLogic(value = "0", delval = "1")
private Long deleted;
}
Loaded 100 of 662 files, more files were not shown because too many files have changed in this diff. Show more