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 @@
+
+
+
+
+ 发现新版本 {{ version }}
+
+
+
+ {{ updateContent }}
+
+
+
+
+ 稍后再说
+ 立即更新
+
+
+
+
+
+
+
+
+
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 @@
-
-