金泰访客查询
本页由本地 Go 服务生成,只监听 127.0.0.1。服务端代发金泰请求,浏览器不再直接跨域访问。
原始 JSON
{{.RawJSON}}package main import ( "bytes" "context" "crypto/rand" "encoding/base64" "encoding/hex" "encoding/json" "errors" "fmt" "html" "html/template" "io" "log" "net/http" "net/http/cookiejar" "net/url" "os" "regexp" "sort" "strings" "sync" "time" qrcode "github.com/skip2/go-qrcode" ) const ( baseURL = "https://www.watvt.com" listenAddr = "127.0.0.1:8787" localSessionName = "jintai_local_sid" // 改成你自己的身份证号,也可以用环境变量 JINTAI_ID_CARD 覆盖。 hardcodedIDCard = "请改成你的身份证号" ) var idCardRE = regexp.MustCompile(`^(\d{6})(\d{4})(\d{2})(\d{2})(\d{3})([0-9]|X)$`) type server struct { mu sync.Mutex sessions map[string]*jintaiClient } type jintaiClient struct { mu sync.Mutex client *http.Client } type apiResponse struct { Success bool `json:"success"` Message string `json:"message"` Result json.RawMessage `json:"result"` } type pageData struct { IDMasked string VisitDate string Captcha string CaptchaImage template.URL StatusClass string StatusMessage string ResultHTML template.HTML RawJSON string } func main() { srv := &server{sessions: make(map[string]*jintaiClient)} mux := http.NewServeMux() mux.HandleFunc("/", srv.handleHome) log.Printf("金泰访客查询本地服务已启动: http://%s", listenAddr) log.Fatal(http.ListenAndServe(listenAddr, mux)) } func (s *server) handleHome(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } idCard := configuredIDCard() visitDate := strings.TrimSpace(r.URL.Query().Get("date")) if visitDate == "" { visitDate = todayText() } data := pageData{ IDMasked: maskIDCard(idCard), VisitDate: visitDate, Captcha: strings.TrimSpace(r.URL.Query().Get("captcha")), StatusClass: "muted", StatusMessage: "等待验证码。", } if !idCardRE.MatchString(idCard) { data.StatusClass = "err" data.StatusMessage = "请先在 Go 文件里修改 hardcodedIDCard,或设置环境变量 JINTAI_ID_CARD。" renderPage(w, data) return } client := s.clientFor(w, r) if data.Captcha != "" { raw, err := client.queryVisitor(r.Context(), idCard, visitDate, data.Captcha) data.RawJSON = prettyJSON(raw) if err != nil { data.StatusClass = "err" data.StatusMessage = "查询失败:" + err.Error() } else { var api apiResponse if json.Unmarshal(raw, &api) == nil { if api.Success { data.StatusClass = "ok" data.StatusMessage = "查询完成。" } else { data.StatusClass = "err" data.StatusMessage = firstNonEmpty(api.Message, "查询接口返回失败。") } } else { data.StatusClass = "ok" data.StatusMessage = "查询完成,返回内容不是标准 JSON。" } data.ResultHTML = renderResultHTML(raw) } } if data.Captcha == "" || data.StatusClass == "err" { img, err := client.fetchCaptcha(r.Context(), idCard) if err != nil { data.StatusClass = "err" data.StatusMessage = "验证码获取失败:" + err.Error() } else { data.Captcha = "" data.CaptchaImage = template.URL("data:image/png;base64," + img) if data.ResultHTML == "" { data.StatusClass = "ok" data.StatusMessage = "验证码已加载,输入后点查询。" } } } renderPage(w, data) } func configuredIDCard() string { if v := strings.TrimSpace(os.Getenv("JINTAI_ID_CARD")); v != "" { return v } return strings.TrimSpace(hardcodedIDCard) } func newJintaiClient() *jintaiClient { jar, _ := cookiejar.New(nil) return &jintaiClient{ client: &http.Client{ Jar: jar, Timeout: 25 * time.Second, }, } } func (s *server) clientFor(w http.ResponseWriter, r *http.Request) *jintaiClient { s.mu.Lock() defer s.mu.Unlock() if c, err := r.Cookie(localSessionName); err == nil && c.Value != "" { if client := s.sessions[c.Value]; client != nil { return client } } sid := randomID() client := newJintaiClient() s.sessions[sid] = client http.SetCookie(w, &http.Cookie{ Name: localSessionName, Value: sid, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, }) return client } func randomID() string { var b [16]byte if _, err := rand.Read(b[:]); err != nil { return fmt.Sprintf("%d", time.Now().UnixNano()) } return hex.EncodeToString(b[:]) } func (c *jintaiClient) fetchCaptcha(ctx context.Context, idCard string) (string, error) { raw, err := c.postEncoded(ctx, "/boot/system/getCaptcha", map[string]string{ "username": idCard, }) if err != nil { return "", err } var api apiResponse if err := json.Unmarshal(raw, &api); err != nil { return "", fmt.Errorf("验证码响应不是 JSON: %s", string(raw)) } if !api.Success { return "", errors.New(firstNonEmpty(api.Message, string(raw))) } if api.Message == "" { return "", errors.New("验证码图片为空") } return api.Message, nil } func (c *jintaiClient) queryVisitor(ctx context.Context, idCard, visitDate, captcha string) ([]byte, error) { return c.postEncoded(ctx, "/boot/system/visitor/getVisitorHisByCard", map[string]string{ "身份证号": idCard, "随行人员": visitDate, "备注": captcha, }) } func (c *jintaiClient) postEncoded(ctx context.Context, path string, payload any) ([]byte, error) { c.mu.Lock() defer c.mu.Unlock() _ = c.warmup(ctx) enc, err := encodeStr(payload) if err != nil { return nil, err } body, err := json.Marshal(map[string]string{"encodeStr": enc}) if err != nil { return nil, err } req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+path, bytes.NewReader(body)) if err != nil { return nil, err } req.Header.Set("Content-Type", "application/json;charset=UTF-8") req.Header.Set("X-Access-Type", "app") req.Header.Set("Origin", baseURL) req.Header.Set("Referer", baseURL+"/app/#/user/visitorSearch") req.Header.Set("User-Agent", "Mozilla/5.0") resp, err := c.client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() data, _ := io.ReadAll(resp.Body) if resp.StatusCode < 200 || resp.StatusCode >= 300 { return data, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(data)) } return normalizeResponse(data) } func (c *jintaiClient) warmup(ctx context.Context) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/app/", nil) if err != nil { return err } req.Header.Set("Referer", baseURL+"/app/#/user/visitorSearch") req.Header.Set("User-Agent", "Mozilla/5.0") resp, err := c.client.Do(req) if err != nil { return err } io.Copy(io.Discard, resp.Body) resp.Body.Close() return nil } func encodeStr(v any) (string, error) { b, err := json.Marshal(v) if err != nil { return "", err } escaped := url.QueryEscape(string(b)) escaped = strings.ReplaceAll(escaped, "+", "%20") return base64.StdEncoding.EncodeToString([]byte(escaped)), nil } func decodeStr(s string) (string, error) { raw, err := base64.StdEncoding.DecodeString(s) if err != nil { return "", err } return url.QueryUnescape(string(raw)) } func normalizeResponse(data []byte) ([]byte, error) { var wrapper struct { ResponseEncodeStr string `json:"responseEncodeStr"` } if err := json.Unmarshal(data, &wrapper); err == nil && wrapper.ResponseEncodeStr != "" { decoded, err := decodeStr(wrapper.ResponseEncodeStr) if err != nil { return data, err } return []byte(decoded), nil } return data, nil } func renderPage(w http.ResponseWriter, data pageData) { w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := pageTemplate.Execute(w, data); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } func renderResultHTML(raw []byte) template.HTML { if len(bytes.TrimSpace(raw)) == 0 { return "" } var value any if err := json.Unmarshal(raw, &value); err != nil { return template.HTML(`
| ` + html.EscapeString(key) + ` | `)
if src, ok := imageSrc(key, value); ok {
b.WriteString(` ` + html.EscapeString(value) + ` `)
} else {
b.WriteString(html.EscapeString(value))
}
b.WriteString(` |
|---|
本页由本地 Go 服务生成,只监听 127.0.0.1。服务端代发金泰请求,浏览器不再直接跨域访问。
{{.RawJSON}}