//go:build windows package main import ( "bytes" "encoding/json" "fmt" "image" "net/http" "strings" "time" "unicode/utf8" "github.com/lxn/walk" . "github.com/lxn/walk/declarative" "github.com/lxn/win" qrcode "github.com/skip2/go-qrcode" ) type guiAPIResponse struct { Code int `json:"code"` Message string `json:"message"` Data json.RawMessage `json:"data"` } type guiCardData struct { Name string `json:"Name"` IDNum string `json:"IDNum"` CardNum string `json:"CardNum"` CardIDCode string `json:"CardIDCode"` } func runGUIProcess() int { cfg, err := loadConfig() if err != nil { walk.MsgBox(nil, "读卡程序", err.Error(), walk.MsgBoxIconError) return 1 } if err = runGUI(cfg); err != nil { walk.MsgBox(nil, "读卡程序", err.Error(), walk.MsgBoxIconError) return 1 } return 0 } func runGUI(cfg Config) error { var window *walk.MainWindow var qrView *walk.ImageView var message *walk.Label var readButton *walk.PushButton var currentBitmap *walk.Bitmap setMessage := func(text string, isError bool) { message.SetText(text) if isError { message.SetTextColor(walk.RGB(196, 43, 28)) } else { message.SetTextColor(walk.RGB(90, 98, 108)) } } readCard := func() { readButton.SetEnabled(false) readButton.SetText("正在读卡…") _ = qrView.SetImage(nil) if currentBitmap != nil { currentBitmap.Dispose() currentBitmap = nil } setMessage("请保持卡片位置不动", false) go func() { card, failure := requestCard(cfg) var qrImage image.Image if failure == nil { content := buildQRContent(card) if strings.Trim(content, "|") == "" { failure = fmt.Errorf("读卡成功,但未获取到有效卡片信息") } else { code, qrErr := qrcode.New(content, qrcode.Medium) if qrErr != nil { failure = fmt.Errorf("生成二维码失败:%w", qrErr) } else { qrImage = code.Image(300) } } } window.Synchronize(func() { defer func() { readButton.SetEnabled(true) readButton.SetText("读卡") }() if failure != nil { setMessage(failure.Error(), true) return } bitmap, bitmapErr := walk.NewBitmapFromImage(qrImage) if bitmapErr != nil { setMessage("显示二维码失败:"+bitmapErr.Error(), true) return } if err := qrView.SetImage(bitmap); err != nil { bitmap.Dispose() setMessage("显示二维码失败:"+err.Error(), true) return } if currentBitmap != nil { currentBitmap.Dispose() } currentBitmap = bitmap setMessage("读卡成功,请扫描二维码", false) }) }() } windowDef := MainWindow{ AssignTo: &window, Title: "社保卡读卡", Size: Size{520, 590}, MinSize: Size{520, 590}, MaxSize: Size{520, 590}, Background: SolidColorBrush{Color: walk.RGB(247, 249, 252)}, Layout: VBox{MarginsZero: true, SpacingZero: true}, Children: []Widget{ Composite{ Layout: VBox{Margins: Margins{24, 22, 24, 12}, Spacing: 12}, Children: []Widget{ Label{Text: "社保卡读卡", Font: Font{Family: "Microsoft YaHei UI", PointSize: 16, Bold: true}, TextColor: walk.RGB(30, 38, 50)}, ImageView{AssignTo: &qrView, MinSize: Size{320, 320}, Mode: ImageViewModeCenter, Background: SolidColorBrush{Color: walk.RGB(255, 255, 255)}}, Label{AssignTo: &message, Text: "请放置社保卡,然后点击读卡", Font: Font{Family: "Microsoft YaHei UI", PointSize: 10}, Alignment: AlignHCenterVCenter, TextAlignment: AlignCenter, MinSize: Size{0, 52}, TextColor: walk.RGB(90, 98, 108)}, }, }, Composite{ Background: SolidColorBrush{Color: walk.RGB(255, 255, 255)}, Layout: HBox{Margins: Margins{24, 18, 24, 18}}, Children: []Widget{ HSpacer{}, PushButton{AssignTo: &readButton, Text: "读卡", MinSize: Size{150, 44}, Font: Font{Family: "Microsoft YaHei UI", PointSize: 11, Bold: true}, OnClicked: readCard}, HSpacer{}, }, }, }, } if err := windowDef.Create(); err != nil { return err } // Keep only the requested minimize and close buttons in the title bar. style := win.GetWindowLong(window.Handle(), win.GWL_STYLE) win.SetWindowLong(window.Handle(), win.GWL_STYLE, style&^win.WS_MAXIMIZEBOX) defer func() { if currentBitmap != nil { currentBitmap.Dispose() } window.Dispose() }() window.Run() return nil } func requestCard(cfg Config) (guiCardData, error) { body, _ := json.Marshal(map[string]string{"reader_type": cfg.GUIReaderType}) client := &http.Client{Timeout: 2 * time.Minute} host := cfg.Host if host == "0.0.0.0" || host == "::" { host = "127.0.0.1" } url := fmt.Sprintf("http://%s:%d/api/card/read-nopin", host, cfg.Port) var lastErr error for attempt := 0; attempt < 8; attempt++ { response, err := client.Post(url, "application/json", bytes.NewReader(body)) if err != nil { lastErr = err time.Sleep(250 * time.Millisecond) continue } defer response.Body.Close() var payload guiAPIResponse if err = json.NewDecoder(response.Body).Decode(&payload); err != nil { return guiCardData{}, fmt.Errorf("读卡服务返回格式错误:%w", err) } if response.StatusCode != http.StatusOK || payload.Code != 0 { if strings.TrimSpace(payload.Message) == "" { payload.Message = fmt.Sprintf("读卡失败(状态码 %d)", response.StatusCode) } return guiCardData{}, fmt.Errorf("%s", payload.Message) } var card guiCardData if err = json.Unmarshal(payload.Data, &card); err != nil { return guiCardData{}, fmt.Errorf("无法解析卡片信息:%w", err) } return card, nil } return guiCardData{}, fmt.Errorf("无法连接读卡服务:%v", lastErr) } func buildQRContent(card guiCardData) string { return encodeURIComponent(card.Name) + "|" + card.IDNum + "|" + card.CardNum + "|" + card.CardIDCode } // encodeURIComponent matches JavaScript's encodeURIComponent for UTF-8 text. func encodeURIComponent(value string) string { const hex = "0123456789ABCDEF" var result strings.Builder result.Grow(len(value)) for len(value) > 0 { r, size := utf8.DecodeRuneInString(value) if r == utf8.RuneError && size == 1 { r = '\uFFFD' } encoded := make([]byte, utf8.RuneLen(r)) utf8.EncodeRune(encoded, r) for _, b := range encoded { if (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9') || strings.ContainsRune("-_.!~*'()", rune(b)) { result.WriteByte(b) } else { result.WriteByte('%') result.WriteByte(hex[b>>4]) result.WriteByte(hex[b&15]) } } value = value[size:] } return result.String() }