diff --git a/app/controller/api/v1/wechat/Wechat.php b/app/controller/api/v1/wechat/Wechat.php index 7aa48d2..c6ed184 100644 --- a/app/controller/api/v1/wechat/Wechat.php +++ b/app/controller/api/v1/wechat/Wechat.php @@ -147,6 +147,34 @@ class Wechat } } + /** + * App 微信绑定(需登录) + * body: code(uni.login provider=weixin 拿到的 code) + * 返回 {openid, unionid}:绑定到当前登录 uid + * + * @param Request $request + * @return mixed + */ + public function appBind(Request $request) + { + [$code] = $request->postMore([['code', '']], true); + $data = $this->services->appBindCode((int)$request->uid(), (string)$code); + return app('json')->success('绑定成功', $data); + } + + /** + * App 取规范身份(需登录) + * 返回 {openid, unionid, source}:routine 优先,其次 app,最后按 unionid 反查 + * + * @param Request $request + * @return mixed + */ + public function appIdentity(Request $request) + { + $data = $this->services->appIdentity((int)$request->uid()); + return app('json')->success('ok', $data); + } + public function follow() { $data = $this->services->follow(); diff --git a/app/services/wechat/WechatServices.php b/app/services/wechat/WechatServices.php index ea02c43..2269044 100644 --- a/app/services/wechat/WechatServices.php +++ b/app/services/wechat/WechatServices.php @@ -18,6 +18,7 @@ use app\services\user\UserServices; use app\services\user\UserVisitServices; use crmeb\services\CacheService; use crmeb\services\CacheService as Cache; +use crmeb\services\wechat\config\OpenAppConfig; use crmeb\services\wechat\OfficialAccount; use crmeb\services\wechat\Work; use crmeb\utils\Canvas; @@ -419,6 +420,159 @@ class WechatServices extends BaseServices return $token; } + /** + * App 微信绑定:code → openid/unionid,并绑定到当前 uid + * + * App 端 uni.login({provider:'weixin'}) 拿 code;服务端用「开放平台移动应用」 + * 的 appid/secret 调 sns/oauth2/access_token(与小程序 code2session 不是同一套)。 + * + * ⚠️ wechat_user.openid 是唯一索引: + * · 同一 uid 重复绑定 → 更新 unionid; + * · 已被其他 uid 占用 → 报错,不静默抢绑(否则会把别人的账号顶掉)。 + * + * @param int $uid + * @param string $code + * @return array{openid:string,unionid:string} + */ + public function appBindCode(int $uid, string $code): array + { + if ($uid <= 0) { + throw new ValidateException('未登录'); + } + $code = trim($code); + if ($code === '') { + throw new ValidateException('缺少微信授权 code'); + } + /** @var OpenAppConfig $cfg */ + $cfg = app()->make(OpenAppConfig::class); + if (!$cfg->appId || !$cfg->secret) { + throw new ValidateException('未配置开放平台移动应用的 appid/secret'); + } + $url = 'https://api.weixin.qq.com/sns/oauth2/access_token?' . http_build_query([ + 'appid' => $cfg->appId, + 'secret' => $cfg->secret, + 'code' => $code, + 'grant_type' => 'authorization_code', + ]); + $res = $this->appHttpGetJson($url); + if (empty($res['openid'])) { + $msg = (string)($res['errmsg'] ?? 'code 无效或已过期'); + Log::warning('[app_bind] 换 openid 失败 uid=' . $uid . ' err=' . $msg); + throw new ValidateException('微信授权失败:' . $msg); + } + $openid = (string)$res['openid']; + $unionid = (string)($res['unionid'] ?? ''); + + $row = $this->dao->getOne(['openid' => $openid]); + if ($row && (int)$row['uid'] !== $uid) { + throw new ValidateException('该微信已绑定其他账号,请先在原账号解绑'); + } + $data = [ + 'uid' => $uid, + 'openid' => $openid, + 'unionid' => $unionid, + 'user_type' => 'app', + 'is_del' => 0, + 'add_time' => time(), + ]; + if ($row) { + $this->dao->update((int)$row['id'], $data, 'id'); + } else { + $this->dao->save($data); + } + Log::info('[app_bind] uid=' . $uid . ' openid=' . substr($openid, 0, 6) + . '… unionid=' . ($unionid ? 'yes' : 'no')); + return ['openid' => $openid, 'unionid' => $unionid]; + } + + /** + * App 取规范身份(宿主注入 H5 用) + * + * 规则(2026-10-26): + * ① 当前 uid 有 routine(小程序)openid → 用它(老用户数据连续) + * ② 否则当前 uid 有 app openid → 用它 + * ③ 否则若 app 的 unionid 能命中**其他 uid** 的 routine openid → 用那个 + * (同一自然人先用小程序、后来用手机号注册 App 的场景) + * ④ 都没有 → openid 空,前端引导微信授权 + * + * @param int $uid + * @return array{openid:string,unionid:string,source:string} + */ + public function appIdentity(int $uid): array + { + $empty = ['openid' => '', 'unionid' => '', 'source' => 'none']; + if ($uid <= 0) { + return $empty; + } + $rows = $this->dao->getList(['uid' => $uid, 'is_del' => 0], 'id,openid,unionid,user_type,add_time', 'id desc'); + $routine = null; + $app = null; + foreach ($rows as $r) { + if (!$routine && $r['user_type'] === 'routine' && $r['openid']) { + $routine = $r; + } + if (!$app && $r['user_type'] === 'app' && $r['openid']) { + $app = $r; + } + if ($routine && $app) { + break; + } + } + if ($routine) { + return [ + 'openid' => (string)$routine['openid'], + 'unionid' => (string)($routine['unionid'] ?: ($app['unionid'] ?? '')), + 'source' => 'routine', + ]; + } + if ($app) { + $unionid = (string)$app['unionid']; + if ($unionid) { + $other = $this->dao->getOne( + ['unionid' => $unionid, 'user_type' => 'routine', 'is_del' => 0], + 'openid,unionid' + ); + if ($other && !empty($other['openid'])) { + return [ + 'openid' => (string)$other['openid'], + 'unionid' => $unionid, + 'source' => 'routine-unionid', + ]; + } + } + return ['openid' => (string)$app['openid'], 'unionid' => $unionid, 'source' => 'app']; + } + return $empty; + } + + /** + * 微信接口 GET(curl;5s 连接 / 8s 总超时) + * @param string $url + * @return array + */ + private function appHttpGetJson(string $url): array + { + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_TIMEOUT => 8, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, + ]); + $body = curl_exec($ch); + $err = curl_error($ch); + curl_close($ch); + if ($body === false) { + throw new ValidateException('微信接口请求失败:' . $err); + } + $json = json_decode((string)$body, true); + if (!is_array($json)) { + throw new ValidateException('微信接口返回异常'); + } + return $json; + } + /** * 是否关注 * @param int $uid diff --git a/route/api.php b/route/api.php index 8144f58..3564558 100644 --- a/route/api.php +++ b/route/api.php @@ -441,6 +441,10 @@ Route::group('api', function () { Route::post('card/gift_info', 'v1.activity.CardGift/giftInfo')->name('礼品卡详情');//礼品卡详情 Route::post('card/receive', 'v1.activity.CardGift/receive')->name('礼品卡兑换');//礼品卡兑换 + //App 身份(2026-10-26):绑定微信拿 unionid / 取宿主注入用的规范 openid + Route::post('wechat/app_bind', 'v1.wechat.Wechat/appBind')->name('appBind');//App 绑定微信(code 换 openid/unionid) + Route::get('wechat/app_identity', 'v1.wechat.Wechat/appIdentity')->name('appIdentity');//App 取规范身份 + })->middleware(StationOpenMiddleware::class)->middleware(AuthTokenMiddleware::class, true);