AI功能提交

This commit is contained in:
wong
2025-09-17 16:51:11 +08:00
parent 706be3efd1
commit 02c94a12d5
15 changed files with 1060 additions and 10 deletions

View File

@@ -14,6 +14,7 @@ Route::group('v1/ai', function () {
//豆包ai
Route::group('doubao', function () {
Route::post('text', 'app\ai\controller\DouBaoAI@text');
Route::post('aiChat', 'app\ai\controller\DouBaoAI@aiChat');
});

View File

@@ -56,4 +56,40 @@ class DouBaoAI
$result = json_decode($result, true);
return successJson($result);
}
public function aiChat()
{
$this->__init();
$content = input('content','');
if (empty($content)){
return json_encode(['code' => 500, 'msg' => '提示词缺失']);
}
$content = $content. '
请结合上面的聊天记录给我最佳的客服回复';
// 发送请求
$params = [
'model' => 'doubao-1-5-pro-32k-250115',
'messages' => [
['role' => 'system', 'content' => '以下是客服跟用户的对话.'],
['role' => 'user', 'content' => $content],
],
/*'extra_headers' => [
'x-is-encrypted' => true
],
'temperature' => 1,
'top_p' => 0.7,
'max_tokens' => 4096,
'frequency_penalty' => 0,*/
];
$result = requestCurl($this->apiUrl, $params, 'POST', $this->headers, 'json');
$result = json_decode($result, true);
return successJson($result);
}
}

View File

@@ -26,8 +26,35 @@ Route::group('v1/', function () {
//客服相关
Route::group('message/', function () {
Route::get('list', 'app\chukebao\controller\MessageController@getList'); // 获取好友列表
Route::get('readMessage', 'app\chukebao\controller\MessageController@readMessage'); // 读取消息
});
//AI相关
Route::group('ai/', function () {
//问答
Route::group('questions/', function () {
Route::get('list', 'app\chukebao\controller\QuestionsController@getList'); // 问答列表
Route::post('add', 'app\chukebao\controller\QuestionsController@create'); // 问答添加
Route::post('update', 'app\chukebao\controller\QuestionsController@update'); // 问答更新
Route::get('delete', 'app\chukebao\controller\QuestionsController@delete'); // 问答删除
Route::get('detail', 'app\chukebao\controller\QuestionsController@detail'); // 问答详情
});
Route::group('settings/', function () {
Route::get('get', 'app\chukebao\controller\AiSettingsController@getSetting');
Route::post('set', 'app\chukebao\controller\AiSettingsController@setSetting');
});
Route::group('friend/', function () {
Route::post('set', 'app\chukebao\controller\AiSettingsController@setFriend');
});
});
});

View File

