Skip to content

C 语言静态库对接(ka-simple-c)

面向 Windows C/C++ 开发者。本 SDK 为 纯 C 静态库#include "kauth.h" + 链接 kauth.lib,最终只产出 一个 exe,不需要再加载 kauth.dll

官网:https://kauth.cn

下载(作者交付物)

不提供 SDK 内部源码。 作者从网盘下载编译好的库与示例即可:

https://pan.kauth.cn/C%E8%AF%AD%E8%A8%80%E5%8D%A1%E5%AF%86%E6%A0%B8%E5%BF%83%E6%BA%90%E7%A0%81

典型包内文件:

文件说明
kauth.h公开头文件(结构体与 API 定义以它为准)
kauth_vmp.h可选 VMP 标记宏(完整示例会用到)
kauth.libMSVC x64 /MT 静态库
ka_login.c快速接入示例(控制台)
ka_win.c完整接入示例(Win32 登录/业务窗/心跳/VMP)
(可选)VMProtectSDK64.lib / .dll仅开启 VMP 时需要

协议加密、签名、HTTPS 均在 kauth.lib 内完成。


环境要求

要求
编译器MSVC(Visual Studio),不要用 MinGW
架构x64
运行时库多线程 (/MT),不可与 /MD 混链
系统Windows(HTTPS 使用 Schannel)

