!
开发指南总览
本指南将帮助你快速将客户端软件对接到卡密管理系统。系统提供 6 个核心 API 接口,覆盖
版本检查 → 软件公告 → 免费试用 → 卡密认证 → 心跳保活 → 自助解绑
完整生命周期。所有接口均无需登录认证,使用固定的 /api/client 前缀。
快速导航
对接流程
版本检查
软件公告
充值链接
免费试用
卡密认证
心跳保活
试用心跳
自助解绑
机器码生成
Python 完整示例
C# 完整示例
实战 UI 示例
其他语言示例
错误码参考
1
对接流程
一、用户使用流程(用户视角)
用户拿到你的EXE软件或APK软件后,从头到尾会发生什么:
1
用户打开软件 → 软件自动连接你的服务器,检查版本是否最新。如果服务器上有新版本,弹窗提示用户下载更新。
2
试用还是输入卡密? → 如果你在后台开启了免费试用,软件会自动帮用户开启试用,用户可以直接用,不用输入卡密。试用到期后,软件弹出输入框要求用户输入卡密。
3
用户输入卡密 → 用户把你发给他的卡密填进去,软件把卡密+本机机器码一起发给服务器验证。服务器检查:卡密存不存在?过期了没?绑定的设备数超了没?都没问题就通过,用户进入软件主功能。
4
软件持续运行 → 软件在后台每隔5分钟自动给服务器发一次心跳,告诉服务器"我还在用"。如果心跳失败(卡密过期、被禁用、设备被解绑),软件立即停止运行,提示用户重新输入卡密。
二、开发者对接流程(你的视角)
开发软件时,你需要做这几步:
1
后台创建应用 → 在「应用管理」页面创建一个应用,填好软件名称、版本号、下载链接。系统会自动生成一个应用密钥(app_key),这是一个32位的随机字符串,相当于你这个软件的身份证号。
2
后台生成卡密 → 在「卡密管理」页面生成卡密,选择卡密类型(天卡/月卡/年卡/永久等)、设备限制(一机一码=1台,最多3台=3)。生成的卡密发给你的用户,用户拿这个卡密激活软件。
3
软件里写入SDK代码 → 把本指南提供的Python/C#/Go等SDK代码复制到你的软件里,填入服务器地址
http://cdk.bcwlgzs.top 和应用密钥。SDK会自动处理版本检查、试用、认证、心跳。
4
软件里生成机器码 → 在软件启动时调用
generate_machine_code() 获取本机机器码(基于CPU+主板+硬盘+MAC生成的唯一标识),每次调用API时带上这个机器码,服务器靠它判断"是不是同一台电脑"。
5
编译发布软件 → 把写好的软件编译成EXE或APK,发给用户。用户打开后自动走上面的"用户使用流程"。
三、API调用链路(技术视角)
软件启动后代码层面的调用顺序:
1
版本检查 → 调用
GET /api/client/version,传 app_key + 当前版本号,返回是否有新版本
2
软件公告 → 调用
GET /api/client/notice,传 app_key,获取公告内容展示给用户
3
试用查询 → 调用
POST /api/client/trial,传 app_key + machine_code。返回试用有效 → 进入软件;返回试用到期 → 跳到第4步
4
卡密认证 → 调用
POST /api/client/auth,传 app_key + key_code + machine_code。首次使用自动激活,自动绑定设备。成功 → 进入软件
5
心跳保活 → 进入软件后每隔 5分钟 调用
POST /api/client/heartbeat,传 app_key + key_code + machine_code。试用用户调 POST /api/client/trial/heartbeat。心跳失败 → 停止运行
6
自助解绑(可选)→ 用户换电脑时调用
POST /api/client/unbind,解除旧设备绑定,释放设备名额。需在后台开启自助解绑功能
所有接口的 app_key 参数来自「应用管理」页面中每个应用自动生成的应用密钥(32位随机字符串)。机器码 machine_code 需要客户端自行生成(见机器码生成章节)。
2
版本检查接口
GET
/api/client/version
无需认证
查询应用最新版本号,判断客户端是否需要更新。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| app_key | string | 是 | 应用密钥 |
| current_version | string | 是 | 客户端当前版本号,如 "1.0" |
✅ 成功响应
{
"status": "success",
"app_name": "测试应用",
"latest_version": "2.0",
"current_version": "1.0",
"update_available": true,
"update_url": "https://example.com/download",
"update_tip": "发现新版本 2.0",
"notice": "欢迎使用本软件!如有问题请联系客服。",
"recharge_url": "https://example.com/buy"
}
3
软件公告接口
GET
/api/client/notice
无需认证
获取软件公告内容,可在软件启动时展示给用户(如维护通知、活动公告等)。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| app_key | string | 是 | 应用密钥 |
✅ 成功响应
{
"status": "success",
"app_name": "测试应用",
"app_key": "a1b2c3d4...",
"notice": "欢迎使用本软件!\n如有问题请联系客服QQ:123456789\n\n【公告】9月15日凌晨2点系统维护,预计1小时。"
}
说明:公告内容在后台「应用管理 → 编辑 → 软件公告」中设置,支持换行。
4
充值链接接口
GET
/api/client/recharge
无需认证
获取充值购买链接,用户点击充值按钮时跳转到此链接进行购买续费。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| app_key | string | 是 | 应用密钥 |
✅ 成功响应
{
"status": "success",
"app_name": "测试应用",
"app_key": "a1b2c3d4...",
"recharge_url": "https://example.com/buy"
}
说明:充值链接在后台「应用管理 → 编辑 → 充值链接」中设置。
5
免费试用接口
POST
/api/client/trial
无需认证
每台设备每个应用可免费试用一次,到期后需输入卡密继续使用。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| app_key | string | 是 | 应用密钥 |
| machine_code | string | 是 | 设备机器码 |
✅ 试用有效 / 首次开启
{
"status": "success",
"message": "试用已开启,有效期 24 小时",
"app_name": "测试应用",
"start_time": "2026-09-10 10:00:00",
"expire_time": "2026-09-11 10:00:00",
"remaining_hours": 24.0
}
❌ 试用到期
HTTP 403
{
"status": "error",
"message": "试用已到期,请输入卡密继续使用"
}
6
卡密认证接口
POST
/api/client/auth
无需认证
核心认证接口:验证卡密有效性,绑定设备,首次使用自动激活。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| app_key | string | 是 | 应用密钥 |
| key_code | string | 是 | 用户输入的卡密 |
| machine_code | string | 是 | 设备机器码 |
| ip_address | string | 否 | 设备IP,不传则使用请求来源IP |
✅ 认证成功
{
"status": "success",
"message": "认证成功",
"key_info": {
"key_code": "A1B2C3D4E5F6G7H8",
"app_name": "测试应用",
"key_type": "month",
"start_time": "2026-09-10 10:00:00",
"expire_time": "2026-10-10 10:00:00",
"device_limit": 2,
"used_count": 1
},
"app_info": {
"app_name": "测试应用",
"app_key": "a1b2c3d4...",
"latest_version": "2.0",
"update_available": false,
"update_url": "",
"notice": "欢迎使用本软件!如有问题请联系客服。"
}
}
key_type 可选值:day / week / month / quarter / year / permanent / custom
device_limit 为该卡密允许绑定的最大设备数,used_count 为已绑定设备数
device_limit 为该卡密允许绑定的最大设备数,used_count 为已绑定设备数
7
心跳保活接口
POST
/api/client/heartbeat
无需认证
认证成功后每 5 分钟调用一次,维持在线状态并检查卡密有效性。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| app_key | string | 是 | 应用密钥 |
| key_code | string | 是 | 已认证的卡密 |
| machine_code | string | 是 | 已绑定的设备机器码 |
✅ 心跳成功
{
"status": "success",
"message": "心跳更新成功",
"key_code": "A1B2C3D4E5F6G7H8",
"app_name": "测试应用",
"machine_code": "1A2B3C...",
"last_heartbeat": "2026-09-10 10:05:00",
"expire_time": "2026-10-10 10:00:00",
"remaining": "720.0 小时",
"device_limit": 2
}
心跳接口同时也会检查卡密是否过期/禁用。若心跳返回 403,应立即停止软件运行并提示用户重新认证。
8
试用心跳保活接口
POST
/api/client/trial/heartbeat
无需认证
免费试用用户的专用心跳接口,试用期间每 5 分钟调用一次,维持试用在线状态。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| app_key | string | 是 | 应用密钥 |
| machine_code | string | 是 | 试用设备机器码 |
✅ 心跳成功
{
"status": "success",
"message": "心跳更新成功",
"app_name": "测试应用",
"machine_code": "1A2B3C...",
"start_time": "2026-09-10 10:00:00",
"expire_time": "2026-09-11 10:00:00",
"remaining_hours": 23.5,
"last_heartbeat": "2026-09-10 10:05:00"
}
❌ 试用已到期
HTTP 403
{
"status": "error",
"message": "试用已到期,请输入卡密继续使用"
}
试用心跳与普通心跳独立,仅用于免费试用设备。试用到期后应切换为卡密认证流程。
9
自助解绑接口
POST
/api/client/unbind
需应用开启
客户端自助解除当前设备与卡密的绑定,释放设备名额。需应用开启自助解绑功能,且受30天内次数限制。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| app_key | string | 是 | 应用密钥 |
| key_code | string | 是 | 卡密码 |
| machine_code | string | 是 | 要解绑的设备机器码 |
✅ 解绑成功
{
"status": "success",
"message": "解绑成功",
"remaining_unbinds": 2,
"limit_30d": 3
}
❌ 解绑失败
HTTP 403
{
"status": "error",
"message": "30天内解绑次数已达上限(3次),请联系管理员"
}
remaining_unbinds 表示该卡密30天内剩余可解绑次数。limit_30d 为30天解绑次数上限。解绑次数限制由应用配置决定,默认为3次/30天。
10
机器码生成
机器码用于唯一标识设备,建议组合 CPU、主板、硬盘等硬件信息生成 MD5。以下是 Windows 平台的生成方法:
Python
C#
Go
Node.js
Python (需安装 wmi, psutil)
import wmi, hashlib, psutil
def get_cpu_info():
return wmi.WMI().Win32_Processor()[0].ProcessorId
def get_motherboard_info():
return wmi.WMI().Win32_BaseBoard()[0].SerialNumber
def get_disk_info():
return wmi.WMI().Win32_DiskDrive()[0].SerialNumber.strip()
def get_mac_address():
for interface, addrs in psutil.net_if_addrs().items():
for addr in addrs:
if addr.family == psutil.AF_LINK and interface != "Loopback Pseudo-Interface 1":
return addr.address.replace("-", "").upper()
def generate_machine_code():
raw = f"{get_cpu_info()}_{get_motherboard_info()}_{get_disk_info()}_{get_mac_address()}"
return hashlib.md5(raw.encode()).hexdigest().upper()
11
Python 完整对接示例
包含版本检查、软件公告、试用查询、卡密认证、心跳保活的完整封装类:
Python (需安装 requests)
class KeyManagerClient:
"""卡密管理系统客户端 SDK(完整封装)
包含:版本检查、软件公告、免费试用、试用心跳、卡密认证、心跳保活、自助解绑
"""
def __init__(self, server_url, app_key, current_version="1.0"):
self.server_url = server_url.rstrip("/")
self.app_key = app_key
self.current_version = current_version
self.machine_code = self._generate_machine_code()
self.key_code = ""
self._heartbeat_thread = None
self._running = False
def _generate_machine_code(self):
"""生成机器码(MAC+计算机名+硬盘卷序列号)"""
import hashlib, uuid, os, sys, ctypes
mac = uuid.getnode()
name = os.environ.get('COMPUTERNAME', '')
vol = ''
if sys.platform == 'win32':
try:
serial = ctypes.c_ulong(0)
ctypes.windll.kernel32.GetVolumeInformationW(
ctypes.c_wchar_p("C:\\"), None, 0, ctypes.byref(serial),
None, None, None, 0
)
vol = f"{serial.value:08X}"
except:
pass
raw = f"{mac}_{name}_{vol}"
return hashlib.md5(raw.encode()).hexdigest().upper()
def set_machine_code(self, code):
"""手动设置机器码(如需自定义)"""
self.machine_code = code
# ── 1. 版本检查 ────────────────────────
def check_version(self):
"""检查是否有新版本
返回: (has_update: bool, data: dict)
"""
import requests
try:
resp = requests.get(f"{self.server_url}/api/client/version", params={
"app_key": self.app_key,
"current_version": self.current_version,
}, timeout=10)
data = resp.json()
if data.get("status") == "success":
return data.get("update_available", False), data
return False, data
except Exception as e:
return False, {"status": "error", "message": str(e)}
# ── 2. 软件公告 ────────────────────────
def get_notice(self):
"""获取软件公告
返回: (notice: str, data: dict)
"""
import requests
try:
resp = requests.get(f"{self.server_url}/api/client/notice", params={
"app_key": self.app_key,
}, timeout=10)
data = resp.json()
if data.get("status") == "success":
return data.get("notice", ""), data
return "", data
except Exception as e:
return "", {"status": "error", "message": str(e)}
# ── 3. 充值链接 ────────────────────────
def get_recharge_url(self):
"""获取充值购买链接
返回: (recharge_url: str, data: dict)
"""
import requests
try:
resp = requests.get(f"{self.server_url}/api/client/recharge", params={
"app_key": self.app_key,
}, timeout=10)
data = resp.json()
if data.get("status") == "success":
return data.get("recharge_url", ""), data
return "", data
except Exception as e:
return "", {"status": "error", "message": str(e)}
# ── 4. 免费试用 ────────────────────────
def start_trial(self):
"""开启/查询免费试用
返回: (success: bool, data: dict)
"""
import requests
try:
resp = requests.post(f"{self.server_url}/api/client/trial", json={
"app_key": self.app_key,
"machine_code": self.machine_code,
}, timeout=10)
data = resp.json()
return resp.status_code == 200, data
except Exception as e:
return False, {"status": "error", "message": str(e)}
# ── 4. 卡密认证 ────────────────────────
def authenticate(self, key_code):
"""卡密认证(激活 + 绑定设备)
返回: (success: bool, data: dict)
"""
import requests
self.key_code = key_code
try:
resp = requests.post(f"{self.server_url}/api/client/auth", json={
"app_key": self.app_key,
"key_code": key_code,
"machine_code": self.machine_code,
}, timeout=10)
data = resp.json()
if data.get("status") == "success":
self._start_heartbeat()
return True, data
return False, data
except Exception as e:
return False, {"status": "error", "message": str(e)}
# ── 5. 心跳保活 ────────────────────────
def heartbeat(self):
"""发送心跳(保持在线状态)
返回: (success: bool, data: dict)
"""
import requests
try:
resp = requests.post(f"{self.server_url}/api/client/heartbeat", json={
"app_key": self.app_key,
"key_code": self.key_code,
"machine_code": self.machine_code,
}, timeout=10)
data = resp.json()
return resp.status_code == 200 and data.get("status") == "success", data
except Exception as e:
return False, {"status": "error", "message": str(e)}
# ── 6. 自助解绑 ────────────────────────
def self_unbind(self):
"""自助解绑当前设备
返回: (success: bool, data: dict)
"""
import requests
try:
resp = requests.post(f"{self.server_url}/api/client/unbind", json={
"app_key": self.app_key,
"key_code": self.key_code,
"machine_code": self.machine_code,
}, timeout=10)
data = resp.json()
if resp.status_code == 200 and data.get("status") == "success":
self._stop_heartbeat()
return True, data
return False, data
except Exception as e:
return False, {"status": "error", "message": str(e)}
# ── 7. 试用心跳 ────────────────────────
def trial_heartbeat(self):
"""试用设备心跳(免费试用用户调用此接口保活)
返回: (success: bool, data: dict)
"""
import requests
try:
resp = requests.post(f"{self.server_url}/api/client/trial/heartbeat", json={
"app_key": self.app_key,
"machine_code": self.machine_code,
}, timeout=10)
data = resp.json()
return resp.status_code == 200 and data.get("status") == "success", data
except Exception as e:
return False, {"status": "error", "message": str(e)}
# ── 心跳自动管理 ────────────────────────
def _start_heartbeat(self):
"""启动后台心跳线程(5分钟一次)"""
import threading, time
if self._running:
return
self._running = True
def _loop():
while self._running:
time.sleep(300) # 5分钟
if not self._running:
break
try:
success, data = self.heartbeat()
if not success:
self._running = False
break
except:
pass
self._heartbeat_thread = threading.Thread(target=_loop, daemon=True)
self._heartbeat_thread.start()
def _stop_heartbeat(self):
"""停止心跳"""
self._running = False
# ── 完整使用示例 ──────────────────────────
if __name__ == "__main__":
# 初始化客户端
client = KeyManagerClient(
server_url="http://cdk.bcwlgzs.top",
app_key="你的应用密钥",
current_version="1.0"
)
print(f"本机机器码: {client.machine_code}")
print("=" * 50)
# 1. 版本检查
print("→ 检查版本...")
has_update, ver_info = client.check_version()
if has_update:
print(f" 发现新版本: {ver_info.get('latest_version')}")
print(f" 下载地址: {ver_info.get('update_url')}")
else:
print(" 当前已是最新版本")
# 2. 获取软件公告
print("\n→ 获取公告...")
notice, notice_data = client.get_notice()
if notice:
print(" 【软件公告】")
for line in notice.split("\n"):
print(f" {line}")
# 3. 获取充值链接
print("\n→ 获取充值链接...")
recharge_url, recharge_data = client.get_recharge_url()
if recharge_url:
print(f" 充值地址: {recharge_url}")
# 4. 先尝试免费试用
print("\n→ 查询试用...")
trial_ok, trial_data = client.start_trial()
is_trial_mode = False
if trial_ok and trial_data.get("status") == "success":
is_trial_mode = True
remaining = trial_data.get("remaining_hours", 0)
print(f" ✓ 试用中,剩余 {remaining} 小时")
print(f" 到期时间: {trial_data.get('expire_time')}")
else:
print(f" ✕ 试用不可用: {trial_data.get('message')}")
# 5. 如果试用不行,让用户输入卡密
if not is_trial_mode:
print("\n→ 卡密认证")
key_code = input(" 请输入卡密: ").strip()
auth_ok, auth_data = client.authenticate(key_code)
if auth_ok:
ki = auth_data.get("key_info", {})
print(f" ✓ 认证成功!卡密类型: {ki.get('key_type')}")
print(f" 到期时间: {ki.get('expire_time')}")
print(f" 已绑定设备: {ki.get('used_count')}/{ki.get('device_limit')}")
else:
print(f" ✕ 认证失败: {auth_data.get('message')}")
exit()
# 6. 软件主逻辑
print("\n" + "=" * 50)
print("软件运行中,心跳自动保活...")
print("按 Ctrl+C 退出")
try:
import time
while True:
time.sleep(1)
except KeyboardInterrupt:
client._stop_heartbeat()
print("\n已退出")
12
C# 完整对接示例
适用于 WinForms / WPF 桌面应用的完整封装:
C# (需 Newtonsoft.Json 或 System.Text.Json)
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
public class KeyManagerClient
{
private readonly string _serverUrl;
private readonly string _appKey;
private readonly string _currentVersion;
private string _machineCode;
private string _keyCode;
private Timer _heartbeatTimer;
private readonly HttpClient _http;
public KeyManagerClient(string serverUrl, string appKey, string currentVersion = "1.0")
{
_serverUrl = serverUrl.TrimEnd('/');
_appKey = appKey;
_currentVersion = currentVersion;
_http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
}
public void SetMachineCode(string code) => _machineCode = code;
// ── 版本检查 ──────────────────────────
public async Task<(bool hasUpdate, JsonElement data)> CheckVersionAsync()
{
var url = $"{_serverUrl}/api/client/version?app_key={_appKey}¤t_version={_currentVersion}";
var resp = await _http.GetAsync(url);
var data = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()).RootElement;
if (data.GetProperty("status").GetString() == "success")
return (data.GetProperty("update_available").GetBoolean(), data);
return (false, data);
}
// ── 免费试用 ──────────────────────────
public async Task<(bool success, JsonElement data)> StartTrialAsync()
{
var body = new { app_key = _appKey, machine_code = _machineCode };
var resp = await _http.PostAsync($"{_serverUrl}/api/client/trial",
new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"));
var data = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()).RootElement;
return (resp.IsSuccessStatusCode, data);
}
// ── 卡密认证 ──────────────────────────
public async Task<(bool success, JsonElement data)> AuthenticateAsync(string keyCode)
{
_keyCode = keyCode;
var body = new { app_key = _appKey, key_code = keyCode, machine_code = _machineCode };
var resp = await _http.PostAsync($"{_serverUrl}/api/client/auth",
new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"));
var data = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()).RootElement;
if (data.GetProperty("status").GetString() == "success")
{
StartHeartbeat();
return (true, data);
}
return (false, data);
}
// ── 心跳保活 ──────────────────────────
public async Task<(bool success, JsonElement data)> HeartbeatAsync()
{
var body = new { app_key = _appKey, key_code = _keyCode, machine_code = _machineCode };
var resp = await _http.PostAsync($"{_serverUrl}/api/client/heartbeat",
new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"));
var data = JsonDocument.Parse(await resp.Content.ReadAsStringAsync()).RootElement;
return (resp.IsSuccessStatusCode, data);
}
private void StartHeartbeat()
{
_heartbeatTimer?.Dispose();
_heartbeatTimer = new Timer(async _ =>
{
try
{
var (success, data) = await HeartbeatAsync();
if (!success)
Console.WriteLine($"心跳失败: {data.GetProperty("message").GetString()}");
}
catch (Exception ex)
{
Console.WriteLine($"心跳异常: {ex.Message}");
}
}, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
}
public void Stop() => _heartbeatTimer?.Dispose();
}
// ── 使用示例 ────────────────────────────
// var client = new KeyManagerClient("http://cdk.bcwlgzs.top", "你的应用密钥", "1.0");
// client.SetMachineCode("AABBCCDDEE112233");
//
// var (hasUpdate, info) = await client.CheckVersionAsync();
// var (trialOk, trialData) = await client.StartTrialAsync();
// var (authOk, authData) = await client.AuthenticateAsync("用户输入的卡密");
// client.Stop();
13
实战 Python UI 对接示例(带界面)
打开软件后先弹出验证窗口,用户可以选择「免费试用」直接进入,或者输入卡密点「激活」进入。界面美观,开箱即用:
🔐 卡密激活
输入卡密一键激活,绑定机器码
🎁 免费试用
每台设备限试用一次,防刷
💓 心跳保活
后台自动心跳,掉线自动退出
⏳ 卡密加时
新卡密叠加时长,不用解绑
🔓 自助解绑
用户可自行解绑,换设备用
📢 软件公告
后台发布公告,实时推送
💰 充值跳转
一键跳转到购买页面
💾 自动登录
记住卡密,下次自动填
Python (需安装 requests, tkinter 自带)
# -*- coding: utf-8 -*-
"""
卡密授权客户端 - 精美 UI 版
"""
import requests
import threading
import time
import hashlib
import uuid
import os
import sys
import ctypes
import json
import webbrowser
import tkinter as tk
from tkinter import ttk, messagebox
# ═══════════════════════════════════════════════
# 配置区
# ═══════════════════════════════════════════════
SERVER_URL = "http://cdk.bcwlgzs.top"
APP_KEY = "77752870ae7091240e92e8d1d63a1631"
CURRENT_VERSION = "1.0"
HEARTBEAT_INTERVAL = 60
CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "auth_config.json")
# 主题色
PRIMARY = "#6366f1" # 主色 - 靛蓝
PRIMARY_DARK = "#4f46e5" # 主色深
PRIMARY_LIGHT = "#818cf8" # 主色浅
ACCENT = "#ec4899" # 点缀色 - 粉
SUCCESS = "#10b981"
WARNING = "#f59e0b"
ERROR = "#ef4444"
BG = "#f8fafc"
CARD = "#ffffff"
TEXT = "#1e293b"
TEXT_SEC = "#64748b"
TEXT_MUTE = "#94a3b8"
BORDER = "#e2e8f0"
# ═══════════════════════════════════════════════
# 工具函数
# ═══════════════════════════════════════════════
def generate_machine_code():
mac = uuid.getnode()
name = os.environ.get('COMPUTERNAME', '')
vol = ''
if sys.platform == 'win32':
try:
serial = ctypes.c_ulong(0)
ctypes.windll.kernel32.GetVolumeInformationW(
ctypes.c_wchar_p("C:\\"), None, 0, ctypes.byref(serial),
None, None, None, 0
)
vol = f"{serial.value:08X}"
except:
pass
raw = f"{mac}_{name}_{vol}"
return hashlib.md5(raw.encode()).hexdigest().upper()
def save_config(key_code="", remember=False, auto_login=False, is_trial=False):
config = {"key_code": key_code, "remember": remember, "auto_login": auto_login, "is_trial": is_trial}
try:
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(config, f, ensure_ascii=False, indent=2)
except:
pass
def load_config():
try:
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except:
pass
return {}
def format_expire_time(time_str):
"""格式化到期时间为中文格式:2026年9月12日 01:53:44"""
if not time_str:
return "未知"
try:
# 支持 2026-09-12 01:53:44 格式
if " " in time_str and "-" in time_str:
date_part, time_part = time_str.split(" ", 1)
parts = date_part.split("-")
if len(parts) == 3:
y, m, d = parts
return f"{int(y)}年{int(m)}月{int(d)}日 {time_part}"
except:
pass
return time_str
def api_post(path, data):
try:
resp = requests.post(f"{SERVER_URL}{path}", json=data, timeout=10)
return resp.status_code, resp.json()
except Exception as e:
return 0, {"status": "error", "message": str(e)}
def api_get(path, params):
try:
resp = requests.get(f"{SERVER_URL}{path}", params=params, timeout=10)
return resp.json()
except Exception as e:
return {"status": "error", "message": str(e)}
# ═══════════════════════════════════════════════
# 精美圆角按钮
# ═══════════════════════════════════════════════
class FancyButton(tk.Canvas):
def __init__(self, master, text, command=None, width=160, height=44,
bg=PRIMARY, fg="white", hover_bg=PRIMARY_DARK,
active_bg="#3730a3", shadow_color="#e0e7ff",
font=("Microsoft YaHei", 10, "bold"),
radius=12, icon=None, **kwargs):
super().__init__(master, width=width, height=height,
bg=master["bg"], highlightthickness=0, **kwargs)
self.command = command
self.bg_color = bg
self.hover_color = hover_bg
self.active_color = active_bg
self.shadow_color = shadow_color
self.fg_color = fg
self.radius = radius
self.text = text
self.font = font
self.width = width
self.height = height
self.icon = icon
self._disabled = False
self._current_color = bg
self.bind("<Enter>", self._on_enter)
self.bind("<Leave>", self._on_leave)
self.bind("<Button-1>", self._on_click)
self.bind("<ButtonRelease-1>", self._on_release)
self._draw(self.bg_color)
def _draw(self, color):
self._current_color = color
self.delete("all")
r = self.radius
w, h = self.width, self.height
# 底部阴影(模拟)
shadow_y = h - 2
sc = self.shadow_color
self.create_arc((2, shadow_y - r*2 + 2, r*2 + 2, shadow_y + 2),
start=180, extent=90, fill=sc, outline="")
self.create_arc((w-r*2 - 2, shadow_y - r*2 + 2, w - 2, shadow_y + 2),
start=270, extent=90, fill=sc, outline="")
self.create_rectangle((r+2, shadow_y, w-r-2, shadow_y+2), fill=sc, outline="")
self.create_rectangle((2, shadow_y - r + 2, w-2, shadow_y + 2 - r + r), fill=sc, outline="")
# 主体圆角矩形
self.create_arc((0, 0, r*2, r*2), start=90, extent=90, fill=color, outline=color)
self.create_arc((w-r*2, 0, w, r*2), start=0, extent=90, fill=color, outline=color)
self.create_arc((0, h-r*2 - 2, r*2, h - 2), start=180, extent=90, fill=color, outline=color)
self.create_arc((w-r*2, h-r*2 - 2, w, h - 2), start=270, extent=90, fill=color, outline=color)
self.create_rectangle((r, 0, w-r, h-2), fill=color, outline=color)
self.create_rectangle((0, r, w, h-r-2), fill=color, outline=color)
# 顶部高光
self.create_arc((4, 2, r*2 - 4, r*2 - 4), start=90, extent=90,
fill=self._lighten(color, 20), outline="")
self.create_arc((w-r*2 + 4, 2, w - 4, r*2 - 4), start=0, extent=90,
fill=self._lighten(color, 20), outline="")
self.create_rectangle((r, 2, w-r, r-2), fill=self._lighten(color, 20), outline="")
# 文字 + 图标
display_text = f"{self.icon} {self.text}" if self.icon else self.text
self.create_text(w//2, h//2 - 1, text=display_text,
fill=self.fg_color, font=self.font)
def _lighten(self, hex_color, amount):
"""颜色变亮"""
# 处理英文颜色名
color_map = {
"white": "#ffffff",
"black": "#000000",
"red": "#ff0000",
"green": "#00ff00",
"blue": "#0000ff",
}
if hex_color.lower() in color_map:
hex_color = color_map[hex_color.lower()]
if not hex_color.startswith("#") or len(hex_color) != 7:
return hex_color
try:
r = int(hex_color[1:3], 16)
g = int(hex_color[3:5], 16)
b = int(hex_color[5:7], 16)
r = min(255, r + amount)
g = min(255, g + amount)
b = min(255, b + amount)
return f"#{r:02x}{g:02x}{b:02x}"
except:
return hex_color
def _on_enter(self, event):
if not self._disabled:
self._draw(self.hover_color)
self.config(cursor="hand2")
def _on_leave(self, event):
if not self._disabled:
self._draw(self.bg_color)
self.config(cursor="")
def _on_click(self, event):
if not self._disabled:
self._draw(self.active_color)
def _on_release(self, event):
if not self._disabled:
self._draw(self.hover_color)
if self.command:
self.command()
def config(self, **kwargs):
if "state" in kwargs:
state = kwargs["state"]
if state == tk.DISABLED:
self._disabled = True
self._draw("#9ca3af")
elif state == tk.NORMAL:
self._disabled = False
self._draw(self.bg_color)
super().config(**kwargs)
# ═══════════════════════════════════════════════
# 精致输入框
# ═══════════════════════════════════════════════
class FancyEntry(tk.Frame):
def __init__(self, master, placeholder="", icon=None, show=None, **kwargs):
super().__init__(master, bg=master["bg"], **kwargs)
self.placeholder = placeholder
self.has_focus = False
self.show = show
# 外层边框容器
self.border_frame = tk.Frame(self, bg=BORDER)
self.border_frame.pack(fill="x", ipady=1)
# 内层白色背景
self.inner = tk.Frame(self.border_frame, bg="white")
self.inner.pack(fill="x", padx=1, pady=1)
# 图标
if icon:
self.icon_label = tk.Label(self.inner, text=icon, font=("Segoe UI Emoji", 13),
bg="white", fg=TEXT_MUTE)
self.icon_label.pack(side="left", padx=(12, 6), pady=8)
# 输入框
self.entry = tk.Entry(self.inner, font=("Microsoft YaHei", 11),
relief="flat", bd=0, bg="white", fg=TEXT,
insertbackground=PRIMARY, show=show or "")
self.entry.pack(side="left", fill="x", expand=True, padx=(0, 12), pady=8)
# 占位符
self._show_placeholder()
# 事件绑定
self.entry.bind("<FocusIn>", self._on_focus_in)
self.entry.bind("<FocusOut>", self._on_focus_out)
def _show_placeholder(self):
if not self.entry.get():
self.entry.insert(0, self.placeholder)
self.entry.config(fg=TEXT_MUTE)
if self.show:
self.entry.config(show="")
def _hide_placeholder(self):
if self.entry.get() == self.placeholder:
self.entry.delete(0, tk.END)
self.entry.config(fg=TEXT)
if self.show:
self.entry.config(show=self.show)
def _on_focus_in(self, event):
self.has_focus = True
self.border_frame.config(bg=PRIMARY_LIGHT)
self._hide_placeholder()
def _on_focus_out(self, event):
self.has_focus = False
self.border_frame.config(bg=BORDER)
if not self.entry.get():
self._show_placeholder()
def get(self):
text = self.entry.get()
if text == self.placeholder:
return ""
return text.strip()
def set(self, text):
self.entry.delete(0, tk.END)
self.entry.insert(0, text)
self.entry.config(fg=TEXT)
if self.show:
self.entry.config(show=self.show)
def bind(self, event, callback):
self.entry.bind(event, callback)
# ═══════════════════════════════════════════════
# 自定义复选框
# ═══════════════════════════════════════════════
class FancyCheckbox(tk.Frame):
def __init__(self, master, text="", variable=None, **kwargs):
super().__init__(master, bg=master["bg"], **kwargs)
self.var = variable if variable else tk.BooleanVar(value=False)
self.text = text
self.box = tk.Label(self, text="☐", font=("Segoe UI Emoji", 12),
bg=master["bg"], fg=TEXT_MUTE, cursor="hand2")
self.box.pack(side="left")
self.label = tk.Label(self, text=text, font=("Microsoft YaHei", 9),
bg=master["bg"], fg=TEXT_SEC, cursor="hand2")
self.label.pack(side="left", padx=(4, 0))
self.box.bind("<Button-1>", self._toggle)
self.label.bind("<Button-1>", self._toggle)
self._update_display()
def _toggle(self, event=None):
self.var.set(not self.var.get())
self._update_display()
def _update_display(self):
if self.var.get():
self.box.config(text="☑", fg=PRIMARY)
self.label.config(fg=PRIMARY)
else:
self.box.config(text="☐", fg=TEXT_MUTE)
self.label.config(fg=TEXT_SEC)
def get(self):
return self.var.get()
def set(self, value):
self.var.set(value)
self._update_display()
# ═══════════════════════════════════════════════
# 版本更新对话框
# ═══════════════════════════════════════════════
class UpdateDialog(tk.Toplevel):
def __init__(self, master, latest_version, tip, update_url="",
on_skip=None, on_update=None):
super().__init__(master)
self.master_window = master
self.update_url = update_url
self.on_skip = on_skip
self.on_update = on_update
self.title("版本更新")
self.resizable(False, False)
self.configure(bg=BG)
self.transient(master)
self.grab_set()
w, h = 380, 270
sw, sh = self.winfo_screenwidth(), self.winfo_screenheight()
self.geometry(f"{w}x{h}+{(sw-w)//2}+{(sh-h)//2}")
# 顶部紫色标题区
header = tk.Frame(self, bg=PRIMARY, height=70)
header.pack(fill="x")
header.pack_propagate(False)
# 图标 + 标题
head_inner = tk.Frame(header, bg=PRIMARY)
head_inner.pack(expand=True)
icon_canvas = tk.Canvas(head_inner, width=36, height=36, bg=PRIMARY, highlightthickness=0)
icon_canvas.pack(side="left", padx=(0, 10))
icon_canvas.create_oval(2, 2, 34, 34, fill="white", outline="")
icon_canvas.create_text(18, 20, text="ℹ", font=("Segoe UI", 16, "bold"), fill=PRIMARY)
title_col = tk.Frame(head_inner, bg=PRIMARY)
title_col.pack(side="left")
tk.Label(title_col, text=f"检测到新版本 v{latest_version}",
font=("Microsoft YaHei", 12, "bold"),
fg="white", bg=PRIMARY).pack(anchor="w")
tk.Label(title_col, text=tip, font=("Microsoft YaHei", 8),
fg="#c7d2fe", bg=PRIMARY).pack(anchor="w", pady=(1, 0))
# 白色卡片内容区
card = tk.Frame(self, bg="white")
card.pack(fill="both", expand=True, padx=16, pady=(0, 10))
content = tk.Frame(card, bg="white")
content.pack(fill="both", expand=True, padx=14, pady=10)
# 下载地址
if update_url:
tk.Label(content, text="更新包下载地址:",
font=("Microsoft YaHei", 8), fg=TEXT_SEC,
bg="white").pack(anchor="w", pady=(0, 4))
url_box = tk.Frame(content, bg="#f8fafc")
url_box.pack(fill="x", pady=(0, 10))
tk.Label(url_box, text=update_url,
font=("Consolas", 8), fg=PRIMARY,
bg="#f8fafc", wraplength=300, justify="left").pack(
anchor="w", padx=8, pady=6)
# 按钮区:同一行两个按钮
btn_row = tk.Frame(content, bg="white")
btn_row.pack(fill="x", pady=(4, 0))
# 立即更新(蓝色按钮)
update_btn = FancyButton(btn_row, text="立即更新", command=self._do_update,
width=130, height=36,
bg=PRIMARY, hover_bg=PRIMARY_DARK,
active_bg="#3730a3", radius=8,
font=("Microsoft YaHei", 9, "bold"))
update_btn.pack(side="left", fill="x", expand=True, padx=(0, 5))
# 先不更新(红色按钮)
skip_btn = FancyButton(btn_row, text="先不更新", command=self._do_skip,
width=130, height=36,
bg="#ef4444", fg="white",
hover_bg="#dc2626",
active_bg="#b91c1c",
radius=8,
font=("Microsoft YaHei", 9, "bold"))
skip_btn.pack(side="left", fill="x", expand=True, padx=(5, 0))
# 居中到父窗口
self.update_idletasks()
mx = master.winfo_rootx() + (master.winfo_width() - w) // 2
my = master.winfo_rooty() + (master.winfo_height() - h) // 2
self.geometry(f"+{mx}+{my}")
def _do_update(self):
"""立即更新:打开浏览器并关闭程序"""
if self.update_url:
import webbrowser
webbrowser.open(self.update_url)
self.destroy()
if self.on_update:
self.on_update()
def _do_skip(self):
"""先不更新:关闭弹窗,执行回调"""
self.destroy()
if self.on_skip:
self.on_skip()
# ═══════════════════════════════════════════════
# 解绑确认对话框
# ═══════════════════════════════════════════════
class UnbindDialog(tk.Toplevel):
def __init__(self, master, on_confirm):
super().__init__(master)
self.master_window = master
self.on_confirm = on_confirm
self.result = False
self.title("确认解绑")
self.resizable(False, False)
self.configure(bg=BG)
self.transient(master)
self.grab_set()
w, h = 340, 240
sw, sh = self.winfo_screenwidth(), self.winfo_screenheight()
self.geometry(f"{w}x{h}+{(sw-w)//2}+{(sh-h)//2}")
# 顶部橙色警告区
header = tk.Frame(self, bg=WARNING, height=70)
header.pack(fill="x")
header.pack_propagate(False)
head_inner = tk.Frame(header, bg=WARNING)
head_inner.pack(expand=True)
icon_canvas = tk.Canvas(head_inner, width=36, height=36, bg=WARNING, highlightthickness=0)
icon_canvas.pack(side="left", padx=(0, 10))
icon_canvas.create_oval(2, 2, 34, 34, fill="white", outline="")
icon_canvas.create_text(18, 20, text="!", font=("Segoe UI", 18, "bold"), fill=WARNING)
title_col = tk.Frame(head_inner, bg=WARNING)
title_col.pack(side="left")
tk.Label(title_col, text="确认解绑",
font=("Microsoft YaHei", 13, "bold"),
fg="white", bg=WARNING).pack(anchor="w")
tk.Label(title_col, text="解绑后需要重新激活",
font=("Microsoft YaHei", 8),
fg="#fef3c7", bg=WARNING).pack(anchor="w", pady=(1, 0))
# 白色卡片
card = tk.Frame(self, bg="white")
card.pack(fill="both", expand=True, padx=16, pady=(0, 10))
content = tk.Frame(card, bg="white")
content.pack(fill="both", expand=True, padx=14, pady=14)
tk.Label(content, text="确定要解绑当前设备吗?",
font=("Microsoft YaHei", 10, "bold"),
fg=TEXT, bg="white").pack(pady=(4, 4))
tk.Label(content, text="解绑后该卡密可在其他设备上激活",
font=("Microsoft YaHei", 8),
fg=TEXT_MUTE, bg="white").pack()
# 按钮区
btn_row = tk.Frame(content, bg="white")
btn_row.pack(fill="x", pady=(12, 0))
cancel_btn = FancyButton(btn_row, text="取消", command=self._on_cancel,
width=110, height=36,
bg="white", fg=TEXT_SEC,
hover_bg="#f1f5f9",
active_bg="#e2e8f0",
radius=8,
font=("Microsoft YaHei", 9, "bold"))
cancel_btn.pack(side="left", fill="x", expand=True, padx=(0, 5))
confirm_btn = FancyButton(btn_row, text="确认解绑", command=self._on_confirm,
width=110, height=36,
bg=WARNING, fg="white",
hover_bg="#d97706",
active_bg="#b45309",
radius=8,
font=("Microsoft YaHei", 9, "bold"))
confirm_btn.pack(side="left", fill="x", expand=True, padx=(5, 0))
# 居中
self.update_idletasks()
mx = master.winfo_rootx() + (master.winfo_width() - w) // 2
my = master.winfo_rooty() + (master.winfo_height() - h) // 2
self.geometry(f"+{mx}+{my}")
def _on_confirm(self):
self.result = True
if self.on_confirm:
self.on_confirm()
self.destroy()
def _on_cancel(self):
self.result = False
self.destroy()
# ═══════════════════════════════════════════════
# 精美消息弹窗(替换系统 messagebox)
# ═══════════════════════════════════════════════
class FancyDialog(tk.Toplevel):
"""精美弹窗,支持 info/success/warning/error/confirm 五种类型。"""
TYPES = {
"info": {"icon": "ℹ️", "color": PRIMARY, "bg_light": "#eef2ff"},
"success": {"icon": "✓", "color": SUCCESS, "bg_light": "#ecfdf5"},
"warning": {"icon": "⚠", "color": WARNING, "bg_light": "#fffbeb"},
"error": {"icon": "✕", "color": ERROR, "bg_light": "#fef2f2"},
"confirm": {"icon": "❓", "color": PRIMARY, "bg_light": "#eef2ff"},
}
def __init__(self, master, title="提示", message="", dialog_type="info",
on_ok=None, on_cancel=None, ok_text="确定", cancel_text="取消"):
super().__init__(master)
self.result = False
self.on_ok = on_ok
self.on_cancel = on_cancel
cfg = self.TYPES.get(dialog_type, self.TYPES["info"])
self.title(title)
self.resizable(False, False)
self.configure(bg="white")
self.transient(master)
self.grab_set()
# 计算消息行数,自适应高度
lines = message.count("\n") + 1
msg_height = max(60, lines * 26 + 20)
w, h = 340, 160 + msg_height - 60
sw, sh = self.winfo_screenwidth(), self.winfo_screenheight()
# 定位到父窗口中心
try:
mx = master.winfo_rootx() + (master.winfo_width() - w) // 2
my = master.winfo_rooty() + (master.winfo_height() - h) // 2
except:
mx, my = (sw - w) // 2, (sh - h) // 2
self.geometry(f"{w}x{h}+{mx}+{my}")
self._build_ui(title, message, cfg, dialog_type, ok_text, cancel_text)
def _build_ui(self, title, message, cfg, dialog_type, ok_text, cancel_text):
# 顶部色条
top_bar = tk.Frame(self, bg=cfg["color"], height=4)
top_bar.pack(fill="x")
# 标题栏
header = tk.Frame(self, bg="white")
header.pack(fill="x", padx=16, pady=(12, 4))
# 图标 + 标题
title_row = tk.Frame(header, bg="white")
title_row.pack(fill="x")
icon_canvas = tk.Canvas(title_row, width=28, height=28, bg="white", highlightthickness=0)
icon_canvas.pack(side="left", padx=(0, 8))
icon_canvas.create_oval(1, 1, 27, 27, fill=cfg["bg_light"], outline="")
icon_canvas.create_text(14, 15, text=cfg["icon"],
fill=cfg["color"], font=("Segoe UI Emoji", 11, "bold"))
tk.Label(title_row, text=title, font=("Microsoft YaHei", 12, "bold"),
fg=TEXT, bg="white").pack(side="left", pady=2)
# 消息内容区
content = tk.Frame(self, bg=cfg["bg_light"])
content.pack(fill="both", expand=True, padx=16, pady=(8, 12))
inner = tk.Frame(content, bg=cfg["bg_light"])
inner.pack(fill="both", expand=True, padx=14, pady=12)
tk.Label(inner, text=message, font=("Microsoft YaHei", 9),
fg=TEXT, bg=cfg["bg_light"],
wraplength=280, justify="left").pack(anchor="w")
# 按钮区
btn_frame = tk.Frame(self, bg="white")
btn_frame.pack(fill="x", padx=16, pady=(0, 14))
if dialog_type == "confirm":
cancel_btn = FancyButton(btn_frame, text=cancel_text, command=self._on_cancel,
width=120, height=36,
bg="#f1f5f9", fg=TEXT_SEC,
hover_bg="#e2e8f0",
active_bg="#cbd5e1",
shadow_color="#e2e8f0",
radius=8,
font=("Microsoft YaHei", 9, "bold"))
cancel_btn.pack(side="right", padx=(8, 0))
ok_btn = FancyButton(btn_frame, text=ok_text, command=self._on_ok,
width=120 if dialog_type == "confirm" else 140, height=36,
bg=cfg["color"], fg="white",
hover_bg=self._darken(cfg["color"], 15),
active_bg=self._darken(cfg["color"], 30),
shadow_color=cfg["bg_light"],
radius=8,
font=("Microsoft YaHei", 9, "bold"))
ok_btn.pack(side="right")
# 回车确定
self.bind("<Return>", lambda e: self._on_ok())
if dialog_type == "confirm":
self.bind("<Escape>", lambda e: self._on_cancel())
def _darken(self, hex_color, amount):
"""颜色变暗"""
if not hex_color.startswith("#") or len(hex_color) != 7:
return hex_color
try:
r = max(0, int(hex_color[1:3], 16) - amount)
g = max(0, int(hex_color[3:5], 16) - amount)
b = max(0, int(hex_color[5:7], 16) - amount)
return f"#{r:02x}{g:02x}{b:02x}"
except:
return hex_color
def _on_ok(self):
self.result = True
if self.on_ok:
self.on_ok()
self.destroy()
def _on_cancel(self):
self.result = False
if self.on_cancel:
self.on_cancel()
self.destroy()
def fancy_showinfo(master, title, message):
"""显示信息弹窗"""
dlg = FancyDialog(master, title=title, message=message, dialog_type="info")
master.wait_window(dlg)
def fancy_showerror(master, title, message):
"""显示错误弹窗"""
dlg = FancyDialog(master, title=title, message=message, dialog_type="error")
master.wait_window(dlg)
def fancy_showwarning(master, title, message):
"""显示警告弹窗"""
dlg = FancyDialog(master, title=title, message=message, dialog_type="warning")
master.wait_window(dlg)
def fancy_askyesno(master, title, message, on_yes=None, on_no=None):
"""确认弹窗,返回 True/False"""
result = {"value": False}
def _yes():
result["value"] = True
dlg = FancyDialog(master, title=title, message=message, dialog_type="confirm",
on_ok=_yes, ok_text="确定", cancel_text="取消")
master.wait_window(dlg)
return result["value"]
# ═══════════════════════════════════════════════
# 激活窗口
# ═══════════════════════════════════════════════
class AuthWindow(tk.Tk):
def __init__(self):
super().__init__()
self.machine_code = generate_machine_code()
self.key_code = ""
self.is_trial = False
self.expire_time = ""
self.heartbeat_running = False
self.heartbeat_thread = None
self.main_window = None
self.notice_text = "" # 软件公告内容
self.title("软件激活")
self.resizable(False, False)
self.configure(bg=BG)
w, h = 400, 520
sw, sh = self.winfo_screenwidth(), self.winfo_screenheight()
self.geometry(f"{w}x{h}+{(sw-w)//2}+{(sh-h)//2}")
self.config_data = load_config()
self._build_ui()
self.bind("<Return>", lambda e: self.do_auth())
# 启动版本检查(自动登录会在版本检查后决定是否执行)
self.after(300, self._check_version)
def _check_version(self):
"""检查版本更新 + 获取公告"""
has_update = False
# ── 获取软件公告 ──
try:
notice_data = api_get("/api/client/notice", {"app_key": APP_KEY})
if notice_data.get("status") == "success":
self.notice_text = notice_data.get("notice", "")
except:
pass
# ── 版本检查 ──
try:
data = api_get("/api/client/version", {
"app_key": APP_KEY,
"current_version": CURRENT_VERSION,
})
if data.get("status") == "success" and data.get("update_available"):
has_update = True
latest = data.get("latest_version", "")
tip = data.get("update_tip", "发现新版本")
url = data.get("update_url", "")
UpdateDialog(
self, latest, tip, url,
on_skip=self._try_auto_login,
on_update=self._quit_for_update,
)
except:
pass
# 没有更新,直接尝试自动登录
if not has_update:
self._try_auto_login()
def _try_auto_login(self):
"""尝试自动登录"""
if not self.config_data.get("auto_login"):
return
# 卡密自动登录
if self.config_data.get("key_code"):
self._auto_login()
return
# 试用自动登录:先验证试用是否还有效
if self.config_data.get("is_trial"):
self.status_label.config(text="正在验证试用状态...", fg=TEXT_SEC)
self._set_buttons_enabled(False)
def worker():
code, data = api_post("/api/client/trial/heartbeat", {
"app_key": APP_KEY,
"machine_code": self.machine_code,
})
self.after(0, lambda: self._handle_trial_auto_login(code, data))
threading.Thread(target=worker, daemon=True).start()
def _handle_trial_auto_login(self, code, data):
"""处理试用自动登录验证结果"""
if code == 200 and data.get("status") == "success":
# 试用有效,自动登录
self.is_trial = True
self.heartbeat_running = True
self.expire_time = data.get("expire_time", "")
self.destroy()
main = MainApp(self)
self.start_heartbeat(main)
main.mainloop()
else:
# 试用过期了,清除自动登录
save_config(key_code="", remember=False, auto_login=False, is_trial=False)
self._set_buttons_enabled(True)
self.status_label.config(text="试用已过期,请重新激活", fg=ERROR)
def _quit_for_update(self):
"""点击立即更新后关闭程序"""
self.destroy()
def _build_ui(self):
# ── 顶部紫色标题区 ──
header = tk.Frame(self, bg=PRIMARY, height=120)
header.pack(fill="x")
header.pack_propagate(False)
# Logo
logo_frame = tk.Frame(header, bg=PRIMARY)
logo_frame.pack(pady=(16, 4))
logo_bg = tk.Canvas(logo_frame, width=44, height=44, bg=PRIMARY, highlightthickness=0)
logo_bg.pack()
logo_bg.create_oval(2, 2, 42, 42, fill="white", outline="")
logo_bg.create_text(22, 24, text="🔐", font=("Segoe UI Emoji", 18))
tk.Label(header, text="欢迎使用", font=("Microsoft YaHei", 13, "bold"),
fg="white", bg=PRIMARY).pack()
tk.Label(header, text="请输入卡密以激活软件",
font=("Microsoft YaHei", 8), fg="#c7d2fe", bg=PRIMARY).pack(pady=(1, 0))
# ── 白色卡片 ──
card = tk.Frame(self, bg="white")
card.pack(fill="both", expand=True, padx=20, pady=(0, 8))
content = tk.Frame(card, bg="white")
content.pack(fill="both", expand=True, padx=18, pady=12)
# 标题
tk.Label(content, text="卡密激活", font=("Microsoft YaHei", 13, "bold"),
fg=TEXT, bg="white").pack(anchor="w")
tk.Label(content, text="输入您的卡密码开始使用", font=("Microsoft YaHei", 8),
fg=TEXT_MUTE, bg="white").pack(anchor="w", pady=(2, 10))
# 1. 卡密输入框(在上面)
self.key_entry = FancyEntry(content, placeholder="请输入卡密码", icon="🔑")
self.key_entry.pack(fill="x", pady=(0, 8))
if self.config_data.get("remember") and self.config_data.get("key_code"):
self.key_entry.set(self.config_data["key_code"])
# 2. 机器码(在下面)
mc_wrap = tk.Frame(content, bg="#f1f5f9")
mc_wrap.pack(fill="x", pady=(0, 10))
mc_inner = tk.Frame(mc_wrap, bg="#f1f5f9")
mc_inner.pack(fill="x", padx=10, pady=6)
tk.Label(mc_inner, text="💻", font=("Segoe UI Emoji", 10),
bg="#f1f5f9").pack(side="left", padx=(0, 6))
tk.Label(mc_inner, text="机器码:", font=("Microsoft YaHei", 8),
fg=TEXT_SEC, bg="#f1f5f9").pack(side="left")
tk.Label(mc_inner, text=self.machine_code[:18] + "...",
font=("Consolas", 9), fg=TEXT, bg="#f1f5f9").pack(side="left", padx=(2, 0))
copy_lbl = tk.Label(mc_inner, text="复制", font=("Microsoft YaHei", 8, "bold"),
fg=PRIMARY, bg="#f1f5f9", cursor="hand2")
copy_lbl.pack(side="right")
copy_lbl.bind("<Button-1>", lambda e: self._copy_machine_code())
# 记住卡密 / 自动登录
opt_row = tk.Frame(content, bg="white")
opt_row.pack(fill="x", pady=(0, 10))
self.remember_cb = FancyCheckbox(opt_row, text="记住卡密")
self.remember_cb.set(self.config_data.get("remember", False))
self.remember_cb.pack(side="left")
self.auto_cb = FancyCheckbox(opt_row, text="自动登录")
self.auto_cb.set(self.config_data.get("auto_login", False))
self.auto_cb.pack(side="right")
# 3. 立即激活 + 免费试用(一行两个按钮)
btn_row = tk.Frame(content, bg="white")
btn_row.pack(fill="x", pady=(0, 8))
self.auth_btn = FancyButton(btn_row, text="立即激活", command=self.do_auth,
width=130, height=38,
bg=PRIMARY, hover_bg=PRIMARY_DARK,
active_bg="#3730a3", radius=8,
icon="✨",
font=("Microsoft YaHei", 9, "bold"))
self.auth_btn.pack(side="left", fill="x", expand=True, padx=(0, 5))
self.trial_btn = FancyButton(btn_row, text="免费试用", command=self.do_trial,
width=130, height=38,
bg=PRIMARY, hover_bg=PRIMARY_DARK,
active_bg="#3730a3", radius=8,
icon="🎁",
font=("Microsoft YaHei", 9, "bold"))
self.trial_btn.pack(side="left", fill="x", expand=True, padx=(5, 0))
# 4. 充值卡密 + 自助解绑(一行两个按钮)
bottom_btn_frame = tk.Frame(content, bg="white")
bottom_btn_frame.pack(fill="x", pady=(0, 8))
self.recharge_btn = FancyButton(bottom_btn_frame, text="充值卡密", command=self.do_recharge,
width=130, height=38,
bg="#10b981", fg="white",
hover_bg="#059669",
active_bg="#047857",
shadow_color="#a7f3d0",
radius=8, icon="💰",
font=("Microsoft YaHei", 9, "bold"))
self.recharge_btn.pack(side="left", fill="x", expand=True, padx=(0, 5))
self.unbind_btn = FancyButton(bottom_btn_frame, text="自助解绑", command=self.do_unbind,
width=130, height=38,
bg="#f59e0b", fg="white",
hover_bg="#d97706",
active_bg="#b45309",
shadow_color="#fde68a",
radius=8, icon="🔓",
font=("Microsoft YaHei", 9, "bold"))
self.unbind_btn.pack(side="left", fill="x", expand=True, padx=(5, 0))
# 状态提示
self.status_label = tk.Label(content, text="", font=("Microsoft YaHei", 8),
fg=TEXT_MUTE, bg="white")
self.status_label.pack(pady=(4, 0))
# 5. 底部版本号
bottom = tk.Frame(self, bg=BG)
bottom.pack(fill="x", pady=(0, 8))
tk.Label(bottom, text=f"版本号:{CURRENT_VERSION}",
font=("Microsoft YaHei", 8), fg=TEXT_SEC, bg=BG).pack()
def _copy_machine_code(self):
self.clipboard_clear()
self.clipboard_append(self.machine_code)
self.status_label.config(text="✓ 机器码已复制到剪贴板", fg=SUCCESS)
def do_recharge(self):
"""充值卡密 - 获取充值链接并打开"""
self._set_buttons_enabled(False)
self.status_label.config(text="正在获取充值链接...", fg=TEXT_MUTE)
self.update()
def worker():
data = api_get("/api/client/recharge", {"app_key": APP_KEY})
self.after(0, lambda: self._on_recharge_result(data))
threading.Thread(target=worker, daemon=True).start()
def _on_recharge_result(self, data):
self._set_buttons_enabled(True)
if data.get("status") == "success":
url = data.get("recharge_url", "")
if url:
webbrowser.open(url)
self.status_label.config(text="✓ 已打开充值页面", fg=SUCCESS)
else:
self.status_label.config(text="⚠ 未获取到充值链接", fg=ERROR)
else:
msg = data.get("message", "获取失败")
self.status_label.config(text=f"❌ {msg}", fg=ERROR)
def do_unbind(self):
"""激活页的自助解绑"""
key = self.key_entry.get().strip()
if not key:
self.status_label.config(text="⚠ 请先输入卡密", fg=ERROR)
return
def confirm_callback():
self.status_label.config(text="正在解绑...", fg=TEXT_SEC)
self._set_buttons_enabled(False)
self.update()
def worker():
code, data = api_post("/api/client/unbind", {
"app_key": APP_KEY,
"key_code": key,
"machine_code": self.machine_code,
})
self.after(0, lambda: self._handle_unbind_result(code, data))
threading.Thread(target=worker, daemon=True).start()
UnbindDialog(self, confirm_callback)
def _handle_unbind_result(self, code, data):
self._set_buttons_enabled(True)
if code == 200 and data.get("status") == "success":
remaining = data.get("remaining_unbind", 0)
fancy_showinfo(self, "解绑成功", f"设备解绑成功!\n剩余解绑次数: {remaining}")
self.status_label.config(text="✓ 解绑成功", fg=SUCCESS)
else:
msg = data.get("message", "解绑失败")
self.status_label.config(text=f"✕ {msg}", fg=ERROR)
def _auto_login(self):
key = self.config_data.get("key_code", "")
if key:
# 演示模式:如果是演示卡密,直接模拟登录成功
if key == "DEMO_SHOW_UI":
self.is_trial = False
self.key_code = key
self.heartbeat_running = True
self.expire_time = "2026-10-12 23:59:59"
self.destroy()
main = MainApp(self)
self.start_heartbeat(main)
main.mainloop()
else:
self.key_entry.set(key)
self.do_auth()
def _set_buttons_enabled(self, enabled):
state = tk.NORMAL if enabled else tk.DISABLED
self.auth_btn.config(state=state)
self.trial_btn.config(state=state)
self.recharge_btn.config(state=state)
self.unbind_btn.config(state=state)
def do_auth(self):
key = self.key_entry.get().strip()
if not key:
self.status_label.config(text="⚠ 请输入卡密码", fg=ERROR)
return
self.status_label.config(text="正在验证卡密...", fg=TEXT_SEC)
self._set_buttons_enabled(False)
self.update()
def worker():
code, data = api_post("/api/client/auth", {
"app_key": APP_KEY,
"key_code": key,
"machine_code": self.machine_code,
})
self.after(0, lambda: self._handle_auth_result(code, data, key))
threading.Thread(target=worker, daemon=True).start()
def _handle_auth_result(self, code, data, key):
self._set_buttons_enabled(True)
if code == 200 and data.get("status") == "success":
self.key_code = key
self.is_trial = False
self.heartbeat_running = True
key_info = data.get("key_info", {})
self.expire_time = key_info.get("expire_time", "")
remember = self.remember_cb.get()
auto_login = self.auto_cb.get()
save_config(key_code=key if remember else "", remember=remember,
auto_login=auto_login, is_trial=False)
self.status_label.config(text="✓ 激活成功,正在进入...", fg=SUCCESS)
self.update()
time.sleep(0.5)
self.destroy()
main = MainApp(self)
self.start_heartbeat(main)
main.mainloop()
else:
msg = data.get("message", "验证失败")
self.status_label.config(text=f"✕ {msg}", fg=ERROR)
def do_trial(self):
self.status_label.config(text="正在申请试用...", fg=TEXT_SEC)
self._set_buttons_enabled(False)
self.update()
def worker():
code, data = api_post("/api/client/trial", {
"app_key": APP_KEY,
"machine_code": self.machine_code,
})
self.after(0, lambda: self._handle_trial_result(code, data))
threading.Thread(target=worker, daemon=True).start()
def _handle_trial_result(self, code, data):
self._set_buttons_enabled(True)
if code == 200 and data.get("status") == "success":
self.is_trial = True
self.heartbeat_running = True
self.expire_time = data.get("expire_time", "")
# 保存自动登录状态
remember = self.remember_cb.get()
auto = self.auto_cb.get()
save_config(key_code="", remember=remember, auto_login=auto, is_trial=True)
self.status_label.config(text="✓ 试用开启成功", fg=SUCCESS)
self.update()
time.sleep(0.5)
self.destroy()
main = MainApp(self)
self.start_heartbeat(main)
main.mainloop()
else:
msg = data.get("message", "试用不可用")
self.status_label.config(text=f"✕ {msg}", fg=ERROR)
def start_heartbeat(self, main_window=None):
self.main_window = main_window
def _loop():
while self.heartbeat_running:
time.sleep(HEARTBEAT_INTERVAL)
if not self.heartbeat_running:
break
try:
if self.is_trial:
code, data = api_post("/api/client/trial/heartbeat", {
"app_key": APP_KEY, "machine_code": self.machine_code,
})
else:
code, data = api_post("/api/client/heartbeat", {
"app_key": APP_KEY, "key_code": self.key_code,
"machine_code": self.machine_code,
})
if code == 200 and data.get("status") == "success":
# 心跳成功,更新到期时间
new_expire = data.get("expire_time", "")
if new_expire:
self.expire_time = new_expire
if self.main_window:
try:
self.main_window.after(0, lambda: self.main_window.update_expire_time(new_expire))
except:
pass
else:
self.heartbeat_running = False
msg = data.get("message", "授权已失效")
if self.main_window:
try:
self.main_window.after(0, lambda: self.main_window.on_auth_lost(msg))
except:
pass
break
except:
pass
self.heartbeat_thread = threading.Thread(target=_loop, daemon=True)
self.heartbeat_thread.start()
# ═══════════════════════════════════════════════
# 主程序窗口
# ═══════════════════════════════════════════════
class MainApp(tk.Tk):
def __init__(self, auth):
super().__init__()
self.auth = auth
self.title("软件主程序")
self.resizable(False, False)
self.configure(bg=BG)
w, h = 440, 640
sw, sh = self.winfo_screenwidth(), self.winfo_screenheight()
self.geometry(f"{w}x{h}+{(sw-w)//2}+{(sh-h)//2}")
self._build_ui()
self.protocol("WM_DELETE_WINDOW", self.on_close)
# 加载公告
self._load_notice()
def _build_ui(self):
# ── 顶部渐变紫色标题区 ──
header = tk.Frame(self, bg=PRIMARY, height=140)
header.pack(fill="x")
header.pack_propagate(False)
# 装饰性光晕
deco = tk.Canvas(header, width=440, height=140, bg=PRIMARY, highlightthickness=0)
deco.place(x=0, y=0)
deco.create_oval(350, -30, 480, 80, fill=PRIMARY_LIGHT, outline="")
deco.create_oval(-40, 60, 80, 160, fill=PRIMARY_DARK, outline="")
# Logo
logo_frame = tk.Frame(header, bg=PRIMARY)
logo_frame.pack(pady=(18, 4))
logo_bg = tk.Canvas(logo_frame, width=56, height=56, bg=PRIMARY, highlightthickness=0)
logo_bg.pack()
logo_bg.create_oval(2, 2, 54, 54, fill="white", outline="")
logo_bg.create_text(28, 30, text="🚀", font=("Segoe UI Emoji", 22))
tk.Label(header, text="软件主程序", font=("Microsoft YaHei", 15, "bold"),
fg="white", bg=PRIMARY).pack()
tk.Label(header, text="TG炒群机器人 · 智能高效",
font=("Microsoft YaHei", 8), fg="#c7d2fe", bg=PRIMARY).pack(pady=(2, 0))
# ── 白色卡片:授权信息 ──
card = tk.Frame(self, bg="white")
card.pack(fill="both", expand=True, padx=20, pady=(0, 10))
# 状态徽章(悬浮在卡片顶部)
mode_text = "试用模式" if self.auth.is_trial else "已激活"
mode_color = "#8b5cf6" if self.auth.is_trial else SUCCESS
mode_bg = "#f5f3ff" if self.auth.is_trial else "#ecfdf5"
badge_wrap = tk.Frame(card, bg="white")
badge_wrap.pack(fill="x", pady=(14, 8))
badge = tk.Frame(badge_wrap, bg=mode_bg)
badge.pack(anchor="center")
tk.Label(badge, text=f"● {mode_text}", font=("Microsoft YaHei", 9, "bold"),
fg=mode_color, bg=mode_bg).pack(padx=20, pady=5)
content = tk.Frame(card, bg="white")
content.pack(fill="both", expand=True, padx=22, pady=(0, 14))
# 公告区域
notice_wrap = tk.Frame(content, bg="#f5f3ff")
notice_wrap.pack(fill="x", pady=(0, 16))
# 左侧紫色装饰条
tk.Frame(notice_wrap, bg=PRIMARY, width=4).pack(side="left", fill="y")
notice_inner = tk.Frame(notice_wrap, bg="#f5f3ff")
notice_inner.pack(side="left", fill="both", expand=True)
notice_content = tk.Frame(notice_inner, bg="#f5f3ff")
notice_content.pack(fill="x", padx=12, pady=10)
# 顶部:图标 + 标题
notice_header = tk.Frame(notice_content, bg="#f5f3ff")
notice_header.pack(fill="x", pady=(0, 6))
# 图标背景圆
icon_canvas = tk.Canvas(notice_header, width=26, height=26, bg="#f5f3ff", highlightthickness=0)
icon_canvas.pack(side="left", padx=(0, 8))
icon_canvas.create_oval(1, 1, 25, 25, fill=PRIMARY, outline="")
icon_canvas.create_text(13, 15, text="📢", font=("Segoe UI Emoji", 11))
tk.Label(notice_header, text="信息公告", font=("Microsoft YaHei", 10, "bold"),
fg=PRIMARY, bg="#f5f3ff").pack(side="left")
tk.Label(notice_header, text="OFFICIAL",
font=("Consolas", 7, "bold"),
fg="#a5b4fc", bg="#f5f3ff").pack(side="right")
# 公告内容
self.notice_label = tk.Label(notice_content,
text="加载中...",
font=("Microsoft YaHei", 9),
fg="#4c1d95", bg="#f5f3ff",
wraplength=300, justify="left")
self.notice_label.pack(fill="x")
# 标题:授权信息
info_header = tk.Frame(content, bg="white")
info_header.pack(fill="x", pady=(0, 10))
tk.Label(info_header, text="授权信息", font=("Microsoft YaHei", 13, "bold"),
fg=TEXT, bg="white").pack(side="left")
tk.Label(info_header, text="AUTH INFO",
font=("Consolas", 7, "bold"),
fg=TEXT_MUTE, bg="white").pack(side="right", pady=(6, 0))
# 信息行 - 卡片式
self._build_info_card(content, "机器码", self.auth.machine_code[:22] + "...",
TEXT, copy_btn=True, copy_text=self.auth.machine_code, icon="💻")
self._build_info_card(content, "卡密信息",
"免费试用" if self.auth.is_trial else (self.auth.key_code[:14] + "..."),
PRIMARY if not self.auth.is_trial else "#8b5cf6", icon="🔑")
self.expire_label = self._build_info_card(
content, "到期时间", format_expire_time(self.auth.expire_time), WARNING, icon="⏰")
# 分割线
tk.Frame(content, bg=BORDER, height=1).pack(fill="x", pady=14)
# 卡密加时区域(仅已激活用户显示)
if not self.auth.is_trial:
recharge_section = tk.Frame(content, bg="#f0fdf4")
recharge_section.pack(fill="x", pady=(0, 12))
tk.Frame(recharge_section, bg=SUCCESS, width=4).pack(side="left", fill="y")
recharge_inner = tk.Frame(recharge_section, bg="#f0fdf4")
recharge_inner.pack(side="left", fill="both", expand=True, padx=12, pady=10)
recharge_title = tk.Frame(recharge_inner, bg="#f0fdf4")
recharge_title.pack(fill="x", pady=(0, 8))
tk.Label(recharge_title, text="⏳ 卡密加时",
font=("Microsoft YaHei", 10, "bold"),
fg=SUCCESS, bg="#f0fdf4").pack(side="left")
tk.Label(recharge_title, text="叠加时长",
font=("Consolas", 7, "bold"),
fg="#6ee7b7", bg="#f0fdf4").pack(side="right", pady=(3, 0))
recharge_row = tk.Frame(recharge_inner, bg="#f0fdf4")
recharge_row.pack(fill="x")
self.recharge_entry = FancyEntry(recharge_row, placeholder="输入新卡密叠加时长", icon="🎫")
self.recharge_entry.pack(side="left", fill="x", expand=True, padx=(0, 8))
self.recharge_btn = FancyButton(recharge_row, text="加时", command=self.do_recharge_key,
width=70, height=36,
bg=SUCCESS, fg="white",
hover_bg="#059669",
active_bg="#047857",
shadow_color="#a7f3d0",
radius=8,
font=("Microsoft YaHei", 9, "bold"))
self.recharge_btn.pack(side="right")
# 操作按钮区
btn_row = tk.Frame(content, bg="white")
btn_row.pack(fill="x")
if not self.auth.is_trial:
unbind_btn = FancyButton(btn_row, text="解绑机器码", command=self.do_unbind,
width=140, height=40,
bg="white", fg=WARNING,
hover_bg="#fffbeb",
active_bg="#fef3c7",
shadow_color="#fef3c7",
radius=8, icon="🔓",
font=("Microsoft YaHei", 9, "bold"))
unbind_btn.pack(side="left", fill="x", expand=True, padx=(0, 6))
logout_btn = FancyButton(btn_row, text="退出登录", command=self.do_logout,
width=140, height=40,
bg="white", fg=ERROR,
hover_bg="#fef2f2",
active_bg="#fee2e2",
shadow_color="#fee2e2",
radius=8, icon="🚪",
font=("Microsoft YaHei", 9, "bold"))
logout_btn.pack(side="left", fill="x", expand=True, padx=(6, 0))
else:
tk.Label(content, text="🎁 试用模式不支持设备解绑",
font=("Microsoft YaHei", 8), fg=TEXT_MUTE, bg="white").pack(pady=6)
# ── 底部版本号 ──
bottom = tk.Frame(self, bg=BG)
bottom.pack(fill="x", pady=(0, 10))
tk.Label(bottom, text=f"版本号:{CURRENT_VERSION}",
font=("Microsoft YaHei", 8), fg=TEXT_SEC, bg=BG).pack()
def _build_info_card(self, parent, label, value, value_color, copy_btn=False, copy_text="", icon=""):
"""创建一个卡片式信息行,返回值标签引用"""
card = tk.Frame(parent, bg="#f8fafc", highlightbackground=BORDER, highlightthickness=1)
card.pack(fill="x", pady=5)
inner = tk.Frame(card, bg="#f8fafc")
inner.pack(fill="x", padx=12, pady=9)
# 图标
if icon:
tk.Label(inner, text=icon, font=("Segoe UI Emoji", 12),
bg="#f8fafc").pack(side="left", padx=(0, 8))
tk.Label(inner, text=label, font=("Microsoft YaHei", 8),
fg=TEXT_MUTE, bg="#f8fafc").pack(side="left")
value_label = tk.Label(inner, text=value, font=("Consolas", 9, "bold"),
fg=value_color, bg="#f8fafc")
value_label.pack(side="right")
if copy_btn:
copy_lbl = tk.Label(inner, text="复制", font=("Microsoft YaHei", 8, "bold"),
fg=PRIMARY, bg="#f8fafc", cursor="hand2")
copy_lbl.pack(side="right", padx=(0, 8))
copy_lbl.bind("<Button-1>", lambda e: self._copy_to_clipboard(copy_text))
return value_label
def _copy_to_clipboard(self, text):
self.clipboard_clear()
self.clipboard_append(text)
def do_recharge_key(self):
"""卡密加时 - 用新卡密叠加时长"""
new_key = self.recharge_entry.get().strip()
if not new_key:
fancy_showwarning(self, "提示", "请输入要叠加的新卡密")
return
self.recharge_btn.config(state=tk.DISABLED)
def worker():
code, data = api_post("/api/client/recharge-key", {
"app_key": APP_KEY,
"key_code": self.auth.key_code,
"recharge_key": new_key,
"machine_code": self.auth.machine_code,
})
self.after(0, lambda: self._on_recharge_key_result(code, data))
threading.Thread(target=worker, daemon=True).start()
def _on_recharge_key_result(self, code, data):
self.recharge_btn.config(state=tk.NORMAL)
if code == 200 and data.get("status") == "success":
new_expire = data.get("new_expire_time", "")
added_days = data.get("added_days", 0)
# 更新显示
self.expire_label.config(text=format_expire_time(new_expire))
# 同步到 auth 对象
self.auth.expire_time = new_expire
fancy_showinfo(self, "加时成功",
f"时长已叠加成功!\n增加:{added_days} 天\n新到期时间:{format_expire_time(new_expire)}")
self.recharge_entry.set("")
else:
msg = data.get("message", "加时失败")
fancy_showerror(self, "加时失败", msg)
def _load_notice(self):
"""加载公告"""
def worker():
try:
data = api_get("/api/client/notice", {"app_key": APP_KEY})
if data.get("status") == "success":
notice = data.get("notice", "")
self.after(0, lambda: self._update_notice(notice))
except:
pass
threading.Thread(target=worker, daemon=True).start()
def _update_notice(self, notice):
"""更新公告显示"""
if hasattr(self, 'notice_label') and notice:
self.notice_label.config(text=notice)
def update_expire_time(self, expire_time):
"""更新到期时间显示"""
if hasattr(self, 'expire_label'):
self.expire_label.config(text=format_expire_time(expire_time))
def do_unbind(self):
def confirm_callback():
def worker():
code, data = api_post("/api/client/unbind", {
"app_key": APP_KEY, "key_code": self.auth.key_code,
"machine_code": self.auth.machine_code,
})
self.after(0, lambda: self._handle_unbind_result(code, data))
threading.Thread(target=worker, daemon=True).start()
UnbindDialog(self, confirm_callback)
def _handle_unbind_result(self, code, data):
if code == 200 and data.get("status") == "success":
remaining = data.get("remaining_unbind", 0)
fancy_showinfo(self, "解绑成功", f"设备解绑成功!\n剩余解绑次数: {remaining}")
# 解绑成功后清除记住的卡密和自动登录
save_config(key_code="", remember=False, auto_login=False)
self.auth.heartbeat_running = False
self.destroy()
app = AuthWindow()
app.mainloop()
else:
msg = data.get("message", "解绑失败")
fancy_showerror(self, "解绑失败", msg)
def do_logout(self):
if not fancy_askyesno(self, "确认", "确定要退出登录吗?"):
return
save_config(key_code="", remember=False, auto_login=False)
self.auth.heartbeat_running = False
self.destroy()
app = AuthWindow()
app.mainloop()
def on_close(self):
self.auth.heartbeat_running = False
self.destroy()
def on_auth_lost(self, message="授权已失效"):
self.auth.heartbeat_running = False
fancy_showwarning(self, "授权失效", f"{message}\n\n软件将退出,请重新激活。")
save_config(key_code="", remember=False, auto_login=False)
self.destroy()
app = AuthWindow()
app.mainloop()
# ═══════════════════════════════════════════════
# 启动
# ═══════════════════════════════════════════════
if __name__ == "__main__":
try:
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(1)
except:
pass
app = AuthWindow()
app.mainloop()
14
其他语言快速示例
以下是 Go、Node.js、Java 的卡密认证快速调用示例:
Go
Node.js
Java
curl
PHP
Go
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
)
func main() {
serverURL := "http://cdk.bcwlgzs.top"
// 卡密认证
body, _ := json.Marshal(map[string]string{
"app_key": "你的应用密钥",
"key_code": "用户输入的卡密",
"machine_code": "AABBCCDDEE112233",
})
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Post(serverURL+"/api/client/auth",
"application/json", bytes.NewReader(body))
if err != nil {
fmt.Println("网络错误:", err)
return
}
defer resp.Body.Close()
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
if result["status"] == "success" {
keyInfo := result["key_info"].(map[string]interface{})
fmt.Printf("认证成功!到期: %s\n", keyInfo["expire_time"])
} else {
fmt.Printf("认证失败: %s\n", result["message"])
}
}
15
错误码参考
所有接口的错误响应统一格式:
{
"status": "error",
"message": "具体错误描述"
}
| HTTP状态码 | message | 触发场景 |
|---|---|---|
| 400 | 缺少必要参数:{field} | 必填参数未传 |
| 400 | 卡密与应用不匹配 | 卡密不属于该应用 |
| 403 | 卡密已过期,请去官方重新购买 | 超过卡密有效期 |
| 403 | 卡密已被管理员禁止使用 | 管理员在后台禁用了卡密 |
| 403 | 设备数量已达上限(N台) | 绑定设备超过卡密限制 |
| 403 | 试用已到期,请输入卡密继续使用 | 免费试用过期 |
| 403 | 该应用未开启免费试用 | 应用未开启试用功能 |
| 403 | 设备未授权,请先认证 | 心跳时设备未绑定 |
| 403 | 该应用未开启自助解绑功能 | 调用解绑接口但应用未开启 |
| 403 | 30天内解绑次数已达上限(N次) | 超过30天解绑次数限制 |
| 404 | 设备未绑定 | 解绑的设备不存在绑定关系 |
| 404 | 应用密钥无效 | app_key 不存在 |
| 404 | 卡密不存在 | key_code 不存在 |
| 429 | 请求过于频繁,请稍后再试 | 超过频率限制 |
16
注意事项与最佳实践
1.
卡密首次使用自动激活,有效期从激活时刻开始计算,非从创建时间算起。
2.
心跳间隔建议 5 分钟,过短会增加服务器压力,过长可能导致在线状态判断不准确。
3.
心跳返回 403 时,应立即停止软件运行,提示用户卡密已过期或被禁用。
4.
同一设备多次认证不增加设备计数,仅更新心跳时间。
5.
所有时间字段均为北京时间(UTC+8)。
6.
频率限制:认证 10次/分钟,试用 5次/分钟,心跳 30次/分钟,解绑 5次/分钟。
7.
建议客户端对网络请求做超时处理(10秒),避免网络异常导致界面卡死。
8.
机器码应持久化保存,避免每次启动都不同。建议组合多个硬件特征值取 MD5。
9.
自助解绑功能需应用管理员在后台开启,每张卡密30天内有次数限制(默认3次)。
10.
设备绑定限制以卡密为单位,同一卡密可绑定的设备数由卡密的 device_limit 字段决定。