181 lines
5.5 KiB
PHP
181 lines
5.5 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)');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 登录:手机号 + 密码,返回用户信息与 user-token,并写入 Redis 缓存
|
||
*/
|
||
public function login(string $phone, string $password): array
|
||
{
|
||
self::validatePhone($phone);
|
||
|
||
$user = DicePlayer::where('phone', $phone)->find();
|
||
if (!$user) {
|
||
throw new ApiException('手机号未注册');
|
||
}
|
||
if ((int) $user->status !== self::STATUS_NORMAL) {
|
||
throw new ApiException('账号已被禁用');
|
||
}
|
||
$hashed = $this->hashPassword($password);
|
||
if ($user->password !== $hashed) {
|
||
throw new ApiException('密码错误');
|
||
}
|
||
|
||
$userArr = $user->hidden(['password'])->toArray();
|
||
UserCache::setUser((int) $user->id, $userArr);
|
||
|
||
$userToken = $this->generateUserToken((int) $user->id);
|
||
return [
|
||
'user' => $userArr,
|
||
'user-token' => $userToken,
|
||
'user_id' => (int) $user->id,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 注册:手机号 + 密码(+60),创建玩家并返回用户信息与 user-token,写入 Redis
|
||
*/
|
||
public function register(string $phone, string $password, ?string $nickname = null): array
|
||
{
|
||
self::validatePhone($phone);
|
||
|
||
if (strlen($password) < 6) {
|
||
throw new ApiException('密码至少 6 位');
|
||
}
|
||
|
||
$exists = DicePlayer::where('phone', $phone)->find();
|
||
if ($exists) {
|
||
throw new ApiException('该手机号已注册');
|
||
}
|
||
|
||
$user = new DicePlayer();
|
||
$user->phone = $phone;
|
||
$user->username = $phone;
|
||
if ($nickname !== null && $nickname !== '') {
|
||
$user->name = $nickname;
|
||
}
|
||
// name 未传时由 DicePlayer::onBeforeInsert 默认设为 uid
|
||
$user->password = $this->hashPassword($password);
|
||
$user->status = self::STATUS_NORMAL;
|
||
$user->save();
|
||
|
||
$userArr = $user->hidden(['password'])->toArray();
|
||
UserCache::setUser((int) $user->id, $userArr);
|
||
|
||
$userToken = $this->generateUserToken((int) $user->id);
|
||
return [
|
||
'user' => $userArr,
|
||
'user-token' => $userToken,
|
||
'user_id' => (int) $user->id,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 与 DicePlayerLogic 一致的密码加密:md5(salt . password)
|
||
*/
|
||
private function hashPassword(string $password): string
|
||
{
|
||
return md5(self::PASSWORD_SALT . $password);
|
||
}
|
||
|
||
/**
|
||
* 生成 user-token(JWT,plat=api_user,id=user_id)
|
||
*/
|
||
private function generateUserToken(int $userId): string
|
||
{
|
||
$exp = config('api.user_token_exp', 604800);
|
||
$result = JwtToken::generateToken([
|
||
'id' => $userId,
|
||
'plat' => 'api_user',
|
||
'access_exp' => $exp,
|
||
]);
|
||
return $result['access_token'];
|
||
}
|
||
|
||
/**
|
||
* 根据 user-token 获取 user_id(不写缓存,仅解析 JWT)
|
||
* 若 token 已通过退出接口加入黑名单,返回 null
|
||
*/
|
||
public static function getUserIdFromToken(string $userToken): ?int
|
||
{
|
||
if (UserCache::isTokenBlacklisted($userToken)) {
|
||
return null;
|
||
}
|
||
try {
|
||
$decoded = JwtToken::verify(1, $userToken);
|
||
$extend = $decoded['extend'] ?? [];
|
||
if (($extend['plat'] ?? '') !== 'api_user') {
|
||
return null;
|
||
}
|
||
$id = $extend['id'] ?? null;
|
||
return $id !== null ? (int) $id : null;
|
||
} catch (\Throwable $e) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 退出登录:将当前 user-token 加入黑名单,使该 token 失效
|
||
*/
|
||
public static function logout(string $userToken): bool
|
||
{
|
||
try {
|
||
$decoded = JwtToken::verify(1, $userToken);
|
||
$exp = (int) ($decoded['exp'] ?? 0);
|
||
$ttl = $exp > time() ? $exp - time() : 86400;
|
||
return UserCache::addTokenToBlacklist($userToken, $ttl);
|
||
} catch (\Throwable $e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从 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;
|
||
}
|
||
}
|