系统库 ws2_32 / secur32 / crypt32 / bcrypt 已由库内自动链接。ka_win.c 另需 comctl32(示例内已 #pragma comment)。


商家后台配置

字段来源
api_domainhttps://api.kauth.cn
program_id程序管理 → 程序列表
program_secret同上
merchant_public_key系统设置 → 密钥配置 → RSA 公钥 → PKCS#8

签名类型固定 RSA_SHA256_V2(可不传,SDK 默认)。请把示例里的配置换成你自己的再编译。


API 一览(能力总表)

约定:

  • 业务接口多数返回 BaseResponsecode == 200 成功,失败看 msg
  • 带「出参结构体」的接口:返回值仍是 BaseResponse,详细字段写在第二个参数里(部分接口只有请求、无独立出参结构体)。
  • 字符串均为 UTF-8;结构体请先 memset 再填字段。
  • CaptchaResponse.captchaCode 需标准库 free;加密 HTTP POST 返回串需 kauth_free
  • deviceId = kauth 设备绑定;hardwareId = VMP HWID(可选,仅开 VMP 时需要)。

能力速览

分类能力
初始化 / Token配置、设/取 token、释放内存
设备生成 UUID、本机 deviceId
用户认证验证码、卡密/账密/试用登录、注册、退出、心跳单次、用户信息
充值 / 改密 / 解绑账号充值、以卡充卡、改密、账密/卡密解绑、已登录卡密解绑
配置 / 程序用户/卡密自定义配置、程序详情、服务器时间
远程控制远程变量、远程数据 CRUD、远程函数
脚本最新脚本、脚本下载、脚本错误上报
自动心跳启停心跳、查询状态
底层 / 调试加密 HTTP POST
VMP(可选)HWID、设锁、状态、序列号明文

每个接口均为二级标题,下面跟方法签名,再写用途 / 入参 / 出参。

初始化 SDK

c
int kauth_init(const kauth_init_options *opt);

用途:按字段初始化 SDK。

入参 kauth_init_options

字段类型说明
api_domainconst char*API 域名,如 https://api.kauth.cn
program_idconst char*程序 ID
program_secretconst char*程序密钥
merchant_public_keyconst char*商户 RSA 公钥(PKCS#8 Base64)
sign_typeconst char*NULL,默认 RSA_SHA256_V2

出参:返回 0 成功,非 0 失败。

设置访问令牌

c
void kauth_set_token(const char *token);

用途:手动写入访问令牌。登录成功时 SDK 一般会自动设置。

入参token 字符串;传空串可清空。

出参:无。

读取配置与令牌

c
const char *kauth_get_token(void);
const char *kauth_get_api_domain(void);
const char *kauth_get_program_id(void);
const char *kauth_get_program_secret(void);
const char *kauth_get_merchant_public_key(void);
const char *kauth_get_sign_type(void);

用途:读取当前内存中的配置或 token。

入参:无。

出参:内部静态缓冲的 const char*(勿 free)。

释放堆内存

c
void kauth_free(char *ptr);

用途:释放「加密 HTTP POST」等返回的堆内存。

入参:待释放指针(可为 NULL)。

出参:无。

加密 HTTP POST(底层)

c
char *kauth_http_post(const char *url, const char *data_json);

用途:走 SDK 加密协议的 HTTP POST(一般业务请用封装好的 API,不必直接调)。

入参

参数说明
url路径或完整 URL(以实现为准)
data_json业务 data JSON;空 body 接口可传 "{}"

出参:envelope JSON 字符串,需 kauth_free;失败可能返回 NULL

生成 UUID

c
char *kauth_generate_uuid(char *output);

用途:生成 UUID 字符串。

入参output 调用方缓冲(足够长)。

出参:返回 output 指针(便于链式使用)。

获取本机设备 ID

c
char *kauth_get_device_unique_id(char *output, size_t output_size);

用途:取本机设备唯一 ID,填入登录/解绑的 deviceId

入参:输出缓冲及容量。

出参:成功返回 output;失败可能返回 NULL(以实现为准)。

获取图形验证码

c
BaseResponse kauth_get_captcha(CaptchaRequest *request, CaptchaResponse *response);

用途:获取图形验证码。

入参 CaptchaRequest

字段说明
uuid可选;客户端可自带 uuid

出参 CaptchaResponse

字段说明
code / msg结果
captchaUuid后续登录/注册要带回
captchaCode验证码内容指针,free
captchaData附加数据(如 Base64 图)

同时返回 BaseResponse(与 response 中 code/msg 一致使用即可)。

卡密登录

c
BaseResponse kauth_ka_login(KaLoginRequest *request, LoginResponse *response);

用途:卡密登录。成功后 SDK 自动设置 token;若有 vmpSerial 会尝试设锁。

入参 KaLoginRequest

字段说明
kaPwd卡密
captchaCode / captchaUuid若后台开启验证码则必填
deviceId设备绑定 ID
platformType平台标识,如 win / windows
hardwareId可选,VMP HWID

出参 LoginResponse

字段说明
code / msg结果;msg 常含到期文案
userId / nickName用户信息
token访问令牌
pongInterval心跳间隔(秒或毫秒,示例会兼容解析)
vmpSerialVMP 授权锁;未配置 VMP 时为空

账号密码登录

c
BaseResponse kauth_pwd_login(PwdLoginRequest *request, LoginResponse *response);

用途:账号密码登录。

入参 PwdLoginRequest

字段说明
userName / password账号密码
captchaCode / captchaUuid按需
deviceId设备 ID
hardwareId可选 VMP HWID

出参:同 LoginResponse

试用登录

c
BaseResponse kauth_trial_login(TrialLoginReq *request, LoginResponse *response);

用途:试用登录。

入参 TrialLoginReq

字段说明
captchaCode / captchaUuid按需
deviceId设备 ID
platformType平台
hardwareId可选 VMP HWID

出参:同 LoginResponse

账号注册

c
BaseResponse kauth_register(RegisterRequest *request, RegisterResponse *response);

用途:账号注册(绑定卡密)。成功不等于已登录,需再调登录接口。

入参 RegisterRequest

字段说明
userName / password账号密码
kaPassword绑定卡密
captchaCode / captchaUuid按需

出参 RegisterResponse

字段说明
code / msg结果
userId / nickName / token / pongInterval若服务端返回则填入;示例会清空 token,避免误入业务

退出登录

c
BaseResponse kauth_logout(void);

用途:退出登录。

入参:无(使用当前 token)。

出参BaseResponse

单次心跳

c
BaseResponse kauth_pong(void);

用途:发送一次心跳(手动)。日常更推荐「启动自动心跳」。

入参:无。

出参BaseResponse

获取用户信息

c
BaseResponse kauth_get_user_info(UserInfo *response);

用途:获取当前登录用户信息。

入参:无(需已登录)。

出参 UserInfo

字段说明
code / msg结果
userIdStr用户 ID 字符串
serverExpireTime服务到期时间
serverRemainNum剩余次数/点数等
serverType服务类型
trial是否试用

账号充值

c
BaseResponse kauth_recharge(RechargeRequest *request);

用途:用卡密给账号充值。

入参 RechargeRequest

字段说明
loginName账号
kaPassword充值卡密
deviceId设备 ID
captchaCode / captchaUuid按需

出参BaseResponse(无独立业务结构体)。

以卡充卡

c
BaseResponse kauth_recharge_ka(KaRechargeKaReq *request);

用途:以卡充卡(充值卡续原卡)。

入参 KaRechargeKaReq

字段说明
cardPwd原卡密
rechargeCardPwd充值卡

出参BaseResponse

修改密码

c
BaseResponse kauth_change_password(ResetPwdRequest *request);

用途:修改账号密码。

入参 ResetPwdRequest

字段说明
userName账号
oldPassword旧密码
newPassword新密码
confirmPassword确认密码(可与新密码相同)
captchaCode / captchaUuid按需

出参BaseResponse

账密解绑设备(未登录)

c
BaseResponse kauth_unbind_device(UnbindDeviceRequest *request);

用途:未登录状态下,用账密解绑设备。

入参 UnbindDeviceRequest

字段说明
loginName / password账号密码
deviceId要解绑的设备
captchaCode / captchaUuid按需

出参BaseResponse

卡密解绑设备(未登录)

c
BaseResponse kauth_unbind_device_ka_pwd(UnbindDeviceKaPwdRequest *request);

用途:未登录状态下,用卡密解绑设备。

入参 UnbindDeviceKaPwdRequest

字段说明
kaPwd卡密
deviceId设备 ID

出参BaseResponse

卡密解绑当前设备(已登录)

c
BaseResponse kauth_card_unbind_device(void);

用途:已登录卡密会话下,解绑当前设备。

入参:无(使用当前 token)。

出参BaseResponse

获取用户自定义配置

c
BaseResponse kauth_get_user_config(CustomConfigResp *response);

用途:读取用户自定义配置。

入参:无(需已登录,视后台策略)。

出参 CustomConfigRespcode / msg / configJson

更新用户自定义配置

c
BaseResponse kauth_update_user_config(UpdateCustomConfigReq *request);

用途:更新用户自定义配置。

入参 UpdateCustomConfigReqconfig(最长约 4096,JSON 或文本以业务约定为准)。

出参BaseResponse

获取卡密自定义配置

c
BaseResponse kauth_get_ka_config(CustomConfigResp *response);

用途:读取卡密自定义配置。

出参 CustomConfigRespcode / msg / configJson

更新卡密自定义配置

c
BaseResponse kauth_update_ka_config(UpdateCustomConfigReq *request);

用途:更新卡密自定义配置。

入参 UpdateCustomConfigReqconfig

出参BaseResponse

获取程序详情

c
BaseResponse kauth_get_program_detail(ProgramDetailResponse *response);

用途:获取程序详情与版本信息(可未登录调用,视后台策略)。

入参:无(使用已 init 的 programId)。

出参 ProgramDetailResponse

字段说明
programIdStr / name程序 ID / 名称
forceUpdateDict是否强制更新
status程序状态
enableRegisterDict是否允许注册
notice公告
versionversionNo / versionName / versionDesc / versionDownUrl

获取服务器时间

c
BaseResponse kauth_get_server_time(ServerTimeResp *response);

用途:获取服务器时间。

入参:无。

出参 ServerTimeRespserverTime / serverTimeSec / serverTimeStr

获取远程变量

c
BaseResponse kauth_get_remote_var(GetRemoteVarReq *request, RemoteVarResp *response);

用途:获取远程变量。

入参 GetRemoteVarReqkey

出参 RemoteVarRespkey / value / type / createTime / updateTime

获取远程数据

c
BaseResponse kauth_get_remote_data(GetRemoteVarReq *request, RemoteVarResp *response);

用途:按 key 读取远程数据。

入参key

出参RemoteVarResp

添加远程数据

c
BaseResponse kauth_add_remote_data(RemoteDataAddReq *request);

用途:新增远程数据。

入参 RemoteDataAddReqkey / value / type

出参BaseResponse

更新远程数据

c
BaseResponse kauth_update_remote_data(RemoteDataUpdateReq *request);

用途:更新远程数据。

入参 RemoteDataUpdateReqkey / value

出参BaseResponse

删除远程数据

c
BaseResponse kauth_delete_remote_data(RemoteDataDeleteReq *request);

用途:删除远程数据。

入参 RemoteDataDeleteReqkey

出参BaseResponse

调用远程函数

c
BaseResponse kauth_call_function(CallFunctionReq *request, CallFunctionResp *response);

用途:调用云端/远程函数。

入参 CallFunctionReqfunctionNameparamsJson(参数 JSON 字符串)。

出参 CallFunctionRespfunctionName / result / success

获取最新脚本信息

c
BaseResponse kauth_get_newest_script(GetNewestScriptReq *request, NewestScriptResp *response);

用途:查询某脚本最新版本元信息。

入参 GetNewestScriptReqscriptName

出参 NewestScriptRespscriptName / versionNumber / versionDescription / scriptType / scriptReleaseTime

下载脚本

c
BaseResponse kauth_script_download_v2(ScriptDownloadReq *request, ScriptDownloadResp *response);

用途:下载指定版本脚本。

入参 ScriptDownloadReqscriptNameversionNumber

出参 ScriptDownloadRespdownloadUrl / md5 / fileSize / scriptContent(内容缓冲有上限,过大请用 URL)。

上报脚本错误

c
BaseResponse kauth_report_script_error(ScriptErrorReportReq *request);

用途:上报脚本运行错误。

入参 ScriptErrorReportReqerrorMsg / scriptName / scriptMd5 / lineNo / errorStack

出参BaseResponse

启动自动心跳

c
int kauth_start_heartbeat(int interval_ms, int max_failures, kauth_heartbeat_callback callback);

用途:后台线程按间隔调用心跳。

入参

参数说明
interval_ms间隔毫秒;0 用默认;也可用登录返回的 pongInterval 换算
max_failures连续失败上限(网络失败累计)
callback回调:(event, code, msg, failure_count)

eventKAUTH_HEARTBEAT_SUCCESS / NETWORK_FAIL / SERVER_FAIL / STOPPED

出参0 启动成功,非 0 失败。

停止自动心跳

c
void kauth_stop_heartbeat(void);

用途:停止心跳。

入参 / 出参:无。

查询心跳是否运行中

c
int kauth_is_heartbeat_running(void);

用途:是否在跑;非 0 表示运行中。

查询心跳连续失败次数

c
int kauth_get_heartbeat_failure_count(void);

用途:当前连续失败次数。

未开启 KAUTH_USE_VMP 宏时,以下 VMP 接口为桩:多数返回 -1 / 空串,不影响普通登录。

获取 VMP 硬件 ID

c
int kauth_vmp_get_hwid(char *out, size_t out_cap);

用途:取本机 VMP HWID,填入登录 hardwareId

出参0 成功;-1/-2 失败。

设置 VMP 授权锁

c
int kauth_vmp_set_serial(const char *serial);

用途:写入授权锁。登录若下发 vmpSerial 时 SDK 会自动调用。

出参0 表示 SetSerialNumber 成功(与 VMP 约定一致)。

查询授权锁状态

c
int kauth_vmp_get_serial_state(void);

用途:查询序列号状态位(0 = OK)。

格式化授权锁状态

c
void kauth_vmp_format_serial_state(int state, char *out, size_t out_cap);

用途:把状态格式化为可读文本(含 INVALID / BAD_HWID 等)。

读取授权锁明文

c
int kauth_vmp_get_serial_info(kauth_vmp_serial_info *out);

用途:读取序列号明文(需已成功设锁)。

出参结构 kauth_vmp_serial_infostate / userName / email / 到期日 / 最大编译日 / runningTimeMinutes / userData

格式化授权锁明文

c
void kauth_vmp_format_serial_info(const kauth_vmp_serial_info *info, char *out, size_t out_cap);

用途:格式化为多行 UTF-8,便于弹窗展示。

代码标记宏见 kauth_vmp.hKAUTH_VMP_BEGIN / KAUTH_VMP_BEGIN_LOCK / KAUTH_VMP_END(详见下文 VMP 节)。


接入工程

  1. 从网盘下载文件,放到工程目录。
  2. 工程平台 x64,运行时库 /MTka_login 用控制台子系统,ka_win 用 WINDOWS 子系统。
  3. Include 指向头文件目录;链接 kauth.lib
  4. ka_win.c 需定义 UNICODE/DUNICODE /D_UNICODE)。
  5. 调用顺序:初始化 → 登录 → 业务;登录成功后 SDK 自动保存 token。

快速接入(ka_login.c)

控制台最小示例:初始化 → 取设备 ID → 拉程序详情。与网盘中的 ka_login.c 一致,可直接复制编译。

c
/**
 * 静态链接示例:编译后只有一个 exe,无需 kauth.dll。
 *
 * 把下面的字段换成你后台「程序管理」里的值后再运行。
 * 用 kauth_init 按字段传入(便于后续混淆、降低关键字检索风险)。
 */
#include "kauth.h"
#include <stdio.h>
#include <string.h>

#ifdef _WIN32
#include <windows.h>
#endif

static int init_sdk(void) {
    kauth_init_options opt;
    memset(&opt, 0, sizeof(opt));
    opt.api_domain = "https://api.kauth.cn";
    opt.program_id = "1959821336266936321";
    opt.program_secret = "F77VzI7UWAElpWrz";
    opt.merchant_public_key =
        "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCIpy3ae27yDJOUd5rW/S6tUbAmt/AqJm+VPonT9WJn5VME4FkYJUwdBmIWpzANVQmU+7CA3wv5eVFIOW0xMv9EyoFWDRR24Jt/hgDsZQtUPMaZPivKWxx2S4n4SJWWrGdIRkdC3+fmxrEri1qYicq8PO7mDIrwPR2I0USoKKOFMwIDAQAB";
    /* sign_type 可省略,默认 RSA_SHA256_V2 */
    return kauth_init(&opt);
}

int main(void) {
    BaseResponse r;
    ProgramDetailResponse detail;
    char device[64];
#ifdef _WIN32
    SetConsoleOutputCP(CP_UTF8);
#endif
    if (init_sdk() != 0) {
        printf("init sdk failed\n");
        return 1;
    }
    if (kauth_get_device_unique_id(device, sizeof(device))) {
        printf("deviceId=%s\n", device);
    }
    memset(&detail, 0, sizeof(detail));
    r = kauth_get_program_detail(&detail);
    printf("program detail code=%d msg=%s name=%s\n", r.code, r.msg, detail.name);
    return r.code == 200 ? 0 : 2;
}

编译示意(x64 开发者命令提示符):

bat
cl /nologo /O2 /MT /I. ka_login.c /link kauth.lib /OUT:ka_login.exe

完整接入(ka_win.c)

Win32 完整示例,涵盖:

  • 单码 / 账密双层页签(登录、注册、改密、充值、解绑、试用)
  • 登录成功进入业务窗:到期 msg、心跳、退出
  • VMP:hardwareIdvmpSerial、设锁状态、解密弹窗、LockByKey 计算演示

需 UNICODE 编译;可选定义 KAUTH_USE_VMP 并链接 VMProtect(见下文)。

与网盘中的 ka_win.c 一致,完整源码如下:

c
/**
 * Win32 示例:登录窗口(单码 / 账密页签)+ 登录成功后的业务窗口(心跳 + 到期提示)。
 * 编译产物为子系统 WINDOWS 的单个 exe,静态链接 kauth.lib。
 * 需以 UNICODE 编译(CMake / build_msvc 已带 /DUNICODE)。
 */
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <commctrl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "kauth.h"
#include "kauth_vmp.h"

#pragma comment(lib, "comctl32.lib")
#pragma comment(lib, "user32.lib")
#pragma comment(lib, "gdi32.lib")
#pragma comment(linker, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")

#define WM_HB_EVENT (WM_APP + 40)

#define CLR_BG       RGB(241, 245, 249)
#define CLR_CARD     RGB(255, 255, 255)
#define CLR_HEADER    RGB(15, 23, 42)
#define CLR_ACCENT    RGB(13, 148, 136)
#define CLR_ACCENT_D  RGB(15, 118, 110)
#define CLR_DANGER    RGB(185, 28, 28)
#define CLR_TEXT      RGB(15, 23, 42)
#define CLR_MUTED     RGB(100, 116, 139)
#define CLR_LINE      RGB(226, 232, 240)
#define CLR_OK        RGB(22, 163, 74)
#define CLR_WARN      RGB(217, 119, 6)

#define HEADER_H 72
#define FOOTER_H 64
#define FORM_W   420

enum {
    IDC_MAIN_TAB = 1001,
    IDC_KA_TAB,
    IDC_ACC_TAB,
    IDC_STATUS,
    IDC_TRIAL_KA,
    IDC_TRIAL_ACC,

    IDC_KA_LOGIN_PWD,
    IDC_KA_LOGIN_BTN,
    IDC_KA_RECHARGE_OLD,
    IDC_KA_RECHARGE_NEW,
    IDC_KA_RECHARGE_BTN,
    IDC_KA_UNBIND_PWD,
    IDC_KA_UNBIND_BTN,

    IDC_ACC_LOGIN_USER,
    IDC_ACC_LOGIN_PWD,
    IDC_ACC_LOGIN_BTN,
    IDC_ACC_REG_USER,
    IDC_ACC_REG_PWD,
    IDC_ACC_REG_KA,
    IDC_ACC_REG_BTN,
    IDC_ACC_CHG_USER,
    IDC_ACC_CHG_OLD,
    IDC_ACC_CHG_NEW,
    IDC_ACC_CHG_BTN,
    IDC_ACC_RECHARGE_USER,
    IDC_ACC_RECHARGE_KA,
    IDC_ACC_RECHARGE_BTN,
    IDC_ACC_UNBIND_USER,
    IDC_ACC_UNBIND_PWD,
    IDC_ACC_UNBIND_BTN,

    IDC_BIZ_EXPIRE,
    IDC_BIZ_HB,
    IDC_BIZ_VMP_SERIAL,
    IDC_BIZ_VMP_STATE,
    IDC_BIZ_VMP_DECODE,
    IDC_BIZ_A,
    IDC_BIZ_OP,
    IDC_BIZ_B,
    IDC_BIZ_CALC,
    IDC_BIZ_RESULT,
    IDC_BIZ_LOGOUT
};

enum {
    BTN_PRIMARY = 1,
    BTN_SECONDARY = 2,
    BTN_DANGER = 3
};

static HINSTANCE g_inst;
static HWND g_login;
static HWND g_biz;
static HWND g_main_tab;
static HWND g_ka_tab;
static HWND g_acc_tab;
static HWND g_ka_pages[3];
static HWND g_acc_pages[5];
static HWND g_status;
static char g_device[128];
static char g_expire_msg[512];
static char g_vmp_serial[1024];
static HFONT g_font_ui;
static HFONT g_font_title;
static HFONT g_font_brand;
static HFONT g_font_small;
static HFONT g_font_btn;
static HBRUSH g_br_bg;
static HBRUSH g_br_card;
static HBRUSH g_br_header;

typedef struct {
    int event;
    int code;
    int failure_count;
    char msg[256];
} HbEvent;

static int utf8_to_wide(const char *utf8, wchar_t *out, int out_chars) {
    if (!utf8 || !out || out_chars <= 0) return 0;
    return MultiByteToWideChar(CP_UTF8, 0, utf8, -1, out, out_chars);
}

static int wide_to_utf8(const wchar_t *wide, char *out, int out_bytes) {
    if (!wide || !out || out_bytes <= 0) return 0;
    return WideCharToMultiByte(CP_UTF8, 0, wide, -1, out, out_bytes, NULL, NULL);
}

static void set_status(const char *utf8) {
    wchar_t w[1024];
    if (!g_status) return;
    utf8_to_wide(utf8 ? utf8 : "", w, 1024);
    SetWindowTextW(g_status, w);
}

static void refresh_vmp_state_ui(void);

static void show_msg(HWND owner, const char *title_utf8, const char *text_utf8) {
    wchar_t t[128], m[2048];
    utf8_to_wide(title_utf8, t, 128);
    utf8_to_wide(text_utf8, m, 2048);
    MessageBoxW(owner, m, t, MB_OK | MB_ICONINFORMATION);
}

static void show_vmp_serial_decode(HWND owner) {
    kauth_vmp_serial_info info;
    char text[2048];
    int set_rc;

    if (!g_vmp_serial[0]) {
        show_msg(owner, "解密 vmpSerial", "当前没有 vmpSerial(登录未下发或程序未配置 VMP 密钥)。");
        return;
    }

    set_rc = kauth_vmp_set_serial(g_vmp_serial);
    refresh_vmp_state_ui();
#ifndef KAUTH_USE_VMP
    (void)set_rc;
    show_msg(owner, "解密 vmpSerial",
             "当前未以 KAUTH_USE_VMP 编译,无法调用 VMProtectGetSerialNumberData。\n"
             "请用 build_vmp_ka_win.bat 编译后再试(加壳后字段校验才完整生效)。");
    return;
#else
    if (kauth_vmp_get_serial_info(&info) != 0) {
        char tip[512];
        char st[160];
        kauth_vmp_format_serial_state(set_rc != 0 ? set_rc : kauth_vmp_get_serial_state(), st, sizeof(st));
        snprintf(tip, sizeof(tip),
                 "读取序列号明文失败。\n"
                 "设锁结果: %s\n\n"
                 "常见原因:未加壳、或加壳用的 .vmp 与签发密钥不是同一份。",
                 st);
        show_msg(owner, "解密 vmpSerial", tip);
        return;
    }
    kauth_vmp_format_serial_info(&info, text, sizeof(text));
    show_msg(owner, "vmpSerial 解密结果", text);
#endif
}

static void get_edit_utf8(HWND root, int id, char *out, size_t out_size) {
    wchar_t w[1024];
    HWND h = GetDlgItem(root, id);
    if (!out || out_size == 0) return;
    out[0] = '\0';
    if (!h) return;
    GetWindowTextW(h, w, 1024);
    wide_to_utf8(w, out, (int)out_size);
}

static void paint_header(HDC hdc, RECT *rc, const wchar_t *brand, const wchar_t *subtitle) {
    RECT band = *rc;
    band.bottom = HEADER_H;
    FillRect(hdc, &band, g_br_header);
    SetBkMode(hdc, TRANSPARENT);
    SetTextColor(hdc, RGB(255, 255, 255));
    SelectObject(hdc, g_font_brand);
    TextOutW(hdc, 28, 14, brand, (int)wcslen(brand));
    SetTextColor(hdc, RGB(148, 163, 184));
    SelectObject(hdc, g_font_small);
    TextOutW(hdc, 28, 44, subtitle, (int)wcslen(subtitle));
}

static void paint_footer(HDC hdc, RECT *rc) {
    RECT foot = *rc;
    HPEN pen, old;
    foot.top = foot.bottom - FOOTER_H;
    FillRect(hdc, &foot, g_br_card);
    pen = CreatePen(PS_SOLID, 1, CLR_LINE);
    old = (HPEN)SelectObject(hdc, pen);
    MoveToEx(hdc, 0, foot.top, NULL);
    LineTo(hdc, foot.right, foot.top);
    SelectObject(hdc, old);
    DeleteObject(pen);
}

static void draw_owner_button(DRAWITEMSTRUCT *dis, int style) {
    HDC hdc = dis->hDC;
    RECT rc = dis->rcItem;
    int pressed = (dis->itemState & ODS_SELECTED) != 0;
    int disabled = (dis->itemState & ODS_DISABLED) != 0;
    COLORREF fill, text;
    HBRUSH br;
    wchar_t caption[128];
    HFONT old_font;

    if (style == BTN_DANGER) {
        fill = pressed ? RGB(153, 27, 27) : CLR_DANGER;
        text = RGB(255, 255, 255);
    } else if (style == BTN_SECONDARY) {
        fill = pressed ? RGB(226, 232, 240) : RGB(241, 245, 249);
        text = CLR_TEXT;
    } else {
        fill = pressed ? CLR_ACCENT_D : CLR_ACCENT;
        text = RGB(255, 255, 255);
    }
    if (disabled) {
        fill = RGB(203, 213, 225);
        text = RGB(148, 163, 184);
    }

    br = CreateSolidBrush(fill);
    FillRect(hdc, &rc, br);
    DeleteObject(br);

    if (style == BTN_SECONDARY) {
        HPEN pen = CreatePen(PS_SOLID, 1, CLR_LINE);
        HPEN old = (HPEN)SelectObject(hdc, pen);
        HBRUSH oldbr = (HBRUSH)SelectObject(hdc, GetStockObject(NULL_BRUSH));
        Rectangle(hdc, rc.left, rc.top, rc.right, rc.bottom);
        SelectObject(hdc, oldbr);
        SelectObject(hdc, old);
        DeleteObject(pen);
    }

    GetWindowTextW(dis->hwndItem, caption, 128);
    SetBkMode(hdc, TRANSPARENT);
    SetTextColor(hdc, text);
    old_font = (HFONT)SelectObject(hdc, g_font_btn);
    DrawTextW(hdc, caption, -1, &rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
    SelectObject(hdc, old_font);

    if (dis->itemState & ODS_FOCUS) {
        RECT fr = rc;
        InflateRect(&fr, -3, -3);
        DrawFocusRect(hdc, &fr);
    }
}

static int button_style_from_id(int id) {
    if (id == IDC_BIZ_LOGOUT) return BTN_DANGER;
    if (id == IDC_TRIAL_KA || id == IDC_TRIAL_ACC || id == IDC_BIZ_VMP_DECODE) return BTN_SECONDARY;
    return BTN_PRIMARY;
}

static HWND create_label(HWND parent, const wchar_t *text, int x, int y, int w, int h, HFONT font, COLORREF color) {
    HWND hwnd = CreateWindowExW(0, L"STATIC", text, WS_CHILD | WS_VISIBLE,
                                x, y, w, h, parent, NULL, g_inst, NULL);
    SendMessageW(hwnd, WM_SETFONT, (WPARAM)font, TRUE);
    SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR)color);
    return hwnd;
}

