文件
fenleixinxi/application/api/controller/Auth.php
linda 297e1a7f79 feat(admin): 会员列表去角色组/邮箱/等级列,手机号全量展示;新增微信手机号快捷获取接口
需求:后台「会员列表」取消『角色组 / 电子邮箱 / 等级』三列;手机号要全部能取到并展示。

- user/user 列表:移除 group.name / email / level 三列,手机号列移到昵称之后并加格式化:
  有绑定号→直接显示;未绑定但有发布信息→显示该会员最近一条发布信息的联系电话并标注「来自发布信息」;两者都没有→显示「未绑定」
- user/user 控制器 index():改用 toArray() 后按行补 mobile_source,未绑定手机号时回落到 fc_post.phone(不含软删)
- 新增 A5 POST /api/v1/auth/wx-phone:用小程序 access_token 调 phonenumber.getPhoneNumber 换号写入 fa_user.mobile;
  access_token 带缓存(fc_config wx.accessToken/wx.accessTokenExpire,提前 5 分钟失效);微信侧失败时给出「请改用手动输入」
  的业务文案,前端据此回落 A2 bind-phone —— 修复「微信会员手机号取不到」的根因(此前小程序侧绑定是假实现)
- 路由登记:application/route.php 新增 api/v1/auth/wx-phone
- tools/verify-api.mjs:新增 A5(缺 code 文案)与 A2(手机号格式)检查
2026-10-16 09:33:56 +08:00

