映射 HTTP 请求入参键值对到 JSON

前言

当对接一些 HTTP API 时,如果请求参数使用 GET 传递,或者使用 POST 但是入参格式为 x-www-form-urlencoded,如果参数较多时,可能需要将内容作为对象输入或输出。

最近有一个接口对接,对方参数达到一百多个,市面上找不到合适的工具来进行解析,所以尝试自己手撸一个工具类来用。

键值对参数到 JSON 对象

分析

打开浏览器控制台,引用 jQuery 提交一个 ajax 请求,查看请求内容:

1
2
3
4
5
6
7
8
9
10
11
var obj = {
c: [[1, 2, 3], [4, 5, 6, {
d: 1,
e: 2
}]]
};

$.ajax({
url: 'https://www.baidu.com',
data: obj
});

然后查看网络窗口的请求信息,可以找到请求的查询字符串参数:

1
2
3
4
5
6
7
8
c[0][]: 1
c[0][]: 2
c[0][]: 3
c[1][]: 4
c[1][]: 5
c[1][]: 6
c[1][3][d]: 1
c[1][3][e]: 2

根据浏览器中解析规则,数据一定是一个对象,入参信息在属性中,属性内容可以是数值、字符串、对象、数组等,可以嵌套。

开始内容一定是一个属性名,后续内容如果是数组,中括号中将体现为数字,并且数组中每个值拆分为一个键值对,键以 [] 结尾。如果是对象属性,中括号中内容为属性名称。

由此,可以写出一个简单的正则表达式匹配这个规则:^(?<n1>[^\[\]]+?)(?<n2>(\[([^\[\]]+?)\])*)(?<n3>\[\])?$

其中提取项目 n1 是属性名,n2 为多层嵌套的数组下标或属性名,n3 内容用于判断该值是否为数组对象。

注意我们需要解析的是源信息,调整为 查看源 可以获取:c%5B0%5D%5B%5D=1&c%5B0%5D%5B%5D=2&c%5B0%5D%5B%5D=3&c%5B1%5D%5B%5D=4&c%5B1%5D%5B%5D=5&c%5B1%5D%5B%5D=6&c%5B1%5D%5B3%5D%5Bd%5D=1&c%5B1%5D%5B3%5D%5Be%5D=2

实现

思路为解析每个键值对内容,根据其嵌套关系设置每层属性的信息,内容保存到 ExpandoObject(其本质是一个 IDictionary<string, object>),每解析一个与上一个键值对解析到的内容进行合并。

所有键值对内容解析完成后返回,因为使用的是 ExpandoObject 对象保存信息,作为 dynamic 返回后可以方便后续使用或反序列化为 Json 对象。

解析代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
public static dynamic ToQueryObject(this string query)
{
if (query == null || string.IsNullOrWhiteSpace(query)) return null;

IDictionary<string, object> obj = null;
var collection = HttpUtility.ParseQueryString(query);
for (int i = 0; i < collection.Keys.Count; i++)
{
// 偷懒的写法,解析后再合并,相对容易理解
var key = collection.Keys[i];
var values = collection.GetValues(key);
obj = obj.Combine(GetDictionary(key, values));
}

return obj;
}

private static IDictionary<string, object> GetDictionary(string key, string[] values)
{
if (key == null)
{
throw new ArgumentNullException(nameof(key));
}

// 用于匹配属性或数组的正则
var regex = new Regex(@"^(?<n1>[^\[\]]+?)(?<n2>(\[([^\[\]]+?)\])*)(?<n3>\[\])?$");
var match = regex.Match(key);

if (!match.Success)
{
throw new Exception($"Property \"{key}\" failed to match.");
}
else
{
// 解析 Key 内容
var firstKey = match.Result("${n1}");
var lastKey = match.Result("${n2}");
bool isAarry = match.Result("${n3}") == "[]";

if (!isAarry && values.Length > 1)
{
throw new Exception($"Property \"{key}\" value are not allowed to be arrays.");
}
else
{
object value = isAarry ? new List<object>(values) : (values.Length > 0 && !string.IsNullOrEmpty(values[0]) ? values[0] : null);

// 属性路径上所有的内容
var keys = new List<string> { firstKey };
if (!string.IsNullOrEmpty(lastKey))
{
keys.AddRange(lastKey.Trim('[', ']').Split("]["));
}

// 由内向外迭代赋值
for (int i = keys.Count - 1; i >= 0; i--)
{
if (i != 0 && int.TryParse(keys[i], out var index))
{
// 如果是数字则说明是索引
var list = new List<object>(new object[index + 1]);
list[index] = value;
value = list;
}
else
{
// 否则说明是属性
IDictionary<string, object> dict = new ExpandoObject();
dict.TryAdd(keys[i], value);
value = dict;
}
}

return value as IDictionary<string, object>;
}
}
}

private static IDictionary<string, object> Combine(this IDictionary<string, object> target, IDictionary<string, object> from)
{
if (target == null)
{
target = from;
}
else if (target != null && from != null)
{
foreach (var fromItem in from)
{
if (fromItem.Value == null) continue;
if (target.TryGetValue(fromItem.Key, out object targetValue) && targetValue != null)
{
if (targetValue is List<object> targetList && fromItem.Value is List<object> fromList)
{
// 合并集合
var list = targetList.Combine(fromList);
target[fromItem.Key] = list;
}
else if (targetValue is IDictionary<string, object> targetDict && fromItem.Value is IDictionary<string, object> fromDict)
{
// 合并字典
var dict = targetDict.Combine(fromDict);
target[fromItem.Key] = dict;
}
else
{
throw new Exception($"Type \"{targetValue.GetType()}\" and type \"{fromItem.Value.GetType()}\" cannot be merged.");
}
}
else
{
target[fromItem.Key] = fromItem.Value;
}
}
}

return target;
}

private static List<object> Combine(this List<object> target, List<object> from)
{
if (target == null)
{
target = from;
}
else if (target != null && from != null)
{
for (int i = 0; i < Math.Max(target.Count, from.Count); i++)
{
var targetItem = target.Count > i ? target[i] : null;
var fromItem = from.Count > i ? from[i] : null;
if (targetItem == null || fromItem == null)
{
var value = targetItem ?? fromItem;
if (target.Count > i) target[i] = value;
else target.Add(value);
}
else if (targetItem is List<object> targetList && fromItem is List<object> fromList)
{
// 合并集合
var list = targetList.Combine(fromList);
if (target.Count > i) target[i] = list;
else target.Add(list);
}
else if (targetItem is IDictionary<string, object> targetDict && fromItem is IDictionary<string, object> fromDict)
{
// 合并字典
var dict = targetDict.Combine(fromDict);
if (target.Count > i) target[i] = dict;
else target.Add(dict);
}
else
{
throw new Exception($"Type \"{targetItem.GetType()}\" and type \"{fromItem.GetType()}\" cannot be merged.");
}
}
}

return target;
}

测试

参考文章开头请求的入参,进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var query = "c%5B0%5D%5B%5D=1&c%5B0%5D%5B%5D=2&c%5B0%5D%5B%5D=3&c%5B1%5D%5B%5D=4&c%5B1%5D%5B%5D=5&c%5B1%5D%5B%5D=6&c%5B1%5D%5B3%5D%5Bd%5D=1&c%5B1%5D%5B3%5D%5Be%5D=2";
var obj = query.ToQueryObject();
var json = JsonConvert.SerializeObject(obj, Formatting.Indented);

// 输出内容为:
// {
// "c": [
// [
// "1",
// "2",
// "3"
// ],
// [
// "4",
// "5",
// "6",
// {
// "d": "1",
// "e": "2"
// }
// ]
// ]
// }

JSON 对象到键值对参数

分析

同样的,我们解析后的对象,在填值以后,在请求 HTTP API 后需要按照以上键值对的格式输出。

这时我们需要使用 Newtonsoft.Json 中的 JToken 将对象进行解析。这里有个好处,在解析时如果对象属性设置了别名,会自动将键中的属性名映射为别名。

实现

实现思路就是递归,逐层获取属性或数组内容,判断当内容为 JObjectJArray 继续获取成员值,并将上级内容作为前缀内容拼接作为参数传递。

当递归到所属层级为 JValue 时,拼接前缀内容进行返回,需要注意的是数组内容要根据下级内容是否为 JValue 判断中括号中内容是否应该给数组下标。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public static string ToQueryString<T>(this T @this) where T : class
{
var queries = ToQueryString("", JToken.FromObject(@this));
return queries.Any() ? $"?{string.Join("&", queries)}" : "";
}

private static IEnumerable<string> ToQueryString(string prefix, JToken token)
{
if (token is JObject obj)
{
foreach (var property in obj.Properties())
{
var subToken = obj[property.Name];
var name = string.IsNullOrEmpty(prefix) ? property.Name : $"{prefix}[{property.Name}]";
foreach (var item in ToQueryString(name, subToken))
{
yield return item;
}
}
}
else if (!string.IsNullOrEmpty(prefix))
{
if (token is JValue value)
{
yield return $"{Uri.EscapeDataString(prefix)}={Uri.EscapeDataString(value.ToString())}";
}
else if (token is JArray array)
{
for (int i = 0; i < array.Count; i++)
{
var subToken = array[i];
var name = $"{prefix + (subToken is JValue ? "[]" : $"[{i}]")}";
foreach (var item in ToQueryString(name, array[i]))
{
yield return item;
}
}
}
}
}

测试

同样,使用前文中对象测试输出是否与浏览器一致:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var obj = new
{
c = new object[]
{
new[] { 1, 2, 3, },
new object[]
{
4, 5, 6,
new
{
d = 1,
e = 2
}
}
}
};
var query = obj.ToQueryString();

// 输出内容为:
// ?c%5B0%5D%5B%5D=1&c%5B0%5D%5B%5D=2&c%5B0%5D%5B%5D=3&c%5B1%5D%5B%5D=4&c%5B1%5D%5B%5D=5&c%5B1%5D%5B%5D=6&c%5B1%5D%5B3%5D%5Bd%5D=1&c%5B1%5D%5B3%5D%5Be%5D=2

测试属性名设置特性后的输出效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Person
{
[JsonProperty("first_name")]
public string FirstName { get; set; }

[JsonProperty("last_name")]
public string LastName { get; set; }

[JsonProperty("full_name")]
public string FullName { get; set; }

[JsonProperty("age")]
public int? Age { get; set; }
}

var obj = new Person
{
FirstName = "John",
LastName = "Sun",
FullName = "John Sun",
Age = 3
};
var query = obj.ToQueryString();

// 输出内容为:
// ?first_name=John&last_name=Sun&full_name=John%20Sun&age=3

映射 JSON 文件的实体类

前言

想要使用接口返回的内容,直接生成实体类,用于后期开发。

如果简单使用可以参考:http://tools.jb51.net/code/json2csharp/

但是因为其生成的属性名不规范,以及对于特殊属性名例如 Fflag.appType.brands 没有做好处理,所以自己写了一个工具类。

实现代码

首先需要引用 Newtonsoft.Jsonjson 文件的解析,以及生成实体的内容都依赖这个包。

