146 lines
4.9 KiB
PHP
146 lines
4.9 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
namespace app\api\logic;
|
||
|
||
use app\dice\model\player\DicePlayer;
|
||
use app\api\cache\UserCache;
|
||
use plugin\saiadmin\exception\ApiException;
|
||
use Tinywan\Jwt\JwtToken;
|
||
|
||
/**
|
||
* API 用户登录/注册逻辑(基于 DicePlayer 表)
|
||
* 手机号格式限制:+60(马来西亚)
|
||
*/
|
||
class UserLogic
|
||
{
|
||
/** 手机号正则:+60 开头,后跟 9–10 位数字(马来西亚) */
|
||
private const PHONE_REGEX = '/^\+60\d{9,10}$/';
|
||
|
||
/** 与 DicePlayerLogic 保持一致的密码盐,用于登录校验与注册写入 */
|
||
private const PASSWORD_SALT = 'dice_player_salt_2024';
|
||
|
||
/** 状态:正常 */
|
||
private const STATUS_NORMAL = 1;
|
||
|
||
/**
|
||
* 手机号格式校验:+60 开头
|
||
*/
|
||
public static function validatePhone(string $phone): void
|
||
{
|
||
if (!preg_match(self::PHONE_REGEX, $phone)) {
|
||
throw new ApiException('手机号格式错误,仅支持 +60 开头的马来西亚号码(如 +60123456789)');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 与 DicePlayerLogic 一致的密码加密:md5(salt . password)
|
||
*/
|
||
private function hashPassword(string $password): string
|
||
{
|
||
return md5(self::PASSWORD_SALT . $password);
|
||
}
|
||
|
||
/**
|
||
* 登录(JSON:username, password, lang, coin, time)
|
||
* 存在则校验密码并更新 coin(累加);不存在则创建用户并写入 coin。
|
||
* 将会话写入 Redis,返回 token 与前端连接地址。
|
||
*/
|
||
public function loginByUsername(string $username, string $password, string $lang, float $coin, string $time): array
|
||
{
|
||
$username = trim($username);
|
||
if ($username === '') {
|
||
throw new ApiException('username 不能为空');
|
||
}
|
||
|
||
$player = DicePlayer::where('username', $username)->find();
|
||
if ($player) {
|
||
if ((int) ($player->status ?? 1) === 0) {
|
||
throw new ApiException('账号已被禁用,无法登录');
|
||
}
|
||
$hashed = $this->hashPassword($password);
|
||
if ($player->password !== $hashed) {
|
||
throw new ApiException('密码错误');
|
||
}
|
||
$currentCoin = (float) $player->coin;
|
||
$player->coin = $currentCoin + $coin;
|
||
$player->save();
|
||
} else {
|
||
$player = new DicePlayer();
|
||
$player->username = $username;
|
||
$player->phone = $username;
|
||
$player->password = $this->hashPassword($password);
|
||
$player->status = self::STATUS_NORMAL;
|
||
$player->coin = $coin;
|
||
$player->save();
|
||
}
|
||
|
||
$exp = (int) config('api.session_expire', 604800);
|
||
$tokenResult = JwtToken::generateToken([
|
||
'id' => (int) $player->id,
|
||
'username' => $username,
|
||
'plat' => 'api_login',
|
||
'access_exp' => $exp,
|
||
]);
|
||
$token = $tokenResult['access_token'];
|
||
UserCache::setSessionByUsername($username, $token);
|
||
|
||
$userArr = $player->hidden(['password', 'lottery_config_id', 't1_weight', 't2_weight', 't3_weight', 't4_weight', 't5_weight'])->toArray();
|
||
UserCache::setUser((int) $player->id, $userArr);
|
||
UserCache::setPlayerByUsername($username, $userArr);
|
||
|
||
$baseUrl = rtrim(config('api.login_url_base', 'https://127.0.0.1:6777'), '/');
|
||
$lang = in_array($lang, ['chs', 'en'], true) ? $lang : 'chs';
|
||
$tokenInUrl = str_replace('%3D', '=', urlencode($token));
|
||
$url = $baseUrl . '?token=' . $tokenInUrl . '&lang=' . $lang;
|
||
|
||
return [
|
||
'url' => $url,
|
||
'token' => $token,
|
||
'lang' => $lang,
|
||
'user_id' => (int) $player->id,
|
||
'user' => $userArr,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 从 JWT 中解析 username(仅解码 payload,不校验签名与过期,用于退出时清除会话)
|
||
*/
|
||
public static function getUsernameFromJwtPayload(string $token): ?string
|
||
{
|
||
$parts = explode('.', $token);
|
||
if (count($parts) !== 3) {
|
||
return null;
|
||
}
|
||
$payload = base64_decode(strtr($parts[1], '-_', '+/'), true);
|
||
if ($payload === false) {
|
||
return null;
|
||
}
|
||
$data = json_decode($payload, true);
|
||
if (!is_array($data)) {
|
||
return null;
|
||
}
|
||
$extend = $data['extend'] ?? $data;
|
||
$username = $extend['username'] ?? null;
|
||
return $username !== null ? trim((string) $username) : null;
|
||
}
|
||
|
||
/**
|
||
* 从 Redis 获取用户信息(key = base64(user_id)),未命中则查 DicePlayer 并回写缓存
|
||
*/
|
||
public static function getCachedUser(int $userId): array
|
||
{
|
||
$cached = UserCache::getUser($userId);
|
||
if (!empty($cached)) {
|
||
return $cached;
|
||
}
|
||
$user = DicePlayer::find($userId);
|
||
if (!$user) {
|
||
return [];
|
||
}
|
||
$arr = $user->hidden(['password'])->toArray();
|
||
UserCache::setUser($userId, $arr);
|
||
return $arr;
|
||
}
|
||
}
|