MpAuth.php 11.1 KB
<?php
declare(strict_types=1);

namespace app\controller\api;

use app\BaseController;
use app\service\WechatMini;
use app\service\AuthContext;
use Throwable;
use think\facade\Db;
use think\response\Json;

/**
 * 小程序登录:游客 / 微信 code;手机号注册与登录
 */
class MpAuth extends BaseController
{
    private function issueTokenJson(int $uid): Json
    {
        $now = date('Y-m-d H:i:s');
        $user = Db::name('mp_user')->find($uid);
        if (!$user) {
            return json(['code' => 500, 'msg' => '用户不存在', 'data' => []]);
        }
        if ((int) ($user['status'] ?? 1) !== 1) {
            return json(['code' => 403, 'msg' => '账号已被禁用', 'data' => []]);
        }

        $token = bin2hex(random_bytes(32));
        // 业务上长期有效(库字段仍为 datetime,写远期时间便于排查;鉴权不再依赖该字段)
        $expire = '2099-12-31 23:59:59';
        Db::name('mp_user')->where('id', $uid)->update([
            'api_token'    => $token,
            'token_expire' => $expire,
            'update_time'  => $now,
        ]);
        $user = Db::name('mp_user')->find($uid);

        return json([
            'code' => 0,
            'msg'  => 'ok',
            'data' => [
                'token'     => $token,
                'client_id' => $user['client_id'],
                'user'      => [
                    'id'       => (int) $user['id'],
                    'nickname' => $user['nickname'],
                    'avatar'   => $user['avatar'],
                    'bio'      => $user['bio'],
                    'gender'   => (int) $user['gender'],
                    'phone'    => trim((string) ($user['phone'] ?? '')),
                ],
            ],
        ]);
    }

    public function bootstrap()
    {
        try {
            $clientId = trim((string) $this->request->post('client_id', ''));
            $wxCode   = trim((string) $this->request->post('wx_code', ''));
            $wxNickname = trim((string) $this->request->post('wx_nickname', ''));
            $wxAvatar   = trim((string) $this->request->post('wx_avatar', ''));
            $wxGender   = (int) $this->request->post('wx_gender', 0);
            $now      = date('Y-m-d H:i:s');

            $uid = 0;

            if ($wxCode !== '') {
                if (mb_strlen($wxNickname) > 64) {
                    $wxNickname = mb_substr($wxNickname, 0, 64);
                }
                $sess = WechatMini::code2Session($wxCode);
                if (!$sess['ok']) {
                    return json(['code' => 400, 'msg' => $sess['msg'], 'data' => []]);
                }
                $openid     = $sess['openid'];
                $unionid    = (string) ($sess['unionid'] ?? '');
                $sessionKey = (string) ($sess['session_key'] ?? '');

                $exist = Db::name('mp_user')->where('openid', $openid)->find();
                if ($exist) {
                    $uid = (int) $exist['id'];
                    $up  = ['update_time' => $now];
                    if ($sessionKey !== '') {
                        $up['session_key'] = $sessionKey;
                    }
                    if ($unionid !== '' && empty($exist['unionid'])) {
                        $up['unionid'] = $unionid;
                    }
                    // 有真实微信资料时更新;无资料不阻断登录
                    if ($wxNickname !== '' && !in_array($wxNickname, ['微信用户', '游客'], true)) {
                        $up['nickname'] = $wxNickname;
                    }
                    if ($wxAvatar !== '') {
                        $up['avatar'] = $wxAvatar;
                    }
                    $up['gender'] = $wxGender;
                    if (count($up) > 1) {
                        Db::name('mp_user')->where('id', $uid)->update($up);
                    }
                } else {
                    $bound = false;
                    if ($clientId !== '') {
                        $guest = Db::name('mp_user')->where('client_id', $clientId)->find();
                        if ($guest && empty($guest['openid'])) {
                            $uid = (int) $guest['id'];
                            Db::name('mp_user')->where('id', $uid)->update([
                                'openid'      => $openid,
                                'unionid'     => $unionid !== '' ? $unionid : (string) ($guest['unionid'] ?? ''),
                                'session_key' => $sessionKey,
                                'nickname'    => ($wxNickname !== '' && !in_array($wxNickname, ['微信用户', '游客'], true))
                                    ? $wxNickname
                                    : (string) ($guest['nickname'] ?? '微信用户'),
                                'avatar'      => $wxAvatar !== '' ? $wxAvatar : (string) ($guest['avatar'] ?? ''),
                                'gender'      => $wxGender,
                                'update_time' => $now,
                            ]);
                            $bound = true;
                        }
                    }
                    if (!$bound) {
                        if ($clientId === '') {
                            $clientId = 'cid_' . bin2hex(random_bytes(10));
                        } elseif (Db::name('mp_user')->where('client_id', $clientId)->count() > 0) {
                            $clientId = 'cid_' . bin2hex(random_bytes(10));
                        }
                        $uid = Db::name('mp_user')->insertGetId([
                            'client_id'    => $clientId,
                            'openid'       => $openid,
                            'unionid'      => $unionid,
                            'session_key'  => $sessionKey,
                            'nickname'     => ($wxNickname !== '' && !in_array($wxNickname, ['微信用户', '游客'], true))
                                ? $wxNickname
                                : '微信用户',
                            'avatar'       => $wxAvatar,
                            'gender'       => $wxGender,
                            'create_time'  => $now,
                            'update_time'  => $now,
                        ]);
                    }
                }
            } else {
                if ($clientId === '') {
                    $clientId = 'cid_' . bin2hex(random_bytes(12));
                    $uid      = Db::name('mp_user')->insertGetId([
                        'client_id'    => $clientId,
                        'nickname'     => '游客',
                        'create_time'  => $now,
                        'update_time'  => $now,
                    ]);
                } else {
                    $row = Db::name('mp_user')->where('client_id', $clientId)->find();
                    if (!$row) {
                        $uid = Db::name('mp_user')->insertGetId([
                            'client_id'    => $clientId,
                            'nickname'     => '游客',
                            'create_time'  => $now,
                            'update_time'  => $now,
                        ]);
                    } else {
                        $uid = (int) $row['id'];
                    }
                }
            }

            if ($uid <= 0) {
                return json(['code' => 500, 'msg' => '用户创建失败', 'data' => []]);
            }

            return $this->issueTokenJson($uid);
        } catch (Throwable $e) {
            $msg = '微信登录失败';
            if ((bool) env('APP_DEBUG', false)) {
                $msg .= ':' . $e->getMessage();
            }

            return json(['code' => 500, 'msg' => $msg, 'data' => []]);
        }
    }

