Compare commits
3 Commits
c7f85ffbda
...
feat/app-a
| Author | SHA1 | Date | |
|---|---|---|---|
| 49c4ce8183 | |||
| 9a1c922642 | |||
| fc353899a9 |
@@ -20,6 +20,7 @@ use crmeb\services\wechat\config\WorkConfig;
|
||||
use think\db\exception\DataNotFoundException;
|
||||
use think\db\exception\DbException;
|
||||
use think\db\exception\ModelNotFoundException;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 客户管理
|
||||
@@ -83,11 +84,29 @@ class Client extends AuthController
|
||||
*/
|
||||
public function synch()
|
||||
{
|
||||
if (!sys_config('wechat_work_corpid', '')) {
|
||||
$corpId = sys_config('wechat_work_corpid', '');
|
||||
if (!$corpId) {
|
||||
return $this->fail('请先配置企微信息');
|
||||
}
|
||||
try {
|
||||
$beforeCount = $this->services->count([]);
|
||||
Log::info('work client synch start ' . json_encode([
|
||||
'corp_id' => $corpId,
|
||||
'before_count' => $beforeCount
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('work client synch precheck error ' . json_encode([
|
||||
'corp_id' => $corpId,
|
||||
'message' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
$this->services->delete([["id", "<>", 0]]);
|
||||
$this->services->authGetExternalcontact();
|
||||
Log::info('work client synch dispatched ' . json_encode([
|
||||
'corp_id' => $corpId
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
return $this->success('已加入消息队列,请稍后查看');
|
||||
}
|
||||
|
||||
|
||||
134
app/controller/api/v1/InternalController.php
Normal file
134
app/controller/api/v1/InternalController.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: CRMEB Team <admin@crmeb.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
namespace app\controller\api\v1;
|
||||
|
||||
use app\Request;
|
||||
use app\services\user\UserServices;
|
||||
use app\services\wechat\RoutineServices;
|
||||
use app\services\wechat\WechatUserServices;
|
||||
use think\db\exception\DataNotFoundException;
|
||||
use think\db\exception\DbException;
|
||||
use think\db\exception\ModelNotFoundException;
|
||||
use think\exception\ValidateException;
|
||||
use think\Response;
|
||||
|
||||
/**
|
||||
* 内部服务接口(不经过用户认证)
|
||||
* 用于 H5 / app_03 后端通过 openid 自动补齐用户信息
|
||||
*/
|
||||
class InternalController
|
||||
{
|
||||
/**
|
||||
* 验证内部 API Key
|
||||
* @param Request $request
|
||||
* @return bool true=通过,false=拒绝
|
||||
*/
|
||||
private function checkInternalKey(Request $request): bool
|
||||
{
|
||||
$apiKey = $request->header('X-Internal-Key');
|
||||
// 注意:在 Swoole 沙箱里 env()/getenv() 经常读不到 .env
|
||||
// 用 config() 兜底,确保两种部署(PHP-FPM / Swoole)都生效
|
||||
$configKey = (string)(config('internal.api_key') ?: env('INTERNAL_API_KEY', ''));
|
||||
return $apiKey && $configKey && $apiKey === $configKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 openid 获取用户信息
|
||||
* @param Request $request
|
||||
* @param WechatUserServices $wechatUserServices
|
||||
* @return Response
|
||||
*/
|
||||
public function userInfo(Request $request, WechatUserServices $wechatUserServices): Response
|
||||
{
|
||||
if (!$this->checkInternalKey($request)) {
|
||||
return Response::create(['code' => 403, 'msg' => 'Forbidden'], 'json');
|
||||
}
|
||||
|
||||
$openid = $request->param('openid', '');
|
||||
if (!$openid) {
|
||||
return Response::create(['code' => 400, 'msg' => 'openid is required'], 'json');
|
||||
}
|
||||
|
||||
try {
|
||||
$wechatUser = $wechatUserServices->getWechatUserInfo(['openid' => $openid]);
|
||||
} catch (DataNotFoundException | ModelNotFoundException | DbException $e) {
|
||||
return Response::create(['code' => 500, 'msg' => '查询失败'], 'json');
|
||||
}
|
||||
if (!$wechatUser) {
|
||||
return Response::create(['code' => 404, 'msg' => 'user not found'], 'json');
|
||||
}
|
||||
|
||||
$uid = (int)$wechatUser['uid'];
|
||||
$userServices = app()->make(UserServices::class);
|
||||
try {
|
||||
$user = $userServices->getUserInfo($uid);
|
||||
} catch (DataNotFoundException | ModelNotFoundException | DbException $e) {
|
||||
$user = null;
|
||||
}
|
||||
|
||||
return Response::create([
|
||||
'code' => 200,
|
||||
'data' => [
|
||||
'uid' => $uid,
|
||||
'nickname' => $user['nickname'] ?? $wechatUser['nickname'] ?? '',
|
||||
'phone' => $user['phone'] ?? $wechatUser['phone'] ?? '',
|
||||
'avatar' => $user['avatar'] ?? $wechatUser['headimgurl'] ?? '',
|
||||
],
|
||||
], 'json');
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过微信 code 换 openid(绕过 store_user_mobile 限制)
|
||||
*
|
||||
* 设计目的:H5/app_03 在新用户场景下(CRMEB 强制绑手机号=true),
|
||||
* 原 silenceAuth 返回 {auth_login:1, key:...},前端拿不到 openid 会被
|
||||
* 引导到授权页。本端点只调微信 code2session 拿 openid/unionid,不查
|
||||
* store_user_mobile、不建用户、不发 token,前端拿到 openid 后走自己的
|
||||
* openid-login 即可自助登录。
|
||||
*
|
||||
* @param Request $request
|
||||
* @param RoutineServices $routineServices
|
||||
* @return Response
|
||||
*/
|
||||
public function getOpenidByCode(Request $request, RoutineServices $routineServices): Response
|
||||
{
|
||||
if (!$this->checkInternalKey($request)) {
|
||||
return Response::create(['code' => 403, 'msg' => 'Forbidden'], 'json');
|
||||
}
|
||||
|
||||
$code = $request->param('code', '');
|
||||
if (!$code) {
|
||||
return Response::create(['code' => 400, 'msg' => 'code is required'], 'json');
|
||||
}
|
||||
|
||||
try {
|
||||
$wechatInfo = $routineServices->getUserInfoByCode((string)$code);
|
||||
} catch (ValidateException $e) {
|
||||
return Response::create(['code' => 400, 'msg' => $e->getMessage()], 'json');
|
||||
} catch (\Throwable $e) {
|
||||
return Response::create(['code' => 500, 'msg' => 'code2session 失败: ' . $e->getMessage()], 'json');
|
||||
}
|
||||
|
||||
$openid = $wechatInfo['openid'] ?? '';
|
||||
if (!$openid) {
|
||||
return Response::create(['code' => 500, 'msg' => 'openid 缺失'], 'json');
|
||||
}
|
||||
|
||||
return Response::create([
|
||||
'code' => 200,
|
||||
'data' => [
|
||||
'openid' => $openid,
|
||||
'unionid' => $wechatInfo['unionid'] ?? '',
|
||||
],
|
||||
], 'json');
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -151,7 +151,13 @@ class Routine
|
||||
{
|
||||
$token = $this->services->silenceAuth($code, (int)$spread_code, (int)$spread_spid, true);
|
||||
if ($token && isset($token['auth_login'])) {
|
||||
return app('json')->success('授权成功');
|
||||
// 透传 openid/unionid 给小程序端,用于向 H5 注入身份参数
|
||||
return app('json')->success('授权成功', [
|
||||
'auth_login' => $token['auth_login'],
|
||||
'key' => $token['key'],
|
||||
'openid' => $token['openid'] ?? '',
|
||||
'unionid' => $token['unionid'] ?? '',
|
||||
]);
|
||||
} else if ($token) {
|
||||
return app('json')->success('登录成功', ['token' => $token['token'], 'userInfo' => $token['userInfo'], 'expires_time' => $token['params']['exp'], 'store_user_avatar' => $token['store_user_avatar'] ?? 0]);
|
||||
} else
|
||||
|
||||
@@ -85,7 +85,7 @@ abstract class HaichaoBridge
|
||||
$options['http'] = [
|
||||
'timeout' => 60,
|
||||
'method' => 'POST',
|
||||
'header' => 'Content-Type: application/json;charset=utf-8',
|
||||
'header' => 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8',
|
||||
'content' => http_build_query($baseBody)
|
||||
];
|
||||
$raw = file_get_contents("{$this->api['api']}$api", false, stream_context_create($options));
|
||||
@@ -174,12 +174,24 @@ abstract class HaichaoBridge
|
||||
|
||||
private function sign(array $data): string
|
||||
{
|
||||
$computerStr = "";
|
||||
foreach ($data as $k => $v) {
|
||||
$computerStr .= $k . $v;
|
||||
}
|
||||
$computerStr = $this->api['appSecret'] . $computerStr;
|
||||
return md5($computerStr);
|
||||
if (!$data) {
|
||||
return '';
|
||||
}
|
||||
|
||||
ksort($data);
|
||||
$str = '';
|
||||
foreach ($data as $k => $v) {
|
||||
if ($k === null || $k === '' || $k === 'sign' || $v === '' || $v === null) {
|
||||
continue;
|
||||
}
|
||||
if (is_array($v) || is_object($v)) {
|
||||
$v = json_encode($v, JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
$str .= $k . $v;
|
||||
}
|
||||
|
||||
$str = $this->api['appSecret'] . $str;
|
||||
return bin2hex(md5($str, true));
|
||||
}
|
||||
|
||||
private function isTokenError(array $rt): bool
|
||||
|
||||
@@ -18,6 +18,7 @@ use crmeb\traits\QueueTrait;
|
||||
use think\db\exception\DataNotFoundException;
|
||||
use think\db\exception\DbException;
|
||||
use think\db\exception\ModelNotFoundException;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 同步客户
|
||||
@@ -39,9 +40,17 @@ class WorkClientJob extends BaseJobs
|
||||
*/
|
||||
public function authClient($page, $cursor)
|
||||
{
|
||||
Log::info('work client job authClient start ' . json_encode([
|
||||
'page' => (int)$page,
|
||||
'cursor_len' => strlen((string)$cursor),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
/** @var WorkClientServices $make */
|
||||
$make = app()->make(WorkClientServices::class);
|
||||
$make->authGetExternalcontact($page, $cursor);
|
||||
Log::info('work client job authClient done ' . json_encode([
|
||||
'page' => (int)$page,
|
||||
'cursor_len' => strlen((string)$cursor),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -60,13 +60,15 @@ abstract class BaseServices
|
||||
* 创建token
|
||||
* @param int $id
|
||||
* @param $type
|
||||
* @param string $pwd
|
||||
* @param array $extras 额外字段,例如 ['phone' => '138...', 'openid' => 'o...']
|
||||
* @return array
|
||||
*/
|
||||
public function createToken(int $id, $type, string $pwd = '')
|
||||
public function createToken(int $id, $type, string $pwd = '', array $extras = [])
|
||||
{
|
||||
/** @var JwtAuth $jwtAuth */
|
||||
$jwtAuth = app()->make(JwtAuth::class);
|
||||
return $jwtAuth->createToken($id, $type, ['auth' => md5($pwd)]);
|
||||
return $jwtAuth->createToken($id, $type, array_merge(['auth' => md5($pwd)], $extras));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -51,7 +51,7 @@ class LoginServices extends BaseServices
|
||||
*/
|
||||
public function login($account, $password, $spread_uid)
|
||||
{
|
||||
$user = $this->dao->getOne(['phone' => $account], 'uid,pwd,status');
|
||||
$user = $this->dao->getOne(['phone' => $account], 'uid,pwd,status,phone');
|
||||
if ($user) {
|
||||
if ($user->pwd !== md5((string)$password))
|
||||
throw new ValidateException('账号或密码错误');
|
||||
@@ -65,7 +65,7 @@ class LoginServices extends BaseServices
|
||||
|
||||
$uid = (int)$user['uid'];
|
||||
//更新用户信息
|
||||
$token = $this->createToken($uid, 'api', $user->pwd);
|
||||
$token = $this->createToken($uid, 'api', $user->pwd, ['phone' => $user->phone ?? '']);
|
||||
if ($token) {
|
||||
$this->updateUserInfo(['spread_uid' => $spread_uid], $user);
|
||||
// 用户登录成功事件
|
||||
@@ -268,7 +268,7 @@ class LoginServices extends BaseServices
|
||||
$this->updateUserInfo(['spread_uid' => $spread_uid], $user);
|
||||
|
||||
$uid = (int)$user['uid'];
|
||||
$token = $this->createToken($uid, 'api', $user->pwd);
|
||||
$token = $this->createToken($uid, 'api', $user->pwd, ['phone' => $user->phone ?? '']);
|
||||
if ($token) {
|
||||
// 用户登录成功事件
|
||||
event('user.login', [$uid, app('request')->ip()]);
|
||||
@@ -305,7 +305,7 @@ class LoginServices extends BaseServices
|
||||
throw new ValidateException('修改新用户登录类型出错');
|
||||
}
|
||||
$uid = (int)$switch_user['uid'];
|
||||
$token = $this->createToken($uid, 'api', $switch_user['pwd']);
|
||||
$token = $this->createToken($uid, 'api', $switch_user['pwd'], ['phone' => $switch_user->phone ?? '']);
|
||||
if ($token) {
|
||||
// 用户登录成功事件
|
||||
event('user.login', [$uid, app('request')->ip()]);
|
||||
@@ -336,7 +336,10 @@ class LoginServices extends BaseServices
|
||||
$wechatUser = app()->make(WechatUserServices::class);
|
||||
//更新用户信息
|
||||
$user = $wechatUser->wechatOauthAfter([$openid, $wechatInfo, $spread_uid, $login_type, $userType]);
|
||||
$token = $this->createToken((int)$user['uid'], $userType, $user['pwd']);
|
||||
$token = $this->createToken((int)$user['uid'], $userType, $user['pwd'], [
|
||||
'phone' => $user['phone'] ?? $phone,
|
||||
'openid' => $openid,
|
||||
]);
|
||||
if ($token) {
|
||||
return [
|
||||
'token' => $token['token'],
|
||||
|
||||
@@ -98,7 +98,7 @@ class RoutineServices extends BaseServices
|
||||
$wechatUserServices = app()->make(WechatUserServices::class);
|
||||
//写入用户信息
|
||||
$user = $wechatUserServices->wechatOauthAfter([$openid, $wechatInfo, $spread_uid, $login_type, $userType]);
|
||||
return $this->getReturnInfo($user);
|
||||
return $this->getReturnInfo($user, 'routine', $openid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,7 +133,7 @@ class RoutineServices extends BaseServices
|
||||
$wechatUserServices = app()->make(WechatUserServices::class);
|
||||
//写入用户信息
|
||||
$user = $wechatUserServices->wechatOauthAfter($createData);
|
||||
return $this->getReturnInfo($user);
|
||||
return $this->getReturnInfo($user, 'routine', $openid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -289,15 +289,22 @@ class RoutineServices extends BaseServices
|
||||
* 获取返回信息
|
||||
* @param $user
|
||||
* @param string $userType
|
||||
* @param string $openid
|
||||
* @return array
|
||||
*/
|
||||
public function getReturnInfo($user, string $userType = 'routine')
|
||||
public function getReturnInfo($user, string $userType = 'routine', string $openid = '')
|
||||
{
|
||||
if (!$user || !isset($user['uid']) || !$user['uid']) {
|
||||
throw new ValidateException('获取用户信息失败');
|
||||
}
|
||||
$uid = (int)$user['uid'];
|
||||
$token = $this->createToken($uid, $userType, $user['pwd']);
|
||||
// 查询 unionid(从 wechat_user 表)
|
||||
$unionid = $this->dao->value(['uid' => $uid, 'user_type' => $userType, 'is_del' => 0], 'unionid') ?? '';
|
||||
$token = $this->createToken($uid, $userType, $user["pwd"] ?? "", [
|
||||
"phone" => $user["phone"] ?? "",
|
||||
"openid" => $openid,
|
||||
"unionid" => $unionid,
|
||||
]);
|
||||
if (!$token) {
|
||||
throw new ValidateException('登录失败!');
|
||||
}
|
||||
@@ -347,7 +354,7 @@ class RoutineServices extends BaseServices
|
||||
//更新用户信息
|
||||
$wechatUserServices->wechatUpdata([$user['uid'], $wechatInfo]);
|
||||
}
|
||||
return $this->getReturnInfo($user);
|
||||
return $this->getReturnInfo($user, $userType, $openid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -387,7 +394,7 @@ class RoutineServices extends BaseServices
|
||||
//更新用户信息
|
||||
$wechatUserServices->wechatUpdata([$user['uid'], $wechatInfo]);
|
||||
}
|
||||
return $this->getReturnInfo($user);
|
||||
return $this->getReturnInfo($user, $userType, $openid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -416,7 +423,13 @@ class RoutineServices extends BaseServices
|
||||
if ($notLogin || $storeUserMobile) {
|
||||
$userInfoKey = md5($openid . '_' . time() . '_routine');
|
||||
CacheService::setTokenBucket($userInfoKey, $createData, 7200);
|
||||
return ['auth_login' => 1, 'key' => $userInfoKey];
|
||||
// notLogin=true 时也透传 openid/unionid,让前端(H5/小程序)能拿到稳定身份标识
|
||||
return [
|
||||
'auth_login' => 1,
|
||||
'key' => $userInfoKey,
|
||||
'openid' => $openid,
|
||||
'unionid' => $routineInfo['unionid'] ?? '',
|
||||
];
|
||||
} else {
|
||||
//写入用户信息
|
||||
$user = $wechatUserServices->wechatOauthAfter($createData);
|
||||
@@ -425,7 +438,7 @@ class RoutineServices extends BaseServices
|
||||
//更新用户信息
|
||||
$wechatUserServices->wechatUpdata([$user['uid'], ['spread_uid' => $spread_uid]]);
|
||||
}
|
||||
return $this->getReturnInfo($user);
|
||||
return $this->getReturnInfo($user, 'routine', $openid);
|
||||
|
||||
}
|
||||
|
||||
@@ -452,7 +465,7 @@ class RoutineServices extends BaseServices
|
||||
$wechatUserServices = app()->make(WechatUserServices::class);
|
||||
//写入用户信息
|
||||
$user = $wechatUserServices->wechatOauthAfter($createData);
|
||||
return $this->getReturnInfo($user);
|
||||
return $this->getReturnInfo($user, 'routine', $openid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -493,7 +506,7 @@ class RoutineServices extends BaseServices
|
||||
$wechatUserServices = app()->make(WechatUserServices::class);
|
||||
//写入用户信息
|
||||
$user = $wechatUserServices->wechatOauthAfter([$openid, $wechatInfo, $spread_uid, $login_type, $userType]);
|
||||
return $this->getReturnInfo($user);
|
||||
return $this->getReturnInfo($user, 'routine', $openid);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
@@ -116,15 +117,19 @@ class WechatServices extends BaseServices
|
||||
* 获取返回信息
|
||||
* @param $user
|
||||
* @param string $userType
|
||||
* @param string $openid
|
||||
* @return array
|
||||
*/
|
||||
public function getReturnInfo($user, string $userType = 'wechat')
|
||||
public function getReturnInfo($user, string $userType = 'wechat', string $openid = '')
|
||||
{
|
||||
if (!$user || !isset($user['uid']) || !$user['uid']) {
|
||||
throw new ValidateException('获取用户信息失败');
|
||||
}
|
||||
$uid = (int)$user['uid'];
|
||||
$token = $this->createToken($uid, $userType, $user['pwd'] ?? '');
|
||||
$token = $this->createToken($uid, $userType, $user['pwd'] ?? '', [
|
||||
'phone' => $user['phone'] ?? '',
|
||||
'openid' => $openid,
|
||||
]);
|
||||
if (!$token) {
|
||||
throw new ValidateException('登录失败!');
|
||||
}
|
||||
@@ -179,7 +184,7 @@ class WechatServices extends BaseServices
|
||||
} else {
|
||||
$wechatUserServices->wechatUpdata([$user['uid'], $wechatInfo]);
|
||||
}
|
||||
return $this->getReturnInfo($user);
|
||||
return $this->getReturnInfo($user, 'wechat', $openid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -218,7 +223,7 @@ class WechatServices extends BaseServices
|
||||
//更新用户信息
|
||||
$wechatUserServices->wechatUpdata([$user['uid'], $wechatInfo]);
|
||||
}
|
||||
return $this->getReturnInfo($user);
|
||||
return $this->getReturnInfo($user, 'wechat', $openid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -260,7 +265,7 @@ class WechatServices extends BaseServices
|
||||
//更新用户信息
|
||||
$wechatUserServices->wechatUpdata([$user['uid'], $wechatInfo]);
|
||||
}
|
||||
return $this->getReturnInfo($user);
|
||||
return $this->getReturnInfo($user, 'wechat', $openid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -332,7 +337,7 @@ class WechatServices extends BaseServices
|
||||
//更新用户信息
|
||||
$wechatUserServices->wechatUpdata([$user['uid'], ['spread_uid' => $spread_uid]]);
|
||||
}
|
||||
return $this->getReturnInfo($user);
|
||||
return $this->getReturnInfo($user, 'wechat', $openid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -357,7 +362,7 @@ class WechatServices extends BaseServices
|
||||
$wechatUser = app()->make(WechatUserServices::class);
|
||||
//更新用户信息
|
||||
$user = $wechatUser->wechatOauthAfter([$openid, $wechatInfo, $spread_uid, $login_type, $userType]);
|
||||
return $this->getReturnInfo($user);
|
||||
return $this->getReturnInfo($user, $userType, $openid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -410,11 +415,164 @@ class WechatServices extends BaseServices
|
||||
$wechatUser = app()->make(WechatUserServices::class);
|
||||
//更新用户信息
|
||||
$user = $wechatUser->wechatOauthAfter([$openid, $userInfo, $spread_uid, $login_type, $userType]);
|
||||
$token = $this->getReturnInfo($user);
|
||||
$token = $this->getReturnInfo($user, $userType, $openid);
|
||||
$token['isbind'] = false;
|
||||
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
|
||||
|
||||
@@ -104,6 +104,10 @@ class WorkClientServices extends BaseServices
|
||||
$config = app()->make(WorkConfig::class);
|
||||
$corpId = $config->corpId;
|
||||
if (!$corpId) {
|
||||
Log::info('work client authGetExternalcontact skip (empty corp_id) ' . json_encode([
|
||||
'page' => $page,
|
||||
'cursor_len' => strlen($cursor),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
return true;
|
||||
}
|
||||
/** @var WorkMemberServices $memberService */
|
||||
@@ -111,6 +115,22 @@ class WorkClientServices extends BaseServices
|
||||
$menmberList = $memberService->getDataList(['corp_id' => $corpId], ['userid'], $page, 1);
|
||||
//没有数据就返回成功
|
||||
if (!$menmberList) {
|
||||
$memberCount = 0;
|
||||
try {
|
||||
$memberCount = $memberService->count(['corp_id' => $corpId]);
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('work client authGetExternalcontact member count error ' . json_encode([
|
||||
'corp_id' => $corpId,
|
||||
'message' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
Log::info('work client authGetExternalcontact done (no member) ' . json_encode([
|
||||
'corp_id' => $corpId,
|
||||
'page' => $page,
|
||||
'member_count' => $memberCount,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -123,18 +143,43 @@ class WorkClientServices extends BaseServices
|
||||
}
|
||||
|
||||
$userids = array_column($menmberList, 'userid');
|
||||
Log::info('work client authGetExternalcontact fetch ' . json_encode([
|
||||
'corp_id' => $corpId,
|
||||
'page' => $page,
|
||||
'cursor_len' => strlen($cursor),
|
||||
'userids_count' => count($userids),
|
||||
'tag_group_count' => count($tagRes['tag_group'] ?? []),
|
||||
'tag_map_count' => count($tagMap),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
try {
|
||||
$response = Work::getBatchClientList($userids, $cursor);
|
||||
$externalContactList = $response['external_contact_list'] ?? [];
|
||||
}catch (\Throwable $e){
|
||||
Log::error('work client authGetExternalcontact batchGet exception ' . json_encode([
|
||||
'corp_id' => $corpId,
|
||||
'page' => $page,
|
||||
'cursor_len' => strlen($cursor),
|
||||
'message' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine(),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
WorkClientJob::dispatchDo('authClient', [$page + 1, '']);
|
||||
return true;
|
||||
}
|
||||
|
||||
$stats = [
|
||||
'external_contact_count' => count($externalContactList),
|
||||
'client_insert' => 0,
|
||||
'client_update' => 0,
|
||||
'follow_insert' => 0,
|
||||
'follow_update' => 0,
|
||||
'tag_save' => 0,
|
||||
'missing_client_map' => 0,
|
||||
];
|
||||
$external = [];
|
||||
$followUser = [];//内部人员跟踪
|
||||
$externalUserids = [];//客户信息
|
||||
$this->transaction(function () use ($externalContactList, $corpId, $externalUserids, $followUser, $external, $tagMap) {
|
||||
$followUser = [];
|
||||
$externalUserids = [];
|
||||
$this->transaction(function () use ($externalContactList, $corpId, $tagMap, &$stats, &$externalUserids, &$followUser, &$external) {
|
||||
/** @var WechatUserServices $wechatUserService */
|
||||
$wechatUserService = app()->make(WechatUserServices::class);
|
||||
foreach ($externalContactList as $item) {
|
||||
@@ -196,10 +241,12 @@ class WorkClientServices extends BaseServices
|
||||
if ($this->dao->count(['external_userid' => $externalUserid, 'corp_id' => $corpId])) {
|
||||
unset($externalContact['external_userid']);
|
||||
$this->dao->update(['external_userid' => $externalUserid], $externalContact);
|
||||
$stats['client_update']++;
|
||||
} else {
|
||||
$externalContact['create_time'] = $item['follow_info']['createtime'] ??time();
|
||||
$externalContact['update_time'] = time();
|
||||
$external[] = $externalContact;
|
||||
$stats['client_insert']++;
|
||||
}
|
||||
}
|
||||
if ($external) {
|
||||
@@ -214,6 +261,7 @@ class WorkClientServices extends BaseServices
|
||||
$tagService = app()->make(WorkClientFollowTagsServices::class);
|
||||
foreach ($followUser as $userid => $items) {
|
||||
if(!isset($clientList[$userid])){
|
||||
$stats['missing_client_map']++;
|
||||
continue;
|
||||
}
|
||||
$items['client_id'] = $clientList[$userid];
|
||||
@@ -227,9 +275,11 @@ class WorkClientServices extends BaseServices
|
||||
'add_way' => $items['add_way'],
|
||||
'oper_userid' => $items['oper_userid'],
|
||||
]);
|
||||
$stats['follow_update']++;
|
||||
} else {
|
||||
$res = $followService->save($items);
|
||||
$id = $res->id;
|
||||
$stats['follow_insert']++;
|
||||
}
|
||||
if (!empty($items['tags'])) {
|
||||
$tagService->delete(['follow_id' => $id]);
|
||||
@@ -237,14 +287,32 @@ class WorkClientServices extends BaseServices
|
||||
$tag['follow_id'] = $id;
|
||||
}
|
||||
$tagService->saveAll($items['tags']);
|
||||
$stats['tag_save'] += count($items['tags']);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Log::info('work client authGetExternalcontact processed ' . json_encode([
|
||||
'corp_id' => $corpId,
|
||||
'page' => $page,
|
||||
'cursor_len' => strlen($cursor),
|
||||
'next_cursor_len' => strlen((string)($response['next_cursor'] ?? '')),
|
||||
'stats' => $stats,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
|
||||
if (isset($response['next_cursor']) && $response['next_cursor']) {
|
||||
Log::info('work client authGetExternalcontact dispatch next_cursor ' . json_encode([
|
||||
'corp_id' => $corpId,
|
||||
'page' => $page,
|
||||
'next_cursor_len' => strlen((string)$response['next_cursor']),
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
WorkClientJob::dispatchDo('authClient', [$page, $response['next_cursor']]);
|
||||
} else if (empty($response['next_cursor'])) {
|
||||
Log::info('work client authGetExternalcontact dispatch next_page ' . json_encode([
|
||||
'corp_id' => $corpId,
|
||||
'page' => $page + 1,
|
||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||||
WorkClientJob::dispatchDo('authClient', [$page + 1, '']);
|
||||
}
|
||||
|
||||
|
||||
18
config/internal.php
Normal file
18
config/internal.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | CRMEB [ CRMEB赋能开发者,助力企业发展 ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2016~2026 https://www.crmeb.com All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Licensed CRMEB并不是自由软件,未经许可不能去掉CRMEB相关版权
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: CRMEB Team <admin@crmeb.com>
|
||||
// +----------------------------------------------------------------------
|
||||
|
||||
// 内部服务通信密钥(app_03 等调用 /api/internal/* 时使用)
|
||||
// 优先读 .env 的 INTERNAL_API_KEY,没有则用占位符(生产必须改)
|
||||
// 注意:.env 用 CRLF 换行时,env() 读到的值末尾会带 \r
|
||||
// 用 trim() 避免请求头和 config 值的字节级比较失败
|
||||
return [
|
||||
'api_key' => trim((string)env('INTERNAL_API_KEY', '')),
|
||||
];
|
||||
@@ -453,8 +453,11 @@ class PayClient extends BaseClient
|
||||
*/
|
||||
public function configForAppPayment(string $prepayId): array
|
||||
{
|
||||
/* ⚠️ App 支付必须用「开放平台移动应用」的 appid(appAppid),
|
||||
不能用公众号 appid(wechatAppid)—— 下单用的是 appAppid(见 appPay()),
|
||||
两边不一致会导致客户端调起微信支付时报 appid/签名不匹配(2026-10-26 修复)。 */
|
||||
$params = [
|
||||
'appid' => $this->config->wechatAppid,
|
||||
'appid' => $this->config->appAppid,
|
||||
'partnerid' => $this->config->mchId,
|
||||
'prepayid' => $prepayId,
|
||||
'noncestr' => uniqid(),
|
||||
|
||||
@@ -61,6 +61,7 @@ class JwtAuth
|
||||
'iat' => $time,
|
||||
'nbf' => $time,
|
||||
'exp' => $exp_time,
|
||||
'uid' => $id,
|
||||
];
|
||||
$params['jti'] = compact('id', 'type');
|
||||
$token = JWT::encode($params, Env::get('app.app_key', $this->app_key) ?: $this->app_key, 'HS256');
|
||||
|
||||
@@ -17,6 +17,11 @@ use think\Response;
|
||||
*/
|
||||
Route::group('api', function () {
|
||||
|
||||
// 内部接口(无需用户认证)
|
||||
Route::get('internal/user-info', 'v1.InternalController/userInfo')->name('internalUserInfo');
|
||||
// 通过微信 code 换 openid(绕过 store_user_mobile 限制)
|
||||
Route::any('internal/openid-by-code', 'v1.InternalController/getOpenidByCode')->name('internalOpenidByCode');
|
||||
|
||||
Route::any('order_call_back', 'v1.order.StoreOrder/callBack');//商家寄件回调
|
||||
Route::any('jst/callback', 'v1.PublicController/jstCallback')->name('jstCallback');
|
||||
Route::any('wechat/serve', 'v1.wechat.Wechat/serve');//公众号服务
|
||||
@@ -436,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);
|
||||
|
||||
Reference in New Issue
Block a user