You've already forked CC-Framework.BriskGameServer
Release v0.2.0 archive and space API update
This commit is contained in:
@@ -50,7 +50,7 @@ public sealed class BriskArchiveModule
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var finalChecksum = string.IsNullOrWhiteSpace(checksum) ? ComputeSha256(bytes) : checksum;
|
||||
var finalChecksum = string.IsNullOrWhiteSpace(checksum) ? ComputeSha256(bytes) : NormalizeChecksum(checksum);
|
||||
var sections = new List<IMultipartFormSection>
|
||||
{
|
||||
new MultipartFormDataSection("base_version", (baseVersion ?? 0).ToString()),
|
||||
@@ -63,6 +63,24 @@ public sealed class BriskArchiveModule
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以 UTF-8 文本形式上传指定槽位的存档。
|
||||
/// </summary>
|
||||
public Task<BriskArchiveUploadResult> UploadTextAsync(int slotNo, string text, int? baseVersion = null, string checksum = null)
|
||||
{
|
||||
RequireNotNull(text, nameof(text));
|
||||
return UploadAsync(slotNo, Encoding.UTF8.GetBytes(text), baseVersion, checksum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以 JSON 文本形式上传指定槽位的存档。
|
||||
/// </summary>
|
||||
public Task<BriskArchiveUploadResult> UploadJsonAsync(int slotNo, object payload, int? baseVersion = null, string checksum = null)
|
||||
{
|
||||
RequireNotNull(payload, nameof(payload));
|
||||
return UploadAsync(slotNo, Encoding.UTF8.GetBytes(BriskJson.Serialize(payload)), baseVersion, checksum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载指定槽位的二进制存档。
|
||||
/// </summary>
|
||||
@@ -82,6 +100,24 @@ public sealed class BriskArchiveModule
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以 UTF-8 文本形式下载指定槽位的存档。
|
||||
/// </summary>
|
||||
public async Task<string> DownloadTextAsync(int slotNo)
|
||||
{
|
||||
var result = await DownloadAsync(slotNo);
|
||||
return result == null || result.Bytes == null ? string.Empty : Encoding.UTF8.GetString(result.Bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以 JSON 对象形式下载指定槽位的存档。
|
||||
/// </summary>
|
||||
public async Task<object> DownloadJsonAsync(int slotNo)
|
||||
{
|
||||
var text = await DownloadTextAsync(slotNo);
|
||||
return string.IsNullOrWhiteSpace(text) ? null : BriskJson.Deserialize(text);
|
||||
}
|
||||
|
||||
private static void ValidateSlotNo(int slotNo)
|
||||
{
|
||||
RequirePositive(slotNo, nameof(slotNo), "slotNo must be greater than 0.");
|
||||
@@ -92,8 +128,7 @@ public sealed class BriskArchiveModule
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
var builder = new StringBuilder(hash.Length * 2 + 7);
|
||||
builder.Append("sha256:");
|
||||
var builder = new StringBuilder(hash.Length * 2);
|
||||
for (var i = 0; i < hash.Length; i++)
|
||||
{
|
||||
builder.Append(hash[i].ToString("x2"));
|
||||
@@ -103,6 +138,19 @@ public sealed class BriskArchiveModule
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeChecksum(string checksum)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(checksum))
|
||||
{
|
||||
return checksum;
|
||||
}
|
||||
|
||||
var value = checksum.Trim();
|
||||
return value.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase)
|
||||
? value.Substring("sha256:".Length)
|
||||
: value;
|
||||
}
|
||||
|
||||
private static int ReadHeaderInt(Dictionary<string, string> headers, string key)
|
||||
{
|
||||
var value = ReadHeader(headers, key);
|
||||
|
||||
@@ -51,23 +51,26 @@ public sealed class BriskHttpClient
|
||||
|
||||
public async Task<Dictionary<string, object>> PostMultipartAsync(string path, List<IMultipartFormSection> formSections, bool auth = false)
|
||||
{
|
||||
using (var request = UnityWebRequest.Post(BuildUrl(path, null), formSections))
|
||||
return await SendMultipartAsync(UnityWebRequest.kHttpVerbPOST, path, formSections, auth);
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, object>> PutMultipartAsync(string path, List<IMultipartFormSection> formSections, bool auth = false)
|
||||
{
|
||||
return await SendMultipartAsync(UnityWebRequest.kHttpVerbPUT, path, formSections, auth);
|
||||
}
|
||||
|
||||
public async Task<BriskBinaryResponse> GetBytesAsync(string path, Dictionary<string, string> query = null, bool auth = false)
|
||||
{
|
||||
using (var request = new UnityWebRequest(BuildUrl(path, query), UnityWebRequest.kHttpVerbGET))
|
||||
{
|
||||
request.downloadHandler = new DownloadHandlerBuffer();
|
||||
|
||||
if (auth)
|
||||
{
|
||||
AddAuthorizationHeader(request);
|
||||
}
|
||||
|
||||
request.SetRequestHeader("Accept", "application/json");
|
||||
await SendRequestAsync(request);
|
||||
return ParseEnvelope(request) as Dictionary<string, object> ?? new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<BriskBinaryResponse> GetBytesAsync(string path, Dictionary<string, string> query = null, bool auth = false)
|
||||
{
|
||||
using (var request = BuildRequest(UnityWebRequest.kHttpVerbGET, path, query, null, auth))
|
||||
{
|
||||
request.SetRequestHeader("Accept", "*/*");
|
||||
await SendRequestAsync(request);
|
||||
EnsureSuccessOrThrow(request);
|
||||
return new BriskBinaryResponse
|
||||
@@ -93,6 +96,23 @@ public sealed class BriskHttpClient
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, object>> SendMultipartAsync(string method, string path, List<IMultipartFormSection> formSections, bool auth)
|
||||
{
|
||||
using (var request = UnityWebRequest.Post(BuildUrl(path, null), formSections))
|
||||
{
|
||||
request.method = method;
|
||||
|
||||
if (auth)
|
||||
{
|
||||
AddAuthorizationHeader(request);
|
||||
}
|
||||
|
||||
request.SetRequestHeader("Accept", "application/json");
|
||||
await SendRequestAsync(request);
|
||||
return ParseEnvelope(request) as Dictionary<string, object> ?? new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
|
||||
private UnityWebRequest BuildRequest(string method, string path, Dictionary<string, string> query, object body, bool auth)
|
||||
{
|
||||
var url = BuildUrl(path, query);
|
||||
|
||||
@@ -237,10 +237,21 @@ internal static class BriskModelMapper
|
||||
|
||||
return new BriskSpaceView
|
||||
{
|
||||
ProjectAccountId = BriskValueReader.GetString(data, "project_account_id"),
|
||||
PlayerId = BriskValueReader.GetString(data, "player_id"),
|
||||
LoginProvider = BriskValueReader.GetString(data, "login_provider"),
|
||||
LoginUserId = BriskValueReader.GetString(data, "login_user_id"),
|
||||
Payload = BriskValueReader.GetDictionary(data, "payload_json") ?? BriskValueReader.GetDictionary(data, "payload")
|
||||
Nickname = BriskValueReader.GetString(data, "nickname"),
|
||||
AvatarUrl = BriskValueReader.GetString(data, "avatar_url"),
|
||||
ContentExists = BriskValueReader.GetBool(data, "content_exists"),
|
||||
ContentVersion = BriskValueReader.GetLong(data, "content_version"),
|
||||
ContentType = BriskValueReader.GetString(data, "content_type"),
|
||||
ContentSizeBytes = BriskValueReader.GetLong(data, "content_size_bytes"),
|
||||
ContentChecksum = BriskValueReader.GetString(data, "content_checksum"),
|
||||
LikeCount = BriskValueReader.GetLong(data, "like_count"),
|
||||
VisitCount = BriskValueReader.GetLong(data, "visit_count"),
|
||||
LikedByMe = BriskValueReader.GetBool(data, "liked_by_me"),
|
||||
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -253,8 +264,52 @@ internal static class BriskModelMapper
|
||||
|
||||
return new BriskSpaceStats
|
||||
{
|
||||
LikeCount = BriskValueReader.GetInt(data, "like_count"),
|
||||
VisitCount = BriskValueReader.GetInt(data, "visit_count")
|
||||
ProjectAccountId = BriskValueReader.GetString(data, "project_account_id"),
|
||||
PlayerId = BriskValueReader.GetString(data, "player_id"),
|
||||
LoginProvider = BriskValueReader.GetString(data, "login_provider"),
|
||||
LoginUserId = BriskValueReader.GetString(data, "login_user_id"),
|
||||
Nickname = BriskValueReader.GetString(data, "nickname"),
|
||||
AvatarUrl = BriskValueReader.GetString(data, "avatar_url"),
|
||||
ContentExists = BriskValueReader.GetBool(data, "content_exists"),
|
||||
ContentVersion = BriskValueReader.GetLong(data, "content_version"),
|
||||
ContentType = BriskValueReader.GetString(data, "content_type"),
|
||||
ContentSizeBytes = BriskValueReader.GetLong(data, "content_size_bytes"),
|
||||
ContentChecksum = BriskValueReader.GetString(data, "content_checksum"),
|
||||
LikeCount = BriskValueReader.GetLong(data, "like_count"),
|
||||
VisitCount = BriskValueReader.GetLong(data, "visit_count"),
|
||||
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
|
||||
};
|
||||
}
|
||||
|
||||
public static BriskSpaceContentUpdateResult ToSpaceContentUpdateResult(Dictionary<string, object> data)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BriskSpaceContentUpdateResult
|
||||
{
|
||||
PlayerId = BriskValueReader.GetString(data, "player_id"),
|
||||
ContentVersion = BriskValueReader.GetLong(data, "content_version"),
|
||||
ContentType = BriskValueReader.GetString(data, "content_type"),
|
||||
ContentSizeBytes = BriskValueReader.GetLong(data, "content_size_bytes"),
|
||||
ContentChecksum = BriskValueReader.GetString(data, "content_checksum"),
|
||||
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
|
||||
};
|
||||
}
|
||||
|
||||
public static BriskSpaceLikeResult ToSpaceLikeResult(Dictionary<string, object> data)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BriskSpaceLikeResult
|
||||
{
|
||||
Liked = BriskValueReader.GetBool(data, "liked"),
|
||||
LikeCount = BriskValueReader.GetLong(data, "like_count")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -284,6 +339,30 @@ internal static class BriskModelMapper
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static BriskSpaceLikeItem ToSpaceLikeItem(Dictionary<string, object> data)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BriskSpaceLikeItem
|
||||
{
|
||||
PlayerId = BriskValueReader.GetString(data, "player_id"),
|
||||
Nickname = BriskValueReader.GetString(data, "nickname"),
|
||||
AvatarUrl = BriskValueReader.GetString(data, "avatar_url"),
|
||||
CreatedAt = BriskValueReader.GetString(data, "created_at")
|
||||
};
|
||||
}
|
||||
|
||||
public static List<BriskSpaceLikeItem> ToSpaceLikeItems(object data)
|
||||
{
|
||||
return ExtractList(data)
|
||||
.Select(item => ToSpaceLikeItem(item as Dictionary<string, object>))
|
||||
.Where(item => item != null)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static Dictionary<string, object> ExtractObject(object data)
|
||||
{
|
||||
return data as Dictionary<string, object>;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
public sealed class BriskSpaceContentDownloadResult
|
||||
{
|
||||
public byte[] Bytes;
|
||||
public long Version;
|
||||
public string Checksum;
|
||||
public string ContentType;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6f27d1ba8b24f5e85362b9c6c9f4c01
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
public sealed class BriskSpaceContentUpdateResult
|
||||
{
|
||||
public string PlayerId;
|
||||
public long ContentVersion;
|
||||
public string ContentType;
|
||||
public long ContentSizeBytes;
|
||||
public string ContentChecksum;
|
||||
public string UpdatedAt;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4bd67f378d264cc69c71f1d03d5b564b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
7
Assets/BriskSdk/Runtime/Models/BriskSpaceLikeItem.cs
Normal file
7
Assets/BriskSdk/Runtime/Models/BriskSpaceLikeItem.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
public sealed class BriskSpaceLikeItem
|
||||
{
|
||||
public string PlayerId;
|
||||
public string Nickname;
|
||||
public string AvatarUrl;
|
||||
public string CreatedAt;
|
||||
}
|
||||
11
Assets/BriskSdk/Runtime/Models/BriskSpaceLikeItem.cs.meta
Normal file
11
Assets/BriskSdk/Runtime/Models/BriskSpaceLikeItem.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef7d53b3f0ab43e7b74a4c23a76f84f5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
5
Assets/BriskSdk/Runtime/Models/BriskSpaceLikeResult.cs
Normal file
5
Assets/BriskSdk/Runtime/Models/BriskSpaceLikeResult.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
public sealed class BriskSpaceLikeResult
|
||||
{
|
||||
public bool Liked;
|
||||
public long LikeCount;
|
||||
}
|
||||
11
Assets/BriskSdk/Runtime/Models/BriskSpaceLikeResult.cs.meta
Normal file
11
Assets/BriskSdk/Runtime/Models/BriskSpaceLikeResult.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 966f47c348db46dfb60120dba20f1ffb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,5 +1,17 @@
|
||||
public sealed class BriskSpaceStats
|
||||
{
|
||||
public int LikeCount;
|
||||
public int VisitCount;
|
||||
public string ProjectAccountId;
|
||||
public string PlayerId;
|
||||
public string LoginProvider;
|
||||
public string LoginUserId;
|
||||
public string Nickname;
|
||||
public string AvatarUrl;
|
||||
public bool ContentExists;
|
||||
public long ContentVersion;
|
||||
public string ContentType;
|
||||
public long ContentSizeBytes;
|
||||
public string ContentChecksum;
|
||||
public long LikeCount;
|
||||
public long VisitCount;
|
||||
public string UpdatedAt;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
public sealed class BriskSpaceView
|
||||
{
|
||||
public string ProjectAccountId;
|
||||
public string PlayerId;
|
||||
public string LoginProvider;
|
||||
public string LoginUserId;
|
||||
public object Payload;
|
||||
public string Nickname;
|
||||
public string AvatarUrl;
|
||||
public bool ContentExists;
|
||||
public long ContentVersion;
|
||||
public string ContentType;
|
||||
public long ContentSizeBytes;
|
||||
public string ContentChecksum;
|
||||
public long LikeCount;
|
||||
public long VisitCount;
|
||||
public bool LikedByMe;
|
||||
public string UpdatedAt;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
/// <summary>
|
||||
/// 玩家空间模块。
|
||||
@@ -65,77 +68,179 @@ public sealed class BriskSpaceModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按玩家 ID 点赞空间。
|
||||
/// 按玩家 ID 获取最近点赞列表。
|
||||
/// </summary>
|
||||
public async Task LikeByPlayerIdAsync(string playerId)
|
||||
public async Task<IReadOnlyList<BriskSpaceLikeItem>> GetLikesByPlayerIdAsync(string playerId, int limit = 20)
|
||||
{
|
||||
ValidatePlayerId(playerId);
|
||||
await ExecuteAsync(async context =>
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
await context.HttpClient.PostJsonRawAsync($"/spaces/{playerId}/like", new Dictionary<string, object>(), true);
|
||||
var data = await context.HttpClient.GetRawDataAsync($"/spaces/{playerId}/likes", CreateLimitQuery(limit), true);
|
||||
return (IReadOnlyList<BriskSpaceLikeItem>)BriskModelMapper.ToSpaceLikeItems(data);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按登录身份获取最近点赞列表。
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<BriskSpaceLikeItem>> GetLikesByLoginIdentityAsync(string loginProvider, string loginUserId, int limit = 20)
|
||||
{
|
||||
ValidateLoginIdentity(loginProvider, loginUserId);
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var query = CreateLoginIdentityQuery(loginProvider, loginUserId);
|
||||
query["limit"] = NormalizeLimit(limit);
|
||||
var data = await context.HttpClient.GetRawDataAsync("/spaces/by-login/likes", query, true);
|
||||
return (IReadOnlyList<BriskSpaceLikeItem>)BriskModelMapper.ToSpaceLikeItems(data);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按玩家 ID 点赞空间。
|
||||
/// </summary>
|
||||
public async Task<BriskSpaceLikeResult> LikeByPlayerIdAsync(string playerId)
|
||||
{
|
||||
ValidatePlayerId(playerId);
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var data = await context.HttpClient.PostJsonRawAsync($"/spaces/{playerId}/like", new Dictionary<string, object>(), true);
|
||||
return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按玩家 ID 取消点赞空间。
|
||||
/// </summary>
|
||||
public async Task UnlikeByPlayerIdAsync(string playerId)
|
||||
public async Task<BriskSpaceLikeResult> UnlikeByPlayerIdAsync(string playerId)
|
||||
{
|
||||
ValidatePlayerId(playerId);
|
||||
await ExecuteAsync(async context =>
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
await context.HttpClient.SendDeleteJsonRawAsync($"/spaces/{playerId}/like", null, true);
|
||||
var data = await context.HttpClient.SendDeleteJsonRawAsync($"/spaces/{playerId}/like", null, true);
|
||||
return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按登录身份点赞空间。
|
||||
/// </summary>
|
||||
public async Task LikeByLoginIdentityAsync(string loginProvider, string loginUserId)
|
||||
public async Task<BriskSpaceLikeResult> LikeByLoginIdentityAsync(string loginProvider, string loginUserId)
|
||||
{
|
||||
ValidateLoginIdentity(loginProvider, loginUserId);
|
||||
await ExecuteAsync(async context =>
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
await context.HttpClient.PostJsonRawAsync("/spaces/by-login/like", new Dictionary<string, object>(), true, CreateLoginIdentityQuery(loginProvider, loginUserId));
|
||||
var data = await context.HttpClient.PostJsonRawAsync("/spaces/by-login/like", new Dictionary<string, object>(), true, CreateLoginIdentityQuery(loginProvider, loginUserId));
|
||||
return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按登录身份取消点赞空间。
|
||||
/// </summary>
|
||||
public async Task UnlikeByLoginIdentityAsync(string loginProvider, string loginUserId)
|
||||
public async Task<BriskSpaceLikeResult> UnlikeByLoginIdentityAsync(string loginProvider, string loginUserId)
|
||||
{
|
||||
ValidateLoginIdentity(loginProvider, loginUserId);
|
||||
await ExecuteAsync(async context =>
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
await context.HttpClient.SendDeleteJsonRawAsync("/spaces/by-login/like", CreateLoginIdentityQuery(loginProvider, loginUserId), true);
|
||||
var data = await context.HttpClient.SendDeleteJsonRawAsync("/spaces/by-login/like", CreateLoginIdentityQuery(loginProvider, loginUserId), true);
|
||||
return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传当前玩家自己的空间内容。
|
||||
/// </summary>
|
||||
public async Task<BriskSpaceContentUpdateResult> UploadMyContentAsync(long? baseVersion, string contentType, string checksum, byte[] bytes)
|
||||
{
|
||||
RequireNotNull(bytes, nameof(bytes));
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var finalChecksum = string.IsNullOrWhiteSpace(checksum) ? ComputeSha256(bytes) : NormalizeChecksum(checksum);
|
||||
var sections = new List<IMultipartFormSection>
|
||||
{
|
||||
new MultipartFormDataSection("base_version", (baseVersion ?? 0L).ToString()),
|
||||
new MultipartFormFileSection("file", bytes, "space-content.bin", string.IsNullOrWhiteSpace(contentType) ? "application/octet-stream" : contentType)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(contentType))
|
||||
{
|
||||
sections.Insert(1, new MultipartFormDataSection("content_type", contentType));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(finalChecksum))
|
||||
{
|
||||
sections.Insert(string.IsNullOrWhiteSpace(contentType) ? 1 : 2, new MultipartFormDataSection("checksum", finalChecksum));
|
||||
}
|
||||
|
||||
var data = await context.HttpClient.PutMultipartAsync("/spaces/me/content", sections, true);
|
||||
return BriskModelMapper.ToSpaceContentUpdateResult(data);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新当前玩家自己的空间内容。
|
||||
/// </summary>
|
||||
public async Task UpdateMyAsync(object payload)
|
||||
public Task<BriskSpaceContentUpdateResult> UpdateMyAsync(object payload, long? baseVersion = null, string contentType = null, string checksum = null)
|
||||
{
|
||||
RequireNotNull(payload, nameof(payload));
|
||||
|
||||
await ExecuteAsync(async context =>
|
||||
if (payload is byte[] bytes)
|
||||
{
|
||||
await context.HttpClient.SendPutJsonRawAsync(
|
||||
"/spaces/me",
|
||||
new Dictionary<string, object> { { "payload_json", payload } },
|
||||
true);
|
||||
return UploadMyContentAsync(baseVersion, contentType ?? "application/octet-stream", checksum, bytes);
|
||||
}
|
||||
|
||||
if (payload is string text)
|
||||
{
|
||||
return UploadMyContentAsync(baseVersion, contentType ?? "text/plain", checksum, Encoding.UTF8.GetBytes(text));
|
||||
}
|
||||
|
||||
var json = BriskJson.Serialize(payload);
|
||||
return UploadMyContentAsync(baseVersion, contentType ?? "application/json", checksum, Encoding.UTF8.GetBytes(json));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按玩家 ID 下载空间内容。
|
||||
/// </summary>
|
||||
public async Task<BriskSpaceContentDownloadResult> DownloadContentByPlayerIdAsync(string playerId)
|
||||
{
|
||||
ValidatePlayerId(playerId);
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var response = await context.HttpClient.GetBytesAsync($"/spaces/{playerId}/content", null, true);
|
||||
return CreateDownloadResult(response);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按登录身份下载空间内容。
|
||||
/// </summary>
|
||||
public async Task<BriskSpaceContentDownloadResult> DownloadContentByLoginIdentityAsync(string loginProvider, string loginUserId)
|
||||
{
|
||||
ValidateLoginIdentity(loginProvider, loginUserId);
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var response = await context.HttpClient.GetBytesAsync("/spaces/by-login/content", CreateLoginIdentityQuery(loginProvider, loginUserId), true);
|
||||
return CreateDownloadResult(response);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取我的访客列表。
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<BriskSpaceVisit>> GetMyVisitsAsync()
|
||||
public async Task<IReadOnlyList<BriskSpaceVisit>> GetMyVisitsAsync(int limit = 20)
|
||||
{
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var data = await context.HttpClient.GetRawDataAsync("/spaces/me/visits", null, true);
|
||||
var data = await context.HttpClient.GetRawDataAsync("/spaces/me/visits", CreateLimitQuery(limit), true);
|
||||
return (IReadOnlyList<BriskSpaceVisit>)BriskModelMapper.ToSpaceVisits(data);
|
||||
});
|
||||
}
|
||||
@@ -149,6 +254,14 @@ public sealed class BriskSpaceModule
|
||||
};
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> CreateLimitQuery(int limit)
|
||||
{
|
||||
return new Dictionary<string, string>
|
||||
{
|
||||
{ "limit", NormalizeLimit(limit) }
|
||||
};
|
||||
}
|
||||
|
||||
private static void ValidatePlayerId(string playerId)
|
||||
{
|
||||
RequireNotEmpty(playerId, nameof(playerId));
|
||||
@@ -159,4 +272,72 @@ public sealed class BriskSpaceModule
|
||||
RequireNotEmpty(loginProvider, nameof(loginProvider));
|
||||
RequireNotEmpty(loginUserId, nameof(loginUserId));
|
||||
}
|
||||
|
||||
private static string NormalizeLimit(int limit)
|
||||
{
|
||||
return (limit > 0 ? limit : 20).ToString();
|
||||
}
|
||||
|
||||
private static BriskSpaceContentDownloadResult CreateDownloadResult(BriskBinaryResponse response)
|
||||
{
|
||||
return new BriskSpaceContentDownloadResult
|
||||
{
|
||||
Bytes = response.Bytes,
|
||||
Version = ReadHeaderLong(response.Headers, "X-Space-Version"),
|
||||
Checksum = ReadHeader(response.Headers, "X-Space-Checksum"),
|
||||
ContentType = ReadHeader(response.Headers, "Content-Type")
|
||||
};
|
||||
}
|
||||
|
||||
private static string ComputeSha256(byte[] bytes)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
var builder = new StringBuilder(hash.Length * 2);
|
||||
for (var i = 0; i < hash.Length; i++)
|
||||
{
|
||||
builder.Append(hash[i].ToString("x2"));
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeChecksum(string checksum)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(checksum))
|
||||
{
|
||||
return checksum;
|
||||
}
|
||||
|
||||
var value = checksum.Trim();
|
||||
return value.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase)
|
||||
? value.Substring("sha256:".Length)
|
||||
: value;
|
||||
}
|
||||
|
||||
private static long ReadHeaderLong(Dictionary<string, string> headers, string key)
|
||||
{
|
||||
var value = ReadHeader(headers, key);
|
||||
return long.TryParse(value, out var result) ? result : 0L;
|
||||
}
|
||||
|
||||
private static string ReadHeader(Dictionary<string, string> headers, string key)
|
||||
{
|
||||
if (headers == null || string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var pair in headers)
|
||||
{
|
||||
if (string.Equals(pair.Key, key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return pair.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,7 +38,6 @@ RenderSettings:
|
||||
m_ReflectionIntensity: 1
|
||||
m_CustomReflection: {fileID: 0}
|
||||
m_Sun: {fileID: 0}
|
||||
m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_UseRadianceAmbientProbe: 0
|
||||
--- !u!157 &3
|
||||
LightmapSettings:
|
||||
@@ -104,7 +103,7 @@ NavMeshSettings:
|
||||
serializedVersion: 2
|
||||
m_ObjectHideFlags: 0
|
||||
m_BuildSettings:
|
||||
serializedVersion: 2
|
||||
serializedVersion: 3
|
||||
agentTypeID: 0
|
||||
agentRadius: 0.5
|
||||
agentHeight: 2
|
||||
@@ -117,7 +116,7 @@ NavMeshSettings:
|
||||
cellSize: 0.16666667
|
||||
manualTileSize: 0
|
||||
tileSize: 256
|
||||
accuratePlacement: 0
|
||||
buildHeightMesh: 0
|
||||
maxJobWorkers: 0
|
||||
preserveTilesOutsideBounds: 0
|
||||
debug:
|
||||
@@ -163,9 +162,17 @@ Camera:
|
||||
m_projectionMatrixMode: 1
|
||||
m_GateFitMode: 2
|
||||
m_FOVAxisMode: 0
|
||||
m_Iso: 200
|
||||
m_ShutterSpeed: 0.005
|
||||
m_Aperture: 16
|
||||
m_FocusDistance: 10
|
||||
m_FocalLength: 50
|
||||
m_BladeCount: 5
|
||||
m_Curvature: {x: 2, y: 11}
|
||||
m_BarrelClipping: 0.25
|
||||
m_Anamorphism: 0
|
||||
m_SensorSize: {x: 36, y: 24}
|
||||
m_LensShift: {x: 0, y: 0}
|
||||
m_FocalLength: 50
|
||||
m_NormalizedViewPortRect:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
@@ -199,12 +206,13 @@ Transform:
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 519420028}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: -10}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1600000000
|
||||
GameObject:
|
||||
@@ -230,12 +238,13 @@ Transform:
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1600000000}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_RootOrder: 1
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &1600000002
|
||||
MonoBehaviour:
|
||||
@@ -250,16 +259,16 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
BaseUrl: https://brisk.lightyears.ltd
|
||||
GameKey: demo-game
|
||||
GameKey: briskh5verify
|
||||
ClientVersion: 1.0.0
|
||||
DeviceId: editor-device
|
||||
ValidateSessionOnInitialize: 1
|
||||
LoginProvider: tap
|
||||
LoginUserId: tap_user_10001
|
||||
LoginCode:
|
||||
Nickname: Unity示例玩家
|
||||
Nickname: "Unity\u793A\u4F8B\u73A9\u5BB6"
|
||||
AvatarUrl:
|
||||
RankKey: season-score
|
||||
RankKey: h5-verify-rank-20260410034312-5cdd
|
||||
SubmitScoreValue: 128
|
||||
LeaderboardLimit: 10
|
||||
AroundMeRange: 5
|
||||
@@ -267,10 +276,18 @@ MonoBehaviour:
|
||||
SeasonHistoryLimit: 20
|
||||
ArchiveSlotNo: 1
|
||||
ArchiveBaseVersion:
|
||||
ArchiveContent: '{"save":1,"coins":128,"hero":"mage","title":"中文测试存档"}'
|
||||
ArchiveContent: "{\n \"save\": 1,\n \"coins\": 128,\n \"hero\": \"mage\",\n
|
||||
\"title\": \"\u4E2D\u6587\u6D4B\u8BD5\u5B58\u6863\"\n}"
|
||||
AnnouncementId:
|
||||
SpacePlayerId:
|
||||
SpaceLoginProvider: tap
|
||||
SpaceLoginUserId: tap_user_10001
|
||||
SpacePayloadText: '{"mood":"ready","title":"你好 Brisk","desc":"这是中文测试空间数据"}'
|
||||
SpacePayloadText: "{\n \"mood\": \"ready\",\n \"title\": \"\u4F60\u597D Brisk\",\n
|
||||
\"desc\": \"\u8FD9\u662F\u4E2D\u6587\u6D4B\u8BD5\u7A7A\u95F4\u6570\u636E\"\n}"
|
||||
AutoRunOnStart: 0
|
||||
--- !u!1660057539 &9223372036854775807
|
||||
SceneRoots:
|
||||
m_ObjectHideFlags: 0
|
||||
m_Roots:
|
||||
- {fileID: 519420032}
|
||||
- {fileID: 1600000001}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 0.2.0
|
||||
|
||||
- Refined archive APIs with direct text and JSON upload/download helpers
|
||||
- Refactored player space to the latest metadata plus binary content architecture
|
||||
- Added space content download/update result models and like list models
|
||||
- Updated quick start sample to use the latest archive and space flows
|
||||
- Updated package and integration docs to match the current API surface
|
||||
|
||||
## 0.1.0
|
||||
|
||||
- Initial embedded package structure
|
||||
|
||||
@@ -27,6 +27,52 @@ var top = await Brisk.Leaderboard.GetTopAsync("season-score", 20);
|
||||
await Brisk.Leaderboard.SubmitScoreAsync("season-score", 128);
|
||||
```
|
||||
|
||||
## Archive upload
|
||||
|
||||
```csharp
|
||||
await Brisk.Archive.UploadTextAsync(1, "{\"save\":1}");
|
||||
await Brisk.Archive.UploadJsonAsync(2, new
|
||||
{
|
||||
save = 1,
|
||||
coins = 128
|
||||
});
|
||||
|
||||
var text = await Brisk.Archive.DownloadTextAsync(1);
|
||||
var json = await Brisk.Archive.DownloadJsonAsync(2);
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- if you already have binary data, keep using `Brisk.Archive.UploadAsync(slotNo, bytes)`
|
||||
- if you need version and checksum, keep using `Brisk.Archive.DownloadAsync(slotNo)`
|
||||
- `checksum` is optional in normal use
|
||||
- the SDK computes SHA256 for you automatically
|
||||
- if you pass a manual checksum, use plain SHA256 hex
|
||||
- values like `sha256:abcd...` will be normalized by the SDK before upload
|
||||
|
||||
## Space content
|
||||
|
||||
```csharp
|
||||
await Brisk.Space.UpdateMyAsync("Hello Brisk Space");
|
||||
|
||||
await Brisk.Space.UpdateMyAsync(new
|
||||
{
|
||||
mood = "ready",
|
||||
title = "hello"
|
||||
});
|
||||
|
||||
var mySpace = await Brisk.Space.GetByPlayerIdAsync(Brisk.PlayerId);
|
||||
var myContent = await Brisk.Space.DownloadContentByPlayerIdAsync(Brisk.PlayerId);
|
||||
var text = Encoding.UTF8.GetString(myContent.Bytes);
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- space metadata and space content are now separated
|
||||
- `GetByPlayerIdAsync(...)` returns metadata only
|
||||
- use `DownloadContentByPlayerIdAsync(...)` to read the actual content bytes
|
||||
- `UpdateMyAsync(...)` automatically picks text / binary / json behavior from the payload type
|
||||
|
||||
## Sample
|
||||
|
||||
For the current source project, open directly:
|
||||
|
||||
@@ -24,6 +24,32 @@ Sync package content from the Unity source project with:
|
||||
- Player space
|
||||
- Default blocking error UI
|
||||
|
||||
## Archive checksum
|
||||
|
||||
Archive upload checksum is handled by the SDK by default.
|
||||
|
||||
- For quick use, the archive module now provides:
|
||||
- `UploadAsync(slotNo, bytes)` for binary
|
||||
- `UploadTextAsync(slotNo, text)` for UTF-8 text
|
||||
- `UploadJsonAsync(slotNo, payload)` for JSON objects
|
||||
- `DownloadAsync(slotNo)` for raw bytes + metadata
|
||||
- `DownloadTextAsync(slotNo)` for UTF-8 text
|
||||
- `DownloadJsonAsync(slotNo)` for JSON payloads
|
||||
- The SDK computes SHA256 automatically when uploading archive bytes
|
||||
- The current Brisk archive API expects a plain lowercase SHA256 hex string
|
||||
- Do not send values with a `sha256:` prefix
|
||||
- If a manual checksum includes that prefix, the SDK will normalize it before sending
|
||||
|
||||
## Space content
|
||||
|
||||
Player space now follows a metadata + binary content model.
|
||||
|
||||
- `GetByPlayerIdAsync(...)` and `GetByLoginIdentityAsync(...)` return metadata only
|
||||
- `DownloadContentByPlayerIdAsync(...)` and `DownloadContentByLoginIdentityAsync(...)` return raw bytes
|
||||
- `UpdateMyAsync(string)` uploads text content directly
|
||||
- `UpdateMyAsync(byte[])` uploads binary content directly
|
||||
- `UpdateMyAsync(object)` serializes the object as JSON automatically
|
||||
|
||||
## Package layout
|
||||
|
||||
- `Runtime`
|
||||
|
||||
@@ -50,7 +50,7 @@ public sealed class BriskArchiveModule
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var finalChecksum = string.IsNullOrWhiteSpace(checksum) ? ComputeSha256(bytes) : checksum;
|
||||
var finalChecksum = string.IsNullOrWhiteSpace(checksum) ? ComputeSha256(bytes) : NormalizeChecksum(checksum);
|
||||
var sections = new List<IMultipartFormSection>
|
||||
{
|
||||
new MultipartFormDataSection("base_version", (baseVersion ?? 0).ToString()),
|
||||
@@ -63,6 +63,24 @@ public sealed class BriskArchiveModule
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以 UTF-8 文本形式上传指定槽位的存档。
|
||||
/// </summary>
|
||||
public Task<BriskArchiveUploadResult> UploadTextAsync(int slotNo, string text, int? baseVersion = null, string checksum = null)
|
||||
{
|
||||
RequireNotNull(text, nameof(text));
|
||||
return UploadAsync(slotNo, Encoding.UTF8.GetBytes(text), baseVersion, checksum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以 JSON 文本形式上传指定槽位的存档。
|
||||
/// </summary>
|
||||
public Task<BriskArchiveUploadResult> UploadJsonAsync(int slotNo, object payload, int? baseVersion = null, string checksum = null)
|
||||
{
|
||||
RequireNotNull(payload, nameof(payload));
|
||||
return UploadAsync(slotNo, Encoding.UTF8.GetBytes(BriskJson.Serialize(payload)), baseVersion, checksum);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下载指定槽位的二进制存档。
|
||||
/// </summary>
|
||||
@@ -82,6 +100,24 @@ public sealed class BriskArchiveModule
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以 UTF-8 文本形式下载指定槽位的存档。
|
||||
/// </summary>
|
||||
public async Task<string> DownloadTextAsync(int slotNo)
|
||||
{
|
||||
var result = await DownloadAsync(slotNo);
|
||||
return result == null || result.Bytes == null ? string.Empty : Encoding.UTF8.GetString(result.Bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 以 JSON 对象形式下载指定槽位的存档。
|
||||
/// </summary>
|
||||
public async Task<object> DownloadJsonAsync(int slotNo)
|
||||
{
|
||||
var text = await DownloadTextAsync(slotNo);
|
||||
return string.IsNullOrWhiteSpace(text) ? null : BriskJson.Deserialize(text);
|
||||
}
|
||||
|
||||
private static void ValidateSlotNo(int slotNo)
|
||||
{
|
||||
RequirePositive(slotNo, nameof(slotNo), "slotNo must be greater than 0.");
|
||||
@@ -92,8 +128,7 @@ public sealed class BriskArchiveModule
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
var builder = new StringBuilder(hash.Length * 2 + 7);
|
||||
builder.Append("sha256:");
|
||||
var builder = new StringBuilder(hash.Length * 2);
|
||||
for (var i = 0; i < hash.Length; i++)
|
||||
{
|
||||
builder.Append(hash[i].ToString("x2"));
|
||||
@@ -103,6 +138,19 @@ public sealed class BriskArchiveModule
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeChecksum(string checksum)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(checksum))
|
||||
{
|
||||
return checksum;
|
||||
}
|
||||
|
||||
var value = checksum.Trim();
|
||||
return value.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase)
|
||||
? value.Substring("sha256:".Length)
|
||||
: value;
|
||||
}
|
||||
|
||||
private static int ReadHeaderInt(Dictionary<string, string> headers, string key)
|
||||
{
|
||||
var value = ReadHeader(headers, key);
|
||||
|
||||
@@ -51,23 +51,26 @@ public sealed class BriskHttpClient
|
||||
|
||||
public async Task<Dictionary<string, object>> PostMultipartAsync(string path, List<IMultipartFormSection> formSections, bool auth = false)
|
||||
{
|
||||
using (var request = UnityWebRequest.Post(BuildUrl(path, null), formSections))
|
||||
return await SendMultipartAsync(UnityWebRequest.kHttpVerbPOST, path, formSections, auth);
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, object>> PutMultipartAsync(string path, List<IMultipartFormSection> formSections, bool auth = false)
|
||||
{
|
||||
return await SendMultipartAsync(UnityWebRequest.kHttpVerbPUT, path, formSections, auth);
|
||||
}
|
||||
|
||||
public async Task<BriskBinaryResponse> GetBytesAsync(string path, Dictionary<string, string> query = null, bool auth = false)
|
||||
{
|
||||
using (var request = new UnityWebRequest(BuildUrl(path, query), UnityWebRequest.kHttpVerbGET))
|
||||
{
|
||||
request.downloadHandler = new DownloadHandlerBuffer();
|
||||
|
||||
if (auth)
|
||||
{
|
||||
AddAuthorizationHeader(request);
|
||||
}
|
||||
|
||||
request.SetRequestHeader("Accept", "application/json");
|
||||
await SendRequestAsync(request);
|
||||
return ParseEnvelope(request) as Dictionary<string, object> ?? new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<BriskBinaryResponse> GetBytesAsync(string path, Dictionary<string, string> query = null, bool auth = false)
|
||||
{
|
||||
using (var request = BuildRequest(UnityWebRequest.kHttpVerbGET, path, query, null, auth))
|
||||
{
|
||||
request.SetRequestHeader("Accept", "*/*");
|
||||
await SendRequestAsync(request);
|
||||
EnsureSuccessOrThrow(request);
|
||||
return new BriskBinaryResponse
|
||||
@@ -93,6 +96,23 @@ public sealed class BriskHttpClient
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, object>> SendMultipartAsync(string method, string path, List<IMultipartFormSection> formSections, bool auth)
|
||||
{
|
||||
using (var request = UnityWebRequest.Post(BuildUrl(path, null), formSections))
|
||||
{
|
||||
request.method = method;
|
||||
|
||||
if (auth)
|
||||
{
|
||||
AddAuthorizationHeader(request);
|
||||
}
|
||||
|
||||
request.SetRequestHeader("Accept", "application/json");
|
||||
await SendRequestAsync(request);
|
||||
return ParseEnvelope(request) as Dictionary<string, object> ?? new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
|
||||
private UnityWebRequest BuildRequest(string method, string path, Dictionary<string, string> query, object body, bool auth)
|
||||
{
|
||||
var url = BuildUrl(path, query);
|
||||
|
||||
@@ -237,10 +237,21 @@ internal static class BriskModelMapper
|
||||
|
||||
return new BriskSpaceView
|
||||
{
|
||||
ProjectAccountId = BriskValueReader.GetString(data, "project_account_id"),
|
||||
PlayerId = BriskValueReader.GetString(data, "player_id"),
|
||||
LoginProvider = BriskValueReader.GetString(data, "login_provider"),
|
||||
LoginUserId = BriskValueReader.GetString(data, "login_user_id"),
|
||||
Payload = BriskValueReader.GetDictionary(data, "payload_json") ?? BriskValueReader.GetDictionary(data, "payload")
|
||||
Nickname = BriskValueReader.GetString(data, "nickname"),
|
||||
AvatarUrl = BriskValueReader.GetString(data, "avatar_url"),
|
||||
ContentExists = BriskValueReader.GetBool(data, "content_exists"),
|
||||
ContentVersion = BriskValueReader.GetLong(data, "content_version"),
|
||||
ContentType = BriskValueReader.GetString(data, "content_type"),
|
||||
ContentSizeBytes = BriskValueReader.GetLong(data, "content_size_bytes"),
|
||||
ContentChecksum = BriskValueReader.GetString(data, "content_checksum"),
|
||||
LikeCount = BriskValueReader.GetLong(data, "like_count"),
|
||||
VisitCount = BriskValueReader.GetLong(data, "visit_count"),
|
||||
LikedByMe = BriskValueReader.GetBool(data, "liked_by_me"),
|
||||
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -253,8 +264,52 @@ internal static class BriskModelMapper
|
||||
|
||||
return new BriskSpaceStats
|
||||
{
|
||||
LikeCount = BriskValueReader.GetInt(data, "like_count"),
|
||||
VisitCount = BriskValueReader.GetInt(data, "visit_count")
|
||||
ProjectAccountId = BriskValueReader.GetString(data, "project_account_id"),
|
||||
PlayerId = BriskValueReader.GetString(data, "player_id"),
|
||||
LoginProvider = BriskValueReader.GetString(data, "login_provider"),
|
||||
LoginUserId = BriskValueReader.GetString(data, "login_user_id"),
|
||||
Nickname = BriskValueReader.GetString(data, "nickname"),
|
||||
AvatarUrl = BriskValueReader.GetString(data, "avatar_url"),
|
||||
ContentExists = BriskValueReader.GetBool(data, "content_exists"),
|
||||
ContentVersion = BriskValueReader.GetLong(data, "content_version"),
|
||||
ContentType = BriskValueReader.GetString(data, "content_type"),
|
||||
ContentSizeBytes = BriskValueReader.GetLong(data, "content_size_bytes"),
|
||||
ContentChecksum = BriskValueReader.GetString(data, "content_checksum"),
|
||||
LikeCount = BriskValueReader.GetLong(data, "like_count"),
|
||||
VisitCount = BriskValueReader.GetLong(data, "visit_count"),
|
||||
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
|
||||
};
|
||||
}
|
||||
|
||||
public static BriskSpaceContentUpdateResult ToSpaceContentUpdateResult(Dictionary<string, object> data)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BriskSpaceContentUpdateResult
|
||||
{
|
||||
PlayerId = BriskValueReader.GetString(data, "player_id"),
|
||||
ContentVersion = BriskValueReader.GetLong(data, "content_version"),
|
||||
ContentType = BriskValueReader.GetString(data, "content_type"),
|
||||
ContentSizeBytes = BriskValueReader.GetLong(data, "content_size_bytes"),
|
||||
ContentChecksum = BriskValueReader.GetString(data, "content_checksum"),
|
||||
UpdatedAt = BriskValueReader.GetString(data, "updated_at")
|
||||
};
|
||||
}
|
||||
|
||||
public static BriskSpaceLikeResult ToSpaceLikeResult(Dictionary<string, object> data)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BriskSpaceLikeResult
|
||||
{
|
||||
Liked = BriskValueReader.GetBool(data, "liked"),
|
||||
LikeCount = BriskValueReader.GetLong(data, "like_count")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -284,6 +339,30 @@ internal static class BriskModelMapper
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static BriskSpaceLikeItem ToSpaceLikeItem(Dictionary<string, object> data)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BriskSpaceLikeItem
|
||||
{
|
||||
PlayerId = BriskValueReader.GetString(data, "player_id"),
|
||||
Nickname = BriskValueReader.GetString(data, "nickname"),
|
||||
AvatarUrl = BriskValueReader.GetString(data, "avatar_url"),
|
||||
CreatedAt = BriskValueReader.GetString(data, "created_at")
|
||||
};
|
||||
}
|
||||
|
||||
public static List<BriskSpaceLikeItem> ToSpaceLikeItems(object data)
|
||||
{
|
||||
return ExtractList(data)
|
||||
.Select(item => ToSpaceLikeItem(item as Dictionary<string, object>))
|
||||
.Where(item => item != null)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static Dictionary<string, object> ExtractObject(object data)
|
||||
{
|
||||
return data as Dictionary<string, object>;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
public sealed class BriskSpaceContentDownloadResult
|
||||
{
|
||||
public byte[] Bytes;
|
||||
public long Version;
|
||||
public string Checksum;
|
||||
public string ContentType;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e6f27d1ba8b24f5e85362b9c6c9f4c01
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,9 @@
|
||||
public sealed class BriskSpaceContentUpdateResult
|
||||
{
|
||||
public string PlayerId;
|
||||
public long ContentVersion;
|
||||
public string ContentType;
|
||||
public long ContentSizeBytes;
|
||||
public string ContentChecksum;
|
||||
public string UpdatedAt;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4bd67f378d264cc69c71f1d03d5b564b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,7 @@
|
||||
public sealed class BriskSpaceLikeItem
|
||||
{
|
||||
public string PlayerId;
|
||||
public string Nickname;
|
||||
public string AvatarUrl;
|
||||
public string CreatedAt;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef7d53b3f0ab43e7b74a4c23a76f84f5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,5 @@
|
||||
public sealed class BriskSpaceLikeResult
|
||||
{
|
||||
public bool Liked;
|
||||
public long LikeCount;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 966f47c348db46dfb60120dba20f1ffb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,5 +1,17 @@
|
||||
public sealed class BriskSpaceStats
|
||||
{
|
||||
public int LikeCount;
|
||||
public int VisitCount;
|
||||
public string ProjectAccountId;
|
||||
public string PlayerId;
|
||||
public string LoginProvider;
|
||||
public string LoginUserId;
|
||||
public string Nickname;
|
||||
public string AvatarUrl;
|
||||
public bool ContentExists;
|
||||
public long ContentVersion;
|
||||
public string ContentType;
|
||||
public long ContentSizeBytes;
|
||||
public string ContentChecksum;
|
||||
public long LikeCount;
|
||||
public long VisitCount;
|
||||
public string UpdatedAt;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
public sealed class BriskSpaceView
|
||||
{
|
||||
public string ProjectAccountId;
|
||||
public string PlayerId;
|
||||
public string LoginProvider;
|
||||
public string LoginUserId;
|
||||
public object Payload;
|
||||
public string Nickname;
|
||||
public string AvatarUrl;
|
||||
public bool ContentExists;
|
||||
public long ContentVersion;
|
||||
public string ContentType;
|
||||
public long ContentSizeBytes;
|
||||
public string ContentChecksum;
|
||||
public long LikeCount;
|
||||
public long VisitCount;
|
||||
public bool LikedByMe;
|
||||
public string UpdatedAt;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
/// <summary>
|
||||
/// 玩家空间模块。
|
||||
@@ -65,77 +68,179 @@ public sealed class BriskSpaceModule
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按玩家 ID 点赞空间。
|
||||
/// 按玩家 ID 获取最近点赞列表。
|
||||
/// </summary>
|
||||
public async Task LikeByPlayerIdAsync(string playerId)
|
||||
public async Task<IReadOnlyList<BriskSpaceLikeItem>> GetLikesByPlayerIdAsync(string playerId, int limit = 20)
|
||||
{
|
||||
ValidatePlayerId(playerId);
|
||||
await ExecuteAsync(async context =>
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
await context.HttpClient.PostJsonRawAsync($"/spaces/{playerId}/like", new Dictionary<string, object>(), true);
|
||||
var data = await context.HttpClient.GetRawDataAsync($"/spaces/{playerId}/likes", CreateLimitQuery(limit), true);
|
||||
return (IReadOnlyList<BriskSpaceLikeItem>)BriskModelMapper.ToSpaceLikeItems(data);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按登录身份获取最近点赞列表。
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<BriskSpaceLikeItem>> GetLikesByLoginIdentityAsync(string loginProvider, string loginUserId, int limit = 20)
|
||||
{
|
||||
ValidateLoginIdentity(loginProvider, loginUserId);
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var query = CreateLoginIdentityQuery(loginProvider, loginUserId);
|
||||
query["limit"] = NormalizeLimit(limit);
|
||||
var data = await context.HttpClient.GetRawDataAsync("/spaces/by-login/likes", query, true);
|
||||
return (IReadOnlyList<BriskSpaceLikeItem>)BriskModelMapper.ToSpaceLikeItems(data);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按玩家 ID 点赞空间。
|
||||
/// </summary>
|
||||
public async Task<BriskSpaceLikeResult> LikeByPlayerIdAsync(string playerId)
|
||||
{
|
||||
ValidatePlayerId(playerId);
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var data = await context.HttpClient.PostJsonRawAsync($"/spaces/{playerId}/like", new Dictionary<string, object>(), true);
|
||||
return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按玩家 ID 取消点赞空间。
|
||||
/// </summary>
|
||||
public async Task UnlikeByPlayerIdAsync(string playerId)
|
||||
public async Task<BriskSpaceLikeResult> UnlikeByPlayerIdAsync(string playerId)
|
||||
{
|
||||
ValidatePlayerId(playerId);
|
||||
await ExecuteAsync(async context =>
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
await context.HttpClient.SendDeleteJsonRawAsync($"/spaces/{playerId}/like", null, true);
|
||||
var data = await context.HttpClient.SendDeleteJsonRawAsync($"/spaces/{playerId}/like", null, true);
|
||||
return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按登录身份点赞空间。
|
||||
/// </summary>
|
||||
public async Task LikeByLoginIdentityAsync(string loginProvider, string loginUserId)
|
||||
public async Task<BriskSpaceLikeResult> LikeByLoginIdentityAsync(string loginProvider, string loginUserId)
|
||||
{
|
||||
ValidateLoginIdentity(loginProvider, loginUserId);
|
||||
await ExecuteAsync(async context =>
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
await context.HttpClient.PostJsonRawAsync("/spaces/by-login/like", new Dictionary<string, object>(), true, CreateLoginIdentityQuery(loginProvider, loginUserId));
|
||||
var data = await context.HttpClient.PostJsonRawAsync("/spaces/by-login/like", new Dictionary<string, object>(), true, CreateLoginIdentityQuery(loginProvider, loginUserId));
|
||||
return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按登录身份取消点赞空间。
|
||||
/// </summary>
|
||||
public async Task UnlikeByLoginIdentityAsync(string loginProvider, string loginUserId)
|
||||
public async Task<BriskSpaceLikeResult> UnlikeByLoginIdentityAsync(string loginProvider, string loginUserId)
|
||||
{
|
||||
ValidateLoginIdentity(loginProvider, loginUserId);
|
||||
await ExecuteAsync(async context =>
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
await context.HttpClient.SendDeleteJsonRawAsync("/spaces/by-login/like", CreateLoginIdentityQuery(loginProvider, loginUserId), true);
|
||||
var data = await context.HttpClient.SendDeleteJsonRawAsync("/spaces/by-login/like", CreateLoginIdentityQuery(loginProvider, loginUserId), true);
|
||||
return BriskModelMapper.ToSpaceLikeResult(BriskModelMapper.ExtractObject(data));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上传当前玩家自己的空间内容。
|
||||
/// </summary>
|
||||
public async Task<BriskSpaceContentUpdateResult> UploadMyContentAsync(long? baseVersion, string contentType, string checksum, byte[] bytes)
|
||||
{
|
||||
RequireNotNull(bytes, nameof(bytes));
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var finalChecksum = string.IsNullOrWhiteSpace(checksum) ? ComputeSha256(bytes) : NormalizeChecksum(checksum);
|
||||
var sections = new List<IMultipartFormSection>
|
||||
{
|
||||
new MultipartFormDataSection("base_version", (baseVersion ?? 0L).ToString()),
|
||||
new MultipartFormFileSection("file", bytes, "space-content.bin", string.IsNullOrWhiteSpace(contentType) ? "application/octet-stream" : contentType)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(contentType))
|
||||
{
|
||||
sections.Insert(1, new MultipartFormDataSection("content_type", contentType));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(finalChecksum))
|
||||
{
|
||||
sections.Insert(string.IsNullOrWhiteSpace(contentType) ? 1 : 2, new MultipartFormDataSection("checksum", finalChecksum));
|
||||
}
|
||||
|
||||
var data = await context.HttpClient.PutMultipartAsync("/spaces/me/content", sections, true);
|
||||
return BriskModelMapper.ToSpaceContentUpdateResult(data);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新当前玩家自己的空间内容。
|
||||
/// </summary>
|
||||
public async Task UpdateMyAsync(object payload)
|
||||
public Task<BriskSpaceContentUpdateResult> UpdateMyAsync(object payload, long? baseVersion = null, string contentType = null, string checksum = null)
|
||||
{
|
||||
RequireNotNull(payload, nameof(payload));
|
||||
|
||||
await ExecuteAsync(async context =>
|
||||
if (payload is byte[] bytes)
|
||||
{
|
||||
await context.HttpClient.SendPutJsonRawAsync(
|
||||
"/spaces/me",
|
||||
new Dictionary<string, object> { { "payload_json", payload } },
|
||||
true);
|
||||
return UploadMyContentAsync(baseVersion, contentType ?? "application/octet-stream", checksum, bytes);
|
||||
}
|
||||
|
||||
if (payload is string text)
|
||||
{
|
||||
return UploadMyContentAsync(baseVersion, contentType ?? "text/plain", checksum, Encoding.UTF8.GetBytes(text));
|
||||
}
|
||||
|
||||
var json = BriskJson.Serialize(payload);
|
||||
return UploadMyContentAsync(baseVersion, contentType ?? "application/json", checksum, Encoding.UTF8.GetBytes(json));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按玩家 ID 下载空间内容。
|
||||
/// </summary>
|
||||
public async Task<BriskSpaceContentDownloadResult> DownloadContentByPlayerIdAsync(string playerId)
|
||||
{
|
||||
ValidatePlayerId(playerId);
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var response = await context.HttpClient.GetBytesAsync($"/spaces/{playerId}/content", null, true);
|
||||
return CreateDownloadResult(response);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按登录身份下载空间内容。
|
||||
/// </summary>
|
||||
public async Task<BriskSpaceContentDownloadResult> DownloadContentByLoginIdentityAsync(string loginProvider, string loginUserId)
|
||||
{
|
||||
ValidateLoginIdentity(loginProvider, loginUserId);
|
||||
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var response = await context.HttpClient.GetBytesAsync("/spaces/by-login/content", CreateLoginIdentityQuery(loginProvider, loginUserId), true);
|
||||
return CreateDownloadResult(response);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取我的访客列表。
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<BriskSpaceVisit>> GetMyVisitsAsync()
|
||||
public async Task<IReadOnlyList<BriskSpaceVisit>> GetMyVisitsAsync(int limit = 20)
|
||||
{
|
||||
return await ExecuteAsync(async context =>
|
||||
{
|
||||
var data = await context.HttpClient.GetRawDataAsync("/spaces/me/visits", null, true);
|
||||
var data = await context.HttpClient.GetRawDataAsync("/spaces/me/visits", CreateLimitQuery(limit), true);
|
||||
return (IReadOnlyList<BriskSpaceVisit>)BriskModelMapper.ToSpaceVisits(data);
|
||||
});
|
||||
}
|
||||
@@ -149,6 +254,14 @@ public sealed class BriskSpaceModule
|
||||
};
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> CreateLimitQuery(int limit)
|
||||
{
|
||||
return new Dictionary<string, string>
|
||||
{
|
||||
{ "limit", NormalizeLimit(limit) }
|
||||
};
|
||||
}
|
||||
|
||||
private static void ValidatePlayerId(string playerId)
|
||||
{
|
||||
RequireNotEmpty(playerId, nameof(playerId));
|
||||
@@ -159,4 +272,72 @@ public sealed class BriskSpaceModule
|
||||
RequireNotEmpty(loginProvider, nameof(loginProvider));
|
||||
RequireNotEmpty(loginUserId, nameof(loginUserId));
|
||||
}
|
||||
|
||||
private static string NormalizeLimit(int limit)
|
||||
{
|
||||
return (limit > 0 ? limit : 20).ToString();
|
||||
}
|
||||
|
||||
private static BriskSpaceContentDownloadResult CreateDownloadResult(BriskBinaryResponse response)
|
||||
{
|
||||
return new BriskSpaceContentDownloadResult
|
||||
{
|
||||
Bytes = response.Bytes,
|
||||
Version = ReadHeaderLong(response.Headers, "X-Space-Version"),
|
||||
Checksum = ReadHeader(response.Headers, "X-Space-Checksum"),
|
||||
ContentType = ReadHeader(response.Headers, "Content-Type")
|
||||
};
|
||||
}
|
||||
|
||||
private static string ComputeSha256(byte[] bytes)
|
||||
{
|
||||
using (var sha = SHA256.Create())
|
||||
{
|
||||
var hash = sha.ComputeHash(bytes);
|
||||
var builder = new StringBuilder(hash.Length * 2);
|
||||
for (var i = 0; i < hash.Length; i++)
|
||||
{
|
||||
builder.Append(hash[i].ToString("x2"));
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeChecksum(string checksum)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(checksum))
|
||||
{
|
||||
return checksum;
|
||||
}
|
||||
|
||||
var value = checksum.Trim();
|
||||
return value.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase)
|
||||
? value.Substring("sha256:".Length)
|
||||
: value;
|
||||
}
|
||||
|
||||
private static long ReadHeaderLong(Dictionary<string, string> headers, string key)
|
||||
{
|
||||
var value = ReadHeader(headers, key);
|
||||
return long.TryParse(value, out var result) ? result : 0L;
|
||||
}
|
||||
|
||||
private static string ReadHeader(Dictionary<string, string> headers, string key)
|
||||
{
|
||||
if (headers == null || string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var pair in headers)
|
||||
{
|
||||
if (string.Equals(pair.Key, key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return pair.Value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,7 +38,6 @@ RenderSettings:
|
||||
m_ReflectionIntensity: 1
|
||||
m_CustomReflection: {fileID: 0}
|
||||
m_Sun: {fileID: 0}
|
||||
m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1}
|
||||
m_UseRadianceAmbientProbe: 0
|
||||
--- !u!157 &3
|
||||
LightmapSettings:
|
||||
@@ -104,7 +103,7 @@ NavMeshSettings:
|
||||
serializedVersion: 2
|
||||
m_ObjectHideFlags: 0
|
||||
m_BuildSettings:
|
||||
serializedVersion: 2
|
||||
serializedVersion: 3
|
||||
agentTypeID: 0
|
||||
agentRadius: 0.5
|
||||
agentHeight: 2
|
||||
@@ -117,7 +116,7 @@ NavMeshSettings:
|
||||
cellSize: 0.16666667
|
||||
manualTileSize: 0
|
||||
tileSize: 256
|
||||
accuratePlacement: 0
|
||||
buildHeightMesh: 0
|
||||
maxJobWorkers: 0
|
||||
preserveTilesOutsideBounds: 0
|
||||
debug:
|
||||
@@ -163,9 +162,17 @@ Camera:
|
||||
m_projectionMatrixMode: 1
|
||||
m_GateFitMode: 2
|
||||
m_FOVAxisMode: 0
|
||||
m_Iso: 200
|
||||
m_ShutterSpeed: 0.005
|
||||
m_Aperture: 16
|
||||
m_FocusDistance: 10
|
||||
m_FocalLength: 50
|
||||
m_BladeCount: 5
|
||||
m_Curvature: {x: 2, y: 11}
|
||||
m_BarrelClipping: 0.25
|
||||
m_Anamorphism: 0
|
||||
m_SensorSize: {x: 36, y: 24}
|
||||
m_LensShift: {x: 0, y: 0}
|
||||
m_FocalLength: 50
|
||||
m_NormalizedViewPortRect:
|
||||
serializedVersion: 2
|
||||
x: 0
|
||||
@@ -199,12 +206,13 @@ Transform:
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 519420028}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: -10}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_RootOrder: 0
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!1 &1600000000
|
||||
GameObject:
|
||||
@@ -230,12 +238,13 @@ Transform:
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1600000000}
|
||||
serializedVersion: 2
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 0}
|
||||
m_RootOrder: 1
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
--- !u!114 &1600000002
|
||||
MonoBehaviour:
|
||||
@@ -250,16 +259,16 @@ MonoBehaviour:
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
BaseUrl: https://brisk.lightyears.ltd
|
||||
GameKey: demo-game
|
||||
GameKey: briskh5verify
|
||||
ClientVersion: 1.0.0
|
||||
DeviceId: editor-device
|
||||
ValidateSessionOnInitialize: 1
|
||||
LoginProvider: tap
|
||||
LoginUserId: tap_user_10001
|
||||
LoginCode:
|
||||
Nickname: Unity示例玩家
|
||||
Nickname: "Unity\u793A\u4F8B\u73A9\u5BB6"
|
||||
AvatarUrl:
|
||||
RankKey: season-score
|
||||
RankKey: h5-verify-rank-20260410034312-5cdd
|
||||
SubmitScoreValue: 128
|
||||
LeaderboardLimit: 10
|
||||
AroundMeRange: 5
|
||||
@@ -267,10 +276,18 @@ MonoBehaviour:
|
||||
SeasonHistoryLimit: 20
|
||||
ArchiveSlotNo: 1
|
||||
ArchiveBaseVersion:
|
||||
ArchiveContent: '{"save":1,"coins":128,"hero":"mage","title":"中文测试存档"}'
|
||||
ArchiveContent: "{\n \"save\": 1,\n \"coins\": 128,\n \"hero\": \"mage\",\n
|
||||
\"title\": \"\u4E2D\u6587\u6D4B\u8BD5\u5B58\u6863\"\n}"
|
||||
AnnouncementId:
|
||||
SpacePlayerId:
|
||||
SpaceLoginProvider: tap
|
||||
SpaceLoginUserId: tap_user_10001
|
||||
SpacePayloadText: '{"mood":"ready","title":"你好 Brisk","desc":"这是中文测试空间数据"}'
|
||||
SpacePayloadText: "{\n \"mood\": \"ready\",\n \"title\": \"\u4F60\u597D Brisk\",\n
|
||||
\"desc\": \"\u8FD9\u662F\u4E2D\u6587\u6D4B\u8BD5\u7A7A\u95F4\u6570\u636E\"\n}"
|
||||
AutoRunOnStart: 0
|
||||
--- !u!1660057539 &9223372036854775807
|
||||
SceneRoots:
|
||||
m_ObjectHideFlags: 0
|
||||
m_Roots:
|
||||
- {fileID: 519420032}
|
||||
- {fileID: 1600000001}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "com.foldcc.cc-framework.BriskGameServer",
|
||||
"displayName": "FoldCC Brisk Game Server SDK",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"unity": "2021.3",
|
||||
"description": "A lightweight Unity SDK for Brisk game services, including bootstrap, auth, announcements, leaderboard, archive, and player space modules.",
|
||||
"keywords": [
|
||||
|
||||
30
README.md
30
README.md
@@ -20,6 +20,28 @@ Brisk Unity SDK 原始开发工程。
|
||||
- 玩家空间
|
||||
- 严重错误默认 IMGUI 提示
|
||||
|
||||
内容型接口当前默认推荐用法:
|
||||
|
||||
- 云存档:
|
||||
- `Brisk.Archive.UploadAsync(slotNo, bytes)` 直接传二进制
|
||||
- `Brisk.Archive.UploadTextAsync(slotNo, text)` 直接传文本
|
||||
- `Brisk.Archive.UploadJsonAsync(slotNo, payload)` 直接传对象
|
||||
- `Brisk.Archive.DownloadAsync(slotNo)` 获取原始 bytes + 元信息
|
||||
- `Brisk.Archive.DownloadTextAsync(slotNo)` 直接取文本
|
||||
- `Brisk.Archive.DownloadJsonAsync(slotNo)` 直接取 JSON 对象
|
||||
- 玩家空间:
|
||||
- `Brisk.Space.UpdateMyAsync(string)` 可直接更新文本内容
|
||||
- `Brisk.Space.UpdateMyAsync(byte[])` 可直接更新二进制内容
|
||||
- `Brisk.Space.UpdateMyAsync(object)` 会自动转成 JSON 上传
|
||||
- `Brisk.Space.DownloadContentByPlayerIdAsync(playerId)` 获取原始 bytes
|
||||
|
||||
云存档上传补充约定:
|
||||
|
||||
- SDK 默认会自动计算上传文件的 `checksum`
|
||||
- 当前服务端要求的是纯 SHA256 十六进制字符串
|
||||
- 不应传 `sha256:xxxx` 这类带算法前缀的值
|
||||
- 如果手动传入 `checksum`,SDK 也会自动去掉 `sha256:` 前缀再发送
|
||||
|
||||
静态入口保持为:
|
||||
|
||||
- `Brisk.xxx`
|
||||
@@ -62,6 +84,8 @@ Brisk Unity SDK 原始开发工程。
|
||||
- 玩家空间读取、点赞、更新、访客
|
||||
- 全局事件日志与最近一次结果输出
|
||||
|
||||
当前样例已经直接使用 `UploadTextAsync` / `DownloadTextAsync` 这类快捷接口;如果业务需要,也可以继续走原始 bytes 接口。
|
||||
|
||||
## 发布结构
|
||||
|
||||
对外 package 发布目录位于:
|
||||
@@ -99,7 +123,7 @@ Brisk Unity SDK 原始开发工程。
|
||||
|
||||
- 开发分支:`feature/...`、`fix/...`
|
||||
- 发包分支:`release/upm-vX.Y.Z`
|
||||
- 发包 tag:`upm/vX.Y.Z`
|
||||
- 发包 tag:`vX.Y.Z`
|
||||
|
||||
推荐发布步骤:
|
||||
|
||||
@@ -108,13 +132,13 @@ Brisk Unity SDK 原始开发工程。
|
||||
3. 检查 `PackageSource/com.foldcc.cc-framework.BriskGameServer`
|
||||
4. 更新 `package.json` 版本号与 `CHANGELOG.md`
|
||||
5. 创建发布分支 `release/upm-vX.Y.Z`
|
||||
6. 打 tag `upm/vX.Y.Z`
|
||||
6. 打 tag `vX.Y.Z`
|
||||
7. 推送发布分支与 tag
|
||||
|
||||
外部项目 Git Package 接入示例:
|
||||
|
||||
```text
|
||||
http://private.lightyears.ltd:18650/foldcc/CC-Framework.BriskGameServer.git?path=/PackageSource/com.foldcc.cc-framework.BriskGameServer#upm/v0.1.0
|
||||
http://private.lightyears.ltd:18650/foldcc/CC-Framework.BriskGameServer.git?path=/PackageSource/com.foldcc.cc-framework.BriskGameServer#v0.2.0
|
||||
```
|
||||
|
||||
## 文档位置
|
||||
|
||||
@@ -234,25 +234,66 @@ curl -X POST "https://brisk.lightyears.ltd/api/archives/slot/1/upload" \
|
||||
|
||||
说明:
|
||||
|
||||
- 空间数据当前为 `payload_json`
|
||||
- 适合公开资料、展示文案、轻量个性化内容
|
||||
- 如果是游戏主存档,请优先走云存档模块
|
||||
- 空间主信息接口只返回元数据,不再内嵌内容本体
|
||||
- 空间内容本体通过独立 `/content` 接口上传和下载
|
||||
- 空间内容支持任意 bytes
|
||||
- 可以上传二进制、文本、JSON、protobuf、msgpack、压缩包等格式
|
||||
- 如果是多槽位私有主存档,仍优先走云存档模块;如果是空间自定义内容,可直接走空间内容接口
|
||||
|
||||
接口:
|
||||
|
||||
- `PUT /api/spaces/me`
|
||||
- `PUT /api/spaces/me/content`
|
||||
- `GET /api/spaces/{player_id}`
|
||||
- `GET /api/spaces/{player_id}/content`
|
||||
- `GET /api/spaces/{player_id}/stats`
|
||||
- `GET /api/spaces/{player_id}/likes`
|
||||
- `POST /api/spaces/{player_id}/like`
|
||||
- `DELETE /api/spaces/{player_id}/like`
|
||||
- `GET /api/spaces/me/visits`
|
||||
- `GET /api/spaces/by-login`
|
||||
- `GET /api/spaces/by-login/content`
|
||||
- `GET /api/spaces/by-login/stats`
|
||||
- `GET /api/spaces/by-login/likes`
|
||||
- `POST /api/spaces/by-login/like`
|
||||
- `DELETE /api/spaces/by-login/like`
|
||||
|
||||
主信息接口返回重点:
|
||||
|
||||
- `content_exists`
|
||||
- `content_version`
|
||||
- `content_type`
|
||||
- `content_size_bytes`
|
||||
- `content_checksum`
|
||||
- `like_count`
|
||||
- `visit_count`
|
||||
- `updated_at`
|
||||
|
||||
上传表单字段:
|
||||
|
||||
- `base_version`:用于乐观锁;首次上传可传 `0`
|
||||
- `content_type`:可选,建议传,例如 `application/octet-stream`、`text/plain`、`application/json`
|
||||
- `checksum`:可选;如果传入,服务端会校验 SHA-256
|
||||
- `file`:必传,内容本体
|
||||
|
||||
上传空间内容示例:
|
||||
|
||||
```bash
|
||||
curl -X PUT "https://brisk.lightyears.ltd/api/spaces/me/content" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-F "base_version=0" \
|
||||
-F "content_type=application/octet-stream" \
|
||||
-F "checksum=<sha256>" \
|
||||
-F "file=@space-content.bin"
|
||||
```
|
||||
|
||||
下载空间内容示例:
|
||||
|
||||
```bash
|
||||
curl "https://brisk.lightyears.ltd/api/spaces/by-login/content?login_provider=tap&login_user_id=tap_user_10001" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
--output space-content.bin
|
||||
```
|
||||
|
||||
按第三方身份查看空间示例:
|
||||
|
||||
```bash
|
||||
@@ -312,6 +353,24 @@ curl -X POST "https://brisk.lightyears.ltd/api/spaces/by-login/like?login_provid
|
||||
- `login_provider`
|
||||
- `login_user_id`
|
||||
|
||||
### 10.5 空间管理
|
||||
|
||||
- `GET /api/admin/spaces/overview?project_id=...`
|
||||
- `GET /api/admin/spaces?project_id=...`
|
||||
- `GET /api/admin/spaces/{player_id}?project_id=...`
|
||||
- `GET /api/admin/spaces/{player_id}/content?project_id=...`
|
||||
- `GET /api/admin/spaces/{player_id}/likes?project_id=...`
|
||||
- `GET /api/admin/spaces/{player_id}/visits?project_id=...`
|
||||
|
||||
说明:
|
||||
|
||||
- 后台空间详情返回内容元数据
|
||||
- 如果需要查看空间内容本体,使用独立 `/content` 下载接口
|
||||
- 下载响应会返回:
|
||||
- `Content-Type`
|
||||
- `X-Space-Version`
|
||||
- `X-Space-Checksum`
|
||||
|
||||
## 11. OpenAPI
|
||||
|
||||
完整 OpenAPI 导出:
|
||||
|
||||
@@ -2,17 +2,18 @@
|
||||
|
||||
## 1. 这份文档适合做什么
|
||||
|
||||
这份文档用于在 Unity 侧封装 Brisk SDK。
|
||||
这份文档用于说明当前 Brisk Unity SDK 的接入结构与推荐调用方式。
|
||||
|
||||
建议把 SDK 拆成以下层次:
|
||||
当前 SDK 已采用静态总入口:
|
||||
|
||||
- `BriskClient`
|
||||
- `BriskAuth`
|
||||
- `BriskConfig`
|
||||
- `BriskAnnouncements`
|
||||
- `BriskRanks`
|
||||
- `BriskArchives`
|
||||
- `BriskSpaces`
|
||||
- `Brisk.InitializeAsync(...)`
|
||||
- `Brisk.Auth.xxx`
|
||||
- `Brisk.Player.xxx`
|
||||
- `Brisk.Config.xxx`
|
||||
- `Brisk.Announcements.xxx`
|
||||
- `Brisk.Leaderboard.xxx`
|
||||
- `Brisk.Archive.xxx`
|
||||
- `Brisk.Space.xxx`
|
||||
|
||||
## 2. 初始化模型
|
||||
|
||||
@@ -141,16 +142,31 @@ Task LikeSpaceByLoginIdentityAsync(string loginProvider, string loginUserId)
|
||||
- msgpack bytes
|
||||
- 压缩后的存档二进制
|
||||
|
||||
Unity SDK 建议提供:
|
||||
当前 SDK 已提供:
|
||||
|
||||
- `GetArchiveSlotsAsync()`
|
||||
- `GetArchiveMetaAsync(slotNo)`
|
||||
- `UploadArchiveAsync(slotNo, baseVersion, checksum, byte[])`
|
||||
- `UploadAsync(slotNo, byte[], baseVersion, checksum)`
|
||||
- `UploadTextAsync(slotNo, text, baseVersion, checksum)`
|
||||
- `UploadJsonAsync(slotNo, payload, baseVersion, checksum)`
|
||||
- `DownloadArchiveAsync(slotNo)`
|
||||
- `DownloadTextAsync(slotNo)`
|
||||
- `DownloadJsonAsync(slotNo)`
|
||||
|
||||
推荐上层使用策略:
|
||||
|
||||
- 文本存档:优先直接用 `UploadTextAsync / DownloadTextAsync`
|
||||
- JSON 存档:优先直接用 `UploadJsonAsync / DownloadJsonAsync`
|
||||
- 二进制存档:继续用 `UploadAsync / DownloadAsync`
|
||||
|
||||
其中:
|
||||
|
||||
- SDK 会自动计算上传用 `checksum`
|
||||
- 手动传 `checksum` 时也可以带 `sha256:` 前缀,SDK 会自动归一化
|
||||
|
||||
## 9. 玩家空间模块建议
|
||||
|
||||
玩家空间适合承载公开展示数据,不建议放主存档。
|
||||
玩家空间适合承载公开展示数据或空间自定义内容,不再限制为 JSON。
|
||||
|
||||
推荐放:
|
||||
|
||||
@@ -158,16 +174,48 @@ Unity SDK 建议提供:
|
||||
- 头像展示字段
|
||||
- 成就展示摘要
|
||||
- 公开资料卡
|
||||
- 小型二进制空间内容
|
||||
- 文本、JSON、protobuf、msgpack 等自定义格式
|
||||
|
||||
当前接口核心是:
|
||||
当前 SDK / 接口核心是:
|
||||
|
||||
- `UpdateMySpaceAsync(payloadJson)`
|
||||
- `UploadMySpaceContentAsync(baseVersion, contentType, checksum, byte[])`
|
||||
- `DownloadSpaceContentByPlayerIdAsync(playerId)`
|
||||
- `DownloadSpaceContentByLoginIdentityAsync(loginProvider, loginUserId)`
|
||||
- `GetSpaceByPlayerIdAsync(playerId)`
|
||||
- `GetSpaceByLoginIdentityAsync(loginProvider, loginUserId)`
|
||||
- `LikeSpaceAsync(playerId)`
|
||||
- `LikeSpaceByLoginIdentityAsync(loginProvider, loginUserId)`
|
||||
- `GetMyVisitsAsync(limit)`
|
||||
|
||||
当前 SDK 额外提供了一个极简便捷入口:
|
||||
|
||||
- `Brisk.Space.UpdateMyAsync(payload, baseVersion, contentType, checksum)`
|
||||
|
||||
它的行为是:
|
||||
|
||||
- `payload` 为 `string` 时,自动按 `text/plain` 上传
|
||||
- `payload` 为 `byte[]` 时,自动按 `application/octet-stream` 上传
|
||||
- `payload` 为其他对象时,自动序列化为 JSON 并按 `application/json` 上传
|
||||
|
||||
建议 SDK 暴露两层模型:
|
||||
|
||||
- 空间主信息:
|
||||
- `ContentExists`
|
||||
- `ContentVersion`
|
||||
- `ContentType`
|
||||
- `ContentSizeBytes`
|
||||
- `ContentChecksum`
|
||||
- `LikeCount`
|
||||
- `VisitCount`
|
||||
- 空间内容本体:
|
||||
- 原始 `byte[]`
|
||||
- 下载响应头中的 `X-Space-Version`
|
||||
- 下载响应头中的 `X-Space-Checksum`
|
||||
|
||||
如果上层业务追求最省事,可以直接用 `UpdateMyAsync(string)` 或 `UpdateMyAsync(object)`;
|
||||
如果业务自己管理编码、压缩、protobuf、msgpack 等格式,则继续走 `byte[]` 上传下载接口。
|
||||
|
||||
## 10. 动态配置建议
|
||||
|
||||
动态配置拆为两层:
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
| `41000 - 41999` | 后台公告 |
|
||||
| `50000 - 50999` | 排行榜 |
|
||||
| `51000 - 51999` | 后台榜单 |
|
||||
| `52000 - 52999` | 后台空间 |
|
||||
| `60000 - 60999` | 存档 |
|
||||
| `70000 - 70999` | 玩家空间 |
|
||||
| `80000 - 80999` | 限流 |
|
||||
@@ -146,6 +147,18 @@
|
||||
| `51013` | `admin/ranks` | 缺少榜单必填字段 |
|
||||
| `51014` | `admin/ranks` | 创建榜单失败 |
|
||||
|
||||
| 错误码 | 接口/模块 | 含义 |
|
||||
|---|---|---|
|
||||
| `52010` | `admin/spaces` | project_id 非法 |
|
||||
| `52011` | `admin/spaces` | 空间列表查询失败 |
|
||||
| `52012` | `admin/spaces/overview` | 空间总览查询失败 |
|
||||
| `52013` | `admin/spaces/{player_id}` | player_id 非法 |
|
||||
| `52014` | `admin/spaces/{player_id}` | 空间不存在 |
|
||||
| `52015` | `admin/spaces/{player_id}` | 空间详情查询失败 |
|
||||
| `52016` | `admin/spaces/{player_id}/likes` | 空间点赞列表查询失败 |
|
||||
| `52017` | `admin/spaces/{player_id}/visits` | 空间访客列表查询失败 |
|
||||
| `52018` | `admin/spaces/{player_id}/content` | 空间内容下载失败 |
|
||||
|
||||
| 错误码 | 接口/模块 | 含义 |
|
||||
|---|---|---|
|
||||
| `60010` | `archives/meta` | 缺少会话 |
|
||||
@@ -167,17 +180,36 @@
|
||||
| `70010` | `spaces/{player_id}` | 缺少会话 |
|
||||
| `70011` | `spaces/{player_id}` | 玩家不存在 |
|
||||
| `70012` | `spaces/{player_id}` | 空间读取失败 |
|
||||
| `70013` | `spaces/me` | 缺少会话 |
|
||||
| `70014` | `spaces/me` | 请求体错误 |
|
||||
| `70015` | `spaces/me` | 空间更新失败 |
|
||||
| `70016` | `spaces/{player_id}/like` | 缺少会话 |
|
||||
| `70017` | `spaces/{player_id}/like` | 玩家不存在 |
|
||||
| `70018` | `spaces/{player_id}/like` | 点赞失败 |
|
||||
| `70019` | `spaces/{player_id}/like` | 缺少会话 |
|
||||
| `70020` | `spaces/{player_id}/like` | 玩家不存在 |
|
||||
| `70021` | `spaces/{player_id}/like` | 取消点赞失败 |
|
||||
| `70022` | `spaces/me/visits` | 缺少会话 |
|
||||
| `70023` | `spaces/me/visits` | 访客列表查询失败 |
|
||||
| `70013` | `spaces/me/content` | 缺少会话 |
|
||||
| `70014` | `spaces/me/content` | multipart 表单错误 |
|
||||
| `70015` | `spaces/me/content` | 缺少文件 |
|
||||
| `70016` | `spaces/me/content` | 空间内容版本冲突 |
|
||||
| `70017` | `spaces/me/content` | checksum 不匹配 |
|
||||
| `70018` | `spaces/me/content` | 空间内容更新失败 |
|
||||
| `70019` | `spaces/*/content` | 缺少会话 |
|
||||
| `70020` | `spaces/*/content` | 玩家不存在 |
|
||||
| `70021` | `spaces/*/content` | 空间内容不存在 |
|
||||
| `70022` | `spaces/*/content` | 空间内容下载失败 |
|
||||
| `70023` | `spaces/{player_id}/like` | 缺少会话 |
|
||||
| `70024` | `spaces/{player_id}/like` | 玩家不存在 |
|
||||
| `70025` | `spaces/{player_id}/like` | 点赞失败 |
|
||||
| `70026` | `spaces/{player_id}/like` | 缺少会话 |
|
||||
| `70027` | `spaces/{player_id}/like` | 玩家不存在 |
|
||||
| `70028` | `spaces/{player_id}/like` | 取消点赞失败 |
|
||||
| `70029` | `spaces/me/visits` | 缺少会话 |
|
||||
| `70030` | `spaces` | 缺少空间目标参数 |
|
||||
| `70031` | `spaces/*/content` | 缺少空间目标参数 |
|
||||
| `70032` | `spaces/{player_id}/like` | 缺少空间目标参数 |
|
||||
| `70033` | `spaces/{player_id}/like` | 缺少空间目标参数 |
|
||||
| `70034` | `spaces/me/visits` | 访客列表查询失败 |
|
||||
| `70035` | `spaces/{player_id}/stats` | 缺少会话 |
|
||||
| `70036` | `spaces/{player_id}/stats` | 缺少空间目标参数 |
|
||||
| `70037` | `spaces/{player_id}/stats` | 玩家不存在 |
|
||||
| `70038` | `spaces/{player_id}/stats` | 空间统计读取失败 |
|
||||
| `70039` | `spaces/{player_id}/likes` | 缺少会话 |
|
||||
| `70040` | `spaces/{player_id}/likes` | 缺少空间目标参数 |
|
||||
| `70041` | `spaces/{player_id}/likes` | 玩家不存在 |
|
||||
| `70042` | `spaces/{player_id}/likes` | 点赞列表查询失败 |
|
||||
|
||||
| 错误码 | 接口/模块 | 含义 |
|
||||
|---|---|---|
|
||||
|
||||
@@ -66,7 +66,9 @@ docker compose up --build
|
||||
- 云存档:
|
||||
- 支持二进制文件上传/下载,适合直接存游戏存档 bytes
|
||||
- 玩家空间:
|
||||
- 当前为 `payload_json` 结构,适合资料卡、展示文本等轻量公开数据
|
||||
- 主信息接口返回空间元数据
|
||||
- 自定义空间内容通过独立二进制接口上传/下载
|
||||
- 支持任意 bytes,兼容二进制、文本、JSON、protobuf、msgpack 等格式
|
||||
|
||||
## 测试脚本
|
||||
|
||||
@@ -83,6 +85,7 @@ Windows PowerShell:
|
||||
|
||||
- [0001_init.sql](/F:/OtherWork/BriskGameSerivce/internal/platform/db/migrations/0001_init.sql)
|
||||
- [0008_identity_and_config_simplification.sql](/F:/OtherWork/BriskGameSerivce/internal/platform/db/migrations/0008_identity_and_config_simplification.sql)
|
||||
- [0009_space_binary_content.sql](/F:/OtherWork/BriskGameSerivce/internal/platform/db/migrations/0009_space_binary_content.sql)
|
||||
|
||||
服务启动时会自动执行未应用迁移。
|
||||
|
||||
|
||||
@@ -350,13 +350,149 @@ components:
|
||||
score:
|
||||
type: integer
|
||||
format: int64
|
||||
SpaceUpdateRequest:
|
||||
SpaceContentUploadRequest:
|
||||
type: object
|
||||
required: [payload_json]
|
||||
required: [file]
|
||||
properties:
|
||||
payload_json:
|
||||
base_version:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Optimistic lock base version. Use 0 for the first upload.
|
||||
content_type:
|
||||
type: string
|
||||
description: Optional MIME type. Defaults to application/octet-stream when omitted.
|
||||
checksum:
|
||||
type: string
|
||||
description: Optional SHA-256 hex checksum. When provided, the server validates it before saving.
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
SpaceMetadata:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
properties:
|
||||
project_account_id:
|
||||
type: string
|
||||
player_id:
|
||||
type: string
|
||||
login_provider:
|
||||
type: string
|
||||
login_user_id:
|
||||
type: string
|
||||
nickname:
|
||||
type: string
|
||||
avatar_url:
|
||||
type: string
|
||||
content_exists:
|
||||
type: boolean
|
||||
content_version:
|
||||
type: integer
|
||||
format: int64
|
||||
content_type:
|
||||
type: string
|
||||
content_size_bytes:
|
||||
type: integer
|
||||
format: int64
|
||||
content_checksum:
|
||||
type: string
|
||||
like_count:
|
||||
type: integer
|
||||
format: int64
|
||||
visit_count:
|
||||
type: integer
|
||||
format: int64
|
||||
liked_by_me:
|
||||
type: boolean
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
SpaceStats:
|
||||
type: object
|
||||
properties:
|
||||
project_account_id:
|
||||
type: string
|
||||
player_id:
|
||||
type: string
|
||||
login_provider:
|
||||
type: string
|
||||
login_user_id:
|
||||
type: string
|
||||
nickname:
|
||||
type: string
|
||||
avatar_url:
|
||||
type: string
|
||||
content_exists:
|
||||
type: boolean
|
||||
content_version:
|
||||
type: integer
|
||||
format: int64
|
||||
content_type:
|
||||
type: string
|
||||
content_size_bytes:
|
||||
type: integer
|
||||
format: int64
|
||||
content_checksum:
|
||||
type: string
|
||||
like_count:
|
||||
type: integer
|
||||
format: int64
|
||||
visit_count:
|
||||
type: integer
|
||||
format: int64
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
SpaceContentUpdateResult:
|
||||
type: object
|
||||
properties:
|
||||
player_id:
|
||||
type: string
|
||||
content_version:
|
||||
type: integer
|
||||
format: int64
|
||||
content_type:
|
||||
type: string
|
||||
content_size_bytes:
|
||||
type: integer
|
||||
format: int64
|
||||
content_checksum:
|
||||
type: string
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
SpaceLikeResult:
|
||||
type: object
|
||||
properties:
|
||||
liked:
|
||||
type: boolean
|
||||
like_count:
|
||||
type: integer
|
||||
format: int64
|
||||
SpaceVisitItem:
|
||||
type: object
|
||||
properties:
|
||||
visitor_player_id:
|
||||
type: string
|
||||
nickname:
|
||||
type: string
|
||||
avatar_url:
|
||||
type: string
|
||||
visited_at:
|
||||
type: string
|
||||
format: date-time
|
||||
SpaceLikeItem:
|
||||
type: object
|
||||
properties:
|
||||
player_id:
|
||||
type: string
|
||||
nickname:
|
||||
type: string
|
||||
avatar_url:
|
||||
type: string
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
paths:
|
||||
/client/bootstrap:
|
||||
get:
|
||||
@@ -644,7 +780,7 @@ paths:
|
||||
get:
|
||||
tags: [spaces]
|
||||
security: [{ bearerAuth: [] }]
|
||||
summary: Get player space
|
||||
summary: Get player space metadata
|
||||
parameters:
|
||||
- in: path
|
||||
name: player_id
|
||||
@@ -659,7 +795,46 @@ paths:
|
||||
schema: { type: string }
|
||||
description: Optional target login user id used with login provider lookup.
|
||||
responses:
|
||||
'200': { description: Space payload }
|
||||
'200':
|
||||
description: Space metadata
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SpaceMetadata'
|
||||
/spaces/{player_id}/content:
|
||||
get:
|
||||
tags: [spaces]
|
||||
security: [{ bearerAuth: [] }]
|
||||
summary: Download player space content
|
||||
parameters:
|
||||
- in: path
|
||||
name: player_id
|
||||
required: true
|
||||
schema: { type: string }
|
||||
- in: query
|
||||
name: login_provider
|
||||
schema: { type: string }
|
||||
description: Optional target login provider. When present, login_user_id is also required and overrides player_id lookup.
|
||||
- in: query
|
||||
name: login_user_id
|
||||
schema: { type: string }
|
||||
description: Optional target login user id used with login provider lookup.
|
||||
responses:
|
||||
'200':
|
||||
description: Binary space content stream
|
||||
headers:
|
||||
X-Space-Version:
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
X-Space-Checksum:
|
||||
schema:
|
||||
type: string
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
/spaces/{player_id}/stats:
|
||||
get:
|
||||
tags: [spaces]
|
||||
@@ -677,7 +852,12 @@ paths:
|
||||
name: login_user_id
|
||||
schema: { type: string }
|
||||
responses:
|
||||
'200': { description: Space stats }
|
||||
'200':
|
||||
description: Space stats
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SpaceStats'
|
||||
/spaces/{player_id}/likes:
|
||||
get:
|
||||
tags: [spaces]
|
||||
@@ -698,20 +878,33 @@ paths:
|
||||
name: limit
|
||||
schema: { type: integer }
|
||||
responses:
|
||||
'200': { description: Space likes }
|
||||
/spaces/me:
|
||||
put:
|
||||
tags: [spaces]
|
||||
security: [{ bearerAuth: [] }]
|
||||
summary: Update my space
|
||||
requestBody:
|
||||
required: true
|
||||
'200':
|
||||
description: Space likes
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SpaceUpdateRequest'
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/SpaceLikeItem'
|
||||
/spaces/me/content:
|
||||
put:
|
||||
tags: [spaces]
|
||||
security: [{ bearerAuth: [] }]
|
||||
summary: Upload my space content
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SpaceContentUploadRequest'
|
||||
responses:
|
||||
'200': { description: Space updated }
|
||||
'200':
|
||||
description: Space content updated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SpaceContentUpdateResult'
|
||||
'409': { description: Version conflict }
|
||||
/spaces/{player_id}/like:
|
||||
post:
|
||||
tags: [spaces]
|
||||
@@ -729,7 +922,12 @@ paths:
|
||||
name: login_user_id
|
||||
schema: { type: string }
|
||||
responses:
|
||||
'200': { description: Liked }
|
||||
'200':
|
||||
description: Liked
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SpaceLikeResult'
|
||||
'429': { description: Rate limited }
|
||||
delete:
|
||||
tags: [spaces]
|
||||
@@ -747,13 +945,18 @@ paths:
|
||||
name: login_user_id
|
||||
schema: { type: string }
|
||||
responses:
|
||||
'200': { description: Unliked }
|
||||
'200':
|
||||
description: Unliked
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SpaceLikeResult'
|
||||
'429': { description: Rate limited }
|
||||
/spaces/by-login:
|
||||
get:
|
||||
tags: [spaces]
|
||||
security: [{ bearerAuth: [] }]
|
||||
summary: Get player space by login identity
|
||||
summary: Get player space metadata by login identity
|
||||
parameters:
|
||||
- in: query
|
||||
name: login_provider
|
||||
@@ -764,7 +967,42 @@ paths:
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
'200': { description: Space payload }
|
||||
'200':
|
||||
description: Space metadata
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SpaceMetadata'
|
||||
/spaces/by-login/content:
|
||||
get:
|
||||
tags: [spaces]
|
||||
security: [{ bearerAuth: [] }]
|
||||
summary: Download player space content by login identity
|
||||
parameters:
|
||||
- in: query
|
||||
name: login_provider
|
||||
required: true
|
||||
schema: { type: string }
|
||||
- in: query
|
||||
name: login_user_id
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
'200':
|
||||
description: Binary space content stream
|
||||
headers:
|
||||
X-Space-Version:
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
X-Space-Checksum:
|
||||
schema:
|
||||
type: string
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
/spaces/by-login/stats:
|
||||
get:
|
||||
tags: [spaces]
|
||||
@@ -780,7 +1018,12 @@ paths:
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
'200': { description: Space stats }
|
||||
'200':
|
||||
description: Space stats
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SpaceStats'
|
||||
/spaces/by-login/likes:
|
||||
get:
|
||||
tags: [spaces]
|
||||
@@ -799,7 +1042,14 @@ paths:
|
||||
name: limit
|
||||
schema: { type: integer }
|
||||
responses:
|
||||
'200': { description: Space likes }
|
||||
'200':
|
||||
description: Space likes
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/SpaceLikeItem'
|
||||
/spaces/by-login/like:
|
||||
post:
|
||||
tags: [spaces]
|
||||
@@ -815,7 +1065,12 @@ paths:
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
'200': { description: Liked }
|
||||
'200':
|
||||
description: Liked
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SpaceLikeResult'
|
||||
'429': { description: Rate limited }
|
||||
delete:
|
||||
tags: [spaces]
|
||||
@@ -831,7 +1086,12 @@ paths:
|
||||
required: true
|
||||
schema: { type: string }
|
||||
responses:
|
||||
'200': { description: Unliked }
|
||||
'200':
|
||||
description: Unliked
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SpaceLikeResult'
|
||||
'429': { description: Rate limited }
|
||||
/spaces/me/visits:
|
||||
get:
|
||||
@@ -843,7 +1103,14 @@ paths:
|
||||
name: limit
|
||||
schema: { type: integer }
|
||||
responses:
|
||||
'200': { description: Visitor list }
|
||||
'200':
|
||||
description: Visitor list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/SpaceVisitItem'
|
||||
/admin/auth/login:
|
||||
post:
|
||||
tags: [admin-auth]
|
||||
@@ -1499,7 +1766,7 @@ paths:
|
||||
get:
|
||||
tags: [admin-spaces]
|
||||
security: [{ bearerAuth: [] }]
|
||||
summary: Get player space detail
|
||||
summary: Get player space metadata detail
|
||||
parameters:
|
||||
- in: path
|
||||
name: player_id
|
||||
@@ -1511,6 +1778,36 @@ paths:
|
||||
schema: { type: integer, format: int64 }
|
||||
responses:
|
||||
'200': { description: Space detail }
|
||||
/admin/spaces/{player_id}/content:
|
||||
get:
|
||||
tags: [admin-spaces]
|
||||
security: [{ bearerAuth: [] }]
|
||||
summary: Download player space content
|
||||
parameters:
|
||||
- in: path
|
||||
name: player_id
|
||||
required: true
|
||||
schema: { type: string }
|
||||
- in: query
|
||||
name: project_id
|
||||
required: true
|
||||
schema: { type: integer, format: int64 }
|
||||
responses:
|
||||
'200':
|
||||
description: Binary space content stream
|
||||
headers:
|
||||
X-Space-Version:
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
X-Space-Checksum:
|
||||
schema:
|
||||
type: string
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
/admin/spaces/{player_id}/likes:
|
||||
get:
|
||||
tags: [admin-spaces]
|
||||
|
||||
Reference in New Issue
Block a user