    /** 微信手机号授权绑定(需已登录) */
    public function bindPhone(): Json
    {
        $u = AuthContext::mpUser();
        if (!$u) {
            return json(['code' => 401, 'msg' => '未授权', 'data' => []]);
        }
        $phoneCode = trim((string) $this->request->post('phone_code', ''));
        if ($phoneCode === '') {
            return json(['code' => 400, 'msg' => '缺少手机号授权参数', 'data' => []]);
        }
        $res = WechatMini::getPhoneByCode($phoneCode);
        if (!$res['ok']) {
            return json(['code' => 400, 'msg' => $res['msg'], 'data' => []]);
        }
        $phone = trim((string) ($res['phone'] ?? ''));
        if ($phone === '') {
            return json(['code' => 400, 'msg' => '未获取到手机号', 'data' => []]);
        }
        $uid = (int) $u['id'];
        $exist = Db::name('mp_user')->where('phone', $phone)->where('id', '<>', $uid)->find();
        if ($exist) {
            return json(['code' => 400, 'msg' => '该手机号已绑定其他账号', 'data' => []]);
        }
        Db::name('mp_user')->where('id', $uid)->update([
            'phone' => $phone,
            'update_time' => date('Y-m-d H:i:s'),
        ]);
        return json(['code' => 0, 'msg' => 'ok', 'data' => ['phone' => $phone]]);
    }

    /** 手机号注册:需 password 与 password_confirm 一致 */
    public function register(): Json
    {
        $phone    = trim((string) $this->request->post('phone', ''));
        $password = (string) $this->request->post('password', '');
        $confirm  = (string) $this->request->post('password_confirm', '');

        if (!preg_match('/^1\d{10}$/', $phone)) {
            return json(['code' => 400, 'msg' => '请输入正确的11位手机号', 'data' => []]);
        }
        if (strlen($password) < 6) {
            return json(['code' => 400, 'msg' => '密码至少6位', 'data' => []]);
        }
        if ($password !== $confirm) {
            return json(['code' => 400, 'msg' => '两次密码不一致', 'data' => []]);
        }
        if (Db::name('mp_user')->where('phone', $phone)->count() > 0) {
            return json(['code' => 400, 'msg' => '该手机号已注册', 'data' => []]);
        }

        $now      = date('Y-m-d H:i:s');
        $clientId = 'cid_' . bin2hex(random_bytes(12));
        $suffix   = substr($phone, -4);
        $uid      = Db::name('mp_user')->insertGetId([
            'client_id'    => $clientId,
            'phone'        => $phone,
            'pwd_hash'     => password_hash($password, PASSWORD_DEFAULT),
            'nickname'     => '用户' . $suffix,
            'create_time'  => $now,
            'update_time'  => $now,
        ]);

        return $this->issueTokenJson((int) $uid);
    }

    /** 手机号 + 密码登录 */
    public function loginPhone(): Json
    {
        $phone    = trim((string) $this->request->post('phone', ''));
        $password = (string) $this->request->post('password', '');

        if (!preg_match('/^1\d{10}$/', $phone)) {
            return json(['code' => 400, 'msg' => '请输入正确的11位手机号', 'data' => []]);
        }
        if ($password === '') {
            return json(['code' => 400, 'msg' => '请输入密码', 'data' => []]);
        }

        $user = Db::name('mp_user')->where('phone', $phone)->find();
        $hash = (string) ($user['pwd_hash'] ?? '');
        if (!$user || $hash === '' || !password_verify($password, $hash)) {
            return json(['code' => 400, 'msg' => '账号或密码错误', 'data' => []]);
        }

        return $this->issueTokenJson((int) $user['id']);
    }
}