This commit is contained in:
wanghu
2026-09-02 10:23:24 +08:00
parent 5a414b14d2
commit ce436cbb32
117 changed files with 13768 additions and 0 deletions
+20
View File
@@ -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";
}
}
+42
View File
@@ -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;
}
}
}
+287
View File
@@ -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&lt;T&gt;,对应 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);
}
}
}
+19
View File
@@ -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";
}
}
+25
View File
@@ -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; }
}
}
+14
View File
@@ -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; }
}
}
+11
View File
@@ -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";
}
}
+17
View File
@@ -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;
}
}
+47
View File
@@ -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;
}
}
+42
View File
@@ -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;
}
}
}
+66
View File
@@ -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));
}
}
}
+24
View File
@@ -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>
+25
View File
@@ -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; }
}
}
+33
View File
@@ -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; }
}
}
+28
View File
@@ -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; }
}
}
+30
View File
@@ -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; }
}
}
+23
View File
@@ -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; }
}
}
+24
View File
@@ -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; }
}
}
+24
View File
@@ -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; }
}
}
+27
View File
@@ -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; }
}
}
+30
View File
@@ -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; }
}
}
+28
View File
@@ -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; }
}
}
+23
View File
@@ -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; }
}
}
+24
View File
@@ -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; }
}
}
+27
View File
@@ -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; }
}
}
+73
View File
@@ -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; }
}
}
+47
View File
@@ -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; }
}
}
+32
View File
@@ -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; }
}
}
+29
View File
@@ -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; }
}
}
+23
View File
@@ -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; }
}
}
+25
View File
@@ -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; }
}
}
+27
View File
@@ -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; }
}
}
+21
View File
@@ -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; }
}
}
+27
View File
@@ -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; }
}
}
+12
View File
@@ -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>
+129
View File
@@ -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; }
}
}
+28
View File
@@ -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; }
}
}
+38
View File
@@ -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; }
}
}
+120
View File
@@ -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; }
}
}
+228
View File
@@ -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; }
}
}
+236
View File
@@ -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; }
}
}
+40
View File
@@ -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);
}
}
+16
View File
@@ -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&lt;WY_DocPrint&gt; / DapperChannel。
/// </summary>
public static IServiceCollection AddRepositories(this IServiceCollection services)
{
services.AddScoped(typeof(IRepository<>), typeof(RepositoryBase<>));
services.AddScoped<DapperChannel>();
return services;
}
}
}
+34
View File
@@ -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();
}
}
+128
View File
@@ -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&lt;SchClass&gt;()
/// .And(c =&gt; c.SchoolId == schId)
/// .AndIf(!string.IsNullOrEmpty(name), c =&gt; 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);
}
}
}
}
+102
View File
@@ -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
}
}
+235
View File
@@ -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
}
}
+18
View File
@@ -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);
}
}
+34
View File
@@ -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>
+71
View File
@@ -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));
}
}
}
}
+75
View File
@@ -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;
}
}
}
+25
View File
@@ -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)
}
}
@@ -0,0 +1,32 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using YhCore.Common;
using YhCore.Entity;
using YhCore.Repository;
namespace YhCore.WebApi.Controllers.Api.DocPrint
{
/// <summary>
/// 启动预热控制器(对应旧 Areas/DocPrint/LoadingController,路由 api/docprint/loading)。
/// 无业务逻辑:执行一次 Base_Log 查询触发数据库连接建立,防止首请求慢。
/// 旧接口 void 无返回体;新项目由 GlobalWrapResultFilter 统一包装为 {code:0,msg:"",data:null}。
/// </summary>
[ApiController]
[Route(ApiRoutes.ApiPath + "/docprint/loading")]
public class LoadingController : ControllerBase
{
private readonly IRepository<Base_Log> _log;
public LoadingController(IRepository<Base_Log> log)
{
_log = log;
}
/// <summary>预热(执行一次 Base_Log 查询)。</summary>
[HttpGet("List")]
public async Task List()
{
await _log.FirstOrDefaultAsync(a => a.Id > 0);
}
}
}
@@ -0,0 +1,334 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Yh.Platform.Abstractions;
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.DocPrint
{
/// <summary>
/// 审核角色 + 流程配置控制器(对应旧 Areas/DocPrint/WYSHRoleController,路由 api/docprint/wyshrole)。
/// 45 action 中本批落地 39 个;Excel 导入导出(ExceltotalListSHRole/WYRDExceldaochu/UpfileExcelSHRole/
/// downMBOfSHRole/downUpfileSHRoleFK)与审批单 PDF(DwonWYSQD)依赖 NPOI/PdfHelper,延后迁移。
/// </summary>
[ApiController]
[Route(ApiRoutes.ApiPath + "/docprint/wyshrole")]
public class WYSHRoleController : ControllerBase
{
private readonly IWYSHRoleService _role;
public WYSHRoleController(IWYSHRoleService role)
{
_role = role;
}
#region 审核角色管理
/// <summary>学校是否启用审核角色(旧 getIsSHRole)。</summary>
[HttpGet("getIsSHRole")]
public async Task<string> GetIsSHRole() => await _role.GetIsSHRoleAsync(SchSerID());
/// <summary>保存启用开关(旧 saveIsSHRole)。</summary>
[HttpGet("saveIsSHRole")]
public async Task<string> SaveIsSHRole([FromQuery] string val) => await _role.SaveIsSHRoleAsync(SchSerID(), val);
/// <summary>审核角色列表(旧 getSHRole)。</summary>
[HttpGet("getSHRole")]
public async Task<List<WYSHRoleExt>> GetSHRole() => await _role.GetSHRoleAsync(SchSerID());
/// <summary>角色改名(旧 UpdateSHRoleName)。</summary>
[HttpGet("UpdateSHRoleName")]
public async Task<string> UpdateSHRoleName([FromQuery] decimal serID, [FromQuery] string sHRoleName)
=> await _role.UpdateSHRoleNameAsync(SchSerID(), UserId(), serID, sHRoleName);
/// <summary>新增/编辑角色(旧 EditSHRole,form: WYSHRole=JSON → [FromBody])。</summary>
[HttpPost("EditSHRole")]
public async Task<string> EditSHRole([FromBody] WYSHRoleExt model)
{
if (model == null) throw new DException("参数错误!");
return await _role.EditSHRoleAsync(SchSerID(), UserId(), model);
}
/// <summary>编辑成员行(旧 editSHTeaRow,form: SHTeaTxt → [FromBody])。</summary>
[HttpPost("editSHTeaRow")]
public async Task<string> EditSHTeaRow([FromBody] WY_SHRole_SHTea model)
{
if (model == null) throw new DException("参数错误!");
return await _role.EditSHTeaRowAsync(SchSerID(), UserId(), model);
}
/// <summary>删除成员行(旧 delSHTeaRow)。</summary>
[HttpGet("delSHTeaRow")]
public async Task<string> DelSHTeaRow([FromQuery] decimal serID)
=> await _role.DelSHTeaRowAsync(SchSerID(), serID);
/// <summary>批量保存审核人(旧 editSHTea,form: SHRoleID + listSHTeaTxt → [FromBody])。</summary>
[HttpPost("editSHTea")]
public async Task<string> EditSHTea([FromBody] EditSHTeaDto model)
{
if (model == null) throw new DException("参数错误!");
return await _role.EditSHTeaAsync(SchSerID(), UserId(), model.SHRoleID, model.listSHTea);
}
/// <summary>批量保存被审核人(旧 editBSHTea,form: SHRoleID + listBSHTeaTxt → [FromBody])。</summary>
[HttpPost("editBSHTea")]
public async Task<string> EditBSHTea([FromBody] EditBSHTeaDto model)
{
if (model == null) throw new DException("参数错误!");
return await _role.EditBSHTeaAsync(SchSerID(), UserId(), model.SHRoleID, model.listBSHTea);
}
/// <summary>删除角色(旧 delSHRole,form: listSHRoleIDTxt → [FromBody])。</summary>
[HttpPost("delSHRole")]
public async Task<string> DelSHRole([FromBody] DelSHRoleDto model)
{
if (model == null) throw new DException("参数错误!");
return await _role.DelSHRoleAsync(SchSerID(), model.listSHRoleID);
}
#endregion
#region 审核角色流程
/// <summary>审核流程列表(旧 ListFlow)。</summary>
[HttpGet("ListFlow")]
public async Task<PageVO<SHRoleFlowListItem>> ListFlow(
[FromQuery] string name = null,
[FromQuery] int pageSize = 10,
[FromQuery] int pageIndex = 0,
[FromQuery] int flowType = 0,
[FromQuery] bool edit = false)
=> await _role.ListFlowAsync(name, pageSize, pageIndex, SchSerID(), flowType, edit);
/// <summary>新增/编辑流程(旧 EditFlow;schSerID 取当前用户)。</summary>
[HttpPost("EditFlow")]
public async Task<string> EditFlow([FromBody] Common_flowinfo model)
{
if (model == null) throw new DException("参数错误!");
model.schSerID = SchSerID();
return await _role.EditFlowAsync(model);
}
/// <summary>新增步骤(旧 StepCreateOfSHRole)。</summary>
[HttpPost("StepCreateOfSHRole")]
public async Task<string> StepCreateOfSHRole([FromBody] Common_flowstep model)
{
if (model == null) throw new DException("参数错误!");
return await _role.StepCreateOfSHRoleAsync(model);
}
/// <summary>步骤详情(旧 getStepInfo)。</summary>
[HttpGet("getStepInfo")]
public async Task<SHRoleStepInfo> GetStepInfo([FromQuery] decimal flowid)
=> await _role.GetStepInfoAsync(SchSerID(), flowid);
/// <summary>删除步骤(旧 StepDelete)。</summary>
[HttpGet("StepDelete")]
public async Task<string> StepDelete([FromQuery] int id)
=> await _role.StepDeleteAsync(new List<int> { id });
/// <summary>改步骤名/条件/金额/按钮显隐(旧 updateStepName)。</summary>
[HttpGet("updateStepName")]
public async Task<string> UpdateStepName(
[FromQuery] int serID,
[FromQuery] int flowid,
[FromQuery] string stepName,
[FromQuery] int cond = 0,
[FromQuery] string isshowJE = "0",
[FromQuery] string isshowBtn = "1")
=> await _role.UpdateStepNameAsync(SchSerID(), serID, flowid, stepName, cond, isshowJE, isshowBtn);
/// <summary>步骤绑定角色(旧 AddStepSHRole)。</summary>
[HttpGet("AddStepSHRole")]
public async Task<string> AddStepSHRole([FromQuery] int flowid, [FromQuery] int stepid, [FromQuery] decimal shRoleid)
=> await _role.AddStepSHRoleAsync(SchSerID(), flowid, stepid, shRoleid);
/// <summary>步骤解绑角色(旧 delStepSHRole)。</summary>
[HttpGet("delStepSHRole")]
public async Task<string> DelStepSHRole([FromQuery] int stepid, [FromQuery] decimal shRoleid)
=> await _role.DelStepSHRoleAsync(SchSerID(), stepid, shRoleid);
/// <summary>按金额取步骤(旧 getStepByJE)。</summary>
[HttpGet("getStepByJE")]
public async Task<SHRoleStepByJEResult> GetStepByJE([FromQuery] int flowid, [FromQuery] int je)
=> await _role.GetStepByJEAsync(SchSerID(), UserId(), flowid, je);
#endregion
#region 文印申请页面
/// <summary>流程发起人列表(旧 getLCOfUser)。</summary>
[HttpGet("getLCOfUser")]
public async Task<List<WYFlowForUser>> GetLCOfUser()
=> await _role.GetLCOfUserAsync(SchSerID(), UserId(), UserName());
/// <summary>当前用户是否发起人(旧 IsSendUserSHRole)。</summary>
[HttpGet("IsSendUserSHRole")]
public async Task<List<SendUserItem>> IsSendUserSHRole([FromQuery] int flowid)
=> await _role.IsSendUserSHRoleAsync(SchSerID(), UserContextHolder.Current?.Id ?? 0, flowid);
/// <summary>字典信息(旧 getDicInfo)。</summary>
[HttpGet("getDicInfo")]
public async Task<List<Common_Dictionary>> GetDicInfo([FromQuery] string typeID)
=> await _role.GetDicInfoAsync(SchSerID(), typeID);
/// <summary>编辑文印(审核角色视角;旧 editDocPrint,form 三段 JSON → [FromBody])。</summary>
[HttpPost("editDocPrint")]
public async Task<int> EditDocPrint([FromBody] WYSHRoleEditDocPrintDto model)
{
if (model == null) throw new DException("参数错误!");
return await _role.EditDocPrintAsync(SchSerID(), UserId(), model);
}
/// <summary>审核流程回显(旧 getshenheFlow)。</summary>
[HttpGet("getshenheFlow")]
public async Task<SHRoleShenheFlowResult> GetshenheFlow([FromQuery] decimal docprintID, [FromQuery] int flowid)
=> await _role.GetshenheFlowAsync(SchSerID(), docprintID, flowid);
/// <summary>文印表格数据(旧 getdocOftable)。</summary>
[HttpGet("getdocOftable")]
public async Task<SHRoleDocOfTableResult> GetDocOftable([FromQuery] decimal docPrintID)
=> await _role.GetDocOftableAsync(SchSerID(), docPrintID);
#endregion
#region 审核角色列表与审批
/// <summary>单条详情(旧 MyListtoOneSHRole)。</summary>
[HttpGet("MyListtoOneSHRole")]
public async Task<List<SHRoleDocPrintListItem>> MyListtoOneSHRole([FromQuery] int serID)
=> await _role.MyListtoOneSHRoleAsync(serID, SchSerID());
/// <summary>我的文印(旧 MyListSHRole)。</summary>
[HttpGet("MyListSHRole")]
public async Task<List<SHRoleDocPrintListItem>> MyListSHRole(
[FromQuery] string name = null,
[FromQuery] string status = null,
[FromQuery] string startDate = null,
[FromQuery] string endDate = null,
[FromQuery] int pageSize = 10,
[FromQuery] int pageIndex = 0)
=> await _role.MyListSHRoleAsync(name, status, startDate, endDate, pageSize, pageIndex, UserId(), SchSerID());
/// <summary>全部文印(旧 totalListSHRole)。</summary>
[HttpGet("totalListSHRole")]
public async Task<List<SHRoleDocPrintListItem>> TotalListSHRole(
[FromQuery] int flowid = 0,
[FromQuery] int dwid = 0,
[FromQuery] string name = null,
[FromQuery] string stateid = null,
[FromQuery] string starttime = null,
[FromQuery] string endtime = null,
[FromQuery] int pageSize = 10,
[FromQuery] int pageIndex = 0)
=> await _role.TotalListSHRoleAsync(flowid, dwid, name, stateid, starttime, endtime, pageSize, pageIndex, SchSerID());
/// <summary>抄送列表(旧 SendRdListSHRole)。</summary>
[HttpGet("SendRdListSHRole")]
public async Task<List<SHRoleDocPrintListItem>> SendRdListSHRole(
[FromQuery] int flowid = 0,
[FromQuery] int dwid = 0,
[FromQuery] string name = null,
[FromQuery] string status = null,
[FromQuery] string startDate = null,
[FromQuery] string endDate = null,
[FromQuery] int pageSize = 10,
[FromQuery] int pageIndex = 0)
=> await _role.SendRdListSHRoleAsync(flowid, dwid, name, status, startDate, endDate, UserId(), SchSerID(), pageSize, pageIndex);
/// <summary>审批列表(旧 SPListSHRole)。</summary>
[HttpGet("SPListSHRole")]
public async Task<List<SHRoleDocPrintListItem>> SPListSHRole(
[FromQuery] int flowid = 0,
[FromQuery] int dwid = 0,
[FromQuery] string name = null,
[FromQuery] string status = null,
[FromQuery] string startDate = null,
[FromQuery] string endDate = null,
[FromQuery] int pageSize = 10,
[FromQuery] int pageIndex = 0)
=> await _role.SPListSHRoleAsync(flowid, dwid, name, status, startDate, endDate, UserId(), UserName(), SchSerID(), pageSize, pageIndex);
/// <summary>审核确认(旧 shenheokSHRole)。</summary>
[HttpPost("shenheokSHRole")]
public async Task<string> ShenheOkSHRole([FromBody] FlowTranctDto model)
{
if (model == null) throw new DException("参数错误!");
model.actionuserid = UserContextHolder.Current?.Id ?? 0;
return await _role.ShenheOkSHRoleAsync(model);
}
// 延后:ExceltotalListSHRole / WYRDExceldaochu / UpfileExcelSHRole / downMBOfSHRole / downUpfileSHRoleFK(Excel,NPOI)
// 延后:DwonWYSQD(审批单 PDF,PdfHelper + Server.MapPath)
#endregion
#region 签名 / 退回 / 金额重提
/// <summary>保存签名(旧 SaveSignature,form: pic → [FromBody])。</summary>
[HttpPost("SaveSignature")]
public async Task<string> SaveSignature([FromBody] SaveSignatureDto model)
{
if (model == null) throw new DException("参数错误!");
return await _role.SaveSignatureAsync(SchSerID(), UserId(), model.pic);
}
/// <summary>获取签名(旧 GetSignature)。</summary>
[HttpGet("GetSignature")]
public async Task<string> GetSignature() => await _role.GetSignatureAsync(SchSerID(), UserId());
/// <summary>删除签名(旧 DelSignature)。</summary>
[HttpGet("DelSignature")]
public async Task<string> DelSignature() => await _role.DelSignatureAsync(SchSerID(), UserId());
/// <summary>退回(旧 TuiHui,form: DocID + cont → [FromBody])。</summary>
[HttpPost("TuiHui")]
public async Task<string> TuiHui([FromBody] TuiHuiDto model)
{
if (model == null) throw new DException("参数错误!");
return await _role.TuiHuiAsync(UserId(), model.cont, model.DocID);
}
/// <summary>撤销退回(旧 unTuiHui)。</summary>
[HttpGet("unTuiHui")]
public async Task<string> UnTuiHui([FromQuery] decimal docID)
=> await _role.UnTuiHuiAsync(UserId(), docID);
/// <summary>金额重提(旧 regetJE,form: DocID 逗号分隔 + type → [FromBody])。</summary>
[HttpPost("regetJE")]
public async Task<string> RegetJE([FromBody] RegetJEDto model)
{
if (model == null) throw new DException("参数错误!");
var ids = (model.DocID ?? "").Split(',')
.Where(a => !string.IsNullOrEmpty(a) && decimal.TryParse(a, out _))
.Select(decimal.Parse).ToList();
return await _role.RegetJEAsync(SchSerID(), UserId(), ids, model.type);
}
/// <summary>重提记录(旧 getRegetJERd)。</summary>
[HttpGet("getRegetJERd")]
public async Task<List<SHRoleRegetJERdItem>> GetRegetJERd(
[FromQuery] int pageSize = 10,
[FromQuery] int pageIndex = 0)
=> await _role.GetRegetJERdAsync(SchSerID(), pageSize, pageIndex);
/// <summary>记录下载次数(旧 setDownFileTimes)。</summary>
[HttpGet("setDownFileTimes")]
public async Task<int> SetDownFileTimes([FromQuery] int serID)
=> await _role.SetDownFileTimesAsync(serID);
#endregion
private static string SchSerID() => UserContextHolder.Current.SchSerID;
private static string UserId() => UserContextHolder.Current?.Id.ToString() ?? "0";
private static string UserName() => UserContextHolder.Current?.UserName ?? "";
}
}
@@ -0,0 +1,335 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Yh.Platform.Abstractions;
using YhCore.Common;
using YhCore.Common.UserContext;
using YhCore.Entity;
using YhCore.Model.Dto;
using YhCore.Service.Interfaces;
namespace YhCore.WebApi.Controllers.Api.DocPrint
{
/// <summary>
/// 文印业务主控制器(对应旧 Areas/DocPrint/WY_listController,路由 api/docprint/wy_list)。
/// 第一批:分类 / 版面 / 字典 14 接口;第三批 D1:文印 CRUD / 列表 / 审核 / 统计 / 指派 24 接口;
/// Excel 导出 4 接口延后批次迁移。
/// </summary>
[ApiController]
[Route(ApiRoutes.ApiPath + "/docprint/wy_list")]
public class WY_listController : ControllerBase
{
private readonly IDocPrintClassifyService _classify;
private readonly IDictionaryService _dict;
private readonly IWYDocPrintService _wy;
public WY_listController(IDocPrintClassifyService classify, IDictionaryService dict, IWYDocPrintService wy)
{
_classify = classify;
_dict = dict;
_wy = wy;
}
#region 分类 / 版面 / 字典
/// <summary>添加文印分类。</summary>
[HttpPost("AddDic")]
public async Task AddDic([FromBody] Common_Dictionary model)
{
if (model == null) throw new DException("参数错误!");
model.Schserid = SchSerID();
await _classify.AddDicAsync(model);
}
/// <summary>文印分类列表。</summary>
[HttpGet("CategoryList")]
public async Task<System.Collections.Generic.List<CategoryItem>> CategoryList([FromQuery] string name = null)
=> await _classify.CategoryListAsync(SchSerID(), name);
/// <summary>添加印制版面。</summary>
[HttpPost("AddYZBM")]
public async Task AddYZBM([FromBody] Common_Dictionary model)
{
if (model == null) throw new DException("参数错误!");
model.Schserid = SchSerID();
await _classify.AddYZBMAsync(model);
}
/// <summary>印制版面列表。</summary>
[HttpGet("YZBMList")]
public async Task<System.Collections.Generic.List<CategoryItem>> YZBMList([FromQuery] string name = null)
=> await _classify.YZBMListAsync(SchSerID(), name);
/// <summary>删除文印分类/版面(ids 逗号分隔)。</summary>
[HttpGet("DeleteDic")]
public async Task DeleteDic([FromQuery] string ids)
{
if (string.IsNullOrWhiteSpace(ids)) throw new DException("ids is not null");
decimal[] idArr = ids.Split(',').Select(a => decimal.Parse(a)).ToArray();
await _classify.DeleteDicAsync(idArr);
}
/// <summary>公共类别列表。</summary>
[HttpGet("DictionaryTypeList")]
public async Task<System.Collections.Generic.List<DictionaryTypeItem>> DictionaryTypeList(
[FromQuery] string typeID,
[FromQuery] string name = "",
[FromQuery] bool showAll = false)
=> await _classify.DictionaryTypeListAsync(SchSerID(), name, showAll, typeID);
/// <summary>添加公共类别(复用字典添加逻辑)。</summary>
[HttpPost("AddDictionaryType")]
public async Task AddDictionaryType([FromBody] Common_Dictionary model)
{
if (model == null) throw new DException("参数错误!");
model.Schserid = SchSerID();
await _dict.AddDicAsync(model);
}
/// <summary>编辑公共类别(旧调 GetSysDict.EditDic)。</summary>
[HttpPost("EditDictionaryType")]
public async Task EditDictionaryType([FromBody] Common_Dictionary model)
{
if (model == null) throw new DException("参数有误!");
model.Schserid = SchSerID();
await _dict.EditDicAsync(model);
}
/// <summary>删除公共类别(软删除)。</summary>
[HttpGet("DeleteDictionaryType")]
public async Task DeleteDictionaryType([FromQuery] string ids)
{
if (string.IsNullOrWhiteSpace(ids)) throw new DException("ids is not null");
string[] idArr = ids.Split(',');
await _classify.DeleteDictionaryTypeAsync(idArr);
}
/// <summary>公共类别显隐。</summary>
[HttpGet("SetDictionaryOrd")]
public async Task SetDictionaryOrd([FromQuery] int dicSerID, [FromQuery] bool show)
=> await _classify.SetDictionaryOrdAsync(SchSerID(), dicSerID, show);
/// <summary>公共类别排序。</summary>
[HttpGet("OrderDic")]
public async Task OrderDic([FromQuery] string ids)
=> await _classify.OrderDicAsync(ids, SchSerID());
/// <summary>结果链接(依赖资源平台,已降级)。</summary>
[HttpGet("GetResultUrl")]
public async Task<object> GetResultUrl([FromQuery] int dataId)
{
var user = UserContextHolder.Current;
return await _classify.GetResultUrlAsync(dataId, user?.Id ?? 0, 0, SchSerID());
}
/// <summary>学校文印资源开关。</summary>
[HttpGet("GetSchZyBit")]
public async Task<bool> GetSchZyBit() => await _classify.GetSchZyBitAsync(SchSerID());
/// <summary>设置学校文印资源开关。</summary>
[HttpGet("SetSchZyBit")]
public async Task SetSchZyBit([FromQuery] bool zyBit) => await _classify.SetSchZyBitAsync(SchSerID(), zyBit);
/// <summary>待审数量角标。</summary>
[HttpGet("GetShnheCount")]
public async Task<int> GetShnheCount()
=> await _classify.GetShnheCountAsync(UserContextHolder.Current?.Id.ToString(), SchSerID());
#endregion
#region 文印核心(第三批 D1:抄送 / 附件 / CRUD / 列表 / 审核 / 统计 / 指派)
/// <summary>抄送列表(旧 SendRdList)。</summary>
[HttpGet("SendRdList")]
public async Task<System.Collections.Generic.List<DocPrintListItem>> SendRdList(
[FromQuery] string name = null,
[FromQuery] string startDate = null,
[FromQuery] string endDate = null,
[FromQuery] int pageSize = 10,
[FromQuery] int pageIndex = 0)
=> await _wy.SendRdListAsync(name, startDate, endDate, UserId(), SchSerID(), pageSize, pageIndex);
/// <summary>查看抄送人员详情(旧 SendDetailsList)。</summary>
[HttpGet("SendDetailsList")]
public async Task<System.Collections.Generic.List<SendDetailItem>> SendDetailsList([FromQuery] int serID)
=> await _wy.SendDetailsListAsync(serID);
/// <summary>修改抄送人(旧 EditSend)。</summary>
[HttpGet("EditSend")]
public async Task<string> EditSend([FromQuery] int serID, [FromQuery] string senduserid, [FromQuery] string mysenduserid)
=> await _wy.EditSendAsync(serID, senduserid, mysenduserid, SchSerID(), UserId());
/// <summary>获取附件数据源(旧 filelist)。</summary>
[HttpGet("filelist")]
public async Task<System.Collections.Generic.List<DocPrintFileItem>> FileList([FromQuery] string ids)
=> await _wy.FileListAsync(ids);
/// <summary>添加文印(旧 Create;短信/待办推送已降级为纯落库)。</summary>
[HttpPost("Create")]
public async Task<int> Create([FromBody] WYDocPrintSaveDto model)
{
if (model == null) throw new DException("参数错误!");
model.UserID = UserId();
model.SchSerID = SchSerID();
return await _wy.CreateAsync(model);
}
/// <summary>修改文印(旧 Edit)。</summary>
[HttpPost("Edit")]
public async Task<int> Edit([FromBody] WYDocPrintSaveDto model)
{
if (model == null) throw new DException("参数错误!");
model.UserID = UserId();
model.SchSerID = SchSerID();
return await _wy.EditAsync(model);
}
/// <summary>撤销(旧 Revoke)。</summary>
[HttpPost("Revoke")]
public async Task<string> Revoke([FromBody] FlowTranctDto model)
{
if (model == null) throw new DException("参数错误!");
model.actionuserid = UserContextHolder.Current?.Id;
return await _wy.RevokeAsync(model);
}
/// <summary>我的文印列表(旧 MyList)。</summary>
[HttpGet("MyList")]
public async Task<System.Collections.Generic.List<DocPrintListItem>> MyList(
[FromQuery] string name = null,
[FromQuery] string status = null,
[FromQuery] string startDate = null,
[FromQuery] string endDate = null,
[FromQuery] int pageSize = 10,
[FromQuery] int pageIndex = 0)
=> await _wy.MyListAsync(name, status, startDate, endDate, UserId(), SchSerID(), pageSize, pageIndex);
/// <summary>全部文印列表(旧 AllList 四分类,返回 ctype)。</summary>
[HttpGet("AllList")]
public async Task<System.Collections.Generic.List<DocPrintListItem>> AllList(
[FromQuery] string name = null,
[FromQuery] int pageSize = 10,
[FromQuery] int pageIndex = 0)
=> await _wy.AllListAsync(name, UserId(), SchSerID(), pageSize, pageIndex);
/// <summary>文印审核列表(旧 ShenheList;旧 Name 过滤为死代码,保持不生效)。</summary>
[HttpGet("ShenheList")]
public async Task<System.Collections.Generic.List<DocPrintListItem>> ShenheList(
[FromQuery] int pageSize = 10,
[FromQuery] int pageIndex = 0)
=> await _wy.ShenheListAsync(UserId(), SchSerID(), pageSize, pageIndex);
/// <summary>审批列表(旧 SPList)。</summary>
[HttpGet("SPList")]
public async Task<System.Collections.Generic.List<DocPrintListItem>> SPList(
[FromQuery] string name = null,
[FromQuery] string status = null,
[FromQuery] string startDate = null,
[FromQuery] string endDate = null,
[FromQuery] int pageSize = 10,
[FromQuery] int pageIndex = 0)
=> await _wy.SPListAsync(name, status, startDate, endDate, UserId(), SchSerID(), pageSize, pageIndex);
/// <summary>文印已审列表(旧 YishenList)。</summary>
[HttpGet("YishenList")]
public async Task<System.Collections.Generic.List<DocPrintListItem>> YishenList(
[FromQuery] string name = null,
[FromQuery] int pageSize = 10,
[FromQuery] int pageIndex = 0)
=> await _wy.YishenListAsync(name, UserId(), SchSerID(), pageSize, pageIndex);
/// <summary>全部文印列表(旧 totalList)。</summary>
[HttpGet("totalList")]
public async Task<System.Collections.Generic.List<DocPrintListItem>> TotalList(
[FromQuery] string name = null,
[FromQuery] string stateid = null,
[FromQuery] string starttime = null,
[FromQuery] string endtime = null,
[FromQuery] int pageSize = 10,
[FromQuery] int pageIndex = 0)
=> await _wy.TotalListAsync(name, stateid, starttime, endtime, SchSerID(), pageSize, pageIndex);
/// <summary>获取当前一条文印详情(旧 MyListtoOne;附件明细并入条目 tempresultfile)。</summary>
[HttpGet("MyListtoOne")]
public async Task<System.Collections.Generic.List<DocPrintDetailItem>> MyListtoOne([FromQuery] int serID)
=> await _wy.MyListtoOneAsync(serID, SchSerID());
/// <summary>审核确认(旧 shenheok)。</summary>
[HttpPost("shenheok")]
public async Task<string> ShenheOk([FromBody] FlowTranctDto model)
{
if (model == null) throw new DException("参数错误!");
model.actionuserid = UserContextHolder.Current?.Id ?? 0;
return await _wy.ShenheOkAsync(model);
}
/// <summary>领取确认(旧 AffirmApplyforOk)。</summary>
[HttpPost("AffirmApplyforOk")]
public async Task<string> AffirmApplyforOk([FromBody] FlowTranctDto model)
{
if (model == null) throw new DException("参数错误!");
model.actionuserid = UserContextHolder.Current?.Id ?? 0;
return await _wy.AffirmApplyforOkAsync(model);
}
/// <summary>删除文印(旧 Delete)。</summary>
[HttpGet("Delete")]
public async Task Delete([FromQuery] string ids)
{
if (string.IsNullOrWhiteSpace(ids)) throw new DException("ids is not null");
await _wy.DeleteAsync(ids);
}
/// <summary>绑定流程(旧 Bindflowinfo)。</summary>
[HttpGet("Bindflowinfo")]
public async Task<System.Collections.Generic.List<BindFlowOption>> Bindflowinfo([FromQuery] int flowType)
=> await _wy.BindflowinfoAsync(SchSerID(), flowType);
/// <summary>查询指派(旧 GetUserZhipai)。</summary>
[HttpGet("GetUserZhipai")]
public async Task<string> GetUserZhipai([FromQuery] int? flowid, [FromQuery] int? flowstepid, [FromQuery] string type)
=> await _wy.GetUserZhipaiAsync(UserContextHolder.Current?.Id ?? 0, SchSerID(), flowid, flowstepid, type);
/// <summary>文印数量按月统计(旧 List1)。</summary>
[HttpGet("List1")]
public async Task<System.Collections.Generic.List<MonthStatItem>> List1([FromQuery] string starttime = null, [FromQuery] string endtime = null)
=> await _wy.List1Async(starttime, endtime, SchSerID());
/// <summary>文印详情统计列表(旧 List2)。</summary>
[HttpGet("List2")]
public async Task<System.Collections.Generic.List<DocDetailStatItem>> List2(
[FromQuery] string starttime = null, [FromQuery] string endtime = null,
[FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0)
=> await _wy.List2Async(starttime, endtime, SchSerID(), pageSize, pageIndex);
/// <summary>文印数量按用户排名(旧 List3)。</summary>
[HttpGet("List3")]
public async Task<System.Collections.Generic.List<UserRankItem>> List3(
[FromQuery] string starttime = null, [FromQuery] string endtime = null,
[FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0)
=> await _wy.List3Async(starttime, endtime, SchSerID(), pageSize, pageIndex);
/// <summary>按月份统计明细(旧 List4)。</summary>
[HttpGet("List4")]
public async Task<System.Collections.Generic.List<StatListItem>> List4(
[FromQuery] int year, [FromQuery] int month,
[FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0)
=> await _wy.List4Async(year, month, SchSerID(), pageSize, pageIndex);
/// <summary>按用户统计明细(旧 List5)。</summary>
[HttpGet("List5")]
public async Task<System.Collections.Generic.List<StatListItem>> List5(
[FromQuery] string userid = null, [FromQuery] string starttime = null, [FromQuery] string endtime = null,
[FromQuery] int pageSize = 10, [FromQuery] int pageIndex = 0)
=> await _wy.List5Async(userid, starttime, endtime, SchSerID(), pageSize, pageIndex);
// 延后批次:ExceltotalList / Exceldaochu1 / Exceldaochu2 / Exceldaochu3(Excel 导出,方案待定)
#endregion
private static string SchSerID() => UserContextHolder.Current.SchSerID;
private static string UserId() => UserContextHolder.Current?.Id.ToString() ?? "0";
}
}
@@ -0,0 +1,94 @@
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/LoginUserController,路由 /api/GetUserMod 等)。
/// 注意:Java 版路由以大写开头(GetUserMod / GetUserInfo / Logout),此处保持 URL 大小写一致。
/// </summary>
[ApiController]
[Route(ApiRoutes.ApiPath)]
public class LoginUserController : ControllerBase
{
private readonly IOpenApiUserClient _openApiUserClient;
public LoginUserController(IOpenApiUserClient openApiUserClient)
{
_openApiUserClient = openApiUserClient;
}
/// <summary>
/// 获取当前用户可见的应用模块(对应 Java GET /api/GetUserMod,
/// 内部调用 YschBaseOpenApi.getUserModule(id, schoolId))。
/// </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/GetUserInfo,
/// Java 版直接返回认证切面注入的 authUserWithDept;未登录时为 null)。
/// 返回新框架用户上下文解析的完整 BoaUser(含 School/CurrentRole/RoleIds),与 Java 返回结构一致。
/// </summary>
[HttpGet("GetUserInfo")]
public BoaUser GetUserInfo()
{
return BoaUserContext.Get();
}
/// <summary>
/// 判断是否登录(对应 Java GET /api/checkLogin):
/// 已登录 → 重定向到运维工作台 /work-view.html(兼容 context-path,.NET 用相对路径);
/// 未登录 → 401 + {"code":401,"msg":"未登录"}(通常 CAS 拦截器会先行 302 到 CAS 登录页)。
/// </summary>
[HttpGet("checkLogin")]
public IActionResult CheckLogin()
{
if (BoaUserContext.Get()?.Id > 0)
{
// 对应 Java request.getContextPath() + "/work-view.html"(.NET 无 contextPath,用根相对路径)
return Redirect("/work-view.html");
}
return new ContentResult
{
Content = "{\"code\":401,\"msg\":\"未登录\"}",
ContentType = "application/json;charset=UTF-8",
StatusCode = StatusCodes.Status401Unauthorized
};
}
/// <summary>
/// 登出(对应 Java GET /api/Logout:session.invalidate())。
/// 清除本地 Cookie 认证(新框架 YhCookie 方案)+ 可选跳转到 CAS 单点登出(CasServerUrlPrefix + logout)。
/// TODO:按需启用单点登出(去 yh:sso:cas-server-url-prefix + logout 退出所有 SSO 应用)。
/// </summary>
[HttpGet("Logout")]
public async Task<IActionResult> Logout()
{
// 清除本地 Cookie 登录态(对应 session.invalidate())
await HttpContext.SignOutAsync(YhAuthDefaults.CookieScheme);
// 仅在已启用 Session 中间件时再清一份
if (HttpContext.Features.Get<ISessionFeature>() != null)
{
HttpContext.Session.Clear();
}
return Ok();
}
}
}
@@ -0,0 +1,79 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
using System.Linq;
using System.Threading.Tasks;
using YhCore.Common.Result;
namespace YhCore.WebApi.Filters
{
/// <summary>
/// 排除统一包装特性:标注在 Controller 或 Action 上,该处返回值不参与全局包装。
/// 适用:CAS 协议端点(XML/跳转)、文件下载、已手工构造响应体的接口。
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class NoWrapResultAttribute : Attribute, IFilterMetadata
{
}
/// <summary>
/// 全局统一响应包装过滤器:所有 Controller 的 ObjectResult 正常返回
/// 自动包装为 ApiResult{code=0, msg="", data=...}。
/// 规则:
/// - [NoWrapResult] 标注的 Controller/Action 跳过;
/// - 非 2xx 状态码(参数校验 400 等)不包装,保持错误结构 {code,msg,data,timeNow};
/// - 已是 ApiResult&lt;T&gt; 的返回值不重复包装(防双层嵌套);
/// - ContentResult / Redirect / File 等非对象结果不包装(checkLogin 401、OAuth/CAS 协议端点等);
/// - null 返回值包装为 {"code":0,"msg":"","data":null}(原为 204)。
/// </summary>
public class GlobalWrapResultFilter : IAsyncResultFilter
{
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
// 1) 显式排除
if (context.Filters.Any(f => f is NoWrapResultAttribute))
{
await next();
return;
}
// 2) 只包装对象结果
if (context.Result is ObjectResult objectResult)
{
var statusCode = objectResult.StatusCode ?? StatusCodes.Status200OK;
var isErrorStatus = statusCode < 200 || statusCode >= 300;
if (!isErrorStatus && !IsApiResult(objectResult.Value))
{
context.Result = new ObjectResult(ApiResult<object>.Ok(objectResult.Value))
{
StatusCode = StatusCodes.Status200OK
};
}
}
else if (context.Result is OkResult || context.Result is EmptyResult)
{
// Ok()(200 空 body)/ EmptyResult(204)→ 统一包装为 {"code":0,"msg":"","data":null}
context.Result = new ObjectResult(ApiResult<object>.Ok())
{
StatusCode = StatusCodes.Status200OK
};
}
await next();
}
/// <summary>值是否已是 ApiResult&lt;T&gt;(防双层包装)</summary>
private static bool IsApiResult(object value)
{
if (value == null)
{
return false;
}
var type = value.GetType();
return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ApiResult<>);
}
}
}
@@ -0,0 +1,334 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using Newtonsoft.Json;
using Yh.Platform.Abstractions;
using YhCore.Common.UserContext;
using YhCore.Entity;
using YhCore.Repository;
namespace YhCore.WebApi.Filters
{
/// <summary>
/// 系统审计日志过滤器(对应 Java SysOperationLogAspect)。
/// 全量拦截所有 Controller Action(不依赖标注),每次请求后异步写入 sys_operation_log 表。
/// 与 Java 对齐的行为:
/// - 排除日志查询控制器自身(防递归刷屏);
/// - 成功路径记录响应 GZIP + 业务状态码(0 成功 / 非 0 失败);
/// - 异常路径记录错误码 + 异常堆栈摘要(截断 8000),异常继续向外传播;
/// - 落库失败不影响主流程(审计不阻断业务)。
/// 注:请求/响应脱敏(Java SensitiveDataUtil)未实现,仅做长度截断,后续可接入。
/// </summary>
public class SysOperationLogFilter : IAsyncActionFilter, IAsyncResultFilter
{
private const string AuditEntityKey = "__sys_operation_log_entity";
private const string AuditStopwatchKey = "__sys_operation_log_stopwatch";
private const int MaxResponseSize = 10240; // 响应 JSON 截断长度(对齐 Java DEFAULT_MAX_RESPONSE_SIZE)
private const int MaxErrorMsg = 8000; // 异常信息截断长度(对齐 Java MAX_ERROR_MSG)
private const int MaxParamsSize = 4096; // 请求参数截断长度(Java 无此限制,此处防超大请求体)
/// <summary>排除的控制器(日志查询自身,防递归)</summary>
private static readonly string[] ExcludeControllers =
{
"YhCore.WebApi.Controllers.Api.LogController",
};
private static readonly string[] DeleteKeywords = { "delete", "del", "remove" };
private static readonly string[] CreateKeywords = { "add", "create", "insert", "save", "import", "upload" };
private readonly IRepository<SysOperationLog> _repo;
public SysOperationLogFilter(IRepository<SysOperationLog> repo)
{
_repo = repo;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
if (IsExcluded(context))
{
await next();
return;
}
var entity = BuildEntity(context);
var stopwatch = Stopwatch.StartNew();
context.HttpContext.Items[AuditEntityKey] = entity;
context.HttpContext.Items[AuditStopwatchKey] = stopwatch;
var executed = await next();
stopwatch.Stop();
entity.CostTime = stopwatch.ElapsedMilliseconds;
if (executed.Exception != null)
{
// 异常路径:Result 过滤器不会执行,在此记录错误码与摘要后放行(异常继续传播,由 GlobalExceptionMiddleware 兜底)
entity.StatusCode = ResolveErrorCode(executed.Exception);
entity.ErrorMsg = BuildErrorMsg(executed.Exception);
await SaveQuietlyAsync(entity);
}
// 成功路径:StatusCode / ResponseData 由 OnResultExecutionAsync 填充并落库
}
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next)
{
if (!context.HttpContext.Items.TryGetValue(AuditEntityKey, out var raw) || raw is not SysOperationLog entity)
{
await next();
return;
}
if (context.HttpContext.Items.TryGetValue(AuditStopwatchKey, out var sw) && sw is Stopwatch stopwatch)
{
entity.CostTime = stopwatch.ElapsedMilliseconds;
}
// 从结果对象提取业务状态码与响应体
switch (context.Result)
{
case ObjectResult objectResult:
entity.StatusCode = ResolveStatusCode(objectResult.Value);
entity.ResponseData = BuildResponseData(objectResult.Value);
break;
case OkResult:
case EmptyResult:
case StatusCodeResult statusCodeResult when statusCodeResult.StatusCode >= 200 && statusCodeResult.StatusCode < 300:
entity.StatusCode = ErrorCode.Success.Code;
break;
default:
// 非对象结果(重定向/文件等)仅记 200 默认成功,不序列化响应体
entity.StatusCode = ErrorCode.Success.Code;
break;
}
await next();
await SaveQuietlyAsync(entity);
}
// ==================== 构建 ====================
private static bool IsExcluded(ActionExecutingContext context)
{
if (context.ActionDescriptor is not ControllerActionDescriptor descriptor)
{
return true;
}
var fullName = descriptor.ControllerTypeInfo.FullName;
return ExcludeControllers.Any(c => string.Equals(fullName, c, StringComparison.Ordinal));
}
private static SysOperationLog BuildEntity(ActionExecutingContext context)
{
var descriptor = (ControllerActionDescriptor)context.ActionDescriptor;
var http = context.HttpContext;
var request = http.Request;
var user = UserContextHolder.Current;
var uri = request.Path.HasValue ? request.Path.Value : "";
var httpMethod = request.Method ?? "";
var actionName = descriptor.ActionName ?? "";
var (level, operationType) = ResolveLevelAndType(httpMethod, uri, actionName);
return new SysOperationLog
{
CreateTime = DateTime.Now,
UserId = user?.Id ?? 0,
UserName = string.IsNullOrEmpty(user?.UserName) ? "anonymous" : user.UserName,
SchId = user?.SchoolId,
RequestIp = http.Connection.RemoteIpAddress?.ToString() ?? "",
RequestUri = uri,
RequestMethod = httpMethod,
ClassMethod = $"{descriptor.ControllerTypeInfo.FullName}.{actionName}",
LogLevel = level,
OperationType = operationType,
UserAgent = request.Headers["User-Agent"].ToString(),
RequestParams = BuildRequestParams(context),
StatusCode = ErrorCode.Success.Code
};
}
/// <summary>请求参数序列化(过滤基础设施类型,对齐 Java buildRequestParams)</summary>
private static string BuildRequestParams(ActionExecutingContext context)
{
var args = context.ActionArguments.Values
.Where(v => v != null && !IsInfrastructureType(v))
.ToList();
if (args.Count == 0)
{
return "";
}
try
{
var json = JsonConvert.SerializeObject(args);
return json.Length > MaxParamsSize
? json.Substring(0, MaxParamsSize) + "...[截断]"
: json;
}
catch
{
return "";
}
}
private static bool IsInfrastructureType(object value)
{
var type = value.GetType();
return typeof(IFormFile).IsAssignableFrom(type)
|| type == typeof(CancellationToken)
|| typeof(HttpRequest).IsAssignableFrom(type)
|| typeof(HttpResponse).IsAssignableFrom(type);
}
/// <summary>响应 JSON → GZIP 压缩(对齐 Java fillResponse + GzipUtil.compress)</summary>
private static byte[] BuildResponseData(object value)
{
if (value == null)
{
return null;
}
try
{
var json = JsonConvert.SerializeObject(value);
if (json.Length > MaxResponseSize)
{
json = json.Substring(0, MaxResponseSize) + "...[截断]";
}
var bytes = Encoding.UTF8.GetBytes(json);
using var output = new MemoryStream();
using (var gzip = new GZipStream(output, CompressionLevel.Fastest, true))
{
gzip.Write(bytes, 0, bytes.Length);
}
return output.ToArray();
}
catch
{
return null;
}
}
/// <summary>业务状态码:返回值带 int Code 属性则取之(ApiResult 等),否则默认 0 成功</summary>
private static int ResolveStatusCode(object value)
{
if (value == null)
{
return ErrorCode.Success.Code;
}
var codeProperty = value.GetType().GetProperty("Code", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
if (codeProperty != null && codeProperty.PropertyType == typeof(int))
{
try
{
return (int)codeProperty.GetValue(value);
}
catch
{
// 忽略取值异常,回落默认成功
}
}
return ErrorCode.Success.Code;
}
/// <summary>异常错误码:DException 取业务码,其余取平台兜底码 6(对齐 Java resolveErrorCode)</summary>
private static int ResolveErrorCode(Exception exception)
{
if (exception is DException dex && dex.Code != null)
{
return dex.Code.Code;
}
return ErrorCode.InternalServerError.Code;
}
/// <summary>异常信息摘要:message + 堆栈(截断 8000,对齐 Java buildErrorMsg)</summary>
private static string BuildErrorMsg(Exception exception)
{
var msg = exception.Message;
var head = string.IsNullOrWhiteSpace(msg) ? exception.GetType().Name : msg;
var text = head + Environment.NewLine + exception.StackTrace;
return text.Length > MaxErrorMsg ? text.Substring(0, MaxErrorMsg) : text;
}
/// <summary>日志级别/类型推断(复刻 Java OperationLogResolver):GET=查询、DELETE=删除、关键字兜底、其余更新</summary>
private static (int level, string type) ResolveLevelAndType(string httpMethod, string uri, string actionName)
{
var method = string.IsNullOrEmpty(httpMethod) ? "" : httpMethod.ToUpperInvariant();
if (method == "")
{
return (2, "UPDATE");
}
if (method == "GET")
{
return (3, "QUERY");
}
if (method == "DELETE")
{
return (1, "DELETE");
}
var text = (uri ?? "").ToLowerInvariant() + " " + (actionName ?? "").ToLowerInvariant();
if (ContainsAny(text, DeleteKeywords))
{
return (1, "DELETE");
}
if (ContainsAny(text, CreateKeywords))
{
return (2, "CREATE");
}
return (2, "UPDATE");
}
private static bool ContainsAny(string text, IEnumerable<string> keywords)
{
foreach (var keyword in keywords)
{
if (text.Contains(keyword, StringComparison.Ordinal))
{
return true;
}
}
return false;
}
/// <summary>异步落库,失败不影响主流程(对齐 Java 操作日志不阻断业务)</summary>
private async Task SaveQuietlyAsync(SysOperationLog entity)
{
try
{
await _repo.AddAsync(entity);
await _repo.SaveChangesAsync();
}
catch
{
// 忽略:落库失败仅影响审计,不影响接口结果
}
}
}
}
@@ -0,0 +1,141 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using log4net;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using YhCore.Common.UserContext;
namespace YhCore.WebApi.Middleware
{
/// <summary>
/// 接口访问日志中间件(唯一日志出口,方案 §6.2)。
/// 记录:请求时间 / 渠道 / 路径与方法 / Query / Body(敏感字段脱敏)/
/// 客户端 IP / 当前用户 / 响应状态码 / 耗时 / 异常摘要。
/// 其余一切日志(SQL、框架、业务 Debug)均已关闭。
/// </summary>
public class ApiAccessLogMiddleware
{
private static readonly ILog Log = LogManager.GetLogger(typeof(ApiAccessLogMiddleware));
/// <summary>需要脱敏的入参 key(对应 Java SensitiveDataUtil)</summary>
private static readonly string[] SensitiveKeys = { "password", "pwd", "sign", "token", "secret" };
private readonly RequestDelegate _next;
private readonly ILogger<ApiAccessLogMiddleware> _fallbackLogger;
public ApiAccessLogMiddleware(RequestDelegate next, ILogger<ApiAccessLogMiddleware> fallbackLogger)
{
_next = next;
_fallbackLogger = fallbackLogger;
}
public async Task InvokeAsync(HttpContext context)
{
var sw = Stopwatch.StartNew();
var request = context.Request;
// 请求体读取(可重复读)
var body = await ReadBodyAsync(request);
string exceptionSummary = null;
try
{
await _next(context);
}
catch (Exception ex)
{
exceptionSummary = ex.Message;
throw; // 异常继续向上,由 GlobalExceptionMiddleware 处理响应
}
finally
{
sw.Stop();
WriteLog(context, body, sw.ElapsedMilliseconds, exceptionSummary);
}
}
private void WriteLog(HttpContext context, string body, long elapsedMs, string exceptionSummary)
{
var request = context.Request;
var user = UserContextHolder.Current;
var sb = new StringBuilder();
sb.Append("time=").Append(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"));
sb.Append(" | channel=").Append(ResolveChannel(request.Path.Value));
sb.Append(" | method=").Append(request.Method);
sb.Append(" | path=").Append(request.Path.Value);
sb.Append(" | query=").Append(MaskSensitive(request.QueryString.Value));
if (!string.IsNullOrEmpty(body))
{
sb.Append(" | body=").Append(MaskSensitive(body));
}
sb.Append(" | ip=").Append(context.Connection.RemoteIpAddress?.ToString() ?? "-");
sb.Append(" | user=").Append(user?.Id.ToString() ?? "-");
sb.Append(" | status=").Append(context.Response.StatusCode);
sb.Append(" | cost=").Append(elapsedMs).Append("ms");
if (!string.IsNullOrEmpty(exceptionSummary))
{
sb.Append(" | exception=").Append(exceptionSummary);
}
Log.Info(sb.ToString());
}
/// <summary>渠道解析:/api → api、/mobiapi → mobiapi、/oapi → oapi、其余 -</summary>
private static string ResolveChannel(string path)
{
if (string.IsNullOrEmpty(path)) return "-";
if (path.StartsWith("/api/", StringComparison.OrdinalIgnoreCase)) return "api";
if (path.StartsWith("/mobiapi/", StringComparison.OrdinalIgnoreCase)) return "mobiapi";
if (path.StartsWith("/oapi/", StringComparison.OrdinalIgnoreCase)) return "oapi";
return "-";
}
/// <summary>对查询串/请求体做敏感字段脱敏(key=value 中的 value 替换为 ***)</summary>
private static string MaskSensitive(string raw)
{
if (string.IsNullOrEmpty(raw)) return raw;
var parts = raw.Split('&');
for (var i = 0; i < parts.Length; i++)
{
var eq = parts[i].IndexOf('=');
if (eq <= 0) continue;
var key = parts[i].Substring(0, eq);
if (SensitiveKeys.Any(k => key.Contains(k, StringComparison.OrdinalIgnoreCase)))
{
parts[i] = key + "=***";
}
}
return string.Join("&", parts);
}
private static async Task<string> ReadBodyAsync(HttpRequest request)
{
if (!request.Body.CanRead
|| !request.ContentLength.HasValue
|| request.ContentLength <= 0)
{
return null;
}
request.EnableBuffering();
try
{
using var reader = new StreamReader(request.Body, Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true);
var body = await reader.ReadToEndAsync();
request.Body.Position = 0; // 复位,供后续中间件/模型绑定读取
return body.Length > 2048 ? body.Substring(0, 2048) + "...(truncated)" : body;
}
catch
{
return null;
}
}
}
}
@@ -0,0 +1,90 @@
using System;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Yh.Platform.AppAuth;
using Yh.Platform.Auth;
namespace YhCore.WebApi.Middleware
{
/// <summary>
/// 应用间签名校验中间件(对应 Java yh-app-auth-spring-boot-starter,仅对 /oapi 渠道生效)。
/// 基于新框架 <see cref="AppAuthVerifier"/> 完成请求级签名校验(X-App-Key / X-Timestamp / X-Signature
/// 三件套 + 时间戳过期 + 防重放);Enabled=false(yh:app-auth)时跳过。
/// 签名协议见 yh-platform-net contract/sign-protocol.md。
/// </summary>
public class AppAuthMiddleware
{
private readonly RequestDelegate _next;
private readonly AppAuthVerifier _verifier;
private readonly AppAuthOptions _options;
public AppAuthMiddleware(RequestDelegate next, AppAuthVerifier verifier, IOptions<AppAuthOptions> options)
{
_next = next;
_verifier = verifier;
_options = options.Value;
}
public async Task InvokeAsync(HttpContext context)
{
if (_options.Enabled)
{
var path = context.Request.Path.Value ?? string.Empty;
if (path.StartsWith("/oapi", StringComparison.OrdinalIgnoreCase))
{
var appKey = GetHeader(context, "X-App-Key");
var timestamp = GetHeader(context, "X-Timestamp");
var signature = GetHeader(context, "X-Signature");
var nonce = GetHeader(context, "X-Nonce");
if (string.IsNullOrEmpty(appKey) || string.IsNullOrEmpty(timestamp) || string.IsNullOrEmpty(signature))
{
await RejectAsync(context, "缺少签名头(X-App-Key / X-Timestamp / X-Signature)");
return;
}
// canonical target:path + 排序 query(对齐 Java AppSignInterceptor,挂根路径无 basePath)
var canonicalTarget = SignCanonical.BuildTarget(
new Uri(context.Request.Scheme + "://" + context.Request.Host + context.Request.Path + context.Request.QueryString),
basePath: null);
var ok = await _verifier.VerifyAsync(
appKey, timestamp, signature,
context.Request.Method, canonicalTarget,
string.IsNullOrEmpty(nonce) ? null : nonce,
context.RequestAborted);
if (!ok)
{
await RejectAsync(context, "签名校验失败(签名错误 / 时间戳过期 / 重复请求)");
return;
}
// 验签通过:写入调用方应用身份(对齐 LoginUser.AppKey,业务可经 UserContextHolder 读取)
UserContextHolder.Set(new LoginUser { AppKey = appKey });
}
}
await _next(context);
}
private static async Task RejectAsync(HttpContext context, string message)
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
context.Response.ContentType = "application/json; charset=utf-8";
await context.Response.WriteAsync(JsonSerializer.Serialize(new
{
code = 4,
msg = message,
data = (object)null,
}));
}
private static string GetHeader(HttpContext context, string name)
{
return context.Request.Headers.TryGetValue(name, out var value) ? value.ToString() : null;
}
}
}
@@ -0,0 +1,79 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Yh.Platform.Abstractions;
namespace YhCore.WebApi.Middleware
{
/// <summary>
/// 全局异常中间件(对应 Java @ControllerAdvice)。
/// 捕获未处理异常 → 返回统一错误响应 {code, msg, data, timeNow}(对齐 bschclassmanage ValidationExceptionHandler
/// 与平台 GlobalExceptionHandler);异常摘要由接口访问日志(ApiAccessLogMiddleware)统一记录,本中间件不单独输出日志(方案 §6.2 / §6.4)。
/// 规则(对齐 Java):
/// DException(业务异常,含 /oapi 业务失败信封)→ HTTP 200 + {code, msg, data, timeNow}
/// JSON 解析失败 / 请求体异常 → HTTP 400 + {code:1, msg:"请求体(JSON)解析失败: ...", data:null, timeNow}
/// 未捕获异常(平台兜底) → HTTP 200 + {code:6(INTERNAL_SERVER_ERROR), msg:"内部服务错误", data:null, timeNow}
/// </summary>
public class GlobalExceptionMiddleware
{
private readonly RequestDelegate _next;
public GlobalExceptionMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (DException dex)
{
// 业务异常(含 /oapi 信封失败)→ 200 + {code, msg, data, timeNow}(code 原样透传,对齐 Java handleDException)
await WriteJsonAsync(context, StatusCodes.Status200OK, new
{
code = dex.Code?.Code ?? ErrorCode.Error.Code,
msg = dex.Message,
data = dex.EnvelopeData,
timeNow = TimeNow()
});
}
catch (BadHttpRequestException ex)
{
// @RequestBody 反序列化失败(JSON 格式错 / 字段类型不符)→ 400 + 真实原因透出(对齐 Java handleNotReadable)
await WriteJsonAsync(context, StatusCodes.Status400BadRequest, new
{
code = ErrorCode.Error.Code,
msg = $"请求体(JSON)解析失败: {ex.Message}",
data = (object)null,
timeNow = TimeNow()
});
}
catch (Exception)
{
// 未捕获异常 → 平台兜底:200 + code:6 内部服务错误(对齐 Java GlobalExceptionHandler.handleException)
await WriteJsonAsync(context, StatusCodes.Status200OK, new
{
code = ErrorCode.InternalServerError.Code,
msg = ErrorCode.InternalServerError.Msg,
data = (object)null,
timeNow = TimeNow()
});
}
}
private static async Task WriteJsonAsync(HttpContext context, int statusCode, object body)
{
context.Response.StatusCode = statusCode;
context.Response.ContentType = "application/json; charset=utf-8";
await context.Response.WriteAsync(JsonConvert.SerializeObject(body));
}
/// <summary>时间戳(对齐 Java timeNow 格式 yyyy-MM-dd HH:mm:ss)</summary>
private static string TimeNow() => DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
}
}
@@ -0,0 +1,128 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Yh.Platform.Auth;
namespace YhCore.WebApi.Middleware
{
/// <summary>
/// SSO 路径保护中间件(对应 Java yh-auth-spring-boot-starter 的 /api/* 保护行为)。
/// 规则(对齐 yh:sso 配置节):
/// - yh:sso:enabled=false(默认):所有路径匿名放行,不影响开发联调;
/// - yh:sso:enabled=true:匹配 /api/* / /mobiapi/* / 根路径(/, 浏览器 SSO 入口) 且未认证的请求
/// → Challenge → Yh.Platform.Auth CasAuthenticationHandler 302 重定向到 CAS 登录页;
/// - CAS 回调路径(yh:sso:signInCallbackPath,默认 /signin-cas)未认证时同样 Challenge:
/// 直接访问回调(无 ticket)进入登录流程;携带失效/已消费 ticket 的回调则返回 401 错误页,
/// 不再次 Challenge——避免“回调→登录→新票据→再失败”无限重定向循环(自动登录型 CAS 下必现);
/// 回调校验成功(已认证 + 302)则短路管道,防止 MVC 端点匹配 404 覆盖 handler 的回跳 302;
/// - Swagger 路径始终放行,方便本地接口调试;
/// - /oapi 开放渠道始终放行:其准入由 AppAuthMiddleware 签名验签负责(X-App-Key / X-Timestamp /
/// X-Signature 票据),无需用户登录态,SSO 登录保护永不覆盖该渠道(即使 yh:sso:urlPattern 误配
/// /oapi/* 也不拦截),保证应用间调用不受浏览器登录流程影响。
/// 调用方:Startup.Configure 中 UseAuthentication + UseYhUserContext 之后、UseAuthorization 之前。
/// </summary>
public class SsoProtectionMiddleware
{
private readonly RequestDelegate _next;
private readonly IConfiguration _configuration;
public SsoProtectionMiddleware(RequestDelegate next, IConfiguration configuration)
{
_next = next;
_configuration = configuration;
}
public async Task InvokeAsync(HttpContext context)
{
var sso = _configuration.GetSection("yh:sso");
var isSwaggerPath = context.Request.Path.StartsWithSegments("/swagger");
var isOapiPath = context.Request.Path.StartsWithSegments("/oapi");
var isRootEntry = context.Request.Path == "/";
var isCasCallback = context.Request.Path.StartsWithSegments(sso["signInCallbackPath"] ?? "/signin-cas");
// 已认证(含 Cookie 恢复登录态)直接放行
if (context.User?.Identity?.IsAuthenticated == true)
{
// 回调路径刚完成票据校验:CasAuthenticationHandler 已写入 Cookie 并 Response.Redirect(302 + Location),
// 但 handler 返回 NoResult 不短路管道——若继续 _next 走 MVC 端点匹配,/signin-cas 无对应 action,
// 会返回 404 覆盖 302,浏览器看到"登录成功却 404"。此处已认证 + 已 302 即短路,保住回跳响应。
if (isCasCallback && context.Response.StatusCode == 302 &&
!string.IsNullOrEmpty(context.Response.Headers["Location"]))
{
return;
}
await _next(context);
return;
}
// oapi 开放渠道:无需用户登录,准入由 AppAuthMiddleware 签名验签负责(票据 = 签名头),
// 未带票据的请求由 AppAuthMiddleware 返回 401(而非 SSO 302),保证直接访问语义。
if (isOapiPath)
{
await _next(context);
return;
}
var enabled = sso["enabled"] != null
? bool.TryParse(sso["enabled"], out var en) && en
: _configuration.GetValue("Yh:Sso:Enabled", false);
if (enabled && !isSwaggerPath &&
(isRootEntry || isCasCallback || IsProtectedPath(context.Request.Path.Value, sso)))
{
// 请求携带 ticket 却仍未认证 → 说明 CAS 票据校验失败(一次性票据已被消费/过期,
// 或 service 与服务端登记不一致)。此时若再次 Challenge,自动登录型 CAS 会立刻签发
// 新票据跳回 → 再次失败 → 无限重定向循环。直接返回 401 错误页(带重新登录入口)打破循环。
if (context.Request.Query.ContainsKey("ticket"))
{
context.Response.StatusCode = 401;
context.Response.ContentType = "text/html; charset=utf-8";
await context.Response.WriteAsync(
"<!DOCTYPE html><html lang=\"zh\"><head><meta charset=\"utf-8\"><title>登录失效</title></head>" +
"<body style=\"font-family:system-ui;text-align:center;padding-top:80px\">" +
"<h2>登录票据校验失败或已过期</h2>" +
"<p>CAS 登录票据为一次性凭证,刷新或重复使用同一地址会导致校验失败。</p>" +
"<p><a href=\"/\">重新登录</a></p></body></html>");
return;
}
// 未认证 → Challenge → 302 重定向 CAS 登录页(ticket 校验与回跳由新框架 CasAuthenticationHandler 处理)
await context.ChallengeAsync(YhAuthDefaults.CasScheme);
return;
}
await _next(context);
}
/// <summary>路径是否命中保护模式(yh:sso:urlPattern / yh:sso:mobiApiPattern,默认 /api/*、/mobiapi/*,通配后缀匹配,忽略大小写)</summary>
private static bool IsProtectedPath(string path, IConfigurationSection sso)
{
if (string.IsNullOrEmpty(path))
{
return false;
}
var urlPattern = sso["urlPattern"] ?? "/api/*";
var mobiPattern = sso["mobiApiPattern"] ?? "/mobiapi/*";
return MatchesPattern(path, urlPattern) || MatchesPattern(path, mobiPattern);
}
private static bool MatchesPattern(string path, string pattern)
{
if (string.IsNullOrEmpty(pattern) || string.IsNullOrEmpty(path))
{
return false;
}
// 形如 /api/* → 前缀匹配;否则精确匹配(忽略大小写)
if (pattern.EndsWith("/*", StringComparison.Ordinal))
{
var prefix = pattern.Substring(0, pattern.Length - 1); // 去掉 *
return path.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
}
return path.Equals(pattern, StringComparison.OrdinalIgnoreCase);
}
}
}

Some files were not shown because too many files have changed in this diff Show More