static HWND create_hint(HWND parent, const wchar_t *text, int x, int y, int w, int h) {
    return create_label(parent, text, x, y, w, h, g_font_small, CLR_MUTED);
}

static HWND create_edit(HWND parent, int id, int x, int y, int w, int h, int password) {
    DWORD style = WS_CHILD | WS_VISIBLE | ES_AUTOHSCROLL;
    HWND hwnd;
    if (password) style |= ES_PASSWORD;
    hwnd = CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", L"", style,
                           x, y, w, h, parent, (HMENU)(intptr_t)id, g_inst, NULL);
    SendMessageW(hwnd, WM_SETFONT, (WPARAM)g_font_ui, TRUE);
    return hwnd;
}

static HWND create_button(HWND parent, int id, const wchar_t *text, int x, int y, int w, int h) {
    HWND hwnd = CreateWindowExW(0, L"BUTTON", text,
                                WS_CHILD | WS_VISIBLE | BS_OWNERDRAW,
                                x, y, w, h, parent, (HMENU)(intptr_t)id, g_inst, NULL);
    SendMessageW(hwnd, WM_SETFONT, (WPARAM)g_font_btn, TRUE);
    return hwnd;
}

/* 上标签下输入框;返回下一行 y */
static int add_field(HWND parent, const wchar_t *label, int id, int x, int y, int w, int password) {
    create_label(parent, label, x, y, w, 20, g_font_small, CLR_MUTED);
    create_edit(parent, id, x, y + 22, w, 34, password);
    return y + 68;
}

