fix(phase4.1): 修正短码设计为【明文goods_id + 混淆ticket_id】
正确设计: - 前4位:goods_id 明文 base36(直接可读) - 后5位:ticket_id 经 Feistel8 混淆(保护 ticket_id) 编码流程: 1. goods_id → 4位 base36 明文 2. ticket_id → 5位 base36 → Feistel8 → 5位混淆密文 3. 拼接为9位短码 解码流程 O(1): 1. 前4位 base36_decode → goods_id 2. 用 goods_id 派生 key → Feistel8 解密后5位 → ticket_id 3. 无需暴力搜索,goods_id_hint 仅用于校验 优势: - 解码 O(1),无需暴力搜索 - goods_id 明文暴露(可接受,ticket_id 仍被保护) - ticket_id 受 Feistel8 混淆保护feat/phase4-ticket-wallet
parent
4df288c62a
commit
be9643b471
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue