Merge branch 'develop' into yongpxu-dev

This commit is contained in:
超级老白兔
2025-11-04 19:34:12 +08:00
40 changed files with 4590 additions and 625 deletions

View File

@@ -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');
});

View File

@@ -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' => []]);
}
}

View File

@@ -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' => '提示词缺失']);
}

View File

@@ -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 = [

View File

@@ -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');

View File

@@ -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()
{

View File

@@ -0,0 +1,505 @@
<?php
namespace app\chukebao\controller;
use app\chukebao\model\AiPush;
use app\chukebao\model\AiPushRecord;
use app\chukebao\model\AutoGreetings;
use library\ResponseHelper;
use think\Db;
class AiPushController extends BaseController
{
/**
* 获取推送列表
* @return \think\response\Json
* @throws \Exception
*/
public function getList()
{
$page = $this->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) {
// 静默失败,不影响主流程
}
}
}

View File

@@ -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());

View File

@@ -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']);
}

View File

@@ -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()
{

View File

@@ -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]);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace app\chukebao\model;
use think\Model;
class AiPush extends Model
{
protected $pk = 'id';
protected $name = 'kf_ai_push';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'createTime';
protected $updateTime = 'updateTime';
}

View File

@@ -0,0 +1,16 @@
<?php
namespace app\chukebao\model;
use think\Model;
class AiPushRecord extends Model
{
protected $pk = 'id';
protected $name = 'kf_ai_push_record';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'createTime';
}

View File

@@ -78,6 +78,14 @@ class TaskServer extends Server
});
}
// 在一个进程里处理自动问候任务
if ($current_worker_id == 1) {
// 每60秒检查一次自动问候规则
Timer::add(60, function () use ($adapter) {
$adapter->handleAutoGreetings();
});
}
// 更多其他后台任务
// ......

View File

@@ -30,4 +30,4 @@ Route::group('v1/pay', function () {
Route::get('app/update', 'app\common\controller\PaymentService@createOrder');
Route::get('v1/app/update', 'app\common\controller\Api@uploadApp'); //检测app是否需要更新

View File

@@ -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();

View File

@@ -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',
]
]);

View File

@@ -0,0 +1,52 @@
<?php
namespace app\common\controller;
use EasyWeChat\Factory;
use think\Controller;
use think\facade\Env;
class GetOpenid extends Controller
{
protected $app;
public function __construct()
{
parent::__construct();
// 从环境变量获取配置
$config = [
'app_id' => 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!';
}
}

View File

@@ -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());

View File

@@ -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 '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[XML解析错误]]></return_msg></xml>';
}
// 验证签名
$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 '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名验证失败]]></return_msg></xml>';
}
}
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 '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[' . $errMsg . ']]></return_msg></xml>';
}
// 检查业务结果
if (isset($payload['result_code']) && $payload['result_code'] != 0) {
$errMsg = $payload['err_msg'] ?? '业务处理失败';
\think\facade\Log::error('支付通知:业务处理失败', ['payload' => $payload]);
return '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[' . $errMsg . ']]></return_msg></xml>';
}
// 业务处理:更新订单
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 '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[订单号为空]]></return_msg></xml>';
}
$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 '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[订单不存在]]></return_msg></xml>';
}
// 如果订单已支付,直接返回成功(防止重复处理)
if ($order->status == 1) {
Db::rollback();
return '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';
}
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 '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[' . ($payload['pay_info'] ?? '支付失败') . ']]></return_msg></xml>';
}
$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 '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>';
} catch (\Exception $e) {
Db::rollback();
return json_encode(['code' => 500, 'msg' => '付款失败' . $e->getMessage()]);
\think\facade\Log::error('支付通知:处理异常', ['error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
return '<?xml version="1.0" encoding="UTF-8"?><xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[处理异常]]></return_msg></xml>';
}
}
@@ -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;
}
}

View File

@@ -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']);

View File

@@ -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) {

View File

@@ -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());
}
}
}

View File

