diff --git a/VOL.Core/Utilities/JSSaTokenSignHelper.cs b/VOL.Core/Utilities/JSSaTokenSignHelper.cs
new file mode 100644
index 0000000..5a859ec
--- /dev/null
+++ b/VOL.Core/Utilities/JSSaTokenSignHelper.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace VOL.Core.Utilities
+{
+ public static class SaTokenSignHelper
+ {
+ ///
+ /// 根据业务参数生成 Java Sa-Token Sign 兼容的 QueryString
+ /// 自动生成 timestamp 和 nonce,如果外部未提供
+ ///
+ /// 业务参数字典(可包含 teacherNo 等)
+ /// 应用密钥
+ /// 应用编码,例如 sk-yuedong
+ /// 带 sign 的完整 QueryString
+ public static string BuildQueryString(Dictionary paramMap, string secretKey)
+ {
+ if (paramMap == null)
+ paramMap = new Dictionary();
+
+ // 1️⃣ TreeMap 排序(ASCII 升序)
+ var sorted = new SortedDictionary(StringComparer.Ordinal);
+ foreach (var kv in paramMap)
+ sorted[kv.Key] = kv.Value.ToString();
+
+ // 2️⃣ 拼接签名字符串 k=v&k=v&...&key=secretKey
+ var sbSign = new StringBuilder();
+ foreach (var kv in sorted)
+ sbSign.Append(kv.Key).Append("=").Append(kv.Value).Append("&");
+ sbSign.Append("key=").Append(secretKey.Trim());
+
+ string sign = Md5(sbSign.ToString());
+
+ // 3️⃣ 拼接 QueryString(保留外部字典原顺序)
+ var sbQuery = new StringBuilder();
+ foreach (var kv in paramMap)
+ sbQuery.Append(Uri.EscapeDataString(kv.Key))
+ .Append("=")
+ .Append(Uri.EscapeDataString(kv.Value.ToString()))
+ .Append("&");
+
+ sbQuery.Append("sign=").Append(sign);
+
+ return sbQuery.ToString();
+ }
+
+
+ ///
+ /// 构建 POST JSON 上传接口的 QueryString(包含 sign)
+ /// ⚠️ 不与 GET 共用,避免误用
+ ///
+ public static string BuildPostJsonQueryString(
+ Dictionary paramMap,
+ string secretKey,
+ string jsonBody)
+ {
+ if (paramMap == null)
+ throw new ArgumentNullException(nameof(paramMap));
+
+ if (string.IsNullOrWhiteSpace(jsonBody))
+ throw new ArgumentException("POST JSON body 不能为空");
+
+ // 1️⃣ 参与签名的参数(TreeMap + ASCII 排序)
+ var signParams = new SortedDictionary(StringComparer.Ordinal);
+
+ foreach (var kv in paramMap)
+ signParams[kv.Key] = kv.Value.ToString();
+
+ // ⭐ POST JSON 特有:整个 body 作为 reqbody
+ signParams["reqbody"] = jsonBody;
+
+ // 2️⃣ 拼接签名字符串
+ // k=v&k=v&...&key=secretKey
+ var sbSign = new StringBuilder();
+ foreach (var kv in signParams)
+ sbSign.Append(kv.Key)
+ .Append("=")
+ .Append(kv.Value)
+ .Append("&");
+
+ sbSign.Append("key=").Append(secretKey.Trim());
+
+ string sign = Md5(sbSign.ToString());
+
+ // 3️⃣ 构建 URL QueryString(⚠️ 不包含 reqbody)
+ var sbQuery = new StringBuilder();
+ foreach (var kv in paramMap)
+ sbQuery.Append(Uri.EscapeDataString(kv.Key))
+ .Append("=")
+ .Append(Uri.EscapeDataString(kv.Value.ToString()))
+ .Append("&");
+
+ sbQuery.Append("sign=").Append(sign);
+
+ return sbQuery.ToString();
+ }
+
+ ///
+ /// MD5 小写 32 位
+ ///
+ private static string Md5(string input)
+ {
+ using var md5 = MD5.Create();
+ var bytes = md5.ComputeHash(Encoding.UTF8.GetBytes(input));
+ var sb = new StringBuilder();
+ foreach (var b in bytes)
+ sb.Append(b.ToString("x2"));
+ return sb.ToString();
+ }
+
+ ///
+ /// 生成随机字符串 nonce,可指定长度
+ ///
+ public static string RandomString(int length = 20)
+ {
+ const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ var random = new Random();
+ return new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray());
+ }
+ }
+}