Merge branch 'yongpxu-dev' of https://e.coding.net/g-xtcy5189/cunkebao/cunkebao_v3 into yongpxu-dev

This commit is contained in:
笔记本里的永平
2025-07-21 17:47:39 +08:00
17 changed files with 742 additions and 143 deletions

12
.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
Store_vue/unpackage/
*.js
Store_vue/node_modules/
Server/.specstory/
Cunkebao/.next/
Cunkebao/.specstory/
.idea/
*.zip
*.cursorindexingignore
Store_vue/.specstory/
Store_vue/.vscode/
SuperAdmin/.specstory/

6
Cunkebao/next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -1,87 +0,0 @@
const fs = require('fs');
const path = require('path');
// 需要更新的页面文件列表
const pagesToUpdate = [
'src/pages/scenarios/ScenarioDetail.tsx',
'src/pages/scenarios/NewPlan.tsx',
'src/pages/plans/Plans.tsx',
'src/pages/plans/PlanDetail.tsx',
'src/pages/orders/Orders.tsx',
'src/pages/profile/Profile.tsx',
'src/pages/content/Content.tsx',
'src/pages/contact-import/ContactImport.tsx',
'src/pages/traffic-pool/TrafficPool.tsx',
'src/pages/workspace/Workspace.tsx'
];
// 更新规则
const updateRules = [
{
// 替换旧的header结构
pattern: /<header className="[^"]*fixed[^"]*">\s*<div className="[^"]*">\s*<div className="[^"]*">\s*<button[^>]*onClick=\{\(\) => navigate\(-1\)\}[^>]*>\s*<ChevronLeft[^>]*\/>\s*<\/button>\s*<h1[^>]*>([^<]*)<\/h1>\s*<\/div>\s*(?:<div[^>]*>([\s\S]*?)<\/div>)?\s*<\/div>\s*<\/header>/g,
replacement: (match, title, rightContent) => {
const rightContentStr = rightContent ? `\n rightContent={\n ${rightContent.trim()}\n }` : '';
return `<PageHeader\n title="${title.trim()}"\n defaultBackPath="/"${rightContentStr}\n />`;
}
},
{
// 替换简单的header结构
pattern: /<header className="[^"]*">\s*<div className="[^"]*">\s*<h1[^>]*>([^<]*)<\/h1>\s*<\/div>\s*<\/header>/g,
replacement: (match, title) => {
return `<PageHeader\n title="${title.trim()}"\n showBack={false}\n />`;
}
},
{
// 添加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 检查是否有编译错误');

147
Server/README_moments.md Normal file
View File

@@ -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直接存储在朋友圈主表中不再单独存储到资源表。

View File

@@ -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. 数据同步是增量的,会自动更新已存在的记录

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,199 @@
<?php
namespace app\cunkebao\controller;
use think\Db;
use think\Controller;
class StatsController extends Controller
{
const WEEK = [
0 => '周日',
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, '获取成功');
}
}

View File

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

View File

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

View File

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

88
Server/crontab_tasks.md Normal file
View File

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