hn-readcard-go/reader.go
2026-08-06 11:23:33 +08:00

212 lines
5.7 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.

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
}