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")), "�") } 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 }