版本: 1.0.0 日期: 2026-02-06 作者: 首席架构师 状态: 待评审
将原 Go + CGO 实现的 ts-wekey-skf 完整重构为 C++ + Qt 实现,解决 CGO 内存管理问题,同时保持 HTTP API 100% 兼容。
| 原则 | 要求 |
|---|---|
| 简单性 | YAGNI,标准库优先,反过度工程 |
| 测试先行 | TDD 循环,表格驱动测试 |
| 明确性 | 显式错误处理,无全局变量 |
- 所有原项目功能正常工作
- HTTP API 通过兼容性测试
- 无内存泄漏(Valgrind/AddressSanitizer 验证)
- 设备热插拔不崩溃
- Windows/macOS 双平台可用
| 组件 | 技术选型 | 版本 | 理由 |
|---|---|---|---|
| 语言 | C++17 | - | 支持 std::variant、std::optional、结构化绑定 |
| GUI | Qt Widgets | 6.5+ | 成熟稳定,跨平台,原生外观 |
| HTTP | Qt Network | 6.5+ | 与 Qt 集成,无额外依赖 |
| JSON | QJsonDocument | Qt 内置 | 标准库优先原则 |
| 构建 | CMake | 3.20+ | 跨平台,Qt 官方推荐 |
| 测试 | Qt Test | Qt 内置 | 与 Qt 深度集成 |
| 工具 | 用途 |
|---|---|
| Qt Creator / VS Code | IDE |
| clang-format | 代码格式化 |
| clang-tidy | 静态分析 |
| AddressSanitizer | 内存检测 (Debug) |
| Valgrind | 内存泄漏检测 (Linux/macOS) |
wekey-skf/
├── CMakeLists.txt # 根 CMake
├── Makefile # 便捷命令入口
├── src/
│ ├── CMakeLists.txt
│ ├── app/ # 应用入口
│ ├── common/ # 公共工具
│ ├── config/ # 配置管理
│ ├── log/ # 日志系统
│ ├── plugin/ # 插件系统
│ │ ├── interface/ # 插件接口
│ │ └── skf/ # SKF 插件
│ ├── core/ # 业务逻辑
│ ├── api/ # HTTP API
│ └── gui/ # GUI
├── tests/
│ ├── CMakeLists.txt
│ ├── unit/
│ └── integration/
├── resources/
│ ├── icons/
│ ├── lib/
│ └── wekey-skf.qrc
└── docs/
├── spec.md
├── plan.md
└── api-sketch.md
┌─────────────────────────────────────────────────────────────────┐
│ 开发时间线 │
├─────────┬─────────┬─────────┬─────────┬─────────┬─────────────┤
│ M1 │ M2 │ M3 │ M4 │ M5 │ M6 │
│ 基础框架 │ SKF驱动 │ 核心功能 │ HTTP API│ GUI实现 │ 测试与打包 │
├─────────┼─────────┼─────────┼─────────┼─────────┼─────────────┤
│ common/ │ plugin/ │ core/ │ api/ │ gui/ │ 集成测试 │
│ config/ │ skf/ │ device │ router │ pages │ 打包分发 │
│ log/ │ │ app │ handlers│ dialogs │ │
│ plugin/ │ │ container│ │ tray │ │
│ interface│ │ crypto │ │ │ │
└─────────┴─────────┴─────────┴─────────┴─────────┴─────────────┘
M1 ──→ M2 ──→ M3 ──┬──→ M4 ──┬──→ M6
│ │
└──→ M5 ──┘
- M1 是所有后续里程碑的基础
- M2 依赖 M1 的插件接口
- M3 依赖 M2 的 SKF 驱动
- M4 和 M5 可并行开发,都依赖 M3
- M6 需要 M4 和 M5 都完成
建立项目骨架,实现基础设施模块,为后续开发奠定基础。
任务 M1.1: 创建 CMake 构建系统
# CMakeLists.txt (根目录)
cmake_minimum_required(VERSION 3.20)
project(wekey-skf VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON)
# Qt 依赖
find_package(Qt6 REQUIRED COMPONENTS
Core
Widgets
Network
Test
)
# 子目录
add_subdirectory(src)
add_subdirectory(tests)任务 M1.2: 创建 Makefile 便捷入口
# Makefile
.PHONY: all build run test clean
BUILD_DIR := build
CMAKE := cmake
CTEST := ctest
all: build
configure:
$(CMAKE) -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=Debug
build: configure
$(CMAKE) --build $(BUILD_DIR) -j$(nproc)
run: build
./$(BUILD_DIR)/src/wekey-skf
test: build
cd $(BUILD_DIR) && $(CTEST) --output-on-failure
clean:
rm -rf $(BUILD_DIR)任务 M1.3: 实现 Result 模板
文件: src/common/Result.h
#pragma once
#include <variant>
#include "Error.h"
namespace wekey {
template<typename T>
class Result {
public:
static Result ok(T value) { return Result(std::move(value)); }
static Result err(Error error) { return Result(std::move(error)); }
[[nodiscard]] bool isOk() const { return std::holds_alternative<T>(data_); }
[[nodiscard]] bool isErr() const { return !isOk(); }
[[nodiscard]] const T& value() const& { return std::get<T>(data_); }
[[nodiscard]] T&& value() && { return std::get<T>(std::move(data_)); }
[[nodiscard]] const Error& error() const { return std::get<Error>(data_); }
// 链式操作
template<typename F>
auto map(F&& f) -> Result<decltype(f(std::declval<T>()))> {
using U = decltype(f(std::declval<T>()));
if (isOk()) {
return Result<U>::ok(f(value()));
}
return Result<U>::err(error());
}
template<typename F>
auto andThen(F&& f) -> decltype(f(std::declval<T>())) {
if (isOk()) {
return f(value());
}
return decltype(f(std::declval<T>()))::err(error());
}
private:
explicit Result(T value) : data_(std::move(value)) {}
explicit Result(Error error) : data_(std::move(error)) {}
std::variant<T, Error> data_;
};
// void 特化
template<>
class Result<void> {
public:
static Result ok() { return Result(true); }
static Result err(Error error) { return Result(std::move(error)); }
[[nodiscard]] bool isOk() const { return success_; }
[[nodiscard]] bool isErr() const { return !success_; }
[[nodiscard]] const Error& error() const { return error_; }
private:
explicit Result(bool) : success_(true) {}
explicit Result(Error e) : success_(false), error_(std::move(e)) {}
bool success_ = false;
Error error_;
};
} // namespace wekey任务 M1.4: 实现 Error 类
文件: src/common/Error.h
#pragma once
#include <QString>
#include <cstdint>
#include <QHash>
namespace wekey {
class Error {
public:
enum Code : uint32_t {
// 应用层错误码 (0x00 - 0xFF)
Success = 0x00,
Fail = 0x01,
InvalidParam = 0x03,
NoActiveModule = 0x04,
NotLoggedIn = 0x09,
NotAuthorized = 0x0B,
PortInUse = 0x10,
PluginLoadFailed = 0x11,
// SKF 错误码 (0x0A000000+)
SkfOk = 0x00000000,
SkfFail = 0x0A000001,
SkfUnknown = 0x0A000002,
SkfNotSupported = 0x0A000003,
SkfFileError = 0x0A000004,
SkfInvalidHandle = 0x0A000005,
SkfInvalidParam = 0x0A000006,
SkfDeviceRemoved = 0x0A000023,
SkfPinIncorrect = 0x0A000024,
SkfPinLocked = 0x0A000025,
SkfUserNotLogin = 0x0A00002D,
SkfAppNotExists = 0x0A00002E,
};
Error() = default;
explicit Error(Code code, QString message = {}, QString context = {})
: code_(code), message_(std::move(message)), context_(std::move(context)) {}
[[nodiscard]] Code code() const { return code_; }
[[nodiscard]] const QString& message() const { return message_; }
[[nodiscard]] const QString& context() const { return context_; }
[[nodiscard]] QString toString(bool detailed = false) const;
[[nodiscard]] QString friendlyMessage() const;
static Error fromSkf(uint32_t skfResult, const QString& function = {});
private:
Code code_ = Success;
QString message_;
QString context_;
static const QHash<Code, QString>& friendlyMessages();
};
} // namespace wekey文件: src/common/Error.cpp
#include "Error.h"
namespace wekey {
const QHash<Error::Code, QString>& Error::friendlyMessages() {
static const QHash<Code, QString> messages = {
{Success, "操作成功"},
{Fail, "操作失败"},
{InvalidParam, "参数无效"},
{NoActiveModule, "未激活驱动模块"},
{NotLoggedIn, "未登录"},
{NotAuthorized, "未授权"},
{PortInUse, "端口已被占用"},
{PluginLoadFailed, "插件加载失败"},
{SkfPinIncorrect, "PIN 码错误"},
{SkfPinLocked, "PIN 码已锁定"},
{SkfDeviceRemoved, "设备已移除"},
{SkfUserNotLogin, "用户未登录"},
{SkfAppNotExists, "应用不存在"},
};
return messages;
}
QString Error::friendlyMessage() const {
auto it = friendlyMessages().find(code_);
if (it != friendlyMessages().end()) {
return it.value();
}
return message_.isEmpty() ? "未知错误" : message_;
}
QString Error::toString(bool detailed) const {
QString result = friendlyMessage();
if (detailed) {
result += QString("\n错误码: 0x%1").arg(code_, 8, 16, QChar('0'));
if (!context_.isEmpty()) {
result += QString("\n上下文: %1").arg(context_);
}
}
return result;
}
Error Error::fromSkf(uint32_t skfResult, const QString& function) {
return Error(static_cast<Code>(skfResult), {}, function);
}
} // namespace wekey任务 M1.5: 实现 Config 类
文件: src/config/Config.h
#pragma once
#include <QObject>
#include <QString>
#include <QVariantMap>
#include <QJsonObject>
namespace wekey {
class Config : public QObject {
Q_OBJECT
public:
static Config& instance();
// 禁止拷贝
Config(const Config&) = delete;
Config& operator=(const Config&) = delete;
bool load();
bool save();
void reset();
// Getters
[[nodiscard]] QString listenPort() const;
[[nodiscard]] QString logLevel() const;
[[nodiscard]] QString logPath() const;
[[nodiscard]] bool systrayDisabled() const;
[[nodiscard]] QString errorMode() const;
[[nodiscard]] QVariantMap modPaths() const;
[[nodiscard]] QString activedModName() const;
// 默认值
[[nodiscard]] QString defaultAppName() const;
[[nodiscard]] QString defaultContainerName() const;
[[nodiscard]] QString defaultCommonName() const;
[[nodiscard]] QString defaultOrganization() const;
[[nodiscard]] QString defaultUnit() const;
[[nodiscard]] QString defaultRole() const;
// Setters
void setListenPort(const QString& port);
void setLogLevel(const QString& level);
void setLogPath(const QString& path);
void setSystrayDisabled(bool disabled);
void setErrorMode(const QString& mode);
void setModPath(const QString& name, const QString& path);
void removeModPath(const QString& name);
void setActivedModName(const QString& name);
void setDefault(const QString& key, const QString& value);
signals:
void configChanged();
private:
Config();
[[nodiscard]] QString configPath() const;
[[nodiscard]] QJsonObject defaultConfig() const;
QJsonObject data_;
};
} // namespace wekey文件: src/config/Config.cpp
#include "Config.h"
#include <QFile>
#include <QJsonDocument>
#include <QStandardPaths>
#include <QDir>
namespace wekey {
Config& Config::instance() {
static Config instance;
return instance;
}
Config::Config() {
data_ = defaultConfig();
}
QString Config::configPath() const {
return QDir::homePath() + "/.wekeytool.json";
}
QJsonObject Config::defaultConfig() const {
return QJsonObject{
{"version", "1.0.0"},
{"listen_port", ":9001"},
{"log_level", "info"},
{"log_path", QStandardPaths::writableLocation(QStandardPaths::TempLocation)},
{"systray_disabled", false},
{"error_mode", "simple"},
{"mod_paths", QJsonObject{}},
{"actived_mod_name", ""},
{"defaults", QJsonObject{
{"appName", "TAGM"},
{"containerName", "TrustAsia"},
{"commonName", "TrustAsia"},
{"organization", "TrustAsia Technologies, Inc."},
{"unit", "GMCA"},
{"role", "user"}
}}
};
}
bool Config::load() {
QFile file(configPath());
if (!file.open(QIODevice::ReadOnly)) {
// 配置文件不存在,使用默认值
return true;
}
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
file.close();
if (error.error != QJsonParseError::NoError) {
return false;
}
// 合并配置(保留默认值中新增的字段)
QJsonObject loaded = doc.object();
QJsonObject defaults = defaultConfig();
for (auto it = defaults.begin(); it != defaults.end(); ++it) {
if (!loaded.contains(it.key())) {
loaded[it.key()] = it.value();
}
}
data_ = loaded;
return true;
}
bool Config::save() {
QFile file(configPath());
if (!file.open(QIODevice::WriteOnly)) {
return false;
}
QJsonDocument doc(data_);
file.write(doc.toJson(QJsonDocument::Indented));
file.close();
emit configChanged();
return true;
}
void Config::reset() {
data_ = defaultConfig();
emit configChanged();
}
// Getters
QString Config::listenPort() const {
return data_["listen_port"].toString(":9001");
}
QString Config::logLevel() const {
return data_["log_level"].toString("info");
}
QString Config::logPath() const {
return data_["log_path"].toString();
}
bool Config::systrayDisabled() const {
return data_["systray_disabled"].toBool(false);
}
QString Config::errorMode() const {
return data_["error_mode"].toString("simple");
}
QVariantMap Config::modPaths() const {
return data_["mod_paths"].toObject().toVariantMap();
}
QString Config::activedModName() const {
return data_["actived_mod_name"].toString();
}
QString Config::defaultAppName() const {
return data_["defaults"].toObject()["appName"].toString("TAGM");
}
QString Config::defaultContainerName() const {
return data_["defaults"].toObject()["containerName"].toString("TrustAsia");
}
QString Config::defaultCommonName() const {
return data_["defaults"].toObject()["commonName"].toString("TrustAsia");
}
QString Config::defaultOrganization() const {
return data_["defaults"].toObject()["organization"].toString();
}
QString Config::defaultUnit() const {
return data_["defaults"].toObject()["unit"].toString("GMCA");
}
QString Config::defaultRole() const {
return data_["defaults"].toObject()["role"].toString("user");
}
// Setters
void Config::setListenPort(const QString& port) {
data_["listen_port"] = port;
}
void Config::setLogLevel(const QString& level) {
data_["log_level"] = level;
}
void Config::setLogPath(const QString& path) {
data_["log_path"] = path;
}
void Config::setSystrayDisabled(bool disabled) {
data_["systray_disabled"] = disabled;
}
void Config::setErrorMode(const QString& mode) {
data_["error_mode"] = mode;
}
void Config::setModPath(const QString& name, const QString& path) {
QJsonObject mods = data_["mod_paths"].toObject();
mods[name] = path;
data_["mod_paths"] = mods;
}
void Config::removeModPath(const QString& name) {
QJsonObject mods = data_["mod_paths"].toObject();
mods.remove(name);
data_["mod_paths"] = mods;
}
void Config::setActivedModName(const QString& name) {
data_["actived_mod_name"] = name;
}
void Config::setDefault(const QString& key, const QString& value) {
QJsonObject defaults = data_["defaults"].toObject();
defaults[key] = value;
data_["defaults"] = defaults;
}
} // namespace wekey任务 M1.6: 实现 Logger 类
文件: src/log/Logger.h
#pragma once
#include <QObject>
#include <QString>
#include <QDateTime>
#include <QFile>
#include <QMutex>
namespace wekey {
enum class LogLevel {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3
};
struct LogEntry {
QDateTime timestamp;
LogLevel level;
QString message;
QString source;
};
class Logger : public QObject {
Q_OBJECT
public:
static Logger& instance();
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
void setLevel(LogLevel level);
void setOutputPath(const QString& path);
void debug(const QString& message, const QString& source = {});
void info(const QString& message, const QString& source = {});
void warn(const QString& message, const QString& source = {});
void error(const QString& message, const QString& source = {});
[[nodiscard]] LogLevel level() const { return level_; }
signals:
void logAdded(const LogEntry& entry);
private:
Logger();
void log(LogLevel level, const QString& message, const QString& source);
void writeToFile(const LogEntry& entry);
LogLevel level_ = LogLevel::Info;
QString outputPath_;
QFile file_;
QMutex mutex_;
};
// 便捷宏
#define LOG_DEBUG(msg) wekey::Logger::instance().debug(msg, __FUNCTION__)
#define LOG_INFO(msg) wekey::Logger::instance().info(msg, __FUNCTION__)
#define LOG_WARN(msg) wekey::Logger::instance().warn(msg, __FUNCTION__)
#define LOG_ERROR(msg) wekey::Logger::instance().error(msg, __FUNCTION__)
} // namespace wekey
Q_DECLARE_METATYPE(wekey::LogEntry)文件: src/log/Logger.cpp
#include "Logger.h"
#include <QDir>
#include <QTextStream>
namespace wekey {
Logger& Logger::instance() {
static Logger instance;
return instance;
}
Logger::Logger() {
qRegisterMetaType<LogEntry>("LogEntry");
}
void Logger::setLevel(LogLevel level) {
level_ = level;
}
void Logger::setOutputPath(const QString& path) {
QMutexLocker locker(&mutex_);
if (file_.isOpen()) {
file_.close();
}
outputPath_ = path;
if (!path.isEmpty()) {
QDir dir(path);
if (!dir.exists()) {
dir.mkpath(".");
}
QString filePath = path + "/wekey-skf.log";
file_.setFileName(filePath);
file_.open(QIODevice::Append | QIODevice::Text);
}
}
void Logger::debug(const QString& message, const QString& source) {
log(LogLevel::Debug, message, source);
}
void Logger::info(const QString& message, const QString& source) {
log(LogLevel::Info, message, source);
}
void Logger::warn(const QString& message, const QString& source) {
log(LogLevel::Warn, message, source);
}
void Logger::error(const QString& message, const QString& source) {
log(LogLevel::Error, message, source);
}
void Logger::log(LogLevel level, const QString& message, const QString& source) {
if (level < level_) {
return;
}
LogEntry entry{
QDateTime::currentDateTime(),
level,
message,
source
};
writeToFile(entry);
emit logAdded(entry);
}
void Logger::writeToFile(const LogEntry& entry) {
QMutexLocker locker(&mutex_);
if (!file_.isOpen()) {
return;
}
static const char* levelNames[] = {"DEBUG", "INFO", "WARN", "ERROR"};
QTextStream stream(&file_);
stream << entry.timestamp.toString("yyyy-MM-dd hh:mm:ss.zzz")
<< " [" << levelNames[static_cast<int>(entry.level)] << "] ";
if (!entry.source.isEmpty()) {
stream << "[" << entry.source << "] ";
}
stream << entry.message << "\n";
stream.flush();
}
} // namespace wekey任务 M1.7: 实现 LogModel (供 GUI 使用)
文件: src/log/LogModel.h
#pragma once
#include <QAbstractTableModel>
#include <QList>
#include "Logger.h"
namespace wekey {
class LogModel : public QAbstractTableModel {
Q_OBJECT
public:
enum Column {
Timestamp = 0,
Level,
Source,
Message,
ColumnCount
};
explicit LogModel(QObject* parent = nullptr);
int rowCount(const QModelIndex& parent = {}) const override;
int columnCount(const QModelIndex& parent = {}) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
void setFilter(LogLevel minLevel);
void setSearchText(const QString& text);
void clear();
private slots:
void onLogAdded(const LogEntry& entry);
private:
void applyFilter();
QList<LogEntry> allEntries_;
QList<LogEntry> filteredEntries_;
LogLevel minLevel_ = LogLevel::Debug;
QString searchText_;
};
} // namespace wekey任务 M1.8: 定义 IDriverPlugin 接口
文件: src/plugin/interface/IDriverPlugin.h
#pragma once
#include <QtPlugin>
#include <QList>
#include <QVariantMap>
#include "PluginTypes.h"
#include "common/Result.h"
namespace wekey {
class IDriverPlugin {
public:
virtual ~IDriverPlugin() = default;
//=== 设备管理 ===
virtual Result<QList<DeviceInfo>> enumDevices(bool login = false) = 0;
virtual Result<void> changeDeviceAuth(const QString& devName,
const QString& oldPin,
const QString& newPin) = 0;
virtual Result<void> setDeviceLabel(const QString& devName,
const QString& label) = 0;
virtual Result<int> waitForDeviceEvent() = 0;
//=== 应用管理 ===
virtual Result<QList<AppInfo>> enumApps(const QString& devName) = 0;
virtual Result<void> createApp(const QString& devName,
const QString& appName,
const QVariantMap& args) = 0;
virtual Result<void> deleteApp(const QString& devName,
const QString& appName) = 0;
virtual Result<void> loginApp(const QString& devName,
const QString& appName,
const QVariantMap& args) = 0;
virtual Result<void> logoutApp(const QString& devName,
const QString& appName) = 0;
virtual Result<void> updateAppPin(const QString& devName,
const QString& appName,
const QVariantMap& args) = 0;
virtual Result<void> unblockApp(const QString& devName,
const QString& appName,
const QVariantMap& args) = 0;
virtual Result<int> getRetryCount(const QString& devName,
const QString& appName,
const QString& role) = 0;
//=== 容器管理 ===
virtual Result<QList<ContainerInfo>> enumContainers(const QString& devName,
const QString& appName) = 0;
virtual Result<void> createContainer(const QString& devName,
const QString& appName,
const QString& containerName) = 0;
virtual Result<void> deleteContainer(const QString& devName,
const QString& appName,
const QString& containerName) = 0;
//=== 证书操作 ===
virtual Result<QByteArray> generateCsr(const QString& devName,
const QString& appName,
const QString& containerName,
const QVariantMap& args) = 0;
virtual Result<void> importCertificate(const QString& devName,
const QString& appName,
const QString& containerName,
const QVariantMap& args) = 0;
virtual Result<QList<CertInfo>> exportCertificate(const QString& devName,
const QString& appName,
const QString& containerName) = 0;
virtual Result<bool> verifyCertificate(const QString& devName,
const QString& appName,
const QString& containerName) = 0;
//=== 签名与验签 ===
virtual Result<QByteArray> sign(const QString& devName,
const QString& appName,
const QString& containerName,
const QByteArray& data) = 0;
virtual Result<void> verify(const QString& devName,
const QString& appName,
const QString& containerName,
const QByteArray& data,
const QByteArray& signature) = 0;
//=== 文件操作 ===
virtual Result<QStringList> enumFiles(const QString& devName,
const QString& appName) = 0;
virtual Result<void> createFile(const QString& devName,
const QString& appName,
const QString& fileName,
const QByteArray& data,
const QVariantMap& args) = 0;
virtual Result<QByteArray> readFile(const QString& devName,
const QString& appName,
const QString& fileName) = 0;
virtual Result<void> deleteFile(const QString& devName,
const QString& appName,
const QString& fileName) = 0;
//=== 其他 ===
virtual Result<QByteArray> generateRandom(const QString& devName, int count) = 0;
};
} // namespace wekey
#define IDriverPlugin_iid "com.trustasia.wekey.IDriverPlugin/1.0"
Q_DECLARE_INTERFACE(wekey::IDriverPlugin, IDriverPlugin_iid)任务 M1.9: 定义数据类型
文件: src/plugin/interface/PluginTypes.h
#pragma once
#include <QString>
#include <QDateTime>
#include <QMetaType>
namespace wekey {
struct DeviceInfo {
QString deviceName;
QString devicePath;
QString manufacturer;
QString label;
QString serialNumber;
QString hardwareVersion;
QString firmwareVersion;
bool isLoggedIn = false;
};
struct AppInfo {
QString appName;
bool isLoggedIn = false;
};
struct ContainerInfo {
QString containerName;
bool keyGenerated = false;
int keyType = 0; // 0=未知, 1=RSA, 2=SM2
bool certImported = false;
};
struct CertInfo {
QString serialNumber;
QString subject;
QString commonName;
QString issuer;
QDateTime notBefore;
QDateTime notAfter;
int certType = 0; // 1=签名, 2=加密
QString certPem;
QString publicKeyHash;
};
enum class KeyType {
Unknown = 0,
RSA_2048 = 1,
RSA_3072 = 2,
RSA_4096 = 3,
SM2 = 4
};
// 设备事件类型
enum class DeviceEvent {
None = 0,
Inserted = 1,
Removed = 2
};
} // namespace wekey
Q_DECLARE_METATYPE(wekey::DeviceInfo)
Q_DECLARE_METATYPE(wekey::AppInfo)
Q_DECLARE_METATYPE(wekey::ContainerInfo)
Q_DECLARE_METATYPE(wekey::CertInfo)测试 M1.T1: Result 测试
文件: tests/unit/test_result.cpp
#include <QtTest>
#include "common/Result.h"
using namespace wekey;
class TestResult : public QObject {
Q_OBJECT
private slots:
void testOk() {
auto result = Result<int>::ok(42);
QVERIFY(result.isOk());
QVERIFY(!result.isErr());
QCOMPARE(result.value(), 42);
}
void testErr() {
auto result = Result<int>::err(Error(Error::Fail, "test error"));
QVERIFY(!result.isOk());
QVERIFY(result.isErr());
QCOMPARE(result.error().code(), Error::Fail);
}
void testVoidOk() {
auto result = Result<void>::ok();
QVERIFY(result.isOk());
}
void testVoidErr() {
auto result = Result<void>::err(Error(Error::InvalidParam));
QVERIFY(result.isErr());
}
void testMap() {
auto result = Result<int>::ok(10);
auto mapped = result.map([](int x) { return x * 2; });
QVERIFY(mapped.isOk());
QCOMPARE(mapped.value(), 20);
}
void testMapErr() {
auto result = Result<int>::err(Error(Error::Fail));
auto mapped = result.map([](int x) { return x * 2; });
QVERIFY(mapped.isErr());
}
};
QTEST_MAIN(TestResult)
#include "test_result.moc"测试 M1.T2: Config 测试
文件: tests/unit/test_config.cpp
#include <QtTest>
#include <QTemporaryDir>
#include "config/Config.h"
using namespace wekey;
class TestConfig : public QObject {
Q_OBJECT
private slots:
void initTestCase() {
// 使用临时目录避免污染真实配置
}
void testDefaultValues() {
Config& config = Config::instance();
config.reset();
QCOMPARE(config.listenPort(), QString(":9001"));
QCOMPARE(config.logLevel(), QString("info"));
QCOMPARE(config.errorMode(), QString("simple"));
QCOMPARE(config.defaultAppName(), QString("TAGM"));
}
void testSettersGetters() {
Config& config = Config::instance();
config.reset();
config.setListenPort(":8080");
QCOMPARE(config.listenPort(), QString(":8080"));
config.setErrorMode("detailed");
QCOMPARE(config.errorMode(), QString("detailed"));
}
void testModPaths() {
Config& config = Config::instance();
config.reset();
config.setModPath("skf", "/path/to/skf.dll");
config.setModPath("p11", "/path/to/p11.dll");
auto paths = config.modPaths();
QCOMPARE(paths.size(), 2);
QCOMPARE(paths["skf"].toString(), QString("/path/to/skf.dll"));
config.removeModPath("p11");
paths = config.modPaths();
QCOMPARE(paths.size(), 1);
}
};
QTEST_MAIN(TestConfig)
#include "test_config.moc"- CMake 构建成功,生成可执行文件
-
make test所有单元测试通过 - Result 支持值类型和 void 类型
- Error 类支持 SKF 错误码映射
- Config 能正确读写
~/.wekeytool.json - Logger 能输出到文件和发送信号
实现 SKF 驱动插件,封装对厂商 C 库的调用。
任务 M2.1: 定义 SKF C API 头文件
文件: src/plugin/skf/SkfApi.h
#pragma once
#include <cstdint>
#ifdef _WIN32
#define SKF_API __stdcall
#else
#define SKF_API
#endif
// 句柄类型
typedef void* DEVHANDLE;
typedef void* HAPPLICATION;
typedef void* HCONTAINER;
typedef void* HANDLE;
// 基本类型
typedef uint8_t BYTE;
typedef uint8_t* LPSTR;
typedef uint32_t ULONG;
typedef uint32_t* PULONG;
typedef int32_t LONG;
typedef uint16_t WORD;
typedef int BOOL;
// 版本结构
#pragma pack(push, 1)
typedef struct {
BYTE major;
BYTE minor;
} VERSION;
// 设备信息
typedef struct {
VERSION Version;
BYTE Manufacturer[64];
BYTE Issuer[64];
BYTE Label[32];
BYTE SerialNumber[32];
VERSION HWVersion;
VERSION FirmwareVersion;
ULONG AlgSymCap;
ULONG AlgAsymCap;
ULONG AlgHashCap;
ULONG DevAuthAlgId;
ULONG TotalSpace;
ULONG FreeSpace;
ULONG MaxECCBufferSize;
ULONG MaxBufferSize;
BYTE Reserved[64];
} DEVINFO, *PDEVINFO;
// ECC 公钥
typedef struct {
ULONG BitLen;
BYTE XCoordinate[64];
BYTE YCoordinate[64];
} ECCPUBLICKEYBLOB, *PECCPUBLICKEYBLOB;
// ECC 签名
typedef struct {
BYTE r[64];
BYTE s[64];
} ECCSIGNATUREBLOB, *PECCSIGNATUREBLOB;
// ECC 密文
typedef struct {
BYTE XCoordinate[64];
BYTE YCoordinate[64];
BYTE Hash[32];
ULONG CipherLen;
BYTE Cipher[1];
} ECCCIPHERBLOB, *PECCCIPHERBLOB;
#pragma pack(pop)
// 错误码
#define SAR_OK 0x00000000
#define SAR_FAIL 0x0A000001
#define SAR_UNKNOWNERR 0x0A000002
#define SAR_NOTSUPPORTYETERR 0x0A000003
#define SAR_FILEERR 0x0A000004
#define SAR_INVALIDHANDLEERR 0x0A000005
#define SAR_INVALIDPARAMERR 0x0A000006
#define SAR_READFILEERR 0x0A000007
#define SAR_WRITEFILEERR 0x0A000008
#define SAR_NAMELENERR 0x0A000009
#define SAR_KEYUSAGEERR 0x0A00000A
#define SAR_MODULUSLENERR 0x0A00000B
#define SAR_NOTINITIALIZEERR 0x0A00000C
#define SAR_OBLOCONFLICTERR 0x0A00000D
#define SAR_DEVICE_REMOVED 0x0A000023
#define SAR_PIN_INCORRECT 0x0A000024
#define SAR_PIN_LOCKED 0x0A000025
#define SAR_USER_NOT_LOGGED_IN 0x0A00002D
#define SAR_APPLICATION_NOT_EXISTS 0x0A00002E
// 函数指针类型定义
extern "C" {
// 设备管理
typedef ULONG (SKF_API *PFN_SKF_EnumDev)(BOOL bPresent, LPSTR szNameList, PULONG pulSize);
typedef ULONG (SKF_API *PFN_SKF_ConnectDev)(LPSTR szName, DEVHANDLE* phDev);
typedef ULONG (SKF_API *PFN_SKF_DisConnectDev)(DEVHANDLE hDev);
typedef ULONG (SKF_API *PFN_SKF_GetDevInfo)(DEVHANDLE hDev, PDEVINFO pDevInfo);
typedef ULONG (SKF_API *PFN_SKF_SetLabel)(DEVHANDLE hDev, LPSTR szLabel);
typedef ULONG (SKF_API *PFN_SKF_DevAuth)(DEVHANDLE hDev, BYTE* pbAuthData, ULONG ulLen);
typedef ULONG (SKF_API *PFN_SKF_ChangeDevAuthKey)(DEVHANDLE hDev, BYTE* pbKeyValue, ULONG ulKeyLen);
typedef ULONG (SKF_API *PFN_SKF_WaitForDevEvent)(LPSTR szDevName, PULONG pulDevNameLen, PULONG pulEvent);
// 应用管理
typedef ULONG (SKF_API *PFN_SKF_EnumApplication)(DEVHANDLE hDev, LPSTR szAppName, PULONG pulSize);
typedef ULONG (SKF_API *PFN_SKF_CreateApplication)(DEVHANDLE hDev, LPSTR szAppName,
LPSTR szAdminPin, ULONG dwAdminPinRetryCount,
LPSTR szUserPin, ULONG dwUserPinRetryCount,
ULONG dwCreateFileRights, HAPPLICATION* phApp);
typedef ULONG (SKF_API *PFN_SKF_DeleteApplication)(DEVHANDLE hDev, LPSTR szAppName);
typedef ULONG (SKF_API *PFN_SKF_OpenApplication)(DEVHANDLE hDev, LPSTR szAppName, HAPPLICATION* phApp);
typedef ULONG (SKF_API *PFN_SKF_CloseApplication)(HAPPLICATION hApp);
typedef ULONG (SKF_API *PFN_SKF_VerifyPIN)(HAPPLICATION hApp, ULONG ulPINType,
LPSTR szPIN, PULONG pulRetryCount);
typedef ULONG (SKF_API *PFN_SKF_ChangePIN)(HAPPLICATION hApp, ULONG ulPINType,
LPSTR szOldPIN, LPSTR szNewPIN, PULONG pulRetryCount);
typedef ULONG (SKF_API *PFN_SKF_UnblockPIN)(HAPPLICATION hApp, LPSTR szAdminPIN,
LPSTR szNewUserPIN, PULONG pulRetryCount);
// 容器管理
typedef ULONG (SKF_API *PFN_SKF_EnumContainer)(HAPPLICATION hApp, LPSTR szContainerName, PULONG pulSize);
typedef ULONG (SKF_API *PFN_SKF_CreateContainer)(HAPPLICATION hApp, LPSTR szContainerName, HCONTAINER* phContainer);
typedef ULONG (SKF_API *PFN_SKF_DeleteContainer)(HAPPLICATION hApp, LPSTR szContainerName);
typedef ULONG (SKF_API *PFN_SKF_OpenContainer)(HAPPLICATION hApp, LPSTR szContainerName, HCONTAINER* phContainer);
typedef ULONG (SKF_API *PFN_SKF_CloseContainer)(HCONTAINER hContainer);
typedef ULONG (SKF_API *PFN_SKF_GetContainerType)(HCONTAINER hContainer, PULONG pulContainerType);
// 密钥操作
typedef ULONG (SKF_API *PFN_SKF_GenECCKeyPair)(HCONTAINER hContainer, ULONG ulAlgId, PECCPUBLICKEYBLOB pBlob);
typedef ULONG (SKF_API *PFN_SKF_ExportPublicKey)(HCONTAINER hContainer, BOOL bSignFlag,
BYTE* pbBlob, PULONG pulBlobLen);
typedef ULONG (SKF_API *PFN_SKF_ECCSignData)(HCONTAINER hContainer, BYTE* pbData, ULONG ulDataLen,
PECCSIGNATUREBLOB pSignature);
typedef ULONG (SKF_API *PFN_SKF_ECCVerify)(DEVHANDLE hDev, PECCPUBLICKEYBLOB pECCPubKeyBlob,
BYTE* pbData, ULONG ulDataLen, PECCSIGNATUREBLOB pSignature);
// 证书操作
typedef ULONG (SKF_API *PFN_SKF_ImportCertificate)(HCONTAINER hContainer, BOOL bSignFlag,
BYTE* pbCert, ULONG ulCertLen);
typedef ULONG (SKF_API *PFN_SKF_ExportCertificate)(HCONTAINER hContainer, BOOL bSignFlag,
BYTE* pbCert, PULONG pulCertLen);
// 随机数
typedef ULONG (SKF_API *PFN_SKF_GenRandom)(DEVHANDLE hDev, BYTE* pbRandom, ULONG ulRandomLen);
// 文件操作
typedef ULONG (SKF_API *PFN_SKF_EnumFiles)(HAPPLICATION hApp, LPSTR szFileList, PULONG pulSize);
typedef ULONG (SKF_API *PFN_SKF_CreateFile)(HAPPLICATION hApp, LPSTR szFileName, ULONG ulFileSize,
ULONG ulReadRights, ULONG ulWriteRights);
typedef ULONG (SKF_API *PFN_SKF_DeleteFile)(HAPPLICATION hApp, LPSTR szFileName);
typedef ULONG (SKF_API *PFN_SKF_ReadFile)(HAPPLICATION hApp, LPSTR szFileName, ULONG ulOffset,
ULONG ulSize, BYTE* pbOutData, PULONG pulOutLen);
typedef ULONG (SKF_API *PFN_SKF_WriteFile)(HAPPLICATION hApp, LPSTR szFileName, ULONG ulOffset,
BYTE* pbData, ULONG ulSize);
} // extern "C"任务 M2.2: 实现 SkfLibrary 动态加载
文件: src/plugin/skf/SkfLibrary.h
#pragma once
#include <QLibrary>
#include <QString>
#include <memory>
#include "SkfApi.h"
#include "common/Result.h"
namespace wekey {
class SkfLibrary {
public:
explicit SkfLibrary(const QString& path);
~SkfLibrary();
SkfLibrary(const SkfLibrary&) = delete;
SkfLibrary& operator=(const SkfLibrary&) = delete;
[[nodiscard]] bool isLoaded() const;
[[nodiscard]] QString errorString() const;
// 设备管理
PFN_SKF_EnumDev EnumDev = nullptr;
PFN_SKF_ConnectDev ConnectDev = nullptr;
PFN_SKF_DisConnectDev DisConnectDev = nullptr;
PFN_SKF_GetDevInfo GetDevInfo = nullptr;
PFN_SKF_SetLabel SetLabel = nullptr;
PFN_SKF_DevAuth DevAuth = nullptr;
PFN_SKF_ChangeDevAuthKey ChangeDevAuthKey = nullptr;
PFN_SKF_WaitForDevEvent WaitForDevEvent = nullptr;
// 应用管理
PFN_SKF_EnumApplication EnumApplication = nullptr;
PFN_SKF_CreateApplication CreateApplication = nullptr;
PFN_SKF_DeleteApplication DeleteApplication = nullptr;
PFN_SKF_OpenApplication OpenApplication = nullptr;
PFN_SKF_CloseApplication CloseApplication = nullptr;
PFN_SKF_VerifyPIN VerifyPIN = nullptr;
PFN_SKF_ChangePIN ChangePIN = nullptr;
PFN_SKF_UnblockPIN UnblockPIN = nullptr;
// 容器管理
PFN_SKF_EnumContainer EnumContainer = nullptr;
PFN_SKF_CreateContainer CreateContainer = nullptr;
PFN_SKF_DeleteContainer DeleteContainer = nullptr;
PFN_SKF_OpenContainer OpenContainer = nullptr;
PFN_SKF_CloseContainer CloseContainer = nullptr;
PFN_SKF_GetContainerType GetContainerType = nullptr;
// 密钥操作
PFN_SKF_GenECCKeyPair GenECCKeyPair = nullptr;
PFN_SKF_ExportPublicKey ExportPublicKey = nullptr;
PFN_SKF_ECCSignData ECCSignData = nullptr;
PFN_SKF_ECCVerify ECCVerify = nullptr;
// 证书操作
PFN_SKF_ImportCertificate ImportCertificate = nullptr;
PFN_SKF_ExportCertificate ExportCertificate = nullptr;
// 随机数
PFN_SKF_GenRandom GenRandom = nullptr;
// 文件操作
PFN_SKF_EnumFiles EnumFiles = nullptr;
PFN_SKF_CreateFile CreateFile = nullptr;
PFN_SKF_DeleteFile DeleteFile = nullptr;
PFN_SKF_ReadFile ReadFile = nullptr;
PFN_SKF_WriteFile WriteFile = nullptr;
private:
void loadSymbols();
template<typename T>
T loadSymbol(const char* name) {
return reinterpret_cast<T>(lib_.resolve(name));
}
QLibrary lib_;
};
} // namespace wekey任务 M2.3: 实现 SkfPlugin
文件: src/plugin/skf/SkfPlugin.h
#pragma once
#include <QObject>
#include <QMutex>
#include <QMap>
#include <memory>
#include "plugin/interface/IDriverPlugin.h"
#include "SkfLibrary.h"
namespace wekey {
struct HandleInfo {
DEVHANDLE devHandle = nullptr;
HAPPLICATION appHandle = nullptr;
HCONTAINER containerHandle = nullptr;
bool isLoggedIn = false;
};
class SkfPlugin : public QObject, public IDriverPlugin {
Q_OBJECT
Q_PLUGIN_METADATA(IID IDriverPlugin_iid FILE "skf.json")
Q_INTERFACES(wekey::IDriverPlugin)
public:
explicit SkfPlugin(QObject* parent = nullptr);
~SkfPlugin() override;
// 初始化
Result<void> initialize(const QString& libPath);
// IDriverPlugin 实现
Result<QList<DeviceInfo>> enumDevices(bool login = false) override;
Result<void> changeDeviceAuth(const QString& devName,
const QString& oldPin,
const QString& newPin) override;
Result<void> setDeviceLabel(const QString& devName,
const QString& label) override;
Result<int> waitForDeviceEvent() override;
Result<QList<AppInfo>> enumApps(const QString& devName) override;
Result<void> createApp(const QString& devName,
const QString& appName,
const QVariantMap& args) override;
Result<void> deleteApp(const QString& devName,
const QString& appName) override;
Result<void> loginApp(const QString& devName,
const QString& appName,
const QVariantMap& args) override;
Result<void> logoutApp(const QString& devName,
const QString& appName) override;
Result<void> updateAppPin(const QString& devName,
const QString& appName,
const QVariantMap& args) override;
Result<void> unblockApp(const QString& devName,
const QString& appName,
const QVariantMap& args) override;
Result<int> getRetryCount(const QString& devName,
const QString& appName,
const QString& role) override;
Result<QList<ContainerInfo>> enumContainers(const QString& devName,
const QString& appName) override;
Result<void> createContainer(const QString& devName,
const QString& appName,
const QString& containerName) override;
Result<void> deleteContainer(const QString& devName,
const QString& appName,
const QString& containerName) override;
Result<QByteArray> generateCsr(const QString& devName,
const QString& appName,
const QString& containerName,
const QVariantMap& args) override;
Result<void> importCertificate(const QString& devName,
const QString& appName,
const QString& containerName,
const QVariantMap& args) override;
Result<QList<CertInfo>> exportCertificate(const QString& devName,
const QString& appName,
const QString& containerName) override;
Result<bool> verifyCertificate(const QString& devName,
const QString& appName,
const QString& containerName) override;
Result<QByteArray> sign(const QString& devName,
const QString& appName,
const QString& containerName,
const QByteArray& data) override;
Result<void> verify(const QString& devName,
const QString& appName,
const QString& containerName,
const QByteArray& data,
const QByteArray& signature) override;
Result<QStringList> enumFiles(const QString& devName,
const QString& appName) override;
Result<void> createFile(const QString& devName,
const QString& appName,
const QString& fileName,
const QByteArray& data,
const QVariantMap& args) override;
Result<QByteArray> readFile(const QString& devName,
const QString& appName,
const QString& fileName) override;
Result<void> deleteFile(const QString& devName,
const QString& appName,
const QString& fileName) override;
Result<QByteArray> generateRandom(const QString& devName, int count) override;
private:
// 句柄管理
Result<DEVHANDLE> openDevice(const QString& devName);
void closeDevice(const QString& devName);
Result<HAPPLICATION> openApp(const QString& devName, const QString& appName);
void closeApp(const QString& devName, const QString& appName);
Result<HCONTAINER> openContainer(const QString& devName,
const QString& appName,
const QString& containerName);
void closeContainer(const QString& devName,
const QString& appName,
const QString& containerName);
QString makeKey(const QString& dev, const QString& app = {},
const QString& container = {}) const;
std::unique_ptr<SkfLibrary> lib_;
QMap<QString, HandleInfo> handles_;
QMutex mutex_;
};
} // namespace wekey测试 M2.T1: SkfLibrary 加载测试
// 需要真实设备或 Mock 库
void TestSkfPlugin::testLibraryLoad() {
SkfLibrary lib("/path/to/libskf.dll");
QVERIFY(lib.isLoaded());
QVERIFY(lib.EnumDev != nullptr);
QVERIFY(lib.ConnectDev != nullptr);
}- SkfLibrary 能正确加载厂商库
- 所有 SKF 函数指针正确解析
- SkfPlugin 实现所有 IDriverPlugin 方法
- 句柄生命周期正确管理(无泄漏)
- 错误码正确映射到 Error 类
实现业务逻辑层,提供统一的服务接口供 GUI 和 API 使用。
任务 M3.1: 实现插件管理器
文件: src/plugin/PluginManager.h
#pragma once
#include <QObject>
#include <QMap>
#include <QPluginLoader>
#include <memory>
#include "interface/IDriverPlugin.h"
#include "common/Result.h"
namespace wekey {
class PluginManager : public QObject {
Q_OBJECT
public:
static PluginManager& instance();
Result<void> registerPlugin(const QString& name, const QString& path);
Result<void> unregisterPlugin(const QString& name);
IDriverPlugin* getPlugin(const QString& name);
IDriverPlugin* activePlugin();
Result<void> setActivePlugin(const QString& name);
QStringList listPlugins() const;
// 从配置加载所有插件
void loadFromConfig();
signals:
void pluginRegistered(const QString& name);
void pluginUnregistered(const QString& name);
void activePluginChanged(const QString& name);
private:
PluginManager() = default;
struct PluginEntry {
QString path;
std::unique_ptr<QPluginLoader> loader;
IDriverPlugin* instance = nullptr;
};
QMap<QString, PluginEntry> plugins_;
QString activeName_;
};
} // namespace wekey任务 M3.2: 实现设备服务
// src/core/device/DeviceService.h
#pragma once
#include <QObject>
#include <QThread>
#include "plugin/interface/PluginTypes.h"
#include "common/Result.h"
namespace wekey {
class DeviceService : public QObject {
Q_OBJECT
public:
static DeviceService& instance();
Result<QList<DeviceInfo>> enumDevices(bool login = false);
Result<void> changeDeviceAuth(const QString& devName,
const QString& oldPin,
const QString& newPin);
Result<void> setDeviceLabel(const QString& devName, const QString& label);
// 启动设备监听线程
void startDeviceMonitor();
void stopDeviceMonitor();
signals:
void deviceInserted(const QString& devName);
void deviceRemoved(const QString& devName);
void deviceListChanged();
private:
DeviceService() = default;
void monitorLoop();
QThread* monitorThread_ = nullptr;
bool monitoring_ = false;
};
} // namespace wekey任务 M3.3: 实现应用服务
// src/core/application/AppService.h
#pragma once
#include <QObject>
#include "plugin/interface/PluginTypes.h"
#include "common/Result.h"
namespace wekey {
class AppService : public QObject {
Q_OBJECT
public:
static AppService& instance();
Result<QList<AppInfo>> enumApps(const QString& devName);
Result<void> createApp(const QString& devName,
const QString& appName,
const QString& adminPin,
const QString& userPin);
Result<void> deleteApp(const QString& devName, const QString& appName);
Result<void> login(const QString& devName,
const QString& appName,
const QString& pin,
const QString& role);
Result<void> logout(const QString& devName, const QString& appName);
Result<void> changePin(const QString& devName,
const QString& appName,
const QString& oldPin,
const QString& newPin,
const QString& role);
Result<void> unblockPin(const QString& devName,
const QString& appName,
const QString& adminPin,
const QString& newUserPin);
Result<int> getRetryCount(const QString& devName,
const QString& appName,
const QString& role);
signals:
void loginStateChanged(const QString& devName, const QString& appName, bool loggedIn);
void pinError(const QString& devName, const QString& appName, int retryCount);
void pinLocked(const QString& devName, const QString& appName);
private:
AppService() = default;
};
} // namespace wekey类似模式实现,略。
- PluginManager 能动态加载/卸载插件
- DeviceService 设备监听正常工作
- AppService 登录/登出流程正确
- PIN 错误和锁定场景正确处理
- 所有服务通过单元测试
实现与原 Go 版本 100% 兼容的 HTTP REST API。
任务 M4.1: 实现 HTTP 服务器
// src/api/HttpServer.h
#pragma once
#include <QObject>
#include <QTcpServer>
#include <memory>
#include "common/Result.h"
namespace wekey {
class ApiRouter;
class HttpServer : public QObject {
Q_OBJECT
public:
explicit HttpServer(QObject* parent = nullptr);
~HttpServer();
Result<void> start(quint16 port);
void stop();
bool isRunning() const;
quint16 port() const;
signals:
void started(quint16 port);
void stopped();
void requestReceived(const QString& method, const QString& path);
private slots:
void onNewConnection();
void onReadyRead();
void onDisconnected();
private:
void handleRequest(QTcpSocket* socket, const QByteArray& request);
std::unique_ptr<QTcpServer> server_;
std::unique_ptr<ApiRouter> router_;
};
} // namespace wekey任务 M4.2: 实现路由分发
// src/api/ApiRouter.h
#pragma once
#include <QObject>
#include <QString>
#include <QByteArray>
#include <functional>
#include <QMap>
namespace wekey {
struct HttpRequest {
QString method;
QString path;
QMap<QString, QString> headers;
QMap<QString, QString> queryParams;
QByteArray body;
};
struct HttpResponse {
int statusCode = 200;
QString statusText = "OK";
QMap<QString, QString> headers;
QByteArray body;
void setJson(const QJsonObject& json);
void setError(int code, const QString& message);
};
using RouteHandler = std::function<void(const HttpRequest&, HttpResponse&)>;
class ApiRouter : public QObject {
Q_OBJECT
public:
explicit ApiRouter(QObject* parent = nullptr);
void addRoute(const QString& method, const QString& path, RouteHandler handler);
void handleRequest(const HttpRequest& request, HttpResponse& response);
private:
void setupRoutes();
// 公共接口
void handleHealth(const HttpRequest& req, HttpResponse& res);
void handleExit(const HttpRequest& req, HttpResponse& res);
// 业务接口
void handleEnumDev(const HttpRequest& req, HttpResponse& res);
void handleLogin(const HttpRequest& req, HttpResponse& res);
void handleGenCsr(const HttpRequest& req, HttpResponse& res);
void handleImportCert(const HttpRequest& req, HttpResponse& res);
void handleExportCert(const HttpRequest& req, HttpResponse& res);
void handleSign(const HttpRequest& req, HttpResponse& res);
void handleVerify(const HttpRequest& req, HttpResponse& res);
void handleRandom(const HttpRequest& req, HttpResponse& res);
// 管理接口
void handleModCreate(const HttpRequest& req, HttpResponse& res);
void handleModActive(const HttpRequest& req, HttpResponse& res);
void handleModDelete(const HttpRequest& req, HttpResponse& res);
// ... 其他管理接口
struct Route {
QString method;
QString path;
RouteHandler handler;
};
QList<Route> routes_;
};
} // namespace wekey任务 M4.3: 编写 API 兼容性测试脚本
#!/bin/bash
# tests/integration/test_api_compat.sh
BASE_URL="http://localhost:9001"
# 测试健康检查
echo "Testing /health..."
response=$(curl -s "$BASE_URL/health")
echo "$response" | jq -e '.status == "ok"' || exit 1
# 测试枚举设备
echo "Testing /api/v1/enum-dev..."
response=$(curl -s "$BASE_URL/api/v1/enum-dev")
echo "$response" | jq -e '.code == 0' || exit 1
# 测试登录
echo "Testing /api/v1/login..."
response=$(curl -s -X POST "$BASE_URL/api/v1/login" \
-H "Content-Type: application/json" \
-d '{"serialNumber":"TEST","appName":"TAGM","role":"user","pin":"123456"}')
echo "$response" | jq -e '.code' || exit 1
echo "All API compatibility tests passed!"- HttpServer 能正确监听端口
- 端口冲突时返回友好错误
- 所有
/api/v1/*接口实现 - 所有
/admin/*接口实现 - JSON 响应格式与原项目一致
- 错误码与原项目一致
实现 Qt Widgets 桌面界面,包括 6 个功能页面和系统托盘。
任务 M5.1: 实现主窗口框架
// src/gui/MainWindow.cpp 核心结构
void MainWindow::setupUi() {
setWindowTitle("wekey-skf");
setMinimumSize(900, 600);
// 中心部件
auto* central = new QWidget(this);
setCentralWidget(central);
auto* mainLayout = new QHBoxLayout(central);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(0);
// 左侧导航
navList_ = new QListWidget;
navList_->setFixedWidth(150);
navList_->addItem("模块管理");
navList_->addItem("设备管理");
navList_->addItem("应用管理");
navList_->addItem("容器管理");
navList_->addItem("配置管理");
navList_->addItem("日志查看");
// 右侧页面栈
pageStack_ = new QStackedWidget;
pageStack_->addWidget(modulePage_ = new ModulePage);
pageStack_->addWidget(devicePage_ = new DevicePage);
pageStack_->addWidget(appPage_ = new AppPage);
pageStack_->addWidget(containerPage_ = new ContainerPage);
pageStack_->addWidget(configPage_ = new ConfigPage);
pageStack_->addWidget(logPage_ = new LogPage);
mainLayout->addWidget(navList_);
mainLayout->addWidget(pageStack_, 1);
connect(navList_, &QListWidget::currentRowChanged,
pageStack_, &QStackedWidget::setCurrentIndex);
setupStatusBar();
setupTray();
navList_->setCurrentRow(0);
}任务 M5.2 - M5.7: 实现各功能页面
每个页面遵循相同模式:
// src/gui/pages/DevicePage.h
#pragma once
#include <QWidget>
#include <QTableWidget>
#include <QPushButton>
namespace wekey {
class DevicePage : public QWidget {
Q_OBJECT
public:
explicit DevicePage(QWidget* parent = nullptr);
public slots:
void refresh();
private slots:
void onRefreshClicked();
void onDeviceSelected(int row);
void onSetLabelClicked();
void onChangeAuthClicked();
private:
void setupUi();
void updateDeviceList(const QList<DeviceInfo>& devices);
QTableWidget* table_ = nullptr;
QPushButton* refreshBtn_ = nullptr;
QPushButton* setLabelBtn_ = nullptr;
QPushButton* changeAuthBtn_ = nullptr;
// 详情区
QLabel* manufacturerLabel_ = nullptr;
QLabel* hwVersionLabel_ = nullptr;
QLabel* fwVersionLabel_ = nullptr;
};
} // namespace wekey任务 M5.8: 实现登录对话框
// src/gui/dialogs/LoginDialog.h
#pragma once
#include <QDialog>
#include <QLineEdit>
#include <QRadioButton>
#include <QLabel>
namespace wekey {
class LoginDialog : public QDialog {
Q_OBJECT
public:
explicit LoginDialog(const QString& appName, QWidget* parent = nullptr);
QString pin() const;
QString role() const;
void setRetryCount(int count);
private slots:
void onLoginClicked();
private:
void setupUi();
QString appName_;
QLineEdit* pinEdit_ = nullptr;
QRadioButton* userRadio_ = nullptr;
QRadioButton* adminRadio_ = nullptr;
QLabel* retryLabel_ = nullptr;
};
} // namespace wekey任务 M5.9: 实现系统托盘
// src/gui/SystemTray.h
#pragma once
#include <QSystemTrayIcon>
#include <QMenu>
namespace wekey {
class SystemTray : public QSystemTrayIcon {
Q_OBJECT
public:
explicit SystemTray(QWidget* parent = nullptr);
signals:
void showWindowRequested();
void exitRequested();
private slots:
void onActivated(QSystemTrayIcon::ActivationReason reason);
private:
void setupMenu();
QMenu* menu_ = nullptr;
};
} // namespace wekey- 主窗口布局正确(左导航 + 右内容)
- 6 个页面功能正常
- 对话框交互流畅
- 关闭窗口最小化到托盘
- 托盘菜单功能正常
- 错误提示支持简洁/详细模式切换
- 日志页面实时更新、支持搜索过滤
完成集成测试,制作可分发的安装包。
任务 M6.1: 端到端测试
// tests/integration/test_e2e.cpp
class TestE2E : public QObject {
Q_OBJECT
private slots:
void initTestCase() {
// 启动应用
}
void cleanupTestCase() {
// 关闭应用
}
void testFullWorkflow() {
// 1. 枚举设备
// 2. 登录应用
// 3. 创建容器
// 4. 生成 CSR
// 5. 签名
// 6. 登出
}
void testHotPlug() {
// 模拟设备插拔
}
void testPinLock() {
// 测试 PIN 锁定和解锁
}
void testApiCompatibility() {
// 调用所有 API 端点
}
};任务 M6.2: AddressSanitizer 集成
# CMakeLists.txt
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
if(ENABLE_ASAN)
add_compile_options(-fsanitize=address -fno-omit-frame-pointer)
add_link_options(-fsanitize=address)
endif()任务 M6.3: Windows 绿色版打包脚本
# scripts/package_win.ps1
$BuildDir = "build/release"
$OutputDir = "dist/wekey-skf-win64"
# 清理
Remove-Item -Recurse -Force $OutputDir -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path $OutputDir
# 复制主程序
Copy-Item "$BuildDir/wekey-skf.exe" $OutputDir
# 复制 Qt 依赖
windeployqt --no-translations --no-opengl-sw "$OutputDir/wekey-skf.exe"
# 复制驱动插件
New-Item -ItemType Directory -Path "$OutputDir/plugins"
Copy-Item "$BuildDir/plugins/skf.dll" "$OutputDir/plugins/"
# 复制厂商库
New-Item -ItemType Directory -Path "$OutputDir/lib"
Copy-Item "resources/lib/win/libskf.dll" "$OutputDir/lib/"
# 创建 ZIP
Compress-Archive -Path $OutputDir -DestinationPath "dist/wekey-skf-win64.zip"任务 M6.4: macOS DMG 打包脚本
#!/bin/bash
# scripts/package_mac.sh
BUILD_DIR="build/release"
APP_NAME="wekey-skf"
OUTPUT_DIR="dist"
# 清理
rm -rf "$OUTPUT_DIR/$APP_NAME.app"
# 复制 app bundle
cp -R "$BUILD_DIR/$APP_NAME.app" "$OUTPUT_DIR/"
# 部署 Qt 框架
macdeployqt "$OUTPUT_DIR/$APP_NAME.app" -always-overwrite
# 复制厂商库
mkdir -p "$OUTPUT_DIR/$APP_NAME.app/Contents/Resources/lib"
cp resources/lib/mac/libskf.dylib "$OUTPUT_DIR/$APP_NAME.app/Contents/Resources/lib/"
# 代码签名 (需要开发者证书)
# codesign --force --deep --sign "Developer ID Application: ..." "$OUTPUT_DIR/$APP_NAME.app"
# 创建 DMG
hdiutil create -volname "$APP_NAME" -srcfolder "$OUTPUT_DIR/$APP_NAME.app" \
-ov -format UDZO "$OUTPUT_DIR/$APP_NAME.dmg"- 所有单元测试通过
- 集成测试通过
- AddressSanitizer 无内存错误
- Windows 绿色版可运行
- macOS DMG 安装后可运行
- 配置文件兼容原 Go 版本
- HTTP API 通过兼容性测试
| 风险 | 影响 | 可能性 | 缓解措施 |
|---|---|---|---|
| SKF 厂商库兼容性 | 高 | 中 | 提前获取多厂商库进行测试 |
| Qt 版本差异 | 中 | 低 | 锁定 Qt 6.5 LTS 版本 |
| 跨平台编译问题 | 中 | 中 | 使用 CI/CD 持续验证双平台 |
| 内存泄漏 | 高 | 中 | 全程使用 RAII,定期 ASAN 检测 |
| 风险 | 影响 | 可能性 | 缓解措施 |
|---|---|---|---|
| SKF 驱动实现复杂 | 高 | 高 | M2 预留缓冲时间 |
| API 兼容性问题 | 中 | 中 | 尽早开始兼容性测试 |
| GUI 细节调整多 | 低 | 高 | GUI 功能可渐进式完善 |
| 依赖 | 风险 | 缓解措施 |
|---|---|---|
| 厂商 SKF 库 | 可能有 bug 或文档不全 | 建立厂商沟通渠道 |
| Qt 6.x | 框架可能有 bug | 使用 LTS 版本,关注更新 |
- 命名:类名
PascalCase,方法camelCase,成员变量name_ - 缩进:4 空格
- 行宽:100 字符
- 注释:公共 API 必须有 Doxygen 注释
- 主分支:
main - 开发分支:
dev - 功能分支:
feature/m1-xxx - 修复分支:
fix/xxx
# .github/workflows/build.yml
name: Build
on: [push, pull_request]
jobs:
build-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install Qt
uses: jurplel/install-qt-action@v3
with:
version: '6.5.*'
- name: Build
run: |
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
- name: Test
run: ctest --test-dir build -C Release
build-macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Install Qt
uses: jurplel/install-qt-action@v3
with:
version: '6.5.*'
- name: Build
run: |
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
- name: Test
run: ctest --test-dir build下一步行动:确认本方案后,从 M1 开始实施,首先创建 CMake 构建系统和基础模块。