@@ -0,0 +1,153 @@
<?php
namespace app\chukebao\controller;
use app\chukebao\model\AiFriendSettings;
use app\chukebao\model\Questions;
use library\ResponseHelper;
use think\Db;
class AiSettingsController extends BaseController
{
const SETTING_DEFAULT = [
'audioSetting' => false,
'round' => 10,
'aiStopSetting' => [
'status' => true,
'key' => ['好', '不错', '好的', '下次', '可以']
],
'fileSetting' => [
'type' => 1,
'content' => ''
],
'modelSetting' => [
'model' => 'GPT-4',
'role' => '你是一名销售的AI助理同时也是一个工智能技术专家你的名字叫小灵你是单身女性出生于2003年10月10日喜欢听音乐和看电影有着丰富的人生阅历前成熟大方分享用幽默风趣的语言和客户交流顾客问起你的感情回复内容中不要使用号特别注意不要跟客户问题不要更多选择发送的信息。',
'businessBackground' => '灵销智能公司开发了多款AI营销智能技术产品以提升销售GPT AI大模型为核心接入打造的销售/营销/客服等AI智能应用为企业AI办公AI助理AI销售AI营销AI直播等大AI应用产品。',
'dialogueStyle' => '客户你们的AI解决方案具体是怎么收费的销售朋友我们的AI解决方案是根据项目需求来定的这样吧你能跟我说说你们的具体情况吗不过这样一分钱您看怎么样我们可以给您做个详细的方案对比。',
]
];
const TYPE_DATA = ['audioSetting', 'round', 'aiStopSetting', 'fileSetting', 'modelSetting'];
/**
* 获取配置信息
* @return \think\response\Json
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\ModelNotFoundException
* @throws \think\exception\DbException
*/
public function getSetting()
{
$userId = $this->getUserInfo('id');
$companyId = $this->getUserInfo('companyId');
$data = Db::name('ai_settings')->where(['userId' => $userId, 'companyId' => $companyId])->find();
if (empty($data)) {
$setting = self::SETTING_DEFAULT;
$data = [
'companyId' => $companyId,
'userId' => $userId,
'config' => json_encode($setting, 256),
'createTime' => time(),
'updateTime' => time()
];
Db::name('ai_settings')->insert($data);
} else {
$setting = json_decode($data['config'], true);
}
return ResponseHelper::success($setting, '获取成功');
}
/**
* 配置
* @return \think\response\Json
* @throws \Exception
*/
public function setSetting()
{
$key = $this->request->param('key', '');
$value = $this->request->param('value', '');
$userId = $this->getUserInfo('id');
$companyId = $this->getUserInfo('companyId');
if (empty($key) || empty($value)) {
return ResponseHelper::error('参数缺失');
}
if (!in_array($key, self::TYPE_DATA)) {
return ResponseHelper::error('该类型不在配置项');
}
Db::startTrans();
try {
$data = Db::name('ai_settings')->where(['userId' => $userId, 'companyId' => $companyId])->find();
if (empty($data)) {
$setting = self::SETTING_DEFAULT;
} else {
$setting = json_decode($data['config'], true);
}
$setting[$key] = $value;
$setting = json_encode($setting, 256);
Db::name('ai_settings')->where(['id' => $data['id']])->update(['config' => $setting, 'updateTime' => time()]);
Db::commit();
return ResponseHelper::success(' ', '配置成功');
} catch (\Exception $e) {
Db::rollback();
return ResponseHelper::error('配置失败:' . $e->getMessage());
}
}
public function setFriend()
{
$friendId = $this->request->param('friendId', '');
$wechatAccountId = $this->request->param('wechatAccountId', '');
$type = $this->request->param('type', 0);
$userId = $this->getUserInfo('id');
$companyId = $this->getUserInfo('companyId');
if (empty($friendId) || empty($wechatAccountId)) {
return ResponseHelper::error('参数缺失');
}
$friend = Db::table('s2_wechat_friend')->where(['id' => $friendId,'wechatAccountId' => $wechatAccountId])->find();
if (empty($friend)) {
return ResponseHelper::error('该好友不存在');
}
$aiFriendSettings = AiFriendSettings::where(['userId' => $userId, 'companyId' => $companyId,'friendId' => $friendId,'wechatAccountId' => $wechatAccountId])->find();
Db::startTrans();
try {
if (empty($aiFriendSettings)) {
$aiFriendSettings = new AiFriendSettings();
$aiFriendSettings->companyId = $companyId;
$aiFriendSettings->userId = $userId;
$aiFriendSettings->type = $type;
$aiFriendSettings->wechatAccountId = $wechatAccountId;
$aiFriendSettings->friendId = $friendId;
$aiFriendSettings->createTime = time();
$aiFriendSettings->updateTime = time();
}else{
$aiFriendSettings->type = $type;
$aiFriendSettings->updateTime = time();
}
$aiFriendSettings->save();
Db::commit();
return ResponseHelper::success(' ', '配置成功');
} catch (\Exception $e) {
Db::rollback();
return ResponseHelper::error('配置失败:' . $e->getMessage());
}
}
}

View File