另外为了处理单复数问题,还引用了 PluralizeService.Core

工具类代码内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PluralizeService.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace Utils
{
/// <summary>
/// Json 工具类
/// </summary>
public static class JsonUtil
{
/// <summary>
/// 将 Json 内容转换为 C# 实体类
/// </summary>
/// <param name="json">json 内容</param>
/// <returns>转化后的实体类内容</returns>
public static string ToEntityClass(string json)
{
var obj = JsonConvert.DeserializeObject(json);

var entities = new Dictionary<string, EntityInfo>();
MapEntityInfo("BaseEntity", entities, obj as JToken);

var content = new StringBuilder();

foreach (var entity in entities)
{
// 开始
content.AppendLine($"public class {PluralizationProvider.Singularize(GetPascalName(entity.Key))}");
content.AppendLine("{");

foreach (var property in entity.Value.Properties)
{
content.AppendLine($" [JsonProperty(\"{property.Key}\")]");
content.AppendLine($" public {GetTypeName(property.Value)} {GetPascalName(property.Key)} {{ get; set; }}");
content.AppendLine();
}

// 结束
content.AppendLine("}");
content.AppendLine();
}

return content.ToString();
}

private static string GetTypeName(PropertyInfo property)
{
string name = property.Type;
var type = Type.GetType(name, false);
var typeNameMap = new TypeNameMap();
return $"{(property.IsArray ? "List<" : "")}{(type != null ? (typeNameMap[type.Name] ?? type.Name) : PluralizationProvider.Singularize(GetPascalName(name)))}{(property.Nullable && type != null && type.IsSubclassOf(typeof(ValueType)) ? "?" : "")}{(property.IsArray ? ">" : "")}";
}

private static string GetPascalName(string name)
{

if (string.IsNullOrEmpty(name)) return string.Empty;

// 匹配 特殊字符+小写字母
var regex = new Regex("[^0-9a-zA-Z]+([0-9a-zA-Z]{1})");
var matches = regex.Matches(name);
foreach (Match match in matches)
{
name = name.Replace(match.Value, match.Result("$1").ToUpper());
}

// 移除非数字、字母的内容
name = new Regex("[^0-9a-zA-Z]+").Replace(name, string.Empty);

// 首字母转大写
if (name.Length > 0) name = string.Concat(name.Substring(0, 1).ToUpper(), name.Substring(1));

return name;
}

private static EntityInfo MapEntityInfo(string name, Dictionary<string, EntityInfo> entities, JToken token)
{
// 获取实体信息
if (!entities.TryGetValue(name, out EntityInfo entity))
{
entity = new EntityInfo();
entities.TryAdd(name, entity);
}

// 实体的属性信息
var properties = entity.Properties;

if (token is JObject obj)
{
// 内容是对象时,需要获取属性信息
foreach (var kv in obj)
{
string key = kv.Key;
var value = kv.Value;
if (!properties.TryGetValue(key, out PropertyInfo property))
{
property = new PropertyInfo();
property.Name = key;
properties.TryAdd(key, property);
}

if (value != null)
{
if (value is JValue jValue)
{
switch (jValue.Type)
{
case JTokenType.Integer:
property.Type = typeof(long).FullName;
break;
case JTokenType.Float:
property.Type = typeof(decimal).FullName;
break;
case JTokenType.Null:
case JTokenType.String:
property.Type = typeof(string).FullName;
break;
case JTokenType.Boolean:
property.Type = typeof(bool).FullName;
break;
case JTokenType.Date:
property.Type = typeof(DateTime).FullName;
break;
case JTokenType.Guid:
property.Type = typeof(Guid).FullName;
break;
case JTokenType.Uri:
property.Type = typeof(Uri).FullName;
break;
case JTokenType.TimeSpan:
property.Type = typeof(TimeSpan).FullName;
break;
default:
throw new Exception("解析数值出现未知的数据类型!");
}
}
else if (value is JArray jArray)
{
property.IsArray = true;
property.Nullable = jArray.Any(a => a.Type == JTokenType.Null);
if (jArray.Count > 0 && jArray.Any(a => a.Type != JTokenType.Null))
{
var firstType = jArray.First(a => a.Type != JTokenType.Null).Type;
if (jArray.Any(a => a.Type != firstType && a.Type != JTokenType.Null))
{
throw new Exception("解析数组成员类型不唯一!");
}

switch (firstType)
{
case JTokenType.Object:
property.Type = key;
foreach (var item in jArray.Where(a => a.Type == JTokenType.Object))
{
MapEntityInfo(key, entities, item);
}
break;
case JTokenType.Integer:
property.Type = typeof(long).FullName;
break;
case JTokenType.Float:
property.Type = typeof(decimal).FullName;
break;
case JTokenType.String:
property.Type = typeof(string).FullName;
break;
case JTokenType.Boolean:
property.Type = typeof(bool).FullName;
break;
case JTokenType.Date:
property.Type = typeof(DateTime).FullName;
break;
case JTokenType.Guid:
property.Type = typeof(Guid).FullName;
break;
case JTokenType.Uri:
property.Type = typeof(Uri).FullName;
break;
case JTokenType.TimeSpan:
property.Type = typeof(TimeSpan).FullName;
break;
default:
throw new Exception("解析数组出现未知的数据类型!");
}
}
}
else
{
property.Type = key;
MapEntityInfo(key, entities, value);
}
}
else
{
property.Nullable = true;
}
}
}

return entity;
}
}

class EntityInfo
{
public string Name { get; set; }

public Dictionary<string, PropertyInfo> Properties { get; set; } = new Dictionary<string, PropertyInfo>();
}

class PropertyInfo
{
public string Name { get; set; }

public string Type { get; set; } = typeof(string).FullName;

public bool IsArray { get; set; }

public bool Nullable { get; set; }
}

class TypeNameMap
{
private static readonly Dictionary<string, string> typeNameMap = new()
{
[typeof(bool).Name] = "bool",
[typeof(byte).Name] = "byte",
[typeof(sbyte).Name] = "sbyte",
[typeof(short).Name] = "short",
[typeof(ushort).Name] = "ushort",
[typeof(int).Name] = "int",
[typeof(uint).Name] = "uint",
[typeof(long).Name] = "long",
[typeof(ulong).Name] = "ulong",
[typeof(float).Name] = "float",
[typeof(double).Name] = "double",
[typeof(decimal).Name] = "decimal",
[typeof(string).Name] = "string",
};

internal string this[string name]
{
get
{
typeNameMap.TryGetValue(name, out string value);
return value;
}
}
}
}

测试

