using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Dpz.Core.Entity.Base.MapperConfig;
using Dpz.Core.EnumLibrary;
using Dpz.Core.Public.Entity;
using TypeAdapterConfig = Mapster.TypeAdapterConfig;
namespace Dpz.Core.Public.ViewModel;
/// <summary>
/// 评论 出参
/// </summary>
public class CommentViewModel : IHaveCustomMapping
{
public required string Id { get; set; }
/// <summary>
/// 评论类型
/// </summary>
public CommentNode Node { get; set; }
/// <summary>
/// 关联
/// </summary>
public required string Relation { get; set; }
/// <summary>
/// 回复时间
/// </summary>
public DateTime PublishTime { get; set; }
/// <summary>
/// 评论人 (多态)分为匿名评论和成员评论
/// </summary>
[JsonConverter(typeof(CommenterConverter))]
public required VmCommenter Commenter { get; set; }
/// <summary>
/// 回复内容
/// </summary>
public required string CommentText { get; set; }
/// <summary>
/// 回复ID
/// </summary>
public List<string> Replies { get; set; } = [];
/// <summary>
/// 是否删除
/// </summary>
public required bool IsDelete { get; set; }
/// <summary>
/// 回复
/// </summary>
public List<CommentChildren> Children { get; set; } = [];
public static void CreateMappings(TypeAdapterConfig cfg)
{
cfg.NewConfig<Comment, CommentViewModel>().MapWith(x => CreateCommentViewModel(x));
}
private static CommentViewModel CreateCommentViewModel(Comment entity)
{
var commenter = entity.Commenter;
var commentText = entity.CommentText;
if (entity.IsDelete)
{
commenter = new GuestCommenter
{
Email = "",
NickName = "该用户发言已被删除",
Site = "",
};
commentText = "该用户发言因某些原因已被删除";
}
var viewModel = new CommentViewModel
{
Id = entity.Id.ToString(),
Node = entity.Node,
Relation = entity.Relation,
PublishTime = entity.PublishTime.ToLocalTime(),
CommentText = commentText,
Replies = entity.Replies.Select(x => x.ToString()).ToList(),
IsDelete = entity.IsDelete,
Children = new List<CommentChildren>(),
Commenter = CommentMappingHelper.ToViewModelCommenter(commenter),
};
return viewModel;
}
}
public class CommentChildren : IHaveCustomMapping
{
public required string Id { get; set; }
/// <summary>
/// 回复时间
/// </summary>
public DateTime PublishTime { get; set; }
/// <summary>
/// 评论人
/// </summary>
[JsonConverter(typeof(CommenterConverter))]
public required VmCommenter Commenter { get; set; }
/// <summary>
/// 回复内容
/// </summary>
public required string CommentText { get; set; }
/// <summary>
/// 回复ID
/// </summary>
public List<string> Replies { get; set; } = [];
/// <summary>
/// 是否删除
/// </summary>
public required bool IsDelete { get; set; }
/// <summary>
/// 被回复对象
/// </summary>
public CommentReplyTo? ReplyTo { get; set; }
public static void CreateMappings(TypeAdapterConfig cfg)
{
cfg.NewConfig<Comment, CommentChildren>().MapWith(x => CreateCommentChildren(x));
}
private static CommentChildren CreateCommentChildren(Comment entity)
{
var commenter = entity.Commenter;
var commentText = entity.CommentText;
if (entity.IsDelete)
{
commenter = new GuestCommenter
{
Email = "",
NickName = "该用户发言已被删除",
Site = "",
};
commentText = "该用户发言因某些原因已被删除";
}
var viewModel = new CommentChildren
{
Id = entity.Id.ToString(),
PublishTime = entity.PublishTime.ToLocalTime(),
CommentText = commentText,
Replies = entity.Replies.Select(x => x.ToString()).ToList(),
IsDelete = entity.IsDelete,
Commenter = CommentMappingHelper.ToViewModelCommenter(commenter),
};
return viewModel;
}
}
internal static class CommentMappingHelper
{
internal static VmCommenter ToViewModelCommenter(Commenter commenter)
{
return commenter switch
{
MembleCommenter membleCommenter => new VmMemberCommenter
{
NickName = membleCommenter.NickName,
Avatar = membleCommenter.Avatar,
Identity = membleCommenter.Identity,
},
GuestCommenter guestCommenter => new VmGuestCommenter(guestCommenter.Email)
{
NickName = guestCommenter.NickName,
Site = guestCommenter.Site,
},
_ => throw new NotSupportedException(
$"not supported commenter type:{commenter.GetType().Name}"
),
};
}
}
/// <summary>
/// 被回复对象信息
/// </summary>
/// <param name="Id">被回复评论ID</param>
/// <param name="NickName">被回复人昵称</param>
public record CommentReplyTo(string Id, string NickName);
public abstract class VmCommenter
{
/// <summary>
/// 昵称
/// </summary>
public required string NickName { get; set; }
}
/// <summary>
/// 登录会员评论
/// </summary>
public class VmMemberCommenter : VmCommenter
{
/// <summary>
/// 头像
/// </summary>
public required string Avatar { get; set; }
/// <summary>
/// 身份标识
/// </summary>
public required string Identity { get; set; }
}
/// <summary>
/// 匿名评论
/// </summary>
public class VmGuestCommenter(string email) : VmCommenter
{
/// <summary>
/// 网站
/// </summary>
public string? Site { get; set; }
/// <summary>
/// 邮箱MD5
/// </summary>
public string EmailMd5 => email.GenerateHashMd5();
/// <summary>
/// 获取邮箱
/// </summary>
public string GetEmail() => email;
}
public class CommenterConverter : JsonConverter<VmCommenter>
{
public override VmCommenter? Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options
)
{
if (reader.TokenType == JsonTokenType.Null)
{
return null;
}
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException("JsonTokenType.StartObject not found.");
}
if (
!reader.Read()
|| reader.TokenType != JsonTokenType.PropertyName
|| reader.GetString() != "$type"
)
{
throw new JsonException("Property $type not found.");
}
if (!reader.Read() || reader.TokenType != JsonTokenType.String)
{
throw new JsonException("Value at $type is invalid.");
}
var typeStr = reader.GetString();
var type = typeStr switch
{
"GuestCommenter" => typeof(VmGuestCommenter),
"MemberCommenter" => typeof(VmMemberCommenter),
_ => typeof(Commenter),
};
using var output = new MemoryStream();
ReadObject(ref reader, output, options);
var result = JsonSerializer.Deserialize(output.ToArray(), type, options);
return result as VmCommenter;
}
private void ReadObject(ref Utf8JsonReader reader, Stream output, JsonSerializerOptions options)
{
using var writer = new Utf8JsonWriter(
output,
new JsonWriterOptions { Encoder = options.Encoder, Indented = options.WriteIndented }
);
writer.WriteStartObject();
var objectIntend = 0;
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.None:
case JsonTokenType.Null:
writer.WriteNullValue();
break;
case JsonTokenType.StartObject:
writer.WriteStartObject();
objectIntend++;
break;
case JsonTokenType.EndObject:
writer.WriteEndObject();
if (objectIntend == 0)
{
writer.Flush();
return;
}
objectIntend--;
break;
case JsonTokenType.StartArray:
writer.WriteStartArray();
break;
case JsonTokenType.EndArray:
writer.WriteEndArray();
break;
case JsonTokenType.PropertyName:
writer.WritePropertyName(reader.GetString() ?? "");
break;
case JsonTokenType.Comment:
writer.WriteCommentValue(reader.GetComment());
break;
case JsonTokenType.String:
writer.WriteStringValue(reader.GetString());
break;
case JsonTokenType.Number:
writer.WriteNumberValue(reader.GetInt32());
break;
case JsonTokenType.True:
case JsonTokenType.False:
writer.WriteBooleanValue(reader.GetBoolean());
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
public override void Write(
Utf8JsonWriter writer,
VmCommenter value,
JsonSerializerOptions options
)
{
writer.WriteStartObject();
var valueType = value.GetType();
//var valueAssemblyName = valueType.Assembly.GetName();
writer.WriteString("$type", $"{valueType.Name.Replace("Vm", "")}");
var json = JsonSerializer.Serialize(value, value.GetType(), options);
using (
var document = JsonDocument.Parse(
json,
new JsonDocumentOptions
{
AllowTrailingCommas = options.AllowTrailingCommas,
MaxDepth = options.MaxDepth,
}
)
)
{
foreach (var jsonProperty in document.RootElement.EnumerateObject())
{
jsonProperty.WriteTo(writer);
}
}
writer.WriteEndObject();
}
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
下面把这段代码的结构、职责和关键行为分块说明,方便理解它是做什么的、如何工作以及需要注意的点。
一、总体用途
- 这组类把后台的评论实体(Comment / Commenter 等,实体类型在代码中未给出)映射成用于输出(API/前端)的视图模型(ViewModel),并在序列化/反序列化 VmCommenter(评论者)时实现了自定义的多态 JSON 处理。
- 使用 Mapster(TypeAdapterConfig)为 Comment -> CommentViewModel / CommentChildren 提供自定义映射逻辑(IHaveCustomMapping 风格)。
二、主要类型与职责
- CommentViewModel
- 用于表示“顶层评论”的返回模型:
- Id、Node(评论类型枚举)、Relation(关联对象 id 或类型)、PublishTime(发布时间)、CommentText、Replies(回复 id 列表)、IsDelete、Children(子评论列表)等。
- Commenter:多态字段,类型是 VmCommenter(抽象基类),使用 CommenterConverter 进行 JSON(反)序列化。
- CreateMappings(TypeAdapterConfig cfg):Mapster 配置,指明 Comment -> CommentViewModel 的映射由 CreateCommentViewModel 方法自定义实现。
- CreateCommentViewModel:从实体 Comment 创建 ViewModel,注意:
- 如果实体 IsDelete 为 true,会“脱敏/替换”评论者信息(使用一个 GuestCommenter)并替换评论文本为“已删除”的提示。
- PublishTime 被转换为本地时间(ToLocalTime)。
- Replies、Children、Commenter 的转换:Replies 转成 string 列表,Commenter 由 CommentMappingHelper.ToViewModelCommenter 转换为 VmCommenter。
- CommentChildren
- 表示子回复(或任意单条评论也可作为子项),字段类似但没有 Node/Relation。也有 CreateMappings / CreateCommentChildren 用于 Mapster 映射。
- 同样对已删除的评论做了相同的匿名化处理。
- CommentMappingHelper
- 将域层的 Commenter(实体)转换为视图层的 VmCommenter:
- MembleCommenter -> VmMemberCommenter(注意实体名是 MembleCommenter,可能是拼写)
- GuestCommenter -> VmGuestCommenter
- 不支持的类型会抛出 NotSupportedException。
- CommentReplyTo
- record,表示“被回复的对象”的简要信息 (Id, NickName)。
- VmCommenter 及派生类型
- VmCommenter:抽象基类,仅含 NickName。
- VmMemberCommenter:登录用户的评论者视图(Avatar, Identity)。
- VmGuestCommenter:匿名评论者,定义为 record 带主构造参数 string email:
- 有 Site 可选字段,EmailMd5 属性通过 email.GenerateHashMd5() 生成(依赖外部扩展方法),GetEmail() 返回原始 email。
三、CommenterConverter(自定义 JSON 转换器)
- 这是一个继承 JsonConverter
的自定义转换器,用于把 VmCommenter 序列化成 JSON 时写入类型信息,并在反序列化时根据类型恢复具体子类。 - 写(Write):
- 写入一个对象开始标记,先写入 "$type" 属性,值为 value.GetType().Name 去掉 "Vm" 前缀(例如 VmMemberCommenter -> "MemberCommenter",VmGuestCommenter -> "GuestCommenter")。
- 然后把整个对象序列化成 JSON(使用 JsonSerializer),再把其属性逐个写入到当前 writer 中(避免重复写入 $type 的属性)。
- 读(Read):
- 要求 JSON 非空且首个属性必须是 "$type" 且为字符串(即期望形如 {"$type": "GuestCommenter", ...})。
- 根据 $type 字符串选择要反序列化成的类型:
- "GuestCommenter" -> typeof(VmGuestCommenter)
- "MemberCommenter" -> typeof(VmMemberCommenter)
- 其它 -> typeof(Commenter)(此处有点奇怪,见下文)
- 将剩下的对象内容读取并写入内存流,再用 JsonSerializer.Deserialize(outputBytes, type, options) 反序列化为对象并返回 VmCommenter。
四、关键行为/细节
- 删除处理:若实体 Comment.IsDelete == true,则在映射时把评论者替换成一个 GuestCommenter(邮箱空、昵称为“该用户发言已被删除”等),同时把 CommentText 替换成“该用户发言因某些原因已被删除”。这样前端就无法得知原始用户信息和内容。
- PublishTime 调用了 ToLocalTime():把 UTC/服务器时间转换为本地时间输出。
- Replies:实体的 Replies(可能是 Guid 列表)被映射为字符串列表(ToString)。
- VmGuestCommenter 使用主构造参数 email,EmailMd5 动态从该 email 计算(依赖扩展方法 GenerateHashMd5)。
五、潜在问题 / 需要注意的地方
- CommenterConverter 的 Read 很脆弱:
- 它强制要求 $type 必须是对象的第一个属性(代码通过 reader.Read() 检查第一个 PropertyName 就是 "$type")。如果 JSON 中 $type 不是第一个属性或者有其他属性顺序不同,反序列化会失败。
- Read 中默认分支把未知 $type 映射为 typeof(Commenter),但该类型在本文件中并不是 VmCommenter 的派生(实际 Commenter 是域实体),直接反序列化成 Commenter 并 cast 为 VmCommenter 会导致问题或返回 null。应当在未知类型时抛异常或映射到某个合适的 VmCommenter。
- ReadObject 对数字只用了 reader.GetInt32(),如果 JSON 中数字不是 int(比如 long、double),可能抛异常;另外对字符串/数值/布尔等的处理是逐 token 写出,逻辑较复杂且容易出现边界问题。
- Write 中通过 value.GetType().Name.Replace("Vm", "") 来写 $type,假设命名规则严格为 VmXxx,这在当前实现中成立,但有命名变化会影响兼容性。
- CommentMappingHelper 中有 MembleCommenter(拼写)映射到 VmMemberCommenter,需确认实体类实际名称(是否应为 MemberCommenter)。
- JsonConverter 的 Read 将剩余对象读入 MemoryStream 再 Deserialize,效率上会有额外内存/序列化开销(但通常可接受)。同时如果 options 中包含自定义 converter,需注意递归调用行为是否如预期。
六、总体流程示例
- 后端有 Comment 实体。
- 使用 Mapster 的 TypeAdapterConfig 注册过 CreateMappings 后,执行映射:
- 顶层 Comment -> CommentViewModel(CreateCommentViewModel),若 IsDelete 则替换内容,
- 子评论同理映射为 CommentChildren。
- 将 CommentViewModel JSON 序列化时,VmCommenter 会被 CommenterConverter 拦截并写入 $type 字段,携带子类数据。
- 反序列化时 CommenterConverter 根据 $type 创建具体的 VmGuestCommenter 或 VmMemberCommenter 实例并返回(注意前面提到的对未知类型和属性顺序的脆弱性)。
如果你希望,我可以:
- 指出并修改 CommenterConverter 中的潜在 bug(例如不强制 $type 首位、对未知类型抛异常或返回安全默认值、改进数字处理等);
- 或将映射逻辑改成更稳健的方式(例如 Mapster 的普通映射或使用 System.Text.Json 的 type discriminator 支持)。
评论加载中...