diff --git "a/NewLife.Cube/Entity/\347\224\250\346\210\267\345\234\250\347\272\277.Biz.cs" "b/NewLife.Cube/Entity/\347\224\250\346\210\267\345\234\250\347\272\277.Biz.cs"
index cf427eef..650fe6e8 100644
--- "a/NewLife.Cube/Entity/\347\224\250\346\210\267\345\234\250\347\272\277.Biz.cs"
+++ "b/NewLife.Cube/Entity/\347\224\250\346\210\267\345\234\250\347\272\277.Biz.cs"
@@ -1,115 +1,112 @@
-using System;
-using System.Collections.Generic;
-using NewLife.Data;
+using NewLife.Data;
using XCode;
using XCode.Configuration;
-using XCode.Membership;
-namespace NewLife.Cube.Entity
+namespace NewLife.Cube.Entity;
+
+/// 用户在线
+public partial class UserOnline : Entity
{
- /// 用户在线
- public partial class UserOnline : Entity
+ #region 对象操作
+ static UserOnline()
{
- #region 对象操作
- static UserOnline()
- {
- // 累加字段,生成 Update xx Set Count=Count+1234 Where xxx
- var df = Meta.Factory.AdditionalFields;
- df.Add(__.Times);
- //df.Add(__.OnlineTime);
-
- var sc = Meta.SingleCache;
- if (sc.Expire < 20 * 60) sc.Expire = 20 * 60;
- sc.FindSlaveKeyMethod = k => Find(__.SessionID, k);
- sc.GetSlaveKeyMethod = e => e.SessionID;
-
- // 过滤器 UserModule、TimeModule、IPModule
- Meta.Modules.Add();
- Meta.Modules.Add();
- }
+ // 累加字段,生成 Update xx Set Count=Count+1234 Where xxx
+ var df = Meta.Factory.AdditionalFields;
+ df.Add(__.Times);
+ //df.Add(__.OnlineTime);
+
+ var sc = Meta.SingleCache;
+ if (sc.Expire < 20 * 60) sc.Expire = 20 * 60;
+ sc.FindSlaveKeyMethod = k => Find(__.SessionID, k);
+ sc.GetSlaveKeyMethod = e => e.SessionID;
+
+ // 过滤器 UserModule、TimeModule、IPModule
+ Meta.Modules.Add();
+ Meta.Modules.Add();
+ }
- /// 验证并修补数据,通过抛出异常的方式提示验证失败。
- /// 是否插入
- public override void Valid(Boolean isNew)
- {
- // 如果没有脏数据,则不需要进行任何处理
- if (!HasDirty) return;
+ /// 验证并修补数据,通过抛出异常的方式提示验证失败。
+ /// 是否插入
+ public override void Valid(Boolean isNew)
+ {
+ // 如果没有脏数据,则不需要进行任何处理
+ if (!HasDirty) return;
- // 截取长度
- CutField(_.Status, _.Page, _.Platform, _.OS, _.Device, _.Brower, _.NetType);
+ // 截取长度
+ CutField(_.Status, _.Page, _.Platform, _.OS, _.Device, _.Brower, _.NetType);
- // 建议先调用基类方法,基类方法会做一些统一处理
- base.Valid(isNew);
- }
+ // 建议先调用基类方法,基类方法会做一些统一处理
+ base.Valid(isNew);
+ }
- void CutField(params FieldItem[] fields)
+ void CutField(params FieldItem[] fields)
+ {
+ foreach (var field in fields)
{
- foreach (var field in fields)
+ var len = field.Length;
+ if (len > 0 && field.Type == typeof(String))
{
- var len = field.Length;
- if (len > 0 && field.Type == typeof(String))
+ var str = this[field.Name] as String;
+ if (!str.IsNullOrEmpty() && str.Length > len)
{
- var str = this[field.Name] as String;
- if (!str.IsNullOrEmpty() && str.Length > len)
- {
- str = str[..len];
- SetItem(field.Name, str);
- }
+ str = str[..len];
+ SetItem(field.Name, str);
}
}
}
- #endregion
+ }
+ #endregion
- #region 扩展属性
- #endregion
+ #region 扩展属性
+ #endregion
- #region 扩展查询
- /// 根据编号查找
- /// 编号
- /// 实体对象
- public static UserOnline FindByID(Int32 id)
- {
- if (id <= 0) return null;
+ #region 扩展查询
+ /// 根据编号查找
+ /// 编号
+ /// 实体对象
+ public static UserOnline FindByID(Int32 id)
+ {
+ if (id <= 0) return null;
- //// 实体缓存
- //if (Meta.Session.Count < 1000) return Meta.Cache.Find(e => e.ID == id);
+ //// 实体缓存
+ //if (Meta.Session.Count < 1000) return Meta.Cache.Find(e => e.ID == id);
- // 单对象缓存
- return Meta.SingleCache[id];
+ // 单对象缓存
+ return Meta.SingleCache[id];
- //return Find(_.ID == id);
- }
-
- /// 根据用户查找
- /// 用户
- /// 实体列表
- public static IList FindAllByUserID(Int32 userId)
- {
- //// 实体缓存
- //if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.UserID == userId);
+ //return Find(_.ID == id);
+ }
- return FindAll(_.UserID == userId);
- }
+ /// 根据用户查找
+ /// 用户
+ /// 实体列表
+ public static IList FindAllByUserID(Int32 userId)
+ {
+ //// 实体缓存
+ //if (Meta.Session.Count < 1000) return Meta.Cache.FindAll(e => e.UserID == userId);
- /// 根据会话查找
- /// 会话
- /// 是否从缓存查询
- /// 实体列表
- public static UserOnline FindBySessionID(String sessionId, Boolean cache = true)
- {
- if (sessionId.IsNullOrEmpty()) return null;
+ return FindAll(_.UserID == userId);
+ }
- if (cache)
- {
- //// 实体缓存
- //if (Meta.Session.Count < 1000) return Meta.Cache.Find(e => e.SessionID.EqualIgnoreCase(sessionId));
+ /// 根据会话查找
+ /// 会话
+ /// 是否从缓存查询
+ /// 实体列表
+ public static UserOnline FindBySessionID(String sessionId, Boolean cache = true)
+ {
+ if (sessionId.IsNullOrEmpty()) return null;
- return Meta.SingleCache.GetItemWithSlaveKey(sessionId) as UserOnline;
- }
+ if (cache)
+ {
+ //// 实体缓存
+ //if (Meta.Session.Count < 1000) return Meta.Cache.Find(e => e.SessionID.EqualIgnoreCase(sessionId));
- return Find(_.SessionID == sessionId);
+ return Meta.SingleCache.GetItemWithSlaveKey(sessionId) as UserOnline;
}
+ return Find(_.SessionID == sessionId);
+ }
+
/// 根据会话查找
/// 会话
/// 实体列表
@@ -122,52 +119,51 @@ public static IList FindAllBySessionID(String sessionId)
return FindAll(_.SessionID == sessionId);
}
- #endregion
-
- #region 高级查询
- /// 高级查询
- /// 用户
- /// 会话。Web的SessionID或Server的会话编号
- /// 创建时间开始
- /// 创建时间结束
- /// 关键字
- /// 分页参数信息。可携带统计和数据权限扩展查询等信息
- /// 实体列表
- public static IList Search(Int32 userId, String sessionId, DateTime start, DateTime end, String key, PageParameter page)
- {
- var exp = new WhereExpression();
+ #endregion
+
+ #region 高级查询
+ /// 高级查询
+ /// 用户
+ /// 会话。Web的SessionID或Server的会话编号
+ /// 创建时间开始
+ /// 创建时间结束
+ /// 关键字
+ /// 分页参数信息。可携带统计和数据权限扩展查询等信息
+ /// 实体列表
+ public static IList Search(Int32 userId, String sessionId, DateTime start, DateTime end, String key, PageParameter page)
+ {
+ var exp = new WhereExpression();
- if (userId >= 0) exp &= _.UserID == userId;
- if (!sessionId.IsNullOrEmpty()) exp &= _.SessionID == sessionId;
- exp &= _.CreateTime.Between(start, end);
- if (!key.IsNullOrEmpty()) exp &= _.Name.Contains(key) | _.SessionID.Contains(key) | _.Page.Contains(key) | _.Status.Contains(key) | _.CreateIP.Contains(key) | _.UpdateIP.Contains(key);
+ if (userId >= 0) exp &= _.UserID == userId;
+ if (!sessionId.IsNullOrEmpty()) exp &= _.SessionID == sessionId;
+ exp &= _.CreateTime.Between(start, end);
+ if (!key.IsNullOrEmpty()) exp &= _.Name.Contains(key) | _.SessionID.Contains(key) | _.Page.Contains(key) | _.Status.Contains(key) | _.CreateIP.Contains(key) | _.UpdateIP.Contains(key);
- return FindAll(exp, page);
- }
+ return FindAll(exp, page);
+ }
- //// Select Count(ID) as ID,SessionID From UserOnline Where CreateTime>'2020-01-24 00:00:00' Group By SessionID Order By ID Desc limit 20
- //private static readonly FieldCache _SessionIDCache = new FieldCache(nameof(SessionID))
- //{
- // //Where = _.CreateTime > DateTime.Today.AddDays(-30) & Expression.Empty
- //};
-
- ///// 获取会话列表,字段缓存10分钟,分组统计数据最多的前20种,用于魔方前台下拉选择
- /////
- //public static IDictionary GetSessionIDList() => _SessionIDCache.FindAllName();
- #endregion
-
- #region 业务操作
- ///
- /// 设置错误
- ///
- ///
- public void SetError(String error)
- {
- Status = error;
- LastError = DateTime.Now;
+ //// Select Count(ID) as ID,SessionID From UserOnline Where CreateTime>'2020-01-24 00:00:00' Group By SessionID Order By ID Desc limit 20
+ //private static readonly FieldCache _SessionIDCache = new FieldCache(nameof(SessionID))
+ //{
+ // //Where = _.CreateTime > DateTime.Today.AddDays(-30) & Expression.Empty
+ //};
+
+ ///// 获取会话列表,字段缓存10分钟,分组统计数据最多的前20种,用于魔方前台下拉选择
+ /////
+ //public static IDictionary GetSessionIDList() => _SessionIDCache.FindAllName();
+ #endregion
+
+ #region 业务操作
+ ///
+ /// 设置错误
+ ///
+ ///
+ public void SetError(String error)
+ {
+ Status = error;
+ LastError = DateTime.Now;
- SaveAsync(3_000);
- }
- #endregion
+ SaveAsync(3_000);
}
+ #endregion
}
\ No newline at end of file
diff --git a/NewLife.Cube/WebMiddleware/RunTimeMiddleware.cs b/NewLife.Cube/WebMiddleware/RunTimeMiddleware.cs
index 8a763260..6b7a85e4 100644
--- a/NewLife.Cube/WebMiddleware/RunTimeMiddleware.cs
+++ b/NewLife.Cube/WebMiddleware/RunTimeMiddleware.cs
@@ -22,7 +22,7 @@ public class RunTimeMiddleware
private readonly UserService _userService;
/// 会话提供者
- static readonly SessionProvider _sessionProvider = new SessionProvider();
+ static readonly SessionProvider _sessionProvider = new();
/// 实例化
///
@@ -48,8 +48,8 @@ public async Task Invoke(HttpContext ctx)
var ip = ctx.GetUserHost();
ManageProvider.UserHost = ip;
- //// 创建Session集合
- //var token = CreateSession(ctx);
+ // 创建Session集合
+ var (token, session) = CreateSession(ctx);
var inf = new RunTimeInfo();
ctx.Items[nameof(RunTimeInfo)] = inf;
@@ -65,14 +65,14 @@ public async Task Invoke(HttpContext ctx)
// 设计时收集执行的SQL语句
if (SysConfig.Current.Develop)
{
- inf.Sqls = new List();
+ inf.Sqls = [];
DAL.LocalFilter = s => inf.Sqls.Add(s);
}
// 日志控制,精确标注Web类型线程
WriteLogEventArgs.Current.IsWeb = true;
- UserOnline olt = null;
+ var online = session["Online"] as UserOnline;
var user = ManageProvider.User;
try
{
@@ -85,16 +85,17 @@ public async Task Invoke(HttpContext ctx)
var deviceId = FillDeviceId(ctx);
//var sessionId = token?.MD5_16() ?? ip;
var sessionId = deviceId;
- olt = _userService.SetWebStatus(sessionId, deviceId, p, userAgent, ua, user, ip);
+ online = _userService.SetWebStatus(online, sessionId, deviceId, p, userAgent, ua, user, ip);
//FillDeviceId(ctx, olt);
- ctx.Items["Cube_Online"] = olt;
+ session["Online"] = online;
+ ctx.Items["Cube_Online"] = online;
}
await _next.Invoke(ctx);
}
catch (Exception ex)
{
var uri = ctx.Request.GetRawUrl();
- olt?.SetError(ex.Message);
+ online?.SetError(ex.Message);
XTrace.Log.Error("[{0}]的错误[{1}] {2}", uri, ip, ctx.TraceIdentifier);
@@ -132,8 +133,7 @@ public async Task Invoke(HttpContext ctx)
///
public static String GetInfo(HttpContext ctx)
{
- var rtinf = ctx.Items[nameof(RunTimeInfo)] as RunTimeInfo;
- if (rtinf == null) return null;
+ if (ctx.Items[nameof(RunTimeInfo)] is not RunTimeInfo rtinf) return null;
var inf = String.Format(DbRunTimeFormat,
DAL.QueryTimes - rtinf.QueryTimes,
@@ -150,29 +150,30 @@ public static String GetInfo(HttpContext ctx)
return inf;
}
- //private static String CreateSession(HttpContext ctx)
- //{
- // // 准备Session
- // var ss = ctx.Session;
- // if (ss != null)
- // {
- // //var token = Request.Cookies["Token"];
- // var token = ss.GetString("Cube_Token");
- // if (token.IsNullOrEmpty())
- // {
- // token = Rand.NextString(16);
- // //Response.Cookies.Append("Token", token, new CookieOptions { });
- // ss.SetString("Cube_Token", token);
- // }
-
- // //Session = _sessionProvider.GetSession(ss.Id);
- // ctx.Items["Session"] = _sessionProvider.GetSession(token);
-
- // return token;
- // }
-
- // return null;
- //}
+ private static (String, IDictionary) CreateSession(HttpContext ctx)
+ {
+ // 准备Session
+ var ss = ctx.Session;
+ if (ss != null)
+ {
+ //var token = Request.Cookies["Token"];
+ var token = ss.GetString("Cube_Token");
+ if (token.IsNullOrEmpty())
+ {
+ token = Rand.NextString(16);
+ //Response.Cookies.Append("Token", token, new CookieOptions { });
+ ss.SetString("Cube_Token", token);
+ }
+
+ //Session = _sessionProvider.GetSession(ss.Id);
+ var session = _sessionProvider.GetSession(token);
+ ctx.Items["Session"] = session;
+
+ return (token, session);
+ }
+
+ return (null, null);
+ }
/// 忽略的后缀
public static String[] ExcludeSuffixes { get; set; } = new[] {
diff --git a/NewLife.CubeNC/Services/UserService.cs b/NewLife.CubeNC/Services/UserService.cs
index fb9ccc6f..962ef922 100644
--- a/NewLife.CubeNC/Services/UserService.cs
+++ b/NewLife.CubeNC/Services/UserService.cs
@@ -44,6 +44,7 @@ private void StartTimer()
#region 用户在线
/// 设置会话状态
+ ///
///
///
///
@@ -53,54 +54,58 @@ private void StartTimer()
///
///
///
- public UserOnline SetStatus(String sessionId, String deviceId, String page, String status, UserAgentParser userAgent, Int32 userid = 0, String name = null, String ip = null)
+ public UserOnline SetStatus(UserOnline online, String sessionId, String deviceId, String page, String status, UserAgentParser userAgent, Int32 userid = 0, String name = null, String ip = null)
{
+ if (online != null && online.SessionID != sessionId) online = null;
+
// LastError 设计缺陷,非空设计导致无法在插入中忽略
- var entity = UserOnline.GetOrAdd(sessionId, UserOnline.FindBySessionID, k => new UserOnline
+ online ??= UserOnline.GetOrAdd(sessionId, UserOnline.FindBySessionID, k => new UserOnline
{
SessionID = k,
LastError = new DateTime(1970, 1, 2),//MSSql不能使用1973年之前的日期
CreateIP = ip,
CreateTime = DateTime.Now
});
- //var entity = FindBySessionID(sessionid) ?? new UserOnline();
- //entity.SessionID = sessionid;
- entity.DeviceId = deviceId;
- entity.Page = page;
+ //var online = FindBySessionID(sessionid) ?? new UserOnline();
+ //online.SessionID = sessionid;
+ online.DeviceId = deviceId;
+ online.Page = page;
if (userAgent != null)
{
- entity.Platform = userAgent.Platform;
- entity.OS = userAgent.OSorCPU;
+ online.Platform = userAgent.Platform;
+ online.OS = userAgent.OSorCPU;
if (userAgent.Device.IsNullOrEmpty() || !userAgent.DeviceBuild.IsNullOrEmpty() && userAgent.DeviceBuild.Contains(userAgent.Device))
- entity.Device = userAgent.DeviceBuild;
+ online.Device = userAgent.DeviceBuild;
else
- entity.Device = userAgent.Device;
- entity.Brower = userAgent.Brower;
- entity.NetType = userAgent.NetType;
+ online.Device = userAgent.Device;
+ online.Brower = userAgent.Brower;
+ online.NetType = userAgent.NetType;
}
- if (!status.IsNullOrEmpty() || entity.LastError.AddMinutes(3) < DateTime.Now) entity.Status = status;
+ if (!status.IsNullOrEmpty() || online.LastError.AddMinutes(3) < DateTime.Now) online.Status = status;
- entity.Times++;
- if (userid > 0) entity.UserID = userid;
- if (!name.IsNullOrEmpty()) entity.Name = name;
+ online.Times++;
+ if (userid > 0) online.UserID = userid;
+ if (!name.IsNullOrEmpty()) online.Name = name;
- entity.Address = ip.IPToAddress();
+ online.Address = ip.IPToAddress();
// 累加在线时间
- entity.UpdateTime = DateTime.Now;
- entity.UpdateIP = ip;
- entity.OnlineTime = (Int32)(entity.UpdateTime - entity.CreateTime).TotalSeconds;
- entity.TraceId = DefaultSpan.Current?.TraceId;
- entity.SaveAsync(5_000);
+ online.UpdateTime = DateTime.Now;
+ online.UpdateIP = ip;
+ online.OnlineTime = (Int32)(online.UpdateTime - online.CreateTime).TotalSeconds;
+ online.TraceId = DefaultSpan.Current?.TraceId;
+ online.SaveAsync(5_000);
- Interlocked.Increment(ref _onlines);
+ if (_onlines == 0 || online.Times <= 1)
+ Interlocked.Increment(ref _onlines);
- return entity;
+ return online;
}
/// 设置网页会话状态
+ ///
///
///
///
@@ -109,12 +114,12 @@ public UserOnline SetStatus(String sessionId, String deviceId, String page, Stri
///
///
///
- public UserOnline SetWebStatus(String sessionId, String deviceId, String page, String status, UserAgentParser userAgent, IUser user, String ip)
+ public UserOnline SetWebStatus(UserOnline online, String sessionId, String deviceId, String page, String status, UserAgentParser userAgent, IUser user, String ip)
{
// 网页使用一个定时器来清理过期
StartTimer();
- if (user == null) return SetStatus(sessionId, deviceId, page, status, userAgent, 0, null, ip);
+ if (user == null) return SetStatus(online, sessionId, deviceId, page, status, userAgent, 0, null, ip);
// 根据IP修正用户城市
if (user is User user2 && (user2.AreaId == 0 || user2.AreaId % 10000 == 0))
@@ -134,7 +139,7 @@ public UserOnline SetWebStatus(String sessionId, String deviceId, String page, S
}
}
- return SetStatus(sessionId, deviceId, page, status, userAgent, user.ID, user + "", ip);
+ return SetStatus(online, sessionId, deviceId, page, status, userAgent, user.ID, user + "", ip);
}
/// 删除过期,指定过期时间
@@ -143,7 +148,7 @@ public UserOnline SetWebStatus(String sessionId, String deviceId, String page, S
public IList ClearExpire(Int32 secTimeout = 20 * 60)
{
// 无在线则不执行
- if (_onlines == 0) return new List();
+ if (_onlines == 0) return [];
using var span = _tracer?.NewSpan("ClearExpireOnline");
@@ -162,19 +167,23 @@ public IList ClearExpire(Int32 secTimeout = 20 * 60)
// 修正在线数
var total = UserOnline.Meta.Count;
- _onlines = total - list.Count;
// 设置统计
UserStat stat = null;
- if (set.EnableUserStat)
+ if (total != _onlines || list.Count > 0)
{
- stat = UserStat.GetOrAdd(DateTime.Today);
- if (stat != null)
+ if (set.EnableUserStat)
{
- if (total > stat.MaxOnline) stat.MaxOnline = total;
+ stat = UserStat.GetOrAdd(DateTime.Today);
+ if (stat != null)
+ {
+ if (total > stat.MaxOnline) stat.MaxOnline = total;
+ }
}
}
+ _onlines = total - list.Count;
+
// 设置离线
foreach (var item in list)
{
diff --git a/NewLife.CubeNC/WebMiddleware/RunTimeMiddleware.cs b/NewLife.CubeNC/WebMiddleware/RunTimeMiddleware.cs
index 78c24f69..533e37e2 100644
--- a/NewLife.CubeNC/WebMiddleware/RunTimeMiddleware.cs
+++ b/NewLife.CubeNC/WebMiddleware/RunTimeMiddleware.cs
@@ -22,7 +22,7 @@ public class RunTimeMiddleware
private readonly UserService _userService;
/// 会话提供者
- static readonly SessionProvider _sessionProvider = new SessionProvider();
+ static readonly SessionProvider _sessionProvider = new();
/// 实例化
///
@@ -50,7 +50,7 @@ public async Task Invoke(HttpContext ctx)
ManageProvider.UserHost = ip;
// 创建Session集合
- var token = CreateSession(ctx);
+ var (token, session) = CreateSession(ctx);
var inf = new RunTimeInfo();
ctx.Items[nameof(RunTimeInfo)] = inf;
@@ -66,14 +66,14 @@ public async Task Invoke(HttpContext ctx)
// 设计时收集执行的SQL语句
if (SysConfig.Current.Develop)
{
- inf.Sqls = new List();
+ inf.Sqls = [];
DAL.LocalFilter = s => inf.Sqls.Add(s);
}
// 日志控制,精确标注Web类型线程
WriteLogEventArgs.Current.IsWeb = true;
- UserOnline olt = null;
+ var online = session["Online"] as UserOnline;
var user = ManageProvider.User;
try
{
@@ -86,16 +86,17 @@ public async Task Invoke(HttpContext ctx)
var deviceId = FillDeviceId(ctx);
//var sessionId = token?.MD5_16() ?? ip;
var sessionId = deviceId;
- olt = _userService.SetWebStatus(sessionId, deviceId, p, userAgent, ua, user, ip);
+ online = _userService.SetWebStatus(online, sessionId, deviceId, p, userAgent, ua, user, ip);
//FillDeviceId(ctx, olt);
- ctx.Items["Cube_Online"] = olt;
+ session["Online"] = online;
+ ctx.Items["Cube_Online"] = online;
}
await _next.Invoke(ctx);
}
catch (Exception ex)
{
var uri = ctx.Request.GetRawUrl();
- olt?.SetError(ex.Message);
+ online?.SetError(ex.Message);
XTrace.Log.Error("[{0}]的错误[{1}] {2}", uri, ip, ctx.TraceIdentifier);
@@ -133,8 +134,7 @@ public async Task Invoke(HttpContext ctx)
///
public static String GetInfo(HttpContext ctx)
{
- var rtinf = ctx.Items[nameof(RunTimeInfo)] as RunTimeInfo;
- if (rtinf == null) return null;
+ if (ctx.Items[nameof(RunTimeInfo)] is not RunTimeInfo rtinf) return null;
var inf = String.Format(DbRunTimeFormat,
DAL.QueryTimes - rtinf.QueryTimes,
@@ -151,7 +151,7 @@ public static String GetInfo(HttpContext ctx)
return inf;
}
- private static String CreateSession(HttpContext ctx)
+ private static (String, IDictionary) CreateSession(HttpContext ctx)
{
// 准备Session
var ss = ctx.Session;
@@ -167,12 +167,13 @@ private static String CreateSession(HttpContext ctx)
}
//Session = _sessionProvider.GetSession(ss.Id);
- ctx.Items["Session"] = _sessionProvider.GetSession(token);
+ var session = _sessionProvider.GetSession(token);
+ ctx.Items["Session"] = session;
- return token;
+ return (token, session);
}
- return null;
+ return (null, null);
}
/// 忽略的后缀