2026-08-12 13:40:06 +08:00

226 lines
6.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//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()
}