diff --git a/Server/application/ai/config/route.php b/Server/application/ai/config/route.php index 4fd9f4f6..704b7d09 100644 --- a/Server/application/ai/config/route.php +++ b/Server/application/ai/config/route.php @@ -7,7 +7,7 @@ Route::group('v1/ai', function () { //openai、chatGPT Route::group('openai', function () { - Route::post('text', 'app\ai\controller\OpenAi@text'); + Route::post('text', 'app\ai\controller\OpenAI@text'); }); diff --git a/Server/application/ai/controller/CozeAI.php b/Server/application/ai/controller/CozeAI.php index 9d9bf150..5208326f 100644 --- a/Server/application/ai/controller/CozeAI.php +++ b/Server/application/ai/controller/CozeAI.php @@ -76,7 +76,7 @@ class CozeAI extends Controller $result = requestCurl($this->apiUrl . '/v1/bot/create', $params, 'POST', $this->headers, 'json'); $result = json_decode($result, true); if ($result['code'] != 0) { - return errorJson($result['msg'], $result['code']); + return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]); } return json_encode(['code' => 200, 'msg' => '创建成功', 'data' => $result['data']]); @@ -136,9 +136,9 @@ class CozeAI extends Controller $result = requestCurl($this->apiUrl . '/v1/bot/update', $params, 'POST', $this->headers, 'json'); $result = json_decode($result, true); if ($result['code'] != 0) { - return errorJson($result['msg'], $result['code']); + return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]); } - return json_encode(['code' => 200, 'msg' => '获取成功']); + return json_encode(['code' => 200, 'msg' => '更新成功', 'data' => []]); } @@ -162,9 +162,9 @@ class CozeAI extends Controller $result = requestCurl($this->apiUrl . '/v1/bot/publish', $params, 'POST', $this->headers, 'json'); $result = json_decode($result, true); if ($result['code'] != 0) { - return errorJson($result['msg'], $result['code']); + return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]); } - return json_encode(['code' => 200, 'msg' => '发布成功']); + return json_encode(['code' => 200, 'msg' => '发布成功', 'data' => []]); } @@ -191,7 +191,7 @@ class CozeAI extends Controller $result = json_decode($result, true); if ($result['code'] != 0) { - return errorJson($result['msg'], $result['code']); + return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]); } return json_encode(['code' => 200, 'msg' => '创建成功','data' => $result['data']]); } @@ -231,7 +231,7 @@ class CozeAI extends Controller $result = requestCurl($this->apiUrl . '/open_api/knowledge/document/create', $params, 'POST', $headers, 'json'); $result = json_decode($result, true); if ($result['code'] != 0) { - return errorJson($result['msg'], $result['code']); + return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]); } return json_encode(['code' => 200, 'msg' => '创建成功','data' => $result['document_infos']]); } @@ -254,9 +254,9 @@ class CozeAI extends Controller $result = requestCurl($this->apiUrl . '/open_api/knowledge/document/delete', $params, 'POST', $headers, 'json'); $result = json_decode($result, true); if ($result['code'] != 0) { - return errorJson($result['msg'], $result['code']); + return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]); } - return json_encode(['code' => 200, 'msg' => '删除成功']); + return json_encode(['code' => 200, 'msg' => '删除成功', 'data' => []]); } @@ -285,7 +285,7 @@ class CozeAI extends Controller $result = requestCurl($this->apiUrl . '/v1/conversation/create', $params, 'POST', $this->headers, 'json'); $result = json_decode($result, true); if ($result['code'] != 0) { - return errorJson($result['msg'], $result['code']); + return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]); } return json_encode(['code' => 200, 'msg' => '创建成功','data' => $result['data']]); } @@ -306,15 +306,15 @@ class CozeAI extends Controller if(empty($bot_id)){ - return errorJson('智能体ID不能为空'); + return json_encode(['code' => 500, 'msg' => '智能体ID不能为空', 'data' => []]); } if(empty($conversation_id)){ - return errorJson('会话ID不能为空'); + return json_encode(['code' => 500, 'msg' => '会话ID不能为空', 'data' => []]); } if(empty($question)){ - return errorJson('问题不能为空'); + return json_encode(['code' => 500, 'msg' => '问题不能为空', 'data' => []]); } // 构建请求数据 @@ -330,16 +330,21 @@ class CozeAI extends Controller $result = requestCurl($url, $params, 'POST', $this->headers, 'json'); $result = json_decode($result, true); if ($result['code'] != 0) { - return errorJson($result['msg'], $result['code']); + return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]); } return json_encode(['code' => 200, 'msg' => '发送成功','data' => $result['data']]); } catch (\Exception $e) { - return errorJson('创建对话失败:' . $e->getMessage()); + return json_encode(['code' => 500, 'msg' => '创建对话失败:' . $e->getMessage(), 'data' => []]); } } + /** + * 查看对话详情 + * @param $data + * @return false|string|\think\response\Json + */ public function getConversationChat($data = []) { $conversation_id = !empty($data['conversation_id']) ? $data['conversation_id'] : ''; @@ -348,12 +353,17 @@ class CozeAI extends Controller $result = requestCurl($url, [], 'GET', $this->headers, 'json'); $result = json_decode($result, true); if ($result['code'] != 0) { - return errorJson($result['msg'], $result['code']); + return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]); } - return json_encode(['code' => 200, 'msg' => '发送成功','data' => $result['data']]); + return json_encode(['code' => 200, 'msg' => '获取成功','data' => $result['data']]); } + /** + * 查看对话消息详情 + * @param $data + * @return false|string|\think\response\Json + */ public function listConversationMessage($data = []) { $conversation_id = !empty($data['conversation_id']) ? $data['conversation_id'] : ''; @@ -362,9 +372,35 @@ class CozeAI extends Controller $result = requestCurl($url, [], 'GET', $this->headers, 'json'); $result = json_decode($result, true); if ($result['code'] != 0) { - return errorJson($result['msg'], $result['code']); + return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]); } - return json_encode(['code' => 200, 'msg' => '发送成功','data' => $result['data']]); + return json_encode(['code' => 200, 'msg' => '获取成功','data' => $result['data']]); + } + + + /** + * 取消进行中的对话 + * @param $data + * @return false|string|\think\response\Json + */ + public function cancelConversationChat($data = []) + { + $conversation_id = !empty($data['conversation_id']) ? $data['conversation_id'] : ''; + $chat_id = !empty($data['chat_id']) ? $data['chat_id'] : ''; + + // 构建请求数据 + $params = [ + 'conversation_id' => (string) $conversation_id, + 'chat_id' => (string) $chat_id + ]; + + $url = $this->apiUrl . '/v3/chat/cancel'; + $result = requestCurl($url, $params, 'POST', $this->headers, 'json'); + $result = json_decode($result, true); + if ($result['code'] != 0) { + return json_encode(['code' => $result['code'], 'msg' => $result['msg'], 'data' => []]); + } + return json_encode(['code' => 200, 'msg' => '取消成功', 'data' => []]); } } \ No newline at end of file diff --git a/Server/application/ai/controller/DouBaoAI.php b/Server/application/ai/controller/DouBaoAI.php index e7a50646..5cb71a3b 100644 --- a/Server/application/ai/controller/DouBaoAI.php +++ b/Server/application/ai/controller/DouBaoAI.php @@ -4,15 +4,18 @@ namespace app\ai\controller; use app\common\util\JwtUtil; use think\facade\Env; +use think\Controller; -class DouBaoAI +class DouBaoAI extends Controller { protected $apiUrl; protected $apiKey; protected $headers; - public function __init() + public function __construct() { + parent::__construct(); + $this->apiUrl = Env::get('doubaoAi.api_url'); $this->apiKey = Env::get('doubaoAi.api_key'); @@ -31,7 +34,7 @@ class DouBaoAI public function text($params = []) { - $this->__init(); + if (empty($params)){ return json_encode(['code' => 500, 'msg' => '提示词缺失']); } diff --git a/Server/application/ai/controller/OpenAi.php b/Server/application/ai/controller/OpenAi.php index b71491f6..ac0ff245 100644 --- a/Server/application/ai/controller/OpenAi.php +++ b/Server/application/ai/controller/OpenAi.php @@ -3,14 +3,19 @@ namespace app\ai\controller; use think\facade\Env; -class OpenAI +use think\Controller; + + +class OpenAI extends Controller { protected $apiUrl; protected $apiKey; protected $headers; - public function __init() + public function __construct() { + parent::__construct(); + $this->apiUrl = Env::get('openAi.apiUrl'); $this->apiKey = Env::get('openAi.apiKey'); @@ -24,7 +29,7 @@ class OpenAI public function text() { - $this->__init(); + $params = [ 'model' => 'gpt-3.5-turbo-0125', 'input' => 'DHA 从孕期到出生到老年都需要,助力大脑发育🧠/减缓脑压力有助记忆/给大脑动力#贝蒂喜藻油DHA 双标认证每粒 150毫克,高含量、高性价比从小吃到老,长期吃更健康 重写这条朋友圈 要求: 1、原本的字数和意思不要修改超过10% 2、出现品牌名或个人名字就去除' @@ -46,7 +51,6 @@ class OpenAI */ public function bedtimeStory() { - $this->__init(); // API请求参数 $params = [ diff --git a/Server/application/chukebao/config/route.php b/Server/application/chukebao/config/route.php index 9dd32c2c..353f4877 100644 --- a/Server/application/chukebao/config/route.php +++ b/Server/application/chukebao/config/route.php @@ -12,10 +12,12 @@ Route::group('v1/', function () { //好友相关 Route::group('wechatFriend/', function () { Route::get('list', 'app\chukebao\controller\WechatFriendController@getList'); // 获取好友列表 + Route::get('detail', 'app\chukebao\controller\WechatFriendController@getDetail'); // 获取好友详情 }); //群相关 Route::group('wechatChatroom/', function () { Route::get('list', 'app\chukebao\controller\WechatChatroomController@getList'); // 获取好友列表 + Route::get('detail', 'app\chukebao\controller\WechatChatroomController@getDetail'); // 获取群详情 Route::post('aiAnnouncement', 'app\chukebao\controller\WechatChatroomController@aiAnnouncement'); // AI群公告 }); @@ -139,8 +141,18 @@ Route::group('v1/', function () { Route::get('stats', 'app\chukebao\controller\AutoGreetingsController@stats'); }); + //AI智能推送 + Route::group('aiPush/', function () { + Route::get('list', 'app\chukebao\controller\AiPushController@getList'); // 获取推送列表 + Route::post('add', 'app\chukebao\controller\AiPushController@add'); // 添加推送 + Route::get('details', 'app\chukebao\controller\AiPushController@details'); // 推送详情 + Route::delete('del', 'app\chukebao\controller\AiPushController@del'); // 删除推送 + Route::post('update', 'app\chukebao\controller\AiPushController@update'); // 更新推送 + Route::get('setStatus', 'app\chukebao\controller\AiPushController@setStatus'); // 修改状态 + Route::get('stats', 'app\chukebao\controller\AiPushController@stats'); // 统计概览 + }); + - //自动问候 Route::group('notice/', function () { Route::get('list', 'app\chukebao\controller\NoticeController@getList'); Route::put('readMessage', 'app\chukebao\controller\NoticeController@readMessage'); diff --git a/Server/application/chukebao/controller/AiChatController.php b/Server/application/chukebao/controller/AiChatController.php index 919204f0..4090d578 100644 --- a/Server/application/chukebao/controller/AiChatController.php +++ b/Server/application/chukebao/controller/AiChatController.php @@ -11,6 +11,9 @@ use app\chukebao\model\FriendSettings; use app\chukebao\model\TokensCompany; use library\ResponseHelper; use think\Db; +use think\facade\Cache; +use think\facade\Log; + /** * AI聊天控制器 @@ -27,8 +30,16 @@ class AiChatController extends BaseController const STATUS_CANCELED = 'canceled'; // 对话已取消 // 轮询配置 - const MAX_RETRY_TIMES = 30; // 最大重试次数 - const RETRY_INTERVAL = 2; // 重试间隔(秒) + const MAX_RETRY_TIMES = 1000; // 最大重试次数 + const RETRY_INTERVAL = 500000; // 重试间隔(微秒,即500毫秒) + + // 并发控制 + const CACHE_EXPIRE = 30; // 缓存过期时间(秒) + + // 请求唯一标识符 + private $requestKey = ''; + private $requestId = ''; + private $currentStep = 0; /** * AI聊天主入口 @@ -40,59 +51,219 @@ class AiChatController extends BaseController try { // 1. 参数验证和初始化 $params = $this->validateAndInitParams(); + if ($params === false) { return ResponseHelper::error('参数验证失败'); } + + // 并发控制:检查并处理同一用户的重复请求 + $this->requestKey = "aichat_{$params['friendId']}_{$params['wechatAccountId']}"; + $this->requestId = uniqid('req_', true); + + $concurrentCheck = $this->handleConcurrentRequest($params); + if ($concurrentCheck !== true) { + return $concurrentCheck; // 返回错误响应 + } + + $this->currentStep = 1; // 2. 验证Tokens余额 - if (!$this->checkTokensBalance($params['companyId'])) { + $this->updateRequestStep(2); + if ($this->isRequestCanceled()) { + return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消'); + } + $hasBalance = $this->checkTokensBalance($params['companyId']); + + if (!$hasBalance) { + $this->clearRequestCache(); return ResponseHelper::error('Tokens余额不足,请充值后再试'); } // 3. 获取AI配置 + $this->updateRequestStep(3); + if ($this->isRequestCanceled()) { + return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消'); + } $setting = $this->getAiSettings($params['companyId']); + if (!$setting) { + $this->clearRequestCache(); return ResponseHelper::error('未找到AI配置信息,请先配置AI策略'); } // 4. 获取好友AI设置 + $this->updateRequestStep(4); + if ($this->isRequestCanceled()) { + return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消'); + } $friendSettings = $this->getFriendSettings($params['companyId'], $params['friendId']); + if (!$friendSettings) { + $this->clearRequestCache(); return ResponseHelper::error('该好友未配置或未开启AI功能'); } // 5. 确保会话存在 + $this->updateRequestStep(5); + if ($this->isRequestCanceled()) { + return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消'); + } $conversationId = $this->ensureConversation($friendSettings, $setting, $params); - if (!$conversationId) { + + if (empty($conversationId)) { + $this->clearRequestCache(); return ResponseHelper::error('创建会话失败'); } // 6. 获取历史消息 + $this->updateRequestStep(6); + if ($this->isRequestCanceled()) { + return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消'); + } $msgData = $this->getHistoryMessages($params['friendId'], $friendSettings); - // 7. 创建AI对话 + // 7. 创建AI对话(从这步开始需要保存对话ID以便取消) + $this->updateRequestStep(7); + if ($this->isRequestCanceled($conversationId, null)) { + return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消'); + } $chatId = $this->createAiChat($setting, $friendSettings, $msgData); - if (!$chatId) { + + if (empty($chatId)) { + $this->clearRequestCache(); return ResponseHelper::error('创建对话失败'); } + + // 保存对话ID到缓存,以便新请求可以取消 + $this->updateRequestStep(7, $conversationId, $chatId); // 8. 等待AI处理完成(轮询) - $chatResult = $this->waitForChatCompletion($conversationId, $chatId); - if (!$chatResult) { - return ResponseHelper::error('AI处理超时或失败'); + $this->updateRequestStep(8, $conversationId, $chatId); + if ($this->isRequestCanceled($conversationId, $chatId)) { + return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消'); } + $chatResult = $this->waitForChatCompletion($conversationId, $chatId); + + if (!$chatResult['success']) { + $this->clearRequestCache(); + return ResponseHelper::error($chatResult['error']); + } + + $chatResult = $chatResult['data']; // 9. 扣除Tokens + $this->updateRequestStep(9, $conversationId, $chatId); + if ($this->isRequestCanceled($conversationId, $chatId)) { + return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消'); + } $this->consumeTokens($chatResult, $params, $friendSettings); // 10. 获取对话消息 + $this->updateRequestStep(10, $conversationId, $chatId); + if ($this->isRequestCanceled($conversationId, $chatId)) { + return ResponseHelper::error('该好友有新的AI对话请求正在处理中,当前请求已被取消'); + } $messages = $this->getChatMessages($conversationId, $chatId); + if (!$messages) { return ResponseHelper::error('获取对话消息失败'); } - return ResponseHelper::success($messages[1]['content'], '对话成功'); + + // 筛选type为answer的消息(AI回复的内容) + $answerContent = ''; + foreach ($messages as $msg) { + if (isset($msg['type']) && $msg['type'] === 'answer') { + $answerContent = $msg['content'] ?? ''; + break; + } + } + + if (empty($answerContent)) { + Log::warning('未找到AI回复内容,messages: ' . json_encode($messages)); + return ResponseHelper::error('未获取到AI回复内容'); + } + + // 清理请求缓存 + $this->clearRequestCache(); + + // 返回结果 + return ResponseHelper::success(['content' => $answerContent], '对话成功'); + } catch (\Exception $e) { - \think\facade\Log::error('AI聊天异常:' . $e->getMessage()); + Log::error('AI聊天异常:' . $e->getMessage()); + + // 清理请求缓存 + $this->clearRequestCache(); + + return ResponseHelper::error('系统异常:' . $e->getMessage()); + } + } + + /** + * 取消AI对话 + * 取消当前正在进行的AI对话请求 + * + * @return \think\response\Json + */ + public function cancel() + { + try { + // 获取参数 + $friendId = $this->request->param('friendId', ''); + $wechatAccountId = $this->request->param('wechatAccountId', ''); + + if (empty($wechatAccountId) || empty($friendId)) { + return ResponseHelper::error('参数缺失'); + } + + // 生成缓存键 + $requestKey = "aichat_{$friendId}_{$wechatAccountId}"; + + // 获取缓存数据 + $cacheData = Cache::get($requestKey); + + if (!$cacheData) { + return ResponseHelper::error('当前没有正在进行的AI对话'); + } + + $requestId = $cacheData['request_id'] ?? ''; + $step = $cacheData['step'] ?? 0; + $conversationId = $cacheData['conversation_id'] ?? ''; + $chatId = $cacheData['chat_id'] ?? ''; + + Log::info("手动取消AI对话 - 请求ID: {$requestId}, 步骤: {$step}"); + + // 如果已经到达步骤7或之后,需要调用取消API + if ($step >= 7 && !empty($conversationId) && !empty($chatId)) { + try { + $cozeAI = new CozeAI(); + $cancelResult = $cozeAI->cancelConversationChat([ + 'conversation_id' => $conversationId, + 'chat_id' => $chatId, + ]); + + $result = json_decode($cancelResult, true); + if ($result['code'] != 200) { + Log::error("调用取消API失败 - conversation_id: {$conversationId}, chat_id: {$chatId}, 错误: " . ($result['msg'] ?? '未知错误')); + } else { + Log::info("成功调用取消API - conversation_id: {$conversationId}, chat_id: {$chatId}"); + } + } catch (\Exception $e) { + Log::error("调用取消API异常:" . $e->getMessage()); + } + } + + // 清理缓存 + Cache::rm($requestKey); + Log::info("已清理AI对话缓存 - 请求ID: {$requestId}"); + + return ResponseHelper::success([ + 'canceled_request_id' => $requestId, + 'step' => $step + ], 'AI对话已取消'); + + } catch (\Exception $e) { + Log::error('取消AI对话异常:' . $e->getMessage()); return ResponseHelper::error('系统异常:' . $e->getMessage()); } } @@ -194,7 +365,7 @@ class AiChatController extends BaseController $res = json_decode($res, true); if ($res['code'] != 200) { - \think\facade\Log::error('创建会话失败:' . ($res['msg'] ?? '未知错误')); + Log::error('创建会话失败:' . ($res['msg'] ?? '未知错误')); return null; } @@ -203,7 +374,6 @@ class AiChatController extends BaseController $friendSettings->conversationId = $conversationId; $friendSettings->conversationTime = time(); $friendSettings->save(); - return $conversationId; } @@ -292,7 +462,7 @@ class AiChatController extends BaseController $res = json_decode($res, true); if ($res['code'] != 200) { - \think\facade\Log::error('创建对话失败:' . ($res['msg'] ?? '未知错误')); + Log::error('创建对话失败:' . ($res['msg'] ?? '未知错误')); return null; } @@ -304,7 +474,7 @@ class AiChatController extends BaseController * * @param string $conversationId 会话ID * @param string $chatId 对话ID - * @return array|null + * @return array ['success' => bool, 'data' => array|null, 'error' => string] */ private function waitForChatCompletion($conversationId, $chatId) { @@ -320,8 +490,9 @@ class AiChatController extends BaseController $res = json_decode($res, true); if ($res['code'] != 200) { - \think\facade\Log::error('获取对话状态失败:' . ($res['msg'] ?? '未知错误')); - return null; + $errorMsg = 'AI接口调用失败:' . ($res['msg'] ?? '未知错误'); + Log::error($errorMsg); + return ['success' => false, 'data' => null, 'error' => $errorMsg]; } $status = $res['data']['status'] ?? ''; @@ -330,36 +501,41 @@ class AiChatController extends BaseController switch ($status) { case self::STATUS_COMPLETED: // 对话完成,返回结果 - return $res['data']; + return ['success' => true, 'data' => $res['data'], 'error' => '']; case self::STATUS_IN_PROGRESS: case self::STATUS_CREATED: // 继续等待 $retryCount++; - sleep(self::RETRY_INTERVAL); + usleep(self::RETRY_INTERVAL); break; case self::STATUS_FAILED: - \think\facade\Log::error('对话失败,chat_id: ' . $chatId); - return null; + $errorMsg = 'AI对话处理失败'; + Log::error($errorMsg . ',chat_id: ' . $chatId); + return ['success' => false, 'data' => null, 'error' => $errorMsg]; case self::STATUS_CANCELED: - \think\facade\Log::error('对话已取消,chat_id: ' . $chatId); - return null; + $errorMsg = 'AI对话已被取消'; + Log::error($errorMsg . ',chat_id: ' . $chatId); + return ['success' => false, 'data' => null, 'error' => $errorMsg]; case self::STATUS_REQUIRES_ACTION: - \think\facade\Log::warning('对话需要进一步处理,chat_id: ' . $chatId); - return null; + $errorMsg = 'AI对话需要进一步处理'; + Log::warning($errorMsg . ',chat_id: ' . $chatId); + return ['success' => false, 'data' => null, 'error' => $errorMsg]; default: - \think\facade\Log::error('未知状态:' . $status); - return null; + $errorMsg = 'AI返回未知状态:' . $status; + Log::error($errorMsg); + return ['success' => false, 'data' => null, 'error' => $errorMsg]; } } // 超时 - \think\facade\Log::error('对话处理超时,chat_id: ' . $chatId); - return null; + $errorMsg = 'AI对话处理超时,已等待' . (self::MAX_RETRY_TIMES * self::RETRY_INTERVAL / 1000000) . '秒'; + Log::error($errorMsg . ',chat_id: ' . $chatId); + return ['success' => false, 'data' => null, 'error' => $errorMsg]; } /** @@ -412,13 +588,154 @@ class AiChatController extends BaseController $res = json_decode($res, true); if ($res['code'] != 200) { - \think\facade\Log::error('获取对话消息失败:' . ($res['msg'] ?? '未知错误')); + Log::error('获取对话消息失败:' . ($res['msg'] ?? '未知错误')); return null; } return $res['data'] ?? []; } + /** + * 处理并发请求 + * 检查是否有同一用户的旧请求正在处理,如果有则取消旧请求 + * + * @param array $params 请求参数 + * @return true|\think\response\Json true表示可以继续,否则返回错误响应 + */ + private function handleConcurrentRequest($params) + { + $cacheData = Cache::get($this->requestKey); + + if ($cacheData) { + // 有旧请求正在处理 + $oldRequestId = $cacheData['request_id'] ?? ''; + $oldStep = $cacheData['step'] ?? 0; + $oldConversationId = $cacheData['conversation_id'] ?? ''; + $oldChatId = $cacheData['chat_id'] ?? ''; + + Log::info("检测到并发请求 - 旧请求: {$oldRequestId} (步骤{$oldStep}), 新请求: {$this->requestId}"); + + // 如果旧请求已经到达步骤7或之后,需要调用取消API + if ($oldStep >= 7 && !empty($oldConversationId) && !empty($oldChatId)) { + try { + $cozeAI = new CozeAI(); + $cancelResult = $cozeAI->cancelConversationChat([ + 'conversation_id' => $oldConversationId, + 'chat_id' => $oldChatId, + ]); + Log::info("已调用取消API取消旧请求的对话 - conversation_id: {$oldConversationId}, chat_id: {$oldChatId}"); + } catch (\Exception $e) { + Log::error("取消旧请求对话失败:" . $e->getMessage()); + } + } + + // 标记旧请求为已取消(通过更新缓存的 canceled 标志) + $cacheData['canceled'] = true; + $cacheData['canceled_by'] = $this->requestId; + Cache::set($this->requestKey, $cacheData, self::CACHE_EXPIRE); + } + + // 设置当前请求为活动请求 + $newCacheData = [ + 'request_id' => $this->requestId, + 'step' => 1, + 'start_time' => time(), + 'canceled' => false, + 'conversation_id' => '', + 'chat_id' => '', + ]; + Cache::set($this->requestKey, $newCacheData, self::CACHE_EXPIRE); + + return true; + } + + /** + * 检查当前请求是否被新请求取消 + * + * @param string $conversationId 会话ID(可选,用于取消对话) + * @param string $chatId 对话ID(可选,用于取消对话) + * @return bool + */ + private function isRequestCanceled($conversationId = '', $chatId = '') + { + $cacheData = Cache::get($this->requestKey); + + if (!$cacheData) { + // 缓存不存在,说明被清理或过期,视为被取消 + return true; + } + + $currentRequestId = $cacheData['request_id'] ?? ''; + $isCanceled = $cacheData['canceled'] ?? false; + + // 如果缓存中的请求ID与当前请求ID不一致,或者被标记为取消 + if ($currentRequestId !== $this->requestId || $isCanceled) { + Log::info("当前请求已被取消 - 请求ID: {$this->requestId}, 缓存请求ID: {$currentRequestId}, 取消标志: " . ($isCanceled ? 'true' : 'false')); + + // 如果提供了对话ID,尝试取消对话 + if (!empty($conversationId) && !empty($chatId) && $this->currentStep >= 7) { + try { + $cozeAI = new CozeAI(); + $cancelResult = $cozeAI->cancelConversationChat([ + 'conversation_id' => $conversationId, + 'chat_id' => $chatId, + ]); + Log::info("已取消当前请求的对话 - conversation_id: {$conversationId}, chat_id: {$chatId}"); + } catch (\Exception $e) { + Log::error("取消当前请求对话失败:" . $e->getMessage()); + } + } + + return true; + } + + return false; + } + + /** + * 更新请求步骤 + * + * @param int $step 当前步骤 + * @param string $conversationId 会话ID(可选) + * @param string $chatId 对话ID(可选) + */ + private function updateRequestStep($step, $conversationId = '', $chatId = '') + { + $this->currentStep = $step; + + $cacheData = Cache::get($this->requestKey); + + if ($cacheData && $cacheData['request_id'] === $this->requestId) { + $cacheData['step'] = $step; + $cacheData['update_time'] = time(); + + if (!empty($conversationId)) { + $cacheData['conversation_id'] = $conversationId; + } + if (!empty($chatId)) { + $cacheData['chat_id'] = $chatId; + } + + Cache::set($this->requestKey, $cacheData, self::CACHE_EXPIRE); + } + } + + /** + * 清理请求缓存 + */ + private function clearRequestCache() + { + if (!empty($this->requestKey)) { + $cacheData = Cache::get($this->requestKey); + + // 只有当前请求才能清理自己的缓存 + if ($cacheData && isset($cacheData['request_id']) && $cacheData['request_id'] === $this->requestId) { + Cache::rm($this->requestKey); + Log::info("已清理请求缓存 - 请求ID: {$this->requestId}"); + } + } + } + public function index2222() { diff --git a/Server/application/chukebao/controller/AiPushController.php b/Server/application/chukebao/controller/AiPushController.php new file mode 100644 index 00000000..b1cecbd6 --- /dev/null +++ b/Server/application/chukebao/controller/AiPushController.php @@ -0,0 +1,505 @@ +request->param('page', 1); + $limit = $this->request->param('limit', 10); + $keyword = $this->request->param('keyword', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + $where = [ + ['companyId', '=', $companyId], + ['userId', '=', $userId], + ['isDel', '=', 0], + ]; + + if (!empty($keyword)) { + $where[] = ['name', 'like', '%' . $keyword . '%']; + } + + $query = AiPush::where($where); + $total = $query->count(); + $list = $query->where($where)->page($page, $limit)->order('id desc')->select(); + + // 处理数据 + $list = is_array($list) ? $list : $list->toArray(); + foreach ($list as &$item) { + // 解析标签数组 + $item['tags'] = json_decode($item['tags'], true); + if (!is_array($item['tags'])) { + $item['tags'] = []; + } + // 格式化推送时机显示文本 + $timingTypes = [ + 1 => '立即推送', + 2 => 'AI最佳时机', + 3 => '定时推送' + ]; + $item['timingText'] = $timingTypes[$item['pushTiming']] ?? '未知'; + // 处理定时推送时间 + if ($item['pushTiming'] == 3 && !empty($item['scheduledTime'])) { + $item['scheduledTime'] = date('Y-m-d H:i:s', $item['scheduledTime']); + } else { + $item['scheduledTime'] = ''; + } + // 从记录表计算实际成功率 + $pushId = $item['id']; + $totalCount = Db::name('kf_ai_push_record') + ->where('pushId', $pushId) + ->count(); + $sendCount = Db::name('kf_ai_push_record') + ->where('pushId', $pushId) + ->where('isSend', 1) + ->count(); + $item['successRate'] = $totalCount > 0 ? round(($sendCount * 100) / $totalCount, 1) : 0; + $item['totalPushCount'] = $totalCount; // 推送总数 + $item['sendCount'] = $sendCount; // 成功发送数 + } + unset($item); + + return ResponseHelper::success(['list' => $list, 'total' => $total]); + } + + /** + * 添加 + * @return \think\response\Json + * @throws \Exception + */ + public function add() + { + $name = $this->request->param('name', ''); + $tags = $this->request->param('tags', ''); // 标签,支持逗号分隔的字符串或数组 + $content = $this->request->param('content', ''); + $pushTiming = $this->request->param('pushTiming', 1); // 1=立即推送,2=最佳时机(AI决定),3=定时推送 + $scheduledTime = $this->request->param('scheduledTime', ''); // 定时推送的时间 + $status = $this->request->param('status', 1); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($name) || empty($content)) { + return ResponseHelper::error('推送名称和推送内容不能为空'); + } + + // 验证推送时机 + if (!in_array($pushTiming, [1, 2, 3])) { + return ResponseHelper::error('无效的推送时机类型'); + } + + // 如果是定时推送,需要验证时间 + if ($pushTiming == 3) { + if (empty($scheduledTime)) { + return ResponseHelper::error('定时推送需要设置推送时间'); + } + // 验证时间格式 + $timestamp = strtotime($scheduledTime); + if ($timestamp === false || $timestamp <= time()) { + return ResponseHelper::error('定时推送时间格式不正确或必须大于当前时间'); + } + } else { + $scheduledTime = ''; + } + + // 处理标签 + $tagsArray = []; + if (!empty($tags)) { + if (is_string($tags)) { + // 如果是字符串,按逗号分割 + $tagsArray = array_filter(array_map('trim', explode(',', $tags))); + } elseif (is_array($tags)) { + $tagsArray = array_filter(array_map('trim', $tags)); + } + } + + if (empty($tagsArray)) { + return ResponseHelper::error('目标用户标签不能为空'); + } + + Db::startTrans(); + try { + $aiPush = new AiPush(); + $aiPush->name = $name; + $aiPush->tags = json_encode($tagsArray, JSON_UNESCAPED_UNICODE); + $aiPush->content = $content; + $aiPush->pushTiming = $pushTiming; + $aiPush->scheduledTime = $pushTiming == 3 && !empty($scheduledTime) ? strtotime($scheduledTime) : 0; + $aiPush->status = $status; + $aiPush->successRate = 0; // 初始成功率为0 + $aiPush->userId = $userId; + $aiPush->companyId = $companyId; + $aiPush->createTime = time(); + $aiPush->updateTime = time(); + $aiPush->save(); + Db::commit(); + return ResponseHelper::success(['id' => $aiPush->id], '创建成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('创建失败:' . $e->getMessage()); + } + } + + /** + * 详情 + * @return \think\response\Json + * @throws \Exception + */ + public function details() + { + $id = $this->request->param('id', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($id)) { + return ResponseHelper::error('参数缺失'); + } + + $data = AiPush::where(['id' => $id, 'isDel' => 0, 'userId' => $userId, 'companyId' => $companyId])->find(); + if (empty($data)) { + return ResponseHelper::error('该推送已被删除或者不存在'); + } + + $data = $data->toArray(); + // 解析标签数组 + $data['tags'] = json_decode($data['tags'], true); + if (!is_array($data['tags'])) { + $data['tags'] = []; + } + // 标签转为逗号分隔的字符串(用于编辑时回显) + $data['tagsString'] = implode(',', $data['tags']); + + // 处理定时推送时间 + if ($data['pushTiming'] == 3 && !empty($data['scheduledTime'])) { + $data['scheduledTime'] = date('Y-m-d H:i:s', $data['scheduledTime']); + } else { + $data['scheduledTime'] = ''; + } + + // 成功率保留一位小数 + $data['successRate'] = isset($data['successRate']) ? round($data['successRate'], 1) : 0; + + return ResponseHelper::success($data, '获取成功'); + } + + /** + * 删除 + * @return \think\response\Json + * @throws \Exception + */ + public function del() + { + $id = $this->request->param('id', ''); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($id)) { + return ResponseHelper::error('参数缺失'); + } + + $data = AiPush::where(['id' => $id, 'isDel' => 0, 'userId' => $userId, 'companyId' => $companyId])->find(); + if (empty($data)) { + return ResponseHelper::error('该推送已被删除或者不存在'); + } + + Db::startTrans(); + try { + $data->isDel = 1; + $data->delTime = time(); + $data->save(); + Db::commit(); + return ResponseHelper::success('', '删除成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('删除失败:' . $e->getMessage()); + } + } + + /** + * 更新 + * @return \think\response\Json + * @throws \Exception + */ + public function update() + { + $id = $this->request->param('id', ''); + $name = $this->request->param('name', ''); + $tags = $this->request->param('tags', ''); + $content = $this->request->param('content', ''); + $pushTiming = $this->request->param('pushTiming', 1); + $scheduledTime = $this->request->param('scheduledTime', ''); + $status = $this->request->param('status', 1); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($id) || empty($name) || empty($content)) { + return ResponseHelper::error('参数缺失'); + } + + // 验证推送时机 + if (!in_array($pushTiming, [1, 2, 3])) { + return ResponseHelper::error('无效的推送时机类型'); + } + + // 如果是定时推送,需要验证时间 + if ($pushTiming == 3) { + if (empty($scheduledTime)) { + return ResponseHelper::error('定时推送需要设置推送时间'); + } + // 验证时间格式 + $timestamp = strtotime($scheduledTime); + if ($timestamp === false || $timestamp <= time()) { + return ResponseHelper::error('定时推送时间格式不正确或必须大于当前时间'); + } + } else { + $scheduledTime = ''; + } + + // 处理标签 + $tagsArray = []; + if (!empty($tags)) { + if (is_string($tags)) { + $tagsArray = array_filter(array_map('trim', explode(',', $tags))); + } elseif (is_array($tags)) { + $tagsArray = array_filter(array_map('trim', $tags)); + } + } + + if (empty($tagsArray)) { + return ResponseHelper::error('目标用户标签不能为空'); + } + + $query = AiPush::where(['id' => $id, 'isDel' => 0, 'userId' => $userId, 'companyId' => $companyId])->find(); + if (empty($query)) { + return ResponseHelper::error('该推送已被删除或者不存在'); + } + + Db::startTrans(); + try { + $query->name = $name; + $query->tags = json_encode($tagsArray, JSON_UNESCAPED_UNICODE); + $query->content = $content; + $query->pushTiming = $pushTiming; + $query->scheduledTime = $pushTiming == 3 && !empty($scheduledTime) ? strtotime($scheduledTime) : 0; + $query->status = $status; + $query->updateTime = time(); + $query->save(); + Db::commit(); + return ResponseHelper::success('', '修改成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('修改失败:' . $e->getMessage()); + } + } + + /** + * 修改状态 + * @return \think\response\Json + * @throws \Exception + */ + public function setStatus() + { + $id = $this->request->param('id', ''); + $status = $this->request->param('status', 1); + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + if (empty($id)) { + return ResponseHelper::error('参数缺失'); + } + + if (!in_array($status, [0, 1])) { + return ResponseHelper::error('状态值无效'); + } + + $data = AiPush::where(['id' => $id, 'isDel' => 0, 'userId' => $userId, 'companyId' => $companyId])->find(); + if (empty($data)) { + return ResponseHelper::error('该推送已被删除或者不存在'); + } + + Db::startTrans(); + try { + $data->status = $status; + $data->updateTime = time(); + $data->save(); + Db::commit(); + return ResponseHelper::success('', $status == 1 ? '启用成功' : '禁用成功'); + } catch (\Exception $e) { + Db::rollback(); + return ResponseHelper::error('操作失败:' . $e->getMessage()); + } + } + + /** + * 统计概览(整合自动问候和AI推送) + * - 活跃规则(自动问候规则,近30天) + * - 总触发次数(自动问候记录总数) + * - AI推送成功率(AI推送的成功率) + * - AI智能推送(AI推送规则,近30天活跃) + * - 规则效果排行(自动问候规则,按使用次数排序) + * @return \think\response\Json + */ + public function stats() + { + $companyId = $this->getUserInfo('companyId'); + $userId = $this->getUserInfo('id'); + + $start30d = time() - 30 * 24 * 3600; + + try { + // 公司维度(用于除排行外的统计) + $companyWhere = [ + ['companyId', '=', $companyId], + ]; + // 排行维度(限定个人) + $rankingWhere = [ + ['companyId', '=', $companyId], + ['userId', '=', $userId], + ]; + + // ========== 自动问候统计 ========== + + // 1) 活跃规则(自动问候规则,近30天有记录的) + $activeRules = Db::name('kf_auto_greetings_record') + ->where($companyWhere) + ->where('createTime', '>=', $start30d) + ->distinct(true) + ->count('autoId'); + + // 2) 总触发次数(自动问候记录总数) + $totalTriggers = Db::name('kf_auto_greetings_record') + ->where($companyWhere) + ->count(); + + // ========== AI推送统计 ========== + + // 3) AI推送成功率 + $totalPushes = Db::name('kf_ai_push_record') + ->where($companyWhere) + ->count(); + $sendCount = Db::name('kf_ai_push_record') + ->where($companyWhere) + ->where('isSend', '=', 1) + ->count(); + // 成功率:百分比,保留整数(75%) + $aiPushSuccessRate = $totalPushes > 0 ? round(($sendCount * 100) / $totalPushes, 0) : 0; + + // 4) AI智能推送(AI推送规则,近30天活跃的) + $aiPushCount = Db::name('kf_ai_push_record') + ->where($companyWhere) + ->where('createTime', '>=', $start30d) + ->distinct(true) + ->count('pushId'); + + // ========== 规则效果排行(自动问候规则,按使用次数排序)========== + $ruleRanking = Db::name('kf_auto_greetings_record') + ->where($rankingWhere) + ->field([ + 'autoId AS id', + 'COUNT(*) AS usageCount' + ]) + ->group('autoId') + ->order('usageCount DESC') + ->limit(20) + ->select(); + + // 附加规则名称和触发类型 + $autoIds = array_values(array_unique(array_column($ruleRanking, 'id'))); + $autoIdToRule = []; + if (!empty($autoIds)) { + $rules = AutoGreetings::where([['id', 'in', $autoIds]]) + ->field('id,name,trigger') + ->select(); + foreach ($rules as $rule) { + $triggerTypes = [ + 1 => '新好友', + 2 => '首次发消息', + 3 => '时间触发', + 4 => '关键词', + 5 => '生日触发', + 6 => '自定义' + ]; + $autoIdToRule[$rule['id']] = [ + 'name' => $rule['name'], + 'trigger' => $rule['trigger'], + 'triggerText' => $triggerTypes[$rule['trigger']] ?? '未知', + ]; + } + } + + foreach ($ruleRanking as &$row) { + $row['usageCount'] = (int)($row['usageCount'] ?? 0); + $row['name'] = $autoIdToRule[$row['id']]['name'] ?? ''; + $row['trigger'] = $autoIdToRule[$row['id']]['trigger'] ?? null; + $row['triggerText'] = $autoIdToRule[$row['id']]['triggerText'] ?? ''; + // 格式化使用次数显示 + $row['usageCountText'] = $row['usageCount'] . ' 次'; + } + unset($row); + + // 更新主表中的成功率字段(异步或定期更新) + $this->updatePushSuccessRate($companyId); + + return ResponseHelper::success([ + 'activeRules' => (int)$activeRules, + 'totalTriggers' => (int)$totalTriggers, + 'aiPushSuccessRate' => (int)$aiPushSuccessRate, + 'aiPushCount' => (int)$aiPushCount, + 'ruleRanking' => $ruleRanking, + ], '统计成功'); + } catch (\Exception $e) { + return ResponseHelper::error('统计失败:' . $e->getMessage()); + } + } + + /** + * 更新推送表的成功率字段 + * @param int $companyId + * @return void + */ + private function updatePushSuccessRate($companyId) + { + try { + // 获取所有启用的推送 + $pushes = AiPush::where([ + ['companyId', '=', $companyId], + ['isDel', '=', 0] + ])->field('id')->select(); + + foreach ($pushes as $push) { + $pushId = $push['id']; + $totalCount = Db::name('kf_ai_push_record') + ->where('pushId', $pushId) + ->count(); + $sendCount = Db::name('kf_ai_push_record') + ->where('pushId', $pushId) + ->where('isSend', 1) + ->count(); + + $successRate = $totalCount > 0 ? round(($sendCount * 100) / $totalCount, 2) : 0.00; + + AiPush::where('id', $pushId)->update([ + 'successRate' => $successRate, + 'updateTime' => time() + ]); + } + } catch (\Exception $e) { + // 静默失败,不影响主流程 + } + } +} + diff --git a/Server/application/chukebao/controller/AutoGreetingsController.php b/Server/application/chukebao/controller/AutoGreetingsController.php index 88b66754..ef186b3c 100644 --- a/Server/application/chukebao/controller/AutoGreetingsController.php +++ b/Server/application/chukebao/controller/AutoGreetingsController.php @@ -9,15 +9,19 @@ use think\Db; class AutoGreetingsController extends BaseController { + /** + * 获取问候规则列表 + * @return \think\response\Json + */ public function getList(){ $page = $this->request->param('page', 1); $limit = $this->request->param('limit', 10); $keyword = $this->request->param('keyword', ''); $is_template = $this->request->param('is_template', 0); + $triggerType = $this->request->param('triggerType', ''); // 触发类型筛选 $userId = $this->getUserInfo('id'); $companyId = $this->getUserInfo('companyId'); - if($is_template == 1){ $where = [ ['is_template','=',1], @@ -31,20 +35,46 @@ class AutoGreetingsController extends BaseController ]; } - - - if(!empty($keyword)){ $where[] = ['name','like','%'.$keyword.'%']; } + if(!empty($triggerType)){ + $where[] = ['trigger','=',$triggerType]; + } + $query = AutoGreetings::where($where); $total = $query->count(); - $list = $query->where($where)->page($page,$limit)->order('id desc')->select(); + $list = $query->where($where)->page($page,$limit)->order('level asc,id desc')->select(); + // 获取使用次数 + $list = is_array($list) ? $list : $list->toArray(); + $ids = array_column($list, 'id'); + $usageCounts = []; + if (!empty($ids)) { + $counts = Db::name('kf_auto_greetings_record') + ->where('autoId', 'in', $ids) + ->field('autoId, COUNT(*) as count') + ->group('autoId') + ->select(); + foreach ($counts as $count) { + $usageCounts[$count['autoId']] = (int)$count['count']; + } + } foreach ($list as &$item) { - $item['trigger'] = json_decode($item['trigger'],true); + $item['condition'] = json_decode($item['condition'], true); + $item['usageCount'] = $usageCounts[$item['id']] ?? 0; + // 格式化触发类型显示文本 + $triggerTypes = [ + 1 => '新好友', + 2 => '首次发消息', + 3 => '时间触发', + 4 => '关键词触发', + 5 => '生日触发', + 6 => '自定义' + ]; + $item['triggerText'] = $triggerTypes[$item['trigger']] ?? '未知'; } unset($item); @@ -52,6 +82,315 @@ class AutoGreetingsController extends BaseController } + /** + * 校验trigger类型对应的condition + * @param int $trigger 触发类型 + * @param mixed $condition 条件参数 + * @return array|string 返回处理后的condition数组,或错误信息字符串 + */ + private function validateTriggerCondition($trigger, $condition) + { + // trigger类型:1=新好友,2=首次发消息,3=时间触发,4=关键词触发,5=生日触发,6=自定义 + switch ($trigger) { + case 1: // 新好友 + // 不需要condition + return []; + + case 2: // 首次发消息 + // 不需要condition + return []; + + case 3: // 时间触发 + // 需要condition,格式为:{"type": "daily_time|yearly_datetime|fixed_range|workday", "value": "..."} + if (empty($condition)) { + return '时间触发类型需要配置具体的触发条件'; + } + $condition = is_array($condition) ? $condition : json_decode($condition, true); + if (empty($condition) || !is_array($condition)) { + return '时间触发类型的条件格式不正确,应为数组格式'; + } + + // 验证必须包含type字段 + if (!isset($condition['type']) || empty($condition['type'])) { + return '时间触发类型必须指定触发方式:daily_time(每天固定时间)、yearly_datetime(每年固定日期时间)、fixed_range(固定时间段)、workday(工作日)'; + } + + $timeType = $condition['type']; + $allowedTypes = ['daily_time', 'yearly_datetime', 'fixed_range', 'workday']; + // 兼容旧版本的 fixed_time,自动转换为 daily_time + if ($timeType === 'fixed_time') { + $timeType = 'daily_time'; + } + if (!in_array($timeType, $allowedTypes)) { + return '时间触发类型无效,必须为:daily_time(每天固定时间)、yearly_datetime(每年固定日期时间)、fixed_range(固定时间段)、workday(工作日)'; + } + + // 根据不同的type验证value + switch ($timeType) { + case 'daily_time': // 每天固定时间(每天的几点几分) + // value应该是时间字符串,格式:HH:mm,如 "14:30" + if (!isset($condition['value']) || empty($condition['value'])) { + return '每天固定时间类型需要配置具体时间,格式:HH:mm(如 14:30)'; + } + $timeValue = $condition['value']; + if (!preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $timeValue)) { + return '每天固定时间格式不正确,应为 HH:mm 格式(如 14:30)'; + } + return [ + 'type' => 'daily_time', + 'value' => $timeValue + ]; + + case 'yearly_datetime': // 每年固定日期时间(每年的几月几号几点几分) + // value应该是日期时间字符串,格式:MM-dd HH:mm,如 "12-25 14:30" + if (!isset($condition['value']) || empty($condition['value'])) { + return '每年固定日期时间类型需要配置具体日期和时间,格式:MM-dd HH:mm(如 12-25 14:30)'; + } + $datetimeValue = $condition['value']; + // 验证格式:MM-dd HH:mm + if (!preg_match('/^(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01]) ([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $datetimeValue)) { + return '每年固定日期时间格式不正确,应为 MM-dd HH:mm 格式(如 12-25 14:30)'; + } + // 进一步验证日期是否有效(例如2月30日不存在) + list($datePart, $timePart) = explode(' ', $datetimeValue); + list($month, $day) = explode('-', $datePart); + if (!checkdate((int)$month, (int)$day, 2000)) { // 使用2000年作为参考年份验证日期有效性 + return '日期无效,请检查月份和日期是否正确(如2月不能有30日)'; + } + return [ + 'type' => 'yearly_datetime', + 'value' => $datetimeValue + ]; + + case 'fixed_range': // 固定时间段 + // value应该是时间段数组,格式:["09:00", "18:00"] + if (!isset($condition['value']) || !is_array($condition['value'])) { + return '固定时间段类型需要配置时间段,格式:["开始时间", "结束时间"](如 ["09:00", "18:00"])'; + } + $rangeValue = $condition['value']; + if (count($rangeValue) !== 2) { + return '固定时间段应为包含两个时间点的数组,格式:["09:00", "18:00"]'; + } + // 验证时间格式 + foreach ($rangeValue as $time) { + if (!preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $time)) { + return '时间段格式不正确,应为 HH:mm 格式(如 09:00)'; + } + } + // 验证开始时间小于结束时间 + $startTime = strtotime('2000-01-01 ' . $rangeValue[0]); + $endTime = strtotime('2000-01-01 ' . $rangeValue[1]); + if ($startTime >= $endTime) { + return '开始时间必须小于结束时间'; + } + return [ + 'type' => 'fixed_range', + 'value' => $rangeValue + ]; + + case 'workday': // 工作日 + // 工作日需要配置时间,格式:HH:mm(如 09:00) + if (!isset($condition['value']) || empty($condition['value'])) { + return '工作日触发类型需要配置时间,格式:HH:mm(如 09:00)'; + } + $timeValue = trim($condition['value']); + // 验证格式:HH:mm + if (!preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $timeValue)) { + return '工作日时间格式不正确,应为 HH:mm 格式(如 09:00)'; + } + return [ + 'type' => 'workday', + 'value' => $timeValue + ]; + + default: + return '时间触发类型无效'; + } + + case 4: // 关键词触发 + // 需要condition,格式:{"keywords": ["关键词1", "关键词2"], "match_type": "exact|fuzzy"} + if (empty($condition)) { + return '关键词触发类型需要配置至少一个关键词'; + } + + // 如果是字符串,尝试解析JSON + if (is_string($condition)) { + $decoded = json_decode($condition, true); + if (json_last_error() === JSON_ERROR_NONE) { + $condition = $decoded; + } else { + return '关键词触发类型格式错误,应为对象格式:{"keywords": ["关键词1", "关键词2"], "match_type": "exact|fuzzy"}'; + } + } + + // 必须是对象格式 + if (!is_array($condition) || !isset($condition['keywords'])) { + return '关键词触发类型格式错误,应为对象格式:{"keywords": ["关键词1", "关键词2"], "match_type": "exact|fuzzy"}'; + } + + $keywords = $condition['keywords']; + $matchType = isset($condition['match_type']) ? $condition['match_type'] : 'fuzzy'; + + // 验证match_type + if (!in_array($matchType, ['exact', 'fuzzy'])) { + return '匹配类型无效,必须为:exact(精准匹配)或 fuzzy(模糊匹配)'; + } + + // 处理keywords + if (is_string($keywords)) { + $keywords = explode(',', $keywords); + } + if (!is_array($keywords)) { + return '关键词格式不正确,应为数组格式'; + } + + // 过滤空值并去重 + $keywords = array_filter(array_map('trim', $keywords)); + if (empty($keywords)) { + return '关键词触发类型需要配置至少一个关键词'; + } + + // 验证每个关键词不为空 + foreach ($keywords as $keyword) { + if (empty($keyword)) { + return '关键词不能为空'; + } + } + + return [ + 'keywords' => array_values($keywords), + 'match_type' => $matchType + ]; + + case 5: // 生日触发 + // 需要condition,格式支持: + // 1. 月日字符串:'10-10' 或 '10-10 09:00'(MM-DD格式,不包含年份) + // 2. 对象格式:{'month': 10, 'day': 10, 'time': '09:00'} 或 {'month': '10', 'day': '10', 'time_range': ['09:00', '10:00']} + if (empty($condition)) { + return '生日触发类型需要配置日期条件'; + } + + // 如果是字符串,只接受 MM-DD 格式(不包含年份) + if (is_string($condition)) { + // 检查是否包含时间部分 + if (preg_match('/^(\d{1,2})-(\d{1,2})\s+(\d{2}:\d{2})$/', $condition, $matches)) { + // 格式:'10-10 09:00' + $month = (int)$matches[1]; + $day = (int)$matches[2]; + if ($month < 1 || $month > 12 || $day < 1 || $day > 31) { + return '生日日期格式不正确,月份应为1-12,日期应为1-31'; + } + return [ + 'month' => $month, + 'day' => $day, + 'time' => $matches[3] + ]; + } elseif (preg_match('/^(\d{1,2})-(\d{1,2})$/', $condition, $matches)) { + // 格式:'10-10'(不指定时间,当天任何时间都可以触发) + $month = (int)$matches[1]; + $day = (int)$matches[2]; + if ($month < 1 || $month > 12 || $day < 1 || $day > 31) { + return '生日日期格式不正确,月份应为1-12,日期应为1-31'; + } + return [ + 'month' => $month, + 'day' => $day + ]; + } else { + return '生日日期格式不正确,应为 MM-DD 或 MM-DD HH:mm 格式(如 10-10 或 10-10 09:00),不包含年份'; + } + } + + // 如果是数组,可能是对象格式或旧格式 + if (is_array($condition)) { + // 检查是否是旧格式(仅兼容 MM-DD 格式的数组) + if (isset($condition[0]) && is_string($condition[0])) { + $dateStr = $condition[0]; + // 只接受 MM-DD 格式:'10-10' 或 '10-10 09:00' + if (preg_match('/^(\d{1,2})-(\d{1,2})(?:\s+(\d{2}:\d{2}))?$/', $dateStr, $matches)) { + $month = (int)$matches[1]; + $day = (int)$matches[2]; + if ($month < 1 || $month > 12 || $day < 1 || $day > 31) { + return '生日日期格式不正确,月份应为1-12,日期应为1-31'; + } + if (isset($matches[3])) { + return [ + 'month' => $month, + 'day' => $day, + 'time' => $matches[3] + ]; + } else { + return [ + 'month' => $month, + 'day' => $day + ]; + } + } else { + return '生日日期格式不正确,应为 MM-DD 格式(如 10-10),不包含年份'; + } + } + + // 新格式:{'month': 10, 'day': 10, 'time': '09:00'} + if (isset($condition['month']) && isset($condition['day'])) { + $month = (int)$condition['month']; + $day = (int)$condition['day']; + + if ($month < 1 || $month > 12) { + return '生日月份格式不正确,应为1-12'; + } + if ($day < 1 || $day > 31) { + return '生日日期格式不正确,应为1-31'; + } + + $result = [ + 'month' => $month, + 'day' => $day + ]; + + // 检查是否配置了时间 + if (isset($condition['time']) && !empty($condition['time'])) { + $time = trim($condition['time']); + if (!preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $time)) { + return '生日时间格式不正确,应为 HH:mm 格式(如 09:00)'; + } + $result['time'] = $time; + } + + // 检查是否配置了时间范围 + if (isset($condition['time_range']) && is_array($condition['time_range']) && count($condition['time_range']) === 2) { + $startTime = trim($condition['time_range'][0]); + $endTime = trim($condition['time_range'][1]); + if (!preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $startTime) || + !preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $endTime)) { + return '生日时间范围格式不正确,应为 ["HH:mm", "HH:mm"] 格式'; + } + $result['time_range'] = [$startTime, $endTime]; + } + + return $result; + } + + return '生日触发条件格式不正确,需要提供month和day字段'; + } + + return '生日触发条件格式不正确'; + + case 6: // 自定义 + // 自定义类型,condition可选,如果有则必须是数组格式 + if (!empty($condition)) { + $condition = is_array($condition) ? $condition : json_decode($condition, true); + if (!is_array($condition)) { + return '自定义类型的条件格式不正确,应为数组格式'; + } + return $condition; + } + return []; + + default: + return '无效的触发类型'; + } + } + /** * 添加 * @return \think\response\Json @@ -71,17 +410,18 @@ class AutoGreetingsController extends BaseController return ResponseHelper::error('参数缺失'); } - if (in_array($trigger,[2,3]) && empty($condition)){ - return ResponseHelper::error('具体条件不能为空'); + // 校验trigger类型 + if (!in_array($trigger, [1, 2, 3, 4, 5, 6])) { + return ResponseHelper::error('无效的触发类型'); } - if ($trigger == 2){ - $condition = !empty($condition) ? $condition : []; - } - - if ($trigger == 3){ - $condition = explode(',',$condition); + // 校验并处理condition + $conditionResult = $this->validateTriggerCondition($trigger, $condition); + if (is_string($conditionResult)) { + // 返回的是错误信息 + return ResponseHelper::error($conditionResult); } + $condition = $conditionResult; Db::startTrans(); @@ -97,9 +437,10 @@ class AutoGreetingsController extends BaseController $AutoGreetings->companyId = $companyId; $AutoGreetings->updateTime = time(); $AutoGreetings->createTime = time(); + $AutoGreetings->usageCount = 0; // 初始化使用次数为0 $AutoGreetings->save(); Db::commit(); - return ResponseHelper::success(' ','创建成功'); + return ResponseHelper::success(['id' => $AutoGreetings->id],'创建成功'); } catch (\Exception $e) { Db::rollback(); return ResponseHelper::error('创建失败:'.$e->getMessage()); @@ -128,11 +469,13 @@ class AutoGreetingsController extends BaseController $data['condition'] = json_decode($data['condition'],true); - if ($data['trigger'] == 3){ - $data['condition'] = implode(',',$data['condition']); - } + + // 获取使用次数 + $usageCount = Db::name('kf_auto_greetings_record') + ->where('autoId', $id) + ->count(); + $data['usageCount'] = (int)$usageCount; - unset($data['createTime'],$data['updateTime'],$data['isDel'],$data['delTime']); return ResponseHelper::success($data,'获取成功'); } @@ -175,7 +518,7 @@ class AutoGreetingsController extends BaseController $id = $this->request->param('id', ''); $name = $this->request->param('name', ''); $trigger = $this->request->param('trigger', 0); - $condition = $this->request->param('condition', []); + $condition = $this->request->param('condition', ''); $content = $this->request->param('content', ''); $level = $this->request->param('level', 0); $status = $this->request->param('status', 1); @@ -186,17 +529,18 @@ class AutoGreetingsController extends BaseController return ResponseHelper::error('参数缺失'); } - if (in_array($trigger,[2,3]) && empty($condition)){ - return ResponseHelper::error('具体条件不能为空'); + // 校验trigger类型 + if (!in_array($trigger, [1, 2, 3, 4, 5, 6])) { + return ResponseHelper::error('无效的触发类型'); } - if ($trigger == 2){ - $condition = !empty($condition) ? $condition : []; - } - - if ($trigger == 3){ - $condition = explode(',',$condition); + // 校验并处理condition + $conditionResult = $this->validateTriggerCondition($trigger, $condition); + if (is_string($conditionResult)) { + // 返回的是错误信息 + return ResponseHelper::error($conditionResult); } + $condition = $conditionResult; $query = AutoGreetings::where(['id'=>$id,'isDel' => 0,'userId' => $userId,'companyId' => $companyId])->find(); @@ -244,11 +588,16 @@ class AutoGreetingsController extends BaseController } Db::startTrans(); try { - $query->status = !empty($query['status']) ? 0 : 1; + $status = $this->request->param('status', ''); + if ($status !== '') { + $query->status = (int)$status; + } else { + $query->status = $query->status == 1 ? 0 : 1; + } $query->updateTime = time(); $query->save(); Db::commit(); - return ResponseHelper::success(' ','修改成功'); + return ResponseHelper::success(['status' => $query->status],'修改成功'); } catch (\Exception $e) { Db::rollback(); return ResponseHelper::error('修改失败:'.$e->getMessage()); diff --git a/Server/application/chukebao/controller/MessageController.php b/Server/application/chukebao/controller/MessageController.php index 5593eafb..5a6402c2 100644 --- a/Server/application/chukebao/controller/MessageController.php +++ b/Server/application/chukebao/controller/MessageController.php @@ -2,6 +2,7 @@ namespace app\chukebao\controller; +use app\chukebao\model\FriendSettings; use library\ResponseHelper; use think\Db; @@ -30,7 +31,7 @@ class MessageController extends BaseController // 优化后的查询:使用MySQL兼容的查询方式 $unionQuery = " - (SELECT m.id, m.content, m.wechatFriendId, m.wechatChatroomId, m.createTime, m.wechatTime, 2 as msgType, wc.nickname, wc.chatroomAvatar as avatar, wc.chatroomId + (SELECT m.id, m.content, m.wechatFriendId, m.wechatChatroomId, m.createTime, m.wechatTime,m.wechatAccountId, 2 as msgType, wc.nickname, wc.chatroomAvatar as avatar, wc.chatroomId FROM s2_wechat_chatroom wc INNER JOIN s2_wechat_message m ON wc.id = m.wechatChatroomId AND m.type = 2 INNER JOIN ( @@ -42,7 +43,7 @@ class MessageController extends BaseController WHERE wc.accountId = {$accountId} AND wc.isDeleted = 0 ) UNION ALL - (SELECT m.id, m.content, m.wechatFriendId, m.wechatChatroomId, m.createTime, m.wechatTime, 1 as msgType, 1 as nickname, 1 as avatar, 1 as chatroomId + (SELECT m.id, m.content, m.wechatFriendId, m.wechatChatroomId, m.createTime, m.wechatTime, 1 as msgType, 1 as nickname, 1 as avatar, 1 as chatroomId, 1 as wechatAccountId FROM s2_wechat_message m INNER JOIN ( SELECT wechatFriendId, MAX(wechatTime) as maxTime, MAX(id) as maxId @@ -98,6 +99,10 @@ class MessageController extends BaseController ->column('COUNT(*) AS cnt', 'wechatChatroomId'); } + $aiTypeData = []; + if (!empty($friendIds)) { + $aiTypeData = FriendSettings::where('friendId', 'in', $friendIds)->column('friendId,type'); + } foreach ($list as $k => &$v) { @@ -106,6 +111,7 @@ class MessageController extends BaseController $unreadCount = 0; + $v['aiType'] = 0; if (!empty($v['wechatFriendId'])) { $v['nickname'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['nickname'] : ''; $v['avatar'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['avatar'] : ''; @@ -115,6 +121,7 @@ class MessageController extends BaseController $v['wechatId'] = !empty($friends[$v['wechatFriendId']]) ? $friends[$v['wechatFriendId']]['wechatId'] : ''; $v['labels'] = !empty($friends[$v['wechatFriendId']]) ? json_decode($friends[$v['wechatFriendId']]['labels'], true) : []; $unreadCount = isset($friendUnreadMap[$v['wechatFriendId']]) ? (int)$friendUnreadMap[$v['wechatFriendId']] : 0; + $v['aiType'] = isset($aiTypeData[$v['wechatFriendId']]) ? $aiTypeData[$v['wechatFriendId']] : 0; unset($v['chatroomId']); } diff --git a/Server/application/chukebao/controller/WechatChatroomController.php b/Server/application/chukebao/controller/WechatChatroomController.php index 76edb7e0..bd07bb31 100644 --- a/Server/application/chukebao/controller/WechatChatroomController.php +++ b/Server/application/chukebao/controller/WechatChatroomController.php @@ -83,7 +83,51 @@ class WechatChatroomController extends BaseController return ResponseHelper::success(['list'=>$list,'total'=>$total]); } + public function getDetail(){ + $id = input('id', 0); + + if (!$id) { + return ResponseHelper::error('聊天室ID不能为空'); + } + $accountId = $this->getUserInfo('s2_accountId'); + if (empty($accountId)){ + return ResponseHelper::error('请先登录'); + } + + $detail = Db::table('s2_wechat_chatroom') + ->where(['accountId' => $accountId, 'id' => $id, 'isDeleted' => 0]) + ->find(); + + if (!$detail) { + return ResponseHelper::error('聊天室不存在或无权限访问'); + } + + // 处理时间格式 + $detail['createTime'] = !empty($detail['createTime']) ? date('Y-m-d H:i:s', $detail['createTime']) : ''; + $detail['updateTime'] = !empty($detail['updateTime']) ? date('Y-m-d H:i:s', $detail['updateTime']) : ''; + + // 查询未读消息数量 + $unreadCount = Db::table('s2_wechat_message') + ->where('wechatChatroomId', $id) + ->where('isRead', 0) + ->count(); + + // 查询最新消息 + $latestMessage = Db::table('s2_wechat_message') + ->where('wechatChatroomId', $id) + ->order('id desc') + ->find(); + + $config = [ + 'unreadCount' => $unreadCount, + 'chat' => !empty($latestMessage), + 'msgTime' => isset($latestMessage['wechatTime']) ? $latestMessage['wechatTime'] : 0 + ]; + $detail['config'] = $config; + + return ResponseHelper::success($detail); + } public function aiAnnouncement() { diff --git a/Server/application/chukebao/controller/WechatFriendController.php b/Server/application/chukebao/controller/WechatFriendController.php index d94a0ebb..5851e0f8 100644 --- a/Server/application/chukebao/controller/WechatFriendController.php +++ b/Server/application/chukebao/controller/WechatFriendController.php @@ -24,50 +24,50 @@ class WechatFriendController extends BaseController $list = $query->page($page, $limit)->select(); - /* // 提取所有好友ID + // 提取所有好友ID $friendIds = array_column($list, 'id'); - // 一次性查询所有好友的未读消息数量 - $unreadCounts = []; - if (!empty($friendIds)) { - $unreadResults = Db::table('s2_wechat_message') - ->field('wechatFriendId, COUNT(*) as count') - ->where('wechatFriendId', 'in', $friendIds) - ->where('isRead', 0) - ->group('wechatFriendId') - ->select(); - if (!empty($unreadResults)) { - foreach ($unreadResults as $result) { - $unreadCounts[$result['wechatFriendId']] = $result['count']; - } - } - } + /* // 一次性查询所有好友的未读消息数量 + $unreadCounts = []; + if (!empty($friendIds)) { + $unreadResults = Db::table('s2_wechat_message') + ->field('wechatFriendId, COUNT(*) as count') + ->where('wechatFriendId', 'in', $friendIds) + ->where('isRead', 0) + ->group('wechatFriendId') + ->select(); + if (!empty($unreadResults)) { + foreach ($unreadResults as $result) { + $unreadCounts[$result['wechatFriendId']] = $result['count']; + } + } + } - // 一次性查询所有好友的最新消息 - $latestMessages = []; - if (!empty($friendIds)) { - // 使用子查询获取每个好友的最新消息ID - $subQuery = Db::table('s2_wechat_message') - ->field('MAX(id) as max_id, wechatFriendId') - ->where('wechatFriendId', 'in', $friendIds) - ->group('wechatFriendId') - ->buildSql(); + // 一次性查询所有好友的最新消息 + $latestMessages = []; + if (!empty($friendIds)) { + // 使用子查询获取每个好友的最新消息ID + $subQuery = Db::table('s2_wechat_message') + ->field('MAX(id) as max_id, wechatFriendId') + ->where('wechatFriendId', 'in', $friendIds) + ->group('wechatFriendId') + ->buildSql(); - if (!empty($subQuery)) { - // 查询最新消息的详细信息 - $messageResults = Db::table('s2_wechat_message') - ->alias('m') - ->join([$subQuery => 'sub'], 'm.id = sub.max_id') - ->field('m.*, sub.wechatFriendId') - ->select(); + if (!empty($subQuery)) { + // 查询最新消息的详细信息 + $messageResults = Db::table('s2_wechat_message') + ->alias('m') + ->join([$subQuery => 'sub'], 'm.id = sub.max_id') + ->field('m.*, sub.wechatFriendId') + ->select(); - if (!empty($messageResults)) { - foreach ($messageResults as $message) { - $latestMessages[$message['wechatFriendId']] = $message; - } - } - } - }*/ + if (!empty($messageResults)) { + foreach ($messageResults as $message) { + $latestMessages[$message['wechatFriendId']] = $message; + } + } + } + }*/ $aiTypeData = []; @@ -101,4 +101,44 @@ class WechatFriendController extends BaseController return ResponseHelper::success(['list' => $list, 'total' => $total]); } + + /** + * 获取单个好友详情 + * @return \think\response\Json + */ + public function getDetail() + { + $friendId = $this->request->param('id'); + $accountId = $this->getUserInfo('s2_accountId'); + + if (empty($accountId)) { + return ResponseHelper::error('请先登录'); + } + + if (empty($friendId)) { + return ResponseHelper::error('好友ID不能为空'); + } + + // 查询好友详情 + $friend = Db::table('s2_wechat_friend') + ->where(['id' => $friendId, 'isDeleted' => 0]) + ->find(); + + if (empty($friend)) { + return ResponseHelper::error('好友不存在'); + } + + // 处理好友数据 + $friend['labels'] = json_decode($friend['labels'], true); + $friend['siteLabels'] = json_decode($friend['siteLabels'], true); + $friend['createTime'] = !empty($friend['createTime']) ? date('Y-m-d H:i:s', $friend['createTime']) : ''; + $friend['updateTime'] = !empty($friend['updateTime']) ? date('Y-m-d H:i:s', $friend['updateTime']) : ''; + $friend['passTime'] = !empty($friend['passTime']) ? date('Y-m-d H:i:s', $friend['passTime']) : ''; + + // 获取AI类型设置 + $aiTypeSetting = FriendSettings::where('friendId', $friendId)->find(); + $friend['aiType'] = $aiTypeSetting ? $aiTypeSetting['type'] : 0; + + return ResponseHelper::success(['detail' => $friend]); + } } \ No newline at end of file diff --git a/Server/application/chukebao/model/AiPush.php b/Server/application/chukebao/model/AiPush.php new file mode 100644 index 00000000..567c132f --- /dev/null +++ b/Server/application/chukebao/model/AiPush.php @@ -0,0 +1,17 @@ +handleAutoGreetings(); + }); + } + // 更多其他后台任务 // ...... diff --git a/Server/application/common/config/route.php b/Server/application/common/config/route.php index fc94c79b..54001bde 100644 --- a/Server/application/common/config/route.php +++ b/Server/application/common/config/route.php @@ -30,4 +30,4 @@ Route::group('v1/pay', function () { -Route::get('app/update', 'app\common\controller\PaymentService@createOrder'); \ No newline at end of file +Route::get('v1/app/update', 'app\common\controller\Api@uploadApp'); //检测app是否需要更新 \ No newline at end of file diff --git a/Server/application/common/controller/Api.php b/Server/application/common/controller/Api.php index e95ca699..459e8ee3 100644 --- a/Server/application/common/controller/Api.php +++ b/Server/application/common/controller/Api.php @@ -136,12 +136,12 @@ class Api extends Controller return ResponseHelper::error('参数缺失'); } - if (!in_array($type,['ckb','ai_store'])){ + if (!in_array($type,['ckb','aiStore'])){ return ResponseHelper::error('参数错误'); } $data = Db::name('app_version') - ->field('version,downloadUrl,updateContent') + ->field('version,downloadUrl,updateContent,forceUpdate') ->where(['type'=>$type]) ->order('id DESC') ->find(); diff --git a/Server/application/common/controller/Attachment.php b/Server/application/common/controller/Attachment.php index 68659283..7a2973bf 100644 --- a/Server/application/common/controller/Attachment.php +++ b/Server/application/common/controller/Attachment.php @@ -28,7 +28,7 @@ class Attachment extends Controller $validate = \think\facade\Validate::rule([ 'file' => [ 'fileSize' => 10485760, // 10MB - 'fileExt' => 'jpg,jpeg,png,gif,doc,docx,pdf,zip,rar,mp4,mp3,csv,xlsx,xls,ppt,pptx', + 'fileExt' => 'jpg,jpeg,png,gif,doc,docx,pdf,zip,rar,mp4,mp3,csv,xlsx,xls,ppt,pptx,txt', ] ]); diff --git a/Server/application/common/controller/GetOpenid.php b/Server/application/common/controller/GetOpenid.php new file mode 100644 index 00000000..b33c81de --- /dev/null +++ b/Server/application/common/controller/GetOpenid.php @@ -0,0 +1,52 @@ + Env::get('weChat.appid'), + 'secret' => Env::get('weChat.secret'), + 'response_type' => 'array' + ]; + $this->app = Factory::officialAccount($config); + } + + + + public function index() + { + $app = $this->app; + $oauth = $app->oauth; + + // 未登录 + if (empty($_SESSION['wechat_user'])) { + + $_SESSION['target_url'] = 'user/profile'; + + $redirectUrl = $oauth->redirect(); + + exit_data($redirectUrl); + header("Location: {$redirectUrl}"); + exit; + } + + // 已经登录过 + $user = $_SESSION['wechat_user']; + + exit_data($user); + + return 'Hello, World!'; + } + +} \ No newline at end of file diff --git a/Server/application/common/controller/PasswordLoginController.php b/Server/application/common/controller/PasswordLoginController.php index a3748a06..232e4e68 100644 --- a/Server/application/common/controller/PasswordLoginController.php +++ b/Server/application/common/controller/PasswordLoginController.php @@ -102,15 +102,27 @@ class PasswordLoginController extends BaseController * @param string $account 账号(手机号) * @param string $password 密码(可能是加密后的) * @param string $typeId 登录IP + * @param string $deviceId 本地设备imei * @return array * @throws \Exception */ - protected function doLogin(string $account, string $password, int $typeId): array + protected function doLogin(string $account, string $password, int $typeId, string $deviceId): array { // 获取用户信息 $member = $this->getUser($account, $password, $typeId); $deviceTotal = Db::name('device')->where(['companyId' => $member['companyId'],'deleteTime' => 0])->count(); + //更新设备imei + if ($typeId == 2 && !empty($deviceId)){ + $deviceUser = Db::name('device_user')->where(['companyId' => $member['companyId'],'userId' => $member['id'],'deleteTime' => 0])->find(); + if (!empty($deviceUser)){ + $device = Db::name('device')->where(['companyId' => $member['companyId'],'deleteTime' => 0,'id' => $deviceUser['deviceId']])->find(); + if (!empty($device) && empty($device['deviceImei'])){ + Db::table('s2_device')->where(['id' => $device['id']])->update(['deviceImei' => $deviceId,'updateTime' => time()]); + Db::name('device')->where(['id' => $device['id']])->update(['deviceImei' => $deviceId,'updateTime' => time()]); + } + } + } // 生成JWT令牌 @@ -126,34 +138,17 @@ class PasswordLoginController extends BaseController */ public function index() { - $params = $this->request->only(['account', 'password', 'typeId']); + $params = $this->request->only(['account', 'password', 'typeId','deviceId']); try { + $deviceId = isset($params['deviceId']) ? $params['deviceId'] : ''; $userData = $this->dataValidate($params)->doLogin( $params['account'], $params['password'], - $params['typeId'] + $params['typeId'], + $deviceId ); - //同时登录客服系统 - /* if (!empty($userData['member']['passwordLocal'])){ - $params = [ - 'grant_type' => 'password', - 'username' => $userData['member']['account'], - 'password' => localDecrypt($userData['member']['passwordLocal']) - ]; - // 调用登录接口获取token - $headerData = ['client:kefu-client']; - $header = setHeader($headerData, '', 'plain'); - $result = requestCurl('https://s2.siyuguanli.com:9991/token', $params, 'POST', $header); - $token = handleApiResponse($result); - $userData['kefuData']['token'] = $token; - if (isset($token['access_token']) && !empty($token['access_token'])) { - $headerData = ['client:kefu-client']; - $header = setHeader($headerData, $token['access_token']); - $result = requestCurl( 'https://s2.siyuguanli.com:9991/api/account/self', [], 'GET', $header,'json'); - $self = handleApiResponse($result); - $userData['kefuData']['self'] = $self; - } - }*/ + + return ResponseHelper::success($userData, '登录成功'); } catch (Exception $e) { return ResponseHelper::error($e->getMessage(), $e->getCode()); diff --git a/Server/application/common/controller/PaymentService.php b/Server/application/common/controller/PaymentService.php index 3d4f31eb..9c951c8c 100644 --- a/Server/application/common/controller/PaymentService.php +++ b/Server/application/common/controller/PaymentService.php @@ -16,38 +16,70 @@ use app\common\model\Order; class PaymentService { /** - * 下单 + * 统一支付下单接口 + * 支持扫码付款、微信支付、支付宝支付 * * @param array $order - * - out_trade_no: string 商户订单号(必填) - * - total_fee: int 金额(分,必填) - * - body: string 商品描述(必填) - * - notify_url: string 异步通知地址(可覆盖配置) - * - attach: string 附加数据(可选) - * - time_expire: string 订单失效时间(可选) - * - client_ip: string 终端IP(可选) - * - sign_type: string MD5/RSA_1_256/RSA_1_1(可选,默认MD5) - * - pay_type: string 支付场景,如 JSAPI/APP/H5(可选) - * @return array - * @throws \Exception + * - orderNo: string 商户订单号(必填) + * - money: int 金额(分,必填) + * - goodsName: string 商品描述(必填) + * - service: string 支付服务类型(可选) + * - 'wechat' 或 'pay.weixin.jspay': 微信JSAPI支付 + * - 'alipay' 或 'pay.alipay.jspay': 支付宝JSAPI支付 + * - 不传或空: 默认扫码付款 + * - openid: string 微信用户openid(微信JSAPI支付必填) + * - buyer_id: string 支付宝用户ID(支付宝JSAPI支付可选) + * - notify_url: string 异步通知地址(可选) + * @return string JSON格式响应 */ public function createOrder(array $order) { + // 确定service类型:支持简写形式 wechat/alipay,或完整的 service 值 + $serviceType = $order['service'] ?? ''; + + // 映射简写形式到完整的 service 值 + if ($serviceType === 'wechat' || $serviceType === 'pay.weixin.jspay') { + $service = 'pay.weixin.jspay'; + } elseif ($serviceType === 'alipay' || $serviceType === 'pay.alipay.jspay') { + $service = 'pay.alipay.jspay'; + } elseif ($serviceType === 'qrCode' || $serviceType === 'unified.trade.native') { + $service = 'unified.trade.native'; + } else { + // 默认扫码支付 + $service = 'unified.trade.native'; + } + + // 构建基础参数 $params = [ - 'service' => 'unified.trade.native', + 'service' => $service, 'sign_type' => PaymentUtil::SIGN_TYPE_MD5, 'mch_id' => Env::get('payment.mchId'), 'out_trade_no' => $order['orderNo'], 'body' => $order['goodsName'] ?? '', 'total_fee' => $order['money'] ?? 0, 'mch_create_ip' => Request::ip(), - 'notify_url' => Env::get('payment.notify_url', '127.0.0.1'), + 'notify_url' => $order['notify_url'] ?? Env::get('payment.notify_url', '127.0.0.1'), 'nonce_str' => PaymentUtil::generateNonceStr(), ]; + // 微信JSAPI支付需要openid + if ($service == 'pay.weixin.jspay') { + // $params['sub_openid'] = 'oB44Yw1T6bfVAZwjj729P-6CUSPE'; + $params['is_raw'] = 0; + $params['mch_app_name'] = '存客宝'; + $params['mch_app_id'] = 'https://kr-op.quwanzhi.com'; + } + + // 支付宝JSAPI支付需要buyer_id(可选) + if ($service == 'pay.alipay.jspay') { + $params['is_raw'] = 0; + $params['quit_url'] = $params['notify_url']; + $params['buyer_id'] = ''; + } + Db::startTrans(); try { - // 过滤空值签名 + // 签名 $secret = Env::get('payment.key'); $params['sign_type'] = 'MD5'; $params['sign'] = PaymentUtil::generateSign($params, $secret, 'MD5'); @@ -57,7 +89,7 @@ class PaymentService throw new \Exception('支付网关地址未配置'); } - //创建订单 + // 创建订单 Order::create([ 'mchId' => $params['mch_id'], 'companyId' => isset($order['companyId']) ? $order['companyId'] : 0, @@ -73,17 +105,41 @@ class PaymentService 'nonceStr' => isset($order['nonceStr']) ? $order['nonceStr'] : '', 'createTime' => time(), ]); + // XML POST 请求 $xmlBody = $this->arrayToXml($params); $response = $this->postXml($url, $xmlBody); $parsed = $this->parseXmlOrRaw($response); + if ($parsed['status'] == 0 && $parsed['result_code'] == 0) { Db::commit(); - return json_encode(['code' => 200, 'msg' => '订单创建成功', 'data' => $parsed['code_img_url']]); + + // 根据service类型返回不同的数据格式(仅返回接口文档中的字段) + $responseData = null; + if ($service == 'unified.trade.native') { + // 扫码支付返回二维码URL + $responseData = $parsed['code_img_url'] ?? ''; + } elseif ($service == 'pay.weixin.jspay') { + // 微信JSAPI支付返回支付参数(仅返回接口文档中存在的字段) + $responseData = []; + if (isset($parsed['appid'])) $responseData['appid'] = $parsed['appid']; + if (isset($parsed['time_stamp'])) $responseData['time_stamp'] = $parsed['time_stamp']; + if (isset($parsed['nonce_str'])) $responseData['nonce_str'] = $parsed['nonce_str']; + if (isset($parsed['package'])) $responseData['package'] = $parsed['package']; + if (isset($parsed['sign_type'])) $responseData['sign_type'] = $parsed['sign_type']; + if (isset($parsed['pay_sign'])) $responseData['pay_sign'] = $parsed['pay_sign']; + } elseif ($service == 'pay.alipay.jspay') { + // 支付宝JSAPI支付返回订单信息(仅返回接口文档中存在的字段) + $responseData = []; + if (isset($parsed['order_info'])) $responseData['order_info'] = $parsed['order_info']; + if (isset($parsed['order_string'])) $responseData['order_string'] = $parsed['order_string']; + } + + return json_encode(['code' => 200, 'msg' => '订单创建成功', 'data' => $responseData]); } else { Db::rollback(); - return json_encode(['code' => 500, 'msg' => '订单创建失败:' . $parsed['err_msg']]); + return json_encode(['code' => 500, 'msg' => '订单创建失败:' . ($parsed['err_msg'] ?? '未知错误')]); } } catch (\Exception $e) { @@ -194,54 +250,104 @@ class PaymentService /** * 支付结果异步通知 * - 威富通回调为 XML;需校验签名与业务字段并更新订单 - * - 回应:成功回"success",失败回"fail" - * @return void + * - 支持扫码付款、微信支付、支付宝支付的通知 + * - 回应:成功返回XML格式SUCCESS,失败返回XML格式FAIL + * @return string XML响应 */ public function notify() { $rawBody = file_get_contents('php://input'); $payload = $this->parseXmlOrRaw($rawBody); if (!is_array($payload) || empty($payload)) { - return json_encode(['code' => 500, 'msg' => 'XML解析错误']); + \think\facade\Log::error('支付通知:XML解析错误', ['rawBody' => $rawBody]); + return ''; } + // 验证签名 + $secret = Env::get('payment.key'); + if (!empty($secret) && isset($payload['sign'])) { + $signType = $payload['sign_type'] ?? 'MD5'; + if (!PaymentUtil::verifySign($payload, $secret, $signType)) { + \think\facade\Log::error('支付通知:签名验证失败', ['payload' => $payload]); + return ''; + } + } - if ($payload['status'] != 0 || $payload['result_code'] != 0) { - $errMsg = (isset($payload['err_msg']) ? $payload['err_msg'] : isset($payload['err_msg'])) ? $payload['err_msg'] : '未知错误'; - return json_encode(['code' => 500, 'msg' => $errMsg]); + // 检查通信状态 + if (isset($payload['status']) && $payload['status'] != 0) { + $errMsg = $payload['err_msg'] ?? '通信失败'; + \think\facade\Log::error('支付通知:通信失败', ['payload' => $payload]); + return ''; + } + + // 检查业务结果 + if (isset($payload['result_code']) && $payload['result_code'] != 0) { + $errMsg = $payload['err_msg'] ?? '业务处理失败'; + \think\facade\Log::error('支付通知:业务处理失败', ['payload' => $payload]); + return ''; } // 业务处理:更新订单 Db::startTrans(); try { - $outTradeNo = $payload['out_trade_no']; - $pay_result = $payload['pay_result']; - $time_end = $payload['time_end']; + $outTradeNo = $payload['out_trade_no'] ?? ''; + $pay_result = $payload['pay_result'] ?? 0; + $time_end = $payload['time_end'] ?? ''; + + if (empty($outTradeNo)) { + Db::rollback(); + \think\facade\Log::error('支付通知:订单号为空', ['payload' => $payload]); + return ''; + } + $order = Order::where('orderNo', $outTradeNo)->find(); if (!$order) { Db::rollback(); - return json_encode(['code' => 500, 'msg' => '该订单不存在']); + \think\facade\Log::error('支付通知:订单不存在', ['out_trade_no' => $outTradeNo]); + return ''; + } + + // 如果订单已支付,直接返回成功(防止重复处理) + if ($order->status == 1) { + Db::rollback(); + return ''; } if ($pay_result != 0) { - $order->payInfo = $payload['pay_info']; + $order->payInfo = $payload['pay_info'] ?? '支付失败'; $order->status = 3; $order->save(); Db::commit(); - return json_encode(['code' => 500, 'msg' => $payload['pay_info']]); + \think\facade\Log::error('支付通知:支付失败', ['orderNo' => $outTradeNo, 'pay_info' => $payload['pay_info'] ?? '']); + return ''; } - $order->payType = $payload['trade_type'] == 'pay.wechat.jspay' ? 1 : 2; + + // 根据trade_type判断支付方式 + $tradeType = $payload['trade_type'] ?? ''; + if (strpos($tradeType, 'wechat') !== false || strpos($tradeType, 'weixin') !== false) { + $order->payType = 1; // 微信支付 + } elseif (strpos($tradeType, 'alipay') !== false) { + $order->payType = 2; // 支付宝支付 + } else { + // 默认根据原有逻辑判断 + $order->payType = $tradeType == 'pay.wechat.jspay' ? 1 : 2; + } + $order->status = 1; $order->payTime = $this->parsePayTime($time_end); + $order->transactionId = $payload['transaction_id'] ?? ''; $order->save(); //订单处理 $this->processOrder($order); Db::commit(); - return json_encode(['code' => 200, 'msg' => '付款成功']); + + // 返回成功响应(XML格式) + return ''; } catch (\Exception $e) { Db::rollback(); - return json_encode(['code' => 500, 'msg' => '付款失败' . $e->getMessage()]); + \think\facade\Log::error('支付通知:处理异常', ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]); + return ''; } } @@ -359,12 +465,12 @@ class PaymentService //订单处理 $this->processOrder($order); Db::commit(); - return json_encode(['code' => 200, 'msg' => '支付成功'] ); + return json_encode(['code' => 200, 'msg' => '支付成功']); } catch (\Exception $e) { Db::rollback(); return json_encode(['code' => 500, 'msg' => '付款失败' . $e->getMessage()]); } - }else{ + } else { $order = Order::where('orderNo', $resp['out_trade_no'])->lock(true)->find(); if ($order) { $order->status = 3; @@ -373,8 +479,8 @@ class PaymentService } return json_encode(['code' => 500, 'msg' => '支付失败', 'data' => $resp]); } - - }else{ + + } else { return json_encode(['code' => 500, 'msg' => '通信失败']); } } @@ -413,7 +519,7 @@ class PaymentService $record->form = 5; $record->wechatAccountId = 0; $record->friendIdOrGroupId = 0; - $record->remarks = '购买算力【'.$goodsSpecs['name'].'】'; + $record->remarks = '购买算力【' . $goodsSpecs['name'] . '】'; $record->tokens = $goodsSpecs['tokens']; $record->balanceTokens = $newTokens; $record->createTime = time(); @@ -423,4 +529,5 @@ class PaymentService return true; } + } \ No newline at end of file diff --git a/Server/application/cunkebao/config/route.php b/Server/application/cunkebao/config/route.php index d7f4012e..cf64cef7 100644 --- a/Server/application/cunkebao/config/route.php +++ b/Server/application/cunkebao/config/route.php @@ -154,8 +154,10 @@ Route::group('v1/', function () { // 算力相关 Route::group('tokens', function () { Route::get('list', 'app\cunkebao\controller\TokensController@getList'); - Route::post('pay', 'app\cunkebao\controller\TokensController@pay'); - Route::get('queryOrder', 'app\cunkebao\controller\TokensController@queryOrder'); + Route::post('pay', 'app\cunkebao\controller\TokensController@pay'); // 扫码付款 + Route::get('queryOrder', 'app\cunkebao\controller\TokensController@queryOrder'); // 查询订单(扫码付款) + Route::get('orderList', 'app\cunkebao\controller\TokensController@getOrderList'); // 获取订单列表 + Route::get('statistics', 'app\cunkebao\controller\TokensController@getTokensStatistics'); // 获取算力统计 }); @@ -164,6 +166,7 @@ Route::group('v1/', function () { Route::group('knowledge', function () { Route::get('init', 'app\cunkebao\controller\AiSettingsController@init'); Route::get('release', 'app\cunkebao\controller\AiSettingsController@release'); + Route::post('savePrompt', 'app\cunkebao\controller\AiSettingsController@savePrompt'); // 保存统一提示词 Route::get('typeList', 'app\cunkebao\controller\AiKnowledgeBaseController@typeList'); Route::get('getList', 'app\cunkebao\controller\AiKnowledgeBaseController@getList'); Route::post('add', 'app\cunkebao\controller\AiKnowledgeBaseController@add'); @@ -174,10 +177,20 @@ Route::group('v1/', function () { Route::post('delete', 'app\cunkebao\controller\AiKnowledgeBaseController@delete'); Route::post('addType', 'app\cunkebao\controller\AiKnowledgeBaseController@addType'); Route::post('editType', 'app\cunkebao\controller\AiKnowledgeBaseController@editType'); + Route::put('updateTypeStatus', 'app\cunkebao\controller\AiKnowledgeBaseController@updateTypeStatus'); // 修改类型状态 Route::delete('deleteType', 'app\cunkebao\controller\AiKnowledgeBaseController@deleteType'); Route::get('detailType', 'app\cunkebao\controller\AiKnowledgeBaseController@detailType'); }); + // 门店端账号管理 + Route::group('store-accounts', function () { + Route::get('', 'app\cunkebao\controller\StoreAccountController@index'); // 获取账号列表 + Route::post('', 'app\cunkebao\controller\StoreAccountController@create'); // 创建账号 + Route::put('', 'app\cunkebao\controller\StoreAccountController@update'); // 编辑账号 + Route::delete('', 'app\cunkebao\controller\StoreAccountController@delete'); // 删除账号 + Route::post('disable', 'app\cunkebao\controller\StoreAccountController@disable'); // 禁用/启用账号 + }); + })->middleware(['jwt']); diff --git a/Server/application/cunkebao/controller/AiKnowledgeBaseController.php b/Server/application/cunkebao/controller/AiKnowledgeBaseController.php index 7ab048a9..06a95c88 100644 --- a/Server/application/cunkebao/controller/AiKnowledgeBaseController.php +++ b/Server/application/cunkebao/controller/AiKnowledgeBaseController.php @@ -39,21 +39,42 @@ class AiKnowledgeBaseController extends BaseController if ($includeSystem == 1) { // 包含系统类型和本公司创建的类型 - $where[] = ['type', '=', AiKnowledgeBaseType::TYPE_SYSTEM]; - $where[] = ['companyId|type', 'in', [$companyId, 0]]; + $where[] = ['companyId', 'in', [$companyId, 0]]; } else { // 只显示本公司创建的类型 $where[] = ['companyId', '=', $companyId]; $where[] = ['type', '=', AiKnowledgeBaseType::TYPE_USER]; } - + + // 统计开启的类型总数 + $enabledCountWhere = $where; + $enabledCountWhere[] = ['status', '=', 1]; + $enabledCount = AiKnowledgeBaseType::where($enabledCountWhere)->count(); + // 查询数据 $list = AiKnowledgeBaseType::where($where) ->order('type', 'asc') // 系统类型排在前面 ->order('createTime', 'desc') ->paginate($pageSize, false, ['page' => $page]); - return ResponseHelper::success($list, '获取成功'); + // 为每个类型添加素材数量统计 + $listData = $list->toArray(); + foreach ($listData['data'] as &$item) { + // 统计该类型下的知识库数量(素材数量) + $item['materialCount'] = AiKnowledgeBase::where([ + ['typeId', '=', $item['id']], + ['isDel', '=', 0] + ])->count(); + } + + // 重新构造返回数据 + $result = [ + 'total' => $listData['total'], + 'data' => $listData['data'], + 'enabledCount' => $enabledCount, // 开启的类型总数 + ]; + + return ResponseHelper::success($result, '获取成功'); } catch (\Exception $e) { return ResponseHelper::error('系统异常:' . $e->getMessage()); @@ -80,6 +101,7 @@ class AiKnowledgeBaseController extends BaseController $description = $this->request->param('description', ''); $label = $this->request->param('label', []); $prompt = $this->request->param('prompt', ''); + $status = $this->request->param('status', 1); // 默认启用 // 参数验证 if (empty($name)) { @@ -103,8 +125,9 @@ class AiKnowledgeBaseController extends BaseController 'type' => AiKnowledgeBaseType::TYPE_USER, 'name' => $name, 'description' => $description, - 'label' => json_decode($label,256), + 'label' => json_encode($label,256), 'prompt' => $prompt, + 'status' => $status, 'companyId' => $companyId, 'userId' => $userId, 'createTime' => time(), @@ -142,6 +165,7 @@ class AiKnowledgeBaseController extends BaseController $description = $this->request->param('description', ''); $label = $this->request->param('label', []); $prompt = $this->request->param('prompt', ''); + $status = $this->request->param('status', ''); // 参数验证 if (empty($id)) { @@ -187,8 +211,11 @@ class AiKnowledgeBaseController extends BaseController // 更新数据 $typeModel->name = $name; $typeModel->description = $description; - $typeModel->label = $label; + $typeModel->label = json_encode($label,256); $typeModel->prompt = $prompt; + if ($status !== '') { + $typeModel->status = $status; + } $typeModel->updateTime = time(); if ($typeModel->save()) { @@ -202,6 +229,111 @@ class AiKnowledgeBaseController extends BaseController } } + /** + * 修改知识库类型状态 + * + * @return \think\response\Json + */ + public function updateTypeStatus() + { + try { + $companyId = $this->getUserInfo('companyId'); + if (empty($companyId)) { + return ResponseHelper::error('公司信息获取失败'); + } + + // 获取参数 + $id = $this->request->param('id', 0); + $status = $this->request->param('status', -1); + + // 参数验证 + if (empty($id)) { + return ResponseHelper::error('类型ID不能为空'); + } + + if ($status != 0 && $status != 1) { + return ResponseHelper::error('状态参数错误'); + } + + // 查找类型 + $typeModel = AiKnowledgeBaseType::where([ + ['id', '=', $id], + ['isDel', '=', 0] + ])->find(); + + if (!$typeModel) { + return ResponseHelper::error('类型不存在'); + } + + // 检查是否为系统类型 + if ($typeModel->isSystemType()) { + return ResponseHelper::error('系统类型不允许修改状态'); + } + + // 检查权限(只能修改本公司的类型) + if ($typeModel->companyId != $companyId) { + return ResponseHelper::error('无权限修改该类型'); + } + + // 更新状态 + $typeModel->status = $status; + $typeModel->updateTime = time(); + + if ($typeModel->save()) { + $message = $status == 0 ? '禁用成功' : '启用成功'; + return ResponseHelper::success([], $message); + } else { + return ResponseHelper::error('操作失败'); + } + + } catch (\Exception $e) { + return ResponseHelper::error('系统异常:' . $e->getMessage()); + } + } + + /** + * 获取知识库类型详情 + * + * @return \think\response\Json + */ + public function detailType() + { + try { + $companyId = $this->getUserInfo('companyId'); + if (empty($companyId)) { + return ResponseHelper::error('公司信息获取失败'); + } + + // 获取参数 + $id = $this->request->param('id', 0); + + // 参数验证 + if (empty($id)) { + return ResponseHelper::error('类型ID不能为空'); + } + + // 查找类型 + $typeModel = AiKnowledgeBaseType::where([ + ['id', '=', $id], + ['isDel', '=', 0] + ])->find(); + + if (!$typeModel) { + return ResponseHelper::error('类型不存在'); + } + + // 检查权限(系统类型或本公司的类型都可以查看) + if ($typeModel->companyId != 0 && $typeModel->companyId != $companyId) { + return ResponseHelper::error('无权限查看该类型'); + } + + return ResponseHelper::success($typeModel, '获取成功'); + + } catch (\Exception $e) { + return ResponseHelper::error('系统异常:' . $e->getMessage()); + } + } + /** * 删除知识库类型 * @@ -309,6 +441,11 @@ class AiKnowledgeBaseController extends BaseController ->order('createTime', 'desc') ->paginate($pageSize, false, ['page' => $page]); + foreach ($list as &$v){ + $v['size'] = 0; + } + unset($v); + return ResponseHelper::success($list, '获取成功'); } catch (\Exception $e) { diff --git a/Server/application/cunkebao/controller/AiSettingsController.php b/Server/application/cunkebao/controller/AiSettingsController.php index 8eb41423..429410be 100644 --- a/Server/application/cunkebao/controller/AiSettingsController.php +++ b/Server/application/cunkebao/controller/AiSettingsController.php @@ -40,6 +40,7 @@ class AiSettingsController extends BaseController // 确保智能体已创建 if (empty($settings->botId)) { + $settings->releaseTime = 0; $botCreated = $this->createBot($settings); if (!$botCreated) { return ResponseHelper::error('智能体创建失败'); @@ -48,12 +49,13 @@ class AiSettingsController extends BaseController // 确保知识库已创建 if (empty($settings->datasetId)) { + $settings->releaseTime = 0; $knowledgeCreated = $this->createKnowledge($settings); if (!$knowledgeCreated) { return ResponseHelper::error('知识库创建失败'); } } - if (!empty($settings->botId) && !empty($settings->datasetId)) { + if (!empty($settings->botId) && !empty($settings->datasetId) && $settings->releaseTime <= 0) { $cozeAI = new CozeAI(); $config = json_decode($settings->config,true); $config['bot_id'] = $settings->botId; @@ -133,7 +135,8 @@ class AiSettingsController extends BaseController ## 限制 - 仅依据知识库内容回答问题,对于知识库中没有的信息,如实告知用户无法回答。 - 回答必须严格遵循中国法律法规,不得出现任何违法违规内容。 -- 回答需简洁明了,避免冗长复杂的表述。'; +- 回答需简洁明了,避免冗长复杂的表述(尽量在100字内)。 +- 适当加些表情点缀。'; } /** @@ -341,4 +344,90 @@ class AiSettingsController extends BaseController $settings->save(); return ResponseHelper::success('', '发布成功'); } + + /** + * 保存统一提示词 + * 先更新数据库,再调用CozeAI接口更新智能体 + * + * @return \think\response\Json + */ + public function savePrompt() + { + try { + $companyId = $this->getUserInfo('companyId'); + if (empty($companyId)) { + return ResponseHelper::error('公司信息获取失败'); + } + + // 获取提示词参数 + $promptInfo = $this->request->param('promptInfo', ''); + if (empty($promptInfo)) { + return ResponseHelper::error('提示词内容不能为空'); + } + + // 查找AI设置 + $settings = AiSettingsModel::where(['companyId' => $companyId])->find(); + if (empty($settings)) { + return ResponseHelper::error('AI设置不存在,请先初始化'); + } + + // 检查智能体是否已创建 + if (empty($settings->botId)) { + return ResponseHelper::error('智能体未创建,请先初始化AI设置'); + } + + // 解析现有配置 + $config = json_decode($settings->config, true); + if (!is_array($config)) { + $config = []; + } + + // 更新提示词 + $config['prompt_info'] = $promptInfo; + + // 第一步:更新数据库 + $settings->config = json_encode($config, JSON_UNESCAPED_UNICODE); + $settings->isRelease = 0; // 标记为未发布状态 + $settings->updateTime = time(); + + if (!$settings->save()) { + return ResponseHelper::error('数据库更新失败'); + } + + // 第二步:调用CozeAI接口更新智能体 + try { + $cozeAI = new CozeAI(); + + // 参考 init 方法的参数格式,传递完整的 config + $updateData = $config; + $updateData['bot_id'] = $settings->botId; + + // 如果有知识库,也一并传入 + if (!empty($settings->datasetId)) { + $updateData['dataset_ids'] = [$settings->datasetId]; + } + + $result = $cozeAI->updateBot($updateData); + $result = json_decode($result, true); + + if ($result['code'] != 200) { + \think\facade\Log::error('更新智能体提示词失败:' . json_encode($result)); + return ResponseHelper::error('更新智能体失败:' . ($result['msg'] ?? '未知错误')); + } + + return ResponseHelper::success([ + 'prompt_info' => $promptInfo, + 'isRelease' => 0 + ], '提示词保存成功,请重新发布智能体'); + + } catch (\Exception $e) { + \think\facade\Log::error('调用CozeAI更新接口异常:' . $e->getMessage()); + return ResponseHelper::error('更新智能体接口调用失败:' . $e->getMessage()); + } + + } catch (\Exception $e) { + \think\facade\Log::error('保存提示词异常:' . $e->getMessage()); + return ResponseHelper::error('系统异常:' . $e->getMessage()); + } + } } \ No newline at end of file diff --git a/Server/application/cunkebao/controller/StoreAccountController.php b/Server/application/cunkebao/controller/StoreAccountController.php new file mode 100644 index 00000000..e58d6865 --- /dev/null +++ b/Server/application/cunkebao/controller/StoreAccountController.php @@ -0,0 +1,404 @@ +request->param('account', ''); + $username = $this->request->param('username', ''); + $phone = $this->request->param('phone', ''); + $password = $this->request->param('password', ''); + $deviceId = $this->request->param('deviceId', 0); + + $companyId = $this->getUserInfo('companyId'); + + // 参数验证 + if (empty($account)) { + return ResponseHelper::error('账号不能为空'); + } + if (empty($username)) { + return ResponseHelper::error('昵称不能为空'); + } + if (empty($phone)) { + return ResponseHelper::error('手机号不能为空'); + } + if (!preg_match('/^1[3-9]\d{9}$/', $phone)) { + return ResponseHelper::error('手机号格式不正确'); + } + if (empty($password)) { + return ResponseHelper::error('密码不能为空'); + } + if (strlen($password) < 6 || strlen($password) > 20) { + return ResponseHelper::error('密码长度必须在6-20个字符之间'); + } + if (empty($deviceId)) { + return ResponseHelper::error('请选择设备'); + } + + // 检查账号是否已存在(同一 typeId 和 companyId 下不能重复) + $existUser = Db::name('users')->where(['account' => $account, 'companyId' => $companyId, 'typeId' => 2, 'deleteTime' => 0]) + ->find(); + if ($existUser) { + return ResponseHelper::error('账号已存在'); + } + + // 检查手机号是否已存在(同一 typeId 和 companyId 下不能重复) + $existPhone = Db::name('users')->where(['phone' => $phone, 'companyId' => $companyId, 'typeId' => 2, 'deleteTime' => 0]) + ->find(); + if ($existPhone) { + return ResponseHelper::error('手机号已被使用'); + } + + // 检查设备是否存在且属于当前公司 + $device = Device::where('id', $deviceId) + ->where('companyId', $companyId) + ->find(); + if (!$device) { + return ResponseHelper::error('设备不存在或没有权限'); + } + + // 开始事务 + Db::startTrans(); + try { + // 创建用户 + $userData = [ + 'account' => $account, + 'username' => $username, + 'phone' => $phone, + 'passwordMd5' => md5($password), + 'passwordLocal' => localEncrypt($password), + 'avatar' => 'https://img.icons8.com/color/512/circled-user-male-skin-type-7.png', + 'isAdmin' => 0, + 'companyId' => $companyId, + 'typeId' => 2, // 门店端固定为2 + 'status' => 1, // 默认可用 + 'balance' => 0, + 'tokens' => 0, + 'createTime' => time(), + ]; + + $userId = Db::name('users')->insertGetId($userData); + + // 绑定设备 + Db::name('device_user')->insert([ + 'companyId' => $companyId, + 'userId' => $userId, + 'deviceId' => $deviceId, + 'deleteTime' => 0, + ]); + + // 提交事务 + Db::commit(); + + return ResponseHelper::success('创建账号成功'); + } catch (\Exception $e) { + Db::rollback(); + throw $e; + } + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500); + } + } + + /** + * 编辑账号 + * @return \think\response\Json + */ + public function update() + { + try { + $userId = $this->request->param('userId', 0); + $account = $this->request->param('account', ''); + $username = $this->request->param('username', ''); + $phone = $this->request->param('phone', ''); + $password = $this->request->param('password', ''); + $deviceId = $this->request->param('deviceId', 0); + + $companyId = $this->getUserInfo('companyId'); + + // 参数验证 + if (empty($userId)) { + return ResponseHelper::error('用户ID不能为空'); + } + + // 检查用户是否存在且属于当前公司 + $user = Db::name('users')->where(['id' => $userId, 'companyId' => $companyId, 'typeId' => 2])->find(); + if (!$user) { + return ResponseHelper::error('用户不存在或没有权限'); + } + + $updateData = []; + + // 更新账号 + if (!empty($account)) { + // 检查账号是否已被其他用户使用(同一 typeId 下) + $existUser = Db::name('users')->where(['account' => $account, 'companyId' => $companyId, 'typeId' => 2, 'deleteTime' => 0]) + ->where('id', '<>', $userId) + ->find(); + if ($existUser) { + return ResponseHelper::error('账号已被使用'); + } + $updateData['account'] = $account; + } + + // 更新昵称 + if (!empty($username)) { + $updateData['username'] = $username; + } + + // 更新手机号 + if (!empty($phone)) { + if (!preg_match('/^1[3-9]\d{9}$/', $phone)) { + return ResponseHelper::error('手机号格式不正确'); + } + // 检查手机号是否已被其他用户使用(同一 typeId 下) + $existPhone = Db::name('users')->where(['phone' => $phone, 'companyId' => $companyId, 'typeId' => 2, 'deleteTime' => 0]) + ->where('id', '<>', $userId) + ->find(); + if ($existPhone) { + return ResponseHelper::error('手机号已被使用'); + } + $updateData['phone'] = $phone; + } + + // 更新密码 + if (!empty($password)) { + if (strlen($password) < 6 || strlen($password) > 20) { + return ResponseHelper::error('密码长度必须在6-20个字符之间'); + } + $updateData['passwordMd5'] = md5($password); + $updateData['passwordLocal'] = localEncrypt($password); + } + + // 更新设备绑定 + if (!empty($deviceId)) { + // 检查设备是否存在且属于当前公司 + $device = Device::where('id', $deviceId) + ->where('companyId', $companyId) + ->find(); + if (!$device) { + return ResponseHelper::error('设备不存在或没有权限'); + } + } + + // 开始事务 + Db::startTrans(); + try { + // 更新用户信息 + if (!empty($updateData)) { + $updateData['updateTime'] = time(); + Db::name('users')->where(['id' => $userId])->update($updateData); + } + + // 更新设备绑定 + if (!empty($deviceId)) { + // 删除旧的设备绑定 + Db::name('device_user')->where(['userId' => $userId, 'companyId' => $companyId])->delete(); + + // 添加新的设备绑定 + Db::name('device_user')->insert([ + 'companyId' => $companyId, + 'userId' => $userId, + 'deviceId' => $deviceId, + 'deleteTime' => 0, + ]); + } + + // 提交事务 + Db::commit(); + + return ResponseHelper::success('更新账号成功'); + } catch (\Exception $e) { + Db::rollback(); + throw $e; + } + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500); + } + } + + /** + * 删除账号 + * @return \think\response\Json + */ + public function delete() + { + try { + $userId = $this->request->param('userId', 0); + $companyId = $this->getUserInfo('companyId'); + + if (empty($userId)) { + return ResponseHelper::error('用户ID不能为空'); + } + + // 检查用户是否存在且属于当前公司 + $user = Db::name('users')->where(['id' => $userId, 'companyId' => $companyId, 'typeId' => 2])->find(); + if (!$user) { + return ResponseHelper::error('用户不存在或没有权限'); + } + + // 检查是否是管理账号 + if ($user['isAdmin'] == 1) { + return ResponseHelper::error('管理账号无法删除'); + } + + // 软删除用户 + Db::name('users')->where(['id' => $userId])->update([ + 'deleteTime' => time(), + 'updateTime' => time() + ]); + + // 软删除设备绑定关系 + Db::name('device_user')->where(['userId' => $userId, 'companyId' => $companyId])->update([ + 'deleteTime' => time() + ]); + + return ResponseHelper::success('删除账号成功'); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500); + } + } + + /** + * 禁用/启用账号 + * @return \think\response\Json + */ + public function disable() + { + try { + $userId = $this->request->param('userId', 0); + $status = $this->request->param('status', -1); // 0-禁用 1-启用 + $companyId = $this->getUserInfo('companyId'); + + if (empty($userId)) { + return ResponseHelper::error('用户ID不能为空'); + } + + if ($status != 0 && $status != 1) { + return ResponseHelper::error('状态参数错误'); + } + + // 检查用户是否存在且属于当前公司 + $user = Db::name('users')->where(['id' => $userId, 'companyId' => $companyId, 'typeId' => 2])->find(); + if (!$user) { + return ResponseHelper::error('用户不存在或没有权限'); + } + + // 检查是否是管理账号 + if ($user['isAdmin'] == 1 && $status == 0) { + return ResponseHelper::error('管理账号无法禁用'); + } + + // 更新状态 + Db::name('users')->where(['id' => $userId])->update([ + 'status' => $status, + 'updateTime' => time() + ]); + + $message = $status == 0 ? '禁用账号成功' : '启用账号成功'; + return ResponseHelper::success($message); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500); + } + } + + /** + * 获取账号列表 + * @return \think\response\Json + */ + public function index() + { + try { + $keyword = $this->request->param('keyword', ''); + $status = $this->request->param('status', ''); + $page = $this->request->param('page/d', 1); + $limit = $this->request->param('limit/d', 10); + + $companyId = $this->getUserInfo('companyId'); + + // 构建查询条件 + $where = [ + ['companyId', '=', $companyId], + ['typeId', '=', 2], // 只查询门店端账号 + ['deleteTime', '=', 0] + ]; + + // 关键词搜索(账号、昵称、手机号) + if (!empty($keyword)) { + $where[] = ['account|username|phone', "LIKE", '%'.$keyword.'%']; + } + + // 状态筛选 + if ($status !== '') { + $where[] = ['status', '=', $status]; + } + + // 分页查询 + $query = Db::name('users')->where($where); + $total = $query->count(); + + $list = $query->field('id,account,username,phone,avatar,isAdmin,status,balance,tokens,createTime') + ->order('id desc') + ->page($page, $limit) + ->select(); + + + // 获取每个账号绑定的设备(单个设备) + if (!empty($list)) { + $userIds = array_column($list, 'id'); + $deviceBindings = Db::name('device_user') + ->alias('du') + ->join('device d', 'd.id = du.deviceId', 'left') + ->where([ + ['du.userId', 'in', $userIds], + ['du.companyId', '=', $companyId], + ['du.deleteTime', '=', 0] + ]) + ->field('du.userId,du.deviceId,d.imei,d.memo') + ->order('du.id desc') + ->select(); + + // 组织设备数据(单个设备对象) + $deviceMap = []; + foreach ($deviceBindings as $binding) { + $deviceMap[$binding['userId']] = [ + 'deviceId' => $binding['deviceId'], + 'imei' => $binding['imei'], + 'memo' => $binding['memo'] + ]; + } + + // 将设备信息添加到用户数据中 + foreach ($list as &$item) { + $item['device'] = $deviceMap[$item['id']] ?? null; + } + } + + return ResponseHelper::success([ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ]); + } catch (\Exception $e) { + return ResponseHelper::error($e->getMessage(), $e->getCode() ?: 500); + } + } +} diff --git a/Server/application/cunkebao/controller/TokensController.php b/Server/application/cunkebao/controller/TokensController.php index df70d924..e59fab74 100644 --- a/Server/application/cunkebao/controller/TokensController.php +++ b/Server/application/cunkebao/controller/TokensController.php @@ -5,6 +5,8 @@ namespace app\cunkebao\controller; use app\common\controller\PaymentService; use app\common\model\Order; use app\cunkebao\model\TokensPackage; +use app\chukebao\model\TokensCompany; +use app\chukebao\model\TokensRecord; use library\ResponseHelper; use think\facade\Env; @@ -23,9 +25,9 @@ class TokensController extends BaseController $list = $query->where($where)->page($page, $limit)->order('sort ASC,id desc')->select(); foreach ($list as &$item) { $item['description'] = json_decode($item['description'], true); - $item['discount'] = round(((($item['originalPrice'] - $item['price']) / $item['originalPrice']) * 100),2); - $item['price'] = round( $item['price'], 2); - $item['unitPrice'] = round( $item['price'] / $item['tokens'],6); + $item['discount'] = round(((($item['originalPrice'] - $item['price']) / $item['originalPrice']) * 100), 2); + $item['price'] = round($item['price'], 2); + $item['unitPrice'] = round($item['price'] / $item['tokens'], 6); $item['originalPrice'] = round($item['originalPrice'] / 100, 2); $item['tokens'] = number_format($item['tokens']); } @@ -40,6 +42,12 @@ class TokensController extends BaseController $price = $this->request->param('price', ''); $userId = $this->getUserInfo('id'); $companyId = $this->getUserInfo('companyId'); + $payType = $this->request->param('payType', ''); + + if (!in_array($payType, ['wechat', 'alipay', 'qrCode'])) { + return ResponseHelper::error('付款类型不正确'); + } + if (empty($id) && empty($price)) { return ResponseHelper::error('套餐和自定义购买金额必须选一个'); @@ -73,6 +81,7 @@ class TokensController extends BaseController ]; } + $orderNo = date('YmdHis') . rand(100000, 999999); $order = [ 'companyId' => $companyId, @@ -82,7 +91,8 @@ class TokensController extends BaseController 'goodsName' => $specs['name'], 'goodsSpecs' => $specs, 'orderType' => 1, - 'money' => $specs['price'] + 'money' => $specs['price'], + 'service' => $payType ]; $paymentService = new PaymentService(); $res = $paymentService->createOrder($order); @@ -106,13 +116,200 @@ class TokensController extends BaseController $res = $paymentService->queryOrder($orderNo); $res = json_decode($res, true); if ($res['code'] == 200) { - return ResponseHelper::success('','订单已支付'); + return ResponseHelper::success('', '订单已支付'); } else { $errorMsg = !empty($order['payInfo']) ? $order['payInfo'] : '订单未支付'; return ResponseHelper::error($errorMsg); } } else { - return ResponseHelper::success('','订单已支付'); + return ResponseHelper::success('', '订单已支付'); + } + } + + + + /** + * 获取订单列表 + * @return \think\response\Json + */ + public function getOrderList() + { + try { + $page = $this->request->param('page', 1); + $limit = $this->request->param('limit', 10); + $status = $this->request->param('status', ''); // 订单状态筛选 + $keyword = $this->request->param('keyword', ''); // 关键词搜索(订单号) + $orderType = $this->request->param('orderType', ''); // 订单类型筛选 + $startTime = $this->request->param('startTime', ''); // 开始时间 + $endTime = $this->request->param('endTime', ''); // 结束时间 + + $userId = $this->getUserInfo('id'); + $companyId = $this->getUserInfo('companyId'); + + // 构建查询条件 + $where = [ + ['companyId', '=', $companyId] + ]; + + // 关键词搜索(订单号、商品名称) + if (!empty($keyword)) { + $where[] = ['orderNo|goodsName', 'like', '%' . $keyword . '%']; + } + + // 状态筛选 (0-待支付 1-已付款 2-已退款 3-付款失败) + if ($status !== '') { + $where[] = ['status', '=', $status]; + } + + // 订单类型筛选 + if ($orderType !== '') { + $where[] = ['orderType', '=', $orderType]; + } + + // 时间范围筛选 + if (!empty($startTime)) { + $where[] = ['createTime', '>=', strtotime($startTime)]; + } + if (!empty($endTime)) { + $where[] = ['createTime', '<=', strtotime($endTime . ' 23:59:59')]; + } + + // 分页查询 + $query = Order::where($where) + ->where(function ($query) { + $query->whereNull('deleteTime')->whereOr('deleteTime', 0); + }); + $total = $query->count(); + + $list = $query->field('id,orderNo,goodsId,goodsName,goodsSpecs,orderType,money,status,payType,payTime,createTime') + ->order('id desc') + ->page($page, $limit) + ->select(); + + // 格式化数据 + foreach ($list as &$item) { + // 金额转换(分转元) + $item['money'] = round($item['money'] / 100, 2); + + // 解析商品规格 + if (!empty($item['goodsSpecs'])) { + $specs = is_string($item['goodsSpecs']) ? json_decode($item['goodsSpecs'], true) : $item['goodsSpecs']; + $item['goodsSpecs'] = $specs; + + // 添加算力数量 + if (isset($specs['tokens'])) { + $item['tokens'] = number_format($specs['tokens']); + } + } + + // 状态文本 + $statusText = [ + 0 => '待支付', + 1 => '已付款', + 2 => '已退款', + 3 => '付款失败' + ]; + $item['statusText'] = $statusText[$item['status']] ?? '未知'; + + // 订单类型文本 + $orderTypeText = [ + 1 => '购买算力' + ]; + $item['orderTypeText'] = $orderTypeText[$item['orderType']] ?? '其他'; + + // 支付类型文本 + $payTypeText = [ + 1 => '微信支付', + 2 => '支付宝' + ]; + $item['payTypeText'] = !empty($item['payType']) ? ($payTypeText[$item['payType']] ?? '未知') : ''; + + // 格式化时间 + $item['createTime'] = $item['createTime'] ? date('Y-m-d H:i:s', $item['createTime']) : ''; + $item['payTime'] = $item['payTime'] ? date('Y-m-d H:i:s', $item['payTime']) : ''; + } + unset($item); + + return ResponseHelper::success([ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'limit' => $limit + ]); + + } catch (\Exception $e) { + return ResponseHelper::error('获取订单列表失败:' . $e->getMessage()); + } + } + + /** + * 获取公司算力统计信息 + * 包括:总算力、今日使用、本月使用、剩余算力 + * + * @return \think\response\Json + */ + public function getTokensStatistics() + { + try { + $companyId = $this->getUserInfo('companyId'); + if (empty($companyId)) { + return ResponseHelper::error('公司信息获取失败'); + } + + // 获取公司算力余额 + $tokensCompany = TokensCompany::where('companyId', $companyId)->find(); + $remainingTokens = $tokensCompany ? intval($tokensCompany->tokens) : 0; + + // 获取今日开始和结束时间戳 + $todayStart = strtotime(date('Y-m-d 00:00:00')); + $todayEnd = strtotime(date('Y-m-d 23:59:59')); + + // 获取本月开始和结束时间戳 + $monthStart = strtotime(date('Y-m-01 00:00:00')); + $monthEnd = strtotime(date('Y-m-t 23:59:59')); + + // 统计今日消费(type=0表示消费) + $todayUsed = TokensRecord::where([ + ['companyId', '=', $companyId], + ['type', '=', 0], // 0为减少(消费) + ['createTime', '>=', $todayStart], + ['createTime', '<=', $todayEnd] + ])->sum('tokens'); + $todayUsed = intval($todayUsed); + + // 统计本月消费 + $monthUsed = TokensRecord::where([ + ['companyId', '=', $companyId], + ['type', '=', 0], // 0为减少(消费) + ['createTime', '>=', $monthStart], + ['createTime', '<=', $monthEnd] + ])->sum('tokens'); + $monthUsed = intval($monthUsed); + + // 计算总算力(当前剩余 + 历史总消费) + $totalConsumed = TokensRecord::where([ + ['companyId', '=', $companyId], + ['type', '=', 0] + ])->sum('tokens'); + $totalConsumed = intval($totalConsumed); + + // 总充值算力 + $totalRecharged = TokensRecord::where([ + ['companyId', '=', $companyId], + ['type', '=', 1] // 1为增加(充值) + ])->sum('tokens'); + $totalRecharged = intval($totalRecharged); + + return ResponseHelper::success([ + 'totalTokens' => $totalRecharged, // 总算力(累计充值) + 'todayUsed' => $todayUsed, // 今日使用 + 'monthUsed' => $monthUsed, // 本月使用 + 'remainingTokens' => $remainingTokens, // 剩余算力 + 'totalConsumed' => $totalConsumed, // 累计消费 + ], '获取成功'); + + } catch (\Exception $e) { + return ResponseHelper::error('获取算力统计失败:' . $e->getMessage()); } } } \ No newline at end of file diff --git a/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php b/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php index f51e5d76..aae46d9c 100644 --- a/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php +++ b/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php @@ -12,17 +12,28 @@ use think\Db; class PosterWeChatMiniProgram extends Controller { + + protected $config; + + + public function __construct() + { + parent::__construct(); + + // 从环境变量获取配置 + $this->config = [ + 'app_id' => Env::get('weChat.appidMiniApp','wx789850448e26c91d'), + 'secret' => Env::get('weChat.secretMiniApp','d18f75b3a3623cb40da05648b08365a1'), + 'response_type' => 'array' + ]; + } + + public function index() { return 'Hello, World!'; } - const MINI_PROGRAM_CONFIG = [ - 'app_id' => 'wx789850448e26c91d', - 'secret' => 'd18f75b3a3623cb40da05648b08365a1', - 'response_type' => 'array' - ]; - // 生成小程序码,存客宝-操盘手调用 public function generateMiniProgramCodeWithScene($taskId = '') @@ -34,7 +45,7 @@ class PosterWeChatMiniProgram extends Controller try { - $app = Factory::miniProgram(self::MINI_PROGRAM_CONFIG); + $app = Factory::miniProgram($this->config); // scene参数长度限制为32位 //$scene = 'taskId=' . $taskId; $scene = sprintf("%s", $taskId); @@ -83,7 +94,7 @@ class PosterWeChatMiniProgram extends Controller ]); } - $app = Factory::miniProgram(self::MINI_PROGRAM_CONFIG); + $app = Factory::miniProgram($this->config); $result = $app->phone_number->getUserPhoneNumber($code); diff --git a/Server/application/store/config/route.php b/Server/application/store/config/route.php index dbae6f08..c5fcfe0f 100644 --- a/Server/application/store/config/route.php +++ b/Server/application/store/config/route.php @@ -45,4 +45,6 @@ Route::group('v1/store', function () { Route::get('detail', 'app\store\controller\VendorController@detail'); // 获取供应商详情 Route::post('order', 'app\store\controller\VendorController@createOrder'); // 创建订单 }); -})->middleware(['jwt']); \ No newline at end of file +})->middleware(['jwt']); + +Route::get('v1/store/login', 'app\store\controller\LoginController@index'); \ No newline at end of file diff --git a/Server/application/store/controller/LoginController.php b/Server/application/store/controller/LoginController.php new file mode 100644 index 00000000..8dc571d2 --- /dev/null +++ b/Server/application/store/controller/LoginController.php @@ -0,0 +1,43 @@ +request->param('deviceId', ''); + if (empty($deviceId)) { + return errorJson('缺少必要参数'); + } + + $user = Db::name('users')->alias('u') + ->field('u.*') + ->join('device_user du', 'u.id = du.userId and u.companyId = du.companyId') + ->join('device d', 'du.deviceId = d.id and u.companyId = du.companyId') + ->where(['d.deviceImei' => $deviceId, 'u.deleteTime' => 0, 'du.deleteTime' => 0, 'd.deleteTime' => 0]) + ->find(); + if (empty($user)) { + return errorJson('用户不存在'); + } + $member = array_merge($user, [ + 'lastLoginIp' => $this->request->ip(), + 'lastLoginTime' => time() + ]); + + // 生成JWT令牌 + $token = JwtUtil::createToken($user, 86400 * 30); + $token_expired = time() + 86400 * 30; + + $data = [ + 'member' => $member, + 'token' => $token, + 'token_expired' => $token_expired + ]; + return successJson($data, '登录成功'); + } +} \ No newline at end of file diff --git a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php index a1ef4e3e..adafc321 100644 --- a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php +++ b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php @@ -1537,4 +1537,500 @@ class Adapter implements WeChatServiceInterface } while ($affected > 0); } + /** + * 处理自动问候功能 + * 根据不同的触发类型检查并发送问候消息 + */ + public function handleAutoGreetings() + { + try { + // 获取所有启用的问候规则 + $rules = Db::name('kf_auto_greetings') + ->where(['status' => 1, 'isDel' => 0]) + ->order('level asc, id asc') + ->select(); + + if (empty($rules)) { + return; + } + + foreach ($rules as $rule) { + $trigger = $rule['trigger']; + $condition = json_decode($rule['condition'], true); + + switch ($trigger) { + case 1: // 新好友 + $this->handleNewFriendGreeting($rule); + break; + case 2: // 首次发消息 + $this->handleFirstMessageGreeting($rule); + break; + case 3: // 时间触发 + $this->handleTimeTriggerGreeting($rule, $condition); + break; + case 4: // 关键词触发 + $this->handleKeywordTriggerGreeting($rule, $condition); + break; + case 5: // 生日触发 + $this->handleBirthdayTriggerGreeting($rule, $condition); + break; + case 6: // 自定义 + $this->handleCustomTriggerGreeting($rule, $condition); + break; + } + } + } catch (\Exception $e) { + Log::error('自动问候处理失败:' . $e->getMessage()); + } + } + + /** + * 处理新好友触发 + */ + private function handleNewFriendGreeting($rule) + { + // 获取最近24小时内添加的好友(避免重复处理) + $last24h = time() - 24 * 3600; + + // 查询该用户/公司最近24小时内新添加的好友 + // 通过 s2_wechat_account -> s2_company_account 关联获取 companyId + $friends = Db::table('s2_wechat_friend') + ->alias('wf') + ->join(['s2_wechat_account' => 'wa'], 'wf.wechatAccountId = wa.id') + ->join(['s2_company_account' => 'ca'], 'wa.deviceAccountId = ca.id') + ->where([ + ['wf.isPassed', '=', 1], + ['wf.isDeleted', '=', 0], + ['wf.passTime', '>=', $last24h], + ['ca.departmentId', '=', $rule['companyId']], + ]) + ->field('wf.id, wf.wechatAccountId') + ->select(); + + foreach ($friends as $friend) { + // 检查是否已经发送过问候 + $exists = Db::name('kf_auto_greetings_record') + ->where([ + 'autoId' => $rule['id'], + 'friendIdOrGroupId' => $friend['id'], + 'wechatAccountId' => $friend['wechatAccountId'], + ]) + ->find(); + + if (!$exists) { + $this->sendGreetingMessage($rule, $friend['wechatAccountId'], $friend['id'], 0); + } + } + } + + /** + * 处理首次发消息触发 + */ + private function handleFirstMessageGreeting($rule) + { + // 获取最近1小时内收到的消息 + $last1h = time() - 3600; + + // 查询消息表,找出首次发消息的好友 + // 通过 s2_wechat_account -> s2_company_account 关联获取 companyId + $messages = Db::table('s2_wechat_message') + ->alias('wm') + ->join(['s2_wechat_account' => 'wa'], 'wm.wechatAccountId = wa.id') + ->join(['s2_company_account' => 'ca'], 'wa.deviceAccountId = ca.id') + ->where([ + ['wm.isSend', '=', 0], // 接收的消息 + ['wm.wechatChatroomId', '=', 0], // 个人消息 + ['wm.createTime', '>=', $last1h], + ['ca.departmentId', '=', $rule['companyId']], + ]) + ->group('wm.wechatFriendId, wm.wechatAccountId') + ->field('wm.wechatFriendId, wm.wechatAccountId, MIN(wm.createTime) as firstMsgTime') + ->select(); + + foreach ($messages as $msg) { + // 检查该好友是否之前发送过消息 + $previousMsg = Db::table('s2_wechat_message') + ->where([ + 'wechatFriendId' => $msg['wechatFriendId'], + 'wechatAccountId' => $msg['wechatAccountId'], + 'isSend' => 0, + ]) + ->where('createTime', '<', $msg['firstMsgTime']) + ->find(); + + // 如果是首次发消息,且没有发送过问候 + if (!$previousMsg) { + $exists = Db::name('kf_auto_greetings_record') + ->where([ + 'autoId' => $rule['id'], + 'friendIdOrGroupId' => $msg['wechatFriendId'], + 'wechatAccountId' => $msg['wechatAccountId'], + ]) + ->find(); + + if (!$exists) { + $this->sendGreetingMessage($rule, $msg['wechatAccountId'], $msg['wechatFriendId'], 0); + } + } + } + } + + /** + * 处理时间触发 + */ + private function handleTimeTriggerGreeting($rule, $condition) + { + if (empty($condition) || !isset($condition['type'])) { + return; + } + + $now = time(); + $currentTime = date('H:i', $now); + $currentDate = date('m-d', $now); + $currentDateTime = date('m-d H:i', $now); + $currentWeekday = date('w', $now); // 0=周日, 1=周一, ..., 6=周六 + + $shouldTrigger = false; + + switch ($condition['type']) { + case 'daily_time': // 每天固定时间 + if ($currentTime === $condition['value']) { + $shouldTrigger = true; + } + break; + + case 'yearly_datetime': // 每年固定日期时间 + if ($currentDateTime === $condition['value']) { + $shouldTrigger = true; + } + break; + + case 'fixed_range': // 固定时间段 + if (is_array($condition['value']) && count($condition['value']) === 2) { + $startTime = strtotime('2000-01-01 ' . $condition['value'][0]); + $endTime = strtotime('2000-01-01 ' . $condition['value'][1]); + $currentTimeStamp = strtotime('2000-01-01 ' . $currentTime); + + if ($currentTimeStamp >= $startTime && $currentTimeStamp <= $endTime) { + $shouldTrigger = true; + } + } + break; + + case 'workday': // 工作日 + // 周一到周五(1-5) + if ($currentWeekday >= 1 && $currentWeekday <= 5 && $currentTime === $condition['value']) { + $shouldTrigger = true; + } + break; + } + + if ($shouldTrigger) { + // 获取该用户/公司的所有好友 + // 通过 s2_wechat_account -> s2_company_account 关联获取 companyId + $friends = Db::table('s2_wechat_friend') + ->alias('wf') + ->join(['s2_wechat_account' => 'wa'], 'wf.wechatAccountId = wa.id') + ->join(['s2_company_account' => 'ca'], 'wa.deviceAccountId = ca.id') + ->where([ + ['wf.isPassed', '=', 1], + ['wf.isDeleted', '=', 0], + ['ca.departmentId', '=', $rule['companyId']], + ]) + ->field('wf.id, wf.wechatAccountId') + ->select(); + + foreach ($friends as $friend) { + // 检查今天是否已经发送过 + $todayStart = strtotime(date('Y-m-d 00:00:00')); + $exists = Db::name('kf_auto_greetings_record') + ->where([ + 'autoId' => $rule['id'], + 'friendIdOrGroupId' => $friend['id'], + 'wechatAccountId' => $friend['wechatAccountId'], + ]) + ->where('createTime', '>=', $todayStart) + ->find(); + + if (!$exists) { + $this->sendGreetingMessage($rule, $friend['wechatAccountId'], $friend['id'], 0); + } + } + } + } + + /** + * 处理关键词触发 + */ + private function handleKeywordTriggerGreeting($rule, $condition) + { + if (empty($condition) || empty($condition['keywords'])) { + return; + } + + $keywords = $condition['keywords']; + $matchType = $condition['match_type'] ?? 'fuzzy'; + + // 获取最近1小时内收到的消息 + $last1h = time() - 3600; + + // 通过 s2_wechat_account -> s2_company_account 关联获取 companyId + $messages = Db::table('s2_wechat_message') + ->alias('wm') + ->join(['s2_wechat_account' => 'wa'], 'wm.wechatAccountId = wa.id') + ->join(['s2_company_account' => 'ca'], 'wa.deviceAccountId = ca.id') + ->where([ + ['wm.isSend', '=', 0], // 接收的消息 + ['wm.wechatChatroomId', '=', 0], // 个人消息 + ['wm.msgType', '=', 1], // 文本消息 + ['wm.createTime', '>=', $last1h], + ['ca.departmentId', '=', $rule['companyId']], + ]) + ->field('wm.*') + ->select(); + + foreach ($messages as $msg) { + $content = $msg['content'] ?? ''; + + // 检查关键词匹配 + $matched = false; + foreach ($keywords as $keyword) { + if ($matchType === 'exact') { + // 精准匹配 + if ($content === $keyword) { + $matched = true; + break; + } + } else { + // 模糊匹配 + if (strpos($content, $keyword) !== false) { + $matched = true; + break; + } + } + } + + if ($matched) { + // 检查是否已经发送过问候(同一好友同一规则,1小时内只发送一次) + $last1h = time() - 3600; + $exists = Db::name('kf_auto_greetings_record') + ->where([ + 'autoId' => $rule['id'], + 'friendIdOrGroupId' => $msg['wechatFriendId'], + 'wechatAccountId' => $msg['wechatAccountId'], + ]) + ->where('createTime', '>=', $last1h) + ->find(); + + if (!$exists) { + $this->sendGreetingMessage($rule, $msg['wechatAccountId'], $msg['wechatFriendId'], 0); + } + } + } + } + + /** + * 处理生日触发 + */ + private function handleBirthdayTriggerGreeting($rule, $condition) + { + if (empty($condition)) { + return; + } + + // 解析condition格式 + // 支持格式: + // 1. {'month': 10, 'day': 10} - 当天任何时间都可以触发 + // 2. {'month': 10, 'day': 10, 'time': '09:00'} - 当天指定时间触发 + // 3. {'month': 10, 'day': 10, 'time_range': ['09:00', '10:00']} - 当天时间范围内触发 + // 兼容旧格式:['10-10'] 或 '10-10'(仅支持 MM-DD 格式,不包含年份) + + $birthdayMonth = null; + $birthdayDay = null; + $birthdayTime = null; + $timeRange = null; + + if (is_array($condition)) { + // 新格式:对象格式 {'month': 10, 'day': 10} + if (isset($condition['month']) && isset($condition['day'])) { + $birthdayMonth = (int)$condition['month']; + $birthdayDay = (int)$condition['day']; + $birthdayTime = $condition['time'] ?? null; + $timeRange = $condition['time_range'] ?? null; + } + // 兼容旧格式:['10-10'] 或 ['10-10 09:00'](仅支持 MM-DD 格式) + elseif (isset($condition[0])) { + $dateStr = $condition[0]; + // 只接受月日格式:'10-10' 或 '10-10 09:00' + if (preg_match('/^(\d{1,2})-(\d{1,2})(?:\s+(\d{2}:\d{2}))?$/', $dateStr, $matches)) { + $birthdayMonth = (int)$matches[1]; + $birthdayDay = (int)$matches[2]; + if (isset($matches[3])) { + $birthdayTime = $matches[3]; + } + } + } + } elseif (is_string($condition)) { + // 字符串格式:只接受 '10-10' 或 '10-10 09:00'(MM-DD 格式,不包含年份) + if (preg_match('/^(\d{1,2})-(\d{1,2})(?:\s+(\d{2}:\d{2}))?$/', $condition, $matches)) { + $birthdayMonth = (int)$matches[1]; + $birthdayDay = (int)$matches[2]; + if (isset($matches[3])) { + $birthdayTime = $matches[3]; + } + } + } + + if ($birthdayMonth === null || $birthdayDay === null || $birthdayMonth < 1 || $birthdayMonth > 12 || $birthdayDay < 1 || $birthdayDay > 31) { + return; + } + + $todayMonth = (int)date('m'); + $todayDay = (int)date('d'); + + // 检查今天是否是生日(只匹配月日,不匹配年份) + if ($todayMonth !== $birthdayMonth || $todayDay !== $birthdayDay) { + return; + } + + // 如果配置了时间,检查当前时间是否匹配 + $now = time(); + $currentTime = date('H:i', $now); + + if ($birthdayTime !== null) { + // 指定了具体时间,检查是否在指定时间(允许1分钟误差,避免定时任务执行时间不精确) + $birthdayTimestamp = strtotime('2000-01-01 ' . $birthdayTime); + $currentTimestamp = strtotime('2000-01-01 ' . $currentTime); + $diff = abs($currentTimestamp - $birthdayTimestamp); + + // 如果时间差超过2分钟,不触发(允许1分钟误差) + if ($diff > 120) { + return; + } + } elseif ($timeRange !== null && is_array($timeRange) && count($timeRange) === 2) { + // 指定了时间范围,检查当前时间是否在范围内 + $startTime = strtotime('2000-01-01 ' . $timeRange[0]); + $endTime = strtotime('2000-01-01 ' . $timeRange[1]); + $currentTimestamp = strtotime('2000-01-01 ' . $currentTime); + + if ($currentTimestamp < $startTime || $currentTimestamp > $endTime) { + return; + } + } + // 如果没有配置时间或时间范围,则当天任何时间都可以触发 + + // 获取该用户/公司的所有好友 + // 通过 s2_wechat_account -> s2_company_account 关联获取 companyId + $friends = Db::table('s2_wechat_friend') + ->alias('wf') + ->join(['s2_wechat_account' => 'wa'], 'wf.wechatAccountId = wa.id') + ->join(['s2_company_account' => 'ca'], 'wa.deviceAccountId = ca.id') + ->where([ + ['wf.isPassed', '=', 1], + ['wf.isDeleted', '=', 0], + ['ca.departmentId', '=', $rule['companyId']], + ]) + ->field('wf.id, wf.wechatAccountId') + ->select(); + + foreach ($friends as $friend) { + // 检查今天是否已经发送过 + $todayStart = strtotime(date('Y-m-d 00:00:00')); + $exists = Db::name('kf_auto_greetings_record') + ->where([ + 'autoId' => $rule['id'], + 'friendIdOrGroupId' => $friend['id'], + 'wechatAccountId' => $friend['wechatAccountId'], + ]) + ->where('createTime', '>=', $todayStart) + ->find(); + + if (!$exists) { + $this->sendGreetingMessage($rule, $friend['wechatAccountId'], $friend['id'], 0); + } + } + } + + /** + * 处理自定义触发 + */ + private function handleCustomTriggerGreeting($rule, $condition) + { + // 自定义类型需要根据具体业务需求实现 + // 这里提供一个基础框架,可根据实际需求扩展 + // 暂时不实现,留待后续扩展 + } + + /** + * 发送问候消息 + * @param array $rule 问候规则 + * @param int $wechatAccountId 微信账号ID + * @param int $friendId 好友ID + * @param int $groupId 群ID(0表示个人消息) + */ + private function sendGreetingMessage($rule, $wechatAccountId, $friendId, $groupId = 0) + { + try { + $content = $rule['content']; + + // 创建记录 + $recordId = Db::name('kf_auto_greetings_record')->insertGetId([ + 'autoId' => $rule['id'], + 'userId' => $rule['userId'], + 'companyId' => $rule['companyId'], + 'wechatAccountId' => $wechatAccountId, + 'friendIdOrGroupId' => $friendId, + 'isSend' => 0, + 'sendTime' => 0, + 'receiveTime' => 0, + 'createTime' => time(), + ]); + + // 发送消息(文本消息) + $username = Env::get('api.username', ''); + $password = Env::get('api.password', ''); + $toAccountId = ''; + if (!empty($username) || !empty($password)) { + $toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId'); + } + + $wsController = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]); + + $sendTime = time(); + $result = $wsController->sendPersonal([ + 'wechatFriendId' => $friendId, + 'wechatAccountId' => $wechatAccountId, + 'msgType' => 1, // 文本消息 + 'content' => $content, + ]); + + $isSend = 0; + $receiveTime = 0; + + // 解析返回结果 + $resultData = json_decode($result, true); + if (!empty($resultData) && $resultData['code'] == 200) { + $isSend = 1; + $receiveTime = time(); // 简化处理,实际应该从返回结果中获取 + } + + // 更新记录 + Db::name('kf_auto_greetings_record') + ->where('id', $recordId) + ->update([ + 'isSend' => $isSend, + 'sendTime' => $sendTime, + 'receiveTime' => $receiveTime, + ]); + + // 更新规则使用次数 + Db::name('kf_auto_greetings') + ->where('id', $rule['id']) + ->setInc('usageCount'); + + } catch (\Exception $e) { + Log::error('发送问候消息失败:' . $e->getMessage() . ',规则ID:' . $rule['id']); + } + } + } diff --git a/Store_vue/App.vue b/Store_vue/App.vue index b97e7bc2..0a0f5448 100644 --- a/Store_vue/App.vue +++ b/Store_vue/App.vue @@ -1,14 +1,37 @@ + + + diff --git a/Store_vue/components/UpdateModal.vue b/Store_vue/components/UpdateModal.vue new file mode 100644 index 00000000..8de78f38 --- /dev/null +++ b/Store_vue/components/UpdateModal.vue @@ -0,0 +1,131 @@ + + + + + + diff --git a/Store_vue/manifest.json b/Store_vue/manifest.json index 4c22f42d..7beae997 100644 --- a/Store_vue/manifest.json +++ b/Store_vue/manifest.json @@ -2,8 +2,8 @@ "name" : "AI数智员工", "appid" : "__UNI__9421F6C", "description" : "", - "versionName" : "1.0.1", - "versionCode" : "100", + "versionName" : "1.1.0", + "versionCode" : 100, "transformPx" : false, /* 5+App特有相关 */ "app-plus" : { @@ -107,5 +107,17 @@ }, "vueVersion" : "2", "locale" : "zh-Hans", - "fallbackLocale" : "zh-Hans" + "fallbackLocale" : "zh-Hans", + /* H5特有相关 */ + "h5" : { + "router" : { + "mode" : "hash", + "base" : "./" + }, + "title" : "AI数智员工", + "devServer" : { + "port" : 8080, + "disableHostCheck" : true + } + } } diff --git a/Store_vue/pages.json b/Store_vue/pages.json index 427192f0..6a6c25ed 100644 --- a/Store_vue/pages.json +++ b/Store_vue/pages.json @@ -4,15 +4,15 @@ }, "pages": [ { - "path": "pages/chat/index", + "path": "pages/login/index", "style": { - "navigationBarTitleText": "AI数智员工", "navigationStyle": "custom" } }, { - "path": "pages/login/index", + "path": "pages/chat/index", "style": { + "navigationBarTitleText": "AI数智员工", "navigationStyle": "custom" } } diff --git a/Store_vue/pages/login/index.vue b/Store_vue/pages/login/index.vue index 87bcaec9..9b8a7da5 100644 --- a/Store_vue/pages/login/index.vue +++ b/Store_vue/pages/login/index.vue @@ -1,163 +1,214 @@