hn-readcard-go/reader.go

212 lines
5.7 KiB
Go
Raw Normal View History

2026-08-06 11:23:33 +08:00
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"syscall"
"unsafe"
)
const outMessageSize = 2048
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
procMultiByteToWide = kernel32.NewProc("MultiByteToWideChar")
)
type ReaderStatus struct {
Loaded bool `json:"loaded"`
DLLDir string `json:"dllDir,omitempty"`
Arch string `json:"arch"`
Platform string `json:"platform"`
Error string `json:"error,omitempty"`
}
type DLLResult struct {
Code int `json:"code"`
Success bool `json:"success"`
ReaderType string `json:"reader_type,omitempty"`
Raw string `json:"raw"`
Data any `json:"data"`
}
type cardOperations interface {
Status() ReaderStatus
ReadCard(readerType string, noPIN bool) (DLLResult, error)
HMACSM3(key, secret, timestamp, requestBody string) (DLLResult, error)
}
type DLLReader struct {
mu sync.Mutex
dll *syscall.LazyDLL
read *syscall.LazyProc
readNoPIN *syscall.LazyProc
hmac *syscall.LazyProc
dllDir string
loadErr error
}
func newDLLReader(cfg Config) *DLLReader {
r := &DLLReader{}
r.init(cfg)
return r
}
func (r *DLLReader) init(cfg Config) {
if runtime.GOOS != "windows" {
r.loadErr = errors.New("读卡 DLL 仅支持 Windows")
return
}
if runtime.GOARCH != "386" {
r.loadErr = fmt.Errorf("CardReaderDLL.dll 为 32 位,当前程序架构为 %s请使用 build.ps1 构建 windows/386 版本", runtime.GOARCH)
return
}
r.dllDir = resolveDLLDir(cfg)
dllPath := filepath.Join(r.dllDir, "CardReaderDLL.dll")
if !fileExists(dllPath) {
r.loadErr = fmt.Errorf("未找到 CardReaderDLL.dll: %s", dllPath)
return
}
// 厂商 DLL 会按当前工作目录查找各地市的二级依赖库。
if err := os.Chdir(r.dllDir); err != nil {
r.loadErr = fmt.Errorf("切换 DLL 工作目录失败: %w", err)
return
}
r.dll = syscall.NewLazyDLL(dllPath)
if err := r.dll.Load(); err != nil {
r.loadErr = fmt.Errorf("加载 CardReaderDLL.dll 失败: %w", err)
return
}
r.read = r.dll.NewProc("ZJ_ReadCardInfo")
r.readNoPIN = r.dll.NewProc("ZJ_ReadCardInfo_NoPin")
r.hmac = r.dll.NewProc("ZJ_Hmac_SM3")
for name, proc := range map[string]*syscall.LazyProc{
"ZJ_ReadCardInfo": r.read,
"ZJ_ReadCardInfo_NoPin": r.readNoPIN,
"ZJ_Hmac_SM3": r.hmac,
} {
if err := proc.Find(); err != nil {
r.loadErr = fmt.Errorf("查找 DLL 函数 %s 失败: %w", name, err)
return
}
}
}
func resolveDLLDir(cfg Config) string {
candidates := make([]string, 0, 3)
if filepath.IsAbs(cfg.DLLDir) {
candidates = append(candidates, cfg.DLLDir)
} else {
candidates = append(candidates,
filepath.Join(cfg.AppRoot, cfg.DLLDir),
filepath.Join(cfg.AppRoot, "..", "card-read-service", "package", "DWCardReaderDLL"),
)
if cwd, err := os.Getwd(); err == nil {
candidates = append(candidates, filepath.Join(cwd, cfg.DLLDir))
}
}
for _, candidate := range candidates {
candidate, _ = filepath.Abs(candidate)
if fileExists(filepath.Join(candidate, "CardReaderDLL.dll")) {
return filepath.Clean(candidate)
}
}
if len(candidates) == 0 {
return cfg.DLLDir
}
first, _ := filepath.Abs(candidates[0])
return filepath.Clean(first)
}
func (r *DLLReader) Status() ReaderStatus {
status := ReaderStatus{Loaded: r.loadErr == nil && r.dll != nil, DLLDir: r.dllDir, Arch: runtime.GOARCH, Platform: runtime.GOOS}
if r.loadErr != nil {
status.Error = r.loadErr.Error()
}
return status
}
func (r *DLLReader) ReadCard(readerType string, noPIN bool) (DLLResult, error) {
proc := r.read
if noPIN {
proc = r.readNoPIN
}
return r.invoke(proc, readerType, readerType)
}
func (r *DLLReader) HMACSM3(key, secret, timestamp, requestBody string) (DLLResult, error) {
return r.invoke(r.hmac, "", key, secret, timestamp, requestBody)
}
func (r *DLLReader) invoke(proc *syscall.LazyProc, readerType string, args ...string) (DLLResult, error) {
r.mu.Lock()
defer r.mu.Unlock()
if r.loadErr != nil {
return DLLResult{}, r.loadErr
}
if proc == nil {
return DLLResult{}, errors.New("读卡 DLL 尚未初始化")
}
ptrs := make([]*byte, len(args))
callArgs := make([]uintptr, 0, len(args)+1)
for i, value := range args {
ptr, err := syscall.BytePtrFromString(value)
if err != nil {
return DLLResult{}, fmt.Errorf("DLL 入参包含 NUL 字符: %w", err)
}
ptrs[i] = ptr
callArgs = append(callArgs, uintptr(unsafe.Pointer(ptrs[i])))
}
out := make([]byte, outMessageSize)
callArgs = append(callArgs, uintptr(unsafe.Pointer(&out[0])))
r1, _, _ := proc.Call(callArgs...)
runtime.KeepAlive(ptrs)
runtime.KeepAlive(out)
code := int(int32(r1))
raw, decodeErr := decodeGBK(bytes.TrimRight(out, "\x00"))
if decodeErr != nil {
raw = strings.ToValidUTF8(string(bytes.TrimRight(out, "\x00")), "<22>")
}
raw = strings.TrimSpace(raw)
var data any
if raw != "" {
if jsonErr := json.Unmarshal([]byte(raw), &data); jsonErr != nil {
data = raw
}
}
return DLLResult{Code: code, Success: code == 0, ReaderType: readerType, Raw: raw, Data: data}, nil
}
func decodeGBK(src []byte) (string, error) {
if len(src) == 0 {
return "", nil
}
const codePageGBK = 936
size, _, callErr := procMultiByteToWide.Call(
codePageGBK, 0, uintptr(unsafe.Pointer(&src[0])), uintptr(len(src)), 0, 0,
)
if size == 0 {
return "", fmt.Errorf("GBK 长度转换失败: %v", callErr)
}
wide := make([]uint16, int(size))
written, _, callErr := procMultiByteToWide.Call(
codePageGBK, 0, uintptr(unsafe.Pointer(&src[0])), uintptr(len(src)),
uintptr(unsafe.Pointer(&wide[0])), size,
)
if written == 0 {
return "", fmt.Errorf("GBK 转 UTF-16 失败: %v", callErr)
}
return syscall.UTF16ToString(wide[:int(written)]), nil
}