Files
dafuweng-saiadmin6.x/server/app/api/logic/UserLogic.php
2026-03-19 15:52:06 +08:00

230 lines
8.2 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);
namespace app\api\logic;
use app\dice\model\player\DicePlayer;
use app\api\cache\UserCache;
use plugin\saiadmin\app\model\system\SystemDept;
use plugin\saiadmin\app\model\system\SystemUser;
use plugin\saiadmin\exception\ApiException;
use Tinywan\Jwt\JwtToken;
/**
* API 用户登录/注册逻辑(基于 DicePlayer 表)
* 手机号格式限制:+60马来西亚
*/
class UserLogic
{
/** 手机号正则:+60 开头,后跟 910 位数字(马来西亚) */
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('Invalid phone format, only +60 Malaysia numbers supported (e.g. +60123456789)');
}
}
/**
* 与 DicePlayerLogic 一致的密码加密md5(salt . password)
*/
private function hashPassword(string $password): string
{
return md5(self::PASSWORD_SALT . $password);
}
/**
* 根据 parent_id 向上遍历找到顶级部门parent_id=0
*/
private static function getTopDeptIdByParentId(int $deptId): ?int
{
$currentId = $deptId;
$visited = [];
while ($currentId > 0 && !isset($visited[$currentId])) {
$visited[$currentId] = true;
$dept = SystemDept::find($currentId);
if (!$dept) {
return null;
}
$parentId = (int) ($dept->parent_id ?? 0);
if ($parentId === 0) {
return $currentId;
}
$currentId = $parentId;
}
return $currentId > 0 ? $currentId : null;
}
/**
* 根据顶级部门 id递归获取其下所有部门 id含自身仅用 id 和 parent_id
*/
private static function getAllDeptIdsUnderTop(int $topId): array
{
$deptIds = [$topId];
$prevCount = 0;
while (count($deptIds) > $prevCount) {
$prevCount = count($deptIds);
$children = SystemDept::whereIn('parent_id', $deptIds)->column('id');
$deptIds = array_unique(array_merge($deptIds, array_map('intval', $children)));
}
return array_values($deptIds);
}
/**
* 根据 agent_id 获取当前管理员所在顶级部门下的所有管理员 ID 列表
* 使用 SystemDept 的 id 和 parent_id 字段遍历:先向上找顶级部门(parent_id=0),再向下收集所有子部门
* 用于 getGameUrl 接口判断 DicePlayer 是否属于该部门,同顶级部门下不重复创建玩家
*
* @param string $agentId 代理标识sa_system_user.agent_id
* @return int[] 管理员 ID 列表,空数组表示未找到或无法解析
*/
public static function getAdminIdsByAgentIdTopDept(string $agentId): array
{
$agentId = trim($agentId);
if ($agentId === '') {
return [];
}
$admin = SystemUser::where('agent_id', $agentId)->find();
if (!$admin) {
return [];
}
$deptId = $admin->dept_id ?? null;
if ($deptId === null || $deptId === '') {
return [(int) $admin->id];
}
$deptId = (int) $deptId;
$topId = self::getTopDeptIdByParentId($deptId);
if ($topId === null) {
return [(int) $admin->id];
}
$deptIds = self::getAllDeptIdsUnderTop($topId);
if (empty($deptIds)) {
$deptIds = [$deptId];
}
$adminIds = SystemUser::whereIn('dept_id', $deptIds)->column('id');
return array_map('intval', $adminIds);
}
/**
* 登录JSONusername, password, lang, coin, time
* 存在则校验密码并更新 coin累加不存在则创建用户并写入 coin。
* 将会话写入 Redis返回 token 与前端连接地址。
*
* @param int|null $adminId 创建新用户时关联的后台管理员IDsa_system_user.id可选
* @param int[]|null $adminIdsInTopDept 当前管理员顶级部门下的所有管理员ID用于按部门范围查找玩家为空时退化为仅按 username 查找
*/
public function loginByUsername(string $username, string $password, string $lang, float $coin, string $time, ?int $adminId = null, ?array $adminIdsInTopDept = null): array
{
$username = trim($username);
if ($username === '') {
throw new ApiException('username is required');
}
$query = DicePlayer::where('username', $username);
if ($adminIdsInTopDept !== null && !empty($adminIdsInTopDept)) {
$query->whereIn('admin_id', $adminIdsInTopDept);
}
$player = $query->find();
if ($player) {
if ((int) ($player->status ?? 1) === 0) {
throw new ApiException('Account is disabled and cannot log in');
}
$hashed = $this->hashPassword($password);
if ($player->password !== $hashed) {
throw new ApiException('Wrong password');
}
$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;
if ($adminId !== null && $adminId > 0) {
$player->admin_id = $adminId;
}
$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;
}
}