diff --git a/homepage/dist/index.html b/homepage/dist/index.html
index 2f7c1bd..841cadb 100644
--- a/homepage/dist/index.html
+++ b/homepage/dist/index.html
@@ -227,7 +227,7 @@
先在工作台免费用起来,需要授权与服务时再付费。
-
个人版
+
专业版
¥99/ 年
单设备授权 · 全功能
diff --git a/src-tauri/src/services/license.rs b/src-tauri/src/services/license.rs
index a97efc3..cd53e1c 100644
--- a/src-tauri/src/services/license.rs
+++ b/src-tauri/src/services/license.rs
@@ -432,3 +432,54 @@ pub fn deactivate(app: &AppHandle) -> Result {
let _ = fs::remove_file(&path);
read_status(app)
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use serde_json::json;
+
+ /// 真实服务器签发的离线证(licence.rlwj.cn,product=opsxapi)
+ const REAL_OFFLINE: &str = include_str!("../../../tests/fixtures/licoff-opsxapi.txt");
+ const REAL_DEVICE: &str = include_str!("../../../tests/fixtures/device-id.txt");
+
+ #[test]
+ fn offline_serial_validates_for_bound_device() {
+ let key = REAL_OFFLINE.trim();
+ assert!(is_offline_license_key(key));
+ let claims = verify_offline_serial(key, REAL_DEVICE.trim())
+ .expect("真实离线证应通过本产品+设备校验");
+ assert_eq!(claims.product_code, "opsxapi");
+ assert!(claims.exp > claims.iat);
+ }
+
+ #[test]
+ fn offline_serial_rejects_other_device() {
+ assert!(verify_offline_serial(REAL_OFFLINE.trim(), "00000000-0000-0000-0000-000000000000").is_err());
+ }
+
+ #[test]
+ fn offline_serial_rejects_tampered_payload() {
+ // 篡改 base64 载荷中段一个字符(保持格式合法,签名必失效)
+ let key = REAL_OFFLINE.trim().to_string();
+ let (payload, sig) = key.strip_prefix("LICOFF-1.").unwrap().rsplit_once('.').unwrap();
+ let mut chars: Vec = payload.chars().collect();
+ let mid = chars.len() / 2;
+ chars[mid] = if chars[mid] == 'A' { 'B' } else { 'A' };
+ let tampered = format!("LICOFF-1.{}.{}", chars.into_iter().collect::(), sig);
+ assert_ne!(key, tampered);
+ assert!(verify_offline_serial(&tampered, REAL_DEVICE.trim()).is_err());
+ }
+
+ /// 真实服务器 /v1/license/verify 响应的 Ed25519 签名(hex)与签名原文
+ #[test]
+ fn online_response_sig_verifies_with_embedded_key() {
+ // 1) 原文直接验(证明公钥同源)
+ let msg = include_str!("../../../tests/fixtures/verify-sig-msg.txt");
+ let sig_hex = include_str!("../../../tests/fixtures/verify-sig.txt").trim();
+ let sig = Signature::from_slice(&hex::decode(sig_hex).unwrap()).unwrap();
+ offline_verifying_key().unwrap().verify(msg.as_bytes(), &sig).expect("服务器签名应通过内嵌公钥校验");
+ // 2) 完整响应 JSON 走 verify_response_sig(证明客户端重组串与服务器一致)
+ let payload: Value = serde_json::from_str(include_str!("../../../tests/fixtures/verify-response.json")).unwrap();
+ assert!(verify_response_sig(&payload).is_ok());
+ }
+}
diff --git a/tests/fixtures/device-id.txt b/tests/fixtures/device-id.txt
new file mode 100644
index 0000000..af8d65b
--- /dev/null
+++ b/tests/fixtures/device-id.txt
@@ -0,0 +1 @@
+b6eeb606-82ef-438f-946f-ea6fa6b272ec
diff --git a/tests/fixtures/licoff-opsxapi.txt b/tests/fixtures/licoff-opsxapi.txt
new file mode 100644
index 0000000..4b1d577
--- /dev/null
+++ b/tests/fixtures/licoff-opsxapi.txt
@@ -0,0 +1 @@
+LICOFF-1.eyJ2IjoxLCJwaWQiOiJvcHN4YXBpIiwiZGlkIjoiYjZlZWI2MDYtODJlZi00MzhmLTk0NmYtZWE2ZmE2YjI3MmVjIiwiZW1haWwiOiJsb2dhbkB0ZXN0LmRldiIsInB0IjoieWVhcmx5IiwiaWF0IjoxNzkyMDkyNzQwLCJleHAiOjE4MjM2Mjg3NDAsIm1kIjoxLCJjbiI6IkxvZ2FuKOa1i-ivlSkifQ.1G3TyluNJk1RB1S8vALMelhfUeb0S2YqqgoYYvSoq9JcAI64pMCoPNOUkbS0OOp-s4zIpEIxgHTSShbpsUxDCw
\ No newline at end of file
diff --git a/tests/fixtures/verify-response.json b/tests/fixtures/verify-response.json
new file mode 100644
index 0000000..0a91f70
--- /dev/null
+++ b/tests/fixtures/verify-response.json
@@ -0,0 +1 @@
+{"valid":true,"license_key":"LIC-2026-A424-9335-AA41-AD88","product_id":2,"product_name":"OpsxAPI","product_code":"opsxapi","features":["http","graphql","websocket","sse","socketio","mqtt","mock","testing","ai"],"status":"active","expires_at":"2027-10-16 03:31:21","days_left":364,"max_devices":3,"plan_type":"yearly","message":"验证成功","expires_at_unix":1823628681,"server_time":1792092713,"sig":"4d958e15f1ef10b3f2ea5f4fa2c6aae5f7f323145554ca9b64703ab0295dcacaee57cb02792c647e41eba62dc813ef515b040e644c06fac6c02a7707efe58c0b"}
\ No newline at end of file
diff --git a/tests/fixtures/verify-sig-msg.txt b/tests/fixtures/verify-sig-msg.txt
new file mode 100644
index 0000000..8e4afc3
--- /dev/null
+++ b/tests/fixtures/verify-sig-msg.txt
@@ -0,0 +1 @@
+v1|LIC-2026-A424-9335-AA41-AD88|true|active|1823628681|1792092713|false|3|yearly
\ No newline at end of file
diff --git a/tests/fixtures/verify-sig.txt b/tests/fixtures/verify-sig.txt
new file mode 100644
index 0000000..0218f35
--- /dev/null
+++ b/tests/fixtures/verify-sig.txt
@@ -0,0 +1 @@
+4d958e15f1ef10b3f2ea5f4fa2c6aae5f7f323145554ca9b64703ab0295dcacaee57cb02792c647e41eba62dc813ef515b040e644c06fac6c02a7707efe58c0b
\ No newline at end of file