@@ -25,8 +25,8 @@ class CustomerServiceController extends BaseController
->group('id')
->select();
foreach ($list as $k=>&$v){
$v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s',$v['createTime']) : '';
$v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s',$v['updateTime']) : '';
$v['createTime'] = !empty($v['createTime']) && is_numeric($v['createTime']) ? date('Y-m-d H:i:s',$v['createTime']) : '';
$v['updateTime'] = !empty($v['updateTime']) && is_numeric($v['updateTime']) ? date('Y-m-d H:i:s',$v['updateTime']) : '';
$v['labels'] = json_decode($v['labels'],true);
unset(
$v['accountUserName'],

View File

@@ -53,6 +53,9 @@ class MessageController extends BaseController
->field('id,nickname,avatar')
->find();
$v['msgInfo'] = $friend;
$v['unreadCount'] = Db::table('s2_wechat_message')
->where(['wechatFriendId' => $v['wechatFriendId'],'isRead' => 0])
->count();
}
if (!empty($v['wechatChatroomId'])){
@@ -61,12 +64,45 @@ class MessageController extends BaseController
->field('id,nickname,chatroomAvatar as avatar')
->find();
$v['msgInfo'] = $chatroom;
$v['unreadCount'] = Db::table('s2_wechat_message')
->where(['wechatChatroomId' => $v['wechatChatroomId'],'isRead' => 0])
->count();
}
}
unset($v);
return ResponseHelper::success($list);
}
public function readMessage(){
$wechatFriendId = $this->request->param('wechatFriendId', '');
$wechatChatroomId = $this->request->param('wechatChatroomId', '');
$accountId = $this->getUserInfo('s2_accountId');
if (empty($accountId)){
return ResponseHelper::error('请先登录');
}
if (empty($wechatChatroomId) && empty($wechatFriendId)){
return ResponseHelper::error('参数缺失');
}
$where = [];
if (!empty($wechatChatroomId)){
$where[] = ['wechatChatroomId','=',$wechatChatroomId];
}
if (!empty($wechatFriendId)){
$where[] = ['wechatFriendId','=',$wechatFriendId];
}
Db::table('s2_wechat_message')->where($where)->update(['isRead' => 1]);
return ResponseHelper::success([]);
}
}

View File

