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>