From 5e6449cf1fdc02ffde5bd78b9313ac67f19e835d Mon Sep 17 00:00:00 2001 From: SugarChes <103551815+SugarChes@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:01:56 +0800 Subject: [PATCH] fix(util): preserve complex JSON values in data tables --- 插件库/Lskj.Util.Tests/Lskj.Util.Tests.csproj | 38 ++++++ 插件库/Lskj.Util.Tests/Program.cs | 120 ++++++++++++++++++ 插件库/Lskj.Util/JsonUtil.cs | 80 ++++++++++++ 3 files changed, 238 insertions(+) create mode 100644 插件库/Lskj.Util.Tests/Lskj.Util.Tests.csproj create mode 100644 插件库/Lskj.Util.Tests/Program.cs diff --git a/插件库/Lskj.Util.Tests/Lskj.Util.Tests.csproj b/插件库/Lskj.Util.Tests/Lskj.Util.Tests.csproj new file mode 100644 index 0000000..97651a0 --- /dev/null +++ b/插件库/Lskj.Util.Tests/Lskj.Util.Tests.csproj @@ -0,0 +1,38 @@ + + + + Debug + AnyCPU + {8E501869-816F-4E3E-81EE-A3BF9C223D61} + Exe + Lskj.Util.Tests + Lskj.Util.Tests + v4.0 + + + true + full + bin\Debug\ + AnyCPU + + + + + + + False + ..\..\引用DLL\Newtonsoft.Json.dll + True + + + + + + + + {A51BF642-6543-4DE3-8948-83F558B72BD4} + Lskj.Util + + + + diff --git a/插件库/Lskj.Util.Tests/Program.cs b/插件库/Lskj.Util.Tests/Program.cs new file mode 100644 index 0000000..028cbe6 --- /dev/null +++ b/插件库/Lskj.Util.Tests/Program.cs @@ -0,0 +1,120 @@ +using Lskj.Util; +using Newtonsoft.Json; +using System; +using System.Data; + +namespace Lskj.Util.Tests +{ + internal static class Program + { + private static int Main() + { + try + { + PreservesStylesObjectAsCompactJson(); + PreservesArrayAsCompactJson(); + NormalizesEveryValueInAMixedComplexColumn(); + LeavesScalarColumnsEquivalentToExistingConverter(); + AcceptsSingleObject(); + RejectsInvalidTopLevelValues(); + Console.WriteLine("PASS: JsonUtil complex-value conversion tests"); + return 0; + } + catch (Exception ex) + { + Console.Error.WriteLine(ex); + return 1; + } + } + + private static void PreservesStylesObjectAsCompactJson() + { + const string json = "[{\"id\":1,\"$styles\":{\"rowstyle\":\"{color:'#FFFF00','background-color':'#FF0000'}\"}}]"; + DataTable table = JsonUtil.ToDataTablePreservingComplexValues(json); + AssertEqual(1, table.Rows.Count, "styles row count"); + AssertEqual("{\"rowstyle\":\"{color:'#FFFF00','background-color':'#FF0000'}\"}", + table.Rows[0]["$styles"].ToString(), "styles JSON"); + } + + private static void PreservesArrayAsCompactJson() + { + const string json = "[{\"id\":1,\"items\":[{\"code\":\"A\"},{\"code\":\"B\"}]}]"; + DataTable table = JsonUtil.ToDataTablePreservingComplexValues(json); + AssertEqual("[{\"code\":\"A\"},{\"code\":\"B\"}]", + table.Rows[0]["items"].ToString(), "array JSON"); + } + + private static void NormalizesEveryValueInAMixedComplexColumn() + { + const string json = "[" + + "{\"id\":1,\"payload\":5}," + + "{\"id\":2,\"payload\":{\"code\":\"A\"}}," + + "{\"id\":3,\"payload\":[1,2]}," + + "{\"id\":4,\"payload\":null}," + + "{\"id\":5,\"payload\":\"ready\"}]"; + DataTable table = JsonUtil.ToDataTablePreservingComplexValues(json); + AssertEqual(typeof(string), table.Columns["payload"].DataType, "mixed column type"); + AssertEqual("5", table.Rows[0]["payload"].ToString(), "numeric scalar text"); + AssertEqual("{\"code\":\"A\"}", table.Rows[1]["payload"].ToString(), "object text"); + AssertEqual("[1,2]", table.Rows[2]["payload"].ToString(), "array text"); + AssertTrue(table.Rows[3].IsNull("payload"), "null remains null"); + AssertEqual("ready", table.Rows[4]["payload"].ToString(), "string remains string"); + } + + private static void LeavesScalarColumnsEquivalentToExistingConverter() + { + const string json = "[{\"id\":1,\"title\":\"A\",\"active\":true},{\"id\":2,\"title\":\"B\",\"active\":false}]"; + DataTable expected = JsonConvert.DeserializeObject(json); + DataTable actual = JsonUtil.ToDataTablePreservingComplexValues(json); + AssertEqual(expected.Columns.Count, actual.Columns.Count, "scalar column count"); + AssertEqual(expected.Rows.Count, actual.Rows.Count, "scalar row count"); + foreach (DataColumn column in expected.Columns) + { + AssertEqual(column.DataType, actual.Columns[column.ColumnName].DataType, + "scalar column type " + column.ColumnName); + for (int rowIndex = 0; rowIndex < expected.Rows.Count; rowIndex++) + { + AssertEqual(expected.Rows[rowIndex][column.ColumnName], + actual.Rows[rowIndex][column.ColumnName], + "scalar value " + column.ColumnName + " row " + rowIndex); + } + } + } + + private static void AcceptsSingleObject() + { + const string json = "{\"ProductId\":\"P001\",\"meta\":{\"source\":\"web\"}}"; + DataTable table = JsonUtil.ToDataTablePreservingComplexValues(json); + AssertEqual(1, table.Rows.Count, "single object row count"); + AssertEqual("P001", table.Rows[0]["ProductId"].ToString(), "single object scalar"); + AssertEqual("{\"source\":\"web\"}", table.Rows[0]["meta"].ToString(), "single object nested JSON"); + } + + private static void RejectsInvalidTopLevelValues() + { + AssertThrows(delegate { JsonUtil.ToDataTablePreservingComplexValues(" "); }, "empty input"); + AssertThrows(delegate { JsonUtil.ToDataTablePreservingComplexValues("{"); }, "invalid JSON"); + AssertThrows(delegate { JsonUtil.ToDataTablePreservingComplexValues("[1]"); }, "non-object row"); + } + + private static void AssertThrows(Action action, string name) where TException : Exception + { + try { action(); } + catch (TException) { return; } + throw new InvalidOperationException("Expected " + typeof(TException).Name + ": " + name); + } + + private static void AssertTrue(bool value, string name) + { + if (!value) throw new InvalidOperationException("Assertion failed: " + name); + } + + private static void AssertEqual(T expected, T actual, string name) + { + if (!object.Equals(expected, actual)) + { + throw new InvalidOperationException(name + ": expected <" + expected + ">, actual <" + actual + ">"); + } + } + } +} diff --git a/插件库/Lskj.Util/JsonUtil.cs b/插件库/Lskj.Util/JsonUtil.cs index 9b8dc5e..910abc5 100644 --- a/插件库/Lskj.Util/JsonUtil.cs +++ b/插件库/Lskj.Util/JsonUtil.cs @@ -9,6 +9,7 @@ ******************************/ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; using System.Data; @@ -154,6 +155,85 @@ namespace Lskj.Util return result; } /// + /// 说明:将可能包含对象或数组字段的网页行 JSON 转换为 DataTable + /// 复杂字段所在列统一保留为字符串,纯标量列保持现有类型推断 + /// + /// 顶层对象或对象数组 JSON + /// DataTable. + public static DataTable ToDataTablePreservingComplexValues(string json) + { + if (string.IsNullOrWhiteSpace(json)) + { + throw new ArgumentException("网页行 JSON 不能为空。", "json"); + } + + JToken root = JToken.Parse(json); + JArray rows; + if (root.Type == JTokenType.Array) + { + rows = (JArray)root; + } + else if (root.Type == JTokenType.Object) + { + rows = new JArray(root); + } + else + { + throw new JsonSerializationException("网页行 JSON 的顶层必须是对象或对象数组。"); + } + + HashSet complexColumns = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (JToken rowToken in rows) + { + if (rowToken.Type != JTokenType.Object) + { + throw new JsonSerializationException("网页行 JSON 数组中的每一项都必须是对象。"); + } + + foreach (JProperty property in ((JObject)rowToken).Properties()) + { + if (property.Value.Type == JTokenType.Object || property.Value.Type == JTokenType.Array) + { + complexColumns.Add(property.Name); + } + } + } + + foreach (JObject row in rows.Children()) + { + foreach (JProperty property in row.Properties().ToList()) + { + if (!complexColumns.Contains(property.Name) + || property.Value.Type == JTokenType.Null + || property.Value.Type == JTokenType.Undefined) + { + continue; + } + + string text; + if (property.Value.Type == JTokenType.Object || property.Value.Type == JTokenType.Array) + { + text = property.Value.ToString(Formatting.None); + } + else if (property.Value.Type == JTokenType.String) + { + text = property.Value.Value(); + } + else + { + JValue scalar = property.Value as JValue; + text = scalar == null || scalar.Value == null + ? null + : Convert.ToString(scalar.Value, CultureInfo.InvariantCulture); + } + + property.Value = text == null ? JValue.CreateNull() : new JValue(text); + } + } + + return JsonConvert.DeserializeObject(rows.ToString(Formatting.None)); + } + /// /// 说明: /// 创建人:龚宇超 /// 创建日期:2017-10-23