static int add_action(HWND parent, int id, const wchar_t *text, int x, int y, int w) {
    create_button(parent, id, text, x, y, w, 40);
    return y + 52;
}

static LRESULT CALLBACK PageProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
    switch (msg) {
    case WM_COMMAND:
        if (g_login) return SendMessageW(g_login, msg, wParam, lParam);
        break;
    case WM_DRAWITEM:
        draw_owner_button((DRAWITEMSTRUCT *)lParam, button_style_from_id((int)wParam));
        return TRUE;
    case WM_CTLCOLORSTATIC: {
        HDC hdc = (HDC)wParam;
        HWND ctrl = (HWND)lParam;
        COLORREF c = (COLORREF)GetWindowLongPtrW(ctrl, GWLP_USERDATA);
        if (!c) c = CLR_TEXT;
        SetTextColor(hdc, c);
        SetBkColor(hdc, CLR_CARD);
        return (LRESULT)g_br_card;
    }
    case WM_ERASEBKGND: {
        RECT rc;
        GetClientRect(hwnd, &rc);
        FillRect((HDC)wParam, &rc, g_br_card);
        return 1;
    }
    default:
        break;
    }
    return DefWindowProcW(hwnd, msg, wParam, lParam);
}

static HWND create_page(HWND parent) {
    return CreateWindowExW(0, L"KauthPage", L"",
                           WS_CHILD | WS_CLIPSIBLINGS,
                           0, 0, 10, 10, parent, NULL, g_inst, NULL);
}

static void tab_insert(HWND tab, int index, const wchar_t *title) {
    TCITEMW item;
    memset(&item, 0, sizeof(item));
    item.mask = TCIF_TEXT;
    item.pszText = (LPWSTR)title;
    TabCtrl_InsertItem(tab, index, &item);
}

static void layout_child_tabs(void) {
    RECT client, main_disp, sub_disp;
    int main_sel, i;
    int top = HEADER_H + 16;
    int bottom_pad = FOOTER_H + 12;

    GetClientRect(g_login, &client);
    SetWindowPos(g_main_tab, NULL, 20, top, client.right - 40, client.bottom - top - bottom_pad, SWP_NOZORDER);

    GetClientRect(g_main_tab, &main_disp);
    TabCtrl_AdjustRect(g_main_tab, FALSE, &main_disp);
    MapWindowPoints(g_main_tab, g_login, (LPPOINT)&main_disp, 2);

    main_sel = TabCtrl_GetCurSel(g_main_tab);
    ShowWindow(g_ka_tab, main_sel == 0 ? SW_SHOW : SW_HIDE);
    ShowWindow(g_acc_tab, main_sel == 1 ? SW_SHOW : SW_HIDE);
    SetWindowPos(g_ka_tab, HWND_TOP, main_disp.left + 8, main_disp.top + 8,
                 main_disp.right - main_disp.left - 16, main_disp.bottom - main_disp.top - 16, 0);
    SetWindowPos(g_acc_tab, HWND_TOP, main_disp.left + 8, main_disp.top + 8,
                 main_disp.right - main_disp.left - 16, main_disp.bottom - main_disp.top - 16, 0);

    GetClientRect(g_ka_tab, &sub_disp);
    TabCtrl_AdjustRect(g_ka_tab, FALSE, &sub_disp);
    MapWindowPoints(g_ka_tab, g_login, (LPPOINT)&sub_disp, 2);
    for (i = 0; i < 3; i++) {
        SetWindowPos(g_ka_pages[i], HWND_TOP, sub_disp.left + 1, sub_disp.top + 1,
                     sub_disp.right - sub_disp.left - 2, sub_disp.bottom - sub_disp.top - 2, 0);
        ShowWindow(g_ka_pages[i], (main_sel == 0 && i == TabCtrl_GetCurSel(g_ka_tab)) ? SW_SHOW : SW_HIDE);
    }

    GetClientRect(g_acc_tab, &sub_disp);
    TabCtrl_AdjustRect(g_acc_tab, FALSE, &sub_disp);
    MapWindowPoints(g_acc_tab, g_login, (LPPOINT)&sub_disp, 2);
    for (i = 0; i < 5; i++) {
        SetWindowPos(g_acc_pages[i], HWND_TOP, sub_disp.left + 1, sub_disp.top + 1,
                     sub_disp.right - sub_disp.left - 2, sub_disp.bottom - sub_disp.top - 2, 0);
        ShowWindow(g_acc_pages[i], (main_sel == 1 && i == TabCtrl_GetCurSel(g_acc_tab)) ? SW_SHOW : SW_HIDE);
    }
}

static int init_sdk(void) {
    kauth_init_options opt;
    memset(&opt, 0, sizeof(opt));
    opt.api_domain = "https://api.kauth.cn";
    opt.program_id = "2098969996642799617";
    opt.program_secret = "fmx5nxsOtZFr0Rac";
    opt.merchant_public_key =
        "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCIpy3ae27yDJOUd5rW/S6tUbAmt/AqJm+VPonT9WJn5VME4FkYJUwdBmIWpzANVQmU+7CA3wv5eVFIOW0xMv9EyoFWDRR24Jt/hgDsZQtUPMaZPivKWxx2S4n4SJWWrGdIRkdC3+fmxrEri1qYicq8PO7mDIrwPR2I0USoKKOFMwIDAQAB";
    if (kauth_init(&opt) != 0) return -1;
    kauth_get_device_unique_id(g_device, sizeof(g_device));
    return 0;
}

static void on_heartbeat(kauth_heartbeat_event event, int code, const char *msg, int failure_count) {
    HbEvent *ev = (HbEvent *)malloc(sizeof(HbEvent));
    if (!ev) return;
    memset(ev, 0, sizeof(*ev));
    ev->event = (int)event;
    ev->code = code;
    ev->failure_count = failure_count;
    if (msg) strncpy(ev->msg, msg, sizeof(ev->msg) - 1);
    if (!PostMessageW(g_biz, WM_HB_EVENT, 0, (LPARAM)ev)) free(ev);
}

static void refresh_vmp_state_ui(void) {
    char state_txt[256];
    char line[320];
    wchar_t w[320];
    int st = kauth_vmp_get_serial_state();
    kauth_vmp_format_serial_state(st, state_txt, sizeof(state_txt));
    snprintf(line, sizeof(line), "设锁状态: %s", state_txt);
    utf8_to_wide(line, w, 320);
    if (GetDlgItem(g_biz, IDC_BIZ_VMP_STATE))
        SetWindowTextW(GetDlgItem(g_biz, IDC_BIZ_VMP_STATE), w);
}

