diff --git a/shopxo/app/plugins/vr_ticket/service/BaseService.php b/shopxo/app/plugins/vr_ticket/service/BaseService.php index c65a946..b8bdf4b 100644 --- a/shopxo/app/plugins/vr_ticket/service/BaseService.php +++ b/shopxo/app/plugins/vr_ticket/service/BaseService.php @@ -399,14 +399,14 @@ class BaseService /** * 生成短码 * - * 编码结构:goods_id(4位base36) + ticket_id(5位base36) = 9位 → Feistel8 → 短码 - * - goods_id: 4位 base36 (范围 0-1,679,615,ShopXO 商品上限充足) - * - ticket_id: 5位 base36 (范围 0-60,466,175,每商品可发约6000万张票) - * - 解码 O(1): 直接取前4位=goods_id,后5位=ticket_id,无需暴力搜索 + * 编码结构:【明文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 * * @param int $goods_id 0-1679615 * @param int $ticket_id 0-60466175 - * @return string base36小写短码 + * @return string base36小写短码(9位) * @throws \Exception 参数超范围时抛出 */ public static function shortCodeEncode(int $goods_id, int $ticket_id): string @@ -420,29 +420,31 @@ class BaseService throw new \Exception("ticket_id 超出范围 (max=1073741823), given={$ticket_id}"); } - // goods_id 固定4位 base36 + // goods_id 固定4位 base36(明文) $goods_part = str_pad(base_convert($goods_id, 10, 36), 4, '0', STR_PAD_LEFT); - // ticket_id 固定5位 base36 - $ticket_part = str_pad(base_convert($ticket_id, 10, 36), 5, '0', STR_PAD_LEFT); - // 拼接为9位 base36 字符串,转为大整数 - // 使用 intval(string, 36) 转换 - $packed_str = $goods_part . $ticket_part; - $packed = intval($packed_str, 36); - - // Feistel8 混淆 + // ticket_id 混淆 + // ticket_id 填满5位 base36,用 Feistel8 混淆 + $ticket_int = intval(str_pad(base_convert($ticket_id, 10, 36), 5, '0', STR_PAD_LEFT), 36); $key = self::getGoodsKey($goods_id); - return strtolower(self::feistelEncode($packed, $key)); + $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); } /** * 解析短码(解码回 goods_id + ticket_id) * - * 明文方案:短码经 Feistel8 解密后,前4位=goods_id,后5位=ticket_id - * 解码 O(1),无需暴力搜索 + * 解码结构:【明文4位 goods_id】【混淆5位 ticket_id】 + * - 前4位:直接 base36_decode = goods_id + * - 后5位:用 goods_id 派生 key → Feistel 解密 = ticket_id + * - 解码 O(1),无暴力搜索 * - * @param string $code 短码(小写或大写均可) - * @param int|null $goods_id_hint 可选提示(已不再需要,用于兼容) + * @param string $code 短码(小写或大写均可,9位) + * @param int|null $goods_id_hint 可选提示(已不需要,用于兼容) * @return array ['goods_id' => int, 'ticket_id' => int] * @throws \Exception 解码失败时抛出 */ @@ -450,45 +452,27 @@ class BaseService { $code = strtolower($code); - // 如果有 hint,直接用 hint 的 key 解密 - // 如果没有 hint,暴力搜索(最多尝试 1-100000) - $decoded_goods_id = null; - $decoded_ticket_id = null; + // 前4位:明文 goods_id + $goods_part = substr($code, 0, 4); + $goods_id = intval($goods_part, 36); - if ($goods_id_hint !== null) { - $key = self::getGoodsKey($goods_id_hint); - $packed = self::feistelDecode($code, $key); - $packed_str = base_convert($packed, 10, 36); - // 前4位 goods_id,后5位 ticket_id - $decoded_goods_id = intval(substr($packed_str, 0, 4), 36); - $decoded_ticket_id = intval(substr($packed_str, 4, 5), 36); - // 验证解码出的 goods_id 是否与 hint 匹配 - if ($decoded_goods_id !== $goods_id_hint) { - throw new \Exception("短码解码失败:hint 不匹配 (code={$code}, hint={$goods_id_hint}, decoded={$decoded_goods_id})"); - } - } else { - // 暴力搜索 goods_id(优化:只搜索实际存在的范围) - $max_goods = 100000; - for ($gid = 1; $gid <= $max_goods; $gid++) { - $key = self::getGoodsKey($gid); - $packed = self::feistelDecode($code, $key); - $packed_str = base_convert($packed, 10, 36); - $candidate_goods_id = intval(substr($packed_str, 0, 4), 36); - if ($candidate_goods_id === $gid) { - $decoded_goods_id = $gid; - $decoded_ticket_id = intval(substr($packed_str, 4, 5), 36); - break; - } - } + // 校验 hint(如果提供) + if ($goods_id_hint !== null && $goods_id !== $goods_id_hint) { + throw new \Exception("短码解码失败:hint 不匹配 (code={$code}, hint={$goods_id_hint}, decoded={$goods_id})"); } - if ($decoded_goods_id === null) { - throw new \Exception("短码解码失败:无法找到匹配的 goods_id (code={$code})"); - } + // 用 goods_id 派生 key + $key = self::getGoodsKey($goods_id); + + // 后5位:混淆的 ticket_id → Feistel 解密 + $ticket_part = substr($code, 4, 5); + $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' => $decoded_goods_id, - 'ticket_id' => $decoded_ticket_id, + 'goods_id' => $goods_id, + 'ticket_id' => $ticket_id, ]; } diff --git a/tests/phase4_1_feistel_test.php b/tests/phase4_1_feistel_test.php index 1293532..33ac448 100644 --- a/tests/phase4_1_feistel_test.php +++ b/tests/phase4_1_feistel_test.php @@ -87,57 +87,41 @@ function shortCodeEncode(int $goods_id, int $ticket_id): string throw new Exception("ticket_id 超出范围 (max=1073741823), given={$ticket_id}"); } - // goods_id 固定4位 base36,ticket_id 固定5位 base36 + // goods_id 固定4位 base36(明文) $goods_part = str_pad(base_convert($goods_id, 10, 36), 4, '0', STR_PAD_LEFT); - $ticket_part = str_pad(base_convert($ticket_id, 10, 36), 5, '0', STR_PAD_LEFT); - // 拼接为9位 base36 字符串 - $packed_str = $goods_part . $ticket_part; - $packed = intval($packed_str, 36); - - // Feistel8 混淆 + // ticket_id 混淆 + $ticket_int = intval(str_pad(base_convert($ticket_id, 10, 36), 5, '0', STR_PAD_LEFT), 36); $key = getGoodsKey($goods_id); - return strtolower(feistelEncode($packed, $key)); + $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); } function shortCodeDecode(string $code, ?int $goods_id_hint = null): array { $code = strtolower($code); - $decoded_goods_id = null; - $decoded_ticket_id = null; + // 前4位:明文 goods_id + $goods_part = substr($code, 0, 4); + $goods_id = intval($goods_part, 36); - if ($goods_id_hint !== null) { - $key = getGoodsKey($goods_id_hint); - $packed = feistelDecode($code, $key); - $packed_str = base_convert($packed, 10, 36); - // 前4位 goods_id,后5位 ticket_id - $decoded_goods_id = intval(substr($packed_str, 0, 4), 36); - $decoded_ticket_id = intval(substr($packed_str, 4, 5), 36); - if ($decoded_goods_id !== $goods_id_hint) { - throw new Exception("短码解码失败:hint 不匹配"); - } - } else { - // 暴力搜索 - $max_goods = 100000; - for ($gid = 1; $gid <= $max_goods; $gid++) { - $key = getGoodsKey($gid); - $packed = feistelDecode($code, $key); - $packed_str = base_convert($packed, 10, 36); - $candidate_goods_id = intval(substr($packed_str, 0, 4), 36); - if ($candidate_goods_id === $gid) { - $decoded_goods_id = $gid; - $decoded_ticket_id = intval(substr($packed_str, 4, 5), 36); - break; - } - } + // 校验 hint(如果提供) + if ($goods_id_hint !== null && $goods_id !== $goods_id_hint) { + throw new Exception("短码解码失败:hint 不匹配"); } - if ($decoded_goods_id === null) { - throw new Exception("短码解码失败:无法找到匹配的 goods_id"); - } + // 用 goods_id 派生 key + $key = getGoodsKey($goods_id); - return ['goods_id' => $decoded_goods_id, 'ticket_id' => $decoded_ticket_id]; + // 后5位:混淆的 ticket_id → Feistel 解密 + $ticket_part = substr($code, 4, 5); + $ticket_int = feistelDecode($ticket_part, $key); + $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]; } function signQrPayload(array $payload): string