Files
justice/www/html/Admin/api/daily-reward.php
T

260 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 ต้องยาว 8128 (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);