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 风格)。

二、主要类型与职责

  1. 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。
  1. CommentChildren
  • 表示子回复(或任意单条评论也可作为子项),字段类似但没有 Node/Relation。也有 CreateMappings / CreateCommentChildren 用于 Mapster 映射。
  • 同样对已删除的评论做了相同的匿名化处理。
  1. CommentMappingHelper
  • 将域层的 Commenter(实体)转换为视图层的 VmCommenter:
    • MembleCommenter -> VmMemberCommenter(注意实体名是 MembleCommenter,可能是拼写)
    • GuestCommenter -> VmGuestCommenter
    • 不支持的类型会抛出 NotSupportedException。
  1. CommentReplyTo
  • record,表示“被回复的对象”的简要信息 (Id, NickName)。
  1. 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 支持)。
评论加载中...