开发指南
A
admin
! 开发指南总览

本指南将帮助你快速将客户端软件对接到卡密管理系统。系统提供 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_keystring应用密钥
current_versionstring客户端当前版本号,如 "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_keystring应用密钥
✅ 成功响应
{ "status": "success", "app_name": "测试应用", "app_key": "a1b2c3d4...", "notice": "欢迎使用本软件!\n如有问题请联系客服QQ:123456789\n\n【公告】9月15日凌晨2点系统维护,预计1小时。" }
说明:公告内容在后台「应用管理 → 编辑 → 软件公告」中设置,支持换行。
4 充值链接接口
GET /api/client/recharge 无需认证

获取充值购买链接,用户点击充值按钮时跳转到此链接进行购买续费。

参数类型必填说明
app_keystring应用密钥
✅ 成功响应
{ "status": "success", "app_name": "测试应用", "app_key": "a1b2c3d4...", "recharge_url": "https://example.com/buy" }
说明:充值链接在后台「应用管理 → 编辑 → 充值链接」中设置。
5 免费试用接口
POST /api/client/trial 无需认证

每台设备每个应用可免费试用一次,到期后需输入卡密继续使用。

参数类型必填说明
app_keystring应用密钥
machine_codestring设备机器码
✅ 试用有效 / 首次开启
{ "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_keystring应用密钥
key_codestring用户输入的卡密
machine_codestring设备机器码
ip_addressstring设备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 为已绑定设备数
7 心跳保活接口
POST /api/client/heartbeat 无需认证

认证成功后每 5 分钟调用一次,维持在线状态并检查卡密有效性。

参数类型必填说明
app_keystring应用密钥
key_codestring已认证的卡密
machine_codestring已绑定的设备机器码
✅ 心跳成功
{ "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_keystring应用密钥
machine_codestring试用设备机器码
✅ 心跳成功
{ "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_keystring应用密钥
key_codestring卡密码
machine_codestring要解绑的设备机器码
✅ 解绑成功
{ "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该应用未开启自助解绑功能调用解绑接口但应用未开启
40330天内解绑次数已达上限(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 字段决定。