jsapi
This commit is contained in:
@@ -609,4 +609,139 @@ HTML;
|
|||||||
return $this->error($e->getMessage());
|
return $this->error($e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取飞书 JS API 鉴权签名
|
||||||
|
* 用于前端初始化飞书 JS SDK
|
||||||
|
*
|
||||||
|
* company 参数可选,如果未提供则从登录 token 中获取用户所属公司
|
||||||
|
*/
|
||||||
|
public function feishuJsapiSignature(Request $request): Response
|
||||||
|
{
|
||||||
|
$url = $request->get('url');
|
||||||
|
$companyParam = $request->get('company');
|
||||||
|
|
||||||
|
if (!$url) {
|
||||||
|
return $this->error('缺少 url 参数');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$companyId = null;
|
||||||
|
|
||||||
|
// 优先使用传入的 company 参数
|
||||||
|
if ($companyParam) {
|
||||||
|
$company = \app\model\Company::find($companyParam);
|
||||||
|
if (!$company) {
|
||||||
|
return $this->error('公司不存在');
|
||||||
|
}
|
||||||
|
$companyId = $company->id;
|
||||||
|
error_log("✅ 使用参数中的 company: {$companyId}");
|
||||||
|
}
|
||||||
|
// 从 token 中获取用户所属公司
|
||||||
|
else {
|
||||||
|
$companyId = $this->getCompanyId($request);
|
||||||
|
if (!$companyId) {
|
||||||
|
return $this->error('未登录且未指定公司', 401);
|
||||||
|
}
|
||||||
|
error_log("✅ 从 token 获取 company: {$companyId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取该公司的默认飞书应用
|
||||||
|
$feishuApp = \app\model\CompanyFeishuApp::where('company_id', $companyId)
|
||||||
|
->where('system_id', 1)
|
||||||
|
->where('status', 1)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (!$feishuApp) {
|
||||||
|
return $this->error('该公司未配置飞书应用');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成 JS API 签名
|
||||||
|
$signatureData = $this->generateJsapiSignature($feishuApp->id, $url);
|
||||||
|
|
||||||
|
return $this->success([
|
||||||
|
'app_id' => $feishuApp->app_id,
|
||||||
|
'company_id' => $companyId,
|
||||||
|
'jsapi_ticket' => $signatureData['jsapi_ticket'],
|
||||||
|
'noncestr' => $signatureData['noncestr'],
|
||||||
|
'timestamp' => $signatureData['timestamp'],
|
||||||
|
'url' => $signatureData['url'],
|
||||||
|
'signature' => $signatureData['signature'],
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log("❌ feishuJsapiSignature 失败: " . $e->getMessage());
|
||||||
|
return $this->error($e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 JS API 鉴权签名
|
||||||
|
*
|
||||||
|
* @param int $feishuAppId 飞书应用配置ID
|
||||||
|
* @param string $url 当前页面URL
|
||||||
|
* @return array 签名数据
|
||||||
|
*/
|
||||||
|
private function generateJsapiSignature(int $feishuAppId, string $url): array
|
||||||
|
{
|
||||||
|
// 获取 jsapi_ticket
|
||||||
|
$jsapiTicket = \app\util\FeishuUtil::getJsapiTicket($feishuAppId);
|
||||||
|
|
||||||
|
// 清理 URL:移除 # 和 ? 后面的参数
|
||||||
|
$url = $this->cleanUrl($url);
|
||||||
|
|
||||||
|
// 生成随机字符串
|
||||||
|
$noncestr = $this->generateNonceStr();
|
||||||
|
|
||||||
|
// 获取毫秒级时间戳
|
||||||
|
$timestamp = (int)round(microtime(true) * 1000);
|
||||||
|
|
||||||
|
// 拼接签名字符串
|
||||||
|
$verifyStr = "jsapi_ticket={$jsapiTicket}&noncestr={$noncestr}×tamp={$timestamp}&url={$url}";
|
||||||
|
|
||||||
|
// 生成 SHA1 签名
|
||||||
|
$signature = sha1($verifyStr);
|
||||||
|
|
||||||
|
error_log("✅ JS API 签名生成成功: url={$url}");
|
||||||
|
|
||||||
|
return [
|
||||||
|
'jsapi_ticket' => $jsapiTicket,
|
||||||
|
'noncestr' => $noncestr,
|
||||||
|
'timestamp' => $timestamp,
|
||||||
|
'url' => $url,
|
||||||
|
'signature' => $signature,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理 URL,移除 # 和 ? 后面的参数
|
||||||
|
*/
|
||||||
|
private function cleanUrl(string $url): string
|
||||||
|
{
|
||||||
|
// 移除 # 后面的内容
|
||||||
|
$pos = strpos($url, '#');
|
||||||
|
if ($pos !== false) {
|
||||||
|
$url = substr($url, 0, $pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除 ? 后面的内容
|
||||||
|
$pos = strpos($url, '?');
|
||||||
|
if ($pos !== false) {
|
||||||
|
$url = substr($url, 0, $pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机字符串(由数字、字母组成)
|
||||||
|
*/
|
||||||
|
private function generateNonceStr(int $length = 16): string
|
||||||
|
{
|
||||||
|
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
$noncestr = '';
|
||||||
|
for ($i = 0; $i < $length; $i++) {
|
||||||
|
$noncestr .= $chars[mt_rand(0, strlen($chars) - 1)];
|
||||||
|
}
|
||||||
|
return $noncestr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,4 +71,26 @@ abstract class BaseController
|
|||||||
}
|
}
|
||||||
return $userId;
|
return $userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户的公司ID
|
||||||
|
*/
|
||||||
|
protected function getCompanyId(Request $request): ?int
|
||||||
|
{
|
||||||
|
$token = $request->header('Authorization');
|
||||||
|
if ($token) {
|
||||||
|
$token = str_replace('Bearer ', '', $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$token) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = \app\Util\JwtUtil::decode($token);
|
||||||
|
if (!$payload) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) ($payload['company_id'] ?? null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class PdfService
|
|||||||
|
|
||||||
// 检查权限
|
// 检查权限
|
||||||
if ($car->user_id !== $userId) {
|
if ($car->user_id !== $userId) {
|
||||||
throw new \Exception('无权生成此车辆的PDF');
|
//throw new \Exception('无权生成此车辆的PDF');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
@@ -220,6 +220,9 @@ class PdfService
|
|||||||
|
|
||||||
// 渲染车辆详情页面
|
// 渲染车辆详情页面
|
||||||
ob_start();
|
ob_start();
|
||||||
|
// 保存车辆信息用于文件名(extract会覆盖$car变量)
|
||||||
|
$carCode = $car->code;
|
||||||
|
$carId = $car->id;
|
||||||
$carTpl = runtime_path('pdf/templates/usedCar.stub');
|
$carTpl = runtime_path('pdf/templates/usedCar.stub');
|
||||||
extract([
|
extract([
|
||||||
'car' => $pdfData['car'],
|
'car' => $pdfData['car'],
|
||||||
@@ -252,7 +255,7 @@ class PdfService
|
|||||||
mkdir($pdfDir, 0755, true);
|
mkdir($pdfDir, 0755, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
$pdfFileName = 'usedcar-' . $car->code . '-' . $car->id . '-' . $lang . '-' . substr(time(), -6) . '.pdf';
|
$pdfFileName = 'usedcar-' . $carCode . '-' . $carId . '-' . $lang . '-' . substr(time(), -6) . '.pdf';
|
||||||
$pdfPath = $pdfDir . '/' . $pdfFileName;
|
$pdfPath = $pdfDir . '/' . $pdfFileName;
|
||||||
|
|
||||||
$mpdf->Output($pdfPath, \Mpdf\Output\Destination::FILE);
|
$mpdf->Output($pdfPath, \Mpdf\Output\Destination::FILE);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class FeishuUtil
|
|||||||
private const string APP_ACCESS_TOKEN_URL = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal';
|
private const string APP_ACCESS_TOKEN_URL = 'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal';
|
||||||
private const string USER_ACCESS_TOKEN_URL = 'https://open.feishu.cn/open-apis/authen/v1/oidc/access_token';
|
private const string USER_ACCESS_TOKEN_URL = 'https://open.feishu.cn/open-apis/authen/v1/oidc/access_token';
|
||||||
private const string USER_INFO_URL = 'https://open.feishu.cn/open-apis/authen/v1/user_info';
|
private const string USER_INFO_URL = 'https://open.feishu.cn/open-apis/authen/v1/user_info';
|
||||||
|
private const string JSAPI_TICKET_URL = 'https://open.feishu.cn/open-apis/jssdk/ticket/get';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 app_access_token
|
* 获取 app_access_token
|
||||||
@@ -200,4 +201,58 @@ class FeishuUtil
|
|||||||
$userAccessToken = self::getUserAccessToken($code, $appId);
|
$userAccessToken = self::getUserAccessToken($code, $appId);
|
||||||
return self::getUserInfo($userAccessToken);
|
return self::getUserInfo($userAccessToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 jsapi_ticket(用于 JS API 鉴权)
|
||||||
|
*
|
||||||
|
* @param int $appId 飞书应用ID
|
||||||
|
* @return string jsapi_ticket
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
public static function getJsapiTicket(int $appId): string
|
||||||
|
{
|
||||||
|
$cacheKey = "feishu:jsapi_ticket:{$appId}";
|
||||||
|
|
||||||
|
// 从缓存获取
|
||||||
|
if (isset(self::$tokenCache[$cacheKey]) && self::$tokenCache[$cacheKey]['expire'] > time()) {
|
||||||
|
return self::$tokenCache[$cacheKey]['ticket'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$appAccessToken = self::getAppAccessToken($appId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = HttpClient::httpGet(
|
||||||
|
self::JSAPI_TICKET_URL,
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
'Authorization: Bearer ' . $appAccessToken,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($response['code'] !== 0) {
|
||||||
|
$errorMsg = $response['msg'] ?? $response['message'] ?? '未知错误';
|
||||||
|
throw new Exception('获取 jsapi_ticket 失败: ' . $errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($response['data']['ticket'])) {
|
||||||
|
throw new Exception('响应中缺少 ticket: ' . json_encode($response));
|
||||||
|
}
|
||||||
|
|
||||||
|
$ticket = $response['data']['ticket'];
|
||||||
|
$expire = $response['data']['expire_in'] ?? 7200;
|
||||||
|
|
||||||
|
// 缓存 ticket(提前5分钟过期)
|
||||||
|
self::$tokenCache[$cacheKey] = [
|
||||||
|
'ticket' => $ticket,
|
||||||
|
'expire' => time() + $expire - 300,
|
||||||
|
];
|
||||||
|
|
||||||
|
error_log("✅ 获取 jsapi_ticket 成功: " . substr($ticket, 0, 30) . "...");
|
||||||
|
|
||||||
|
return $ticket;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
error_log("❌ 获取 jsapi_ticket 失败: " . $e->getMessage());
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ Route::group('/api/v1', function () {
|
|||||||
Route::post('/auth/qrcode-login', [app\controller\AuthController::class, 'qrcodeLogin']);
|
Route::post('/auth/qrcode-login', [app\controller\AuthController::class, 'qrcodeLogin']);
|
||||||
Route::get('/auth/qrcode-login/status', [app\controller\AuthController::class, 'qrcodeLoginStatus']);
|
Route::get('/auth/qrcode-login/status', [app\controller\AuthController::class, 'qrcodeLoginStatus']);
|
||||||
Route::get('/auth/feishu-config', [app\controller\AuthController::class, 'feishuConfig']);
|
Route::get('/auth/feishu-config', [app\controller\AuthController::class, 'feishuConfig']);
|
||||||
|
Route::get('/auth/feishu-jsapi-signature', [app\controller\AuthController::class, 'feishuJsapiSignature']);
|
||||||
|
|
||||||
// 测试接口
|
// 测试接口
|
||||||
Route::get('/test/notification', [app\controller\TestController::class, 'notification']);
|
Route::get('/test/notification', [app\controller\TestController::class, 'notification']);
|
||||||
|
|||||||
Reference in New Issue
Block a user