diff --git a/shopxo/app/plugins/vr_ticket/service/BaseService.php b/shopxo/app/plugins/vr_ticket/service/BaseService.php index 36dc1e3..509a914 100644 --- a/shopxo/app/plugins/vr_ticket/service/BaseService.php +++ b/shopxo/app/plugins/vr_ticket/service/BaseService.php @@ -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 防篡改) diff --git a/tests/phase4_1_feistel_test.php b/tests/phase4_1_feistel_test.php index 33ac448..4b13768 100644 --- a/tests/phase4_1_feistel_test.php +++ b/tests/phase4_1_feistel_test.php @@ -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";