You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
180 lines
5.8 KiB
180 lines
5.8 KiB
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Application;
|
|
use App\Models\Competition;
|
|
use App\Models\User;
|
|
use App\Services\BtomSmsService;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class AuthSmsController extends Controller
|
|
{
|
|
private function cacheKey(string $mobile): string
|
|
{
|
|
return 'sms_'.$mobile;
|
|
}
|
|
|
|
/** 日志中脱敏手机号 */
|
|
private function maskMobile(string $mobile): string
|
|
{
|
|
if (strlen($mobile) < 11) {
|
|
return '***';
|
|
}
|
|
|
|
return substr($mobile, 0, 3).'****'.substr($mobile, -4);
|
|
}
|
|
|
|
public function send(Request $request, BtomSmsService $sms): JsonResponse
|
|
{
|
|
$data = $request->validate([
|
|
'mobile' => ['required', 'regex:/^1[3-9]\d{9}$/'],
|
|
]);
|
|
|
|
$mobile = $data['mobile'];
|
|
$key = $this->cacheKey($mobile);
|
|
$check = Cache::get($key);
|
|
|
|
Log::info('sms.send hit', [
|
|
'mobile_mask' => $this->maskMobile($mobile),
|
|
'ip' => $request->ip(),
|
|
'user_agent' => substr((string) $request->userAgent(), 0, 200),
|
|
]);
|
|
|
|
$resendSec = (int) config('sms.resend_interval_seconds', 60);
|
|
if (is_array($check) && isset($check['time']) && (time() - (int) $check['time']) <= $resendSec) {
|
|
Log::warning('sms.send rate_limited', [
|
|
'mobile_mask' => $this->maskMobile($mobile),
|
|
'last_send_at' => $check['time'] ?? null,
|
|
'resend_interval_seconds' => $resendSec,
|
|
]);
|
|
throw ValidationException::withMessages([
|
|
'mobile' => ['请勿频繁发送'],
|
|
]);
|
|
}
|
|
|
|
$code = (string) random_int(100000, 999999);
|
|
$smsSign = (string) config('sms.sign');
|
|
$content = "{$smsSign}您的验证码是:{$code},验证码五分钟内有效,如非本人操作,请忽略。";
|
|
|
|
$useMock = config('sms.mock')
|
|
|| config('sms.skip_gateway')
|
|
|| (config('app.debug') && empty(config('sms.app_id')) && empty(config('sms.secret')));
|
|
|
|
Log::info('sms.send branch', [
|
|
'mobile_mask' => $this->maskMobile($mobile),
|
|
'use_mock' => $useMock,
|
|
'sms_mock_config' => (bool) config('sms.mock'),
|
|
'sms_skip_gateway' => (bool) config('sms.skip_gateway'),
|
|
'app_debug' => (bool) config('app.debug'),
|
|
'has_sms_credentials' => config('sms.app_id') !== '' && config('sms.secret') !== '',
|
|
]);
|
|
|
|
if ($useMock) {
|
|
Cache::put($key, ['code' => $code, 'time' => time()], 300);
|
|
|
|
Log::info('sms.send mock_ok cache_set', [
|
|
'mobile_mask' => $this->maskMobile($mobile),
|
|
'code' => $code,
|
|
'ttl_sec' => 300,
|
|
]);
|
|
|
|
return response()->json([
|
|
'message' => '发送成功',
|
|
'debug_code' => $code,
|
|
]);
|
|
}
|
|
|
|
Log::info('sms.send gateway_call', ['mobile_mask' => $this->maskMobile($mobile)]);
|
|
|
|
$result = $sms->sendContent($mobile, $content);
|
|
if (! $result) {
|
|
Log::warning('sms.send gateway_failed', ['mobile_mask' => $this->maskMobile($mobile)]);
|
|
|
|
return response()->json([
|
|
'message' => '发送失败',
|
|
], 422);
|
|
}
|
|
|
|
Cache::put($key, ['code' => $code, 'time' => time()], 300);
|
|
|
|
Log::info('sms.send gateway_ok cache_set', [
|
|
'mobile_mask' => $this->maskMobile($mobile),
|
|
'debug_code_logged' => config('app.debug') ? $code : null,
|
|
]);
|
|
|
|
return response()->json([
|
|
'message' => '发送成功',
|
|
'debug_code' => config('app.debug') ? $code : null,
|
|
]);
|
|
}
|
|
|
|
public function login(Request $request): JsonResponse
|
|
{
|
|
$data = $request->validate([
|
|
'mobile' => ['required', 'regex:/^1[3-9]\d{9}$/'],
|
|
'code' => ['required', 'string', 'max:64'],
|
|
'competition_slug' => ['required', 'string', 'max:64'],
|
|
]);
|
|
|
|
$key = $this->cacheKey($data['mobile']);
|
|
$cached = Cache::get($key);
|
|
|
|
if (! is_array($cached) || ($cached['code'] ?? null) !== $data['code']) {
|
|
throw ValidationException::withMessages([
|
|
'code' => ['验证码无效或已过期'],
|
|
]);
|
|
}
|
|
|
|
$user = DB::transaction(function () use ($data, $key) {
|
|
Cache::forget($key);
|
|
|
|
$competition = Competition::query()
|
|
->where('slug', $data['competition_slug'])
|
|
->where('published', true)
|
|
->first();
|
|
|
|
if ($competition === null) {
|
|
throw ValidationException::withMessages([
|
|
'competition_slug' => ['赛事不存在或未发布'],
|
|
]);
|
|
}
|
|
|
|
$user = User::query()->firstOrCreate(
|
|
['mobile' => $data['mobile']],
|
|
['name' => null, 'email' => null, 'password' => null]
|
|
);
|
|
|
|
Application::query()->firstOrCreate(
|
|
[
|
|
'user_id' => $user->id,
|
|
'competition_id' => $competition->id,
|
|
],
|
|
['status' => 'draft']
|
|
);
|
|
|
|
return $user;
|
|
});
|
|
|
|
$user->tokens()->delete();
|
|
$token = $user->createToken('web')->plainTextToken;
|
|
|
|
return response()->json([
|
|
'token' => $token,
|
|
'token_type' => 'Bearer',
|
|
'user' => [
|
|
'id' => $user->id,
|
|
'mobile' => $user->mobile,
|
|
'name' => $user->name,
|
|
'email' => $user->email,
|
|
],
|
|
]);
|
|
}
|
|
}
|