@@ -0,0 +1,404 @@
<?php
namespace app\cunkebao\controller;
use app\common\model\Device;
use app\common\model\DeviceUser;
use app\common\model\User;
use library\ResponseHelper;
use think\Db;
/**
* 门店端账号管理控制器
*/
class StoreAccountController extends BaseController
{
/**
* 创建账号
* @return \think\response\Json
*/
public function create()
{
try {
// 获取参数
$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($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);
}
}
}

View File

@@ -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());
}
}
}

View File

@@ -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);

View File

@@ -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']);
})->middleware(['jwt']);
Route::get('v1/store/login', 'app\store\controller\LoginController@index');

View File

@@ -0,0 +1,43 @@
<?php
namespace app\store\controller;
use app\common\util\JwtUtil;
use think\Db;
use think\Controller;
class LoginController extends Controller
{
public function index()
{
$deviceId = $this->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, '登录成功');
}
}

View File

@@ -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 群ID0表示个人消息
*/
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']);
}
}
}

View File

@@ -1,14 +1,37 @@
<script>
import { hasValidToken, redirectToLogin } from './api/utils/auth';
import { appApi } from './api/modules/app';
import UpdateModal from './components/UpdateModal.vue';
export default {
components: {
UpdateModal
},
data() {
return {
// #ifdef APP-PLUS
// 更新弹窗相关数据仅APP端
showUpdateModal: false,
updateInfo: {
version: '',
updateContent: '',
downloadUrl: '',
forceUpdate: false
}
// #endif
};
},
onLaunch: function() {
console.log('App Launch');
// 检测APP更新
this.checkAppUpdate();
// 全局检查token
this.checkToken();
},
onShow: function() {
console.log('App Show');
// 每次显示时检测APP更新
this.checkAppUpdate();
// 应用恢复时再次检查token
this.checkToken();
},
@@ -21,12 +44,69 @@
// 获取当前页面
const pages = getCurrentPages();
const currentPage = pages.length ? pages[pages.length - 1] : null;
const currentRoute = currentPage ? currentPage.route : '';
// 如果token无效且不在登录页面则跳转到登录页面
if (!hasValidToken() && currentPage && currentPage.route !== 'pages/login/index') {
if (!hasValidToken() && currentRoute && currentRoute !== 'pages/login/index') {
console.log('Token无效从', currentRoute, '重定向到登录页面');
redirectToLogin();
}
},
// 检测APP更新
async checkAppUpdate() {
// #ifdef APP-PLUS
try {
console.log('开始检测APP更新...');
const res = await appApi.checkUpdate();
console.log('更新检测结果:', res);
if (res.code === 200 && res.data) {
const data = res.data;
// 清理 downloadUrl 中的换行符
const downloadUrl = data.downloadUrl ? data.downloadUrl.trim() : '';
// 设置更新信息
this.updateInfo = {
version: data.version || '',
updateContent: data.updateContent || '本次更新包含性能优化和问题修复',
downloadUrl: downloadUrl,
forceUpdate: data.forceUpdate || false
};
// 显示更新弹窗
this.showUpdateModal = true;
}
} catch (error) {
console.error('检测更新失败:', error);
// 更新检测失败不影响应用正常使用,只记录日志
}
// #endif
},
// 处理更新确认
handleUpdateConfirm(downloadUrl) {
// #ifdef APP-PLUS
if (downloadUrl) {
plus.runtime.openURL(downloadUrl);
}
// #endif
this.showUpdateModal = false;
},
// 处理更新取消
handleUpdateCancel() {
if (this.updateInfo.forceUpdate) {
// 强制更新时,取消则退出应用
// #ifdef APP-PLUS
plus.runtime.quit();
// #endif
} else {
// 关闭弹窗
this.showUpdateModal = false;
}
}
}
}
@@ -59,6 +139,21 @@
</script>
<template>
<!-- #ifdef APP-PLUS -->
<!-- 更新弹窗仅APP端 -->
<UpdateModal
:show="showUpdateModal"
:version="updateInfo.version"
:updateContent="updateInfo.updateContent"
:downloadUrl="updateInfo.downloadUrl"
:forceUpdate="updateInfo.forceUpdate"
@confirm="handleUpdateConfirm"
@cancel="handleUpdateCancel"
/>
<!-- #endif -->
</template>
<style lang="scss">
/*每个页面公共css */
@import 'uview-ui/index.scss';

