using System.Xml;
using System.Xml.Linq;
namespace Dpz.Core.Service.Mediator.Features.Storage.Commands;
/// <summary>
/// 腾讯云媒体处理队列回调的结构化负载
/// </summary>
internal sealed record TencentMediaProcessCallbackPayload(
string Format,
List<TencentMediaQueueJobRecord> JobsDetail,
List<string> NonExistJobIds
);
/// <summary>
/// 解析腾讯云媒体处理队列回调正文,自动识别 JSON 或 XML 格式
/// </summary>
internal static class TencentMediaProcessCallbackParser
{
/// <summary>
/// 解析回调正文为统一的结构化负载,格式无法识别或内容非法时抛出 FormatException
/// </summary>
public static TencentMediaProcessCallbackPayload Parse(string payload, string? contentType)
{
var format = DetectFormat(payload, contentType);
try
{
return format switch
{
"JSON" => ParseJson(payload),
"XML" => ParseXml(payload),
_ => throw new FormatException("不支持的腾讯云回调格式"),
};
}
catch (JsonException exception)
{
throw new FormatException("腾讯云 JSON 回调格式无效", exception);
}
catch (XmlException exception)
{
throw new FormatException("腾讯云 XML 回调格式无效", exception);
}
}
/// <summary>
/// 根据正文首字符或 Content-Type 判断回调格式
/// </summary>
private static string DetectFormat(string payload, string? contentType)
{
var trimmedPayload = payload.TrimStart();
if (trimmedPayload.StartsWith('<'))
{
return "XML";
}
if (trimmedPayload.StartsWith('{') || trimmedPayload.StartsWith('['))
{
return "JSON";
}
if (contentType?.Contains("xml", StringComparison.OrdinalIgnoreCase) == true)
{
return "XML";
}
if (contentType?.Contains("json", StringComparison.OrdinalIgnoreCase) == true)
{
return "JSON";
}
throw new FormatException("无法识别腾讯云回调格式");
}
/// <summary>
/// 解析 JSON 格式回调,兼容外层 Response 包装与单任务对象两种结构
/// </summary>
private static TencentMediaProcessCallbackPayload ParseJson(string payload)
{
using var document = JsonDocument.Parse(payload);
var response = document.RootElement;
if (TryGetProperty(response, "Response", out var responseElement))
{
response = responseElement;
}
var jobs = new List<TencentMediaQueueJobRecord>();
if (TryGetProperty(response, "JobsDetail", out var jobsElement))
{
if (jobsElement.ValueKind == JsonValueKind.Object)
{
jobs.Add(ParseJsonJob(jobsElement));
}
else if (jobsElement.ValueKind == JsonValueKind.Array)
{
foreach (var item in jobsElement.EnumerateArray())
{
if (item.ValueKind == JsonValueKind.Object)
{
jobs.Add(ParseJsonJob(item));
}
}
}
}
var nonExistJobIds = new List<string>();
if (TryGetProperty(response, "NonExistJobIds", out var nonExistElement))
{
if (nonExistElement.ValueKind == JsonValueKind.Array)
{
foreach (var item in nonExistElement.EnumerateArray())
{
var value = GetJsonString(item);
if (!string.IsNullOrWhiteSpace(value))
{
nonExistJobIds.Add(value);
}
}
}
else
{
var value = GetJsonString(nonExistElement);
if (!string.IsNullOrWhiteSpace(value))
{
nonExistJobIds.Add(value);
}
}
}
return new TencentMediaProcessCallbackPayload("JSON", jobs, nonExistJobIds);
}
/// <summary>
/// 解析单个 JSON 任务对象为任务摘要
/// </summary>
private static TencentMediaQueueJobRecord ParseJsonJob(JsonElement job)
{
return new TencentMediaQueueJobRecord
{
JobId = GetJsonPropertyString(job, "JobId"),
State = GetJsonPropertyString(job, "State"),
Code = GetJsonPropertyString(job, "Code"),
Message = GetJsonPropertyString(job, "Message"),
Tag = GetJsonPropertyString(job, "Tag"),
QueueId = GetJsonPropertyString(job, "QueueId"),
CreationTime = GetJsonPropertyString(job, "CreationTime"),
StartTime = GetJsonPropertyString(job, "StartTime"),
EndTime = GetJsonPropertyString(job, "EndTime"),
InputObject = GetJsonNestedPropertyString(job, "Input", "Object"),
OutputObject = GetJsonNestedPropertyString(job, "Operation", "Output", "Object"),
};
}
/// <summary>
/// 解析 XML 格式回调,兼容外层 Response 包装
/// </summary>
private static TencentMediaProcessCallbackPayload ParseXml(string payload)
{
using var stringReader = new StringReader(payload);
using var reader = XmlReader.Create(
stringReader,
new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null }
);
var document = XDocument.Load(reader, LoadOptions.PreserveWhitespace);
var response = document.Root;
if (response == null || !string.Equals(response.Name.LocalName, "Response"))
{
response = document
.Descendants()
.FirstOrDefault(x => string.Equals(x.Name.LocalName, "Response"));
}
if (response == null)
{
throw new FormatException("腾讯云 XML 回调缺少 Response 节点");
}
var jobs = response
.Elements()
.Where(x => string.Equals(x.Name.LocalName, "JobsDetail"))
.Select(ParseXmlJob)
.ToList();
var nonExistJobIds = response
.Elements()
.Where(x => string.Equals(x.Name.LocalName, "NonExistJobIds"))
.SelectMany(GetXmlStringValues)
.Where(x => !string.IsNullOrWhiteSpace(x))
.ToList();
return new TencentMediaProcessCallbackPayload("XML", jobs, nonExistJobIds);
}
/// <summary>
/// 解析单个 XML 任务节点为任务摘要
/// </summary>
private static TencentMediaQueueJobRecord ParseXmlJob(XElement job)
{
return new TencentMediaQueueJobRecord
{
JobId = GetXmlNestedChildValue(job, "JobId"),
State = GetXmlNestedChildValue(job, "State"),
Code = GetXmlNestedChildValue(job, "Code"),
Message = GetXmlNestedChildValue(job, "Message"),
Tag = GetXmlNestedChildValue(job, "Tag"),
QueueId = GetXmlNestedChildValue(job, "QueueId"),
CreationTime = GetXmlNestedChildValue(job, "CreationTime"),
StartTime = GetXmlNestedChildValue(job, "StartTime"),
EndTime = GetXmlNestedChildValue(job, "EndTime"),
InputObject = GetXmlNestedChildValue(job, "Input", "Object"),
OutputObject = GetXmlNestedChildValue(job, "Operation", "Output", "Object"),
};
}
/// <summary>
/// 按属性名不区分大小写查找 JSON 属性
/// </summary>
private static bool TryGetProperty(
JsonElement element,
string propertyName,
out JsonElement value
)
{
if (element.ValueKind == JsonValueKind.Object)
{
foreach (var property in element.EnumerateObject())
{
if (string.Equals(property.Name, propertyName, StringComparison.OrdinalIgnoreCase))
{
value = property.Value;
return true;
}
}
}
value = default;
return false;
}
/// <summary>
/// 读取 JSON 属性值并转为字符串,属性缺失时返回 null
/// </summary>
private static string? GetJsonPropertyString(JsonElement element, string propertyName)
{
return TryGetProperty(element, propertyName, out var value) ? GetJsonString(value) : null;
}
/// <summary>
/// 按属性名路径逐层读取 JSON 字符串值,任一层缺失时返回 null
/// </summary>
private static string? GetJsonNestedPropertyString(
JsonElement element,
params string[] propertyNames
)
{
foreach (var propertyName in propertyNames)
{
if (!TryGetProperty(element, propertyName, out element))
{
return null;
}
}
return GetJsonString(element);
}
/// <summary>
/// 将 JSON 值转为字符串,仅支持字符串、数字和布尔值,其余返回 null
/// </summary>
private static string? GetJsonString(JsonElement element)
{
return element.ValueKind switch
{
JsonValueKind.String => element.GetString(),
JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False => element.ToString(),
_ => null,
};
}
/// <summary>
/// 按节点名路径逐层查找 XML 子节点并返回其文本值,任一层缺失时返回 null
/// </summary>
private static string? GetXmlNestedChildValue(XElement element, params string[] names)
{
var current = element;
foreach (var name in names)
{
current = current.Elements().FirstOrDefault(x => string.Equals(x.Name.LocalName, name));
if (current is null)
{
return null;
}
}
return current.Value;
}
/// <summary>
/// 读取 NonExistJobIds 节点下所有子节点的文本值,无子节点时返回节点自身文本
/// </summary>
private static List<string> GetXmlStringValues(XElement element)
{
var childValues = element
.Elements()
.Select(x => x.Value)
.Where(x => !string.IsNullOrWhiteSpace(x))
.ToList();
return childValues.Count > 0 ? childValues : [element.Value];
}
}
这段代码是一个用于解析腾讯云媒体处理(MPS)队列回调消息的 C# 解析器。它能把腾讯云回调返回的 JSON 或 XML 正文统一转换成结构化的数据模型,方便后续业务逻辑处理。
1. 整体设计
数据模型:TencentMediaProcessCallbackPayload
这是一个 record,包含三个属性:
Format:识别出的回调格式,固定为"JSON"或"XML"。JobsDetail:任务详情列表,每个任务被抽象为TencentMediaQueueJobRecord。NonExistJobIds:不存在的任务 ID 列表。
它用于统一承载来自两种不同格式的原始数据。
静态解析类:TencentMediaProcessCallbackParser
对外只暴露一个入口方法 Parse,内部根据格式自动分流,并进行容错解析。
2. 核心入口:Parse 方法
public static TencentMediaProcessCallbackPayload Parse(string payload, string? contentType)
参数:
payload:回调正文(字符串)。contentType:HTTP 头中的 Content-Type,可能为null。
流程:
- 调用
DetectFormat判断格式。 - 根据格式调用
ParseJson或ParseXml。 - 捕获解析过程中可能抛出的
JsonException和XmlException,统一包装为FormatException,并附带说明“腾讯云 JSON/XML 回调格式无效”的提示信息。
3. 格式检测:DetectFormat
判断优先级如下:
- 去掉正文开头的空白字符后,若以
<开头 → 判定为 XML。 - 若以
{或[开头 → 判定为 JSON。 - 如果正文没有明显特征,则根据
contentType判断:- 包含
"xml"(不区分大小写)→ XML。 - 包含
"json"(不区分大小写)→ JSON。
- 包含
- 否则抛出
FormatException,提示“无法识别腾讯云回调格式”。
这种策略优先信任实际正文内容,再使用 HTTP 头作为备选,增强了兼容性。
4. JSON 格式解析
兼容外层包装
腾讯云 API 通常返回类似 {"Response": {...}} 的结构,但某些场景也可能直接返回数据本身。代码先尝试从根元素中读取 "Response" 属性(不区分大小写),如果存在就切换到该子元素,否则仍使用原始根元素。
解析任务列表 JobsDetail
- 若
JobsDetail是一个对象,则视为单个任务,直接解析。 - 若
JobsDetail是一个数组,则遍历数组,仅处理对象类型的元素。 - 每个对象通过
ParseJsonJob转换为TencentMediaQueueJobRecord。
解析 NonExistJobIds
- 若数组则逐个读取字符串元素。
- 若单个值则直接读取为该列表的唯一元素。
- 空字符串会被忽略。
ParseJsonJob 方法
从任务对象中提取常用字段:
- 基本字段:
JobId、State、Code、Message、Tag、QueueId、CreationTime、StartTime、EndTime。 - 嵌套字段:
Input.Object和Operation.Output.Object,即输入/输出文件路径。
所有字段都通过辅助方法安全读取,缺失时返回 null,不会因缺少某个属性而中断解析。
5. XML 格式解析
安全初始化
使用 XmlReader.Create 时指定:
DtdProcessing = DtdProcessing.Prohibit:禁止 DTD 处理,防止 XXE(XML 外部实体注入)攻击。XmlResolver = null:不解析外部资源。
查找 Response 节点
与 JSON 类似,先看根节点是否为 <Response>,若不是,则在所有后代节点中查找第一个名为 Response 的节点。若找不到则抛出 FormatException。
解析任务列表 JobsDetail
通过 response.Elements() 直接查找所有名为 JobsDetail 的子节点,逐一调用 ParseXmlJob。
解析 NonExistJobIds
同样查找该子节点,并通过 GetXmlStringValues 提取所有子元素的文本值;若没有子元素,则返回节点自身的文本。
ParseXmlJob 方法
对应 JSON 版本,提取相同的字段,并使用 GetXmlNestedChildValue 逐层查找嵌套节点(如 Input.Object、Operation.Output.Object)。
6. 辅助方法
JSON 辅助方法
TryGetProperty:忽略属性名大小写,在 JSON 对象中查找指定属性,将结果输出到out参数。GetJsonPropertyString:获取指定属性的字符串值。GetJsonNestedPropertyString:按路径逐层查找属性,例如GetJsonNestedPropertyString(job, "Operation", "Output", "Object")。GetJsonString:将 JSON 值转为字符串——只有字符串、数字、布尔值会被转换,其他类型(如对象、数组)返回null。
XML 辅助方法
GetXmlNestedChildValue:按节点名路径逐层查找子节点,最后返回其文本值,任一层缺失返回null。GetXmlStringValues:收集某节点下所有子元素的文本值,如果没有子元素则返回节点本身文本(用于兼容直接以文本形式存在的NonExistJobIds)。
7. 关键特性与优点
- 自动格式识别:不需要调用方特意指定格式,更加健壮。
- 兼容多种腾讯云返回结构:既能处理带
Response包装的标准响应,也能处理直接返回数据的简化结构;任务可以是单个对象或数组。 - 字段缺失容错:所有字段读取均为可选,缺失时置
null,不会因数据不完整而抛异常。 - 安全性:XML 解析时禁用了 DTD 和外部实体,防止 XXE 等安全风险。
- 统一的错误处理:将底层异常(
JsonException/XmlException)包装成FormatException,便于上层捕获并返回明确的业务错误。
8. 使用场景
这段代码适用于接收腾讯云媒体处理(MPS)任务完成、失败等回调通知的场景。它将原始回调消息转换为 TencentMediaProcessCallbackPayload 对象,上层可以据此获取任务 ID、状态、输入输出路径等信息,从而更新业务状态或触发后续流程。
总体而言,这是一个结构清晰、防御性较强的解析器,充分考虑了腾讯云回调的多样性和潜在的安全威胁,是一个可靠的基础组件。