| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273 |
- <?php
- declare(strict_types=1);
- namespace app\miniapi\service;
- use app\miniapi\auth\SmsCode;
- use app\miniapi\exception\ApiException;
- use app\miniapi\http\Fields;
- use app\miniapi\model\MiniUser;
- final class SmsLogin
- {
- public function __construct(private readonly SmsSender $sender = new SmsbaoSender())
- {
- }
- /**
- * Always answers with the same payload whether or not the phone belongs to an account.
- * Distinguishing the two cases would let a caller enumerate registered numbers, so an
- * unbound phone silently consumes nothing and receives no message.
- */
- public function sendCode(array $input, string $ip): array
- {
- if (!SmsCode::enabled()) {
- throw new ApiException(50301, '短信登录未开启', 503);
- }
- Fields::only($input, ['phone']);
- $phone = self::mobile($input);
- SmsCode::assertSendAllowed($phone, $ip);
- if (!MiniUser::query()->where('phone', $phone)->where('status', 1)->exists()) {
- return ['sent' => true, 'expires_in' => SmsCode::ttl()];
- }
- $code = SmsCode::issue($phone, $ip);
- try {
- $this->sender->send($phone, sprintf('您的验证码是 %s,%d 分钟内有效。请勿转发给他人。', $code, intdiv(SmsCode::ttl(), 60)));
- } catch (ApiException $error) {
- // A code the caller never received must not remain verifiable.
- SmsCode::discard($phone, $code);
- throw $error;
- }
- return ['sent' => true, 'expires_in' => SmsCode::ttl()];
- }
- public function login(array $input): array
- {
- if (!SmsCode::enabled()) {
- throw new ApiException(50301, '短信登录未开启', 503);
- }
- Fields::only($input, ['phone', 'code']);
- $phone = self::mobile($input);
- SmsCode::consume($phone, Fields::text($input, 'code', 6));
- $user = MiniUser::query()->where('phone', $phone)->first();
- if (!$user || (int) $user->status !== 1) {
- throw new ApiException(40102, '验证码已失效,请重新获取', 401);
- }
- return (new AuthService())->issueForExisting($user);
- }
- /** SMS gateways only reach mainland mobile numbers; other formats never leave the server. */
- public static function mobile(array $input): string
- {
- $phone = Fields::phone($input);
- if (!preg_match('/^1[3-9][0-9]{9}$/D', $phone)) {
- throw new ApiException(42201, '请填写有效的中国大陆手机号码', 422, ['phone' => '须为1开头的11位手机号']);
- }
- return $phone;
- }
- }
|