Posted in

Windows任务栏集成、通知中心、快捷方式、卸载入口…Go客户端系统级集成缺失清单(附完整代码库)

第一章:Go语言创建Windows客户端的系统级集成概览

Go语言凭借其静态编译、零依赖分发和原生Windows支持能力,成为构建轻量、安全、可嵌入式Windows客户端的理想选择。与传统C++或.NET方案不同,Go生成的二进制文件无需运行时环境,可直接部署至无管理员权限的终端,并天然适配Windows服务、托盘应用、注册表操作及UAC交互等系统级场景。

核心集成能力维度

  • 进程与服务管理:通过 golang.org/x/sys/windows/svc 包可将Go程序注册为Windows服务,支持启动、暂停、停止等标准生命周期控制;
  • 系统通知与UI交互:利用 github.com/getlantern/systray 实现跨平台系统托盘图标与菜单,配合Windows原生消息循环(syscall.NewCallback)响应用户操作;
  • 注册表读写:借助 golang.org/x/sys/windows/registry 直接访问 HKEY_LOCAL_MACHINE\Software 等键,实现启动项配置、版本信息持久化;
  • 文件关联与协议处理:通过修改 HKEY_CLASSES_ROOT 下的 myapp:// 协议注册项,使自定义URL在浏览器中触发本地Go客户端。

快速验证系统托盘能力

以下代码片段可在Windows上启动最小化托盘应用(需先执行 go get github.com/getlantern/systray):

package main

import "github.com/getlantern/systray"

func main() {
    systray.Run(onReady, nil) // 启动systray主循环
}

func onReady() {
    systray.SetTitle("MyApp")           // 设置托盘标题
    systray.SetTooltip("Go Windows Client") // 鼠标悬停提示
    mQuit := systray.AddMenuItem("退出", "Quit the app") // 添加菜单项
    go func() {
        <-mQuit.ClickedCh // 监听点击事件
        systray.Quit()    // 退出托盘
    }()
}

编译后执行 go build -ldflags "-H windowsgui" 可隐藏控制台窗口,生成纯GUI风格客户端。该标志禁用控制台子系统,避免黑框闪现——这是Windows桌面应用交付的关键细节。

集成目标 推荐Go包/机制 典型用途示例
Windows服务 x/sys/windows/svc 后台日志收集器、网络代理守护进程
注册表操作 x/sys/windows/registry 写入软件安装路径、读取系统区域设置
UAC提权调用 syscall.StartProcess + manifest 需管理员权限的磁盘清理工具触发逻辑
文件资源管理器扩展 github.com/lxn/win(Win32封装) 自定义右键菜单项、缩略图提供器

第二章:Windows任务栏深度集成实践

2.1 任务栏图标的动态注册与状态管理(ITaskbarList3接口封装)

ITaskbarList3 是 Windows 7+ 提供的关键 COM 接口,支持任务栏进度条、覆盖图标、跳转列表等高级状态控制。

核心能力对比

功能 ITaskbarList ITaskbarList3 是否支持
设置进度条
动态覆盖图标
多窗口任务栏分组 增强支持