View File

@@ -1,8 +1,14 @@
// API配置文件
// 基础配置
export const BASE_URL = 'http://yishi.com'
//export const BASE_URL = 'https://ckbapi.quwanzhi.com'
//export const BASE_URL = 'http://yishi.com'
export const BASE_URL = 'https://ckbapi.quwanzhi.com'
// APP配置
export const APP_CONFIG = {
version: '1.1.0', // 当前APP版本号与manifest.json保持一致
type: 'aiStore' // APP类型标识
}
// 获取请求头
const getHeaders = (options = {}) => {

View File

@@ -0,0 +1,19 @@
import { request, APP_CONFIG } from '../config'
// APP相关API
export const appApi = {
// 检测APP更新
// @param {string} version - APP版本号可选默认使用配置中的版本号
// @returns {Promise} 返回更新信息
checkUpdate: (version) => {
return request({
url: '/v1/app/update',
method: 'GET',
data: {
type: APP_CONFIG.type, // 固定为 aiStore
version: version || APP_CONFIG.version // 使用传入的版本号或配置中的默认版本号
}
})
}
}

View File

@@ -3,14 +3,30 @@ import { request, requestWithRetry } from '../config'
// 认证相关API
export const authApi = {
// 用户登录
login: (account, password) => {
// @param {string} account - 账号
// @param {string} password - 密码
// @param {string} deviceId - 设备ID仅APP端传递H5端为空字符串
login: (account, password, deviceId) => {
return request({
url: '/v1/auth/login',
method: 'POST',
data: {
account: account,
password: password,
typeId: 2 // 固定为2
typeId: 2, // 固定为2
deviceId: deviceId || '' // 设备ID仅APP端有值
}
})
},
// 免密登录
// @param {string} deviceId - 设备ID
noPasswordLogin: (deviceId) => {
return request({
url: '/v1/store/login',
method: 'GET',
data: {
deviceId: deviceId || ''
}
})
}

View File

@@ -116,6 +116,21 @@
</view>
</view>
</view>
<!-- #ifdef APP-PLUS -->
<view class="module-item" @tap="handleCheckUpdate">
<view class="module-left">
<view class="module-icon green">
<text class="iconfont icon-shezhi" style="color: #33cc99; font-size: 24px;"></text>
</view>
<view class="module-info">
<text class="module-name">检查更新</text>
<text class="module-desc" v-if="!checkingUpdate && !hasNewVersion">当前版本 {{ currentVersion }}</text>
<text class="module-desc" v-if="checkingUpdate" style="color: #33cc99;">检查中...</text>
<text class="module-desc" v-if="!checkingUpdate && hasNewVersion" style="color: #ff6699;">发现新版本 {{ latestVersion }}</text>
</view>
</view>
</view>
<!-- #endif -->
<view class="module-item" @tap="showSettings" v-if='hide'>
<view class="module-left">
<view class="module-icon gray">
@@ -158,6 +173,16 @@
@close="closeLoginPage"
@login-success="handleLoginSuccess"
></login-register>
<!-- 更新弹窗 -->
<update-dialog
:show="showUpdateDialog"
:version="updateInfo.version"
:updateContent="updateInfo.updateContent"
:downloadUrl="updateInfo.downloadUrl"
:forceUpdate="updateInfo.forceUpdate"
@close="closeUpdateDialog"
></update-dialog>
</view>
</template>
@@ -168,8 +193,10 @@
import LoginRegister from './LoginRegister.vue';
import DataStatistics from './DataStatistics.vue';
import CustomerManagement from './CustomerManagement.vue';
import UpdateDialog from './UpdateDialog.vue';
import { hasValidToken, clearToken, redirectToLogin } from '../api/utils/auth';
import { request } from '../api/config';
import { request, APP_CONFIG } from '../api/config';
import { appApi } from '../api/modules/app';
export default {
name: "SideMenu",
@@ -179,7 +206,8 @@
SystemSettings,
LoginRegister,
DataStatistics,
CustomerManagement
CustomerManagement,
UpdateDialog
},
props: {
show: {
@@ -204,7 +232,20 @@
showCustomerManagementPage: false,
showLoginPageFlag: false,
isLoggedIn: false, // 用户登录状态
userInfo: null // 用户信息
userInfo: null, // 用户信息
// 版本信息
currentVersion: APP_CONFIG.version, // 当前版本
latestVersion: '', // 最新版本
hasNewVersion: false, // 是否有新版本
checkingUpdate: false, // 是否正在检查更新
// 更新弹窗
showUpdateDialog: false,
updateInfo: {
version: '',
updateContent: '',
downloadUrl: '',
forceUpdate: false
}
}
},
watch: {
@@ -454,6 +495,83 @@
showSettings() {
// 显示系统设置页面
this.showSystemSettingsPage = true;
},
// 版本号比较函数
// 返回值: 1表示v1>v2, -1表示v1<v2, 0表示相等
compareVersion(v1, v2) {
const arr1 = v1.split('.').map(Number);
const arr2 = v2.split('.').map(Number);
const maxLen = Math.max(arr1.length, arr2.length);
for (let i = 0; i < maxLen; i++) {
const num1 = arr1[i] || 0;
const num2 = arr2[i] || 0;
if (num1 > num2) return 1;
if (num1 < num2) return -1;
}
return 0;
},
// 检查更新
async handleCheckUpdate() {
// #ifdef APP-PLUS
if (this.checkingUpdate) {
return; // 正在检查中,避免重复请求
}
this.checkingUpdate = true;
try {
console.log('开始检查更新,当前版本:', this.currentVersion);
const res = await appApi.checkUpdate();
console.log('更新检测结果:', res);
if (res.code === 200 && res.data) {
const data = res.data;
this.latestVersion = data.version || '';
// 比较版本号
const compareResult = this.compareVersion(this.latestVersion, this.currentVersion);
if (compareResult > 0) {
// 线上版本大于本地版本
this.hasNewVersion = true;
// 设置更新信息并显示自定义弹窗
this.updateInfo = {
version: data.version || '',
updateContent: data.updateContent || '',
downloadUrl: data.downloadUrl ? data.downloadUrl.trim() : '',
forceUpdate: data.forceUpdate || false
};
this.showUpdateDialog = true;
} else {
// 已是最新版本
this.hasNewVersion = false;
uni.showToast({
title: '已是最新版本',
icon: 'success'
});
}
}
} catch (error) {
console.error('检查更新失败:', error);
uni.showToast({
title: '检查更新失败',
icon: 'none'
});
} finally {
this.checkingUpdate = false;
}
// #endif
},
// 关闭更新弹窗
closeUpdateDialog() {
this.showUpdateDialog = false;
}
}
}

View File

@@ -0,0 +1,556 @@
<template>
<view class="update-dialog-mask" v-if="show" @tap.stop="handleMaskClick">
<view class="update-dialog-container" @tap.stop>
<!-- 火箭图标 -->
<view class="rocket-container">
<view class="rocket-wrapper">
<!-- 火箭 SVG 图片 -->
<image class="rocket-svg" :src="rocketBase64" mode="aspectFit"></image>
<!-- 火焰效果 -->
<view class="flame-container">
<view class="flame flame-1"></view>
<view class="flame flame-2"></view>
<view class="flame flame-3"></view>
</view>
<!-- 星星装饰 -->
<view class="star star-1"></view>
<view class="star star-2"></view>
<view class="star star-3"></view>
<view class="star star-4"></view>
</view>
</view>
<!-- 内容区域 -->
<view class="dialog-content">
<!-- 强制更新提示 -->
<view class="force-notice" v-if="forceUpdate">
<text class="force-notice-icon"></text>
<text class="force-notice-text">本次为重要更新需要立即升级</text>
</view>
<!-- 更新内容 -->
<view class="update-content">
<text class="update-item" v-for="(item, index) in updateList" :key="index">{{ index + 1 }}.{{ item }}</text>
</view>
<!-- 下载进度 -->
<view class="progress-container" v-if="downloading">
<view class="progress-bar">
<view class="progress-fill" :style="{width: downloadProgress + '%'}"></view>
</view>
<text class="progress-text">{{ downloadProgress }}%</text>
</view>
<!-- 按钮 -->
<view class="button-container" v-if="!downloading">
<button class="update-button" @tap="handleUpdate">即刻升级</button>
</view>
<!-- 下载中按钮 -->
<view class="button-container" v-else>
<button class="update-button downloading">下载中...</button>
</view>
</view>
<!-- 关闭按钮 -->
<view class="close-button" @tap="handleClose" v-if="!forceUpdate && !downloading">
<text class="close-icon">×</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'UpdateDialog',
props: {
show: {
type: Boolean,
default: false
},
version: {
type: String,
default: ''
},
updateContent: {
type: String,
default: ''
},
downloadUrl: {
type: String,
default: ''
},
forceUpdate: {
type: Boolean,
default: false
}
},
data() {
return {
downloading: false,
downloadProgress: 0,
downloadTask: null,
// 火箭 SVG 的 base64 图片
rocketBase64: 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzYxODA0ODYyMzIwIiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjIwNDAiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMzk1LjI2NCAzMDQuMTI4Yy03MC42NTYgOTIuMTYtMTQ1LjQwOCAxOTQuNTYtMTg5LjQ0IDMwNC4xMjgtNS4xMiAxMi4yODggNi4xNDQgMjMuNTUyIDE4LjQzMiAyMC40OEwzNTguNCA1OTAuODQ4TTYyOC43MzYgMzA0LjEyOGM3MC42NTYgOTIuMTYgMTQ1LjQwOCAxOTQuNTYgMTg5LjQ0IDMwNC4xMjggNS4xMiAxMi4yODgtNi4xNDQgMjMuNTUyLTE4LjQzMiAyMC40OEw2NjUuNiA1OTAuODQ4IiBmaWxsPSIjRjc5ODM5IiBwLWlkPSIyMDQxIj48L3BhdGg+PHBhdGggZD0iTTY3Ni44NjQgNzExLjY4SDM0NS4wODhDMzE4LjQ2NCA2MjQuNjQgMzEyLjMyIDUzMi40OCAzMzEuNzc2IDQ0My4zOTJjMjIuNTI4LTEwMS4zNzYgNzAuNjU2LTE5Ny42MzIgMTQwLjI4OC0yNzcuNTA0bDcuMTY4LTguMTkyYzE2LjM4NC0xOS40NTYgNDYuMDgtMTkuNDU2IDYyLjQ2NCAwbDcuMTY4IDguMTkyYzcwLjY1NiA3OS44NzIgMTE3Ljc2IDE3Ni4xMjggMTQwLjI4OCAyNzcuNTA0IDIwLjQ4IDg5LjA4OCAxNC4zMzYgMTgxLjI0OC0xMi4yODggMjY4LjI4OHoiIGZpbGw9IiMwMDRGRkYiIHAtaWQ9IjIwNDIiPjwvcGF0aD48cGF0aCBkPSJNNDY3Ljk2OCA2NzUuODRjLTUxLjIgMC05NS4yMzItMzcuODg4LTEwMi40LTg4LjA2NC04LjE5Mi02MC40MTYtNi4xNDQtMTIwLjgzMiA2LjE0NC0xODAuMjI0IDIxLjUwNC05NS4yMzIgNjQuNTEyLTE4NS4zNDQgMTI2Ljk3Ni0yNjIuMTQ0LTguMTkyIDIuMDQ4LTE1LjM2IDYuMTQ0LTIwLjQ4IDEyLjI4OGwtNy4xNjggOC4xOTJDNDAyLjQzMiAyNDUuNzYgMzU0LjMwNCAzNDAuOTkyIDMzMS43NzYgNDQzLjM5MiAzMTIuMzIgNTMyLjQ4IDMxOC40NjQgNjI0LjY0IDM0NS4wODggNzExLjY4aDMzMS43NzZjNC4wOTYtMTIuMjg4IDcuMTY4LTIzLjU1MiAxMC4yNC0zNS44NEg0NjcuOTY4eiIgZmlsbD0iIzFENkZGRiIgcC1pZD0iMjA0MyI+PC9wYXRoPjxwYXRoIGQ9Ik0zODEuOTUyIDcyMS45MmgyMzYuNTQ0Vjc3OC4yNEgzODEuOTUyeiIgZmlsbD0iIzAwNEZGRiIgcC1pZD0iMjA0NCI+PC9wYXRoPjxwYXRoIGQ9Ik01MTQuNjk5Mjc2MjUgNDc0LjA2MzEyNjMxSDUwOC42MTAyMTEyM2wzLjA0NDUzMjUxIDIuODg3NTk3ODV6IiBmaWxsPSIjZmZmZmZmIiBwLWlkPSIyMDQ1Ij48L3BhdGg+PHBhdGggZD0iTTQzMC4wOCA0MjcuMDA4YTgwLjg5NiA3OS44NzIgMCAxIDAgMTYxLjc5MiAwIDgwLjg5NiA3OS44NzIgMCAxIDAgLTE2MS43OTIgMFoiIGZpbGw9IiNFOUYzRkIiIHAtaWQ9IjIwNDYiPjwvcGF0aD48L3N2Zz4='
}
},
computed: {
updateList() {
if (!this.updateContent) {
return ['修复已知问题', '优化用户体验', '提升系统稳定性'];
}
// 将更新内容按换行或分号分割
return this.updateContent.split(/[\n;]/).filter(item => item.trim());
}
},
methods: {
handleMaskClick() {
if (!this.forceUpdate && !this.downloading) {
this.handleClose();
}
},
handleClose() {
if (this.forceUpdate || this.downloading) {
return;
}
this.$emit('close');
},
handleUpdate() {
// #ifdef APP-PLUS
if (this.downloading) {
return;
}
if (!this.downloadUrl) {
uni.showToast({
title: '下载地址无效',
icon: 'none'
});
return;
}
this.downloading = true;
this.downloadProgress = 0;
// 创建下载任务
const downloadTask = uni.downloadFile({
url: this.downloadUrl.trim(),
success: (res) => {
if (res.statusCode === 200) {
console.log('下载成功,文件路径:', res.tempFilePath);
// 下载完成,安装应用
this.installApp(res.tempFilePath);
} else {
console.error('下载失败,状态码:', res.statusCode);
uni.showToast({
title: '下载失败',
icon: 'none'
});
this.downloading = false;
}
},
fail: (err) => {
console.error('下载失败:', err);
uni.showToast({
title: '下载失败,请稍后重试',
icon: 'none'
});
this.downloading = false;
}
});
// 监听下载进度
downloadTask.onProgressUpdate((res) => {
this.downloadProgress = res.progress;
console.log('下载进度:', res.progress + '%');
});
this.downloadTask = downloadTask;
// #endif
},
installApp(filePath) {
// #ifdef APP-PLUS
console.log('开始安装应用:', filePath);
plus.runtime.install(
filePath,
{
force: false
},
() => {
console.log('安装成功');
uni.showToast({
title: '安装成功,请重启应用',
icon: 'success',
duration: 2000
});
// 安装成功后关闭弹窗
setTimeout(() => {
this.downloading = false;
this.downloadProgress = 0;
this.$emit('close');
// 如果是强制更新,重启应用
if (this.forceUpdate) {
plus.runtime.restart();
}
}, 2000);
},
(error) => {
console.error('安装失败:', error);
uni.showToast({
title: '安装失败',
icon: 'none'
});
this.downloading = false;
}
);
// #endif
}
},
beforeDestroy() {
// 组件销毁时,取消下载任务
if (this.downloadTask) {
this.downloadTask.abort();
}
}
}
</script>
<style lang="scss" scoped>
.update-dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 99999;
}
.update-dialog-container {
width: 580rpx;
background: linear-gradient(180deg, #4A9FF5 0%, #2E7FD9 100%);
border-radius: 32rpx;
position: relative;
overflow: visible;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.3);
}
.rocket-container {
position: absolute;
top: -120rpx;
left: 50%;
transform: translateX(-50%);
width: 200rpx;
height: 200rpx;
display: flex;
align-items: center;
justify-content: center;
}
.rocket-wrapper {
position: relative;
width: 100%;
height: 100%;
animation: rocketFloat 2s ease-in-out infinite;
}
/* 火箭 SVG 图片 */
.rocket-svg {
position: absolute;
top: 10rpx;
left: 50%;
transform: translateX(-50%);
width: 160rpx;
height: 160rpx;
z-index: 10;
}
/* 火焰容器 */
.flame-container {
position: absolute;
bottom: 10rpx;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 80rpx;
display: flex;
flex-direction: column;
align-items: center;
z-index: 5;
}
/* 火焰效果 */
.flame {
position: absolute;
border-radius: 50%;
animation: flameFlicker 0.3s ease-in-out infinite alternate;
}
.flame-1 {
width: 40rpx;
height: 30rpx;
background: radial-gradient(ellipse at center, #FCD34D 0%, #FBBF24 50%, transparent 80%);
top: 0;
animation-delay: 0s;
}
.flame-2 {
width: 30rpx;
height: 40rpx;
background: radial-gradient(ellipse at center, #FBBF24 0%, #F59E0B 50%, transparent 80%);
top: 15rpx;
animation-delay: 0.1s;
}
.flame-3 {
width: 20rpx;
height: 35rpx;
background: radial-gradient(ellipse at center, #F59E0B 0%, #EF4444 50%, transparent 80%);
top: 30rpx;
animation-delay: 0.2s;
}
/* 星星装饰 */
.star {
position: absolute;
width: 8rpx;
height: 8rpx;
background: #FCD34D;
border-radius: 50%;
box-shadow: 0 0 10rpx #FCD34D;
}
.star-1 {
top: 30rpx;
left: 20rpx;
animation: starTwinkle 2s ease-in-out infinite;
animation-delay: 0s;
}
.star-2 {
top: 50rpx;
right: 15rpx;
animation: starTwinkle 2s ease-in-out infinite;
animation-delay: 0.5s;
}
.star-3 {
top: 80rpx;
left: 10rpx;
animation: starTwinkle 1.5s ease-in-out infinite;
animation-delay: 1s;
}
.star-4 {
top: 100rpx;
right: 20rpx;
animation: starTwinkle 1.5s ease-in-out infinite;
animation-delay: 1.5s;
}
/* 动画定义 */
@keyframes rocketFloat {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-15rpx);
}
}
@keyframes flameFlicker {
0% {
opacity: 0.8;
transform: scaleY(1);
}
100% {
opacity: 1;
transform: scaleY(1.2);
}
}
@keyframes starTwinkle {
0%, 100% {
opacity: 0.3;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.5);
}
}
.dialog-content {
padding: 50rpx 40rpx 40rpx;
background: #FFFFFF;
border-radius: 32rpx;
margin-top: 80rpx;
}
.dialog-title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
text-align: center;
margin-bottom: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.force-tag {
display: inline-block;
margin-top: 10rpx;
padding: 4rpx 16rpx;
background: linear-gradient(135deg, #FF6B6B 0%, #FF4757 100%);
color: #FFFFFF;
font-size: 20rpx;
border-radius: 20rpx;
font-weight: normal;
animation: tagPulse 1.5s ease-in-out infinite;
}
@keyframes tagPulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(255, 71, 87, 0.7);
}
50% {
transform: scale(1.05);
box-shadow: 0 0 0 8rpx rgba(255, 71, 87, 0);
}
}
.force-notice {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx;
margin-bottom: 24rpx;
background: linear-gradient(135deg, #FFF5F5 0%, #FFE5E5 100%);
border-radius: 16rpx;
border: 2rpx solid #FFB8B8;
}
.force-notice-icon {
font-size: 32rpx;
margin-right: 12rpx;
}
.force-notice-text {
font-size: 26rpx;
color: #FF4757;
font-weight: 500;
}
.update-content {
background: #F8F9FA;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 40rpx;
max-height: 300rpx;
overflow-y: auto;
}
.update-item {
display: block;
font-size: 28rpx;
color: #666666;
line-height: 44rpx;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
}
.progress-container {
margin-bottom: 40rpx;
}
.progress-bar {
width: 100%;
height: 12rpx;
background: #E5E7EB;
border-radius: 6rpx;
overflow: hidden;
margin-bottom: 16rpx;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #4A9FF5 0%, #2E7FD9 100%);
border-radius: 6rpx;
transition: width 0.3s ease;
}
.progress-text {
display: block;
text-align: center;
font-size: 24rpx;
color: #4A9FF5;
font-weight: bold;
}
.button-container {
width: 100%;
}
.update-button {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #4A9FF5 0%, #2E7FD9 100%);
border-radius: 44rpx;
border: none;
color: #FFFFFF;
font-size: 32rpx;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(46, 127, 217, 0.4);
&.downloading {
background: #CCCCCC;
box-shadow: none;
}
&::after {
border: none;
}
}
.close-button {
position: absolute;
bottom: -120rpx;
left: 50%;
transform: translateX(-50%);
width: 80rpx;
height: 80rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid rgba(255, 255, 255, 0.6);
}
.close-icon {
font-size: 60rpx;
color: #FFFFFF;
line-height: 1;
font-weight: 300;
}
</style>

View File

@@ -0,0 +1,131 @@
<template>
<view class="update-modal" v-if="show" @tap.stop="handleMaskClick">
<view class="modal-content" @tap.stop>
<!-- 版本号标题 -->
<view class="version-title">发现新版本 {{ version }}</view>
<!-- 更新内容 -->
<view class="update-content">
<text class="content-text">{{ updateContent }}</text>
</view>
<!-- 按钮区域 -->
<view class="button-area">
<view class="cancel-btn" v-if="!forceUpdate" @tap="handleCancel">稍后再说</view>
<view class="confirm-btn" @tap="handleConfirm">立即更新</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'UpdateModal',
props: {
show: {
type: Boolean,
default: false
},
version: {
type: String,
default: ''
},
updateContent: {
type: String,
default: ''
},
downloadUrl: {
type: String,
default: ''
},
forceUpdate: {
type: Boolean,
default: false
}
},
methods: {
handleMaskClick() {
// 强制更新时,点击遮罩不关闭
if (!this.forceUpdate) {
this.handleCancel();
}
},
handleCancel() {
this.$emit('cancel');
},
handleConfirm() {
this.$emit('confirm', this.downloadUrl);
}
}
}
</script>
<style lang="scss" scoped>
.update-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
.modal-content {
width: 600rpx;
background-color: #ffffff;
border-radius: 24rpx;
overflow: hidden;
.version-title {
text-align: center;
font-size: 36rpx;
font-weight: 600;
color: #333333;
padding: 60rpx 40rpx 40rpx;
}
.update-content {
max-height: 600rpx;
padding: 0 40rpx 40rpx;
overflow-y: auto;
.content-text {
font-size: 28rpx;
line-height: 44rpx;
color: #666666;
white-space: pre-wrap;
word-break: break-all;
}
}
.button-area {
display: flex;
border-top: 1px solid #eeeeee;
.cancel-btn,
.confirm-btn {
flex: 1;
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 500;
}
.cancel-btn {
color: #999999;
border-right: 1px solid #eeeeee;
}
.confirm-btn {
color: #007aff;
}
}
}
}
</style>

View File

@@ -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
}
}
}

View File

@@ -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"
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 526 KiB