feat(pigx-upms-biz): 新增密码规则预设功能

This commit is contained in:
PIGCLOUD committed 2026-05-21 17:21:03 +08:00
1 parent 1b87ae9787
commit 3132cc3784
97 files changed
+2721 -462

No files matched your search

@@ -0,0 +1,56 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.admin.api.constant;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* Clarity 数据拉取状态枚举
*
* @author lengleng
* @date 2026-03-31
*/
@Getter
@RequiredArgsConstructor
public enum ClarityFetchStatus {
/**
* 占位中:已插入占位行,API 调用尚未完成
*/
PENDING("pending", "占位中"),
/**
* 成功:API 调用成功,数据已写入
*/
SUCCESS("success", "成功"),
/**
* 失败:API 调用失败,下次调度可重试
*/
FAILED("failed", "失败");
@EnumValue
@JsonValue
private final String value;
private final String desc;
}
@@ -48,6 +48,6 @@ public enum SystemConfigTypeEnum {
WETALK("wetalk");
@Getter
private final String value;
private final String value;
}
@@ -59,11 +59,21 @@ public interface UpmsErrorCodes {
String SYS_MENU_BUTTON_CANNOT_HOME = "sys.menu.button.cannot.home";
String SYS_MENU_SORT_DUPLICATE = "sys.menu.sort.duplicate";
String SYS_MENU_SORT_SCOPE_INVALID = "sys.menu.sort.scopeInvalid";
// === 字典相关 ===
String SYS_DICT_DELETE_SYSTEM = "sys.dict.delete.system";
String SYS_DICT_UPDATE_SYSTEM = "sys.dict.update.system";
String SYS_DICT_NOT_FOUND = "sys.dict.notFound";
String SYS_DICT_ITEM_SORT_DUPLICATE = "sys.dict.item.sort.duplicate";
String SYS_DICT_ITEM_SORT_SCOPE_INVALID = "sys.dict.item.sort.scopeInvalid";
// === 参数相关 ===
String SYS_PARAM_CONFIG_ERROR = "sys.param.config.error";
@@ -1,24 +0,0 @@
package com.pig4cloud.pigx.admin.api.dto;
import lombok.Data;
/**
* AI 聊天消息 DTO
*
* @author lengleng
* @date 2025/02/20
*/
@Data
public class AiChatMessageDTO {
/**
* 消息
*/
private String message;
/**
* 网络搜索
*/
private boolean webSearch;
}
@@ -0,0 +1,51 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*
*/
package com.pig4cloud.pigx.admin.api.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 创建 API 密钥请求体
*
* @author lengleng
* @date 2026-03-23
*/
@Data
@Schema(description = "创建API密钥请求")
public class ApiKeyCreateDTO {
@Schema(description = "Key名称")
private String name;
@Schema(description = "IP白名单,多个以逗号分隔")
private String allowedIps;
@Schema(description = "过期时间")
private LocalDateTime expiresAt;
@NotBlank(message = "当前密码不能为空")
@Schema(description = "当前登录用户密码(用于二次验证)")
private String password;
}
@@ -72,4 +72,14 @@ public class MessageEmailDTO {
*/
private String bizCode;
/**
* 图形验证码类型
*/
private String captchaType;
/**
* 图形验证码凭据
*/
private String captchaVerification;
}
@@ -53,7 +53,7 @@ public class MessageHookDTO {
/**
* 图片链接
*/
private String picUrl;
*/
private String picUrl;
}
@@ -40,6 +40,20 @@ public class MessageSmsDTO {
@Setter
private LinkedHashMap<String, String> params;
/**
* 图形验证码类型
*/
@Getter
@Setter
private String captchaType;
/**
* 图形验证码凭据
*/
@Getter
@Setter
private String captchaVerification;
/**
* ext 参数
*/
@@ -57,8 +71,8 @@ public class MessageSmsDTO {
// 静态方法用于创建Builder实例
public static Builder builder() {
return new Builder();
}
return new Builder();
}
// Builder类
public static class Builder {
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.admin.api.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
/**
* 字典项排序DTO
*
* @author lengleng
* @date 2026/5/21
*/
@Data
@Schema(description = "字典项排序")
public class SysDictItemSortDTO {
/**
* 字典ID
*/
@NotNull(message = "字典ID不能为空")
@Schema(description = "字典ID")
private Long dictId;
/**
* 当前页字典项ID列表,按目标顺序排列
*/
@NotEmpty(message = "字典项ID列表不能为空")
@Schema(description = "字典项ID列表")
private List<Long> itemIds;
}
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.admin.api.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
/**
* 菜单排序DTO
*
* @author lengleng
* @date 2026/5/21
*/
@Data
@Schema(description = "菜单排序")
public class SysMenuSortDTO {
/**
* 父菜单ID
*/
@NotNull(message = "菜单父ID不能为空")
@Schema(description = "父菜单ID")
private Long parentId;
/**
* 同级菜单ID列表,按目标顺序排列
*/
@NotEmpty(message = "菜单ID列表不能为空")
@Schema(description = "菜单ID列表")
private List<Long> menuIds;
}
@@ -0,0 +1,94 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*
*/
package com.pig4cloud.pigx.admin.api.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.pig4cloud.pigx.common.core.util.TenantTable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* API密钥管理
*
* @author lengleng
* @date 2026-03-23
*/
@Data
@TenantTable
@Schema(description = "API密钥")
@EqualsAndHashCode(callSuper = true)
public class SysApiKey extends Model<SysApiKey> {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.ASSIGN_ID)
@Schema(description = "主键")
private Long id;
@Schema(description = "所属用户ID")
private Long userId;
@Schema(description = "所属用户名")
private String username;
@Schema(description = "Key名称")
private String name;
@Schema(description = "SHA-256哈希值")
private String apiKeyHash;
@Schema(description = "IP白名单")
private String allowedIps;
@Schema(description = "过期时间")
private LocalDateTime expiresAt;
@Schema(description = "状态:0-正常,1-禁用")
private String status;
@Schema(description = "最后使用时间")
private LocalDateTime lastUsedAt;
@TableLogic
@TableField(fill = FieldFill.INSERT)
@Schema(description = "删除标记,1:已删除,0:正常")
private String delFlag;
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建者")
private String createBy;
@TableField(fill = FieldFill.UPDATE)
@Schema(description = "更新者")
private String updateBy;
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建时间")
private LocalDateTime createTime;
@TableField(fill = FieldFill.UPDATE)
@Schema(description = "更新时间")
private LocalDateTime updateTime;
}
@@ -0,0 +1,113 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.admin.api.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.pig4cloud.pigx.admin.api.constant.ClarityFetchStatus;
import com.pig4cloud.pigx.common.core.util.TenantTable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* Clarity 监控数据缓存
*
* @author lengleng
* @date 2026-03-26
*/
@Data
@TenantTable
@Schema(description = "Clarity 监控数据缓存")
@EqualsAndHashCode(callSuper = true)
public class SysClarityData extends Model<SysClarityData> {
private static final long serialVersionUID = 1L;
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键")
private Long id;
@Schema(description = "数据日期")
private LocalDate dataDate;
@Schema(description = "总会话数(Traffic.totalSessionCount)")
private Integer totalSessions;
@Schema(description = "独立访客数 UV(Traffic.distinctUserCount)")
private Integer distinctUsers;
@Schema(description = "每会话页面数(Traffic.pagesPerSessionPercentage)")
private BigDecimal pagesPerSession;
@Schema(description = "平均滚动深度 %(ScrollDepth.averageScrollDepth)")
private BigDecimal scrollDepth;
@Schema(description = "死点击率 %(DeadClickCount.sessionsWithMetricPercentage)")
private BigDecimal deadClickRate;
@Schema(description = "激怒点击率 %(RageClickCount.sessionsWithMetricPercentage)")
private BigDecimal rageClickRate;
@Schema(description = "设备分布 JSON 文本,格式:[{name,value}]")
private String deviceData;
@Schema(description = "热门页面 Top10 JSON 文本,格式:[{name,value}]")
private String topUrls;
@Schema(description = "来源页面 Top5 JSON,格式:[{name,value}]")
private String referrerUrlData;
@Schema(description = "页面标题 Top5 JSON,格式:[{name,value}]")
private String pageTitleData;
@Schema(description = "浏览器分布 JSON,格式:[{name,value}]")
private String browserData;
@Schema(description = "Clarity API numOfDays 参数:1=24h, 2=48h, 3=72h")
private Integer numOfDays;
@Schema(description = "数据拉取状态:pending=占位中, success=成功, failed=失败")
private ClarityFetchStatus fetchStatus;
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建人")
private String createBy;
@TableField(fill = FieldFill.UPDATE)
@Schema(description = "修改人")
private String updateBy;
@TableLogic
@TableField(fill = FieldFill.INSERT)
@Schema(description = "删除标记,1:已删除,0:正常")
private String delFlag;
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建时间")
private LocalDateTime createTime;
@TableField(fill = FieldFill.UPDATE)
@Schema(description = "更新时间")
private LocalDateTime updateTime;
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -132,7 +132,7 @@ public class SysFile extends Model<SysFile> {
*/
@TableLogic
@TableField(fill = FieldFill.INSERT)
@Schema(description = "删除标记,1:已删除,0:正常")
private String delFlag;
@Schema(description = "删除标记,1:已删除,0:正常")
private String delFlag;
}
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -19,8 +19,8 @@
package com.pig4cloud.pigx.admin.api.entity;
import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelProperty;
import org.apache.fesod.sheet.annotation.ExcelIgnore;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import com.baomidou.mybatisplus.annotation.*;
import com.pig4cloud.pigx.common.core.util.TenantTable;
import com.pig4cloud.pigx.common.sensitive.annotation.Sensitive;
@@ -0,0 +1,83 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*
*/
package com.pig4cloud.pigx.admin.api.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.pig4cloud.pigx.common.core.util.TenantTable;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 角色首页组件配置
*
* @author lengleng
*/
@Data
@TenantTable
@Schema(description = "角色首页组件配置")
@EqualsAndHashCode(callSuper = true)
public class SysRoleWidget extends Model<SysRoleWidget> {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.ASSIGN_ID)
@Schema(description = "主键")
private Long id;
@Schema(description = "角色ID")
private Long roleId;
@Schema(description = "允许的组件key列表,逗号分隔")
private String widgetKeys;
@Schema(description = "默认布局JSON,格式同前端grid对象")
private String layoutConfig;
/** 创建人 */
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建人")
private String createBy;
/** 修改人 */
@TableField(fill = FieldFill.UPDATE)
@Schema(description = "修改人")
private String updateBy;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
@Schema(description = "创建时间")
private LocalDateTime createTime;
/** 修改时间 */
@TableField(fill = FieldFill.UPDATE)
@Schema(description = "修改时间")
private LocalDateTime updateTime;
/** 删除标识(0-正常,1-删除) */
@TableLogic
@TableField(fill = FieldFill.INSERT)
@Schema(description = "删除标记,1:已删除,0:正常")
private String delFlag;
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -117,6 +117,6 @@ public class SysRouteConf extends Model<SysRouteConf> {
@TableLogic
@TableField(fill = FieldFill.INSERT)
@Schema(description = "删除标记,1:已删除,0:正常")
private String delFlag;
private String delFlag;
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -120,7 +120,7 @@ public class SysScheduleEntity extends Model<SysScheduleEntity> {
*/
@TableLogic
@Schema(description = "删除标记")
@TableField(fill = FieldFill.INSERT)
private String delFlag;
@TableField(fill = FieldFill.INSERT)
private String delFlag;
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -17,7 +17,7 @@
package com.pig4cloud.pigx.admin.api.entity;
import cn.idev.excel.annotation.ExcelProperty;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -0,0 +1,60 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*
*/
package com.pig4cloud.pigx.admin.api.feign;
import com.pig4cloud.pigx.admin.api.entity.SysApiKey;
import com.pig4cloud.pigx.common.core.constant.ServiceNameConstants;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.feign.annotation.NoToken;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PutMapping;
/**
* API密钥远程调用接口
*
* @author lengleng
* @date 2026-03-23
*/
@FeignClient(contextId = "remoteApiKeyService", value = ServiceNameConstants.UPMS_SERVICE)
public interface RemoteApiKeyService {
/**
* 根据 SHA-256 哈希值查询 API Key(仅限内部调用)
*
* @param hash API Key 的 SHA-256 哈希值
* @return 对应的 SysApiKey 记录,不存在时 data 为 null
*/
@NoToken
@GetMapping("/api-key/inner/hash/{hash}")
R<SysApiKey> getByHash(@PathVariable String hash);
/**
* 更新 API Key 的最后使用时间(仅限内部调用)
*
* @param id API Key 主键
* @return 更新是否成功
*/
@NoToken
@PutMapping("/api-key/inner/last-used/{id}")
R<Boolean> updateLastUsed(@PathVariable Long id);
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -38,22 +38,20 @@ import java.util.List;
@FeignClient(contextId = "remoteDataScopeService", value = ServiceNameConstants.UPMS_SERVICE)
public interface RemoteDataScopeService {
/**
* 通过角色ID 查询角色列表
*
* @param roleIdList 角色ID
* @return
*/
@PostMapping("/role/getRoleList")
R<List<SysRole>> getRoleList(@RequestBody List<String> roleIdList);
/**
* 通过角色ID 查询角色列表
* @param roleIdList 角色ID
* @return
*/
@PostMapping("/role/getRoleList")
R<List<SysRole>> getRoleList(@RequestBody List<String> roleIdList);
/**
* 获取子级部门
*
* @param deptId 部门ID
* @return
*/
@GetMapping("/dept/getDescendantList/{deptId}")
R<List<SysDept>> getDescendantList(@PathVariable("deptId") Long deptId);
/**
* 获取子级部门
* @param deptId 部门ID
* @return
*/
@GetMapping("/dept/getDescendantList/{deptId}")
R<List<SysDept>> getDescendantList(@PathVariable("deptId") Long deptId);
}
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -22,7 +22,6 @@ package com.pig4cloud.pigx.admin.api.feign;
import com.pig4cloud.pigx.admin.api.entity.SysDept;
import com.pig4cloud.pigx.common.core.constant.ServiceNameConstants;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.feign.annotation.NoToken;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -50,7 +49,6 @@ public interface RemoteDeptService {
* @param deptId 部门ID
* @return 负责人ID列表
*/
@NoToken
@GetMapping("/dept/leader/{deptId}")
R<List<Long>> getAllDeptLeader(@PathVariable("deptId") Long deptId);
@@ -1,7 +1,7 @@
package com.pig4cloud.pigx.admin.api.vo;
import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelProperty;
import org.apache.fesod.sheet.annotation.ExcelIgnore;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import com.pig4cloud.pigx.common.excel.annotation.ExcelLine;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@@ -1,7 +1,7 @@
package com.pig4cloud.pigx.admin.api.vo;
import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelProperty;
import org.apache.fesod.sheet.annotation.ExcelIgnore;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import com.pig4cloud.pigx.common.excel.annotation.ExcelLine;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@@ -1,7 +1,7 @@
package com.pig4cloud.pigx.admin.api.vo;
import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelProperty;
import org.apache.fesod.sheet.annotation.ExcelIgnore;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import com.pig4cloud.pigx.common.excel.annotation.ExcelLine;
import lombok.Data;
@@ -1,7 +1,7 @@
package com.pig4cloud.pigx.admin.api.vo;
import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelProperty;
import org.apache.fesod.sheet.annotation.ExcelIgnore;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import com.pig4cloud.pigx.common.excel.annotation.ExcelLine;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@@ -1,8 +1,8 @@
package com.pig4cloud.pigx.admin.api.vo;
import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import org.apache.fesod.sheet.annotation.ExcelIgnore;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import org.apache.fesod.sheet.annotation.write.style.ColumnWidth;
import com.pig4cloud.pigx.common.excel.annotation.ExcelLine;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
@@ -1,8 +1,8 @@
package com.pig4cloud.pigx.admin.api.vo;
import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import org.apache.fesod.sheet.annotation.ExcelIgnore;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import org.apache.fesod.sheet.annotation.write.style.ColumnWidth;
import com.pig4cloud.pigx.common.excel.annotation.ExcelLine;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@@ -1,8 +1,8 @@
package com.pig4cloud.pigx.admin.api.vo;
import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelProperty;
import cn.idev.excel.annotation.write.style.ColumnWidth;
import org.apache.fesod.sheet.annotation.ExcelIgnore;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import org.apache.fesod.sheet.annotation.write.style.ColumnWidth;
import com.pig4cloud.pigx.common.excel.annotation.ExcelLine;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@@ -1,7 +1,7 @@
package com.pig4cloud.pigx.admin.api.vo;
import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelProperty;
import org.apache.fesod.sheet.annotation.ExcelIgnore;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import com.pig4cloud.pigx.common.excel.annotation.ExcelLine;
import lombok.Data;
@@ -1,7 +1,7 @@
package com.pig4cloud.pigx.admin.api.vo;
import cn.idev.excel.annotation.ExcelIgnore;
import cn.idev.excel.annotation.ExcelProperty;
import org.apache.fesod.sheet.annotation.ExcelIgnore;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import com.pig4cloud.pigx.common.excel.annotation.ExcelLine;
import lombok.Data;
@@ -13,3 +13,4 @@ com.pig4cloud.pigx.admin.api.feign.RemoteAuditLogService
com.pig4cloud.pigx.admin.api.feign.RemoteMenuService
com.pig4cloud.pigx.admin.api.feign.RemoteRouteConfService
com.pig4cloud.pigx.admin.api.feign.RemoteMessageService
com.pig4cloud.pigx.admin.api.feign.RemoteApiKeyService
@@ -32,10 +32,15 @@ sys.menu.notFound=Menu does not exist
sys.menu.home.set.succeed=Homepage set successfully
sys.menu.home.cancel.succeed=Homepage removed successfully
sys.menu.button.cannot.home=Button-type menu cannot be set as homepage
sys.menu.sort.duplicate=Menu ID list cannot contain duplicates
sys.menu.sort.scopeInvalid=Menu sort data does not belong to the same parent node
# Dictionary related
sys.dict.delete.system=Built-in dictionary items cannot be deleted
sys.dict.update.system=Built-in dictionary items cannot be modified
sys.dict.notFound=Dictionary does not exist
sys.dict.item.sort.duplicate=Dictionary item ID list cannot contain duplicates
sys.dict.item.sort.scopeInvalid=Dictionary item sort data does not belong to the current dictionary
# Parameter related
sys.param.config.error=System parameter configuration error: {0}
@@ -32,10 +32,15 @@ sys.menu.notFound=\u83DC\u5355\u4E0D\u5B58\u5728
sys.menu.home.set.succeed=\u8BBE\u7F6E\u9996\u9875\u6210\u529F
sys.menu.home.cancel.succeed=\u53D6\u6D88\u9996\u9875\u6210\u529F
sys.menu.button.cannot.home=\u6309\u94AE\u7C7B\u578B\u4E0D\u80FD\u8BBE\u7F6E\u4E3A\u9996\u9875
sys.menu.sort.duplicate=\u83DC\u5355ID\u5217\u8868\u4E0D\u80FD\u91CD\u590D
sys.menu.sort.scopeInvalid=\u83DC\u5355\u6392\u5E8F\u6570\u636E\u4E0D\u5C5E\u4E8E\u540C\u4E00\u7236\u8282\u70B9
# 字典相关
sys.dict.delete.system=\u7CFB\u7EDF\u5185\u7F6E\u5B57\u5178\u9879\u76EE\u4E0D\u80FD\u5220\u9664
sys.dict.update.system=\u7CFB\u7EDF\u5185\u7F6E\u5B57\u5178\u9879\u76EE\u4E0D\u80FD\u4FEE\u6539
sys.dict.notFound=\u5B57\u5178\u4E0D\u5B58\u5728
sys.dict.item.sort.duplicate=\u5B57\u5178\u9879ID\u5217\u8868\u4E0D\u80FD\u91CD\u590D
sys.dict.item.sort.scopeInvalid=\u5B57\u5178\u9879\u6392\u5E8F\u6570\u636E\u4E0D\u5C5E\u4E8E\u5F53\u524D\u5B57\u5178
# 参数相关
sys.param.config.error={0} \u7CFB\u7EDF\u53C2\u6570\u914D\u7F6E\u9519\u8BEF
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -37,8 +37,8 @@ import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
public class PigxAdminApplication {
public static void main(String[] args) {
SpringApplication.run(PigxAdminApplication.class, args);
}
public static void main(String[] args) {
SpringApplication.run(PigxAdminApplication.class, args);
}
}
@@ -9,7 +9,7 @@ import com.pig4cloud.pigx.common.data.tenant.TenantBroker;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.web.context.WebServerInitializedEvent;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
@@ -38,11 +38,11 @@ public class ClientDetailsInitRunner implements InitializingBean {
private final SysTenantService tenantService;
/**
* WebServerInitializedEvent 使用 TransactionalEventListener 时启动时无法获取到事件
* ApplicationReadyEvent 使用 TransactionalEventListener 时启动时无法获取到事件
*/
@Async
@Order
@EventListener({ WebServerInitializedEvent.class })
@EventListener({ApplicationReadyEvent.class})
public void webServerInit() {
this.initClientDetails();
}
@@ -63,7 +63,7 @@ public class ClientDetailsInitRunner implements InitializingBean {
// 3. 拼接key 1:client_config_flag:clinetId
String key = String.format("%s:%s:%s", tenantId, CacheConstants.CLIENT_FLAG, client.getClientId());
// 4. hashkey clientId 保存客户端信息
RedisUtils.set(key, client.getAdditionalInformation());
RedisUtils.set(key, client.getAdditionalInformation());
});
});
});
@@ -26,7 +26,7 @@ import com.pig4cloud.pigx.common.gateway.vo.RouteDefinitionVo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.web.context.WebServerInitializedEvent;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
@@ -37,7 +37,6 @@ import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.scheduling.annotation.Async;
import org.springframework.transaction.event.TransactionalEventListener;
@@ -54,6 +53,7 @@ import java.util.Map;
@Configuration
@RequiredArgsConstructor
@ConditionalOnDiscoveryEnabled
@SuppressWarnings("removal")
public class DynamicRouteInitRunner implements InitializingBean {
private final RedisTemplate<String, String> redisTemplate;
@@ -63,11 +63,11 @@ public class DynamicRouteInitRunner implements InitializingBean {
private final RedisMessageListenerContainer listenerContainer;
/**
* WebServerInitializedEvent 使用 TransactionalEventListener 时启动时无法获取到事件
* ApplicationReadyEvent 使用 TransactionalEventListener 时启动时无法获取到事件
*/
@Async
@Order
@EventListener({WebServerInitializedEvent.class})
@EventListener({ApplicationReadyEvent.class})
public void WebServerInit() {
this.initRoute();
}
@@ -93,7 +93,8 @@ public class DynamicRouteInitRunner implements InitializingBean {
vo.setMetadata(JSONUtil.toBean(route.getMetadata(), Map.class));
log.info("加载路由ID:{},{}", route.getRouteId(), vo);
redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(RouteDefinitionVo.class));
redisTemplate.setHashValueSerializer(
new org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer<>(RouteDefinitionVo.class));
HashOperations<String, String, RouteDefinitionVo> stringStringValueOperations = redisTemplate.opsForHash();
stringStringValueOperations.put(CacheConstants.ROUTE_KEY, route.getRouteId(), vo);
});
@@ -1,50 +0,0 @@
package com.pig4cloud.pigx.admin.controller;
import com.pig4cloud.pigx.admin.api.dto.AiChatMessageDTO;
import io.github.pigmesh.ai.deepseek.core.DeepSeekClient;
import io.github.pigmesh.ai.deepseek.core.chat.ChatCompletionResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.util.Optional;
/**
* AI 聊天控制器
*
* @author lengleng
* @date 2025/02/20
*/
@Slf4j
@RestController
@RequestMapping("/ai")
@RequiredArgsConstructor
public class AiChatController {
private final Optional<DeepSeekClient> deepSeekClientOptional;
/**
* 聊天
*
* @param message 消息
* @return {@link Flux }<{@link ChatCompletionResponse }>
*/
@PostMapping("/chat")
public Flux<ChatCompletionResponse> chat(@RequestBody AiChatMessageDTO message) {
if (deepSeekClientOptional.isEmpty()) {
log.warn("DeepSeek 大模型聊天未开启,请检查配置");
return Flux.empty();
}
if (message.isWebSearch()) {
return deepSeekClientOptional.get().chatSearchCompletion(message.getMessage());
}
return deepSeekClientOptional.get().chatFluxCompletion(message.getMessage());
}
}
@@ -0,0 +1,180 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*
*/
package com.pig4cloud.pigx.admin.controller;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.pig4cloud.pigx.admin.api.dto.ApiKeyCreateDTO;
import com.pig4cloud.pigx.admin.api.entity.SysApiKey;
import com.pig4cloud.pigx.admin.service.SysApiKeyService;
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.core.constant.enums.YesNoEnum;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.log.annotation.SysLog;
import com.pig4cloud.pigx.common.security.annotation.HasPermission;
import com.pig4cloud.pigx.common.security.annotation.Inner;
import com.pig4cloud.pigx.common.security.util.SecurityUtils;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
/**
* API密钥管理
*
* @author lengleng
* @date 2026-03-23
*/
@RestController
@AllArgsConstructor
@RequestMapping("/api-key")
@Tag(description = "api-key", name = "API密钥管理")
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
public class SysApiKeyController {
private final SysApiKeyService apiKeyService;
/**
* 创建 API Key
* <p>
* 业务逻辑由 Service 完成:校验当前密码(防 CSRF)、数量上限(每用户最多 5 个)、
* 生成 sk- 前缀密钥并只存储 SHA-256 哈希。明文 Key 仅在本次响应中返回,之后不可再取。
*
* @param dto 创建请求(name、allowedIps、expiresAt、password)
* @return 明文 API Key,格式:sk-xxx
*/
@SysLog("创建API密钥")
@PostMapping
public R<String> create(@Valid @RequestBody ApiKeyCreateDTO dto) {
return apiKeyService.createApiKey(dto);
}
/**
* 查询当前用户的 API Key 列表
* <p>
* 仅返回当前登录用户自己的 Key,按创建时间倒序排列。
* 列表中不包含明文 Key 和哈希值,仅展示元数据。
*/
@GetMapping("/list")
public R list() {
Long userId = SecurityUtils.getUser().getId();
return R.ok(apiKeyService.list(Wrappers.<SysApiKey>lambdaQuery()
.eq(SysApiKey::getUserId, userId)
.orderByDesc(SysApiKey::getCreateTime)));
}
/**
* 管理员分页查询所有用户的 API Key
* <p>
* 需要 admin_apikey_view 权限。支持按 username 模糊搜索。
*
* @param page 分页参数
* @param sysApiKey 查询条件(username 字段用于模糊匹配)
*/
@GetMapping("/page")
@HasPermission("sys_social_details_view")
public R page(@ParameterObject Page page, @ParameterObject SysApiKey sysApiKey) {
return apiKeyService.page(page, sysApiKey);
}
/**
* 更新 API Key 元数据
* <p>
* 仅允许修改 name、allowedIps、expiresAt、status,不可修改 apiKeyHash。
* Service 层校验归属权,并在更新后精确清除对应缓存。
*
* @param apiKey 待更新字段(必须包含 id)
*/
@SysLog("更新API密钥")
@PutMapping
public R update(@RequestBody SysApiKey apiKey) {
return apiKeyService.updateApiKey(apiKey);
}
/**
* 删除当前用户自己的 API Key
* <p>
* Service 层校验归属权后删除,并精确清除该 Key 的缓存。
*
* @param id API Key 主键
*/
@SysLog("删除API密钥")
@DeleteMapping("/{id}")
@HasPermission("sys_social_details_del")
public R delete(@PathVariable Long id) {
return apiKeyService.deleteApiKey(id);
}
/**
* 管理员批量删除 API Key
* <p>
* 需要 admin_apikey_del 权限。Service 层逐条清除缓存后执行批量删除。
*
* @param ids 待删除的 API Key 主键数组
*/
@SysLog("批量删除API密钥")
@DeleteMapping
@HasPermission("sys_social_details_del")
public R deleteBatch(@RequestBody Long[] ids) {
return apiKeyService.deleteBatchApiKey(ids);
}
/**
* 内部接口:根据 SHA-256 哈希值查询有效的 API Key
* <p>
* 仅供 pigx-common-security 的 token 校验流程调用,不对外暴露。
* 结果会被 Spring Cache 缓存(key 为 hash),避免每次请求都查库。
*
* @param hash API Key 的 SHA-256 哈希值
* @return 状态正常的 SysApiKey,不存在或已禁用时 data 为 null
*/
@Inner
@GetMapping("/inner/hash/{hash}")
@Cacheable(value = CacheConstants.API_KEY_DETAILS, key = "#hash", unless = "#result.data == null")
public R<SysApiKey> getByHash(@PathVariable String hash) {
return R.ok(apiKeyService.getOne(Wrappers.<SysApiKey>lambdaQuery()
.eq(SysApiKey::getApiKeyHash, hash)
.eq(SysApiKey::getStatus, YesNoEnum.NO.getCode())));
}
/**
* 内部接口:更新 API Key 最后使用时间
* <p>
* 由 token 校验流程在认证成功后异步调用,记录 Key 的活跃时间。
* 该操作不影响认证结果,失败时仅打印警告日志。
*
* @param id API Key 主键
*/
@Inner
@PutMapping("/inner/last-used/{id}")
public R<Boolean> updateLastUsed(@PathVariable Long id) {
SysApiKey update = new SysApiKey();
update.setId(id);
update.setLastUsedAt(LocalDateTime.now());
return R.ok(apiKeyService.updateById(update));
}
}
@@ -129,7 +129,7 @@ public class SysAreaController {
@HasPermission("sys_sysArea_export")
public List<SysAreaEntity> export(SysAreaEntity sysArea, Long[] ids) {
return sysAreaService.list(Wrappers.lambdaQuery(sysArea)
.in(ArrayUtil.isNotEmpty(ids), SysAreaEntity::getId, CollUtil.toList(ids)));
}
.in(ArrayUtil.isNotEmpty(ids), SysAreaEntity::getId, CollUtil.toList(ids)));
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -111,8 +111,8 @@ public class SysAuditLogController {
@GetMapping("/export")
@HasPermission("sys_audit_export")
public List<SysAuditLog> export(SysAuditLog sysAuditLog, Long[] ids) {
return sysAuditLogService.list(Wrappers.lambdaQuery(sysAuditLog)
.in(ArrayUtil.isNotEmpty(ids), SysAuditLog::getId, CollUtil.toList(ids)));
return sysAuditLogService.list(Wrappers.lambdaQuery(sysAuditLog)
.in(ArrayUtil.isNotEmpty(ids), SysAuditLog::getId, CollUtil.toList(ids)));
}
}
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -27,7 +27,6 @@ import com.pig4cloud.pigx.common.excel.annotation.RequestExcel;
import com.pig4cloud.pigx.common.excel.annotation.ResponseExcel;
import com.pig4cloud.pigx.common.log.annotation.SysLog;
import com.pig4cloud.pigx.common.security.annotation.HasPermission;
import com.pig4cloud.pigx.common.security.annotation.Inner;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
@@ -130,7 +129,6 @@ public class SysDeptController {
return R.ok(sysDeptService.listDescendant(deptId));
}
@Inner
@GetMapping(value = "/leader/{deptId}")
public R getAllDeptLeader(@PathVariable Long deptId) {
return R.ok(sysDeptService.listDeptLeader(deptId));
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -23,6 +23,7 @@ import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.pig4cloud.pigx.admin.api.dto.SysDictItemSortDTO;
import com.pig4cloud.pigx.admin.api.entity.SysDict;
import com.pig4cloud.pigx.admin.api.entity.SysDictItem;
import com.pig4cloud.pigx.admin.service.SysDictItemService;
@@ -112,7 +113,8 @@ public class SysDictController {
return R.ok(sysDictItemService.list(Wrappers.<SysDictItem>query()
.lambda()
.eq(SysDictItem::getDictType, type)
.orderByDesc(SysDictItem::getSortOrder)));
.orderByAsc(SysDictItem::getSortOrder)
.orderByAsc(SysDictItem::getId)));
}
/**
@@ -180,7 +182,9 @@ public class SysDictController {
*/
@GetMapping("/item/page")
public R getSysDictItemPage(Page page, SysDictItem sysDictItem) {
return R.ok(sysDictItemService.page(page, Wrappers.query(sysDictItem)));
return R.ok(sysDictItemService.page(page, Wrappers.<SysDictItem>lambdaQuery(sysDictItem)
.orderByAsc(SysDictItem::getSortOrder)
.orderByAsc(SysDictItem::getId)));
}
/**
@@ -227,6 +231,18 @@ public class SysDictController {
return sysDictItemService.updateDictItem(sysDictItem);
}
/**
* 更新字典项排序
*
* @param sortDTO 字典项排序信息
* @return R
*/
@SysLog("更新字典项排序")
@PutMapping("/item/sort")
public R sortDictItem(@Valid @RequestBody SysDictItemSortDTO sortDTO) {
return sysDictItemService.updateDictItemSort(sortDTO);
}
/**
* 通过id删除字典项
* @param id id
@@ -247,12 +263,12 @@ public class SysDictController {
@PutMapping("/sync")
public R sync() {
return sysDictService.syncDictCache();
}
}
@ResponseExcel
@GetMapping("/export")
public List<SysDictItem> export(SysDictItem sysDictItem) {
return sysDictItemService.list(Wrappers.query(sysDictItem));
}
@ResponseExcel
@GetMapping("/export")
public List<SysDictItem> export(SysDictItem sysDictItem) {
return sysDictItemService.list(Wrappers.query(sysDictItem));
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -125,7 +125,7 @@ public class SysFileController {
* @param fileName 自定义文件名
* @return 文件上传结果
*/
@Inner
@Inner(value = false)
@PostMapping(value = "/inner/upload")
public R innerUpload(@RequestPart("file") MultipartFile file,
@RequestParam(value = "dir", required = false) String dir,
@@ -208,10 +208,10 @@ public class SysFileController {
* 移动文件组
* @param fileGroupDTO SysFileGroupDTO对象,要移动的文件组信息
* @return 包含移动结果的R对象
*/
@PutMapping("/group/move")
public R moveFileGroup(@RequestBody SysFileGroupDTO fileGroupDTO) {
return R.ok(sysFileService.moveFileGroup(fileGroupDTO));
}
*/
@PutMapping("/group/move")
public R moveFileGroup(@RequestBody SysFileGroupDTO fileGroupDTO) {
return R.ok(sysFileService.moveFileGroup(fileGroupDTO));
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -30,7 +30,6 @@ import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.excel.annotation.ResponseExcel;
import com.pig4cloud.pigx.common.log.annotation.SysLog;
import com.pig4cloud.pigx.common.security.annotation.HasPermission;
import com.pig4cloud.pigx.common.security.annotation.Inner;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -55,115 +54,110 @@ import java.util.List;
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
public class SysI18nController {
private final SysI18nService sysI18nService;
private final SysI18nService sysI18nService;
/**
* 分页查询
* @param page 分页对象
* @param sysI18n 系统表-国际化
* @return
*/
@Operation(summary = "分页查询", description = "分页查询")
@GetMapping("/page")
@HasPermission("sys_i18n_view")
public R getsysI18nPage(@ParameterObject Page page, @ParameterObject SysI18nEntity sysI18n) {
LambdaQueryWrapper<SysI18nEntity> wrapper = Wrappers.lambdaQuery();
wrapper.like(StrUtil.isNotBlank(sysI18n.getName()), SysI18nEntity::getName, sysI18n.getName());
wrapper.like(StrUtil.isNotBlank(sysI18n.getZhCn()), SysI18nEntity::getZhCn, sysI18n.getZhCn());
wrapper.like(StrUtil.isNotBlank(sysI18n.getEn()), SysI18nEntity::getEn, sysI18n.getEn());
return R.ok(sysI18nService.page(page, wrapper));
}
/**
* 分页查询
*
* @param page 分页对象
* @param sysI18n 系统表-国际化
* @return
*/
@Operation(summary = "分页查询", description = "分页查询")
@GetMapping("/page")
@HasPermission("sys_i18n_view")
public R getsysI18nPage(@ParameterObject Page page, @ParameterObject SysI18nEntity sysI18n) {
LambdaQueryWrapper<SysI18nEntity> wrapper = Wrappers.lambdaQuery();
wrapper.like(StrUtil.isNotBlank(sysI18n.getName()), SysI18nEntity::getName, sysI18n.getName());
wrapper.like(StrUtil.isNotBlank(sysI18n.getZhCn()), SysI18nEntity::getZhCn, sysI18n.getZhCn());
wrapper.like(StrUtil.isNotBlank(sysI18n.getEn()), SysI18nEntity::getEn, sysI18n.getEn());
return R.ok(sysI18nService.page(page, wrapper));
}
/**
* 通过id查询系统表-国际化
* @param id id
* @return R
*/
@Operation(summary = "通过id查询", description = "通过id查询")
@GetMapping("/details/{id}")
public R getById(@PathVariable("id") Long id) {
return R.ok(sysI18nService.getById(id));
}
/**
* 通过id查询系统表-国际化
*
* @param id id
* @return R
*/
@Operation(summary = "通过id查询", description = "通过id查询")
@GetMapping("/details/{id}")
public R getById(@PathVariable("id") Long id) {
return R.ok(sysI18nService.getById(id));
}
@GetMapping("/details")
public R getDetails(@ParameterObject SysI18nEntity entity) {
return R.ok(sysI18nService.getOne(Wrappers.query(entity)));
}
@GetMapping("/details")
public R getDetails(@ParameterObject SysI18nEntity entity) {
return R.ok(sysI18nService.getOne(Wrappers.query(entity)));
}
/**
* 新增系统表-国际化
* @param sysI18n 系统表-国际化
* @return R
*/
@Operation(summary = "新增系统表-国际化", description = "新增系统表-国际化")
@SysLog("新增系统表-国际化")
@PostMapping
@CacheEvict(value = CacheConstants.I18N_DETAILS, allEntries = true)
@HasPermission("sys_i18n_add")
public R save(@RequestBody SysI18nEntity sysI18n) {
return R.ok(sysI18nService.save(sysI18n));
}
/**
* 新增系统表-国际化
*
* @param sysI18n 系统表-国际化
* @return R
*/
@Operation(summary = "新增系统表-国际化", description = "新增系统表-国际化")
@SysLog("新增系统表-国际化")
@PostMapping
@HasPermission("sys_i18n_add")
@CacheEvict(value = {CacheConstants.SITE_CONFIG_DETAILS}, allEntries = true)
public R save(@RequestBody SysI18nEntity sysI18n) {
return R.ok(sysI18nService.save(sysI18n));
}
/**
* 修改系统表-国际化
* @param sysI18n 系统表-国际化
* @return R
*/
@Operation(summary = "修改系统表-国际化", description = "修改系统表-国际化")
@SysLog("修改系统表-国际化")
@PutMapping
@CacheEvict(value = CacheConstants.I18N_DETAILS, allEntries = true)
@HasPermission("sys_i18n_edit")
public R updateById(@RequestBody SysI18nEntity sysI18n) {
return R.ok(sysI18nService.updateById(sysI18n));
}
/**
* 修改系统表-国际化
*
* @param sysI18n 系统表-国际化
* @return R
*/
@Operation(summary = "修改系统表-国际化", description = "修改系统表-国际化")
@SysLog("修改系统表-国际化")
@PutMapping
@CacheEvict(value = {CacheConstants.SITE_CONFIG_DETAILS}, allEntries = true)
@HasPermission("sys_i18n_edit")
public R updateById(@RequestBody SysI18nEntity sysI18n) {
return R.ok(sysI18nService.updateById(sysI18n));
}
/**
* 通过id删除系统表-国际化
* @param ids id 列表
* @return R
*/
@Operation(summary = "通过id删除系统表-国际化", description = "通过id删除系统表-国际化")
@SysLog("通过id删除系统表-国际化")
@DeleteMapping
@CacheEvict(value = CacheConstants.I18N_DETAILS, allEntries = true)
@HasPermission("sys_i18n_del")
public R removeById(@RequestBody Long[] ids) {
return R.ok(sysI18nService.removeBatchByIds(CollUtil.toList(ids)));
}
/**
* 通过id删除系统表-国际化
*
* @param ids id 列表
* @return R
*/
@Operation(summary = "通过id删除系统表-国际化", description = "通过id删除系统表-国际化")
@SysLog("通过id删除系统表-国际化")
@DeleteMapping
@HasPermission("sys_i18n_del")
@CacheEvict(value = {CacheConstants.SITE_CONFIG_DETAILS}, allEntries = true)
public R removeById(@RequestBody Long[] ids) {
return R.ok(sysI18nService.removeBatchByIds(CollUtil.toList(ids)));
}
/**
* 导出excel 表格
* @param sysI18n 查询条件
* @return excel 文件流
*/
@ResponseExcel
@GetMapping("/export")
@HasPermission("sys_i18n_export")
public List<SysI18nEntity> export(SysI18nEntity sysI18n, Long[] ids) {
return sysI18nService.list(Wrappers.lambdaQuery(sysI18n)
.in(ArrayUtil.isNotEmpty(ids), SysI18nEntity::getId, CollUtil.toList(ids)));
}
/**
* 导出excel 表格
*
* @param sysI18n 查询条件
* @return excel 文件流
*/
@ResponseExcel
@GetMapping("/export")
@HasPermission("sys_i18n_export")
public List<SysI18nEntity> export(SysI18nEntity sysI18n, Long[] ids) {
return sysI18nService.list(Wrappers.lambdaQuery(sysI18n).in(ArrayUtil.isNotEmpty(ids), SysI18nEntity::getId, CollUtil.toList(ids)));
}
/**
* 获取系统I18N配置
* @return I18N 配置
*/
@Operation(summary = "获取系统配置-国际化", description = "获取系统配置-国际化")
@Inner(false)
@GetMapping("/info")
public R list() {
return R.ok(sysI18nService.listMap());
}
/**
* 同步数据
* @return R
*/
@SysLog("同步数据")
@PutMapping("/sync")
public R sync() {
return sysI18nService.syncI18nCache();
}
/**
* 同步数据
*
* @return R
*/
@SysLog("同步数据")
@PutMapping("/sync")
public R sync() {
return sysI18nService.syncI18nCache();
}
}
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -120,8 +120,8 @@ public class SysLogController {
@GetMapping("/export")
@HasPermission("sys_log_export")
public List<SysLog> export(SysLog sysLog, Long[] ids) {
return sysLogService
.list(Wrappers.lambdaQuery(sysLog).in(Objects.nonNull(ids), SysLog::getId, CollUtil.toList(ids)));
return sysLogService
.list(Wrappers.lambdaQuery(sysLog).in(Objects.nonNull(ids), SysLog::getId, CollUtil.toList(ids)));
}
}
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -20,8 +20,10 @@
package com.pig4cloud.pigx.admin.controller;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.pig4cloud.pigx.admin.api.dto.SysMenuSortDTO;
import com.pig4cloud.pigx.admin.api.entity.SysMenu;
import com.pig4cloud.pigx.admin.service.SysMenuService;
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.log.annotation.SysLog;
import com.pig4cloud.pigx.common.security.annotation.HasPermission;
@@ -31,6 +33,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.*;
@@ -139,4 +142,34 @@ public class SysMenuController {
return R.ok(sysMenuService.updateMenuById(sysMenu));
}
/**
* 更新菜单同级排序
*
* @param sortDTO 菜单排序信息
* @return success/false
*/
@SysLog("更新菜单排序")
@PutMapping("/sort")
@HasPermission("sys_menu_edit")
public R sort(@Valid @RequestBody SysMenuSortDTO sortDTO) {
return sysMenuService.updateMenuSort(sortDTO);
}
/**
* 刷新指定角色的菜单权限缓存
* <p>
* menu_details 缓存 key = roleId,精确清除该角色的菜单缓存,
* 与 SysRoleMenuServiceImpl.saveRoleMenus 的 @CacheEvict(key = "#roleId") 逻辑保持一致。
*
* @param roleId 角色ID
* @return success/fail
*/
@SysLog("刷新角色菜单缓存")
@DeleteMapping("/cache/{roleId}")
@HasPermission("sys_role_edit")
@CacheEvict(value = CacheConstants.MENU_DETAILS, key = "#roleId")
public R clearMenuCache(@PathVariable Long roleId) {
return R.ok();
}
}
@@ -185,14 +185,18 @@ public class SysMessageController {
/**
* 发送短信验证码
*
* @param mobile 手机号
* @param registered 是否已注册,默认true
* @param mobile 手机号
* @param registered 是否已注册,默认true
* @param captchaType 图形验证码类型
* @param captchaVerification 图形验证码凭据
* @return 发送结果
*/
@Inner(value = false)
@GetMapping("/send/smsCode")
public R sendSmsCode(@RequestParam String mobile, @RequestParam(required = false, defaultValue = "true") boolean registered) {
return sysMessageService.sendSmsCode(mobile, registered);
public R sendSmsCode(@RequestParam String mobile, @RequestParam(required = false, defaultValue = "true") boolean registered,
@RequestParam(required = false) String captchaType,
@RequestParam(required = false) String captchaVerification) {
return sysMessageService.sendSmsCode(mobile, registered, captchaType, captchaVerification);
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -144,7 +144,7 @@ public class SysPublicParamController {
/**
* 通过id删除公共参数
* @param publicId id
* @param ids id
* @return R
*/
@Operation(description = "删除公共参数", summary = "删除公共参数")
@@ -2,7 +2,9 @@ package com.pig4cloud.pigx.admin.controller;
import com.pig4cloud.pigx.admin.api.dto.RegisterUserDTO;
import com.pig4cloud.pigx.admin.service.SysUserService;
import com.pig4cloud.pigx.common.core.constant.enums.YesNoEnum;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.data.resolver.ParamResolver;
import com.pig4cloud.pigx.common.log.annotation.SysLog;
import com.pig4cloud.pigx.common.security.annotation.Inner;
import lombok.RequiredArgsConstructor;
@@ -33,6 +35,9 @@ public class SysRegisterController {
@PostMapping("/user")
@ConditionalOnProperty(name = "register.user", matchIfMissing = true)
public R<Boolean> registerUser(@RequestBody RegisterUserDTO userDto) {
if (!YesNoEnum.YES.getCode().equals(ParamResolver.getStr("SITE_REGISTER_ENABLE", YesNoEnum.NO.getCode()))) {
return R.failed("系统已关闭注册功能");
}
return userService.registerUser(userDto);
}
@@ -59,6 +64,6 @@ public class SysRegisterController {
@PostMapping("/forget/{code}")
public R<Boolean> forgetUserPassword(@RequestBody RegisterUserDTO userDto, @PathVariable String code) {
return userService.forgetUserPassword(userDto, code);
}
}
}
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -24,9 +24,11 @@ import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.pig4cloud.pigx.admin.api.entity.SysRole;
import com.pig4cloud.pigx.admin.api.entity.SysRoleWidget;
import com.pig4cloud.pigx.admin.api.vo.RoleExcelVO;
import com.pig4cloud.pigx.admin.api.vo.RoleMenuVO;
import com.pig4cloud.pigx.admin.service.SysRoleService;
import com.pig4cloud.pigx.admin.service.SysRoleWidgetService;
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.excel.annotation.RequestExcel;
@@ -58,6 +60,8 @@ public class SysRoleController {
private final SysRoleService sysRoleService;
private final SysRoleWidgetService sysRoleWidgetService;
/**
* 通过ID查询角色信息
* @param id ID
@@ -186,4 +190,36 @@ public class SysRoleController {
return sysRoleService.importRole(excelVOList, bindingResult);
}
/**
* 查询当前用户角色的首页widget配置(取 widgetKeys 最多的那条)
* @return widget配置,无配置时返回 null
*/
@GetMapping("/widget/current")
public R getCurrentUserWidget() {
return R.ok(sysRoleWidgetService.getByCurrentUser());
}
/**
* 查询角色的首页widget配置
* @param roleId 角色ID
* @return widget配置,无配置时返回 null
*/
@GetMapping("/widget/{roleId}")
@HasPermission("sys_role_view")
public R getRoleWidget(@PathVariable Long roleId) {
return R.ok(sysRoleWidgetService.getByRoleId(roleId));
}
/**
* 保存角色的首页widget配置
* @param sysRoleWidget widget配置对象
* @return success/fail
*/
@SysLog("保存角色首页配置")
@PutMapping("/widget")
@HasPermission("sys_role_edit")
public R saveRoleWidget(@RequestBody SysRoleWidget sysRoleWidget) {
return R.ok(sysRoleWidgetService.saveOrUpdateByRoleId(sysRoleWidget));
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -82,6 +82,6 @@ public class SysRouteConfController {
@HasPermission("sys_route_manage")
public R saveRoute(@RequestBody SysRouteConf routeConf) {
return R.ok(sysRouteConfService.saveRoute(routeConf));
}
}
}
@@ -175,10 +175,10 @@ public class SysSensitiveWordController {
@HasPermission("admin_sysSensitiveWord_del")
public R refresh() {
RedisUtils.execute((RedisCallback<Void>) connection -> {
connection.publish(CacheConstants.SENSITIVE_REDIS_RELOAD_TOPIC.getBytes(), "刷新敏感词缓存".getBytes());
return null;
});
return R.ok();
}
connection.publish(CacheConstants.SENSITIVE_REDIS_RELOAD_TOPIC.getBytes(), "刷新敏感词缓存".getBytes());
return null;
});
return R.ok();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -72,7 +72,7 @@ public class SysSocialDetailsController {
*/
@Inner(value = false)
@GetMapping("/getLoginAppList")
public R getLoginAppList() {
public R getLoginAppList() {
return R.ok(sysSocialDetailsService.selectList());
}
@@ -156,8 +156,8 @@ public class SysSocialDetailsController {
*/
@GetMapping("/export")
public List<SysSocialDetails> export(SysSocialDetails sysSocialDetails, Long[] ids) {
return sysSocialDetailsService.list(Wrappers.lambdaQuery(sysSocialDetails)
.in(ArrayUtil.isNotEmpty(ids), SysSocialDetails::getId, CollUtil.toList(ids)));
return sysSocialDetailsService.list(Wrappers.lambdaQuery(sysSocialDetails)
.in(ArrayUtil.isNotEmpty(ids), SysSocialDetails::getId, CollUtil.toList(ids)));
}
}
@@ -117,7 +117,7 @@ public class SysSystemConfigController {
@HasPermission("sys_message_export")
public List<SysSystemConfigEntity> export(SysSystemConfigEntity sysSystemConfig, Long[] ids) {
return sysSystemConfigService.list(Wrappers.lambdaQuery(sysSystemConfig)
.in(ArrayUtil.isNotEmpty(ids), SysSystemConfigEntity::getId, CollUtil.toList(ids)));
}
.in(ArrayUtil.isNotEmpty(ids), SysSystemConfigEntity::getId, CollUtil.toList(ids)));
}
}
@@ -1,21 +1,18 @@
package com.pig4cloud.pigx.admin.controller;
import com.pig4cloud.pigx.admin.api.constant.UpmsErrorCodes;
import com.pig4cloud.pigx.common.core.util.MsgUtils;
import com.pig4cloud.pigx.admin.dto.SiteConfigDTO;
import com.pig4cloud.pigx.admin.service.SysSiteConfigService;
import com.pig4cloud.pigx.admin.service.SysSystemInfoService;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.data.cache.RedisUtils;
import com.pig4cloud.pigx.common.security.annotation.HasPermission;
import com.pig4cloud.pigx.common.security.annotation.Inner;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.connection.RedisServerCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.Map;
@RestController
@RequestMapping("/system")
@@ -24,37 +21,68 @@ import java.util.*;
@SecurityRequirement(name = HttpHeaders.AUTHORIZATION)
public class SysSystemInfoController {
private final SysSystemInfoService sysSystemInfoService;
private final SysSiteConfigService sysSiteConfigService;
/**
* 缓存监控
*
* @return R<Object>
*/
@HasPermission("sys_cache_view")
@GetMapping("/cache")
public R cache() {
Properties info = RedisUtils.execute((RedisCallback<Properties>) RedisServerCommands::info);
Properties commandStats = RedisUtils
.execute((RedisCallback<Properties>) connection -> connection.serverCommands().info("commandstats"));
Object dbSize = RedisUtils.execute((RedisCallback<Object>) RedisServerCommands::dbSize);
if (commandStats == null) {
return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_SYSTEM_CACHE_FETCH_ERROR));
}
Map<String, Object> result = new HashMap<>(3);
result.put("info", info);
result.put("dbSize", dbSize);
List<Map<String, String>> pieList = new ArrayList<>();
commandStats.stringPropertyNames().forEach(key -> {
Map<String, String> data = new HashMap<>(2);
String property = commandStats.getProperty(key);
data.put("name", StringUtils.removeStart(key, "cmdstat_"));
data.put("value", StringUtils.substringBetween(property, "calls=", ",usec"));
pieList.add(data);
});
result.put("commandStats", pieList);
return R.ok(result);
public R<Map<String, Object>> cache() {
return sysSystemInfoService.getCacheInfo();
}
/**
* Clarity 站点监控数据
*
* @return R<?>
*/
@HasPermission("sys_clarity_view")
@GetMapping("/clarity")
public R<?> clarity(@RequestParam(defaultValue = "1") Integer numOfDays) {
return sysSystemInfoService.getClarityData(numOfDays);
}
/**
* 聚合配置(i18n + site 配置)- 公开接口
*/
@Inner(false)
@GetMapping("/config")
public R<Map<String, Object>> config() {
return R.ok(sysSiteConfigService.getAggregatedConfig());
}
/**
* 获取网站配置(管理端用)
*/
@HasPermission("sys_site_config_view")
@GetMapping("/site-config")
public R<SiteConfigDTO> getSiteConfig() {
return R.ok(sysSiteConfigService.getSiteConfig());
}
/**
* 更新网站配置
*/
@HasPermission("sys_site_config_edit")
@PutMapping("/site-config")
public R<Void> updateSiteConfig(@RequestBody SiteConfigDTO dto) {
sysSiteConfigService.updateSiteConfig(dto);
return R.ok();
}
/**
* 刷新网站配置缓存(清空 Redis)
*/
@HasPermission("sys_site_config_edit")
@DeleteMapping("/site-config/refresh")
public R<Void> refreshSiteConfig() {
sysSiteConfigService.refreshCache();
return R.ok();
}
}
@@ -0,0 +1,90 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.admin.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 站点配置DTO
*
* @author lengleng
* @date 2026-03-27
*/
@Data
@Schema(description = "站点配置")
public class SiteConfigDTO {
@Schema(description = "Clarity 统计ID")
private String clarityId;
@Schema(description = "验证码类型:clickWord=点按,blockPuzzle=滑动,math=图形,none=关闭")
private String captchaType;
@Schema(description = "密码规则预设:letterNumber=英文+数字,letterNumberSymbol=英文+数字+符号,upperLowerNumberSymbol=大小写+数字+符号")
private String passwordRule;
@Schema(description = "是否强制重置密码")
private Boolean forceResetPwd;
@Schema(description = "是否强制下线")
private Boolean forceLogout;
@Schema(description = "是否开启短信登录")
private Boolean smsLoginEnable;
@Schema(description = "是否开启社交登录")
private Boolean socialLoginEnable;
@Schema(description = "是否开启注册")
private Boolean registerEnable;
@Schema(description = "是否开启找回/重置密码")
private Boolean resetPassword;
@Schema(description = "是否开启国际化")
private Boolean i18nEnable;
@Schema(description = "是否开启暗黑模式")
private Boolean darkModeEnable;
@Schema(description = "是否开启反调试保护")
private Boolean antiDebugEnable;
@Schema(description = "反调试绕过密钥")
private String antiDebugKey;
@Schema(description = "是否开启钉钉导入")
private Boolean syncDingtalkEnabled;
@Schema(description = "是否开启企业微信导入")
private Boolean syncWechatEnabled;
@Schema(description = "站点标题")
private String title;
@Schema(description = "页脚信息")
private String footer;
@Schema(description = "隐私提示")
private String privacyTip;
@Schema(description = "网站 Logo 图片路径")
private String logo;
}
@@ -0,0 +1,202 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.admin.handler;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.pig4cloud.pigx.admin.api.constant.ClarityFetchStatus;
import com.pig4cloud.pigx.admin.api.entity.SysClarityData;
import com.pig4cloud.pigx.admin.api.entity.SysSocialDetails;
import com.pig4cloud.pigx.admin.mapper.SysClarityDataMapper;
import com.pig4cloud.pigx.admin.mapper.SysSocialDetailsMapper;
import com.pig4cloud.pigx.common.data.tenant.TenantBroker;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Stream;
/**
* Clarity API 异步数据拉取任务
*
* @author lengleng
* @date 2026-03-26
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ClarityFetchTaskHandler {
private static final String CLARITY_API_BASE = "https://www.clarity.ms/export-data/api/v1/project-live-insights?numOfDays=";
private static final String CLARITY_TYPE = "clarity";
private final SysClarityDataMapper clarityDataMapper;
private final SysSocialDetailsMapper socialDetailsMapper;
/**
* 异步从 Clarity API 拉取数据并存库
* 策略:先插入占位行(PENDING),调用成功后更新数据(SUCCESS),失败则标记(FAILED)。
* 并发时第二个请求查到 PENDING/SUCCESS 即跳过,彻底避免重复调用。
* 使用 TenantBroker.runAs 恢复调用线程的租户上下文(@Async 新线程不继承 ThreadLocal)
*
* @param tenantId 调用方当前租户 ID
* @param numOfDays Clarity API numOfDays 参数(1=24h, 2=48h, 3=72h)
*/
@Async
public void fetchAndSave(Long tenantId, int numOfDays) {
try {
TenantBroker.runAs(tenantId, t -> {
// 幂等检查:今日已有 PENDING 或 SUCCESS 记录则跳过(FAILED 允许重试)
boolean alreadyRunning = clarityDataMapper.exists(Wrappers.<SysClarityData>lambdaQuery()
.eq(SysClarityData::getDataDate, LocalDate.now())
.eq(SysClarityData::getNumOfDays, numOfDays)
.in(SysClarityData::getFetchStatus, ClarityFetchStatus.PENDING, ClarityFetchStatus.SUCCESS));
if (alreadyRunning) {
log.debug("[Clarity] 今日 numOfDays={} 已有进行中或成功记录,跳过", numOfDays);
return;
}
// 获取 Clarity 配置(type='clarity')
SysSocialDetails config = socialDetailsMapper.selectOne(
Wrappers.<SysSocialDetails>lambdaQuery().eq(SysSocialDetails::getType, CLARITY_TYPE));
if (config == null) {
log.warn("[Clarity] 未找到 type='clarity' 的配置,跳过拉取");
return;
}
// 插入占位行,防止并发重复调用
SysClarityData placeholder = new SysClarityData();
placeholder.setDataDate(LocalDate.now());
placeholder.setNumOfDays(numOfDays);
placeholder.setFetchStatus(ClarityFetchStatus.PENDING);
clarityDataMapper.insert(placeholder);
log.info("[Clarity] 占位行已插入,id={},numOfDays={}", placeholder.getId(), numOfDays);
// 调用 Clarity API
HttpResponse response = HttpRequest.get(CLARITY_API_BASE + numOfDays)
.header("Authorization", "Bearer " + config.getAppSecret())
.header("Content-Type", "application/json")
.execute();
if (!response.isOk()) {
log.error("[Clarity] API 调用失败,状态码:{}", response.getStatus());
// 标记失败,下次调度可重试
updateStatus(placeholder.getId(), ClarityFetchStatus.FAILED);
return;
}
// 解析响应并填充数据
JSONArray body = JSONUtil.parseArray(response.body());
SysClarityData data = parseResponse(body);
data.setId(placeholder.getId());
data.setFetchStatus(ClarityFetchStatus.SUCCESS);
clarityDataMapper.updateById(data);
log.info("[Clarity] 数据拉取并存库成功,日期:{},numOfDays:{}", LocalDate.now(), numOfDays);
});
}
catch (Exception e) {
log.error("[Clarity] 数据拉取异常", e);
}
}
private void updateStatus(Long id, ClarityFetchStatus status) {
SysClarityData update = new SysClarityData();
update.setId(id);
update.setFetchStatus(status);
clarityDataMapper.updateById(update);
}
/**
* 解析 Clarity API 响应体(JSONArray 结构)到实体
* 字段映射已通过真实 API 调用确认(2026-03-26)
* 所有 count 类字段为 String 类型,使用 Convert.toInt 安全转换(null/空串返回 0)
*/
private SysClarityData parseResponse(JSONArray responseArray) {
SysClarityData data = new SysClarityData();
for (Object item : responseArray) {
if (!(item instanceof JSONObject metric)) {
continue;
}
String metricName = metric.getStr("metricName");
JSONArray info = metric.getJSONArray("information");
if (info == null || info.isEmpty()) {
continue;
}
JSONObject first = info.getJSONObject(0);
switch (metricName) {
case "Traffic" -> {
data.setTotalSessions(Convert.toInt(first.getStr("totalSessionCount"), 0));
data.setDistinctUsers(Convert.toInt(first.getStr("distinctUserCount"), 0));
data.setPagesPerSession(first.getBigDecimal("pagesPerSessionPercentage"));
}
case "ScrollDepth" -> data.setScrollDepth(first.getBigDecimal("averageScrollDepth"));
case "DeadClickCount" -> data.setDeadClickRate(first.getBigDecimal("sessionsWithMetricPercentage"));
case "RageClickCount" -> data.setRageClickRate(first.getBigDecimal("sessionsWithMetricPercentage"));
case "Device" -> data.setDeviceData(toJsonRanking(info.stream(), dev ->
dev.getStr("name"), dev -> Convert.toInt(dev.getStr("sessionsCount"), 0)));
case "PopularPages" -> data.setTopUrls(toJsonRanking(info.stream().limit(5), page ->
page.getStr("url"), page -> Convert.toInt(page.getStr("visitsCount"), 0)));
case "ReferrerUrl" -> data.setReferrerUrlData(toJsonRanking(info.stream().limit(5), ref ->
StrUtil.blankToDefault(ref.getStr("url"), ref.getStr("name", "unknown")),
ref -> Convert.toInt(ref.getStr("sessionsCount"), 0)));
case "PageTitle" -> data.setPageTitleData(toJsonRanking(info.stream().limit(5), pt ->
StrUtil.blankToDefault(pt.getStr("name"), pt.getStr("title", "unknown")),
pt -> Convert.toInt(pt.getStr("sessionsCount"), 0)));
case "Browser" -> data.setBrowserData(toJsonRanking(info.stream(), br ->
StrUtil.blankToDefault(br.getStr("name"), "unknown"),
br -> Convert.toInt(br.getStr("sessionsCount"), 0)));
default -> {
}
}
}
return data;
}
/**
* 将 JSONArray 流转换为 [{name, value}] 格式的 JSON 字符串
*/
private String toJsonRanking(Stream<?> stream,
Function<JSONObject, String> nameExtractor,
Function<JSONObject, Integer> valueExtractor) {
List<Map<String, Object>> list = new ArrayList<>();
stream.forEach(item -> {
if (item instanceof JSONObject obj) {
list.add(Map.of("name", nameExtractor.apply(obj), "value", valueExtractor.apply(obj)));
}
});
return JSONUtil.toJsonStr(list);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -121,7 +121,7 @@ public class DingTalkLoginHandler extends AbstractLoginHandler {
public Boolean bind(SysUser user, String identify) {
user.setWxDingUserid(identify);
sysUserService.updateById(user);
return true;
}
return true;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -34,6 +34,7 @@ import com.pig4cloud.pigx.common.core.constant.enums.LoginTypeEnum;
import com.pig4cloud.pigx.common.core.exception.CheckedException;
import com.pig4cloud.pigx.common.core.util.MsgUtils;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@@ -100,8 +101,8 @@ public class MiniAppLoginHandler extends AbstractLoginHandler {
R<UserInfo> userInfoR = sysUserService.getUserInfo(userDTO);
if (userInfoR.getData() == null) {
log.info("小程序不存在用户:{}", openId);
return null;
log.info("小程序不存在用户:{}, 开始自动创建", openId);
return createMiniAppUser(openId);
}
return userInfoR.getData();
@@ -121,4 +122,39 @@ public class MiniAppLoginHandler extends AbstractLoginHandler {
return true;
}
/**
* 自动创建小程序用户
*
* @param openId 小程序openId
* @return 创建成功返回用户信息,失败返回null
*/
private UserInfo createMiniAppUser(String openId) {
try {
log.info("开始为openId[{}]创建小程序用户", openId);
// 构造用户DTO
UserDTO newUser = new UserDTO();
newUser.setMiniOpenid(openId);
newUser.setUsername(openId);
newUser.setPassword(openId);
newUser.setTenantId(TenantContextHolder.getTenantId());
// 保存用户
sysUserService.saveUser(newUser);
log.info("小程序用户创建成功,openId: {}", openId);
// 重新获取用户信息
UserDTO queryDTO = new UserDTO();
queryDTO.setMiniOpenid(openId);
R<UserInfo> userInfoR = sysUserService.getUserInfo(queryDTO);
if (userInfoR.getData() != null) {
return userInfoR.getData();
}
} catch (Exception e) {
log.error("创建小程序用户异常,openId: {}, 错误信息: {}", openId, e.getMessage(), e);
}
return null;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -127,7 +127,7 @@ public class OscChinaLoginHandler extends AbstractLoginHandler {
public Boolean bind(SysUser user, String identify) {
user.setOscId(identify);
sysUserService.updateById(user);
return true;
}
return true;
}
}
@@ -0,0 +1,35 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*
*/
package com.pig4cloud.pigx.admin.mapper;
import com.pig4cloud.pigx.admin.api.entity.SysApiKey;
import com.pig4cloud.pigx.common.data.datascope.PigxBaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* API密钥 Mapper
*
* @author lengleng
* @date 2026-03-23
*/
@Mapper
public interface SysApiKeyMapper extends PigxBaseMapper<SysApiKey> {
}
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.admin.mapper;
import com.pig4cloud.pigx.admin.api.entity.SysClarityData;
import com.pig4cloud.pigx.common.data.datascope.PigxBaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* Clarity 监控数据 Mapper
*
* @author lengleng
* @date 2026-03-26
*/
@Mapper
public interface SysClarityDataMapper extends PigxBaseMapper<SysClarityData> {
}
@@ -0,0 +1,34 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*
*/
package com.pig4cloud.pigx.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pig4cloud.pigx.admin.api.entity.SysRoleWidget;
import org.apache.ibatis.annotations.Mapper;
/**
* 角色首页组件配置 Mapper
*
* @author lengleng
*/
@Mapper
public interface SysRoleWidgetMapper extends BaseMapper<SysRoleWidget> {
}
@@ -0,0 +1,67 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*
*/
package com.pig4cloud.pigx.admin.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pig4cloud.pigx.admin.api.dto.ApiKeyCreateDTO;
import com.pig4cloud.pigx.admin.api.entity.SysApiKey;
import com.pig4cloud.pigx.common.core.util.R;
/**
* API密钥管理 Service
*
* @author lengleng
* @date 2026-03-23
*/
public interface SysApiKeyService extends IService<SysApiKey> {
/**
* 校验密码、数量上限,生成并持久化 API Key
* @return 明文 Key(仅此一次)
*/
R<String> createApiKey(ApiKeyCreateDTO dto);
/**
* 校验归属权,白名单更新字段,清除缓存
*/
R<Void> updateApiKey(SysApiKey apiKey);
/**
* 校验归属权,删除记录,清除缓存
*/
R<Void> deleteApiKey(Long id);
/**
* 批量清除缓存后批量删除
*/
R<Boolean> deleteBatchApiKey(Long[] ids);
/**
* 管理员分页查询,按 username 模糊过滤
*/
R page(Page page, SysApiKey query);
/**
* 精确清除指定 hash 的缓存
*/
void evictApiKeyCache(String hash);
}
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -40,50 +40,68 @@ import java.util.Map;
*/
public interface SysDeptService extends IService<SysDept> {
/**
* 查询部门树菜单
* @param deptName 部门名称
* @return 树
*/
List<Tree<Long>> selectTree(String deptName, Long parentId);
/**
* 查询部门树菜单
*
* @param deptName 部门名称
* @return 树
*/
List<Tree<Long>> selectTree(String deptName, Long parentId);
/**
* 删除部门
* @param id 部门 ID
* @return 成功、失败
*/
Boolean removeDeptById(Long id);
/**
* 删除部门
*
* @param id 部门 ID
* @return 成功、失败
*/
Boolean removeDeptById(Long id);
List<DeptExcelVO> listExcelVo();
/**
* 获取部门Excel视图对象列表
*
* @return 部门Excel视图对象列表
*/
List<DeptExcelVO> listExcelVo();
R importDept(List<DeptExcelVO> excelVOList, BindingResult bindingResult);
/**
* 导入部门数据
*
* @param excelVOList 部门Excel数据列表
* @param bindingResult 数据绑定结果
* @return 导入结果
*/
R importDept(List<DeptExcelVO> excelVOList, BindingResult bindingResult);
/**
* 获取部门的所有后代部门列表
* @param deptId 部门ID
* @return 后代部门列表
*/
List<SysDept> listDescendant(Long deptId);
/**
* 获取部门的所有后代部门列表
*
* @param deptId 部门ID
* @return 后代部门列表
*/
List<SysDept> listDescendant(Long deptId);
/**
* 获取部门负责人
* @param deptId deptId
* @return user id list
*/
List<Long> listDeptLeader(Long deptId);
/**
* 获取部门负责人
*
* @param deptId deptId
* @return user id list
*/
List<Long> listDeptLeader(Long deptId);
/**
* 查询全部部门包含用户
* @param parentDeptId 父部门ID
* @param type 查询类型
*/
Map<String, Object> listOrgTree(Long parentDeptId, String type);
/**
* 查询全部部门包含用户
*
* @param parentDeptId 父部门ID
* @param type 查询类型
*/
Map<String, Object> listOrgTree(Long parentDeptId, String type);
/**
* 根据用户名模糊搜索用户
* @param username 用户名
* @return List user
*/
List<OrgTreeVO> getOrgTreeUser(String username);
/**
* 根据用户名模糊搜索用户
*
* @param username 用户名
* @return List user
*/
List<OrgTreeVO> getOrgTreeUser(String username);
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -17,6 +17,7 @@
package com.pig4cloud.pigx.admin.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pig4cloud.pigx.admin.api.dto.SysDictItemSortDTO;
import com.pig4cloud.pigx.admin.api.entity.SysDictItem;
import com.pig4cloud.pigx.common.core.util.R;
@@ -42,4 +43,12 @@ public interface SysDictItemService extends IService<SysDictItem> {
*/
R updateDictItem(SysDictItem item);
/**
* 更新字典项排序
*
* @param sortDTO 字典项排序信息(包含字典 ID 和按目标顺序排列的字典项 ID 列表)
* @return 成功 / 失败
*/
R updateDictItemSort(SysDictItemSortDTO sortDTO);
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -38,11 +38,11 @@ public interface SysFileService extends IService<SysFile> {
/**
* 上传文件
* @param file 文件流
* @param file 文件流
* @param fileName 文件名
* @param dir 存储目录
* @param groupId 文件分组ID
* @param type 文件类型
* @param dir 存储目录
* @param groupId 文件分组ID
* @param type 文件类型
* @return 上传结果
*/
R uploadFile(MultipartFile file, String fileName, String dir, Long groupId, String type);
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -21,6 +21,7 @@ package com.pig4cloud.pigx.admin.service;
import cn.hutool.core.lang.tree.Tree;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pig4cloud.pigx.admin.api.dto.SysMenuSortDTO;
import com.pig4cloud.pigx.admin.api.entity.SysMenu;
import com.pig4cloud.pigx.common.core.util.R;
@@ -58,6 +59,14 @@ public interface SysMenuService extends IService<SysMenu> {
*/
Boolean updateMenuById(SysMenu sysMenu);
/**
* 更新同级菜单排序
*
* @param sortDTO 菜单排序信息
* @return 成功、失败
*/
R updateMenuSort(SysMenuSortDTO sortDTO);
/**
* 构建树
* @param parentId 父节点ID
@@ -65,11 +65,13 @@ public interface SysMessageService extends IService<SysMessageEntity> {
/**
* 发送手机验证码
*
* @param mobile 手机号码
* @param registered 是否已注册
* @param mobile 手机号码
* @param registered 是否已注册
* @param captchaType 图形验证码类型
* @param captchaVerification 图形验证码凭据
* @return 发送结果
*/
R<Boolean> sendSmsCode(String mobile, boolean registered);
R<Boolean> sendSmsCode(String mobile, boolean registered, String captchaType, String captchaVerification);
/**
* 发送短信
@@ -99,9 +101,9 @@ public interface SysMessageService extends IService<SysMessageEntity> {
* 列出 Hook 业务代码
*
* @param messageHookDTO 消息钩子 DTO
* @return {@link R }
*/
R listHookBizCode(MessageHookDTO messageHookDTO);
* @return {@link R }
*/
R listHookBizCode(MessageHookDTO messageHookDTO);
/**
* 发送站内消息/公告
@@ -0,0 +1,52 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*
*/
package com.pig4cloud.pigx.admin.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pig4cloud.pigx.admin.api.entity.SysRoleWidget;
/**
* 角色首页组件配置 Service
*
* @author lengleng
*/
public interface SysRoleWidgetService extends IService<SysRoleWidget> {
/**
* 根据角色ID查询widget配置
* @param roleId 角色ID
* @return widget配置,无配置时返回 null
*/
SysRoleWidget getByRoleId(Long roleId);
/**
* 根据当前登录用户的角色列表,返回 widgetKeys 最多的那条配置
* @return widget配置,无配置时返回 null
*/
SysRoleWidget getByCurrentUser();
/**
* 保存或更新角色widget配置
* @param sysRoleWidget widget配置
* @return 是否成功
*/
boolean saveOrUpdateByRoleId(SysRoleWidget sysRoleWidget);
}
@@ -0,0 +1,55 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.admin.service;
import com.pig4cloud.pigx.admin.dto.SiteConfigDTO;
import java.util.Map;
/**
* 站点配置服务
*
* @author lengleng
* @date 2026-03-27
*/
public interface SysSiteConfigService {
/**
* 获取聚合配置(i18n + 站点配置)
* @return Map
*/
Map<String, Object> getAggregatedConfig();
/**
* 获取站点配置
* @return SiteConfigDTO
*/
SiteConfigDTO getSiteConfig();
/**
* 更新站点配置
* @param dto 站点配置DTO
*/
void updateSiteConfig(SiteConfigDTO dto);
/**
* 刷新站点配置缓存(清空 Redis 中的 site_config_details)
*/
void refreshCache();
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -47,6 +47,6 @@ public interface SysSocialDetailsService extends IService<SysSocialDetails> {
*/
UserInfo getUserInfo(String inStr);
List<SysSocialDetailsVO> selectList();
List<SysSocialDetailsVO> selectList();
}
@@ -0,0 +1,48 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.admin.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pig4cloud.pigx.admin.api.entity.SysClarityData;
import com.pig4cloud.pigx.common.core.util.R;
import java.util.Map;
/**
* 系统监控服务
*
* @author lengleng
* @date 2026-03-26
*/
public interface SysSystemInfoService extends IService<SysClarityData> {
/**
* 获取 Redis 缓存监控信息
* @return R
*/
R<Map<String, Object>> getCacheInfo();
/**
* 获取今日 Clarity 数据
* 查库优先:有数据直接返回;无数据触发异步拉取并返回 loading 状态
* @param numOfDays Clarity API numOfDays 参数(1=24h, 2=48h, 3=72h)
* @return R
*/
R<?> getClarityData(int numOfDays);
}
@@ -0,0 +1,172 @@
/*
*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*
*/
package com.pig4cloud.pigx.admin.service.impl;
import cn.hutool.core.codec.Base62;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pig4cloud.pigx.admin.api.dto.ApiKeyCreateDTO;
import com.pig4cloud.pigx.admin.api.entity.SysApiKey;
import com.pig4cloud.pigx.admin.mapper.SysApiKeyMapper;
import com.pig4cloud.pigx.admin.service.SysApiKeyService;
import com.pig4cloud.pigx.admin.service.SysUserService;
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.security.util.SecurityUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.HexFormat;
/**
* API密钥管理 Service 实现
*
* @author lengleng
* @date 2026-03-23
*/
@Service
@RequiredArgsConstructor
public class SysApiKeyServiceImpl extends ServiceImpl<SysApiKeyMapper, SysApiKey> implements SysApiKeyService {
private static final String API_KEY_PREFIX = "sk-";
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
private static final int MAX_KEYS_PER_USER = 5;
private final SysUserService userService;
/**
* 校验密码、数量上限,生成并持久化 API Key
*/
@Override
public R<String> createApiKey(ApiKeyCreateDTO dto) {
String username = SecurityUtils.getUser().getUsername();
R checkResult = userService.checkPassword(username, dto.getPassword());
if (!(boolean) checkResult.getData()) {
return R.failed("密码验证失败");
}
Long userId = SecurityUtils.getUser().getId();
long count = this.count(Wrappers.<SysApiKey>lambdaQuery().eq(SysApiKey::getUserId, userId));
if (count >= MAX_KEYS_PER_USER) {
return R.failed("每个用户最多创建" + MAX_KEYS_PER_USER + "个API密钥");
}
byte[] randomBytes = new byte[32];
SECURE_RANDOM.nextBytes(randomBytes);
String rawKey = API_KEY_PREFIX + Base62.encode(randomBytes);
SysApiKey apiKey = new SysApiKey();
apiKey.setName(dto.getName());
apiKey.setAllowedIps(dto.getAllowedIps());
apiKey.setExpiresAt(dto.getExpiresAt());
apiKey.setApiKeyHash(sha256(rawKey));
apiKey.setUserId(userId);
apiKey.setUsername(username);
apiKey.setStatus("0");
this.save(apiKey);
return R.ok(rawKey);
}
/**
* 校验归属权,白名单更新字段,清除缓存
*/
@Override
public R<Void> updateApiKey(SysApiKey apiKey) {
SysApiKey existing = this.getById(apiKey.getId());
if (existing == null || !existing.getUserId().equals(SecurityUtils.getUser().getId())) {
return R.failed("无权操作");
}
SysApiKey update = new SysApiKey();
update.setId(apiKey.getId());
update.setName(apiKey.getName());
update.setAllowedIps(apiKey.getAllowedIps());
update.setExpiresAt(apiKey.getExpiresAt());
update.setStatus(apiKey.getStatus());
this.updateById(update);
evictApiKeyCache(existing.getApiKeyHash());
return R.ok();
}
/**
* 校验归属权,删除记录,清除缓存
*/
@Override
public R<Void> deleteApiKey(Long id) {
SysApiKey existing = this.getById(id);
if (existing == null || !existing.getUserId().equals(SecurityUtils.getUser().getId())) {
return R.failed("无权操作");
}
this.removeById(id);
evictApiKeyCache(existing.getApiKeyHash());
return R.ok();
}
/**
* 批量清除缓存后批量删除
*/
@Override
public R<Boolean> deleteBatchApiKey(Long[] ids) {
CollUtil.toList(ids).forEach(id -> {
SysApiKey existing = this.getById(id);
if (existing != null) {
evictApiKeyCache(existing.getApiKeyHash());
}
});
return R.ok(this.removeBatchByIds(CollUtil.toList(ids)));
}
/**
* 管理员分页查询,按 username 模糊过滤
*/
@Override
public R page(Page page, SysApiKey query) {
return R.ok(this.page(page, Wrappers.<SysApiKey>lambdaQuery()
.like(StrUtil.isNotBlank(query.getUsername()), SysApiKey::getUsername, query.getUsername())
.orderByDesc(SysApiKey::getCreateTime)));
}
@Override
@CacheEvict(value = CacheConstants.API_KEY_DETAILS, key = "#hash")
public void evictApiKeyCache(String hash) {
// 缓存清除由 @CacheEvict 注解驱动
}
public static String sha256(String input) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(hash);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 algorithm not available", e);
}
}
}
@@ -80,6 +80,6 @@ public class SysAreaServiceImpl extends ServiceImpl<SysAreaMapper, SysAreaEntity
wrapper.orderByAsc(SysAreaEntity::getAreaType);
wrapper.orderByDesc(SysAreaEntity::getAreaSort);
return baseMapper.selectPage(page, wrapper);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -16,8 +16,10 @@
*/
package com.pig4cloud.pigx.admin.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pig4cloud.pigx.admin.api.constant.UpmsErrorCodes;
import com.pig4cloud.pigx.admin.api.dto.SysDictItemSortDTO;
import com.pig4cloud.pigx.admin.api.entity.SysDict;
import com.pig4cloud.pigx.admin.api.entity.SysDictItem;
import com.pig4cloud.pigx.admin.mapper.SysDictItemMapper;
@@ -30,6 +32,11 @@ import com.pig4cloud.pigx.common.core.util.R;
import lombok.AllArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 字典项
@@ -80,4 +87,55 @@ public class SysDictItemServiceImpl extends ServiceImpl<SysDictItemMapper, SysDi
return R.ok(this.updateById(item));
}
@Override
@Transactional(rollbackFor = Exception.class)
@CacheEvict(value = CacheConstants.DICT_DETAILS, allEntries = true)
public R updateDictItemSort(SysDictItemSortDTO sortDTO) {
SysDict dict = dictService.getById(sortDTO.getDictId());
if (dict == null) {
return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_DICT_NOT_FOUND));
}
if (DictTypeEnum.SYSTEM.getType().equals(dict.getSystemFlag())) {
return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_DICT_UPDATE_SYSTEM));
}
List<Long> itemIds = sortDTO.getItemIds();
Set<Long> uniqueItemIds = new HashSet<>(itemIds);
if (uniqueItemIds.size() != itemIds.size()) {
return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_DICT_ITEM_SORT_DUPLICATE));
}
List<SysDictItem> sortedItems = this.list(Wrappers.<SysDictItem>lambdaQuery()
.eq(SysDictItem::getDictId, sortDTO.getDictId())
.orderByAsc(SysDictItem::getSortOrder)
.orderByAsc(SysDictItem::getId));
Map<Long, SysDictItem> itemMap = sortedItems.stream().collect(Collectors.toMap(SysDictItem::getId, Function.identity()));
if (!itemMap.keySet().containsAll(itemIds)) {
return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_DICT_ITEM_SORT_SCOPE_INVALID));
}
// 兼容分页排序:以当前页字典项在全量列表中的最小位置作为插入点,将整页按新顺序回插
Map<Long, Integer> itemIndexMap = new HashMap<>(sortedItems.size());
for (int index = 0; index < sortedItems.size(); index++) {
itemIndexMap.put(sortedItems.get(index).getId(), index);
}
int insertIndex = itemIds.stream().map(itemIndexMap::get).min(Integer::compareTo).orElse(sortedItems.size());
List<SysDictItem> orderedPageItems = itemIds.stream().map(itemMap::get).toList();
List<SysDictItem> reorderedItems = new ArrayList<>(sortedItems);
reorderedItems.removeAll(orderedPageItems);
reorderedItems.addAll(insertIndex, orderedPageItems);
List<SysDictItem> updateList = new ArrayList<>(reorderedItems.size());
for (int index = 0; index < reorderedItems.size(); index++) {
SysDictItem updateItem = new SysDictItem();
updateItem.setId(reorderedItems.get(index).getId());
updateItem.setSortOrder(index);
updateList.add(updateItem);
}
return R.ok(this.updateBatchById(updateList));
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -28,7 +28,6 @@ import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.http.HttpUtil;
import com.amazonaws.services.s3.model.S3Object;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pig4cloud.pigx.admin.api.dto.SysFileGroupDTO;
@@ -40,6 +39,7 @@ import com.pig4cloud.pigx.admin.service.SysFileService;
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder;
import com.pig4cloud.pigx.common.file.core.FileObject;
import com.pig4cloud.pigx.common.file.core.FileProperties;
import com.pig4cloud.pigx.common.file.core.FileTemplate;
import jakarta.servlet.http.HttpServletResponse;
@@ -128,13 +128,13 @@ public class SysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFile> impl
return;
}
try (S3Object s3Object = fileTemplate.getObject(sysFile.getBucketName(), sysFile.getDir(), fileName)) {
try (FileObject fileObject = fileTemplate.getObject(sysFile.getBucketName(), sysFile.getDir(), fileName)) {
response.setContentType("application/octet-stream; charset=UTF-8");
// 文件唯一hash
response.addHeader(SysFile.Fields.hash, sysFile.getHash());
response.addHeader(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=" + URLUtil.encode(sysFile.getOriginal()));
IoUtil.copy(s3Object.getObjectContent(), response.getOutputStream());
IoUtil.copy(fileObject.getObjectContent(), response.getOutputStream());
} catch (Exception e) {
log.error("文件读取异常: {}", e.getLocalizedMessage());
}
@@ -250,12 +250,12 @@ public class SysFileServiceImpl extends ServiceImpl<SysFileMapper, SysFile> impl
sysFile.setOriginal(file.getOriginalFilename());
sysFile.setFileSize(file.getSize());
sysFile.setBucketName(properties.getBucketName());
sysFile.setType(type);
sysFile.setGroupId(groupId);
sysFile.setType(type);
sysFile.setGroupId(groupId);
sysFile.setHash(SecureUtil.md5(file.getInputStream()));
// 调用save方法保存SysFile对象
this.save(sysFile);
}
sysFile.setHash(SecureUtil.md5(file.getInputStream()));
// 调用save方法保存SysFile对象
this.save(sysFile);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -23,7 +23,6 @@ import com.pig4cloud.pigx.admin.service.SysI18nService;
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.core.util.R;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
@@ -45,7 +44,6 @@ public class SysI18nServiceImpl extends ServiceImpl<SysI18nMapper, SysI18nEntity
* @return
*/
@Override
@Cacheable(value = CacheConstants.I18N_DETAILS)
public Map listMap() {
List<SysI18nEntity> sysI18nEntities = baseMapper.selectList(null);
HashMap<String, List<Map<String, String>>> stringListHashMap = new HashMap<>();
@@ -69,7 +67,7 @@ public class SysI18nServiceImpl extends ServiceImpl<SysI18nMapper, SysI18nEntity
* @return
*/
@Override
@CacheEvict(value = CacheConstants.I18N_DETAILS, allEntries = true)
@CacheEvict(value = CacheConstants.SITE_CONFIG_DETAILS, allEntries = true)
public R syncI18nCache() {
return R.ok();
}
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -144,7 +144,7 @@ public class SysLogServiceImpl extends ServiceImpl<SysLogMapper, SysLog> impleme
})
// 按createTime升序排序
.sorted(Comparator.comparing(map -> map.get(SysLog.Fields.createTime).toString()))
.toList();
}
.toList();
}
}
@@ -1,6 +1,6 @@
/*
*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -28,6 +28,7 @@ import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pig4cloud.pigx.admin.api.constant.UpmsErrorCodes;
import com.pig4cloud.pigx.admin.api.dto.SysMenuSortDTO;
import com.pig4cloud.pigx.admin.api.entity.SysI18nEntity;
import com.pig4cloud.pigx.admin.api.entity.SysMenu;
import com.pig4cloud.pigx.admin.api.entity.SysRoleMenu;
@@ -38,10 +39,8 @@ import com.pig4cloud.pigx.admin.service.SysMenuService;
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
import com.pig4cloud.pigx.common.core.constant.enums.MenuTypeEnum;
import com.pig4cloud.pigx.common.core.constant.enums.YesNoEnum;
import com.pig4cloud.pigx.common.core.util.MsgUtils;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.security.util.SecurityUtils;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -54,6 +53,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* <p>
@@ -125,6 +125,37 @@ public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> impl
return this.updateById(sysMenu);
}
@Override
@Transactional(rollbackFor = Exception.class)
@CacheEvict(value = CacheConstants.MENU_DETAILS, allEntries = true)
public R updateMenuSort(SysMenuSortDTO sortDTO) {
List<Long> menuIds = sortDTO.getMenuIds();
Set<Long> uniqueMenuIds = new HashSet<>(menuIds);
if (uniqueMenuIds.size() != menuIds.size()) {
return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_MENU_SORT_DUPLICATE));
}
List<SysMenu> menus = this.list(Wrappers.<SysMenu>lambdaQuery()
.in(SysMenu::getMenuId, menuIds)
.eq(SysMenu::getParentId, sortDTO.getParentId()));
if (menus.size() != menuIds.size()) {
return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_MENU_SORT_SCOPE_INVALID));
}
Map<Long, SysMenu> menuMap = menus.stream().collect(Collectors.toMap(SysMenu::getMenuId, Function.identity()));
List<SysMenu> updateList = new ArrayList<>(menuIds.size());
for (int index = 0; index < menuIds.size(); index++) {
SysMenu menu = menuMap.get(menuIds.get(index));
SysMenu updateMenu = new SysMenu();
updateMenu.setMenuId(menu.getMenuId());
updateMenu.setSortOrder(index);
updateList.add(updateMenu);
}
return R.ok(this.updateBatchById(updateList));
}
/**
* 构建树查询 1. 不是懒加载情况,查询全部 2. 是懒加载,根据parentId 查询 2.1 父节点为空,则查询ID -1
*
@@ -136,7 +167,11 @@ public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> impl
public List<Tree<Long>> treeMenu(Long parentId, String menuName, String type) {
Long parent = parentId == null ? CommonConstants.MENU_TREE_ROOT_ID : parentId;
List<TreeNode<Long>> collect = baseMapper.selectList(Wrappers.<SysMenu>lambdaQuery().eq(Objects.nonNull(parentId), SysMenu::getParentId, parentId).like(StrUtil.isNotBlank(menuName), SysMenu::getName, menuName).eq(StrUtil.isNotBlank(type), SysMenu::getMenuType, type).orderByAsc(SysMenu::getSortOrder)).stream().map(getNodeFunction()).toList();
List<TreeNode<Long>> collect = baseMapper.selectList(Wrappers.<SysMenu>lambdaQuery()
.eq(Objects.nonNull(parentId), SysMenu::getParentId, parentId)
.like(StrUtil.isNotBlank(menuName), SysMenu::getName, menuName)
.eq(StrUtil.isNotBlank(type), SysMenu::getMenuType, type)
.orderByAsc(SysMenu::getSortOrder)).stream().map(getNodeFunction()).toList();
// 模糊查询 不组装树结构 直接返回 表格方便编辑
if (StrUtil.isNotBlank(menuName)) {
@@ -215,7 +250,7 @@ public class SysMenuServiceImpl extends ServiceImpl<SysMenuMapper, SysMenu> impl
return MenuTypeEnum.TOP_MENU.getType().equals(vo.getMenuType());
}
// 其他查询 左侧 + 顶部
return !MenuTypeEnum.BUTTON.getType().equals(vo.getMenuType());
return StrUtil.isNotBlank(vo.getMenuType()) && !MenuTypeEnum.BUTTON.getType().equals(vo.getMenuType());
};
}
@@ -14,7 +14,6 @@ import cn.hutool.extra.template.TemplateEngine;
import cn.hutool.extra.template.TemplateUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.amazonaws.services.s3.model.S3Object;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -44,10 +43,13 @@ import com.pig4cloud.pigx.common.data.cache.RedisUtils;
import com.pig4cloud.pigx.common.data.resolver.ParamResolver;
import com.pig4cloud.pigx.common.data.tenant.TenantBroker;
import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder;
import com.pig4cloud.pigx.common.file.core.FileObject;
import com.pig4cloud.pigx.common.file.core.FileTemplate;
import com.pig4cloud.pigx.common.log.util.JacksonSensitiveFieldUtil;
import com.pig4cloud.pigx.common.log.util.LogTypeEnum;
import com.pig4cloud.pigx.common.log.util.SysLogUtils;
import com.pig4cloud.pigx.common.security.captcha.CaptchaResult;
import com.pig4cloud.pigx.common.security.captcha.CaptchaValidator;
import com.pig4cloud.pigx.common.security.util.SecurityUtils;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
@@ -117,6 +119,8 @@ public class SysMessageServiceImpl extends ServiceImpl<SysMessageMapper, SysMess
private final FileTemplate fileTemplate;
private final CaptchaValidator captchaValidator;
/**
* 发送信息
*
@@ -274,13 +278,15 @@ public class SysMessageServiceImpl extends ServiceImpl<SysMessageMapper, SysMess
/**
* 发送手机验证码
*
* @param mobile 手机号码
* @param registered 是否已注册
* @param mobile 手机号码
* @param registered 是否已注册
* @param captchaType 图形验证码类型
* @param captchaVerification 图形验证码凭据
* @return 发送结果
*/
@Override
public R<Boolean> sendSmsCode(String mobile, boolean registered) {
// IP 限制校验,单位时间内同一IP发送次数限制
public R<Boolean> sendSmsCode(String mobile, boolean registered, String captchaType, String captchaVerification) {
// IP 限制校验前置,避免恶意请求反向放大图形验证码服务压力
String clientIp = WebUtils.getIP();
String ipLimitKey = CacheConstants.DEFAULT_CODE_KEY + "IP_LIMIT:" + clientIp;
if (StrUtil.isNotBlank(clientIp)) {
@@ -295,6 +301,11 @@ public class SysMessageServiceImpl extends ServiceImpl<SysMessageMapper, SysMess
}
}
CaptchaResult captchaResult = captchaValidator.validate(captchaType, captchaVerification);
if (!captchaResult.isOk()) {
return R.failed(Boolean.FALSE, captchaResult.getErrorMessage());
}
List<SysUser> userList = TenantBroker
.noneAs(() -> userMapper.selectList(Wrappers.<SysUser>query().lambda().eq(SysUser::getPhone, mobile)));
@@ -332,6 +343,13 @@ public class SysMessageServiceImpl extends ServiceImpl<SysMessageMapper, SysMess
*/
@Override
public R sendSms(MessageSmsDTO messageSmsDTO) {
R captchaResult = validateCaptchaIfPresent(messageSmsDTO.getCaptchaType(),
messageSmsDTO.getCaptchaVerification());
if (captchaResult != null) {
return captchaResult;
}
messageSmsDTO.setCaptchaType(null);
messageSmsDTO.setCaptchaVerification(null);
// 根据业务编码获取短信通道配置
List<SysSystemConfigEntity> configEntityList = sysSystemConfigMapper
@@ -475,6 +493,14 @@ public class SysMessageServiceImpl extends ServiceImpl<SysMessageMapper, SysMess
@SneakyThrows
@Override
public R sendEmail(MessageEmailDTO messageEmailDTO) {
R captchaResult = validateCaptchaIfPresent(messageEmailDTO.getCaptchaType(),
messageEmailDTO.getCaptchaVerification());
if (captchaResult != null) {
return captchaResult;
}
messageEmailDTO.setCaptchaType(null);
messageEmailDTO.setCaptchaVerification(null);
// 根据业务编码获取短信通道配置
List<SysSystemConfigEntity> configEntityList = sysSystemConfigMapper
.selectList(Wrappers.<SysSystemConfigEntity>lambdaQuery()
@@ -493,10 +519,11 @@ public class SysMessageServiceImpl extends ServiceImpl<SysMessageMapper, SysMess
for (String fileName : messageEmailDTO.getAttachmentList()) {
SysFile sysFile = fileMapper
.selectOne(Wrappers.<SysFile>lambdaQuery().eq(SysFile::getFileName, fileName));
S3Object s3Object = fileTemplate.getObject(sysFile.getBucketName(), sysFile.getDir(), fileName);
File file = FileUtil.file(FileUtil.getTmpDirPath(), sysFile.getOriginal());
tempFileList.add(file);
IoUtil.copy(s3Object.getObjectContent(), FileUtil.getOutputStream(file));
try (FileObject fileObject = fileTemplate.getObject(sysFile.getBucketName(), sysFile.getDir(), fileName)) {
File file = FileUtil.file(FileUtil.getTmpDirPath(), sysFile.getOriginal());
tempFileList.add(file);
IoUtil.copy(fileObject.getObjectContent(), FileUtil.getOutputStream(file));
}
}
}
@@ -533,6 +560,22 @@ public class SysMessageServiceImpl extends ServiceImpl<SysMessageMapper, SysMess
return R.ok();
}
/**
* 校验图形验证码(如提供)。
*
* @param captchaType 验证码类型,为空表示跳过校验
* @param captchaVerification 验证码凭据
* @return 通过返回 null;失败返回带错误信息的 R
*/
private R validateCaptchaIfPresent(String captchaType, String captchaVerification) {
if (StrUtil.isAllBlank(captchaType, captchaVerification)) {
return null;
}
CaptchaResult captchaResult = captchaValidator.validate(captchaType, captchaVerification);
return captchaResult.isOk() ? null : R.failed(captchaResult.getErrorMessage());
}
/**
* 列出 Hook 业务代码
*
@@ -0,0 +1,77 @@
package com.pig4cloud.pigx.admin.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pig4cloud.pigx.admin.api.entity.SysRoleWidget;
import com.pig4cloud.pigx.admin.mapper.SysRoleWidgetMapper;
import com.pig4cloud.pigx.admin.service.SysRoleWidgetService;
import com.pig4cloud.pigx.common.security.util.SecurityUtils;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Comparator;
import java.util.List;
/**
* 角色首页组件配置 Service 实现
*
* @author lengleng
*/
@Service
@AllArgsConstructor
public class SysRoleWidgetServiceImpl extends ServiceImpl<SysRoleWidgetMapper, SysRoleWidget>
implements SysRoleWidgetService {
/**
* 根据角色ID查询widget配置
* @param roleId 角色ID
* @return widget配置,无配置时返回 null
*/
@Override
public SysRoleWidget getByRoleId(Long roleId) {
return getOne(Wrappers.<SysRoleWidget>lambdaQuery()
.eq(SysRoleWidget::getRoleId, roleId));
}
/**
* 根据当前登录用户的角色列表查询widget配置
* <p>遍历用户所有角色对应的配置,返回 widgetKeys 数量最多的那条,
* 以保证多角色场景下首页显示最完整的widget集合。</p>
* @return widget配置,用户无角色或角色均无配置时返回 null
*/
@Override
public SysRoleWidget getByCurrentUser() {
List<Long> roleIds = SecurityUtils.getRoleIds();
if (roleIds.isEmpty()) {
return null;
}
// 查询当前用户所有角色的 widget 配置,取 widgetKeys 数量最多的
return list(Wrappers.<SysRoleWidget>lambdaQuery()
.in(SysRoleWidget::getRoleId, roleIds))
.stream()
.filter(w -> StringUtils.hasText(w.getWidgetKeys()))
.max(Comparator.comparingInt(
w -> w.getWidgetKeys().split(StrUtil.COMMA).length))
.orElse(null);
}
/**
* 保存或更新角色widget配置
* <p>若该角色已存在配置则更新,否则新增。</p>
* @param sysRoleWidget widget配置(roleId 必填)
* @return 操作成功返回 true
*/
@Override
public boolean saveOrUpdateByRoleId(SysRoleWidget sysRoleWidget) {
SysRoleWidget existing = getOne(Wrappers.<SysRoleWidget>lambdaQuery()
.eq(SysRoleWidget::getRoleId, sysRoleWidget.getRoleId()));
if (existing != null) {
sysRoleWidget.setId(existing.getId());
return updateById(sysRoleWidget);
}
return save(sysRoleWidget);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -141,6 +141,6 @@ public class SysRouteConfServiceImpl extends ServiceImpl<SysRouteConfMapper, Sys
baseMapper.insert(routeConf);
this.applicationEventPublisher.publishEvent(new DynamicRouteInitEvent(this));
return routeConf;
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018-2026, lengleng All rights reserved.
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
@@ -56,10 +56,10 @@ public class SysScheduleServiceImpl extends ServiceImpl<SysScheduleMapper, SysSc
public IPage<SysScheduleEntity> getScheduleByScope(Page page, SysScheduleEntity sysSchedule) {
LambdaQueryWrapper<SysScheduleEntity> wrapper = Wrappers.lambdaQuery();
wrapper.like(StrUtil.isNotBlank(sysSchedule.getTitle()), SysScheduleEntity::getTitle, sysSchedule.getTitle());
wrapper.like(StrUtil.isNotBlank(sysSchedule.getScheduleType()), SysScheduleEntity::getScheduleType,
sysSchedule.getScheduleType());
wrapper.eq(Objects.nonNull(sysSchedule.getScheduleDate()), SysScheduleEntity::getScheduleDate,
sysSchedule.getScheduleDate());
wrapper.like(StrUtil.isNotBlank(sysSchedule.getScheduleType()), SysScheduleEntity::getScheduleType,
sysSchedule.getScheduleType());
wrapper.eq(Objects.nonNull(sysSchedule.getScheduleDate()), SysScheduleEntity::getScheduleDate,
sysSchedule.getScheduleDate());
DataScope dataScope = new DataScope();
dataScope.setUsername(SecurityUtils.getUser().getUsername());
return baseMapper.selectPageByScope(page, wrapper, dataScope);
@@ -63,8 +63,8 @@ public class SysSensitiveWordServiceImpl extends ServiceImpl<SysSensitiveWordMap
RedisUtils.execute((RedisCallback<Void>) connection -> {
connection.publish(CacheConstants.SENSITIVE_REDIS_RELOAD_TOPIC.getBytes(), "刷新敏感词缓存".getBytes());
return null;
});
return Boolean.TRUE;
}
});
return Boolean.TRUE;
}
}
@@ -0,0 +1,203 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.admin.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.pig4cloud.pigx.admin.api.entity.SysOauthClientDetails;
import com.pig4cloud.pigx.admin.api.entity.SysPublicParam;
import com.pig4cloud.pigx.admin.config.ClientDetailsInitRunner;
import com.pig4cloud.pigx.admin.dto.SiteConfigDTO;
import com.pig4cloud.pigx.admin.service.SysI18nService;
import com.pig4cloud.pigx.admin.service.SysOauthClientDetailsService;
import com.pig4cloud.pigx.admin.service.SysPublicParamService;
import com.pig4cloud.pigx.admin.service.SysSiteConfigService;
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
import com.pig4cloud.pigx.common.core.constant.enums.DictTypeEnum;
import com.pig4cloud.pigx.common.core.constant.enums.YesNoEnum;
import com.pig4cloud.pigx.common.core.util.SpringContextHolder;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
/**
* 站点配置服务实现
*
* @author lengleng
* @date 2026-03-27
*/
@Service
@RequiredArgsConstructor
public class SysSiteConfigServiceImpl implements SysSiteConfigService {
/**
* SITE_ 参数键与 DTO 字段名的映射
*/
private static final Map<String, String> KEY_FIELD_MAP = new LinkedHashMap<>();
/**
* 布尔类型的字段集合
*/
private static final Set<String> BOOLEAN_FIELDS = Set.of("forceResetPwd",
"forceLogout", "smsLoginEnable", "socialLoginEnable", "registerEnable", "resetPassword", "i18nEnable", "darkModeEnable",
"antiDebugEnable", "syncDingtalkEnabled", "syncWechatEnabled");
static {
KEY_FIELD_MAP.put("SITE_CLARITY_ID", "clarityId");
KEY_FIELD_MAP.put("SITE_CAPTCHA_TYPE", "captchaType");
KEY_FIELD_MAP.put("SITE_PASSWORD_RULE", "passwordRule");
KEY_FIELD_MAP.put("SITE_FORCE_RESET_PWD", "forceResetPwd");
KEY_FIELD_MAP.put("SITE_FORCE_LOGOUT", "forceLogout");
KEY_FIELD_MAP.put("SITE_SMS_LOGIN_ENABLE", "smsLoginEnable");
KEY_FIELD_MAP.put("SITE_SOCIAL_LOGIN_ENABLE", "socialLoginEnable");
KEY_FIELD_MAP.put("SITE_REGISTER_ENABLE", "registerEnable");
KEY_FIELD_MAP.put("SITE_RESET_PASSWORD", "resetPassword");
KEY_FIELD_MAP.put("SITE_I18N_ENABLE", "i18nEnable");
KEY_FIELD_MAP.put("SITE_DARK_MODE_ENABLE", "darkModeEnable");
KEY_FIELD_MAP.put("SITE_ANTI_DEBUG_ENABLE", "antiDebugEnable");
KEY_FIELD_MAP.put("SITE_ANTI_DEBUG_KEY", "antiDebugKey");
KEY_FIELD_MAP.put("SITE_SYNC_DINGTALK_ENABLED", "syncDingtalkEnabled");
KEY_FIELD_MAP.put("SITE_SYNC_WECHAT_ENABLED", "syncWechatEnabled");
KEY_FIELD_MAP.put("SITE_TITLE", "title");
KEY_FIELD_MAP.put("SITE_FOOTER", "footer");
KEY_FIELD_MAP.put("SITE_PRIVACY_TIP", "privacyTip");
KEY_FIELD_MAP.put("SITE_LOGO", "logo");
}
private final SysPublicParamService sysPublicParamService;
private final SysI18nService sysI18nService;
private final SysOauthClientDetailsService sysOauthClientDetailsService;
@Override
@Cacheable(value = CacheConstants.SITE_CONFIG_DETAILS)
public Map<String, Object> getAggregatedConfig() {
Map<String, Object> result = new HashMap<>(4);
result.put("i18n", sysI18nService.listMap());
result.put("site", getSiteConfig());
return result;
}
@Override
public SiteConfigDTO getSiteConfig() {
List<SysPublicParam> paramList = sysPublicParamService
.list(Wrappers.<SysPublicParam>lambdaQuery().likeRight(SysPublicParam::getPublicKey, "SITE_"));
SiteConfigDTO dto = new SiteConfigDTO();
for (SysPublicParam param : paramList) {
String fieldName = KEY_FIELD_MAP.get(param.getPublicKey());
if (fieldName == null) {
continue;
}
Object value;
if (BOOLEAN_FIELDS.contains(fieldName)) {
value = YesNoEnum.YES.getCode().equals(param.getPublicValue());
} else {
value = param.getPublicValue();
}
BeanUtil.setFieldValue(dto, fieldName, value);
}
return dto;
}
@Override
@Transactional(rollbackFor = Exception.class)
@CacheEvict(value = {CacheConstants.SITE_CONFIG_DETAILS, CacheConstants.PARAMS_DETAILS}, allEntries = true)
public void updateSiteConfig(SiteConfigDTO dto) {
Map<String, Object> fieldValues = BeanUtil.beanToMap(dto, false, true);
for (Map.Entry<String, String> entry : KEY_FIELD_MAP.entrySet()) {
String paramKey = entry.getKey();
String fieldName = entry.getValue();
Object fieldValue = fieldValues.get(fieldName);
if (fieldValue == null) {
continue;
}
String publicValue;
if (BOOLEAN_FIELDS.contains(fieldName)) {
publicValue = YesNoEnum.getCode((Boolean) fieldValue);
} else {
publicValue = fieldValue.toString();
}
SysPublicParam existing = sysPublicParamService
.getOne(Wrappers.<SysPublicParam>lambdaQuery().eq(SysPublicParam::getPublicKey, paramKey));
if (Objects.nonNull(existing)) {
existing.setPublicValue(publicValue);
sysPublicParamService.updateById(existing);
} else {
SysPublicParam newParam = new SysPublicParam();
newParam.setPublicKey(paramKey);
newParam.setPublicValue(publicValue);
newParam.setPublicName(paramKey);
newParam.setStatus(YesNoEnum.NO.getCode());
newParam.setSystemFlag(DictTypeEnum.SYSTEM.getType());
sysPublicParamService.save(newParam);
}
}
// 同步 captcha_flag 到 OAuth 客户端扩展信息
if (dto.getCaptchaType() != null) {
syncCaptchaFlagToClients(!"none".equals(dto.getCaptchaType()));
}
}
/**
* 刷新站点配置缓存
*/
@Override
@CacheEvict(value = {CacheConstants.SITE_CONFIG_DETAILS, CacheConstants.PARAMS_DETAILS}, allEntries = true)
public void refreshCache() {
// 仅触发 @CacheEvict,无需额外逻辑
}
/**
* 同步验证码开关到所有 OAuth 客户端的 additionalInformation
*
* @param captchaEnable 是否启用验证码
*/
private void syncCaptchaFlagToClients(Boolean captchaEnable) {
String captchaFlag = YesNoEnum.getCode(captchaEnable);
List<SysOauthClientDetails> clients = sysOauthClientDetailsService.list();
for (SysOauthClientDetails client : clients) {
if (StrUtil.isBlank(client.getAdditionalInformation())) {
continue;
}
JSONObject informationObj = JSONUtil.parseObj(client.getAdditionalInformation());
informationObj.set(CommonConstants.CAPTCHA_FLAG, captchaFlag);
client.setAdditionalInformation(informationObj.toString());
sysOauthClientDetailsService.updateById(client);
}
// 刷新 Redis 客户端缓存
SpringContextHolder.publishEvent(new ClientDetailsInitRunner.ClientDetailsInitEvent(this));
}
}
@@ -113,9 +113,9 @@ public class SysSystemConfigServiceImpl extends ServiceImpl<SysSystemConfigMappe
Field configsField = ReflectUtil.getField(OaFactory.class, "CONFIGS");
configsField.setAccessible(true);
Map<String, OaSender> configs = (Map<String, OaSender>) configsField.get(null);
configs.clear();
}
return R.ok();
}
configs.clear();
}
return R.ok();
}
}
@@ -0,0 +1,104 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.admin.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pig4cloud.pigx.admin.api.entity.SysClarityData;
import com.pig4cloud.pigx.admin.handler.ClarityFetchTaskHandler;
import com.pig4cloud.pigx.admin.mapper.SysClarityDataMapper;
import com.pig4cloud.pigx.admin.service.SysSystemInfoService;
import com.pig4cloud.pigx.admin.api.constant.UpmsErrorCodes;
import com.pig4cloud.pigx.common.core.util.MsgUtils;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.data.cache.RedisUtils;
import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.connection.RedisServerCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.*;
/**
* 系统监控服务实现
*
* @author lengleng
* @date 2026-03-26
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SysSystemInfoServiceImpl extends ServiceImpl<SysClarityDataMapper, SysClarityData>
implements SysSystemInfoService {
private final ClarityFetchTaskHandler clarityFetchTask;
@Override
public R<Map<String, Object>> getCacheInfo() {
Properties info = RedisUtils.execute((RedisCallback<Properties>) RedisServerCommands::info);
Properties commandStats = RedisUtils
.execute((RedisCallback<Properties>) connection -> connection.serverCommands().info("commandstats"));
Object dbSize = RedisUtils.execute((RedisCallback<Object>) RedisServerCommands::dbSize);
if (commandStats == null) {
return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_SYSTEM_CACHE_FETCH_ERROR));
}
Map<String, Object> result = new HashMap<>(3);
result.put("info", info);
result.put("dbSize", dbSize);
List<Map<String, String>> pieList = new ArrayList<>();
commandStats.stringPropertyNames().forEach(key -> {
Map<String, String> data = new HashMap<>(2);
String property = commandStats.getProperty(key);
data.put("name", StringUtils.removeStart(key, "cmdstat_"));
data.put("value", StringUtils.substringBetween(property, "calls=", ",usec"));
pieList.add(data);
});
result.put("commandStats", pieList);
return R.ok(result);
}
@Override
public R<?> getClarityData(int numOfDays) {
// 查询今日对应 numOfDays 的缓存数据
SysClarityData today = baseMapper.selectOne(Wrappers.<SysClarityData>lambdaQuery()
.eq(SysClarityData::getDataDate, LocalDate.now())
.eq(SysClarityData::getNumOfDays, numOfDays));
if (today != null) {
return R.ok(today);
}
// 无数据:触发异步拉取,立即返回 loading 状态
// 提前捕获租户 ID,避免 @Async 新线程丢失 ThreadLocal 上下文
clarityFetchTask.fetchAndSave(TenantContextHolder.getTenantId(), numOfDays);
Map<String, Object> loading = new HashMap<>(2);
loading.put("loading", true);
loading.put("message", "数据获取中,请稍后手动刷新");
return R.ok(loading);
}
}
@@ -246,24 +246,68 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean deleteUserByIds(Long[] ids) {
Long currentTenantId = SecurityUtils.getUser().getTenantId();
List<Long> userIds = CollUtil.toList(ids);
// 删除 spring cache
List<SysUser> userList = baseMapper.selectByIds(CollUtil.toList(ids));
List<SysUser> userList = baseMapper.selectByIds(userIds);
Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS);
for (SysUser sysUser : userList) {
cache.evict(sysUser.getUsername());
}
// 删除用户角色
sysUserRoleMapper.delete(Wrappers.<SysUserRole>lambdaQuery().in(SysUserRole::getUserId, CollUtil.toList(ids)));
sysUserDeptMapper.delete(Wrappers.<SysUserDept>lambdaQuery().in(SysUserDept::getUserId, CollUtil.toList(ids)));
sysUserPostMapper.delete(Wrappers.<SysUserPost>lambdaQuery().in(SysUserPost::getUserId, CollUtil.toList(ids)));
sysUserRoleMapper.delete(Wrappers.<SysUserRole>lambdaQuery().in(SysUserRole::getUserId, userIds));
sysUserDeptMapper.delete(Wrappers.<SysUserDept>lambdaQuery().in(SysUserDept::getUserId, userIds));
sysUserPostMapper.delete(Wrappers.<SysUserPost>lambdaQuery().in(SysUserPost::getUserId, userIds));
sysTenantUserMapper.delete(Wrappers.<SysTenantUser>lambdaQuery()
.in(SysTenantUser::getUserId, CollUtil.toList(ids))
.eq(SysTenantUser::getTenantId, SecurityUtils.getUser().getTenantId()));
this.removeBatchByIds(CollUtil.toList(ids));
.in(SysTenantUser::getUserId, userIds)
.eq(SysTenantUser::getTenantId, currentTenantId));
// 用户降级处理,如果用户关联了其他租户,则不删除用户信息,仅删除当前租户关联关系并更新用户所属租户;如果没有关联其他租户,则删除用户信息
List<Long> removableUserIds = findRemovableUserIds(userList, currentTenantId);
if (CollUtil.isNotEmpty(removableUserIds)) {
this.removeBatchByIds(removableUserIds);
}
return Boolean.TRUE;
}
/**
* 查找可移除的用户ID列表
*
* @param userList 用户列表
* @param currentTenantId 当前租户ID
* @return 可移除的用户ID列表
*/
private List<Long> findRemovableUserIds(List<SysUser> userList, Long currentTenantId) {
return TenantBroker.noneAs(() -> {
List<Long> removableUserIds = new ArrayList<>(userList.size());
for (SysUser sysUser : userList) {
SysTenantUser otherTenantUser = sysTenantUserMapper.selectOne(Wrappers.<SysTenantUser>lambdaQuery()
.eq(SysTenantUser::getUserId, sysUser.getUserId())
.ne(SysTenantUser::getTenantId, currentTenantId), false);
if (Objects.isNull(otherTenantUser)) {
removableUserIds.add(sysUser.getUserId());
continue;
}
UserDTO userDTO = new UserDTO();
userDTO.setUserId(sysUser.getUserId());
userDTO.setUsername(sysUser.getUsername());
userDTO.setTenantId(otherTenantUser.getTenantId());
sysTenantService.updateUserTenant(userDTO);
}
return removableUserIds;
});
}
/**
* 更新用户信息
*
* @param userDto 用户信息传输对象
* @return 更新结果
*/
@Override
@CacheEvict(value = CacheConstants.USER_DETAILS, key = "#userDto.username")
public R<Boolean> updateUserInfo(UserDTO userDto) {
@@ -282,14 +326,25 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
if (!StrUtil.equals(userDto.getCode(), codeObj.toString())) {
return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_APP_SMS_ERROR));
}
sysUser.setPhone(userDto.getPhone());
}
return R.ok(this.updateById(sysUser));
}
/**
* 更新用户信息。
* <p>
* 字段审计:根据 {@code userId} 在方法执行前后分别调用当前 Service 的 Mapper 查询用户快照,
* 对比结果由 pigx-common-audit 写入 sys_audit_log。
* </p>
*
* @param userDto 用户更新数据
* @return 更新结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
@Audit(name = "用户更新", spel = "@sysUserMapper.selectById(#userDto.userId)")
@Audit(name = "用户更新", id = "#userDto.userId")
@CacheEvict(value = CacheConstants.USER_DETAILS, key = "#userDto.username")
public Boolean updateUser(UserDTO userDto) {
// 更新用户表信息
@@ -537,7 +592,7 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
public R<Boolean> registerUser(RegisterUserDTO userDto) {
String feignHeader = WebUtils.getRequest().getHeader(SecurityConstants.FROM);
// 外部注册校验短信验证码,对 AI 的内部注册跳过验证码处理
if (!SecurityConstants.FROM_IN.equals(feignHeader)){
if (!SecurityConstants.FROM_IN.equals(feignHeader)) {
if (StrUtil.isBlank(userDto.getCode())) {
return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_PARAM_ILLEGAL));
}
@@ -12,10 +12,11 @@ spring:
server-addr: ${NACOS_HOST:pigx-register}:${NACOS_PORT:8848}
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
gateway.server.webflux: # 关闭 Spring Cloud Gateway 的能力,避免与 Spring MVC 冲突。
redis.enabled: false
enabled: false
config:
import:
- optional:nacos:application-@profiles.active@.yml
- optional:nacos:${spring.application.name}-@profiles.active@.yml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~
~ Copyright (c) 2018-2026, lengleng All rights reserved.
~ Copyright (c) 2018-2025, lengleng All rights reserved.
~
~ Redistribution and use in source and binary forms, with or without
~ modification, are permitted provided that the following conditions are met:
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~
~ Copyright (c) 2018-2026, lengleng All rights reserved.
~ Copyright (c) 2018-2025, lengleng All rights reserved.
~
~ Redistribution and use in source and binary forms, with or without
~ modification, are permitted provided that the following conditions are met:
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~
~ Copyright (c) 2018-2026, lengleng All rights reserved.
~ Copyright (c) 2018-2025, lengleng All rights reserved.
~
~ Redistribution and use in source and binary forms, with or without
~ modification, are permitted provided that the following conditions are met:
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~
~ Copyright (c) 2018-2026, lengleng All rights reserved.
~ Copyright (c) 2018-2025, lengleng All rights reserved.
~
~ Redistribution and use in source and binary forms, with or without
~ modification, are permitted provided that the following conditions are met:
@@ -48,7 +48,7 @@
WHERE p.post_id = up.post_id
AND p.del_flag = '0'
and up.user_id = #{userId}
and up.tenant_id = #{tenantId}
and up.tenant_id = #{tenantId}
</select>
</mapper>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~
~ Copyright (c) 2018-2026, lengleng All rights reserved.
~ Copyright (c) 2018-2025, lengleng All rights reserved.
~
~ Redistribution and use in source and binary forms, with or without
~ modification, are permitted provided that the following conditions are met:
@@ -49,6 +49,6 @@
WHERE sys_role.role_id = sys_user_role.role_id
AND sys_role.del_flag = '0'
and sys_user_role.user_id = #{userId}
and sys_user_role.tenant_id = #{tenantId}
and sys_user_role.tenant_id = #{tenantId}
</select>
</mapper>
@@ -0,0 +1,4 @@
<?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="com.pig4cloud.pigx.admin.mapper.SysRoleWidgetMapper">
</mapper>