static void enter_business(const char *msg_utf8, const char *pong_interval, const char *vmp_serial) {
    wchar_t expire_w[512];
    wchar_t serial_w[1024];
    int interval_ms = 120000;
    char status[256];
    int set_rc;

    strncpy(g_expire_msg, msg_utf8 ? msg_utf8 : "", sizeof(g_expire_msg) - 1);
    g_expire_msg[sizeof(g_expire_msg) - 1] = '\0';
    utf8_to_wide(g_expire_msg[0] ? g_expire_msg : "(无到期信息)", expire_w, 512);
    SetWindowTextW(GetDlgItem(g_biz, IDC_BIZ_EXPIRE), expire_w);

    strncpy(g_vmp_serial, vmp_serial ? vmp_serial : "", sizeof(g_vmp_serial) - 1);
    g_vmp_serial[sizeof(g_vmp_serial) - 1] = '\0';
    if (g_vmp_serial[0]) {
        utf8_to_wide(g_vmp_serial, serial_w, 1024);
        SetWindowTextW(GetDlgItem(g_biz, IDC_BIZ_VMP_SERIAL), serial_w);
        /* 再设一次锁,便于调试查看返回值(0=成功) */
        set_rc = kauth_vmp_set_serial(g_vmp_serial);
        {
            char tip[320];
            char st[128];
            wchar_t tw[320];
            kauth_vmp_format_serial_state(set_rc != 0 ? set_rc : kauth_vmp_get_serial_state(), st, sizeof(st));
            snprintf(tip, sizeof(tip), "SetSerial=%d;当前状态: %s", set_rc, st);
            utf8_to_wide(tip, tw, 320);
            SetWindowTextW(GetDlgItem(g_biz, IDC_BIZ_VMP_STATE), tw);
        }
        if (set_rc != 0) {
            char tip[512];
            char st[128];
            kauth_vmp_format_serial_state(set_rc, st, sizeof(st));
            snprintf(tip, sizeof(tip),
                     "vmpSerial 已下发,但 SetSerialNumber 失败:%s\n\n"
                     "常见原因:加壳用的 .vmp 工程 / 产品密钥,与商家后台上传的 VMP 密钥不是同一套。\n"
                     "请用后台那份 .vmp 重新加壳 ka_win.exe。",
                     st);
            show_msg(g_biz, "VMP 授权未生效", tip);
        }
    } else {
        SetWindowTextW(GetDlgItem(g_biz, IDC_BIZ_VMP_SERIAL), L"(未颁发:程序未配 VMP 密钥,或未传 hardwareId)");
        SetWindowTextW(GetDlgItem(g_biz, IDC_BIZ_VMP_STATE), L"设锁状态: 无序列号");
    }

    SetWindowTextW(GetDlgItem(g_biz, IDC_BIZ_HB), L"心跳:启动中...");

    if (pong_interval && pong_interval[0]) {
        long v = strtol(pong_interval, NULL, 10);
        if (v > 1000) interval_ms = (int)v;
        else if (v > 0) interval_ms = (int)(v * 1000);
    }

    ShowWindow(g_login, SW_HIDE);
    ShowWindow(g_biz, SW_SHOW);
    SetForegroundWindow(g_biz);

    kauth_stop_heartbeat();
    if (kauth_start_heartbeat(interval_ms, 3, on_heartbeat) != 0) {
        SetWindowTextW(GetDlgItem(g_biz, IDC_BIZ_HB), L"心跳:启动失败");
    } else {
        snprintf(status, sizeof(status), "心跳:运行中(间隔 %d ms)", interval_ms);
        {
            wchar_t w[256];
            utf8_to_wide(status, w, 256);
            SetWindowTextW(GetDlgItem(g_biz, IDC_BIZ_HB), w);
        }
    }
}

static void leave_business(void) {
    kauth_stop_heartbeat();
    kauth_logout();
    kauth_set_token("");
    g_vmp_serial[0] = '\0';
    if (GetDlgItem(g_biz, IDC_BIZ_VMP_SERIAL))
        SetWindowTextW(GetDlgItem(g_biz, IDC_BIZ_VMP_SERIAL), L"");
    ShowWindow(g_biz, SW_HIDE);
    ShowWindow(g_login, SW_SHOW);
    set_status("已退出登录");
}

static void fill_vmp_hwid(char *dst, size_t cap) {
    if (!dst || cap == 0) return;
    if (kauth_vmp_get_hwid(dst, cap) != 0) dst[0] = '\0';
}

static void handle_login_ok(HWND owner, BaseResponse *r, LoginResponse *login) {
    char tip[640];
    if (r->code != 200) {
        snprintf(tip, sizeof(tip), "失败 code=%d\n%s", r->code, r->msg);
        show_msg(owner, "提示", tip);
        set_status(tip);
        return;
    }
    if (login->vmpSerial[0]) {
        snprintf(tip, sizeof(tip), "登录成功(已下发 VMP 授权锁):%s", login->msg);
    } else {
        snprintf(tip, sizeof(tip), "登录成功:%s", login->msg);
    }
    set_status(tip);
    enter_business(login->msg, login->pongInterval, login->vmpSerial);
}

static void do_ka_login(HWND owner) {
    KaLoginRequest req;
    LoginResponse login;
    BaseResponse r;
    memset(&req, 0, sizeof(req));
    memset(&login, 0, sizeof(login));
    get_edit_utf8(owner, IDC_KA_LOGIN_PWD, req.kaPwd, sizeof(req.kaPwd));
    strncpy(req.deviceId, g_device, sizeof(req.deviceId) - 1);
    strncpy(req.platformType, "windows", sizeof(req.platformType) - 1);
    fill_vmp_hwid(req.hardwareId, sizeof(req.hardwareId));
    if (!req.kaPwd[0]) { show_msg(g_login, "提示", "请输入卡密"); return; }
    set_status("正在卡密登录...");
    r = kauth_ka_login(&req, &login);
    handle_login_ok(g_login, &r, &login);
}

static void do_ka_recharge(HWND owner) {
    KaRechargeKaReq req;
    BaseResponse r;
    char tip[640];
    memset(&req, 0, sizeof(req));
    get_edit_utf8(owner, IDC_KA_RECHARGE_OLD, req.cardPwd, sizeof(req.cardPwd));
    get_edit_utf8(owner, IDC_KA_RECHARGE_NEW, req.rechargeCardPwd, sizeof(req.rechargeCardPwd));
    if (!req.cardPwd[0] || !req.rechargeCardPwd[0]) {
        show_msg(g_login, "提示", "请输入原卡密和充值卡");
        return;
    }
    set_status("正在以卡充卡...");
    r = kauth_recharge_ka(&req);
    snprintf(tip, sizeof(tip), "以卡充卡 code=%d\n%s", r.code, r.msg);
    show_msg(g_login, "提示", tip);
    set_status(tip);
}

static void do_ka_unbind(HWND owner) {
    UnbindDeviceKaPwdRequest req;
    BaseResponse r;
    char tip[640];
    memset(&req, 0, sizeof(req));
    get_edit_utf8(owner, IDC_KA_UNBIND_PWD, req.kaPwd, sizeof(req.kaPwd));
    strncpy(req.deviceId, g_device, sizeof(req.deviceId) - 1);
    if (!req.kaPwd[0]) { show_msg(g_login, "提示", "请输入卡密"); return; }
    set_status("正在卡密解绑...");
    r = kauth_unbind_device_ka_pwd(&req);
    snprintf(tip, sizeof(tip), "卡密解绑 code=%d\n%s", r.code, r.msg);
    show_msg(g_login, "提示", tip);
    set_status(tip);
}

static void do_acc_login(HWND owner) {
    PwdLoginRequest req;
    LoginResponse login;
    BaseResponse r;
    memset(&req, 0, sizeof(req));
    memset(&login, 0, sizeof(login));
    get_edit_utf8(owner, IDC_ACC_LOGIN_USER, req.userName, sizeof(req.userName));
    get_edit_utf8(owner, IDC_ACC_LOGIN_PWD, req.password, sizeof(req.password));
    strncpy(req.deviceId, g_device, sizeof(req.deviceId) - 1);
    fill_vmp_hwid(req.hardwareId, sizeof(req.hardwareId));
    if (!req.userName[0] || !req.password[0]) {
        show_msg(g_login, "提示", "请输入账号和密码");
        return;
    }
    set_status("正在账密登录...");
    r = kauth_pwd_login(&req, &login);
    handle_login_ok(g_login, &r, &login);
}

static void do_acc_register(HWND owner) {
    RegisterRequest req;
    RegisterResponse resp;
    BaseResponse r;
    char tip[640];
    memset(&req, 0, sizeof(req));
    memset(&resp, 0, sizeof(resp));
    get_edit_utf8(owner, IDC_ACC_REG_USER, req.userName, sizeof(req.userName));
    get_edit_utf8(owner, IDC_ACC_REG_PWD, req.password, sizeof(req.password));
    get_edit_utf8(owner, IDC_ACC_REG_KA, req.kaPassword, sizeof(req.kaPassword));
    if (!req.userName[0] || !req.password[0] || !req.kaPassword[0]) {
        show_msg(g_login, "提示", "请输入账号、密码和卡密");
        return;
    }
    set_status("正在注册...");
    r = kauth_register(&req, &resp);
    kauth_set_token(""); /* 注册不视为已登录,避免残留 token */
    snprintf(tip, sizeof(tip), "账号注册 code=%d\n%s", r.code, r.msg);
    show_msg(g_login, "提示", tip);
    set_status(tip);
    /* 注册成功后留在登录页,需再走账密登录 */
}

static void do_acc_change_pwd(HWND owner) {
    ResetPwdRequest req;
    BaseResponse r;
    char tip[640];
    memset(&req, 0, sizeof(req));
    get_edit_utf8(owner, IDC_ACC_CHG_USER, req.userName, sizeof(req.userName));
    get_edit_utf8(owner, IDC_ACC_CHG_OLD, req.oldPassword, sizeof(req.oldPassword));
    get_edit_utf8(owner, IDC_ACC_CHG_NEW, req.newPassword, sizeof(req.newPassword));
    /* 页面只收新密码;confirmPassword 与 newPassword 对齐后提交 */
    strncpy(req.confirmPassword, req.newPassword, sizeof(req.confirmPassword) - 1);
    if (!req.userName[0] || !req.oldPassword[0] || !req.newPassword[0]) {
        show_msg(g_login, "提示", "请输入账号、旧密码和新密码");
        return;
    }
    set_status("正在改密...");
    r = kauth_change_password(&req);
    snprintf(tip, sizeof(tip), "账号改密 code=%d\n%s", r.code, r.msg);
    show_msg(g_login, "提示", tip);
    set_status(tip);
}

static void do_acc_recharge(HWND owner) {
    RechargeRequest req;
    BaseResponse r;
    char tip[640];
    memset(&req, 0, sizeof(req));
    get_edit_utf8(owner, IDC_ACC_RECHARGE_USER, req.loginName, sizeof(req.loginName));
    get_edit_utf8(owner, IDC_ACC_RECHARGE_KA, req.kaPassword, sizeof(req.kaPassword));
    strncpy(req.deviceId, g_device, sizeof(req.deviceId) - 1);
    if (!req.loginName[0] || !req.kaPassword[0]) {
        show_msg(g_login, "提示", "请输入账号和卡密");
        return;
    }
    set_status("正在账号充值...");
    r = kauth_recharge(&req);
    snprintf(tip, sizeof(tip), "账号充值 code=%d\n%s", r.code, r.msg);
    show_msg(g_login, "提示", tip);
    set_status(tip);
}

