一克泥在线服务

This commit is contained in:
Developer
2026-06-18 18:03:47 +08:00
parent ebd5dafa2d
commit 04334691d0
25 changed files with 692 additions and 102 deletions

View File

@@ -1,6 +1,8 @@
using System;
using System.Text;
using System.Threading;
using Cysharp.Threading.Tasks;
using IchniOnline.Online.Models;
using IchniOnline.Online.Network;
using IchniOnline.Online.Network.Models;
using TapSDK.Login;
@@ -30,11 +32,24 @@ namespace IchniOnline.Online.Logic
/// </summary>
public static event Action OnLoginCanceled;
/// <summary>
/// TapTap 未绑定账号时触发,参数为 pendingBindOauthId 和浏览器绑定页面 URL
/// </summary>
public static event Action<string, string> OnTapTapUnbound;
/// <summary>
/// 是否正在进行登录流程,用于防止并发登录请求
/// </summary>
public static bool IsLoggingIn { get; private set; }
/// <summary>
/// 当前未绑定的 TapTap oauthId
/// </summary>
private static string _pendingBindOauthId;
private static CancellationTokenSource _pollingCts;
private const string WebBaseUrl = "https://ichni.hoshino.fan";
#region TapTap Login
/// <summary>
@@ -108,9 +123,23 @@ namespace IchniOnline.Online.Logic
if (result.IsSuccess && result.Data != null)
{
LoginCacheManager.SaveAuthSession(result.Data.token, result.Data);
IchniOnlineApiClient.Instance.JwtToken = result.Data.token;
OnLoginSuccess?.Invoke(result.Data);
if (!string.IsNullOrEmpty(result.Data.pendingBindOauthId))
{
_pendingBindOauthId = result.Data.pendingBindOauthId;
var bindUrl = $"{WebBaseUrl}/bind?method=0&id={_pendingBindOauthId}";
OnTapTapUnbound?.Invoke(_pendingBindOauthId, bindUrl);
OpenBrowserBindPage(bindUrl);
_pollingCts?.Cancel();
_pollingCts = new CancellationTokenSource();
PollBindStatusAsync(_pendingBindOauthId, _pollingCts.Token).Forget();
}
else
{
LoginCacheManager.SaveAuthSession(result.Data.token, result.Data);
IchniOnlineApiClient.Instance.JwtToken = result.Data.token;
OnLoginSuccess?.Invoke(result.Data);
}
}
else if (result.IsSuccess && result.Data == null)
{
@@ -132,6 +161,94 @@ namespace IchniOnline.Online.Logic
}
}
private static void OpenBrowserBindPage(string url)
{
Application.OpenURL(url);
}
private static async UniTaskVoid PollBindStatusAsync(string oauthId, CancellationToken cancellationToken)
{
var startTime = DateTime.UtcNow;
var timeout = TimeSpan.FromMinutes(5);
var pollInterval = TimeSpan.FromSeconds(2);
while (!cancellationToken.IsCancellationRequested)
{
if (DateTime.UtcNow - startTime > timeout)
{
IsLoggingIn = false;
OnLoginFailed?.Invoke("绑定超时");
return;
}
try
{
var result = await IchniOnlineApiClient.Instance.GetAsync<BindStatusDto>(
$"/api/auth/third-party/bind-status?oauthId={oauthId}",
cancellationToken);
if (result.IsSuccess && result.Data != null)
{
if (result.Data.status == "bound" && !string.IsNullOrEmpty(result.Data.token))
{
var loginResponse = new LoginResponseDto
{
token = result.Data.token,
user = result.Data.user
};
LoginCacheManager.SaveAuthSession(result.Data.token, loginResponse);
IchniOnlineApiClient.Instance.JwtToken = result.Data.token;
IsLoggingIn = false;
OnLoginSuccess?.Invoke(loginResponse);
return;
}
}
}
catch (Exception ex)
{
Debug.LogWarning($"[IchniOnlineAuthService] Polling error: {ex.Message}");
}
await UniTask.Delay(pollInterval,DelayType.Realtime,PlayerLoopTiming.Update,cancellationToken);
}
}
public static void HandleIchniProtocolCallback(string token)
{
if (string.IsNullOrEmpty(token))
{
OnLoginFailed?.Invoke("浏览器回调 token 为空");
return;
}
_pollingCts?.Cancel();
_pollingCts = null;
_pendingBindOauthId = null;
IchniOnlineApiClient.Instance.JwtToken = token;
LoginCacheData cachedData = LoginCacheManager.CachedData;
if (cachedData != null)
{
cachedData.jwtToken = token;
cachedData.hasServerSession = true;
}
OnLoginSuccess?.Invoke(new LoginResponseDto
{
token = token,
user = cachedData != null ? new UserResponseDto
{
userId = cachedData.userId,
username = cachedData.name,
displayName = cachedData.displayName,
avatarUrl = cachedData.avatarUrl,
permission = cachedData.permission
} : null
});
}
private static void UnsubscribeTapTapEvents(
Action<TapTapAccount> onSuccess,
Action onCanceled,

View File

@@ -0,0 +1,108 @@
using System;
using UnityEngine;
namespace IchniOnline.Online.Logic
{
public class IchniProtocolHandler : MonoBehaviour
{
private static IchniProtocolHandler _instance;
public static IchniProtocolHandler Instance => _instance;
private void Awake()
{
if (_instance != null)
{
Destroy(gameObject);
return;
}
_instance = this;
DontDestroyOnLoad(gameObject);
Application.deepLinkActivated += OnDeepLinkActivated;
if (!string.IsNullOrEmpty(Application.absoluteURL))
{
OnDeepLinkActivated(Application.absoluteURL);
}
}
private void OnDestroy()
{
if (_instance == this)
{
Application.deepLinkActivated -= OnDeepLinkActivated;
}
}
private void OnDeepLinkActivated(string url)
{
if (string.IsNullOrEmpty(url))
{
Debug.LogWarning("[IchniProtocolHandler] Deep link URL is empty");
return;
}
Debug.Log($"[IchniProtocolHandler] Deep link activated: {url}");
if (url.StartsWith("ichni://auth"))
{
HandleAuthCallback(url);
}
else
{
Debug.LogWarning($"[IchniProtocolHandler] Unknown deep link: {url}");
}
}
private void HandleAuthCallback(string url)
{
try
{
var uri = new Uri(url);
var query = uri.Query.TrimStart('?');
var queryParams = ParseQueryString(query);
var token = queryParams.ContainsKey("token") ? queryParams["token"] : null;
if (!string.IsNullOrEmpty(token))
{
Debug.Log("[IchniProtocolHandler] Auth callback received, completing login");
IchniOnlineAuthService.HandleIchniProtocolCallback(token);
}
else
{
var error = queryParams.ContainsKey("error") ? queryParams["error"] : null;
if (!string.IsNullOrEmpty(error))
{
Debug.LogError($"[IchniProtocolHandler] Auth callback error: {error}");
}
else
{
Debug.LogError("[IchniProtocolHandler] Auth callback missing token");
}
}
}
catch (Exception ex)
{
Debug.LogError($"[IchniProtocolHandler] Failed to parse deep link: {ex.Message}");
}
}
private static System.Collections.Generic.Dictionary<string, string> ParseQueryString(string query)
{
var result = new System.Collections.Generic.Dictionary<string, string>();
if (string.IsNullOrEmpty(query)) return result;
var pairs = query.Split('&');
foreach (var pair in pairs)
{
var parts = pair.Split('=');
if (parts.Length == 2)
{
result[Uri.UnescapeDataString(parts[0])] = Uri.UnescapeDataString(parts[1]);
}
}
return result;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ce4f9cf53ae8deb498233195d542cf58

View File

@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using Best.HTTP;
using Cysharp.Threading.Tasks;
using IchniOnline.Online.Network.Models;
@@ -17,12 +18,12 @@ namespace IchniOnline.Online.Network
private static IchniOnlineApiClient _instance;
public static IchniOnlineApiClient Instance => _instance ??= new IchniOnlineApiClient();
public string BaseUrl { get; set; } = "http://localhost:5308";
public string BaseUrl { get; set; } = "https://ichni.hoshino.fan";
public string JwtToken { get; set; }
private IchniOnlineApiClient() { }
public async UniTask<ApiResult<T>> GetAsync<T>(string endpoint)
public async UniTask<ApiResult<T>> GetAsync<T>(string endpoint, CancellationToken cancellationToken = default)
{
string url = BuildUrl(endpoint);
var request = new HTTPRequest(new Uri(url), HTTPMethods.Get);
@@ -30,16 +31,20 @@ namespace IchniOnline.Online.Network
try
{
var resp = await SendAsync(request);
var resp = await SendAsync(request, cancellationToken);
return ProcessResponse<T>(resp);
}
catch (OperationCanceledException)
{
return ApiResult<T>.Fail(ResponseCode.InternalServerError, "Request canceled");
}
catch (Exception ex)
{
return ApiResult<T>.Fail(ResponseCode.InternalServerError, "Network error", ex.Message);
}
}
public async UniTask<ApiResult<T>> PostAsync<T>(string endpoint, object body)
public async UniTask<ApiResult<T>> PostAsync<T>(string endpoint, object body, CancellationToken cancellationToken = default)
{
string url = BuildUrl(endpoint);
var request = new HTTPRequest(new Uri(url), HTTPMethods.Post);
@@ -54,9 +59,13 @@ namespace IchniOnline.Online.Network
try
{
var resp = await SendAsync(request);
var resp = await SendAsync(request, cancellationToken);
return ProcessResponse<T>(resp);
}
catch (OperationCanceledException)
{
return ApiResult<T>.Fail(ResponseCode.InternalServerError, "Request canceled");
}
catch (Exception ex)
{
return ApiResult<T>.Fail(ResponseCode.InternalServerError, "Network error", ex.Message);
@@ -81,12 +90,23 @@ namespace IchniOnline.Online.Network
}
}
private UniTask<HTTPResponse> SendAsync(HTTPRequest request)
private UniTask<HTTPResponse> SendAsync(HTTPRequest request, CancellationToken cancellationToken = default)
{
var completionSource = new UniTaskCompletionSource<HTTPResponse>();
var cancellationRegistration = default(CancellationTokenRegistration);
if (cancellationToken.CanBeCanceled)
{
cancellationRegistration = cancellationToken.Register(() =>
{
request.Abort();
completionSource.TrySetCanceled();
});
}
request.Callback = (req, resp) =>
{
cancellationRegistration.Dispose();
switch (req.State)
{
case HTTPRequestStates.Finished:

View File

@@ -32,6 +32,7 @@ namespace IchniOnline.Online.Network.Models
{
public string token;
public UserResponseDto user;
public string pendingBindOauthId; // 未绑定时有值,用于打开浏览器绑定页面
}
[Serializable]
@@ -43,4 +44,39 @@ namespace IchniOnline.Online.Network.Models
public string avatarUrl;
public int permission;
}
[Serializable]
public class PendingBindInfoDto
{
public string oauthId;
public string name;
public string avatarUrl;
public int method;
}
[Serializable]
public class BindExistingRequestDto
{
public string oauthId;
public string username;
public string encryptedPassword;
public string sessionKey;
}
[Serializable]
public class CreateAndBindRequestDto
{
public string oauthId;
public string username;
public string password;
public string displayName;
}
[Serializable]
public class BindStatusDto
{
public string status;
public string token;
public UserResponseDto user;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 443958d017e24a7da33ad36fecf13d33
timeCreated: 1781771657

View File

@@ -0,0 +1,130 @@
namespace IchniOnline.Online.Util
{
using System;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEngine;
[InitializeOnLoad]
public static class EditorFileLogger
{
private static readonly object LockObj = new();
private static StreamWriter writer;
static EditorFileLogger()
{
try
{
string logDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
"UnityEditorLogs");
Directory.CreateDirectory(logDir);
string logFile = Path.Combine(
logDir,
$"Editor_{DateTime.Now:yyyyMMdd_HHmmss}.log");
writer = new StreamWriter(
new FileStream(
logFile,
FileMode.Create,
FileAccess.Write,
FileShare.ReadWrite,
4096,
FileOptions.WriteThrough),
Encoding.UTF8);
writer.AutoFlush = true;
Application.logMessageReceivedThreaded += OnLog;
EditorApplication.quitting += Shutdown;
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
WriteLine("====================================");
WriteLine($"Unity Version : {Application.unityVersion}");
WriteLine($"Project : {Application.productName}");
WriteLine($"Started : {DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}");
WriteLine("====================================");
}
catch (Exception e)
{
Debug.LogException(e);
}
}
private static void OnPlayModeStateChanged(PlayModeStateChange state)
{
switch (state)
{
case PlayModeStateChange.ExitingEditMode:
WriteLine("[EDITOR] >>> Preparing To Enter Play Mode");
break;
case PlayModeStateChange.EnteredPlayMode:
WriteLine("[EDITOR] >>> Entered Play Mode");
break;
case PlayModeStateChange.ExitingPlayMode:
WriteLine("[EDITOR] <<< Exiting Play Mode");
break;
case PlayModeStateChange.EnteredEditMode:
WriteLine("[EDITOR] <<< Returned To Edit Mode");
break;
}
}
private static void OnLog(
string condition,
string stackTrace,
LogType type)
{
lock (LockObj)
{
if (writer == null)
return;
writer.WriteLine(
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] [{type}] {condition}");
if (!string.IsNullOrWhiteSpace(stackTrace))
{
writer.WriteLine(stackTrace);
}
writer.Flush();
}
}
private static void WriteLine(string message)
{
lock (LockObj)
{
if (writer == null)
return;
writer.WriteLine(
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {message}");
writer.Flush();
}
}
private static void Shutdown()
{
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
EditorApplication.quitting -= Shutdown;
Application.logMessageReceivedThreaded -= OnLog;
lock (LockObj)
{
writer?.Flush();
writer?.Dispose();
writer = null;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2169063fd004462880557c15613be8f9
timeCreated: 1781771972

View File

@@ -0,0 +1,141 @@
using System;
using System.IO;
using System.Text;
using UnityEngine;
namespace IchniOnline.Online.Util
{
public class PersistentFileLogger : MonoBehaviour
{
private static StreamWriter _writer;
private static readonly object LockObj = new object();
private string _logFilePath;
[Header("日志文件夹名称")]
public string logFolderName = "Logs";
[Header("启动时打印日志路径")]
public bool printLogPath = true;
private void Awake()
{
DontDestroyOnLoad(gameObject);
InitializeLogger();
}
private void InitializeLogger()
{
if (_writer != null)
return;
string logDir = Path.Combine(
Application.persistentDataPath,
logFolderName);
Directory.CreateDirectory(logDir);
string fileName =
$"log_{DateTime.Now:yyyyMMdd_HHmmss}.txt";
_logFilePath = Path.Combine(logDir, fileName);
_writer = new StreamWriter(
new FileStream(
_logFilePath,
FileMode.Create,
FileAccess.Write,
FileShare.ReadWrite,
4096,
FileOptions.WriteThrough),
Encoding.UTF8);
_writer.AutoFlush = true;
Application.logMessageReceivedThreaded += OnLogReceived;
WriteRaw("==================================================");
WriteRaw($"Unity Version : {Application.unityVersion}");
WriteRaw($"Platform : {Application.platform}");
WriteRaw($"Start Time : {DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}");
WriteRaw($"Log File : {_logFilePath}");
WriteRaw("==================================================");
if (printLogPath)
{
Debug.Log($"Log file: {_logFilePath}");
}
}
private void OnDestroy()
{
ShutdownLogger();
}
private void OnApplicationQuit()
{
ShutdownLogger();
}
private void ShutdownLogger()
{
Application.logMessageReceivedThreaded -= OnLogReceived;
lock (LockObj)
{
if (_writer != null)
{
WriteRaw("Application Exit");
_writer.Flush();
_writer.Close();
_writer.Dispose();
_writer = null;
}
}
}
private static void OnLogReceived(
string condition,
string stackTrace,
LogType type)
{
lock (LockObj)
{
if (_writer == null)
return;
string timestamp =
DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
_writer.WriteLine(
$"[{timestamp}] [{type}] {condition}");
if (type == LogType.Error ||
type == LogType.Exception ||
type == LogType.Assert)
{
if (!string.IsNullOrWhiteSpace(stackTrace))
{
_writer.WriteLine(stackTrace);
}
}
_writer.Flush();
}
}
private static void WriteRaw(string message)
{
lock (LockObj)
{
if (_writer == null)
return;
_writer.WriteLine(message);
_writer.Flush();
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7e726f6c24c3464cbc0ccc66f372c55f
timeCreated: 1781771683