云端的激活体系要回答三个问题:这个码存在吗、还能不能绑新设备、绑了给多少次。三个问题必须一起回答,否则就会出现「扣了名额没加次数」这类对不上账的情况。
激活码形态
CDC-AI-XXXXXXXX 前缀 CDC-AI- + 8 位随机段
随机段字符集:ABCDEFGHJKLMNPQRSTUVWXYZ23456789兑换的执行顺序
- 规范化输入:去空格、统一转大写
- 设备号为空直接拒绝,且不写任何绑定记录
- 查码是否存在 —— 不存在返回
invalid - 查这台设备是否已经绑过这个码 —— 绑过则幂等返回,次数不变
- 查这个码已绑定的设备数 —— 达到上限返回
used_out - 写入绑定记录,并把次数记到设备配额上
function redeem_code(string $code, string $device, string $user): array {
$code = strtoupper(trim($code));
if ($code === '') return [false, 0, 'invalid', 0];
// 设备号为空直接拒绝,且不写绑定记录(否则空设备会白占一个名额,把码耗掉)
if ($device === '') return [false, 0, 'nodevice', 0];
// 本设备已绑过这个码 → 幂等返回,不重复加次数
$dup = $pdo->prepare("SELECT 1 FROM code_bindings WHERE code = ? AND device_id = ?");
$dup->execute([$code, $device]);
if ($dup->fetch()) { [$left] = get_device_quota($device); return [true, 0, 'already', $left]; }
// 设备数上限:必须先 execute 才能取到 COUNT,否则恒为 0、上限形同虚设
$boundSt = $pdo->prepare("SELECT COUNT(*) FROM code_bindings WHERE code = ?");
$boundSt->execute([$code]);
if ((int)$boundSt->fetchColumn() >= CDC_MAX_DEVICES_PER_CODE)
return [false, 0, 'used_out', 0];
$granted = (int)$row['max_uses'];
$pdo->prepare("INSERT INTO code_bindings(code,device_id,username,bound_at,granted)
VALUES(?,?,?,?,?)")->execute([$code, $device, $user, time(), $granted]);
ensure_device_quota($device);
$pdo->prepare("UPDATE device_quota SET granted = granted + ? WHERE device_id = ?")
->execute([$granted, $device]);
[$left] = get_device_quota($device);
return [true, $granted, 'ok', $left];
}| 返回值 | 含义 | 界面文案 |
|---|---|---|
ok | 兑换成功,次数已到账 | 激活成功!本次 +N 次 |
already | 本机已激活过,幂等 | 该激活码在本机已经激活过,次数不变 |
used_out | 已绑满 3 台设备 | 该激活码已在 3 台电脑上使用,达到上限 |
invalid | 码不存在或写错 | 激活码无效或不存在(注意大小写与横杠) |
nodevice | 设备号取不到 | 本机设备号获取失败,无法绑定 |
设备号怎么来
设备号要稳定(同一台机器每次都一样)、要换机就变,还不能涉及隐私。做法是取计算机名 + 系统盘卷序列号,做一次 5381 哈希,输出 8 位十六进制:
static wstring LocalDeviceId() {
wchar_t name[MAX_COMPUTERNAME_LENGTH + 1] = {0};
DWORD n = MAX_COMPUTERNAME_LENGTH + 1;
if (!GetComputerNameW(name, &n)) name[0] = 0;
DWORD serial = 0;
wchar_t winDir[MAX_PATH] = {0};
if (GetWindowsDirectoryW(winDir, MAX_PATH)) {
wchar_t root[4] = { winDir[0], L':', L'\\', 0 };
GetVolumeInformationW(root, nullptr, 0, &serial, nullptr, nullptr, nullptr, 0);
}
wstring raw = wstring(name) + L"|" + to_wstring((unsigned long long)serial);
unsigned long long h = 5381;
for (wchar_t c : raw) h = ((h << 5) + h) + (unsigned)(unsigned short)c;
// → 8 位大写十六进制
}