2026-04-28 10:23:58 +08:00
using Microsoft.Extensions.Configuration ;
using SqlSugar ;
using System.Collections.Concurrent ;
using System.Globalization ;
using System.Net.Http ;
using System.Text ;
using System.Net.Http.Json ;
using System.Security.Cryptography ;
using System.Text.Json ;
using YY.Admin.Core.Session ;
using YY.Admin.Core.Util ;
using YY.Admin.Services.Service.Config ;
using YY.Admin.Services.Service.Jeecg ;
namespace YY.Admin.Services.Service.Auth
{
public class SysAuthService : ISysAuthService , ISingletonDependency
{
private SysUser ? _currentUser ;
public SysUser ? CurrentUser = > _currentUser ;
public event EventHandler < SysUser ? > ? UserChanged ;
private readonly ISysCacheService _sysCacheService ;
private readonly ISqlSugarClient _dbContext ;
private readonly ISysConfigService _sysConfigService ;
private readonly IEventAggregator _eventAggregator ;
private readonly IConfiguration _configuration ;
private readonly HttpClient _httpClient ;
private readonly ILoggerService _logger ;
public SysAuthService (
ISqlSugarClient dbContext ,
ISysCacheService sysCacheService ,
ISysConfigService sysConfigService ,
IEventAggregator eventAggregator ,
IConfiguration configuration ,
HttpClient httpClient ,
ILoggerService logger ) {
_dbContext = dbContext ;
_sysCacheService = sysCacheService ;
_sysConfigService = sysConfigService ;
_eventAggregator = eventAggregator ;
_configuration = configuration ;
_httpClient = httpClient ;
_logger = logger ;
}
// 添加Token存储
private static readonly ConcurrentDictionary < string , UserContext > _tokenStore =
new ConcurrentDictionary < string , UserContext > ( ) ;
private static bool _localIdentityResetDone = false ;
private static long _localIdSeed = DateTime . UtcNow . Ticks ;
public async Task < LoginOutput > LoginAsync ( LoginInput request )
{
string? jeecgErrorMessage = null ;
try
{
var jeecgEnabled = _configuration . GetValue < bool > ( "JeecgIntegration:Enabled" ) ;
var preferLocal = _configuration . GetValue ( "JeecgIntegration:PreferLocalLogin" , true ) ;
var fallbackToLocal = _configuration . GetValue ( "JeecgIntegration:FallbackToLocal" , true ) ;
var backendReachable = await IsJeecgBackendReachableAsync ( ) ;
// 未启用 Jeecg: 仅本地
if ( ! jeecgEnabled )
{
return await AuthenticateAgainstJeecgMirrorTableAsync ( request , jeecgFailureHint : null , localFirstOfflineHint : true ) ;
}
// 后端断开:仅本地新表登录,不再依赖后端
if ( ! backendReachable )
{
return await AuthenticateAgainstJeecgMirrorTableAsync (
request ,
jeecgFailureHint : "Jeecg服务拒绝连接, 请确认后端服务已启动" ,
localFirstOfflineHint : true ) ;
}
// 工控离线优先:先本地库(不连 MES 即可用种子/已同步账号),失败再尝试 Jeecg
if ( preferLocal )
{
var localFirst = await AuthenticateAgainstJeecgMirrorTableAsync ( request , jeecgFailureHint : null , localFirstOfflineHint : true ) ;
if ( localFirst . Success )
{
return localFirst ;
}
var jeecgResult = await LoginByJeecgAsync ( request ) ;
if ( jeecgResult . Success )
{
return jeecgResult ;
}
jeecgErrorMessage = jeecgResult . Message ;
if ( ! fallbackToLocal )
{
return jeecgResult ;
}
// 本地已失败且已计次,不再二次验密;合并 Jeecg 原因便于排查
return MergeLocalLoginFailureWithJeecg ( localFirst , jeecgErrorMessage ) ;
}
// 在线优先:先 Jeecg, 失败再本地( 传统 SSO)
var jeecgFirst = await LoginByJeecgAsync ( request ) ;
if ( jeecgFirst . Success )
{
return jeecgFirst ;
}
jeecgErrorMessage = jeecgFirst . Message ;
if ( ! fallbackToLocal )
{
return jeecgFirst ;
}
return await AuthenticateAgainstJeecgMirrorTableAsync ( request , jeecgFailureHint : jeecgErrorMessage , localFirstOfflineHint : false ) ;
}
catch ( Exception ex )
{
var friendlyError = ToFriendlyJeecgErrorMessage ( ex . Message ) ;
return new LoginOutput
{
Success = false ,
Message = string . IsNullOrWhiteSpace ( jeecgErrorMessage )
? $"登录失败:{friendlyError}"
: $"{jeecgErrorMessage}( {friendlyError}) "
} ;
}
}
/// <summary>
/// 本地账号表( jeecg_sys_user) 登录。
/// 后端断开时仅依赖此表,不调用远端接口。
/// </summary>
private async Task < LoginOutput > AuthenticateAgainstJeecgMirrorTableAsync (
LoginInput request ,
string? jeecgFailureHint ,
bool localFirstOfflineHint )
{
var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{request.Username}" ;
var passwordErrorTimes = _sysCacheService . Get < int > ( keyPasswordErrorTimes ) ;
var passwordMaxErrorTimes = await _sysConfigService . GetConfigValue < int > ( ConfigConst . SysPasswordMaxErrorTimes ) ;
if ( passwordMaxErrorTimes < 1 )
{
passwordMaxErrorTimes = 10 ;
}
if ( passwordErrorTimes > = passwordMaxErrorTimes )
{
return new LoginOutput
{
Success = false ,
Message = "密码错误次数过多,账号已锁定,请半小时后重试!"
} ;
}
var mirrorCandidates = await _dbContext . Queryable < JeecgSysUser > ( ) . ClearFilter ( )
. WhereIF ( request . TenantId > 0 , u = > u . LoginTenantId = = request . TenantId )
. WhereIF ( ! string . IsNullOrWhiteSpace ( request . Username ) , u = > u . Username = = request . Username | | ( u . Phone ! = null & & u . Phone = = request . Username ) )
. Take ( 1 )
. ToListAsync ( ) ;
var mirrorUser = mirrorCandidates . FirstOrDefault ( ) ;
if ( mirrorUser = = null )
{
return new LoginOutput
{
Success = false ,
Message = string . IsNullOrWhiteSpace ( jeecgFailureHint )
? "用户名或密码错误"
: $"用户名或密码错误。{jeecgFailureHint}"
} ;
}
if ( mirrorUser . Status . HasValue & & mirrorUser . Status . Value ! = 1 )
{
return new LoginOutput
{
Success = false ,
Message = "账号已冻结"
} ;
}
var sessionUser = BuildSessionUserFromMirrorUser ( mirrorUser ) ;
if ( VerifyPassword ( request . Password , keyPasswordErrorTimes , passwordErrorTimes , sessionUser ) )
{
_currentUser = sessionUser ;
UserChanged ? . Invoke ( this , _currentUser ) ;
_sysCacheService . Remove ( keyPasswordErrorTimes ) ;
string okMsg ;
if ( localFirstOfflineHint )
{
okMsg = "登录成功(本地验证;与 MES 断连时数据保留在本机,恢复连接后可再与后台同步)" ;
}
else if ( ! string . IsNullOrWhiteSpace ( jeecgFailureHint ) )
{
okMsg = "登录成功(后台不可达或 Jeecg 未通过,已使用本地账号验证)" ;
}
else
{
okMsg = "登录成功" ;
}
return new LoginOutput
{
Success = true ,
Message = okMsg ,
User = _currentUser ,
Token = await GenerateToken ( sessionUser )
} ;
}
return new LoginOutput
{
Success = false ,
Message = string . IsNullOrWhiteSpace ( jeecgFailureHint )
? "用户名或密码错误"
: $"用户名或密码错误。{jeecgFailureHint}"
} ;
}
/// <summary>
/// 将 jeecg_sys_user 转换为本地会话所需 SysUser 结构。
/// </summary>
private SysUser BuildSessionUserFromMirrorUser ( JeecgSysUser mirrorUser )
{
long localId ;
if ( ! long . TryParse ( mirrorUser . Id , NumberStyles . Integer , CultureInfo . InvariantCulture , out localId ) | | localId < = 0 )
{
localId = Interlocked . Increment ( ref _localIdSeed ) ;
}
return new SysUser
{
Id = localId ,
Account = mirrorUser . Username ? ? string . Empty ,
RealName = string . IsNullOrWhiteSpace ( mirrorUser . Realname ) ? ( mirrorUser . Username ? ? string . Empty ) : mirrorUser . Realname ! ,
Password = mirrorUser . Password ? ? string . Empty ,
JeecgPasswordSalt = mirrorUser . Salt ,
Phone = mirrorUser . Phone ,
Email = mirrorUser . Email ,
JobNum = mirrorUser . WorkNo ,
Status = mirrorUser . Status = = 1 ? StatusEnum . Enable : StatusEnum . Disable ,
TenantId = mirrorUser . LoginTenantId . HasValue ? mirrorUser . LoginTenantId . Value : ( _configuration . GetValue < long? > ( "JeecgIntegration:DefaultTenantId" ) ? ? 1300000000001 ) ,
AccountType = AccountTypeEnum . NormalUser ,
OrgId = 0 ,
PosId = 0
} ;
}
/// <summary>
/// 快速探测 Jeecg 后端是否连通(登录前短超时)。
/// </summary>
private async Task < bool > IsJeecgBackendReachableAsync ( )
{
var baseUrl = _configuration . GetValue < string > ( "JeecgIntegration:BaseUrl" ) ? . TrimEnd ( '/' ) ;
var userListPath = _configuration . GetValue < string > ( "JeecgIntegration:UserListPath" ) ? ? "/sys/user/scada/queryUser" ;
if ( string . IsNullOrWhiteSpace ( baseUrl ) )
{
return false ;
}
var probeUrl = $"{baseUrl}{userListPath}?pageNo=1&pageSize=1&includeDetail=false" ;
try
{
using var cts = new CancellationTokenSource ( TimeSpan . FromSeconds ( 2 ) ) ;
using var req = new HttpRequestMessage ( HttpMethod . Get , probeUrl ) ;
using var resp = await _httpClient . SendAsync ( req , cts . Token ) ;
return resp . IsSuccessStatusCode ;
}
catch
{
return false ;
}
}
/// <summary>
/// 本地已失败时附加 Jeecg 原因,且不重复调用验密(避免错误次数翻倍)
/// </summary>
private static LoginOutput MergeLocalLoginFailureWithJeecg ( LoginOutput localFailure , string? jeecgMessage )
{
if ( localFailure . Success )
{
return localFailure ;
}
// 锁定、冻结等保持原样
if ( ! string . Equals ( localFailure . Message , "用户名或密码错误" , StringComparison . Ordinal ) )
{
return string . IsNullOrWhiteSpace ( jeecgMessage )
? localFailure
: new LoginOutput
{
Success = false ,
Message = $"{localFailure.Message}。{jeecgMessage}"
} ;
}
// 后台不可达时,避免让用户误以为仅是「密码错」:补充本地 Jeecg 盐字段说明
if ( ! string . IsNullOrWhiteSpace ( jeecgMessage ) & & IsJeecgTransportUnreachableMessage ( jeecgMessage ) )
{
return new LoginOutput
{
Success = false ,
Message = $"{jeecgMessage} 本地账号校验未通过时,请确认已从 MES 同步 password 与 jeecg_password_salt, 且登录名与 Jeecg 的 username 一致。"
} ;
}
return new LoginOutput
{
Success = false ,
Message = string . IsNullOrWhiteSpace ( jeecgMessage )
? localFailure . Message
: $"{localFailure.Message}。{jeecgMessage}"
} ;
}
/// <summary>
/// 判断是否为 Jeecg 网络不可达类错误(中英文字符串均兼容)
/// </summary>
private static bool IsJeecgTransportUnreachableMessage ( string message )
{
if ( string . IsNullOrWhiteSpace ( message ) )
{
return false ;
}
return message . Contains ( "Connection refused" , StringComparison . OrdinalIgnoreCase )
| | message . Contains ( "积极拒绝" , StringComparison . Ordinal )
| | message . Contains ( "拒绝连接" , StringComparison . Ordinal )
| | message . Contains ( "No connection could be made" , StringComparison . OrdinalIgnoreCase )
| | message . Contains ( "无法连接" , StringComparison . Ordinal )
| | message . Contains ( "timed out" , StringComparison . OrdinalIgnoreCase )
| | message . Contains ( "超时" , StringComparison . Ordinal )
| | message . Contains ( "No such host" , StringComparison . OrdinalIgnoreCase )
| | message . Contains ( "不知道这样的主机" , StringComparison . Ordinal ) ;
}
/// <summary>
/// 通过 Jeecg 接口进行认证,并映射到本地用户
/// </summary>
private async Task < LoginOutput > LoginByJeecgAsync ( LoginInput request )
{
try
{
var baseUrl = _configuration . GetValue < string > ( "JeecgIntegration:BaseUrl" ) ? . TrimEnd ( '/' ) ;
if ( string . IsNullOrWhiteSpace ( baseUrl ) )
{
return new LoginOutput
{
Success = false ,
Message = "JeecgIntegration 已启用,但未配置 BaseUrl"
} ;
}
var loginPath = _configuration . GetValue < string > ( "JeecgIntegration:LoginPath" ) ? ? "/sys/login" ;
var userInfoPath = _configuration . GetValue < string > ( "JeecgIntegration:UserInfoPath" ) ? ? "/sys/user/getUserInfo" ;
var loginUrl = $"{baseUrl}{loginPath}" ;
var userInfoUrl = $"{baseUrl}{userInfoPath}" ;
var payload = new Dictionary < string , object? >
{
["username"] = request . Username ,
["password"] = request . Password ,
["rememberMe"] = request . RememberMe
} ;
var captcha = _configuration . GetValue < string > ( "JeecgIntegration:Captcha" ) ;
var checkKey = _configuration . GetValue < string > ( "JeecgIntegration:CheckKey" ) ;
if ( ! string . IsNullOrWhiteSpace ( captcha ) ) payload [ "captcha" ] = captcha ;
if ( ! string . IsNullOrWhiteSpace ( checkKey ) ) payload [ "checkKey" ] = checkKey ;
using var loginResponse = await _httpClient . PostAsJsonAsync ( loginUrl , payload ) ;
if ( ! loginResponse . IsSuccessStatusCode )
{
return new LoginOutput
{
Success = false ,
Message = $"Jeecg登录请求失败: HTTP {(int)loginResponse.StatusCode}"
} ;
}
var loginJson = await loginResponse . Content . ReadAsStringAsync ( ) ;
using var loginDoc = JsonDocument . Parse ( loginJson ) ;
var loginRoot = loginDoc . RootElement ;
var loginSuccess = loginRoot . TryGetProperty ( "success" , out var successEl ) & & successEl . GetBoolean ( ) ;
if ( ! loginSuccess )
{
var loginMsg = loginRoot . TryGetProperty ( "message" , out var msgEl ) ? msgEl . GetString ( ) : "Jeecg登录失败" ;
return new LoginOutput
{
Success = false ,
Message = loginMsg ? ? "Jeecg登录失败"
} ;
}
string? jeecgToken = null ;
JsonElement tenantListElement = default ;
bool hasTenantList = false ;
if ( loginRoot . TryGetProperty ( "result" , out var resultEl ) & &
resultEl . ValueKind = = JsonValueKind . Object & &
resultEl . TryGetProperty ( "token" , out var tokenEl ) )
{
jeecgToken = tokenEl . GetString ( ) ;
if ( resultEl . TryGetProperty ( "tenantList" , out var tenantListEl ) & & tenantListEl . ValueKind = = JsonValueKind . Array )
{
tenantListElement = tenantListEl ;
hasTenantList = true ;
}
}
if ( string . IsNullOrWhiteSpace ( jeecgToken ) )
{
return new LoginOutput
{
Success = false ,
Message = "Jeecg登录成功但未返回 token"
} ;
}
using var userReq = new HttpRequestMessage ( HttpMethod . Get , userInfoUrl ) ;
userReq . Headers . TryAddWithoutValidation ( "X-Access-Token" , jeecgToken ) ;
using var userResponse = await _httpClient . SendAsync ( userReq ) ;
if ( ! userResponse . IsSuccessStatusCode )
{
return new LoginOutput
{
Success = false ,
Message = $"获取Jeecg用户信息失败: HTTP {(int)userResponse.StatusCode}"
} ;
}
var userJson = await userResponse . Content . ReadAsStringAsync ( ) ;
using var userDoc = JsonDocument . Parse ( userJson ) ;
var userRoot = userDoc . RootElement ;
if ( ! ( userRoot . TryGetProperty ( "success" , out var userSuccessEl ) & & userSuccessEl . GetBoolean ( ) ) )
{
var userMsg = userRoot . TryGetProperty ( "message" , out var userMsgEl ) ? userMsgEl . GetString ( ) : "获取Jeecg用户信息失败" ;
return new LoginOutput
{
Success = false ,
Message = userMsg ? ? "获取Jeecg用户信息失败"
} ;
}
var remoteUsername = request . Username ;
string? remoteRealName = null ;
string? remoteNickName = null ;
string? remotePhone = null ;
string? remoteEmail = null ;
string? remoteAvatar = null ;
string? remoteAddress = null ;
string? remoteOfficePhone = null ;
string? remoteJobNum = null ;
string? remoteIdCardNum = null ;
string? remoteRemark = null ;
DateTime ? remoteBirthday = null ;
GenderEnum ? remoteSex = null ;
StatusEnum ? remoteStatus = null ;
long? remoteLoginTenantId = null ;
DateTime ? remoteJeecgUpdateTime = null ;
long? remoteJeecgUserId = null ;
string? remoteJeecgIdRaw = null ;
if ( userRoot . TryGetProperty ( "result" , out var userResultEl ) & &
userResultEl . ValueKind = = JsonValueKind . Object & &
userResultEl . TryGetProperty ( "userInfo" , out var userInfoEl ) & &
userInfoEl . ValueKind = = JsonValueKind . Object )
{
if ( userInfoEl . TryGetProperty ( "id" , out var jeecgUserIdEl ) )
{
remoteJeecgUserId = TryParseJeecgSnowflakeUserIdFromJson ( jeecgUserIdEl ) ;
remoteJeecgIdRaw = jeecgUserIdEl . ValueKind = = JsonValueKind . String
? jeecgUserIdEl . GetString ( )
: jeecgUserIdEl . ValueKind = = JsonValueKind . Number
? jeecgUserIdEl . GetRawText ( )
: null ;
}
if ( userInfoEl . TryGetProperty ( "username" , out var usernameEl ) ) remoteUsername = usernameEl . GetString ( ) ? ? remoteUsername ;
if ( userInfoEl . TryGetProperty ( "realname" , out var realnameEl ) ) remoteRealName = realnameEl . GetString ( ) ;
if ( userInfoEl . TryGetProperty ( "nickname" , out var nicknameEl ) ) remoteNickName = nicknameEl . GetString ( ) ;
if ( userInfoEl . TryGetProperty ( "phone" , out var phoneEl ) ) remotePhone = phoneEl . GetString ( ) ;
if ( userInfoEl . TryGetProperty ( "email" , out var emailEl ) ) remoteEmail = emailEl . GetString ( ) ;
if ( userInfoEl . TryGetProperty ( "avatar" , out var avatarEl ) ) remoteAvatar = avatarEl . GetString ( ) ;
if ( userInfoEl . TryGetProperty ( "address" , out var addressEl ) ) remoteAddress = addressEl . GetString ( ) ;
if ( userInfoEl . TryGetProperty ( "telephone" , out var telephoneEl ) ) remoteOfficePhone = telephoneEl . GetString ( ) ;
if ( userInfoEl . TryGetProperty ( "workNo" , out var workNoEl ) ) remoteJobNum = workNoEl . GetString ( ) ;
if ( userInfoEl . TryGetProperty ( "idCard" , out var idCardEl ) ) remoteIdCardNum = idCardEl . GetString ( ) ;
if ( userInfoEl . TryGetProperty ( "description" , out var descriptionEl ) ) remoteRemark = descriptionEl . GetString ( ) ;
if ( userInfoEl . TryGetProperty ( "birthday" , out var birthdayEl ) & &
birthdayEl . ValueKind = = JsonValueKind . String & &
DateTime . TryParse ( birthdayEl . GetString ( ) , out var birthday ) )
{
remoteBirthday = birthday ;
}
if ( userInfoEl . TryGetProperty ( "sex" , out var sexEl ) )
{
remoteSex = ResolveUserSex ( sexEl ) ;
}
if ( userInfoEl . TryGetProperty ( "status" , out var statusEl ) )
{
remoteStatus = ResolveUserStatus ( statusEl ) ;
}
if ( userInfoEl . TryGetProperty ( "loginTenantId" , out var tenantIdEl ) & & tenantIdEl . TryGetInt64 ( out var tenantId ) )
{
remoteLoginTenantId = tenantId ;
}
if ( userInfoEl . TryGetProperty ( "updateTime" , out var loginUpdateEl ) )
{
remoteJeecgUpdateTime = TryParseJeecgRemoteDateTime ( loginUpdateEl ) ;
}
}
// 按配置清空本地用户/角色/租户数据(仅当前进程执行一次)
var resetLocalIdentity = _configuration . GetValue ( "JeecgIntegration:ResetLocalIdentityDataOnJeecgLogin" , false ) ;
if ( resetLocalIdentity & & ! _localIdentityResetDone )
{
await ResetLocalIdentityDataAsync ( ) ;
_localIdentityResetDone = true ;
}
var useJeecgUserIdAsLocalPk = _configuration . GetValue ( "JeecgIntegration:UseJeecgUserIdAsLocalPrimaryKey" , true ) ;
var localUser = await _dbContext . Queryable < SysUser > ( )
. Includes ( u = > u . SysOrg )
. ClearFilter ( )
. Where ( u = > u . Account = = remoteUsername | | ( ! string . IsNullOrEmpty ( remotePhone ) & & u . Phone = = remotePhone ) )
. FirstAsync ( ) ;
if ( localUser = = null )
{
var autoProvision = _configuration . GetValue ( "JeecgIntegration:AutoProvisionLocalUser" , true ) ;
if ( ! autoProvision )
{
return new LoginOutput
{
Success = false ,
Message = $"Jeecg认证成功, 但本地未找到账号映射: {remoteUsername}。请先在工控端创建同名账号。"
} ;
}
var templateUser = await _dbContext . Queryable < SysUser > ( ) . ClearFilter ( ) . OrderBy ( u = > u . Id ) . FirstAsync ( ) ;
var defaultTenantId = _configuration . GetValue < long? > ( "JeecgIntegration:DefaultTenantId" ) ;
var targetTenantId = defaultTenantId ? ? remoteLoginTenantId ? ? templateUser ? . TenantId ? ? 1300000000001 ;
var newUser = new SysUser
{
Account = remoteUsername ,
RealName = string . IsNullOrWhiteSpace ( remoteRealName ) ? remoteUsername : remoteRealName ,
NickName = remoteNickName ,
Password = CryptogramUtil . Encrypt ( Guid . NewGuid ( ) . ToString ( "N" ) ) ,
Sex = remoteSex ? ? GenderEnum . Unknown ,
Birthday = remoteBirthday ,
Phone = remotePhone ,
OfficePhone = remoteOfficePhone ,
Email = remoteEmail ,
Avatar = remoteAvatar ,
Address = remoteAddress ,
JobNum = remoteJobNum ,
IdCardNum = remoteIdCardNum ,
Remark = remoteRemark ,
Status = remoteStatus ? ? StatusEnum . Enable ,
AccountType = targetTenantId = = ( defaultTenantId ? ? 0 ) ? AccountTypeEnum . SysAdmin : AccountTypeEnum . NormalUser ,
TenantId = targetTenantId ,
OrgId = templateUser ? . OrgId ? ? 0 ,
PosId = templateUser ? . PosId ? ? 0 ,
JeecgUpdateTime = remoteJeecgUpdateTime ,
CreateTime = DateTime . Now ,
UpdateTime = DateTime . Now
} ;
long assignUserId ;
if ( useJeecgUserIdAsLocalPk & & remoteJeecgUserId . HasValue & & remoteJeecgUserId . Value > 0 )
{
var idOccupied = await _dbContext . Queryable < SysUser > ( ) . ClearFilter ( ) . AnyAsync ( u = > u . Id = = remoteJeecgUserId . Value ) ;
assignUserId = idOccupied ? await TakeNextSysUserIdAsync ( ) : remoteJeecgUserId . Value ;
}
else
{
assignUserId = await TakeNextSysUserIdAsync ( ) ;
}
newUser . Id = assignUserId ;
if ( ! string . IsNullOrWhiteSpace ( remoteJeecgIdRaw ) )
{
newUser . JeecgBizUserId = TruncateJeecgSyncField ( remoteJeecgIdRaw , 64 ) ;
}
await _dbContext . Insertable ( newUser ) . ExecuteCommandAsync ( ) ;
localUser = await _dbContext . Queryable < SysUser > ( )
. Includes ( u = > u . SysOrg )
. ClearFilter ( )
. Where ( u = > u . Account = = remoteUsername )
. FirstAsync ( ) ;
if ( localUser = = null )
{
return new LoginOutput
{
Success = false ,
Message = "Jeecg认证成功, 但本地自动建档失败。"
} ;
}
}
// 已存在用户:将本地主键迁移为 Jeecg userInfo.id( 与接口一致)
if ( useJeecgUserIdAsLocalPk & & remoteJeecgUserId . HasValue & & remoteJeecgUserId . Value > 0 )
{
if ( localUser . Id ! = remoteJeecgUserId . Value )
{
var idBlocked = await _dbContext . Queryable < SysUser > ( ) . ClearFilter ( )
. Where ( u = > u . Id = = remoteJeecgUserId . Value & & u . Account ! = remoteUsername )
. AnyAsync ( ) ;
if ( ! idBlocked )
{
var oldUid = localUser . Id ;
await TryMigrateSysUserPrimaryKeyToJeecgIdAsync ( oldUid , remoteJeecgUserId . Value ) ;
_sysCacheService . Remove ( $"jeecg:token:{oldUid}" ) ;
localUser = await _dbContext . Queryable < SysUser > ( )
. Includes ( u = > u . SysOrg )
. ClearFilter ( )
. Where ( u = > u . Account = = remoteUsername )
. FirstAsync ( ) ? ? localUser ;
}
}
}
if ( ! string . IsNullOrWhiteSpace ( remoteJeecgIdRaw ) )
{
localUser . JeecgBizUserId = TruncateJeecgSyncField ( remoteJeecgIdRaw , 64 ) ;
}
// Jeecg字段全量同步到本地用户( 以后端为准)
if ( ! string . IsNullOrWhiteSpace ( remoteRealName ) ) localUser . RealName = remoteRealName ;
localUser . NickName = remoteNickName ;
localUser . Phone = remotePhone ;
localUser . Email = remoteEmail ;
localUser . Avatar = remoteAvatar ;
localUser . Address = remoteAddress ;
localUser . OfficePhone = remoteOfficePhone ;
localUser . JobNum = remoteJobNum ;
localUser . IdCardNum = remoteIdCardNum ;
localUser . Remark = remoteRemark ;
if ( remoteBirthday . HasValue ) localUser . Birthday = remoteBirthday ;
if ( remoteSex . HasValue ) localUser . Sex = remoteSex . Value ;
if ( remoteStatus . HasValue ) localUser . Status = remoteStatus . Value ;
if ( remoteLoginTenantId . HasValue & & remoteLoginTenantId . Value > 0 ) localUser . TenantId = remoteLoginTenantId . Value ;
if ( remoteJeecgUpdateTime . HasValue ) localUser . JeecgUpdateTime = remoteJeecgUpdateTime ;
localUser . UpdateTime = DateTime . Now ;
var defaultSyncTenantId = _configuration . GetValue < long? > ( "JeecgIntegration:DefaultTenantId" ) ;
if ( defaultSyncTenantId . HasValue & & localUser . TenantId = = defaultSyncTenantId . Value )
{
localUser . AccountType = AccountTypeEnum . SysAdmin ;
}
var syncToLocal = _configuration . GetValue ( "JeecgIntegration:SyncUserProfileToLocal" , true ) ;
if ( syncToLocal )
{
await _dbContext . Updateable ( localUser ) . UpdateColumns ( u = > new
{
u . RealName ,
u . NickName ,
u . Phone ,
u . Email ,
u . Avatar ,
u . Address ,
u . OfficePhone ,
u . JobNum ,
u . IdCardNum ,
u . Remark ,
u . Birthday ,
u . Sex ,
u . Status ,
u . TenantId ,
u . AccountType ,
u . JeecgUpdateTime ,
u . JeecgBizUserId ,
u . UpdateTime
} ) . ExecuteCommandAsync ( ) ;
}
// 同步租户信息(以后端为准)
if ( hasTenantList )
{
await SyncTenantListAsync ( tenantListElement ) ;
}
// 同步Jeecg全量用户到本地
var syncAllUsersOnLogin = _configuration . GetValue ( "JeecgIntegration:SyncAllUsersOnJeecgLogin" , true ) ;
var userListSyncHint = string . Empty ;
if ( syncAllUsersOnLogin )
{
// 登录场景: SCADA 接口必须全量分页拉取,不能用 updatedAfter 增量提前结束,否则本地用户表长期不全
var ( syncListOk , _ ) = await SyncAllJeecgUsersAsync ( baseUrl , jeecgToken , localUser , allowScadaIncrementalQuery : false ) ;
if ( ! syncListOk )
{
userListSyncHint = "( 提示: Jeecg 用户列表同步未拉到任何记录,用户管理可能为空;请检查 queryUser 接口、网络及程序运行目录下的 Admin.NET.db。) " ;
}
localUser = await _dbContext . Queryable < SysUser > ( )
. Includes ( u = > u . SysOrg )
. ClearFilter ( )
. Where ( u = > u . Id = = localUser . Id )
. FirstAsync ( ) ? ? localUser ;
}
// SysAdmin菜单已在菜单服务中放行, 不再写sys_tenant_menu, 避免SQLite主键冲突
// 同步Jeecg权限明细到本地角色/菜单关系
await SyncJeecgPermissionToLocalAsync ( baseUrl , jeecgToken , localUser ) ;
_currentUser = localUser ;
UserChanged ? . Invoke ( this , _currentUser ) ;
_sysCacheService . Set ( $"jeecg:token:{localUser.Id}" , jeecgToken ! , TimeSpan . FromHours ( 8 ) ) ;
return new LoginOutput
{
Success = true ,
Message = string . IsNullOrEmpty ( userListSyncHint ) ? "登录成功" : $"登录成功{userListSyncHint}" ,
User = _currentUser ,
Token = await GenerateToken ( localUser )
} ;
}
catch ( Exception ex )
{
var friendlyError = ToFriendlyJeecgErrorMessage ( ex . Message ) ;
return new LoginOutput
{
Success = false ,
Message = $"Jeecg登录失败: {friendlyError}"
} ;
}
}
/// <summary>
/// 同步 Jeecg 返回的租户列表到本地,字段以后端为准
/// </summary>
private async Task SyncTenantListAsync ( JsonElement tenantListElement )
{
if ( tenantListElement . ValueKind ! = JsonValueKind . Array ) return ;
var templateTenant = await _dbContext . Queryable < SysTenant > ( ) . ClearFilter ( ) . OrderBy ( t = > t . Id ) . FirstAsync ( ) ;
foreach ( var tenantEl in tenantListElement . EnumerateArray ( ) )
{
if ( tenantEl . ValueKind ! = JsonValueKind . Object ) continue ;
if ( ! tenantEl . TryGetProperty ( "id" , out var idEl ) | | ! idEl . TryGetInt64 ( out var tenantId ) | | tenantId < = 0 ) continue ;
var tenantName = tenantEl . TryGetProperty ( "name" , out var nameEl ) ? nameEl . GetString ( ) : null ;
var companyLogo = tenantEl . TryGetProperty ( "companyLogo" , out var logoEl ) ? logoEl . GetString ( ) : null ;
var status = ResolveTenantStatus ( tenantEl ) ;
var exists = await _dbContext . Queryable < SysTenant > ( ) . ClearFilter ( ) . Where ( t = > t . Id = = tenantId ) . AnyAsync ( ) ;
if ( exists )
{
await _dbContext . Updateable < SysTenant > ( )
. SetColumns ( t = > t . Title = = ( string . IsNullOrWhiteSpace ( tenantName ) ? t . Title : tenantName ) )
. SetColumns ( t = > t . Logo = = companyLogo )
. SetColumns ( t = > t . Status = = status )
. SetColumns ( t = > t . UpdateTime = = DateTime . Now )
. Where ( t = > t . Id = = tenantId )
. ExecuteCommandAsync ( ) ;
}
else
{
var newTenant = new SysTenant
{
Id = tenantId ,
UserId = templateTenant ? . UserId ? ? _currentUser ? . Id ? ? 0 ,
OrgId = templateTenant ? . OrgId ? ? _currentUser ? . OrgId ? ? 0 ,
TenantType = templateTenant ? . TenantType ? ? TenantTypeEnum . Id ,
DbType = templateTenant ? . DbType ? ? DbType . Sqlite ,
Connection = templateTenant ? . Connection ,
ConfigId = templateTenant ? . ConfigId ,
SlaveConnections = templateTenant ? . SlaveConnections ,
EnableReg = templateTenant ? . EnableReg ? ? YesNoEnum . N ,
RegWayId = templateTenant ? . RegWayId ,
Logo = companyLogo ,
Title = string . IsNullOrWhiteSpace ( tenantName ) ? $"租户{tenantId}" : tenantName ,
ViceTitle = templateTenant ? . ViceTitle ,
ViceDesc = templateTenant ? . ViceDesc ,
Watermark = templateTenant ? . Watermark ,
Copyright = templateTenant ? . Copyright ,
Icp = templateTenant ? . Icp ,
IcpUrl = templateTenant ? . IcpUrl ,
OrderNo = templateTenant ? . OrderNo ? ? 100 ,
Remark = templateTenant ? . Remark ,
Status = status ,
CreateTime = DateTime . Now ,
UpdateTime = DateTime . Now
} ;
await _dbContext . Insertable ( newTenant ) . ExecuteCommandAsync ( ) ;
}
}
}
/// <summary>
/// 清空本地用户/角色/租户数据, 为Jeecg全量接管做准备
/// </summary>
private async Task ResetLocalIdentityDataAsync ( )
{
await _dbContext . Ado . BeginTranAsync ( ) ;
try
{
await _dbContext . Deleteable < SysUserRole > ( ) . ExecuteCommandAsync ( ) ;
await _dbContext . Deleteable < SysRoleMenu > ( ) . ExecuteCommandAsync ( ) ;
await _dbContext . Deleteable < SysRole > ( ) . ExecuteCommandAsync ( ) ;
await _dbContext . Deleteable < SysTenantMenu > ( ) . ExecuteCommandAsync ( ) ;
await _dbContext . Deleteable < SysTenant > ( ) . ExecuteCommandAsync ( ) ;
await _dbContext . Deleteable < SysUser > ( ) . ExecuteCommandAsync ( ) ;
await _dbContext . Ado . CommitTranAsync ( ) ;
// 清空 Jeecg 同步水位,否则仍按旧时间增量请求,首屏可能 0 条且误以为同步成功
try
{
new JeecgSyncStateStore ( ) . Save ( new JeecgSyncState ( ) ) ;
}
catch
{
// 忽略状态文件写入失败
}
}
catch
{
await _dbContext . Ado . RollbackTranAsync ( ) ;
throw ;
}
}
/// <summary>
/// 完全采用Jeecg权限: 同步当前用户角色与菜单权限明细到本地
/// </summary>
private async Task SyncJeecgPermissionToLocalAsync ( string baseUrl , string jeecgToken , SysUser localUser )
{
var permissionPath = _configuration . GetValue < string > ( "JeecgIntegration:UserPermissionPath" ) ? ? "/sys/permission/getUserPermissionByToken" ;
var requestUrl = $"{baseUrl}{permissionPath}" ;
using var req = new HttpRequestMessage ( HttpMethod . Get , requestUrl ) ;
req . Headers . TryAddWithoutValidation ( "X-Access-Token" , jeecgToken ) ;
using var resp = await _httpClient . SendAsync ( req ) ;
if ( ! resp . IsSuccessStatusCode ) return ;
var json = await resp . Content . ReadAsStringAsync ( ) ;
using var doc = JsonDocument . Parse ( json ) ;
var root = doc . RootElement ;
if ( ! ( root . TryGetProperty ( "success" , out var successEl ) & & successEl . GetBoolean ( ) ) ) return ;
if ( ! root . TryGetProperty ( "result" , out var resultEl ) | | resultEl . ValueKind ! = JsonValueKind . Object ) return ;
var permissionCodes = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
var routeKeys = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
if ( resultEl . TryGetProperty ( "codeList" , out var codeListEl ) & & codeListEl . ValueKind = = JsonValueKind . Array )
{
foreach ( var item in codeListEl . EnumerateArray ( ) )
{
if ( item . ValueKind = = JsonValueKind . String & & ! string . IsNullOrWhiteSpace ( item . GetString ( ) ) )
{
permissionCodes . Add ( item . GetString ( ) ! ) ;
}
}
}
if ( resultEl . TryGetProperty ( "menu" , out var menuEl ) )
{
CollectJeecgMenuKeys ( menuEl , routeKeys , permissionCodes ) ;
}
var roleCode = $"jeecg_sync_{localUser.Account}" . ToLowerInvariant ( ) ;
var role = await _dbContext . Queryable < SysRole > ( ) . ClearFilter ( ) . Where ( r = > r . Code = = roleCode ) . FirstAsync ( ) ;
if ( role = = null )
{
role = new SysRole
{
Name = $"Jeecg同步角色_{localUser.Account}" ,
Code = roleCode ,
TenantId = localUser . TenantId ? ? 0 ,
DataScope = DataScopeEnum . All ,
Status = StatusEnum . Enable ,
OrderNo = 1 ,
Remark = "Jeecg同步自动生成" ,
CreateTime = DateTime . Now ,
UpdateTime = DateTime . Now
} ;
role . Id = await TakeNextSysRoleIdAsync ( ) ;
await _dbContext . Insertable ( role ) . ExecuteCommandAsync ( ) ;
role = await _dbContext . Queryable < SysRole > ( ) . ClearFilter ( ) . Where ( r = > r . Code = = roleCode ) . FirstAsync ( ) ;
}
if ( role = = null ) return ;
await _dbContext . Deleteable < SysUserRole > ( ) . Where ( x = > x . UserId = = localUser . Id ) . ExecuteCommandAsync ( ) ;
var maxUserRoleId = await _dbContext . Queryable < SysUserRole > ( ) . ClearFilter ( ) . MaxAsync ( x = > ( long? ) x . Id ) ? ? 0 ;
await _dbContext . Insertable ( new SysUserRole
{
Id = maxUserRoleId + 1 ,
UserId = localUser . Id ,
RoleId = role . Id
} ) . ExecuteCommandAsync ( ) ;
await _dbContext . Deleteable < SysRoleMenu > ( ) . Where ( x = > x . RoleId = = role . Id ) . ExecuteCommandAsync ( ) ;
var localMenus = await _dbContext . Queryable < SysMenu > ( ) . ClearFilter ( )
. Where ( m = > m . Type ! = MenuTypeEnum . Btn & & m . Status = = StatusEnum . Enable )
. ToListAsync ( ) ;
var defaultTenantId = _configuration . GetValue < long? > ( "JeecgIntegration:DefaultTenantId" ) ;
var useAllMenus = defaultTenantId . HasValue & & localUser . TenantId = = defaultTenantId . Value ;
var matchedMenuIds = useAllMenus
? localMenus . Select ( m = > m . Id ) . Distinct ( ) . ToList ( )
: localMenus . Where ( m = > IsLocalMenuMatchedByJeecg ( m , routeKeys , permissionCodes ) )
. Select ( m = > m . Id )
. Distinct ( )
. ToList ( ) ;
if ( matchedMenuIds . Count = = 0 ) return ;
var maxRoleMenuId = await _dbContext . Queryable < SysRoleMenu > ( ) . ClearFilter ( ) . MaxAsync ( x = > ( long? ) x . Id ) ? ? 0 ;
foreach ( var menuId in matchedMenuIds . Distinct ( ) )
{
await _dbContext . Insertable ( new SysRoleMenu
{
Id = + + maxRoleMenuId ,
RoleId = role . Id ,
MenuId = menuId
} ) . ExecuteCommandAsync ( ) ;
}
}
/// <summary>
/// 递归提取Jeecg菜单中的路径、名称、权限编码
/// </summary>
private static void CollectJeecgMenuKeys ( JsonElement node , HashSet < string > routeKeys , HashSet < string > permissionCodes )
{
if ( node . ValueKind = = JsonValueKind . Array )
{
foreach ( var child in node . EnumerateArray ( ) )
{
CollectJeecgMenuKeys ( child , routeKeys , permissionCodes ) ;
}
return ;
}
if ( node . ValueKind ! = JsonValueKind . Object ) return ;
AddIfString ( node , "path" , routeKeys ) ;
AddIfString ( node , "url" , routeKeys ) ;
AddIfString ( node , "name" , routeKeys ) ;
AddIfString ( node , "component" , routeKeys ) ;
AddIfString ( node , "title" , routeKeys ) ;
AddIfString ( node , "perms" , permissionCodes ) ;
AddIfString ( node , "permission" , permissionCodes ) ;
if ( node . TryGetProperty ( "meta" , out var metaEl ) & & metaEl . ValueKind = = JsonValueKind . Object )
{
AddIfString ( metaEl , "title" , routeKeys ) ;
AddIfString ( metaEl , "component" , routeKeys ) ;
if ( metaEl . TryGetProperty ( "permissionList" , out var permissionListEl ) & & permissionListEl . ValueKind = = JsonValueKind . Array )
{
foreach ( var p in permissionListEl . EnumerateArray ( ) )
{
if ( p . ValueKind = = JsonValueKind . String & & ! string . IsNullOrWhiteSpace ( p . GetString ( ) ) )
{
permissionCodes . Add ( p . GetString ( ) ! ) ;
}
}
}
}
if ( node . TryGetProperty ( "children" , out var childrenEl ) )
{
CollectJeecgMenuKeys ( childrenEl , routeKeys , permissionCodes ) ;
}
}
private static bool IsLocalMenuMatchedByJeecg ( SysMenu menu , HashSet < string > routeKeys , HashSet < string > permissionCodes )
{
var keys = new [ ]
{
Normalize ( menu . Path ) ,
Normalize ( menu . Name ) ,
Normalize ( menu . Component ) ,
Normalize ( menu . Title )
} ;
if ( ! string . IsNullOrWhiteSpace ( menu . Permission ) & & permissionCodes . Contains ( menu . Permission . Trim ( ) ) )
{
return true ;
}
return keys . Any ( k = > ! string . IsNullOrWhiteSpace ( k ) & & routeKeys . Contains ( k ) ) ;
}
private static void AddIfString ( JsonElement obj , string propertyName , HashSet < string > target )
{
if ( obj . TryGetProperty ( propertyName , out var valueEl ) & &
valueEl . ValueKind = = JsonValueKind . String & &
! string . IsNullOrWhiteSpace ( valueEl . GetString ( ) ) )
{
target . Add ( Normalize ( valueEl . GetString ( ) ) ) ;
}
}
private static string Normalize ( string? value )
{
return ( value ? ? string . Empty ) . Trim ( ) . Trim ( '/' ) . ToLowerInvariant ( ) ;
}
private static string Normalize ( JsonElement valueEl )
{
return Normalize ( valueEl . GetString ( ) ) ;
}
/// <summary>
/// 分配下一个本地用户主键( sys_user 非自增, Jeecg 建档时必须显式赋值)
/// </summary>
private async Task < long > TakeNextSysUserIdAsync ( )
{
var maxId = await _dbContext . Queryable < SysUser > ( ) . ClearFilter ( ) . MaxAsync ( x = > ( long? ) x . Id ) ? ? 0 ;
return maxId + 1 ;
}
/// <summary>
/// 分配下一个本地角色主键( sys_role 非自增,多账号 Jeecg 同步各建角色时须避免 Id=0 重复)
/// </summary>
private async Task < long > TakeNextSysRoleIdAsync ( )
{
var maxId = await _dbContext . Queryable < SysRole > ( ) . ClearFilter ( ) . MaxAsync ( x = > ( long? ) x . Id ) ? ? 0 ;
return maxId + 1 ;
}
/// <summary>
/// 安全解析Jeecg租户状态, 避免null或字符串导致异常
/// </summary>
private static StatusEnum ResolveTenantStatus ( JsonElement tenantEl )
{
if ( ! tenantEl . TryGetProperty ( "status" , out var statusEl ) )
{
return StatusEnum . Disable ;
}
if ( statusEl . ValueKind = = JsonValueKind . Number & & statusEl . TryGetInt32 ( out var numStatus ) )
{
return numStatus = = 1 ? StatusEnum . Enable : StatusEnum . Disable ;
}
if ( statusEl . ValueKind = = JsonValueKind . String & & int . TryParse ( statusEl . GetString ( ) , out var strStatus ) )
{
return strStatus = = 1 ? StatusEnum . Enable : StatusEnum . Disable ;
}
return StatusEnum . Disable ;
}
/// <summary>
/// 有 Token 时附加 X-Access-Token( SCADA 免登录接口可不传)
/// </summary>
private static void AddJeecgAccessTokenIfPresent ( HttpRequestMessage request , string? jeecgToken )
{
if ( ! string . IsNullOrWhiteSpace ( jeecgToken ) )
{
request . Headers . TryAddWithoutValidation ( "X-Access-Token" , jeecgToken ) ;
}
}
/// <summary>
/// 从接口返回的用户数组中累计最大 updateTime/createTime( 转 UTC) , 用于推进 SCADA 同步游标
/// </summary>
private static void AccumulateMaxJeecgSyncCursorUtc ( IEnumerable < JsonElement > records , ref DateTime ? maxUtc )
{
foreach ( var el in records )
{
DateTime ? tUpdate = el . TryGetProperty ( "updateTime" , out var ue ) ? TryParseJeecgRemoteDateTime ( ue ) : null ;
DateTime ? tCreate = el . TryGetProperty ( "createTime" , out var ce ) ? TryParseJeecgRemoteDateTime ( ce ) : null ;
DateTime ? rowBest ;
if ( tUpdate . HasValue & & tCreate . HasValue )
{
rowBest = tUpdate . Value > = tCreate . Value ? tUpdate : tCreate ;
}
else
{
rowBest = tUpdate ? ? tCreate ;
}
if ( ! rowBest . HasValue )
{
continue ;
}
var asUtc = rowBest . Value . Kind = = DateTimeKind . Utc ? rowBest . Value : rowBest . Value . ToUniversalTime ( ) ;
if ( ! maxUtc . HasValue | | asUtc > maxUtc . Value )
{
maxUtc = asUtc ;
}
}
}
/// <summary>
/// 将一批 Jeecg/SCADA 用户 JSON 写入本地;若接口返回 password/salt( 如 SCADA 同步),则写入本地用于与 Jeecg 一致的登录校验。
/// </summary>
private async Task ProcessJeecgUserRecordsAsync (
IReadOnlyList < JsonElement > records ,
SysUser templateUser ,
SysUser currentUser ,
long? defaultTenantId ,
bool skipUnchanged )
{
foreach ( var userEl in records )
{
var remoteUser = ParseJeecgUser ( userEl ) ;
if ( string . IsNullOrWhiteSpace ( remoteUser . Username ) )
{
continue ;
}
// 无匹配行时 FirstAsync 会抛错,导致整批同步中断;用 Take(1) 安全取首条
var existing = ( await _dbContext . Queryable < SysUser > ( )
. ClearFilter ( )
. Where ( u = > u . Account = = remoteUser . Username )
. Take ( 1 )
. ToListAsync ( ) ) . FirstOrDefault ( ) ;
if ( skipUnchanged
& & existing ! = null
& & existing . JeecgUpdateTime . HasValue
& & remoteUser . JeecgSourceUpdateTime . HasValue
& & Math . Abs ( ( existing . JeecgUpdateTime . Value - remoteUser . JeecgSourceUpdateTime . Value ) . TotalSeconds ) < 2 )
{
continue ;
}
var mappedTenantId = remoteUser . TenantId ? ? defaultTenantId ? ? templateUser . TenantId ? ? currentUser . TenantId ? ? 1300000000001 ;
var mappedAccountType = mappedTenantId = = ( defaultTenantId ? ? 0 ) ? AccountTypeEnum . SysAdmin : AccountTypeEnum . NormalUser ;
if ( existing = = null )
{
var createAt = remoteUser . JeecgSourceCreateTime ? ? DateTime . Now ;
var newUser = new SysUser
{
Account = remoteUser . Username ! ,
RealName = string . IsNullOrWhiteSpace ( remoteUser . RealName ) ? remoteUser . Username ! : remoteUser . RealName ! ,
NickName = remoteUser . NickName ,
Password = ! string . IsNullOrWhiteSpace ( remoteUser . PasswordHex )
? remoteUser . PasswordHex !
: CryptogramUtil . Encrypt ( Guid . NewGuid ( ) . ToString ( "N" ) ) ,
JeecgPasswordSalt = remoteUser . Salt ,
Sex = remoteUser . Sex ? ? GenderEnum . Unknown ,
Birthday = remoteUser . Birthday ,
Phone = remoteUser . Phone ,
OfficePhone = remoteUser . OfficePhone ,
Email = remoteUser . Email ,
Avatar = remoteUser . Avatar ,
Address = remoteUser . Address ,
JobNum = remoteUser . JobNum ,
IdCardNum = remoteUser . IdCardNum ,
Remark = remoteUser . Remark ,
PosTitle = remoteUser . PosTitle ,
Status = remoteUser . Status ? ? StatusEnum . Enable ,
TenantId = mappedTenantId ,
AccountType = mappedAccountType ,
OrgId = templateUser . OrgId ,
PosId = templateUser . PosId ,
JeecgUpdateTime = remoteUser . JeecgSourceUpdateTime ,
JeecgBizUserId = remoteUser . JeecgBizUserId ,
JeecgOrgCode = remoteUser . OrgCode ,
JeecgDepartIds = remoteUser . DepartIds ,
CreateTime = createAt ,
UpdateTime = DateTime . Now
} ;
newUser . Id = await TakeNextSysUserIdAsync ( ) ;
await _dbContext . Insertable ( newUser ) . ExecuteCommandAsync ( ) ;
}
else
{
existing . RealName = string . IsNullOrWhiteSpace ( remoteUser . RealName ) ? existing . RealName : remoteUser . RealName ! ;
existing . NickName = remoteUser . NickName ;
existing . Phone = remoteUser . Phone ;
existing . Email = remoteUser . Email ;
existing . Avatar = remoteUser . Avatar ;
existing . Address = remoteUser . Address ;
existing . OfficePhone = remoteUser . OfficePhone ;
existing . JobNum = remoteUser . JobNum ;
existing . IdCardNum = remoteUser . IdCardNum ;
existing . Remark = remoteUser . Remark ;
existing . PosTitle = remoteUser . PosTitle ;
if ( remoteUser . Birthday . HasValue )
{
existing . Birthday = remoteUser . Birthday ;
}
if ( remoteUser . Sex . HasValue )
{
existing . Sex = remoteUser . Sex . Value ;
}
if ( remoteUser . Status . HasValue )
{
existing . Status = remoteUser . Status . Value ;
}
existing . TenantId = mappedTenantId ;
existing . AccountType = mappedAccountType ;
existing . JeecgUpdateTime = remoteUser . JeecgSourceUpdateTime ? ? existing . JeecgUpdateTime ;
if ( ! string . IsNullOrWhiteSpace ( remoteUser . JeecgBizUserId ) )
{
existing . JeecgBizUserId = remoteUser . JeecgBizUserId ;
}
existing . JeecgOrgCode = remoteUser . OrgCode ;
existing . JeecgDepartIds = remoteUser . DepartIds ;
existing . UpdateTime = DateTime . Now ;
if ( ! string . IsNullOrWhiteSpace ( remoteUser . PasswordHex ) )
{
existing . Password = remoteUser . PasswordHex ! ;
}
if ( ! string . IsNullOrWhiteSpace ( remoteUser . Salt ) )
{
existing . JeecgPasswordSalt = remoteUser . Salt ;
}
await _dbContext . Updateable ( existing ) . UpdateColumns ( u = > new
{
u . RealName ,
u . NickName ,
u . Phone ,
u . Email ,
u . Avatar ,
u . Address ,
u . OfficePhone ,
u . JobNum ,
u . IdCardNum ,
u . Remark ,
u . PosTitle ,
u . Birthday ,
u . Sex ,
u . Status ,
u . TenantId ,
u . AccountType ,
u . JeecgUpdateTime ,
u . JeecgBizUserId ,
u . JeecgOrgCode ,
u . JeecgDepartIds ,
u . UpdateTime ,
u . Password ,
u . JeecgPasswordSalt
} ) . ExecuteCommandAsync ( ) ;
}
}
}
/// <summary>
/// 同步 Jeecg 用户:标准列表为 result.records 分页; SCADA 为 result 数组分页 + 可选 updatedAfter( 免登录时可不传 Token) 。SCADA 若返回 password/salt 则写入本地。
/// </summary>
/// <param name="allowScadaIncrementalQuery">false 时( 如登录后首拉) SCADA 不带 updatedAfter, 始终按页拉全量; true 时(如定时后台)可按水位做增量。</param>
/// <returns>是否完成有效同步; ApiRecordRows 为接口返回的用户记录条数(分页累计),为 0 时表示未拉到数据。</returns>
private async Task < ( bool Success , int ApiRecordRows ) > SyncAllJeecgUsersAsync ( string baseUrl , string jeecgToken , SysUser currentUser , bool allowScadaIncrementalQuery = true )
{
var userListPath = _configuration . GetValue < string > ( "JeecgIntegration:UserListPath" ) ? ? "/sys/user/list" ;
var templateCandidates = await _dbContext . Queryable < SysUser > ( ) . ClearFilter ( ) . OrderBy ( u = > u . Id ) . Take ( 1 ) . ToListAsync ( ) ;
var templateUser = templateCandidates . FirstOrDefault ( ) ? ? currentUser ;
var defaultTenantId = _configuration . GetValue < long? > ( "JeecgIntegration:DefaultTenantId" ) ;
var skipUnchanged = _configuration . GetValue ( "JeecgIntegration:UserSyncSkipUnchanged" , true ) ;
var useTimeQuery = _configuration . GetValue ( "JeecgIntegration:UserListUseUpdateTimeQuery" , false ) ;
var overlapMinutes = _configuration . GetValue ( "JeecgIntegration:IncrementalSyncOverlapMinutes" , 2 ) ;
var isScadaStylePath = userListPath . Contains ( "scada" , StringComparison . OrdinalIgnoreCase ) ;
var scadaUseUpdatedAfter = _configuration . GetValue ( "JeecgIntegration:ScadaUseUpdatedAfter" , true ) ;
var scadaPageSize = Math . Clamp ( _configuration . GetValue ( "JeecgIntegration:ScadaUserPageSize" , 500 ) , 1 , 1000 ) ;
var scadaIncludeDetail = _configuration . GetValue ( "JeecgIntegration:ScadaUserIncludeDetail" , false ) ;
var stateStore = new JeecgSyncStateStore ( ) ;
var syncState = stateStore . Load ( ) ;
const int standardPageSize = 200 ;
// pass 0: 增量; 若整轮 0 条则 pass 1 全量(避免清空库后旧水位导致拉不到用户)
for ( var pass = 0 ; pass < 2 ; pass + + )
{
var useIncremental = pass = = 0 & & syncState . LastUserListSyncUtc . HasValue & & (
( isScadaStylePath & & scadaUseUpdatedAfter & & allowScadaIncrementalQuery ) | |
( ! isScadaStylePath & & useTimeQuery ) ) ;
DateTime ? incrementalCursorLocal = null ;
if ( useIncremental )
{
incrementalCursorLocal = syncState . LastUserListSyncUtc ! . Value
. AddMinutes ( - Math . Max ( 0 , overlapMinutes ) )
. ToLocalTime ( ) ;
}
var httpOk = true ;
var rowsThisPass = 0 ;
DateTime ? maxCursorUtcFromData = null ;
if ( isScadaStylePath )
{
var pageNo = 1 ;
while ( true )
{
var listUrl =
$"{baseUrl}{userListPath}?pageNo={pageNo.ToString(CultureInfo.InvariantCulture)}&pageSize={scadaPageSize.ToString(CultureInfo.InvariantCulture)}&includeDetail={(scadaIncludeDetail ? " true " : " false ")}" ;
if ( incrementalCursorLocal . HasValue )
{
var afterStr = incrementalCursorLocal . Value . ToString ( "yyyy-MM-dd HH:mm:ss" , CultureInfo . InvariantCulture ) ;
listUrl + = $"&updatedAfter={Uri.EscapeDataString(afterStr)}" ;
}
using var req = new HttpRequestMessage ( HttpMethod . Get , listUrl ) ;
AddJeecgAccessTokenIfPresent ( req , jeecgToken ) ;
using var resp = await _httpClient . SendAsync ( req ) ;
if ( ! resp . IsSuccessStatusCode )
{
httpOk = false ;
break ;
}
var json = await resp . Content . ReadAsStringAsync ( ) ;
using var doc = JsonDocument . Parse ( json ) ;
var root = doc . RootElement ;
if ( ! ( root . TryGetProperty ( "success" , out var successEl ) & & successEl . GetBoolean ( ) ) )
{
httpOk = false ;
break ;
}
if ( ! root . TryGetProperty ( "result" , out var resultEl ) )
{
httpOk = false ;
break ;
}
List < JsonElement > records ;
if ( resultEl . ValueKind = = JsonValueKind . Array )
{
records = resultEl . EnumerateArray ( ) . ToList ( ) ;
}
else if ( resultEl . ValueKind = = JsonValueKind . Object
& & resultEl . TryGetProperty ( "records" , out var recordsEl )
& & recordsEl . ValueKind = = JsonValueKind . Array )
{
records = recordsEl . EnumerateArray ( ) . ToList ( ) ;
}
else
{
httpOk = false ;
break ;
}
if ( records . Count = = 0 )
{
break ;
}
rowsThisPass + = records . Count ;
AccumulateMaxJeecgSyncCursorUtc ( records , ref maxCursorUtcFromData ) ;
await ProcessJeecgUserRecordsAsync ( records , templateUser , currentUser , defaultTenantId , skipUnchanged ) ;
if ( records . Count < scadaPageSize )
{
break ;
}
pageNo + + ;
}
if ( ! httpOk )
{
return ( false , rowsThisPass ) ;
}
if ( useIncremental & & rowsThisPass = = 0 )
{
continue ;
}
// 接口 success 但本批 0 条:不能当作同步成功,否则界面误导且水位仍会推进
if ( rowsThisPass = = 0 )
{
return ( false , 0 ) ;
}
syncState . LastUserListSyncUtc = maxCursorUtcFromData ? ? DateTime . UtcNow ;
stateStore . Save ( syncState ) ;
return ( true , rowsThisPass ) ;
}
var stdPageNo = 1 ;
while ( true )
{
var listUrl = $"{baseUrl}{userListPath}?pageNo={stdPageNo}&pageSize={standardPageSize}" ;
if ( incrementalCursorLocal . HasValue )
{
var beginStr = incrementalCursorLocal . Value . ToString ( "yyyy-MM-dd HH:mm:ss" , CultureInfo . InvariantCulture ) ;
listUrl + = $"&updateTime_begin={Uri.EscapeDataString(beginStr)}" ;
}
using var req = new HttpRequestMessage ( HttpMethod . Get , listUrl ) ;
AddJeecgAccessTokenIfPresent ( req , jeecgToken ) ;
using var resp = await _httpClient . SendAsync ( req ) ;
if ( ! resp . IsSuccessStatusCode )
{
httpOk = false ;
break ;
}
var json = await resp . Content . ReadAsStringAsync ( ) ;
using var doc = JsonDocument . Parse ( json ) ;
var root = doc . RootElement ;
if ( ! ( root . TryGetProperty ( "success" , out var successEl ) & & successEl . GetBoolean ( ) ) )
{
httpOk = false ;
break ;
}
if ( ! root . TryGetProperty ( "result" , out var resultEl ) | | resultEl . ValueKind ! = JsonValueKind . Object )
{
httpOk = false ;
break ;
}
if ( ! resultEl . TryGetProperty ( "records" , out var recordsEl ) | | recordsEl . ValueKind ! = JsonValueKind . Array )
{
httpOk = false ;
break ;
}
var records = recordsEl . EnumerateArray ( ) . ToList ( ) ;
if ( records . Count = = 0 )
{
break ;
}
rowsThisPass + = records . Count ;
await ProcessJeecgUserRecordsAsync ( records , templateUser , currentUser , defaultTenantId , skipUnchanged ) ;
long total = 0 ;
if ( resultEl . TryGetProperty ( "total" , out var totalEl ) )
{
if ( totalEl . TryGetInt64 ( out var tl ) )
{
total = tl ;
}
else if ( totalEl . TryGetInt32 ( out var ti ) )
{
total = ti ;
}
}
if ( total < = ( long ) stdPageNo * standardPageSize )
{
break ;
}
stdPageNo + + ;
}
if ( ! httpOk )
{
return ( false , rowsThisPass ) ;
}
if ( useIncremental & & rowsThisPass = = 0 )
{
continue ;
}
if ( rowsThisPass = = 0 )
{
return ( false , 0 ) ;
}
syncState . LastUserListSyncUtc = DateTime . UtcNow ;
stateStore . Save ( syncState ) ;
return ( true , rowsThisPass ) ;
}
return ( false , 0 ) ;
}
/// <summary>
/// 登录页一键同步:依赖 SCADA 免登录 queryUser( 无需 Token) ; 空库时用配置租户/机构占位写入新用户。
/// </summary>
public async Task < ( bool Success , string Message ) > SyncJeecgUsersToLocalFromLoginScreenAsync ( CancellationToken cancellationToken = default )
{
if ( ! _configuration . GetValue ( "JeecgIntegration:Enabled" , false ) )
{
return ( false , "未启用 Jeecg 集成( JeecgIntegration:Enabled) 。" ) ;
}
var baseUrl = _configuration . GetValue < string > ( "JeecgIntegration:BaseUrl" ) ? . TrimEnd ( '/' ) ;
if ( string . IsNullOrWhiteSpace ( baseUrl ) )
{
return ( false , "未配置 JeecgIntegration:BaseUrl。" ) ;
}
var userListPath = _configuration . GetValue < string > ( "JeecgIntegration:UserListPath" ) ? ? string . Empty ;
if ( ! userListPath . Contains ( "scada" , StringComparison . OrdinalIgnoreCase ) )
{
return ( false , "一键同步需使用免登录 SCADA 接口。请将 UserListPath 配置为 /sys/user/scada/queryUser, 或先登录后由系统内定时同步。" ) ;
}
try
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
var ( ok , apiRows , _ ) = await SyncScadaUsersToJeecgMirrorTableAsync ( baseUrl , userListPath , cancellationToken ) ;
return ok
? ( true , $"已从 Jeecg 拉取用户并写入本地同构表 jeecg_sys_user( 接口累计 {apiRows} 条)。" )
: ( false , apiRows = = 0
? "同步未完成:接口未返回任何用户记录。请检查 Jeecg queryUser、BaseUrl/UserListPath、网络与白名单; 若用 Navicat 查看数据,请打开程序运行目录(如 bin\\Debug\\...\\win-x64) 下的 Admin.NET.db, 而非仅源码目录下的文件。"
: "同步未完成:请检查 Jeecg 地址、网络,或查看接口是否返回 success=true。" ) ;
}
catch ( OperationCanceledException )
{
throw ;
}
catch ( Exception ex )
{
return ( false , $"同步异常:{ex.Message}" ) ;
}
}
/// <summary>
/// 登录页按钮专用:将 SCADA 接口用户同步到 Jeecg 同构表( jeecg_sys_user) 。
/// </summary>
private async Task < ( bool Success , int ApiRecordRows , int ChangedRows ) > SyncScadaUsersToJeecgMirrorTableAsync ( string baseUrl , string userListPath , CancellationToken cancellationToken )
{
var scadaPageSize = Math . Clamp ( _configuration . GetValue ( "JeecgIntegration:ScadaUserPageSize" , 500 ) , 1 , 1000 ) ;
var scadaIncludeDetail = _configuration . GetValue ( "JeecgIntegration:ScadaUserIncludeDetail" , false ) ;
var pageNo = 1 ;
var totalRows = 0 ;
var changedRows = 0 ;
var remoteIds = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
var remoteUsernames = new HashSet < string > ( StringComparer . OrdinalIgnoreCase ) ;
while ( true )
{
cancellationToken . ThrowIfCancellationRequested ( ) ;
var listUrl =
$"{baseUrl}{userListPath}?pageNo={pageNo.ToString(CultureInfo.InvariantCulture)}&pageSize={scadaPageSize.ToString(CultureInfo.InvariantCulture)}&includeDetail={(scadaIncludeDetail ? " true " : " false ")}" ;
using var req = new HttpRequestMessage ( HttpMethod . Get , listUrl ) ;
using var resp = await _httpClient . SendAsync ( req , cancellationToken ) ;
if ( ! resp . IsSuccessStatusCode )
{
return ( false , totalRows , changedRows ) ;
}
var json = await resp . Content . ReadAsStringAsync ( cancellationToken ) ;
using var doc = JsonDocument . Parse ( json ) ;
var root = doc . RootElement ;
if ( ! ( root . TryGetProperty ( "success" , out var successEl ) & & successEl . GetBoolean ( ) ) )
{
return ( false , totalRows , changedRows ) ;
}
if ( ! root . TryGetProperty ( "result" , out var resultEl ) )
{
return ( false , totalRows , changedRows ) ;
}
List < JsonElement > records ;
if ( resultEl . ValueKind = = JsonValueKind . Array )
{
records = resultEl . EnumerateArray ( ) . ToList ( ) ;
}
else if ( resultEl . ValueKind = = JsonValueKind . Object
& & resultEl . TryGetProperty ( "records" , out var recordsEl )
& & recordsEl . ValueKind = = JsonValueKind . Array )
{
records = recordsEl . EnumerateArray ( ) . ToList ( ) ;
}
else
{
return ( false , totalRows , changedRows ) ;
}
if ( records . Count = = 0 )
{
break ;
}
CollectRemoteUserKeys ( records , remoteIds , remoteUsernames ) ;
totalRows + = records . Count ;
changedRows + = await UpsertJeecgMirrorUsersAsync ( records ) ;
long total = 0 ;
if ( resultEl . ValueKind = = JsonValueKind . Object & & resultEl . TryGetProperty ( "total" , out var totalEl ) )
{
if ( totalEl . TryGetInt64 ( out var tl ) )
{
total = tl ;
}
else if ( totalEl . TryGetInt32 ( out var ti ) )
{
total = ti ;
}
}
if ( total > 0 )
{
if ( total < = ( long ) pageNo * scadaPageSize )
{
break ;
}
}
else if ( records . Count < scadaPageSize )
{
break ;
}
pageNo + + ;
}
var deletedRows = await DeleteMissingJeecgMirrorUsersAsync ( remoteIds , remoteUsernames ) ;
changedRows + = deletedRows ;
return ( true , totalRows , changedRows ) ;
}
/// <summary>
/// 收集远端用户主键集合,用于全量同步后的本地删除对齐。
/// </summary>
private static void CollectRemoteUserKeys (
IReadOnlyList < JsonElement > records ,
ISet < string > remoteIds ,
ISet < string > remoteUsernames )
{
foreach ( var userEl in records )
{
if ( userEl . ValueKind ! = JsonValueKind . Object )
{
continue ;
}
if ( userEl . TryGetProperty ( "id" , out var idEl ) )
{
var idValue = idEl . ValueKind switch
{
JsonValueKind . String = > idEl . GetString ( ) ,
JsonValueKind . Number = > idEl . GetRawText ( ) ,
_ = > null
} ;
if ( ! string . IsNullOrWhiteSpace ( idValue ) )
{
remoteIds . Add ( idValue ) ;
}
}
if ( userEl . TryGetProperty ( "username" , out var usernameEl ) & & usernameEl . ValueKind = = JsonValueKind . String )
{
var username = usernameEl . GetString ( ) ;
if ( ! string . IsNullOrWhiteSpace ( username ) )
{
remoteUsernames . Add ( username ) ;
}
}
}
}
/// <summary>
/// 删除本地 jeecg_sys_user 中已被后端删除的账号,保持镜像一致。
/// </summary>
private async Task < int > DeleteMissingJeecgMirrorUsersAsync ( ISet < string > remoteIds , ISet < string > remoteUsernames )
{
var localRows = await _dbContext . Queryable < JeecgSysUser > ( )
. ClearFilter ( )
. Select ( x = > new { x . Id , x . Username } )
. ToListAsync ( ) ;
if ( localRows . Count = = 0 )
{
return 0 ;
}
var staleIds = localRows
. Where ( x = > ! string . IsNullOrWhiteSpace ( x . Id )
& & ! remoteIds . Contains ( x . Id )
& & ( string . IsNullOrWhiteSpace ( x . Username ) | | ! remoteUsernames . Contains ( x . Username ) ) )
. Select ( x = > x . Id ! )
. Distinct ( StringComparer . OrdinalIgnoreCase )
. ToList ( ) ;
if ( staleIds . Count = = 0 )
{
return 0 ;
}
var deletedRows = await _dbContext . Deleteable < JeecgSysUser > ( )
. In ( it = > it . Id , staleIds )
. ExecuteCommandAsync ( ) ;
_logger . Information ( $"Jeecg镜像删除同步: 表=jeecg_sys_user, 删除行数={deletedRows}" ) ;
return deletedRows ;
}
/// <summary>
/// 写入 Jeecg 同构用户表 jeecg_sys_user( 按 id 或 username 做幂等更新)。
/// </summary>
private async Task < int > UpsertJeecgMirrorUsersAsync ( IReadOnlyList < JsonElement > records )
{
var changedRows = 0 ;
foreach ( var userEl in records )
{
var remoteUser = ParseJeecgUser ( userEl ) ;
if ( string . IsNullOrWhiteSpace ( remoteUser . Username ) )
{
continue ;
}
var remoteId = remoteUser . JeecgBizUserId ;
if ( string . IsNullOrWhiteSpace ( remoteId ) & & userEl . TryGetProperty ( "id" , out var idEl ) )
{
if ( idEl . ValueKind = = JsonValueKind . String )
{
remoteId = idEl . GetString ( ) ;
}
else if ( idEl . ValueKind = = JsonValueKind . Number )
{
remoteId = idEl . GetRawText ( ) ;
}
}
remoteId ? ? = remoteUser . Username ! ;
var existing = ( await _dbContext . Queryable < JeecgSysUser > ( )
. ClearFilter ( )
. Where ( x = > x . Id = = remoteId | | x . Username = = remoteUser . Username )
. Take ( 1 )
. ToListAsync ( ) ) . FirstOrDefault ( ) ;
var isUpdate = existing ! = null ;
var oldAccount = existing ? . Username ;
var oldRealname = existing ? . Realname ;
var oldPhone = existing ? . Phone ;
var oldUpdateTime = existing ? . UpdateTime ? . ToString ( "yyyy-MM-dd HH:mm:ss" ) ;
if ( existing = = null )
{
existing = new JeecgSysUser { Id = remoteId } ;
}
existing . Username = remoteUser . Username ;
existing . Realname = remoteUser . RealName ;
existing . Password = remoteUser . PasswordHex ;
existing . Salt = remoteUser . Salt ;
existing . Birthday = remoteUser . Birthday ;
existing . Sex = remoteUser . Sex . HasValue ? ( int? ) remoteUser . Sex . Value : null ;
existing . Email = remoteUser . Email ;
existing . Phone = remoteUser . Phone ;
existing . OrgCode = remoteUser . OrgCode ;
existing . LoginTenantId = remoteUser . TenantId . HasValue ? ( int? ) remoteUser . TenantId . Value : null ;
existing . Status = remoteUser . Status . HasValue ? ( int? ) remoteUser . Status . Value : null ;
existing . WorkNo = remoteUser . JobNum ;
existing . Telephone = remoteUser . OfficePhone ;
existing . CreateTime = remoteUser . JeecgSourceCreateTime ;
existing . UpdateTime = remoteUser . JeecgSourceUpdateTime ;
existing . DepartIds = remoteUser . DepartIds ;
existing . ClientId = userEl . TryGetProperty ( "clientId" , out var clientIdEl ) ? clientIdEl . GetString ( ) : existing . ClientId ;
existing . BpmStatus = userEl . TryGetProperty ( "bpmStatus" , out var bpmStatusEl ) ? bpmStatusEl . GetString ( ) : existing . BpmStatus ;
existing . Sign = userEl . TryGetProperty ( "sign" , out var signEl ) ? signEl . GetString ( ) : existing . Sign ;
existing . SignEnable = userEl . TryGetProperty ( "signEnable" , out var signEnableEl ) & & signEnableEl . TryGetInt32 ( out var signEnable )
? signEnable
: existing . SignEnable ;
existing . MainDepPostId = userEl . TryGetProperty ( "mainDepPostId" , out var mainDepPostIdEl ) ? mainDepPostIdEl . GetString ( ) : existing . MainDepPostId ;
existing . PositionType = userEl . TryGetProperty ( "positionType" , out var positionTypeEl ) ? positionTypeEl . GetString ( ) : existing . PositionType ;
existing . LastPwdUpdateTime = userEl . TryGetProperty ( "lastPwdUpdateTime" , out var lastPwdUpdateEl ) ? TryParseJeecgRemoteDateTime ( lastPwdUpdateEl ) : existing . LastPwdUpdateTime ;
existing . Sort = userEl . TryGetProperty ( "sort" , out var sortEl ) & & sortEl . TryGetInt32 ( out var sort ) ? sort : existing . Sort ;
existing . IzHideContact = userEl . TryGetProperty ( "izHideContact" , out var hideContactEl ) ? hideContactEl . GetString ( ) : existing . IzHideContact ;
var newAccount = existing . Username ;
var newRealname = existing . Realname ;
var newPhone = existing . Phone ;
var newUpdateTime = existing . UpdateTime ? . ToString ( "yyyy-MM-dd HH:mm:ss" ) ;
var hasChanged = ! isUpdate
| | ! string . Equals ( oldAccount , newAccount , StringComparison . Ordinal )
| | ! string . Equals ( oldRealname , newRealname , StringComparison . Ordinal )
| | ! string . Equals ( oldPhone , newPhone , StringComparison . Ordinal )
| | ! string . Equals ( oldUpdateTime , newUpdateTime , StringComparison . Ordinal ) ;
if ( ! hasChanged )
{
continue ;
}
if ( isUpdate )
{
await _dbContext . Updateable ( existing ) . ExecuteCommandAsync ( ) ;
}
else
{
await _dbContext . Insertable ( existing ) . ExecuteCommandAsync ( ) ;
}
changedRows + + ;
_logger . Information (
$"Jeecg镜像写库明细[{(isUpdate ? " UPDATE " : " INSERT ")}] 表=jeecg_sys_user, " +
$"账号: {ToLogValue(oldAccount)} -> {ToLogValue(newAccount)}, " +
$"姓名: {ToLogValue(oldRealname)} -> {ToLogValue(newRealname)}, " +
$"手机: {ToLogValue(oldPhone)} -> {ToLogValue(newPhone)}, " +
$"更新时间: {ToLogValue(oldUpdateTime)} -> {ToLogValue(newUpdateTime)}" ) ;
}
return changedRows ;
}
private static string ToLogValue ( string? value )
{
return string . IsNullOrWhiteSpace ( value ) ? "<null>" : value ;
}
/// <summary>
/// 后台使用缓存 Token 同步用户(不弹窗)
/// </summary>
public async Task < bool > TryBackgroundSyncJeecgUsersAsync ( CancellationToken cancellationToken = default )
{
if ( ! _configuration . GetValue ( "JeecgIntegration:Enabled" , false ) )
{
return false ;
}
if ( ! _configuration . GetValue ( "JeecgIntegration:SyncAllUsersOnJeecgLogin" , false ) )
{
return false ;
}
var userListPath = _configuration . GetValue < string > ( "JeecgIntegration:UserListPath" ) ? ? string . Empty ;
var isScadaUserPath = userListPath . Contains ( "scada" , StringComparison . OrdinalIgnoreCase ) ;
var user = _currentUser ;
if ( user = = null & & ! isScadaUserPath )
{
return false ;
}
var token = user = = null ? string . Empty : _sysCacheService . Get < string > ( $"jeecg:token:{user.Id}" ) ;
if ( string . IsNullOrWhiteSpace ( token ) & & ! isScadaUserPath )
{
return false ;
}
var baseUrl = _configuration . GetValue < string > ( "JeecgIntegration:BaseUrl" ) ? . TrimEnd ( '/' ) ;
if ( string . IsNullOrWhiteSpace ( baseUrl ) )
{
return false ;
}
cancellationToken . ThrowIfCancellationRequested ( ) ;
try
{
( bool ok , int rows , int changedRows ) result ;
if ( isScadaUserPath )
{
// 实时接收链路统一写入 jeecg_sys_user, 避免写到 sys_user 导致展示与同步表不一致
result = await SyncScadaUsersToJeecgMirrorTableAsync ( baseUrl , userListPath , cancellationToken ) ;
}
else
{
var standardResult = await SyncAllJeecgUsersAsync ( baseUrl , token ? ? string . Empty , user ! ) ;
result = ( standardResult . Success , standardResult . ApiRecordRows , standardResult . ApiRecordRows ) ;
}
var ( ok , rows , changedRows ) = result ;
if ( ok & & changedRows > 0 )
{
_eventAggregator . GetEvent < SysUserEvents . JeecgMirrorUsersSyncedEvent > ( ) . Publish ( rows ) ;
}
return ok ;
}
catch ( OperationCanceledException )
{
throw ;
}
catch
{
return false ;
}
}
/// <summary>
/// 解析 Jeecg userInfo.id( 字符串或数字雪花) , 超出 long 范围则返回 null
/// </summary>
private static long? TryParseJeecgSnowflakeUserIdFromJson ( JsonElement idEl )
{
switch ( idEl . ValueKind )
{
case JsonValueKind . Number :
if ( idEl . TryGetInt64 ( out var n ) & & n > 0 )
{
return n ;
}
var raw = idEl . GetRawText ( ) ;
if ( long . TryParse ( raw , NumberStyles . Integer , CultureInfo . InvariantCulture , out var r ) & & r > 0 )
{
return r ;
}
return null ;
case JsonValueKind . String :
if ( long . TryParse ( idEl . GetString ( ) , NumberStyles . Integer , CultureInfo . InvariantCulture , out var s ) & & s > 0 )
{
return s ;
}
return null ;
default :
return null ;
}
}
/// <summary>
/// 将 sys_user 主键从 oldId 改为 newId( SQLite) , 并同步常见子表 user_id
/// </summary>
private async Task TryMigrateSysUserPrimaryKeyToJeecgIdAsync ( long oldId , long newId )
{
if ( oldId = = newId )
{
return ;
}
await _dbContext . Ado . BeginTranAsync ( ) ;
try
{
await _dbContext . Ado . ExecuteCommandAsync ( "PRAGMA foreign_keys = OFF" ) ;
var fkUpdates = new ( string Table , string Column ) [ ]
{
( "sys_user_role" , "user_id" ) ,
( "sys_user_menu" , "user_id" ) ,
( "sys_user_ext_org" , "user_id" ) ,
( "sys_user_ldap" , "user_id" ) ,
( "sys_user_config_data" , "user_id" ) ,
( "sys_online_user" , "user_id" ) ,
( "sys_notice_user" , "user_id" ) ,
( "sys_schedule" , "user_id" ) ,
( "sys_tenant" , "user_id" ) ,
} ;
foreach ( var ( table , column ) in fkUpdates )
{
await _dbContext . Ado . ExecuteCommandAsync (
$"UPDATE {table} SET {column} = @newId WHERE {column} = @oldId" ,
new { newId , oldId } ) ;
}
await _dbContext . Ado . ExecuteCommandAsync (
"UPDATE sys_user SET id = @newId WHERE id = @oldId" ,
new { newId , oldId } ) ;
await _dbContext . Ado . ExecuteCommandAsync ( "PRAGMA foreign_keys = ON" ) ;
await _dbContext . Ado . CommitTranAsync ( ) ;
}
catch
{
await _dbContext . Ado . RollbackTranAsync ( ) ;
try
{
await _dbContext . Ado . ExecuteCommandAsync ( "PRAGMA foreign_keys = ON" ) ;
}
catch
{
// 忽略恢复失败
}
throw ;
}
}
private static DateTime ? TryParseJeecgRemoteDateTime ( JsonElement el )
{
if ( el . ValueKind = = JsonValueKind . String )
{
if ( DateTime . TryParse ( el . GetString ( ) , CultureInfo . InvariantCulture , DateTimeStyles . AssumeLocal , out var d1 ) )
{
return d1 ;
}
if ( DateTime . TryParse ( el . GetString ( ) , CultureInfo . GetCultureInfo ( "zh-CN" ) , DateTimeStyles . AssumeLocal , out var d2 ) )
{
return d2 ;
}
return null ;
}
if ( el . ValueKind = = JsonValueKind . Number & & el . TryGetInt64 ( out var n ) )
{
if ( n > 1_000_000_000_000L )
{
return DateTimeOffset . FromUnixTimeMilliseconds ( n ) . LocalDateTime ;
}
if ( n > 1_000_000_000L )
{
return DateTimeOffset . FromUnixTimeSeconds ( n ) . LocalDateTime ;
}
}
return null ;
}
private static JeecgUserProfile ParseJeecgUser ( JsonElement userInfoEl )
{
var profile = new JeecgUserProfile ( ) ;
if ( userInfoEl . ValueKind ! = JsonValueKind . Object ) return profile ;
if ( userInfoEl . TryGetProperty ( "username" , out var usernameEl ) ) profile . Username = usernameEl . GetString ( ) ;
if ( string . IsNullOrWhiteSpace ( profile . Username ) & & userInfoEl . TryGetProperty ( "account" , out var accountEl ) )
{
profile . Username = accountEl . ValueKind = = JsonValueKind . String ? accountEl . GetString ( ) : null ;
}
if ( userInfoEl . TryGetProperty ( "realname" , out var realnameEl ) ) profile . RealName = realnameEl . GetString ( ) ;
if ( userInfoEl . TryGetProperty ( "nickname" , out var nicknameEl ) ) profile . NickName = nicknameEl . GetString ( ) ;
if ( userInfoEl . TryGetProperty ( "phone" , out var phoneEl ) ) profile . Phone = phoneEl . GetString ( ) ;
if ( userInfoEl . TryGetProperty ( "email" , out var emailEl ) ) profile . Email = emailEl . GetString ( ) ;
if ( userInfoEl . TryGetProperty ( "avatar" , out var avatarEl ) ) profile . Avatar = avatarEl . GetString ( ) ;
if ( userInfoEl . TryGetProperty ( "address" , out var addressEl ) ) profile . Address = addressEl . GetString ( ) ;
if ( userInfoEl . TryGetProperty ( "telephone" , out var telephoneEl ) ) profile . OfficePhone = telephoneEl . GetString ( ) ;
if ( userInfoEl . TryGetProperty ( "workNo" , out var workNoEl ) ) profile . JobNum = workNoEl . GetString ( ) ;
if ( userInfoEl . TryGetProperty ( "idCard" , out var idCardEl ) ) profile . IdCardNum = idCardEl . GetString ( ) ;
if ( userInfoEl . TryGetProperty ( "description" , out var descriptionEl ) ) profile . Remark = descriptionEl . GetString ( ) ;
if ( userInfoEl . TryGetProperty ( "birthday" , out var birthdayEl ) & &
birthdayEl . ValueKind = = JsonValueKind . String & &
DateTime . TryParse ( birthdayEl . GetString ( ) , CultureInfo . InvariantCulture , DateTimeStyles . None , out var birthday ) )
{
profile . Birthday = birthday ;
}
if ( userInfoEl . TryGetProperty ( "sex" , out var sexEl ) )
{
profile . Sex = ResolveUserSex ( sexEl ) ;
}
if ( userInfoEl . TryGetProperty ( "status" , out var statusEl ) )
{
profile . Status = ResolveUserStatus ( statusEl ) ;
}
if ( TryResolveTenantId ( userInfoEl , out var tenantId ) )
{
profile . TenantId = tenantId ;
}
if ( userInfoEl . TryGetProperty ( "updateTime" , out var updateTimeEl ) )
{
profile . JeecgSourceUpdateTime = TryParseJeecgRemoteDateTime ( updateTimeEl ) ;
}
if ( userInfoEl . TryGetProperty ( "createTime" , out var createTimeEl ) )
{
profile . JeecgSourceCreateTime = TryParseJeecgRemoteDateTime ( createTimeEl ) ;
}
if ( userInfoEl . TryGetProperty ( "id" , out var idEl ) )
{
profile . JeecgBizUserId = idEl . ValueKind switch
{
JsonValueKind . String = > TruncateJeecgSyncField ( idEl . GetString ( ) , 64 ) ,
JsonValueKind . Number when idEl . TryGetInt64 ( out var idNum ) = > idNum . ToString ( CultureInfo . InvariantCulture ) ,
JsonValueKind . Number = > TruncateJeecgSyncField ( idEl . GetRawText ( ) , 64 ) ,
_ = > profile . JeecgBizUserId
} ;
}
if ( userInfoEl . TryGetProperty ( "orgCode" , out var orgCodeEl ) & & orgCodeEl . ValueKind = = JsonValueKind . String )
{
profile . OrgCode = TruncateJeecgSyncField ( orgCodeEl . GetString ( ) , 64 ) ;
}
if ( userInfoEl . TryGetProperty ( "departIds" , out var departIdsEl ) & & departIdsEl . ValueKind = = JsonValueKind . String )
{
profile . DepartIds = TruncateJeecgSyncField ( departIdsEl . GetString ( ) , 512 ) ;
}
if ( userInfoEl . TryGetProperty ( "post" , out var postEl ) & & postEl . ValueKind = = JsonValueKind . String )
{
profile . PosTitle = TruncateJeecgSyncField ( postEl . GetString ( ) , 32 ) ;
}
// Jeecg 密码密文(十六进制)与盐,用于本地按 PBEWithMD5AndDES 校验(与 SCADA 同步接口字段一致)
if ( userInfoEl . TryGetProperty ( "password" , out var pwdHexEl ) & & pwdHexEl . ValueKind = = JsonValueKind . String )
{
profile . PasswordHex = TruncateJeecgSyncField ( pwdHexEl . GetString ( ) , 512 ) ;
}
if ( userInfoEl . TryGetProperty ( "salt" , out var saltEl ) & & saltEl . ValueKind = = JsonValueKind . String )
{
profile . Salt = TruncateJeecgSyncField ( saltEl . GetString ( ) , 64 ) ;
}
return profile ;
}
/// <summary>
/// 截断同步字段,避免超过库表长度
/// </summary>
private static string? TruncateJeecgSyncField ( string? value , int maxLen )
{
if ( string . IsNullOrEmpty ( value ) | | maxLen < = 0 )
{
return value ;
}
return value . Length < = maxLen ? value : value [ . . maxLen ] ;
}
private static bool TryResolveTenantId ( JsonElement userInfoEl , out long tenantId )
{
tenantId = 0 ;
if ( userInfoEl . TryGetProperty ( "loginTenantId" , out var loginTenantIdEl ) )
{
if ( loginTenantIdEl . ValueKind = = JsonValueKind . Number & & loginTenantIdEl . TryGetInt64 ( out tenantId ) & & tenantId > 0 )
{
return true ;
}
if ( loginTenantIdEl . ValueKind = = JsonValueKind . String
& & long . TryParse ( loginTenantIdEl . GetString ( ) , out tenantId )
& & tenantId > 0 )
{
return true ;
}
}
// SCADA 等接口: tenantList[].tenantUserId
if ( userInfoEl . TryGetProperty ( "tenantList" , out var tenantListEl ) & & tenantListEl . ValueKind = = JsonValueKind . Array )
{
foreach ( var t in tenantListEl . EnumerateArray ( ) )
{
if ( ! t . TryGetProperty ( "tenantUserId" , out var tuidEl ) )
{
continue ;
}
if ( tuidEl . ValueKind = = JsonValueKind . Number & & tuidEl . TryGetInt64 ( out tenantId ) & & tenantId > 0 )
{
return true ;
}
if ( tuidEl . ValueKind = = JsonValueKind . String
& & long . TryParse ( tuidEl . GetString ( ) , out tenantId )
& & tenantId > 0 )
{
return true ;
}
}
}
if ( userInfoEl . TryGetProperty ( "relTenantIds" , out var relTenantIdsEl ) & & relTenantIdsEl . ValueKind = = JsonValueKind . String )
{
var firstTenant = relTenantIdsEl . GetString ( ) ? . Split ( ',' , StringSplitOptions . RemoveEmptyEntries | StringSplitOptions . TrimEntries ) . FirstOrDefault ( ) ;
if ( ! string . IsNullOrWhiteSpace ( firstTenant ) & & long . TryParse ( firstTenant , out tenantId ) & & tenantId > 0 )
{
return true ;
}
}
return false ;
}
private sealed class JeecgUserProfile
{
public string? Username { get ; set ; }
public string? RealName { get ; set ; }
public string? NickName { get ; set ; }
public string? Phone { get ; set ; }
public string? Email { get ; set ; }
public string? Avatar { get ; set ; }
public string? Address { get ; set ; }
public string? OfficePhone { get ; set ; }
public string? JobNum { get ; set ; }
public string? IdCardNum { get ; set ; }
public string? Remark { get ; set ; }
public DateTime ? Birthday { get ; set ; }
public GenderEnum ? Sex { get ; set ; }
public StatusEnum ? Status { get ; set ; }
public long? TenantId { get ; set ; }
/// <summary>
/// Jeecg 返回的 updateTime, 用于跳过未变化的本地更新
/// </summary>
public DateTime ? JeecgSourceUpdateTime { get ; set ; }
/// <summary>
/// Jeecg createTime( 仅新建本地用户时写入 CreateTime)
/// </summary>
public DateTime ? JeecgSourceCreateTime { get ; set ; }
public string? JeecgBizUserId { get ; set ; }
public string? OrgCode { get ; set ; }
public string? DepartIds { get ; set ; }
public string? PosTitle { get ; set ; }
/// <summary>Jeecg sys_user.password 十六进制密文</summary>
public string? PasswordHex { get ; set ; }
/// <summary>Jeecg sys_user.salt</summary>
public string? Salt { get ; set ; }
}
/// <summary>
/// 解析Jeecg用户性别( 1男、2女, 其他未知)
/// </summary>
private static GenderEnum ResolveUserSex ( JsonElement sexEl )
{
if ( sexEl . ValueKind = = JsonValueKind . Number & & sexEl . TryGetInt32 ( out var numSex ) )
{
return numSex switch
{
1 = > GenderEnum . Male ,
2 = > GenderEnum . Female ,
_ = > GenderEnum . Unknown
} ;
}
if ( sexEl . ValueKind = = JsonValueKind . String & & int . TryParse ( sexEl . GetString ( ) , out var strSex ) )
{
return strSex switch
{
1 = > GenderEnum . Male ,
2 = > GenderEnum . Female ,
_ = > GenderEnum . Unknown
} ;
}
return GenderEnum . Unknown ;
}
/// <summary>
/// 解析Jeecg用户状态( 1启用, 其他禁用)
/// </summary>
private static StatusEnum ResolveUserStatus ( JsonElement statusEl )
{
if ( statusEl . ValueKind = = JsonValueKind . Number & & statusEl . TryGetInt32 ( out var numStatus ) )
{
return numStatus = = 1 ? StatusEnum . Enable : StatusEnum . Disable ;
}
if ( statusEl . ValueKind = = JsonValueKind . String & & int . TryParse ( statusEl . GetString ( ) , out var strStatus ) )
{
return strStatus = = 1 ? StatusEnum . Enable : StatusEnum . Disable ;
}
return StatusEnum . Disable ;
}
/// <summary>
/// 将常见英文异常转换成中文可读提示
/// </summary>
private static string ToFriendlyJeecgErrorMessage ( string? errorMessage )
{
if ( string . IsNullOrWhiteSpace ( errorMessage ) )
{
return "未知错误, 请检查Jeecg服务与网络连接" ;
}
var msg = errorMessage . Trim ( ) ;
if ( msg . Contains ( "The requested operation requires an element of type 'Number', but the target element has type 'Null'" , StringComparison . OrdinalIgnoreCase ) )
{
return "后台返回的数据格式不正确: 需要数字字段但返回了空值( null) " ;
}
if ( msg . Contains ( "No such host is known" , StringComparison . OrdinalIgnoreCase ) )
{
return "无法连接Jeecg服务, 请检查后端地址或网络" ;
}
if ( msg . Contains ( "Connection refused" , StringComparison . OrdinalIgnoreCase )
| | msg . Contains ( "积极拒绝" , StringComparison . Ordinal )
| | msg . Contains ( "拒绝连接" , StringComparison . Ordinal ) )
{
return "Jeecg服务拒绝连接, 请确认后端服务已启动" ;
}
if ( msg . Contains ( "timed out" , StringComparison . OrdinalIgnoreCase ) )
{
return "请求Jeecg超时, 请检查网络或后端响应" ;
}
return msg ;
}
/// <summary>
///密码验证
/// </summary>
/// <param name="password"></param>
/// <param name="keyPasswordErrorTimes"></param>
/// <param name="passwordErrorTimes"></param>
/// <param name="user"></param>
/// <returns></returns>
public bool VerifyPassword ( string password , string keyPasswordErrorTimes , int passwordErrorTimes , SysUser user )
{
// 已从 Jeecg 同步的用户:携带 salt 时仅按 JeecgBoot PasswordUtil( PBEWithMD5AndDES) 校验, 不再走 SM2/MD5 以免误判
if ( ! string . IsNullOrWhiteSpace ( user . JeecgPasswordSalt ) )
{
if ( JeecgPasswordUtil . Verify ( user . Account , password , user . JeecgPasswordSalt , user . Password , Encoding . UTF8 ) )
{
return true ;
}
// 少数 Jeecg 环境 salt 按 JVM 默认编码落库,与 UTF-8 不一致时再尝试本机默认编码
if ( Encoding . Default . CodePage ! = Encoding . UTF8 . CodePage
& & JeecgPasswordUtil . Verify ( user . Account , password , user . JeecgPasswordSalt , user . Password , Encoding . Default ) )
{
return true ;
}
_sysCacheService . Set ( keyPasswordErrorTimes , + + passwordErrorTimes , TimeSpan . FromMinutes ( 30 ) ) ;
return false ;
}
if ( CryptogramUtil . CryptoType = = CryptogramEnum . MD5 . ToString ( ) )
{
if ( user . Password . Equals ( CryptogramUtil . Encrypt ( password ) ) ) return true ;
}
else
{
if ( CryptogramUtil . Decrypt ( user . Password ) . Equals ( password ) ) return true ;
}
_sysCacheService . Set ( keyPasswordErrorTimes , + + passwordErrorTimes , TimeSpan . FromMinutes ( 30 ) ) ;
return false ;
}
public async Task < bool > IsAuthenticatedAsync ( )
{
await Task . Delay ( 50 ) ;
return _currentUser ! = null ;
}
private async Task < int > getSysTokenExpireAsync ( )
{
2026-04-30 15:28:20 +08:00
var v = await _sysConfigService . GetConfigValue < int > ( ConfigConst . SysTokenExpire ) ;
return v > 0 ? v : 30 ;
}
/// <summary>
/// 用户操作时续期阈值(分钟),默认 20
/// </summary>
private async Task < int > getSysTokenIdleExtendMinutesAsync ( )
{
var v = await _sysConfigService . GetConfigValue < int > ( ConfigConst . SysTokenIdleExtendMinutes ) ;
return v > 0 ? v : 20 ;
}
/// <summary>
/// 是否开启永不过期
/// </summary>
private async Task < bool > getSysTokenNeverExpireAsync ( )
{
return await _sysConfigService . GetConfigValue < bool > ( ConfigConst . SysTokenNeverExpire ) ;
2026-04-28 10:23:58 +08:00
}
/// <summary>
///创建token
/// </summary>
/// <returns></returns>
private async Task < UserToken > GenerateToken ( SysUser user )
{
// 生成访问令牌( 实际项目应使用JWT)
var accessToken = GenerateSecureToken ( 32 ) ;
var refreshToken = GenerateSecureToken ( 32 ) ;
2026-04-30 15:28:20 +08:00
var neverExpire = await getSysTokenNeverExpireAsync ( ) ;
2026-04-28 10:23:58 +08:00
var refreshExpires = await getSysTokenExpireAsync ( ) ;
2026-04-30 15:28:20 +08:00
var refreshExpiration = neverExpire ? DateTime . MaxValue : DateTime . Now . AddMinutes ( refreshExpires ) ;
2026-04-28 10:23:58 +08:00
// 存储Token关联信息
_tokenStore [ accessToken ] = new UserContext
{
UserId = user . Id ,
IsSuperAdmin = user . AccountType = = AccountTypeEnum . SuperAdmin ,
Account = user . Account ,
AccountType = user . AccountType ,
OrgId = user . OrgId ,
RealName = user . RealName ,
TenantId = user . TenantId ! . Value ,
Token = new UserToken ( ) {
RefreshToken = refreshToken ,
RefreshExpires = refreshExpiration ,
}
} ;
return new UserToken
{
AccessToken = accessToken ,
RefreshToken = refreshToken ,
RefreshExpires = refreshExpiration
} ;
}
private string GenerateSecureToken ( int length )
{
using var rng = RandomNumberGenerator . Create ( ) ;
var tokenData = new byte [ length ] ;
rng . GetBytes ( tokenData ) ;
return Convert . ToBase64String ( tokenData ) ;
}
/// <summary>
/// 实现Token验证
/// </summary>
/// <param name="accessToken"></param>
/// <returns></returns>
public bool ValidateToken ( string accessToken )
{
if ( string . IsNullOrEmpty ( accessToken ) )
return false ;
2026-04-30 15:28:20 +08:00
if ( getSysTokenNeverExpireAsync ( ) . GetAwaiter ( ) . GetResult ( ) )
return _tokenStore . ContainsKey ( accessToken ) ;
2026-04-28 10:23:58 +08:00
return _tokenStore . TryGetValue ( accessToken , out var tokenInfo ) & &
tokenInfo . Token . RefreshExpires > = DateTime . Now ;
}
public async Task RefreshToken ( string? accessToken )
{
if ( string . IsNullOrEmpty ( accessToken ) )
return ;
2026-04-30 15:28:20 +08:00
if ( await getSysTokenNeverExpireAsync ( ) )
return ;
2026-04-28 10:23:58 +08:00
if ( _tokenStore . TryGetValue ( accessToken , out var tokenInfo ) )
{
2026-04-30 15:28:20 +08:00
var idleMin = await getSysTokenIdleExtendMinutesAsync ( ) ;
var idleSpan = TimeSpan . FromMinutes ( idleMin ) ;
if ( tokenInfo . Token . RefreshExpires - DateTime . Now < = idleSpan )
2026-04-28 10:23:58 +08:00
{
var refreshExpires = await getSysTokenExpireAsync ( ) ;
tokenInfo . Token . RefreshExpires = DateTime . Now . AddMinutes ( refreshExpires ) ;
}
}
}
/// <summary>
/// 实现刷新Token
/// </summary>
/// <param name="accessToken"></param>
/// <param name="refreshToken"></param>
/// <returns></returns>
public async Task < RefreshTokenResult > RefreshToken ( string accessToken , string refreshToken )
{
if ( ! _tokenStore . TryGetValue ( accessToken , out var tokenInfo ) | |
tokenInfo . Token . RefreshToken ! = refreshToken | |
tokenInfo . Token . RefreshExpires < DateTime . Now )
{
return new RefreshTokenResult { Success = false } ;
}
// 获取用户信息
var user = await _dbContext . Queryable < SysUser > ( )
. Where ( u = > u . Id = = tokenInfo . UserId )
. FirstAsync ( ) ;
if ( user = = null )
return new RefreshTokenResult { Success = false } ;
// 生成新Token
var newToken = GenerateSecureToken ( 32 ) ;
var newRefreshToken = GenerateSecureToken ( 32 ) ;
// 更新Token存储
_tokenStore . TryRemove ( accessToken , out _ ) ;
var refreshExpires = await getSysTokenExpireAsync ( ) ;
2026-04-30 15:28:20 +08:00
var neverExpire = await getSysTokenNeverExpireAsync ( ) ;
2026-04-28 10:23:58 +08:00
_tokenStore [ newToken ] = new UserContext
{
UserId = user . Id ,
IsSuperAdmin = user . AccountType = = AccountTypeEnum . SuperAdmin ,
Account = user . Account ,
AccountType = user . AccountType ,
OrgId = user . OrgId ,
RealName = user . RealName ,
TenantId = user . TenantId ! . Value ,
Token = new UserToken ( )
{
RefreshToken = refreshToken ,
2026-04-30 15:28:20 +08:00
RefreshExpires = neverExpire ? DateTime . MaxValue : DateTime . Now . AddMinutes ( refreshExpires ) ,
2026-04-28 10:23:58 +08:00
}
} ;
return new RefreshTokenResult
{
Success = true ,
AccessToken = newToken ,
RefreshToken = newRefreshToken
} ;
}
/// <summary>
/// 增强退出登录实现
/// </summary>
/// <returns></returns>
public void LogoutAsync ( )
{
if ( _currentUser ! = null )
{
// 移除关联的Token
var tokensToRemove = _tokenStore
. Where ( kvp = > kvp . Value . UserId = = _currentUser . Id )
. Select ( kvp = > kvp . Key )
. ToList ( ) ;
foreach ( var token in tokensToRemove )
{
_tokenStore . TryRemove ( token , out _ ) ;
}
}
AppSession . CurrentUser = null ; // 更新静态会话
_currentUser = null ;
UserChanged ? . Invoke ( this , null ) ;
//await Task.Delay(1000);
}
public async Task UpdateUserLoginInfoAsync ( SysUser user )
{
// 延迟1秒再执行后续的用户登录信息更新操作, 目的是避免与主线程的资源竞争或操作冲突
// 如果不延迟执行会影响主窗口TabControl区域视图导航速度, 影响用户体验
await Task . Delay ( 2000 ) ;
user . LastLoginIp = await DeviceInfoUtil . GetPublicIpAddressAsync ( ) ;
//(user.LastLoginAddress, double? longitude, double? latitude) = CommonUtil.GetIpAddress(user.LastLoginIp);
user . LastLoginTime = DateTime . Now ;
user . LastLoginDevice = DeviceInfoUtil . GetOsVersion ( ) ;
await _dbContext . Updateable ( user ) . UpdateColumns ( u = > new
{
u . LastLoginIp ,
//u.LastLoginAddress,
u . LastLoginTime ,
u . LastLoginDevice ,
} ) . ExecuteCommandAsync ( ) ;
}
}
}