static void do_acc_unbind(HWND owner) {
    UnbindDeviceRequest req;
    BaseResponse r;
    char tip[640];
    memset(&req, 0, sizeof(req));
    get_edit_utf8(owner, IDC_ACC_UNBIND_USER, req.loginName, sizeof(req.loginName));
    get_edit_utf8(owner, IDC_ACC_UNBIND_PWD, req.password, sizeof(req.password));
    strncpy(req.deviceId, g_device, sizeof(req.deviceId) - 1);
    if (!req.loginName[0] || !req.password[0]) {
        show_msg(g_login, "提示", "请输入账号和密码");
        return;
    }
    set_status("正在账号解绑...");
    r = kauth_unbind_device(&req);
    snprintf(tip, sizeof(tip), "账号解绑 code=%d\n%s", r.code, r.msg);
    show_msg(g_login, "提示", tip);
    set_status(tip);
}

static void do_trial_login(void) {
    TrialLoginReq req;
    LoginResponse login;
    BaseResponse r;
    memset(&req, 0, sizeof(req));
    memset(&login, 0, sizeof(login));
    strncpy(req.deviceId, g_device, sizeof(req.deviceId) - 1);
    strncpy(req.platformType, "windows", sizeof(req.platformType) - 1);
    fill_vmp_hwid(req.hardwareId, sizeof(req.hardwareId));
    set_status("正在试用登录...");
    r = kauth_trial_login(&req, &login);
    handle_login_ok(g_login, &r, &login);
}

static void build_page_shell(HWND page, const wchar_t *title, const wchar_t *hint) {
    create_label(page, title, 28, 18, FORM_W, 28, g_font_title, CLR_TEXT);
    create_hint(page, hint, 28, 48, FORM_W, 20);
}

static void build_ka_pages(HWND parent) {
    int y;
    g_ka_pages[0] = create_page(parent);
    build_page_shell(g_ka_pages[0], L"卡密登录", L"输入卡密后登录,成功将进入业务窗口并开启心跳");
    y = add_field(g_ka_pages[0], L"卡密", IDC_KA_LOGIN_PWD, 28, 84, FORM_W, 0);
    create_button(g_ka_pages[0], IDC_KA_LOGIN_BTN, L"登录", 28, y, 140, 40);
    create_button(g_ka_pages[0], IDC_TRIAL_KA, L"试用登录", 180, y, 140, 40);

    g_ka_pages[1] = create_page(parent);
    build_page_shell(g_ka_pages[1], L"以卡充卡", L"用充值卡为原卡密续期 / 加时");
    y = add_field(g_ka_pages[1], L"原卡密", IDC_KA_RECHARGE_OLD, 28, 84, FORM_W, 0);
    y = add_field(g_ka_pages[1], L"充值卡", IDC_KA_RECHARGE_NEW, 28, y, FORM_W, 0);
    add_action(g_ka_pages[1], IDC_KA_RECHARGE_BTN, L"充值", 28, y, 140);

    g_ka_pages[2] = create_page(parent);
    build_page_shell(g_ka_pages[2], L"卡密解绑", L"解绑当前设备与卡密的绑定关系");
    y = add_field(g_ka_pages[2], L"卡密", IDC_KA_UNBIND_PWD, 28, 84, FORM_W, 0);
    add_action(g_ka_pages[2], IDC_KA_UNBIND_BTN, L"解绑", 28, y, 140);
}

static void build_acc_pages(HWND parent) {
    int y;
    g_acc_pages[0] = create_page(parent);
    build_page_shell(g_acc_pages[0], L"账密登录", L"使用账号和密码登录");
    y = add_field(g_acc_pages[0], L"账号", IDC_ACC_LOGIN_USER, 28, 84, FORM_W, 0);
    y = add_field(g_acc_pages[0], L"密码", IDC_ACC_LOGIN_PWD, 28, y, FORM_W, 1);
    create_button(g_acc_pages[0], IDC_ACC_LOGIN_BTN, L"登录", 28, y, 140, 40);
    create_button(g_acc_pages[0], IDC_TRIAL_ACC, L"试用登录", 180, y, 140, 40);

    g_acc_pages[1] = create_page(parent);
    build_page_shell(g_acc_pages[1], L"账号注册", L"注册账号并绑定卡密,成功后请再使用账密登录");
    y = add_field(g_acc_pages[1], L"账号", IDC_ACC_REG_USER, 28, 84, FORM_W, 0);
    y = add_field(g_acc_pages[1], L"密码", IDC_ACC_REG_PWD, 28, y, FORM_W, 1);
    y = add_field(g_acc_pages[1], L"卡密", IDC_ACC_REG_KA, 28, y, FORM_W, 0);
    add_action(g_acc_pages[1], IDC_ACC_REG_BTN, L"注册", 28, y, 140);

    g_acc_pages[2] = create_page(parent);
    build_page_shell(g_acc_pages[2], L"账号改密", L"填写账号、旧密码与新密码");
    y = add_field(g_acc_pages[2], L"账号", IDC_ACC_CHG_USER, 28, 84, FORM_W, 0);
    y = add_field(g_acc_pages[2], L"旧密码", IDC_ACC_CHG_OLD, 28, y, FORM_W, 1);
    y = add_field(g_acc_pages[2], L"新密码", IDC_ACC_CHG_NEW, 28, y, FORM_W, 1);
    add_action(g_acc_pages[2], IDC_ACC_CHG_BTN, L"改密", 28, y, 140);

    g_acc_pages[3] = create_page(parent);
    build_page_shell(g_acc_pages[3], L"账号充值", L"使用卡密为账号充值");
    y = add_field(g_acc_pages[3], L"账号", IDC_ACC_RECHARGE_USER, 28, 84, FORM_W, 0);
    y = add_field(g_acc_pages[3], L"卡密", IDC_ACC_RECHARGE_KA, 28, y, FORM_W, 0);
    add_action(g_acc_pages[3], IDC_ACC_RECHARGE_BTN, L"充值", 28, y, 140);

    g_acc_pages[4] = create_page(parent);
    build_page_shell(g_acc_pages[4], L"账号解绑", L"解绑账号与当前设备");
    y = add_field(g_acc_pages[4], L"账号", IDC_ACC_UNBIND_USER, 28, 84, FORM_W, 0);
    y = add_field(g_acc_pages[4], L"密码", IDC_ACC_UNBIND_PWD, 28, y, FORM_W, 1);
    add_action(g_acc_pages[4], IDC_ACC_UNBIND_BTN, L"解绑", 28, y, 140);
}

static void on_login_command(HWND hwnd, int id) {
    (void)hwnd;
    switch (id) {
    case IDC_KA_LOGIN_BTN: do_ka_login(g_ka_pages[0]); break;
    case IDC_KA_RECHARGE_BTN: do_ka_recharge(g_ka_pages[1]); break;
    case IDC_KA_UNBIND_BTN: do_ka_unbind(g_ka_pages[2]); break;
    case IDC_ACC_LOGIN_BTN: do_acc_login(g_acc_pages[0]); break;
    case IDC_ACC_REG_BTN: do_acc_register(g_acc_pages[1]); break;
    case IDC_ACC_CHG_BTN: do_acc_change_pwd(g_acc_pages[2]); break;
    case IDC_ACC_RECHARGE_BTN: do_acc_recharge(g_acc_pages[3]); break;
    case IDC_ACC_UNBIND_BTN: do_acc_unbind(g_acc_pages[4]); break;
    case IDC_TRIAL_KA:
    case IDC_TRIAL_ACC: do_trial_login(); break;
    default: break;
    }
}

static LRESULT CALLBACK LoginProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
    switch (msg) {
    case WM_CREATE: {
        INITCOMMONCONTROLSEX icc;
        icc.dwSize = sizeof(icc);
        icc.dwICC = ICC_TAB_CLASSES;
        InitCommonControlsEx(&icc);

        g_main_tab = CreateWindowExW(0, WC_TABCONTROLW, L"",
                                     WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS,
                                     0, 0, 100, 100, hwnd, (HMENU)(intptr_t)IDC_MAIN_TAB, g_inst, NULL);
        SendMessageW(g_main_tab, WM_SETFONT, (WPARAM)g_font_ui, TRUE);
        TabCtrl_SetItemSize(g_main_tab, 120, 32);
        tab_insert(g_main_tab, 0, L"单码体系");
        tab_insert(g_main_tab, 1, L"账密体系");

        g_ka_tab = CreateWindowExW(0, WC_TABCONTROLW, L"",
                                   WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS,
                                   0, 0, 100, 100, hwnd, (HMENU)(intptr_t)IDC_KA_TAB, g_inst, NULL);
        SendMessageW(g_ka_tab, WM_SETFONT, (WPARAM)g_font_ui, TRUE);
        tab_insert(g_ka_tab, 0, L"卡密登录");
        tab_insert(g_ka_tab, 1, L"以卡充卡");
        tab_insert(g_ka_tab, 2, L"卡密解绑");

        g_acc_tab = CreateWindowExW(0, WC_TABCONTROLW, L"",
                                    WS_CHILD | WS_CLIPSIBLINGS,
                                    0, 0, 100, 100, hwnd, (HMENU)(intptr_t)IDC_ACC_TAB, g_inst, NULL);
        SendMessageW(g_acc_tab, WM_SETFONT, (WPARAM)g_font_ui, TRUE);
        tab_insert(g_acc_tab, 0, L"账密登录");
        tab_insert(g_acc_tab, 1, L"账号注册");
        tab_insert(g_acc_tab, 2, L"账号改密");
        tab_insert(g_acc_tab, 3, L"账号充值");
        tab_insert(g_acc_tab, 4, L"账号解绑");

        build_ka_pages(hwnd);
        build_acc_pages(hwnd);

        g_status = CreateWindowExW(0, L"STATIC", L"就绪",
                                   WS_CHILD | WS_VISIBLE | SS_LEFT | SS_ENDELLIPSIS,
                                   24, 0, 400, 24, hwnd, (HMENU)(intptr_t)IDC_STATUS, g_inst, NULL);
        SendMessageW(g_status, WM_SETFONT, (WPARAM)g_font_small, TRUE);
        SetWindowLongPtrW(g_status, GWLP_USERDATA, (LONG_PTR)CLR_MUTED);
        layout_child_tabs();
        return 0;
    }
    case WM_SIZE: {
        int w = LOWORD(lParam), h = HIWORD(lParam);
        SetWindowPos(g_status, NULL, 24, h - 40, w - 48, 24, SWP_NOZORDER);
        layout_child_tabs();
        InvalidateRect(hwnd, NULL, TRUE);
        return 0;
    }
    case WM_ERASEBKGND: {
        RECT rc;
        GetClientRect(hwnd, &rc);
        FillRect((HDC)wParam, &rc, g_br_bg);
        return 1;
    }
    case WM_PAINT: {
        PAINTSTRUCT ps;
        RECT rc;
        HDC hdc = BeginPaint(hwnd, &ps);
        GetClientRect(hwnd, &rc);
        paint_header(hdc, &rc, L"kauth", L"卡密 / 账密登录示例");
        paint_footer(hdc, &rc);
        EndPaint(hwnd, &ps);
        return 0;
    }
    case WM_CTLCOLORSTATIC: {
        HDC hdc = (HDC)wParam;
        SetTextColor(hdc, CLR_MUTED);
        SetBkColor(hdc, CLR_CARD);
        return (LRESULT)g_br_card;
    }
    case WM_DRAWITEM:
        draw_owner_button((DRAWITEMSTRUCT *)lParam, button_style_from_id((int)wParam));
        return TRUE;
    case WM_NOTIFY: {
        NMHDR *hdr = (NMHDR *)lParam;
        if (hdr && hdr->code == TCN_SELCHANGE) {
            layout_child_tabs();
            return 0;
        }
        break;
    }
    case WM_COMMAND:
        if (HIWORD(wParam) == BN_CLICKED) {
            on_login_command(hwnd, LOWORD(wParam));
            return 0;
        }
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    default:
        break;
    }
    return DefWindowProcW(hwnd, msg, wParam, lParam);
}

