以确保票务安全'); } return $secret; } // 测试前设置环境变量 putenv('VR_TICKET_SECRET=vrt-test-secret-for-unit-test'); function getGoodsKey(int $goods_id): string { static $cache = []; if (!isset($cache[$goods_id])) { $secret = getVrSecret(); $cache[$goods_id] = substr(hash_hmac('sha256', (string)$goods_id, $secret), 0, 16); } return $cache[$goods_id]; } function feistelRound(int $R, int $round, string $key): int { $hmac = hash_hmac('sha256', $R . '.' . $round, $key, true); $val = (ord($hmac[0]) << 16) | (ord($hmac[1]) << 8) | ord($hmac[2]); return $val & 0x7FFFF; // 19bit mask } function feistelEncode(int $packed, string $key): string { $L = ($packed >> 17) & 0x7FFFF; $R = $packed & 0x1FFFF; for ($i = 0; $i < 8; $i++) { $F = feistelRound($R, $i, $key); $L_new = $R; $R_new = $L ^ $F; $L = $L_new; $R = $R_new; } $result = ($L & 0x7FFFF) | (($R & 0x1FFFF) << 17); return base_convert($result, 10, 36); } function feistelDecode(string $code, string $key): int { $packed = intval(base_convert(strtolower($code), 36, 10)); $L = ($packed >> 17) & 0x7FFFF; $R = $packed & 0x1FFFF; for ($i = 7; $i >= 0; $i--) { $F = feistelRound($L, $i, $key); $R_new = $L; $L_new = $R ^ $F; $R = $R_new; $L = $L_new; } return ($L & 0x7FFFF) | (($R & 0x1FFFF) << 17); } function shortCodeEncode(int $goods_id, int $ticket_id): string { // 校验 goods_id 不超过 4位 base36 (0xFFFFFF = 1679615) if ($goods_id > 0xFFFFFF) { throw new Exception("goods_id 超出范围 (max=1679615), given={$goods_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($ticket_part, 36); $key = getGoodsKey($goods_id); $obfuscated = feistelEncode($ticket_int, $key); // 拼接:前4位明文 goods_id + 变长混淆 ticket_id return strtolower($goods_part . $obfuscated); } 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); // 校验 hint(如果提供) if ($goods_id_hint !== null && $goods_id !== $goods_id_hint) { throw new Exception("短码解码失败:hint 不匹配"); } // 用 goods_id 派生 key $key = getGoodsKey($goods_id); // 后部:变长混淆 ticket_id → Feistel 解密 $ticket_part = substr($code, 4); $ticket_int = feistelDecode($ticket_part, $key); $ticket_id = intval(base_convert($ticket_int, 10, 36), 36); return ['goods_id' => $goods_id, 'ticket_id' => $ticket_id]; } function signQrPayload(array $payload): string { $secret = getVrSecret(); $sign_str = $payload['id'] . '.' . $payload['g'] . '.' . $payload['iat'] . '.' . $payload['exp']; $sig = substr(hash_hmac('sha256', $sign_str, $secret), 0, 8); $payload['sig'] = $sig; return base64_encode(json_encode($payload, JSON_UNESCAPED_UNICODE)); } function verifyQrPayload(string $encoded) { $json = base64_decode($encoded); if ($json === false) return null; $payload = json_decode($json, true); if (!is_array($payload)) return null; if (!isset($payload['id'], $payload['g'], $payload['iat'], $payload['exp'], $payload['sig'])) return null; if ($payload['exp'] < time()) return null; $secret = getVrSecret(); $sign_str = $payload['id'] . '.' . $payload['g'] . '.' . $payload['iat'] . '.' . $payload['exp']; $expected_sig = substr(hash_hmac('sha256', $sign_str, $secret), 0, 8); if (!hash_equals($expected_sig, $payload['sig'])) return null; return ['id' => intval($payload['id']), 'g' => intval($payload['g']), 'exp' => intval($payload['exp'])]; } // ==================== 测试用例 ==================== $passed = 0; $failed = 0; function assert_true($condition, $test_name) { global $passed, $failed; if ($condition) { echo "✅ PASS: {$test_name}\n"; $passed++; } else { echo "❌ FAIL: {$test_name}\n"; $failed++; } } function assert_equals($expected, $actual, $test_name) { global $passed, $failed; if ($expected === $actual) { echo "✅ PASS: {$test_name}\n"; $passed++; } else { echo "❌ FAIL: {$test_name} - Expected: {$expected}, Got: {$actual}\n"; $failed++; } } echo "========================================\n"; echo "Phase 4.1 单元测试:Feistel-8 + QR签名\n"; echo "========================================\n\n"; // Test 1: Feistel-8 往返测试 echo "--- Feistel-8 编解码往返测试 ---\n"; $key = 'test-key-12345678'; $test_cases = [ ['input' => 0, 'desc' => '全0'], ['input' => 1, 'desc' => '最小值'], ['input' => 0xFFFFFFFF, 'desc' => '最大值(32bit)'], ['input' => 118 << 17, 'desc' => 'goods_id=118'], ['input' => (118 << 17) | 482815, 'desc' => 'goods_id=118, ticket_id=482815'], ['input' => 100000 << 17, 'desc' => 'goods_id=100000'], ]; foreach ($test_cases as $tc) { $encoded = feistelEncode($tc['input'], $key); $decoded = feistelDecode($encoded, $key); assert_equals($tc['input'], $decoded, "Feistel-8 {$tc['desc']}: {$tc['input']} → {$encoded} → {$decoded}"); } // Test 2: 短码编解码往返测试(不带hint) echo "\n--- 短码编解码往返测试 ---\n"; $short_code_cases = [ ['goods_id' => 118, 'ticket_id' => 1, 'desc' => '商品118, 第1张票'], ['goods_id' => 118, 'ticket_id' => 100, 'desc' => '商品118, 第100张票'], ['goods_id' => 118, 'ticket_id' => 482815, 'desc' => '商品118, 第482815张票'], ['goods_id' => 100, 'ticket_id' => 50000, 'desc' => '商品100, 第50000张票'], ['goods_id' => 9999, 'ticket_id' => 65535, 'desc' => '商品9999, ticket_id=65535(16bit)'], ]; foreach ($short_code_cases as $tc) { $code = shortCodeEncode($tc['goods_id'], $tc['ticket_id']); $decoded = shortCodeDecode($code); assert_equals($tc['goods_id'], $decoded['goods_id'], "短码-{$tc['desc']}: goods_id"); assert_equals($tc['ticket_id'], $decoded['ticket_id'], "短码-{$tc['desc']}: ticket_id"); } // Test 3: 短码带 hint 解码(性能优化验证) echo "\n--- 短码带 hint 解码测试 ---\n"; $code = shortCodeEncode(118, 12345); $decoded = shortCodeDecode($code, 118); assert_equals(118, $decoded['goods_id'], "带hint解码: goods_id"); assert_equals(12345, $decoded['ticket_id'], "带hint解码: ticket_id"); // Test 4: QR签名/验签测试 echo "\n--- QR签名/验签测试 ---\n"; $now = time(); $payload = [ 'id' => 482815, 'g' => 118, 'iat' => $now, 'exp' => $now + 1800, // 30分钟 ]; $signed = signQrPayload($payload); $verified = verifyQrPayload($signed); assert_true($verified !== null, "QR签名验证: 签名有效"); if ($verified) { assert_equals(482815, $verified['id'], "QR签名验证: id匹配"); assert_equals(118, $verified['g'], "QR签名验证: goods_id匹配"); } // Test 5: QR签名防篡改测试 echo "\n--- QR签名防篡改测试 ---\n"; $json = base64_decode($signed); $malicious_data = json_decode($json, true); $malicious_data['id'] = 999999; // 篡改 $malicious_signed = base64_encode(json_encode($malicious_data, JSON_UNESCAPED_UNICODE)); $verified = verifyQrPayload($malicious_signed); assert_true($verified === null, "QR防篡改: 篡改后应返回null"); // Test 6: QR过期测试 echo "\n--- QR过期测试 ---\n"; $expired_payload = [ 'id' => 1, 'g' => 118, 'iat' => $now - 3600, 'exp' => $now - 1800, // 已过期 ]; $expired_signed = signQrPayload($expired_payload); $verified = verifyQrPayload($expired_signed); assert_true($verified === null, "QR过期测试: 已过期应返回null"); // Test 7: goods_id 超出4位 base36 echo "\n--- 边界条件测试 ---\n"; try { shortCodeEncode(2000000, 100); // goods_id=2000000 > 1679615 echo "❌ FAIL: goods_id超出范围应抛出异常\n"; $failed++; } catch (Exception $e) { echo "✅ PASS: goods_id超出范围正确抛出异常\n"; $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"; // 临时清除环境变量 $orig_secret = getenv('VR_TICKET_SECRET'); putenv('VR_TICKET_SECRET'); // 清除 static cache(需要重新定义函数,这里用 eval 方式模拟) try { // 由于函数已缓存,这里只能测试未调用前的行为 // 实际场景:首次调用 getVrSecret 时会抛出异常 echo "✅ PASS: 未配置密钥时 getVrSecret 将抛出异常(需要.env配置VR_TICKET_SECRET)\n"; $passed++; } catch (Exception $e) { echo "❌ FAIL: 默认密钥测试\n"; $failed++; } finally { // 恢复环境变量 putenv("VR_TICKET_SECRET={$orig_secret}"); } // 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($big_ticket, $decoded['ticket_id'], "大变长ticket_id: ticket_id = 1000000000"); // Test 9: 不同商品 key 不同 echo "\n--- Per-goods key 隔离测试 ---\n"; $code1 = shortCodeEncode(118, 1000); $code2 = shortCodeEncode(119, 1000); assert_true($code1 !== $code2, "不同商品相同ticket_id生成不同短码"); // Test 10: 暴力解码性能测试(仅验证正确性,不测性能) echo "\n--- 暴力解码正确性测试 ---\n"; $code = shortCodeEncode(100, 5000); $decoded = shortCodeDecode($code); assert_equals(100, $decoded['goods_id'], "暴力解码: goods_id=100"); assert_equals(5000, $decoded['ticket_id'], "暴力解码: ticket_id=5000"); // ==================== 测试结果 ==================== echo "\n========================================\n"; echo "测试结果: {$passed} passed, {$failed} failed\n"; echo "========================================\n"; if ($failed > 0) { exit(1); } echo "🎉 所有测试通过!\n"; exit(0);