226 lines
6.5 KiB
Go
226 lines
6.5 KiB
Go
//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()
|
||
}
|