فهرست منبع

盛大API基本完成

何彬龙 2 سال پیش
والد
کامیت
4e9afa67a9
15فایلهای تغییر یافته به همراه2383 افزوده شده و 5 حذف شده
  1. 27 2
      Cargo.toml
  2. 115 0
      api_test.py
  3. 5 2
      build.rs
  4. 16 0
      config-test.toml
  5. 16 0
      config.toml
  6. 39 0
      logs/log.2023-03-23
  7. 2 0
      logs/log.2023-03-24
  8. 337 0
      src/backend.rs
  9. 112 0
      src/config.rs
  10. 127 0
      src/extract.rs
  11. 306 0
      src/gmssl.rs
  12. 175 0
      src/lib.rs
  13. 65 1
      src/main.rs
  14. 347 0
      src/router.rs
  15. 694 0
      src/sd_model.rs

+ 27 - 2
Cargo.toml

@@ -6,8 +6,33 @@ build = "build.rs"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-
+anyhow = "1.0.70"
+axum = "0.6"
+base16ct = { version = "0.2.0", features = ["std"] }
+base64ct = { version = "1.6.0", features = ["std"] }
+# chrono = { version = "0.4.24", features = ["serde"] }
+futures = "0.3"
+hex = "0.4"
+hyper = { version = "0.14"  }
+md-5 = { version = "0.10.5", features = ["asm"] }
+pin-project-lite = "0.2"
+reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls", "gzip", "brotli", "deflate", "stream", "trust-dns"] }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1.0"
+serde_qs = "0.12.0"
+time = { version = "0.3", features = ["serde", "formatting", "macros", "parsing", "local-offset"] }
+tokio = { version = "1.26", features = ["full", "tracing"] }
+toml = "0.7"
+tower = { version = "0.4", features = ["timeout"] }
+tracing = { version = "0.1", features = ["async-await"] }
+tracing-appender = "0.2"
+tracing-subscriber = { version = "0.3", features = ["local-time", "time"] }
+url = { version = "2.3.1", features = ["serde"] }
 
 [build-dependencies]
 cc = "1"
-cmake = "0.1"
+cmake = "0.1"
+
+
+[dev-dependencies]
+libsm = "*"

+ 115 - 0
api_test.py

@@ -0,0 +1,115 @@
+from gmssl import sm2
+import requests
+import time
+import json
+import hashlib
+import pprint
+import datetime
+
+
+pay_url = "http://127.0.0.1:8848/pay"
+search_url = "http://127.0.0.1:8848/search"
+
+# 测试用 秘钥组1 脚本加密用
+pk1 = "04865e672fa71ab6e37429fd731800792c1015cf4e936bd01ec0092b7da571fb63cb99cca8104b35243175d3f535784f6127af451bed43b5b929db6b864de3b207"
+sk1 = "b73190a4fa4f6786fa36a35c1c16e533943fb5951a40c6b9e2d06b5a19d5c19a"
+
+
+# 测试用 秘钥组2 脚本解密用
+pk2 = "04f87981e38e98c5d400daed3e3dbf0a77e622860923b37ab94622c435401604a2e23f7c2ddb656f0b7735ca1f36cc203e6ac8fe47d6bc15f3739608e81b424c04"
+sk2 = "6907ee4516db5e2f04eef3976551f2c8ead793648edf37b82035013527b3ee42"
+
+sinKey = "ZctLTt2tugU2ntZdVgMXssrGuENPH6V7b7ROYN88Msw="
+pub_head = {
+    "appId": "CESHI",
+    "method": "sd.tovc.recharge",
+    "signType": "md5",
+    "sign": "",
+    "timestamp": "",
+    "version": "v1.0",
+    "requestId": str(int(time.time()*1000)),
+    "bizContent": ""
+}
+
+
+def encrypt(data: str):
+    en = sm2.CryptSM2(private_key="", public_key=pk1)
+    r = en.encrypt(data.encode("utf-8"))
+    # result = bytearray([0x4])
+    result = bytearray()
+    result.extend(r)  # type:ignore
+    return result.hex()  # type:ignore
+
+
+def decrypt(data: str):
+    en = sm2.CryptSM2(private_key=sk2, public_key="")
+    r = en.decrypt(data.encode("utf-8"))
+    return r.hex()  # type:ignore
+
+
+def sign_head(head: dict):
+    keys = list(head.keys())
+    keys.sort()
+    ks = []
+    for k in keys:
+        v = head[k].strip()
+        if k != "sign" and v:
+            ks.append(f"{k}={v}")
+    ks.append(f"key={sinKey}")
+    s = "&".join(ks)
+    m5 = hashlib.md5(s.encode("utf-8"))
+    sign = m5.hexdigest()
+    head["sign"] = sign
+
+
+def test_pay():
+    pay_body = {
+        "gameUserId": "13856023633",
+        "serverType": "tel",
+        "productNo": "YDHF10",
+        "spOrderId": str(int(time.time()*1000)),
+        "source": "SD"
+    }
+    pay_body = json.dumps(pay_body, ensure_ascii=False)
+    head = pub_head.copy()
+    now = datetime.datetime.now()
+    head["timestamp"] = f"{now:%Y-%m-%d %H:%M:%S}"
+    head["requestId"] = str(int(time.time()*1000))
+    head["bizContent"] = encrypt(pay_body)
+    sign_head(head)
+    pprint.pprint(pay_body)
+    r = requests.post(pay_url, json=head)
+    result = r.json()
+    print(result)
+
+
+order = {
+    'code': '0000',
+    'msg': 'success',
+    'data':'{"resultCode":"000","resultDesc":"OK","data":{"state":"0","orderCash":"","orderId":"1679647728031106994","spOrderId":"1679647727617","gameUserId":"13856023633","productName":"YDHF10"}}',
+    'sign': '2B4C3FBD6C65A8A78BCA7EDF71F6C666',
+    'requestId': '1679647727617'}
+
+
+def test_search():
+    search_body = {
+        "spOrderId": "1679680499000"
+    }
+    pay_body = json.dumps(search_body, ensure_ascii=False)
+    head = pub_head.copy()
+    now = datetime.datetime.now()
+    head["timestamp"] = f"{now:%Y-%m-%d %H:%M:%S}"
+    head["requestId"] = str(int(time.time()*1000))
+    head["bizContent"] = encrypt(pay_body)
+    sign_head(head)
+    pprint.pprint(head)
+    r = requests.post(search_url, json=head)
+    result = r.json()
+    print(result)
+
+
+# test_pay()
+test_search()
+
+# s = "{\"HEADER\":{\"VERSION\":\"V1.1\",\"TIMESTAMP\":\"2023-03-24 21:31:47\",\"SEQNO\":\"1679664707214\",\"APPID\":\"jtceshi\",\"SECERTKEY\":\"376F93167C475920C274EE682650031D\"},\"MSGBODY\":{\"CONTENT\":{\"ORDERID\":\"1679647728031106994\",\"USER\":\"13856023633\",\"REQDATE\":\"2023-03-24\",\"PACKAGEID\":\"YDHF10\",\"STATUS\":\"4\",\"EXTORDER\":\"1679647727617\",\"MSG\":\"充值失败\",\"CHECKTIME\":\"20230324164950000\",\"PRICE\":\"100000\"},\"RESP\":{\"RCODE\":\"00\",\"RMSG\":\"OK\"}}}"
+# pprint.pprint(json.loads(s))

+ 5 - 2
build.rs

@@ -2,10 +2,13 @@ fn main() {
     let path = cmake::Config::new("GmSSL")
         .define("ENABLE_SM2_EXTS", "ON")
         .define("BUILD_SHARED_LIBS", "OFF")
-        .configure_arg("--no-warn-unused-cli")
+        .configure_arg("--no-warn-unused-cli") // 交叉编译时防止cmake 报错
         .build_target("gmssl")
         .build();
     println!("cargo:rerun-if-changed=GmSSL");
-    println!("cargo:rustc-link-search=native={}/build/bin", path.display());
+    println!(
+        "cargo:rustc-link-search=native={}/build/bin",
+        path.display()
+    );
     println!("cargo:rustc-link-lib=static=gmssl");
 }

+ 16 - 0
config-test.toml

@@ -0,0 +1,16 @@
+addr = "0.0.0.0:8848"
+timeout = 5000
+
+[fscgConfig]
+payUrl = "http://192.144.212.211:36000/gateway/flowservice/makeorder.ws"
+searchUrl = "http://192.144.212.211:36000/gateway/flowservice/queryorder.ws"
+balanceUrl = "http://192.144.212.211:36000/gateway/flowservice/queryAccount.ws"
+appId = "jtceshi"
+appSecret = "5f1b8349873841e3afe9c6ec91c9847e"
+
+[sdConfig]
+sinKey = "ZctLTt2tugU2ntZdVgMXssrGuENPH6V7b7ROYN88Msw="
+publicKey = "03a937d952388700a18e1d4c69fba338fa507440c4d710a72be80a87269cb1045f"
+privateKey = "b73190a4fa4f6786fa36a35c1c16e533943fb5951a40c6b9e2d06b5a19d5c19a"
+supplierCode = "CESHI"
+callbackUrl = "https://api-test.schengle.com/gateway"

+ 16 - 0
config.toml

@@ -0,0 +1,16 @@
+addr = "0.0.0.0:8848"
+timeout = 5000
+
+[fscgConfig]
+payUrl = "http://192.144.212.211:36000/gateway/flowservice/makeorder.ws"
+searchUrl = "http://192.144.212.211:36000/gateway/flowservice/queryorder.ws"
+balanceUrl = "http://192.144.212.211:36000/gateway/flowservice/queryAccount.ws"
+appId = "123456"
+appSecret = "123456"
+
+[sdConfig]
+sinKey = "ZctLTt2tugU2ntZdVgMXssrGuENPH6V7b7ROYN88Msw="
+publicKey = "03a937d952388700a18e1d4c69fba338fa507440c4d710a72be80a87269cb1045f"
+privateKey = "244889352ab13f8601a9871ef47dc2a694bd293f8ae156538aea036d07f47729"
+supplierCode = "CESHI"
+callbackUrl = "https://api.schengle.com/gateway"

+ 39 - 0
logs/log.2023-03-23