/**
 * LockByKey 演示:加壳后无有效 vmpSerial 时,此函数体不会真正执行。
 * 未定义 KAUTH_USE_VMP 时宏为空,本地调试仍可正常计算。
 */
static int biz_locked_calc(double a, int op, double b, double *out) {
    int ok = 0;
    if (!out) return 0;
    *out = 0;
    KAUTH_VMP_BEGIN_LOCK("biz_calc");
    switch (op) {
    case 0: *out = a + b; ok = 1; break;
    case 1: *out = a - b; ok = 1; break;
    case 2: *out = a * b; ok = 1; break;
    case 3:
        if (b == 0.0) ok = 0;
        else {
            *out = a / b;
            ok = 1;
        }
        break;
    default:
        ok = 0;
        break;
    }
    KAUTH_VMP_END();
    return ok;
}

static LRESULT CALLBACK BizProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
    switch (msg) {
    case WM_CREATE: {
        HWND op;
        int y = HEADER_H + 24;

        create_label(hwnd, L"到期信息", 32, y, 200, 20, g_font_small, CLR_MUTED);
        y += 24;
        CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", L"",
                        WS_CHILD | WS_VISIBLE | ES_READONLY | ES_AUTOHSCROLL,
                        32, y, 460, 32, hwnd, (HMENU)(intptr_t)IDC_BIZ_EXPIRE, g_inst, NULL);
        SendMessageW(GetDlgItem(hwnd, IDC_BIZ_EXPIRE), WM_SETFONT, (WPARAM)g_font_ui, TRUE);

        y += 44;
        create_label(hwnd, L"心跳状态", 32, y, 200, 20, g_font_small, CLR_MUTED);
        y += 24;
        CreateWindowExW(0, L"STATIC", L"心跳:未启动",
                        WS_CHILD | WS_VISIBLE | SS_LEFT,
                        32, y, 460, 24, hwnd, (HMENU)(intptr_t)IDC_BIZ_HB, g_inst, NULL);
        SendMessageW(GetDlgItem(hwnd, IDC_BIZ_HB), WM_SETFONT, (WPARAM)g_font_ui, TRUE);
        SetWindowLongPtrW(GetDlgItem(hwnd, IDC_BIZ_HB), GWLP_USERDATA, (LONG_PTR)CLR_TEXT);

        y += 36;
        create_label(hwnd, L"VMP 授权锁 vmpSerial", 32, y, 240, 20, g_font_small, CLR_MUTED);
        create_button(hwnd, IDC_BIZ_VMP_DECODE, L"解密", 360, y - 4, 100, 28);
        y += 22;
        CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", L"",
                        WS_CHILD | WS_VISIBLE | ES_READONLY | ES_AUTOHSCROLL | ES_MULTILINE,
                        32, y, 460, 48, hwnd, (HMENU)(intptr_t)IDC_BIZ_VMP_SERIAL, g_inst, NULL);
        SendMessageW(GetDlgItem(hwnd, IDC_BIZ_VMP_SERIAL), WM_SETFONT, (WPARAM)g_font_small, TRUE);

        y += 56;
        CreateWindowExW(0, L"STATIC", L"设锁状态: -",
                        WS_CHILD | WS_VISIBLE | SS_LEFT,
                        32, y, 460, 22, hwnd, (HMENU)(intptr_t)IDC_BIZ_VMP_STATE, g_inst, NULL);
        SendMessageW(GetDlgItem(hwnd, IDC_BIZ_VMP_STATE), WM_SETFONT, (WPARAM)g_font_small, TRUE);
        SetWindowLongPtrW(GetDlgItem(hwnd, IDC_BIZ_VMP_STATE), GWLP_USERDATA, (LONG_PTR)CLR_MUTED);

        y += 32;
        create_label(hwnd, L"受保护计算(LockByKey 演示)", 32, y, 400, 20, g_font_small, CLR_MUTED);
        y += 22;
        create_hint(hwnd, L"加壳 + 登录下发 vmpSerial 后才可执行;未开 VMP 时本地仍可算", 32, y, 460, 20);

        y += 28;
        create_edit(hwnd, IDC_BIZ_A, 32, y, 100, 32, 0);
        SetWindowTextW(GetDlgItem(hwnd, IDC_BIZ_A), L"35");

        op = CreateWindowExW(0, L"COMBOBOX", L"",
                             WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | WS_VSCROLL,
                             144, y, 90, 200, hwnd, (HMENU)(intptr_t)IDC_BIZ_OP, g_inst, NULL);
        SendMessageW(op, WM_SETFONT, (WPARAM)g_font_ui, TRUE);
        SendMessageW(op, CB_ADDSTRING, 0, (LPARAM)L"加 +");
        SendMessageW(op, CB_ADDSTRING, 0, (LPARAM)L"减 -");
        SendMessageW(op, CB_ADDSTRING, 0, (LPARAM)L"乘 ×");
        SendMessageW(op, CB_ADDSTRING, 0, (LPARAM)L"除 ÷");
        SendMessageW(op, CB_SETCURSEL, 2, 0); /* 默认乘法 */

        create_edit(hwnd, IDC_BIZ_B, 246, y, 100, 32, 0);
        SetWindowTextW(GetDlgItem(hwnd, IDC_BIZ_B), L"2");

        create_button(hwnd, IDC_BIZ_CALC, L"计算", 360, y, 100, 36);

        y += 48;
        create_label(hwnd, L"结果", 32, y, 60, 20, g_font_small, CLR_MUTED);
        CreateWindowExW(WS_EX_CLIENTEDGE, L"EDIT", L"",
                        WS_CHILD | WS_VISIBLE | ES_READONLY | ES_AUTOHSCROLL,
                        100, y - 4, 360, 32, hwnd, (HMENU)(intptr_t)IDC_BIZ_RESULT, g_inst, NULL);
        SendMessageW(GetDlgItem(hwnd, IDC_BIZ_RESULT), WM_SETFONT, (WPARAM)g_font_ui, TRUE);

        y += 52;
        create_button(hwnd, IDC_BIZ_LOGOUT, L"退出登录", 32, y, 140, 40);
        return 0;
    }
    case WM_ERASEBKGND: {
        RECT rc;
        GetClientRect(hwnd, &rc);
        FillRect((HDC)wParam, &rc, g_br_bg);
        return 1;
    }
    case WM_PAINT: {
        PAINTSTRUCT ps;
        RECT rc, card;
        HDC hdc = BeginPaint(hwnd, &ps);
        GetClientRect(hwnd, &rc);
        paint_header(hdc, &rc, L"kauth", L"已登录 · 业务状态");
        card.left = 20;
        card.top = HEADER_H + 16;
        card.right = rc.right - 20;
        card.bottom = rc.bottom - 20;
        FillRect(hdc, &card, g_br_card);
        {
            HPEN pen = CreatePen(PS_SOLID, 1, CLR_LINE);
            HPEN old = (HPEN)SelectObject(hdc, pen);
            HBRUSH oldbr = (HBRUSH)SelectObject(hdc, GetStockObject(NULL_BRUSH));
            Rectangle(hdc, card.left, card.top, card.right, card.bottom);
            SelectObject(hdc, oldbr);
            SelectObject(hdc, old);
            DeleteObject(pen);
        }
        EndPaint(hwnd, &ps);
        return 0;
    }
    case WM_CTLCOLORSTATIC: {
        HDC hdc = (HDC)wParam;
        HWND ctrl = (HWND)lParam;
        COLORREF c = (COLORREF)GetWindowLongPtrW(ctrl, GWLP_USERDATA);
        if (!c) c = CLR_TEXT;
        SetTextColor(hdc, c);
        SetBkColor(hdc, CLR_CARD);
        return (LRESULT)g_br_card;
    }
    case WM_DRAWITEM:
        draw_owner_button((DRAWITEMSTRUCT *)lParam, button_style_from_id((int)wParam));
        return TRUE;
    case WM_COMMAND:
        if (HIWORD(wParam) == BN_CLICKED && LOWORD(wParam) == IDC_BIZ_VMP_DECODE) {
            show_vmp_serial_decode(hwnd);
            return 0;
        }
        if (HIWORD(wParam) == BN_CLICKED && LOWORD(wParam) == IDC_BIZ_CALC) {
            char a_s[64], b_s[64], out_s[128];
            wchar_t wr[128];
            double a, b, result = 0;
            int op, ok, st;
            /* 计算前检查授权;无效则提示,避免 VMP 直接杀进程 */
            if (g_vmp_serial[0]) {
                (void)kauth_vmp_set_serial(g_vmp_serial);
            }
            refresh_vmp_state_ui();
#ifdef KAUTH_USE_VMP
            st = kauth_vmp_get_serial_state();
            if (st != 0) {
                char tip[512];
                char sttxt[128];
                kauth_vmp_format_serial_state(st, sttxt, sizeof(sttxt));
                snprintf(tip, sizeof(tip),
                         "当前序列号无效:%s\n\n"
                         "有 vmpSerial 文本 ≠ 加壳程序已接受授权。\n"
                         "请确认:商家后台上传的 .vmp,与 VMProtect 加壳本 exe 时用的是同一工程/同一产品密钥。",
                         sttxt);
                show_msg(hwnd, "无法计算", tip);
                SetWindowTextW(GetDlgItem(hwnd, IDC_BIZ_RESULT), L"授权无效,未执行受保护计算");
                return 0;
            }
#else
            (void)st;
#endif
            get_edit_utf8(hwnd, IDC_BIZ_A, a_s, sizeof(a_s));
            get_edit_utf8(hwnd, IDC_BIZ_B, b_s, sizeof(b_s));
            op = (int)SendMessageW(GetDlgItem(hwnd, IDC_BIZ_OP), CB_GETCURSEL, 0, 0);
            a = atof(a_s);
            b = atof(b_s);
            ok = biz_locked_calc(a, op, b, &result);
            if (!ok) {
                SetWindowTextW(GetDlgItem(hwnd, IDC_BIZ_RESULT), L"计算失败(除零)");
            } else {
                snprintf(out_s, sizeof(out_s), "%.10g", result);
                utf8_to_wide(out_s, wr, 128);
                SetWindowTextW(GetDlgItem(hwnd, IDC_BIZ_RESULT), wr);
            }
            return 0;
        }
        if (LOWORD(wParam) == IDC_BIZ_LOGOUT && HIWORD(wParam) == BN_CLICKED) {
            leave_business();
            return 0;
        }
        break;
    case WM_HB_EVENT: {
        HbEvent *ev = (HbEvent *)lParam;
        char tip[320];
        wchar_t w[320];
        COLORREF color = CLR_TEXT;
        if (!ev) return 0;
        switch (ev->event) {
        case KAUTH_HEARTBEAT_SUCCESS:
            snprintf(tip, sizeof(tip), "心跳:正常");
            color = CLR_OK;
            break;
        case KAUTH_HEARTBEAT_NETWORK_FAIL:
            snprintf(tip, sizeof(tip), "心跳:网络失败 (%d) 连续失败=%d %s", ev->code, ev->failure_count, ev->msg);
            color = CLR_WARN;
            break;
        case KAUTH_HEARTBEAT_SERVER_FAIL:
            snprintf(tip, sizeof(tip), "心跳:服务端失败 code=%d %s", ev->code, ev->msg);
            color = CLR_DANGER;
            break;
        case KAUTH_HEARTBEAT_STOPPED:
            snprintf(tip, sizeof(tip), "心跳:已停止 code=%d %s", ev->code, ev->msg);
            color = CLR_MUTED;
            break;
        default:
            snprintf(tip, sizeof(tip), "心跳:未知事件");
            break;
        }
        SetWindowLongPtrW(GetDlgItem(hwnd, IDC_BIZ_HB), GWLP_USERDATA, (LONG_PTR)color);
        utf8_to_wide(tip, w, 320);
        SetWindowTextW(GetDlgItem(hwnd, IDC_BIZ_HB), w);
        free(ev);
        return 0;
    }
    case WM_CLOSE:
        leave_business();
        ShowWindow(hwnd, SW_HIDE);
        return 0;
    case WM_DESTROY:
        return 0;
    default:
        break;
    }
    return DefWindowProcW(hwnd, msg, wParam, lParam);
}

