Compare commits
2 Commits
main
...
feat/app-a
| Author | SHA1 | Date | |
|---|---|---|---|
| 49c4ce8183 | |||
| 9a1c922642 |
@@ -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()
|
public function follow()
|
||||||
{
|
{
|
||||||
$data = $this->services->follow();
|
$data = $this->services->follow();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use app\services\user\UserServices;
|
|||||||
use app\services\user\UserVisitServices;
|
use app\services\user\UserVisitServices;
|
||||||
use crmeb\services\CacheService;
|
use crmeb\services\CacheService;
|
||||||
use crmeb\services\CacheService as Cache;
|
use crmeb\services\CacheService as Cache;
|
||||||
|
use crmeb\services\wechat\config\OpenAppConfig;
|
||||||
use crmeb\services\wechat\OfficialAccount;
|
use crmeb\services\wechat\OfficialAccount;
|
||||||
use crmeb\services\wechat\Work;
|
use crmeb\services\wechat\Work;
|
||||||
use crmeb\utils\Canvas;
|
use crmeb\utils\Canvas;
|
||||||
@@ -419,6 +420,159 @@ class WechatServices extends BaseServices
|
|||||||
return $token;
|
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
|
* @param int $uid
|
||||||
|
|||||||
@@ -453,8 +453,11 @@ class PayClient extends BaseClient
|
|||||||
*/
|
*/
|
||||||
public function configForAppPayment(string $prepayId): array
|
public function configForAppPayment(string $prepayId): array
|
||||||
{
|
{
|
||||||
|
/* ⚠️ App 支付必须用「开放平台移动应用」的 appid(appAppid),
|
||||||
|
不能用公众号 appid(wechatAppid)—— 下单用的是 appAppid(见 appPay()),
|
||||||
|
两边不一致会导致客户端调起微信支付时报 appid/签名不匹配(2026-10-26 修复)。 */
|
||||||
$params = [
|
$params = [
|
||||||
'appid' => $this->config->wechatAppid,
|
'appid' => $this->config->appAppid,
|
||||||
'partnerid' => $this->config->mchId,
|
'partnerid' => $this->config->mchId,
|
||||||
'prepayid' => $prepayId,
|
'prepayid' => $prepayId,
|
||||||
'noncestr' => uniqid(),
|
'noncestr' => uniqid(),
|
||||||
|
|||||||
@@ -441,6 +441,10 @@ Route::group('api', function () {
|
|||||||
Route::post('card/gift_info', 'v1.activity.CardGift/giftInfo')->name('礼品卡详情');//礼品卡详情
|
Route::post('card/gift_info', 'v1.activity.CardGift/giftInfo')->name('礼品卡详情');//礼品卡详情
|
||||||
Route::post('card/receive', 'v1.activity.CardGift/receive')->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);
|
})->middleware(StationOpenMiddleware::class)->middleware(AuthTokenMiddleware::class, true);
|
||||||
|
|||||||
Reference in New Issue
Block a user