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