feat: 核心电费查询 Go 服务

- FasterLZU 登录 + xiaofubao 6 步电费链路

- shiroJID 会话缓存 + 每日记录存储

- 定时任务: 每天 00:00 记录, 失败重试 5 次

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-08-23 21:29:33 +08:00
commit b2ad61e3fe
6 changed files with 1122 additions and 0 deletions

130
go/lzuapi.go Normal file
View File

@@ -0,0 +1,130 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// lzuapi.go - FasterLZU (appservice.lzu.edu.cn) 登录客户端
// 对应 Python lzuapi.py 中的 login() 与 get_st()
const (
appServiceBase = "https://appservice.lzu.edu.cn"
lzuUserAgent = "Mozilla/5.0 (Linux; Android 12; SM-S7110 Build/V417IR; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/101.0.4951.61 Mobile Safari/537.36 lzdx_ua JHZF_LZDXAPP"
)
// lzuClient FasterLZU API 客户端
type lzuClient struct {
http *http.Client
username string
password string
loginToken string
gatewayToken string
}
func newLzuClient(username, password string) *lzuClient {
return &lzuClient{
http: &http.Client{},
username: username,
password: password,
}
}
// encryptedHeaders 加密请求头
func encryptedHeaders() http.Header {
h := http.Header{}
h.Set("Content-Type", "application/json;charset=UTF-8")
h.Set("Transfer-Encrypt", "true")
h.Set("User-Agent", lzuUserAgent)
return h
}
// Login 登录,返回 (loginToken, gatewayToken)
func (c *lzuClient) Login() (string, string, error) {
url := appServiceBase + "/api/eusp-unify-terminal/app-user/login"
data := map[string]any{
"app_os": 2,
"name": c.username,
"pwd": c.password,
}
plaintext, _ := json.Marshal(data) // 无空格,与 Python separators=(",",":") 一致
ciphertext, err := aesEncrypt(string(plaintext))
if err != nil {
return "", "", err
}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBufferString(ciphertext))
if err != nil {
return "", "", err
}
req.Header = encryptedHeaders()
resp, err := c.http.Do(req)
if err != nil {
return "", "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
decrypted, err := aesDecrypt(strings.TrimSpace(string(body)))
if err != nil {
return "", "", fmt.Errorf("登录响应解密失败: %w", err)
}
var res struct {
Code int `json:"code"`
Data struct {
LoginToken string `json:"login_token"`
GatewayToken string `json:"gateway_token"`
} `json:"data"`
}
if err := json.Unmarshal([]byte(decrypted), &res); err != nil {
return "", "", fmt.Errorf("登录响应解析失败: %w", err)
}
if res.Code != 1 {
return "", "", fmt.Errorf("登录失败 code=%d", res.Code)
}
c.loginToken = res.Data.LoginToken
c.gatewayToken = res.Data.GatewayToken
return c.loginToken, c.gatewayToken, nil
}
// GetSt 获取服务令牌 ST
func (c *lzuClient) GetSt(serviceID string) (string, error) {
url := fmt.Sprintf("%s/api/eusp-unify-terminal/app-user/getSt?loginToken=%s&serviceId=%s&service=",
appServiceBase, c.loginToken, serviceID)
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return "", err
}
req.Header = encryptedHeaders()
resp, err := c.http.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
decrypted, err := aesDecrypt(strings.TrimSpace(string(body)))
if err != nil {
return "", fmt.Errorf("getSt 响应解密失败: %w", err)
}
var res struct {
Code int `json:"code"`
Data string `json:"data"`
}
if err := json.Unmarshal([]byte(decrypted), &res); err != nil {
return "", fmt.Errorf("getSt 响应解析失败: %w", err)
}
if res.Code != 1 {
return "", fmt.Errorf("getSt 失败 code=%d", res.Code)
}
return res.Data, nil
}