实现后台及前台通过API访问UV埋点,所有代码全部保存
This commit is contained in:
@@ -13,6 +13,7 @@ use liliuwei\think\Jump;
|
||||
use think\facade\Route;
|
||||
use think\facade\Config;
|
||||
use think\facade\Env;
|
||||
use app\tracking\VisitorTracker;
|
||||
|
||||
/**
|
||||
* 控制器基础类
|
||||
@@ -79,6 +80,23 @@ abstract class Controller
|
||||
}
|
||||
//固定一个版本号
|
||||
$this->assign('version', 128);
|
||||
|
||||
// ------------ 埋点:新增用户访问来源统计数据 ------------
|
||||
try {
|
||||
$visitorTracker = new VisitorTracker();
|
||||
$siteId = property_exists($this, 'site_id') ? $this->site_id : 0;
|
||||
$visitorTracker->shop_visit(['site_id' => $siteId]);
|
||||
if (property_exists($this, 'store_id')) {
|
||||
$storeId = $this->store_id;
|
||||
if (!empty($storeId)) {
|
||||
$visitorTracker->store_visit(['site_id' => $siteId, 'store_id' => $storeId]);
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// 埋点异常不影响正常业务
|
||||
log_write('埋点异常:' . $e->getMessage(), 'error');
|
||||
}
|
||||
|
||||
return View::fetch($template, $vars);
|
||||
}
|
||||
|
||||
|
||||
505
src/app/api/controller/AI.php
Normal file
505
src/app/api/controller/AI.php
Normal file
@@ -0,0 +1,505 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* AI API服务, 主要用来与终端进行AI相关的交互
|
||||
* 1. 与微信小程序/H5的智能客服进行交互
|
||||
* 2. 连接Dify平台或RAGFlow平台的智能体
|
||||
*/
|
||||
namespace app\api\controller;
|
||||
use Exception;
|
||||
use think\facade\Cache;
|
||||
use think\facade\Db;
|
||||
use think\facade\Request;
|
||||
use think\response\Json;
|
||||
use app\exception\ApiException;
|
||||
|
||||
use app\model\ai\AiChatSession;
|
||||
use app\model\ai\AiChatHistory;
|
||||
use app\model\web\Config;
|
||||
|
||||
class AI extends BaseApi
|
||||
{
|
||||
/**
|
||||
* 日志文件名称
|
||||
* @var string
|
||||
*/
|
||||
private $log_file = 'ai.log';
|
||||
|
||||
/**
|
||||
* 配置模型实例
|
||||
* @var Config
|
||||
*/
|
||||
protected $configModel;
|
||||
|
||||
/**
|
||||
* AiChatSession模型实例
|
||||
* @var AiChatSession
|
||||
*/
|
||||
protected $aiChatSessionModel;
|
||||
|
||||
/**
|
||||
* AiChatHistory模型实例
|
||||
* @var AiChatHistory
|
||||
*/
|
||||
protected $aiChatHistoryModel;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->aiChatSessionModel = new AiChatSession();
|
||||
$this->aiChatHistoryModel = new AiChatHistory();
|
||||
$this->configModel = new Config();
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台类型常量
|
||||
*/
|
||||
const PLATFORM_DIFY = 'dify';
|
||||
const PLATFORM_RAGFLOW = 'ragflow';
|
||||
|
||||
/**
|
||||
* AI对话接口
|
||||
* 发送对话或者创建会话
|
||||
* 1. 支持与Dify平台或RAGFlow平台的智能体进行交互
|
||||
* 2. 支持会话管理,每个用户在每个平台上的会话是独立的
|
||||
* 3. 支持上下文管理,每个会话都维护一个上下文信息,用于持续对话
|
||||
*
|
||||
* @return Json
|
||||
*/
|
||||
public function chat()
|
||||
{
|
||||
log_write('AI chat request: ' . json_encode($this->params), 'info', $this->log_file);
|
||||
try {
|
||||
// 获取请求参数
|
||||
$message = $this->params['message'] ?? ''; // 用户消息
|
||||
$session_id = $this->params['session_id'] ?? ''; // 会话ID
|
||||
$user_id = $this->params['user_id'] ?? $this->member_id; // 用户ID
|
||||
$context = $this->params['context'] ?? []; // 上下文信息
|
||||
|
||||
$site_id = $this->params['uniacid'] ?? $this->site_id; // 站点ID
|
||||
$app_module = $this->params['app_module'] ?? $this->app_module; // 应用模块
|
||||
|
||||
// 参数验证
|
||||
if (empty($message)) {
|
||||
return $this->response($this->error('', 'MESSAGE_EMPTY'));
|
||||
}
|
||||
|
||||
// 获取平台配置
|
||||
$config = $this->getPlatformConfig($site_id, $app_module);
|
||||
if (!$config['status']) {
|
||||
return $this->response($this->error('', $config['message']));
|
||||
}
|
||||
|
||||
$platform = $config['data']['default']['type'] ?? self::PLATFORM_DIFY; // 平台类型
|
||||
|
||||
// 生成或使用现有会话ID
|
||||
$session_id = $session_id ?: $this->generateSessionId($user_id, $platform);
|
||||
|
||||
// 根据平台类型调用不同的方法
|
||||
$result = [];
|
||||
if ($platform === self::PLATFORM_DIFY) {
|
||||
$result = $this->callDifyApi($config['data'], $message, $session_id, $user_id, $context);
|
||||
} else if ($platform === self::PLATFORM_RAGFLOW) {
|
||||
$result = $this->callRagflowApi($config['data'], $message, $session_id, $user_id, $context);
|
||||
}
|
||||
|
||||
if (!$result['status']) {
|
||||
return $this->response($this->error('', $result['message']));
|
||||
}
|
||||
|
||||
// 保存会话记录
|
||||
$this->saveChatHistory($user_id, $session_id, $platform, $message, $result['data']['content']);
|
||||
|
||||
return $this->response($this->success([
|
||||
'session_id' => $session_id,
|
||||
'content' => $result['data']['content'],
|
||||
'tokens' => $result['data']['tokens'] ?? null,
|
||||
'response_time' => $result['data']['response_time'] ?? null
|
||||
]));
|
||||
|
||||
} catch (Exception $e) {
|
||||
// 记录错误日志
|
||||
log_write('AI chat error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return $this->response($this->error('', 'AI_SERVICE_ERROR'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台配置
|
||||
* @param int $site_id 站点ID 默认为1, 业务站点uniacid值与site_id保持一致
|
||||
* @param string $app_module 应用模块 默认为shop
|
||||
* @return array
|
||||
*/
|
||||
private function getPlatformConfig($site_id, $app_module)
|
||||
{
|
||||
$config = [];
|
||||
try {
|
||||
// 从配置模型获取平台配置
|
||||
$config = $this->configModel->getAIPlatformConfig($site_id, $app_module);
|
||||
|
||||
throw new \Exception('Get AI platform config error: ' . json_encode($config));
|
||||
|
||||
if (!$config || !$config['status']) {
|
||||
return ['status' => false, 'message' => 'PLATFORM_CONFIG_NOT_FOUND'];
|
||||
}
|
||||
|
||||
$config_data = json_decode($config['config'], true);
|
||||
if (!$config_data || empty($config_data['api_key']) || empty($config_data['base_url']) || empty($config_data['app_id'])) {
|
||||
return ['status' => false, 'message' => 'PLATFORM_CONFIG_INVALID'];
|
||||
}
|
||||
|
||||
return ['status' => true, 'data' => $config_data];
|
||||
|
||||
} catch (Exception $e) {
|
||||
return ['status' => false, 'message' => 'GET_CONFIG_ERROR' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成会话ID
|
||||
* @param string $user_id
|
||||
* @param string $platform
|
||||
* @return string
|
||||
*/
|
||||
private function generateSessionId($user_id, $platform)
|
||||
{
|
||||
return md5($user_id . '_' . $platform . '_' . time() . '_' . rand(1000, 9999));
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用Dify API
|
||||
* @param array $config
|
||||
* @param string $message
|
||||
* @param string $session_id
|
||||
* @param string $user_id
|
||||
* @param array $context
|
||||
* @return array
|
||||
*/
|
||||
private function callDifyApi($config, $message, $session_id, $user_id, $context = [])
|
||||
{
|
||||
try {
|
||||
$start_time = microtime(true);
|
||||
|
||||
$headers = [
|
||||
'Authorization: Bearer ' . $config['api_key'],
|
||||
'Content-Type: application/json',
|
||||
'Accept: application/json',
|
||||
];
|
||||
|
||||
$data = [
|
||||
'inputs' => $context,
|
||||
'query' => $message,
|
||||
'response_mode' => $config['response_mode'] ?? 'streaming',
|
||||
'user' => $user_id,
|
||||
'conversation_id' => $session_id
|
||||
];
|
||||
|
||||
$url = rtrim($config['base_url'], '/') . '/v1/chat-messages';
|
||||
|
||||
// 发送请求
|
||||
$result = $this->httpRequest($url, $data, $headers);
|
||||
|
||||
if (!$result['status']) {
|
||||
return ['status' => false, 'message' => 'DIFY_API_ERROR'];
|
||||
}
|
||||
|
||||
$response_time = round((microtime(true) - $start_time) * 1000, 2);
|
||||
|
||||
return [
|
||||
'status' => true,
|
||||
'data' => [
|
||||
'content' => $result['data']['answer'] ?? $result['data']['choices'][0]['message']['content'] ?? '',
|
||||
'tokens' => [
|
||||
'prompt' => $result['data']['usage']['prompt_tokens'] ?? 0,
|
||||
'completion' => $result['data']['usage']['completion_tokens'] ?? 0,
|
||||
'total' => $result['data']['usage']['total_tokens'] ?? 0
|
||||
],
|
||||
'response_time' => $response_time
|
||||
]
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
log_write('Dify API error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return ['status' => false, 'message' => 'DIFY_CALL_ERROR'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用RAGFlow API
|
||||
* @param array $config
|
||||
* @param string $message
|
||||
* @param string $session_id
|
||||
* @param string $user_id
|
||||
* @param array $context
|
||||
* @return array
|
||||
*/
|
||||
private function callRagflowApi($config, $message, $session_id, $user_id, $context = [])
|
||||
{
|
||||
try {
|
||||
$start_time = microtime(true);
|
||||
|
||||
$headers = [
|
||||
'Authorization: Bearer ' . $config['api_key'],
|
||||
'Content-Type: application/json',
|
||||
'Accept: application/json',
|
||||
];
|
||||
|
||||
$data = [
|
||||
'query' => $message,
|
||||
'conversation_id' => $session_id,
|
||||
'user_id' => $user_id,
|
||||
'agent_id' => $config['app_id'],
|
||||
'stream' => $config['stream'] ?? false
|
||||
];
|
||||
|
||||
$url = rtrim($config['base_url'], '/') . '/api/v1/chat/completions';
|
||||
|
||||
// 发送请求
|
||||
$result = $this->httpRequest($url, $data, $headers);
|
||||
|
||||
if (!$result['status']) {
|
||||
return ['status' => false, 'message' => 'RAGFLOW_API_ERROR'];
|
||||
}
|
||||
|
||||
$response_time = round((microtime(true) - $start_time) * 1000, 2);
|
||||
|
||||
return [
|
||||
'status' => true,
|
||||
'data' => [
|
||||
'content' => $result['data']['choices'][0]['message']['content'] ?? $result['data']['answer'] ?? '',
|
||||
'tokens' => [
|
||||
'prompt' => $result['data']['usage']['prompt_tokens'] ?? 0,
|
||||
'completion' => $result['data']['usage']['completion_tokens'] ?? 0,
|
||||
'total' => $result['data']['usage']['total_tokens'] ?? 0
|
||||
],
|
||||
'response_time' => $response_time
|
||||
]
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
log_write('RAGFlow API error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return ['status' => false, 'message' => 'RAGFLOW_CALL_ERROR'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存聊天记录
|
||||
* @param string $user_id
|
||||
* @param string $session_id
|
||||
* @param string $platform
|
||||
* @param string $user_message
|
||||
* @param string $ai_message
|
||||
*/
|
||||
private function saveChatHistory($user_id, $session_id, $platform, $user_message, $ai_message)
|
||||
{
|
||||
try {
|
||||
$data = [
|
||||
'site_id' => $this->site_id,
|
||||
'user_id' => $user_id,
|
||||
'session_id' => $session_id,
|
||||
'platform' => $platform,
|
||||
'user_message' => $user_message,
|
||||
'ai_message' => $ai_message,
|
||||
'create_time' => time(),
|
||||
'ip' => Request::ip()
|
||||
];
|
||||
|
||||
// 使用事务保存聊天记录
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 使用模型保存聊天记录
|
||||
$save_result = $this->aiChatHistoryModel->saveHistory($data);
|
||||
if (!$save_result['success']) {
|
||||
log_write('Save chat history failed: ' . $save_result['msg'], 'error', $this->log_file);
|
||||
}
|
||||
|
||||
// 更新会话最后活动时间
|
||||
$update_result = $this->aiChatSessionModel->updateLastActiveTime($session_id);
|
||||
if (!$update_result['success']) {
|
||||
log_write('Update session active time failed: ' . $update_result['msg'], 'error', $this->log_file);
|
||||
}
|
||||
|
||||
// 如果会话不存在,创建新会话
|
||||
$session_info = $this->aiChatSessionModel->getSessionInfo(['session_id' => $session_id]);
|
||||
if (!$session_info['success']) {
|
||||
$session_data = [
|
||||
'site_id' => $this->site_id,
|
||||
'user_id' => $user_id,
|
||||
'session_id' => $session_id,
|
||||
'platform' => $platform,
|
||||
'create_time' => time(),
|
||||
'last_active_time' => time()
|
||||
];
|
||||
$create_result = $this->aiChatSessionModel->createSession($session_data);
|
||||
if (!$create_result['success']) {
|
||||
log_write('Create session failed: ' . $create_result['msg'], 'error', $this->log_file);
|
||||
}
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
} catch (Exception $e) {
|
||||
Db::rollback();
|
||||
log_write('Save chat history error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// 记录错误但不影响主流程
|
||||
log_write('Save chat history exception: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP请求方法
|
||||
* @param string $url
|
||||
* @param array $data
|
||||
* @param array $headers
|
||||
* @return array
|
||||
*/
|
||||
private function httpRequest($url, $data = [], $headers = [])
|
||||
{
|
||||
try {
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
|
||||
if (!empty($data)) {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
}
|
||||
|
||||
if (!empty($headers)) {
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
}
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
if ($http_code != 200) {
|
||||
log_write('HTTP request failed: URL=' . $url . ', Code=' . $http_code . ', Response=' . $response, 'error', $this->log_file);
|
||||
return ['status' => false, 'message' => 'HTTP_REQUEST_FAILED', 'code' => $http_code];
|
||||
}
|
||||
|
||||
$result = json_decode($response, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
log_write('JSON decode error: ' . json_last_error_msg(), 'error', $this->log_file);
|
||||
return ['status' => false, 'message' => 'JSON_DECODE_ERROR'];
|
||||
}
|
||||
|
||||
return ['status' => true, 'data' => $result];
|
||||
|
||||
} catch (Exception $e) {
|
||||
log_write('HTTP request exception: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return ['status' => false, 'message' => 'HTTP_REQUEST_EXCEPTION'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话历史
|
||||
* @return Json
|
||||
*/
|
||||
public function getHistory()
|
||||
{
|
||||
try {
|
||||
$session_id = $this->params['session_id'] ?? '';
|
||||
$user_id = $this->params['user_id'] ?? $this->member_id;
|
||||
$page = $this->params['page'] ?? 1;
|
||||
$page_size = $this->params['page_size'] ?? 20;
|
||||
|
||||
if (empty($session_id)) {
|
||||
return $this->response($this->error('', 'SESSION_ID_EMPTY'));
|
||||
}
|
||||
|
||||
// 使用模型获取会话历史
|
||||
$where = [
|
||||
'site_id' => $this->site_id,
|
||||
'user_id' => $user_id
|
||||
];
|
||||
|
||||
$result = $this->aiChatHistoryModel->getHistoryBySessionId($session_id, $where, $page, $page_size);
|
||||
|
||||
if (!$result['success']) {
|
||||
log_write('Get history failed: ' . $result['msg'], 'error', $this->log_file);
|
||||
return $this->response($this->error('', 'GET_HISTORY_ERROR'));
|
||||
}
|
||||
|
||||
return $this->response($this->success($result['data']));
|
||||
|
||||
} catch (Exception $e) {
|
||||
log_write('Get history error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return $this->response($this->error('', 'GET_HISTORY_ERROR'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的会话列表
|
||||
* @return Json
|
||||
*/
|
||||
public function getSessions()
|
||||
{
|
||||
try {
|
||||
$user_id = $this->params['user_id'] ?? $this->member_id;
|
||||
$page = $this->params['page'] ?? 1;
|
||||
$page_size = $this->params['page_size'] ?? 20;
|
||||
|
||||
// 使用模型获取会话列表
|
||||
$where = [
|
||||
'site_id' => $this->site_id,
|
||||
'user_id' => $user_id
|
||||
];
|
||||
|
||||
$result = $this->aiChatSessionModel->getSessionList($where, ['*'], 'last_active_time DESC', $page, $page_size);
|
||||
|
||||
if (!$result['success']) {
|
||||
return $this->response($this->error('', 'GET_SESSIONS_ERROR'));
|
||||
}
|
||||
|
||||
return $this->response($this->success($result['data']));
|
||||
|
||||
} catch (Exception $e) {
|
||||
log_write('Get sessions error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return $this->response($this->error('', 'GET_SESSIONS_ERROR'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
* @return Json
|
||||
*/
|
||||
public function deleteSession()
|
||||
{
|
||||
try {
|
||||
$session_id = $this->params['session_id'] ?? '';
|
||||
$user_id = $this->params['user_id'] ?? $this->member_id;
|
||||
|
||||
if (empty($session_id)) {
|
||||
return $this->response($this->error('', 'SESSION_ID_EMPTY'));
|
||||
}
|
||||
|
||||
// 使用模型删除会话
|
||||
$where = [
|
||||
'site_id' => $this->site_id,
|
||||
'session_id' => $session_id,
|
||||
'user_id' => $user_id
|
||||
];
|
||||
|
||||
$result = $this->aiChatSessionModel->deleteSession($where);
|
||||
|
||||
if (!$result['success']) {
|
||||
return $this->response($this->error('', 'DELETE_SESSION_ERROR'));
|
||||
}
|
||||
|
||||
return $this->response($this->success());
|
||||
|
||||
} catch (Exception $e) {
|
||||
log_write('Delete session exception: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return $this->response($this->error('', 'DELETE_SESSION_EXCEPTION'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ use think\facade\Cache;
|
||||
use addon\store\model\Config as StoreConfig;
|
||||
use app\model\store\Store;
|
||||
use think\Response;
|
||||
use app\tracking\VisitorTracker;
|
||||
|
||||
class BaseApi
|
||||
{
|
||||
@@ -97,6 +98,15 @@ class BaseApi
|
||||
}
|
||||
|
||||
$this->store_id = $this->params[ 'store_id' ] ?? 0;
|
||||
|
||||
|
||||
// ------------ 埋点:新增用户访问来源统计数据 ------------
|
||||
$visitorTracker = new VisitorTracker();
|
||||
$visitorTracker->shop_visit(['site_id' => $this->site_id,]);
|
||||
if (!empty($this->params[ 'store_id' ])) {
|
||||
$visitorTracker->store_visit(['site_id' => $this->site_id, 'store_id' => $this->store_id]);
|
||||
}
|
||||
// ------------------------------------------- ------------
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,6 +11,7 @@ use app\model\web\DiyView as DiyViewModel;
|
||||
use app\model\shop\Shop as ShopModel;
|
||||
|
||||
use app\model\member\Config as ConfigMemberModel;
|
||||
|
||||
class Config extends BaseApi
|
||||
{
|
||||
|
||||
@@ -107,7 +108,6 @@ class Config extends BaseApi
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
|
||||
$diy_view = new DiyViewModel();
|
||||
$diy_style = $diy_view->getStyleConfig($this->site_id)[ 'data' ][ 'value' ];
|
||||
|
||||
|
||||
@@ -21,8 +21,14 @@ class Schedule extends Task
|
||||
*/
|
||||
protected function execute()
|
||||
{
|
||||
//...具体的任务执行
|
||||
$cron_model = new Cron();
|
||||
$cron_model->execute(ScheduleDict::cli);
|
||||
try {
|
||||
log_write('Cron的Schedule开始执行', 'debug');
|
||||
//...具体的任务执行
|
||||
$cron_model = new Cron();
|
||||
$cron_model->execute(ScheduleDict::cli);
|
||||
log_write('Cron的Schedule执行完成', 'debug');
|
||||
} catch (\Exception $e) {
|
||||
log_write('Cron的Schedule执行失败:' . $e->getMessage(), 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,10 @@ error_reporting(E_NOTICE);
|
||||
use extend\QRcode as QRcode;
|
||||
use think\facade\Session;
|
||||
use think\facade\Event;
|
||||
use think\facade\App;
|
||||
use app\model\system\Addon;
|
||||
use extend\Barcode;
|
||||
use app\common\library\CallerInfo;
|
||||
|
||||
/*****************************************************基础函数*********************************************************/
|
||||
|
||||
@@ -1877,6 +1879,77 @@ function paramFilter($param)
|
||||
return preg_replace($filter_rule, '', $param);
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单日志写入
|
||||
* @param string $msg 日志内容
|
||||
* @param string $level 日志级别 debug、info、warning、error
|
||||
* @param string $file 日志文件名(不含路径)
|
||||
*/
|
||||
function log_write(string $message, string $level = 'info', string $filename = '', int $maxFileSize = 5242880): void
|
||||
{
|
||||
$callerInfo = CallerInfo::getCallerInfo(1);
|
||||
|
||||
// 格式化日志内容
|
||||
$content = sprintf(
|
||||
"[%s] %s: %s (文件:%s,行:%d) " . PHP_EOL,
|
||||
date('Y-m-d H:i:s'),
|
||||
strtoupper($level),
|
||||
is_array($message) ? json_encode($message, JSON_UNESCAPED_UNICODE) : $message,
|
||||
$callerInfo['file'],
|
||||
$callerInfo['line']
|
||||
);
|
||||
|
||||
$logPath = app()->getRuntimePath() . 'log/app_run_info/'; // eg. /runtime/shop/log
|
||||
|
||||
// 确保日志目录存在
|
||||
if (!is_dir($logPath)) {
|
||||
mkdir($logPath, 0755, true);
|
||||
}
|
||||
|
||||
// 生成日志文件名
|
||||
if (empty($filename)) {
|
||||
$filename = date('Y-m-d') . '.log';
|
||||
}
|
||||
|
||||
// 获取基础日志文件名(不含扩展名)和扩展名
|
||||
$fileInfo = pathinfo($filename);
|
||||
$baseName = $fileInfo['filename'];
|
||||
$extension = isset($fileInfo['extension']) ? '.' . $fileInfo['extension'] : '.log';
|
||||
|
||||
// 日志文件路径
|
||||
$logFile = $logPath . $filename;
|
||||
|
||||
// 检查文件大小并处理日志分割
|
||||
if (file_exists($logFile) && filesize($logFile) >= $maxFileSize) {
|
||||
// 查找现有的带序号的日志文件,确定下一个序号
|
||||
$nextIndex = 1;
|
||||
$pattern = $logPath . $baseName . '-???' . $extension;
|
||||
$matchingFiles = glob($pattern);
|
||||
|
||||
if (!empty($matchingFiles)) {
|
||||
// 提取最大序号
|
||||
$maxIndex = 0;
|
||||
foreach ($matchingFiles as $file) {
|
||||
$fileBase = pathinfo($file, PATHINFO_FILENAME);
|
||||
if (preg_match('/-([0-9]{3})$/', $fileBase, $matches)) {
|
||||
$index = (int)$matches[1];
|
||||
$maxIndex = max($maxIndex, $index);
|
||||
}
|
||||
}
|
||||
$nextIndex = $maxIndex + 1;
|
||||
}
|
||||
|
||||
// 生成带序号的新文件名
|
||||
$newFilename = $baseName . '-' . sprintf('%03d', $nextIndex) . $extension;
|
||||
$logFile = $logPath . $newFilename;
|
||||
}
|
||||
|
||||
// 写入文件(追加模式)
|
||||
file_put_contents($logFile, $content, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 将异常格式化HTML输出,方便定位
|
||||
* @param mixed $th
|
||||
|
||||
346
src/app/common/library/BrowserDetector.php
Normal file
346
src/app/common/library/BrowserDetector.php
Normal file
@@ -0,0 +1,346 @@
|
||||
<?php
|
||||
// app/common/library/BrowserDetector.php
|
||||
namespace app\common\library;
|
||||
|
||||
class BrowserDetector
|
||||
{
|
||||
private $userAgent;
|
||||
private $headers;
|
||||
private $server;
|
||||
private $request;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||
$this->headers = $this->getAllHeaders();
|
||||
$this->server = $_SERVER;
|
||||
$this->request = $_REQUEST;
|
||||
}
|
||||
|
||||
/**
|
||||
* 综合检测方法
|
||||
*/
|
||||
public function detect()
|
||||
{
|
||||
return [
|
||||
// 基础信息
|
||||
'user_agent' => $this->userAgent,
|
||||
'ip_address' => $this->getClientIp(),
|
||||
'request_time' => date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']),
|
||||
|
||||
// 平台类型
|
||||
'is_wechat' => $this->isWeChat(),
|
||||
'is_alipay' => $this->isAlipay(),
|
||||
'is_dingtalk' => $this->isDingTalk(),
|
||||
'is_miniprogram' => $this->isMiniProgram(),
|
||||
|
||||
// 设备类型
|
||||
'is_mobile' => $this->isMobile(),
|
||||
'is_tablet' => $this->isTablet(),
|
||||
'is_desktop' => $this->isDesktop(),
|
||||
|
||||
// 浏览器类型
|
||||
'is_chrome' => $this->isChrome(),
|
||||
'is_firefox' => $this->isFirefox(),
|
||||
'is_safari' => $this->isSafari(),
|
||||
'is_edge' => $this->isEdge(),
|
||||
'is_ie' => $this->isIE(),
|
||||
|
||||
// 操作系统
|
||||
'is_windows' => $this->isWindows(),
|
||||
'is_mac' => $this->isMac(),
|
||||
'is_linux' => $this->isLinux(),
|
||||
'is_ios' => $this->isIOS(),
|
||||
'is_android' => $this->isAndroid(),
|
||||
|
||||
// 来源类型
|
||||
'source_type' => $this->getSourceType(),
|
||||
'source_name' => $this->getSourceName(),
|
||||
|
||||
// URL参数
|
||||
'referer' => $_SERVER['HTTP_REFERER'] ?? '',
|
||||
'utm_source' => $_GET['utm_source'] ?? '',
|
||||
'utm_medium' => $_GET['utm_medium'] ?? '',
|
||||
'utm_campaign' => $_GET['utm_campaign'] ?? '',
|
||||
|
||||
// 自定义来源
|
||||
'custom_source' => $this->getCustomSource(),
|
||||
|
||||
// 请求信息
|
||||
'request_method' => $_SERVER['REQUEST_METHOD'],
|
||||
'request_uri' => $_SERVER['REQUEST_URI'],
|
||||
'query_string' => $_SERVER['QUERY_STRING'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有请求头
|
||||
*/
|
||||
private function getAllHeaders()
|
||||
{
|
||||
if (function_exists('getallheaders')) {
|
||||
return getallheaders();
|
||||
}
|
||||
|
||||
$headers = [];
|
||||
foreach ($_SERVER as $name => $value) {
|
||||
if (substr($name, 0, 5) == 'HTTP_') {
|
||||
$headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value;
|
||||
}
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端IP
|
||||
*/
|
||||
private function getClientIp()
|
||||
{
|
||||
$ip = '';
|
||||
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
||||
$ip = $_SERVER['HTTP_CLIENT_IP'];
|
||||
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
|
||||
} elseif (!empty($_SERVER['REMOTE_ADDR'])) {
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
return $ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断微信环境
|
||||
*/
|
||||
public function isWeChat()
|
||||
{
|
||||
return stripos($this->userAgent, 'micromessenger') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断支付宝环境
|
||||
*/
|
||||
public function isAlipay()
|
||||
{
|
||||
return stripos($this->userAgent, 'alipay') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断钉钉环境
|
||||
*/
|
||||
public function isDingTalk()
|
||||
{
|
||||
return stripos($this->userAgent, 'dingtalk') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断小程序环境
|
||||
*/
|
||||
public function isMiniProgram()
|
||||
{
|
||||
// 微信小程序
|
||||
if ($this->isWeChat() && stripos($this->userAgent, 'miniprogram') !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 支付宝小程序
|
||||
if ($this->isAlipay() && stripos($this->userAgent, 'miniprogram') !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 通过Referer判断(小程序跳转H5时)
|
||||
$referer = $_SERVER['HTTP_REFERER'] ?? '';
|
||||
if (strpos($referer, 'servicewechat.com') !== false ||
|
||||
strpos($referer, 'alipay.com') !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断移动设备
|
||||
*/
|
||||
public function isMobile()
|
||||
{
|
||||
return preg_match('/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i', $this->userAgent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断平板设备
|
||||
*/
|
||||
public function isTablet()
|
||||
{
|
||||
return preg_match('/ipad|android(?!.*mobile)|tablet/i', $this->userAgent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断桌面设备
|
||||
*/
|
||||
public function isDesktop()
|
||||
{
|
||||
return !$this->isMobile() && !$this->isTablet();
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览器类型判断
|
||||
*/
|
||||
public function isChrome()
|
||||
{
|
||||
return stripos($this->userAgent, 'chrome') !== false && stripos($this->userAgent, 'edge') === false;
|
||||
}
|
||||
|
||||
public function isFirefox()
|
||||
{
|
||||
return stripos($this->userAgent, 'firefox') !== false;
|
||||
}
|
||||
|
||||
public function isSafari()
|
||||
{
|
||||
return stripos($this->userAgent, 'safari') !== false && stripos($this->userAgent, 'chrome') === false;
|
||||
}
|
||||
|
||||
public function isEdge()
|
||||
{
|
||||
return stripos($this->userAgent, 'edge') !== false;
|
||||
}
|
||||
|
||||
public function isIE()
|
||||
{
|
||||
return stripos($this->userAgent, 'msie') !== false || stripos($this->userAgent, 'trident') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作系统判断
|
||||
*/
|
||||
public function isWindows()
|
||||
{
|
||||
return stripos($this->userAgent, 'windows') !== false;
|
||||
}
|
||||
|
||||
public function isMac()
|
||||
{
|
||||
return stripos($this->userAgent, 'macintosh') !== false || stripos($this->userAgent, 'mac os x') !== false;
|
||||
}
|
||||
|
||||
public function isLinux()
|
||||
{
|
||||
return stripos($this->userAgent, 'linux') !== false && !$this->isAndroid();
|
||||
}
|
||||
|
||||
public function isIOS()
|
||||
{
|
||||
return preg_match('/iphone|ipad|ipod/i', $this->userAgent);
|
||||
}
|
||||
|
||||
public function isAndroid()
|
||||
{
|
||||
return stripos($this->userAgent, 'android') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取自定义来源
|
||||
*/
|
||||
private function getCustomSource()
|
||||
{
|
||||
return [
|
||||
'from' => $_GET['from'] ?? '',
|
||||
'source' => $_GET['source'] ?? '',
|
||||
'channel' => $_GET['channel'] ?? '',
|
||||
'campaign' => $_GET['campaign'] ?? ''
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 综合判断来源类型
|
||||
*/
|
||||
public function getSourceType()
|
||||
{
|
||||
if ($this->isMiniProgram()) {
|
||||
return 'miniprogram';
|
||||
} elseif ($this->isWeChat()) {
|
||||
return 'wechat';
|
||||
} elseif ($this->isAlipay()) {
|
||||
return 'alipay';
|
||||
} elseif ($this->isMobile()) {
|
||||
return 'h5';
|
||||
} elseif ($this->isDesktop()) {
|
||||
return 'pc';
|
||||
} else {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取详细的来源名称
|
||||
*/
|
||||
public function getSourceName()
|
||||
{
|
||||
$sourceType = $this->getSourceType();
|
||||
|
||||
switch ($sourceType) {
|
||||
case 'miniprogram':
|
||||
if ($this->isWeChat()) return 'wechat_miniprogram';
|
||||
if ($this->isAlipay()) return 'alipay_miniprogram';
|
||||
return 'miniprogram';
|
||||
|
||||
case 'wechat':
|
||||
if ($this->isIOS()) return 'wechat_ios';
|
||||
if ($this->isAndroid()) return 'wechat_android';
|
||||
return 'wechat';
|
||||
|
||||
case 'h5':
|
||||
if ($this->isChrome()) return 'h5_chrome';
|
||||
if ($this->isSafari()) return 'h5_safari';
|
||||
return 'h5';
|
||||
|
||||
case 'pc':
|
||||
if ($this->isChrome()) return 'pc_chrome';
|
||||
if ($this->isFirefox()) return 'pc_firefox';
|
||||
if ($this->isEdge()) return 'pc_edge';
|
||||
return 'pc';
|
||||
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成客户端指纹
|
||||
*/
|
||||
public function generateFingerprint()
|
||||
{
|
||||
$components = [
|
||||
$this->userAgent,
|
||||
$_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '',
|
||||
$this->getClientIp(),
|
||||
$_SERVER['HTTP_ACCEPT'] ?? '',
|
||||
$_SERVER['HTTP_ACCEPT_ENCODING'] ?? ''
|
||||
];
|
||||
|
||||
return md5(implode('|', $components));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存检测结果到日志
|
||||
*/
|
||||
public function saveToLog($filename = 'browser_detection.log')
|
||||
{
|
||||
$data = $this->detect();
|
||||
$logEntry = json_encode($data, JSON_UNESCAPED_UNICODE) . PHP_EOL;
|
||||
file_put_contents($filename, $logEntry, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统计信息
|
||||
*/
|
||||
public function getStats()
|
||||
{
|
||||
return [
|
||||
'user_agent_length' => strlen($this->userAgent),
|
||||
'header_count' => count($this->headers),
|
||||
'is_secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
|
||||
'is_ajax' => !empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest',
|
||||
'is_post' => $_SERVER['REQUEST_METHOD'] === 'POST'
|
||||
];
|
||||
}
|
||||
}
|
||||
112
src/app/common/library/CallerInfo.php
Normal file
112
src/app/common/library/CallerInfo.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
namespace app\common\library;
|
||||
/**
|
||||
* 获取调用者信息的基础类
|
||||
*/
|
||||
class CallerInfo
|
||||
{
|
||||
/**
|
||||
* 获取调用者信息
|
||||
* @param int $depth 调用深度(0-当前函数,1-直接调用者,2-调用者的调用者)
|
||||
* @return array
|
||||
*/
|
||||
public static function getCallerInfo(int $depth = 1): array
|
||||
{
|
||||
$backtrace = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, $depth + 2);
|
||||
|
||||
if (!isset($backtrace[$depth])) {
|
||||
return [
|
||||
'file' => 'unknown',
|
||||
'line' => 0,
|
||||
'function' => 'unknown',
|
||||
'class' => 'unknown',
|
||||
'type' => 'unknown'
|
||||
];
|
||||
}
|
||||
|
||||
$caller = $backtrace[$depth];
|
||||
|
||||
return [
|
||||
'file' => $caller['file'] ?? 'unknown',
|
||||
'line' => $caller['line'] ?? 0,
|
||||
'function' => $caller['function'] ?? 'unknown',
|
||||
'class' => $caller['class'] ?? 'unknown',
|
||||
'type' => $caller['type'] ?? 'unknown', // '->' 或 '::'
|
||||
'args_count' => isset($caller['args']) ? count($caller['args']) : 0
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取调用者文件路径
|
||||
*/
|
||||
public static function getCallerFile(int $depth = 1): string
|
||||
{
|
||||
$info = self::getCallerInfo($depth + 1);
|
||||
return $info['file'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取调用者行号
|
||||
*/
|
||||
public static function getCallerLine(int $depth = 1): int
|
||||
{
|
||||
$info = self::getCallerInfo($depth + 1);
|
||||
return $info['line'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取调用栈信息(完整)
|
||||
*/
|
||||
public static function getCallStack(int $limit = 10): array
|
||||
{
|
||||
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $limit);
|
||||
$stack = [];
|
||||
|
||||
// 跳过当前函数
|
||||
array_shift($backtrace);
|
||||
|
||||
foreach ($backtrace as $index => $trace) {
|
||||
$stack[] = [
|
||||
'level' => $index,
|
||||
'file' => $trace['file'] ?? 'internal',
|
||||
'line' => $trace['line'] ?? 0,
|
||||
'function' => $trace['function'] ?? 'unknown',
|
||||
'class' => $trace['class'] ?? '',
|
||||
'type' => $trace['type'] ?? '',
|
||||
'args' => isset($trace['args']) ? count($trace['args']) : 0
|
||||
];
|
||||
}
|
||||
|
||||
return $stack;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取调用链字符串(用于日志)
|
||||
*/
|
||||
public static function getCallChain(): string
|
||||
{
|
||||
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5);
|
||||
$chain = [];
|
||||
|
||||
// 跳过当前函数
|
||||
array_shift($backtrace);
|
||||
|
||||
foreach ($backtrace as $trace) {
|
||||
$call = '';
|
||||
|
||||
if (isset($trace['class'])) {
|
||||
$call .= $trace['class'] . $trace['type'];
|
||||
}
|
||||
|
||||
$call .= $trace['function'];
|
||||
|
||||
if (isset($trace['file'])) {
|
||||
$call .= ' (' . basename($trace['file']) . ':' . $trace['line'] . ')';
|
||||
}
|
||||
|
||||
$chain[] = $call;
|
||||
}
|
||||
|
||||
return implode(' -> ', array_reverse($chain));
|
||||
}
|
||||
}
|
||||
115
src/app/common/library/EnhancedLogger.php
Normal file
115
src/app/common/library/EnhancedLogger.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
namespace app\common\library;
|
||||
|
||||
use app\common\library\CallerInfo;
|
||||
|
||||
/**
|
||||
* 增强的日志类(包含调用者信息)
|
||||
*/
|
||||
class EnhancedLogger
|
||||
{
|
||||
/**
|
||||
* 记录日志(包含调用者信息)
|
||||
*/
|
||||
public static function log(string $message, string $level = 'info', array $context = []): void
|
||||
{
|
||||
$callerInfo = CallerInfo::getCallerInfo(2); // 跳过log方法本身
|
||||
|
||||
$logData = [
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
'level' => strtoupper($level),
|
||||
'message' => $message,
|
||||
'context' => $context,
|
||||
'caller' => [
|
||||
'file' => $callerInfo['file'],
|
||||
'line' => $callerInfo['line'],
|
||||
'function' => $callerInfo['function'],
|
||||
'class' => $callerInfo['class']
|
||||
],
|
||||
'memory_usage' => memory_get_usage(true),
|
||||
'peak_memory' => memory_get_peak_usage(true)
|
||||
];
|
||||
|
||||
// 写入日志文件
|
||||
self::writeToFile($logData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试日志(自动包含调用栈)
|
||||
*/
|
||||
public static function debug(string $message, array $context = []): void
|
||||
{
|
||||
$callStack = CallerInfo::getCallStack(5);
|
||||
|
||||
self::log($message, 'debug', array_merge($context, [
|
||||
'call_stack' => $callStack
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 性能日志
|
||||
*/
|
||||
public static function profile(string $operation, callable $callback, array $context = []): mixed
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$startMemory = memory_get_usage(true);
|
||||
|
||||
try {
|
||||
$result = $callback();
|
||||
|
||||
$endTime = microtime(true);
|
||||
$endMemory = memory_get_usage(true);
|
||||
|
||||
self::log($operation, 'profile', array_merge($context, [
|
||||
'execution_time' => round(($endTime - $startTime) * 1000, 2) . 'ms',
|
||||
'memory_used' => self::formatBytes($endMemory - $startMemory),
|
||||
'success' => true
|
||||
]));
|
||||
|
||||
return $result;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$endTime = microtime(true);
|
||||
|
||||
self::log($operation, 'profile', array_merge($context, [
|
||||
'execution_time' => round(($endTime - $startTime) * 1000, 2) . 'ms',
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]));
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private static function writeToFile(array $logData): void
|
||||
{
|
||||
$logLine = sprintf(
|
||||
"[%s] %s: %s [%s:%d in %s::%s()] %s\n",
|
||||
$logData['timestamp'],
|
||||
$logData['level'],
|
||||
$logData['message'],
|
||||
basename($logData['caller']['file']),
|
||||
$logData['caller']['line'],
|
||||
$logData['caller']['class'],
|
||||
$logData['caller']['function'],
|
||||
json_encode($logData['context'], JSON_UNESCAPED_UNICODE)
|
||||
);
|
||||
|
||||
file_put_contents(
|
||||
__DIR__ . '/../logs/app.log',
|
||||
$logLine,
|
||||
FILE_APPEND | LOCK_EX
|
||||
);
|
||||
}
|
||||
|
||||
private static function formatBytes(int $bytes): string
|
||||
{
|
||||
$units = ['B', 'KB', 'MB', 'GB'];
|
||||
$bytes = max($bytes, 0);
|
||||
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
|
||||
$pow = min($pow, count($units) - 1);
|
||||
$bytes /= pow(1024, $pow);
|
||||
|
||||
return round($bytes, 2) . ' ' . $units[$pow];
|
||||
}
|
||||
}
|
||||
63
src/app/common/library/PerformanceAwareCallerInfo.php
Normal file
63
src/app/common/library/PerformanceAwareCallerInfo.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
namespace app\common\library;
|
||||
|
||||
use think\facade\App;
|
||||
|
||||
class PerformanceAwareCallerInfo
|
||||
{
|
||||
private static $enabled = true;
|
||||
|
||||
/**
|
||||
* 启用/禁用调用者信息(生产环境可禁用)
|
||||
*/
|
||||
public static function setEnabled(bool $enabled): void
|
||||
{
|
||||
self::$enabled = $enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否启用
|
||||
*/
|
||||
public static function isEnabled(): bool
|
||||
{
|
||||
return self::$enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 高性能的调用者信息获取
|
||||
*/
|
||||
public static function getCallerInfoOpt(int $depth = 1): array
|
||||
{
|
||||
if (!self::$enabled) {
|
||||
return ['file' => 'disabled', 'line' => 0];
|
||||
}
|
||||
|
||||
// 生产环境使用更轻量的方式
|
||||
if (app()->isDebug()) {
|
||||
// 开发环境:详细信息
|
||||
return self::getDetailedCallerInfo($depth);
|
||||
} else {
|
||||
// 生产环境:基本信息
|
||||
return self::getBasicCallerInfo($depth);
|
||||
}
|
||||
}
|
||||
|
||||
private static function getDetailedCallerInfo(int $depth): array
|
||||
{
|
||||
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $depth + 2);
|
||||
return $backtrace[$depth] ?? ['file' => 'unknown', 'line' => 0];
|
||||
}
|
||||
|
||||
private static function getBasicCallerInfo(int $depth): array
|
||||
{
|
||||
// 使用更轻量的方法
|
||||
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $depth + 2);
|
||||
$caller = $backtrace[$depth] ?? [];
|
||||
|
||||
return [
|
||||
'file' => $caller['file'] ?? 'unknown',
|
||||
'line' => $caller['line'] ?? 0,
|
||||
'function' => $caller['function'] ?? 'unknown'
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,10 @@ class Task extends Controller
|
||||
*/
|
||||
public function checkCron()
|
||||
{
|
||||
log_write('Task checkCron ...', 'debug');
|
||||
$cron_model = new Cron();
|
||||
$result = $cron_model->checkSchedule();
|
||||
log_write('Task checkCron result: ' . json_encode($result), 'debug');
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -39,7 +41,10 @@ class Task extends Controller
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
if (config('cron.default') == ScheduleDict::url) {
|
||||
// 只允许系统配置中定义默认的是通过url请求,才执行该计划任务
|
||||
if (config('cron.default') != ScheduleDict::url) {
|
||||
return $this->error('只允许系统配置cron.default=url请求,才执行该计划任务');
|
||||
} else {
|
||||
$cron_model = new Cron();
|
||||
$cron_model->execute();
|
||||
return true;
|
||||
@@ -51,6 +56,7 @@ class Task extends Controller
|
||||
*/
|
||||
public function execute()
|
||||
{
|
||||
log_write('单独计划任务 开始执行', 'info');
|
||||
if (config('cron.default') == ScheduleDict::default) {
|
||||
ignore_user_abort(true);
|
||||
set_time_limit(0);
|
||||
|
||||
@@ -25,6 +25,7 @@ class InitConfig
|
||||
$this->initConst();
|
||||
//初始化配置信息
|
||||
$this->initConfig();
|
||||
log_write('系统配置信息已初始化', 'debug');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
|
||||
|
||||
@@ -23,43 +24,71 @@ class InitCron
|
||||
{
|
||||
public function handle()
|
||||
{
|
||||
//根据计划任务类型来判断
|
||||
if(config('cron.default') != ScheduleDict::default) return;
|
||||
if (defined('BIND_MODULE') && BIND_MODULE === 'install') {
|
||||
return;
|
||||
}
|
||||
$last_time = Cache::get("cron_last_load_time");
|
||||
if (empty($last_time)) {
|
||||
$last_time = 0;
|
||||
}
|
||||
$last_exec_time = Cache::get("cron_http_last_exec_time");
|
||||
if (empty($last_exec_time)) {
|
||||
$last_exec_time = 0;
|
||||
}
|
||||
$module = request()->module();
|
||||
if ($module != 'cron') {
|
||||
if (!defined('CRON_EXECUTE') && time() - $last_time > 100 && time() - $last_exec_time > 100) {
|
||||
Cache::set("cron_http_last_exec_time", time());
|
||||
defined('CRON_EXECUTE') or define('CRON_EXECUTE', 1);
|
||||
$url = url('cron/task/cronExecute');
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HEADER, true);
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 1);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_exec($ch);
|
||||
|
||||
// // 获取错误信息并打印
|
||||
// $error = curl_error($ch);
|
||||
// if($error){
|
||||
// //保存错误
|
||||
// Cron::setError(ScheduleDict::default, $error);
|
||||
// }
|
||||
// // 关闭cURL资源句柄
|
||||
// curl_close($ch);
|
||||
log_write('InitCron计划任务初始化', 'debug');
|
||||
try {
|
||||
//根据计划任务类型来判断
|
||||
if (config('cron.default') != ScheduleDict::default) {
|
||||
log_write('InitCron计划任务未开启', 'warning');
|
||||
return;
|
||||
}
|
||||
if (defined('BIND_MODULE') && BIND_MODULE === 'install') {
|
||||
log_write('InitCron计划任务未开启,安装模块时不执行计划任务', 'warning');
|
||||
return;
|
||||
}
|
||||
$last_time = Cache::get("cron_last_load_time");
|
||||
if (empty($last_time)) {
|
||||
$last_time = 0;
|
||||
}
|
||||
$last_exec_time = Cache::get("cron_http_last_exec_time");
|
||||
if (empty($last_exec_time)) {
|
||||
$last_exec_time = 0;
|
||||
}
|
||||
$module = request()->module();
|
||||
if ($module == 'cron') {
|
||||
log_write('InitCron计划任务未开启, 请求模块是 cron 模块', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
$enable_start = !defined('CRON_EXECUTE') && time() - $last_time > 100 && time() - $last_exec_time > 100;
|
||||
|
||||
if (!$enable_start) {
|
||||
$content = sprintf('InitCron计划任务未启动,[%s, %s, %s], cron_last_load_time: %s, cron_http_last_exec_time: %s, CRON_EXECUTE:%s',
|
||||
!defined('CRON_EXECUTE') ? '1' : '0',
|
||||
time() - $last_time > 100 ? '1' : '0',
|
||||
time() - $last_exec_time > 100 ? '1' : '0',
|
||||
date('Y-m-d H:i:s', $last_time),
|
||||
date('Y-m-d H:i:s', $last_exec_time),
|
||||
defined('CRON_EXECUTE') ? 'true' : 'false'
|
||||
);
|
||||
log_write($content, 'debug');
|
||||
return;
|
||||
}
|
||||
|
||||
Cache::set("cron_http_last_exec_time", time());
|
||||
defined('CRON_EXECUTE') or define('CRON_EXECUTE', 1);
|
||||
$url = url('cron/task/cronExecute');
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HEADER, true);
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 1);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
curl_exec($ch);
|
||||
|
||||
// 获取错误信息并打印
|
||||
$error = curl_error($ch);
|
||||
if ($error) {
|
||||
//保存错误
|
||||
Cron::setError(ScheduleDict::default, $error);
|
||||
}
|
||||
// 关闭cURL资源句柄
|
||||
curl_close($ch);
|
||||
log_write('InitCron计划任务已启动', 'debug');
|
||||
} catch (\Exception $e) {
|
||||
// 计划任务异常,直接返回
|
||||
log_write('InitCron计划任务异常:' . $e->getMessage(), 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,27 +15,26 @@ class Cronexecute
|
||||
{
|
||||
public function fire(Job $job, $data)
|
||||
{
|
||||
$job->delete();
|
||||
try {
|
||||
|
||||
$res = event($data[ 'event' ], [ 'relate_id' => $data[ 'relate_id' ] ]);
|
||||
log_write('Job开始执行:' . $data['name'], 'debug');
|
||||
$job->delete();
|
||||
$res = event($data['event'], ['relate_id' => $data['relate_id']]);
|
||||
$data_log = [
|
||||
'name' => $data[ 'name' ],
|
||||
'event' => $data[ 'event' ],
|
||||
'relate_id' => $data[ 'relate_id' ],
|
||||
'name' => $data['name'],
|
||||
'event' => $data['event'],
|
||||
'relate_id' => $data['relate_id'],
|
||||
'message' => json_encode($res)
|
||||
];
|
||||
|
||||
Log::write("计划任务:{$data[ 'event' ]} relate_id: {$data[ 'relate_id' ]}执行结果:" . json_encode($res, JSON_UNESCAPED_UNICODE));
|
||||
Log::write("计划任务:{$data['event']} relate_id: {$data['relate_id']}执行结果:" . json_encode($res, JSON_UNESCAPED_UNICODE));
|
||||
$cron_model = new Cron();
|
||||
//定义最新的执行时间或错误
|
||||
$cron_model->addCronLog($data_log);
|
||||
|
||||
|
||||
log_write('Job执行成功:' . $data['name'], 'debug');
|
||||
} catch (\Exception $e) {
|
||||
Log::write($e->getMessage());
|
||||
log_write('Job执行失败:' . $e->getMessage(), 'error');
|
||||
$job->delete();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
261
src/app/model/ai/AiChatHistory.php
Normal file
261
src/app/model/ai/AiChatHistory.php
Normal file
@@ -0,0 +1,261 @@
|
||||
<?php
|
||||
/**
|
||||
* AI聊天历史记录模型
|
||||
* 负责处理AI对话中的聊天记录数据存储和管理
|
||||
*/
|
||||
namespace app\model\ai;
|
||||
|
||||
use think\facade\Db;
|
||||
use app\model\BaseModel;
|
||||
use Exception;
|
||||
|
||||
class AiChatHistory extends BaseModel
|
||||
{
|
||||
/**
|
||||
* 日志文件名称
|
||||
* @var string
|
||||
*/
|
||||
private $log_file = 'ai_chat_history.log';
|
||||
/**
|
||||
* 表名
|
||||
* @var string
|
||||
*/
|
||||
protected $name = 'ai_chat_history';
|
||||
|
||||
/**
|
||||
* 保存聊天记录
|
||||
* @param array $data 聊天记录数据
|
||||
* @return array
|
||||
*/
|
||||
public function saveHistory($data)
|
||||
{
|
||||
try {
|
||||
// 验证必要字段
|
||||
if (empty($data['site_id']) || empty($data['user_id']) || empty($data['session_id']) ||
|
||||
empty($data['platform']) || empty($data['user_message']) || empty($data['ai_message'])) {
|
||||
return $this->error('', 'PARAMETERS_INCOMPLETE');
|
||||
}
|
||||
|
||||
// 补充默认字段
|
||||
$data['create_time'] = $data['create_time'] ?? time();
|
||||
$data['ip'] = $data['ip'] ?? request()->ip();
|
||||
|
||||
// 保存数据
|
||||
$result = Db::name($this->name)->insert($data);
|
||||
|
||||
if ($result) {
|
||||
return $this->success(['id' => Db::name($this->name)->getLastInsID()]);
|
||||
} else {
|
||||
return $this->error('', 'SAVE_FAILED');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// 记录错误日志
|
||||
log_write('Save chat history error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return $this->error('', 'SAVE_EXCEPTION');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天历史记录列表
|
||||
* @param array $where 条件数组
|
||||
* @param array $field 查询字段
|
||||
* @param string $order 排序方式
|
||||
* @param int $page 页码
|
||||
* @param int $page_size 每页数量
|
||||
* @return array
|
||||
*/
|
||||
public function getHistoryList($where = [], $field = ['*'], $order = 'create_time ASC', $page = 1, $page_size = 20)
|
||||
{
|
||||
try {
|
||||
// 计算偏移量
|
||||
$offset = ($page - 1) * $page_size;
|
||||
|
||||
// 查询列表
|
||||
$list = Db::name($this->name)
|
||||
->field($field)
|
||||
->where($where)
|
||||
->order($order)
|
||||
->limit($offset, $page_size)
|
||||
->select();
|
||||
|
||||
// 查询总数
|
||||
$total = Db::name($this->name)
|
||||
->where($where)
|
||||
->count();
|
||||
|
||||
return $this->success([
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'page_size' => $page_size,
|
||||
'total_page' => ceil($total / $page_size)
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
log_write('Get chat history list error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return $this->error('', 'GET_LIST_FAILED');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据会话ID获取聊天记录
|
||||
* @param string $session_id 会话ID
|
||||
* @param array $where 额外条件
|
||||
* @param int $page 页码
|
||||
* @param int $page_size 每页数量
|
||||
* @return array
|
||||
*/
|
||||
public function getHistoryBySessionId($session_id, $where = [], $page = 1, $page_size = 20)
|
||||
{
|
||||
if (empty($session_id)) {
|
||||
return $this->error('', 'SESSION_ID_EMPTY');
|
||||
}
|
||||
|
||||
$where['session_id'] = $session_id;
|
||||
return $this->getHistoryList($where, ['*'], 'create_time ASC', $page, $page_size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的所有聊天记录
|
||||
* @param string $user_id 用户ID
|
||||
* @param array $where 额外条件
|
||||
* @param int $page 页码
|
||||
* @param int $page_size 每页数量
|
||||
* @return array
|
||||
*/
|
||||
public function getUserHistory($user_id, $where = [], $page = 1, $page_size = 20)
|
||||
{
|
||||
if (empty($user_id)) {
|
||||
return $this->error('', 'USER_ID_EMPTY');
|
||||
}
|
||||
|
||||
$where['user_id'] = $user_id;
|
||||
return $this->getHistoryList($where, ['*'], 'create_time DESC', $page, $page_size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除聊天记录
|
||||
* @param array $where 条件数组
|
||||
* @return array
|
||||
*/
|
||||
public function deleteHistory($where)
|
||||
{
|
||||
try {
|
||||
if (empty($where)) {
|
||||
return $this->error('', 'DELETE_CONDITION_EMPTY');
|
||||
}
|
||||
|
||||
$result = Db::name($this->name)->where($where)->delete();
|
||||
|
||||
if ($result !== false) {
|
||||
return $this->success(['deleted' => $result]);
|
||||
} else {
|
||||
return $this->error('', 'DELETE_FAILED');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
log_write('Delete chat history error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return $this->error('', 'DELETE_EXCEPTION');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据会话ID删除聊天记录
|
||||
* @param string $session_id 会话ID
|
||||
* @param array $where 额外条件
|
||||
* @return array
|
||||
*/
|
||||
public function deleteHistoryBySessionId($session_id, $where = [])
|
||||
{
|
||||
if (empty($session_id)) {
|
||||
return $this->error('', 'SESSION_ID_EMPTY');
|
||||
}
|
||||
|
||||
$where['session_id'] = $session_id;
|
||||
return $this->deleteHistory($where);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的会话消息统计
|
||||
* @param string $user_id 用户ID
|
||||
* @param array $where 额外条件
|
||||
* @return array
|
||||
*/
|
||||
public function getUserMessageStats($user_id, $where = [])
|
||||
{
|
||||
try {
|
||||
if (empty($user_id)) {
|
||||
return $this->error('', 'USER_ID_EMPTY');
|
||||
}
|
||||
|
||||
$where['user_id'] = $user_id;
|
||||
|
||||
// 统计总消息数
|
||||
$total_count = Db::name($this->name)->where($where)->count();
|
||||
|
||||
// 统计今日消息数
|
||||
$today_count = Db::name($this->name)
|
||||
->where($where)
|
||||
->whereTime('create_time', 'today')
|
||||
->count();
|
||||
|
||||
// 统计最近消息时间
|
||||
$last_message = Db::name($this->name)
|
||||
->where($where)
|
||||
->order('create_time DESC')
|
||||
->find();
|
||||
|
||||
return $this->success([
|
||||
'total_count' => $total_count,
|
||||
'today_count' => $today_count,
|
||||
'last_message_time' => $last_message ? $last_message['create_time'] : 0
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
log_write('Get user message stats error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return $this->error('', 'GET_STATS_FAILED');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的聊天记录
|
||||
* @param int $days 保留天数
|
||||
* @param array $where 额外条件
|
||||
* @return array
|
||||
*/
|
||||
public function cleanupExpiredHistory($days = 30, $where = [])
|
||||
{
|
||||
try {
|
||||
$expire_time = time() - ($days * 24 * 60 * 60);
|
||||
$where['create_time'] = ['<', $expire_time];
|
||||
|
||||
return $this->deleteHistory($where);
|
||||
} catch (Exception $e) {
|
||||
log_write('Cleanup expired history error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return $this->error('', 'CLEANUP_FAILED');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量保存聊天记录
|
||||
* @param array $data 聊天记录数组
|
||||
* @return array
|
||||
*/
|
||||
public function batchSaveHistory($data)
|
||||
{
|
||||
try {
|
||||
if (empty($data) || !is_array($data)) {
|
||||
return $this->error('', 'DATA_EMPTY');
|
||||
}
|
||||
|
||||
// 批量插入数据
|
||||
$result = Db::name($this->name)->insertAll($data);
|
||||
|
||||
if ($result) {
|
||||
return $this->success(['count' => $result]);
|
||||
} else {
|
||||
return $this->error('', 'BATCH_SAVE_FAILED');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
log_write('Batch save chat history error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return $this->error('', 'BATCH_SAVE_EXCEPTION');
|
||||
}
|
||||
}
|
||||
}
|
||||
266
src/app/model/ai/AiChatSession.php
Normal file
266
src/app/model/ai/AiChatSession.php
Normal file
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* AI聊天会话模型
|
||||
* 用于管理AI聊天的会话信息
|
||||
*/
|
||||
namespace app\model\ai;
|
||||
|
||||
use \app\model\BaseModel;
|
||||
use think\facade\Db;
|
||||
|
||||
class AiChatSession extends BaseModel
|
||||
{
|
||||
/**
|
||||
* 日志文件名称
|
||||
* @var string
|
||||
*/
|
||||
private $log_file = 'ai_chat_session.log';
|
||||
/**
|
||||
* 表名
|
||||
* @var string
|
||||
*/
|
||||
protected $name = 'ai_chat_session';
|
||||
|
||||
/**
|
||||
* 获取会话信息
|
||||
* @param array $where 条件
|
||||
* @param array $field 字段
|
||||
* @return array
|
||||
*/
|
||||
public function getSessionInfo($where = [], $field = ['*'])
|
||||
{
|
||||
try {
|
||||
$data = Db::name($this->name)
|
||||
->where($where)
|
||||
->field($field)
|
||||
->find();
|
||||
|
||||
return $this->success($data);
|
||||
} catch (\Exception $e) {
|
||||
log_write('Get session info error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return $this->error('', 'GET_SESSION_INFO_ERROR');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话列表
|
||||
* @param array $where 条件
|
||||
* @param array $field 字段
|
||||
* @param string $order 排序
|
||||
* @param int $page 页码
|
||||
* @param int $page_size 每页数量
|
||||
* @return array
|
||||
*/
|
||||
public function getSessionList($where = [], $field = ['*'], $order = 'last_active_time DESC', $page = 1, $page_size = 20)
|
||||
{
|
||||
try {
|
||||
$count = Db::name($this->name)
|
||||
->where($where)
|
||||
->count();
|
||||
|
||||
$list = [];
|
||||
if ($count > 0) {
|
||||
$list = Db::name($this->name)
|
||||
->where($where)
|
||||
->field($field)
|
||||
->order($order)
|
||||
->page($page, $page_size)
|
||||
->select();
|
||||
}
|
||||
|
||||
return $this->success(json_encode([
|
||||
'list' => $list,
|
||||
'total' => $count,
|
||||
'page' => $page,
|
||||
'page_size' => $page_size
|
||||
]));
|
||||
} catch (\Exception $e) {
|
||||
log_write('Get session list error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return $this->error('', 'GET_SESSION_LIST_ERROR');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建会话
|
||||
* @param array $data 数据
|
||||
* @return array
|
||||
*/
|
||||
public function createSession($data = [])
|
||||
{
|
||||
try {
|
||||
// 确保必要字段存在
|
||||
if (empty($data['session_id']) || empty($data['user_id']) || empty($data['site_id'])) {
|
||||
return $this->error('', 'MISSING_REQUIRED_FIELDS');
|
||||
}
|
||||
|
||||
// 检查会话是否已存在
|
||||
$exists = Db::name($this->name)
|
||||
->where('session_id', $data['session_id'])
|
||||
->count();
|
||||
|
||||
if ($exists > 0) {
|
||||
return $this->error('', 'SESSION_ALREADY_EXISTS');
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
$data['create_time'] = $data['create_time'] ?? time();
|
||||
$data['last_active_time'] = $data['last_active_time'] ?? time();
|
||||
|
||||
$result = Db::name($this->name)->insert($data);
|
||||
|
||||
if ($result) {
|
||||
return $this->success(json_encode(['session_id' => $data['session_id']]));
|
||||
} else {
|
||||
return $this->error('', 'CREATE_SESSION_FAILED');
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
log_write('Create session error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return $this->error('', 'CREATE_SESSION_ERROR');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话
|
||||
* @param array $where 条件
|
||||
* @param array $data 数据
|
||||
* @return array
|
||||
*/
|
||||
public function updateSession($where = [], $data = [])
|
||||
{
|
||||
try {
|
||||
if (empty($where)) {
|
||||
return $this->error('', 'WHERE_CONDITION_EMPTY');
|
||||
}
|
||||
|
||||
$result = Db::name($this->name)
|
||||
->where($where)
|
||||
->update($data);
|
||||
|
||||
return $this->success(['affected_rows' => $result]);
|
||||
} catch (\Exception $e) {
|
||||
log_write('Update session error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return $this->error('', 'UPDATE_SESSION_ERROR');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除会话
|
||||
* @param array $where 条件
|
||||
* @return array
|
||||
*/
|
||||
public function deleteSession($where = [])
|
||||
{
|
||||
try {
|
||||
if (empty($where)) {
|
||||
return $this->error('', 'WHERE_CONDITION_EMPTY');
|
||||
}
|
||||
|
||||
// 开启事务
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 删除会话
|
||||
$result = Db::name($this->name)
|
||||
->where($where)
|
||||
->delete();
|
||||
|
||||
// 删除相关的聊天历史
|
||||
Db::name('shop_ai_chat_history')
|
||||
->where($where)
|
||||
->delete();
|
||||
|
||||
Db::commit();
|
||||
return $this->success(['affected_rows' => $result]);
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
throw $e;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
log_write('Delete session error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return $this->error('', 'DELETE_SESSION_ERROR');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话最后活动时间
|
||||
* @param string $session_id 会话ID
|
||||
* @param int $time 时间戳
|
||||
* @return array
|
||||
*/
|
||||
public function updateLastActiveTime($session_id, $time = null)
|
||||
{
|
||||
$time = $time ?? time();
|
||||
return $this->updateSession(['session_id' => $session_id], ['last_active_time' => $time]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的活跃会话数
|
||||
* @param int $user_id 用户ID
|
||||
* @param int $site_id 站点ID
|
||||
* @param int $days 天数
|
||||
* @return array
|
||||
*/
|
||||
public function getUserActiveSessionsCount($user_id, $site_id, $days = 7)
|
||||
{
|
||||
try {
|
||||
$start_time = time() - ($days * 24 * 3600);
|
||||
|
||||
$count = Db::name($this->name)
|
||||
->where('user_id', $user_id)
|
||||
->where('site_id', $site_id)
|
||||
->where('last_active_time', '>=', $start_time)
|
||||
->count();
|
||||
|
||||
return $this->success(['count' => $count]);
|
||||
} catch (\Exception $e) {
|
||||
log_write('Get active sessions count error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return $this->error('', 'GET_ACTIVE_SESSIONS_COUNT_ERROR');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期会话
|
||||
* @param int $days 天数
|
||||
* @return array
|
||||
*/
|
||||
public function cleanupExpiredSessions($days = 30)
|
||||
{
|
||||
try {
|
||||
$expire_time = time() - ($days * 24 * 3600);
|
||||
|
||||
// 开启事务
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 找出所有过期会话ID
|
||||
$expired_sessions = Db::name($this->name)
|
||||
->where('last_active_time', '<', $expire_time)
|
||||
->column('session_id');
|
||||
|
||||
if (!empty($expired_sessions)) {
|
||||
// 删除过期会话
|
||||
$session_result = Db::name($this->name)
|
||||
->where('session_id', 'in', $expired_sessions)
|
||||
->delete();
|
||||
|
||||
// 删除相关聊天历史
|
||||
$history_result = Db::name('shop_ai_chat_history')
|
||||
->where('session_id', 'in', $expired_sessions)
|
||||
->delete();
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
return $this->success([
|
||||
'expired_count' => count($expired_sessions),
|
||||
'session_deleted' => $session_result ?? 0,
|
||||
'history_deleted' => $history_result ?? 0
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
throw $e;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
log_write('Cleanup expired sessions error: ' . $e->getMessage(), 'error', $this->log_file);
|
||||
return $this->error('', 'CLEANUP_EXPIRED_SESSIONS_ERROR');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,34 +30,53 @@ class StatShop extends BaseModel
|
||||
*/
|
||||
public function addShopStat($data)
|
||||
{
|
||||
$carbon = Carbon::now();
|
||||
$dir = __UPLOAD__.'/stat/stat_shop/';
|
||||
if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
|
||||
return $this->error(sprintf('Directory "%s" was not created', $dir));
|
||||
}
|
||||
$filename = $dir.$carbon->year.'_'.$carbon->month.'_'.$carbon->day.'_'.$carbon->second.'_'.unique_random().'.json';
|
||||
$stat_extend = new Stat($filename, 'stat_shop',$data['site_id']);
|
||||
$stat_extend->handleData($data);//写入文件
|
||||
log_write('店铺按天新统计数据开始添加', 'debug');
|
||||
try{
|
||||
$carbon = Carbon::now();
|
||||
$dir = __UPLOAD__.'/stat/stat_shop/';
|
||||
if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
|
||||
return $this->error(sprintf('Directory "%s" was not created', $dir));
|
||||
}
|
||||
$filename = $dir.$carbon->year.'_'.$carbon->month.'_'.$carbon->day.'_'.$carbon->second.'_'.unique_random().'.json';
|
||||
$stat_extend = new Stat($filename, 'stat_shop',$data['site_id']);
|
||||
$stat_extend->handleData($data);//写入文件
|
||||
|
||||
//增加当天时统计
|
||||
$this->addShopHourStat($data, $carbon);
|
||||
//增加当天时统计
|
||||
$this->addShopHourStat($data, $carbon);
|
||||
}catch (\Exception $e){
|
||||
log_write('店铺按天新统计数据添加失败:' . $e->getMessage(), 'error');
|
||||
return $this->error('店铺按天新统计数据添加失败');
|
||||
}
|
||||
|
||||
log_write('店铺按天新统计数据已添加', 'debug');
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从stat_shop目录下读取所有文件,将数据处理后,写入数据表中
|
||||
* 处理完每个文件后,删除文件
|
||||
*/
|
||||
public function cronShopStat()
|
||||
{
|
||||
log_write('店铺按天统计数据开始处理', 'debug');
|
||||
$path = __UPLOAD__.'/stat/stat_shop';
|
||||
if(!is_dir($path)) return;
|
||||
if(!is_dir($path)) {
|
||||
log_write('店铺按天统计数据处理失败:目录不存在', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->scanFile($path);
|
||||
if(empty($result)) return;
|
||||
if(empty($result)) {
|
||||
log_write('店铺按天统计数据处理失败:目录下无文件', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$json_array = [];
|
||||
foreach ($result as $key => $val){
|
||||
$stat_extend = new Stat($path.'/'.$val, 'stat_shop');
|
||||
$json_array[] = $stat_extend->load();
|
||||
unlink($path.'/'.$val);
|
||||
unlink($path.'/'.$val); // 处理完文件后,删除文件
|
||||
}
|
||||
|
||||
|
||||
@@ -83,8 +102,10 @@ class StatShop extends BaseModel
|
||||
foreach ($data_array as $json_k => $json_v){
|
||||
$system_stat->addStatShopModel($json_v);
|
||||
}
|
||||
log_write('店铺按天统计数据处理成功', 'debug');
|
||||
} catch (\Exception $e) {
|
||||
|
||||
log_write('店铺按天统计数据处理失败:' . $e->getMessage(), 'error');
|
||||
return $this->error('店铺按天统计数据处理失败');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -97,23 +118,37 @@ class StatShop extends BaseModel
|
||||
*/
|
||||
public function addShopHourStat($data, $carbon)
|
||||
{
|
||||
$dir = __UPLOAD__.'/stat/stat_shop_hour/';
|
||||
if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
|
||||
return $this->error(sprintf('Directory "%s" was not created', $dir));
|
||||
}
|
||||
$filename = $dir.$carbon->year.'_'.$carbon->month.'_'.$carbon->day.'_'.$carbon->hour.'_'.$carbon->second.'_'.unique_random().'.json';
|
||||
$stat_extend = new Stat($filename, 'stat_shop_hour',$data['site_id']);
|
||||
$stat_extend->handleData($data);//写入文件
|
||||
log_write('店铺按小时新统计数据开始添加', 'debug');
|
||||
try{
|
||||
$dir = __UPLOAD__.'/stat/stat_shop_hour/';
|
||||
if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
|
||||
return $this->error(sprintf('Directory "%s" was not created', $dir));
|
||||
}
|
||||
$filename = $dir.$carbon->year.'_'.$carbon->month.'_'.$carbon->day.'_'.$carbon->hour.'_'.$carbon->second.'_'.unique_random().'.json';
|
||||
$stat_extend = new Stat($filename, 'stat_shop_hour',$data['site_id']);
|
||||
$stat_extend->handleData($data);//写入文件
|
||||
log_write('店铺按小时新统计数据已添加', 'debug');
|
||||
}catch (\Exception $e){
|
||||
log_write('店铺按小时新统计数据添加失败:' . $e->getMessage(), 'error');
|
||||
return $this->error('店铺按小时新统计数据添加失败');
|
||||
}
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
public function cronShopStatHour()
|
||||
{
|
||||
log_write('系统计划任务开始执行店铺按小时统计');
|
||||
$path = __UPLOAD__.'/stat/stat_shop_hour';
|
||||
if(!is_dir($path)) return;
|
||||
if(!is_dir($path)) {
|
||||
log_write('系统计划任务执行店铺按小时统计异常:目录不存在' . $path . ',请检查目录是否存在');
|
||||
return;
|
||||
}
|
||||
|
||||
$result = $this->scanFile($path);
|
||||
if(empty($result)) return;
|
||||
if(empty($result)) {
|
||||
log_write('系统计划任务执行店铺按小时统计异常:目录下无文件' . $path . ',请检查是否有文件存在');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$json_array = [];
|
||||
@@ -145,8 +180,9 @@ class StatShop extends BaseModel
|
||||
foreach ($json_array as $json_k => $json_v){
|
||||
$system_stat->addStatShopHourModel($json_v);
|
||||
}
|
||||
log_write('系统计划任务执行店铺按小时统计完成');
|
||||
} catch (\Exception $e) {
|
||||
|
||||
log_write('系统计划任务执行店铺按小时统计异常:'.$e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace app\model\system;
|
||||
|
||||
use app\dict\system\ScheduleDict;
|
||||
@@ -15,7 +16,7 @@ use think\facade\Queue;
|
||||
class Cron extends BaseModel
|
||||
{
|
||||
|
||||
public $time_diff = 60;//默认半个小时检测一次
|
||||
public $time_diff = 60; //默认半个小时检测一次
|
||||
|
||||
/**
|
||||
* 添加计划任务
|
||||
@@ -25,7 +26,7 @@ class Cron extends BaseModel
|
||||
* @param string $event 执行事件
|
||||
* @param int $execute_time 待执行时间
|
||||
* @param int $relate_id 关联id
|
||||
* @param int $period_type 周期类型
|
||||
* @param int $period_type 周期类型 0 分钟 1 天 2 周 3 月
|
||||
*/
|
||||
public function addCron($type, $period, $name, $event, $execute_time, $relate_id, $period_type = 0)
|
||||
{
|
||||
@@ -40,6 +41,7 @@ class Cron extends BaseModel
|
||||
'create_time' => time()
|
||||
];
|
||||
$res = model('cron')->add($data);
|
||||
log_write('添加计划任务:' . json_encode($data), 'debug');
|
||||
return $this->success($res);
|
||||
}
|
||||
|
||||
@@ -59,87 +61,98 @@ class Cron extends BaseModel
|
||||
*/
|
||||
public function execute($type = 'default')
|
||||
{
|
||||
if(config('cron.default') != $type)
|
||||
log_write('计划任务开始执行', 'debug');
|
||||
if (config('cron.default') != $type) {
|
||||
log_write('计划任务方式不匹配<不能执行/model/system/Cron/execute>:' . config('cron.default') . ' != ' . $type, 'debug');
|
||||
return true;
|
||||
}
|
||||
|
||||
Log::write('计划任务方式'.$type);
|
||||
//写入计划任务标记运行
|
||||
$this->writeSchedule();
|
||||
$system_config_model = new SystemConfig();
|
||||
$config = $system_config_model->getSystemConfig()[ 'data' ] ?? [];
|
||||
$is_open_queue = $config[ 'is_open_queue' ] ?? 0;
|
||||
$query_execute_time = $is_open_queue == 1 ? time() + 60 : time();
|
||||
$list = model('cron')->getList([ [ 'execute_time', '<=', $query_execute_time ] ]);
|
||||
$now_time = time();
|
||||
if (!empty($list)) {
|
||||
foreach ($list as $k => $v) {
|
||||
$event_res = checkQueue($v, function($params) {
|
||||
//加入消息队列
|
||||
$job_handler_classname = 'Cronexecute';
|
||||
try {
|
||||
if ($params[ 'execute_time' ] <= time()) {
|
||||
Queue::push($job_handler_classname, $params);
|
||||
} else {
|
||||
Queue::later($params[ 'execute_time' ] - time(), $job_handler_classname, $params);
|
||||
log_write('当前执行方式:' . $type, 'debug');
|
||||
|
||||
try {
|
||||
//写入计划任务标记运行
|
||||
$this->writeSchedule();
|
||||
$system_config_model = new SystemConfig();
|
||||
$config = $system_config_model->getSystemConfig()['data'] ?? [];
|
||||
$is_open_queue = $config['is_open_queue'] ?? 0;
|
||||
$query_execute_time = $is_open_queue == 1 ? time() + 60 : time();
|
||||
$list = model('cron')->getList([['execute_time', '<=', $query_execute_time]]);
|
||||
$now_time = time();
|
||||
log_write('计划任务开始执行,查询计划任务列表', 'debug');
|
||||
|
||||
if (!empty($list)) {
|
||||
foreach ($list as $k => $v) {
|
||||
$event_res = checkQueue($v, function ($params) {
|
||||
//加入消息队列
|
||||
$job_handler_classname = 'Cronexecute';
|
||||
try {
|
||||
if ($params['execute_time'] <= time()) {
|
||||
Queue::push($job_handler_classname, $params);
|
||||
} else {
|
||||
Queue::later($params['execute_time'] - time(), $job_handler_classname, $params);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$res = $this->error($e->getMessage(), $e->getMessage());
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$res = $this->error($e->getMessage(), $e->getMessage());
|
||||
}
|
||||
return $res ?? $this->success();
|
||||
}, function($params) {
|
||||
try {
|
||||
$res = event($params[ 'event' ], [ 'relate_id' => $params[ 'relate_id' ] ]);
|
||||
} catch (\Exception $e) {
|
||||
$res = $this->error($e->getMessage(), $e->getMessage());
|
||||
return $res ?? $this->success();
|
||||
}, function ($params) {
|
||||
try {
|
||||
log_write('调用事件名称:' . $params['name'], 'debug');
|
||||
$res = event($params['event'], ['relate_id' => $params['relate_id']]);
|
||||
} catch (\Exception $e) {
|
||||
$res = $this->error($e->getMessage(), $e->getMessage());
|
||||
}
|
||||
|
||||
$data_log = [
|
||||
'name' => $params['name'],
|
||||
'event' => $params['event'],
|
||||
'relate_id' => $params['relate_id'],
|
||||
'message' => json_encode($res)
|
||||
];
|
||||
$this->addCronLog($data_log);
|
||||
return $res;
|
||||
});
|
||||
|
||||
//定义最新的执行时间或错误
|
||||
$event_code = $event_res['code'] ?? 0;
|
||||
if ($event_code < 0) {
|
||||
Log::write($event_res);
|
||||
continue;
|
||||
}
|
||||
|
||||
$data_log = [
|
||||
'name' => $params[ 'name' ],
|
||||
'event' => $params[ 'event' ],
|
||||
'relate_id' => $params[ 'relate_id' ],
|
||||
'message' => json_encode($res)
|
||||
];
|
||||
$this->addCronLog($data_log);
|
||||
return $res;
|
||||
});
|
||||
//循环任务
|
||||
if ($v['type'] == 2) {
|
||||
$period = $v['period'] == 0 ? 1 : $v['period'];
|
||||
switch ($v['period_type']) {
|
||||
case 0: //分
|
||||
|
||||
//定义最新的执行时间或错误
|
||||
$event_code = $event_res[ 'code' ] ?? 0;
|
||||
if ($event_code < 0) {
|
||||
Log::write($event_res);
|
||||
continue;
|
||||
}
|
||||
$execute_time = $now_time + $period * 60;
|
||||
break;
|
||||
case 1: //天
|
||||
|
||||
//循环任务
|
||||
if ($v[ 'type' ] == 2) {
|
||||
$period = $v[ 'period' ] == 0 ? 1 : $v[ 'period' ];
|
||||
switch ( $v[ 'period_type' ] ) {
|
||||
case 0://分
|
||||
$execute_time = strtotime('+' . $period . 'day', $v['execute_time']);
|
||||
break;
|
||||
case 2: //周
|
||||
|
||||
$execute_time = $now_time + $period * 60;
|
||||
break;
|
||||
case 1://天
|
||||
$execute_time = strtotime('+' . $period . 'week', $v['execute_time']);
|
||||
break;
|
||||
case 3: //月
|
||||
|
||||
$execute_time = strtotime('+' . $period . 'day', $v[ 'execute_time' ]);
|
||||
break;
|
||||
case 2://周
|
||||
|
||||
$execute_time = strtotime('+' . $period . 'week', $v[ 'execute_time' ]);
|
||||
break;
|
||||
case 3://月
|
||||
|
||||
$execute_time = strtotime('+' . $period . 'month', $v[ 'execute_time' ]);
|
||||
break;
|
||||
$execute_time = strtotime('+' . $period . 'month', $v['execute_time']);
|
||||
break;
|
||||
}
|
||||
model('cron')->update(['execute_time' => $execute_time], [['id', '=', $v['id']]]);
|
||||
} else {
|
||||
model('cron')->delete([['id', '=', $v['id']]]);
|
||||
}
|
||||
model('cron')->update([ 'execute_time' => $execute_time ], [ [ 'id', '=', $v[ 'id' ] ] ]);
|
||||
|
||||
} else {
|
||||
model('cron')->delete([ [ 'id', '=', $v[ 'id' ] ] ]);
|
||||
}
|
||||
}
|
||||
// $this->setCron();
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
log_write('计划任务执行异常<model/system/Cron/execute>:' . $e->getMessage(), 'debug');
|
||||
return true;
|
||||
}
|
||||
// $this->setCron();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,8 +163,8 @@ class Cron extends BaseModel
|
||||
public function addCronLog($data)
|
||||
{
|
||||
// 日常不需要添加,调试使用
|
||||
// $data[ 'execute_time' ] = time();
|
||||
// model('cron_log')->add($data);
|
||||
$data['execute_time'] = time();
|
||||
model('cron_log')->add($data);
|
||||
return $this->success();
|
||||
}
|
||||
|
||||
@@ -167,22 +180,22 @@ class Cron extends BaseModel
|
||||
if (empty($cron_cache)) {
|
||||
//todo 不存在缓存标识,并不视为任务停止
|
||||
//创建缓存标识,当前时间填充
|
||||
Cache::set('cron_cache', [ 'time' => $now_time, 'error' => '' ]);
|
||||
Cache::set('cron_cache', ['time' => $now_time, 'error' => '']);
|
||||
} else {
|
||||
$time = $cron_cache[ 'time' ];
|
||||
$error = $cron_cache[ 'error' ] ?? '';
|
||||
$attempts = $cron_cache[ 'attempts' ] ?? 0;//尝试次数
|
||||
if (!empty($error) || ( $now_time - $time ) > $diff) {
|
||||
$time = $cron_cache['time'];
|
||||
$error = $cron_cache['error'] ?? '';
|
||||
$attempts = $cron_cache['attempts'] ?? 0; //尝试次数
|
||||
if (!empty($error) || ($now_time - $time) > $diff) {
|
||||
$message = '自动任务已停止';
|
||||
if (!empty($error)) {
|
||||
$message .= ',停止原因:' . $error;
|
||||
} else {
|
||||
$system_config_model = new \app\model\system\SystemConfig();
|
||||
$config = $system_config_model->getSystemConfig()[ 'data' ] ?? [];
|
||||
$is_open_queue = $config[ 'is_open_queue' ] ?? 0;
|
||||
if (!$is_open_queue) {//如果不是消息队列的话,可以尝试异步调用一下
|
||||
$config = $system_config_model->getSystemConfig()['data'] ?? [];
|
||||
$is_open_queue = $config['is_open_queue'] ?? 0;
|
||||
if (!$is_open_queue) { //如果不是消息队列的话,可以尝试异步调用一下
|
||||
if ($attempts < 1) {
|
||||
Cache::set('cron_cache', [ 'time' => $now_time, 'error' => '', 'attempts' => 1 ]);
|
||||
Cache::set('cron_cache', ['time' => $now_time, 'error' => '', 'attempts' => 1]);
|
||||
$url = url('cron/task/execute');
|
||||
http($url, 1);
|
||||
return $this->success();
|
||||
@@ -194,10 +207,8 @@ class Cron extends BaseModel
|
||||
//判断任务是 消息队列自动任务,还是默认睡眠sleep自动任务
|
||||
return $this->error([], $message);
|
||||
}
|
||||
|
||||
}
|
||||
return $this->success();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,14 +222,14 @@ class Cron extends BaseModel
|
||||
if (empty($cron_cache)) {
|
||||
$cron_cache = [];
|
||||
}
|
||||
// $code = $params['code'] ?? 0;
|
||||
// if($code < 0){
|
||||
// $error = $params['message'] ?? '位置的错误';
|
||||
// $cron_cache['error'] = $error;
|
||||
// }
|
||||
// $code = $params['code'] ?? 0;
|
||||
// if($code < 0){
|
||||
// $error = $params['message'] ?? '位置的错误';
|
||||
// $cron_cache['error'] = $error;
|
||||
// }
|
||||
|
||||
$cron_cache[ 'time' ] = time();
|
||||
$cron_cache[ 'attempts' ] = 0;
|
||||
$cron_cache['time'] = time();
|
||||
$cron_cache['attempts'] = 0;
|
||||
Cache::set('cron_cache', $cron_cache);
|
||||
return $this->success();
|
||||
}
|
||||
@@ -229,27 +240,35 @@ class Cron extends BaseModel
|
||||
*/
|
||||
public function checkSchedule()
|
||||
{
|
||||
$file = root_path('runtime') . '.schedule';
|
||||
if (file_exists($file)) {
|
||||
$time = file_get_contents($file);
|
||||
if (!empty($time) && abs($time - time()) < 90) {
|
||||
return $this->success();
|
||||
try {
|
||||
$file = root_path('runtime') . '.schedule';
|
||||
if (file_exists($file)) {
|
||||
$time = file_get_contents($file);
|
||||
if (!empty($time) && abs($time - time()) < 90) {
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
|
||||
$remark = 'Cron计划任务已停止!当前启动的任务方式:' . ScheduleDict::getType(config('cron.default')) . '。';
|
||||
$error = self::getError(config('cron.default'));
|
||||
if (!empty($error)) {
|
||||
$remark .= json_encode($error);
|
||||
}
|
||||
log_write('Cron计划任务校验计划任务是否正常运行,计划任务异常,异常信息:' . json_encode($error) . ',文件路径:' . $file, 'warning');
|
||||
return $this->error([], $remark);
|
||||
} catch (\Exception $e) {
|
||||
log_write('Cron计划任务校验计划任务是否正常运行异常:' . $e->getMessage() . ',异常行:' . $e->getLine() . ',文件路径:' . $file, 'error');
|
||||
return $this->error([], '计划任务校验计划任务是否正常运行异常:' . $e->getMessage());
|
||||
}
|
||||
$remark = '计划任务已停止!当前启动的任务方式:'.ScheduleDict::getType(config('cron.default')).'。';
|
||||
$error = self::getError(config('cron.default'));
|
||||
if(!empty($error)){
|
||||
$remark .= $error;
|
||||
}
|
||||
return $this->error([], $remark);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入校验计划任务
|
||||
* @return true
|
||||
*/
|
||||
public function writeSchedule(){
|
||||
$file = root_path('runtime').'.schedule';
|
||||
public function writeSchedule()
|
||||
{
|
||||
$file = root_path('runtime') . '.schedule';
|
||||
file_put_contents($file, time());
|
||||
return true;
|
||||
}
|
||||
@@ -260,7 +279,8 @@ class Cron extends BaseModel
|
||||
* @param $error
|
||||
* @return true
|
||||
*/
|
||||
public static function setError($type, $error = ''){
|
||||
public static function setError($type, $error = '')
|
||||
{
|
||||
Cache::set('cron_error', [$type => $error]);
|
||||
return true;
|
||||
}
|
||||
@@ -270,10 +290,11 @@ class Cron extends BaseModel
|
||||
* @param $type
|
||||
* @return mixed
|
||||
*/
|
||||
public static function getError($type = ''){
|
||||
public static function getError($type = '')
|
||||
{
|
||||
$error = Cache::get('cron_error');
|
||||
if(!empty($type))
|
||||
if (!empty($type))
|
||||
return $error;
|
||||
return $error[$type];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -123,12 +123,11 @@ class BaseShop extends Controller
|
||||
$config_view = Config::get('view');
|
||||
$config_view[ 'tpl_replace_string' ] = array_merge($config_view[ 'tpl_replace_string' ], $this->replace);
|
||||
Config::set($config_view, 'view');
|
||||
|
||||
// 其他操作
|
||||
if (!request()->isAjax()) {
|
||||
$this->initBaseInfo();
|
||||
$this->loadTemplate();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -188,6 +188,52 @@ class Config extends BaseShop
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AI配置
|
||||
*/
|
||||
public function ai()
|
||||
{
|
||||
// 获取当前请求的 site_id
|
||||
$site_id = $this->site_id;
|
||||
$app_module = $this->app_module;
|
||||
|
||||
$config_model = new ConfigModel();
|
||||
if (request()->isJson()) {
|
||||
$type = input('type', '');
|
||||
$data = input('config', []);
|
||||
|
||||
if ($type == 'save_platform_cfg') {
|
||||
$data = json_decode($data, true);
|
||||
$result_platform = $config_model->setAIPlatformConfig($data, $site_id, $app_module);
|
||||
return $result_platform;
|
||||
} else if ($type == 'save_aiagent_cfg') {
|
||||
$data = json_decode($data, true);
|
||||
$result_agent = $config_model->setAIAgentServicesConfig($data, $site_id, $app_module);
|
||||
return $result_agent;
|
||||
}
|
||||
|
||||
return '无法识别的操作类型: ' . $type;
|
||||
} else {
|
||||
$support_app_modules = $config_model->getSupportAppModules();
|
||||
$support_ai_platform_types = $config_model->getSupportAIPlatformTypes();
|
||||
$config_platform = $config_model->getAIPlatformConfig($site_id)[ 'data' ][ 'value' ];
|
||||
$config_agent = $config_model->getAIAgentServicesConfig($site_id)[ 'data' ][ 'value' ];
|
||||
|
||||
$this->assign('support_app_modules', $support_app_modules);
|
||||
$this->assign('support_ai_platform_types', $support_ai_platform_types);
|
||||
$this->assign('platform_info', $config_platform);
|
||||
$this->assign('agent_info', $config_agent);
|
||||
|
||||
// return json_encode([
|
||||
// 'support_ai_platform_types' => $support_ai_platform_types,
|
||||
// 'support_app_modules' => $support_app_modules,
|
||||
// 'platform_info' => $config_platform,
|
||||
// ]);
|
||||
|
||||
return $this->fetch('config/ai/index');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 客服配置
|
||||
*/
|
||||
|
||||
@@ -35,7 +35,6 @@ class Index extends BaseShop
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
|
||||
$this->assign('shop_status', 1);
|
||||
|
||||
$this->handlePromotion();
|
||||
|
||||
@@ -1,617 +0,0 @@
|
||||
<!-- 页面样式定义 -->
|
||||
<style>
|
||||
.layui-card {
|
||||
margin: 20px 0;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.layui-card-header {
|
||||
background-color: #f2f2f2;
|
||||
padding: 12px 20px;
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.layui-card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.layui-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.layui-field-title {
|
||||
margin-top: 25px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.layui-field-title legend {
|
||||
padding: 0 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.word-aux {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #f00;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.form-wrap {
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
min-height: calc(100vh - 40px);
|
||||
}
|
||||
|
||||
.lang-tabs {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.lang-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.lang-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.layui-form-label {
|
||||
padding: 9px 15px;
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
.layui-input-block {
|
||||
margin-left: 110px;
|
||||
}
|
||||
|
||||
.all-shop-information {
|
||||
width: 100%;
|
||||
background: white;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.all-shop-information .all-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.all-shop-information .all-top .title {
|
||||
color: #333333;
|
||||
margin-bottom: 0;
|
||||
font-size: 17px;
|
||||
padding-left: 10px;
|
||||
border-left: 3px solid var(--base-color);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
select[name="selected_platform"] + .layui-form-select {
|
||||
min-width: 400px;
|
||||
max-width: fit-content;
|
||||
}
|
||||
|
||||
/* 扁平化表格样式 */
|
||||
.layui-table {
|
||||
margin: 10px 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
.layui-table th {
|
||||
font-weight: 400;
|
||||
background-color: #f5f7fa;
|
||||
border-bottom: 1px solid #e6e8eb;
|
||||
text-align: center;
|
||||
}
|
||||
.layui-table td {
|
||||
vertical-align: top;
|
||||
border-bottom: 1px solid #e6e8eb;
|
||||
padding: 12px 15px;
|
||||
}
|
||||
|
||||
.layui-table tbody tr:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
/* 调整类型列宽度 */
|
||||
.layui-table th:nth-child(2),
|
||||
.layui-table td:nth-child(2) {
|
||||
width: 120px;
|
||||
}
|
||||
/* 调整输入框样式 */
|
||||
.layui-table-edit {
|
||||
width: calc(100% - 4px);
|
||||
border-radius: 2px;
|
||||
border: 1px solid #dcdfe6;
|
||||
transition: border-color 0.2s;
|
||||
/* 确保输入框和选择框垂直居中对齐 */
|
||||
line-height: 30px;
|
||||
padding: 0 10px;
|
||||
box-sizing: border-box;
|
||||
margin: 4px;
|
||||
margin-top: 12px;
|
||||
height: 34px;
|
||||
}
|
||||
.layui-table-edit:focus {
|
||||
border-color: #c0c4cc;
|
||||
outline: none;
|
||||
}
|
||||
.layui-table td select.layui-select {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 0 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* 调整操作列宽度 */
|
||||
.layui-table th:last-child,
|
||||
.layui-table td:last-child {
|
||||
width: 120px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* 按钮图标样式 */
|
||||
.btn-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
margin: 0 2px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="layui-form">
|
||||
<!-- 根据功能分两个Tab进行配置,一个是AI智能客服,一个是AI平台 -->
|
||||
<div class="layui-tab" lay-filter="mainTab">
|
||||
<ul class="layui-tab-title">
|
||||
<li class="layui-this" lay-id="ai_agent">AI智能客服</li>
|
||||
<li lay-id="ai_platform">AI服务平台</li>
|
||||
</ul>
|
||||
<div class="layui-tab-content">
|
||||
<div class="layui-tab-item layui-show">
|
||||
<!-- AI智能客服配置 -->
|
||||
<div class="layui-form form-wrap card-common">
|
||||
<!-- 基础设置 -->
|
||||
<fieldset class="layui-elem-field layui-field-title">
|
||||
<legend>基础设置</legend>
|
||||
</fieldset>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">启用状态</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox" name="enable" lay-skin="switch" lay-text="开启|关闭" {if
|
||||
condition="isset($agent_info) && $agent_info.enable" }checked{/if}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 语言切换 -->
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-tab lang-tabs" lay-filter="langTab">
|
||||
<ul class="layui-tab-title">
|
||||
<li class="layui-this" lay-id="zh_CN">简体中文</li>
|
||||
<li lay-id="en">English</li>
|
||||
</ul>
|
||||
<div class="layui-tab-content">
|
||||
<div class="layui-tab-item layui-show">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">AI客服姓名</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="ai_name[zh_CN]" placeholder="请输入AI客服姓名"
|
||||
class="layui-input" value="{$agent_info.ai_name.zh_CN|default=''}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">AI客服头像</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="ai_avatar[zh_CN]" placeholder="请输入AI客服头像URL"
|
||||
class="layui-input" value="{$agent_info.ai_avatar.zh_CN|default=''}">
|
||||
<div class="word-aux">支持JPG、PNG、GIF格式,建议尺寸:100x100px</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">用户头像</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="user_avatar[zh_CN]" placeholder="请输入用户头像URL"
|
||||
class="layui-input" value="{$agent_info.user_avatar.zh_CN|default=''}">
|
||||
<div class="word-aux">支持JPG、PNG、GIF格式,建议尺寸:100x100px</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">欢迎语</label>
|
||||
<div class="layui-input-block">
|
||||
<textarea name="welcome_messages[zh_CN]" placeholder="请输入欢迎语,每行一条"
|
||||
class="layui-textarea" rows="4">{if isset($agent_info.welcome_messages.zh_CN)}{foreach $agent_info.welcome_messages.zh_CN as $msg}{$msg}
|
||||
{/foreach}{else}您好,我是智能客服助手,有什么可以帮助您的吗?{/if}</textarea>
|
||||
<!-- <div class="word-aux">每行一条欢迎语,系统会随机选择一条显示</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">AI Name</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="ai_name[en]" placeholder="Please enter AI name"
|
||||
class="layui-input" value="{$agent_info.ai_name.en|default=''}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">AI Avatar</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="ai_avatar[en]"
|
||||
placeholder="Please enter AI avatar URL" class="layui-input"
|
||||
value="{$agent_info.ai_avatar.en|default=''}">
|
||||
<div class="word-aux">Support JPG, PNG, GIF formats, recommended size:
|
||||
100x100px</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">User Avatar</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="user_avatar[en]"
|
||||
placeholder="Please enter user avatar URL" class="layui-input"
|
||||
value="{$agent_info.user_avatar.en|default=''}">
|
||||
<div class="word-aux">Support JPG, PNG, GIF formats, recommended size:
|
||||
100x100px</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">Welcome Messages</label>
|
||||
<div class="layui-input-block">
|
||||
<textarea name="welcome_messages[en]"
|
||||
placeholder="Please enter welcome messages, one per line"
|
||||
class="layui-textarea" rows="4">{if isset($agent_info.welcome_messages.en)}{foreach $agent_info.welcome_messages.en as $msg}{$msg}
|
||||
{/foreach}{else}Hello, I'm the AI customer service assistant. How can I help you?{/if}</textarea>
|
||||
<!-- <div class="word-aux">One welcome message per line, the system will randomly select one to display</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 显示设置 -->
|
||||
<fieldset class="layui-elem-field layui-field-title">
|
||||
<legend>显示设置</legend>
|
||||
</fieldset>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">显示加载更多按钮</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox" name="show_load_more_button" lay-skin="switch" lay-text="开启|关闭" {if
|
||||
condition="isset($agent_info) && $agent_info.show_load_more_button"
|
||||
}checked{else}checked{/if}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">最大消息数量</label>
|
||||
<div class="layui-input-inline" style="width: 100px;">
|
||||
<input type="number" name="max_messages" value="{$agent_info.max_messages|default='100'}"
|
||||
class="layui-input" min="10" max="500">
|
||||
</div>
|
||||
<div class="layui-form-mid">条</div>
|
||||
<div class="word-aux" style="margin-left: 110px;">建议设置在10-500条之间</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">启用流式响应</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox" name="stream_mode" lay-skin="switch" lay-text="开启|关闭" {if
|
||||
condition="isset($agent_info) && $agent_info.stream_mode" }checked{else}checked{/if}>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-tab-item" lay-id="ai_platform">
|
||||
<!-- AI平台配置 -->
|
||||
<div class="layui-form form-wrap card-common">
|
||||
<!-- 平台选择 -->
|
||||
<fieldset class="layui-elem-field layui-field-title">
|
||||
<legend>基本设置</legend>
|
||||
</fieldset>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">默认平台</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="selected_platform" lay-filter="platformSelect">
|
||||
<option value="">请选择要默认使用的AI服务平台</option>
|
||||
{if isset($enable_platform_list) && !empty($enable_platform_list)}
|
||||
{foreach $enable_platform_list as $platform}
|
||||
<option value="{$platform.id}">{$platform.name}</option>
|
||||
{/foreach}
|
||||
{/if}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 平台配置表格 -->
|
||||
<fieldset class="layui-elem-field layui-field-title">
|
||||
<legend>AI服务平台配置</legend>
|
||||
</fieldset>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<table class="layui-table" lay-even lay-skin="line">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>类型</th>
|
||||
<th>API URL</th>
|
||||
<th>API Key</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="platformTable">
|
||||
{if isset($platform_list) && !empty($platform_list)}
|
||||
{foreach $platform_list as $platform}
|
||||
<tr data-id="{$platform.id}">
|
||||
<td>
|
||||
<input type="text" name="platform_name[{$platform.id}]" value="{$platform.name}"
|
||||
class="layui-input layui-table-edit" placeholder="请输入AI服务平台名称">
|
||||
</td>
|
||||
<td>
|
||||
<select name="platform_type[{$platform.id}]" class="layui-select layui-table-edit">
|
||||
{foreach $support_ai_platform_types as $item}
|
||||
<option value="{$item.value}" {if $platform.type == $item.value}selected{/if}>{$item.label}</option>
|
||||
{/foreach}
|
||||
</select>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="platform_api_url[{$platform.id}]" value="{$platform.api_url}"
|
||||
class="layui-input layui-table-edit" placeholder="请输入API URL">
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" name="platform_api_key[{$platform.id}]" value="{$platform.api_key}"
|
||||
class="layui-input layui-table-edit" placeholder="请输入API Key">
|
||||
</td>
|
||||
<td class="layui-row">
|
||||
<button class="layui-col-xs4 layui-col-sm12 layui-col-md4 layui-btn layui-btn-xs layui-btn-danger btn-icon delete-platform" data-id="{$platform.id}" title="删除">
|
||||
<i class="layui-icon layui-icon-delete"></i>
|
||||
</button>
|
||||
<button class="layui-col-xs4 layui-col-sm12 layui-col-md4 layui-btn layui-btn-xs layui-btn-warm btn-icon toggle-platform" data-id="{$platform.id}" data-status="{if $platform.enable === 1}1{else}0{/if}" title="{if $platform.enable === 1}禁用{else}开启{/if}">
|
||||
{if $platform.enable === 1}<i class="layui-icon layui-icon-radio"></i>{else}<i class="layui-icon layui-icon-circle"></i>{/if}
|
||||
</button>
|
||||
<button class="layui-col-xs4 layui-col-sm12 layui-col-md4 layui-btn layui-btn-xs btn-icon save-platform" data-id="{$platform.id}" title="保存">
|
||||
<i class="layui-icon layui-icon-ok"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/foreach}
|
||||
{else}
|
||||
<tr>
|
||||
<td colspan="5" style="text-align: center;">暂无平台配置</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 添加新平台 -->
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label"></label>
|
||||
<div class="layui-input-block">
|
||||
<button class="layui-btn" id="addPlatform">添加平台</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 页面脚本 -->
|
||||
<script>
|
||||
layui.use(['form', 'layer', 'element'], function () {
|
||||
var form = layui.form;
|
||||
var layer = layui.layer;
|
||||
var element = layui.element;
|
||||
|
||||
// 初始化表单
|
||||
form.render();
|
||||
|
||||
// layui 2.5.5版本需要手动初始化Tab组件
|
||||
element.render('tab');
|
||||
|
||||
// 主Tab切换处理
|
||||
element.on('tab(mainTab)', function (data) { });
|
||||
|
||||
// 语言切换 - 使用layui原生的Tab切换,不再需要自定义逻辑
|
||||
element.on('tab(langTab)', function (data) { });
|
||||
|
||||
// 平台选择下拉框处理
|
||||
form.on('select(platformSelect)', function(data) {
|
||||
var platformId = data.value;
|
||||
if (platformId) {
|
||||
// 可以在这里添加根据平台ID高亮显示对应表格行的逻辑
|
||||
$('#platformTable tr').removeClass('layui-bg-blue');
|
||||
$('#platformTable tr[data-id="' + platformId + '"]').addClass('layui-bg-blue');
|
||||
}
|
||||
});
|
||||
|
||||
// 保存平台配置
|
||||
$(document).on('click', '.save-platform', function() {
|
||||
var platformId = $(this).data('id');
|
||||
var name = $('input[name="platform_name[' + platformId + ']"]').val();
|
||||
var type = $('select[name="platform_type[' + platformId + ']"]').val();
|
||||
var apiUrl = $('input[name="platform_api_url[' + platformId + ']"]').val();
|
||||
var apiKey = $('input[name="platform_api_key[' + platformId + ']"]').val();
|
||||
|
||||
if (!name) {
|
||||
layer.msg('平台名称不能为空', {icon: 2});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
layer.msg('请选择平台类型', {icon: 2});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!apiUrl) {
|
||||
layer.msg('API URL不能为空', {icon: 2});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
layer.msg('API Key不能为空', {icon: 2});
|
||||
return;
|
||||
}
|
||||
|
||||
// 这里应该是AJAX请求保存数据
|
||||
// 模拟保存成功并获取新的平台ID
|
||||
var newPlatformId = platformId.startsWith('temp_') ? 'new_' + new Date().getTime() : platformId;
|
||||
|
||||
// 更新行的data-id属性
|
||||
$('#platformTable tr[data-id="' + platformId + '"]').attr('data-id', newPlatformId);
|
||||
|
||||
// 更新表单元素的name属性中的ID
|
||||
$('#platformTable tr[data-id="' + newPlatformId + '"] input, #platformTable tr[data-id="' + newPlatformId + '"] select').each(function() {
|
||||
var oldName = $(this).attr('name');
|
||||
var newName = oldName.replace(platformId, newPlatformId);
|
||||
$(this).attr('name', newName);
|
||||
});
|
||||
|
||||
// 更新按钮的data-id属性
|
||||
$('#platformTable tr[data-id="' + newPlatformId + '"] button').data('id', newPlatformId);
|
||||
|
||||
// 更新默认平台下拉框
|
||||
var select = $('select[name="selected_platform"]');
|
||||
// 检查是否已存在相同ID的选项
|
||||
if (select.find('option[value="' + newPlatformId + '"]').length === 0) {
|
||||
select.append('<option value="' + newPlatformId + '">' + name + '</option>');
|
||||
form.render('select'); // 重新渲染select
|
||||
}
|
||||
|
||||
// 显示保存成功提示
|
||||
layer.msg('保存成功', {icon: 1});
|
||||
});
|
||||
|
||||
// 删除平台
|
||||
$(document).on('click', '.delete-platform', function() {
|
||||
var platformId = $(this).data('id');
|
||||
layer.confirm('确定要删除这个平台配置吗?', {
|
||||
btn: ['确定', '取消']
|
||||
}, function(index) {
|
||||
// 删除行
|
||||
$('#platformTable tr[data-id="' + platformId + '"]').remove();
|
||||
|
||||
// 从下拉框中移除对应的选项
|
||||
$('select[name="selected_platform"] option[value="' + platformId + '"]').remove();
|
||||
form.render('select');
|
||||
|
||||
// 检查表格是否为空,如果为空则显示提示
|
||||
if ($('#platformTable tr').length === 0) {
|
||||
$('#platformTable').append('<tr><td colspan="5" style="text-align: center;">暂无平台配置</td></tr>');
|
||||
}
|
||||
|
||||
layer.close(index);
|
||||
layer.msg('删除成功', {icon: 1});
|
||||
});
|
||||
});
|
||||
|
||||
// 禁用/开启平台
|
||||
$(document).on('click', '.toggle-platform', function() {
|
||||
var platformId = $(this).data('id');
|
||||
var status = $(this).data('status');
|
||||
var newStatus = status === 1 ? 0 : 1;
|
||||
|
||||
// 更新按钮状态
|
||||
$(this).data('status', newStatus);
|
||||
$(this).html(newStatus === 1 ? '<i class="layui-icon layui-icon-radio"></i>' : '<i class="layui-icon layui-icon-circle"></i>');
|
||||
$(this).attr('title', newStatus === 1 ? '开启' : '禁用');
|
||||
|
||||
// 如果平台被禁用,从默认平台下拉框中移除
|
||||
if (newStatus === 0) {
|
||||
var option = $('select[name="selected_platform"] option[value="' + platformId + '"]');
|
||||
if (option.length > 0) {
|
||||
option.detach().data('disabled', true); // 保存选项但不显示
|
||||
form.render('select');
|
||||
}
|
||||
}
|
||||
// 如果平台被启用,将选项添加回下拉框
|
||||
else {
|
||||
var savedOption = $('select[name="selected_platform"] option[data-disabled="true"][value="' + platformId + '"]');
|
||||
if (savedOption.length === 0) {
|
||||
// 如果没有保存的选项,获取平台名称并创建新选项
|
||||
var platformName = $('#platformTable tr[data-id="' + platformId + '"] input[name^="platform_name"]').val();
|
||||
if (platformName) {
|
||||
$('select[name="selected_platform"]').append('<option value="' + platformId + '">' + platformName + '</option>');
|
||||
form.render('select');
|
||||
}
|
||||
} else {
|
||||
savedOption.removeData('disabled').appendTo('select[name="selected_platform"]');
|
||||
form.render('select');
|
||||
}
|
||||
}
|
||||
|
||||
layer.msg(newStatus === 1 ? '已开启' : '已禁用', {icon: 1});
|
||||
});
|
||||
|
||||
// 添加新平台
|
||||
$('#addPlatform').on('click', function() {
|
||||
// 生成一个临时ID
|
||||
var tempId = 'temp_' + new Date().getTime();
|
||||
|
||||
// 创建新行HTML
|
||||
var newRow = '<tr data-id="' + tempId + '">' +
|
||||
'<td>' +
|
||||
'<input type="text" name="platform_name[' + tempId + ']" ' +
|
||||
'class="layui-input layui-table-edit" placeholder="请输入AI服务平台名称">' +
|
||||
'</td>' +
|
||||
'<td>' +
|
||||
'<select name="platform_type[' + tempId + ']" class="layui-select layui-table-edit">' +
|
||||
'{foreach $support_ai_platform_types as $type}' +
|
||||
'<option value="{$type.value}">{$type.label}</option>' +
|
||||
'{/foreach}' +
|
||||
'</select>' +
|
||||
'</td>' +
|
||||
'<td>' +
|
||||
'<input type="text" name="platform_api_url[' + tempId + ']" ' +
|
||||
'class="layui-input layui-table-edit" placeholder="请输入API URL">' +
|
||||
'</td>' +
|
||||
'<td>' +
|
||||
'<input type="text" name="platform_api_key[' + tempId + ']" ' +
|
||||
'class="layui-input layui-table-edit" placeholder="请输入API Key">' +
|
||||
'</td>' +
|
||||
'<td class="layui-row">' +
|
||||
'<button class="layui-col-xs4 layui-col-sm12 layui-col-md4 layui-btn layui-btn-xs btn-icon delete-platform" data-id="' + tempId + '" title="删除">' +
|
||||
'<i class="layui-icon layui-icon-delete"></i>' +
|
||||
'</button>' +
|
||||
'<button class="layui-col-xs4 layui-col-sm12 layui-col-md4 layui-btn layui-btn-xs btn-icon toggle-platform" data-id="' + tempId + '" data-status="0" title="开启">'+
|
||||
'<i class="layui-icon layui-icon-circle"></i>' +
|
||||
'</button>' +
|
||||
'<button class="layui-col-xs4 layui-col-sm12 layui-col-md4 layui-btn layui-btn-xs btn-icon save-platform" data-id="' + tempId + '" title="保存">' +
|
||||
'<i class="layui-icon layui-icon-ok"></i>' +
|
||||
'</button>' +
|
||||
'</td>' +
|
||||
'</tr>'
|
||||
|
||||
// 检查是否有暂无平台配置的提示行
|
||||
var emptyRow = $('#platformTable tr:has(td[colspan="5"])');
|
||||
if (emptyRow.length > 0) {
|
||||
emptyRow.replaceWith(newRow);
|
||||
} else {
|
||||
$('#platformTable').append(newRow);
|
||||
}
|
||||
|
||||
// 重新渲染表单元素
|
||||
form.render();
|
||||
|
||||
// 聚焦到新添加的行的第一个输入框
|
||||
$('input[name="platform_name[' + tempId + ']"]').focus();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
141
src/app/shop/view/config/ai/agent.html
Normal file
141
src/app/shop/view/config/ai/agent.html
Normal file
@@ -0,0 +1,141 @@
|
||||
<div class="layui-tab-item layui-show">
|
||||
<!-- AI智能客服配置 -->
|
||||
<div class="layui-form form-wrap card-common">
|
||||
<!-- 基础设置 -->
|
||||
<fieldset class="layui-elem-field layui-field-title">
|
||||
<legend>基础设置</legend>
|
||||
</fieldset>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">启用状态</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox" name="enable" lay-skin="switch" lay-text="开启|关闭" {if
|
||||
condition="isset($agent_info) && $agent_info.enable" }checked{/if}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 语言切换 -->
|
||||
<div class="layui-form-item">
|
||||
<div class="layui-tab lang-tabs" lay-filter="langTab">
|
||||
<ul class="layui-tab-title">
|
||||
<li class="layui-this" lay-id="zh_CN">简体中文</li>
|
||||
<li lay-id="en">English</li>
|
||||
</ul>
|
||||
<div class="layui-tab-content">
|
||||
<div class="layui-tab-item layui-show">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">AI客服姓名</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="ai_name[zh_CN]" placeholder="请输入AI客服姓名" class="layui-input"
|
||||
value="{$agent_info.ai_name.zh_CN|default=''}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">AI客服头像</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="ai_avatar[zh_CN]" placeholder="请输入AI客服头像URL"
|
||||
class="layui-input" value="{$agent_info.ai_avatar.zh_CN|default=''}">
|
||||
<div class="word-aux">支持JPG、PNG、GIF格式,建议尺寸:100x100px</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">用户头像</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="user_avatar[zh_CN]" placeholder="请输入用户头像URL"
|
||||
class="layui-input" value="{$agent_info.user_avatar.zh_CN|default=''}">
|
||||
<div class="word-aux">支持JPG、PNG、GIF格式,建议尺寸:100x100px</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">欢迎语</label>
|
||||
<div class="layui-input-block">
|
||||
<textarea name="welcome_messages[zh_CN]" placeholder="请输入欢迎语,每行一条"
|
||||
class="layui-textarea" rows="4">{if isset($agent_info.welcome_messages.zh_CN)}{foreach $agent_info.welcome_messages.zh_CN as $msg}{$msg}
|
||||
{/foreach}{else}您好,我是智能客服助手,有什么可以帮助您的吗?{/if}</textarea>
|
||||
<!-- <div class="word-aux">每行一条欢迎语,系统会随机选择一条显示</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="layui-tab-item">
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">AI Name</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="ai_name[en]" placeholder="Please enter AI name"
|
||||
class="layui-input" value="{$agent_info.ai_name.en|default=''}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">AI Avatar</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="ai_avatar[en]" placeholder="Please enter AI avatar URL"
|
||||
class="layui-input" value="{$agent_info.ai_avatar.en|default=''}">
|
||||
<div class="word-aux">Support JPG, PNG, GIF formats, recommended size:
|
||||
100x100px</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">User Avatar</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="text" name="user_avatar[en]" placeholder="Please enter user avatar URL"
|
||||
class="layui-input" value="{$agent_info.user_avatar.en|default=''}">
|
||||
<div class="word-aux">Support JPG, PNG, GIF formats, recommended size:
|
||||
100x100px</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">Welcome Messages</label>
|
||||
<div class="layui-input-block">
|
||||
<textarea name="welcome_messages[en]"
|
||||
placeholder="Please enter welcome messages, one per line" class="layui-textarea"
|
||||
rows="4">{if isset($agent_info.welcome_messages.en)}{foreach $agent_info.welcome_messages.en as $msg}{$msg}
|
||||
{/foreach}{else}Hello, I'm the AI customer service assistant. How can I help you?{/if}</textarea>
|
||||
<!-- <div class="word-aux">One welcome message per line, the system will randomly select one to display</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 显示设置 -->
|
||||
<fieldset class="layui-elem-field layui-field-title">
|
||||
<legend>显示设置</legend>
|
||||
</fieldset>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">显示加载更多按钮</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox" name="show_load_more_button" lay-skin="switch" lay-text="开启|关闭" {if
|
||||
condition="isset($agent_info) && $agent_info.show_load_more_button" }checked{else}checked{/if}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">最大消息数量</label>
|
||||
<div class="layui-input-inline" style="width: 100px; margin-left: 50px;">
|
||||
<input type="number" name="max_messages" value="{$agent_info.max_messages|default=100}"
|
||||
class="layui-input" min="10" max="500">
|
||||
</div>
|
||||
<div class="layui-form-mid">条</div>
|
||||
<div class="word-aux" style="margin-left: 110px;">建议设置在10-500条之间</div>
|
||||
</div>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">启用流式响应</label>
|
||||
<div class="layui-input-block">
|
||||
<input type="checkbox" name="stream_mode" lay-skin="switch" lay-text="开启|关闭" {if
|
||||
condition="isset($agent_info) && $agent_info.stream_mode" }checked{else}checked{/if}>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 独立的保存按钮 -->
|
||||
<div class="layui-form-item">
|
||||
<button class="layui-btn" lay-submit lay-filter="save_aiagent_cfg">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
209
src/app/shop/view/config/ai/index.html
Normal file
209
src/app/shop/view/config/ai/index.html
Normal file
@@ -0,0 +1,209 @@
|
||||
<!-- 页面样式定义 -->
|
||||
<style>
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
.layui-card {
|
||||
margin: 20px 0;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.layui-card-header {
|
||||
background-color: #f2f2f2;
|
||||
padding: 12px 20px;
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
.layui-card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.layui-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.layui-field-title {
|
||||
margin-top: 25px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.layui-field-title legend {
|
||||
padding: 0 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.word-aux {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #f00;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.form-wrap {
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
min-height: calc(100vh - 40px);
|
||||
}
|
||||
|
||||
.lang-tabs {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.lang-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.lang-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.layui-form-label {
|
||||
padding: 9px 15px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.layui-input-block {
|
||||
margin-left: 150px;
|
||||
}
|
||||
|
||||
.all-shop-information {
|
||||
width: 100%;
|
||||
background: white;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.all-shop-information .all-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.all-shop-information .all-top .title {
|
||||
color: #333333;
|
||||
margin-bottom: 0;
|
||||
font-size: 17px;
|
||||
padding-left: 10px;
|
||||
border-left: 3px solid var(--base-color);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
select[name="selected_platform"] + .layui-form-select {
|
||||
min-width: 400px;
|
||||
max-width: fit-content;
|
||||
}
|
||||
|
||||
/* 扁平化表格样式 */
|
||||
.layui-table {
|
||||
margin: 10px 0;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
.layui-table th {
|
||||
font-weight: 400;
|
||||
background-color: #f5f7fa;
|
||||
border-bottom: 1px solid #e6e8eb;
|
||||
text-align: center;
|
||||
}
|
||||
.layui-table td {
|
||||
vertical-align: top;
|
||||
border-bottom: 1px solid #e6e8eb;
|
||||
padding: 12px 15px;
|
||||
}
|
||||
|
||||
.layui-table tbody tr:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
/* 调整类型列宽度 */
|
||||
.layui-table th:nth-child(2),
|
||||
.layui-table td:nth-child(2) {
|
||||
width: 120px;
|
||||
}
|
||||
/* 调整输入框样式 */
|
||||
.layui-table-edit {
|
||||
width: calc(100% - 4px);
|
||||
border-radius: 2px;
|
||||
border: 1px solid #dcdfe6;
|
||||
transition: border-color 0.2s;
|
||||
/* 确保输入框和选择框垂直居中对齐 */
|
||||
line-height: 30px;
|
||||
padding: 0 10px;
|
||||
box-sizing: border-box;
|
||||
margin: 4px;
|
||||
margin-top: 12px;
|
||||
height: 34px;
|
||||
}
|
||||
.layui-table-edit:focus {
|
||||
border-color: #c0c4cc;
|
||||
outline: none;
|
||||
}
|
||||
.layui-table td select.layui-select {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
padding: 0 10px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* 调整操作列宽度 */
|
||||
.layui-table th:last-child,
|
||||
.layui-table td:last-child {
|
||||
width: 120px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* 按钮图标样式 */
|
||||
.btn-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
margin: 0 2px;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="layui-form">
|
||||
<!-- 根据功能分两个Tab进行配置,一个是AI智能客服,一个是AI平台 -->
|
||||
<div class="layui-tab" lay-filter="mainTab">
|
||||
<ul class="layui-tab-title">
|
||||
<li class="layui-this" lay-id="ai_agent">AI智能客服</li>
|
||||
<li lay-id="ai_platform">AI服务平台</li>
|
||||
</ul>
|
||||
<div class="layui-tab-content">
|
||||
<!-- AI服务平台配置,必须使用绝对路径 -->
|
||||
{include file="app/shop/view/config/ai/platform.html" /}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 页面脚本 -->
|
||||
<script>
|
||||
// layui 2.5.5 版本
|
||||
layui.use(['form', 'layer', 'element'], function () {
|
||||
var form = layui.form;
|
||||
var layer = layui.layer;
|
||||
var element = layui.element;
|
||||
|
||||
// 初始化表单
|
||||
form.render();
|
||||
|
||||
// layui 2.5.5版本需要手动初始化Tab组件
|
||||
element.render('tab');
|
||||
|
||||
// 主Tab切换处理
|
||||
element.on('tab(mainTab)', function (data) { });
|
||||
|
||||
// 语言切换 - 使用layui原生的Tab切换,不再需要自定义逻辑
|
||||
element.on('tab(langTab)', function (data) { });
|
||||
});
|
||||
</script>
|
||||
397
src/app/shop/view/config/ai/js/platform.js
Normal file
397
src/app/shop/view/config/ai/js/platform.js
Normal file
@@ -0,0 +1,397 @@
|
||||
// 平台选择下拉框处理
|
||||
form.on('select(platformSelect)', function (data) {
|
||||
var platformId = data.value;
|
||||
if (platformId) {
|
||||
// 可以在这里添加根据平台ID高亮显示对应表格行的逻辑
|
||||
$('#platformTable tr').removeClass('layui-bg-blue');
|
||||
$('#platformTable tr[data-id="' + platformId + '"]').addClass('layui-bg-blue');
|
||||
}
|
||||
});
|
||||
|
||||
// 应用模块选择下拉框处理
|
||||
form.on('select(appModuleSelect)', function (data) {
|
||||
var appModule = data.value;
|
||||
loadPlatformConfigByAppModule(appModule);
|
||||
});
|
||||
|
||||
// 存储平台配置的初始状态,用于检测数据变更
|
||||
function storeInitialData() {
|
||||
$('#platformTable tr[data-id]').each(function () {
|
||||
var platformId = $(this).data('id');
|
||||
var initialData = {
|
||||
name: $('input[name="platform_name[' + platformId + ']"]').val(),
|
||||
type: $('select[name="platform_type[' + platformId + ']"]').val(),
|
||||
base_url: $('input[name="platform_base_url[' + platformId + ']"]').val(),
|
||||
api_key: $('input[name="platform_api_key[' + platformId + ']"]').val(),
|
||||
desc: $('input[name="platform_desc[' + platformId + ']"]').val()
|
||||
};
|
||||
// 使用data属性存储初始数据
|
||||
$(this).data('initial-data', initialData);
|
||||
});
|
||||
}
|
||||
|
||||
// 检查平台数据是否有变更
|
||||
function hasDataChanged(platformId) {
|
||||
var row = $('#platformTable tr[data-id="' + platformId + '"]');
|
||||
var initialData = row.data('initial-data');
|
||||
|
||||
// 对于新添加的平台(没有初始数据),认为有变更
|
||||
if (!initialData) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var currentData = {
|
||||
name: $('input[name="platform_name[' + platformId + ']"]').val(),
|
||||
type: $('select[name="platform_type[' + platformId + ']"]').val(),
|
||||
base_url: $('input[name="platform_base_url[' + platformId + ']"]').val(),
|
||||
api_key: $('input[name="platform_api_key[' + platformId + ']"]').val(),
|
||||
desc: $('input[name="platform_desc[' + platformId + ']"]').val()
|
||||
};
|
||||
|
||||
// 比较初始数据和当前数据
|
||||
for (var key in initialData) {
|
||||
if (initialData[key] !== currentData[key]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// 更新保存按钮图标
|
||||
function updateSaveButtonIcon(platformId) {
|
||||
var saveButton = $('#platformTable tr[data-id="' + platformId + '"] .save-platform');
|
||||
var hasChanged = hasDataChanged(platformId);
|
||||
|
||||
if (hasChanged) {
|
||||
saveButton.find('i').removeClass('layui-icon-ok').addClass('layui-icon-edit');
|
||||
} else {
|
||||
saveButton.find('i').removeClass('layui-icon-edit').addClass('layui-icon-ok');
|
||||
}
|
||||
}
|
||||
|
||||
// 监听输入框变化
|
||||
$(document).on('input propertychange', '#platformTable input.layui-table-edit', function () {
|
||||
var platformId = $(this).closest('tr').data('id');
|
||||
updateSaveButtonIcon(platformId);
|
||||
});
|
||||
|
||||
// 监听选择框变化
|
||||
$(document).on('change', '#platformTable select.layui-table-edit', function () {
|
||||
var platformId = $(this).closest('tr').data('id');
|
||||
updateSaveButtonIcon(platformId);
|
||||
});
|
||||
|
||||
// 初始化时存储初始数据
|
||||
storeInitialData();
|
||||
|
||||
function genNewPlatformId() {
|
||||
return 'temp_' + new Date().getTime() + Math.random().toString(36).substring(2);
|
||||
}
|
||||
|
||||
// 保存平台配置
|
||||
$(document).on('click', '.save-platform', function () {
|
||||
var platformId = $(this).data('id');
|
||||
var name = $('input[name="platform_name[' + platformId + ']"]').val();
|
||||
var type = $('select[name="platform_type[' + platformId + ']"]').val();
|
||||
var apiUrl = $('input[name="platform_base_url[' + platformId + ']"]').val();
|
||||
var apiKey = $('input[name="platform_api_key[' + platformId + ']"]').val();
|
||||
|
||||
if (!name) {
|
||||
layer.msg('平台名称不能为空', { icon: 2 });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!type) {
|
||||
layer.msg('请选择平台类型', { icon: 2 });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!apiUrl) {
|
||||
layer.msg('API URL不能为空', { icon: 2 });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
layer.msg('API Key不能为空', { icon: 2 });
|
||||
return;
|
||||
}
|
||||
|
||||
// 建立临时的平台ID
|
||||
var newPlatformId = String(platformId).startsWith('temp_') ? genNewPlatformId() : platformId;
|
||||
|
||||
// 更新行的data-id属性
|
||||
$('#platformTable tr[data-id="' + platformId + '"]').attr('data-id', newPlatformId);
|
||||
|
||||
// 更新表单元素的name属性中的ID
|
||||
$('#platformTable tr[data-id="' + newPlatformId + '"] input, #platformTable tr[data-id="' + newPlatformId + '"] select').each(function () {
|
||||
var oldName = $(this).attr('name');
|
||||
if (oldName) { // 确保oldName存在才调用replace方法
|
||||
var newName = oldName.replace(platformId, newPlatformId);
|
||||
$(this).attr('name', newName);
|
||||
}
|
||||
});
|
||||
|
||||
// 更新按钮的data-id属性
|
||||
$('#platformTable tr[data-id="' + newPlatformId + '"] button').data('id', newPlatformId);
|
||||
|
||||
// 更新默认平台下拉框
|
||||
var select = $('select[name="selected_platform"]');
|
||||
// 检查是否已存在相同ID的选项
|
||||
if (select.find('option[value="' + newPlatformId + '"]').length === 0) {
|
||||
select.append('<option value="' + newPlatformId + '">' + name + '</option>');
|
||||
form.render('select'); // 重新渲染select
|
||||
}
|
||||
|
||||
// 保存成功后,更新初始数据并将图标改回layui-icon-ok
|
||||
var newRow = $('#platformTable tr[data-id="' + newPlatformId + '"]');
|
||||
var updatedData = {
|
||||
name: name,
|
||||
type: type,
|
||||
base_url: apiUrl,
|
||||
api_key: apiKey,
|
||||
desc: $('input[name="platform_desc[' + newPlatformId + ']"]').val()
|
||||
};
|
||||
newRow.data('initial-data', updatedData);
|
||||
|
||||
// 更新保存按钮图标为layui-icon-ok
|
||||
newRow.find('.save-platform i').removeClass('layui-icon-edit').addClass('layui-icon-ok');
|
||||
|
||||
// 显示保存成功提示
|
||||
layer.msg('保存成功', { icon: 1 });
|
||||
});
|
||||
|
||||
// 删除平台
|
||||
$(document).on('click', '.delete-platform', function () {
|
||||
var platformId = $(this).data('id');
|
||||
layer.confirm('确定要删除这个平台配置吗?', {
|
||||
btn: ['确定', '取消']
|
||||
}, function (index) {
|
||||
// 删除行
|
||||
$('#platformTable tr[data-id="' + platformId + '"]').remove();
|
||||
|
||||
// 从下拉框中移除对应的选项
|
||||
$('select[name="selected_platform"] option[value="' + platformId + '"]').remove();
|
||||
form.render('select');
|
||||
|
||||
// 检查表格是否为空,如果为空则显示提示
|
||||
if ($('#platformTable tr').length === 0) {
|
||||
$('#platformTable').append('<tr><td colspan="5" style="text-align: center;">暂无平台配置</td></tr>');
|
||||
}
|
||||
|
||||
layer.close(index);
|
||||
});
|
||||
});
|
||||
|
||||
// 禁用/开启平台
|
||||
$(document).on('click', '.toggle-platform', function () {
|
||||
var platformId = $(this).data('id');
|
||||
var status = $(this).data('status');
|
||||
var newStatus = status === 1 ? 0 : 1;
|
||||
|
||||
// 更新按钮状态
|
||||
$(this).data('status', newStatus);
|
||||
$(this).html(newStatus === 1 ? '<i class="layui-icon layui-icon-radio"></i>' : '<i class="layui-icon layui-icon-circle"></i>');
|
||||
$(this).attr('title', newStatus === 1 ? '开启' : '禁用');
|
||||
|
||||
// 如果平台被禁用,从默认平台下拉框中移除
|
||||
if (newStatus === 0) {
|
||||
var option = $('select[name="selected_platform"] option[value="' + platformId + '"]');
|
||||
if (option.length > 0) {
|
||||
option.detach().data('disabled', true); // 保存选项但不显示
|
||||
form.render('select');
|
||||
}
|
||||
}
|
||||
// 如果平台被启用,将选项添加回下拉框
|
||||
else {
|
||||
var savedOption = $('select[name="selected_platform"] option[data-disabled="true"][value="' + platformId + '"]');
|
||||
if (savedOption.length === 0) {
|
||||
// 如果没有保存的选项,获取平台名称并创建新选项
|
||||
var platformName = $('#platformTable tr[data-id="' + platformId + '"] input[name^="platform_name"]').val();
|
||||
if (platformName) {
|
||||
$('select[name="selected_platform"]').append('<option value="' + platformId + '">' + platformName + '</option>');
|
||||
form.render('select');
|
||||
}
|
||||
} else {
|
||||
savedOption.removeData('disabled').appendTo('select[name="selected_platform"]');
|
||||
form.render('select');
|
||||
}
|
||||
}
|
||||
|
||||
layer.msg(newStatus === 1 ? '已设置为开启,保存后生效' : '已设置为禁用,保存后生效', { icon: 1 });
|
||||
});
|
||||
|
||||
// 添加新平台
|
||||
$('#addPlatform').on('click', function () {
|
||||
// 生成一个临时ID
|
||||
var tempId = genNewPlatformId();
|
||||
|
||||
// 创建新行HTML
|
||||
var newRow = '<tr data-id="' + tempId + '">' +
|
||||
'<td>' +
|
||||
'<input type="text" name="platform_name[' + tempId + ']" ' +
|
||||
'class="layui-input layui-table-edit" placeholder="请输入AI服务平台名称">' +
|
||||
'</td>' +
|
||||
'<td>' +
|
||||
'<select name="platform_type[' + tempId + ']" class="layui-select layui-table-edit">' +
|
||||
'{foreach $support_ai_platform_types as $type}' +
|
||||
'<option value="{$type.value}">{$type.label}</option>' +
|
||||
'{/foreach}' +
|
||||
'</select>' +
|
||||
'</td>' +
|
||||
'<td>' +
|
||||
'<input type="text" name="platform_base_url[' + tempId + ']" ' +
|
||||
'class="layui-input layui-table-edit" placeholder="请输入API URL">' +
|
||||
'</td>' +
|
||||
'<td>' +
|
||||
'<input type="text" name="platform_api_key[' + tempId + ']" ' +
|
||||
'class="layui-input layui-table-edit" placeholder="请输入API Key">' +
|
||||
'</td>' +
|
||||
'<td>' +
|
||||
'<input type="text" name="platform_desc[' + tempId + ']" ' +
|
||||
'class="layui-input layui-table-edit" placeholder="请输入描述">' +
|
||||
'</td>' +
|
||||
'<td class="layui-row">' +
|
||||
'<button class="layui-col-xs6 layui-col-sm6 layui-col-md6 layui-btn layui-btn-xs btn-icon delete-platform" data-id="' + tempId + '" title="删除">' +
|
||||
'<i class="layui-icon layui-icon-delete"></i>' +
|
||||
'</button>' +
|
||||
'<button class="layui-col-xs6 layui-col-sm6 layui-col-md6 layui-btn layui-btn-xs btn-icon toggle-platform" data-id="' + tempId + '" data-status="0" title="开启">' +
|
||||
'<i class="layui-icon layui-icon-circle"></i>' +
|
||||
'</button>' +
|
||||
'<button class="layui-col-xs6 layui-col-sm6 layui-col-md6 layui-btn layui-btn-xs btn-icon save-platform" data-id="' + tempId + '" title="保存">' +
|
||||
'<i class="layui-icon layui-icon-ok"></i>' +
|
||||
'</button>' +
|
||||
'</td>' +
|
||||
'</tr>';
|
||||
|
||||
// 获取表格body
|
||||
var tbody = $('#platformTableBody');
|
||||
|
||||
// 检查是否有暂无平台配置的提示行
|
||||
var emptyRow = tbody.find('tr:has(td[colspan="5"])');
|
||||
if (emptyRow.length > 0) {
|
||||
emptyRow.replaceWith(newRow);
|
||||
} else {
|
||||
tbody.append(newRow);
|
||||
}
|
||||
|
||||
// 重新渲染表单元素
|
||||
form.render();
|
||||
|
||||
// 为新添加的行设置初始数据为null,确保图标显示为编辑状态
|
||||
tbody.find('tr[data-id="' + tempId + '"]').data('initial-data', null);
|
||||
|
||||
// 新添加的行默认显示编辑图标
|
||||
tbody.find('tr[data-id="' + tempId + '"] .save-platform i').removeClass('layui-icon-ok').addClass('layui-icon-edit');
|
||||
|
||||
// 聚焦到新添加的行的第一个输入框
|
||||
tbody.find('input[name="platform_name[' + tempId + ']"]').focus();
|
||||
});
|
||||
|
||||
// 保存平台配置
|
||||
form.on('submit(save_platform_cfg)', function (data) {
|
||||
// 验证表单数据
|
||||
if (!form.verify()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 验证是否选择了默认平台
|
||||
if (!data.field['selected_platform']) {
|
||||
layer.msg('请选择默认AI智能客服平台', { icon: 2 });
|
||||
return false;
|
||||
}
|
||||
|
||||
// 处理新增的平台配置
|
||||
var cfg = {};
|
||||
var platforms = [];
|
||||
$('#platformTableBody tr[data-id]').each(function () {
|
||||
var id = $(this).data('id');
|
||||
var platform = {
|
||||
'id': id,
|
||||
'name': $(this).find('input[name^="platform_name"]').val(),
|
||||
'type': $(this).find('select[name^="platform_type"]').val(),
|
||||
'type_label': $(this).find('select[name^="platform_type"]').find('option:selected').text(),
|
||||
'enable': parseInt($(this).find('button.toggle-platform').data('status') || 0),
|
||||
'desc': $(this).find('input[name^="platform_desc"]').val(),
|
||||
'base_url': $(this).find('input[name^="platform_base_url"]').val(),
|
||||
'api_key': $(this).find('input[name^="platform_api_key"]').val(),
|
||||
};
|
||||
platforms.push(platform);
|
||||
});
|
||||
|
||||
cfg = {
|
||||
'default': {
|
||||
'id': data.field['selected_platform'] || (platforms[0] ? platforms[0].id : ''),
|
||||
'name': data.field['selected_platform_name'] || (platforms[0] ? platforms[0].name : ''),
|
||||
},
|
||||
'list': platforms,
|
||||
};
|
||||
|
||||
$.ajax({
|
||||
url: ns.url("shop/config/ai"),
|
||||
data: {
|
||||
config: JSON.stringify(cfg),
|
||||
type: 'save_platform_cfg',
|
||||
app_module: data.field['app_module']
|
||||
},
|
||||
dataType: 'JSON',
|
||||
type: 'POST',
|
||||
success: function (res) {
|
||||
if (res.code == 0) {
|
||||
layer.msg('保存成功', { icon: 1 });
|
||||
// 重新加载当前应用模块的配置,确保数据同步
|
||||
loadPlatformConfigByAppModule(data.field['app_module']);
|
||||
layer.closeAll();
|
||||
} else {
|
||||
layer.msg('保存失败:' + res.message, { icon: 2 });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 保存AI智能客服平台配置
|
||||
form.on('submit(save_aiagent_cfg)', function (data) {
|
||||
// 验证表单数据
|
||||
if (!form.verify()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('data.field =', data.field);
|
||||
|
||||
var cfg = {};
|
||||
|
||||
// 处理关于AI智能客服配置,将ai_avater[en], ai_avater[zh] 合并到 ai_avater 字段
|
||||
['ai_avater', 'ai_name', 'user_avater', 'welcome_messages'].forEach(function (field) {
|
||||
cfg[field] = {
|
||||
'en': data.field[field + '[en]'],
|
||||
'zh_CN': data.field[field + '[zh_CN]'],
|
||||
};
|
||||
});
|
||||
|
||||
['show_load_more_button', 'stream_mode'].forEach(function (field) {
|
||||
cfg[field] = data.field[field] === 'on';
|
||||
});
|
||||
|
||||
['max_messages'].forEach(function (field) {
|
||||
cfg[field] = data.field[field] || 0;
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
url: ns.url("shop/config/ai"),
|
||||
data: {
|
||||
config: JSON.stringify(cfg),
|
||||
type: 'save_aiagent_cfg',
|
||||
},
|
||||
dataType: 'JSON',
|
||||
type: 'POST',
|
||||
success: function (res) {
|
||||
if (res.code == 0) {
|
||||
layer.msg('保存成功', { icon: 1 });
|
||||
listenerHash(); // 刷新页面
|
||||
layer.closeAll();
|
||||
} else {
|
||||
layer.msg('保存失败:' + res.message, { icon: 2 });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
465
src/app/shop/view/config/ai/platform.html
Normal file
465
src/app/shop/view/config/ai/platform.html
Normal file
@@ -0,0 +1,465 @@
|
||||
<div class="layui-tab-item" lay-id="ai_platform">
|
||||
<!-- AI平台配置 -->
|
||||
<div class="layui-form form-wrap card-common">
|
||||
<!-- 应用模块选择 -->
|
||||
<fieldset class="layui-elem-field layui-field-title">
|
||||
<legend>应用模块选择</legend>
|
||||
</fieldset>
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">应用模块</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="app_module" class="layui-select" lay-filter="appModuleSelect">
|
||||
<option value="">请选择应用模块</option>
|
||||
{foreach $support_app_modules as $item}
|
||||
<option value="{$item.value}">{$item.label}</option>
|
||||
{/foreach}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 平台选择 -->
|
||||
<fieldset class="layui-elem-field layui-field-title">
|
||||
<legend>基本设置</legend>
|
||||
</fieldset>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<label class="layui-form-label">默认平台</label>
|
||||
<div class="layui-input-block">
|
||||
<select name="selected_platform" lay-filter="platformSelect" id="selectedPlatformSelect">
|
||||
<option value="">请选择要默认使用的AI服务平台</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 平台配置表格 -->
|
||||
<fieldset class="layui-elem-field layui-field-title">
|
||||
<legend>AI服务平台配置</legend>
|
||||
</fieldset>
|
||||
|
||||
<div class="layui-form-item">
|
||||
<table class="layui-table" lay-even lay-skin="line">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>类型</th>
|
||||
<th>Base URL</th>
|
||||
<th>API Key</th>
|
||||
<th>描述</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="platformTableBody">
|
||||
<tr>
|
||||
<td colspan="6" style="text-align: center;">请选择应用模块</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 添加新平台 -->
|
||||
<div class="layui-form-item text-center">
|
||||
<button class="layui-btn" id="addPlatform">添加平台</button>
|
||||
</div>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<div class="layui-form-item">
|
||||
<button class="layui-btn" lay-submit lay-filter="save_platform_cfg">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 从PHP获得的应用参数说明
|
||||
* $support_ai_platform_types 支持的AI平台类型
|
||||
* $support_app_modules 支持的应用模块
|
||||
* $platform_info 平台的配置信息,包含各应用模块的平台配置
|
||||
**/
|
||||
|
||||
// 存储PHP传递过来的平台配置数据
|
||||
var support_ai_platform_types = <? php echo json_encode($support_ai_platform_types ?? []); ?>;
|
||||
var support_app_modules = <? php echo json_encode($support_app_modules ?? []); ?>;
|
||||
var platform_info = <? php echo json_encode($platform_info ?? []); ?>;
|
||||
|
||||
console.log('platform_info', platform_info);
|
||||
|
||||
// 创建深度代理对象
|
||||
var proxy_platform_info = new PerformanceDeepProxy(platform_info, {});
|
||||
console.log('proxy_platform_info', proxy_platform_info);
|
||||
|
||||
// 当前选中的应用模块
|
||||
var currentAppModule = '';
|
||||
// 临时编辑状态标识
|
||||
var editingPlatformId = null;
|
||||
|
||||
// 验证平台数据
|
||||
function validatePlatformData(platformData, isEdit, existingId) {
|
||||
// 检查名称是否重复
|
||||
if (!platformData.name || platformData.name.trim() === '') {
|
||||
return '名称不能为空';
|
||||
}
|
||||
|
||||
var moduleInfo = proxy_platform_info[currentAppModule];
|
||||
var platformList = moduleInfo.list || [];
|
||||
var nameExists = platformList.some(function (p) {
|
||||
return p.name === platformData.name && (!isEdit || p.id.toString() !== existingId.toString());
|
||||
});
|
||||
if (nameExists) {
|
||||
return '名称已存在';
|
||||
}
|
||||
|
||||
// 检查类型是否选择
|
||||
if (!platformData.type) {
|
||||
return '类型必须选择';
|
||||
}
|
||||
|
||||
// 检查Base URL是否为空
|
||||
if (!platformData.base_url || platformData.base_url.trim() === '') {
|
||||
return 'Base URL不能为空';
|
||||
}
|
||||
|
||||
// 检查API Key是否为空
|
||||
if (!platformData.api_key || platformData.api_key.trim() === '') {
|
||||
return 'API Key不能为空';
|
||||
}
|
||||
|
||||
return null; // 验证通过
|
||||
}
|
||||
|
||||
// 初始化layui
|
||||
layui.use(['form', 'layer'], function () {
|
||||
var form = layui.form;
|
||||
var layer = layui.layer;
|
||||
|
||||
// 监听应用模块选择变化
|
||||
form.on('select(appModuleSelect)', function (data) {
|
||||
currentAppModule = data.value;
|
||||
editingPlatformId = null; // 重置编辑状态
|
||||
updatePlatformSettings();
|
||||
});
|
||||
|
||||
// 生成平台类型选择框HTML
|
||||
function generatePlatformTypeSelect(selectedValue) {
|
||||
var selectHtml = '<select class="layui-select">';
|
||||
selectHtml += '<option value="">请选择类型</option>';
|
||||
support_ai_platform_types.forEach(function (type) {
|
||||
var selected = selectedValue === type.value ? ' selected' : '';
|
||||
selectHtml += '<option value="' + type.value + '"' + selected + '>' + type.label + '</option>';
|
||||
});
|
||||
selectHtml += '</select>';
|
||||
return selectHtml;
|
||||
}
|
||||
|
||||
// 获取平台类型标签
|
||||
function getPlatformTypeLabel(typeValue) {
|
||||
var type = support_ai_platform_types.find(function (t) { return t.value === typeValue; });
|
||||
return type ? type.label : typeValue;
|
||||
}
|
||||
|
||||
// 更新平台设置(默认平台下拉框和平台配置表格)
|
||||
function updatePlatformSettings() {
|
||||
var platformSelect = document.getElementById('selectedPlatformSelect');
|
||||
var tableBody = document.getElementById('platformTableBody');
|
||||
|
||||
// 清空默认平台下拉框
|
||||
platformSelect.innerHTML = '<option value="">请选择要默认使用的AI服务平台</option>';
|
||||
|
||||
// 如果没有选择应用模块,显示提示信息
|
||||
if (!currentAppModule || !proxy_platform_info[currentAppModule]) {
|
||||
tableBody.innerHTML = '<tr><td colspan="6" style="text-align: center;">请选择应用模块</td></tr>';
|
||||
form.render('select'); // 重新渲染表单
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取当前应用模块的平台信息
|
||||
var moduleInfo = proxy_platform_info[currentAppModule];
|
||||
var platformList = moduleInfo.list || [];
|
||||
var defaultPlatformId = moduleInfo.default && moduleInfo.default.id ? moduleInfo.default.id.toString() : '';
|
||||
|
||||
// 更新默认平台下拉框
|
||||
platformList.forEach(function (platform) {
|
||||
var option = document.createElement('option');
|
||||
option.value = platform.id;
|
||||
option.textContent = platform.name;
|
||||
if (platform.id.toString() === defaultPlatformId) {
|
||||
option.selected = true;
|
||||
}
|
||||
platformSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// 更新平台配置表格
|
||||
if (platformList.length === 0) {
|
||||
tableBody.innerHTML = '<tr><td colspan="6" style="text-align: center;">当前应用模块暂无平台配置</td></tr>';
|
||||
} else {
|
||||
var tableHtml = '';
|
||||
platformList.forEach(function (platform) {
|
||||
// API Key 脱敏显示
|
||||
var displayApiKey = platform.api_key ? platform.api_key.substring(0, 4) + '****' + platform.api_key.substring(platform.api_key.length - 4) : '';
|
||||
|
||||
// 构建操作按钮
|
||||
var actionButtons = '';
|
||||
if (editingPlatformId === platform.id) {
|
||||
// 编辑状态下显示保存和取消按钮
|
||||
actionButtons =
|
||||
'<button class="layui-col-xs6 layui-col-sm6 layui-col-md6 layui-btn layui-btn-xs layui-btn-danger btn-icon delete-platform" data-id="' + platform.id + '"><i class="layui-icon layui-icon-delete"></i></button>' +
|
||||
'<button class="layui-col-xs6 layui-col-sm6 layui-col-md6 layui-btn layui-btn-xs layui-btn-primary btn-icon cancel-edit-platform" onclick="cancelEditPlatform(' + platform.id + ')">取消</button>' +
|
||||
'<button class="layui-col-xs6 layui-col-sm6 layui-col-md6 layui-btn layui-btn-xs btn-icon save-platform" onclick="saveEditPlatform(' + platform.id + ')">保存</button>';
|
||||
} else {
|
||||
// 普通状态下显示编辑、删除和启用/禁用按钮
|
||||
var toggleIcon = platform.enable ? 'layui-icon-radio' : 'layui-icon-circle';
|
||||
var toggleText = platform.enable ? '启用' : '禁用';
|
||||
var toggleStatus = platform.enable ? '0' : '1';
|
||||
|
||||
actionButtons =
|
||||
'<button class="layui-col-xs6 layui-col-sm6 layui-col-md6 layui-btn layui-btn-xs layui-btn-danger btn-icon delete-platform" data-id="' + platform.id + '"><i class="layui-icon layui-icon-delete"></i></button>' +
|
||||
'<button class="layui-col-xs6 layui-col-sm6 layui-col-md6 layui-btn layui-btn-xs layui-btn-warm btn-icon toggle-platform" data-id="' + platform.id + '" data-status="' + toggleStatus + '" title="' + toggleText + '">' + '<i class="layui-icon ' + toggleIcon + '"></i>' + '</button>' +
|
||||
'<button class="layui-col-xs6 layui-col-sm6 layui-col-md6 layui-btn layui-btn-xs btn-icon save-platform" data-id="' + platform.id + '"><i class="layui-icon layui-icon-ok"></i></button>';
|
||||
|
||||
}
|
||||
|
||||
tableHtml += '<tr data-id="' + platform.id + '">' +
|
||||
'<td>' + (editingPlatformId === platform.id ? '<input type="text" class="layui-input" value="' + platform.name + '" />' : platform.name) + '</td>' +
|
||||
'<td>' + (editingPlatformId === platform.id ? generatePlatformTypeSelect(platform.type) : platform.type_label) + '</td>' +
|
||||
'<td>' + (editingPlatformId === platform.id ? '<input type="text" class="layui-input" value="' + platform.base_url + '" />' : platform.base_url) + '</td>' +
|
||||
'<td>' + (editingPlatformId === platform.id ? '<input type="text" class="layui-input" value="' + platform.api_key + '" />' : displayApiKey) + '</td>' +
|
||||
'<td>' + (editingPlatformId === platform.id ? '<input type="text" class="layui-input" value="' + (platform.desc || '') + '" />' : (platform.desc || '')) + '</td>' +
|
||||
'<td class="layui-row">' + actionButtons + '</td>' +
|
||||
'</tr>';
|
||||
});
|
||||
tableBody.innerHTML = tableHtml;
|
||||
|
||||
// 如果当前有编辑的行,重新渲染表单以初始化select组件
|
||||
if (editingPlatformId) {
|
||||
form.render('select');
|
||||
}
|
||||
}
|
||||
|
||||
// 重新渲染表单
|
||||
form.render('select');
|
||||
}
|
||||
|
||||
// 切换平台启用状态
|
||||
$('.toggle-platform').on('click', function () {
|
||||
var id = $(this).data('id');
|
||||
var status = $(this).data('status');
|
||||
if (!currentAppModule || !proxy_platform_info[currentAppModule]) return;
|
||||
|
||||
// 查找平台
|
||||
var platformList = proxy_platform_info[currentAppModule].list;
|
||||
var platform = platformList.find(function (p) { return p.id === id; });
|
||||
|
||||
if (platform) {
|
||||
// 切换启用状态
|
||||
platform.enable = platform.enable ? 0 : 1;
|
||||
// 同步更新 platform_info 中的数据
|
||||
var platformList = proxy_platform_info[currentAppModule].list;
|
||||
var idx = platformList.findIndex(function (p) { return p.id === id; });
|
||||
if (idx !== -1) {
|
||||
platformList[idx].enable = platform.enable;
|
||||
}
|
||||
updatePlatformSettings();
|
||||
layui.layer.msg(platform.enable ? '已启用' : '已关闭');
|
||||
}
|
||||
});
|
||||
|
||||
// 编辑平台
|
||||
|
||||
|
||||
// 添加平台按钮点击事件
|
||||
document.getElementById('addPlatform').onclick = function () {
|
||||
if (!currentAppModule) {
|
||||
layer.msg('请先选择应用模块');
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保platform_info中存在当前应用模块的数据
|
||||
if (!proxy_platform_info[currentAppModule]) {
|
||||
proxy_platform_info[currentAppModule] = {
|
||||
default: null,
|
||||
list: []
|
||||
};
|
||||
}
|
||||
|
||||
// 生成新的平台ID(使用时间戳)
|
||||
var newId = Date.now();
|
||||
|
||||
// 创建新的平台对象
|
||||
var newPlatform = {
|
||||
id: newId,
|
||||
name: '',
|
||||
type: '',
|
||||
type_label: '',
|
||||
base_url: '',
|
||||
api_key: '',
|
||||
desc: '',
|
||||
enable: 1
|
||||
};
|
||||
|
||||
// 添加到平台列表
|
||||
proxy_platform_info[currentAppModule].list.push(newPlatform);
|
||||
|
||||
// 设置为编辑状态
|
||||
editingPlatformId = newId;
|
||||
|
||||
// 更新表格显示
|
||||
updatePlatformSettings();
|
||||
|
||||
// 滚动到新添加的行
|
||||
setTimeout(function () {
|
||||
var newRow = document.querySelector('tr[data-id="' + newId + '"]');
|
||||
if (newRow) {
|
||||
newRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
// 聚焦到第一个输入框
|
||||
var firstInput = newRow.querySelector('input');
|
||||
if (firstInput) firstInput.focus();
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// 保存按钮点击事件(占位,需要根据实际需求实现)
|
||||
form.on('submit(save_platform_cfg)', function (data) {
|
||||
if (!currentAppModule) {
|
||||
layer.msg('请先选择应用模块');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否有正在编辑的行
|
||||
if (editingPlatformId) {
|
||||
layer.msg('请先完成当前行的编辑操作');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 添加应用模块信息到提交数据中
|
||||
data.field.app_module = currentAppModule;
|
||||
data.field.platform_data = proxy_platform_info[currentAppModule];
|
||||
|
||||
// 这里应该发送保存请求,暂时用alert显示要保存的数据
|
||||
layer.msg('保存功能待实现');
|
||||
console.log('要保存的数据:', data.field);
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
// 编辑平台
|
||||
function editPlatform(id) {
|
||||
if (!currentAppModule || !proxy_platform_info[currentAppModule]) return;
|
||||
|
||||
// 查找要编辑的平台
|
||||
var platformList = proxy_platform_info[currentAppModule].list;
|
||||
var platform = platformList.find(function (p) { return p.id === id; });
|
||||
|
||||
if (platform) {
|
||||
editingPlatformId = id;
|
||||
updatePlatformSettings();
|
||||
|
||||
// 滚动到编辑行并聚焦
|
||||
setTimeout(function () {
|
||||
var editRow = document.querySelector('tr[data-id="' + id + '"]');
|
||||
if (editRow) {
|
||||
editRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
var firstInput = editRow.querySelector('input');
|
||||
if (firstInput) firstInput.focus();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
// 取消编辑
|
||||
function cancelEditPlatform(id) {
|
||||
if (!currentAppModule || !proxy_platform_info[currentAppModule]) return;
|
||||
|
||||
// 如果是新增的行且未保存,则从列表中移除
|
||||
var platformList = proxy_platform_info[currentAppModule].list;
|
||||
var platformIndex = platformList.findIndex(function (p) { return p.id === id; });
|
||||
|
||||
if (platformIndex !== -1 && !platformList[platformIndex].name) {
|
||||
// 如果是未填写任何内容的新行,直接删除
|
||||
platformList.splice(platformIndex, 1);
|
||||
}
|
||||
|
||||
editingPlatformId = null;
|
||||
updatePlatformSettings();
|
||||
}
|
||||
|
||||
// 保存编辑
|
||||
function saveEditPlatform(id) {
|
||||
if (!currentAppModule || !proxy_platform_info[currentAppModule]) return;
|
||||
|
||||
var rowElement = document.querySelector('tr[data-id="' + id + '"]');
|
||||
if (!rowElement) return;
|
||||
|
||||
// 获取表单数据
|
||||
var inputs = rowElement.querySelectorAll('input');
|
||||
var selects = rowElement.querySelectorAll('select');
|
||||
|
||||
var platformData = {
|
||||
name: inputs[0] ? inputs[0].value.trim() : '',
|
||||
type: selects[0] ? selects[0].value : '',
|
||||
base_url: inputs[1] ? inputs[1].value.trim() : '',
|
||||
api_key: inputs[2] ? inputs[2].value.trim() : '',
|
||||
desc: inputs[3] ? inputs[3].value.trim() : ''
|
||||
};
|
||||
|
||||
// 验证数据
|
||||
var validationError = validatePlatformData(platformData, true, id);
|
||||
if (validationError) {
|
||||
layui.layer.msg(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找要更新的平台
|
||||
var platformList = proxy_platform_info[currentAppModule].list;
|
||||
var platformIndex = platformList.findIndex(function (p) { return p.id === id; });
|
||||
|
||||
if (platformIndex !== -1) {
|
||||
// 更新平台数据
|
||||
platformList[platformIndex].name = platformData.name;
|
||||
platformList[platformIndex].type = platformData.type;
|
||||
platformList[platformIndex].type_label = getPlatformTypeLabel(platformData.type);
|
||||
platformList[platformIndex].base_url = platformData.base_url;
|
||||
platformList[platformIndex].api_key = platformData.api_key;
|
||||
platformList[platformIndex].desc = platformData.desc;
|
||||
}
|
||||
|
||||
editingPlatformId = null;
|
||||
updatePlatformSettings();
|
||||
layui.layer.msg('保存成功');
|
||||
}
|
||||
|
||||
// 删除平台
|
||||
function deletePlatform(id) {
|
||||
layui.layer.confirm('确定要删除这个平台吗?', function (index) {
|
||||
if (!currentAppModule || !proxy_platform_info[currentAppModule]) {
|
||||
layui.layer.close(index);
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找并删除平台
|
||||
var platformList = proxy_platform_info[currentAppModule].list;
|
||||
var platformIndex = platformList.findIndex(function (p) { return p.id === id; });
|
||||
|
||||
if (platformIndex !== -1) {
|
||||
platformList.splice(platformIndex, 1);
|
||||
|
||||
// 如果删除的是默认平台,清除默认设置
|
||||
if (proxy_platform_info[currentAppModule].default &&
|
||||
proxy_platform_info[currentAppModule].default.id.toString() === id.toString()) {
|
||||
proxy_platform_info[currentAppModule].default = null;
|
||||
}
|
||||
|
||||
updatePlatformSettings();
|
||||
layui.layer.msg('删除成功');
|
||||
}
|
||||
|
||||
layui.layer.close(index);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 获取平台类型标签
|
||||
function getPlatformTypeLabel(typeValue) {
|
||||
var type = support_ai_platform_types.find(function (t) { return t.value === typeValue; });
|
||||
return type ? type.label : typeValue;
|
||||
}
|
||||
</script>
|
||||
@@ -22,6 +22,7 @@
|
||||
<link rel="stylesheet" type="text/css" href="SHOP_CSS/style2/common.css?v={$version}" />
|
||||
<script src="__STATIC__/js/jquery-3.1.1.js"></script>
|
||||
<script src="__STATIC__/js/jquery.cookie.js"></script>
|
||||
<script src="__STATIC__/js/deep-proxy-1.0.js?t={$version}5"></script>
|
||||
<script src="__STATIC__/ext/layui/layui.js"></script>
|
||||
<script>
|
||||
layui.use(['layer', 'upload', 'element'], function() {});
|
||||
|
||||
101
src/app/tracking/VisitorTracker.php
Normal file
101
src/app/tracking/VisitorTracker.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace app\tracking;
|
||||
|
||||
use app\common\library\BrowserDetector;
|
||||
|
||||
use app\model\stat\StatShop;
|
||||
use app\model\stat\StatStore;
|
||||
|
||||
class VisitorTracker
|
||||
{
|
||||
|
||||
private $detector;
|
||||
private $sourceType;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->detector = new BrowserDetector();
|
||||
$this->sourceType = $this->detector->getSourceType();
|
||||
log_write('访问来源检测:' . $this->sourceType, 'debug');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取通用埋点数据
|
||||
* @param $params 埋点参数
|
||||
* @param array $fileds 埋点字段
|
||||
* @return array 埋点数据
|
||||
*/
|
||||
private function getCommonTraceData($params, $fileds = ['site_id'])
|
||||
{
|
||||
$statData = [
|
||||
'visit_count' => 1,
|
||||
];
|
||||
|
||||
foreach ($fileds as $filed) {
|
||||
$statData[$filed] = $params[$filed] ?? 0;
|
||||
}
|
||||
|
||||
switch ($this->sourceType) {
|
||||
case 'miniprogram':
|
||||
$statData = array_merge($statData, ['weapp_visit_count' => 1,]);
|
||||
break;
|
||||
case 'wechat':
|
||||
$statData = array_merge($statData, ['wechat_visit_count' => 1,]);
|
||||
break;
|
||||
case 'h5':
|
||||
$statData = array_merge($statData, ['h5_visit_count' => 1,]);
|
||||
break;
|
||||
case 'pc':
|
||||
$statData = array_merge($statData, ['pc_visit_count' => 1,]);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return $statData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 店铺UV埋点
|
||||
* @param $params 埋点参数
|
||||
* @return bool
|
||||
*/
|
||||
public function shop_visit($params)
|
||||
{
|
||||
try {
|
||||
$statData = $this->getCommonTraceData($params, ['site_id', 'store_id']);
|
||||
// 访问来源统计
|
||||
if (!empty($statData)) {
|
||||
$stat_model = new StatShop();
|
||||
$stat_model->addShopStat($statData);
|
||||
}
|
||||
return true;
|
||||
} catch (\Throwable $th) {
|
||||
//throw $th;
|
||||
log_write('访问者跟踪失败:' . $th->getMessage(), 'error');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 门店UV埋点
|
||||
* @param $params 埋点参数
|
||||
* @return bool
|
||||
*/
|
||||
public function store_visit($params)
|
||||
{
|
||||
try {
|
||||
$statData = $this->getCommonTraceData($params);
|
||||
// 访问来源统计
|
||||
if (!empty($statData)) {
|
||||
$stat_model = new StatStore();
|
||||
$stat_model->addStoreStat($statData);
|
||||
}
|
||||
return true;
|
||||
} catch (\Throwable $th) {
|
||||
//throw $th;
|
||||
log_write('访问者跟踪失败:' . $th->getMessage(), 'error');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,24 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'default' => 'default',//url URL接口启动 cli 命令启动 default 系统任务
|
||||
// 是否开启定时任务
|
||||
'enable' => true,
|
||||
|
||||
/**
|
||||
* 定时任务默认模式
|
||||
* url URL接口启动
|
||||
* cli 命令启动。 是通过 php think cron:schedule 命令创建一个守护进程来启动的
|
||||
* default 系统任务
|
||||
*/
|
||||
'default' => 'cli',
|
||||
|
||||
// 类方法任务列表, yunwuxin/think-cron 配置方式
|
||||
// 然后通过 php think cron:schedule 命令启动一个进程来调度处理
|
||||
// 特别要注意,有的时候,cli 模式下,计划任务会因为守护进程的原因,导致计划任务不执行
|
||||
// 解决办法:
|
||||
// 1. 检查是否开启了守护进程
|
||||
// 2. 检查守护进程是否正常运行
|
||||
// 3. 检查计划任务是否正常运行
|
||||
'tasks' => [
|
||||
\app\command\Schedule::class
|
||||
]
|
||||
|
||||
1124
src/public/static/js/deep-proxy-1.0.js
Normal file
1124
src/public/static/js/deep-proxy-1.0.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user