更新项目配置,新增设备同步模块,优化WebSocket和Swagger配置,增强SCADA系统的免登录接口,支持数据字典项和登录日志的免登录查询与记录。调整Java编译设置,确保更好的开发体验。
This commit is contained in:
7
.vscode/keybindings.json
vendored
Normal file
7
.vscode/keybindings.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"key": "ctrl+alt+r",
|
||||
"command": "workbench.action.tasks.runTask",
|
||||
"args": "YY.Admin: run"
|
||||
}
|
||||
]
|
||||
53
.vscode/tasks.json
vendored
Normal file
53
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "YY.Admin: restore",
|
||||
"type": "process",
|
||||
"command": "C:\\Program Files\\dotnet\\dotnet.exe",
|
||||
"args": ["restore", "YY.Admin.Entry.sln"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}\\yy-admin-master"
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "YY.Admin: build",
|
||||
"type": "process",
|
||||
"command": "C:\\Program Files\\dotnet\\dotnet.exe",
|
||||
"args": ["build", "YY.Admin.Entry.sln", "-c", "Debug"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}\\yy-admin-master"
|
||||
},
|
||||
"dependsOn": "YY.Admin: restore",
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "YY.Admin: run",
|
||||
"type": "process",
|
||||
"command": "${workspaceFolder}\\yy-admin-master\\YY.Admin\\bin\\Debug\\net8.0-windows10.0.19041\\win-x64\\YY.Admin.exe",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}\\yy-admin-master\\YY.Admin\\bin\\Debug\\net8.0-windows10.0.19041\\win-x64"
|
||||
},
|
||||
"dependsOn": "YY.Admin: build",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "YY.Admin: run (script)",
|
||||
"type": "process",
|
||||
"command": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
|
||||
"args": [
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
"${workspaceFolder}\\yy-admin-master\\.vscode\\run-yyadmin.ps1"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}\\yy-admin-master"
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
158
docs/SCADA-用户同步接口文档.md
Normal file
158
docs/SCADA-用户同步接口文档.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# SCADA 用户同步接口文档
|
||||
|
||||
## 1. 目标与适用场景
|
||||
|
||||
本文档用于规范 SCADA 系统对 JeecgBoot 用户数据的读取方式,支持以下场景:
|
||||
|
||||
- SCADA 首次全量拉取用户并缓存到本地。
|
||||
- SCADA 按更新时间进行增量同步(断网续传)。
|
||||
- SCADA 按条件(用户名、工号、手机号)查询特定用户。
|
||||
- 在需要时拉取用户的组织与租户详细信息。
|
||||
|
||||
---
|
||||
|
||||
## 2. 接口信息
|
||||
|
||||
- **接口名称**:SCADA-免登录查询用户信息
|
||||
- **请求方法**:`GET`
|
||||
- **接口路径**:`/jeecg-boot/sys/user/scada/queryUser`
|
||||
- **认证要求**:免登录(已加入后端白名单)
|
||||
- **文档状态**:已在 Knife4j 中取消 `X-Access-Token` 必填限制
|
||||
|
||||
---
|
||||
|
||||
## 3. 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| `username` | string | 否 | - | 用户名,精确匹配 |
|
||||
| `workNo` | string | 否 | - | 工号,精确匹配 |
|
||||
| `phone` | string | 否 | - | 手机号,精确匹配 |
|
||||
| `updatedAfter` | string | 否 | - | 增量同步时间游标,仅返回更新时间或创建时间大于等于该值的数据。支持格式:`yyyy-MM-dd HH:mm:ss`、`yyyy-MM-dd'T'HH:mm:ss` |
|
||||
| `pageNo` | int | 否 | `1` | 页码,从 1 开始 |
|
||||
| `pageSize` | int | 否 | `200` | 每页条数,最大 `1000` |
|
||||
| `includeDetail` | boolean | 否 | `false` | 是否返回部门/公司/租户详细信息;`true` 会增加查询耗时 |
|
||||
|
||||
> 说明:`username/workNo/phone` 三者都可为空;都为空时按分页返回“全部有效用户”。
|
||||
|
||||
---
|
||||
|
||||
## 4. 返回结构
|
||||
|
||||
顶层响应遵循 JeecgBoot 标准格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"code": 200,
|
||||
"result": [
|
||||
{
|
||||
"id": "用户ID",
|
||||
"username": "登录账号",
|
||||
"realname": "真实姓名",
|
||||
"password": "加密密码串",
|
||||
"salt": "密码盐值",
|
||||
"workNo": "工号",
|
||||
"phone": "手机号",
|
||||
"email": "邮箱",
|
||||
"orgCode": "当前组织编码",
|
||||
"post": "岗位ID/信息",
|
||||
"status": 1,
|
||||
"createTime": "创建时间",
|
||||
"updateTime": "更新时间",
|
||||
"lastPwdUpdateTime": "最后修改密码时间",
|
||||
"departIds": "负责部门IDs",
|
||||
"departmentList": [],
|
||||
"companyList": [],
|
||||
"loginTenantId": 1002,
|
||||
"tenantList": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 4.1 字段说明(核心)
|
||||
|
||||
- `updateTime`:**增量同步主游标字段**,建议 SCADA 以此推进同步位点。
|
||||
- `createTime`:用于补偿“新增但未更新”的数据。
|
||||
- `departmentList`:仅在 `includeDetail=true` 时有值(否则为空数组)。
|
||||
- `companyList`:仅在 `includeDetail=true` 时有值(否则为空数组)。
|
||||
- `tenantList`:仅在 `includeDetail=true` 时有值(否则为空数组)。
|
||||
- `password`/`salt`:均为后端存储态字段(非明文);如无强依赖,建议 SCADA 侧不落库或脱敏保存。
|
||||
|
||||
---
|
||||
|
||||
## 5. 查询与同步规则
|
||||
|
||||
后端固定只返回“有效用户”:
|
||||
|
||||
- `delFlag = 0`
|
||||
- `status = 1`
|
||||
|
||||
当传入 `updatedAfter` 时,后端过滤条件为:
|
||||
|
||||
- `updateTime >= updatedAfter` **或**
|
||||
- `createTime >= updatedAfter`
|
||||
|
||||
---
|
||||
|
||||
## 6. 推荐调用策略(给 SCADA)
|
||||
|
||||
### 6.1 首次全量同步
|
||||
|
||||
1. `pageNo=1`,`pageSize=500`,`includeDetail=false`。
|
||||
2. 按页拉取至空页(或小于 `pageSize`)。
|
||||
3. 本地记录本次最大 `updateTime` 作为 `syncCursor`。
|
||||
|
||||
示例:
|
||||
|
||||
```http
|
||||
GET /jeecg-boot/sys/user/scada/queryUser?pageNo=1&pageSize=500&includeDetail=false
|
||||
```
|
||||
|
||||
### 6.2 增量同步(断网续传)
|
||||
|
||||
1. 使用本地 `syncCursor` 作为 `updatedAfter`。
|
||||
2. 分页拉取,直到无新增数据。
|
||||
3. 每处理完一页更新本地游标(建议按返回中最大 `updateTime`)。
|
||||
|
||||
示例:
|
||||
|
||||
```http
|
||||
GET /jeecg-boot/sys/user/scada/queryUser?updatedAfter=2026-04-27 10:00:00&pageNo=1&pageSize=500&includeDetail=false
|
||||
```
|
||||
|
||||
### 6.3 明细补拉策略
|
||||
|
||||
若 SCADA 需要组织和租户完整信息,仅对“已识别变更的用户”按条件补拉,并使用:
|
||||
|
||||
- `includeDetail=true`
|
||||
- 同时带 `username/workNo/phone` 之一,缩小范围
|
||||
|
||||
---
|
||||
|
||||
## 7. 性能说明
|
||||
|
||||
- 默认 `includeDetail=false`,可避免部门与租户明细查询带来的额外耗时。
|
||||
- 单次 `pageSize` 上限 `1000`,防止单请求过大。
|
||||
- 如业务需要更高吞吐,建议后续将部门/租户查询改为批量聚合查询,避免 N+1 查询。
|
||||
|
||||
---
|
||||
|
||||
## 8. 安全建议
|
||||
|
||||
当前接口为免登录接口,建议尽快补充至少一种安全手段:
|
||||
|
||||
- IP 白名单(仅允许 SCADA 网段访问)
|
||||
- 网关层签名校验(`appKey + timestamp + sign`)
|
||||
- HTTPS 强制 + 调用方证书
|
||||
|
||||
---
|
||||
|
||||
## 9. 当前实现位置
|
||||
|
||||
- 控制器:`jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysUserController.java`
|
||||
- 文档与安全配置:`jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/Swagger3Config.java`
|
||||
- 免登录白名单:`jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-*.yml`
|
||||
|
||||
@@ -48,7 +48,10 @@ public class Swagger3Config implements WebMvcConfigurer {
|
||||
"/sys/cas/client/validateLogin",
|
||||
"/test/jeecgDemo/demo3",
|
||||
"/sys/thirdLogin/**",
|
||||
"/sys/user/register"
|
||||
"/sys/user/register",
|
||||
"/sys/user/scada/queryUser",
|
||||
"/sys/dict/scada/queryDictItem",
|
||||
"/sys/log/scada/addLoginLog"
|
||||
));
|
||||
// 预处理通配符模式,提高匹配效率
|
||||
private static final Set<String> wildcardPatterns = new HashSet<>();
|
||||
@@ -79,7 +82,18 @@ public class Swagger3Config implements WebMvcConfigurer {
|
||||
|
||||
@Bean
|
||||
public GlobalOpenApiMethodFilter globalOpenApiMethodFilter() {
|
||||
return method -> method.isAnnotationPresent(Operation.class);
|
||||
return method -> {
|
||||
if (method.isAnnotationPresent(Operation.class)) {
|
||||
return true;
|
||||
}
|
||||
RequestMapping classMapping = method.getDeclaringClass().getAnnotation(RequestMapping.class);
|
||||
if (classMapping != null && classMapping.value().length > 0) {
|
||||
String classPath = classMapping.value()[0];
|
||||
// 兼容系统用户管理接口,未加 @Operation 的接口也展示在文档中
|
||||
return classPath.startsWith("/sys/user");
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -89,6 +103,8 @@ public class Swagger3Config implements WebMvcConfigurer {
|
||||
if (!isExcludedPath(path)) {
|
||||
operation.addSecurityItem(new SecurityRequirement().addList(CommonConstant.X_ACCESS_TOKEN));
|
||||
}else{
|
||||
// 对于免登录接口,显式清空 security,覆盖 OpenAPI 全局安全要求
|
||||
operation.setSecurity(new java.util.ArrayList<>());
|
||||
log.info("忽略加入 X_ACCESS_TOKEN 的 PATH:" + path);
|
||||
}
|
||||
return operation;
|
||||
|
||||
@@ -31,7 +31,7 @@ public class WebSocketConfig {
|
||||
FilterRegistrationBean bean = new FilterRegistrationBean();
|
||||
bean.setFilter(websocketFilter());
|
||||
//TODO 临时注释掉,测试下线上socket总断的问题
|
||||
bean.addUrlPatterns("/taskCountSocket/*", "/websocket/*","/eoaSocket/*","/eoaNewChatSocket/*", "/newsWebsocket/*", "/dragChannelSocket/*", "/vxeSocket/*");
|
||||
bean.addUrlPatterns("/taskCountSocket/*", "/websocket/*","/eoaSocket/*","/eoaNewChatSocket/*", "/newsWebsocket/*", "/dragChannelSocket/*", "/vxeSocket/*", "/ws/*");
|
||||
return bean;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,9 +35,15 @@ public class WebsocketFilter implements Filter {
|
||||
redisUtil = SpringContextUtils.getBean(RedisUtil.class);
|
||||
}
|
||||
HttpServletRequest request = (HttpServletRequest)servletRequest;
|
||||
String requestUri = request.getRequestURI();
|
||||
// SCADA桌面端固定通道:允许免token接入,保障工控机实时推送可用
|
||||
if (isScadaAnonymousEndpoint(requestUri)) {
|
||||
filterChain.doFilter(servletRequest, servletResponse);
|
||||
return;
|
||||
}
|
||||
String token = request.getHeader(TOKEN_KEY);
|
||||
|
||||
log.debug("Websocket连接 Token安全校验,Path = {},token:{}", request.getRequestURI(), token);
|
||||
log.debug("Websocket连接 Token安全校验,Path = {},token:{}", requestUri, token);
|
||||
|
||||
try {
|
||||
TokenUtils.verifyToken(token, commonApi, redisUtil);
|
||||
@@ -51,4 +57,16 @@ public class WebsocketFilter implements Filter {
|
||||
filterChain.doFilter(servletRequest, servletResponse);
|
||||
}
|
||||
|
||||
private boolean isScadaAnonymousEndpoint(String requestUri) {
|
||||
if (oConvertUtils.isEmpty(requestUri)) {
|
||||
return false;
|
||||
}
|
||||
String uri = requestUri.toLowerCase();
|
||||
// 设备同步 STOMP 原生握手(与 jeecg-module-device-sync 的 /ws/device 一致)
|
||||
if (uri.contains("/ws/device")) {
|
||||
return true;
|
||||
}
|
||||
return uri.endsWith("/websocket/scada-sync");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -172,6 +172,8 @@ public class ShiroConfig {
|
||||
|
||||
//websocket排除
|
||||
filterChainDefinitionMap.put("/websocket/**", "anon");//系统通知和公告
|
||||
// 设备同步模块 STOMP(工控机统一通道,与桌面端 /ws/device 对接)
|
||||
filterChainDefinitionMap.put("/ws/**", "anon");
|
||||
filterChainDefinitionMap.put("/newsWebsocket/**", "anon");//CMS模块
|
||||
filterChainDefinitionMap.put("/vxeSocket/**", "anon");//JVxeTable无痕刷新示例
|
||||
//App vue3版本查询版本接口
|
||||
|
||||
33
jeecg-boot/jeecg-module-device-sync/pom.xml
Normal file
33
jeecg-boot/jeecg-module-device-sync/pom.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.jeecgframework.boot3</groupId>
|
||||
<artifactId>jeecg-boot-parent</artifactId>
|
||||
<version>3.9.1</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>jeecg-module-device-sync</artifactId>
|
||||
<name>jeecg-module-device-sync</name>
|
||||
<description>设备断联续传同步模块</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework.boot3</groupId>
|
||||
<artifactId>jeecg-boot-base-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.jeecg.modules.device.sync.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||
|
||||
/**
|
||||
* 设备同步 WebSocket STOMP 配置。
|
||||
*/
|
||||
@Configuration("deviceSyncWebSocketConfig")
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
registry.enableSimpleBroker("/topic", "/queue");
|
||||
registry.setApplicationDestinationPrefixes("/app");
|
||||
registry.setUserDestinationPrefix("/user");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry.addEndpoint("/ws/device")
|
||||
.setAllowedOriginPatterns("*");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.jeecg.modules.device.sync.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.device.sync.entity.DeviceStatus;
|
||||
import org.jeecg.modules.device.sync.mapper.DeviceStatusMapper;
|
||||
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
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 java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 设备 WebSocket 控制器。
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/sys/device")
|
||||
@RequiredArgsConstructor
|
||||
public class DeviceWebSocketController {
|
||||
|
||||
private final DeviceStatusMapper deviceStatusMapper;
|
||||
private final SimpMessagingTemplate messagingTemplate;
|
||||
|
||||
@MessageMapping("/device/status")
|
||||
public void receiveDeviceStatus(DeviceStatus payload) {
|
||||
if (payload == null || payload.getDeviceId() == null || payload.getDeviceId().isBlank()) {
|
||||
log.warn("设备状态上报参数无效");
|
||||
return;
|
||||
}
|
||||
Date now = new Date();
|
||||
payload.setLastSeenTime(now);
|
||||
payload.setUpdateTime(now);
|
||||
if (payload.getCreatedTime() == null) {
|
||||
payload.setCreatedTime(now);
|
||||
}
|
||||
|
||||
DeviceStatus exists = deviceStatusMapper.selectOne(new LambdaQueryWrapper<DeviceStatus>()
|
||||
.eq(DeviceStatus::getDeviceId, payload.getDeviceId())
|
||||
.last("limit 1"));
|
||||
if (exists == null) {
|
||||
deviceStatusMapper.insert(payload);
|
||||
} else {
|
||||
payload.setId(exists.getId());
|
||||
payload.setCreatedTime(exists.getCreatedTime());
|
||||
deviceStatusMapper.updateById(payload);
|
||||
}
|
||||
|
||||
messagingTemplate.convertAndSend("/topic/device/" + payload.getDeviceId(), payload);
|
||||
log.debug("设备状态已广播, deviceId={}", payload.getDeviceId());
|
||||
}
|
||||
|
||||
@PostMapping("/command")
|
||||
public Result<Map<String, Object>> sendCommand(@RequestBody Map<String, Object> request) {
|
||||
String deviceId = request == null ? null : String.valueOf(request.getOrDefault("deviceId", ""));
|
||||
if (deviceId == null || deviceId.isBlank()) {
|
||||
return Result.error("deviceId不能为空");
|
||||
}
|
||||
String commandJson = request == null ? "{}" : String.valueOf(request.getOrDefault("commandJson", "{}"));
|
||||
Map<String, Object> commandPayload = new HashMap<>();
|
||||
commandPayload.put("deviceId", deviceId);
|
||||
commandPayload.put("commandJson", commandJson);
|
||||
commandPayload.put("sentAt", System.currentTimeMillis());
|
||||
|
||||
messagingTemplate.convertAndSendToUser(deviceId, "/queue/command", commandPayload);
|
||||
log.info("下发设备指令成功, deviceId={}", deviceId);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("deviceId", deviceId);
|
||||
result.put("queued", true);
|
||||
return Result.OK(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package org.jeecg.modules.device.sync.controller;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.modules.device.sync.dto.SyncMessageDto;
|
||||
import org.jeecg.modules.device.sync.entity.DeviceRegistry;
|
||||
import org.jeecg.modules.device.sync.entity.DeviceStatus;
|
||||
import org.jeecg.modules.device.sync.entity.SyncIdempotentLog;
|
||||
import org.jeecg.modules.device.sync.mapper.DeviceRegistryMapper;
|
||||
import org.jeecg.modules.device.sync.mapper.DeviceStatusMapper;
|
||||
import org.jeecg.modules.device.sync.mapper.SyncIdempotentLogMapper;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
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 java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 数据同步控制器。
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/sys/sync")
|
||||
@RequiredArgsConstructor
|
||||
public class SyncController {
|
||||
|
||||
private final SyncIdempotentLogMapper syncIdempotentLogMapper;
|
||||
private final DeviceStatusMapper deviceStatusMapper;
|
||||
private final DeviceRegistryMapper deviceRegistryMapper;
|
||||
|
||||
@PostMapping("/batch")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Map<String, Integer>> batch(@RequestBody List<SyncMessageDto> messages) {
|
||||
int received = messages == null ? 0 : messages.size();
|
||||
int inserted = 0;
|
||||
if (messages == null || messages.isEmpty()) {
|
||||
Map<String, Integer> empty = new HashMap<>();
|
||||
empty.put("received", 0);
|
||||
empty.put("inserted", 0);
|
||||
return Result.OK(empty);
|
||||
}
|
||||
|
||||
for (SyncMessageDto message : messages) {
|
||||
if (message == null || message.getMessageId() == null || message.getMessageId().isBlank()) {
|
||||
continue;
|
||||
}
|
||||
if (syncIdempotentLogMapper.exists(message.getMessageId())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
routeAndPersist(message);
|
||||
SyncIdempotentLog logEntity = new SyncIdempotentLog();
|
||||
logEntity.setMessageId(message.getMessageId());
|
||||
logEntity.setAggregateType(message.getAggregateType());
|
||||
logEntity.setAggregateId(message.getAggregateId());
|
||||
logEntity.setEventType(message.getEventType());
|
||||
logEntity.setCreatedTime(new Date());
|
||||
syncIdempotentLogMapper.insert(logEntity);
|
||||
inserted++;
|
||||
}
|
||||
|
||||
Map<String, Integer> result = new HashMap<>();
|
||||
result.put("received", received);
|
||||
result.put("inserted", inserted);
|
||||
return Result.OK(result);
|
||||
}
|
||||
|
||||
private void routeAndPersist(SyncMessageDto message) {
|
||||
String aggregateType = message.getAggregateType() == null ? "" : message.getAggregateType().trim().toUpperCase();
|
||||
switch (aggregateType) {
|
||||
case "DEVICE_STATUS":
|
||||
saveDeviceStatus(message);
|
||||
break;
|
||||
case "DEVICE_REGISTRY":
|
||||
saveDeviceRegistry(message);
|
||||
break;
|
||||
default:
|
||||
log.debug("未识别aggregateType,按透传记录处理,messageId={}, aggregateType={}", message.getMessageId(), aggregateType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void saveDeviceStatus(SyncMessageDto message) {
|
||||
DeviceStatus incoming = JSON.parseObject(message.getPayload(), DeviceStatus.class);
|
||||
if (incoming == null || incoming.getDeviceId() == null || incoming.getDeviceId().isBlank()) {
|
||||
return;
|
||||
}
|
||||
Date now = new Date();
|
||||
incoming.setUpdateTime(now);
|
||||
if (incoming.getCreatedTime() == null) {
|
||||
incoming.setCreatedTime(now);
|
||||
}
|
||||
|
||||
DeviceStatus exists = deviceStatusMapper.selectOne(new LambdaQueryWrapper<DeviceStatus>()
|
||||
.eq(DeviceStatus::getDeviceId, incoming.getDeviceId())
|
||||
.last("limit 1"));
|
||||
if (exists == null) {
|
||||
deviceStatusMapper.insert(incoming);
|
||||
} else {
|
||||
incoming.setId(exists.getId());
|
||||
incoming.setCreatedTime(exists.getCreatedTime());
|
||||
deviceStatusMapper.updateById(incoming);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveDeviceRegistry(SyncMessageDto message) {
|
||||
DeviceRegistry incoming = JSON.parseObject(message.getPayload(), DeviceRegistry.class);
|
||||
if (incoming == null || incoming.getDeviceId() == null || incoming.getDeviceId().isBlank()) {
|
||||
return;
|
||||
}
|
||||
Date now = new Date();
|
||||
incoming.setUpdateTime(now);
|
||||
if (incoming.getCreatedTime() == null) {
|
||||
incoming.setCreatedTime(now);
|
||||
}
|
||||
|
||||
DeviceRegistry exists = deviceRegistryMapper.selectOne(new LambdaQueryWrapper<DeviceRegistry>()
|
||||
.eq(DeviceRegistry::getDeviceId, incoming.getDeviceId())
|
||||
.last("limit 1"));
|
||||
if (exists == null) {
|
||||
deviceRegistryMapper.insert(incoming);
|
||||
} else {
|
||||
incoming.setId(exists.getId());
|
||||
incoming.setCreatedTime(exists.getCreatedTime());
|
||||
deviceRegistryMapper.updateById(incoming);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.jeecg.modules.device.sync.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 同步消息 DTO。
|
||||
*/
|
||||
@Data
|
||||
public class SyncMessageDto implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 消息唯一ID(幂等键)。
|
||||
*/
|
||||
private String messageId;
|
||||
|
||||
/**
|
||||
* 聚合类型。
|
||||
*/
|
||||
private String aggregateType;
|
||||
|
||||
/**
|
||||
* 聚合ID。
|
||||
*/
|
||||
private String aggregateId;
|
||||
|
||||
/**
|
||||
* 事件类型。
|
||||
*/
|
||||
private String eventType;
|
||||
|
||||
/**
|
||||
* JSON字符串载荷。
|
||||
*/
|
||||
private String payload;
|
||||
|
||||
/**
|
||||
* 事件发生时间。
|
||||
*/
|
||||
private Date occurredAt;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.jeecg.modules.device.sync.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 设备注册实体。
|
||||
*/
|
||||
@Data
|
||||
@TableName("device_registry")
|
||||
public class DeviceRegistry implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private String deviceId;
|
||||
|
||||
private String deviceName;
|
||||
|
||||
private String deviceType;
|
||||
|
||||
private Integer tokenVersion;
|
||||
|
||||
private Integer enabled;
|
||||
|
||||
private Date lastSyncTime;
|
||||
|
||||
private Date createdTime;
|
||||
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.jeecg.modules.device.sync.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 设备状态实体。
|
||||
*/
|
||||
@Data
|
||||
@TableName("device_status")
|
||||
public class DeviceStatus implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private String deviceId;
|
||||
|
||||
private Integer onlineStatus;
|
||||
|
||||
private Date lastSeenTime;
|
||||
|
||||
private String statusPayload;
|
||||
|
||||
private Date createdTime;
|
||||
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.jeecg.modules.device.sync.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 同步幂等日志实体。
|
||||
*/
|
||||
@Data
|
||||
@TableName("sync_idempotent_log")
|
||||
public class SyncIdempotentLog implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private String messageId;
|
||||
|
||||
private String aggregateType;
|
||||
|
||||
private String aggregateId;
|
||||
|
||||
private String eventType;
|
||||
|
||||
private Date createdTime;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.jeecg.modules.device.sync.filter;
|
||||
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.algorithms.Algorithm;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.util.RedisUtil;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 设备 Token 滑动续签过滤器。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
|
||||
public class DeviceTokenRefreshFilter extends OncePerRequestFilter {
|
||||
private static final long DEVICE_TOKEN_TTL_SECONDS = 30L * 24 * 60 * 60;
|
||||
private static final long DEVICE_TOKEN_EXPIRE_MILLIS = 30L * 24 * 60 * 60 * 1000;
|
||||
private static final String PREFIX_DEVICE_TOKEN = "prefix_device_token:";
|
||||
|
||||
private final RedisUtil redisUtil;
|
||||
|
||||
public DeviceTokenRefreshFilter(RedisUtil redisUtil) {
|
||||
this.redisUtil = redisUtil;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
String auth = request.getHeader("Authorization");
|
||||
if (auth != null && auth.startsWith("Bearer ")) {
|
||||
String token = auth.substring(7).trim();
|
||||
try {
|
||||
DecodedJWT jwt = JWT.decode(token);
|
||||
String tokenType = jwt.getClaim("tokenType").asString();
|
||||
if ("device".equalsIgnoreCase(tokenType)) {
|
||||
String deviceId = resolveDeviceId(jwt);
|
||||
if (deviceId != null && !deviceId.isBlank()) {
|
||||
redisUtil.expire(PREFIX_DEVICE_TOKEN + token, DEVICE_TOKEN_TTL_SECONDS);
|
||||
String refreshed = refreshDeviceToken(deviceId);
|
||||
redisUtil.set(PREFIX_DEVICE_TOKEN + refreshed, refreshed, DEVICE_TOKEN_TTL_SECONDS);
|
||||
response.setHeader("X-Refresh-Token", refreshed);
|
||||
log.debug("设备Token已续签, deviceId={}", deviceId);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("设备Token续签跳过: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private String resolveDeviceId(DecodedJWT jwt) {
|
||||
String deviceId = jwt.getClaim("deviceId").asString();
|
||||
if (deviceId == null || deviceId.isBlank()) {
|
||||
deviceId = jwt.getClaim("username").asString();
|
||||
}
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
private String refreshDeviceToken(String deviceId) {
|
||||
Date expiresAt = new Date(System.currentTimeMillis() + DEVICE_TOKEN_EXPIRE_MILLIS);
|
||||
Algorithm algorithm = Algorithm.HMAC256(deviceId + "_device_secret");
|
||||
return JWT.create()
|
||||
.withClaim("username", deviceId)
|
||||
.withClaim("deviceId", deviceId)
|
||||
.withClaim("tokenType", "device")
|
||||
.withExpiresAt(expiresAt)
|
||||
.sign(algorithm);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.jeecg.modules.device.sync.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.jeecg.modules.device.sync.entity.DeviceRegistry;
|
||||
|
||||
@Mapper
|
||||
public interface DeviceRegistryMapper extends BaseMapper<DeviceRegistry> {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.jeecg.modules.device.sync.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.jeecg.modules.device.sync.entity.DeviceStatus;
|
||||
|
||||
@Mapper
|
||||
public interface DeviceStatusMapper extends BaseMapper<DeviceStatus> {
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.jeecg.modules.device.sync.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.jeecg.modules.device.sync.entity.SyncIdempotentLog;
|
||||
|
||||
/**
|
||||
* 同步幂等日志 Mapper。
|
||||
*/
|
||||
@Mapper
|
||||
public interface SyncIdempotentLogMapper extends BaseMapper<SyncIdempotentLog> {
|
||||
|
||||
@Select("SELECT COUNT(1) > 0 FROM sync_idempotent_log WHERE message_id = #{messageId}")
|
||||
boolean exists(@Param("messageId") String messageId);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
CREATE TABLE IF NOT EXISTS sync_idempotent_log (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
message_id VARCHAR(64) NOT NULL COMMENT '消息唯一ID',
|
||||
aggregate_type VARCHAR(64) NOT NULL COMMENT '聚合类型',
|
||||
aggregate_id VARCHAR(64) NOT NULL COMMENT '聚合ID',
|
||||
event_type VARCHAR(64) NOT NULL COMMENT '事件类型',
|
||||
created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_sync_idempotent_log_message_id (message_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同步幂等日志';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS device_status (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
device_id VARCHAR(64) NOT NULL COMMENT '设备ID',
|
||||
online_status TINYINT NOT NULL DEFAULT 0 COMMENT '在线状态:0离线1在线',
|
||||
last_seen_time DATETIME NULL COMMENT '最后在线时间',
|
||||
status_payload JSON NULL COMMENT '状态载荷',
|
||||
created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_device_status_device_id (device_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备状态表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS device_registry (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
device_id VARCHAR(64) NOT NULL COMMENT '设备ID',
|
||||
device_name VARCHAR(128) NULL COMMENT '设备名称',
|
||||
device_type VARCHAR(64) NULL COMMENT '设备类型',
|
||||
token_version INT NOT NULL DEFAULT 1 COMMENT '设备令牌版本',
|
||||
enabled TINYINT NOT NULL DEFAULT 1 COMMENT '是否启用:0否1是',
|
||||
last_sync_time DATETIME NULL COMMENT '最后同步时间',
|
||||
created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_device_registry_device_id (device_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备注册表';
|
||||
@@ -61,6 +61,10 @@
|
||||
<artifactId>jeecg-boot-module-airag</artifactId>
|
||||
<version>${jeecgboot.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-messaging</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.pdfbox</groupId>
|
||||
<artifactId>pdfbox</artifactId>
|
||||
|
||||
@@ -10,9 +10,14 @@ import com.alibaba.fastjson.JSONObject;
|
||||
import org.jeecg.common.base.BaseMap;
|
||||
import org.jeecg.common.constant.WebsocketConst;
|
||||
import org.jeecg.common.modules.redis.client.JeecgRedisClient;
|
||||
import org.jeecg.modules.system.entity.SysLog;
|
||||
import org.jeecg.modules.system.service.ISysLogService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @Author scott
|
||||
@@ -27,6 +32,13 @@ public class WebSocket {
|
||||
/**线程安全Map*/
|
||||
private static ConcurrentHashMap<String, Session> sessionPool = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 当前在线 WebSocket 连接数。
|
||||
*/
|
||||
public static int getOnlineCount() {
|
||||
return sessionPool.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis触发监听名字
|
||||
*/
|
||||
@@ -34,10 +46,15 @@ public class WebSocket {
|
||||
|
||||
//避免初次调用出现空指针的情况
|
||||
private static JeecgRedisClient jeecgRedisClient;
|
||||
private static ISysLogService sysLogService;
|
||||
@Autowired
|
||||
private void setJeecgRedisClient(JeecgRedisClient jeecgRedisClient){
|
||||
WebSocket.jeecgRedisClient = jeecgRedisClient;
|
||||
}
|
||||
@Autowired
|
||||
private void setSysLogService(ISysLogService sysLogService){
|
||||
WebSocket.sysLogService = sysLogService;
|
||||
}
|
||||
|
||||
|
||||
//==========【websocket接受、推送消息等方法 —— 具体服务节点推送ws消息】========================================================================================
|
||||
@@ -45,7 +62,7 @@ public class WebSocket {
|
||||
public void onOpen(Session session, @PathParam(value = "userId") String userId) {
|
||||
try {
|
||||
sessionPool.put(userId, session);
|
||||
log.debug("【系统 WebSocket】有新的连接,总数为:" + sessionPool.size());
|
||||
log.info("【系统 WebSocket】连接建立 userId={}, sessionId={}, online={}", userId, session.getId(), sessionPool.size());
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
@@ -54,7 +71,7 @@ public class WebSocket {
|
||||
public void onClose(@PathParam("userId") String userId) {
|
||||
try {
|
||||
sessionPool.remove(userId);
|
||||
log.debug("【系统 WebSocket】连接断开,总数为:" + sessionPool.size());
|
||||
log.info("【系统 WebSocket】连接断开 userId={}, online={}", userId, sessionPool.size());
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
@@ -67,6 +84,8 @@ public class WebSocket {
|
||||
* @param message
|
||||
*/
|
||||
public void pushMessage(String userId, String message) {
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
for (Map.Entry<String, Session> item : sessionPool.entrySet()) {
|
||||
//userId key值= {用户id + "_"+ 登录token的md5串}
|
||||
//TODO vue2未改key新规则,暂时不影响逻辑
|
||||
@@ -75,29 +94,57 @@ public class WebSocket {
|
||||
try {
|
||||
// 代码逻辑说明: websocket报错 https://gitee.com/jeecg/jeecg-boot/issues/I4C0MU
|
||||
synchronized (session){
|
||||
log.debug("【系统 WebSocket】推送单人消息:" + message);
|
||||
session.getBasicRemote().sendText(message);
|
||||
}
|
||||
successCount++;
|
||||
} catch (Exception e) {
|
||||
failCount++;
|
||||
log.error(e.getMessage(),e);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 日志分级策略:业务消息 info,心跳消息 debug,异常场景 warn/error
|
||||
if (failCount > 0) {
|
||||
log.warn("【系统 WebSocket】定向推送存在失败 userKeyContains={}, success={}, fail={}, payload={}",
|
||||
userId, successCount, failCount, message);
|
||||
} else if (isHeartbeatOrBlank(message == null ? "" : message.trim())) {
|
||||
log.debug("【系统 WebSocket】定向心跳推送 userKeyContains={}, success={}, fail={}",
|
||||
userId, successCount, failCount);
|
||||
} else {
|
||||
log.info("【系统 WebSocket】定向业务推送 userKeyContains={}, success={}, fail={}, payload={}",
|
||||
userId, successCount, failCount, message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ws遍历群发消息
|
||||
*/
|
||||
public void pushMessage(String message) {
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
try {
|
||||
for (Map.Entry<String, Session> item : sessionPool.entrySet()) {
|
||||
String sessionKey = item.getKey();
|
||||
Session session = item.getValue();
|
||||
try {
|
||||
item.getValue().getAsyncRemote().sendText(message);
|
||||
if (session == null || !session.isOpen()) {
|
||||
failCount++;
|
||||
log.warn("【系统 WebSocket】连接不可用,跳过发送,key={}", sessionKey);
|
||||
continue;
|
||||
}
|
||||
// 使用同步发送提升送达确定性,避免 AsyncRemote 在断连边界下静默丢消息
|
||||
synchronized (session) {
|
||||
session.getBasicRemote().sendText(message);
|
||||
}
|
||||
successCount++;
|
||||
} catch (Exception e) {
|
||||
failCount++;
|
||||
log.warn("【系统 WebSocket】发送失败,key={},原因={}", sessionKey, e.getMessage());
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
log.debug("【系统 WebSocket】群发消息:" + message);
|
||||
log.info("【系统 WebSocket】群发完成 success={}, fail={}, online={}, payload={}",
|
||||
successCount, failCount, sessionPool.size(), message);
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
@@ -109,13 +156,25 @@ public class WebSocket {
|
||||
*/
|
||||
@OnMessage
|
||||
public void onMessage(String message, @PathParam(value = "userId") String userId) {
|
||||
if(!"ping".equals(message) && !WebsocketConst.CMD_CHECK.equals(message)){
|
||||
log.debug("【系统 WebSocket】收到客户端消息:" + message);
|
||||
}else{
|
||||
log.debug("【系统 WebSocket】收到客户端消息:" + message);
|
||||
String normalized = message == null ? "" : message.trim();
|
||||
if (isHeartbeatOrBlank(normalized)) {
|
||||
log.debug("【系统 WebSocket】收到客户端心跳/空消息:{}", normalized);
|
||||
if ("ping".equalsIgnoreCase(normalized) || WebsocketConst.CMD_CHECK.equalsIgnoreCase(normalized)) {
|
||||
// 代码逻辑说明: 【issues/1161】前端websocket因心跳导致监听不起作用---
|
||||
this.sendMessage(userId, "ping");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isJsonMessage(normalized)) {
|
||||
log.debug("【系统 WebSocket】忽略非JSON消息:{}", normalized);
|
||||
return;
|
||||
}
|
||||
|
||||
if (handleScadaLog(normalized, userId)) {
|
||||
return;
|
||||
}
|
||||
log.debug("【系统 WebSocket】收到客户端消息:" + normalized);
|
||||
|
||||
// //------------------------------------------------------------------------------
|
||||
// JSONObject obj = new JSONObject();
|
||||
@@ -179,6 +238,82 @@ public class WebSocket {
|
||||
sendMessage(userId, message);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean handleScadaLog(String message, String userId) {
|
||||
try {
|
||||
JSONObject obj = JSONObject.parseObject(message);
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
String cmd = obj.getString("cmd");
|
||||
if (!"SCADA_LOGIN_LOG".equals(cmd) && !"SCADA_LOG".equals(cmd)) {
|
||||
return false;
|
||||
}
|
||||
if (sysLogService == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String category = obj.getString("category");
|
||||
String account = obj.getString("account");
|
||||
Boolean success = obj.getBoolean("success");
|
||||
String msg = obj.getString("message");
|
||||
String clientIp = obj.getString("clientIp");
|
||||
String clientType = normalizeClientType(obj.getString("clientType"));
|
||||
String exception = obj.getString("exception");
|
||||
Integer logType = obj.getInteger("logType");
|
||||
Integer operateType = obj.getInteger("operateType");
|
||||
String method = obj.getString("method");
|
||||
String requestUrl = obj.getString("requestUrl");
|
||||
|
||||
SysLog logEntity = new SysLog();
|
||||
logEntity.setCreateTime(new Date());
|
||||
logEntity.setLogType(logType == null ? ("EXCEPTION".equalsIgnoreCase(category) ? 2 : 1) : logType);
|
||||
logEntity.setOperateType(operateType == null ? ("LOGIN".equalsIgnoreCase(category) ? 1 : 5) : operateType);
|
||||
logEntity.setRequestType("WS");
|
||||
logEntity.setRequestUrl(oConvertUtils.isEmpty(requestUrl) ? ("/websocket/" + userId) : requestUrl);
|
||||
logEntity.setMethod(oConvertUtils.isEmpty(method) ? "SCADA_LOG" : method);
|
||||
logEntity.setUsername(account);
|
||||
logEntity.setUserid(account);
|
||||
logEntity.setIp(oConvertUtils.isEmpty(clientIp) ? "desktop" : clientIp);
|
||||
logEntity.setClientType(clientType);
|
||||
if ("LOGIN".equalsIgnoreCase(category) || "SCADA_LOGIN_LOG".equals(cmd)) {
|
||||
logEntity.setLogContent(String.format("桌面端登录%s:%s", Boolean.TRUE.equals(success) ? "成功" : "失败", oConvertUtils.isEmpty(msg) ? "" : msg));
|
||||
} else if ("EXCEPTION".equalsIgnoreCase(category)) {
|
||||
String exceptionMsg = oConvertUtils.isEmpty(exception) ? "" : (" 异常: " + exception);
|
||||
logEntity.setLogContent(String.format("桌面端异常日志:%s%s", oConvertUtils.isEmpty(msg) ? "" : msg, exceptionMsg));
|
||||
} else {
|
||||
logEntity.setLogContent(String.format("桌面端操作日志:%s", oConvertUtils.isEmpty(msg) ? "" : msg));
|
||||
}
|
||||
logEntity.setRequestParam(message);
|
||||
sysLogService.save(logEntity);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.warn("处理 SCADA 登录日志消息失败: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isHeartbeatOrBlank(String message) {
|
||||
return oConvertUtils.isEmpty(message)
|
||||
|| "ping".equalsIgnoreCase(message)
|
||||
|| "pong".equalsIgnoreCase(message)
|
||||
|| WebsocketConst.CMD_CHECK.equalsIgnoreCase(message);
|
||||
}
|
||||
|
||||
private boolean isJsonMessage(String message) {
|
||||
return !oConvertUtils.isEmpty(message) && message.startsWith("{");
|
||||
}
|
||||
|
||||
private String normalizeClientType(String rawClientType) {
|
||||
if (oConvertUtils.isEmpty(rawClientType)) {
|
||||
return "gkj";
|
||||
}
|
||||
String value = rawClientType.trim().toLowerCase();
|
||||
if ("desktop".equals(value) || "pc".equals(value) || "windows".equals(value)) {
|
||||
return "gkj";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
//=======【采用redis发布订阅模式——推送消息】==========================================================================================
|
||||
|
||||
}
|
||||
@@ -40,6 +40,7 @@ import org.jeecgframework.poi.excel.entity.ExportParams;
|
||||
import org.jeecgframework.poi.excel.entity.ImportParams;
|
||||
import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
|
||||
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cache.annotation.CacheEvict;
|
||||
@@ -153,6 +154,108 @@ public class SysDictController {
|
||||
return Result.ok(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* SCADA 系统专用:免登录查询数据字典项。
|
||||
* 支持按 dictCode / dictName / itemText / itemValue 条件过滤,支持增量同步。
|
||||
*/
|
||||
@GetMapping("/scada/queryDictItem")
|
||||
@Operation(summary = "SCADA-免登录查询数据字典项")
|
||||
public Result<List<Map<String, Object>>> scadaQueryDictItem(
|
||||
@RequestParam(name = "dictCode", required = false) String dictCode,
|
||||
@RequestParam(name = "dictName", required = false) String dictName,
|
||||
@RequestParam(name = "itemText", required = false) String itemText,
|
||||
@RequestParam(name = "itemValue", required = false) String itemValue,
|
||||
@RequestParam(name = "status", required = false) Integer status,
|
||||
@RequestParam(name = "updatedAfter", required = false) String updatedAfter,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "500") Integer pageSize) {
|
||||
|
||||
if (pageNo == null || pageNo < 1) {
|
||||
pageNo = 1;
|
||||
}
|
||||
if (pageSize == null || pageSize < 1) {
|
||||
pageSize = 500;
|
||||
}
|
||||
pageSize = Math.min(pageSize, 1000);
|
||||
|
||||
LambdaQueryWrapper<SysDictItem> itemWrapper = new LambdaQueryWrapper<>();
|
||||
itemWrapper.like(oConvertUtils.isNotEmpty(itemText), SysDictItem::getItemText, itemText);
|
||||
itemWrapper.like(oConvertUtils.isNotEmpty(itemValue), SysDictItem::getItemValue, itemValue);
|
||||
itemWrapper.eq(status != null, SysDictItem::getStatus, status);
|
||||
|
||||
if (oConvertUtils.isNotEmpty(updatedAfter)) {
|
||||
try {
|
||||
Date parsedSyncTime;
|
||||
try {
|
||||
parsedSyncTime = DateUtils.parseDate(updatedAfter, "yyyy-MM-dd HH:mm:ss");
|
||||
} catch (Exception firstEx) {
|
||||
parsedSyncTime = DateUtils.parseDate(updatedAfter, "yyyy-MM-dd'T'HH:mm:ss");
|
||||
}
|
||||
final Date syncTime = parsedSyncTime;
|
||||
itemWrapper.and(w -> w.ge(SysDictItem::getUpdateTime, syncTime).or().ge(SysDictItem::getCreateTime, syncTime));
|
||||
} catch (Exception e) {
|
||||
return Result.error("updatedAfter格式错误,支持:yyyy-MM-dd HH:mm:ss 或 yyyy-MM-dd'T'HH:mm:ss");
|
||||
}
|
||||
}
|
||||
|
||||
// 若传入字典头过滤条件,先按字典头筛出 dictId 再查询字典项
|
||||
if (oConvertUtils.isNotEmpty(dictCode) || oConvertUtils.isNotEmpty(dictName)) {
|
||||
LambdaQueryWrapper<SysDict> dictFilter = new LambdaQueryWrapper<>();
|
||||
dictFilter.eq(SysDict::getDelFlag, CommonConstant.DEL_FLAG_0);
|
||||
dictFilter.like(oConvertUtils.isNotEmpty(dictCode), SysDict::getDictCode, dictCode);
|
||||
dictFilter.like(oConvertUtils.isNotEmpty(dictName), SysDict::getDictName, dictName);
|
||||
List<SysDict> filteredDicts = sysDictService.list(dictFilter);
|
||||
if (oConvertUtils.isEmpty(filteredDicts)) {
|
||||
return Result.ok(new ArrayList<>());
|
||||
}
|
||||
List<String> filteredDictIds = filteredDicts.stream().map(SysDict::getId).collect(java.util.stream.Collectors.toList());
|
||||
itemWrapper.in(SysDictItem::getDictId, filteredDictIds);
|
||||
}
|
||||
|
||||
itemWrapper.orderByAsc(SysDictItem::getSortOrder);
|
||||
itemWrapper.orderByDesc(SysDictItem::getCreateTime);
|
||||
|
||||
IPage<SysDictItem> itemPage = sysDictItemService.page(new Page<>(pageNo, pageSize), itemWrapper);
|
||||
List<SysDictItem> itemList = itemPage.getRecords();
|
||||
if (oConvertUtils.isEmpty(itemList)) {
|
||||
return Result.ok(new ArrayList<>());
|
||||
}
|
||||
|
||||
List<String> dictIds = itemList.stream().map(SysDictItem::getDictId).distinct().collect(java.util.stream.Collectors.toList());
|
||||
LambdaQueryWrapper<SysDict> dictQuery = new LambdaQueryWrapper<>();
|
||||
dictQuery.in(SysDict::getId, dictIds);
|
||||
dictQuery.eq(SysDict::getDelFlag, CommonConstant.DEL_FLAG_0);
|
||||
List<SysDict> dictList = sysDictService.list(dictQuery);
|
||||
Map<String, SysDict> dictMap = dictList.stream().collect(java.util.stream.Collectors.toMap(SysDict::getId, d -> d));
|
||||
|
||||
List<Map<String, Object>> resultList = new ArrayList<>();
|
||||
for (SysDictItem item : itemList) {
|
||||
SysDict dict = dictMap.get(item.getDictId());
|
||||
if (dict == null) {
|
||||
continue;
|
||||
}
|
||||
Map<String, Object> row = new LinkedHashMap<>(24);
|
||||
row.put("id", item.getId());
|
||||
row.put("dictId", item.getDictId());
|
||||
row.put("dictName", dict.getDictName());
|
||||
row.put("dictCode", dict.getDictCode());
|
||||
row.put("dictType", dict.getType());
|
||||
row.put("dictDescription", dict.getDescription());
|
||||
row.put("itemText", item.getItemText());
|
||||
row.put("itemValue", item.getItemValue());
|
||||
row.put("itemDescription", item.getDescription());
|
||||
row.put("sortOrder", item.getSortOrder());
|
||||
row.put("status", item.getStatus());
|
||||
row.put("itemColor", item.getItemColor());
|
||||
row.put("createBy", item.getCreateBy());
|
||||
row.put("createTime", item.getCreateTime());
|
||||
row.put("updateBy", item.getUpdateBy());
|
||||
row.put("updateTime", item.getUpdateTime());
|
||||
resultList.add(row);
|
||||
}
|
||||
return Result.ok(resultList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典数据
|
||||
* @param dictCode
|
||||
|
||||
@@ -8,11 +8,11 @@ import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.jeecg.common.api.vo.Result;
|
||||
import org.jeecg.common.system.base.controller.JeecgController;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.system.vo.LoginUser;
|
||||
import org.jeecg.common.util.IpUtils;
|
||||
import org.jeecg.common.util.oConvertUtils;
|
||||
import org.jeecg.config.JeecgBaseConfig;
|
||||
import org.jeecg.modules.system.entity.SysLog;
|
||||
@@ -23,6 +23,8 @@ import org.jeecgframework.poi.excel.entity.ExportParams;
|
||||
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
|
||||
import org.jeecgframework.poi.handler.inter.IExcelExportServerEnhanced;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
@@ -32,6 +34,7 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
@@ -51,6 +54,77 @@ public class SysLogController extends JeecgController<SysLog, ISysLogService> {
|
||||
@Autowired
|
||||
private ISysLogService sysLogService;
|
||||
|
||||
@PostMapping("/scada/addLoginLog")
|
||||
@Operation(summary = "SCADA-免登录写入登录日志")
|
||||
public Result<String> scadaAddLoginLog(@RequestBody java.util.Map<String, Object> payload, HttpServletRequest request) {
|
||||
payload.put("category", "LOGIN");
|
||||
return scadaAddLog(payload, request);
|
||||
}
|
||||
|
||||
@PostMapping("/scada/addLog")
|
||||
@Operation(summary = "SCADA-免登录写入通用日志")
|
||||
public Result<String> scadaAddLog(@RequestBody java.util.Map<String, Object> payload, HttpServletRequest request) {
|
||||
try {
|
||||
String account = oConvertUtils.getString(payload.get("account"));
|
||||
String message = oConvertUtils.getString(payload.get("message"));
|
||||
String category = oConvertUtils.getString(payload.get("category"));
|
||||
boolean success = Boolean.parseBoolean(oConvertUtils.getString(payload.get("success")));
|
||||
String clientType = normalizeClientType(oConvertUtils.getString(payload.get("clientType")));
|
||||
String exception = oConvertUtils.getString(payload.get("exception"));
|
||||
int logType = oConvertUtils.getInt(payload.get("logType"), "EXCEPTION".equalsIgnoreCase(category) ? 2 : 1);
|
||||
int operateType = oConvertUtils.getInt(payload.get("operateType"), "LOGIN".equalsIgnoreCase(category) ? 1 : 5);
|
||||
String method = oConvertUtils.getString(payload.get("method"));
|
||||
if (oConvertUtils.isEmpty(method)) {
|
||||
method = "LOGIN".equalsIgnoreCase(category) ? "LOGIN" : "SCADA_LOG";
|
||||
}
|
||||
String requestUrl = oConvertUtils.getString(payload.get("requestUrl"));
|
||||
if (oConvertUtils.isEmpty(requestUrl)) {
|
||||
requestUrl = "/sys/log/scada/addLog";
|
||||
}
|
||||
|
||||
SysLog logEntity = new SysLog();
|
||||
logEntity.setCreateTime(new java.util.Date());
|
||||
logEntity.setLogType(logType);
|
||||
logEntity.setOperateType(operateType);
|
||||
logEntity.setRequestType("WS/HTTP");
|
||||
logEntity.setRequestUrl(requestUrl);
|
||||
logEntity.setMethod(method);
|
||||
logEntity.setUsername(account);
|
||||
logEntity.setUserid(account);
|
||||
logEntity.setIp(IpUtils.getIpAddr(request));
|
||||
logEntity.setClientType(clientType);
|
||||
logEntity.setLogContent(buildLogContent(category, success, message, exception));
|
||||
logEntity.setRequestParam(com.alibaba.fastjson.JSON.toJSONString(payload));
|
||||
sysLogService.save(logEntity);
|
||||
return Result.OK("ok");
|
||||
} catch (Exception e) {
|
||||
log.error("SCADA写入日志失败", e);
|
||||
return Result.error("写入失败:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private String buildLogContent(String category, boolean success, String message, String exception) {
|
||||
if ("LOGIN".equalsIgnoreCase(category)) {
|
||||
return String.format("桌面端登录%s:%s", success ? "成功" : "失败", oConvertUtils.isEmpty(message) ? "" : message);
|
||||
}
|
||||
if ("EXCEPTION".equalsIgnoreCase(category)) {
|
||||
String exceptionMsg = oConvertUtils.isEmpty(exception) ? "" : (" 异常: " + exception);
|
||||
return String.format("桌面端异常日志:%s%s", oConvertUtils.isEmpty(message) ? "" : message, exceptionMsg);
|
||||
}
|
||||
return String.format("桌面端操作日志:%s", oConvertUtils.isEmpty(message) ? "" : message);
|
||||
}
|
||||
|
||||
private String normalizeClientType(String rawClientType) {
|
||||
if (oConvertUtils.isEmpty(rawClientType)) {
|
||||
return "gkj";
|
||||
}
|
||||
String value = rawClientType.trim().toLowerCase();
|
||||
if ("desktop".equals(value) || "pc".equals(value) || "windows".equals(value)) {
|
||||
return "gkj";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* for [issues/8699]AutoPoi在使用@ExcelEntity当设置show=true并且该项为null时报错
|
||||
*/
|
||||
|
||||
@@ -9,8 +9,11 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.commons.lang.time.DateUtils;
|
||||
import org.apache.shiro.SecurityUtils;
|
||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||
import org.apache.shiro.authz.annotation.RequiresRoles;
|
||||
@@ -22,6 +25,7 @@ import org.jeecg.common.constant.CacheConstant;
|
||||
import org.jeecg.common.constant.CommonConstant;
|
||||
import org.jeecg.common.constant.PasswordConstant;
|
||||
import org.jeecg.common.constant.SymbolConstant;
|
||||
import org.jeecg.common.constant.WebsocketConst;
|
||||
import org.jeecg.common.modules.redis.client.JeecgRedisClient;
|
||||
import org.jeecg.common.system.query.QueryGenerator;
|
||||
import org.jeecg.common.system.util.JwtUtil;
|
||||
@@ -35,11 +39,13 @@ import org.jeecg.modules.system.excelstyle.ExcelExportSysUserStyle;
|
||||
import org.jeecg.modules.system.model.DepartIdModel;
|
||||
import org.jeecg.modules.system.model.SysUserSysDepPostModel;
|
||||
import org.jeecg.modules.system.model.SysUserSysDepartModel;
|
||||
import org.jeecg.modules.message.websocket.WebSocket;
|
||||
import org.jeecg.modules.system.service.*;
|
||||
import org.jeecg.modules.system.util.ImportSysUserCache;
|
||||
import org.jeecg.modules.system.vo.SysDepartUsersVO;
|
||||
import org.jeecg.modules.system.vo.SysUserExportVo;
|
||||
import org.jeecg.modules.system.vo.SysUserRoleVO;
|
||||
import org.jeecg.modules.system.vo.SysUserTenantVo;
|
||||
import org.jeecg.modules.system.vo.lowapp.DepartAndUserInfo;
|
||||
import org.jeecg.modules.system.vo.lowapp.UpdateDepartInfo;
|
||||
import org.jeecgframework.poi.excel.def.NormalExcelConstants;
|
||||
@@ -48,6 +54,7 @@ import org.jeecgframework.poi.excel.entity.enmus.ExcelType;
|
||||
import org.jeecgframework.poi.excel.view.JeecgEntityExcelView;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
@@ -70,6 +77,7 @@ import java.util.stream.Collectors;
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/sys/user")
|
||||
@Tag(name = "用户管理")
|
||||
public class SysUserController {
|
||||
|
||||
@Autowired
|
||||
@@ -110,6 +118,66 @@ public class SysUserController {
|
||||
@Autowired
|
||||
private JeecgBaseConfig jeecgBaseConfig;
|
||||
|
||||
@Autowired
|
||||
private WebSocket webSocket;
|
||||
|
||||
/** 设备同步模块 STOMP:与桌面端统一通道 /topic/sync/jeecg-users(无 device-sync 模块时为 null) */
|
||||
@Autowired(required = false)
|
||||
private SimpMessagingTemplate simpMessagingTemplate;
|
||||
|
||||
private void notifyScadaUserChanged(String action, String userId) {
|
||||
try {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put(WebsocketConst.MSG_CMD, "SCADA_USER_CHANGED");
|
||||
payload.put("action", action);
|
||||
payload.put("userId", userId);
|
||||
payload.put("timestamp", System.currentTimeMillis());
|
||||
log.info("SCADA用户变更通知开始 action={}, userId={}, onlineWs={}", action, userId, WebSocket.getOnlineCount());
|
||||
// 固定工控机通道定向推送(桌面端连接 /websocket/scada-sync)
|
||||
webSocket.pushMessage("scada-sync", payload.toJSONString());
|
||||
// 单机直推(避免仅依赖Redis链路导致消息偶发丢失)
|
||||
webSocket.pushMessage(payload.toJSONString());
|
||||
// 集群转发:通过Redis发布订阅推送到其它节点在线客户端
|
||||
webSocket.sendMessage(payload.toJSONString());
|
||||
pushUserChangeToStompTopic(payload.toJSONString());
|
||||
log.info("SCADA用户变更通知完成 action={}, userId={}, payload={}", action, userId, payload.toJSONString());
|
||||
} catch (Exception e) {
|
||||
log.warn("推送SCADA用户变更消息失败 action={}, userId={}", action, userId, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void pushUserChangeToStompTopic(String jsonPayload) {
|
||||
if (simpMessagingTemplate == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
simpMessagingTemplate.convertAndSend("/topic/sync/jeecg-users", jsonPayload);
|
||||
} catch (Exception e) {
|
||||
log.debug("STOMP 广播用户变更跳过(device-sync 未启用或 Broker 未就绪): {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyScadaUsersChanged(String action, String ids) {
|
||||
try {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put(WebsocketConst.MSG_CMD, "SCADA_USER_CHANGED");
|
||||
payload.put("action", action);
|
||||
payload.put("userIds", ids);
|
||||
payload.put("timestamp", System.currentTimeMillis());
|
||||
log.info("SCADA批量用户变更通知开始 action={}, ids={}, onlineWs={}", action, ids, WebSocket.getOnlineCount());
|
||||
// 固定工控机通道定向推送(桌面端连接 /websocket/scada-sync)
|
||||
webSocket.pushMessage("scada-sync", payload.toJSONString());
|
||||
// 单机直推(避免仅依赖Redis链路导致消息偶发丢失)
|
||||
webSocket.pushMessage(payload.toJSONString());
|
||||
// 集群转发:通过Redis发布订阅推送到其它节点在线客户端
|
||||
webSocket.sendMessage(payload.toJSONString());
|
||||
pushUserChangeToStompTopic(payload.toJSONString());
|
||||
log.info("SCADA批量用户变更通知完成 action={}, ids={}, payload={}", action, ids, payload.toJSONString());
|
||||
} catch (Exception e) {
|
||||
log.warn("推送SCADA批量用户变更消息失败 action={}, ids={}", action, ids, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户下用户数据(支持租户隔离)
|
||||
* @param user
|
||||
@@ -119,6 +187,7 @@ public class SysUserController {
|
||||
* @return
|
||||
*/
|
||||
@PermissionData(pageComponent = "system/UserList")
|
||||
@Operation(summary = "用户管理-分页查询")
|
||||
@RequestMapping(value = "/list", method = RequestMethod.GET)
|
||||
public Result<IPage<SysUser>> queryPageList(SysUser user,@RequestParam(name="pageNo", defaultValue="1") Integer pageNo,
|
||||
@RequestParam(name="pageSize", defaultValue="10") Integer pageSize,HttpServletRequest req) {
|
||||
@@ -156,6 +225,7 @@ public class SysUserController {
|
||||
}
|
||||
|
||||
@RequiresPermissions("system:user:add")
|
||||
@Operation(summary = "用户管理-新增用户")
|
||||
@RequestMapping(value = "/add", method = RequestMethod.POST)
|
||||
public Result<SysUser> add(@RequestBody JSONObject jsonObject) {
|
||||
Result<SysUser> result = new Result<SysUser>();
|
||||
@@ -178,6 +248,7 @@ public class SysUserController {
|
||||
String relTenantIds = jsonObject.getString("relTenantIds");
|
||||
sysUserService.saveUser(user, selectedRoles, selectedDeparts, relTenantIds, false);
|
||||
baseCommonService.addLog("添加用户,username: " +user.getUsername() ,CommonConstant.LOG_TYPE_2, 2);
|
||||
notifyScadaUserChanged("add", user.getId());
|
||||
result.success("添加成功!");
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
@@ -187,6 +258,7 @@ public class SysUserController {
|
||||
}
|
||||
|
||||
@RequiresPermissions("system:user:edit")
|
||||
@Operation(summary = "用户管理-编辑用户")
|
||||
@RequestMapping(value = "/edit", method = {RequestMethod.PUT,RequestMethod.POST})
|
||||
public Result<SysUser> edit(@RequestBody JSONObject jsonObject) {
|
||||
Result<SysUser> result = new Result<SysUser>();
|
||||
@@ -215,6 +287,7 @@ public class SysUserController {
|
||||
//update-begin---author:wangshuai---date:2025-11-12---for:【JHHB-776】用户编辑,应该从数据库查出老数据,页面传递什么字段,把这些字段覆盖数据库查询结果,再更新---
|
||||
oConvertUtils.copyNonNullFields(user, sysUser);
|
||||
sysUserService.editUser(sysUser, roles, departs, relTenantIds, updateFromPage);
|
||||
notifyScadaUserChanged("edit", sysUser.getId());
|
||||
//update-end---author:wangshuai---date:2025-11-12---for:【JHHB-776】用户编辑,应该从数据库查出老数据,页面传递什么字段,把这些字段覆盖数据库查询结果,再更新---
|
||||
result.success("修改成功!");
|
||||
}
|
||||
@@ -253,6 +326,7 @@ public class SysUserController {
|
||||
String relTenantIds = jsonObject.getString("relTenantIds");
|
||||
sysUserService.saveUser(user, selectedRoles, selectedDeparts, relTenantIds, true);
|
||||
baseCommonService.addLog("添加用户,username: " + user.getUsername(), CommonConstant.LOG_TYPE_2, 2);
|
||||
notifyScadaUserChanged("addTenantUser", user.getId());
|
||||
result.success("添加成功!");
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
@@ -265,11 +339,13 @@ public class SysUserController {
|
||||
* 删除用户
|
||||
*/
|
||||
@RequiresPermissions("system:user:delete")
|
||||
@Operation(summary = "用户管理-删除用户")
|
||||
@RequestMapping(value = "/delete", method = RequestMethod.DELETE)
|
||||
public Result<?> delete(@RequestParam(name="id",required=true) String id) {
|
||||
baseCommonService.addLog("删除用户,id: " +id ,CommonConstant.LOG_TYPE_2, 3);
|
||||
List<String> userNameList = sysUserService.userIdToUsername(Arrays.asList(id));
|
||||
this.sysUserService.deleteUser(id);
|
||||
notifyScadaUserChanged("delete", id);
|
||||
|
||||
if (!userNameList.isEmpty()) {
|
||||
String joinedString = String.join(",", userNameList);
|
||||
@@ -281,11 +357,13 @@ public class SysUserController {
|
||||
* 批量删除用户
|
||||
*/
|
||||
@RequiresPermissions("system:user:deleteBatch")
|
||||
@Operation(summary = "用户管理-批量删除用户")
|
||||
@RequestMapping(value = "/deleteBatch", method = RequestMethod.DELETE)
|
||||
public Result<?> deleteBatch(@RequestParam(name="ids",required=true) String ids) {
|
||||
baseCommonService.addLog("批量删除用户, ids: " +ids ,CommonConstant.LOG_TYPE_2, 3);
|
||||
List<String> userNameList = sysUserService.userIdToUsername(Arrays.asList(ids.split(",")));
|
||||
this.sysUserService.deleteBatchUsers(ids);
|
||||
notifyScadaUsersChanged("deleteBatch", ids);
|
||||
|
||||
// 用户变更,触发同步工作流
|
||||
if (!userNameList.isEmpty()) {
|
||||
@@ -312,6 +390,7 @@ public class SysUserController {
|
||||
if(oConvertUtils.isNotEmpty(id)) {
|
||||
// 代码逻辑说明: [QQYUN-5577]用户列表-冻结用户,再解冻之后,用户还是无法登陆,有缓存问题 #5066------------
|
||||
sysUserService.updateStatus(id,status);
|
||||
notifyScadaUserChanged("status", id);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -329,6 +408,7 @@ public class SysUserController {
|
||||
*/
|
||||
@RequiresRoles({"admin"})
|
||||
@RequiresPermissions("system:user:resetPassword")
|
||||
@Operation(summary = "用户管理-重置密码")
|
||||
@RequestMapping(value = "/resetPassword", method = RequestMethod.PUT)
|
||||
public Result<SysUser> resetPassword(@RequestParam(name = "usernames") String usernames) {
|
||||
Result<SysUser> result = new Result<SysUser>();
|
||||
@@ -344,6 +424,7 @@ public class SysUserController {
|
||||
}
|
||||
|
||||
@RequiresPermissions("system:user:queryById")
|
||||
@Operation(summary = "用户管理-根据ID查询")
|
||||
@RequestMapping(value = "/queryById", method = RequestMethod.GET)
|
||||
public Result<SysUser> queryById(@RequestParam(name = "id", required = true) String id) {
|
||||
Result<SysUser> result = new Result<SysUser>();
|
||||
@@ -384,6 +465,7 @@ public class SysUserController {
|
||||
* @return
|
||||
*/
|
||||
@RequestMapping(value = "/checkOnlyUser", method = RequestMethod.GET)
|
||||
@Operation(summary = "用户管理-校验账号唯一")
|
||||
public Result<Boolean> checkOnlyUser(SysUser sysUser) {
|
||||
Result<Boolean> result = new Result<>();
|
||||
//如果此参数为false则程序发生异常
|
||||
@@ -411,6 +493,7 @@ public class SysUserController {
|
||||
* 修改密码
|
||||
*/
|
||||
@RequiresPermissions("system:user:changepwd")
|
||||
@Operation(summary = "用户管理-修改密码")
|
||||
@RequestMapping(value = "/changePassword", method = RequestMethod.PUT)
|
||||
public Result<?> changePassword(@RequestBody SysUser sysUser, HttpServletRequest request) {
|
||||
//-------------------------------------------------------------------------------------
|
||||
@@ -432,6 +515,107 @@ public class SysUserController {
|
||||
return sysUserService.changePassword(sysUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* SCADA 系统专用:免登录查询用户信息。
|
||||
* 支持通过 username / workNo / phone 任一条件查询,返回基础信息(不返回敏感字段)。
|
||||
*/
|
||||
@GetMapping("/scada/queryUser")
|
||||
@Operation(summary = "SCADA-免登录查询用户信息")
|
||||
public Result<List<Map<String, Object>>> scadaQueryUser(@RequestParam(name = "username", required = false) String username,
|
||||
@RequestParam(name = "workNo", required = false) String workNo,
|
||||
@RequestParam(name = "phone", required = false) String phone,
|
||||
@RequestParam(name = "updatedAfter", required = false) String updatedAfter,
|
||||
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
|
||||
@RequestParam(name = "pageSize", defaultValue = "200") Integer pageSize,
|
||||
@RequestParam(name = "includeDetail", defaultValue = "false") Boolean includeDetail) {
|
||||
if (pageNo == null || pageNo < 1) {
|
||||
pageNo = 1;
|
||||
}
|
||||
if (pageSize == null || pageSize < 1) {
|
||||
pageSize = 200;
|
||||
}
|
||||
// 控制单次最大拉取量,避免接口被大查询拖慢
|
||||
pageSize = Math.min(pageSize, 1000);
|
||||
|
||||
LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(SysUser::getDelFlag, CommonConstant.DEL_FLAG_0);
|
||||
queryWrapper.eq(SysUser::getStatus, Integer.valueOf(CommonConstant.STATUS_1));
|
||||
queryWrapper.eq(oConvertUtils.isNotEmpty(username), SysUser::getUsername, username);
|
||||
queryWrapper.eq(oConvertUtils.isNotEmpty(workNo), SysUser::getWorkNo, workNo);
|
||||
queryWrapper.eq(oConvertUtils.isNotEmpty(phone), SysUser::getPhone, phone);
|
||||
if (oConvertUtils.isNotEmpty(updatedAfter)) {
|
||||
try {
|
||||
Date syncTime = DateUtils.parseDate(updatedAfter, new String[]{"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd'T'HH:mm:ss"});
|
||||
queryWrapper.and(w -> w.ge(SysUser::getUpdateTime, syncTime).or().ge(SysUser::getCreateTime, syncTime));
|
||||
} catch (Exception e) {
|
||||
return Result.error("updatedAfter格式错误,支持:yyyy-MM-dd HH:mm:ss 或 yyyy-MM-dd'T'HH:mm:ss");
|
||||
}
|
||||
}
|
||||
queryWrapper.orderByAsc(SysUser::getSort);
|
||||
queryWrapper.orderByDesc(SysUser::getCreateTime);
|
||||
|
||||
IPage<SysUser> userPage = sysUserService.page(new Page<>(pageNo, pageSize), queryWrapper);
|
||||
List<SysUser> userList = userPage.getRecords();
|
||||
|
||||
List<Map<String, Object>> resultList = new ArrayList<>();
|
||||
for (SysUser user : userList) {
|
||||
List<Map<String, Object>> departList = new ArrayList<>();
|
||||
List<Map<String, Object>> companyList = new ArrayList<>();
|
||||
List<Map<String, Object>> tenantList = new ArrayList<>();
|
||||
|
||||
if (Boolean.TRUE.equals(includeDetail)) {
|
||||
List<SysDepart> userDeparts = sysDepartService.queryUserDeparts(user.getId());
|
||||
for (SysDepart depart : userDeparts) {
|
||||
Map<String, Object> departInfo = new LinkedHashMap<>(8);
|
||||
departInfo.put("id", depart.getId());
|
||||
departInfo.put("departName", depart.getDepartName());
|
||||
departInfo.put("orgCode", depart.getOrgCode());
|
||||
departInfo.put("orgCategory", depart.getOrgCategory());
|
||||
departList.add(departInfo);
|
||||
// 机构类别:1公司、4子公司
|
||||
if ("1".equals(depart.getOrgCategory()) || "4".equals(depart.getOrgCategory())) {
|
||||
companyList.add(departInfo);
|
||||
}
|
||||
}
|
||||
List<SysUserTenantVo> userTenants = userTenantService.getTenantListByUserId(user.getId(), Collections.singletonList(CommonConstant.STATUS_1));
|
||||
for (SysUserTenantVo tenantVo : userTenants) {
|
||||
Map<String, Object> tenantInfo = new LinkedHashMap<>(8);
|
||||
tenantInfo.put("tenantId", tenantVo.getRelTenantIds());
|
||||
tenantInfo.put("tenantName", tenantVo.getName());
|
||||
tenantInfo.put("tenantUserStatus", tenantVo.getUserTenantStatus());
|
||||
tenantInfo.put("tenantUserId", tenantVo.getTenantUserId());
|
||||
tenantInfo.put("tenantAdmin", tenantVo.getTenantAdmin());
|
||||
tenantList.add(tenantInfo);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> userInfo = new LinkedHashMap<>(16);
|
||||
userInfo.put("id", user.getId());
|
||||
userInfo.put("username", user.getUsername());
|
||||
userInfo.put("realname", user.getRealname());
|
||||
// SysUser无独立nickname字段,SCADA同步端约定用realname兼容昵称展示
|
||||
userInfo.put("nickname", user.getRealname());
|
||||
userInfo.put("password", user.getPassword());
|
||||
userInfo.put("salt", user.getSalt());
|
||||
userInfo.put("workNo", user.getWorkNo());
|
||||
userInfo.put("phone", user.getPhone());
|
||||
userInfo.put("email", user.getEmail());
|
||||
userInfo.put("orgCode", user.getOrgCode());
|
||||
userInfo.put("post", user.getPost());
|
||||
userInfo.put("status", user.getStatus());
|
||||
userInfo.put("createTime", user.getCreateTime());
|
||||
userInfo.put("updateTime", user.getUpdateTime());
|
||||
userInfo.put("lastPwdUpdateTime", user.getLastPwdUpdateTime());
|
||||
userInfo.put("departIds", user.getDepartIds());
|
||||
userInfo.put("departmentList", departList);
|
||||
userInfo.put("companyList", companyList);
|
||||
userInfo.put("loginTenantId", user.getLoginTenantId());
|
||||
userInfo.put("tenantList", tenantList);
|
||||
resultList.add(userInfo);
|
||||
}
|
||||
return Result.ok(resultList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定用户和部门关联的数据
|
||||
*
|
||||
|
||||
@@ -36,6 +36,12 @@
|
||||
<artifactId>jeecg-module-xslmes</artifactId>
|
||||
<version>${jeecgboot.version}</version>
|
||||
</dependency>
|
||||
<!-- 设备断联续传同步模块 -->
|
||||
<dependency>
|
||||
<groupId>org.jeecgframework.boot3</groupId>
|
||||
<artifactId>jeecg-module-device-sync</artifactId>
|
||||
<version>${jeecgboot.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- flyway 数据库自动升级 -->
|
||||
<dependency>
|
||||
|
||||
@@ -22,7 +22,10 @@ import java.util.Map;
|
||||
* 报错提醒: 未集成mongo报错,可以打开启动类上面的注释 exclude={MongoAutoConfiguration.class}
|
||||
*/
|
||||
@Slf4j
|
||||
@SpringBootApplication(exclude = MongoAutoConfiguration.class)
|
||||
@SpringBootApplication(
|
||||
exclude = MongoAutoConfiguration.class,
|
||||
scanBasePackages = {"org.jeecg"}
|
||||
)
|
||||
@ImportAutoConfiguration(JustAuthAutoConfiguration.class) // spring boot 3.x justauth 兼容性处理
|
||||
public class JeecgSystemApplication extends SpringBootServletInitializer {
|
||||
|
||||
@@ -32,6 +35,7 @@ public class JeecgSystemApplication extends SpringBootServletInitializer {
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws UnknownHostException {
|
||||
|
||||
SpringApplication app = new SpringApplication(JeecgSystemApplication.class);
|
||||
Map<String, Object> defaultProperties = new HashMap<>();
|
||||
defaultProperties.put("management.health.elasticsearch.enabled", false);
|
||||
|
||||
@@ -233,7 +233,7 @@ jeecg:
|
||||
#webapp文件路径
|
||||
webapp: /opt/webapp
|
||||
shiro:
|
||||
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**
|
||||
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/sys/user/scada/queryUser,/sys/dict/scada/queryDictItem,/sys/log/scada/addLoginLog
|
||||
# 短信发送方式 aliyun阿里云短信 tencent腾讯云短信
|
||||
smsSendType: aliyun
|
||||
#阿里云oss存储和大鱼短信秘钥配置
|
||||
|
||||
@@ -199,7 +199,7 @@ jeecg:
|
||||
#webapp文件路径
|
||||
webapp: /opt/webapp
|
||||
shiro:
|
||||
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**
|
||||
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/sys/user/scada/queryUser,/sys/dict/scada/queryDictItem,/sys/log/scada/addLoginLog
|
||||
#阿里云oss存储和大鱼短信秘钥配置
|
||||
oss:
|
||||
accessKey: ??
|
||||
|
||||
@@ -222,7 +222,7 @@ jeecg:
|
||||
#webapp文件路径
|
||||
webapp: /opt/webapp
|
||||
shiro:
|
||||
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**
|
||||
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/sys/user/scada/queryUser,/sys/dict/scada/queryDictItem,/sys/log/scada/addLoginLog
|
||||
#阿里云oss存储和大鱼短信秘钥配置
|
||||
oss:
|
||||
accessKey: ??
|
||||
|
||||
@@ -219,7 +219,7 @@ jeecg:
|
||||
#webapp文件路径
|
||||
webapp: /opt/webapp
|
||||
shiro:
|
||||
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**
|
||||
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/sys/user/scada/queryUser,/sys/dict/scada/queryDictItem,/sys/log/scada/addLoginLog
|
||||
#阿里云oss存储和大鱼短信秘钥配置
|
||||
oss:
|
||||
accessKey: ??
|
||||
|
||||
@@ -213,7 +213,7 @@ jeecg:
|
||||
#webapp文件路径
|
||||
webapp: /opt/webapp
|
||||
shiro:
|
||||
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**
|
||||
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/sys/user/scada/queryUser,/sys/dict/scada/queryDictItem,/sys/log/scada/addLoginLog
|
||||
#阿里云oss存储和大鱼短信秘钥配置
|
||||
oss:
|
||||
accessKey: ??
|
||||
|
||||
@@ -226,7 +226,7 @@ jeecg:
|
||||
#webapp文件路径
|
||||
webapp: /opt/webapp
|
||||
shiro:
|
||||
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**
|
||||
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/sys/user/scada/queryUser,/sys/dict/scada/queryDictItem,/sys/log/scada/addLoginLog
|
||||
#阿里云oss存储和大鱼短信秘钥配置
|
||||
oss:
|
||||
accessKey: ??
|
||||
|
||||
@@ -226,7 +226,7 @@ jeecg:
|
||||
#webapp文件路径
|
||||
webapp: /opt/jeecg-boot/webapp
|
||||
shiro:
|
||||
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/api/getUserInfo
|
||||
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/api/getUserInfo,/sys/user/scada/queryUser,/sys/dict/scada/queryDictItem,/sys/log/scada/addLoginLog
|
||||
# 短信发送方式 aliyun阿里云短信 tencent腾讯云短信
|
||||
smsSendType: aliyun
|
||||
#阿里云oss存储和大鱼短信秘钥配置
|
||||
|
||||
@@ -208,7 +208,7 @@ jeecg:
|
||||
#webapp文件路径
|
||||
webapp: /opt/webapp
|
||||
shiro:
|
||||
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**
|
||||
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/sys/user/scada/queryUser,/sys/dict/scada/queryDictItem,/sys/log/scada/addLoginLog
|
||||
#阿里云oss存储和大鱼短信秘钥配置
|
||||
oss:
|
||||
accessKey: ??
|
||||
|
||||
@@ -227,7 +227,7 @@ jeecg:
|
||||
#webapp文件路径
|
||||
webapp: D://opt//webapp
|
||||
shiro:
|
||||
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**
|
||||
excludeUrls: /test/jeecgDemo/demo3,/test/jeecgDemo/redisDemo/**,/bigscreen/category/**,/bigscreen/visual/**,/bigscreen/map/**,/jmreport/bigscreen2/**,/sys/user/scada/queryUser,/sys/dict/scada/queryDictItem,/sys/log/scada/addLoginLog
|
||||
# 短信发送方式 aliyun阿里云短信 tencent腾讯云短信
|
||||
smsSendType: aliyun
|
||||
#阿里云oss存储和大鱼短信秘钥配置
|
||||
|
||||
@@ -88,6 +88,7 @@
|
||||
<module>jeecg-boot-base-core</module>
|
||||
<module>jeecg-module-system</module>
|
||||
<module>jeecg-boot-module</module>
|
||||
<module>jeecg-module-device-sync</module>
|
||||
</modules>
|
||||
|
||||
<repositories>
|
||||
|
||||
13
jeecgboot-vue3/pnpm-lock.yaml
generated
13
jeecgboot-vue3/pnpm-lock.yaml
generated
@@ -18,7 +18,7 @@ importers:
|
||||
specifier: ^5.2.9
|
||||
version: 5.2.9
|
||||
'@fontsource/noto-serif-sc':
|
||||
specifier: ^5.2.9
|
||||
specifier: ^5.2.8
|
||||
version: 5.2.9
|
||||
'@fontsource/open-sans':
|
||||
specifier: ^5.2.7
|
||||
@@ -1939,56 +1939,67 @@ packages:
|
||||
resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm-musleabihf@4.52.5':
|
||||
resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-arm64-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-arm64-musl@4.52.5':
|
||||
resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-loong64-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-ppc64-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-riscv64-musl@4.52.5':
|
||||
resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-linux-s390x-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-gnu@4.52.5':
|
||||
resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/rollup-linux-x64-musl@4.52.5':
|
||||
resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/rollup-openharmony-arm64@4.52.5':
|
||||
resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==}
|
||||
|
||||
@@ -7,6 +7,14 @@
|
||||
{
|
||||
"path": "jeecgboot-vue3",
|
||||
"name": "前端 (jeecgboot-vue3)"
|
||||
},
|
||||
{
|
||||
"path": "yy-admin-master",
|
||||
"name": "桌面端 (yy-admin-master)"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"java.compile.nullAnalysis.mode": "automatic",
|
||||
"java.configuration.updateBuildConfiguration": "interactive"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
139
yy-admin-master/.gitignore
vendored
Normal file
139
yy-admin-master/.gitignore
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
################################################################################
|
||||
# 此 .gitignore 文件已由 Microsoft(R) Visual Studio 自动创建。
|
||||
################################################################################
|
||||
|
||||
/.vs/YY.Admin.Entry/FileContentIndex
|
||||
/YY.Admin/bin/Debug/net8.0-windows
|
||||
/YY.Admin/obj
|
||||
/YY.Admin.Core/YY.Admin.Core/bin/Debug/net8.0
|
||||
/YY.Admin.Core/YY.Admin.Core/obj
|
||||
/YY.Admin.Services/YY.Admin.Services/bin/Debug/net8.0
|
||||
/YY.Admin.Services/YY.Admin.Services/obj
|
||||
/.vs/YY.Admin.Entry
|
||||
/.vs/ProjectEvaluation
|
||||
/YY.Admin.Application/YY.Admin.Services/bin/Debug/net8.0
|
||||
/YY.Admin.Application/YY.Admin.Services/obj
|
||||
/YY.Admin.Core/obj
|
||||
/YY.Admin.Services/bin/Debug/net8.0-windows
|
||||
/YY.Admin.Services/obj
|
||||
/YY.Admin.Core/bin/Debug/net8.0-windows
|
||||
/TEsrt/obj
|
||||
|
||||
# Visual Studio 2022 特定文件
|
||||
.vs/
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
*.suo
|
||||
*.cache
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
*.vc.db
|
||||
*.vc.vc.opendb
|
||||
|
||||
# 编译生成文件
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Dd]ebug/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]uild/
|
||||
[Bb]uildOutput/
|
||||
|
||||
# NuGet 包目录
|
||||
[Nn]u[Gg]et/
|
||||
*.nupkg
|
||||
packages/
|
||||
|
||||
# 项目特定文件
|
||||
_ReSharper.*
|
||||
*.resharper
|
||||
*.csproj.user
|
||||
*.DotSettings
|
||||
*.DotSettings.user
|
||||
|
||||
# MSTest 测试结果
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# 本地化文件
|
||||
*.iobj
|
||||
*.ipdb
|
||||
*.pdb
|
||||
*.pgd
|
||||
*.idb
|
||||
|
||||
# 发布输出
|
||||
[Dd]ebugPublic/
|
||||
[Rr]eleasePublic/
|
||||
[Pp]ublish/
|
||||
|
||||
# 日志文件
|
||||
*.log
|
||||
*.tlog
|
||||
*.bak
|
||||
*.tmp
|
||||
|
||||
# Visual Studio 缓存/临时文件
|
||||
*.cachefile
|
||||
*.temporary
|
||||
*.temp
|
||||
|
||||
# Visual Studio Code 工作区文件
|
||||
*.code-workspace
|
||||
|
||||
# Rider IDE 文件
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# 操作系统文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
|
||||
# 项目引用和依赖
|
||||
.project.json
|
||||
.project.lock.json
|
||||
*.njsproj
|
||||
*.pubxml
|
||||
|
||||
# SQL Server 文件
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.sdf
|
||||
|
||||
# Azure 函数文件
|
||||
.local.settings.json
|
||||
|
||||
# 包依赖文件
|
||||
packages.lock.json
|
||||
|
||||
# 覆盖率报告
|
||||
coverage.xml
|
||||
coverage.json
|
||||
*.coverage
|
||||
*.coveragesession
|
||||
|
||||
# 性能分析文件
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
|
||||
# 本地开发配置文件(可根据需要取消注释)
|
||||
# appsettings.Development.json
|
||||
# appsettings.Local.json
|
||||
# secrets.json
|
||||
|
||||
# 前端构建文件(如果包含前端项目)
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
*.tsbuildinfo
|
||||
|
||||
# VS Code C# Dev Tools - Auto-generated runners
|
||||
.vscodecsdt/
|
||||
15
yy-admin-master/.vscode/launch.json
vendored
Normal file
15
yy-admin-master/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "YY.Admin 一键启动",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "YY.Admin: build",
|
||||
"program": "${workspaceFolder}/YY.Admin/bin/Debug/net8.0-windows10.0.19041/win-x64/YY.Admin.exe",
|
||||
"cwd": "${workspaceFolder}/YY.Admin/bin/Debug/net8.0-windows10.0.19041/win-x64",
|
||||
"console": "internalConsole",
|
||||
"stopAtEntry": false
|
||||
}
|
||||
]
|
||||
}
|
||||
19
yy-admin-master/.vscode/run-yyadmin.ps1
vendored
Normal file
19
yy-admin-master/.vscode/run-yyadmin.ps1
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$dotnet = "C:\Program Files\dotnet\dotnet.exe"
|
||||
$solution = Join-Path $PSScriptRoot "..\YY.Admin.Entry.sln"
|
||||
$exe = Join-Path $PSScriptRoot "..\YY.Admin\bin\Debug\net8.0-windows10.0.19041\win-x64\YY.Admin.exe"
|
||||
$exeDir = Split-Path $exe -Parent
|
||||
|
||||
& $dotnet build $solution -c Debug
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
Push-Location $exeDir
|
||||
try {
|
||||
& $exe
|
||||
}
|
||||
finally {
|
||||
Pop-Location
|
||||
}
|
||||
5
yy-admin-master/.vscode/settings.json
vendored
Normal file
5
yy-admin-master/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"terminal.integrated.env.windows": {
|
||||
"PATH": "C:\\Program Files\\dotnet;${env:PATH}"
|
||||
}
|
||||
}
|
||||
53
yy-admin-master/.vscode/tasks.json
vendored
Normal file
53
yy-admin-master/.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "YY.Admin: restore",
|
||||
"type": "process",
|
||||
"command": "C:\\Program Files\\dotnet\\dotnet.exe",
|
||||
"args": ["restore", "YY.Admin.Entry.sln"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
},
|
||||
{
|
||||
"label": "YY.Admin: build",
|
||||
"type": "process",
|
||||
"command": "C:\\Program Files\\dotnet\\dotnet.exe",
|
||||
"args": ["build", "YY.Admin.Entry.sln", "-c", "Debug"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"dependsOn": "YY.Admin: restore",
|
||||
"problemMatcher": "$msCompile",
|
||||
"group": "build"
|
||||
},
|
||||
{
|
||||
"label": "YY.Admin: run",
|
||||
"type": "process",
|
||||
"command": "${workspaceFolder}\\YY.Admin\\bin\\Debug\\net8.0-windows10.0.19041\\win-x64\\YY.Admin.exe",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}\\YY.Admin\\bin\\Debug\\net8.0-windows10.0.19041\\win-x64"
|
||||
},
|
||||
"dependsOn": "YY.Admin: build",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "dotnet run --project YY.Admin.csproj --no-build",
|
||||
"type": "process",
|
||||
"command": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
|
||||
"args": [
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
"${workspaceFolder}\\.vscode\\run-yyadmin.ps1"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
]
|
||||
}
|
||||
21
yy-admin-master/LICENSE
Normal file
21
yy-admin-master/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 一个后端
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
36
yy-admin-master/README.en.md
Normal file
36
yy-admin-master/README.en.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# YY-Admin
|
||||
|
||||
#### Description
|
||||
WPF123123123
|
||||
|
||||
#### Software Architecture
|
||||
Software architecture description
|
||||
|
||||
#### Installation
|
||||
|
||||
1. xxxx
|
||||
2. xxxx
|
||||
3. xxxx
|
||||
|
||||
#### Instructions
|
||||
|
||||
1. xxxx
|
||||
2. xxxx
|
||||
3. xxxx
|
||||
|
||||
#### Contribution
|
||||
|
||||
1. Fork the repository
|
||||
2. Create Feat_xxx branch
|
||||
3. Commit your code
|
||||
4. Create Pull Request
|
||||
|
||||
|
||||
#### Gitee Feature
|
||||
|
||||
1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md
|
||||
2. Gitee blog [blog.gitee.com](https://blog.gitee.com)
|
||||
3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore)
|
||||
4. The most valuable open source project [GVP](https://gitee.com/gvp)
|
||||
5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help)
|
||||
6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
|
||||
96
yy-admin-master/README.md
Normal file
96
yy-admin-master/README.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# YY-Admin WPF 后端通用框架(持续维护中,后续会完整实现RBAC权限管理)
|
||||
## 🍎效果截图
|
||||
<table>
|
||||
<tr>
|
||||
<td><img src="https://gitee.com/peng_xi_wei/yy-admin/raw/master/doc/1753498486751.png"/></td>
|
||||
<td><img src="https://gitee.com/peng_xi_wei/yy-admin/raw/master/doc/17846464112.png"/></td>
|
||||
<td><img src="https://gitee.com/peng_xi_wei/yy-admin/raw/master/doc/1753498486755.png"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 📙介绍
|
||||
|
||||
|
||||
YY-Admin 是一个基于 WPF 的现代后端管理系统通用框架,专为快速构建企业级管理后台而设计。
|
||||
该框架融合了前沿的 WPF 开发技术和最佳实践,提供了丰富的 UI 组件、
|
||||
强大的导航系统和完善的权限管理机制,帮助开发者高效构建功能强大、
|
||||
界面美观的管理系统,开源免费使用。
|
||||
|
||||
框架采用模块化架构设计,内置用户管理、角色权限、系统监控等核心模块,
|
||||
支持快速扩展业务功能。通过精心设计的界面布局和交互体验,
|
||||
YY-Admin 使开发者能够专注于业务逻辑实现,大幅提升开发效率。
|
||||
|
||||
# 动动发财小手,点个⭐ 堆码不易
|
||||
|
||||
**🎁核心价值**:
|
||||
|
||||
🚀 **快速开发**:提供通用管理功能模板,减少重复编码
|
||||
🎨 **现代化界面**:基于 HandyControl 的优雅 UI 设计
|
||||
🔒 **完善权限体系**:细粒度的角色和权限控制
|
||||
📱 **响应式布局**:适配不同屏幕尺寸
|
||||
⚙️ **模块化架构**:支持功能模块灵活扩展
|
||||
|
||||
## 🍁说明
|
||||
1. 支持各种数据库,后台配置文件自行修改(自动生成数据库及种子数据)
|
||||
2. 支持主题切换、自定义字体图标、按钮图标
|
||||
|
||||
## 软件架构
|
||||
|
||||
### 技术栈
|
||||
|
||||
| 组件 | 名称 | 版本 | 用途 |
|
||||
|--------------|------------------|------------|--------------------------|
|
||||
| UI框架 | HandyControl | 3.5.1 | 提供现代化UI组件和样式 |
|
||||
| MVVM框架 | Prism.Core | 9.0.537 | 应用架构和模块化管理 |
|
||||
| IoC容器 | Prism.DryIoc | 9.0.537 | 依赖注入和控制反转 |
|
||||
| ORM | SqlSugar | - | 数据库访问和对象映射 |
|
||||
| 导航 | Prism.Regions | 9.0.537 | 实现区域导航系统 |
|
||||
| 验证 | FluentValidation | - | 数据验证解决方案 |
|
||||
|
||||
### 架构设计
|
||||
```text
|
||||
YY-Admin
|
||||
├── YY.Admin.Core # 核心公共组件
|
||||
│ ├── Models # 数据模型
|
||||
│ ├── Enums # 枚举类型
|
||||
│ └── Extensions # 扩展方法
|
||||
├── YY.Admin.Services # 业务服务层
|
||||
│ ├── AuthService.cs # 认证服务
|
||||
│ ├── UserService.cs # 用户服务
|
||||
│ └── ... # 其他业务服务
|
||||
├── YY.Admin.Views # 视图层
|
||||
│ ├── LoginWindow.xaml # 登录窗口
|
||||
│ ├── MainWindow.xaml # 主窗口
|
||||
│ └── ... # 功能视图
|
||||
├── YY.Admin.ViewModels # 视图模型层
|
||||
│ ├── LoginViewModel.cs # 登录逻辑
|
||||
│ ├── MainViewModel.cs # 主窗口逻辑
|
||||
│ └── ... # 其他视图模型
|
||||
└── App.xaml.cs # 应用入口和配置
|
||||
```
|
||||
## 🎖️更新日志
|
||||
```bash
|
||||
1. 完成种子数据生成与表设计
|
||||
|
||||
2. 完成菜单页的加载
|
||||
|
||||
3. 完成自定义图标
|
||||
|
||||
4. 完成主题设置
|
||||
|
||||
5. 增加菜单标签页
|
||||
|
||||
# 本项目会持续迭代维护,有问题和建议请提出PR!
|
||||
|
||||
```
|
||||
## 参与贡献
|
||||
欢迎贡献代码,共同打造更好的 WPF 管理框架!
|
||||
## 开源不易求打赏!!!
|
||||
<div style="display: flex; justify-content: center; gap: 20px;">
|
||||
<img src="doc/20260226090853.jpg" width="200" alt="打赏码1" />
|
||||
<img src="doc/2026022609091.jpg" width="200" alt="打赏码2" />
|
||||
</div>
|
||||
## 联系作者微信
|
||||
<div style="display: flex; justify-content: center; gap: 20px;">
|
||||
<img src="doc/123213.png" width="200" alt="联系作者微信" />
|
||||
</div>
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.Windows.Controls;
|
||||
using System.Windows;
|
||||
|
||||
namespace YY.Admin.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// DataGrid绑定数据源描述
|
||||
/// </summary>
|
||||
public class BindDescriptionAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 列名
|
||||
/// </summary>
|
||||
public string HeaderName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示为
|
||||
/// </summary>
|
||||
public ShowScheme ShowAs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示顺序
|
||||
/// </summary>
|
||||
public int DisplayIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// DataGrid列绑定属性名称
|
||||
/// </summary>
|
||||
public string PropertyName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 应用内的容模板Key
|
||||
/// </summary>
|
||||
public string ResourceKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 列宽
|
||||
/// </summary>
|
||||
public DataGridLength Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 列宽ByGrid
|
||||
/// </summary>
|
||||
public GridLength CloumnWidth { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// DataGrid绑定数据源描述
|
||||
/// </summary>
|
||||
/// <param name="headerName">列名</param>
|
||||
/// <param name="showAs">显示为</param>
|
||||
/// <param name="width">宽度</param>
|
||||
/// <param name="displayIndex">显示顺序</param>
|
||||
/// <param name="resourceKey">自定义列Key</param>
|
||||
public BindDescriptionAttribute(string headerName, ShowScheme showAs = ShowScheme.普通文本, string width = "Auto", int displayIndex = 0, string resourceKey = "")
|
||||
{
|
||||
this.HeaderName= headerName;
|
||||
DisplayIndex = displayIndex;
|
||||
ResourceKey = resourceKey;
|
||||
ShowAs = showAs;
|
||||
var convert = new DataGridLengthConverter();
|
||||
Width = (DataGridLength)convert.ConvertFrom(width);
|
||||
var gridCOnvert = new GridLengthConverter();
|
||||
CloumnWidth = (GridLength)gridCOnvert.ConvertFrom(width);
|
||||
|
||||
if (showAs == ShowScheme.自定义 && string.IsNullOrWhiteSpace(resourceKey))
|
||||
throw new ArgumentException($"自定义列时需要指定{nameof(resourceKey)}参数!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 展示方式
|
||||
/// </summary>
|
||||
public enum ShowScheme
|
||||
{
|
||||
普通文本 = 1,
|
||||
自定义 = 4
|
||||
}
|
||||
}
|
||||
22
yy-admin-master/YY.Admin.Core/Attribute/ConstAttribute.cs
Normal file
22
yy-admin-master/YY.Admin.Core/Attribute/ConstAttribute.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace YY.Admin.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 常量特性
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = true)]
|
||||
public class ConstAttribute : Attribute
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public ConstAttribute(string name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace YY.Admin.Core
|
||||
{
|
||||
// 定义生命周期标记接口
|
||||
public interface ISingletonDependency { }
|
||||
public interface ITransientDependency { }
|
||||
public interface IScopedDependency { }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace YY.Admin.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 忽略表结构初始化特性(标记在实体)
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||
public class IgnoreTableAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace YY.Admin.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 忽略更新种子特性(标记在种子类)
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||
public class IgnoreUpdateSeedAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
namespace YY.Admin.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 忽略更新种子列特性(标记在实体属性)
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
|
||||
public class IgnoreUpdateSeedColumnAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace YY.Admin.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 增量种子特性
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||
public class IncreSeedAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace YY.Admin.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 增量表特性
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||
public class IncreTableAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace YY.Admin.Core
|
||||
{
|
||||
public enum Lifecycle
|
||||
{
|
||||
Singleton,
|
||||
Transient,
|
||||
Scoped
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class LifecycleAttribute : Attribute
|
||||
{
|
||||
public Lifecycle Lifecycle { get; }
|
||||
|
||||
public LifecycleAttribute(Lifecycle lifecycle)
|
||||
{
|
||||
Lifecycle = lifecycle;
|
||||
}
|
||||
}
|
||||
}
|
||||
16
yy-admin-master/YY.Admin.Core/Attribute/LogTableAttribute.cs
Normal file
16
yy-admin-master/YY.Admin.Core/Attribute/LogTableAttribute.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace YY.Admin.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 日志表特性
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||
public class LogTableAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace YY.Admin.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 表示查询操作的前台操作码
|
||||
/// </summary>
|
||||
public class OperateCodeAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化一个<see cref="OperateCodeAttribute"/>类型的新实例
|
||||
/// </summary>
|
||||
public OperateCodeAttribute(string code)
|
||||
{
|
||||
Code = code;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 属性名称
|
||||
/// </summary>
|
||||
public string Code { get; private set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace YY.Admin.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 所属用户数据权限
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
|
||||
public class OwnerUserAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace YY.Admin.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 查询规则特性
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class QueryRuleAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 查询字段名称
|
||||
/// </summary>
|
||||
public string FieldName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分组名称
|
||||
/// </summary>
|
||||
public string Group { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 查询操作符
|
||||
/// </summary>
|
||||
public FilterOperateEnum Operate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分组查询操作符(生成sql后面的where 带括号的查询,取值只能为or 或 and)
|
||||
/// </summary>
|
||||
public FilterOperateEnum GroupOperate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 查询规则构造函数
|
||||
/// </summary>
|
||||
/// <param name="operate">操作符</param>
|
||||
/// <param name="fieldName">数据库可接受的查询字段名称,未传直接取属性名称</param>
|
||||
/// <param name="group">隶属分组</param>
|
||||
/// <param name="groupOperate">分组查询操作符</param>
|
||||
public QueryRuleAttribute(FilterOperateEnum operate, string fieldName, string group = "", FilterOperateEnum groupOperate = FilterOperateEnum.And)
|
||||
{
|
||||
FieldName = fieldName;
|
||||
Group = group;
|
||||
Operate = operate;
|
||||
GroupOperate = groupOperate;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
yy-admin-master/YY.Admin.Core/Attribute/SeedDataAttribute.cs
Normal file
25
yy-admin-master/YY.Admin.Core/Attribute/SeedDataAttribute.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace YY.Admin.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 种子数据特性
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||
public class SeedDataAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 排序(越大越后执行)
|
||||
/// </summary>
|
||||
public int Order { get; set; } = 0;
|
||||
|
||||
public SeedDataAttribute(int orderNo)
|
||||
{
|
||||
Order = orderNo;
|
||||
}
|
||||
}
|
||||
}
|
||||
16
yy-admin-master/YY.Admin.Core/Attribute/SysTableAttribute.cs
Normal file
16
yy-admin-master/YY.Admin.Core/Attribute/SysTableAttribute.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace YY.Admin.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 系统表特性
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||
public class SysTableAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
22
yy-admin-master/YY.Admin.Core/Attribute/ThemeAttribute.cs
Normal file
22
yy-admin-master/YY.Admin.Core/Attribute/ThemeAttribute.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace YY.Admin.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 枚举拓展主题样式
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Enum | AttributeTargets.Field)]
|
||||
public class ThemeAttribute : Attribute
|
||||
{
|
||||
public string Theme { get; private set; }
|
||||
|
||||
public ThemeAttribute(string theme)
|
||||
{
|
||||
this.Theme = theme;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using PropertyMetadata = System.Windows.PropertyMetadata;
|
||||
|
||||
namespace YY.Admin.Core.Behavior
|
||||
{
|
||||
public class TreeViewItemClickBehavior
|
||||
{
|
||||
// ICommand attached property
|
||||
public static readonly DependencyProperty CommandProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"Command",
|
||||
typeof(ICommand),
|
||||
typeof(TreeViewItemClickBehavior),
|
||||
new PropertyMetadata(null, OnCommandChanged));
|
||||
|
||||
public static void SetCommand(DependencyObject obj, ICommand value) => obj.SetValue(CommandProperty, value);
|
||||
public static ICommand GetCommand(DependencyObject obj) => (ICommand)obj.GetValue(CommandProperty);
|
||||
|
||||
// CommandParameter attached property
|
||||
public static readonly DependencyProperty CommandParameterProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
"CommandParameter",
|
||||
typeof(object),
|
||||
typeof(TreeViewItemClickBehavior),
|
||||
new PropertyMetadata(null));
|
||||
|
||||
public static void SetCommandParameter(DependencyObject obj, object value) => obj.SetValue(CommandParameterProperty, value);
|
||||
public static object GetCommandParameter(DependencyObject obj) => obj.GetValue(CommandParameterProperty);
|
||||
|
||||
private static void OnCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is TreeViewItem tvi)
|
||||
{
|
||||
if (e.NewValue != null && e.OldValue == null)
|
||||
{
|
||||
tvi.PreviewMouseLeftButtonUp += OnTreeViewItemMouseLeftButtonUp;
|
||||
// 监听 TreeViewItem 展开事件Expanded
|
||||
tvi.Expanded += OnTreeViewItemExpanded;
|
||||
}
|
||||
else if (e.NewValue == null && e.OldValue != null)
|
||||
{
|
||||
tvi.PreviewMouseLeftButtonUp -= OnTreeViewItemMouseLeftButtonUp;
|
||||
tvi.Expanded -= OnTreeViewItemExpanded;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果Style被应用到容器等,也可能不是TreeViewItem,延迟订阅由容器化过程处理
|
||||
// 一般我们只使用在 TreeViewItem 上设置
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnTreeViewItemMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
// 忽略点击展开按钮
|
||||
if (e.OriginalSource is DependencyObject dep)
|
||||
{
|
||||
if (FindAncestor<ToggleButton>(dep) != null) return;
|
||||
|
||||
// 找到真正的 TreeViewItem
|
||||
var tvi = FindAncestor<TreeViewItem>(dep);
|
||||
if (tvi == null) return;
|
||||
|
||||
var cmd = GetCommand(tvi);
|
||||
var param = GetCommandParameter(tvi) ?? tvi.DataContext;
|
||||
|
||||
// 检查是否有子项
|
||||
bool hasChildren = tvi.HasItems;
|
||||
|
||||
// 如果有子项,则切换展开状态
|
||||
if (hasChildren)
|
||||
{
|
||||
bool IsExpanded = !tvi.IsExpanded;
|
||||
tvi.IsExpanded = IsExpanded;
|
||||
|
||||
// 展开
|
||||
//if (IsExpanded)
|
||||
//{
|
||||
// // 关闭同级其它节点
|
||||
// CollapseSiblings(tvi);
|
||||
//}
|
||||
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd != null && cmd.CanExecute(param))
|
||||
{
|
||||
cmd.Execute(param);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnTreeViewItemExpanded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is TreeViewItem tvi)
|
||||
{
|
||||
// 防止事件冒泡重复执行
|
||||
if (e.OriginalSource != tvi)
|
||||
return;
|
||||
|
||||
CollapseSiblings(tvi);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 折叠同级其它 TreeViewItem
|
||||
/// </summary>
|
||||
private static void CollapseSiblings(TreeViewItem currentItem)
|
||||
{
|
||||
ItemsControl parent = ItemsControl.ItemsControlFromItemContainer(currentItem);
|
||||
if (parent == null) return;
|
||||
|
||||
foreach (var item in parent.Items)
|
||||
{
|
||||
var container = parent.ItemContainerGenerator.ContainerFromItem(item) as TreeViewItem;
|
||||
if (container != null && container != currentItem)
|
||||
{
|
||||
container.IsExpanded = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 查找祖先辅助方法
|
||||
private static T FindAncestor<T>(DependencyObject current) where T : DependencyObject
|
||||
{
|
||||
while (current != null)
|
||||
{
|
||||
if (current is T t) return t;
|
||||
current = VisualTreeHelper.GetParent(current);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace YY.Admin.Core.BusinessException
|
||||
{
|
||||
public class BusinessException : Exception
|
||||
{
|
||||
public ErrorCodeEnum ErrorCode { get; }
|
||||
|
||||
public BusinessException(ErrorCodeEnum errorCode, string message)
|
||||
: base(message) => ErrorCode = errorCode;
|
||||
}
|
||||
public static class Oops
|
||||
{
|
||||
public static BusinessException Oh(ErrorCodeEnum errorCode)
|
||||
{
|
||||
// 获取错误信息
|
||||
string message = $"业务错误 {errorCode}: {errorCode.GetDescription()}";
|
||||
return new BusinessException(errorCode, message);
|
||||
}
|
||||
public static BusinessException Oh(string error)
|
||||
{
|
||||
// 获取错误信息
|
||||
string message = $"业务错误 {error}";
|
||||
return new BusinessException(ErrorCodeEnum.A1000, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
151
yy-admin-master/YY.Admin.Core/Cache/ISysCacheService.cs
Normal file
151
yy-admin-master/YY.Admin.Core/Cache/ISysCacheService.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace YY.Admin.Core
|
||||
{
|
||||
public interface ISysCacheService
|
||||
{
|
||||
/// <summary>
|
||||
/// 申请分布式锁
|
||||
/// </summary>
|
||||
IDisposable? BeginCacheLock(string key, int msTimeout = 500, int msExpire = 10000, bool throwOnFailure = true);
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存键名集合
|
||||
/// </summary>
|
||||
List<string> GetKeyList();
|
||||
|
||||
/// <summary>
|
||||
/// 增加缓存
|
||||
/// </summary>
|
||||
bool Set(string key, object value);
|
||||
|
||||
/// <summary>
|
||||
/// 增加缓存并设置过期时间
|
||||
/// </summary>
|
||||
bool Set(string key, object value, TimeSpan expire);
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取或添加缓存(无参数)
|
||||
/// </summary>
|
||||
Task<TR> AdGetAsync<TR>(string cacheName, Func<Task<TR>> del, TimeSpan? expiry = default) where TR : class;
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取或添加缓存(1个参数)
|
||||
/// </summary>
|
||||
Task<TR> AdGetAsync<TR, T1>(string cacheName, Func<T1, Task<TR>> del, T1 t1, TimeSpan? expiry = default) where TR : class;
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取或添加缓存(2个参数)
|
||||
/// </summary>
|
||||
Task<TR> AdGetAsync<TR, T1, T2>(string cacheName, Func<T1, T2, Task<TR>> del, T1 t1, T2 t2, TimeSpan? expiry = default) where TR : class;
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取或添加缓存(3个参数)
|
||||
/// </summary>
|
||||
Task<TR> AdGetAsync<TR, T1, T2, T3>(string cacheName, Func<T1, T2, T3, Task<TR>> del, T1 t1, T2 t2, T3 t3, TimeSpan? expiry = default) where TR : class;
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存(1个参数)
|
||||
/// </summary>
|
||||
T Get<T>(string cacheName, object t1);
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存(2个参数)
|
||||
/// </summary>
|
||||
T Get<T>(string cacheName, object t1, object t2);
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存(3个参数)
|
||||
/// </summary>
|
||||
T Get<T>(string cacheName, object t1, object t2, object t3);
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存的剩余生存时间
|
||||
/// </summary>
|
||||
TimeSpan GetExpire(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存
|
||||
/// </summary>
|
||||
T Get<T>(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 删除缓存
|
||||
/// </summary>
|
||||
int Remove(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有缓存
|
||||
/// </summary>
|
||||
void Clear();
|
||||
|
||||
/// <summary>
|
||||
/// 检查缓存是否存在
|
||||
/// </summary>
|
||||
bool ExistKey(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 根据键名前缀删除缓存
|
||||
/// </summary>
|
||||
int RemoveByPrefixKey(string prefixKey);
|
||||
|
||||
/// <summary>
|
||||
/// 根据键名前缀获取键名集合
|
||||
/// </summary>
|
||||
List<string> GetKeysByPrefixKey(string prefixKey);
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存值(原始对象)
|
||||
/// </summary>
|
||||
object GetValue(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 获取或添加缓存(在数据不存在时执行委托请求数据)
|
||||
/// </summary>
|
||||
T GetOrAdd<T>(string key, Func<string, T> callback, int expire = -1);
|
||||
|
||||
/// <summary>
|
||||
/// 获取Hash缓存字典
|
||||
/// </summary>
|
||||
IDictionary<string, T> GetHashMap<T>(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 批量添加Hash值
|
||||
/// </summary>
|
||||
bool HashSet<T>(string key, Dictionary<string, T> dic);
|
||||
|
||||
/// <summary>
|
||||
/// 添加一条Hash值
|
||||
/// </summary>
|
||||
void HashAdd<T>(string key, string hashKey, T value);
|
||||
|
||||
/// <summary>
|
||||
/// 添加或更新一条Hash值
|
||||
/// </summary>
|
||||
void HashAddOrUpdate<T>(string key, string hashKey, T value);
|
||||
|
||||
/// <summary>
|
||||
/// 获取多条Hash值
|
||||
/// </summary>
|
||||
List<T> HashGet<T>(string key, params string[] fields);
|
||||
|
||||
/// <summary>
|
||||
/// 获取一条Hash值
|
||||
/// </summary>
|
||||
T HashGetOne<T>(string key, string field);
|
||||
|
||||
/// <summary>
|
||||
/// 根据KEY获取所有Hash值
|
||||
/// </summary>
|
||||
IDictionary<string, T> HashGetAll<T>(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 删除Hash值
|
||||
/// </summary>
|
||||
int HashDel<T>(string key, params string[] fields);
|
||||
}
|
||||
}
|
||||
437
yy-admin-master/YY.Admin.Core/Cache/SysCacheService.cs
Normal file
437
yy-admin-master/YY.Admin.Core/Cache/SysCacheService.cs
Normal file
@@ -0,0 +1,437 @@
|
||||
|
||||
using NewLife.Caching;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Web;
|
||||
using YY.Admin.Core.Option;
|
||||
|
||||
namespace YY.Admin.Core
|
||||
{
|
||||
public class SysCacheService : ISysCacheService
|
||||
{
|
||||
private readonly ICacheProvider _cacheProvider;
|
||||
private readonly BaseCacheOptions _cacheOptions;
|
||||
|
||||
public SysCacheService(ICacheProvider provider, BaseCacheOptions cacheOptions)
|
||||
{
|
||||
_cacheProvider = provider;
|
||||
_cacheOptions = cacheOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 申请分布式锁 🔖
|
||||
/// </summary>
|
||||
/// <param name="key">要锁定的key</param>
|
||||
/// <param name="msTimeout">申请锁等待的时间,单位毫秒</param>
|
||||
/// <param name="msExpire">锁过期时间,超过该时间没有主动是放则自动是放,必须整数秒,单位毫秒</param>
|
||||
/// <param name="throwOnFailure">失败时是否抛出异常,如不抛出异常,可通过判断返回null得知申请锁失败</param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("申请分布式锁")]
|
||||
public IDisposable? BeginCacheLock(string key, int msTimeout = 500, int msExpire = 10000, bool throwOnFailure = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _cacheProvider.Cache.AcquireLock(key, msTimeout, msExpire, throwOnFailure);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存键名集合 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取缓存键名集合")]
|
||||
public List<string> GetKeyList()
|
||||
{
|
||||
return _cacheProvider.Cache == Cache.Default
|
||||
? [.. _cacheProvider.Cache.Keys.Where(u => u.StartsWith(_cacheOptions.Prefix)).Select(u => u[_cacheOptions.Prefix.Length..]).OrderBy(u => u)]
|
||||
: [.. ((FullRedis)_cacheProvider.Cache).Search($"{_cacheOptions.Prefix}*", int.MaxValue).Select(u => u[_cacheOptions.Prefix.Length..]).OrderBy(u => u)];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加缓存
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
|
||||
public bool Set(string key, object value)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(key) && _cacheProvider.Cache.Set($"{_cacheOptions.Prefix}{key}", value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增加缓存并设置过期时间
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="expire"></param>
|
||||
/// <returns></returns>
|
||||
|
||||
public bool Set(string key, object value, TimeSpan expire)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(key) && _cacheProvider.Cache.Set($"{_cacheOptions.Prefix}{key}", value, expire);
|
||||
}
|
||||
|
||||
public async Task<TR> AdGetAsync<TR>(String cacheName, Func<Task<TR>> del, TimeSpan? expiry = default) where TR : class
|
||||
{
|
||||
return await AdGetAsync<TR>(cacheName, del, [], expiry);
|
||||
}
|
||||
|
||||
public async Task<TR> AdGetAsync<TR, T1>(String cacheName, Func<T1, Task<TR>> del, T1 t1, TimeSpan? expiry = default) where TR : class
|
||||
{
|
||||
return await AdGetAsync<TR>(cacheName, del, [t1], expiry);
|
||||
}
|
||||
|
||||
public async Task<TR> AdGetAsync<TR, T1, T2>(String cacheName, Func<T1, T2, Task<TR>> del, T1 t1, T2 t2, TimeSpan? expiry = default) where TR : class
|
||||
{
|
||||
return await AdGetAsync<TR>(cacheName, del, [t1, t2], expiry);
|
||||
}
|
||||
|
||||
public async Task<TR> AdGetAsync<TR, T1, T2, T3>(String cacheName, Func<T1, T2, T3, Task<TR>> del, T1 t1, T2 t2, T3 t3, TimeSpan? expiry = default) where TR : class
|
||||
{
|
||||
return await AdGetAsync<TR>(cacheName, del, [t1, t2, t3], expiry);
|
||||
}
|
||||
|
||||
private async Task<T> AdGetAsync<T>(string cacheName, Delegate del, Object[] obs, TimeSpan? expiry) where T : class
|
||||
{
|
||||
var key = Key(cacheName, obs);
|
||||
// 使用分布式锁
|
||||
using (_cacheProvider.Cache.AcquireLock($@"lock:AdGetAsync:{cacheName}", 1000))
|
||||
{
|
||||
var value = Get<T>(key);
|
||||
value ??= await ((dynamic)del).DynamicInvokeAsync(obs);
|
||||
Set(key, value);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
public T Get<T>(String cacheName, object t1)
|
||||
{
|
||||
return Get<T>(cacheName, [t1]);
|
||||
}
|
||||
|
||||
public T Get<T>(String cacheName, object t1, object t2)
|
||||
{
|
||||
return Get<T>(cacheName, [t1, t2]);
|
||||
}
|
||||
|
||||
public T Get<T>(String cacheName, object t1, object t2, object t3)
|
||||
{
|
||||
return Get<T>(cacheName, [t1, t2, t3]);
|
||||
}
|
||||
|
||||
private T Get<T>(String cacheName, Object[] obs)
|
||||
{
|
||||
var key = cacheName + ":" + obs.Aggregate(string.Empty, (current, o) => current + $"<{o}>");
|
||||
return Get<T>(key);
|
||||
}
|
||||
|
||||
private static string Key(string cacheName, object[] obs)
|
||||
{
|
||||
if (obs.OfType<TimeSpan>().Any()) throw new Exception("缓存参数类型不能能是:TimeSpan类型");
|
||||
StringBuilder sb = new(cacheName + ":");
|
||||
foreach (var a in obs) sb.Append($"<{KeySingle(a)}>");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string KeySingle(object t)
|
||||
{
|
||||
return t.GetType().IsClass && !t.GetType().IsPrimitive ? JsonConvert.SerializeObject(t) : t.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存的剩余生存时间
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
|
||||
public TimeSpan GetExpire(string key)
|
||||
{
|
||||
return _cacheProvider.Cache.GetExpire(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
|
||||
public T Get<T>(string key)
|
||||
{
|
||||
return _cacheProvider.Cache.Get<T>($"{_cacheOptions.Prefix}{key}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除缓存 🔖
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("删除缓存")]
|
||||
public int Remove(string key)
|
||||
{
|
||||
return _cacheProvider.Cache.Remove($"{_cacheOptions.Prefix}{key}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有缓存 🔖
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[DisplayName("清空所有缓存")]
|
||||
public void Clear()
|
||||
{
|
||||
_cacheProvider.Cache.Clear();
|
||||
|
||||
Cache.Default.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查缓存是否存在
|
||||
/// </summary>
|
||||
/// <param name="key">键</param>
|
||||
/// <returns></returns>
|
||||
|
||||
public bool ExistKey(string key)
|
||||
{
|
||||
return _cacheProvider.Cache.ContainsKey($"{_cacheOptions.Prefix}{key}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据键名前缀删除缓存 🔖
|
||||
/// </summary>
|
||||
/// <param name="prefixKey">键名前缀</param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("根据键名前缀删除缓存")]
|
||||
public int RemoveByPrefixKey(string prefixKey)
|
||||
{
|
||||
var delKeys = _cacheProvider.Cache == Cache.Default
|
||||
? _cacheProvider.Cache.Keys.Where(u => u.StartsWith($"{_cacheOptions.Prefix}{prefixKey}")).ToArray()
|
||||
: ((FullRedis)_cacheProvider.Cache).Search($"{_cacheOptions.Prefix}{prefixKey}*", int.MaxValue).ToArray();
|
||||
return _cacheProvider.Cache.Remove(delKeys);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据键名前缀获取键名集合 🔖
|
||||
/// </summary>
|
||||
/// <param name="prefixKey">键名前缀</param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("根据键名前缀获取键名集合")]
|
||||
public List<string> GetKeysByPrefixKey(string prefixKey)
|
||||
{
|
||||
return _cacheProvider.Cache == Cache.Default
|
||||
? _cacheProvider.Cache.Keys.Where(u => u.StartsWith($"{_cacheOptions.Prefix}{prefixKey}")).Select(u => u[_cacheOptions.Prefix.Length..]).ToList()
|
||||
: ((FullRedis)_cacheProvider.Cache).Search($"{_cacheOptions.Prefix}{prefixKey}*", int.MaxValue).Select(u => u[_cacheOptions.Prefix.Length..]).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓存值 🔖
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
[DisplayName("获取缓存值")]
|
||||
public object GetValue(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key)) return null;
|
||||
|
||||
if (Regex.IsMatch(key, @"%[0-9a-fA-F]{2}"))
|
||||
key = HttpUtility.UrlDecode(key);
|
||||
|
||||
var fullKey = $"{_cacheOptions.Prefix}{key}";
|
||||
|
||||
if (_cacheProvider.Cache == Cache.Default)
|
||||
return _cacheProvider.Cache.Get<object>(fullKey);
|
||||
|
||||
if (_cacheProvider.Cache is FullRedis redisCache)
|
||||
{
|
||||
if (!redisCache.ContainsKey(fullKey))
|
||||
return null;
|
||||
try
|
||||
{
|
||||
var keyType = redisCache.TYPE(fullKey)?.ToLower();
|
||||
switch (keyType)
|
||||
{
|
||||
case "string":
|
||||
return redisCache.Get<string>(fullKey);
|
||||
|
||||
case "list":
|
||||
var list = redisCache.GetList<string>(fullKey);
|
||||
return list?.ToList();
|
||||
|
||||
case "hash":
|
||||
var hash = redisCache.GetDictionary<string>(fullKey);
|
||||
return hash?.ToDictionary(k => k.Key, v => v.Value);
|
||||
|
||||
case "set":
|
||||
var set = redisCache.GetSet<string>(fullKey);
|
||||
return set?.ToArray();
|
||||
|
||||
case "zset":
|
||||
var sortedSet = redisCache.GetSortedSet<string>(fullKey);
|
||||
return sortedSet?.Range(0, -1)?.ToList();
|
||||
|
||||
case "none":
|
||||
return null;
|
||||
|
||||
default:
|
||||
// 未知类型或特殊类型
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
{ "key", key },
|
||||
{ "type", keyType ?? "unknown" },
|
||||
{ "message", "无法使用标准方式获取此类型数据" }
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new Dictionary<string, object>
|
||||
{
|
||||
{ "key", key },
|
||||
{ "error", ex.Message },
|
||||
{ "type", "exception" }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return _cacheProvider.Cache.Get<object>(fullKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取或添加缓存(在数据不存在时执行委托请求数据)
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="callback"></param>
|
||||
/// <param name="expire">过期时间,单位秒</param>
|
||||
/// <returns></returns>
|
||||
|
||||
public T GetOrAdd<T>(string key, Func<string, T> callback, int expire = -1)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key)) return default;
|
||||
return _cacheProvider.Cache.GetOrAdd($"{_cacheOptions.Prefix}{key}", callback, expire);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hash匹配
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
|
||||
public IDictionary<String, T> GetHashMap<T>(string key)
|
||||
{
|
||||
return _cacheProvider.Cache.GetDictionary<T>(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量添加HASH
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="dic"></param>
|
||||
/// <returns></returns>
|
||||
|
||||
public bool HashSet<T>(string key, Dictionary<string, T> dic)
|
||||
{
|
||||
var hash = GetHashMap<T>(key);
|
||||
foreach (var v in dic)
|
||||
{
|
||||
hash.Add(v);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加一条HASH
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="hashKey"></param>
|
||||
/// <param name="value"></param>
|
||||
|
||||
public void HashAdd<T>(string key, string hashKey, T value)
|
||||
{
|
||||
var hash = GetHashMap<T>(key);
|
||||
hash.Add(hashKey, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加或更新一条HASH
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="hashKey"></param>
|
||||
/// <param name="value"></param>
|
||||
|
||||
public void HashAddOrUpdate<T>(string key, string hashKey, T value)
|
||||
{
|
||||
var hash = GetHashMap<T>(key);
|
||||
if (hash.ContainsKey(hashKey))
|
||||
hash[hashKey] = value;
|
||||
else
|
||||
hash.Add(hashKey, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取多条HASH
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="fields"></param>
|
||||
/// <returns></returns>
|
||||
|
||||
public List<T> HashGet<T>(string key, params string[] fields)
|
||||
{
|
||||
var hash = GetHashMap<T>(key);
|
||||
return hash.Where(t => fields.Any(c => t.Key == c)).Select(t => t.Value).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取一条HASH
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="field"></param>
|
||||
/// <returns></returns>
|
||||
|
||||
public T HashGetOne<T>(string key, string field)
|
||||
{
|
||||
var hash = GetHashMap<T>(key);
|
||||
return hash.TryGetValue(field, out T value) ? value : default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据KEY获取所有HASH
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
|
||||
public IDictionary<string, T> HashGetAll<T>(string key)
|
||||
{
|
||||
var hash = GetHashMap<T>(key);
|
||||
return hash;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除HASH
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="fields"></param>
|
||||
/// <returns></returns>
|
||||
|
||||
public int HashDel<T>(string key, params string[] fields)
|
||||
{
|
||||
var hash = GetHashMap<T>(key);
|
||||
fields.ToList().ForEach(t => hash.Remove(t));
|
||||
return fields.Length;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
124
yy-admin-master/YY.Admin.Core/Const/CacheConst.cs
Normal file
124
yy-admin-master/YY.Admin.Core/Const/CacheConst.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
|
||||
|
||||
namespace YY.Admin.Core.Const;
|
||||
|
||||
/// <summary>
|
||||
/// 缓存相关常量
|
||||
/// </summary>
|
||||
public class CacheConst
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户权限缓存(按钮集合)
|
||||
/// </summary>
|
||||
public const string KeyUserButton = "sys_user_button:";
|
||||
|
||||
/// <summary>
|
||||
/// 用户机构缓存
|
||||
/// </summary>
|
||||
public const string KeyUserOrg = "sys_user_org:";
|
||||
|
||||
/// <summary>
|
||||
/// 角色最大数据范围缓存
|
||||
/// </summary>
|
||||
public const string KeyRoleMaxDataScope = "sys_role_maxDataScope:";
|
||||
|
||||
/// <summary>
|
||||
/// 在线用户缓存
|
||||
/// </summary>
|
||||
public const string KeyUserOnline = "sys_user_online:";
|
||||
|
||||
/// <summary>
|
||||
/// 图形验证码缓存
|
||||
/// </summary>
|
||||
public const string KeyVerCode = "sys_verCode:";
|
||||
|
||||
/// <summary>
|
||||
/// 手机验证码缓存
|
||||
/// </summary>
|
||||
public const string KeyPhoneVerCode = "sys_phoneVerCode:";
|
||||
|
||||
/// <summary>
|
||||
/// 密码错误次数缓存
|
||||
/// </summary>
|
||||
public const string KeyPasswordErrorTimes = "sys_password_error_times:";
|
||||
|
||||
/// <summary>
|
||||
/// 租户缓存
|
||||
/// </summary>
|
||||
public const string KeyTenant = "sys_tenant";
|
||||
|
||||
/// <summary>
|
||||
/// 常量下拉框
|
||||
/// </summary>
|
||||
public const string KeyConst = "sys_const:";
|
||||
|
||||
/// <summary>
|
||||
/// 所有缓存关键字集合
|
||||
/// </summary>
|
||||
public const string KeyAll = "sys_keys";
|
||||
|
||||
/// <summary>
|
||||
/// SqlSugar二级缓存
|
||||
/// </summary>
|
||||
public const string SqlSugar = "sys_sqlSugar:";
|
||||
|
||||
/// <summary>
|
||||
/// 开放接口身份缓存
|
||||
/// </summary>
|
||||
public const string KeyOpenAccess = "sys_open_access:";
|
||||
|
||||
/// <summary>
|
||||
/// 开放接口身份随机数缓存
|
||||
/// </summary>
|
||||
public const string KeyOpenAccessNonce = "sys_open_access_nonce:";
|
||||
|
||||
/// <summary>
|
||||
/// 登录黑名单
|
||||
/// </summary>
|
||||
public const string KeyBlacklist = "sys_blacklist:";
|
||||
|
||||
/// <summary>
|
||||
/// 系统配置缓存
|
||||
/// </summary>
|
||||
public const string KeyConfig = "sys_config:";
|
||||
|
||||
/// <summary>
|
||||
/// 系统租户配置缓存
|
||||
/// </summary>
|
||||
public const string KeyTenantConfig = "sys_tenant_config:";
|
||||
|
||||
/// <summary>
|
||||
/// 系统用户配置缓存
|
||||
/// </summary>
|
||||
public const string KeyUserConfig = "sys_user_config:";
|
||||
|
||||
/// <summary>
|
||||
/// 系统字典缓存
|
||||
/// </summary>
|
||||
public const string KeyDict = "sys_dict:";
|
||||
|
||||
/// <summary>
|
||||
/// 系统租户字典缓存
|
||||
/// </summary>
|
||||
public const string KeyTenantDict = "sys_tenant_dict:";
|
||||
|
||||
/// <summary>
|
||||
/// 重复请求(幂等)字典缓存
|
||||
/// </summary>
|
||||
public const string KeyIdempotent = "sys_idempotent:";
|
||||
|
||||
/// <summary>
|
||||
/// Excel临时文件缓存
|
||||
/// </summary>
|
||||
public const string KeyExcelTemp = "sys_excel_temp:";
|
||||
|
||||
/// <summary>
|
||||
/// 系统更新命令日志缓存
|
||||
/// </summary>
|
||||
public const string KeySysUpdateLog = "sys_update_log";
|
||||
|
||||
/// <summary>
|
||||
/// 系统更新间隔标记缓存
|
||||
/// </summary>
|
||||
public const string KeySysUpdateInterval = "sys_update_interval";
|
||||
}
|
||||
64
yy-admin-master/YY.Admin.Core/Const/ClaimConst.cs
Normal file
64
yy-admin-master/YY.Admin.Core/Const/ClaimConst.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
|
||||
|
||||
namespace YY.Admin.Core.Const;
|
||||
|
||||
/// <summary>
|
||||
/// Claim相关常量
|
||||
/// </summary>
|
||||
public class ClaimConst
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户Id
|
||||
/// </summary>
|
||||
public const string UserId = "UserId";
|
||||
|
||||
/// <summary>
|
||||
/// 账号
|
||||
/// </summary>
|
||||
public const string Account = "Account";
|
||||
|
||||
/// <summary>
|
||||
/// 真实姓名
|
||||
/// </summary>
|
||||
public const string RealName = "RealName";
|
||||
|
||||
/// <summary>
|
||||
/// 昵称
|
||||
/// </summary>
|
||||
public const string NickName = "NickName";
|
||||
|
||||
/// <summary>
|
||||
/// 账号类型
|
||||
/// </summary>
|
||||
public const string AccountType = "AccountType";
|
||||
|
||||
/// <summary>
|
||||
/// 租户Id
|
||||
/// </summary>
|
||||
public const string TenantId = "TenantId";
|
||||
|
||||
/// <summary>
|
||||
/// 组织机构Id
|
||||
/// </summary>
|
||||
public const string OrgId = "OrgId";
|
||||
|
||||
/// <summary>
|
||||
/// 组织机构名称
|
||||
/// </summary>
|
||||
public const string OrgName = "OrgName";
|
||||
|
||||
/// <summary>
|
||||
/// 组织机构类型
|
||||
/// </summary>
|
||||
public const string OrgType = "OrgType";
|
||||
|
||||
/// <summary>
|
||||
/// 微信OpenId
|
||||
/// </summary>
|
||||
public const string OpenId = "OpenId";
|
||||
|
||||
/// <summary>
|
||||
/// 登录模式PC、APP
|
||||
/// </summary>
|
||||
public const string LoginMode = "LoginMode";
|
||||
}
|
||||
53
yy-admin-master/YY.Admin.Core/Const/CommonConst.cs
Normal file
53
yy-admin-master/YY.Admin.Core/Const/CommonConst.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
namespace YY.Admin.Core.Const;
|
||||
|
||||
/// <summary>
|
||||
/// 通用常量
|
||||
/// </summary>
|
||||
[Const("平台配置")]
|
||||
public class CommonConst
|
||||
{
|
||||
/// <summary>
|
||||
/// 日志分组名称
|
||||
/// </summary>
|
||||
public const string SysLogCategoryName = "System.Logging.LoggingMonitor";
|
||||
|
||||
/// <summary>
|
||||
/// 事件-增加异常日志
|
||||
/// </summary>
|
||||
public const string AddExLog = "Add:ExLog";
|
||||
|
||||
/// <summary>
|
||||
/// 事件-发送异常邮件
|
||||
/// </summary>
|
||||
public const string SendErrorMail = "Send:ErrorMail";
|
||||
|
||||
/// <summary>
|
||||
/// 默认基本角色名称
|
||||
/// </summary>
|
||||
public const string DefaultBaseRoleName = "默认基本角色";
|
||||
|
||||
/// <summary>
|
||||
/// 默认基本角色编码
|
||||
/// </summary>
|
||||
public const string DefaultBaseRoleCode = "default_base_role";
|
||||
|
||||
/// <summary>
|
||||
/// MainWinndow 主内容区域名称
|
||||
/// </summary>
|
||||
public const string ContentRegion = "ContentRegion";
|
||||
|
||||
/// <summary>
|
||||
/// MainWinndow 菜单区域名称
|
||||
/// </summary>
|
||||
public const string MenuRegion = "MenuRegion";
|
||||
|
||||
/// <summary>
|
||||
/// 系统名称
|
||||
/// </summary>
|
||||
public const string SystemName = "智能制造MES工控";
|
||||
|
||||
/// <summary>
|
||||
/// 系统设置文件路径
|
||||
/// </summary>
|
||||
public const string AppSettingsFilePath = "AppSettings\\{0}\\appsettings.json";
|
||||
}
|
||||
144
yy-admin-master/YY.Admin.Core/Const/ConfigConst.cs
Normal file
144
yy-admin-master/YY.Admin.Core/Const/ConfigConst.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
|
||||
|
||||
namespace YY.Admin.Core.Const;
|
||||
|
||||
/// <summary>
|
||||
/// 配置常量
|
||||
/// </summary>
|
||||
public class ConfigConst
|
||||
{
|
||||
/// <summary>
|
||||
/// 演示环境
|
||||
/// </summary>
|
||||
public const string SysDemoEnv = "sys_demo";
|
||||
|
||||
/// <summary>
|
||||
/// 默认密码
|
||||
/// </summary>
|
||||
public const string SysPassword = "sys_password";
|
||||
|
||||
/// <summary>
|
||||
/// 密码最大错误次数
|
||||
/// </summary>
|
||||
public const string SysPasswordMaxErrorTimes = "sys_password_max_error_times";
|
||||
|
||||
/// <summary>
|
||||
/// 日志保留天数
|
||||
/// </summary>
|
||||
public const string SysLogRetentionDays = "sys_log_retention_days";
|
||||
|
||||
/// <summary>
|
||||
/// 记录操作日志
|
||||
/// </summary>
|
||||
public const string SysOpLog = "sys_oplog";
|
||||
|
||||
/// <summary>
|
||||
/// 单设备登录
|
||||
/// </summary>
|
||||
public const string SysSingleLogin = "sys_single_login";
|
||||
|
||||
/// <summary>
|
||||
/// 登入登出提醒
|
||||
/// </summary>
|
||||
public const string SysLoginOutReminder = "sys_login_out_reminder";
|
||||
|
||||
/// <summary>
|
||||
/// 登陆时隐藏租户
|
||||
/// </summary>
|
||||
public const string SysHideTenantLogin = "sys_hide_tenant_login";
|
||||
|
||||
/// <summary>
|
||||
/// 登录二次验证
|
||||
/// </summary>
|
||||
public const string SysSecondVer = "sys_second_ver";
|
||||
|
||||
/// <summary>
|
||||
/// 图形验证码
|
||||
/// </summary>
|
||||
public const string SysCaptcha = "sys_captcha";
|
||||
|
||||
/// <summary>
|
||||
/// Token过期时间
|
||||
/// </summary>
|
||||
public const string SysTokenExpire = "sys_token_expire";
|
||||
|
||||
/// <summary>
|
||||
/// RefreshToken过期时间
|
||||
/// </summary>
|
||||
public const string SysRefreshTokenExpire = "sys_refresh_token_expire";
|
||||
|
||||
/// <summary>
|
||||
/// 发送异常日志邮件
|
||||
/// </summary>
|
||||
public const string SysErrorMail = "sys_error_mail";
|
||||
|
||||
/// <summary>
|
||||
/// 域登录验证
|
||||
/// </summary>
|
||||
public const string SysDomainLogin = "sys_domain_login";
|
||||
|
||||
// /// <summary>
|
||||
// /// 租户域名隔离登录验证
|
||||
// /// </summary>
|
||||
// public const string SysTenantHostLogin = "sys_tenant_host_login";
|
||||
|
||||
/// <summary>
|
||||
/// 数据校验日志
|
||||
/// </summary>
|
||||
public const string SysValidationLog = "sys_validation_log";
|
||||
|
||||
/// <summary>
|
||||
/// 行政区域同步层级 1-省级,2-市级,3-区县级,4-街道级,5-村级
|
||||
/// </summary>
|
||||
public const string SysRegionSyncLevel = "sys_region_sync_level";
|
||||
|
||||
/// <summary>
|
||||
/// Default 分组
|
||||
/// </summary>
|
||||
public const string SysDefaultGroup = "Default";
|
||||
|
||||
/// <summary>
|
||||
/// 支付宝授权页面地址
|
||||
/// </summary>
|
||||
public const string AlipayAuthPageUrl = "alipay_auth_page_url_";
|
||||
|
||||
// /// <summary>
|
||||
// /// 系统图标
|
||||
// /// </summary>
|
||||
// public const string SysWebLogo = "sys_web_logo";
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 系统主标题
|
||||
// /// </summary>
|
||||
// public const string SysWebTitle = "sys_web_title";
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 系统副标题
|
||||
// /// </summary>
|
||||
// public const string SysWebViceTitle = "sys_web_viceTitle";
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 系统描述
|
||||
// /// </summary>
|
||||
// public const string SysWebViceDesc = "sys_web_viceDesc";
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 水印内容
|
||||
// /// </summary>
|
||||
// public const string SysWebWatermark = "sys_web_watermark";
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 版权说明
|
||||
// /// </summary>
|
||||
// public const string SysWebCopyright = "sys_web_copyright";
|
||||
//
|
||||
// /// <summary>
|
||||
// /// ICP备案号
|
||||
// /// </summary>
|
||||
// public const string SysWebIcp = "sys_web_icp";
|
||||
//
|
||||
// /// <summary>
|
||||
// /// ICP地址
|
||||
// /// </summary>
|
||||
// public const string SysWebIcpUrl = "sys_web_icpUrl";
|
||||
}
|
||||
31
yy-admin-master/YY.Admin.Core/Const/SqlSugarConst.cs
Normal file
31
yy-admin-master/YY.Admin.Core/Const/SqlSugarConst.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace YY.Admin.Core.Const
|
||||
{
|
||||
public class SqlSugarConst
|
||||
{
|
||||
/// <summary>
|
||||
/// 默认主数据库标识(默认租户)
|
||||
/// </summary>
|
||||
public const string MainConfigId = "1300000000001";
|
||||
|
||||
/// <summary>
|
||||
/// 默认日志数据库标识
|
||||
/// </summary>
|
||||
public const string LogConfigId = "1300000000002";
|
||||
|
||||
/// <summary>
|
||||
/// 默认表主键
|
||||
/// </summary>
|
||||
public const string PrimaryKey = "Id";
|
||||
|
||||
/// <summary>
|
||||
/// 默认租户Id
|
||||
/// </summary>
|
||||
public const long DefaultTenantId = 1300000000001;
|
||||
}
|
||||
}
|
||||
300
yy-admin-master/YY.Admin.Core/Controls/FontAwesomeIcon.cs
Normal file
300
yy-admin-master/YY.Admin.Core/Controls/FontAwesomeIcon.cs
Normal file
@@ -0,0 +1,300 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Threading;
|
||||
using PropertyMetadata = System.Windows.PropertyMetadata;
|
||||
|
||||
namespace YY.Admin.Core.Controls
|
||||
{
|
||||
//图标官网
|
||||
public enum FontAwesomeStyle
|
||||
{
|
||||
/// <summary>
|
||||
/// 常规
|
||||
/// </summary>
|
||||
Regular,
|
||||
|
||||
/// <summary>
|
||||
/// 实心的
|
||||
/// </summary>
|
||||
Solid,
|
||||
|
||||
/// <summary>
|
||||
/// 品牌(Logo)
|
||||
/// </summary>
|
||||
Brands
|
||||
}
|
||||
|
||||
public class FontAwesomeIcon : TextBlock
|
||||
{
|
||||
|
||||
#region Icon 属性
|
||||
public static readonly DependencyProperty IconProperty =
|
||||
DependencyProperty.Register(nameof(Icon), typeof(string), typeof(FontAwesomeIcon),
|
||||
new PropertyMetadata(null, (d, e) => ((FontAwesomeIcon)d).Text = e.NewValue?.ToString()));
|
||||
|
||||
public string Icon
|
||||
{
|
||||
get => (string)GetValue(IconProperty);
|
||||
set => SetValue(IconProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Spin 属性
|
||||
public static readonly DependencyProperty SpinProperty =
|
||||
DependencyProperty.Register(nameof(Spin), typeof(bool), typeof(FontAwesomeIcon),
|
||||
new PropertyMetadata(false, (d, e) => ((FontAwesomeIcon)d).UpdateAnimation()));
|
||||
|
||||
public bool Spin
|
||||
{
|
||||
get => (bool)GetValue(SpinProperty);
|
||||
set => SetValue(SpinProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region SpinSpeed 属性
|
||||
public static readonly DependencyProperty SpinSpeedProperty =
|
||||
DependencyProperty.Register(nameof(SpinSpeed), typeof(double), typeof(FontAwesomeIcon),
|
||||
new PropertyMetadata(1.0, (d, e) =>
|
||||
{
|
||||
var fa = (FontAwesomeIcon)d;
|
||||
if (fa.Spin)
|
||||
fa.StartSpin();
|
||||
}));
|
||||
|
||||
public double SpinSpeed
|
||||
{
|
||||
get => (double)GetValue(SpinSpeedProperty);
|
||||
set => SetValue(SpinSpeedProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Beat 属性
|
||||
public static readonly DependencyProperty BeatProperty =
|
||||
DependencyProperty.Register(nameof(Beat), typeof(bool), typeof(FontAwesomeIcon),
|
||||
new PropertyMetadata(false, (d, e) => ((FontAwesomeIcon)d).UpdateAnimation()));
|
||||
|
||||
public bool Beat
|
||||
{
|
||||
get => (bool)GetValue(BeatProperty);
|
||||
set => SetValue(BeatProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region BeatScale 属性
|
||||
public static readonly DependencyProperty BeatScaleProperty =
|
||||
DependencyProperty.Register(nameof(BeatScale), typeof(double), typeof(FontAwesomeIcon),
|
||||
new PropertyMetadata(1.3));
|
||||
|
||||
public double BeatScale
|
||||
{
|
||||
get => (double)GetValue(BeatScaleProperty);
|
||||
set => SetValue(BeatScaleProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region BeatDuration 属性
|
||||
public static readonly DependencyProperty BeatDurationProperty =
|
||||
DependencyProperty.Register(nameof(BeatDuration), typeof(double), typeof(FontAwesomeIcon),
|
||||
new PropertyMetadata(0.5));
|
||||
|
||||
public double BeatDuration
|
||||
{
|
||||
get => (double)GetValue(BeatDurationProperty);
|
||||
set => SetValue(BeatDurationProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region IconFamily 属性
|
||||
public static readonly DependencyProperty IconFamilyProperty =
|
||||
DependencyProperty.Register(nameof(IconFamily), typeof(FontAwesomeStyle), typeof(FontAwesomeIcon),
|
||||
new PropertyMetadata(FontAwesomeStyle.Regular, OnIconFamilyChanged));
|
||||
|
||||
public FontAwesomeStyle IconFamily
|
||||
{
|
||||
get => (FontAwesomeStyle)GetValue(IconFamilyProperty);
|
||||
set => SetValue(IconFamilyProperty, value);
|
||||
}
|
||||
|
||||
private static void OnIconFamilyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var fa = (FontAwesomeIcon)d;
|
||||
fa.UpdateFontFamily();
|
||||
}
|
||||
|
||||
private void UpdateFontFamily()
|
||||
{
|
||||
FontFamily = IconFamily switch
|
||||
{
|
||||
FontAwesomeStyle.Regular => (FontFamily)Application.Current.Resources["FontAwesomeRegular"],
|
||||
FontAwesomeStyle.Solid => (FontFamily)Application.Current.Resources["FontAwesomeSolid"],
|
||||
FontAwesomeStyle.Brands => (FontFamily)Application.Current.Resources["FontAwesomeBrands"],
|
||||
_ => (FontFamily)Application.Current.Resources["FontAwesomeRegular"]
|
||||
};
|
||||
}
|
||||
#endregion
|
||||
|
||||
// Transform components
|
||||
private RotateTransform? _rotateTransform;
|
||||
private ScaleTransform? _scaleTransform;
|
||||
private TransformGroup? _transformGroup;
|
||||
|
||||
private bool _initialized = false;
|
||||
private DoubleAnimation? _currentSpinAnimation;
|
||||
|
||||
public FontAwesomeIcon()
|
||||
{
|
||||
// 设置文本对齐方式,确保内容居中
|
||||
TextAlignment = TextAlignment.Center;
|
||||
VerticalAlignment = VerticalAlignment.Center;
|
||||
HorizontalAlignment = HorizontalAlignment.Center;
|
||||
|
||||
// 设置默认字体家庭
|
||||
UpdateFontFamily();
|
||||
|
||||
// 创建 transforms
|
||||
_rotateTransform = new RotateTransform();
|
||||
_scaleTransform = new ScaleTransform(1, 1);
|
||||
_transformGroup = new TransformGroup();
|
||||
_transformGroup.Children.Add(_scaleTransform);
|
||||
_transformGroup.Children.Add(_rotateTransform);
|
||||
|
||||
// 使用 RenderTransform 避免布局抖动
|
||||
RenderTransform = _transformGroup;
|
||||
// 关键:设置变换原点为内容中心
|
||||
RenderTransformOrigin = new Point(0.5, 0.5);
|
||||
|
||||
// 延迟初始化到 Loaded
|
||||
Loaded += OnLoadedSafe;
|
||||
Unloaded += OnUnloadedSafe;
|
||||
SizeChanged += OnSizeChangedSafe;
|
||||
}
|
||||
|
||||
private void OnLoadedSafe(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
Dispatcher.BeginInvoke((Action)(() =>
|
||||
{
|
||||
if (_initialized) return;
|
||||
_initialized = true;
|
||||
|
||||
UpdateAnimation();
|
||||
}), DispatcherPriority.Loaded);
|
||||
}
|
||||
|
||||
private void OnSizeChangedSafe(object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
// 确保变换原点始终居中
|
||||
RenderTransformOrigin = new Point(0.5, 0.5);
|
||||
}
|
||||
|
||||
private void OnUnloadedSafe(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
// 停止动画,避免内存泄漏
|
||||
StopSpin();
|
||||
StopBeat();
|
||||
}
|
||||
|
||||
private void UpdateAnimation()
|
||||
{
|
||||
if (DesignerProperties.GetIsInDesignMode(this)) return;
|
||||
|
||||
if (!_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Spin) StartSpin(); else StopSpin();
|
||||
if (Beat) StartBeat(); else StopBeat();
|
||||
}
|
||||
|
||||
private void StartSpin()
|
||||
{
|
||||
if (_rotateTransform == null) return;
|
||||
|
||||
// 重新确认 RenderTransform 已正确赋值
|
||||
if (RenderTransform != _transformGroup)
|
||||
{
|
||||
RenderTransform = _transformGroup;
|
||||
RenderTransformOrigin = new Point(0.5, 0.5);
|
||||
}
|
||||
|
||||
// 先取消旧动画
|
||||
StopSpin();
|
||||
|
||||
// 方法1:使用 By 动画实现无缝旋转(推荐)
|
||||
var anim = new DoubleAnimation
|
||||
{
|
||||
By = 360, // 每次增加360度
|
||||
Duration = TimeSpan.FromSeconds(Math.Max(0.01, SpinSpeed)),
|
||||
RepeatBehavior = RepeatBehavior.Forever
|
||||
};
|
||||
|
||||
// 动画在 WPF 内部直接使用 Freezable 缓存,提高性能
|
||||
anim.Freeze();
|
||||
|
||||
_currentSpinAnimation = anim;
|
||||
_rotateTransform.BeginAnimation(RotateTransform.AngleProperty, anim);
|
||||
|
||||
// 方法2:使用 IsCumulative 属性(备选方案)
|
||||
/*
|
||||
var anim = new DoubleAnimation(0, 360, new Duration(TimeSpan.FromSeconds(Math.Max(0.01, SpinSpeed))))
|
||||
{
|
||||
RepeatBehavior = RepeatBehavior.Forever,
|
||||
IsCumulative = true // 累积值,避免跳回0度
|
||||
};
|
||||
*/
|
||||
}
|
||||
|
||||
private void StopSpin()
|
||||
{
|
||||
_rotateTransform?.BeginAnimation(RotateTransform.AngleProperty, null);
|
||||
_currentSpinAnimation = null;
|
||||
|
||||
// 重置旋转角度
|
||||
if (_rotateTransform != null)
|
||||
_rotateTransform.Angle = 0;
|
||||
}
|
||||
|
||||
private void StartBeat()
|
||||
{
|
||||
if (_scaleTransform == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 重新确认 RenderTransform 已正确赋值
|
||||
if (RenderTransform != _transformGroup)
|
||||
{
|
||||
RenderTransform = _transformGroup;
|
||||
RenderTransformOrigin = new Point(0.5, 0.5);
|
||||
}
|
||||
|
||||
// 先停止已有缩放动画
|
||||
_scaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, null);
|
||||
_scaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, null);
|
||||
|
||||
// 使用更平滑的缓动函数
|
||||
var anim = new DoubleAnimation(1.0, BeatScale, new Duration(TimeSpan.FromSeconds(Math.Max(0.01, BeatDuration))))
|
||||
{
|
||||
AutoReverse = true,
|
||||
RepeatBehavior = RepeatBehavior.Forever,
|
||||
EasingFunction = new QuadraticEase() { EasingMode = EasingMode.EaseInOut }
|
||||
};
|
||||
|
||||
_scaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, anim);
|
||||
_scaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, anim);
|
||||
}
|
||||
|
||||
private void StopBeat()
|
||||
{
|
||||
if (_scaleTransform != null)
|
||||
{
|
||||
_scaleTransform.BeginAnimation(ScaleTransform.ScaleXProperty, null);
|
||||
_scaleTransform.BeginAnimation(ScaleTransform.ScaleYProperty, null);
|
||||
_scaleTransform.ScaleX = 1.0;
|
||||
_scaleTransform.ScaleY = 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
255
yy-admin-master/YY.Admin.Core/Controls/GridPanel.cs
Normal file
255
yy-admin-master/YY.Admin.Core/Controls/GridPanel.cs
Normal file
@@ -0,0 +1,255 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace YY.Admin.Core.Controls;
|
||||
|
||||
public class GridPanel : Panel
|
||||
{
|
||||
// ------------------------------------------------------------
|
||||
// 间距属性(行列间距)
|
||||
// ------------------------------------------------------------
|
||||
public double Gap
|
||||
{
|
||||
get => (double)GetValue(GapProperty);
|
||||
set => SetValue(GapProperty, value);
|
||||
}
|
||||
public static readonly DependencyProperty GapProperty =
|
||||
DependencyProperty.Register(nameof(Gap), typeof(double), typeof(GridPanel),
|
||||
new FrameworkPropertyMetadata(20.0, FrameworkPropertyMetadataOptions.AffectsMeasure));
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 横向/纵向布局(类似 CSS flow)
|
||||
// ------------------------------------------------------------
|
||||
public Orientation Orientation
|
||||
{
|
||||
get => (Orientation)GetValue(OrientationProperty);
|
||||
set => SetValue(OrientationProperty, value);
|
||||
}
|
||||
public static readonly DependencyProperty OrientationProperty =
|
||||
DependencyProperty.Register(nameof(Orientation), typeof(Orientation), typeof(GridPanel),
|
||||
new FrameworkPropertyMetadata(Orientation.Horizontal, FrameworkPropertyMetadataOptions.AffectsMeasure));
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 测量
|
||||
// ------------------------------------------------------------
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
return Orientation == Orientation.Horizontal
|
||||
? MeasureHorizontal(availableSize)
|
||||
: MeasureVertical(availableSize);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 布局
|
||||
// ------------------------------------------------------------
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
return Orientation == Orientation.Horizontal
|
||||
? ArrangeHorizontal(finalSize)
|
||||
: ArrangeVertical(finalSize);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 横向布局(类似 CSS grid-template-columns)
|
||||
// ------------------------------------------------------------
|
||||
private Size MeasureHorizontal(Size availableSize)
|
||||
{
|
||||
double panelWidth = double.IsInfinity(availableSize.Width)
|
||||
? MinWidth * 4 + Gap * 3
|
||||
: availableSize.Width;
|
||||
|
||||
// ---- 修复:不能用 MinWidth+Gap 来估算列数,应使用最小子项宽度 ----
|
||||
double minChildWidth = MinWidth <= 0 ? 1 : MinWidth;
|
||||
|
||||
// ---- 修复:列数 = 能放下多少个最小宽度 ----
|
||||
int columnCount = Math.Max(1, (int)Math.Floor((panelWidth + Gap) / (minChildWidth + Gap)));
|
||||
|
||||
int childCount = InternalChildren.Count;
|
||||
// ---- 修复2:如果列数超过子项数,裁到子项数(实现 auto-fit 行为) ----
|
||||
if (childCount > 0 && columnCount > childCount)
|
||||
{
|
||||
columnCount = childCount;
|
||||
}
|
||||
|
||||
double columnWidth = (panelWidth - Gap * (columnCount - 1)) / columnCount;
|
||||
|
||||
double totalHeight = 0;
|
||||
double rowHeight = 0;
|
||||
int col = 0;
|
||||
|
||||
for (int i = 0; i < childCount; i++)
|
||||
{
|
||||
UIElement child = InternalChildren[i];
|
||||
child.Measure(new Size(columnWidth, double.PositiveInfinity));
|
||||
rowHeight = Math.Max(rowHeight, child.DesiredSize.Height);
|
||||
col++;
|
||||
|
||||
if (col >= columnCount || i == childCount - 1)
|
||||
{
|
||||
// ------------------------------------------------------------
|
||||
// 最后一行不加Gap
|
||||
// ------------------------------------------------------------
|
||||
totalHeight += rowHeight;
|
||||
if (i != childCount - 1) totalHeight += Gap;
|
||||
|
||||
col = 0;
|
||||
rowHeight = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return new Size(panelWidth, totalHeight);
|
||||
}
|
||||
|
||||
private Size ArrangeHorizontal(Size finalSize)
|
||||
{
|
||||
double panelWidth = finalSize.Width;
|
||||
|
||||
// ---- 修复:与 Measure 保持一致 ----
|
||||
double minChildWidth = MinWidth <= 0 ? 1 : MinWidth;
|
||||
int columnCount = Math.Max(1, (int)Math.Floor((panelWidth + Gap) / (minChildWidth + Gap)));
|
||||
|
||||
int childCount = InternalChildren.Count;
|
||||
// ---- 修复2:裁到子项数以实现 auto-fit ----
|
||||
if (childCount > 0 && columnCount > childCount)
|
||||
{
|
||||
columnCount = childCount;
|
||||
}
|
||||
|
||||
double columnWidth = (panelWidth - Gap * (columnCount - 1)) / columnCount;
|
||||
|
||||
double x = 0;
|
||||
double y = 0;
|
||||
double rowHeight = 0;
|
||||
int col = 0;
|
||||
|
||||
for (int i = 0; i < childCount; i++)
|
||||
{
|
||||
UIElement child = InternalChildren[i];
|
||||
// 为了保证每列宽度一致,传入 columnWidth 并在 Arrange 时使用 columnWidth
|
||||
child.Arrange(new Rect(x, y, columnWidth, child.DesiredSize.Height));
|
||||
rowHeight = Math.Max(rowHeight, child.DesiredSize.Height);
|
||||
col++;
|
||||
|
||||
if (col >= columnCount || i == childCount - 1)
|
||||
{
|
||||
x = 0;
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 最后一行不加Gap
|
||||
// ------------------------------------------------------------
|
||||
if (i != childCount - 1) y += rowHeight + Gap;
|
||||
else y += rowHeight;
|
||||
|
||||
col = 0;
|
||||
rowHeight = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
x += columnWidth + Gap;
|
||||
}
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// 纵向布局(类似 CSS grid-template-rows)
|
||||
// ------------------------------------------------------------
|
||||
private Size MeasureVertical(Size availableSize)
|
||||
{
|
||||
double panelHeight = double.IsInfinity(availableSize.Height)
|
||||
? MinHeight * 4 + Gap * 3
|
||||
: availableSize.Height;
|
||||
|
||||
// ---- 修复:使用最小子项高度,不能用 MinHeight+Gap ----
|
||||
double minChildHeight = MinHeight <= 0 ? 1 : MinHeight;
|
||||
|
||||
// ---- 修复:行数 = 能放下多少个最小高度 ----
|
||||
int rowCount = Math.Max(1, (int)Math.Floor((panelHeight + Gap) / (minChildHeight + Gap)));
|
||||
|
||||
int childCount = InternalChildren.Count;
|
||||
// ---- 修复2:如果行数超过子项数,裁到子项数(auto-fit) ----
|
||||
if (childCount > 0 && rowCount > childCount)
|
||||
{
|
||||
rowCount = childCount;
|
||||
}
|
||||
|
||||
double rowHeight = (panelHeight - Gap * (rowCount - 1)) / rowCount;
|
||||
|
||||
double totalWidth = 0;
|
||||
double columnWidth = 0;
|
||||
int row = 0;
|
||||
|
||||
for (int i = 0; i < childCount; i++)
|
||||
{
|
||||
UIElement child = InternalChildren[i];
|
||||
child.Measure(new Size(double.PositiveInfinity, rowHeight));
|
||||
columnWidth = Math.Max(columnWidth, child.DesiredSize.Width);
|
||||
row++;
|
||||
|
||||
if (row >= rowCount || i == childCount - 1)
|
||||
{
|
||||
// ------------------------------------------------------------
|
||||
// 最后一列不加Gap
|
||||
// ------------------------------------------------------------
|
||||
totalWidth += columnWidth;
|
||||
if (i != childCount - 1) totalWidth += Gap;
|
||||
|
||||
row = 0;
|
||||
columnWidth = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return new Size(totalWidth, panelHeight);
|
||||
}
|
||||
|
||||
private Size ArrangeVertical(Size finalSize)
|
||||
{
|
||||
double panelHeight = finalSize.Height;
|
||||
|
||||
// ---- 修复:与 MeasureVertical 一致 ----
|
||||
double minChildHeight = MinHeight <= 0 ? 1 : MinHeight;
|
||||
int rowCount = Math.Max(1, (int)Math.Floor((panelHeight + Gap) / (minChildHeight + Gap)));
|
||||
|
||||
int childCount = InternalChildren.Count;
|
||||
// ---- 修复2:裁到子项数 ----
|
||||
if (childCount > 0 && rowCount > childCount)
|
||||
{
|
||||
rowCount = childCount;
|
||||
}
|
||||
|
||||
double rowHeight = (panelHeight - Gap * (rowCount - 1)) / rowCount;
|
||||
|
||||
double x = 0;
|
||||
double y = 0;
|
||||
double columnWidth = 0;
|
||||
int row = 0;
|
||||
|
||||
for (int i = 0; i < childCount; i++)
|
||||
{
|
||||
UIElement child = InternalChildren[i];
|
||||
child.Arrange(new Rect(x, y, child.DesiredSize.Width, rowHeight));
|
||||
columnWidth = Math.Max(columnWidth, child.DesiredSize.Width);
|
||||
row++;
|
||||
|
||||
if (row >= rowCount || i == childCount - 1)
|
||||
{
|
||||
// ------------------------------------------------------------
|
||||
// 最后一列不加Gap
|
||||
// ------------------------------------------------------------
|
||||
x += columnWidth;
|
||||
if (i != childCount - 1) x += Gap;
|
||||
|
||||
y = 0;
|
||||
row = 0;
|
||||
columnWidth = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
y += rowHeight + Gap;
|
||||
}
|
||||
}
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using LiveChartsCore.SkiaSharpView.Painting;
|
||||
using SkiaSharp;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
using YY.Admin.Core.LiveCharts2;
|
||||
|
||||
namespace YY.Admin.Core.Converter
|
||||
{
|
||||
public class BrushToSolidColorPaintConverter : IValueConverter
|
||||
{
|
||||
public object? Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (parameter is not CusSolidColorPaint sourcePaint)
|
||||
return new SolidColorPaint(SKColors.Transparent);
|
||||
|
||||
var skColor = ResolveColor(sourcePaint.CusColor);
|
||||
|
||||
var targetPaint = new CusSolidColorPaint(skColor);
|
||||
|
||||
MapProperties(sourcePaint, targetPaint);
|
||||
|
||||
return targetPaint;
|
||||
}
|
||||
|
||||
// 统一的颜色解析方法
|
||||
private SKColor ResolveColor(string? colorKey)
|
||||
{
|
||||
if (SKColor.TryParse(colorKey, out var parsedColor))
|
||||
return parsedColor;
|
||||
|
||||
var resource = Application.Current.FindResource(colorKey);
|
||||
|
||||
return resource switch
|
||||
{
|
||||
Color mediaColor => new SKColor(mediaColor.R, mediaColor.G, mediaColor.B, mediaColor.A),
|
||||
SolidColorBrush brush => new SKColor(brush.Color.R, brush.Color.G, brush.Color.B, brush.Color.A),
|
||||
_ => SKColors.Transparent,
|
||||
};
|
||||
}
|
||||
|
||||
protected void MapProperties(CusSolidColorPaint sourcePaint, CusSolidColorPaint targetPaint)
|
||||
{
|
||||
targetPaint.IsAntialias = sourcePaint.IsAntialias;
|
||||
targetPaint.StrokeThickness = sourcePaint.StrokeThickness;
|
||||
targetPaint.StrokeCap = sourcePaint.StrokeCap;
|
||||
targetPaint.StrokeMiter = sourcePaint.StrokeMiter;
|
||||
targetPaint.StrokeJoin = sourcePaint.StrokeJoin;
|
||||
targetPaint.ImageFilter = sourcePaint.ImageFilter;
|
||||
targetPaint.PathEffect = sourcePaint.PathEffect;
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (sourcePaint.FontWeight != SKFontStyleWeight.Normal || sourcePaint.FontWidth != SKFontStyleWidth.Normal || sourcePaint.FontSlant != SKFontStyleSlant.Upright)
|
||||
targetPaint.SKFontStyle = new SKFontStyle(sourcePaint.FontWeight, sourcePaint.FontWidth, sourcePaint.FontSlant);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
if (sourcePaint?.FontFamily is not null)
|
||||
{
|
||||
targetPaint.SKTypeface = SKTypeface.FromFamilyName(sourcePaint?.FontFamily);
|
||||
}
|
||||
else if (sourcePaint?.TypefaceMatchesChar is not null && sourcePaint.TypefaceMatchesChar.Length > 0)
|
||||
{
|
||||
targetPaint.SKTypeface = SKFontManager.Default.MatchCharacter(sourcePaint.TypefaceMatchesChar[0]);
|
||||
}
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace YY.Admin.Core.Converter
|
||||
{
|
||||
public class EnumDescriptionConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value == null || value.ToString() == null) return string.Empty;
|
||||
|
||||
var field = value.GetType().GetField(value.ToString()!);
|
||||
if (field == null) return value.ToString()!;
|
||||
|
||||
var attribute = field.GetCustomAttribute<DescriptionAttribute>();
|
||||
return attribute?.Description ?? value.ToString()!;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace YY.Admin.Core.Converter
|
||||
{
|
||||
/// <summary>
|
||||
/// 将枚举值和 bool 互转,适用于 ToggleButton 或 RadioButton
|
||||
/// </summary>
|
||||
public class EnumToBoolConverter : IValueConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// 将枚举值转换为 bool
|
||||
/// </summary>
|
||||
/// <param name="value">绑定的枚举值</param>
|
||||
/// <param name="targetType"></param>
|
||||
/// <param name="parameter">ConverterParameter 指定要匹配的枚举值</param>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns>匹配返回 true,否则 false</returns>
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
string enumValue = value?.ToString() ?? string.Empty;
|
||||
string targetValue = parameter?.ToString() ?? string.Empty;
|
||||
|
||||
return enumValue.Equals(targetValue, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 bool 转回枚举值
|
||||
/// </summary>
|
||||
/// <param name="value">ToggleButton.IsChecked</param>
|
||||
/// <param name="targetType"></param>
|
||||
/// <param name="parameter">ConverterParameter 指定对应的枚举值</param>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns>如果 true 返回枚举,否则返回 Binding.DoNothing</returns>
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is bool isChecked && isChecked && parameter != null)
|
||||
{
|
||||
// 判断 targetType 是否是 Nullable 类型
|
||||
if (Nullable.GetUnderlyingType(targetType) != null)
|
||||
{
|
||||
// 获取 Nullable 的基础类型
|
||||
Type underlyingType = Nullable.GetUnderlyingType(targetType)!;
|
||||
|
||||
// 确保基础类型是枚举类型
|
||||
if (underlyingType?.IsEnum == true)
|
||||
{
|
||||
return Enum.Parse(underlyingType, parameter.ToString()!);
|
||||
}
|
||||
}
|
||||
// 如果不是枚举类型或 Nullable 类型,返回 Binding.DoNothing
|
||||
else if (targetType.IsEnum)
|
||||
{
|
||||
return Enum.Parse(targetType, parameter.ToString()!);
|
||||
}
|
||||
}
|
||||
return Binding.DoNothing;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace YY.Admin.Core.Converter
|
||||
{
|
||||
public class EnumToIntConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is Enum enumValue)
|
||||
{
|
||||
System.Convert.ToInt32(enumValue);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is int intValue && parameter is Type enumType && enumType.IsEnum)
|
||||
{
|
||||
return Enum.ToObject(enumType, intValue);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace YY.Admin.Core.Converter
|
||||
{
|
||||
public class EnumToTagTypeConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is StatusEnum status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
StatusEnum.Enable => Application.Current.FindResource("SuccessBrush"), // 启用 - 绿色
|
||||
StatusEnum.Disable => Application.Current.FindResource("DangerBrush"), // 禁用 - 红色
|
||||
_ => Application.Current.FindResource("InfoBrush")
|
||||
};
|
||||
}
|
||||
|
||||
return Application.Current.FindResource("InfoBrush");
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace YY.Admin.Core.Converter
|
||||
{
|
||||
/// <summary>
|
||||
/// 将枚举值转换为Visibility
|
||||
/// </summary>
|
||||
public class EnumToVisibilityConverter : IValueConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// 将枚举值转换为 Visibility
|
||||
/// </summary>
|
||||
/// <param name="value">绑定的枚举值</param>
|
||||
/// <param name="targetType"></param>
|
||||
/// <param name="parameter">ConverterParameter 指定要匹配的枚举值</param>
|
||||
/// <param name="culture"></param>
|
||||
/// <returns>匹配返回Visible</returns>
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value == null || parameter == null)
|
||||
return Visibility.Collapsed;
|
||||
|
||||
return value.ToString()!.Equals(parameter.ToString(), StringComparison.OrdinalIgnoreCase)
|
||||
? Visibility.Visible
|
||||
: Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
23
yy-admin-master/YY.Admin.Core/Converter/MaxWidthConverter.cs
Normal file
23
yy-admin-master/YY.Admin.Core/Converter/MaxWidthConverter.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace YY.Admin.Core.Converter
|
||||
{
|
||||
public class MaxWidthConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is double totalWidth && totalWidth > 0 && parameter != null && double.TryParse(parameter.ToString(), out var offset) && totalWidth >= offset)
|
||||
{
|
||||
// 最大宽度
|
||||
return totalWidth - offset;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using HandyControl.Data;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace YY.Admin.Core.Converter
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据 TitleWidth 计算 Margin.Left,如果 TitlePlacement=Top 则不偏移
|
||||
/// </summary>
|
||||
public class NegativeLeftThicknessConverter : IMultiValueConverter
|
||||
{
|
||||
// 这里改成 MultiBinding,方便同时拿 TitleWidth 和 TitlePlacement
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
double left = 0;
|
||||
double bottomShift = -18;
|
||||
|
||||
if (parameter != null && double.TryParse(parameter.ToString(), out var p))
|
||||
bottomShift = p;
|
||||
|
||||
// 如果 TitleWidth 还未设置,直接返回默认 Thickness
|
||||
if (values.Length > 0 && values[0] != DependencyProperty.UnsetValue)
|
||||
{
|
||||
var widthValue = values[0];
|
||||
if (widthValue is GridLength gridLength && gridLength.IsAbsolute)
|
||||
left = gridLength.Value;
|
||||
else if (widthValue is double d)
|
||||
left = d;
|
||||
}
|
||||
|
||||
// 如果 TitlePlacement 还未设置,也要避免异常
|
||||
if (values.Length > 1 && values[1] != DependencyProperty.UnsetValue)
|
||||
{
|
||||
if (values[1] is TitlePlacementType placement)
|
||||
{
|
||||
// 如果标题在上方,则不偏移
|
||||
if (placement == TitlePlacementType.Top)
|
||||
left = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return new Thickness(left, 0, 0, bottomShift);
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace YY.Admin.Core.Converter
|
||||
{
|
||||
public class PercentageConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is double actualHeight && parameter is string paramString)
|
||||
{
|
||||
double percentage = double.Parse(paramString);
|
||||
return actualHeight * percentage;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace YY.Admin.Core.Converter
|
||||
{
|
||||
public class RadioButtonEnumMultiConverter : IMultiValueConverter
|
||||
{
|
||||
// values[0] = RadioButton 自身
|
||||
// values[1] = SysUser.Status
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (values.Length < 2) return false;
|
||||
if (values[0] == null || values[1] == null) return false;
|
||||
return values[0].Equals(values[1]);
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||
{
|
||||
// VM中通过事件实现UI -> VM的同步,这里不处理,直接返回Binding.DoNothing
|
||||
return new object[] { Binding.DoNothing, Binding.DoNothing };
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Prism.Events;
|
||||
|
||||
namespace YY.Admin.Core.Events;
|
||||
|
||||
public sealed class NetworkStatusChangedPayload
|
||||
{
|
||||
public bool IsOnline { get; set; }
|
||||
public DateTime ChangedAt { get; set; }
|
||||
}
|
||||
|
||||
public class NetworkStatusChangedEvent : PubSubEvent<NetworkStatusChangedPayload>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using Prism.Events;
|
||||
|
||||
namespace YY.Admin.Core.Events;
|
||||
|
||||
public sealed class RemoteCommandPayload
|
||||
{
|
||||
public string DeviceId { get; set; } = string.Empty;
|
||||
public string CommandJson { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class RemoteCommandReceivedEvent : PubSubEvent<RemoteCommandPayload>
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using Prism.Events;
|
||||
|
||||
namespace YY.Admin.Core.Events;
|
||||
|
||||
public class SyncCompletedEvent : PubSubEvent<string>
|
||||
{
|
||||
}
|
||||
25
yy-admin-master/YY.Admin.Core/Core/Models/DeviceStatus.cs
Normal file
25
yy-admin-master/YY.Admin.Core/Core/Models/DeviceStatus.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using SqlSugar;
|
||||
|
||||
namespace YY.Admin.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 设备本地状态快照。
|
||||
/// </summary>
|
||||
[SugarTable("device_status_snapshot")]
|
||||
public class DeviceStatus
|
||||
{
|
||||
[SugarColumn(IsPrimaryKey = true, Length = 64)]
|
||||
public string DeviceId { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(IsNullable = false)]
|
||||
public bool IsOnline { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public DateTime? LastHeartbeatAt { get; set; }
|
||||
|
||||
[SugarColumn(ColumnDataType = "TEXT", IsNullable = true)]
|
||||
public string? StatusJson { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = false)]
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
43
yy-admin-master/YY.Admin.Core/Core/Models/OutboxMessage.cs
Normal file
43
yy-admin-master/YY.Admin.Core/Core/Models/OutboxMessage.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using SqlSugar;
|
||||
|
||||
namespace YY.Admin.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 断联续传消息实体。
|
||||
/// </summary>
|
||||
[SugarTable("outbox_messages")]
|
||||
public class OutboxMessage
|
||||
{
|
||||
[SugarColumn(IsPrimaryKey = true, Length = 64)]
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||||
|
||||
[SugarColumn(Length = 128, IsNullable = false)]
|
||||
public string AggregateType { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(Length = 128, IsNullable = false)]
|
||||
public string AggregateId { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(Length = 128, IsNullable = false)]
|
||||
public string EventType { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(ColumnDataType = "TEXT", IsNullable = false)]
|
||||
public string Payload { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(IsNullable = false)]
|
||||
public int Status { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = false)]
|
||||
public int RetryCount { get; set; }
|
||||
|
||||
[SugarColumn(Length = 2000, IsNullable = true)]
|
||||
public string? ErrorMessage { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = false)]
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public DateTime? LastTriedAt { get; set; }
|
||||
|
||||
[SugarColumn(IsNullable = true)]
|
||||
public DateTime? SentAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace YY.Admin.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 通过 REST(SCADA)拉取用户并写入本地 jeecg_sys_user,供 Outbox 统一线路消费。
|
||||
/// </summary>
|
||||
public interface IJeecgUserMirrorPullHandler
|
||||
{
|
||||
Task<bool> ExecutePullAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace YY.Admin.Core.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 将「需拉取 Jeecg 用户镜像」写入 Outbox(断网不丢、联网续传),由 Services 侧协调器调用。
|
||||
/// </summary>
|
||||
public interface IJeecgUserMirrorPullOutbox
|
||||
{
|
||||
Task EnqueuePullAsync(string eventType, string? payloadJson, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace YY.Admin.Core.Services;
|
||||
|
||||
public interface INetworkMonitor
|
||||
{
|
||||
bool IsOnline { get; }
|
||||
event Action<bool>? StatusChanged;
|
||||
Task StartAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace YY.Admin.Core.Services;
|
||||
|
||||
public interface ISignalRService
|
||||
{
|
||||
Task ConnectAsync(string token, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 设备同步统一通道:STOMP 订阅 /topic/sync/jeecg-users(免密或带设备 Token),与 Outbox+REST 同属一条规范线路。
|
||||
/// </summary>
|
||||
Task ConnectUnifiedDeviceChannelAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
Task SendDeviceStatusAsync(object status, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace YY.Admin.Core.Sync;
|
||||
|
||||
/// <summary>
|
||||
/// 用户镜像同步在 Outbox 中的聚合类型(与设备批量上报区分,走本地拉取 REST 而非 /sys/sync/batch)。
|
||||
/// </summary>
|
||||
public static class JeecgUserMirrorOutbox
|
||||
{
|
||||
public const string AggregateType = "JeecgUserMirror";
|
||||
public const string EventSignal = "Signal";
|
||||
public const string EventBoot = "Boot";
|
||||
}
|
||||
192
yy-admin-master/YY.Admin.Core/Entity/EntityBase.cs
Normal file
192
yy-admin-master/YY.Admin.Core/Entity/EntityBase.cs
Normal file
@@ -0,0 +1,192 @@
|
||||
|
||||
|
||||
namespace YY.Admin.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 框架实体基类Id
|
||||
/// </summary>
|
||||
public abstract class EntityBaseId
|
||||
{
|
||||
/// <summary>
|
||||
/// 雪花Id
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnName = "Id", ColumnDescription = "主键Id", IsPrimaryKey = true, IsIdentity = false)]
|
||||
public virtual long Id { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 框架实体基类
|
||||
/// </summary>
|
||||
[SugarIndex("index_{table}_CT", nameof(CreateTime), OrderByType.Asc)]
|
||||
public abstract class EntityBase : EntityBaseId
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建时间
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "创建时间", IsNullable = true, IsOnlyIgnoreUpdate = true)]
|
||||
public virtual DateTime CreateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "更新时间")]
|
||||
public virtual DateTime? UpdateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建者Id
|
||||
/// </summary>
|
||||
[OwnerUser]
|
||||
[SugarColumn(ColumnDescription = "创建者Id", IsOnlyIgnoreUpdate = true)]
|
||||
public virtual long? CreateUserId { get; set; }
|
||||
|
||||
///// <summary>
|
||||
///// 创建者
|
||||
///// </summary>
|
||||
//[Newtonsoft.Json.JsonIgnore]
|
||||
//[System.Text.Json.Serialization.JsonIgnore]
|
||||
//[Navigate(NavigateType.OneToOne, nameof(CreateUserId))]
|
||||
//public virtual SysUser CreateUser { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建者姓名
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "创建者姓名", Length = 64, IsOnlyIgnoreUpdate = true)]
|
||||
public virtual string? CreateUserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 修改者Id
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "修改者Id")]
|
||||
public virtual long? UpdateUserId { get; set; }
|
||||
|
||||
///// <summary>
|
||||
///// 修改者
|
||||
///// </summary>
|
||||
//[Newtonsoft.Json.JsonIgnore]
|
||||
//[System.Text.Json.Serialization.JsonIgnore]
|
||||
//[Navigate(NavigateType.OneToOne, nameof(UpdateUserId))]
|
||||
//public virtual SysUser UpdateUser { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 修改者姓名
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "修改者姓名", Length = 64)]
|
||||
public virtual string? UpdateUserName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 框架实体基类(删除标志)
|
||||
/// </summary>
|
||||
[SugarIndex("index_{table}_D", nameof(IsDelete), OrderByType.Asc)]
|
||||
public abstract class EntityBaseDel : EntityBase, IDeletedFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// 软删除
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "软删除")]
|
||||
public virtual bool IsDelete { get; set; } = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 机构实体基类(数据权限)
|
||||
/// </summary>
|
||||
public abstract class EntityBaseOrg : EntityBase, IOrgIdFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// 机构Id
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "机构Id", IsNullable = true)]
|
||||
public virtual long OrgId { get; set; }
|
||||
|
||||
///// <summary>
|
||||
///// 创建者部门Id
|
||||
///// </summary>
|
||||
//[SugarColumn(ColumnDescription = "创建者部门Id", IsOnlyIgnoreUpdate = true)]
|
||||
//public virtual long? CreateOrgId { get; set; }
|
||||
|
||||
///// <summary>
|
||||
///// 创建者部门
|
||||
///// </summary>
|
||||
//[Newtonsoft.Json.JsonIgnore]
|
||||
//[System.Text.Json.Serialization.JsonIgnore]
|
||||
//[Navigate(NavigateType.OneToOne, nameof(CreateOrgId))]
|
||||
//public virtual SysOrg CreateOrg { get; set; }
|
||||
|
||||
///// <summary>
|
||||
///// 创建者部门名称
|
||||
///// </summary>
|
||||
//[SugarColumn(ColumnDescription = "创建者部门名称", Length = 64, IsOnlyIgnoreUpdate = true)]
|
||||
//public virtual string? CreateOrgName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 机构实体基类(数据权限、删除标志)
|
||||
/// </summary>
|
||||
public abstract class EntityBaseOrgDel : EntityBaseDel, IOrgIdFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// 机构Id
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "机构Id", IsNullable = true)]
|
||||
public virtual long OrgId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 租户实体基类
|
||||
/// </summary>
|
||||
public abstract class EntityBaseTenant : EntityBase, ITenantIdFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户Id
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "租户Id", IsOnlyIgnoreUpdate = true)]
|
||||
public virtual long? TenantId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 租户实体基类(删除标志)
|
||||
/// </summary>
|
||||
public abstract class EntityBaseTenantDel : EntityBaseDel, ITenantIdFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户Id
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "租户Id", IsOnlyIgnoreUpdate = true)]
|
||||
public virtual long? TenantId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 租户实体基类Id
|
||||
/// </summary>
|
||||
public abstract class EntityBaseTenantId : EntityBaseId, ITenantIdFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户Id
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "租户Id", IsOnlyIgnoreUpdate = true)]
|
||||
public virtual long? TenantId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 租户机构实体基类(数据权限)
|
||||
/// </summary>
|
||||
public abstract class EntityBaseTenantOrg : EntityBaseOrg, ITenantIdFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户Id
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "租户Id", IsOnlyIgnoreUpdate = true)]
|
||||
public virtual long? TenantId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 租户机构实体基类(数据权限、删除标志)
|
||||
/// </summary>
|
||||
public abstract class EntityBaseTenantOrgDel : EntityBaseOrgDel, ITenantIdFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户Id
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "租户Id", IsOnlyIgnoreUpdate = true)]
|
||||
public virtual long? TenantId { get; set; }
|
||||
}
|
||||
36
yy-admin-master/YY.Admin.Core/Entity/IEntityFilter.cs
Normal file
36
yy-admin-master/YY.Admin.Core/Entity/IEntityFilter.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
|
||||
namespace YY.Admin.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 假删除接口过滤器
|
||||
/// </summary>
|
||||
public interface IDeletedFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// 软删除
|
||||
/// </summary>
|
||||
bool IsDelete { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 租户Id接口过滤器
|
||||
/// </summary>
|
||||
public interface ITenantIdFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户Id
|
||||
/// </summary>
|
||||
long? TenantId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 机构Id接口过滤器
|
||||
/// </summary>
|
||||
public interface IOrgIdFilter
|
||||
{
|
||||
/// <summary>
|
||||
/// 机构Id
|
||||
/// </summary>
|
||||
long OrgId { get; set; }
|
||||
}
|
||||
59
yy-admin-master/YY.Admin.Core/Entity/JeecgSysDictItem.cs
Normal file
59
yy-admin-master/YY.Admin.Core/Entity/JeecgSysDictItem.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using SqlSugar;
|
||||
|
||||
namespace YY.Admin.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Jeecg 数据字典项同构表(桌面端)
|
||||
/// </summary>
|
||||
[SugarTable("jeecg_sys_dict_item", "Jeecg数据字典项同构表")]
|
||||
[SysTable]
|
||||
public class JeecgSysDictItem
|
||||
{
|
||||
[SugarColumn(ColumnName = "id", IsPrimaryKey = true, Length = 64)]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
[SugarColumn(ColumnName = "dict_id", Length = 64, IsNullable = true)]
|
||||
public string? DictId { get; set; }
|
||||
|
||||
[SugarColumn(ColumnName = "dict_name", Length = 255, IsNullable = true)]
|
||||
public string? DictName { get; set; }
|
||||
|
||||
[SugarColumn(ColumnName = "dict_code", Length = 255, IsNullable = true)]
|
||||
public string? DictCode { get; set; }
|
||||
|
||||
[SugarColumn(ColumnName = "dict_type", IsNullable = true)]
|
||||
public int? DictType { get; set; }
|
||||
|
||||
[SugarColumn(ColumnName = "dict_description", Length = 500, IsNullable = true)]
|
||||
public string? DictDescription { get; set; }
|
||||
|
||||
[SugarColumn(ColumnName = "item_text", Length = 255, IsNullable = true)]
|
||||
public string? ItemText { get; set; }
|
||||
|
||||
[SugarColumn(ColumnName = "item_value", Length = 255, IsNullable = true)]
|
||||
public string? ItemValue { get; set; }
|
||||
|
||||
[SugarColumn(ColumnName = "item_description", Length = 500, IsNullable = true)]
|
||||
public string? ItemDescription { get; set; }
|
||||
|
||||
[SugarColumn(ColumnName = "sort_order", IsNullable = true)]
|
||||
public int? SortOrder { get; set; }
|
||||
|
||||
[SugarColumn(ColumnName = "status", IsNullable = true)]
|
||||
public int? Status { get; set; }
|
||||
|
||||
[SugarColumn(ColumnName = "item_color", Length = 100, IsNullable = true)]
|
||||
public string? ItemColor { get; set; }
|
||||
|
||||
[SugarColumn(ColumnName = "create_by", Length = 100, IsNullable = true)]
|
||||
public string? CreateBy { get; set; }
|
||||
|
||||
[SugarColumn(ColumnName = "create_time", IsNullable = true)]
|
||||
public DateTime? CreateTime { get; set; }
|
||||
|
||||
[SugarColumn(ColumnName = "update_by", Length = 100, IsNullable = true)]
|
||||
public string? UpdateBy { get; set; }
|
||||
|
||||
[SugarColumn(ColumnName = "update_time", IsNullable = true)]
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
}
|
||||
181
yy-admin-master/YY.Admin.Core/Entity/JeecgSysUser.cs
Normal file
181
yy-admin-master/YY.Admin.Core/Entity/JeecgSysUser.cs
Normal file
@@ -0,0 +1,181 @@
|
||||
using SqlSugar;
|
||||
|
||||
namespace YY.Admin.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Jeecg 用户同构表(桌面端)
|
||||
/// 说明:
|
||||
/// 1. 按 JeecgBoot 的 sys_user 字段模型定义。
|
||||
/// 2. 为避免影响现有 YY.Admin 的 sys_user 业务表,这里单独落表 jeecg_sys_user。
|
||||
/// </summary>
|
||||
[SugarTable("jeecg_sys_user", "Jeecg用户同构表")]
|
||||
[SysTable]
|
||||
public class JeecgSysUser
|
||||
{
|
||||
/// <summary>id(Jeecg为字符串雪花ID)</summary>
|
||||
[SugarColumn(ColumnName = "id", IsPrimaryKey = true, Length = 64)]
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>登录账号</summary>
|
||||
[SugarColumn(ColumnName = "username", Length = 64, IsNullable = true)]
|
||||
public string? Username { get; set; }
|
||||
|
||||
/// <summary>真实姓名</summary>
|
||||
[SugarColumn(ColumnName = "realname", Length = 64, IsNullable = true)]
|
||||
public string? Realname { get; set; }
|
||||
|
||||
/// <summary>密码</summary>
|
||||
[SugarColumn(ColumnName = "password", Length = 512, IsNullable = true)]
|
||||
public string? Password { get; set; }
|
||||
|
||||
/// <summary>密码盐</summary>
|
||||
[SugarColumn(ColumnName = "salt", Length = 64, IsNullable = true)]
|
||||
public string? Salt { get; set; }
|
||||
|
||||
/// <summary>头像</summary>
|
||||
[SugarColumn(ColumnName = "avatar", Length = 512, IsNullable = true)]
|
||||
public string? Avatar { get; set; }
|
||||
|
||||
/// <summary>生日</summary>
|
||||
[SugarColumn(ColumnName = "birthday", IsNullable = true)]
|
||||
public DateTime? Birthday { get; set; }
|
||||
|
||||
/// <summary>性别(1男2女)</summary>
|
||||
[SugarColumn(ColumnName = "sex", IsNullable = true)]
|
||||
public int? Sex { get; set; }
|
||||
|
||||
/// <summary>邮箱</summary>
|
||||
[SugarColumn(ColumnName = "email", Length = 128, IsNullable = true)]
|
||||
public string? Email { get; set; }
|
||||
|
||||
/// <summary>手机号</summary>
|
||||
[SugarColumn(ColumnName = "phone", Length = 32, IsNullable = true)]
|
||||
public string? Phone { get; set; }
|
||||
|
||||
/// <summary>登录选择部门编码</summary>
|
||||
[SugarColumn(ColumnName = "org_code", Length = 128, IsNullable = true)]
|
||||
public string? OrgCode { get; set; }
|
||||
|
||||
/// <summary>登录选择租户ID</summary>
|
||||
[SugarColumn(ColumnName = "login_tenant_id", IsNullable = true)]
|
||||
public int? LoginTenantId { get; set; }
|
||||
|
||||
/// <summary>状态(1正常 2冻结)</summary>
|
||||
[SugarColumn(ColumnName = "status", IsNullable = true)]
|
||||
public int? Status { get; set; }
|
||||
|
||||
/// <summary>删除标志(0正常 1已删)</summary>
|
||||
[SugarColumn(ColumnName = "del_flag", IsNullable = true)]
|
||||
public int? DelFlag { get; set; }
|
||||
|
||||
/// <summary>工号</summary>
|
||||
[SugarColumn(ColumnName = "work_no", Length = 64, IsNullable = true)]
|
||||
public string? WorkNo { get; set; }
|
||||
|
||||
/// <summary>座机号</summary>
|
||||
[SugarColumn(ColumnName = "telephone", Length = 32, IsNullable = true)]
|
||||
public string? Telephone { get; set; }
|
||||
|
||||
/// <summary>创建人</summary>
|
||||
[SugarColumn(ColumnName = "create_by", Length = 64, IsNullable = true)]
|
||||
public string? CreateBy { get; set; }
|
||||
|
||||
/// <summary>创建时间</summary>
|
||||
[SugarColumn(ColumnName = "create_time", IsNullable = true)]
|
||||
public DateTime? CreateTime { get; set; }
|
||||
|
||||
/// <summary>更新人</summary>
|
||||
[SugarColumn(ColumnName = "update_by", Length = 64, IsNullable = true)]
|
||||
public string? UpdateBy { get; set; }
|
||||
|
||||
/// <summary>更新时间</summary>
|
||||
[SugarColumn(ColumnName = "update_time", IsNullable = true)]
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
|
||||
/// <summary>流程同步标志</summary>
|
||||
[SugarColumn(ColumnName = "activiti_sync", IsNullable = true)]
|
||||
public int? ActivitiSync { get; set; }
|
||||
|
||||
/// <summary>身份(0普通成员 1上级)</summary>
|
||||
[SugarColumn(ColumnName = "user_identity", IsNullable = true)]
|
||||
public int? UserIdentity { get; set; }
|
||||
|
||||
/// <summary>负责部门IDs</summary>
|
||||
[SugarColumn(ColumnName = "depart_ids", Length = 1024, IsNullable = true)]
|
||||
public string? DepartIds { get; set; }
|
||||
|
||||
/// <summary>设备ID</summary>
|
||||
[SugarColumn(ColumnName = "client_id", Length = 256, IsNullable = true)]
|
||||
public string? ClientId { get; set; }
|
||||
|
||||
/// <summary>流程状态</summary>
|
||||
[SugarColumn(ColumnName = "bpm_status", Length = 64, IsNullable = true)]
|
||||
public string? BpmStatus { get; set; }
|
||||
|
||||
/// <summary>个性签名</summary>
|
||||
[SugarColumn(ColumnName = "sign", Length = 512, IsNullable = true)]
|
||||
public string? Sign { get; set; }
|
||||
|
||||
/// <summary>是否开启个性签名</summary>
|
||||
[SugarColumn(ColumnName = "sign_enable", IsNullable = true)]
|
||||
public int? SignEnable { get; set; }
|
||||
|
||||
/// <summary>主岗位</summary>
|
||||
[SugarColumn(ColumnName = "main_dep_post_id", Length = 128, IsNullable = true)]
|
||||
public string? MainDepPostId { get; set; }
|
||||
|
||||
/// <summary>职务(字典)</summary>
|
||||
[SugarColumn(ColumnName = "position_type", Length = 64, IsNullable = true)]
|
||||
public string? PositionType { get; set; }
|
||||
|
||||
/// <summary>最后修改密码时间</summary>
|
||||
[SugarColumn(ColumnName = "last_pwd_update_time", IsNullable = true)]
|
||||
public DateTime? LastPwdUpdateTime { get; set; }
|
||||
|
||||
/// <summary>排序</summary>
|
||||
[SugarColumn(ColumnName = "sort", IsNullable = true)]
|
||||
public int? Sort { get; set; }
|
||||
|
||||
/// <summary>是否隐藏联系方式 0否1是</summary>
|
||||
[SugarColumn(ColumnName = "iz_hide_contact", Length = 8, IsNullable = true)]
|
||||
public string? IzHideContact { get; set; }
|
||||
|
||||
// ------------------------- 非持久化字段(与 Jeecg 实体保持一致) -------------------------
|
||||
|
||||
/// <summary>部门名称(非持久化)</summary>
|
||||
[SugarColumn(IsIgnore = true)]
|
||||
public string? OrgCodeTxt { get; set; }
|
||||
|
||||
/// <summary>职务(非持久化)</summary>
|
||||
[SugarColumn(IsIgnore = true)]
|
||||
public string? Post { get; set; }
|
||||
|
||||
/// <summary>多租户ids临时字段(非持久化)</summary>
|
||||
[SugarColumn(IsIgnore = true)]
|
||||
public string? RelTenantIds { get; set; }
|
||||
|
||||
/// <summary>首页路径(非持久化)</summary>
|
||||
[SugarColumn(IsIgnore = true)]
|
||||
public string? HomePath { get; set; }
|
||||
|
||||
/// <summary>职位名称(非持久化)</summary>
|
||||
[SugarColumn(IsIgnore = true)]
|
||||
public string? PostText { get; set; }
|
||||
|
||||
/// <summary>是否绑定第三方(非持久化)</summary>
|
||||
[SugarColumn(IsIgnore = true)]
|
||||
public bool IzBindThird { get; set; }
|
||||
|
||||
/// <summary>兼职岗位(非持久化)</summary>
|
||||
[SugarColumn(IsIgnore = true)]
|
||||
public string? OtherDepPostId { get; set; }
|
||||
|
||||
/// <summary>登录时选择部门编码(非持久化)</summary>
|
||||
[SugarColumn(IsIgnore = true)]
|
||||
public string? LoginOrgCode { get; set; }
|
||||
|
||||
/// <summary>所属部门IDs(非持久化)</summary>
|
||||
[SugarColumn(IsIgnore = true)]
|
||||
public string? BelongDepIds { get; set; }
|
||||
}
|
||||
|
||||
64
yy-admin-master/YY.Admin.Core/Entity/SysConfig.cs
Normal file
64
yy-admin-master/YY.Admin.Core/Entity/SysConfig.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
namespace YY.Admin.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 系统配置参数表
|
||||
/// </summary>
|
||||
[SugarTable("sys_config", "系统配置参数表")]
|
||||
[SysTable]
|
||||
[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)]
|
||||
[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc, IsUnique = true)]
|
||||
public partial class SysConfig : EntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "名称", Length = 64)]
|
||||
[Required, MaxLength(64)]
|
||||
public virtual string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 编码
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "编码", Length = 64)]
|
||||
[MaxLength(64)]
|
||||
public string? Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 参数值
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "参数值", Length = 512)]
|
||||
[MaxLength(512)]
|
||||
[IgnoreUpdateSeedColumn]
|
||||
public string? Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否是内置参数(Y-是,N-否)
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "是否是内置参数", DefaultValue = "1")]
|
||||
public YesNoEnum SysFlag { get; set; } = YesNoEnum.Y;
|
||||
|
||||
/// <summary>
|
||||
/// 分组编码
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "分组编码", Length = 64)]
|
||||
[MaxLength(64)]
|
||||
public string? GroupCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "排序", DefaultValue = "100")]
|
||||
public int OrderNo { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// 备注
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "备注", Length = 256)]
|
||||
[MaxLength(256)]
|
||||
public string? Remark { get; set; }
|
||||
}
|
||||
101
yy-admin-master/YY.Admin.Core/Entity/SysDictData.cs
Normal file
101
yy-admin-master/YY.Admin.Core/Entity/SysDictData.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
|
||||
|
||||
namespace YY.Admin.Core;
|
||||
|
||||
/// <summary>
|
||||
/// 系统字典值表
|
||||
/// </summary>
|
||||
[SugarTable("sys_dict_data", "系统字典值表")]
|
||||
[SysTable]
|
||||
[SugarIndex("index_{table}_TV", nameof(DictTypeId), OrderByType.Asc, nameof(Value), OrderByType.Asc, IsUnique = true)]
|
||||
public partial class SysDictData : EntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 字典类型Id
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "字典类型Id")]
|
||||
public long DictTypeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 字典类型
|
||||
/// </summary>
|
||||
[Newtonsoft.Json.JsonIgnore]
|
||||
[System.Text.Json.Serialization.JsonIgnore]
|
||||
[Navigate(NavigateType.OneToOne, nameof(DictTypeId))]
|
||||
public SysDictType DictType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示文本
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "显示文本", Length = 256)]
|
||||
[Required, MaxLength(256)]
|
||||
public virtual string Label { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 值
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "值", Length = 256)]
|
||||
[Required, MaxLength(256)]
|
||||
public virtual string Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 编码
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// </remarks>
|
||||
[SugarColumn(ColumnDescription = "编码", Length = 256)]
|
||||
public virtual string? Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 名称
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "名称", Length = 256)]
|
||||
[MaxLength(256)]
|
||||
public virtual string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示样式-标签颜色
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "显示样式-标签颜色", Length = 16)]
|
||||
[MaxLength(16)]
|
||||
public string? TagType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示样式-Style(控制显示样式)
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "显示样式-Style", Length = 512)]
|
||||
[MaxLength(512)]
|
||||
public string? StyleSetting { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示样式-Class(控制显示样式)
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "显示样式-Class", Length = 512)]
|
||||
[MaxLength(512)]
|
||||
public string? ClassSetting { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 排序
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "排序", DefaultValue = "100")]
|
||||
public int OrderNo { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// 备注
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "备注", Length = 2048)]
|
||||
[MaxLength(2048)]
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 拓展数据(保存业务功能的配置项)
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "拓展数据(保存业务功能的配置项)", ColumnDataType = StaticConfig.CodeFirst_BigString)]
|
||||
public string? ExtData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 状态
|
||||
/// </summary>
|
||||
[SugarColumn(ColumnDescription = "状态", DefaultValue = "1")]
|
||||
public StatusEnum Status { get; set; } = StatusEnum.Enable;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user