测试 json 内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
{
"ProductInfoList": [
{
"Sku": "58396277",
"CategoryFirstCode": "4841",
"CategoryFirstName": "家居和园艺",
"CategoryFirstNameEN": "Home & Garden",
"CategorySecondCode": "48410020",
"CategorySecondName": "家装五金",
"CategorySecondNameEN": "Home Improvement",
"CategoryThirdCode": "484100206798",
"CategoryThirdName": "家庭安全",
"CategoryThirdNameEN": "Home Security",
"CnName": "英文字典迷你保险柜存钱盒大号钥匙款 蓝色 27*20*6.5CM",
"EnName": "Cute Simulation English Dictionary Style Mini Safety Storage Box Blue L",
"SpecLength": 27.50,
"SpecWidth": 21.00,
"SpecHeight": 7.50,
"SpecWeight": 1120.00,
"Published": false,
"IsClear": false,
"FreightAttrCode": "X0018",
"FreightAttrName": "普货",
"PlatformCommodityCode": "C13145463",
"CommodityWarehouseMode": 0,
"GoodsImageList": [
{
"ImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/202012081550097585_f535c1c5-5bea-4a83-8c43-00f325527a00.JPG",
"ThumbnailUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/202012081550102897_18e38ad6-5f9e-46b6-ad67-f4847aa0c705.JPG",
"Sort": 0,
"IsMainImage": true,
"GooodsThumbnailList": [
{
"StandardLength": 350,
"StandardWidth": 350,
"StandardImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/dcda886b-72c1-41d8-8f03-f712c1ab233f.JPG"
},
{
"StandardLength": 600,
"StandardWidth": 600,
"StandardImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/ada5247f-9d95-46d7-8223-cc3ae733a89b.JPG"
}
]
},
{
"ImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/202012081550103366_12dbbb9d-0bc0-4154-a206-0f37a4011f23.JPG",
"ThumbnailUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/202012081550109616_b993bbff-68e4-4b4d-8bba-fadcc07a1194.JPG",
"Sort": 1,
"IsMainImage": false,
"GooodsThumbnailList": [
{
"StandardLength": 350,
"StandardWidth": 350,
"StandardImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/6f7fe5e7-6935-4062-8828-52bdc74feb01.JPG"
},
{
"StandardLength": 600,
"StandardWidth": 600,
"StandardImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/bccf0963-319d-469e-b12b-541888df755a.JPG"
}
]
},
{
"ImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/202012081550110241_087e4b65-314e-4035-9eb3-5970d6192211.JPG",
"ThumbnailUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/202012081550116960_a2932fc9-5fc2-4d6f-9552-4421ea6e79ff.JPG",
"Sort": 2,
"IsMainImage": false,
"GooodsThumbnailList": [
{
"StandardLength": 350,
"StandardWidth": 350,
"StandardImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/2ca3f29f-775f-4b24-86b2-88bd778f11b1.JPG"
},
{
"StandardLength": 600,
"StandardWidth": 600,
"StandardImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/ce3fd469-b08e-4364-a185-d13245b5ee57.JPG"
}
]
},
{
"ImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/202012081550117429_636348d8-aca3-4628-89c8-05aaf63be259.JPG",
"ThumbnailUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/202012081550121022_2dcdc4c0-bcc3-412b-b242-450839220052.JPG",
"Sort": 3,
"IsMainImage": false,
"GooodsThumbnailList": [
{
"StandardLength": 350,
"StandardWidth": 350,
"StandardImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/199bb9ce-9a48-4493-8d1d-a591e7c56cf7.JPG"
},
{
"StandardLength": 600,
"StandardWidth": 600,
"StandardImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/54ed6d08-8b9f-4b9c-89ae-5f6c19e25c78.JPG"
}
]
},
{
"ImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/202012081550121491_8198fac2-13da-48a6-8286-afde185c7fba.JPG",
"ThumbnailUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/202012081550127429_7153e854-d5f4-4ab2-a868-e1b6b22a480a.JPG",
"Sort": 4,
"IsMainImage": false,
"GooodsThumbnailList": [
{
"StandardLength": 350,
"StandardWidth": 350,
"StandardImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/af946cc4-2e2a-4bfb-81e3-8ee4e0334d4f.JPG"
},
{
"StandardLength": 600,
"StandardWidth": 600,
"StandardImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/c86dd714-0dab-477b-ab20-7e7246ee6003.JPG"
}
]
},
{
"ImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/202012081550128210_85dccec4-ebb7-48dd-b673-ffa9ed0ba5d0.JPG",
"ThumbnailUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/202012081550131647_eadb06be-0400-46fb-b7c6-f104120c61e0.JPG",
"Sort": 5,
"IsMainImage": false,
"GooodsThumbnailList": [
{
"StandardLength": 350,
"StandardWidth": 350,
"StandardImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/b2f9779f-c143-45a6-ac7c-a57170f1ebc6.JPG"
},
{
"StandardLength": 600,
"StandardWidth": 600,
"StandardImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/c79e35bb-7ca5-4527-b60f-523a2990b863.JPG"
}
]
},
{
"ImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/202012081550132116_da004c50-bb32-4a7f-bcc6-9e431d21caf8.JPG",
"ThumbnailUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/202012081550138678_169e8bc0-7f67-4c8e-88e3-6dfde45890e1.JPG",
"Sort": 6,
"IsMainImage": false,
"GooodsThumbnailList": [
{
"StandardLength": 350,
"StandardWidth": 350,
"StandardImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/521e7c4e-68fb-4de7-ba6b-4b9029c7c160.JPG"
},
{
"StandardLength": 600,
"StandardWidth": 600,
"StandardImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/18172593-beda-478c-b455-295672e1b3a4.JPG"
}
]
},
{
"ImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/202012081550139147_a80119d9-24f8-4da4-abc1-fbf4590090b8.JPG",
"ThumbnailUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/202012081550142428_545ff06e-2db3-4988-b2b9-955d577b7449.JPG",
"Sort": 7,
"IsMainImage": false,
"GooodsThumbnailList": [
{
"StandardLength": 350,
"StandardWidth": 350,
"StandardImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/ec9b4d9d-1fbf-4062-bad2-8f5c304affba.JPG"
},
{
"StandardLength": 600,
"StandardWidth": 600,
"StandardImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/ea0690d1-9c53-4cdd-a912-aea15509689e.JPG"
}
]
},
{
"ImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/202012081550142897_eae670d9-9615-4e41-823a-7f1170c7283e.JPG",
"ThumbnailUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/202012081550149616_7b451919-ee54-4ea2-a6db-32b7bb179317.JPG",
"Sort": 8,
"IsMainImage": false,
"GooodsThumbnailList": [
{
"StandardLength": 350,
"StandardWidth": 350,
"StandardImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/045580ad-87fc-4be8-ab4a-c60cad68ee64.JPG"
},
{
"StandardLength": 600,
"StandardWidth": 600,
"StandardImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/82412afb-0f76-47e0-bf60-82e4b85732ec.JPG"
}
]
},
{
"ImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/202012081550150397_226c7997-6b93-4f82-a7ae-ceeb40076dce.JPG",
"ThumbnailUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/202012081550157741_ea1c450b-5984-45d0-b126-43096e220325.JPG",
"Sort": 9,
"IsMainImage": false,
"GooodsThumbnailList": [
{
"StandardLength": 350,
"StandardWidth": 350,
"StandardImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/eaac56d0-1a00-4d2c-9950-0d6249031bd4.JPG"
},
{
"StandardLength": 600,
"StandardWidth": 600,
"StandardImageUrl": "https://img.goten.com/Resources/GoodsImages//2020/202012/dd6b6757-0dc5-41f3-995b-40dcb4775fbe.JPG"
}
]
}
],
"GoodsAttachmentList": [],
"GoodsDescriptionList": [
{
"Title": "Cute Simulation English Dictionary Style Mini Safety Storage Box Blue L",
"GoodsDescriptionKeywordList": [
{
"KeyWord": "Safety Box"
}
],
"GoodsDescriptionLabelList": [
{
"LabelName": "英文"
}
],
"GoodsDescriptionParagraphList": [
{
"ParagraphName": "首段描述",
"SortNo": 1,
"GoodsDescription": "<p><STRONG>Introductions:</STRONG><BR>Fantastic dictionary look makes this storage box rather distinctive from ordinary ones. Due to this feature, it can well protect your secrets without being found easily. Made with high-class material as well exquisite craftsmanship, this storage box is wear-resistant for long-term use. With quite spacious space, it allows you to put important things inside. Close and then place it on the book shelf, it seems to be a real English dictionary. Such a mini gadget can also be a good decoration for your room. Well, wanna give it a try? Just click to buy our Cute Simulation English Dictionary Style Mini Safety Storage Box!<BR></p>"
},
{
"ParagraphName": "特征",
"SortNo": 2,
"GoodsDescription": "<p><STRONG>Features:</STRONG><BR>1. Specially designed into English dictionary style, quite mini and cute<BR>2. Great for deceiving the public so as to keep your belongings safe and unnoticed<BR>3. Made with first-rate material and exquisite workmanship, of great durability and reliability<BR>4. Compact size, lightweight and room-saving<BR>5. Hollow dictionary, can be used for storing important gadgets<BR>6. Also a nice decoration for room fitment&nbsp;&nbsp;<BR></p>"
},
{
"ParagraphName": "规格",
"SortNo": 3,
"GoodsDescription": "<p><P><STRONG>Specifications:</STRONG><BR>1. Material: Iron &amp; ABS &amp; Specialty Paper/Imitation Cloth<BR>2. Color: Blue<BR>3. Dimensions: (10.62 x 7.87 x 2.56)\" / (27 x 20 x 6.5)cm (L x W x H)<BR>4. Weight: 35.27oz / 1000g</P>\n<P>&nbsp;</P></p>"
},
{
"ParagraphName": "包装内含",
"SortNo": 4,
"GoodsDescription": "<p><STRONG>Package Includes:</STRONG><BR>1 x English Dictionary Mini Safety Box</p>"
},
{
"ParagraphName": "通用性",
"SortNo": 5,
"GoodsDescription": "<p></p>"
},
{
"ParagraphName": "附加信息",
"SortNo": 6,
"GoodsDescription": "<p></p>"
}
]
}
],
"CreateTime": "2018-03-12T12:31:20",
"UpdateTime": "2021-03-01T03:20:20",
"TortInfo": {
"TortReasons": null,
"TortTypes": null,
"TortStatus": 2
},
"IsProductAuth": true,
"ProductSiteModelList": [
{
"SiteHosts": "www.gotenchina.com",
"WarehouseCodeList": [
"SZ0001"
]
}
],
"BrandName": null
}
],
"PageIndex": 1,
"TotalCount": 6656,
"PageTotal": 666,
"PageSize": 10
}

生成的实体类内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
public class BaseEntity
{
[JsonProperty("ProductInfoList")]
public List<ProductInfoList> ProductInfoList { get; set; }

[JsonProperty("PageIndex")]
public long PageIndex { get; set; }

[JsonProperty("TotalCount")]
public long TotalCount { get; set; }

[JsonProperty("PageTotal")]
public long PageTotal { get; set; }

[JsonProperty("PageSize")]
public long PageSize { get; set; }

}

public class ProductInfoList
{
[JsonProperty("Sku")]
public string Sku { get; set; }

[JsonProperty("CategoryFirstCode")]
public string CategoryFirstCode { get; set; }

[JsonProperty("CategoryFirstName")]
public string CategoryFirstName { get; set; }

[JsonProperty("CategoryFirstNameEN")]
public string CategoryFirstNameEN { get; set; }

[JsonProperty("CategorySecondCode")]
public string CategorySecondCode { get; set; }

[JsonProperty("CategorySecondName")]
public string CategorySecondName { get; set; }

[JsonProperty("CategorySecondNameEN")]
public string CategorySecondNameEN { get; set; }

[JsonProperty("CategoryThirdCode")]
public string CategoryThirdCode { get; set; }

[JsonProperty("CategoryThirdName")]
public string CategoryThirdName { get; set; }

[JsonProperty("CategoryThirdNameEN")]
public string CategoryThirdNameEN { get; set; }

[JsonProperty("CnName")]
public string CnName { get; set; }

[JsonProperty("EnName")]
public string EnName { get; set; }

[JsonProperty("SpecLength")]
public decimal SpecLength { get; set; }

[JsonProperty("SpecWidth")]
public decimal SpecWidth { get; set; }

[JsonProperty("SpecHeight")]
public decimal SpecHeight { get; set; }

[JsonProperty("SpecWeight")]
public decimal SpecWeight { get; set; }

[JsonProperty("Published")]
public bool Published { get; set; }

[JsonProperty("IsClear")]
public bool IsClear { get; set; }

[JsonProperty("FreightAttrCode")]
public string FreightAttrCode { get; set; }

[JsonProperty("FreightAttrName")]
public string FreightAttrName { get; set; }

[JsonProperty("PlatformCommodityCode")]
public string PlatformCommodityCode { get; set; }

[JsonProperty("CommodityWarehouseMode")]
public long CommodityWarehouseMode { get; set; }

[JsonProperty("GoodsImageList")]
public List<GoodsImageList> GoodsImageList { get; set; }

[JsonProperty("GoodsAttachmentList")]
public List<string> GoodsAttachmentList { get; set; }

[JsonProperty("GoodsDescriptionList")]
public List<GoodsDescriptionList> GoodsDescriptionList { get; set; }

[JsonProperty("CreateTime")]
public DateTime CreateTime { get; set; }

[JsonProperty("UpdateTime")]
public DateTime UpdateTime { get; set; }

[JsonProperty("TortInfo")]
public TortInfo TortInfo { get; set; }

[JsonProperty("IsProductAuth")]
public bool IsProductAuth { get; set; }

[JsonProperty("ProductSiteModelList")]
public List<ProductSiteModelList> ProductSiteModelList { get; set; }

[JsonProperty("BrandName")]
public string BrandName { get; set; }

}

public class GoodsImageList
{
[JsonProperty("ImageUrl")]
public string ImageUrl { get; set; }

[JsonProperty("ThumbnailUrl")]
public string ThumbnailUrl { get; set; }

[JsonProperty("Sort")]
public long Sort { get; set; }

[JsonProperty("IsMainImage")]
public bool IsMainImage { get; set; }

[JsonProperty("GooodsThumbnailList")]
public List<GooodsThumbnailList> GooodsThumbnailList { get; set; }

}

public class GooodsThumbnailList
{
[JsonProperty("StandardLength")]
public long StandardLength { get; set; }

[JsonProperty("StandardWidth")]
public long StandardWidth { get; set; }

[JsonProperty("StandardImageUrl")]
public string StandardImageUrl { get; set; }

}

public class GoodsDescriptionList
{
[JsonProperty("Title")]
public string Title { get; set; }

[JsonProperty("GoodsDescriptionKeywordList")]
public List<GoodsDescriptionKeywordList> GoodsDescriptionKeywordList { get; set; }

[JsonProperty("GoodsDescriptionLabelList")]
public List<GoodsDescriptionLabelList> GoodsDescriptionLabelList { get; set; }

[JsonProperty("GoodsDescriptionParagraphList")]
public List<GoodsDescriptionParagraphList> GoodsDescriptionParagraphList { get; set; }

}

public class GoodsDescriptionKeywordList
{
[JsonProperty("KeyWord")]
public string KeyWord { get; set; }

}

public class GoodsDescriptionLabelList
{
[JsonProperty("LabelName")]
public string LabelName { get; set; }

}

