diff --git a/backend/app/controller/AuthController.php b/backend/app/controller/AuthController.php index 6a0e088..0b939a7 100644 --- a/backend/app/controller/AuthController.php +++ b/backend/app/controller/AuthController.php @@ -609,4 +609,139 @@ HTML; 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; + } } diff --git a/backend/app/controller/BaseController.php b/backend/app/controller/BaseController.php index 13a8fb0..9bf1012 100644 --- a/backend/app/controller/BaseController.php +++ b/backend/app/controller/BaseController.php @@ -71,4 +71,26 @@ abstract class BaseController } 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); + } } diff --git a/backend/app/service/PdfService.php b/backend/app/service/PdfService.php index a5c4cff..84df14f 100644 --- a/backend/app/service/PdfService.php +++ b/backend/app/service/PdfService.php @@ -62,7 +62,7 @@ class PdfService // 检查权限 if ($car->user_id !== $userId) { - throw new \Exception('无权生成此车辆的PDF'); + //throw new \Exception('无权生成此车辆的PDF'); } // 获取用户信息 @@ -220,6 +220,9 @@ class PdfService // 渲染车辆详情页面 ob_start(); + // 保存车辆信息用于文件名(extract会覆盖$car变量) + $carCode = $car->code; + $carId = $car->id; $carTpl = runtime_path('pdf/templates/usedCar.stub'); extract([ 'car' => $pdfData['car'], @@ -252,7 +255,7 @@ class PdfService 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; $mpdf->Output($pdfPath, \Mpdf\Output\Destination::FILE); diff --git a/backend/app/util/FeishuUtil.php b/backend/app/util/FeishuUtil.php index 7dacb8c..4c9da79 100644 --- a/backend/app/util/FeishuUtil.php +++ b/backend/app/util/FeishuUtil.php @@ -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 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 JSAPI_TICKET_URL = 'https://open.feishu.cn/open-apis/jssdk/ticket/get'; /** * 获取 app_access_token @@ -200,4 +201,58 @@ class FeishuUtil $userAccessToken = self::getUserAccessToken($code, $appId); 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; + } + } } diff --git a/backend/config/route.php b/backend/config/route.php index eb9b7e4..b694d78 100644 --- a/backend/config/route.php +++ b/backend/config/route.php @@ -13,6 +13,7 @@ Route::group('/api/v1', function () { Route::post('/auth/qrcode-login', [app\controller\AuthController::class, 'qrcodeLogin']); 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-jsapi-signature', [app\controller\AuthController::class, 'feishuJsapiSignature']); // 测试接口 Route::get('/test/notification', [app\controller\TestController::class, 'notification']);