初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PCL.Core.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// System.Text.Json 兼容 Newtonsoft.Json 宽松行为的统一入口。
|
||||
/// </summary>
|
||||
public static class JsonCompat
|
||||
{
|
||||
public static readonly JsonNodeOptions NodeOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
public static readonly JsonDocumentOptions DocumentOptions = new()
|
||||
{
|
||||
AllowTrailingCommas = true,
|
||||
CommentHandling = JsonCommentHandling.Skip
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 统一的宽松 JSON 序列化配置。该实例在静态初始化时已被冻结,调用方不能修改全局行为。
|
||||
/// 如需追加调用点专用设置,请使用 <c>new JsonSerializerOptions(JsonCompat.SerializerOptions)</c> 克隆后修改。
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions SerializerOptions { get; } = _CreateSerializerOptions();
|
||||
|
||||
private static JsonSerializerOptions _CreateSerializerOptions()
|
||||
{
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
AllowTrailingCommas = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
Converters =
|
||||
{
|
||||
new FlexibleDateTimeConverter(),
|
||||
new FlexibleBoolConverter(),
|
||||
new FlexibleStringConverter(),
|
||||
new JsonStringEnumConverter()
|
||||
}
|
||||
};
|
||||
|
||||
options.MakeReadOnly(true);
|
||||
return options;
|
||||
}
|
||||
|
||||
public static JsonNode ParseNode(string text)
|
||||
{
|
||||
return JsonNode.Parse(text, NodeOptions, DocumentOptions)!;
|
||||
}
|
||||
|
||||
public static T? ToObject<T>(this JsonNode? node)
|
||||
{
|
||||
return node is null ? default : node.Deserialize<T>(SerializerOptions);
|
||||
}
|
||||
|
||||
public static JsonArray FromObject<T>(IEnumerable<T> items)
|
||||
{
|
||||
var arr = new JsonArray();
|
||||
foreach (var item in items)
|
||||
arr.Add(JsonSerializer.SerializeToNode(item, SerializerOptions));
|
||||
return arr;
|
||||
}
|
||||
|
||||
public static bool TryGetDateTime(JsonNode? node, out DateTime dateTime)
|
||||
{
|
||||
dateTime = default;
|
||||
switch (node)
|
||||
{
|
||||
case null:
|
||||
return false;
|
||||
case JsonValue value when value.TryGetValue<DateTime>(out var rawDateTime):
|
||||
dateTime = NormalizeDateTime(rawDateTime);
|
||||
return true;
|
||||
case JsonValue value
|
||||
when value.TryGetValue<string>(out var rawText) && TryParseDateTime(rawText, out dateTime):
|
||||
return true;
|
||||
default:
|
||||
try
|
||||
{
|
||||
dateTime = NormalizeDateTime(node.Deserialize<DateTime>(SerializerOptions));
|
||||
return true;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryParseDateTime(string? value, out DateTime dateTime)
|
||||
{
|
||||
dateTime = default;
|
||||
if (string.IsNullOrWhiteSpace(value)) return false;
|
||||
|
||||
// ISO 8601 允许用 24:00:00 表示当日终点(语义等于次日零点),但 .NET 的日期解析器不接受小时为 24,
|
||||
// 需先把小时 24 归一化为 00,再在解析成功后补一天。例如社区版本清单中的 2009-10-24T24:00:00+00:00。
|
||||
var addDay = false;
|
||||
var endOfDay = RegexPatterns.Iso8601EndOfDay.Match(value);
|
||||
if (endOfDay.Success)
|
||||
{
|
||||
value = value.Remove(endOfDay.Index + 1, 2).Insert(endOfDay.Index + 1, "00");
|
||||
addDay = true;
|
||||
}
|
||||
|
||||
if (DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind,
|
||||
out var dateTimeOffset))
|
||||
{
|
||||
if (addDay) dateTimeOffset = dateTimeOffset.AddDays(1);
|
||||
dateTime = dateTimeOffset.LocalDateTime;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var parsed))
|
||||
return false;
|
||||
|
||||
dateTime = NormalizeDateTime(addDay ? parsed.AddDays(1) : parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static DateTime NormalizeDateTime(DateTime dateTime)
|
||||
{
|
||||
return dateTime.Kind == DateTimeKind.Utc ? dateTime.ToLocalTime() : dateTime;
|
||||
}
|
||||
|
||||
public static void Merge(this JsonObject target, JsonNode? source)
|
||||
{
|
||||
if (source is not JsonObject sourceObj) return;
|
||||
|
||||
foreach (var prop in sourceObj.ToArray())
|
||||
switch (target[prop.Key])
|
||||
{
|
||||
case JsonObject targetChild when
|
||||
prop.Value is JsonObject sourceChild:
|
||||
targetChild.Merge(sourceChild);
|
||||
break;
|
||||
case JsonArray targetArray when
|
||||
prop.Value is JsonArray sourceArray:
|
||||
targetArray.Merge(sourceArray);
|
||||
break;
|
||||
default:
|
||||
target[prop.Key] = prop.Value?.DeepClone();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Merge(this JsonArray target, JsonNode? source)
|
||||
{
|
||||
if (source is not JsonArray sourceArr) return;
|
||||
foreach (var item in sourceArr)
|
||||
target.Add(item?.DeepClone());
|
||||
}
|
||||
|
||||
private sealed class FlexibleDateTimeConverter : JsonConverter<DateTime>
|
||||
{
|
||||
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.String)
|
||||
return NormalizeDateTime(reader.GetDateTime());
|
||||
|
||||
var value = reader.GetString();
|
||||
return TryParseDateTime(value, out var dateTime)
|
||||
? dateTime
|
||||
: NormalizeDateTime(reader.GetDateTime());
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FlexibleBoolConverter : JsonConverter<bool>
|
||||
{
|
||||
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.True => true,
|
||||
JsonTokenType.False => false,
|
||||
JsonTokenType.String when bool.TryParse(reader.GetString(), out var value) => value,
|
||||
JsonTokenType.String when int.TryParse(reader.GetString(), NumberStyles.Integer,
|
||||
CultureInfo.InvariantCulture, out var number) => number != 0,
|
||||
JsonTokenType.Number when reader.TryGetInt64(out var number) => number != 0,
|
||||
_ => throw new JsonException($"Can not convert JSON token {reader.TokenType} to Boolean.")
|
||||
};
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteBooleanValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FlexibleStringConverter : JsonConverter<string>
|
||||
{
|
||||
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.Null => null,
|
||||
JsonTokenType.String => reader.GetString(),
|
||||
JsonTokenType.Number => _ReadRawJsonValue(ref reader),
|
||||
JsonTokenType.True => bool.TrueString,
|
||||
JsonTokenType.False => bool.FalseString,
|
||||
_ => _ReadRawJsonValue(ref reader)
|
||||
};
|
||||
}
|
||||
|
||||
private static string _ReadRawJsonValue(ref Utf8JsonReader reader)
|
||||
{
|
||||
using var document = JsonDocument.ParseValue(ref reader);
|
||||
return document.RootElement.GetRawText();
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user