260 lines
10 KiB
PHP
260 lines
10 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
/**
|
||
* สาธารณะ — ระบบรางวัลล็อกอินรายวัน (Daily Login Reward) แบบ server-authoritative
|
||
*
|
||
* GET ?playerKey=... → คืนสถานะปัจจุบัน { claimedDays, currentDay, msUntilReset, coins, rewards }
|
||
* POST { playerKey, day } → เคลมรางวัลของวันนั้น (server ตรวจสอบ + บวกเหรียญจริง กันยิงซ้ำ/ฟาร์ม)
|
||
*
|
||
* เก็บสถานะไว้ในบัญชี guest (providerUserId == playerKey) ใต้คีย์ "daily"
|
||
* รีเซ็ตทุกเที่ยงคืนตามเวลาไทย (Asia/Bangkok)
|
||
*/
|
||
require __DIR__ . '/_common.php';
|
||
|
||
date_default_timezone_set('Asia/Bangkok');
|
||
|
||
const DAILY_TOTAL_DAYS = 7;
|
||
// จำนวนเหรียญต่อวัน (วันที่ 7 ให้เยอะสุด) — server เป็นเจ้าของค่านี้ ฝั่ง client เลือกเองไม่ได้
|
||
const DAILY_REWARDS = [10, 15, 20, 25, 30, 50, 100];
|
||
|
||
function daily_now_ms(): int
|
||
{
|
||
return (int) round(microtime(true) * 1000);
|
||
}
|
||
|
||
/** เที่ยงคืนของ "วันนี้" (เวลาไทย) เป็น ms */
|
||
function daily_midnight_ms(int $nowMs): int
|
||
{
|
||
$t = (int) floor($nowMs / 1000);
|
||
$mid = strtotime('today 00:00:00', $t);
|
||
return ($mid !== false ? $mid : $t) * 1000;
|
||
}
|
||
|
||
/** เที่ยงคืนของ "พรุ่งนี้" (เวลาไทย) เป็น ms */
|
||
function daily_next_midnight_ms(int $nowMs): int
|
||
{
|
||
$t = (int) floor($nowMs / 1000);
|
||
$mid = strtotime('tomorrow 00:00:00', $t);
|
||
return ($mid !== false ? $mid : $t + 86400) * 1000;
|
||
}
|
||
|
||
function daily_default_state(int $nowMs): array
|
||
{
|
||
return [
|
||
'anchorMs' => daily_midnight_ms($nowMs),
|
||
'claimedDays' => array_fill(0, DAILY_TOTAL_DAYS, false),
|
||
'lockUntilMs' => 0,
|
||
];
|
||
}
|
||
|
||
/** ทำให้ state ถูกต้อง (รีเซ็ตรอบใหม่ถ้าถึงเวลา) แล้วคำนวณ currentDay/msUntilReset */
|
||
function daily_compute(array &$daily, int $nowMs): array
|
||
{
|
||
if (!is_array($daily['claimedDays'] ?? null) || count($daily['claimedDays']) !== DAILY_TOTAL_DAYS) {
|
||
$daily = daily_default_state($nowMs);
|
||
}
|
||
// normalize types
|
||
$claimed = [];
|
||
for ($i = 0; $i < DAILY_TOTAL_DAYS; $i++) {
|
||
$claimed[$i] = !empty($daily['claimedDays'][$i]);
|
||
}
|
||
$daily['claimedDays'] = $claimed;
|
||
$daily['anchorMs'] = (int) ($daily['anchorMs'] ?? daily_midnight_ms($nowMs));
|
||
$daily['lockUntilMs'] = (int) ($daily['lockUntilMs'] ?? 0);
|
||
|
||
$changed = false;
|
||
|
||
// วันแรกที่ยังไม่เคลม
|
||
$firstUnclaimed = 0;
|
||
for ($i = 0; $i < DAILY_TOTAL_DAYS; $i++) {
|
||
if (!$claimed[$i]) { $firstUnclaimed = $i + 1; break; }
|
||
}
|
||
|
||
$nowMidnight = daily_midnight_ms($nowMs);
|
||
$remainMs = max(0, daily_next_midnight_ms($nowMs) - $nowMs);
|
||
|
||
// self-heal: เคลมครบแต่ไม่มี lock → ตั้ง lock ถึงเที่ยงคืนถัดไป
|
||
if (!$firstUnclaimed && $daily['lockUntilMs'] <= 0) {
|
||
$daily['lockUntilMs'] = daily_next_midnight_ms($nowMs);
|
||
$changed = true;
|
||
}
|
||
|
||
// เคลมครบ 7 วัน + ถึงเวลารีเซ็ตแล้ว → เริ่มรอบใหม่
|
||
if (!$firstUnclaimed && $daily['lockUntilMs'] > 0 && $nowMs >= $daily['lockUntilMs']) {
|
||
$daily['anchorMs'] = $nowMidnight;
|
||
$daily['claimedDays'] = array_fill(0, DAILY_TOTAL_DAYS, false);
|
||
$daily['lockUntilMs'] = 0;
|
||
$claimed = $daily['claimedDays'];
|
||
$firstUnclaimed = 1;
|
||
$changed = true;
|
||
}
|
||
|
||
// วันที่ปลดล็อกตามเวลา (เพิ่มทีละวันทุกเที่ยงคืน)
|
||
$anchorMidnight = daily_midnight_ms((int) $daily['anchorMs']);
|
||
$dayIndex = (int) max(0, floor(($nowMidnight - $anchorMidnight) / 86400000));
|
||
$unlockedByTime = min(DAILY_TOTAL_DAYS, $dayIndex + 1);
|
||
|
||
$lockActive = $daily['lockUntilMs'] > $nowMs;
|
||
if (!$lockActive && $daily['lockUntilMs'] > 0) {
|
||
$daily['lockUntilMs'] = 0;
|
||
$changed = true;
|
||
}
|
||
|
||
$currentDay = 0;
|
||
if ($firstUnclaimed > 0 && $firstUnclaimed <= $unlockedByTime && !$lockActive) {
|
||
$currentDay = $firstUnclaimed;
|
||
}
|
||
|
||
return [
|
||
'changed' => $changed,
|
||
'currentDay' => $currentDay,
|
||
'msUntilReset' => $remainMs,
|
||
'firstUnclaimed' => $firstUnclaimed,
|
||
];
|
||
}
|
||
|
||
function daily_public_state(array $daily, array $computed, int $coins, int $nowMs): array
|
||
{
|
||
return [
|
||
'ok' => true,
|
||
'claimedDays' => array_values($daily['claimedDays']),
|
||
'anchorMs' => (int) $daily['anchorMs'],
|
||
'lockUntilMs' => (int) $daily['lockUntilMs'],
|
||
'currentDay' => (int) $computed['currentDay'],
|
||
'msUntilReset' => (int) $computed['msUntilReset'],
|
||
'serverNowMs' => $nowMs,
|
||
'coins' => $coins,
|
||
'rewards' => DAILY_REWARDS,
|
||
];
|
||
}
|
||
|
||
function daily_validate_key(string $key): void
|
||
{
|
||
if (!preg_match('/^[a-zA-Z0-9_-]{8,128}$/', $key)) {
|
||
json_response(['ok' => false, 'error' => 'playerKey ต้องยาว 8–128 (a-z 0-9 _ -)'], 400);
|
||
}
|
||
}
|
||
|
||
/** หา index บัญชี guest ตาม playerKey (คืน -1 ถ้าไม่มี) */
|
||
function daily_find_account(array $store, string $key): int
|
||
{
|
||
foreach ($store['accounts'] ?? [] as $i => $a) {
|
||
if (($a['loginType'] ?? '') === 'guest' && ($a['providerUserId'] ?? '') === $key) {
|
||
return $i;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||
$nowMs = daily_now_ms();
|
||
|
||
if ($method === 'GET') {
|
||
$key = trim((string) ($_GET['playerKey'] ?? ''));
|
||
daily_validate_key($key);
|
||
|
||
$store = read_store();
|
||
$idx = daily_find_account($store, $key);
|
||
|
||
if ($idx < 0) {
|
||
// ยังไม่มีบัญชี → คืน state เริ่มต้น (ไม่เขียน store จนกว่าจะเคลมจริง)
|
||
$daily = daily_default_state($nowMs);
|
||
$computed = daily_compute($daily, $nowMs);
|
||
json_response(daily_public_state($daily, $computed, 0, $nowMs));
|
||
}
|
||
|
||
$acc = $store['accounts'][$idx];
|
||
$daily = is_array($acc['daily'] ?? null) ? $acc['daily'] : daily_default_state($nowMs);
|
||
$computed = daily_compute($daily, $nowMs);
|
||
$coins = max(0, (int) ($acc['coins'] ?? 0));
|
||
|
||
if ($computed['changed']) {
|
||
$store['accounts'][$idx]['daily'] = $daily;
|
||
$store['accounts'][$idx]['updatedAt'] = gmdate('c');
|
||
write_store($store);
|
||
}
|
||
|
||
json_response(daily_public_state($daily, $computed, $coins, $nowMs));
|
||
}
|
||
|
||
if ($method !== 'POST') {
|
||
json_response(['ok' => false, 'error' => 'Use GET or POST'], 405);
|
||
}
|
||
|
||
// ---- POST: เคลมรางวัล ----
|
||
$body = require_json_body();
|
||
$key = trim((string) ($body['playerKey'] ?? ''));
|
||
daily_validate_key($key);
|
||
|
||
$day = (int) ($body['day'] ?? 0);
|
||
if ($day < 1 || $day > DAILY_TOTAL_DAYS) {
|
||
json_response(['ok' => false, 'error' => 'day ต้องอยู่ระหว่าง 1–' . DAILY_TOTAL_DAYS], 400);
|
||
}
|
||
|
||
$store = read_store();
|
||
$idx = daily_find_account($store, $key);
|
||
|
||
if ($idx < 0) {
|
||
// สร้างบัญชี guest ใหม่พร้อม daily state
|
||
$idx = count($store['accounts'] ?? []);
|
||
$store['accounts'][] = [
|
||
'id' => new_id(),
|
||
'email' => '',
|
||
'displayName' => 'Guest',
|
||
'loginType' => 'guest',
|
||
'providerUserId' => $key,
|
||
'notes' => 'auto: daily-reward',
|
||
'blocked' => false,
|
||
'coins' => 0,
|
||
'daily' => daily_default_state($nowMs),
|
||
'createdAt' => gmdate('c'),
|
||
'updatedAt' => gmdate('c'),
|
||
];
|
||
}
|
||
|
||
$acc = $store['accounts'][$idx];
|
||
if (!empty($acc['blocked'])) {
|
||
json_response(['ok' => false, 'error' => 'บัญชีถูกระงับ'], 403);
|
||
}
|
||
|
||
$daily = is_array($acc['daily'] ?? null) ? $acc['daily'] : daily_default_state($nowMs);
|
||
$computed = daily_compute($daily, $nowMs);
|
||
|
||
// ตรวจสอบ server-side: วันที่เคลมต้องเป็น "วันที่กดได้" จริงในตอนนี้
|
||
if ($computed['currentDay'] !== $day) {
|
||
$coinsNow = max(0, (int) ($acc['coins'] ?? 0));
|
||
$resp = daily_public_state($daily, $computed, $coinsNow, $nowMs);
|
||
$resp['ok'] = false;
|
||
$resp['error'] = $daily['claimedDays'][$day - 1]
|
||
? 'รับรางวัลวันนี้ไปแล้ว รอรีเซ็ตรอบถัดไป'
|
||
: 'ยังเปิดรับรางวัลวันนี้ไม่ได้';
|
||
// เขียน state ที่ normalize แล้ว (เผื่อมีการรีเซ็ต) ก่อนตอบ
|
||
$store['accounts'][$idx]['daily'] = $daily;
|
||
$store['accounts'][$idx]['updatedAt'] = gmdate('c');
|
||
write_store($store);
|
||
json_response($resp, 409);
|
||
}
|
||
|
||
// ผ่านการตรวจสอบ → เคลม + บวกเหรียญจริง
|
||
$reward = DAILY_REWARDS[$day - 1] ?? 0;
|
||
$daily['claimedDays'][$day - 1] = true;
|
||
$daily['lockUntilMs'] = daily_next_midnight_ms($nowMs);
|
||
|
||
$coins = max(0, (int) ($acc['coins'] ?? 0)) + $reward;
|
||
$store['accounts'][$idx]['coins'] = $coins;
|
||
$store['accounts'][$idx]['daily'] = $daily;
|
||
$store['accounts'][$idx]['updatedAt'] = gmdate('c');
|
||
|
||
if (!write_store($store)) {
|
||
json_response(['ok' => false, 'error' => 'บันทึกไม่สำเร็จ'], 500);
|
||
}
|
||
|
||
// คำนวณสถานะใหม่หลังเคลม (currentDay จะเลื่อนเป็น 0 เพราะ lock แล้ว)
|
||
$computedAfter = daily_compute($daily, $nowMs);
|
||
$resp = daily_public_state($daily, $computedAfter, $coins, $nowMs);
|
||
$resp['claimedDay'] = $day;
|
||
$resp['reward'] = $reward;
|
||
$resp['added'] = $reward;
|
||
json_response($resp);
|