Files
lingniu-vehicle-ingest/vehicle-data-platform/apps/api/internal/gzev/serverchan.go
T
lingniu d6b957dfc8 feat: sync vehicle monitoring and accumulated platform updates
Add monitoring metrics, numeric sorting, report-time display, vehicle number labels, and map layout fixes. Include accumulated mileage, hydrogen, OneOS authentication, open platform, ingestion, deployment, and UI updates.

Exclude temporary files, generated reports, and credentials. Validate frontend 632 tests and production build, API packages, vehicle-map tests, and release installer tests. Gateway suite has one pre-existing date-sensitive identity throttling test failure reproduced on the parent commit.
2026-10-26 18:05:59 +08:00

85 lines
3.3 KiB
Go

package gzev
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"time"
)
type ServerChan struct {
SendKey, SessionPath, UploadURL string
HTTP *http.Client
}
func ServerChanFromEnv() ServerChan {
path := os.Getenv("GZEV_SESSION_FILE")
if path == "" {
path = "/var/lib/lingniu-gzev/session.json"
}
return ServerChan{SendKey: strings.TrimSpace(os.Getenv("SERVERCHAN_SENDKEY")), SessionPath: path, UploadURL: os.Getenv("GZEV_SESSION_UPLOAD_URL"), HTTP: &http.Client{Timeout: 20 * time.Second, CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }}}
}
func (s ServerChan) Send(ctx context.Context) error { return s.send(ctx, false) }
func (s ServerChan) SendTest(ctx context.Context) error { return s.send(ctx, true) }
func (s ServerChan) send(ctx context.Context, test bool) error {
if !regexp.MustCompile(`^SCT[A-Za-z0-9]{8,253}$`).MatchString(s.SendKey) {
return errors.New("ServerChan SendKey missing or invalid")
}
if s.UploadURL != "https://vehicle.d.lnoneos.com/gzev/session/" {
return errors.New("unsupported recovery URL")
}
code, err := EnsureRecoveryCode(s.SessionPath + ".recovery.json")
if err != nil {
return errors.New("cannot create recovery code")
}
title := "广州里程采集登录失效"
intro := "定时采集前验证未通过,当前采集已暂停。请登录广州平台并提交新的登录状态。"
if test {
title = "广州里程采集推送测试"
intro = "这是 Server酱推送渠道连通性测试,不表示登录状态已失效。以后采集前发现登录失效,将通过此渠道通知。"
}
body := fmt.Sprintf("%s\n\n[打开登录状态上传页面](%s)\n\n通知恢复码:\n\n`%s`\n\n恢复码有效期 48 小时,成功上传后失效。上传页面支持粘贴 token 或上传会话 JSON;验证通过后,下次定时采集自动继续。请勿转发恢复码。", intro, s.UploadURL, code)
return s.SendMessage(ctx, title, body)
}
// SendMessage uses the existing configured channel without creating a recovery ticket.
func (s ServerChan) SendMessage(ctx context.Context, title, body string) error {
if !regexp.MustCompile(`^SCT[A-Za-z0-9]{8,253}$`).MatchString(s.SendKey) {
return errors.New("ServerChan SendKey missing or invalid")
}
form := url.Values{"title": {title}, "desp": {body}}
req, err := http.NewRequestWithContext(ctx, "POST", "https://sctapi.ftqq.com/"+s.SendKey+".send", strings.NewReader(form.Encode()))
if err != nil {
return errors.New("cannot create push request")
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := s.HTTP.Do(req)
if err != nil {
return errors.New("ServerChan transport failure; delivery status may be unknown")
}
defer resp.Body.Close()
var result struct {
Code *int `json:"code"`
Data struct {
Errno *int `json:"errno"`
} `json:"data"`
}
if json.NewDecoder(io.LimitReader(resp.Body, 1<<20)).Decode(&result) != nil || resp.StatusCode != 200 || result.Code == nil {
return errors.New("ServerChan did not confirm acceptance")
}
if *result.Code != 0 {
return fmt.Errorf("ServerChan rejected push (code=%d)", *result.Code)
}
if result.Data.Errno != nil && *result.Data.Errno != 0 {
return fmt.Errorf("ServerChan channel error (errno=%d)", *result.Data.Errno)
}
return nil
}