public class GoodsDescriptionParagraphList
{
[JsonProperty("ParagraphName")]
public string ParagraphName { get; set; }

[JsonProperty("SortNo")]
public long SortNo { get; set; }

[JsonProperty("GoodsDescription")]
public string GoodsDescription { get; set; }

}

public class TortInfo
{
[JsonProperty("TortReasons")]
public string TortReasons { get; set; }

[JsonProperty("TortTypes")]
public string TortTypes { get; set; }

[JsonProperty("TortStatus")]
public long TortStatus { get; set; }

}

public class ProductSiteModelList
{
[JsonProperty("SiteHosts")]
public string SiteHosts { get; set; }

[JsonProperty("WarehouseCodeList")]
public List<string> WarehouseCodeList { get; set; }

}

部署企业知识库 Confluence

一、Docker 部署

Confluence Docker Hub:https://hub.docker.com/r/cptactionhank/atlassian-confluence

1
2
3
# 创建容器
# 映射端口如果被占用可以调整例如 18090:8090
docker run -d --name confluence -p 8090:8090 --user root:root cptactionhank/atlassian-confluence:latest

二、安装过程

以下过程是在 Windows 下操作,将 Confluence 部署到 CentOS,需要拷贝文件或编辑文件,建议使用 finalshell 工具连接创建 SSH 连接。

1. 数据库

准备一个数据库,MySQL、PostgreSQL 等数据库均可,这里以 MySQL 为例,安装过程略。

首先需要调整 InnoDB 日志文件大小,Confluence 建议日志文件大小再 256M 以上。

在 Linux 下设置过程如下:

1
2
3
4
5
6
7
8
9
10
11
# 暂停 MySQL 服务
service mysqld stop

# 删除旧的日志文件
rm -f /var/lib/mysql/ib_logfile*

# 编辑 my.cnf 调整 innodb_log_file_size=256M
vim /etc/my.conf

# 重新启动 MySQL
service mysqld start

然后就是创建数据库以及授权:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 创建数据库
CREATE DATABASE `confluence` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;

-- 创建用户
CREATE USER 'confluence'@'%' IDENTIFIED BY 'password';

-- 授权
GRANT ALL ON confluence.* TO 'confluence'@'%';

-- 登录 root 用户更改事务隔离级别
-- MySQL 8.x
SET GLOBAL transaction_isolation='READ-COMMITTED';
-- MySQL 5.x
SET GLOBAL tx_isolation='READ-COMMITTED';

2. 代理配置

首先调整 Confluence 的 Tomcat 配置,默认使用 HTTPS 代理访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 复制配置文件
docker cp confluence:/opt/atlassian/confluence/conf/server.xml /opt/

# 编辑配置文件 Connector 节点,修改为示例 HTTPS 代理部分的内容、
#
# <Connector port="8090" connectionTimeout="20000" redirectPort="8443"
# maxThreads="48" minSpareThreads="10"
# enableLookups="false" acceptCount="10" debug="0" URIEncoding="UTF-8"
# protocol="org.apache.coyote.http11.Http11NioProtocol"
# scheme="https" secure="true" proxyName="<subdomain>.<domain>.com" proxyPort="443"/>
#
# 注意需要修改 proxyName 属性为自己的域名

# 将配置文件覆盖回去
docker cp /opt/server.xml confluence:/opt/atlassian/confluence/conf/server.xml

这里使用 Caddy 进行代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 安装 Caddy 软件包
yum install caddy -y

# 使用 vim 编辑 Caddyfile
vim /etc/caddy/conf.d/Caddyfile.conf

# 修改配置文件,代理指定接口并且 HTTP 访问自动跳转到 HTTPS
#
# https://<subdomain>.<domain>.com {
# gzip
# tls <user>@<emaildomain>.com
# proxy / localhost:18090 {
# transparent
# }
# }
#
# http://<subdomain>.<domain>.com {
# redir https://<subdomain>.<domain>.com{url}
# }
#
# 注意需要调整域名信息以及邮箱,邮箱用于自动是申请域名证书

# 开启自启 Caddy 服务
systemctl enable caddy

# 启动 Caddy
service caddy start

# 停止运行 Caddy
service caddy stop

# 重启 Caddy
service caddy restart

# 查看 Caddy 运行状态
service caddy status

3. 安装及破解

访问 https://<subdomain>.<domain>.com 执行以下安装流程。

安装页面可以调整语言,建议修改为中文。

  1. 设置 Confluence 选择 产品安装
  2. 选择功能 根据需求选择。
  3. 授权码 过程见下方破解流程。
    • 下载授权工具:https://pan.xunlei.com/s/VMSNMp0MczvkJfcam4CAxLXlA1 提取码:zrv6
    • 复制 jar 包:docker cp confluence:/opt/atlassian/confluence/confluence/WEB-INF/lib/atlassian-extras-decoder-v2-3.4.1.jar /opt/
    • 打开授权工具:java -jar confluence_keygen.jar
    • 将授权页面的 Server ID 拷贝到授权工具中,并补充必填信息后点击 .gen! 按钮生成 Key。
    • 将从容器中拷贝下来的 atlassian-extras-decoder-v2-3.4.1.jar 更名为 atlassian-extras-2.4.jar,然后点击 .patch! 进行破解。
    • 当显示 Jar successfully patched. 表示破解成功,将破解后的文件名改回 atlassian-extras-decoder-v2-3.4.1.jar
    • 将文件拷贝回容器中覆盖原有文件:docker cp /opt/atlassian-extras-decoder-v2-3.4.1.jar confluence:/opt/atlassian/confluence/confluence/WEB-INF/lib/atlassian-extras-decoder-v2-3.4.1.jar
    • 刷新页面,将授权工具生成的 Key 填入并继续。
  4. 配置数据库 选择 我自己的数据库
  5. 设置你的数据库 填写自己的数据库信息及密码,填写以后等待安装完成即可。

注意: 使用授权工具需要安装 Java。

三、常用目录

  • 产品开发
    • 001-团队大纲
    • 002-团队成员
    • 003-新人指南
    • 004-技术架构
    • 005-会议纪要
    • 006-团队规范
    • 007-踩坑历程
    • 008-业务梳理
    • 009-部门团建
    • 010-团队分享
    • 011-业务小组
    • 012-团队总结

参考:

使用 Fiddler 调试 WebService 服务

前言

近期一个老客户的古董级数据上传工具出现了问题,反映一个通过 WebService 进行数据通信的工具无法正常使用。

因为通信接口是旧版的,源码不太好找,并且该通信接口 WebService 服务地址配置后无法生效,不能进行本地调试。

为了尽快的锁定问题,考虑用 HTTP 转发的方式来排查问题产生原因,以下是问题处理的流程,供以后处理类似问题时参考。

锁定问题原因

首先,我们需要出现问题的节点,通过日志文件可以看到错误提示中存在两个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
由于目标计算机积极拒绝,无法连接。 127.0.0.1:3349
System.Web.Services.Protocols.SoapException: 服务器无法处理请求。 ---> System.NullReferenceException: 未将对象引用设置到对象的实例。
在 YunHuLIS.MacWS.MacInfo.SetItemSample(String instrument, String itemCode, String result, String seq, String barcode, DateTime datetime)
--- 内部异常堆栈跟踪的结尾 ---
System.Web.Services.Protocols.SoapException: 服务器无法处理请求。 ---> System.NullReferenceException: 未将对象引用设置到对象的实例。
在 YunHuLIS.MacWS.MacInfo.SetItemSample(String instrument, String itemCode, String result, String seq, String barcode, DateTime datetime)
--- 内部异常堆栈跟踪的结尾 ---

由于目标计算机积极拒绝,无法连接。 127.0.0.1:3349
System.Web.Services.Protocols.SoapException: 服务器无法处理请求。 ---> System.NullReferenceException: 未将对象引用设置到对象的实例。
在 YunHuLIS.MacWS.MacInfo.SetItemSample(String instrument, String itemCode, String result, String seq, String barcode, DateTime datetime)
--- 内部异常堆栈跟踪的结尾 ---

由于目标计算机积极拒绝,无法连接。 127.0.0.1:3349
System.Web.Services.Protocols.SoapException: 服务器无法处理请求。 ---> System.NullReferenceException: 未将对象引用设置到对象的实例。
在 YunHuLIS.MacWS.MacInfo.SetItemSample(String instrument, String itemCode, String result, String seq, String barcode, DateTime datetime)
--- 内部异常堆栈跟踪的结尾 ---
System.Web.Services.Protocols.SoapException: 服务器无法处理请求。 ---> System.NullReferenceException: 未将对象引用设置到对象的实例。
在 YunHuLIS.MacWS.MacInfo.SetItemSample(String instrument, String itemCode, String result, String seq, String barcode, DateTime datetime)
--- 内部异常堆栈跟踪的结尾 ---

首先是使用 Socket 与仪器无法连接,其次 WebService 也有异常抛出。

经检查确认仪器的传输是否正常,网口是否能正常监听,确认抛出异常的部分日志,可能是当时仪器软件没有启动导致。

使用 Socket Tool 工具监听,可以看到接口可以正常连接:

至于 WebService 的问题,只能确认在调用 SetItemSample 方法时出现了问题。

调试

首先使用接口能够正常登录,可以确认现场访问 WebService 是正常的,不存在网络问题。

之前出现过入参出现 0x00 的字符,服务方法访问异常,经检查通信接口调用入参是正确的,方法不存在问题,那么可以断定就是 SetItemSample 方法内的业务有问题。

前文提到,目前接口内的服务是编译在源码中的,无法通过配置修改,正常的思路是找到源码修改成本地的服务地址或者反编译修改内部的服务地址,方便本地调试。

当然这里不推荐这样做,如果想以最快的速度锁定服务的问题,我们还可以将通信接口的请求转发到本地。

这时候就要祭出 Fiddler 了:

从下图可以看到,很多向 WebService 的请求 500(服务器内部错误),错误内容是内部抛出了:System.NullReferenceException

这时,我们可以选择 AutoResponder 选项卡,设置转发的规则:

这时,我们的调试环境可以将抛出捕获的异常:

这时可以看到,SoapHeader 携带的用户信息,获取失败,所以抛出了异常。

经检查,问题发生在通信接口中,没有对用户账户的输入进行限制,SoapHeader 中的用户 账号信息中携带了空格。

而在服务中,登录时对空格进行了处理,但是后续调用其他服务方法时,服务却没有处理账号中的空格,导致了该问题的产生。

同样的,我们还可以通过 Fiddler 编辑我们的请求,来确认是否是因为 SoapHeader 的问题:

经验证,删除了登录账号中的空格,接口就可以正常使用了。

当然后续就是视情况,对出现问题的接口进行调整。祖传代码,维护时就会发现真的是什么妖魔鬼怪的代码都有啊。

获取瑞美系统数据库密码

前言

瑞美单机版默认打包了其所有已经开发的仪器通信接口,并且包含一个配置文件,记录了软件安装后初始化的一些参数。

包括:labitem、labitemref、labiteminter、labitemval、labitemparam、labitemexp。

