bug: 无订阅情况 出现下多笔订单 支付状态乱
Build docker and publish / build (20.15.1) (push) Successful in 7m18s

This commit is contained in:
2026-03-30 00:32:41 -07:00
parent a9205cc3fc
commit df7303738a
4 changed files with 531 additions and 13 deletions
+295
View File
@@ -0,0 +1,295 @@
package main
// 设备登录测试脚本
// 用法: go run scripts/test_device_login.go
// 功能: 模拟客户端设备登录,自动加密请求体,解密响应,打印 token
import (
"bytes"
"crypto/hmac"
"crypto/md5"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/forgoer/openssl"
)
// ==================== 配置区域 ====================
const (
serverURL = "https://tapi.hifast.biz" // 服务地址
securitySecret = "c0qhq99a-nq8h-ropg-wrlc-ezj4dlkxqpzx" // device.security_secret
identifier = "test-device-script-001" // 设备唯一标识
userAgent = "TestScript/1.0" // UserAgent
appId = "android-client" // AppSignature.AppSecrets 中的 key
appSecret = "uB4G,XxL2{7b" // AppSignature.AppSecrets 中的 value
)
// ==================== AES 工具(与服务端 pkg/aes/aes.go 一致)====================
func generateKey(key string) []byte {
hash := sha256.Sum256([]byte(key))
return hash[:32]
}
func generateIv(iv, key string) []byte {
h := md5.New()
h.Write([]byte(iv))
return generateKey(hex.EncodeToString(h.Sum(nil)) + key)
}
func aesEncrypt(plainText []byte, keyStr string) (data string, nonce string, err error) {
nonce = fmt.Sprintf("%x", time.Now().UnixNano())
key := generateKey(keyStr)
iv := generateIv(nonce, keyStr)
dst, err := openssl.AesCBCEncrypt(plainText, key, iv, openssl.PKCS7_PADDING)
if err != nil {
return "", "", err
}
return base64.StdEncoding.EncodeToString(dst), nonce, nil
}
func aesDecrypt(cipherText string, keyStr string, ivStr string) (string, error) {
decode, err := base64.StdEncoding.DecodeString(cipherText)
if err != nil {
return "", err
}
key := generateKey(keyStr)
iv := generateIv(ivStr, keyStr)
dst, err := openssl.AesCBCDecrypt(decode, key, iv, openssl.PKCS7_PADDING)
return string(dst), err
}
// ==================== 签名工具(与服务端 pkg/signature 一致)====================
func buildStringToSign(method, path, rawQuery string, body []byte, xAppId, timestamp, nonce string) string {
canonical := canonicalQuery(rawQuery)
bodyHash := sha256Hex(body)
parts := []string{
strings.ToUpper(method),
path,
canonical,
bodyHash,
xAppId,
timestamp,
nonce,
}
return strings.Join(parts, "\n")
}
func canonicalQuery(rawQuery string) string {
if rawQuery == "" {
return ""
}
pairs := strings.Split(rawQuery, "&")
sort.Strings(pairs)
return strings.Join(pairs, "&")
}
func sha256Hex(data []byte) string {
h := sha256.Sum256(data)
return fmt.Sprintf("%x", h)
}
func buildSignature(stringToSign, secret string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(stringToSign))
return hex.EncodeToString(mac.Sum(nil))
}
func signedRequest(method, url, rawQuery string, body []byte, token string) (*http.Request, error) {
var bodyReader io.Reader
if body != nil {
bodyReader = bytes.NewReader(body)
}
req, err := http.NewRequest(method, url, bodyReader)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
if token != "" {
req.Header.Set("Authorization", token) // 不带 Bearer 前缀,服务端直接 Parse token
}
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
nonce := fmt.Sprintf("%x", time.Now().UnixNano())
// 提取 path
path := req.URL.Path
sts := buildStringToSign(method, path, rawQuery, body, appId, timestamp, nonce)
sig := buildSignature(sts, appSecret)
req.Header.Set("X-App-Id", appId)
req.Header.Set("X-Timestamp", timestamp)
req.Header.Set("X-Nonce", nonce)
req.Header.Set("X-Signature", sig)
return req, nil
}
// ==================== 主逻辑 ====================
func main() {
fmt.Println("=== 设备登录测试 ===")
fmt.Printf("Server: %s\n", serverURL)
fmt.Printf("Identifier: %s\n", identifier)
fmt.Println()
// 1. 构造原始请求体
payload := map[string]string{
"identifier": identifier,
"user_agent": userAgent,
}
plainBytes, err := json.Marshal(payload)
if err != nil {
fmt.Printf("[ERROR] marshal payload: %v\n", err)
return
}
fmt.Printf("原始请求体: %s\n", string(plainBytes))
// 2. AES 加密请求体
encData, nonce, err := aesEncrypt(plainBytes, securitySecret)
if err != nil {
fmt.Printf("[ERROR] encrypt: %v\n", err)
return
}
encBody := map[string]string{
"data": encData,
"time": nonce,
}
encBytes, _ := json.Marshal(encBody)
fmt.Printf("加密请求体: %s\n\n", string(encBytes))
// 3. 发送请求
req, err := signedRequest("POST", serverURL+"/v1/auth/login/device", "", encBytes, "")
if err != nil {
fmt.Printf("[ERROR] new request: %v\n", err)
return
}
req.Header.Set("Login-Type", "device")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("[ERROR] request failed: %v\n", err)
return
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
fmt.Printf("HTTP Status: %d\n", resp.StatusCode)
fmt.Printf("原始响应: %s\n\n", string(respBody))
// 4. 解密响应
// 响应格式: {"code":200,"data":{"data":"<encrypted>","time":"<nonce>"},"message":""}
var outer struct {
Code int `json:"code"`
Message string `json:"message"`
Data json.RawMessage `json:"data"`
}
if err := json.Unmarshal(respBody, &outer); err != nil {
fmt.Printf("[ERROR] parse response: %v\n", err)
return
}
if outer.Code != 200 {
fmt.Printf("[FAIL] 登录失败: code=%d message=%s\n", outer.Code, outer.Message)
return
}
// data 字段是加密对象
var encResp struct {
Data string `json:"data"`
Time string `json:"time"`
}
if err := json.Unmarshal(outer.Data, &encResp); err != nil {
// 如果 Device.Enable=falsedata 直接就是明文对象
fmt.Printf("响应 data 非加密格式,直接解析: %s\n", string(outer.Data))
var loginResp struct {
Token string `json:"token"`
}
if err2 := json.Unmarshal(outer.Data, &loginResp); err2 == nil && loginResp.Token != "" {
fmt.Printf("[OK] Token: %s\n", loginResp.Token)
}
return
}
decrypted, err := aesDecrypt(encResp.Data, securitySecret, encResp.Time)
if err != nil {
fmt.Printf("[ERROR] decrypt response: %v\n", err)
return
}
fmt.Printf("解密后响应: %s\n\n", decrypted)
var loginResp struct {
Token string `json:"token"`
}
if err := json.Unmarshal([]byte(decrypted), &loginResp); err != nil {
fmt.Printf("[ERROR] parse decrypted: %v\n", err)
return
}
fmt.Printf("[OK] Token: %s\n", loginResp.Token)
// 5. 用 token 请求订阅列表
fmt.Println("\n=== 请求订阅列表 ===")
subReq, err := signedRequest("GET", serverURL+"/v1/public/subscribe/list", "", nil, loginResp.Token)
if err != nil {
fmt.Printf("[ERROR] build subscribe request: %v\n", err)
return
}
subReq.Header.Set("Login-Type", "device")
subResp, err := client.Do(subReq)
if err != nil {
fmt.Printf("[ERROR] subscribe list request: %v\n", err)
return
}
defer subResp.Body.Close()
subBody, _ := io.ReadAll(subResp.Body)
fmt.Printf("HTTP Status: %d\n", subResp.StatusCode)
fmt.Printf("原始响应: %s\n", string(subBody))
// 解密订阅列表响应
var subOuter struct {
Code int `json:"code"`
Message string `json:"message"`
Data json.RawMessage `json:"data"`
}
if err := json.Unmarshal(subBody, &subOuter); err != nil {
fmt.Printf("[ERROR] parse subscribe response: %v\n", err)
return
}
if subOuter.Code != 200 {
fmt.Printf("[FAIL] 订阅列表失败: code=%d message=%s\n", subOuter.Code, subOuter.Message)
return
}
var subEnc struct {
Data string `json:"data"`
Time string `json:"time"`
}
if err := json.Unmarshal(subOuter.Data, &subEnc); err != nil || subEnc.Data == "" {
// 无加密,直接打印
fmt.Printf("\n[OK] 订阅列表(明文): %s\n", string(subOuter.Data))
return
}
subDecrypted, err := aesDecrypt(subEnc.Data, securitySecret, subEnc.Time)
if err != nil {
fmt.Printf("[ERROR] decrypt subscribe list: %v\n", err)
return
}
fmt.Printf("\n[OK] 订阅列表(解密): %s\n", subDecrypted)
}