@@ -0,0 +1,232 @@
<?php
namespace app\chukebao\controller;
use app\chukebao\model\Questions;
use library\ResponseHelper;
use think\Db;
class QuestionsController 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', '');
$accountId = $this->getUserInfo('s2_accountId');
$userId = $this->getUserInfo('id');
$companyId = $this->getUserInfo('companyId');
if (empty($accountId)){
return ResponseHelper::error('请先登录');
}
$query = Questions::where(['userId' => $userId,'companyId' => $companyId,'isDel' => 0])
->order('id desc');
if (!empty($keyword)){
$query->where('questions|answers', 'like', '%'.$keyword.'%');
}
$list = $query->page($page, $limit)->select()->toArray();
$total = $query->count();
foreach ($list as $k => &$v){
$user = Db::name('users')->where(['id' => $v['userId']])->field('username,account')->find();
if (!empty($user)){
$v['userName'] = !empty($user['username']) ? $user['username'] : $user['account'];
}else{
$v['userName'] = '';
}
$v['answers'] = json_decode($v['answers'],true);
}
unset($v);
return ResponseHelper::success(['list'=>$list,'total'=>$total]);
}
/**
* 新增
* @return \think\response\Json
* @throws \Exception
*/
public function create(){
$type = $this->request->param('type', 0);
$questions = $this->request->param('questions', '');
$answers = $this->request->param('answers', []);
$status = $this->request->param('status', 0);
$accountId = $this->getUserInfo('s2_accountId');
$userId = $this->getUserInfo('id');
$companyId = $this->getUserInfo('companyId');
if (empty($accountId)){
return ResponseHelper::error('请先登录');
}
if (empty($questions) || empty($answers)){
return ResponseHelper::error('问题和答案不能为空');
}
Db::startTrans();
try {
$questionsModel = new Questions();
$questionsModel->type = $type;
$questionsModel->questions = $questions;
$questionsModel->answers = !empty($answers) ? json_encode($answers,256) : json_encode([],256);
$questionsModel->status = $status;
$questionsModel->accountId = $accountId;
$questionsModel->userId = $userId;
$questionsModel->companyId = $companyId;
$questionsModel->createTime = time();
$questionsModel->updateTime = time();
$questionsModel->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', 0);
$accountId = $this->getUserInfo('s2_accountId');
$userId = $this->getUserInfo('id');
$companyId = $this->getUserInfo('companyId');
$type = $this->request->param('type', 0);
$questions = $this->request->param('questions', '');
$answers = $this->request->param('answers', []);
$status = $this->request->param('status', 0);
$accountId = $this->getUserInfo('s2_accountId');
$userId = $this->getUserInfo('id');
$companyId = $this->getUserInfo('companyId');
if (empty($accountId)){
return ResponseHelper::error('请先登录');
}
if (empty($id)){
return ResponseHelper::error('参数缺失');
}
if (empty($questions) || empty($answers)){
return ResponseHelper::error('问题和答案不能为空');
}
Db::startTrans();
try {
$questionsData = Questions::where(['id' => $id,'userId' => $userId,'companyId' => $companyId,'isDel' => 0])->find();
$questionsData->type = $type;
$questionsData->questions = $questions;
$questionsData->answers = !empty($answers) ? json_encode($answers,256) : json_encode([],256);
$questionsData->status = $status;
$questionsData->accountId = $accountId;
$questionsData->userId = $userId;
$questionsData->companyId = $companyId;
$questionsData->updateTime = time();
$questionsData->save();
Db::commit();
return ResponseHelper::success(' ','更新成功');
} catch (\Exception $e) {
Db::rollback();
return ResponseHelper::error('更新失败:'.$e->getMessage());
}
}
/**
* 删除
* @return \think\response\Json
* @throws \Exception
*/
public function delete(){
$id = $this->request->param('id', 0);
$accountId = $this->getUserInfo('s2_accountId');
$userId = $this->getUserInfo('id');
$companyId = $this->getUserInfo('companyId');
if (empty($accountId)){
return ResponseHelper::error('请先登录');
}
if (empty($id)){
return ResponseHelper::error('参数缺失');
}
$questions = Questions::where(['id' => $id,'userId' => $userId,'companyId' => $companyId,'isDel' => 0])->find();
if (empty($questions)){
return ResponseHelper::error('该问题不存在或者已删除');
}
$res = Questions::where(['id' => $id])->update(['isDel' => 1,'deleteTime' => time()]);
if (!empty($res)){
return ResponseHelper::success('','已删除');
}else{
return ResponseHelper::error('删除失败');
}
}
/**
* 详情
* @return \think\response\Json
* @throws \Exception
*/
public function detail(){
$id = $this->request->param('id', 0);
$accountId = $this->getUserInfo('s2_accountId');
$userId = $this->getUserInfo('id');
$companyId = $this->getUserInfo('companyId');
if (empty($accountId)){
return ResponseHelper::error('请先登录');
}
if (empty($id)){
return ResponseHelper::error('参数缺失');
}
$questions = Questions::where(['id' => $id,'userId' => $userId,'companyId' => $companyId,'isDel' => 0])->find();
if (empty($questions)){
return ResponseHelper::error('该问题不存在或者已删除');
}
$questions['answers'] = json_decode($questions['answers'],true);
$user = Db::name('users')->where(['id' => $questions['userId']])->field('username,account')->find();
if (!empty($user)){
$questions['userName'] = !empty($user['username']) ? $user['username'] : $user['account'];
}else{
$questions['userName'] = '';
}
unset(
$questions['isDel'],
$questions['deleteTime'],
$questions['createTime'],
$questions['updateTime']
);
return ResponseHelper::success($questions,'获取成功');
}
}

View File