static void init_ui_resources(void) {
    g_font_brand = CreateFontW(-22, 0, 0, 0, FW_SEMIBOLD, 0, 0, 0,
                               DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, L"Segoe UI");
    g_font_title = CreateFontW(-18, 0, 0, 0, FW_SEMIBOLD, 0, 0, 0,
                               DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, L"Segoe UI");
    g_font_ui = CreateFontW(-15, 0, 0, 0, FW_NORMAL, 0, 0, 0,
                            DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, L"Segoe UI");
    g_font_small = CreateFontW(-13, 0, 0, 0, FW_NORMAL, 0, 0, 0,
                               DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, L"Segoe UI");
    g_font_btn = CreateFontW(-15, 0, 0, 0, FW_SEMIBOLD, 0, 0, 0,
                             DEFAULT_CHARSET, 0, 0, CLEARTYPE_QUALITY, 0, L"Segoe UI");
    g_br_bg = CreateSolidBrush(CLR_BG);
    g_br_card = CreateSolidBrush(CLR_CARD);
    g_br_header = CreateSolidBrush(CLR_HEADER);
}

static void free_ui_resources(void) {
    if (g_font_brand) DeleteObject(g_font_brand);
    if (g_font_title) DeleteObject(g_font_title);
    if (g_font_ui) DeleteObject(g_font_ui);
    if (g_font_small) DeleteObject(g_font_small);
    if (g_font_btn) DeleteObject(g_font_btn);
    if (g_br_bg) DeleteObject(g_br_bg);
    if (g_br_card) DeleteObject(g_br_card);
    if (g_br_header) DeleteObject(g_br_header);
}

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrev, PWSTR cmd, int show) {
    WNDCLASSW wc_login, wc_biz, wc_page;
    MSG msg;
    int cx, cy;
    (void)hPrev;
    (void)cmd;

    g_inst = hInstance;
    init_ui_resources();

    if (init_sdk() != 0) {
        MessageBoxW(NULL, L"SDK 初始化失败,请检查程序配置", L"kauth", MB_ICONERROR);
        free_ui_resources();
        return 1;
    }

    memset(&wc_page, 0, sizeof(wc_page));
    wc_page.lpfnWndProc = PageProc;
    wc_page.hInstance = hInstance;
    wc_page.lpszClassName = L"KauthPage";
    wc_page.hbrBackground = g_br_card;
    RegisterClassW(&wc_page);

    memset(&wc_login, 0, sizeof(wc_login));
    wc_login.lpfnWndProc = LoginProc;
    wc_login.hInstance = hInstance;
    wc_login.lpszClassName = L"KauthLoginWnd";
    wc_login.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc_login.hbrBackground = g_br_bg;
    RegisterClassW(&wc_login);

    memset(&wc_biz, 0, sizeof(wc_biz));
    wc_biz.lpfnWndProc = BizProc;
    wc_biz.hInstance = hInstance;
    wc_biz.lpszClassName = L"KauthBizWnd";
    wc_biz.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc_biz.hbrBackground = g_br_bg;
    RegisterClassW(&wc_biz);

    cx = GetSystemMetrics(SM_CXSCREEN);
    cy = GetSystemMetrics(SM_CYSCREEN);

    g_login = CreateWindowExW(0, L"KauthLoginWnd", L"kauth",
                              WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_THICKFRAME,
                              (cx - 640) / 2, (cy - 620) / 2, 640, 620,
                              NULL, NULL, hInstance, NULL);
    g_biz = CreateWindowExW(0, L"KauthBizWnd", L"kauth",
                            WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
                            (cx - 540) / 2, (cy - 700) / 2, 540, 700,
                            NULL, NULL, hInstance, NULL);

    ShowWindow(g_login, show);
    UpdateWindow(g_login);

    while (GetMessageW(&msg, NULL, 0, 0) > 0) {
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }

    kauth_stop_heartbeat();
    free_ui_resources();
    return (int)msg.wParam;
}

编译示意(无 VMP):

bat
cl /nologo /O2 /MT /DUNICODE /D_UNICODE /I. ka_win.c /link kauth.lib user32.lib gdi32.lib comctl32.lib /SUBSYSTEM:WINDOWS /OUT:ka_win.exe

VMProtect(可选)

默认可不启用。未定义 KAUTH_USE_VMP 时,登录与业务仍可用;HWID/序列号 API 为桩。详细入参出参见上文「VMP 辅助」。

流程

text
编译定义 KAUTH_USE_VMP + 链接 VMProtectSDK64.lib
  → 登录填 hardwareId(ka_win 已调用 kauth_vmp_get_hwid)
  → 服务端返回 vmpSerial(商家已上传 .vmp)
  → SDK 自动 SetSerialNumber
  → 用同一份 .vmp 对 exe 加壳并启用 Licensing
  → 敏感逻辑用 KAUTH_VMP_BEGIN_LOCK(见 ka_win 计算器)

商家后台

程序 → VMP 密钥:上传与加壳 同一份 .vmp。可选参数(过期日 / 最大编译日 / 运行时间等)由 VMP 运行时自动校验,一般不必手写判断。卡密业务到期仍由 kauth 心跳管理。

保护业务代码

c
#include "kauth_vmp.h"

KAUTH_VMP_BEGIN_LOCK("biz_calc");
/* ... */
KAUTH_VMP_END();

Begin/End 必须同函数成对。加壳与后台签发必须同一 .vmp,否则序列号 INVALID

ka_win 业务窗「解密」按钮会弹窗展示 kauth_vmp_get_serial_info 明文。


常见问题

没有 SDK 源码怎么办? 正常。用网盘的 kauth.lib + 头文件 + 示例即可。

MinGW? 不支持,请用 MSVC。

/MD 冲突? 必须 /MT

没有 vmpSerial? 程序未配 VMP 密钥时不下发,属正常。

有 vmpSerial 但 LockByKey 失败? 加壳 .vmp 须与后台一致。

注册成功会进业务吗? 不会,需再登录(ka_win 已按此处理)。


不要做的事

  • 不要用 /MD 或 MinGW 链本库
  • 不要期望拿到 SDK 内部源码
  • 不要把演示配置里的密钥直接用于生产(请换成你自己的)

网盘再次确认:
https://pan.kauth.cn/C语言卡密核心源码

旧版「C 动态库」说明见 C语言接口。新工程请用本文静态库交付包。