当前是打算写一个小工具,将瑞美内已经维护的这些参数,批量读取到我们的云检测数据库中,作为以后新增仪器的基础参数,方便未来维护仪器以及检验项目信息。

但是目前参数文件内的部分内容含义不明,需要通过瑞美的数据库表结构,完成这些内容的匹配。

安装

瑞美的软件可以在其官网下载:http://www.ruimei.com.cn/p/softdownload/index.html

这里建议下载《瑞美单机版 5.0》,解压可以得到安装文件:

安装时会提示选择安装的仪器型号,下拉列表实际提供的是 instr 文件夹内的子文件夹列表。

确认安装后,会拷贝对应文件夹内的通信接口程序,并读取其内的参数文件,向数据库添加默认的参数信息。

使用

安装以后即可启动瑞美程序,简单测试一下程序是否安装正常。

默认安装后没有登录密码,无需输入直接“登录”即可进入主界面。

启动时通信接口程序会同时启动,如果需要测试通信接口,密码为 13311667706

然后就需要找到瑞美的数据库,打开接口根目录:

可以很清楚的看到了,database 内存放的就是其数据库文件,dbdriver 为其驱动文件,E411 则是主程序、通信接口文件所在的目录。

反编译

以下反编译操作仅供学习。

网上检索了一些资料,并没有找到关于瑞美单机版数据库如何连接,只知道其使用的数据库是 sybase

另外,查看了下 ODBC,发现安装后多出了一个名为 lis2017 的数据源。

而该数据库文件默认是带有密码,无法使用 Windows 身份认证 等认证方式进行登录。

而找到密码最简单的自然是通过反编译,查看程序里如何获取数据库身份认证信息。

这里推荐使用 PBKiller 进行反编译,反编译的程序可以选择瑞美的通信解码程序,也就是之前提到的 instr 内的程序。

PBKiller 下载地址:http://www.greenxf.com/soft/118706.html

PBKiller 查找内容使用体验不好,建议导出后使用 Notepad++ 等文本工具检索。

因为猜测其用户名没有修改,默认为 DBA,在 Notepad++ 中打开导出的所有文件,输入该关键字直接搜索“所有打开的文件”,可以很容易的找到其设置数据库连接信息的内容。

可以看到,其如果使用的数据库为 lis2002 时,密码为 thisisapig,否则密码为 thisisaflyingpig

注:另外,开发接口时如果不知道接收到的内容如何解析,可以参考瑞美接口内的反编译内容,经研究发现 wf_addresult 为其保存解析结果的方法,可以查找该方法锁定解析的位置。

连接数据库

首先需要启动数据库,可以运行瑞美根目录 dbdriver 文件夹下的 dbeng9.exe 程序,托管数据库文件。

如果程序成功启动,可以看到其显示托管的端口,上图显示端口为 2638

数据库建议使用 sybase central 进行连接,下载地址:http://www.121down.com/soft/softview-93247.html

该程序为绿色版,需要运行批处理脚本注册,如果无法正常启动,可以尝试运行 卸载Sybase Central插件.bat 脚本后,重新注册。

启动 sybase central 后,选择 工具连接Sybase IQ,标识标签下,用户输入 DBA,口令输入 thisisaflyingpig,数据库标签下,我们可以查找可供连接的服务器:

正常情况下,这时所有准备工作就完成了,右键连接的数据库,选择 Open Interactive SQL 可以打开 SQL 脚本编辑框:

注意:

  1. 以上过程仅供学习。
  2. 部分单工、双工接口如果不想开发上位机接口,可以通过读写瑞美数据库数据实现。
  3. 瑞美单机版数据库使用版本比较低,印象中测试需要使用 .NET Framework 2.0 提供的驱动才能连接成功。
  4. 目前网络版使用比较多,使用的一般是 SQL Server 数据库,如果需要对接,建议直接向瑞美工程师申请,开放视图进行读取。

汉字帮助类——获取拼音与五笔简码

前言

系统各类的数据字典、业务数据检索,普遍是通过下拉框实现,部分会提供模糊检索,但是很少会提供通过五笔或拼音首码来检索的方式。

在过去参与的系统设计中,发现对于计算机系统不熟悉、键盘操作不熟练的用户来说,提供五笔或拼音首码检索的方式,可以大幅度提高表单录入的速度。

本文主要介绍如果通过汉字信息,获取拼音、五笔首码的信息。

字典数据的检索建议提供 ID、名称、代码、拼音首码、五笔首码 等方式,提升用户在表单录入时的录入速度。

准备

数据字典中的 ID、名称、代码 都是原生信息,很方便获取,但是 拼音首码 与 五笔首码 需要维护字典库存储用于生成数据字典。

目前已经通过 百度汉语 以及 汉典网 提取到基本汉字的信息,包括 拼音、五笔 等信息。

链接: https://pan.baidu.com/s/17OOspKdKk5MJi3YT3Nhsjw 密码: tgd1

帮助类

已经预先导出了一份包含 汉字、拼音首码、五笔首码 信息的文件。

链接: https://pan.baidu.com/s/14Z5waW6OIGo2_PMFf7cvow 密码: g37t

五笔与拼音首码获取帮助类,可以参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
/// <summary>
/// 拼音五笔帮助类
/// </summary>
public static class PinyinWubiHelper
{
private static Dictionary<char, (char[] pinyin, char[] wubi)> _dicHanzi = new Dictionary<char, (char[] pinyin, char[] wubi)>();
static PinyinWubiHelper()
{
string text = Resource.chinese;
// 记录没有五笔或拼音的字符,存在需要抛出异常
List<char> listWubi = new List<char>();
List<char> listPinyin = new List<char>();
foreach (string hanzi in text.Split('\n', StringSplitOptions.RemoveEmptyEntries))
{
string[] info = hanzi.Split(',');
_dicHanzi.Add(info[0][0]
, (pinyin: info[2].Split('|', StringSplitOptions.RemoveEmptyEntries).Select(py => py[0]).ToArray()
, wubi: info[1].Split('|', StringSplitOptions.RemoveEmptyEntries).Select(wb => wb[0]).ToArray()));
if (_dicHanzi[info[0][0]].wubi.Length == 0) listWubi.Add(info[0][0]);
if (_dicHanzi[info[0][0]].pinyin.Length == 0) listPinyin.Add(info[0][0]);
}
if (listWubi.Count > 0 || listPinyin.Count > 0)
{
throw new Exception($"{(listWubi.Count > 0 ? $"存在无五笔首码的字符:{string.Concat(listWubi)}" : "")}\r\n{(listPinyin.Count > 0 ? $"存在无拼音首码的字符:{string.Concat(listPinyin)}" : "")}");
}
}

/// <summary>
/// 获取一段中文的五笔首码
/// </summary>
/// <param name="text">中文内容</param>
/// <param name="length">期望返回五笔首码的最大长度,默认 20</param>
/// <returns>处理后的五笔首码</returns>
public static IEnumerable<string> GetWubiCode(string text, int length = 20)
{
text = HandlingText(text, length);
if (!string.IsNullOrEmpty(text))
{
// 默认第一位首码时的内容并返回
var def = text
.ToCharArray()
.Select(ch => _dicHanzi.ContainsKey(ch) ? _dicHanzi[ch].wubi[0] : ch);

yield return new string(def.ToArray());

// 记录迭代到指定位置时还未返回的所有可能
List<List<char>> list = new List<List<char>> { new List<char>() };

// 循环字符串的所有字符
for (int i = 0; i < text.Length; i++)
{
if (_dicHanzi.ContainsKey(text[i]))
{
// 当前字符存在首码
char[] wubi = _dicHanzi[text[i]].wubi;
if (wubi.Length > 1)
{
// 1. 存在多个首码,需要将每种可能的结果返回,并且更新所有组合的可能性
// 获取当前位置之后的默认字符,用于返回
IEnumerable<char> last = def.Take(i + 1);

// 更新截止到当前字符之前的所有可能
List<List<char>> update = new List<List<char>>();

// 循环截止到当前字符之前的所有可能
for (int j = 0; j < list.Count; j++)
{
// 循环当前字符的所有首码
for (int k = 0; k < wubi.Length; k++)
{
var current = new List<char>(list[j]) { wubi[k] };

// 将截至到当前位置的所有默认可能返回
if (k > 0)
{
var result = new List<char>(current);
result.AddRange(last);
yield return new string(result.ToArray());
}

update.Add(current);
}
}

// 可能性增加,需要更新
list = update;
}
else
{
// 2. 只存在一个首码,将当前字符的首码添加到内容中
list.ForEach(l => l.Add(_dicHanzi[text[i]].wubi[0]));
}
}
else
{
// 当前字符不存在五笔首码,则将当前字符添加到内容中
list.ForEach(l => l.Add(text[i]));
}
}
}
}

/// <summary>
/// 获取一段中文的拼音首码
/// </summary>
/// <param name="text">中文内容</param>
/// <param name="length">期望返回拼音首码的最大长度,默认 20</param>
/// <returns>处理后的拼音首码</returns>
public static IEnumerable<string> GetPinyinCode(string text, int length = 20)
{
text = HandlingText(text, length);
if (!string.IsNullOrEmpty(text))
{
// 默认第一位首码时的内容并返回
var def = text
.ToCharArray()
.Select(ch => _dicHanzi.ContainsKey(ch) ? _dicHanzi[ch].pinyin[0] : ch);

yield return new string(def.ToArray());

// 记录迭代到指定位置时还未返回的所有可能
List<List<char>> list = new List<List<char>> { new List<char>() };

// 循环字符串的所有字符
for (int i = 0; i < text.Length; i++)
{
if (_dicHanzi.ContainsKey(text[i]))
{
// 当前字符存在首码
char[] pinyin = _dicHanzi[text[i]].pinyin;
if (pinyin.Length > 1)
{
// 1. 存在多个首码,需要将每种可能的结果返回,并且更新所有组合的可能性
// 获取当前位置之后的默认字符,用于返回
IEnumerable<char> last = def.Skip(i + 1);

// 更新截止到当前字符之前的所有可能
List<List<char>> update = new List<List<char>>();

// 循环截止到当前字符之前的所有可能
for (int j = 0; j < list.Count; j++)
{
// 循环当前字符的所有首码
for (int k = 0; k < pinyin.Length; k++)
{
var current = new List<char>(list[j]) { pinyin[k] };

// 将截至到当前位置的所有默认可能返回
if (k > 0)
{
var result = new List<char>(current);
result.AddRange(last);
yield return new string(result.ToArray());
}

update.Add(current);
}
}

// 可能性增加,需要更新
list = update;
}
else
{
// 2. 只存在一个首码,将当前字符的首码添加到内容中
list.ForEach(l => l.Add(_dicHanzi[text[i]].pinyin[0]));
}
}
else
{
// 当前字符不存在拼音首码,则将当前字符添加到内容中
list.ForEach(l => l.Add(text[i]));
}
}
}
}

// 特殊字符字典
private static Dictionary<char, char> _dicChar = new Dictionary<char, char>
{
['A'] = 'A',
['B'] = 'B',
['C'] = 'C',
['D'] = 'D',
['E'] = 'E',
['F'] = 'F',
['G'] = 'G',
['H'] = 'H',
['I'] = 'I',
['J'] = 'J',
['K'] = 'K',
['L'] = 'L',
['M'] = 'M',
['N'] = 'N',
['O'] = 'O',
['P'] = 'P',
['Q'] = 'Q',
['R'] = 'R',
['S'] = 'S',
['T'] = 'T',
['U'] = 'U',
['V'] = 'V',
['W'] = 'W',
['X'] = 'X',
['Y'] = 'Y',
['Z'] = 'Z',
['a'] = 'a',
['b'] = 'b',
['c'] = 'c',
['d'] = 'd',
['e'] = 'e',
['f'] = 'f',
['g'] = 'g',
['h'] = 'h',
['i'] = 'i',
['j'] = 'j',
['k'] = 'k',
['l'] = 'l',
['m'] = 'm',
['n'] = 'n',
['o'] = 'o',
['p'] = 'p',
['q'] = 'q',
['r'] = 'r',
['s'] = 's',
['t'] = 't',
['u'] = 'u',
['v'] = 'v',
['w'] = 'w',
['x'] = 'x',
['y'] = 'y',
['z'] = 'z',
['0'] = '0',
['1'] = '1',
['2'] = '2',
['3'] = '3',
['4'] = '4',
['5'] = '5',
['6'] = '6',
['7'] = '7',
['8'] = '8',
['9'] = '9',
};
// 用于替换内容,保留数字、小写字母、大写字母、基本汉字、〇、全角数字、全角小写字母、全角大写字母
private static readonly Regex _regex = new Regex(@"[^0-9A-Za-z\u4E00-\u9FA5\u30070-9a-zA-Z]");
private static string HandlingText(string text, int length = 20)
{
if (string.IsNullOrWhiteSpace(text)) return "";

// 将非数字字母汉字等移除
text = _regex.Replace(text, "");

List<char> content = new List<char>();
// 正则会移除高位字符,这里进行迭代已经不会出现高位字符被拆成两个特殊字符的情况
foreach (char ch in text)
{
// 将特殊字符替换,例如全角字符替换为半角
if (_dicChar.ContainsKey(ch))
{
content.Add(_dicChar[ch]);
}
else
{
content.Add(ch);
}

if (content.Count >= length) break;
}

return new string(content.ToArray()).ToUpper();
}
}

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
string text = "南京市长江大桥参观南京市长江大桥)!@$#AaBbCc123AaBbCc123";
Console.WriteLine($"[{text}]");
Console.WriteLine("拼音:");
foreach (var py in PinyinWubiHelper.GetPinyinCode(text, 50))
{
Console.WriteLine(py);
}
Console.WriteLine("五笔:");
foreach (var wb in PinyinWubiHelper.GetWubiCode(text, 50))
{
Console.WriteLine(wb);
}

