226 lines
6.5 KiB
Go
Raw Permalink Normal View History

2026-08-12 13:40:06 +08:00
//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()
}