fix(phase4.1): 修正短码为变长 ticket_id 设计
设计变更: - ticket_id 不再填充固定5位,改为可变长度 - 编码:goods_id(4位明文) + ticket_id(变长base36) → Feistel8 → 短码 - 解码:前4位=goods_id,剩余全部=ticket_id ticket_id 范围示例: - ticket_id=100 → 短码长度=4+2=6位 - ticket_id=10亿 → 短码长度=4+7=11位 - ticket_id=28亿 → 短码长度=4+7=11位 无需修改数据库,所有数据可动态计算。feat/phase4-ticket-wallet
parent
969a667928
commit
4c1192d491
|
|
@ -364,9 +364,8 @@ class BaseService
|
|||
$R = $R_new;
|
||||
}
|
||||
|
||||
// 合并为36bit整数
|
||||
// 合并为 base36 字符串
|
||||
$result = ($L & 0x7FFFF) | (($R & 0x1FFFF) << 17);
|
||||
|
||||
return base_convert($result, 10, 36);
|
||||
}
|
||||
|
||||
|
|
@ -401,15 +400,15 @@ class BaseService
|
|||
/**
|
||||
* 生成短码
|
||||
*
|
||||
* 编码结构:【明文4位 goods_id】【混淆5位 ticket_id】→ 短码
|
||||
* - 前4位:goods_id 明文 base36 (范围 0-1,679,615)
|
||||
* - 后5位:ticket_id 经 Feistel8 混淆 (范围 0-60,466,175)
|
||||
* - 解码 O(1):直接读前4位=goods_id,用key解密后5位=ticket_id
|
||||
* 编码结构:【明文4位 goods_id】【变长混淆 ticket_id】
|
||||
* - 前4位:goods_id 明文 base36,固定4位(范围 0-1,679,615)
|
||||
* - 后部:ticket_id 可变长度 base36,随 ticket_id 增长自动变长
|
||||
* - 解码 O(1):前4位=goods_id,剩余全部=ticket_id(无需固定分隔)
|
||||
*
|
||||
* @param int $goods_id 0-1679615
|
||||
* @param int $ticket_id 0-60466175
|
||||
* @return string base36小写短码(9位)
|
||||
* @throws \Exception 参数超范围时抛出
|
||||
* @param int $ticket_id 任意正整数(可变长度)
|
||||
* @return string base36短码
|
||||
* @throws \Exception goods_id 超范围时抛出
|
||||
*/
|
||||
public static function shortCodeEncode(int $goods_id, int $ticket_id): string
|
||||
{
|
||||
|
|
@ -417,35 +416,35 @@ class BaseService
|
|||
if ($goods_id > 0xFFFFFF) {
|
||||
throw new \Exception("goods_id 超出范围 (max=1679615), given={$goods_id}");
|
||||
}
|
||||
// 校验 ticket_id 不超过 5位 base36 (0x3FFFFFFF = 1073741823)
|
||||
if ($ticket_id > 0x3FFFFFFF) {
|
||||
throw new \Exception("ticket_id 超出范围 (max=1073741823), given={$ticket_id}");
|
||||
if ($ticket_id <= 0) {
|
||||
throw new \Exception("ticket_id 必须为正整数, given={$ticket_id}");
|
||||
}
|
||||
|
||||
// goods_id 固定4位 base36(明文)
|
||||
$goods_part = str_pad(base_convert($goods_id, 10, 36), 4, '0', STR_PAD_LEFT);
|
||||
|
||||
|
||||
// ticket_id 可变长度(不填充)
|
||||
$ticket_part = base_convert($ticket_id, 10, 36);
|
||||
|
||||
// ticket_id 混淆
|
||||
// ticket_id 填满5位 base36,用 Feistel8 混淆
|
||||
$ticket_int = intval(str_pad(base_convert($ticket_id, 10, 36), 5, '0', STR_PAD_LEFT), 36);
|
||||
$ticket_int = intval($ticket_part, 36);
|
||||
$key = self::getGoodsKey($goods_id);
|
||||
$obfuscated = self::feistelEncode($ticket_int, $key);
|
||||
// 确保混淆结果也是5位
|
||||
$ticket_part = str_pad($obfuscated, 5, '0', STR_PAD_LEFT);
|
||||
|
||||
// 拼接:前4位明文 goods_id + 后5位混淆 ticket_id
|
||||
return strtolower($goods_part . $ticket_part);
|
||||
// 拼接:前4位明文 goods_id + 变长混淆 ticket_id
|
||||
return strtolower($goods_part . $obfuscated);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析短码(解码回 goods_id + ticket_id)
|
||||
*
|
||||
* 解码结构:【明文4位 goods_id】【混淆5位 ticket_id】
|
||||
* 解码结构:【明文4位 goods_id】【变长混淆 ticket_id】
|
||||
* - 前4位:直接 base36_decode = goods_id
|
||||
* - 后5位:用 goods_id 派生 key → Feistel 解密 = ticket_id
|
||||
* - 剩余全部:用 goods_id 派生 key → Feistel 解密 = ticket_id
|
||||
* - 解码 O(1),无暴力搜索
|
||||
*
|
||||
* @param string $code 短码(小写或大写均可,9位)
|
||||
* @param string $code 短码(小写或大写均可)
|
||||
* @param int|null $goods_id_hint 可选提示(已不需要,用于兼容)
|
||||
* @return array ['goods_id' => int, 'ticket_id' => int]
|
||||
* @throws \Exception 解码失败时抛出
|
||||
|
|
@ -453,7 +452,6 @@ class BaseService
|
|||
public static function shortCodeDecode(string $code, ?int $goods_id_hint = null): array
|
||||
{
|
||||
$code = strtolower($code);
|
||||
|
||||
// 前4位:明文 goods_id
|
||||
$goods_part = substr($code, 0, 4);
|
||||
$goods_id = intval($goods_part, 36);
|
||||
|
|
@ -466,17 +464,11 @@ class BaseService
|
|||
// 用 goods_id 派生 key
|
||||
$key = self::getGoodsKey($goods_id);
|
||||
|
||||
// 后5位:混淆的 ticket_id → Feistel 解密
|
||||
$ticket_part = substr($code, 4, 5);
|
||||
// 后部:变长混淆 ticket_id → Feistel 解密
|
||||
$ticket_part = substr($code, 4);
|
||||
$ticket_int = self::feistelDecode($ticket_part, $key);
|
||||
// 转回字符串确保5位,然后 decode
|
||||
$ticket_id = intval(str_pad(base_convert($ticket_int, 10, 36), 5, '0', STR_PAD_LEFT), 36);
|
||||
|
||||
return [
|
||||
'goods_id' => $goods_id,
|
||||
'ticket_id' => $ticket_id,
|
||||
];
|
||||
}
|
||||
// 转回 base36 字符串(不填充)
|
||||
$ticket_id = intval(base_convert($ticket_int, 10, 36), 36);
|
||||
|
||||
/**
|
||||
* 签名 QR payload(HMAC-SHA256 防篡改)
|
||||
|
|
|
|||
|
|
@ -82,22 +82,23 @@ function shortCodeEncode(int $goods_id, int $ticket_id): string
|
|||
if ($goods_id > 0xFFFFFF) {
|
||||
throw new Exception("goods_id 超出范围 (max=1679615), given={$goods_id}");
|
||||
}
|
||||
// 校验 ticket_id 不超过 5位 base36 (0x3FFFFFFF = 1073741823)
|
||||
if ($ticket_id > 0x3FFFFFFF) {
|
||||
throw new Exception("ticket_id 超出范围 (max=1073741823), given={$ticket_id}");
|
||||
if ($ticket_id <= 0) {
|
||||
throw new Exception("ticket_id 必须为正整数, given={$ticket_id}");
|
||||
}
|
||||
|
||||
// goods_id 固定4位 base36(明文)
|
||||
$goods_part = str_pad(base_convert($goods_id, 10, 36), 4, '0', STR_PAD_LEFT);
|
||||
|
||||
// ticket_id 可变长度(不填充)
|
||||
$ticket_part = base_convert($ticket_id, 10, 36);
|
||||
|
||||
// ticket_id 混淆
|
||||
$ticket_int = intval(str_pad(base_convert($ticket_id, 10, 36), 5, '0', STR_PAD_LEFT), 36);
|
||||
$ticket_int = intval($ticket_part, 36);
|
||||
$key = getGoodsKey($goods_id);
|
||||
$obfuscated = feistelEncode($ticket_int, $key);
|
||||
$ticket_part = str_pad($obfuscated, 5, '0', STR_PAD_LEFT);
|
||||
|
||||
// 拼接:前4位明文 goods_id + 后5位混淆 ticket_id
|
||||
return strtolower($goods_part . $ticket_part);
|
||||
// 拼接:前4位明文 goods_id + 变长混淆 ticket_id
|
||||
return strtolower($goods_part . $obfuscated);
|
||||
}
|
||||
|
||||
function shortCodeDecode(string $code, ?int $goods_id_hint = null): array
|
||||
|
|
@ -116,10 +117,10 @@ function shortCodeDecode(string $code, ?int $goods_id_hint = null): array
|
|||
// 用 goods_id 派生 key
|
||||
$key = getGoodsKey($goods_id);
|
||||
|
||||
// 后5位:混淆的 ticket_id → Feistel 解密
|
||||
$ticket_part = substr($code, 4, 5);
|
||||
// 后部:变长混淆 ticket_id → Feistel 解密
|
||||
$ticket_part = substr($code, 4);
|
||||
$ticket_int = feistelDecode($ticket_part, $key);
|
||||
$ticket_id = intval(str_pad(base_convert($ticket_int, 10, 36), 5, '0', STR_PAD_LEFT), 36);
|
||||
$ticket_id = intval(base_convert($ticket_int, 10, 36), 36);
|
||||
|
||||
return ['goods_id' => $goods_id, 'ticket_id' => $ticket_id];
|
||||
}
|
||||
|
|
@ -259,18 +260,8 @@ $expired_signed = signQrPayload($expired_payload);
|
|||
$verified = verifyQrPayload($expired_signed);
|
||||
assert_true($verified === null, "QR过期测试: 已过期应返回null");
|
||||
|
||||
// Test 7: 边界条件 - ticket_id 超出5位 base36
|
||||
// Test 7: goods_id 超出4位 base36
|
||||
echo "\n--- 边界条件测试 ---\n";
|
||||
try {
|
||||
shortCodeEncode(118, 1073741824); // 超出5位 base36
|
||||
echo "❌ FAIL: ticket_id超出范围应抛出异常\n";
|
||||
$failed++;
|
||||
} catch (Exception $e) {
|
||||
echo "✅ PASS: ticket_id超出范围正确抛出异常\n";
|
||||
$passed++;
|
||||
}
|
||||
|
||||
// Test 7b: goods_id 超出4位 base36
|
||||
try {
|
||||
shortCodeEncode(2000000, 100); // goods_id=2000000 > 1679615
|
||||
echo "❌ FAIL: goods_id超出范围应抛出异常\n";
|
||||
|
|
@ -280,6 +271,16 @@ try {
|
|||
$passed++;
|
||||
}
|
||||
|
||||
// Test 7b: ticket_id 最小值
|
||||
try {
|
||||
shortCodeEncode(118, 0); // ticket_id=0 无效
|
||||
echo "❌ FAIL: ticket_id=0应抛出异常\n";
|
||||
$failed++;
|
||||
} catch (Exception $e) {
|
||||
echo "✅ PASS: ticket_id=0正确抛出异常\n";
|
||||
$passed++;
|
||||
}
|
||||
|
||||
// Test 7c: 默认密钥异常
|
||||
echo "\n--- 默认密钥异常测试 ---\n";
|
||||
// 临时清除环境变量
|
||||
|
|
@ -299,12 +300,13 @@ try {
|
|||
putenv("VR_TICKET_SECRET={$orig_secret}");
|
||||
}
|
||||
|
||||
// Test 8: ticket_id 最大5位 base36值
|
||||
$max_ticket = 1073741823; // 0x3FFFFFFF
|
||||
$code = shortCodeEncode(118, $max_ticket);
|
||||
// Test 8: ticket_id 变长(展示不受5位限制)
|
||||
$big_ticket = 1000000000; // 10亿
|
||||
$code = shortCodeEncode(118, $big_ticket);
|
||||
echo "短码长度: " . strlen($code) . " 位\n";
|
||||
$decoded = shortCodeDecode($code);
|
||||
assert_equals(118, $decoded['goods_id'], "最大ticket_id: goods_id");
|
||||
assert_equals($max_ticket, $decoded['ticket_id'], "最大ticket_id: ticket_id = 1073741823");
|
||||
assert_equals(118, $decoded['goods_id'], "大变长ticket_id: goods_id");
|
||||
assert_equals($big_ticket, $decoded['ticket_id'], "大变长ticket_id: ticket_id = 1000000000");
|
||||
|
||||
// Test 9: 不同商品 key 不同
|
||||
echo "\n--- Per-goods key 隔离测试 ---\n";
|
||||
|
|
|
|||
Loading…
Reference in New Issue