// 控制台输出内容:
// [南京市长江大桥参观南京市长江大桥)!@$#AaBbCc123AaBbCc123]
// 拼音:
// NJSCJDQCGNJSCJDQAABBCC123AABBCC123
// NJSZJDQCGNJSCJDQAABBCC123AABBCC123
// NJSCJDQSGNJSCJDQAABBCC123AABBCC123
// NJSZJDQSGNJSCJDQAABBCC123AABBCC123
// NJSCJDQCGNJSZJDQAABBCC123AABBCC123
// NJSCJDQSGNJSZJDQAABBCC123AABBCC123
// NJSZJDQCGNJSZJDQAABBCC123AABBCC123
// NJSZJDQSGNJSZJDQAABBCC123AABBCC123
// 五笔:
// FYYTIDSCCFYYTIDSAABBCC123AABBCC123

如何处理图片

在实验室信息系统(LIS)中,除常见定量、定性、说明文本等结果外,图表、标本图片等也是常见的结果表现形式。

例如:血常规、电泳、病理、精子分析、阴道微生态、各种高倍镜镜检等。

常见的数据提供方式是 base64 编码的字节数组、图片路径、数据库二进制数据等,部分也需要根据仪器提供数据,LIS自行绘制折线图、柱状图。

这里以常见的图片提供方式,说明一些常见的图片处理。

解析 Base64 图片

常见于血常规图片的处理,部分仪器需要设置位图传输、指定图片类型为位图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
string base64Path = Path.Combine(desktop, "base64.txt");
string base64 = File.ReadAllText(base64Path);

var binary = Convert.FromBase64String(base64);
using (MemoryStream stream = new MemoryStream(binary))
using (Image image = Image.FromStream(stream))
{
string imagePath = Path.Combine(desktop, $"WBC{image.GetExtension()}");
image.Save(imagePath);
Console.WriteLine($"原始图片大小:{binary.Length}");
Console.WriteLine($"保存到本地图片大小:{new FileInfo(imagePath).Length}");
}
// 原始图片大小:89654
// 保存到本地图片大小:179254

base64 文件内容链接: https://pan.baidu.com/s/1YPPJftih0YecJYys1Jenng 密码: dbfu

图片压缩

如上面演示的代码,如果我们直接使用 Image.Save() 方法,由于 GDI+ 的处理,所保存图片大小与原始大小比较有大概一倍的增长。

不过这个流程还是要做,因为这样解析另外一个目的是为了获取图片的格式,以及确保二进制内容存储的是图片内容。

所以以上为错误演示,确认可以正确被解析为图片、解析到图片格式后,应该直接保存字节数组的内容到文件即可,无需使用 Image 对象保存。

1
2
// image.Save(imagePath);
File.WriteAllBytes(imagePath, binary);

当然,以上的做法可以避免 GDI 绘图导致的图片增长,但是无法避免另外一个问题:原始图片就比较大。

常见的血常规图片因为是软件绘制,并且文件大小都在 100KB 以内,大小可以接受。

但是部分镜检的仪器,因为输出的是”照片“,所以普遍文件大小都以 MB 计。

但是作为要体现在报告单上的图片,系统并不需要太多的图片细节。而且太大的图片,还会影响报告单文件的大小,所以还需要对图片进行压缩。

其实 GDI 已经提供了一个获取图片缩略图的方法,已经封装到工具类中,可以直接使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
string base64Path = Path.Combine(desktop, "base64.txt");
string base64 = File.ReadAllText(base64Path);

var binary = Convert.FromBase64String(base64);
using (MemoryStream stream = new MemoryStream(binary))
using (Image image = Image.FromStream(stream))
using (Image thumb = image.GetThumbnail())
{
string imagePath = Path.Combine(desktop, $"WBC{image.GetExtension()}");
string thumbPath = Path.Combine(desktop, $"WBC-EX{image.GetExtension()}");
//image.Save(imagePath);
File.WriteAllBytes(imagePath, binary);
thumb.Save(thumbPath);

Console.WriteLine($"原始图片大小:{new FileInfo(imagePath).Length}");
Console.WriteLine($"缩略图大小:{new FileInfo(thumbPath).Length}");
}
// 原始图片大小:89654
// 缩略图大小:1501

how-to-process-images

可以看到,对于图片细节比较少的位图,图片可以从 88KB 缩小到不足 2K,而且由于没有设置缩略图尺寸,所以在软件中几乎看不到两张图的差异。

而对于大尺寸、高分辨率、细节比较多的图片,还可以进行如下处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
string imagePath = Path.Combine(desktop, "原始图.jpg");
string thumbPath = Path.Combine(desktop, "缩略图.jpg");

using (Image image = Image.FromFile(imagePath))
using (Image thumb = image.GetThumbnail(1000))
{
// 必须指定保存时的缩略图格式,否则如果按位图输出将没有明显的压缩效果
thumb.Save(thumbPath, ImageFormat.Jpeg);

Console.WriteLine($"原始图片 尺寸:{image.Width}×{image.Height} 大小:{new FileInfo(imagePath).Length}");
Console.WriteLine($"缩略图 尺寸:{thumb.Width}×{thumb.Height} 大小:{new FileInfo(thumbPath).Length}");
}
// 原始图片 尺寸:2736×3648 大小:2652323
// 缩略图 尺寸:1000×1334 大小:98715

当然,优秀的压缩效果,也损失了大量的图片细节,具体如何使用要根据实际情况进行调整。

反色处理

如缩略图演示的图片,部分图片为了方便在仪器软件展示,提供的图片为黑底,但是在报告单中不可能展示黑底的图片。

这时常规的做法为将图片进行反色处理(可以在图片编辑软件中使用 Ctrl+Shift+i 看到反色效果)。

同样反色也已经封装在工具类的扩展方法中,调用如下:

1
2
3
4
5
6
7
8
9
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
string imagePath = Path.Combine(desktop, "WBC.bmp");
string invertPath = Path.Combine(desktop, "WBC-EX.bmp");

using (Image image = Image.FromFile(imagePath))
using (Image invertImage = image.InvertColors())
{
invertImage.Save(invertPath);
}

how-to-process-images-02

绘图

常见绘制的图片为柱状图、散点图,可以参考文章:贝克曼 DxH800 血球仪图片绘制问题

附常用图片相关扩展方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Text;