封装关键步骤

  • 初始化 COM 并获取 ITaskbarList3 实例(需 CoCreateInstance + IID_ITaskbarList3
  • 调用 HrInit() 验证系统兼容性
  • 使用 SetProgressValue(hwnd, ullCompleted, ullTotal) 实时更新进度
// 示例:设置 65% 进度(0~1000 刻度制)
HRESULT hr = pTaskbar->SetProgressValue(hWnd, 650, 1000);
// 参数说明:
// hWnd: 关联的顶层窗口句柄(必须已注册到任务栏)
// 650/1000: 当前完成值与总量,系统自动映射为 0–100% 可视化进度
// 返回 S_OK 表示成功,E_FAIL 表示窗口未激活或接口未就绪
graph TD
    A[初始化COM] --> B[QueryInterface ITaskbarList3]
    B --> C{HrInit成功?}
    C -->|是| D[调用SetProgressValue/SetOverlayIcon]
    C -->|否| E[降级为静态图标管理]

2.2 跳转列表(Jump List)的构建与自定义分类实现

Windows Jump List 是 Shell API 提供的用户快捷入口机制,支持任务(Tasks)与最近/常用项目(Recent/Frequent)两类默认分类。自定义分类需通过 ICustomDestinationList 接口扩展。

创建自定义类别容器

// 初始化自定义跳转列表
ICustomDestinationList* pDestList = nullptr;
HRESULT hr = CoCreateInstance(CLSID_DestinationList, nullptr,
    CLSCTX_INPROC_SERVER, IID_ICustomDestinationList,
    (void**)&pDestList);
// 参数说明:CLSCTX_INPROC_SERVER 表示在宿主进程内加载COM对象;
// IID_ICustomDestinationList 是跳转列表核心接口标识符。

分类注册与数据源绑定

  • 调用 SetAppID(L"Contoso.MyApp") 指定应用唯一标识
  • 使用 BeginList() 获取 IObjectArray 接口写入自定义 IShellLink 集合
  • 通过 AppendCategory(L"报表中心", pItems) 注册命名分类
分类类型 支持动态更新 是否显示图标
Tasks
Custom 是(需 ShellLink 设置)
graph TD
    A[初始化ICustomDestinationList] --> B[SetAppID]
    B --> C[BeginList]
    C --> D[构建IShellLink数组]
    D --> E[AppendCategory]
    E --> F[CommitList]

2.3 任务栏缩略图工具栏(Thumbnail Toolbar)的按钮绑定与事件响应

缩略图工具栏允许在任务栏预览窗口下方直接嵌入自定义命令按钮,无需激活主窗口即可触发操作。

按钮定义与注册流程

需通过 ITaskbarList3::ThumbBarAddButtons 注册最多7个按钮,每个按钮由 THUMBBUTTON 结构描述:

THUMBBUTTON thumbBtn = {0};
thumbBtn.dwMask = THB_ICON | THB_TOOLTIP | THB_FLAGS;
thumbBtn.iId = 1; // 唯一标识符,用于消息分发
thumbBtn.hIcon = LoadIcon(hInst, MAKEINTRESOURCE(IDI_PLAY));
wcscpy_s(thumbBtn.szTip, L"播放");
thumbBtn.dwFlags = THBF_ENABLED;
// 调用 ThumbBarAddButtons(...) 完成绑定

iId 是后续 WM_COMMAND 消息中 wParam 的低位字,系统据此路由点击事件;dwFlags 控制启用/禁用/不可见状态,动态调用 ThumbBarUpdateButtons 可刷新。

事件响应机制

窗口需处理 WM_COMMAND 消息并检查 HIWORD(wParam) == THBN_CLICKED

wParam (loword) 含义 响应建议
1 播放按钮 启动媒体播放线程
2 暂停按钮 切换暂停/继续状态
3 静音按钮 切换音频输出
graph TD
    A[用户悬停缩略图] --> B[系统显示工具栏]
    B --> C[点击某按钮]
    C --> D[发送 WM_COMMAND + THBN_CLICKED]
    D --> E[窗口消息循环捕获]
    E --> F[根据 iId 分发业务逻辑]

2.4 进度条与覆盖图标(Overlay Icon)的实时同步机制设计

数据同步机制

进度条与 Overlay Icon 的状态必须严格一致,否则引发用户认知冲突。核心采用单源状态驱动:所有 UI 变更均源于统一的 TaskProgress 模型。

class TaskProgress {
  private _value: number = 0;
  private observers: Array<(v: number) => void> = [];

  set value(v: number) {
    this._value = Math.max(0, Math.min(100, v)); // 限幅 [0,100]
    this.observers.forEach(cb => cb(this._value));
  }

  subscribe(cb: (v: number) => void): () => void {
    this.observers.push(cb);
    return () => { this.observers = this.observers.filter(f => f !== cb); };
  }
}

逻辑分析:value setter 强制归一化并触发广播;subscribe 支持多端响应(进度条 DOM 更新 + Shell Icon 刷新),避免轮询或竞态。

同步策略对比

策略 延迟 CPU 开销 Shell API 兼容性
轮询(每200ms)
文件系统事件监听 ❌(仅限 NTFS)
状态模型广播 极低 极低 ✅(跨平台)

图标更新流程

graph TD
  A[TaskProgress.value = 65] --> B{状态变更?}
  B -->|是| C[通知进度条组件]
  B -->|是| D[调用 IShellIconOverlayIdentifier]
  C --> E[CSS transition 渲染]
  D --> F[Shell 重绘覆盖图标]

2.5 多实例场景下任务栏分组与窗口关联策略(AppUserModelID一致性保障)

在多进程启动模式下,Windows 任务栏默认将同名进程视为独立应用,导致图标重复、跳转混乱。核心解法是强制统一 AppUserModelID(AUMID)。

AUMID 设置时机与范围

必须在窗口创建前调用 SetCurrentProcessExplicitAppUserModelID(),且所有子进程需继承同一 AUMID 字符串(如 "com.example.myapp.v2"),不可动态生成。

关键代码示例

// 主进程入口设置(仅一次,早于 CreateWindow)
HRESULT hr = SetCurrentProcessExplicitAppUserModelID(
    L"com.example.myapp.standalone"  // ✅ 全局唯一、静态、无版本漂移
);
if (FAILED(hr)) { /* 日志告警 */ }

逻辑分析SetCurrentProcessExplicitAppUserModelID 作用于当前进程句柄,影响后续所有 CreateWindowEx 创建的顶层窗口;参数为 UTF-16 字符串,需全局常量存储,避免拼接或运行时构造——否则多实例间 AUMID 不一致,任务栏分组失效。

多实例协同要点

  • 所有子进程(含 ShellExecute 启动的副本)须显式继承父进程 AUMID
  • 窗口消息循环中监听 WM_TASKBARBUTTONCREATED 进行二次绑定
场景 AUMID 是否一致 任务栏分组效果
单实例启动 正常合并
多实例 + 静态 AUMID 统一图标+跳转
多实例 + 动态 AUMID 分散图标
graph TD
    A[新进程启动] --> B{是否调用 SetCurrentProcessExplicitAppUserModelID?}
    B -->|是| C[注册统一AUMID]
    B -->|否| D[使用默认可执行名 → 分组失败]
    C --> E[所有顶层窗口绑定同一AUMID]
    E --> F[任务栏按AUMID聚合窗口]

第三章:通知中心与现代Toast通知集成

3.1 Windows 10/11 Toast XML模板生成与动态内容注入

Toast 通知的 XML 模板需严格遵循 UWP 通知架构规范,支持 <toast> 根节点及 <visual><actions> 等子结构。

动态内容注入方式

  • 使用 ToastNotificationManager.GetTemplateContent() 获取预定义模板(如 ToastGeneric
  • 通过 XmlDocument.LoadXml() 加载后,用 SelectSingleNode() 定位占位节点(如 //text[@id='title']
  • 调用 InnerText 属性写入运行时数据(用户昵称、时间戳等)

示例:带图像与按钮的模板片段

<toast launch="action=view&id=123">
  <visual>
    <binding template="ToastGeneric">
      <text id="title">欢迎回来</text>
      <text id="body">您有 {{unreadCount}} 条新消息</text>
      <image id="appLogo" src="{{logoUri}}" alt="App Logo"/>
    </binding>
  </visual>
  <actions>
    <action content="查看" activationType="foreground" arguments="view"/>
  </actions>
</toast>

逻辑分析{{unreadCount}}{{logoUri}} 是占位符,需在 C# 中通过 XmlDocumentSelectSingleNode 找到对应 id 节点并替换 InnerTextSetAttributelaunch 属性决定后台激活行为,activationType="foreground" 表示前台启动应用。

占位符 替换方式 安全要求
{{title}} node.InnerText 需 HTML 编码
{{logoUri}} node.SetAttribute("src", uri) 必须为 ms-appx:/// 或 HTTPS
graph TD
  A[加载XML模板] --> B[解析DOM树]
  B --> C[定位id节点]
  C --> D[注入动态值]
  D --> E[序列化为IBuffer]
  E --> F[创建ToastNotification]

3.2 后台激活与前台唤醒的双模式通知处理(IActivatedEventArgs适配)

Windows 应用需统一响应两类启动上下文:后台推送唤醒(如 Toast 激活)与前台用户点击(如协议启动)。核心在于 IActivatedEventArgs 的多态适配。

统一入口分发逻辑

protected override void OnActivated(IActivatedEventArgs args)
{
    switch (args.Kind)
    {
        case ActivationKind.ToastNotification:
            HandleToastActivation(args as ToastNotificationActivatedEventArgs);
            break;
        case ActivationKind.Protocol:
            HandleProtocolActivation(args as ProtocolActivatedEventArgs);
            break;
        default:
            // 忽略非通知类激活
            break;
    }
}

args.Kind 决定激活来源类型;ToastNotificationActivatedEventArgs 提供 Arguments(原始 JSON 字符串)和 UserInput(交互字段);ProtocolActivatedEventArgs.Uri 携带自定义协议载荷。必须显式类型转换,避免空引用。

激活参数对比表

属性 ToastNotificationActivatedEventArgs ProtocolActivatedEventArgs
核心数据源 Arguments(JSON 字符串) Uri(URI Scheme 载荷)
用户交互支持 UserInput 字典 ❌ 无原生输入映射
后台静默能力 ✅ 支持后台任务唤醒 ❌ 仅前台进程激活

生命周期协同流程

graph TD
    A[系统触发激活] --> B{IActivatedEventArgs.Kind}
    B -->|ToastNotification| C[启动后台任务/恢复前台]
    B -->|Protocol| D[解析Uri并导航]
    C --> E[同步状态 → 更新UI/推送日志]

3.3 通知操作按钮响应与应用内事件路由(通过COM激活上下文桥接)

当用户点击系统通知中的操作按钮(如“回复”“标记为已读”),Windows 通过 COM 激活机制触发注册的 IActivatedEventArgs 实现类,并将上下文注入应用进程。

路由分发核心逻辑

// 在 App::OnActivated 中提取操作ID与payload
void App::OnActivated(IActivatedEventArgs* args) {
    if (args && args->get_Kind() == ActivationKind_ToastNotificationActionTriggered) {
        ComPtr<IToastNotificationActionTriggerDetail> detail;
        args->QueryInterface(IID_PPV_ARGS(&detail));
        HSTRING actionId; detail->get_ActionId(&actionId); // e.g., "reply"
        HSTRING arguments; detail->get_Arguments(&arguments); // JSON string
    }
}

ActionId 标识预定义行为类型;Arguments 是开发者序列化的上下文数据(如 "threadId=123&userId=456"),需在 UI 线程安全解析。

COM 上下文桥接关键约束

组件 要求 说明
激活契约 必须注册 windows.toastNotificationActivation 清单中声明 ToastNotificationActivated 扩展
线程模型 应用主STA线程接收回调 非UI线程需 CoreDispatcher::RunAsync 路由
数据大小 Arguments ≤ 2048 字符 超长需转为本地存储Key
graph TD
    A[Toast Button Click] --> B[OS COM Dispatcher]
    B --> C{App 已运行?}
    C -->|是| D[STA线程调用 OnActivated]
    C -->|否| E[启动新实例 + OnActivated]
    D --> F[解析 Arguments → 路由至 ViewModel]

第四章:系统级快捷方式与卸载入口标准化建设

4.1 Start Menu快捷方式的ShellLink COM自动化创建与属性配置

通过 Windows Shell COM 接口 IShellLink,可编程生成符合系统规范的 Start Menu 快捷方式。

核心接口调用流程

// 创建 IShellLink 实例并获取 IPersistFile
HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER,
                              IID_IShellLink, (void**)&pShellLink);
pShellLink->SetPath(L"C:\\App\\launcher.exe");
pShellLink->SetArguments(L"--start-minimized");
pShellLink->SetWorkingDirectory(L"C:\\App\\");
pShellLink->SetIconLocation(L"C:\\App\\icon.ico", 0);

SetPath() 指定目标可执行文件;SetArguments() 传递启动参数;SetWorkingDirectory() 确保相对路径解析正确;SetIconLocation() 绑定图标资源及索引。

关键属性对照表

属性 COM 方法 说明
目标路径 SetPath() 必填,支持 EXE/LNK/URL
启动参数 SetArguments() 非空时自动追加到路径后
工作目录 SetWorkingDirectory() 影响 . 解析与 DLL 加载
graph TD
    A[CoCreateInstance] --> B[IShellLink::SetPath]
    B --> C[IShellLink::SetArguments]
    C --> D[IPersistFile::Save]

4.2 桌面与快速启动栏快捷方式的符号链接兼容性处理(LNK vs. JUNCTION)

Windows 桌面及快速启动栏(%APPDATA%\Microsoft\Internet Explorer\Quick Launch)仅原生识别 .lnk 快捷方式,对 JUNCTION(目录交接点)或 SYMLINKD 无响应——点击即报“找不到项目”。

兼容性限制根源

  • .lnk 是 COM 封装的二进制格式,含目标路径、工作目录、图标索引等元数据;
  • JUNCTION 是 NTFS 内核级重解析点,无 Shell 扩展支持,Explorer 不触发其解析。

推荐迁移方案

# 创建语义等价的.lnk(非junction),指向统一配置目录
$shell = New-Object -ComObject WScript.Shell
$shortcut = $shell.CreateShortcut("$env:USERPROFILE\Desktop\MyApp.lnk")
$shortcut.TargetPath = "C:\Config\MyApp\Launcher.exe"
$shortcut.WorkingDirectory = "C:\Config\MyApp"
$shortcut.Save()

逻辑分析WScript.Shell.CreateShortcut 生成标准 COM .lnk,确保 Shell 命名空间完全兼容;TargetPath 必须为绝对路径,WorkingDirectory 影响相对资源加载,二者缺一不可。

特性 .lnk JUNCTION
Explorer 可见 ❌(仅显示为空白图标)
支持工作目录设置
跨卷支持 ✅(需启用/D参数) ❌(仅限NTFS同卷)
graph TD
    A[用户点击桌面图标] --> B{Shell 解析类型}
    B -->|LNK文件| C[调用IShellLink解析路径+参数]
    B -->|JUNCTION| D[内核重定向,但Shell不触发后续动作]
    C --> E[正常启动]
    D --> F[静默失败/提示“找不到”]

4.3 注册表卸载项(Uninstall Registry Key)的合规写入与版本语义化管理

Windows 应用卸载体验依赖 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{GUID} 下键值的规范性。语义化版本(SemVer 2.0)必须精确映射到 DisplayVersionVersion(DWORD)双字段。

关键字段语义对齐

  • DisplayName:用户可见名称(不可含控制字符)
  • DisplayVersion:严格遵循 MAJOR.MINOR.PATCH[-prerelease](如 "2.1.0-beta.3"
  • Version:仅用于排序,需转换为 32 位整数(MAJOR << 16 | MINOR << 8 | PATCH

写入示例(PowerShell)

$version = [System.Version]"2.1.0"
$dwVersion = ($version.Major -shl 16) -bor ($version.Minor -shl 8) -bor $version.Build

Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\MyApp" `
  -Name "DisplayVersion" -Value "2.1.0" `
  -Type String
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\MyApp" `
  -Name "Version" -Value $dwVersion `
  -Type DWord

逻辑说明:-shl 实现位移对齐;-bor 执行无进位按位或;确保 Version 字段可被 MSI/Control Panel 正确解析并参与版本比较。

版本字段映射对照表

DisplayVersion Version (DWORD, hex) 解析逻辑
1.0.0 0x00010000 1<<16 \| 0<<8 \| 0
2.1.5 0x00020105 2<<16 \| 1<<8 \| 5
graph TD
  A[语义化字符串] --> B{Parse Version}
  B --> C[Major/Minor/Patch]
  C --> D[Shift & OR → DWORD]
  D --> E[写入Uninstall Key]

4.4 MSI/WIX协同方案:Go客户端作为Custom Action宿主的注册表清理钩子设计

核心设计思路

将轻量级 Go CLI 二进制嵌入 MSI,通过 CustomActionType = 3072msiexec 外部进程调用)触发,避免 DLL 注册/卸载依赖与 .NET 运行时限制。

注册表清理钩子实现

// cleanup_hook.go:接收 MSI 传递的 ProductCode 和 InstallLocation
func main() {
    if len(os.Args) < 3 {
        os.Exit(1) // MSI 传入: [exe, ProductCode, InstallLocation]
    }
    productCode := os.Args[1]
    installPath := os.Args[2]

    keyPath := fmt.Sprintf(`SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\%s`, productCode)
    _ = registry.DeleteKey(registry.LOCAL_MACHINE, keyPath) // 清理遗留卸载项
}

逻辑分析:MSI 在 InstallExecuteSequenceRemoveExistingProducts 后、InstallFinalize 前调用该二进制;productCode 确保精准定位,installPath 可扩展用于路径级清理。Go 静态链接避免 DLL Hell。

WIX 集成关键配置

属性 说明
Binary cleanup_hook.exe 嵌入 MSI 的 Go 二进制
CustomAction CleanupRegistryHook Type=3072, Source=Binary, Target=cleanup_hook.exe
InstallExecuteSequence CleanupRegistryHook After="RemoveExistingProducts"
graph TD
    A[MSI 执行 RemoveExistingProducts] --> B[启动 cleanup_hook.exe]
    B --> C[读取 MSI 会话属性 ProductCode]
    C --> D[删除 HKLM\...\Uninstall\{ProductCode}]
    D --> E[返回 exit code 0 → MSI 继续 InstallFinalize]

第五章:完整代码库结构说明与跨版本兼容性总结

项目根目录组织规范

当前代码库采用标准化的分层结构,根目录下包含 src/(核心逻辑)、tests/(单元与集成测试)、migrations/(数据库迁移脚本)、configs/(环境感知配置)、scripts/(CI/CD辅助工具)和 docs/(API契约与部署手册)。其中 src/ 下严格遵循领域驱动设计原则,划分为 core/(业务规则引擎)、adapters/(外部服务适配器,含 Kafka、PostgreSQL、Redis 客户端封装)、interfaces/(REST/gRPC 接口定义)三类子模块。所有模块均通过 pyproject.toml 中的 [[project.optional-dependencies]] 实现按需加载,避免运行时依赖污染。

多版本 Python 兼容策略

代码库支持 Python 3.9–3.12 全版本,关键兼容措施包括:

  • 使用 typing.Union 替代 | 操作符(Python
  • dataclasses 字段默认值通过 field(default_factory=list) 显式声明,规避 3.9 中 default=[] 的可变默认陷阱;
  • 异步日志写入采用 asyncio.to_thread()(3.9+)与 loop.run_in_executor()(3.9 降级方案)双路径实现;
  • 所有类型注解在 py.typed 文件存在前提下,通过 mypy --python-version 3.9--python-version 3.12 分别校验。

数据库迁移版本控制矩阵

迁移文件名 支持最低 PostgreSQL 版本 关键变更描述 是否可逆
001_init_schema.py 12.0 创建 users, orders 表及索引
002_add_jsonb_index.py 13.0 orders.metadata 上添加 GIN 索引
003_partition_orders.py 14.0 created_at 对 orders 表分区 是(需先禁用写入)

跨版本 HTTP 客户端行为差异处理

当调用第三方支付网关时,adapters/payment/client.py 内置自动协商机制:

  • Python 3.11+:启用 httpx.AsyncClient(transport=httpx.HTTPTransport(retries=3))
  • Python 3.9–3.10:降级为 httpx.AsyncClient(timeout=httpx.Timeout(30.0, connect=10.0)) 并手动重试;
  • 所有版本统一注入 X-Client-Version: v2.4.1-py{major}{minor} 请求头,供上游服务路由至对应兼容逻辑分支。
flowchart LR
    A[HTTP 请求发起] --> B{Python 版本 ≥ 3.11?}
    B -->|是| C[启用 Transport 层重试]
    B -->|否| D[应用 asyncio.sleep + try/except 重试]
    C --> E[发送请求]
    D --> E
    E --> F[解析响应状态码]
    F -->|5xx| G[触发指数退避重试]
    F -->|2xx| H[返回 JSON 响应体]

配置加载优先级链

环境变量 > configs/local.yaml(本地开发) > configs/staging.yaml(预发) > configs/production.yaml(生产),但 configs/*.yaml 中所有 secret_* 字段均被 os.getenv() 强制覆盖,确保密钥不硬编码。configs/base.yaml 定义全部可选字段默认值,并通过 pydantic.BaseSettings 校验类型安全。

测试覆盖率保障机制

tests/ 目录下包含三类测试:

  • unit/:使用 pytest-mock 隔离外部依赖,覆盖所有 core/ 业务逻辑分支;
  • integration/:启动 Docker Compose 启动 PostgreSQL 13/14 双实例,验证迁移脚本在不同版本下的执行一致性;
  • compatibility/:独立 CI job,在 GitHub Actions 中并行运行 ubuntu-20.04-py39ubuntu-22.04-py311macos-13-py312 三个环境,强制要求 coverage report -m | grep 'TOTAL' | awk '{print $4}' 输出 ≥92.5%。

关注异构系统集成,打通服务之间的最后一公里。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注