261 行
9.5 KiB
PHP

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<?php
namespace app\api\controller;
use think\Db;
/**
* 用户端 · 认证与登录(Module-AUTH)
* 路由:
* POST /api/v1/auth/wx-login A1 微信授权登录
* POST /api/v1/auth/bind-phone A2 手机号绑定(手动输入)
* POST /api/v1/auth/wx-phone A5 微信手机号快捷获取(getPhoneNumber)
* POST /api/v1/auth/switch-type A3 个人/商家身份切换
* GET /api/v1/auth/profile A4 个人资料
*/
class Auth extends V1
{
/**
* A1 微信一键授权登录
* 入参 { code } → 出参 { token, refreshToken, userInfo }
* 规则:code 必填;首次登录自动建档并生成邀请码
*/
public function wxLogin()
{
$code = trim((string)$this->input('code', ''));
if (!$code) {
$this->fail('缺少微信授权 code');
}
$openid = $this->resolveOpenid($code);
$ext = Db::name('fc_user_ext')->where('openid', $openid)->find();
$user = $ext ? Db::name('user')->where('id', $ext['user_id'])->find() : null;
if (!$user) {
// 首次登录:自动建号
$uid = Db::name('user')->insertGetId([
'group_id' => 1,
'username' => 'wx' . substr(md5($openid), 0, 10),
'nickname' => '同城用户' . mt_rand(1000, 9999),
'password' => md5(mt_rand()),
'salt' => 'wx',
'email' => substr(md5($openid), 0, 12) . '@wx.local',
'mobile' => '',
'avatar' => '',
'level' => 1,
'money' => 0,
'score' => 0,
'status' => 'normal',
'jointime' => time(),
'joinip' => $this->request->ip(),
'createtime' => time(),
'updatetime' => time(),
]);
Db::name('fc_user_ext')->insert([
'user_id' => $uid, 'openid' => $openid, 'user_type' => 1, 'realname_status' => 0,
'is_member' => 0, 'member_level' => '', 'member_expiretime' => null,
'invite_code' => strtoupper(substr(md5('fc' . $uid), 0, 6)), 'inviter_id' => 0,
'free_view_date' => date('Y-m-d'), 'free_view_used' => 0,
'createtime' => time(), 'updatetime' => time(),
]);
$user = Db::name('user')->where('id', $uid)->find();
}
if ($user['status'] != 'normal') {
$this->fail('账号已被禁用,请联系客服');
}
$token = $this->issueToken($user['id']);
$this->ok([
'token' => $token,
'refreshToken' => md5($token . 'refresh'),
'userInfo' => $this->userInfo($user),
]);
}
/**
* A2 手机号授权绑定
* 入参 { phone } → 出参 { phone }
* 规则 BR-02:必须匹配 ^1\d{10}$
*/
public function bindPhone()
{
$user = $this->auth();
$phone = trim((string)$this->input('phone', ''));
if (!preg_match('/^1\d{10}$/', $phone)) {
$this->fail('手机号格式不正确');
}
Db::name('user')->where('id', $user['id'])->update([
'mobile' => $phone, 'updatetime' => time(),
]);
$this->ok(['phone' => $phone]);
}
/**
* A5 微信手机号快捷获取
* 入参 { code }(`<button open-type="getPhoneNumber">` 回调里的 `e.detail.code`)
* → 出参 { phone }
* 规则:用小程序 access_token 调 phonenumber.getPhoneNumber 换号并写入 fa_user.mobile;
* 未配置微信参数 / 微信侧失败(快捷验证未开通、code 失效、用户拒绝)时返回业务失败,
* 前端据此回落到「手动输入手机号」(A2 bindPhone),保证任何情况下都能拿到号码。
*/
public function wxPhone()
{
$user = $this->auth();
$code = trim((string)$this->input('code', ''));
if ($code === '') {
$this->fail('缺少手机号授权 code');
}
$appid = (string)$this->cfg('wx.appid', '');
$secret = (string)$this->cfg('wx.secret', '');
if ($appid === '' || $secret === '') {
$this->fail('未配置小程序 AppID/AppSecret,请改用手动输入');
}
$token = $this->wxAccessToken($appid, $secret);
if ($token === '') {
$this->fail('微信 access_token 获取失败,请改用手动输入');
}
$ctx = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/json\r\n",
'content' => json_encode(['code' => $code]),
'timeout' => 10,
'ignore_errors' => true,
],
]);
$res = @json_decode(@file_get_contents(
'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=' . $token,
false,
$ctx
), true);
$phone = '';
if (!empty($res['phone_info']['phoneNumber'])) {
$phone = (string)$res['phone_info']['phoneNumber'];
} elseif (!empty($res['phone_info']['purePhoneNumber'])) {
$phone = (string)$res['phone_info']['purePhoneNumber'];
}
if (!preg_match('/^1\d{10}$/', $phone)) {
// 常见原因:手机号快捷验证未开通/额度用尽、code 已失效、用户拒绝授权
$this->fail('微信手机号获取失败,请改用手动输入');
}
Db::name('user')->where('id', $user['id'])->update([
'mobile' => $phone, 'updatetime' => time(),
]);
$this->ok(['phone' => $phone]);
}
/**
* 小程序 access_token(带缓存,提前 5 分钟失效)
* 缓存写入 fc_config 的 wx.accessToken / wx.accessTokenExpire(仅服务端读取,不下发前端)
* @param string $appid
* @param string $secret
* @return string 失败返回空串
*/
protected function wxAccessToken($appid, $secret)
{
$now = time();
$cached = (string)$this->cfg('wx.accessToken', '');
$expire = (int)$this->cfg('wx.accessTokenExpire', 0);
if ($cached !== '' && $expire > $now + 300) {
return $cached;
}
$url = 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential'
. '&appid=' . $appid . '&secret=' . $secret;
$res = @json_decode(@file_get_contents($url), true);
if (empty($res['access_token'])) {
return '';
}
$this->setCfg('wx.accessToken', (string)$res['access_token']);
$this->setCfg('wx.accessTokenExpire', $now + (int)(!empty($res['expires_in']) ? $res['expires_in'] : 7200));
return (string)$res['access_token'];
}
/**
* A3 用户类型切换
* 入参 { userType: 1|2, license? } → 出参 { userType, needAudit }
* 规则 BR-01:切换商家必须提交营业执照,提交后进入待审核
*/
public function switchType()
{
$user = $this->auth();
$userType = (int)$this->input('userType', 0);
if (!in_array($userType, [1, 2], true)) {
$this->fail('用户类型不合法');
}
if ($userType === 2) {
$license = trim((string)$this->input('license', ''));
if (!$license) {
$this->fail('商家用户需上传营业执照完成资质认证');
}
$name = trim((string)$this->input('merchantName', ''));
$exists = Db::name('fc_merchant')->where('user_id', $user['id'])->find();
$data = [
'user_id' => $user['id'],
'name' => $name !== '' ? $name : (string)$user['nickname'],
'license' => $license,
'audit_status' => 0,
'shop_status' => 'pending',
'updatetime' => time(),
];
if ($exists) {
Db::name('fc_merchant')->where('id', $exists['id'])->update($data);
} else {
$data['createtime'] = time();
Db::name('fc_merchant')->insert($data);
}
}
Db::name('fc_user_ext')->where('user_id', $user['id'])->update([
'user_type' => $userType, 'updatetime' => time(),
]);
$this->ok(['userType' => $userType, 'needAudit' => $userType === 2]);
}
/**
* A4 个人资料
* 返回 userInfo(含会员状态、余额)
*/
public function profile()
{
$user = $this->auth();
$this->ok($this->userInfo($user));
}
/**
* code -> openid
* 已配置微信参数时走 jscode2session;未配置(本地/联调)时用确定性派生,便于前后端联调。
*/
protected function resolveOpenid($code)
{
$appid = (string)$this->cfg('wx.appid', '');
$secret = (string)$this->cfg('wx.secret', '');
if ($appid && $secret) {
$url = 'https://api.weixin.qq.com/sns/jscode2session?appid=' . $appid
. '&secret=' . $secret . '&js_code=' . urlencode($code) . '&grant_type=authorization_code';
$res = @json_decode(@file_get_contents($url), true);
if (!empty($res['openid'])) {
return $res['openid'];
}
}
// 未配置微信参数:若 code 本身即为 openid 标识则直接采用,否则派生
if (strpos($code, 'openid_') === 0) {
return $code;
}
return 'openid_' . md5($code);
}
}