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

<?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,
],
]);
}
}