namespace JohnSun.Util
{
/// <summary>
/// Image 扩展方法
/// </summary>
public static class ImageExtensions
{
/// <summary>
/// 获取图形的字节数组内容
/// </summary>
/// <param name="image">原始图片</param>
/// <param name="format">图片格式</param>
/// <returns>图片的内容</returns>
public static byte[] GetBytes(this Image image, ImageFormat format = default)
{
using (var stream = new MemoryStream())
{
image.Save(stream, new ImageFormat((format ?? image.RawFormat).Guid));
return stream.ToArray();
}
}

/// <summary>
/// 获取图形的反色内容
/// </summary>
/// <param name="image">原始图片</param>
/// <returns>反色的图形,同画图工具中 Ctrl+Shift+I 快捷键反色的效果</returns>
public static Image InvertColors(this Image image)
{
Bitmap bitmap = new Bitmap(image);
for (int x = 0; x < bitmap.Width; x++)
{
for (int y = 0; y < bitmap.Height; y++)
{
var pixel = bitmap.GetPixel(x, y);
var invertPixel = Color.FromArgb(0xff - pixel.R, 0xff - pixel.G, 0xff - pixel.B);
bitmap.SetPixel(x, y, invertPixel);
}
}

return bitmap;
}

/// <summary>
/// 获取图像缩略图
/// </summary>
/// <param name="image">原始图像</param>
/// <param name="width">缩略图宽度,默认为原图宽度,如果只设置了高度会等比缩放</param>
/// <param name="height">缩略图高度,默认为原图高度,如果只设置了宽度会等比缩放</param>
/// <returns>返回缩略图</returns>
public static Image GetThumbnail(this Image image, int width = default, int height = default)
{
if (width <= 0) width = height <= 0 ? image.Width : (int)Math.Ceiling(image.Width * (height / (decimal)image.Height));
if (height <= 0) height = (int)Math.Ceiling(image.Height * (width / (decimal)image.Width));

return image.GetThumbnailImage(width, height, new Image.GetThumbnailImageAbort(() => true), IntPtr.Zero);
}

/// <summary>
/// 获取图片的原始格式
/// </summary>
/// <param name="image">原始图像</param>
/// <returns>返回 ImageFormat 属性中已确定的格式</returns>
public static ImageFormat GetRawFormat(this Image image)
{
return image.RawFormat.GetRawFormat();
}

/// <summary>
/// 获取图片格式的后缀名
/// </summary>
/// <param name="image">原始图像</param>
/// <returns>返回后缀名,支持 ImageFormat 属性中已确定的格式</returns>
public static string GetExtension(this Image image)
{
return image.RawFormat.GetExtension();
}

/// <summary>
/// 获取图片的原始格式
/// </summary>
/// <param name="format">当前从图片中读取的格式</param>
/// <returns>返回 ImageFormat 属性中已确定的格式</returns>
public static ImageFormat GetRawFormat(this ImageFormat format)
{
if (format.Equals(ImageFormat.MemoryBmp)) return ImageFormat.MemoryBmp;
else if (format.Equals(ImageFormat.Bmp)) return ImageFormat.Bmp;
else if (format.Equals(ImageFormat.Emf)) return ImageFormat.Emf;
else if (format.Equals(ImageFormat.Wmf)) return ImageFormat.Wmf;
else if (format.Equals(ImageFormat.Gif)) return ImageFormat.Gif;
else if (format.Equals(ImageFormat.Jpeg)) return ImageFormat.Jpeg;
else if (format.Equals(ImageFormat.Png)) return ImageFormat.Png;
else if (format.Equals(ImageFormat.Tiff)) return ImageFormat.Tiff;
else if (format.Equals(ImageFormat.Exif)) return ImageFormat.Exif;
else if (format.Equals(ImageFormat.Icon)) return ImageFormat.Icon;
else return format;
}

/// <summary>
/// 获取图片格式的后缀名
/// </summary>
/// <param name="format">当前从图片中读取的格式</param>
/// <returns>返回后缀名,支持 ImageFormat 属性中已确定的格式</returns>
public static string GetExtension(this ImageFormat format)
{
if (format.Equals(ImageFormat.MemoryBmp)) return ".bmp";
else if (format.Equals(ImageFormat.Bmp)) return ".bmp";
else if (format.Equals(ImageFormat.Emf)) return ".emf";
else if (format.Equals(ImageFormat.Wmf)) return ".wmf";
else if (format.Equals(ImageFormat.Gif)) return ".gif";
else if (format.Equals(ImageFormat.Jpeg)) return ".jpg";
else if (format.Equals(ImageFormat.Png)) return ".png";
else if (format.Equals(ImageFormat.Tiff)) return ".tiff";
else if (format.Equals(ImageFormat.Exif)) return ".exif";
else if (format.Equals(ImageFormat.Icon)) return ".ico";
else throw new Exception($"未知的图片格式:{format}");
}
}
}

检验仪器常见通信方案

一、串口

串口通信

串口通信(Serial Communication)的概念非常简单,串口按位(bit)发送和接收字节的通信方式。

串口通信最重要的参数是波特率、数据位、停止位和奇偶校验,对于两个进行通信的端口,这些参数必须匹配。

RS-232 标准是电气通信中最常见的串行连接标准,所以工作中串口通信一般都是RS-232协议通信,而在系统中或一些调试工具中通信端口也会称之为COM口(Cluster communication port)即串行通信端口。

RS-232 是美国电子工业联盟(EIA)制定的串行数据通信的接口标准,原始编号全称是 EIA-RS-232(简称 232,RS232)。它被广泛用于计算机串行接口外设连接。

RS-232 设置:

串口号:所使用的串行接口。常见如 COM1、COM2。

波特率:是指从一设备发到另一设备的波特率,即每秒钟多少符號。典型的波特率是 300, 1200, 2400, 9600, 19200, 115200 等。一般通信两端设备都要设为相同的波特率,但有些设备也可设置为自动检测波特率。

校验位:是用来验证数据的正确性。常见如 NONE 、ODD 、EVEN。

数据位:数据位紧跟在起始位之后,是通信中的真正有效信息。数据位的位数由通信双方共同约定,一般可以是 6 位、7 位或 8 位,比如标准的 ASCII 码是 0 至 127(7 位),扩展的 ASCII 码是 0 至 255(8 位)。

停止位:停止位在最后,用以标志一个字符传送的结束,对应于逻辑 1(高电平)状态。停止位可以是 1 位、1.5 位或 2 位。

硬件连接

实际连接中,我们可以通过设备后方的串口端口完成连接。

但是需要注意的是,建议由仪器工程师完成连接工作,LIS 开发人员再进行调试测试。

如果没有仪器工程师指导,自行连接,需要将连接的仪器和电脑关闭断电后再操作,否则可能因为连接时的电流,烧坏硬件设备。

instrument-communication-01

如上图为电脑上的串口端口,以及连接需要使用的线材。如果没有串口端口,可以考虑使用 USB 转串口的转接线,但是这类设备可能会因为驱动或兼容问题,不够稳定。

软件驱动

在某些情况下,硬件连接将不适用。例如开发人员调试、仪器软件与 LIS 系统部署在同一台电脑,这时如果考虑购置双串口的电脑自然是不合理的。

建议可以考虑使用软件驱动,可以使用 VSPD 工具虚拟串口:

instrument-communication-02

发送/接收数据

发送或接收数据这里建议使用一款名为【串口调试助手 SComAssistant V2.2】的工具。

instrument-communication-03

该工具可以实现一些较为简单的数据收、发操作,基本能够满足需求,如果不满足需要还可以考虑 AccessPort 等工具。

instrument-communication-04

推荐 AccessPort 工具的另外一个原因是,其还可以作为一个监控工具使用,即类似“抓包工具”来使用。

以上串口工具已经打包,可以直接下载: https://pan.baidu.com/s/1CqcharAtWJLifPI0dSRSQA 密码: 5q2g

二、网口

网口通信

网口通信即通过以太网连接到仪器,进行数据的传输的通信方式。

因为绝大多数使用 TCP/IP 协议进行通信,所以一般指基于 TCP/IP 协议进行数据传输的通信方式。

而 Socket 通信与 TCP/IP 协议都是基础知识,这里不再展开叙述。

硬件连接

网线连接网口这种很基础的知识,这里应该不用再赘述了。

instrument-communication-05

这里需要注意的是一个双网口电脑问题。

理论上仪器软件所在的电脑可以接入路由器,与 LIS 所在电脑在同一个局域网内,双方就可以通信。

但是部分仪器厂商会因为网络安全的问题,要求仪器软件不接入局域网,直接与 LIS 所在电脑直接连接。

这样仪器将会占用电脑的网口,所以只有采购双网口的电脑供 LIS 使用。

当然也可以考虑再采购一个外置网卡,但是考虑驱动与兼容性问题,建议采购时优先考虑双网口的电脑。

发送/接收数据

网口的测试工具,这里推荐【TCP/UDP Socket 调试工具(Socket Tool)】:

instrument-communication-06

另外还有 sokit 工具,体验也不错:

instrument-communication-07

如果需要抓包,这里推荐 wireshark,使用教程建议参考博客园文章:https://www.cnblogs.com/mq0036/p/11187138.html

以上网口工具已经打包,可以直接下载:https://pan.baidu.com/s/1SkbeDkbE0KN46Qp9ShNXuw 密码: cfhm

三、其他

一般主流仪器只会提供串口、网口两种通信方式。

但是如果是国产仪器,或者国内代理商对仪器软件进行了汉化,再或者实验室使用了第三方的 LIS 而且想保留,那么就可能需要考虑其他的对接方案。

例如希森美康血球仪国内代理商会安装 Laborman 与仪器连接,用户在没有使用 LIS 可以直接在该软件上出具报告。而如果需要与仪器连接就需要读取仪器输出的 cfd 后缀文件。

有些国内一些镜检仪器,软件上没有针对性的开发通信模块,而是提供数据库访问权限,让 LIS 直接读取他们软件的数据库。

而针对这些其他的连接方式,选择一个最简单、最稳定的选项即可。

回顾2020年

不知不觉已经在杭州工作了近一年,交接圈有了一些变化,认识了很多很 NICE 的小伙伴,也慢慢适应了杭州的生活节奏。

但是当前这份工作不甚满意,公司内后端技术栈 C#、PHP、Java 中,目前过的最惨的也就是 C# 了,所以想在正式离职一些,总结一下过去一年的经历。

工作

归纳总结

之前工作的团队有线上的云课堂,可以学习公司中的各个产品,开发也有各种文档可以学习,甚至文档的生产与学习称为了工作中的一个指标。

新的这份工作也有在线的 Wiki,是使用 Atlassian 部署,但是明显上面的内容就没法系统性的阅读与学习。

接收这份工作的第一件事情,就是花了半个月的时间整理了一份产品手册,并且学习源码。

当然后面也了解到所属团队的人手不足,没有余力做这些事情,但是一个优秀的团队一定要重视各类文档的输出,不能害怕或抗拒做这件事情。

团队 Leader

领导可以不懂技术,但是一定要尊重团队成员的选择。

经历过领导与研发团队争论数据库应该用 SQL Server 还是 MySQL,技术栈是升级到 ASP.NET Core 还是守着 ASP.NET MVC,一个小工具到底用 Windows Form 还是 WPF 等。

感叹不怕领导不懂这些,就怕领导遵循他所了解到过时的经验,来限制产品的进步。

领导可以不懂业务,但是一定要重视新产品与新业务的调研。

过去一年碰到最多的情况就是,这个产品或这个功能我明天就要。

当然最后经过”讨价还价“,会把时间延长到一周或者半个月,但是最后的结果都不理想,甚至可能因为破坏了现有的业务流程,要花费几个月的时间去摆平后续的烂摊子。

领导可以不善言谈,但是一定要言而有信,注意自己的言行举止。

……

太多的牢骚就发到这吧。

逆流程的重要性

之前第一份工作是传统的 HIS 企业,所以为了满足用户需求与缓解运维压力,比较重视逆流程与重要数据的更新。

但是,目前这个工作,因为之前产品设计不重视逆流程的原因,过去一年在后台改了太多的数据。

当然,经过努力,我把所负责的产品,功能慢慢都补全了,但是与其他产品线对接,仍然会因为这个出现一些问题。

比如,已经同步的订单删除了,比如订单的状态在我负责的产品回退了但是其他产品不能回退,比如订单信息更新了没有通知导致各系统信息不对称等等。

当然其他研发和产品普遍认为,这个操作是不合适的所以不考虑添加类似的功能,但是日常业务的确需要这样操作,没办法,只能后台修改数据。

其实这些是可以通过权限控制来避免一些错误的操作的,至少要让客服主管和后台管理员有这部分权限,否则只能大半夜被电话骚扰起床改数据了。

职责划分

如果工作环境需要多个部门,多个团队协作,应该都经历过相互甩锅与扯皮。

大家都希望做简单的事情,都希望承担最少的责任获取最大的收益。

所以并不是技术栈越多越好,产品线越多越好,一切都要有一个度。

