重构初始化

This commit is contained in:
技术老胡
2026-04-15 11:45:46 +08:00
parent 72d72d735b
commit 7612026773
381 changed files with 28287 additions and 14717 deletions

View File

@@ -0,0 +1,188 @@
<?php
declare(strict_types=1);
namespace app\common\util;
use DateTimeInterface;
/**
* 通用格式化帮助类。
*
* 集中处理金额、时间、JSON、映射文案和脱敏逻辑避免各服务层重复实现。
*/
class FormatHelper
{
/**
* 金额格式化,单位为元。
*/
public static function amount(int $amount): string
{
return number_format($amount / 100, 2, '.', '');
}
/**
* 金额格式化0 时显示不限。
*/
public static function amountOrUnlimited(int $amount): string
{
return $amount > 0 ? self::amount($amount) : '不限';
}
/**
* 次数格式化0 时显示不限。
*/
public static function countOrUnlimited(int $count): string
{
return $count > 0 ? (string) $count : '不限';
}
/**
* 费率格式化,单位为百分点。
*/
public static function rate(int $basisPoints): string
{
return number_format($basisPoints / 100, 2, '.', '') . '%';
}
/**
* 延迟格式化。
*/
public static function latency(int $latencyMs): string
{
return $latencyMs > 0 ? $latencyMs . ' ms' : '0 ms';
}
/**
* 日期格式化。
*/
public static function date(mixed $value, string $emptyText = ''): string
{
return self::formatTemporalValue($value, 'Y-m-d', $emptyText);
}
/**
* 日期时间格式化。
*/
public static function dateTime(mixed $value, string $emptyText = ''): string
{
return self::formatTemporalValue($value, 'Y-m-d H:i:s', $emptyText);
}
/**
* 按时间戳格式化。
*/
public static function timestamp(int $timestamp, string $pattern = 'Y-m-d H:i:s', string $emptyText = ''): string
{
if ($timestamp <= 0) {
return $emptyText;
}
return date($pattern, $timestamp);
}
/**
* JSON 文本格式化。
*/
public static function json(mixed $value, string $emptyText = ''): string
{
if ($value === null || $value === '' || $value === []) {
return $emptyText;
}
if (is_string($value)) {
$decoded = json_decode($value, true);
if (json_last_error() === JSON_ERROR_NONE) {
$encoded = json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
return $encoded !== false ? $encoded : $emptyText;
}
return $value;
}
$encoded = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
return $encoded !== false ? $encoded : $emptyText;
}
/**
* 映射表文本转换。
*/
public static function textFromMap(int $value, array $map, string $default = '未知'): string
{
return (string) ($map[$value] ?? $default);
}
/**
* 接口凭证明文脱敏。
*/
public static function maskCredentialValue(string $credentialValue, bool $maskShortValue = true): string
{
$credentialValue = trim($credentialValue);
if ($credentialValue === '') {
return '';
}
$length = strlen($credentialValue);
if ($length <= 8) {
return $maskShortValue ? str_repeat('*', $length) : $credentialValue;
}
return substr($credentialValue, 0, 4) . '****' . substr($credentialValue, -4);
}
/**
* 将模型或对象归一化成数组。
*/
public static function normalizeModel(mixed $value): ?array
{
if ($value === null) {
return null;
}
if (is_array($value)) {
return $value;
}
if (is_object($value)) {
if (method_exists($value, 'toArray')) {
$data = $value->toArray();
return is_array($data) ? $data : null;
}
$json = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($json === false) {
return null;
}
$data = json_decode($json, true);
return is_array($data) ? $data : null;
}
return null;
}
/**
* 统一格式化时间值。
*/
private static function formatTemporalValue(mixed $value, string $pattern, string $emptyText): string
{
if ($value === null || $value === '') {
return $emptyText;
}
if (is_string($value)) {
$text = trim($value);
return $text === '' ? $emptyText : $text;
}
if ($value instanceof DateTimeInterface) {
return $value->format($pattern);
}
if (is_object($value) && method_exists($value, 'format')) {
return $value->format($pattern);
}
return (string) $value;
}
}

View File