@@ -22,9 +22,57 @@ class WechatChatroomController extends BaseController
$total = $query->count();
foreach ($list as $k=>&$v){
$v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s',$v['createTime']) : '';
$v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s',$v['updateTime']) : '';
// 提取所有聊天室ID用于批量查询
$chatroomIds = array_column($list, 'id');
// 一次性查询所有聊天室的未读消息数量
$unreadCounts = [];
if (!empty($chatroomIds)) {
$unreadResults = Db::table('s2_wechat_message')
->field('wechatChatroomId, COUNT(*) as count')
->where('wechatChatroomId', 'in', $chatroomIds)
->where('isRead', 0)
->group('wechatChatroomId')
->select();
foreach ($unreadResults as $result) {
$unreadCounts[$result['wechatChatroomId']] = $result['count'];
}
}
// 一次性查询所有聊天室的最新消息
$latestMessages = [];
if (!empty($chatroomIds)) {
// 使用子查询获取每个聊天室的最新消息ID
$subQuery = Db::table('s2_wechat_message')
->field('MAX(id) as max_id, wechatChatroomId')
->where('wechatChatroomId', 'in', $chatroomIds)
->group('wechatChatroomId')
->buildSql();
// 查询最新消息的详细信息
$messageResults = Db::table('s2_wechat_message')
->alias('m')
->join([$subQuery => 'sub'], 'm.id = sub.max_id')
->field('m.*, sub.wechatChatroomId')
->select();
foreach ($messageResults as $message) {
$latestMessages[$message['wechatChatroomId']] = $message;
}
}
// 处理每个聊天室的数据
foreach ($list as $k => &$v) {
$v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s', $v['createTime']) : '';
$v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s', $v['updateTime']) : '';
$config = [
'unreadCount' => isset($unreadCounts[$v['id']]) ? $unreadCounts[$v['id']] : 0,
'chat' => isset($latestMessages[$v['id']]),
'msgTime' => isset($latestMessages[$v['id']]) ? $latestMessages[$v['id']]['wechatTime'] : 0
];
$v['config'] = $config;
}
unset($v);

View File

@@ -22,10 +22,60 @@ class WechatFriendController extends BaseController
$total = $query->count();
foreach ($list as $k=>&$v){
$v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s',$v['createTime']) : '';
$v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s',$v['updateTime']) : '';
$v['passTime'] = !empty($v['passTime']) ? date('Y-m-d H:i:s',$v['passTime']) : '';
// 提取所有好友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();
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();
// 查询最新消息的详细信息
$messageResults = Db::table('s2_wechat_message')
->alias('m')
->join([$subQuery => 'sub'], 'm.id = sub.max_id')
->field('m.*, sub.wechatFriendId')
->select();
foreach ($messageResults as $message) {
$latestMessages[$message['wechatFriendId']] = $message;
}
}
// 处理每个好友的数据
foreach ($list as $k => &$v) {
$v['createTime'] = !empty($v['createTime']) ? date('Y-m-d H:i:s', $v['createTime']) : '';
$v['updateTime'] = !empty($v['updateTime']) ? date('Y-m-d H:i:s', $v['updateTime']) : '';
$v['passTime'] = !empty($v['passTime']) ? date('Y-m-d H:i:s', $v['passTime']) : '';
$config = [
'unreadCount' => isset($unreadCounts[$v['id']]) ? $unreadCounts[$v['id']] : 0,
'chat' => isset($latestMessages[$v['id']]),
'msgTime' => isset($latestMessages[$v['id']]) ? $latestMessages[$v['id']]['wechatTime'] : 0
];
// 将消息配置添加到好友数据中
$v['config'] = $config;
}
unset($v);

View File

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

View File

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

View File

@@ -0,0 +1,100 @@
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\console\input\Option;
use think\Db;
class CleanExpiredGroupMessages extends Command
{
protected function configure()
{
$this->setName('clean:expired_group_messages')
->setDescription('Clean expired group messages from the database')
->addOption('days', 'd', Option::VALUE_OPTIONAL, 'Number of days to keep messages (default: 90)', 90)
->addOption('dry-run', null, Option::VALUE_NONE, 'Perform a dry run without deleting any data')
->addOption('batch-size', 'b', Option::VALUE_OPTIONAL, 'Batch size for deletion (default: 1000)', 1000);
}
protected function execute(Input $input, Output $output)
{
$days = (int)$input->getOption('days');
$dryRun = $input->getOption('dry-run');
$batchSize = (int)$input->getOption('batch-size');
if ($dryRun) {
$output->writeln("<info>Running in dry-run mode. No data will be deleted.</info>");
}
$cutoffDate = date('Y-m-d H:i:s', strtotime("-{$days} days"));
$output->writeln("<info>Cleaning group messages older than {$cutoffDate} (keeping last {$days} days)</info>");
// 清理微信群组消息
$this->cleanWechatGroupMessages($cutoffDate, $dryRun, $batchSize, $output);
$output->writeln("<info>Group message cleanup completed successfully.</info>");
}
protected function cleanWechatGroupMessages($cutoffDate, $dryRun, $batchSize, Output $output)
{
$output->writeln("\nCleaning s2_wechat_group_message table...");
// 获取符合条件的消息总数
$totalCount = Db::table('s2_wechat_group_message')
->where('createTime', '<', $cutoffDate)
->count();
if ($totalCount === 0) {
$output->writeln(" <comment>No expired group messages found.</comment>");
return;
}
$output->writeln(" Found {$totalCount} group messages to clean up.");
if ($dryRun) {
$output->writeln(" <comment>Dry run mode: would delete {$totalCount} group messages.</comment>");
return;
}
// 计算需要执行的批次数
$batches = ceil($totalCount / $batchSize);
$deletedCount = 0;
$output->writeln(" Deleting in {$batches} batches of {$batchSize} records...");
// 分批删除数据
for ($i = 0; $i < $batches; $i++) {
// 获取一批要删除的ID
$ids = Db::table('s2_wechat_group_message')
->where('createTime', '<', $cutoffDate)
->limit($batchSize)
->column('id');
if (empty($ids)) {
break;
}
// 删除这批数据
$count = Db::table('s2_wechat_group_message')
->whereIn('id', $ids)
->delete();
$deletedCount += $count;
$progress = round(($deletedCount / $totalCount) * 100, 2);
$output->write(" Progress: {$progress}% ({$deletedCount}/{$totalCount})\r");
// 短暂暂停,减轻数据库负担
usleep(500000); // 暂停0.5秒
}
$output->writeln("");
$output->writeln(" <info>Successfully deleted {$deletedCount} expired group messages.</info>");
// 优化表
$output->writeln(" Optimizing table...");
Db::execute("OPTIMIZE TABLE s2_wechat_group_message");
$output->writeln(" <info>Table optimization completed.</info>");
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\console\input\Option;
use think\Db;
class CleanExpiredMessages extends Command
{
protected function configure()
{
$this->setName('clean:expired_messages')
->setDescription('Clean expired messages from the database')
->addOption('days', 'd', Option::VALUE_OPTIONAL, 'Number of days to keep messages (default: 90)', 90)
->addOption('dry-run', null, Option::VALUE_NONE, 'Perform a dry run without deleting any data')
->addOption('batch-size', 'b', Option::VALUE_OPTIONAL, 'Batch size for deletion (default: 1000)', 1000);
}
protected function execute(Input $input, Output $output)
{
$days = (int)$input->getOption('days');
$dryRun = $input->getOption('dry-run');
$batchSize = (int)$input->getOption('batch-size');
if ($dryRun) {
$output->writeln("<info>Running in dry-run mode. No data will be deleted.</info>");
}
$cutoffDate = date('Y-m-d H:i:s', strtotime("-{$days} days"));
$output->writeln("<info>Cleaning messages older than {$cutoffDate} (keeping last {$days} days)</info>");
// 清理微信消息
$this->cleanWechatMessages($cutoffDate, $dryRun, $batchSize, $output);
$output->writeln("<info>Message cleanup completed successfully.</info>");
}
protected function cleanWechatMessages($cutoffDate, $dryRun, $batchSize, Output $output)
{
$output->writeln("\nCleaning s2_wechat_message table...");
// 获取符合条件的消息总数
$totalCount = Db::table('s2_wechat_message')
->where('createTime', '<', $cutoffDate)
->count();
if ($totalCount === 0) {
$output->writeln(" <comment>No expired messages found.</comment>");
return;
}
$output->writeln(" Found {$totalCount} messages to clean up.");
if ($dryRun) {
$output->writeln(" <comment>Dry run mode: would delete {$totalCount} messages.</comment>");
return;
}
// 计算需要执行的批次数
$batches = ceil($totalCount / $batchSize);
$deletedCount = 0;
$output->writeln(" Deleting in {$batches} batches of {$batchSize} records...");
// 分批删除数据
for ($i = 0; $i < $batches; $i++) {
// 获取一批要删除的ID
$ids = Db::table('s2_wechat_message')
->where('createTime', '<', $cutoffDate)
->limit($batchSize)
->column('id');
if (empty($ids)) {
break;
}
// 删除这批数据
$count = Db::table('s2_wechat_message')
->whereIn('id', $ids)
->delete();
$deletedCount += $count;
$progress = round(($deletedCount / $totalCount) * 100, 2);
$output->write(" Progress: {$progress}% ({$deletedCount}/{$totalCount})\r");
// 短暂暂停,减轻数据库负担
usleep(500000); // 暂停0.5秒
}
$output->writeln("");
$output->writeln(" <info>Successfully deleted {$deletedCount} expired messages.</info>");
// 优化表
$output->writeln(" Optimizing table...");
Db::execute("OPTIMIZE TABLE s2_wechat_message");
$output->writeln(" <info>Table optimization completed.</info>");
}
}

View File

@@ -0,0 +1,112 @@
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\Db;
class OptimizeMessageIndexes extends Command
{
protected function configure()
{
$this->setName('optimize:message_indexes')
->setDescription('Optimize database indexes for message-related tables');
}
protected function execute(Input $input, Output $output)
{
$output->writeln("Starting index optimization for message-related tables...");
// 优化 s2_wechat_message 表索引
$this->optimizeWechatMessageIndexes($output);
// 优化 s2_wechat_chatroom 表索引
$this->optimizeWechatChatroomIndexes($output);
// 优化 s2_wechat_friend 表索引
$this->optimizeWechatFriendIndexes($output);
$output->writeln("Index optimization completed successfully.");
}
protected function optimizeWechatMessageIndexes(Output $output)
{
$output->writeln("Optimizing s2_wechat_message table indexes...");
// 检查并添加 wechatChatroomId 索引
$this->addIndexIfNotExists('s2_wechat_message', 'idx_chatroom_id', 'wechatChatroomId', $output);
// 检查并添加 wechatFriendId 索引
$this->addIndexIfNotExists('s2_wechat_message', 'idx_friend_id', 'wechatFriendId', $output);
// 检查并添加 isRead 索引
$this->addIndexIfNotExists('s2_wechat_message', 'idx_is_read', 'isRead', $output);
// 检查并添加 type 索引
$this->addIndexIfNotExists('s2_wechat_message', 'idx_type', 'type', $output);
// 检查并添加 createTime 索引
$this->addIndexIfNotExists('s2_wechat_message', 'idx_create_time', 'createTime', $output);
// 检查并添加组合索引 (wechatChatroomId, isRead)
$this->addIndexIfNotExists('s2_wechat_message', 'idx_chatroom_read', 'wechatChatroomId,isRead', $output);
// 检查并添加组合索引 (wechatFriendId, isRead)
$this->addIndexIfNotExists('s2_wechat_message', 'idx_friend_read', 'wechatFriendId,isRead', $output);
}
protected function optimizeWechatChatroomIndexes(Output $output)
{
$output->writeln("Optimizing s2_wechat_chatroom table indexes...");
// 检查并添加 accountId 索引
$this->addIndexIfNotExists('s2_wechat_chatroom', 'idx_account_id', 'accountId', $output);
// 检查并添加 isDeleted 索引
$this->addIndexIfNotExists('s2_wechat_chatroom', 'idx_is_deleted', 'isDeleted', $output);
// 检查并添加组合索引 (accountId, isDeleted)
$this->addIndexIfNotExists('s2_wechat_chatroom', 'idx_account_deleted', 'accountId,isDeleted', $output);
}
protected function optimizeWechatFriendIndexes(Output $output)
{
$output->writeln("Optimizing s2_wechat_friend table indexes...");
// 检查并添加 accountId 索引
$this->addIndexIfNotExists('s2_wechat_friend', 'idx_account_id', 'accountId', $output);
// 检查并添加 isDeleted 索引
$this->addIndexIfNotExists('s2_wechat_friend', 'idx_is_deleted', 'isDeleted', $output);
// 检查并添加组合索引 (accountId, isDeleted)
$this->addIndexIfNotExists('s2_wechat_friend', 'idx_account_deleted', 'accountId,isDeleted', $output);
}
protected function addIndexIfNotExists($table, $indexName, $columns, Output $output)
{
try {
// 检查索引是否已存在
$indexExists = false;
$indexes = Db::query("SHOW INDEX FROM {$table}");
foreach ($indexes as $index) {
if ($index['Key_name'] === $indexName) {
$indexExists = true;
break;
}
}
if (!$indexExists) {
// 添加索引
Db::execute("ALTER TABLE {$table} ADD INDEX {$indexName} ({$columns})");
$output->writeln(" - Added index {$indexName} on {$table}({$columns})");
} else {
$output->writeln(" - Index {$indexName} already exists on {$table}");
}
} catch (\Exception $e) {
$output->writeln(" - Error adding index {$indexName} to {$table}: " . $e->getMessage());
}
}
}

View File

@@ -0,0 +1,121 @@
<?php
namespace app\command;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\console\input\Option;
class ScheduleMessageMaintenance extends Command
{
protected function configure()
{
$this->setName('schedule:message_maintenance')
->setDescription('Schedule and run message maintenance tasks')
->addOption('optimize-indexes', null, Option::VALUE_NONE, 'Run index optimization')
->addOption('clean-messages', null, Option::VALUE_NONE, 'Clean expired messages')
->addOption('days', 'd', Option::VALUE_OPTIONAL, 'Number of days to keep messages (default: 90)', 90)
->addOption('batch-size', 'b', Option::VALUE_OPTIONAL, 'Batch size for deletion (default: 1000)', 1000)
->addOption('dry-run', null, Option::VALUE_NONE, 'Perform a dry run without deleting any data');
}
protected function execute(Input $input, Output $output)
{
$optimizeIndexes = $input->getOption('optimize-indexes');
$cleanMessages = $input->getOption('clean-messages');
$days = (int)$input->getOption('days');
$batchSize = (int)$input->getOption('batch-size');
$dryRun = $input->getOption('dry-run');
// 如果没有指定任何选项,则运行所有维护任务
if (!$optimizeIndexes && !$cleanMessages) {
$optimizeIndexes = true;
$cleanMessages = true;
}
$output->writeln("<info>Starting scheduled message maintenance tasks...</info>");
$startTime = microtime(true);
// 运行索引优化
if ($optimizeIndexes) {
$this->runCommand($output, 'optimize:message_indexes');
}
// 清理过期消息
if ($cleanMessages) {
$options = [];
if ($days !== 90) {
$options[] = "--days={$days}";
}
if ($batchSize !== 1000) {
$options[] = "--batch-size={$batchSize}";
}
if ($dryRun) {
$options[] = "--dry-run";
}
$this->runCommand($output, 'clean:expired_messages', $options);
$this->runCommand($output, 'clean:expired_group_messages', $options);
}
$endTime = microtime(true);
$executionTime = round($endTime - $startTime, 2);
$output->writeln("<info>All maintenance tasks completed in {$executionTime} seconds.</info>");
}
protected function runCommand(Output $output, $command, array $options = [])
{
$output->writeln("\n<comment>Running command: {$command}</comment>");
$optionsStr = implode(' ', $options);
$fullCommand = "php think {$command} {$optionsStr}";
$output->writeln("Executing: {$fullCommand}");
$output->writeln("\n<info>Command output:</info>");
// 执行命令并实时输出结果
$descriptorSpec = [
0 => ["pipe", "r"], // stdin
1 => ["pipe", "w"], // stdout
2 => ["pipe", "w"] // stderr
];
$process = proc_open($fullCommand, $descriptorSpec, $pipes);
if (is_resource($process)) {
// 关闭标准输入
fclose($pipes[0]);
// 读取标准输出
while (!feof($pipes[1])) {
$line = fgets($pipes[1]);
if ($line !== false) {
$output->write($line);
}
}
fclose($pipes[1]);
// 读取标准错误
$errorOutput = stream_get_contents($pipes[2]);
fclose($pipes[2]);
// 获取命令执行结果
$exitCode = proc_close($process);
if ($exitCode !== 0) {
$output->writeln("\n<error>Command failed with exit code {$exitCode}</error>");
if (!empty($errorOutput)) {
$output->writeln("<error>Error output:</error>");
$output->writeln($errorOutput);
}
} else {
$output->writeln("\n<info>Command completed successfully.</info>");
}
} else {
$output->writeln("<error>Failed to execute command.</error>");
}
}
}