@@ -0,0 +1,39 @@
+2023-03-24 00:46:49  INFO tests::test_log example_span: sd_proxy::tests: src/lib.rs:136: this is an info message
+2023-03-24 00:46:49  WARN tests::test_log example_span: sd_proxy::tests: src/lib.rs:137: this is a warning message
+2023-03-24 00:46:49 ERROR tests::test_log example_span: sd_proxy::tests: src/lib.rs:138: this is an error message
+2023-03-24 00:47:12  INFO tests::test_log sd_proxy::tests: src/lib.rs:133: this is an info message
+2023-03-24 00:47:12  WARN tests::test_log sd_proxy::tests: src/lib.rs:134: this is a warning message
+2023-03-24 00:47:12 ERROR tests::test_log sd_proxy::tests: src/lib.rs:135: this is an error message
+2023-03-24 00:47:12  INFO tests::test_log example_span: sd_proxy::tests: src/lib.rs:141: this is an info message
+2023-03-24 00:47:12  WARN tests::test_log example_span: sd_proxy::tests: src/lib.rs:142: this is a warning message
+2023-03-24 00:47:12 ERROR tests::test_log example_span: sd_proxy::tests: src/lib.rs:143: this is an error message
+2023-03-24 00:54:19  INFO tests::test_log sd_proxy::tests: src/lib.rs:133: this is an info message
+2023-03-24 00:54:19  WARN tests::test_log sd_proxy::tests: src/lib.rs:134: this is a warning message
+2023-03-24 00:54:19 ERROR tests::test_log sd_proxy::tests: src/lib.rs:135: this is an error message
+2023-03-24 00:54:19  INFO tests::test_log example_span: sd_proxy::tests: src/lib.rs:141: this is an info message
+2023-03-24 00:54:19  WARN tests::test_log example_span: sd_proxy::tests: src/lib.rs:142: this is a warning message
+2023-03-24 00:54:19 ERROR tests::test_log example_span: sd_proxy::tests: src/lib.rs:143: this is an error message
+2023-03-24 01:02:43  INFO tests::test_log sd_proxy::tests: src/lib.rs:133: this is an info message
+2023-03-24 01:02:43  WARN tests::test_log sd_proxy::tests: src/lib.rs:134: this is a warning message
+2023-03-24 01:02:43 ERROR tests::test_log sd_proxy::tests: src/lib.rs:135: this is an error message
+2023-03-24 01:02:43  INFO tests::test_log sd_proxy::tests: src/lib.rs:141: this is an info message
+2023-03-24 01:02:43  WARN tests::test_log sd_proxy::tests: src/lib.rs:142: this is a warning message
+2023-03-24 01:02:43 ERROR tests::test_log sd_proxy::tests: src/lib.rs:143: this is an error message
+2023-03-24 01:02:58  INFO tests::test_log sd_proxy::tests: src/lib.rs:133: this is an info message
+2023-03-24 01:02:58  WARN tests::test_log sd_proxy::tests: src/lib.rs:134: this is a warning message
+2023-03-24 01:02:58 ERROR tests::test_log sd_proxy::tests: src/lib.rs:135: this is an error message
+2023-03-24 01:02:58  INFO tests::test_log sd_proxy::tests: src/lib.rs:141: this is an info message
+2023-03-24 01:02:58  WARN tests::test_log sd_proxy::tests: src/lib.rs:142: this is a warning message
+2023-03-24 01:02:58 ERROR tests::test_log sd_proxy::tests: src/lib.rs:143: this is an error message
+2023-03-24 01:03:15  INFO tests::test_log sd_proxy::tests: src/lib.rs:133: this is an info message
+2023-03-24 01:03:15  WARN tests::test_log sd_proxy::tests: src/lib.rs:134: this is a warning message
+2023-03-24 01:03:15 ERROR tests::test_log sd_proxy::tests: src/lib.rs:135: this is an error message
+2023-03-24 01:03:15  INFO tests::test_log sd_proxy::tests: src/lib.rs:141: this is an info message
+2023-03-24 01:03:15  WARN tests::test_log sd_proxy::tests: src/lib.rs:142: this is a warning message
+2023-03-24 01:03:15 ERROR tests::test_log sd_proxy::tests: src/lib.rs:143: this is an error message
+2023-03-24 01:06:29  INFO tests::test_log sd_proxy::tests: src/lib.rs:133: this is an info message
+2023-03-24 01:06:29  WARN tests::test_log sd_proxy::tests: src/lib.rs:134: this is a warning message
+2023-03-24 01:06:29 ERROR tests::test_log sd_proxy::tests: src/lib.rs:135: this is an error message
+2023-03-24 01:06:29  INFO tests::test_log example_span: sd_proxy::tests: src/lib.rs:141: this is an info message asdwa
+2023-03-24 01:06:29  WARN tests::test_log example_span: sd_proxy::tests: src/lib.rs:142: this is a warning message asdwa
+2023-03-24 01:06:29 ERROR tests::test_log example_span: sd_proxy::tests: src/lib.rs:143: this is an error message asdwa

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 0
logs/log.2023-03-24


+ 337 - 0
src/backend.rs