@@ -0,0 +1,254 @@
<?php
namespace app\common\util;
use Firebase\JWT\ExpiredException;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use Firebase\JWT\SignatureInvalidException;
use support\Redis;
use Throwable;
/**
* JWT 工具类,负责签发、验证和撤销登录态。
*
* 设计说明:
* - JWT 只负责承载身份声明,不保存业务权限细节。
* - Redis 保存会话态,支持主动注销、过期控制和最近访问时间更新。
* - guard 用于区分不同登录域,例如管理员和商户。
* - Redis Key 推荐由配置中的前缀 + jti 组成,例如:
* `mpay:auth:admin:{jti}`、`mpay:auth:merchant:{jti}`。
*/
class JwtTokenManager
{
/**
* 签发 JWT并把会话态写入 Redis。
*
* @param string $guard 认证域名称,例如 admin 或 merchant
* @param array $claims JWT 自定义声明,通常包含主体 ID 等核心身份信息
* @param array $sessionData Redis 会话数据,通常包含用户展示信息和登录上下文
* @param int|null $ttlSeconds 有效期,单位秒;为空时使用 guard 默认 TTL
* @return array{token:string,expires_in:int,jti:string,claims:array,session:array}
*/
public function issue(string $guard, array $claims, array $sessionData, ?int $ttlSeconds = null): array
{
$guardConfig = $this->guardConfig($guard);
$this->assertHmacSecretLength($guard, (string) $guardConfig['secret']);
$ttlSeconds = max(60, $ttlSeconds ?? (int) $guardConfig['ttl']);
$now = time();
$jti = bin2hex(random_bytes(16));
$payload = array_merge([
'iss' => (string) config('auth.issuer', 'mpay'),
'iat' => $now,
'nbf' => $now,
'exp' => $now + $ttlSeconds,
'jti' => $jti,
'guard' => $guard,
], $claims);
$token = JWT::encode($payload, (string) $guardConfig['secret'], 'HS256');
$session = array_merge($sessionData, [
'guard' => $guard,
'jti' => $jti,
'issued_at' => FormatHelper::timestamp($now),
'expires_at' => FormatHelper::timestamp($now + $ttlSeconds),
]);
$this->storeSession($guard, $jti, $session, $ttlSeconds);
return [
'token' => $token,
'expires_in' => $ttlSeconds,
'jti' => $jti,
'claims' => $payload,
'session' => $session,
];
}
/**
* 验证 JWT并恢复对应的 Redis 会话数据。
*
* 说明:
* - 先校验签名和过期时间。
* - 再通过 jti 反查 Redis 会话,确保 token 仍然有效。
* - 每次命中会刷新最近访问时间。
*
* @return array{claims:array,session:array}|null
*/
public function verify(string $guard, string $token, string $ip = '', string $userAgent = ''): ?array
{
$payload = $this->decode($guard, $token);
if ($payload === null) {
return null;
}
$jti = (string) ($payload['jti'] ?? '');
if ($jti === '') {
return null;
}
$session = $this->session($guard, $jti);
if ($session === null) {
return null;
}
$now = time();
$expiresAt = (int) ($payload['exp'] ?? 0);
$ttl = max(1, $expiresAt - $now);
$session['last_used_at'] = FormatHelper::timestamp($now);
if ($ip !== '') {
$session['last_used_ip'] = $ip;
}
if ($userAgent !== '') {
$session['user_agent'] = $userAgent;
}
$this->storeSession($guard, $jti, $session, $ttl);
return [
'claims' => $payload,
'session' => $session,
];
}
/**
* 通过 token 撤销登录态。
*
* 适用于主动退出登录场景。
*/
public function revoke(string $guard, string $token): bool
{
$payload = $this->decode($guard, $token);
if ($payload === null) {
return false;
}
$jti = (string) ($payload['jti'] ?? '');
if ($jti === '') {
return false;
}
return (bool) Redis::connection()->del($this->sessionKey($guard, $jti));
}
/**
* 通过 jti 直接撤销登录态。
*
* 适用于已经掌握会话标识但没有原始 token 的补偿清理场景。
*/
public function revokeByJti(string $guard, string $jti): bool
{
if ($jti === '') {
return false;
}
return (bool) Redis::connection()->del($this->sessionKey($guard, $jti));
}
/**
* 根据 jti 获取会话数据。
*
* 返回值来自 Redis若已过期或不存在则返回 null。
*/
public function session(string $guard, string $jti): ?array
{
$raw = Redis::connection()->get($this->sessionKey($guard, $jti));
if (!is_string($raw) || $raw === '') {
return null;
}
$session = json_decode($raw, true);
return is_array($session) ? $session : null;
}
/**
* 解码并校验 JWT。
*
* 只做签名、过期和 guard 校验,不处理 Redis 会话。
*/
protected function decode(string $guard, string $token): ?array
{
$guardConfig = $this->guardConfig($guard);
$this->assertHmacSecretLength($guard, (string) $guardConfig['secret']);
try {
JWT::$leeway = (int) config('auth.leeway', 30);
$payload = JWT::decode($token, new Key((string) $guardConfig['secret'], 'HS256'));
} catch (ExpiredException|SignatureInvalidException|Throwable) {
return null;
}
$data = json_decode(json_encode($payload, JSON_UNESCAPED_UNICODE), true);
if (!is_array($data)) {
return null;
}
if (($data['guard'] ?? '') !== $guard) {
return null;
}
return $data;
}
/**
* 将会话数据写入 Redis并设置 TTL。
*/
protected function storeSession(string $guard, string $jti, array $session, int $ttlSeconds): void
{
Redis::connection()->setEx(
$this->sessionKey($guard, $jti),
max(60, $ttlSeconds),
json_encode($session, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
);
}
/**
* 构造 Redis 会话键。
*
* 最终格式由 guard 对应的 redis_prefix 加上 jti 组成。
*/
protected function sessionKey(string $guard, string $jti): string
{
return $this->guardConfig($guard)['redis_prefix'] . $jti;
}
/**
* 获取指定认证域的配置。
*
* @throws \InvalidArgumentException 当 guard 未配置时抛出
*/
protected function guardConfig(string $guard): array
{
$guards = (array) config('auth.guards', []);
if (!isset($guards[$guard])) {
throw new \InvalidArgumentException("Unknown auth guard: {$guard}");
}
return $guards[$guard];
}
/**
* 校验 HS256 密钥长度,避免 firebase/php-jwt 抛出底层异常。
*/
protected function assertHmacSecretLength(string $guard, string $secret): void
{
if (strlen($secret) >= 32) {
return;
}
$envNames = match ($guard) {
'admin' => 'AUTH_ADMIN_JWT_SECRET or AUTH_JWT_SECRET',
'merchant' => 'AUTH_MERCHANT_JWT_SECRET or AUTH_JWT_SECRET',
default => 'the configured JWT secret',
};
throw new \RuntimeException(sprintf(
'JWT secret for guard "%s" is too short for HS256. Please set %s to at least 32 ASCII characters.',
$guard,
$envNames
));
}
}