另外就是工作职责,如果公司不重视技术运维、产品实施、技术支持这些岗位,那研发工作的开展将会很难或者说很累。

生活

健康

一个比我还年轻些的小伙伴今年住院了,其实前年体检的时候对应的指标就不正常,没有重视。

虽然保险买了不用担心生病后给家里增加负担,但是也要重视自己的身体健康,定期做做体检。

另外,体重的问题也要重视,加油减掉身上的赘肉。

心态

目前最焦虑的事情就是患上了:拖延症……

克服、克服、再克服吧!!!

未来

学习进阶

工作真的不能被编程语言困住,目前还停留在 C# 中,但是工作机会越来越少,是时候考虑学习一些新的编程语言。

目前考虑是 Java、Go 与 Rust,后续再根据需求做一些调整吧。

另外也要把当前用过的技术栈,系统性的学习一下,例如 Redis、Elasticsearch、RabbitMQ 等。

极客时间、慕课网刷起来吧。

另外,博客最近更新太少了,过去一年学了很多东西,也该总结一下了。

GitHub 的绿墙也该刷起来了。

生活的计划

因为社保的原因,车子暂时不可能上浙A的牌照了,房子也要再努力努力,但是是时候脱单了。(逃

C# 中表达式计算问题

问题:计算公式为 [A/G] = [ALB] / ([TP] - [ALB]),已知 [TP][ALB] 的值,求得 [A/G]

这是一个很简单的四则运算问题,下面将演示如何在 C# 代码中如何简单、高效的进行求解。

一、解析算式

首先,需要考虑的是如何将已知的内容,替换到表达式中,其实想必大家和我一样,想到了正则的替换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// <summary>
/// 解析表达式内容
/// </summary>
/// <param name="text">文本</param>
/// <param name="values">需要替换的值</param>
/// <returns>解析后的内容</returns>
public static string Parse(string text, Dictionary<string, string> values)
{
var regex = new Regex(@"\[.+?\]");
return regex.Replace(text, match =>
{
return values[match.Value];
});
}

然后可以进行简单的测试:

1
2
3
4
5
6
7
8
9
10
11
12
[Fact]
public void ParseSuccess()
{
string text = "[ALB] / ([TP] - [ALB])";
var values = new Dictionary<string, string>
{
{"[ALB]", "2.3"},
{"[TP]", "5.4"},
};
var result = Expression.Parse(text, values);
Assert.Equal("2.3 / (5.4 - 2.3)", result);
}

二、进行运算

内容替换完成后,需要解析以上字符串,获取运算结果。网上会有一些讲解,如何用算法解析,再进行运算。

但复杂的算法不是我们追求的,我们要的是在尽可能简单的前提下完成运算工作。

以下介绍的是一些易用,且代码非常简单的解决方案。

1. 使用 DataTable 的 Computer 方法

DataTable 大家应该非常熟悉,而 Computer 如果有使用过 DataTable 的聚合函数,应该也了解这个方法。

这里也是利用了 Computer 方法支持四则运算的特点:

1
2
3
4
5
6
private static readonly DataTable _table = new DataTable();

public static string UseDataTableComputer(string exp)
{
return _table.Compute(exp, "").ToString();
}

2. 使用数据库计算

数据库查询支持四则运算,这个是常识,所以同样的我们可以使用数据库查询获取四则运算结果。

如果采用这个方法,建议用 SQLite 的内存模式,不用担心数据库断连,导致查询出现问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static SQLiteConnection _conn;
private static SQLiteConnection _connection
{
get
{
if (_conn == null)
{
_conn = new SQLiteConnection("Data Source=:memory:;Version=3;");
_conn.Open();
}
return _conn;
}
}

public static string UseDatabase(string exp)
{
using (SQLiteCommand command = new SQLiteCommand($"select {exp}", _connection))
{
return command.ExecuteScalar().ToString();
}
}

以上使用 NuGet 引用引用了 System.Data.SQLite.Core

3. 使用 JavaScript 解析

说到 JavaScript 运行代码段,肯定想到了 eval 这个方法,而在 C# 中其实有很多开源的 JavaScript 解释器,可以运行 JavaScript 代码。

下面就是使用 Jint 实现的一个解决方案:

1
2
3
4
5
6
private static readonly JsValue _jsValue = new Engine().Execute(@"function calc(exp) { return eval(exp); }").GetValue("calc");

public static string UseJavaScript(string exp)
{
return _jsValue.Invoke(exp).ToObject().ToString();
}

以上使用 NuGet 引用引用了 Jint

4. 使用 Math.NET 计算

如果有用 JavaScript 处理过一些复杂的数学问题,例如代数求解,应该知道 algebra.jsmath.js 这两个库。

C# 同样有一个工具包,可以处理复杂的数学问题,就是 Math.NET Team (github.com)

而四则运算这样简单的问题自然不在话下,甚至感觉有点大材小用:

1
2
3
4
5
public static string UseMathNet(string exp)
{
var expression = SymbolicExpression.Parse(exp);
return expression.Evaluate(null).RealValue.ToString();
}

以上使用 NuGet 引用引用了 MathNet.Symbolics

5. 使用 Liquid 计算

liquid 是一种开源的模板语言,类似 Razor,但是相对来说更简单、高效。

其支持四则运算,所以如果模板内容就是一个四则运算,输出的内容就是我们需要的结果:

1
2
3
4
public static string UseLiquid(string exp)
{
return Template.Parse($"{{{{{exp}}}}}").Render();
}

以上使用 NuGet 引用引用了 Scriban

注意: 以上使用的 JintMath.NETScriban 均在 GitHub 开源,如何使用也可以到 GitHub 了解,其实这里仅仅使用了它们最简单的一个功能,强烈推荐都了解一下,它们的功能在实际项目中还可以有更广泛的应用。

三、测试

xUnit 测试文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
using System;
using System.Collections.Generic;
using Xunit;

public class CalculationShould
{
[Theory]
[MemberData(nameof(TestData.List), MemberType = typeof(TestData))]
public void UseDataTableComputerPass(string exp, double result)
{
var value = Convert.ToDouble(Calculation.UseDataTableComputer(exp));
value = Math.Round(value, 2, MidpointRounding.AwayFromZero);
Assert.Equal(result, value);
}

[Theory]
[MemberData(nameof(TestData.List), MemberType = typeof(TestData))]
public void UseDatabasePass(string exp, double result)
{
var value = Convert.ToDouble(Calculation.UseDatabase(exp));
value = Math.Round(value, 2, MidpointRounding.AwayFromZero);
Assert.Equal(result, value);
}

[Theory]
[MemberData(nameof(TestData.List), MemberType = typeof(TestData))]
public void UseJavaScriptPass(string exp, double result)
{
var value = Convert.ToDouble(Calculation.UseJavaScript(exp));
value = Math.Round(value, 2, MidpointRounding.AwayFromZero);
Assert.Equal(result, value);
}

[Theory]
[MemberData(nameof(TestData.List), MemberType = typeof(TestData))]
public void UseLiquidPass(string exp, double result)
{
var value = Convert.ToDouble(Calculation.UseLiquid(exp));
value = Math.Round(value, 2, MidpointRounding.AwayFromZero);
Assert.Equal(result, value);
}

[Theory]
[MemberData(nameof(TestData.List), MemberType = typeof(TestData))]
public void UseMathNetPass(string exp, double result)
{
var value = Convert.ToDouble(Calculation.UseMathNet(exp));
value = Math.Round(value, 2, MidpointRounding.AwayFromZero);
Assert.Equal(result, value);
}

public class TestData
{
private static readonly List<object[]> Data = new List<object[]>
{
new object[] {"1 + 2", 3.0},
new object[] {"10.0 / (5 - 2)", 3.33},
new object[] {"3.14", 3.14},
new object[] {"1 + 2 * 3 / 4.0", 2.5}
};

public static IEnumerable<object[]> List => Data;
}
}

测试结果:

calc-test

四、优缺点

谈优缺点之前,首先看一下性能,测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class Program
{
static void Main(string[] args)
{
BenchmarkRunner.Run<CalculationBenchmark>(new DebugInProcessConfig());

Console.ReadKey();
}
}

[RPlotExporter]
public class CalculationBenchmark
{
private List<string> _exps = new List<string> { "1 + 2", "10.0 / (5 - 2)", "3.14", "1 + 2 * 3 / 4.0" };

[Benchmark]
public List<string> UseDatabase()
{
return _exps.Select(exp => Calculation.UseDatabase(exp)).ToList();
}

[Benchmark]
public List<string> UseDataTableComputer()
{
return _exps.Select(exp => Calculation.UseDataTableComputer(exp)).ToList();
}

[Benchmark]
public List<string> UseJavaScript()
{
return _exps.Select(exp => Calculation.UseJavaScript(exp)).ToList();
}

[Benchmark]
public List<string> UseLiquid()
{
return _exps.Select(exp => Calculation.UseLiquid(exp)).ToList();
}

[Benchmark]
public List<string> UseMathNet()
{
return _exps.Select(exp => Calculation.UseMathNet(exp)).ToList();
}
}

测试结果如下:

Method Mean Error StdDev
UseDatabase 20.287 us 0.3213 us 0.2848 us
UseDataTableComputer 4.210 us 0.0643 us 0.1093 us
UseJavaScript 20.248 us 0.1872 us 0.1659 us
UseLiquid 110.420 us 1.7733 us 1.8974 us
UseMathNet 12.116 us 0.1938 us 0.1813 us

可以看到在性能测试中,DataTableComputer 方法表现最好,但是,更推荐用 UseJavaScriptUseMathNet

原因如下:

  1. UseDataTableComputer 只支持四则运算,如果遇到指数、对数等情况则无法进行运算。
  2. UseDatabase 需要依赖数据库,另外如果遇到整数运算,例如 10 / 3,默认是整除,暂时没有找到好的解决方案处理这个问题,这也是为什么上面的例子中每个算式都有浮点数。
  3. UseLiquid 应用中感觉和 UseJavaScript 很相似,所以在 UseJavaScript 表现更好的前提下,更推荐用后者。
  4. UseMathNet 有更好的性能,并且支持在算式中使用一些常量,例如 eπ,运算符也支持指数符号 ^,但是只能处理与数学相关的问题(不支持位运算)。
  5. UseJavaScript 更灵活而且更方便测试,有更好的扩展性,但是复杂的运算需要使用函数来处理,不如 UseMathNet 易读。

备注:目前我是用 JavaScript,因为公式不是由客户维护,所以不需要开发编辑器,所有计算公式纯粹靠手撸。
目前碰到过最复杂的一个计算公式是:100 * Math.sqrt(-12 + 2.38 * Math.LN2([T4]) + 0.0626 * Math.LN2([CA125Ⅱ]))/(1 + Math.sqrt(-12 + 2.38 * Math.LN2([T4]) + 0.0626 * Math.LN2([CA125Ⅱ])))
当然应用中还涉及一些其他的问题,比如 [GLB] = [TP] / [ALB][A/G] = [ALB] / [GLB],需要用到递归进行处理;计算项因为浮点数精度的问题,所有结果都要指定小数位数等。这个后面有时间再慢慢补。