diff --git a/.vscode/keybindings.json b/.vscode/keybindings.json new file mode 100644 index 0000000..45aa55e --- /dev/null +++ b/.vscode/keybindings.json @@ -0,0 +1,7 @@ +[ + { + "key": "ctrl+alt+r", + "command": "workbench.action.tasks.runTask", + "args": "YY.Admin: run" + } +] diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..2405e59 --- /dev/null +++ b/.vscode/tasks.json @@ -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" + } + ] +} diff --git a/docs/SCADA-用户同步接口文档.md b/docs/SCADA-用户同步接口文档.md new file mode 100644 index 0000000..fc2c92d --- /dev/null +++ b/docs/SCADA-用户同步接口文档.md @@ -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` + diff --git a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/Swagger3Config.java b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/Swagger3Config.java index 0d09379..b479413 100644 --- a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/Swagger3Config.java +++ b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/Swagger3Config.java @@ -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 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; diff --git a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/WebSocketConfig.java b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/WebSocketConfig.java index 801544b..8aa091e 100644 --- a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/WebSocketConfig.java +++ b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/WebSocketConfig.java @@ -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; } diff --git a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/filter/WebsocketFilter.java b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/filter/WebsocketFilter.java index b9a788e..2f26b01 100644 --- a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/filter/WebsocketFilter.java +++ b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/filter/WebsocketFilter.java @@ -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"); + } + } diff --git a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java index 7d44d62..3a545a2 100644 --- a/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java +++ b/jeecg-boot/jeecg-boot-base-core/src/main/java/org/jeecg/config/shiro/ShiroConfig.java @@ -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版本查询版本接口 diff --git a/jeecg-boot/jeecg-module-device-sync/pom.xml b/jeecg-boot/jeecg-module-device-sync/pom.xml new file mode 100644 index 0000000..2b2f276 --- /dev/null +++ b/jeecg-boot/jeecg-module-device-sync/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + + + org.jeecgframework.boot3 + jeecg-boot-parent + 3.9.1 + ../pom.xml + + + jeecg-module-device-sync + jeecg-module-device-sync + 设备断联续传同步模块 + + + + org.jeecgframework.boot3 + jeecg-boot-base-core + ${project.version} + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-websocket + + + diff --git a/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/config/WebSocketConfig.java b/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/config/WebSocketConfig.java new file mode 100644 index 0000000..a19f3a4 --- /dev/null +++ b/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/config/WebSocketConfig.java @@ -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("*"); + } +} diff --git a/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/controller/DeviceWebSocketController.java b/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/controller/DeviceWebSocketController.java new file mode 100644 index 0000000..7689f20 --- /dev/null +++ b/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/controller/DeviceWebSocketController.java @@ -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() + .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> sendCommand(@RequestBody Map 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 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 result = new HashMap<>(); + result.put("deviceId", deviceId); + result.put("queued", true); + return Result.OK(result); + } +} diff --git a/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/controller/SyncController.java b/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/controller/SyncController.java new file mode 100644 index 0000000..558853f --- /dev/null +++ b/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/controller/SyncController.java @@ -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> batch(@RequestBody List messages) { + int received = messages == null ? 0 : messages.size(); + int inserted = 0; + if (messages == null || messages.isEmpty()) { + Map 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 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() + .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() + .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); + } + } +} diff --git a/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/dto/SyncMessageDto.java b/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/dto/SyncMessageDto.java new file mode 100644 index 0000000..4dd8bb4 --- /dev/null +++ b/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/dto/SyncMessageDto.java @@ -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; +} diff --git a/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/entity/DeviceRegistry.java b/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/entity/DeviceRegistry.java new file mode 100644 index 0000000..ca5cc2b --- /dev/null +++ b/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/entity/DeviceRegistry.java @@ -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; +} diff --git a/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/entity/DeviceStatus.java b/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/entity/DeviceStatus.java new file mode 100644 index 0000000..7ddc995 --- /dev/null +++ b/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/entity/DeviceStatus.java @@ -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; +} diff --git a/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/entity/SyncIdempotentLog.java b/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/entity/SyncIdempotentLog.java new file mode 100644 index 0000000..3e3ca55 --- /dev/null +++ b/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/entity/SyncIdempotentLog.java @@ -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; +} diff --git a/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/filter/DeviceTokenRefreshFilter.java b/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/filter/DeviceTokenRefreshFilter.java new file mode 100644 index 0000000..9322623 --- /dev/null +++ b/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/filter/DeviceTokenRefreshFilter.java @@ -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); + } +} diff --git a/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/mapper/DeviceRegistryMapper.java b/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/mapper/DeviceRegistryMapper.java new file mode 100644 index 0000000..6a8d5ea --- /dev/null +++ b/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/mapper/DeviceRegistryMapper.java @@ -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 { +} diff --git a/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/mapper/DeviceStatusMapper.java b/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/mapper/DeviceStatusMapper.java new file mode 100644 index 0000000..ad66a84 --- /dev/null +++ b/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/mapper/DeviceStatusMapper.java @@ -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 { +} diff --git a/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/mapper/SyncIdempotentLogMapper.java b/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/mapper/SyncIdempotentLogMapper.java new file mode 100644 index 0000000..19fdae5 --- /dev/null +++ b/jeecg-boot/jeecg-module-device-sync/src/main/java/org/jeecg/modules/device/sync/mapper/SyncIdempotentLogMapper.java @@ -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 { + + @Select("SELECT COUNT(1) > 0 FROM sync_idempotent_log WHERE message_id = #{messageId}") + boolean exists(@Param("messageId") String messageId); +} diff --git a/jeecg-boot/jeecg-module-device-sync/src/main/resources/db/V1__device_sync.sql b/jeecg-boot/jeecg-module-device-sync/src/main/resources/db/V1__device_sync.sql new file mode 100644 index 0000000..05221a0 --- /dev/null +++ b/jeecg-boot/jeecg-module-device-sync/src/main/resources/db/V1__device_sync.sql @@ -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='设备注册表'; diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/pom.xml b/jeecg-boot/jeecg-module-system/jeecg-system-biz/pom.xml index 75f7739..1ae45a3 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/pom.xml +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/pom.xml @@ -61,6 +61,10 @@ jeecg-boot-module-airag ${jeecgboot.version} + + org.springframework + spring-messaging + org.apache.pdfbox pdfbox diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/message/websocket/WebSocket.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/message/websocket/WebSocket.java index 2592715..b450c27 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/message/websocket/WebSocket.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/message/websocket/WebSocket.java @@ -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 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 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 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); - // 代码逻辑说明: 【issues/1161】前端websocket因心跳导致监听不起作用--- - this.sendMessage(userId, "ping"); + 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发布订阅模式——推送消息】========================================================================================== } \ No newline at end of file diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysDictController.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysDictController.java index 268c3c0..947b2bc 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysDictController.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysDictController.java @@ -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>> 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 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 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 filteredDicts = sysDictService.list(dictFilter); + if (oConvertUtils.isEmpty(filteredDicts)) { + return Result.ok(new ArrayList<>()); + } + List 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 itemPage = sysDictItemService.page(new Page<>(pageNo, pageSize), itemWrapper); + List itemList = itemPage.getRecords(); + if (oConvertUtils.isEmpty(itemList)) { + return Result.ok(new ArrayList<>()); + } + + List dictIds = itemList.stream().map(SysDictItem::getDictId).distinct().collect(java.util.stream.Collectors.toList()); + LambdaQueryWrapper dictQuery = new LambdaQueryWrapper<>(); + dictQuery.in(SysDict::getId, dictIds); + dictQuery.eq(SysDict::getDelFlag, CommonConstant.DEL_FLAG_0); + List dictList = sysDictService.list(dictQuery); + Map dictMap = dictList.stream().collect(java.util.stream.Collectors.toMap(SysDict::getId, d -> d)); + + List> resultList = new ArrayList<>(); + for (SysDictItem item : itemList) { + SysDict dict = dictMap.get(item.getDictId()); + if (dict == null) { + continue; + } + Map 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 diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysLogController.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysLogController.java index 05a2ead..e60a5ec 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysLogController.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysLogController.java @@ -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 { @Autowired private ISysLogService sysLogService; + @PostMapping("/scada/addLoginLog") + @Operation(summary = "SCADA-免登录写入登录日志") + public Result scadaAddLoginLog(@RequestBody java.util.Map payload, HttpServletRequest request) { + payload.put("category", "LOGIN"); + return scadaAddLog(payload, request); + } + + @PostMapping("/scada/addLog") + @Operation(summary = "SCADA-免登录写入通用日志") + public Result scadaAddLog(@RequestBody java.util.Map 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时报错 */ diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysUserController.java b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysUserController.java index 51aa698..087f90e 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysUserController.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/system/controller/SysUserController.java @@ -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 @@ -109,6 +117,66 @@ public class SysUserController { private JeecgRedisClient jeecgRedisClient; @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); + } + } /** * 获取租户下用户数据(支持租户隔离) @@ -119,6 +187,7 @@ public class SysUserController { * @return */ @PermissionData(pageComponent = "system/UserList") + @Operation(summary = "用户管理-分页查询") @RequestMapping(value = "/list", method = RequestMethod.GET) public Result> 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 add(@RequestBody JSONObject jsonObject) { Result result = new Result(); @@ -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 edit(@RequestBody JSONObject jsonObject) { Result result = new Result(); @@ -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 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 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 resetPassword(@RequestParam(name = "usernames") String usernames) { Result result = new Result(); @@ -344,6 +424,7 @@ public class SysUserController { } @RequiresPermissions("system:user:queryById") + @Operation(summary = "用户管理-根据ID查询") @RequestMapping(value = "/queryById", method = RequestMethod.GET) public Result queryById(@RequestParam(name = "id", required = true) String id) { Result result = new Result(); @@ -384,6 +465,7 @@ public class SysUserController { * @return */ @RequestMapping(value = "/checkOnlyUser", method = RequestMethod.GET) + @Operation(summary = "用户管理-校验账号唯一") public Result checkOnlyUser(SysUser sysUser) { Result 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>> 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 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 userPage = sysUserService.page(new Page<>(pageNo, pageSize), queryWrapper); + List userList = userPage.getRecords(); + + List> resultList = new ArrayList<>(); + for (SysUser user : userList) { + List> departList = new ArrayList<>(); + List> companyList = new ArrayList<>(); + List> tenantList = new ArrayList<>(); + + if (Boolean.TRUE.equals(includeDetail)) { + List userDeparts = sysDepartService.queryUserDeparts(user.getId()); + for (SysDepart depart : userDeparts) { + Map 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 userTenants = userTenantService.getTenantListByUserId(user.getId(), Collections.singletonList(CommonConstant.STATUS_1)); + for (SysUserTenantVo tenantVo : userTenants) { + Map 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 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); + } + /** * 查询指定用户和部门关联的数据 * diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/pom.xml b/jeecg-boot/jeecg-module-system/jeecg-system-start/pom.xml index 2e4dfff..4a42567 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-start/pom.xml +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/pom.xml @@ -36,6 +36,12 @@ jeecg-module-xslmes ${jeecgboot.version} + + + org.jeecgframework.boot3 + jeecg-module-device-sync + ${jeecgboot.version} + diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/java/org/jeecg/JeecgSystemApplication.java b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/java/org/jeecg/JeecgSystemApplication.java index 0fe431e..3370680 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/java/org/jeecg/JeecgSystemApplication.java +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/java/org/jeecg/JeecgSystemApplication.java @@ -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 defaultProperties = new HashMap<>(); defaultProperties.put("management.health.elasticsearch.enabled", false); diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml index a55d056..de56ad4 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml @@ -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存储和大鱼短信秘钥配置 diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dm8.yml b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dm8.yml index 1190f74..c8ecf5a 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dm8.yml +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-dm8.yml @@ -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: ?? diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-docker.yml b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-docker.yml index 858fd0a..3022f69 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-docker.yml +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-docker.yml @@ -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: ?? diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-kingbase8.yml b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-kingbase8.yml index 3d6ad38..dce1993 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-kingbase8.yml +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-kingbase8.yml @@ -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: ?? diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-oracle.yml b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-oracle.yml index 7b25046..a8a15c0 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-oracle.yml +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-oracle.yml @@ -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: ?? diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-postgresql.yml b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-postgresql.yml index 44b0b0c..53b8607 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-postgresql.yml +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-postgresql.yml @@ -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: ?? diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-prod.yml b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-prod.yml index e77658e..d6d1ddb 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-prod.yml +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-prod.yml @@ -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存储和大鱼短信秘钥配置 diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-sqlserver.yml b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-sqlserver.yml index 5292ebd..3e95a50 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-sqlserver.yml +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-sqlserver.yml @@ -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: ?? diff --git a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-test.yml b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-test.yml index 4ad7ea0..8d05f71 100644 --- a/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-test.yml +++ b/jeecg-boot/jeecg-module-system/jeecg-system-start/src/main/resources/application-test.yml @@ -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存储和大鱼短信秘钥配置 diff --git a/jeecg-boot/pom.xml b/jeecg-boot/pom.xml index f820f64..1c6d999 100644 --- a/jeecg-boot/pom.xml +++ b/jeecg-boot/pom.xml @@ -88,6 +88,7 @@ jeecg-boot-base-core jeecg-module-system jeecg-boot-module + jeecg-module-device-sync diff --git a/jeecgboot-vue3/pnpm-lock.yaml b/jeecgboot-vue3/pnpm-lock.yaml index 49050c5..25f0a98 100644 --- a/jeecgboot-vue3/pnpm-lock.yaml +++ b/jeecgboot-vue3/pnpm-lock.yaml @@ -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==} diff --git a/qhmes.code-workspace b/qhmes.code-workspace index 6bf67e9..dfe0cf2 100644 --- a/qhmes.code-workspace +++ b/qhmes.code-workspace @@ -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" + } } diff --git a/yy-admin-master/.gitignore b/yy-admin-master/.gitignore new file mode 100644 index 0000000..39aaadf --- /dev/null +++ b/yy-admin-master/.gitignore @@ -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/ diff --git a/yy-admin-master/.vscode/launch.json b/yy-admin-master/.vscode/launch.json new file mode 100644 index 0000000..e6c70e4 --- /dev/null +++ b/yy-admin-master/.vscode/launch.json @@ -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 + } + ] +} diff --git a/yy-admin-master/.vscode/run-yyadmin.ps1 b/yy-admin-master/.vscode/run-yyadmin.ps1 new file mode 100644 index 0000000..81dc708 --- /dev/null +++ b/yy-admin-master/.vscode/run-yyadmin.ps1 @@ -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 +} diff --git a/yy-admin-master/.vscode/settings.json b/yy-admin-master/.vscode/settings.json new file mode 100644 index 0000000..35880a7 --- /dev/null +++ b/yy-admin-master/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "terminal.integrated.env.windows": { + "PATH": "C:\\Program Files\\dotnet;${env:PATH}" + } +} \ No newline at end of file diff --git a/yy-admin-master/.vscode/tasks.json b/yy-admin-master/.vscode/tasks.json new file mode 100644 index 0000000..d386d87 --- /dev/null +++ b/yy-admin-master/.vscode/tasks.json @@ -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" + } + ] +} diff --git a/yy-admin-master/LICENSE b/yy-admin-master/LICENSE new file mode 100644 index 0000000..c71412e --- /dev/null +++ b/yy-admin-master/LICENSE @@ -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. diff --git a/yy-admin-master/README.en.md b/yy-admin-master/README.en.md new file mode 100644 index 0000000..2401a5d --- /dev/null +++ b/yy-admin-master/README.en.md @@ -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/) diff --git a/yy-admin-master/README.md b/yy-admin-master/README.md new file mode 100644 index 0000000..fe6e26a --- /dev/null +++ b/yy-admin-master/README.md @@ -0,0 +1,96 @@ +# YY-Admin WPF 后端通用框架(持续维护中,后续会完整实现RBAC权限管理) +## 🍎效果截图 + + + + + + +
+ +## 📙介绍 + + +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 管理框架! +## 开源不易求打赏!!! +
+ 打赏码1 + 打赏码2 +
+## 联系作者微信 +
+ 联系作者微信 +
\ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Attribute/BindDescriptionAttribute.cs b/yy-admin-master/YY.Admin.Core/Attribute/BindDescriptionAttribute.cs new file mode 100644 index 0000000..8659bec --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Attribute/BindDescriptionAttribute.cs @@ -0,0 +1,79 @@ +using System.Windows.Controls; +using System.Windows; + +namespace YY.Admin.Core +{ + /// + /// DataGrid绑定数据源描述 + /// + public class BindDescriptionAttribute : Attribute + { + /// + /// 列名 + /// + public string HeaderName { get; set; } + + /// + /// 显示为 + /// + public ShowScheme ShowAs { get; set; } + + /// + /// 显示顺序 + /// + public int DisplayIndex { get; set; } + + /// + /// DataGrid列绑定属性名称 + /// + public string PropertyName { get; set; } + + /// + /// 应用内的容模板Key + /// + public string ResourceKey { get; set; } + + /// + /// 列宽 + /// + public DataGridLength Width { get; set; } + + /// + /// 列宽ByGrid + /// + public GridLength CloumnWidth { get; set; } + + + /// + /// DataGrid绑定数据源描述 + /// + /// 列名 + /// 显示为 + /// 宽度 + /// 显示顺序 + /// 自定义列Key + 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)}参数!"); + } + } + + /// + /// 展示方式 + /// + public enum ShowScheme + { + 普通文本 = 1, + 自定义 = 4 + } +} diff --git a/yy-admin-master/YY.Admin.Core/Attribute/ConstAttribute.cs b/yy-admin-master/YY.Admin.Core/Attribute/ConstAttribute.cs new file mode 100644 index 0000000..1239640 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Attribute/ConstAttribute.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core +{ + /// + /// 常量特性 + /// + [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = true)] + public class ConstAttribute : Attribute + { + public string Name { get; set; } + + public ConstAttribute(string name) + { + Name = name; + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Attribute/Dependencyinterface.cs b/yy-admin-master/YY.Admin.Core/Attribute/Dependencyinterface.cs new file mode 100644 index 0000000..d58d576 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Attribute/Dependencyinterface.cs @@ -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 { } +} diff --git a/yy-admin-master/YY.Admin.Core/Attribute/IgnoreTableAttribute.cs b/yy-admin-master/YY.Admin.Core/Attribute/IgnoreTableAttribute.cs new file mode 100644 index 0000000..b0de611 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Attribute/IgnoreTableAttribute.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core +{ + /// + /// 忽略表结构初始化特性(标记在实体) + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] + public class IgnoreTableAttribute : Attribute + { + } +} diff --git a/yy-admin-master/YY.Admin.Core/Attribute/IgnoreUpdateSeedAttribute.cs b/yy-admin-master/YY.Admin.Core/Attribute/IgnoreUpdateSeedAttribute.cs new file mode 100644 index 0000000..688bf05 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Attribute/IgnoreUpdateSeedAttribute.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core +{ + /// + /// 忽略更新种子特性(标记在种子类) + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] + public class IgnoreUpdateSeedAttribute : Attribute + { + } +} diff --git a/yy-admin-master/YY.Admin.Core/Attribute/IgnoreUpdateSeedColumnAttribute.cs b/yy-admin-master/YY.Admin.Core/Attribute/IgnoreUpdateSeedColumnAttribute.cs new file mode 100644 index 0000000..b21216b --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Attribute/IgnoreUpdateSeedColumnAttribute.cs @@ -0,0 +1,11 @@ + +namespace YY.Admin.Core +{ + /// + /// 忽略更新种子列特性(标记在实体属性) + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)] + public class IgnoreUpdateSeedColumnAttribute : Attribute + { + } +} diff --git a/yy-admin-master/YY.Admin.Core/Attribute/IncreSeedAttribute.cs b/yy-admin-master/YY.Admin.Core/Attribute/IncreSeedAttribute.cs new file mode 100644 index 0000000..612650c --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Attribute/IncreSeedAttribute.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core +{ + /// + /// 增量种子特性 + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] + public class IncreSeedAttribute : Attribute + { + } +} diff --git a/yy-admin-master/YY.Admin.Core/Attribute/IncreTableAttribute.cs b/yy-admin-master/YY.Admin.Core/Attribute/IncreTableAttribute.cs new file mode 100644 index 0000000..8d711be --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Attribute/IncreTableAttribute.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core +{ + /// + /// 增量表特性 + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] + public class IncreTableAttribute : Attribute + { + } +} diff --git a/yy-admin-master/YY.Admin.Core/Attribute/LifecycleAttribute.cs b/yy-admin-master/YY.Admin.Core/Attribute/LifecycleAttribute.cs new file mode 100644 index 0000000..0431b51 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Attribute/LifecycleAttribute.cs @@ -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; + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Attribute/LogTableAttribute.cs b/yy-admin-master/YY.Admin.Core/Attribute/LogTableAttribute.cs new file mode 100644 index 0000000..90c1f0b --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Attribute/LogTableAttribute.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core +{ + /// + /// 日志表特性 + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] + public class LogTableAttribute : Attribute + { + } +} diff --git a/yy-admin-master/YY.Admin.Core/Attribute/OperateCodeAttribute.cs b/yy-admin-master/YY.Admin.Core/Attribute/OperateCodeAttribute.cs new file mode 100644 index 0000000..b4368a3 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Attribute/OperateCodeAttribute.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core +{ + /// + /// 表示查询操作的前台操作码 + /// + public class OperateCodeAttribute : Attribute + { + /// + /// 初始化一个类型的新实例 + /// + public OperateCodeAttribute(string code) + { + Code = code; + } + + /// + /// 获取 属性名称 + /// + public string Code { get; private set; } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Attribute/OwnerUserAttribute.cs b/yy-admin-master/YY.Admin.Core/Attribute/OwnerUserAttribute.cs new file mode 100644 index 0000000..6d0e3d1 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Attribute/OwnerUserAttribute.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core +{ + /// + /// 所属用户数据权限 + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)] + public class OwnerUserAttribute : Attribute + { + } +} diff --git a/yy-admin-master/YY.Admin.Core/Attribute/QueryRuleAttribute.cs b/yy-admin-master/YY.Admin.Core/Attribute/QueryRuleAttribute.cs new file mode 100644 index 0000000..a0d4b09 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Attribute/QueryRuleAttribute.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core +{ + /// + /// 查询规则特性 + /// + [AttributeUsage(AttributeTargets.Property)] + public class QueryRuleAttribute : Attribute + { + /// + /// 查询字段名称 + /// + public string FieldName { get; set; } + + /// + /// 分组名称 + /// + public string Group { get; set; } + + /// + /// 查询操作符 + /// + public FilterOperateEnum Operate { get; set; } + + /// + /// 分组查询操作符(生成sql后面的where 带括号的查询,取值只能为or 或 and) + /// + public FilterOperateEnum GroupOperate { get; set; } + + /// + /// 查询规则构造函数 + /// + /// 操作符 + /// 数据库可接受的查询字段名称,未传直接取属性名称 + /// 隶属分组 + /// 分组查询操作符 + public QueryRuleAttribute(FilterOperateEnum operate, string fieldName, string group = "", FilterOperateEnum groupOperate = FilterOperateEnum.And) + { + FieldName = fieldName; + Group = group; + Operate = operate; + GroupOperate = groupOperate; + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Attribute/SeedDataAttribute.cs b/yy-admin-master/YY.Admin.Core/Attribute/SeedDataAttribute.cs new file mode 100644 index 0000000..a5411b1 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Attribute/SeedDataAttribute.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core +{ + /// + /// 种子数据特性 + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] + public class SeedDataAttribute : Attribute + { + /// + /// 排序(越大越后执行) + /// + public int Order { get; set; } = 0; + + public SeedDataAttribute(int orderNo) + { + Order = orderNo; + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Attribute/SysTableAttribute.cs b/yy-admin-master/YY.Admin.Core/Attribute/SysTableAttribute.cs new file mode 100644 index 0000000..2171538 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Attribute/SysTableAttribute.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core +{ + /// + /// 系统表特性 + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] + public class SysTableAttribute : Attribute + { + } +} diff --git a/yy-admin-master/YY.Admin.Core/Attribute/ThemeAttribute.cs b/yy-admin-master/YY.Admin.Core/Attribute/ThemeAttribute.cs new file mode 100644 index 0000000..a91c3cc --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Attribute/ThemeAttribute.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core +{ + /// + /// 枚举拓展主题样式 + /// + [AttributeUsage(AttributeTargets.Enum | AttributeTargets.Field)] + public class ThemeAttribute : Attribute + { + public string Theme { get; private set; } + + public ThemeAttribute(string theme) + { + this.Theme = theme; + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Behavior/TreeViewItemClickBehavior.cs b/yy-admin-master/YY.Admin.Core/Behavior/TreeViewItemClickBehavior.cs new file mode 100644 index 0000000..ae748fe --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Behavior/TreeViewItemClickBehavior.cs @@ -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(dep) != null) return; + + // 找到真正的 TreeViewItem + var tvi = FindAncestor(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); + } + } + + + /// + /// 折叠同级其它 TreeViewItem + /// + 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(DependencyObject current) where T : DependencyObject + { + while (current != null) + { + if (current is T t) return t; + current = VisualTreeHelper.GetParent(current); + } + return null; + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/BusinessException/BusinessException.cs b/yy-admin-master/YY.Admin.Core/BusinessException/BusinessException.cs new file mode 100644 index 0000000..f4398f5 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/BusinessException/BusinessException.cs @@ -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); + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Cache/ISysCacheService.cs b/yy-admin-master/YY.Admin.Core/Cache/ISysCacheService.cs new file mode 100644 index 0000000..8549a6f --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Cache/ISysCacheService.cs @@ -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 + { + /// + /// 申请分布式锁 + /// + IDisposable? BeginCacheLock(string key, int msTimeout = 500, int msExpire = 10000, bool throwOnFailure = true); + + /// + /// 获取缓存键名集合 + /// + List GetKeyList(); + + /// + /// 增加缓存 + /// + bool Set(string key, object value); + + /// + /// 增加缓存并设置过期时间 + /// + bool Set(string key, object value, TimeSpan expire); + + /// + /// 异步获取或添加缓存(无参数) + /// + Task AdGetAsync(string cacheName, Func> del, TimeSpan? expiry = default) where TR : class; + + /// + /// 异步获取或添加缓存(1个参数) + /// + Task AdGetAsync(string cacheName, Func> del, T1 t1, TimeSpan? expiry = default) where TR : class; + + /// + /// 异步获取或添加缓存(2个参数) + /// + Task AdGetAsync(string cacheName, Func> del, T1 t1, T2 t2, TimeSpan? expiry = default) where TR : class; + + /// + /// 异步获取或添加缓存(3个参数) + /// + Task AdGetAsync(string cacheName, Func> del, T1 t1, T2 t2, T3 t3, TimeSpan? expiry = default) where TR : class; + + /// + /// 获取缓存(1个参数) + /// + T Get(string cacheName, object t1); + + /// + /// 获取缓存(2个参数) + /// + T Get(string cacheName, object t1, object t2); + + /// + /// 获取缓存(3个参数) + /// + T Get(string cacheName, object t1, object t2, object t3); + + /// + /// 获取缓存的剩余生存时间 + /// + TimeSpan GetExpire(string key); + + /// + /// 获取缓存 + /// + T Get(string key); + + /// + /// 删除缓存 + /// + int Remove(string key); + + /// + /// 清空所有缓存 + /// + void Clear(); + + /// + /// 检查缓存是否存在 + /// + bool ExistKey(string key); + + /// + /// 根据键名前缀删除缓存 + /// + int RemoveByPrefixKey(string prefixKey); + + /// + /// 根据键名前缀获取键名集合 + /// + List GetKeysByPrefixKey(string prefixKey); + + /// + /// 获取缓存值(原始对象) + /// + object GetValue(string key); + + /// + /// 获取或添加缓存(在数据不存在时执行委托请求数据) + /// + T GetOrAdd(string key, Func callback, int expire = -1); + + /// + /// 获取Hash缓存字典 + /// + IDictionary GetHashMap(string key); + + /// + /// 批量添加Hash值 + /// + bool HashSet(string key, Dictionary dic); + + /// + /// 添加一条Hash值 + /// + void HashAdd(string key, string hashKey, T value); + + /// + /// 添加或更新一条Hash值 + /// + void HashAddOrUpdate(string key, string hashKey, T value); + + /// + /// 获取多条Hash值 + /// + List HashGet(string key, params string[] fields); + + /// + /// 获取一条Hash值 + /// + T HashGetOne(string key, string field); + + /// + /// 根据KEY获取所有Hash值 + /// + IDictionary HashGetAll(string key); + + /// + /// 删除Hash值 + /// + int HashDel(string key, params string[] fields); + } +} diff --git a/yy-admin-master/YY.Admin.Core/Cache/SysCacheService.cs b/yy-admin-master/YY.Admin.Core/Cache/SysCacheService.cs new file mode 100644 index 0000000..c4e5e19 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Cache/SysCacheService.cs @@ -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; + } + + /// + /// 申请分布式锁 🔖 + /// + /// 要锁定的key + /// 申请锁等待的时间,单位毫秒 + /// 锁过期时间,超过该时间没有主动是放则自动是放,必须整数秒,单位毫秒 + /// 失败时是否抛出异常,如不抛出异常,可通过判断返回null得知申请锁失败 + /// + [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; + } + } + + /// + /// 获取缓存键名集合 🔖 + /// + /// + [DisplayName("获取缓存键名集合")] + public List 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)]; + } + + /// + /// 增加缓存 + /// + /// + /// + /// + + public bool Set(string key, object value) + { + return !string.IsNullOrWhiteSpace(key) && _cacheProvider.Cache.Set($"{_cacheOptions.Prefix}{key}", value); + } + + /// + /// 增加缓存并设置过期时间 + /// + /// + /// + /// + /// + + public bool Set(string key, object value, TimeSpan expire) + { + return !string.IsNullOrWhiteSpace(key) && _cacheProvider.Cache.Set($"{_cacheOptions.Prefix}{key}", value, expire); + } + + public async Task AdGetAsync(String cacheName, Func> del, TimeSpan? expiry = default) where TR : class + { + return await AdGetAsync(cacheName, del, [], expiry); + } + + public async Task AdGetAsync(String cacheName, Func> del, T1 t1, TimeSpan? expiry = default) where TR : class + { + return await AdGetAsync(cacheName, del, [t1], expiry); + } + + public async Task AdGetAsync(String cacheName, Func> del, T1 t1, T2 t2, TimeSpan? expiry = default) where TR : class + { + return await AdGetAsync(cacheName, del, [t1, t2], expiry); + } + + public async Task AdGetAsync(String cacheName, Func> del, T1 t1, T2 t2, T3 t3, TimeSpan? expiry = default) where TR : class + { + return await AdGetAsync(cacheName, del, [t1, t2, t3], expiry); + } + + private async Task AdGetAsync(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(key); + value ??= await ((dynamic)del).DynamicInvokeAsync(obs); + Set(key, value); + return value; + } + } + + public T Get(String cacheName, object t1) + { + return Get(cacheName, [t1]); + } + + public T Get(String cacheName, object t1, object t2) + { + return Get(cacheName, [t1, t2]); + } + + public T Get(String cacheName, object t1, object t2, object t3) + { + return Get(cacheName, [t1, t2, t3]); + } + + private T Get(String cacheName, Object[] obs) + { + var key = cacheName + ":" + obs.Aggregate(string.Empty, (current, o) => current + $"<{o}>"); + return Get(key); + } + + private static string Key(string cacheName, object[] obs) + { + if (obs.OfType().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(); + } + + /// + /// 获取缓存的剩余生存时间 + /// + /// + /// + + public TimeSpan GetExpire(string key) + { + return _cacheProvider.Cache.GetExpire(key); + } + + /// + /// 获取缓存 + /// + /// + /// + /// + + public T Get(string key) + { + return _cacheProvider.Cache.Get($"{_cacheOptions.Prefix}{key}"); + } + + /// + /// 删除缓存 🔖 + /// + /// + /// + [DisplayName("删除缓存")] + public int Remove(string key) + { + return _cacheProvider.Cache.Remove($"{_cacheOptions.Prefix}{key}"); + } + + /// + /// 清空所有缓存 🔖 + /// + /// + [DisplayName("清空所有缓存")] + public void Clear() + { + _cacheProvider.Cache.Clear(); + + Cache.Default.Clear(); + } + + /// + /// 检查缓存是否存在 + /// + /// 键 + /// + + public bool ExistKey(string key) + { + return _cacheProvider.Cache.ContainsKey($"{_cacheOptions.Prefix}{key}"); + } + + /// + /// 根据键名前缀删除缓存 🔖 + /// + /// 键名前缀 + /// + [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); + } + + /// + /// 根据键名前缀获取键名集合 🔖 + /// + /// 键名前缀 + /// + [DisplayName("根据键名前缀获取键名集合")] + public List 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(); + } + + /// + /// 获取缓存值 🔖 + /// + /// + /// + [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(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(fullKey); + + case "list": + var list = redisCache.GetList(fullKey); + return list?.ToList(); + + case "hash": + var hash = redisCache.GetDictionary(fullKey); + return hash?.ToDictionary(k => k.Key, v => v.Value); + + case "set": + var set = redisCache.GetSet(fullKey); + return set?.ToArray(); + + case "zset": + var sortedSet = redisCache.GetSortedSet(fullKey); + return sortedSet?.Range(0, -1)?.ToList(); + + case "none": + return null; + + default: + // 未知类型或特殊类型 + return new Dictionary + { + { "key", key }, + { "type", keyType ?? "unknown" }, + { "message", "无法使用标准方式获取此类型数据" } + }; + } + } + catch (Exception ex) + { + return new Dictionary + { + { "key", key }, + { "error", ex.Message }, + { "type", "exception" } + }; + } + } + + return _cacheProvider.Cache.Get(fullKey); + } + + /// + /// 获取或添加缓存(在数据不存在时执行委托请求数据) + /// + /// + /// + /// + /// 过期时间,单位秒 + /// + + public T GetOrAdd(string key, Func callback, int expire = -1) + { + if (string.IsNullOrWhiteSpace(key)) return default; + return _cacheProvider.Cache.GetOrAdd($"{_cacheOptions.Prefix}{key}", callback, expire); + } + + /// + /// Hash匹配 + /// + /// + /// + /// + + public IDictionary GetHashMap(string key) + { + return _cacheProvider.Cache.GetDictionary(key); + } + + /// + /// 批量添加HASH + /// + /// + /// + /// + /// + + public bool HashSet(string key, Dictionary dic) + { + var hash = GetHashMap(key); + foreach (var v in dic) + { + hash.Add(v); + } + return true; + } + + /// + /// 添加一条HASH + /// + /// + /// + /// + /// + + public void HashAdd(string key, string hashKey, T value) + { + var hash = GetHashMap(key); + hash.Add(hashKey, value); + } + + /// + /// 添加或更新一条HASH + /// + /// + /// + /// + /// + + public void HashAddOrUpdate(string key, string hashKey, T value) + { + var hash = GetHashMap(key); + if (hash.ContainsKey(hashKey)) + hash[hashKey] = value; + else + hash.Add(hashKey, value); + } + + /// + /// 获取多条HASH + /// + /// + /// + /// + /// + + public List HashGet(string key, params string[] fields) + { + var hash = GetHashMap(key); + return hash.Where(t => fields.Any(c => t.Key == c)).Select(t => t.Value).ToList(); + } + + /// + /// 获取一条HASH + /// + /// + /// + /// + /// + + public T HashGetOne(string key, string field) + { + var hash = GetHashMap(key); + return hash.TryGetValue(field, out T value) ? value : default; + } + + /// + /// 根据KEY获取所有HASH + /// + /// + /// + /// + + public IDictionary HashGetAll(string key) + { + var hash = GetHashMap(key); + return hash; + } + + /// + /// 删除HASH + /// + /// + /// + /// + /// + + public int HashDel(string key, params string[] fields) + { + var hash = GetHashMap(key); + fields.ToList().ForEach(t => hash.Remove(t)); + return fields.Length; + } + + } +} diff --git a/yy-admin-master/YY.Admin.Core/Const/CacheConst.cs b/yy-admin-master/YY.Admin.Core/Const/CacheConst.cs new file mode 100644 index 0000000..9ec11f7 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Const/CacheConst.cs @@ -0,0 +1,124 @@ + + +namespace YY.Admin.Core.Const; + +/// +/// 缓存相关常量 +/// +public class CacheConst +{ + /// + /// 用户权限缓存(按钮集合) + /// + public const string KeyUserButton = "sys_user_button:"; + + /// + /// 用户机构缓存 + /// + public const string KeyUserOrg = "sys_user_org:"; + + /// + /// 角色最大数据范围缓存 + /// + public const string KeyRoleMaxDataScope = "sys_role_maxDataScope:"; + + /// + /// 在线用户缓存 + /// + public const string KeyUserOnline = "sys_user_online:"; + + /// + /// 图形验证码缓存 + /// + public const string KeyVerCode = "sys_verCode:"; + + /// + /// 手机验证码缓存 + /// + public const string KeyPhoneVerCode = "sys_phoneVerCode:"; + + /// + /// 密码错误次数缓存 + /// + public const string KeyPasswordErrorTimes = "sys_password_error_times:"; + + /// + /// 租户缓存 + /// + public const string KeyTenant = "sys_tenant"; + + /// + /// 常量下拉框 + /// + public const string KeyConst = "sys_const:"; + + /// + /// 所有缓存关键字集合 + /// + public const string KeyAll = "sys_keys"; + + /// + /// SqlSugar二级缓存 + /// + public const string SqlSugar = "sys_sqlSugar:"; + + /// + /// 开放接口身份缓存 + /// + public const string KeyOpenAccess = "sys_open_access:"; + + /// + /// 开放接口身份随机数缓存 + /// + public const string KeyOpenAccessNonce = "sys_open_access_nonce:"; + + /// + /// 登录黑名单 + /// + public const string KeyBlacklist = "sys_blacklist:"; + + /// + /// 系统配置缓存 + /// + public const string KeyConfig = "sys_config:"; + + /// + /// 系统租户配置缓存 + /// + public const string KeyTenantConfig = "sys_tenant_config:"; + + /// + /// 系统用户配置缓存 + /// + public const string KeyUserConfig = "sys_user_config:"; + + /// + /// 系统字典缓存 + /// + public const string KeyDict = "sys_dict:"; + + /// + /// 系统租户字典缓存 + /// + public const string KeyTenantDict = "sys_tenant_dict:"; + + /// + /// 重复请求(幂等)字典缓存 + /// + public const string KeyIdempotent = "sys_idempotent:"; + + /// + /// Excel临时文件缓存 + /// + public const string KeyExcelTemp = "sys_excel_temp:"; + + /// + /// 系统更新命令日志缓存 + /// + public const string KeySysUpdateLog = "sys_update_log"; + + /// + /// 系统更新间隔标记缓存 + /// + public const string KeySysUpdateInterval = "sys_update_interval"; +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Const/ClaimConst.cs b/yy-admin-master/YY.Admin.Core/Const/ClaimConst.cs new file mode 100644 index 0000000..058fbd8 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Const/ClaimConst.cs @@ -0,0 +1,64 @@ + + +namespace YY.Admin.Core.Const; + +/// +/// Claim相关常量 +/// +public class ClaimConst +{ + /// + /// 用户Id + /// + public const string UserId = "UserId"; + + /// + /// 账号 + /// + public const string Account = "Account"; + + /// + /// 真实姓名 + /// + public const string RealName = "RealName"; + + /// + /// 昵称 + /// + public const string NickName = "NickName"; + + /// + /// 账号类型 + /// + public const string AccountType = "AccountType"; + + /// + /// 租户Id + /// + public const string TenantId = "TenantId"; + + /// + /// 组织机构Id + /// + public const string OrgId = "OrgId"; + + /// + /// 组织机构名称 + /// + public const string OrgName = "OrgName"; + + /// + /// 组织机构类型 + /// + public const string OrgType = "OrgType"; + + /// + /// 微信OpenId + /// + public const string OpenId = "OpenId"; + + /// + /// 登录模式PC、APP + /// + public const string LoginMode = "LoginMode"; +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Const/CommonConst.cs b/yy-admin-master/YY.Admin.Core/Const/CommonConst.cs new file mode 100644 index 0000000..48db59f --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Const/CommonConst.cs @@ -0,0 +1,53 @@ +namespace YY.Admin.Core.Const; + +/// +/// 通用常量 +/// +[Const("平台配置")] +public class CommonConst +{ + /// + /// 日志分组名称 + /// + public const string SysLogCategoryName = "System.Logging.LoggingMonitor"; + + /// + /// 事件-增加异常日志 + /// + public const string AddExLog = "Add:ExLog"; + + /// + /// 事件-发送异常邮件 + /// + public const string SendErrorMail = "Send:ErrorMail"; + + /// + /// 默认基本角色名称 + /// + public const string DefaultBaseRoleName = "默认基本角色"; + + /// + /// 默认基本角色编码 + /// + public const string DefaultBaseRoleCode = "default_base_role"; + + /// + /// MainWinndow 主内容区域名称 + /// + public const string ContentRegion = "ContentRegion"; + + /// + /// MainWinndow 菜单区域名称 + /// + public const string MenuRegion = "MenuRegion"; + + /// + /// 系统名称 + /// + public const string SystemName = "智能制造MES工控"; + + /// + /// 系统设置文件路径 + /// + public const string AppSettingsFilePath = "AppSettings\\{0}\\appsettings.json"; +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Const/ConfigConst.cs b/yy-admin-master/YY.Admin.Core/Const/ConfigConst.cs new file mode 100644 index 0000000..6d1dedd --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Const/ConfigConst.cs @@ -0,0 +1,144 @@ + + +namespace YY.Admin.Core.Const; + +/// +/// 配置常量 +/// +public class ConfigConst +{ + /// + /// 演示环境 + /// + public const string SysDemoEnv = "sys_demo"; + + /// + /// 默认密码 + /// + public const string SysPassword = "sys_password"; + + /// + /// 密码最大错误次数 + /// + public const string SysPasswordMaxErrorTimes = "sys_password_max_error_times"; + + /// + /// 日志保留天数 + /// + public const string SysLogRetentionDays = "sys_log_retention_days"; + + /// + /// 记录操作日志 + /// + public const string SysOpLog = "sys_oplog"; + + /// + /// 单设备登录 + /// + public const string SysSingleLogin = "sys_single_login"; + + /// + /// 登入登出提醒 + /// + public const string SysLoginOutReminder = "sys_login_out_reminder"; + + /// + /// 登陆时隐藏租户 + /// + public const string SysHideTenantLogin = "sys_hide_tenant_login"; + + /// + /// 登录二次验证 + /// + public const string SysSecondVer = "sys_second_ver"; + + /// + /// 图形验证码 + /// + public const string SysCaptcha = "sys_captcha"; + + /// + /// Token过期时间 + /// + public const string SysTokenExpire = "sys_token_expire"; + + /// + /// RefreshToken过期时间 + /// + public const string SysRefreshTokenExpire = "sys_refresh_token_expire"; + + /// + /// 发送异常日志邮件 + /// + public const string SysErrorMail = "sys_error_mail"; + + /// + /// 域登录验证 + /// + public const string SysDomainLogin = "sys_domain_login"; + + // /// + // /// 租户域名隔离登录验证 + // /// + // public const string SysTenantHostLogin = "sys_tenant_host_login"; + + /// + /// 数据校验日志 + /// + public const string SysValidationLog = "sys_validation_log"; + + /// + /// 行政区域同步层级 1-省级,2-市级,3-区县级,4-街道级,5-村级 + /// + public const string SysRegionSyncLevel = "sys_region_sync_level"; + + /// + /// Default 分组 + /// + public const string SysDefaultGroup = "Default"; + + /// + /// 支付宝授权页面地址 + /// + public const string AlipayAuthPageUrl = "alipay_auth_page_url_"; + + // /// + // /// 系统图标 + // /// + // public const string SysWebLogo = "sys_web_logo"; + // + // /// + // /// 系统主标题 + // /// + // public const string SysWebTitle = "sys_web_title"; + // + // /// + // /// 系统副标题 + // /// + // public const string SysWebViceTitle = "sys_web_viceTitle"; + // + // /// + // /// 系统描述 + // /// + // public const string SysWebViceDesc = "sys_web_viceDesc"; + // + // /// + // /// 水印内容 + // /// + // public const string SysWebWatermark = "sys_web_watermark"; + // + // /// + // /// 版权说明 + // /// + // public const string SysWebCopyright = "sys_web_copyright"; + // + // /// + // /// ICP备案号 + // /// + // public const string SysWebIcp = "sys_web_icp"; + // + // /// + // /// ICP地址 + // /// + // public const string SysWebIcpUrl = "sys_web_icpUrl"; +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Const/SqlSugarConst.cs b/yy-admin-master/YY.Admin.Core/Const/SqlSugarConst.cs new file mode 100644 index 0000000..58f4906 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Const/SqlSugarConst.cs @@ -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 + { + /// + /// 默认主数据库标识(默认租户) + /// + public const string MainConfigId = "1300000000001"; + + /// + /// 默认日志数据库标识 + /// + public const string LogConfigId = "1300000000002"; + + /// + /// 默认表主键 + /// + public const string PrimaryKey = "Id"; + + /// + /// 默认租户Id + /// + public const long DefaultTenantId = 1300000000001; + } +} diff --git a/yy-admin-master/YY.Admin.Core/Controls/FontAwesomeIcon.cs b/yy-admin-master/YY.Admin.Core/Controls/FontAwesomeIcon.cs new file mode 100644 index 0000000..0c72cdb --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Controls/FontAwesomeIcon.cs @@ -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 + { + /// + /// 常规 + /// + Regular, + + /// + /// 实心的 + /// + Solid, + + /// + /// 品牌(Logo) + /// + 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; + } + } + } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Controls/GridPanel.cs b/yy-admin-master/YY.Admin.Core/Controls/GridPanel.cs new file mode 100644 index 0000000..e6a05cd --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Controls/GridPanel.cs @@ -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; + } +} diff --git a/yy-admin-master/YY.Admin.Core/Converter/BrushToSolidColorPaintConverter.cs b/yy-admin-master/YY.Admin.Core/Converter/BrushToSolidColorPaintConverter.cs new file mode 100644 index 0000000..0ceb0b6 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Converter/BrushToSolidColorPaintConverter.cs @@ -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(); + } + + } +} diff --git a/yy-admin-master/YY.Admin.Core/Converter/EnumDescriptionConverter.cs b/yy-admin-master/YY.Admin.Core/Converter/EnumDescriptionConverter.cs new file mode 100644 index 0000000..3fc9643 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Converter/EnumDescriptionConverter.cs @@ -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(); + return attribute?.Description ?? value.ToString()!; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Converter/EnumToBoolConverter.cs b/yy-admin-master/YY.Admin.Core/Converter/EnumToBoolConverter.cs new file mode 100644 index 0000000..ecea599 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Converter/EnumToBoolConverter.cs @@ -0,0 +1,60 @@ +using System.Globalization; +using System.Windows.Data; + +namespace YY.Admin.Core.Converter +{ + /// + /// 将枚举值和 bool 互转,适用于 ToggleButton 或 RadioButton + /// + public class EnumToBoolConverter : IValueConverter + { + /// + /// 将枚举值转换为 bool + /// + /// 绑定的枚举值 + /// + /// ConverterParameter 指定要匹配的枚举值 + /// + /// 匹配返回 true,否则 false + 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); + } + + /// + /// 将 bool 转回枚举值 + /// + /// ToggleButton.IsChecked + /// + /// ConverterParameter 指定对应的枚举值 + /// + /// 如果 true 返回枚举,否则返回 Binding.DoNothing + 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; + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Converter/EnumToIntConverter.cs b/yy-admin-master/YY.Admin.Core/Converter/EnumToIntConverter.cs new file mode 100644 index 0000000..0903540 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Converter/EnumToIntConverter.cs @@ -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; + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Converter/EnumToTagTypeConverter.cs b/yy-admin-master/YY.Admin.Core/Converter/EnumToTagTypeConverter.cs new file mode 100644 index 0000000..1d337de --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Converter/EnumToTagTypeConverter.cs @@ -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(); + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Converter/EnumToVisibilityConverter.cs b/yy-admin-master/YY.Admin.Core/Converter/EnumToVisibilityConverter.cs new file mode 100644 index 0000000..1162d66 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Converter/EnumToVisibilityConverter.cs @@ -0,0 +1,33 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace YY.Admin.Core.Converter +{ + /// + /// 将枚举值转换为Visibility + /// + public class EnumToVisibilityConverter : IValueConverter + { + /// + /// 将枚举值转换为 Visibility + /// + /// 绑定的枚举值 + /// + /// ConverterParameter 指定要匹配的枚举值 + /// + /// 匹配返回Visible + 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(); + } +} diff --git a/yy-admin-master/YY.Admin.Core/Converter/MaxWidthConverter.cs b/yy-admin-master/YY.Admin.Core/Converter/MaxWidthConverter.cs new file mode 100644 index 0000000..72e22cc --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Converter/MaxWidthConverter.cs @@ -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(); + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Converter/NegativeLeftThicknessConverter.cs b/yy-admin-master/YY.Admin.Core/Converter/NegativeLeftThicknessConverter.cs new file mode 100644 index 0000000..3799971 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Converter/NegativeLeftThicknessConverter.cs @@ -0,0 +1,49 @@ +using HandyControl.Data; +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace YY.Admin.Core.Converter +{ + /// + /// 根据 TitleWidth 计算 Margin.Left,如果 TitlePlacement=Top 则不偏移 + /// + 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(); + } +} diff --git a/yy-admin-master/YY.Admin.Core/Converter/PercentageConverter.cs b/yy-admin-master/YY.Admin.Core/Converter/PercentageConverter.cs new file mode 100644 index 0000000..a94dd56 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Converter/PercentageConverter.cs @@ -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(); + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Converter/RadioButtonEnumMultiConverter.cs b/yy-admin-master/YY.Admin.Core/Converter/RadioButtonEnumMultiConverter.cs new file mode 100644 index 0000000..94d454a --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Converter/RadioButtonEnumMultiConverter.cs @@ -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 }; + } + } + +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Core/Events/NetworkStatusChangedEvent.cs b/yy-admin-master/YY.Admin.Core/Core/Events/NetworkStatusChangedEvent.cs new file mode 100644 index 0000000..f0f92a2 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Core/Events/NetworkStatusChangedEvent.cs @@ -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 +{ +} diff --git a/yy-admin-master/YY.Admin.Core/Core/Events/RemoteCommandReceivedEvent.cs b/yy-admin-master/YY.Admin.Core/Core/Events/RemoteCommandReceivedEvent.cs new file mode 100644 index 0000000..ffcd101 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Core/Events/RemoteCommandReceivedEvent.cs @@ -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 +{ +} diff --git a/yy-admin-master/YY.Admin.Core/Core/Events/SyncCompletedEvent.cs b/yy-admin-master/YY.Admin.Core/Core/Events/SyncCompletedEvent.cs new file mode 100644 index 0000000..d793cc5 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Core/Events/SyncCompletedEvent.cs @@ -0,0 +1,7 @@ +using Prism.Events; + +namespace YY.Admin.Core.Events; + +public class SyncCompletedEvent : PubSubEvent +{ +} diff --git a/yy-admin-master/YY.Admin.Core/Core/Models/DeviceStatus.cs b/yy-admin-master/YY.Admin.Core/Core/Models/DeviceStatus.cs new file mode 100644 index 0000000..b3fa073 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Core/Models/DeviceStatus.cs @@ -0,0 +1,25 @@ +using SqlSugar; + +namespace YY.Admin.Core.Models; + +/// +/// 设备本地状态快照。 +/// +[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; +} diff --git a/yy-admin-master/YY.Admin.Core/Core/Models/OutboxMessage.cs b/yy-admin-master/YY.Admin.Core/Core/Models/OutboxMessage.cs new file mode 100644 index 0000000..c8b0163 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Core/Models/OutboxMessage.cs @@ -0,0 +1,43 @@ +using SqlSugar; + +namespace YY.Admin.Core.Models; + +/// +/// 断联续传消息实体。 +/// +[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; } +} diff --git a/yy-admin-master/YY.Admin.Core/Core/Services/IJeecgUserMirrorPullHandler.cs b/yy-admin-master/YY.Admin.Core/Core/Services/IJeecgUserMirrorPullHandler.cs new file mode 100644 index 0000000..ea4421a --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Core/Services/IJeecgUserMirrorPullHandler.cs @@ -0,0 +1,9 @@ +namespace YY.Admin.Core.Services; + +/// +/// 通过 REST(SCADA)拉取用户并写入本地 jeecg_sys_user,供 Outbox 统一线路消费。 +/// +public interface IJeecgUserMirrorPullHandler +{ + Task ExecutePullAsync(CancellationToken cancellationToken = default); +} diff --git a/yy-admin-master/YY.Admin.Core/Core/Services/IJeecgUserMirrorPullOutbox.cs b/yy-admin-master/YY.Admin.Core/Core/Services/IJeecgUserMirrorPullOutbox.cs new file mode 100644 index 0000000..8d09579 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Core/Services/IJeecgUserMirrorPullOutbox.cs @@ -0,0 +1,9 @@ +namespace YY.Admin.Core.Services; + +/// +/// 将「需拉取 Jeecg 用户镜像」写入 Outbox(断网不丢、联网续传),由 Services 侧协调器调用。 +/// +public interface IJeecgUserMirrorPullOutbox +{ + Task EnqueuePullAsync(string eventType, string? payloadJson, CancellationToken cancellationToken = default); +} diff --git a/yy-admin-master/YY.Admin.Core/Core/Services/INetworkMonitor.cs b/yy-admin-master/YY.Admin.Core/Core/Services/INetworkMonitor.cs new file mode 100644 index 0000000..1abc627 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Core/Services/INetworkMonitor.cs @@ -0,0 +1,8 @@ +namespace YY.Admin.Core.Services; + +public interface INetworkMonitor +{ + bool IsOnline { get; } + event Action? StatusChanged; + Task StartAsync(CancellationToken cancellationToken = default); +} diff --git a/yy-admin-master/YY.Admin.Core/Core/Services/ISignalRService.cs b/yy-admin-master/YY.Admin.Core/Core/Services/ISignalRService.cs new file mode 100644 index 0000000..b653705 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Core/Services/ISignalRService.cs @@ -0,0 +1,13 @@ +namespace YY.Admin.Core.Services; + +public interface ISignalRService +{ + Task ConnectAsync(string token, CancellationToken cancellationToken = default); + + /// + /// 设备同步统一通道:STOMP 订阅 /topic/sync/jeecg-users(免密或带设备 Token),与 Outbox+REST 同属一条规范线路。 + /// + Task ConnectUnifiedDeviceChannelAsync(CancellationToken cancellationToken = default); + + Task SendDeviceStatusAsync(object status, CancellationToken cancellationToken = default); +} diff --git a/yy-admin-master/YY.Admin.Core/Core/Sync/JeecgUserMirrorOutbox.cs b/yy-admin-master/YY.Admin.Core/Core/Sync/JeecgUserMirrorOutbox.cs new file mode 100644 index 0000000..e853bce --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Core/Sync/JeecgUserMirrorOutbox.cs @@ -0,0 +1,11 @@ +namespace YY.Admin.Core.Sync; + +/// +/// 用户镜像同步在 Outbox 中的聚合类型(与设备批量上报区分,走本地拉取 REST 而非 /sys/sync/batch)。 +/// +public static class JeecgUserMirrorOutbox +{ + public const string AggregateType = "JeecgUserMirror"; + public const string EventSignal = "Signal"; + public const string EventBoot = "Boot"; +} diff --git a/yy-admin-master/YY.Admin.Core/Entity/EntityBase.cs b/yy-admin-master/YY.Admin.Core/Entity/EntityBase.cs new file mode 100644 index 0000000..396c69c --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/EntityBase.cs @@ -0,0 +1,192 @@ + + +namespace YY.Admin.Core; + +/// +/// 框架实体基类Id +/// +public abstract class EntityBaseId +{ + /// + /// 雪花Id + /// + [SugarColumn(ColumnName = "Id", ColumnDescription = "主键Id", IsPrimaryKey = true, IsIdentity = false)] + public virtual long Id { get; set; } +} + +/// +/// 框架实体基类 +/// +[SugarIndex("index_{table}_CT", nameof(CreateTime), OrderByType.Asc)] +public abstract class EntityBase : EntityBaseId +{ + /// + /// 创建时间 + /// + [SugarColumn(ColumnDescription = "创建时间", IsNullable = true, IsOnlyIgnoreUpdate = true)] + public virtual DateTime CreateTime { get; set; } + + /// + /// 更新时间 + /// + [SugarColumn(ColumnDescription = "更新时间")] + public virtual DateTime? UpdateTime { get; set; } + + /// + /// 创建者Id + /// + [OwnerUser] + [SugarColumn(ColumnDescription = "创建者Id", IsOnlyIgnoreUpdate = true)] + public virtual long? CreateUserId { get; set; } + + ///// + ///// 创建者 + ///// + //[Newtonsoft.Json.JsonIgnore] + //[System.Text.Json.Serialization.JsonIgnore] + //[Navigate(NavigateType.OneToOne, nameof(CreateUserId))] + //public virtual SysUser CreateUser { get; set; } + + /// + /// 创建者姓名 + /// + [SugarColumn(ColumnDescription = "创建者姓名", Length = 64, IsOnlyIgnoreUpdate = true)] + public virtual string? CreateUserName { get; set; } + + /// + /// 修改者Id + /// + [SugarColumn(ColumnDescription = "修改者Id")] + public virtual long? UpdateUserId { get; set; } + + ///// + ///// 修改者 + ///// + //[Newtonsoft.Json.JsonIgnore] + //[System.Text.Json.Serialization.JsonIgnore] + //[Navigate(NavigateType.OneToOne, nameof(UpdateUserId))] + //public virtual SysUser UpdateUser { get; set; } + + /// + /// 修改者姓名 + /// + [SugarColumn(ColumnDescription = "修改者姓名", Length = 64)] + public virtual string? UpdateUserName { get; set; } +} + +/// +/// 框架实体基类(删除标志) +/// +[SugarIndex("index_{table}_D", nameof(IsDelete), OrderByType.Asc)] +public abstract class EntityBaseDel : EntityBase, IDeletedFilter +{ + /// + /// 软删除 + /// + [SugarColumn(ColumnDescription = "软删除")] + public virtual bool IsDelete { get; set; } = false; +} + +/// +/// 机构实体基类(数据权限) +/// +public abstract class EntityBaseOrg : EntityBase, IOrgIdFilter +{ + /// + /// 机构Id + /// + [SugarColumn(ColumnDescription = "机构Id", IsNullable = true)] + public virtual long OrgId { get; set; } + + ///// + ///// 创建者部门Id + ///// + //[SugarColumn(ColumnDescription = "创建者部门Id", IsOnlyIgnoreUpdate = true)] + //public virtual long? CreateOrgId { get; set; } + + ///// + ///// 创建者部门 + ///// + //[Newtonsoft.Json.JsonIgnore] + //[System.Text.Json.Serialization.JsonIgnore] + //[Navigate(NavigateType.OneToOne, nameof(CreateOrgId))] + //public virtual SysOrg CreateOrg { get; set; } + + ///// + ///// 创建者部门名称 + ///// + //[SugarColumn(ColumnDescription = "创建者部门名称", Length = 64, IsOnlyIgnoreUpdate = true)] + //public virtual string? CreateOrgName { get; set; } +} + +/// +/// 机构实体基类(数据权限、删除标志) +/// +public abstract class EntityBaseOrgDel : EntityBaseDel, IOrgIdFilter +{ + /// + /// 机构Id + /// + [SugarColumn(ColumnDescription = "机构Id", IsNullable = true)] + public virtual long OrgId { get; set; } +} + +/// +/// 租户实体基类 +/// +public abstract class EntityBaseTenant : EntityBase, ITenantIdFilter +{ + /// + /// 租户Id + /// + [SugarColumn(ColumnDescription = "租户Id", IsOnlyIgnoreUpdate = true)] + public virtual long? TenantId { get; set; } +} + +/// +/// 租户实体基类(删除标志) +/// +public abstract class EntityBaseTenantDel : EntityBaseDel, ITenantIdFilter +{ + /// + /// 租户Id + /// + [SugarColumn(ColumnDescription = "租户Id", IsOnlyIgnoreUpdate = true)] + public virtual long? TenantId { get; set; } +} + +/// +/// 租户实体基类Id +/// +public abstract class EntityBaseTenantId : EntityBaseId, ITenantIdFilter +{ + /// + /// 租户Id + /// + [SugarColumn(ColumnDescription = "租户Id", IsOnlyIgnoreUpdate = true)] + public virtual long? TenantId { get; set; } +} + +/// +/// 租户机构实体基类(数据权限) +/// +public abstract class EntityBaseTenantOrg : EntityBaseOrg, ITenantIdFilter +{ + /// + /// 租户Id + /// + [SugarColumn(ColumnDescription = "租户Id", IsOnlyIgnoreUpdate = true)] + public virtual long? TenantId { get; set; } +} + +/// +/// 租户机构实体基类(数据权限、删除标志) +/// +public abstract class EntityBaseTenantOrgDel : EntityBaseOrgDel, ITenantIdFilter +{ + /// + /// 租户Id + /// + [SugarColumn(ColumnDescription = "租户Id", IsOnlyIgnoreUpdate = true)] + public virtual long? TenantId { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/IEntityFilter.cs b/yy-admin-master/YY.Admin.Core/Entity/IEntityFilter.cs new file mode 100644 index 0000000..1bda2f3 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/IEntityFilter.cs @@ -0,0 +1,36 @@ + + +namespace YY.Admin.Core; + +/// +/// 假删除接口过滤器 +/// +public interface IDeletedFilter +{ + /// + /// 软删除 + /// + bool IsDelete { get; set; } +} + +/// +/// 租户Id接口过滤器 +/// +public interface ITenantIdFilter +{ + /// + /// 租户Id + /// + long? TenantId { get; set; } +} + +/// +/// 机构Id接口过滤器 +/// +public interface IOrgIdFilter +{ + /// + /// 机构Id + /// + long OrgId { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/JeecgSysDictItem.cs b/yy-admin-master/YY.Admin.Core/Entity/JeecgSysDictItem.cs new file mode 100644 index 0000000..7c83f18 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/JeecgSysDictItem.cs @@ -0,0 +1,59 @@ +using SqlSugar; + +namespace YY.Admin.Core; + +/// +/// Jeecg 数据字典项同构表(桌面端) +/// +[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; } +} diff --git a/yy-admin-master/YY.Admin.Core/Entity/JeecgSysUser.cs b/yy-admin-master/YY.Admin.Core/Entity/JeecgSysUser.cs new file mode 100644 index 0000000..68821fd --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/JeecgSysUser.cs @@ -0,0 +1,181 @@ +using SqlSugar; + +namespace YY.Admin.Core; + +/// +/// Jeecg 用户同构表(桌面端) +/// 说明: +/// 1. 按 JeecgBoot 的 sys_user 字段模型定义。 +/// 2. 为避免影响现有 YY.Admin 的 sys_user 业务表,这里单独落表 jeecg_sys_user。 +/// +[SugarTable("jeecg_sys_user", "Jeecg用户同构表")] +[SysTable] +public class JeecgSysUser +{ + /// id(Jeecg为字符串雪花ID) + [SugarColumn(ColumnName = "id", IsPrimaryKey = true, Length = 64)] + public string Id { get; set; } = string.Empty; + + /// 登录账号 + [SugarColumn(ColumnName = "username", Length = 64, IsNullable = true)] + public string? Username { get; set; } + + /// 真实姓名 + [SugarColumn(ColumnName = "realname", Length = 64, IsNullable = true)] + public string? Realname { get; set; } + + /// 密码 + [SugarColumn(ColumnName = "password", Length = 512, IsNullable = true)] + public string? Password { get; set; } + + /// 密码盐 + [SugarColumn(ColumnName = "salt", Length = 64, IsNullable = true)] + public string? Salt { get; set; } + + /// 头像 + [SugarColumn(ColumnName = "avatar", Length = 512, IsNullable = true)] + public string? Avatar { get; set; } + + /// 生日 + [SugarColumn(ColumnName = "birthday", IsNullable = true)] + public DateTime? Birthday { get; set; } + + /// 性别(1男2女) + [SugarColumn(ColumnName = "sex", IsNullable = true)] + public int? Sex { get; set; } + + /// 邮箱 + [SugarColumn(ColumnName = "email", Length = 128, IsNullable = true)] + public string? Email { get; set; } + + /// 手机号 + [SugarColumn(ColumnName = "phone", Length = 32, IsNullable = true)] + public string? Phone { get; set; } + + /// 登录选择部门编码 + [SugarColumn(ColumnName = "org_code", Length = 128, IsNullable = true)] + public string? OrgCode { get; set; } + + /// 登录选择租户ID + [SugarColumn(ColumnName = "login_tenant_id", IsNullable = true)] + public int? LoginTenantId { get; set; } + + /// 状态(1正常 2冻结) + [SugarColumn(ColumnName = "status", IsNullable = true)] + public int? Status { get; set; } + + /// 删除标志(0正常 1已删) + [SugarColumn(ColumnName = "del_flag", IsNullable = true)] + public int? DelFlag { get; set; } + + /// 工号 + [SugarColumn(ColumnName = "work_no", Length = 64, IsNullable = true)] + public string? WorkNo { get; set; } + + /// 座机号 + [SugarColumn(ColumnName = "telephone", Length = 32, IsNullable = true)] + public string? Telephone { get; set; } + + /// 创建人 + [SugarColumn(ColumnName = "create_by", Length = 64, IsNullable = true)] + public string? CreateBy { get; set; } + + /// 创建时间 + [SugarColumn(ColumnName = "create_time", IsNullable = true)] + public DateTime? CreateTime { get; set; } + + /// 更新人 + [SugarColumn(ColumnName = "update_by", Length = 64, IsNullable = true)] + public string? UpdateBy { get; set; } + + /// 更新时间 + [SugarColumn(ColumnName = "update_time", IsNullable = true)] + public DateTime? UpdateTime { get; set; } + + /// 流程同步标志 + [SugarColumn(ColumnName = "activiti_sync", IsNullable = true)] + public int? ActivitiSync { get; set; } + + /// 身份(0普通成员 1上级) + [SugarColumn(ColumnName = "user_identity", IsNullable = true)] + public int? UserIdentity { get; set; } + + /// 负责部门IDs + [SugarColumn(ColumnName = "depart_ids", Length = 1024, IsNullable = true)] + public string? DepartIds { get; set; } + + /// 设备ID + [SugarColumn(ColumnName = "client_id", Length = 256, IsNullable = true)] + public string? ClientId { get; set; } + + /// 流程状态 + [SugarColumn(ColumnName = "bpm_status", Length = 64, IsNullable = true)] + public string? BpmStatus { get; set; } + + /// 个性签名 + [SugarColumn(ColumnName = "sign", Length = 512, IsNullable = true)] + public string? Sign { get; set; } + + /// 是否开启个性签名 + [SugarColumn(ColumnName = "sign_enable", IsNullable = true)] + public int? SignEnable { get; set; } + + /// 主岗位 + [SugarColumn(ColumnName = "main_dep_post_id", Length = 128, IsNullable = true)] + public string? MainDepPostId { get; set; } + + /// 职务(字典) + [SugarColumn(ColumnName = "position_type", Length = 64, IsNullable = true)] + public string? PositionType { get; set; } + + /// 最后修改密码时间 + [SugarColumn(ColumnName = "last_pwd_update_time", IsNullable = true)] + public DateTime? LastPwdUpdateTime { get; set; } + + /// 排序 + [SugarColumn(ColumnName = "sort", IsNullable = true)] + public int? Sort { get; set; } + + /// 是否隐藏联系方式 0否1是 + [SugarColumn(ColumnName = "iz_hide_contact", Length = 8, IsNullable = true)] + public string? IzHideContact { get; set; } + + // ------------------------- 非持久化字段(与 Jeecg 实体保持一致) ------------------------- + + /// 部门名称(非持久化) + [SugarColumn(IsIgnore = true)] + public string? OrgCodeTxt { get; set; } + + /// 职务(非持久化) + [SugarColumn(IsIgnore = true)] + public string? Post { get; set; } + + /// 多租户ids临时字段(非持久化) + [SugarColumn(IsIgnore = true)] + public string? RelTenantIds { get; set; } + + /// 首页路径(非持久化) + [SugarColumn(IsIgnore = true)] + public string? HomePath { get; set; } + + /// 职位名称(非持久化) + [SugarColumn(IsIgnore = true)] + public string? PostText { get; set; } + + /// 是否绑定第三方(非持久化) + [SugarColumn(IsIgnore = true)] + public bool IzBindThird { get; set; } + + /// 兼职岗位(非持久化) + [SugarColumn(IsIgnore = true)] + public string? OtherDepPostId { get; set; } + + /// 登录时选择部门编码(非持久化) + [SugarColumn(IsIgnore = true)] + public string? LoginOrgCode { get; set; } + + /// 所属部门IDs(非持久化) + [SugarColumn(IsIgnore = true)] + public string? BelongDepIds { get; set; } +} + diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysConfig.cs b/yy-admin-master/YY.Admin.Core/Entity/SysConfig.cs new file mode 100644 index 0000000..3a431f9 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysConfig.cs @@ -0,0 +1,64 @@ + + + + + +namespace YY.Admin.Core; + +/// +/// 系统配置参数表 +/// +[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 +{ + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string Name { get; set; } + + /// + /// 编码 + /// + [SugarColumn(ColumnDescription = "编码", Length = 64)] + [MaxLength(64)] + public string? Code { get; set; } + + /// + /// 参数值 + /// + [SugarColumn(ColumnDescription = "参数值", Length = 512)] + [MaxLength(512)] + [IgnoreUpdateSeedColumn] + public string? Value { get; set; } + + /// + /// 是否是内置参数(Y-是,N-否) + /// + [SugarColumn(ColumnDescription = "是否是内置参数", DefaultValue = "1")] + public YesNoEnum SysFlag { get; set; } = YesNoEnum.Y; + + /// + /// 分组编码 + /// + [SugarColumn(ColumnDescription = "分组编码", Length = 64)] + [MaxLength(64)] + public string? GroupCode { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序", DefaultValue = "100")] + public int OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 256)] + [MaxLength(256)] + public string? Remark { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysDictData.cs b/yy-admin-master/YY.Admin.Core/Entity/SysDictData.cs new file mode 100644 index 0000000..0e798b0 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysDictData.cs @@ -0,0 +1,101 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统字典值表 +/// +[SugarTable("sys_dict_data", "系统字典值表")] +[SysTable] +[SugarIndex("index_{table}_TV", nameof(DictTypeId), OrderByType.Asc, nameof(Value), OrderByType.Asc, IsUnique = true)] +public partial class SysDictData : EntityBase +{ + /// + /// 字典类型Id + /// + [SugarColumn(ColumnDescription = "字典类型Id")] + public long DictTypeId { get; set; } + + /// + /// 字典类型 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(DictTypeId))] + public SysDictType DictType { get; set; } + + /// + /// 显示文本 + /// + [SugarColumn(ColumnDescription = "显示文本", Length = 256)] + [Required, MaxLength(256)] + public virtual string Label { get; set; } + + /// + /// 值 + /// + [SugarColumn(ColumnDescription = "值", Length = 256)] + [Required, MaxLength(256)] + public virtual string Value { get; set; } + + /// + /// 编码 + /// + /// + /// + [SugarColumn(ColumnDescription = "编码", Length = 256)] + public virtual string? Code { get; set; } + + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 256)] + [MaxLength(256)] + public virtual string? Name { get; set; } + + /// + /// 显示样式-标签颜色 + /// + [SugarColumn(ColumnDescription = "显示样式-标签颜色", Length = 16)] + [MaxLength(16)] + public string? TagType { get; set; } + + /// + /// 显示样式-Style(控制显示样式) + /// + [SugarColumn(ColumnDescription = "显示样式-Style", Length = 512)] + [MaxLength(512)] + public string? StyleSetting { get; set; } + + /// + /// 显示样式-Class(控制显示样式) + /// + [SugarColumn(ColumnDescription = "显示样式-Class", Length = 512)] + [MaxLength(512)] + public string? ClassSetting { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序", DefaultValue = "100")] + public int OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 2048)] + [MaxLength(2048)] + public string? Remark { get; set; } + + /// + /// 拓展数据(保存业务功能的配置项) + /// + [SugarColumn(ColumnDescription = "拓展数据(保存业务功能的配置项)", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? ExtData { get; set; } + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态", DefaultValue = "1")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysDictDataTenant.cs b/yy-admin-master/YY.Admin.Core/Entity/SysDictDataTenant.cs new file mode 100644 index 0000000..47dd994 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysDictDataTenant.cs @@ -0,0 +1,18 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统租户字典值表 +/// +[SugarTable("sys_dict_data_tenant", "系统租户字典值表")] +[SysTable] +[SugarIndex("index_{table}_TV", nameof(DictTypeId), OrderByType.Asc, nameof(Value), OrderByType.Asc)] +public partial class SysDictDataTenant : SysDictData, ITenantIdFilter +{ + /// + /// 租户Id + /// + [SugarColumn(ColumnDescription = "租户Id", IsOnlyIgnoreUpdate = true)] + public virtual long? TenantId { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysDictType.cs b/yy-admin-master/YY.Admin.Core/Entity/SysDictType.cs new file mode 100644 index 0000000..e451e88 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysDictType.cs @@ -0,0 +1,64 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统字典类型表 +/// +[SugarTable("sys_dict_type", "系统字典类型表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc)] +public partial class SysDictType : EntityBase +{ + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string Name { get; set; } + + /// + /// 编码 + /// + [SugarColumn(ColumnDescription = "编码", Length = 64)] + [Required, MaxLength(64)] + public virtual string Code { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序", DefaultValue = "100")] + public int OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 256)] + [MaxLength(256)] + public string? Remark { get; set; } + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态", DefaultValue = "1")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; + + /// + /// 是否是内置字典(Y-是,N-否) + /// + [SugarColumn(ColumnDescription = "是否是内置字典", DefaultValue = "1")] + public virtual YesNoEnum SysFlag { get; set; } = YesNoEnum.Y; + + /// + /// 是否是租户字典(Y-是,N-否) + /// + [SugarColumn(ColumnDescription = "是否是租户字典", DefaultValue = "2")] + public virtual YesNoEnum IsTenant { get; set; } = YesNoEnum.N; + + /// + /// 字典值集合 + /// + [Navigate(NavigateType.OneToMany, nameof(SysDictData.DictTypeId))] + public List Children { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysFile.cs b/yy-admin-master/YY.Admin.Core/Entity/SysFile.cs new file mode 100644 index 0000000..cee9760 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysFile.cs @@ -0,0 +1,100 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统文件表 +/// +[SugarTable("sys_file", "系统文件表")] +[SysTable] +[SugarIndex("index_{table}_F", nameof(FileName), OrderByType.Asc)] +public partial class SysFile : EntityBaseTenantOrg +{ + /// + /// 提供者 + /// + [SugarColumn(ColumnDescription = "提供者", Length = 128)] + [MaxLength(128)] + public string? Provider { get; set; } + + /// + /// 仓储名称 + /// + [SugarColumn(ColumnDescription = "仓储名称", Length = 128)] + [MaxLength(128)] + public string? BucketName { get; set; } + + /// + /// 文件名称(源文件名) + /// + [SugarColumn(ColumnDescription = "文件名称", Length = 128)] + [MaxLength(128)] + public string? FileName { get; set; } + + /// + /// 文件后缀 + /// + [SugarColumn(ColumnDescription = "文件后缀", Length = 16)] + [MaxLength(16)] + public string? Suffix { get; set; } + + /// + /// 存储路径 + /// + [SugarColumn(ColumnDescription = "存储路径", Length = 512)] + [MaxLength(512)] + public string? FilePath { get; set; } + + /// + /// 文件大小KB + /// + [SugarColumn(ColumnDescription = "文件大小KB")] + public long SizeKb { get; set; } + + /// + /// 文件大小信息-计算后的 + /// + [SugarColumn(ColumnDescription = "文件大小信息", Length = 64)] + [MaxLength(64)] + public string? SizeInfo { get; set; } + + /// + /// 外链地址-OSS上传后生成外链地址方便前端预览 + /// + [SugarColumn(ColumnDescription = "外链地址", Length = 512)] + [MaxLength(512)] + public string? Url { get; set; } + + /// + /// 文件MD5 + /// + [SugarColumn(ColumnDescription = "文件MD5", Length = 128)] + [MaxLength(128)] + public string? FileMd5 { get; set; } + + /// + /// 文件类别 + /// + [SugarColumn(ColumnDescription = "文件类别", Length = 128)] + [MaxLength(128)] + public virtual string? FileType { get; set; } + + /// + /// 文件别名 + /// + [SugarColumn(ColumnDescription = "文件别名", Length = 128)] + [MaxLength(128)] + public string? FileAlias { get; set; } + + /// + /// 是否公开 + /// + [SugarColumn(ColumnDescription = "是否公开")] + public virtual bool IsPublic { get; set; } = false; + + /// + /// 业务数据Id + /// + [SugarColumn(ColumnDescription = "业务数据Id")] + public long? DataId { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysLdap.cs b/yy-admin-master/YY.Admin.Core/Entity/SysLdap.cs new file mode 100644 index 0000000..b0e7d57 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysLdap.cs @@ -0,0 +1,85 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统域登录信息配置表 +/// +[SugarTable("sys_ldap", "系统域登录信息配置表")] +[SysTable] +public class SysLdap : EntityBaseTenantDel +{ + /// + /// 主机 + /// + [SugarColumn(ColumnDescription = "主机", Length = 128)] + [Required] + public virtual string Host { get; set; } + + /// + /// 端口 + /// + [SugarColumn(ColumnDescription = "端口")] + public virtual int Port { get; set; } + + /// + /// 用户搜索基准 + /// + [SugarColumn(ColumnDescription = "用户搜索基准", Length = 128)] + [Required] + public virtual string BaseDn { get; set; } + + /// + /// 绑定DN(有管理权限制的用户) + /// + [SugarColumn(ColumnDescription = "绑定DN", Length = 128)] + [Required] + public virtual string BindDn { get; set; } + + /// + /// 绑定密码(有管理权限制的用户密码) + /// + [SugarColumn(ColumnDescription = "绑定密码", Length = 512)] + [Required] + public virtual string BindPass { get; set; } + + /// + /// 用户过滤规则 + /// + [SugarColumn(ColumnDescription = "用户过滤规则", Length = 128)] + [Required] + public virtual string AuthFilter { get; set; } = "sAMAccountName=%s"; + + /// + /// Ldap版本 + /// + [SugarColumn(ColumnDescription = "Ldap版本")] + public int Version { get; set; } + + /// + /// 绑定域账号字段属性值 + /// + [SugarColumn(ColumnDescription = "绑定域账号字段属性值", Length = 32)] + [Required] + public virtual string BindAttrAccount { get; set; } = "sAMAccountName"; + + /// + /// 绑定用户EmployeeId属性值 + /// + [SugarColumn(ColumnDescription = "绑定用户EmployeeId属性值", Length = 32)] + [Required] + public virtual string BindAttrEmployeeId { get; set; } = "EmployeeId"; + + /// + /// 绑定Code属性值 + /// + [SugarColumn(ColumnDescription = "绑定对象Code属性值", Length = 64)] + [Required] + public virtual string BindAttrCode { get; set; } = "objectGUID"; + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysLogDiff.cs b/yy-admin-master/YY.Admin.Core/Entity/SysLogDiff.cs new file mode 100644 index 0000000..af285c3 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysLogDiff.cs @@ -0,0 +1,48 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统差异日志表 +/// +[SugarTable("sys_log_diff", "系统差异日志表")] +[SysTable] +[LogTable] +public partial class SysLogDiff : EntityBaseTenant +{ + /// + /// 差异数据 + /// + [SugarColumn(ColumnDescription = "差异数据", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? DiffData { get; set; } + + /// + /// Sql + /// + [SugarColumn(ColumnDescription = "Sql", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? Sql { get; set; } + + /// + /// 参数 手动传入的参数 + /// + [SugarColumn(ColumnDescription = "参数", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? Parameters { get; set; } + + /// + /// 业务对象 + /// + [SugarColumn(ColumnDescription = "业务对象", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? BusinessData { get; set; } + + /// + /// 差异操作 + /// + [SugarColumn(ColumnDescription = "差异操作", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? DiffType { get; set; } + + /// + /// 耗时 + /// + [SugarColumn(ColumnDescription = "耗时")] + public long? Elapsed { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysLogEx.cs b/yy-admin-master/YY.Admin.Core/Entity/SysLogEx.cs new file mode 100644 index 0000000..a078b47 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysLogEx.cs @@ -0,0 +1,68 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统异常日志表 +/// +[SugarTable("sys_log_ex", "系统异常日志表")] +[SysTable] +[LogTable] +public partial class SysLogEx : SysLogVis +{ + /// + /// 请求方式 + /// + [SugarColumn(ColumnDescription = "请求方式", Length = 32)] + [MaxLength(32)] + public string? HttpMethod { get; set; } + + /// + /// 请求地址 + /// + [SugarColumn(ColumnDescription = "请求地址", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? RequestUrl { get; set; } + + /// + /// 请求参数 + /// + [SugarColumn(ColumnDescription = "请求参数", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? RequestParam { get; set; } + + /// + /// 返回结果 + /// + [SugarColumn(ColumnDescription = "返回结果", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? ReturnResult { get; set; } + + /// + /// 事件Id + /// + [SugarColumn(ColumnDescription = "事件Id")] + public int? EventId { get; set; } + + /// + /// 线程Id + /// + [SugarColumn(ColumnDescription = "线程Id")] + public int? ThreadId { get; set; } + + /// + /// 请求跟踪Id + /// + [SugarColumn(ColumnDescription = "请求跟踪Id", Length = 128)] + [MaxLength(128)] + public string? TraceId { get; set; } + + /// + /// 异常信息 + /// + [SugarColumn(ColumnDescription = "异常信息", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? Exception { get; set; } + + /// + /// 日志消息Json + /// + [SugarColumn(ColumnDescription = "日志消息Json", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? Message { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysLogOp.cs b/yy-admin-master/YY.Admin.Core/Entity/SysLogOp.cs new file mode 100644 index 0000000..10f301a --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysLogOp.cs @@ -0,0 +1,68 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统操作日志表 +/// +[SugarTable("sys_log_op", "系统操作日志表")] +[SysTable] +[LogTable] +public partial class SysLogOp : SysLogVis +{ + /// + /// 请求方式 + /// + [SugarColumn(ColumnDescription = "请求方式", Length = 32)] + [MaxLength(32)] + public string? HttpMethod { get; set; } + + /// + /// 请求地址 + /// + [SugarColumn(ColumnDescription = "请求地址", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? RequestUrl { get; set; } + + /// + /// 请求参数 + /// + [SugarColumn(ColumnDescription = "请求参数", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? RequestParam { get; set; } + + /// + /// 返回结果 + /// + [SugarColumn(ColumnDescription = "返回结果", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? ReturnResult { get; set; } + + /// + /// 事件Id + /// + [SugarColumn(ColumnDescription = "事件Id")] + public int? EventId { get; set; } + + /// + /// 线程Id + /// + [SugarColumn(ColumnDescription = "线程Id")] + public int? ThreadId { get; set; } + + /// + /// 请求跟踪Id + /// + [SugarColumn(ColumnDescription = "请求跟踪Id", Length = 128)] + [MaxLength(128)] + public string? TraceId { get; set; } + + /// + /// 异常信息 + /// + [SugarColumn(ColumnDescription = "异常信息", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? Exception { get; set; } + + /// + /// 日志消息Json + /// + [SugarColumn(ColumnDescription = "日志消息Json", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? Message { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysLogVis.cs b/yy-admin-master/YY.Admin.Core/Entity/SysLogVis.cs new file mode 100644 index 0000000..6686aeb --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysLogVis.cs @@ -0,0 +1,113 @@ + + + +namespace YY.Admin.Core; + +/// +/// 系统访问日志表 +/// +[SugarTable("sys_log_vis", "系统访问日志表")] +[SysTable] +[LogTable] +public partial class SysLogVis : EntityBaseTenant +{ + /// + /// 模块名称 + /// + [SugarColumn(ColumnDescription = "模块名称", Length = 256)] + [MaxLength(256)] + public string? ControllerName { get; set; } + + /// + /// 方法名称 + /// + [SugarColumn(ColumnDescription = "方法名称", Length = 256)] + [MaxLength(256)] + public string? ActionName { get; set; } + + /// + /// 显示名称 + /// + [SugarColumn(ColumnDescription = "显示名称", Length = 256)] + [MaxLength(256)] + public string? DisplayTitle { get; set; } + + /// + /// 执行状态 + /// + [SugarColumn(ColumnDescription = "执行状态", Length = 32)] + [MaxLength(32)] + public string? Status { get; set; } + + /// + /// IP地址 + /// + [SugarColumn(ColumnDescription = "IP地址", Length = 256)] + [MaxLength(256)] + public string? RemoteIp { get; set; } + + /// + /// 登录地点 + /// + [SugarColumn(ColumnDescription = "登录地点", Length = 128)] + [MaxLength(128)] + public string? Location { get; set; } + + /// + /// 经度 + /// + [SugarColumn(ColumnDescription = "经度")] + public double? Longitude { get; set; } + + /// + /// 维度 + /// + [SugarColumn(ColumnDescription = "维度")] + public double? Latitude { get; set; } + + /// + /// 浏览器 + /// + [SugarColumn(ColumnDescription = "浏览器", Length = 1024)] + [MaxLength(1024)] + public string? Browser { get; set; } + + /// + /// 操作系统 + /// + [SugarColumn(ColumnDescription = "操作系统", Length = 256)] + [MaxLength(256)] + public string? Os { get; set; } + + /// + /// 操作用时 + /// + [SugarColumn(ColumnDescription = "操作用时")] + public long? Elapsed { get; set; } + + /// + /// 日志时间 + /// + [SugarColumn(ColumnDescription = "日志时间")] + public DateTime? LogDateTime { get; set; } + + /// + /// 日志级别 + /// + [SugarColumn(ColumnDescription = "日志级别")] + public LogLevel? LogLevel { get; set; } + + /// + /// 账号 + /// + [SugarColumn(ColumnDescription = "账号", Length = 32)] + [MaxLength(32)] + public string? Account { get; set; } + + /// + /// 真实姓名 + /// + [SugarColumn(ColumnDescription = "真实姓名", Length = 32)] + [MaxLength(32)] + public string? RealName { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysMenu.cs b/yy-admin-master/YY.Admin.Core/Entity/SysMenu.cs new file mode 100644 index 0000000..7abb9af --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysMenu.cs @@ -0,0 +1,130 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统菜单表 +/// +[SugarTable("sys_menu", "系统菜单表")] +[SysTable] +[SugarIndex("index_{table}_T", nameof(Title), OrderByType.Asc)] +[SugarIndex("index_{table}_T2", nameof(Type), OrderByType.Asc)] +public partial class SysMenu : EntityBase +{ + /// + /// 父Id + /// + [SugarColumn(ColumnDescription = "父Id")] + public long Pid { get; set; } + + /// + /// 菜单类型(1目录 2菜单 3按钮) + /// + [SugarColumn(ColumnDescription = "菜单类型")] + public MenuTypeEnum Type { get; set; } + + /// + /// 路由名称 + /// + [SugarColumn(ColumnDescription = "路由名称", Length = 64)] + [MaxLength(64)] + public string? Name { get; set; } + + /// + /// 路由地址 + /// + [SugarColumn(ColumnDescription = "路由地址", Length = 128)] + [MaxLength(128)] + public string? Path { get; set; } + + /// + /// 组件路径 + /// + [SugarColumn(ColumnDescription = "组件路径", Length = 128)] + [MaxLength(128)] + public string? Component { get; set; } + + /// + /// 重定向 + /// + [SugarColumn(ColumnDescription = "重定向", Length = 128)] + [MaxLength(128)] + public string? Redirect { get; set; } + + /// + /// 权限标识 + /// + [SugarColumn(ColumnDescription = "权限标识", Length = 128)] + [MaxLength(128)] + public string? Permission { get; set; } + + /// + /// 菜单名称 + /// + [SugarColumn(ColumnDescription = "菜单名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string Title { get; set; } + + /// + /// 图标 + /// + [SugarColumn(ColumnDescription = "图标", Length = 128)] + [MaxLength(128)] + public string? Icon { get; set; } = "ele-Menu"; + + /// + /// 是否内嵌 + /// + [SugarColumn(ColumnDescription = "是否内嵌")] + public bool IsIframe { get; set; } + + /// + /// 外链链接 + /// + [SugarColumn(ColumnDescription = "外链链接", Length = 256)] + [MaxLength(256)] + public string? OutLink { get; set; } + + /// + /// 是否隐藏 + /// + [SugarColumn(ColumnDescription = "是否隐藏")] + public bool IsHide { get; set; } + + /// + /// 是否缓存 + /// + [SugarColumn(ColumnDescription = "是否缓存")] + public bool IsKeepAlive { get; set; } = true; + + /// + /// 是否固定 + /// + [SugarColumn(ColumnDescription = "是否固定")] + public bool IsAffix { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 256)] + [MaxLength(256)] + public string? Remark { get; set; } + + /// + /// 菜单子项 + /// + [SugarColumn(IsIgnore = true)] + public List Children { get; set; } = new(); +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysNotice.cs b/yy-admin-master/YY.Admin.Core/Entity/SysNotice.cs new file mode 100644 index 0000000..bd7c3c7 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysNotice.cs @@ -0,0 +1,76 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统通知公告表 +/// +[SugarTable("sys_notice", "系统通知公告表")] +[SysTable] +[SugarIndex("index_{table}_T", nameof(Type), OrderByType.Asc)] +public partial class SysNotice : EntityBase +{ + /// + /// 标题 + /// + [SugarColumn(ColumnDescription = "标题", Length = 32)] + [Required, MaxLength(32)] + public virtual string Title { get; set; } + + /// + /// 内容 + /// + [SugarColumn(ColumnDescription = "内容", ColumnDataType = StaticConfig.CodeFirst_BigString)] + [Required] + public virtual string Content { get; set; } + + /// + /// 类型(1通知 2公告) + /// + [SugarColumn(ColumnDescription = "类型(1通知 2公告)")] + public NoticeTypeEnum Type { get; set; } + + /// + /// 发布人Id + /// + [SugarColumn(ColumnDescription = "发布人Id")] + public long PublicUserId { get; set; } + + /// + /// 发布人姓名 + /// + [SugarColumn(ColumnDescription = "发布人姓名", Length = 32)] + [MaxLength(32)] + public string? PublicUserName { get; set; } + + /// + /// 发布机构Id + /// + [SugarColumn(ColumnDescription = "发布机构Id")] + public long PublicOrgId { get; set; } + + /// + /// 发布机构名称 + /// + [SugarColumn(ColumnDescription = "发布机构名称", Length = 64)] + [MaxLength(64)] + public string? PublicOrgName { get; set; } + + /// + /// 发布时间 + /// + [SugarColumn(ColumnDescription = "发布时间")] + public DateTime? PublicTime { get; set; } + + /// + /// 撤回时间 + /// + [SugarColumn(ColumnDescription = "撤回时间")] + public DateTime? CancelTime { get; set; } + + /// + /// 状态(0草稿 1发布 2撤回 3删除) + /// + [SugarColumn(ColumnDescription = "状态(0草稿 1发布 2撤回 3删除)")] + public NoticeStatusEnum Status { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysNoticeUser.cs b/yy-admin-master/YY.Admin.Core/Entity/SysNoticeUser.cs new file mode 100644 index 0000000..da6598a --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysNoticeUser.cs @@ -0,0 +1,41 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统通知公告用户表 +/// +[SugarTable("sys_notice_user", "系统通知公告用户表")] +[SysTable] +public partial class SysNoticeUser : EntityBaseId +{ + /// + /// 通知公告Id + /// + [SugarColumn(ColumnDescription = "通知公告Id")] + public long NoticeId { get; set; } + + /// + /// 通知公告 + /// + [Navigate(NavigateType.OneToOne, nameof(NoticeId))] + public SysNotice SysNotice { get; set; } + + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 阅读时间 + /// + [SugarColumn(ColumnDescription = "阅读时间")] + public DateTime? ReadTime { get; set; } + + /// + /// 状态(0未读 1已读) + /// + [SugarColumn(ColumnDescription = "状态(0未读 1已读)")] + public NoticeUserStatusEnum ReadStatus { get; set; } = NoticeUserStatusEnum.UNREAD; +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysOnlineUser.cs b/yy-admin-master/YY.Admin.Core/Entity/SysOnlineUser.cs new file mode 100644 index 0000000..ca8ac45 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysOnlineUser.cs @@ -0,0 +1,64 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统在线用户表 +/// +[SugarTable("sys_online_user", "系统在线用户表")] +[SysTable] +public partial class SysOnlineUser : EntityBaseTenantId +{ + /// + /// 连接Id + /// + [SugarColumn(ColumnDescription = "连接Id")] + public string? ConnectionId { get; set; } + + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 账号 + /// + [SugarColumn(ColumnDescription = "账号", Length = 32)] + [Required, MaxLength(32)] + public virtual string UserName { get; set; } + + /// + /// 真实姓名 + /// + [SugarColumn(ColumnDescription = "真实姓名", Length = 32)] + [MaxLength(32)] + public string? RealName { get; set; } + + /// + /// 连接时间 + /// + [SugarColumn(ColumnDescription = "连接时间")] + public DateTime? Time { get; set; } + + /// + /// 连接IP + /// + [SugarColumn(ColumnDescription = "连接IP", Length = 256)] + [MaxLength(256)] + public string? Ip { get; set; } + + /// + /// 浏览器 + /// + [SugarColumn(ColumnDescription = "浏览器", Length = 128)] + [MaxLength(128)] + public string? Browser { get; set; } + + /// + /// 操作系统 + /// + [SugarColumn(ColumnDescription = "操作系统", Length = 128)] + [MaxLength(128)] + public string? Os { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysOrg.cs b/yy-admin-master/YY.Admin.Core/Entity/SysOrg.cs new file mode 100644 index 0000000..2042513 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysOrg.cs @@ -0,0 +1,92 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统机构表 +/// +[SugarTable("sys_org", "系统机构表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc)] +[SugarIndex("index_{table}_T", nameof(Type), OrderByType.Asc)] +public partial class SysOrg : EntityBaseTenant +{ + /// + /// 父Id + /// + [SugarColumn(ColumnDescription = "父Id")] + public long Pid { get; set; } + + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string Name { get; set; } + + /// + /// 编码 + /// + [SugarColumn(ColumnDescription = "编码", Length = 64)] + [MaxLength(64)] + public string? Code { get; set; } + + /// + /// 级别 + /// + [SugarColumn(ColumnDescription = "级别")] + public int? Level { get; set; } + + /// + /// 机构类型-数据字典 + /// + [SugarColumn(ColumnDescription = "机构类型", Length = 64)] + [MaxLength(64)] + public virtual string? Type { get; set; } + + /// + /// 负责人Id + /// + [SugarColumn(ColumnDescription = "负责人Id", IsNullable = true)] + public long? DirectorId { get; set; } + + /// + /// 负责人 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(DirectorId))] + public SysUser Director { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 128)] + [MaxLength(128)] + public string? Remark { get; set; } + + /// + /// 机构子项 + /// + [SugarColumn(IsIgnore = true)] + public List Children { get; set; } + + /// + /// 是否禁止选中 + /// + [SugarColumn(IsIgnore = true)] + public bool Disabled { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysPos.cs b/yy-admin-master/YY.Admin.Core/Entity/SysPos.cs new file mode 100644 index 0000000..f2ba355 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysPos.cs @@ -0,0 +1,52 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统职位表 +/// +[SugarTable("sys_pos", "系统职位表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc)] +public partial class SysPos : EntityBaseTenant +{ + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string Name { get; set; } + + /// + /// 编码 + /// + [SugarColumn(ColumnDescription = "编码", Length = 64)] + [MaxLength(64)] + public string? Code { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 128)] + [MaxLength(128)] + public string? Remark { get; set; } + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; + + /// + /// 在职人员 + /// + [SugarColumn(IsIgnore = true)] + public List UserList { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysPrint.cs b/yy-admin-master/YY.Admin.Core/Entity/SysPrint.cs new file mode 100644 index 0000000..cd7fca5 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysPrint.cs @@ -0,0 +1,71 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统打印模板表 +/// +[SugarTable("sys_print", "系统打印模板表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +public partial class SysPrint : EntityBaseTenant +{ + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string Name { get; set; } + + /// + /// 打印模板 + /// + [SugarColumn(ColumnDescription = "打印模板", ColumnDataType = StaticConfig.CodeFirst_BigString)] + [Required] + public virtual string Template { get; set; } + + /// + /// 打印类型 + /// + [SugarColumn(ColumnDescription = "打印类型")] + [Required] + public virtual PrintTypeEnum? PrintType { get; set; } + + /// + /// 客户端服务地址 + /// + [SugarColumn(ColumnDescription = "客户端服务地址", Length = 128)] + [MaxLength(128)] + public virtual string? ClientServiceAddress { get; set; } + + /// + /// 打印参数 + /// + [SugarColumn(ColumnDescription = "打印参数", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public virtual string? PrintParam { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 128)] + [MaxLength(128)] + public string? Remark { get; set; } + + /// + /// 打印预览测试数据 + /// + [SugarColumn(ColumnDescription = "打印预览测试数据", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public string? PrintDataDemo { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysRegion.cs b/yy-admin-master/YY.Admin.Core/Entity/SysRegion.cs new file mode 100644 index 0000000..c3ba55f --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysRegion.cs @@ -0,0 +1,105 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统行政地区表 +/// +[SugarTable("sys_region", "系统行政地区表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc, IsUnique = true)] +public partial class SysRegion : EntityBaseId +{ + /// + /// 父Id + /// + [SugarColumn(ColumnDescription = "父Id")] + public long Pid { get; set; } + + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 128)] + [Required, MaxLength(128)] + public virtual string Name { get; set; } + + /// + /// 简称 + /// + [SugarColumn(ColumnDescription = "简称", Length = 32)] + [MaxLength(32)] + public string? ShortName { get; set; } + + /// + /// 组合名 + /// + [SugarColumn(ColumnDescription = "组合名", Length = 64)] + [MaxLength(64)] + public string? MergerName { get; set; } + + /// + /// 行政代码 + /// + [SugarColumn(ColumnDescription = "行政代码", Length = 32)] + [MaxLength(32)] + public string? Code { get; set; } + + /// + /// 邮政编码 + /// + [SugarColumn(ColumnDescription = "邮政编码", Length = 6)] + [MaxLength(6)] + public string? ZipCode { get; set; } + + /// + /// 区号 + /// + [SugarColumn(ColumnDescription = "区号", Length = 6)] + [MaxLength(6)] + public string? CityCode { get; set; } + + /// + /// 层级 + /// + [SugarColumn(ColumnDescription = "层级")] + public int Level { get; set; } + + /// + /// 拼音 + /// + [SugarColumn(ColumnDescription = "拼音", Length = 128)] + [MaxLength(128)] + public string? PinYin { get; set; } + + /// + /// 经度 + /// + [SugarColumn(ColumnDescription = "经度")] + public float Lng { get; set; } + + /// + /// 维度 + /// + [SugarColumn(ColumnDescription = "维度")] + public float Lat { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 128)] + [MaxLength(128)] + public string? Remark { get; set; } + + /// + /// 机构子项 + /// + [SugarColumn(IsIgnore = true)] + public List Children { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysRole.cs b/yy-admin-master/YY.Admin.Core/Entity/SysRole.cs new file mode 100644 index 0000000..34862b7 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysRole.cs @@ -0,0 +1,52 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统角色表 +/// +[SugarTable("sys_role", "系统角色表")] +[SysTable] +[SugarIndex("index_{table}_N", nameof(Name), OrderByType.Asc)] +[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc)] +public partial class SysRole : EntityBaseTenant +{ + /// + /// 名称 + /// + [SugarColumn(ColumnDescription = "名称", Length = 64)] + [Required, MaxLength(64)] + public virtual string Name { get; set; } + + /// + /// 编码 + /// + [SugarColumn(ColumnDescription = "编码", Length = 64)] + [MaxLength(64)] + public string? Code { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 数据范围(1全部数据 2本部门及以下数据 3本部门数据 4仅本人数据 5自定义数据) + /// + [SugarColumn(ColumnDescription = "数据范围")] + public DataScopeEnum DataScope { get; set; } = DataScopeEnum.Self; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 128)] + [MaxLength(128)] + public string? Remark { get; set; } + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysRoleMenu.cs b/yy-admin-master/YY.Admin.Core/Entity/SysRoleMenu.cs new file mode 100644 index 0000000..cd01f6e --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysRoleMenu.cs @@ -0,0 +1,31 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统角色菜单表 +/// +[SugarTable("sys_role_menu", "系统角色菜单表")] +[SysTable] +public class SysRoleMenu : EntityBaseId +{ + /// + /// 角色Id + /// + [SugarColumn(ColumnDescription = "角色Id")] + public long RoleId { get; set; } + + /// + /// 菜单Id + /// + [SugarColumn(ColumnDescription = "菜单Id")] + public long MenuId { get; set; } + + /// + /// 菜单 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(MenuId))] + public SysMenu SysMenu { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysRoleOrg.cs b/yy-admin-master/YY.Admin.Core/Entity/SysRoleOrg.cs new file mode 100644 index 0000000..1cb1f00 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysRoleOrg.cs @@ -0,0 +1,31 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统角色机构表 +/// +[SugarTable("sys_role_org", "系统角色机构表")] +[SysTable] +public class SysRoleOrg : EntityBaseId +{ + /// + /// 角色Id + /// + [SugarColumn(ColumnDescription = "角色Id")] + public long RoleId { get; set; } + + /// + /// 机构Id + /// + [SugarColumn(ColumnDescription = "机构Id")] + public long OrgId { get; set; } + + /// + /// 机构 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(OrgId))] + public SysOrg SysOrg { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysSchedule.cs b/yy-admin-master/YY.Admin.Core/Entity/SysSchedule.cs new file mode 100644 index 0000000..23efffa --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysSchedule.cs @@ -0,0 +1,48 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统日程表 +/// +[SugarTable("sys_schedule", "系统日程表")] +[SysTable] +public class SysSchedule : EntityBaseTenant +{ + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 日程日期 + /// + [SugarColumn(ColumnDescription = "日程日期")] + public DateTime? ScheduleTime { get; set; } + + /// + /// 开始时间 + /// + [SugarColumn(ColumnDescription = "开始时间", Length = 10)] + public string? StartTime { get; set; } + + /// + /// 结束时间 + /// + [SugarColumn(ColumnDescription = "结束时间", Length = 10)] + public string? EndTime { get; set; } + + /// + /// 日程内容 + /// + [SugarColumn(ColumnDescription = "日程内容", Length = 256)] + [Required, MaxLength(256)] + public virtual string Content { get; set; } + + /// + /// 完成状态 + /// + [SugarColumn(ColumnDescription = "完成状态")] + public FinishStatusEnum Status { get; set; } = FinishStatusEnum.UnFinish; +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysTemplate.cs b/yy-admin-master/YY.Admin.Core/Entity/SysTemplate.cs new file mode 100644 index 0000000..1c38a91 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysTemplate.cs @@ -0,0 +1,59 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统模板表 +/// +[SysTable] +[SugarTable("sys_template", "系统模板表")] +[SugarIndex("index_{table}_C", nameof(Code), OrderByType.Asc, IsUnique = true)] +[SugarIndex("index_{table}_G", nameof(GroupName), OrderByType.Asc)] +public partial class SysTemplate : EntityBaseTenant +{ + /// + /// 名称 + /// + [MaxLength(128)] + [SugarColumn(ColumnDescription = "名称", Length = 128)] + public virtual string Name { get; set; } + + /// + /// 分组名称 + /// + [SugarColumn(ColumnDescription = "分组名称")] + public virtual TemplateTypeEnum Type { get; set; } + + /// + /// 编码 + /// + [MaxLength(128)] + [SugarColumn(ColumnDescription = "编码", Length = 128)] + public virtual string Code { get; set; } + + /// + /// 分组名称 + /// + [MaxLength(32)] + [SugarColumn(ColumnDescription = "分组名称", Length = 32)] + public virtual string GroupName { get; set; } + + /// + /// 模板内容 + /// + [SugarColumn(ColumnDescription = "模板内容", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public virtual string Content { get; set; } + + /// + /// 备注 + /// + [MaxLength(128)] + [SugarColumn(ColumnDescription = "备注", Length = 128)] + public virtual string? Remark { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public virtual int OrderNo { get; set; } = 100; +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysTenant.cs b/yy-admin-master/YY.Admin.Core/Entity/SysTenant.cs new file mode 100644 index 0000000..16cd11b --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysTenant.cs @@ -0,0 +1,141 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统租户表 +/// +[SugarTable("sys_tenant", "系统租户表")] +[SysTable] +public partial class SysTenant : EntityBase +{ + /// + /// 租管用户Id + /// + [SugarColumn(ColumnDescription = "租管用户Id")] + public virtual long UserId { get; set; } + + /// + /// 机构Id + /// + [SugarColumn(ColumnDescription = "机构Id")] + public virtual long OrgId { get; set; } + + /// + /// 域名 + /// + [SugarColumn(ColumnDescription = "域名", Length = 128)] + [MaxLength(128)] + public virtual string? Host { get; set; } + + /// + /// 租户类型 + /// + [SugarColumn(ColumnDescription = "租户类型")] + public virtual TenantTypeEnum TenantType { get; set; } + + /// + /// 数据库类型 + /// + [SugarColumn(ColumnDescription = "数据库类型")] + public virtual DbType DbType { get; set; } + + /// + /// 数据库连接 + /// + [SugarColumn(ColumnDescription = "数据库连接", Length = 256)] + [MaxLength(256)] + public virtual string? Connection { get; set; } + + /// + /// 数据库标识 + /// + [SugarColumn(ColumnDescription = "数据库标识", Length = 64)] + [MaxLength(64)] + public virtual string? ConfigId { get; set; } + + /// + /// 从库连接/读写分离 + /// + [SugarColumn(ColumnDescription = "从库连接/读写分离", ColumnDataType = StaticConfig.CodeFirst_BigString)] + public virtual string? SlaveConnections { get; set; } + + /// + /// 启用注册功能 + /// + [SugarColumn(ColumnDescription = "启用注册功能")] + public virtual YesNoEnum? EnableReg { get; set; } = YesNoEnum.N; + + /// + /// 默认注册方案Id + /// + [SugarColumn(ColumnDescription = "默认注册方案")] + public virtual long? RegWayId { get; set; } + + /// + /// 图标 + /// + [SugarColumn(ColumnDescription = "图标", Length = 256), MaxLength(256)] + public virtual string? Logo { get; set; } + + /// + /// 标题 + /// + [SugarColumn(ColumnDescription = "标题", Length = 32), MaxLength(32)] + public virtual string? Title { get; set; } + + /// + /// 副标题 + /// + [SugarColumn(ColumnDescription = "副标题", Length = 32), MaxLength(32)] + public virtual string? ViceTitle { get; set; } + + /// + /// 副描述 + /// + [SugarColumn(ColumnDescription = "副描述", Length = 64), MaxLength(64)] + public virtual string? ViceDesc { get; set; } + + /// + /// 水印 + /// + [SugarColumn(ColumnDescription = "水印", Length = 32), MaxLength(32)] + public virtual string? Watermark { get; set; } + + /// + /// 版权信息 + /// + [SugarColumn(ColumnDescription = "版权信息", Length = 64), MaxLength(64)] + public virtual string? Copyright { get; set; } + + /// + /// ICP备案号 + /// + [SugarColumn(ColumnDescription = "ICP备案号", Length = 32), MaxLength(32)] + public virtual string? Icp { get; set; } + + /// + /// ICP地址 + /// + [SugarColumn(ColumnDescription = "ICP地址", Length = 32), MaxLength(32)] + public virtual string? IcpUrl { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public virtual int OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 128)] + [MaxLength(128)] + public virtual string? Remark { get; set; } + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public virtual StatusEnum Status { get; set; } = StatusEnum.Enable; +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysTenantConfig.cs b/yy-admin-master/YY.Admin.Core/Entity/SysTenantConfig.cs new file mode 100644 index 0000000..c5d3517 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysTenantConfig.cs @@ -0,0 +1,17 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统租户配置参数表 +/// +[SugarTable("sys_tenant_config", "系统租户配置参数表")] +[SysTable] +public partial class SysTenantConfig : SysConfig +{ + /// + /// 无效字段,用于忽略实体类的Value字段 + /// + [SugarColumn(IsIgnore = true)] + private new string? Value { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysTenantConfigData.cs b/yy-admin-master/YY.Admin.Core/Entity/SysTenantConfigData.cs new file mode 100644 index 0000000..e76bfc5 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysTenantConfigData.cs @@ -0,0 +1,25 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统租户配置参数值表 +/// +[SugarTable("sys_tenant_config_data", "系统租户配置参数值表")] +[SysTable] +[SugarIndex("index_{table}_TC", nameof(TenantId), OrderByType.Asc, nameof(ConfigId), OrderByType.Asc)] +public class SysTenantConfigData : EntityBaseTenantId +{ + /// + /// 配置项Id + /// + [SugarColumn(ColumnDescription = "配置项Id")] + public long ConfigId { get; set; } + + /// + /// 参数值 + /// + [SugarColumn(ColumnDescription = "参数值", Length = 512)] + [MaxLength(512)] + public string? Value { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysTenantMenu.cs b/yy-admin-master/YY.Admin.Core/Entity/SysTenantMenu.cs new file mode 100644 index 0000000..ad02e9a --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysTenantMenu.cs @@ -0,0 +1,23 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统租户菜单表 +/// +[SysTable] +[SugarTable("sys_tenant_menu", "系统租户菜单表")] +public class SysTenantMenu : EntityBaseId +{ + /// + /// 租户Id + /// + [SugarColumn(ColumnDescription = "租户Id")] + public long TenantId { get; set; } + + /// + /// 菜单Id + /// + [SugarColumn(ColumnDescription = "菜单Id")] + public long MenuId { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysUser.cs b/yy-admin-master/YY.Admin.Core/Entity/SysUser.cs new file mode 100644 index 0000000..50e170d --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysUser.cs @@ -0,0 +1,390 @@ + + + +namespace YY.Admin.Core; + +/// +/// 系统用户表 +/// +[SugarTable("sys_user", "系统用户表")] +[SysTable] +[SugarIndex("index_{table}_A", nameof(Account), OrderByType.Asc)] +[SugarIndex("index_{table}_P", nameof(Phone), OrderByType.Asc)] +public partial class SysUser : EntityBaseTenantOrg +{ + /// + /// 账号 + /// + [SugarColumn(ColumnDescription = "账号", Length = 32)] + [Required, MaxLength(32)] + public virtual string Account { get; set; } + + /// + /// 密码 + /// + [SugarColumn(ColumnDescription = "密码", Length = 512)] + [MaxLength(512)] + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public virtual string Password { get; set; } + + /// + /// Jeecg 用户表中的 salt,用于 PBEWithMD5AndDES 与后台密码字段一致;从 Jeecg 同步时写入 + /// + [SugarColumn(ColumnDescription = "Jeecg密码盐", Length = 64, IsNullable = true)] + [MaxLength(64)] + public string? JeecgPasswordSalt { get; set; } + + /// + /// 真实姓名 + /// + [SugarColumn(ColumnDescription = "真实姓名", Length = 32)] + [MaxLength(32)] + public virtual string RealName { get; set; } + + /// + /// 昵称 + /// + [SugarColumn(ColumnDescription = "昵称", Length = 32)] + [MaxLength(32)] + public string? NickName { get; set; } + + /// + /// 头像 + /// + [SugarColumn(ColumnDescription = "头像", Length = 512)] + [MaxLength(512)] + public string? Avatar { get; set; } + + /// + /// Jeecg 侧用户更新时间(用于增量同步时跳过未变化记录) + /// + [SugarColumn(ColumnDescription = "Jeecg更新时间", IsNullable = true)] + public DateTime? JeecgUpdateTime { get; set; } + + /// + /// Jeecg/业务后台用户主键(雪花 ID 等,以字符串保存避免溢出) + /// + [SugarColumn(ColumnDescription = "Jeecg用户ID", Length = 64, IsNullable = true)] + [MaxLength(64)] + public string? JeecgBizUserId { get; set; } + + /// + /// Jeecg 机构编码 orgCode + /// + [SugarColumn(ColumnDescription = "Jeecg机构编码", Length = 64, IsNullable = true)] + [MaxLength(64)] + public string? JeecgOrgCode { get; set; } + + /// + /// Jeecg 部门 id 列表(逗号分隔的 departIds) + /// + [SugarColumn(ColumnDescription = "Jeecg部门IDs", Length = 512, IsNullable = true)] + [MaxLength(512)] + public string? JeecgDepartIds { get; set; } + + /// + /// 性别-男_1、女_2 + /// + [SugarColumn(ColumnDescription = "性别")] + public GenderEnum? Sex { get; set; } + + /// + /// 年龄 + /// + [SugarColumn(ColumnDescription = "年龄")] + public int Age { get; set; } + + /// + /// 出生日期 + /// + [SugarColumn(ColumnDescription = "出生日期")] + public DateTime? Birthday { get; set; } + + /// + /// 民族 + /// + [SugarColumn(ColumnDescription = "民族", Length = 32)] + [MaxLength(32)] + public string? Nation { get; set; } + + /// + /// 手机号码 + /// + [SugarColumn(ColumnDescription = "手机号码", Length = 16)] + [MaxLength(16)] + public string? Phone { get; set; } + + /// + /// 证件类型 + /// + [SugarColumn(ColumnDescription = "证件类型")] + public CardTypeEnum CardType { get; set; } + + /// + /// 身份证号 + /// + [SugarColumn(ColumnDescription = "身份证号", Length = 32)] + [MaxLength(32)] + public string? IdCardNum { get; set; } + + /// + /// 邮箱 + /// + [SugarColumn(ColumnDescription = "邮箱", Length = 64)] + [MaxLength(64)] + public string? Email { get; set; } + + /// + /// 地址 + /// + [SugarColumn(ColumnDescription = "地址", Length = 256)] + [MaxLength(256)] + public string? Address { get; set; } + + /// + /// 文化程度 + /// + [SugarColumn(ColumnDescription = "文化程度")] + public CultureLevelEnum CultureLevel { get; set; } + + /// + /// 政治面貌 + /// + [SugarColumn(ColumnDescription = "政治面貌", Length = 16)] + [MaxLength(16)] + public string? PoliticalOutlook { get; set; } + + /// + /// 毕业院校 + /// + [SugarColumn(ColumnDescription = "毕业院校", Length = 128)] + [MaxLength(128)] + public string? College { get; set; } + + /// + /// 办公电话 + /// + [SugarColumn(ColumnDescription = "办公电话", Length = 16)] + [MaxLength(16)] + public string? OfficePhone { get; set; } + + /// + /// 紧急联系人 + /// + [SugarColumn(ColumnDescription = "紧急联系人", Length = 32)] + [MaxLength(32)] + public string? EmergencyContact { get; set; } + + /// + /// 紧急联系人电话 + /// + [SugarColumn(ColumnDescription = "紧急联系人电话", Length = 16)] + [MaxLength(16)] + public string? EmergencyPhone { get; set; } + + /// + /// 紧急联系人地址 + /// + [SugarColumn(ColumnDescription = "紧急联系人地址", Length = 256)] + [MaxLength(256)] + public string? EmergencyAddress { get; set; } + + /// + /// 个人简介 + /// + [SugarColumn(ColumnDescription = "个人简介", Length = 512)] + [MaxLength(512)] + public string? Introduction { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 状态 + /// + [SugarColumn(ColumnDescription = "状态")] + public StatusEnum Status { get; set; } = StatusEnum.Enable; + + /// + /// 备注 + /// + [SugarColumn(ColumnDescription = "备注", Length = 256)] + [MaxLength(256)] + public string? Remark { get; set; } + + /// + /// 账号类型 + /// + [SugarColumn(ColumnDescription = "账号类型")] + public AccountTypeEnum AccountType { get; set; } = AccountTypeEnum.NormalUser; + + ///// + ///// 直属机构Id + ///// + //[SugarColumn(ColumnDescription = "直属机构Id")] + //public long OrgId { get; set; } + + /// + /// 直属机构 + /// + [Navigate(NavigateType.OneToOne, nameof(OrgId))] + public SysOrg SysOrg { get; set; } + + /// + /// 直属主管Id + /// + [SugarColumn(ColumnDescription = "直属主管Id")] + public long? ManagerUserId { get; set; } + + /// + /// 直属主管 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(ManagerUserId))] + public SysUser ManagerUser { get; set; } + + /// + /// 职位Id + /// + [SugarColumn(ColumnDescription = "职位Id")] + public long PosId { get; set; } + + /// + /// 职位 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(PosId))] + public SysPos SysPos { get; set; } + + /// + /// 工号 + /// + [SugarColumn(ColumnDescription = "工号", Length = 32)] + [MaxLength(32)] + public string? JobNum { get; set; } + + /// + /// 职级 + /// + [SugarColumn(ColumnDescription = "职级", Length = 32)] + [MaxLength(32)] + public string? PosLevel { get; set; } + + /// + /// 职称 + /// + [SugarColumn(ColumnDescription = "职称", Length = 32)] + [MaxLength(32)] + public string? PosTitle { get; set; } + + /// + /// 擅长领域 + /// + [SugarColumn(ColumnDescription = "擅长领域", Length = 32)] + [MaxLength(32)] + public string? Expertise { get; set; } + + /// + /// 办公区域 + /// + [SugarColumn(ColumnDescription = "办公区域", Length = 32)] + [MaxLength(32)] + public string? OfficeZone { get; set; } + + /// + /// 办公室 + /// + [SugarColumn(ColumnDescription = "办公室", Length = 32)] + [MaxLength(32)] + public string? Office { get; set; } + + /// + /// 入职日期 + /// + [SugarColumn(ColumnDescription = "入职日期")] + public DateTime? JoinDate { get; set; } + + /// + /// 最新登录Ip + /// + [SugarColumn(ColumnDescription = "最新登录Ip", Length = 256)] + [MaxLength(256)] + public string? LastLoginIp { get; set; } + + /// + /// 最新登录地点 + /// + [SugarColumn(ColumnDescription = "最新登录地点", Length = 128)] + [MaxLength(128)] + public string? LastLoginAddress { get; set; } + + /// + /// 最新登录时间 + /// + [SugarColumn(ColumnDescription = "最新登录时间")] + public DateTime? LastLoginTime { get; set; } + + /// + /// 最新登录设备 + /// + [SugarColumn(ColumnDescription = "最新登录设备", Length = 128)] + [MaxLength(128)] + public string? LastLoginDevice { get; set; } + + /// + /// 电子签名 + /// + [SugarColumn(ColumnDescription = "电子签名", Length = 512)] + [MaxLength(512)] + public string? Signature { get; set; } + [SugarColumn(IsIgnore = true)] + public string? AccountTypeZh { get { + return this.AccountType.GetDescription(); + } } + [SugarColumn(IsIgnore = true)] + public bool IsSuperAdmin + { + get + { + return this.AccountType== AccountTypeEnum.SuperAdmin; + } + } + [SugarColumn(IsIgnore = true)] + public bool IsSysAdmin + { + get + { + return this.AccountType == AccountTypeEnum.SysAdmin; + } + } + /// + /// 验证超级管理员类型,若账号类型为超级管理员则报错 + /// + /// 自定义错误消息 + public void ValidateIsSuperAdminAccountType(ErrorCodeEnum? errorMsg = ErrorCodeEnum.D1014) + { + if (AccountType == AccountTypeEnum.SuperAdmin) + { + throw new Exception(errorMsg.GetDescription()); + } + } + + /// + /// 验证用户Id是否相同,若用户Id相同则报错 + /// + /// 用户Id + /// 自定义错误消息 + public void ValidateIsUserId(long userId, ErrorCodeEnum? errorMsg = ErrorCodeEnum.D1001) + { + if (Id == userId) + { + throw new Exception(errorMsg.GetDescription()); + } + } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysUserConfig.cs b/yy-admin-master/YY.Admin.Core/Entity/SysUserConfig.cs new file mode 100644 index 0000000..55f483d --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysUserConfig.cs @@ -0,0 +1,17 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统用户配置参数表 +/// +[SugarTable("sys_user_config", "系统用户配置参数表")] +[SysTable] +public partial class SysUserConfig : SysConfig +{ + /// + /// 无效字段,用于忽略实体类的Value字段 + /// + [SugarColumn(IsIgnore = true)] + private new string? Value { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysUserConfigData.cs b/yy-admin-master/YY.Admin.Core/Entity/SysUserConfigData.cs new file mode 100644 index 0000000..a02ebdb --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysUserConfigData.cs @@ -0,0 +1,31 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统租户配置参数值表 +/// +[SugarTable("sys_user_config_data", "系统租户配置参数值表")] +[SysTable] +[SugarIndex("index_{table}_UC", nameof(UserId), OrderByType.Asc, nameof(ConfigId), OrderByType.Asc)] +public class SysUserConfigData : EntityBaseId +{ + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 配置项Id + /// + [SugarColumn(ColumnDescription = "配置项Id")] + public long ConfigId { get; set; } + + /// + /// 参数值 + /// + [SugarColumn(ColumnDescription = "参数值", Length = 512)] + [MaxLength(512)] + public string? Value { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysUserExtOrg.cs b/yy-admin-master/YY.Admin.Core/Entity/SysUserExtOrg.cs new file mode 100644 index 0000000..ce2177c --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysUserExtOrg.cs @@ -0,0 +1,73 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统用户扩展机构表 +/// +[SugarTable("sys_user_ext_org", "系统用户扩展机构表")] +[SysTable] +public partial class SysUserExtOrg : EntityBaseId +{ + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 用户 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(UserId))] + public SysUser SysUser { get; set; } + + /// + /// 机构Id + /// + [SugarColumn(ColumnDescription = "机构Id")] + public long OrgId { get; set; } + + /// + /// 机构 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(OrgId))] + public SysOrg SysOrg { get; set; } + + /// + /// 职位Id + /// + [SugarColumn(ColumnDescription = "职位Id")] + public long PosId { get; set; } + + /// + /// 职位 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(PosId))] + public SysPos SysPos { get; set; } + + /// + /// 工号 + /// + [SugarColumn(ColumnDescription = "工号", Length = 32)] + [MaxLength(32)] + public string? JobNum { get; set; } + + /// + /// 职级 + /// + [SugarColumn(ColumnDescription = "职级", Length = 32)] + [MaxLength(32)] + public string? PosLevel { get; set; } + + /// + /// 入职日期 + /// + [SugarColumn(ColumnDescription = "入职日期")] + public DateTime? JoinDate { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysUserLdap.cs b/yy-admin-master/YY.Admin.Core/Entity/SysUserLdap.cs new file mode 100644 index 0000000..6823237 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysUserLdap.cs @@ -0,0 +1,76 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统用户域配置表 +/// +[SugarTable("sys_user_ldap", "系统用户域配置表")] +[SysTable] +[SugarIndex("index_{table}_A", nameof(Account), OrderByType.Asc)] +[SugarIndex("index_{table}_U", nameof(UserId), OrderByType.Asc)] +public class SysUserLdap : EntityBaseTenantId +{ + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 域账号 + /// AD域对应sAMAccountName + /// Ldap对应uid + /// + [SugarColumn(ColumnDescription = "域账号", Length = 32)] + [Required] + public string Account { get; set; } + + /// + /// 域用户名 + /// + [SugarColumn(ColumnDescription = "域用户名", Length = 32)] + public string UserName { get; set; } + + /// + /// 对应EmployeeId(用于数据导入对照) + /// + [SugarColumn(ColumnDescription = "对应EmployeeId", Length = 32)] + public string? EmployeeId { get; set; } + + /// + /// 组织代码 + /// + [SugarColumn(ColumnDescription = "组织代码", Length = 64)] + public string? DeptCode { get; set; } + + /// + /// 最后设置密码时间 + /// + [SugarColumn(ColumnDescription = "最后设置密码时间")] + public DateTime? PwdLastSetTime { get; set; } + + /// + /// 邮箱 + /// + [SugarColumn(ColumnDescription = "组织代码", Length = 64)] + public string? Mail { get; set; } + + /// + /// 检查账户是否已过期 + /// + [SugarColumn(ColumnDescription = "检查账户是否已过期")] + public bool AccountExpiresFlag { get; set; } = false; + + /// + /// 密码设置是否永不过期 + /// + [SugarColumn(ColumnDescription = "密码设置是否永不过期")] + public bool DontExpiresFlag { get; set; } = false; + + /// + /// DN + /// + [SugarColumn(ColumnDescription = "DN", Length = 512)] + public string Dn { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysUserMenu.cs b/yy-admin-master/YY.Admin.Core/Entity/SysUserMenu.cs new file mode 100644 index 0000000..1940ee1 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysUserMenu.cs @@ -0,0 +1,37 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统用户菜单快捷导航表 +/// +[SugarTable("sys_user_menu", "系统用户菜单快捷导航表")] +[SysTable] +public partial class SysUserMenu : EntityBaseId +{ + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 用户 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(UserId))] + public SysUser SysUser { get; set; } + + /// + /// 菜单Id + /// + [SugarColumn(ColumnDescription = "菜单Id")] + public long MenuId { get; set; } + + /// + /// 菜单 + /// + [Navigate(NavigateType.OneToOne, nameof(MenuId))] + public SysMenu SysMenu { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysUserRegWay.cs b/yy-admin-master/YY.Admin.Core/Entity/SysUserRegWay.cs new file mode 100644 index 0000000..ac15e70 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysUserRegWay.cs @@ -0,0 +1,55 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统用户注册方案表 +/// +[SugarTable("sys_user_reg_way", "系统用户注册方案表")] +[SysTable] +public partial class SysUserRegWay : EntityBaseTenant +{ + /// + /// 方案名称 + /// + [MaxLength(32)] + [SugarColumn(ColumnDescription = "方案名称", Length = 32)] + public virtual string Name { get; set; } + + /// + /// 账号类型 + /// + [SugarColumn(ColumnDescription = "账号类型")] + public virtual AccountTypeEnum AccountType { get; set; } = AccountTypeEnum.NormalUser; + + /// + /// 注册用户默认角色 + /// + [SugarColumn(ColumnDescription = "角色")] + public virtual long RoleId { get; set; } + + /// + /// 注册用户默认机构 + /// + [SugarColumn(ColumnDescription = "机构")] + public virtual long OrgId { get; set; } + + /// + /// 注册用户默认职位 + /// + [SugarColumn(ColumnDescription = "职位")] + public virtual long PosId { get; set; } + + /// + /// 排序 + /// + [SugarColumn(ColumnDescription = "排序")] + public int OrderNo { get; set; } = 100; + + /// + /// 备注 + /// + [MaxLength(128)] + [SugarColumn(ColumnDescription = "备注", Length = 128)] + public string? Remark { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Entity/SysUserRole.cs b/yy-admin-master/YY.Admin.Core/Entity/SysUserRole.cs new file mode 100644 index 0000000..3a5c25a --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Entity/SysUserRole.cs @@ -0,0 +1,37 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统用户角色表 +/// +[SugarTable("sys_user_role", "系统用户角色表")] +[SysTable] +public class SysUserRole : EntityBaseId +{ + /// + /// 用户Id + /// + [SugarColumn(ColumnDescription = "用户Id")] + public long UserId { get; set; } + + /// + /// 用户 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + [Navigate(NavigateType.OneToOne, nameof(UserId))] + public SysUser SysUser { get; set; } + + /// + /// 角色Id + /// + [SugarColumn(ColumnDescription = "角色Id")] + public long RoleId { get; set; } + + /// + /// 角色 + /// + [Navigate(NavigateType.OneToOne, nameof(RoleId))] + public SysRole SysRole { get; set; } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/AccountTypeEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/AccountTypeEnum.cs new file mode 100644 index 0000000..11e69d8 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/AccountTypeEnum.cs @@ -0,0 +1,35 @@ + + + +namespace YY.Admin.Core; + +/// +/// 账号类型枚举 +/// +[Description("账号类型枚举")] +public enum AccountTypeEnum +{ + /// + /// 超级管理员 + /// + [Description("超级管理员")] + SuperAdmin = 999, + + /// + /// 系统管理员 + /// + [Description("系统管理员")] + SysAdmin = 888, + + /// + /// 普通账号 + /// + [Description("普通账号")] + NormalUser = 777, + + /// + /// 会员 + /// + [Description("会员")] + Member = 666, +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/AlipayCertTypeEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/AlipayCertTypeEnum.cs new file mode 100644 index 0000000..0138f31 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/AlipayCertTypeEnum.cs @@ -0,0 +1,16 @@ + + +namespace YY.Admin.Core; + +/// +/// 参与方的证件类型枚举 +/// +[Description("参与方的证件类型枚举")] +public enum AlipayCertTypeEnum +{ + [Description("身份证")] + IDENTITY_CARD, + + [Description("护照")] + PASSPORT +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/AlipayIdentityTypeEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/AlipayIdentityTypeEnum.cs new file mode 100644 index 0000000..2c7f0cb --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/AlipayIdentityTypeEnum.cs @@ -0,0 +1,16 @@ + + +namespace YY.Admin.Core; + +/// +/// 参与方的标识类型枚举 +/// +[Description("参与方的标识类型枚举")] +public enum AlipayIdentityTypeEnum +{ + [Description("支付宝用户UID")] + ALIPAY_USER_ID, + + [Description("支付宝登录号")] + ALIPAY_LOGON_ID +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/CacheTypeEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/CacheTypeEnum.cs new file mode 100644 index 0000000..d1e953d --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/CacheTypeEnum.cs @@ -0,0 +1,22 @@ + + +namespace YY.Admin.Core; + +/// +/// 缓存类型枚举 +/// +[Description("缓存类型枚举")] +public enum CacheTypeEnum +{ + /// + /// 内存缓存 + /// + [Description("内存缓存")] + Memory, + + /// + /// Redis缓存 + /// + [Description("Redis缓存")] + Redis +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/CardTypeEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/CardTypeEnum.cs new file mode 100644 index 0000000..e6eca8c --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/CardTypeEnum.cs @@ -0,0 +1,46 @@ + + +namespace YY.Admin.Core; + +/// +/// 证件类型枚举 +/// +[Description("证件类型枚举")] +public enum CardTypeEnum +{ + /// + /// 身份证 + /// + [Description("身份证")] + IdCard = 0, + + /// + /// 护照 + /// + [Description("护照")] + PassportCard = 1, + + /// + /// 出生证 + /// + [Description("出生证")] + BirthCard = 2, + + /// + /// 港澳台通行证 + /// + [Description("港澳台通行证")] + GatCard = 3, + + /// + /// 外国人居留证 + /// + [Description("外国人居留证")] + ForeignCard = 4, + + /// + /// 营业执照 + /// + [Description("营业执照")] + License = 5, +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/CryptogramEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/CryptogramEnum.cs new file mode 100644 index 0000000..06b6fe0 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/CryptogramEnum.cs @@ -0,0 +1,28 @@ + + +namespace YY.Admin.Core; + +/// +/// 密码加密枚举 +/// +[Description("密码加密枚举")] +public enum CryptogramEnum +{ + /// + /// MD5 + /// + [Description("MD5")] + MD5 = 0, + + /// + /// SM2(国密) + /// + [Description("SM2")] + SM2 = 1, + + /// + /// SM4(国密) + /// + [Description("SM4")] + SM4 = 2 +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/CultureLevelEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/CultureLevelEnum.cs new file mode 100644 index 0000000..7fb57ec --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/CultureLevelEnum.cs @@ -0,0 +1,88 @@ + + +namespace YY.Admin.Core; + +/// +/// 文化程度枚举 +/// +[Description("文化程度枚举")] +public enum CultureLevelEnum +{ + /// + /// 其他 + /// + [Description("其他"), Theme("info")] + Level0 = 0, + + /// + /// 文盲 + /// + [Description("文盲")] + Level1 = 1, + + /// + /// 小学 + /// + [Description("小学")] + Level2 = 2, + + /// + /// 初中 + /// + [Description("初中")] + Level3 = 3, + + /// + /// 普通高中 + /// + [Description("普通高中")] + Level4 = 4, + + /// + /// 技工学校 + /// + [Description("技工学校")] + Level5 = 5, + + /// + /// 职业教育 + /// + [Description("职业教育")] + Level6 = 6, + + /// + /// 职业高中 + /// + [Description("职业高中")] + Level7 = 7, + + /// + /// 中等专科 + /// + [Description("中等专科")] + Level8 = 8, + + /// + /// 大学专科 + /// + [Description("大学专科")] + Level9 = 9, + + /// + /// 大学本科 + /// + [Description("大学本科")] + Level10 = 10, + + /// + /// 硕士研究生 + /// + [Description("硕士研究生")] + Level11 = 11, + + /// + /// 博士研究生 + /// + [Description("博士研究生")] + Level12 = 12, +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/DataOpTypeEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/DataOpTypeEnum.cs new file mode 100644 index 0000000..663ad96 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/DataOpTypeEnum.cs @@ -0,0 +1,88 @@ + + +namespace YY.Admin.Core; + +/// +/// 数据操作类型枚举 +/// +[Description("数据操作类型枚举")] +public enum DataOpTypeEnum +{ + /// + /// 其它 + /// + [Description("其它"), Theme("info")] + Other, + + /// + /// 增加 + /// + [Description("增加")] + Add, + + /// + /// 删除 + /// + [Description("删除")] + Delete, + + /// + /// 编辑 + /// + [Description("编辑")] + Edit, + + /// + /// 更新 + /// + [Description("更新")] + Update, + + /// + /// 查询 + /// + [Description("查询")] + Query, + + /// + /// 详情 + /// + [Description("详情")] + Detail, + + /// + /// 树 + /// + [Description("树")] + Tree, + + /// + /// 导入 + /// + [Description("导入")] + Import, + + /// + /// 导出 + /// + [Description("导出")] + Export, + + /// + /// 授权 + /// + [Description("授权")] + Grant, + + /// + /// 强退 + /// + [Description("强退")] + Force, + + /// + /// 清空 + /// + [Description("清空")] + Clean +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/DataScopeEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/DataScopeEnum.cs new file mode 100644 index 0000000..96a5abd --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/DataScopeEnum.cs @@ -0,0 +1,40 @@ + + +namespace YY.Admin.Core; + +/// +/// 角色数据范围枚举 +/// +[Description("角色数据范围枚举")] +public enum DataScopeEnum +{ + /// + /// 全部数据 + /// + [Description("全部数据")] + All = 1, + + /// + /// 本部门及以下数据 + /// + [Description("本部门及以下数据")] + DeptChild = 2, + + /// + /// 本部门数据 + /// + [Description("本部门数据")] + Dept = 3, + + /// + /// 仅本人数据 + /// + [Description("仅本人数据")] + Self = 4, + + /// + /// 自定义数据 + /// + [Description("自定义数据")] + Define = 5 +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/ElasticSearchAuthTypeEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/ElasticSearchAuthTypeEnum.cs new file mode 100644 index 0000000..302891d --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/ElasticSearchAuthTypeEnum.cs @@ -0,0 +1,29 @@ + + +namespace YY.Admin.Core; + +/// +/// ES认证类型枚举 +/// https://www.elastic.co/guide/en/elasticsearch/client/net-api/current/_options_on_elasticsearchclientsettings.html +/// +[Description("ES认证类型枚举")] +public enum ElasticSearchAuthTypeEnum +{ + /// + /// BasicAuthentication + /// + [Description("BasicAuthentication")] + Basic = 1, + + /// + /// ApiKey + /// + [Description("ApiKey")] + ApiKey = 2, + + /// + /// Base64ApiKey + /// + [Description("Base64ApiKey")] + Base64ApiKey = 3 +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/ErrorCodeEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/ErrorCodeEnum.cs new file mode 100644 index 0000000..ded4f8b --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/ErrorCodeEnum.cs @@ -0,0 +1,904 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统错误码 +/// +[Description("系统错误码")] +public enum ErrorCodeEnum +{ + /// + /// 验证码错误 + /// + [Description("验证码错误")] + D0008, + + /// + /// 账号不存在 + /// + [Description("账号不存在")] + D0009, + + /// + /// 密匙不匹配 + /// + [Description("密匙不匹配")] + D0010, + + /// + /// 密码不正确 + /// + [Description("密码不正确")] + D1000, + + /// + /// 非法操作!禁止删除自己 + /// + [Description("非法操作,禁止删除自己")] + D1001, + + /// + /// 记录不存在 + /// + [Description("记录不存在")] + D1002, + + /// + /// 账号已存在 + /// + [Description("账号已存在")] + D1003, + + /// + /// 旧密码不匹配 + /// + [Description("旧密码输入错误")] + D1004, + + /// + /// 测试数据禁止更改admin密码 + /// + [Description("测试数据禁止更改用户【admin】密码")] + D1005, + + /// + /// 数据已存在 + /// + [Description("数据已存在")] + D1006, + + /// + /// 数据不存在或含有关联引用,禁止删除 + /// + [Description("数据不存在或含有关联引用,禁止删除")] + D1007, + + /// + /// 禁止为管理员分配角色 + /// + [Description("禁止为管理员分配角色")] + D1008, + + /// + /// 重复数据或记录含有不存在数据 + /// + [Description("重复数据或记录含有不存在数据")] + D1009, + + /// + /// 禁止为超级管理员角色分配权限 + /// + [Description("禁止为超级管理员角色分配权限")] + D1010, + + /// + /// 非法操作,未登录 + /// + [Description("非法操作,未登录")] + D1011, + + /// + /// Id不能为空 + /// + [Description("Id不能为空")] + D1012, + + /// + /// 所属机构不在自己的数据范围内 + /// + [Description("没有权限操作该数据")] + D1013, + + /// + /// 禁止删除超级管理员 + /// + [Description("禁止删除超级管理员")] + D1014, + + /// + /// 禁止修改超级管理员状态 + /// + [Description("禁止修改超级管理员状态")] + D1015, + + /// + /// 没有权限 + /// + [Description("没有权限")] + D1016, + + /// + /// 账号已冻结 + /// + [Description("账号已冻结")] + D1017, + + /// + /// 该租户下角色菜单权限集为空 + /// + [Description("该租户下角色菜单权限集为空")] + D1019, + + /// + /// 禁止删除默认租户 + /// + [Description("禁止删除默认租户")] + D1023, + + /// + /// 已将其他地方登录账号下线 + /// + [Description("已将其他地方登录账号下线")] + D1024, + + /// + /// 此角色下面存在账号禁止删除 + /// + [Description("此角色下面存在账号禁止删除")] + D1025, + + /// + /// 禁止修改本人账号状态 + /// + [Description("禁止修改本人账号状态")] + D1026, + + /// + /// 密码错误次数过多,账号已锁定,请半小时后重试! + /// + [Description("密码错误次数过多,账号已锁定,请半小时后重试!")] + D1027, + + /// + /// 新密码不能与旧密码相同 + /// + [Description("新密码不能与旧密码相同")] + D1028, + + /// + /// 系统默认账号禁止删除 + /// + [Description("系统默认账号禁止删除")] + D1029, + + /// + /// 开放接口绑定账号禁止删除 + /// + [Description("开放接口绑定账号禁止删除")] + D1030, + + /// + /// 开放接口绑定租户禁止删除 + /// + [Description("开放接口绑定租户禁止删除")] + D1031, + + /// + /// 手机号已存在 + /// + [Description("手机号已存在")] + D1032, + + /// + /// 此角色下存在注册方案禁止删除 + /// + [Description("此角色下存在注册方案禁止删除")] + D1033, + + /// + /// 注册功能未开启禁止注册 + /// + [Description("注册功能未开启禁止注册")] + D1034, + + /// + /// 注册方案不存在 + /// + [Description("注册方案不存在")] + D1035, + + /// + /// 角色不存在 + /// + [Description("角色不存在")] + D1036, + + /// + /// 禁止注册超级管理员和系统管理员 + /// + [Description("禁止注册超级管理员和系统管理员")] + D1037, + + /// + /// 禁止越权操作系统账户 + /// + [Description("禁止越权操作系统账户")] + D1038, + + /// + /// 父机构不存在 + /// + [Description("父机构不存在")] + D2000, + + /// + /// 当前机构Id不能与父机构Id相同 + /// + [Description("当前机构Id不能与父机构Id相同")] + D2001, + + /// + /// 已有相同组织机构,编码或名称相同 + /// + [Description("已有相同组织机构,编码或名称相同")] + D2002, + + /// + /// 没有权限操作机构 + /// + [Description("没有权限操作机构")] + D2003, + + /// + /// 该机构下有用户禁止删除 + /// + [Description("该机构下有用户禁止删除")] + D2004, + + /// + /// 附属机构下有用户禁止删除 + /// + [Description("附属机构下有用户禁止删除")] + D2005, + + /// + /// 只能增加下级机构 + /// + [Description("只能增加下级机构")] + D2006, + + /// + /// 下级机构下有用户禁止删除 + /// + [Description("下级机构下有用户禁止删除")] + D2007, + + /// + /// 系统默认机构禁止删除 + /// + [Description("系统默认机构禁止删除")] + D2008, + + /// + /// 禁止增加根节点机构 + /// + [Description("禁止增加根节点机构")] + D2009, + + /// + /// 此机构下存在注册方案禁止删除 + /// + [Description("此机构下存在注册方案禁止删除")] + D2010, + + /// + /// 机构不存在 + /// + [Description("机构不存在")] + D2011, + + /// + /// 系统默认机构禁止修改 + /// + [Description("系统默认机构禁止修改")] + D2012, + + /// + /// 字典类型不存在 + /// + [Description("字典类型不存在")] + D3000, + + /// + /// 字典类型已存在 + /// + [Description("字典类型已存在,名称或编码重复")] + D3001, + + /// + /// 字典类型下面有字典值禁止删除 + /// + [Description("字典类型下面有字典值禁止删除")] + D3002, + + /// + /// 字典值已存在 + /// + [Description("字典值已存在")] + D3003, + + /// + /// 字典值不存在 + /// + [Description("字典值不存在")] + D3004, + + /// + /// 字典状态错误 + /// + [Description("字典状态错误")] + D3005, + + /// + /// 字典编码不能以Enum结尾 + /// + [Description("字典编码不能以Enum结尾")] + D3006, + + /// + /// 禁止修改枚举类型的字典编码 + /// + [Description("禁止修改枚举类型的字典编码")] + D3007, + + /// + /// 禁止迁移枚举字典 + /// + [Description("禁止迁移枚举字典")] + D3008, + + /// + /// 字典已在该租户禁止迁移 + /// + [Description("字典已在该租户禁止迁移")] + D3009, + + /// + /// 非超管用户禁止操作系统字典 + /// + [Description("非超管用户禁止操作系统字典")] + D3010, + + /// + /// 获取字典值集合入参有误 + /// + [Description("获取字典值集合入参有误")] + D3011, + + /// + /// 禁止修改租户字典状态 + /// + [Description("禁止修改租户字典状态")] + D3012, + + /// + /// 菜单已存在 + /// + [Description("菜单已存在")] + D4000, + + /// + /// 路由地址为空 + /// + [Description("路由地址为空")] + D4001, + + /// + /// 打开方式为空 + /// + [Description("打开方式为空")] + D4002, + + /// + /// 权限标识格式为空 + /// + [Description("权限标识格式为空")] + D4003, + + /// + /// 权限标识格式错误 + /// + [Description("权限标识格式错误 如xxx:xxx")] + D4004, + + /// + /// 权限不存在 + /// + [Description("权限不存在")] + D4005, + + /// + /// 父级菜单不能为当前节点,请重新选择父级菜单 + /// + [Description("父级菜单不能为当前节点,请重新选择父级菜单")] + D4006, + + /// + /// 不能移动根节点 + /// + [Description("不能移动根节点")] + D4007, + + /// + /// 禁止本节点与父节点相同 + /// + [Description("禁止本节点与父节点相同")] + D4008, + + /// + /// 路由名称重复 + /// + [Description("路由名称重复")] + D4009, + + /// + /// 父节点不能为按钮类型 + /// + [Description("父节点不能为按钮类型")] + D4010, + + /// + /// 租户不能为空 + /// + [Description("租户不能为空")] + D4011, + + /// + /// 系统菜单禁止修改 + /// + [Description("系统菜单禁止修改")] + D4012, + + /// + /// 系统菜单禁止删除 + /// + [Description("系统菜单禁止删除")] + D4013, + + /// + /// 已存在同名或同编码应用 + /// + [Description("已存在同名或同编码应用")] + D5000, + + /// + /// 默认激活系统只能有一个 + /// + [Description("默认激活系统只能有一个")] + D5001, + + /// + /// 该应用下有菜单禁止删除 + /// + [Description("该应用下有菜单禁止删除")] + D5002, + + /// + /// 已存在同名或同编码应用 + /// + [Description("已存在同名或同编码应用")] + D5003, + + /// + /// 已存在同名或同编码职位 + /// + [Description("已存在同名或同编码职位")] + D6000, + + /// + /// 该职位下有用户禁止删除 + /// + [Description("该职位下有用户禁止删除")] + D6001, + + /// + /// 无权修改本职位 + /// + [Description("无权修改本职位")] + D6002, + + /// + /// 职位不存在 + /// + [Description("职位不存在")] + D6003, + + /// + /// 此职位下存在注册方案禁止删除 + /// + [Description("此职位下存在注册方案禁止删除")] + D6004, + + /// + /// 通知公告状态错误 + /// + [Description("通知公告状态错误")] + D7000, + + /// + /// 通知公告删除失败 + /// + [Description("通知公告删除失败")] + D7001, + + /// + /// 通知公告编辑失败 + /// + [Description("通知公告编辑失败,类型必须为草稿")] + D7002, + + /// + /// 通知公告操作失败,非发布者不能进行操作 + /// + [Description("通知公告操作失败,非发布者不能进行操作")] + D7003, + + /// + /// 文件不存在 + /// + [Description("文件不存在")] + D8000, + + /// + /// 不允许的文件类型 + /// + [Description("不允许的文件类型")] + D8001, + + /// + /// 文件超过允许大小 + /// + [Description("文件超过允许大小")] + D8002, + + /// + /// 文件后缀错误 + /// + [Description("文件后缀错误")] + D8003, + + /// + /// 文件已存在 + /// + [Description("文件已存在")] + D8004, + + /// + /// 无效的文件名 + /// + [Description("无效的文件名")] + D8005, + + /// + /// 已存在同名或同编码参数配置 + /// + [Description("已存在同名或同编码参数配置")] + D9000, + + /// + /// 禁止删除系统参数 + /// + [Description("禁止删除系统参数")] + D9001, + + /// + /// 已存在同名任务调度 + /// + [Description("已存在同名任务调度")] + D1100, + + /// + /// 任务调度不存在 + /// + [Description("任务调度不存在")] + D1101, + + /// + /// 演示环境禁止修改数据 + /// + [Description("演示环境禁止修改数据")] + D1200, + + /// + /// 已存在同名的租户 + /// + [Description("已存在同名的租户")] + D1300, + + /// + /// 已存在同名的租户管理员 + /// + [Description("已存在同名的租户管理员")] + D1301, + + /// + /// 租户从库配置错误 + /// + [Description("租户从库配置错误")] + D1302, + + /// + /// 已存在同名的租户域名 + /// + [Description("已存在同名的租户域名")] + D1303, + + /// + /// 授权菜单存在重复项 + /// + [Description("授权菜单存在重复项")] + D1304, + + /// + /// 该表代码模板已经生成过 + /// + [Description("该表代码模板已经生成过")] + D1400, + + /// + /// 数据库配置不存在 + /// + [Description("数据库配置不存在")] + D1401, + + /// + /// 该类型不存在 + /// + [Description("该类型不存在")] + D1501, + + /// + /// 该字段不存在 + /// + [Description("该字段不存在")] + D1502, + + /// + /// 该类型不是枚举类型 + /// + [Description("该类型不是枚举类型")] + D1503, + + /// + /// 该实体不存在 + /// + [Description("该实体不存在")] + D1504, + + /// + /// 父菜单不存在 + /// + [Description("父菜单不存在")] + D1505, + + /// + /// 父资源不存在 + /// + [Description("父资源不存在")] + D1600, + + /// + /// 当前资源Id不能与父资源Id相同 + /// + [Description("当前资源Id不能与父资源Id相同")] + D1601, + + /// + /// 已有相同编码或名称 + /// + [Description("已有相同编码或名称")] + D1602, + + /// + /// 脚本代码不能为空 + /// + [Description("脚本代码不能为空")] + D1701, + + /// + /// 脚本代码中的作业类,需要定义 [JobDetail] 特性 + /// + [Description("脚本代码中的作业类,需要定义 [JobDetail] 特性")] + D1702, + + /// + /// 作业编号需要与脚本代码中的作业类 [JobDetail('jobId')] 一致 + /// + [Description("作业编号需要与脚本代码中的作业类 [JobDetail('jobId')] 一致")] + D1703, + + /// + /// 禁止修改作业编号 + /// + [Description("禁止修改作业编号")] + D1704, + + /// + /// 执行作业失败 + /// + [Description("执行作业失败")] + D1705, + + /// + /// 已存在同名打印模板 + /// + [Description("已存在同名打印模板")] + D1800, + + /// + /// 已存在同名功能或同名程序及插件 + /// + [Description("已存在同名功能或同名程序及插件")] + D1900, + + /// + /// 注册方案名称已存在 + /// + [Description("注册方案名称已存在")] + D2101, + + /// + /// 已存在同名模板 + /// + [Description("已存在同名模板")] + T1000, + + /// + /// 已存在相同编码模板 + /// + [Description("已存在相同编码模板")] + T1001, + + /// + /// 禁止删除存在关联租户的应用 + /// + [Description("禁止删除存在关联租户的应用")] + A1001, + + /// + /// 禁止删除存在关联菜单的应用 + /// + [Description("禁止删除存在关联菜单的应用")] + A1002, + + /// + /// 找不到系统应用 + /// + [Description("找不到系统应用")] + A1000, + + /// + /// 已存在同名或同编码项目 + /// + [Description("已存在同名或同编码项目")] + xg1000, + + /// + /// 已存在相同证件号码人员 + /// + [Description("已存在相同证件号码人员")] + xg1001, + + /// + /// 检测数据不存在 + /// + [Description("检测数据不存在")] + xg1002, + + /// + /// 请添加数据列 + /// + [Description("请添加数据列")] + db1000, + + /// + /// 数据表不存在 + /// + [Description("数据表不存在")] + db1001, + + /// + /// 数据表不存在 + /// + [Description("不允许添加相同字段名")] + db1002, + + /// + /// 实体文件不存在或匹配不到。如果是刚刚生成的实体,请重启服务后再试 + /// + [Description("实体文件不存在或匹配不到。如果是刚刚生成的实体,请重启服务后再试")] + db1003, + + /// + /// 父节点不存在 + /// + [Description("父节点不存在")] + R2000, + + /// + /// 当前节点Id不能与父节点Id相同 + /// + [Description("当前节点Id不能与父节点Id相同")] + R2001, + + /// + /// 已有相同编码或名称 + /// + [Description("已有相同编码或名称")] + R2002, + + /// + /// 行政区代码只能为6、9或12位 + /// + [Description("行政区代码只能为6、9或12位")] + R2003, + + /// + /// 父节点不能为自己的子节点 + /// + [Description("父节点不能为自己的子节点")] + R2004, + + /// + /// 同步国家统计局数据异常,请稍后重试 + /// + [Description("同步国家统计局数据异常,请稍后重试")] + R2005, + + /// + /// 默认租户状态禁止修改 + /// + [Description("默认租户状态禁止修改")] + Z1001, + + /// + /// 禁止创建此类型的数据库 + /// + [Description("禁止创建此类型的数据库")] + Z1002, + + /// + /// 租户不存在或已禁用 + /// + [Description("租户不存在或已禁用")] + Z1003, + + /// + /// 租户库连接不能为空 + /// + [Description("租户库连接不能为空")] + Z1004, + + /// + /// 身份标识已存在 + /// + [Description("身份标识已存在")] + O1000, + + /// + /// 禁止非超级管理员操作 + /// + [Description("禁止非超级管理员操作")] + SA001 +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/FilterLogicEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/FilterLogicEnum.cs new file mode 100644 index 0000000..83c94e9 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/FilterLogicEnum.cs @@ -0,0 +1,32 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace YY.Admin.Core; + +/// +/// 过滤条件 +/// +[Description("过滤条件")] +public enum FilterLogicEnum +{ + /// + /// 并且 + /// + [Description("并且")] + And, + + /// + /// 或者 + /// + [Description("或者")] + Or, + + /// + /// 异或 + /// + [Description("异或")] + Xor +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/FilterOperateEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/FilterOperateEnum.cs new file mode 100644 index 0000000..3bcc61c --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/FilterOperateEnum.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.ComponentModel; + + +namespace YY.Admin.Core +{ + /// + /// 筛选操作方式 + /// + public enum FilterOperateEnum + { + /// + /// 并且 + /// + [OperateCode("and")] + [Description("并且")] + And = 1, + + /// + /// 或者 + /// + [OperateCode("or")] + [Description("或者")] + Or = 2, + + /// + /// 等于 + /// + [OperateCode("equal")] + [Description("等于")] + Equal = 3, + + /// + /// 不等于 + /// + [OperateCode("notequal")] + [Description("不等于")] + NotEqual = 4, + + /// + /// 小于 + /// + [OperateCode("less")] + [Description("小于")] + Less = 5, + + /// + /// 小于或等于 + /// + [OperateCode("lessorequal")] + [Description("小于等于")] + LessOrEqual = 6, + + /// + /// 大于 + /// + [OperateCode("greater")] + [Description("大于")] + Greater = 7, + + /// + /// 大于或等于 + /// + [OperateCode("greaterorequal")] + [Description("大于等于")] + GreaterOrEqual = 8, + + /// + /// 以……开始 + /// + [OperateCode("startswith")] + [Description("开始于")] + StartsWith = 9, + + /// + /// 以……结束 + /// + [OperateCode("endswith")] + [Description("结束于")] + EndsWith = 10, + + /// + /// 字符串的包含(相似) + /// + [OperateCode("contains")] + [Description("包含")] + Contains = 11, + + /// + /// 字符串的不包含 + /// + [OperateCode("notcontains")] + [Description("不包含")] + NotContains = 12 + } +} diff --git a/yy-admin-master/YY.Admin.Core/Enum/FilterOperatorEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/FilterOperatorEnum.cs new file mode 100644 index 0000000..71026f0 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/FilterOperatorEnum.cs @@ -0,0 +1,68 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace YY.Admin.Core; + +/// +/// 过滤逻辑运算符 +/// +[Description("过滤逻辑运算符")] +public enum FilterOperatorEnum +{ + /// + /// 等于(=) + /// + [Description("等于")] + EQ, + + /// + /// 不等于(!=) + /// + [Description("不等于")] + NEQ, + + /// + /// 小于 + /// + [Description("小于")] + LT, + + /// + /// 小于等于 + /// + [Description("小于等于")] + LTE, + + /// + /// 大于(>) + /// + [Description("大于")] + GT, + + /// + /// 大于等于(>=) + /// + [Description("大于等于")] + GTE, + + /// + /// 开始包含 + /// + [Description("开始包含")] + StartsWith, + + /// + /// 末尾包含 + /// + [Description("末尾包含")] + EndsWith, + + /// + /// 包含 + /// + [Description("包含")] + Contains +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/FinishStatusEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/FinishStatusEnum.cs new file mode 100644 index 0000000..7bff7b8 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/FinishStatusEnum.cs @@ -0,0 +1,22 @@ + + +namespace YY.Admin.Core; + +/// +/// 完成状态枚举 +/// +[Description("完成状态枚举")] +public enum FinishStatusEnum +{ + /// + /// 已完成 + /// + [Description("已完成"), Theme("success")] + Finish = 1, + + /// + /// 未完成 + /// + [Description("未完成"), Theme("danger")] + UnFinish = 0, +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/GenderEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/GenderEnum.cs new file mode 100644 index 0000000..b7d3a72 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/GenderEnum.cs @@ -0,0 +1,34 @@ + + +namespace YY.Admin.Core; + +/// +/// 性别枚举(GB/T 2261.1-2003) +/// +[Description("性别枚举")] +public enum GenderEnum +{ + /// + /// 未知的性别 + /// + [Description("未知的性别"), Theme("info")] + Unknown = 0, + + /// + /// 男性 + /// + [Description("男性"), Theme("success")] + Male = 1, + + /// + /// 女性 + /// + [Description("女性"), Theme("danger")] + Female = 2, + + /// + /// 未说明的性别 + /// + [Description("未说明的性别"), Theme("warning")] + Unspecified = 9 +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/HttpMethodEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/HttpMethodEnum.cs new file mode 100644 index 0000000..742336a --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/HttpMethodEnum.cs @@ -0,0 +1,64 @@ + + +namespace YY.Admin.Core; + +/// +/// HTTP请求方法枚举 +/// +[Description("HTTP请求方法枚举")] +public enum HttpMethodEnum +{ + /// + /// HTTP "GET" method. + /// + [Description("HTTP \"GET\" method.")] + Get, + + /// + /// HTTP "POST" method. + /// + [Description("HTTP \"POST\" method.")] + Post, + + /// + /// HTTP "PUT" method. + /// + [Description(" HTTP \"PUT\" method.")] + Put, + + /// + /// HTTP "DELETE" method. + /// + [Description("HTTP \"DELETE\" method.")] + Delete, + + /// + /// HTTP "PATCH" method. + /// + [Description("HTTP \"PATCH\" method. ")] + Patch, + + /// + /// HTTP "HEAD" method. + /// + [Description("HTTP \"HEAD\" method.")] + Head, + + /// + /// HTTP "OPTIONS" method. + /// + [Description("HTTP \"OPTIONS\" method.")] + Options, + + /// + /// HTTP "TRACE" method. + /// + [Description(" HTTP \"TRACE\" method.")] + Trace, + + /// + /// HTTP "CONNECT" method. + /// + [Description("HTTP \"CONNECT\" method.")] + Connect +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/IconTypeEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/IconTypeEnum.cs new file mode 100644 index 0000000..0a8d2ab --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/IconTypeEnum.cs @@ -0,0 +1,12 @@ +namespace YY.Admin.Core +{ + /// + /// 图标类型枚举 + /// + public enum IconTypeEnum + { + AntDesign, + MaterialDesign, + FontAwesome + } +} diff --git a/yy-admin-master/YY.Admin.Core/Enum/JobCreateTypeEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/JobCreateTypeEnum.cs new file mode 100644 index 0000000..8b0347b --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/JobCreateTypeEnum.cs @@ -0,0 +1,28 @@ + + +namespace YY.Admin.Core; + +/// +/// 作业创建类型枚举 +/// +[Description("作业创建类型枚举")] +public enum JobCreateTypeEnum +{ + /// + /// 内置 + /// + [Description("内置"), Theme("info")] + BuiltIn = 0, + + /// + /// 脚本 + /// + [Description("脚本")] + Script = 1, + + /// + /// HTTP请求 + /// + [Description("HTTP请求")] + Http = 2, +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/JobStatusEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/JobStatusEnum.cs new file mode 100644 index 0000000..5214526 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/JobStatusEnum.cs @@ -0,0 +1,34 @@ + + +namespace YY.Admin.Core; + +/// +/// 岗位状态枚举 +/// +[Description("岗位状态枚举")] +public enum JobStatusEnum +{ + /// + /// 在职 + /// + [Description("在职")] + On = 1, + + /// + /// 离职 + /// + [Description("离职")] + Off = 2, + + /// + /// 请假 + /// + [Description("请假")] + Leave = 3, + + /// + /// 其他 + /// + [Description("其他")] + Other = 4, +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/LoginModeEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/LoginModeEnum.cs new file mode 100644 index 0000000..fefb3ac --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/LoginModeEnum.cs @@ -0,0 +1,22 @@ + + +namespace YY.Admin.Core; + +/// +/// 登录模式枚举 +/// +[Description("登录模式枚举")] +public enum LoginModeEnum +{ + /// + /// PC模式 + /// + [Description("PC模式")] + PC = 1, + + /// + /// APP + /// + [Description("APP")] + APP = 2 +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/LoginTypeEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/LoginTypeEnum.cs new file mode 100644 index 0000000..1bc8175 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/LoginTypeEnum.cs @@ -0,0 +1,28 @@ + + +namespace YY.Admin.Core; + +/// +/// 登录类型枚举 +/// +[Description("登录类型枚举")] +public enum LoginTypeEnum +{ + /// + /// PC登录 + /// + [Description("PC登录")] + Login = 1, + + /// + /// PC退出 + /// + [Description("PC退出")] + Logout = 2, + + /// + /// PC注册 + /// + [Description("PC注册")] + Register = 3 +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/MaritalStatusEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/MaritalStatusEnum.cs new file mode 100644 index 0000000..e678438 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/MaritalStatusEnum.cs @@ -0,0 +1,46 @@ + + +namespace YY.Admin.Core; + +/// +/// 婚姻状况枚举 +/// +[Description("婚姻状况枚举")] +public enum MaritalStatusEnum +{ + /// + /// 未婚 + /// + [Description("未婚")] + UnMarried = 1, + + /// + /// 已婚 + /// + [Description("已婚")] + Married = 2, + + /// + /// 离异 + /// + [Description("离异")] + Divorce = 3, + + /// + /// 再婚 + /// + [Description("再婚")] + Remarry = 4, + + /// + /// 丧偶 + /// + [Description("丧偶")] + Widowed = 5, + + /// + /// 未知 + /// + [Description("未知")] + None = 6, +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/MenuTypeEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/MenuTypeEnum.cs new file mode 100644 index 0000000..687ab3c --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/MenuTypeEnum.cs @@ -0,0 +1,29 @@ + + +namespace YY.Admin.Core; + +/// +/// 系统菜单类型枚举 +/// +[Description("系统菜单类型枚举")] +public enum MenuTypeEnum +{ + /// + /// 目录 + /// + + [Description("目录"), Theme("warning")] + Dir = 1, + + /// + /// 菜单 + /// + [Description("菜单")] + Menu = 2, + + /// + /// 按钮 + /// + [Description("按钮"), Theme("info")] + Btn = 3 +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/MessageTypeEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/MessageTypeEnum.cs new file mode 100644 index 0000000..f8d2c02 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/MessageTypeEnum.cs @@ -0,0 +1,34 @@ + + +namespace YY.Admin.Core; + +/// +/// 消息类型枚举 +/// +[Description("消息类型枚举")] +public enum MessageTypeEnum +{ + /// + /// 普通信息 + /// + [Description("消息"), Theme("info")] + Info = 0, + + /// + /// 成功提示 + /// + [Description("成功"), Theme("success")] + Success = 1, + + /// + /// 警告提示 + /// + [Description("警告"), Theme("warning")] + Warning = 2, + + /// + /// 错误提示 + /// + [Description("错误"), Theme("danger")] + Error = 3 +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/NationEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/NationEnum.cs new file mode 100644 index 0000000..34e06d9 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/NationEnum.cs @@ -0,0 +1,346 @@ + + +namespace YY.Admin.Core; + +/// +/// 民族枚举 +/// +[Description("民族枚举")] +public enum NationEnum +{ + /// + /// 汉族 + /// + [Description("汉族")] + HanZu = 1, + + /// + /// 壮族 + /// + [Description("壮族")] + ZhuangZu = 2, + + /// + /// 满族 + /// + [Description("满族")] + ManZu = 3, + + /// + /// 回族 + /// + [Description("回族")] + HuiZu = 4, + + /// + /// 苗族 + /// + [Description("苗族")] + MiaoZu = 5, + + /// + /// 维吾尔族 + /// + [Description("维吾尔族")] + WeiWuErZu = 6, + + /// + /// 土家族 + /// + [Description("土家族")] + TuJiaZu = 7, + + /// + /// 彝族 + /// + [Description("彝族")] + YiZu = 8, + + /// + /// 蒙古族 + /// + [Description("蒙古族")] + MengGuZu = 9, + + /// + /// 藏族 + /// + [Description("藏族")] + ZangZu = 10, + + /// + /// 布依族 + /// + [Description("布依族")] + BuYiZu = 11, + + /// + /// 侗族 + /// + [Description("侗族")] + DongZu = 12, + + /// + /// 瑶族 + /// + [Description("瑶族")] + YaoZu = 13, + + /// + /// 朝鲜族 + /// + [Description("朝鲜族")] + ChaoXianZu = 14, + + /// + /// 白族 + /// + [Description("白族")] + BaiZu = 15, + + /// + /// 哈尼族 + /// + [Description("哈尼族")] + HaNiZu = 16, + + /// + /// 哈萨克族 + /// + [Description("哈萨克族")] + HaSaKeZu = 17, + + /// + /// 黎族 + /// + [Description("黎族")] + LiZu = 18, + + /// + /// 傣族 + /// + [Description("傣族")] + DaiZu = 19, + + /// + /// 畲族 + /// + [Description("畲族")] + SheZu = 20, + + /// + /// 傈僳族 + /// + [Description("傈僳族")] + LiSuZu = 21, + + /// + /// 仡佬族 + /// + [Description("仡佬族")] + GeLaoZu = 22, + + /// + /// 拉祜族 + /// + [Description("拉祜族")] + LaHuZu = 23, + + /// + /// 东乡族 + /// + [Description("东乡族")] + DongXiangZu = 24, + + /// + /// 纳西族 + /// + [Description("纳西族")] + NaXiZu = 25, + + /// + /// 景颇族 + /// + [Description("景颇族")] + JingPoZu = 26, + + /// + /// 柯尔克孜族 + /// + [Description("柯尔克孜族")] + KeErKeZiZu = 27, + + /// + /// 土族 + /// + [Description("土族")] + TuZu = 28, + + /// + /// 达斡尔族 + /// + [Description("达斡尔族")] + DaWoErZu = 29, + + /// + /// 仫佬族 + /// + [Description("仫佬族")] + MuLaoZu = 30, + + /// + /// 羌族 + /// + [Description("羌族")] + QiangZu = 31, + + /// + /// 布朗族 + /// + [Description("布朗族")] + BuLangZu = 32, + + /// + /// 撒拉族 + /// + [Description("撒拉族")] + SaLaZu = 33, + + /// + /// 毛南族 + /// + [Description("毛南族")] + MaoNanZu = 34, + + /// + /// 仡族 + /// + [Description("仡族")] + GeZu = 35, + + /// + /// 锡伯族 + /// + [Description("锡伯族")] + XiBoZu = 36, + + /// + /// 阿昌族 + /// + [Description("阿昌族")] + AChangZu = 37, + + /// + /// 普米族 + /// + [Description("普米族")] + PuMiZu = 38, + + /// + /// 塔吉克族 + /// + [Description("塔吉克族")] + TaJiKeZu = 39, + + /// + /// 怒族 + /// + [Description("怒族")] + NuZu = 40, + + /// + /// 乌孜别克族 + /// + [Description("乌孜别克族")] + WuZiBieKeZu = 41, + + /// + /// 俄罗斯族 + /// + [Description("俄罗斯族")] + ELuoSiZu = 42, + + /// + /// 鄂温克族 + /// + [Description("鄂温克族")] + EwenKeZu = 43, + + /// + /// 德昂族 + /// + [Description("德昂族")] + DeAngZu = 44, + + /// + /// 保安族 + /// + [Description("保安族")] + BaoAnZu = 45, + + /// + /// 裕固族 + /// + [Description("裕固族")] + YuGuZu = 46, + + /// + /// 京族 + /// + [Description("京族")] + JingZu = 47, + + /// + /// 塔塔尔族 + /// + [Description("塔塔尔族")] + TaTaErZu = 48, + + /// + /// 独龙族 + /// + [Description("独龙族")] + DuLongZu = 49, + + /// + /// 鄂伦春族 + /// + [Description("鄂伦春族")] + ELunChunZu = 50, + + /// + /// 赫哲族 + /// + [Description("赫哲族")] + HeZheZu = 51, + + /// + /// 门巴族 + /// + [Description("门巴族")] + MenBaZu = 52, + + /// + /// 珞巴族 + /// + [Description("珞巴族")] + LuoBaZu = 53, + + /// + /// 高山族 + /// + [Description("高山族")] + GaoShanZu = 54, + + /// + /// 佤族 + /// + [Description("佤族")] + WaZu = 55, + + /// + /// 基诺族 + /// + [Description("基诺族")] + JiNuoZu = 56 +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/NoticeStatusEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/NoticeStatusEnum.cs new file mode 100644 index 0000000..0985a4d --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/NoticeStatusEnum.cs @@ -0,0 +1,34 @@ + + +namespace YY.Admin.Core; + +/// +/// 通知公告状态枚举 +/// +[Description("通知公告状态枚举")] +public enum NoticeStatusEnum +{ + /// + /// 草稿 + /// + [Description("草稿"), Theme("info")] + DRAFT = 0, + + /// + /// 发布 + /// + [Description("发布")] + PUBLIC = 1, + + /// + /// 撤回 + /// + [Description("撤回")] + CANCEL = 2, + + /// + /// 删除 + /// + [Description("删除")] + DELETED = 3 +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/NoticeTypeEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/NoticeTypeEnum.cs new file mode 100644 index 0000000..5af3628 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/NoticeTypeEnum.cs @@ -0,0 +1,22 @@ + + +namespace YY.Admin.Core; + +/// +/// 通知公告状类型枚举 +/// +[Description("通知公告状类型枚举")] +public enum NoticeTypeEnum +{ + /// + /// 通知 + /// + [Description("通知")] + NOTICE = 1, + + /// + /// 公告 + /// + [Description("公告")] + ANNOUNCEMENT = 2, +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/NoticeUserStatusEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/NoticeUserStatusEnum.cs new file mode 100644 index 0000000..c7b700c --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/NoticeUserStatusEnum.cs @@ -0,0 +1,22 @@ + + +namespace YY.Admin.Core; + +/// +/// 通知公告用户状态枚举 +/// +[Description("通知公告用户状态枚举")] +public enum NoticeUserStatusEnum +{ + /// + /// 未读 + /// + [Description("未读")] + UNREAD = 0, + + /// + /// 已读 + /// + [Description("已读"), Theme("info")] + READ = 1 +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/PlatformTypeEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/PlatformTypeEnum.cs new file mode 100644 index 0000000..d388ce6 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/PlatformTypeEnum.cs @@ -0,0 +1,40 @@ + + +namespace YY.Admin.Core; + +/// +/// 平台类型枚举 +/// +[Description("平台类型枚举")] +public enum PlatformTypeEnum +{ + /// + /// 微信公众号 + /// + [Description("微信公众号")] + 微信公众号 = 1, + + /// + /// 微信小程序 + /// + [Description("微信小程序")] + 微信小程序 = 2, + + /// + /// QQ + /// + [Description("QQ")] + QQ = 3, + + /// + /// 支付宝 + /// + [Description("支付宝")] + Alipay = 4, + + /// + /// Gitee + /// + [Description("Gitee")] + Gitee = 5, +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/PrintTypeEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/PrintTypeEnum.cs new file mode 100644 index 0000000..68f2f08 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/PrintTypeEnum.cs @@ -0,0 +1,26 @@ +// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。 +// +// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。 +// +// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任! + +namespace YY.Admin.Core; + +/// +/// 打印类型枚举 +/// +[Description("打印类型枚举")] +public enum PrintTypeEnum +{ + /// + /// 浏览器打印 + /// + [Description("浏览器打印")] + Browser = 1, + + /// + /// 浏览器打印 + /// + [Description("客户端打印")] + Client = 2, +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/RequestTypeEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/RequestTypeEnum.cs new file mode 100644 index 0000000..a068fea --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/RequestTypeEnum.cs @@ -0,0 +1,35 @@ + + +namespace YY.Admin.Core; + +/// +/// HTTP请求类型 +/// +[Description("HTTP请求类型")] +public enum RequestTypeEnum +{ + /// + /// 执行内部方法 + /// + Run = 0, + + /// + /// GET + /// + Get = 1, + + /// + /// POST + /// + Post = 2, + + /// + /// PUT + /// + Put = 3, + + /// + /// DELETE + /// + Delete = 4 +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/StatusEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/StatusEnum.cs new file mode 100644 index 0000000..0a2019b --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/StatusEnum.cs @@ -0,0 +1,22 @@ + + +namespace YY.Admin.Core; + +/// +/// 通用状态枚举 +/// +[Description("通用状态枚举")] +public enum StatusEnum +{ + /// + /// 启用 + /// + [Description("启用"), Theme("success")] + Enable = 1, + + /// + /// 停用 + /// + [Description("停用"), Theme("danger")] + Disable = 2, +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/SysUserEventTypeEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/SysUserEventTypeEnum.cs new file mode 100644 index 0000000..0ab78f3 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/SysUserEventTypeEnum.cs @@ -0,0 +1,83 @@ + + +namespace YY.Admin.Core; + +/// +/// 事件类型-系统用户操作枚举 +/// + +[Description("事件类型-系统用户操作枚举")] +public enum SysUserEventTypeEnum +{ + /// + /// 增加用户 + /// + [Description("增加用户")] + Add = 111, + + /// + /// 更新用户 + /// + [Description("更新用户")] + Update = 222, + + /// + /// 授权用户角色 + /// + [Description("授权用户角色")] + UpdateRole = 333, + + /// + /// 删除用户 + /// + [Description("删除用户")] + Delete = 444, + + /// + /// 设置用户状态 + /// + [Description("设置用户状态")] + SetStatus = 555, + + /// + /// 修改密码 + /// + [Description("修改密码")] + ChangePwd = 666, + + /// + /// 重置密码 + /// + [Description("重置密码")] + ResetPwd = 777, + + /// + /// 解除登录锁定 + /// + [Description("解除登录锁定")] + UnlockLogin = 888, + + /// + /// 注册用户 + /// + [Description("注册用户")] + Register = 999, + + /// + /// 用户登录 + /// + [Description("用户登录")] + Login = 1000, + + /// + /// 用户退出 + /// + [Description("用户退出")] + LoginOut = 1001, + + /// + /// RefreshToken + /// + [Description("刷新Token")] + RefreshToken = 1002, +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/TemplateTypeEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/TemplateTypeEnum.cs new file mode 100644 index 0000000..eccc438 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/TemplateTypeEnum.cs @@ -0,0 +1,46 @@ + + +namespace YY.Admin.Core; + +/// +/// 消息模板类型枚举 +/// +[Description("消息模板类型枚举")] +public enum TemplateTypeEnum +{ + /// + /// 通知公告 + /// + [Description("通知")] + Notice = 1, + + /// + /// 短信 + /// + [Description("短信")] + SMS = 2, + + /// + /// 邮件 + /// + [Description("邮件")] + Email = 3, + + /// + /// 微信 + /// + [Description("微信")] + Wechat = 4, + + /// + /// 钉钉 + /// + [Description("钉钉")] + DingTalk = 5, + + /// + /// 企业微信 + /// + [Description("企业微信")] + WeChatWork = 7 +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/TenantTypeEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/TenantTypeEnum.cs new file mode 100644 index 0000000..e41706c --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/TenantTypeEnum.cs @@ -0,0 +1,22 @@ + + +namespace YY.Admin.Core; + +/// +/// 租户类型枚举 +/// +[Description("租户类型枚举")] +public enum TenantTypeEnum +{ + /// + /// Id隔离 + /// + [Description("Id隔离")] + Id = 0, + + /// + /// 库隔离 + /// + [Description("库隔离")] + Db = 1, +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/WechatReturnCodeEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/WechatReturnCodeEnum.cs new file mode 100644 index 0000000..dbb3453 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/WechatReturnCodeEnum.cs @@ -0,0 +1,285 @@ + + +namespace YY.Admin.Core; + +/// +/// 微信开发返回码 +/// +[Description("微信开发返回码")] +public enum WechatReturnCodeEnum +{ + SenparcWeixinSDK配置错误 = -99, // 0xFFFFFF9D + 系统繁忙此时请开发者稍候再试 = -1, // 0xFFFFFFFF + 请求成功 = 0, + 工商数据返回_企业已注销 = 101, // 0x00000065 + 工商数据返回_企业不存在或企业信息未更新 = 102, // 0x00000066 + 工商数据返回_企业法定代表人姓名不一致 = 103, // 0x00000067 + 工商数据返回_企业法定代表人身份证号码不一致 = 104, // 0x00000068 + 法定代表人身份证号码_工商数据未更新_请5_15个工作日之后尝试 = 105, // 0x00000069 + 工商数据返回_企业信息或法定代表人信息不一致 = 1000, // 0x000003E8 + 对方不是粉丝 = 10700, // 0x000029CC + 发送消息失败_对方关闭了接收消息 = 10703, // 0x000029CF + 发送消息失败_48小时内用户未互动 = 10706, // 0x000029D2 + POST参数非法 = 20002, // 0x00004E22 + 获取access_token时AppSecret错误或者access_token无效 = 40001, // 0x00009C41 + + /// + /// 公众号:不合法的凭证类型 + /// 小程序:暂无生成权限 + /// + 不合法的凭证类型 = 40002, // 0x00009C42 + + 不合法的OpenID = 40003, // 0x00009C43 + 不合法的媒体文件类型 = 40004, // 0x00009C44 + 不合法的文件类型 = 40005, // 0x00009C45 + 不合法的文件大小 = 40006, // 0x00009C46 + 不合法的媒体文件id = 40007, // 0x00009C47 + 不合法的消息类型_40008 = 40008, // 0x00009C48 + 不合法的图片文件大小 = 40009, // 0x00009C49 + 不合法的语音文件大小 = 40010, // 0x00009C4A + 不合法的视频文件大小 = 40011, // 0x00009C4B + 不合法的缩略图文件大小 = 40012, // 0x00009C4C + + /// + /// 微信:不合法的APPID + /// 小程序:生成权限被封禁 + /// + 不合法的APPID = 40013, // 0x00009C4D + + 不合法的access_token = 40014, // 0x00009C4E + 不合法的菜单类型 = 40015, // 0x00009C4F + 不合法的按钮个数1 = 40016, // 0x00009C50 + 不合法的按钮个数2 = 40017, // 0x00009C51 + 不合法的按钮名字长度 = 40018, // 0x00009C52 + 不合法的按钮KEY长度 = 40019, // 0x00009C53 + 不合法的按钮URL长度 = 40020, // 0x00009C54 + 不合法的菜单版本号 = 40021, // 0x00009C55 + 不合法的子菜单级数 = 40022, // 0x00009C56 + 不合法的子菜单按钮个数 = 40023, // 0x00009C57 + 不合法的子菜单按钮类型 = 40024, // 0x00009C58 + 不合法的子菜单按钮名字长度 = 40025, // 0x00009C59 + 不合法的子菜单按钮KEY长度 = 40026, // 0x00009C5A + 不合法的子菜单按钮URL长度 = 40027, // 0x00009C5B + 不合法的自定义菜单使用用户 = 40028, // 0x00009C5C + 不合法的oauth_code = 40029, // 0x00009C5D + 不合法的refresh_token = 40030, // 0x00009C5E + 不合法的openid列表 = 40031, // 0x00009C5F + 不合法的openid列表长度 = 40032, // 0x00009C60 + 不合法的请求字符不能包含uxxxx格式的字符 = 40033, // 0x00009C61 + 不合法的参数 = 40035, // 0x00009C63 + template_id不正确 = 40037, // 0x00009C65 + 不合法的请求格式 = 40038, // 0x00009C66 + 不合法的URL长度 = 40039, // 0x00009C67 + 不合法的分组id = 40050, // 0x00009C72 + 分组名字不合法 = 40051, // 0x00009C73 + + /// + /// 公众号:输入参数有误 + /// 小程序:参数expire_time填写错误 + /// + 输入参数有误 = 40097, // 0x00009CA1 + + appsecret不正确 = 40125, // 0x00009CBD + 调用接口的IP地址不在白名单中 = 40164, // 0x00009CE4 + 参数path填写错误 = 40165, // 0x00009CE5 + 小程序Appid不存在 = 40166, // 0x00009CE6 + 参数query填写错误 = 40212, // 0x00009D14 + 缺少access_token参数 = 41001, // 0x0000A029 + 缺少appid参数 = 41002, // 0x0000A02A + 缺少refresh_token参数 = 41003, // 0x0000A02B + 缺少secret参数 = 41004, // 0x0000A02C + 缺少多媒体文件数据 = 41005, // 0x0000A02D + 缺少media_id参数 = 41006, // 0x0000A02E + 缺少子菜单数据 = 41007, // 0x0000A02F + 缺少oauth_code = 41008, // 0x0000A030 + 缺少openid = 41009, // 0x0000A031 + form_id不正确_或者过期 = 41028, // 0x0000A044 + form_id已被使用 = 41029, // 0x0000A045 + page不正确 = 41030, // 0x0000A046 + access_token超时 = 42001, // 0x0000A411 + refresh_token超时 = 42002, // 0x0000A412 + oauth_code超时 = 42003, // 0x0000A413 + 需要GET请求 = 43001, // 0x0000A7F9 + 需要POST请求 = 43002, // 0x0000A7FA + 需要HTTPS请求 = 43003, // 0x0000A7FB + 需要接收者关注 = 43004, // 0x0000A7FC + 需要好友关系 = 43005, // 0x0000A7FD + + /// [小程序订阅消息]用户拒绝接受消息,如果用户之前曾经订阅过,则表示用户取消了订阅关系 + 用户拒绝接受消息 = 43101, // 0x0000A85D + + 没有权限 = 43104, // 0x0000A860 + 多媒体文件为空 = 44001, // 0x0000ABE1 + POST的数据包为空 = 44002, // 0x0000ABE2 + 图文消息内容为空 = 44003, // 0x0000ABE3 + 文本消息内容为空 = 44004, // 0x0000ABE4 + 多媒体文件大小超过限制 = 45001, // 0x0000AFC9 + 消息内容超过限制 = 45002, // 0x0000AFCA + 标题字段超过限制 = 45003, // 0x0000AFCB + 描述字段超过限制 = 45004, // 0x0000AFCC + 链接字段超过限制 = 45005, // 0x0000AFCD + 图片链接字段超过限制 = 45006, // 0x0000AFCE + 语音播放时间超过限制 = 45007, // 0x0000AFCF + 图文消息超过限制 = 45008, // 0x0000AFD0 + 接口调用超过限制 = 45009, // 0x0000AFD1 + 创建菜单个数超过限制 = 45010, // 0x0000AFD2 + 回复时间超过限制 = 45015, // 0x0000AFD7 + 系统分组不允许修改 = 45016, // 0x0000AFD8 + 分组名字过长 = 45017, // 0x0000AFD9 + 分组数量超过上限 = 45018, // 0x0000AFDA + 超出响应数量限制 = 45047, // 0x0000AFF7 + 创建的标签数过多请注意不能超过100个 = 45056, // 0x0000B000 + 标签名非法请注意不能和其他标签重名 = 45157, // 0x0000B065 + 标签名长度超过30个字节 = 45158, // 0x0000B066 + 不存在媒体数据 = 46001, // 0x0000B3B1 + 不存在的菜单版本 = 46002, // 0x0000B3B2 + 不存在的菜单数据 = 46003, // 0x0000B3B3 + 解析JSON_XML内容错误 = 47001, // 0x0000B799 + + /// [小程序订阅消息]模板参数不准确,可能为空或者不满足规则,errmsg会提示具体是哪个字段出错 + 模板参数不准确 = 47003, // 0x0000B79B + + api功能未授权 = 48001, // 0x0000BB81 + 用户未授权该api = 50001, // 0x0000C351 + 名称格式不合法 = 53010, // 0x0000CF12 + 名称检测命中频率限制 = 53011, // 0x0000CF13 + 禁止使用该名称 = 53012, // 0x0000CF14 + 公众号_名称与已有公众号名称重复_小程序_该名称与已有小程序名称重复 = 53013, // 0x0000CF15 + 公众号_公众号已有_名称A_时_需与该帐号相同主体才可申请_名称A_小程序_小程序已有_名称A_时_需与该帐号相同主体才可申请_名称A_ = 53014, // 0x0000CF16 + 公众号_该名称与已有小程序名称重复_需与该小程序帐号相同主体才可申请_小程序_该名称与已有公众号名称重复_需与该公众号帐号相同主体才可申请 = 53015, // 0x0000CF17 + 公众号_该名称与已有多个小程序名称重复_暂不支持申请_小程序_该名称与已有多个公众号名称重复_暂不支持申请 = 53016, // 0x0000CF18 + 公众号_小程序已有_名称A_时_需与该帐号相同主体才可申请_名称A_小程序_公众号已有_名称A_时_需与该帐号相同主体才可申请_名称A = 53017, // 0x0000CF19 + 名称命中微信号 = 53018, // 0x0000CF1A + 名称在保护期内 = 53019, // 0x0000CF1B + 法人姓名与微信号不一致 = 61070, // 0x0000EE8E + 系统错误system_error = 61450, // 0x0000F00A + 参数错误invalid_parameter = 61451, // 0x0000F00B + 无效客服账号invalid_kf_account = 61452, // 0x0000F00C + 客服帐号已存在kf_account_exsited = 61453, // 0x0000F00D + + /// + /// 客服帐号名长度超过限制(仅允许10个英文字符,不包括@及@后的公众号的微信号)(invalid kf_acount length) + /// + 客服帐号名长度超过限制 = 61454, // 0x0000F00E + + /// + /// 客服帐号名包含非法字符(仅允许英文+数字)(illegal character in kf_account) + /// + 客服帐号名包含非法字符 = 61455, // 0x0000F00F + + /// 客服帐号个数超过限制(10个客服账号)(kf_account count exceeded) + 客服帐号个数超过限制 = 61456, // 0x0000F010 + + 无效头像文件类型invalid_file_type = 61457, // 0x0000F011 + 日期格式错误 = 61500, // 0x0000F03C + 日期范围错误 = 61501, // 0x0000F03D + 发送消息失败_该用户已被加入黑名单_无法向此发送消息 = 62751, // 0x0000F51F + 门店不存在 = 65115, // 0x0000FE5B + 该门店状态不允许更新 = 65118, // 0x0000FE5E + 标签格式错误 = 85006, // 0x00014C0E + 页面路径错误 = 85007, // 0x00014C0F + 类目填写错误 = 85008, // 0x00014C10 + 已经有正在审核的版本 = 85009, // 0x00014C11 + item_list有项目为空 = 85010, // 0x00014C12 + 标题填写错误 = 85011, // 0x00014C13 + 无效的审核id = 85012, // 0x00014C14 + 版本输入错误 = 85015, // 0x00014C17 + 没有审核版本 = 85019, // 0x00014C1B + 审核状态未满足发布 = 85020, // 0x00014C1C + 状态不可变 = 85021, // 0x00014C1D + action非法 = 85022, // 0x00014C1E + 审核列表填写的项目数不在1到5以内 = 85023, // 0x00014C1F + 需要补充相应资料_填写org_code和other_files参数 = 85024, // 0x00014C20 + 管理员手机登记数量已超过上限 = 85025, // 0x00014C21 + 该微信号已绑定5个管理员 = 85026, // 0x00014C22 + 管理员身份证已登记过5次 = 85027, // 0x00014C23 + 该主体登记数量已超过上限 = 85028, // 0x00014C24 + 商家名称已被占用 = 85029, // 0x00014C25 + 不能使用该名称 = 85031, // 0x00014C27 + 该名称在侵权投诉保护期 = 85032, // 0x00014C28 + 名称包含违规内容或微信等保留字 = 85033, // 0x00014C29 + 商家名称在改名15天保护期内 = 85034, // 0x00014C2A + 需与该帐号相同主体才可申请 = 85035, // 0x00014C2B + 介绍中含有虚假混淆内容 = 85036, // 0x00014C2C + 头像或者简介修改达到每个月上限 = 85049, // 0x00014C39 + 正在审核中_请勿重复提交 = 85050, // 0x00014C3A + 请先成功创建门店后再调用 = 85053, // 0x00014C3D + 临时mediaid无效 = 85056, // 0x00014C40 + 链接错误 = 85066, // 0x00014C4A + 测试链接不是子链接 = 85068, // 0x00014C4C + 校验文件失败 = 85069, // 0x00014C4D + 个人类型小程序无法设置二维码规则 = 85070, // 0x00014C4E + 已添加该链接_请勿重复添加 = 85071, // 0x00014C4F + 该链接已被占用 = 85072, // 0x00014C50 + 二维码规则已满 = 85073, // 0x00014C51 + 小程序未发布_小程序必须先发布代码才可以发布二维码跳转规则 = 85074, // 0x00014C52 + 个人类型小程序无法设置二维码规则1 = 85075, // 0x00014C53 + 小程序没有线上版本_不能进行灰度 = 85079, // 0x00014C57 + 小程序提交的审核未审核通过 = 85080, // 0x00014C58 + 无效的发布比例 = 85081, // 0x00014C59 + 当前的发布比例需要比之前设置的高 = 85082, // 0x00014C5A + 小程序提审数量已达本月上限 = 85085, // 0x00014C5D + 提交代码审核之前需提前上传代码 = 85086, // 0x00014C5E + 小程序已使用_api_navigateToMiniProgram_请声明跳转_appid_列表后再次提交 = 85087, // 0x00014C5F + 不是由第三方代小程序进行调用 = 86000, // 0x00014FF0 + 不存在第三方的已经提交的代码 = 86001, // 0x00014FF1 + 小程序还未设置昵称_头像_简介_请先设置完后再重新提交 = 86002, // 0x00014FF2 + 无效微信号 = 86004, // 0x00014FF4 + + /// + /// 小程序为“签名错误”。对应公众号: 87009, “errmsg” : “reply is not exists” //该回复不存在 + /// + 签名错误 = 87009, // 0x000153E1 + + 现网已经在灰度发布_不能进行版本回退 = 87011, // 0x000153E3 + 该版本不能回退_可能的原因_1_无上一个线上版用于回退_2_此版本为已回退版本_不能回退_3_此版本为回退功能上线之前的版本_不能回退 = 87012, // 0x000153E4 + 内容含有违法违规内容 = 87014, // 0x000153E6 + 没有留言权限 = 88000, // 0x000157C0 + 该图文不存在 = 88001, // 0x000157C1 + 文章存在敏感信息 = 88002, // 0x000157C2 + 精选评论数已达上限 = 88003, // 0x000157C3 + 已被用户删除_无法精选 = 88004, // 0x000157C4 + 已经回复过了 = 88005, // 0x000157C5 + 回复超过长度限制或为0 = 88007, // 0x000157C7 + 该评论不存在 = 88008, // 0x000157C8 + 获取评论数目不合法 = 88010, // 0x000157CA + 该公众号_小程序已经绑定了开放平台帐号 = 89000, // 0x00015BA8 + 业务域名无更改_无需重复设置 = 89019, // 0x00015BBB + 尚未设置小程序业务域名_请先在第三方平台中设置小程序业务域名后在调用本接口 = 89020, // 0x00015BBC + 请求保存的域名不是第三方平台中已设置的小程序业务域名或子域名 = 89021, // 0x00015BBD + 业务域名数量超过限制_最多可以添加100个业务域名 = 89029, // 0x00015BC5 + 个人小程序不支持调用_setwebviewdomain_接口 = 89231, // 0x00015C8F + 内部错误 = 89247, // 0x00015C9F + 企业代码类型无效_请选择正确类型填写 = 89248, // 0x00015CA0 + 该主体已有任务执行中_距上次任务24h后再试 = 89249, // 0x00015CA1 + 未找到该任务 = 89250, // 0x00015CA2 + 待法人人脸核身校验 = 89251, // 0x00015CA3 + 法人_企业信息一致性校验中 = 89252, // 0x00015CA4 + 缺少参数 = 89253, // 0x00015CA5 + 第三方权限集不全_补全权限集全网发布后生效 = 89254, // 0x00015CA6 + 系统不稳定_请稍后再试_如多次失败请通过社区反馈 = 89401, // 0x00015D39 + 该审核单不在待审核队列_请检查是否已提交审核或已审完 = 89402, // 0x00015D3A + 本单属于平台不支持加急种类_请等待正常审核流程 = 89403, // 0x00015D3B + 本单已加速成功_请勿重复提交 = 89404, // 0x00015D3C + 本月加急额度不足_请提升提审质量以获取更多额度 = 89405, // 0x00015D3D + 该经营资质已添加_请勿重复添加 = 92000, // 0x00016760 + 附近地点添加数量达到上线_无法继续添加 = 92002, // 0x00016762 + 地点已被其它小程序占用 = 92003, // 0x00016763 + 附近功能被封禁 = 92004, // 0x00016764 + 地点正在审核中 = 92005, // 0x00016765 + 地点正在展示小程序 = 92006, // 0x00016766 + 地点审核失败 = 92007, // 0x00016767 + 程序未展示在该地点 = 92008, // 0x00016768 + 小程序未上架或不可见 = 92009, // 0x00016769 + 地点不存在 = 93010, // 0x00016B52 + 个人类型小程序不可用 = 93011, // 0x00016B53 + 已下发的模板消息法人并未确认且已超时_24h_未进行身份证校验 = 100001, // 0x000186A1 + 已下发的模板消息法人并未确认且已超时_24h_未进行人脸识别校验 = 100002, // 0x000186A2 + 已下发的模板消息法人并未确认且已超时_24h = 100003, // 0x000186A3 + 此账号已被封禁_无法操作 = 200011, // 0x00030D4B + 私有模板数已达上限_上限_50_个 = 200012, // 0x00030D4C + 此模版已被封禁_无法选用 = 200013, // 0x00030D4D + 模版tid参数错误 = 200014, // 0x00030D4E + 关键词列表kidList参数错误 = 200020, // 0x00030D54 + 场景描述sceneDesc参数错误 = 200021, // 0x00030D55 +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Enum/YesNoEnum.cs b/yy-admin-master/YY.Admin.Core/Enum/YesNoEnum.cs new file mode 100644 index 0000000..50d3a16 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Enum/YesNoEnum.cs @@ -0,0 +1,22 @@ + + +namespace YY.Admin.Core; + +/// +/// 是否枚举 +/// +[Description("是否枚举")] +public enum YesNoEnum +{ + /// + /// 是 + /// + [Description("是"), Theme("success")] + Y = 1, + + /// + /// 否 + /// + [Description("否"), Theme("danger")] + N = 2 +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/EventBus/SysUserEvents.cs b/yy-admin-master/YY.Admin.Core/EventBus/SysUserEvents.cs new file mode 100644 index 0000000..c5702c4 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/EventBus/SysUserEvents.cs @@ -0,0 +1,78 @@ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core +{ + public static class SysUserEvents + { + /// + /// 添加用户事件 + /// + /// + public class AddUserEvent : PubSubEvent { } + /// + /// 更新用户事件 + /// + /// + /// + public class UpdateUserEvent : PubSubEvent<(SysUser Original, SysUser Updated)> { } + /// + /// 删除用户事件 + /// + /// + public class DeleteUserEvent: PubSubEvent { } + + /// + /// 设置用户状态事件 + /// + /// + /// + public class SetUserStatusEvent : PubSubEvent<(SysUser User, StatusEnum NewStatus)> { } + /// + /// 修改密码事件 + /// + /// + public class ChangePwdEvent : PubSubEvent { } + /// + /// 重置密码 + /// + /// + public class ResetPwdEvent : PubSubEvent { } + /// + /// 解除登录锁定事件 + /// + /// + public class UnlockUserLoginEvent : PubSubEvent { } + + /// + /// 注册用户事件 + /// + /// + public class RegisterUserEvent : PubSubEvent { } + /// + /// 用户登录事件 + /// + /// + public class LoginUserEvent : PubSubEvent { } + /// + /// 用户退出事件 + /// + /// + public class LoginOutEvent : PubSubEvent { } + + /// + /// 更新用户角色事件 + /// + public class UpdateUserRoleEvent : PubSubEvent<(SysUser User, List RoleIds)> { } + + /// + /// Jeecg 同构用户同步完成事件(用于页面实时刷新)。 + /// + public class JeecgMirrorUsersSyncedEvent : PubSubEvent { } + + } +} diff --git a/yy-admin-master/YY.Admin.Core/EventBus/ThemeChangedEvent.cs b/yy-admin-master/YY.Admin.Core/EventBus/ThemeChangedEvent.cs new file mode 100644 index 0000000..8166877 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/EventBus/ThemeChangedEvent.cs @@ -0,0 +1,6 @@ +using HandyControl.Data; + +namespace YY.Admin.Core.EventBus +{ + public class ThemeChangedEvent : PubSubEvent { } +} diff --git a/yy-admin-master/YY.Admin.Core/Extension/DataGridExtensions.cs b/yy-admin-master/YY.Admin.Core/Extension/DataGridExtensions.cs new file mode 100644 index 0000000..54b348a --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Extension/DataGridExtensions.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Media; + + +namespace YY.Admin.Core.Extension +{ + /// + /// DataGrid扩展方法 + /// + public static class DataGridExtensions + { + /// + /// 动态生成列 + /// + /// DataGrid控件实例 + /// 列插入位置 + /// 数据源 + /// 操作列资源 + /// 操作列宽度 + public static void GenerateColumns(this DataGrid dataGrid, int index, object data, string operationKey, DataGridLength operationWidth) + { + IList list = GetColumns(data); + //Window win = Application.Current.Windows.OfType().SingleOrDefault(x => x.IsActive); + //Page page = win.GetChildObject("page"); + //if (page == null) throw new Exception("未獲取到當前窗口名稱爲page的(Page)頁面對象,原因:沒有爲Page設置Name,且名稱必須爲【page】!"); + + Page page = GetParentObject(dataGrid, "page"); + + for (int i = 0; i < list.Count; i++) + { + switch (list[i].ShowAs) + { + case ShowScheme.普通文本: + dataGrid.Columns.Insert(i + index, new DataGridTextColumn + { + Header = list[i].HeaderName, + Binding = new Binding(list[i].PropertyName), + Width = list[i].Width + }); + break; + case ShowScheme.自定义: + if (page.FindResource(list[i].ResourceKey) != null) + { + DataGridTemplateColumn val = new DataGridTemplateColumn(); + val.Header = list[i].HeaderName; + val.Width = list[i].Width; + val.CellTemplate = page.FindResource(list[i].ResourceKey) as DataTemplate; + dataGrid.Columns.Insert(i + index, val); + } + break; + } + } + if (!string.IsNullOrWhiteSpace(operationKey) && page != null) + { + var resource = page.FindResource(operationKey); + if (resource != null) + { + + var col = new DataGridTemplateColumn() { Header = "操作", Width = operationWidth }; + col.CellTemplate = resource as DataTemplate; + dataGrid.Columns.Add(col); + } + } + } + + /// + /// 获取数据源对象到列的映射关系 + /// + /// + /// + private static IList GetColumns(object data) + { + List list = new List(); + var pros = data.GetType().GenericTypeArguments[0].GetProperties(); + foreach (var item in pros) + { + var a = item.GetCustomAttribute(); + if (a != null) { a.PropertyName = item.Name; list.Add(a); } + } + return list.OrderBy(x => x.DisplayIndex).ToArray(); + } + + /// + /// 查找父级控件 + /// + /// + /// + /// + /// + public static T GetParentObject(DependencyObject obj, string name) where T : FrameworkElement + { + DependencyObject parent = VisualTreeHelper.GetParent(obj); + + while (parent != null) + { + if (parent is T && (((T)parent).Name == name | string.IsNullOrEmpty(name))) + { + return (T)parent; + } + + parent = VisualTreeHelper.GetParent(parent); + } + + return null; + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Extension/EnumExtensions.cs b/yy-admin-master/YY.Admin.Core/Extension/EnumExtensions.cs new file mode 100644 index 0000000..52443bc --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Extension/EnumExtensions.cs @@ -0,0 +1,14 @@ +using System.Reflection; + +namespace YY.Admin.Core.Extension +{ + public static class EnumExtensions + { + public static string GetDescription(this Enum value) + { + var field = value.GetType().GetField(value.ToString()); + var attribute = field?.GetCustomAttribute(); + return attribute?.Description ?? value.ToString(); + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Extension/EnumerableExtension.cs b/yy-admin-master/YY.Admin.Core/Extension/EnumerableExtension.cs new file mode 100644 index 0000000..a099aa0 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Extension/EnumerableExtension.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using YY.Admin.Core.Extension; + +namespace YY.Admin.Core +{ + /// + /// 数据集合拓展类 + /// + public static class EnumerableExtension + { + private static readonly ConcurrentDictionary PropertyCache = new(); + + /// + /// 查询有父子关系的数据集 + /// + /// 数据集 + /// 主键ID字段 + /// 父级字段 + /// 顶级节点父级字段值 + /// 是否包含顶级节点本身 + /// + public static IEnumerable ToChildList(this IEnumerable list, + Expression> idExpression, + Expression> parentIdExpression, + object topParentIdValue, + bool isContainOneself = true) + { + if (list == null || !list.Any()) return Enumerable.Empty(); + + var propId = GetPropertyInfo(idExpression); + var propParentId = GetPropertyInfo(parentIdExpression); + + // 查找所有顶级节点 + var topNodes = list.Where(item => Equals(propId.GetValue(item), topParentIdValue)).ToList(); + + return TraverseHierarchy(list, propId, propParentId, topNodes, isContainOneself); + } + + /// + /// 查询有父子关系的数据集 + /// + /// 数据集 + /// 主键ID字段 + /// 父级字段 + /// 顶级节点的选择条件 + /// 是否包含顶级节点本身 + /// + public static IEnumerable ToChildList(this IEnumerable list, + Expression> idExpression, + Expression> parentIdExpression, + Expression> topLevelPredicate, + bool isContainOneself = true) + { + if (list == null || !list.Any()) return Enumerable.Empty(); + + // 获取顶级节点 + var topNodes = list.Where(topLevelPredicate.Compile()).ToList(); + + if (!topNodes.Any()) return Enumerable.Empty(); + + var idPropertyInfo = GetPropertyInfo(idExpression); + var parentPropertyInfo = GetPropertyInfo(parentIdExpression); + + return TraverseHierarchy(list, idPropertyInfo, parentPropertyInfo, topNodes, isContainOneself); + } + + /// + /// 辅助方法,从表达式中提取属性信息并使用临时缓存 + /// + private static PropertyInfo GetPropertyInfo(Expression> expression) + { + // 使用 ConcurrentDictionary 确保线程安全 + return PropertyCache.GetOrAdd(typeof(T).FullName + "." + ((MemberExpression)expression.Body).Member.Name, k => + { + if (expression.Body is UnaryExpression { Operand: MemberExpression member }) return (PropertyInfo)member.Member; + if (expression.Body is MemberExpression memberExpression) return (PropertyInfo)memberExpression.Member; + throw new Exception("表达式必须是一个属性访问: " + expression); + }); + } + + /// + /// 使用队列遍历层级结构 + /// + private static IEnumerable TraverseHierarchy(IEnumerable list, + PropertyInfo idPropertyInfo, + PropertyInfo parentPropertyInfo, + List topNodes, + bool isContainOneself) + { + var queue = new Queue(topNodes); + var result = new HashSet(topNodes); + + while (queue.Count > 0) + { + var currentNode = queue.Dequeue(); + var children = list.Where(item => Equals(parentPropertyInfo.GetValue(item), idPropertyInfo.GetValue(currentNode))).ToList(); + children.Where(child => result.Add(child)).ForEach(child => queue.Enqueue(child)); + } + if (isContainOneself) return result; + + // 如果不需要包含顶级节点本身,则移除它们 + topNodes.ForEach(e => result.Remove(e)); + + return result; + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Extension/ListExtensions.cs b/yy-admin-master/YY.Admin.Core/Extension/ListExtensions.cs new file mode 100644 index 0000000..d995f16 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Extension/ListExtensions.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core.Extension +{ + public static class ListExtensions + { + public static async Task ForEachAsync(this List list, Func func) + { + foreach (var value in list) + { + await func(value); + } + } + + public static async Task ForEachAsync(this IEnumerable source, Func action) + { + foreach (var value in source) + { + await action(value); + } + } + + public static void ForEach(this IEnumerable enumerable, Action consumer) + { + foreach (T item in enumerable) + { + consumer(item); + } + } + + public static void AddRange(this IList list, IEnumerable items) + { + if (list is List list2) + { + list2.AddRange(items); + return; + } + + foreach (T item in items) + { + list.Add(item); + } + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Extension/ObjectExtension.cs b/yy-admin-master/YY.Admin.Core/Extension/ObjectExtension.cs new file mode 100644 index 0000000..fbf579c --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Extension/ObjectExtension.cs @@ -0,0 +1,221 @@ +using NewLife.Serialization; +using Newtonsoft.Json; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Dynamic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Serialization.Formatters.Binary; +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml; + +namespace YY.Admin.Core.Extension +{ + public static partial class ObjectExtension + { + /// + /// 类型属性列表映射表 + /// + private static readonly ConcurrentDictionary PropertyCache = new(); + + /// + /// 判断类型是否实现某个泛型 + /// + /// 类型 + /// 泛型类型 + /// bool + public static bool HasImplementedRawGeneric(this Type type, Type generic) + { + // 检查接口类型 + var isTheRawGenericType = type.GetInterfaces().Any(IsTheRawGenericType); + if (isTheRawGenericType) return true; + + // 检查类型 + while (type != null && type != typeof(object)) + { + isTheRawGenericType = IsTheRawGenericType(type); + if (isTheRawGenericType) return true; + type = type.BaseType; + } + + return false; + + // 判断逻辑 + bool IsTheRawGenericType(Type type) => generic == (type.IsGenericType ? type.GetGenericTypeDefinition() : type); + } + /// + /// 判断类型是否为Nullable类型 + /// + /// 要处理的类型 + /// 是返回True,不是返回False + public static bool IsNullableType(this Type type) + { + return type != null && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + } + /// + /// 通过类型转换器获取Nullable类型的基础类型 + /// + /// 要处理的类型对象 + /// + public static Type GetUnNullableType(this Type type) + { + if (IsNullableType(type)) + { + NullableConverter nullableConverter = new NullableConverter(type); + return nullableConverter.UnderlyingType; + } + return type; + } + #region 公共方法 + /// + /// 从类型成员获取指定Attribute特性 + /// + /// Attribute特性类型 + /// 类型类型成员 + /// 是否从继承中查找 + /// 存在返回第一个,不存在返回null + public static T GetAttribute(this MemberInfo memberInfo, bool inherit = true) where T : Attribute + { + var attributes = memberInfo.GetCustomAttributes(typeof(T), inherit); + return attributes.FirstOrDefault() as T; + } + /// + /// 把对象类型转换为指定类型 + /// + /// + /// + /// + public static object CastTo(this object value, Type conversionType) + { + if (value == null) + { + return null; + } + if (conversionType.IsNullableType()) + { + conversionType = conversionType.GetUnNullableType(); + } + if (conversionType.IsEnum) + { + return Enum.Parse(conversionType, value.ToString()); + } + if (conversionType == typeof(Guid)) + { + return Guid.Parse(value.ToString()); + } + + if (conversionType == typeof(string)) return value.ToString(); + if (value.GetType() == typeof(JsonElement)) return Convert.ChangeType(value?.ToString(), conversionType); + return Convert.ChangeType(value, conversionType); + } + + /// + /// 把对象类型转化为指定类型 + /// + /// 动态类型 + /// 要转化的源对象 + /// 转化后的指定类型的对象,转化失败引发异常。 + public static T CastTo(this object value) + { + if (value == null || default(T) == null) + { + return default; + } + if (value.GetType() == typeof(T)) + { + return (T)value; + } + object result = CastTo(value, typeof(T)); + return (T)result; + } + + /// + /// 把对象类型转化为指定类型,转化失败时返回指定的默认值 + /// + /// 动态类型 + /// 要转化的源对象 + /// 转化失败返回的指定默认值 + /// 转化后的指定类型对象,转化失败时返回指定的默认值 + public static T CastTo(this object value, T defaultValue) + { + try + { + return CastTo(value); + } + catch (Exception) + { + return defaultValue; + } + } + + /// + /// 判断当前值是否介于指定范围内 + /// + /// 动态类型 + /// 动态类型对象 + /// 范围起点 + /// 范围终点 + /// 是否可等于上限(默认等于) + /// 是否可等于下限(默认等于) + /// 是否介于 + public static bool IsBetween(this IComparable value, T start, T end, bool leftEqual = true, bool rightEqual = true) where T : IComparable + { + bool flag = leftEqual ? value.CompareTo(start) >= 0 : value.CompareTo(start) > 0; + return flag && (rightEqual ? value.CompareTo(end) <= 0 : value.CompareTo(end) < 0); + } + + /// + /// 判断当前值是否介于指定范围内 + /// + /// 动态类型 + /// 动态类型对象 + /// 范围小值 + /// 范围大值 + /// 是否可等于小值(默认等于) + /// 是否可等于大值(默认等于) + public static bool IsInRange(this IComparable value, T min, T max, bool minEqual = true, bool maxEqual = true) where T : IComparable + { + bool flag = minEqual ? value.CompareTo(min) >= 0 : value.CompareTo(min) > 0; + return flag && (maxEqual ? value.CompareTo(max) <= 0 : value.CompareTo(max) < 0); + } + + /// + /// 是否存在于 + /// + public static bool IsIn(this T value, params T[] source) + { + return source.Contains(value); + } + + /// + /// 将对象[主要是匿名对象]转换为dynamic + /// + public static dynamic ToDynamic(this object value) + { + IDictionary expando = new ExpandoObject(); + Type type = value.GetType(); + PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(type); + foreach (PropertyDescriptor property in properties) + { + var val = property.GetValue(value); + if (property.PropertyType.FullName != null && property.PropertyType.FullName.StartsWith("<>f__AnonymousType")) + { + dynamic dval = val.ToDynamic(); + expando.Add(property.Name, dval); + } + else + { + expando.Add(property.Name, val); + } + } + return (ExpandoObject)expando; + } + + #endregion + } +} diff --git a/yy-admin-master/YY.Admin.Core/Extension/WindowExtensions.cs b/yy-admin-master/YY.Admin.Core/Extension/WindowExtensions.cs new file mode 100644 index 0000000..462d352 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Extension/WindowExtensions.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Media; +using System.Windows; + +namespace YY.Admin.Core.Extension +{ + /// + /// 窗体的扩展类 + /// + public static class WindowExtensions + { + /// + /// 查找子控件 + /// + /// 子控件的类型 + /// 要找的是obj的子控件 + /// 想找的子控件的Name属性 + /// 目标子控件 + public static T GetChildObject(this DependencyObject obj, string name) where T : FrameworkElement + { + DependencyObject child = null; + T grandChild = null; + + for (int i = 0; i <= VisualTreeHelper.GetChildrenCount(obj) - 1; i++) + { + child = VisualTreeHelper.GetChild(obj, i); + + if (child is T && (((T)child).Name == name | string.IsNullOrEmpty(name))) + { + return (T)child; + } + else + { + grandChild = GetChildObject(child, name); + if (grandChild != null) + return grandChild; + } + } + return null; + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/FluentValidation/FluentAutoValidationRule.cs b/yy-admin-master/YY.Admin.Core/FluentValidation/FluentAutoValidationRule.cs new file mode 100644 index 0000000..45c355e --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/FluentValidation/FluentAutoValidationRule.cs @@ -0,0 +1,82 @@ +using FluentValidation; +using System.Globalization; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using ValidationResult = System.Windows.Controls.ValidationResult; + +namespace YY.Admin.Core.FluentValidation +{ + internal class FluentAutoValidationRule : ValidationRule, IFluentAutoValidationRule where T : class + { + public IValidator? Validator { get; set; } + + /// + /// 当前绑定控件对应的属性名 + /// + public string? PropertyName { get; set; } + + // 👇 ValidationStep.UpdatedValue为true: 让Validate方法中value参数为BindingExpression类型,而非值类型 + public FluentAutoValidationRule() : base(ValidationStep.UpdatedValue, true) { } + + public override ValidationResult Validate(object value, CultureInfo cultureInfo) + { + if (Validator == null || string.IsNullOrEmpty(PropertyName)) + return ValidationResult.ValidResult; + + // 如果 value 不是 BindingExpression,先尝试判断是否是 BindingGroup (兼容性保护) + if (value is not BindingExpression bindingExpr) + { + // 有时 WPF 在不同阶段会传入 BindingGroup 或其他类型,直接跳过初始化阶段的验证,避免误报 + return ValidationResult.ValidResult; + } + + // --- 更可靠地判断“初始化阶段的第一次验证” --- + // 如果绑定目标尚未挂到视觉树或控件未 Loaded,则视为初始化,不执行验证 + var target = bindingExpr.Target as DependencyObject; + if (target != null) + { + // 1) 如果是 FrameworkElement 并且还没 Loaded,则跳过 + if (target is FrameworkElement fe && !fe.IsLoaded) + return ValidationResult.ValidResult; + + // 2) 如果 PresentationSource 为空,也说明还没连接到可视树,跳过 + var ps = System.Windows.PresentationSource.FromVisual(target as System.Windows.Media.Visual); + if (ps == null) + return ValidationResult.ValidResult; + + // 3) 另外,如果控件不可见,也可能是初始化阶段,可根据需要跳过 + if (target is UIElement ui && !ui.IsVisible) + return ValidationResult.ValidResult; + } + + var dataItem = bindingExpr.DataItem; + if (dataItem == null) + return new ValidationResult(false, "绑定源为空"); + + // 解析 Path,例如 "SysUser.Account" + var path = bindingExpr.ParentBinding.Path?.Path ?? string.Empty; + var rootPropName = path.Split('.')[0]; // SysUser + var modelProp = dataItem.GetType().GetProperty(rootPropName); + if (modelProp == null) + return ValidationResult.ValidResult; + + var modelObj = modelProp.GetValue(dataItem); + if (modelObj is not T model) + return ValidationResult.ValidResult; + + // 调用 FluentValidation 验证指定属性 + var result = Validator.ValidateAsync(model, opts => opts.IncludeProperties(PropertyName)) + .ConfigureAwait(false) // 同步等待Task(ValidationRule 不支持 async → 只能同步等待。) + .GetAwaiter() + .GetResult(); + var error = result.Errors.FirstOrDefault(e => e.PropertyName == PropertyName); + + if (error != null) + return new ValidationResult(false, error.ErrorMessage); + + return ValidationResult.ValidResult; + + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/FluentValidation/FluentValidationHelper.cs b/yy-admin-master/YY.Admin.Core/FluentValidation/FluentValidationHelper.cs new file mode 100644 index 0000000..916b1bd --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/FluentValidation/FluentValidationHelper.cs @@ -0,0 +1,332 @@ +using FluentValidation; +using HandyControl.Controls; +using System.Reflection; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Data; +using System.Windows.Media; +using static Dm.util.ByteArrayQueue; +using ComboBox = System.Windows.Controls.ComboBox; +using DatePicker = System.Windows.Controls.DatePicker; +using PasswordBox = System.Windows.Controls.PasswordBox; +using TextBox = System.Windows.Controls.TextBox; + +namespace YY.Admin.Core.FluentValidation +{ + public static class FluentValidationHelper + { + #region ✅ SkipValidation 附加属性 + public static readonly DependencyProperty SkipValidationProperty = + DependencyProperty.RegisterAttached( + "SkipValidation", + typeof(bool), + typeof(FluentValidationHelper), + new System.Windows.PropertyMetadata(false)); + + public static void SetSkipValidation(DependencyObject element, bool value) => + element.SetValue(SkipValidationProperty, value); + + public static bool GetSkipValidation(DependencyObject element) => + (bool)element.GetValue(SkipValidationProperty); + #endregion + + /// + /// 入口方法:从 ViewModel 自动获取 Validator,并绑定到所有子控件 + /// + public static void Attach(FrameworkElement root, Type validatorOwnerType) + { + if (root == null || validatorOwnerType == null) + return; + + // 找出 validator 属性 + var validatorProp = validatorOwnerType + .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .FirstOrDefault(p => + typeof(IValidator).IsAssignableFrom(p.PropertyType) && + p.Name.EndsWith("Validator", StringComparison.OrdinalIgnoreCase)); + + if (validatorProp == null) + return; + + var validatorInstance = root.DataContext != null + ? validatorProp.GetValue(root.DataContext) + : null; + + if (validatorInstance == null) + return; + + var validatedType = validatorProp.PropertyType.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IValidator<>)) + ?.GetGenericArguments()[0]; + + if (validatedType == null) + return; + + AttachRecursive(root, validatorInstance, validatedType); + } + + /// + /// 递归绑定所有子控件 + /// + private static void AttachRecursive(DependencyObject parent, object validatorInstance, Type validatedType) + { + if (parent == null) return; + + // 先尝试走逻辑树 —— 能穿透 ScrollViewer、Row、Col、ContentPresenter 等容器 + foreach (var childObj in LogicalTreeHelper.GetChildren(parent).OfType()) + { + var child = childObj; + + if (child is FrameworkElement feLogical) + { + // ⚠️ 先设置 ErrorTemplate,保证 skip 的控件也能显示样式 + var template = feLogical.TryFindResource("BottomLeftErrorTemplate_ForInfoElement") as ControlTemplate; + if (template != null) + { + Validation.SetErrorTemplate(feLogical, template); + } + + // 🧭 再判断是否跳过校验 + if (GetSkipValidation(feLogical)) + { + AttachRecursive(child, validatorInstance, validatedType); + continue; + } + + // 🪄 普通控件 / PasswordBox + AttachValidationForElement(feLogical, validatorInstance, validatedType); + } + + // 递归逻辑树的子元素 + AttachRecursive(child, validatorInstance, validatedType); + } + + // 再补充视觉树 —— 仅在 parent 是 Visual 或 Visual3D 时调用,避免 ColumnDefinition 等非 Visual 抛错 + if (parent is Visual || parent is System.Windows.Media.Media3D.Visual3D) + { + int childrenCount = VisualTreeHelper.GetChildrenCount(parent); + for (int i = 0; i < childrenCount; i++) + { + var child = VisualTreeHelper.GetChild(parent, i); + + if (child is FrameworkElement fe) + { + // ⚠️ 先设置 ErrorTemplate,保证 skip 的控件也能显示样式 + var template = fe.TryFindResource("BottomLeftErrorTemplate_ForInfoElement") as ControlTemplate; + if (template != null) + { + Validation.SetErrorTemplate(fe, template); + } + + // 🧭 再判断是否跳过校验 + if (GetSkipValidation(fe)) + { + AttachRecursive(child, validatorInstance, validatedType); + continue; + } + + // 🪄 普通控件 / PasswordBox + AttachValidationForElement(fe, validatorInstance, validatedType); + } + + // 递归子元素 + AttachRecursive(child, validatorInstance, validatedType); + } + } + } + + #region 普通控件处理 + + private static void AttachValidationForElement(FrameworkElement fe, object validatorInstance, Type validatedType) + { + var localValues = fe.GetLocalValueEnumerator(); + while (localValues.MoveNext()) + { + var entry = localValues.Current; + var binding = BindingOperations.GetBinding(fe, entry.Property); + if (binding == null) continue; + + AddValidationRule(binding, validatorInstance, validatedType); + + // 重新应用绑定才能生效 + BindingOperations.ClearBinding(fe, entry.Property); + BindingOperations.SetBinding(fe, entry.Property, binding); + } + } + + private static void AddValidationRule(Binding binding, object validatorInstance, Type validatedType) + { + var propertyPath = binding.Path?.Path; + if (string.IsNullOrEmpty(propertyPath)) return; + + var propertyName = propertyPath.Split('.').Last(); + var ruleType = typeof(FluentAutoValidationRule<>).MakeGenericType(validatedType); + var rule = (ValidationRule)Activator.CreateInstance(ruleType)!; + + ruleType.GetProperty("Validator")?.SetValue(rule, validatorInstance); + ruleType.GetProperty("PropertyName")?.SetValue(rule, propertyName); + + binding.ValidationRules.Add(rule); + } + + #endregion + + #region 校验触发与跳过 + + public static bool ValidateAll(FrameworkElement root, T model, IValidator validator) where T : class + { + // 1️⃣ 获取所有 skip 的属性名 + var skipPropertyNames = GetSkipValidationPropertyNames(root); + + // 2️⃣ 获取模型所有属性名 + var allPropertyNames = typeof(T) + .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Select(p => p.Name) + .ToList(); + + // 3️⃣ 排除 skip,剩下的就是要验证的属性 + var includePropertyNames = allPropertyNames + .Except(skipPropertyNames, StringComparer.Ordinal) + .ToArray(); + + // 4️⃣ 验证时只验证这些属性 + var result = validator.ValidateAsync(model, options => + { + options.IncludeProperties(includePropertyNames); + }).ConfigureAwait(false) // 同步等待Task(ValidationRule 不支持 async → 只能同步等待。) + .GetAwaiter() + .GetResult(); + + // 5️⃣ 清除之前的错误 + ClearAllValidationErrors(root); + + // 6️⃣ 只对未 skip 的属性标记错误 + foreach (var error in result.Errors) + { + var controls = FindBoundControls(root, error.PropertyName); + foreach (var control in controls) + { + var depProperty = GetBoundDependencyProperty(control); + if (depProperty == null) + continue; + + var bindingExpr = BindingOperations.GetBindingExpression(control, depProperty); + if (bindingExpr != null) + { + Validation.MarkInvalid( + bindingExpr, + new ValidationError( + new ExceptionValidationRule(), + bindingExpr, + error.ErrorMessage, + null)); + } + } + } + + return result.IsValid; + } + + private static HashSet GetSkipValidationPropertyNames(DependencyObject root) + { + var skipPropertyNames = new HashSet(StringComparer.Ordinal); + + foreach (var control in FindVisualChildren(root)) + { + if (!GetSkipValidation(control)) + continue; + + var dp = GetBoundDependencyProperty(control); + if (dp == null) continue; + + var binding = BindingOperations.GetBinding(control, dp); + if (binding?.Path?.Path is string propertyPath && !string.IsNullOrEmpty(propertyPath)) + { + var propertyName = propertyPath.Split('.').Last(); + skipPropertyNames.Add(propertyName); + } + } + + return skipPropertyNames; + } + + public static void ClearAllValidationErrors(DependencyObject parent) + { + foreach (var control in FindVisualChildren(parent)) + { + if (GetSkipValidation(control)) + continue; + + var depProperty = GetBoundDependencyProperty(control); + if (depProperty == null) continue; + + var expr = BindingOperations.GetBindingExpression(control, depProperty); + if (expr != null) + Validation.ClearInvalid(expr); + } + } + + #endregion + + #region 工具方法 + + private static DependencyProperty? GetBoundDependencyProperty(FrameworkElement control) + { + return control switch + { + // 原生控件 + TextBox => TextBox.TextProperty, + // ✅ WPF 原生 PasswordBox(注意:WPF 原生 PasswordBox 没有现成的依赖属性绑定 Password,需要额外处理) + PasswordBox => PasswordBoxHelper.PasswordProperty, + ComboBox cb => BindingOperations.GetBinding(cb, Selector.SelectedItemProperty) != null + ? Selector.SelectedItemProperty + : BindingOperations.GetBinding(cb, Selector.SelectedValueProperty) != null + ? Selector.SelectedValueProperty + : null, + DatePicker => DatePicker.SelectedDateProperty, + CheckBox => ToggleButton.IsCheckedProperty, + + // HandyControl控件 + HandyControl.Controls.PasswordBox => HandyControl.Controls.PasswordBox.UnsafePasswordProperty, + TimePicker => TimePicker.SelectedTimeProperty, + NumericUpDown => NumericUpDown.ValueProperty, + CheckComboBox => CheckComboBox.SelectedItemsProperty, + + _ => null + }; + } + + private static IEnumerable FindBoundControls(DependencyObject parent, string propertyName) + { + return FindVisualChildren(parent) + .Where(fe => + { + if (GetSkipValidation(fe)) return false; + + var dp = GetBoundDependencyProperty(fe); + if (dp == null) return false; + + var expr = BindingOperations.GetBindingExpression(fe, dp); + return expr != null && expr.ParentBinding.Path.Path.EndsWith(propertyName); + }); + } + + private static IEnumerable FindVisualChildren(DependencyObject depObj) where T : DependencyObject + { + if (depObj == null) yield break; + + for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++) + { + var child = VisualTreeHelper.GetChild(depObj, i); + if (child is T tChild) + yield return tChild; + + foreach (var subChild in FindVisualChildren(child)) + yield return subChild; + } + } + + #endregion + } +} diff --git a/yy-admin-master/YY.Admin.Core/FluentValidation/IFluentAutoValidationRule.cs b/yy-admin-master/YY.Admin.Core/FluentValidation/IFluentAutoValidationRule.cs new file mode 100644 index 0000000..02beea7 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/FluentValidation/IFluentAutoValidationRule.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core.FluentValidation +{ + internal interface IFluentAutoValidationRule + { + } +} diff --git a/yy-admin-master/YY.Admin.Core/FluentValidation/MyFluentValidation.cs b/yy-admin-master/YY.Admin.Core/FluentValidation/MyFluentValidation.cs new file mode 100644 index 0000000..2d8a05a --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/FluentValidation/MyFluentValidation.cs @@ -0,0 +1,15 @@ +using System.Globalization; +using System.Windows.Controls; +using ValidationResult = System.Windows.Controls.ValidationResult; + +namespace YY.Admin.Core.FluentValidation +{ + public class MyFluentValidation : ValidationRule + { + public override ValidationResult Validate(object value, CultureInfo cultureInfo) + { + Console.WriteLine($"[FluentAutoValidationRule] Validate 调用,当前值:{value ?? "null"}"); + return ValidationResult.ValidResult; + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/FluentValidation/PasswordBoxHelper.cs b/yy-admin-master/YY.Admin.Core/FluentValidation/PasswordBoxHelper.cs new file mode 100644 index 0000000..e3d6ce1 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/FluentValidation/PasswordBoxHelper.cs @@ -0,0 +1,82 @@ +using System.Windows; +using System.Windows.Controls; + +namespace YY.Admin.Core.FluentValidation +{ + public static class PasswordBoxHelper + { + public static readonly DependencyProperty PasswordProperty = + DependencyProperty.RegisterAttached( + "Password", + typeof(string), + typeof(PasswordBoxHelper), + new FrameworkPropertyMetadata(string.Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnPasswordPropertyChanged)); + + public static readonly DependencyProperty AttachProperty = + DependencyProperty.RegisterAttached( + "Attach", + typeof(bool), + typeof(PasswordBoxHelper), + new System.Windows.PropertyMetadata(false, OnAttachChanged)); + + public static void SetAttach(DependencyObject dp, bool value) + { + dp.SetValue(AttachProperty, value); + } + + public static bool GetAttach(DependencyObject dp) + { + return (bool)dp.GetValue(AttachProperty); + } + + public static string GetPassword(DependencyObject dp) + { + return (string)dp.GetValue(PasswordProperty); + } + + public static void SetPassword(DependencyObject dp, string value) + { + dp.SetValue(PasswordProperty, value); + } + + private static void OnAttachChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) + { + if (sender is PasswordBox passwordBox) + { + if ((bool)e.OldValue) + { + passwordBox.PasswordChanged -= PasswordChanged; + } + + if ((bool)e.NewValue) + { + passwordBox.PasswordChanged += PasswordChanged; + } + } + } + + private static void OnPasswordPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) + { + if (sender is PasswordBox passwordBox) + { + // 防止无限循环 + passwordBox.PasswordChanged -= PasswordChanged; + + if (passwordBox.Password != (e.NewValue ?? "")) + { + passwordBox.Password = e.NewValue?.ToString() ?? ""; + } + + passwordBox.PasswordChanged += PasswordChanged; + } + } + + private static void PasswordChanged(object sender, RoutedEventArgs e) + { + if (sender is PasswordBox passwordBox) + { + SetPassword(passwordBox, passwordBox.Password); + } + } + } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/GlobalUsings.cs b/yy-admin-master/YY.Admin.Core/GlobalUsings.cs new file mode 100644 index 0000000..fe8f39f --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/GlobalUsings.cs @@ -0,0 +1,12 @@ + + +global using SqlSugar; +global using YY.Admin.Core; +global using System.ComponentModel.DataAnnotations; +global using System.ComponentModel; +global using Microsoft.Extensions.Logging; +global using YY.Admin.Core.Util; +global using YY.Admin.Core.SqlSugar; +global using YY.Admin.Core.Const; + + diff --git a/yy-admin-master/YY.Admin.Core/Helper/ScrollViewerHelper.cs b/yy-admin-master/YY.Admin.Core/Helper/ScrollViewerHelper.cs new file mode 100644 index 0000000..f273867 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Helper/ScrollViewerHelper.cs @@ -0,0 +1,80 @@ +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using PropertyMetadata = System.Windows.PropertyMetadata; + +namespace YY.Admin.Core.Helper +{ + /// + /// ScrollViewer 辅助类: + /// 解决 ScrollViewer 包裹 TreeView / ListBox 等控件时, + /// 鼠标滚轮在内部控件上无法触发外层 ScrollViewer 滚动的问题。 + /// + public static class ScrollViewerHelper + { + /// + /// 附加属性:是否启用鼠标滚轮事件向上传递 + /// + public static readonly DependencyProperty EnableMouseWheelPropagationProperty = + DependencyProperty.RegisterAttached( + "EnableMouseWheelPropagation", // 属性名 + typeof(bool), // 属性类型 + typeof(ScrollViewerHelper), // 所属类型 + new PropertyMetadata(false, OnEnableMouseWheelPropagationChanged)); // 默认值+回调 + + /// + /// 获取附加属性值(XAML 绑定/读取) + /// + public static bool GetEnableMouseWheelPropagation(DependencyObject obj) + => (bool)obj.GetValue(EnableMouseWheelPropagationProperty); + + /// + /// 设置附加属性值(XAML 设置) + /// + public static void SetEnableMouseWheelPropagation(DependencyObject obj, bool value) + => obj.SetValue(EnableMouseWheelPropagationProperty, value); + + /// + /// 当附加属性值发生变化时触发 + /// + private static void OnEnableMouseWheelPropagationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is UIElement element) + { + // 如果启用,则订阅 PreviewMouseWheel 事件 + if ((bool)e.NewValue) + element.PreviewMouseWheel += Element_PreviewMouseWheel; + else + // 否则移除事件 + element.PreviewMouseWheel -= Element_PreviewMouseWheel; + } + } + + /// + /// 当鼠标滚轮在目标控件上滚动时触发, + /// 将事件手动冒泡到上层(如 ScrollViewer)以实现滚动。 + /// + private static void Element_PreviewMouseWheel(object sender, MouseWheelEventArgs e) + { + // 若事件已被处理,则直接返回 + if (e.Handled) return; + + // 标记事件已处理,防止重复触发 + e.Handled = true; + + // 构造一个新的 MouseWheelEventArgs,用于向父级转发 + var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta) + { + RoutedEvent = UIElement.MouseWheelEvent, // 指定为标准鼠标滚轮事件 + Source = sender // 来源设为当前控件 + }; + + // 获取父元素,并将事件重新抛出 + if (sender is Control control) + { + var parent = control.Parent as UIElement; + parent?.RaiseEvent(eventArg); + } + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Helper/SystemHelper.cs b/yy-admin-master/YY.Admin.Core/Helper/SystemHelper.cs new file mode 100644 index 0000000..aee32ce --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Helper/SystemHelper.cs @@ -0,0 +1,29 @@ +using HandyControl.Data; +using Microsoft.Win32; +using System.Runtime.InteropServices; +using YY.Admin.Tools.Interop; + +namespace YY.Admin.Core.Helper +{ + public class SystemHelper + { + public static SystemVersionInfo GetSystemVersionInfo() + { + var osv = new InteropValues.RTL_OSVERSIONINFOEX(); + osv.dwOSVersionInfoSize = (uint)Marshal.SizeOf(osv); + InteropMethods.Gdip.RtlGetVersion(out osv); + return new SystemVersionInfo((int)osv.dwMajorVersion, (int)osv.dwMinorVersion, (int)osv.dwBuildNumber); + } + + private const string SkinTypeRegistryKeyName = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + + private const string SkinTypeRegistryValueName = "AppsUseLightTheme"; + + public static bool DetermineIfInLightThemeMode() + { + var value = Registry.GetValue(SkinTypeRegistryKeyName, SkinTypeRegistryValueName, "0"); + + return value != null && value.ToString() != "0"; + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Helper/UIHelper.cs b/yy-admin-master/YY.Admin.Core/Helper/UIHelper.cs new file mode 100644 index 0000000..319a569 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Helper/UIHelper.cs @@ -0,0 +1,31 @@ +using System.Windows.Media; + +namespace YY.Admin.Core.Helper +{ + public static class UIHelper + { + /// + /// 等待 WPF 完成下一次渲染帧,确保 UI 更新已经完成 + /// + /// + public static async Task WaitForRenderAsync() + { + var tcs = new TaskCompletionSource(); + + EventHandler handler = null!; + handler = (s, e) => + { + // 取消订阅 + CompositionTarget.Rendering -= handler; + // 标记任务完成 + tcs.TrySetResult(null); + }; + + // 订阅渲染事件 + CompositionTarget.Rendering += handler; + + // 等待下一次渲染 + await tcs.Task; + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Identity/UserContext.cs b/yy-admin-master/YY.Admin.Core/Identity/UserContext.cs new file mode 100644 index 0000000..86633ad --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Identity/UserContext.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +namespace YY.Admin.Core +{ + /// + ///用户上下文 + /// + public class UserContext + { + /// + /// 用户Id + /// + public long UserId { get; set; } + /// + ///租户Id + /// + public long TenantId { get; set; } + /// + ///账号 + /// + public string Account { get; set; } + /// + ///账号类型 + /// + public AccountTypeEnum AccountType { get; set; } + /// + ///真实姓名 + /// + public string RealName { get; set; } + /// + ///是否超管 + /// + public bool IsSuperAdmin { get; set; } + /// + /// 机构Id + /// + public long OrgId { get; set; } + /// + /// 用户token + /// + public UserToken Token { get; set; } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Identity/UserToken.cs b/yy-admin-master/YY.Admin.Core/Identity/UserToken.cs new file mode 100644 index 0000000..e466f37 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Identity/UserToken.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core +{ + /// + /// 会话token + /// + public class UserToken + { + /// + /// 访客token + /// + public string AccessToken { get; set; } + + /// + /// 刷新token + /// + public string RefreshToken { get; set; } + + /// + /// 过期时间 + /// + public DateTime RefreshExpires { get; set; } + } + public class RefreshTokenResult + { + public bool Success { get; set; } + public string AccessToken { get; set; } + public string RefreshToken { get; set; } + } +} diff --git a/yy-admin-master/YY.Admin.Core/LiveCharts2/CusSolidColorPaint.cs b/yy-admin-master/YY.Admin.Core/LiveCharts2/CusSolidColorPaint.cs new file mode 100644 index 0000000..c68e513 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/LiveCharts2/CusSolidColorPaint.cs @@ -0,0 +1,60 @@ +using LiveChartsCore.SkiaSharpView.Painting; +using SkiaSharp; + +namespace YY.Admin.Core.LiveCharts2 +{ + public class CusSolidColorPaint : SolidColorPaint + { + + /// + /// Gets or sets the font weight. + /// + public SKFontStyleWeight FontWeight { get; set; } = SKFontStyleWeight.Normal; + + /// + /// Gets or sets the font width. + /// + public SKFontStyleWidth FontWidth { get; set; } = SKFontStyleWidth.Normal; + + /// + /// Gets or sets the font slant. + /// + public SKFontStyleSlant FontSlant { get; set; } = SKFontStyleSlant.Upright; + + /// + /// Gets or sets a character to match the typeface to use. + /// + public string? TypefaceMatchesChar { get; set; } = null; + + + public String? CusColor { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public CusSolidColorPaint() + : base() + { } + + /// + /// Initializes a new instance of the class. + /// + /// The color. + public CusSolidColorPaint(SKColor color) + : base(color) + { + Color = color; + } + + /// + /// Initializes a new instance of the class. + /// + /// The color. + /// Width of the stroke. + public CusSolidColorPaint(SKColor color, float strokeWidth) + : base(color, strokeWidth) + { + Color = color; + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Logger/IClientLogReportSink.cs b/yy-admin-master/YY.Admin.Core/Logger/IClientLogReportSink.cs new file mode 100644 index 0000000..4d5ac46 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Logger/IClientLogReportSink.cs @@ -0,0 +1,18 @@ +namespace YY.Admin.Core; + +/// +/// 客户端日志上报接入点,用于将本地日志异步转发到后端。 +/// +public interface IClientLogReportSink +{ + /// + /// 上报一条通用日志。 + /// + Task ReportLogAsync( + string category, + string message, + string? account = null, + bool? success = null, + string? exception = null, + CancellationToken cancellationToken = default); +} diff --git a/yy-admin-master/YY.Admin.Core/Logger/ILoggerService.cs b/yy-admin-master/YY.Admin.Core/Logger/ILoggerService.cs new file mode 100644 index 0000000..38af0fc --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Logger/ILoggerService.cs @@ -0,0 +1,13 @@ +using System.Runtime.CompilerServices; + +namespace YY.Admin.Core +{ + public interface ILoggerService + { + void Information(string message); + void Warning(string message); + void Error(string message, Exception? ex = null); + void Debug(string message); + void Performance(string operationName, TimeSpan duration); + } +} diff --git a/yy-admin-master/YY.Admin.Core/Logger/SerilogLoggerService.cs b/yy-admin-master/YY.Admin.Core/Logger/SerilogLoggerService.cs new file mode 100644 index 0000000..3f17575 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Logger/SerilogLoggerService.cs @@ -0,0 +1,82 @@ +using Serilog; +using Serilog.Context; +using Serilog.Core; +using Serilog.Events; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace YY.Admin.Core +{ + public class SerilogLoggerService : ILoggerService + { + private readonly Serilog.ILogger _logger; + private readonly IReadOnlyList _logReportSinks; + + public SerilogLoggerService(IEnumerable logReportSinks) + { + _logReportSinks = logReportSinks?.ToList() ?? new List(); + _logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .Enrich.WithThreadId() + .Enrich.WithMachineName() + .WriteTo.Debug(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}") + .WriteTo.Console(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] {Message:lj}{NewLine}{Exception}") + .WriteTo.File("logs/app.log", + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: 7, + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] ({ThreadId}) {Message:lj}{NewLine}{Exception}") + .CreateLogger(); + } + + public void Information(string message) + { + _logger.Information(message); + _ = PushRemoteAsync("OPERATION", message); + } + + public void Warning(string message) + { + _logger.Warning(message); + _ = PushRemoteAsync("WARNING", message); + } + + public void Error(string message, Exception? ex = null) + { + _logger.Error(ex, message); + _ = PushRemoteAsync("EXCEPTION", message, exception: ex?.ToString()); + } + + public void Debug(string message) + { + _logger.Debug(message); + _ = PushRemoteAsync("DEBUG", message); + } + + public void Performance(string operationName, TimeSpan duration) + { + _logger.Information("Performance: {Operation} completed in {Duration}ms", + operationName, duration.TotalMilliseconds); + _ = PushRemoteAsync("OPERATION", $"Performance: {operationName} completed in {duration.TotalMilliseconds}ms"); + } + + private async Task PushRemoteAsync(string category, string message, bool? success = null, string? exception = null) + { + if (_logReportSinks.Count == 0 || string.IsNullOrWhiteSpace(message)) + { + return; + } + + foreach (var sink in _logReportSinks) + { + try + { + await sink.ReportLogAsync(category, message, null, success, exception); + } + catch + { + // 上报失败不影响本地主日志链路 + } + } + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Model/AppSettings.cs b/yy-admin-master/YY.Admin.Core/Model/AppSettings.cs new file mode 100644 index 0000000..06ae374 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Model/AppSettings.cs @@ -0,0 +1,25 @@ +using HandyControl.Data; + +namespace YY.Admin.Core.Model +{ + /// + /// 系统设置 + /// + public class AppSettings + { + /// + /// 皮肤类型 + /// + public SkinType SkinType { get; set; } + + /// + /// 皮肤是否与系统同步 + /// + public bool SyncWithSystem { get; set; } + + /// + /// 是否显示TabControl + /// + public bool IsTabControlVisible { get; set; } + } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Option/BaseCacheOptions.cs b/yy-admin-master/YY.Admin.Core/Option/BaseCacheOptions.cs new file mode 100644 index 0000000..559031c --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Option/BaseCacheOptions.cs @@ -0,0 +1,44 @@ +using NewLife.Caching; +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core.Option +{ + /// + /// 缓存配置选项 + /// + public sealed class BaseCacheOptions + { + public const string SectionName = "Cache"; + /// + /// 缓存前缀 + /// + public string Prefix { get; set; } + + /// + /// 缓存类型 + /// + public string CacheType { get; set; } + + /// + /// Redis缓存 + /// + public RedisOption Redis { get; set; } + } + + /// + /// Redis缓存 + /// + public sealed class RedisOption : RedisOptions + { + /// + /// 最大消息大小 + /// + public int MaxMessageSize { get; set; } + } + +} diff --git a/yy-admin-master/YY.Admin.Core/Option/DbConnectionOptions.cs b/yy-admin-master/YY.Admin.Core/Option/DbConnectionOptions.cs new file mode 100644 index 0000000..1aec975 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Option/DbConnectionOptions.cs @@ -0,0 +1,130 @@ +using Microsoft.Extensions.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using YY.Admin.Core.Const; + +namespace YY.Admin.Core.Option +{ + /// + /// 数据库配置选项 + /// + public sealed class DbConnectionOptions + { + /// + /// 启用控制台打印SQL + /// + public bool EnableConsoleSql { get; set; } + + /// + /// 超级管理员是否忽略逻辑删除过滤器 + /// + public bool SuperAdminIgnoreIDeletedFilter { get; set; } + + /// + /// 数据库 + /// + public List ConnectionConfigs { get; set; } + } + + /// + /// 数据库连接配置 + /// + public sealed class DbConnectionConfig : ConnectionConfig + { + /// + /// 数据库名称 + /// + public string DbNickName { get; set; } + + /// + /// 数据库配置 + /// + public DbSettings DbSettings { get; set; } + + /// + /// 表配置 + /// + public TableSettings TableSettings { get; set; } + + /// + /// 种子配置 + /// + public SeedSettings SeedSettings { get; set; } + + /// + /// 隔离方式 + /// + public TenantTypeEnum TenantType { get; set; } = TenantTypeEnum.Id; + + /// + /// 数据库存储目录(仅SqlServer支持指定目录创建) + /// + public string DatabaseDirectory { get; set; } + } + + /// + /// 数据库配置 + /// + public sealed class DbSettings + { + /// + /// 启用库表初始化 + /// + public bool EnableInitDb { get; set; } + + /// + /// 启用视图初始化 + /// + public bool EnableInitView { get; set; } + + /// + /// 启用库表差异日志 + /// + public bool EnableDiffLog { get; set; } + + /// + /// 启用驼峰转下划线 + /// + public bool EnableUnderLine { get; set; } + + /// + /// 启用数据库连接串加密策略 + /// + public bool EnableConnStringEncrypt { get; set; } + } + + /// + /// 表配置 + /// + public sealed class TableSettings + { + /// + /// 启用表初始化 + /// + public bool EnableInitTable { get; set; } + + /// + /// 启用表增量更新 + /// + public bool EnableIncreTable { get; set; } + } + + /// + /// 种子配置 + /// + public sealed class SeedSettings + { + /// + /// 启用种子初始化 + /// + public bool EnableInitSeed { get; set; } + + /// + /// 启用种子增量更新 + /// + public bool EnableIncreSeed { get; set; } + } +} diff --git a/yy-admin-master/YY.Admin.Core/SeedData/SysConfigSeedData.cs b/yy-admin-master/YY.Admin.Core/SeedData/SysConfigSeedData.cs new file mode 100644 index 0000000..32f5416 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/SeedData/SysConfigSeedData.cs @@ -0,0 +1,39 @@ + + + + + +namespace YY.Admin.Core.SeedData; + +/// +/// 系统配置表种子数据 +/// +public class SysConfigSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return new[] + { + new SysConfig{ Id=1300000000101, Name="演示环境", Code=ConfigConst.SysDemoEnv, Value="False", SysFlag=YesNoEnum.Y, Remark="演示环境", OrderNo=10, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000111, Name="默认密码", Code=ConfigConst.SysPassword, Value="123456", SysFlag=YesNoEnum.Y, Remark="默认密码", OrderNo=20, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000121, Name="密码最大错误次数", Code=ConfigConst.SysPasswordMaxErrorTimes, Value="5", SysFlag=YesNoEnum.Y, Remark="允许密码最大输入错误次数", OrderNo=30, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000131, Name="日志保留天数", Code=ConfigConst.SysLogRetentionDays, Value="180", SysFlag=YesNoEnum.Y, Remark="日志保留天数(天)", OrderNo=40, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000141, Name="记录操作日志", Code=ConfigConst.SysOpLog, Value="True", SysFlag=YesNoEnum.Y, Remark="是否记录操作日志", OrderNo=50, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000151, Name="单设备登录", Code=ConfigConst.SysSingleLogin, Value="False", SysFlag=YesNoEnum.Y, Remark="是否开启单设备登录", OrderNo=60, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000152, Name="登入登出提醒", Code=ConfigConst.SysLoginOutReminder, Value="False", SysFlag=YesNoEnum.Y, Remark="是否开启登入登出提醒", OrderNo=60, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000161, Name="登录二次验证", Code=ConfigConst.SysSecondVer, Value="False", SysFlag=YesNoEnum.Y, Remark="是否开启登录二次验证", OrderNo=70, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000171, Name="图形验证码", Code=ConfigConst.SysCaptcha, Value="True", SysFlag=YesNoEnum.Y, Remark="是否开启图形验证码", OrderNo=80, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000172, Name="登录时隐藏租户", Code=ConfigConst.SysHideTenantLogin, Value="True", SysFlag=YesNoEnum.Y, Remark="登录时隐藏租户", OrderNo=90, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000181, Name="Token过期时间", Code=ConfigConst.SysTokenExpire, Value="30", SysFlag=YesNoEnum.Y, Remark="Token过期时间(分钟)", OrderNo=100, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000191, Name="RefreshToken过期时间", Code=ConfigConst.SysRefreshTokenExpire, Value="20160", SysFlag=YesNoEnum.Y, Remark="刷新Token过期时间(分钟)(一般 refresh_token 的有效时间 > 2 * access_token 的有效时间)", OrderNo=110, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000201, Name="发送异常日志邮件", Code=ConfigConst.SysErrorMail, Value="False", SysFlag=YesNoEnum.Y, Remark="是否发送异常日志邮件", OrderNo=120, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000211, Name="域登录验证", Code=ConfigConst.SysDomainLogin, Value="False", SysFlag=YesNoEnum.Y, Remark="是否开启域登录验证", OrderNo=130, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000221, Name="数据校验日志", Code=ConfigConst.SysValidationLog, Value="True", SysFlag=YesNoEnum.Y, Remark="是否数据校验日志", OrderNo=140, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysConfig{ Id=1300000000231, Name="行政区域同步层级", Code=ConfigConst.SysRegionSyncLevel, Value="3", SysFlag=YesNoEnum.Y, Remark="行政区域同步层级 1-省级,2-市级,3-区县级,4-街道级,5-村级", OrderNo=150, GroupCode=ConfigConst.SysDefaultGroup, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + }; + } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/SeedData/SysDictDataSeedData.cs b/yy-admin-master/YY.Admin.Core/SeedData/SysDictDataSeedData.cs new file mode 100644 index 0000000..954fe12 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/SeedData/SysDictDataSeedData.cs @@ -0,0 +1,80 @@ + + +namespace YY.Admin.Core.SeedData; + +/// +/// 系统字典值表种子数据 +/// +public class SysDictDataSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + var typeList = new SysDictTypeSeedData().HasData().ToList(); + return new[] + { + new SysDictData{ Id=1300000000101, DictTypeId=typeList[0].Id, Label="输入框", Value="Input", OrderNo=100, Remark="输入框", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000102, DictTypeId=typeList[0].Id, Label="字典选择器", Value="DictSelector", OrderNo=100, Remark="字典选择器", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000103, DictTypeId=typeList[0].Id, Label="常量选择器", Value="ConstSelector", OrderNo=100, Remark="常量选择器", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000104, DictTypeId=typeList[0].Id, Label="枚举选择器", Value="EnumSelector", OrderNo=100, Remark="枚举选择器", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000105, DictTypeId=typeList[0].Id, Label="树选择器", Value="ApiTreeSelector", OrderNo=100, Remark="树选择器", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000106, DictTypeId=typeList[0].Id, Label="外键", Value="ForeignKey", OrderNo=100, Remark="外键", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000107, DictTypeId=typeList[0].Id, Label="数字输入框", Value="InputNumber", OrderNo=100, Remark="数字输入框", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000108, DictTypeId=typeList[0].Id, Label="时间选择", Value="DatePicker", OrderNo=100, Remark="时间选择", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000109, DictTypeId=typeList[0].Id, Label="文本域", Value="InputTextArea", OrderNo=100, Remark="文本域", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000110, DictTypeId=typeList[0].Id, Label="上传", Value="Upload", OrderNo=100, Remark="上传", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000111, DictTypeId=typeList[0].Id, Label="开关", Value="Switch", OrderNo=100, Remark="开关", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + + new SysDictData{ Id=1300000000201, DictTypeId=typeList[1].Id, Label="等于", Value="==", OrderNo=1, Remark="等于", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000202, DictTypeId=typeList[1].Id, Label="模糊", Value="like", OrderNo=1, Remark="模糊", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000203, DictTypeId=typeList[1].Id, Label="大于", Value=">", OrderNo=1, Remark="大于", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000204, DictTypeId=typeList[1].Id, Label="小于", Value="<", OrderNo=1, Remark="小于", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000205, DictTypeId=typeList[1].Id, Label="不等于", Value="!=", OrderNo=1, Remark="不等于", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000206, DictTypeId=typeList[1].Id, Label="大于等于", Value=">=", OrderNo=1, Remark="大于等于", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000207, DictTypeId=typeList[1].Id, Label="小于等于", Value="<=", OrderNo=1, Remark="小于等于", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000208, DictTypeId=typeList[1].Id, Label="不为空", Value="isNotNull", OrderNo=1, Remark="不为空", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000209, DictTypeId=typeList[1].Id, Label="时间范围", Value="~", OrderNo=1, Remark="时间范围", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + + new SysDictData{ Id=1300000000301, DictTypeId=typeList[2].Id, Label="long", Value="long", OrderNo=1, Remark="long", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000302, DictTypeId=typeList[2].Id, Label="string", Value="string", OrderNo=1, Remark="string", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000303, DictTypeId=typeList[2].Id, Label="DateTime", Value="DateTime", OrderNo=1, Remark="DateTime", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000304, DictTypeId=typeList[2].Id, Label="bool", Value="bool", OrderNo=1, Remark="bool", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000305, DictTypeId=typeList[2].Id, Label="int", Value="int", OrderNo=1, Remark="int", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000306, DictTypeId=typeList[2].Id, Label="double", Value="double", OrderNo=1, Remark="double", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000307, DictTypeId=typeList[2].Id, Label="float", Value="float", OrderNo=1, Remark="float", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000308, DictTypeId=typeList[2].Id, Label="decimal", Value="decimal", OrderNo=1, Remark="decimal", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000309, DictTypeId=typeList[2].Id, Label="Guid", Value="Guid", OrderNo=1, Remark="Guid", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000310, DictTypeId=typeList[2].Id, Label="DateTimeOffset", Value="DateTimeOffset", OrderNo=1, Remark="DateTimeOffset", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + + new SysDictData{ Id=1300000000401, DictTypeId=typeList[3].Id, Label="下载压缩包", Value="100", OrderNo=1, Remark="下载压缩包", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000402, DictTypeId=typeList[3].Id, Label="下载压缩包(前端)", Value="111", OrderNo=2, Remark="下载压缩包(前端)", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000403, DictTypeId=typeList[3].Id, Label="下载压缩包(后端)", Value="121", OrderNo=3, Remark="下载压缩包(后端)", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000404, DictTypeId=typeList[3].Id, Label="生成到本项目", Value="200", OrderNo=4, Remark="生成到本项目", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000405, DictTypeId=typeList[3].Id, Label="生成到本项目(前端)", Value="211", OrderNo=5, Remark="生成到本项目(前端)", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000406, DictTypeId=typeList[3].Id, Label="生成到本项目(后端)", Value="221", OrderNo=6, Remark="生成到本项目(后端)", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + + new SysDictData{ Id=1300000000501, DictTypeId=typeList[4].Id, Label="EntityBaseId【基础实体Id】", Value="EntityBaseId", OrderNo=1, Remark="【基础实体Id】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000502, DictTypeId=typeList[4].Id, Label="EntityBase【基础实体】", Value="EntityBase", OrderNo=1, Remark="【基础实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000503, DictTypeId=typeList[4].Id, Label="EntityBaseDel【基础软删除实体】", Value="EntityBaseDel", OrderNo=1, Remark="【基础软删除实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000504, DictTypeId=typeList[4].Id, Label="EntityBaseOrg【机构实体】", Value="EntityBaseOrg", OrderNo=1, Remark="【机构实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000505, DictTypeId=typeList[4].Id, Label="EntityBaseOrgDel【机构软删除实体】", Value="EntityBaseOrgDel", OrderNo=1, Remark="【机构软删除实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000506, DictTypeId=typeList[4].Id, Label="EntityBaseTenantId【租户实体Id】", Value="EntityBaseTenantId", OrderNo=1, Remark="【租户实体Id】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000507, DictTypeId=typeList[4].Id, Label="EntityBaseTenant【租户实体】", Value="EntityBaseTenant", OrderNo=1, Remark="【租户实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000508, DictTypeId=typeList[4].Id, Label="EntityBaseTenantDel【租户软删除实体】", Value="EntityBaseTenantDel", OrderNo=1, Remark="【租户软删除实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000509, DictTypeId=typeList[4].Id, Label="EntityBaseTenantOrg【租户机构实体】", Value="EntityBaseTenantOrg", OrderNo=1, Remark="【租户机构实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictData{ Id=1300000000510, DictTypeId=typeList[4].Id, Label="EntityBaseTenantOrgDel【租户机构软删除实体】", Value="EntityBaseTenantOrgDel", OrderNo=1, Remark="【租户机构软删除实体】", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + + new SysDictData{ Id=1300000000601, DictTypeId=typeList[5].Id, Label="不需要", Value="off", OrderNo=100, Remark="不需要打印支持", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-12-04 00:00:00") }, + new SysDictData{ Id=1300000000602, DictTypeId=typeList[5].Id, Label="绑定打印模版", Value="custom", OrderNo=101, Remark="绑定打印模版", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-12-04 00:00:00") }, + + new SysDictData{ Id=1300000000701, DictTypeId=typeList[6].Id, Label="集团", Value="101", OrderNo=100, Remark="集团", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-02-10 00:00:00") }, + new SysDictData{ Id=1300000000702, DictTypeId=typeList[6].Id, Label="公司", Value="201", OrderNo=101, Remark="公司", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-02-10 00:00:00") }, + new SysDictData{ Id=1300000000703, DictTypeId=typeList[6].Id, Label="部门", Value="301", OrderNo=102, Remark="部门", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-02-10 00:00:00") }, + new SysDictData{ Id=1300000000704, DictTypeId=typeList[6].Id, Label="区域", Value="401", OrderNo=103, Remark="区域", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-02-10 00:00:00") }, + new SysDictData{ Id=1300000000705, DictTypeId=typeList[6].Id, Label="组", Value="501", OrderNo=104, Remark="组", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-02-10 00:00:00") }, + }; + } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/SeedData/SysDictTypeSeedData.cs b/yy-admin-master/YY.Admin.Core/SeedData/SysDictTypeSeedData.cs new file mode 100644 index 0000000..213c73d --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/SeedData/SysDictTypeSeedData.cs @@ -0,0 +1,27 @@ + + +namespace YY.Admin.Core.SeedData; + +/// +/// 系统字典类型表种子数据 +/// +public class SysDictTypeSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return new[] + { + new SysDictType{ Id=1300000000111, Name="代码生成控件类型", Code="code_gen_effect_type", SysFlag=YesNoEnum.Y, OrderNo=100, Remark="代码生成控件类型", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictType{ Id=1300000000121, Name="代码生成查询类型", Code="code_gen_query_type", SysFlag=YesNoEnum.Y, OrderNo=101, Remark="代码生成查询类型", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictType{ Id=1300000000131, Name="代码生成.NET类型", Code="code_gen_net_type", SysFlag=YesNoEnum.Y, OrderNo=102, Remark="代码生成.NET类型", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictType{ Id=1300000000141, Name="代码生成方式", Code="code_gen_create_type", SysFlag=YesNoEnum.Y, OrderNo=103, Remark="代码生成方式", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictType{ Id=1300000000151, Name="代码生成基类", Code="code_gen_base_class", SysFlag=YesNoEnum.Y, OrderNo=104, Remark="代码生成基类", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + new SysDictType{ Id=1300000000161, Name="代码生成打印类型", Code="code_gen_print_type", SysFlag=YesNoEnum.Y, OrderNo=105, Remark="代码生成打印类型", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-12-04 00:00:00") }, + new SysDictType{ Id=1300000000171, Name="机构类型", Code="org_type", SysFlag=YesNoEnum.Y, OrderNo=201, Remark="机构类型", Status=StatusEnum.Enable, CreateTime=DateTime.Parse("2023-02-10 00:00:00") }, + }; + } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/SeedData/SysMenuSeedData.cs b/yy-admin-master/YY.Admin.Core/SeedData/SysMenuSeedData.cs new file mode 100644 index 0000000..c31fdcd --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/SeedData/SysMenuSeedData.cs @@ -0,0 +1,224 @@ + + +namespace YY.Admin.Core.SeedData; + +/// +/// 系统菜单表种子数据 +/// +public class SysMenuSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return new[] + { + new SysMenu{ Id=1300100000101, Pid=0, Title="工作台", Path="", Name="dashboard", Component="Layout", Icon="", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=0 }, + new SysMenu{ Id=1300100010101, Pid=1300100000101, Title="仪表盘", Path="DashboardView", Name="home", Component="/home/index", IsAffix=true, Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300100010201, Pid=1300100000101, Title="站内信", Path="/dashboard/notice", Name="notice", Component="/home/notice/index", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=101 }, + + // 建议此处Id范围之间放置具体业务应用菜单 + + #region 系统管理 + + new SysMenu{ Id=1300200000101, Pid=0, Title="系统管理", Path="", Name="system", Component="Layout", Icon="", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=10000 }, + + // 账号管理 + new SysMenu{ Id=1300200010101, Pid=1300200000101, Title="账号管理", Path="UserManagementView", Name="sysUser", Component="/system/user/index", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010201, Pid=1300200010101, Title="查询", Permission="sysUser:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010301, Pid=1300200010101, Title="编辑", Permission="sysUser:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010401, Pid=1300200010101, Title="增加", Permission="sysUser:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010501, Pid=1300200010101, Title="删除", Permission="sysUser:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010601, Pid=1300200010101, Title="详情", Permission="sysUser:detail", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010701, Pid=1300200010101, Title="授权角色", Permission="sysUser:grantRole", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010801, Pid=1300200010101, Title="重置密码", Permission="sysUser:resetPwd", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200010901, Pid=1300200010101, Title="设置状态", Permission="sysUser:setStatus", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200011001, Pid=1300200010101, Title="强制下线", Permission="sysOnlineUser:forceOffline", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200011101, Pid=1300200010101, Title="解除锁定", Permission="sysUser:unlockLogin", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 数据字典(SCADA同步) + new SysMenu{ Id=1300200012001, Pid=1300200000101, Title="数据字典", Path="DataDictionaryManagementView", Name="sysDict", Component="/system/dict/index", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=105 }, + new SysMenu{ Id=1300200012011, Pid=1300200012001, Title="查询", Permission="sysDict:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200012021, Pid=1300200012001, Title="同步", Permission="sysDict:sync", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 角色管理 + new SysMenu{ Id=1300200020101, Pid=1300200000101, Title="角色管理", Path="RoleManagementView", Name="sysRole", Component="/system/role/index", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 }, + new SysMenu{ Id=1300200020201, Pid=1300200020101, Title="查询", Permission="sysRole:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200020301, Pid=1300200020101, Title="编辑", Permission="sysRole:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200020401, Pid=1300200020101, Title="增加", Permission="sysRole:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200020501, Pid=1300200020101, Title="删除", Permission="sysRole:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200020601, Pid=1300200020101, Title="授权菜单", Permission="sysRole:grantMenu", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200020701, Pid=1300200020101, Title="授权数据", Permission="sysRole:grantDataScope", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200020801, Pid=1300200020101, Title="设置状态", Permission="sysRole:setStatus", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 机构管理 + new SysMenu{ Id=1300200030101, Pid=1300200000101, Title="机构管理", Path="/system/org", Name="sysOrg", Component="/system/org/index", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=120 }, + new SysMenu{ Id=1300200030201, Pid=1300200030101, Title="查询", Permission="sysOrg:list", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200030301, Pid=1300200030101, Title="编辑", Permission="sysOrg:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200030401, Pid=1300200030101, Title="增加", Permission="sysOrg:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200030501, Pid=1300200030101, Title="删除", Permission="sysOrg:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 职位管理 + new SysMenu{ Id=1300200040101, Pid=1300200000101, Title="职位管理", Path="/system/pos", Name="sysPos", Component="/system/pos/index",Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=130 }, + new SysMenu{ Id=1300200040201, Pid=1300200040101, Title="查询", Permission="sysPos:list", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200040301, Pid=1300200040101, Title="编辑", Permission="sysPos:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200040401, Pid=1300200040101, Title="增加", Permission="sysPos:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200040501, Pid=1300200040101, Title="删除", Permission="sysPos:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 个人中心 + new SysMenu{ Id=1300200050101, Pid=1300200000101, Title="个人中心", Path="/system/userCenter", Name="sysUserCenter", Component="/system/user/component/userCenter",Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=140 }, + new SysMenu{ Id=1300200050201, Pid=1300200050101, Title="修改密码", Permission="sysUser:changePwd", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200050301, Pid=1300200050101, Title="基本信息", Permission="sysUser:baseInfo", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200050401, Pid=1300200050101, Title="电子签名", Permission="sysFile:uploadSignature", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200050501, Pid=1300200050101, Title="上传头像", Permission="sysFile:uploadAvatar", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 通知公告 + new SysMenu{ Id=1300200060101, Pid=1300200000101, Title="通知公告", Path="/system/notice", Name="sysNotice", Component="/system/notice/index",Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=150 }, + new SysMenu{ Id=1300200060201, Pid=1300200060101, Title="查询", Permission="sysNotice:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200060301, Pid=1300200060101, Title="编辑", Permission="sysNotice:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200060401, Pid=1300200060101, Title="增加", Permission="sysNotice:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200060501, Pid=1300200060101, Title="删除", Permission="sysNotice:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200060601, Pid=1300200060101, Title="发布", Permission="sysNotice:public", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200060701, Pid=1300200060101, Title="撤回", Permission="sysNotice:cancel", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // AD域配置 + new SysMenu{ Id=1300200080101, Pid=1300200000101, Title="AD域配置", Path="/system/ldap", Name="sysLdap", Component="/system/ldap/index",Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=170 }, + new SysMenu{ Id=1300200080201, Pid=1300200080101, Title="查询", Permission="sysLdap:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200080301, Pid=1300200080101, Title="详情", Permission="sysLdap:detail", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 }, + new SysMenu{ Id=1300200080401, Pid=1300200080101, Title="编辑", Permission="sysLdap:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=120 }, + new SysMenu{ Id=1300200080501, Pid=1300200080101, Title="增加", Permission="sysLdap:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=130 }, + new SysMenu{ Id=1300200080601, Pid=1300200080101, Title="删除", Permission="sysLdap:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=140 }, + new SysMenu{ Id=1300200080701, Pid=1300200080101, Title="同步域账户", Permission="sysLdap:syncUser", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=150 }, + new SysMenu{ Id=1300200080801, Pid=1300200080101, Title="同步域组织", Permission="sysLdap:syncOrg", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=160 }, + + #endregion + + #region 平台管理 + + new SysMenu{ Id=1300300000101, Pid=0, Title="平台管理", Path="/platform", Name="platform", Component="Layout", Icon="", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=11000 }, + + // 租户管理 + new SysMenu{ Id=1300300010101, Pid=1300300000101, Title="租户管理", Path="/platform/tenant", Name="sysTenant", Component="/system/tenant/index", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010201, Pid=1300300010101, Title="查询", Permission="sysTenant:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010301, Pid=1300300010101, Title="编辑", Permission="sysTenant:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010401, Pid=1300300010101, Title="增加", Permission="sysTenant:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010501, Pid=1300300010101, Title="删除", Permission="sysTenant:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010601, Pid=1300300010101, Title="授权菜单", Permission="sysTenant:grantMenu", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010701, Pid=1300300010101, Title="重置密码", Permission="sysTenant:resetPwd", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010801, Pid=1300300010101, Title="生成库", Permission="sysTenant:createDb", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300010901, Pid=1300300010101, Title="设置状态", Permission="sysTenant:setStatus", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300011001, Pid=1300300010101, Title="同步授权", Permission="sysTenant:syncGrantMenu", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300011101, Pid=1300300010101, Title="切换租户", Permission="sysTenant:changeTenant", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300011201, Pid=1300300010101, Title="进入租管端", Permission="sysTenant:goTenant", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 菜单管理 + new SysMenu{ Id=1300300030101, Pid=1300300000101, Title="菜单管理", Path="/platform/menu", Name="sysMenu", Component="/system/menu/index", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 }, + new SysMenu{ Id=1300300030201, Pid=1300300030101, Title="查询", Permission="sysMenu:list", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300030301, Pid=1300300030101, Title="编辑", Permission="sysMenu:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300030401, Pid=1300300030101, Title="增加", Permission="sysMenu:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300030501, Pid=1300300030101, Title="删除", Permission="sysMenu:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 平台参数 + new SysMenu{ Id=1300300040101, Pid=1300300000101, Title="平台参数", Path="/platform/config", Name="sysConfig", Component="/system/config/index", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=120 }, + new SysMenu{ Id=1300300040201, Pid=1300300040101, Title="查询", Permission="sysConfig:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300040301, Pid=1300300040101, Title="编辑", Permission="sysConfig:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300040401, Pid=1300300040101, Title="增加", Permission="sysConfig:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300040501, Pid=1300300040101, Title="删除", Permission="sysConfig:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 字典管理 + new SysMenu{ Id=1300300050101, Pid=1300300000101, Title="字典管理", Path="/platform/dict", Name="sysDict", Component="/system/dict/index", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=130 }, + new SysMenu{ Id=1300300050201, Pid=1300300050101, Title="查询", Permission="sysDictType:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300050301, Pid=1300300050101, Title="编辑", Permission="sysDictType:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300050401, Pid=1300300050101, Title="增加", Permission="sysDictType:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300050501, Pid=1300300050101, Title="删除", Permission="sysDictType:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300050601, Pid=1300300050101, Title="增加字典值", Permission="sysDictData:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300050701, Pid=1300300050101, Title="删除字典值", Permission="sysDictData:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300050801, Pid=1300300050101, Title="编辑字典值", Permission="sysDictData:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300050901, Pid=1300300050101, Title="字典迁移", Permission="sysDictType:move", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 模板管理 + new SysMenu{ Id=1300300051101, Pid=1300300000101, Title="模板管理", Path="/platform/template", Name="sysTemplate", Component="/system/template/index", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=135 }, + new SysMenu{ Id=1300300051201, Pid=1300300051101, Title="查询", Permission="sysTemplate:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300051301, Pid=1300300051101, Title="编辑", Permission="sysTemplate:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300051401, Pid=1300300051101, Title="增加", Permission="sysTemplate:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300051501, Pid=1300300051101, Title="删除", Permission="sysTemplate:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300051601, Pid=1300300051101, Title="预览", Permission="sysTemplate:preview", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 任务调度 + new SysMenu{ Id=1300300060101, Pid=1300300000101, Title="任务调度", Path="/platform/job", Name="sysJob", Component="/system/job/index", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=140 }, + new SysMenu{ Id=1300300060201, Pid=1300300060101, Title="查询", Permission="sysJob:pageJobDetail", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300060301, Pid=1300300060101, Title="编辑", Permission="sysJob:updateJobDetail", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300060401, Pid=1300300060101, Title="增加", Permission="sysJob:addJobDetail", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300060501, Pid=1300300060101, Title="删除", Permission="sysJob:deleteJobDetail", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 系统参数 + new SysMenu{ Id=1300200090101, Pid=1300200000101, Title="系统参数", Path="/system/tenantConfig", Name="sysTenantConfig", Component="/system/tenantConfig/index", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=180 }, + new SysMenu{ Id=1300200090201, Pid=1300200090101, Title="查询", Permission="sysTenantConfig:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200090301, Pid=1300200090101, Title="编辑", Permission="sysTenantConfig:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200090401, Pid=1300200090101, Title="增加", Permission="sysTenantConfig:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300200090501, Pid=1300200090101, Title="删除", Permission="sysTenantConfig:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 系统监控 + new SysMenu{ Id=1300300070101, Pid=1300300000101, Title="系统监控", Path="/platform/server", Name="sysServer", Component="/system/server/index", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=150 }, + + // 行政区域 + new SysMenu{ Id=1300300090101, Pid=1300300000101, Title="行政区域", Path="/platform/region", Name="sysRegion", Component="/system/region/index", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=170 }, + new SysMenu{ Id=1300300090201, Pid=1300300090101, Title="查询", Permission="sysRegion:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300090301, Pid=1300300090101, Title="编辑", Permission="sysRegion:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300090401, Pid=1300300090101, Title="增加", Permission="sysRegion:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300090501, Pid=1300300090101, Title="删除", Permission="sysRegion:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300090601, Pid=1300300090101, Title="同步", Permission="sysRegion:sync", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 文件管理 + new SysMenu{ Id=1300300100101, Pid=1300300000101, Title="文件管理", Path="/platform/file", Name="sysFile", Component="/system/file/index", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=180 }, + new SysMenu{ Id=1300300100201, Pid=1300300100101, Title="查询", Permission="sysFile:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300100301, Pid=1300300100101, Title="上传", Permission="sysFile:uploadFile", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300100401, Pid=1300300100101, Title="下载", Permission="sysFile:downloadFile", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300100501, Pid=1300300100101, Title="删除", Permission="sysFile:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300100601, Pid=1300300100101, Title="编辑", Permission="sysFile:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2023-10-27 00:00:00"), OrderNo=100 }, + + // 打印模板 + new SysMenu{ Id=1300300110101, Pid=1300300000101, Title="打印模板", Path="/platform/print", Name="sysPrint", Component="/system/print/index", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=190 }, + new SysMenu{ Id=1300300110201, Pid=1300300110101, Title="查询", Permission="sysPrint:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300110301, Pid=1300300110101, Title="编辑", Permission="sysPrint:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300110401, Pid=1300300110101, Title="增加", Permission="sysPrint:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300300110501, Pid=1300300110101, Title="删除", Permission="sysPrint:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 系统配置 + new SysMenu{ Id=1300300140101, Pid=1300300000101, Title="系统配置", Path="/platform/infoSetting", Name="sysInfoSetting", Component="/system/infoSetting/index", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=220 }, + #endregion + + #region 日志管理 + + new SysMenu{ Id=1300400000101, Pid=0, Title="日志管理", Path="/log", Name="log", Component="Layout", Icon="", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=12000 }, + + // 访问日志 + new SysMenu{ Id=1300400010101, Pid=1300400000101, Title="访问日志", Path="/log/vislog", Name="sysVisLog", Component="/system/log/vislog/index", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300400010201, Pid=1300400010101, Title="查询", Permission="sysVislog:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300400010301, Pid=1300400010101, Title="清空", Permission="sysVislog:clear", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 操作日志 + new SysMenu{ Id=1300400020101, Pid=1300400000101, Title="操作日志", Path="/log/oplog", Name="sysOpLog", Component="/system/log/oplog/index", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=110 }, + new SysMenu{ Id=1300400020201, Pid=1300400020101, Title="查询", Permission="sysOplog:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300400020301, Pid=1300400020101, Title="清空", Permission="sysOplog:clear", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300400020401, Pid=1300400020101, Title="导出", Permission="sysOplog:export", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 异常日志 + new SysMenu{ Id=1300400030101, Pid=1300400000101, Title="异常日志", Path="/log/exlog", Name="sysExLog", Component="/system/log/exlog/index", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=120 }, + new SysMenu{ Id=1300400030201, Pid=1300400030101, Title="查询", Permission="sysExlog:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300400030301, Pid=1300400030101, Title="清空", Permission="sysExlog:clear", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300400030401, Pid=1300400030101, Title="导出", Permission="sysExlog:export", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + // 差异日志 + new SysMenu{ Id=1300400040101, Pid=1300400000101, Title="差异日志", Path="/log/difflog", Name="sysDiffLog", Component="/system/log/difflog/index", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=130 }, + new SysMenu{ Id=1300400040201, Pid=1300400040101, Title="查询", Permission="sysDifflog:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + new SysMenu{ Id=1300400040301, Pid=1300400040101, Title="清空", Permission="sysDifflog:clear", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 }, + + #endregion + // 关于项目 + new SysMenu{ Id=1300700000101, Pid=0, Title="关于项目", Path="/about", Name="about", Component="/about/index", Icon="", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2023-03-12 00:00:00"), OrderNo=15000 }, + }; + } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/SeedData/SysOrgSeedData.cs b/yy-admin-master/YY.Admin.Core/SeedData/SysOrgSeedData.cs new file mode 100644 index 0000000..c76eed8 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/SeedData/SysOrgSeedData.cs @@ -0,0 +1,27 @@ + + +namespace YY.Admin.Core.SeedData; + +/// +/// 系统机构表种子数据 +/// +[IgnoreUpdateSeed] +public class SysOrgSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + var admin = new SysUserSeedData().HasData().First(u => u.Account == "admin"); + return new[] + { + new SysOrg{ Id=SqlSugarConst.DefaultTenantId, Pid=0, Name="系统默认", Code="1001", Type="101", Level=1, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="系统默认", TenantId=SqlSugarConst.DefaultTenantId }, + new SysOrg{ Id=SqlSugarConst.DefaultTenantId + 1, Pid=SqlSugarConst.DefaultTenantId, Name="市场部", Code="100101", Level=2, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="市场部", CreateUserId=admin.Id, TenantId=SqlSugarConst.DefaultTenantId }, + new SysOrg{ Id=SqlSugarConst.DefaultTenantId + 2, Pid=SqlSugarConst.DefaultTenantId, Name="开发部", Code="100102", Level=2, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="开发部", CreateUserId=admin.Id, TenantId=SqlSugarConst.DefaultTenantId }, + new SysOrg{ Id=SqlSugarConst.DefaultTenantId + 3, Pid=SqlSugarConst.DefaultTenantId, Name="售后部", Code="100103", Level=2, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="售后部", CreateUserId=admin.Id, TenantId=SqlSugarConst.DefaultTenantId }, + new SysOrg{ Id=SqlSugarConst.DefaultTenantId + 4, Pid=SqlSugarConst.DefaultTenantId, Name="其他", Code="10010301", Level=3, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="其他", CreateUserId=admin.Id, TenantId=SqlSugarConst.DefaultTenantId }, + }; + } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/SeedData/SysPosSeedData.cs b/yy-admin-master/YY.Admin.Core/SeedData/SysPosSeedData.cs new file mode 100644 index 0000000..046e285 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/SeedData/SysPosSeedData.cs @@ -0,0 +1,36 @@ + + +namespace YY.Admin.Core.SeedData; + +/// +/// 系统职位表种子数据 +/// +public class SysPosSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return new[] + { + new SysPos{ Id=1300000000101, Name="党委书记", Code="dwsj", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="党委书记", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000102, Name="董事长", Code="dsz", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="董事长", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000103, Name="副董事长", Code="fdsz", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="副董事长", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000104, Name="总经理", Code="zjl", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="总经理", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000105, Name="副总经理", Code="fzjl", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="副总经理", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000106, Name="部门经理", Code="bmjl", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="部门经理", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000107, Name="部门副经理", Code="bmfjl", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="部门副经理", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000108, Name="主任", Code="zr", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="主任", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000109, Name="副主任", Code="fzr", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="副主任", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000110, Name="局长", Code="jz", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="局长", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000111, Name="副局长", Code="fjz", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="副局长", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000112, Name="科长", Code="kz", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="科长", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000113, Name="副科长", Code="fkz", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="副科长", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000114, Name="财务", Code="cw", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="财务", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000115, Name="职员", Code="zy", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="职员", TenantId=SqlSugarConst.DefaultTenantId }, + new SysPos{ Id=1300000000116, Name="其他", Code="qt", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="其他", TenantId=SqlSugarConst.DefaultTenantId }, + }; + } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/SeedData/SysRoleMenuSeedData.cs b/yy-admin-master/YY.Admin.Core/SeedData/SysRoleMenuSeedData.cs new file mode 100644 index 0000000..af42c72 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/SeedData/SysRoleMenuSeedData.cs @@ -0,0 +1,32 @@ + + +namespace YY.Admin.Core.SeedData; + +/// +/// 系统角色菜单表种子数据 +/// +public class SysRoleMenuSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + var roleMenuList = new List(); + + var roleList = new SysRoleSeedData().HasData().ToList(); + var menuList = new SysMenuSeedData().HasData().ToList(); + var defaultMenuList = new SysTenantMenuSeedData().HasData().ToList(); + + // 第一个角色拥有全部默认租户菜单 + roleMenuList.AddRange(defaultMenuList.Select(u => new SysRoleMenu { Id = u.MenuId + (roleList[0].Id % 1300000000000), RoleId = roleList[0].Id, MenuId = u.MenuId })); + + // 其他角色权限:工作台、系统管理、个人中心、帮助文档、关于项目 + var otherRoleMenuList = menuList.ToChildList(u => u.Id, u => u.Pid, u => new[] { "工作台", "帮助文档", "关于项目", "个人中心" }.Contains(u.Title)).ToList(); + otherRoleMenuList.Add(menuList.First(u => u.Type == MenuTypeEnum.Dir && u.Title == "系统管理")); + foreach (var role in roleList.Skip(1)) roleMenuList.AddRange(otherRoleMenuList.Select(u => new SysRoleMenu { Id = u.Id + (role.Id % 1300000000000), RoleId = role.Id, MenuId = u.Id })); + + return roleMenuList; + } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/SeedData/SysRoleSeedData.cs b/yy-admin-master/YY.Admin.Core/SeedData/SysRoleSeedData.cs new file mode 100644 index 0000000..873d5a5 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/SeedData/SysRoleSeedData.cs @@ -0,0 +1,25 @@ + + +namespace YY.Admin.Core.SeedData; + +/// +/// 系统角色表种子数据 +/// +public class SysRoleSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return new[] + { + new SysRole{ Id=1300000000101, Name="系统管理员", DataScope=DataScopeEnum.All, Code="sys_admin", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="系统管理员", TenantId=SqlSugarConst.DefaultTenantId }, + new SysRole{ Id=1300000000102, Name="本部门及以下数据", DataScope=DataScopeEnum.DeptChild, Code="sys_deptChild", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="本部门及以下数据", TenantId=SqlSugarConst.DefaultTenantId }, + new SysRole{ Id=1300000000103, Name="本部门数据", DataScope=DataScopeEnum.Dept, Code="sys_dept", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="本部门数据", TenantId=SqlSugarConst.DefaultTenantId }, + new SysRole{ Id=1300000000104, Name="仅本人数据", DataScope=DataScopeEnum.Self, Code="sys_self", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="仅本人数据", TenantId=SqlSugarConst.DefaultTenantId }, + new SysRole{ Id=1300000000105, Name="自定义数据", DataScope=DataScopeEnum.Define, Code="sys_define", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="自定义数据", TenantId=SqlSugarConst.DefaultTenantId }, + }; + } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/SeedData/SysTenantMenuSeedData.cs b/yy-admin-master/YY.Admin.Core/SeedData/SysTenantMenuSeedData.cs new file mode 100644 index 0000000..a90df13 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/SeedData/SysTenantMenuSeedData.cs @@ -0,0 +1,210 @@ + + +using System.Collections; +using System.Reflection; +using YY.Admin.Core.Option; + +namespace YY.Admin.Core.SeedData; + +/// +/// 系统租户菜单表种子数据 +/// +[IgnoreUpdateSeed] +public class SysTenantMenuSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + return new[] + { + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300100000101}, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200010701 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300100601 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200090401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300051101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300050101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300150101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300160601 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300090101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200070501 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300060401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300160401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300400030201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300100401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200010501 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300051201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300050201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300150201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300090601 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300090201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200011101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200010101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300500010101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300500030101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300051301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300050301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300150301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300400010201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200070101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300090501 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200080801 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300090301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200020101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300120101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300020101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200030501 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300020401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300120401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300400030401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200020801 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300051401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300050401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300150401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300090401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300040401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200050501 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200010301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300100201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300051501 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300050501 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300150501 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300160201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300060201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200070301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300600000101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200050201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200020301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200090201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300120301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300020301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200030301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300080201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300020201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300120201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200090301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300051601 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300050601 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300150601 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300070101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300040201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200050301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300010401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300110401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200070201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300400010101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200020201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300100301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300400040101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300050701 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300500040101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200050401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300010501 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300110501 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200080401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200020501 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300120501 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300020501 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200030101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200090101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300050801 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200050101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300010601 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200080501 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300500020101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300400010301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300400020101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200020401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200000101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300100101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300000101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300400040301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300050901 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300010701 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200080601 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200010801 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200010401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300400020201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300400030301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200020701 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300600030101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200060501 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300400040201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300160501 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300060501 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300011001 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300600020101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200080701 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300400020301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200020601 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200010901 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200060401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300100010101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300011101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300010101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300110101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300030401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300130401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200010601 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300400020401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300400030101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300010901 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300080401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200090501 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300040101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300140101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200060701 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200040101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300160701 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300080101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300011201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300010201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300110201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200080101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200070401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300030501 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300130501 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300010801 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300100501 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200060601 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200040201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300500050101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300010301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300110301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200080201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300030201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300130201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200011001 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300500000101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300400000101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300040301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200060101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200040301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300160101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300060101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200060201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300100010201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300080301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200030201 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200080301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300030301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300130301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300130101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300030101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200030401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200040401 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300060301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300300160301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300600010101 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200040501 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId=1300200060301 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId= 1300300040501 }, + new SysTenantMenu(){ TenantId=1300000000001,MenuId = 1300200010201}, + new SysTenantMenu() { TenantId = 1300000000001,MenuId = 1300600040101}, + }; + } + +} + diff --git a/yy-admin-master/YY.Admin.Core/SeedData/SysTenantSeedData.cs b/yy-admin-master/YY.Admin.Core/SeedData/SysTenantSeedData.cs new file mode 100644 index 0000000..387fb75 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/SeedData/SysTenantSeedData.cs @@ -0,0 +1,24 @@ + + +namespace YY.Admin.Core.SeedData; + +/// +/// 系统租户表种子数据 +/// +public class SysTenantSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + var userList = new SysUserSeedData().HasData().ToList(); + var admin = userList.First(u => u.Account == "admin"); + return new[] + { + new SysTenant{ Id=SqlSugarConst.DefaultTenantId, OrgId=SqlSugarConst.DefaultTenantId, UserId=admin.Id, Host="gitee.com", TenantType=TenantTypeEnum.Id, DbType=DbType.Sqlite, + Connection="", ConfigId=SqlSugarConst.MainConfigId, Logo="/upload/logo.png", Title="Admin.NET", ViceTitle="Admin.NET", ViceDesc="站在巨人肩膀上的 .NET 通用权限开发框架", Watermark="Admin.NET", Copyright="Copyright \u00a9 2021-present Admin.NET All rights reserved.", Icp="省ICP备12345678号", IcpUrl="https://beian.miit.gov.cn", Remark="系统默认", CreateTime=DateTime.Parse("2022-02-10 00:00:00") }, + }; + } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/SeedData/SysUserExtOrgSeedData.cs b/yy-admin-master/YY.Admin.Core/SeedData/SysUserExtOrgSeedData.cs new file mode 100644 index 0000000..dc8f306 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/SeedData/SysUserExtOrgSeedData.cs @@ -0,0 +1,31 @@ + + +namespace YY.Admin.Core.SeedData; + +/// +/// 系统用户扩展机构表种子数据 +/// +public class SysUserExtOrgSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + var userList = new SysUserSeedData().HasData().ToList(); + var orgList = new SysOrgSeedData().HasData().ToList(); + var posList = new SysPosSeedData().HasData().ToList(); + var admin = userList.First(u => u.Account == "admin"); + var user3 = userList.First(u => u.Account == "user3"); + var org1 = orgList.First(u => u.Name == "系统默认"); + var org2 = orgList.First(u => u.Name == "开发部"); + var pos1 = posList.First(u => u.Name == "部门经理"); + var pos2 = posList.First(u => u.Name == "主任"); + return new[] + { + new SysUserExtOrg{ Id=1300000000101, UserId=admin.Id, OrgId=org1.Id, PosId=pos1.Id }, + new SysUserExtOrg{ Id=1300000000102, UserId=user3.Id, OrgId=org2.Id, PosId=pos2.Id } + }; + } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/SeedData/SysUserRoleSeedData.cs b/yy-admin-master/YY.Admin.Core/SeedData/SysUserRoleSeedData.cs new file mode 100644 index 0000000..791e0b0 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/SeedData/SysUserRoleSeedData.cs @@ -0,0 +1,26 @@ + + +namespace YY.Admin.Core.SeedData; + +/// +/// 系统用户角色表种子数据 +/// +public class SysUserRoleSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + var userList = new SysUserSeedData().HasData().ToList(); + var roleList = new SysRoleSeedData().HasData().ToList(); + return new[] + { + new SysUserRole{ Id=1300000000101, UserId=userList.First(u => u.Account == "user1").Id, RoleId=roleList.First(u => u.Code == "sys_deptChild").Id }, + new SysUserRole{ Id=1300000000102, UserId=userList.First(u => u.Account == "user2").Id, RoleId=roleList.First(u => u.Code == "sys_dept").Id }, + new SysUserRole{ Id=1300000000103, UserId=userList.First(u => u.Account == "user3").Id, RoleId=roleList.First(u => u.Code == "sys_self").Id }, + new SysUserRole{ Id=1300000000104, UserId=userList.First(u => u.Account == "user4").Id, RoleId=roleList.First(u => u.Code == "sys_define").Id }, + }; + } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/SeedData/SysUserSeedData.cs b/yy-admin-master/YY.Admin.Core/SeedData/SysUserSeedData.cs new file mode 100644 index 0000000..3b33bcb --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/SeedData/SysUserSeedData.cs @@ -0,0 +1,29 @@ + + +namespace YY.Admin.Core.SeedData; + +/// +/// 系统用户表种子数据 +/// +[IgnoreUpdateSeed] +public class SysUserSeedData : ISqlSugarEntitySeedData +{ + /// + /// 种子数据 + /// + /// + public IEnumerable HasData() + { + var encryptPassword = CryptogramUtil.Encrypt(new SysConfigSeedData().HasData().First(u => u.Code == ConfigConst.SysPassword).Value); + var posList = new SysPosSeedData().HasData().ToList(); + return new[] + { + new SysUser{ Id=1300000000101, Account="superadmin", Password=encryptPassword, NickName="超级管理员", RealName="超级管理员", Phone="18012345678", Birthday=DateTime.Parse("2000-01-01"), Sex=GenderEnum.Male, AccountType=AccountTypeEnum.SuperAdmin, Remark="超级管理员", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), TenantId=SqlSugarConst.DefaultTenantId }, + new SysUser{ Id=1300000000111, Account="admin", Password=encryptPassword, NickName="系统管理员", RealName="系统管理员", Phone="18012345677", Birthday=DateTime.Parse("2000-01-01"), Sex=GenderEnum.Male, AccountType=AccountTypeEnum.SysAdmin, Remark="系统管理员", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrgId=SqlSugarConst.DefaultTenantId, PosId=posList[0].Id, TenantId=SqlSugarConst.DefaultTenantId }, + new SysUser{ Id=1300000000112, Account="user1", Password=encryptPassword, NickName="部门主管", RealName="部门主管", Phone="18012345676", Birthday=DateTime.Parse("2000-01-01"), Sex=GenderEnum.Female, AccountType=AccountTypeEnum.NormalUser, Remark="部门主管", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrgId=SqlSugarConst.DefaultTenantId + 1, PosId=posList[1].Id, TenantId=SqlSugarConst.DefaultTenantId }, + new SysUser{ Id=1300000000113, Account="user2", Password=encryptPassword, NickName="部门职员", RealName="部门职员", Phone="18012345675", Birthday=DateTime.Parse("2000-01-01"), Sex=GenderEnum.Female, AccountType=AccountTypeEnum.NormalUser, Remark="部门职员", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrgId=SqlSugarConst.DefaultTenantId + 2, PosId=posList[2].Id, TenantId=SqlSugarConst.DefaultTenantId }, + new SysUser{ Id=1300000000114, Account="user3", Password=encryptPassword, NickName="普通用户", RealName="普通用户", Phone="18012345674", Birthday=DateTime.Parse("2000-01-01"), Sex=GenderEnum.Female, AccountType=AccountTypeEnum.NormalUser, Remark="普通用户", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrgId=SqlSugarConst.DefaultTenantId + 3, PosId=posList[3].Id, TenantId=SqlSugarConst.DefaultTenantId }, + new SysUser{ Id=1300000000115, Account="user4", Password=encryptPassword, NickName="其他", RealName="其他", Phone="18012345673", Birthday=DateTime.Parse("2000-01-01"), Sex=GenderEnum.Female, AccountType=AccountTypeEnum.Member, Remark="会员", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrgId=SqlSugarConst.DefaultTenantId + 4, PosId=posList[4].Id, TenantId=SqlSugarConst.DefaultTenantId }, + }; + } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Session/AppSession.cs b/yy-admin-master/YY.Admin.Core/Session/AppSession.cs new file mode 100644 index 0000000..f93680d --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Session/AppSession.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core.Session +{ + /// + /// 静态会话类 + /// + public static class AppSession + { + private static SysUser? _currentUser; + private static readonly object _lock = new object(); + + public static SysUser? CurrentUser + { + get + { + lock (_lock) + { + return _currentUser; + } + } + set + { + lock (_lock) + { + _currentUser = value; + } + } + } + + // 添加安全访问方法 + public static bool IsAuthenticated => CurrentUser != null; + public static long? UserId => CurrentUser?.Id; + } + +} diff --git a/yy-admin-master/YY.Admin.Core/SqlSugar/ISqlSugarEntitySeedData.cs b/yy-admin-master/YY.Admin.Core/SqlSugar/ISqlSugarEntitySeedData.cs new file mode 100644 index 0000000..7cbdda5 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/SqlSugar/ISqlSugarEntitySeedData.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core.SqlSugar +{ + /// + /// 实体种子数据接口 + /// + /// + public interface ISqlSugarEntitySeedData + where TEntity : class, new() + { + /// + /// 种子数据 + /// + /// + IEnumerable HasData(); + } +} diff --git a/yy-admin-master/YY.Admin.Core/SqlSugar/SqlSugarPagedList.cs b/yy-admin-master/YY.Admin.Core/SqlSugar/SqlSugarPagedList.cs new file mode 100644 index 0000000..8fb4c74 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/SqlSugar/SqlSugarPagedList.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core.SqlSugar +{ + + /// + /// 分页泛型集合 + /// + /// + public class SqlSugarPagedList + { + /// + /// 页码 + /// + public int Page { get; set; } + + /// + /// 页容量 + /// + public int PageSize { get; set; } + + /// + /// 总条数 + /// + public int Total { get; set; } + + /// + /// 总页数 + /// + public int TotalPages { get; set; } + + /// + /// 当前页集合 + /// + public IEnumerable Items { get; set; } + + /// + /// 是否有上一页 + /// + public bool HasPrevPage { get; set; } + + /// + /// 是否有下一页 + /// + public bool HasNextPage { get; set; } + } + /// + /// 分页拓展类 + /// + public static class SqlSugarPagedExtensions + { + /// + /// 分页拓展 + /// + /// 对象 + /// 当前页码,从1开始 + /// 页码容量 + /// 查询结果 Select 表达式 + /// + public static SqlSugarPagedList ToPagedList(this ISugarQueryable query, int pageIndex, int pageSize, + Expression> expression) + { + var total = 0; + var items = query.ToPageList(pageIndex, pageSize, ref total, expression); + return CreateSqlSugarPagedList(items, total, pageIndex, pageSize); + } + + /// + /// 分页拓展 + /// + /// 对象 + /// 当前页码,从1开始 + /// 页码容量 + /// + public static SqlSugarPagedList ToPagedList(this ISugarQueryable query, int pageIndex, int pageSize) + { + var total = 0; + var items = query.ToPageList(pageIndex, pageSize, ref total); + return CreateSqlSugarPagedList(items, total, pageIndex, pageSize); + } + + /// + /// 分页拓展 + /// + /// 对象 + /// 当前页码,从1开始 + /// 页码容量 + /// 查询结果 Select 表达式 + /// + public static async Task> ToPagedListAsync(this ISugarQueryable query, int pageIndex, int pageSize, + Expression> expression) + { + RefAsync total = 0; + var items = await query.ToPageListAsync(pageIndex, pageSize, total, expression); + return CreateSqlSugarPagedList(items, total, pageIndex, pageSize); + } + + /// + /// 分页拓展 + /// + /// 对象 + /// 当前页码,从1开始 + /// 页码容量 + /// + public static async Task> ToPagedListAsync(this ISugarQueryable query, int pageIndex, int pageSize) + { + RefAsync total = 0; + var items = await query.ToPageListAsync(pageIndex, pageSize, total); + return CreateSqlSugarPagedList(items, total, pageIndex, pageSize); + } + + /// + /// 分页拓展 + /// + /// 集合对象 + /// 当前页码,从1开始 + /// 页码容量 + /// + public static SqlSugarPagedList ToPagedList(this IEnumerable list, int pageIndex, int pageSize) + { + var total = list.Count(); + var items = list.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + return CreateSqlSugarPagedList(items, total, pageIndex, pageSize); + } + + /// + /// 创建 对象 + /// + /// + /// 分页内容的对象集合 + /// 总条数 + /// 当前页码,从1开始 + /// 页码容量 + /// + private static SqlSugarPagedList CreateSqlSugarPagedList(IEnumerable items, int total, int pageIndex, int pageSize) + { + var totalPages = pageSize > 0 ? (int)Math.Ceiling(total / (double)pageSize) : 0; + return new SqlSugarPagedList + { + Page = pageIndex, + PageSize = pageSize, + Items = items, + Total = total, + TotalPages = totalPages, + HasNextPage = pageIndex < totalPages, + HasPrevPage = pageIndex - 1 > 0 + }; + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/SqlSugar/SqlSugarSetup.cs b/yy-admin-master/YY.Admin.Core/SqlSugar/SqlSugarSetup.cs new file mode 100644 index 0000000..94ab22f --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/SqlSugar/SqlSugarSetup.cs @@ -0,0 +1,757 @@ +using Dm.util; +using Mapster; +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using NewLife; +using Prism.Ioc; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.Diagnostics; +using System.Linq; +using System.Linq.Dynamic.Core; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Yitter.IdGenerator; +using YY.Admin.Core; +using YY.Admin.Core.Extension; +using YY.Admin.Core.Option; +using YY.Admin.Core.Session; +using DbType = SqlSugar.DbType; + +namespace YY.Admin.Core.SqlSugar +{ + public static class SqlSugarSetup + { + // 多租户实例 + public static ITenant ITenant { get; set; } + + + // 是否正在处理种子数据 + private static bool _isHandlingSeedData = false; + public static void AddSqlSugar(this IContainerRegistry containerRegistry,IConfiguration configuration) + { + //var _logger = Container.Resolve(); + // 直接读取配置 + var dbOptions = configuration.GetSection("DbConnection").Get(); + // 注册雪花Id + var snowIdOpt = new IdGeneratorOptions + { + // 扩展 WorkerId 位数至 12 位 (0-4095) + WorkerIdBitLength = 12, + + // 减少序列号位数保持 64 位总长 + SeqBitLength = 8, + + // 调整时间戳位数 (仍可支持 68 年) + BaseTime = DateTime.Now.AddYears(-30) + }; + YitIdHelper.SetIdGenerator(snowIdOpt); + // 自定义 SqlSugar 雪花ID算法 + SnowFlakeSingle.WorkId = snowIdOpt.WorkerId; + StaticConfig.CustomSnowFlakeFunc = () => + { + return YitIdHelper.NextId(); + }; + // 动态表达式 SqlFunc 支持,https://www.donet5.com/Home/Doc?typeId=2569 + StaticConfig.DynamicExpressionParserType = typeof(DynamicExpressionParser); + StaticConfig.DynamicExpressionParsingConfig = new ParsingConfig + { + CustomTypeProvider = new SqlSugarTypeProvider() + }; + dbOptions!.ConnectionConfigs = SetDbConfig(dbOptions.ConnectionConfigs); + + SqlSugarScope sqlSugar = new(dbOptions.ConnectionConfigs.Cast().ToList(), db => + { + foreach(var config in dbOptions.ConnectionConfigs) + { + var dbProvider = db.GetConnectionScope(config.ConfigId); + SetDbAop(dbProvider, config, dbOptions.EnableConsoleSql, dbOptions.SuperAdminIgnoreIDeletedFilter); + } + }); + ITenant = sqlSugar; + // 注册为单例服务(Prism 方式) + containerRegistry.RegisterInstance(sqlSugar); + foreach (var config in dbOptions.ConnectionConfigs) + { + InitDatabase(sqlSugar, config); + } + } + + /// + /// 配置Aop + /// + /// + /// + /// + public static void SetDbAop(SqlSugarScopeProvider db, DbConnectionConfig config, bool enableConsoleSql, bool superAdminIgnoreIDeletedFilter) + { + // 设置超时时间 + db.Ado.CommandTimeOut = 30; + + // 打印SQL语句 + if (enableConsoleSql) + { + db.Aop.OnLogExecuting = (sql, pars) => + { + //var log = $"【{DateTime.Now}——执行SQL】\r\n{UtilMethods.GetNativeSql(sql, pars)}\r\n"; + var log = $"【{DateTime.Now}——执行SQL】\r\n{UtilMethods.GetSqlString(dbType: config.DbType, sql, pars)}\r\n"; + var originColor = Console.ForegroundColor; + if (sql.StartsWith("SELECT", StringComparison.OrdinalIgnoreCase)) + Console.ForegroundColor = ConsoleColor.Green; + if (sql.StartsWith("UPDATE", StringComparison.OrdinalIgnoreCase) || sql.StartsWith("INSERT", StringComparison.OrdinalIgnoreCase)) + Console.ForegroundColor = ConsoleColor.Yellow; + if (sql.StartsWith("DELETE", StringComparison.OrdinalIgnoreCase)) + Console.ForegroundColor = ConsoleColor.Red; + Debug.WriteLine(log); + Console.ForegroundColor = originColor; + }; + } + db.Aop.OnError = ex => + { + if (ex.Parametres == null) return; + var log = $"【{DateTime.Now}——错误SQL】\r\n{UtilMethods.GetSqlString(config.DbType, ex.Sql, (SugarParameter[])ex.Parametres)}\r\n"; + Debug.WriteLine(log); + // logger.Error(log, ex); + }; + db.Aop.OnLogExecuted = (sql, pars) => + { + // 执行时间超过5秒时 + if (!(db.Ado.SqlExecutionTime.TotalSeconds > 5)) return; + + var fileName = db.Ado.SqlStackTrace.FirstFileName; // 文件名 + var fileLine = db.Ado.SqlStackTrace.FirstLine; // 行号 + var firstMethodName = db.Ado.SqlStackTrace.FirstMethodName; // 方法名 + var log = $"【{DateTime.Now}——超时SQL】\r\n【所在文件名】:{fileName}\r\n【代码行数】:{fileLine}\r\n【方法名】:{firstMethodName}\r\n" + $"【SQL语句】:{UtilMethods.GetNativeSql(sql, pars)}"; + Debug.WriteLine(log); + // logger.Warning(log); + }; + var currentUser = AppSession.CurrentUser; + + var isSuperAdmin = currentUser?.AccountType == AccountTypeEnum.SuperAdmin; + + // 配置假删除过滤器,如果当前用户是超级管理员并且允许忽略软删除过滤器则不会应用 + if (!isSuperAdmin || !superAdminIgnoreIDeletedFilter) + db.QueryFilter.AddTableFilter(u => u.IsDelete == false); + + // 超级管理员排除其他过滤器 + if (isSuperAdmin) return; + + // 配置租户过滤器 + var tenantId = currentUser?.TenantId ?? 0; + if (tenantId > 0) + db.QueryFilter.AddTableFilter(u => u.TenantId == tenantId); + } + + /// + /// 配置连接属性 + /// + /// + /// + public static List SetDbConfig(List configs) + { + foreach (var config in configs) + { + if (config.DbSettings.EnableConnStringEncrypt) + { + config.ConnectionString = CryptogramUtil.Decrypt(config.ConnectionString); + } + + var configureExternalServices = new ConfigureExternalServices + { + EntityNameService = (type, entity) => // 处理表 + { + entity.IsDisabledDelete = true; // 禁止删除非 sqlsugar 创建的列 + // 只处理贴了特性[SugarTable]表 + if (!type.GetCustomAttributes().Any()) + return; + if (config.DbSettings.EnableUnderLine && !entity.DbTableName.Contains('_')) + entity.DbTableName = UtilMethods.ToUnderLine(entity.DbTableName); // 驼峰转下划线 + }, + EntityService = (type, column) => // 处理列 + { + // 只处理贴了特性[SugarColumn]列 + if (!type.GetCustomAttributes().Any()) + { + return; + } + if (new NullabilityInfoContext().Create(type).WriteState is NullabilityState.Nullable) + { + + column.IsNullable = true; + } + if (config.DbSettings.EnableUnderLine && !column.IsIgnore && !column.DbColumnName.Contains('_')) + { + + column.DbColumnName = UtilMethods.ToUnderLine(column.DbColumnName); // 驼峰转下划线 + } + }, + }; + config.ConfigureExternalServices = configureExternalServices; + config.InitKeyType = InitKeyType.Attribute; + config.IsAutoCloseConnection = true; + config.MoreSettings = new ConnMoreSettings + { + IsAutoRemoveDataCache = true, // 启用自动删除缓存,所有增删改会自动调用.RemoveDataCache() + IsAutoDeleteQueryFilter = true, // 启用删除查询过滤器 + IsAutoUpdateQueryFilter = true, // 启用更新查询过滤器 + SqlServerCodeFirstNvarchar = true // 采用Nvarchar + }; + + // 若库类型是人大金仓则默认设置PG模式 + if (config.DbType == DbType.Kdbndp) + config.MoreSettings.DatabaseModel = DbType.PostgreSQL; // 配置PG模式主要是兼容系统表差异 + + // 若库类型是Oracle则默认主键名字和参数名字最大长度 + if (config.DbType == DbType.Oracle) + config.MoreSettings.MaxParameterNameLength = 30; + } + + return configs; + } + + /// + /// 初始化数据库 + /// + /// SqlSugarScope 实例 + /// 数据库连接配置 + private static void InitDatabase(SqlSugarScope db, DbConnectionConfig config) + { + var dbProvider = db.GetConnectionScope(config.ConfigId); + + // 等待数据库连接就绪 + WaitForDatabaseReady(dbProvider); + + // 初始化数据库 + if (config.DbSettings.EnableInitDb) + { + //Log.Information($"初始化数据库 {config.DbType} - {config.ConfigId} - {config.ConnectionString}"); + if (config.DbType != DbType.Oracle) dbProvider.DbMaintenance.CreateDatabase(); + } + + // 初始化表结构 + if (config.TableSettings.EnableInitTable) + { + //Log.Information($"初始化表结构 {config.DbType} - {config.ConfigId}"); + var entityTypes = GetEntityTypesForInit(config); + InitializeTables(dbProvider, entityTypes, config); + } + + // 兼容旧库:曾关闭表初始化或升级前库无 Jeecg 盐列时补齐,避免脱网按 Jeecg 规则验密时读列失败 + EnsureSysUserJeecgPasswordSaltColumn(dbProvider, config); + // 兜底:确保 Jeecg 用户同构表存在(登录页“同步 Jeecg 用户”写入此表) + EnsureJeecgSysUserMirrorTable(dbProvider, config); + // 兜底:确保 Jeecg 数据字典同构表存在(数据字典页面“同步数据字典”写入此表) + EnsureJeecgSysDictItemMirrorTable(dbProvider, config); + //// 初始化视图 + //if (config.DbSettings.EnableInitView) InitView(dbProvider); + // 初始化种子数据 + if (config.SeedSettings.EnableInitSeed) InitSeedData(db, config); + } + + /// + /// 若 sys_user 缺少 jeecg_password_salt,则执行 ALTER 补列(与实体 SysUser.JeecgPasswordSalt 一致) + /// + private static void EnsureSysUserJeecgPasswordSaltColumn(SqlSugarScopeProvider dbProvider, DbConnectionConfig config) + { + try + { + var tableName = dbProvider.EntityMaintenance.GetEntityInfo(typeof(SysUser)).DbTableName; + if (!dbProvider.DbMaintenance.IsAnyTable(tableName, false)) + { + return; + } + + var columns = dbProvider.DbMaintenance.GetColumnInfosByTableName(tableName); + if (columns != null && columns.Any(c => string.Equals(c.DbColumnName, "jeecg_password_salt", StringComparison.OrdinalIgnoreCase))) + { + return; + } + + var sql = config.DbType switch + { + DbType.Sqlite => $"ALTER TABLE {tableName} ADD COLUMN jeecg_password_salt TEXT NULL;", + DbType.MySql => $"ALTER TABLE `{tableName}` ADD COLUMN `jeecg_password_salt` VARCHAR(64) NULL COMMENT 'Jeecg密码盐';", + DbType.PostgreSQL => $"ALTER TABLE \"{tableName}\" ADD COLUMN IF NOT EXISTS jeecg_password_salt VARCHAR(64) NULL;", + DbType.SqlServer => $"ALTER TABLE [{tableName}] ADD [jeecg_password_salt] NVARCHAR(64) NULL;", + DbType.Kdbndp => $"ALTER TABLE \"{tableName}\" ADD COLUMN IF NOT EXISTS jeecg_password_salt VARCHAR(64) NULL;", + DbType.Dm => $"ALTER TABLE {tableName} ADD jeecg_password_salt VARCHAR(64) NULL;", + _ => (string?)null + }; + + if (string.IsNullOrEmpty(sql)) + { + return; + } + + Retry(() => dbProvider.Ado.ExecuteCommand(sql), maxRetry: 3, retryIntervalMs: 1000); + } + catch + { + // 无权限、从库只读等场景不阻断启动 + } + } + + /// + /// 若缺少 Jeecg 同构用户表,则启动时强制创建。 + /// 说明:用于规避某些环境 AppDomain 扫描不到新实体导致 CodeFirst 漏建的问题。 + /// + private static void EnsureJeecgSysUserMirrorTable(SqlSugarScopeProvider dbProvider, DbConnectionConfig config) + { + try + { + var tableName = dbProvider.EntityMaintenance.GetEntityInfo(typeof(JeecgSysUser)).DbTableName; + if (dbProvider.DbMaintenance.IsAnyTable(tableName, false)) + { + return; + } + + Retry(() => + { + dbProvider.CodeFirst.InitTables(typeof(JeecgSysUser)); + }, maxRetry: 3, retryIntervalMs: 1000); + + // CodeFirst 仍未创建时,针对 SQLite 做 SQL 级兜底,确保表可见 + if (!dbProvider.DbMaintenance.IsAnyTable(tableName, false) && config.DbType == DbType.Sqlite) + { + const string createSql = @" +CREATE TABLE IF NOT EXISTS jeecg_sys_user ( + id TEXT PRIMARY KEY NOT NULL, + username TEXT NULL, + realname TEXT NULL, + password TEXT NULL, + salt TEXT NULL, + avatar TEXT NULL, + birthday TEXT NULL, + sex INTEGER NULL, + email TEXT NULL, + phone TEXT NULL, + org_code TEXT NULL, + login_tenant_id INTEGER NULL, + status INTEGER NULL, + del_flag INTEGER NULL, + work_no TEXT NULL, + telephone TEXT NULL, + create_by TEXT NULL, + create_time TEXT NULL, + update_by TEXT NULL, + update_time TEXT NULL, + activiti_sync INTEGER NULL, + user_identity INTEGER NULL, + depart_ids TEXT NULL, + client_id TEXT NULL, + bpm_status TEXT NULL, + sign TEXT NULL, + sign_enable INTEGER NULL, + main_dep_post_id TEXT NULL, + position_type TEXT NULL, + last_pwd_update_time TEXT NULL, + sort INTEGER NULL, + iz_hide_contact TEXT NULL +);"; + Retry(() => dbProvider.Ado.ExecuteCommand(createSql), maxRetry: 3, retryIntervalMs: 1000); + } + } + catch + { + // 兜底逻辑不阻断启动 + } + } + + /// + /// 若缺少 Jeecg 同构数据字典项表,则启动时强制创建。 + /// + private static void EnsureJeecgSysDictItemMirrorTable(SqlSugarScopeProvider dbProvider, DbConnectionConfig config) + { + try + { + var tableName = dbProvider.EntityMaintenance.GetEntityInfo(typeof(JeecgSysDictItem)).DbTableName; + if (dbProvider.DbMaintenance.IsAnyTable(tableName, false)) + { + return; + } + + Retry(() => + { + dbProvider.CodeFirst.InitTables(typeof(JeecgSysDictItem)); + }, maxRetry: 3, retryIntervalMs: 1000); + + if (!dbProvider.DbMaintenance.IsAnyTable(tableName, false) && config.DbType == DbType.Sqlite) + { + const string createSql = @" +CREATE TABLE IF NOT EXISTS jeecg_sys_dict_item ( + id TEXT PRIMARY KEY NOT NULL, + dict_id TEXT NULL, + dict_name TEXT NULL, + dict_code TEXT NULL, + dict_type INTEGER NULL, + dict_description TEXT NULL, + item_text TEXT NULL, + item_value TEXT NULL, + item_description TEXT NULL, + sort_order INTEGER NULL, + status INTEGER NULL, + item_color TEXT NULL, + create_by TEXT NULL, + create_time TEXT NULL, + update_by TEXT NULL, + update_time TEXT NULL +);"; + Retry(() => dbProvider.Ado.ExecuteCommand(createSql), maxRetry: 3, retryIntervalMs: 1000); + } + } + catch + { + // 兜底逻辑不阻断启动 + } + } + + /// + /// 初始化种子数据 + /// + /// SqlSugarScope 实例 + /// 数据库连接配置 + private static void InitSeedData(SqlSugarScope db, DbConnectionConfig config) + { + var dbProvider = db.GetConnectionScope(config.ConfigId); + _isHandlingSeedData = true; + + // Log.Information($"初始化种子数据 {config.DbType} - {config.ConfigId}"); + var seedDataTypes = GetSeedDataTypes(config); + + int count = 0, sum = seedDataTypes.Count; + var tasks = seedDataTypes.Select(seedType => Task.Run(() => + { + var entityType = seedType.GetInterfaces().First().GetGenericArguments().First(); + if (!IsEntityForConfig(entityType, config)) return; + + var seedData = GetSeedData(seedType); + if (seedData == null) return; + + AdjustSeedDataIds(seedData, config); + InsertOrUpdateSeedData(dbProvider, seedType, entityType, seedData, config, ref count, sum); + })); + + Task.WhenAll(tasks).GetAwaiter().GetResult(); + _isHandlingSeedData = false; + } + + /// + /// 获取需要初始化的实体类型 + /// + /// 数据库连接配置 + /// 实体类型列表 + /// + /// 获取需要初始化的实体类型 + /// + /// 数据库连接配置 + /// 实体类型列表 + private static List GetEntityTypesForInit(DbConnectionConfig config) + { + // 获取当前应用程序域中的所有程序集 + var assemblies = AppDomain.CurrentDomain.GetAssemblies(); + + // 收集所有符合条件的类型 + var allTypes = new List(); + foreach (var assembly in assemblies) + { + try + { + // 跳过动态程序集(通常不包含用户定义的实体类) + if (assembly.IsDynamic) continue; + + // 获取程序集中定义的所有类型 + var types = assembly.GetTypes(); + allTypes.AddRange(types); + } + catch (ReflectionTypeLoadException ex) + { + // 处理部分类型加载失败的情况 + var loadedTypes = ex.Types.Where(t => t != null); + allTypes.AddRange(loadedTypes!); + } + catch + { + // 忽略无法加载的程序集 + } + } + + // 过滤类型 + return allTypes + .Where(u => u != null) + .Where(u => u.IsClass && !u.IsAbstract && !u.IsInterface) + .Where(u => u.IsDefined(typeof(SugarTable), false)) + .Where(u => !u.IsDefined(typeof(IgnoreTableAttribute), false)) + .WhereIF(config.TableSettings.EnableIncreTable, u => u.IsDefined(typeof(IncreTableAttribute), false)) + .Where(u => IsEntityForConfig(u, config)) + .ToList(); + } + + /// + /// 获取种子数据类型 + /// + /// 数据库连接配置 + /// 种子数据类型列表 + private static List GetSeedDataTypes(DbConnectionConfig config) + { + // 获取所有程序集及其类型 + var allTypes = AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !a.IsDynamic) // 跳过动态程序集 + .SelectMany(assembly => + { + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + // 处理部分类型加载失败的情况 + return ex.Types?.Where(t => t != null) ?? Enumerable.Empty(); + } + catch + { + // 忽略无法加载的程序集 + return Enumerable.Empty(); + } + }) + .Where(t => t != null) + .ToList(); + + // 过滤种子数据类型 + var seedTypes = allTypes + .Where(u => u!.IsClass && !u.IsAbstract && !u.IsInterface) + .Where(u => u!.HasImplementedRawGeneric(typeof(ISqlSugarEntitySeedData<>))) + .WhereIF(config.SeedSettings.EnableIncreSeed, + u => u!.IsDefined(typeof(IncreSeedAttribute), false)) + .OrderBy(u => + { + var seedAttrs = u!.GetCustomAttributes(typeof(SeedDataAttribute), false); + return seedAttrs.Length > 0 ? ((SeedDataAttribute)seedAttrs[0]).Order : 0; + }) + .ToList(); + + return seedTypes!; + } + + /// + /// 判断实体是否属于当前配置 + /// + /// 实体类型 + /// 数据库连接配置 + /// 是否属于当前配置 + private static bool IsEntityForConfig(Type entityType, DbConnectionConfig config) + { + switch (config.ConfigId.ToString()) + { + case SqlSugarConst.MainConfigId: + return entityType.GetCustomAttributes().Any() || + (!entityType.GetCustomAttributes().Any() && + !entityType.GetCustomAttributes().Any()); + + case SqlSugarConst.LogConfigId: + return entityType.GetCustomAttributes().Any(); + + default: + { + var tenantAttribute = entityType.GetCustomAttribute(); + return tenantAttribute != null && tenantAttribute.configId.ToString() == config.ConfigId.ToString(); + } + } + } + + /// + /// 初始化表结构 + /// + /// SqlSugarScopeProvider 实例 + /// 实体类型列表 + /// 数据库连接配置 + private static void InitializeTables(SqlSugarScopeProvider dbProvider, List entityTypes, DbConnectionConfig config) + { + int count = 0, sum = entityTypes.Count; + var tasks = entityTypes.Select(entityType => Task.Run(() => + { + Console.WriteLine($"初始化表结构 {entityType.FullName,-64} ({config.ConfigId} - {Interlocked.Increment(ref count):D003}/{sum:D003})"); + UpdateNullableColumns(dbProvider, entityType); + InitializeTable(dbProvider, entityType); + })); + + Task.WhenAll(tasks).GetAwaiter().GetResult(); + } + + /// + /// 更新表中不存在于实体的字段为可空 + /// + /// SqlSugarScopeProvider 实例 + /// 实体类型 + private static void UpdateNullableColumns(SqlSugarScopeProvider dbProvider, Type entityType) + { + var entityInfo = dbProvider.EntityMaintenance.GetEntityInfo(entityType); + var dbColumns = dbProvider.DbMaintenance.GetColumnInfosByTableName(entityInfo.DbTableName) ?? new List(); + + foreach (var dbColumn in dbColumns.Where(c => !c.IsPrimarykey && entityInfo.Columns.All(u => u.DbColumnName != c.DbColumnName))) + { + dbColumn.IsNullable = true; + Retry(() => + { + dbProvider.DbMaintenance.UpdateColumn(entityInfo.DbTableName, dbColumn); + }, maxRetry: 3, retryIntervalMs: 1000); + } + } + + /// + /// 等待数据库就绪 + /// + /// + private static void WaitForDatabaseReady(SqlSugarScopeProvider db) + { + do + { + try + { + if (db.Ado.Connection.State != ConnectionState.Open) + db.Ado.Connection.Open(); + + // 如果连接成功,直接返回 + //logger.Information("数据库连接成功。"); + return; + } + catch (Exception ex) + { + // logger.Warning($"数据库尚未就绪,等待中... 错误:{ex.Message}"); + Thread.Sleep(1000); + } + } while (true); + } + + /// + /// 简单的重试机制 + /// + /// + /// + /// + private static void Retry(Action action, int maxRetry, int retryIntervalMs) + { + int attempt = 0; + while (true) + { + try + { + action(); + return; + } + catch (SqliteException ex) when (ex.SqliteErrorCode == 5) // SQLITE_BUSY + { + if (++attempt >= maxRetry) + { + // logger.Error($"简单的重试机制:{ex.Message}"); throw; + } + //logger.Information($"数据库忙,正在重试... (尝试 {attempt}/{maxRetry})"); + Thread.Sleep(retryIntervalMs); + } + } + } + + /// + /// 获取种子数据 + /// + /// 种子数据类型 + /// 种子数据列表 + private static IEnumerable GetSeedData(Type seedType) + { + var instance = Activator.CreateInstance(seedType); + var hasDataMethod = seedType.GetMethod("HasData"); + return ((IEnumerable)hasDataMethod?.Invoke(instance, null)!)?.Cast()!; + } + /// + /// 插入或更新种子数据 + /// + /// SqlSugarScopeProvider 实例 + /// 种子数据类型 + /// 实体类型 + /// 种子数据列表 + /// 数据库连接配置 + /// 当前处理的数量 + /// 总数量 + private static void InsertOrUpdateSeedData(SqlSugarScopeProvider dbProvider, Type seedType, Type entityType, IEnumerable seedData, DbConnectionConfig config, ref int count, int sum) + { + var entityInfo = dbProvider.EntityMaintenance.GetEntityInfo(entityType); + var dataList = seedData.ToList(); + + if (entityType.GetCustomAttribute(true) != null) + { + var initMethod = seedType.GetMethod("Init"); + initMethod?.Invoke(Activator.CreateInstance(seedType), new object[] { dbProvider }); + } + else + { + int updateCount = 0, insertCount = 0; + if (entityInfo.Columns.Any(u => u.IsPrimarykey)) + { + var storage = dbProvider.StorageableByObject(dataList).ToStorage(); + if (seedType.GetCustomAttribute() == null) + { + updateCount = storage.AsUpdateable + .IgnoreColumns(entityInfo.Columns + .Where(u => u.PropertyInfo.GetCustomAttribute() != null) + .Select(u => u.PropertyName).ToArray()) + .ExecuteCommand(); + } + insertCount = storage.AsInsertable.ExecuteCommand(); + } + else + { + if (!dbProvider.Queryable(entityInfo.DbTableName, entityInfo.DbTableName).Any()) + { + insertCount = dataList.Count; + dbProvider.InsertableByObject(dataList).ExecuteCommand(); + } + } + Console.WriteLine($"添加数据 {entityInfo.DbTableName,-32} ({config.ConfigId} - {Interlocked.Increment(ref count):D003}/{sum:D003},数据量:{dataList.Count:D003},插入 {insertCount:D003} 条记录,修改 {updateCount:D003} 条记录)"); + } + } + + /// + /// 调整种子数据的 ID + /// + /// 种子数据列表 + /// 数据库连接配置 + private static void AdjustSeedDataIds(IEnumerable seedData, DbConnectionConfig config) + { + var seedId = config.ConfigId.ToLong(); + foreach (var data in seedData) + { + var idProperty = data.GetType().GetProperty(nameof(EntityBaseId.Id)); + if (idProperty == null || idProperty.PropertyType != typeof(Int64)) continue; + + var idValue = idProperty.GetValue(data); + if (idValue == null || idValue.ToString() == "0" || string.IsNullOrWhiteSpace(idValue.ToString())) + { + idProperty.SetValue(data, ++seedId); + } + } + } + + /// + /// 初始化表 + /// + /// SqlSugarScopeProvider 实例 + /// 实体类型 + private static void InitializeTable(SqlSugarScopeProvider dbProvider, Type entityType) + { + Retry(() => + { + if (entityType.GetCustomAttribute() == null) + { + dbProvider.CodeFirst.InitTables(entityType); + } + else + { + dbProvider.CodeFirst.SplitTables().InitTables(entityType); + } + }, maxRetry: 3, retryIntervalMs: 1000); + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/SqlSugar/SqlSugarTypeProvider.cs b/yy-admin-master/YY.Admin.Core/SqlSugar/SqlSugarTypeProvider.cs new file mode 100644 index 0000000..d24ef1b --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/SqlSugar/SqlSugarTypeProvider.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Dynamic.Core.CustomTypeProviders; +using System.Linq.Dynamic.Core; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core.SqlSugar +{ + public class SqlSugarTypeProvider : DefaultDynamicLinqCustomTypeProvider + { + public SqlSugarTypeProvider(bool cacheCustomTypes = true) : base(ParsingConfig.Default, cacheCustomTypes) + { + } + + public override HashSet GetCustomTypes() + { + var customTypes = base.GetCustomTypes(); + customTypes.Add(typeof(SqlFunc)); + return customTypes; + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Tools/Interop/Handle/BitmapHandle.cs b/yy-admin-master/YY.Admin.Core/Tools/Interop/Handle/BitmapHandle.cs new file mode 100644 index 0000000..e89bd73 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Tools/Interop/Handle/BitmapHandle.cs @@ -0,0 +1,43 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Security; + +namespace YY.Admin.Tools.Interop; + +[SuppressMessage("ReSharper", "UnusedMember.Local")] +internal sealed class BitmapHandle : WpfSafeHandle +{ + [SecurityCritical] + private BitmapHandle() : this(true) + { + //请不要删除此构造函数,否则当使用自定义ico文件时会报错 + } + + [SecurityCritical] + private BitmapHandle(bool ownsHandle) : base(ownsHandle, CommonHandles.GDI) + { + } + + [SecurityCritical] + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + protected override bool ReleaseHandle() + { + return InteropMethods.DeleteObject(handle); + } + + [SecurityCritical] + internal HandleRef MakeHandleRef(object obj) + { + return new(obj, handle); + } + + [SecurityCritical] + internal static BitmapHandle CreateFromHandle(IntPtr hbitmap, bool ownsHandle = true) + { + return new(ownsHandle) + { + handle = hbitmap, + }; + } +} diff --git a/yy-admin-master/YY.Admin.Core/Tools/Interop/Handle/CommonHandles.cs b/yy-admin-master/YY.Admin.Core/Tools/Interop/Handle/CommonHandles.cs new file mode 100644 index 0000000..82ce608 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Tools/Interop/Handle/CommonHandles.cs @@ -0,0 +1,15 @@ +using System.Diagnostics.CodeAnalysis; + +namespace YY.Admin.Tools.Interop; + +[SuppressMessage("ReSharper", "InconsistentNaming")] +internal static class CommonHandles +{ + public static readonly int Icon = HandleCollector.RegisterType(nameof(Icon), 20, 500); + + public static readonly int HDC = HandleCollector.RegisterType(nameof(HDC), 100, 2); + + public static readonly int GDI = HandleCollector.RegisterType(nameof(GDI), 50, 500); + + public static readonly int Kernel = HandleCollector.RegisterType(nameof(Kernel), 0, 1000); +} diff --git a/yy-admin-master/YY.Admin.Core/Tools/Interop/Handle/HandleCollector.cs b/yy-admin-master/YY.Admin.Core/Tools/Interop/Handle/HandleCollector.cs new file mode 100644 index 0000000..7a81dfe --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Tools/Interop/Handle/HandleCollector.cs @@ -0,0 +1,131 @@ +// reference from https://referencesource.microsoft.com/#WindowsBase/Shared/MS/Win32/HandleCollector.cs,d0f99220d8e1b708 + +using System; +using System.Runtime.InteropServices; + +namespace YY.Admin.Tools.Interop; + +internal static class HandleCollector +{ + private static HandleType[] HandleTypes; + private static int HandleTypeCount; + + private static readonly object HandleMutex = new(); + + internal static IntPtr Add(IntPtr handle, int type) + { + HandleTypes[type - 1].Add(); + return handle; + } + + [System.Security.SecuritySafeCritical] + internal static SafeHandle Add(SafeHandle handle, int type) + { + HandleTypes[type - 1].Add(); + return handle; + } + + internal static void Add(int type) + { + HandleTypes[type - 1].Add(); + } + + internal static int RegisterType(string typeName, int expense, int initialThreshold) + { + lock (HandleMutex) + { + if (HandleTypeCount == 0 || HandleTypeCount == HandleTypes.Length) + { + HandleType[] newTypes = new HandleType[HandleTypeCount + 10]; + if (HandleTypes != null) + { + Array.Copy(HandleTypes, 0, newTypes, 0, HandleTypeCount); + } + HandleTypes = newTypes; + } + + HandleTypes[HandleTypeCount++] = new HandleType(expense, initialThreshold); + return HandleTypeCount; + } + } + + internal static IntPtr Remove(IntPtr handle, int type) + { + HandleTypes[type - 1].Remove(); + return handle; + } + + [System.Security.SecuritySafeCritical] + internal static SafeHandle Remove(SafeHandle handle, int type) + { + HandleTypes[type - 1].Remove(); + return handle; + } + + internal static void Remove(int type) + { + HandleTypes[type - 1].Remove(); + } + + private class HandleType + { + private readonly int _initialThreshHold; + private int _threshHold; + private int _handleCount; + private readonly int _deltaPercent; + + internal HandleType(int expense, int initialThreshHold) + { + _initialThreshHold = initialThreshHold; + _threshHold = initialThreshHold; + _deltaPercent = 100 - expense; + } + + internal void Add() + { + lock (this) + { + _handleCount++; + var performCollect = NeedCollection(); + + if (!performCollect) + { + return; + } + } + + GC.Collect(); + + var sleep = (100 - _deltaPercent) / 4; + System.Threading.Thread.Sleep(sleep); + } + + private bool NeedCollection() + { + + if (_handleCount > _threshHold) + { + _threshHold = _handleCount + _handleCount * _deltaPercent / 100; + return true; + } + + var oldThreshHold = 100 * _threshHold / (100 + _deltaPercent); + if (oldThreshHold >= _initialThreshHold && _handleCount < (int) (oldThreshHold * .9F)) + { + _threshHold = oldThreshHold; + } + + return false; + } + + internal void Remove() + { + lock (this) + { + _handleCount--; + + _handleCount = Math.Max(0, _handleCount); + } + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Tools/Interop/Handle/IconHandle.cs b/yy-admin-master/YY.Admin.Core/Tools/Interop/Handle/IconHandle.cs new file mode 100644 index 0000000..872073e --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Tools/Interop/Handle/IconHandle.cs @@ -0,0 +1,31 @@ +using System.Runtime.ConstrainedExecution; +using System.Security; + +namespace YY.Admin.Tools.Interop; + +internal sealed class IconHandle : WpfSafeHandle +{ + [SecurityCritical] + private IconHandle() : base(true, CommonHandles.Icon) + { + } + + [SecurityCritical] + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] + protected override bool ReleaseHandle() + { + return InteropMethods.DestroyIcon(handle); + } + + [SecurityCritical, SecuritySafeCritical] + internal static IconHandle GetInvalidIcon() + { + return new(); + } + + [SecurityCritical] + internal IntPtr CriticalGetHandle() + { + return handle; + } +} diff --git a/yy-admin-master/YY.Admin.Core/Tools/Interop/Handle/SafeFileMappingHandle.cs b/yy-admin-master/YY.Admin.Core/Tools/Interop/Handle/SafeFileMappingHandle.cs new file mode 100644 index 0000000..5a2279e --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Tools/Interop/Handle/SafeFileMappingHandle.cs @@ -0,0 +1,48 @@ +using Microsoft.Win32.SafeHandles; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Permissions; + +namespace YY.Admin.Tools.Interop; + +internal sealed class SafeFileMappingHandle : SafeHandleZeroOrMinusOneIsInvalid +{ + [SecurityCritical] + internal SafeFileMappingHandle(IntPtr handle) : base(false) + { + SetHandle(handle); + } + + [SecurityCritical, SecuritySafeCritical] + internal SafeFileMappingHandle() : base(true) + { + } + + public override bool IsInvalid + { + [SecurityCritical, SecuritySafeCritical] + get => handle == IntPtr.Zero; + } + + [SecurityCritical, SecuritySafeCritical] + protected override bool ReleaseHandle() + { + new SecurityPermission(SecurityPermissionFlag.UnmanagedCode).Assert(); + try + { + return CloseHandleNoThrow(new HandleRef(null, handle)); + } + finally + { + CodeAccessPermission.RevertAssert(); + } + } + + [SecurityCritical] + public static bool CloseHandleNoThrow(HandleRef handle) + { + HandleCollector.Remove((IntPtr) handle, CommonHandles.Kernel); + var result = InteropMethods.IntCloseHandle(handle); + return result; + } +} diff --git a/yy-admin-master/YY.Admin.Core/Tools/Interop/Handle/WpfSafeHandle.cs b/yy-admin-master/YY.Admin.Core/Tools/Interop/Handle/WpfSafeHandle.cs new file mode 100644 index 0000000..eb43fc7 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Tools/Interop/Handle/WpfSafeHandle.cs @@ -0,0 +1,23 @@ +using System.Security; +using Microsoft.Win32.SafeHandles; + +namespace YY.Admin.Tools.Interop; + +internal abstract class WpfSafeHandle : SafeHandleZeroOrMinusOneIsInvalid +{ + private readonly int _collectorId; + + [SecurityCritical] + protected WpfSafeHandle(bool ownsHandle, int collectorId) : base(ownsHandle) + { + HandleCollector.Add(collectorId); + _collectorId = collectorId; + } + + [SecurityCritical, SecuritySafeCritical] + protected override void Dispose(bool disposing) + { + HandleCollector.Remove(_collectorId); + base.Dispose(disposing); + } +} diff --git a/yy-admin-master/YY.Admin.Core/Tools/Interop/InteropMethods.cs b/yy-admin-master/YY.Admin.Core/Tools/Interop/InteropMethods.cs new file mode 100644 index 0000000..5d1998c --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Tools/Interop/InteropMethods.cs @@ -0,0 +1,747 @@ +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Security; +using System.Security.Permissions; +using System.Text; + +namespace YY.Admin.Tools.Interop; + +internal class InteropMethods +{ + #region common + + internal const int E_FAIL = unchecked((int) 0x80004005); + + internal static readonly IntPtr HRGN_NONE = new(-1); + + [DllImport(InteropValues.ExternDll.User32, CharSet = CharSet.Auto)] + [ResourceExposure(ResourceScope.None)] + internal static extern int RegisterWindowMessage(string msg); + + [DllImport(InteropValues.ExternDll.Kernel32, SetLastError = true, CharSet = CharSet.Auto)] + internal static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, out InteropValues.TBBUTTON lpBuffer, + int dwSize, out int lpNumberOfBytesRead); + + [DllImport(InteropValues.ExternDll.Kernel32, SetLastError = true)] + internal static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, out InteropValues.RECT lpBuffer, + int dwSize, out int lpNumberOfBytesRead); + + [DllImport(InteropValues.ExternDll.Kernel32, SetLastError = true)] + internal static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, out InteropValues.TRAYDATA lpBuffer, + int dwSize, out int lpNumberOfBytesRead); + + [DllImport(InteropValues.ExternDll.User32, CharSet = CharSet.Auto)] + internal static extern uint SendMessage(IntPtr hWnd, uint Msg, uint wParam, IntPtr lParam); + + [DllImport(InteropValues.ExternDll.User32, SetLastError = true, CharSet = CharSet.Auto)] + internal static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + + [DllImport(InteropValues.ExternDll.User32, SetLastError = true, CharSet = CharSet.Auto)] + internal static extern bool AttachThreadInput(in uint currentForegroundWindowThreadId, + in uint thisWindowThreadId, bool isAttach); + + [DllImport(InteropValues.ExternDll.User32, SetLastError = true, CharSet = CharSet.Auto)] + internal static extern IntPtr GetForegroundWindow(); + + [DllImport(InteropValues.ExternDll.Kernel32, SetLastError = true, CharSet = CharSet.Auto)] + internal static extern IntPtr OpenProcess(InteropValues.ProcessAccess dwDesiredAccess, + [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwProcessId); + + [DllImport(InteropValues.ExternDll.Kernel32, SetLastError = true, CharSet = CharSet.Auto)] + internal static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, int dwSize, + InteropValues.AllocationType flAllocationType, InteropValues.MemoryProtection flProtect); + + [DllImport(InteropValues.ExternDll.Kernel32, SetLastError = true, CharSet = CharSet.Auto)] + internal static extern int CloseHandle(IntPtr hObject); + + [DllImport(InteropValues.ExternDll.Kernel32, SetLastError = true, CharSet = CharSet.Auto)] + internal static extern bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddress, int dwSize, InteropValues.FreeType dwFreeType); + + [DllImport(InteropValues.ExternDll.User32, SetLastError = true)] + internal static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + + [DllImport(InteropValues.ExternDll.User32, SetLastError = true)] + internal static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, + string lpszWindow); + + [DllImport(InteropValues.ExternDll.User32)] + internal static extern bool GetWindowRect(IntPtr hwnd, out InteropValues.RECT lpRect); + + [DllImport(InteropValues.ExternDll.User32, CharSet = CharSet.Auto)] + internal static extern bool GetCursorPos(out InteropValues.POINT pt); + + [DllImport(InteropValues.ExternDll.User32)] + internal static extern IntPtr GetDesktopWindow(); + + [DllImport(InteropValues.ExternDll.User32, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool AddClipboardFormatListener(IntPtr hwnd); + + [DllImport(InteropValues.ExternDll.User32, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool RemoveClipboardFormatListener(IntPtr hwnd); + + [DllImport(InteropValues.ExternDll.User32)] + internal static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert); + + [DllImport(InteropValues.ExternDll.User32)] + internal static extern bool EnableMenuItem(IntPtr hMenu, int UIDEnabledItem, int uEnable); + + [DllImport(InteropValues.ExternDll.User32)] + internal static extern bool InsertMenu(IntPtr hMenu, int wPosition, int wFlags, int wIDNewItem, string lpNewItem); + + [DllImport(InteropValues.ExternDll.User32, ExactSpelling = true, EntryPoint = "DestroyMenu", CharSet = CharSet.Auto)] + [ResourceExposure(ResourceScope.None)] + internal static extern bool IntDestroyMenu(HandleRef hMenu); + + [SecurityCritical] + [SuppressUnmanagedCodeSecurity] + [DllImport(InteropValues.ExternDll.User32, SetLastError = true, ExactSpelling = true, EntryPoint = nameof(GetDC), + CharSet = CharSet.Auto)] + internal static extern IntPtr IntGetDC(HandleRef hWnd); + + [SecurityCritical] + internal static IntPtr GetDC(HandleRef hWnd) + { + var hDc = IntGetDC(hWnd); + if (hDc == IntPtr.Zero) throw new Win32Exception(); + + return HandleCollector.Add(hDc, CommonHandles.HDC); + } + + [SecurityCritical] + [SuppressUnmanagedCodeSecurity] + [DllImport(InteropValues.ExternDll.User32, ExactSpelling = true, EntryPoint = nameof(ReleaseDC), CharSet = CharSet.Auto)] + internal static extern int IntReleaseDC(HandleRef hWnd, HandleRef hDC); + + [SecurityCritical] + internal static int ReleaseDC(HandleRef hWnd, HandleRef hDC) + { + HandleCollector.Remove((IntPtr) hDC, CommonHandles.HDC); + return IntReleaseDC(hWnd, hDC); + } + + [SecurityCritical] + [SuppressUnmanagedCodeSecurity] + [DllImport(InteropValues.ExternDll.Gdi32, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Auto)] + internal static extern int GetDeviceCaps(HandleRef hDC, int nIndex); + + [SecurityCritical] + [SuppressUnmanagedCodeSecurity] + [DllImport(InteropValues.ExternDll.User32)] + internal static extern int GetSystemMetrics(InteropValues.SM nIndex); + + [SecurityCritical] + [SuppressUnmanagedCodeSecurity] + [DllImport(InteropValues.ExternDll.User32, EntryPoint = nameof(DestroyIcon), CharSet = CharSet.Auto, SetLastError = true)] + private static extern bool IntDestroyIcon(IntPtr hIcon); + + [SecurityCritical] + internal static bool DestroyIcon(IntPtr hIcon) + { + var result = IntDestroyIcon(hIcon); + return result; + } + + [SecurityCritical] + [SuppressUnmanagedCodeSecurity] + [DllImport(InteropValues.ExternDll.Gdi32, EntryPoint = nameof(DeleteObject), CharSet = CharSet.Auto, SetLastError = true)] + private static extern bool IntDeleteObject(IntPtr hObject); + + [SecurityCritical] + internal static bool DeleteObject(IntPtr hObject) + { + var result = IntDeleteObject(hObject); + return result; + } + + [SecurityCritical] + internal static BitmapHandle CreateDIBSection(HandleRef hdc, ref InteropValues.BITMAPINFO bitmapInfo, int iUsage, + ref IntPtr ppvBits, SafeFileMappingHandle hSection, int dwOffset) + { + hSection ??= new SafeFileMappingHandle(IntPtr.Zero); + + var hBitmap = PrivateCreateDIBSection(hdc, ref bitmapInfo, iUsage, ref ppvBits, hSection, dwOffset); + return hBitmap; + } + + [DllImport(InteropValues.ExternDll.Kernel32, EntryPoint = "CloseHandle", CharSet = CharSet.Auto, SetLastError = true)] + internal static extern bool IntCloseHandle(HandleRef handle); + + [SecurityCritical] + [SuppressUnmanagedCodeSecurity] + [DllImport(InteropValues.ExternDll.Gdi32, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Auto, + EntryPoint = nameof(CreateDIBSection))] + private static extern BitmapHandle PrivateCreateDIBSection(HandleRef hdc, ref InteropValues.BITMAPINFO bitmapInfo, int iUsage, + ref IntPtr ppvBits, SafeFileMappingHandle hSection, int dwOffset); + + [SecurityCritical] + [SuppressUnmanagedCodeSecurity] + [DllImport(InteropValues.ExternDll.User32, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Auto, + EntryPoint = nameof(CreateIconIndirect))] + private static extern IconHandle PrivateCreateIconIndirect([In] [MarshalAs(UnmanagedType.LPStruct)] + InteropValues.ICONINFO iconInfo); + + [SecurityCritical] + internal static IconHandle CreateIconIndirect([In] [MarshalAs(UnmanagedType.LPStruct)] + InteropValues.ICONINFO iconInfo) + { + var hIcon = PrivateCreateIconIndirect(iconInfo); + return hIcon; + } + + [SecurityCritical] + [SuppressUnmanagedCodeSecurity] + [DllImport(InteropValues.ExternDll.Gdi32, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Auto, + EntryPoint = nameof(CreateBitmap))] + private static extern BitmapHandle PrivateCreateBitmap(int width, int height, int planes, int bitsPerPixel, + byte[] lpvBits); + + [SecurityCritical] + internal static BitmapHandle CreateBitmap(int width, int height, int planes, int bitsPerPixel, byte[] lpvBits) + { + var hBitmap = PrivateCreateBitmap(width, height, planes, bitsPerPixel, lpvBits); + return hBitmap; + } + + [SecurityCritical] + [SuppressUnmanagedCodeSecurity] + [DllImport(InteropValues.ExternDll.Kernel32, EntryPoint = "GetModuleFileName", CharSet = CharSet.Unicode, + SetLastError = true)] + private static extern int IntGetModuleFileName(HandleRef hModule, StringBuilder buffer, int length); + + [SecurityCritical] + internal static string GetModuleFileName(HandleRef hModule) + { + var buffer = new StringBuilder(InteropValues.Win32Constant.MAX_PATH); + while (true) + { + var size = IntGetModuleFileName(hModule, buffer, buffer.Capacity); + if (size == 0) throw new Win32Exception(); + + if (size == buffer.Capacity) + { + buffer.EnsureCapacity(buffer.Capacity * 2); + continue; + } + + return buffer.ToString(); + } + } + + [SecurityCritical] + [SuppressUnmanagedCodeSecurity] + [DllImport(InteropValues.ExternDll.Shell32, CharSet = CharSet.Auto, BestFitMapping = false, ThrowOnUnmappableChar = true)] + internal static extern int ExtractIconEx(string szExeFileName, int nIconIndex, out IconHandle phiconLarge, + out IconHandle phiconSmall, int nIcons); + + [DllImport(InteropValues.ExternDll.Shell32, CharSet = CharSet.Auto)] + internal static extern int Shell_NotifyIcon(int message, InteropValues.NOTIFYICONDATA pnid); + + [SecurityCritical] + [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [DllImport(InteropValues.ExternDll.User32, SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "CreateWindowExW")] + internal static extern IntPtr CreateWindowEx( + int dwExStyle, + [MarshalAs(UnmanagedType.LPWStr)] string lpClassName, + [MarshalAs(UnmanagedType.LPWStr)] string lpWindowName, + int dwStyle, + int x, + int y, + int nWidth, + int nHeight, + IntPtr hWndParent, + IntPtr hMenu, + IntPtr hInstance, + IntPtr lpParam); + + [SecurityCritical] + [SuppressUnmanagedCodeSecurity] + [DllImport(InteropValues.ExternDll.User32, CharSet = CharSet.Unicode, SetLastError = true, BestFitMapping = false)] + internal static extern short RegisterClass(InteropValues.WNDCLASS4ICON wc); + + [DllImport(InteropValues.ExternDll.User32, CharSet = CharSet.Auto)] + internal static extern IntPtr DefWindowProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); + + [DllImport(InteropValues.ExternDll.User32, ExactSpelling = true, CharSet = CharSet.Auto)] + internal static extern bool SetForegroundWindow(IntPtr hWnd); + + [DllImport(InteropValues.ExternDll.User32, CharSet = CharSet.Auto, SetLastError = true)] + internal static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam); + + [DllImport(InteropValues.ExternDll.Kernel32, CharSet = CharSet.Auto, SetLastError = true)] + internal static extern IntPtr GetModuleHandle(string lpModuleName); + + [DllImport(InteropValues.ExternDll.User32, CharSet = CharSet.Auto, SetLastError = true)] + internal static extern bool UnhookWindowsHookEx(IntPtr hhk); + + [DllImport(InteropValues.ExternDll.User32, CharSet = CharSet.Auto, SetLastError = true)] + internal static extern IntPtr SetWindowsHookEx(int idHook, InteropValues.HookProc lpfn, IntPtr hMod, uint dwThreadId); + + [DllImport(InteropValues.ExternDll.User32, SetLastError = true)] + internal static extern IntPtr GetWindowDC(IntPtr window); + + [DllImport(InteropValues.ExternDll.Gdi32, SetLastError = true)] + internal static extern uint GetPixel(IntPtr dc, int x, int y); + + [DllImport(InteropValues.ExternDll.User32, SetLastError = true)] + internal static extern int ReleaseDC(IntPtr window, IntPtr dc); + + [DllImport(InteropValues.ExternDll.Gdi32, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Auto)] + internal static extern int GetDeviceCaps(IntPtr hdc, int nIndex); + + [DllImport(InteropValues.ExternDll.User32, CharSet = CharSet.Auto)] + internal static extern IntPtr GetDC(IntPtr ptr); + + [DllImport(InteropValues.ExternDll.User32, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetWindowPlacement(IntPtr hwnd, InteropValues.WINDOWPLACEMENT lpwndpl); + + internal static InteropValues.WINDOWPLACEMENT GetWindowPlacement(IntPtr hwnd) + { + InteropValues.WINDOWPLACEMENT wINDOWPLACEMENT = InteropValues.WINDOWPLACEMENT.Default; + if (GetWindowPlacement(hwnd, wINDOWPLACEMENT)) + { + return wINDOWPLACEMENT; + } + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + internal static int GetXLParam(int lParam) => LoWord(lParam); + + internal static int GetYLParam(int lParam) => HiWord(lParam); + + internal static int HiWord(int value) => (short) (value >> 16); + + internal static int LoWord(int value) => (short) (value & 65535); + + [DllImport(InteropValues.ExternDll.User32)] + internal static extern IntPtr MonitorFromWindow(IntPtr handle, int flags); + + [DllImport(InteropValues.ExternDll.User32)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool EnumThreadWindows(uint dwThreadId, InteropValues.EnumWindowsProc lpfn, IntPtr lParam); + + [DllImport(InteropValues.ExternDll.Gdi32)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool DeleteDC(IntPtr hdc); + + [DllImport(InteropValues.ExternDll.Gdi32, SetLastError = true)] + internal static extern IntPtr CreateCompatibleDC(IntPtr hdc); + + [DllImport(InteropValues.ExternDll.Gdi32, ExactSpelling = true, SetLastError = true)] + internal static extern IntPtr SelectObject(IntPtr hdc, IntPtr hgdiobj); + + [DllImport(InteropValues.ExternDll.User32, CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern IntPtr SendMessage(IntPtr hWnd, int nMsg, IntPtr wParam, IntPtr lParam); + + [DllImport(InteropValues.ExternDll.User32)] + internal static extern IntPtr MonitorFromPoint(InteropValues.POINT pt, int flags); + + [DllImport(InteropValues.ExternDll.User32)] + internal static extern IntPtr GetWindow(IntPtr hwnd, int nCmd); + + [DllImport(InteropValues.ExternDll.User32)] + internal static extern IntPtr GetActiveWindow(); + + [DllImport(InteropValues.ExternDll.User32)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool IsWindowVisible(IntPtr hwnd); + + [DllImport(InteropValues.ExternDll.User32)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool IsIconic(IntPtr hwnd); + + [DllImport(InteropValues.ExternDll.User32)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool IsZoomed(IntPtr hwnd); + + [DllImport(InteropValues.ExternDll.User32, CharSet = CharSet.Auto, ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, int flags); + + internal static System.Windows.Point GetCursorPos() + { + var result = default(System.Windows.Point); + if (GetCursorPos(out var point)) + { + result.X = point.X; + result.Y = point.Y; + } + return result; + } + + [DllImport(InteropValues.ExternDll.User32)] + private static extern int GetWindowLong(IntPtr hWnd, int nIndex); + + internal static int GetWindowLong(IntPtr hWnd, InteropValues.GWL nIndex) => GetWindowLong(hWnd, (int) nIndex); + + internal static IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong) => IntPtr.Size == 4 + ? SetWindowLongPtr32(hWnd, nIndex, dwNewLong) + : SetWindowLongPtr64(hWnd, nIndex, dwNewLong); + + [DllImport(InteropValues.ExternDll.User32, CharSet = CharSet.Auto, EntryPoint = "SetWindowLong")] + internal static extern IntPtr SetWindowLongPtr32(IntPtr hWnd, int nIndex, IntPtr dwNewLong); + + [DllImport(InteropValues.ExternDll.User32, CharSet = CharSet.Auto, EntryPoint = "SetWindowLongPtr")] + internal static extern IntPtr SetWindowLongPtr64(IntPtr hWnd, int nIndex, IntPtr dwNewLong); + + [DllImport(InteropValues.ExternDll.User32, CharSet = CharSet.Unicode)] + private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); + + [DllImport(InteropValues.ExternDll.User32, CharSet = CharSet.Unicode)] + private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong); + + internal static IntPtr SetWindowLongPtr(IntPtr hWnd, InteropValues.GWLP nIndex, IntPtr dwNewLong) + { + if (IntPtr.Size == 8) + { + return SetWindowLongPtr(hWnd, (int) nIndex, dwNewLong); + } + return new IntPtr(SetWindowLong(hWnd, (int) nIndex, dwNewLong.ToInt32())); + } + + internal static int SetWindowLong(IntPtr hWnd, InteropValues.GWL nIndex, int dwNewLong) => SetWindowLong(hWnd, (int) nIndex, dwNewLong); + + [DllImport(InteropValues.ExternDll.User32, CharSet = CharSet.Unicode)] + internal static extern ushort RegisterClass(ref InteropValues.WNDCLASS lpWndClass); + + [DllImport(InteropValues.ExternDll.Kernel32)] + internal static extern uint GetCurrentThreadId(); + + [DllImport(InteropValues.ExternDll.User32, CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern IntPtr CreateWindowEx(int dwExStyle, IntPtr classAtom, string lpWindowName, int dwStyle, int x, int y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam); + + [DllImport(InteropValues.ExternDll.User32)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool DestroyWindow(IntPtr hwnd); + + [DllImport(InteropValues.ExternDll.User32)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool UnregisterClass(IntPtr classAtom, IntPtr hInstance); + + [DllImport(InteropValues.ExternDll.User32)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool UpdateLayeredWindow(IntPtr hwnd, IntPtr hdcDest, ref InteropValues.POINT pptDest, ref InteropValues.SIZE psize, IntPtr hdcSrc, ref InteropValues.POINT pptSrc, uint crKey, [In] ref InteropValues.BLENDFUNCTION pblend, uint dwFlags); + + [DllImport(InteropValues.ExternDll.User32)] + internal static extern bool RedrawWindow(IntPtr hWnd, IntPtr lprcUpdate, IntPtr hrgnUpdate, InteropValues.RedrawWindowFlags flags); + + [DllImport(InteropValues.ExternDll.User32)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, InteropValues.EnumMonitorsDelegate lpfnEnum, IntPtr dwData); + + [DllImport(InteropValues.ExternDll.User32)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool IntersectRect(out InteropValues.RECT lprcDst, [In] ref InteropValues.RECT lprcSrc1, [In] ref InteropValues.RECT lprcSrc2); + + [DllImport(InteropValues.ExternDll.User32)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool GetMonitorInfo(IntPtr hMonitor, ref InteropValues.MONITORINFO monitorInfo); + + [DllImport(InteropValues.ExternDll.User32, ExactSpelling = true)] + [ResourceExposure(ResourceScope.None)] + public static extern IntPtr MonitorFromRect(ref InteropValues.RECT rect, int flags); + + [DllImport(InteropValues.ExternDll.Gdi32, SetLastError = true)] + internal static extern IntPtr CreateDIBSection(IntPtr hdc, ref InteropValues.BITMAPINFO pbmi, uint iUsage, out IntPtr ppvBits, IntPtr hSection, uint dwOffset); + + [DllImport(InteropValues.ExternDll.MsImg)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool AlphaBlend(IntPtr hdcDest, int xoriginDest, int yoriginDest, int wDest, int hDest, IntPtr hdcSrc, int xoriginSrc, int yoriginSrc, int wSrc, int hSrc, InteropValues.BLENDFUNCTION pfn); + + internal static int GET_SC_WPARAM(IntPtr wParam) => (int) wParam & 65520; + + [DllImport(InteropValues.ExternDll.User32)] + internal static extern IntPtr ChildWindowFromPointEx(IntPtr hwndParent, InteropValues.POINT pt, int uFlags); + + [DllImport(InteropValues.ExternDll.Gdi32)] + internal static extern IntPtr CreateCompatibleBitmap(IntPtr hDC, int width, int height); + + [DllImport(InteropValues.ExternDll.Gdi32)] + internal static extern bool BitBlt(IntPtr hDC, int x, int y, int nWidth, int nHeight, IntPtr hSrcDC, int xSrc, int ySrc, int dwRop); + + [DllImport(InteropValues.ExternDll.User32)] + [ResourceExposure(ResourceScope.None)] + internal static extern bool EnableWindow(IntPtr hWnd, bool enable); + + [DllImport(InteropValues.ExternDll.User32)] + public static extern bool ShowWindow(IntPtr hwnd, InteropValues.SW nCmdShow); + + [ReflectionPermission(SecurityAction.Assert, Unrestricted = true), SecurityPermission(SecurityAction.Assert, Flags = SecurityPermissionFlag.UnmanagedCode)] + internal static object PtrToStructure(IntPtr lparam, Type cls) => Marshal.PtrToStructure(lparam, cls); + + [ReflectionPermission(SecurityAction.Assert, Unrestricted = true), + SecurityPermission(SecurityAction.Assert, Flags = SecurityPermissionFlag.UnmanagedCode)] + internal static void PtrToStructure(IntPtr lparam, object data) => Marshal.PtrToStructure(lparam, data); + + [DllImport(InteropValues.ExternDll.Shell32, CallingConvention = CallingConvention.StdCall)] + internal static extern uint SHAppBarMessage(int dwMessage, ref InteropValues.APPBARDATA pData); + + [SecurityCritical] + [DllImport(InteropValues.ExternDll.DwmApi, EntryPoint = "DwmGetColorizationColor", PreserveSig = true)] + internal static extern int DwmGetColorizationColor(out uint pcrColorization, out bool pfOpaqueBlend); + + [DllImport(InteropValues.ExternDll.DwmApi, ExactSpelling = true, SetLastError = true)] + internal static extern int DwmSetWindowAttribute(IntPtr hwnd, InteropValues.DwmWindowAttribute dwAttribute, + in int pvAttribute, uint cbAttribute); + + [DllImport(InteropValues.ExternDll.User32, EntryPoint = "GetWindowLong")] + private static extern IntPtr GetWindowLongPtr32(IntPtr hWnd, int nIndex); + + [DllImport(InteropValues.ExternDll.User32, EntryPoint = "GetWindowLongPtr")] + private static extern IntPtr GetWindowLongPtr64(IntPtr hWnd, int nIndex); + + internal static IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex) => + IntPtr.Size == 8 ? GetWindowLongPtr64(hWnd, nIndex) : GetWindowLongPtr32(hWnd, nIndex); + + [DllImport(InteropValues.ExternDll.User32, ExactSpelling = true, CharSet = CharSet.Auto)] + [ResourceExposure(ResourceScope.None)] + internal static extern bool SetWindowPlacement(IntPtr hWnd, [In] ref InteropValues.WINDOWPLACEMENT placement); + + #endregion + + internal class Gdip + { + private const string ThreadDataSlotName = "system.drawing.threaddata"; + + private static IntPtr InitToken; + + private static bool Initialized => InitToken != IntPtr.Zero; + + internal const int + Ok = 0, + GenericError = 1, + InvalidParameter = 2, + OutOfMemory = 3, + ObjectBusy = 4, + InsufficientBuffer = 5, + NotImplemented = 6, + Win32Error = 7, + WrongState = 8, + Aborted = 9, + FileNotFound = 10, + ValueOverflow = 11, + AccessDenied = 12, + UnknownImageFormat = 13, + FontFamilyNotFound = 14, + FontStyleNotFound = 15, + NotTrueTypeFont = 16, + UnsupportedGdiplusVersion = 17, + GdiplusNotInitialized = 18, + PropertyNotFound = 19, + PropertyNotSupported = 20, + E_UNEXPECTED = unchecked((int) 0x8000FFFF); + + static Gdip() + { + Initialize(); + } + + [StructLayout(LayoutKind.Sequential)] + private struct StartupInput + { + private int GdiplusVersion; + + private readonly IntPtr DebugEventCallback; + + private bool SuppressBackgroundThread; + + private bool SuppressExternalCodecs; + + public static StartupInput GetDefault() + { + var result = new StartupInput + { + GdiplusVersion = 1, + SuppressBackgroundThread = false, + SuppressExternalCodecs = false + }; + return result; + } + } + + [StructLayout(LayoutKind.Sequential)] + private readonly struct StartupOutput + { + private readonly IntPtr hook; + + private readonly IntPtr unhook; + } + + [ResourceExposure(ResourceScope.None)] + [ResourceConsumption(ResourceScope.AppDomain, ResourceScope.AppDomain)] + [SuppressMessage("Microsoft.Performance", "CA1804:RemoveUnusedLocals")] + private static void Initialize() + { + var input = StartupInput.GetDefault(); + + var status = GdiplusStartup(out InitToken, ref input, out _); + + if (status != Ok) + { + throw StatusException(status); + } + + var currentDomain = AppDomain.CurrentDomain; + currentDomain.ProcessExit += OnProcessExit; + + if (!currentDomain.IsDefaultAppDomain()) + { + currentDomain.DomainUnload += OnProcessExit; + } + } + + [PrePrepareMethod] + [ResourceExposure(ResourceScope.AppDomain)] + [ResourceConsumption(ResourceScope.AppDomain)] + private static void OnProcessExit(object sender, EventArgs e) => Shutdown(); + + [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods")] + [ResourceExposure(ResourceScope.AppDomain)] + [ResourceConsumption(ResourceScope.AppDomain)] + private static void Shutdown() + { + if (Initialized) + { + ClearThreadData(); + // unhook our shutdown handlers as we do not need to shut down more than once + var currentDomain = AppDomain.CurrentDomain; + currentDomain.ProcessExit -= OnProcessExit; + if (!currentDomain.IsDefaultAppDomain()) + { + currentDomain.DomainUnload -= OnProcessExit; + } + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ClearThreadData() + { + var slot = Thread.GetNamedDataSlot(ThreadDataSlotName); + Thread.SetData(slot, null); + } + + [DllImport(InteropValues.ExternDll.GdiPlus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)] + [ResourceExposure(ResourceScope.None)] + internal static extern int GdipImageGetFrameDimensionsCount(HandleRef image, out int count); + + internal static Exception StatusException(int status) + { + return status switch + { + GenericError => new ExternalException("GdiplusGenericError"), + InvalidParameter => new ArgumentException("GdiplusInvalidParameter"), + OutOfMemory => new OutOfMemoryException("GdiplusOutOfMemory"), + ObjectBusy => new InvalidOperationException("GdiplusObjectBusy"), + InsufficientBuffer => new OutOfMemoryException("GdiplusInsufficientBuffer"), + NotImplemented => new NotImplementedException("GdiplusNotImplemented"), + Win32Error => new ExternalException("GdiplusGenericError"), + WrongState => new InvalidOperationException("GdiplusWrongState"), + Aborted => new ExternalException("GdiplusAborted"), + FileNotFound => new FileNotFoundException("GdiplusFileNotFound"), + ValueOverflow => new OverflowException("GdiplusOverflow"), + AccessDenied => new ExternalException("GdiplusAccessDenied"), + UnknownImageFormat => new ArgumentException("GdiplusUnknownImageFormat"), + PropertyNotFound => new ArgumentException("GdiplusPropertyNotFoundError"), + PropertyNotSupported => new ArgumentException("GdiplusPropertyNotSupportedError"), + FontFamilyNotFound => new ArgumentException("GdiplusFontFamilyNotFound"), + FontStyleNotFound => new ArgumentException("GdiplusFontStyleNotFound"), + NotTrueTypeFont => new ArgumentException("GdiplusNotTrueTypeFont_NoName"), + UnsupportedGdiplusVersion => new ExternalException("GdiplusUnsupportedGdiplusVersion"), + GdiplusNotInitialized => new ExternalException("GdiplusNotInitialized"), + _ => new ExternalException("GdiplusUnknown") + }; + } + + [DllImport(InteropValues.ExternDll.GdiPlus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)] + [ResourceExposure(ResourceScope.None)] + internal static extern int GdipImageGetFrameDimensionsList(HandleRef image, IntPtr buffer, int count); + + [DllImport(InteropValues.ExternDll.GdiPlus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)] + [ResourceExposure(ResourceScope.None)] + internal static extern int GdipImageGetFrameCount(HandleRef image, ref Guid dimensionId, int[] count); + + [DllImport(InteropValues.ExternDll.GdiPlus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)] + [ResourceExposure(ResourceScope.None)] + internal static extern int GdipGetPropertyItemSize(HandleRef image, int propid, out int size); + + [DllImport(InteropValues.ExternDll.GdiPlus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)] + [ResourceExposure(ResourceScope.None)] + internal static extern int GdipGetPropertyItem(HandleRef image, int propid, int size, IntPtr buffer); + + [DllImport(InteropValues.ExternDll.GdiPlus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)] + [ResourceExposure(ResourceScope.Machine)] + internal static extern int GdipCreateHBITMAPFromBitmap(HandleRef nativeBitmap, out IntPtr hbitmap, int argbBackground); + + [DllImport(InteropValues.ExternDll.GdiPlus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)] + [ResourceExposure(ResourceScope.None)] + internal static extern int GdipImageSelectActiveFrame(HandleRef image, ref Guid dimensionId, int frameIndex); + + [DllImport(InteropValues.ExternDll.GdiPlus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)] + [ResourceExposure(ResourceScope.Machine)] + internal static extern int GdipCreateBitmapFromFile(string filename, out IntPtr bitmap); + + [DllImport(InteropValues.ExternDll.GdiPlus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)] + [ResourceExposure(ResourceScope.None)] + internal static extern int GdipImageForceValidation(HandleRef image); + + [DllImport(InteropValues.ExternDll.GdiPlus, SetLastError = true, ExactSpelling = true, EntryPoint = "GdipDisposeImage", CharSet = CharSet.Unicode)] + [ResourceExposure(ResourceScope.None)] + private static extern int IntGdipDisposeImage(HandleRef image); + + internal static int GdipDisposeImage(HandleRef image) + { + if (!Initialized) return Ok; + var result = IntGdipDisposeImage(image); + return result; + } + + [DllImport(InteropValues.ExternDll.GdiPlus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)] + [ResourceExposure(ResourceScope.Process)] + private static extern int GdiplusStartup(out IntPtr token, ref StartupInput input, out StartupOutput output); + + [DllImport(InteropValues.ExternDll.GdiPlus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)] + [ResourceExposure(ResourceScope.None)] + internal static extern int GdipGetImageRawFormat(HandleRef image, ref Guid format); + + [DllImport(InteropValues.ExternDll.User32)] + internal static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref InteropValues.WINCOMPATTRDATA data); + + [DllImport(InteropValues.ExternDll.GdiPlus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)] + [ResourceExposure(ResourceScope.Machine)] + internal static extern int GdipCreateBitmapFromStream(InteropValues.IStream stream, out IntPtr bitmap); + + [DllImport(InteropValues.ExternDll.GdiPlus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)] + [ResourceExposure(ResourceScope.Machine)] + internal static extern int GdipCreateBitmapFromHBITMAP(HandleRef hbitmap, HandleRef hpalette, out IntPtr bitmap); + + [DllImport(InteropValues.ExternDll.GdiPlus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)] + [ResourceExposure(ResourceScope.None)] + internal static extern int GdipGetImageEncodersSize(out int numEncoders, out int size); + + [DllImport(InteropValues.ExternDll.GdiPlus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)] + [ResourceExposure(ResourceScope.None)] + internal static extern int GdipGetImageDecodersSize(out int numDecoders, out int size); + + [DllImport(InteropValues.ExternDll.GdiPlus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)] + [ResourceExposure(ResourceScope.None)] + internal static extern int GdipGetImageDecoders(int numDecoders, int size, IntPtr decoders); + + [DllImport(InteropValues.ExternDll.GdiPlus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)] + [ResourceExposure(ResourceScope.None)] + internal static extern int GdipGetImageEncoders(int numEncoders, int size, IntPtr encoders); + + [DllImport(InteropValues.ExternDll.GdiPlus, SetLastError = true, ExactSpelling = true, CharSet = CharSet.Unicode)] + [ResourceExposure(ResourceScope.None)] + internal static extern int GdipSaveImageToStream(HandleRef image, InteropValues.IStream stream, ref Guid classId, HandleRef encoderParams); + + [DllImport(InteropValues.ExternDll.NTdll)] + internal static extern int RtlGetVersion(out InteropValues.RTL_OSVERSIONINFOEX lpVersionInformation); + + } +} diff --git a/yy-admin-master/YY.Admin.Core/Tools/Interop/InteropValues.cs b/yy-admin-master/YY.Admin.Core/Tools/Interop/InteropValues.cs new file mode 100644 index 0000000..abd5f01 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Tools/Interop/InteropValues.cs @@ -0,0 +1,1192 @@ +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.InteropServices; +using System.Security; +using System.Windows; + +namespace YY.Admin.Tools.Interop; + +internal class InteropValues +{ + internal static class ExternDll + { + public const string + User32 = "user32.dll", + Gdi32 = "gdi32.dll", + GdiPlus = "gdiplus.dll", + Kernel32 = "kernel32.dll", + Shell32 = "shell32.dll", + MsImg = "msimg32.dll", + NTdll = "ntdll.dll", + DwmApi = "dwmapi.dll"; + } + + internal delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam); + + internal delegate IntPtr WndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); + + [return: MarshalAs(UnmanagedType.Bool)] + internal delegate bool EnumMonitorsDelegate(IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprcMonitor, IntPtr dwData); + + internal delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam); + + internal const int + BITSPIXEL = 12, + PLANES = 14, + BI_RGB = 0, + DIB_RGB_COLORS = 0, + E_FAIL = unchecked((int) 0x80004005), + HWND_TOP = 0, + GWL_STYLE = -16, + NIF_MESSAGE = 0x00000001, + NIF_ICON = 0x00000002, + NIF_TIP = 0x00000004, + NIF_INFO = 0x00000010, + NIM_ADD = 0x00000000, + NIM_MODIFY = 0x00000001, + NIM_DELETE = 0x00000002, + NIIF_NONE = 0x00000000, + NIIF_INFO = 0x00000001, + NIIF_WARNING = 0x00000002, + NIIF_ERROR = 0x00000003, + WM_ACTIVATE = 0x0006, + WM_QUIT = 0x0012, + WM_GETMINMAXINFO = 0x0024, + WM_WINDOWPOSCHANGING = 0x0046, + WM_WINDOWPOSCHANGED = 0x0047, + WM_SETICON = 0x0080, + WM_NCCREATE = 0x0081, + WM_NCDESTROY = 0x0082, + WM_NCHITTEST = 0x0084, + WM_NCACTIVATE = 0x0086, + WM_NCRBUTTONDOWN = 0x00A4, + WM_NCRBUTTONUP = 0x00A5, + WM_NCRBUTTONDBLCLK = 0x00A6, + WM_NCUAHDRAWCAPTION = 0x00AE, + WM_NCUAHDRAWFRAME = 0x00AF, + WM_KEYDOWN = 0x0100, + WM_KEYUP = 0x0101, + WM_SYSKEYDOWN = 0x0104, + WM_SYSKEYUP = 0x0105, + WM_SYSCOMMAND = 0x112, + WM_MOUSEMOVE = 0x0200, + WM_LBUTTONUP = 0x0202, + WM_LBUTTONDBLCLK = 0x0203, + WM_RBUTTONUP = 0x0205, + WM_ENTERSIZEMOVE = 0x0231, + WM_EXITSIZEMOVE = 0x0232, + WM_CLIPBOARDUPDATE = 0x031D, + WM_USER = 0x0400, + WS_VISIBLE = 0x10000000, + MF_BYCOMMAND = 0x00000000, + MF_BYPOSITION = 0x400, + MF_GRAYED = 0x00000001, + MF_SEPARATOR = 0x800, + NIN_BALLOONUSERCLICK = WM_USER + 5, + TB_GETBUTTON = WM_USER + 23, + TB_BUTTONCOUNT = WM_USER + 24, + TB_GETITEMRECT = WM_USER + 29, + WM_TRAYMOUSEMESSAGE = WM_USER + 1024, + VERTRES = 10, + DESKTOPVERTRES = 117, + LOGPIXELSX = 88, + LOGPIXELSY = 90, + SC_CLOSE = 0xF060, + SC_SIZE = 0xF000, + SC_MOVE = 0xF010, + SC_MINIMIZE = 0xF020, + SC_MAXIMIZE = 0xF030, + SC_RESTORE = 0xF120, + SRCCOPY = 0x00CC0020, + MONITOR_DEFAULTTOPRIMARY = 0x00000001, + MONITOR_DEFAULTTONEAREST = 0x00000002; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + internal class NOTIFYICONDATA + { + public int cbSize = Marshal.SizeOf(typeof(NOTIFYICONDATA)); + public IntPtr hWnd; + public int uID; + public int uFlags; + public int uCallbackMessage; + public IntPtr hIcon; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string szTip = string.Empty; + public int dwState = 0x01; + public int dwStateMask = 0x01; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] + public string szInfo = string.Empty; + public int uTimeoutOrVersion; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] + public string szInfoTitle = string.Empty; + public int dwInfoFlags; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct TBBUTTON + { + public int iBitmap; + public int idCommand; + public IntPtr fsStateStylePadding; + public IntPtr dwData; + public IntPtr iString; + } + + [Flags] + internal enum AllocationType + { + Commit = 0x1000, + Reserve = 0x2000, + Decommit = 0x4000, + Release = 0x8000, + Reset = 0x80000, + Physical = 0x400000, + TopDown = 0x100000, + WriteWatch = 0x200000, + LargePages = 0x20000000 + } + + [Flags] + internal enum MemoryProtection + { + Execute = 0x10, + ExecuteRead = 0x20, + ExecuteReadWrite = 0x40, + ExecuteWriteCopy = 0x80, + NoAccess = 0x01, + ReadOnly = 0x02, + ReadWrite = 0x04, + WriteCopy = 0x08, + GuardModifierflag = 0x100, + NoCacheModifierflag = 0x200, + WriteCombineModifierflag = 0x400 + } + + [StructLayout(LayoutKind.Sequential)] + internal struct TRAYDATA + { + public IntPtr hwnd; + public uint uID; + public uint uCallbackMessage; + public uint bReserved0; + public uint bReserved1; + public IntPtr hIcon; + } + + [Flags] + internal enum FreeType + { + Decommit = 0x4000, + Release = 0x8000, + } + + [StructLayout(LayoutKind.Sequential)] + internal struct POINT + { + public int X; + public int Y; + + public POINT(int x, int y) + { + X = x; + Y = y; + } + } + + internal enum HookType + { + WH_KEYBOARD_LL = 13, + WH_MOUSE_LL = 14 + } + + [StructLayout(LayoutKind.Sequential)] + internal struct MOUSEHOOKSTRUCT + { + public POINT pt; + public IntPtr hwnd; + public uint wHitTestCode; + public IntPtr dwExtraInfo; + } + + [Flags] + internal enum ProcessAccess + { + AllAccess = CreateThread | DuplicateHandle | QueryInformation | SetInformation | Terminate | VMOperation | VMRead | VMWrite | Synchronize, + CreateThread = 0x2, + DuplicateHandle = 0x40, + QueryInformation = 0x400, + SetInformation = 0x200, + Terminate = 0x1, + VMOperation = 0x8, + VMRead = 0x10, + VMWrite = 0x20, + Synchronize = 0x100000 + } + + [Serializable, StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + internal struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + + public RECT(int left, int top, int right, int bottom) + { + Left = left; + Top = top; + Right = right; + Bottom = bottom; + } + + public RECT(Rect rect) + { + Left = (int) rect.Left; + Top = (int) rect.Top; + Right = (int) rect.Right; + Bottom = (int) rect.Bottom; + } + + public Point Position => new(Left, Top); + public Size Size => new(Width, Height); + + public int Height + { + get => Bottom - Top; + set => Bottom = Top + value; + } + + public int Width + { + get => Right - Left; + set => Right = Left + value; + } + + public bool Equals(RECT other) + { + return Left == other.Left && Right == other.Right && Top == other.Top && Bottom == other.Bottom; + } + + public override bool Equals(object obj) + { + return obj is RECT rectangle && Equals(rectangle); + } + + public static bool operator ==(RECT left, RECT right) + { + return left.Equals(right); + } + + public static bool operator !=(RECT left, RECT right) + { + return !(left == right); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = Left; + hashCode = (hashCode * 397) ^ Top; + hashCode = (hashCode * 397) ^ Right; + hashCode = (hashCode * 397) ^ Bottom; + return hashCode; + } + } + } + + internal struct BLENDFUNCTION + { + public byte BlendOp; + public byte BlendFlags; + public byte SourceConstantAlpha; + public byte AlphaFormat; + } + + internal enum GWL + { + STYLE = -16, + EXSTYLE = -20 + } + + internal enum GWLP + { + WNDPROC = -4, + HINSTANCE = -6, + HWNDPARENT = -8, + USERDATA = -21, + ID = -12 + } + + internal struct BITMAPINFOHEADER + { + internal uint biSize; + internal int biWidth; + internal int biHeight; + internal ushort biPlanes; + internal ushort biBitCount; + internal uint biCompression; + internal uint biSizeImage; + internal int biXPelsPerMeter; + internal int biYPelsPerMeter; + internal uint biClrUsed; + internal uint biClrImportant; + } + + [Flags] + internal enum RedrawWindowFlags : uint + { + Invalidate = 1u, + InternalPaint = 2u, + Erase = 4u, + Validate = 8u, + NoInternalPaint = 16u, + NoErase = 32u, + NoChildren = 64u, + AllChildren = 128u, + UpdateNow = 256u, + EraseNow = 512u, + Frame = 1024u, + NoFrame = 2048u + } + + [StructLayout(LayoutKind.Sequential)] + internal class WINDOWPOS + { + public IntPtr hwnd; + public IntPtr hwndInsertAfter; + public int x; + public int y; + public int cx; + public int cy; + public uint flags; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct WINDOWPLACEMENT + { + public int length; + public int flags; + public SW showCmd; + public POINT ptMinPosition; + public POINT ptMaxPosition; + public RECT rcNormalPosition; + + /// + /// Gets the default (empty) value. + /// + public static WINDOWPLACEMENT Default + { + get + { + WINDOWPLACEMENT result = new WINDOWPLACEMENT(); + result.length = Marshal.SizeOf(typeof(WINDOWPLACEMENT)); + return result; + } + } + } + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + internal struct SIZE + { + [ComAliasName("Microsoft.VisualStudio.OLE.Interop.LONG")] + public int cx; + [ComAliasName("Microsoft.VisualStudio.OLE.Interop.LONG")] + public int cy; + } + + internal struct MONITORINFO + { + public uint cbSize; + public RECT rcMonitor; + public RECT rcWork; + public uint dwFlags; + } + + internal enum SM + { + CXSCREEN = 0, + CYSCREEN = 1, + CXVSCROLL = 2, + CYHSCROLL = 3, + CYCAPTION = 4, + CXBORDER = 5, + CYBORDER = 6, + CXFIXEDFRAME = 7, + CYFIXEDFRAME = 8, + CYVTHUMB = 9, + CXHTHUMB = 10, + CXICON = 11, + CYICON = 12, + CXCURSOR = 13, + CYCURSOR = 14, + CYMENU = 15, + CXFULLSCREEN = 16, + CYFULLSCREEN = 17, + CYKANJIWINDOW = 18, + MOUSEPRESENT = 19, + CYVSCROLL = 20, + CXHSCROLL = 21, + DEBUG = 22, + SWAPBUTTON = 23, + CXMIN = 28, + CYMIN = 29, + CXSIZE = 30, + CYSIZE = 31, + CXFRAME = 32, + CXSIZEFRAME = CXFRAME, + CYFRAME = 33, + CYSIZEFRAME = CYFRAME, + CXMINTRACK = 34, + CYMINTRACK = 35, + CXDOUBLECLK = 36, + CYDOUBLECLK = 37, + CXICONSPACING = 38, + CYICONSPACING = 39, + MENUDROPALIGNMENT = 40, + PENWINDOWS = 41, + DBCSENABLED = 42, + CMOUSEBUTTONS = 43, + SECURE = 44, + CXEDGE = 45, + CYEDGE = 46, + CXMINSPACING = 47, + CYMINSPACING = 48, + CXSMICON = 49, + CYSMICON = 50, + CYSMCAPTION = 51, + CXSMSIZE = 52, + CYSMSIZE = 53, + CXMENUSIZE = 54, + CYMENUSIZE = 55, + ARRANGE = 56, + CXMINIMIZED = 57, + CYMINIMIZED = 58, + CXMAXTRACK = 59, + CYMAXTRACK = 60, + CXMAXIMIZED = 61, + CYMAXIMIZED = 62, + NETWORK = 63, + CLEANBOOT = 67, + CXDRAG = 68, + CYDRAG = 69, + SHOWSOUNDS = 70, + CXMENUCHECK = 71, + CYMENUCHECK = 72, + SLOWMACHINE = 73, + MIDEASTENABLED = 74, + MOUSEWHEELPRESENT = 75, + XVIRTUALSCREEN = 76, + YVIRTUALSCREEN = 77, + CXVIRTUALSCREEN = 78, + CYVIRTUALSCREEN = 79, + CMONITORS = 80, + SAMEDISPLAYFORMAT = 81, + IMMENABLED = 82, + CXFOCUSBORDER = 83, + CYFOCUSBORDER = 84, + TABLETPC = 86, + MEDIACENTER = 87, + REMOTESESSION = 0x1000, + REMOTECONTROL = 0x2001 + } + + internal enum CacheSlot + { + DpiX, + + FocusBorderWidth, + FocusBorderHeight, + HighContrast, + MouseVanish, + + DropShadow, + FlatMenu, + WorkAreaInternal, + WorkArea, + + IconMetrics, + + KeyboardCues, + KeyboardDelay, + KeyboardPreference, + KeyboardSpeed, + SnapToDefaultButton, + WheelScrollLines, + MouseHoverTime, + MouseHoverHeight, + MouseHoverWidth, + + MenuDropAlignment, + MenuFade, + MenuShowDelay, + + ComboBoxAnimation, + ClientAreaAnimation, + CursorShadow, + GradientCaptions, + HotTracking, + ListBoxSmoothScrolling, + MenuAnimation, + SelectionFade, + StylusHotTracking, + ToolTipAnimation, + ToolTipFade, + UIEffects, + + MinimizeAnimation, + Border, + CaretWidth, + ForegroundFlashCount, + DragFullWindows, + NonClientMetrics, + + ThinHorizontalBorderHeight, + ThinVerticalBorderWidth, + CursorWidth, + CursorHeight, + ThickHorizontalBorderHeight, + ThickVerticalBorderWidth, + MinimumHorizontalDragDistance, + MinimumVerticalDragDistance, + FixedFrameHorizontalBorderHeight, + FixedFrameVerticalBorderWidth, + FocusHorizontalBorderHeight, + FocusVerticalBorderWidth, + FullPrimaryScreenWidth, + FullPrimaryScreenHeight, + HorizontalScrollBarButtonWidth, + HorizontalScrollBarHeight, + HorizontalScrollBarThumbWidth, + IconWidth, + IconHeight, + IconGridWidth, + IconGridHeight, + MaximizedPrimaryScreenWidth, + MaximizedPrimaryScreenHeight, + MaximumWindowTrackWidth, + MaximumWindowTrackHeight, + MenuCheckmarkWidth, + MenuCheckmarkHeight, + MenuButtonWidth, + MenuButtonHeight, + MinimumWindowWidth, + MinimumWindowHeight, + MinimizedWindowWidth, + MinimizedWindowHeight, + MinimizedGridWidth, + MinimizedGridHeight, + MinimumWindowTrackWidth, + MinimumWindowTrackHeight, + PrimaryScreenWidth, + PrimaryScreenHeight, + WindowCaptionButtonWidth, + WindowCaptionButtonHeight, + ResizeFrameHorizontalBorderHeight, + ResizeFrameVerticalBorderWidth, + SmallIconWidth, + SmallIconHeight, + SmallWindowCaptionButtonWidth, + SmallWindowCaptionButtonHeight, + VirtualScreenWidth, + VirtualScreenHeight, + VerticalScrollBarWidth, + VerticalScrollBarButtonHeight, + WindowCaptionHeight, + KanjiWindowHeight, + MenuBarHeight, + VerticalScrollBarThumbHeight, + IsImmEnabled, + IsMediaCenter, + IsMenuDropRightAligned, + IsMiddleEastEnabled, + IsMousePresent, + IsMouseWheelPresent, + IsPenWindows, + IsRemotelyControlled, + IsRemoteSession, + ShowSounds, + IsSlowMachine, + SwapButtons, + IsTabletPC, + VirtualScreenLeft, + VirtualScreenTop, + + PowerLineStatus, + + IsGlassEnabled, + UxThemeName, + UxThemeColor, + WindowCornerRadius, + WindowGlassColor, + WindowGlassBrush, + WindowNonClientFrameThickness, + WindowResizeBorderThickness, + + NumSlots + } + + internal static class Win32Constant + { + internal const int MAX_PATH = 260; + internal const int INFOTIPSIZE = 1024; + internal const int TRUE = 1; + internal const int FALSE = 0; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct WNDCLASS + { + public uint style; + public Delegate lpfnWndProc; + public int cbClsExtra; + public int cbWndExtra; + public IntPtr hInstance; + public IntPtr hIcon; + public IntPtr hCursor; + public IntPtr hbrBackground; + [MarshalAs(UnmanagedType.LPWStr)] + public string lpszMenuName; + [MarshalAs(UnmanagedType.LPWStr)] + public string lpszClassName; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + internal class WNDCLASS4ICON + { + public int style; + public WndProc lpfnWndProc; + public int cbClsExtra; + public int cbWndExtra; + public IntPtr hInstance; + public IntPtr hIcon; + public IntPtr hCursor; + public IntPtr hbrBackground; + public string lpszMenuName; + public string lpszClassName; + } + + [StructLayout(LayoutKind.Sequential, Pack = 2)] + internal struct BITMAPINFO + { + public int biSize; + + public int biWidth; + + public int biHeight; + + public short biPlanes; + + public short biBitCount; + + public int biCompression; + + public int biSizeImage; + + public int biXPelsPerMeter; + + public int biYPelsPerMeter; + + public int biClrUsed; + + public int biClrImportant; + + public BITMAPINFO(int width, int height, short bpp) + { + biSize = SizeOf(); + biWidth = width; + biHeight = height; + biPlanes = 1; + biBitCount = bpp; + biCompression = 0; + biSizeImage = 0; + biXPelsPerMeter = 0; + biYPelsPerMeter = 0; + biClrUsed = 0; + biClrImportant = 0; + } + + [SecuritySafeCritical] + private static int SizeOf() + { + return Marshal.SizeOf(typeof(BITMAPINFO)); + } + } + + [StructLayout(LayoutKind.Sequential)] + internal class ICONINFO + { + public bool fIcon = false; + public int xHotspot = 0; + public int yHotspot = 0; + public BitmapHandle hbmMask = null; + public BitmapHandle hbmColor = null; + } + + internal enum WINDOWCOMPOSITIONATTRIB + { + WCA_ACCENT_POLICY = 19 + } + + [StructLayout(LayoutKind.Sequential)] + internal struct WINCOMPATTRDATA + { + public WINDOWCOMPOSITIONATTRIB Attribute; + public IntPtr Data; + public int DataSize; + } + + internal enum ACCENTSTATE + { + ACCENT_DISABLED = 0, + ACCENT_ENABLE_GRADIENT = 1, + ACCENT_ENABLE_TRANSPARENTGRADIENT = 2, + ACCENT_ENABLE_BLURBEHIND = 3, + ACCENT_ENABLE_ACRYLICBLURBEHIND = 4, + ACCENT_INVALID_STATE = 5 + } + + [StructLayout(LayoutKind.Sequential)] + internal struct ACCENTPOLICY + { + public ACCENTSTATE AccentState; + public int AccentFlags; + public uint GradientColor; + public int AnimationId; + } + + [ComImport, Guid("0000000C-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + internal interface IStream + { + int Read([In] IntPtr buf, [In] int len); + + int Write([In] IntPtr buf, [In] int len); + + [return: MarshalAs(UnmanagedType.I8)] + long Seek([In, MarshalAs(UnmanagedType.I8)] long dlibMove, [In] int dwOrigin); + + void SetSize([In, MarshalAs(UnmanagedType.I8)] long libNewSize); + + [return: MarshalAs(UnmanagedType.I8)] + long CopyTo([In, MarshalAs(UnmanagedType.Interface)] IStream pstm, [In, MarshalAs(UnmanagedType.I8)] long cb, [Out, MarshalAs(UnmanagedType.LPArray)] long[] pcbRead); + + void Commit([In] int grfCommitFlags); + + void Revert(); + + void LockRegion([In, MarshalAs(UnmanagedType.I8)] long libOffset, [In, MarshalAs(UnmanagedType.I8)] long cb, [In] int dwLockType); + + void UnlockRegion([In, MarshalAs(UnmanagedType.I8)] long libOffset, [In, MarshalAs(UnmanagedType.I8)] long cb, [In] int dwLockType); + + void Stat([In] IntPtr pStatstg, [In] int grfStatFlag); + + [return: MarshalAs(UnmanagedType.Interface)] + IStream Clone(); + } + + [SuppressMessage("ReSharper", "InconsistentNaming")] + internal class StreamConsts + { + public const int LOCK_WRITE = 0x1; + public const int LOCK_EXCLUSIVE = 0x2; + public const int LOCK_ONLYONCE = 0x4; + public const int STATFLAG_DEFAULT = 0x0; + public const int STATFLAG_NONAME = 0x1; + public const int STATFLAG_NOOPEN = 0x2; + public const int STGC_DEFAULT = 0x0; + public const int STGC_OVERWRITE = 0x1; + public const int STGC_ONLYIFCURRENT = 0x2; + public const int STGC_DANGEROUSLYCOMMITMERELYTODISKCACHE = 0x4; + public const int STREAM_SEEK_SET = 0x0; + public const int STREAM_SEEK_CUR = 0x1; + public const int STREAM_SEEK_END = 0x2; + } + + [StructLayout(LayoutKind.Sequential, Pack = 8)] + internal class ImageCodecInfoPrivate + { + [MarshalAs(UnmanagedType.Struct)] + public Guid Clsid; + [MarshalAs(UnmanagedType.Struct)] + public Guid FormatID; + + public IntPtr CodecName = IntPtr.Zero; + public IntPtr DllName = IntPtr.Zero; + public IntPtr FormatDescription = IntPtr.Zero; + public IntPtr FilenameExtension = IntPtr.Zero; + public IntPtr MimeType = IntPtr.Zero; + + public int Flags; + public int Version; + public int SigCount; + public int SigSize; + + public IntPtr SigPattern = IntPtr.Zero; + public IntPtr SigMask = IntPtr.Zero; + } + + internal class ComStreamFromDataStream : IStream + { + protected Stream DataStream; + + // to support seeking ahead of the stream length... + private long _virtualPosition = -1; + + internal ComStreamFromDataStream(Stream dataStream) + { + this.DataStream = dataStream ?? throw new ArgumentNullException(nameof(dataStream)); + } + + private void ActualizeVirtualPosition() + { + if (_virtualPosition == -1) return; + + if (_virtualPosition > DataStream.Length) + DataStream.SetLength(_virtualPosition); + + DataStream.Position = _virtualPosition; + + _virtualPosition = -1; + } + + public virtual IStream Clone() + { + NotImplemented(); + return null; + } + + public virtual void Commit(int grfCommitFlags) + { + DataStream.Flush(); + ActualizeVirtualPosition(); + } + + public virtual long CopyTo(IStream pstm, long cb, long[] pcbRead) + { + const int bufsize = 4096; // one page + var buffer = Marshal.AllocHGlobal(bufsize); + if (buffer == IntPtr.Zero) throw new OutOfMemoryException(); + long written = 0; + + try + { + while (written < cb) + { + var toRead = bufsize; + if (written + toRead > cb) toRead = (int) (cb - written); + var read = Read(buffer, toRead); + if (read == 0) break; + if (pstm.Write(buffer, read) != read) + { + throw EFail("Wrote an incorrect number of bytes"); + } + written += read; + } + } + finally + { + Marshal.FreeHGlobal(buffer); + } + if (pcbRead != null && pcbRead.Length > 0) + { + pcbRead[0] = written; + } + + return written; + } + + public virtual Stream GetDataStream() => DataStream; + + public virtual void LockRegion(long libOffset, long cb, int dwLockType) + { + } + + protected static ExternalException EFail(string msg) => throw new ExternalException(msg, E_FAIL); + + protected static void NotImplemented() => throw new NotImplementedException(); + + public virtual int Read(IntPtr buf, int length) + { + var buffer = new byte[length]; + var count = Read(buffer, length); + Marshal.Copy(buffer, 0, buf, length); + return count; + } + + public virtual int Read(byte[] buffer, int length) + { + ActualizeVirtualPosition(); + return DataStream.Read(buffer, 0, length); + } + + public virtual void Revert() => NotImplemented(); + + public virtual long Seek(long offset, int origin) + { + var pos = _virtualPosition; + if (_virtualPosition == -1) + { + pos = DataStream.Position; + } + var len = DataStream.Length; + + switch (origin) + { + case StreamConsts.STREAM_SEEK_SET: + if (offset <= len) + { + DataStream.Position = offset; + _virtualPosition = -1; + } + else + { + _virtualPosition = offset; + } + break; + case StreamConsts.STREAM_SEEK_END: + if (offset <= 0) + { + DataStream.Position = len + offset; + _virtualPosition = -1; + } + else + { + _virtualPosition = len + offset; + } + break; + case StreamConsts.STREAM_SEEK_CUR: + if (offset + pos <= len) + { + DataStream.Position = pos + offset; + _virtualPosition = -1; + } + else + { + _virtualPosition = offset + pos; + } + break; + } + + return _virtualPosition != -1 ? _virtualPosition : DataStream.Position; + } + + public virtual void SetSize(long value) => DataStream.SetLength(value); + + public virtual void Stat(IntPtr pstatstg, int grfStatFlag) => NotImplemented(); + + public virtual void UnlockRegion(long libOffset, long cb, int dwLockType) + { + } + + public virtual int Write(IntPtr buf, int length) + { + var buffer = new byte[length]; + Marshal.Copy(buf, buffer, 0, length); + return Write(buffer, length); + } + + public virtual int Write(byte[] buffer, int length) + { + ActualizeVirtualPosition(); + DataStream.Write(buffer, 0, length); + return length; + } + } + + [StructLayout(LayoutKind.Sequential)] + internal class MINMAXINFO + { + public POINT ptReserved; + public POINT ptMaxSize; + public POINT ptMaxPosition; + public POINT ptMinTrackSize; + public POINT ptMaxTrackSize; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct APPBARDATA + { + public int cbSize; + public IntPtr hWnd; + public uint uCallbackMessage; + public uint uEdge; + public RECT rc; + public int lParam; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct RTL_OSVERSIONINFOEX + { + internal uint dwOSVersionInfoSize; + internal uint dwMajorVersion; + internal uint dwMinorVersion; + internal uint dwBuildNumber; + internal uint dwPlatformId; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + internal string szCSDVersion; + } + + [Flags] + internal enum WindowPositionFlags + { + /// + /// If the calling thread and the thread that owns the window are attached to different input queues, the system posts + /// the request to the thread that owns the window. This prevents the calling thread from blocking its execution while + /// other threads process the request. + /// + SWP_ASYNCWINDOWPOS = 0x4000, + + /// + /// Prevents generation of the WM_SYNCPAINT message. + /// + SWP_DEFERERASE = 0x2000, + + /// + /// Draws a frame (defined in the window's class description) around the window. + /// + SWP_DRAWFRAME = 0x0020, + + /// + /// Applies new frame styles set using the SetWindowLong function. Sends a WM_NCCALCSIZE message to the window, even if + /// the window's size is not being changed. If this flag is not specified, WM_NCCALCSIZE is sent only when the window's + /// size is being changed. + /// + SWP_FRAMECHANGED = 0x0020, + + /// + /// Hides the window. + /// + SWP_HIDEWINDOW = 0x0080, + + /// + /// Does not activate the window. If this flag is not set, the window is activated and moved to the top of either the + /// topmost or non-topmost group (depending on the setting of the hWndInsertAfter parameter). + /// + SWP_NOACTIVATE = 0x0010, + + /// + /// Discards the entire contents of the client area. If this flag is not specified, the valid contents of the client + /// area are saved and copied back into the client area after the window is sized or repositioned. + /// + SWP_NOCOPYBITS = 0x0100, + + /// + /// Retains the current position (ignores X and Y parameters). + /// + SWP_NOMOVE = 0x0002, + + /// + /// Does not change the owner window's position in the Z order. + /// + SWP_NOOWNERZORDER = 0x0200, + + /// + /// Does not redraw changes. If this flag is set, no repainting of any kind occurs. This applies to the client area, + /// the nonclient area (including the title bar and scroll bars), and any part of the parent window uncovered as a + /// result of the window being moved. When this flag is set, the application must explicitly invalidate or redraw any + /// parts of the window and parent window that need redrawing. + /// + SWP_NOREDRAW = 0x0008, + + /// + /// Same as the SWP_NOOWNERZORDER flag. + /// + SWP_NOREPOSITION = 0x0200, + + /// + /// Prevents the window from receiving the WM_WINDOWPOSCHANGING message. + /// + SWP_NOSENDCHANGING = 0x0400, + + /// + /// Retains the current size (ignores the cx and cy parameters). + /// + SWP_NOSIZE = 0x0001, + + /// + /// Retains the current Z order (ignores the hWndInsertAfter parameter). + /// + SWP_NOZORDER = 0x0004, + + /// + /// Displays the window. + /// + SWP_SHOWWINDOW = 0x0040 + } + + [StructLayout(LayoutKind.Sequential)] + internal struct WindowPosition + { + public IntPtr Hwnd; + public IntPtr HwndZOrderInsertAfter; + public int X; + public int Y; + public int Width; + public int Height; + public WindowPositionFlags Flags; + } + + [Flags] + internal enum DwmWindowAttribute : uint + { + DWMWA_NCRENDERING_ENABLED = 1, + DWMWA_NCRENDERING_POLICY, + DWMWA_TRANSITIONS_FORCEDISABLED, + DWMWA_ALLOW_NCPAINT, + DWMWA_CAPTION_BUTTON_BOUNDS, + DWMWA_NONCLIENT_RTL_LAYOUT, + DWMWA_FORCE_ICONIC_REPRESENTATION, + DWMWA_FLIP3D_POLICY, + DWMWA_EXTENDED_FRAME_BOUNDS, + DWMWA_HAS_ICONIC_BITMAP, + DWMWA_DISALLOW_PEEK, + DWMWA_EXCLUDED_FROM_PEEK, + DWMWA_LAST + } + + [Flags] + internal enum WindowStyles + { + /// + /// The window is initially maximized. + /// + WS_MAXIMIZE = 0x01000000, + + /// + /// The window has a maximize button. Cannot be combined with the WS_EX_CONTEXTHELP style. The WS_SYSMENU style must + /// also be specified. + /// + WS_MAXIMIZEBOX = 0x00010000, + + /// + /// The window is initially minimized. Same as the WS_ICONIC style. + /// + WS_MINIMIZE = 0x20000000, + + /// + /// The window has a sizing border. Same as the WS_SIZEBOX style. + /// + WS_THICKFRAME = 0x00040000, + } + + /// + /// ShowWindow options + /// + internal enum SW + { + HIDE = 0, + SHOWNORMAL = 1, + NORMAL = 1, + SHOWMINIMIZED = 2, + SHOWMAXIMIZED = 3, + MAXIMIZE = 3, + SHOWNOACTIVATE = 4, + SHOW = 5, + MINIMIZE = 6, + SHOWMINNOACTIVE = 7, + SHOWNA = 8, + RESTORE = 9, + SHOWDEFAULT = 10, + FORCEMINIMIZE = 11, + } +} diff --git a/yy-admin-master/YY.Admin.Core/Util/CryptogramUtil.cs b/yy-admin-master/YY.Admin.Core/Util/CryptogramUtil.cs new file mode 100644 index 0000000..911f0be --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Util/CryptogramUtil.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core.Util +{ + public class CryptogramUtil + { + public static readonly string CryptoType = "SM2"; // 加密类型 + public static readonly string PublicKey = "0484C7466D950E120E5ECE5DD85D0C90EAA85081A3A2BD7C57AE6DC822EFCCBD66620C67B0103FC8DD280E36C3B282977B722AAEC3C56518EDCEBAFB72C5A05312"; // 公钥 + public static readonly string PrivateKey = "8EDB615B1D48B8BE188FC0F18EC08A41DF50EA731FA28BF409E6552809E3A111"; // 私钥 + + public static readonly string SM4_key = "0123456789abcdeffedcba9876543210"; + public static readonly string SM4_iv = "595298c7c6fd271f0402f804c33d3f66"; + + /// + /// Md5加密 + /// + /// + /// + public static string Encrypt(string plainText) + { + if (CryptoType == CryptogramEnum.MD5.ToString()) + { + return Encrypt(plainText); + } + else if (CryptoType == CryptogramEnum.SM2.ToString()) + { + return SM2Encrypt(plainText); + } + else if (CryptoType == CryptogramEnum.SM4.ToString()) + { + return SM4EncryptECB(plainText); + } + return plainText; + } + + /// + /// 解密 + /// + /// + /// + public static string Decrypt(string cipherText) + { + if (CryptoType == CryptogramEnum.SM2.ToString()) + { + return SM2Decrypt(cipherText); + } + else if (CryptoType == CryptogramEnum.SM4.ToString()) + { + return SM4DecryptECB(cipherText); + } + return cipherText; + } + + /// + /// SM2加密 + /// + /// + /// + public static string SM2Encrypt(string plainText) + { + return GMUtil.SM2Encrypt(PublicKey, plainText); + } + + /// + /// SM2解密 + /// + /// + /// + public static string SM2Decrypt(string cipherText) + { + return GMUtil.SM2Decrypt(PrivateKey, cipherText); + } + + /// + /// SM4加密(ECB) + /// + /// + /// + public static string SM4EncryptECB(string plainText) + { + return GMUtil.SM4EncryptECB(SM4_key, plainText); + } + + /// + /// SM4解密(ECB) + /// + /// + /// + public static string SM4DecryptECB(string cipherText) + { + return GMUtil.SM4DecryptECB(SM4_key, cipherText); + } + + /// + /// SM4加密(CBC) + /// + /// + /// + public static string SM4EncryptCBC(string plainText) + { + return GMUtil.SM4EncryptCBC(SM4_key, SM4_iv, plainText); + } + + /// + /// SM4解密(CBC) + /// + /// + /// + public static string SM4DecryptCBC(string cipherText) + { + return GMUtil.SM4DecryptCBC(SM4_key, SM4_iv, cipherText); + } + /// + /// MD5 比较 + /// + /// 加密文本 + /// MD5 字符串 + /// 是否输出大写加密,默认 false + /// 是否输出 16 位 + /// bool + public static bool Compare(string text, string hash, bool uppercase = false, bool is16 = false) + { + return Compare(Encoding.UTF8.GetBytes(text), hash, uppercase, is16); + } + + /// + /// MD5 加密 + /// + /// 加密文本 + /// 是否输出大写加密,默认 false + /// 是否输出 16 位 + /// + public static string Encrypt(string text, bool uppercase = false, bool is16 = false) + { + return Encrypt(Encoding.UTF8.GetBytes(text), uppercase, is16); + } + + /// + /// MD5 加密 + /// + /// 字节数组 + /// 是否输出大写加密,默认 false + /// 是否输出 16 位 + /// + public static string Encrypt(byte[] bytes, bool uppercase = false, bool is16 = false) + { + var data = MD5.HashData(bytes); + + var stringBuilder = new StringBuilder(); + for (var i = 0; i < data.Length; i++) + { + stringBuilder.Append(data[i].ToString("x2")); + } + + var md5String = stringBuilder.ToString(); + var hash = !is16 ? md5String : md5String.Substring(8, 16); + return !uppercase ? hash : hash.ToUpper(); + } + + /// + /// MD5 比较 + /// + /// 字节数组 + /// MD5 字符串 + /// 是否输出大写加密,默认 false + /// 是否输出 16 位 + /// bool + public static bool Compare(byte[] bytes, string hash, bool uppercase = false, bool is16 = false) + { + var hashOfInput = Encrypt(bytes, uppercase, is16); + return hash.Equals(hashOfInput, StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Util/DeviceInfoUtil.cs b/yy-admin-master/YY.Admin.Core/Util/DeviceInfoUtil.cs new file mode 100644 index 0000000..dbc4537 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Util/DeviceInfoUtil.cs @@ -0,0 +1,150 @@ +using System.Diagnostics; +using System.Management; +using System.Net; +using System.Net.Http; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Runtime.InteropServices; + +namespace YY.Admin.Core.Util +{ + public static class DeviceInfoUtil + { + private static readonly ILoggerService _logger = ContainerLocator.Container.Resolve(); + + private static readonly HttpClient _httpClient = ContainerLocator.Container.Resolve(); + + public static async Task GetPublicIpAddressAsync() + { + try + { + using var ipinfoCts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); + var response = await _httpClient.GetStringAsync("https://ipinfo.io/ip", ipinfoCts.Token); + return response.Trim(); + } + catch (Exception ex) + { + _logger.Error($"获取登录IP地址失败:{ex.Message}"); + // 备用方案 + try + { + using var ipifyCts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); + var response = await _httpClient.GetStringAsync("https://api.ipify.org", ipifyCts.Token); + return response.Trim(); + } + catch (Exception ex2) + { + _logger.Error($"获取登录IP地址失败2:{ex2.Message}"); + return "无法获取公网IP"; + } + } + } + + public static IEnumerable GetLocalIpAddresses() + { + try + { + var host = Dns.GetHostEntry(Dns.GetHostName()); + return host.AddressList + .Where(ip => ip.AddressFamily == AddressFamily.InterNetwork) + .Select(ip => ip.ToString()) + .ToList(); + } + catch + { + return new List { "无法获取本地IP" }; + } + } + + public static string GetMachineName() + { + return Environment.MachineName; + } + + public static string GetOsVersion() + { + return $"{Environment.OSVersion} ({(Environment.Is64BitOperatingSystem ? "64位" : "32位")})"; + } + + public static string GetMacAddress() + { + try + { + var networkInterface = NetworkInterface.GetAllNetworkInterfaces() + .FirstOrDefault(nic => nic.OperationalStatus == OperationalStatus.Up && + nic.NetworkInterfaceType != NetworkInterfaceType.Loopback); + + return networkInterface?.GetPhysicalAddress().ToString() ?? "无法获取MAC地址"; + } + catch + { + return "无法获取MAC地址"; + } + } + + public static string GetProcessorInfo() + { + try + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + using (var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_Processor")) + using (var collection = searcher.Get()) + { + foreach (var item in collection) + { + return $"{item["Name"]} ({item["NumberOfCores"]}核)"; + } + } + } + return Environment.ProcessorCount + " 核处理器"; + } + catch + { + return Environment.ProcessorCount + " 核处理器"; + } + } + + public static long GetTotalMemory() + { + try + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + using (var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_ComputerSystem")) + using (var collection = searcher.Get()) + { + foreach (var item in collection) + { + var totalMemory = Convert.ToInt64(item["TotalPhysicalMemory"]); + return totalMemory / (1024 * 1024); // 转换为MB + } + } + } + return 0; + } + catch + { + return 0; + } + } + + public static string GetSystemUpTime() + { + try + { + using (var uptime = new PerformanceCounter("System", "System Up Time")) + { + uptime.NextValue(); + var seconds = (int)uptime.NextValue(); + var ts = TimeSpan.FromSeconds(seconds); + return $"{ts.Days}天 {ts.Hours}小时 {ts.Minutes}分钟"; + } + } + catch + { + return "无法获取运行时间"; + } + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Util/EnumUtil.cs b/yy-admin-master/YY.Admin.Core/Util/EnumUtil.cs new file mode 100644 index 0000000..81ad87a --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Util/EnumUtil.cs @@ -0,0 +1,178 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using YY.Admin.Core.Extension; + +namespace YY.Admin.Core.Util +{ + public static class EnumUtil + { + // + // 摘要: + // 枚举变量是否包含指定标识 + // + // 参数: + // value: + // 枚举变量 + // + // flag: + // 要判断的标识 + public static bool Has(this Enum value, Enum flag) + { + if (value.GetType() != flag.GetType()) + { + throw new ArgumentException("flag", "Enumeration identification judgment must be of the same type"); + } + + ulong num = Convert.ToUInt64(flag); + return (Convert.ToUInt64(value) & num) == num; + } + + // + // 摘要: + // 设置标识位 + // + // 参数: + // source: + // + // flag: + // + // value: + // 数值 + // + // 类型参数: + // T: + public static T Set(this Enum source, T flag, bool value) + { + if (!(source is T)) + { + throw new ArgumentException("source", "Enumeration identification judgment must be of the same type"); + } + + ulong num = Convert.ToUInt64(source); + ulong num2 = Convert.ToUInt64(flag); + num = ((!value) ? (num & ~num2) : (num | num2)); + return (T)Enum.ToObject(typeof(T), num); + } + + // + // 摘要: + // 获取枚举字段的注释 + // + // 参数: + // value: + // 数值 + public static string? GetDescription(this Enum value) + { + if (value == null) + { + return null; + } + + FieldInfo field = value.GetType().GetField(value.ToString(), BindingFlags.Static | BindingFlags.Public); + if (field == null) + { + return null; + } + + DescriptionAttribute customAttribute = field.GetCustomAttribute(inherit: false); + if (customAttribute != null && !string.IsNullOrEmpty(customAttribute.Description)) + { + return customAttribute.Description; + } + + return null; + } + + // + // 摘要: + // 获取枚举类型的所有字段注释 + // + // 类型参数: + // TEnum: + public static Dictionary GetDescriptions() where TEnum : notnull + { + Dictionary dictionary = new Dictionary(); + foreach (KeyValuePair description in GetDescriptions(typeof(TEnum))) + { + dictionary.Add((TEnum)Enum.ToObject(typeof(TEnum), description.Key), description.Value); + } + + return dictionary; + } + + // + // 摘要: + // 获取枚举类型的所有字段注释 + // + // 参数: + // enumType: + public static Dictionary GetDescriptions(Type enumType) + { + Dictionary dictionary = new Dictionary(); + FieldInfo[] fields = enumType.GetFields(BindingFlags.Static | BindingFlags.Public); + foreach (FieldInfo fieldInfo in fields) + { + if (fieldInfo.IsStatic) + { + int key = Convert.ToInt32(fieldInfo.GetValue(null)); + string value = fieldInfo.Name; + DisplayNameAttribute customAttribute = fieldInfo.GetCustomAttribute(inherit: false); + if (customAttribute != null && !string.IsNullOrEmpty(customAttribute.DisplayName)) + { + value = customAttribute.DisplayName; + } + + DescriptionAttribute customAttribute2 = fieldInfo.GetCustomAttribute(inherit: false); + if (customAttribute2 != null && !string.IsNullOrEmpty(customAttribute2.Description)) + { + value = customAttribute2.Description; + } + + dictionary[key] = value; + } + } + + return dictionary; + } + /// + /// 获取操作码的查询操作枚举表示 + /// + /// 操作码 + /// + public static FilterOperateEnum GetFilterOperate(string code) + { + //code.CheckNotNullOrEmpty("code"); + Type type = typeof(FilterOperateEnum); + MemberInfo[] members = type.GetMembers(BindingFlags.Public | BindingFlags.Static); + foreach (MemberInfo member in members) + { + FilterOperateEnum operate = member.Name.CastTo(); + if (operate.ToOperateCode() == code) + { + return operate; + } + } + throw new NotSupportedException("获取操作码的查询操作枚举表示时不支持代码:" + code); + } + /// + /// 把查询操作的枚举表示转换为操作码 + /// + /// 查询操作的枚举表示 + public static string ToOperateCode(this FilterOperateEnum operate) + { + Type type = operate.GetType(); + MemberInfo[] members = type.GetMember(operate.CastTo()); + if (members.Length == 0) + { + return null; + } + + OperateCodeAttribute attribute = members[0].GetAttribute(); + return attribute?.Code; + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Util/GM/GM.cs b/yy-admin-master/YY.Admin.Core/Util/GM/GM.cs new file mode 100644 index 0000000..c36fc4f --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Util/GM/GM.cs @@ -0,0 +1,471 @@ +using System.Collections; +using System.IO; +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.GM; +using Org.BouncyCastle.Asn1.X9; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Math.EC; +using Org.BouncyCastle.Math.EC.Multiplier; +using Org.BouncyCastle.Security; +using Org.BouncyCastle.Utilities; +using Org.BouncyCastle.Utilities.Collections; +using Org.BouncyCastle.Utilities.Encoders; +using Org.BouncyCastle.X509; + +namespace YY.Admin.Core.Util +{ + /** + * + * 用BC的注意点: + * 这个版本的BC对SM3withSM2的结果为asn1格式的r和s,如果需要直接拼接的r||s需要自己转换。下面rsAsn1ToPlainByteArray、rsPlainByteArrayToAsn1就在干这事。 + * 这个版本的BC对SM2的结果为C1||C2||C3,据说为旧标准,新标准为C1||C3||C2,用新标准的需要自己转换。下面(被注释掉的)changeC1C2C3ToC1C3C2、changeC1C3C2ToC1C2C3就在干这事。java版的高版本有加上C1C3C2,csharp版没准以后也会加,但目前还没有,java版的目前可以初始化时“ SM2Engine sm2Engine = new SM2Engine(SM2Engine.Mode.C1C3C2);”。 + * + */ + + public class GM + { + private static X9ECParameters x9ECParameters = GMNamedCurves.GetByName("sm2p256v1"); + private static ECDomainParameters ecDomainParameters = new(x9ECParameters.Curve, x9ECParameters.G, x9ECParameters.N); + + /** + * + * @param msg + * @param userId + * @param privateKey + * @return r||s,直接拼接byte数组的rs + */ + + public static byte[] SignSm3WithSm2(byte[] msg, byte[] userId, AsymmetricKeyParameter privateKey) + { + return RsAsn1ToPlainByteArray(SignSm3WithSm2Asn1Rs(msg, userId, privateKey)); + } + + /** + * @param msg + * @param userId + * @param privateKey + * @return rs in asn1 format + */ + + public static byte[] SignSm3WithSm2Asn1Rs(byte[] msg, byte[] userId, AsymmetricKeyParameter privateKey) + { + ISigner signer = SignerUtilities.GetSigner("SM3withSM2"); + signer.Init(true, new ParametersWithID(privateKey, userId)); + signer.BlockUpdate(msg, 0, msg.Length); + byte[] sig = signer.GenerateSignature(); + return sig; + } + + /** + * + * @param msg + * @param userId + * @param rs r||s,直接拼接byte数组的rs + * @param publicKey + * @return + */ + + public static bool VerifySm3WithSm2(byte[] msg, byte[] userId, byte[] rs, AsymmetricKeyParameter publicKey) + { + if (rs == null || msg == null || userId == null) return false; + if (rs.Length != RS_LEN * 2) return false; + return VerifySm3WithSm2Asn1Rs(msg, userId, RsPlainByteArrayToAsn1(rs), publicKey); + } + + /** + * + * @param msg + * @param userId + * @param rs in asn1 format + * @param publicKey + * @return + */ + + public static bool VerifySm3WithSm2Asn1Rs(byte[] msg, byte[] userId, byte[] sign, AsymmetricKeyParameter publicKey) + { + ISigner signer = SignerUtilities.GetSigner("SM3withSM2"); + signer.Init(false, new ParametersWithID(publicKey, userId)); + signer.BlockUpdate(msg, 0, msg.Length); + return signer.VerifySignature(sign); + } + + /** + * bc加解密使用旧标c1||c2||c3,此方法在加密后调用,将结果转化为c1||c3||c2 + * @param c1c2c3 + * @return + */ + + private static byte[] ChangeC1C2C3ToC1C3C2(byte[] c1c2c3) + { + int c1Len = (x9ECParameters.Curve.FieldSize + 7) / 8 * 2 + 1; //sm2p256v1的这个固定65。可看GMNamedCurves、ECCurve代码。 + const int c3Len = 32; //new SM3Digest().getDigestSize(); + byte[] result = new byte[c1c2c3.Length]; + Buffer.BlockCopy(c1c2c3, 0, result, 0, c1Len); //c1 + Buffer.BlockCopy(c1c2c3, c1c2c3.Length - c3Len, result, c1Len, c3Len); //c3 + Buffer.BlockCopy(c1c2c3, c1Len, result, c1Len + c3Len, c1c2c3.Length - c1Len - c3Len); //c2 + return result; + } + + /** + * bc加解密使用旧标c1||c3||c2,此方法在解密前调用,将密文转化为c1||c2||c3再去解密 + * @param c1c3c2 + * @return + */ + + private static byte[] ChangeC1C3C2ToC1C2C3(byte[] c1c3c2) + { + int c1Len = (x9ECParameters.Curve.FieldSize + 7) / 8 * 2 + 1; //sm2p256v1的这个固定65。可看GMNamedCurves、ECCurve代码。 + const int c3Len = 32; //new SM3Digest().GetDigestSize(); + byte[] result = new byte[c1c3c2.Length]; + Buffer.BlockCopy(c1c3c2, 0, result, 0, c1Len); //c1: 0->65 + Buffer.BlockCopy(c1c3c2, c1Len + c3Len, result, c1Len, c1c3c2.Length - c1Len - c3Len); //c2 + Buffer.BlockCopy(c1c3c2, c1Len, result, c1c3c2.Length - c3Len, c3Len); //c3 + return result; + } + + /** + * c1||c3||c2 + * @param data + * @param key + * @return + */ + + public static byte[] Sm2Decrypt(byte[] data, AsymmetricKeyParameter key) + { + return Sm2DecryptOld(ChangeC1C3C2ToC1C2C3(data), key); + } + + /** + * c1||c3||c2 + * @param data + * @param key + * @return + */ + + public static byte[] Sm2Encrypt(byte[] data, AsymmetricKeyParameter key) + { + return ChangeC1C2C3ToC1C3C2(Sm2EncryptOld(data, key)); + } + + /** + * c1||c2||c3 + * @param data + * @param key + * @return + */ + + public static byte[] Sm2EncryptOld(byte[] data, AsymmetricKeyParameter pubkey) + { + SM2Engine sm2Engine = new SM2Engine(); + sm2Engine.Init(true, new ParametersWithRandom(pubkey, new SecureRandom())); + return sm2Engine.ProcessBlock(data, 0, data.Length); + } + + /** + * c1||c2||c3 + * @param data + * @param key + * @return + */ + + public static byte[] Sm2DecryptOld(byte[] data, AsymmetricKeyParameter key) + { + SM2Engine sm2Engine = new SM2Engine(); + sm2Engine.Init(false, key); + return sm2Engine.ProcessBlock(data, 0, data.Length); + } + + /** + * @param bytes + * @return + */ + + public static byte[] Sm3(byte[] bytes) + { + SM3Digest digest = new(); + digest.BlockUpdate(bytes, 0, bytes.Length); + byte[] result = DigestUtilities.DoFinal(digest); + return result; + } + + private const int RS_LEN = 32; + + private static byte[] BigIntToFixexLengthBytes(BigInteger rOrS) + { + // for sm2p256v1, n is 00fffffffeffffffffffffffffffffffff7203df6b21c6052b53bbf40939d54123, + // r and s are the result of mod n, so they should be less than n and have length<=32 + byte[] rs = rOrS.ToByteArray(); + if (rs.Length == RS_LEN) return rs; + else if (rs.Length == RS_LEN + 1 && rs[0] == 0) return Arrays.CopyOfRange(rs, 1, RS_LEN + 1); + else if (rs.Length < RS_LEN) + { + byte[] result = new byte[RS_LEN]; + Arrays.Fill(result, (byte)0); + Buffer.BlockCopy(rs, 0, result, RS_LEN - rs.Length, rs.Length); + return result; + } + else + { + throw new ArgumentException("err rs: " + Hex.ToHexString(rs)); + } + } + + /** + * BC的SM3withSM2签名得到的结果的rs是asn1格式的,这个方法转化成直接拼接r||s + * @param rsDer rs in asn1 format + * @return sign result in plain byte array + */ + + private static byte[] RsAsn1ToPlainByteArray(byte[] rsDer) + { + Asn1Sequence seq = Asn1Sequence.GetInstance(rsDer); + byte[] r = BigIntToFixexLengthBytes(DerInteger.GetInstance(seq[0]).Value); + byte[] s = BigIntToFixexLengthBytes(DerInteger.GetInstance(seq[1]).Value); + byte[] result = new byte[RS_LEN * 2]; + Buffer.BlockCopy(r, 0, result, 0, r.Length); + Buffer.BlockCopy(s, 0, result, RS_LEN, s.Length); + return result; + } + + /** + * BC的SM3withSM2验签需要的rs是asn1格式的,这个方法将直接拼接r||s的字节数组转化成asn1格式 + * @param sign in plain byte array + * @return rs result in asn1 format + */ + + private static byte[] RsPlainByteArrayToAsn1(byte[] sign) + { + if (sign.Length != RS_LEN * 2) throw new ArgumentException("err rs. "); + BigInteger r = new BigInteger(1, Arrays.CopyOfRange(sign, 0, RS_LEN)); + BigInteger s = new BigInteger(1, Arrays.CopyOfRange(sign, RS_LEN, RS_LEN * 2)); + Asn1EncodableVector v = new Asn1EncodableVector + { + new DerInteger(r), + new DerInteger(s) + }; + + return new DerSequence(v).GetEncoded("DER"); + } + + // 生成公私匙对 + public static AsymmetricCipherKeyPair GenerateKeyPair() + { + ECKeyPairGenerator kpGen = new(); + kpGen.Init(new ECKeyGenerationParameters(ecDomainParameters, new SecureRandom())); + return kpGen.GenerateKeyPair(); + } + + public static ECPrivateKeyParameters GetPrivatekeyFromD(BigInteger d) + { + return new ECPrivateKeyParameters(d, ecDomainParameters); + } + + public static ECPublicKeyParameters GetPublickeyFromXY(BigInteger x, BigInteger y) + { + return new ECPublicKeyParameters(x9ECParameters.Curve.CreatePoint(x, y), ecDomainParameters); + } + + public static AsymmetricKeyParameter GetPublickeyFromX509File(FileInfo file) + { + FileStream fileStream = null; + try + { + //file.DirectoryName + "\\" + file.Name + fileStream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read); + X509Certificate certificate = new X509CertificateParser().ReadCertificate(fileStream); + return certificate.GetPublicKey(); + } + catch (Exception) + { + //log.Error(file.Name + "读取失败,异常:" + e); + } + finally + { + if (fileStream != null) + fileStream.Close(); + } + return null; + } + + public class Sm2Cert + { + public AsymmetricKeyParameter privateKey; + public AsymmetricKeyParameter publicKey; + public string certId; + } + + private static byte[] ToByteArray(int i) + { + byte[] byteArray = new byte[4]; + byteArray[0] = (byte)(i >> 24); + byteArray[1] = (byte)((i & 0xFFFFFF) >> 16); + byteArray[2] = (byte)((i & 0xFFFF) >> 8); + byteArray[3] = (byte)(i & 0xFF); + return byteArray; + } + + /** + * 字节数组拼接 + * + * @param params + * @return + */ + + private static byte[] Join(params byte[][] byteArrays) + { + List byteSource = new(); + for (int i = 0; i < byteArrays.Length; i++) + { + byteSource.AddRange(byteArrays[i]); + } + byte[] data = byteSource.ToArray(); + return data; + } + + /** + * 密钥派生函数 + * + * @param Z + * @param klen + * 生成klen字节数长度的密钥 + * @return + */ + + private static byte[] KDF(byte[] Z, int klen) + { + int ct = 1; + int end = (int)Math.Ceiling(klen * 1.0 / 32); + List byteSource = new(); + + for (int i = 1; i < end; i++) + { + byteSource.AddRange(Sm3(Join(Z, ToByteArray(ct)))); + ct++; + } + byte[] last = Sm3(Join(Z, ToByteArray(ct))); + if (klen % 32 == 0) + { + byteSource.AddRange(last); + } + else + byteSource.AddRange(Arrays.CopyOfRange(last, 0, klen % 32)); + return byteSource.ToArray(); + } + + public static byte[] Sm4DecryptCBC(byte[] keyBytes, byte[] cipher, byte[] iv, string algo) + { + if (keyBytes.Length != 16) throw new ArgumentException("err key length"); + if (cipher.Length % 16 != 0 && algo.Contains("NoPadding")) throw new ArgumentException("err data length"); + + KeyParameter key = ParameterUtilities.CreateKeyParameter("SM4", keyBytes); + IBufferedCipher c = CipherUtilities.GetCipher(algo); + if (iv == null) iv = ZeroIv(algo); + c.Init(false, new ParametersWithIV(key, iv)); + return c.DoFinal(cipher); + } + + public static byte[] Sm4EncryptCBC(byte[] keyBytes, byte[] plain, byte[] iv, string algo) + { + if (keyBytes.Length != 16) throw new ArgumentException("err key length"); + if (plain.Length % 16 != 0 && algo.Contains("NoPadding")) throw new ArgumentException("err data length"); + + KeyParameter key = ParameterUtilities.CreateKeyParameter("SM4", keyBytes); + IBufferedCipher c = CipherUtilities.GetCipher(algo); + if (iv == null) iv = ZeroIv(algo); + c.Init(true, new ParametersWithIV(key, iv)); + return c.DoFinal(plain); + } + + public static byte[] Sm4EncryptECB(byte[] keyBytes, byte[] plain, string algo) + { + if (keyBytes.Length != 16) throw new ArgumentException("err key length"); + //NoPadding 的情况下需要校验数据长度是16的倍数. + if (plain.Length % 16 != 0 && algo.Contains("NoPadding")) throw new ArgumentException("err data length"); + + KeyParameter key = ParameterUtilities.CreateKeyParameter("SM4", keyBytes); + IBufferedCipher c = CipherUtilities.GetCipher(algo); + c.Init(true, key); + return c.DoFinal(plain); + } + + public static byte[] Sm4DecryptECB(byte[] keyBytes, byte[] cipher, string algo) + { + if (keyBytes.Length != 16) throw new ArgumentException("err key length"); + if (cipher.Length % 16 != 0 && algo.Contains("NoPadding")) throw new ArgumentException("err data length"); + + KeyParameter key = ParameterUtilities.CreateKeyParameter("SM4", keyBytes); + IBufferedCipher c = CipherUtilities.GetCipher(algo); + c.Init(false, key); + return c.DoFinal(cipher); + } + + public const string SM4_ECB_NOPADDING = "SM4/ECB/NoPadding"; + public const string SM4_CBC_NOPADDING = "SM4/CBC/NoPadding"; + public const string SM4_ECB_PKCS7PADDING = "SM4/ECB/PKCS7Padding"; + public const string SM4_CBC_PKCS7PADDING = "SM4/CBC/PKCS7Padding"; + + /** + * cfca官网CSP沙箱导出的sm2文件 + * @param pem 二进制原文 + * @param pwd 密码 + * @return + */ + + public static Sm2Cert ReadSm2File(byte[] pem, string pwd) + { + Sm2Cert sm2Cert = new(); + + Asn1Sequence asn1Sequence = (Asn1Sequence)Asn1Object.FromByteArray(pem); + // ASN1Integer asn1Integer = (ASN1Integer) asn1Sequence.getObjectAt(0); //version=1 + Asn1Sequence priSeq = (Asn1Sequence)asn1Sequence[1];//private key + Asn1Sequence pubSeq = (Asn1Sequence)asn1Sequence[2];//public key and x509 cert + + // ASN1ObjectIdentifier sm2DataOid = (ASN1ObjectIdentifier) priSeq.getObjectAt(0); + // ASN1ObjectIdentifier sm4AlgOid = (ASN1ObjectIdentifier) priSeq.getObjectAt(1); + Asn1OctetString priKeyAsn1 = (Asn1OctetString)priSeq[2]; + byte[] key = KDF(System.Text.Encoding.UTF8.GetBytes(pwd), 32); + byte[] priKeyD = Sm4DecryptCBC(Arrays.CopyOfRange(key, 16, 32), + priKeyAsn1.GetOctets(), + Arrays.CopyOfRange(key, 0, 16), SM4_CBC_PKCS7PADDING); + sm2Cert.privateKey = GetPrivatekeyFromD(new BigInteger(1, priKeyD)); + // log.Info(Hex.toHexString(priKeyD)); + + // ASN1ObjectIdentifier sm2DataOidPub = (ASN1ObjectIdentifier) pubSeq.getObjectAt(0); + Asn1OctetString pubKeyX509 = (Asn1OctetString)pubSeq[1]; + X509Certificate x509 = new X509CertificateParser().ReadCertificate(pubKeyX509.GetOctets()); + sm2Cert.publicKey = x509.GetPublicKey(); + sm2Cert.certId = x509.SerialNumber.ToString(10); //这里转10进制,有啥其他进制要求的自己改改 + return sm2Cert; + } + + /** + * + * @param cert + * @return + */ + + public static Sm2Cert ReadSm2X509Cert(byte[] cert) + { + Sm2Cert sm2Cert = new(); + + X509Certificate x509 = new X509CertificateParser().ReadCertificate(cert); + sm2Cert.publicKey = x509.GetPublicKey(); + sm2Cert.certId = x509.SerialNumber.ToString(10); //这里转10进制,有啥其他进制要求的自己改改 + return sm2Cert; + } + + public static byte[] ZeroIv(string algo) + { + IBufferedCipher cipher = CipherUtilities.GetCipher(algo); + int blockSize = cipher.GetBlockSize(); + byte[] iv = new byte[blockSize]; + Arrays.Fill(iv, (byte)0); + return iv; + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Util/GM/GMUtil.cs b/yy-admin-master/YY.Admin.Core/Util/GM/GMUtil.cs new file mode 100644 index 0000000..71c98f7 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Util/GM/GMUtil.cs @@ -0,0 +1,147 @@ +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Utilities.Encoders; +using System.Text; +namespace YY.Admin.Core.Util +{ + /// + /// GM工具类 + /// + public class GMUtil + { + /// + /// SM2加密 + /// + /// + /// + /// + public static string SM2Encrypt(string publicKeyHex, string data_string) + { + // 如果是130位公钥,.NET使用的话,把开头的04截取掉 + if (publicKeyHex.Length == 130) + { + publicKeyHex = publicKeyHex.Substring(2, 128); + } + // 公钥X,前64位 + string x = publicKeyHex.Substring(0, 64); + // 公钥Y,后64位 + string y = publicKeyHex.Substring(64); + // 获取公钥对象 + AsymmetricKeyParameter publicKey1 = GM.GetPublickeyFromXY(new BigInteger(x, 16), new BigInteger(y, 16)); + // Sm2Encrypt: C1C3C2 + // Sm2EncryptOld: C1C2C3 + byte[] digestByte = GM.Sm2Encrypt(Encoding.UTF8.GetBytes(data_string), publicKey1); + string strSM2 = Hex.ToHexString(digestByte); + return strSM2; + } + + /// + /// SM2解密 + /// + /// + /// + /// + public static string SM2Decrypt(string privateKey_string, string encryptedData_string) + { + //return Encoding.Default.GetString(SM2Util.Decrypt(Hex.Decode(privateKey_string), Hex.Decode(encryptedData_string))); + if (!encryptedData_string.StartsWith("04")) + encryptedData_string = "04" + encryptedData_string; + BigInteger d = new(privateKey_string, 16); + // 先拿到私钥对象,用ECPrivateKeyParameters 或 AsymmetricKeyParameter 都可以 + // ECPrivateKeyParameters bcecPrivateKey = GmUtil.GetPrivatekeyFromD(d); + AsymmetricKeyParameter bcecPrivateKey = GM.GetPrivatekeyFromD(d); + byte[] byToDecrypt = Hex.Decode(encryptedData_string); + byte[] byDecrypted = GM.Sm2Decrypt(byToDecrypt, bcecPrivateKey); + string strDecrypted = Encoding.UTF8.GetString(byDecrypted); + return strDecrypted; + } + + /// + /// SM4加密(ECB) + /// + /// + /// + /// + public static string SM4EncryptECB(string key_string, string plainText) + { + byte[] key = Hex.Decode(key_string); + byte[] bs = GM.Sm4EncryptECB(key, Encoding.UTF8.GetBytes(plainText), GM.SM4_ECB_PKCS7PADDING);//NoPadding 的情况下需要校验数据长度是16的倍数. 使用 HandleSm4Padding 处理 + return Hex.ToHexString(bs); + } + + /// + /// SM4解密(ECB) + /// + /// + /// + /// + public static string SM4DecryptECB(string key_string, string cipherText) + { + byte[] key = Hex.Decode(key_string); + byte[] bs = GM.Sm4DecryptECB(key, Hex.Decode(cipherText), GM.SM4_ECB_PKCS7PADDING); + return Encoding.UTF8.GetString(bs); + } + + /// + /// SM4加密(CBC) + /// + /// + /// + /// + /// + public static string SM4EncryptCBC(string key_string, string iv_string, string plainText) + { + byte[] key = Hex.Decode(key_string); + byte[] iv = Hex.Decode(iv_string); + byte[] bs = GM.Sm4EncryptCBC(key, Encoding.UTF8.GetBytes(plainText), iv, GM.SM4_CBC_PKCS7PADDING); + return Hex.ToHexString(bs); + } + + /// + /// SM4解密(CBC) + /// + /// + /// + /// + /// + public static string SM4DecryptCBC(string key_string, string iv_string, string cipherText) + { + byte[] key = Hex.Decode(key_string); + byte[] iv = Hex.Decode(iv_string); + byte[] bs = GM.Sm4DecryptCBC(key, Hex.Decode(cipherText), iv, GM.SM4_CBC_PKCS7PADDING); + return Encoding.UTF8.GetString(bs); + } + + /// + /// 补足 16 进制字符串的 0 字符,返回不带 0x 的16进制字符串 + /// + /// + /// 1表示加密,0表示解密 + /// + private static byte[] HandleSm4Padding(byte[] input, int mode) + { + if (input == null) + { + return null; + } + byte[] ret = (byte[])null; + if (mode == 1) + { + int p = 16 - input.Length % 16; + ret = new byte[input.Length + p]; + Array.Copy(input, 0, ret, 0, input.Length); + for (int i = 0; i < p; i++) + { + ret[input.Length + i] = (byte)p; + } + } + else + { + int p = input[input.Length - 1]; + ret = new byte[input.Length - p]; + Array.Copy(input, 0, ret, 0, input.Length - p); + } + return ret; + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Util/JeecgPasswordUtil.cs b/yy-admin-master/YY.Admin.Core/Util/JeecgPasswordUtil.cs new file mode 100644 index 0000000..1971afc --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Util/JeecgPasswordUtil.cs @@ -0,0 +1,74 @@ +using System.Globalization; +using System.Text; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Modes; +using Org.BouncyCastle.Crypto.Paddings; +using Org.BouncyCastle.Crypto.Parameters; + +namespace YY.Admin.Core.Util; + +/// +/// 与 JeecgBoot PasswordUtil.encrypt(username, plainPassword, salt) 对齐: +/// 算法 PBEWithMD5AndDES,迭代 1000 次;明文为 用户名 UTF-8,口令为明文密码;盐为字符串转字节(与 Java 一致建议使用 UTF-8)。 +/// +public static class JeecgPasswordUtil +{ + /// 与 Java PasswordUtil 中 ITERATIONCOUNT 一致 + public const int IterationCount = 1000; + + /// + /// 生成与 Jeecg 数据库 sys_user.password 一致的十六进制密文(小写)。 + /// + /// 登录账号(Jeecg 的 username) + /// 明文密码 + /// Jeecg 用户表 salt 字段字符串 + /// 盐字节编码;Jeecg 服务端 JVM 多为 UTF-8,建议固定 UTF-8 + public static string Encrypt(string username, string plainPassword, string salt, Encoding? saltEncoding = null) + { + saltEncoding ??= Encoding.UTF8; + byte[] saltBytes = saltEncoding.GetBytes(salt); + byte[] plainBytes = Encoding.UTF8.GetBytes(username); + + // PKCS#5 Scheme1(MD5)+ DES/CBC/PKCS7,与 Java JCE PBEWithMD5AndDES 一致 + var generator = new Pkcs5S1ParametersGenerator(new MD5Digest()); + generator.Init(PbeParametersGenerator.Pkcs5PasswordToBytes(plainPassword.ToCharArray()), saltBytes, IterationCount); + + // 派生 DES 密钥 + IV(各 64 bit) + ParametersWithIV keyIv = (ParametersWithIV)generator.GenerateDerivedParameters("DES", 64, 64); + + var cipher = new PaddedBufferedBlockCipher(new CbcBlockCipher(new DesEngine()), new Pkcs7Padding()); + cipher.Init(true, keyIv); + + byte[] output = new byte[cipher.GetOutputSize(plainBytes.Length)]; + int len = cipher.ProcessBytes(plainBytes, 0, plainBytes.Length, output, 0); + len += cipher.DoFinal(output, len); + if (len < output.Length) + Array.Resize(ref output, len); + + return BytesToHexLower(output); + } + + /// + /// 校验明文密码是否与 Jeecg 存储的十六进制密文一致。 + /// + public static bool Verify(string username, string plainPassword, string salt, string storedPasswordHex, Encoding? saltEncoding = null) + { + if (string.IsNullOrEmpty(storedPasswordHex)) + return false; + string computed = Encrypt(username, plainPassword, salt, saltEncoding); + return string.Equals(computed, storedPasswordHex, StringComparison.OrdinalIgnoreCase); + } + + private static string BytesToHexLower(byte[] src) + { + if (src == null || src.Length == 0) + return string.Empty; + var sb = new StringBuilder(src.Length * 2); + foreach (byte b in src) + sb.Append(b.ToString("x2", CultureInfo.InvariantCulture)); + return sb.ToString(); + } +} diff --git a/yy-admin-master/YY.Admin.Core/Util/PageFilter/BaseFilter.cs b/yy-admin-master/YY.Admin.Core/Util/PageFilter/BaseFilter.cs new file mode 100644 index 0000000..526530d --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Util/PageFilter/BaseFilter.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core +{ + /// + /// 模糊查询条件 + /// + public class Search + { + /// + /// 字段名称集合 + /// + public List Fields { get; set; } + + /// + /// 关键字 + /// + public string? Keyword { get; set; } + } + + /// + /// 筛选过滤条件 + /// + public class Filter + { + /// + /// 过滤条件 + /// + public FilterLogicEnum? Logic { get; set; } + + /// + /// 筛选过滤条件子项 + /// + public IEnumerable? Filters { get; set; } + + /// + /// 字段名称 + /// + public string? Field { get; set; } + + /// + /// 逻辑运算符 + /// + public FilterOperatorEnum? Operator { get; set; } + + /// + /// 字段值 + /// + public object? Value { get; set; } + } + + /// + /// 过滤条件基类 + /// + public abstract class BaseFilter + { + /// + /// 模糊查询条件 + /// + public Search? Search { get; set; } + + /// + /// 模糊查询关键字 + /// + public string? Keyword { get; set; } + + /// + /// 筛选过滤条件 + /// + public Filter? Filter { get; set; } + } +} diff --git a/yy-admin-master/YY.Admin.Core/Util/PageFilter/BasePageInput.cs b/yy-admin-master/YY.Admin.Core/Util/PageFilter/BasePageInput.cs new file mode 100644 index 0000000..d74b8ba --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Util/PageFilter/BasePageInput.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; + +namespace YY.Admin.Core +{ + /// + /// 全局分页查询输入参数 + /// + public class BasePageInput : BaseFilter + { + /// + /// 当前页码 + /// + public virtual int Page { get; set; } = 1; + + /// + /// 页码容量 + /// + //[Range(0, 100, ErrorMessage = "页码容量超过最大限制")] + public virtual int PageSize { get; set; } = 20; + + /// + /// 排序字段 + /// + public virtual string Field { get; set; } + + /// + /// 排序方向 + /// + public virtual string Order { get; set; } + + /// + /// 降序排序 + /// + public virtual string DescStr { get; set; } = "descending"; + } +} diff --git a/yy-admin-master/YY.Admin.Core/Util/PageFilter/PagedRequestBase.cs b/yy-admin-master/YY.Admin.Core/Util/PageFilter/PagedRequestBase.cs new file mode 100644 index 0000000..00ab0b6 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Util/PageFilter/PagedRequestBase.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Core +{ + /// + /// 分页请求基类 + /// + public class PagedRequestBase : BindableBase + { + /// + /// 页码,从1开始 + /// + public int Page { get; set; } = 1; + + /// + /// 页容量 + /// + public int PageSize { get; set; } = 10; + + /// + /// 搜索关键词 + /// + public string SearchKey { get; set; } = string.Empty; + } + ///// + ///// 用户分页查询输入 + ///// + //public class PageUserInput : PagedRequestBase + //{ + // /// + // /// 机构ID + // /// + // public long OrgId { get; set; } + + // /// + // /// 租户ID + // /// + // public long TenantId { get; set; } + + // /// + // /// 账号 + // /// + // public string Account { get; set; } = string.Empty; + + // /// + // /// 真实姓名 + // /// + // public string RealName { get; set; } = string.Empty; + + // /// + // /// 职位名称 + // /// + // public string PosName { get; set; } = string.Empty; + + // /// + // /// 电话 + // /// + // public string Phone { get; set; } = string.Empty; + //} +} diff --git a/yy-admin-master/YY.Admin.Core/Util/SM/Cipher.cs b/yy-admin-master/YY.Admin.Core/Util/SM/Cipher.cs new file mode 100644 index 0000000..934e70e --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Util/SM/Cipher.cs @@ -0,0 +1,152 @@ + +// + +// + +// + + + + +//using Org.BouncyCastle.Crypto; +//using Org.BouncyCastle.Crypto.Parameters; +//using Org.BouncyCastle.Math; +//using Org.BouncyCastle.Math.EC; +//extern alias OldBC; +//extern alias NewBC; + +//// 使用时通过别名限定 +//OldBC::BouncyCastle.Crypto.ECPoint oldEcPoint; +//NewBC::BouncyCastle.Cryptography.ECPoint newEcPoint; + +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Math.EC; + +namespace YY.Admin.Core.Util; + +public class Cipher +{ + private int ct; + private ECPoint p2; + private SM3Digest sm3keybase; + private SM3Digest sm3c3; + private readonly byte[] key; + private byte keyOff; + + public Cipher() + { + ct = 1; + key = new byte[32]; + keyOff = 0; + } + + public static byte[] ByteConvert32Bytes(BigInteger n) + { + if (n == null) + return null; + + byte[] tmpd; + if (n.ToByteArray().Length == 33) + { + tmpd = new byte[32]; + Array.Copy(n.ToByteArray(), 1, tmpd, 0, 32); + } + else if (n.ToByteArray().Length == 32) + { + tmpd = n.ToByteArray(); + } + else + { + tmpd = new byte[32]; + for (int i = 0; i < 32 - n.ToByteArray().Length; i++) + { + tmpd[i] = 0; + } + Array.Copy(n.ToByteArray(), 0, tmpd, 32 - n.ToByteArray().Length, n.ToByteArray().Length); + } + return tmpd; + } + + private void Reset() + { + sm3keybase = new SM3Digest(); + sm3c3 = new SM3Digest(); + + byte[] p = ByteConvert32Bytes(p2.Normalize().XCoord.ToBigInteger()); + sm3keybase.BlockUpdate(p, 0, p.Length); + sm3c3.BlockUpdate(p, 0, p.Length); + + p = ByteConvert32Bytes(p2.Normalize().YCoord.ToBigInteger()); + sm3keybase.BlockUpdate(p, 0, p.Length); + ct = 1; + NextKey(); + } + + private void NextKey() + { + var sm3keycur = new SM3Digest(this.sm3keybase); + sm3keycur.Update((byte)(ct >> 24 & 0xff)); + sm3keycur.Update((byte)(ct >> 16 & 0xff)); + sm3keycur.Update((byte)(ct >> 8 & 0xff)); + sm3keycur.Update((byte)(ct & 0xff)); + sm3keycur.DoFinal(key, 0); + keyOff = 0; + ct++; + } + + public ECPoint Init_enc(SM2 sm2, ECPoint userKey) + { + AsymmetricCipherKeyPair key = sm2.ecc_key_pair_generator.GenerateKeyPair(); + ECPrivateKeyParameters ecpriv = (ECPrivateKeyParameters)key.Private; + ECPublicKeyParameters ecpub = (ECPublicKeyParameters)key.Public; + BigInteger k = ecpriv.D; + ECPoint c1 = ecpub.Q; + p2 = userKey.Multiply(k); + Reset(); + return c1; + } + + public void Encrypt(byte[] data) + { + sm3c3.BlockUpdate(data, 0, data.Length); + for (int i = 0; i < data.Length; i++) + { + if (keyOff == key.Length) + { + NextKey(); + } + data[i] ^= key[keyOff++]; + } + } + + public void Init_dec(BigInteger userD, ECPoint c1) + { + p2 = c1.Multiply(userD); + Reset(); + } + + public void Decrypt(byte[] data) + { + for (int i = 0; i < data.Length; i++) + { + if (keyOff == key.Length) + { + NextKey(); + } + data[i] ^= key[keyOff++]; + } + + sm3c3.BlockUpdate(data, 0, data.Length); + } + + public void Dofinal(byte[] c3) + { + byte[] p = ByteConvert32Bytes(p2.Normalize().YCoord.ToBigInteger()); + sm3c3.BlockUpdate(p, 0, p.Length); + sm3c3.DoFinal(c3, 0); + Reset(); + } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Util/SM/SM2.cs b/yy-admin-master/YY.Admin.Core/Util/SM/SM2.cs new file mode 100644 index 0000000..c82a2c4 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Util/SM/SM2.cs @@ -0,0 +1,120 @@ + +// + +// + +// + + + +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Math.EC; +using Org.BouncyCastle.Security; + +namespace YY.Admin.Core.Util; + +public class SM2 +{ + public static SM2 Instance + { + get + { + return new SM2(); + } + } + + public static SM2 InstanceTest + { + get + { + return new SM2(); + } + } + + public static readonly string[] sm2_param = { + "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFF",// p,0 + "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF00000000FFFFFFFFFFFFFFFC",// a,1 + "28E9FA9E9D9F5E344D5A9E4BCF6509A7F39789F515AB8F92DDBCBD414D940E93",// b,2 + "FFFFFFFEFFFFFFFFFFFFFFFFFFFFFFFF7203DF6B21C6052B53BBF40939D54123",// n,3 + "32C4AE2C1F1981195F9904466A39C9948FE30BBFF2660BE1715A4589334C74C7",// gx,4 + "BC3736A2F4F6779C59BDCEE36B692153D0A9877CC62A474002DF32E52139F0A0" // gy,5 + }; + + public string[] ecc_param = sm2_param; + + public readonly BigInteger ecc_p; + public readonly BigInteger ecc_a; + public readonly BigInteger ecc_b; + public readonly BigInteger ecc_n; + public readonly BigInteger ecc_gx; + public readonly BigInteger ecc_gy; + + public readonly ECCurve ecc_curve; + public readonly ECPoint ecc_point_g; + + public readonly ECDomainParameters ecc_bc_spec; + + public readonly ECKeyPairGenerator ecc_key_pair_generator; + + private SM2() + { + ecc_param = sm2_param; + + ecc_p = new BigInteger(ecc_param[0], 16); + ecc_a = new BigInteger(ecc_param[1], 16); + ecc_b = new BigInteger(ecc_param[2], 16); + ecc_n = new BigInteger(ecc_param[3], 16); + ecc_gx = new BigInteger(ecc_param[4], 16); + ecc_gy = new BigInteger(ecc_param[5], 16); + + ecc_curve = new FpCurve(ecc_p, ecc_a, ecc_b, null, null); + ecc_point_g = ecc_curve.CreatePoint(ecc_gx, ecc_gy); + + ecc_bc_spec = new ECDomainParameters(ecc_curve, ecc_point_g, ecc_n); + + ECKeyGenerationParameters ecc_ecgenparam; + ecc_ecgenparam = new ECKeyGenerationParameters(ecc_bc_spec, new SecureRandom()); + + ecc_key_pair_generator = new ECKeyPairGenerator(); + ecc_key_pair_generator.Init(ecc_ecgenparam); + } + + public virtual byte[] Sm2GetZ(byte[] userId, ECPoint userKey) + { + var sm3 = new SM3Digest(); + byte[] p; + // userId length + int len = userId.Length * 8; + sm3.Update((byte)(len >> 8 & 0x00ff)); + sm3.Update((byte)(len & 0x00ff)); + + // userId + sm3.BlockUpdate(userId, 0, userId.Length); + + // a,b + p = ecc_a.ToByteArray(); + sm3.BlockUpdate(p, 0, p.Length); + p = ecc_b.ToByteArray(); + sm3.BlockUpdate(p, 0, p.Length); + // gx,gy + p = ecc_gx.ToByteArray(); + sm3.BlockUpdate(p, 0, p.Length); + p = ecc_gy.ToByteArray(); + sm3.BlockUpdate(p, 0, p.Length); + + // x,y + p = userKey.AffineXCoord.ToBigInteger().ToByteArray(); + sm3.BlockUpdate(p, 0, p.Length); + p = userKey.AffineYCoord.ToBigInteger().ToByteArray(); + sm3.BlockUpdate(p, 0, p.Length); + + // Z + byte[] md = new byte[sm3.GetDigestSize()]; + sm3.DoFinal(md, 0); + + return md; + } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Util/SM/SM2Util.cs b/yy-admin-master/YY.Admin.Core/Util/SM/SM2Util.cs new file mode 100644 index 0000000..be3ef0d --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Util/SM/SM2Util.cs @@ -0,0 +1,177 @@ + +// + +// + +// + + + +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Math.EC; +using Org.BouncyCastle.Utilities.Encoders; +using System.Text; + +namespace YY.Admin.Core.Util; + +/// +/// SM2工具类 +/// +public class SM2Util +{ + /// + /// 加密 + /// + /// + /// + /// + public static string Encrypt(string publicKey_string, string data_string) + { + var publicKey = Hex.Decode(publicKey_string); + var data = Encoding.UTF8.GetBytes(data_string); + return Encrypt(publicKey, data); + } + + /// + /// 解密 + /// + /// + /// + /// + public static string Decrypt(string privateKey_string, string encryptedData_string) + { + var privateKey = Hex.Decode(privateKey_string); + var encryptedData = Hex.Decode(encryptedData_string); + var de_str = SM2Util.Decrypt(privateKey, encryptedData); + string plainText = Encoding.UTF8.GetString(de_str); + return plainText; + } + + public static void GenerateKeyPair() + { + SM2 sm2 = SM2.Instance; + AsymmetricCipherKeyPair key = sm2.ecc_key_pair_generator.GenerateKeyPair(); + ECPrivateKeyParameters ecpriv = (ECPrivateKeyParameters)key.Private; + ECPublicKeyParameters ecpub = (ECPublicKeyParameters)key.Public; + BigInteger privateKey = ecpriv.D; + ECPoint publicKey = ecpub.Q; + + Console.Out.WriteLine("公钥: " + Encoding.ASCII.GetString(Hex.Encode(publicKey.GetEncoded())).ToUpper()); + Console.Out.WriteLine("私钥: " + Encoding.ASCII.GetString(Hex.Encode(privateKey.ToByteArray())).ToUpper()); + } + + public static string Encrypt(byte[] publicKey, byte[] data) + { + if (null == publicKey || publicKey.Length == 0) + { + return null; + } + if (data == null || data.Length == 0) + { + return null; + } + + byte[] source = new byte[data.Length]; + Array.Copy(data, 0, source, 0, data.Length); + + var cipher = new Cipher(); + SM2 sm2 = SM2.Instance; + + ECPoint userKey = sm2.ecc_curve.DecodePoint(publicKey); + + ECPoint c1 = cipher.Init_enc(sm2, userKey); + cipher.Encrypt(source); + + byte[] c3 = new byte[32]; + cipher.Dofinal(c3); + + string sc1 = Encoding.ASCII.GetString(Hex.Encode(c1.GetEncoded())); + string sc2 = Encoding.ASCII.GetString(Hex.Encode(source)); + string sc3 = Encoding.ASCII.GetString(Hex.Encode(c3)); + + return (sc1 + sc2 + sc3).ToUpper(); + } + + public static byte[] Decrypt(byte[] privateKey, byte[] encryptedData) + { + if (null == privateKey || privateKey.Length == 0) + { + return null; + } + if (encryptedData == null || encryptedData.Length == 0) + { + return null; + } + + string data = Encoding.ASCII.GetString(Hex.Encode(encryptedData)); + + byte[] c1Bytes = Hex.Decode(Encoding.ASCII.GetBytes(data.Substring(0, 130))); + int c2Len = encryptedData.Length - 97; + byte[] c2 = Hex.Decode(Encoding.ASCII.GetBytes(data.Substring(130, 2 * c2Len))); + byte[] c3 = Hex.Decode(Encoding.ASCII.GetBytes(data.Substring(130 + 2 * c2Len, 64))); + + SM2 sm2 = SM2.Instance; + var userD = new BigInteger(1, privateKey); + + ECPoint c1 = sm2.ecc_curve.DecodePoint(c1Bytes); + var cipher = new Cipher(); + cipher.Init_dec(userD, c1); + cipher.Decrypt(c2); + cipher.Dofinal(c3); + + return c2; + } + + //[STAThread] + //public static void Main() + //{ + // GenerateKeyPair(); + + // String plainText = "ererfeiisgod"; + // byte[] sourceData = Encoding.Default.GetBytes(plainText); + + // //下面的秘钥可以使用generateKeyPair()生成的秘钥内容 + // // 国密规范正式私钥 + // String prik = "3690655E33D5EA3D9A4AE1A1ADD766FDEA045CDEAA43A9206FB8C430CEFE0D94"; + // // 国密规范正式公钥 + // String pubk = "04F6E0C3345AE42B51E06BF50B98834988D54EBC7460FE135A48171BC0629EAE205EEDE253A530608178A98F1E19BB737302813BA39ED3FA3C51639D7A20C7391A"; + + // System.Console.Out.WriteLine("加密: "); + // String cipherText = SM2Utils.Encrypt(Hex.Decode(pubk), sourceData); + // System.Console.Out.WriteLine(cipherText); + // System.Console.Out.WriteLine("解密: "); + // plainText = Encoding.Default.GetString(SM2Utils.Decrypt(Hex.Decode(prik), Hex.Decode(cipherText))); + // System.Console.Out.WriteLine(plainText); + + // Console.ReadLine(); + //} + + /// + /// SM2加密 + /// + /// 明文 + /// 密文 + public static String 加密(String plainText) + { + // 国密规范正式公钥 + String pubk = "04F6E0C3345AE42B51E06BF50B98834988D54EBC7460FE135A48171BC0629EAE205EEDE253A530608178A98F1E19BB737302813BA39ED3FA3C51639D7A20C7391A"; + byte[] sourceData = Encoding.Default.GetBytes(plainText); + String cipherText = SM2Util.Encrypt(Hex.Decode(pubk), sourceData); + return cipherText; + } + + /// + /// SM2解密 + /// + /// 密文 + /// 明文 + public static string 解密(String cipherText) + { + // 国密规范正式私钥 + String prik = "3690655E33D5EA3D9A4AE1A1ADD766FDEA045CDEAA43A9206FB8C430CEFE0D94"; + String plainText = Encoding.Default.GetString(SM2Util.Decrypt(Hex.Decode(prik), Hex.Decode(cipherText))); + return plainText; + } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin.Core/Util/StringUtil.cs b/yy-admin-master/YY.Admin.Core/Util/StringUtil.cs new file mode 100644 index 0000000..d4edc43 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/Util/StringUtil.cs @@ -0,0 +1,28 @@ +using System.Globalization; + +namespace YY.Admin.Core.Util +{ + public class StringUtil + { + /// + /// HTML实体编码转换为C#/WPF支持的Unicode转义格式 + /// + /// + /// + public static string ConvertHtmlEntityToUnicode(string htmlEntity) + { + if (string.IsNullOrEmpty(htmlEntity)) + return string.Empty; + + if (htmlEntity.StartsWith("&#x") && htmlEntity.EndsWith(";")) + { + string hexCode = htmlEntity.Substring(3, htmlEntity.Length - 4); + if (int.TryParse(hexCode, NumberStyles.HexNumber, null, out int unicodeValue)) + { + return char.ConvertFromUtf32(unicodeValue); + } + } + return htmlEntity; + } + } +} diff --git a/yy-admin-master/YY.Admin.Core/YY.Admin.Core.csproj b/yy-admin-master/YY.Admin.Core/YY.Admin.Core.csproj new file mode 100644 index 0000000..32fbc61 --- /dev/null +++ b/yy-admin-master/YY.Admin.Core/YY.Admin.Core.csproj @@ -0,0 +1,50 @@ + + + + net8.0-windows10.0.19041 + enable + true + enable + preview + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + libs\HandyControl.dll + + + diff --git a/yy-admin-master/YY.Admin.Core/libs/HandyControl.dll b/yy-admin-master/YY.Admin.Core/libs/HandyControl.dll new file mode 100644 index 0000000..75235b6 Binary files /dev/null and b/yy-admin-master/YY.Admin.Core/libs/HandyControl.dll differ diff --git a/yy-admin-master/YY.Admin.Entry.sln b/yy-admin-master/YY.Admin.Entry.sln new file mode 100644 index 0000000..0effa0b --- /dev/null +++ b/yy-admin-master/YY.Admin.Entry.sln @@ -0,0 +1,36 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35931.197 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YY.Admin", "YY.Admin\YY.Admin.csproj", "{11B4A9C2-E1C1-4F3C-99E2-D104C82E5C65}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YY.Admin.Core", "YY.Admin.Core\YY.Admin.Core.csproj", "{4E232859-BCDE-4A6D-902B-EDAE201EC511}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YY.Admin.Services", "YY.Admin.Services\YY.Admin.Services.csproj", "{27916A92-94E8-423B-AFB5-CB053EBC3BA1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {11B4A9C2-E1C1-4F3C-99E2-D104C82E5C65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11B4A9C2-E1C1-4F3C-99E2-D104C82E5C65}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11B4A9C2-E1C1-4F3C-99E2-D104C82E5C65}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11B4A9C2-E1C1-4F3C-99E2-D104C82E5C65}.Release|Any CPU.Build.0 = Release|Any CPU + {4E232859-BCDE-4A6D-902B-EDAE201EC511}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E232859-BCDE-4A6D-902B-EDAE201EC511}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E232859-BCDE-4A6D-902B-EDAE201EC511}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E232859-BCDE-4A6D-902B-EDAE201EC511}.Release|Any CPU.Build.0 = Release|Any CPU + {27916A92-94E8-423B-AFB5-CB053EBC3BA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {27916A92-94E8-423B-AFB5-CB053EBC3BA1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {27916A92-94E8-423B-AFB5-CB053EBC3BA1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {27916A92-94E8-423B-AFB5-CB053EBC3BA1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DDF402D0-A7AC-421D-9AAF-AE9ACBD137C3} + EndGlobalSection +EndGlobal diff --git a/yy-admin-master/YY.Admin.Services/Configuration/appsettings.json b/yy-admin-master/YY.Admin.Services/Configuration/appsettings.json new file mode 100644 index 0000000..9abbb4e --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Configuration/appsettings.json @@ -0,0 +1,96 @@ +{ + // 缓存配置 + "Cache": { + "Prefix": "yyadmin_", // 全局缓存前缀 + "CacheType": "Memory", // Memory、Redis + "Redis": { + "Configuration": "server=localhost;db=2;password=123456;", // Redis连接字符串 + "Prefix": "yyadmin_", // Redis前缀(目前没用) + "MaxMessageSize": "1048576" // 最大消息大小 默认1024 * 1024 + } + }, + // 数据库连接字符串参考地址:https://www.connectionstrings.com/ + "DbConnection": { + "EnableConsoleSql": true, // 启用控制台打印SQL + "ConnectionConfigs": [ + { + "ConfigId": "1300000000001", // 默认库标识-禁止修改 + "DbType": "Sqlite", // MySql、SqlServer、Sqlite、Oracle、PostgreSQL、Dm、Kdbndp、Oscar、MySqlConnector、Access、OpenGauss、QuestDB、HG、ClickHouse、GBase、Odbc、Custom + "DbNickName": "系统库", + //"ConnectionString": "DataSource=./Admin.NET.db", // Sqlite + "ConnectionString": "DataSource=./Admin.NET.db", // Sqlite + //"ConnectionString": "PORT=5432;DATABASE=xxx;HOST=localhost;PASSWORD=xxx;USER ID=xxx", // PostgreSQL + //"ConnectionString": "server= ;port=;database=;user=;password=;CharSet=utf8;sslmode=none;max pool size=1000;", // MySql, + "DbSettings": { + "EnableInitDb": true, // 启用库初始化(若实体没有变化建议关闭) + "EnableInitView": false, // 启用视图初始化(若实体和视图没有变化建议关闭) + "EnableDiffLog": false, // 启用库表差异日志 + "EnableUnderLine": true, // 启用驼峰转下划线 + "EnableConnEncrypt": false // 启用数据库连接串加密(国密SM2加解密) + }, + "TableSettings": { + "EnableInitTable": true, // 启用表初始化(若实体没有变化建议关闭) + "EnableIncreTable": false // 启用表增量更新(只更新贴了特性[IncreTable]的实体表) + }, + "SeedSettings": { + "EnableInitSeed": true, // 启用种子初始化(若种子没有变化建议关闭) + "EnableIncreSeed": false // 启用种子增量更新(只更新贴了特性[IncreSeed]的种子表) + } + }, + { + "ConfigId": "Slave", // 从数据库 + "DbType": "Sqlite", // 数据库类型 + "DbNickName": "业务库", + "ConnectionString": "Data Source=./Slave.db", // Sqlite + "DbSettings": { + "EnableInitDb": true, // 启用库初始化(若实体没有变化建议关闭) + "EnableInitView": false, // 启用视图初始化(若实体和视图没有变化建议关闭) + "EnableDiffLog": false, // 启用库表差异日志 + "EnableUnderLine": true, // 启用驼峰转下划线 + "EnableConnEncrypt": false // 启用数据库连接串加密(国密SM2加解密) + }, + "TableSettings": { + "EnableInitTable": true, // 启用表初始化(若实体没有变化建议关闭) + "EnableIncreTable": false // 启用表增量更新(只更新贴了特性[IncreTable]的实体表) + }, + "SeedSettings": { + "EnableInitSeed": true, // 启用种子初始化(若种子没有变化建议关闭) + "EnableIncreSeed": false // 启用种子增量更新(只更新贴了特性[IncreSeed]的种子表) + } + } + ] + }, + "AutoUpdate": { + "RemoteConfigUrl": "http://14.103.155.227:8083/updates/version.xml" //更新文件地址 + }, + "JeecgIntegration": { + "Enabled": true, // 是否启用Jeecg(可与本地并存;见 PreferLocalLogin 决定先后顺序) + "PreferLocalLogin": true, // true:先校验本地库再尝试 Jeecg(工控脱网时无需等待 MES,直接用本地账号登录) + "FallbackToLocal": true, // false:仅 MES 登录,失败即结束(不推荐工控场景) + "BaseUrl": "http://127.0.0.1:8080/jeecg-boot", // Jeecg后端地址(按实际环境修改) + "LoginPath": "/sys/login", // Jeecg登录接口 + "UserInfoPath": "/sys/user/getUserInfo", // Jeecg用户信息接口 + "UserListPath": "/sys/user/scada/queryUser", // Jeecg 用户列表(SCADA:分页 + updatedAfter 增量,见文档) + "ScadaUserPageSize": 500, // SCADA queryUser 每页条数,最大 1000 + "ScadaUserIncludeDetail": false, // true 时含部门/公司/租户明细,耗时更高 + "ScadaUseUpdatedAfter": true, // 后台/定时同步:有水位时用 updatedAfter 增量。登录触发的 SCADA 同步固定全量分页,避免只拉到少数变更用户 + "TenantListPath": "/sys/tenant/list", // Jeecg租户分页接口 + "UserPermissionPath": "/sys/permission/getUserPermissionByToken", // Jeecg当前用户菜单与按钮权限接口 + "ResetLocalIdentityDataOnJeecgLogin": false, // true 时 Jeecg 登录成功会清空本地用户/角色等(易丢失种子账号导致脱网无法登录)。工控独立运行建议保持 false + "AutoProvisionLocalUser": true, // Jeecg认证成功后本地不存在账号时自动创建 + "SyncUserProfileToLocal": true, // 每次登录时同步Jeecg用户基础信息到本地 + "SyncAllUsersOnJeecgLogin": true, // true:Jeecg 登录成功后拉取用户列表并写入本地(关闭则不会同步用户表) + "UseJeecgUserIdAsLocalPrimaryKey": true, // true 时本地 sys_user.id 与 Jeecg getUserInfo 的 id 一致(雪花 long) + "UserSyncSkipUnchanged": true, // 与Jeecg updateTime 一致时跳过写库,减少重复保存 + "UserListUseUpdateTimeQuery": false, // 仅非 SCADA 的 /sys/user/list:true 时附带 updateTime_begin。SCADA 增量由 ScadaUseUpdatedAfter 控制 + "IncrementalSyncOverlapMinutes": 2, // 增量时间窗口重叠,避免时钟误差漏数据 + "BackgroundSyncIntervalMinutes": 30, // 主窗口在线时定时增量同步间隔(分钟) + "AnonymousMode": true, // 工控机免密模式:优先走免登录接口与匿名WebSocket通道 + "WebSocketUrl": "", // 可选:Jeecg 或自建推送地址;免密模式下若误配到 /ws/device/websocket 会自动切回 /websocket/scada-sync + "WebSocketPath": "/websocket/scada-sync", // 匿名实时推送通道 + "WebSocketInactivityReconnectSeconds": 0, // 0=关闭空闲强制重连;仅保留WS心跳保活,避免重连窗口丢推送 + "DefaultTenantId": 1002, // 自动创建本地用户时使用的默认租户ID + "Captcha": "", // 如启用登录验证码,在此传入验证码 + "CheckKey": "" // 如启用登录验证码,在此传入验证码key + } +} diff --git a/yy-admin-master/YY.Admin.Services/GlobalUsings.cs b/yy-admin-master/YY.Admin.Services/GlobalUsings.cs new file mode 100644 index 0000000..88bf7a9 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/GlobalUsings.cs @@ -0,0 +1,6 @@ +global using YY.Admin.Core.Const; +global using YY.Admin.Core.SqlSugar; +global using YY.Admin.Core; +global using static YY.Admin.Core.SysUserEvents; + + diff --git a/yy-admin-master/YY.Admin.Services/Service/Auth/Dto/LoginInput.cs b/yy-admin-master/YY.Admin.Services/Service/Auth/Dto/LoginInput.cs new file mode 100644 index 0000000..cf5e656 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Auth/Dto/LoginInput.cs @@ -0,0 +1,9 @@ +namespace YY.Admin.Services; + public class LoginInput + { + public string Username { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public long TenantId { get; set; } + public bool RememberMe { get; set; } + } + diff --git a/yy-admin-master/YY.Admin.Services/Service/Auth/Dto/LoginOutput.cs b/yy-admin-master/YY.Admin.Services/Service/Auth/Dto/LoginOutput.cs new file mode 100644 index 0000000..783cb5a --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Auth/Dto/LoginOutput.cs @@ -0,0 +1,11 @@ +namespace YY.Admin.Services +{ + public class LoginOutput + { + public bool Success { get; set; } + public string Message { get; set; } = string.Empty; + public SysUser? User { get; set; } + public UserToken Token { get; set; } + + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Auth/ISysAuthService.cs b/yy-admin-master/YY.Admin.Services/Service/Auth/ISysAuthService.cs new file mode 100644 index 0000000..9f30b09 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Auth/ISysAuthService.cs @@ -0,0 +1,26 @@ + +namespace YY.Admin.Services.Service.Auth +{ + public interface ISysAuthService + { + Task LoginAsync(LoginInput request); + void LogoutAsync(); + Task IsAuthenticatedAsync(); + bool ValidateToken(string accessToken); + Task RefreshToken(string? accessToken); + SysUser? CurrentUser { get; } + event EventHandler UserChanged; + + Task UpdateUserLoginInfoAsync(SysUser sysUser); + + /// + /// 使用缓存的 Jeecg Token 做用户增量同步(定时/WebSocket 调用,断网恢复后自动补拉) + /// + Task TryBackgroundSyncJeecgUsersAsync(CancellationToken cancellationToken = default); + + /// + /// 登录页一键同步 Jeecg 用户到本地 SQLite(依赖 SCADA 免登录 queryUser,无需先登录) + /// + Task<(bool Success, string Message)> SyncJeecgUsersToLocalFromLoginScreenAsync(CancellationToken cancellationToken = default); + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Auth/SysAuthService.cs b/yy-admin-master/YY.Admin.Services/Service/Auth/SysAuthService.cs new file mode 100644 index 0000000..2ad6ce5 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Auth/SysAuthService.cs @@ -0,0 +1,2501 @@ +using Microsoft.Extensions.Configuration; +using SqlSugar; +using System.Collections.Concurrent; +using System.Globalization; +using System.Net.Http; +using System.Text; +using System.Net.Http.Json; +using System.Security.Cryptography; +using System.Text.Json; +using YY.Admin.Core.Session; +using YY.Admin.Core.Util; +using YY.Admin.Services.Service.Config; +using YY.Admin.Services.Service.Jeecg; +namespace YY.Admin.Services.Service.Auth +{ + public class SysAuthService : ISysAuthService, ISingletonDependency + { + /// + ///token过期时间(分) + /// + //private const int _refreshExpires = 30; + // token过期剩余时间(分) + private readonly TimeSpan _idleTimeout = TimeSpan.FromMinutes(20); + + private SysUser? _currentUser; + public SysUser? CurrentUser => _currentUser; + public event EventHandler? UserChanged; + private readonly ISysCacheService _sysCacheService; + private readonly ISqlSugarClient _dbContext; + private readonly ISysConfigService _sysConfigService; + private readonly IEventAggregator _eventAggregator; + private readonly IConfiguration _configuration; + private readonly HttpClient _httpClient; + private readonly ILoggerService _logger; + public SysAuthService( + ISqlSugarClient dbContext, + ISysCacheService sysCacheService, + ISysConfigService sysConfigService, + IEventAggregator eventAggregator, + IConfiguration configuration, + HttpClient httpClient, + ILoggerService logger) { + _dbContext=dbContext; + _sysCacheService=sysCacheService; + _sysConfigService=sysConfigService; + _eventAggregator = eventAggregator; + _configuration = configuration; + _httpClient = httpClient; + _logger = logger; + } + // 添加Token存储 + private static readonly ConcurrentDictionary _tokenStore = + new ConcurrentDictionary(); + private static bool _localIdentityResetDone = false; + private static long _localIdSeed = DateTime.UtcNow.Ticks; + + public async Task LoginAsync(LoginInput request) + { + string? jeecgErrorMessage = null; + try + { + var jeecgEnabled = _configuration.GetValue("JeecgIntegration:Enabled"); + var preferLocal = _configuration.GetValue("JeecgIntegration:PreferLocalLogin", true); + var fallbackToLocal = _configuration.GetValue("JeecgIntegration:FallbackToLocal", true); + var backendReachable = await IsJeecgBackendReachableAsync(); + + // 未启用 Jeecg:仅本地 + if (!jeecgEnabled) + { + return await AuthenticateAgainstJeecgMirrorTableAsync(request, jeecgFailureHint: null, localFirstOfflineHint: true); + } + + // 后端断开:仅本地新表登录,不再依赖后端 + if (!backendReachable) + { + return await AuthenticateAgainstJeecgMirrorTableAsync( + request, + jeecgFailureHint: "Jeecg服务拒绝连接,请确认后端服务已启动", + localFirstOfflineHint: true); + } + + // 工控离线优先:先本地库(不连 MES 即可用种子/已同步账号),失败再尝试 Jeecg + if (preferLocal) + { + var localFirst = await AuthenticateAgainstJeecgMirrorTableAsync(request, jeecgFailureHint: null, localFirstOfflineHint: true); + if (localFirst.Success) + { + return localFirst; + } + + var jeecgResult = await LoginByJeecgAsync(request); + if (jeecgResult.Success) + { + return jeecgResult; + } + + jeecgErrorMessage = jeecgResult.Message; + if (!fallbackToLocal) + { + return jeecgResult; + } + + // 本地已失败且已计次,不再二次验密;合并 Jeecg 原因便于排查 + return MergeLocalLoginFailureWithJeecg(localFirst, jeecgErrorMessage); + } + + // 在线优先:先 Jeecg,失败再本地(传统 SSO) + var jeecgFirst = await LoginByJeecgAsync(request); + if (jeecgFirst.Success) + { + return jeecgFirst; + } + + jeecgErrorMessage = jeecgFirst.Message; + if (!fallbackToLocal) + { + return jeecgFirst; + } + + return await AuthenticateAgainstJeecgMirrorTableAsync(request, jeecgFailureHint: jeecgErrorMessage, localFirstOfflineHint: false); + } + catch (Exception ex) + { + var friendlyError = ToFriendlyJeecgErrorMessage(ex.Message); + return new LoginOutput + { + Success = false, + Message = string.IsNullOrWhiteSpace(jeecgErrorMessage) + ? $"登录失败:{friendlyError}" + : $"{jeecgErrorMessage}({friendlyError})" + }; + } + } + + /// + /// 本地账号表(jeecg_sys_user)登录。 + /// 后端断开时仅依赖此表,不调用远端接口。 + /// + private async Task AuthenticateAgainstJeecgMirrorTableAsync( + LoginInput request, + string? jeecgFailureHint, + bool localFirstOfflineHint) + { + var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{request.Username}"; + var passwordErrorTimes = _sysCacheService.Get(keyPasswordErrorTimes); + var passwordMaxErrorTimes = await _sysConfigService.GetConfigValue(ConfigConst.SysPasswordMaxErrorTimes); + if (passwordMaxErrorTimes < 1) + { + passwordMaxErrorTimes = 10; + } + + if (passwordErrorTimes >= passwordMaxErrorTimes) + { + return new LoginOutput + { + Success = false, + Message = "密码错误次数过多,账号已锁定,请半小时后重试!" + }; + } + + var mirrorCandidates = await _dbContext.Queryable().ClearFilter() + .WhereIF(request.TenantId > 0, u => u.LoginTenantId == request.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(request.Username), u => u.Username == request.Username || (u.Phone != null && u.Phone == request.Username)) + .Take(1) + .ToListAsync(); + var mirrorUser = mirrorCandidates.FirstOrDefault(); + if (mirrorUser == null) + { + return new LoginOutput + { + Success = false, + Message = string.IsNullOrWhiteSpace(jeecgFailureHint) + ? "用户名或密码错误" + : $"用户名或密码错误。{jeecgFailureHint}" + }; + } + + if (mirrorUser.Status.HasValue && mirrorUser.Status.Value != 1) + { + return new LoginOutput + { + Success = false, + Message = "账号已冻结" + }; + } + + var sessionUser = BuildSessionUserFromMirrorUser(mirrorUser); + if (VerifyPassword(request.Password, keyPasswordErrorTimes, passwordErrorTimes, sessionUser)) + { + _currentUser = sessionUser; + UserChanged?.Invoke(this, _currentUser); + _sysCacheService.Remove(keyPasswordErrorTimes); + + string okMsg; + if (localFirstOfflineHint) + { + okMsg = "登录成功(本地验证;与 MES 断连时数据保留在本机,恢复连接后可再与后台同步)"; + } + else if (!string.IsNullOrWhiteSpace(jeecgFailureHint)) + { + okMsg = "登录成功(后台不可达或 Jeecg 未通过,已使用本地账号验证)"; + } + else + { + okMsg = "登录成功"; + } + + return new LoginOutput + { + Success = true, + Message = okMsg, + User = _currentUser, + Token = await GenerateToken(sessionUser) + }; + } + + return new LoginOutput + { + Success = false, + Message = string.IsNullOrWhiteSpace(jeecgFailureHint) + ? "用户名或密码错误" + : $"用户名或密码错误。{jeecgFailureHint}" + }; + } + + /// + /// 将 jeecg_sys_user 转换为本地会话所需 SysUser 结构。 + /// + private SysUser BuildSessionUserFromMirrorUser(JeecgSysUser mirrorUser) + { + long localId; + if (!long.TryParse(mirrorUser.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out localId) || localId <= 0) + { + localId = Interlocked.Increment(ref _localIdSeed); + } + + return new SysUser + { + Id = localId, + Account = mirrorUser.Username ?? string.Empty, + RealName = string.IsNullOrWhiteSpace(mirrorUser.Realname) ? (mirrorUser.Username ?? string.Empty) : mirrorUser.Realname!, + Password = mirrorUser.Password ?? string.Empty, + JeecgPasswordSalt = mirrorUser.Salt, + Phone = mirrorUser.Phone, + Email = mirrorUser.Email, + JobNum = mirrorUser.WorkNo, + Status = mirrorUser.Status == 1 ? StatusEnum.Enable : StatusEnum.Disable, + TenantId = mirrorUser.LoginTenantId.HasValue ? mirrorUser.LoginTenantId.Value : (_configuration.GetValue("JeecgIntegration:DefaultTenantId") ?? 1300000000001), + AccountType = AccountTypeEnum.NormalUser, + OrgId = 0, + PosId = 0 + }; + } + + /// + /// 快速探测 Jeecg 后端是否连通(登录前短超时)。 + /// + private async Task IsJeecgBackendReachableAsync() + { + var baseUrl = _configuration.GetValue("JeecgIntegration:BaseUrl")?.TrimEnd('/'); + var userListPath = _configuration.GetValue("JeecgIntegration:UserListPath") ?? "/sys/user/scada/queryUser"; + if (string.IsNullOrWhiteSpace(baseUrl)) + { + return false; + } + + var probeUrl = $"{baseUrl}{userListPath}?pageNo=1&pageSize=1&includeDetail=false"; + try + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + using var req = new HttpRequestMessage(HttpMethod.Get, probeUrl); + using var resp = await _httpClient.SendAsync(req, cts.Token); + return resp.IsSuccessStatusCode; + } + catch + { + return false; + } + } + + /// + /// 本地已失败时附加 Jeecg 原因,且不重复调用验密(避免错误次数翻倍) + /// + private static LoginOutput MergeLocalLoginFailureWithJeecg(LoginOutput localFailure, string? jeecgMessage) + { + if (localFailure.Success) + { + return localFailure; + } + + // 锁定、冻结等保持原样 + if (!string.Equals(localFailure.Message, "用户名或密码错误", StringComparison.Ordinal)) + { + return string.IsNullOrWhiteSpace(jeecgMessage) + ? localFailure + : new LoginOutput + { + Success = false, + Message = $"{localFailure.Message}。{jeecgMessage}" + }; + } + + // 后台不可达时,避免让用户误以为仅是「密码错」:补充本地 Jeecg 盐字段说明 + if (!string.IsNullOrWhiteSpace(jeecgMessage) && IsJeecgTransportUnreachableMessage(jeecgMessage)) + { + return new LoginOutput + { + Success = false, + Message = $"{jeecgMessage} 本地账号校验未通过时,请确认已从 MES 同步 password 与 jeecg_password_salt,且登录名与 Jeecg 的 username 一致。" + }; + } + + return new LoginOutput + { + Success = false, + Message = string.IsNullOrWhiteSpace(jeecgMessage) + ? localFailure.Message + : $"{localFailure.Message}。{jeecgMessage}" + }; + } + + /// + /// 判断是否为 Jeecg 网络不可达类错误(中英文字符串均兼容) + /// + private static bool IsJeecgTransportUnreachableMessage(string message) + { + if (string.IsNullOrWhiteSpace(message)) + { + return false; + } + + return message.Contains("Connection refused", StringComparison.OrdinalIgnoreCase) + || message.Contains("积极拒绝", StringComparison.Ordinal) + || message.Contains("拒绝连接", StringComparison.Ordinal) + || message.Contains("No connection could be made", StringComparison.OrdinalIgnoreCase) + || message.Contains("无法连接", StringComparison.Ordinal) + || message.Contains("timed out", StringComparison.OrdinalIgnoreCase) + || message.Contains("超时", StringComparison.Ordinal) + || message.Contains("No such host", StringComparison.OrdinalIgnoreCase) + || message.Contains("不知道这样的主机", StringComparison.Ordinal); + } + + /// + /// 通过 Jeecg 接口进行认证,并映射到本地用户 + /// + private async Task LoginByJeecgAsync(LoginInput request) + { + try + { + var baseUrl = _configuration.GetValue("JeecgIntegration:BaseUrl")?.TrimEnd('/'); + if (string.IsNullOrWhiteSpace(baseUrl)) + { + return new LoginOutput + { + Success = false, + Message = "JeecgIntegration 已启用,但未配置 BaseUrl" + }; + } + + var loginPath = _configuration.GetValue("JeecgIntegration:LoginPath") ?? "/sys/login"; + var userInfoPath = _configuration.GetValue("JeecgIntegration:UserInfoPath") ?? "/sys/user/getUserInfo"; + var loginUrl = $"{baseUrl}{loginPath}"; + var userInfoUrl = $"{baseUrl}{userInfoPath}"; + + var payload = new Dictionary + { + ["username"] = request.Username, + ["password"] = request.Password, + ["rememberMe"] = request.RememberMe + }; + + var captcha = _configuration.GetValue("JeecgIntegration:Captcha"); + var checkKey = _configuration.GetValue("JeecgIntegration:CheckKey"); + if (!string.IsNullOrWhiteSpace(captcha)) payload["captcha"] = captcha; + if (!string.IsNullOrWhiteSpace(checkKey)) payload["checkKey"] = checkKey; + + using var loginResponse = await _httpClient.PostAsJsonAsync(loginUrl, payload); + if (!loginResponse.IsSuccessStatusCode) + { + return new LoginOutput + { + Success = false, + Message = $"Jeecg登录请求失败:HTTP {(int)loginResponse.StatusCode}" + }; + } + + var loginJson = await loginResponse.Content.ReadAsStringAsync(); + using var loginDoc = JsonDocument.Parse(loginJson); + var loginRoot = loginDoc.RootElement; + + var loginSuccess = loginRoot.TryGetProperty("success", out var successEl) && successEl.GetBoolean(); + if (!loginSuccess) + { + var loginMsg = loginRoot.TryGetProperty("message", out var msgEl) ? msgEl.GetString() : "Jeecg登录失败"; + return new LoginOutput + { + Success = false, + Message = loginMsg ?? "Jeecg登录失败" + }; + } + + string? jeecgToken = null; + JsonElement tenantListElement = default; + bool hasTenantList = false; + if (loginRoot.TryGetProperty("result", out var resultEl) && + resultEl.ValueKind == JsonValueKind.Object && + resultEl.TryGetProperty("token", out var tokenEl)) + { + jeecgToken = tokenEl.GetString(); + if (resultEl.TryGetProperty("tenantList", out var tenantListEl) && tenantListEl.ValueKind == JsonValueKind.Array) + { + tenantListElement = tenantListEl; + hasTenantList = true; + } + } + if (string.IsNullOrWhiteSpace(jeecgToken)) + { + return new LoginOutput + { + Success = false, + Message = "Jeecg登录成功但未返回 token" + }; + } + + using var userReq = new HttpRequestMessage(HttpMethod.Get, userInfoUrl); + userReq.Headers.TryAddWithoutValidation("X-Access-Token", jeecgToken); + using var userResponse = await _httpClient.SendAsync(userReq); + if (!userResponse.IsSuccessStatusCode) + { + return new LoginOutput + { + Success = false, + Message = $"获取Jeecg用户信息失败:HTTP {(int)userResponse.StatusCode}" + }; + } + + var userJson = await userResponse.Content.ReadAsStringAsync(); + using var userDoc = JsonDocument.Parse(userJson); + var userRoot = userDoc.RootElement; + if (!(userRoot.TryGetProperty("success", out var userSuccessEl) && userSuccessEl.GetBoolean())) + { + var userMsg = userRoot.TryGetProperty("message", out var userMsgEl) ? userMsgEl.GetString() : "获取Jeecg用户信息失败"; + return new LoginOutput + { + Success = false, + Message = userMsg ?? "获取Jeecg用户信息失败" + }; + } + + var remoteUsername = request.Username; + string? remoteRealName = null; + string? remoteNickName = null; + string? remotePhone = null; + string? remoteEmail = null; + string? remoteAvatar = null; + string? remoteAddress = null; + string? remoteOfficePhone = null; + string? remoteJobNum = null; + string? remoteIdCardNum = null; + string? remoteRemark = null; + DateTime? remoteBirthday = null; + GenderEnum? remoteSex = null; + StatusEnum? remoteStatus = null; + long? remoteLoginTenantId = null; + DateTime? remoteJeecgUpdateTime = null; + long? remoteJeecgUserId = null; + string? remoteJeecgIdRaw = null; + + if (userRoot.TryGetProperty("result", out var userResultEl) && + userResultEl.ValueKind == JsonValueKind.Object && + userResultEl.TryGetProperty("userInfo", out var userInfoEl) && + userInfoEl.ValueKind == JsonValueKind.Object) + { + if (userInfoEl.TryGetProperty("id", out var jeecgUserIdEl)) + { + remoteJeecgUserId = TryParseJeecgSnowflakeUserIdFromJson(jeecgUserIdEl); + remoteJeecgIdRaw = jeecgUserIdEl.ValueKind == JsonValueKind.String + ? jeecgUserIdEl.GetString() + : jeecgUserIdEl.ValueKind == JsonValueKind.Number + ? jeecgUserIdEl.GetRawText() + : null; + } + + if (userInfoEl.TryGetProperty("username", out var usernameEl)) remoteUsername = usernameEl.GetString() ?? remoteUsername; + if (userInfoEl.TryGetProperty("realname", out var realnameEl)) remoteRealName = realnameEl.GetString(); + if (userInfoEl.TryGetProperty("nickname", out var nicknameEl)) remoteNickName = nicknameEl.GetString(); + if (userInfoEl.TryGetProperty("phone", out var phoneEl)) remotePhone = phoneEl.GetString(); + if (userInfoEl.TryGetProperty("email", out var emailEl)) remoteEmail = emailEl.GetString(); + if (userInfoEl.TryGetProperty("avatar", out var avatarEl)) remoteAvatar = avatarEl.GetString(); + if (userInfoEl.TryGetProperty("address", out var addressEl)) remoteAddress = addressEl.GetString(); + if (userInfoEl.TryGetProperty("telephone", out var telephoneEl)) remoteOfficePhone = telephoneEl.GetString(); + if (userInfoEl.TryGetProperty("workNo", out var workNoEl)) remoteJobNum = workNoEl.GetString(); + if (userInfoEl.TryGetProperty("idCard", out var idCardEl)) remoteIdCardNum = idCardEl.GetString(); + if (userInfoEl.TryGetProperty("description", out var descriptionEl)) remoteRemark = descriptionEl.GetString(); + + if (userInfoEl.TryGetProperty("birthday", out var birthdayEl) && + birthdayEl.ValueKind == JsonValueKind.String && + DateTime.TryParse(birthdayEl.GetString(), out var birthday)) + { + remoteBirthday = birthday; + } + + if (userInfoEl.TryGetProperty("sex", out var sexEl)) + { + remoteSex = ResolveUserSex(sexEl); + } + + if (userInfoEl.TryGetProperty("status", out var statusEl)) + { + remoteStatus = ResolveUserStatus(statusEl); + } + + if (userInfoEl.TryGetProperty("loginTenantId", out var tenantIdEl) && tenantIdEl.TryGetInt64(out var tenantId)) + { + remoteLoginTenantId = tenantId; + } + + if (userInfoEl.TryGetProperty("updateTime", out var loginUpdateEl)) + { + remoteJeecgUpdateTime = TryParseJeecgRemoteDateTime(loginUpdateEl); + } + } + + // 按配置清空本地用户/角色/租户数据(仅当前进程执行一次) + var resetLocalIdentity = _configuration.GetValue("JeecgIntegration:ResetLocalIdentityDataOnJeecgLogin", false); + if (resetLocalIdentity && !_localIdentityResetDone) + { + await ResetLocalIdentityDataAsync(); + _localIdentityResetDone = true; + } + + var useJeecgUserIdAsLocalPk = _configuration.GetValue("JeecgIntegration:UseJeecgUserIdAsLocalPrimaryKey", true); + + var localUser = await _dbContext.Queryable() + .Includes(u => u.SysOrg) + .ClearFilter() + .Where(u => u.Account == remoteUsername || (!string.IsNullOrEmpty(remotePhone) && u.Phone == remotePhone)) + .FirstAsync(); + + if (localUser == null) + { + var autoProvision = _configuration.GetValue("JeecgIntegration:AutoProvisionLocalUser", true); + if (!autoProvision) + { + return new LoginOutput + { + Success = false, + Message = $"Jeecg认证成功,但本地未找到账号映射:{remoteUsername}。请先在工控端创建同名账号。" + }; + } + + var templateUser = await _dbContext.Queryable().ClearFilter().OrderBy(u => u.Id).FirstAsync(); + + var defaultTenantId = _configuration.GetValue("JeecgIntegration:DefaultTenantId"); + var targetTenantId = defaultTenantId ?? remoteLoginTenantId ?? templateUser?.TenantId ?? 1300000000001; + var newUser = new SysUser + { + Account = remoteUsername, + RealName = string.IsNullOrWhiteSpace(remoteRealName) ? remoteUsername : remoteRealName, + NickName = remoteNickName, + Password = CryptogramUtil.Encrypt(Guid.NewGuid().ToString("N")), + Sex = remoteSex ?? GenderEnum.Unknown, + Birthday = remoteBirthday, + Phone = remotePhone, + OfficePhone = remoteOfficePhone, + Email = remoteEmail, + Avatar = remoteAvatar, + Address = remoteAddress, + JobNum = remoteJobNum, + IdCardNum = remoteIdCardNum, + Remark = remoteRemark, + Status = remoteStatus ?? StatusEnum.Enable, + AccountType = targetTenantId == (defaultTenantId ?? 0) ? AccountTypeEnum.SysAdmin : AccountTypeEnum.NormalUser, + TenantId = targetTenantId, + OrgId = templateUser?.OrgId ?? 0, + PosId = templateUser?.PosId ?? 0, + JeecgUpdateTime = remoteJeecgUpdateTime, + CreateTime = DateTime.Now, + UpdateTime = DateTime.Now + }; + + long assignUserId; + if (useJeecgUserIdAsLocalPk && remoteJeecgUserId.HasValue && remoteJeecgUserId.Value > 0) + { + var idOccupied = await _dbContext.Queryable().ClearFilter().AnyAsync(u => u.Id == remoteJeecgUserId.Value); + assignUserId = idOccupied ? await TakeNextSysUserIdAsync() : remoteJeecgUserId.Value; + } + else + { + assignUserId = await TakeNextSysUserIdAsync(); + } + + newUser.Id = assignUserId; + if (!string.IsNullOrWhiteSpace(remoteJeecgIdRaw)) + { + newUser.JeecgBizUserId = TruncateJeecgSyncField(remoteJeecgIdRaw, 64); + } + + await _dbContext.Insertable(newUser).ExecuteCommandAsync(); + localUser = await _dbContext.Queryable() + .Includes(u => u.SysOrg) + .ClearFilter() + .Where(u => u.Account == remoteUsername) + .FirstAsync(); + if (localUser == null) + { + return new LoginOutput + { + Success = false, + Message = "Jeecg认证成功,但本地自动建档失败。" + }; + } + } + + // 已存在用户:将本地主键迁移为 Jeecg userInfo.id(与接口一致) + if (useJeecgUserIdAsLocalPk && remoteJeecgUserId.HasValue && remoteJeecgUserId.Value > 0) + { + if (localUser.Id != remoteJeecgUserId.Value) + { + var idBlocked = await _dbContext.Queryable().ClearFilter() + .Where(u => u.Id == remoteJeecgUserId.Value && u.Account != remoteUsername) + .AnyAsync(); + if (!idBlocked) + { + var oldUid = localUser.Id; + await TryMigrateSysUserPrimaryKeyToJeecgIdAsync(oldUid, remoteJeecgUserId.Value); + _sysCacheService.Remove($"jeecg:token:{oldUid}"); + localUser = await _dbContext.Queryable() + .Includes(u => u.SysOrg) + .ClearFilter() + .Where(u => u.Account == remoteUsername) + .FirstAsync() ?? localUser; + } + } + } + + if (!string.IsNullOrWhiteSpace(remoteJeecgIdRaw)) + { + localUser.JeecgBizUserId = TruncateJeecgSyncField(remoteJeecgIdRaw, 64); + } + + // Jeecg字段全量同步到本地用户(以后端为准) + if (!string.IsNullOrWhiteSpace(remoteRealName)) localUser.RealName = remoteRealName; + localUser.NickName = remoteNickName; + localUser.Phone = remotePhone; + localUser.Email = remoteEmail; + localUser.Avatar = remoteAvatar; + localUser.Address = remoteAddress; + localUser.OfficePhone = remoteOfficePhone; + localUser.JobNum = remoteJobNum; + localUser.IdCardNum = remoteIdCardNum; + localUser.Remark = remoteRemark; + if (remoteBirthday.HasValue) localUser.Birthday = remoteBirthday; + if (remoteSex.HasValue) localUser.Sex = remoteSex.Value; + if (remoteStatus.HasValue) localUser.Status = remoteStatus.Value; + if (remoteLoginTenantId.HasValue && remoteLoginTenantId.Value > 0) localUser.TenantId = remoteLoginTenantId.Value; + if (remoteJeecgUpdateTime.HasValue) localUser.JeecgUpdateTime = remoteJeecgUpdateTime; + localUser.UpdateTime = DateTime.Now; + + var defaultSyncTenantId = _configuration.GetValue("JeecgIntegration:DefaultTenantId"); + if (defaultSyncTenantId.HasValue && localUser.TenantId == defaultSyncTenantId.Value) + { + localUser.AccountType = AccountTypeEnum.SysAdmin; + } + + var syncToLocal = _configuration.GetValue("JeecgIntegration:SyncUserProfileToLocal", true); + if (syncToLocal) + { + await _dbContext.Updateable(localUser).UpdateColumns(u => new + { + u.RealName, + u.NickName, + u.Phone, + u.Email, + u.Avatar, + u.Address, + u.OfficePhone, + u.JobNum, + u.IdCardNum, + u.Remark, + u.Birthday, + u.Sex, + u.Status, + u.TenantId, + u.AccountType, + u.JeecgUpdateTime, + u.JeecgBizUserId, + u.UpdateTime + }).ExecuteCommandAsync(); + } + + // 同步租户信息(以后端为准) + if (hasTenantList) + { + await SyncTenantListAsync(tenantListElement); + } + + // 同步Jeecg全量用户到本地 + var syncAllUsersOnLogin = _configuration.GetValue("JeecgIntegration:SyncAllUsersOnJeecgLogin", true); + var userListSyncHint = string.Empty; + if (syncAllUsersOnLogin) + { + // 登录场景:SCADA 接口必须全量分页拉取,不能用 updatedAfter 增量提前结束,否则本地用户表长期不全 + var (syncListOk, _) = await SyncAllJeecgUsersAsync(baseUrl, jeecgToken, localUser, allowScadaIncrementalQuery: false); + if (!syncListOk) + { + userListSyncHint = "(提示:Jeecg 用户列表同步未拉到任何记录,用户管理可能为空;请检查 queryUser 接口、网络及程序运行目录下的 Admin.NET.db。)"; + } + + localUser = await _dbContext.Queryable() + .Includes(u => u.SysOrg) + .ClearFilter() + .Where(u => u.Id == localUser.Id) + .FirstAsync() ?? localUser; + } + + // SysAdmin菜单已在菜单服务中放行,不再写sys_tenant_menu,避免SQLite主键冲突 + + // 同步Jeecg权限明细到本地角色/菜单关系 + await SyncJeecgPermissionToLocalAsync(baseUrl, jeecgToken, localUser); + + _currentUser = localUser; + UserChanged?.Invoke(this, _currentUser); + _sysCacheService.Set($"jeecg:token:{localUser.Id}", jeecgToken!, TimeSpan.FromHours(8)); + + return new LoginOutput + { + Success = true, + Message = string.IsNullOrEmpty(userListSyncHint) ? "登录成功" : $"登录成功{userListSyncHint}", + User = _currentUser, + Token = await GenerateToken(localUser) + }; + } + catch (Exception ex) + { + var friendlyError = ToFriendlyJeecgErrorMessage(ex.Message); + return new LoginOutput + { + Success = false, + Message = $"Jeecg登录失败:{friendlyError}" + }; + } + } + + /// + /// 同步 Jeecg 返回的租户列表到本地,字段以后端为准 + /// + private async Task SyncTenantListAsync(JsonElement tenantListElement) + { + if (tenantListElement.ValueKind != JsonValueKind.Array) return; + + var templateTenant = await _dbContext.Queryable().ClearFilter().OrderBy(t => t.Id).FirstAsync(); + + foreach (var tenantEl in tenantListElement.EnumerateArray()) + { + if (tenantEl.ValueKind != JsonValueKind.Object) continue; + if (!tenantEl.TryGetProperty("id", out var idEl) || !idEl.TryGetInt64(out var tenantId) || tenantId <= 0) continue; + + var tenantName = tenantEl.TryGetProperty("name", out var nameEl) ? nameEl.GetString() : null; + var companyLogo = tenantEl.TryGetProperty("companyLogo", out var logoEl) ? logoEl.GetString() : null; + var status = ResolveTenantStatus(tenantEl); + + var exists = await _dbContext.Queryable().ClearFilter().Where(t => t.Id == tenantId).AnyAsync(); + if (exists) + { + await _dbContext.Updateable() + .SetColumns(t => t.Title == (string.IsNullOrWhiteSpace(tenantName) ? t.Title : tenantName)) + .SetColumns(t => t.Logo == companyLogo) + .SetColumns(t => t.Status == status) + .SetColumns(t => t.UpdateTime == DateTime.Now) + .Where(t => t.Id == tenantId) + .ExecuteCommandAsync(); + } + else + { + var newTenant = new SysTenant + { + Id = tenantId, + UserId = templateTenant?.UserId ?? _currentUser?.Id ?? 0, + OrgId = templateTenant?.OrgId ?? _currentUser?.OrgId ?? 0, + TenantType = templateTenant?.TenantType ?? TenantTypeEnum.Id, + DbType = templateTenant?.DbType ?? DbType.Sqlite, + Connection = templateTenant?.Connection, + ConfigId = templateTenant?.ConfigId, + SlaveConnections = templateTenant?.SlaveConnections, + EnableReg = templateTenant?.EnableReg ?? YesNoEnum.N, + RegWayId = templateTenant?.RegWayId, + Logo = companyLogo, + Title = string.IsNullOrWhiteSpace(tenantName) ? $"租户{tenantId}" : tenantName, + ViceTitle = templateTenant?.ViceTitle, + ViceDesc = templateTenant?.ViceDesc, + Watermark = templateTenant?.Watermark, + Copyright = templateTenant?.Copyright, + Icp = templateTenant?.Icp, + IcpUrl = templateTenant?.IcpUrl, + OrderNo = templateTenant?.OrderNo ?? 100, + Remark = templateTenant?.Remark, + Status = status, + CreateTime = DateTime.Now, + UpdateTime = DateTime.Now + }; + await _dbContext.Insertable(newTenant).ExecuteCommandAsync(); + } + } + } + + /// + /// 清空本地用户/角色/租户数据,为Jeecg全量接管做准备 + /// + private async Task ResetLocalIdentityDataAsync() + { + await _dbContext.Ado.BeginTranAsync(); + try + { + await _dbContext.Deleteable().ExecuteCommandAsync(); + await _dbContext.Deleteable().ExecuteCommandAsync(); + await _dbContext.Deleteable().ExecuteCommandAsync(); + await _dbContext.Deleteable().ExecuteCommandAsync(); + await _dbContext.Deleteable().ExecuteCommandAsync(); + await _dbContext.Deleteable().ExecuteCommandAsync(); + await _dbContext.Ado.CommitTranAsync(); + + // 清空 Jeecg 同步水位,否则仍按旧时间增量请求,首屏可能 0 条且误以为同步成功 + try + { + new JeecgSyncStateStore().Save(new JeecgSyncState()); + } + catch + { + // 忽略状态文件写入失败 + } + } + catch + { + await _dbContext.Ado.RollbackTranAsync(); + throw; + } + } + + /// + /// 完全采用Jeecg权限:同步当前用户角色与菜单权限明细到本地 + /// + private async Task SyncJeecgPermissionToLocalAsync(string baseUrl, string jeecgToken, SysUser localUser) + { + var permissionPath = _configuration.GetValue("JeecgIntegration:UserPermissionPath") ?? "/sys/permission/getUserPermissionByToken"; + var requestUrl = $"{baseUrl}{permissionPath}"; + + using var req = new HttpRequestMessage(HttpMethod.Get, requestUrl); + req.Headers.TryAddWithoutValidation("X-Access-Token", jeecgToken); + using var resp = await _httpClient.SendAsync(req); + if (!resp.IsSuccessStatusCode) return; + + var json = await resp.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + if (!(root.TryGetProperty("success", out var successEl) && successEl.GetBoolean())) return; + if (!root.TryGetProperty("result", out var resultEl) || resultEl.ValueKind != JsonValueKind.Object) return; + + var permissionCodes = new HashSet(StringComparer.OrdinalIgnoreCase); + var routeKeys = new HashSet(StringComparer.OrdinalIgnoreCase); + + if (resultEl.TryGetProperty("codeList", out var codeListEl) && codeListEl.ValueKind == JsonValueKind.Array) + { + foreach (var item in codeListEl.EnumerateArray()) + { + if (item.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(item.GetString())) + { + permissionCodes.Add(item.GetString()!); + } + } + } + + if (resultEl.TryGetProperty("menu", out var menuEl)) + { + CollectJeecgMenuKeys(menuEl, routeKeys, permissionCodes); + } + + var roleCode = $"jeecg_sync_{localUser.Account}".ToLowerInvariant(); + var role = await _dbContext.Queryable().ClearFilter().Where(r => r.Code == roleCode).FirstAsync(); + if (role == null) + { + role = new SysRole + { + Name = $"Jeecg同步角色_{localUser.Account}", + Code = roleCode, + TenantId = localUser.TenantId ?? 0, + DataScope = DataScopeEnum.All, + Status = StatusEnum.Enable, + OrderNo = 1, + Remark = "Jeecg同步自动生成", + CreateTime = DateTime.Now, + UpdateTime = DateTime.Now + }; + role.Id = await TakeNextSysRoleIdAsync(); + await _dbContext.Insertable(role).ExecuteCommandAsync(); + role = await _dbContext.Queryable().ClearFilter().Where(r => r.Code == roleCode).FirstAsync(); + } + + if (role == null) return; + + await _dbContext.Deleteable().Where(x => x.UserId == localUser.Id).ExecuteCommandAsync(); + var maxUserRoleId = await _dbContext.Queryable().ClearFilter().MaxAsync(x => (long?)x.Id) ?? 0; + await _dbContext.Insertable(new SysUserRole + { + Id = maxUserRoleId + 1, + UserId = localUser.Id, + RoleId = role.Id + }).ExecuteCommandAsync(); + + await _dbContext.Deleteable().Where(x => x.RoleId == role.Id).ExecuteCommandAsync(); + + var localMenus = await _dbContext.Queryable().ClearFilter() + .Where(m => m.Type != MenuTypeEnum.Btn && m.Status == StatusEnum.Enable) + .ToListAsync(); + + var defaultTenantId = _configuration.GetValue("JeecgIntegration:DefaultTenantId"); + var useAllMenus = defaultTenantId.HasValue && localUser.TenantId == defaultTenantId.Value; + + var matchedMenuIds = useAllMenus + ? localMenus.Select(m => m.Id).Distinct().ToList() + : localMenus.Where(m => IsLocalMenuMatchedByJeecg(m, routeKeys, permissionCodes)) + .Select(m => m.Id) + .Distinct() + .ToList(); + + if (matchedMenuIds.Count == 0) return; + var maxRoleMenuId = await _dbContext.Queryable().ClearFilter().MaxAsync(x => (long?)x.Id) ?? 0; + foreach (var menuId in matchedMenuIds.Distinct()) + { + await _dbContext.Insertable(new SysRoleMenu + { + Id = ++maxRoleMenuId, + RoleId = role.Id, + MenuId = menuId + }).ExecuteCommandAsync(); + } + } + + /// + /// 递归提取Jeecg菜单中的路径、名称、权限编码 + /// + private static void CollectJeecgMenuKeys(JsonElement node, HashSet routeKeys, HashSet permissionCodes) + { + if (node.ValueKind == JsonValueKind.Array) + { + foreach (var child in node.EnumerateArray()) + { + CollectJeecgMenuKeys(child, routeKeys, permissionCodes); + } + return; + } + + if (node.ValueKind != JsonValueKind.Object) return; + + AddIfString(node, "path", routeKeys); + AddIfString(node, "url", routeKeys); + AddIfString(node, "name", routeKeys); + AddIfString(node, "component", routeKeys); + AddIfString(node, "title", routeKeys); + AddIfString(node, "perms", permissionCodes); + AddIfString(node, "permission", permissionCodes); + + if (node.TryGetProperty("meta", out var metaEl) && metaEl.ValueKind == JsonValueKind.Object) + { + AddIfString(metaEl, "title", routeKeys); + AddIfString(metaEl, "component", routeKeys); + if (metaEl.TryGetProperty("permissionList", out var permissionListEl) && permissionListEl.ValueKind == JsonValueKind.Array) + { + foreach (var p in permissionListEl.EnumerateArray()) + { + if (p.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(p.GetString())) + { + permissionCodes.Add(p.GetString()!); + } + } + } + } + + if (node.TryGetProperty("children", out var childrenEl)) + { + CollectJeecgMenuKeys(childrenEl, routeKeys, permissionCodes); + } + } + + private static bool IsLocalMenuMatchedByJeecg(SysMenu menu, HashSet routeKeys, HashSet permissionCodes) + { + var keys = new[] + { + Normalize(menu.Path), + Normalize(menu.Name), + Normalize(menu.Component), + Normalize(menu.Title) + }; + + if (!string.IsNullOrWhiteSpace(menu.Permission) && permissionCodes.Contains(menu.Permission.Trim())) + { + return true; + } + + return keys.Any(k => !string.IsNullOrWhiteSpace(k) && routeKeys.Contains(k)); + } + + private static void AddIfString(JsonElement obj, string propertyName, HashSet target) + { + if (obj.TryGetProperty(propertyName, out var valueEl) && + valueEl.ValueKind == JsonValueKind.String && + !string.IsNullOrWhiteSpace(valueEl.GetString())) + { + target.Add(Normalize(valueEl.GetString())); + } + } + + private static string Normalize(string? value) + { + return (value ?? string.Empty).Trim().Trim('/').ToLowerInvariant(); + } + + private static string Normalize(JsonElement valueEl) + { + return Normalize(valueEl.GetString()); + } + + /// + /// 分配下一个本地用户主键(sys_user 非自增,Jeecg 建档时必须显式赋值) + /// + private async Task TakeNextSysUserIdAsync() + { + var maxId = await _dbContext.Queryable().ClearFilter().MaxAsync(x => (long?)x.Id) ?? 0; + return maxId + 1; + } + + /// + /// 分配下一个本地角色主键(sys_role 非自增,多账号 Jeecg 同步各建角色时须避免 Id=0 重复) + /// + private async Task TakeNextSysRoleIdAsync() + { + var maxId = await _dbContext.Queryable().ClearFilter().MaxAsync(x => (long?)x.Id) ?? 0; + return maxId + 1; + } + + /// + /// 安全解析Jeecg租户状态,避免null或字符串导致异常 + /// + private static StatusEnum ResolveTenantStatus(JsonElement tenantEl) + { + if (!tenantEl.TryGetProperty("status", out var statusEl)) + { + return StatusEnum.Disable; + } + + if (statusEl.ValueKind == JsonValueKind.Number && statusEl.TryGetInt32(out var numStatus)) + { + return numStatus == 1 ? StatusEnum.Enable : StatusEnum.Disable; + } + + if (statusEl.ValueKind == JsonValueKind.String && int.TryParse(statusEl.GetString(), out var strStatus)) + { + return strStatus == 1 ? StatusEnum.Enable : StatusEnum.Disable; + } + + return StatusEnum.Disable; + } + + /// + /// 有 Token 时附加 X-Access-Token(SCADA 免登录接口可不传) + /// + private static void AddJeecgAccessTokenIfPresent(HttpRequestMessage request, string? jeecgToken) + { + if (!string.IsNullOrWhiteSpace(jeecgToken)) + { + request.Headers.TryAddWithoutValidation("X-Access-Token", jeecgToken); + } + } + + /// + /// 从接口返回的用户数组中累计最大 updateTime/createTime(转 UTC),用于推进 SCADA 同步游标 + /// + private static void AccumulateMaxJeecgSyncCursorUtc(IEnumerable records, ref DateTime? maxUtc) + { + foreach (var el in records) + { + DateTime? tUpdate = el.TryGetProperty("updateTime", out var ue) ? TryParseJeecgRemoteDateTime(ue) : null; + DateTime? tCreate = el.TryGetProperty("createTime", out var ce) ? TryParseJeecgRemoteDateTime(ce) : null; + DateTime? rowBest; + if (tUpdate.HasValue && tCreate.HasValue) + { + rowBest = tUpdate.Value >= tCreate.Value ? tUpdate : tCreate; + } + else + { + rowBest = tUpdate ?? tCreate; + } + + if (!rowBest.HasValue) + { + continue; + } + + var asUtc = rowBest.Value.Kind == DateTimeKind.Utc ? rowBest.Value : rowBest.Value.ToUniversalTime(); + if (!maxUtc.HasValue || asUtc > maxUtc.Value) + { + maxUtc = asUtc; + } + } + } + + /// + /// 将一批 Jeecg/SCADA 用户 JSON 写入本地;若接口返回 password/salt(如 SCADA 同步),则写入本地用于与 Jeecg 一致的登录校验。 + /// + private async Task ProcessJeecgUserRecordsAsync( + IReadOnlyList records, + SysUser templateUser, + SysUser currentUser, + long? defaultTenantId, + bool skipUnchanged) + { + foreach (var userEl in records) + { + var remoteUser = ParseJeecgUser(userEl); + if (string.IsNullOrWhiteSpace(remoteUser.Username)) + { + continue; + } + + // 无匹配行时 FirstAsync 会抛错,导致整批同步中断;用 Take(1) 安全取首条 + var existing = (await _dbContext.Queryable() + .ClearFilter() + .Where(u => u.Account == remoteUser.Username) + .Take(1) + .ToListAsync()).FirstOrDefault(); + + if (skipUnchanged + && existing != null + && existing.JeecgUpdateTime.HasValue + && remoteUser.JeecgSourceUpdateTime.HasValue + && Math.Abs((existing.JeecgUpdateTime.Value - remoteUser.JeecgSourceUpdateTime.Value).TotalSeconds) < 2) + { + continue; + } + + var mappedTenantId = remoteUser.TenantId ?? defaultTenantId ?? templateUser.TenantId ?? currentUser.TenantId ?? 1300000000001; + var mappedAccountType = mappedTenantId == (defaultTenantId ?? 0) ? AccountTypeEnum.SysAdmin : AccountTypeEnum.NormalUser; + + if (existing == null) + { + var createAt = remoteUser.JeecgSourceCreateTime ?? DateTime.Now; + var newUser = new SysUser + { + Account = remoteUser.Username!, + RealName = string.IsNullOrWhiteSpace(remoteUser.RealName) ? remoteUser.Username! : remoteUser.RealName!, + NickName = remoteUser.NickName, + Password = !string.IsNullOrWhiteSpace(remoteUser.PasswordHex) + ? remoteUser.PasswordHex! + : CryptogramUtil.Encrypt(Guid.NewGuid().ToString("N")), + JeecgPasswordSalt = remoteUser.Salt, + Sex = remoteUser.Sex ?? GenderEnum.Unknown, + Birthday = remoteUser.Birthday, + Phone = remoteUser.Phone, + OfficePhone = remoteUser.OfficePhone, + Email = remoteUser.Email, + Avatar = remoteUser.Avatar, + Address = remoteUser.Address, + JobNum = remoteUser.JobNum, + IdCardNum = remoteUser.IdCardNum, + Remark = remoteUser.Remark, + PosTitle = remoteUser.PosTitle, + Status = remoteUser.Status ?? StatusEnum.Enable, + TenantId = mappedTenantId, + AccountType = mappedAccountType, + OrgId = templateUser.OrgId, + PosId = templateUser.PosId, + JeecgUpdateTime = remoteUser.JeecgSourceUpdateTime, + JeecgBizUserId = remoteUser.JeecgBizUserId, + JeecgOrgCode = remoteUser.OrgCode, + JeecgDepartIds = remoteUser.DepartIds, + CreateTime = createAt, + UpdateTime = DateTime.Now + }; + newUser.Id = await TakeNextSysUserIdAsync(); + await _dbContext.Insertable(newUser).ExecuteCommandAsync(); + } + else + { + existing.RealName = string.IsNullOrWhiteSpace(remoteUser.RealName) ? existing.RealName : remoteUser.RealName!; + existing.NickName = remoteUser.NickName; + existing.Phone = remoteUser.Phone; + existing.Email = remoteUser.Email; + existing.Avatar = remoteUser.Avatar; + existing.Address = remoteUser.Address; + existing.OfficePhone = remoteUser.OfficePhone; + existing.JobNum = remoteUser.JobNum; + existing.IdCardNum = remoteUser.IdCardNum; + existing.Remark = remoteUser.Remark; + existing.PosTitle = remoteUser.PosTitle; + if (remoteUser.Birthday.HasValue) + { + existing.Birthday = remoteUser.Birthday; + } + + if (remoteUser.Sex.HasValue) + { + existing.Sex = remoteUser.Sex.Value; + } + + if (remoteUser.Status.HasValue) + { + existing.Status = remoteUser.Status.Value; + } + + existing.TenantId = mappedTenantId; + existing.AccountType = mappedAccountType; + existing.JeecgUpdateTime = remoteUser.JeecgSourceUpdateTime ?? existing.JeecgUpdateTime; + if (!string.IsNullOrWhiteSpace(remoteUser.JeecgBizUserId)) + { + existing.JeecgBizUserId = remoteUser.JeecgBizUserId; + } + + existing.JeecgOrgCode = remoteUser.OrgCode; + existing.JeecgDepartIds = remoteUser.DepartIds; + existing.UpdateTime = DateTime.Now; + if (!string.IsNullOrWhiteSpace(remoteUser.PasswordHex)) + { + existing.Password = remoteUser.PasswordHex!; + } + + if (!string.IsNullOrWhiteSpace(remoteUser.Salt)) + { + existing.JeecgPasswordSalt = remoteUser.Salt; + } + + await _dbContext.Updateable(existing).UpdateColumns(u => new + { + u.RealName, + u.NickName, + u.Phone, + u.Email, + u.Avatar, + u.Address, + u.OfficePhone, + u.JobNum, + u.IdCardNum, + u.Remark, + u.PosTitle, + u.Birthday, + u.Sex, + u.Status, + u.TenantId, + u.AccountType, + u.JeecgUpdateTime, + u.JeecgBizUserId, + u.JeecgOrgCode, + u.JeecgDepartIds, + u.UpdateTime, + u.Password, + u.JeecgPasswordSalt + }).ExecuteCommandAsync(); + } + } + } + + /// + /// 同步 Jeecg 用户:标准列表为 result.records 分页;SCADA 为 result 数组分页 + 可选 updatedAfter(免登录时可不传 Token)。SCADA 若返回 password/salt 则写入本地。 + /// + /// false 时(如登录后首拉)SCADA 不带 updatedAfter,始终按页拉全量;true 时(如定时后台)可按水位做增量。 + /// 是否完成有效同步;ApiRecordRows 为接口返回的用户记录条数(分页累计),为 0 时表示未拉到数据。 + private async Task<(bool Success, int ApiRecordRows)> SyncAllJeecgUsersAsync(string baseUrl, string jeecgToken, SysUser currentUser, bool allowScadaIncrementalQuery = true) + { + var userListPath = _configuration.GetValue("JeecgIntegration:UserListPath") ?? "/sys/user/list"; + var templateCandidates = await _dbContext.Queryable().ClearFilter().OrderBy(u => u.Id).Take(1).ToListAsync(); + var templateUser = templateCandidates.FirstOrDefault() ?? currentUser; + var defaultTenantId = _configuration.GetValue("JeecgIntegration:DefaultTenantId"); + var skipUnchanged = _configuration.GetValue("JeecgIntegration:UserSyncSkipUnchanged", true); + var useTimeQuery = _configuration.GetValue("JeecgIntegration:UserListUseUpdateTimeQuery", false); + var overlapMinutes = _configuration.GetValue("JeecgIntegration:IncrementalSyncOverlapMinutes", 2); + var isScadaStylePath = userListPath.Contains("scada", StringComparison.OrdinalIgnoreCase); + var scadaUseUpdatedAfter = _configuration.GetValue("JeecgIntegration:ScadaUseUpdatedAfter", true); + var scadaPageSize = Math.Clamp(_configuration.GetValue("JeecgIntegration:ScadaUserPageSize", 500), 1, 1000); + var scadaIncludeDetail = _configuration.GetValue("JeecgIntegration:ScadaUserIncludeDetail", false); + + var stateStore = new JeecgSyncStateStore(); + var syncState = stateStore.Load(); + const int standardPageSize = 200; + + // pass 0:增量;若整轮 0 条则 pass 1 全量(避免清空库后旧水位导致拉不到用户) + for (var pass = 0; pass < 2; pass++) + { + var useIncremental = pass == 0 && syncState.LastUserListSyncUtc.HasValue && ( + (isScadaStylePath && scadaUseUpdatedAfter && allowScadaIncrementalQuery) || + (!isScadaStylePath && useTimeQuery)); + + DateTime? incrementalCursorLocal = null; + if (useIncremental) + { + incrementalCursorLocal = syncState.LastUserListSyncUtc!.Value + .AddMinutes(-Math.Max(0, overlapMinutes)) + .ToLocalTime(); + } + + var httpOk = true; + var rowsThisPass = 0; + DateTime? maxCursorUtcFromData = null; + + if (isScadaStylePath) + { + var pageNo = 1; + while (true) + { + var listUrl = + $"{baseUrl}{userListPath}?pageNo={pageNo.ToString(CultureInfo.InvariantCulture)}&pageSize={scadaPageSize.ToString(CultureInfo.InvariantCulture)}&includeDetail={(scadaIncludeDetail ? "true" : "false")}"; + if (incrementalCursorLocal.HasValue) + { + var afterStr = incrementalCursorLocal.Value.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); + listUrl += $"&updatedAfter={Uri.EscapeDataString(afterStr)}"; + } + + using var req = new HttpRequestMessage(HttpMethod.Get, listUrl); + AddJeecgAccessTokenIfPresent(req, jeecgToken); + using var resp = await _httpClient.SendAsync(req); + if (!resp.IsSuccessStatusCode) + { + httpOk = false; + break; + } + + var json = await resp.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + if (!(root.TryGetProperty("success", out var successEl) && successEl.GetBoolean())) + { + httpOk = false; + break; + } + + if (!root.TryGetProperty("result", out var resultEl)) + { + httpOk = false; + break; + } + + List records; + if (resultEl.ValueKind == JsonValueKind.Array) + { + records = resultEl.EnumerateArray().ToList(); + } + else if (resultEl.ValueKind == JsonValueKind.Object + && resultEl.TryGetProperty("records", out var recordsEl) + && recordsEl.ValueKind == JsonValueKind.Array) + { + records = recordsEl.EnumerateArray().ToList(); + } + else + { + httpOk = false; + break; + } + + if (records.Count == 0) + { + break; + } + + rowsThisPass += records.Count; + AccumulateMaxJeecgSyncCursorUtc(records, ref maxCursorUtcFromData); + await ProcessJeecgUserRecordsAsync(records, templateUser, currentUser, defaultTenantId, skipUnchanged); + + if (records.Count < scadaPageSize) + { + break; + } + + pageNo++; + } + + if (!httpOk) + { + return (false, rowsThisPass); + } + + if (useIncremental && rowsThisPass == 0) + { + continue; + } + + // 接口 success 但本批 0 条:不能当作同步成功,否则界面误导且水位仍会推进 + if (rowsThisPass == 0) + { + return (false, 0); + } + + syncState.LastUserListSyncUtc = maxCursorUtcFromData ?? DateTime.UtcNow; + stateStore.Save(syncState); + return (true, rowsThisPass); + } + + var stdPageNo = 1; + while (true) + { + var listUrl = $"{baseUrl}{userListPath}?pageNo={stdPageNo}&pageSize={standardPageSize}"; + if (incrementalCursorLocal.HasValue) + { + var beginStr = incrementalCursorLocal.Value.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); + listUrl += $"&updateTime_begin={Uri.EscapeDataString(beginStr)}"; + } + + using var req = new HttpRequestMessage(HttpMethod.Get, listUrl); + AddJeecgAccessTokenIfPresent(req, jeecgToken); + using var resp = await _httpClient.SendAsync(req); + if (!resp.IsSuccessStatusCode) + { + httpOk = false; + break; + } + + var json = await resp.Content.ReadAsStringAsync(); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + if (!(root.TryGetProperty("success", out var successEl) && successEl.GetBoolean())) + { + httpOk = false; + break; + } + + if (!root.TryGetProperty("result", out var resultEl) || resultEl.ValueKind != JsonValueKind.Object) + { + httpOk = false; + break; + } + + if (!resultEl.TryGetProperty("records", out var recordsEl) || recordsEl.ValueKind != JsonValueKind.Array) + { + httpOk = false; + break; + } + + var records = recordsEl.EnumerateArray().ToList(); + if (records.Count == 0) + { + break; + } + + rowsThisPass += records.Count; + await ProcessJeecgUserRecordsAsync(records, templateUser, currentUser, defaultTenantId, skipUnchanged); + + long total = 0; + if (resultEl.TryGetProperty("total", out var totalEl)) + { + if (totalEl.TryGetInt64(out var tl)) + { + total = tl; + } + else if (totalEl.TryGetInt32(out var ti)) + { + total = ti; + } + } + + if (total <= (long)stdPageNo * standardPageSize) + { + break; + } + + stdPageNo++; + } + + if (!httpOk) + { + return (false, rowsThisPass); + } + + if (useIncremental && rowsThisPass == 0) + { + continue; + } + + if (rowsThisPass == 0) + { + return (false, 0); + } + + syncState.LastUserListSyncUtc = DateTime.UtcNow; + stateStore.Save(syncState); + return (true, rowsThisPass); + } + + return (false, 0); + } + + /// + /// 登录页一键同步:依赖 SCADA 免登录 queryUser(无需 Token);空库时用配置租户/机构占位写入新用户。 + /// + public async Task<(bool Success, string Message)> SyncJeecgUsersToLocalFromLoginScreenAsync(CancellationToken cancellationToken = default) + { + if (!_configuration.GetValue("JeecgIntegration:Enabled", false)) + { + return (false, "未启用 Jeecg 集成(JeecgIntegration:Enabled)。"); + } + + var baseUrl = _configuration.GetValue("JeecgIntegration:BaseUrl")?.TrimEnd('/'); + if (string.IsNullOrWhiteSpace(baseUrl)) + { + return (false, "未配置 JeecgIntegration:BaseUrl。"); + } + + var userListPath = _configuration.GetValue("JeecgIntegration:UserListPath") ?? string.Empty; + if (!userListPath.Contains("scada", StringComparison.OrdinalIgnoreCase)) + { + return (false, "一键同步需使用免登录 SCADA 接口。请将 UserListPath 配置为 /sys/user/scada/queryUser,或先登录后由系统内定时同步。"); + } + + try + { + cancellationToken.ThrowIfCancellationRequested(); + var (ok, apiRows, _) = await SyncScadaUsersToJeecgMirrorTableAsync(baseUrl, userListPath, cancellationToken); + return ok + ? (true, $"已从 Jeecg 拉取用户并写入本地同构表 jeecg_sys_user(接口累计 {apiRows} 条)。") + : (false, apiRows == 0 + ? "同步未完成:接口未返回任何用户记录。请检查 Jeecg queryUser、BaseUrl/UserListPath、网络与白名单;若用 Navicat 查看数据,请打开程序运行目录(如 bin\\Debug\\...\\win-x64)下的 Admin.NET.db,而非仅源码目录下的文件。" + : "同步未完成:请检查 Jeecg 地址、网络,或查看接口是否返回 success=true。"); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception ex) + { + return (false, $"同步异常:{ex.Message}"); + } + } + + /// + /// 登录页按钮专用:将 SCADA 接口用户同步到 Jeecg 同构表(jeecg_sys_user)。 + /// + private async Task<(bool Success, int ApiRecordRows, int ChangedRows)> SyncScadaUsersToJeecgMirrorTableAsync(string baseUrl, string userListPath, CancellationToken cancellationToken) + { + var scadaPageSize = Math.Clamp(_configuration.GetValue("JeecgIntegration:ScadaUserPageSize", 500), 1, 1000); + var scadaIncludeDetail = _configuration.GetValue("JeecgIntegration:ScadaUserIncludeDetail", false); + + var pageNo = 1; + var totalRows = 0; + var changedRows = 0; + var remoteIds = new HashSet(StringComparer.OrdinalIgnoreCase); + var remoteUsernames = new HashSet(StringComparer.OrdinalIgnoreCase); + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + var listUrl = + $"{baseUrl}{userListPath}?pageNo={pageNo.ToString(CultureInfo.InvariantCulture)}&pageSize={scadaPageSize.ToString(CultureInfo.InvariantCulture)}&includeDetail={(scadaIncludeDetail ? "true" : "false")}"; + using var req = new HttpRequestMessage(HttpMethod.Get, listUrl); + using var resp = await _httpClient.SendAsync(req, cancellationToken); + if (!resp.IsSuccessStatusCode) + { + return (false, totalRows, changedRows); + } + + var json = await resp.Content.ReadAsStringAsync(cancellationToken); + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + if (!(root.TryGetProperty("success", out var successEl) && successEl.GetBoolean())) + { + return (false, totalRows, changedRows); + } + + if (!root.TryGetProperty("result", out var resultEl)) + { + return (false, totalRows, changedRows); + } + + List records; + if (resultEl.ValueKind == JsonValueKind.Array) + { + records = resultEl.EnumerateArray().ToList(); + } + else if (resultEl.ValueKind == JsonValueKind.Object + && resultEl.TryGetProperty("records", out var recordsEl) + && recordsEl.ValueKind == JsonValueKind.Array) + { + records = recordsEl.EnumerateArray().ToList(); + } + else + { + return (false, totalRows, changedRows); + } + + if (records.Count == 0) + { + break; + } + + CollectRemoteUserKeys(records, remoteIds, remoteUsernames); + totalRows += records.Count; + changedRows += await UpsertJeecgMirrorUsersAsync(records); + + long total = 0; + if (resultEl.ValueKind == JsonValueKind.Object && resultEl.TryGetProperty("total", out var totalEl)) + { + if (totalEl.TryGetInt64(out var tl)) + { + total = tl; + } + else if (totalEl.TryGetInt32(out var ti)) + { + total = ti; + } + } + + if (total > 0) + { + if (total <= (long)pageNo * scadaPageSize) + { + break; + } + } + else if (records.Count < scadaPageSize) + { + break; + } + + pageNo++; + } + + var deletedRows = await DeleteMissingJeecgMirrorUsersAsync(remoteIds, remoteUsernames); + changedRows += deletedRows; + return (true, totalRows, changedRows); + } + + /// + /// 收集远端用户主键集合,用于全量同步后的本地删除对齐。 + /// + private static void CollectRemoteUserKeys( + IReadOnlyList records, + ISet remoteIds, + ISet remoteUsernames) + { + foreach (var userEl in records) + { + if (userEl.ValueKind != JsonValueKind.Object) + { + continue; + } + + if (userEl.TryGetProperty("id", out var idEl)) + { + var idValue = idEl.ValueKind switch + { + JsonValueKind.String => idEl.GetString(), + JsonValueKind.Number => idEl.GetRawText(), + _ => null + }; + if (!string.IsNullOrWhiteSpace(idValue)) + { + remoteIds.Add(idValue); + } + } + + if (userEl.TryGetProperty("username", out var usernameEl) && usernameEl.ValueKind == JsonValueKind.String) + { + var username = usernameEl.GetString(); + if (!string.IsNullOrWhiteSpace(username)) + { + remoteUsernames.Add(username); + } + } + } + } + + /// + /// 删除本地 jeecg_sys_user 中已被后端删除的账号,保持镜像一致。 + /// + private async Task DeleteMissingJeecgMirrorUsersAsync(ISet remoteIds, ISet remoteUsernames) + { + var localRows = await _dbContext.Queryable() + .ClearFilter() + .Select(x => new { x.Id, x.Username }) + .ToListAsync(); + + if (localRows.Count == 0) + { + return 0; + } + + var staleIds = localRows + .Where(x => !string.IsNullOrWhiteSpace(x.Id) + && !remoteIds.Contains(x.Id) + && (string.IsNullOrWhiteSpace(x.Username) || !remoteUsernames.Contains(x.Username))) + .Select(x => x.Id!) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (staleIds.Count == 0) + { + return 0; + } + + var deletedRows = await _dbContext.Deleteable() + .In(it => it.Id, staleIds) + .ExecuteCommandAsync(); + + _logger.Information($"Jeecg镜像删除同步: 表=jeecg_sys_user, 删除行数={deletedRows}"); + return deletedRows; + } + + /// + /// 写入 Jeecg 同构用户表 jeecg_sys_user(按 id 或 username 做幂等更新)。 + /// + private async Task UpsertJeecgMirrorUsersAsync(IReadOnlyList records) + { + var changedRows = 0; + foreach (var userEl in records) + { + var remoteUser = ParseJeecgUser(userEl); + if (string.IsNullOrWhiteSpace(remoteUser.Username)) + { + continue; + } + + var remoteId = remoteUser.JeecgBizUserId; + if (string.IsNullOrWhiteSpace(remoteId) && userEl.TryGetProperty("id", out var idEl)) + { + if (idEl.ValueKind == JsonValueKind.String) + { + remoteId = idEl.GetString(); + } + else if (idEl.ValueKind == JsonValueKind.Number) + { + remoteId = idEl.GetRawText(); + } + } + remoteId ??= remoteUser.Username!; + + var existing = (await _dbContext.Queryable() + .ClearFilter() + .Where(x => x.Id == remoteId || x.Username == remoteUser.Username) + .Take(1) + .ToListAsync()).FirstOrDefault(); + var isUpdate = existing != null; + + var oldAccount = existing?.Username; + var oldRealname = existing?.Realname; + var oldPhone = existing?.Phone; + var oldUpdateTime = existing?.UpdateTime?.ToString("yyyy-MM-dd HH:mm:ss"); + + if (existing == null) + { + existing = new JeecgSysUser { Id = remoteId }; + } + + existing.Username = remoteUser.Username; + existing.Realname = remoteUser.RealName; + existing.Password = remoteUser.PasswordHex; + existing.Salt = remoteUser.Salt; + existing.Birthday = remoteUser.Birthday; + existing.Sex = remoteUser.Sex.HasValue ? (int?)remoteUser.Sex.Value : null; + existing.Email = remoteUser.Email; + existing.Phone = remoteUser.Phone; + existing.OrgCode = remoteUser.OrgCode; + existing.LoginTenantId = remoteUser.TenantId.HasValue ? (int?)remoteUser.TenantId.Value : null; + existing.Status = remoteUser.Status.HasValue ? (int?)remoteUser.Status.Value : null; + existing.WorkNo = remoteUser.JobNum; + existing.Telephone = remoteUser.OfficePhone; + existing.CreateTime = remoteUser.JeecgSourceCreateTime; + existing.UpdateTime = remoteUser.JeecgSourceUpdateTime; + existing.DepartIds = remoteUser.DepartIds; + existing.ClientId = userEl.TryGetProperty("clientId", out var clientIdEl) ? clientIdEl.GetString() : existing.ClientId; + existing.BpmStatus = userEl.TryGetProperty("bpmStatus", out var bpmStatusEl) ? bpmStatusEl.GetString() : existing.BpmStatus; + existing.Sign = userEl.TryGetProperty("sign", out var signEl) ? signEl.GetString() : existing.Sign; + existing.SignEnable = userEl.TryGetProperty("signEnable", out var signEnableEl) && signEnableEl.TryGetInt32(out var signEnable) + ? signEnable + : existing.SignEnable; + existing.MainDepPostId = userEl.TryGetProperty("mainDepPostId", out var mainDepPostIdEl) ? mainDepPostIdEl.GetString() : existing.MainDepPostId; + existing.PositionType = userEl.TryGetProperty("positionType", out var positionTypeEl) ? positionTypeEl.GetString() : existing.PositionType; + existing.LastPwdUpdateTime = userEl.TryGetProperty("lastPwdUpdateTime", out var lastPwdUpdateEl) ? TryParseJeecgRemoteDateTime(lastPwdUpdateEl) : existing.LastPwdUpdateTime; + existing.Sort = userEl.TryGetProperty("sort", out var sortEl) && sortEl.TryGetInt32(out var sort) ? sort : existing.Sort; + existing.IzHideContact = userEl.TryGetProperty("izHideContact", out var hideContactEl) ? hideContactEl.GetString() : existing.IzHideContact; + + var newAccount = existing.Username; + var newRealname = existing.Realname; + var newPhone = existing.Phone; + var newUpdateTime = existing.UpdateTime?.ToString("yyyy-MM-dd HH:mm:ss"); + + var hasChanged = !isUpdate + || !string.Equals(oldAccount, newAccount, StringComparison.Ordinal) + || !string.Equals(oldRealname, newRealname, StringComparison.Ordinal) + || !string.Equals(oldPhone, newPhone, StringComparison.Ordinal) + || !string.Equals(oldUpdateTime, newUpdateTime, StringComparison.Ordinal); + if (!hasChanged) + { + continue; + } + + if (isUpdate) + { + await _dbContext.Updateable(existing).ExecuteCommandAsync(); + } + else + { + await _dbContext.Insertable(existing).ExecuteCommandAsync(); + } + changedRows++; + + _logger.Information( + $"Jeecg镜像写库明细[{(isUpdate ? "UPDATE" : "INSERT")}] 表=jeecg_sys_user, " + + $"账号: {ToLogValue(oldAccount)} -> {ToLogValue(newAccount)}, " + + $"姓名: {ToLogValue(oldRealname)} -> {ToLogValue(newRealname)}, " + + $"手机: {ToLogValue(oldPhone)} -> {ToLogValue(newPhone)}, " + + $"更新时间: {ToLogValue(oldUpdateTime)} -> {ToLogValue(newUpdateTime)}"); + } + return changedRows; + } + + private static string ToLogValue(string? value) + { + return string.IsNullOrWhiteSpace(value) ? "" : value; + } + + /// + /// 后台使用缓存 Token 同步用户(不弹窗) + /// + public async Task TryBackgroundSyncJeecgUsersAsync(CancellationToken cancellationToken = default) + { + if (!_configuration.GetValue("JeecgIntegration:Enabled", false)) + { + return false; + } + + if (!_configuration.GetValue("JeecgIntegration:SyncAllUsersOnJeecgLogin", false)) + { + return false; + } + + var userListPath = _configuration.GetValue("JeecgIntegration:UserListPath") ?? string.Empty; + var isScadaUserPath = userListPath.Contains("scada", StringComparison.OrdinalIgnoreCase); + var user = _currentUser; + if (user == null && !isScadaUserPath) + { + return false; + } + + var token = user == null ? string.Empty : _sysCacheService.Get($"jeecg:token:{user.Id}"); + if (string.IsNullOrWhiteSpace(token) && !isScadaUserPath) + { + return false; + } + + var baseUrl = _configuration.GetValue("JeecgIntegration:BaseUrl")?.TrimEnd('/'); + if (string.IsNullOrWhiteSpace(baseUrl)) + { + return false; + } + + cancellationToken.ThrowIfCancellationRequested(); + try + { + (bool ok, int rows, int changedRows) result; + if (isScadaUserPath) + { + // 实时接收链路统一写入 jeecg_sys_user,避免写到 sys_user 导致展示与同步表不一致 + result = await SyncScadaUsersToJeecgMirrorTableAsync(baseUrl, userListPath, cancellationToken); + } + else + { + var standardResult = await SyncAllJeecgUsersAsync(baseUrl, token ?? string.Empty, user!); + result = (standardResult.Success, standardResult.ApiRecordRows, standardResult.ApiRecordRows); + } + + var (ok, rows, changedRows) = result; + if (ok && changedRows > 0) + { + _eventAggregator.GetEvent().Publish(rows); + } + return ok; + } + catch (OperationCanceledException) + { + throw; + } + catch + { + return false; + } + } + + /// + /// 解析 Jeecg userInfo.id(字符串或数字雪花),超出 long 范围则返回 null + /// + private static long? TryParseJeecgSnowflakeUserIdFromJson(JsonElement idEl) + { + switch (idEl.ValueKind) + { + case JsonValueKind.Number: + if (idEl.TryGetInt64(out var n) && n > 0) + { + return n; + } + + var raw = idEl.GetRawText(); + if (long.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var r) && r > 0) + { + return r; + } + + return null; + case JsonValueKind.String: + if (long.TryParse(idEl.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var s) && s > 0) + { + return s; + } + + return null; + default: + return null; + } + } + + /// + /// 将 sys_user 主键从 oldId 改为 newId(SQLite),并同步常见子表 user_id + /// + private async Task TryMigrateSysUserPrimaryKeyToJeecgIdAsync(long oldId, long newId) + { + if (oldId == newId) + { + return; + } + + await _dbContext.Ado.BeginTranAsync(); + try + { + await _dbContext.Ado.ExecuteCommandAsync("PRAGMA foreign_keys = OFF"); + var fkUpdates = new (string Table, string Column)[] + { + ("sys_user_role", "user_id"), + ("sys_user_menu", "user_id"), + ("sys_user_ext_org", "user_id"), + ("sys_user_ldap", "user_id"), + ("sys_user_config_data", "user_id"), + ("sys_online_user", "user_id"), + ("sys_notice_user", "user_id"), + ("sys_schedule", "user_id"), + ("sys_tenant", "user_id"), + }; + foreach (var (table, column) in fkUpdates) + { + await _dbContext.Ado.ExecuteCommandAsync( + $"UPDATE {table} SET {column} = @newId WHERE {column} = @oldId", + new { newId, oldId }); + } + + await _dbContext.Ado.ExecuteCommandAsync( + "UPDATE sys_user SET id = @newId WHERE id = @oldId", + new { newId, oldId }); + await _dbContext.Ado.ExecuteCommandAsync("PRAGMA foreign_keys = ON"); + await _dbContext.Ado.CommitTranAsync(); + } + catch + { + await _dbContext.Ado.RollbackTranAsync(); + try + { + await _dbContext.Ado.ExecuteCommandAsync("PRAGMA foreign_keys = ON"); + } + catch + { + // 忽略恢复失败 + } + + throw; + } + } + + private static DateTime? TryParseJeecgRemoteDateTime(JsonElement el) + { + if (el.ValueKind == JsonValueKind.String) + { + if (DateTime.TryParse(el.GetString(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var d1)) + { + return d1; + } + + if (DateTime.TryParse(el.GetString(), CultureInfo.GetCultureInfo("zh-CN"), DateTimeStyles.AssumeLocal, out var d2)) + { + return d2; + } + + return null; + } + + if (el.ValueKind == JsonValueKind.Number && el.TryGetInt64(out var n)) + { + if (n > 1_000_000_000_000L) + { + return DateTimeOffset.FromUnixTimeMilliseconds(n).LocalDateTime; + } + + if (n > 1_000_000_000L) + { + return DateTimeOffset.FromUnixTimeSeconds(n).LocalDateTime; + } + } + + return null; + } + + private static JeecgUserProfile ParseJeecgUser(JsonElement userInfoEl) + { + var profile = new JeecgUserProfile(); + if (userInfoEl.ValueKind != JsonValueKind.Object) return profile; + + if (userInfoEl.TryGetProperty("username", out var usernameEl)) profile.Username = usernameEl.GetString(); + if (string.IsNullOrWhiteSpace(profile.Username) && userInfoEl.TryGetProperty("account", out var accountEl)) + { + profile.Username = accountEl.ValueKind == JsonValueKind.String ? accountEl.GetString() : null; + } + if (userInfoEl.TryGetProperty("realname", out var realnameEl)) profile.RealName = realnameEl.GetString(); + if (userInfoEl.TryGetProperty("nickname", out var nicknameEl)) profile.NickName = nicknameEl.GetString(); + if (userInfoEl.TryGetProperty("phone", out var phoneEl)) profile.Phone = phoneEl.GetString(); + if (userInfoEl.TryGetProperty("email", out var emailEl)) profile.Email = emailEl.GetString(); + if (userInfoEl.TryGetProperty("avatar", out var avatarEl)) profile.Avatar = avatarEl.GetString(); + if (userInfoEl.TryGetProperty("address", out var addressEl)) profile.Address = addressEl.GetString(); + if (userInfoEl.TryGetProperty("telephone", out var telephoneEl)) profile.OfficePhone = telephoneEl.GetString(); + if (userInfoEl.TryGetProperty("workNo", out var workNoEl)) profile.JobNum = workNoEl.GetString(); + if (userInfoEl.TryGetProperty("idCard", out var idCardEl)) profile.IdCardNum = idCardEl.GetString(); + if (userInfoEl.TryGetProperty("description", out var descriptionEl)) profile.Remark = descriptionEl.GetString(); + + if (userInfoEl.TryGetProperty("birthday", out var birthdayEl) && + birthdayEl.ValueKind == JsonValueKind.String && + DateTime.TryParse(birthdayEl.GetString(), CultureInfo.InvariantCulture, DateTimeStyles.None, out var birthday)) + { + profile.Birthday = birthday; + } + + if (userInfoEl.TryGetProperty("sex", out var sexEl)) + { + profile.Sex = ResolveUserSex(sexEl); + } + + if (userInfoEl.TryGetProperty("status", out var statusEl)) + { + profile.Status = ResolveUserStatus(statusEl); + } + + if (TryResolveTenantId(userInfoEl, out var tenantId)) + { + profile.TenantId = tenantId; + } + + if (userInfoEl.TryGetProperty("updateTime", out var updateTimeEl)) + { + profile.JeecgSourceUpdateTime = TryParseJeecgRemoteDateTime(updateTimeEl); + } + + if (userInfoEl.TryGetProperty("createTime", out var createTimeEl)) + { + profile.JeecgSourceCreateTime = TryParseJeecgRemoteDateTime(createTimeEl); + } + + if (userInfoEl.TryGetProperty("id", out var idEl)) + { + profile.JeecgBizUserId = idEl.ValueKind switch + { + JsonValueKind.String => TruncateJeecgSyncField(idEl.GetString(), 64), + JsonValueKind.Number when idEl.TryGetInt64(out var idNum) => idNum.ToString(CultureInfo.InvariantCulture), + JsonValueKind.Number => TruncateJeecgSyncField(idEl.GetRawText(), 64), + _ => profile.JeecgBizUserId + }; + } + + if (userInfoEl.TryGetProperty("orgCode", out var orgCodeEl) && orgCodeEl.ValueKind == JsonValueKind.String) + { + profile.OrgCode = TruncateJeecgSyncField(orgCodeEl.GetString(), 64); + } + + if (userInfoEl.TryGetProperty("departIds", out var departIdsEl) && departIdsEl.ValueKind == JsonValueKind.String) + { + profile.DepartIds = TruncateJeecgSyncField(departIdsEl.GetString(), 512); + } + + if (userInfoEl.TryGetProperty("post", out var postEl) && postEl.ValueKind == JsonValueKind.String) + { + profile.PosTitle = TruncateJeecgSyncField(postEl.GetString(), 32); + } + + // Jeecg 密码密文(十六进制)与盐,用于本地按 PBEWithMD5AndDES 校验(与 SCADA 同步接口字段一致) + if (userInfoEl.TryGetProperty("password", out var pwdHexEl) && pwdHexEl.ValueKind == JsonValueKind.String) + { + profile.PasswordHex = TruncateJeecgSyncField(pwdHexEl.GetString(), 512); + } + + if (userInfoEl.TryGetProperty("salt", out var saltEl) && saltEl.ValueKind == JsonValueKind.String) + { + profile.Salt = TruncateJeecgSyncField(saltEl.GetString(), 64); + } + + return profile; + } + + /// + /// 截断同步字段,避免超过库表长度 + /// + private static string? TruncateJeecgSyncField(string? value, int maxLen) + { + if (string.IsNullOrEmpty(value) || maxLen <= 0) + { + return value; + } + + return value.Length <= maxLen ? value : value[..maxLen]; + } + + private static bool TryResolveTenantId(JsonElement userInfoEl, out long tenantId) + { + tenantId = 0; + if (userInfoEl.TryGetProperty("loginTenantId", out var loginTenantIdEl)) + { + if (loginTenantIdEl.ValueKind == JsonValueKind.Number && loginTenantIdEl.TryGetInt64(out tenantId) && tenantId > 0) + { + return true; + } + + if (loginTenantIdEl.ValueKind == JsonValueKind.String + && long.TryParse(loginTenantIdEl.GetString(), out tenantId) + && tenantId > 0) + { + return true; + } + } + + // SCADA 等接口:tenantList[].tenantUserId + if (userInfoEl.TryGetProperty("tenantList", out var tenantListEl) && tenantListEl.ValueKind == JsonValueKind.Array) + { + foreach (var t in tenantListEl.EnumerateArray()) + { + if (!t.TryGetProperty("tenantUserId", out var tuidEl)) + { + continue; + } + + if (tuidEl.ValueKind == JsonValueKind.Number && tuidEl.TryGetInt64(out tenantId) && tenantId > 0) + { + return true; + } + + if (tuidEl.ValueKind == JsonValueKind.String + && long.TryParse(tuidEl.GetString(), out tenantId) + && tenantId > 0) + { + return true; + } + } + } + + if (userInfoEl.TryGetProperty("relTenantIds", out var relTenantIdsEl) && relTenantIdsEl.ValueKind == JsonValueKind.String) + { + var firstTenant = relTenantIdsEl.GetString()?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(firstTenant) && long.TryParse(firstTenant, out tenantId) && tenantId > 0) + { + return true; + } + } + + return false; + } + + private sealed class JeecgUserProfile + { + public string? Username { get; set; } + public string? RealName { get; set; } + public string? NickName { get; set; } + public string? Phone { get; set; } + public string? Email { get; set; } + public string? Avatar { get; set; } + public string? Address { get; set; } + public string? OfficePhone { get; set; } + public string? JobNum { get; set; } + public string? IdCardNum { get; set; } + public string? Remark { get; set; } + public DateTime? Birthday { get; set; } + public GenderEnum? Sex { get; set; } + public StatusEnum? Status { get; set; } + public long? TenantId { get; set; } + /// + /// Jeecg 返回的 updateTime,用于跳过未变化的本地更新 + /// + public DateTime? JeecgSourceUpdateTime { get; set; } + /// + /// Jeecg createTime(仅新建本地用户时写入 CreateTime) + /// + public DateTime? JeecgSourceCreateTime { get; set; } + public string? JeecgBizUserId { get; set; } + public string? OrgCode { get; set; } + public string? DepartIds { get; set; } + public string? PosTitle { get; set; } + /// Jeecg sys_user.password 十六进制密文 + public string? PasswordHex { get; set; } + /// Jeecg sys_user.salt + public string? Salt { get; set; } + } + + /// + /// 解析Jeecg用户性别(1男、2女,其他未知) + /// + private static GenderEnum ResolveUserSex(JsonElement sexEl) + { + if (sexEl.ValueKind == JsonValueKind.Number && sexEl.TryGetInt32(out var numSex)) + { + return numSex switch + { + 1 => GenderEnum.Male, + 2 => GenderEnum.Female, + _ => GenderEnum.Unknown + }; + } + + if (sexEl.ValueKind == JsonValueKind.String && int.TryParse(sexEl.GetString(), out var strSex)) + { + return strSex switch + { + 1 => GenderEnum.Male, + 2 => GenderEnum.Female, + _ => GenderEnum.Unknown + }; + } + + return GenderEnum.Unknown; + } + + /// + /// 解析Jeecg用户状态(1启用,其他禁用) + /// + private static StatusEnum ResolveUserStatus(JsonElement statusEl) + { + if (statusEl.ValueKind == JsonValueKind.Number && statusEl.TryGetInt32(out var numStatus)) + { + return numStatus == 1 ? StatusEnum.Enable : StatusEnum.Disable; + } + + if (statusEl.ValueKind == JsonValueKind.String && int.TryParse(statusEl.GetString(), out var strStatus)) + { + return strStatus == 1 ? StatusEnum.Enable : StatusEnum.Disable; + } + + return StatusEnum.Disable; + } + + /// + /// 将常见英文异常转换成中文可读提示 + /// + private static string ToFriendlyJeecgErrorMessage(string? errorMessage) + { + if (string.IsNullOrWhiteSpace(errorMessage)) + { + return "未知错误,请检查Jeecg服务与网络连接"; + } + + var msg = errorMessage.Trim(); + if (msg.Contains("The requested operation requires an element of type 'Number', but the target element has type 'Null'", StringComparison.OrdinalIgnoreCase)) + { + return "后台返回的数据格式不正确:需要数字字段但返回了空值(null)"; + } + + if (msg.Contains("No such host is known", StringComparison.OrdinalIgnoreCase)) + { + return "无法连接Jeecg服务,请检查后端地址或网络"; + } + + if (msg.Contains("Connection refused", StringComparison.OrdinalIgnoreCase) + || msg.Contains("积极拒绝", StringComparison.Ordinal) + || msg.Contains("拒绝连接", StringComparison.Ordinal)) + { + return "Jeecg服务拒绝连接,请确认后端服务已启动"; + } + + if (msg.Contains("timed out", StringComparison.OrdinalIgnoreCase)) + { + return "请求Jeecg超时,请检查网络或后端响应"; + } + + return msg; + } + + /// + ///密码验证 + /// + /// + /// + /// + /// + /// + public bool VerifyPassword(string password, string keyPasswordErrorTimes, int passwordErrorTimes, SysUser user) + { + // 已从 Jeecg 同步的用户:携带 salt 时仅按 JeecgBoot PasswordUtil(PBEWithMD5AndDES)校验,不再走 SM2/MD5 以免误判 + if (!string.IsNullOrWhiteSpace(user.JeecgPasswordSalt)) + { + if (JeecgPasswordUtil.Verify(user.Account, password, user.JeecgPasswordSalt, user.Password, Encoding.UTF8)) + { + return true; + } + + // 少数 Jeecg 环境 salt 按 JVM 默认编码落库,与 UTF-8 不一致时再尝试本机默认编码 + if (Encoding.Default.CodePage != Encoding.UTF8.CodePage + && JeecgPasswordUtil.Verify(user.Account, password, user.JeecgPasswordSalt, user.Password, Encoding.Default)) + { + return true; + } + + _sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30)); + return false; + } + + if (CryptogramUtil.CryptoType == CryptogramEnum.MD5.ToString()) + { + if (user.Password.Equals(CryptogramUtil.Encrypt(password)))return true; + } + else + { + if (CryptogramUtil.Decrypt(user.Password).Equals(password)) return true; + } + _sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30)); + return false; + } + public async Task IsAuthenticatedAsync() + { + await Task.Delay(50); + return _currentUser != null; + } + + private async Task getSysTokenExpireAsync() + { + return await _sysConfigService.GetConfigValue(ConfigConst.SysTokenExpire); + } + + + + /// + ///创建token + /// + /// + private async Task GenerateToken(SysUser user) + { + // 生成访问令牌(实际项目应使用JWT) + var accessToken = GenerateSecureToken(32); + var refreshToken = GenerateSecureToken(32); + var refreshExpires = await getSysTokenExpireAsync(); + var refreshExpiration = DateTime.Now.AddMinutes(refreshExpires); + // 存储Token关联信息 + _tokenStore[accessToken] = new UserContext + { + UserId = user.Id, + IsSuperAdmin = user.AccountType == AccountTypeEnum.SuperAdmin, + Account= user.Account, + AccountType = user.AccountType, + OrgId= user.OrgId, + RealName = user.RealName, + TenantId= user.TenantId!.Value, + Token=new UserToken() { + RefreshToken = refreshToken, + RefreshExpires = refreshExpiration, + } + }; + return new UserToken + { + AccessToken = accessToken, + RefreshToken = refreshToken, + RefreshExpires = refreshExpiration + }; + } + private string GenerateSecureToken(int length) + { + using var rng = RandomNumberGenerator.Create(); + var tokenData = new byte[length]; + rng.GetBytes(tokenData); + return Convert.ToBase64String(tokenData); + } + + /// + /// 实现Token验证 + /// + /// + /// + public bool ValidateToken(string accessToken) + { + if (string.IsNullOrEmpty(accessToken)) + return false; + + return _tokenStore.TryGetValue(accessToken, out var tokenInfo) && + tokenInfo.Token.RefreshExpires >= DateTime.Now; + } + + public async Task RefreshToken(string? accessToken) + { + if (string.IsNullOrEmpty(accessToken)) + return; + if (_tokenStore.TryGetValue(accessToken, out var tokenInfo)) + { + if (tokenInfo.Token.RefreshExpires - DateTime.Now <= _idleTimeout) + { + var refreshExpires = await getSysTokenExpireAsync(); + tokenInfo.Token.RefreshExpires = DateTime.Now.AddMinutes(refreshExpires); + } + } + } + + + /// + /// 实现刷新Token + /// + /// + /// + /// + public async Task RefreshToken(string accessToken, string refreshToken) + { + if (!_tokenStore.TryGetValue(accessToken, out var tokenInfo) || + tokenInfo.Token.RefreshToken != refreshToken || + tokenInfo.Token.RefreshExpires < DateTime.Now) + { + return new RefreshTokenResult { Success = false }; + } + + // 获取用户信息 + var user = await _dbContext.Queryable() + .Where(u => u.Id == tokenInfo.UserId) + .FirstAsync(); + + if (user == null) + return new RefreshTokenResult { Success = false }; + + // 生成新Token + var newToken = GenerateSecureToken(32); + var newRefreshToken = GenerateSecureToken(32); + + // 更新Token存储 + _tokenStore.TryRemove(accessToken, out _); + + var refreshExpires = await getSysTokenExpireAsync(); + _tokenStore[newToken] = new UserContext + { + UserId = user.Id, + IsSuperAdmin = user.AccountType == AccountTypeEnum.SuperAdmin, + Account = user.Account, + AccountType = user.AccountType, + OrgId = user.OrgId, + RealName = user.RealName, + TenantId = user.TenantId!.Value, + Token = new UserToken() + { + RefreshToken = refreshToken, + RefreshExpires = DateTime.Now.AddMinutes(refreshExpires), + } + }; + + return new RefreshTokenResult + { + Success = true, + AccessToken = newToken, + RefreshToken = newRefreshToken + }; + } + + /// + /// 增强退出登录实现 + /// + /// + public void LogoutAsync() + { + if (_currentUser != null) + { + // 移除关联的Token + var tokensToRemove = _tokenStore + .Where(kvp => kvp.Value.UserId == _currentUser.Id) + .Select(kvp => kvp.Key) + .ToList(); + + foreach (var token in tokensToRemove) + { + _tokenStore.TryRemove(token, out _); + } + } + AppSession.CurrentUser = null; // 更新静态会话 + _currentUser = null; + UserChanged?.Invoke(this, null); + //await Task.Delay(1000); + } + + public async Task UpdateUserLoginInfoAsync(SysUser user) + { + // 延迟1秒再执行后续的用户登录信息更新操作,目的是避免与主线程的资源竞争或操作冲突 + // 如果不延迟执行会影响主窗口TabControl区域视图导航速度,影响用户体验 + await Task.Delay(2000); + + user.LastLoginIp = await DeviceInfoUtil.GetPublicIpAddressAsync(); + //(user.LastLoginAddress, double? longitude, double? latitude) = CommonUtil.GetIpAddress(user.LastLoginIp); + user.LastLoginTime = DateTime.Now; + user.LastLoginDevice = DeviceInfoUtil.GetOsVersion(); + await _dbContext.Updateable(user).UpdateColumns(u => new + { + u.LastLoginIp, + //u.LastLoginAddress, + u.LastLoginTime, + u.LastLoginDevice, + }).ExecuteCommandAsync(); + } + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/AutoUpdate/AutoUpdateService.cs b/yy-admin-master/YY.Admin.Services/Service/AutoUpdate/AutoUpdateService.cs new file mode 100644 index 0000000..b9f5f0e --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/AutoUpdate/AutoUpdateService.cs @@ -0,0 +1,190 @@ +using AutoUpdaterDotNET; +using Microsoft.Extensions.Configuration; +using System.IO; +using System.Net.Http; +using System.Reflection; +using System.Xml; + +namespace YY.Admin.Services.Service.AutoUpdate +{ + public class AutoUpdateService : IAutoUpdateService + { + private readonly IConfiguration _configuration; + + private readonly HttpClient _httpClient; + public AutoUpdateService(IConfiguration configuration, HttpClient httpClient) + { + _configuration = configuration; + _httpClient = httpClient; + } + /// + /// 获取当前程序版本 + /// + /// + public string GetCurrentVersion() + { + try + { + var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(); + var version = assembly.GetName().Version; + return version?.ToString() ?? "1.0.0.0"; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"获取当前版本失败: {ex.Message}"); + return "1.0.0.0"; + } + } + /// + /// 获取程序版本信息(包含版本号、产品名称等) + /// + /// + public VersionInfo GetApplicationInfo() + { + try + { + var assembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(); + var version = assembly.GetName().Version; + var title = assembly.GetCustomAttribute()?.Title; + var product = assembly.GetCustomAttribute()?.Product; + var company = assembly.GetCustomAttribute()?.Company; + + return new VersionInfo + { + CurrentVersion = version?.ToString() ?? "1.0.0.0", + ApplicationName = title ?? product ?? "应用程序", + CompanyName = company ?? "", + PublishDate = File.GetLastWriteTime(assembly.Location).ToString("yyyy-MM-dd") + }; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"获取应用程序信息失败: {ex.Message}"); + return new VersionInfo + { + CurrentVersion = "1.0.0.0", + ApplicationName = "应用程序", + CompanyName = "", + PublishDate = DateTime.Now.ToString("yyyy-MM-dd") + }; + } + } + public async Task CheckForUpdatesAsync() + { + try + { + VersionInfo? versionInfo = await ReadRemoteVersionInfoAsync(); + + if (versionInfo == null) return false; + + var currentVersion = new Version(GetCurrentVersion()); + var latestVersion = new Version(versionInfo.LatestVersion); + + return latestVersion > currentVersion; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"自动更新检查失败: {ex.Message}"); + return false; + } + } + + + public async Task GetVersionInfo() + { + try + { + VersionInfo? versionInfo = await ReadRemoteVersionInfoAsync(); + if (versionInfo == null) return null; + + versionInfo.CurrentVersion = versionInfo.CurrentVersion; + versionInfo.ApplicationName = versionInfo.ApplicationName; + + return versionInfo; + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"获取版本信息失败: {ex.Message}"); + return null; + } + } + + private async Task ReadRemoteVersionInfoAsync() + { + try + { + // 从配置文件读取远程更新地址 + string remoteConfigUrl = _configuration.GetSection("AutoUpdate:RemoteConfigUrl").Value!; + + var xmlContent = await _httpClient.GetStringAsync(remoteConfigUrl); + return ParseVersionXml(xmlContent); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"读取远程版本信息失败: {ex.Message}"); + return null; + } + } + + private VersionInfo? ParseVersionXml(string xmlContent) + { + try + { + var xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(xmlContent); + + var item = xmlDoc.SelectSingleNode("item"); + if (item != null) + { + return new VersionInfo + { + LatestVersion = item.SelectSingleNode("version")?.InnerText ?? "1.0.0", + DownloadUrl = item.SelectSingleNode("url")?.InnerText ?? "", + Changelog = item.SelectSingleNode("changelog")?.InnerText ?? "暂无更新内容", + PublishDate = item.SelectSingleNode("publishDate")?.InnerText ?? DateTime.Now.ToString("yyyy-MM-dd"), + Mandatory = bool.TryParse(item.SelectSingleNode("mandatory")?.InnerText, out var mandatory) && mandatory + }; + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"解析版本XML失败: {ex.Message}"); + } + + return null; + } + + + public void StartUpdate(string downloadUrl) + { + try + { + // 设置更新完成后的回调 + AutoUpdater.ApplicationExitEvent += () => + { + // 更新完成后可以执行一些清理操作 + System.Diagnostics.Debug.WriteLine("应用程序准备退出以完成更新"); + }; + + AutoUpdater.Start(downloadUrl); + } + catch (Exception ex) + { + throw new Exception($"更新启动失败: {ex.Message}"); + } + } + + // 检查是否需要强制更新 + public bool IsMandatoryUpdate(VersionInfo versionInfo) + { + if (versionInfo == null) return false; + + var currentVersion = new Version(GetCurrentVersion()); + var latestVersion = new Version(versionInfo.LatestVersion); + + return versionInfo.Mandatory && latestVersion > currentVersion; + } + + } + +} diff --git a/yy-admin-master/YY.Admin.Services/Service/AutoUpdate/IAutoUpdateService.cs b/yy-admin-master/YY.Admin.Services/Service/AutoUpdate/IAutoUpdateService.cs new file mode 100644 index 0000000..35ba795 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/AutoUpdate/IAutoUpdateService.cs @@ -0,0 +1,22 @@ +namespace YY.Admin.Services.Service.AutoUpdate +{ + public interface IAutoUpdateService + { + Task CheckForUpdatesAsync(); + void StartUpdate(string downloadUrl); + Task GetVersionInfo(); + string GetCurrentVersion(); + } + + public class VersionInfo + { + public string CurrentVersion { get; set; } = "1.0.0.0"; + public string LatestVersion { get; set; } = "1.0.0"; + public string DownloadUrl { get; set; } = ""; + public string Changelog { get; set; } = "暂无更新内容"; + public string PublishDate { get; set; } = ""; + public string ApplicationName { get; set; } = "应用程序"; + public string CompanyName { get; set; } = ""; + public bool Mandatory { get; set; } = false; + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Config/ISysConfigService.cs b/yy-admin-master/YY.Admin.Services/Service/Config/ISysConfigService.cs new file mode 100644 index 0000000..ff3276f --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Config/ISysConfigService.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Services.Service.Config +{ + public interface ISysConfigService + { + Task GetConfigValue(string code); + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Config/SysConfigService.cs b/yy-admin-master/YY.Admin.Services/Service/Config/SysConfigService.cs new file mode 100644 index 0000000..010846b --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Config/SysConfigService.cs @@ -0,0 +1,35 @@ +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Services.Service.Config +{ + public class SysConfigService : ISysConfigService, ISingletonDependency + { + private readonly ISysCacheService _sysCacheService; + private readonly ISqlSugarClient _dbContext; + public SysConfigService( + ISysCacheService sysCacheService, + ISqlSugarClient dbContext) + { + _sysCacheService= sysCacheService; + _dbContext= dbContext; + } + public async Task GetConfigValue(string code) + { + if (string.IsNullOrWhiteSpace(code)) return default; + + var value = _sysCacheService.Get($"{CacheConst.KeyConfig}{code}"); + if (string.IsNullOrEmpty(value)) + { + value = (await _dbContext.Queryable().FirstAsync(u => u.Code == code))?.Value; + _sysCacheService.Set($"{CacheConst.KeyConfig}{code}", value); + } + if (string.IsNullOrWhiteSpace(value)) return default; + return (T)Convert.ChangeType(value, typeof(T)); + } + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Dict/Dto/JeecgDictItemOutput.cs b/yy-admin-master/YY.Admin.Services/Service/Dict/Dto/JeecgDictItemOutput.cs new file mode 100644 index 0000000..d91d3b1 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Dict/Dto/JeecgDictItemOutput.cs @@ -0,0 +1,29 @@ +namespace YY.Admin.Services.Service; + +/// +/// 数据字典项输出 +/// +public class JeecgDictItemOutput +{ + public string Id { get; set; } = string.Empty; + + public string? DictCode { get; set; } + + public string? DictName { get; set; } + + public string? ItemText { get; set; } + + public string? ItemValue { get; set; } + + public string? ItemDescription { get; set; } + + public int? SortOrder { get; set; } + + public StatusEnum Status { get; set; } = StatusEnum.Disable; + + public string? ItemColor { get; set; } + + public DateTime? CreateTime { get; set; } + + public DateTime? UpdateTime { get; set; } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Dict/Dto/PageJeecgDictItemInput.cs b/yy-admin-master/YY.Admin.Services/Service/Dict/Dto/PageJeecgDictItemInput.cs new file mode 100644 index 0000000..eb36f10 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Dict/Dto/PageJeecgDictItemInput.cs @@ -0,0 +1,17 @@ +namespace YY.Admin.Services.Service; + +/// +/// 数据字典分页查询参数 +/// +public class PageJeecgDictItemInput : PagedRequestBase +{ + public string? DictCode { get; set; } + + public string? DictName { get; set; } + + public string? ItemText { get; set; } + + public string? ItemValue { get; set; } + + public StatusEnum? Status { get; set; } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Dict/IJeecgDictSyncService.cs b/yy-admin-master/YY.Admin.Services/Service/Dict/IJeecgDictSyncService.cs new file mode 100644 index 0000000..f871594 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Dict/IJeecgDictSyncService.cs @@ -0,0 +1,11 @@ +namespace YY.Admin.Services.Service; + +public interface IJeecgDictSyncService +{ + Task> PageAsync(PageJeecgDictItemInput input); + + /// + /// 从 Jeecg 后端同步数据字典到本地同构表 + /// + Task SyncFromJeecgAsync(); +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Dict/JeecgDictSyncService.cs b/yy-admin-master/YY.Admin.Services/Service/Dict/JeecgDictSyncService.cs new file mode 100644 index 0000000..1788511 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Dict/JeecgDictSyncService.cs @@ -0,0 +1,209 @@ +using Microsoft.Extensions.Configuration; +using SqlSugar; +using System.Text.Json; +using YY.Admin.Core; +using YY.Admin.Services.Service.Jeecg; + +namespace YY.Admin.Services.Service; + +public class JeecgDictSyncService : IJeecgDictSyncService, ISingletonDependency +{ + private readonly ISqlSugarClient _dbContext; + private readonly IConfiguration _configuration; + private readonly IJeecgBackendGateway _jeecgGateway; + + public JeecgDictSyncService( + ISqlSugarClient dbContext, + IConfiguration configuration, + IJeecgBackendGateway jeecgGateway) + { + _dbContext = dbContext; + _configuration = configuration; + _jeecgGateway = jeecgGateway; + } + + public async Task> PageAsync(PageJeecgDictItemInput input) + { + var statusFilter = input.Status.HasValue ? (int?)input.Status.Value : null; + var query = _dbContext.Queryable().ClearFilter() + .WhereIF(!string.IsNullOrWhiteSpace(input.DictCode), x => x.DictCode != null && x.DictCode.Contains(input.DictCode)) + .WhereIF(!string.IsNullOrWhiteSpace(input.DictName), x => x.DictName != null && x.DictName.Contains(input.DictName)) + .WhereIF(!string.IsNullOrWhiteSpace(input.ItemText), x => x.ItemText != null && x.ItemText.Contains(input.ItemText)) + .WhereIF(!string.IsNullOrWhiteSpace(input.ItemValue), x => x.ItemValue != null && x.ItemValue.Contains(input.ItemValue)); + + if (statusFilter.HasValue) + { + var statusValue = statusFilter.Value; + query = query.Where(x => x.Status == statusValue); + } + + query = query.OrderBy(x => SqlFunc.Asc(x.DictCode)) + .OrderBy(x => SqlFunc.Asc(x.SortOrder)) + .OrderBy(x => SqlFunc.Desc(x.CreateTime)); + + RefAsync total = 0; + var list = await query.ToPageListAsync(input.Page, input.PageSize, total); + var items = list.Select(x => new JeecgDictItemOutput + { + Id = x.Id, + DictCode = x.DictCode, + DictName = x.DictName, + ItemText = x.ItemText, + ItemValue = x.ItemValue, + ItemDescription = x.ItemDescription, + SortOrder = x.SortOrder, + Status = x.Status == 1 ? StatusEnum.Enable : StatusEnum.Disable, + ItemColor = x.ItemColor, + CreateTime = x.CreateTime, + UpdateTime = x.UpdateTime + }).ToList(); + + return new SqlSugarPagedList + { + Page = input.Page, + PageSize = input.PageSize, + Total = total, + TotalPages = input.PageSize > 0 ? (int)Math.Ceiling(total / (double)input.PageSize) : 0, + HasNextPage = input.PageSize > 0 && input.Page < (int)Math.Ceiling(total / (double)input.PageSize), + HasPrevPage = input.Page > 1, + Items = items + }; + } + + public async Task SyncFromJeecgAsync() + { + var baseUrl = _configuration.GetValue("JeecgIntegration:BaseUrl")?.TrimEnd('/'); + if (string.IsNullOrWhiteSpace(baseUrl)) + { + return 0; + } + + var dictPath = _configuration.GetValue("JeecgIntegration:DictListPath") ?? "/sys/dict/scada/queryDictItem"; + const int pageSize = 500; + var pageNo = 1; + var synced = 0; + + while (true) + { + var requestUrl = $"{baseUrl}{dictPath}?pageNo={pageNo}&pageSize={pageSize}"; + var json = await _jeecgGateway.ExecuteGetStringAsync(requestUrl); + if (string.IsNullOrWhiteSpace(json)) + { + break; + } + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + if (!(root.TryGetProperty("success", out var successEl) && successEl.GetBoolean())) + { + break; + } + if (!root.TryGetProperty("result", out var recordsEl) || recordsEl.ValueKind != JsonValueKind.Array) + { + break; + } + + var currentBatch = 0; + foreach (var row in recordsEl.EnumerateArray()) + { + var id = GetString(row, "id"); + if (string.IsNullOrWhiteSpace(id)) + { + continue; + } + + var existing = await _dbContext.Queryable() + .ClearFilter() + .Where(x => x.Id == id) + .FirstAsync(); + + if (existing == null) + { + existing = new JeecgSysDictItem { Id = id }; + } + + existing.DictId = GetString(row, "dictId"); + existing.DictName = GetString(row, "dictName"); + existing.DictCode = GetString(row, "dictCode"); + existing.DictType = GetInt(row, "dictType"); + existing.DictDescription = GetString(row, "dictDescription"); + existing.ItemText = GetString(row, "itemText"); + existing.ItemValue = GetString(row, "itemValue"); + existing.ItemDescription = GetString(row, "itemDescription"); + existing.SortOrder = GetInt(row, "sortOrder"); + existing.Status = GetInt(row, "status"); + existing.ItemColor = GetString(row, "itemColor"); + existing.CreateBy = GetString(row, "createBy"); + existing.CreateTime = GetDateTime(row, "createTime"); + existing.UpdateBy = GetString(row, "updateBy"); + existing.UpdateTime = GetDateTime(row, "updateTime"); + + var existsCount = await _dbContext.Queryable() + .ClearFilter() + .Where(x => x.Id == existing.Id) + .CountAsync(); + if (existsCount > 0) + { + await _dbContext.Updateable(existing).ExecuteCommandAsync(); + } + else + { + await _dbContext.Insertable(existing).ExecuteCommandAsync(); + } + + currentBatch++; + } + + synced += currentBatch; + if (currentBatch < pageSize) + { + break; + } + pageNo++; + } + + return synced; + } + + private static string? GetString(JsonElement row, string propertyName) + { + if (!row.TryGetProperty(propertyName, out var el)) + { + return null; + } + if (el.ValueKind == JsonValueKind.String) + { + return el.GetString(); + } + return el.ToString(); + } + + private static int? GetInt(JsonElement row, string propertyName) + { + if (!row.TryGetProperty(propertyName, out var el)) + { + return null; + } + if (el.ValueKind == JsonValueKind.Number && el.TryGetInt32(out var intVal)) + { + return intVal; + } + if (el.ValueKind == JsonValueKind.String && int.TryParse(el.GetString(), out var intStr)) + { + return intStr; + } + return null; + } + + private static DateTime? GetDateTime(JsonElement row, string propertyName) + { + if (!row.TryGetProperty(propertyName, out var el)) + { + return null; + } + if (el.ValueKind == JsonValueKind.String && DateTime.TryParse(el.GetString(), out var dt)) + { + return dt; + } + return null; + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Jeecg/IJeecgBackendGateway.cs b/yy-admin-master/YY.Admin.Services/Service/Jeecg/IJeecgBackendGateway.cs new file mode 100644 index 0000000..670cbf7 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Jeecg/IJeecgBackendGateway.cs @@ -0,0 +1,45 @@ +using System.Net.Http; + +namespace YY.Admin.Services.Service.Jeecg; + +/// +/// Jeecg 后端访问网关: +/// 1. 统一封装 HTTP 调用; +/// 2. 统一封装 WebSocket 双向连接; +/// 3. 作为后续 Jeecg 集成功能的统一入口。 +/// +public interface IJeecgBackendGateway +{ + /// + /// 统一执行 Jeecg GET 请求(自动拼接 BaseUrl)。 + /// + Task ExecuteGetAsync( + string relativeOrAbsoluteUrl, + Dictionary? headers = null, + CancellationToken cancellationToken = default); + + /// + /// 统一执行 Jeecg GET 请求并返回文本。 + /// + Task ExecuteGetStringAsync( + string relativeOrAbsoluteUrl, + Dictionary? headers = null, + CancellationToken cancellationToken = default); + + /// + /// 启动 Jeecg WebSocket 双向连接循环(自动重连)。 + /// + Task RunWebSocketLoopAsync( + Func onMessage, + CancellationToken cancellationToken); + + /// + /// 发送一条 WebSocket 消息(连接可用时)。 + /// + Task SendWebSocketMessageAsync(string message, CancellationToken cancellationToken = default); + + /// + /// 单次 WebSocket 上报(临时连接,适用于登录页等未常驻连接场景)。 + /// + Task SendWebSocketOneShotAsync(string message, CancellationToken cancellationToken = default); +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Jeecg/IJeecgLoginLogReportService.cs b/yy-admin-master/YY.Admin.Services/Service/Jeecg/IJeecgLoginLogReportService.cs new file mode 100644 index 0000000..137e726 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Jeecg/IJeecgLoginLogReportService.cs @@ -0,0 +1,28 @@ +namespace YY.Admin.Services.Service.Jeecg; + +/// +/// Jeecg 登录日志上报服务(WebSocket + HTTP + 本地离线队列)。 +/// +public interface IJeecgLoginLogReportService +{ + /// + /// 启动后台离线日志补传循环(可重复调用,内部幂等)。 + /// + void StartBackgroundSync(); + + /// + /// 上报一次登录日志;网络不可达时自动落本地,联网后自动补传。 + /// + Task ReportLoginAsync(string account, bool success, string message, CancellationToken cancellationToken = default); + + /// + /// 上报通用日志(操作/异常/告警等),自动走 WS/HTTP/本地队列兜底。 + /// + Task ReportLogAsync( + string category, + string message, + string? account = null, + bool? success = null, + string? exception = null, + CancellationToken cancellationToken = default); +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Jeecg/IJeecgUserSyncCoordinator.cs b/yy-admin-master/YY.Admin.Services/Service/Jeecg/IJeecgUserSyncCoordinator.cs new file mode 100644 index 0000000..6fd9026 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Jeecg/IJeecgUserSyncCoordinator.cs @@ -0,0 +1,18 @@ +namespace YY.Admin.Services.Service.Jeecg +{ + /// + /// Jeecg 用户镜像后台同步协调器:统一设备通道(STOMP)信号 + Outbox 拉取,不再使用独立 Jeecg WebSocket 收包循环。 + /// + public interface IJeecgUserSyncCoordinator + { + /// + /// 启动后台同步(主窗口登录成功后调用) + /// + void Start(); + + /// + /// 停止后台同步与 STOMP 信号订阅(登出或关闭主窗口时调用) + /// + void Stop(); + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgBackendGateway.cs b/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgBackendGateway.cs new file mode 100644 index 0000000..c3f514e --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgBackendGateway.cs @@ -0,0 +1,352 @@ +using Microsoft.Extensions.Configuration; +using System.Net.Http; +using System.Net.WebSockets; +using System.Text; +using System.IO; + +namespace YY.Admin.Services.Service.Jeecg; + +/// +/// Jeecg 后端网关实现。 +/// +public class JeecgBackendGateway : IJeecgBackendGateway, ISingletonDependency +{ + private readonly IConfiguration _configuration; + private readonly HttpClient _httpClient; + private readonly ILoggerService _logger; + private readonly SemaphoreSlim _wsSendLock = new(1, 1); + private readonly object _wsStateLock = new(); + private ClientWebSocket? _activeWebSocket; + + public JeecgBackendGateway( + IConfiguration configuration, + HttpClient httpClient, + ILoggerService logger) + { + _configuration = configuration; + _httpClient = httpClient; + _logger = logger; + } + + public async Task ExecuteGetAsync( + string relativeOrAbsoluteUrl, + Dictionary? headers = null, + CancellationToken cancellationToken = default) + { + var requestUrl = BuildUrl(relativeOrAbsoluteUrl); + using var req = new HttpRequestMessage(HttpMethod.Get, requestUrl); + if (headers != null) + { + foreach (var kv in headers) + { + req.Headers.TryAddWithoutValidation(kv.Key, kv.Value); + } + } + + return await _httpClient.SendAsync(req, cancellationToken); + } + + public async Task ExecuteGetStringAsync( + string relativeOrAbsoluteUrl, + Dictionary? headers = null, + CancellationToken cancellationToken = default) + { + using var resp = await ExecuteGetAsync(relativeOrAbsoluteUrl, headers, cancellationToken); + if (!resp.IsSuccessStatusCode) + { + return null; + } + return await resp.Content.ReadAsStringAsync(cancellationToken); + } + + public async Task RunWebSocketLoopAsync( + Func onMessage, + CancellationToken cancellationToken) + { + var wsUrl = ResolveWebSocketUrl(); + _logger.Information($"Jeecg WebSocket 解析地址: {wsUrl}"); + if (string.IsNullOrWhiteSpace(wsUrl)) + { + _logger.Warning("Jeecg WebSocket 未启动:解析地址为空"); + return; + } + + var backoffSeconds = 5; + var buffer = new byte[8192]; + + while (!cancellationToken.IsCancellationRequested) + { + using var ws = new ClientWebSocket(); + try + { + ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(10); + await ws.ConnectAsync(new Uri(wsUrl), cancellationToken); + lock (_wsStateLock) + { + _activeWebSocket = ws; + } + backoffSeconds = 5; + _logger.Information($"Jeecg WebSocket 已连接: {wsUrl}"); + using var heartbeatCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var lastReceiveTicks = DateTime.UtcNow.Ticks; + _ = Task.Run(() => HeartbeatLoopAsync(ws, heartbeatCts.Token), heartbeatCts.Token); + var inactivitySeconds = _configuration.GetValue("JeecgIntegration:WebSocketInactivityReconnectSeconds", 0); + CancellationTokenSource? inactivityCts = null; + if (inactivitySeconds > 0) + { + inactivityCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + _ = Task.Run(() => InactivityReconnectLoopAsync(ws, () => Interlocked.Read(ref lastReceiveTicks), inactivityCts.Token), inactivityCts.Token); + } + + while (ws.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested) + { + using var ms = new MemoryStream(); + WebSocketReceiveResult result; + do + { + var seg = new ArraySegment(buffer); + result = await ws.ReceiveAsync(seg, cancellationToken); + _logger.Information($"Jeecg WebSocket 收帧: type={result.MessageType}, count={result.Count}, end={result.EndOfMessage}, state={ws.State}"); + if (result.MessageType == WebSocketMessageType.Close) + { + _logger.Warning($"Jeecg WebSocket 收到关闭帧: closeStatus={result.CloseStatus}, desc={result.CloseStatusDescription}"); + break; + } + ms.Write(buffer, 0, result.Count); + } while (!result.EndOfMessage); + + if (result.MessageType == WebSocketMessageType.Close) + { + _logger.Warning("Jeecg WebSocket 接收循环检测到关闭帧,准备重连"); + break; + } + + var payload = Encoding.UTF8.GetString(ms.ToArray()); + Interlocked.Exchange(ref lastReceiveTicks, DateTime.UtcNow.Ticks); + _logger.Information($"Jeecg WebSocket 收到原始消息: {payload}"); + await onMessage(payload); + } + heartbeatCts.Cancel(); + inactivityCts?.Cancel(); + _logger.Warning($"Jeecg WebSocket 接收循环退出,当前状态={ws.State}"); + } + catch (OperationCanceledException) + { + _logger.Warning("Jeecg WebSocket 接收循环取消"); + break; + } + catch (Exception ex) + { + _logger.Warning($"Jeecg WebSocket 断开,{backoffSeconds} 秒后重连。地址: {wsUrl},异常: {ex.Message}"); + } + finally + { + lock (_wsStateLock) + { + if (ReferenceEquals(_activeWebSocket, ws)) + { + _activeWebSocket = null; + } + } + } + + try + { + await Task.Delay(TimeSpan.FromSeconds(Math.Min(backoffSeconds, 120)), cancellationToken); + } + catch (OperationCanceledException) + { + break; + } + + backoffSeconds = Math.Min(backoffSeconds * 2, 120); + } + } + + private async Task HeartbeatLoopAsync(ClientWebSocket ws, CancellationToken cancellationToken) + { + var payload = Encoding.UTF8.GetBytes("ping"); + while (!cancellationToken.IsCancellationRequested && ws.State == WebSocketState.Open) + { + try + { + await Task.Delay(TimeSpan.FromSeconds(12), cancellationToken); + if (ws.State != WebSocketState.Open) + { + break; + } + await ws.SendAsync(new ArraySegment(payload), WebSocketMessageType.Text, true, cancellationToken); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + _logger.Warning($"Jeecg WebSocket 心跳发送失败: {ex.Message}"); + break; + } + } + } + + private async Task InactivityReconnectLoopAsync(ClientWebSocket ws, Func getLastReceiveTicks, CancellationToken cancellationToken) + { + var inactivitySeconds = _configuration.GetValue("JeecgIntegration:WebSocketInactivityReconnectSeconds", 0); + if (inactivitySeconds <= 0) + { + return; + } + while (!cancellationToken.IsCancellationRequested && ws.State == WebSocketState.Open) + { + try + { + await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken); + if (ws.State != WebSocketState.Open) + { + break; + } + + var lastReceiveUtc = new DateTime(getLastReceiveTicks(), DateTimeKind.Utc); + var idleSeconds = (DateTime.UtcNow - lastReceiveUtc).TotalSeconds; + if (idleSeconds < inactivitySeconds) + { + continue; + } + + _logger.Warning($"Jeecg WebSocket 超过 {Math.Round(idleSeconds)} 秒未收到任何消息,主动重连"); + ws.Abort(); + break; + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + _logger.Warning($"Jeecg WebSocket 空闲检测异常: {ex.Message}"); + break; + } + } + } + + public async Task SendWebSocketMessageAsync(string message, CancellationToken cancellationToken = default) + { + ClientWebSocket? socket; + lock (_wsStateLock) + { + socket = _activeWebSocket; + } + if (socket == null || socket.State != WebSocketState.Open) + { + return false; + } + + var bytes = Encoding.UTF8.GetBytes(message); + var seg = new ArraySegment(bytes); + await _wsSendLock.WaitAsync(cancellationToken); + try + { + await socket.SendAsync(seg, WebSocketMessageType.Text, true, cancellationToken); + return true; + } + catch + { + return false; + } + finally + { + _wsSendLock.Release(); + } + } + + public async Task SendWebSocketOneShotAsync(string message, CancellationToken cancellationToken = default) + { + var wsUrl = ResolveWebSocketUrl(); + if (string.IsNullOrWhiteSpace(wsUrl)) + { + return false; + } + + using var ws = new ClientWebSocket(); + try + { + await ws.ConnectAsync(new Uri(wsUrl), cancellationToken); + var bytes = Encoding.UTF8.GetBytes(message); + var seg = new ArraySegment(bytes); + await ws.SendAsync(seg, WebSocketMessageType.Text, true, cancellationToken); + await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "done", cancellationToken); + return true; + } + catch (Exception ex) + { + _logger.Warning($"Jeecg WebSocket 单次上报失败: {ex.Message}"); + return false; + } + } + + private string BuildUrl(string relativeOrAbsoluteUrl) + { + if (Uri.TryCreate(relativeOrAbsoluteUrl, UriKind.Absolute, out _)) + { + return relativeOrAbsoluteUrl; + } + + var baseUrl = _configuration.GetValue("JeecgIntegration:BaseUrl")?.TrimEnd('/'); + if (string.IsNullOrWhiteSpace(baseUrl)) + { + return relativeOrAbsoluteUrl; + } + var path = relativeOrAbsoluteUrl.StartsWith("/") ? relativeOrAbsoluteUrl : "/" + relativeOrAbsoluteUrl; + return $"{baseUrl}{path}"; + } + + private string ResolveWebSocketUrl() + { + var anonymousMode = _configuration.GetValue("JeecgIntegration:AnonymousMode", true); + var configUrl = _configuration.GetValue("JeecgIntegration:WebSocketUrl"); + if (!string.IsNullOrWhiteSpace(configUrl)) + { + return NormalizeWebSocketUrl(configUrl.Trim(), anonymousMode); + } + + var baseUrl = _configuration.GetValue("JeecgIntegration:BaseUrl")?.TrimEnd('/'); + if (string.IsNullOrWhiteSpace(baseUrl)) + { + return string.Empty; + } + + var wsPath = _configuration.GetValue("JeecgIntegration:WebSocketPath"); + if (string.IsNullOrWhiteSpace(wsPath)) + { + wsPath = "/websocket/scada-sync"; + } + else if (!wsPath.StartsWith("/")) + { + wsPath = "/" + wsPath; + } + + // 默认从 BaseUrl + WebSocketPath 推导地址,避免只连到根路径导致握手失败 + if (baseUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + return NormalizeWebSocketUrl("wss://" + baseUrl["https://".Length..] + wsPath, anonymousMode); + } + if (baseUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase)) + { + return NormalizeWebSocketUrl("ws://" + baseUrl["http://".Length..] + wsPath, anonymousMode); + } + return string.Empty; + } + + private static string NormalizeWebSocketUrl(string wsUrl, bool anonymousMode) + { + if (!anonymousMode) + { + return wsUrl; + } + if (wsUrl.Contains("/ws/device/websocket", StringComparison.OrdinalIgnoreCase)) + { + return wsUrl.Replace("/ws/device/websocket", "/websocket/scada-sync", StringComparison.OrdinalIgnoreCase); + } + return wsUrl; + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgLoginLogReportService.cs b/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgLoginLogReportService.cs new file mode 100644 index 0000000..b9b94f3 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgLoginLogReportService.cs @@ -0,0 +1,287 @@ +using Microsoft.Extensions.Configuration; +using System.Net.Http; +using System.IO; +using System.Text; +using System.Text.Json; + +namespace YY.Admin.Services.Service.Jeecg; + +/// +/// 登录日志上报: +/// 1) WebSocket 优先; +/// 2) 失败自动 HTTP 兜底; +/// 3) 仍失败则写本地队列,后台自动补传。 +/// +public class JeecgLoginLogReportService : IJeecgLoginLogReportService, IClientLogReportSink, ISingletonDependency +{ + private readonly IConfiguration _configuration; + private readonly IJeecgBackendGateway _jeecgBackendGateway; + private readonly HttpClient _httpClient; + private readonly SemaphoreSlim _queueLock = new(1, 1); + private readonly CancellationTokenSource _syncCts = new(); + private int _started; + + private sealed class LoginLogQueueItem + { + public string Id { get; set; } = Guid.NewGuid().ToString("N"); + public string Category { get; set; } = "LOGIN"; + public string Account { get; set; } = string.Empty; + public bool? Success { get; set; } + public string Message { get; set; } = string.Empty; + public string? Exception { get; set; } + public int LogType { get; set; } = 1; + public int OperateType { get; set; } = 5; + public string Method { get; set; } = "LOGIN"; + public string RequestUrl { get; set; } = "/desktop/log"; + public long Timestamp { get; set; } = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + public string ClientType { get; set; } = "gkj"; + } + + public JeecgLoginLogReportService( + IConfiguration configuration, + IJeecgBackendGateway jeecgBackendGateway, + HttpClient httpClient) + { + _configuration = configuration; + _jeecgBackendGateway = jeecgBackendGateway; + _httpClient = httpClient; + } + + public void StartBackgroundSync() + { + if (Interlocked.Exchange(ref _started, 1) == 1) + { + return; + } + + _ = Task.Run(() => BackgroundSyncLoopAsync(_syncCts.Token), _syncCts.Token); + } + + public async Task ReportLoginAsync(string account, bool success, string message, CancellationToken cancellationToken = default) + { + await ReportLogAsync("LOGIN", message, account, success, null, cancellationToken); + } + + public async Task ReportLogAsync( + string category, + string message, + string? account = null, + bool? success = null, + string? exception = null, + CancellationToken cancellationToken = default) + { + var normalizedCategory = string.IsNullOrWhiteSpace(category) ? "OPERATION" : category.Trim().ToUpperInvariant(); + var item = BuildQueueItem(normalizedCategory, account, success, message, exception); + if (await TrySendToBackendAsync(item, cancellationToken)) + { + return; + } + + await EnqueueAsync(item, cancellationToken); + } + + private async Task BackgroundSyncLoopAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + await FlushQueueAsync(cancellationToken); + } + catch (Exception ex) + { + Console.WriteLine($"日志离线补传失败: {ex.Message}"); + } + + try + { + await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); + } + catch (OperationCanceledException) + { + break; + } + } + } + + private async Task TrySendToBackendAsync(LoginLogQueueItem item, CancellationToken cancellationToken) + { + var payload = new + { + cmd = "SCADA_LOG", + category = item.Category, + account = item.Account, + success = item.Success, + message = item.Message, + exception = item.Exception, + logType = item.LogType, + operateType = item.OperateType, + method = item.Method, + requestUrl = item.RequestUrl, + clientType = item.ClientType, + timestamp = item.Timestamp + }; + var json = JsonSerializer.Serialize(payload); + + if (await _jeecgBackendGateway.SendWebSocketOneShotAsync(json, cancellationToken)) + { + return true; + } + + var baseUrl = _configuration.GetValue("JeecgIntegration:BaseUrl")?.TrimEnd('/'); + if (string.IsNullOrWhiteSpace(baseUrl)) + { + return false; + } + + var url = $"{baseUrl}/sys/log/scada/addLoginLog"; + using var req = new HttpRequestMessage(HttpMethod.Post, url) + { + Content = new StringContent(json, Encoding.UTF8, "application/json") + }; + + try + { + using var resp = await _httpClient.SendAsync(req, cancellationToken); + return resp.IsSuccessStatusCode; + } + catch + { + return false; + } + } + + private async Task EnqueueAsync(LoginLogQueueItem item, CancellationToken cancellationToken) + { + await _queueLock.WaitAsync(cancellationToken); + try + { + var lines = new List(); + var path = GetQueuePath(); + if (File.Exists(path)) + { + lines = File.ReadAllLines(path).Where(l => !string.IsNullOrWhiteSpace(l)).ToList(); + } + lines.Add(JsonSerializer.Serialize(item)); + EnsureQueueDir(path); + File.WriteAllLines(path, lines); + } + finally + { + _queueLock.Release(); + } + } + + private async Task FlushQueueAsync(CancellationToken cancellationToken) + { + await _queueLock.WaitAsync(cancellationToken); + try + { + var path = GetQueuePath(); + if (!File.Exists(path)) + { + return; + } + + var lines = File.ReadAllLines(path).Where(l => !string.IsNullOrWhiteSpace(l)).ToList(); + if (lines.Count == 0) + { + return; + } + + var remain = new List(); + foreach (var line in lines) + { + LoginLogQueueItem? item = null; + try + { + item = JsonSerializer.Deserialize(line); + } + catch + { + // 解析失败的数据直接丢弃,避免阻塞后续 + continue; + } + + if (item == null || !await TrySendToBackendAsync(item, cancellationToken)) + { + remain.Add(line); + } + } + + if (remain.Count == 0) + { + File.Delete(path); + } + else + { + File.WriteAllLines(path, remain); + } + } + finally + { + _queueLock.Release(); + } + } + + private static string GetQueuePath() + { + return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "AppSettings", "offline-scada-log-queue.jsonl"); + } + + private static void EnsureQueueDir(string path) + { + var dir = Path.GetDirectoryName(path); + if (!string.IsNullOrWhiteSpace(dir) && !Directory.Exists(dir)) + { + Directory.CreateDirectory(dir); + } + } + + private static LoginLogQueueItem BuildQueueItem( + string category, + string? account, + bool? success, + string? message, + string? exception) + { + var item = new LoginLogQueueItem + { + Category = category, + Account = account ?? string.Empty, + Success = success, + Message = message ?? string.Empty, + Exception = exception + }; + + switch (category) + { + case "LOGIN": + item.LogType = 1; + item.OperateType = 1; + item.Method = "LOGIN"; + item.RequestUrl = "/desktop/login"; + break; + case "EXCEPTION": + item.LogType = 2; + item.OperateType = 5; + item.Method = "ERROR"; + item.RequestUrl = "/desktop/exception"; + break; + case "WARNING": + item.LogType = 2; + item.OperateType = 4; + item.Method = "WARNING"; + item.RequestUrl = "/desktop/warning"; + break; + default: + item.LogType = 1; + item.OperateType = 5; + item.Method = "OPERATION"; + item.RequestUrl = "/desktop/operation"; + break; + } + + return item; + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgSyncState.cs b/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgSyncState.cs new file mode 100644 index 0000000..06f467f --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgSyncState.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace YY.Admin.Services.Service.Jeecg +{ + /// + /// Jeecg 同步本地状态(工控机断网续传水位) + /// + public class JeecgSyncState + { + /// + /// 同步水位(UTC):标准列表作 updateTime_begin;SCADA 作 updatedAfter(与接口文档游标一致) + /// + [JsonPropertyName("lastUserListSyncUtc")] + public DateTime? LastUserListSyncUtc { get; set; } + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgSyncStateStore.cs b/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgSyncStateStore.cs new file mode 100644 index 0000000..abedaf7 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgSyncStateStore.cs @@ -0,0 +1,49 @@ +using System.IO; +using System.Text.Json; + +namespace YY.Admin.Services.Service.Jeecg +{ + /// + /// 读写本地 Jeecg 同步状态文件(与 appsettings 同目录下的 Configuration) + /// + public class JeecgSyncStateStore + { + private readonly string _filePath; + + public JeecgSyncStateStore() + { + var dir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Configuration"); + _filePath = Path.Combine(dir, "jeecg-sync-state.json"); + } + + public JeecgSyncState Load() + { + try + { + if (!File.Exists(_filePath)) + { + return new JeecgSyncState(); + } + + var json = File.ReadAllText(_filePath); + return JsonSerializer.Deserialize(json) ?? new JeecgSyncState(); + } + catch + { + return new JeecgSyncState(); + } + } + + public void Save(JeecgSyncState state) + { + var dir = Path.GetDirectoryName(_filePath); + if (!string.IsNullOrEmpty(dir)) + { + Directory.CreateDirectory(dir); + } + + var json = JsonSerializer.Serialize(state, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(_filePath, json); + } + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgUserMirrorPullHandler.cs b/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgUserMirrorPullHandler.cs new file mode 100644 index 0000000..52af5b8 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgUserMirrorPullHandler.cs @@ -0,0 +1,24 @@ +using YY.Admin.Core; +using YY.Admin.Core.Services; +using YY.Admin.Services.Service.Auth; + +namespace YY.Admin.Services.Service.Jeecg; + +/// +/// Outbox 消费端:执行 SCADA 全量拉取并写入 jeecg_sys_user(与设备模块 HTTP 幂等上报并列的第二条 REST 能力)。 +/// +public class JeecgUserMirrorPullHandler : IJeecgUserMirrorPullHandler, ISingletonDependency +{ + private readonly ISysAuthService _authService; + + public JeecgUserMirrorPullHandler(ISysAuthService authService) + { + _authService = authService; + } + + /// + public Task ExecutePullAsync(CancellationToken cancellationToken = default) + { + return _authService.TryBackgroundSyncJeecgUsersAsync(cancellationToken); + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgUserSyncCoordinator.cs b/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgUserSyncCoordinator.cs new file mode 100644 index 0000000..9ba286e --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgUserSyncCoordinator.cs @@ -0,0 +1,191 @@ +using Microsoft.Extensions.Configuration; +using Prism.Events; +using System.Text.Json; +using YY.Admin.Core; +using YY.Admin.Core.Events; +using YY.Admin.Core.Services; +using YY.Admin.Core.Sync; + +namespace YY.Admin.Services.Service.Jeecg; + +/// +/// 用户镜像同步:统一走设备同步规范线路(STOMP 收信号 → Outbox → REST 拉取 SCADA),不再使用独立 Jeecg 原生 WebSocket 收包循环。 +/// +public class JeecgUserSyncCoordinator : IJeecgUserSyncCoordinator, ISingletonDependency +{ + private readonly IConfiguration _configuration; + private readonly IEventAggregator _eventAggregator; + private readonly IJeecgUserMirrorPullOutbox _mirrorOutbox; + private readonly ILoggerService _logger; + private CancellationTokenSource? _cts; + private readonly object _lifecycleLock = new(); + private SubscriptionToken? _remoteCommandSubscription; + + public JeecgUserSyncCoordinator( + IConfiguration configuration, + IEventAggregator eventAggregator, + IJeecgUserMirrorPullOutbox mirrorOutbox, + ILoggerService logger) + { + _configuration = configuration; + _eventAggregator = eventAggregator; + _mirrorOutbox = mirrorOutbox; + _logger = logger; + } + + /// + public void Start() + { + var enabled = _configuration.GetValue("JeecgIntegration:Enabled", false); + var baseUrl = _configuration.GetValue("JeecgIntegration:BaseUrl") ?? string.Empty; + var stompPath = "/ws/device/websocket"; + _logger.Information($"Jeecg用户同步协调器启动(统一设备通道),Enabled={enabled}, BaseUrl={baseUrl}, Stomp={stompPath}"); + + if (!enabled) + { + _logger.Warning("Jeecg用户同步协调器未启动:JeecgIntegration:Enabled=false"); + return; + } + + CancellationToken token; + lock (_lifecycleLock) + { + CancelAndDisposeCts(); + UnsubscribeRemoteCommand(); + _remoteCommandSubscription = _eventAggregator.GetEvent().Subscribe(OnRemoteCommand, ThreadOption.BackgroundThread); + + _cts = new CancellationTokenSource(); + token = _cts.Token; + } + + // 进入主窗口后稍延迟再入队一次全量拉取,避免与登录同步抢带宽 + _ = Task.Run(async () => + { + try + { + await Task.Delay(3000, token).ConfigureAwait(false); + await _mirrorOutbox.EnqueuePullAsync(JeecgUserMirrorOutbox.EventBoot, null, token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // 忽略 + } + catch (Exception ex) + { + _logger.Warning($"Jeecg 启动后入队同步失败: {ex.Message}"); + } + }, token); + } + + /// + public void Stop() + { + lock (_lifecycleLock) + { + UnsubscribeRemoteCommand(); + CancelAndDisposeCts(); + } + } + + private void OnRemoteCommand(RemoteCommandPayload payload) + { + try + { + var json = payload.CommandJson ?? string.Empty; + if (!ShouldTriggerUserSync(json)) + { + return; + } + + _logger.Information($"收到设备统一通道(STOMP)用户变更信号,入队 Outbox: {json}"); + _ = _mirrorOutbox.EnqueuePullAsync(JeecgUserMirrorOutbox.EventSignal, json, CancellationToken.None); + } + catch (Exception ex) + { + _logger.Warning($"处理 STOMP 用户变更信号失败: {ex.Message}"); + } + } + + private void UnsubscribeRemoteCommand() + { + if (_remoteCommandSubscription != null) + { + _eventAggregator.GetEvent().Unsubscribe(_remoteCommandSubscription); + _remoteCommandSubscription = null; + } + } + + private void CancelAndDisposeCts() + { + try + { + _cts?.Cancel(); + } + catch + { + // 忽略 + } + finally + { + _cts?.Dispose(); + _cts = null; + } + } + + private static bool ShouldTriggerUserSync(string message) + { + if (string.IsNullOrWhiteSpace(message)) + { + return false; + } + try + { + using var doc = JsonDocument.Parse(message); + var root = doc.RootElement; + if (TryMatchCmd(root)) + { + return true; + } + // 设备模块 REST 下发的 commandJson 包裹 + if (root.TryGetProperty("commandJson", out var innerEl) && innerEl.ValueKind == JsonValueKind.String) + { + var rawInner = innerEl.GetString(); + if (!string.IsNullOrWhiteSpace(rawInner)) + { + using var innerDoc = JsonDocument.Parse(rawInner); + return TryMatchCmd(innerDoc.RootElement); + } + } + if (root.TryGetProperty("message", out var innerMessage) + && innerMessage.ValueKind == JsonValueKind.String) + { + var rawInner = innerMessage.GetString(); + if (!string.IsNullOrWhiteSpace(rawInner)) + { + using var innerDoc = JsonDocument.Parse(rawInner); + return TryMatchCmd(innerDoc.RootElement); + } + } + return false; + } + catch + { + return false; + } + } + + private static bool TryMatchCmd(JsonElement element) + { + if (element.ValueKind != JsonValueKind.Object) + { + return false; + } + if (!element.TryGetProperty("cmd", out var cmd)) + { + return false; + } + var cmdValue = cmd.GetString(); + return string.Equals(cmdValue, "SCADA_USER_CHANGED", StringComparison.OrdinalIgnoreCase) + || string.Equals(cmdValue, "SCADA_USERS_CHANGED", StringComparison.OrdinalIgnoreCase); + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Menu/Dto/MenuOutput.cs b/yy-admin-master/YY.Admin.Services/Service/Menu/Dto/MenuOutput.cs new file mode 100644 index 0000000..5ea53f0 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Menu/Dto/MenuOutput.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Services +{ + /// + /// 系统菜单返回结果 + /// + public class MenuOutput + { + /// + /// Id + /// + public long Id { get; set; } + + /// + /// 父Id + /// + public long Pid { get; set; } + + /// + /// 菜单类型(0目录 1菜单 2按钮) + /// + public MenuTypeEnum Type { get; set; } + + /// + /// 名称 + /// + public string Name { get; set; } + + /// + /// 路由地址 + /// + public string Path { get; set; } + + /// + /// 组件路径 + /// + public string Component { get; set; } + + /// + /// 权限标识 + /// + public string Permission { get; set; } + + /// + /// 重定向 + /// + public string Redirect { get; set; } + + /// + /// 排序 + /// + public int OrderNo { get; set; } + + /// + /// 状态 + /// + public StatusEnum Status { get; set; } + + /// + /// 备注 + /// + public string Remark { get; set; } + + /// + /// 创建时间 + /// + public virtual DateTime CreateTime { get; set; } + + /// + /// 更新时间 + /// + public virtual DateTime UpdateTime { get; set; } + + /// + /// 创建者姓名 + /// + public virtual string CreateUserName { get; set; } + + /// + /// 修改者姓名 + /// + public virtual string UpdateUserName { get; set; } + + #region 菜单Meta + /// + /// 标题 + /// + public string Title { get; set; } + + /// + /// 图标 + /// + public string Icon { get; set; } + + /// + /// 是否内嵌 + /// + public bool IsIframe { get; set; } + + /// + /// 外链链接 + /// + public string IsLink { get; set; } + + /// + /// 是否隐藏 + /// + public bool IsHide { get; set; } + + /// + /// 是否缓存 + /// + public bool IsKeepAlive { get; set; } + + /// + /// 是否固定 + /// + public bool IsAffix { get; set; } + #endregion + /// + /// 菜单子项 + /// + public List Children { get; set; } + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Menu/ISysMenuService.cs b/yy-admin-master/YY.Admin.Services/Service/Menu/ISysMenuService.cs new file mode 100644 index 0000000..adfac08 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Menu/ISysMenuService.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Services.Service.Menu +{ + public interface ISysMenuService + { + Task> GetLoginMenuTree(); + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Menu/SysMenuService.cs b/yy-admin-master/YY.Admin.Services/Service/Menu/SysMenuService.cs new file mode 100644 index 0000000..aa20849 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Menu/SysMenuService.cs @@ -0,0 +1,136 @@ +using Mapster; +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using YY.Admin.Core.Session; +using YY.Admin.Services.Service.Role; +using YY.Admin.Services.Service.User; + +namespace YY.Admin.Services.Service.Menu +{ + public class SysMenuService : ISysMenuService, ISingletonDependency + { + private readonly ISqlSugarClient _dbContext; + private readonly SysUserRoleService _sysUserRoleService; + private readonly SysRoleMenuService _sysRoleMenuService; + public SysMenuService( + ISqlSugarClient context, + SysRoleMenuService sysRoleMenuService, + SysUserRoleService sysUserRoleService) { + _dbContext=context; + _sysUserRoleService=sysUserRoleService; + _sysRoleMenuService=sysRoleMenuService; + } + /// + /// 获取登录菜单树 + /// + /// + public async Task> GetLoginMenuTree() + { + var currentUser = AppSession.CurrentUser; + if (currentUser == null) + { + return new List(); + } + + var tenantId = currentUser.TenantId ?? 0; + var (query, _) = GetSugarQueryableAndTenantId(tenantId); + if (currentUser.IsSuperAdmin || currentUser.IsSysAdmin) + { + var menuList = await query.Where(u => u.Type != MenuTypeEnum.Btn && u.Status == StatusEnum.Enable) + .OrderBy(u => new { u.OrderNo, u.Id }) + .ToTreeAsync(u => u.Children, u => u.Pid, 0); + return menuList.Adapt>(); + } + var menuIdList = await GetMenuIdList(); + if (menuIdList == null || menuIdList.Count == 0) + { + // Jeecg自动建档用户可能暂未分配本地角色,这里回退显示基础菜单 + var fallbackMenus = await GetFallbackMenuTreeAsync(); + return fallbackMenus.Adapt>(); + } + + var menuTree = await query.Where(u => u.Type != MenuTypeEnum.Btn && u.Status == StatusEnum.Enable) + .OrderBy(u => new { u.OrderNo, u.Id }).ToTreeAsync(u => u.Children, u => u.Pid, 0, menuIdList.Select(d => (object)d).ToArray()); + + // 角色或租户菜单未配置时,避免左侧功能列表全空白 + if (menuTree == null || menuTree.Count == 0) + { + menuTree = await GetFallbackMenuTreeAsync(); + } + + return menuTree.Adapt>(); + } + /// + /// 获取当前用户菜单Id集合 + /// + /// + public async Task> GetMenuIdList() + { + var currentUser = AppSession.CurrentUser; + if (currentUser == null) + { + return new List(); + } + + var roleIdList = await _sysUserRoleService.GetUserRoleIdList(currentUser.Id); + return await _sysRoleMenuService.GetRoleMenuIdList(roleIdList); + } + /// + /// 根据租户id获取构建菜单联表查询实例 + /// + /// + /// + public (ISugarQueryable query, long tenantId) GetSugarQueryableAndTenantId(long tenantId) + { + if (!AppSession.CurrentUser!.IsSuperAdmin) tenantId = AppSession.CurrentUser.TenantId!.Value; + + // 超管用户菜单范围:种子菜单 + 租户id菜单 + ISugarQueryable query; + if (AppSession.CurrentUser.IsSuperAdmin) + { + if (tenantId <= 0) + { + query = _dbContext.Queryable().InnerJoinIF(false, (u, t) => true); + } + else + { + // 指定租户的菜单 + var menuIds = _dbContext.Queryable().Where(u => u.TenantId == tenantId).ToList(u => u.MenuId) ?? new(); + + // 种子菜单 + //menuIds.AddRange(new SysMenuSeedData().HasData().Select(u => u.Id).ToList()); + + menuIds = menuIds.Distinct().ToList(); + query = _dbContext.Queryable().InnerJoinIF(false, (u, t) => true).Where(u => menuIds.Contains(u.Id)); + } + } + else if (AppSession.CurrentUser.IsSysAdmin) + { + // 系统管理员直接读取全量启用菜单,不依赖租户菜单关联表 + query = _dbContext.Queryable().InnerJoinIF(false, (u, t) => true); + } + else + { + query = _dbContext.Queryable().InnerJoinIF(tenantId > 0, (u, t) => t.TenantId == tenantId && u.Id == t.MenuId); + } + + return (query, tenantId); + } + + /// + /// 菜单兜底:当角色/租户未完成绑定时返回可用基础菜单,避免界面空白 + /// + private async Task> GetFallbackMenuTreeAsync() + { + return await _dbContext.Queryable() + .Where(u => u.Type != MenuTypeEnum.Btn && u.Status == StatusEnum.Enable) + .OrderBy(u => new { u.OrderNo, u.Id }) + .ToTreeAsync(u => u.Children, u => u.Pid, 0); + } + + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Org/ISysOrgService.cs b/yy-admin-master/YY.Admin.Services/Service/Org/ISysOrgService.cs new file mode 100644 index 0000000..b3062df --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Org/ISysOrgService.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Services.Service +{ + public interface ISysOrgService + { + /// + /// 根据节点Id获取子节点Id集合(包含自己) + /// + /// + /// + Task> GetChildIdListWithSelfById(long pid); + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Org/SysOrgService.cs b/yy-admin-master/YY.Admin.Services/Service/Org/SysOrgService.cs new file mode 100644 index 0000000..218b372 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Org/SysOrgService.cs @@ -0,0 +1,22 @@ +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Services.Service.Org +{ + public class SysOrgService : ISysOrgService, ISingletonDependency + { + private readonly ISqlSugarClient _dbContext; + public SysOrgService(ISqlSugarClient dbContext) { + _dbContext=dbContext; + } + public async Task> GetChildIdListWithSelfById(long pid) + { + var orgTreeList = await _dbContext.Queryable().ToChildListAsync(u => u.Pid, pid, true); + return orgTreeList.Select(u => u.Id).ToList(); + } + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Role/ISysRoleMenuService.cs b/yy-admin-master/YY.Admin.Services/Service/Role/ISysRoleMenuService.cs new file mode 100644 index 0000000..18bdb7b --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Role/ISysRoleMenuService.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Services.Service.Role +{ + public interface ISysRoleMenuService + { + /// + /// 根据角色Id集合获取菜单Id集合 + /// + /// + /// + Task> GetRoleMenuIdList(List roleIdList); + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Role/SysRoleMenuService.cs b/yy-admin-master/YY.Admin.Services/Service/Role/SysRoleMenuService.cs new file mode 100644 index 0000000..da7f7e9 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Role/SysRoleMenuService.cs @@ -0,0 +1,24 @@ +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Services.Service.Role +{ + public class SysRoleMenuService : ISysRoleMenuService, ISingletonDependency + { + private readonly ISqlSugarClient _dbContext; + public SysRoleMenuService(ISqlSugarClient context) + { + _dbContext = context; + } + public async Task> GetRoleMenuIdList(List roleIdList) + { + return await _dbContext.Queryable() + .Where(u => roleIdList.Contains(u.RoleId)) + .Select(u => u.MenuId).ToListAsync(); + } + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Tenant/Dto/PageTenantInput.cs b/yy-admin-master/YY.Admin.Services/Service/Tenant/Dto/PageTenantInput.cs new file mode 100644 index 0000000..5416548 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Tenant/Dto/PageTenantInput.cs @@ -0,0 +1,18 @@ +namespace YY.Admin.Services.Service +{ + /// + /// 租户分页查询参数 + /// + public class PageTenantInput : PagedRequestBase + { + /// + /// 租户名称(本地映射为Title) + /// + public string? Name { get; set; } + + /// + /// 状态 + /// + public StatusEnum? Status { get; set; } + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Tenant/Dto/TenantOutput.cs b/yy-admin-master/YY.Admin.Services/Service/Tenant/Dto/TenantOutput.cs new file mode 100644 index 0000000..c9f49aa --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Tenant/Dto/TenantOutput.cs @@ -0,0 +1,20 @@ +namespace YY.Admin.Services.Service +{ + /// + /// 租户列表输出 + /// + public class TenantOutput + { + public long Id { get; set; } + + public string? Title { get; set; } + + public string? Logo { get; set; } + + public StatusEnum Status { get; set; } + + public DateTime CreateTime { get; set; } + + public DateTime? UpdateTime { get; set; } + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Tenant/ISysTenantSyncService.cs b/yy-admin-master/YY.Admin.Services/Service/Tenant/ISysTenantSyncService.cs new file mode 100644 index 0000000..20eed85 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Tenant/ISysTenantSyncService.cs @@ -0,0 +1,12 @@ +namespace YY.Admin.Services.Service.Tenant +{ + public interface ISysTenantSyncService + { + Task> PageAsync(PageTenantInput input); + + /// + /// 从 Jeecg 后台同步租户信息到本地 + /// + Task SyncFromJeecgAsync(); + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/Tenant/SysTenantSyncService.cs b/yy-admin-master/YY.Admin.Services/Service/Tenant/SysTenantSyncService.cs new file mode 100644 index 0000000..96c7a6e --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/Tenant/SysTenantSyncService.cs @@ -0,0 +1,164 @@ +using Microsoft.Extensions.Configuration; +using SqlSugar; +using System.Text.Json; +using YY.Admin.Core.Session; +using YY.Admin.Services.Service.Jeecg; + +namespace YY.Admin.Services.Service.Tenant +{ + public class SysTenantSyncService : ISysTenantSyncService, ISingletonDependency + { + private readonly ISqlSugarClient _dbContext; + private readonly IConfiguration _configuration; + private readonly ISysCacheService _sysCacheService; + private readonly IJeecgBackendGateway _jeecgGateway; + + public SysTenantSyncService( + ISqlSugarClient dbContext, + IConfiguration configuration, + ISysCacheService sysCacheService, + IJeecgBackendGateway jeecgGateway) + { + _dbContext = dbContext; + _configuration = configuration; + _sysCacheService = sysCacheService; + _jeecgGateway = jeecgGateway; + } + + public async Task> PageAsync(PageTenantInput input) + { + var status = input.Status; + var statusValue = status.GetValueOrDefault(); + var query = _dbContext.Queryable().ClearFilter() + .WhereIF(!string.IsNullOrWhiteSpace(input.Name), t => t.Title!.Contains(input.Name!)) + .WhereIF(status.HasValue, t => t.Status == statusValue) + .OrderBy(t => t.Id); + + RefAsync total = 0; + var list = await query.ToPageListAsync(input.Page, input.PageSize, total); + var items = list.Select(t => new TenantOutput + { + Id = t.Id, + Title = t.Title, + Logo = t.Logo, + Status = t.Status, + CreateTime = t.CreateTime, + UpdateTime = t.UpdateTime + }).ToList(); + + return new SqlSugarPagedList + { + Page = input.Page, + PageSize = input.PageSize, + Total = total, + TotalPages = input.PageSize > 0 ? (int)Math.Ceiling(total / (double)input.PageSize) : 0, + HasNextPage = input.PageSize > 0 && input.Page < (int)Math.Ceiling(total / (double)input.PageSize), + HasPrevPage = input.Page > 1, + Items = items + }; + } + + public async Task SyncFromJeecgAsync() + { + var userId = AppSession.CurrentUser?.Id; + if (!userId.HasValue || userId.Value <= 0) return 0; + + var tokenKey = $"jeecg:token:{userId.Value}"; + var token = _sysCacheService.Get(tokenKey); + if (string.IsNullOrWhiteSpace(token)) return 0; + + var baseUrl = _configuration.GetValue("JeecgIntegration:BaseUrl")?.TrimEnd('/'); + if (string.IsNullOrWhiteSpace(baseUrl)) return 0; + + var tenantListPath = _configuration.GetValue("JeecgIntegration:TenantListPath") ?? "/sys/tenant/list"; + var requestUrl = $"{baseUrl}{tenantListPath}?pageNo=1&pageSize=200"; + var headers = new Dictionary { ["X-Access-Token"] = token }; + var json = await _jeecgGateway.ExecuteGetStringAsync(requestUrl, headers); + if (string.IsNullOrWhiteSpace(json)) return 0; + using var doc = JsonDocument.Parse(json); + var root = doc.RootElement; + if (!(root.TryGetProperty("success", out var successEl) && successEl.GetBoolean())) return 0; + if (!root.TryGetProperty("result", out var resultEl) || resultEl.ValueKind != JsonValueKind.Object) return 0; + if (!resultEl.TryGetProperty("records", out var recordsEl) || recordsEl.ValueKind != JsonValueKind.Array) return 0; + + var templateTenant = await _dbContext.Queryable().ClearFilter().OrderBy(t => t.Id).FirstAsync(); + if (templateTenant == null) return 0; + + var synced = 0; + foreach (var tenantEl in recordsEl.EnumerateArray()) + { + if (!tenantEl.TryGetProperty("id", out var idEl) || !idEl.TryGetInt64(out var tenantId) || tenantId <= 0) continue; + var name = tenantEl.TryGetProperty("name", out var nameEl) ? nameEl.GetString() : null; + var logo = tenantEl.TryGetProperty("companyLogo", out var logoEl) ? logoEl.GetString() : null; + var status = ResolveTenantStatus(tenantEl); + + var exists = await _dbContext.Queryable().ClearFilter().Where(t => t.Id == tenantId).AnyAsync(); + if (exists) + { + await _dbContext.Updateable() + .SetColumns(t => t.Title == (string.IsNullOrWhiteSpace(name) ? t.Title : name)) + .SetColumns(t => t.Logo == logo) + .SetColumns(t => t.Status == status) + .SetColumns(t => t.UpdateTime == DateTime.Now) + .Where(t => t.Id == tenantId) + .ExecuteCommandAsync(); + } + else + { + var entity = new SysTenant + { + Id = tenantId, + UserId = templateTenant.UserId, + OrgId = templateTenant.OrgId, + TenantType = templateTenant.TenantType, + DbType = templateTenant.DbType, + Connection = templateTenant.Connection, + ConfigId = templateTenant.ConfigId, + SlaveConnections = templateTenant.SlaveConnections, + EnableReg = templateTenant.EnableReg, + RegWayId = templateTenant.RegWayId, + Logo = logo, + Title = string.IsNullOrWhiteSpace(name) ? $"租户{tenantId}" : name, + ViceTitle = templateTenant.ViceTitle, + ViceDesc = templateTenant.ViceDesc, + Watermark = templateTenant.Watermark, + Copyright = templateTenant.Copyright, + Icp = templateTenant.Icp, + IcpUrl = templateTenant.IcpUrl, + OrderNo = templateTenant.OrderNo, + Remark = templateTenant.Remark, + Status = status, + CreateTime = DateTime.Now, + UpdateTime = DateTime.Now + }; + await _dbContext.Insertable(entity).ExecuteCommandAsync(); + } + synced++; + } + return synced; + } + + /// + /// 安全解析Jeecg租户状态,避免null或字符串导致异常 + /// + private static StatusEnum ResolveTenantStatus(JsonElement tenantEl) + { + if (!tenantEl.TryGetProperty("status", out var statusEl)) + { + return StatusEnum.Disable; + } + + if (statusEl.ValueKind == JsonValueKind.Number && statusEl.TryGetInt32(out var numStatus)) + { + return numStatus == 1 ? StatusEnum.Enable : StatusEnum.Disable; + } + + if (statusEl.ValueKind == JsonValueKind.String && int.TryParse(statusEl.GetString(), out var strStatus)) + { + return strStatus == 1 ? StatusEnum.Enable : StatusEnum.Disable; + } + + return StatusEnum.Disable; + } + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/User/Dto/UserInput.cs b/yy-admin-master/YY.Admin.Services/Service/User/Dto/UserInput.cs new file mode 100644 index 0000000..29a825f --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/User/Dto/UserInput.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Services.Service +{ + /// + /// 获取用户分页列表输入参数 + /// + public class PageUserInput : PagedRequestBase + { + /// + /// 租户Id + /// + public long TenantId { get; set; } + + /// + /// 账号 + /// + public string Account { get; set; } + + /// + /// 姓名 + /// + public string RealName { get; set; } + + /// + /// 昵称 + /// + public string NickName { get; set; } + + /// + /// 性别 + /// + public GenderEnum? Sex { get; set; } + + /// + /// 职位名称 + /// + public string PosName { get; set; } + + /// + /// 手机号 + /// + public string Phone { get; set; } + + + /// + /// 状态 + /// + public StatusEnum? Status { get; set; } + + /// + /// 查询时所选机构Id + /// + public long OrgId { get; set; } + + public DateTime? BeginTime { get; set; } + + public DateTime? EndTime { get; set; } + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/User/Dto/UserOutput.cs b/yy-admin-master/YY.Admin.Services/Service/User/Dto/UserOutput.cs new file mode 100644 index 0000000..15a994b --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/User/Dto/UserOutput.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Services.Service +{ + public class UserOutput : BindableBase + { + /// + /// 账号 + /// + [BindDescription("账号", ShowScheme.普通文本, "Auto", 0 )] + public string Account { get; set; } + /// + /// 姓名 + /// + [BindDescription("姓名", ShowScheme.普通文本, "1", 1)] + public virtual string RealName { get; set; } + + /// + /// 昵称 + /// + [BindDescription("昵称", ShowScheme.普通文本, "Auto", 2)] + public string? NickName { get; set; } + + /// + /// 头像 + /// + public string? Avatar { get; set; } + + /// + /// 性别-男_1、女_2 + /// + public GenderEnum Sex { get; set; } + + + /// + /// 出生日期 + /// + public DateTime? Birthday { get; set; } + + /// + /// 年龄 + /// + public int Age { get; set; } + + + /// + /// 手机号码 + /// + public string? Phone { get; set; } + + /// + /// 证件类型 + /// + public CardTypeEnum CardType { get; set; } + + /// + /// 身份证号 + /// + public string? IdCardNum { get; set; } + + /// + /// 邮箱 + /// + public string? Email { get; set; } + + /// + /// 文化程度 + /// + public CultureLevelEnum CultureLevel { get; set; } + + /// + /// 政治面貌 + /// + public string? PoliticalOutlook { get; set; } + + /// + /// 毕业院校 + /// + public string? College { get; set; } + + /// + /// 办公电话 + /// + public string? OfficePhone { get; set; } + + /// + /// 紧急联系人 + /// + public string? EmergencyContact { get; set; } + + /// + /// 紧急联系人电话 + /// + public string? EmergencyPhone { get; set; } + + /// + /// 状态 + /// + private StatusEnum _status; + public StatusEnum Status { get => _status; set => SetProperty(ref _status, value); } + + /// + /// 账号类型 + /// + public AccountTypeEnum AccountType { get; set; } + + /// + /// 创建时间 + /// + public DateTime? CreateTime { get; set; } + + /// + /// 机构名称 + /// + [BindDescription("机构名称", ShowScheme.普通文本, "Auto", 3)] + public string OrgName { get; set; } + + /// + /// 职位名称 + /// s + [BindDescription("职位名称", ShowScheme.普通文本, "Auto", 4)] + public string PosName { get; set; } + + /// + /// 角色名称 + /// + [BindDescription("角色名称", ShowScheme.普通文本, "Auto", 5)] + public string RoleName { get; set; } + + private bool _isSelected; + public bool IsSelected + { + get => _isSelected; + set + { + if (SetProperty(ref _isSelected, value)) + { + //SelectionChanged?.Invoke(this, EventArgs.Empty); + } + } + } + + // 添加主键ID,用于批量删除 + public long Id { get; set; } + + //public event EventHandler SelectionChanged; + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/User/ISysUserRoleService.cs b/yy-admin-master/YY.Admin.Services/Service/User/ISysUserRoleService.cs new file mode 100644 index 0000000..ff3c0b0 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/User/ISysUserRoleService.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Services.Service.User +{ + public interface ISysUserRoleService + { + /// + /// 根据用户Id获取角色Id集合 + /// + /// + /// + Task> GetUserRoleIdList(long userId); + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/User/ISysUserService.cs b/yy-admin-master/YY.Admin.Services/Service/User/ISysUserService.cs new file mode 100644 index 0000000..5b374a7 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/User/ISysUserService.cs @@ -0,0 +1,25 @@ +using YY.Admin.Services.Service.User; + +namespace YY.Admin.Services.Service.User +{ + public interface ISysUserService + { + Task> GetUsersAsync(); + + Task> PageAsync(PageUserInput input); + + Task BatchDeleteAsync(List ids); + + Task DeleteAsync(long id); + + Task CreateAsync(SysUser sysUser); + + Task UpdateAsync(SysUser sysUser); + + Task ReadMaxIdAsync(); + + Task AccountExistsAsync(string account, long? excludeUserId = null); + + Task ToggleStatus(SysUser sysUser); + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/User/SysUserRoleService.cs b/yy-admin-master/YY.Admin.Services/Service/User/SysUserRoleService.cs new file mode 100644 index 0000000..10babed --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/User/SysUserRoleService.cs @@ -0,0 +1,27 @@ +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.Services.Service.User +{ + public class SysUserRoleService : ISysUserRoleService, ISingletonDependency + { + private readonly ISqlSugarClient _dbContext; + public SysUserRoleService(ISqlSugarClient context) { + _dbContext=context; + } + /// + ///根据用户Id获取角色Id集合 + /// + /// + /// + public async Task> GetUserRoleIdList(long userId) + { + return await _dbContext.Queryable() + .Where(u => u.UserId == userId).Select(u => u.RoleId).ToListAsync(); + } + } +} diff --git a/yy-admin-master/YY.Admin.Services/Service/User/SysUserService.cs b/yy-admin-master/YY.Admin.Services/Service/User/SysUserService.cs new file mode 100644 index 0000000..1fd7111 --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/Service/User/SysUserService.cs @@ -0,0 +1,239 @@ + +using Dm.util; +using SqlSugar; +using System.Globalization; +using YY.Admin.Core; +using YY.Admin.Core.SeedData; +using YY.Admin.Core.Session; +using YY.Admin.Core.Util; + +namespace YY.Admin.Services.Service.User +{ + public class SysUserService : ISysUserService, ISingletonDependency + { + private readonly ISysOrgService _sysOrgService; + private readonly ISqlSugarClient _dbContext; + public SysUserService(ISysOrgService orgService, ISqlSugarClient dbContext) + { + _sysOrgService = orgService; + _dbContext = dbContext; + } + + public async Task> GetUsersAsync() + { + await Task.Delay(200); + return new List(); + } + + public async Task> PageAsync(PageUserInput input) + { + var sexFilter = input.Sex.HasValue ? (int?)input.Sex.Value : null; + var statusFilter = input.Status.HasValue ? (int?)input.Status.Value : null; + + // 账号管理查询改为从 Jeecg 同构账号表读取 + var query = _dbContext.Queryable().ClearFilter() + .WhereIF(input.TenantId > 0, u => u.LoginTenantId == input.TenantId) + .WhereIF(!string.IsNullOrWhiteSpace(input.Account), u => u.Username != null && u.Username.Contains(input.Account)) + .WhereIF(!string.IsNullOrWhiteSpace(input.RealName), u => u.Realname != null && u.Realname.Contains(input.RealName)) + .WhereIF(!string.IsNullOrWhiteSpace(input.Phone), u => u.Phone != null && u.Phone.Contains(input.Phone)) + .WhereIF(input.BeginTime.HasValue, u => u.CreateTime >= input.BeginTime) + .WhereIF(input.EndTime.HasValue, u => u.CreateTime <= input.EndTime) + .OrderBy(u => SqlFunc.Desc(u.CreateTime)); + if (sexFilter.HasValue) + { + var sexValue = sexFilter.Value; + query = query.Where(u => u.Sex == sexValue); + } + if (statusFilter.HasValue) + { + var statusValue = statusFilter.Value; + query = query.Where(u => u.Status == statusValue); + } + + var pageData = await query.ToPagedListAsync(input.Page, input.PageSize); + var mapped = pageData.Items.Select(u => + { + long id = 0; + long.TryParse(u.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out id); + var sex = GenderEnum.Unknown; + if (u.Sex == 1) sex = GenderEnum.Male; + if (u.Sex == 2) sex = GenderEnum.Female; + var status = u.Status == 1 ? StatusEnum.Enable : StatusEnum.Disable; + return new UserOutput + { + Id = id, + Account = u.Username ?? string.Empty, + RealName = u.Realname ?? string.Empty, + // Jeecg 同构表无 nickname 字段,昵称回退为真实姓名,避免页面显示被“清空” + NickName = string.IsNullOrWhiteSpace(u.Realname) ? (u.Username ?? string.Empty) : u.Realname, + Avatar = u.Avatar, + Sex = sex, + Birthday = u.Birthday, + Phone = u.Phone, + Email = u.Email, + OfficePhone = u.Telephone, + Status = status, + CreateTime = u.CreateTime, + OrgName = u.OrgCode ?? string.Empty, + PosName = u.PositionType ?? string.Empty, + RoleName = string.Empty, + AccountType = AccountTypeEnum.NormalUser + }; + }).ToList(); + + return new SqlSugarPagedList + { + Page = pageData.Page, + PageSize = pageData.PageSize, + Items = mapped, + Total = pageData.Total, + TotalPages = pageData.TotalPages, + HasNextPage = pageData.HasNextPage, + HasPrevPage = pageData.HasPrevPage + }; + } + + public async Task BatchDeleteAsync(List ids) + { + int count = 0; + if (ids == null || ids.isEmpty()) + { + return count; + } + + try + { + await _dbContext.AsTenant().BeginTranAsync(); + + count = await _dbContext.Deleteable().In(ids).ExecuteCommandAsync(); + + await _dbContext.AsTenant().CommitTranAsync(); + } + catch (Exception) + { + await _dbContext.AsTenant().RollbackTranAsync(); + } + + return count; + } + + public async Task DeleteAsync(long id) + { + int count = 0; + try + { + await _dbContext.AsTenant().BeginTranAsync(); + + count = await _dbContext.Deleteable().In(id).ExecuteCommandAsync(); + + await _dbContext.AsTenant().CommitTranAsync(); + } + catch (Exception) + { + await _dbContext.AsTenant().RollbackTranAsync(); + } + + return count; + } + + public async Task CreateAsync(SysUser sysUser) + { + long maxId = await ReadMaxIdAsync(); + + sysUser.Id = ++maxId; + sysUser.Password = CryptogramUtil.Encrypt(sysUser.Password); + sysUser.CardType = CardTypeEnum.IdCard; + sysUser.CultureLevel = CultureLevelEnum.Level0; + sysUser.PosId = new SysPosSeedData().HasData().ToList()[0].Id; + sysUser.TenantId = SqlSugarConst.DefaultTenantId; + sysUser.CreateTime = DateTime.Now; + sysUser.CreateUserId = AppSession.UserId; + sysUser.CreateUserName = AppSession.CurrentUser!.Account; + + int count = 0; + try + { + await _dbContext.AsTenant().BeginTranAsync(); + + count = await _dbContext.Insertable(sysUser).ExecuteCommandAsync(); + + await _dbContext.AsTenant().CommitTranAsync(); + } + catch (Exception) + { + await _dbContext.AsTenant().RollbackTranAsync(); + } + + return count; + } + + public async Task UpdateAsync(SysUser sysUser) + { + sysUser.UpdateUserId = AppSession.UserId; ; + sysUser.UpdateUserName = AppSession.CurrentUser!.Account; + sysUser.UpdateTime = DateTime.Now; + + int count = 0; + try + { + await _dbContext.AsTenant().BeginTranAsync(); + + count = await _dbContext.Updateable(sysUser) + .UpdateColumns(it => new { it.RealName, it.NickName, it.Sex, it.Birthday, it.Age, it.Status, it.UpdateUserId, it.UpdateUserName, it.UpdateTime }) + .ExecuteCommandAsync(); + + await _dbContext.AsTenant().CommitTranAsync(); + } + catch (Exception) + { + await _dbContext.AsTenant().RollbackTranAsync(); + } + + return count; + } + + public async Task ReadMaxIdAsync() + { + return await _dbContext.Queryable().MaxAsync("Id"); + } + + public async Task AccountExistsAsync(string account, long? excludeUserId) + { + var query = _dbContext.Queryable() + . Where(u => u.Account == account); + + // excludeUserId不等于null && 不等于 0 + if (excludeUserId.HasValue && excludeUserId != 0) + { + query = query.Where(u => u.Id != excludeUserId.Value); + } + + return await query.AnyAsync(); + } + + public async Task ToggleStatus(SysUser sysUser) + { + sysUser.UpdateUserId = AppSession.UserId; ; + sysUser.UpdateUserName = AppSession.CurrentUser!.Account; + sysUser.UpdateTime = DateTime.Now; + + int count = 0; + try + { + await _dbContext.AsTenant().BeginTranAsync(); + + count = await _dbContext.Updateable(sysUser) + .UpdateColumns(it => new { it.Status, it.UpdateUserId, it.UpdateUserName, it.UpdateTime }) + .ExecuteCommandAsync(); + + await _dbContext.AsTenant().CommitTranAsync(); + } + catch (Exception) + { + await _dbContext.AsTenant().RollbackTranAsync(); + } + + return count; + } + } +} diff --git a/yy-admin-master/YY.Admin.Services/YY.Admin.Services.csproj b/yy-admin-master/YY.Admin.Services/YY.Admin.Services.csproj new file mode 100644 index 0000000..59cc40a --- /dev/null +++ b/yy-admin-master/YY.Admin.Services/YY.Admin.Services.csproj @@ -0,0 +1,27 @@ + + + + net8.0-windows10.0.19041 + enable + true + enable + preview + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/yy-admin-master/YY.Admin/Admin.NET.db b/yy-admin-master/YY.Admin/Admin.NET.db new file mode 100644 index 0000000..6d909b6 Binary files /dev/null and b/yy-admin-master/YY.Admin/Admin.NET.db differ diff --git a/yy-admin-master/YY.Admin/App.config b/yy-admin-master/YY.Admin/App.config new file mode 100644 index 0000000..bda80c2 --- /dev/null +++ b/yy-admin-master/YY.Admin/App.config @@ -0,0 +1,18 @@ + + + + +
+ + +
+ + + + + + 0 + + + + \ No newline at end of file diff --git a/yy-admin-master/YY.Admin/App.xaml b/yy-admin-master/YY.Admin/App.xaml new file mode 100644 index 0000000..346cafb --- /dev/null +++ b/yy-admin-master/YY.Admin/App.xaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/yy-admin-master/YY.Admin/App.xaml.cs b/yy-admin-master/YY.Admin/App.xaml.cs new file mode 100644 index 0000000..a7a9afc --- /dev/null +++ b/yy-admin-master/YY.Admin/App.xaml.cs @@ -0,0 +1,109 @@ + +using FluentValidation; +using Mapster; +using Microsoft.Extensions.Configuration; +using NewLife; +using System.IO; +using System.Windows; +using YY.Admin.Core; +using YY.Admin.EventBus; +using YY.Admin.Filter; +using YY.Admin.Module; +using YY.Admin.Properties; +using YY.Admin.Setup; +using YY.Admin.ViewModels; +using YY.Admin.Views; +namespace YY.Admin +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : PrismApplication + { + private IConfiguration? _configuration; + private ILoggerService? _logger; + private readonly SyncModule _syncModule = new(); + protected override Window CreateShell() + { + return Container.Resolve(); + } + protected override void OnStartup(StartupEventArgs e) + { + var baseDirectory = AppDomain.CurrentDomain.BaseDirectory; + // 构建配置 + _configuration = new ConfigurationBuilder() + .SetBasePath(baseDirectory) + .AddJsonFile("Configuration/appsettings.json", optional: false, reloadOnChange: true) + .Build(); + // 全局配置 + TypeAdapterConfig.GlobalSettings.Default + .IgnoreNullValues(true) + .NameMatchingStrategy(NameMatchingStrategy.IgnoreCase); + + // FluentValidation 全局规则级别配置 + ValidatorOptions.Global.DefaultRuleLevelCascadeMode = CascadeMode.Stop; + + // Mapster 全局配置 + #if DEBUG + //TypeAdapterConfig.GlobalSettings.RequireExplicitMapping = true; + #endif + + base.OnStartup(e); + } + //注册 + protected override void RegisterTypes(IContainerRegistry containerRegistry) + { + //后续调整为配置依赖注入(Prism Module) + // 注册错误处理 + containerRegistry.RegisterSingleton(); + //全局错误处理 + containerRegistry.RegisterSingleton(); + + // 注册事件聚合器(Prism自带) + containerRegistry.RegisterSingleton(); + + //项目配置选项 + containerRegistry.AddProjectOptions(_configuration!); + // 注册原始配置,供业务服务读取第三方对接参数 + containerRegistry.RegisterInstance(_configuration!); + // 注册缓存服务 + containerRegistry.AddNewLifeCache(_configuration!); + //注册数据库服务 + containerRegistry.AddDbContext(_configuration!); + //注册通知事件服务 + containerRegistry.AddNotificationEventBus(); + // 服务注册 + containerRegistry.AddService(_configuration!); + // 注册HttpClient连接池 + containerRegistry.AddHttpClient(); + + // 注册断联续传同步模块 + _syncModule.RegisterTypes(containerRegistry); + + // 注册所有需要导航的视图 + containerRegistry.AddNavigation(); + } + protected override void OnInitialized() + { + base.OnInitialized(); + // 获取日志服务 + _logger = Container.Resolve(); + // 初始化全局异常处理(通过解析触发构造函数注册) + Container.Resolve(); + // 保存默认主题 + AppSettings.Default.SkinType = AppSettingsViewModel.GetSkinType().ToInt(); + AppSettings.Default.Save(); + + _logger.Information("应用程序已启动"); + + // 启动断联续传同步模块 + _syncModule.OnInitialized(Container); + } + protected override void OnExit(ExitEventArgs e) + { + BaseViewModel.StopTokenCheckTimer(); + base.OnExit(e); + _logger?.Information("应用程序已退出"); + } + } +} diff --git a/yy-admin-master/YY.Admin/AssemblyInfo.cs b/yy-admin-master/YY.Admin/AssemblyInfo.cs new file mode 100644 index 0000000..b0ec827 --- /dev/null +++ b/yy-admin-master/YY.Admin/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/yy-admin-master/YY.Admin/Event/TabClosedEvent.cs b/yy-admin-master/YY.Admin/Event/TabClosedEvent.cs new file mode 100644 index 0000000..2368dd2 --- /dev/null +++ b/yy-admin-master/YY.Admin/Event/TabClosedEvent.cs @@ -0,0 +1,11 @@ +using YY.Admin.Module; + +namespace YY.Admin.Event +{ + /// + /// Tab关闭事件 + /// + public class TabClosedEvent : PubSubEvent + { + } +} diff --git a/yy-admin-master/YY.Admin/Event/TabRefreshEvent.cs b/yy-admin-master/YY.Admin/Event/TabRefreshEvent.cs new file mode 100644 index 0000000..2d1bf6d --- /dev/null +++ b/yy-admin-master/YY.Admin/Event/TabRefreshEvent.cs @@ -0,0 +1,11 @@ +using YY.Admin.Module; + +namespace YY.Admin.Event +{ + /// + /// Tab刷新事件 + /// + public class TabRefreshEvent : PubSubEvent + { + } +} diff --git a/yy-admin-master/YY.Admin/Event/TabSelectedEvent.cs b/yy-admin-master/YY.Admin/Event/TabSelectedEvent.cs new file mode 100644 index 0000000..0f4a08d --- /dev/null +++ b/yy-admin-master/YY.Admin/Event/TabSelectedEvent.cs @@ -0,0 +1,11 @@ +using YY.Admin.Module; + +namespace YY.Admin.Event +{ + /// + /// Tab选中事件 + /// + public class TabSelectedEvent : PubSubEvent + { + } +} diff --git a/yy-admin-master/YY.Admin/Event/TabSourceSelectedEvent.cs b/yy-admin-master/YY.Admin/Event/TabSourceSelectedEvent.cs new file mode 100644 index 0000000..cba373a --- /dev/null +++ b/yy-admin-master/YY.Admin/Event/TabSourceSelectedEvent.cs @@ -0,0 +1,12 @@ + +using YY.Admin.Module; + +namespace YY.Admin.Event +{ + /// + /// Tab源选中事件 + /// + public class TabSourceSelectedEvent : PubSubEvent + { + } +} diff --git a/yy-admin-master/YY.Admin/Filter/GlobalExceptionHandler.cs b/yy-admin-master/YY.Admin/Filter/GlobalExceptionHandler.cs new file mode 100644 index 0000000..ebd3cc1 --- /dev/null +++ b/yy-admin-master/YY.Admin/Filter/GlobalExceptionHandler.cs @@ -0,0 +1,46 @@ +using System.Windows.Threading; +using YY.Admin.Core; +using YY.Admin.EventBus; + +namespace YY.Admin.Filter +{ + public class GlobalExceptionHandler + { + private readonly IErrorHandler _errorHandler; + private ILoggerService _logger; + public GlobalExceptionHandler(IErrorHandler errorHandler, + ILoggerService logger) + { + _errorHandler = errorHandler; + _logger= logger; + + // 注册全局异常处理 + System.Windows.Application.Current.DispatcherUnhandledException += OnDispatcherUnhandledException; + AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; + TaskScheduler.UnobservedTaskException += OnUnobservedTaskException!; + } + + private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) + { + _errorHandler.HandleError(e.Exception); + _logger.Error("未处理的异常", e.Exception); + e.Handled = true; // 标记为已处理 + } + + private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) + { + if (e.ExceptionObject is Exception ex) + { + _errorHandler.HandleError(ex); + _logger.Error("UI线程未处理异常", ex); + } + } + + private void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) + { + _errorHandler.HandleError(e.Exception); + _logger.Error("未观察到的任务异常", e.Exception); + e.SetObserved(); // 标记为已观察 + } + } +} diff --git a/yy-admin-master/YY.Admin/FluentValidation/LoginInputValidator.cs b/yy-admin-master/YY.Admin/FluentValidation/LoginInputValidator.cs new file mode 100644 index 0000000..22e3ec3 --- /dev/null +++ b/yy-admin-master/YY.Admin/FluentValidation/LoginInputValidator.cs @@ -0,0 +1,17 @@ +using FluentValidation; +using YY.Admin.Services; + +namespace YY.Admin.FluentValidation +{ + public class LoginInputValidator : AbstractValidator + { + public LoginInputValidator() + { + RuleFor(x => x.Username) + .NotEmpty().WithMessage("用户名不能为空"); + + RuleFor(x => x.Password) + .NotEmpty().WithMessage("密码不能为空"); + } + } +} diff --git a/yy-admin-master/YY.Admin/FluentValidation/SysUserValidator.cs b/yy-admin-master/YY.Admin/FluentValidation/SysUserValidator.cs new file mode 100644 index 0000000..35a1712 --- /dev/null +++ b/yy-admin-master/YY.Admin/FluentValidation/SysUserValidator.cs @@ -0,0 +1,55 @@ +using FluentValidation; +using NewLife; +using YY.Admin.Core; +using YY.Admin.Services.Service.User; + +namespace YY.Admin.FluentValidation +{ + public class SysUserValidator : AbstractValidator + { + private readonly ISysUserService _userService; + public SysUserValidator(ISysUserService userService) + { + _userService = userService; + + RuleFor(x => x.Account) + .NotEmpty().WithMessage("账号不能为空") + .Length(4, 20).WithMessage("账号长度需在4~20个字符之间") + .Matches(@"^[a-zA-Z0-9_]+$").WithMessage("账号仅能包含字母、数字和下划线") + .MustAsync(async (user, account, cancellation) => + { + var exists = await _userService.AccountExistsAsync(account, user.Id); + return !exists; + }).WithMessage("账号已存在") + .When(x => x.Id == 0); + + RuleFor(x => x.RealName) + .NotEmpty().WithMessage("姓名不能为空") + .Length(2, 10).WithMessage("姓名长度需在2~10个字符之间"); + + RuleFor(x => x.Password) + .NotEmpty().WithMessage("密码不能为空") + .MinimumLength(6).WithMessage("密码长度不能少于6个字符") + .When(x => x.Id == 0); + + RuleFor(x => x.NickName) + .Length(2, 10).WithMessage("昵称长度需在2~10个字符之间") + .When(x => !x.NickName.IsNullOrEmpty()); + + RuleFor(x => x.Sex) + .NotNull().WithMessage("性别不能为空"); + + RuleFor(x => x.Age) + .NotNull().WithMessage("年龄不能为空") + .GreaterThanOrEqualTo(0).WithMessage("年龄不能小于0") + .LessThanOrEqualTo(150).WithMessage("年龄不能大于150"); + + //RuleFor(x => x.Birthday) + // .NotNull().WithMessage("出生日期不能为空"); + + //RuleFor(x => x.Phone) + // .Matches(@"^1[3456789]\d{9}$").WithMessage("手机号码格式不正确!") + // .When(x => !string.IsNullOrWhiteSpace(x.Phone)); + } + } +} diff --git a/yy-admin-master/YY.Admin/Helper/ServerSettingsStore.cs b/yy-admin-master/YY.Admin/Helper/ServerSettingsStore.cs new file mode 100644 index 0000000..1739464 --- /dev/null +++ b/yy-admin-master/YY.Admin/Helper/ServerSettingsStore.cs @@ -0,0 +1,117 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.IO; + +namespace YY.Admin.Helper +{ + /// + /// 服务器连接配置读写工具。 + /// + public static class ServerSettingsStore + { + private const string DefaultWebSocketPath = "/websocket/scada-sync"; + + public class ServerSettingsModel + { + public string Ip { get; set; } = "127.0.0.1"; + public int Port { get; set; } = 8080; + public string BaseScheme { get; set; } = "http"; + public string BasePath { get; set; } = "/jeecg-boot"; + public string WebSocketUrl { get; set; } = string.Empty; + public string WebSocketPath { get; set; } = DefaultWebSocketPath; + } + + public static string GetConfigPath() + { + return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Configuration", "appsettings.json"); + } + + public static ServerSettingsModel Load() + { + var model = new ServerSettingsModel(); + var path = GetConfigPath(); + if (!File.Exists(path)) + { + return model; + } + + var content = File.ReadAllText(path); + var root = JObject.Parse(content); + var jeecg = root["JeecgIntegration"] as JObject; + if (jeecg == null) + { + return model; + } + + var baseUrl = jeecg.Value("BaseUrl") ?? string.Empty; + if (Uri.TryCreate(baseUrl, UriKind.Absolute, out var uri)) + { + model.BaseScheme = uri.Scheme; + model.Ip = uri.Host; + model.Port = uri.Port; + model.BasePath = string.IsNullOrWhiteSpace(uri.AbsolutePath) ? string.Empty : uri.AbsolutePath.TrimEnd('/'); + } + + model.WebSocketUrl = jeecg.Value("WebSocketUrl") ?? string.Empty; + model.WebSocketPath = NormalizeWebSocketPath(jeecg.Value("WebSocketPath")); + return model; + } + + public static void Save(ServerSettingsModel model) + { + var path = GetConfigPath(); + if (!File.Exists(path)) + { + throw new FileNotFoundException("未找到配置文件 appsettings.json", path); + } + + var content = File.ReadAllText(path); + var root = JObject.Parse(content); + var jeecg = root["JeecgIntegration"] as JObject; + if (jeecg == null) + { + jeecg = new JObject(); + root["JeecgIntegration"] = jeecg; + } + + var basePath = string.IsNullOrWhiteSpace(model.BasePath) ? string.Empty : model.BasePath.TrimEnd('/'); + if (!string.IsNullOrWhiteSpace(basePath) && !basePath.StartsWith('/')) + { + basePath = "/" + basePath; + } + + var baseUrl = $"{model.BaseScheme}://{model.Ip}:{model.Port}{basePath}"; + var webSocketPath = NormalizeWebSocketPath(model.WebSocketPath); + var webSocketUrl = string.IsNullOrWhiteSpace(model.WebSocketUrl) + ? BuildDefaultWebSocketUrl(model.BaseScheme, model.Ip, model.Port, basePath, webSocketPath) + : model.WebSocketUrl.Trim(); + jeecg["BaseUrl"] = baseUrl; + jeecg["WebSocketUrl"] = webSocketUrl; + jeecg["WebSocketPath"] = webSocketPath; + + File.WriteAllText(path, root.ToString(Formatting.Indented)); + } + + public static string BuildDefaultWebSocketUrl(string baseScheme, string ip, int port, string basePath, string webSocketPath = DefaultWebSocketPath) + { + var safeScheme = string.Equals(baseScheme, "https", StringComparison.OrdinalIgnoreCase) ? "wss" : "ws"; + var safeBasePath = string.IsNullOrWhiteSpace(basePath) ? string.Empty : basePath.TrimEnd('/'); + if (!string.IsNullOrWhiteSpace(safeBasePath) && !safeBasePath.StartsWith('/')) + { + safeBasePath = "/" + safeBasePath; + } + var safeWsPath = NormalizeWebSocketPath(webSocketPath); + return $"{safeScheme}://{ip}:{port}{safeBasePath}{safeWsPath}"; + } + + private static string NormalizeWebSocketPath(string? webSocketPath) + { + var value = string.IsNullOrWhiteSpace(webSocketPath) ? DefaultWebSocketPath : webSocketPath.Trim(); + if (!value.StartsWith('/')) + { + value = "/" + value; + } + return value; + } + } +} diff --git a/yy-admin-master/YY.Admin/Infrastructure/Hubs/StompWebSocketService.cs b/yy-admin-master/YY.Admin/Infrastructure/Hubs/StompWebSocketService.cs new file mode 100644 index 0000000..582a3e3 --- /dev/null +++ b/yy-admin-master/YY.Admin/Infrastructure/Hubs/StompWebSocketService.cs @@ -0,0 +1,233 @@ +using Microsoft.Extensions.Configuration; +using Prism.Events; +using System.IO; +using System.Net.WebSockets; +using System.Text; +using YY.Admin.Core.Events; +using YY.Admin.Core.Services; +using YY.Admin.Infrastructure.Storage; + +namespace YY.Admin.Infrastructure.Hubs; + +public class StompWebSocketService : ISignalRService +{ + private readonly IConfiguration _configuration; + private readonly IEventAggregator _eventAggregator; + private readonly TokenStore _tokenStore; + private ClientWebSocket? _socket; + private string _deviceId = "default-device"; + private string _token = string.Empty; + + public StompWebSocketService( + IConfiguration configuration, + IEventAggregator eventAggregator, + TokenStore tokenStore) + { + _configuration = configuration; + _eventAggregator = eventAggregator; + _tokenStore = tokenStore; + } + + /// + public async Task ConnectAsync(string token, CancellationToken cancellationToken = default) + { + _token = token ?? string.Empty; + await ConnectUnifiedDeviceChannelAsync(cancellationToken).ConfigureAwait(false); + } + + /// + public async Task ConnectUnifiedDeviceChannelAsync(CancellationToken cancellationToken = default) + { + var anonymous = _configuration.GetValue("JeecgIntegration:AnonymousMode", true); + if (anonymous) + { + _token = string.Empty; + } + else if (string.IsNullOrWhiteSpace(_token)) + { + _token = await _tokenStore.GetTokenAsync(cancellationToken).ConfigureAwait(false) ?? string.Empty; + } + + _deviceId = ResolveDeviceId(_token); + var wsUrl = ResolveWsUrl(); + var retryDelays = new[] { 0, 2, 5, 10, 30 }; + + foreach (var delay in retryDelays) + { + if (delay > 0) + { + await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(false); + } + + try + { + _socket?.Dispose(); + _socket = new ClientWebSocket(); + _socket.Options.AddSubProtocol("v12.stomp"); + await _socket.ConnectAsync(new Uri(wsUrl), cancellationToken).ConfigureAwait(false); + + var connectFrame = anonymous || string.IsNullOrWhiteSpace(_token) + ? "CONNECT\naccept-version:1.2\nheart-beat:10000,10000\n\n\0" + : BuildConnectFrame(_token); + await SendFrameAsync(connectFrame, cancellationToken).ConfigureAwait(false); + + // 用户镜像变更:与后端 /topic/sync/jeecg-users 对齐(设备同步统一线路) + await SendFrameAsync(BuildSubscribeFrame("sub-jeecg-users", "/topic/sync/jeecg-users"), cancellationToken).ConfigureAwait(false); + + // 非免密时同时订阅设备点对点指令队列 + if (!anonymous && !string.IsNullOrWhiteSpace(_token)) + { + await SendFrameAsync(BuildSubscribeFrame("sub-device-command", $"/user/{_deviceId}/queue/command"), cancellationToken).ConfigureAwait(false); + } + + _eventAggregator.GetEvent().Publish(new NetworkStatusChangedPayload + { + IsOnline = true, + ChangedAt = DateTime.UtcNow + }); + + _ = Task.Run(() => ReceiveLoopAsync(cancellationToken), cancellationToken); + return; + } + catch + { + _eventAggregator.GetEvent().Publish(new NetworkStatusChangedPayload + { + IsOnline = false, + ChangedAt = DateTime.UtcNow + }); + } + } + } + + /// + public async Task SendDeviceStatusAsync(object status, CancellationToken cancellationToken = default) + { + if (_socket == null || _socket.State != WebSocketState.Open) + { + return; + } + var json = System.Text.Json.JsonSerializer.Serialize(status); + var frame = $"SEND\n" + + $"destination:/app/device/status\n" + + $"content-type:application/json\n" + + $"content-length:{Encoding.UTF8.GetByteCount(json)}\n\n" + + $"{json}\0"; + await SendFrameAsync(frame, cancellationToken).ConfigureAwait(false); + } + + private async Task ReceiveLoopAsync(CancellationToken cancellationToken) + { + if (_socket == null) + { + return; + } + var buffer = new byte[8192]; + while (_socket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested) + { + using var ms = new MemoryStream(); + WebSocketReceiveResult result; + do + { + result = await _socket.ReceiveAsync(new ArraySegment(buffer), cancellationToken).ConfigureAwait(false); + if (result.MessageType == WebSocketMessageType.Close) + { + _eventAggregator.GetEvent().Publish(new NetworkStatusChangedPayload + { + IsOnline = false, + ChangedAt = DateTime.UtcNow + }); + await ConnectUnifiedDeviceChannelAsync(cancellationToken).ConfigureAwait(false); + return; + } + ms.Write(buffer, 0, result.Count); + } while (!result.EndOfMessage); + + var text = Encoding.UTF8.GetString(ms.ToArray()); + if (!text.StartsWith("MESSAGE", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + var idx = text.IndexOf("\n\n", StringComparison.Ordinal); + if (idx < 0) + { + continue; + } + var body = text[(idx + 2)..].TrimEnd('\0'); + _eventAggregator.GetEvent().Publish(new RemoteCommandPayload + { + DeviceId = _deviceId, + CommandJson = body + }); + } + } + + private async Task SendFrameAsync(string frame, CancellationToken cancellationToken) + { + if (_socket == null || _socket.State != WebSocketState.Open) + { + return; + } + var data = Encoding.UTF8.GetBytes(frame); + await _socket.SendAsync(new ArraySegment(data), WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false); + } + + private string ResolveWsUrl() + { + var baseUrl = _configuration.GetValue("JeecgIntegration:BaseUrl")?.TrimEnd('/'); + if (string.IsNullOrWhiteSpace(baseUrl)) + { + return "ws://127.0.0.1:8080/jeecg-boot/ws/device/websocket"; + } + if (baseUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) + { + return "wss://" + baseUrl["https://".Length..] + "/ws/device/websocket"; + } + return "ws://" + baseUrl["http://".Length..] + "/ws/device/websocket"; + } + + private static string ResolveDeviceId(string token) + { + try + { + var parts = token.Split('.'); + if (parts.Length < 2) + { + return "default-device"; + } + var payload = parts[1].Replace('-', '+').Replace('_', '/'); + payload = payload.PadRight(payload.Length + (4 - payload.Length % 4) % 4, '='); + var json = Encoding.UTF8.GetString(Convert.FromBase64String(payload)); + using var doc = System.Text.Json.JsonDocument.Parse(json); + if (doc.RootElement.TryGetProperty("deviceId", out var deviceId)) + { + return deviceId.GetString() ?? "default-device"; + } + if (doc.RootElement.TryGetProperty("username", out var username)) + { + return username.GetString() ?? "default-device"; + } + return "default-device"; + } + catch + { + return "default-device"; + } + } + + private static string BuildConnectFrame(string token) + { + return "CONNECT\n" + + "accept-version:1.2\n" + + "heart-beat:10000,10000\n" + + $"Authorization:Bearer {token}\n\n\0"; + } + + private static string BuildSubscribeFrame(string subscriptionId, string destination) + { + return "SUBSCRIBE\n" + + $"id:{subscriptionId}\n" + + $"destination:{destination}\n" + + "ack:auto\n\n\0"; + } +} diff --git a/yy-admin-master/YY.Admin/Infrastructure/Network/NetworkMonitor.cs b/yy-admin-master/YY.Admin/Infrastructure/Network/NetworkMonitor.cs new file mode 100644 index 0000000..066ba64 --- /dev/null +++ b/yy-admin-master/YY.Admin/Infrastructure/Network/NetworkMonitor.cs @@ -0,0 +1,111 @@ +using Microsoft.Extensions.Configuration; +using Prism.Events; +using System.Net.Http; +using YY.Admin.Core.Events; +using YY.Admin.Core.Services; + +namespace YY.Admin.Infrastructure.Network; + +public class NetworkMonitor : INetworkMonitor +{ + private readonly IHttpClientFactory _httpClientFactory; + private readonly IConfiguration _configuration; + private readonly IEventAggregator _eventAggregator; + private readonly SemaphoreSlim _startLock = new(1, 1); + private Task? _loopTask; + private CancellationTokenSource? _cts; + private volatile bool _isOnline; + + public NetworkMonitor( + IHttpClientFactory httpClientFactory, + IConfiguration configuration, + IEventAggregator eventAggregator) + { + _httpClientFactory = httpClientFactory; + _configuration = configuration; + _eventAggregator = eventAggregator; + } + + public bool IsOnline => _isOnline; + + public event Action? StatusChanged; + + public async Task StartAsync(CancellationToken cancellationToken = default) + { + await _startLock.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + if (_cts != null) + { + return; + } + + _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + _loopTask = Task.Run(() => MonitorLoopAsync(_cts.Token), _cts.Token); + } + finally + { + _startLock.Release(); + } + } + + private async Task MonitorLoopAsync(CancellationToken cancellationToken) + { + using var timer = new PeriodicTimer(TimeSpan.FromSeconds(10)); + while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false)) + { + var online = await ProbeAsync(cancellationToken).ConfigureAwait(false); + if (online == _isOnline) + { + continue; + } + + _isOnline = online; + StatusChanged?.Invoke(online); + _eventAggregator.GetEvent().Publish(new NetworkStatusChangedPayload + { + IsOnline = online, + ChangedAt = DateTime.UtcNow + }); + } + } + + private async Task ProbeAsync(CancellationToken cancellationToken) + { + var baseUrl = _configuration.GetValue("JeecgIntegration:BaseUrl")?.TrimEnd('/'); + if (string.IsNullOrWhiteSpace(baseUrl)) + { + return false; + } + + // 探活策略:优先调用免登录接口,失败后再降级到健康检查接口 + var probeUrls = new[] + { + $"{baseUrl}/sys/user/scada/queryUser?current=1&pageSize=1", + $"{baseUrl}/sys/dict/scada/queryDictItem?pageNo=1&pageSize=1", + $"{baseUrl}/actuator/health" + }; + + using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + timeoutCts.CancelAfter(TimeSpan.FromSeconds(3)); + var client = _httpClientFactory.CreateClient("JeecgApi"); + foreach (var url in probeUrls) + { + try + { + using var req = new HttpRequestMessage(HttpMethod.Get, url); + using var resp = await client.SendAsync(req, timeoutCts.Token).ConfigureAwait(false); + if (resp.IsSuccessStatusCode) + { + return true; + } + } + catch + { + // 当前探活地址失败后继续尝试下一个降级地址 + } + } + + return false; + } +} diff --git a/yy-admin-master/YY.Admin/Infrastructure/Storage/TokenStore.cs b/yy-admin-master/YY.Admin/Infrastructure/Storage/TokenStore.cs new file mode 100644 index 0000000..00eccb9 --- /dev/null +++ b/yy-admin-master/YY.Admin/Infrastructure/Storage/TokenStore.cs @@ -0,0 +1,48 @@ +using SqlSugar; + +namespace YY.Admin.Infrastructure.Storage; + +public class TokenStore +{ + private const string TokenKey = "DeviceToken"; + private readonly ISqlSugarClient _db; + + public TokenStore(ISqlSugarClient db) + { + _db = db; + } + + public async Task UpdateTokenAsync(string token, CancellationToken cancellationToken = default) + { + var value = token ?? string.Empty; + await EnsureTableAsync(cancellationToken).ConfigureAwait(false); + var sql = "INSERT INTO app_config(config_key,config_value,updated_at) VALUES(@key,@val,@ts) " + + "ON CONFLICT(config_key) DO UPDATE SET config_value=@val, updated_at=@ts;"; + var now = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss"); + await _db.Ado.ExecuteCommandAsync(sql, new[] + { + new SugarParameter("@key", TokenKey), + new SugarParameter("@val", value), + new SugarParameter("@ts", now) + }).ConfigureAwait(false); + _ = cancellationToken; + } + + public async Task GetTokenAsync(CancellationToken cancellationToken = default) + { + await EnsureTableAsync(cancellationToken).ConfigureAwait(false); + var sql = "SELECT config_value FROM app_config WHERE config_key=@key LIMIT 1;"; + var token = await _db.Ado.GetStringAsync(sql, new[] { new SugarParameter("@key", TokenKey) }).ConfigureAwait(false); + return token ?? string.Empty; + } + + private async Task EnsureTableAsync(CancellationToken cancellationToken) + { + const string sql = "CREATE TABLE IF NOT EXISTS app_config(" + + "config_key TEXT PRIMARY KEY," + + "config_value TEXT NULL," + + "updated_at TEXT NULL);"; + await _db.Ado.ExecuteCommandAsync(sql).ConfigureAwait(false); + _ = cancellationToken; + } +} diff --git a/yy-admin-master/YY.Admin/Infrastructure/Sync/HttpSyncClient.cs b/yy-admin-master/YY.Admin/Infrastructure/Sync/HttpSyncClient.cs new file mode 100644 index 0000000..b1e4016 --- /dev/null +++ b/yy-admin-master/YY.Admin/Infrastructure/Sync/HttpSyncClient.cs @@ -0,0 +1,58 @@ +using System.Net.Http.Json; +using System.Net.Http; +using YY.Admin.Core.Models; +using YY.Admin.Infrastructure.Storage; + +namespace YY.Admin.Infrastructure.Sync; + +public class HttpSyncClient +{ + private readonly IHttpClientFactory _httpClientFactory; + private readonly TokenStore _tokenStore; + + private sealed class SyncBatchItem + { + public string MessageId { get; set; } = string.Empty; + public string AggregateType { get; set; } = string.Empty; + public string AggregateId { get; set; } = string.Empty; + public string EventType { get; set; } = string.Empty; + public string Payload { get; set; } = string.Empty; + public DateTime OccurredAt { get; set; } + } + + public HttpSyncClient(IHttpClientFactory httpClientFactory, TokenStore tokenStore) + { + _httpClientFactory = httpClientFactory; + _tokenStore = tokenStore; + } + + public async Task SendBatchAsync(IReadOnlyCollection messages, CancellationToken cancellationToken) + { + if (messages.Count == 0) + { + return true; + } + + var client = _httpClientFactory.CreateClient("JeecgApi"); + var body = messages.Select(m => new SyncBatchItem + { + MessageId = m.Id, + AggregateType = m.AggregateType, + AggregateId = m.AggregateId, + EventType = m.EventType, + Payload = m.Payload, + OccurredAt = m.CreatedAt + }).ToList(); + + using var response = await client.PostAsJsonAsync("/sys/sync/batch", body, cancellationToken).ConfigureAwait(false); + if (response.Headers.TryGetValues("X-Refresh-Token", out var values)) + { + var refreshed = values.FirstOrDefault(); + if (!string.IsNullOrWhiteSpace(refreshed)) + { + await _tokenStore.UpdateTokenAsync(refreshed, cancellationToken).ConfigureAwait(false); + } + } + return response.IsSuccessStatusCode; + } +} diff --git a/yy-admin-master/YY.Admin/Infrastructure/Sync/JeecgUserMirrorPullOutbox.cs b/yy-admin-master/YY.Admin/Infrastructure/Sync/JeecgUserMirrorPullOutbox.cs new file mode 100644 index 0000000..6efebf9 --- /dev/null +++ b/yy-admin-master/YY.Admin/Infrastructure/Sync/JeecgUserMirrorPullOutbox.cs @@ -0,0 +1,29 @@ +using YY.Admin.Core.Services; +using YY.Admin.Core.Sync; + +namespace YY.Admin.Infrastructure.Sync; + +/// +/// 用户镜像拉取入队:统一走 Outbox(断网续传),与设备同步模块同一条基础设施线路。 +/// +public sealed class JeecgUserMirrorPullOutbox : IJeecgUserMirrorPullOutbox +{ + private readonly OutboxProcessor _outboxProcessor; + + public JeecgUserMirrorPullOutbox(OutboxProcessor outboxProcessor) + { + _outboxProcessor = outboxProcessor; + } + + /// + public Task EnqueuePullAsync(string eventType, string? payloadJson, CancellationToken cancellationToken = default) + { + var payload = string.IsNullOrWhiteSpace(payloadJson) ? "{}" : payloadJson; + return _outboxProcessor.EnqueueAsync( + JeecgUserMirrorOutbox.AggregateType, + "mirror", + eventType, + new { source = "unified-device-channel", detail = payload }, + cancellationToken); + } +} diff --git a/yy-admin-master/YY.Admin/Infrastructure/Sync/OutboxProcessor.cs b/yy-admin-master/YY.Admin/Infrastructure/Sync/OutboxProcessor.cs new file mode 100644 index 0000000..d40306b --- /dev/null +++ b/yy-admin-master/YY.Admin/Infrastructure/Sync/OutboxProcessor.cs @@ -0,0 +1,232 @@ +using SqlSugar; +using System.Threading.Channels; +using Prism.Events; +using YY.Admin.Core.Events; +using YY.Admin.Core.Models; +using YY.Admin.Core.Services; +using YY.Admin.Core.Sync; + +namespace YY.Admin.Infrastructure.Sync; + +public class OutboxProcessor +{ + private readonly INetworkMonitor _networkMonitor; + private readonly HttpSyncClient _httpSyncClient; + private readonly IJeecgUserMirrorPullHandler _mirrorPullHandler; + private readonly ISqlSugarClient _db; + private readonly IEventAggregator _eventAggregator; + private readonly Channel _channel = Channel.CreateBounded(new BoundedChannelOptions(1000) + { + SingleReader = true, + SingleWriter = false, + FullMode = BoundedChannelFullMode.Wait + }); + private readonly SemaphoreSlim _flushLock = new(1, 1); + + public OutboxProcessor( + INetworkMonitor networkMonitor, + HttpSyncClient httpSyncClient, + IJeecgUserMirrorPullHandler mirrorPullHandler, + ISqlSugarClient db, + IEventAggregator eventAggregator) + { + _networkMonitor = networkMonitor; + _httpSyncClient = httpSyncClient; + _mirrorPullHandler = mirrorPullHandler; + _db = db.AsTenant().GetConnectionScope("Slave"); + _eventAggregator = eventAggregator; + _networkMonitor.StatusChanged += OnNetworkStatusChanged; + } + + private static bool IsJeecgUserMirrorMessage(OutboxMessage m) => + string.Equals(m.AggregateType, JeecgUserMirrorOutbox.AggregateType, StringComparison.OrdinalIgnoreCase); + + public async Task EnqueueAsync( + string aggregateType, + string aggregateId, + string eventType, + T payload, + CancellationToken cancellationToken) + { + var now = DateTime.UtcNow; + var message = new OutboxMessage + { + AggregateType = aggregateType, + AggregateId = aggregateId, + EventType = eventType, + Payload = System.Text.Json.JsonSerializer.Serialize(payload), + Status = 0, + RetryCount = 0, + CreatedAt = now + }; + await EnsureTableAsync(cancellationToken).ConfigureAwait(false); + await _db.Insertable(message).ExecuteCommandAsync(cancellationToken).ConfigureAwait(false); + if (_networkMonitor.IsOnline) + { + await _channel.Writer.WriteAsync(message, cancellationToken).ConfigureAwait(false); + } + } + + public async Task StartConsumerAsync(CancellationToken cancellationToken) + { + await EnsureTableAsync(cancellationToken).ConfigureAwait(false); + _ = Task.Run(() => ConsumeLoopAsync(cancellationToken), cancellationToken); + } + + public async Task FlushPendingAsync(CancellationToken cancellationToken) + { + if (!await _flushLock.WaitAsync(0, cancellationToken).ConfigureAwait(false)) + { + return; + } + try + { + var pending = await _db.Queryable() + .Where(x => x.Status == 0 && x.RetryCount < 5) + .OrderBy(x => x.CreatedAt) + .ToListAsync(cancellationToken) + .ConfigureAwait(false); + if (pending.Count == 0) + { + return; + } + + var mirror = pending.Where(IsJeecgUserMirrorMessage).ToList(); + var serverBatch = pending.Where(m => !IsJeecgUserMirrorMessage(m)).ToList(); + + foreach (var item in mirror) + { + var ok = await _mirrorPullHandler.ExecutePullAsync(cancellationToken).ConfigureAwait(false); + if (ok) + { + await MarkSentAsync(item, cancellationToken).ConfigureAwait(false); + _eventAggregator.GetEvent().Publish(item.AggregateId); + } + else + { + await MarkFailedAsync(item, "Jeecg用户镜像拉取失败", cancellationToken).ConfigureAwait(false); + } + } + + if (serverBatch.Count == 0) + { + return; + } + + var success = await _httpSyncClient.SendBatchAsync(serverBatch, cancellationToken).ConfigureAwait(false); + if (success) + { + var ids = serverBatch.Select(x => x.Id).ToArray(); + await _db.Updateable() + .SetColumns(x => new OutboxMessage + { + Status = 1, + SentAt = DateTime.UtcNow, + LastTriedAt = DateTime.UtcNow, + ErrorMessage = null + }) + .Where(x => ids.Contains(x.Id)) + .ExecuteCommandAsync(cancellationToken) + .ConfigureAwait(false); + foreach (var item in serverBatch) + { + _eventAggregator.GetEvent().Publish(item.AggregateId); + } + return; + } + + foreach (var item in serverBatch) + { + await MarkFailedAsync(item, "批量同步失败", cancellationToken).ConfigureAwait(false); + } + } + finally + { + _flushLock.Release(); + } + } + + private async Task ConsumeLoopAsync(CancellationToken cancellationToken) + { + while (await _channel.Reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) + { + while (_channel.Reader.TryRead(out var message)) + { + var success = IsJeecgUserMirrorMessage(message) + ? await _mirrorPullHandler.ExecutePullAsync(cancellationToken).ConfigureAwait(false) + : await _httpSyncClient.SendBatchAsync(new[] { message }, cancellationToken).ConfigureAwait(false); + if (success) + { + await MarkSentAsync(message, cancellationToken).ConfigureAwait(false); + _eventAggregator.GetEvent().Publish(message.AggregateId); + } + else + { + await MarkFailedAsync(message, "实时同步失败", cancellationToken).ConfigureAwait(false); + } + } + } + } + + private async Task MarkSentAsync(OutboxMessage message, CancellationToken cancellationToken) + { + await _db.Updateable() + .SetColumns(x => new OutboxMessage + { + Status = 1, + SentAt = DateTime.UtcNow, + LastTriedAt = DateTime.UtcNow, + ErrorMessage = null + }) + .Where(x => x.Id == message.Id) + .ExecuteCommandAsync(cancellationToken) + .ConfigureAwait(false); + } + + private async Task MarkFailedAsync(OutboxMessage message, string error, CancellationToken cancellationToken) + { + var nextRetry = message.RetryCount + 1; + var backoff = (int)Math.Pow(2, Math.Min(nextRetry, 5)); + await _db.Updateable() + .SetColumns(x => new OutboxMessage + { + RetryCount = nextRetry, + Status = nextRetry >= 5 ? 2 : 0, + ErrorMessage = error, + LastTriedAt = DateTime.UtcNow + }) + .Where(x => x.Id == message.Id) + .ExecuteCommandAsync(cancellationToken) + .ConfigureAwait(false); + + if (nextRetry < 5) + { + await Task.Delay(TimeSpan.FromSeconds(backoff), cancellationToken).ConfigureAwait(false); + if (_networkMonitor.IsOnline) + { + var retryMessage = await _db.Queryable() + .FirstAsync(x => x.Id == message.Id, cancellationToken) + .ConfigureAwait(false); + if (retryMessage != null && retryMessage.Status == 0) + { + await _channel.Writer.WriteAsync(retryMessage, cancellationToken).ConfigureAwait(false); + } + } + } + } + + private async Task EnsureTableAsync(CancellationToken cancellationToken) + { + _ = cancellationToken; + await Task.Run(() => _db.CodeFirst.InitTables(), cancellationToken).ConfigureAwait(false); + } + + private void OnNetworkStatusChanged(bool isOnline) + { + if (!isOnline) + { + return; + } + _ = FlushPendingAsync(default); + } +} diff --git a/yy-admin-master/YY.Admin/Module/CacheExtensions.cs b/yy-admin-master/YY.Admin/Module/CacheExtensions.cs new file mode 100644 index 0000000..ab6bfcf --- /dev/null +++ b/yy-admin-master/YY.Admin/Module/CacheExtensions.cs @@ -0,0 +1,78 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Identity.Client; +using NewLife.Caching.Services; +using NewLife.Caching; +using NewLife.Log; +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using YY.Admin.Core; +using YY.Admin.Core.Option; +using NewLife.Configuration; + +public static class CacheExtensions +{ + /// + /// 注册缓存服务 + /// + public static void AddNewLifeCache(this IContainerRegistry containerRegistry,IConfiguration configuration) + { + // 读取配置 + var BaseCacheOptions = configuration.GetSection("Cache").Get(); + + // 注册配置选项 + containerRegistry.RegisterInstance(BaseCacheOptions); + + // 注册缓存提供器 + if (BaseCacheOptions.CacheType == CacheTypeEnum.Redis.ToString()) + { + containerRegistry.RegisterSingleton(() => + { + return CreateRedisCacheProvider(BaseCacheOptions); + }); + } + else + { + // 默认使用内存缓存 + containerRegistry.RegisterSingleton(); + } + + // 注册缓存服务 + containerRegistry.RegisterSingleton(); + } + private static ICacheProvider CreateRedisCacheProvider(BaseCacheOptions options) + { + var redis = new FullRedis + { + Name = "RedisCache", + Tracer = null, // 禁用跟踪器 + Log = XTrace.Log // 使用NewLife日志 + }; + + // 初始化Redis + redis.Init(options.Redis.Configuration!); + + // 设置前缀 + if (!string.IsNullOrEmpty(options.Redis.Prefix)) + { + redis.Prefix = options.Redis.Prefix; + } + + // 设置最大消息大小 + if (options.Redis.MaxMessageSize > 0) + { + redis.MaxMessageSize = options.Redis.MaxMessageSize; + } + + //// 测试连接 + //if (!redis.Ping()) + //{ + // throw new Exception("Redis连接失败,请检查配置"); + //} + + return new RedisCacheProvider { Cache = redis }; + } +} diff --git a/yy-admin-master/YY.Admin/Module/DbExtensions.cs b/yy-admin-master/YY.Admin/Module/DbExtensions.cs new file mode 100644 index 0000000..9356763 --- /dev/null +++ b/yy-admin-master/YY.Admin/Module/DbExtensions.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Identity.Client; +using NewLife.Caching.Services; +using NewLife.Caching; +using NewLife.Log; +using SqlSugar; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using YY.Admin.Core; +using YY.Admin.Core.Option; +using NewLife.Configuration; +using YY.Admin.Core.SqlSugar; + +public static class DbExtensions +{ + /// + /// 注册缓存服务 + /// + public static void AddDbContext(this IContainerRegistry containerRegistry,IConfiguration configuration) + { + containerRegistry.AddSqlSugar(configuration); + } + +} diff --git a/yy-admin-master/YY.Admin/Module/HttpClientExtensions.cs b/yy-admin-master/YY.Admin/Module/HttpClientExtensions.cs new file mode 100644 index 0000000..a2beb4b --- /dev/null +++ b/yy-admin-master/YY.Admin/Module/HttpClientExtensions.cs @@ -0,0 +1,41 @@ +using System.Net.Http; + +namespace YY.Admin.Module +{ + public static class HttpClientExtensions + { + + public static void AddHttpClient(this IContainerRegistry containerRegistry) + { + containerRegistry.RegisterSingleton(() => + { + var handler = new SocketsHttpHandler + { + // 连接池大小 + MaxConnectionsPerServer = 50, + + // 最大存活时间(绝对时间) + PooledConnectionLifetime = TimeSpan.FromMinutes(5), + + // 空闲连接超时时间,超时后自动回收 + PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2), + + // 是否自动重试 + AutomaticDecompression = System.Net.DecompressionMethods.GZip | + System.Net.DecompressionMethods.Deflate + }; + + var client = new HttpClient(handler) + { + // 默认超时时间 30秒 + Timeout = TimeSpan.FromSeconds(30) + }; + + client.DefaultRequestHeaders.UserAgent.ParseAdd("YY.Admin"); + + return client; + }); + + } + } +} diff --git a/yy-admin-master/YY.Admin/Module/NavItem.cs b/yy-admin-master/YY.Admin/Module/NavItem.cs new file mode 100644 index 0000000..f12acaf --- /dev/null +++ b/yy-admin-master/YY.Admin/Module/NavItem.cs @@ -0,0 +1,29 @@ +using YY.Admin.Core; + +namespace YY.Admin.Module +{ + /// + /// SideBar选项 + /// + public class NavItem : TabSource + { + /// + /// 是否放到底部 + /// + public bool AlignBottom { get; set; } = false; + + /// + /// 是否支持高亮选中 + /// + public bool IsActive { get; set; } = true; + + /// + /// 图标类型 + /// + public override IconTypeEnum IconType { get => base.IconType; set => base.IconType = value; } + public NavItem() + { + IconType = IconTypeEnum.MaterialDesign; + } + } +} diff --git a/yy-admin-master/YY.Admin/Module/NavigationExtensions.cs b/yy-admin-master/YY.Admin/Module/NavigationExtensions.cs new file mode 100644 index 0000000..2f6bebe --- /dev/null +++ b/yy-admin-master/YY.Admin/Module/NavigationExtensions.cs @@ -0,0 +1,82 @@ + +using System.Windows; +using System.Windows.Media; +using YY.Admin.ViewModels.Control; +using YY.Admin.ViewModels.Dialogs; +using YY.Admin.Views; +using YY.Admin.Views.Control; +using YY.Admin.Views.Dialogs; +using YY.Admin.Views.SysManage; + +namespace YY.Admin +{ + public static class NavigationExtensions + { + /// + /// 注册导航 + /// + public static void AddNavigation(this IContainerRegistry containerRegistry) + { + // 注册对话框 + containerRegistry.RegisterDialog("AlertDialog"); + containerRegistry.RegisterDialog("SuccessDialog"); + containerRegistry.RegisterDialog("ErrorDialog"); + containerRegistry.RegisterDialog("WarningDialog"); + containerRegistry.RegisterDialog("ConfirmDialog"); + containerRegistry.RegisterDialog("ServerSettingsDialog"); + + // 设置对话框样式 + containerRegistry.RegisterDialogWindow(); + + // 注册导航 + containerRegistry.RegisterForNavigation("DashboardView"); + // 404视图 + containerRegistry.RegisterForNavigation("NotFoundView"); + //containerRegistry.RegisterForNavigation("RoleManagementView"); + //containerRegistry.RegisterForNavigation("PermissionManagementView"); + //containerRegistry.RegisterForNavigation("OrderManagementView"); + //containerRegistry.RegisterForNavigation("ProductManagementView"); + //containerRegistry.RegisterForNavigation("ReportView"); + //containerRegistry.RegisterForNavigation("MonitorView"); + + // 窗口注册 + containerRegistry.Register(); + containerRegistry.Register(); + + // 注册视图(页面) + containerRegistry.RegisterForNavigation(); + containerRegistry.RegisterForNavigation(); + containerRegistry.RegisterForNavigation(); + containerRegistry.RegisterForNavigation(); + containerRegistry.RegisterForNavigation(); + + } + } + public class DialogWindow : Window, IDialogWindow + { + public DialogWindow() + { + WindowStyle = WindowStyle.None; + AllowsTransparency = true; + Background = Brushes.Transparent; // 背景透明 + WindowStartupLocation = WindowStartupLocation.CenterOwner; + SizeToContent = SizeToContent.WidthAndHeight; + ResizeMode = ResizeMode.NoResize; + } + public IDialogResult? Result { get; set; } + } + //public class DialogWindow : Window, IDialogWindow + //{ + // public DialogWindow() + // { + // //InitializeComponent(); + + // // 去掉最大化最小化 + // ResizeMode = ResizeMode.NoResize; + + // // 去掉右上角系统按钮 + // WindowStyle = WindowStyle.None; + // } + // public IDialogResult Result { get; set; } + //} +} diff --git a/yy-admin-master/YY.Admin/Module/NotificationExtensions.cs b/yy-admin-master/YY.Admin/Module/NotificationExtensions.cs new file mode 100644 index 0000000..afe9c43 --- /dev/null +++ b/yy-admin-master/YY.Admin/Module/NotificationExtensions.cs @@ -0,0 +1,24 @@ +using System; + +using System.Collections.Generic; +using System.Threading.Tasks; +using YY.Admin.EventBus; +using static YY.Admin.Core.SysUserEvents; +using System.Reflection; +using YY.Admin.Core; +using Prism.Ioc; + +namespace YY.Admin.Module +{ + public static class NotificationExtensions + { + /// + /// 注册通知事件 + /// + public static void AddNotificationEventBus(this IContainerRegistry containerRegistry) + { + // 注册EventAggregator为单例 + containerRegistry.RegisterSingleton(); + } + } +} diff --git a/yy-admin-master/YY.Admin/Module/ProjectOptions.cs b/yy-admin-master/YY.Admin/Module/ProjectOptions.cs new file mode 100644 index 0000000..c6ec833 --- /dev/null +++ b/yy-admin-master/YY.Admin/Module/ProjectOptions.cs @@ -0,0 +1,31 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using YY.Admin.Core.Option; +using YY.Admin.Core.SqlSugar; + +namespace YY.Admin.Setup +{ + public static class ProjectOptions + { + /// + /// 注册项目配置选项到Prism容器 + /// + public static IContainerRegistry AddProjectOptions( + this IContainerRegistry containerRegistry, + IConfiguration configuration) + { + // 绑定数据库连接配置到对象 + var dbOptions = configuration.GetSection("DbConnection").Get(); + + // 注册配置实例 + containerRegistry.RegisterInstance(dbOptions); + + return containerRegistry; + } + } +} diff --git a/yy-admin-master/YY.Admin/Module/ServiceExtensions.cs b/yy-admin-master/YY.Admin/Module/ServiceExtensions.cs new file mode 100644 index 0000000..b85798e --- /dev/null +++ b/yy-admin-master/YY.Admin/Module/ServiceExtensions.cs @@ -0,0 +1,109 @@ +using Microsoft.Extensions.Configuration; +using System.Reflection; +using YY.Admin.Core; +using YY.Admin.Services.Service.Auth; + +namespace YY.Admin +{ + public static class ServiceExtensions + { + /// + /// 注册服务 + /// + public static void AddService(this IContainerRegistry containerRegistry, IConfiguration configuration) + { + // 注册配置 + containerRegistry.RegisterInstance(configuration); + //// 注册日志服务为单例 + containerRegistry.RegisterSingleton(); + // 自动扫描并注册应用层服务 + RegisterServicesByAssembly(containerRegistry, typeof(ISysAuthService).Assembly); + } + /// + /// 自动注册程序集中所有服务 + /// + private static void RegisterServicesByAssembly(IContainerRegistry containerRegistry, Assembly assembly) + { + // 获取所有公开的非抽象类 + var serviceTypes = assembly.GetExportedTypes() + .Where(t => t.IsClass && !t.IsAbstract); + + foreach (var implementationType in serviceTypes) + { + // 查找服务接口(名称以"I"开头,去掉首字母后匹配) + var serviceInterface = implementationType.GetInterfaces() + .FirstOrDefault(i => i.Name == $"I{implementationType.Name}"); + + // 如果没有直接匹配的接口,尝试查找其他接口 + if (serviceInterface == null) + { + // 备选方案1:注册所有实现的接口(适合多接口实现) + RegisterAllInterfaces(containerRegistry, implementationType); + + // 备选方案2:只注册主接口 + // serviceInterface = implementationType.GetInterface($"I{implementationType.Name}"); + } + else + { + // 注册找到的接口 + RegisterService(containerRegistry, serviceInterface, implementationType); + } + } + } + + /// + /// 注册服务到容器 + /// + private static void RegisterService( + IContainerRegistry containerRegistry, + Type serviceType, + Type implementationType) + { + // 根据命名约定判断生命周期 + bool isSingleton = implementationType.Name.EndsWith("Service") || + implementationType.Name.EndsWith("Repository"); + + // 根据基类判断生命周期 + bool isSingletonByBase = typeof(ISingletonDependency).IsAssignableFrom(implementationType); + + // 根据特性判断生命周期 + var attribute = implementationType.GetCustomAttribute(); + var lifecycle = attribute?.Lifecycle ?? Lifecycle.Transient; + + // 确定注册方式 + if (isSingleton || isSingletonByBase || lifecycle == Lifecycle.Singleton) + { + containerRegistry.RegisterSingleton(serviceType, implementationType); + Console.WriteLine($"注册单例服务: {serviceType.Name} -> {implementationType.Name}"); + } + else + { + containerRegistry.Register(serviceType, implementationType); + Console.WriteLine($"注册瞬时服务: {serviceType.Name} -> {implementationType.Name}"); + } + } + + /// + /// 注册所有实现的接口 + /// + private static void RegisterAllInterfaces(IContainerRegistry containerRegistry, Type implementationType) + { + var interfaces = implementationType.GetInterfaces() + .Where(i => i != typeof(IDisposable) && + !i.Name.StartsWith("_") && // 排除某些特殊接口 + i.Assembly == implementationType.Assembly); // 只注册同程序集接口 + + foreach (var serviceType in interfaces) + { + RegisterService(containerRegistry, serviceType, implementationType); + } + + if (!interfaces.Any()) + { + // 如果没有接口,直接注册具体类型 + containerRegistry.Register(implementationType); + Console.WriteLine($"注册具体类型: {implementationType.Name}"); + } + } +} +} diff --git a/yy-admin-master/YY.Admin/Module/SyncModule.cs b/yy-admin-master/YY.Admin/Module/SyncModule.cs new file mode 100644 index 0000000..d81e622 --- /dev/null +++ b/yy-admin-master/YY.Admin/Module/SyncModule.cs @@ -0,0 +1,69 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Configuration; +using Polly; +using Polly.Extensions.Http; +using Prism.Ioc; +using Prism.Modularity; +using System.Net.Http; +using YY.Admin.Core.Services; +using YY.Admin.Infrastructure.Hubs; +using YY.Admin.Infrastructure.Network; +using YY.Admin.Infrastructure.Storage; +using YY.Admin.Infrastructure.Sync; + +namespace YY.Admin.Module; + +public class SyncModule : IModule +{ + public void RegisterTypes(IContainerRegistry containerRegistry) + { + containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); + containerRegistry.RegisterSingleton(); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddHttpClient("JeecgApi", (sp, client) => + { + var config = containerRegistry.GetContainer().Resolve(); + var baseUrl = config.GetValue("JeecgIntegration:BaseUrl")?.TrimEnd('/'); + if (!string.IsNullOrWhiteSpace(baseUrl)) + { + client.BaseAddress = new Uri(baseUrl); + } + + var tokenStore = containerRegistry.GetContainer().Resolve(); + var token = tokenStore.GetTokenAsync(default).ConfigureAwait(false).GetAwaiter().GetResult(); + if (!string.IsNullOrWhiteSpace(token)) + { + client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + } + }).AddPolicyHandler(GetRetryPolicy()); + + var provider = serviceCollection.BuildServiceProvider(); + var httpClientFactory = provider.GetRequiredService(); + containerRegistry.RegisterInstance(httpClientFactory); + } + + public void OnInitialized(IContainerProvider containerProvider) + { + var networkMonitor = containerProvider.Resolve(); + var outboxProcessor = containerProvider.Resolve(); + var signalService = containerProvider.Resolve(); + + _ = networkMonitor.StartAsync(CancellationToken.None); + _ = outboxProcessor.StartConsumerAsync(CancellationToken.None); + // 用户镜像 + 设备指令:统一 STOMP(/ws/device),免密与设备 Token 模式均启动 + _ = Task.Run(() => signalService.ConnectUnifiedDeviceChannelAsync(CancellationToken.None)); + } + + private static IAsyncPolicy GetRetryPolicy() + { + return HttpPolicyExtensions + .HandleTransientHttpError() + .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); + } + +} diff --git a/yy-admin-master/YY.Admin/Module/TabItemModel.cs b/yy-admin-master/YY.Admin/Module/TabItemModel.cs new file mode 100644 index 0000000..105529d --- /dev/null +++ b/yy-admin-master/YY.Admin/Module/TabItemModel.cs @@ -0,0 +1,246 @@ +using System.Collections.ObjectModel; +using YY.Admin.Core; +using YY.Admin.Event; + +namespace YY.Admin.Module +{ + /// + /// 单个标签的数据模型 + /// + public class TabItemModel : BindableBase + { + public ObservableCollection? OpenTabs { get; set; } + + public IEventAggregator? EventAggregator { get; set; } + + private string? _id; + public string? Id + { + get => _id; + set => SetProperty(ref _id, value); + } + + private string? _header; + public string? Header + { + get => _header; + set => SetProperty(ref _header, value); + } + + private string? _icon = string.Empty; + public string? Icon + { + get => _icon; + set => SetProperty(ref _icon, value); + } + + private IconTypeEnum? _iconType; + public IconTypeEnum IconType { + get { + return _iconType ?? IconTypeEnum.AntDesign; + } + set => SetProperty(ref _iconType, value); + } + + private string? _viewName = string.Empty; + public string? ViewName + { + get => _viewName; + set => SetProperty(ref _viewName, value); + } + + /// + /// Tab是否允许关闭 + /// + private bool _isClosable = true; + public bool IsClosable + { + get => _isClosable; + set => SetProperty(ref _isClosable, value); + } + + private bool _isSelected; + public bool IsSelected + { + get => _isSelected; + set => SetProperty(ref _isSelected, value); + } + + // 添加对Tab源的引用 + public TabSource? TabSource { get; set; } + + /// + /// 左侧是否有可关闭Tab + /// + public bool HasClosableLeft => OpenTabs != null && OpenTabs + .Take(OpenTabs.IndexOf(this)) + .Any(t => t.IsClosable); + + /// + /// 右侧是否有可关闭Tab + /// + public bool HasClosableRight => OpenTabs != null && OpenTabs + .Skip(OpenTabs.IndexOf(this) + 1) + .Any(t => t.IsClosable); + + /// + /// 其他Tab(除当前)是否有可关闭Tab + /// + public bool HasClosableOther => OpenTabs != null && OpenTabs + .Where(t => t != this) + .Any(t => t.IsClosable); + + /// + /// 是否有任意可关闭Tab(全部) + /// + public bool HasClosableAny => OpenTabs != null && OpenTabs + .Any(t => t.IsClosable); + + public DelegateCommand RefreshTabCommand { get; } + public DelegateCommand CloseTabCommand { get; } + public DelegateCommand CloseLeftTabsCommand { get; } + public DelegateCommand CloseRightTabsCommand { get; } + public DelegateCommand CloseOtherTabsCommand { get; } + public DelegateCommand CloseAllTabsCommand { get; } + + public TabItemModel() + { + Id = $"TabRegion_{Guid.NewGuid():N}"; // 保证唯一 + + RefreshTabCommand = new DelegateCommand(RefreshTab); + CloseTabCommand = new DelegateCommand(CloseTab); + CloseLeftTabsCommand = new DelegateCommand(CloseLeftTabs); + CloseRightTabsCommand = new DelegateCommand(CloseRightTabs); + CloseOtherTabsCommand = new DelegateCommand(CloseOtherTabs); + CloseAllTabsCommand = new DelegateCommand(CloseAllTabs); + } + + /// + /// 刷新当前Tab + /// + /// + private void RefreshTab(TabItemModel tabItemModel) + { + if (tabItemModel?.TabSource == null) + { + return; + } + // 发布事件 + EventAggregator?.GetEvent().Publish(tabItemModel); + } + + /// + /// 关闭当前Tab + /// + /// + private void CloseTab(TabItemModel tabItemModel) + { + if (tabItemModel?.IsClosable != true || OpenTabs?.Any() != true) + { + return; + } + // 发布事件 + EventAggregator?.GetEvent().Publish(tabItemModel); + + // 从集合中移除标签 + OpenTabs.Remove(tabItemModel); + } + + /// + /// 关闭左侧Tab + /// + private void CloseLeftTabs(TabItemModel tabItemModel) + { + if (OpenTabs?.Any() != true) + { + return; + } + + // 找到当前 Tab 的索引 + int currentIndex = OpenTabs.IndexOf(tabItemModel); + // 左侧没有 Tab + if (currentIndex <= 0) + { + return; + } + + // 找出左侧所有可关闭的 Tab + var leftTabs = OpenTabs + .Take(currentIndex) // 取前面所有 Tab + .Where(t => t.IsClosable) // 只关闭可关闭的 + .ToList(); // 先 ToList 避免集合修改时报错 + + foreach (var tab in leftTabs) + { + CloseTab(tab); + } + } + + /// + /// 关闭右侧Tab + /// + private void CloseRightTabs(TabItemModel tabItemModel) + { + if (OpenTabs?.Any() != true) + { + return; + } + + int currentIndex = OpenTabs.IndexOf(tabItemModel); + if (currentIndex < 0 || currentIndex >= OpenTabs.Count - 1) + { + return; + } + + var rightTabs = OpenTabs + .Skip(currentIndex + 1) + .Where(t => t.IsClosable) + .ToList(); + + foreach (var tab in rightTabs) + { + CloseTab(tab); + } + } + + /// + /// 关闭其他Tab(仅保留当前) + /// + private void CloseOtherTabs(TabItemModel tabItemModel) + { + if (OpenTabs?.Any() != true) + { + return; + } + + var otherTabs = OpenTabs + .Where(t => t != tabItemModel && t.IsClosable) + .ToList(); + + foreach (var tab in otherTabs) + { + CloseTab(tab); + } + } + + /// + /// 关闭全部(包括当前) + /// + private void CloseAllTabs(TabItemModel tabItemModel) + { + if (OpenTabs?.Any() != true) + { + return; + } + + var allClosableTabs = OpenTabs + .Where(t => t.IsClosable) + .ToList(); + + foreach (var tab in allClosableTabs) + { + CloseTab(tab); + } + } + } +} diff --git a/yy-admin-master/YY.Admin/Module/TabSource.cs b/yy-admin-master/YY.Admin/Module/TabSource.cs new file mode 100644 index 0000000..264fba3 --- /dev/null +++ b/yy-admin-master/YY.Admin/Module/TabSource.cs @@ -0,0 +1,56 @@ +using System.Windows.Input; +using YY.Admin.Core; + +namespace YY.Admin.Module +{ + public class TabSource : BindableBase + { + /// + /// 名称 + /// + public virtual string? Name { get; set; } + + /// + /// 图标 + /// + public virtual string? Icon { get; set; } + + /// + /// 图标类型 + /// + public virtual IconTypeEnum IconType { get; set; } = IconTypeEnum.AntDesign; + + /// + /// 视图 + /// + public virtual string? ViewName { get; set; } + + /// + /// 视图的导航参数 + /// + public INavigationParameters? NavigationParameter { get; set; } + + /// + /// 是否选中 + /// + private bool _isSelected; + + public virtual bool IsSelected + { + get => _isSelected; + set => SetProperty(ref _isSelected, value); + } + + /// + /// Tab是否允许关闭 + /// + private bool _isClosable = true; + public bool IsClosable + { + get => _isClosable; + set => SetProperty(ref _isClosable, value); + } + + public ICommand? Command { get; set; } + } +} diff --git a/yy-admin-master/YY.Admin/Properties/AppSettings.Designer.cs b/yy-admin-master/YY.Admin/Properties/AppSettings.Designer.cs new file mode 100644 index 0000000..6dda7de --- /dev/null +++ b/yy-admin-master/YY.Admin/Properties/AppSettings.Designer.cs @@ -0,0 +1,38 @@ +//------------------------------------------------------------------------------ +// +// 此代码由工具生成。 +// 运行时版本:4.0.30319.42000 +// +// 对此文件的更改可能会导致不正确的行为,并且如果 +// 重新生成代码,这些更改将会丢失。 +// +//------------------------------------------------------------------------------ + +namespace YY.Admin.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.14.0.0")] + internal sealed partial class AppSettings : global::System.Configuration.ApplicationSettingsBase { + + private static AppSettings defaultInstance = ((AppSettings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new AppSettings()))); + + public static AppSettings Default { + get { + return defaultInstance; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("0")] + public int SkinType { + get { + return ((int)(this["SkinType"])); + } + set { + this["SkinType"] = value; + } + } + } +} diff --git a/yy-admin-master/YY.Admin/Properties/AppSettings.settings b/yy-admin-master/YY.Admin/Properties/AppSettings.settings new file mode 100644 index 0000000..7292173 --- /dev/null +++ b/yy-admin-master/YY.Admin/Properties/AppSettings.settings @@ -0,0 +1,9 @@ + + + + + + 0 + + + \ No newline at end of file diff --git a/yy-admin-master/YY.Admin/Resources/Icon/404.png b/yy-admin-master/YY.Admin/Resources/Icon/404.png new file mode 100644 index 0000000..3f47b5d Binary files /dev/null and b/yy-admin-master/YY.Admin/Resources/Icon/404.png differ diff --git a/yy-admin-master/YY.Admin/Resources/Icon/FontAwesome/Font Awesome 7 Brands-Regular-400.otf b/yy-admin-master/YY.Admin/Resources/Icon/FontAwesome/Font Awesome 7 Brands-Regular-400.otf new file mode 100644 index 0000000..dc9a6df Binary files /dev/null and b/yy-admin-master/YY.Admin/Resources/Icon/FontAwesome/Font Awesome 7 Brands-Regular-400.otf differ diff --git a/yy-admin-master/YY.Admin/Resources/Icon/FontAwesome/Font Awesome 7 Free-Regular-400.otf b/yy-admin-master/YY.Admin/Resources/Icon/FontAwesome/Font Awesome 7 Free-Regular-400.otf new file mode 100644 index 0000000..bdaae48 Binary files /dev/null and b/yy-admin-master/YY.Admin/Resources/Icon/FontAwesome/Font Awesome 7 Free-Regular-400.otf differ diff --git a/yy-admin-master/YY.Admin/Resources/Icon/FontAwesome/Font Awesome 7 Free-Solid-900.otf b/yy-admin-master/YY.Admin/Resources/Icon/FontAwesome/Font Awesome 7 Free-Solid-900.otf new file mode 100644 index 0000000..881f5d3 Binary files /dev/null and b/yy-admin-master/YY.Admin/Resources/Icon/FontAwesome/Font Awesome 7 Free-Solid-900.otf differ diff --git a/yy-admin-master/YY.Admin/Resources/Icon/avatar.png b/yy-admin-master/YY.Admin/Resources/Icon/avatar.png new file mode 100644 index 0000000..30bdc9e Binary files /dev/null and b/yy-admin-master/YY.Admin/Resources/Icon/avatar.png differ diff --git a/yy-admin-master/YY.Admin/Resources/Icon/iconfont.ttf b/yy-admin-master/YY.Admin/Resources/Icon/iconfont.ttf new file mode 100644 index 0000000..f9e8d4d Binary files /dev/null and b/yy-admin-master/YY.Admin/Resources/Icon/iconfont.ttf differ diff --git a/yy-admin-master/YY.Admin/Resources/Icon/login.png b/yy-admin-master/YY.Admin/Resources/Icon/login.png new file mode 100644 index 0000000..d59a15c Binary files /dev/null and b/yy-admin-master/YY.Admin/Resources/Icon/login.png differ diff --git a/yy-admin-master/YY.Admin/Resources/Icon/logo.ico b/yy-admin-master/YY.Admin/Resources/Icon/logo.ico new file mode 100644 index 0000000..c5ac729 Binary files /dev/null and b/yy-admin-master/YY.Admin/Resources/Icon/logo.ico differ diff --git a/yy-admin-master/YY.Admin/Resources/Icon/logo.png b/yy-admin-master/YY.Admin/Resources/Icon/logo.png new file mode 100644 index 0000000..3e0bc8b Binary files /dev/null and b/yy-admin-master/YY.Admin/Resources/Icon/logo.png differ diff --git a/yy-admin-master/YY.Admin/Resources/Styles/HandyControl/Brushes.xaml b/yy-admin-master/YY.Admin/Resources/Styles/HandyControl/Brushes.xaml new file mode 100644 index 0000000..ecdcb3f --- /dev/null +++ b/yy-admin-master/YY.Admin/Resources/Styles/HandyControl/Brushes.xaml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/yy-admin-master/YY.Admin/Resources/Styles/HandyControl/Colors.xaml b/yy-admin-master/YY.Admin/Resources/Styles/HandyControl/Colors.xaml new file mode 100644 index 0000000..d746c6d --- /dev/null +++ b/yy-admin-master/YY.Admin/Resources/Styles/HandyControl/Colors.xaml @@ -0,0 +1,6 @@ + + + #e0e0e0 + + \ No newline at end of file diff --git a/yy-admin-master/YY.Admin/Resources/Styles/HandyControl/ColorsDark.xaml b/yy-admin-master/YY.Admin/Resources/Styles/HandyControl/ColorsDark.xaml new file mode 100644 index 0000000..c8e09e6 --- /dev/null +++ b/yy-admin-master/YY.Admin/Resources/Styles/HandyControl/ColorsDark.xaml @@ -0,0 +1,11 @@ + + + #8d9095 + #cfd3dc + #616161 + + + + + \ No newline at end of file diff --git a/yy-admin-master/YY.Admin/Resources/Styles/HandyControl/ColorsViolet.xaml b/yy-admin-master/YY.Admin/Resources/Styles/HandyControl/ColorsViolet.xaml new file mode 100644 index 0000000..d746c6d --- /dev/null +++ b/yy-admin-master/YY.Admin/Resources/Styles/HandyControl/ColorsViolet.xaml @@ -0,0 +1,6 @@ + + + #e0e0e0 + + \ No newline at end of file diff --git a/yy-admin-master/YY.Admin/Resources/Styles/HandyControl/Styles.xaml b/yy-admin-master/YY.Admin/Resources/Styles/HandyControl/Styles.xaml new file mode 100644 index 0000000..f443804 --- /dev/null +++ b/yy-admin-master/YY.Admin/Resources/Styles/HandyControl/Styles.xaml @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/yy-admin-master/YY.Admin/Resources/Styles/Icons.xaml b/yy-admin-master/YY.Admin/Resources/Styles/Icons.xaml new file mode 100644 index 0000000..5a2b4fa --- /dev/null +++ b/yy-admin-master/YY.Admin/Resources/Styles/Icons.xaml @@ -0,0 +1,43 @@ + + + + M549.61981 133.022476l319.683047 203.605334A70.851048 70.851048 0 0 1 902.095238 396.361143v434.883047A70.89981 70.89981 0 0 1 831.146667 902.095238h-282.819048l0.024381-218.112h-71.826286v218.087619L192.853333 902.095238A70.89981 70.89981 0 0 1 121.904762 831.24419V390.241524c0-24.527238 12.678095-47.299048 33.54819-60.220953l318.659048-197.485714a70.972952 70.972952 0 0 1 75.50781 0.487619zM828.952381 828.952381V397.214476L511.488 195.047619 195.047619 391.119238V828.952381h211.309714v-216.551619h212.187429v216.527238L828.952381 828.952381z + + + M782.208 883.968q-27.712 48-83.2 48H324.992q-55.424 0-83.136-48L54.784 560q-27.712-48 0-96l187.008-323.968q27.712-48 83.2-48h374.08q55.424 0 83.136 48l187.008 323.968q27.712 48 0 96l-187.008 323.968z m-55.424-32l187.008-323.968q9.28-16 0-32l-187.008-323.968q-9.28-16-27.712-16H324.928q-18.432 0-27.712 16L110.208 496q-9.28 16 0 32l187.008 323.968q9.28 16 27.712 16h374.144q18.432 0 27.712-16zM672 512a160 160 0 1 1-320 0 160 160 0 0 1 320 0z m-64 0a96 96 0 1 0-192 0 96 96 0 0 0 192 0z + + + M858.5 763.6a374 374 0 0 0-80.6-119.5 375.63 375.63 0 0 0-119.5-80.6c-54.3-26.7-114.5-40.3-175.8-40.3-61.3 0-121.5 13.6-175.8 40.3a373.6 373.6 0 0 0-119.5 80.6 375.57 375.57 0 0 0-80.6 119.5A375.55 375.55 0 0 0 136 901.8a8 8 0 0 0 8 8.2h60c4.4 0 7.9-3.5 8-7.8 2-77.2 33-149.5 87.8-204.3 56.7-56.7 132-87.9 212.2-87.9s155.5 31.2 212.2 87.9C779 752.7 810 825 812 902.2c.1 4.4 3.6 7.8 8 7.8h60a8 8 0 0 0 8-8.2c-1-47.8-10.9-94.3-29.5-138.2zM512 534c-79.5 0-144-64.5-144-144s64.5-144 144-144 144 64.5 144 144-64.5 144-144 144z + + + M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372zm0-620c-48.6 0-88 39.4-88 88s39.4 88 88 88 88-39.4 88-88-39.4-88-88-88zm0 288c-66.2 0-120 53.8-120 120 0 4.4 3.6 8 8 8h224c4.4 0 8-3.6 8-8 0-66.2-53.8-120-120-120z + + + M832 340.992l-320 312-320-312a28.8 28.8 0 0 0-20.992-8.96 28.8 28.8 0 0 0-20.992 8.96 28.8 28.8 0 0 0-8.96 20.992c0 8 2.624 14.72 8 19.968l340.992 332.032c5.952 6.016 13.312 8.96 22.016 8.96 8.64 0 16-2.944 22.016-8.96l340.992-331.008a31.232 31.232 0 0 0 8-21.504c0-8.32-3.008-15.36-8.96-20.992a29.76 29.76 0 0 0-42.048 0.448v0.064z + + M509.824 874.624c-51.3536 0-94.336-21.76-112.9984-53.632h225.9712c-18.6624 31.8464-61.6448 53.632-112.9728 53.632zM507.5968 142.464c53.2992 0 98.3296 33.7152 110.0288 80.128 97.3312 47.4624 153.088 142.976 153.088 263.936l-0.0768 22.3232c-0.3072 34.432-1.9712 72.3968-11.0592 104.2688 13.5424 8.704 26.6496 20.8128 37.5552 34.944 17.5872 22.4 27.264 46.4384 27.264 67.6352v2.0224c0 60.7744-66.6368 88.448-128.6144 88.448H319.4112c-30.8992 0-59.776-6.8608-81.3824-19.3792-27.648-16.1536-42.8032-41.0112-42.8032-70.0672v-2.048c0-36.3264 26.6496-79.36 60.5696-101.5552-11.3152-40.3712-11.3152-92.8768-11.3152-132.6592 0-118.912 54.3232-209.9968 153.088-257.8432 11.6992-46.4384 56.704-80.1536 110.0288-80.1536z m0 64c-22.7584 0-41.1136 12.416-46.976 28.544l-1.024 3.2512-7.296 28.928-26.8544 13.0304c-76.2368 36.9408-116.9664 105.4464-116.9664 200.2432l0.1024 24.576c0.128 14.08 0.4352 24.32 1.0496 35.072l0.256 4.0704c1.1776 18.5344 3.2512 34.1248 6.2208 46.5664l1.3056 5.12 12.6464 45.1328-39.1936 25.6768c-16.0768 10.496-29.5936 31.744-31.4368 45.0816l-0.2048 2.944v2.0224c0 5.632 2.56 9.8048 10.88 14.6688 9.984 5.8112 25.216 9.8048 42.6496 10.624l6.656 0.1536h376.3712c21.632 0 42.0608-4.864 54.8352-12.4416 6.9888-4.1472 9.216-6.8096 9.7024-10.3936l0.0768-1.6128v-2.0224c0-5.5808-4.3008-16.256-13.9264-28.544-5.12-6.656-11.0592-12.544-16.9984-17.0496l-4.4544-3.1232-40.0128-25.6768 13.056-45.7216c3.328-11.6992 5.6064-26.24 6.9632-43.8528l0.512-7.7312c0.9472-15.7696 1.152-29.0816 1.152-57.472 0-94.08-39.8336-165.504-110.3872-202.9824l-6.7072-3.4048-26.752-13.056-7.2704-28.8256c-4.4544-17.664-23.7056-31.7952-47.9744-31.7952z + + + M1243.428571 1024h-219.428571a73.142857 73.142857 0 0 0-146.285714 0H512a73.142857 73.142857 0 0 0-146.285714 0H146.285714a146.285714 146.285714 0 0 1-146.285714-146.285714V146.285714a146.285714 146.285714 0 0 1 146.285714-146.285714h512a146.285714 146.285714 0 0 1 146.285715 146.285714h438.857142a146.285714 146.285714 0 0 1 146.285715 146.285715v585.142857a146.285714 146.285714 0 0 1-146.285715 146.285714zM658.285714 707.072a203.922286 203.922286 0 0 0-139.629714-97.572571 142.482286 142.482286 0 0 1-45.348571-48.713143h-1.170286A92.891429 92.891429 0 0 1 425.545143 512a21.942857 21.942857 0 0 1 0.658286-3.145143A142.336 142.336 0 0 0 518.656 365.714286a119.442286 119.442286 0 1 0-232.740571 0 142.336 142.336 0 0 0 92.452571 143.140571 21.942857 21.942857 0 0 1 0.658286 3.145143 93.769143 93.769143 0 0 1-46.592 48.786286 141.092571 141.092571 0 0 1-47.323429 48.713143 204.8 204.8 0 0 0-138.971428 97.572571V804.571429h512V707.072zM1243.428571 292.571429h-438.857142v73.142857h438.857142V292.571429z m0 219.428571h-438.857142v73.142857h438.857142V512z m0 219.428571h-438.857142v73.142858h438.857142v-73.142858z + + M870 126H663.8c-17.4 0-32.9 11.9-37 29.3C614.3 208.1 567 246 512 246s-102.3-37.9-114.8-90.7c-4.1-17.4-19.5-29.3-37-29.3H154c-24.3 0-44 19.7-44 44v252c0 24.3 19.7 44 44 44h75v388c0 24.3 19.7 44 44 44h478c24.3 0 44-19.7 44-44V466h75c24.3 0 44-19.7 44-44V170c0-24.3-19.7-44-44-44z m-28 268H723v432H301V394H182V198h153.3c28.2 71.2 97.5 120 176.7 120s148.5-48.8 176.7-120H842v196z + + M880 240H704v-64c0-52.8-43.2-96-96-96H416c-52.8 0-96 43.2-96 96v64H144c-17.6 0-32 14.4-32 32s14.4 32 32 32h48v512c0 70.4 57.6 128 128 128h384c70.4 0 128-57.6 128-128V304h48c17.6 0 32-14.4 32-32s-14.4-32-32-32z m-496-64c0-17.6 14.4-32 32-32h192c17.6 0 32 14.4 32 32v64H384v-64z m384 640c0 35.2-28.8 64-64 64H320c-35.2 0-64-28.8-64-64V304h512v512zM416 432c-17.6 0-32 14.4-32 32v256c0 17.6 14.4 32 32 32s32-14.4 32-32V464c0-17.6-14.4-32-32-32z m192 0c-17.6 0-32 14.4-32 32v256c0 17.6 14.4 32 32 32s32-14.4 32-32V464c0-17.6-14.4-32-32-32z + + + Pack://application:,,,/YY.Admin;component/Resources/Icon/#iconfont + + + + pack://application:,,,/YY.Admin;component/Resources/Icon/FontAwesome/#Font Awesome 7 Free Regular + + + pack://application:,,,/YY.Admin;component/Resources/Icon/FontAwesome/#Font Awesome 7 Free Solid + + + pack://application:,,,/YY.Admin;component/Resources/Icon/FontAwesome/#Font Awesome 7 Brands + + + + \ No newline at end of file diff --git a/yy-admin-master/YY.Admin/Resources/Styles/YY.xaml b/yy-admin-master/YY.Admin/Resources/Styles/YY.xaml new file mode 100644 index 0000000..fc30201 --- /dev/null +++ b/yy-admin-master/YY.Admin/Resources/Styles/YY.xaml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + 14 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/yy-admin-master/YY.Admin/Slave.db b/yy-admin-master/YY.Admin/Slave.db new file mode 100644 index 0000000..6aeea6a Binary files /dev/null and b/yy-admin-master/YY.Admin/Slave.db differ diff --git a/yy-admin-master/YY.Admin/Subscriber/ErrorNotification.cs b/yy-admin-master/YY.Admin/Subscriber/ErrorNotification.cs new file mode 100644 index 0000000..193f19a --- /dev/null +++ b/yy-admin-master/YY.Admin/Subscriber/ErrorNotification.cs @@ -0,0 +1,46 @@ +using HandyControl.Controls; +using YY.Admin.Core.BusinessException; + +namespace YY.Admin.EventBus +{ + // 错误事件 + public class ErrorEvent : PubSubEvent + { + } + + // 错误处理服务 + public interface IErrorHandler + { + void HandleError(Exception ex); + } + + public class ErrorHandler : IErrorHandler + { + private readonly IEventAggregator _eventAggregator; + + public ErrorHandler( + IEventAggregator eventAggregator) + { + _eventAggregator = eventAggregator; + } + + public void HandleError(Exception ex) + { + if (ex is BusinessException bex) + { + // _logger.LogWarning($"业务错误: {bex.ErrorCode} - {bex.Message}"); + + // 在UI线程显示HandyControl提示 + System.Windows.Application.Current.Dispatcher.Invoke(() => + { + Growl.Error(bex.Message); + }); + } + else + { + // _logger.LogError(ex, "未处理的异常"); + _eventAggregator.GetEvent().Publish($"系统错误: {ex.Message}"); + } + } + } +} diff --git a/yy-admin-master/YY.Admin/Subscriber/SysUserEventSubscriber.cs b/yy-admin-master/YY.Admin/Subscriber/SysUserEventSubscriber.cs new file mode 100644 index 0000000..1cce8b5 --- /dev/null +++ b/yy-admin-master/YY.Admin/Subscriber/SysUserEventSubscriber.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using YY.Admin.Core; +using static YY.Admin.Core.SysUserEvents; +using Prism.Events; +namespace YY.Admin.EventBus +{ + public class SysUserEventSubscriber : IDisposable + { + private readonly IEventAggregator _eventAggregator; + private readonly ILoggerService _logger; + private readonly List _subscriptions = new(); + public SysUserEventSubscriber( + IEventAggregator eventAggregator, + ILoggerService logger) + { + _eventAggregator = eventAggregator; + _logger = logger; + SubscribeEvents(); + } + public void SubscribeEvents() + { + _eventAggregator.GetEvent().Subscribe(OnAddUser, ThreadOption.BackgroundThread); + _eventAggregator.GetEvent().Subscribe(OnUpdateUser, ThreadOption.BackgroundThread); + _eventAggregator.GetEvent().Subscribe(OnDeleteUser, ThreadOption.BackgroundThread); + _eventAggregator.GetEvent().Subscribe(OnSetUserStatus, ThreadOption.BackgroundThread); + _eventAggregator.GetEvent().Subscribe(OnChangePwd, ThreadOption.BackgroundThread); + _eventAggregator.GetEvent().Subscribe(OnResetPwd, ThreadOption.BackgroundThread); + _eventAggregator.GetEvent().Subscribe(OnUnlockUserLogin, ThreadOption.BackgroundThread); + _eventAggregator.GetEvent().Subscribe(OnRegisterUser, ThreadOption.BackgroundThread); + _eventAggregator.GetEvent().Subscribe(OnLoginUser, ThreadOption.BackgroundThread); + _eventAggregator.GetEvent().Subscribe(OnLoginOut, ThreadOption.BackgroundThread); + _eventAggregator.GetEvent().Subscribe(OnUpdateUserRole, ThreadOption.BackgroundThread); + } + + public void OnAddUser(SysUser payload) + { + try + { + _logger.Information($"添加新用户: {payload.Account}"); + } + catch (Exception ex) + { + _logger.Error($"添加用户事件处理失败: {ex.Message}", ex); + } + } + + public void OnRegisterUser(SysUser payload) + { + try + { + Task.Run(() => { + + + }); + _logger.Information($"用户注册"); + } + catch (Exception ex) + { + _logger.Error($"注册用户事件处理失败: {ex.Message}", ex); + } + } + + public void OnUpdateUser((SysUser Original, SysUser Updated) payload) + { + try + { + _logger.Information($"更新用户"); + } + catch (Exception ex) + { + _logger.Error($"更新用户事件处理失败: {ex.Message}", ex); + } + } + + public void OnDeleteUser(SysUser payload) + { + try + { + } + catch (Exception ex) + { + _logger.Error($"删除用户事件处理失败: {ex.Message}", ex); + } + } + + public void OnSetUserStatus((SysUser User, StatusEnum NewStatus) payload) + { + try + { + } + catch (Exception ex) + { + _logger.Error($"设置用户状态事件处理失败: {ex.Message}", ex); + } + } + public void OnChangePwd(SysUser payload) + { + try + { + } + catch (Exception ex) + { + _logger.Error($"设置用户状态事件处理失败: {ex.Message}", ex); + } + } + public void OnUpdateUserRole((SysUser User, List RoleIds) payload) + { + try + { + } + catch (Exception ex) + { + _logger.Error($"更新用户角色事件处理失败: {ex.Message}", ex); + } + } + + public void OnUnlockUserLogin(SysUser payload) + { + try + { + } + catch (Exception ex) + { + _logger.Error($"解除登录锁定事件处理失败: {ex.Message}", ex); + } + } + + public void OnResetPwd(SysUser payload) + { + throw new NotImplementedException(); + } + + public void OnLoginUser(SysUser payload) + { + try + { + _logger.Information($"登录成功"); + } + catch (Exception ex) + { + _logger.Error($"登录处理失败: {ex.Message}", ex); + } + } + + public void OnLoginOut(SysUser payload) + { + // 勿抛异常:Prism 事件总线上若此处抛错,可能影响同事件其它订阅者(如主窗口释放与 WS 停止) + _logger.Information($"用户登出事件: {payload?.Account ?? ""}"); + } + public void Dispose() + { + // 显式取消所有订阅 + foreach (var token in _subscriptions) + { + token.Dispose(); + } + } + } +} diff --git a/yy-admin-master/YY.Admin/Updates/version.xml b/yy-admin-master/YY.Admin/Updates/version.xml new file mode 100644 index 0000000..2f5a08e --- /dev/null +++ b/yy-admin-master/YY.Admin/Updates/version.xml @@ -0,0 +1,13 @@ + + + 1.2.0.0 + http://your-update-server.com/YourAppSetup.exe + + • 新增用户管理功能 + • 优化系统性能 + • 修复已知问题 + • 改进用户体验 + + 2025-11-26 + true + \ No newline at end of file diff --git a/yy-admin-master/YY.Admin/ViewModels/AppSettingsViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/AppSettingsViewModel.cs new file mode 100644 index 0000000..39c097a --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/AppSettingsViewModel.cs @@ -0,0 +1,257 @@ +using HandyControl.Themes; +using HandyControl.Tools; +using Mapster; +using Newtonsoft.Json; +using SqlSugar; +using System.IO; +using System.Windows; +using YY.Admin.Core; +using YY.Admin.Core.Const; +using YY.Admin.Core.EventBus; +using YY.Admin.Core.Helper; +using YY.Admin.Core.Model; +using YY.Admin.Core.Session; +using HcSkinType = HandyControl.Data.SkinType; + +namespace YY.Admin.ViewModels +{ + /// + /// 系统设置 + /// + public class AppSettingsViewModel : BindableBase + { + /// + /// 皮肤类型 + /// + private HcSkinType? _skinType; + public HcSkinType? SkinType + { + get => _skinType ??= GetDefaultSkinType(); + set + { + if (SetProperty(ref _skinType, value) && value != null) + { + if (!SyncWithSystem) + { + UpdateSkin(value.Value); + UpdateAppSettings(); + } + } + } + } + + /// + /// 皮肤是否与系统同步 + /// + private bool _syncWithSystem; + public bool SyncWithSystem + { + get => _syncWithSystem; + set + { + if (SetProperty(ref _syncWithSystem, value)) + { + RaisePropertyChanged(nameof(IsNotSyncWithSystem)); + if (value) + { + SkinType = GetSkinTypeBySystem(); + UpdateSkin(SkinType.Value); + } + UpdateAppSettings(); + } + } + } + + public bool IsNotSyncWithSystem => !SyncWithSystem; + + /// + /// 是否显示TabControl + /// + private bool _isTabControlVisible = true; + + public bool IsTabControlVisible + { + get => _isTabControlVisible; + set { + if (SetProperty(ref _isTabControlVisible, value)) + { + UpdateAppSettings(); + } + } + } + + /// + /// 获取默认皮肤类型 + /// + /// + private HcSkinType GetDefaultSkinType() + { + return SyncWithSystem ? GetSkinTypeBySystem() : (HcSkinType)Properties.AppSettings.Default.SkinType; + } + + /// + /// 根据系统主题配置获取对应皮肤类型 + /// + /// + public static HcSkinType GetSkinTypeBySystem() + { + return SystemHelper.DetermineIfInLightThemeMode() + ? HcSkinType.Default + : HcSkinType.Dark; + } + + /// + /// 从ResourceDictionary中获取皮肤类型 + /// + /// + public static HcSkinType GetSkinType() + { + var _theme = Application.Current.Resources.MergedDictionaries.OfType().FirstOrDefault(); + if (_theme != null) + { + return _theme?.Skin ?? HcSkinType.Default; + } + var skin = Application.Current.Resources.MergedDictionaries + .Where(it => it.Source.OriginalString.StartsWith("pack://application:,,,/HandyControl;component/Themes/Skin")) + .FirstOrDefault(); + if (skin != null) + { + string OriginalString = skin.Source.OriginalString; + // 4:Skin的长度 + int skinStart = OriginalString.IndexOf("Skin") + 4; + int dotIndex = OriginalString.LastIndexOf('.'); + string skinName = OriginalString.Substring(skinStart, dotIndex - skinStart); + return (HcSkinType)Enum.Parse(typeof(HcSkinType), skinName); + } + return HcSkinType.Default; + } + + /// + /// 更新皮肤 + /// + /// + public static void UpdateSkin(HcSkinType skinType) + { + // 配置方式: + var theme = Application.Current.Resources.MergedDictionaries.OfType().FirstOrDefault(); + if (theme != null) + { + //theme.Skin = skin; + theme.MergedDictionaries[0].Source = ResourceHelper.GetSkin(skinType).Source; + theme.MergedDictionaries[1].Source = ResourceHelper.GetStandaloneTheme().Source; + AddResourceDictionary(skinType); + return; + } + + // 配置方式: + var skins = Application.Current.Resources.MergedDictionaries + .Where(it => it.Source.OriginalString.StartsWith("pack://application:,,,/HandyControl;component/Themes/")) + .ToList(); + if (skins == null || skins.Count < 2) + { + return; + } + skins[0].Source = ResourceHelper.GetSkin(skinType).Source; + skins[1].Source = ResourceHelper.GetStandaloneTheme().Source; + + // 添加除HandyControl之外的资源字典 + AddResourceDictionary(skinType); + + ContainerLocator.Container.Resolve().GetEvent().Publish(skinType); + } + + /// + /// 获取文件完整路径 + /// + /// + public static string GetFilePath() + { + string filePathSuffix = string.Format(CommonConst.AppSettingsFilePath, AppSession.CurrentUser!.Account); + return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, filePathSuffix); + } + + /// + /// 更新系统设置 + /// 默认配置由MainWindowViewModel保存 + /// + /// 系统设置VM + public void UpdateAppSettings() + { + var filePath = GetFilePath(); + if (!File.Exists(filePath)) + { + return; + } + SaveAppSettings(filePath); + } + + /// + /// 保存系统设置 + /// + /// + public void SaveAppSettings(string? filePath = null) + { + filePath ??= GetFilePath(); + var directory = Path.GetDirectoryName(filePath); + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory!); + } + var appSettings = this.Adapt(); + var json = JsonConvert.SerializeObject(appSettings, Formatting.Indented); + // 保存到文件 + File.WriteAllText(filePath, json); + } + + /// + /// 添加资源字典 + /// + /// + private static void AddResourceDictionary(HcSkinType skinType) + { + // 先移除上一次主题添加的资源字典,否则会影响当前主题 + RemoveResourceDictionaryBySource("/Resources/Styles/HandyControl/Colors.xaml"); + RemoveResourceDictionaryBySource("/Resources/Styles/HandyControl/ColorsDark.xaml"); + RemoveResourceDictionaryBySource("/Resources/Styles/HandyControl/ColorsViolet.xaml"); + RemoveResourceDictionaryBySource("/Resources/Styles/HandyControl/Brushes.xaml"); + if (skinType == HcSkinType.Default) + { + Application.Current.Resources.MergedDictionaries.Add(new ResourceDictionary + { + Source = new Uri("/Resources/Styles/HandyControl/Colors.xaml", UriKind.Relative) + }); + } else if (skinType == HcSkinType.Dark) + { + Application.Current.Resources.MergedDictionaries.Add(new ResourceDictionary + { + Source = new Uri("/Resources/Styles/HandyControl/ColorsDark.xaml", UriKind.Relative) + }); + } else if (skinType == HcSkinType.Violet) + { + Application.Current.Resources.MergedDictionaries.Add(new ResourceDictionary + { + Source = new Uri("/Resources/Styles/HandyControl/ColorsViolet.xaml", UriKind.Relative) + }); + } + Application.Current.Resources.MergedDictionaries.Add(new ResourceDictionary + { + Source = new Uri("/Resources/Styles/HandyControl/Brushes.xaml", UriKind.Relative) + }); + } + + /// + /// 移除资源字典 + /// + /// + private static void RemoveResourceDictionaryBySource(string sourceEndsWith) + { + var dictToRemove = Application.Current.Resources.MergedDictionaries + .FirstOrDefault(d => d.Source?.OriginalString?.EndsWith(sourceEndsWith) == true); + + if (dictToRemove != null) + { + Application.Current.Resources.MergedDictionaries.Remove(dictToRemove); + } + } + } +} diff --git a/yy-admin-master/YY.Admin/ViewModels/Base/BaseViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/Base/BaseViewModel.cs new file mode 100644 index 0000000..262756c --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/Base/BaseViewModel.cs @@ -0,0 +1,592 @@ +using HandyControl.Data; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Threading; +using YY.Admin.Core; +using YY.Admin.Core.Const; +using YY.Admin.Core.EventBus; +using YY.Admin.Core.Session; +using YY.Admin.Services.Service.Auth; +using YY.Admin.Views; + +namespace YY.Admin.ViewModels +{ + public class BaseViewModel : BindableBase, IDestructible + { + protected bool _isLoading; + private string _title = string.Empty; + + // 添加Token检查定时器 + private static DispatcherTimer? _tokenCheckTimer; + /// + ///加载 + /// + public bool IsLoading + { + get => _isLoading; + set + { + if (SetProperty(ref _isLoading, value)) + { + // 触发一个虚拟方法,让派生类可以响应 + OnIsLoadingChanged(); + } + } + } + + public bool IsNotLoading => !IsLoading; + + /// + ///标题 + /// + public string Title + { + get => _title; + set => SetProperty(ref _title, value); + } + private readonly IDialogService _dialogService; + protected readonly IRegionManager _regionManager; + /// + /// 日志对象 + /// + + protected ILoggerService _logger; + /// + /// 依赖注入容器 + /// + protected IContainerExtension _container { get; } + /// + /// 事件汇总器,用于发布或订阅事件 + /// + protected IEventAggregator _eventAggregator; + + /// + /// 当前已登录用户信息 + /// + protected static UserContext? _userContext { get; set; } + + private SkinType _skinType; + + public SkinType SkinType { get => _skinType; set => SetProperty(ref _skinType, value); } + + private SubscriptionToken? _themeChangedEventToken; + + protected BaseViewModel(IContainerExtension container, IRegionManager regionManager) + { + _container = container; + _logger = container.Resolve(); + _eventAggregator = container.Resolve(); + this._dialogService = container.Resolve(); + this._regionManager = regionManager; + + _themeChangedEventToken = _eventAggregator.GetEvent().Subscribe(skinType => SkinType = skinType); + + } + + #region 用户操作 + // 启动定时器的方法 + public static void StartTokenCheckTimer() + { + if (_tokenCheckTimer == null) + { + _tokenCheckTimer = new DispatcherTimer + { + Interval = TimeSpan.FromMinutes(1) + }; + _tokenCheckTimer.Tick += CheckTokenExpiration; + + // 捕获全局用户输入事件 + EventManager.RegisterClassHandler(typeof(Window), + UIElement.PreviewMouseDownEvent, + new MouseButtonEventHandler(OnUserActivity)); + EventManager.RegisterClassHandler(typeof(Window), + UIElement.PreviewKeyDownEvent, + new KeyEventHandler(OnUserActivity)); + } + + if (!_tokenCheckTimer.IsEnabled) + { + _tokenCheckTimer.Start(); + } + } + + // 停止定时器的方法 + public static void StopTokenCheckTimer() + { + if (_tokenCheckTimer != null && _tokenCheckTimer.IsEnabled) + { + _tokenCheckTimer.Stop(); + } + } + + private static void OnUserActivity(object sender, EventArgs e) + { + var authService = ContainerLocator.Current.Resolve(); + authService.RefreshToken(UserContext?.Token?.AccessToken); + } + + + /// + ///定时器检查方法 + /// + /// + /// + private static void CheckTokenExpiration(object? sender, EventArgs e) + { + // 确保在主线程执行 + Application.Current.Dispatcher.Invoke(() => + { + if (UserContext == null || UserContext.Token == null) + { + // 如果没有用户信息,停止定时器 + StopTokenCheckTimer(); + return; + } + + var authService = ContainerLocator.Current.Resolve(); + if (!authService.ValidateToken(UserContext.Token.AccessToken)) + { + // 停止定时器防止重复触发 + StopTokenCheckTimer(); + + // 显示过期提示 + Application.Current.Dispatcher.Invoke(() => + { + var currentWindow = Application.Current.MainWindow; + if (currentWindow != null) + { + var viewModel = currentWindow.DataContext as BaseViewModel; + viewModel?.ForceLogout("您的登录已过期,请重新登录"); + } + }); + } + }); + } + /// + /// 强制退出方法 + /// + /// + public async void ForceLogout(string message) + { + // 先显示对话框 + await ShowAlertAsync(message); + + // 发布登出事件 + _eventAggregator.GetEvent().Publish(AppSession.CurrentUser!); + + Logout(); + } + + /// + /// 登出 + /// + public void Logout() + { + // 停止定时器 + StopTokenCheckTimer(); + // 清除用户上下文 + ClearUserContext(); + // 再执行退出操作 + var authService = _container.Resolve(); + authService.LogoutAsync(); + + // 当前窗口 + var mainWindow = Application.Current.MainWindow; + // 跳转到登录页 + var loginWindow = _container.Resolve(); + Application.Current.MainWindow = loginWindow; + loginWindow.Show(); + mainWindow.Close(); + + // 移除所有区域 + RemoveAllRegion(); + } + + public static UserContext? UserContext + { + get => _userContext; + private set + { + if (_userContext != value) + { + _userContext = value; + // 通知静态属性变化(需要额外实现) + StaticPropertyChanged?.Invoke(null, new PropertyChangedEventArgs(nameof(UserContext))); + } + } + } + // 静态属性变更通知事件 + public static event EventHandler? StaticPropertyChanged; + /// + /// 设置用户上下文 + /// + /// + /// + public static void SetUserContext(SysUser user, UserToken token) + { + UserContext = new UserContext + { + UserId = user.Id, + TenantId = user.TenantId!.Value, + Account = user.Account, + AccountType = user.AccountType, + RealName = user.RealName, + IsSuperAdmin = user.AccountType == AccountTypeEnum.SuperAdmin, + OrgId = user.OrgId, + Token = token + }; + } + /// + /// 清除用户上下文 + /// + public static void ClearUserContext() + { + UserContext = null; + } + #endregion + + #region 导航 + /// + /// 导航到指定Page + /// + /// 区域名称 + /// 目标Page名称 + /// 导航回调函数 + protected void RequestNavigate(string regionName, string target, Action navigationCallback = null) + { + IRegion region = _regionManager.Regions[regionName]; + if (region == null) return; + region.RemoveAll(); + if (navigationCallback != null) + region.RequestNavigate(target, navigationCallback); + else + region.RequestNavigate(target); + } + + protected void SetRegionManager(DependencyObject target) + { + RegionManager.SetRegionManager(target, _regionManager); + } + /// + /// 安全导航到指定视图 + /// + protected void SafeNavigate(string regionName, string targetView, NavigationParameters parameters = null) + { + ExecuteWithExceptionHandling(() => + { + // 导航页面 + _regionManager.RequestNavigate(regionName, targetView, parameters); + }); + } + protected void ExecuteWithExceptionHandling(Action action) + { + try + { + action(); + } + catch (Exception ex) + { + HandleException(ex); + } + } + + private void HandleException(Exception ex) + { + LogError("操作发生异常", ex); + // ShowErrorDialog($"操作异常: {ex.Message}"); + } + #endregion + + #region 弹出框 + /// + /// 弹框提示 + /// + /// 消息内容 + /// 回调函数 + protected void Alert(string message, Action? callback = null) + { + _dialogService.ShowDialog("AlertDialog", new DialogParameters($"message={message}"), callback); + } + protected Task ShowAlertAsync(string message) + { + var tcs = new TaskCompletionSource(); + Alert(message,result => tcs.SetResult(true)); + return tcs.Task; + } + /// + /// 弹出消息提示框,1秒钟自动关闭 + /// + /// 消息内容 + /// 消息类型 + /// 回调函数 + protected void AlertPopup(string message, MessageTypeEnum messageType = MessageTypeEnum.Success, Action callback = null) + { + switch (messageType) + { + case MessageTypeEnum.Success: + _dialogService.ShowDialog("SuccessDialog", new DialogParameters($"message={message}"), callback); + break; + case MessageTypeEnum.Error: + _dialogService.ShowDialog("ErrorDialog", new DialogParameters($"message={message}"), callback); + break; + case MessageTypeEnum.Warning: + _dialogService.ShowDialog("WarningDialog", new DialogParameters($"message={message}"), callback); + break; + default: + _dialogService.ShowDialog("SuccessDialog", new DialogParameters($"message={message}"), callback); + break; + } + } + + /// + /// 确认框提示 + /// + /// 确认框消息 + /// 回调函数 + protected void Confirm(string message, Action? callback = null) + { + _dialogService.ShowDialog("ConfirmDialog", new DialogParameters($"message={message}"), callback); + } + /// + /// 异步确认框 + /// + /// 确认消息 + /// 用户是否确认 + protected Task ConfirmAsync(string message) + { + var tcs = new TaskCompletionSource(); + + Confirm(message, result => + { + // 从 IDialogResult 中提取用户选择 + var userConfirmed = result.Result == ButtonResult.Yes; + tcs.SetResult(userConfirmed); + }); + + return tcs.Task; + } + #endregion + + #region 日志功能封装 + protected void LogInfo(string message, [CallerMemberName] string caller = "") + => _logger.Information($"[{GetType().Name}.{caller}] {message}"); + + protected void LogWarning(string message, [CallerMemberName] string caller = "") + => _logger.Warning($"[{GetType().Name}.{caller}] {message}"); + + protected void LogError(string message, Exception ex = null, [CallerMemberName] string caller = "") + => _logger.Error($"[{GetType().Name}.{caller}] {message}", ex); + #endregion + + /// + /// 异常处理封装 + /// + /// + /// + /// + protected async Task ExecuteAsync(Func asyncAction, Action? onFinally = null) + { + try + { + IsLoading = true; + await asyncAction(); + } + catch (Exception ex) + { + HandleException(ex); + } + finally + { + IsLoading = false; + onFinally?.Invoke(); + } + } + + /// + /// 窗口管理 + /// + /// + protected void SwitchMainWindow() where T : Window + { + ExecuteWithExceptionHandling(() => + { + var currentWindow = Application.Current.MainWindow; + var newWindow = _container.Resolve(); + + Application.Current.MainWindow?.Hide(); + Application.Current.MainWindow = newWindow; + newWindow.Show(); + + currentWindow?.Close(); + }); + } + + /// + /// 资源管理 + /// + /// + /// + /// + /// + protected T GetResource(string resourceKey) + { + if (Application.Current.TryFindResource(resourceKey) is T resource) + { + return resource; + } + throw new ResourceNotFoundException($"资源未找到: {resourceKey}"); + } + + /// + /// 移除区域,包括清空区域内的内容,导航日志 + /// + /// 区域名称 + protected void RemoveRegion(string regionName) + { + if (string.IsNullOrEmpty(regionName)) + { + return; + } + if (!_regionManager.Regions.ContainsRegionWithName(regionName)) + { + return; + } + + var region = _regionManager.Regions[regionName]; + RemoveRegion(region); + } + + /// + /// 移除区域,包括清空区域内的内容,导航日志 + /// + /// + protected void RemoveRegion(IRegion region) + { + if (region == null) + { + return; + } + // 清空区域内容 + region.RemoveAll(); + + // 清空导航历史 + region.NavigationService?.Journal?.Clear(); + + // 从区域管理器中移除区域 + _regionManager.Regions.Remove(region.Name); + + _logger.Debug($"移除区域{region.Name}"); + } + + /// + /// 移除所有区域 + /// + protected void RemoveAllRegion() + { + foreach (var region in _regionManager.Regions.ToList()) + { + RemoveRegion(region); + } + _logger.Debug($"移除所有区域"); + } + + /// + /// 移除区域视图 + /// + /// 区域名称 + /// 视图名称 + protected void RemoveView(string regionName, string? viewName) + { + if (string.IsNullOrEmpty(regionName) || string.IsNullOrEmpty(viewName)) + { + return; + } + if (!_regionManager.Regions.ContainsRegionWithName(regionName)) + { + return; + } + + var region = _regionManager.Regions[regionName]; + // 查找并移除指定的视图 + var viewToRemove = region.Views.FirstOrDefault(v => + { + // 根据视图名称或类型来匹配 + var viewType = v.GetType(); + return viewType.Name == viewName || viewType.Name == viewName + "View"; + }); + + if (viewToRemove != null) + { + region.Remove(viewToRemove); + _logger.Debug($"从区域{regionName}移除视图{viewName}"); + } + } + + /// + /// 移除区域ContentRegion视图 + /// + /// + protected void RemoveContentRegionView(string? viewName) + { + RemoveView(CommonConst.ContentRegion, viewName); + } + + /// + /// 移除区域所有视图 + /// + /// 区域名称 + protected void RemoveAllView(string regionName) + { + if (string.IsNullOrEmpty(regionName)) + { + return; + } + if (!_regionManager.Regions.ContainsRegionWithName(regionName)) + { + return; + } + var region = _regionManager.Regions[regionName]; + region.RemoveAll(); + _logger.Debug($"移除区域{regionName}所有视图"); + } + + /// + /// 当 IsLoading 变化时的回调方法 + /// + protected virtual void OnIsLoadingChanged() + { + RaisePropertyChanged(nameof(IsNotLoading)); + } + + /// + /// 清理资源 + /// + protected virtual void CleanUp() + { + } + + /// + /// 清空资源 + /// 执行时机:从Region移除View 或 导航到另一个View时 + /// + public void Destroy() + { + if (_themeChangedEventToken != null) + { + _eventAggregator.GetEvent().Unsubscribe(_themeChangedEventToken); + _themeChangedEventToken = null; + } + // 清理派生类的资源 + CleanUp(); + } + } + + // 自定义异常类型 + public class ResourceNotFoundException : Exception + { + public ResourceNotFoundException(string message) : base(message) { } + } +} diff --git a/yy-admin-master/YY.Admin/ViewModels/Control/MenuTreeViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/Control/MenuTreeViewModel.cs new file mode 100644 index 0000000..f24b89a --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/Control/MenuTreeViewModel.cs @@ -0,0 +1,376 @@ +using System.Collections.ObjectModel; +using System.Windows; +using System.Windows.Threading; +using YY.Admin.Core; +using YY.Admin.Core.Util; +using YY.Admin.Event; +using YY.Admin.Module; +using YY.Admin.Services; +using YY.Admin.Services.Service.Menu; + +namespace YY.Admin.ViewModels.Control +{ + /// + /// 菜单选项 + /// + public class MenuItem : TabSource + { + public MenuItem? Parent { get; set; } // 父节点引用 + public ObservableCollection Children { get; set; } = []; + + + private bool _isExpanded; + + public bool IsExpanded + { + get => _isExpanded; + set => SetProperty(ref _isExpanded, value); + } + } + + public class MenuTreeViewModel : BaseViewModel + { + private static readonly Dictionary RouteToViewMap = new(StringComparer.OrdinalIgnoreCase) + { + // 已实现页面:仪表盘 + ["DashboardView"] = "DashboardView", + ["/dashboard"] = "DashboardView", + ["/dashboard/index"] = "DashboardView", + ["/home/index"] = "DashboardView", + ["dashboard"] = "DashboardView", + ["home"] = "DashboardView", + + // 已实现页面:账号管理 + ["UserManagementView"] = "UserManagementView", + ["/system/user"] = "UserManagementView", + ["/system/user/index"] = "UserManagementView", + ["sysUser"] = "UserManagementView", + + // 已实现页面:数据字典 + ["DataDictionaryManagementView"] = "DataDictionaryManagementView", + ["/system/dict"] = "DataDictionaryManagementView", + ["/system/dict/index"] = "DataDictionaryManagementView", + ["/platform/dict"] = "DataDictionaryManagementView", + ["sysDict"] = "DataDictionaryManagementView", + + // 已实现页面:角色管理 + ["RoleManagementView"] = "RoleManagementView", + ["/system/role"] = "RoleManagementView", + ["/system/role/index"] = "RoleManagementView", + ["sysRole"] = "RoleManagementView", + + // 已实现页面:租户管理 + ["TenantManagementView"] = "TenantManagementView", + ["/system/tenant"] = "TenantManagementView", + ["/system/tenant/index"] = "TenantManagementView", + ["/platform/tenant"] = "TenantManagementView", + ["sysTenant"] = "TenantManagementView" + }; + + private MenuItem? _selectedMenuItem; + + public MenuItem? SelectedMenuItem + { + get => _selectedMenuItem; + set => SetProperty(ref _selectedMenuItem, value); + } + + private readonly ISysMenuService _sysMenuService; + + public ObservableCollection MenuItems { get; } = []; + + public DelegateCommand NavigateCommand { get; } + + private SubscriptionToken? tabSelectedToken; + private SubscriptionToken? tabClosedToken; + + public MenuTreeViewModel( + ISysMenuService sysMenuService, + IContainerExtension _container, + IRegionManager regionManager) : base(_container, regionManager) + { + _sysMenuService = sysMenuService; + + // 异步初始化菜单 + LoadMenuAsync(); + + NavigateCommand = new DelegateCommand(OpenOrActivateTab); + + // 订阅事件 + tabSelectedToken = _eventAggregator.GetEvent().Subscribe(OnTabSelected); + tabClosedToken = _eventAggregator.GetEvent().Subscribe(OnTabClosed); + } + + public void OpenOrActivateTab(MenuItem menuItem) + { + // 发布事件 + _eventAggregator.GetEvent().Publish(menuItem); + } + + private void OnTabSelected(TabItemModel tab) + { + if (tab.TabSource is MenuItem menuItem) + { + // 取消上一个选中菜单选中状态 + SelectedMenuItem?.IsSelected = false; + + SelectedMenuItem = menuItem; + + // 设置菜单选中 + SelectedMenuItem.IsSelected = true; + + // 展开父节点 + ToggleParents(SelectedMenuItem, true); + } + } + + private void OnTabClosed(TabItemModel tab) + { + if (tab.TabSource is MenuItem menuItem) + { + // 折叠父节点 + ToggleParents(menuItem, false); + + // 取消菜单选中 + menuItem?.IsSelected = false; + } + } + + /// + /// 异步加载菜单【后续使用缓存以及权限管理】 + /// + private async void LoadMenuAsync() + { + try + { + // 异步获取菜单数据 + var menuTree = await _sysMenuService.GetLoginMenuTree(); + // 转换菜单数据 + ConvertMenuTreeToViewModel(menuTree); + // 默认导航 + ScheduleDefaultNavigation(); + } + catch (Exception ex) + { + _logger.Error($"菜单加载失败: {ex.Message}", ex); + // 显示错误菜单项 + MenuItems.Add(new MenuItem + { + Name = "菜单加载失败", + Icon = "ErrorOutline", + ViewName = "ErrorView", + Children = { new MenuItem { Name = "点击重试", Icon = "Refresh" } } + }); + } + } + + /// + /// 将服务层菜单树转换为视图模型 + /// + private void ConvertMenuTreeToViewModel(List menuTree) + { + // 过滤并排序菜单项:只包含目录和菜单类型,排除按钮类型,并按排序号排序 + var rootMenus = menuTree + .Where(m => m.Type == MenuTypeEnum.Dir || m.Type == MenuTypeEnum.Menu) + .Where(m => m.Status == StatusEnum.Enable) // 只包含启用状态的菜单 + .Where(m => !(m?.IsHide ?? false)) // 排除隐藏的菜单 + .OrderBy(m => m.OrderNo) + .ToList(); + // 递归转换菜单项 + void ConvertMenu(MenuOutput source, MenuItem target, MenuItem? parent = null) + { + target.Name = source?.Title ?? source?.Name; + target.Icon = ConvertHtmlEntityToUnicode(source?.Icon ?? ""); + target.ViewName = ResolveViewName(source); // 将菜单路由映射到已注册的WPF视图 + target.Parent = parent; // 设置父节点 + + // 添加子菜单(如果有) + if (source.Children != null && source.Children.Any()) + { + // 过滤并排序子菜单 + var childMenus = source.Children + .Where(c => c.Type == MenuTypeEnum.Dir || c.Type == MenuTypeEnum.Menu) // 子菜单支持目录和菜单两种类型 + .Where(c => c.Status == StatusEnum.Enable) + .Where(c => !(c?.IsHide ?? false)) + .OrderBy(c => c.OrderNo) + .ToList(); + + foreach (var child in childMenus) + { + var childItem = new MenuItem(); + ConvertMenu(child, childItem, target); + target.Children.Add(childItem); + } + } + } + // 处理每个根菜单 + foreach (var root in rootMenus) + { + var rootItem = new MenuItem(); + ConvertMenu(root, rootItem); + + // 如果根菜单是目录但没有子菜单,则不显示 + if (root.Type == MenuTypeEnum.Dir && !rootItem.Children.Any()) + continue; + + MenuItems.Add(rootItem); + } + } + + /// + /// 解析菜单对应的视图名称 + /// + private string? ResolveViewName(MenuOutput? menu) + { + if (menu == null) + return null; + + // 目录节点不参与内容区导航 + if (menu.Type == MenuTypeEnum.Dir) + return null; + + // 依次尝试 Path / Component / Name / Title,兼容不同来源的菜单数据 + var candidates = new[] + { + menu.Path, + menu.Component, + menu.Name, + menu.Title + }; + + foreach (var candidate in candidates) + { + if (string.IsNullOrWhiteSpace(candidate)) + continue; + + if (RouteToViewMap.TryGetValue(candidate.Trim(), out var viewName)) + return viewName; + } + + // 保留原始Path,若未注册将统一展示NotFoundView + return menu.Path; + } + + private string ConvertHtmlEntityToUnicode(string htmlEntity) + { + if (string.IsNullOrEmpty(htmlEntity)) + return "\ue7c6"; // 默认图标 + + return StringUtil.ConvertHtmlEntityToUnicode(htmlEntity); + } + + /// + /// 安排默认导航 + /// + private void ScheduleDefaultNavigation() + { + Application.Current.Dispatcher.BeginInvoke(new Action(() => + { + try + { + // 默认菜单 + var defaultMenuItem = GetFirstLeaf(MenuItems.FirstOrDefault()); + if (defaultMenuItem != null) + { + // Tab不允许关闭 + defaultMenuItem.IsClosable = false; + + // 导航菜单 + OpenOrActivateTab(defaultMenuItem); + } + } + catch (Exception ex) + { + _logger.Error($"默认导航失败: {ex.Message}", ex); + } + }), DispatcherPriority.ApplicationIdle); + } + + private MenuItem? GetFirstLeaf(MenuItem? menu) + { + if (menu == null) + return null; + + if (menu.Children == null || menu.Children.Count == 0) + return menu; // 自己就是叶子节点 + + // 递归向下找第一个叶子 + return GetFirstLeaf(menu.Children.FirstOrDefault()); + } + + public void ToggleParents(MenuItem? item, bool IsExpanded) + { + if (item == null) + { + return; + } + var parent = item.Parent; + while (parent != null) + { + parent.IsExpanded = IsExpanded; // 展开父节点 + if (!IsExpanded) + { + parent.IsSelected = IsExpanded; + } + parent = parent.Parent; + } + } + + /// + /// 清空资源 + /// + protected override void CleanUp() + { + if (tabSelectedToken != null) + { + _eventAggregator + .GetEvent() + .Unsubscribe(tabSelectedToken); + tabSelectedToken = null; + } + + if (tabClosedToken != null) + { + _eventAggregator + .GetEvent() + .Unsubscribe(tabClosedToken); + tabClosedToken = null; + } + } + + /// + /// 根据ViewName同步选中对应的菜单项 + /// + //private MenuItem? GetSelectedMenuItem(string? viewName) + //{ + // if (string.IsNullOrEmpty(viewName)) + // return null; + + // // 递归查找匹配的菜单项 + // return FindMenuItemByViewName(MenuItems, viewName); + + //} + + /// + /// 递归查找匹配ViewName的菜单项 + /// + //private MenuItem? FindMenuItemByViewName(ObservableCollection menuItems, string viewName) + //{ + // foreach (var menuItem in menuItems) + // { + // // 如果当前菜单项匹配 + // if (menuItem.ViewName == viewName) + // return menuItem; + + // // 递归查找子菜单 + // if (menuItem.Children?.Count > 0) + // { + // var found = FindMenuItemByViewName(menuItem.Children, viewName); + // if (found != null) + // return found; + // } + // } + // return null; + //} + } +} diff --git a/yy-admin-master/YY.Admin/ViewModels/Control/PaginationDataGridViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/Control/PaginationDataGridViewModel.cs new file mode 100644 index 0000000..e3c178d --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/Control/PaginationDataGridViewModel.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.ViewModels.Control +{ + public class PaginationDataGridViewModel : BindableBase + { + private int _pageIndex = 1; + private int _dataCountPerPage = 10; + private int _totalCount; + private int _maxPageCount; + private ObservableCollection _data; + //private string _pageInfo; + + public int MyProperty { get; private set; } + + public int PageIndex + { + get => _pageIndex; + set => SetProperty(ref _pageIndex, value); + } + + public int DataCountPerPage + { + get => _dataCountPerPage; + set => SetProperty(ref _dataCountPerPage, value); + } + + public int TotalCount + { + get => _totalCount; + set => SetProperty(ref _totalCount, value); + } + + public int MaxPageCount + { + get => _maxPageCount; + set => SetProperty(ref _maxPageCount, value); + } + + public ObservableCollection Data + { + get => _data; + set => SetProperty(ref _data, value); + } + + //public string PageInfo + //{ + // get => _pageInfo; + // set => SetProperty(ref _pageInfo, value); + //} + + // 页面大小选项 + public Dictionary PageSizes { get; } = new Dictionary + { + { 10, "10条/页" }, + { 20, "20条/页" }, + { 30, "30条/页" }, + { 40, "40条/页" }, + { 50, "50条/页" }, + { 100, "100条/页" } + }; + + public DelegateCommand PageUpdatedCmd { get; private set; } + + public DelegateCommand PageSizeUpdatedCmd { get; private set; } + + private Func data, int totalCount)>> _fetchData; + + public PaginationDataGridViewModel(Func data, int totalCount)>> fetchData) + { + _fetchData = fetchData; + PageUpdatedCmd = new DelegateCommand(async () => await LoadDataAsync()); + PageSizeUpdatedCmd = new DelegateCommand(async () => await LoadDataAsync()); + + // 初始化时加载数据 + //_ = LoadData(); + } + + public async Task LoadDataAsync() + { + try + { + var (data, totalCount) = await _fetchData(); + Data = new ObservableCollection(data); + TotalCount = totalCount; + // 通知分页总数变化 + //RaisePropertyChanged(nameof(MaxPageCount)); + MaxPageCount = totalCount == 0 ? 1 : (int)Math.Ceiling((double)totalCount / DataCountPerPage); + // 更新分页信息 + //PageInfo = $"共 {_totalCount} 条"; + } catch (OperationCanceledException) + { + // 查询被取消,保持表格不变 + return; + } + } + } + + +} diff --git a/yy-admin-master/YY.Admin/ViewModels/DashboardViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/DashboardViewModel.cs new file mode 100644 index 0000000..041ecbb --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/DashboardViewModel.cs @@ -0,0 +1,172 @@ +using LiveChartsCore; +using LiveChartsCore.Drawing; +using LiveChartsCore.Kernel; +using System.Collections.ObjectModel; +using System.Windows.Input; +using YY.Admin.Core; +using YY.Admin.Services.Service.User; + +namespace YY.Admin.ViewModels +{ + public class StatisticCard + { + public string Title { get; set; } = string.Empty; + public string Value { get; set; } = string.Empty; + public string Icon { get; set; } = string.Empty; + public string Color { get; set; } = string.Empty; + public string Trend { get; set; } = string.Empty; + } + + public class RecentActivity + { + public string Title { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public DateTime Time { get; set; } + public string Icon { get; set; } = string.Empty; + } + + public class DashboardViewModel : BaseViewModel + { + private readonly ISysUserService _userService; + + public float[] Values1 { get; set; } = FetchVales(0); + public float[] Values2 { get; set; } = FetchVales(-0.15f); + + public ICommand PointMeasuredCommand { get; } + + public DashboardViewModel( + ISysUserService userService, + IContainerExtension _container, + IRegionManager regionManager + ) : base(_container, regionManager) + { + // _eventAggregator.GetEvent().Subscribe(ApplyTheme); + _userService = userService; + + PointMeasuredCommand = new DelegateCommand(OnPointMeasured); + + _ = LoadDashboardDataAsync(); + + } + + public ObservableCollection StatisticCards { get; } = new(); + public ObservableCollection RecentActivities { get; } = new(); + + private async Task LoadDashboardDataAsync() + { + IsLoading = true; + + try + { + // 加载统计数据 + var users = await _userService.GetUsersAsync(); + + StatisticCards.Clear(); + StatisticCards.Add(new StatisticCard + { + Title = "总用户数", + Value = users.Count.ToString(), + Icon = "UserOutlined", + Color = "#1890ff", + Trend = "+12%" + }); + StatisticCards.Add(new StatisticCard + { + Title = "活跃用户", + Value = users.Count(u => u.Status== StatusEnum.Enable).ToString(), + Icon = "UserCheckOutlined", + Color = "#52c41a", + Trend = "+8%" + }); + StatisticCards.Add(new StatisticCard + { + Title = "今日访问", + Value = "1,234", + Icon = "EyeOutlined", + Color = "#faad14", + Trend = "+5%" + }); + StatisticCards.Add(new StatisticCard + { + Title = "系统消息", + Value = "56", + Icon = "MessageOutlined", + Color = "#f5222d", + Trend = "+2" + }); + + // 加载最近活动 + RecentActivities.Clear(); + RecentActivities.Add(new RecentActivity + { + Title = "用户登录", + Description = "管理员 admin 登录系统", + Time = DateTime.Now.AddMinutes(-5), + Icon = "LoginOutlined" + }); + RecentActivities.Add(new RecentActivity + { + Title = "数据更新", + Description = "用户数据已同步更新", + Time = DateTime.Now.AddMinutes(-15), + Icon = "SyncOutlined" + }); + RecentActivities.Add(new RecentActivity + { + Title = "系统备份", + Description = "数据库备份已完成", + Time = DateTime.Now.AddHours(-2), + Icon = "DatabaseOutlined" + }); + } + finally + { + IsLoading = false; + } + } + + private static float[] FetchVales(float offset) + { + var values = new List(); + + // the EasingFunctions.BounceInOut, is just + // a function that looks nice! + + var fx = EasingFunctions.BounceInOut; + var x = 0f; + + while (x <= 1) + { + values.Add(fx(x + offset)); + x += 0.025f; + } + + return [.. values]; + } + + private void OnPointMeasured(ChartPoint point) + { + // each point will have a different delay depending on its index + var index = point.Context.Entity.MetaData!.EntityIndex; // the index of the point in the data source + var delay = index / (float)Values1.Length; + + // the animation takes a function, that represents the normalized progress of the animation + // the parameter is the normalized time of the animation, it goes from 0 to 1 + // the function must return a value from 0 to 1, where 0 is the initial state + // and 1 is the final state + var duration = TimeSpan.FromSeconds(3); + var animation = new Animation(t => DelayedEase(t, delay), duration); + + point.Context.Visual?.SetTransition(animation); + } + + private static float DelayedEase(float t, float delay) + { + if (t <= delay) return 0f; + + var remappedT = (t - delay) / (1f - delay); + var baseEasing = EasingFunctions.BuildCustomElasticOut(1.5f, 0.60f); + return baseEasing(Math.Clamp(remappedT, 0f, 1f)); + } + } +} diff --git a/yy-admin-master/YY.Admin/ViewModels/Dialogs/AlertDialogViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/Dialogs/AlertDialogViewModel.cs new file mode 100644 index 0000000..c06f922 --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/Dialogs/AlertDialogViewModel.cs @@ -0,0 +1,42 @@ +using Prism.Dialogs; +using Prism.Mvvm; +using Prism.Commands; +using System; + +namespace YY.Admin.ViewModels.Dialogs +{ + public class AlertDialogViewModel : BindableBase, IDialogAware + { + private string _message; + public string Message + { + get => _message; + set => SetProperty(ref _message, value); + } + + public DelegateCommand CloseCommand { get; } + + public AlertDialogViewModel() + { + CloseCommand = new DelegateCommand(() => + { + // 调用 RequestClose 属性触发关闭 + RequestClose.Invoke(new DialogResult(ButtonResult.OK)); + }); + } + + public bool CanCloseDialog() => true; + + public void OnDialogOpened(IDialogParameters parameters) + { + Message = parameters.GetValue("message"); + } + + public void OnDialogClosed() { } + + public string Title => "提示"; + + // 这里实现的是属性,不是 event + public DialogCloseListener RequestClose { get; private set; } + } +} diff --git a/yy-admin-master/YY.Admin/ViewModels/Dialogs/ConfirmDialogViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/Dialogs/ConfirmDialogViewModel.cs new file mode 100644 index 0000000..3ead98e --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/Dialogs/ConfirmDialogViewModel.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace YY.Admin.ViewModels.Dialogs +{ + public class ConfirmDialogViewModel : BindableBase, IDialogAware + { + public string Title => "确认"; + + private string _message; + public string Message + { + get => _message; + set => SetProperty(ref _message, value); + } + + // 这是 Prism.Dialogs 版本的 RequestClose + public DialogCloseListener RequestClose { get; private set; } + + public DelegateCommand YesCommand { get; } + public DelegateCommand NoCommand { get; } + + public ConfirmDialogViewModel() + { + YesCommand = new DelegateCommand(() => CloseDialog(ButtonResult.Yes)); + NoCommand = new DelegateCommand(() => CloseDialog(ButtonResult.No)); + } + + private void CloseDialog(ButtonResult result) + { + // 触发关闭 + RequestClose.Invoke(new DialogResult(result)); + } + + public bool CanCloseDialog() => true; + + public void OnDialogOpened(IDialogParameters parameters) + { + Message = parameters.GetValue("message"); + } + + public void OnDialogClosed() { } + } +} diff --git a/yy-admin-master/YY.Admin/ViewModels/Dialogs/ErrorDialogViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/Dialogs/ErrorDialogViewModel.cs new file mode 100644 index 0000000..26ba3d0 --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/Dialogs/ErrorDialogViewModel.cs @@ -0,0 +1,45 @@ +using Prism.Dialogs; +using Prism.Mvvm; +using System; +using System.Windows.Threading; + +namespace YY.Admin.ViewModels.Dialogs +{ + public class ErrorDialogViewModel : BindableBase, IDialogAware + { + public string Title => "错误"; + + private string _message; + public string Message + { + get => _message; + set => SetProperty(ref _message, value); + } + + // 用属性来实现,而不是 event + public DialogCloseListener RequestClose { get; private set; } + + public ErrorDialogViewModel() + { + // 自动关闭定时器(2秒) + var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) }; + timer.Tick += (s, e) => + { + timer.Stop(); + RequestClose.Invoke(new DialogResult(ButtonResult.OK)); + }; + timer.Start(); + } + + public bool CanCloseDialog() => true; + + public void OnDialogOpened(IDialogParameters parameters) + { + Message = parameters.GetValue("message"); + } + + public void OnDialogClosed() + { + } + } +} diff --git a/yy-admin-master/YY.Admin/ViewModels/Dialogs/ServerSettingsDialogViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/Dialogs/ServerSettingsDialogViewModel.cs new file mode 100644 index 0000000..1254a7b --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/Dialogs/ServerSettingsDialogViewModel.cs @@ -0,0 +1,115 @@ +using YY.Admin.Helper; + +namespace YY.Admin.ViewModels.Dialogs +{ + public class ServerSettingsDialogViewModel : BindableBase, IDialogAware + { + private const string DefaultWebSocketPath = "/websocket/scada-sync"; + private string _loadedWebSocketUrl = string.Empty; + private string _loadedAutoWebSocketUrl = string.Empty; + public string Title => "服务器设置"; + + private string _ip = "127.0.0.1"; + public string Ip + { + get => _ip; + set => SetProperty(ref _ip, value); + } + + private int _port = 8080; + public int Port + { + get => _port; + set => SetProperty(ref _port, value); + } + + private string _webSocketUrl = string.Empty; + public string WebSocketUrl + { + get => _webSocketUrl; + set => SetProperty(ref _webSocketUrl, value); + } + + private string _basePath = "/jeecg-boot"; + public string BasePath + { + get => _basePath; + set => SetProperty(ref _basePath, value); + } + + private string _errorMessage = string.Empty; + public string ErrorMessage + { + get => _errorMessage; + set => SetProperty(ref _errorMessage, value); + } + + public DelegateCommand SaveCommand { get; } + public DelegateCommand CancelCommand { get; } + public DialogCloseListener RequestClose { get; private set; } + + public ServerSettingsDialogViewModel() + { + SaveCommand = new DelegateCommand(Save); + CancelCommand = new DelegateCommand(() => RequestClose.Invoke(new DialogResult(ButtonResult.Cancel))); + } + + public bool CanCloseDialog() => true; + public void OnDialogClosed() { } + + public void OnDialogOpened(IDialogParameters parameters) + { + var settings = ServerSettingsStore.Load(); + _loadedAutoWebSocketUrl = ServerSettingsStore.BuildDefaultWebSocketUrl(settings.BaseScheme, settings.Ip, settings.Port, settings.BasePath, settings.WebSocketPath); + Ip = settings.Ip; + Port = settings.Port; + WebSocketUrl = string.IsNullOrWhiteSpace(settings.WebSocketUrl) + ? _loadedAutoWebSocketUrl + : settings.WebSocketUrl; + _loadedWebSocketUrl = WebSocketUrl; + BasePath = string.IsNullOrWhiteSpace(settings.BasePath) ? "/jeecg-boot" : settings.BasePath; + ErrorMessage = string.Empty; + } + + private void Save() + { + ErrorMessage = string.Empty; + if (string.IsNullOrWhiteSpace(Ip)) + { + ErrorMessage = "IP 不能为空"; + return; + } + if (Port <= 0 || Port > 65535) + { + ErrorMessage = "端口号必须在 1-65535 之间"; + return; + } + + try + { + var basePath = BasePath?.Trim() ?? "/jeecg-boot"; + var shouldAutoRebuildWs = string.IsNullOrWhiteSpace(WebSocketUrl) + || string.Equals(WebSocketUrl.Trim(), _loadedAutoWebSocketUrl, StringComparison.OrdinalIgnoreCase) + || string.Equals(WebSocketUrl.Trim(), _loadedWebSocketUrl, StringComparison.OrdinalIgnoreCase); + var finalWsUrl = shouldAutoRebuildWs + ? ServerSettingsStore.BuildDefaultWebSocketUrl("http", Ip.Trim(), Port, basePath, DefaultWebSocketPath) + : WebSocketUrl.Trim(); + + ServerSettingsStore.Save(new ServerSettingsStore.ServerSettingsModel + { + Ip = Ip.Trim(), + Port = Port, + BaseScheme = "http", + BasePath = basePath, + WebSocketUrl = finalWsUrl, + WebSocketPath = DefaultWebSocketPath + }); + RequestClose.Invoke(new DialogResult(ButtonResult.OK)); + } + catch (Exception ex) + { + ErrorMessage = $"保存失败:{ex.Message}"; + } + } + } +} diff --git a/yy-admin-master/YY.Admin/ViewModels/Dialogs/SuccessDialogViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/Dialogs/SuccessDialogViewModel.cs new file mode 100644 index 0000000..ccd1c83 --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/Dialogs/SuccessDialogViewModel.cs @@ -0,0 +1,45 @@ +using Prism.Dialogs; +using Prism.Mvvm; +using System; +using System.Windows.Threading; + +namespace YY.Admin.ViewModels.Dialogs +{ + public class SuccessDialogViewModel : BindableBase, IDialogAware + { + public string Title => "成功"; + + private string _message; + public string Message + { + get => _message; + set => SetProperty(ref _message, value); + } + + // Prism.Dialogs 版本的 RequestClose 是属性,不是 event + public DialogCloseListener RequestClose { get; private set; } + + public SuccessDialogViewModel() + { + // 自动关闭定时器 + var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1.5) }; + timer.Tick += (s, e) => + { + timer.Stop(); + RequestClose.Invoke(new DialogResult(ButtonResult.OK)); + }; + timer.Start(); + } + + public bool CanCloseDialog() => true; + + public void OnDialogOpened(IDialogParameters parameters) + { + Message = parameters.GetValue("message"); + } + + public void OnDialogClosed() + { + } + } +} diff --git a/yy-admin-master/YY.Admin/ViewModels/Dialogs/WarningDialogViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/Dialogs/WarningDialogViewModel.cs new file mode 100644 index 0000000..419b40e --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/Dialogs/WarningDialogViewModel.cs @@ -0,0 +1,45 @@ +using Prism.Dialogs; +using Prism.Mvvm; +using Prism.Commands; +using System; + +namespace YY.Admin.ViewModels.Dialogs +{ + public class WarningDialogViewModel : BindableBase, IDialogAware + { + public string Title => "警告"; + + private string _message; + public string Message + { + get => _message; + set => SetProperty(ref _message, value); + } + + // Prism.Dialogs 版本的 RequestClose 是属性 + public DialogCloseListener RequestClose { get; private set; } + + public DelegateCommand CloseCommand { get; } + + public WarningDialogViewModel() + { + CloseCommand = new DelegateCommand(CloseDialog); + } + + private void CloseDialog() + { + RequestClose.Invoke(new DialogResult(ButtonResult.OK)); + } + + public bool CanCloseDialog() => true; + + public void OnDialogOpened(IDialogParameters parameters) + { + Message = parameters.GetValue("message"); + } + + public void OnDialogClosed() + { + } + } +} diff --git a/yy-admin-master/YY.Admin/ViewModels/LoginWindowViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/LoginWindowViewModel.cs new file mode 100644 index 0000000..ac4aede --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/LoginWindowViewModel.cs @@ -0,0 +1,277 @@ +using System.Windows; +using System.Windows.Media; +using System.Net.Http; +using Microsoft.Extensions.Configuration; +using YY.Admin.Core.Helper; +using YY.Admin.Core.Session; +using YY.Admin.FluentValidation; +using YY.Admin.Services; +using YY.Admin.Services.Service.Auth; +using YY.Admin.Services.Service.Jeecg; +using YY.Admin.Views; + +namespace YY.Admin.ViewModels +{ + // ViewModels/LoginViewModel.cs + public class LoginWindowViewModel : BaseViewModel + { + private readonly ISysAuthService _authService; + private readonly IDialogService _dialogService; + private readonly IJeecgLoginLogReportService _loginLogReportService; + private readonly IConfiguration _configuration; + private readonly HttpClient _httpClient = new(); + private readonly CancellationTokenSource _connectivityCts = new(); + private const int ConnectivityCheckIntervalSeconds = 5; + public LoginInput LoginInput { get; set; } + private string _loginMessage = string.Empty; + public DelegateCommand LoginCommand { get; } + public DelegateCommand SyncJeecgUsersCommand { get; } + public DelegateCommand OpenServerSettingsCommand { get; } + public LoginInputValidator LoginInputValidator { get; private set; } + + private bool _isSyncingJeecgUsers; + /// + /// 正在从 Jeecg 同步用户到本地库 + /// + public bool IsSyncingJeecgUsers + { + get => _isSyncingJeecgUsers; + set + { + if (SetProperty(ref _isSyncingJeecgUsers, value)) + { + SyncJeecgUsersCommand.RaiseCanExecuteChanged(); + LoginCommand.RaiseCanExecuteChanged(); + RaisePropertyChanged(nameof(SyncJeecgUsersButtonText)); + RaisePropertyChanged(nameof(CanInteractWithLogin)); + } + } + } + + public string SyncJeecgUsersButtonText => IsSyncingJeecgUsers ? "同步中..." : "同步 Jeecg 用户"; + + /// 登录与同步互斥,用于禁用登录按钮 + public bool CanInteractWithLogin => !IsLoading && !IsSyncingJeecgUsers; + + private bool _isBackendConnected; + /// + /// 后端连接状态:true=连接中,false=已断开 + /// + public bool IsBackendConnected + { + get => _isBackendConnected; + set + { + if (SetProperty(ref _isBackendConnected, value)) + { + RaisePropertyChanged(nameof(BackendConnectionStatusText)); + RaisePropertyChanged(nameof(BackendConnectionStatusBrush)); + } + } + } + + public string BackendConnectionStatusText => IsBackendConnected ? "后端连接中" : "后端已断开"; + public Brush BackendConnectionStatusBrush => IsBackendConnected ? Brushes.LimeGreen : Brushes.Red; + + public LoginWindowViewModel( + ISysAuthService authService, + IDialogService dialogService, + IJeecgLoginLogReportService loginLogReportService, + IContainerExtension _container, + IRegionManager regionManager + ) : base(_container, regionManager) + { + _authService = authService; + _dialogService = dialogService; + _loginLogReportService = loginLogReportService; + _configuration = _container.Resolve(); + _loginLogReportService.StartBackgroundSync(); + Title = "系统登录"; + + LoginInput = new LoginInput() + { + Username = "admin", + Password = "123456" + }; + + LoginInputValidator = new LoginInputValidator(); + + LoginCommand = new DelegateCommand(async () => await LoginAsync(), CanLogin) + .ObservesProperty(() => IsLoading) + .ObservesProperty(() => IsSyncingJeecgUsers); + + SyncJeecgUsersCommand = new DelegateCommand(async () => await SyncJeecgUsersAsync(), CanSyncJeecgUsers) + .ObservesProperty(() => IsLoading) + .ObservesProperty(() => IsSyncingJeecgUsers); + + OpenServerSettingsCommand = new DelegateCommand(OpenServerSettings); + + _ = StartBackendConnectivityLoopAsync(_connectivityCts.Token); + } + + public string LoginMessage + { + get => _loginMessage; + set => SetProperty(ref _loginMessage, value); + } + + public string LoginButtonText => IsLoading ? "登录中..." : "登录"; + + protected override void OnIsLoadingChanged() + { + base.OnIsLoadingChanged(); + RaisePropertyChanged(nameof(LoginButtonText)); + RaisePropertyChanged(nameof(CanInteractWithLogin)); + LoginCommand.RaiseCanExecuteChanged(); + SyncJeecgUsersCommand.RaiseCanExecuteChanged(); + } + + private bool CanLogin() + { + return !IsLoading && !IsSyncingJeecgUsers; + } + + private bool CanSyncJeecgUsers() + { + return !IsLoading && !IsSyncingJeecgUsers; + } + + private async Task SyncJeecgUsersAsync() + { + LoginMessage = string.Empty; + IsSyncingJeecgUsers = true; + try + { + await UIHelper.WaitForRenderAsync(); + var (success, message) = await _authService.SyncJeecgUsersToLocalFromLoginScreenAsync(); + LoginMessage = message; + if (success) + { + _logger?.Information("登录页一键同步 Jeecg 用户成功"); + } + else + { + _logger?.Warning($"登录页一键同步 Jeecg 用户:{message}"); + } + } + catch (Exception ex) + { + LoginMessage = $"同步出错:{ex.Message}"; + } + finally + { + IsSyncingJeecgUsers = false; + } + } + + private async Task LoginAsync() + { + IsLoading = true; + //LoginMessage = string.Empty; + + try + { + // 让出线程,让UI先渲染 + await UIHelper.WaitForRenderAsync(); + + var response = await _authService.LoginAsync(LoginInput); + + if (response.Success) + { + _ = _loginLogReportService.ReportLoginAsync(LoginInput.Username, true, "登录成功"); + _connectivityCts.Cancel(); + //设置会话 + AppSession.CurrentUser = response.User; + // 设置用户上下文 + SetUserContext(response.User!, response.Token); + // 启动定时器 + StartTokenCheckTimer(); + // 登录成功,打开主窗口 + var loginWindow = Application.Current.MainWindow; + var mainWindow = _container.Resolve(); + Application.Current.MainWindow = mainWindow; + //把窗口和RegionManager 绑定一下即可 + SetRegionManager(mainWindow); + mainWindow.Show(); + loginWindow.Close(); + + // 不等待异步更新用户登录信息,不阻塞主窗口打开 + _ = _authService.UpdateUserLoginInfoAsync(response.User!); + } + else + { + LoginMessage = response.Message; + _ = _loginLogReportService.ReportLoginAsync(LoginInput.Username, false, response.Message); + } + } + catch (Exception ex) + { + LoginMessage = $"登录出错:{ex.Message}"; + _ = _loginLogReportService.ReportLoginAsync(LoginInput.Username, false, ex.Message); + } + finally + { + IsLoading = false; + } + } + + private async Task StartBackendConnectivityLoopAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + bool connected = false; + try + { + // 每轮都重新读取配置,保存服务器设置后可即时生效 + var baseUrl = _configuration.GetValue("JeecgIntegration:BaseUrl")?.TrimEnd('/'); + var userListPath = _configuration.GetValue("JeecgIntegration:UserListPath") ?? "/sys/user/scada/queryUser"; + var probeUrl = string.IsNullOrWhiteSpace(baseUrl) + ? string.Empty + : $"{baseUrl}{userListPath}?pageNo=1&pageSize=1&includeDetail=false"; + if (!string.IsNullOrWhiteSpace(probeUrl)) + { + using var req = new HttpRequestMessage(HttpMethod.Get, probeUrl); + using var resp = await _httpClient.SendAsync(req, cancellationToken); + connected = resp.IsSuccessStatusCode; + } + } + catch + { + connected = false; + } + + try + { + await Application.Current.Dispatcher.InvokeAsync(() => + { + IsBackendConnected = connected; + }); + } + catch + { + // 忽略窗口关闭后的调度异常 + } + + try + { + await Task.Delay(TimeSpan.FromSeconds(ConnectivityCheckIntervalSeconds), cancellationToken); + } + catch (OperationCanceledException) + { + break; + } + } + } + + private void OpenServerSettings() + { + _dialogService.ShowDialog("ServerSettingsDialog", r => + { + if (r.Result == ButtonResult.OK) + { + LoginMessage = "服务器配置已保存"; + } + }); + } + } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin/ViewModels/MainWindowViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..7b30958 --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,691 @@ +using Mapster; +using Microsoft.Extensions.Configuration; +using Newtonsoft.Json; +using SqlSugar; +using System.Collections.ObjectModel; +using System.IO; +using System.Net.Http; +using System.Windows; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Threading; +using YY.Admin.Core; +using YY.Admin.Core.Const; +using YY.Admin.Core.Model; +using YY.Admin.Core.Session; +using YY.Admin.Core.Util; +using YY.Admin.Event; +using YY.Admin.Module; +using YY.Admin.Services.Service.Auth; +using YY.Admin.Services.Service.Jeecg; +using YY.Admin.Services.Service.Menu; +using YY.Admin.ViewModels.Control; + +namespace YY.Admin.ViewModels +{ + public class MainWindowViewModel : BaseViewModel + { + private readonly ISysAuthService _authService; + private readonly IDialogService _dialogService; + private readonly IJeecgLoginLogReportService _loginLogReportService; + private readonly IJeecgUserSyncCoordinator _jeecgUserSyncCoordinator; + private readonly IConfiguration _configuration; + private readonly HttpClient _httpClient = new(); + private readonly CancellationTokenSource _backendConnectivityCts = new(); + private const int ConnectivityCheckIntervalSeconds = 5; + private SysUser? _currentUser; + private bool _isBackendConnected; + + #region 标签集合 + public ObservableCollection OpenTabs { get; } = new ObservableCollection(); + private CancellationTokenSource? _navigateCts; + private bool _isTabClosing; + private TabItemModel? _selectedTab; + public TabItemModel? SelectedTab + { + get => _selectedTab; + set + { + if (SetProperty(ref _selectedTab, value) && value != null) + { + // 发布事件 + _eventAggregator.GetEvent().Publish(value); + + bool isNotMenuTreeView = _regionManager.Regions[CommonConst.MenuRegion].ActiveViews.Where(it => it.GetType().Name != "MenuTreeView").Any(); + if (value.TabSource is MenuItem menuItem && !isNotMenuTreeView) + { + var navItem = NavItems?.Where(it => it.ViewName == "MenuTreeView").FirstOrDefault(); + if (navItem != null) + { + if (navItem.AlignBottom) + { + SelectedBottomNavItem = navItem; + } + else + { + SelectedTopNavItem = navItem; + } + } + } else if (value.TabSource is NavItem navItem) + { + if (navItem.AlignBottom) + { + SelectedBottomNavItem = navItem; + } else + { + SelectedTopNavItem = navItem; + } + } + + // 取消上一次延迟导航 + // 防止拖拽Tab时由于两次导航出现视觉上闪烁问题 + // TabA拖动到TabB过程:TabA -> TabB -> TabA + _navigateCts?.Cancel(); + _navigateCts = new CancellationTokenSource(); + + // 延迟 100ms 执行导航(过滤临时选中) + _ = Task.Delay(100, _navigateCts.Token) + .ContinueWith(_ => + { + Application.Current.Dispatcher.Invoke(() => + { + _ = NavigateToViewAsync(CommonConst.ContentRegion, value.ViewName, value.TabSource?.NavigationParameter); + }); + }, + TaskContinuationOptions.OnlyOnRanToCompletion + ); + } + } + } + #endregion + + # region 侧边栏 + public ObservableCollection? NavItems { get; set; } + public ObservableCollection? TopNavItems { get; set; } + public ObservableCollection? BottomNavItems { get; set; } + + private NavItem? _selectedNavItem; + public NavItem? SelectedNavItem + { + get => _selectedNavItem; + set { + if (SetProperty(ref _selectedNavItem, value) && value != null) + { + // 执行命令(如果有) + if (value.Command?.CanExecute(value) == true) + { + value.Command.Execute(value); + } + } + } + } + + private NavItem? _selectedTopNavItem; + public NavItem? SelectedTopNavItem + { + get => _selectedTopNavItem; + set + { + if (SetProperty(ref _selectedTopNavItem, value) && value != null) + { + + // 取消底部选中 互斥 + SelectedBottomNavItem = null; + + SelectedNavItem = value; + } + } + } + + private NavItem? _selectedBottomNavItem; + public NavItem? SelectedBottomNavItem + { + get => _selectedBottomNavItem; + set + { + if (SetProperty(ref _selectedBottomNavItem, value) && value != null) + { + + // 取消顶部选中 互斥 + SelectedTopNavItem = null; + + SelectedNavItem = value; + } + } + } + #endregion + + #region 系统设置 + private bool _isAppSettingsOpen; + public bool IsAppSettingsOpen + { + get => _isAppSettingsOpen; + set => SetProperty(ref _isAppSettingsOpen, value); + } + + private AppSettingsViewModel? _appSettingsViewModel; + public AppSettingsViewModel? AppSettingsViewModel { + get => _appSettingsViewModel; + set => SetProperty(ref _appSettingsViewModel, value); + } + + // 系统设置命令 + public ICommand ResetAppSettingsCommand { get; } + public ICommand OpenAppSettingsCommand { get; } + public ICommand OpenServerSettingsCommand { get; } + + #endregion + + private SubscriptionToken? _openOrActivateTabToken; + private SubscriptionToken? _tabClosedToken; + private SubscriptionToken? _refreshTabToken; + private SubscriptionToken? _loginOutToken; + + public MainWindowViewModel( + ISysAuthService authService, + IDialogService dialogService, + IJeecgLoginLogReportService loginLogReportService, + IJeecgUserSyncCoordinator jeecgUserSyncCoordinator, + ISysMenuService sysMenuService, + IContainerExtension _container, + IRegionManager regionManager + ) : base(_container, regionManager) + { + // 加载系统设置 + LoadAppSettings(); + + _authService = authService; + _dialogService = dialogService; + _loginLogReportService = loginLogReportService; + _jeecgUserSyncCoordinator = jeecgUserSyncCoordinator; + _configuration = _container.Resolve(); + _loginLogReportService.StartBackgroundSync(); + // 订阅用户变更事件 + _currentUser = _authService.CurrentUser; + _authService.UserChanged += OnUserChanged; + + // 登出命令 + LogoutCommand = new DelegateCommand(LogoutAsync); + // 系统设置命令 + OpenAppSettingsCommand = new DelegateCommand(OpenAppSettings); + ResetAppSettingsCommand = new DelegateCommand(ResetAppSettings); + OpenServerSettingsCommand = new DelegateCommand(OpenServerSettings); + + // 初始化Sidebar数据 + InitNavItems(); + + // 订阅事件 + _openOrActivateTabToken = _eventAggregator.GetEvent().Subscribe(OnOpenOrActivateTab); + _tabClosedToken = _eventAggregator.GetEvent().Subscribe(OnTabClosed); + _refreshTabToken = _eventAggregator.GetEvent().Subscribe(OnRefreshTab); + _loginOutToken = _eventAggregator.GetEvent().Subscribe(Destroy); + + // Jeecg 用户增量同步:定时 + 可选 WebSocket(工控机断网续传) + _jeecgUserSyncCoordinator.Start(); + + // 主窗口底部连接状态圆点 + _ = StartBackendConnectivityLoopAsync(_backendConnectivityCts.Token); + } + + + public SysUser? CurrentUser + { + get => _currentUser; + set => SetProperty(ref _currentUser, value); + } + + /// + /// 后端连接状态:true=连接中,false=已断开 + /// + public bool IsBackendConnected + { + get => _isBackendConnected; + set + { + if (SetProperty(ref _isBackendConnected, value)) + { + RaisePropertyChanged(nameof(BackendConnectionStatusBrush)); + } + } + } + + public Brush BackendConnectionStatusBrush => IsBackendConnected ? Brushes.LimeGreen : Brushes.Red; + + public DelegateCommand LogoutCommand { get; } + + private void InitNavItems() + { + NavItems = new ObservableCollection + { + new NavItem { + Icon = "FileTreeOutline", + Name = "功能菜单", + ViewName = "MenuTreeView", + Command = new DelegateCommand(it => _ = NavigateToViewAsync(CommonConst.MenuRegion, it.ViewName)) + }, + new NavItem { + Icon = "GamepadVariantOutline", + Name = "菜单区域", + Command = new DelegateCommand(it => _ = NavigateToViewAsync(CommonConst.MenuRegion, it.ViewName)) + }, + new NavItem { + Icon = "FoodAppleOutline", + Name = "Tab区域", + Command = new DelegateCommand(OnOpenOrActivateTab) + }, + new NavItem { + Icon = "Server", + Name = "服务器设置", + AlignBottom = true, + IsActive = false, + Command = OpenServerSettingsCommand + }, + new NavItem { + Icon = "AccountCircleOutline", + Name = "个人中心", + AlignBottom = true, + Command = new DelegateCommand(OnOpenOrActivateTab) + }, + new NavItem { + Icon = StringUtil.ConvertHtmlEntityToUnicode(""), + IconType = IconTypeEnum.AntDesign, + Name = "系统设置", + AlignBottom = true, + IsActive = false, + Command = OpenAppSettingsCommand + }, + new NavItem { + Icon = "Power", + Name = "退出登录", + AlignBottom = true, + IsActive = false, + Command = LogoutCommand + } + }; + TopNavItems = new(NavItems.Where(t => !t.AlignBottom)); + BottomNavItems = new(NavItems.Where(t => t.AlignBottom)); + + SelectedTopNavItem = TopNavItems.FirstOrDefault(); + } + + private void OnUserChanged(object? sender, SysUser? user) + { + CurrentUser = user; + //// 用户变更时刷新菜单 + //LoadMenuAsync(); + } + + /// + /// 导航 + /// + /// 区域名称 + /// 视图名称 + /// + private async Task NavigateToViewAsync(string regionName, string? viewName, INavigationParameters? parameters = null) + { + try + { + if (string.IsNullOrEmpty(regionName)) + return; + + var tcs = new TaskCompletionSource(); + + // 视图为空 或者 视图未在容器中注册 + if (string.IsNullOrEmpty(viewName) || !(_container as IContainerProvider).IsRegistered(viewName)) + { + _logger.Error($"视图未注册: {viewName}"); + _regionManager.RequestNavigate(regionName, "NotFoundView"); + tcs.SetResult(false); + return; + } + + _logger.Debug($"开始后台异步导航到: {viewName}"); + + + // 在UI线程执行导航(但不在属性设置的调用栈中) + await Application.Current.Dispatcher.InvokeAsync(() => + { + _regionManager.RequestNavigate(regionName, viewName, + result => + { + if (result.Success) + { + _logger.Debug($"导航成功: {viewName}"); + tcs.SetResult(true); + } + else + { + _logger.Error($"导航失败: {viewName}"); + tcs.SetResult(false); + } + }, parameters); + }, DispatcherPriority.Background); // 使用较低优先级 + + await tcs.Task; + } + catch (Exception ex) + { + _logger.Error($"导航异常: {ex.Message}"); + } + } + + /// + /// 打开或激活一个标签页(若已打开则激活,否则新建) + /// + private void OnOpenOrActivateTab(TabSource tabSource) + { + if (tabSource == null) { + _logger.Debug("tabSource为空"); + return; + } + + if (tabSource is MenuItem menuItem) + { + // 检查是否是父级菜单(有子菜单) + if (menuItem.Children?.Count > 0) + { + _logger.Debug($"跳过父级菜单: {menuItem.Name},它有 {menuItem.Children.Count} 个子菜单"); + return; + } + + // 检查是否有有效的 ViewName + //if (string.IsNullOrEmpty(menuItem.ViewName)) + //{ + // _logger.Debug($"菜单没有 ViewName: {menuItem.Name}"); + // return; + //} + + _logger.Debug($"点击菜单: {menuItem.Name}, ViewName: {menuItem.ViewName}"); + } + + // 检查标签是否已打开 + var existingTab = OpenTabs.FirstOrDefault(t => t.TabSource == tabSource); + if (existingTab != null) + { + _logger.Debug($"切换到已存在标签: {tabSource.ViewName}"); + SelectedTab = existingTab; + return; + } + + // 创建新标签 + var newTab = new TabItemModel + { + Header = tabSource.Name, + Icon = tabSource.Icon, + IconType = tabSource.IconType, + ViewName = tabSource.ViewName, + IsClosable = tabSource.IsClosable, + TabSource = tabSource, + OpenTabs = OpenTabs, + EventAggregator = _eventAggregator + }; + + _logger.Debug($"创建新标签: {tabSource.ViewName}"); + OpenTabs.Add(newTab); + SelectedTab = newTab; + } + + /// + /// 刷新Tab + /// + /// + private void OnRefreshTab(TabItemModel tabItemModel) + { + if (tabItemModel?.TabSource == null) + { + return; + } + + // 当前Tab选中 + SelectedTab = tabItemModel; + + // 从Region中移除现有视图(避免缓存问题) + RemoveContentRegionView(tabItemModel.ViewName); + + // 重新导航,确保加载新的视图 + _ = NavigateToViewAsync(CommonConst.ContentRegion, tabItemModel.ViewName, tabItemModel.TabSource.NavigationParameter); + } + + /// + /// TabItem关闭回调 + /// + private void OnTabClosed(TabItemModel tabModel) + { + if (tabModel != null) + { + _logger.Debug($"标签已关闭: {tabModel.Header}"); + + //var viewName = OpenTabs.Count <= 1 ? null : tabModel.ViewName; + var viewName = tabModel.ViewName; + + RemoveContentRegionView(viewName); + + // 当前Tab是最后一个 + if (OpenTabs.Count <= 1) + { + var region = _regionManager.Regions[CommonConst.MenuRegion]; + var activeView = region.ActiveViews.FirstOrDefault(); + if (activeView != null) + { + var navItem = NavItems?.Where(it => it.ViewName == activeView.GetType().Name).FirstOrDefault(); + if (navItem != null) + { + if (navItem.AlignBottom) + { + SelectedBottomNavItem = navItem; + } + else + { + SelectedTopNavItem = navItem; + } + } + } + } else if (tabModel.TabSource is NavItem navItem) + { + if (navItem == SelectedTopNavItem) + { + SelectedTopNavItem = null; + SelectedNavItem = null; + } + else if (navItem == SelectedBottomNavItem) + { + SelectedBottomNavItem = null; + SelectedNavItem = null; + } + } + + // 如果关闭的不是当前选中的Tab + if (SelectedTab != null && SelectedTab != tabModel) + { + // 发布事件:强制刷新当前选中(因为右键关闭未切换,不会触发SelectedTab属性setter方法) + _eventAggregator.GetEvent().Publish(SelectedTab); + } + } + } + + private async void LogoutAsync() + { + // 使用异步版本 + var confirmed = await ConfirmAsync("确定退出登录吗?"); + if (confirmed) + { + var account = AppSession.CurrentUser?.Account ?? CurrentUser?.Account ?? string.Empty; + _ = _loginLogReportService.ReportLogAsync("LOGIN", "退出登录", account, true); + // 发布登出事件 + _eventAggregator.GetEvent().Publish(AppSession.CurrentUser!); + Logout(); + } + // var messageBoxResult = HandyControl.Controls.MessageBox.Show( + // $"确定退出登录吗?", + // "确认登出", + //MessageBoxButton.OKCancel, + //MessageBoxImage.Warning); + + // if (messageBoxResult != MessageBoxResult.OK) return; + + // // 发布登出事件 + // _eventAggregator.GetEvent().Publish(AppSession.CurrentUser!); + + // Logout(); + } + + #region 主题 + /// + /// 打开系统设置 + /// + private void OpenAppSettings() + { + IsAppSettingsOpen = true; + } + + /// + /// 重置系统设置 + /// + private void ResetAppSettings() + { + // 重置为默认值 + // new AppSettingsViewModel().Adapt(AppSettingsViewModel); + + AppSettingsViewModel = new AppSettingsViewModel(); + AppSettingsViewModel.UpdateSkin(AppSettingsViewModel.SkinType!.Value); + AppSettingsViewModel.UpdateAppSettings(); + } + + /// + /// 加载系统设置 + /// + private void LoadAppSettings() + { + try + { + var filePath = AppSettingsViewModel.GetFilePath(); + if (!File.Exists(filePath)) + { + AppSettingsViewModel = new AppSettingsViewModel(); + AppSettingsViewModel.UpdateSkin(AppSettingsViewModel.SkinType!.Value); + AppSettingsViewModel.SaveAppSettings(); + return; + } + var json = File.ReadAllText(filePath); + var appSettings = JsonConvert.DeserializeObject(json); + AppSettingsViewModel = appSettings.Adapt(); + AppSettingsViewModel.SkinType = AppSettingsViewModel.SyncWithSystem ? AppSettingsViewModel.GetSkinTypeBySystem() : AppSettingsViewModel.SkinType; + AppSettingsViewModel.UpdateSkin(AppSettingsViewModel.SkinType!.Value); + AppSettingsViewModel.UpdateAppSettings(); + } + catch (Exception ex) + { + _logger.Error($"加载系统设置失败: {ex.Message}", ex); + } + } + + #endregion + + private async Task StartBackendConnectivityLoopAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + bool connected = false; + try + { + // 每轮都重新读取配置,保存服务器设置后可即时生效 + var baseUrl = _configuration.GetValue("JeecgIntegration:BaseUrl")?.TrimEnd('/'); + var userListPath = _configuration.GetValue("JeecgIntegration:UserListPath") ?? "/sys/user/scada/queryUser"; + var probeUrl = string.IsNullOrWhiteSpace(baseUrl) + ? string.Empty + : $"{baseUrl}{userListPath}?pageNo=1&pageSize=1&includeDetail=false"; + if (!string.IsNullOrWhiteSpace(probeUrl)) + { + using var req = new HttpRequestMessage(HttpMethod.Get, probeUrl); + using var resp = await _httpClient.SendAsync(req, cancellationToken); + connected = resp.IsSuccessStatusCode; + } + } + catch + { + connected = false; + } + + try + { + await Application.Current.Dispatcher.InvokeAsync(() => + { + IsBackendConnected = connected; + }); + } + catch + { + // 忽略窗口关闭后的调度异常 + } + + try + { + await Task.Delay(TimeSpan.FromSeconds(ConnectivityCheckIntervalSeconds), cancellationToken); + } + catch (OperationCanceledException) + { + break; + } + } + } + + private void OpenServerSettings() + { + _dialogService.ShowDialog("ServerSettingsDialog", r => { }); + } + + /// + /// 清空资源 + /// + private void Destroy(SysUser? sysUser = null) + { + if (_openOrActivateTabToken != null) + { + _eventAggregator + .GetEvent() + .Unsubscribe(_openOrActivateTabToken); + + _openOrActivateTabToken = null; + } + + if (_tabClosedToken != null) + { + _eventAggregator + .GetEvent() + .Unsubscribe(_tabClosedToken); + + _tabClosedToken = null; + } + + if (_refreshTabToken != null) + { + _eventAggregator + .GetEvent() + .Unsubscribe(_refreshTabToken); + + _refreshTabToken = null; + } + + if (_loginOutToken != null) + { + _eventAggregator + .GetEvent() + .Unsubscribe(_loginOutToken); + + _loginOutToken = null; + } + + if (_authService != null) + { + _authService.UserChanged -= OnUserChanged; + } + + if (!_backendConnectivityCts.IsCancellationRequested) + { + _backendConnectivityCts.Cancel(); + } + + _jeecgUserSyncCoordinator.Stop(); + } + } +} diff --git a/yy-admin-master/YY.Admin/ViewModels/NotFoundViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/NotFoundViewModel.cs new file mode 100644 index 0000000..61b30ed --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/NotFoundViewModel.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using YY.Admin.Core.Const; + +namespace YY.Admin.ViewModels +{ + public class NotFoundViewModel : BindableBase + { + private readonly IRegionManager _regionManager; + + public NotFoundViewModel(IRegionManager regionManager) + { + _regionManager = regionManager; + GoHomeCommand = new DelegateCommand(GoHome); + } + + public DelegateCommand GoHomeCommand { get; } + + private void GoHome() + { + _regionManager.RequestNavigate(CommonConst.ContentRegion, "DashboardView"); + } + } +} diff --git a/yy-admin-master/YY.Admin/ViewModels/SysManage/DataDictionaryManagementViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/SysManage/DataDictionaryManagementViewModel.cs new file mode 100644 index 0000000..6299761 --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/SysManage/DataDictionaryManagementViewModel.cs @@ -0,0 +1,94 @@ +using HandyControl.Controls; +using YY.Admin.Core; +using YY.Admin.Core.Extension; +using YY.Admin.Core.Helper; +using YY.Admin.Services.Service; +using YY.Admin.ViewModels.Control; + +namespace YY.Admin.ViewModels.SysManage; + +public class DataDictionaryManagementViewModel : BaseViewModel +{ + private readonly IJeecgDictSyncService _dictSyncService; + + private PaginationDataGridViewModel _paginationDataGridViewModel; + public PaginationDataGridViewModel PaginationDataGridViewModel + { + get => _paginationDataGridViewModel; + set => SetProperty(ref _paginationDataGridViewModel, value); + } + + private PageJeecgDictItemInput _input; + public PageJeecgDictItemInput Input + { + get => _input; + set => SetProperty(ref _input, value); + } + + public List> StatusList => + Enum.GetValues(typeof(StatusEnum)) + .Cast() + .Select(e => new KeyValuePair(e.GetDescription(), (int)e)) + .ToList(); + + public DelegateCommand SearchCommand { get; } + public DelegateCommand ResetCommand { get; } + public DelegateCommand SyncCommand { get; } + + public DataDictionaryManagementViewModel( + IJeecgDictSyncService dictSyncService, + IContainerExtension container, + IRegionManager regionManager) : base(container, regionManager) + { + _dictSyncService = dictSyncService; + _paginationDataGridViewModel = new PaginationDataGridViewModel(FetchAsync); + _input = new PageJeecgDictItemInput(); + + SearchCommand = new DelegateCommand(async () => await SearchAsync()); + ResetCommand = new DelegateCommand(async () => await ResetAsync()); + SyncCommand = new DelegateCommand(async () => await SyncAsync()); + + _ = InitializeAsync(); + } + + private async Task InitializeAsync() + { + await UIHelper.WaitForRenderAsync(); + await PaginationDataGridViewModel.LoadDataAsync(); + } + + private async Task<(IEnumerable data, int totalCount)> FetchAsync() + { + Input.Page = PaginationDataGridViewModel.PageIndex; + Input.PageSize = PaginationDataGridViewModel.DataCountPerPage; + var result = await _dictSyncService.PageAsync(Input); + return (result.Items, result.Total); + } + + private async Task SearchAsync() + { + PaginationDataGridViewModel.PageIndex = 1; + await PaginationDataGridViewModel.LoadDataAsync(); + } + + private async Task ResetAsync() + { + Input = new PageJeecgDictItemInput(); + await UIHelper.WaitForRenderAsync(); + await SearchAsync(); + } + + private async Task SyncAsync() + { + var count = await _dictSyncService.SyncFromJeecgAsync(); + if (count > 0) + { + Growl.Success($"同步完成,共处理 {count} 条数据字典项"); + } + else + { + Growl.Warning("未同步到数据字典,请确认后端可访问"); + } + await SearchAsync(); + } +} diff --git a/yy-admin-master/YY.Admin/ViewModels/SysManage/RoleManagementViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/SysManage/RoleManagementViewModel.cs new file mode 100644 index 0000000..8ff5a01 --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/SysManage/RoleManagementViewModel.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using YY.Admin.Services.Service.User; +using YY.Admin.Services.Service; +using YY.Admin.ViewModels.Control; + +namespace YY.Admin.ViewModels.SysManage +{ + public class RoleManagementViewModel : BaseViewModel + { + public DelegateCommand AlertDialogCommand { get; } + public DelegateCommand ConfirmDialogCommand { get; } + public DelegateCommand ErrorDialogCommand { get; } + public DelegateCommand SuccessDialogCommand { get; } + public DelegateCommand WarningDialogCommand { get; } + + public RoleManagementViewModel( + IContainerExtension container, + IRegionManager regionManager + ) : base(container, regionManager) + { + AlertDialogCommand = new DelegateCommand( () => AlertDialogAsync()); + ConfirmDialogCommand = new DelegateCommand( () => ConfirmDialogAsync()); + ErrorDialogCommand = new DelegateCommand( () => ErrorDialogAsync()); + SuccessDialogCommand = new DelegateCommand( () => SuccessDialogAsync()); + WarningDialogCommand = new DelegateCommand( () => WarningDialogAsync()); + } + + private void WarningDialogAsync() + { + throw new NotImplementedException(); + } + + private void SuccessDialogAsync() + { + throw new NotImplementedException(); + } + + private void ErrorDialogAsync() + { + throw new NotImplementedException(); + } + + private void ConfirmDialogAsync() + { + base.Confirm("测试Confirm"); + } + + private void AlertDialogAsync() + { + base.Alert("测试Alert"); + } + } +} diff --git a/yy-admin-master/YY.Admin/ViewModels/SysManage/TenantManagementViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/SysManage/TenantManagementViewModel.cs new file mode 100644 index 0000000..d58f3b7 --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/SysManage/TenantManagementViewModel.cs @@ -0,0 +1,96 @@ +using HandyControl.Controls; +using YY.Admin.Core; +using YY.Admin.Core.Extension; +using YY.Admin.Core.Helper; +using YY.Admin.Services.Service; +using YY.Admin.Services.Service.Tenant; +using YY.Admin.ViewModels.Control; + +namespace YY.Admin.ViewModels.SysManage +{ + public class TenantManagementViewModel : BaseViewModel + { + private readonly ISysTenantSyncService _tenantSyncService; + + private PaginationDataGridViewModel _paginationDataGridViewModel; + public PaginationDataGridViewModel PaginationDataGridViewModel + { + get => _paginationDataGridViewModel; + set => SetProperty(ref _paginationDataGridViewModel, value); + } + + private PageTenantInput _input; + public PageTenantInput Input + { + get => _input; + set => SetProperty(ref _input, value); + } + + public List> StatusList => + Enum.GetValues(typeof(StatusEnum)) + .Cast() + .Select(e => new KeyValuePair(e.GetDescription(), (int)e)) + .ToList(); + + public DelegateCommand SearchCommand { get; } + public DelegateCommand ResetCommand { get; } + public DelegateCommand SyncCommand { get; } + + public TenantManagementViewModel( + ISysTenantSyncService tenantSyncService, + IContainerExtension container, + IRegionManager regionManager) : base(container, regionManager) + { + _tenantSyncService = tenantSyncService; + _paginationDataGridViewModel = new PaginationDataGridViewModel(FetchTenantsAsync); + _input = new PageTenantInput(); + + SearchCommand = new DelegateCommand(async () => await SearchAsync()); + ResetCommand = new DelegateCommand(async () => await ResetAsync()); + SyncCommand = new DelegateCommand(async () => await SyncAsync()); + + _ = InitializeAsync(); + } + + private async Task InitializeAsync() + { + await UIHelper.WaitForRenderAsync(); + await PaginationDataGridViewModel.LoadDataAsync(); + } + + private async Task<(IEnumerable data, int totalCount)> FetchTenantsAsync() + { + Input.Page = PaginationDataGridViewModel.PageIndex; + Input.PageSize = PaginationDataGridViewModel.DataCountPerPage; + var result = await _tenantSyncService.PageAsync(Input); + return (result.Items, result.Total); + } + + private async Task SearchAsync() + { + PaginationDataGridViewModel.PageIndex = 1; + await PaginationDataGridViewModel.LoadDataAsync(); + } + + private async Task ResetAsync() + { + Input = new PageTenantInput(); + await UIHelper.WaitForRenderAsync(); + await SearchAsync(); + } + + private async Task SyncAsync() + { + var count = await _tenantSyncService.SyncFromJeecgAsync(); + if (count > 0) + { + Growl.Success($"同步完成,共处理 {count} 条租户数据"); + } + else + { + Growl.Warning("未同步到租户数据,请确认已使用Jeecg账号登录且后端可访问"); + } + await SearchAsync(); + } + } +} diff --git a/yy-admin-master/YY.Admin/ViewModels/SysManage/UserEditDialogViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/SysManage/UserEditDialogViewModel.cs new file mode 100644 index 0000000..4b558c8 --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/SysManage/UserEditDialogViewModel.cs @@ -0,0 +1,179 @@ +using HandyControl.Controls; +using HandyControl.Data; +using HandyControl.Tools.Extension; +using System.Collections.ObjectModel; +using YY.Admin.Core; +using YY.Admin.FluentValidation; +using YY.Admin.Services.Service; +using YY.Admin.Services.Service.User; + +namespace YY.Admin.ViewModels.SysManage +{ + public class UserEditDialogViewModel : BaseViewModel, IDialogResultable + { + private SysUser? _sysUser; + private readonly ISysUserService _sysUserService; + + public SysUserValidator SysUserValidator { get; private set; } + + public SysUser? SysUser + { + get => _sysUser; + set { + SetProperty(ref _sysUser, value); + } + } + + public bool IsAddMode => SysUser?.Id == 0; + + public string DialogTitle => IsAddMode ? "新增用户" : "编辑用户"; + + // 性别枚举列表 + //public List> GenderList => + // Enum.GetValues(typeof(GenderEnum)) + // .Cast() + // .Select(e => new KeyValuePair(e.GetDescription(), (int)e)) + // .ToList(); + + public ObservableCollection GenderList { get; } + = new ObservableCollection(Enum.GetValues(typeof(GenderEnum)).Cast().ToArray()); + + public ObservableCollection StatusOptions { get; } + = new ObservableCollection(Enum.GetValues(typeof(StatusEnum)).Cast().ToArray()); + + //// 状态枚举列表 + //public List> StatusList => + // Enum.GetValues(typeof(StatusEnum)) + // .Cast() + // .Select(e => new KeyValuePair(e.GetDescription(), (int)e)) + // .ToList(); + + public DelegateCommand SaveCommand { get; private set; } + public DelegateCommand CancelCommand { get; private set; } + + public DelegateCommand StatusSelectedCommand { get; } + + private bool _result; + public bool Result { get => _result; set => SetProperty(ref _result, value); } + public Action? CloseAction { get; set; } + + public UserEditDialogViewModel( + ISysUserService sysUserService, + IContainerExtension container, + IRegionManager regionManager) : base(container, regionManager) + { + _sysUserService = sysUserService; + SysUserValidator = new SysUserValidator(sysUserService); + //SysUser = new SysUser(); + + StatusSelectedCommand = new DelegateCommand(OnStatusSelected); + + SaveCommand = new DelegateCommand(async () => await SaveUserAsync()); + CancelCommand = new DelegateCommand(() => CloseAction?.Invoke()); + } + + private void OnStatusSelected(object statusObj) + { + if (statusObj is StatusEnum status) + { + SysUser?.Status = status; + } + } + + public async Task SaveUserAsync() + { + try + { + if (IsAddMode) + { + var result = await _sysUserService.CreateAsync(SysUser!); + Result = result > 0; + if (Result) + { + Growl.Success(new GrowlInfo + { + Message = "新增用户成功!", + ShowDateTime = false, + WaitTime = 1 + }); + } + else + { + Growl.Error("新增用户失败!"); + return; + } + } + else + { + var result = await _sysUserService.UpdateAsync(SysUser!); + Result = result > 0; + if (Result) + { + Growl.Success("修改用户成功!"); + } + else + { + Growl.Error("修改用户失败!"); + return; + } + } + + // 关闭对话框 + CloseAction?.Invoke(); + } + catch (Exception ex) + { + Growl.Error($"操作失败:{ex.Message}"); + } + } + + // 初始化编辑数据 + public void InitializeForEdit(UserOutput user) + { + if (user != null) + { + SysUser = new SysUser + { + Id = user.Id, + //Account = user.Account, + RealName = user.RealName, + NickName = user.NickName, + //Phone = user.Phone, + Sex = user.Sex, + Birthday = user.Birthday, + Age = user.Age, + //AccountType = user.AccountType, + Status = user.Status + }; + } + } + + // 初始化新增数据 + public void InitializeForAdd() + { + SysUser = new SysUser(); + //IsAddMode = true; + RaisePropertyChanged(nameof(IsAddMode)); + RaisePropertyChanged(nameof(DialogTitle)); + } + + //private readonly SysUserValidator _addValidator = new SysUserValidator(true); + //private readonly SysUserValidator _editValidator = new SysUserValidator(false); + + //public string this[string columnName] + //{ + // get + // { + // if (SysUser == null) return string.Empty; + + // var validator = IsAddMode ? _addValidator : _editValidator; + // var result = validator.Validate(SysUser); + + // var error = result.Errors.FirstOrDefault(e => e.PropertyName == columnName); + // return error?.ErrorMessage; + // } + //} + + + } +} diff --git a/yy-admin-master/YY.Admin/ViewModels/SysManage/UserManagementViewModel.cs b/yy-admin-master/YY.Admin/ViewModels/SysManage/UserManagementViewModel.cs new file mode 100644 index 0000000..1861c5a --- /dev/null +++ b/yy-admin-master/YY.Admin/ViewModels/SysManage/UserManagementViewModel.cs @@ -0,0 +1,607 @@ +using HandyControl.Controls; +using HandyControl.Tools.Extension; +using SqlSugar; +using System.Diagnostics; +using System.Windows; +using YY.Admin.Core; +using YY.Admin.Core.Extension; +using YY.Admin.Core.Helper; +using YY.Admin.Services.Service; +using YY.Admin.Services.Service.User; +using YY.Admin.ViewModels.Control; +using YY.Admin.Views.SysManage; + +namespace YY.Admin.ViewModels.SysManage +{ + public class UserManagementViewModel : BaseViewModel + { + private PaginationDataGridViewModel _paginationDataGridViewModel; + + private PageUserInput _userInput; + + private readonly ISysUserService _sysUserService; + + private readonly IDialogService _dialogService; + private SubscriptionToken? _jeecgSyncToken; + public PaginationDataGridViewModel PaginationDataGridViewModel + { + get => _paginationDataGridViewModel; + set => SetProperty(ref _paginationDataGridViewModel, value); + } + + + public PageUserInput UserInput + { get => _userInput; + set => SetProperty(ref _userInput, value); + } + + // 性别枚举列表属性 + public List GenderList => + [.. Enum.GetValues(typeof(GenderEnum)).Cast()]; + + // 状态枚举列表属性 + public List> StatusList => + Enum.GetValues(typeof(StatusEnum)) + .Cast() + .Select(e => new KeyValuePair(e.GetDescription(), (int)e)) + .ToList(); + + // 全选 + //private bool? _isAllSelected = false; + //public bool? IsAllSelected + //{ + // get => _isAllSelected; + // set + // { + // if (SetProperty(ref _isAllSelected, value) && value.HasValue) + // { + // // 只有当值真正改变时才执行全选逻辑 + // SelectAll(value.Value); + // } + // } + //} + + // 选中用户数量 + private int _selectedCount; + public int SelectedCount + { + get => _selectedCount; + set + { + if (SetProperty(ref _selectedCount, value)) + { + // 当 SelectedCount 变化时,触发 HasSelectedItems 通知 + RaisePropertyChanged(nameof(HasSelectedItems)); + } + } + } + + // 是否有选中项 + public bool HasSelectedItems => SelectedCount > 0; + + // 批量删除 + public DelegateCommand BatchDeleteCommand { get; private set; } + + // 单个删除 + public DelegateCommand DeleteCommand { get; private set; } + + // 查询 + public DelegateCommand SearchCommand { get; private set; } + + public DelegateCommand ResetCommand { get; private set; } + + public DelegateCommand AddCommand { get; private set; } + public DelegateCommand EditCommand { get; private set; } + + public DelegateCommand StatusToggleCommand { get; private set; } + + + // 行选择改变命令 + //public DelegateCommand RowSelectionChangedCommand { get; private set; } + + public UserManagementViewModel( + ISysUserService sysUserService, + IContainerExtension container, + IDialogService dialogService, + IRegionManager regionManager + ) : base(container, regionManager) + { + _sysUserService= sysUserService; + _dialogService = dialogService; + // 创建分页控件的 ViewModel,传递一个获取数据的委托 + _paginationDataGridViewModel = new PaginationDataGridViewModel(FetchUsersAsync); + _userInput = new PageUserInput(); + + // 初始化批量删除命令 + BatchDeleteCommand = new DelegateCommand(async () => await BatchDelete(), + () => HasSelectedItems); + + DeleteCommand = new DelegateCommand(async (id) => { + if (id.HasValue) + { + await Delete(id.Value); + } + }); + + SearchCommand = new DelegateCommand(async () => await ReadAsync()); + + ResetCommand = new DelegateCommand(async () => await ResetFormAsync()); + + AddCommand = new DelegateCommand(async () => await ShowAddDialog()); + + EditCommand = new DelegateCommand(async (user) => await ShowEditDialog(user)); + + StatusToggleCommand = new DelegateCommand(async (user) => await ToggleStatus(user)); + + //RowSelectionChangedCommand = new DelegateCommand(OnRowSelectionChanged); + + // 监听数据变化 + //PaginationDataGridViewModel.PropertyChanged += OnPaginationDataChanged; + //_ = PaginationDataGridViewModel.LoadDataAsync(); // 默认加载第一页数据 + + //_dataList = GetDemoDataList(10); + + // 启动异步初始化,但不等待 + _ = InitializeAsync(); + + // Jeecg 同构用户表同步完成后,自动刷新当前列表 + _jeecgSyncToken = _eventAggregator + .GetEvent() + .Subscribe(async _ => + { + await PaginationDataGridViewModel.LoadDataAsync(); + }, ThreadOption.UIThread); + } + + private async Task InitializeAsync() + { + try + { + // 不阻塞UI线程,让UI先渲染(效果就是先看到界面,然后再加载表格数据,提升用户体验) + await UIHelper.WaitForRenderAsync(); + + // 然后异步加载数据 + await PaginationDataGridViewModel.LoadDataAsync(); + } + catch (Exception ex) + { + Debug.WriteLine($"初始化失败: {ex.Message}"); + } + } + + protected override void CleanUp() + { + base.CleanUp(); + if (_jeecgSyncToken != null) + { + _eventAggregator.GetEvent().Unsubscribe(_jeecgSyncToken); + _jeecgSyncToken = null; + } + } + + // 行选择改变事件处理 + //private void OnRowSelectionChanged(UserOutput user) + //{ + // if (user == null) return; + + // user.IsSelected = !user.IsSelected; + + // // 更新选中数量 + // UpdateSelectedCount(); + + // // 更新全选状态 + // UpdateSelectAllStatus(); + + // // 更新批量删除命令状态 + // BatchDeleteCommand.RaiseCanExecuteChanged(); + + // // 调试输出 + // //System.Diagnostics.Debug.WriteLine($"用户 {user.UserName} 选中状态: {user.IsSelected}"); + //} + + + //private void OnPaginationDataChanged(object sender, PropertyChangedEventArgs e) + //{ + // if (e.PropertyName == nameof(PaginationDataGridViewModel.Data)) + // { + // RegisterSelectionEvents(); + // UpdateSelectedCount(); + // } + //} + + // 注册选择事件监听 + //private void RegisterSelectionEvents() + //{ + // if (PaginationDataGridViewModel.Data == null) return; + + // foreach (var user in PaginationDataGridViewModel.Data.OfType()) + // { + // user.SelectionChanged -= OnUserSelectionChanged; + // user.SelectionChanged += OnUserSelectionChanged; + // } + //} + + //private void OnUserSelectionChanged(object sender, EventArgs e) + //{ + // UpdateSelectedCount(); + // // 更新全选状态 + // UpdateSelectAllStatus(); + // BatchDeleteCommand.RaiseCanExecuteChanged(); + //} + + + public void UpdateSelectionState() + { + UpdateSelectedCount(); + //UpdateSelectAllStatus(); + BatchDeleteCommand.RaiseCanExecuteChanged(); + } + + // 更新选中数量 + private void UpdateSelectedCount() + { + if (PaginationDataGridViewModel.Data == null) + { + SelectedCount = 0; + return; + } + + SelectedCount = PaginationDataGridViewModel.Data.Count(user => + user is UserOutput output && output.IsSelected); + } + + // 获取选中的用户ID列表 + private List GetSelectedUserIds() + { + if (PaginationDataGridViewModel.Data == null) + return new List(); + + return PaginationDataGridViewModel.Data + .Where(user => user is UserOutput output && output.IsSelected) + .Select(user => (user as UserOutput).Id) + .ToList(); + } + + // 批量删除执行方法 + private async Task BatchDelete() + { + var selectedUserIds = GetSelectedUserIds(); + if (selectedUserIds.Count == 0) return; + + var messageBoxResult = HandyControl.Controls.MessageBox.Show( + $"确定要删除选中的 {selectedUserIds.Count} 个用户吗?此操作不可恢复!", + "确认删除", + MessageBoxButton.OKCancel, + MessageBoxImage.Question); + + if (messageBoxResult != MessageBoxResult.OK) return; + + int count = await _sysUserService.BatchDeleteAsync(selectedUserIds); + + if (count > 0) + { + PaginationDataGridViewModel.PageIndex = 1; + await PaginationDataGridViewModel.LoadDataAsync(); + } + + // 1. 准备传递给对话框的参数 + //var parameters = new DialogParameters(); + //parameters.Add("Title", "操作确认"); + //parameters.Add("Message", "确定要执行此操作吗?"); + //parameters.Add("ConfirmButtonText", "确认"); // 可选:自定义确认按钮文本 + //parameters.Add("CancelButtonText", "取消"); // 可选:自定义取消按钮文本 + + // 2. 显示对话框并处理返回结果 + //_dialogService.ShowDialog( + // "ConfirmDialog", // 注册时使用的对话框名称 + // parameters, + // result => + // { + // // 3. 根据对话框返回结果执行后续逻辑 + // if (result.Result == ButtonResult.OK) + // { + // // 用户点击了确认按钮 + // //ExecuteConfirmedAction(); + // } + // else if (result.Result == ButtonResult.Cancel) + // { + // // 用户点击了取消按钮 + // //ExecuteCancelledAction(); + // } + // } + //); + } + + // 删除执行方法 + private async Task Delete(long id) + { + var messageBoxResult = HandyControl.Controls.MessageBox.Show( + $"确定删除ID为 {id} 的用户吗?此操作不可恢复!", + "确认删除", + MessageBoxButton.OKCancel, + MessageBoxImage.Question, + MessageBoxResult.No); + + if (messageBoxResult != MessageBoxResult.OK) return; + + int count = await _sysUserService.DeleteAsync(id); + + if (count > 0) + { + PaginationDataGridViewModel.PageIndex = 1; + await PaginationDataGridViewModel.LoadDataAsync(); + } + } + + private async Task<(IEnumerable data, int totalCount)> FetchUsersAsync() + { + if (UserInput.EndTime.HasValue && UserInput.EndTime.HasValue && UserInput.EndTime < UserInput.BeginTime) + { + HandyControl.Controls.MessageBox.Error("开始时间不能大于结束时间!"); + //return (PaginationDataGridViewModel.Data, PaginationDataGridViewModel.TotalCount); + throw new OperationCanceledException("时间条件不合法,取消查询"); + } + UserInput.Page = PaginationDataGridViewModel.PageIndex; + UserInput.PageSize = PaginationDataGridViewModel.DataCountPerPage; + + // 延迟10秒 + //await Task.Delay(10000); + + var rlt = await _sysUserService.PageAsync(UserInput); + return (rlt.Items, rlt.Total); // 返回分页数据和总条目数 + } + + private async Task ReadAsync() + { + PaginationDataGridViewModel.PageIndex = 1; + await PaginationDataGridViewModel.LoadDataAsync(); + + } + + public async Task ResetFormAsync() + { + UserInput = new PageUserInput(); + + // 立即更新UI + await UIHelper.WaitForRenderAsync(); + + await ReadAsync(); + } + + // 显示新增对话框 + private async Task ShowAddDialog() + { + try + { + //var vm = new UserEditDialogViewModel(_sysUserService, _container); + //var view = new UserEditDialogView { DataContext = vm }; + + var result = await HandyControl.Controls.Dialog.Show() + .Initialize(v => v.InitializeForAdd()) + .GetResultAsync(); + // 使用泛型方式,通过 Initialize 方法设置数据 + //var result = await HandyControl.Controls.Dialog.Show() + // .Initialize(vm => + // { + // vm.InitializeForAdd(); + // }) + // .GetResultAsync(); + + if (result) + { + await PaginationDataGridViewModel.LoadDataAsync(); + //Growl.Success("用户新增成功!"); + } + } + catch (Exception ex) + { + Growl.Error($"打开对话框失败:{ex.Message}"); + } + } + + // 显示编辑对话框 + private async Task ShowEditDialog(UserOutput user) + { + if (user == null) return; + + try + { + var result = await HandyControl.Controls.Dialog.Show() + .Initialize(vm => + { + vm.InitializeForEdit(user); + }) + .GetResultAsync(); + + if (result) + { + await PaginationDataGridViewModel.LoadDataAsync(); + //Growl.Success("用户修改成功!"); + } + } + catch (Exception ex) + { + Growl.Error($"打开对话框失败:{ex.Message}"); + } + } + + private async Task ToggleStatus(UserOutput user) + { + if (user == null) return; + + // 保存原始状态以便回滚 + var originalStatus = user.Status; + try + { + // 先切换本地状态(提供即时反馈) + user.Status = originalStatus == StatusEnum.Enable + ? StatusEnum.Disable + : StatusEnum.Enable; + + SysUser sysUser = new SysUser(); + sysUser.Id = user.Id; + sysUser.Status = user.Status; + + int count = await _sysUserService.ToggleStatus(sysUser); + + if (count <= 0) + { + // 如果服务调用失败,回滚状态 + user.Status = originalStatus; + Growl.Warning("状态切换失败"); + } + } catch (Exception) + { + user.Status = originalStatus; + Growl.Warning("状态切换失败"); + } + } + + // 全选/取消全选方法 + //private void SelectAll(bool isSelected) + //{ + // if (PaginationDataGridViewModel?.Data == null) return; + + // foreach (var user in PaginationDataGridViewModel.Data.OfType()) + // { + // user.IsSelected = isSelected; + // } + + // // 更新选中数量 + // UpdateSelectedCount(); + //} + + // 更新全选状态(根据当前选中情况) + //public void UpdateSelectAllStatus() + //{ + // if (PaginationDataGridViewModel?.Data == null || !PaginationDataGridViewModel.Data.Any()) + // { + // IsAllSelected = false; + // return; + // } + + // var selectedCount = PaginationDataGridViewModel.Data.Count(user => + // user is UserOutput output && output.IsSelected); + // var totalCount = PaginationDataGridViewModel.Data.Count; + + // if (selectedCount == 0) + // { + // IsAllSelected = false; + // } + // else if (selectedCount == totalCount) + // { + // IsAllSelected = true; + // } + // else + // { + // IsAllSelected = null; // 部分选中状态 + // } + + //} + + //private string _searchText; + //public string SearchText + //{ + // get => _searchText; + // set + // { + // if (SetProperty(ref _searchText, value)) + // { + // Debug.WriteLine($"SearchText Updated: {value}"); + // FilterItems(value); + // } + // } + //} + + //private DemoDataModel? _selectedItem; + //public DemoDataModel? SelectedItem + //{ + // get => _selectedItem; + // set + // { + // if (SetProperty(ref _selectedItem, value)) + // { + // Debug.WriteLine($"SelectedItem Updated: {value}"); + // } + // } + //} + + //// 修改 Items 属性为 ManualObservableCollection + //private ManualObservableCollection _items = new(); + //public ManualObservableCollection Items + //{ + // get => _items; + // set => SetProperty(ref _items, value); + //} + + //private readonly List _dataList; + + //private void FilterItems(string key) + //{ + // //Items.CanNotify = false; + + // Items.Clear(); + + // foreach (var data in _dataList) + // { + // if (data.Name.ToLower().Contains(key.ToLower())) + // { + // Items.Add(data); + // } + // } + + // //RaisePropertyChanged(nameof(Items)); + // //Items.CanNotify = true; + //} + //List GetDemoDataList(int count) + //{ + // var list = new List(); + // for (var i = 1; i <= count; i++) + // { + // var index = i % 6 + 1; + // var model = new DemoDataModel + // { + // Index = i, + // IsSelected = i % 2 == 0, + // Name = $"Name{i}", + // Type = (DemoType)index, + // ImgPath = $"/HandyControlDemo;component/Resources/Img/Avatar/avatar{index}.png", + // Remark = new string(i.ToString()[0], 10) + // }; + // list.Add(model); + // } + + // return list; + //} + } + + + // public class DemoDataModel + //{ + // public int Index { get; set; } + + // public string Name { get; set; } = string.Empty; + + // public bool IsSelected { get; set; } + + // public string Remark { get; set; } = string.Empty; + + // public DemoType Type { get; set; } + + // public string ImgPath { get; set; } = string.Empty; + + // public List DataList { get; set; } = []; + //} + + //public enum DemoType + //{ + // Type1 = 1, + // Type2, + // Type3, + // Type4, + // Type5, + // Type6 + //} + +} + diff --git a/yy-admin-master/YY.Admin/Views/Control/MenuTreeView.xaml b/yy-admin-master/YY.Admin/Views/Control/MenuTreeView.xaml new file mode 100644 index 0000000..a8f249e --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/Control/MenuTreeView.xaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + diff --git a/yy-admin-master/YY.Admin/Views/Control/MenuTreeView.xaml.cs b/yy-admin-master/YY.Admin/Views/Control/MenuTreeView.xaml.cs new file mode 100644 index 0000000..a96770d --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/Control/MenuTreeView.xaml.cs @@ -0,0 +1,22 @@ +using System.Windows.Controls; +using System.Windows.Input; + +namespace YY.Admin.Views.Control +{ + /// + /// Interaction logic for MenuTreeView.xaml + /// + public partial class MenuTreeView : UserControl + { + public MenuTreeView() + { + InitializeComponent(); + } + + private void TreeViewItem_MouseDoubleClick(object sender, MouseButtonEventArgs e) + { + // 阻止双击默认展开行为 + e.Handled = true; + } + } +} diff --git a/yy-admin-master/YY.Admin/Views/Control/PaginationDataGridControl.xaml b/yy-admin-master/YY.Admin/Views/Control/PaginationDataGridControl.xaml new file mode 100644 index 0000000..d285e7a --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/Control/PaginationDataGridControl.xaml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/yy-admin-master/YY.Admin/Views/Control/PaginationDataGridControl.xaml.cs b/yy-admin-master/YY.Admin/Views/Control/PaginationDataGridControl.xaml.cs new file mode 100644 index 0000000..8d44123 --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/Control/PaginationDataGridControl.xaml.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; +using System.Windows.Threading; +using YY.Admin.Core; +using YY.Admin.Services.Service; + +namespace YY.Admin.Views.Control +{ + /// + /// PaginationDataGridControl.xaml 的交互逻辑 + /// + public partial class PaginationDataGridControl : UserControl + { + public PaginationDataGridControl() + { + InitializeComponent(); + //this.Loaded += PaginationDataGridControl_Loaded; + } + + // 确保用户在控制加载时触发列生成 + //private void PaginationDataGridControl_Loaded(object sender, RoutedEventArgs e) + //{ + // // 确保 DataGrid 已完全加载 + // Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() => + // { + // // 确保 DataGrid 控件已初始化并且 ItemsSource 不为 null + // var data = DataGrid.ItemsSource; + // if (data == null) + // { + // return; + // } + + // // 获取数据模型的类型 + // var type = data.GetType().GetGenericArguments().FirstOrDefault(); + // if (type == null) return; + + // // 获取所有带有 BindDescriptionAttribute 的属性 + // var properties = type.GetProperties() + // .Where(prop => Attribute.IsDefined(prop, typeof(BindDescriptionAttribute))) + // .OrderBy(prop => ((BindDescriptionAttribute)prop.GetCustomAttribute(typeof(BindDescriptionAttribute))).DisplayIndex) + // .ToList(); + + // foreach (var prop in properties) + // { + // var attribute = prop.GetCustomAttribute(); + // if (attribute != null) + // { + // // 检查列是否已经存在 + // if (!ColumnExists(attribute.HeaderName)) + // { + // var column = CreateDataGridColumn(attribute, prop); + // if (column != null) + // { + // // 通过绑定添加列 + // DataGrid.Columns.Add(column); + // } + // } + // } + // } + // })); + //} + + //// 检查列是否已经存在 + //private bool ColumnExists(string headerName) + //{ + // return DataGrid.Columns.Any(c => c.Header.ToString() == headerName); + //} + + //private DataGridColumn CreateDataGridColumn(BindDescriptionAttribute attribute, PropertyInfo property) + //{ + // DataGridColumn column = null; + + // // 根据属性的显示方式来创建不同的列 + // switch (attribute.ShowAs) + // { + // case ShowScheme.普通文本: + // column = new DataGridTextColumn + // { + // Header = attribute.HeaderName, + // Binding = new Binding(property.Name), + // Width = attribute.Width, + // DisplayIndex = attribute.DisplayIndex + // }; + // break; + + // case ShowScheme.自定义: + // // 如果需要自定义列,您可以在这里扩展并处理自定义类型的列 + // break; + // } + + // return column; + //} + } +} diff --git a/yy-admin-master/YY.Admin/Views/Control/SidebarControl.xaml b/yy-admin-master/YY.Admin/Views/Control/SidebarControl.xaml new file mode 100644 index 0000000..b19db7e --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/Control/SidebarControl.xaml @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/yy-admin-master/YY.Admin/Views/Control/SidebarControl.xaml.cs b/yy-admin-master/YY.Admin/Views/Control/SidebarControl.xaml.cs new file mode 100644 index 0000000..2e57fd5 --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/Control/SidebarControl.xaml.cs @@ -0,0 +1,37 @@ +using System.Windows.Controls; +using System.Windows.Input; +using YY.Admin.Module; + +namespace YY.Admin.Views.Control +{ + /// + /// Interaction logic for Sidebar.xaml + /// + public partial class SidebarControl : UserControl + { + public SidebarControl() + { + InitializeComponent(); + } + private void OnNavItemMouseDown(object sender, MouseButtonEventArgs e) + { + if (sender is ListBoxItem item && item.DataContext is NavItem navItem) + { + // 执行命令(如果有) + if (navItem.Command?.CanExecute(navItem) == true) + { + navItem.Command.Execute(navItem); + } + + // 如果不可选,阻止选中 + if (!navItem.IsActive) + { + // 阻止 ListBox 自动选择该项 + e.Handled = true; + return; + } + } + } + + } +} diff --git a/yy-admin-master/YY.Admin/Views/Control/TabContentView.xaml b/yy-admin-master/YY.Admin/Views/Control/TabContentView.xaml new file mode 100644 index 0000000..b227df1 --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/Control/TabContentView.xaml @@ -0,0 +1,9 @@ + + + + + + diff --git a/yy-admin-master/YY.Admin/Views/Control/TabContentView.xaml.cs b/yy-admin-master/YY.Admin/Views/Control/TabContentView.xaml.cs new file mode 100644 index 0000000..a65bdcb --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/Control/TabContentView.xaml.cs @@ -0,0 +1,371 @@ +//using Prism.Ioc; +//using System; +//using System.Windows; +//using System.Windows.Controls; +//using System.Windows.Threading; + +//namespace YY.Admin.Views.Control +//{ +// /// +// /// TabContentView.xaml 的交互逻辑 +// /// +// public partial class TabContentView : UserControl +// { +// public TabContentView() +// { +// InitializeComponent(); +// this.Loaded += TabContentView_Loaded; +// this.Unloaded += TabContentView_Unloaded; +// } + +// private bool _navigated = false; + +// private void TabContentView_Loaded(object sender, RoutedEventArgs e) +// { +// try +// { +// if (string.IsNullOrWhiteSpace(RegionName)) +// return; + +// var regionManager = ContainerLocator.Current.Resolve(); + +// // 把内部 ContentControl 注册为 region(基于 RegionName) +// RegionManager.SetRegionName(PART_ContentHost, RegionName); +// RegionManager.SetRegionManager(PART_ContentHost, regionManager); + +// // 延迟导航,确保 region 真正注册到 RegionManager 并且模板生成为止 +// if (!_navigated && !string.IsNullOrEmpty(ViewName)) +// { +// Dispatcher.BeginInvoke(new Action(() => +// { +// try +// { +// // 再次检查 region 是否存在并执行导航 +// if (regionManager.Regions.ContainsRegionWithName(RegionName)) +// { +// regionManager.RequestNavigate(RegionName, ViewName); +// } +// else +// { +// // 如果 region 仍然不存在,尝试 RequestNavigate(Prism 通常会创建 region) +// regionManager.RequestNavigate(RegionName, ViewName); +// } +// } +// catch (Exception ex) +// { +// System.Diagnostics.Debug.WriteLine($"TabContentView 导航异常: {ex.Message}"); +// } +// }), DispatcherPriority.Background); + +// _navigated = true; +// } +// } +// catch (Exception ex) +// { +// System.Diagnostics.Debug.WriteLine($"TabContentView_Loaded 出错: {ex.Message}"); +// } +// } + +// private void TabContentView_Unloaded(object sender, RoutedEventArgs e) +// { +// try +// { +// if (!string.IsNullOrWhiteSpace(RegionName)) +// { +// var regionManager = ContainerLocator.Current.Resolve(); +// if (regionManager.Regions.ContainsRegionWithName(RegionName)) +// { +// var region = regionManager.Regions[RegionName]; +// region.RemoveAll(); +// regionManager.Regions.Remove(RegionName); +// } +// } +// } +// catch (Exception ex) +// { +// System.Diagnostics.Debug.WriteLine($"TabContentView_Unloaded 清理异常: {ex.Message}"); +// } +// } + +// #region RegionName DP +// public static readonly DependencyProperty RegionNameProperty = +// DependencyProperty.Register(nameof(RegionName), typeof(string), typeof(TabContentView), new PropertyMetadata(null)); + +// public string RegionName +// { +// get => (string)GetValue(RegionNameProperty); +// set => SetValue(RegionNameProperty, value); +// } +// #endregion + +// #region ViewName DP +// public static readonly DependencyProperty ViewNameProperty = +// DependencyProperty.Register(nameof(ViewName), typeof(string), typeof(TabContentView), new PropertyMetadata(null)); + +// public string ViewName +// { +// get => (string)GetValue(ViewNameProperty); +// set => SetValue(ViewNameProperty, value); +// } +// #endregion +// } +//} +using Prism.Ioc; +using System; +using System.Diagnostics; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Threading; + +namespace YY.Admin.Views.Control +{ + public partial class TabContentView : UserControl + { + public TabContentView() + { + InitializeComponent(); + + this.Loaded += TabContentView_Loaded; + this.Unloaded += TabContentView_Unloaded; + this.IsVisibleChanged += TabContentView_IsVisibleChanged; + this.DataContextChanged += TabContentView_DataContextChanged; + } + + private bool _navigated = false; + private IRegionManager _regionManager; + private DispatcherOperation _pendingNavigationOp; + + #region DPs + public static readonly DependencyProperty RegionNameProperty = + DependencyProperty.Register(nameof(RegionName), typeof(string), typeof(TabContentView), new PropertyMetadata(null, OnRegionOrViewNameChanged)); + + public string RegionName + { + get => (string)GetValue(RegionNameProperty); + set => SetValue(RegionNameProperty, value); + } + + public static readonly DependencyProperty ViewNameProperty = + DependencyProperty.Register(nameof(ViewName), typeof(string), typeof(TabContentView), new PropertyMetadata(null, OnRegionOrViewNameChanged)); + + public string ViewName + { + get => (string)GetValue(ViewNameProperty); + set => SetValue(ViewNameProperty, value); + } + #endregion + + private void TabContentView_Loaded(object sender, RoutedEventArgs e) + { + try + { + if (string.IsNullOrWhiteSpace(RegionName)) + return; + + _regionManager = ContainerLocator.Current.Resolve(); + + // 确保 region/manager 设置在后续导航前准备好(但不要重复注册 region 这里) + // We will call PrepareRegionForRegistration inside EnsureNavigateIfNeeded before actual registration. + EnsureNavigateIfNeeded(); + } + catch (Exception ex) + { + Debug.WriteLine($"TabContentView_Loaded 出错: {ex.Message}"); + } + } + + private void TabContentView_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) + => EnsureNavigateIfNeeded(); + + private void TabContentView_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e) + => EnsureNavigateIfNeeded(); + + private void EnsureNavigateIfNeeded() + { + try + { + if (_navigated) + return; + + if (string.IsNullOrWhiteSpace(RegionName) || string.IsNullOrWhiteSpace(ViewName)) + return; + + if (!IsLoaded || !IsVisible) + return; + + if (_regionManager == null) + _regionManager = ContainerLocator.Current.Resolve(); + + // 在注册 region 之前准备 host(清理旧 region 与清空 Content),避免 Prism 抛 Content 非空异常 + PrepareRegionForRegistration(); + + // 取消之前挂起的导航(保证最后一次胜出) + if (_pendingNavigationOp != null && _pendingNavigationOp.Status == DispatcherOperationStatus.Pending) + { + Debug.WriteLine($"取消挂起导航: region={RegionName}, view={ViewName}"); + _pendingNavigationOp.Abort(); + _pendingNavigationOp = null; + } + + var desiredView = ViewName; + var desiredRegion = RegionName; + + _pendingNavigationOp = Dispatcher.BeginInvoke(new Action(() => + { + try + { + if (_regionManager == null) + _regionManager = ContainerLocator.Current.Resolve(); + + // 现在把 PART_ContentHost 注册为一个 region(设置 RegionName/RegionManager) + // 注意:如果前面清理正确,这里不会再触发 ContentControl already has content 错误 + RegionManager.SetRegionName(PART_ContentHost, desiredRegion); + RegionManager.SetRegionManager(PART_ContentHost, _regionManager); + + Debug.WriteLine($"开始 RequestNavigate -> region={desiredRegion}, view={desiredView}"); + + _regionManager.RequestNavigate(desiredRegion, desiredView, result => + { + try + { + if (result.Success == true) + { + _navigated = true; + Debug.WriteLine($"导航成功: region={desiredRegion}, view={desiredView}"); + } + else + { + _navigated = false; + Debug.WriteLine($"导航失败: region={desiredRegion}, view={desiredView}, error={result.Exception?.Message}"); + } + } + catch (Exception ex) + { + _navigated = false; + Debug.WriteLine($"导航回调异常: {ex.Message}"); + } + }); + } + catch (Exception ex) + { + Debug.WriteLine($"调度导航异常: {ex.Message}"); + } + finally + { + _pendingNavigationOp = null; + } + }), DispatcherPriority.Background); + + Debug.WriteLine($"调度导航: Region={RegionName}, View={ViewName}, Loaded={IsLoaded}, Visible={IsVisible}, navigated={_navigated}"); + } + catch (Exception ex) + { + Debug.WriteLine($"EnsureNavigateIfNeeded 异常: {ex.Message}"); + } + } + + /// + /// 在注册 region 之前清理宿主控件与 RegionManager 中可能残留的 region。 + /// 这一步是防止 Prism 在 adapt 时因为 ContentControl.Content 非空而抛异常。 + /// + private void PrepareRegionForRegistration() + { + try + { + // 如果 RegionManager 中已存在同名 region,则先移除它(并 RemoveAll 内容) + if (_regionManager != null && _regionManager.Regions.ContainsRegionWithName(RegionName)) + { + try + { + var existing = _regionManager.Regions[RegionName]; + existing.RemoveAll(); + _regionManager.Regions.Remove(RegionName); + Debug.WriteLine($"已移除旧 region: {RegionName}"); + } + catch (Exception ex) + { + Debug.WriteLine($"移除旧 region 发生异常: {ex.Message}"); + } + } + + // 如果宿主 ContentControl 已经有内容,清空它(很关键) + if (PART_ContentHost != null && PART_ContentHost.Content != null) + { + Debug.WriteLine($"清空 PART_ContentHost.Content (原内容类型: {PART_ContentHost.Content.GetType().Name})"); + PART_ContentHost.Content = null; + } + } + catch (Exception ex) + { + Debug.WriteLine($"PrepareRegionForRegistration 异常: {ex.Message}"); + } + } + + private void TabContentView_Unloaded(object sender, RoutedEventArgs e) + { + try + { + // 取消挂起导航 + if (_pendingNavigationOp != null && _pendingNavigationOp.Status == DispatcherOperationStatus.Pending) + { + _pendingNavigationOp.Abort(); + _pendingNavigationOp = null; + } + + // 清理 region 与宿主内容 + if (!string.IsNullOrWhiteSpace(RegionName)) + { + var rm = _regionManager ?? ContainerLocator.Current.Resolve(); + try + { + if (rm != null && rm.Regions.ContainsRegionWithName(RegionName)) + { + var region = rm.Regions[RegionName]; + region.RemoveAll(); + rm.Regions.Remove(RegionName); + Debug.WriteLine($"卸载并移除 region: {RegionName}"); + } + } + catch (Exception ex) + { + Debug.WriteLine($"TabContentView_Unloaded 清理 region 异常: {ex.Message}"); + } + } + + if (PART_ContentHost != null && PART_ContentHost.Content != null) + { + PART_ContentHost.Content = null; + Debug.WriteLine($"Unloaded: 清空 PART_ContentHost.Content"); + } + } + catch (Exception ex) + { + Debug.WriteLine($"TabContentView_Unloaded 清理异常: {ex.Message}"); + } + finally + { + _navigated = false; + } + } + + private static void OnRegionOrViewNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TabContentView tc) + { + // 新参数到来,允许再次导航 + tc._navigated = false; + + // 取消旧挂起导航 + if (tc._pendingNavigationOp != null && tc._pendingNavigationOp.Status == DispatcherOperationStatus.Pending) + { + tc._pendingNavigationOp.Abort(); + tc._pendingNavigationOp = null; + } + + // 尝试导航(这一调用会在 Loaded/可见时真正执行) + tc.EnsureNavigateIfNeeded(); + } + } + } +} + diff --git a/yy-admin-master/YY.Admin/Views/DashboardView.xaml b/yy-admin-master/YY.Admin/Views/DashboardView.xaml new file mode 100644 index 0000000..fe3e74d --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/DashboardView.xaml @@ -0,0 +1,361 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/yy-admin-master/YY.Admin/Views/DashboardView.xaml.cs b/yy-admin-master/YY.Admin/Views/DashboardView.xaml.cs new file mode 100644 index 0000000..f746674 --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/DashboardView.xaml.cs @@ -0,0 +1,17 @@ + +using System.Windows.Controls; + +namespace YY.Admin.Views +{ + /// + /// DashboardView.xaml 的交互逻辑 + /// + public partial class DashboardView : UserControl + { + public DashboardView() + { + InitializeComponent(); + } + + } +} diff --git a/yy-admin-master/YY.Admin/Views/Dialogs/AlertDialogView.xaml b/yy-admin-master/YY.Admin/Views/Dialogs/AlertDialogView.xaml new file mode 100644 index 0000000..72cd66e --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/Dialogs/AlertDialogView.xaml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/yy-admin-master/YY.Admin/Views/Dialogs/ServerSettingsDialogView.xaml.cs b/yy-admin-master/YY.Admin/Views/Dialogs/ServerSettingsDialogView.xaml.cs new file mode 100644 index 0000000..d5e2b3d --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/Dialogs/ServerSettingsDialogView.xaml.cs @@ -0,0 +1,15 @@ +using System.Windows.Controls; + +namespace YY.Admin.Views.Dialogs +{ + /// + /// ServerSettingsDialogView.xaml 的交互逻辑 + /// + public partial class ServerSettingsDialogView : UserControl + { + public ServerSettingsDialogView() + { + InitializeComponent(); + } + } +} diff --git a/yy-admin-master/YY.Admin/Views/Dialogs/SuccessDialogView.xaml b/yy-admin-master/YY.Admin/Views/Dialogs/SuccessDialogView.xaml new file mode 100644 index 0000000..ee6fdba --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/Dialogs/SuccessDialogView.xaml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/yy-admin-master/YY.Admin/Views/LoginWindow.xaml.cs b/yy-admin-master/YY.Admin/Views/LoginWindow.xaml.cs new file mode 100644 index 0000000..3f5b6c6 --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/LoginWindow.xaml.cs @@ -0,0 +1,124 @@ +using Npgsql.Replication.PgOutput.Messages; +using System.Windows; +using System.Windows.Input; +using YY.Admin.Core.FluentValidation; +using YY.Admin.Services.Service.AutoUpdate; +using YY.Admin.ViewModels; +using Window = HandyControl.Controls.Window; +namespace YY.Admin.Views +{ + /// + /// LoginWindow.xaml 的交互逻辑 + /// + public partial class LoginWindow : Window + { + private readonly IAutoUpdateService _autoUpdateService; + public LoginWindow(IAutoUpdateService autoUpdateService) + { + InitializeComponent(); + _autoUpdateService = autoUpdateService; + // 窗口加载完成后检查更新 + Loaded += async (s, e) => await CheckForUpdatesAsync(); + // 当 DataContext 设置后附加验证 + DataContextChanged += OnDataContextChanged; + } + private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e) + { + if (e.NewValue != null) + { + // 附加验证到 UI 控件 + FluentValidationHelper.Attach(LoginForm, typeof(LoginWindowViewModel)); + } + } + private async Task CheckForUpdatesAsync() + { + try + { + // 延迟一点时间,确保UI已经加载完成 + await Task.Delay(1000); + + var hasUpdate = await _autoUpdateService.CheckForUpdatesAsync(); + if (hasUpdate) + { + await ShowUpdateDialogAsync(); + } + } + catch (Exception ex) + { + // 更新检查失败不影响登录 + System.Diagnostics.Debug.WriteLine($"更新检查异常: {ex.Message}"); + } + } + private async Task ShowUpdateDialogAsync() + { + await Dispatcher.InvokeAsync(async () => + { + // 检查窗口是否仍然打开 + if (!IsLoaded || !IsVisible || Visibility == Visibility.Collapsed) + { + return; + } + var versionInfo =await _autoUpdateService.GetVersionInfo(); + if (versionInfo == null) return; + + var currentVersion = _autoUpdateService.GetCurrentVersion(); + + var updateWindow = new UpdateWindow + { + CurrentVersion = currentVersion, + LatestVersion = versionInfo.LatestVersion, + PublishDate = versionInfo.PublishDate, + Changelog = versionInfo.Changelog, + DownloadUrl = versionInfo.DownloadUrl, + ApplicationName = versionInfo.ApplicationName, + IsMandatory = versionInfo.Mandatory, + Owner = this + }; + + updateWindow.UpdateRequested += (downloadUrl) => + { + try + { + _autoUpdateService.StartUpdate(downloadUrl); + Application.Current.Shutdown(); + } + catch (Exception ex) + { + MessageBox.Show($"更新启动失败: {ex.Message}", "更新错误", + MessageBoxButton.OK, MessageBoxImage.Error); + } + }; + + updateWindow.ShowDialog(); + }); + } + private void LoginWindow_Closed(object? sender, EventArgs e) + { + Application.Current.Shutdown(); + } + + private void Window_KeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Enter) + { + Submit_Click(sender, e); + } + } + + private void Submit_Click(object sender, RoutedEventArgs e) + { + var vm = (LoginWindowViewModel)DataContext; + vm.LoginMessage = string.Empty; + bool valid = FluentValidationHelper.ValidateAll(LoginForm, vm.LoginInput!, vm.LoginInputValidator); + if (valid) + { + if (vm.LoginCommand.CanExecute()) + { + vm.LoginCommand.Execute(); + } + } + // 阻止事件继续冒泡 + e.Handled = true; + } + } +} diff --git a/yy-admin-master/YY.Admin/Views/MainWindow.xaml b/yy-admin-master/YY.Admin/Views/MainWindow.xaml new file mode 100644 index 0000000..8a4465d --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/MainWindow.xaml @@ -0,0 +1,597 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/yy-admin-master/YY.Admin/Views/MainWindow.xaml.cs b/yy-admin-master/YY.Admin/Views/MainWindow.xaml.cs new file mode 100644 index 0000000..6652b1e --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/MainWindow.xaml.cs @@ -0,0 +1,108 @@ +using HandyControl.Data; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using YY.Admin.ViewModels; +using Window = HandyControl.Controls.Window; + +namespace YY.Admin.Views +{ + /// + /// Interaction logic for MainWindow.xaml + /// + public partial class MainWindow : Window + { + public MainWindow() + { + InitializeComponent(); + //this.Closed += MainWindow_Closed; + ContentScrollViewer.SizeChanged += OnScrollViewerSizeChanged; + } + + private void MainWindow_Closed(object? sender, EventArgs e) + { + Application.Current.Shutdown(); + } + + private void UserMenuButton_Click(object sender, RoutedEventArgs e) + { + if (sender is Button button && button.ContextMenu != null) + { + button.ContextMenu.PlacementTarget = button; + button.ContextMenu.IsOpen = true; + } + } + + private void Splitter_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e) + { + double newWidth = LeftMenuTree.Width + e.HorizontalChange; + + // 获取主窗口宽度 + double windowWidth = this.ActualWidth; + + // 最小宽度 + double minWidth = 50; + + // 最大宽度 + double maxWidth = windowWidth - LeftSidebar.Width - GridSplitter.Width - minWidth; + + if (newWidth < minWidth) + newWidth = minWidth; + else if (newWidth > maxWidth) + newWidth = maxWidth; + + LeftMenuTree.Width = newWidth; + } + + private void TabControl_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e) + { + // 查找点击点是否在 TabItem Header 上 + var dep = e.OriginalSource as DependencyObject; + + while (dep != null && dep is not TabItem) + { + dep = VisualTreeHelper.GetParent(dep); + } + + // 如果右键点在 TabItem 上 + if (dep is TabItem) + { + // 阻止 TabControl 切换 SelectedItem + e.Handled = true; + } + } + + private void OnScrollViewerSizeChanged(object sender, SizeChangedEventArgs e) + { + // 检查是否需要显示滚动条 + //bool needsScroll = ContentPanel.ActualHeight > ContentScrollViewer.ActualHeight; + + //bool needsScroll = ContentScrollViewer.ScrollableHeight > 0; + + bool needsScroll = ContentScrollViewer.ComputedVerticalScrollBarVisibility == Visibility.Visible; + + if (needsScroll) + { + // 有滚动条:显示固定按钮,隐藏内部按钮 + FixedButton.Visibility = Visibility.Visible; + InnerButton.Visibility = Visibility.Collapsed; + } + else + { + // 无滚动条:隐藏固定按钮,显示内部按钮 + FixedButton.Visibility = Visibility.Collapsed; + InnerButton.Visibility = Visibility.Visible; + } + } + + private void OnSkinTypeChanged(object sender, RoutedEventArgs e) + { + if (e.OriginalSource is Button { Tag: SkinType skinType }) + { + var vm = DataContext as MainWindowViewModel; + vm?.AppSettingsViewModel?.SkinType = skinType; + } + } + } +} \ No newline at end of file diff --git a/yy-admin-master/YY.Admin/Views/NotFoundView.xaml b/yy-admin-master/YY.Admin/Views/NotFoundView.xaml new file mode 100644 index 0000000..52a7025 --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/NotFoundView.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + diff --git a/yy-admin-master/YY.Admin/Views/NotFoundView.xaml.cs b/yy-admin-master/YY.Admin/Views/NotFoundView.xaml.cs new file mode 100644 index 0000000..e456b75 --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/NotFoundView.xaml.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace YY.Admin.Views +{ + /// + /// NotFoundView.xaml 的交互逻辑 + /// + public partial class NotFoundView : UserControl + { + public NotFoundView() + { + InitializeComponent(); + } + } +} diff --git a/yy-admin-master/YY.Admin/Views/SysManage/DataDictionaryManagementView.xaml b/yy-admin-master/YY.Admin/Views/SysManage/DataDictionaryManagementView.xaml new file mode 100644 index 0000000..50d5f5e --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/SysManage/DataDictionaryManagementView.xaml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/yy-admin-master/YY.Admin/Views/SysManage/DataDictionaryManagementView.xaml.cs b/yy-admin-master/YY.Admin/Views/SysManage/DataDictionaryManagementView.xaml.cs new file mode 100644 index 0000000..f457509 --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/SysManage/DataDictionaryManagementView.xaml.cs @@ -0,0 +1,15 @@ +using System.Windows.Controls; + +namespace YY.Admin.Views.SysManage +{ + /// + /// DataDictionaryManagementView.xaml 的交互逻辑 + /// + public partial class DataDictionaryManagementView : UserControl + { + public DataDictionaryManagementView() + { + InitializeComponent(); + } + } +} diff --git a/yy-admin-master/YY.Admin/Views/SysManage/RoleManagementView.xaml b/yy-admin-master/YY.Admin/Views/SysManage/RoleManagementView.xaml new file mode 100644 index 0000000..d1e7d68 --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/SysManage/RoleManagementView.xaml @@ -0,0 +1,12 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/yy-admin-master/YY.Admin/Views/SysManage/TenantManagementView.xaml.cs b/yy-admin-master/YY.Admin/Views/SysManage/TenantManagementView.xaml.cs new file mode 100644 index 0000000..debaf2f --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/SysManage/TenantManagementView.xaml.cs @@ -0,0 +1,15 @@ +using System.Windows.Controls; + +namespace YY.Admin.Views.SysManage +{ + /// + /// TenantManagementView.xaml 的交互逻辑 + /// + public partial class TenantManagementView : UserControl + { + public TenantManagementView() + { + InitializeComponent(); + } + } +} diff --git a/yy-admin-master/YY.Admin/Views/SysManage/UserEditDialogView.xaml b/yy-admin-master/YY.Admin/Views/SysManage/UserEditDialogView.xaml new file mode 100644 index 0000000..48da3ed --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/SysManage/UserEditDialogView.xaml @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/yy-admin-master/YY.Admin/Views/SysManage/UserManagementView.xaml.cs b/yy-admin-master/YY.Admin/Views/SysManage/UserManagementView.xaml.cs new file mode 100644 index 0000000..e46fd2b --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/SysManage/UserManagementView.xaml.cs @@ -0,0 +1,87 @@ +using System.Windows; +using System.Windows.Controls; +using YY.Admin.Services.Service; +using YY.Admin.ViewModels.SysManage; + +namespace YY.Admin.Views.SysManage +{ + /// + /// UserManagementView.xaml 的交互逻辑 + /// + public partial class UserManagementView : UserControl + { + public UserManagementView() + { + InitializeComponent(); + //this.Loaded += UserManagementView_Loaded; + } + + //private void UserManagementView_Loaded(object sender, RoutedEventArgs e) + //{ + // if (DataContext is UserManagementViewModel viewModel) + // { + // Task.Run(viewModel.PaginationDataGridViewModel.LoadData); + // } + + // // 记得取消事件订阅,避免重复执行 + // this.Loaded -= UserManagementView_Loaded; + //} + + // 在 Window 或 UserControl 的代码后台 + private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (sender is DataGrid dataGrid && dataGrid.DataContext is UserManagementViewModel viewModel) + { + // 获取所有新增选中的项 + foreach (var addedItem in e.AddedItems) + { + if (addedItem is UserOutput user) + { + user.IsSelected = true; + } + } + + // 获取所有取消选中的项 + foreach (var removedItem in e.RemovedItems) + { + if (removedItem is UserOutput user) + { + user.IsSelected = false; + } + } + + // 通知 ViewModel 更新状态 + viewModel.UpdateSelectionState(); + } + } + + private async void ResetButton_Click(object sender, RoutedEventArgs e) + { + // 手动清除 DateTimePicker + // TO-DO HC DateTimePicker控件存在Bug,这里需要显示地清除,Bug已修复已提交PR + //BeginTimePicker.Text = string.Empty; + //EndTimePicker.Text = string.Empty; + + // 通过反射获取内部 TextBox + //var searchTextBoxField = typeof(AutoCompleteTextBox).GetField("_searchTextBox", + // System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + + //if (searchTextBoxField?.GetValue(AutoCompleteTextBox) is System.Windows.Controls.TextBox textBox) + //{ + // // 直接清空内部 TextBox + // textBox.Text = string.Empty; + //} + + //// 清空其他状态 + //AutoCompleteTextBox.SelectedItem = null; + //AutoCompleteTextBox.SelectedIndex = -1; + + + // 调用 ViewModel 的重置方法 + if (DataContext is UserManagementViewModel viewModel) + { + await viewModel.ResetFormAsync(); + } + } + } +} diff --git a/yy-admin-master/YY.Admin/Views/UpdateWindow.xaml b/yy-admin-master/YY.Admin/Views/UpdateWindow.xaml new file mode 100644 index 0000000..1b31ab6 --- /dev/null +++ b/yy-admin-master/YY.Admin/Views/UpdateWindow.xaml @@ -0,0 +1,267 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +