fixed แต่งตัวไม่ได้

This commit is contained in:
2026-06-19 10:47:45 +00:00
parent 57642f2d30
commit 4fc71fd08a
11 changed files with 859 additions and 76 deletions
+228
View File
@@ -0,0 +1,228 @@
<?php
declare(strict_types=1);
/**
* สาธารณะ — ระบบ "รหัสผู้เล่น" สำหรับ sync โปรไฟล์ข้าม browser/เครื่อง (Guest)
* ผูกกับ jdPlayerKey (guest account: providerUserId = key) เหมือน player-coins.php
*
* GET ?action=code&playerKey=...&displayName=...&agentId=...
* → คืนรหัสของบัญชีนี้ (สร้างถ้ายังไม่มี) + อัปเดตชื่อ/agent ฝั่ง server
* { ok, code, codeDisplay }
*
* POST { action:'redeem', code }
* → ดึงโปรไฟล์จากรหัส → คืน playerKey + ข้อมูลโปรไฟล์ของบัญชีนั้น
* { ok, playerKey, coins, colorThemeIndex, skinToneIndex, displayName, agentId }
*
* รหัสที่ไม่ถูกใช้เกิน 30 วัน → หมดอายุ (ลบแบบ lazy ตอนมีการเรียก)
*/
require __DIR__ . '/_common.php';
const LINK_CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789'; // ตัด 0 O 1 I L ที่กำกวม
const LINK_CODE_LEN = 8;
const LINK_CODE_TTL_DAYS = 30;
function valid_player_key(string $key): bool
{
return (bool)preg_match('/^[a-zA-Z0-9_-]{8,128}$/', $key);
}
function sanitize_display_name(string $raw): string
{
$s = trim($raw);
$s = preg_replace('/[\x00-\x1f\x7f]/u', '', $s) ?? '';
if (function_exists('mb_substr')) {
$s = mb_substr($s, 0, 24);
} else {
$s = substr($s, 0, 24);
}
return $s;
}
function sanitize_agent_id(string $raw): string
{
return preg_match('/^\d{6}$/', trim($raw)) ? trim($raw) : '';
}
function normalize_code(string $raw): string
{
$s = strtoupper(trim($raw));
$s = str_replace([' ', '-', '_'], '', $s);
// กันสับสน: ผู้ใช้พิมพ์ 0/O/1/I/L → map เข้าตัวที่ใช้จริง
$s = strtr($s, ['0' => 'O', '1' => 'I']);
return $s;
}
function generate_code(): string
{
$n = strlen(LINK_CODE_ALPHABET);
$out = '';
for ($i = 0; $i < LINK_CODE_LEN; $i++) {
$out .= LINK_CODE_ALPHABET[random_int(0, $n - 1)];
}
return $out;
}
function format_code_display(string $code): string
{
if (strlen($code) === 8) {
return substr($code, 0, 4) . '-' . substr($code, 4, 4);
}
return $code;
}
function code_is_expired($lastUsedAt): bool
{
if (!$lastUsedAt) {
return false;
}
$ts = strtotime((string)$lastUsedAt);
if ($ts === false) {
return false;
}
return (time() - $ts) > (LINK_CODE_TTL_DAYS * 86400);
}
/** ลบรหัสที่หมดอายุออกจากทุกบัญชี (lazy GC) — แก้ใน $store ตรง ๆ */
function gc_expired_codes(array &$store): bool
{
$changed = false;
foreach ($store['accounts'] as $i => $a) {
if (!empty($a['linkCode']) && code_is_expired($a['linkCodeLastUsedAt'] ?? null)) {
unset($store['accounts'][$i]['linkCode']);
$store['accounts'][$i]['linkCodeLastUsedAt'] = null;
$changed = true;
}
}
return $changed;
}
function code_in_use(array $store, string $code, ?string $exceptKey = null): bool
{
foreach ($store['accounts'] as $a) {
if (($a['linkCode'] ?? '') !== $code) {
continue;
}
if ($exceptKey !== null && ($a['providerUserId'] ?? '') === $exceptKey) {
continue;
}
if (!code_is_expired($a['linkCodeLastUsedAt'] ?? null)) {
return true;
}
}
return false;
}
function profile_payload(array $a): array
{
return [
'playerKey' => $a['providerUserId'] ?? '',
'coins' => max(0, (int)($a['coins'] ?? 0)),
'colorThemeIndex' => ($v = (int)($a['lobbyColorThemeIndex'] ?? 0)) >= 1 && $v <= 8 ? $v : null,
'skinToneIndex' => ($s = (int)($a['lobbySkinToneIndex'] ?? 0)) >= 1 && $s <= 3 ? $s : null,
'displayName' => (string)($a['displayName'] ?? ''),
'agentId' => (string)($a['agentDisplayId'] ?? ''),
];
}
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
if ($method === 'GET') {
$action = (string)($_GET['action'] ?? 'code');
if ($action !== 'code') {
json_response(['ok' => false, 'error' => 'unknown action'], 400);
}
$key = trim((string)($_GET['playerKey'] ?? ''));
if (!valid_player_key($key)) {
json_response(['ok' => false, 'error' => 'playerKey ต้องยาว 8128 (a-z 0-9 _ -)'], 400);
}
$displayName = sanitize_display_name((string)($_GET['displayName'] ?? ''));
$agentId = sanitize_agent_id((string)($_GET['agentId'] ?? ''));
$store = read_store();
gc_expired_codes($store);
$now = gmdate('c');
$idx = -1;
foreach ($store['accounts'] as $i => $a) {
if (($a['loginType'] ?? '') === 'guest' && ($a['providerUserId'] ?? '') === $key) {
$idx = $i;
break;
}
}
if ($idx < 0) {
$store['accounts'][] = [
'id' => new_id(),
'email' => '',
'displayName' => $displayName,
'loginType' => 'guest',
'providerUserId' => $key,
'notes' => 'auto: player-link',
'blocked' => false,
'coins' => 0,
'agentDisplayId' => $agentId,
'createdAt' => $now,
'updatedAt' => $now,
];
$idx = count($store['accounts']) - 1;
} else {
if ($displayName !== '') {
$store['accounts'][$idx]['displayName'] = $displayName;
}
if ($agentId !== '') {
$store['accounts'][$idx]['agentDisplayId'] = $agentId;
}
$store['accounts'][$idx]['updatedAt'] = $now;
}
$code = (string)($store['accounts'][$idx]['linkCode'] ?? '');
if ($code === '' || code_is_expired($store['accounts'][$idx]['linkCodeLastUsedAt'] ?? null)) {
do {
$code = generate_code();
} while (code_in_use($store, $code, $key));
$store['accounts'][$idx]['linkCode'] = $code;
}
$store['accounts'][$idx]['linkCodeLastUsedAt'] = $now;
if (!write_store($store)) {
json_response(['ok' => false, 'error' => 'สร้างรหัสไม่สำเร็จ'], 500);
}
json_response(['ok' => true, 'code' => $code, 'codeDisplay' => format_code_display($code)]);
}
if ($method === 'POST') {
$body = require_json_body();
$action = (string)($body['action'] ?? 'redeem');
if ($action !== 'redeem') {
json_response(['ok' => false, 'error' => 'unknown action'], 400);
}
$code = normalize_code((string)($body['code'] ?? ''));
if (!preg_match('/^[A-Z2-9]{' . LINK_CODE_LEN . '}$/', $code)) {
json_response(['ok' => false, 'error' => 'รูปแบบรหัสไม่ถูกต้อง'], 400);
}
$store = read_store();
$sweep = gc_expired_codes($store);
$now = gmdate('c');
$found = -1;
foreach ($store['accounts'] as $i => $a) {
if (($a['linkCode'] ?? '') === $code && !code_is_expired($a['linkCodeLastUsedAt'] ?? null)) {
$found = $i;
break;
}
}
if ($found < 0) {
if ($sweep) {
write_store($store);
}
json_response(['ok' => false, 'error' => 'ไม่พบรหัสนี้ หรือรหัสหมดอายุแล้ว'], 404);
}
$store['accounts'][$found]['linkCodeLastUsedAt'] = $now;
$store['accounts'][$found]['updatedAt'] = $now;
write_store($store);
$payload = profile_payload($store['accounts'][$found]);
$payload['ok'] = true;
json_response($payload);
}
json_response(['ok' => false, 'error' => 'Use GET or POST'], 405);