bag
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
# YhCore — .NET 5.0 底层框架搭建方案
|
||||
|
||||
> 参考项目:`E:\YHXT\java\bschclassmanage`(Spring Boot 2.7 + JPA + MySQL + Redis + Nacos)
|
||||
> 本地类库支撑:`E:\YHXT\net\Library`
|
||||
> 范围:**仅底层框架**(分层结构、基础设施、横切组件),不含任何业务逻辑
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
将现有 Java(Spring Boot 2.7)项目 `bschclassmanage` 的底层框架能力平移到 .NET 5.0,形成一套可复用的后端基础框架,供 `YhCore` 及后续 .NET 项目使用。
|
||||
|
||||
**目标:**
|
||||
|
||||
1. 保持与 Java 版一致的分层模型与开发习惯(Controller → Service → Repository → Entity),降低团队切换成本。
|
||||
2. 复用 `E:\YHXT\net\Library` 下已有的 DLL(日志、调度、文档处理、JSON 等),减少重复采购/引入。
|
||||
3. 提供统一的基础设施:配置管理、日志、异常处理、统一响应、用户上下文、操作日志 AOP、数据访问、缓存、任务调度、接口文档、鉴权。
|
||||
4. 只搭骨架,不写业务——框架落地后业务模块以"填空"方式接入。
|
||||
|
||||
---
|
||||
|
||||
## 2. Java 参考项目结构分析
|
||||
|
||||
`bschclassmanage` 的包结构即框架的职责划分,逐项映射如下:
|
||||
|
||||
| Java 包 / 组件 | 职责 | 关键实现 |
|
||||
| -------------------------------------------- | ------------------------------------------------ | ------------------------------- |
|
||||
| `controller` (api / mobiapi / oapi) | 接口入口,按渠道分三套 | Spring MVC |
|
||||
| `service` / `service.impl` | 业务服务层 | 接口 + 实现 |
|
||||
| `Repository` / `specification` | 数据访问层 + 动态查询 | Spring Data JPA + Specification |
|
||||
| `entity` | 数据库实体 | Hibernate 注解实体 |
|
||||
| `model` (dto / vo / excel / constant / tool) | 传输与视图模型 | POJO + ModelMapper 转换 |
|
||||
| `config` | 框架配置(Async / Redis / Swagger / WebMvc / File) | `@Configuration` |
|
||||
| `interceptor` | 用户上下文拦截器 | `UserContextInterceptor` |
|
||||
| `annotation` + `aspect` | 操作日志、同步日志 AOP | `@OperationLog` + AspectJ |
|
||||
| `scheduler` | 定时任务(升级、日志清理) | Spring `@Scheduled` |
|
||||
| `validation` | 自定义校验(密码等) | Hibernate Validator |
|
||||
| `utils` | 30+ 工具类(日期/分页/签名/加解压/Redis/HTTP…) | 静态工具类 |
|
||||
| 配置体系 | `bootstrap.yml`(Nacos) + `application-{env}.yml` | Nacos 配置中心,多 Profile |
|
||||
| 日志 | `logback-spring.xml` | Logback + Slf4j |
|
||||
|
||||
**中间件清单**(来自 pom.xml / application.yml):MySQL 8、Redis、RabbitMQ(已预留配置)、Nacos、CAS SSO(yh-auth)、应用间签名鉴权(yh-app-auth)、Swagger(SpringDoc)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 技术选型对照表(Java → .NET 5.0)
|
||||
|
||||
| 能力 | Java 实现 | .NET 5.0 方案 | 来源 |
|
||||
| -------- | -------------------------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| Web 框架 | Spring MVC | **ASP.NET Core 5.0 Web API** | 内置 |
|
||||
| 依赖注入 | Spring IoC | **内置 DI**(Autofac 可选,AOP 需要时引入) | 内置 / NuGet |
|
||||
| ORM | Spring Data JPA (Hibernate) | **EF Core 5.0** 为主;高频 SQL 用 **Dapper** | NuGet `Microsoft.EntityFrameworkCore.SqlServer 5.x` + Library `Dapper.Common.dll` |
|
||||
| 数据库 | mysql-connector-java 8.0.33(MySQL 8) | **主库 SQL Server**(`Microsoft.EntityFrameworkCore.SqlServer 5.x`);**MySQL 可切换**(`Pomelo.EntityFrameworkCore.MySql 5.x`,按 `DatabaseProvider` 配置切换) | NuGet |
|
||||
| 动态条件查询 | JPA Specification | EF Core `IQueryable` 链式拼装 + 自定义 Specification 模式 | 框架自研(轻量) |
|
||||
| 缓存 | spring-data-redis | **StackExchange.Redis**(封装 `RedisUtil` 对应 Java 版) | NuGet |
|
||||
| 配置中心 | Nacos + 多 Profile | **appsettings.{env}.json** 为基础;预留 **nacos-sdk-csharp** 接入点 | NuGet(可选) |
|
||||
| 日志 | Logback + Slf4j接口日志 | **log4net**(Library 已有),**仅用于接口访问日志**:单一中间件记录接口调用详情,其余日志(SQL/框架/业务 Debug)全部关闭 | Library |
|
||||
| JSON | fastjson / Jackson | **Newtonsoft.Json**(Library 已有,兼容旧接口格式) | Library |
|
||||
| 对象映射 | ModelMapper | **AutoMapper** | NuGet |
|
||||
| 参数校验 | Hibernate Validator | **FluentValidation**(或 DataAnnotations) | NuGet |
|
||||
| 接口文档 | SpringDoc OpenAPI | **Swashbuckle.AspNetCore 5.x** | NuGet |
|
||||
| 定时任务 | `@Scheduled` / scheduler 包 | **Quartz.NET 3.x**(Library 已有 `Quartz.dll`,需核版本)或 `BackgroundService` | Library |
|
||||
| AOP | AspectJ(`aspect` + `annotation`) | **ActionFilter + 自定义 Attribute**(操作日志);全局逻辑用 **Middleware** | 内置 |
|
||||
| 拦截器 | HandlerInterceptor | **Middleware / ActionFilter** | 内置 |
|
||||
| HTTP 客户端 | HttpClient / OkHttp | **IHttpClientFactory** | NuGet `Microsoft.Extensions.Http` |
|
||||
| Excel/文档 | easypoi (POI) | **Aspose.Cells / Aspose.Words**(Library 已有,打印场景核心) | Library |
|
||||
| 压缩 | GzipUtil | `ICSharpCode.SharpZipLib`(Library 已有)或 BCL `System.IO.Compression` | Library |
|
||||
| HTML 解析 | CommonMark / jsoup 类 | **HtmlAgilityPack**(Library 已有) | Library |
|
||||
| SSO / 鉴权 | CAS(yh-auth)+ yh-app-auth 签名 | 自定义 `AuthenticationHandler` + 签名中间件(对接 `BschOpenApi.dll`) | Library + 自研 |
|
||||
| 短信/消息 | — | `HyCloud.Sms.dll` / `BestschSendMsg.dll`(按需接入) | Library |
|
||||
| Lombok | `@Getter/@Setter` | C# 9 `record` / 自动属性,无需替代 | 语言内置 |
|
||||
| 异步线程池 | AsyncConfig | `async/await` + `Task.Run`,无需独立配置 | 语言内置 |
|
||||
|
||||
---
|
||||
|
||||
## 4. Library DLL 适配性评估(.NET 5.0 兼容性)
|
||||
|
||||
> .NET 5.0 可引用面向 **.NET Standard 2.0/2.1** 或 **.NET Core/.NET 5+** 的程序集;纯 .NET Framework 4.x 程序集多数可运行但需逐一验证。
|
||||
|
||||
| DLL | 用途 | .NET 5 兼容策略 |
|
||||
| ------------------------------------------------------------------ | ------------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| `Aspose.Cells/Words/Slides.dll` | Excel/Word/PPT 文档生成(打印核心) | 需确认版本 ≥ 20.x(支持 .NET Standard 2.0)。旧 .NET Framework 版建议升级 NuGet 官方包 |
|
||||
| `EntityFramework.dll` + `EntityFramework.SqlServer.dll` | EF6(SQL Server) | ⚠️ **不建议使用**:EF6 仅 6.4+ 支持 .NET Core;本方案选 EF Core 5(SqlServer 提供方) |
|
||||
| `Dapper.Common.dll` | Dapper 扩展 | 验证目标框架;不兼容则改用 NuGet `Dapper` + `Dapper.Contrib` |
|
||||
| `log4net.dll` + `Common.Logging*.dll` | 日志 | log4net 2.0.8+ 支持 .NET Standard 2.0,可直接用;建议只保留 log4net,弃用 Common.Logging(.NET 5 有官方 `ILogger` 抽象) |
|
||||
| `Quartz.dll` | 任务调度 | 需 ≥ 3.1(.NET Standard 2.0);旧 2.x 不兼容,则 NuGet 升级 `Quartz` 3.x |
|
||||
| `Newtonsoft.Json.dll` | JSON | 直接用(或 NuGet 对齐版本) |
|
||||
| `ICSharpCode.SharpZipLib.dll` | 压缩/解压 | 1.2+ 支持 .NET Standard 2.0,可用 |
|
||||
| `HtmlAgilityPack.dll` | HTML 解析 | 1.11+ 支持 .NET Standard 2.0,可用 |
|
||||
| `C5.dll` | 扩展集合 | 2.5+ 支持 .NET Standard,可用(优先级低) |
|
||||
| `BschOpenApi.dll` | 开放平台签名 | ⚠️ 需反编译确认目标框架;若为 .NET Framework 4.x 且无 Core 版,封装为独立 Facade 隔离风险 |
|
||||
| `BestschSendMsg.dll` / `HyCloud.Sms.dll` / `BestSchool.Unread.dll` | 消息/短信 | 同上,逐个验证;不兼容走 HTTP API 调用替代 |
|
||||
| `HTTPSSSL.dll` / `DecompressFile.dll` | HTTPS / 解压 | .NET 5 原生支持,建议**弃用**,用 BCL 替代 |
|
||||
|
||||
**结论**:核心三件(Aspose、log4net、Quartz)+ Newtonsoft.Json + SharpZipLib + HtmlAgilityPack 构成 Library 复用主力;EntityFramework、HTTPSSSL、DecompressFile 弃用;业务 DLL(BschOpenApi/Sms 等)做兼容性验证后封装接入。
|
||||
|
||||
---
|
||||
|
||||
## 5. 解决方案结构设计
|
||||
|
||||
```
|
||||
E:\YHXT\net\YhCore\
|
||||
├─ YhCore.sln
|
||||
├─ docs\ # 方案与设计文档
|
||||
├─ lib\ # 本地 DLL 引用(软链或复制自 E:\YHXT\net\Library)
|
||||
├─ sqlscript\ # 数据库脚本(沿用 Java 项目惯例)
|
||||
├─ src\
|
||||
│ ├─ YhCore.WebApi\ # ★ 启动项目(对应 Java 的 controller + config)
|
||||
│ │ ├─ Controllers\
|
||||
│ │ │ ├─ Api\ # 对应 controller/api
|
||||
│ │ │ ├─ MobiApi\ # 对应 controller/mobiapi
|
||||
│ │ │ └─ OApi\ # 对应 controller/oapi
|
||||
│ │ ├─ Middleware\ # 对应 interceptor:接口访问日志、用户上下文、异常、签名
|
||||
│ │ ├─ Filters\ # 对应 aspect+annotation:操作日志、校验
|
||||
│ │ ├─ Attributes\ # 对应 annotation:[OperationLog] 等
|
||||
│ │ ├─ Extensions\ # ServiceCollection / ApplicationBuilder 扩展
|
||||
│ │ ├─ appsettings.json
|
||||
│ │ ├─ appsettings.Development.json
|
||||
│ │ ├─ appsettings.Production.json
|
||||
│ │ ├─ log4net.config
|
||||
│ │ ├─ Program.cs
|
||||
│ │ └─ Startup.cs
|
||||
│ ├─ YhCore.Service\ # 业务服务层(对应 service + service.impl)
|
||||
│ │ ├─ Interfaces\
|
||||
│ │ └─ Impl\
|
||||
│ ├─ YhCore.Repository\ # 数据访问层(对应 Repository + specification)
|
||||
│ │ ├─ Base\ # Repository 基类、IRepository<T>、UnitOfWork
|
||||
│ │ ├─ Specification\ # 动态查询条件构造
|
||||
│ │ └─ YHDbContext.cs # EF Core 上下文
|
||||
│ ├─ YhCore.Entity\ # 数据库实体(对应 entity,DB First 生成)
|
||||
│ ├─ YhCore.Model\ # 对应 model:DTO / VO / Excel / 常量
|
||||
│ │ ├─ Dto\
|
||||
│ │ ├─ Vo\
|
||||
│ │ ├─ Excel\
|
||||
│ │ └─ Constant\
|
||||
│ ├─ YhCore.Common\ # 对应 utils:30+ 工具类的 .NET 版
|
||||
│ │ ├─ Result\ # ServiceResult / ApiResult 统一响应
|
||||
│ │ ├─ UserContext\ # 当前用户上下文(AsyncLocal 实现)
|
||||
│ │ ├─ Cache\ # RedisUtil 封装
|
||||
│ │ ├─ Http\ # HttpClient 封装
|
||||
│ │ ├─ Security\ # Signature 签名、加密
|
||||
│ │ └─ Utils\ # 日期/分页/枚举/校验/Gzip…
|
||||
│ └─ YhCore.Task\ # 对应 scheduler:Quartz 任务宿主
|
||||
│ ├─ Jobs\
|
||||
│ └─ QuartzHostedService.cs
|
||||
├─ tests\
|
||||
│ └─ YhCore.Tests\ # xUnit 单元测试
|
||||
└─ docker\ # Dockerfile / compose(沿用 Java 项目惯例)
|
||||
```
|
||||
|
||||
**分层依赖方向**(单向,禁止反向引用):
|
||||
|
||||
```
|
||||
WebApi → Service → Repository → Entity
|
||||
└──────→ Model ←──────┘
|
||||
└──────────→ Common ←──────────┘(Common 不依赖任何上层)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 底层框架核心组件设计
|
||||
|
||||
### 6.1 配置体系(对应 bootstrap.yml + application-{env}.yml)
|
||||
|
||||
- 本地开发:`appsettings.json` + `appsettings.Development.json`(默认不连 Nacos,对应 Java 的 `profile=local`)。
|
||||
- 生产:环境变量注入(对应 CI 注入 `SPRING_PROFILES_ACTIVE=prod`)+ 可选 Nacos(`nacos-sdk-csharp`),加载顺序沿用"共享配置 → 应用配置 → 环境变量覆盖"的三级优先级约定。
|
||||
- 强类型配置:`services.Configure<YhSsoOptions>(configuration.GetSection("Yh:Sso"))`,Options 类按 Java yml 的 key 结构一一对应(`yh.sso.*`、`yh.app-auth.*`、datasource、redis 等)。
|
||||
|
||||
### 6.2 日志(对应 logback-spring.xml)
|
||||
|
||||
**只保留接口访问日志,其余日志一律去除。**
|
||||
|
||||
- 通过 `ApiAccessLogMiddleware`(请求管道最外层)统一记录每次接口调用详情,框架不再输出任何其他日志(SQL 日志、框架运行日志、业务 Debug/Info 日志全部关闭)。
|
||||
- **记录字段**:请求时间、渠道(api / mobiapi / oapi)、请求路径与方法、Query 与 Body(敏感字段脱敏,对应 `SensitiveDataUtil`)、客户端 IP、当前用户(取 `UserContext`)、响应状态码、响应耗时、异常摘要(若本次请求出错)。
|
||||
- **落地**:log4net(Library 复用)配置单个 `RollingFileAppender`,按天滚动一个接口日志文件,不做级别分文件。
|
||||
- **屏蔽其他输出**:`appsettings.json` 中将 `Microsoft.*`、`System.*`、`Pomelo.*`(EF Core SQL 日志)等内置日志类别全部设为 `None`;代码规范上禁止在业务代码中调用 `ILogger` 写日志。
|
||||
|
||||
### 6.3 统一响应(对应 yh-api-std / BestSchool.Common 的 ServiceResult)
|
||||
|
||||
复用 `Library\新建文件夹\BestSchool.Common\ServiceResult.cs` 的思路,定义:
|
||||
|
||||
```csharp
|
||||
public class ApiResult<T>
|
||||
{
|
||||
public int Code { get; set; } // 0=成功
|
||||
public string Message { get; set; }
|
||||
public T Data { get; set; }
|
||||
public static ApiResult<T> Ok(T data) => new() { Code = 0, Data = data };
|
||||
public static ApiResult<T> Fail(string msg, int code = -1) => new() { Code = code, Message = msg };
|
||||
}
|
||||
```
|
||||
|
||||
配合 `ActionFilter` 自动包装 Controller 返回值,业务代码只返回 `T`。
|
||||
|
||||
### 6.4 全局异常处理中间件
|
||||
|
||||
- 捕获未处理异常 → 返回统一 `ApiResult.Fail`(对应 Java 的 `@ControllerAdvice`);异常详情由 6.2 的接口访问日志统一记录,不单独输出错误日志。
|
||||
- 自定义 `BizException`:Service 层抛出业务异常,中间件翻译为友好错误码。
|
||||
|
||||
### 6.5 用户上下文(对应 UserContextInterceptor + UserContext + ControllerUserContextAspect)
|
||||
|
||||
- `UserContextMiddleware`:从 CAS Ticket / Header 解析当前用户,写入 `AsyncLocal<UserContext>`。
|
||||
- 全链路任意位置通过 `UserContextHolder.Current` 获取(对应 Java 的 ThreadLocal 方案,AsyncLocal 是 .NET 的等价物且对 async 安全)。
|
||||
- **Java `DWebMvcConfig` 不迁移、直接移除**:它仅负责把 `UserContextInterceptor` 注册到 `/**`,中间件方式天然作用于全管道,无需等价的注册配置类。
|
||||
|
||||
### 6.6 操作日志 AOP(对应 @OperationLog + OperationLogAspect)
|
||||
|
||||
```csharp
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public class OperationLogAttribute : Attribute
|
||||
{
|
||||
public string Module { get; }
|
||||
public string Action { get; }
|
||||
}
|
||||
```
|
||||
|
||||
- `[OperationLog("班级", "升级")]` 标注在 Controller 方法上;
|
||||
- `OperationLogFilter : IAsyncActionFilter` 在方法执行后解析 SpEL 等价表达式(用路由参数/参数名占位符),异步落库到操作日志表(对应 `OperationLogResolver` + `OperationLogRecordRepository`)。
|
||||
|
||||
### 6.7 数据访问(对应 JPA Repository + Specification)
|
||||
|
||||
- **数据库方案**:主库 **SQL Server**(`Server=localhost;Database=EduCloudDocPrint`),同时**支持切换 MySQL**——`appsettings.json` 中 `DatabaseProvider` 取 `SqlServer`(默认)或 `MySql`,`AddDataLayer` 扩展按此选择 EF Core 提供方(`Microsoft.EntityFrameworkCore.SqlServer 5.x` / `Pomelo.EntityFrameworkCore.MySql 5.x`)。连接串生产环境用环境变量覆盖(`ConnectionStrings__SqlServer`),不入库密码。
|
||||
- **EF Core 5**:`YHDbContext` 集中管理实体;旧 EF6 实体无 `[Key]` 标注,统一按约定推断主键(优先 `Id`,其次 `SerID`,皆无则 Keyless);`IRepository<T>` / `RepositoryBase<T>` 提供 CRUD、分页(对应 `PageUtil`)、批量操作。
|
||||
- **Specification 轻量实现**:`Expression<Func<T, bool>>` 组合器(And/Or/Not),对应 Java `specification` 包的动态查询。
|
||||
- **Dapper 通道**:复杂报表/批量快照插入(对应 Java 用 JdbcTemplate 的 `backupAllSnapshot` 场景)走 Dapper,与 EF 共存。
|
||||
- 事务:`IDbContextTransaction` 显式事务 + UnitOfWork 模式;生产 `ddl-auto=none` 对应 EF **禁用自动迁移**,结构变更走 `sqlscript` 脚本。
|
||||
|
||||
### 6.8 Redis 缓存(对应 RedisConfig + RedisUtil)
|
||||
|
||||
- `StackExchange.Redis` 单例 `ConnectionMultiplexer` 注入;
|
||||
- `RedisUtil` 封装 String/Hash/Set/分布式锁,Key 规范常量集中在 `RedisConstants`(对应 Java 同名类)。
|
||||
|
||||
### 6.9 任务调度(对应 scheduler 包)
|
||||
|
||||
- Quartz.NET 3.x + `QuartzHostedService` 承载;Job 通过 DI 构造(可访问 Service 层)。
|
||||
- 框架内置两个示例 Job 骨架:定时任务模板、日志清理任务模板(对应 `SysOperationLogCleanupTask`),业务 Job 后续按模板添加。
|
||||
- 轻量场景可直接用 `BackgroundService` + `PeriodicTimer`。
|
||||
|
||||
### 6.10 接口文档(对应 SwaggerConfig)
|
||||
|
||||
- Swashbuckle:按渠道分组(api / mobiapi / oapi 三个 Swagger Doc),Header 鉴权参数预置,对应 Java 的三套 controller 分组。
|
||||
|
||||
### 6.11 鉴权(对应 yh-auth CAS + yh-app-auth 签名)
|
||||
|
||||
- `CasAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>`:复刻 CAS ticket 校验与 SSO 重定向逻辑(配置项对齐 `yh.sso.*`)。
|
||||
- `AppAuthMiddleware`:应用间调用签名校验(app-key / timestamp / sign),对应 `yh.app-auth.*`;优先尝试复用 `BschOpenApi.dll` 的签名算法,不兼容则按 Signature.java 逻辑重写。
|
||||
|
||||
### 6.12 参数校验(对应 validation 包)
|
||||
|
||||
- FluentValidation:每个 DTO 一个 `AbstractValidator<T>`,自动注册;
|
||||
- 自定义校验规则(如密码强度,对应 `PasswordValidation`)实现为可复用扩展方法。
|
||||
|
||||
### 6.13 文件上传(对应 FileConfig + multipart 50MB)
|
||||
|
||||
- `FormOptions` 配置 `MultipartBodyLengthLimit = 50MB`;
|
||||
- 上传路径走强类型配置 `FileOptions`,对应 Java `FileConfig`。
|
||||
|
||||
### 6.14 路由常量(对应 Java `Config` 枚举的 API_PATH / MOBILEAPI_PATH / OAPI)
|
||||
|
||||
- Java `Config` 枚举中的三个渠道路由前缀,集中在 `Common` 层 `ApiRoutes` 常量类:
|
||||
`ApiPath = "api"`、`MobiApiPath = "mobiapi"`、`OApiPath = "oapi"`。
|
||||
- Controller 一律用 `[Route(ApiRoutes.ApiPath + "/log")]` 引用常量,**禁止硬编码 `"api/..."` 字符串**,与 Java 版 `Config.API_PATH` 常量化做法保持一致。
|
||||
- `Config` 枚举的 `FILE_PATH` / `FILE_WEB_PATH` 已由 6.13 `FileOptions` 覆盖;`getString/getInt`(ResourceBundle 读 config.properties)无对应需求,不迁移。
|
||||
|
||||
---
|
||||
|
||||
## 7. 关键骨架代码示例
|
||||
|
||||
**Program.cs / Startup.cs 装配顺序:**
|
||||
|
||||
```csharp
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddControllers()
|
||||
.AddNewtonsoftJson(o => { // 对齐 Java Jackson 配置
|
||||
o.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss";
|
||||
});
|
||||
services.AddDataLayer(Configuration); // SQL Server 主库;DatabaseProvider=MySql 时切 Pomelo
|
||||
services.AddRedisCache(Configuration); // RedisUtil 单例(StackExchange.Redis,连接串取 Redis:ConnectionString)
|
||||
services.AddRepositories(); // 扩展方法:批量注册 IRepository<T>
|
||||
services.AddServices(); // 扩展方法:批量注册 Service
|
||||
services.AddAutoMapper(typeof(Startup));
|
||||
services.AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<Startup>());
|
||||
services.AddSwaggerGen(/* 三组文档 + 鉴权 Header */);
|
||||
services.AddQuartzHosted(); // 调度
|
||||
services.AddLog4Net(); // 日志
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
{
|
||||
app.UseMiddleware<ApiAccessLogMiddleware>(); // 0. 接口访问日志(唯一日志出口)
|
||||
app.UseMiddleware<GlobalExceptionMiddleware>(); // 1. 异常兜底(异常摘要并入接口日志)
|
||||
app.UseMiddleware<UserContextMiddleware>(); // 2. 用户上下文
|
||||
app.UseMiddleware<AppAuthMiddleware>(); // 3. 应用签名(oapi 渠道)
|
||||
app.UseAuthentication(); // 4. CAS
|
||||
app.UseAuthorization();
|
||||
app.UseEndpoints(e => e.MapControllers());
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 实施步骤
|
||||
|
||||
| 阶段 | 内容 | 产出 |
|
||||
| -------- | ------------------------------------------------------------------- | ----------------- |
|
||||
| 1. 环境准备 | 安装 .NET 5 SDK、创建解决方案与 7 个类库项目、配置 `lib` 本地引用 | sln 骨架可编译 |
|
||||
| 2. 基础件接入 | log4net、Newtonsoft、Swagger、AutoMapper、FluentValidation | 启动即出 Swagger 页 |
|
||||
| 3. 数据层 | 建 YHDbContext + Repository 基类 + Specification + Dapper 通道(暂不生成业务实体) | 连通 SQL Server 冒烟通过 |
|
||||
| 4. 横切组件 | 统一响应、异常中间件、**接口访问日志中间件(唯一日志出口)**、用户上下文、操作日志 Filter | Postman 验证全链路 |
|
||||
| 5. 缓存与调度 | RedisUtil + Quartz 宿主 + Job 模板 | Redis 读写、Job 触发验证 |
|
||||
| 6. 鉴权 | CAS Handler + 签名中间件(对接 yh 平台配置) | SSO 联调通过 |
|
||||
| 7. 收尾 | Dockerfile、CI 配置、开发规范文档 | 框架交付 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 风险与注意事项
|
||||
|
||||
1. **.NET 5 已 EOS**(2022-05 停止支持):按用户要求采用 5.0,但建议方案预留升级路径(代码只使用与 .NET 6/8 兼容的 API,将来改 `TargetFramework` 即可)。
|
||||
2. **Library DLL 多为 .NET Framework 时代产物**:开工第一件事是对 `lib` 全部 DLL 做目标框架核验(可用 `dotnet` 反编译或 `Assembly.LoadFrom` 试加载),不兼容的立即走 NuGet 替代,避免后期返工。
|
||||
3. **EF6 → EF Core 语义差异**:Java JPA 的 `ddl-auto: update` 在 EF Core 中对应 Migrate,生产环境一律禁用自动迁移,结构变更走 SQL 脚本(与 Java 侧生产约定一致)。
|
||||
4. **AsyncLocal vs ThreadLocal**:用户上下文必须用 `AsyncLocal`,否则 async 方法链中上下文丢失。
|
||||
5. **Nacos 为可选项**:本地/单机部署先用 `appsettings.{env}.json` + 环境变量;需接入配置中心时再启用 `nacos-sdk-csharp`,配置 key 结构保持与 Java yml 一致,降低运维心智成本。
|
||||
6. **命名习惯迁移**:Java 的 `getClassHistoryTree` 等接口路径与返回结构保持原样(URL 与 JSON 字段不变),保证前端/移动端零改动切换。
|
||||
@@ -0,0 +1,9 @@
|
||||
**/bin/
|
||||
**/obj/
|
||||
**/target/
|
||||
**/.vs/
|
||||
**/.idea/
|
||||
**/logs/
|
||||
*.user
|
||||
*.suo
|
||||
.DS_Store
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Rr]elease/
|
||||
x64/
|
||||
x86/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
artifacts/
|
||||
|
||||
# Visual Studio cache/options
|
||||
.vs/
|
||||
.idea/
|
||||
*.DotSettings.user
|
||||
|
||||
# NuGet
|
||||
*.nupkg
|
||||
*.snupkg
|
||||
**/[Pp]ackages/*
|
||||
!**/[Pp]ackages/build/
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Rider
|
||||
*.sln.iml
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 业务侧特有
|
||||
# 临时验证目录(Mock CAS 脚本、curl 日志、清理清单等)
|
||||
Temp/
|
||||
# 项目记忆与本地技能(含会话数据,不入库)
|
||||
.workbuddy/
|
||||
# 日志
|
||||
*.log
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
# ============ 多阶段构建:.NET 5.0 ============
|
||||
# 构建阶段
|
||||
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
# 复制解决方案与全部项目文件(利用层缓存)
|
||||
COPY YHDocPrintCore.sln* ./
|
||||
COPY src/*/*.csproj ./
|
||||
RUN for f in src/*/*.csproj; do \
|
||||
mkdir -p "$(dirname "$f")" && mv "$f" "$(dirname "$f")/"; \
|
||||
done \
|
||||
&& dotnet restore YHDocPrintCore.sln
|
||||
|
||||
# 复制源码并发布
|
||||
COPY src/ src/
|
||||
RUN dotnet publish src/YHDocPrintCore.WebApi/YHDocPrintCore.WebApi.csproj \
|
||||
-c Release -o /app/publish --no-restore
|
||||
|
||||
# 运行阶段
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS runtime
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/publish .
|
||||
|
||||
# 日志目录(接口访问日志唯一输出,挂载卷便于采集)
|
||||
RUN mkdir -p /app/logs
|
||||
VOLUME ["/app/logs"]
|
||||
|
||||
EXPOSE 80
|
||||
ENV ASPNETCORE_URLS=http://+:80
|
||||
|
||||
# 生产配置通过环境变量注入(ConnectionStrings__SqlServer / Redis:ConnectionString 等)
|
||||
ENTRYPOINT ["dotnet", "YHDocPrintCore.WebApi.dll"]
|
||||
@@ -0,0 +1,11 @@
|
||||
<Solution>
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/YhCore.Common/YhCore.Common.csproj" />
|
||||
<Project Path="src/YhCore.Entity/YhCore.Entity.csproj" />
|
||||
<Project Path="src/YhCore.Model/YhCore.Model.csproj" />
|
||||
<Project Path="src/YhCore.Repository/YhCore.Repository.csproj" />
|
||||
<Project Path="src/YhCore.Service/YhCore.Service.csproj" />
|
||||
<Project Path="src/YhCore.Task/YhCore.Task.csproj" />
|
||||
<Project Path="src/YhCore.WebApi/YhCore.WebApi.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
@@ -0,0 +1,285 @@
|
||||
# 底层框架代码审查报告
|
||||
|
||||
> 审查对象:`YHDocPrintCore`(业务侧,YhCore.\*)+ `YHBasePlatformDotnet`(底层框架,Yh.Platform.\*)
|
||||
> 审查日期:2026-09-01 | 审查范围:Startup / 中间件 / 公共类 / 框架注册与验签链路 / CAS 认证 / OpenApi 客户端
|
||||
|
||||
## 结论
|
||||
|
||||
整体迁移质量良好:DI 生命周期、配置绑定、异常链路、签名协议与 Java 侧对齐度高,测试套件 98/98 通过。
|
||||
审查发现 **5 个 P1 契约缺陷**(3 个静态审查 + 2 个冒烟触发)与 **4 个 P2 健壮性问题**,均已修复。
|
||||
|
||||
---
|
||||
|
||||
## 一、P1 问题(已修复)
|
||||
|
||||
### 1.1 管道顺序缺陷:/oapi 应用身份(AppKey)被下游中间件清空
|
||||
|
||||
**位置**:`YhCore.WebApi/Startup.cs`(管道顺序)+ `Yh.Platform.Auth/UserContextMiddleware.cs`
|
||||
|
||||
**现象**:原管道 `AppAuth(2) → Authentication(3) → UseYhUserContext(4) → ...`。`AppAuthMiddleware` 验签通过后写入 `UserContextHolder.Set(LoginUser{AppKey})`(位置 2),而 `UserContextMiddleware` 的 `finally` 在请求结束前**无条件** `UserContextHolder.Clear()`(位置 4 更内层,其 finally 在整条链路末尾执行)。结果:`/oapi` 业务 Controller 中 `UserContextHolder.Get()` 恒为 `null`,`AppAuthMiddleware` 注释承诺的"业务可经 UserContextHolder 读取 AppKey"失效。
|
||||
|
||||
**修复**:调整管道顺序,`AppAuth` 移到 `UseYhUserContext` 之后——`Authentication → SsoProtection → UseYhUserContext → AppAuth → LegacyAdapter → Authorization`。`UserContextMiddleware` 的 finally 在 AppAuth 写入之后才执行,AppKey 可存活到业务层;且不再被误清空。
|
||||
|
||||
```
|
||||
修复前: ApiAccessLog → GlobalException → AppAuth → Auth → YhUserContext → Legacy → Sso → Authorization
|
||||
修复后: ApiAccessLog → GlobalException → Auth → Sso → YhUserContext → AppAuth → Legacy → Authorization
|
||||
```
|
||||
|
||||
### 1.2 业务自定义错误码被 `ErrorCodeUtil.From` 吞成 Error=1
|
||||
|
||||
**位置**:`YhCore.Common/Exceptions/BizException.cs`
|
||||
|
||||
**现象**:`ErrorCodeUtil.From(int)` 对未知码 `_ => ErrorCode.Error`(Code=1)。业务 `throw new BizException("库存不足", 5001)` 经映射后 `Code` 变成 1,前端收到 `code:1` 而非 `code:5001`,错误码契约失效。
|
||||
|
||||
**修复**:未知码返回保留原始 code 的 `RawErrorCode(code, "")`(内部 record),msg 由业务消息承载。已知码(0/2/3/4/5/6/405)映射不变。
|
||||
|
||||
### 1.3 CAS 回跳开放重定向
|
||||
|
||||
**位置**:`Yh.Platform.Auth/CasAuthenticationHandler.cs`
|
||||
|
||||
**现象**:`HandleAuthenticateAsync` 中 `Response.Redirect(returnUrl)` 未校验来源,攻击者可诱导已登录用户访问 `https://app/api/x?returnUrl=https://evil.com`,登录后浏览器被跳转到外站(开放重定向)。
|
||||
|
||||
**修复**:新增 `IsLocalUrl` 校验(仅放行 `/` 开头的本地相对路径,拒绝 `//host`、`/\host`、绝对地址),Challenge 拼 service 与回调回跳两处均校验;非法 returnUrl 回退 `/`。
|
||||
|
||||
### 1.4 UserContextMiddleware 构造注入 Scoped 服务,服务启动即崩溃
|
||||
|
||||
**位置**:`Yh.Platform.Auth/UserContextMiddleware.cs`
|
||||
|
||||
**现象**:`IUserDetailsProvider` 注册为 **Scoped**,而中间件在**构造函数**注入它。ASP.NET Core 的 `UseMiddleware<T>` 在管道构建(启动)时用 `ActivatorUtilities` 从**根容器**激活中间件,构造含 Scoped 依赖直接抛 `InvalidOperationException: Cannot resolve scoped service 'Yh.Platform.Auth.IUserDetailsProvider' from root provider`,**WebApi 起不来**(此前迁移冒烟未重启实例,缺陷被掩盖;本轮验证 /oapi 时首次触发)。
|
||||
|
||||
**修复**:改为 ASP.NET Core 官方模式——构造仅保留 `RequestDelegate`(Singleton),Scoped 依赖 `IUserDetailsProvider` 移到 `InvokeAsync(HttpContext, IUserDetailsProvider)` **方法注入**(每请求由 DI 从请求作用域解析)。同步更新 `AuthContextTests` 两处调用。
|
||||
|
||||
### 1.5 服务端验签密钥链不读配置,合法签名恒 401
|
||||
|
||||
**位置**:`Yh.Platform.AppAuth/SignKeyResolver.cs`
|
||||
|
||||
**现象**:`GetSecretAsync` 解析链仅 `Redis(sign:app:{appKey}) → IMemoryCache → UserBase 兜底`,**不消费 `yh:app-auth` 的 AppKey/AppSecret**;且 `AppAuthOptions.TrustedApps` 对应的 `PropertyAppSecretProvider` 从未被注册(`IAppSecretProvider` 写死为 `SignKeyResolverAppSecretProvider`)。本地联调无 Redis 预置 key、无 UserBase → secret 恒为 null → **任意合法签名都 401**。此前冒烟只断言"无签名/假签名 → 401",未验证"合法签名 → 通过",契约缺陷未被发现。
|
||||
|
||||
**修复**:解析链头部增加「配置内置应用」层——`appKey == yh:app-auth:AppKey` 时直接使用配置的 `AppSecret`(覆盖"A 以自身密钥调 B 的 /oapi,B 用配置密钥验签"的自服务/联调场景);第三方应用密钥仍走 Redis → Cache → UserBase。配置密钥从此对服务端验签生效。
|
||||
|
||||
---
|
||||
|
||||
## 二、P2 问题(已修复)
|
||||
|
||||
### 2.1 MemoryReplayGuard 并发下防重放失效
|
||||
|
||||
**位置**:`Yh.Platform.AppAuth/ReplayGuard.cs`
|
||||
|
||||
**现象**:`TryGetValue` 检查后 `_seen[key] = ...` 写入,非原子。两个并发相同签名请求(同 appKey/timestamp/nonce)可同时通过检查同时放行,防重放被绕过。
|
||||
|
||||
**修复**:改为 `ConcurrentDictionary.TryAdd` 原子插入;未过期命中拒绝,过期条目允许覆盖。
|
||||
|
||||
### 2.2 SignCanonical 非法 URL 编码导致 500
|
||||
|
||||
**位置**:`Yh.Platform.AppAuth/SignCanonical.cs`
|
||||
|
||||
**现象**:`Uri.UnescapeDataString` 对未编码的 `%`(如 `?a=100%`)抛 `UriFormatException`,`AppAuthMiddleware` 验签前调用 `BuildTarget` 未捕获 → 兜底返回 `code:6` 内部错误,而非 `401`。
|
||||
|
||||
**修复**:`UrlDecode` 捕获 `UriFormatException` 原样返回;恶意参数验签必失败 → 正确返回 401。
|
||||
|
||||
### 2.3 CasAuthenticationOptions.TicketValidateTimeoutMs 配置未生效
|
||||
|
||||
**位置**:`Yh.Platform.Auth/CasServiceTicketValidator.cs` + `AuthServiceCollectionExtensions.cs` + `CasAuthenticationHandler.cs`
|
||||
|
||||
**现象**:配置项定义了(默认 5000ms)但 validator 硬编码 `5000`,改配置无效。
|
||||
|
||||
**修复(两阶段)**:
|
||||
1. `CasServiceTicketValidator` 构造函数接收 `timeoutMs`,注册时从 `IOptions<CasAuthenticationOptions>` 传入;
|
||||
2. **补漏(9/1 SSO 故障排查发现)**:`CasAuthenticationHandler` 此前绕过 DI、自建 validator 时只传 URL 不传超时 → 配置仍不生效。改为**构造注入 DI 注册的 `CasServiceTicketValidator`**,注册源升级 `IOptionsMonitor<CasAuthenticationOptions>.CurrentValue`。业务侧 `yh:sso:TicketValidateTimeoutMs` 提升到 10000(实测 CAS serviceValidate 延迟 0.5~4.5s+ 波动,5s 默认值存在偶发超时风险)。
|
||||
|
||||
### 2.4 AppAuthMiddleware 手拼 JSON 转义不完整
|
||||
|
||||
**位置**:`YhCore.WebApi/Middleware/AppAuthMiddleware.cs`
|
||||
|
||||
**现象**:`"{\"code\":4,\"msg\":\"" + message.Replace("\"", "\\\"") + ...` 仅转义引号,含反斜杠/换行时产出非法 JSON(当前 message 均为内部常量,风险低)。
|
||||
|
||||
**修复**:改用 `System.Text.Json.JsonSerializer.Serialize`。
|
||||
|
||||
---
|
||||
|
||||
## 三、观察项(未改动,建议后续处理)
|
||||
|
||||
| # | 位置 | 说明 | 建议 |
|
||||
|---|---|---|---|
|
||||
| 3.1 | `OpenApiOptions.Enabled` / `YhAuthOptions.Enabled` | 配置项定义了但注册链路未消费,`yh:open:Enabled=false` 仍会注册客户端并装配门面 | 消费方明确后接入(涉及门面装配语义,需测试覆盖) |
|
||||
| 3.2 | `Startup` CORS | `AllowAnyOrigin` 宽松,注释已标注待 CAS 接入后按 `yh.sso.allow-origins` 收紧 | 上线前提供允许源清单后收紧 |
|
||||
| 3.3 | 反代部署 | `BuildServiceUrl` 依赖 `request.Scheme/Host`,反代后 HTTPS 终止时 service 可能错配 | 生产配置 `AppBaseUrl` 或加 `UseForwardedHeaders` |
|
||||
| 3.4 | `CasServiceTicketValidator` | 回调低频,每次 new `SocketsHttpHandler`,可接受 | 高并发可换共享 `HttpClient` |
|
||||
| 3.5 | `GlobalExceptionMiddleware` | `OperationCanceledException`(客户端断开)走兜底 `code:6` | 若需区分可前置 catch |
|
||||
| 3.6 | `OpenApiClientBase` | `GetRawText()` 二次反序列化,大响应内存开销 | 数据量大时改流式/直接反序列化 |
|
||||
| 3.7 | `SsoProtectionMiddleware` | 直接读 `IConfiguration` 而非 `IOptions`,且保留旧节 `Yh:Sso:Enabled` 兼容 | 框架稳定后移除旧节回退逻辑 |
|
||||
|
||||
---
|
||||
|
||||
## 四、验证结果
|
||||
|
||||
- ✅ 框架重编(Release):**0 警告 0 错误**
|
||||
- ✅ 框架测试套件:**98/98 通过**(Abstractions 8 / AppAuth 42 / Auth 11 / OpenApi 37)——9 处修复零回归
|
||||
- ✅ 业务侧编译(Debug):**0 错误**(仅 net5.0 EOL 警告 NETSDK1138)
|
||||
- ✅ 框架 Release net5.0 产物已拷回 `E:\YHXT\net\Library`(时间戳 2026-09-01 10:03)
|
||||
- ✅ **/oapi 冒烟矩阵**(实例 51835 实启验证):
|
||||
|
||||
| 用例 | 结果 | 断言 |
|
||||
|---|---|---|
|
||||
| 无签名 `GET /oapi/hello` | 401 `{code:4,"msg":"缺少签名头…"}` | 未带票据被拒 |
|
||||
| 合法签名 `GET /oapi/hello`(无 Cookie) | **404**(直达路由层) | 验签通过、**匿名直访成功** |
|
||||
| 合法签名 + 排序 query | 404 | 签名口径含 query 正确 |
|
||||
| 假签名 | 401 `{code:4}` | 票据非法被拒 |
|
||||
| 无 Cookie `GET /api/xxx` | **302** → CAS 登录页 | SSO 保护不受影响 |
|
||||
| 无 Cookie `GET /mobiapi/xxx` | **302** → CAS 登录页 | SSO 保护不受影响 |
|
||||
|
||||
> 备注:编译期间遇到 `MSB3021/MSB3027` 文件锁定,根因为**上次冒烟测试遗留的 YhCore.WebApi 实例进程(PID 21104,监听 51999)**仍持有 `bin` 下 DLL 句柄,`Stop-Process` 结束后编译恢复。排查路径:`netstat -ano | grep <PID>` → 确认连 Redis → taskkill。
|
||||
|
||||
## 五、修复文件清单
|
||||
|
||||
**业务侧(YHDocPrintCore)**
|
||||
- `src/YhCore.WebApi/Startup.cs` — 管道顺序调整(1.1)
|
||||
- `src/YhCore.Common/Exceptions/BizException.cs` — 未知码透传(1.2)
|
||||
- `src/YhCore.WebApi/Middleware/AppAuthMiddleware.cs` — JSON 序列化(2.4)
|
||||
- `src/YhCore.WebApi/Middleware/SsoProtectionMiddleware.cs` — 显式放行 /oapi(渠道契约,1.1 配套)
|
||||
|
||||
**框架侧(YHBasePlatformDotnet)**
|
||||
- `src/Yh.Platform.Auth/CasAuthenticationHandler.cs` — 开放重定向修复(1.3)
|
||||
- `src/Yh.Platform.Auth/UserContextMiddleware.cs` — Scoped 构造注入改方法注入(1.4)
|
||||
- `tests/Yh.Platform.Auth.Tests/AuthContextTests.cs` — 同步新调用约定(1.4)
|
||||
- `src/Yh.Platform.AppAuth/SignKeyResolver.cs` — 配置内置应用密钥层(1.5)
|
||||
- `src/Yh.Platform.AppAuth/ReplayGuard.cs` — 并发防重放(2.1)
|
||||
- `src/Yh.Platform.AppAuth/SignCanonical.cs` — 非法编码容错(2.2)
|
||||
- `src/Yh.Platform.Auth/CasServiceTicketValidator.cs` — 超时配置接入(2.3)
|
||||
- `src/Yh.Platform.Auth/AuthServiceCollectionExtensions.cs` — 超时配置接入(2.3)
|
||||
|
||||
---
|
||||
|
||||
## 六、追加:BizException 退役(9/1)
|
||||
|
||||
业务异常统一改为**直接使用框架 `DException`**(`Yh.Platform.Abstractions`),`BizException` 包装类已删除:
|
||||
|
||||
- `src/YhCore.Common/Exceptions/BizException.cs` **已删除**;`ErrorCodeUtil.From(int)` 独立为 `ErrorCodeUtil.cs`(int → IErrorCode 映射保留,业务抛码仍可用)
|
||||
- `GlobalExceptionMiddleware`:原先逻辑完全相同的 `catch (BizException)` + `catch (DException)` 合并为单个 `catch (DException)`
|
||||
- `WrapBizExceptionAttribute` 更名 `WrapDExceptionAttribute`(当前无业务标注,改名安全)
|
||||
- 业务写法:`throw new DException(ErrorCodeUtil.From(5001), "库存不足")`;仅提示语用 `new DException("提示语")`(默认 Custom=5)
|
||||
|
||||
---
|
||||
|
||||
## 七、追加:SSO 单点登录失败排查(9/1)
|
||||
|
||||
**现象**:浏览器打开 `http://localhost:51835/signin-cas?ticket=ST-7-...` 报"单点登录失败"。
|
||||
|
||||
### 根因(四层)
|
||||
|
||||
1. **CAS 票据单次有效**:`ST-7` 来自更早会话(00:41 首个实例),已被消费或过期。curl 直调 `serviceValidate` 实测 CAS 返回 `<cas:authenticationFailure code="INVALID_TICKET">Ticket 'ST-7-...' not recognized</cas:authenticationFailure>`——**任何重复校验必然失败**,这是 CAS 协议设计(防重放),非缺陷。
|
||||
2. **应用当时未运行**:51835 端口无监听进程(netstat 确认),回调 URL 无法被服务。
|
||||
3. **UX 缺陷**:修复前失效票据回调 → `AuthenticateResult.Fail` → 未认证 → 路径不在保护集 → 落 MVC 裸 **404**,用户感知为"登录失败"。
|
||||
4. **潜在超时风险**:实测 CAS serviceValidate 延迟 **0.5~4.5s+ 波动**,框架默认超时 5s 且此前配置未真正生效(见 2.3 补漏),CAS 慢时登录偶发失败。
|
||||
|
||||
### 修复(框架 2 处 + 业务 2 处,第一轮)
|
||||
|
||||
- 框架 `CasAuthenticationHandler`:**构造注入 DI 注册的 `CasServiceTicketValidator`**(2.3 补漏),并补回旧版有、迁移时丢失的 **`Context.User = principal`**(SignInAsync 只写 Cookie 不改当前请求身份,缺它后续中间件误判未认证)。
|
||||
- 框架 `AuthServiceCollectionExtensions`:validator 注册源 `IOptions` → `IOptionsMonitor<CasAuthenticationOptions>.CurrentValue`。
|
||||
- 业务 `SsoProtectionMiddleware`:CAS 回调路径(`yh:sso:signInCallbackPath`)纳入保护集——失效票据回调**自动 302 弹回 CAS 登录页重新登录**,不再裸 404。
|
||||
- 业务 `appsettings.json`:`yh:sso:TicketValidateTimeoutMs: 10000`(吸收 0.5~4.5s+ 延迟波动)。
|
||||
|
||||
### 验证(实例 51835 实启,第一轮)
|
||||
|
||||
| 用例 | 修复前 | 修复后 |
|
||||
|---|---|---|
|
||||
| 旧票据 `GET /signin-cas?ticket=ST-7-...` | **404** | **302 → CAS 登录页**(自动重登) |
|
||||
| `GET /` | 302 → CAS | 302 → CAS(不变) |
|
||||
| 无 Cookie `GET /api/GetUserInfo` | 302 → CAS | 302 → CAS(不变) |
|
||||
| 无签名 `GET /oapi/Test` | 401 | 401(不变) |
|
||||
| `GET /swagger/index.html` | 200 | 200(不变) |
|
||||
|
||||
⚠️ **此轮验证只覆盖了失败路径**:真实成功路径(票据校验 → Cookie → 回跳)依赖真实 CAS 账号,无法端到端验证。随后用本地 Mock CAS 服务器补测,**立刻暴露一个更深层的框架级缺陷**(见附录八)——第一轮的"302 弹回登录页"只是防循环表象,票据校验实际从未被触发。
|
||||
|
||||
> **给使用者的操作指引**:CAS 票据单次有效且时效短——登录流程必须**从受保护页面(`/` 或 `/api/xxx`)发起**,不要收藏/刷新带 `ticket` 的回调 URL(刷新必失败)。失败后回到 `/` 重新走一遍即可。
|
||||
|
||||
## 八、追加:SSO 成功路径端到端验证——DefaultScheme 缺陷(9/1 第二轮)
|
||||
|
||||
**背景**:附录七修复后,用本地 Mock CAS(`Temp/mock-cas.js`,自动登录 + 单次票据语义)通过环境变量 `yh__sso__CasServerUrlPrefix` 覆盖指向 Mock,对**成功路径**做全链路验证。结果发现:票据校验请求**从未送达** CAS(Mock 日志只有 `/cas/login`,无 `/serviceValidate`),回调 2ms 即返回——校验在网络请求之前就被跳过。
|
||||
|
||||
### 根因(框架级,P0)
|
||||
|
||||
`AuthServiceCollectionExtensions.AddAuthentication` 中 **`DefaultAuthenticateScheme = CookieScheme`**。`UseAuthentication` 每请求只调用 `AuthenticateAsync(默认 scheme)` 恢复 Cookie 登录态;**CAS 票据校验(`CasAuthenticationHandler.HandleAuthenticateAsync` 的 ticket 分支)在回调请求上没有任何触发点**:
|
||||
|
||||
```
|
||||
回调 /signin-cas?ticket=ST-… 进入
|
||||
└─ UseAuthentication → 只查 Cookie(无)→ 未认证
|
||||
└─ SsoProtectionMiddleware:带 ticket + 未认证 → 直接 401 防循环(附录七引入)
|
||||
└─ CasAuthenticationHandler.HandleAuthenticateAsync 从未执行 → 票据从不校验 → 登录永远失败
|
||||
```
|
||||
|
||||
即:第一轮"防循环"把**合法票据路径一并拦截**,与 `DefaultScheme` 错误叠加,导致任何 CAS 登录都不可能成功(Mock 端到端立即暴露,真实账号难以察觉)。
|
||||
|
||||
### 修复(框架 1 处 + 业务 1 处)
|
||||
|
||||
- **框架 `AuthServiceCollectionExtensions`**:`DefaultAuthenticateScheme` 改为 `YhAuthDefaults.CasScheme`。`CasAuthenticationHandler.HandleAuthenticateAsync` 第一步就是 `AuthenticateAsync(CookieScheme)` 恢复登录态,因此语义等价"先 Cookie 恢复、后 ticket 校验",每请求行为不变、仅补上票据校验触发点。
|
||||
- **业务 `SsoProtectionMiddleware`**:回调路径**已认证且已 302**(handler 验票成功 `Response.Redirect`)时**短路管道**——handler 返回 `NoResult` 不短路,继续走 MVC 端点匹配会因 `/signin-cas` 无 action 返回 **404 覆盖 302**(实测现象),短路保住回跳响应。
|
||||
|
||||
### 验证(Mock CAS 端到端,实例 51835 实启)
|
||||
|
||||
| # | 场景 | 结果 |
|
||||
|---|---|---|
|
||||
| A | `/` → Mock 登录 → 回调验票 → Cookie → 回跳 `/` | **302 → 200**,Cookie 落盘 ✓ |
|
||||
| B | 带 Cookie `GET /api/GetUserInfo` | **200**(登录态生效)✓ |
|
||||
| C | 复用已消费票据 `GET /signin-cas?ticket=同前` | **401 防循环页** ✓(Mock 第二次校验返回 INVALID_TICKET) |
|
||||
| D | 无 Cookie `GET /api/GetUserInfo` | 302 → CAS ✓ |
|
||||
| E | 无签名 `GET /oapi/Test` | 401(SSO 不覆盖)✓ |
|
||||
|
||||
Mock 日志互证:`02:35:09 /cas/serviceValidate?ticket=ST-MOCK-…` 送达并成功、票据被消费;复用同一票据时 Mock 返回 `INVALID_TICKET` → 应用 401。
|
||||
|
||||
框架重编 0 错误、测试 **98/98 通过**、业务编译 0 错误;`Yh.Platform.Auth.dll` 已更新至 `E:\YHXT\net\Library`。
|
||||
|
||||
> **教训**:SSO 类链路缺陷必须覆盖**成功路径**验证——失败路径的"防循环 401/302"表象可能掩盖"校验从未发生"的框架级问题。无真实账号时用 Mock CAS 全链路补测是必要手段。
|
||||
|
||||
---
|
||||
|
||||
## 附录九:登录态 Cookie 会话改造(9/1 需求:关闭失效 / 1 分钟过期 / 32 位)
|
||||
|
||||
**需求**:① 关闭浏览器 Cookie 失效;② 过期时间 1 分钟;③ Cookie 长度为 32 位。
|
||||
|
||||
**实现(框架 `Yh.Platform.Auth` 3 处 + 业务 1 处)**:
|
||||
|
||||
| 文件 | 变更 |
|
||||
|---|---|
|
||||
| `SessionTicketDataFormat.cs`(新增) | 自定义 `ISecureDataFormat<AuthenticationTicket>`:`Protect` 把完整票据存服务端内存、返回 **32 位 key**(Guid N 格式);`Unprotect` 查表并校验 `ExpiresUtc`(过期返回 null 并清理)。key 为 128 位随机熵,不可枚举 |
|
||||
| `AuthServiceCollectionExtensions.cs` | Cookie 注册:`options.TicketDataFormat = new SessionTicketDataFormat()`;`ExpireTimeSpan` 由 `yh:auth:CookieExpireMinutes` 决定(默认 1 分钟);`SlidingExpiration = false`(固定过期不滑动) |
|
||||
| `CasAuthenticationHandler.cs` | `IsPersistent: true → false`(会话级 Cookie,关闭浏览器失效;旧版 `_legacy` 本为 false,系迁移回归,已修正) |
|
||||
| 业务 `appsettings.json` | `CookieExpireHours: 8` → `CookieExpireMinutes: 1`(注:文件曾被外部覆盖一次,CookieName 现为用户保存的 `.yh.auth`) |
|
||||
|
||||
**关键技术发现**:**.NET 5 的 `CookieAuthenticationOptions.SessionStore` 并不能缩短 Cookie**——官方源码(v5.0.17 `CookieAuthenticationHandler.HandleSignInAsync`)显示即便配置了 SessionStore,cookie 值仍是 `TicketDataFormat.Protect(精简票据)` 的加密长串(`CfDJ8…`),只是把完整用户票据移到了服务端。**要实现"Cookie 32 位",必须自定义 `TicketDataFormat`(Protect 落库返回 key)**,而非依赖 SessionStore。首次尝试(SessionTicketStore + SessionStore)经最小复现程序(独立 net5.0 Web 项目直连框架 DLL)证实无效后放弃。
|
||||
|
||||
**验证(Mock CAS 端到端 + 真实配置冒烟)**:
|
||||
|
||||
| 需求 | 验证结果 |
|
||||
|---|---|
|
||||
| ③ Cookie 32 位 | `Set-Cookie: .yh.auth=3a3f40993606438e86d8e756b99a6f07` = **32 字符** ✓ |
|
||||
| ① 关闭浏览器失效 | Set-Cookie 无 `Max-Age`/`Expires`(会话 Cookie)+ `IsPersistent=false` ✓ |
|
||||
| ② 1 分钟过期 | 登录后立即带 Cookie 访问 `/` → 200;**65 秒后** → **302 弹回 CAS 登录页**(服务端票据过期)✓ |
|
||||
|
||||
框架双目标(net5.0/net8.0)编译 0 错误 0 警告、测试 **98/98 通过**、业务编译 0 错误;DLL 已更新至 `E:/YHXT/net/Library`。
|
||||
|
||||
**注意**:票据存进程内存(ConcurrentDictionary)——应用重启后所有登录态失效(用户需重新登录);多实例部署时各实例票据互不可见(如需跨实例共享可换 Redis 实现)。
|
||||
|
||||
### 附录九补充:退出即吊销——显式登出服务端票据立即失效(9/1 第二轮)
|
||||
|
||||
**背景**:附录九初版只覆盖"登录态自然过期/关浏览器"。审查发现缺口:`SignOutAsync`(业务 `GET /api/Logout`)只删 Cookie,**服务端内存票据残留到 ExpireTimeSpan 过期**(默认 1 分钟)——期间若旧 Cookie key 被恢复/复用,登录态仍有效。
|
||||
|
||||
**修复(框架 `Yh.Platform.Auth` 2 处)**:
|
||||
|
||||
| 文件 | 变更 |
|
||||
|---|---|
|
||||
| `SessionTicketDataFormat.cs` | 新增 `Revoke(string? key)`:删除指定票据(显式退出立即失效);新增分钟级摊还清理 `CleanupExpiredIfDue()`(Unprotect/Revoke 路径触发,Interlocked 门闩保证并发下每分钟最多一次全量扫描,防内存膨胀) |
|
||||
| `AuthServiceCollectionExtensions.cs` | `SessionTicketDataFormat` 提升为 DI 单例(`services.AddSingleton`,TicketDataFormat 与事件共享同一票据表);Cookie 注册挂 `options.Events.OnSigningOut`:SignOutAsync 时从请求 Cookie 读 32 位 key → `Revoke(key)` → 服务端票据**立即删除** |
|
||||
|
||||
**语义闭环**:① 关闭浏览器 → 会话 Cookie 删除(服务端无法感知浏览器关闭,1 分钟过期兜底);② 显式退出(/api/Logout)→ Cookie 删除 **+** 服务端票据立即吊销,不等过期。
|
||||
|
||||
**验证(Mock CAS 端到端,实例 51835 实启)**:
|
||||
|
||||
| 步骤 | 期望 | 实测 |
|
||||
|---|---|---|
|
||||
| 登录流 → Cookie(32 位 key) | 200 + Cookie 落盘 | ✓ |
|
||||
| 带 Cookie 访问 `/api/GetUserInfo` | 200 | ✓ |
|
||||
| `GET /api/Logout`(带 Cookie) | 200 | ✓ |
|
||||
| **Logout 后旧 key 手动放回访问** | **302 弹回 CAS**(服务端已吊销;旧行为下会 200) | ✓ **302** |
|
||||
|
||||
框架测试 **106/106 通过**(Auth 19 = 原 11 + 新增 `SessionTicketDataFormatTests` 8 项:32 位 key 往返 / 未知 key null / Revoke 立即失效 / 过期 null / 未过期正常 / Revoke 不影响其他票据等),双目标编译 0 错误 0 警告;`Yh.Platform.Auth.dll`(Release net5.0)已更新至 `E:/YHXT/net/Library`。
|
||||
@@ -0,0 +1,86 @@
|
||||
# YhCore 开发规范
|
||||
|
||||
> .NET 5.0 后端框架(参照 Java 版 bschclassmanage 迁移),仅框架无业务逻辑。
|
||||
|
||||
## 1. 解决方案结构
|
||||
|
||||
```
|
||||
YhCore.sln
|
||||
├─ src/
|
||||
│ ├─ YhCore.WebApi # 启动项:Controllers / Middleware / Filters / Attributes / Auth
|
||||
│ ├─ YhCore.Service # 业务服务层:Interfaces / Impl(不含业务时用 *Stub 占位)
|
||||
│ ├─ YhCore.Repository # 数据层:YHDbContext / IRepository<T> / Specification / DapperChannel
|
||||
│ ├─ YhCore.Entity # 数据库实体(旧 EF6 POCO 迁移而来,无导航属性)
|
||||
│ ├─ YhCore.Model # DTO / VO / Boa 平台占位模型
|
||||
│ ├─ YhCore.Common # 工具:ApiRoutes / UserContext / RedisUtil / PageUtil / ApiResult
|
||||
│ └─ YhCore.Tasks # 调度:QuartzHostedService / Jobs
|
||||
└─ lib / docker / docs / sqlscript
|
||||
```
|
||||
|
||||
**依赖方向(单向,禁止反向引用)**:WebApi → Service → Repository → Entity;WebApi → Model ← Service;Common 不依赖任何上层。
|
||||
|
||||
**底层框架(Yh.Platform.\*)**:CAS 认证、用户上下文(BoaUserContext)、应用验签(AppAuthVerifier)、用户中心(OpenApi)均由底层框架提供:
|
||||
- 引用 DLL:`Yh.Platform.Abstractions` / `Yh.Platform.AppAuth` / `Yh.Platform.Auth` / `Yh.Platform.OpenApi`(lib 目录,源码在 `E:\YHXT\net\YHBasePlatformDotnet`)
|
||||
- `Startup` 用 `services.AddYhPlatform(Configuration)` 聚合注册(= AddYhAppAuth + AddYhAuth[含 AddYhOpenApi]),管道用 `app.UseYhUserContext()` 注入用户上下文
|
||||
- 旧自研 CAS/验签/用户中心实现已退役到 `src/_legacy/`(不参与编译,仅留档备份);业务代码取当前用户用 `BoaUserContext.Get()`
|
||||
|
||||
**`/oapi` 开放渠道契约**:应用间调用渠道,**无需用户登录**,凭签名票据直接访问:
|
||||
- 票据 = 请求头 `X-App-Key` / `X-Timestamp` / `X-Signature`(`signature = Base64(HmacSHA256(METHOD + "\n" + path[?sortedQuery] + "\n" + timestamp, secret))`,详见框架仓库 `YHBasePlatformDotnet\BasePlatformDotnet\contract\sign-protocol.md`)
|
||||
- 未带票据/票据非法 → `401 {code:4,msg:"缺少签名头…"/"签名校验失败…"}`;验签通过 → 放行到业务(无用户上下文,业务只可读 `UserContextHolder.Get().AppKey` 应用身份)
|
||||
- SSO 登录保护**永不拦截** `/oapi`(SsoProtectionMiddleware 显式放行,即使 `yh:sso:urlPattern` 误配也不影响);服务端验签密钥解析链:配置内置应用(`yh:app-auth` AppKey/AppSecret)→ Redis `sign:app:{appKey}` → 本地缓存 → UserBase 兜底
|
||||
|
||||
**`/oapi` 冒烟验证方法**:本地联调/自测票据可用 `scripts/oapi-sign.js` 生成(对齐签名协议):
|
||||
- 生成票据:`node scripts/oapi-sign.js GET "/oapi/hello?a=2&b=1"` → 输出 `appKey/ts/signature` 三头
|
||||
- 直接出 curl:`node scripts/oapi-sign.js --curl POST "/oapi/xxx"` → 复制替换 `PORT` 即可
|
||||
- 密钥默认取开发联调密钥(`yh:app-auth`),生产环境用 `APP_KEY` / `APP_SECRET` 环境变量覆盖,**禁止提交真实密钥到仓库**
|
||||
- 断言基线:无票据 → 401 `{code:4}`;合法票据 → 直达业务(404/业务结果);假票据 → 401;`/api`、`/mobiapi` 无 Cookie → 302 CAS(SSO 保护不受影响)
|
||||
|
||||
## 2. 强制规范
|
||||
|
||||
| 约定 | 说明 |
|
||||
|---|---|
|
||||
| **接口路径** | 一律用 `[Route(ApiRoutes.ApiPath + "/xxx")]`,禁止硬编码 `"api/..."`;渠道前缀见 `ApiRoutes`(api/mobiapi/oapi) |
|
||||
| **接口返回结构** | 与 Java 版保持一致(裸返回),**全局 `GlobalWrapResultFilter` 自动统一包装**(ApiResult{code,msg,data});需要裸返回用 `[NoWrapResult]` 显式排除;异常由 `GlobalExceptionMiddleware` 统一转失败响应 |
|
||||
| **业务异常** | Service 层抛 `new DException(ErrorCodeUtil.From(code), "提示语")`(框架 `Yh.Platform.Abstractions`);仅提示语可用便捷构造 `new DException("提示语")`(默认 Custom=5);中间件自动转统一失败响应 |
|
||||
| **日志** | **只允许接口访问日志**(ApiAccessLogMiddleware 自动记录)。业务代码禁止调用 `ILogger` 写日志;异常摘要由接口日志统一带出 |
|
||||
| **缓存 Key** | 必须用 `RedisConstants.KeyOf(...)` 生成;过期时间用 `RedisConstants.CACHE_EXPIRE_*` |
|
||||
| **分页** | 页码从 0 开始(与 Spring Pageable 对齐);数据库分页用 `IRepository<T>.PageAsync`,内存分页用 `PageUtil` |
|
||||
| **动态条件** | 用 `Specification.True<T>().And(...).AndIf(cond, ...)` 拼装 |
|
||||
| **操作日志** | **接口级审计日志,全局自动**:`SysOperationLogFilter`(Startup 全局注册)每次 Controller 请求后落库 `sys_operation_log`(对齐 Java `SysOperationLog`,含 URI/参数/响应 GZIP/耗时/状态码/异常),无需任何标注;级别按 HTTP 方法推导(GET=QUERY/POST=ADD/PUT=EDIT/DELETE=DELETE);`LogController` 已显式排除防递归 |
|
||||
| **实体主键** | 实体无 `[Key]` 时按约定推断(Id → SerID);新增表实体建议显式标注 |
|
||||
| **数据库** | 默认 SQL Server 主库;`DatabaseProvider=MySql` 切换;**缺表自动补建**(`Database:AutoCreateMissingTables`,默认 true:启动时对比 EF 模型与数据库,缺失的表按模型自动 CREATE,幂等;已存在表的结构变更仍走 sqlscript 脚本) |
|
||||
| **数据库写操作** | 注意 `RepositoryBase` 的 Add/Update 需调用 `SaveChangesAsync()` 提交(UnitOfWork 模式) |
|
||||
|
||||
## 3. 常用命令
|
||||
|
||||
```bash
|
||||
dotnet build # 编译
|
||||
dotnet run --project src/YhCore.WebApi # 本地运行(Swagger: /swagger/index.html)
|
||||
dotnet watch run --project src/YhCore.WebApi
|
||||
```
|
||||
|
||||
## 4. 环境配置(appsettings.json 或环境变量覆盖)
|
||||
|
||||
| 配置 | 默认值 | 说明 |
|
||||
|---|---|---|
|
||||
| `DatabaseProvider` | `SqlServer` | `SqlServer` / `MySql` |
|
||||
| `Database:AutoCreateMissingTables` | `true` | 启动时自动补建 EF 模型中有而数据库缺失的表(仅 SQL Server;幂等;已存在表不触碰,结构变更仍走 sqlscript) |
|
||||
| `ConnectionStrings__SqlServer` | localhost/EduCloudDocPrint | 主库连接串(生产用环境变量,勿提交明文密码) |
|
||||
| `ConnectionStrings__Mysql` | localhost:3306 | MySQL 备用连接串 |
|
||||
| `Redis:ConnectionString` | localhost:6379,defaultDatabase=0 | Redis 连接串 |
|
||||
| `yh:auth:CookieName` | `.yh.auth` | 认证 Cookie 名 |
|
||||
| `yh:auth:CookieExpireMinutes` | `1` | 登录态固定过期(分钟),不滑动续期;票据存服务端内存(SessionTicketDataFormat),Cookie 只写 32 位 key;会话级 Cookie(IsPersistent=false),关闭浏览器即失效;**显式退出(/api/Logout)时服务端票据立即吊销**(OnSigningOut 事件,不等过期兜底) |
|
||||
| `yh:sso:Enabled` | `true` | CAS SSO 开关(拦截 urlPattern / mobiApiPattern) |
|
||||
| `yh:sso:CasServerUrlPrefix` | - | CAS 服务端地址前缀(Challenge 跳转用) |
|
||||
| `yh:app-auth:Enabled` | `true` | /oapi 应用验签开关(X-App-Key / X-Timestamp / X-Signature) |
|
||||
| `yh:open:AppKey` / `yh:open:AppSecret` | 100002 | OpenApi 调用方凭证 |
|
||||
|
||||
## 5. 待办(TODO)
|
||||
|
||||
- [x] CAS 认证:已由 `Yh.Platform.Auth`(CasScheme)接管,旧本地实现退役到 `_legacy/Auth`
|
||||
- [x] 应用签名:`AppAuthMiddleware` 已接入 `AppAuthVerifier` + `SignCanonical` 真实验签
|
||||
- [x] 用户中心:`IUserCenterService` 已替换为 `Yh.Platform.OpenApi`(`BoaUserContext.Get()` 取上下文)
|
||||
- [ ] `ILogService` 真实查询实现(替换 LogServiceStub)
|
||||
- [x] Quartz Job 按业务启用:`CleanupLogJob` 已实现(每日凌晨 3:30 删除 30 天前 `sys_operation_log`),`QuartzHostedService.InitJobs` 已注册 `cleanup-log-job`(cron `0 30 3 * * ?`)
|
||||
- [ ] 审计日志增强(`SysOperationLogFilter` 目前按 HTTP 方法推导级别,`Module/Operation` 两列恒空,可接入业务字典映射;日志查询走 `LogController` 但 `LogServiceStub` 尚未实现真实分页查询)
|
||||
- [ ] .NET 5 已 EOL:框架代码避免使用 5.0 专属 API,便于日后升 6/8
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env node
|
||||
// /oapi 签名票据生成器(对齐 yh-platform-net SignUtil.SignRequest / SignCanonical 协议)
|
||||
//
|
||||
// 协议:signature = Base64(HmacSHA256(METHOD + "\n" + path[?sortedQuery] + "\n" + timestamp, secret))
|
||||
// - query 按 k=v 整体字典序排序后拼接参与签名
|
||||
// - timestamp 为毫秒级 Unix 时间戳(服务端校验 5 分钟窗口)
|
||||
//
|
||||
// 用法:
|
||||
// node scripts/oapi-sign.js [METHOD] [path?query]
|
||||
// node scripts/oapi-sign.js --curl POST /oapi/print/submit # 输出可直接复制的 curl 命令
|
||||
//
|
||||
// 密钥来源(优先级):环境变量 APP_KEY / APP_SECRET → 文件默认值(开发联调密钥)
|
||||
// 生产环境务必通过环境变量注入,勿提交真实密钥。
|
||||
const crypto = require('crypto');
|
||||
|
||||
const APP_KEY = process.env.APP_KEY || '100002';
|
||||
const APP_SECRET = process.env.APP_SECRET || '2314a3b158b24a838b2b9b69fd3db118';
|
||||
|
||||
// 排序 query(对齐 SignCanonical.SortRawQuery:k=v 整体排序,空值保留 = 号)
|
||||
function sortQuery(raw) {
|
||||
if (!raw) return '';
|
||||
return raw.split('&').filter(Boolean).map(p => {
|
||||
const i = p.indexOf('=');
|
||||
return (i < 0 ? p : p.slice(0, i)) + '=' + (i < 0 ? '' : p.slice(i + 1));
|
||||
}).sort().join('&');
|
||||
}
|
||||
|
||||
function sign(method, pathWithQuery, ts) {
|
||||
const qi = pathWithQuery.indexOf('?');
|
||||
const target = qi >= 0
|
||||
? pathWithQuery.slice(0, qi) + '?' + sortQuery(pathWithQuery.slice(qi + 1))
|
||||
: pathWithQuery;
|
||||
const source = method.toUpperCase() + '\n' + target + '\n' + ts;
|
||||
const sig = crypto.createHmac('sha256', APP_SECRET).update(source, 'utf8').digest('base64');
|
||||
return { ts, target, signature: sig };
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
let method = 'GET';
|
||||
let pathWithQuery = '/oapi/hello';
|
||||
let curlMode = false;
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const a = args[i];
|
||||
if (a === '--curl') { curlMode = true; continue; }
|
||||
if (!/^-/.test(a)) {
|
||||
if (method === 'GET' && !/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)$/i.test(a) && !a.startsWith('/')) {
|
||||
// 第一个非选项参数若不像 METHOD 也不像路径,则视为路径
|
||||
pathWithQuery = a;
|
||||
} else if (a.startsWith('/')) {
|
||||
pathWithQuery = a;
|
||||
} else {
|
||||
method = a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { ts, target, signature } = sign(method, pathWithQuery, Date.now().toString());
|
||||
|
||||
if (curlMode) {
|
||||
const u = target.replace(/^\//, '');
|
||||
console.log(`curl -i -X ${method.toUpperCase()} "http://localhost:PORT/${u}" \\`);
|
||||
console.log(` -H "X-App-Key: ${APP_KEY}" \\`);
|
||||
console.log(` -H "X-Timestamp: ${ts}" \\`);
|
||||
console.log(` -H "X-Signature: ${signature}"`);
|
||||
} else {
|
||||
console.log(JSON.stringify({ appKey: APP_KEY, method, target, ts, signature, curl: [
|
||||
'-H', `X-App-Key: ${APP_KEY}`,
|
||||
'-H', `X-Timestamp: ${ts}`,
|
||||
'-H', `X-Signature: ${signature}`
|
||||
].join(' ') }, null, 2));
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
-- ============================================================
|
||||
-- 数据变更日志表(A_DataLog)
|
||||
-- 对应实体:YhCore.Entity.A_DataLog(旧 EF6 EDMX 生成 POCO,主键约定 SerID)
|
||||
-- 用途:数据变更审计预留表(当前无业务代码使用,仅 DbContext 注册)
|
||||
-- 数据库:SQL Server(EduCloudDocPrint)
|
||||
-- 风格:对齐同库 Java 遗留表(SerID 主键自增、varchar 字符串、可空)
|
||||
-- ============================================================
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'A_DataLog')
|
||||
BEGIN
|
||||
CREATE TABLE [dbo].[A_DataLog] (
|
||||
[SerID] INT IDENTITY(1,1) NOT NULL CONSTRAINT [PK_A_DataLog] PRIMARY KEY,
|
||||
[SchSerID] VARCHAR(200) NULL,
|
||||
[ModuleName] VARCHAR(50) NULL,
|
||||
[TableName] VARCHAR(50) NULL,
|
||||
[OldSerID] INT NULL,
|
||||
[NowSerID] INT NULL,
|
||||
[RId] INT NULL
|
||||
);
|
||||
|
||||
PRINT 'A_DataLog table created.';
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
PRINT 'A_DataLog table already exists, skip.';
|
||||
END
|
||||
@@ -0,0 +1,55 @@
|
||||
-- ============================================================
|
||||
-- 系统审计日志表(sys_operation_log)
|
||||
-- 对应实体:YhCore.Entity.SysOperationLog(EF Core,OnModelCreating ToTable 映射)
|
||||
-- 对应 Java:bschclassmanage 的 entity.SysOperationLog / sqlscript/sys_operation_log.sql
|
||||
-- 用途:SysOperationLogFilter 全量记录 HTTP 接口日志 / CleanupLogJob 30 天清理
|
||||
-- 说明:本表取代旧业务操作日志表 OperationLogRecord(旧表无数据,直接删除)
|
||||
-- 数据库:SQL Server(EduCloudDocPrint)
|
||||
-- ============================================================
|
||||
|
||||
-- 1) 清理旧业务操作日志表(OperationLogRecord,已被 sys_operation_log 取代)
|
||||
IF OBJECT_ID(N'[dbo].[OperationLogRecord]', N'U') IS NOT NULL
|
||||
BEGIN
|
||||
DROP TABLE [dbo].[OperationLogRecord];
|
||||
PRINT 'Legacy OperationLogRecord table dropped.';
|
||||
END
|
||||
|
||||
-- 2) 创建审计日志表(幂等)
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.tables WHERE name = 'sys_operation_log')
|
||||
BEGIN
|
||||
CREATE TABLE [dbo].[sys_operation_log] (
|
||||
[Id] BIGINT IDENTITY(1,1) NOT NULL CONSTRAINT [PK_sys_operation_log] PRIMARY KEY,
|
||||
[LogLevel] TINYINT NOT NULL, -- 1-重要(删除) 2-次要(新增/编辑) 3-一般(查询)
|
||||
[OperationType] NVARCHAR(16) NOT NULL, -- DELETE/CREATE/UPDATE/QUERY
|
||||
[UserId] INT NULL, -- 操作人 ID
|
||||
[UserName] NVARCHAR(64) NULL, -- 操作人姓名
|
||||
[SchId] INT NULL, -- 学校 ID(多校隔离)
|
||||
[RequestIp] NVARCHAR(64) NOT NULL, -- 客户端 IP
|
||||
[RequestUri] NVARCHAR(512) NOT NULL, -- 请求 URI
|
||||
[RequestMethod] NVARCHAR(16) NOT NULL, -- HTTP 方法
|
||||
[ClassMethod] NVARCHAR(512) NOT NULL, -- 类名.方法名
|
||||
[RequestParams] NVARCHAR(MAX) NULL, -- 请求参数(JSON,截断)
|
||||
[ResponseData] VARBINARY(MAX) NULL, -- GZIP 压缩后的响应 JSON
|
||||
[StatusCode] INT NOT NULL, -- 业务状态码:0 成功 / 非 0 失败
|
||||
[CostTime] BIGINT NOT NULL, -- 耗时(ms)
|
||||
[UserAgent] NVARCHAR(512) NULL,
|
||||
[ErrorMsg] NVARCHAR(MAX) NULL, -- 异常信息(截断 8000)
|
||||
[RetryCount] INT NULL DEFAULT 0, -- 重试次数(对齐 Java 双轨字段,.NET 暂不写入)
|
||||
[LastRetryStatus] INT NULL, -- 上次重试状态:1 成功 / 0 失败
|
||||
[LastRetryTime] DATETIME NULL, -- 上次重试时间
|
||||
[CreateTime] DATETIME NOT NULL -- 记录时间
|
||||
);
|
||||
|
||||
-- 查询/清理索引(对齐 Java idx_sol_*)
|
||||
CREATE INDEX [IX_sol_CreateTime] ON [dbo].[sys_operation_log] ([CreateTime]);
|
||||
CREATE INDEX [IX_sol_UserId_Time] ON [dbo].[sys_operation_log] ([UserId], [CreateTime]);
|
||||
CREATE INDEX [IX_sol_SchId_Time] ON [dbo].[sys_operation_log] ([SchId], [CreateTime]);
|
||||
CREATE INDEX [IX_sol_Uri_Time] ON [dbo].[sys_operation_log] ([RequestUri], [CreateTime]);
|
||||
CREATE INDEX [IX_sol_Level_Time] ON [dbo].[sys_operation_log] ([LogLevel], [CreateTime]);
|
||||
|
||||
PRINT 'sys_operation_log table created.';
|
||||
END
|
||||
ELSE
|
||||
BEGIN
|
||||
PRINT 'sys_operation_log table already exists, skip.';
|
||||
END
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace YhCore.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// 渠道路由前缀常量(对应 Java 版 config.Config 枚举的
|
||||
/// API_PATH / MOBILEAPI_PATH / OAPI)。
|
||||
/// Controller 一律通过常量引用,禁止硬编码 "api/..." 字符串:
|
||||
/// <code>[Route(ApiRoutes.ApiPath + "/log")]</code>
|
||||
/// </summary>
|
||||
public static class ApiRoutes
|
||||
{
|
||||
/// <summary>PC 端接口前缀(对应 Config.API_PATH)</summary>
|
||||
public const string ApiPath = "api";
|
||||
|
||||
/// <summary>移动端接口前缀(对应 Config.MOBILEAPI_PATH)</summary>
|
||||
public const string MobiApiPath = "mobiapi";
|
||||
|
||||
/// <summary>开放接口前缀(对应 Config.OAPI)</summary>
|
||||
public const string OApiPath = "oapi";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace YhCore.Common.Cache
|
||||
{
|
||||
/// <summary>
|
||||
/// Redis Key 规范常量(对应 Java 版 utils.RedisConstants)。
|
||||
/// 所有业务 Key 必须基于 <see cref="CachePrefix"/> 通过 <see cref="KeyOf"/> 生成,禁止散落硬编码。
|
||||
/// </summary>
|
||||
public static class RedisConstants
|
||||
{
|
||||
/// <summary>本项目缓存前缀(Java 班级管理为 bsch.classmanage,本项目为 yh.docprint)</summary>
|
||||
public const string CachePrefix = "yh.docprint";
|
||||
|
||||
// ---- 常用过期时间(秒),对应 Java CACHE_EXPIRE_* ----
|
||||
public const long CACHE_EXPIRE_1M = 60;
|
||||
public const long CACHE_EXPIRE_5M = 5 * 60;
|
||||
public const long CACHE_EXPIRE_10M = 10 * 60;
|
||||
public const long CACHE_EXPIRE_30M = 3 * CACHE_EXPIRE_10M;
|
||||
public const long CACHE_EXPIRE_1H = 60 * 60;
|
||||
public const long CACHE_EXPIRE_1D = 24 * CACHE_EXPIRE_1H;
|
||||
public const long CACHE_EXPIRE_7D = 7 * CACHE_EXPIRE_1D;
|
||||
public const long CACHE_EXPIRE_30D = 30 * CACHE_EXPIRE_1D;
|
||||
|
||||
/// <summary>分布式锁 Key 前缀</summary>
|
||||
public const string LockPrefix = CachePrefix + ".lock";
|
||||
|
||||
/// <summary>
|
||||
/// 生成规范 Key:CachePrefix + "." + 各段拼接。
|
||||
/// 例:KeyOf("sch", "list") → "yh.docprint.sch.list"
|
||||
/// </summary>
|
||||
public static string KeyOf(params string[] parts)
|
||||
{
|
||||
return CachePrefix + "." + string.Join(".", parts);
|
||||
}
|
||||
|
||||
/// <summary>生成分布式锁 Key</summary>
|
||||
public static string LockKeyOf(string name)
|
||||
{
|
||||
return LockPrefix + "." + name;
|
||||
}
|
||||
|
||||
// TODO:业务缓存 Key 按模块在此追加(参照 Java 版 SYSTEM_* / SCHOOL_* 的命名风格)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace YhCore.Common.Cache
|
||||
{
|
||||
/// <summary>
|
||||
/// Redis 缓存注册扩展(对应 Java RedisConfig 的 Bean 装配)。
|
||||
/// 连接串取配置 "Redis:ConnectionString",如 "localhost:6379,defaultDatabase=0"。
|
||||
/// </summary>
|
||||
public static class RedisServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddRedisCache(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var connStr = configuration["Redis:ConnectionString"];
|
||||
if (string.IsNullOrWhiteSpace(connStr))
|
||||
{
|
||||
connStr = "localhost:6379,defaultDatabase=0";
|
||||
}
|
||||
|
||||
// 连接多路复用器全局单例(对应 Java 单例 RedisTemplate)
|
||||
services.AddSingleton<IConnectionMultiplexer>(
|
||||
ConnectionMultiplexer.Connect(connStr));
|
||||
services.AddSingleton<RedisUtil>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace YhCore.Common.Cache
|
||||
{
|
||||
/// <summary>
|
||||
/// Redis 操作封装(对应 Java 版 utils.RedisUtil)。
|
||||
/// 基于 StackExchange.Redis,对象值统一 JSON 序列化(Newtonsoft,与全局 JSON 方案一致);
|
||||
/// Incr/Decr 计数类 Key 存裸数字,不走 JSON。
|
||||
/// 注册为单例(内部复用 IConnectionMultiplexer,线程安全)。
|
||||
/// </summary>
|
||||
public class RedisUtil
|
||||
{
|
||||
private readonly IConnectionMultiplexer _connection;
|
||||
|
||||
public RedisUtil(IConnectionMultiplexer connection)
|
||||
{
|
||||
_connection = connection;
|
||||
}
|
||||
|
||||
private IDatabase Db => _connection.GetDatabase();
|
||||
|
||||
// ==================== Key / 过期 ====================
|
||||
|
||||
/// <summary>设置过期时间(秒),对应 expire(key, time)</summary>
|
||||
public bool Expire(string key, long seconds)
|
||||
{
|
||||
return Db.KeyExpire(key, TimeSpan.FromSeconds(seconds));
|
||||
}
|
||||
|
||||
/// <summary>获取剩余过期时间(秒;0=已过期或无此 key;-1=无过期时间),对应 getExpire</summary>
|
||||
public long GetExpire(string key)
|
||||
{
|
||||
var ttl = Db.KeyTimeToLive(key);
|
||||
return ttl.HasValue ? (long)ttl.Value.TotalSeconds : -1;
|
||||
}
|
||||
|
||||
/// <summary>判断 key 是否存在,对应 hasKey</summary>
|
||||
public bool HasKey(string key)
|
||||
{
|
||||
return Db.KeyExists(key);
|
||||
}
|
||||
|
||||
/// <summary>删除一个或多个 key,对应 del(key...)</summary>
|
||||
public void Del(params string[] keys)
|
||||
{
|
||||
Db.KeyDelete(keys.Select(k => (RedisKey)k).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>按模式扫描 key(对应 keys(pattern);生产慎用通配全库)</summary>
|
||||
public List<string> Keys(string pattern)
|
||||
{
|
||||
var server = _connection.GetServer(_connection.GetEndPoints().First());
|
||||
return server.Keys(pattern: pattern).Select(k => (string)k).ToList();
|
||||
}
|
||||
|
||||
// ==================== String ====================
|
||||
|
||||
/// <summary>读取并反序列化为指定类型;不存在或反序列化失败返回 default</summary>
|
||||
public T Get<T>(string key)
|
||||
{
|
||||
var value = Db.StringGet(key);
|
||||
if (value.IsNullOrEmpty)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return JsonConvert.DeserializeObject<T>(value);
|
||||
}
|
||||
|
||||
/// <summary>读取并反序列化为 List<T>,对应 getList</summary>
|
||||
public List<T> GetList<T>(string key)
|
||||
{
|
||||
return Get<List<T>>(key);
|
||||
}
|
||||
|
||||
/// <summary>写入(不过期),对应 set(key, value)</summary>
|
||||
public bool Set(string key, object value)
|
||||
{
|
||||
return Db.StringSet(key, Serialize(value));
|
||||
}
|
||||
|
||||
/// <summary>写入并设置过期时间(秒),对应 set(key, value, time)</summary>
|
||||
public bool Set(string key, object value, long seconds)
|
||||
{
|
||||
return Db.StringSet(key, Serialize(value), TimeSpan.FromSeconds(seconds));
|
||||
}
|
||||
|
||||
/// <summary>自增(delta 可为负),对应 incr/decr</summary>
|
||||
public long Incr(string key, long delta = 1)
|
||||
{
|
||||
return delta >= 0 ? Db.StringIncrement(key, delta) : Db.StringDecrement(key, -delta);
|
||||
}
|
||||
|
||||
/// <summary>自减,对应 decr</summary>
|
||||
public long Decr(string key, long delta = 1)
|
||||
{
|
||||
return Db.StringDecrement(key, delta);
|
||||
}
|
||||
|
||||
// ==================== Hash ====================
|
||||
|
||||
public T HGet<T>(string key, string field)
|
||||
{
|
||||
var value = Db.HashGet(key, field);
|
||||
return value.IsNullOrEmpty ? default : JsonConvert.DeserializeObject<T>(value);
|
||||
}
|
||||
|
||||
/// <summary>获取整个 Hash(field → 原始 JSON 字符串),对应 hmget</summary>
|
||||
public Dictionary<string, string> HGetAll(string key)
|
||||
{
|
||||
return Db.HashGetAll(key).ToDictionary(e => (string)e.Name, e => (string)e.Value);
|
||||
}
|
||||
|
||||
public bool HSet(string key, string field, object value)
|
||||
{
|
||||
return Db.HashSet(key, field, Serialize(value));
|
||||
}
|
||||
|
||||
/// <summary>写入并设置整个 key 过期时间(秒),对应 hset(key, item, value, time)</summary>
|
||||
public bool HSet(string key, string field, object value, long seconds)
|
||||
{
|
||||
var ok = HSet(key, field, value);
|
||||
if (ok)
|
||||
{
|
||||
Expire(key, seconds);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
/// <summary>批量写入,对应 hmset</summary>
|
||||
public void HMSet(string key, Dictionary<string, object> map, long? seconds = null)
|
||||
{
|
||||
var entries = map.Select(kv => new HashEntry(kv.Key, Serialize(kv.Value))).ToArray();
|
||||
Db.HashSet(key, entries);
|
||||
if (seconds.HasValue)
|
||||
{
|
||||
Expire(key, seconds.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public void HDel(string key, params string[] fields)
|
||||
{
|
||||
Db.HashDelete(key, fields.Select(f => (RedisValue)f).ToArray());
|
||||
}
|
||||
|
||||
public bool HHasKey(string key, string field)
|
||||
{
|
||||
return Db.HashExists(key, field);
|
||||
}
|
||||
|
||||
public double HIncr(string key, string field, double by = 1)
|
||||
{
|
||||
return Db.HashIncrement(key, field, by);
|
||||
}
|
||||
|
||||
public double HDecr(string key, string field, double by = 1)
|
||||
{
|
||||
return Db.HashDecrement(key, field, by);
|
||||
}
|
||||
|
||||
// ==================== Set ====================
|
||||
|
||||
public List<T> SGet<T>(string key)
|
||||
{
|
||||
return Db.SetMembers(key)
|
||||
.Select(v => JsonConvert.DeserializeObject<T>(v))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public bool SHasKey(string key, object value)
|
||||
{
|
||||
return Db.SetContains(key, Serialize(value));
|
||||
}
|
||||
|
||||
/// <summary>向 Set 添加成员,返回新增数量,对应 sSet</summary>
|
||||
public long SAdd(string key, params object[] values)
|
||||
{
|
||||
return Db.SetAdd(key, values.Select(Serialize).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>添加并设置过期时间(秒),对应 sSetAndTime</summary>
|
||||
public long SAdd(string key, long seconds, params object[] values)
|
||||
{
|
||||
var count = SAdd(key, values);
|
||||
Expire(key, seconds);
|
||||
return count;
|
||||
}
|
||||
|
||||
public long SSize(string key)
|
||||
{
|
||||
return Db.SetLength(key);
|
||||
}
|
||||
|
||||
/// <summary>移除成员,返回移除数量,对应 setRemove</summary>
|
||||
public long SRemove(string key, params object[] values)
|
||||
{
|
||||
return Db.SetRemove(key, values.Select(Serialize).ToArray());
|
||||
}
|
||||
|
||||
// ==================== List ====================
|
||||
|
||||
/// <summary>按区间读取(-1 表示末尾),对应 lGet</summary>
|
||||
public List<T> LGet<T>(string key, long start = 0, long end = -1)
|
||||
{
|
||||
return Db.ListRange(key, start, end)
|
||||
.Select(v => JsonConvert.DeserializeObject<T>(v))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public long LSize(string key)
|
||||
{
|
||||
return Db.ListLength(key);
|
||||
}
|
||||
|
||||
public T LGetIndex<T>(string key, long index)
|
||||
{
|
||||
var value = Db.ListGetByIndex(key, index);
|
||||
return value.IsNullOrEmpty ? default : JsonConvert.DeserializeObject<T>(value);
|
||||
}
|
||||
|
||||
/// <summary>右推入单个元素,对应 lSet(key, value)</summary>
|
||||
public long LPush(string key, object value, long? seconds = null)
|
||||
{
|
||||
var len = Db.ListRightPush(key, Serialize(value));
|
||||
if (seconds.HasValue)
|
||||
{
|
||||
Expire(key, seconds.Value);
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
/// <summary>右推入整个列表(覆盖式写入对应 Java lSet(key, list))</summary>
|
||||
public long LPushRange(string key, IEnumerable<object> values, long? seconds = null)
|
||||
{
|
||||
var len = Db.ListRightPush(key, values.Select(Serialize).ToArray());
|
||||
if (seconds.HasValue)
|
||||
{
|
||||
Expire(key, seconds.Value);
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
/// <summary>按索引更新,对应 lUpdateIndex</summary>
|
||||
public void LSetByIndex(string key, long index, object value)
|
||||
{
|
||||
Db.ListSetByIndex(key, index, Serialize(value));
|
||||
}
|
||||
|
||||
/// <summary>移除 count 个等于 value 的元素,对应 lRemove</summary>
|
||||
public long LRemove(string key, long count, object value)
|
||||
{
|
||||
return Db.ListRemove(key, Serialize(value), count);
|
||||
}
|
||||
|
||||
// ==================== 分布式锁 ====================
|
||||
|
||||
/// <summary>
|
||||
/// 尝试获取分布式锁(SET NX EX)。token 建议使用 Guid,
|
||||
/// 释放时必须传入同一 token(防止误删他人锁)。
|
||||
/// </summary>
|
||||
public bool TryLock(string name, string token, int expireSeconds = 30)
|
||||
{
|
||||
return Db.StringSet(RedisConstants.LockKeyOf(name), token,
|
||||
TimeSpan.FromSeconds(expireSeconds), When.NotExists);
|
||||
}
|
||||
|
||||
/// <summary>释放锁(Lua 校验 token 后删除,原子操作)</summary>
|
||||
public bool ReleaseLock(string name, string token)
|
||||
{
|
||||
const string script =
|
||||
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
|
||||
var result = (int)Db.ScriptEvaluate(script,
|
||||
new RedisKey[] { RedisConstants.LockKeyOf(name) },
|
||||
new RedisValue[] { token });
|
||||
return result == 1;
|
||||
}
|
||||
|
||||
private static RedisValue Serialize(object value)
|
||||
{
|
||||
return value is string s ? s : JsonConvert.SerializeObject(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace YhCore.Common.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局配置门面(对齐 Java com.yunhai.bschclassmanage.config.Config 枚举:
|
||||
/// API_PATH / MOBILEAPI_PATH / OAPI + FILE_WEB_PATH / FILE_PATH)。
|
||||
/// 路由常量复用 <see cref="YhCore.Common.ApiRoutes"/>;文件路径由 Startup 从配置初始化。
|
||||
/// </summary>
|
||||
public static class ApiPathConfig
|
||||
{
|
||||
/// <summary>PC 端接口前缀(对应 Java Config.API_PATH)</summary>
|
||||
public const string ApiPath = "api";
|
||||
|
||||
/// <summary>移动端接口前缀(对应 Java Config.MOBILEAPI_PATH)</summary>
|
||||
public const string MobiApiPath = "mobiapi";
|
||||
|
||||
/// <summary>开放接口前缀(对应 Java Config.OAPI)</summary>
|
||||
public const string OApiPath = "oapi";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace YhCore.Common.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局配置静态门面(对齐 Java com.yunhai.bschclassmanage.config.Config 枚举:
|
||||
/// API_PATH / MOBILEAPI_PATH / OAPI + FILE_WEB_PATH / FILE_PATH)。
|
||||
/// 路由常量复用 <see cref="YhCore.Common.ApiRoutes"/>;文件路径由 Startup 从配置初始化。
|
||||
/// </summary>
|
||||
public static class Config
|
||||
{
|
||||
/// <summary>PC 端接口前缀(对应 Java Config.API_PATH)</summary>
|
||||
public const string ApiPath = "api";
|
||||
|
||||
/// <summary>移动端接口前缀(对应 Java Config.MOBILEAPI_PATH)</summary>
|
||||
public const string MobiApiPath = "mobiapi";
|
||||
|
||||
/// <summary>开放接口前缀(对应 Java Config.OAPI)</summary>
|
||||
public const string OApiPath = "oapi";
|
||||
|
||||
/// <summary>文件存储根路径(对应 Java FILE_PATH,由 Startup 注入)</summary>
|
||||
public static string FilePath { get; set; }
|
||||
|
||||
/// <summary>文件访问 URL 前缀(对应 Java FILE_WEB_PATH,由 Startup 注入)</summary>
|
||||
public static string FileWebPath { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace YhCore.Common.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据库配置(对应 appsettings DatabaseProvider + ConnectionStrings 节)。
|
||||
/// </summary>
|
||||
public class DatabaseConfig
|
||||
{
|
||||
/// <summary>数据库提供者:SqlServer / MySql</summary>
|
||||
public string DatabaseProvider { get; set; } = "SqlServer";
|
||||
|
||||
/// <summary>SQL Server 连接串</summary>
|
||||
public string SqlServer { get; set; }
|
||||
|
||||
/// <summary>MySQL 连接串</summary>
|
||||
public string Mysql { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace YhCore.Common.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件存储配置(对应 appsettings FILE_PATH / FILE_WEB_PATH,对齐 Java config.FileConfig)。
|
||||
/// </summary>
|
||||
public class FileConfig
|
||||
{
|
||||
/// <summary>文件存储根路径(对应 Java FILE_PATH)</summary>
|
||||
public string FilePath { get; set; }
|
||||
|
||||
/// <summary>文件访问 URL 前缀(对应 Java FILE_WEB_PATH)</summary>
|
||||
public string FileWebPath { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace YhCore.Common.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// Redis 配置(对应 appsettings Redis 节,对齐 Java config.RedisConfig)。
|
||||
/// </summary>
|
||||
public class RedisConfig
|
||||
{
|
||||
/// <summary>连接串(如 localhost:6379,defaultDatabase=0)</summary>
|
||||
public string ConnectionString { get; set; } = "localhost:6379,defaultDatabase=0";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace YhCore.Common.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// Swagger 配置(对应 appsettings Swagger 节,对齐 Java config.SwaggerConfig)。
|
||||
/// </summary>
|
||||
public class SwaggerConfig
|
||||
{
|
||||
public string Title { get; set; } = "YhCore API";
|
||||
|
||||
public string Description { get; set; }
|
||||
|
||||
public string Version { get; set; } = "v1";
|
||||
|
||||
/// <summary>分组(api / mobiapi / oapi)</summary>
|
||||
public string[] Groups { get; set; } = { ApiPathConfig.ApiPath, ApiPathConfig.MobiApiPath, ApiPathConfig.OApiPath };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using Yh.Platform.Abstractions;
|
||||
|
||||
namespace YhCore.Common.Exceptions
|
||||
{
|
||||
/// <summary>int 错误码 → 新框架 <see cref="IErrorCode"/> 映射(对齐 Java ErrorCode 常量值)。</summary>
|
||||
/// <remarks>
|
||||
/// 用途:业务直接抛 <see cref="DException"/> 时,把数值错误码转成 IErrorCode:
|
||||
/// <code>throw new DException(ErrorCodeUtil.From(5001), "库存不足");</code>
|
||||
/// </remarks>
|
||||
public static class ErrorCodeUtil
|
||||
{
|
||||
public const int Default = 1;
|
||||
|
||||
/// <summary>按数值映射平台已知错误码;未知码保留原值(避免业务自定义错误码被吞成 Error=1)。</summary>
|
||||
public static IErrorCode From(int code) => code switch
|
||||
{
|
||||
0 => ErrorCode.Success,
|
||||
2 => ErrorCode.ParamError,
|
||||
3 => ErrorCode.Forbidden,
|
||||
4 => ErrorCode.Unauthorized,
|
||||
5 => ErrorCode.Custom,
|
||||
6 => ErrorCode.InternalServerError,
|
||||
405 => ErrorCode.HttpMethodNotAllowed,
|
||||
_ => new RawErrorCode(code, ""),
|
||||
};
|
||||
|
||||
/// <summary>未知码透传实现:保留调用方原始 code,msg 由业务消息承载。</summary>
|
||||
private sealed record RawErrorCode(int Code, string Msg) : IErrorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using Yh.Platform.Abstractions;
|
||||
|
||||
namespace YhCore.Common.Result
|
||||
{
|
||||
/// <summary>
|
||||
/// 统一响应(对应 Java yh-api-std:com.yunhai.platform.apistd.ApiResponse)。
|
||||
/// 字段与 Java 对齐:code/msg/data(Newtonsoft CamelCase 序列化输出小写)。
|
||||
/// 注意:Controller 返回值由全局过滤器 GlobalWrapResultFilter 自动包装;
|
||||
/// 不需要包装的接口用 [NoWrapResult] 特性显式排除(CAS 协议端点、文件下载等)。
|
||||
/// </summary>
|
||||
public class ApiResult<T>
|
||||
{
|
||||
/// <summary>0=成功,非 0=失败(对齐 Java ErrorCode)</summary>
|
||||
public int Code { get; set; }
|
||||
|
||||
/// <summary>提示消息(对齐 Java msg)</summary>
|
||||
public string Msg { get; set; }
|
||||
|
||||
public T Data { get; set; }
|
||||
|
||||
public static ApiResult<T> Ok(T data) => new() { Code = ErrorCode.Success.Code, Msg = "", Data = data };
|
||||
|
||||
public static ApiResult<T> Ok() => new() { Code = ErrorCode.Success.Code, Msg = "" };
|
||||
|
||||
/// <summary>失败(对齐 Java fail(IErrorCode) / fail(int, String))</summary>
|
||||
public static ApiResult<T> Fail(int code, string msg) => new() { Code = code, Msg = msg };
|
||||
|
||||
/// <summary>失败(保留旧签名:msg 在前,默认错误码 ERROR=1)</summary>
|
||||
public static ApiResult<T> Fail(string msg, int code = 1) => new() { Code = code, Msg = msg };
|
||||
|
||||
/// <summary>失败(对齐 Java fail(IErrorCode[, String]),msg 缺省取错误码默认消息)</summary>
|
||||
public static ApiResult<T> Fail(IErrorCode errorCode, string msg = null) => new()
|
||||
{
|
||||
Code = errorCode.Code,
|
||||
Msg = string.IsNullOrEmpty(msg) ? errorCode.Msg : msg
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>无数据体的统一响应便捷类</summary>
|
||||
public static class ApiResult
|
||||
{
|
||||
public static ApiResult<object> Ok(object data = null) => ApiResult<object>.Ok(data);
|
||||
public static ApiResult<object> Fail(string msg, int code = 1) => ApiResult<object>.Fail(msg, code);
|
||||
public static ApiResult<object> Fail(int code, string msg) => ApiResult<object>.Fail(code, msg);
|
||||
public static ApiResult<object> Fail(IErrorCode errorCode, string msg = null) => ApiResult<object>.Fail(errorCode, msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
namespace YhCore.Common.UserContext
|
||||
{
|
||||
/// <summary>
|
||||
/// 当前登录用户上下文(对应 Java 版 com.yunhai.bschclassmanage.utils.UserContext)。
|
||||
/// 由 UserContextMiddleware 在请求入口写入,全链路通过 <see cref="UserContextHolder"/> 读取。
|
||||
/// </summary>
|
||||
public class UserContext
|
||||
{
|
||||
/// <summary>用户 ID(对应 BoaUser.id)</summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>用户姓名</summary>
|
||||
public string UserName { get; set; }
|
||||
|
||||
/// <summary>登录账号</summary>
|
||||
public string Account { get; set; }
|
||||
|
||||
/// <summary>所属学校 ID(对应 Java UserContext.getSchId())</summary>
|
||||
public int? SchoolId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属学校 UUID 标识(旧 SysUser.schId / SchInfoVo.SchSerId,如 "xxxx-xxxx-xxxx")。
|
||||
/// 由 LegacyUserContextAdapter 通过教务 SDK GetSchFirstAsync 反查填充(进程内缓存,避免每请求调用);
|
||||
/// 反查失败或无学校时为 null。
|
||||
/// </summary>
|
||||
public string SchSerID { get; set; }
|
||||
|
||||
/// <summary>所属学校名称</summary>
|
||||
public string SchoolName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户类型(对应 Java UserContext.getType() / BoaUser.Type;1=教师等,具体枚举见用户中心约定)。
|
||||
/// </summary>
|
||||
public int? UserType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色 ID(对应 Java UserContext.getRoleId():取 BoaUser.RoleIds 首个,回退 CurrentRole.RoleId)。
|
||||
/// </summary>
|
||||
public int? Role { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户中心返回的完整 BoaUser(JSON 从认证 Claim 反序列化,GetUserInfo 直接返回)。
|
||||
/// </summary>
|
||||
public object FullBoaUser { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Threading;
|
||||
|
||||
namespace YhCore.Common.UserContext
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户上下文持有器。基于 <see cref="AsyncLocal{T}"/> 实现,
|
||||
/// 是 Java ThreadLocal 方案在 .NET async 环境下的安全等价物。
|
||||
/// </summary>
|
||||
public static class UserContextHolder
|
||||
{
|
||||
private static readonly AsyncLocal<UserContext> _current = new AsyncLocal<UserContext>();
|
||||
|
||||
/// <summary>当前请求的用户;未登录或中间件未写入时为 null。</summary>
|
||||
public static UserContext Current
|
||||
{
|
||||
get => _current.Value;
|
||||
set => _current.Value = value;
|
||||
}
|
||||
|
||||
/// <summary>当前用户学校 ID;无上下文时返回 0(对应 Java UserContext.getSchId())。</summary>
|
||||
public static int GetSchId() => _current.Value?.SchoolId ?? 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace YhCore.Common.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页元信息(对应 Java 版 model.tool.PageInfo)。
|
||||
/// C# 属性为 PascalCase,ASP.NET Core 默认 camelCase 序列化后
|
||||
/// JSON 字段与 Java 版一致(pageNum/pageSize/total/totalPages/hasNext/hasPrev)。
|
||||
/// </summary>
|
||||
public class PageInfo
|
||||
{
|
||||
/// <summary>当前页码(从 0 开始,与 Spring Pageable 一致)</summary>
|
||||
public int PageNum { get; set; }
|
||||
|
||||
/// <summary>每页大小</summary>
|
||||
public int PageSize { get; set; }
|
||||
|
||||
/// <summary>总记录数</summary>
|
||||
public int Total { get; set; }
|
||||
|
||||
/// <summary>总页数</summary>
|
||||
public int TotalPages { get; set; }
|
||||
|
||||
/// <summary>是否有下一页</summary>
|
||||
public bool HasNext { get; set; }
|
||||
|
||||
/// <summary>是否有上一页</summary>
|
||||
public bool HasPrev { get; set; }
|
||||
|
||||
public PageInfo()
|
||||
{
|
||||
}
|
||||
|
||||
public PageInfo(int pageNum, int pageSize, int total, int totalPages, bool hasNext, bool hasPrev)
|
||||
{
|
||||
PageNum = pageNum;
|
||||
PageSize = pageSize;
|
||||
Total = total;
|
||||
TotalPages = totalPages;
|
||||
HasNext = hasNext;
|
||||
HasPrev = hasPrev;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace YhCore.Common.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// 内存分页工具(对应 Java 版 utils.PageUtil)。
|
||||
/// 用于已对全量数据在内存中的场景(如外部接口聚合结果)做分页;
|
||||
/// 数据库分页请在 Repository 层用 IQueryable Skip/Take 下推,不要本类。
|
||||
/// 页码从 0 开始,与 Spring Pageable 一致。
|
||||
/// </summary>
|
||||
public static class PageUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// 对列表做内存分页,返回当前页数据(对应 Java paginate)。
|
||||
/// 空列表或页码越界时返回空列表。
|
||||
/// </summary>
|
||||
/// <param name="list">全量数据</param>
|
||||
/// <param name="pageNum">页码,从 0 开始</param>
|
||||
/// <param name="pageSize">每页大小</param>
|
||||
public static List<T> Paginate<T>(IReadOnlyCollection<T> list, int pageNum, int pageSize)
|
||||
{
|
||||
if (list == null || list.Count == 0 || pageSize <= 0)
|
||||
{
|
||||
return new List<T>();
|
||||
}
|
||||
|
||||
var fromIndex = pageNum * pageSize;
|
||||
if (fromIndex >= list.Count)
|
||||
{
|
||||
return new List<T>();
|
||||
}
|
||||
|
||||
return list.Skip(fromIndex).Take(pageSize).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取分页元信息(对应 Java getPageInfo)。
|
||||
/// </summary>
|
||||
/// <param name="list">原始数据列表</param>
|
||||
/// <param name="pageNum">页码,从 0 开始</param>
|
||||
/// <param name="pageSize">每页大小</param>
|
||||
public static PageInfo GetPageInfo<T>(IReadOnlyCollection<T> list, int pageNum, int pageSize)
|
||||
{
|
||||
var total = list?.Count ?? 0;
|
||||
var totalPages = pageSize > 0 ? (int)Math.Ceiling((double)total / pageSize) : 0;
|
||||
return new PageInfo(
|
||||
pageNum,
|
||||
pageSize,
|
||||
total,
|
||||
totalPages,
|
||||
hasNext: pageNum < totalPages - 1,
|
||||
hasPrev: pageNum > 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 内存分页 + 元信息一次完成(Paginate 与 GetPageInfo 的组合便捷方法)。
|
||||
/// </summary>
|
||||
public static (List<T> Items, PageInfo Info) PaginateWithInfo<T>(
|
||||
IReadOnlyCollection<T> list, int pageNum, int pageSize)
|
||||
{
|
||||
return (Paginate(list, pageNum, pageSize), GetPageInfo(list, pageNum, pageSize));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<RootNamespace>YhCore.Common</RootNamespace>
|
||||
<AssemblyName>YhCore.Common</AssemblyName>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="StackExchange.Redis" Version="2.6.122" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Yh.Platform.Abstractions">
|
||||
<HintPath>..\..\..\Library\Yh.Platform.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,25 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class A_DataLog
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public string ModuleName { get; set; }
|
||||
public string TableName { get; set; }
|
||||
public Nullable<int> OldSerID { get; set; }
|
||||
public Nullable<int> NowSerID { get; set; }
|
||||
public Nullable<int> RId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class AppMsgRecord
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public int AppSerID { get; set; }
|
||||
public string AppName { get; set; }
|
||||
public string AppCode { get; set; }
|
||||
public string MsgType { get; set; }
|
||||
public string MsgContent { get; set; }
|
||||
public string MsgError { get; set; }
|
||||
public string UserID { get; set; }
|
||||
public string SUserID { get; set; }
|
||||
public string SUserPhone { get; set; }
|
||||
public string SendType { get; set; }
|
||||
public string TabCode { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public Nullable<System.DateTime> Datetime { get; set; }
|
||||
public Nullable<int> Read { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Base_Log
|
||||
{
|
||||
public Nullable<System.DateTime> Date { get; set; }
|
||||
public Nullable<int> Thread { get; set; }
|
||||
public string LEVEL { get; set; }
|
||||
public string Logger { get; set; }
|
||||
public string MESSAGE { get; set; }
|
||||
public string EXCEPTION { get; set; }
|
||||
public int Id { get; set; }
|
||||
public string AppName { get; set; }
|
||||
public string UserId { get; set; }
|
||||
public string Type { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_Dictionary
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public string typeID { get; set; }
|
||||
public string typeName { get; set; }
|
||||
public string CodeID { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Des { get; set; }
|
||||
public int Enable { get; set; }
|
||||
public string Schserid { get; set; }
|
||||
public Nullable<System.Guid> PID { get; set; }
|
||||
public string Path { get; set; }
|
||||
public Nullable<int> Sort { get; set; }
|
||||
public string Code { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_DictionaryOrd
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public int DicSerID { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public Nullable<bool> Open { get; set; }
|
||||
public Nullable<int> Ord { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_Groupuser
|
||||
{
|
||||
public int id { get; set; }
|
||||
public Nullable<int> usergroup { get; set; }
|
||||
public Nullable<int> userid { get; set; }
|
||||
public Nullable<int> flowid { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public Nullable<int> IsOk { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_Groupuser_SHRole
|
||||
{
|
||||
public int id { get; set; }
|
||||
public Nullable<int> usergroup { get; set; }
|
||||
public Nullable<decimal> SHRoleid { get; set; }
|
||||
public Nullable<int> flowid { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public Nullable<int> IsOk { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_SendUserinfo
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public Nullable<int> userid { get; set; }
|
||||
public string Senduserid { get; set; }
|
||||
public string schserid { get; set; }
|
||||
public Nullable<System.DateTime> createTime { get; set; }
|
||||
public Nullable<int> flowType { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_UpFiles
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string TopicType { get; set; }
|
||||
public Nullable<int> TopicID { get; set; }
|
||||
public string FileTrueName { get; set; }
|
||||
public string FileName { get; set; }
|
||||
public Nullable<int> FileSize { get; set; }
|
||||
public string FileType { get; set; }
|
||||
public Nullable<System.DateTime> DateAndTime { get; set; }
|
||||
public Nullable<int> DownTimes { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_UserFlowZhipai
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public int UserID { get; set; }
|
||||
public int MSerID { get; set; }
|
||||
public Nullable<int> FlowID { get; set; }
|
||||
public Nullable<int> FlowStepID { get; set; }
|
||||
public string zhipai { get; set; }
|
||||
public string type { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_flowinfo
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string flowname { get; set; }
|
||||
public string flowdes { get; set; }
|
||||
public int enable { get; set; }
|
||||
public int flowType { get; set; }
|
||||
public System.DateTime deitdate { get; set; }
|
||||
public string schSerID { get; set; }
|
||||
public string deptid { get; set; }
|
||||
public string typeid { get; set; }
|
||||
public string ChaoSend { get; set; }
|
||||
public Nullable<int> StepSend { get; set; }
|
||||
public string WYPageGG { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_flowstep
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public Nullable<int> flowid { get; set; }
|
||||
public string StepName { get; set; }
|
||||
public Nullable<int> ordernum { get; set; }
|
||||
public Nullable<int> prestep { get; set; }
|
||||
public Nullable<int> action_user { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public Nullable<int> cond { get; set; }
|
||||
public string isshowJE { get; set; }
|
||||
public string isshowBtn { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_flowtranctInfo
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string datatype { get; set; }
|
||||
public int dataid { get; set; }
|
||||
public int stepid { get; set; }
|
||||
public Nullable<int> actionuserid { get; set; }
|
||||
public short actionstatus { get; set; }
|
||||
public Nullable<System.DateTime> actiondate { get; set; }
|
||||
public string remark { get; set; }
|
||||
public string undertaker { get; set; }
|
||||
public string Pid { get; set; }
|
||||
public string DoneUserID { get; set; }
|
||||
public string filespath { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_flowuser
|
||||
{
|
||||
public int id { get; set; }
|
||||
public Nullable<int> usergroup { get; set; }
|
||||
public Nullable<int> userid { get; set; }
|
||||
public Nullable<int> flowid { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_freeGroupuser
|
||||
{
|
||||
public int id { get; set; }
|
||||
public Nullable<int> usergroup { get; set; }
|
||||
public Nullable<int> userid { get; set; }
|
||||
public Nullable<int> flowid { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public Nullable<int> IsOk { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class Common_freeflowinfo
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public Nullable<int> userid { get; set; }
|
||||
public string shenheuserid { get; set; }
|
||||
public string schserid { get; set; }
|
||||
public Nullable<int> steptype { get; set; }
|
||||
public Nullable<System.DateTime> createTime { get; set; }
|
||||
public Nullable<int> flowType { get; set; }
|
||||
public Nullable<int> dataid { get; set; }
|
||||
public Nullable<int> laststep { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
/// <summary>
|
||||
/// 系统审计日志(对应 Java 版 entity.SysOperationLog,表 sys_operation_log)。
|
||||
/// HTTP 接口级全量日志:由 SysOperationLogFilter 在每次 Controller 请求后写入。
|
||||
/// 字段与 Java 实体一一对齐;重试三字段(RetryCount/LastRetryStatus/LastRetryTime)
|
||||
/// 为 Java 双轨同步保留,.NET 侧当前不写入(保持 NULL/默认)。
|
||||
/// 表名映射见 YHDbContext.OnModelCreating:ToTable("sys_operation_log")。
|
||||
/// </summary>
|
||||
public class SysOperationLog
|
||||
{
|
||||
public long Id { get; set; }
|
||||
|
||||
/// <summary>日志级别:1-重要(删除) 2-次要(新增/编辑) 3-一般(查询)</summary>
|
||||
public int LogLevel { get; set; }
|
||||
|
||||
/// <summary>操作类型:DELETE / CREATE / UPDATE / QUERY</summary>
|
||||
public string OperationType { get; set; }
|
||||
|
||||
/// <summary>操作人 ID(来自 UserContext.Id)</summary>
|
||||
public int? UserId { get; set; }
|
||||
|
||||
/// <summary>操作人姓名(来自 UserContext.UserName,匿名时 anonymous)</summary>
|
||||
public string UserName { get; set; }
|
||||
|
||||
/// <summary>学校 ID(多校隔离,来自 UserContext.SchoolId)</summary>
|
||||
public int? SchId { get; set; }
|
||||
|
||||
/// <summary>客户端 IP</summary>
|
||||
public string RequestIp { get; set; }
|
||||
|
||||
/// <summary>请求 URI</summary>
|
||||
public string RequestUri { get; set; }
|
||||
|
||||
/// <summary>HTTP 方法</summary>
|
||||
public string RequestMethod { get; set; }
|
||||
|
||||
/// <summary>类名.方法名</summary>
|
||||
public string ClassMethod { get; set; }
|
||||
|
||||
/// <summary>请求参数(JSON 序列化,含截断)</summary>
|
||||
public string RequestParams { get; set; }
|
||||
|
||||
/// <summary>GZIP 压缩后的响应 JSON</summary>
|
||||
public byte[] ResponseData { get; set; }
|
||||
|
||||
/// <summary>业务状态码:0 成功 / 非 0 失败(全局返回对象 code)</summary>
|
||||
public int StatusCode { get; set; }
|
||||
|
||||
/// <summary>耗时(ms)</summary>
|
||||
public long CostTime { get; set; }
|
||||
|
||||
/// <summary>User-Agent</summary>
|
||||
public string UserAgent { get; set; }
|
||||
|
||||
/// <summary>异常信息(截断 8000)</summary>
|
||||
public string ErrorMsg { get; set; }
|
||||
|
||||
/// <summary>重试次数(对齐 Java 双轨字段,暂不写入)</summary>
|
||||
public int? RetryCount { get; set; }
|
||||
|
||||
/// <summary>上次重试状态:1 成功 / 0 失败(对齐 Java 双轨字段,暂不写入)</summary>
|
||||
public int? LastRetryStatus { get; set; }
|
||||
|
||||
/// <summary>上次重试时间(对齐 Java 双轨字段,暂不写入)</summary>
|
||||
public DateTime? LastRetryTime { get; set; }
|
||||
|
||||
/// <summary>记录时间</summary>
|
||||
public DateTime CreateTime { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class WY_DocPrint
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public string DocTitle { get; set; }
|
||||
public string DocType { get; set; }
|
||||
public string DocStyle { get; set; }
|
||||
public int DocNum { get; set; }
|
||||
public string DocContent { get; set; }
|
||||
public string UserID { get; set; }
|
||||
public System.DateTime EditDate { get; set; }
|
||||
public string ChkUserID { get; set; }
|
||||
public string DocStatus { get; set; }
|
||||
public string ChkDate { get; set; }
|
||||
public string UpFile { get; set; }
|
||||
public string DPType { get; set; }
|
||||
public Nullable<System.DateTime> TakeDate { get; set; }
|
||||
public string DocCode { get; set; }
|
||||
public Nullable<int> DocStep { get; set; }
|
||||
public Nullable<int> flowid { get; set; }
|
||||
public Nullable<int> rid { get; set; }
|
||||
public Nullable<int> reid { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public string explain { get; set; }
|
||||
public string grade { get; set; }
|
||||
public string Examtype { get; set; }
|
||||
public Nullable<int> DocFix { get; set; }
|
||||
public Nullable<int> DocShare { get; set; }
|
||||
public string FreeUserID { get; set; }
|
||||
public string SendUserID { get; set; }
|
||||
public Nullable<int> freestepid { get; set; }
|
||||
public string zhipai { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class WY_DocPrint_Num
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public Nullable<decimal> DocPrintID { get; set; }
|
||||
public string YSLXName { get; set; }
|
||||
public string YSLXtypeID { get; set; }
|
||||
public Nullable<double> YSLXOrder { get; set; }
|
||||
public string YSGGName { get; set; }
|
||||
public string YSGGtypeID { get; set; }
|
||||
public Nullable<double> YSGGOrder { get; set; }
|
||||
public Nullable<int> DocNum { get; set; }
|
||||
public Nullable<int> DocPage { get; set; }
|
||||
public Nullable<int> DocFix { get; set; }
|
||||
public Nullable<int> DocShare { get; set; }
|
||||
public Nullable<double> UnitPrice { get; set; }
|
||||
public Nullable<double> TotalPrice { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class WY_DocPrint_SH
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public Nullable<decimal> DocPrintSerID { get; set; }
|
||||
public Nullable<decimal> flowSerID { get; set; }
|
||||
public Nullable<decimal> stepSerID { get; set; }
|
||||
public Nullable<int> stepOrder { get; set; }
|
||||
public Nullable<decimal> SHTeaID { get; set; }
|
||||
public string SHTeaName { get; set; }
|
||||
public string AndOr { get; set; }
|
||||
public string isSH { get; set; }
|
||||
public Nullable<System.DateTime> inserttime { get; set; }
|
||||
public string writor { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class WY_SHRole
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public string SchSerId { get; set; }
|
||||
public string SHRoleName { get; set; }
|
||||
public Nullable<System.DateTime> inserttime { get; set; }
|
||||
public string writor { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class WY_SHRole_BSHTea
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public Nullable<decimal> SHRoleID { get; set; }
|
||||
public Nullable<decimal> BSHTeaID { get; set; }
|
||||
public string BSHTeaName { get; set; }
|
||||
public Nullable<System.DateTime> inserttime { get; set; }
|
||||
public string writor { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class WY_SHRole_SHTea
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public Nullable<decimal> SHRoleID { get; set; }
|
||||
public Nullable<decimal> SHTeaID { get; set; }
|
||||
public string SHTeaName { get; set; }
|
||||
public string AndOr { get; set; }
|
||||
public string photo { get; set; }
|
||||
public Nullable<System.DateTime> inserttime { get; set; }
|
||||
public string writor { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class WY_SchZyBit
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public Nullable<bool> ziyuanbit { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码已从模板生成。
|
||||
//
|
||||
// 手动更改此文件可能导致应用程序出现意外的行为。
|
||||
// 如果重新生成代码,将覆盖对此文件的手动更改。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace YhCore.Entity
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class WY_reGetJERd
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public Nullable<decimal> DocPrintID { get; set; }
|
||||
public string DocTitle { get; set; }
|
||||
public string DocContent { get; set; }
|
||||
public Nullable<System.DateTime> EditDatetime { get; set; }
|
||||
public string writor { get; set; }
|
||||
public string type { get; set; }
|
||||
public string UnitPrice { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<RootNamespace>YhCore.Entity</RootNamespace>
|
||||
<AssemblyName>YhCore.Entity</AssemblyName>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>disable</Nullable>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,129 @@
|
||||
using System;
|
||||
|
||||
namespace YhCore.Model.Dto
|
||||
{
|
||||
/// <summary>字典下拉框选项(旧 GetDictList 的 {k=CodeID, v=Name})。</summary>
|
||||
public class DicOption
|
||||
{
|
||||
public string K { get; set; }
|
||||
public string V { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>字典管理列表项(旧 GetDictMangeList 的 {SerID, CodeID, Name, Des, Enable})。</summary>
|
||||
public class DicManageItem
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public string CodeID { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Des { get; set; }
|
||||
public int Enable { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>文印分类 / 印制版面列表项(旧 CategoryList / YZBMList)。</summary>
|
||||
public class CategoryItem
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public string CodeID { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Des { get; set; }
|
||||
public int Enable { get; set; }
|
||||
public int? Sort { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public string UserSchSerID { get; set; }
|
||||
public bool _disabled { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>公共类别列表项(旧 DictionaryTypeList)。</summary>
|
||||
public class DictionaryTypeItem
|
||||
{
|
||||
public string Code { get; set; }
|
||||
public decimal SerID { get; set; }
|
||||
public string CodeID { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Des { get; set; }
|
||||
public int Enable { get; set; }
|
||||
public int? Sort { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public string UserSchSerID { get; set; }
|
||||
public bool _disabled { get; set; }
|
||||
public bool Status { get; set; }
|
||||
public bool Open { get; set; }
|
||||
public int Ord { get; set; }
|
||||
public string Path { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 字典类型常量与映射(对齐旧 EnumConfig.Dictionary 枚举及 CommonController 的硬编码 switch)。
|
||||
/// </summary>
|
||||
public static class DictionaryTypeHelper
|
||||
{
|
||||
/// <summary>GetSysDic 入参 Type(1~10) → 字典 typeID(对齐旧 switch)。</summary>
|
||||
private static readonly string[] SysDic =
|
||||
{ null, "DEPT", "WYFL", "YZBM", "YZFL", "LeaveType", "FoodType", "Unit", "LevelOrder", "filetype", "doclevel" };
|
||||
|
||||
/// <summary>旧 EnumConfig.Dictionary 枚举序值 → typeID(用于 GetDictMangeList 的 Type 参数)。</summary>
|
||||
private static readonly string[] EnumOrdinal =
|
||||
{ "DEPT", "BXBM", "WYFL", "YZBM", "YZFL", "LeaveType", "FoodType", "Unit", "VenueType", "Vefacilities", "Time", "Repeat", "LevelOrder", "filetype", "doclevel", "XKCategory" };
|
||||
|
||||
/// <summary>全部字典类别(typeID, typeName),对齐旧 EnumConfig.Dictionary 的 Description。</summary>
|
||||
public static readonly (string Id, string Name)[] AllTypes =
|
||||
{
|
||||
("DEPT", "部门类别"),
|
||||
("BXBM", "报修部门"),
|
||||
("WYFL", "文印分类"),
|
||||
("YZBM", "印制版面"),
|
||||
("YZFL", "印制分类"),
|
||||
("LeaveType", "请假类别"),
|
||||
("FoodType", "物品类别"),
|
||||
("Unit", "单位"),
|
||||
("VenueType", "场馆类别"),
|
||||
("Vefacilities", "场馆设施"),
|
||||
("Time", "场馆时间"),
|
||||
("Repeat", "场馆重复"),
|
||||
("LevelOrder", "奖次"),
|
||||
("filetype", "文件类型"),
|
||||
("doclevel", "公文级别"),
|
||||
("XKCategory", "选课类型"),
|
||||
};
|
||||
|
||||
/// <summary>GetSysDic 的 Type(int) → typeID;越界返回 null。</summary>
|
||||
public static string FromSysDicType(int type) => type >= 1 && type <= 10 ? SysDic[type] : null;
|
||||
|
||||
/// <summary>GetDictMangeList 的 Type 参数 → typeID(支持枚举名、枚举序值、typeID 直传)。</summary>
|
||||
public static string ResolveTypeId(string type)
|
||||
{
|
||||
if (string.IsNullOrEmpty(type)) return null;
|
||||
foreach (var t in AllTypes)
|
||||
{
|
||||
if (t.Id == type) return t.Id;
|
||||
}
|
||||
|
||||
if (int.TryParse(type, out var n) && n >= 0 && n < EnumOrdinal.Length)
|
||||
{
|
||||
return EnumOrdinal[n];
|
||||
}
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
/// <summary>typeID → typeName(描述);未知则原样返回。</summary>
|
||||
public static string GetTypeName(string typeId)
|
||||
{
|
||||
foreach (var t in AllTypes)
|
||||
{
|
||||
if (t.Id == typeId) return t.Name;
|
||||
}
|
||||
|
||||
return typeId;
|
||||
}
|
||||
|
||||
public static bool IsDept(string typeId) => typeId == "DEPT";
|
||||
|
||||
/// <summary>Des(排序字符串)→ int,解析失败/空回退 int.MaxValue(对齐旧 int.Parse 的降级,避免非数值崩)。</summary>
|
||||
public static int ParseSort(string des)
|
||||
{
|
||||
if (string.IsNullOrEmpty(des)) return int.MaxValue;
|
||||
return int.TryParse(des, out var n) ? n : int.MaxValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace YhCore.Model.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// 文印统计看板 DTO(移植旧 BestSchool.DocPrint.DocPrintStatistics,5 接口)。
|
||||
/// 旧实现以 dynamic 匿名对象返回 {k,v} 结构,新项目统一为强类型 DTO。
|
||||
/// 说明:旧 GetSchSerID(int) 走 HTTP 把 int schId 映射为 GUID;新库 SchSerID 约定已是 int 字符串,
|
||||
/// 故此处直接用 schId.ToString() 匹配,不再依赖外部转换。
|
||||
/// </summary>
|
||||
|
||||
/// <summary>统计项(旧 GetDocPrintCount 的 {k,v},v 为数量或类型名)。</summary>
|
||||
public class DocPrintCountItem
|
||||
{
|
||||
public string k { get; set; }
|
||||
public object v { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>饼图分类项(旧 GetPieChart 的 series.list 元素 {k,v})。</summary>
|
||||
public class PieChartKv
|
||||
{
|
||||
public string k { get; set; }
|
||||
public object v { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>饼图系列(旧 GetPieChart 的 {title,list})。</summary>
|
||||
public class PieChartSeries
|
||||
{
|
||||
public string title { get; set; }
|
||||
public List<PieChartKv> list { get; set; } = new List<PieChartKv>();
|
||||
}
|
||||
|
||||
/// <summary>印刷类型下拉项(旧 GetDocPrintType 的 {CodeID,Name})。</summary>
|
||||
public class DocPrintTypeItem
|
||||
{
|
||||
public string CodeID { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>文印统计列表项(旧 GetDocPrintList 的投影)。</summary>
|
||||
public class DocPrintStatItem
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string DocContent { get; set; }
|
||||
public string DocType { get; set; }
|
||||
public int DocNum { get; set; }
|
||||
public string DocTypeName { get; set; }
|
||||
public string DocStyle { get; set; }
|
||||
public string DocStyleName { get; set; }
|
||||
public string EditDate { get; set; }
|
||||
public string TakeDate { get; set; }
|
||||
public string UserID { get; set; }
|
||||
public string teaname { get; set; }
|
||||
public string UpFile { get; set; }
|
||||
public string DocTitle { get; set; }
|
||||
public string DocStatus { get; set; }
|
||||
public string DocStatusName { get; set; }
|
||||
public int? DocStep { get; set; }
|
||||
public int? flowid { get; set; }
|
||||
public string FreeUserID { get; set; }
|
||||
public string SendUserID { get; set; }
|
||||
public int? freestepid { get; set; }
|
||||
public int? DocShare { get; set; }
|
||||
public string explain { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>文印统计列表结果(旧 GetDocPrintList 的 {tlist,totle})。</summary>
|
||||
public class DocPrintListResult
|
||||
{
|
||||
public List<DocPrintStatItem> tlist { get; set; } = new List<DocPrintStatItem>();
|
||||
public int totle { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
namespace YhCore.Model.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// 文件信息(旧 BestSchool.Entity.FileObj,GetEnclosure/FileSave/FileSaveImage/FileMergeSave 共用)。
|
||||
/// fileurl 为相对路径(如 /UploadFile/docprint/20260101/{guid}.jpg,与旧库数据格式一致);
|
||||
/// host 为文件访问主机前缀(旧 AuthConfig.weboa_url,新 Config.FileWebPath)。
|
||||
/// </summary>
|
||||
public class FileObj
|
||||
{
|
||||
/// <summary>附件表 SerID(SaveEnclosure 落库后回填)。</summary>
|
||||
public int fileid { get; set; }
|
||||
|
||||
/// <summary>文件大小(字节;旧 FileMergeSave 存的是 MB 整数,保持旧语义)。</summary>
|
||||
public int filesize { get; set; }
|
||||
|
||||
/// <summary>扩展名(含点,如 ".jpg")。</summary>
|
||||
public string filetype { get; set; }
|
||||
|
||||
/// <summary>文件真实名称(含扩展名)。</summary>
|
||||
public string filename { get; set; }
|
||||
|
||||
/// <summary>相对访问路径(如 /UploadFile/docprint/20260101/{guid}.jpg)。</summary>
|
||||
public string fileurl { get; set; }
|
||||
|
||||
/// <summary>文件主机前缀(Config.FileWebPath)。</summary>
|
||||
public string host { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace YhCore.Model.Dto
|
||||
{
|
||||
/// <summary>自由流程审核人(旧 ApproveFlow.GetFleeFlow/GetAfterFleeFlow 的 user 数组元素)。</summary>
|
||||
public class FreeFlowUser
|
||||
{
|
||||
public int userid { get; set; }
|
||||
public string name { get; set; }
|
||||
public string photo { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>自由流程节点(旧 GetFleeFlow 列表元素:{ user: [...] })。</summary>
|
||||
public class FreeFlowNode
|
||||
{
|
||||
public List<FreeFlowUser> user { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>审核流程步骤信息(旧 ApproveFlow.FreeShenheinfo,Remark 序列化后为 camelCase 的 remark)。</summary>
|
||||
public class FreeShenheInfo
|
||||
{
|
||||
public string steptname { get; set; }
|
||||
public string steptstatus { get; set; }
|
||||
public string photo { get; set; }
|
||||
public string Remark { get; set; }
|
||||
public string actiondate { get; set; }
|
||||
public string actionstatus { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>抄送人(旧 ChaoSendList 返回 { uid, photo, name, status })。</summary>
|
||||
public class ChaoSendUser
|
||||
{
|
||||
public int uid { get; set; }
|
||||
public string photo { get; set; }
|
||||
public string name { get; set; }
|
||||
public bool status { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace YhCore.Model.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// DocPrint 应用常量(旧 WebHelper.AppCode / AuthConfig.AppKeyMap["docprint"][3])。
|
||||
/// </summary>
|
||||
public static class DocPrintConstants
|
||||
{
|
||||
/// <summary>文印申请 AppCode,即审核流程 / 自由流程的 flowType。</summary>
|
||||
public const int AppCode = 1003;
|
||||
}
|
||||
|
||||
/// <summary>审核流程列表项(旧 Reviewprocess.List / List2 的匿名类型)。</summary>
|
||||
public class FlowListItem
|
||||
{
|
||||
public string SerID { get; set; }
|
||||
public string flowname { get; set; }
|
||||
public string deptid { get; set; }
|
||||
public string typeid { get; set; }
|
||||
public int enable { get; set; }
|
||||
public string enablename { get; set; }
|
||||
public string miaoshu { get; set; }
|
||||
public string deitdate { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>步骤流程列表项(旧 StepList 的匿名类型)。</summary>
|
||||
public class StepListItem
|
||||
{
|
||||
public string SerID { get; set; }
|
||||
public string StepName { get; set; }
|
||||
public string flowid { get; set; }
|
||||
public int? prestep { get; set; }
|
||||
public string TeaName { get; set; }
|
||||
public string Teaid { get; set; }
|
||||
public bool istrue { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>流程绑定步骤项(旧 BindstepList)。</summary>
|
||||
public class BindStepItem
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string StepName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>绑定步骤人员项(旧 BindGroupuserList)。</summary>
|
||||
public class BindGroupuserItem
|
||||
{
|
||||
public int StepId { get; set; }
|
||||
public int? userid { get; set; }
|
||||
public string StepName { get; set; }
|
||||
public string TeaName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>抄送人(旧 Bindflowinfo 的 ChaoSendArr 元素)。</summary>
|
||||
public class ChaoSendItem
|
||||
{
|
||||
public string uid { get; set; }
|
||||
public string name { get; set; }
|
||||
public string photo { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>绑定流程项(旧 Bindflowinfo)。</summary>
|
||||
public class BindFlowItem
|
||||
{
|
||||
public string SerID { get; set; }
|
||||
public string flowname { get; set; }
|
||||
public List<ChaoSendItem> ChaoSendArr { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>教师下拉选项(旧 GetTeaList 的 {value,label})。</summary>
|
||||
public class TeaOption
|
||||
{
|
||||
public int value { get; set; }
|
||||
public string label { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>附件项(旧 filelist)。</summary>
|
||||
public class FileListItem
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string FileTrueName { get; set; }
|
||||
public string FileName { get; set; }
|
||||
public int? FileSize { get; set; }
|
||||
public string FileType { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>自由流程审核人(旧 IsShenheflow 的 OtherData 元素)。</summary>
|
||||
public class FreeFlowItem
|
||||
{
|
||||
public string shenheuserid { get; set; }
|
||||
public string teamnamestr { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>审核流程类型判断结果(旧 IsShenheflow:Data=flowflag,OtherData=自由流程审核人)。</summary>
|
||||
public class ShenheFlowResult
|
||||
{
|
||||
public bool flowflag { get; set; }
|
||||
public List<FreeFlowItem> freeflowlist { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>绑定抄送人(旧 IsSendUser)。</summary>
|
||||
public class SendUserItem
|
||||
{
|
||||
public int uid { get; set; }
|
||||
public string photo { get; set; }
|
||||
public string name { get; set; }
|
||||
public bool status { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>当前用户信息(旧 GetUser 返回 SysUser,新框架取 UserContext 有限字段)。</summary>
|
||||
public class CurrentUserInfo
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public string Account { get; set; }
|
||||
public int? SchoolId { get; set; }
|
||||
public string SchoolName { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace YhCore.Model.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// 文印新增/修改入参(旧 BestSchool.Entity.Model.WY_DocPrintDto : WY_DocPrint + ctype/sms)。
|
||||
/// SerID 仅修改时使用;UserID/SchSerID 由控制器从登录态覆盖。
|
||||
/// </summary>
|
||||
public class WYDocPrintSaveDto
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public string DocTitle { get; set; }
|
||||
public string DocType { get; set; }
|
||||
public string DocStyle { get; set; }
|
||||
public int DocNum { get; set; }
|
||||
public string DocContent { get; set; }
|
||||
public string UserID { get; set; }
|
||||
public string UpFile { get; set; }
|
||||
public string DPType { get; set; }
|
||||
public DateTime? TakeDate { get; set; }
|
||||
public int? DocStep { get; set; }
|
||||
public int? flowid { get; set; }
|
||||
public int? rid { get; set; }
|
||||
public int? reid { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public string explain { get; set; }
|
||||
public string grade { get; set; }
|
||||
public string Examtype { get; set; }
|
||||
public int? DocFix { get; set; }
|
||||
public int? DocShare { get; set; }
|
||||
public string FreeUserID { get; set; }
|
||||
public string SendUserID { get; set; }
|
||||
public int? freestepid { get; set; }
|
||||
public string zhipai { get; set; }
|
||||
/// <summary>旧 WY_DocPrintDto.ctype(AllList 四分类标记),保存时不用,仅保持入参兼容。</summary>
|
||||
public int ctype { get; set; }
|
||||
/// <summary>旧 sms 短信开关;新项目无短信/待办通道,仅保留入参不生效。</summary>
|
||||
public bool sms { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 审核动作入参(旧 Common_flowtranctInfoDto : Common_flowtranctInfo + SendUserID/MySendUserID/sms),
|
||||
/// 用于 Revoke / shenheok / AffirmApplyforOk;actionuserid 由控制器从登录态覆盖。
|
||||
/// </summary>
|
||||
public class FlowTranctDto
|
||||
{
|
||||
public int dataid { get; set; }
|
||||
public int stepid { get; set; }
|
||||
public short actionstatus { get; set; }
|
||||
public int? actionuserid { get; set; }
|
||||
public string remark { get; set; }
|
||||
public string undertaker { get; set; }
|
||||
public string Pid { get; set; }
|
||||
public string DoneUserID { get; set; }
|
||||
public string filespath { get; set; }
|
||||
public string SendUserID { get; set; }
|
||||
public string MySendUserID { get; set; }
|
||||
/// <summary>旧 sms 短信开关;新项目无短信通道,仅保留入参不生效。</summary>
|
||||
public bool sms { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 文印列表项(旧 MyList/SendRdList/SPList/ShenheList/YishenList/totalList/AllList 的匿名投影,
|
||||
/// 序列化 camelCase 后字段名与旧 JSON 一致;ctype 仅 AllList 填充)。
|
||||
/// </summary>
|
||||
public class DocPrintListItem
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string DocContent { get; set; }
|
||||
public string DocType { get; set; }
|
||||
public string DocTypeName { get; set; }
|
||||
public string DocStyle { get; set; }
|
||||
public string DocStyleName { get; set; }
|
||||
public string DPType { get; set; }
|
||||
public string DPTypeName { get; set; }
|
||||
public string EditDate { get; set; }
|
||||
public string TakeDate { get; set; }
|
||||
public string UserID { get; set; }
|
||||
public string teaname { get; set; }
|
||||
public string TeaPhoto { get; set; }
|
||||
public int? DocShare { get; set; }
|
||||
public string UpFile { get; set; }
|
||||
public string DocTitle { get; set; }
|
||||
public string DocStatus { get; set; }
|
||||
public string DocStatusName { get; set; }
|
||||
public int? DocStep { get; set; }
|
||||
public int? flowid { get; set; }
|
||||
public bool _disabled { get; set; }
|
||||
public string FreeUserID { get; set; }
|
||||
public string SendUserID { get; set; }
|
||||
public int? freestepid { get; set; }
|
||||
public string explain { get; set; }
|
||||
/// <summary>AllList 四分类标记:0=待我审 / 1=我已审 / 2=抄送我 / 3=我发起;仅 AllList 返回,其余列表为 null。</summary>
|
||||
public int? ctype { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 文印单条详情(旧 MyListtoOne 的匿名投影:DocStep/flowid 为字符串,含完整业务字段;
|
||||
/// 旧 OtherData.tempresultfile(附件明细)并入 data,因新统一响应无 OtherData 槽位)。
|
||||
/// </summary>
|
||||
public class DocPrintDetailItem
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string DocContent { get; set; }
|
||||
public string DocType { get; set; }
|
||||
public string DocTypeName { get; set; }
|
||||
public string DocStyle { get; set; }
|
||||
public string DocStyleName { get; set; }
|
||||
public string DPType { get; set; }
|
||||
public string DPTypeName { get; set; }
|
||||
public string EditDate { get; set; }
|
||||
public string TakeDate { get; set; }
|
||||
public string UserID { get; set; }
|
||||
public string teaname { get; set; }
|
||||
public string TeaPhoto { get; set; }
|
||||
public string UpFile { get; set; }
|
||||
public string DocTitle { get; set; }
|
||||
public string DocStatus { get; set; }
|
||||
public string DocStatusName { get; set; }
|
||||
/// <summary>旧实现为 item.DocStep.ToString()(null → 空串)。</summary>
|
||||
public string DocStep { get; set; }
|
||||
public string explain { get; set; }
|
||||
/// <summary>旧实现为 item.flowid.ToString()(null → 空串)。</summary>
|
||||
public string flowid { get; set; }
|
||||
public int? DocShare { get; set; }
|
||||
public int? DocFix { get; set; }
|
||||
public int DocNum { get; set; }
|
||||
public bool _disabled { get; set; }
|
||||
public string FreeUserID { get; set; }
|
||||
public string SendUserID { get; set; }
|
||||
public int? freestepid { get; set; }
|
||||
public string zhipai { get; set; }
|
||||
public int? rid { get; set; }
|
||||
public int? reid { get; set; }
|
||||
public string grade { get; set; }
|
||||
public string Examtype { get; set; }
|
||||
/// <summary>旧 OtherData.tempresultfile:附件明细(office 文件替换为 .pdf 预览名)。</summary>
|
||||
public List<DocPrintFileItem> tempresultfile { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>抄送人详情(旧 SendDetailsList 的 {TeaName, TeaPhoto})。</summary>
|
||||
public class SendDetailItem
|
||||
{
|
||||
public string TeaName { get; set; }
|
||||
public string TeaPhoto { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>附件项(旧 WY_list.filelist / MyListtoOne.tempresultfile)。</summary>
|
||||
public class DocPrintFileItem
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string FileTrueName { get; set; }
|
||||
public string FileName { get; set; }
|
||||
public int? FileSize { get; set; }
|
||||
public string FileType { get; set; }
|
||||
public int DownTimes { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>绑定流程下拉项(旧 Bindflowinfo 的 {SerID: string, flowname})。</summary>
|
||||
public class BindFlowOption
|
||||
{
|
||||
public string SerID { get; set; }
|
||||
public string flowname { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>文印数量按月统计项(旧 List1 / Exceldaochu1 的 {dateyear, datemonth, num, DocNum})。</summary>
|
||||
public class MonthStatItem
|
||||
{
|
||||
public int dateyear { get; set; }
|
||||
public int datemonth { get; set; }
|
||||
public int num { get; set; }
|
||||
public int DocNum { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>文印详情统计项(旧 List2 匿名投影)。</summary>
|
||||
public class DocDetailStatItem
|
||||
{
|
||||
public string UserID { get; set; }
|
||||
public string teaname { get; set; }
|
||||
public string DocContent { get; set; }
|
||||
public string DocType { get; set; }
|
||||
public string DocTypeName { get; set; }
|
||||
public string DocStyle { get; set; }
|
||||
public string DocStyleName { get; set; }
|
||||
public string DPType { get; set; }
|
||||
public string DPTypeName { get; set; }
|
||||
public string EditDate { get; set; }
|
||||
public string UpFile { get; set; }
|
||||
public string DocTitle { get; set; }
|
||||
public int DocNum { get; set; }
|
||||
public string DocStatus { get; set; }
|
||||
public string DocStatusName { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>文印数量按用户排名项(旧 List3 / Exceldaochu3 的 {UserID, teaname, num, DocNum})。</summary>
|
||||
public class UserRankItem
|
||||
{
|
||||
public string UserID { get; set; }
|
||||
public string teaname { get; set; }
|
||||
public int num { get; set; }
|
||||
public int DocNum { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>按月/按用户统计明细项(旧 List4 / List5 匿名投影,editstatus 对应旧 Common.GetEditDeleteStatus)。</summary>
|
||||
public class StatListItem
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string DocContent { get; set; }
|
||||
public string DocType { get; set; }
|
||||
public string DocTypeName { get; set; }
|
||||
public string DocStyle { get; set; }
|
||||
public string DocStyleName { get; set; }
|
||||
public string DPType { get; set; }
|
||||
public string DPTypeName { get; set; }
|
||||
public string EditDate { get; set; }
|
||||
public string UserID { get; set; }
|
||||
public string teaname { get; set; }
|
||||
public string UpFile { get; set; }
|
||||
public string DocTitle { get; set; }
|
||||
public string DocStatus { get; set; }
|
||||
public string DocStatusName { get; set; }
|
||||
/// <summary>旧 Common.GetEditDeleteStatus(status, step):一审(且非通过/拒绝态)可编辑 → false。</summary>
|
||||
public bool editstatus { get; set; }
|
||||
public int? DocStep { get; set; }
|
||||
public int? flowid { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using YhCore.Entity;
|
||||
|
||||
namespace YhCore.Model.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// 审核角色扩展对象(旧 WY_SHRoleP.cs 文件尾 WYSHRoleExt):角色 + 审核人 + 被审核人树形结构。
|
||||
/// SerID == 0 表示新增,否则更新。
|
||||
/// </summary>
|
||||
public class WYSHRoleExt
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public string SchSerId { get; set; }
|
||||
public string SHRoleName { get; set; }
|
||||
public List<WY_SHRole_SHTea> SHTea { get; set; } = new List<WY_SHRole_SHTea>();
|
||||
public List<WY_SHRole_BSHTea> BSHTea { get; set; } = new List<WY_SHRole_BSHTea>();
|
||||
public DateTime? inserttime { get; set; }
|
||||
public string writor { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>步骤 + 审核教师(旧 WYStepSHTea,SHState 形如 "待审核_0" / "同意_1")。</summary>
|
||||
public class WYStepSHTea
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public int? flowid { get; set; }
|
||||
public string StepName { get; set; }
|
||||
public string SHState { get; set; }
|
||||
public string SHtime { get; set; }
|
||||
public int? ordernum { get; set; }
|
||||
public int? prestep { get; set; }
|
||||
public int? cond { get; set; }
|
||||
public List<WY_SHRole_SHTea> SHTea { get; set; } = new List<WY_SHRole_SHTea>();
|
||||
}
|
||||
|
||||
/// <summary>用户可申请的流程(旧 WYFlowForUser,getLCOfUser / editDocPrint 的 data 段)。</summary>
|
||||
public class WYFlowForUser
|
||||
{
|
||||
public decimal SerID { get; set; }
|
||||
public string txt { get; set; }
|
||||
public string flowname { get; set; }
|
||||
public string UserID { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public string DateNowStr { get; set; }
|
||||
public string ChaoSend { get; set; }
|
||||
public string ChaoSendName { get; set; }
|
||||
public int? StepSend { get; set; }
|
||||
public string WYPageGG { get; set; }
|
||||
public List<WYStepSHTea> StepSHTea { get; set; } = new List<WYStepSHTea>();
|
||||
}
|
||||
|
||||
/// <summary>审核轨迹展示项(旧 FreeShenheinfoSHRole)。</summary>
|
||||
public class FreeShenheinfoSHRole
|
||||
{
|
||||
public string steptname { get; set; }
|
||||
public string steptstatus { get; set; }
|
||||
public string photo { get; set; }
|
||||
public string Remark { get; set; }
|
||||
public string actiondate { get; set; }
|
||||
public string actionstatus { get; set; }
|
||||
public List<string> SignaturePic { get; set; } = new List<string>();
|
||||
}
|
||||
|
||||
/// <summary>审核角色流程列表项(旧 ListFlow 投影,含 ChaoSend/StepSend/WYPageGG)。</summary>
|
||||
public class SHRoleFlowListItem
|
||||
{
|
||||
public string SerID { get; set; }
|
||||
public string flowname { get; set; }
|
||||
public string deptid { get; set; }
|
||||
public string typeid { get; set; }
|
||||
public int enable { get; set; }
|
||||
public string enablename { get; set; }
|
||||
public string miaoshu { get; set; }
|
||||
public string deitdate { get; set; }
|
||||
public string ChaoSend { get; set; }
|
||||
public int? StepSend { get; set; }
|
||||
public string WYPageGG { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>步骤详情(旧 getStepInfo:Data=步骤,OtherData=步骤绑定的审核角色)。</summary>
|
||||
public class SHRoleStepInfo
|
||||
{
|
||||
public List<Common_flowstep> Steps { get; set; } = new List<Common_flowstep>();
|
||||
public List<Common_Groupuser_SHRole> GroupUsers { get; set; } = new List<Common_Groupuser_SHRole>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 审核轨迹结果(旧 getshenheFlow:Data=时间线,OtherData=步骤+审核教师明细,Page=当前步骤列表)。
|
||||
/// 新统一响应无 OtherData/Page 槽位,三部分合并为一个对象返回。
|
||||
/// </summary>
|
||||
public class SHRoleShenheFlowResult
|
||||
{
|
||||
public List<FreeShenheinfoSHRole> Flow { get; set; } = new List<FreeShenheinfoSHRole>();
|
||||
public List<WYStepSHTea> Steps { get; set; } = new List<WYStepSHTea>();
|
||||
public List<Common_flowstep> CurrentStep { get; set; } = new List<Common_flowstep>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 文印规格表结果(旧 getdocOftable:Data=规格表,OtherData=每规格默认页数,Page=首列合并行数数组)。
|
||||
/// 合并为一个对象返回。
|
||||
/// </summary>
|
||||
public class SHRoleDocOfTableResult
|
||||
{
|
||||
public List<WY_DocPrint_Num> Table { get; set; } = new List<WY_DocPrint_Num>();
|
||||
public List<int> MinPageNum { get; set; } = new List<int>();
|
||||
public List<int> RowSpan { get; set; } = new List<int>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 审核角色视角的文印列表项(旧 MyListSHRole / totalListSHRole / SendRdListSHRole / SPListSHRole 投影,
|
||||
/// 各列表仅填充自身所需字段,其余为默认值)。
|
||||
/// </summary>
|
||||
public class SHRoleDocPrintListItem
|
||||
{
|
||||
public int SerID { get; set; }
|
||||
public string DocContent { get; set; }
|
||||
public string DocType { get; set; }
|
||||
public string DocTypeName { get; set; }
|
||||
public string DocStyle { get; set; }
|
||||
public string DocStyleName { get; set; }
|
||||
public string DPType { get; set; }
|
||||
public string DPTypeName { get; set; }
|
||||
public string EditDate { get; set; }
|
||||
public string TakeDate { get; set; }
|
||||
public string UserID { get; set; }
|
||||
public string teaname { get; set; }
|
||||
public string TeaPhoto { get; set; }
|
||||
public string UpFile { get; set; }
|
||||
public string DocTitle { get; set; }
|
||||
public string DocStatus { get; set; }
|
||||
public string DocStatusName { get; set; }
|
||||
public bool _disabled { get; set; }
|
||||
public int? DocStep { get; set; }
|
||||
public int? flowid { get; set; }
|
||||
public string FreeUserID { get; set; }
|
||||
public string SendUserID { get; set; }
|
||||
public int? freestepid { get; set; }
|
||||
public int? DocShare { get; set; }
|
||||
public string explain { get; set; }
|
||||
public int? rid { get; set; }
|
||||
public int? reid { get; set; }
|
||||
public string DWName { get; set; }
|
||||
public string ZBName { get; set; }
|
||||
public string grade { get; set; }
|
||||
public string Examtype { get; set; }
|
||||
public int DocNum { get; set; }
|
||||
public int? DocFix { get; set; }
|
||||
public string zhipai { get; set; }
|
||||
public List<WY_DocPrint_Num> DocPrintNum { get; set; } = new List<WY_DocPrint_Num>();
|
||||
public string SHRName { get; set; }
|
||||
public bool isshowje { get; set; }
|
||||
public string ChkDate { get; set; }
|
||||
public string firstSHDate { get; set; }
|
||||
public string firstSHDateTiket { get; set; }
|
||||
public int FJDownInfo { get; set; }
|
||||
/// <summary>附件明细(仅 MyListtoOneSHRole 填充,旧 OtherData.tempresultfile 并入 data)。</summary>
|
||||
public List<DocPrintFileItem> tempresultfile { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按金额取步骤结果(旧 getStepByJE:Data=可见步骤+审核教师,OtherData=通过金额过滤的全部步骤)。
|
||||
/// 新统一响应无 OtherData 槽位,两部分合并为一个对象返回。
|
||||
/// </summary>
|
||||
public class SHRoleStepByJEResult
|
||||
{
|
||||
public List<WYStepSHTea> StepSHTea { get; set; } = new List<WYStepSHTea>();
|
||||
public List<Common_flowstep> Steps { get; set; } = new List<Common_flowstep>();
|
||||
}
|
||||
|
||||
/// <summary>金额重算记录列表项(旧 getRegetJERd 投影)。</summary>
|
||||
public class SHRoleRegetJERdItem
|
||||
{
|
||||
public string teaname { get; set; }
|
||||
public string writor { get; set; }
|
||||
public int SerID { get; set; }
|
||||
public int? DcoPrintID { get; set; }
|
||||
public string DocTitle { get; set; }
|
||||
public string DocContent { get; set; }
|
||||
public string DocInfo { get; set; }
|
||||
public string EditDatetime { get; set; }
|
||||
public string datestr { get; set; }
|
||||
public string type { get; set; }
|
||||
public string typename { get; set; }
|
||||
public string SchSerID { get; set; }
|
||||
public string UnitPrice { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>editDocPrint 入参(旧 form:isedit + data + docdata + docnumdata)。</summary>
|
||||
public class WYSHRoleEditDocPrintDto
|
||||
{
|
||||
public int isedit { get; set; }
|
||||
public WYFlowForUser data { get; set; }
|
||||
public WYDocPrintSaveDto docdata { get; set; }
|
||||
public List<WY_DocPrint_Num> docnumdata { get; set; } = new List<WY_DocPrint_Num>();
|
||||
}
|
||||
|
||||
/// <summary>editSHTea 入参(旧 form:SHRoleID + listSHTeaTxt)。</summary>
|
||||
public class EditSHTeaDto
|
||||
{
|
||||
public decimal SHRoleID { get; set; }
|
||||
public List<WY_SHRole_SHTea> listSHTea { get; set; } = new List<WY_SHRole_SHTea>();
|
||||
}
|
||||
|
||||
/// <summary>editBSHTea 入参(旧 form:SHRoleID + listBSHTeaTxt)。</summary>
|
||||
public class EditBSHTeaDto
|
||||
{
|
||||
public decimal SHRoleID { get; set; }
|
||||
public List<WY_SHRole_BSHTea> listBSHTea { get; set; } = new List<WY_SHRole_BSHTea>();
|
||||
}
|
||||
|
||||
/// <summary>delSHRole 入参(旧 form:listSHRoleIDTxt)。</summary>
|
||||
public class DelSHRoleDto
|
||||
{
|
||||
public List<decimal?> listSHRoleID { get; set; } = new List<decimal?>();
|
||||
}
|
||||
|
||||
/// <summary>SaveSignature 入参(旧 form:pic)。</summary>
|
||||
public class SaveSignatureDto
|
||||
{
|
||||
public string pic { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>TuiHui 入参(旧 form:DocID + cont)。</summary>
|
||||
public class TuiHuiDto
|
||||
{
|
||||
public decimal DocID { get; set; }
|
||||
public string cont { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>regetJE 入参(旧 form:DocID 逗号串 + type)。</summary>
|
||||
public class RegetJEDto
|
||||
{
|
||||
public string DocID { get; set; }
|
||||
public string type { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace YhCore.Model.Vo
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页返回对象(对应 Java 版 PageVO,字段与 Spring Page 序列化结构保持一致,
|
||||
/// 保证前端零改动切换)。
|
||||
/// </summary>
|
||||
public class PageVO<T>
|
||||
{
|
||||
/// <summary>当前页数据</summary>
|
||||
public List<T> Content { get; set; } = new List<T>();
|
||||
|
||||
/// <summary>总记录数</summary>
|
||||
public long TotalElements { get; set; }
|
||||
|
||||
/// <summary>总页数</summary>
|
||||
public int TotalPages { get; set; }
|
||||
|
||||
/// <summary>当前页码(从 0 开始,与 Spring Pageable 一致)</summary>
|
||||
public int Number { get; set; }
|
||||
|
||||
/// <summary>每页大小</summary>
|
||||
public int Size { get; set; }
|
||||
|
||||
public static PageVO<T> Of(List<T> content, long totalElements, int number, int size)
|
||||
{
|
||||
return new PageVO<T>
|
||||
{
|
||||
Content = content ?? new List<T>(),
|
||||
TotalElements = totalElements,
|
||||
TotalPages = size > 0 ? (int)((totalElements + size - 1) / size) : 0,
|
||||
Number = number,
|
||||
Size = size
|
||||
};
|
||||
}
|
||||
|
||||
public static PageVO<T> Empty(int number = 0, int size = 20) => Of(new List<T>(), 0, number, size);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<RootNamespace>YhCore.Model</RootNamespace>
|
||||
<AssemblyName>YhCore.Model</AssemblyName>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- 审核角色 DTO 直接引用实体(WY_SHRole_SHTea / Common_flowstep / WY_DocPrint_Num 等) -->
|
||||
<ProjectReference Include="..\YhCore.Entity\YhCore.Entity.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Dapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace YhCore.Repository.Dapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Dapper 通道(对应 Java 显式引入 JdbcTemplate 的场景,如 backupAllSnapshot 批量快照插入)。
|
||||
/// 与 EF Core 共用同一连接(YHDbContext.Database.GetDbConnection),连接生命周期由 DbContext 管理。
|
||||
/// </summary>
|
||||
public class DapperChannel
|
||||
{
|
||||
private readonly YHDbContext _context;
|
||||
|
||||
public DapperChannel(YHDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
private IDbConnection Connection => _context.Database.GetDbConnection();
|
||||
|
||||
/// <summary>执行非查询 SQL(增删改),返回受影响行数</summary>
|
||||
public async Task<int> ExecuteAsync(string sql, object param = null)
|
||||
{
|
||||
return await Connection.ExecuteAsync(sql, param);
|
||||
}
|
||||
|
||||
/// <summary>查询单值(如 SELECT COUNT(*))</summary>
|
||||
public async Task<T> ExecuteScalarAsync<T>(string sql, object param = null)
|
||||
{
|
||||
return await Connection.ExecuteScalarAsync<T>(sql, param);
|
||||
}
|
||||
|
||||
/// <summary>查询列表</summary>
|
||||
public async Task<IEnumerable<T>> QueryAsync<T>(string sql, object param = null)
|
||||
{
|
||||
return await Connection.QueryAsync<T>(sql, param);
|
||||
}
|
||||
|
||||
/// <summary>查询单条</summary>
|
||||
public async Task<T> QueryFirstOrDefaultAsync<T>(string sql, object param = null)
|
||||
{
|
||||
return await Connection.QueryFirstOrDefaultAsync<T>(sql, param);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量快照插入(对应 Java backupAllSnapshot 的批量插入场景)。
|
||||
/// 逐条循环 Insert 的通用实现;数据量大时可替换为 SqlBulkCopy / 表值参数提升性能。
|
||||
/// </summary>
|
||||
public async Task<int> BulkInsertAsync<T>(string table, IEnumerable<T> rows)
|
||||
{
|
||||
var affected = 0;
|
||||
foreach (var row in rows)
|
||||
{
|
||||
affected += await Connection.ExecuteAsync(
|
||||
$"INSERT INTO {table} ({string.Join(",", GetColumns(row))}) " +
|
||||
$"VALUES ({string.Join(",", GetColumns(row).Select(c => "@" + c))})", row);
|
||||
}
|
||||
|
||||
return affected;
|
||||
}
|
||||
|
||||
private static string[] GetColumns<T>(T row)
|
||||
{
|
||||
var props = row?.GetType().GetProperties() ?? System.Array.Empty<System.Reflection.PropertyInfo>();
|
||||
return props.Select(p => p.Name).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace YhCore.Repository
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据库自动补表初始化器:对比 EF Core 实体模型与数据库实际表,
|
||||
/// 把「实体里有而数据库没有」的表自动创建(仅补建缺失表,不触碰已存在表;
|
||||
/// 已存在表的结构变更仍走 sqlscript 脚本,与既有约定一致)。
|
||||
/// 免除新增实体后手写建表脚本。
|
||||
/// </summary>
|
||||
public static class DatabaseInitializer
|
||||
{
|
||||
/// <summary>
|
||||
/// 补建缺失表。返回本次创建的表名列表(空 = 无缺失表)。
|
||||
/// 仅支持 SQL Server(对象存在性守卫/脚本语法为 SQL Server 专用);
|
||||
/// 其他 Provider(如 MySQL)跳过不动作,避免破坏其脚本语法。
|
||||
/// </summary>
|
||||
public static List<string> EnsureMissingTables(YHDbContext context)
|
||||
{
|
||||
if (!context.Database.IsSqlServer())
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
// 提前捕获连接串:此后不依赖 EF 托管连接(GenerateCreateScript 等内部操作
|
||||
// 可能干扰 EF 连接状态),DDL 用独立 SqlConnection 执行
|
||||
var connectionString = context.Database.GetDbConnection().ConnectionString;
|
||||
|
||||
// 1. 实体模型期望的表(Keyless 实体不建表,与 EnsureCreated 语义一致)
|
||||
var expected = context.Model.GetEntityTypes()
|
||||
.Where(e => e.FindPrimaryKey() != null)
|
||||
.Select(e => e.GetTableName())
|
||||
.Where(n => !string.IsNullOrWhiteSpace(n))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (expected.Count == 0)
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
// 2. 数据库不存在时:EnsureCreated 一次创建库 + 全部表
|
||||
if (!context.Database.CanConnect())
|
||||
{
|
||||
if (context.Database.EnsureCreated())
|
||||
{
|
||||
return expected.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 数据库实际存在的表
|
||||
var existing = GetExistingTables(context);
|
||||
|
||||
// 4. 差集:缺失的表
|
||||
var missing = expected.Except(existing, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
if (missing.Count == 0)
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
// 5. 从 EF 生成的建库脚本中提取缺失表的 DDL 并执行
|
||||
return ExecuteMissingTableScripts(context, missing, connectionString);
|
||||
}
|
||||
|
||||
private static HashSet<string> GetExistingTables(YHDbContext context)
|
||||
{
|
||||
var tables = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var conn = context.Database.GetDbConnection();
|
||||
var wasOpen = conn.State == ConnectionState.Open;
|
||||
if (!wasOpen)
|
||||
{
|
||||
conn.Open();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.CommandText =
|
||||
"SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'";
|
||||
using var reader = cmd.ExecuteReader();
|
||||
while (reader.Read())
|
||||
{
|
||||
tables.Add(reader.GetString(0));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 只关闭不释放:连接由 EF 托管,外部释放会破坏其内部状态
|
||||
if (!wasOpen)
|
||||
{
|
||||
conn.Close();
|
||||
}
|
||||
}
|
||||
|
||||
return tables;
|
||||
}
|
||||
|
||||
private static List<string> ExecuteMissingTableScripts(
|
||||
YHDbContext context, List<string> missing, string connectionString)
|
||||
{
|
||||
var missingSet = missing.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var script = context.Database.GenerateCreateScript();
|
||||
|
||||
// EF 生成的脚本以独立 GO 行为语句分隔;无 GO 时整体视为一段
|
||||
var segments = Regex.Split(script, @"(?m)^\s*GO\s*$", RegexOptions.IgnoreCase)
|
||||
.Select(s => s.Trim())
|
||||
.Where(s => s.Length > 0);
|
||||
|
||||
var statements = new List<string>();
|
||||
foreach (var segment in segments)
|
||||
{
|
||||
var tableMatch = Regex.Match(segment,
|
||||
@"^\s*CREATE\s+TABLE\s+(?:\[([^\]]+)\]\.)?\[([^\]]+)\]",
|
||||
RegexOptions.IgnoreCase);
|
||||
if (tableMatch.Success)
|
||||
{
|
||||
var schema = tableMatch.Groups[1].Success ? tableMatch.Groups[1].Value : "dbo";
|
||||
var name = tableMatch.Groups[2].Value;
|
||||
if (missingSet.Contains(name))
|
||||
{
|
||||
// 守卫包裹:竞态/重复执行安全(与迁移幂等脚本语义一致)
|
||||
statements.Add(
|
||||
$"IF OBJECT_ID(N'[{schema}].[{name}]', N'U') IS NULL\nBEGIN\n{segment}\nEND;");
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var indexMatch = Regex.Match(segment,
|
||||
@"^\s*CREATE\s+(?:UNIQUE\s+)?INDEX\s+\[([^\]]+)\]\s+ON\s+(?:\[([^\]]+)\]\.)?\[([^\]]+)\]",
|
||||
RegexOptions.IgnoreCase);
|
||||
if (indexMatch.Success)
|
||||
{
|
||||
var indexName = indexMatch.Groups[1].Value;
|
||||
var schema = indexMatch.Groups[2].Success ? indexMatch.Groups[2].Value : "dbo";
|
||||
var tableName = indexMatch.Groups[3].Value;
|
||||
if (missingSet.Contains(tableName))
|
||||
{
|
||||
statements.Add(
|
||||
$"IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'{indexName}' AND object_id = OBJECT_ID(N'[{schema}].[{tableName}]'))\nBEGIN\n{segment}\nEND;");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (statements.Count == 0)
|
||||
{
|
||||
// 模型表与脚本段未匹配上(理论上不发生):保守起见不执行任何 DDL
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
// 用独立 SqlConnection 执行 DDL(不依赖 EF 托管连接,避免其状态干扰)
|
||||
using var conn = new SqlConnection(connectionString);
|
||||
conn.Open();
|
||||
|
||||
using (var tx = conn.BeginTransaction())
|
||||
{
|
||||
foreach (var sql in statements)
|
||||
{
|
||||
using var cmd = conn.CreateCommand();
|
||||
cmd.Transaction = tx;
|
||||
cmd.CommandText = sql;
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
tx.Commit();
|
||||
}
|
||||
|
||||
return missing.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using YhCore.Repository.Dapper;
|
||||
|
||||
namespace YhCore.Repository.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据层注册扩展。按配置 DatabaseProvider 选择数据库提供方:
|
||||
/// "SqlServer"(默认,主库)或 "MySql"(可切换)。
|
||||
/// </summary>
|
||||
public static class DataLayerServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddDataLayer(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
var provider = configuration["DatabaseProvider"];
|
||||
|
||||
services.AddDbContext<YHDbContext>(options =>
|
||||
{
|
||||
if (string.Equals(provider, "MySql", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// MySQL 切换通道(对应 Java 版 application.yml 的 MySQL 数据源)
|
||||
var conn = configuration.GetConnectionString("Mysql");
|
||||
options.UseMySql(conn, ServerVersion.AutoDetect(conn));
|
||||
}
|
||||
else
|
||||
{
|
||||
// 默认主库:SQL Server(EduCloudDocPrint)
|
||||
options.UseSqlServer(configuration.GetConnectionString("SqlServer"));
|
||||
}
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量注册泛型仓储与 Dapper 通道(对应 Java Repository 包由 Spring 自动装配)。
|
||||
/// 注入方式:IRepository<WY_DocPrint> / DapperChannel。
|
||||
/// </summary>
|
||||
public static IServiceCollection AddRepositories(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped(typeof(IRepository<>), typeof(RepositoryBase<>));
|
||||
services.AddScoped<DapperChannel>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
using YhCore.Model.Vo;
|
||||
|
||||
namespace YhCore.Repository
|
||||
{
|
||||
/// <summary>
|
||||
/// 泛型仓储接口(对应 Java Spring Data JPA Repository)。
|
||||
/// </summary>
|
||||
public interface IRepository<T> where T : class
|
||||
{
|
||||
// ---- 读取 ----
|
||||
Task<T> GetByIdAsync(object id);
|
||||
Task<List<T>> GetAllAsync();
|
||||
Task<List<T>> FindAsync(Expression<Func<T, bool>> predicate);
|
||||
Task<T> FirstOrDefaultAsync(Expression<Func<T, bool>> predicate);
|
||||
Task<bool> AnyAsync(Expression<Func<T, bool>> predicate);
|
||||
Task<long> CountAsync(Expression<Func<T, bool>> predicate = null);
|
||||
|
||||
/// <summary>分页查询(页码从 0 开始),对应 PageUtil + PageVO</summary>
|
||||
Task<PageVO<T>> PageAsync(Expression<Func<T, bool>> predicate, int page, int size,
|
||||
Expression<Func<T, object>> orderBy = null, bool asc = true);
|
||||
|
||||
// ---- 写入 ----
|
||||
Task<T> AddAsync(T entity);
|
||||
Task AddRangeAsync(IEnumerable<T> entities);
|
||||
Task UpdateAsync(T entity);
|
||||
Task RemoveAsync(T entity);
|
||||
Task RemoveAsync(object id);
|
||||
Task<int> SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using YhCore.Model.Vo;
|
||||
|
||||
namespace YhCore.Repository
|
||||
{
|
||||
/// <summary>
|
||||
/// 泛型仓储基类(对应 Java Spring Data JPA 的 SimpleJpaRepository)。
|
||||
/// 基于 EF Core IQueryable,分页查询下推到数据库执行。
|
||||
/// </summary>
|
||||
public class RepositoryBase<T> : IRepository<T> where T : class
|
||||
{
|
||||
protected readonly YHDbContext Context;
|
||||
protected readonly DbSet<T> DbSet;
|
||||
|
||||
public RepositoryBase(YHDbContext context)
|
||||
{
|
||||
Context = context;
|
||||
DbSet = context.Set<T>();
|
||||
}
|
||||
|
||||
public virtual async Task<T> GetByIdAsync(object id)
|
||||
{
|
||||
return await DbSet.FindAsync(id);
|
||||
}
|
||||
|
||||
public virtual async Task<List<T>> GetAllAsync()
|
||||
{
|
||||
return await DbSet.AsNoTracking().ToListAsync();
|
||||
}
|
||||
|
||||
public virtual async Task<List<T>> FindAsync(Expression<Func<T, bool>> predicate)
|
||||
{
|
||||
return await DbSet.AsNoTracking().Where(predicate).ToListAsync();
|
||||
}
|
||||
|
||||
public virtual async Task<T> FirstOrDefaultAsync(Expression<Func<T, bool>> predicate)
|
||||
{
|
||||
return await DbSet.AsNoTracking().FirstOrDefaultAsync(predicate);
|
||||
}
|
||||
|
||||
public virtual async Task<bool> AnyAsync(Expression<Func<T, bool>> predicate)
|
||||
{
|
||||
return await DbSet.AnyAsync(predicate);
|
||||
}
|
||||
|
||||
public virtual async Task<long> CountAsync(Expression<Func<T, bool>> predicate = null)
|
||||
{
|
||||
return predicate == null
|
||||
? await DbSet.LongCountAsync()
|
||||
: await DbSet.LongCountAsync(predicate);
|
||||
}
|
||||
|
||||
public virtual async Task<PageVO<T>> PageAsync(
|
||||
Expression<Func<T, bool>> predicate,
|
||||
int page, int size,
|
||||
Expression<Func<T, object>> orderBy = null,
|
||||
bool asc = true)
|
||||
{
|
||||
if (page < 0) page = 0;
|
||||
if (size <= 0) size = 20;
|
||||
|
||||
IQueryable<T> query = DbSet.AsNoTracking();
|
||||
if (predicate != null)
|
||||
{
|
||||
query = query.Where(predicate);
|
||||
}
|
||||
|
||||
var total = await query.LongCountAsync();
|
||||
|
||||
IOrderedQueryable<T> ordered;
|
||||
if (orderBy != null)
|
||||
{
|
||||
ordered = asc ? query.OrderBy(orderBy) : query.OrderByDescending(orderBy);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 无显式排序时按主键约定排序(Id → SerID),保证 Skip/Take 稳定
|
||||
var keyName = typeof(T).GetProperty("Id") != null ? "Id" : "SerID";
|
||||
ordered = query.OrderBy(e => EF.Property<object>(e, keyName));
|
||||
}
|
||||
|
||||
var items = await ordered.Skip(page * size).Take(size).ToListAsync();
|
||||
return PageVO<T>.Of(items, total, page, size);
|
||||
}
|
||||
|
||||
public virtual async Task<T> AddAsync(T entity)
|
||||
{
|
||||
var entry = await DbSet.AddAsync(entity);
|
||||
return entry.Entity;
|
||||
}
|
||||
|
||||
public virtual async Task AddRangeAsync(IEnumerable<T> entities)
|
||||
{
|
||||
await DbSet.AddRangeAsync(entities);
|
||||
}
|
||||
|
||||
public virtual Task UpdateAsync(T entity)
|
||||
{
|
||||
DbSet.Update(entity);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual Task RemoveAsync(T entity)
|
||||
{
|
||||
DbSet.Remove(entity);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual async Task RemoveAsync(object id)
|
||||
{
|
||||
var entity = await GetByIdAsync(id);
|
||||
if (entity != null)
|
||||
{
|
||||
DbSet.Remove(entity);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual async Task<int> SaveChangesAsync()
|
||||
{
|
||||
return await Context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace YhCore.Repository.Specification
|
||||
{
|
||||
/// <summary>
|
||||
/// 动态条件表达式组合器(对应 Java Repository/specification 包的轻量实现)。
|
||||
/// 用于按需拼装 where 条件:
|
||||
/// <code>
|
||||
/// var spec = Specification.True<SchClass>()
|
||||
/// .And(c => c.SchoolId == schId)
|
||||
/// .AndIf(!string.IsNullOrEmpty(name), c => c.Name.Contains(name));
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public static class Specification
|
||||
{
|
||||
public static Expression<Func<T, bool>> True<T>() => _ => true;
|
||||
|
||||
public static Expression<Func<T, bool>> False<T>() => _ => false;
|
||||
|
||||
public static Expression<Func<T, bool>> And<T>(
|
||||
this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
|
||||
{
|
||||
if (left == null) return right;
|
||||
if (right == null) return left;
|
||||
|
||||
var body = Expression.AndAlso(left.Body, right.Body.ReplaceParameter(right.Parameters[0], left.Parameters[0]));
|
||||
return Expression.Lambda<Func<T, bool>>(body, left.Parameters);
|
||||
}
|
||||
|
||||
public static Expression<Func<T, bool>> Or<T>(
|
||||
this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
|
||||
{
|
||||
if (left == null) return right;
|
||||
if (right == null) return left;
|
||||
|
||||
var body = Expression.OrElse(left.Body, right.Body.ReplaceParameter(right.Parameters[0], left.Parameters[0]));
|
||||
return Expression.Lambda<Func<T, bool>>(body, left.Parameters);
|
||||
}
|
||||
|
||||
public static Expression<Func<T, bool>> Not<T>(this Expression<Func<T, bool>> expr)
|
||||
{
|
||||
if (expr == null) return null;
|
||||
return Expression.Lambda<Func<T, bool>>(Expression.Not(expr.Body), expr.Parameters);
|
||||
}
|
||||
|
||||
/// <summary>条件成立时才追加条件(Java Specification 常用写法)</summary>
|
||||
public static Expression<Func<T, bool>> AndIf<T>(
|
||||
this Expression<Func<T, bool>> spec, bool condition, Expression<Func<T, bool>> conditionExpr)
|
||||
{
|
||||
return condition ? spec.And(conditionExpr) : spec;
|
||||
}
|
||||
|
||||
/// <summary>条件不成立时才追加条件</summary>
|
||||
public static Expression<Func<T, bool>> AndIfNot<T>(
|
||||
this Expression<Func<T, bool>> spec, bool condition, Expression<Func<T, bool>> conditionExpr)
|
||||
{
|
||||
return spec.AndIf(!condition, conditionExpr);
|
||||
}
|
||||
|
||||
private static Expression ReplaceParameter(this Expression expression, ParameterExpression source, ParameterExpression target)
|
||||
{
|
||||
return new ParameterReplacer(source, target).Visit(expression);
|
||||
}
|
||||
|
||||
private class ParameterReplacer : ExpressionVisitor
|
||||
{
|
||||
private readonly ParameterExpression _source;
|
||||
private readonly ParameterExpression _target;
|
||||
|
||||
public ParameterReplacer(ParameterExpression source, ParameterExpression target)
|
||||
{
|
||||
_source = source;
|
||||
_target = target;
|
||||
}
|
||||
|
||||
protected override Expression VisitParameter(ParameterExpression node)
|
||||
{
|
||||
return node == _source ? _target : base.VisitParameter(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using YhCore.Entity;
|
||||
|
||||
namespace YhCore.Repository
|
||||
{
|
||||
/// <summary>
|
||||
/// EF Core 数据上下文。同时支持 SQL Server(主库)与 MySQL(可切换),
|
||||
/// 提供方在 Startup 通过 AddDataLayer 按 DatabaseProvider 配置选择。
|
||||
/// 生产环境禁用自动迁移,结构变更走 sqlscript 脚本。
|
||||
/// </summary>
|
||||
public class YHDbContext : DbContext
|
||||
{
|
||||
public YHDbContext(DbContextOptions<YHDbContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
// 文印业务
|
||||
public DbSet<WY_DocPrint> WY_DocPrint { get; set; }
|
||||
public DbSet<WY_DocPrint_Num> WY_DocPrint_Num { get; set; }
|
||||
public DbSet<WY_DocPrint_SH> WY_DocPrint_SH { get; set; }
|
||||
public DbSet<WY_reGetJERd> WY_reGetJERd { get; set; }
|
||||
public DbSet<WY_SchZyBit> WY_SchZyBit { get; set; }
|
||||
|
||||
// 审批流
|
||||
public DbSet<Common_flowinfo> Common_flowinfo { get; set; }
|
||||
public DbSet<Common_flowstep> Common_flowstep { get; set; }
|
||||
public DbSet<Common_flowtranctInfo> Common_flowtranctInfo { get; set; }
|
||||
public DbSet<Common_flowuser> Common_flowuser { get; set; }
|
||||
public DbSet<Common_freeflowinfo> Common_freeflowinfo { get; set; }
|
||||
public DbSet<Common_freeGroupuser> Common_freeGroupuser { get; set; }
|
||||
public DbSet<Common_Groupuser> Common_Groupuser { get; set; }
|
||||
public DbSet<Common_Groupuser_SHRole> Common_Groupuser_SHRole { get; set; }
|
||||
public DbSet<Common_SendUserinfo> Common_SendUserinfo { get; set; }
|
||||
public DbSet<Common_UserFlowZhipai> Common_UserFlowZhipai { get; set; }
|
||||
|
||||
// 审核角色
|
||||
public DbSet<WY_SHRole> WY_SHRole { get; set; }
|
||||
public DbSet<WY_SHRole_BSHTea> WY_SHRole_BSHTea { get; set; }
|
||||
public DbSet<WY_SHRole_SHTea> WY_SHRole_SHTea { get; set; }
|
||||
|
||||
// 基础数据
|
||||
public DbSet<Common_Dictionary> Common_Dictionary { get; set; }
|
||||
public DbSet<Common_DictionaryOrd> Common_DictionaryOrd { get; set; }
|
||||
public DbSet<Common_UpFiles> Common_UpFiles { get; set; }
|
||||
|
||||
// 日志
|
||||
public DbSet<A_DataLog> A_DataLog { get; set; }
|
||||
public DbSet<Base_Log> Base_Log { get; set; }
|
||||
public DbSet<AppMsgRecord> AppMsgRecord { get; set; }
|
||||
// 系统审计日志(HTTP 接口级,对齐 Java sys_operation_log,表名在 OnModelCreating 映射)
|
||||
public DbSet<SysOperationLog> SysOperationLog { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// 主键约定:实体为旧 EF6 EDMX 生成的纯 POCO(无 [Key] 标注),
|
||||
// 统一按属性名推断主键——优先 Id(不区分大小写,兼容小写 id),其次 SerID;
|
||||
// 两者皆无的实体按 Keyless 处理(仅用于查询)。
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
if (entityType.FindPrimaryKey() != null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var clrType = entityType.ClrType;
|
||||
const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase;
|
||||
var keyProperty = clrType.GetProperty("Id", flags)
|
||||
?? clrType.GetProperty("SerID", flags);
|
||||
|
||||
if (keyProperty != null)
|
||||
{
|
||||
modelBuilder.Entity(clrType).HasKey(keyProperty.Name);
|
||||
}
|
||||
else
|
||||
{
|
||||
modelBuilder.Entity(clrType).HasNoKey();
|
||||
}
|
||||
}
|
||||
|
||||
// decimal 主键(多数表的 SerID)EF Core 约定不自增;
|
||||
// 若数据库对应列为 IDENTITY,需在此补充 ValueGeneratedOnAdd 配置。
|
||||
// WY_DocPrint.SerID:旧 EF6 依赖数据库 IDENTITY 回填新增单据号,此处显式声明。
|
||||
// WY_SHRole* / WY_DocPrint_SH / WY_DocPrint_Num / WY_reGetJERd / Common_Dictionary
|
||||
// 同属旧库 IDENTITY 列(旧代码新增时均不显式赋值 SerID),一并补充。
|
||||
modelBuilder.Entity<WY_DocPrint>().Property(e => e.SerID).ValueGeneratedOnAdd();
|
||||
modelBuilder.Entity<WY_SHRole>().Property(e => e.SerID).ValueGeneratedOnAdd();
|
||||
modelBuilder.Entity<WY_SHRole_SHTea>().Property(e => e.SerID).ValueGeneratedOnAdd();
|
||||
modelBuilder.Entity<WY_SHRole_BSHTea>().Property(e => e.SerID).ValueGeneratedOnAdd();
|
||||
modelBuilder.Entity<WY_DocPrint_SH>().Property(e => e.SerID).ValueGeneratedOnAdd();
|
||||
modelBuilder.Entity<WY_DocPrint_Num>().Property(e => e.SerID).ValueGeneratedOnAdd();
|
||||
modelBuilder.Entity<WY_reGetJERd>().Property(e => e.SerID).ValueGeneratedOnAdd();
|
||||
modelBuilder.Entity<Common_Dictionary>().Property(e => e.SerID).ValueGeneratedOnAdd();
|
||||
|
||||
// 系统审计日志表名映射:sys_operation_log(与 Java bschclassmanage 同表结构,双轨对齐)
|
||||
modelBuilder.Entity<SysOperationLog>().ToTable("sys_operation_log");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<RootNamespace>YhCore.Repository</RootNamespace>
|
||||
<AssemblyName>YhCore.Repository</AssemblyName>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- 主库:SQL Server -->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="5.0.17" />
|
||||
<!-- 可切换:MySQL(Pomelo,对齐 EF Core 5) -->
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="5.0.4" />
|
||||
<!-- Dapper 通道:复杂报表/批量快照插入(对应 Java JdbcTemplate 场景) -->
|
||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\YhCore.Entity\YhCore.Entity.csproj" />
|
||||
<ProjectReference Include="..\YhCore.Model\YhCore.Model.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,171 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Yh.Platform.Abstractions;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Repository;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.Service.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// 公共字典服务实现(移植旧 BestSchool.Service.GetSysDict)。
|
||||
/// </summary>
|
||||
public class DictionaryService : IDictionaryService
|
||||
{
|
||||
private readonly IRepository<Common_Dictionary> _dic;
|
||||
|
||||
public DictionaryService(IRepository<Common_Dictionary> dic)
|
||||
{
|
||||
_dic = dic;
|
||||
}
|
||||
|
||||
public async Task<List<DicOption>> GetDictListAsync(string typeId, string schId)
|
||||
{
|
||||
var has = await _dic.AnyAsync(d => d.typeID == typeId);
|
||||
if (!has)
|
||||
{
|
||||
throw new DException("尚未添加发文部门,请联系管理员添加!");
|
||||
}
|
||||
|
||||
var list = await _dic.FindAsync(d =>
|
||||
d.typeID == typeId &&
|
||||
(string.IsNullOrEmpty(schId) || d.Schserid == schId) &&
|
||||
d.Enable == 1);
|
||||
|
||||
return list
|
||||
.OrderBy(d => DictionaryTypeHelper.ParseSort(d.Des))
|
||||
.Select(d => new DicOption { K = d.CodeID, V = d.Name })
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public Task<List<DicOption>> GetDicTypeListAsync()
|
||||
{
|
||||
var list = DictionaryTypeHelper.AllTypes
|
||||
.Where(t => t.Id != "DEPT")
|
||||
.Select(t => new DicOption { K = t.Id, V = t.Name })
|
||||
.ToList();
|
||||
return Task.FromResult(list);
|
||||
}
|
||||
|
||||
public async Task<List<DicManageItem>> GetDictMangeListAsync(string typeId, string schId)
|
||||
{
|
||||
var has = await _dic.AnyAsync(d => d.typeID == typeId);
|
||||
if (!has)
|
||||
{
|
||||
throw new DException("字典类型错误,未找到该字典类型");
|
||||
}
|
||||
|
||||
var list = await _dic.FindAsync(d =>
|
||||
d.typeID == typeId &&
|
||||
(string.IsNullOrEmpty(schId) || d.Schserid == schId));
|
||||
|
||||
return list
|
||||
.OrderBy(d => string.IsNullOrEmpty(d.Des) ? 999 : DictionaryTypeHelper.ParseSort(d.Des))
|
||||
.Select(d => new DicManageItem
|
||||
{
|
||||
SerID = d.SerID,
|
||||
CodeID = d.CodeID,
|
||||
Name = d.Name,
|
||||
Des = d.Des,
|
||||
Enable = d.Enable
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task AddDicAsync(Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
if (string.IsNullOrWhiteSpace(model.Name)) throw new DException("输入字符串格式不正确!");
|
||||
|
||||
var listed = await _dic.AnyAsync(b =>
|
||||
b.typeID == model.typeID &&
|
||||
(b.Schserid == model.Schserid || b.Schserid == null || b.Schserid == "") &&
|
||||
b.Name == model.Name && b.Enable != 3);
|
||||
|
||||
if (listed || await IsDicnameAsync(model.typeID, model.Name))
|
||||
{
|
||||
throw new DException("已存在该分类,请重新填写!");
|
||||
}
|
||||
|
||||
var candidates = await _dic.FindAsync(x =>
|
||||
x.typeID == model.typeID && x.Enable != 3 &&
|
||||
(x.Schserid == model.Schserid || x.Schserid == null || x.Schserid == ""));
|
||||
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
model.Sort = 1;
|
||||
model.Des = "1";
|
||||
}
|
||||
else
|
||||
{
|
||||
var maxSort = candidates.Max(c => c.Sort ?? (int)c.SerID);
|
||||
model.Sort = maxSort + 1;
|
||||
model.Des = model.Sort.ToString();
|
||||
}
|
||||
|
||||
model.Enable = 1;
|
||||
model.CodeID = model.typeID + model.Sort.ToString();
|
||||
|
||||
await _dic.AddAsync(model);
|
||||
await _dic.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateDicAsync(Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
|
||||
var dup = await _dic.AnyAsync(m =>
|
||||
m.SerID != model.SerID && m.Name == model.Name &&
|
||||
(m.Schserid == model.Schserid || m.Schserid == null) && m.Enable != 3);
|
||||
|
||||
if (dup || await IsDicnameAsync(model.typeID, model.Name))
|
||||
{
|
||||
throw new DException("已存在该分类,请重新填写!");
|
||||
}
|
||||
|
||||
var obj = await _dic.FirstOrDefaultAsync(x => x.SerID == model.SerID);
|
||||
if (obj == null) throw new DException("未找到要更新的分类!");
|
||||
|
||||
obj.Name = model.Name;
|
||||
obj.Des = model.Des;
|
||||
obj.Sort = model.Sort;
|
||||
obj.Enable = model.Enable;
|
||||
|
||||
await _dic.UpdateAsync(obj);
|
||||
await _dic.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task EditDicAsync(Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
|
||||
var current = await _dic.FirstOrDefaultAsync(m => m.SerID == model.SerID);
|
||||
if (current == null) throw new DException("未找到要编辑的分类!");
|
||||
|
||||
var dup = await _dic.AnyAsync(m =>
|
||||
m.SerID != model.SerID && m.Enable != 3 && m.typeID == current.typeID &&
|
||||
m.Name == model.Name && (m.Schserid == model.Schserid || m.Schserid == null));
|
||||
|
||||
if (dup)
|
||||
{
|
||||
throw new DException("已存在该分类,请重新填写!");
|
||||
}
|
||||
|
||||
current.Name = model.Name;
|
||||
current.Code = model.Code;
|
||||
current.Path = model.Path;
|
||||
|
||||
await _dic.UpdateAsync(current);
|
||||
await _dic.SaveChangesAsync();
|
||||
}
|
||||
|
||||
/// <summary>判断全局(Schserid 为空)是否已存在同名类别(对齐旧 IsDicname)。</summary>
|
||||
private async Task<bool> IsDicnameAsync(string typeId, string dicName)
|
||||
{
|
||||
var names = await _dic.FindAsync(b => b.typeID == typeId && b.Schserid == null);
|
||||
return names.Select(a => a.Name).Distinct().Contains(dicName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Yh.Platform.Abstractions;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Repository;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.Service.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// 文印分类 / 印制版面 / 公共类别服务实现
|
||||
/// (移植旧 BestSchool.DocPrint.WY_list 分类部分 + WY_Classify)。
|
||||
/// </summary>
|
||||
public class DocPrintClassifyService : IDocPrintClassifyService
|
||||
{
|
||||
private readonly IRepository<Common_Dictionary> _dic;
|
||||
private readonly IRepository<Common_DictionaryOrd> _ord;
|
||||
private readonly IRepository<WY_DocPrint> _doc;
|
||||
private readonly IRepository<Common_Groupuser> _groupuser;
|
||||
private readonly IRepository<Common_freeGroupuser> _freegroup;
|
||||
private readonly IRepository<WY_SchZyBit> _zybit;
|
||||
|
||||
public DocPrintClassifyService(
|
||||
IRepository<Common_Dictionary> dic,
|
||||
IRepository<Common_DictionaryOrd> ord,
|
||||
IRepository<WY_DocPrint> doc,
|
||||
IRepository<Common_Groupuser> groupuser,
|
||||
IRepository<Common_freeGroupuser> freegroup,
|
||||
IRepository<WY_SchZyBit> zybit)
|
||||
{
|
||||
_dic = dic;
|
||||
_ord = ord;
|
||||
_doc = doc;
|
||||
_groupuser = groupuser;
|
||||
_freegroup = freegroup;
|
||||
_zybit = zybit;
|
||||
}
|
||||
|
||||
#region 文印分类 / 印制版面
|
||||
|
||||
public async Task AddDicAsync(Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
|
||||
var listed = await _dic.AnyAsync(b =>
|
||||
b.typeID == model.typeID && b.Name == model.Name && b.Schserid == model.Schserid);
|
||||
if (listed) throw new DException("该分类已经存在!");
|
||||
|
||||
var max = (await _dic.FindAsync(x => x.typeID == model.typeID))
|
||||
.OrderByDescending(x => x.SerID).FirstOrDefault();
|
||||
model.Sort = max != null ? (max.Sort ?? 0) + 1 : 1;
|
||||
model.Enable = 1;
|
||||
model.CodeID = model.typeID + model.Sort;
|
||||
model.typeName = "印制分类";
|
||||
|
||||
await _dic.AddAsync(model);
|
||||
await _dic.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task AddYZBMAsync(Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
|
||||
var listed = await _dic.AnyAsync(b =>
|
||||
b.typeID == model.typeID && b.Name == model.Name && b.Schserid == model.Schserid);
|
||||
if (listed) throw new DException("该版面已经存在!");
|
||||
|
||||
var max = (await _dic.FindAsync(x => x.typeID == model.typeID))
|
||||
.OrderByDescending(x => x.SerID).FirstOrDefault();
|
||||
model.Sort = max != null ? (max.Sort ?? 0) + 1 : 1;
|
||||
model.Enable = 1;
|
||||
model.CodeID = model.typeID + model.Sort;
|
||||
model.typeName = "印制版面";
|
||||
|
||||
await _dic.AddAsync(model);
|
||||
await _dic.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<List<CategoryItem>> CategoryListAsync(string schSerId, string name)
|
||||
=> await QueryClassifyListAsync("印制分类", schSerId, name);
|
||||
|
||||
public async Task<List<CategoryItem>> YZBMListAsync(string schSerId, string name)
|
||||
=> await QueryClassifyListAsync("印制版面", schSerId, name);
|
||||
|
||||
private async Task<List<CategoryItem>> QueryClassifyListAsync(string typeName, string schSerId, string name)
|
||||
{
|
||||
var query = await _dic.FindAsync(m => m.typeName == typeName);
|
||||
|
||||
if (!string.IsNullOrEmpty(schSerId))
|
||||
{
|
||||
query = query.Where(m => m.Schserid == schSerId || m.Schserid == null || m.Schserid == "").ToList();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
query = query.Where(m => m.Name != null && m.Name.IndexOf(name) >= 0).ToList();
|
||||
}
|
||||
|
||||
return query
|
||||
.OrderBy(x => string.IsNullOrEmpty(x.Des) ? 999 : DictionaryTypeHelper.ParseSort(x.Des))
|
||||
.Select(item => new CategoryItem
|
||||
{
|
||||
SerID = item.SerID,
|
||||
CodeID = item.CodeID,
|
||||
Name = item.Name,
|
||||
Des = item.Des,
|
||||
Enable = item.Enable,
|
||||
Sort = item.Sort,
|
||||
SchSerID = item.Schserid,
|
||||
UserSchSerID = schSerId,
|
||||
_disabled = string.IsNullOrEmpty(item.Schserid)
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task DeleteDicAsync(decimal[] ids)
|
||||
{
|
||||
if (ids == null || ids.Length == 0) throw new DException("ids is not null");
|
||||
|
||||
var items = await _dic.FindAsync(m => ids.Contains(m.SerID));
|
||||
if (items.Count == 0) throw new DException("未找到要删除的分类!");
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
var used = await _doc.AnyAsync(m => m.DPType == item.CodeID);
|
||||
if (used) throw new DException("该类别下有使用记录无法删除!");
|
||||
}
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
await _dic.RemoveAsync(item);
|
||||
}
|
||||
|
||||
await _dic.SaveChangesAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 公共类别
|
||||
|
||||
public async Task<List<DictionaryTypeItem>> DictionaryTypeListAsync(string schSerId, string name, bool showAll, string typeId)
|
||||
{
|
||||
var resultList = await _dic.FindAsync(m => m.typeID == typeId && m.Enable == 1);
|
||||
|
||||
if (!string.IsNullOrEmpty(schSerId))
|
||||
{
|
||||
resultList = resultList.Where(m => m.Schserid == schSerId || m.Schserid == null || m.Schserid == "").ToList();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
resultList = resultList.Where(m => m.Name != null && m.Name.IndexOf(name) >= 0).ToList();
|
||||
}
|
||||
|
||||
var ordList = await _ord.FindAsync(a => a.SchSerID == schSerId);
|
||||
|
||||
var items = resultList.Select(item => new DictionaryTypeItem
|
||||
{
|
||||
Code = item.Code,
|
||||
SerID = item.SerID,
|
||||
CodeID = item.CodeID,
|
||||
Name = item.Name,
|
||||
Des = item.Des,
|
||||
Enable = item.Enable,
|
||||
Sort = item.Sort,
|
||||
SchSerID = item.Schserid,
|
||||
UserSchSerID = schSerId,
|
||||
_disabled = string.IsNullOrEmpty(item.Schserid),
|
||||
Status = false,
|
||||
Open = ordList.FirstOrDefault(a => a.DicSerID == (int)item.SerID)?.Open ?? true,
|
||||
Ord = ordList.FirstOrDefault(a => a.DicSerID == (int)item.SerID)?.Ord
|
||||
?? (string.IsNullOrEmpty(item.Des) ? 999 : DictionaryTypeHelper.ParseSort(item.Des)),
|
||||
Path = item.Path
|
||||
})
|
||||
.OrderBy(a => a.Ord)
|
||||
.ToList();
|
||||
|
||||
if (!showAll)
|
||||
{
|
||||
items = items.Where(a => a.Open).ToList();
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
public async Task DeleteDictionaryTypeAsync(string[] ids)
|
||||
{
|
||||
if (ids == null || ids.Length == 0) throw new DException("ids is not null");
|
||||
|
||||
var items = await _dic.FindAsync(m => ids.Contains(m.SerID.ToString()));
|
||||
foreach (var item in items)
|
||||
{
|
||||
item.Enable = 3;
|
||||
}
|
||||
|
||||
await _dic.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task SetDictionaryOrdAsync(string schSerId, int dicSerId, bool show)
|
||||
{
|
||||
var obj = await _ord.FirstOrDefaultAsync(m => m.DicSerID == dicSerId && m.SchSerID == schSerId);
|
||||
if (obj == null)
|
||||
{
|
||||
await _ord.AddAsync(new Common_DictionaryOrd
|
||||
{
|
||||
DicSerID = dicSerId,
|
||||
Open = show,
|
||||
SchSerID = schSerId
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
obj.Open = show;
|
||||
await _ord.UpdateAsync(obj);
|
||||
}
|
||||
|
||||
await _ord.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task OrderDicAsync(string ids, string schSerId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(ids)) throw new DException("ids is not null");
|
||||
|
||||
var ssids = ids.Split(',').Select(a => int.Parse(a)).ToArray();
|
||||
var list = await _ord.FindAsync(m => m.SchSerID == schSerId);
|
||||
|
||||
var i = 0;
|
||||
foreach (var id in ssids)
|
||||
{
|
||||
var obj = list.FirstOrDefault(a => a.DicSerID == id && a.SchSerID == schSerId);
|
||||
if (obj == null)
|
||||
{
|
||||
await _ord.AddAsync(new Common_DictionaryOrd
|
||||
{
|
||||
DicSerID = id,
|
||||
Open = true,
|
||||
SchSerID = schSerId,
|
||||
Ord = i
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
obj.Ord = i;
|
||||
await _ord.UpdateAsync(obj);
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
await _ord.SaveChangesAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 文印资源开关 / 结果链接 / 待审角标
|
||||
|
||||
/// <summary>
|
||||
/// 结果链接:旧实现调用资源平台 ziyuan 的 /SaveResource/Batch 生成分享链接,
|
||||
/// 新项目未接入资源平台,按「未配置 ziyuan」的旧分支降级返回 false。
|
||||
/// </summary>
|
||||
public Task<object> GetResultUrlAsync(int dataId, int uid, int schId, string schSerId)
|
||||
{
|
||||
return Task.FromResult<object>(false);
|
||||
}
|
||||
|
||||
public async Task<bool> GetSchZyBitAsync(string schSerId)
|
||||
{
|
||||
// 旧实现仅在 ziyuan 配置存在时返回库值、否则 false;新项目未接入 ziyuan,直接返回库内开关值。
|
||||
var bit = await _zybit.FirstOrDefaultAsync(a => a.SchSerID == schSerId);
|
||||
return bit?.ziyuanbit ?? true;
|
||||
}
|
||||
|
||||
public async Task SetSchZyBitAsync(string schSerId, bool zyBit)
|
||||
{
|
||||
var obj = await _zybit.FirstOrDefaultAsync(a => a.SchSerID == schSerId);
|
||||
if (obj == null)
|
||||
{
|
||||
await _zybit.AddAsync(new WY_SchZyBit { SchSerID = schSerId, ziyuanbit = zyBit });
|
||||
}
|
||||
else
|
||||
{
|
||||
obj.ziyuanbit = zyBit;
|
||||
await _zybit.UpdateAsync(obj);
|
||||
}
|
||||
|
||||
await _zybit.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<int> GetShnheCountAsync(string userId, string schSerId)
|
||||
{
|
||||
if (!int.TryParse(userId, out var userid))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var docs = await _doc.FindAsync(a => a.SchSerID == schSerId && a.DocStatus == "0");
|
||||
var fixedGroupIds = (await _groupuser.FindAsync(b => b.userid == userid))
|
||||
.Select(b => b.usergroup).Where(g => g.HasValue).Select(g => g.Value).ToList();
|
||||
var freeGroupIds = (await _freegroup.FindAsync(b => b.userid == userid))
|
||||
.Select(b => b.usergroup).Where(g => g.HasValue).Select(g => g.Value).ToList();
|
||||
|
||||
var result = new HashSet<decimal>();
|
||||
foreach (var a in docs)
|
||||
{
|
||||
bool match = false;
|
||||
|
||||
// 固定流程:DocStep 命中本人组
|
||||
if (a.flowid != null && a.zhipai == null && a.DocStep.HasValue && fixedGroupIds.Contains(a.DocStep.Value))
|
||||
{
|
||||
match = true;
|
||||
}
|
||||
|
||||
// 指派:zhipai 含本人
|
||||
if (!match && a.zhipai != null && a.flowid != null && a.zhipai.Contains(userId))
|
||||
{
|
||||
match = true;
|
||||
}
|
||||
|
||||
// 自由流程:freestepid 命中本人自由组
|
||||
if (!match && a.freestepid.HasValue && a.freestepid.Value > 0 && freeGroupIds.Contains(a.freestepid.Value))
|
||||
{
|
||||
match = true;
|
||||
}
|
||||
|
||||
if (match)
|
||||
{
|
||||
result.Add(a.SerID);
|
||||
}
|
||||
}
|
||||
|
||||
return result.Count;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Yh.SchClass.OpenApi;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Repository;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.Service.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// 文印统计看板服务(移植旧 BestSchool.DocPrint.DocPrintStatistics,4 接口,ExportRecord 延后)。
|
||||
/// 旧 [NoAuth] 无用户上下文,schId 由前端传入;新库 SchSerID 已是 int 字符串,直接 schId.ToString() 匹配,
|
||||
/// 不再依赖旧 GetSchSerID(int) 的 HTTP 转换。roleId 参数旧实现零引用,已删除。
|
||||
/// 旧 StatusArray = {"1","11"}(印刷中/已印刷)。
|
||||
/// </summary>
|
||||
public class DocPrintStatisticsService : IDocPrintStatisticsService
|
||||
{
|
||||
private static readonly string[] StatusArray = { "1", "11" };
|
||||
|
||||
private readonly IRepository<WY_DocPrint> _doc;
|
||||
private readonly IRepository<Common_Dictionary> _dic;
|
||||
private readonly SchClassOpenApi _sch;
|
||||
|
||||
public DocPrintStatisticsService(
|
||||
IRepository<WY_DocPrint> doc,
|
||||
IRepository<Common_Dictionary> dic,
|
||||
SchClassOpenApi sch)
|
||||
{
|
||||
_doc = doc;
|
||||
_dic = dic;
|
||||
_sch = sch;
|
||||
}
|
||||
|
||||
public async Task<List<DocPrintCountItem>> GetDocPrintCountAsync(int schId, DateTime? startDate = null, DateTime? endDate = null)
|
||||
{
|
||||
var schSerId = schId.ToString();
|
||||
|
||||
var list = (await _doc.FindAsync(a =>
|
||||
a.SchSerID == schSerId && StatusArray.Contains(a.DocStatus))).ToList();
|
||||
|
||||
if (startDate != null)
|
||||
list = list.Where(a => a.EditDate >= startDate).ToList();
|
||||
if (endDate != null)
|
||||
list = list.Where(a => a.EditDate <= endDate).ToList();
|
||||
|
||||
var count = list.Count;
|
||||
var xhzsl = list.Select(a => a.DocNum).DefaultIfEmpty(0).Sum();
|
||||
|
||||
var doctypeId = list.GroupBy(a => a.DPType)
|
||||
.Select(a => new { type = a.Key, sum = a.Sum(s => s.DocNum) })
|
||||
.OrderByDescending(a => a.sum).FirstOrDefault()?.type ?? "";
|
||||
|
||||
string yzfl = "";
|
||||
if (!string.IsNullOrEmpty(doctypeId))
|
||||
{
|
||||
yzfl = (await _dic.FindAsync(g =>
|
||||
g.typeID == "YZFL" && g.CodeID == doctypeId &&
|
||||
(g.Schserid == schSerId || g.Schserid == null)))
|
||||
.OrderBy(g => g.Enable).Select(g => g.Name).FirstOrDefault() ?? "";
|
||||
}
|
||||
|
||||
return new List<DocPrintCountItem>
|
||||
{
|
||||
new DocPrintCountItem { k = "文印数量(条)", v = count },
|
||||
new DocPrintCountItem { k = "消耗总数(张)", v = xhzsl },
|
||||
new DocPrintCountItem { k = "最多消耗类型", v = yzfl },
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<List<PieChartSeries>> GetPieChartAsync(int schId, DateTime? startDate = null, DateTime? endDate = null)
|
||||
{
|
||||
var schSerId = schId.ToString();
|
||||
|
||||
var list = (await _doc.FindAsync(a =>
|
||||
a.SchSerID == schSerId && StatusArray.Contains(a.DocStatus))).ToList();
|
||||
|
||||
if (startDate != null)
|
||||
list = list.Where(a => a.EditDate >= startDate).ToList();
|
||||
if (endDate != null)
|
||||
list = list.Where(a => a.EditDate <= endDate).ToList();
|
||||
|
||||
var yzfl = (await _dic.FindAsync(g =>
|
||||
g.typeID == "YZFL" && (g.Schserid == schSerId || g.Schserid == null)))
|
||||
.OrderBy(g => g.Enable).ToList();
|
||||
|
||||
var yslx = list.GroupBy(a => a.DPType)
|
||||
.Select(a => new { k = a.Key, v = a.Sum(s => s.DocNum) }).ToList()
|
||||
.Select(a => new PieChartKv { k = yzfl.FirstOrDefault(s => s.CodeID == a.k)?.Name, v = a.v }).ToList();
|
||||
|
||||
var ysxh = list.GroupBy(a => a.DPType)
|
||||
.Select(a => new { k = a.Key, v = a.Count() }).ToList()
|
||||
.Select(a => new PieChartKv { k = yzfl.FirstOrDefault(s => s.CodeID == a.k)?.Name, v = a.v }).ToList();
|
||||
|
||||
return new List<PieChartSeries>
|
||||
{
|
||||
new PieChartSeries { title = "印刷类型", list = yslx },
|
||||
new PieChartSeries { title = "印刷消耗", list = ysxh },
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<List<DocPrintTypeItem>> GetDocPrintTypeAsync(int schId)
|
||||
{
|
||||
var schSerId = schId.ToString();
|
||||
|
||||
var list = await _dic.FindAsync(g =>
|
||||
g.typeID == "YZFL" && g.Enable == 1 && (g.Schserid == schSerId || g.Schserid == null));
|
||||
|
||||
return list.OrderBy(g => g.Des)
|
||||
.Select(a => new DocPrintTypeItem { CodeID = a.CodeID, Name = a.Name })
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<DocPrintListResult> GetDocPrintListAsync(int schId, string docType, string likeName, int pageIndex, int pageSize, DateTime? startDate = null, DateTime? endDate = null)
|
||||
{
|
||||
var schSerId = schId.ToString();
|
||||
if (pageSize <= 0) pageSize = 10;
|
||||
|
||||
var list = (await _doc.FindAsync(a =>
|
||||
a.SchSerID == schSerId && StatusArray.Contains(a.DocStatus))).ToList();
|
||||
|
||||
if (!string.IsNullOrEmpty(docType) && docType != "ALL")
|
||||
list = list.Where(a => a.DPType == docType).ToList();
|
||||
|
||||
if (startDate != null)
|
||||
list = list.Where(a => a.TakeDate >= startDate).ToList();
|
||||
if (endDate != null)
|
||||
list = list.Where(a => a.TakeDate <= endDate).ToList();
|
||||
|
||||
if (!string.IsNullOrEmpty(likeName))
|
||||
{
|
||||
var teaMap = await GetTeaMapAsync(schSerId);
|
||||
var userids = teaMap.Where(a => a.Value.Name.Contains(likeName))
|
||||
.Select(a => a.Key.ToString()).ToArray();
|
||||
list = list.Where(m =>
|
||||
(m.DocTitle != null && m.DocTitle.Contains(likeName)) ||
|
||||
(m.DocContent != null && m.DocContent.Contains(likeName)) ||
|
||||
(m.UserID != null && userids.Contains(m.UserID))).ToList();
|
||||
}
|
||||
|
||||
var totle = list.Count;
|
||||
if (totle == 0)
|
||||
return new DocPrintListResult { tlist = new List<DocPrintStatItem>(), totle = totle };
|
||||
|
||||
// 旧 1 基分页 → 0 基
|
||||
pageIndex -= 1;
|
||||
if (pageIndex < 0) pageIndex = 0;
|
||||
|
||||
var yzflList = (await _dic.FindAsync(g =>
|
||||
g.typeID == "YZFL" && (g.Schserid == schSerId || g.Schserid == null)))
|
||||
.OrderBy(g => g.Des)
|
||||
.Select(a => new DocPrintTypeItem { CodeID = a.CodeID, Name = a.Name }).ToList();
|
||||
|
||||
var yzbmList = (await _dic.FindAsync(g =>
|
||||
g.typeID == "YZBM" && (g.Schserid == schSerId || g.Schserid == null)))
|
||||
.OrderBy(g => g.Des)
|
||||
.Select(a => new DocPrintTypeItem { CodeID = a.CodeID, Name = a.Name }).ToList();
|
||||
|
||||
var teaMap2 = await GetTeaMapAsync(schSerId);
|
||||
var tlist = list.OrderByDescending(a => a.TakeDate)
|
||||
.Skip(pageIndex * pageSize).Take(pageSize)
|
||||
.Select(item => new DocPrintStatItem
|
||||
{
|
||||
SerID = (int)item.SerID,
|
||||
DocContent = item.DocContent,
|
||||
DocType = item.DPType,
|
||||
DocNum = item.DocNum,
|
||||
DocTypeName = yzflList.FirstOrDefault(a => a.CodeID == item.DPType)?.Name,
|
||||
DocStyle = item.DocStyle,
|
||||
DocStyleName = yzbmList.FirstOrDefault(a => a.CodeID == item.DocStyle)?.Name,
|
||||
EditDate = item.EditDate.ToString("yyyy-MM-dd HH:mm"),
|
||||
TakeDate = item.TakeDate?.ToString("yyyy-MM-dd"),
|
||||
UserID = item.UserID,
|
||||
teaname = GetTeaName(teaMap2, item.UserID),
|
||||
UpFile = item.UpFile,
|
||||
DocTitle = item.DocTitle,
|
||||
DocStatus = item.DocStatus,
|
||||
DocStatusName = item.DocStatus == "1" ? "印刷中" : "已印刷",
|
||||
DocStep = item.DocStep,
|
||||
flowid = item.flowid,
|
||||
FreeUserID = item.FreeUserID,
|
||||
SendUserID = item.SendUserID,
|
||||
freestepid = item.freestepid,
|
||||
DocShare = item.DocShare,
|
||||
explain = item.explain,
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return new DocPrintListResult { tlist = tlist, totle = totle };
|
||||
}
|
||||
|
||||
#region 辅助
|
||||
|
||||
private async Task<Dictionary<int, (string Name, string Photo)>> GetTeaMapAsync(string schSerId)
|
||||
{
|
||||
var teas = await _sch.TeaInfo.GetSchTeaInfoAsync(schSerId, null);
|
||||
return teas
|
||||
.Where(t => int.TryParse(t.TeaSerId, out _))
|
||||
.GroupBy(t => int.Parse(t.TeaSerId))
|
||||
.ToDictionary(g => g.Key, g => (g.First().Name ?? "", g.First().Photo ?? ""));
|
||||
}
|
||||
|
||||
private static string GetTeaName(Dictionary<int, (string Name, string Photo)> teaMap, string userId)
|
||||
{
|
||||
if (!int.TryParse(userId, out var uid)) return "";
|
||||
return teaMap.TryGetValue(uid, out var tea) ? tea.Name : "";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Yh.Platform.Abstractions;
|
||||
using YhCore.Common.Config;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Repository;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.Service.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// 公共文件服务实现(见 IFileService 注释)。
|
||||
/// 降级:Office→PDF(ToOfficeHelper)新项目缺失,上传不转 PDF;预览兜底 officeapps.live.com。
|
||||
/// </summary>
|
||||
public class FileService : IFileService
|
||||
{
|
||||
private readonly IRepository<Common_UpFiles> _upfiles;
|
||||
|
||||
public FileService(IRepository<Common_UpFiles> upfiles)
|
||||
{
|
||||
_upfiles = upfiles;
|
||||
}
|
||||
|
||||
public async Task<FileObj> GetEnclosureAsync(int id)
|
||||
{
|
||||
var model = await _upfiles.FirstOrDefaultAsync(x => x.SerID == id);
|
||||
if (model == null)
|
||||
throw new DException("文件ID错误!");
|
||||
|
||||
return new FileObj
|
||||
{
|
||||
fileurl = model.FileName,
|
||||
filename = model.FileTrueName,
|
||||
filetype = model.FileType,
|
||||
filesize = model.FileSize ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<int> SaveEnclosureAsync(FileObj file, string appType)
|
||||
{
|
||||
// 替换文件名中的特殊字符(旧 FileSaveEnclosure)
|
||||
string safeName = file.filename ?? "";
|
||||
foreach (var c in Path.GetInvalidFileNameChars())
|
||||
safeName = safeName.Replace(c, '_');
|
||||
safeName = safeName.Replace(",", "_").Replace(",", "_");
|
||||
|
||||
var model = new Common_UpFiles
|
||||
{
|
||||
FileName = file.fileurl,
|
||||
FileSize = file.filesize,
|
||||
FileTrueName = safeName,
|
||||
FileType = file.filetype,
|
||||
DateAndTime = DateTime.Now,
|
||||
TopicType = appType,
|
||||
};
|
||||
await _upfiles.AddAsync(model);
|
||||
await _upfiles.SaveChangesAsync();
|
||||
return model.SerID;
|
||||
}
|
||||
|
||||
public async Task<FileObj> SaveFileAsync(string appType, IFormFile file)
|
||||
{
|
||||
if (file == null || file.Length == 0)
|
||||
throw new DException("上传失败!");
|
||||
|
||||
if (appType.ToLower() == "noticemsg") appType = "notice";
|
||||
string dtStr = DateTime.Now.ToString("yyyyMMdd");
|
||||
string physicalDir = Path.Combine(StorageRoot(), appType, dtStr);
|
||||
string outFilePath = "/UploadFile/" + appType + "/" + dtStr + "/";
|
||||
|
||||
if (!Directory.Exists(physicalDir))
|
||||
Directory.CreateDirectory(physicalDir);
|
||||
|
||||
string guid = Guid.NewGuid().ToString();
|
||||
string fileName = Path.GetFileName(file.FileName);
|
||||
string suffixName = Path.GetExtension(file.FileName);
|
||||
|
||||
string[] suffArry = { "jpg", "gif", "png", "xls", "doc", "txt", "docx", "xlsx", "zip", "rar", "pdf", "ppt", "pptx" };
|
||||
suffArry = suffArry.Select(x => "." + x).ToArray();
|
||||
bool isSuffType = suffArry.Any(x => x == suffixName.ToLower());
|
||||
if (!isSuffType)
|
||||
throw new DException("不能上传" + suffixName + "格式的文件");
|
||||
|
||||
int fileSize = (int)file.Length;
|
||||
string savePath = Path.Combine(physicalDir, guid + suffixName);
|
||||
using (var stream = new FileStream(savePath, FileMode.Create))
|
||||
{
|
||||
await file.CopyToAsync(stream);
|
||||
}
|
||||
// 旧 ToOfficeHelper.OfficeConvertToPDF(Office COM)新项目缺失 → 降级不转 PDF
|
||||
|
||||
return new FileObj
|
||||
{
|
||||
fileurl = outFilePath + guid + suffixName,
|
||||
filename = fileName,
|
||||
filetype = suffixName,
|
||||
filesize = fileSize,
|
||||
host = Config.FileWebPath,
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<List<FileObj>> SaveFileImageAsync(string appType, IFormFileCollection files)
|
||||
{
|
||||
if (appType.ToLower() == "noticemsg") appType = "notice";
|
||||
string dtStr = DateTime.Now.ToString("yyyyMMdd");
|
||||
string physicalDir = Path.Combine(StorageRoot(), appType, dtStr);
|
||||
string outFilePath = "/UploadFile/" + appType + "/" + dtStr + "/";
|
||||
|
||||
if (!Directory.Exists(physicalDir))
|
||||
Directory.CreateDirectory(physicalDir);
|
||||
|
||||
string[] suffArry = { "jpg", "gif", "png", "jpeg" };
|
||||
suffArry = suffArry.Select(x => "." + x).ToArray();
|
||||
|
||||
var list = new List<FileObj>();
|
||||
foreach (var file in files)
|
||||
{
|
||||
if (file == null || file.Length == 0) continue;
|
||||
string suffixName = Path.GetExtension(file.FileName);
|
||||
if (!suffArry.Any(x => x == suffixName.ToLower())) continue;
|
||||
|
||||
string guid = Guid.NewGuid().ToString();
|
||||
string fileName = Path.GetFileName(file.FileName);
|
||||
int fileSize = (int)file.Length;
|
||||
string savePath = Path.Combine(physicalDir, guid + suffixName);
|
||||
using (var stream = new FileStream(savePath, FileMode.Create))
|
||||
{
|
||||
await file.CopyToAsync(stream);
|
||||
}
|
||||
|
||||
list.Add(new FileObj
|
||||
{
|
||||
fileurl = outFilePath + guid + suffixName,
|
||||
filename = fileName,
|
||||
filetype = suffixName,
|
||||
filesize = fileSize,
|
||||
host = Config.FileWebPath,
|
||||
});
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public async Task SaveChunkAsync(string guid, string chunkIndex, IFormFile chunk)
|
||||
{
|
||||
if (string.IsNullOrEmpty(guid) || chunk == null || chunk.Length == 0)
|
||||
throw new DException("上传失败!");
|
||||
|
||||
var dir = Path.Combine(StorageRoot(), guid);
|
||||
if (!Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
|
||||
string filePath = Path.Combine(dir, chunkIndex);
|
||||
using (var stream = new FileStream(filePath, FileMode.Create))
|
||||
{
|
||||
await chunk.CopyToAsync(stream);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<FileObj> MergeSaveAsync(string appType, string guid, string fileName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(guid) || string.IsNullOrEmpty(fileName))
|
||||
throw new DException("上传失败!");
|
||||
|
||||
if (appType.ToLower() == "noticemsg") appType = "notice";
|
||||
string dtStr = DateTime.Now.ToString("yyyyMMdd");
|
||||
string physicalDir = Path.Combine(StorageRoot(), appType, dtStr);
|
||||
string outFilePath = "/UploadFile/" + appType + "/" + dtStr + "/";
|
||||
if (!Directory.Exists(physicalDir))
|
||||
Directory.CreateDirectory(physicalDir);
|
||||
|
||||
var uploadDir = Path.Combine(StorageRoot());
|
||||
var dir = Path.Combine(uploadDir, guid);
|
||||
if (!Directory.Exists(dir))
|
||||
throw new DException("上传失败!");
|
||||
|
||||
var files = Directory.GetFiles(dir);
|
||||
string fileRealName = Path.GetFileName(fileName);
|
||||
string suffixName = Path.GetExtension(fileRealName);
|
||||
string fileGuid = Guid.NewGuid().ToString();
|
||||
string outUrl = outFilePath + fileGuid + suffixName;
|
||||
string finalPath = Path.Combine(physicalDir, fileGuid + suffixName);
|
||||
|
||||
using (var fs = new FileStream(finalPath, FileMode.Create))
|
||||
{
|
||||
foreach (var part in files.OrderBy(x => x.Length).ThenBy(x => x))
|
||||
{
|
||||
var bytes = await File.ReadAllBytesAsync(part);
|
||||
fs.Write(bytes, 0, bytes.Length);
|
||||
File.Delete(part);
|
||||
}
|
||||
}
|
||||
int fileSize = (int)(new FileInfo(finalPath).Length / 1024 / 1024); // 旧实现以 MB 存储,保持一致
|
||||
Directory.Delete(dir);
|
||||
|
||||
// 旧 ToOfficeHelper.OfficeConvertToPDF 缺失 → 降级不转 PDF
|
||||
|
||||
return new FileObj
|
||||
{
|
||||
fileurl = outUrl,
|
||||
filename = fileRealName,
|
||||
filetype = suffixName,
|
||||
filesize = fileSize,
|
||||
host = Config.FileWebPath,
|
||||
};
|
||||
}
|
||||
|
||||
public string MapPhysicalPath(string fileUrl)
|
||||
{
|
||||
if (string.IsNullOrEmpty(fileUrl))
|
||||
return "";
|
||||
|
||||
// 相对路径如 /UploadFile/docprint/20260101/{guid}.jpg → Config.FilePath + 子路径
|
||||
var relative = fileUrl.TrimStart('/');
|
||||
const string prefix = "UploadFile/";
|
||||
if (relative.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
relative = relative.Substring(prefix.Length);
|
||||
|
||||
return Path.Combine(StorageRoot(), relative.Replace('/', Path.DirectorySeparatorChar));
|
||||
}
|
||||
|
||||
/// <summary>存储根目录(旧 Server.MapPath("~/UploadFile/");Config.FilePath 缺省时退回进程目录/UploadFile)。</summary>
|
||||
private static string StorageRoot()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(Config.FilePath))
|
||||
return Config.FilePath;
|
||||
|
||||
return Path.Combine(AppContext.BaseDirectory, "UploadFile");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,595 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Yh.Platform.Abstractions;
|
||||
using Yh.SchClass.OpenApi;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Repository;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.Service.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// 自由审批流服务(移植旧 BestSchool.Service.ApproveFlow)。
|
||||
/// 教师查询改走教务 SDK(Yh.SchClass.OpenApi),教师标识用 teaId(TeaSerId)对齐新教务。
|
||||
/// </summary>
|
||||
public class FreeApproveService : IFreeApproveService
|
||||
{
|
||||
private readonly IRepository<Common_freeflowinfo> _freeflowinfo;
|
||||
private readonly IRepository<Common_freeGroupuser> _freegroup;
|
||||
private readonly IRepository<Common_flowtranctInfo> _flowtranct;
|
||||
private readonly IRepository<Common_flowstep> _flowstep;
|
||||
private readonly IRepository<Common_Groupuser> _groupuser;
|
||||
private readonly IRepository<Common_UserFlowZhipai> _userflowzhipai;
|
||||
private readonly YHDbContext _db;
|
||||
private readonly SchClassOpenApi _sch;
|
||||
|
||||
public FreeApproveService(
|
||||
IRepository<Common_freeflowinfo> freeflowinfo,
|
||||
IRepository<Common_freeGroupuser> freegroup,
|
||||
IRepository<Common_flowtranctInfo> flowtranct,
|
||||
IRepository<Common_flowstep> flowstep,
|
||||
IRepository<Common_Groupuser> groupuser,
|
||||
IRepository<Common_UserFlowZhipai> userflowzhipai,
|
||||
YHDbContext db,
|
||||
SchClassOpenApi sch)
|
||||
{
|
||||
_freeflowinfo = freeflowinfo;
|
||||
_freegroup = freegroup;
|
||||
_flowtranct = flowtranct;
|
||||
_flowstep = flowstep;
|
||||
_groupuser = groupuser;
|
||||
_userflowzhipai = userflowzhipai;
|
||||
_db = db;
|
||||
_sch = sch;
|
||||
}
|
||||
|
||||
public async Task<List<FreeFlowNode>> GetFleeFlowAsync(int userId, int? dataid)
|
||||
{
|
||||
int appid = DocPrintConstants.AppCode;
|
||||
|
||||
if ((dataid ?? 0) == 0)
|
||||
{
|
||||
var all = await _freeflowinfo.FindAsync(a => a.flowType == appid && a.userid == userId);
|
||||
dataid = all.Count == 0 ? 0 : all.Max(a => a.dataid ?? 0);
|
||||
}
|
||||
|
||||
if ((dataid ?? 0) <= 0) return new List<FreeFlowNode>();
|
||||
|
||||
var nodes = (await _freeflowinfo.FindAsync(a => a.flowType == appid && a.userid == userId && a.dataid == dataid))
|
||||
.OrderBy(a => a.SerID).ToList();
|
||||
return await BuildFreeFlowNodesAsync(nodes);
|
||||
}
|
||||
|
||||
public async Task<int> EditFleeFlowAsync(int userId, string schSerId, int? dataid, string flowUids, bool isFlowNull)
|
||||
{
|
||||
if (string.IsNullOrEmpty(flowUids))
|
||||
{
|
||||
if (!isFlowNull)
|
||||
throw new DException("请选择审核步骤人员!");
|
||||
flowUids = "";
|
||||
}
|
||||
|
||||
var flowUidArr = flowUids.Split('_');
|
||||
int appid = DocPrintConstants.AppCode;
|
||||
int firstStepId = 0;
|
||||
|
||||
using var tx = await _db.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
if ((dataid ?? 0) > 0)
|
||||
{
|
||||
var old = await _freeflowinfo.FindAsync(a => a.dataid == dataid && a.flowType == appid && a.userid == userId);
|
||||
foreach (var item in old)
|
||||
{
|
||||
await _freeflowinfo.RemoveAsync(item.SerID);
|
||||
}
|
||||
await _freeflowinfo.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var upSeptid = 0;
|
||||
var date = DateTime.Now;
|
||||
foreach (var fw in flowUidArr)
|
||||
{
|
||||
var free = new Common_freeflowinfo
|
||||
{
|
||||
laststep = upSeptid,
|
||||
userid = userId,
|
||||
createTime = date,
|
||||
dataid = dataid,
|
||||
schserid = schSerId,
|
||||
flowType = appid,
|
||||
};
|
||||
await _freeflowinfo.AddAsync(free);
|
||||
await _freeflowinfo.SaveChangesAsync();
|
||||
|
||||
if (upSeptid == 0)
|
||||
firstStepId = free.SerID;
|
||||
upSeptid = free.SerID;
|
||||
|
||||
foreach (var guserid in fw.Split(','))
|
||||
{
|
||||
await _freegroup.AddAsync(new Common_freeGroupuser
|
||||
{
|
||||
flowid = upSeptid,
|
||||
SchSerID = schSerId,
|
||||
usergroup = upSeptid,
|
||||
userid = string.IsNullOrEmpty(guserid) ? (int?)null : int.Parse(guserid),
|
||||
});
|
||||
}
|
||||
await _freegroup.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await tx.CommitAsync();
|
||||
return firstStepId;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await tx.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> AddShenheRemarkAsync(int userId, int? dataid, string remark, int freestepid)
|
||||
{
|
||||
var info = new Common_flowtranctInfo
|
||||
{
|
||||
actionstatus = -1,
|
||||
remark = remark,
|
||||
dataid = dataid ?? 0,
|
||||
stepid = freestepid,
|
||||
datatype = DocPrintConstants.AppCode.ToString(),
|
||||
actiondate = DateTime.Now,
|
||||
actionuserid = userId,
|
||||
};
|
||||
await _flowtranct.AddAsync(info);
|
||||
await _flowtranct.SaveChangesAsync();
|
||||
return "备注成功!";
|
||||
}
|
||||
|
||||
public async Task<List<FreeFlowNode>> GetAfterFleeFlowAsync(int? dataid, int freestepid)
|
||||
{
|
||||
int appid = DocPrintConstants.AppCode;
|
||||
|
||||
if ((dataid ?? 0) <= 0) return new List<FreeFlowNode>();
|
||||
|
||||
var nodes = (await _freeflowinfo.FindAsync(a => a.flowType == appid && a.SerID > freestepid && a.dataid == dataid))
|
||||
.OrderBy(a => a.SerID).ToList();
|
||||
return await BuildFreeFlowNodesAsync(nodes);
|
||||
}
|
||||
|
||||
public async Task<int> EditAfterFleeFlowAsync(int? dataid, int freestepid, string flowUids)
|
||||
{
|
||||
int appid = DocPrintConstants.AppCode;
|
||||
|
||||
// 删除 freestepid 之后的剩余步骤
|
||||
var nextSteps = await _freeflowinfo.FindAsync(a => a.dataid == dataid && a.SerID > freestepid);
|
||||
if (nextSteps.Count > 0)
|
||||
{
|
||||
foreach (var item in nextSteps)
|
||||
{
|
||||
await _freeflowinfo.RemoveAsync(item.SerID);
|
||||
}
|
||||
await _freeflowinfo.SaveChangesAsync();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(flowUids))
|
||||
return 0;
|
||||
|
||||
var flowUidArr = flowUids.Split('_');
|
||||
var laststept = await _freeflowinfo.FirstOrDefaultAsync(a => a.SerID == freestepid);
|
||||
if (laststept == null)
|
||||
throw new DException("未找到自由流程步骤!");
|
||||
|
||||
var upSeptid = freestepid;
|
||||
using var tx = await _db.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
var date = DateTime.Now;
|
||||
foreach (var fw in flowUidArr)
|
||||
{
|
||||
var free = new Common_freeflowinfo
|
||||
{
|
||||
laststep = upSeptid,
|
||||
userid = laststept.userid,
|
||||
createTime = date,
|
||||
dataid = dataid,
|
||||
schserid = laststept.schserid,
|
||||
flowType = appid,
|
||||
};
|
||||
await _freeflowinfo.AddAsync(free);
|
||||
await _freeflowinfo.SaveChangesAsync();
|
||||
|
||||
upSeptid = free.SerID;
|
||||
foreach (var guserid in fw.Split(','))
|
||||
{
|
||||
await _freegroup.AddAsync(new Common_freeGroupuser
|
||||
{
|
||||
flowid = upSeptid,
|
||||
SchSerID = laststept.schserid,
|
||||
usergroup = upSeptid,
|
||||
userid = string.IsNullOrEmpty(guserid) ? (int?)null : int.Parse(guserid),
|
||||
});
|
||||
}
|
||||
await _freegroup.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await tx.CommitAsync();
|
||||
return upSeptid;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await tx.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<FreeShenheInfo>> ShenheFlowAsync(int uid, string ctime, int dataid, int? freestepid, int? flowid)
|
||||
{
|
||||
int flowType = DocPrintConstants.AppCode;
|
||||
string appCode = DocPrintConstants.AppCode.ToString();
|
||||
|
||||
if ((freestepid ?? 0) > 0)
|
||||
return await BuildFreeShenheFlowAsync(uid, ctime, dataid, freestepid, flowType, appCode);
|
||||
|
||||
if ((flowid ?? 0) > 0)
|
||||
return await BuildFixShenheFlowAsync(uid, ctime, dataid, flowid, appCode);
|
||||
|
||||
return await BuildOnlyTranctShenheFlowAsync(uid, ctime, dataid, appCode);
|
||||
}
|
||||
|
||||
public async Task<List<ChaoSendUser>> ChaoSendListAsync(string uids)
|
||||
{
|
||||
var result = new List<ChaoSendUser>();
|
||||
|
||||
if (string.IsNullOrEmpty(uids) || uids == "undefined")
|
||||
return result;
|
||||
|
||||
var userids = uids.Split(',')
|
||||
.Where(a => !string.IsNullOrEmpty(a) && int.TryParse(a, out _))
|
||||
.Select(int.Parse).ToArray();
|
||||
if (userids.Length == 0)
|
||||
return result;
|
||||
|
||||
var teas = await _sch.TeaInfo.GetArrTeaInfoAsync(string.Join(",", userids));
|
||||
return teas.Select(a => new ChaoSendUser
|
||||
{
|
||||
uid = ParseTeaId(a.TeaSerId),
|
||||
photo = a.Photo,
|
||||
name = a.Name,
|
||||
status = false,
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
#region 审核流程回显
|
||||
|
||||
private async Task<List<FreeShenheInfo>> BuildFreeShenheFlowAsync(int uid, string ctime, int dataid, int? freestepid, int flowType, string appCode)
|
||||
{
|
||||
var freeflow = (await _freeflowinfo.FindAsync(a => a.dataid == dataid && a.flowType == flowType))
|
||||
.OrderBy(a => a.SerID).ToList();
|
||||
var freeflowids = freeflow.Select(a => a.SerID).ToArray();
|
||||
var freeflowGroup = (await _freegroup.FindAsync(a => freeflowids.Contains(a.usergroup ?? 0))).ToList();
|
||||
var freeshenhe = await _flowtranct.FindAsync(a => a.dataid == dataid && a.datatype == appCode);
|
||||
|
||||
var frees = new List<FreeShenheInfo>();
|
||||
var firstFlow = freeflow.FirstOrDefault();
|
||||
frees.Add(new FreeShenheInfo
|
||||
{
|
||||
steptname = await GetUserNameAsync(firstFlow?.userid?.ToString() ?? ""),
|
||||
steptstatus = "发起申请",
|
||||
photo = await GetUserPhotoAsync(firstFlow?.userid?.ToString() ?? ""),
|
||||
actiondate = firstFlow?.createTime?.ToString("yyyy-MM-dd HH:mm") ?? "",
|
||||
});
|
||||
|
||||
if (freeshenhe.Count(a => a.actionstatus > 2) == 0)
|
||||
{
|
||||
foreach (var item in freeflow)
|
||||
{
|
||||
var act = new FreeShenheInfo();
|
||||
var isShok = freeshenhe.FirstOrDefault(a => a.stepid == item.SerID);
|
||||
if (isShok != null)
|
||||
{
|
||||
act.steptname = await ChaoSendNameAsync(isShok.actionuserid?.ToString());
|
||||
act.photo = await GetUserPhotoAsync(isShok.actionuserid?.ToString());
|
||||
act.steptstatus = ActionstatusName(isShok.actionstatus);
|
||||
act.actionstatus = isShok.actionstatus.ToString();
|
||||
act.Remark = isShok.remark;
|
||||
act.actiondate = isShok.actiondate?.ToString("yyyy-MM-dd HH:mm") ?? "";
|
||||
|
||||
if (isShok.actionstatus == 2)
|
||||
{
|
||||
frees.Add(act);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var userids = ToUserIds(freeflowGroup.Where(a => a.usergroup == item.SerID).Select(a => a.userid));
|
||||
act.steptname = await ChaoSendNameAsync(string.Join(",", userids));
|
||||
act.photo = userids.Length == 1 ? await GetUserPhotoAsync(userids[0].ToString()) : "/static/image/site/moreperpeo.png";
|
||||
act.steptstatus = "待审核";
|
||||
act.actionstatus = "0";
|
||||
}
|
||||
frees.Add(act);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in freeshenhe)
|
||||
{
|
||||
frees.Add(new FreeShenheInfo
|
||||
{
|
||||
steptname = await ChaoSendNameAsync((item.actionuserid ?? uid).ToString()),
|
||||
photo = await GetUserPhotoAsync((item.actionuserid ?? uid).ToString()),
|
||||
steptstatus = ActionstatusName(item.actionstatus),
|
||||
actionstatus = item.actionstatus.ToString(),
|
||||
Remark = item.remark,
|
||||
actiondate = item.actiondate?.ToString("yyyy-MM-dd HH:mm") ?? "",
|
||||
});
|
||||
}
|
||||
|
||||
var zuihouyibu = freeshenhe.Where(a => a.actionstatus >= 2).OrderByDescending(a => a.SerID).FirstOrDefault();
|
||||
if (zuihouyibu != null && zuihouyibu.actionstatus == 4)
|
||||
{
|
||||
var isShok = freeshenhe.FirstOrDefault(a => a.SerID > zuihouyibu.SerID);
|
||||
if (isShok == null)
|
||||
{
|
||||
var userids = ToUserIds(freeflowGroup.Where(a => a.usergroup == freestepid).Select(a => a.userid));
|
||||
frees.Add(new FreeShenheInfo
|
||||
{
|
||||
steptname = await ChaoSendNameAsync(string.Join(",", userids)),
|
||||
photo = userids.Length == 1 ? await GetUserPhotoAsync(userids[0].ToString()) : "/static/image/site/moreperpeo.png",
|
||||
steptstatus = "待审核",
|
||||
actionstatus = "0",
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (zuihouyibu != null && zuihouyibu.actionstatus == 6)
|
||||
{
|
||||
foreach (var item in freeflow)
|
||||
{
|
||||
var isShok = freeshenhe.FirstOrDefault(a => a.stepid == item.SerID && a.SerID > zuihouyibu.SerID);
|
||||
if (isShok == null)
|
||||
{
|
||||
var userids = ToUserIds(freeflowGroup.Where(a => a.usergroup == item.SerID).Select(a => a.userid));
|
||||
frees.Add(new FreeShenheInfo
|
||||
{
|
||||
steptname = await ChaoSendNameAsync(string.Join(",", userids)),
|
||||
photo = userids.Length == 1 ? await GetUserPhotoAsync(userids[0].ToString()) : "/static/image/site/moreperpeo.png",
|
||||
steptstatus = "待审核",
|
||||
actionstatus = "0",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return frees;
|
||||
}
|
||||
|
||||
private async Task<List<FreeShenheInfo>> BuildFixShenheFlowAsync(int uid, string ctime, int dataid, int? flowid, string appCode)
|
||||
{
|
||||
var fixflow = (await _flowstep.FindAsync(a => a.flowid == flowid)).OrderBy(a => a.SerID).ToList();
|
||||
var fixflowids = fixflow.Select(a => a.SerID).ToArray();
|
||||
var fixflowGroup = (await _groupuser.FindAsync(a => fixflowids.Contains(a.usergroup ?? 0))).ToList();
|
||||
var fixshenhe = await _flowtranct.FindAsync(a => a.dataid == dataid && a.datatype == appCode);
|
||||
|
||||
var frees = new List<FreeShenheInfo>();
|
||||
frees.Add(new FreeShenheInfo
|
||||
{
|
||||
steptname = await GetUserNameAsync(uid.ToString()),
|
||||
steptstatus = "发起申请",
|
||||
actiondate = ctime,
|
||||
photo = await GetUserPhotoAsync(uid.ToString()),
|
||||
});
|
||||
|
||||
if (fixshenhe.Count(a => a.actionstatus > 2) == 0)
|
||||
{
|
||||
foreach (var item in fixflow)
|
||||
{
|
||||
var act = new FreeShenheInfo();
|
||||
var isShok = fixshenhe.FirstOrDefault(a => a.stepid == item.SerID);
|
||||
if (isShok != null)
|
||||
{
|
||||
act.steptname = await ChaoSendNameAsync(isShok.actionuserid?.ToString());
|
||||
act.photo = await GetUserPhotoAsync(isShok.actionuserid?.ToString());
|
||||
act.steptstatus = ActionstatusName(isShok.actionstatus);
|
||||
act.actionstatus = isShok.actionstatus.ToString();
|
||||
act.Remark = isShok.remark;
|
||||
act.actiondate = isShok.actiondate?.ToString("yyyy-MM-dd HH:mm") ?? "";
|
||||
|
||||
if (isShok.actionstatus == 2)
|
||||
{
|
||||
frees.Add(act);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var userids = ToUserIds(fixflowGroup.Where(a => a.usergroup == item.SerID).Select(a => a.userid));
|
||||
|
||||
var userzhipai = await _userflowzhipai.FirstOrDefaultAsync(
|
||||
a => a.MSerID == dataid && a.FlowID == flowid && a.FlowStepID == item.SerID);
|
||||
if (!string.IsNullOrEmpty(userzhipai?.zhipai))
|
||||
{
|
||||
userids = userzhipai.zhipai.Split(',')
|
||||
.Where(s => int.TryParse(s, out _))
|
||||
.Select(int.Parse).Distinct().ToArray();
|
||||
}
|
||||
|
||||
act.steptname = await ChaoSendNameAsync(string.Join(",", userids));
|
||||
act.photo = userids.Length == 1 ? await GetUserPhotoAsync(userids[0].ToString()) : "/static/image/site/moreperpeo.png";
|
||||
act.steptstatus = "待审核";
|
||||
act.actionstatus = "0";
|
||||
}
|
||||
frees.Add(act);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in fixshenhe)
|
||||
{
|
||||
frees.Add(new FreeShenheInfo
|
||||
{
|
||||
steptname = await ChaoSendNameAsync((item.actionuserid ?? uid).ToString()),
|
||||
photo = await GetUserPhotoAsync((item.actionuserid ?? uid).ToString()),
|
||||
steptstatus = ActionstatusName(item.actionstatus),
|
||||
actionstatus = item.actionstatus.ToString(),
|
||||
Remark = item.remark,
|
||||
actiondate = item.actiondate?.ToString("yyyy-MM-dd HH:mm") ?? "",
|
||||
});
|
||||
}
|
||||
|
||||
var zuihouyibu = fixshenhe.Where(a => a.actionstatus >= 2).OrderByDescending(a => a.SerID).FirstOrDefault();
|
||||
if (zuihouyibu != null && zuihouyibu.actionstatus == 4)
|
||||
{
|
||||
var isShok = fixshenhe.FirstOrDefault(a => a.SerID > zuihouyibu.SerID);
|
||||
if (isShok == null)
|
||||
{
|
||||
var lastStepId = fixflowids.OrderByDescending(p => p).FirstOrDefault();
|
||||
var userids = ToUserIds(fixflowGroup.Where(a => a.usergroup == lastStepId).Select(a => a.userid));
|
||||
frees.Add(new FreeShenheInfo
|
||||
{
|
||||
steptname = await ChaoSendNameAsync(string.Join(",", userids)),
|
||||
photo = userids.Length == 1 ? await GetUserPhotoAsync(userids[0].ToString()) : "/static/image/site/moreperpeo.png",
|
||||
steptstatus = "待审核",
|
||||
actionstatus = "0",
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (zuihouyibu != null && zuihouyibu.actionstatus == 6)
|
||||
{
|
||||
foreach (var item in fixflow)
|
||||
{
|
||||
var isShok = fixshenhe.FirstOrDefault(a => a.stepid == item.SerID && a.SerID > zuihouyibu.SerID);
|
||||
if (isShok == null)
|
||||
{
|
||||
var userids = ToUserIds(fixflowGroup.Where(a => a.usergroup == item.SerID).Select(a => a.userid));
|
||||
frees.Add(new FreeShenheInfo
|
||||
{
|
||||
steptname = await ChaoSendNameAsync(string.Join(",", userids)),
|
||||
photo = userids.Length == 1 ? await GetUserPhotoAsync(userids[0].ToString()) : "/static/image/site/moreperpeo.png",
|
||||
steptstatus = "待审核",
|
||||
actionstatus = "0",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return frees;
|
||||
}
|
||||
|
||||
private async Task<List<FreeShenheInfo>> BuildOnlyTranctShenheFlowAsync(int uid, string ctime, int dataid, string appCode)
|
||||
{
|
||||
var shenhe = await _flowtranct.FindAsync(a => a.dataid == dataid && a.datatype == appCode);
|
||||
|
||||
var frees = new List<FreeShenheInfo>();
|
||||
frees.Add(new FreeShenheInfo
|
||||
{
|
||||
steptname = await GetUserNameAsync(uid.ToString()),
|
||||
steptstatus = "发起申请",
|
||||
actiondate = ctime,
|
||||
photo = await GetUserPhotoAsync(uid.ToString()),
|
||||
});
|
||||
|
||||
if (shenhe.Count(a => a.actionstatus > 2) > 0)
|
||||
{
|
||||
foreach (var item in shenhe)
|
||||
{
|
||||
frees.Add(new FreeShenheInfo
|
||||
{
|
||||
steptname = await ChaoSendNameAsync(item.actionuserid?.ToString()),
|
||||
photo = await GetUserPhotoAsync(item.actionuserid?.ToString()),
|
||||
steptstatus = ActionstatusName(item.actionstatus),
|
||||
actionstatus = item.actionstatus.ToString(),
|
||||
Remark = item.remark,
|
||||
actiondate = item.actiondate?.ToString("yyyy-MM-dd HH:mm") ?? "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return frees;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 辅助
|
||||
|
||||
private async Task<List<FreeFlowNode>> BuildFreeFlowNodesAsync(List<Common_freeflowinfo> nodes)
|
||||
{
|
||||
var result = new List<FreeFlowNode>();
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
var userIds = (await _freegroup.FindAsync(n => n.flowid == node.SerID))
|
||||
.Select(n => n.userid).Where(u => u.HasValue).Select(u => u.Value).ToList();
|
||||
var users = new List<FreeFlowUser>();
|
||||
if (userIds.Count > 0)
|
||||
{
|
||||
var teas = await _sch.TeaInfo.GetArrTeaInfoAsync(string.Join(",", userIds));
|
||||
users = teas.Select(t => new FreeFlowUser
|
||||
{
|
||||
userid = ParseTeaId(t.TeaSerId),
|
||||
name = t.Name,
|
||||
photo = t.Photo,
|
||||
}).ToList();
|
||||
}
|
||||
result.Add(new FreeFlowNode { user = users });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int ParseTeaId(string teaSerId)
|
||||
=> int.TryParse(teaSerId, out var id) ? id : 0;
|
||||
|
||||
private static int[] ToUserIds(IEnumerable<int?> source)
|
||||
=> source.Select(u => u ?? 0).ToArray();
|
||||
|
||||
private async Task<string> GetUserNameAsync(string teaId)
|
||||
{
|
||||
if (!int.TryParse(teaId, out var id)) return "";
|
||||
var tea = await _sch.TeaInfo.GetTeaInfoAsync(id);
|
||||
return tea?.Name ?? "";
|
||||
}
|
||||
|
||||
private async Task<string> GetUserPhotoAsync(string teaId)
|
||||
{
|
||||
if (!int.TryParse(teaId, out var id)) return "";
|
||||
var tea = await _sch.TeaInfo.GetTeaInfoAsync(id);
|
||||
return tea?.Photo ?? "";
|
||||
}
|
||||
|
||||
private async Task<string> ChaoSendNameAsync(string uids)
|
||||
{
|
||||
if (string.IsNullOrEmpty(uids) || uids == "undefined") return "";
|
||||
var ids = uids.Split(',')
|
||||
.Where(s => int.TryParse(s, out _)).Select(int.Parse).Distinct().ToArray();
|
||||
if (ids.Length == 0) return "";
|
||||
var teas = await _sch.TeaInfo.GetArrTeaInfoAsync(string.Join(",", ids));
|
||||
return string.Join(",", teas.Select(t => t.Name).Where(n => !string.IsNullOrEmpty(n)));
|
||||
}
|
||||
|
||||
private static string ActionstatusName(int actionstatus)
|
||||
{
|
||||
return actionstatus == -1 ? "备注"
|
||||
: actionstatus == 0 ? "发起"
|
||||
: actionstatus == 1 ? "已同意"
|
||||
: actionstatus == 2 ? "拒绝"
|
||||
: actionstatus == 3 ? "已撤销"
|
||||
: actionstatus == 4 ? "撤销发起"
|
||||
: actionstatus == 5 ? "通过(撤销被拒绝)"
|
||||
: actionstatus == 6 ? "发起修改申请"
|
||||
: actionstatus == 7 ? "已领取"
|
||||
: actionstatus == 8 ? "已维修"
|
||||
: actionstatus == 9 ? "已归还"
|
||||
: actionstatus == 10 ? "已领取"
|
||||
: actionstatus == 11 ? "印刷中"
|
||||
: "其他";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Vo;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.Service.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// 日志查询服务占位实现(框架期保证接口可运行)。
|
||||
/// TODO:接入 Repository 层后实现真实查询(按 schId + systemScope 过滤、keyword 模糊匹配、分页)。
|
||||
/// </summary>
|
||||
public class LogServiceStub : ILogService
|
||||
{
|
||||
public PageVO<SysOperationLog> Search(int schId, string keyword, bool systemScope, int page, int size)
|
||||
{
|
||||
return PageVO<SysOperationLog>.Empty(page, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Yh.Platform.Abstractions;
|
||||
using Yh.SchClass.OpenApi;
|
||||
using Yh.SchClass.OpenApi.Model;
|
||||
using YhCore.Common.UserContext;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Model.Vo;
|
||||
using YhCore.Repository;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.Service.Impl
|
||||
{
|
||||
/// <summary>
|
||||
/// 审核流程管理服务(移植旧 BestSchool.Service.Reviewprocess + UserFlowRole)。
|
||||
/// 教师查询改走教务 SDK(Yh.SchClass.OpenApi),缺失 groupId 的局版接口降级为当前学校。
|
||||
/// </summary>
|
||||
public class ReviewProcessService : IReviewProcessService
|
||||
{
|
||||
private readonly IRepository<Common_flowinfo> _flowinfo;
|
||||
private readonly IRepository<Common_flowstep> _flowstep;
|
||||
private readonly IRepository<Common_Groupuser> _groupuser;
|
||||
private readonly IRepository<Common_freeflowinfo> _freeflowinfo;
|
||||
private readonly IRepository<Common_freeGroupuser> _freegroup;
|
||||
private readonly IRepository<Common_SendUserinfo> _senduser;
|
||||
private readonly IRepository<Common_UpFiles> _upfiles;
|
||||
private readonly SchClassOpenApi _sch;
|
||||
|
||||
public ReviewProcessService(
|
||||
IRepository<Common_flowinfo> flowinfo,
|
||||
IRepository<Common_flowstep> flowstep,
|
||||
IRepository<Common_Groupuser> groupuser,
|
||||
IRepository<Common_freeflowinfo> freeflowinfo,
|
||||
IRepository<Common_freeGroupuser> freegroup,
|
||||
IRepository<Common_SendUserinfo> senduser,
|
||||
IRepository<Common_UpFiles> upfiles,
|
||||
SchClassOpenApi sch)
|
||||
{
|
||||
_flowinfo = flowinfo;
|
||||
_flowstep = flowstep;
|
||||
_groupuser = groupuser;
|
||||
_freeflowinfo = freeflowinfo;
|
||||
_freegroup = freegroup;
|
||||
_senduser = senduser;
|
||||
_upfiles = upfiles;
|
||||
_sch = sch;
|
||||
}
|
||||
|
||||
#region 流程与步骤
|
||||
|
||||
public Task<PageVO<FlowListItem>> List2Async(string name, int pageSize, int pageIndex, int flowType, bool edit, string schSerId)
|
||||
=> ListAsync(name, pageSize, pageIndex, flowType, edit, schSerId);
|
||||
|
||||
public async Task<PageVO<FlowListItem>> ListAsync(string name, int pageSize, int pageIndex, int flowType, bool edit, string schSerId)
|
||||
{
|
||||
var resultList = await _flowinfo.FindAsync(m => m.flowType == flowType && m.schSerID == schSerId);
|
||||
|
||||
if (pageSize == 100 && pageIndex == 0)
|
||||
{
|
||||
if (!edit)
|
||||
resultList = resultList.Where(a => a.enable == 1).ToList();
|
||||
|
||||
var stepFlowIds = (await _flowstep.FindAsync(s => true))
|
||||
.Select(s => s.flowid).Where(f => f.HasValue).Select(f => f.Value).Distinct().ToHashSet();
|
||||
resultList = resultList.Where(a => stepFlowIds.Contains(a.SerID)).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!edit)
|
||||
resultList = resultList.Where(a => a.enable != 3).ToList();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
resultList = resultList.Where(m => m.flowname != null && m.flowname.IndexOf(name) >= 0).ToList();
|
||||
}
|
||||
|
||||
var ordered = resultList.OrderBy(m => m.SerID).ToList();
|
||||
var items = ordered.Skip(pageSize * pageIndex).Take(pageSize)
|
||||
.Select(item => new FlowListItem
|
||||
{
|
||||
SerID = item.SerID.ToString(),
|
||||
flowname = item.flowname,
|
||||
deptid = item.deptid,
|
||||
typeid = item.typeid,
|
||||
enable = item.enable,
|
||||
enablename = item.enable == 1 ? "是" : "否",
|
||||
miaoshu = item.flowdes,
|
||||
deitdate = item.deitdate.ToString("yyyy-MM-dd"),
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return PageVO<FlowListItem>.Of(items, ordered.Count, pageIndex, pageSize);
|
||||
}
|
||||
|
||||
public async Task<PageVO<StepListItem>> StepListAsync(string name, int serid)
|
||||
{
|
||||
var steps = await _flowstep.FindAsync(a => a.flowid == serid);
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
steps = steps.Where(m => m.StepName != null && m.StepName.IndexOf(name) >= 0).ToList();
|
||||
}
|
||||
|
||||
var ordered = steps.OrderBy(m => m.SerID).ToList();
|
||||
var items = new List<StepListItem>();
|
||||
foreach (var item in ordered)
|
||||
{
|
||||
items.Add(new StepListItem
|
||||
{
|
||||
SerID = item.SerID.ToString(),
|
||||
StepName = item.StepName,
|
||||
flowid = item.flowid?.ToString(),
|
||||
prestep = item.prestep,
|
||||
TeaName = await GetTeaNamesByStepAsync(item.SerID),
|
||||
Teaid = await GetTeaIdsByStepAsync(item.SerID),
|
||||
istrue = steps.Any(a => a.prestep == item.SerID),
|
||||
});
|
||||
}
|
||||
|
||||
return PageVO<StepListItem>.Of(items, ordered.Count, 0, ordered.Count == 0 ? 20 : ordered.Count);
|
||||
}
|
||||
|
||||
public async Task CreateAsync(Common_flowinfo model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
|
||||
var flowname = model.flowname?.Trim();
|
||||
var dup = await _flowinfo.AnyAsync(a =>
|
||||
a.schSerID == model.schSerID && a.flowType == model.flowType &&
|
||||
a.flowname != null && a.flowname.Trim() == flowname && a.enable != 3);
|
||||
if (dup) throw new DException("流程名不能重名!");
|
||||
|
||||
var info = new Common_flowinfo
|
||||
{
|
||||
flowname = model.flowname,
|
||||
deptid = model.deptid,
|
||||
typeid = model.typeid,
|
||||
flowdes = model.flowdes,
|
||||
enable = model.enable,
|
||||
flowType = model.flowType,
|
||||
deitdate = DateTime.Now,
|
||||
schSerID = model.schSerID,
|
||||
};
|
||||
|
||||
await _flowinfo.AddAsync(info);
|
||||
await _flowinfo.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task EditAsync(Common_flowinfo model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
|
||||
var flowname = model.flowname?.Trim();
|
||||
var dup = (await _flowinfo.FindAsync(a =>
|
||||
a.schSerID == model.schSerID && a.flowType == DocPrintConstants.AppCode &&
|
||||
a.flowname != null && a.flowname.Trim() == flowname && a.enable != 3)).Count;
|
||||
if (dup > 1) throw new DException("流程名不能重名!");
|
||||
|
||||
var info = await _flowinfo.FirstOrDefaultAsync(a => a.SerID == model.SerID);
|
||||
if (info == null) throw new DException("未找到要修改的流程!");
|
||||
|
||||
info.flowname = model.flowname;
|
||||
info.deptid = model.deptid;
|
||||
info.typeid = model.typeid;
|
||||
info.flowdes = model.flowdes;
|
||||
info.enable = model.enable;
|
||||
|
||||
await _flowinfo.UpdateAsync(info);
|
||||
await _flowinfo.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task StepCreateAsync(Common_flowstep model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
|
||||
var info = new Common_flowstep
|
||||
{
|
||||
StepName = model.StepName,
|
||||
prestep = model.prestep,
|
||||
flowid = model.flowid,
|
||||
ordernum = 0,
|
||||
};
|
||||
await _flowstep.AddAsync(info);
|
||||
await _flowstep.SaveChangesAsync();
|
||||
|
||||
if (string.IsNullOrEmpty(model.SchSerID))
|
||||
{
|
||||
throw new DException("添加失败!");
|
||||
}
|
||||
|
||||
await AddGroupUsersAsync(info.SerID, model.flowid, model.SchSerID);
|
||||
}
|
||||
|
||||
public async Task StepEditAsync(Common_flowstep model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
|
||||
var info = await _flowstep.FirstOrDefaultAsync(a => a.SerID == model.SerID);
|
||||
if (info == null) throw new DException("未找到要修改的步骤!");
|
||||
|
||||
info.flowid = model.flowid;
|
||||
info.StepName = model.StepName;
|
||||
info.prestep = model.prestep;
|
||||
await _flowstep.UpdateAsync(info);
|
||||
|
||||
// 重写该步骤的人员绑定(先清后加)
|
||||
var existing = await _groupuser.FindAsync(a => a.usergroup == info.SerID);
|
||||
foreach (var itt in existing)
|
||||
{
|
||||
await _groupuser.RemoveAsync(itt);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(model.SchSerID))
|
||||
{
|
||||
await _groupuser.SaveChangesAsync();
|
||||
throw new DException("修改失败!");
|
||||
}
|
||||
|
||||
await AddGroupUsersAsync(info.SerID, info.flowid, model.SchSerID);
|
||||
}
|
||||
|
||||
private async Task AddGroupUsersAsync(int stepSerId, int? flowId, string userIds)
|
||||
{
|
||||
foreach (var idStr in userIds.Split(','))
|
||||
{
|
||||
if (!int.TryParse(idStr.Trim(), out var userId)) continue;
|
||||
await _groupuser.AddAsync(new Common_Groupuser
|
||||
{
|
||||
usergroup = stepSerId,
|
||||
flowid = flowId,
|
||||
userid = userId,
|
||||
IsOk = 0,
|
||||
});
|
||||
}
|
||||
await _groupuser.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string ids)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ids)) throw new DException("ids is not null");
|
||||
|
||||
var lids = ids.Split(',').Select(a => int.Parse(a)).ToArray();
|
||||
var flows = await _flowinfo.FindAsync(a => lids.Contains(a.SerID));
|
||||
foreach (var item in flows)
|
||||
{
|
||||
item.enable = 3;
|
||||
}
|
||||
await _flowinfo.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task StepDeleteAsync(decimal[] ids)
|
||||
{
|
||||
if (ids == null || ids.Length == 0) throw new DException("ids is not null");
|
||||
|
||||
var intIds = ids.Select(d => (int)d).ToArray();
|
||||
var objlist = await _flowstep.FindAsync(m => intIds.Contains(m.SerID));
|
||||
foreach (var item in objlist)
|
||||
{
|
||||
await _flowstep.RemoveAsync(item);
|
||||
}
|
||||
await _flowstep.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<List<BindStepItem>> BindstepListAsync(int serid)
|
||||
{
|
||||
var steps = await _flowstep.FindAsync(a => a.flowid == serid);
|
||||
return steps.OrderBy(a => a.prestep)
|
||||
.Select(a => new BindStepItem { SerID = a.SerID, StepName = a.StepName })
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<List<BindGroupuserItem>> BindGroupuserListAsync(int flowid, int prestep)
|
||||
{
|
||||
var steps = await _flowstep.FindAsync(a => a.flowid == flowid && a.prestep == prestep);
|
||||
var result = new List<BindGroupuserItem>();
|
||||
foreach (var step in steps)
|
||||
{
|
||||
var users = await _groupuser.FindAsync(b => b.usergroup == step.SerID);
|
||||
foreach (var u in users)
|
||||
{
|
||||
result.Add(new BindGroupuserItem
|
||||
{
|
||||
StepId = step.SerID,
|
||||
userid = u.userid,
|
||||
StepName = step.StepName,
|
||||
TeaName = await GetTeaNameAsync(u.userid?.ToString()),
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<BindFlowItem>> BindflowinfoAsync(string schSerId, int flowType)
|
||||
{
|
||||
var flows = await _flowinfo.FindAsync(a => a.flowType == flowType && a.schSerID == schSerId && a.enable == 1);
|
||||
var result = new List<BindFlowItem>();
|
||||
foreach (var item in flows.OrderBy(m => m.SerID))
|
||||
{
|
||||
var chaoSend = new List<ChaoSendItem>();
|
||||
if (!string.IsNullOrEmpty(item.ChaoSend))
|
||||
{
|
||||
foreach (var uid in item.ChaoSend.Split(','))
|
||||
{
|
||||
if (string.IsNullOrEmpty(uid)) continue;
|
||||
chaoSend.Add(new ChaoSendItem
|
||||
{
|
||||
uid = uid,
|
||||
name = await GetTeaNameAsync(uid),
|
||||
photo = await GetTeaPhotoAsync(uid),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
result.Add(new BindFlowItem
|
||||
{
|
||||
SerID = item.SerID.ToString(),
|
||||
flowname = item.flowname,
|
||||
ChaoSendArr = chaoSend,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 教师 / 人员
|
||||
|
||||
public async Task<List<TeaOption>> GetTeaListAsync(string schSerId)
|
||||
{
|
||||
var teas = await _sch.TeaInfo.GetSchTeaInfoAsync(schSerId, null);
|
||||
return teas.Select(a => new TeaOption { value = ParseTeaId(a.TeaSerId), label = a.Name }).ToList();
|
||||
}
|
||||
|
||||
public Task<List<TeaOption>> GetTeaListSupAsync(string schSerId)
|
||||
=> GetTeaListAsync(schSerId);
|
||||
|
||||
public async Task<PageVO<TeaInfoVo>> GetUserinfoAsync(string schSerId, string name, int statetype, int pageSize, int pageIndex)
|
||||
{
|
||||
var teas = await _sch.TeaInfo.GetSchTeaInfoAsync(schSerId, null);
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
teas = teas.Where(m => m.Name != null && m.Name.IndexOf(name) >= 0).ToList();
|
||||
}
|
||||
if (statetype > -1)
|
||||
{
|
||||
teas = teas.Where(m => m.IsLeadingMiddle == statetype).ToList();
|
||||
}
|
||||
|
||||
var ordered = teas.OrderBy(m => ParseTeaId(m.TeaSerId)).ToList();
|
||||
var items = ordered.Skip(pageSize * pageIndex).Take(pageSize).ToList();
|
||||
return PageVO<TeaInfoVo>.Of(items, ordered.Count, pageIndex, pageSize);
|
||||
}
|
||||
|
||||
public async Task<string[]> GetNoLeadingMiddleInfoAsync(string schSerId)
|
||||
{
|
||||
var teas = await _sch.TeaInfo.GetSchTeaInfoAsync(schSerId, null);
|
||||
return teas.Where(m => m.IsLeadingMiddle != 1)
|
||||
.Select(m => m.TeaSerId)
|
||||
.Where(id => !string.IsNullOrEmpty(id))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public Task<string[]> GetNoLeadingMiddleInfoSupAsync(string schSerId)
|
||||
=> GetNoLeadingMiddleInfoAsync(schSerId);
|
||||
|
||||
public Task ChangeLeadingMiddleStatusAsync(int state, string userTeaSerIdStr)
|
||||
{
|
||||
// 旧实现调用 OATea/EditIntheBearingStatus 写教师状态;教务 SDK 未提供该写接口,降级拒绝。
|
||||
throw new DException("该功能依赖教师状态写接口,教务 SDK 暂未提供,无法修改中层领导状态!");
|
||||
}
|
||||
|
||||
public async Task<List<FileListItem>> FilelistAsync(string ids)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ids)) return new List<FileListItem>();
|
||||
|
||||
var idStrings = ids.Split(',');
|
||||
var all = await _upfiles.FindAsync(a => true);
|
||||
return all.Where(a => idStrings.Contains(a.SerID.ToString()))
|
||||
.Select(a => new FileListItem
|
||||
{
|
||||
SerID = a.SerID,
|
||||
FileTrueName = string.IsNullOrEmpty(a.FileTrueName) ? a.FileTrueName : a.FileTrueName.Split('.')[0],
|
||||
FileName = a.FileName,
|
||||
FileSize = a.FileSize,
|
||||
FileType = a.FileType,
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 流程判断 / 用户
|
||||
|
||||
public async Task<ShenheFlowResult> IsShenheflowAsync(string schSerId, int userId)
|
||||
{
|
||||
var stepFlowIds = (await _flowstep.FindAsync(s => true))
|
||||
.Select(s => s.flowid).Where(f => f.HasValue).Select(f => f.Value).Distinct().ToHashSet();
|
||||
|
||||
var flowlist = await _flowinfo.FindAsync(a =>
|
||||
a.schSerID == schSerId && a.flowType == DocPrintConstants.AppCode && a.enable == 1);
|
||||
var hasFixed = flowlist.Any(a => stepFlowIds.Contains(a.SerID));
|
||||
|
||||
var result = new ShenheFlowResult { flowflag = hasFixed, freeflowlist = new List<FreeFlowItem>() };
|
||||
|
||||
if (!hasFixed)
|
||||
{
|
||||
var freeflowlist = await _freeflowinfo.FindAsync(a => a.userid == userId && a.flowType == DocPrintConstants.AppCode);
|
||||
foreach (var a in freeflowlist)
|
||||
{
|
||||
result.freeflowlist.Add(new FreeFlowItem
|
||||
{
|
||||
shenheuserid = a.shenheuserid,
|
||||
teamnamestr = await GetTeaNamesStrAsync(a.shenheuserid),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<List<SendUserItem>> IsSendUserAsync(string schSerId, int userId)
|
||||
{
|
||||
var result = new List<SendUserItem>();
|
||||
var senduserlist = await _senduser.FirstOrDefaultAsync(a => a.userid == userId && a.flowType == DocPrintConstants.AppCode);
|
||||
if (senduserlist == null || string.IsNullOrEmpty(senduserlist.Senduserid))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var userids = senduserlist.Senduserid.Split(',')
|
||||
.Where(a => !string.IsNullOrEmpty(a) && a != "undefined" && int.TryParse(a, out _))
|
||||
.Select(int.Parse).ToArray();
|
||||
|
||||
if (userids.Length == 0) return result;
|
||||
|
||||
var teas = await _sch.TeaInfo.GetArrTeaInfoAsync(string.Join(",", userids));
|
||||
return teas.Select(a => new SendUserItem
|
||||
{
|
||||
uid = ParseTeaId(a.TeaSerId),
|
||||
photo = a.Photo,
|
||||
name = a.Name,
|
||||
status = false,
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public Task<CurrentUserInfo> GetUserAsync()
|
||||
{
|
||||
var user = UserContextHolder.Current;
|
||||
return Task.FromResult(new CurrentUserInfo
|
||||
{
|
||||
Id = user?.Id ?? 0,
|
||||
UserName = user?.UserName,
|
||||
Account = user?.Account,
|
||||
SchoolId = user?.SchoolId,
|
||||
SchoolName = user?.SchoolName,
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<int> GetUserFlowRoleAsync(int userId)
|
||||
{
|
||||
var fixFlowIds = (await _flowinfo.FindAsync(f => f.enable == 1 && f.flowType == DocPrintConstants.AppCode))
|
||||
.Select(f => f.SerID).ToHashSet();
|
||||
var fixFlow = await _groupuser.AnyAsync(a => a.userid == userId && a.flowid.HasValue && fixFlowIds.Contains(a.flowid.Value));
|
||||
|
||||
var freeFlowIds = (await _freeflowinfo.FindAsync(f => f.flowType == DocPrintConstants.AppCode))
|
||||
.Select(f => f.SerID).ToHashSet();
|
||||
var freeFlow = await _freegroup.AnyAsync(a => a.userid == userId && a.flowid.HasValue && freeFlowIds.Contains(a.flowid.Value));
|
||||
|
||||
return (fixFlow || freeFlow) ? 1 : 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 教师名称/头像辅助
|
||||
|
||||
private static int ParseTeaId(string teaSerId)
|
||||
=> int.TryParse(teaSerId, out var id) ? id : 0;
|
||||
|
||||
private async Task<string> GetTeaNameAsync(string teaId)
|
||||
{
|
||||
if (!int.TryParse(teaId, out var id)) return "";
|
||||
var tea = await _sch.TeaInfo.GetTeaInfoAsync(id);
|
||||
return tea?.Name ?? "";
|
||||
}
|
||||
|
||||
private async Task<string> GetTeaPhotoAsync(string teaId)
|
||||
{
|
||||
if (!int.TryParse(teaId, out var id)) return "";
|
||||
var tea = await _sch.TeaInfo.GetTeaInfoAsync(id);
|
||||
return tea?.Photo ?? "";
|
||||
}
|
||||
|
||||
private async Task<string> GetTeaIdsByStepAsync(int stepSerId)
|
||||
{
|
||||
var ids = (await _groupuser.FindAsync(a => a.usergroup == stepSerId))
|
||||
.Select(a => a.userid).Where(u => u.HasValue).Select(u => u.Value).ToList();
|
||||
return string.Join(",", ids);
|
||||
}
|
||||
|
||||
private async Task<string> GetTeaNamesByStepAsync(int stepSerId)
|
||||
{
|
||||
var ids = (await _groupuser.FindAsync(a => a.usergroup == stepSerId))
|
||||
.Select(a => a.userid).Where(u => u.HasValue).Select(u => u.Value).ToList();
|
||||
return await GetTeaNamesByIdsAsync(ids);
|
||||
}
|
||||
|
||||
private async Task<string> GetTeaNamesByIdsAsync(IEnumerable<int> ids)
|
||||
{
|
||||
var list = ids.Distinct().ToList();
|
||||
if (list.Count == 0) return "";
|
||||
var teas = await _sch.TeaInfo.GetArrTeaInfoAsync(string.Join(",", list));
|
||||
return string.Join(",", teas.Select(t => t.Name).Where(n => !string.IsNullOrEmpty(n)));
|
||||
}
|
||||
|
||||
private async Task<string> GetTeaNamesStrAsync(string commaIds)
|
||||
{
|
||||
if (string.IsNullOrEmpty(commaIds)) return "";
|
||||
var ids = commaIds.Split(',')
|
||||
.Where(s => int.TryParse(s, out _)).Select(int.Parse).Distinct().ToList();
|
||||
if (ids.Count == 0) return "";
|
||||
return await GetTeaNamesByIdsAsync(ids);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
|
||||
namespace YhCore.Service.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 公共字典服务(对应旧 BestSchool.Service.GetSysDict)。
|
||||
/// </summary>
|
||||
public interface IDictionaryService
|
||||
{
|
||||
/// <summary>指定字典下拉框列表({k=CodeID, v=Name},Enable=1)。</summary>
|
||||
Task<List<DicOption>> GetDictListAsync(string typeId, string schId);
|
||||
|
||||
/// <summary>字典类别列表(排除 DEPT)。</summary>
|
||||
Task<List<DicOption>> GetDicTypeListAsync();
|
||||
|
||||
/// <summary>字典管理列表(含已删除,供超管管理)。</summary>
|
||||
Task<List<DicManageItem>> GetDictMangeListAsync(string typeId, string schId);
|
||||
|
||||
/// <summary>添加字典(typeID/typeName/Name/Schserid 由调用方填好,此处计算 Sort/Des/Enable/CodeID)。</summary>
|
||||
Task AddDicAsync(Common_Dictionary model);
|
||||
|
||||
/// <summary>更新字典(Name/Des/Sort/Enable)。</summary>
|
||||
Task UpdateDicAsync(Common_Dictionary model);
|
||||
|
||||
/// <summary>编辑字典名称(Name/Code/Path)。</summary>
|
||||
Task EditDicAsync(Common_Dictionary model);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
|
||||
namespace YhCore.Service.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 文印分类 / 印制版面 / 公共类别服务(对应旧 BestSchool.DocPrint.WY_list 分类部分 + WY_Classify)。
|
||||
/// </summary>
|
||||
public interface IDocPrintClassifyService
|
||||
{
|
||||
/// <summary>添加文印分类(typeName 固定「印制分类」)。</summary>
|
||||
Task AddDicAsync(Common_Dictionary model);
|
||||
|
||||
/// <summary>文印分类列表。</summary>
|
||||
Task<List<CategoryItem>> CategoryListAsync(string schSerId, string name);
|
||||
|
||||
/// <summary>添加印制版面(typeName 固定「印制版面」)。</summary>
|
||||
Task AddYZBMAsync(Common_Dictionary model);
|
||||
|
||||
/// <summary>印制版面列表。</summary>
|
||||
Task<List<CategoryItem>> YZBMListAsync(string schSerId, string name);
|
||||
|
||||
/// <summary>删除文印分类/版面(有使用记录则拒绝)。</summary>
|
||||
Task DeleteDicAsync(decimal[] ids);
|
||||
|
||||
/// <summary>公共类别列表(含排序/显隐)。</summary>
|
||||
Task<List<DictionaryTypeItem>> DictionaryTypeListAsync(string schSerId, string name, bool showAll, string typeId);
|
||||
|
||||
/// <summary>删除公共类别(软删除 Enable=3)。</summary>
|
||||
Task DeleteDictionaryTypeAsync(string[] ids);
|
||||
|
||||
/// <summary>公共类别显隐。</summary>
|
||||
Task SetDictionaryOrdAsync(string schSerId, int dicSerId, bool show);
|
||||
|
||||
/// <summary>公共类别排序。</summary>
|
||||
Task OrderDicAsync(string ids, string schSerId);
|
||||
|
||||
/// <summary>结果链接(依赖资源平台 ziyuan,新项目未接入,降级返回 false)。</summary>
|
||||
Task<object> GetResultUrlAsync(int dataId, int uid, int schId, string schSerId);
|
||||
|
||||
/// <summary>学校文印资源开关。</summary>
|
||||
Task<bool> GetSchZyBitAsync(string schSerId);
|
||||
|
||||
/// <summary>设置学校文印资源开关。</summary>
|
||||
Task SetSchZyBitAsync(string schSerId, bool zyBit);
|
||||
|
||||
/// <summary>待审数量角标。</summary>
|
||||
Task<int> GetShnheCountAsync(string userId, string schSerId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using YhCore.Model.Dto;
|
||||
|
||||
namespace YhCore.Service.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 文印统计看板服务(移植旧 BestSchool.DocPrint.DocPrintStatistics,5 接口)。
|
||||
/// 旧 [NoAuth] 无用户上下文,schId 由前端传入;新库 SchSerID 已是 int 字符串,直接 schId.ToString() 匹配。
|
||||
/// roleId 参数旧实现零引用,已删除。ExportRecord(Excel 导出,NPOI)延后。
|
||||
/// </summary>
|
||||
public interface IDocPrintStatisticsService
|
||||
{
|
||||
Task<List<DocPrintCountItem>> GetDocPrintCountAsync(int schId, DateTime? startDate = null, DateTime? endDate = null);
|
||||
Task<List<PieChartSeries>> GetPieChartAsync(int schId, DateTime? startDate = null, DateTime? endDate = null);
|
||||
Task<List<DocPrintTypeItem>> GetDocPrintTypeAsync(int schId);
|
||||
Task<DocPrintListResult> GetDocPrintListAsync(int schId, string docType, string likeName, int pageIndex, int pageSize, DateTime? startDate = null, DateTime? endDate = null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using YhCore.Model.Dto;
|
||||
|
||||
namespace YhCore.Service.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 公共文件服务(移植旧 BestSchool.Service.Common.GetEnclosure/FileSaveEnclosure +
|
||||
/// BestSchool.Common.UpLoadFile.FileSave/FileSaveImage/FileChunkSave/FileMergeSave)。
|
||||
/// 存储约定:物理根目录 = Config.FilePath(≈旧 ~/UploadFile/),相对路径仍存 /UploadFile/{AppType}/{yyyyMMdd}/{guid}{ext}
|
||||
/// (与旧库数据格式一致,兼容历史记录);host 取 Config.FileWebPath(≈旧 AuthConfig.weboa_url)。
|
||||
/// 旧 ToOfficeHelper.OfficeConvertToPDF(Office COM 转 PDF)新项目缺失,降级:上传不做转 PDF,
|
||||
/// 预览时本地无 .pdf 则降级 officeapps.live.com 在线预览。
|
||||
/// </summary>
|
||||
public interface IFileService
|
||||
{
|
||||
/// <summary>按附件 id 取文件信息(旧 GetEnclosure:Common_UpFiles → FileObj)。</summary>
|
||||
Task<FileObj> GetEnclosureAsync(int id);
|
||||
|
||||
/// <summary>存储附件信息(旧 FileSaveEnclosure:Common_UpFiles 落库,返回 SerID;文件名非法字符替换为 _)。</summary>
|
||||
Task<int> SaveEnclosureAsync(FileObj file, string appType);
|
||||
|
||||
/// <summary>单文件上传(旧 UpLoadFile.FileSave:扩展名白名单 + 保存 + 返回 FileObj)。</summary>
|
||||
Task<FileObj> SaveFileAsync(string appType, IFormFile file);
|
||||
|
||||
/// <summary>图片多文件上传(旧 UpLoadFile.FileSaveImage:jpg/gif/png/jpeg)。</summary>
|
||||
Task<List<FileObj>> SaveFileImageAsync(string appType, IFormFileCollection files);
|
||||
|
||||
/// <summary>保存分片(旧 FileChunkSave:临时目录 {guid}/ 下按索引存块)。</summary>
|
||||
Task SaveChunkAsync(string guid, string chunkIndex, IFormFile chunk);
|
||||
|
||||
/// <summary>合并分片(旧 FileMergeSave:按块合并 → 正式目录 + FileObj)。</summary>
|
||||
Task<FileObj> MergeSaveAsync(string appType, string guid, string fileName);
|
||||
|
||||
/// <summary>相对路径 → 物理路径(fileurl 以 /UploadFile/ 开头 → Config.FilePath + 子路径)。</summary>
|
||||
string MapPhysicalPath(string fileUrl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using YhCore.Model.Dto;
|
||||
|
||||
namespace YhCore.Service.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 自由审批流服务(移植旧 BestSchool.Service.ApproveFlow)。
|
||||
/// 教师查询走教务 SDK(Yh.SchClass.OpenApi)。
|
||||
/// </summary>
|
||||
public interface IFreeApproveService
|
||||
{
|
||||
/// <summary>获取自己的自由流程节点(dataid 为空时取最大 dataid)。</summary>
|
||||
Task<List<FreeFlowNode>> GetFleeFlowAsync(int userId, int? dataid);
|
||||
|
||||
/// <summary>保存自由流程审核人,返回首步骤 SerID。</summary>
|
||||
Task<int> EditFleeFlowAsync(int userId, string schSerId, int? dataid, string flowUids, bool isFlowNull);
|
||||
|
||||
/// <summary>添加审核意见(写 flowtranctInfo,actionstatus=-1)。</summary>
|
||||
Task<string> AddShenheRemarkAsync(int userId, int? dataid, string remark, int freestepid);
|
||||
|
||||
/// <summary>获取 freestepid 之后的自由流程节点。</summary>
|
||||
Task<List<FreeFlowNode>> GetAfterFleeFlowAsync(int? dataid, int freestepid);
|
||||
|
||||
/// <summary>编辑 freestepid 之后的自由流程,返回末步骤 SerID。</summary>
|
||||
Task<int> EditAfterFleeFlowAsync(int? dataid, int freestepid, string flowUids);
|
||||
|
||||
/// <summary>查看发起人 + 审核人步骤(自由 / 固定 / 仅记录)。</summary>
|
||||
Task<List<FreeShenheInfo>> ShenheFlowAsync(int uid, string ctime, int dataid, int? freestepid, int? flowid);
|
||||
|
||||
/// <summary>抄送人列表(按 uids 查用户)。</summary>
|
||||
Task<List<ChaoSendUser>> ChaoSendListAsync(string uids);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Vo;
|
||||
|
||||
namespace YhCore.Service.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 日志查询服务(对应 Java 版 ILogService)。
|
||||
/// </summary>
|
||||
public interface ILogService
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页搜索系统审计日志。
|
||||
/// </summary>
|
||||
/// <param name="schId">学校 ID</param>
|
||||
/// <param name="keyword">关键字(可为空)</param>
|
||||
/// <param name="systemScope">true=系统操作日志(systemSearch);false=普通操作日志(search)</param>
|
||||
/// <param name="page">页码,从 0 开始</param>
|
||||
/// <param name="size">每页大小</param>
|
||||
PageVO<SysOperationLog> Search(int schId, string keyword, bool systemScope, int page, int size);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Yh.SchClass.OpenApi.Model;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Model.Vo;
|
||||
|
||||
namespace YhCore.Service.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 审核流程管理服务(对应旧 BestSchool.Service.Reviewprocess + UserFlowRole)。
|
||||
/// </summary>
|
||||
public interface IReviewProcessService
|
||||
{
|
||||
/// <summary>审核流程列表(分页)。</summary>
|
||||
Task<PageVO<FlowListItem>> ListAsync(string name, int pageSize, int pageIndex, int flowType, bool edit, string schSerId);
|
||||
|
||||
/// <summary>审核流程列表【局版】(新框架无 groupId,降级为与 List 同源)。</summary>
|
||||
Task<PageVO<FlowListItem>> List2Async(string name, int pageSize, int pageIndex, int flowType, bool edit, string schSerId);
|
||||
|
||||
/// <summary>步骤流程列表。</summary>
|
||||
Task<PageVO<StepListItem>> StepListAsync(string name, int serid);
|
||||
|
||||
/// <summary>添加审核流程。</summary>
|
||||
Task CreateAsync(Common_flowinfo model);
|
||||
|
||||
/// <summary>修改审核流程。</summary>
|
||||
Task EditAsync(Common_flowinfo model);
|
||||
|
||||
/// <summary>添加步骤流程。</summary>
|
||||
Task StepCreateAsync(Common_flowstep model);
|
||||
|
||||
/// <summary>修改步骤流程。</summary>
|
||||
Task StepEditAsync(Common_flowstep model);
|
||||
|
||||
/// <summary>删除审核流程(软删除 enable=3)。</summary>
|
||||
Task DeleteAsync(string ids);
|
||||
|
||||
/// <summary>删除步骤。</summary>
|
||||
Task StepDeleteAsync(decimal[] ids);
|
||||
|
||||
/// <summary>流程绑定的步骤列表。</summary>
|
||||
Task<List<BindStepItem>> BindstepListAsync(int serid);
|
||||
|
||||
/// <summary>绑定步骤的人员。</summary>
|
||||
Task<List<BindGroupuserItem>> BindGroupuserListAsync(int flowid, int prestep);
|
||||
|
||||
/// <summary>绑定流程(流程 + 抄送人)。</summary>
|
||||
Task<List<BindFlowItem>> BindflowinfoAsync(string schSerId, int flowType);
|
||||
|
||||
/// <summary>当前学校教师列表。</summary>
|
||||
Task<List<TeaOption>> GetTeaListAsync(string schSerId);
|
||||
|
||||
/// <summary>学校 + 教育局教师列表(降级为当前学校)。</summary>
|
||||
Task<List<TeaOption>> GetTeaListSupAsync(string schSerId);
|
||||
|
||||
/// <summary>教师信息分页。</summary>
|
||||
Task<PageVO<TeaInfoVo>> GetUserinfoAsync(string schSerId, string name, int statetype, int pageSize, int pageIndex);
|
||||
|
||||
/// <summary>非中层领导人员(teaId 数组)。</summary>
|
||||
Task<string[]> GetNoLeadingMiddleInfoAsync(string schSerId);
|
||||
|
||||
/// <summary>非中层领导人员【局版】(降级为当前学校)。</summary>
|
||||
Task<string[]> GetNoLeadingMiddleInfoSupAsync(string schSerId);
|
||||
|
||||
/// <summary>设置/取消中层领导(教务 SDK 未提供写接口,暂不支持)。</summary>
|
||||
Task ChangeLeadingMiddleStatusAsync(int state, string userTeaSerIdStr);
|
||||
|
||||
/// <summary>附件数据源。</summary>
|
||||
Task<List<FileListItem>> FilelistAsync(string ids);
|
||||
|
||||
/// <summary>该校审核流程是固定还是自由。</summary>
|
||||
Task<ShenheFlowResult> IsShenheflowAsync(string schSerId, int userId);
|
||||
|
||||
/// <summary>绑定抄送默认人。</summary>
|
||||
Task<List<SendUserItem>> IsSendUserAsync(string schSerId, int userId);
|
||||
|
||||
/// <summary>当前用户基础信息。</summary>
|
||||
Task<CurrentUserInfo> GetUserAsync();
|
||||
|
||||
/// <summary>用户流程角色(1 有固定/自由流程,0 无)。</summary>
|
||||
Task<int> GetUserFlowRoleAsync(int userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using YhCore.Model.Dto;
|
||||
|
||||
namespace YhCore.Service.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 文印核心业务服务(移植旧 BestSchool.DocPrint.WY_list 除分类/字典外的部分:
|
||||
/// 抄送、附件、文印 CRUD、各类列表、审核动作、统计、指派)。
|
||||
/// </summary>
|
||||
public interface IWYDocPrintService
|
||||
{
|
||||
// ---- 抄送 ----
|
||||
Task<List<DocPrintListItem>> SendRdListAsync(string name, string startDate, string endDate, string userId, string schSerId, int pageSize, int pageIndex);
|
||||
Task<List<SendDetailItem>> SendDetailsListAsync(int serId);
|
||||
Task<string> EditSendAsync(int serId, string senduserid, string mysenduserid, string schSerId, string uid);
|
||||
|
||||
// ---- 附件 ----
|
||||
Task<List<DocPrintFileItem>> FileListAsync(string ids);
|
||||
|
||||
// ---- 文印 CRUD ----
|
||||
Task<int> CreateAsync(WYDocPrintSaveDto model);
|
||||
Task<int> EditAsync(WYDocPrintSaveDto model);
|
||||
Task DeleteAsync(string ids);
|
||||
Task<string> RevokeAsync(FlowTranctDto model);
|
||||
|
||||
// ---- 列表 ----
|
||||
Task<List<DocPrintListItem>> MyListAsync(string name, string status, string startDate, string endDate, string userId, string schSerId, int pageSize, int pageIndex);
|
||||
Task<List<DocPrintListItem>> AllListAsync(string name, string userId, string schSerId, int pageSize, int pageIndex);
|
||||
Task<List<DocPrintListItem>> ShenheListAsync(string userId, string schSerId, int pageSize, int pageIndex);
|
||||
Task<List<DocPrintListItem>> SPListAsync(string name, string status, string startDate, string endDate, string userId, string schSerId, int pageSize, int pageIndex);
|
||||
Task<List<DocPrintListItem>> YishenListAsync(string name, string userId, string schSerId, int pageSize, int pageIndex);
|
||||
Task<List<DocPrintListItem>> TotalListAsync(string name, string stateid, string starttime, string endtime, string schSerId, int pageSize, int pageIndex);
|
||||
Task<List<DocPrintDetailItem>> MyListtoOneAsync(int serId, string schSerId);
|
||||
Task<List<BindFlowOption>> BindflowinfoAsync(string schSerId, int flowType);
|
||||
|
||||
// ---- 审核动作 ----
|
||||
Task<string> ShenheOkAsync(FlowTranctDto model);
|
||||
Task<string> AffirmApplyforOkAsync(FlowTranctDto model);
|
||||
|
||||
// ---- 指派 ----
|
||||
Task<string> GetUserZhipaiAsync(int userid, string schSerId, int? flowid, int? flowstepid, string type);
|
||||
|
||||
// ---- 统计 ----
|
||||
Task<List<MonthStatItem>> List1Async(string starttime, string endtime, string schSerId);
|
||||
Task<List<DocDetailStatItem>> List2Async(string starttime, string endtime, string schSerId, int pageSize, int pageIndex);
|
||||
Task<List<UserRankItem>> List3Async(string starttime, string endtime, string schSerId, int pageSize, int pageIndex);
|
||||
Task<List<StatListItem>> List4Async(int year, int month, string schSerId, int pageSize, int pageIndex);
|
||||
Task<List<StatListItem>> List5Async(string userid, string starttime, string endtime, string schSerId, int pageSize, int pageIndex);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Model.Vo;
|
||||
|
||||
namespace YhCore.Service.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 审核角色业务服务(移植旧 BestSchool.DocPrint.WY_SHRoleP,45 接口)。
|
||||
/// 降级说明:
|
||||
/// 1. 旧 SendMessage/SendUnread/Backlog(短信、已读未读、待办中心)新项目缺失,全部降级为纯落库;
|
||||
/// 2. 教师/头像改走教务 SDK(Yh.SchClass.OpenApi),教师标识用 int(TeaSerId 解析),
|
||||
/// 落库 WY_SHRole_SHTea.SHTeaID / WY_SHRole_BSHTea.BSHTeaID 仍为 decimal 用户 id;
|
||||
/// 3. 签名存文件系统(DES 加密),非数据库表;
|
||||
/// 4. 旧 ServiceResult 的 Data/OtherData/Page 三段合并为单个 Result 对象(getshenheFlow / getStepInfo / getStepByJE / getdocOftable)。
|
||||
/// 延后:Excel 导入导出(UpfileExcelSHRole/downMBOfSHRole/downUpfileSHRoleFK/ExceltotalListSHRole/WYRDExceldaochu)、审批单 PDF(DwonWYSQD)。
|
||||
/// </summary>
|
||||
public interface IWYSHRoleService
|
||||
{
|
||||
// ---- 审核角色管理 ----
|
||||
Task<string> GetIsSHRoleAsync(string schSerId);
|
||||
Task<string> SaveIsSHRoleAsync(string schSerId, string val);
|
||||
Task<List<WYSHRoleExt>> GetSHRoleAsync(string schSerId);
|
||||
Task<string> UpdateSHRoleNameAsync(string schSerId, string userId, decimal serId, string roleName);
|
||||
Task<string> EditSHRoleAsync(string schSerId, string userId, WYSHRoleExt roleInfo);
|
||||
Task<string> EditSHTeaRowAsync(string schSerId, string userId, WY_SHRole_SHTea data);
|
||||
Task<string> DelSHTeaRowAsync(string schSerId, decimal serId);
|
||||
Task<string> EditSHTeaAsync(string schSerId, string userId, decimal shRoleId, List<WY_SHRole_SHTea> listSHTea);
|
||||
Task<string> EditBSHTeaAsync(string schSerId, string userId, decimal shRoleId, List<WY_SHRole_BSHTea> listBSHTea);
|
||||
Task<string> DelSHRoleAsync(string schSerId, List<decimal?> listSHRoleId);
|
||||
|
||||
// ---- 审核角色流程 ----
|
||||
Task<PageVO<SHRoleFlowListItem>> ListFlowAsync(string name, int pageSize, int pageIndex, string schSerId, int flowType, bool edit);
|
||||
Task<string> EditFlowAsync(Common_flowinfo model);
|
||||
Task<string> StepCreateOfSHRoleAsync(Common_flowstep model);
|
||||
Task<SHRoleStepInfo> GetStepInfoAsync(string schSerId, decimal flowid);
|
||||
Task<string> StepDeleteAsync(List<int> ids);
|
||||
Task<string> UpdateStepNameAsync(string schSerId, int serId, int flowid, string stepName, int cond, string isshowJE, string isshowBtn);
|
||||
Task<string> AddStepSHRoleAsync(string schSerId, int flowid, int stepid, decimal shRoleid);
|
||||
Task<string> DelStepSHRoleAsync(string schSerId, int stepid, decimal shRoleid);
|
||||
Task<SHRoleStepByJEResult> GetStepByJEAsync(string schSerId, string userId, int flowid, int je);
|
||||
|
||||
// ---- 文印申请页面 ----
|
||||
Task<List<WYFlowForUser>> GetLCOfUserAsync(string schSerId, string userId, string userName);
|
||||
Task<List<SendUserItem>> IsSendUserSHRoleAsync(string schSerId, int userId, int flowid);
|
||||
Task<List<Common_Dictionary>> GetDicInfoAsync(string schSerId, string typeId);
|
||||
Task<int> EditDocPrintAsync(string schSerId, string userId, WYSHRoleEditDocPrintDto model);
|
||||
|
||||
// ---- 用户记录查看和审核 ----
|
||||
Task<SHRoleShenheFlowResult> GetshenheFlowAsync(string schSerId, decimal docprintId, int flowid);
|
||||
Task<List<SHRoleDocPrintListItem>> MyListtoOneSHRoleAsync(int serId, string schSerId);
|
||||
Task<List<SHRoleDocPrintListItem>> MyListSHRoleAsync(string name, string status, string startDate, string endDate, int pageSize, int pageIndex, string userId, string schSerId);
|
||||
Task<List<SHRoleDocPrintListItem>> TotalListSHRoleAsync(int flowid, int dwid, string name, string stateid, string starttime, string endtime, int pageSize, int pageIndex, string schSerId);
|
||||
Task<List<SHRoleDocPrintListItem>> SendRdListSHRoleAsync(int flowid, int dwid, string name, string status, string startDate, string endDate, string userId, string schSerId, int pageSize, int pageIndex);
|
||||
Task<List<SHRoleDocPrintListItem>> SPListSHRoleAsync(int flowid, int dwid, string name, string status, string startDate, string endDate, string userId, string userName, string schSerId, int pageSize, int pageIndex);
|
||||
Task<string> ShenheOkSHRoleAsync(FlowTranctDto model);
|
||||
|
||||
// ---- 规格表 / 签名 / 退回 / 金额 ----
|
||||
Task<SHRoleDocOfTableResult> GetDocOftableAsync(string schSerId, decimal docPrintId);
|
||||
Task<string> SaveSignatureAsync(string schSerId, string userId, string pic);
|
||||
Task<string> GetSignatureAsync(string schSerId, string userId);
|
||||
Task<string> DelSignatureAsync(string schSerId, string userId);
|
||||
Task<string> TuiHuiAsync(string userId, string cont, decimal docId);
|
||||
Task<string> UnTuiHuiAsync(string userId, decimal docId);
|
||||
Task<string> RegetJEAsync(string schSerId, string userId, List<decimal> listDocPrintId, string type);
|
||||
Task<List<SHRoleRegetJERdItem>> GetRegetJERdAsync(string schSerId, int pageSize, int pageIndex);
|
||||
Task<int> SetDownFileTimesAsync(int serId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<RootNamespace>YhCore.Service</RootNamespace>
|
||||
<AssemblyName>YhCore.Service</AssemblyName>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- 文件上传:IFileService 使用 IFormFile / IFormFileCollection(ASP.NET Core 共享框架) -->
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\YhCore.Entity\YhCore.Entity.csproj" />
|
||||
<ProjectReference Include="..\YhCore.Model\YhCore.Model.csproj" />
|
||||
<ProjectReference Include="..\YhCore.Repository\YhCore.Repository.csproj" />
|
||||
<ProjectReference Include="..\YhCore.Common\YhCore.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- 业务异常 DException / ErrorCode / IErrorCode -->
|
||||
<Reference Include="Yh.Platform.Abstractions">
|
||||
<HintPath>..\..\..\Library\Yh.Platform.Abstractions.dll</HintPath>
|
||||
</Reference>
|
||||
<!-- 教务(SchClass)开放接口 SDK:教师/学校查询(C1 reviewprocess 教师类接口) -->
|
||||
<Reference Include="Yh.SchClass.OpenApi">
|
||||
<HintPath>..\..\..\Library\Yh.SchClass.OpenApi.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Quartz;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Repository;
|
||||
|
||||
namespace YhCore.Tasks.Jobs
|
||||
{
|
||||
/// <summary>
|
||||
/// 审计日志清理 Job(对应 Java scheduler/SysOperationLogCleanupTask)。
|
||||
/// 定时删除 30 天前的系统审计日志(sys_operation_log 表)。
|
||||
/// </summary>
|
||||
public class CleanupLogJob : IJob
|
||||
{
|
||||
private const int BatchSize = 1000;
|
||||
|
||||
public async Task Execute(IJobExecutionContext context)
|
||||
{
|
||||
var services = (IServiceProvider)context.Scheduler.Context.Get("services");
|
||||
using var scope = services.CreateScope();
|
||||
var provider = scope.ServiceProvider;
|
||||
|
||||
var logger = provider.GetRequiredService<ILogger<CleanupLogJob>>();
|
||||
var repo = provider.GetRequiredService<IRepository<SysOperationLog>>();
|
||||
|
||||
var cutoff = DateTime.Now.AddDays(-30);
|
||||
logger.LogInformation("审计日志清理开始:删除 {Cutoff:yyyy-MM-dd HH:mm:ss} 之前的数据", cutoff);
|
||||
|
||||
try
|
||||
{
|
||||
var oldList = await repo.FindAsync(l => l.CreateTime < cutoff);
|
||||
if (oldList.Count == 0)
|
||||
{
|
||||
logger.LogInformation("审计日志清理完成:无 30 天前的数据");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation("审计日志清理:待删除 {Count} 条,分块提交(每批 {Batch} 条)", oldList.Count, BatchSize);
|
||||
|
||||
var removed = 0;
|
||||
foreach (var batch in Chunk(oldList, BatchSize))
|
||||
{
|
||||
foreach (var item in batch)
|
||||
{
|
||||
await repo.RemoveAsync(item);
|
||||
}
|
||||
removed += await repo.SaveChangesAsync();
|
||||
}
|
||||
|
||||
logger.LogInformation("审计日志清理完成:共删除 {Count} 条", removed);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 缺表/数据库异常时记录错误日志,不让 Job 崩溃(Quartz 连续失败会标记 Job 错误状态)
|
||||
logger.LogError(ex, "审计日志清理失败:{Message}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>将列表分块(net5.0 无 Enumerable.Chunk,自行实现)</summary>
|
||||
private static IEnumerable<List<T>> Chunk<T>(List<T> source, int size)
|
||||
{
|
||||
for (var i = 0; i < source.Count; i += size)
|
||||
{
|
||||
yield return source.GetRange(i, Math.Min(size, source.Count - i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Quartz;
|
||||
using YhCore.Tasks.Jobs;
|
||||
|
||||
namespace YhCore.Tasks
|
||||
{
|
||||
/// <summary>
|
||||
/// Quartz 调度宿主(对应 Java scheduler 包由 Spring 管理的调度器)。
|
||||
/// 承载框架内置 Job 模板;业务 Job 在 InitJobs 中追加注册。
|
||||
/// </summary>
|
||||
public class QuartzHostedService : IHostedService
|
||||
{
|
||||
private const string ServiceProviderKey = "services";
|
||||
|
||||
private readonly ISchedulerFactory _schedulerFactory;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private IScheduler _scheduler;
|
||||
|
||||
public QuartzHostedService(ISchedulerFactory schedulerFactory, IServiceProvider serviceProvider)
|
||||
{
|
||||
_schedulerFactory = schedulerFactory;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_scheduler = await _schedulerFactory.GetScheduler(cancellationToken);
|
||||
_scheduler.Context.Put(ServiceProviderKey, _serviceProvider); // Job 经此取 DI 服务
|
||||
|
||||
await InitJobs();
|
||||
|
||||
await _scheduler.Start(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_scheduler != null)
|
||||
{
|
||||
await _scheduler.Shutdown(waitForJobsToComplete: true, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InitJobs()
|
||||
{
|
||||
// ============ 定时任务注册 ============
|
||||
|
||||
// 1. 审计日志清理(对应 Java SysOperationLogCleanupTask:每日凌晨 3:30 删除 30 天前 sys_operation_log 数据)
|
||||
await ScheduleJobAsync<CleanupLogJob>("cleanup-log-job", "0 30 3 * * ?");
|
||||
|
||||
// 2. 其他业务 Job 在此追加注册(示例:每 10 分钟执行一次)
|
||||
// await ScheduleJobAsync<TJob>("job-name", "0 */10 * * * ?");
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>注册一个 Cron 定时任务</summary>
|
||||
protected async Task ScheduleJobAsync<TJob>(string jobName, string cronExpression) where TJob : IJob
|
||||
{
|
||||
var job = JobBuilder.Create<TJob>()
|
||||
.WithIdentity(jobName)
|
||||
.Build();
|
||||
|
||||
var trigger = TriggerBuilder.Create()
|
||||
.WithIdentity(jobName + "-trigger")
|
||||
.WithCronSchedule(cronExpression)
|
||||
.Build();
|
||||
|
||||
await _scheduler.ScheduleJob(job, trigger);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Quartz;
|
||||
using Quartz.Impl;
|
||||
|
||||
namespace YhCore.Tasks
|
||||
{
|
||||
/// <summary>
|
||||
/// 任务调度注册扩展:Quartz 调度器工厂 + 宿主服务(对应 Java scheduler 自动装配)。
|
||||
/// </summary>
|
||||
public static class QuartzServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddQuartzHosted(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<ISchedulerFactory, StdSchedulerFactory>();
|
||||
services.AddHostedService<QuartzHostedService>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<RootNamespace>YhCore.Task</RootNamespace>
|
||||
<AssemblyName>YhCore.Task</AssemblyName>
|
||||
<LangVersion>9.0</LangVersion>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- 任务调度:对应 Java Quartz / scheduler 包 -->
|
||||
<PackageReference Include="Quartz" Version="3.3.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\YhCore.Common\YhCore.Common.csproj" />
|
||||
<ProjectReference Include="..\YhCore.Entity\YhCore.Entity.csproj" />
|
||||
<ProjectReference Include="..\YhCore.Repository\YhCore.Repository.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,51 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using YhCore.Common;
|
||||
using YhCore.Common.UserContext;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Vo;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.WebApi.Controllers.Api
|
||||
{
|
||||
/// <summary>
|
||||
/// 日志查询接口(对应 Java 版 controller/api/LogController,路由 /api/log/)。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route(ApiRoutes.ApiPath + "/log")]
|
||||
public class LogController : ControllerBase
|
||||
{
|
||||
private readonly ILogService _logService;
|
||||
|
||||
public LogController(ILogService logService)
|
||||
{
|
||||
_logService = logService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 系统操作日志搜索(对应 Java GET /api/log/systemSearch)。
|
||||
/// </summary>
|
||||
/// <param name="keyword">关键字(可空)</param>
|
||||
/// <param name="page">页码,从 0 开始(对应 Spring Pageable.page)</param>
|
||||
/// <param name="size">每页大小(对应 Spring Pageable.size)</param>
|
||||
[HttpGet("systemSearch")]
|
||||
public PageVO<SysOperationLog> SystemSearch(
|
||||
[FromQuery] string keyword,
|
||||
[FromQuery] int page = 0,
|
||||
[FromQuery] int size = 20)
|
||||
{
|
||||
return _logService.Search(UserContextHolder.GetSchId(), keyword, true, page, size);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 操作日志搜索(对应 Java GET /api/log/search)。
|
||||
/// </summary>
|
||||
[HttpGet("search")]
|
||||
public PageVO<SysOperationLog> Search(
|
||||
[FromQuery] string keyword,
|
||||
[FromQuery] int page = 0,
|
||||
[FromQuery] int size = 20)
|
||||
{
|
||||
return _logService.Search(UserContextHolder.GetSchId(), keyword, false, page, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Yh.SchClass.OpenApi;
|
||||
using Yh.SchClass.OpenApi.Model;
|
||||
using YhCore.Common;
|
||||
|
||||
namespace YhCore.WebApi.Controllers.Api
|
||||
{
|
||||
/// <summary>
|
||||
/// 教务(SchClass)开放接口演示控制器:引用 Yh.SchClass.OpenApi.dll(Library),
|
||||
/// 对应 Java 教务 bschclassmanage 的 /oapi 端点(配置节 yh:schclass:open)。
|
||||
/// <para>SDK 门面 <see cref="SchClassOpenApi"/> 聚合 10 个客户端,本控制器演示其中 5 个。</para>
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route(ApiRoutes.ApiPath + "/com/schClass")]
|
||||
public class SchClassController : ControllerBase
|
||||
{
|
||||
private readonly SchClassOpenApi _sch;
|
||||
|
||||
public SchClassController(SchClassOpenApi sch)
|
||||
{
|
||||
_sch = sch ?? throw new ArgumentNullException(nameof(sch));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 全部学校列表(GET /oapi/SchInfo/AllList)。
|
||||
/// </summary>
|
||||
[HttpGet("schools")]
|
||||
public async Task<List<SchInfoVo>> Schools()
|
||||
{
|
||||
return await _sch.SchInfo.AllListAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按学校外部序列号查询学校(GET /oapi/SchInfo/GetSchFirst)。
|
||||
/// </summary>
|
||||
/// <param name="schId">学校外部序列号</param>
|
||||
[HttpGet("school")]
|
||||
public async Task<SchInfoVo> School([FromQuery] string schId)
|
||||
{
|
||||
return await _sch.SchInfo.GetSchFirstAsync(schId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按学校查询教师列表(GET /oapi/OATea/GetSchTeainfo)。
|
||||
/// </summary>
|
||||
/// <param name="schSerId">学校外部序列号</param>
|
||||
/// <param name="schId">学校内部 ID(优先使用)</param>
|
||||
[HttpGet("teachers")]
|
||||
public async Task<List<TeaInfoVo>> Teachers(
|
||||
[FromQuery] string schSerId,
|
||||
[FromQuery] int? schId)
|
||||
{
|
||||
return await _sch.TeaInfo.GetSchTeaInfoAsync(schSerId, schId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按学校查询班级列表(GET /oapi/VSchClass/GetVClassList)。
|
||||
/// </summary>
|
||||
/// <param name="schSerId">学校外部序列号</param>
|
||||
[HttpGet("classes")]
|
||||
public async Task<List<Dictionary<string, object>>> Classes([FromQuery] string schSerId)
|
||||
{
|
||||
var rows = await _sch.VClass.GetVClassListAsync(schSerId);
|
||||
return rows.Select(ToPlainRow).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按学校查询学生列表(GET /oapi/OAStu/GetSchStuList)。
|
||||
/// </summary>
|
||||
/// <param name="schserid">学校外部序列号</param>
|
||||
/// <param name="schgroupid">学校分组 ID(可空)</param>
|
||||
[HttpGet("students")]
|
||||
public async Task<List<Dictionary<string, object>>> Students(
|
||||
[FromQuery] string schserid,
|
||||
[FromQuery] string schgroupid)
|
||||
{
|
||||
var rows = await _sch.StuInfo.GetSchStuListAsync(schserid, schgroupid);
|
||||
return rows.Select(ToPlainRow).ToList();
|
||||
}
|
||||
|
||||
// ===================== JsonElement → 普通对象 =====================
|
||||
// SDK 行类型 SchClassRow = Dictionary<string, JsonElement>(Java Map<String,Object> 兼容);
|
||||
// 直接返回会被 Newtonsoft 序列化成 JsonElement 内部结构,这里转成原生对象。
|
||||
|
||||
private static Dictionary<string, object> ToPlainRow(Dictionary<string, JsonElement> row)
|
||||
=> row.ToDictionary(kv => kv.Key, kv => ToPlain(kv.Value));
|
||||
|
||||
private static object ToPlain(JsonElement el)
|
||||
{
|
||||
switch (el.ValueKind)
|
||||
{
|
||||
case JsonValueKind.String:
|
||||
return el.GetString();
|
||||
case JsonValueKind.Number:
|
||||
return el.TryGetInt32(out var i) ? (object)i : el.GetDouble();
|
||||
case JsonValueKind.True:
|
||||
return true;
|
||||
case JsonValueKind.False:
|
||||
return false;
|
||||
case JsonValueKind.Null:
|
||||
return null;
|
||||
case JsonValueKind.Array:
|
||||
return el.EnumerateArray().Select(ToPlain).ToList();
|
||||
case JsonValueKind.Object:
|
||||
return el.EnumerateObject().ToDictionary(p => p.Name, p => ToPlain(p.Value));
|
||||
default:
|
||||
return el.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Yh.Platform.Abstractions;
|
||||
using Yh.Platform.Auth;
|
||||
using Yh.Platform.OpenApi;
|
||||
using YhCore.Common;
|
||||
|
||||
namespace YhCore.WebApi.Controllers.Api
|
||||
{
|
||||
/// <summary>
|
||||
/// 用户基础接口(对应 Java 版 controller/api/UserController,路由 /api/base/)。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route(ApiRoutes.ApiPath + "/base")]
|
||||
public class UserController : ControllerBase
|
||||
{
|
||||
private readonly IOpenApiUserClient _openApiUserClient;
|
||||
|
||||
public UserController(IOpenApiUserClient openApiUserClient)
|
||||
{
|
||||
_openApiUserClient = openApiUserClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户可见的应用模块(对应 Java GET /api/base/getUserMod,
|
||||
/// 内部调用 YschBaseOpenApi.getUserModule)。
|
||||
/// </summary>
|
||||
[HttpGet("getUserMod")]
|
||||
public async Task<List<BoaModule>> GetUserMod()
|
||||
{
|
||||
var boa = BoaUserContext.Get();
|
||||
return await _openApiUserClient.GetUserModuleAsync(boa?.Id ?? 0, boa?.School?.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前登录用户信息(对应 Java GET /api/base/getUserInfo,
|
||||
/// Java 版直接返回认证切面注入的 authUserWithDept)。
|
||||
/// </summary>
|
||||
[HttpGet("getUserInfo")]
|
||||
public BoaUser GetUserInfo()
|
||||
{
|
||||
return BoaUserContext.Get();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 登出(对应 Java GET /api/base/logout:session.invalidate())。
|
||||
/// </summary>
|
||||
[HttpGet("logout")]
|
||||
public async Task<IActionResult> Logout()
|
||||
{
|
||||
// 清除本地 Cookie 登录态
|
||||
await HttpContext.SignOutAsync(YhAuthDefaults.CookieScheme);
|
||||
|
||||
if (HttpContext.Features.Get<ISessionFeature>() != null)
|
||||
{
|
||||
HttpContext.Session.Clear();
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Yh.Platform.Abstractions;
|
||||
using YhCore.Common;
|
||||
using YhCore.Common.Config;
|
||||
using YhCore.Common.UserContext;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.WebApi.Controllers.Api.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// 公共字典 / 文件(对应旧 Areas/Common/CommonController,路由 api/base)。
|
||||
/// 字典部分 8 接口 + 文件部分 4 接口(FileDown / FilePreview / FileUpload / FileUploadImage)已落地;
|
||||
/// 旧 [NoAuth] 语义:无需用户上下文,schId 由前端传入或取当前用户。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route(ApiRoutes.ApiPath + "/base")]
|
||||
public class CommonController : ControllerBase
|
||||
{
|
||||
private readonly IDictionaryService _dict;
|
||||
private readonly IFileService _file;
|
||||
|
||||
public CommonController(IDictionaryService dict, IFileService file)
|
||||
{
|
||||
_dict = dict;
|
||||
_file = file;
|
||||
}
|
||||
|
||||
/// <summary>获取指定字典数据(Type 1~10)。</summary>
|
||||
[HttpGet("GetSysDic")]
|
||||
public async Task<List<DicOption>> GetSysDic([FromQuery] int type = 0)
|
||||
{
|
||||
var typeId = DictionaryTypeHelper.FromSysDicType(type);
|
||||
if (typeId == null)
|
||||
{
|
||||
throw new DException("参数错误");
|
||||
}
|
||||
|
||||
return await _dict.GetDictListAsync(typeId, SchSerID());
|
||||
}
|
||||
|
||||
/// <summary>字典类别列表([NoAuth],排除部门)。</summary>
|
||||
[HttpGet("GetDicTypeList")]
|
||||
public async Task<List<DicOption>> GetDicTypeList() => await _dict.GetDicTypeListAsync();
|
||||
|
||||
/// <summary>字典列表管理(Type=0/DEPT 取全校;其他仅超管 bestsch)。</summary>
|
||||
[HttpGet("GetDictMangeList")]
|
||||
public async Task<List<DicManageItem>> GetDictMangeList([FromQuery] string type)
|
||||
{
|
||||
var typeId = DictionaryTypeHelper.ResolveTypeId(type);
|
||||
if (typeId == null)
|
||||
{
|
||||
throw new DException("参数错误");
|
||||
}
|
||||
|
||||
if (DictionaryTypeHelper.IsDept(typeId))
|
||||
{
|
||||
return await _dict.GetDictMangeListAsync("DEPT", SchSerID());
|
||||
}
|
||||
|
||||
//if (UserContextHolder.Current.() =1 )
|
||||
//{
|
||||
// throw new DException("非超管不得操作!");
|
||||
//}
|
||||
|
||||
return await _dict.GetDictMangeListAsync(typeId, "");
|
||||
}
|
||||
|
||||
/// <summary>添加字典。</summary>
|
||||
[HttpPost("AddDic")]
|
||||
public async Task AddDic([FromBody] Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数错误!");
|
||||
|
||||
var info = new Common_Dictionary
|
||||
{
|
||||
typeID = model.typeID,
|
||||
typeName = DictionaryTypeHelper.GetTypeName(model.typeID),
|
||||
Name = model.Name,
|
||||
Schserid = SchSerID()
|
||||
};
|
||||
|
||||
await _dict.AddDicAsync(info);
|
||||
}
|
||||
|
||||
/// <summary>更新字典。</summary>
|
||||
[HttpPost("UpdateDic")]
|
||||
public async Task UpdateDic([FromBody] Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数有误!");
|
||||
model.Schserid = SchSerID();
|
||||
await _dict.UpdateDicAsync(model);
|
||||
}
|
||||
|
||||
/// <summary>编辑字典名称。</summary>
|
||||
[HttpPost("EditDic")]
|
||||
public async Task EditDic([FromBody] Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数有误!");
|
||||
model.Schserid = SchSerID();
|
||||
await _dict.EditDicAsync(model);
|
||||
}
|
||||
|
||||
/// <summary>添加发文部门。</summary>
|
||||
[HttpPost("AddDeptDic")]
|
||||
public async Task AddDeptDic([FromBody] Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数有误!");
|
||||
|
||||
var info = new Common_Dictionary
|
||||
{
|
||||
typeID = "DEPT",
|
||||
typeName = "部门类别",
|
||||
Name = model.Name,
|
||||
Schserid = SchSerID()
|
||||
};
|
||||
|
||||
await _dict.AddDicAsync(info);
|
||||
}
|
||||
|
||||
/// <summary>更新发文部门。</summary>
|
||||
[HttpPost("UpdateDeptDic")]
|
||||
public async Task UpdateDeptDic([FromBody] Common_Dictionary model)
|
||||
{
|
||||
if (model == null) throw new DException("参数有误!");
|
||||
model.Schserid = SchSerID();
|
||||
await _dict.UpdateDicAsync(model);
|
||||
}
|
||||
|
||||
#region 文件:下载 / 预览 / 上传
|
||||
|
||||
/// <summary>
|
||||
/// 公用下载文件(旧 FileDown,[NoAuth])。yl=true 且为图片/PDF 时重定向到文件 URL(webapp 预览),
|
||||
/// 否则物理文件下载。
|
||||
/// </summary>
|
||||
[HttpGet("FileDown")]
|
||||
public async Task<IActionResult> FileDown([FromQuery] int id, [FromQuery] bool yl = false)
|
||||
{
|
||||
if (id == 0)
|
||||
return Ok();
|
||||
|
||||
var file = await _file.GetEnclosureAsync(id);
|
||||
|
||||
string[] previewArry = { ".jpg", ".gif", ".png", ".pdf", ".jpeg", ".bmp" };
|
||||
if (yl)
|
||||
{
|
||||
// 旧 sch_weboa_url.Replace("notice/api", AppType + "/") + fileurl → 新 FileWebPath + fileurl
|
||||
string fileUrl = (Config.FileWebPath ?? "") + file.fileurl;
|
||||
string suffName = Path.GetExtension(fileUrl);
|
||||
if (previewArry.Any(x => x == suffName.ToLower()))
|
||||
return Redirect(fileUrl);
|
||||
}
|
||||
|
||||
// WebHelper.DownFile:物理文件下载
|
||||
string filePath = _file.MapPhysicalPath(file.fileurl);
|
||||
if (!System.IO.File.Exists(filePath))
|
||||
return Content("请求的文件不存在!");
|
||||
|
||||
string contentType = GetContentType(filePath);
|
||||
string outputFileName = file.filename ?? "file";
|
||||
return PhysicalFile(filePath, contentType, outputFileName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 公用预览 office 文件(旧 FilePreview / CommonPreview,[NoAuth])。
|
||||
/// type=1 → 返回可用预览 url(本地 .pdf 存在优先);type=2 → 重定向本地 pdf 或 officeapps.live.com;
|
||||
/// 其余 → 重定向替换后缀为 .pdf 的地址。跳转返回 RedirectResult(全局包装过滤器不拦截)。
|
||||
/// </summary>
|
||||
[HttpGet("FilePreview")]
|
||||
public async Task<object> FilePreview([FromQuery] int id, [FromQuery] string type = null)
|
||||
{
|
||||
if (id == 0)
|
||||
throw new DException("文件ID错误!");
|
||||
|
||||
var file = await _file.GetEnclosureAsync(id);
|
||||
|
||||
// 公文另做处理
|
||||
if (file.fileurl != null && file.fileurl.IndexOf("gw.") > -1)
|
||||
{
|
||||
string fileUrl = "http://" + file.fileurl;
|
||||
string suffName = Path.GetExtension(fileUrl);
|
||||
string newName = fileUrl.Remove(fileUrl.LastIndexOf(suffName, StringComparison.Ordinal), suffName.Length) + ".pdf";
|
||||
return Redirect(newName);
|
||||
}
|
||||
|
||||
string fileUrl2 = (Config.FileWebPath ?? "") + file.fileurl;
|
||||
string suffName2 = Path.GetExtension(fileUrl2);
|
||||
if (!string.IsNullOrEmpty(type))
|
||||
{
|
||||
// 单独读取文件是否存在
|
||||
string newFile = file.fileurl;
|
||||
string pdfFile = newFile.Remove(newFile.LastIndexOf(suffName2, StringComparison.Ordinal), suffName2.Length) + ".pdf";
|
||||
string outFile = _file.MapPhysicalPath(pdfFile);
|
||||
|
||||
if (type == "1")
|
||||
{
|
||||
if (System.IO.File.Exists(outFile)) return (Config.FileWebPath ?? "") + pdfFile;
|
||||
return fileUrl2;
|
||||
}
|
||||
if (type == "2")
|
||||
{
|
||||
string pdfUrl = string.Empty;
|
||||
if (System.IO.File.Exists(outFile)) pdfUrl = (Config.FileWebPath ?? "") + pdfFile;
|
||||
else pdfUrl = "https://view.officeapps.live.com/op/view.aspx?src=" + fileUrl2;
|
||||
return Redirect(pdfUrl);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
string newName2 = fileUrl2.Remove(fileUrl2.LastIndexOf(suffName2, StringComparison.Ordinal), suffName2.Length) + ".pdf";
|
||||
return Redirect(newName2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 公用附件上传(旧 FileUpload,[NoAuth])。
|
||||
/// chunk 参数 → 分片保存返回 {code:1,type:"chunk",msg};fileName 参数 → 合并分片;否则单文件上传。
|
||||
/// 落库 Common_UpFiles(TopicType=AppCode),返回 {fileurl,host,fileid,name,type}。
|
||||
/// </summary>
|
||||
[HttpPost("FileUpload")]
|
||||
public async Task<object> Upload()
|
||||
{
|
||||
string apptype = "docprint"; // 旧 WebHelper.AppType 固定 docprint
|
||||
string guid = Request.Form["guid"];
|
||||
string chunk = Request.Form["chunk"];
|
||||
string fileName = Request.Form["fileName"];
|
||||
|
||||
if (!string.IsNullOrEmpty(chunk))
|
||||
{
|
||||
var chunkFile = Request.Form.Files.FirstOrDefault(f => f.Name == "file") ?? Request.Form.Files.FirstOrDefault();
|
||||
await _file.SaveChunkAsync(guid, chunk, chunkFile);
|
||||
return new { code = 1, type = "chunk", msg = "分片上传成功!" };
|
||||
}
|
||||
|
||||
FileObj url;
|
||||
string errMsg = "上传失败!";
|
||||
if (string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
url = await _file.SaveFileAsync(apptype, Request.Form.Files.FirstOrDefault());
|
||||
}
|
||||
else
|
||||
{
|
||||
url = await _file.MergeSaveAsync(apptype, guid, fileName);
|
||||
}
|
||||
if (url == null)
|
||||
throw new DException(errMsg);
|
||||
|
||||
// 旧 WebHelper.AppCode == "1102" ? "1002" : AppCode → docprint 固定 1003
|
||||
int serid = await _file.SaveEnclosureAsync(url, DocPrintConstants.AppCode.ToString());
|
||||
return new { fileurl = url.fileurl, host = url.host, fileid = serid, name = url.filename, type = url.filetype };
|
||||
}
|
||||
|
||||
/// <summary>公用图片上传(旧 FileUploadImage,[NoAuth];多图,返回数组)。</summary>
|
||||
[HttpPost("FileUploadImage")]
|
||||
public async Task<object> UploadImg()
|
||||
{
|
||||
string apptype = "docprint";
|
||||
var urlList = await _file.SaveFileImageAsync(apptype, Request.Form.Files);
|
||||
if (urlList.Count == 0)
|
||||
throw new DException("上传失败!");
|
||||
|
||||
var result = new List<object>();
|
||||
foreach (var item in urlList)
|
||||
{
|
||||
int serid = await _file.SaveEnclosureAsync(item, DocPrintConstants.AppCode.ToString());
|
||||
result.Add(new { fileurl = item.fileurl, host = item.host, fileid = serid, name = item.filename, type = item.filetype });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>按扩展名取 ContentType(替代旧 MimeMapping.GetMimeMapping)。</summary>
|
||||
private static string GetContentType(string filePath)
|
||||
{
|
||||
string ext = Path.GetExtension(filePath).ToLower();
|
||||
return ext switch
|
||||
{
|
||||
".jpg" or ".jpeg" => "image/jpeg",
|
||||
".gif" => "image/gif",
|
||||
".png" => "image/png",
|
||||
".bmp" => "image/bmp",
|
||||
".pdf" => "application/pdf",
|
||||
".txt" => "text/plain",
|
||||
".doc" => "application/msword",
|
||||
".docx" => "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
".xls" => "application/vnd.ms-excel",
|
||||
".xlsx" => "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
".ppt" => "application/vnd.ms-powerpoint",
|
||||
".pptx" => "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
".zip" => "application/zip",
|
||||
".rar" => "application/x-rar-compressed",
|
||||
".mp4" => "video/mp4",
|
||||
_ => "application/octet-stream",
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static string SchSerID() => UserContextHolder.Current.SchSerID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using YhCore.Common;
|
||||
using YhCore.Common.UserContext;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.WebApi.Controllers.Api.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// 自由审批流控制器(对应旧 Areas/Common/FreeApproveController,路由 api/common/freeapprove)。
|
||||
/// 7 接口:自由流程节点查询/保存、审核意见、审核回显、抄送人。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route(ApiRoutes.ApiPath + "/common/freeapprove")]
|
||||
public class FreeApproveController : ControllerBase
|
||||
{
|
||||
private readonly IFreeApproveService _svc;
|
||||
|
||||
public FreeApproveController(IFreeApproveService svc)
|
||||
{
|
||||
_svc = svc;
|
||||
}
|
||||
|
||||
/// <summary>获取当前自由流程节点(dataid 为空时取最大 dataid)。</summary>
|
||||
[HttpGet("GetFleeFlow")]
|
||||
public Task<List<FreeFlowNode>> GetFleeFlow([FromQuery] int? dataid = null)
|
||||
=> _svc.GetFleeFlowAsync(UserContextHolder.Current?.Id ?? 0, dataid);
|
||||
|
||||
/// <summary>保存自由流程审核人,返回首步骤 SerID。</summary>
|
||||
[HttpGet("EditFleeFlow")]
|
||||
public Task<int> EditFleeFlow([FromQuery] string FlowUids, [FromQuery] int? dataid = null, [FromQuery] bool IsFlowNull = false)
|
||||
=> _svc.EditFleeFlowAsync(UserContextHolder.Current?.Id ?? 0, SchSerID(), dataid, FlowUids, IsFlowNull);
|
||||
|
||||
/// <summary>添加审核意见。</summary>
|
||||
[HttpGet("AddShenheRemark")]
|
||||
public Task<string> AddShenheRemark([FromQuery] int? dataid, [FromQuery] string remark, [FromQuery] int freestepid)
|
||||
=> _svc.AddShenheRemarkAsync(UserContextHolder.Current?.Id ?? 0, dataid, remark, freestepid);
|
||||
|
||||
/// <summary>获取后续自由流程节点。</summary>
|
||||
[HttpGet("GetAfterFleeFlow")]
|
||||
public Task<List<FreeFlowNode>> GetAfterFleeFlow([FromQuery] int? dataid, [FromQuery] int freestepid)
|
||||
=> _svc.GetAfterFleeFlowAsync(dataid, freestepid);
|
||||
|
||||
/// <summary>编辑后续自由流程,返回末步骤 SerID。</summary>
|
||||
[HttpGet("EditAfterFleeFlow")]
|
||||
public Task<int> EditAfterFleeFlow([FromQuery] int? dataid, [FromQuery] int freestepid, [FromQuery] string FlowUids)
|
||||
=> _svc.EditAfterFleeFlowAsync(dataid, freestepid, FlowUids);
|
||||
|
||||
/// <summary>自由流程审核回显(发起人 + 审核人步骤)。</summary>
|
||||
[HttpGet("ShenheFlow")]
|
||||
public Task<List<FreeShenheInfo>> ShenheFlow([FromQuery] int uid, [FromQuery] string ctime, [FromQuery] int dataid,
|
||||
[FromQuery] int? freestepid = null, [FromQuery] int? flowid = null)
|
||||
=> _svc.ShenheFlowAsync(uid, ctime, dataid, freestepid, flowid);
|
||||
|
||||
/// <summary>抄送人列表(按 uids 查用户)。</summary>
|
||||
[HttpGet("ChaoSendList")]
|
||||
public Task<List<ChaoSendUser>> ChaoSendList([FromQuery] string uids)
|
||||
=> _svc.ChaoSendListAsync(uids);
|
||||
|
||||
private static string SchSerID() => UserContextHolder.Current.SchSerID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Yh.SchClass.OpenApi.Model;
|
||||
using YhCore.Common;
|
||||
using YhCore.Common.UserContext;
|
||||
using YhCore.Entity;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Model.Vo;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.WebApi.Controllers.Api.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// 审核流程管理控制器(对应旧 Areas/Common/reviewprocessController,路由 api/common/reviewprocess)。
|
||||
/// 23 接口:流程/步骤 CRUD、教师查询、流程判断、用户角色。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route(ApiRoutes.ApiPath + "/common/reviewprocess")]
|
||||
public class ReviewprocessController : ControllerBase
|
||||
{
|
||||
private readonly IReviewProcessService _svc;
|
||||
|
||||
public ReviewprocessController(IReviewProcessService svc)
|
||||
{
|
||||
_svc = svc;
|
||||
}
|
||||
|
||||
#region 流程与步骤
|
||||
|
||||
/// <summary>审核流程列表(分页)。</summary>
|
||||
[HttpGet("List")]
|
||||
public Task<PageVO<FlowListItem>> List([FromQuery] string Name, [FromQuery] int PageSize,
|
||||
[FromQuery] int PageIndex, [FromQuery] int flowType, [FromQuery] bool Edit = false)
|
||||
=> _svc.ListAsync(Name, PageSize, PageIndex, flowType, Edit, SchSerID());
|
||||
|
||||
/// <summary>审核流程列表【局版】(降级为当前学校)。</summary>
|
||||
[HttpGet("List2")]
|
||||
public Task<PageVO<FlowListItem>> List2([FromQuery] string Name, [FromQuery] int PageSize,
|
||||
[FromQuery] int PageIndex, [FromQuery] int flowType, [FromQuery] bool Edit = false)
|
||||
=> _svc.List2Async(Name, PageSize, PageIndex, flowType, Edit, SchSerID());
|
||||
|
||||
/// <summary>步骤流程列表。</summary>
|
||||
[HttpGet("StepList")]
|
||||
public Task<PageVO<StepListItem>> StepList([FromQuery] string Name, [FromQuery] int Serid)
|
||||
=> _svc.StepListAsync(Name, Serid);
|
||||
|
||||
/// <summary>添加审核流程。</summary>
|
||||
[HttpPost("Create")]
|
||||
public async Task Create([FromBody] Common_flowinfo model)
|
||||
{
|
||||
model.schSerID = SchSerID();
|
||||
await _svc.CreateAsync(model);
|
||||
}
|
||||
|
||||
/// <summary>修改审核流程。</summary>
|
||||
[HttpPost("Edit")]
|
||||
public async Task Edit([FromBody] Common_flowinfo model)
|
||||
{
|
||||
model.schSerID = SchSerID();
|
||||
await _svc.EditAsync(model);
|
||||
}
|
||||
|
||||
/// <summary>添加步骤流程。</summary>
|
||||
[HttpPost("StepCreate")]
|
||||
public Task StepCreate([FromBody] Common_flowstep model)
|
||||
=> _svc.StepCreateAsync(model);
|
||||
|
||||
/// <summary>修改步骤流程。</summary>
|
||||
[HttpPost("StepEdit")]
|
||||
public Task StepEdit([FromBody] Common_flowstep model)
|
||||
=> _svc.StepEditAsync(model);
|
||||
|
||||
/// <summary>删除审核流程(ids 逗号分隔)。</summary>
|
||||
[HttpGet("Delete")]
|
||||
public Task Delete([FromQuery] string ids)
|
||||
=> _svc.DeleteAsync(ids);
|
||||
|
||||
/// <summary>删除步骤。</summary>
|
||||
[HttpPost("StepDelete")]
|
||||
public Task StepDelete([FromBody] decimal[] ids)
|
||||
=> _svc.StepDeleteAsync(ids);
|
||||
|
||||
/// <summary>流程绑定的步骤列表。</summary>
|
||||
[HttpGet("BindstepList")]
|
||||
public Task<List<BindStepItem>> BindstepList([FromQuery] int Serid)
|
||||
=> _svc.BindstepListAsync(Serid);
|
||||
|
||||
/// <summary>绑定步骤的人员。</summary>
|
||||
[HttpGet("BindGroupuserList")]
|
||||
public Task<List<BindGroupuserItem>> BindGroupuserList([FromQuery] int flowid, [FromQuery] int prestep)
|
||||
=> _svc.BindGroupuserListAsync(flowid, prestep);
|
||||
|
||||
/// <summary>绑定流程(流程 + 抄送人)。</summary>
|
||||
[HttpGet("Bindflowinfo")]
|
||||
public Task<List<BindFlowItem>> Bindflowinfo([FromQuery] int flowType)
|
||||
=> _svc.BindflowinfoAsync(SchSerID(), flowType);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 教师 / 人员
|
||||
|
||||
/// <summary>当前学校教师列表。</summary>
|
||||
[HttpGet("GetTeaList")]
|
||||
public Task<List<TeaOption>> GetTeaList()
|
||||
=> _svc.GetTeaListAsync(SchSerID());
|
||||
|
||||
/// <summary>学校 + 教育局教师列表(降级为当前学校)。</summary>
|
||||
[HttpGet("GetTeaListSup")]
|
||||
public Task<List<TeaOption>> GetTeaListSup()
|
||||
=> _svc.GetTeaListSupAsync(SchSerID());
|
||||
|
||||
/// <summary>教师信息分页。</summary>
|
||||
[HttpGet("GetUserinfo")]
|
||||
public Task<PageVO<TeaInfoVo>> GetUserinfo([FromQuery] string Name, [FromQuery] int statetype,
|
||||
[FromQuery] int PageSize, [FromQuery] int PageIndex)
|
||||
=> _svc.GetUserinfoAsync(SchSerID(), Name, statetype, PageSize, PageIndex);
|
||||
|
||||
/// <summary>非中层领导人员。</summary>
|
||||
[HttpGet("GetNoLeadingMiddleInfo")]
|
||||
public Task<string[]> GetNoLeadingMiddleInfo()
|
||||
=> _svc.GetNoLeadingMiddleInfoAsync(SchSerID());
|
||||
|
||||
/// <summary>非中层领导人员【局版】(降级为当前学校)。</summary>
|
||||
[HttpGet("GetNoLeadingMiddleInfoSup")]
|
||||
public Task<string[]> GetNoLeadingMiddleInfoSup()
|
||||
=> _svc.GetNoLeadingMiddleInfoSupAsync(SchSerID());
|
||||
|
||||
/// <summary>设置/取消中层领导(教务 SDK 未提供,暂不支持)。</summary>
|
||||
[HttpGet("changeLeadingMiddleStatus")]
|
||||
public Task ChangeLeadingMiddleStatus([FromQuery] int state, [FromQuery] string UserTeaSerIDstr)
|
||||
=> _svc.ChangeLeadingMiddleStatusAsync(state, UserTeaSerIDstr);
|
||||
|
||||
/// <summary>附件数据源。</summary>
|
||||
[HttpGet("filelist")]
|
||||
public Task<List<FileListItem>> Filelist([FromQuery] string ids)
|
||||
=> _svc.FilelistAsync(ids);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 流程判断 / 用户
|
||||
|
||||
/// <summary>该校审核流程是固定还是自由。</summary>
|
||||
[HttpGet("IsShenheflow")]
|
||||
public Task<ShenheFlowResult> IsShenheflow()
|
||||
=> _svc.IsShenheflowAsync(SchSerID(), UserContextHolder.Current?.Id ?? 0);
|
||||
|
||||
/// <summary>绑定抄送默认人。</summary>
|
||||
[HttpGet("IsSendUser")]
|
||||
public Task<List<SendUserItem>> IsSendUser()
|
||||
=> _svc.IsSendUserAsync(SchSerID(), UserContextHolder.Current?.Id ?? 0);
|
||||
|
||||
/// <summary>当前用户基础信息。</summary>
|
||||
[HttpGet("GetUser")]
|
||||
public Task<CurrentUserInfo> GetUser()
|
||||
=> _svc.GetUserAsync();
|
||||
|
||||
/// <summary>用户流程角色(1 有流程,0 无)。</summary>
|
||||
[HttpGet("GetUserFlowRole")]
|
||||
public Task<int> GetUserFlowRole()
|
||||
=> _svc.GetUserFlowRoleAsync(UserContextHolder.Current?.Id ?? 0);
|
||||
|
||||
#endregion
|
||||
|
||||
private static string SchSerID() => UserContextHolder.Current.SchSerID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Yh.Platform.Abstractions;
|
||||
using YhCore.Common;
|
||||
using YhCore.Model.Dto;
|
||||
using YhCore.Service.Interfaces;
|
||||
|
||||
namespace YhCore.WebApi.Controllers.Api.DocPrint
|
||||
{
|
||||
/// <summary>
|
||||
/// 文印统计看板控制器(对应旧 Areas/DocPrint/DocPrintStatisticsController,路由 api/docprint/statistics)。
|
||||
/// 旧类级 [NoAuth]:schId 由前端传入(非当前用户),新项目 SSO 关闭时匿名放行、开启时按配置保护,
|
||||
/// 语义一致。ExportRecord(Excel 导出,NPOI)延后迁移。
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route(ApiRoutes.ApiPath + "/docprint/statistics")]
|
||||
public class DocPrintStatisticsController : ControllerBase
|
||||
{
|
||||
private readonly IDocPrintStatisticsService _statistics;
|
||||
|
||||
public DocPrintStatisticsController(IDocPrintStatisticsService statistics)
|
||||
{
|
||||
_statistics = statistics;
|
||||
}
|
||||
|
||||
/// <summary>打印总数/消耗/最多类型统计(旧 GetDocPrintCount)。</summary>
|
||||
[HttpGet("GetDocPrintCount")]
|
||||
public async Task<System.Collections.Generic.List<DocPrintCountItem>> GetDocPrintCount(
|
||||
[FromQuery] int schId,
|
||||
[FromQuery] DateTime? startDate = null,
|
||||
[FromQuery] DateTime? endDate = null)
|
||||
=> await _statistics.GetDocPrintCountAsync(schId, startDate, endDate);
|
||||
|
||||
/// <summary>饼图(按类型,消耗/数量,旧 GetPieChart)。</summary>
|
||||
[HttpGet("GetPieChart")]
|
||||
public async Task<System.Collections.Generic.List<PieChartSeries>> GetPieChart(
|
||||
[FromQuery] int schId,
|
||||
[FromQuery] DateTime? startDate = null,
|
||||
[FromQuery] DateTime? endDate = null)
|
||||
=> await _statistics.GetPieChartAsync(schId, startDate, endDate);
|
||||
|
||||
/// <summary>打印类型列表(旧 GetDocPrintType)。</summary>
|
||||
[HttpGet("GetDocPrintType")]
|
||||
public async Task<System.Collections.Generic.List<DocPrintTypeItem>> GetDocPrintType([FromQuery] int schId)
|
||||
=> await _statistics.GetDocPrintTypeAsync(schId);
|
||||
|
||||
/// <summary>分页明细(旧 GetDocPrintList;旧 1 基 pageIndex 内部转 0 基)。</summary>
|
||||
[HttpGet("GetDocPrintList")]
|
||||
public async Task<DocPrintListResult> GetDocPrintList(
|
||||
[FromQuery] int schId,
|
||||
[FromQuery] string docType = "ALL",
|
||||
[FromQuery] string likeName = null,
|
||||
[FromQuery] int pageIndex = 1,
|
||||
[FromQuery] int pageSize = 10,
|
||||
[FromQuery] DateTime? startDate = null,
|
||||
[FromQuery] DateTime? endDate = null)
|
||||
{
|
||||
if (pageSize <= 0) throw new DException("pageSize 必须大于 0");
|
||||
return await _statistics.GetDocPrintListAsync(schId, docType, likeName, pageIndex, pageSize, startDate, endDate);
|
||||
}
|
||||
|
||||
// 延后:ExportRecord(Excel 导出,NPOI)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user