]*>([\s\S]*?)<\/div>)?\s*<\/div>\s*<\/header>/g,
- replacement: (match, title, rightContent) => {
- const rightContentStr = rightContent ? `\n rightContent={\n ${rightContent.trim()}\n }` : '';
- return `
`;
- }
- },
- {
- // 替换简单的header结构
- pattern: /
\s*\s*
]*>([^<]*)<\/h1>\s*<\/div>\s*<\/header>/g,
- replacement: (match, title) => {
- return ``;
- }
- },
- {
- // 添加PageHeader导入
- pattern: /import React[^;]+;/,
- replacement: (match) => {
- return `${match}\nimport PageHeader from '@/components/PageHeader';`;
- }
- }
-];
-
-function updateFile(filePath) {
- try {
- const fullPath = path.join(process.cwd(), filePath);
- if (!fs.existsSync(fullPath)) {
- console.log(`文件不存在: ${filePath}`);
- return;
- }
-
- let content = fs.readFileSync(fullPath, 'utf8');
- let updated = false;
-
- // 应用更新规则
- updateRules.forEach(rule => {
- const newContent = content.replace(rule.pattern, rule.replacement);
- if (newContent !== content) {
- content = newContent;
- updated = true;
- }
- });
-
- if (updated) {
- fs.writeFileSync(fullPath, content, 'utf8');
- console.log(`✅ 已更新: ${filePath}`);
- } else {
- console.log(`⏭️ 无需更新: ${filePath}`);
- }
- } catch (error) {
- console.error(`❌ 更新失败: ${filePath}`, error.message);
- }
-}
-
-// 执行批量更新
-console.log('🚀 开始批量更新页面Header...\n');
-
-pagesToUpdate.forEach(filePath => {
- updateFile(filePath);
-});
-
-console.log('\n✨ 批量更新完成!');
-console.log('\n📝 注意事项:');
-console.log('1. 请检查更新后的文件是否正确');
-console.log('2. 可能需要手动调整一些特殊的header结构');
-console.log('3. 确保所有页面都正确导入了PageHeader组件');
-console.log('4. 运行 npm run build 检查是否有编译错误');
\ No newline at end of file
diff --git a/Server/README_moments.md b/Server/README_moments.md
new file mode 100644
index 00000000..9e31075d
--- /dev/null
+++ b/Server/README_moments.md
@@ -0,0 +1,147 @@
+# 微信朋友圈数据处理功能
+
+本模块提供了微信朋友圈数据的获取、存储和查询功能,支持保留驼峰命名结构的原始数据。
+
+## 数据库表结构
+
+项目包含一个数据表:
+
+**wechat_moments** - 存储朋友圈基本信息
+- `id`: 自增主键
+- `wechatAccountId`: 微信账号ID
+- `wechatFriendId`: 微信好友ID
+- `snsId`: 朋友圈消息ID
+- `commentList`: 评论列表JSON
+- `createTime`: 创建时间戳
+- `likeList`: 点赞列表JSON
+- `content`: 朋友圈内容
+- `lat`: 纬度
+- `lng`: 经度
+- `location`: 位置信息
+- `picSize`: 图片大小
+- `resUrls`: 资源URL列表
+- `userName`: 用户名
+- `type`: 朋友圈类型
+- `create_time`: 数据创建时间
+- `update_time`: 数据更新时间
+
+## API接口
+
+### 1. 获取朋友圈信息
+
+```
+GET/POST /api/websocket/getMoments
+```
+
+**参数:**
+- `wechatAccountId`: 微信账号ID
+- `wechatFriendId`: 微信好友ID
+- `count`: 获取条数,默认5条
+
+获取指定账号和好友的朋友圈信息,并自动保存到数据库。
+
+### 2. 保存单条朋友圈数据
+
+```
+POST /api/websocket/saveSingleMoment
+```
+
+**参数:**
+- `commentList`: 评论列表
+- `createTime`: 创建时间戳
+- `likeList`: 点赞列表
+- `momentEntity`: 朋友圈实体,包含以下字段:
+ - `content`: 朋友圈内容
+ - `lat`: 纬度
+ - `lng`: 经度
+ - `location`: 位置信息
+ - `picSize`: 图片大小
+ - `resUrls`: 资源URL列表
+ - `urls`: 媒体URL列表
+ - `userName`: 用户名
+- `snsId`: 朋友圈ID
+- `type`: 朋友圈类型
+- `wechatAccountId`: 微信账号ID
+- `wechatFriendId`: 微信好友ID
+
+保存单条朋友圈数据到数据库,保持原有的驼峰数据结构。系统会将`momentEntity`中的字段提取并单独存储,不包括`objectType`和`createTime`字段。
+
+### 3. 获取朋友圈数据列表
+
+```
+GET/POST /api/websocket/getMomentsList
+```
+
+**参数:**
+- `wechatAccountId`: 微信账号ID (可选)
+- `wechatFriendId`: 微信好友ID (可选)
+- `page`: 页码,默认1
+- `pageSize`: 每页条数,默认10
+- `startTime`: 开始时间戳 (可选)
+- `endTime`: 结束时间戳 (可选)
+
+获取已保存的朋友圈数据列表,支持分页和条件筛选。返回的数据会自动构建`momentEntity`字段以保持API兼容性。
+
+### 4. 获取朋友圈详情
+
+```
+GET/POST /api/websocket/getMomentDetail
+```
+
+**参数:**
+- `snsId`: 朋友圈ID
+- `wechatAccountId`: 微信账号ID
+
+获取单条朋友圈的详细信息,包括评论、点赞和资源URL等。返回的数据会自动构建`momentEntity`字段以保持API兼容性。
+
+## 使用示例
+
+### 保存单条朋友圈数据
+
+```php
+$data = [
+ 'commentList' => [],
+ 'createTime' => 1742777232,
+ 'likeList' => [],
+ 'momentEntity' => [
+ 'content' => "第一位个人与Stussy联名的中国名人,不是陈冠希,不是葛民辉,而是周杰伦!",
+ 'lat' => 0,
+ 'lng' => 0,
+ 'location' => "",
+ 'picSize' => 0,
+ 'resUrls' => [],
+ 'snsId' => "-3827269039168736643",
+ 'urls' => ["http://wxapp.tc.qq.com/251/20304/stodownload?encfilekey=..."],
+ 'userName' => "wxid_afixeeh53lt012"
+ ],
+ 'snsId' => "-3827269039168736643",
+ 'type' => 28,
+ 'wechatAccountId' => 123456, // 替换为实际的微信账号ID
+ 'wechatFriendId' => "wxid_example" // 替换为实际的微信好友ID
+];
+
+// 发送请求
+$result = curl_post('/api/websocket/saveSingleMoment', $data);
+```
+
+### 查询朋友圈列表
+
+```php
+// 获取特定账号的朋友圈
+$params = [
+ 'wechatAccountId' => 123456,
+ 'page' => 1,
+ 'pageSize' => 20
+];
+
+// 发送请求
+$result = curl_get('/api/websocket/getMomentsList', $params);
+```
+
+## 注意事项
+
+1. 所有JSON格式的数据在保存时都会进行编码,查询时会自动解码并还原为原始数据结构。
+2. 数据库中的字段名保持驼峰命名格式,与微信API返回的数据结构保持一致。
+3. 尽管数据库中将`momentEntity`的字段拆分为独立字段存储,但API接口返回时会重新构建`momentEntity`结构,以保持与原始API的兼容性。
+4. `objectType`和`createTime`字段已从`momentEntity`中移除,不再单独存储。
+5. 图片或视频资源URLs直接存储在朋友圈主表中,不再单独存储到资源表。
\ No newline at end of file
diff --git a/Server/README_wechat_chatroom_sync.md b/Server/README_wechat_chatroom_sync.md
new file mode 100644
index 00000000..fb2e1868
--- /dev/null
+++ b/Server/README_wechat_chatroom_sync.md
@@ -0,0 +1,105 @@
+# 微信群聊同步功能
+
+本功能用于自动同步微信群聊数据,支持分页获取群聊列表以及群成员信息,并将数据保存到数据库中。
+
+## 功能特点
+
+1. 支持分页获取微信群聊列表
+2. 自动获取每个群聊的成员信息
+3. 支持通过关键词筛选群聊
+4. 支持按微信账号筛选群聊
+5. 可选择是否包含已删除的群聊
+6. 使用队列处理,支持大量数据的同步
+7. 支持失败重试机制
+8. 提供命令行和HTTP接口两种触发方式
+
+## 数据表结构
+
+本功能使用以下数据表:
+
+1. **wechat_chatroom** - 存储微信群聊信息
+2. **wechat_chatroom_member** - 存储微信群聊成员信息
+
+## 使用方法
+
+### 1. HTTP接口触发
+
+```
+GET/POST /api/wechat_chatroom/syncChatrooms
+```
+
+**参数:**
+- `pageIndex`: 起始页码,默认0
+- `pageSize`: 每页大小,默认100
+- `keyword`: 群名关键词,可选
+- `wechatAccountKeyword`: 微信账号关键词,可选
+- `isDeleted`: 是否包含已删除群聊,可选
+
+**示例:**
+```
+/api/wechat_chatroom/syncChatrooms?pageSize=50
+```
+
+### 2. 命令行触发
+
+```bash
+php think sync:wechat:chatrooms [选项]
+```
+
+**选项:**
+- `-p, --pageIndex`: 起始页码,默认0
+- `-s, --pageSize`: 每页大小,默认100
+- `-k, --keyword`: 群名关键词,可选
+- `-a, --account`: 微信账号关键词,可选
+- `-d, --deleted`: 是否包含已删除群聊,可选
+
+**示例:**
+```bash
+# 基本用法
+php think sync:wechat:chatrooms
+
+# 指定页大小和关键词
+php think sync:wechat:chatrooms -s 50 -k "测试群"
+
+# 指定账号关键词
+php think sync:wechat:chatrooms --account "张三"
+```
+
+### 3. 定时任务配置
+
+可以将命令添加到系统的定时任务(crontab)中,实现定期自动同步:
+
+```
+# 每天凌晨3点执行微信群聊同步
+0 3 * * * cd /path/to/your/project && php think sync:wechat:chatrooms
+```
+
+## 队列消费者配置
+
+为了处理同步任务,需要启动队列消费者:
+
+```bash
+# 启动微信群聊队列消费者
+php think queue:work --queue wechat_chatrooms
+```
+
+建议在生产环境中使用supervisor等工具来管理队列消费者进程。
+
+## 同步过程
+
+1. 触发同步任务,将初始页任务加入队列
+2. 队列消费者处理任务,获取当前页的群聊列表
+3. 如果当前页有数据且数量等于页大小,则将下一页任务加入队列
+4. 对每个获取到的群聊,添加获取群成员的任务
+5. 所有数据会自动保存到数据库中
+
+## 调试与日志
+
+同步过程的日志会记录在应用的日志目录中,可以通过查看日志了解同步状态和错误信息。
+
+## 注意事项
+
+1. 页大小建议设置为合理值(50-100),过大会导致请求超时
+2. 当数据量较大时,建议增加队列消费者的数量
+3. 确保系统授权信息正确,否则无法获取数据
+4. 数据同步是增量的,会自动更新已存在的记录
\ No newline at end of file
diff --git a/Server/application/api/controller/MessageController.php b/Server/application/api/controller/MessageController.php
index da033741..12d732c1 100644
--- a/Server/application/api/controller/MessageController.php
+++ b/Server/application/api/controller/MessageController.php
@@ -425,15 +425,31 @@ class MessageController extends BaseController
}
}
}
-
-
-
-
-
-
-
// 创建新记录
- WechatMessageModel::create($data);
+ $res = WechatMessageModel::create($data);
+
+ // 1 文字 3图片 47动态图片 34语言 43视频 42名片 40/20链接 49文件
+ if (!empty($res) && empty($item['isSend']) && in_array($item['msgType'],[1,3,20,34,40,42,43,47,49])){
+ $friend = Db::name('wechat_friendship')->where('id',$item['wechatFriendId'])->find();
+ if (!empty($friend)){
+ $trafficPoolId = Db::name('traffic_pool')->where('identifier',$friend['wechatId'])->value('id');
+ if (!empty($trafficPoolId)){
+ $data = [
+ 'type' => 4,
+ 'companyId' => $friend['companyId'],
+ 'trafficPoolId' => $trafficPoolId,
+ 'source' => 0,
+ 'uniqueId' => $res['id'],
+ 'sourceData' => json_encode([]),
+ 'remark' => '用户发送了消息',
+ 'createTime' => time(),
+ 'updateTime' => time()
+ ];
+ Db::name('user_portrait')->insert($data);
+ }
+ }
+ }
+
}
/**
diff --git a/Server/application/command.php b/Server/application/command.php
index 2dcd3178..570c76a4 100644
--- a/Server/application/command.php
+++ b/Server/application/command.php
@@ -32,4 +32,5 @@ return [
'sync:wechatData' => 'app\command\SyncWechatDataToCkbTask', // 同步微信数据到存客宝
'sync:allFriends' => 'app\command\SyncAllFriendsCommand', // 同步所有在线好友
'workbench:trafficDistribute' => 'app\command\WorkbenchTrafficDistributeCommand', // 工作台流量分发任务
+ 'switch:friends' => 'app\command\SwitchFriendsCommand',
];
diff --git a/Server/application/command/SwitchFriendsCommand.php b/Server/application/command/SwitchFriendsCommand.php
index d153f372..2ea77512 100644
--- a/Server/application/command/SwitchFriendsCommand.php
+++ b/Server/application/command/SwitchFriendsCommand.php
@@ -26,6 +26,9 @@ class SwitchFriendsCommand extends Command
protected function execute(Input $input, Output $output)
{
+ // 清理可能损坏的缓存数据
+ $this->clearCorruptedCache($output);
+
//处理流量分过期数据
$expUserData = Db::name('workbench_traffic_config_item')
->where('expTime','<=',time())
@@ -122,7 +125,15 @@ class SwitchFriendsCommand extends Command
$output->writeln('开始执行好友切换任务...');
do {
- $friends = Cache::get($cacheKey, []);
+ try {
+ $friends = Cache::get($cacheKey, []);
+ } catch (\Exception $e) {
+ // 如果缓存数据损坏,清空缓存并记录错误
+ $output->writeln('缓存数据损坏,正在清空缓存: ' . $e->getMessage());
+ Cache::rm($cacheKey);
+ $friends = [];
+ }
+
$toSwitch = [];
foreach ($friends as $friend) {
if (isset($friend['time']) && $friend['time'] < $now) {
@@ -196,7 +207,15 @@ class SwitchFriendsCommand extends Command
}
// 过滤掉已切换的,保留未切换和新进来的
- $newFriends = Cache::get($cacheKey, []);
+ try {
+ $newFriends = Cache::get($cacheKey, []);
+ } catch (\Exception $e) {
+ // 如果缓存数据损坏,清空缓存并记录错误
+ $output->writeln('缓存数据损坏,正在清空缓存: ' . $e->getMessage());
+ Cache::rm($cacheKey);
+ $newFriends = [];
+ }
+
$updated = [];
foreach ($newFriends as $friend) {
$friendId = !empty($friend['friendId']) ? $friend['friendId'] : $friend['id'];
@@ -210,7 +229,13 @@ class SwitchFriendsCommand extends Command
return ($a['time'] ?? 0) <=> ($b['time'] ?? 0);
});
- $success = Cache::set($cacheKey, $updated);
+ try {
+ $success = Cache::set($cacheKey, $updated);
+ } catch (\Exception $e) {
+ // 如果缓存设置失败,记录错误并继续
+ $output->writeln('缓存设置失败: ' . $e->getMessage());
+ $success = false;
+ }
$retry++;
} while (!$success && $retry < $maxRetry);
@@ -222,4 +247,24 @@ class SwitchFriendsCommand extends Command
$output->writeln('缓存已更新并排序');
}
+ /**
+ * 清理损坏的缓存数据
+ * @param Output $output
+ */
+ private function clearCorruptedCache(Output $output)
+ {
+ $cacheKey = 'allotWechatFriend';
+ try {
+ // 尝试读取缓存,如果失败则清空
+ $testData = Cache::get($cacheKey, []);
+ if (!is_array($testData)) {
+ $output->writeln('缓存数据格式错误,正在清空缓存');
+ Cache::rm($cacheKey);
+ }
+ } catch (\Exception $e) {
+ $output->writeln('检测到损坏的缓存数据,正在清空: ' . $e->getMessage());
+ Cache::rm($cacheKey);
+ }
+ }
+
}
\ No newline at end of file
diff --git a/Server/application/common.php b/Server/application/common.php
index d412b53a..5b6987ae 100644
--- a/Server/application/common.php
+++ b/Server/application/common.php
@@ -526,3 +526,29 @@ function dump()
{
call_user_func_array(['app\\common\\helper\\Debug', 'dump'], func_get_args());
}
+
+if (!function_exists('artificialAllotWechatFriend')) {
+ function artificialAllotWechatFriend($friend = [])
+ {
+ if (empty($friend)) {
+ return false;
+ }
+
+ //记录切换好友
+ $cacheKey = 'allotWechatFriend';
+ $cacheFriend = $friend;
+ $cacheFriend['time'] = time() + 120;
+ $maxRetry = 5;
+ $retry = 0;
+ do {
+ $cacheFriendData = Cache::get($cacheKey, []);
+ // 去重:移除同friendId的旧数据
+ $cacheFriendData = array_filter($cacheFriendData, function($item) use ($cacheFriend) {
+ return $item['friendId'] !== $cacheFriend['friendId'];
+ });
+ $cacheFriendData[] = $cacheFriend;
+ $success = Cache::set($cacheKey, $cacheFriendData);
+ $retry++;
+ } while (!$success && $retry < $maxRetry);
+ }
+}
\ No newline at end of file
diff --git a/Server/application/common/controller/PasswordLoginController.php b/Server/application/common/controller/PasswordLoginController.php
index c5cce3d4..74058fe4 100644
--- a/Server/application/common/controller/PasswordLoginController.php
+++ b/Server/application/common/controller/PasswordLoginController.php
@@ -53,7 +53,11 @@ class PasswordLoginController extends BaseController
throw new \Exception('用户不存在或已禁用', 403);
}
- if ($user->passwordMd5 !== md5($password)) {
+
+ $password = md5($password);
+
+
+ if ($user->passwordMd5 !== $password) {
throw new \Exception('账号或密码错误', 403);
}
diff --git a/Server/application/cunkebao/config/route.php b/Server/application/cunkebao/config/route.php
index 01efb604..69290e14 100644
--- a/Server/application/cunkebao/config/route.php
+++ b/Server/application/cunkebao/config/route.php
@@ -40,12 +40,13 @@ Route::group('v1/', function () {
Route::get('scenes-detail', 'app\cunkebao\controller\plan\GetPlanSceneListV1Controller@detail');
Route::post('create', 'app\cunkebao\controller\plan\PostCreateAddFriendPlanV1Controller@index');
Route::get('list', 'app\cunkebao\controller\plan\PlanSceneV1Controller@index');
- Route::get('copy', 'app\cunkebao\controller\plan\PlanSceneV1Controller@copy');
+ Route::get('copy', 'app\cunkebao\controller\plan\GetCreateAddFriendPlanV1Controller@copy');
Route::delete('delete', 'app\cunkebao\controller\plan\PlanSceneV1Controller@delete');
Route::post('updateStatus', 'app\cunkebao\controller\plan\PlanSceneV1Controller@updateStatus');
Route::get('detail', 'app\cunkebao\controller\plan\GetAddFriendPlanDetailV1Controller@index');
Route::PUT('update', 'app\cunkebao\controller\plan\PostUpdateAddFriendPlanV1Controller@index');
Route::get('getWxMinAppCode', 'app\cunkebao\controller\plan\PlanSceneV1Controller@getWxMinAppCode');
+ Route::get('getUserList', 'app\cunkebao\controller\plan\PlanSceneV1Controller@getUserList');
});
// 流量池相关
@@ -55,6 +56,9 @@ Route::group('v1/', function () {
Route::get('types', 'app\cunkebao\controller\traffic\GetPotentialTypeSectionV1Controller@index');
Route::get('sources', 'app\cunkebao\controller\traffic\GetTrafficSourceSectionV1Controller@index');
Route::get('statistics', 'app\cunkebao\controller\traffic\GetPoolStatisticsV1Controller@index');
+
+
+ Route::get('list', 'app\cunkebao\controller\TrafficController@getList');
});
// 工作台相关
@@ -99,6 +103,17 @@ Route::group('v1/', function () {
Route::get('getMemberList', 'app\cunkebao\controller\chatroom\GetChatroomListV1Controller@getMemberList'); // 获取群详情
});
+
+
+ //数据统计相关
+ Route::group('dashboard',function (){
+ Route::get('', 'app\cunkebao\controller\StatsController@baseInfoStats');
+ Route::get('plan-stats', 'app\cunkebao\controller\StatsController@planStats');
+ Route::get('sevenDay-stats', 'app\cunkebao\controller\StatsController@customerAcquisitionStats7Days');
+ Route::get('today-stats', 'app\cunkebao\controller\StatsController@todayStats');
+ });
+
+
})->middleware(['jwt']);
diff --git a/Server/application/cunkebao/controller/ContentLibraryController.php b/Server/application/cunkebao/controller/ContentLibraryController.php
index 6c522a6d..0d28937c 100644
--- a/Server/application/cunkebao/controller/ContentLibraryController.php
+++ b/Server/application/cunkebao/controller/ContentLibraryController.php
@@ -415,7 +415,7 @@ class ContentLibraryController extends Controller
// 关键词搜索
if (!empty($keyword)) {
- $where[] = ['content', 'like', '%' . $keyword . '%'];
+ $where[] = ['content|title', 'like', '%' . $keyword . '%'];
}
// 查询数据
@@ -440,22 +440,22 @@ class ContentLibraryController extends Controller
}
// 获取发送者信息
- if ($item['type'] == 'moment' && $item['friendId']) {
+ if ($item['type'] == 'moment' && !empty($item['friendId'])) {
$friendInfo = Db::name('wechat_friendship')
->alias('wf')
->join('wechat_account wa', 'wf.wechatId = wa.wechatId')
->where('wf.id', $item['friendId'])
->field('wa.nickname, wa.avatar')
->find();
- $item['senderNickname'] = $friendInfo['nickname'] ?: '';
- $item['senderAvatar'] = $friendInfo['avatar'] ?: '';
- }else if ($item['type'] == 'group_message' && $item['wechatChatroomId']) {
+ $item['senderNickname'] = !empty($friendInfo['nickname']) ? $friendInfo['nickname'] : '';
+ $item['senderAvatar'] = !empty( $friendInfo['avatar']) ? $friendInfo['avatar'] : '';
+ }else if ($item['type'] == 'group_message' && !empty($item['wechatChatroomId'])) {
$friendInfo = Db::table('s2_wechat_chatroom_member')
->field('nickname, avatar')
->where('wechatId', $item['wechatId'])
->find();
- $item['senderNickname'] = $friendInfo['nickname'] ?: '';
- $item['senderAvatar'] = $friendInfo['avatar'] ?: '';
+ $item['senderNickname'] = !empty($friendInfo['nickname']) ? $friendInfo['nickname'] : '';
+ $item['senderAvatar'] = !empty($friendInfo['avatar']) ? $friendInfo['avatar'] : '';
}
}
unset($item);
diff --git a/Server/application/cunkebao/controller/StatsController.php b/Server/application/cunkebao/controller/StatsController.php
new file mode 100644
index 00000000..acea1e6a
--- /dev/null
+++ b/Server/application/cunkebao/controller/StatsController.php
@@ -0,0 +1,199 @@
+ '周日',
+ 1 => '周一',
+ 2 => '周二',
+ 3 => '周三',
+ 4 => '周四',
+ 5 => '周五',
+ 6 => '周六',
+ ];
+
+ /**
+ * 基础信息
+ * @return \think\response\Json
+ */
+ public function baseInfoStats()
+ {
+ $deviceNum = Db::name('device')->where(['companyId' => $this->request->userInfo['companyId'], 'deleteTime' => 0])->count();
+ $wechatNum = Db::name('wechat_customer')->where(['companyId' => $this->request->userInfo['companyId']])->count();
+ $aliveWechatNum = Db::name('wechat_customer')->alias('wc')
+ ->join('device_wechat_login dwl', 'wc.wechatId = dwl.wechatId')
+ ->where(['wc.companyId' => $this->request->userInfo['companyId'], 'dwl.alive' => 1])
+ ->group('wc.wechatId')
+ ->count();
+ $data = [
+ 'deviceNum' => $deviceNum,
+ 'wechatNum' => $wechatNum,
+ 'aliveWechatNum' => $aliveWechatNum,
+ ];
+ return successJson($data, '获取成功');
+ }
+
+ /**
+ * 场景获客统计
+ * @return \think\response\Json
+ * @throws \think\db\exception\DataNotFoundException
+ * @throws \think\db\exception\ModelNotFoundException
+ * @throws \think\exception\DbException
+ */
+ public function planStats()
+ {
+
+ $num = $this->request->param('num', 4);
+
+ $planScene = Db::name('plan_scene')
+ ->field('id,name,image')
+ ->where(['status' => 1])
+ ->order('sort DESC')
+ ->page(1, $num)
+ ->select();
+
+ foreach ($planScene as &$v) {
+ $allNum = Db::name('customer_acquisition_task')->alias('ac')
+ ->join('task_customer tc', 'tc.task_id = ac.id')
+ ->where(['ac.sceneId' => $v['id'], 'ac.companyId' => $this->request->userInfo['companyId'], 'ac.deleteTime' => 0])
+ ->count();
+
+ $addNum = Db::name('customer_acquisition_task')->alias('ac')
+ ->join('task_customer tc', 'tc.task_id = ac.id')
+ ->where(['ac.sceneId' => $v['id'], 'ac.companyId' => $this->request->userInfo['companyId'], 'ac.deleteTime' => 0])
+ ->whereIn('tc.status', [1, 2, 3, 4])
+ ->count();
+
+ $passNum = Db::name('customer_acquisition_task')->alias('ac')
+ ->join('task_customer tc', 'tc.task_id = ac.id')
+ ->where(['ac.sceneId' => $v['id'], 'ac.companyId' => $this->request->userInfo['companyId'], 'ac.deleteTime' => 0])
+ ->whereIn('tc.status', [4])
+ ->count();
+
+ $v['allNum'] = $allNum;
+ $v['addNum'] = $addNum;
+ $v['passNum'] = $passNum;
+ }
+ unset($v);
+ return successJson($planScene, '获取成功');
+ }
+
+
+ public function todayStats()
+ {
+ $date = date('Y-m-d',time());
+ $start = strtotime($date . ' 00:00:00');
+ $end = strtotime($date . ' 23:59:59');
+ $companyId = $this->request->userInfo['companyId'];
+
+
+ $momentsNum = Db::name('workbench')->alias('w')
+ ->join('workbench_moments_sync_item wi', 'w.id = wi.workbenchId')
+ ->where(['w.companyId' => $companyId])
+ ->where('wi.createTime', 'between', [$start, $end])
+ ->count();
+
+ $groupPushNum = Db::name('workbench')->alias('w')
+ ->join('workbench_group_push_item wi', 'w.id = wi.workbenchId')
+ ->where(['w.companyId' => $companyId])
+ ->where('wi.createTime', 'between', [$start, $end])
+ ->count();
+
+
+ $addNum = Db::name('customer_acquisition_task')->alias('ac')
+ ->join('task_customer tc', 'tc.task_id = ac.id')
+ ->where(['ac.companyId' => $companyId, 'ac.deleteTime' => 0])
+ ->where('tc.updateTime', 'between', [$start, $end])
+ ->whereIn('tc.status', [1, 2, 3, 4])
+ ->count();
+
+ // 通过量
+ $passNum = Db::name('customer_acquisition_task')->alias('ac')
+ ->join('task_customer tc', 'tc.task_id = ac.id')
+ ->where(['ac.companyId' => $companyId, 'ac.deleteTime' => 0])
+ ->where('tc.updateTime', 'between', [$start, $end])
+ ->whereIn('tc.status', [4])
+ ->count();
+
+ if (!empty($passNum)){
+ $passRate = number_format(($addNum / $passNum) * 100,2) ;
+ }else{
+ $passRate = '0%';
+ }
+
+ $sysActive = '90%';
+ $data = [
+ 'momentsNum' => $momentsNum,
+ 'groupPushNum' => $groupPushNum,
+ 'addNum' => $addNum,
+ 'passNum' => $passNum,
+ 'passRate' => $passRate,
+ 'sysActive' => $sysActive,
+ ];
+ return successJson($data, '获取成功');
+ }
+
+
+
+ /**
+ * 近7天获客统计
+ * @return \think\response\Json
+ */
+ public function customerAcquisitionStats7Days()
+ {
+ $companyId = $this->request->userInfo['companyId'];
+ $days = 7;
+
+ $dates = [];
+ $allNum = [];
+ $addNum = [];
+ $passNum = [];
+
+ for ($i = $days - 1; $i >= 0; $i--) {
+ $date = date('Y-m-d', strtotime("-$i day"));
+ $start = strtotime($date . ' 00:00:00');
+ $end = strtotime($date . ' 23:59:59');
+
+ // 获客总量
+ $allNum[] = Db::name('customer_acquisition_task')->alias('ac')
+ ->join('task_customer tc', 'tc.task_id = ac.id')
+ ->where(['ac.companyId' => $companyId, 'ac.deleteTime' => 0])
+ ->where('tc.createTime', 'between', [$start, $end])
+ ->count();
+
+ // 添加量
+ $addNum[] = Db::name('customer_acquisition_task')->alias('ac')
+ ->join('task_customer tc', 'tc.task_id = ac.id')
+ ->where(['ac.companyId' => $companyId, 'ac.deleteTime' => 0])
+ ->where('tc.updateTime', 'between', [$start, $end])
+ ->whereIn('tc.status', [1, 2, 3, 4])
+ ->count();
+
+ // 通过量
+ $passNum[] = Db::name('customer_acquisition_task')->alias('ac')
+ ->join('task_customer tc', 'tc.task_id = ac.id')
+ ->where(['ac.companyId' => $companyId, 'ac.deleteTime' => 0])
+ ->where('tc.updateTime', 'between', [$start, $end])
+ ->whereIn('tc.status', [4])
+ ->count();
+
+ $week = date("w", strtotime($date));
+ $dates[] = self::WEEK[$week];
+ }
+ $data = [
+ 'date' => $dates,
+ 'allNum' => $allNum,
+ 'addNum' => $addNum,
+ 'passNum' => $passNum,
+ ];
+
+ return successJson($data, '获取成功');
+ }
+}
\ No newline at end of file
diff --git a/Server/application/cunkebao/controller/plan/PlanSceneV1Controller.php b/Server/application/cunkebao/controller/plan/PlanSceneV1Controller.php
index dca65350..c4cd7e08 100644
--- a/Server/application/cunkebao/controller/plan/PlanSceneV1Controller.php
+++ b/Server/application/cunkebao/controller/plan/PlanSceneV1Controller.php
@@ -407,7 +407,13 @@ class PlanSceneV1Controller extends BaseController
$posterWeChatMiniProgram = new PosterWeChatMiniProgram();
$result = $posterWeChatMiniProgram->generateMiniProgramCodeWithScene($taskId);
- return ResponseHelper::success($result, '获取小程序码成功');
+ $result = json_decode($result, true);
+ if ($result['code'] == 200){
+ return ResponseHelper::success($result['data'], '获取小程序码成功');
+ }else{
+ return ResponseHelper::error('获取小程序失败:' . $result['msg']);
+ }
+
}
@@ -433,7 +439,9 @@ class PlanSceneV1Controller extends BaseController
return ResponseHelper::error('获客场景id不能为空');
}
- $task = Db::name('customer_acquisition_task')->where(['id' => $planId, 'deleteTime' => 0])->find();
+ $task = Db::name('customer_acquisition_task')
+ ->where(['id' => $planId, 'deleteTime' => 0,'companyId' => $this->getUserInfo('companyId')])
+ ->find();
if(empty($task)) {
return ResponseHelper::error('活动不存在');
}
@@ -449,12 +457,24 @@ class PlanSceneV1Controller extends BaseController
$total = $query->count();
$list = $query->page($page, $pageSize)->order('id', 'desc')->select();
-
foreach ($list as &$item) {
+ unset($item['fail_reason'],$item['processed_wechat_ids'],$item['task_id']);
+ $userinfo = Db::table('s2_wechat_friend')
+ ->field('alias,wechatId,nickname,avatar')
+ ->where('alias|wechatId|phone|conRemark','like','%'.$item['phone'].'%')
+ ->order('id DESC')
+ ->find();
+
+ if (!empty($userinfo)) {
+ $item['userinfo'] = $userinfo;
+ }else{
+ $item['userinfo'] = [];
+ }
+
$item['tags'] = json_decode($item['tags'], true);
$item['siteTags'] = json_decode($item['siteTags'], true);
- $item['createTime'] = date('Y-m-d H:i:s', $item['createTime']);
- $item['updateTime'] = date('Y-m-d H:i:s', $item['updateTime']);
+ $item['createTime'] = !empty($item['createTime']) ? date('Y-m-d H:i:s', $item['createTime']) : '';
+ $item['updateTime'] = !empty($item['updateTime']) ? date('Y-m-d H:i:s', $item['updateTime']) : '';
}
diff --git a/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php b/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php
index 4ce512f7..b05b981f 100644
--- a/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php
+++ b/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php
@@ -25,36 +25,37 @@ class PosterWeChatMiniProgram extends Controller
// 生成小程序码,存客宝-操盘手调用
public function generateMiniProgramCodeWithScene($taskId = '') {
- $taskId = request()->param('id');
-
- $app = Factory::miniProgram(self::MINI_PROGRAM_CONFIG);
-
- // scene参数长度限制为32位
- // $scene = 'taskId=' . $taskId;
- $scene = 'id=' . $taskId;
-
- // 调用接口生成小程序码
- $response = $app->app_code->getUnlimit($scene, [
- 'page' => 'pages/poster/index2', // 必须是已经发布的小程序页面
- 'width' => 430, // 二维码的宽度,默认430
- // 'auto_color' => false, // 自动配置线条颜色
- // 'line_color' => ['r' => 0, 'g' => 0, 'b' => 0], // 颜色设置
- // 'is_hyaline' => false, // 是否需要透明底色
- ]);
-
- // 保存小程序码到文件
- if ($response instanceof StreamResponse) {
- // $filename = 'minicode_' . $taskId . '.png';
- // $response->saveAs('path/to/codes', $filename);
- // return 'path/to/codes/' . $filename;
-
- $img = $response->getBody()->getContents();//获取图片二进制流
- $img_base64 = 'data:image/png;base64,' .base64_encode($img);//转化base64
- return $img_base64;
+ if (empty($taskId)){
+ return json_encode(['code' => 500,'data' => '','msg' => '任务id不能为空']);
}
- // return false;
- return null;
+
+ try {
+ $app = Factory::miniProgram(self::MINI_PROGRAM_CONFIG);
+ // scene参数长度限制为32位
+ //$scene = 'taskId=' . $taskId;
+ $scene = sprintf("%s", $taskId);
+ // 调用接口生成小程序码
+ $response = $app->app_code->getUnlimit($scene, [
+ 'page' => 'pages/poster/index2', // 必须是已经发布的小程序页面
+ 'width' => 430, // 二维码的宽度,默认430
+ // 'auto_color' => false, // 自动配置线条颜色
+ // 'line_color' => ['r' => 0, 'g' => 0, 'b' => 0], // 颜色设置
+ // 'is_hyaline' => false, // 是否需要透明底色
+ ]);
+ // 保存小程序码到文件
+ if ($response instanceof StreamResponse) {
+ // $filename = 'minicode_' . $taskId . '.png';
+ // $response->saveAs('path/to/codes', $filename);
+ // return 'path/to/codes/' . $filename;
+
+ $img = $response->getBody()->getContents();//获取图片二进制流
+ $img_base64 = 'data:image/png;base64,' . base64_encode($img);//转化base64
+ return json_encode(['code' => 200, 'data' => $img_base64]);
+ }
+ }catch (\Exception $e) {
+ return json_encode(['code' => 500,'data' => '','msg' => $e->getMessage()]);
+ }
}
// getPhoneNumber
@@ -90,7 +91,8 @@ class PosterWeChatMiniProgram extends Controller
if (!$trafficPool) {
Db::name('traffic_pool')->insert([
'identifier' => $result['phone_info']['phoneNumber'],
- 'mobile' => $result['phone_info']['phoneNumber']
+ 'mobile' => $result['phone_info']['phoneNumber'],
+ 'createTime' => time()
]);
}
// 2. 写入 ck_task_customer: 以 task_id ~~identifier~~ phone 为条件,如果存在则忽略,使用类似laravel的firstOrcreate(但我不知道thinkphp5.1里的写法)
@@ -154,7 +156,7 @@ class PosterWeChatMiniProgram extends Controller
'id' => $task['id'],
'name' => $task['name'],
'poster' => ['sUrl' => $posterUrl],
- 'sTip' => '啦啦啦啦',
+ 'sTip' => '',
];
diff --git a/Server/application/cunkebao/controller/wechat/GetWechatsOnDevicesV1Controller.php b/Server/application/cunkebao/controller/wechat/GetWechatsOnDevicesV1Controller.php
index e41cdfc6..15f1714b 100644
--- a/Server/application/cunkebao/controller/wechat/GetWechatsOnDevicesV1Controller.php
+++ b/Server/application/cunkebao/controller/wechat/GetWechatsOnDevicesV1Controller.php
@@ -196,7 +196,7 @@ class GetWechatsOnDevicesV1Controller extends BaseController
// 关键词搜索(同时搜索微信号和昵称)
if (!empty($keyword = $this->request->param('keyword'))) {
- $where[] = ['exp', "w.alias LIKE '%{$keyword}%' OR w.nickname LIKE '%{$keyword}%'"];
+ $where[] = ["w.wechatId|w.alias|w.nickname", 'LIKE', '%' . $keyword . '%'];
}
$where['w.wechatId'] = array('in', implode(',', $wechatIds));
diff --git a/Server/crontab_tasks.md b/Server/crontab_tasks.md
new file mode 100644
index 00000000..00aadff3
--- /dev/null
+++ b/Server/crontab_tasks.md
@@ -0,0 +1,88 @@
+# 新版微信服务器定时任务配置
+
+以下为当前 command.php 注册的所有计划任务示例,按需调整执行频率和日志路径。
+
+```bash
+# 设备列表
+*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think device:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/device_list.log 2>&1
+
+# 微信好友列表
+*/5 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatFriends:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/wechat_friends_list.log 2>&1
+
+# 微信群列表
+*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatChatroom:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/wechat_chatroom_list.log 2>&1
+
+# 添加好友任务列表
+*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think friendTask:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/friend_task_list.log 2>&1
+
+# 微信客服列表
+*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think wechatList:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/wechat_list.log 2>&1
+
+# 公司账号列表
+*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think account:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/account_list.log 2>&1
+
+# 微信好友消息列表
+*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think message:friendsList >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/message_friends_list.log 2>&1
+
+# 微信群聊消息列表
+*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think message:chatroomList >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/message_chatroom_list.log 2>&1
+
+# 部门列表
+*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think department:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/department_list.log 2>&1
+
+# 同步内容库
+0 2 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think content:sync >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/content_sync.log 2>&1
+
+# 微信群好友列表
+*/30 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think groupFriends:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/group_friends_list.log 2>&1
+
+# 分配规则列表
+0 3 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think allotrule:list >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/allot_rule_list.log 2>&1
+
+# 自动创建分配规则
+0 4 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think allotrule:autocreate >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/allot_rule_autocreate.log 2>&1
+
+# 内容采集任务
+0 5 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think content:collect >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/content_collect.log 2>&1
+
+# 朋友圈采集任务
+0 6 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think moments:collect >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/moments_collect.log 2>&1
+
+# 工作台自动点赞任务
+0 7 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:autoLike >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_auto_like.log 2>&1
+
+# 工作台朋友圈同步任务
+0 8 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:moments >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_moments.log 2>&1
+
+# 同步微信数据到存客宝
+0 9 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think sync:wechatData >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/sync_wechat_data.log 2>&1
+
+# 工作台流量分发
+0 9 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:trafficDistribute >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/traffic_distribute.log 2>&1
+
+# 预防性切换好友
+*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think switch:friends >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/switch_friends.log 2>&1
+
+
+```
+
+## 说明
+
+- 所有命令都在 `/www/wwwroot/mckb_quwanzhi_com/Server` 目录下执行
+- 默认只获取未删除(活跃)的设备、微信好友和群聊
+- 已注释的命令(以#开头)是获取已删除或已停用数据的任务,可根据需要取消注释启用
+- 每个命令的执行结果都会记录到对应的日志文件中
+- 日志文件名格式包含了数据状态(如 `_active`, `_deleted`, `_stopped`)
+- 日志文件位于 `/www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/` 目录下
+- 大部分任务每5分钟执行一次(`*/5 * * * *` 表示每小时的第0,5,10,15...55分钟执行)
+- 设备列表的未删除设备任务每天凌晨1点执行一次(`0 1 * * *`)
+- 自动创建分配规则每小时整点执行一次(`0 * * * *`)
+- 内容采集任务每5分钟执行一次(`*/5 * * * *`)
+
+## 检查定时任务
+
+使用以下命令查看当前配置的 crontab 任务:
+
+```bash
+crontab -l
+```
\ No newline at end of file