@@ -0,0 +1,337 @@
+use crate::{config::Config, Sign};
+use md5::{Digest, Md5};
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[serde(rename_all = "UPPERCASE")]
+pub struct PubMsgBody<C, R> {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub content: Option<C>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub resp: Option<R>,
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[serde(rename_all = "UPPERCASE")]
+pub struct Header {
+    pub version: String,
+    pub timestamp: String,
+    pub seqno: String,
+    pub appid: String,
+    pub secertkey: String,
+}
+
+impl crate::Verify for Header {
+    fn verify(&self, config: &crate::config::Config) -> anyhow::Result<()> {
+        let r = self.sign_var(config);
+        match hex::decode(&self.secertkey) {
+            Ok(o) => {
+                if o == r {
+                    Ok(())
+                } else {
+                    Err(anyhow::Error::msg("sign 不匹配"))
+                }
+            }
+            Err(e) => Err(anyhow::Error::msg(format!("sign 解码异常: {e:#?}"))),
+        }
+    }
+}
+
+impl Sign for Header {
+    fn sign(&mut self, config: &crate::config::Config) {
+        let r = self.sign_var(config);
+        self.secertkey = hex::encode_upper(r);
+    }
+}
+
+impl Header {
+    pub fn sign_var(&self, config: &crate::config::Config) -> Vec<u8> {
+        let str = format!(
+            "{}{}{}{}",
+            self.timestamp, self.seqno, self.appid, config.fscg_config.app_secret
+        );
+
+        let mut aa = Md5::new();
+        aa.update(str);
+
+        aa.finalize().to_vec()
+    }
+}
+
+impl Default for Header {
+    fn default() -> Self {
+        let time_format = time::macros::format_description!("[year][month padding:zero][day padding:zero][hour padding:zero][minute padding:zero][second padding:zero][subsecond digits:1+]");
+        let offset = time::UtcOffset::from_hms(8, 0, 0).unwrap();
+        let now = time::OffsetDateTime::now_utc().to_offset(offset);
+        let timestamp = now.format(time_format).unwrap();
+
+        Self {
+            version: "V1.1".to_string(),
+            timestamp,
+            seqno: now.unix_timestamp_nanos().to_string(),
+            appid: Default::default(),
+            secertkey: Default::default(),
+        }
+    }
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[serde(rename_all = "UPPERCASE")]
+pub struct PayReqContent {
+    pub sign: String,
+    pub user: String,
+    pub packageid: String,
+    pub ordertype: isize,
+    pub extorder: String,
+    pub r#type: Option<String>,
+    pub note: Option<String>,
+}
+
+impl Default for PayReqContent {
+    fn default() -> Self {
+        Self {
+            sign: Default::default(),
+            user: Default::default(),
+            packageid: Default::default(),
+            ordertype: 3,
+            extorder: Default::default(),
+            r#type: None,
+            note: None,
+        }
+    }
+}
+
+impl Sign for PayReqContent {
+    fn sign(&mut self, config: &crate::config::Config) {
+        let r = self.sign_var(config);
+        self.sign = hex::encode_upper(r)
+    }
+}
+
+impl PayReqContent {
+    pub fn sign_var(&self, config: &crate::config::Config) -> Vec<u8> {
+        let str = format!(
+            "{}{}{}{}{}",
+            config.fscg_config.app_secret, self.user, self.packageid, self.ordertype, self.extorder
+        );
+        let mut aa = Md5::new();
+        aa.update(str);
+
+        aa.finalize().to_vec()
+    }
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[serde(rename_all = "UPPERCASE")]
+pub struct PayRespContent {
+    pub orderid: String,
+    pub extorder: String,
+}
+
+impl crate::Verify for PayRespContent {}
+
+pub type PayReqBody = PubMsgBody<PayReqContent, ()>;
+
+impl Sign for PayReqBody {
+    fn sign(&mut self, config: &crate::config::Config) {
+        self.content.sign(config)
+    }
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[serde(rename_all = "UPPERCASE")]
+pub struct RespResp {
+    pub rcode: String,
+    pub rmsg: Option<String>,
+}
+
+pub type PubRespBody<T> = PubMsgBody<T, RespResp>;
+
+pub type PayRespBody = PubRespBody<PayRespContent>;
+
+impl<T> crate::Verify for PubRespBody<T>
+where
+    T: crate::Verify,
+{
+    fn verify(&self, config: &crate::config::Config) -> anyhow::Result<()> {
+        self.content
+            .as_ref()
+            .map(move |o| o.verify(config))
+            .unwrap_or(Ok(()))
+    }
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[serde(rename_all = "UPPERCASE")]
+pub struct SearchReqContent {
+    pub sign: String,
+    pub orderid: Option<String>,
+    pub extorderid: Option<String>,
+}
+
+pub type SearchReqBody = PubMsgBody<SearchReqContent, ()>;
+
+impl crate::Sign for SearchReqBody {
+    fn sign(&mut self, config: &crate::config::Config) {
+        self.content.sign(config)
+    }
+}
+
+impl Default for SearchReqContent {
+    fn default() -> Self {
+        Self {
+            sign: "".to_string(),
+            orderid: None,
+            extorderid: None,
+        }
+    }
+}
+
+impl SearchReqContent {
+    pub fn sign_var(&self, config: &crate::config::Config) -> Vec<u8> {
+        let str = format!(
+            "{}{}{}",
+            config.fscg_config.app_secret,
+            self.orderid.as_deref().unwrap_or(""),
+            self.extorderid.as_deref().unwrap_or(""),
+        );
+        let mut aa = Md5::new();
+        aa.update(str);
+
+        aa.finalize().to_vec()
+    }
+}
+
+impl crate::Sign for SearchReqContent {
+    fn sign(&mut self, config: &crate::config::Config) {
+        let r = self.sign_var(config);
+        self.sign = hex::encode_upper(r)
+    }
+}
+
+impl crate::Verify for SearchReqContent {
+    fn verify(&self, config: &crate::config::Config) -> anyhow::Result<()> {
+        let r = self.sign_var(config);
+        match hex::decode(&self.sign) {
+            Ok(o) => {
+                if o == r {
+                    Ok(())
+                } else {
+                    Err(anyhow::Error::msg("sign 不匹配"))
+                }
+            }
+            Err(e) => Err(anyhow::Error::msg(format!("sign 解码异常: {e:#?}"))),
+        }
+    }
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[serde(rename_all = "UPPERCASE")]
+pub struct SearchRespContent {
+    pub orderid: String,
+    pub ordertype: Option<isize>,
+    pub user: String,
+    pub reqdate: String,
+    pub packageid: String,
+    pub status: String,
+    pub extorder: String,
+    pub msg: String,
+    pub price: String,
+    pub checktime: String,
+    pub attr1: Option<String>,
+    pub attr2: Option<String>,
+    pub attr3: Option<String>,
+}
+
+impl crate::Verify for SearchRespContent {}
+
+pub type SearchRespBody = PubRespBody<SearchRespContent>;
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[serde(rename_all = "UPPERCASE")]
+pub struct Body<T> {
+    pub header: Header,
+    pub msgbody: T,
+}
+
+impl<T> Sign for Body<T>
+where
+    T: Sign,
+{
+    fn sign(&mut self, config: &crate::config::Config) {
+        self.header.sign(config);
+        self.msgbody.sign(config);
+    }
+}
+impl<T> crate::Verify for Body<T>
+where
+    T: crate::Verify,
+{
+    fn verify(&self, config: &crate::config::Config) -> anyhow::Result<()> {
+        self.header
+            .verify(config)
+            .and_then(|_| self.msgbody.verify(config))
+    }
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[serde(rename_all = "UPPERCASE")]
+pub struct AccountReqContent {
+    sign: String,
+    timestamp: String,
+}
+
+impl AccountReqContent {
+    pub fn sign_var(&self, config: &crate::config::Config) -> Vec<u8> {
+        let str = format!("{}{}", config.fscg_config.app_secret, self.timestamp);
+        let mut aa = Md5::new();
+        aa.update(str);
+
+        aa.finalize().to_vec()
+    }
+}
+impl crate::Sign for AccountReqContent {
+    fn sign(&mut self, config: &crate::config::Config) {
+        let r = self.sign_var(config);
+        self.sign = hex::encode_upper(r)
+    }
+}
+
+pub type AccountReqBody = PubMsgBody<AccountReqContent, ()>;
+
+impl crate::Sign for AccountReqBody {
+    fn sign(&mut self, config: &Config) {
+        self.content.sign(config)
+    }
+}
+
+impl AccountReqBody {
+    pub fn new() -> Self {
+        let offset = time::UtcOffset::from_hms(8, 0, 0).unwrap();
+        let now = time::OffsetDateTime::now_utc().to_offset(offset);
+        let content = AccountReqContent {
+            sign: "".to_string(),
+            timestamp: now.unix_timestamp().to_string(),
+        };
+        Self {
+            content: Some(content),
+            resp: None,
+        }
+    }
+}
+
+impl Default for AccountReqBody {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[serde(rename_all = "UPPERCASE")]
+pub struct AccountRespContent {
+    pub balance: String,
+    pub status: String,
+}
+
+pub type AccountRespBody = PubMsgBody<AccountRespContent, RespResp>;
+
+impl crate::Verify for AccountRespBody{}

+ 112 - 0
src/config.rs

@@ -0,0 +1,112 @@
+use crate::gmssl::sm2::{SM2_KEY, SM2_POINT};
+
+#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct SdConfig {
+    pub public_key: String,
+    pub private_key: String,
+    pub sin_key: String,
+    pub supplier_code: String,
+    pub callback_url: reqwest::Url,
+}
+
+impl SdConfig {
+    pub fn new() -> Self {
+        Default::default()
+    }
+
+    pub fn get_pk(&self) -> anyhow::Result<SM2_KEY> {
+        unsafe {
+            let mut key: SM2_KEY = std::mem::zeroed();
+            let public_key = <SM2_POINT as TryFrom<&str>>::try_from(&self.public_key)?;
+            key.set_pk(public_key);
+            Ok(key)
+        }
+    }
+    pub fn get_sk(&self) -> anyhow::Result<SM2_KEY> {
+        unsafe {
+            let mut key: SM2_KEY = std::mem::zeroed();
+
+            key.set_sk(&self.private_key);
+            Ok(key)
+        }
+    }
+}
+
+impl Default for SdConfig {
+    fn default() -> Self {
+        let key = SM2_KEY::key_generate();
+
+        Self {
+            public_key: key.get_pk(false),
+            private_key: key.get_sk(),
+            sin_key: "ZctLTt2tugU2ntZdVgMXssrGuENPH6V7b7ROYN88Msw=".to_string(),
+            supplier_code: "CESHI".to_string(),
+            callback_url: reqwest::Url::parse("https://api-test.schengle.com/gateway").unwrap(),
+        }
+    }
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct FscgConfig {
+    pub pay_url: reqwest::Url,
+    pub search_url: reqwest::Url,
+    pub balance_url: reqwest::Url,
+    pub app_id: String,
+    pub app_secret: String,
+}
+
+impl Default for FscgConfig {
+    fn default() -> Self {
+        Self {
+            app_id: "som app_id".to_string(),
+            app_secret: "som app_secret".to_string(),
+            balance_url: reqwest::Url::parse(
+                "http://192.144.212.211:36000/gateway/flowservice/queryAccount.ws",
+            )
+            .unwrap(),
+            search_url: reqwest::Url::parse(
+                "http://192.144.212.211:36000/gateway/flowservice/queryorder.ws",
+            )
+            .unwrap(),
+            pay_url: reqwest::Url::parse(
+                "http://192.144.212.211:36000/gateway/flowservice/makeorder.ws",
+            )
+            .unwrap(),
+        }
+    }
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct Config {
+    pub sd_config: SdConfig,
+    pub addr: std::net::SocketAddr,
+    pub timeout: u64,
+    pub fscg_config: FscgConfig,
+}
+
+impl Default for Config {
+    fn default() -> Self {
+        Self {
+            sd_config: Default::default(),
+            addr: "0.0.0.0:8848".parse().unwrap(),
+            timeout: Default::default(),
+            fscg_config: Default::default(),
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_name() {
+        let config = SdConfig::default();
+        let r = config.get_pk().unwrap();
+        // let r = r.encrypt("sdgasfawerashfrgLLLLLLLLLLLLLLLLLLLLLLLJFB;ljebfffffffffffffffffffffffffffffffffffffffffffsdqwdadfwefdf");
+        println!("{r:?}")
+    }
+}

+ 127 - 0
src/extract.rs

@@ -0,0 +1,127 @@
+use std::sync::Arc;
+
+use axum::{
+    body::HttpBody,
+    extract::{FromRequest, FromRequestParts},
+    response::IntoResponse,
+    BoxError, Json,
+};
+use hyper::Request;
+use serde::{de::DeserializeOwned, Serialize};
+
+use crate::{config::Config, sd_model};
+
+/// 盛大请求 验证,解密
+#[derive(Debug)]
+pub struct SdReqExtract<T> {
+    pub head: sd_model::PubReqHead,
+    pub data: T,
+}
+
+#[axum::async_trait]
+
+impl<T, S, B> FromRequest<S, B> for SdReqExtract<T>
+where
+    T: DeserializeOwned,
+    T: sd_model::TryFromPubHeadConfig + crate::Verify,
+    B: HttpBody + Send + 'static,
+    B::Data: Send,
+    B::Error: Into<BoxError>,
+    S: Send + Sync,
+{
+    type Rejection = sd_model::PubRespHead;
+    async fn from_request(req: Request<B>, state: &S) -> Result<Self, Self::Rejection> {
+        let (mut parts, body) = req.into_parts();
+
+        let mut err_result = sd_model::PubRespHead {
+            code: "999".to_string(),
+            msg: "解析Request头异常".to_string(),
+            ..Default::default()
+        };
+
+        let config = axum::Extension::<Arc<Config>>::from_request_parts(&mut parts, state).await;
+        let config = match config {
+            Ok(o) => o,
+            Err(e) => {
+                let msg = format!("获取配置错误: {}", e.body_text());
+                tracing::error!("{}", msg);
+                err_result.msg = msg;
+                return Err(err_result);
+            }
+        };
+        let req = Request::from_parts(parts, body);
+
+        let Json(head) = match Json::<sd_model::PubReqHead>::from_request(req, state).await {
+            Ok(o) => o,
+            Err(e) => {
+                let msg = format!("Json 转换错误: {}", e.body_text());
+                tracing::error!("{}", msg);
+                err_result.msg = msg;
+                return Err(err_result);
+            }
+        };
+
+        use crate::Verify;
+        if let Err(e) = head.verify(&config) {
+            let e = e.context("Proxy verify PubReqHead Error");
+            let msg = format!("{:#?}", e);
+            tracing::error!("{}", msg);
+            err_result.msg = msg;
+            err_result.sign(&config);
+            return Err(err_result);
+        }
+
+        let data = match T::try_from_pub_req_head_config(&head, &config) {
+            Ok(o) => o,
+            Err(e) => {
+                let msg = format!("类型转换错误: {:#?}", e);
+                tracing::error!("{}", msg);
+                err_result.msg = msg;
+                err_result.sign(&config);
+
+                return Err(err_result);
+            }
+        };
+        if let Err(e) = data.verify(&config) {
+            let msg = format!("Proxy Verify Body Error: {:#?}", e);
+            tracing::error!("{}", msg);
+            err_result.msg = msg;
+            err_result.sign(&config);
+
+            return Err(err_result);
+        }
+
+        Ok(Self { head, data })
+    }
+}
+
+
+/// 盛大返回, 加签,加密
+#[derive(Debug)]
+pub struct SdRespExtract<T> {
+    pub data: T,
+    pub request_id: String,
+    pub config: Arc<Config>,
+}
+
+impl<T> IntoResponse for SdRespExtract<T>
+where
+    T: Serialize + crate::Sign,
+{
+    fn into_response(mut self) -> axum::response::Response {
+        let mut result = sd_model::PubRespHead {
+            request_id: self.request_id,
+            ..Default::default()
+        };
+        self.data.sign(&self.config);
+        let data = match serde_json::to_string(&self.data) {
+            Ok(o) => o,
+            Err(e) => {
+                format!("serde_json::to_string Error: {}", e)
+            }
+        };
+        result.data = data;
+        result.sign(&self.config);
+        Json(result).into_response()
+    }
+}

+ 306 - 0
src/gmssl.rs

@@ -0,0 +1,306 @@
+//! GmSSL FFI 绑定
+//!
+//!
+//!
+
+// #[allow()]
+pub mod sm2 {
+    use std::fmt::Display;
+
+    use anyhow::Context;
+
+    #[repr(C)]
+    #[derive(Debug)]
+
+    pub struct SM2_POINT {
+        pub x: [u8; 32],
+        pub y: [u8; 32],
+    }
+
+    impl Display for SM2_POINT {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            unsafe {
+                let mut out = [0; 65];
+                sm2_point_to_uncompressed_octets(self, &mut out);
+                let s = hex::encode(out);
+                f.write_str(&s)
+            }
+        }
+    }
+
+    impl TryFrom<&[u8]> for SM2_POINT {
+        type Error = anyhow::Error;
+
+        fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
+            unsafe {
+                let mut result = std::mem::zeroed();
+                let r = sm2_point_from_octets(&mut result, value.as_ptr(), value.len());
+                if r == 1 {
+                    Ok(result)
+                } else {
+                    Err(anyhow::Error::msg(format!(
+                        "FFI sm2_point_from_octets Error {}",
+                        r
+                    )))
+                }
+            }
+        }
+    }
+
+    impl TryFrom<&str> for SM2_POINT {
+        type Error = anyhow::Error;
+
+        fn try_from(value: &str) -> Result<Self, Self::Error> {
+            let value = hex::decode(value).context("SM2_POINT try_from str Error")?;
+            TryFrom::<&[u8]>::try_from(&value)
+        }
+    }
+
+    #[repr(C)]
+    #[derive(Debug)]
+    pub struct SM2_KEY {
+        pub public_key: SM2_POINT,
+        pub private_key: [u8; 32],
+    }
+
+    pub enum PassOrder {
+        C1C2C3,
+        C1C3C2,
+    }
+
+    impl Display for SM2_KEY {
+        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+            f.write_fmt(format_args!(
+                "public_key:{}\nprivate_key:{}",
+                self.public_key,
+                hex::encode(self.private_key)
+            ))
+        }
+    }
+
+    impl SM2_KEY {
+        /// Note: 会生成public_key
+        pub fn set_sk(&mut self, s: &str) -> usize {
+            let r = hex::decode(s).unwrap();
+            unsafe { sm2_key_set_private_key(self, r.as_ptr() as _) }
+        }
+
+        /// Note: 会清除private_key
+        pub fn set_pk(&mut self, point: SM2_POINT) -> usize {
+            unsafe { sm2_key_set_public_key(self, &point) }
+        }
+
+        pub fn get_sk(&self) -> String {
+            hex::encode(self.private_key)
+        }
+
+        pub fn get_pk(&self, compressed: bool) -> String {
+            unsafe {
+                let mut out = Vec::with_capacity(65);
+                if compressed {
+                    sm2_point_to_compressed_octets(&self.public_key, out.as_mut_ptr() as _);
+                    out.set_len(33)
+                } else {
+                    sm2_point_to_uncompressed_octets(&self.public_key, out.as_mut_ptr() as _);
+                    out.set_len(65)
+                };
+
+                hex::encode(out)
+            }
+        }
+
+        pub fn key_generate() -> Self {
+            unsafe {
+                let mut result = std::mem::zeroed();
+                let r = sm2_key_generate(&mut result);
+                if r != 1 {
+                    panic!("key_generate Error")
+                }
+                result
+            }
+        }
+
+        pub fn encrypt(&self, data: impl AsRef<[u8]>, order: PassOrder) -> anyhow::Result<Vec<u8>> {
+            unsafe {
+                let data = data.as_ref();
+                let len = data.len();
+                if !(1..=255).contains(&len) {
+                    return Err(anyhow::Error::msg("sm2_encrypt error 数据长度错误"));
+                }
+
+                let mut out = std::mem::zeroed();
+                let ffi_r = sm2_do_encrypt(self, data.as_ptr(), len, &mut out);
+                if ffi_r != 1 {
+                    return Err(anyhow::Error::msg("sm2_do_encrypt error "));
+                }
+                // let point = [0x4]
+                //     .into_iter()
+                //     .chain(out.point.x.iter().copied())
+                //     .chain(out.point.y.iter().copied());
+                let mut point = [0; 33];
+                sm2_point_to_compressed_octets(&out.point, &mut point);
+                let result = match order {
+                    PassOrder::C1C2C3 => point
+                        .into_iter()
+                        .chain(out.ciphertext[..out.ciphertext_size as _].iter().copied())
+                        .chain(out.hash.iter().copied())
+                        .collect(),
+                    PassOrder::C1C3C2 => point
+                        .into_iter()
+                        .chain(out.hash.iter().copied())
+                        .chain(out.ciphertext[..out.ciphertext_size as _].iter().copied())
+                        .collect(),
+                };
+                Ok(result)
+            }
+        }
+
+        pub fn decrypt(&self, data: impl AsRef<[u8]>, order: PassOrder) -> anyhow::Result<Vec<u8>> {
+            unsafe {
+                let mut data = data.as_ref();
+                let (point, pass, hash) = {
+                    let point = {
+                        let size = if data.len() > 33 && (data[0] == 0x3 || data[0] == 0x2) {
+                            33
+                        } else if data.len() > 65 && data[0] == 0x4 {
+                            65
+                        } else if data.len() > 64 {
+                            64
+                        } else {
+                            return Err(anyhow::Error::msg("decrypt C1 value Error"));
+                        };
+                        let mut p = std::mem::zeroed();
+                        let ffi_r = if size == 64 {
+                            let mut _in = Vec::with_capacity(65);
+                            _in.push(0x4);
+                            _in.extend(data[..size].iter());
+                            sm2_point_from_octets(&mut p, _in.as_ptr(), 65)
+                        } else {
+                            sm2_point_from_octets(&mut p, data[..size].as_ptr(), size)
+                        };
+                        if ffi_r != 1 {
+                            return Err(anyhow::Error::msg(format!(
+                                "decrypt Error sm2_point_from_octets: {}",
+                                ffi_r
+                            )));
+                        }
+                        data = &data[size..];
+                        p
+                    };
+
+                    if data.len() < 32 {
+                        return Err(anyhow::Error::msg("decrypt C2/C3 value Error"));
+                    }
+
+                    let (pass, hash) = match order {
+                        PassOrder::C1C2C3 => (&data[..data.len() - 32], &data[data.len() - 32..]),
+                        PassOrder::C1C3C2 => (&data[..32], &data[32..]),
+                    };
+                    (point, pass, hash)
+                };
+                let mut _in: SM2_CIPHERTEXT = std::mem::zeroed();
+                _in.ciphertext_size = pass.len() as _;
+                _in.ciphertext[..pass.len()].clone_from_slice(pass);
+                _in.point = point;
+                _in.hash.clone_from_slice(hash);
+                let mut out = Vec::with_capacity(255);
+                let mut outlen = 0;
+                let ffi_r = sm2_do_decrypt(self, &_in, out.as_mut_ptr(), &mut outlen);
+                if ffi_r != 1 {
+                    return Err(anyhow::Error::msg("sm2_do_decrypt Error"));
+                }
+                out.set_len(outlen);
+                Ok(out)
+            }
+        }
+    }
+
+    #[repr(C)]
+    #[derive(Debug)]
+    pub struct SM2_CIPHERTEXT {
+        pub point: SM2_POINT,
+        pub hash: [u8; 32],
+        pub ciphertext_size: u8,
+        pub ciphertext: [u8; 255],
+    }
+
+    extern "C" {
+        pub fn sm2_key_generate(key: *mut SM2_KEY) -> isize;
+        pub fn sm2_encrypt(
+            key: *const SM2_KEY,
+            _in: *const u8,
+            inlen: usize,
+            out: *mut u8,
+            outlen: *mut usize,
+        ) -> isize;
+        pub fn sm2_decrypt(
+            key: *const SM2_KEY,
+            _in: *const u8,
+            inlen: usize,
+            out: *mut u8,
+            outlen: *mut usize,
+        ) -> isize;
+        pub fn sm2_key_set_private_key(key: *mut SM2_KEY, private_key: *const [u8; 32]) -> usize;
+        pub fn sm2_key_set_public_key(key: *mut SM2_KEY, point: *const SM2_POINT) -> usize;
+        /// Note: 源码中未使用 _gmssl_export
+        pub fn sm2_do_encrypt(
+            key: *const SM2_KEY,
+            _in: *const u8,
+            inlen: usize,
+            out: *mut SM2_CIPHERTEXT,
+        ) -> usize;
+        /// Note: 源码中未使用 _gmssl_export
+        pub fn sm2_do_decrypt(
+            key: *const SM2_KEY,
+            _in: *const SM2_CIPHERTEXT,
+            out: *mut u8,
+            outlen: *mut usize,
+        ) -> usize;
+        /// Note: 源码中未使用 _gmssl_export
+        fn sm2_point_from_octets(p: *mut SM2_POINT, _in: *const u8, inlen: usize) -> usize;
+        /// Note: 源码中未使用 _gmssl_export
+        fn sm2_point_to_compressed_octets(p: *const SM2_POINT, out: *mut [u8; 33]);
+        /// Note: 源码中未使用 _gmssl_export
+        fn sm2_point_to_uncompressed_octets(p: *const SM2_POINT, out: *mut [u8; 65]);
+    }
+
+    #[cfg(test)]
+    mod tests {
+        use base64ct::Encoding;
+
+        use super::*;
+
+        #[test]
+        fn test_key_generate() {
+            let key = SM2_KEY::key_generate();
+            let pk = key.get_pk(false);
+            let sk = key.get_sk();
+            println!("\npk: {pk}\nsk: {sk}")
+        }
+
+        #[test]
+        fn test_en() {
+            let key = SM2_KEY::key_generate();
+
+            println!("key {key}");
+            let data = b"hello";
+            let r = key.encrypt(data, PassOrder::C1C2C3).unwrap();
+            use base64ct::Base64;
+            let s = Base64::encode_string(&r);
+            println!("pass: {}", s);
+        }
+        #[test]
+        fn feature() {
+            unsafe {
+                let mut key: SM2_KEY = std::mem::zeroed();
+                let r =
+                    hex::decode("4bee5485e155449d6ccc325806da8d216aa36a343722b1487dca752d423ea68e")
+                        .unwrap();
+                key.private_key.clone_from_slice(&r);
+                let pass = base64ct::Base64::decode_vec("Ape+C8i8OmF9UUQSCqHscpY/kdVyFgMZbZe94dddWDvZy2xBEMl6e2X36TN8VV4DbXflXncIyyLLZlKsDx+kixQf/Ln5Kg==").unwrap();
+                let r = key.decrypt(pass, PassOrder::C1C2C3).unwrap();
+                assert_eq!(r, b"hello")
+            }
+        }
+    }
+}

+ 175 - 0
src/lib.rs

@@ -0,0 +1,175 @@
+use std::{fs::read_to_string, io, sync::Arc, time::Duration};
+
+use axum::{error_handling::HandleErrorLayer, BoxError};
+use config::Config;
+use hyper::StatusCode;
+use time::UtcOffset;
+use tower::ServiceBuilder;
+// use tracing::log::LevelFilter;
+use tracing_subscriber::{filter::LevelFilter, fmt::time::OffsetTime, prelude::*};
+
+pub mod config;
+pub mod gmssl;
+pub mod router;
+
+pub mod backend;
+pub mod extract;
+pub mod sd_model;
+static APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
+
+pub async fn init_router(config: Config) -> axum::Router {
+    let req = reqwest::Client::builder()
+        .user_agent(APP_USER_AGENT)
+        .deflate(true)
+        .gzip(true)
+        .brotli(true)
+        .timeout(Duration::from_millis(config.timeout))
+        .no_proxy()
+        .trust_dns(true)
+        .build()
+        .unwrap();
+    let layer = ServiceBuilder::new()
+        .layer(HandleErrorLayer::new(handle_timeout_error))
+        .timeout(Duration::from_millis(config.timeout));
+
+    crate::router::init_route()
+        .await
+        .layer(axum::Extension(req))
+        .layer(axum::Extension(Arc::new(config)))
+        .layer(layer)
+}
+
+async fn handle_timeout_error(err: BoxError) -> (StatusCode, String) {
+    if err.is::<tower::timeout::error::Elapsed>() {
+        (StatusCode::REQUEST_TIMEOUT, "timeout".to_string())
+    } else {
+        (StatusCode::INTERNAL_SERVER_ERROR, format!("error: {err:?}"))
+    }
+}
+
+pub fn load_config() -> config::Config {
+    let path = if cfg!(not(debug_assertions)) {
+        "config.toml"
+    } else {
+        "config-test.toml"
+    };
+    println!("{path}");
+    let config = read_to_string(path).expect("读取配置文件错误");
+    toml::from_str(&config).expect("解析配置文件错误")
+}
+
+pub fn init_tracing() {
+    let time_format = time::macros::format_description!(
+        "[year]-[month padding:zero]-[day padding:zero] [hour padding:zero]:[minute padding:zero]:[second padding:zero]"
+    );
+    let offset = UtcOffset::from_hms(8, 0, 0).unwrap();
+    let local_time = OffsetTime::new(offset, time_format);
+
+    let writer = tracing_appender::rolling::daily("logs", "log");
+
+    let fmt = tracing_subscriber::fmt::format()
+        .with_timer(local_time)
+        .with_level(true)
+        .with_file(true)
+        .with_line_number(true)
+        .with_target(true)
+        // .with_thread_ids(true)
+        .with_thread_names(true)
+        .compact();
+
+    let log_file = tracing_subscriber::fmt::layer()
+        .event_format(fmt.clone())
+        .with_writer(writer)
+        .with_ansi(false)
+        .with_filter(LevelFilter::INFO);
+
+    let log_stdout = tracing_subscriber::fmt::layer()
+        .event_format(fmt)
+        .with_ansi(true)
+        .with_writer(io::stdout)
+        .with_filter(LevelFilter::DEBUG);
+
+    tracing_subscriber::registry()
+        .with(log_file)
+        .with(log_stdout)
+        .init();
+}
+
+pub trait Sign {
+    fn sign(&mut self, config: &Config);
+}
+
+impl<T> Sign for Option<T>
+where
+    T: Sign,
+{
+    fn sign(&mut self, config: &Config) {
+        if let Some(s) = self {
+            s.sign(config)
+        }
+    }
+}
+
+#[allow(unused)]
+pub trait Verify {
+    fn verify(&self, config: &Config) -> anyhow::Result<()> {
+        Ok(())
+    }
+}
+
+impl<T> Verify for Option<T>
+where
+    T: Verify,
+{
+    fn verify(&self, config: &Config) -> anyhow::Result<()> {
+        if let Some(v) = self {
+            v.verify(config)
+        } else {
+            Ok(())
+        }
+    }
+}
+
+#[cfg(test)]
+#[allow(unused)]
+mod tests {
+    use tracing_subscriber::{filter::LevelFilter, Registry};
+
+    use super::*;
+
+    #[test]
+    fn test_name() {
+        load_config();
+    }
+
+    #[derive(Debug)]
+    struct TestStruct {
+        fild: String,
+        fild2: String,
+    }
+
+    #[test]
+    fn test_log() {
+        init_tracing();
+        // let arg = TestStruct {
+        //     fild: "fild".to_string(),
+        //     fild2: "fild2".to_string(),
+        // };
+        // let span = tracing::span!(tracing::Level::WARN, "HandlerPay", arg=%arg.fild);
+        // let _g = span.enter();
+        tracing::trace!("this is a trace message");
+        tracing::debug!("this is a debug message");
+        tracing::info!("this is an info message");
+        tracing::warn!("this is a warning message");
+        tracing::error!("this is an error message");
+        let span = tracing::span!(tracing::Level::ERROR, "example_span", "asdwa");
+        let guard = span.enter();
+
+        tracing::trace!("this is a trace message");
+        tracing::debug!("this is a debug message");
+        tracing::info!("this is an info message");
+        tracing::warn!("this is a warning message");
+        tracing::error!("this is an error message");
+        drop(guard)
+    }
+}

+ 65 - 1
src/main.rs

@@ -1,3 +1,67 @@
+use std::{
+    sync::atomic::{AtomicUsize, Ordering},
+    time::Duration,
+};
+use tracing::Instrument;
 fn main() {
-    println!("Hello, world!");
+    sd_proxy::init_tracing();
+    let config = sd_proxy::load_config();
+
+    tokio::runtime::Builder::new_multi_thread()
+        .enable_all()
+        .thread_keep_alive(Duration::from_secs(600)) // 10分钟
+        // .event_interval(61)
+        // .global_queue_interval(30)
+        .thread_name_fn(|| {
+            static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0);
+            let id = ATOMIC_ID.fetch_add(1, Ordering::SeqCst);
+            format!("TokioRT-{}", id)
+        })
+        .build()
+        .unwrap()
+        .block_on(async {
+            let addr = config.addr;
+
+            let route = sd_proxy::init_router(config)
+                .instrument(tracing::span!(tracing::Level::TRACE, "Init Router"))
+                .await;
+
+            let serve = route.into_make_service_with_connect_info::<std::net::SocketAddr>();
+            tracing::info!("Server addr: {}", addr);
+            axum::Server::bind(&addr)
+                .serve(serve)
+                .instrument(tracing::span!(tracing::Level::INFO, "Service"))
+                .await
+                .expect("server failed");
+        });
+}
+
+#[cfg(test)]
+mod tests {
+    use sd_proxy::gmssl::sm2::*;
+
+    #[test]
+    fn test_name() {
+        let private_key = "244889352ab13f8601a9871ef47dc2a694bd293f8ae156538aea036d07f47729";
+        let public_key = "03a937d952388700a18e1d4c69fba338fa507440c4d710a72be80a87269cb1045f";
+        let private_key = hex::decode(private_key).unwrap();
+        let public_key = hex::decode(public_key).unwrap();
+        println!(
+            "private_key: {}, public_key:{}",
+            private_key.len(),
+            public_key.len()
+        );
+
+        unsafe {
+            let mut arg = std::mem::zeroed::<SM2_KEY>();
+            arg.private_key.clone_from_slice(&private_key);
+            arg.public_key = TryFrom::<&[u8]>::try_from(&public_key).unwrap();
+            let str = b"Hello";
+            let mut out = [0_u8; 255];
+            let mut outlen = 0;
+            let r = sm2_encrypt(&arg, str.as_ptr(), str.len(), out.as_mut_ptr(), &mut outlen);
+
+            println!("{r}: {:?}", &out[..outlen])
+        }
+    }
 }

+ 347 - 0
src/router.rs

@@ -0,0 +1,347 @@
+pub async fn init_route() -> axum::Router {
+    axum::Router::new()
+        .route("/pay", axum::routing::post(pay::route))
+        .route("/search", axum::routing::post(search::route))
+        .route("/balance", axum::routing::post(balance::route))
+        .route("/callback", axum::routing::post(callback::route))
+}
+/// 盛大下单
+pub(crate) mod pay {
+    use std::sync::Arc;
+
+    use anyhow::Context;
+    use axum::Extension;
+    use tracing::Instrument;
+
+    use crate::{backend, sd_model};
+
+    #[tracing::instrument(name="HandlerPay", ret(Debug), fields(privateKey=config.sd_config.private_key, Request=?data), skip_all )]
+    pub(crate) async fn route(
+        Extension(session): Extension<reqwest::Client>,
+        Extension(config): Extension<Arc<crate::config::Config>>,
+        data: crate::extract::SdReqExtract<sd_model::PayReqBody>,
+    ) -> crate::extract::SdRespExtract<sd_model::PayRespBody> {
+        let crate::extract::SdReqExtract { head, data } = data;
+
+        let backend_req = data.into_backend_pay_req(&head, &config);
+
+        // 请求服务器
+        let backend_resp = match session
+            .post(config.fscg_config.pay_url.clone())
+            .json(&backend_req)
+            .send()
+            .instrument(tracing::info_span!("BackendClient", BackendRequest=?backend_req,url=%config.fscg_config.pay_url))
+            .await
+            .context("链接")
+        {
+            Ok(o) => o,
+            Err(e) => {
+                let response = sd_model::PubRespBody::error("99", format!("请求后端服务器错误: {:#?}", e));
+                tracing::error!(
+                    url=%config.fscg_config.pay_url,
+                    BackendRequest=?backend_req,
+                    Response=?response,
+                    "{}", response.result_desc
+                );
+
+                return crate::extract::SdRespExtract{ data: response, request_id: head.request_id, config };
+            }
+        };
+        // Http 状态检测
+        let status = backend_resp.status();
+        if !status.is_success() {
+            let response =
+                sd_model::PubRespBody::error("99", format!("Backend Http Status: {}", status));
+            let data = serde_json::to_string(&response).unwrap();
+            tracing::error!(
+                BackendStatus = %status,
+                Response = data,
+                "Backend Http Status Error"
+            );
+            return crate::extract::SdRespExtract {
+                data: response,
+                request_id: head.request_id,
+                config,
+            };
+        }
+
+        // 读取数据
+        let backend_data = match backend_resp
+            .text()
+            .instrument(tracing::info_span!("BackendRead"))
+            .await
+        {
+            Ok(o) => o,
+            Err(e) => {
+                let response =
+                    sd_model::PubRespBody::error("99", format!("读取Response: {:#?}", e));
+                // let data = serde_json::to_string(&response).unwrap();
+                tracing::error!(Response = ?response, "{}", response.result_desc);
+                return crate::extract::SdRespExtract {
+                    data: response,
+                    request_id: head.request_id,
+                    config,
+                };
+            }
+        };
+
+        // 解析后端数据
+        let resp = match serde_json::from_str::<backend::Body<backend::PayRespBody>>(&backend_data)
+        {
+            Ok(o) => o,
+            Err(e) => {
+                let response = sd_model::PubRespBody::error("99", format!("解析JSON错误: {:?}", e));
+                tracing::error!(
+                    BackendResponse = backend_data,
+                    Response = ?response,
+                    "{}",
+                    response.result_desc
+                );
+                return crate::extract::SdRespExtract {
+                    data: response,
+                    request_id: head.request_id,
+                    config,
+                };
+            }
+        };
+
+        let result = sd_model::PayRespBody::from_backend_pay_resp(&config, resp, &data);
+        crate::extract::SdRespExtract {
+            data: result,
+            request_id: head.request_id,
+            config,
+        }
+    }
+}
+
+/// 盛大订单查询
+pub(crate) mod search {
+    use std::sync::Arc;
+
+    use anyhow::Context;
+    use axum::Extension;
+    use tracing::Instrument;
+
+    use crate::{backend, sd_model};
+
+    #[tracing::instrument(name="HandlerSearch", ret(Debug), fields(privateKey=config.sd_config.private_key, Request=?data), skip_all )]
+    pub async fn route(
+        Extension(session): Extension<reqwest::Client>,
+        Extension(config): Extension<Arc<crate::config::Config>>,
+        data: crate::extract::SdReqExtract<sd_model::SearchReqBody>,
+    ) -> crate::extract::SdRespExtract<sd_model::SearchRespBody> {
+        let crate::extract::SdReqExtract { head, data } = data;
+        let backend_req = data.into_backend_search_req(&head, &config);
+
+        let backend_resp = match session
+            .post(config.fscg_config.search_url.clone())
+            .json(&backend_req)
+            .send()
+            .instrument(tracing::info_span!("BackendClient", BackendRequest=?backend_req,url=%config.fscg_config.pay_url))
+            .await
+            .context("链接")
+        {
+            Ok(o) => o,
+            Err(e) => {
+                let response = sd_model::SearchRespBody::error("99", format!("请求后端服务器错误: {:#?}", e));
+                tracing::error!(
+                    url=%config.fscg_config.pay_url,
+                    BackendRequest=?backend_req,
+                    Response=?response,
+                    "{}", response.result_desc
+                );
+                return crate::extract::SdRespExtract{ data: response, request_id: head.request_id, config };
+            }
+        };
+
+        let status = backend_resp.status();
+        if !status.is_success() {
+            let response =
+                sd_model::SearchRespBody::error("99", format!("Backend Http Status: {}", status));
+            tracing::error!(
+                BackendStatus = %status,
+                Response = ?response,
+                "Backend Http Status Error"
+            );
+            return crate::extract::SdRespExtract {
+                data: response,
+                request_id: head.request_id,
+                config,
+            };
+        }
+
+        let backend_data = match backend_resp
+            .text()
+            .instrument(tracing::info_span!("BackendRead"))
+            .await
+        {
+            Ok(o) => o,
+            Err(e) => {
+                let response =
+                    sd_model::SearchRespBody::error("99", format!("读取Response: {:#?}", e));
+                tracing::error!(Response = ?response, "{}", response.result_desc);
+                return crate::extract::SdRespExtract {
+                    data: response,
+                    request_id: head.request_id,
+                    config,
+                };
+            }
+        };
+
+        let resp =
+            match serde_json::from_str::<backend::Body<backend::SearchRespBody>>(&backend_data) {
+                Ok(o) => o,
+                Err(e) => {
+                    let response =
+                        sd_model::SearchRespBody::error("99", format!("解析JSON错误: {:?}", e));
+                    tracing::error!(
+                        BackendResponse = backend_data,
+                        Response = ?response,
+                        "{}",
+                        response.result_desc
+                    );
+                    return crate::extract::SdRespExtract {
+                        data: response,
+                        request_id: head.request_id,
+                        config,
+                    };
+                }
+            };
+
+        let response = sd_model::SearchRespBody::from_backend_search_resp(&config, resp);
+
+        crate::extract::SdRespExtract {
+            data: response,
+            request_id: head.request_id,
+            config,
+        }
+    }
+}
+/// 盛大余额查询
+pub(crate) mod balance {
+    use std::sync::Arc;
+
+    use anyhow::Context;
+    use axum::Extension;
+    use tracing::Instrument;
+
+    use crate::{backend, sd_model, Sign};
+
+    // #[tracing::instrument(name="HandlerBalance", ret(Debug), fields(privateKey=config.sd_config.private_key, Request=?req), skip_all )]
+    pub async fn route(
+        Extension(session): Extension<reqwest::Client>,
+        Extension(config): Extension<Arc<crate::config::Config>>,
+        data: crate::extract::SdReqExtract<sd_model::BalanceReqBody>,
+    ) -> crate::extract::SdRespExtract<sd_model::BalanceRespBody> {
+        let crate::extract::SdReqExtract { head, data: _data } = data;
+
+        let backend_req = backend::AccountReqBody::new();
+        let mut backend_req = backend::Body {
+            header: backend::Header {
+                appid: config.fscg_config.app_id.clone(),
+                seqno: head.request_id.clone(),
+                ..Default::default()
+            },
+            msgbody: backend_req,
+        };
+        backend_req.sign(&config);
+
+        let backend_resp = match session
+            .post(config.fscg_config.search_url.clone())
+            .json(&backend_req)
+            .send()
+            .instrument(tracing::info_span!("BackendClient", BackendRequest=?backend_req,url=%config.fscg_config.pay_url))
+            .await
+            .context("链接")
+        {
+            Ok(o) => o,
+            Err(e) => {
+                let response = sd_model::BalanceRespBody::error("99", format!("请求后端服务器错误: {:#?}", e));
+                tracing::error!(
+                    url=%config.fscg_config.pay_url,
+                    BackendRequest=?backend_req,
+                    Response=?response,
+                    "{}", response.result_desc
+                );
+                return crate::extract::SdRespExtract{ data: response, request_id: head.request_id, config };
+            }
+        };
+
+        let status = backend_resp.status();
+        if !status.is_success() {
+            let response =
+                sd_model::BalanceRespBody::error("99", format!("Backend Http Status: {}", status));
+            tracing::error!(
+                BackendStatus = %status,
+                Response = ?response,
+                "Backend Http Status Error"
+            );
+            return crate::extract::SdRespExtract {
+                data: response,
+                request_id: head.request_id,
+                config,
+            };
+        }
+
+        let backend_data = match backend_resp
+            .text()
+            .instrument(tracing::info_span!("BackendRead"))
+            .await
+        {
+            Ok(o) => o,
+            Err(e) => {
+                let response =
+                    sd_model::BalanceRespBody::error("99", format!("读取Response: {:#?}", e));
+                tracing::error!(Response = ?response, "{}", response.result_desc);
+                return crate::extract::SdRespExtract {
+                    data: response,
+                    request_id: head.request_id,
+                    config,
+                };
+            }
+        };
+
+        let resp =
+            match serde_json::from_str::<backend::Body<backend::AccountRespBody>>(&backend_data) {
+                Ok(o) => o,
+                Err(e) => {
+                    let response =
+                        sd_model::BalanceRespBody::error("99", format!("解析JSON错误: {:?}", e));
+                    tracing::error!(
+                        BackendResponse = backend_data,
+                        Response = ?response,
+                        "{}",
+                        response.result_desc
+                    );
+                    return crate::extract::SdRespExtract {
+                        data: response,
+                        request_id: head.request_id,
+                        config,
+                    };
+                }
+            };
+        let response = sd_model::BalanceRespBody::from_backend_account_resp(&config, resp);
+        crate::extract::SdRespExtract {
+            data: response,
+            request_id: head.request_id,
+            config,
+        }
+    }
+}
+
+/// FSCG 回调
+pub(crate) mod callback {
+    use std::sync::Arc;
+
+    use axum::{Extension, Json};
+
+    #[tracing::instrument(name="HandlerCallback", ret(Debug), fields(Request=?req), skip_all )]
+
+    pub async fn route(
+        Extension(_session): Extension<reqwest::Client>,
+        Extension(_config): Extension<Arc<crate::config::Config>>,
+        req: Json<u32>,
+    ) {
+        todo!()
+    }
+}

+ 694 - 0
src/sd_model.rs

@@ -0,0 +1,694 @@
+//! 盛大数据模型
+
+use crate::{
+    backend::{self},
+    config::Config,
+    gmssl, Sign, Verify,
+};
+use anyhow::Context;
+use axum::{response::IntoResponse, Json};
+use md5::{Digest, Md5};
+use serde::de::DeserializeOwned;
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct PubReqHead {
+    pub app_id: String,
+
+    pub method: String,
+
+    pub sign_type: String,
+
+    pub sign: String,
+
+    pub timestamp: String, // yyyy-MM-dd HH:mm:ss
+
+    pub version: String,
+
+    pub request_id: String,
+
+    pub biz_content: String,
+}
+
+pub trait TryFromPubHeadConfig: Sized {
+    fn try_from_pub_req_head_config(head: &PubReqHead, config: &Config) -> anyhow::Result<Self>;
+}
+
+impl<T> TryFromPubHeadConfig for T
+where
+    T: Sized + DeserializeOwned + crate::Verify,
+{
+    fn try_from_pub_req_head_config(head: &PubReqHead, config: &Config) -> anyhow::Result<Self> {
+        let req: T = head.decopy(config)?;
+        req.verify(config).context("验证出错")?;
+        Ok(req)
+    }
+}
+
+impl crate::Verify for PubReqHead {
+    fn verify(&self, config: &Config) -> anyhow::Result<()> {
+        if self.sign_type != "md5"
+            || self.app_id != config.sd_config.supplier_code
+            || self.method != "sd.tovc.recharge"
+        {
+            return Err(anyhow::Error::msg("PubReqHead Verify Value Error"));
+        }
+
+        match hex::decode(&self.sign) {
+            Ok(o) => {
+                let new_sign = self.sign_var(config);
+                if o == new_sign {
+                    Ok(())
+                } else {
+                    Err(anyhow::Error::msg("sign 不匹配"))
+                }
+            }
+            Err(e) => {
+                tracing::error!("verify: {self:?} error: {e}");
+                Err(anyhow::Error::msg(format!(
+                    "PubReqHead Verify Value sign hex::decode Error: {e:#?}"
+                )))
+            }
+        }
+    }
+}
+
+impl Default for PubReqHead {
+    fn default() -> Self {
+        Self {
+            app_id: "".to_string(),
+            method: "".to_string(),
+            sign_type: "md5".to_string(),
+            sign: "".to_string(),
+            timestamp: "".to_string(),
+            version: "v1.0".to_string(),
+            request_id: "".to_string(),
+            biz_content: "".to_string(),
+        }
+    }
+}
+
+macro_rules! str_per {
+    ( $($p:ident).*) => {
+        (str_per!(@internal $($p).*), $($p).*)
+    };
+    (@internal $p:ident ) => {
+        {
+            let s = stringify!($p);
+            let mut str = String::new();
+            let mut capitalize = false;
+            for i in s.chars(){
+                if i == '_'{
+                    capitalize = true;
+                }else if capitalize {
+                    str.push(i.to_ascii_uppercase());
+                    capitalize = false;
+                }else{
+                    str.push(i)
+                }
+            }
+            str
+        }
+
+    };
+    (@internal $_p:ident.$($p:ident).*) => {
+        str_per!(@internal $($p).*)
+    };
+}
+
+macro_rules! bt_map {
+    ( $( ($k:expr, $v:expr) ),* $(,)?) => {
+        {
+            let mut map = std::collections::BTreeMap::new();
+            $(
+                map.insert($k, $v)
+            )*
+            map
+        }
+    };
+    ($($k:expr),* $(,)?) => {
+        {
+            let mut map = std::collections::BTreeMap::new();
+            $(
+                let entry: (_,_) = $k;
+                map.insert(entry.0, entry.1);
+            )*
+            map
+        }
+    }
+}
+
+impl PubReqHead {
+    pub fn decopy<T>(&self, config: &Config) -> anyhow::Result<T>
+    where
+        T: for<'a> serde::Deserialize<'a>,
+    {
+        let key = config.sd_config.get_sk().context("PayReqHead::decopy")?;
+        let data = hex::decode(&self.biz_content).context("hex::decode Error")?;
+        let s = key
+            .decrypt(data, gmssl::sm2::PassOrder::C1C2C3)
+            .context("解密 biz_content")?;
+        serde_json::from_slice(&s).context("反序列化 biz_content")
+    }
+
+    pub fn sign(&mut self, config: &Config) {
+        self.sign = hex::encode_upper(self.sign_var(config))
+    }
+
+    pub fn sign_var(&self, config: &Config) -> Vec<u8> {
+        let mut aa = Md5::new();
+        let s = self.format_str(config);
+        aa.update(s);
+        let r = aa.finalize().to_vec();
+        tracing::debug!("{self:?} Sing:{}", hex::encode(&r));
+        r
+    }
+
+    pub fn format_str(&self, config: &Config) -> String {
+        let Self {
+            app_id,
+            method,
+            sign_type,
+            sign,
+            timestamp,
+            version,
+            request_id,
+            biz_content,
+        } = self;
+        let map = bt_map!(
+            str_per!(app_id),
+            str_per!(method),
+            str_per!(sign_type),
+            str_per!(sign),
+            str_per!(timestamp),
+            str_per!(version),
+            str_per!(request_id),
+            str_per!(biz_content)
+        );
+
+        let items: Vec<_> = map
+            .into_iter()
+            .map(|(k, v)| (k.trim().to_owned(), v.trim().to_owned()))
+            .filter(|(k, v)| !v.is_empty() && k != "sign")
+            .map(|(k, v)| format!("{}={}", k, v))
+            .collect();
+        let mut result = items.join("&");
+        result.push('&');
+        result.push_str(&format!("key={}", config.sd_config.sin_key));
+        result
+    }
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct PayReqBody {
+    pub game_user_id: String,
+
+    pub server_type: String,
+
+    pub product_no: String,
+
+    pub sp_order_id: String,
+
+    pub source: String,
+}
+
+impl crate::Verify for PayReqBody {
+    fn verify(&self, _: &Config) -> anyhow::Result<()> {
+        if self.server_type != "tel" {
+            return Err(anyhow::Error::msg("仅支持话费"));
+        }
+        Ok(())
+    }
+}
+
+impl PayReqBody {
+    pub fn into_backend_pay_req(
+        &self,
+        head: &PubReqHead,
+        config: &Config,
+    ) -> backend::Body<backend::PayReqBody> {
+        let timestamp = head.timestamp.trim().replace(
+            |c: char| c.is_whitespace() || c == '-' || c == '_' || c == ':',
+            "",
+        );
+        let mut body = backend::Body {
+            header: backend::Header {
+                timestamp,
+                seqno: head.request_id.clone(),
+                appid: config.fscg_config.app_id.clone(),
+                ..Default::default()
+            },
+            msgbody: backend::PayReqBody {
+                content: Some(backend::PayReqContent {
+                    user: self.game_user_id.clone(),
+                    packageid: self.product_no.clone(),
+                    extorder: self.sp_order_id.clone(),
+                    note: Some(self.source.clone()),
+                    ..Default::default()
+                }),
+                resp: None,
+            },
+        };
+
+        body.sign(config);
+        body
+    }
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct PayRespBodyData {
+    state: String,
+
+    order_cash: String,
+
+    order_id: String,
+
+    sp_order_id: String,
+
+    game_user_id: String,
+
+    product_name: String,
+}
+
+pub type PayRespBody = PubRespBody<PayRespBodyData>;
+
+impl PayRespBody {
+    pub fn from_backend_pay_resp(
+        config: &Config,
+        body: backend::Body<backend::PayRespBody>,
+        req: &PayReqBody,
+    ) -> Self {
+        if let Err(e) = body.verify(config) {
+            // return Self::error("99", "服务响应验证错误");
+            return Self::error("99", format!("服务响应验证错误: {:#?}", e));
+        }
+
+        let resp = match body.msgbody.resp {
+            Some(resp) => resp,
+            None => {
+                return Self::error("99", "服务没有响应字段: resp");
+            }
+        };
+
+        let content = match body.msgbody.content {
+            Some(content) => content,
+            None => {
+                return Self::error(
+                    resp.rcode,
+                    resp.rmsg.unwrap_or("服务没有响应字段: content".to_string()),
+                );
+            }
+        };
+
+        let state = if resp.rcode == "00" {
+            "0".to_string()
+        } else {
+            "9".to_string()
+        };
+
+        Self::success(PayRespBodyData {
+            state,
+            order_cash: "".to_string(),
+            order_id: content.orderid,
+            sp_order_id: content.extorder,
+            game_user_id: req.game_user_id.clone(),
+            product_name: req.product_no.clone(),
+        })
+    }
+}
+
+impl crate::Sign for PayRespBody {
+    fn sign(&mut self, _: &Config) {}
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct BalanceRespBody {
+    pub result_code: String,
+    pub result_desc: String,
+    pub balance: Option<String>,
+}
+
+impl BalanceRespBody {
+    pub fn from_backend_account_resp(
+        config: &Config,
+        body: backend::Body<backend::AccountRespBody>,
+    ) -> Self {
+        if let Err(e) = body.verify(config) {
+            return Self::error("99", format!("服务响应验证错误: {:#?}", e));
+        }
+        let resp = match body.msgbody.resp {
+            Some(resp) => resp,
+            None => {
+                return Self::error("99", "服务没有响应字段: resp");
+            }
+        };
+        let content = match body.msgbody.content {
+            Some(content) => content,
+            None => {
+                return Self::error(
+                    resp.rcode,
+                    resp.rmsg.unwrap_or("服务没有响应字段: content".to_string()),
+                );
+            }
+        };
+
+        Self::success(content.balance)
+
+    }
+
+    pub fn success(data: impl Into<Option<String>>) -> Self {
+        Self {
+            result_code: "000".to_string(),
+            result_desc: "success".to_string(),
+            balance: data.into(),
+        }
+    }
+
+    pub fn error(code: impl ToString, desc: impl ToString) -> Self {
+        Self {
+            result_code: code.to_string(),
+            result_desc: desc.to_string(),
+            balance: None,
+        }
+    }
+}
+
+impl crate::Sign for BalanceRespBody {
+    fn sign(&mut self, _: &Config) {}
+}
+
+#[derive(serde::Serialize, serde::Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct BalanceReqBody {
+    source: String,
+}
+
+impl crate::Verify for BalanceReqBody {
+    fn verify(&self, _: &Config) -> anyhow::Result<()> {
+        if self.source == "SD" {
+            Ok(())
+        } else {
+            Err(anyhow::Error::msg("BalanceReqBody Verify False"))
+        }
+    }
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct SearchReqBody {
+    sp_order_id: String,
+}
+
+impl SearchReqBody {
+    pub fn into_backend_search_req(
+        &self,
+        head: &PubReqHead,
+        config: &Config,
+    ) -> backend::Body<backend::SearchReqBody> {
+        let time_format = time::macros::format_description!("[year]-[month padding:zero]-[day padding:zero] [hour padding:zero]:[minute padding:zero]:[second padding:zero]");
+
+        let offset = time::UtcOffset::from_hms(8, 0, 0).unwrap();
+        let now = time::OffsetDateTime::now_utc().to_offset(offset);
+        let timestamp = now.format(time_format).unwrap();
+        let mut body = backend::Body {
+            header: backend::Header {
+                timestamp,
+                seqno: head.request_id.clone(),
+                appid: config.fscg_config.app_id.clone(),
+                ..Default::default()
+            },
+            msgbody: backend::SearchReqBody {
+                content: Some(backend::SearchReqContent {
+                    // orderid: Some(req.sp_order_id.clone()),
+                    extorderid: Some(self.sp_order_id.clone()),
+                    ..Default::default()
+                }),
+                resp: None,
+            },
+        };
+        body.sign(config);
+
+        body
+    }
+}
+
+impl crate::Verify for SearchReqBody {}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct SearchRespData {
+    state: String,
+    message: String,
+    success_time: Option<String>,
+    order_id: String,
+    sp_order_id: String,
+    game_user_id: String,
+    operator_order: Option<String>,
+    card_no: Option<String>,        // 卡密
+    card_pwd: Option<String>,       // 卡密
+    effective_time: Option<String>, // 卡密
+}
+
+pub type SearchRespBody = PubRespBody<SearchRespData>;
+
+impl SearchRespBody {
+    pub fn from_backend_search_resp(
+        config: &Config,
+        body: backend::Body<backend::SearchRespBody>,
+    ) -> Self {
+        if let Err(e) = body.verify(config) {
+            Self::error("99", format!("服务响应验证错误: {:#?}", e));
+        }
+        let resp = match body.msgbody.resp {
+            Some(resp) => resp,
+            None => {
+                return Self::error("99", "服务没有响应字段: resp".to_string());
+            }
+        };
+
+        let content = match body.msgbody.content {
+            Some(content) => content,
+            None => {
+                return Self::error(
+                    resp.rcode,
+                    resp.rmsg.unwrap_or("服务没有响应字段: content".to_string()),
+                );
+            }
+        };
+        let state = if content.status == "6" {
+            "1"
+        } else if content.status == "4" {
+            "9"
+        } else {
+            "0"
+        };
+
+        let time_str = if content.checktime.is_empty() {
+            None
+        } else {
+            let time_format = time::macros::format_description!("[year]-[month padding:zero]-[day padding:zero] [hour padding:zero]:[minute padding:zero]:[second padding:zero]");
+            let backend_time = time::macros::format_description!("[year][month padding:zero][day padding:zero][hour padding:zero][minute padding:zero][second padding:zero][subsecond digits:1+]");
+            let time = time::PrimitiveDateTime::parse(&content.checktime, backend_time).unwrap();
+            let time_str = time.format(time_format).unwrap();
+            Some(time_str)
+        };
+
+        let data = SearchRespData {
+            state: state.to_string(),
+            message: content.msg,
+            success_time: time_str,
+            order_id: content.orderid,
+            sp_order_id: content.extorder,
+            game_user_id: content.user,
+            operator_order: content.attr1,
+            card_no: None,
+            card_pwd: None,
+            effective_time: None,
+        };
+
+        Self::success(data)
+    }
+}
+
+impl crate::Sign for SearchRespBody {
+    fn sign(&mut self, _: &Config) {}
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct PubRespBody<T> {
+    pub result_code: String,
+    pub result_desc: String,
+    pub data: Option<T>,
+}
+
+impl<T> PubRespBody<T> {
+    pub fn success(data: T) -> Self {
+        Self {
+            result_code: "000".to_string(),
+            result_desc: "success".to_string(),
+            data: Some(data),
+        }
+    }
+
+    pub fn error(code: impl ToString, desc: impl ToString) -> Self {
+        Self {
+            result_code: code.to_string(),
+            result_desc: desc.to_string(),
+            data: None,
+        }
+    }
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[serde(rename_all = "camelCase")]
+pub struct PubRespHead {
+    pub code: String,
+    pub msg: String,
+    pub data: String,
+    pub sign: String,
+    pub request_id: String,
+}
+
+impl IntoResponse for PubRespHead {
+    fn into_response(self) -> axum::response::Response {
+        Json(self).into_response()
+    }
+}
+
+impl PubRespHead {
+    pub fn new(config: &Config, data: impl ToString, request_id: impl ToString) -> Self {
+        let mut result = Self {
+            data: data.to_string(),
+            request_id: request_id.to_string(),
+            ..Default::default()
+        };
+        result.sign(config);
+
+        result
+    }
+
+    pub fn sign(&mut self, config: &Config) {
+        self.sign = hex::encode_upper(self.sign_var(config))
+    }
+
+    pub fn sign_var(&self, config: &Config) -> Vec<u8> {
+        let mut aa = Md5::new();
+        let s = self.format_str(config);
+        aa.update(&s);
+        let r = aa.finalize().to_vec();
+        tracing::debug!(sign = hex::encode(&r), data = s, "签名");
+        r
+    }
+
+    pub fn format_str(&self, config: &Config) -> String {
+        let Self {
+            code,
+            msg,
+            data,
+            sign,
+            request_id,
+        } = self;
+        let map = bt_map!(
+            str_per!(code),
+            str_per!(msg),
+            str_per!(data),
+            str_per!(sign),
+            str_per!(request_id),
+        );
+
+        let items: Vec<_> = map
+            .into_iter()
+            .map(|(k, v)| (k.trim().to_owned(), v.trim().to_owned()))
+            .filter(|(k, v)| !v.is_empty() && k != "sign")
+            .map(|(k, v)| format!("{}={}", k, v))
+            .collect();
+        let mut result = items.join("&");
+        result.push('&');
+        result.push_str(&format!("key={}", config.sd_config.sin_key));
+        result
+    }
+}
+
+impl Default for PubRespHead {
+    fn default() -> Self {
+        Self {
+            code: "000".to_string(),
+            msg: "success".to_string(),
+            data: "".to_string(),
+            sign: "".to_string(),
+            request_id: "".to_string(),
+        }
+    }
+}
+
+#[cfg(test)]
+#[allow(unused)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_sign() {
+        // 2C5A4C48A9BD07B79C250049A17C21C2
+        // 2C5A4C48A9BD07B79C250049A17C21C2
+        let mut a = PubReqHead {
+            app_id: "asdaw".to_string(),
+            method: "asdaw".to_string(),
+            sign_type: "asdaw".to_string(),
+            sign: "asdaw".to_string(),
+            timestamp: "2023-03-23 16:25:00".to_string(),
+            version: "asdaw".to_string(),
+            request_id: "asdaw".to_string(),
+            biz_content: "".to_string(),
+        };
+        let c = Default::default();
+        a.sign(&c);
+        println!("{a:?}");
+    }
+
+    #[test]
+    fn feature() {
+        let r = PayReqBody {
+            game_user_id: "13856023633".to_string(),
+            server_type: "tel".to_string(),
+            product_no: "LD_5477".to_string(),
+            sp_order_id: "QHGHFBGF0235641202".to_string(),
+            source: "SD".to_string(),
+        };
+
+        let r = serde_json::to_string(&r).unwrap();
+        println!("{r}")
+    }
+
+    #[test]
+    fn test_try_into_fscg_req() {
+        // 470EF517335D444BD003098D6F6ABCE0
+        // 470EF517335D444BD003098D6F6ABCE0
+        let now = std::time::Instant::now();
+        let mut a = PubReqHead {
+            app_id: "asdaw".to_string(),
+            method: "asdaw".to_string(),
+            sign_type: "asdaw".to_string(),
+            sign: "asdaw".to_string(),
+            timestamp: "2023-03-23 16:25:00".to_string(),
+            version: "asdaw".to_string(),
+            request_id: "asdaw".to_string(),
+            biz_content: r#"{"gameUserId":"13856023633","serverType":"tel","productNo":"LD_5477","spOrderId":"QHGHFBGF0235641202","source":"SD"}"#.to_string(),
+        };
+        let c: Config = Default::default();
+        // let r = c.sd_config.;
+        let key = c.sd_config.get_sk().unwrap();
+        let r = key
+            .encrypt(a.biz_content, gmssl::sm2::PassOrder::C1C2C3)
+            .unwrap();
+        a.biz_content = hex::encode(r);
+        a.sign(&c);
+
+        println!("{a:?}");
+
+        let new = std::time::Instant::now();
+        println!("use Time{:?}", new - now);
+    }
+}

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است