From 1ff08a77ff24ef9a313fb508a79c75c308370f43 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Fri, 25 Jul 2025 16:25:00 +0800 Subject: [PATCH 01/93] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E6=8A=A5=E9=94=99=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/controller/PasswordLoginController.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Server/application/common/controller/PasswordLoginController.php b/Server/application/common/controller/PasswordLoginController.php index 74058fe4..6f3d62cd 100644 --- a/Server/application/common/controller/PasswordLoginController.php +++ b/Server/application/common/controller/PasswordLoginController.php @@ -21,7 +21,7 @@ class PasswordLoginController extends BaseController * @param int $typeId * @return UserModel */ - protected function getUserProfileWithAccountAndType(string $account, int $typeId): UserModel + protected function getUserProfileWithAccountAndType(string $account, int $typeId) { $user = UserModel::where( function ($query) use ($account) { @@ -34,7 +34,11 @@ class PasswordLoginController extends BaseController } )->find(); - return $user; + if(!empty($user)){ + return $user; + }else{ + return ''; + } } /** @@ -48,7 +52,6 @@ class PasswordLoginController extends BaseController protected function getUser(string $account, string $password, int $typeId): array { $user = $this->getUserProfileWithAccountAndType($account, $typeId); - if (!$user) { throw new \Exception('用户不存在或已禁用', 403); } From 3c0bcc9aa0e037aeb59cb7ef446f17ad6d7e17f9 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Fri, 25 Jul 2025 16:25:33 +0800 Subject: [PATCH 02/93] =?UTF-8?q?=E5=9C=BA=E6=99=AF=E8=8E=B7=E5=AE=A2?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/plan/PostCreateAddFriendPlanV1Controller.php | 6 ++++-- .../cunkebao/controller/plan/PosterWeChatMiniProgram.php | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Server/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php b/Server/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php index a2f9d3db..21befd30 100644 --- a/Server/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php +++ b/Server/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php @@ -62,7 +62,7 @@ class PostCreateAddFriendPlanV1Controller extends Controller return ResponseHelper::error('计划名称不能为空', 400); } - if (empty($params['scenario'])) { + if (empty($params['sceneId'])) { return ResponseHelper::error('场景ID不能为空', 400); } @@ -126,7 +126,9 @@ class PostCreateAddFriendPlanV1Controller extends Controller 'updateTime'=> time(), ]; - + + print_r($data); + exit; try { diff --git a/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php b/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php index da954377..13fe3e74 100644 --- a/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php +++ b/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php @@ -146,8 +146,8 @@ class PosterWeChatMiniProgram extends Controller $sceneConf = json_decode($task['sceneConf'], true); - if(isset($sceneConf['posters'][0]['preview'])) { - $posterUrl = $sceneConf['posters'][0]['preview']; + if(isset($sceneConf['posters'][0]['url'])) { + $posterUrl = $sceneConf['posters'][0]['url']; } else { $posterUrl = ''; } From a74f56506c2e878cb888fbc10dc17bfefc94af66 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Fri, 25 Jul 2025 18:11:39 +0800 Subject: [PATCH 03/93] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Server/application/common/controller/Attachment.php | 3 +-- .../controller/plan/PostCreateAddFriendPlanV1Controller.php | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Server/application/common/controller/Attachment.php b/Server/application/common/controller/Attachment.php index 062bde02..6787686b 100644 --- a/Server/application/common/controller/Attachment.php +++ b/Server/application/common/controller/Attachment.php @@ -28,8 +28,7 @@ class Attachment extends Controller $validate = \think\facade\Validate::rule([ 'file' => [ 'fileSize' => 10485760, // 10MB - 'fileExt' => 'jpg,jpeg,png,gif,doc,docx,pdf,zip,rar,mp4,mp3', - 'fileMime' => 'image/jpeg,image/png,image/gif,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/pdf,application/zip,application/x-rar-compressed,video/mp4,audio/mp3' + 'fileExt' => 'jpg,jpeg,png,gif,doc,docx,pdf,zip,rar,mp4,mp3,csv,xlsx,xls', ] ]); diff --git a/Server/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php b/Server/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php index 21befd30..e74b61bc 100644 --- a/Server/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php +++ b/Server/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php @@ -127,9 +127,6 @@ class PostCreateAddFriendPlanV1Controller extends Controller ]; - print_r($data); - exit; - try { Db::startTrans(); From 6a71516d6cfe85b7a615a94b835648f55b677c57 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Mon, 28 Jul 2025 17:38:45 +0800 Subject: [PATCH 04/93] =?UTF-8?q?=E7=99=BB=E5=BD=95=E8=BF=87=E6=9C=9F?= =?UTF-8?q?=E6=97=B6=E6=95=88=E6=94=B9=E4=B8=BA30=E5=A4=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/controller/PasswordLoginController.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Server/application/common/controller/PasswordLoginController.php b/Server/application/common/controller/PasswordLoginController.php index 6f3d62cd..f4d12cf0 100644 --- a/Server/application/common/controller/PasswordLoginController.php +++ b/Server/application/common/controller/PasswordLoginController.php @@ -6,6 +6,7 @@ use app\common\model\User as UserModel; use app\common\util\JwtUtil; use Exception; use library\ResponseHelper; +use think\Db; use think\Validate; /** @@ -111,12 +112,15 @@ class PasswordLoginController extends BaseController { // 获取用户信息 $member = $this->getUser($account, $password, $typeId); + $deviceTotal = Db::name('device')->where(['companyId' => $member['companyId'],'deleteTime' => 0])->count(); + + // 生成JWT令牌 - $token = JwtUtil::createToken($member, 86400); - $token_expired = time() + 86400; + $token = JwtUtil::createToken($member, 86400 * 30); + $token_expired = time() + 86400 * 30; - return compact('member', 'token', 'token_expired'); + return compact('member', 'token', 'token_expired','deviceTotal'); } /** From 842f04fc5b94ec5ebd694e4f88bb91f814b6f710 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Mon, 28 Jul 2025 17:40:23 +0800 Subject: [PATCH 05/93] =?UTF-8?q?=E5=A5=BD=E5=8F=8B=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Server/application/cunkebao/config/route.php | 5 +- .../PostCreateAddFriendPlanV1Controller.php | 2 +- .../controller/wechat/PostTransferFriends.php | 135 ++++++++++++++++++ 3 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 Server/application/cunkebao/controller/wechat/PostTransferFriends.php diff --git a/Server/application/cunkebao/config/route.php b/Server/application/cunkebao/config/route.php index c0a1ba26..76b32f5f 100644 --- a/Server/application/cunkebao/config/route.php +++ b/Server/application/cunkebao/config/route.php @@ -29,15 +29,14 @@ Route::group('v1/', function () { Route::get('getWechatInfo', 'app\cunkebao\controller\wechat\GetWechatController@getWechatInfo'); Route::get(':wechatId', 'app\cunkebao\controller\wechat\GetWechatProfileV1Controller@index'); - - + Route::post('transfer-friends', 'app\cunkebao\controller\wechat\PostTransferFriends@index'); // 微信好友转移 Route::get('count', 'app\cunkebao\controller\DeviceWechat@count'); Route::get('device-count', 'app\cunkebao\controller\DeviceWechat@deviceCount'); // 获取有登录微信的设备数量 Route::put('refresh', 'app\cunkebao\controller\DeviceWechat@refresh'); // 刷新设备微信状态 - Route::post('transfer-friends', 'app\cunkebao\controller\DeviceWechat@transferFriends'); // 微信好友转移 + }); // 获客场景相关 diff --git a/Server/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php b/Server/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php index e74b61bc..a6d0cec2 100644 --- a/Server/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php +++ b/Server/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php @@ -19,7 +19,7 @@ class PostCreateAddFriendPlanV1Controller extends Controller * * @return string */ - private function generateApiKey() + public function generateApiKey() { // 生成5组随机字符串,每组5个字符 $chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; diff --git a/Server/application/cunkebao/controller/wechat/PostTransferFriends.php b/Server/application/cunkebao/controller/wechat/PostTransferFriends.php new file mode 100644 index 00000000..2b1106a8 --- /dev/null +++ b/Server/application/cunkebao/controller/wechat/PostTransferFriends.php @@ -0,0 +1,135 @@ +request->param('wechatId', ''); + $inherit = $this->request->param('inherit', ''); + $devices = $this->request->param('devices', []); + $companyId = $this->getUserInfo('companyId'); + + if (empty($wechatId)){ + return ResponseHelper::error('迁移的微信不能为空'); + } + + if (empty($devices)){ + return ResponseHelper::error('迁移的设备不能为空'); + } + if (!is_array($devices)){ + return ResponseHelper::error('迁移的设备必须为数组'); + } + + $wechat = Db::name('wechat_customer')->alias('wc') + ->join('wechat_account wa', 'wc.wechatId = wa.wechatId') + ->where(['wc.wechatId' => $wechatId, 'wc.companyId' => $companyId]) + ->field('wa.*') + ->find(); + + if (empty($wechat)) { + return ResponseHelper::error('该微信不存在'); + } + + $devices = Db::name('device') + ->where(['companyId' => $companyId,'deleteTime' => 0]) + ->whereIn('id', $devices) + ->column('id'); + + + + + + try { + $sceneConf = [ + 'enabled' => true, + 'posters' => [ + 'id' => 'poster-3', + 'name' => '点击咨询', + 'src' => 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%92%A8%E8%AF%A2-FTiyAMAPop2g9LvjLOLDz0VwPg3KVu.gif' + ], + '$posters' => [ + 'id' => 'poster-3', + 'name' => '点击咨询', + 'src' => 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%92%A8%E8%AF%A2-FTiyAMAPop2g9LvjLOLDz0VwPg3KVu.gif' + ] + ]; + $reqConf = [ + 'device' => $devices, + 'startTime' => '09:00', + 'endTime' => '18:00', + 'remarkType' => 'phone', + 'addFriendInterval' => 60, + 'greeting' => '您好,我是'. $wechat['nickname'] .'的辅助客服,请通过' + ]; + + $createAddFriendPlan = new PostCreateAddFriendPlanV1Controller(); + + $taskId = Db::name('customer_acquisition_task')->insertGetId([ + 'name' => '迁移好友('. $wechat['nickname'] .')', + 'sceneId' => 1, + 'sceneConf' => json_encode($sceneConf), + 'reqConf' => json_encode($reqConf), + 'tagConf' => json_encode([]), + 'userId' => $this->getUserInfo('id'), + 'companyId' => $companyId, + 'status' => 0, + 'createTime' => time(), + 'apiKey' => $createAddFriendPlan->generateApiKey(), + ]); + + $friends = Db::table('s2_wechat_friend') + ->where(['ownerWechatId' => $wechatId]) + ->group('wechatId') + ->order('id DESC') + ->column('id', 'wechatId,alias,phone,labels,conRemark'); + + // 1000条为一组进行批量处理 + $batchSize = 1000; + $totalRows = count($friends); + + for ($i = 0; $i < $totalRows; $i += $batchSize) { + $batchRows = array_slice($friends, $i, $batchSize); + if (!empty($batchRows)) { + $newData = []; + foreach ($batchRows as $row) { + if (!empty($row['phone'])) { + $phone = $row['phone']; + } elseif (!empty($row['alias'])) { + $phone = $row['alias']; + } else { + $phone = $row['wechatId']; + } + + $tags = !empty($row['labels']) ? json_decode($row['labels'], true) : []; + $newData[] = [ + 'task_id' => $taskId, + 'name' => '', + 'source' => '迁移好友('. $wechat['nickname'] .')', + 'phone' => $phone, + 'remark' => !empty($inherit) ? $row['conRemark'] : '', + 'tags' => !empty($inherit) ? json_encode($tags, JSON_UNESCAPED_UNICODE) : json_encode([]), + 'siteTags' => json_encode([]), + 'status' => 0, + 'createTime' => time(), + ]; + } + Db::name('task_customer')->insertAll($newData); + } + } + return ResponseHelper::success('好友迁移创建成功' ); + } catch (\Exception $e) { + // 回滚事务 + Db::rollback(); + return ResponseHelper::error('好友迁移创建失败:' . $e->getMessage()); + } + + + } +} \ No newline at end of file From c09af61051ea8e541e1b17df66a2dfd22a8ba2dc Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Mon, 28 Jul 2025 17:40:50 +0800 Subject: [PATCH 06/93] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=A5=BD=E5=8F=8B?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/plan/PostExternalApiV1Controller.php | 1 + .../cunkebao/controller/plan/PosterWeChatMiniProgram.php | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Server/application/cunkebao/controller/plan/PostExternalApiV1Controller.php b/Server/application/cunkebao/controller/plan/PostExternalApiV1Controller.php index 10c91780..042f92c8 100644 --- a/Server/application/cunkebao/controller/plan/PostExternalApiV1Controller.php +++ b/Server/application/cunkebao/controller/plan/PostExternalApiV1Controller.php @@ -120,6 +120,7 @@ class PostExternalApiV1Controller extends Controller 'remark' => !empty($params['remark']) ? $params['remark'] : '', 'tags' => json_encode($tags,256), 'siteTags' => json_encode($siteTags,256), + 'createTime' => time(), ]); return json([ diff --git a/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php b/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php index 13fe3e74..7539e66e 100644 --- a/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php +++ b/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php @@ -102,7 +102,11 @@ class PosterWeChatMiniProgram extends Controller Db::name('task_customer')->insert([ 'task_id' => $taskId, // 'identifier' => $result['phone_info']['phoneNumber'], - 'phone' => $result['phone_info']['phoneNumber'] + 'phone' => $result['phone_info']['phoneNumber'], + 'source' => $task['name'], + 'createTime' => time(), + 'tags' => json_encode([]), + 'siteTags' => json_encode([]), ]); } // return $result['phone_info']['phoneNumber']; From d78ff1e37618768d5dd315671b11a03fd1565b0e Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Mon, 28 Jul 2025 17:41:13 +0800 Subject: [PATCH 07/93] =?UTF-8?q?=E5=8E=BB=E9=99=A4=E6=97=A0=E5=85=B3?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cunkebao/controller/TrafficController.php | 121 ------------------ 1 file changed, 121 deletions(-) delete mode 100644 Server/application/cunkebao/controller/TrafficController.php diff --git a/Server/application/cunkebao/controller/TrafficController.php b/Server/application/cunkebao/controller/TrafficController.php deleted file mode 100644 index c00154de..00000000 --- a/Server/application/cunkebao/controller/TrafficController.php +++ /dev/null @@ -1,121 +0,0 @@ -request->param('page',1); - $pageSize = $this->request->param('pageSize',10); - $device = $this->request->param('device',''); - $packageId = $this->request->param('packageId',''); // 流量池id - $userValue = $this->request->param('userValue',''); // 1高价值客户 2中价值客户 3低价值客户 - $addStatus = $this->request->param('addStatus',''); // 1待添加 2已添加 3添加失败 4重复用户 - $keyword = $this->request->param('keyword',''); - - $companyId = $this->request->userInfo['companyId']; - - // 1 文字 3图片 47动态图片 34语言 43视频 42名片 40/20链接 49文件 419430449转账 436207665红包 - - $where = []; - - // 添加筛选条件 - if (!empty($device)) { - $where['d.id'] = $device; - } - - if (!empty($packageId)) { - $where['tp.id'] = $packageId; - } - - if (!empty($userValue)) { - $where['tp.userValue'] = $userValue; - } - - if (!empty($addStatus)) { - $where['tp.addStatus'] = $addStatus; - } - - - // 构建查询 - 通过traffic_pool的identifier关联wechat_account的wechatId、alias或phone - $query = Db::name('traffic_pool')->alias('tp') - ->join('wechat_account wa', 'wa.wechatId = tp.wechatId', 'LEFT') - ->field('tp.id, tp.identifier,tp.createTime, tp.updateTime, - wa.wechatId, wa.alias, wa.phone, wa.nickname, wa.avatar') - ->where($where); - - // 关键词搜索 - 支持通过wechat_friendship的identifier关联wechat_account的wechatId、alias或phone - if (!empty($keyword)) { - $query->where(function($q) use ($keyword) { - $q->where('tp.identifier', 'like', '%' . $keyword . '%') - ->whereOr('wa.wechatId', 'like', '%' . $keyword . '%') - ->whereOr('wa.alias', 'like', '%' . $keyword . '%') - ->whereOr('wa.phone', 'like', '%' . $keyword . '%') - ->whereOr('wa.nickname', 'like', '%' . $keyword . '%'); - }); - } - - // 获取总数 - $total = $query->count(); - - - // 分页查询 - $list = $query->order('tp.createTime desc') - ->group('tp.identifier') - ->order('tp.id desc') - ->page($page, $pageSize) - ->select(); - - - - return json([ - 'code' => 200, - 'msg' => '获取成功', - 'data' => [ - 'list' => $list, - 'total' => $total, - ] - ]); - } - - - /** - * 用户旅程 - * @return false|string - * @throws \think\db\exception\DataNotFoundException - * @throws \think\db\exception\ModelNotFoundException - * @throws \think\exception\DbException - */ - public function getUserJourney() - { - $page = $this->request->param('page',1); - $pageSize = $this->request->param('pageSize',10); - $userId = $this->request->param('userId',''); - if(empty($userId)){ - return json_encode(['code' => 500, 'msg' => '用户id不能为空']); - } - - $query = Db::name('user_portrait') - ->field('id,type,trafficPoolId,remark,count,createTime,updateTime') - ->where(['trafficPoolId' => $userId]); - - $total = $query->count(); - - $list = $query->order('createTime desc') - ->page($page,$pageSize) - ->select(); - - - foreach ($list as $k=>$v){ - $list[$k]['createTime'] = date('Y-m-d H:i:s',$v['createTime']); - $list[$k]['updateTime'] = date('Y-m-d H:i:s',$v['updateTime']); - } - return json_encode(['code' => 200,'data'=>['list' => $list,'total'=>$total],'获取成功']); - } - - -} \ No newline at end of file From 28778a95664b04ce8a3bcbdfd92abb77e4376dda Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Tue, 29 Jul 2025 09:43:32 +0800 Subject: [PATCH 08/93] =?UTF-8?q?=E6=9C=8B=E5=8F=8B=E5=9C=88=E9=87=87?= =?UTF-8?q?=E9=9B=86=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/controller/WebSocketController.php | 2 + .../controller/WebSocketControllerCopy.php | 2 + Server/application/cunkebao/config/route.php | 2 +- .../controller/ContentCollectController.php | 30 - .../controller/ContentLibraryController.php | 562 ++++++++++-------- 5 files changed, 306 insertions(+), 292 deletions(-) delete mode 100644 Server/application/cunkebao/controller/ContentCollectController.php diff --git a/Server/application/api/controller/WebSocketController.php b/Server/application/api/controller/WebSocketController.php index 8400183d..27fb9c18 100644 --- a/Server/application/api/controller/WebSocketController.php +++ b/Server/application/api/controller/WebSocketController.php @@ -534,6 +534,8 @@ class WebSocketController extends BaseController 'userName' => $momentEntity['userName'] ?? '', 'snsId' => $moment['snsId'] ?? '', 'type' => $moment['type'] ?? 0, + 'title' => $moment['title'] ?? '', + 'coverImage' => $moment['coverImage'] ?? '', 'update_time' => time() ]; if (!empty($momentId)) { diff --git a/Server/application/api/controller/WebSocketControllerCopy.php b/Server/application/api/controller/WebSocketControllerCopy.php index 60bb2fbd..33bf77f3 100644 --- a/Server/application/api/controller/WebSocketControllerCopy.php +++ b/Server/application/api/controller/WebSocketControllerCopy.php @@ -341,6 +341,8 @@ class WebSocketControllerCopy extends BaseController 'userName' => $momentEntity['userName'] ?? '', 'snsId' => $moment['snsId'] ?? '', 'type' => $moment['type'] ?? 0, + 'title' => $moment['title'] ?? '', + 'coverImage' => $moment['coverImage'] ?? '', 'update_time' => time() ]; diff --git a/Server/application/cunkebao/config/route.php b/Server/application/cunkebao/config/route.php index 76b32f5f..c0dfc22e 100644 --- a/Server/application/cunkebao/config/route.php +++ b/Server/application/cunkebao/config/route.php @@ -28,7 +28,6 @@ Route::group('v1/', function () { Route::get(':id/friends', 'app\cunkebao\controller\wechat\GetWechatOnDeviceFriendsV1Controller@index'); Route::get('getWechatInfo', 'app\cunkebao\controller\wechat\GetWechatController@getWechatInfo'); Route::get(':wechatId', 'app\cunkebao\controller\wechat\GetWechatProfileV1Controller@index'); - Route::post('transfer-friends', 'app\cunkebao\controller\wechat\PostTransferFriends@index'); // 微信好友转移 @@ -100,6 +99,7 @@ Route::group('v1/', function () { Route::delete('delete-item', 'app\cunkebao\controller\ContentLibraryController@deleteItem'); // 删除内容库素材 Route::get('get-item-detail', 'app\cunkebao\controller\ContentLibraryController@getItemDetail'); // 获取内容库素材详情 Route::post('update-item', 'app\cunkebao\controller\ContentLibraryController@updateItem'); // 更新内容库素材 + Route::get('aiEditContent', 'app\cunkebao\controller\ContentLibraryController@aiEditContent'); }); // 好友相关 diff --git a/Server/application/cunkebao/controller/ContentCollectController.php b/Server/application/cunkebao/controller/ContentCollectController.php deleted file mode 100644 index 171c57e1..00000000 --- a/Server/application/cunkebao/controller/ContentCollectController.php +++ /dev/null @@ -1,30 +0,0 @@ - input('libraryId/d', 0), // 0表示采集所有内容库 - 'timestamp' => time() - ]; - - Queue::push(ContentCollectJob::class, $data, 'content_collect'); - - return json(['code' => 200, 'msg' => '采集任务已加入队列']); - } catch (\Exception $e) { - return json(['code' => 500, 'msg' => '添加采集任务失败:' . $e->getMessage()]); - } - } -} \ No newline at end of file diff --git a/Server/application/cunkebao/controller/ContentLibraryController.php b/Server/application/cunkebao/controller/ContentLibraryController.php index 86cbf7de..c59d7d0b 100644 --- a/Server/application/cunkebao/controller/ContentLibraryController.php +++ b/Server/application/cunkebao/controller/ContentLibraryController.php @@ -11,6 +11,7 @@ use app\api\controller\WebSocketController; use think\facade\Cache; use think\facade\Env; use app\api\controller\AutomaticAssign; +use think\facade\Request; /** * 内容库控制器 @@ -20,7 +21,7 @@ class ContentLibraryController extends Controller /************************************ * 内容库基础管理功能 ************************************/ - + /** * 创建内容库 * @return \think\response\Json @@ -40,7 +41,7 @@ class ContentLibraryController extends Controller } // 检查内容库名称是否已存在 - $exists = ContentLibrary::where(['name' => $param['name'],'userId' => $this->request->userInfo['id'],'isDel' => 0])->find(); + $exists = ContentLibrary::where(['name' => $param['name'], 'userId' => $this->request->userInfo['id'], 'isDel' => 0])->find(); if ($exists) { return json(['code' => 400, 'msg' => '内容库名称已存在']); } @@ -48,8 +49,8 @@ class ContentLibraryController extends Controller Db::startTrans(); try { - $keywordInclude = isset($param['keywordInclude']) ? json_encode($param['keywordInclude'],256) : json_encode([]); - $keywordExclude = isset($param['keywordExclude']) ? json_encode($param['keywordExclude'],256) : json_encode([]); + $keywordInclude = isset($param['keywordInclude']) ? json_encode($param['keywordInclude'], 256) : json_encode([]); + $keywordExclude = isset($param['keywordExclude']) ? json_encode($param['keywordExclude'], 256) : json_encode([]); $sourceType = isset($param['sourceType']) ? $param['sourceType'] : 1; @@ -59,7 +60,7 @@ class ContentLibraryController extends Controller // 数据来源配置 'sourceFriends' => $sourceType == 1 ? json_encode($param['friends']) : json_encode([]), // 选择的微信好友 'sourceGroups' => $sourceType == 2 ? json_encode($param['groups']) : json_encode([]), // 选择的微信群 - 'groupMembers' => $sourceType == 2 ? json_encode($param['groupMembers']) : json_encode([]), // 群组成员 + 'groupMembers' => $sourceType == 2 ? json_encode($param['groupMembers']) : json_encode([]), // 群组成员 // 关键词配置 'keywordInclude' => $keywordInclude, // 包含的关键词 'keywordExclude' => $keywordExclude, // 排除的关键词 @@ -96,7 +97,7 @@ class ContentLibraryController extends Controller return json(['code' => 500, 'msg' => '创建失败:' . $e->getMessage()]); } } - + /** * 获取内容库列表 * @return \think\response\Json @@ -112,22 +113,21 @@ class ContentLibraryController extends Controller ['userId', '=', $this->request->userInfo['id']], ['isDel', '=', 0] // 只查询未删除的记录 ]; - + // 添加名称模糊搜索 if ($keyword !== '') { $where[] = ['name', 'like', '%' . $keyword . '%']; } - // 添加名称模糊搜索 + // 添加名称模糊搜索 if (!empty($sourceType)) { $where[] = ['sourceType', '=', $sourceType]; } - $list = ContentLibrary::where($where) ->field('id,name,sourceFriends,sourceGroups,keywordInclude,keywordExclude,aiEnabled,aiPrompt,timeEnabled,timeStart,timeEnd,status,sourceType,userId,createTime,updateTime') - ->with(['user' => function($query) { + ->with(['user' => function ($query) { $query->field('id,username'); }]) ->order('id', 'desc') @@ -162,7 +162,7 @@ class ContentLibraryController extends Controller $item['selectedFriends'] = $friendsInfo; } - + if (!empty($item['sourceGroups']) && $item['sourceType'] == 2) { $groupIds = $item['sourceGroups']; $groupsInfo = []; @@ -212,8 +212,8 @@ class ContentLibraryController extends Controller ['userId', '=', $this->request->userInfo['id']], ['isDel', '=', 0] // 只查询未删除的记录 ]) - ->field('id,name,sourceType,sourceFriends,sourceGroups,keywordInclude,keywordExclude,aiEnabled,aiPrompt,timeEnabled,timeStart,timeEnd,status,userId,companyId,createTime,updateTime,groupMembers') - ->find(); + ->field('id,name,sourceType,sourceFriends,sourceGroups,keywordInclude,keywordExclude,aiEnabled,aiPrompt,timeEnabled,timeStart,timeEnd,status,userId,companyId,createTime,updateTime,groupMembers') + ->find(); if (empty($library)) { return json(['code' => 404, 'msg' => '内容库不存在']); @@ -226,53 +226,53 @@ class ContentLibraryController extends Controller $library['keywordExclude'] = json_decode($library['keywordExclude'] ?: '[]', true); $library['groupMembers'] = json_decode($library['groupMembers'] ?: '[]', true); - // 将时间戳转换为日期格式(精确到日) - if (!empty($library['timeStart'])) { - $library['timeStart'] = date('Y-m-d', $library['timeStart']); - } - if (!empty($library['timeEnd'])) { - $library['timeEnd'] = date('Y-m-d', $library['timeEnd']); - } - - // 获取好友详细信息 - if (!empty($library['sourceFriends'])) { - $friendIds = $library['sourceFriends']; - $friendsInfo = []; - - if (!empty($friendIds)) { - // 查询好友信息,使用wechat_friendship表 - $friendsInfo = Db::name('wechat_friendship')->alias('wf') - ->field('wf.id,wf.wechatId, wa.nickname, wa.avatar') - ->join('wechat_account wa', 'wf.wechatId = wa.wechatId') - ->whereIn('wf.id', $friendIds) - ->select(); + // 将时间戳转换为日期格式(精确到日) + if (!empty($library['timeStart'])) { + $library['timeStart'] = date('Y-m-d', $library['timeStart']); } - - // 将好友信息添加到返回数据中 - $library['selectedFriends'] = $friendsInfo; - } - - // 获取群组详细信息 - if (!empty($library['sourceGroups'])) { - $groupIds = $library['sourceGroups']; - $groupsInfo = []; - - if (!empty($groupIds)) { - // 查询群组信息 - $groupsInfo = Db::name('wechat_group')->alias('g') - ->field('g.id, g.chatroomId, g.name, g.avatar, g.ownerWechatId,wa.nickname as ownerNickname,wa.avatar as ownerAvatar,wa.alias as ownerAlias') - ->join('wechat_account wa', 'g.ownerWechatId = wa.wechatId') - ->whereIn('g.id', $groupIds) - ->select(); + if (!empty($library['timeEnd'])) { + $library['timeEnd'] = date('Y-m-d', $library['timeEnd']); + } + + // 获取好友详细信息 + if (!empty($library['sourceFriends'])) { + $friendIds = $library['sourceFriends']; + $friendsInfo = []; + + if (!empty($friendIds)) { + // 查询好友信息,使用wechat_friendship表 + $friendsInfo = Db::name('wechat_friendship')->alias('wf') + ->field('wf.id,wf.wechatId, wa.nickname, wa.avatar') + ->join('wechat_account wa', 'wf.wechatId = wa.wechatId') + ->whereIn('wf.id', $friendIds) + ->select(); + } + + // 将好友信息添加到返回数据中 + $library['selectedFriends'] = $friendsInfo; + } + + // 获取群组详细信息 + if (!empty($library['sourceGroups'])) { + $groupIds = $library['sourceGroups']; + $groupsInfo = []; + + if (!empty($groupIds)) { + // 查询群组信息 + $groupsInfo = Db::name('wechat_group')->alias('g') + ->field('g.id, g.chatroomId, g.name, g.avatar, g.ownerWechatId,wa.nickname as ownerNickname,wa.avatar as ownerAvatar,wa.alias as ownerAlias') + ->join('wechat_account wa', 'g.ownerWechatId = wa.wechatId') + ->whereIn('g.id', $groupIds) + ->select(); + } + + // 将群组信息添加到返回数据中 + $library['selectedGroups'] = $groupsInfo; } - - // 将群组信息添加到返回数据中 - $library['selectedGroups'] = $groupsInfo; - } return json([ - 'code' => 200, - 'msg' => '获取成功', + 'code' => 200, + 'msg' => '获取成功', 'data' => $library ]); } @@ -312,8 +312,8 @@ class ContentLibraryController extends Controller Db::startTrans(); try { - $keywordInclude = isset($param['keywordInclude']) ? json_encode($param['keywordInclude'],256) : json_encode([]); - $keywordExclude = isset($param['keywordExclude']) ? json_encode($param['keywordExclude'],256) : json_encode([]); + $keywordInclude = isset($param['keywordInclude']) ? json_encode($param['keywordInclude'], 256) : json_encode([]); + $keywordExclude = isset($param['keywordExclude']) ? json_encode($param['keywordExclude'], 256) : json_encode([]); // 更新内容库基本信息 @@ -429,7 +429,7 @@ class ContentLibraryController extends Controller // 处理资源URL $item['resUrls'] = json_decode($item['resUrls'] ?: '[]', true); $item['urls'] = json_decode($item['urls'] ?: '[]', true); - + // 格式化时间 //$item['createTime'] = date('Y-m-d H:i:s', $item['createTime']); if ($item['createMomentTime']) { @@ -448,8 +448,8 @@ class ContentLibraryController extends Controller ->field('wa.nickname, wa.avatar') ->find(); $item['senderNickname'] = !empty($friendInfo['nickname']) ? $friendInfo['nickname'] : ''; - $item['senderAvatar'] = !empty( $friendInfo['avatar']) ? $friendInfo['avatar'] : ''; - }else if ($item['type'] == 'group_message' && !empty($item['wechatChatroomId'])) { + $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']) @@ -500,14 +500,14 @@ class ContentLibraryController extends Controller if (empty($param['content'])) { return json(['code' => 400, 'msg' => '内容数据不能为空']); } - + // 当类型为群消息时,限制图片只能上传一张 if ($param['type'] == 'group_message') { $images = isset($param['images']) ? $param['images'] : []; if (is_string($images)) { $images = json_decode($images, true); } - + if (count($images) > 1) { return json(['code' => 400, 'msg' => '群消息类型只能上传一张图片']); } @@ -533,8 +533,8 @@ class ContentLibraryController extends Controller $item->content = $param['content']; $item->comment = $param['comment'] ?? ''; $item->sendTime = strtotime($param['sendTime']); - $item->resUrls = json_encode($param['resUrls'] ?? [],256); - $item->urls = json_encode($param['urls'] ?? [],256); + $item->resUrls = json_encode($param['resUrls'] ?? [], 256); + $item->urls = json_encode($param['urls'] ?? [], 256); $item->senderNickname = '系统创建'; $item->coverImage = $param['coverImage'] ?? ''; $item->save(); @@ -615,7 +615,7 @@ class ContentLibraryController extends Controller // 处理资源URL $item['resUrls'] = json_decode($item['resUrls'] ?: '[]', true); $item['urls'] = json_decode($item['urls'] ?: '[]', true); - + // 添加内容类型的文字描述 $contentTypeMap = [ 0 => '未知', @@ -627,7 +627,7 @@ class ContentLibraryController extends Controller 6 => '图文' ]; $item['contentTypeName'] = $contentTypeMap[$item['contentType'] ?? 0] ?? '未知'; - + // 格式化时间 if ($item['createMomentTime']) { $item['createMomentTimeFormatted'] = date('Y-m-d H:i:s', $item['createMomentTime']); @@ -660,8 +660,8 @@ class ContentLibraryController extends Controller } return json([ - 'code' => 200, - 'msg' => '获取成功', + 'code' => 200, + 'msg' => '获取成功', 'data' => $item ]); } @@ -688,7 +688,7 @@ class ContentLibraryController extends Controller $item = ContentItem::where([ ['id', '=', $param['id']], ['isDel', '=', 0] - ]) ->find(); + ])->find(); if (!$item) { return json(['code' => 404, 'msg' => '内容项目不存在或无权限操作']); @@ -699,7 +699,7 @@ class ContentLibraryController extends Controller $item->title = $param['title'] ?? $item->title; $item->content = $param['content'] ?? $item->content; $item->comment = $param['comment'] ?? $item->comment; - + // 处理发送时间 if (!empty($param['sendTime'])) { $item->sendTime = strtotime($param['sendTime']); @@ -714,7 +714,7 @@ class ContentLibraryController extends Controller if (isset($param['resUrls'])) { $resUrls = is_string($param['resUrls']) ? json_decode($param['resUrls'], true) : $param['resUrls']; $item->resUrls = json_encode($resUrls, JSON_UNESCAPED_UNICODE); - + // 设置封面图片 if (!empty($resUrls[0])) { $item->coverImage = $resUrls[0]; @@ -750,6 +750,44 @@ class ContentLibraryController extends Controller } + public function aiEditContent() + { + + $id = Request::param('id', ''); + + // 简单验证 + if (empty($id)) { + return json(['code' => 400, 'msg' => '参数错误']); + } + + // 查询内容项目是否存在并检查权限 + $item = ContentItem::where([ + ['id', '=', $id], + ['isDel', '=', 0] + ])->find(); + + if (empty($item)) { + return json(['code' => 404, 'msg' => '内容项目不存在或无权限操作']); + } + + if (empty($item['content'])) { + return json(['code' => 404, 'msg' => '内容不能为空']); + } + + try { + $contentAi = $this->aiRewrite(['aiEnabled' => true], $item['content']); + if (!empty($contentAi)) { + ContentItem::where(['id' => $item['id']])->update(['contentAi' => $contentAi, 'updateTime' => time()]); + return json(['code' => 200, 'msg' => 'ai编写成功', 'data' => ['editAfter' => $contentAi, 'editFront' => $item['content']]]); + } else { + return json(['code' => 500, 'msg' => 'ai编写失败']); + } + } catch (\Exception $e) { + return json(['code' => 500, 'msg' => 'ai编写失败:' . $e->getMessage()]); + } + } + + /************************************ * 数据采集相关功能 ************************************/ @@ -784,7 +822,7 @@ class ContentLibraryController extends Controller ['isDel', '=', 0], // 未删除 ['status', '=', 1], // 已开启 ]; - + // 查询符合条件的内容库 $libraries = ContentLibrary::where($where) ->field('id,name,sourceType,sourceFriends,sourceGroups,keywordInclude,keywordExclude,aiEnabled,aiPrompt,timeEnabled,timeStart,timeEnd,status,userId,companyId,createTime,updateTime,groupMembers') @@ -794,11 +832,11 @@ class ContentLibraryController extends Controller if (empty($libraries)) { return json(['code' => 200, 'msg' => '没有可用的内容库配置']); } - + $successCount = 0; $failCount = 0; $results = []; - + // 处理每个内容库的采集任务 foreach ($libraries as $library) { try { @@ -808,7 +846,7 @@ class ContentLibraryController extends Controller $library['keywordInclude'] = json_decode($library['keywordInclude'] ?: '[]', true); $library['keywordExclude'] = json_decode($library['keywordExclude'] ?: '[]', true); $library['groupMembers'] = json_decode($library['groupMembers'] ?: '[]', true); - + // 根据数据来源类型执行不同的采集逻辑 $collectResult = []; switch ($library['sourceType']) { @@ -817,26 +855,26 @@ class ContentLibraryController extends Controller $collectResult = $this->collectFromFriends($library); } break; - + case 2: // 群类型 if (!empty($library['sourceGroups'])) { $collectResult = $this->collectFromGroups($library); } break; - + default: $collectResult = [ 'status' => 'failed', 'message' => '不支持的数据来源类型' ]; } - + if ($collectResult['status'] == 'success') { $successCount++; } else { $failCount++; } - + $results[] = [ 'library_id' => $library['id'], 'library_name' => $library['name'], @@ -844,7 +882,7 @@ class ContentLibraryController extends Controller 'message' => $collectResult['message'] ?? '', 'data' => $collectResult['data'] ?? [] ]; - + } catch (\Exception $e) { $failCount++; $results[] = [ @@ -855,7 +893,7 @@ class ContentLibraryController extends Controller ]; } } - + // 返回采集结果 return json_encode([ 'code' => 200, @@ -868,7 +906,7 @@ class ContentLibraryController extends Controller ] ]); } - + /** * 从好友采集朋友圈内容 * @param array $library 内容库配置 @@ -889,7 +927,7 @@ class ContentLibraryController extends Controller $username = Env::get('api.username2', ''); $password = Env::get('api.password2', ''); if (!empty($username) || !empty($password)) { - $toAccountId = Db::name('users')->where('account',$username)->value('s2_accountId'); + $toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId'); } @@ -905,7 +943,7 @@ class ContentLibraryController extends Controller 'message' => '未找到有效的好友信息' ]; } - + // 从朋友圈采集内容 $collectedData = []; $totalMomentsCount = 0; @@ -915,15 +953,15 @@ class ContentLibraryController extends Controller if (!empty($username) && !empty($password)) { //执行切换好友命令 $automaticAssign = new AutomaticAssign(); - $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['id'],'toAccountId' => $toAccountId],true); + $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['id'], 'toAccountId' => $toAccountId], true); //存入缓存 $friendData['friendId'] = $friend['id']; artificialAllotWechatFriend($friendData); //执行采集朋友圈命令 - $webSocket = new WebSocketController(['userName' => $username,'password' => $password,'accountId' => $toAccountId]); - $webSocket->getMoments(['wechatFriendId' => $friend['id'],'wechatAccountId' => $friend['wechatAccountId']]); + $webSocket = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]); + $webSocket->getMoments(['wechatFriendId' => $friend['id'], 'wechatAccountId' => $friend['wechatAccountId']]); //采集完毕切换 - $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['id'],'toAccountId' => $friend['accountId']],true); + $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['id'], 'toAccountId' => $friend['accountId']], true); } @@ -935,33 +973,32 @@ class ContentLibraryController extends Controller ]) ->order('createTime', 'desc') //->where('create_time', '>=', time() - 86400) - ->page(1,20) + ->page(1, 20) ->select(); - if (empty($moments)) { continue; } - + // 获取好友详细信息 $friendInfo = Db::table('s2_wechat_friend') ->where('wechatId', $friend['wechatId']) ->field('nickname, avatar') ->find(); - + $nickname = $friendInfo['nickname'] ?? '未知好友'; $friendMomentsCount = 0; - + // 处理每条朋友圈数据 foreach ($moments as $moment) { // 处理关键词过滤 $content = $moment['content'] ?? ''; - + // 如果启用了关键词过滤 $includeKeywords = $library['keywordInclude']; $excludeKeywords = $library['keywordExclude']; - + // 检查是否包含必须关键词 $includeMatch = empty($includeKeywords); if (!empty($includeKeywords)) { @@ -972,12 +1009,12 @@ class ContentLibraryController extends Controller } } } - + // 如果不满足包含条件,跳过 if (!$includeMatch) { continue; } - + // 检查是否包含排除关键词 $excludeMatch = false; if (!empty($excludeKeywords)) { @@ -988,28 +1025,28 @@ class ContentLibraryController extends Controller } } } - + // 如果满足排除条件,跳过 if ($excludeMatch) { continue; } // 如果启用了AI处理 - if (!empty($library['aiEnabled']) && !empty($content)) { - $contentAi = $this->aiRewrite($library,$content); - if (!empty($content)){ - $moment['contentAi'] = $contentAi; - }else{ - $moment['contentAi'] = ''; - } + if (!empty($library['aiEnabled']) && !empty($content)) { + $contentAi = $this->aiRewrite($library, $content); + if (!empty($content)) { + $moment['contentAi'] = $contentAi; + } else { + $moment['contentAi'] = ''; + } } // 保存到内容库的content_item表 $this->saveMomentToContentItem($moment, $library['id'], $friend, $nickname); - + $friendMomentsCount++; } - + if ($friendMomentsCount > 0) { // 记录采集结果 $collectedData[$friend['wechatId']] = [ @@ -1017,20 +1054,19 @@ class ContentLibraryController extends Controller 'nickname' => $nickname, 'count' => $friendMomentsCount ]; - + $totalMomentsCount += $friendMomentsCount; } } - - + if (empty($collectedData)) { return [ 'status' => 'warning', 'message' => '未采集到任何朋友圈内容' ]; } - + return [ 'status' => 'success', 'message' => '成功采集到' . count($collectedData) . '位好友的' . $totalMomentsCount . '条朋友圈内容', @@ -1040,7 +1076,7 @@ class ContentLibraryController extends Controller 'details' => $collectedData ] ]; - + } catch (\Exception $e) { return [ 'status' => 'error', @@ -1048,7 +1084,7 @@ class ContentLibraryController extends Controller ]; } } - + /** * 从群组采集消息内容 * @param array $library 内容库配置 @@ -1063,7 +1099,7 @@ class ContentLibraryController extends Controller 'message' => '没有指定要采集的群组' ]; } - + try { // 查询群组信息 $groups = Db::name('wechat_group')->alias('g') @@ -1071,14 +1107,14 @@ class ContentLibraryController extends Controller ->whereIn('g.id', $groupIds) ->where('g.deleteTime', 0) ->select(); - + if (empty($groups)) { return [ 'status' => 'failed', 'message' => '未找到有效的群组信息' ]; } - + // 获取群成员信息 $groupMembers = $library['groupMembers']; if (empty($groupMembers)) { @@ -1088,23 +1124,23 @@ class ContentLibraryController extends Controller 'message' => '未找到有效的群成员信息' ]; } - + // 从群组采集内容 $collectedData = []; $totalMessagesCount = 0; $chatroomIds = array_column($groups, 'id'); - + // 获取群消息 - 支持时间范围过滤 $messageWhere = [ ['wechatChatroomId', 'in', $chatroomIds], ['type', '=', 2] ]; - + // 如果启用时间限制 if ($library['timeEnabled'] && $library['timeStart'] > 0 && $library['timeEnd'] > 0) { $messageWhere[] = ['createTime', 'between', [$library['timeStart'], $library['timeEnd']]]; } - + // 查询群消息 $groupMessages = Db::table('s2_wechat_message') ->where($messageWhere) @@ -1127,14 +1163,14 @@ class ContentLibraryController extends Controller 'messages' => [] ]; } - + // 处理消息内容 $content = $message['content'] ?? ''; - + // 如果启用了关键词过滤 $includeKeywords = $library['keywordInclude']; $excludeKeywords = $library['keywordExclude']; - + // 检查是否包含必须关键词 $includeMatch = empty($includeKeywords); @@ -1146,12 +1182,12 @@ class ContentLibraryController extends Controller } } } - + // 如果不满足包含条件,跳过 if (!$includeMatch) { continue; } - + // 检查是否包含排除关键词 $excludeMatch = false; if (!empty($excludeKeywords)) { @@ -1162,12 +1198,12 @@ class ContentLibraryController extends Controller } } } - + // 如果满足排除条件,跳过 if ($excludeMatch) { continue; } - + // 找到对应的群组信息 $groupInfo = null; foreach ($groups as $group) { @@ -1176,18 +1212,18 @@ class ContentLibraryController extends Controller break; } } - + if (!$groupInfo) { continue; } // 如果启用了AI处理 - if (!empty($library['aiEnabled']) && !empty($content)) { - $contentAi = $this->aiRewrite($library,$content); - if (!empty($content)){ + if (!empty($library['aiEnabled']) && !empty($content)) { + $contentAi = $this->aiRewrite($library, $content); + if (!empty($content)) { $moment['contentAi'] = $contentAi; - }else{ + } else { $moment['contentAi'] = ''; } } @@ -1195,7 +1231,7 @@ class ContentLibraryController extends Controller // 保存消息到内容库 $this->saveMessageToContentItem($message, $library['id'], $groupInfo); - + // 累计计数 $groupedMessages[$chatroomId]['count']++; $groupedMessages[$chatroomId]['messages'][] = [ @@ -1204,10 +1240,10 @@ class ContentLibraryController extends Controller 'sender' => $message['senderNickname'], 'time' => date('Y-m-d H:i:s', $message['createTime']) ]; - + $totalMessagesCount++; } - + // 构建结果数据 foreach ($groups as $group) { $chatroomId = $group['chatroomId']; @@ -1220,14 +1256,14 @@ class ContentLibraryController extends Controller ]; } } - + if (empty($collectedData)) { return [ 'status' => 'warning', 'message' => '未采集到符合条件的群消息内容' ]; } - + return [ 'status' => 'success', 'message' => '成功采集到' . count($collectedData) . '个群的' . $totalMessagesCount . '条消息', @@ -1237,7 +1273,7 @@ class ContentLibraryController extends Controller 'details' => $collectedData ] ]; - + } catch (\Exception $e) { return [ 'status' => 'error', @@ -1245,7 +1281,7 @@ class ContentLibraryController extends Controller ]; } } - + /** * 判断内容类型 * @param string $content 内容文本 @@ -1259,13 +1295,13 @@ class ContentLibraryController extends Controller if (empty($content) && empty($resUrls) && empty($urls)) { return 0; // 未知类型 } - + // 分析内容中可能包含的链接或图片地址 if (!empty($content)) { // 检查内容中是否有链接 $urlPattern = '/https?:\/\/[-A-Za-z0-9+&@#\/%?=~_|!:,.;]+[-A-Za-z0-9+&@#\/%=~_|]/'; preg_match_all($urlPattern, $content, $contentUrlMatches); - + if (!empty($contentUrlMatches[0])) { // 将内容中的链接添加到urls数组中(去重) foreach ($contentUrlMatches[0] as $url) { @@ -1274,25 +1310,25 @@ class ContentLibraryController extends Controller } } } - + // 检查内容中是否包含图片或视频链接 foreach ($contentUrlMatches[0] ?? [] as $url) { // 检查是否为图片文件 - if (stripos($url, '.jpg') !== false || - stripos($url, '.jpeg') !== false || - stripos($url, '.png') !== false || - stripos($url, '.gif') !== false || - stripos($url, '.webp') !== false || + if (stripos($url, '.jpg') !== false || + stripos($url, '.jpeg') !== false || + stripos($url, '.png') !== false || + stripos($url, '.gif') !== false || + stripos($url, '.webp') !== false || stripos($url, '.bmp') !== false || stripos($url, 'image') !== false) { if (!in_array($url, $resUrls)) { $resUrls[] = $url; } } - + // 检查是否为视频文件 - if (stripos($url, '.mp4') !== false || - stripos($url, '.mov') !== false || + if (stripos($url, '.mp4') !== false || + stripos($url, '.mov') !== false || stripos($url, '.avi') !== false || stripos($url, '.wmv') !== false || stripos($url, '.flv') !== false || @@ -1303,21 +1339,21 @@ class ContentLibraryController extends Controller } } } - + // 判断是否有小程序信息 if (strpos($content, '小程序') !== false || strpos($content, 'appid') !== false) { return 5; // 小程序 } - + // 检查资源URL中是否有视频或图片 $hasVideo = false; $hasImage = false; - + if (!empty($resUrls)) { foreach ($resUrls as $url) { // 检查是否为视频文件 - if (stripos($url, '.mp4') !== false || - stripos($url, '.mov') !== false || + if (stripos($url, '.mp4') !== false || + stripos($url, '.mov') !== false || stripos($url, '.avi') !== false || stripos($url, '.wmv') !== false || stripos($url, '.flv') !== false || @@ -1325,13 +1361,13 @@ class ContentLibraryController extends Controller $hasVideo = true; break; // 一旦发现视频文件,立即退出循环 } - + // 检查是否为图片文件 - if (stripos($url, '.jpg') !== false || - stripos($url, '.jpeg') !== false || - stripos($url, '.png') !== false || - stripos($url, '.gif') !== false || - stripos($url, '.webp') !== false || + if (stripos($url, '.jpg') !== false || + stripos($url, '.jpeg') !== false || + stripos($url, '.png') !== false || + stripos($url, '.gif') !== false || + stripos($url, '.webp') !== false || stripos($url, '.bmp') !== false || stripos($url, 'image') !== false) { $hasImage = true; @@ -1339,12 +1375,12 @@ class ContentLibraryController extends Controller } } } - + // 如果发现视频文件,判定为视频类型 if ($hasVideo) { return 3; // 视频 } - + // 判断内容是否纯链接 $isPureLink = false; if (!empty($content) && !empty($urls)) { @@ -1357,12 +1393,12 @@ class ContentLibraryController extends Controller $isPureLink = true; } } - + // 如果内容是纯链接,判定为链接类型 if ($isPureLink) { return 2; // 链接 } - + // 优先判断内容文本 // 如果有文本内容(不仅仅是链接) if (!empty($content) && !$isPureLink) { @@ -1373,17 +1409,17 @@ class ContentLibraryController extends Controller return 4; // 纯文本 } } - + // 判断是否为图片类型 if ($hasImage) { return 1; // 图片 } - + // 判断是否为链接类型 if (!empty($urls)) { return 2; // 链接 } - + // 默认为文本类型 return 4; // 文本 } @@ -1404,81 +1440,90 @@ class ContentLibraryController extends Controller try { - + // 检查朋友圈数据是否已存在于内容项目中 $exists = ContentItem::where('libraryId', $libraryId) ->where('snsId', $moment['snsId'] ?? '') ->find(); - + if ($exists) { return true; } - + // 解析资源URL (可能是JSON字符串) $resUrls = $moment['resUrls']; if (is_string($resUrls)) { $resUrls = json_decode($resUrls, true); } - + // 处理urls字段 $urls = $moment['urls'] ?? []; if (is_string($urls)) { $urls = json_decode($urls, true); } - + // 构建封面图片 $coverImage = ''; if (!empty($resUrls) && is_array($resUrls) && count($resUrls) > 0) { $coverImage = $resUrls[0]; } - + // 判断内容类型 (0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序) - if($moment['type'] == 1) { + if ($moment['type'] == 1) { //图文 $contentType = 1; - }elseif ($moment['type'] == 3){ + } elseif ($moment['type'] == 3) { //链接 $contentType = 2; $urls = []; $url = is_string($moment['urls']) ? json_decode($moment['urls'], true) : $moment['urls'] ?? []; $url = $url[0]; - // 检查是否是飞书链接 - if (strpos($url, 'feishu.cn') !== false) { - // 飞书文档需要登录,无法直接获取内容,返回默认信息 + //兼容链接采集不到标题及图标 + if (empty($moment['title']) || empty($moment['coverImage'])) { + // 检查是否是飞书链接 + if (strpos($url, 'feishu.cn') !== false) { + // 飞书文档需要登录,无法直接获取内容,返回默认信息 + $urls[] = [ + 'url' => $url, + 'image' => 'http://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/2025/07/09/3db2a5d7fe49011ab68175a42a5094ce.jpeg', + 'desc' => '飞书文档' + ]; + } else { + $getUrlDetails = $this->getExternalPageDetails($url); + $icon = 'http://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/2025/07/09/ec039d96fad6eab1d960f207d3d9ca9f.jpeg'; + if (!empty($getUrlDetails['title'])) { + $urls[] = [ + 'url' => $url, + 'image' => $icon, + 'desc' => '点击查看详情' + ]; + } else { + $urls[] = [ + 'url' => $url, + 'image' => !empty($getUrlDetails['icon']) ? $getUrlDetails['icon'] : $icon, + 'desc' => $getUrlDetails['title'] + ]; + } + } + }else{ $urls[] = [ 'url' => $url, - 'image' => 'http://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/2025/07/09/3db2a5d7fe49011ab68175a42a5094ce.jpeg', - 'desc' => '飞书文档' + 'image' => $moment['coverImage'], + 'desc' => $moment['title'] ]; - }else{ - $getUrlDetails = $this->getExternalPageDetails($url); - $icon = 'http://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/2025/07/09/ec039d96fad6eab1d960f207d3d9ca9f.jpeg'; - if (!empty($getUrlDetails['title'])) { - $urls[] = [ - 'url' => $url, - 'image' => $icon, - 'desc' => '点击查看详情' - ]; - }else{ - $urls[] = [ - 'url' => $url, - 'image' => !empty($getUrlDetails['icon']) ? $getUrlDetails['icon'] : $icon, - 'desc' => $getUrlDetails['title'] - ]; - } } $moment['urls'] = $urls; - }elseif ($moment['type'] == 15){ + } elseif ($moment['type'] == 15) { //视频 $contentType = 3; - }elseif ($moment['type'] == 2){ + } elseif ($moment['type'] == 2) { //纯文本 $contentType = 4; - }elseif ($moment['type'] == 30){ + } elseif ($moment['type'] == 30) { //小程序 $contentType = 5; - }else{ + } else { $contentType = 1; } @@ -1497,11 +1542,11 @@ class ContentLibraryController extends Controller $item->contentAi = $moment['contentAi'] ?? ''; $item->coverImage = $coverImage; $item->contentType = $contentType; // 设置内容类型 - + // 独立存储resUrls和urls字段 $item->resUrls = is_string($moment['resUrls']) ? $moment['resUrls'] : json_encode($resUrls, JSON_UNESCAPED_UNICODE); $item->urls = is_string($moment['urls']) ? $moment['urls'] : json_encode($urls, JSON_UNESCAPED_UNICODE); - + // 保存地理位置信息 $item->location = $moment['location'] ?? ''; $item->lat = $moment['lat'] ?? 0; @@ -1514,7 +1559,7 @@ class ContentLibraryController extends Controller return false; } } - + /** * 保存群聊消息到内容项目表 * @param array $message 消息数据 @@ -1537,26 +1582,26 @@ class ContentLibraryController extends Controller if ($exists) { return true; } - + // 提取消息内容中的链接 $content = $message['content'] ?? ''; $links = []; $pattern = '/https?:\/\/[-A-Za-z0-9+&@#\/%?=~_|!:,.;]+[-A-Za-z0-9+&@#\/%=~_|]/'; preg_match_all($pattern, $content, $matches); - + if (!empty($matches[0])) { $links = $matches[0]; } - + // 提取可能的图片URL $resUrls = []; if (isset($message['imageUrl']) && !empty($message['imageUrl'])) { $resUrls[] = $message['imageUrl']; } - + // 判断内容类型 (0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序, 6=图文) $contentType = $this->determineContentType($content, $resUrls, $links); - + // 创建新的内容项目 $item = new ContentItem(); $item->libraryId = $libraryId; @@ -1567,13 +1612,13 @@ class ContentLibraryController extends Controller $item->createTime = time(); $item->content = $content; $item->contentType = $contentType; // 设置内容类型 - + // 设置发送者信息 $item->wechatId = $message['senderWechatId'] ?? ''; $item->wechatChatroomId = $message['wechatChatroomId'] ?? ''; $item->senderNickname = $message['senderNickname'] ?? ''; $item->createMessageTime = $message['createTime'] ?? 0; - + // 处理资源URL if (!empty($resUrls)) { $item->resUrls = json_encode($resUrls, JSON_UNESCAPED_UNICODE); @@ -1582,15 +1627,15 @@ class ContentLibraryController extends Controller $item->coverImage = $resUrls[0]; } } - + // 处理链接 if (!empty($links)) { $item->urls = json_encode($links, JSON_UNESCAPED_UNICODE); } - + // 设置商品信息(需根据消息内容解析) $this->extractProductInfo($item, $content); - + $item->save(); return true; } catch (\Exception $e) { @@ -1599,7 +1644,7 @@ class ContentLibraryController extends Controller return false; } } - + /** * 从消息内容中提取商品信息 * @param ContentItem $item 内容项目对象 @@ -1614,7 +1659,7 @@ class ContentLibraryController extends Controller '/《(.+?)》/', // 匹配《》中的内容 '/商品名称[::](.+?)[\r\n]/' // 匹配"商品名称:"后的内容 ]; - + foreach ($titlePatterns as $pattern) { preg_match($pattern, $content, $matches); if (!empty($matches[1])) { @@ -1622,7 +1667,7 @@ class ContentLibraryController extends Controller break; } } - + // 如果没有找到商品名称,尝试使用内容的前部分作为标题 if (empty($item->productTitle)) { // 获取第一行非空内容作为标题 @@ -1636,7 +1681,7 @@ class ContentLibraryController extends Controller } } } - + /** * 获取朋友圈数据 * @param string $wechatId 微信ID @@ -1652,7 +1697,7 @@ class ContentLibraryController extends Controller ['id' => 2, 'content' => '分享一个有趣的项目', 'createTime' => time() - 7200], ]; } - + /** * 根据关键词过滤朋友圈内容 * @param array $moments 朋友圈内容 @@ -1665,16 +1710,16 @@ class ContentLibraryController extends Controller if (empty($moments)) { return []; } - + $filtered = []; foreach ($moments as $moment) { $content = $moment['content'] ?? ''; - + // 如果内容为空,跳过 if (empty($content)) { continue; } - + // 检查是否包含必须关键词 $includeMatch = empty($includeKeywords); if (!empty($includeKeywords)) { @@ -1685,12 +1730,12 @@ class ContentLibraryController extends Controller } } } - + // 如果不满足包含条件,跳过 if (!$includeMatch) { continue; } - + // 检查是否包含排除关键词 $excludeMatch = false; if (!empty($excludeKeywords)) { @@ -1701,19 +1746,19 @@ class ContentLibraryController extends Controller } } } - + // 如果满足排除条件,跳过 if ($excludeMatch) { continue; } - + // 通过所有过滤,添加到结果中 $filtered[] = $moment; } - + return $filtered; } - + /** * 使用AI处理采集的数据 * @param array $data 采集的数据 @@ -1726,7 +1771,7 @@ class ContentLibraryController extends Controller // 实际实现需要根据具体的AI API return $data; } - + /** * 保存采集的数据到内容项目 * @param array $data 采集的数据 @@ -1738,7 +1783,7 @@ class ContentLibraryController extends Controller if (empty($data) || empty($libraryId)) { return false; } - + try { foreach ($data as $wechatId => $userData) { foreach ($userData['moments'] as $moment) { @@ -1759,7 +1804,7 @@ class ContentLibraryController extends Controller return false; } } - + /** * 获取所有群成员 * @param array $groupIds 群组ID列表 @@ -1770,7 +1815,7 @@ class ContentLibraryController extends Controller if (empty($groupIds)) { return []; } - + try { // 查询群成员信息 $members = Db::name('wechat_group_member')->alias('gm') @@ -1779,7 +1824,7 @@ class ContentLibraryController extends Controller ->whereIn('gm.groupId', $groupIds) ->where('gm.isDel', 0) ->select(); - + return $members; } catch (\Exception $e) { \think\facade\Log::error('获取群成员失败: ' . $e->getMessage()); @@ -1788,9 +1833,7 @@ class ContentLibraryController extends Controller } - - - /** + /** * 解析URL获取网页信息(内部调用) * @param string $url 要解析的URL * @return array 包含title、icon的数组,失败返回空数组 @@ -1822,7 +1865,7 @@ class ContentLibraryController extends Controller // 获取网页内容 $html = @file_get_contents($url, false, $context); - + if ($html === false) { return []; } @@ -1879,9 +1922,6 @@ class ContentLibraryController extends Controller } - - - /** * 将相对URL转换为绝对URL * @param string $relativeUrl 相对URL @@ -1907,9 +1947,9 @@ class ContentLibraryController extends Controller // 处理以/开头的绝对路径 if (strpos($relativeUrl, '/') === 0) { - return $baseParts['scheme'] . '://' . $baseParts['host'] . - (isset($baseParts['port']) ? ':' . $baseParts['port'] : '') . - $relativeUrl; + return $baseParts['scheme'] . '://' . $baseParts['host'] . + (isset($baseParts['port']) ? ':' . $baseParts['port'] : '') . + $relativeUrl; } // 处理相对路径 @@ -1918,9 +1958,9 @@ class ContentLibraryController extends Controller $basePath = '/'; } - return $baseParts['scheme'] . '://' . $baseParts['host'] . - (isset($baseParts['port']) ? ':' . $baseParts['port'] : '') . - $basePath . '/' . $relativeUrl; + return $baseParts['scheme'] . '://' . $baseParts['host'] . + (isset($baseParts['port']) ? ':' . $baseParts['port'] : '') . + $basePath . '/' . $relativeUrl; } /** @@ -1936,20 +1976,20 @@ class ContentLibraryController extends Controller // 移除HTML实体 $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); - + // 移除多余的空白字符 $text = preg_replace('/\s+/', ' ', $text); - + // 移除控制字符 $text = preg_replace('/[\x00-\x1F\x7F]/', '', $text); - + return trim($text); } - public function aiRewrite($library = [],$content = '') + public function aiRewrite($library = [], $content = '') { - if (empty($library['aiEnabled']) && empty($content)){ + if (empty($library['aiEnabled']) && empty($content)) { return false; } @@ -1957,19 +1997,19 @@ class ContentLibraryController extends Controller $utl = Env::get('doubaoAi.api_url', ''); $apiKey = Env::get('doubaoAi.api_key', ''); $model = Env::get('doubaoAi.model', 'doubao-1-5-pro-32k-250115'); - if (empty($apiKey)){ + if (empty($apiKey)) { return false; } if (!empty($library['aiPrompt'])) { $aiPrompt = $library['aiPrompt']; - }else{ + } else { $aiPrompt = '重写这条朋友圈 要求: 1、原本的字数和意思不要修改超过10% 2、出现品牌名或个人名字就去除'; } - $content = $aiPrompt .' ' . $content; + $content = $aiPrompt . ' ' . $content; $headerData = ['Authorization:Bearer ' . $apiKey]; $header = setHeader($headerData); @@ -1977,16 +2017,16 @@ class ContentLibraryController extends Controller $params = [ 'model' => $model, 'messages' => [ - ['role' => 'system','content' => '你是人工智能助手.'], - ['role' => 'user','content' => $content], + ['role' => 'system', 'content' => '你是人工智能助手.'], + ['role' => 'user', 'content' => $content], ] ]; - $result = requestCurl($utl, $params, 'POST', $header,'json'); - $result = json_decode($result,true); - if (!empty($result['choices'])){ + $result = requestCurl($utl, $params, 'POST', $header, 'json'); + $result = json_decode($result, true); + if (!empty($result['choices'])) { $contentAI = $result['choices'][0]['message']['content']; return $contentAI; - }else{ + } else { return false; } } From 7b34feabc68843d68295e6f80027cfd4bcdf4918 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Tue, 29 Jul 2025 10:24:16 +0800 Subject: [PATCH 09/93] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cunkebao/controller/ContentLibraryController.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Server/application/cunkebao/controller/ContentLibraryController.php b/Server/application/cunkebao/controller/ContentLibraryController.php index c59d7d0b..a55a574b 100644 --- a/Server/application/cunkebao/controller/ContentLibraryController.php +++ b/Server/application/cunkebao/controller/ContentLibraryController.php @@ -1480,7 +1480,7 @@ class ContentLibraryController extends Controller $url = $url[0]; //兼容链接采集不到标题及图标 - if (empty($moment['title']) || empty($moment['coverImage'])) { + if (empty($moment['title'])) { // 检查是否是飞书链接 if (strpos($url, 'feishu.cn') !== false) { // 飞书文档需要登录,无法直接获取内容,返回默认信息 @@ -1507,9 +1507,15 @@ class ContentLibraryController extends Controller } } }else{ + if (strpos($url, 'feishu.cn') !== false) { + $coverImage = 'http://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/2025/07/09/3db2a5d7fe49011ab68175a42a5094ce.jpeg'; + }else{ + $coverImage = 'http://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/2025/07/09/ec039d96fad6eab1d960f207d3d9ca9f.jpeg'; + } + $urls[] = [ 'url' => $url, - 'image' => $moment['coverImage'], + 'image' => !empty($moment['coverImage']) ? $moment['coverImage'] : $coverImage, 'desc' => $moment['title'] ]; } From 9726518a460d3b3a9a639cd1af309ff5db1f4639 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Tue, 29 Jul 2025 10:47:02 +0800 Subject: [PATCH 10/93] =?UTF-8?q?=E6=9C=8B=E5=8F=8B=E5=9C=88=E9=87=87?= =?UTF-8?q?=E9=9B=86=E4=BF=AE=E5=A4=8D=E9=93=BE=E6=8E=A5=E6=B2=A1=E6=9C=89?= =?UTF-8?q?=E6=A0=87=E9=A2=98=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/controller/WebSocketController.php | 11 ++++++----- .../cunkebao/controller/ContentLibraryController.php | 5 ++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Server/application/api/controller/WebSocketController.php b/Server/application/api/controller/WebSocketController.php index 27fb9c18..5090a578 100644 --- a/Server/application/api/controller/WebSocketController.php +++ b/Server/application/api/controller/WebSocketController.php @@ -252,7 +252,6 @@ class WebSocketController extends BaseController "wechatFriendId" => $wechatFriendId, "seq" => time(), ]; - Log::info('获取朋友圈信:' . json_encode($params, 256)); $message = $this->sendMessage($params); Log::info('获取朋友圈信成功:' . json_encode($message, 256)); @@ -293,7 +292,6 @@ class WebSocketController extends BaseController // 合并朋友圈数据 $allMoments = array_merge($allMoments, $message['result']); - // 存储当前页的朋友圈数据到数据库 $this->saveMomentsToDatabase($message['result'], $wechatAccountId, $wechatFriendId); @@ -519,7 +517,9 @@ class WebSocketController extends BaseController $momentId = WechatMoments::where('snsId', $moment['snsId']) ->where('wechatAccountId', $wechatAccountId) ->value('id'); - + + + $dataToSave = [ 'commentList' => json_encode($moment['commentList'] ?? [], 256), 'createTime' => $moment['createTime'] ?? 0, @@ -534,10 +534,11 @@ class WebSocketController extends BaseController 'userName' => $momentEntity['userName'] ?? '', 'snsId' => $moment['snsId'] ?? '', 'type' => $moment['type'] ?? 0, - 'title' => $moment['title'] ?? '', - 'coverImage' => $moment['coverImage'] ?? '', + 'title' => $momentEntity['title'] ?? '', + 'coverImage' => $momentEntity['coverImage'] ?? '', 'update_time' => time() ]; + if (!empty($momentId)) { // 如果已存在,则更新数据 Db::table('s2_wechat_moments')->where('id', $momentId)->update($dataToSave); diff --git a/Server/application/cunkebao/controller/ContentLibraryController.php b/Server/application/cunkebao/controller/ContentLibraryController.php index a55a574b..1002e7ae 100644 --- a/Server/application/cunkebao/controller/ContentLibraryController.php +++ b/Server/application/cunkebao/controller/ContentLibraryController.php @@ -821,6 +821,7 @@ class ContentLibraryController extends Controller $where = [ ['isDel', '=', 0], // 未删除 ['status', '=', 1], // 已开启 + ['id', '=', 61], // 已开启 ]; // 查询符合条件的内容库 @@ -832,7 +833,6 @@ class ContentLibraryController extends Controller if (empty($libraries)) { return json(['code' => 200, 'msg' => '没有可用的内容库配置']); } - $successCount = 0; $failCount = 0; $results = []; @@ -964,7 +964,6 @@ class ContentLibraryController extends Controller $automaticAssign->allotWechatFriend(['wechatFriendId' => $friend['id'], 'toAccountId' => $friend['accountId']], true); } - // 从s2_wechat_moments表获取朋友圈数据 $moments = Db::table('s2_wechat_moments') ->where([ @@ -1512,7 +1511,7 @@ class ContentLibraryController extends Controller }else{ $coverImage = 'http://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/2025/07/09/ec039d96fad6eab1d960f207d3d9ca9f.jpeg'; } - + $urls[] = [ 'url' => $url, 'image' => !empty($moment['coverImage']) ? $moment['coverImage'] : $coverImage, From 2b67bd6b77495aa458da8ba6d74e0d9418e21847 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Tue, 29 Jul 2025 17:04:00 +0800 Subject: [PATCH 11/93] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/controller/WechatFriendController.php | 4 +- Server/application/cunkebao/config/route.php | 4 +- .../controller/ContentLibraryController.php | 68 ++++---- ...PotentialListWithInCompanyV1Controller.php | 149 ++++++++++++++---- .../Adapters/ChuKeBao/Adapter.php | 39 +++-- 5 files changed, 194 insertions(+), 70 deletions(-) diff --git a/Server/application/api/controller/WechatFriendController.php b/Server/application/api/controller/WechatFriendController.php index 6a3d3f61..2d747689 100644 --- a/Server/application/api/controller/WechatFriendController.php +++ b/Server/application/api/controller/WechatFriendController.php @@ -123,6 +123,7 @@ class WechatFriendController extends BaseController 'region' => $item['region'], 'addFrom' => $item['addFrom'], 'labels' => is_array($item['labels']) ? json_encode($item['labels']) : json_encode([]), + 'siteLabels' => json_encode([]), 'signature' => $item['signature'], 'isDeleted' => $item['isDeleted'], 'isPassed' => $item['isPassed'], @@ -153,7 +154,8 @@ class WechatFriendController extends BaseController $friend = WechatFriendModel::where('id', $item['id'])->find(); if ($friend) { - $result = $friend->save($data); + unset($data['siteLabels']); + $friend->save($data); return true; } else { WechatFriendModel::create($data); diff --git a/Server/application/cunkebao/config/route.php b/Server/application/cunkebao/config/route.php index c0dfc22e..8f7777f8 100644 --- a/Server/application/cunkebao/config/route.php +++ b/Server/application/cunkebao/config/route.php @@ -57,6 +57,8 @@ Route::group('v1/', function () { Route::group('traffic/pool', function () { Route::get('', 'app\cunkebao\controller\traffic\GetPotentialListWithInCompanyV1Controller@index'); Route::get('getUserJourney', 'app\cunkebao\controller\traffic\GetPotentialListWithInCompanyV1Controller@getUserJourney'); + Route::get('getUserTags', 'app\cunkebao\controller\traffic\GetPotentialListWithInCompanyV1Controller@getUserTags'); + Route::get('getUserInfo', 'app\cunkebao\controller\traffic\GetPotentialListWithInCompanyV1Controller@getUser'); @@ -99,7 +101,7 @@ Route::group('v1/', function () { Route::delete('delete-item', 'app\cunkebao\controller\ContentLibraryController@deleteItem'); // 删除内容库素材 Route::get('get-item-detail', 'app\cunkebao\controller\ContentLibraryController@getItemDetail'); // 获取内容库素材详情 Route::post('update-item', 'app\cunkebao\controller\ContentLibraryController@updateItem'); // 更新内容库素材 - Route::get('aiEditContent', 'app\cunkebao\controller\ContentLibraryController@aiEditContent'); + Route::any('aiEditContent', 'app\cunkebao\controller\ContentLibraryController@aiEditContent'); }); // 好友相关 diff --git a/Server/application/cunkebao/controller/ContentLibraryController.php b/Server/application/cunkebao/controller/ContentLibraryController.php index 1002e7ae..5e6b6956 100644 --- a/Server/application/cunkebao/controller/ContentLibraryController.php +++ b/Server/application/cunkebao/controller/ContentLibraryController.php @@ -216,7 +216,7 @@ class ContentLibraryController extends Controller ->find(); if (empty($library)) { - return json(['code' => 404, 'msg' => '内容库不存在']); + return json(['code' => 500, 'msg' => '内容库不存在']); } // 处理JSON字段转数组 @@ -306,7 +306,7 @@ class ContentLibraryController extends Controller ])->find(); if (!$library) { - return json(['code' => 404, 'msg' => '内容库不存在']); + return json(['code' => 500, 'msg' => '内容库不存在']); } Db::startTrans(); @@ -361,7 +361,7 @@ class ContentLibraryController extends Controller ])->find(); if (empty($library)) { - return json(['code' => 404, 'msg' => '内容库不存在']); + return json(['code' => 500, 'msg' => '内容库不存在']); } try { @@ -403,7 +403,7 @@ class ContentLibraryController extends Controller ])->find(); if (empty($library)) { - return json(['code' => 404, 'msg' => '内容库不存在或无权限访问']); + return json(['code' => 500, 'msg' => '内容库不存在或无权限访问']); } // 构建查询条件 @@ -520,7 +520,7 @@ class ContentLibraryController extends Controller ])->find(); if (!$library) { - return json(['code' => 404, 'msg' => '内容库不存在']); + return json(['code' => 500, 'msg' => '内容库不存在']); } try { @@ -568,7 +568,7 @@ class ContentLibraryController extends Controller ->find(); if (!$item) { - return json(['code' => 404, 'msg' => '内容项目不存在或无权限操作']); + return json(['code' => 500, 'msg' => '内容项目不存在或无权限操作']); } try { @@ -608,7 +608,7 @@ class ContentLibraryController extends Controller ->find(); if (empty($item)) { - return json(['code' => 404, 'msg' => '内容项目不存在或无权限访问']); + return json(['code' => 500, 'msg' => '内容项目不存在或无权限访问']); } // 处理数据 @@ -691,7 +691,7 @@ class ContentLibraryController extends Controller ])->find(); if (!$item) { - return json(['code' => 404, 'msg' => '内容项目不存在或无权限操作']); + return json(['code' => 500, 'msg' => '内容项目不存在或无权限操作']); } try { @@ -754,36 +754,50 @@ class ContentLibraryController extends Controller { $id = Request::param('id', ''); - + $aiPrompt = Request::param('aiPrompt', ''); + $content = Request::param('content', ''); + $companyId = $this->request->userInfo['companyId']; // 简单验证 if (empty($id)) { return json(['code' => 400, 'msg' => '参数错误']); } // 查询内容项目是否存在并检查权限 - $item = ContentItem::where([ - ['id', '=', $id], - ['isDel', '=', 0] - ])->find(); + $item = ContentItem::alias('ci') + ->join('content_library cl', 'ci.libraryId = cl.id') + ->where(['ci.id' => $id, 'ci.isDel' => 0, 'cl.isDel' => 0, 'cl.companyId' => $companyId]) + ->field('ci.*') + ->find(); if (empty($item)) { - return json(['code' => 404, 'msg' => '内容项目不存在或无权限操作']); + return json(['code' => 500, 'msg' => '内容项目不存在或无权限操作']); } if (empty($item['content'])) { - return json(['code' => 404, 'msg' => '内容不能为空']); + return json(['code' => 500, 'msg' => '内容不能为空']); } - - try { - $contentAi = $this->aiRewrite(['aiEnabled' => true], $item['content']); - if (!empty($contentAi)) { - ContentItem::where(['id' => $item['id']])->update(['contentAi' => $contentAi, 'updateTime' => time()]); - return json(['code' => 200, 'msg' => 'ai编写成功', 'data' => ['editAfter' => $contentAi, 'editFront' => $item['content']]]); - } else { - return json(['code' => 500, 'msg' => 'ai编写失败']); + $contentFront = !empty($item['contentAi']) ? $item['contentAi'] : $item['content']; + if (!$this->request->isPost()) { + try { + $contentAi = $this->aiRewrite(['aiEnabled' => true, 'aiPrompt' => $aiPrompt], $contentFront); + if (!empty($contentAi)) { + return json(['code' => 200, 'msg' => 'ai编写成功', 'data' => ['contentAfter' => $contentAi, 'contentFront' => $contentFront]]); + } else { + return json(['code' => 500, 'msg' => 'ai编写失败']); + } + } catch (\Exception $e) { + return json(['code' => 500, 'msg' => 'ai编写失败:' . $e->getMessage()]); + } + } else { + if (empty($content)) { + return json(['code' => 500, 'msg' => '新内容不能为空']); + } + $res = ContentItem::where(['id' => $item['id']])->update(['contentAi' => $content, 'updateTime' => time()]); + if (!empty($res)) { + return json(['code' => 200, 'msg' => '更新成功']); + } else { + return json(['code' => 500, 'msg' => '更新失败']); } - } catch (\Exception $e) { - return json(['code' => 500, 'msg' => 'ai编写失败:' . $e->getMessage()]); } } @@ -1505,10 +1519,10 @@ class ContentLibraryController extends Controller ]; } } - }else{ + } else { if (strpos($url, 'feishu.cn') !== false) { $coverImage = 'http://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/2025/07/09/3db2a5d7fe49011ab68175a42a5094ce.jpeg'; - }else{ + } else { $coverImage = 'http://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/2025/07/09/ec039d96fad6eab1d960f207d3d9ca9f.jpeg'; } diff --git a/Server/application/cunkebao/controller/traffic/GetPotentialListWithInCompanyV1Controller.php b/Server/application/cunkebao/controller/traffic/GetPotentialListWithInCompanyV1Controller.php index 0ebfe936..0bcd7b10 100644 --- a/Server/application/cunkebao/controller/traffic/GetPotentialListWithInCompanyV1Controller.php +++ b/Server/application/cunkebao/controller/traffic/GetPotentialListWithInCompanyV1Controller.php @@ -23,7 +23,7 @@ class GetPotentialListWithInCompanyV1Controller extends BaseController protected function makeWhere(array $params = []): array { if (!empty($keyword = $this->request->param('keyword'))) { - $where[] = ['p.identifier|wa.nickname|wa.phone|wa.wechatId|wa.alias','like', '%'. $keyword .'%']; + $where[] = ['p.identifier|wa.nickname|wa.phone|wa.wechatId|wa.alias', 'like', '%' . $keyword . '%']; } // 状态筛选 @@ -35,10 +35,10 @@ class GetPotentialListWithInCompanyV1Controller extends BaseController // 来源的筛选 if ($fromd = $this->request->param('packageId')) { - if ($fromd != -1){ + if ($fromd != -1) { $where['tsp.id'] = $fromd; - }else{ - $where[] = ['tsp.id',null]; + } else { + $where[] = ['tsp.id', null]; } } @@ -48,7 +48,6 @@ class GetPotentialListWithInCompanyV1Controller extends BaseController } - $where['s.companyId'] = $this->getUserInfo('companyId'); return array_merge($where, $params); @@ -65,16 +64,16 @@ class GetPotentialListWithInCompanyV1Controller extends BaseController $query = TrafficPoolModel::alias('p') ->field( [ - 'p.id','p.identifier', 'p.mobile', 'p.wechatId', 'p.identifier', - 's.fromd', 's.status', 's.createTime','s.companyId','s.sourceId','s.type', + 'p.id', 'p.identifier', 'p.mobile', 'p.wechatId', 'p.identifier', + 's.fromd', 's.status', 's.createTime', 's.companyId', 's.sourceId', 's.type', 'wa.nickname', 'wa.avatar', 'wa.gender', 'wa.phone', ] ) - ->join('traffic_source s', 'p.identifier=s.identifier','left') - ->join('wechat_account wa', 'p.identifier=wa.wechatId','left') - ->join('traffic_source_package_item tspi', 'p.identifier = tspi.identifier AND s.companyId = tspi.companyId','left') - ->join('traffic_source_package tsp', 'tspi.packageId=tsp.id','left') - ->join('device_wechat_login d', 's.sourceId=d.wechatId','left') + ->join('traffic_source s', 'p.identifier=s.identifier', 'left') + ->join('wechat_account wa', 'p.identifier=wa.wechatId', 'left') + ->join('traffic_source_package_item tspi', 'p.identifier = tspi.identifier AND s.companyId = tspi.companyId', 'left') + ->join('traffic_source_package tsp', 'tspi.packageId=tsp.id', 'left') + ->join('device_wechat_login d', 's.sourceId=d.wechatId', 'left') ->order('p.id DESC,s.id DESC') ->group('p.identifier'); @@ -90,7 +89,7 @@ class GetPotentialListWithInCompanyV1Controller extends BaseController $query->where($key, $value); } - $result = $query->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]); + $result = $query->paginate($this->request->param('limit/d', 10), false, ['page' => $this->request->param('page/d', 1)]); $list = $result->items(); $total = $result->total(); @@ -98,29 +97,29 @@ class GetPotentialListWithInCompanyV1Controller extends BaseController //流量池筛选 $package = Db::name('traffic_source_package_item')->alias('tspi') ->join('traffic_source_package p', 'tspi.packageId=p.id AND tspi.companyId=p.companyId') - ->where(['tspi.companyId' => $item->companyId,'tspi.identifier' => $item->identifier]) + ->where(['tspi.companyId' => $item->companyId, 'tspi.identifier' => $item->identifier]) ->column('p.name'); $package2 = Db::name('traffic_source_package_item')->alias('tspi') ->join('traffic_source_package p', 'tspi.packageId=p.id') - ->where(['tspi.companyId' => $item->companyId,'tspi.identifier' => $item->identifier,'p.isSys' => 1]) + ->where(['tspi.companyId' => $item->companyId, 'tspi.identifier' => $item->identifier, 'p.isSys' => 1]) ->column('p.name'); $packages = array_merge($package, $package2); $item['packages'] = $packages; - if ($item->type == 1){ + if ($item->type == 1) { $tag = Db::name('wechat_friendship')->where(['wechatId' => $item->wechatId])->column('tags'); $tags = []; foreach ($tag as $k => $v) { - $v = json_decode($v,true); - $tags = array_merge($tags, $v); + $v = json_decode($v, true); + if (!empty($v)) { + $tags = array_merge($tags, $v); + } } $item['tags'] = $tags; } - - } unset($item); $data = ['list' => $list, 'total' => $total]; @@ -135,11 +134,11 @@ class GetPotentialListWithInCompanyV1Controller extends BaseController public function index() { try { - $result = $this->getPoolListByCompanyId( $this->makeWhere() ); + $result = $this->getPoolListByCompanyId($this->makeWhere()); $result = json_decode($result, true); return ResponseHelper::success( [ - 'list' => $result['list'], + 'list' => $result['list'], 'total' => $result['total'], ] ); @@ -148,7 +147,62 @@ class GetPotentialListWithInCompanyV1Controller extends BaseController } } + public function getUser() + { + $userId = $this->request->param('userId', ''); + if (empty($userId)) { + return json_encode(['code' => 500, 'msg' => '用户id不能为空']); + } + + + $item = TrafficPoolModel::alias('p') + ->field( + [ + 'p.id', 'p.identifier', 'p.mobile', 'p.wechatId', 'p.identifier', + 's.fromd', 's.status', 's.createTime', 's.companyId', 's.sourceId', 's.type', + 'wa.nickname', 'wa.avatar', 'wa.gender', 'wa.phone', + ] + ) + ->join('traffic_source s', 'p.identifier=s.identifier', 'left') + ->join('wechat_account wa', 'p.identifier=wa.wechatId', 'left') + ->join('traffic_source_package_item tspi', 'p.identifier = tspi.identifier AND s.companyId = tspi.companyId', 'left') + ->join('traffic_source_package tsp', 'tspi.packageId=tsp.id', 'left') + ->join('device_wechat_login d', 's.sourceId=d.wechatId', 'left') + ->order('p.id DESC,s.id DESC') + ->where(['p.id' => $userId]) + ->group('p.identifier') + ->find(); + + + //流量池筛选 + $package = Db::name('traffic_source_package_item')->alias('tspi') + ->join('traffic_source_package p', 'tspi.packageId=p.id AND tspi.companyId=p.companyId') + ->where(['tspi.companyId' => $item->companyId, 'tspi.identifier' => $item->identifier]) + ->column('p.name'); + + $package2 = Db::name('traffic_source_package_item')->alias('tspi') + ->join('traffic_source_package p', 'tspi.packageId=p.id') + ->where(['tspi.companyId' => $item->companyId, 'tspi.identifier' => $item->identifier, 'p.isSys' => 1]) + ->column('p.name'); + $packages = array_merge($package, $package2); + $item['packages'] = $packages; + + + if ($item->type == 1) { + $tag = Db::name('wechat_friendship')->where(['wechatId' => $item->wechatId])->column('tags'); + $tags = []; + foreach ($tag as $k => $v) { + $v = json_decode($v, true); + if (!empty($v)) { + $tags = array_merge($tags, $v); + } + } + $item['tags'] = $tags; + } + + + } /** @@ -160,10 +214,10 @@ class GetPotentialListWithInCompanyV1Controller extends BaseController */ public function getUserJourney() { - $page = $this->request->param('page',1); - $pageSize = $this->request->param('pageSize',10); - $userId = $this->request->param('userId',''); - if(empty($userId)){ + $page = $this->request->param('page', 1); + $pageSize = $this->request->param('pageSize', 10); + $userId = $this->request->param('userId', ''); + if (empty($userId)) { return json_encode(['code' => 500, 'msg' => '用户id不能为空']); } @@ -174,16 +228,49 @@ class GetPotentialListWithInCompanyV1Controller extends BaseController $total = $query->count(); $list = $query->order('createTime desc') - ->page($page,$pageSize) + ->page($page, $pageSize) ->select(); - foreach ($list as $k=>$v){ - $list[$k]['createTime'] = date('Y-m-d H:i:s',$v['createTime']); - $list[$k]['updateTime'] = date('Y-m-d H:i:s',$v['updateTime']); + foreach ($list as $k => $v) { + $list[$k]['createTime'] = date('Y-m-d H:i:s', $v['createTime']); + $list[$k]['updateTime'] = date('Y-m-d H:i:s', $v['updateTime']); } - return ResponseHelper::success(['list' => $list,'total'=>$total]); + return ResponseHelper::success(['list' => $list, 'total' => $total]); } + + public function getUserTags() + { + $userId = $this->request->param('userId', ''); + if (empty($userId)) { + return json_encode(['code' => 500, 'msg' => '用户id不能为空']); + } + $data = Db::name('traffic_pool')->alias('tp') + ->join(['s2_wechat_friend' => 'wf'], 'tp.wechatId=wf.wechatId', 'left') + ->where(['tp.id' => $userId]) + ->order('tp.createTime desc') + ->column('wf.id,wf.labels,wf.siteLabels'); + + $tags = []; + $siteLabels = []; + foreach ($data as $k => $v) { + $tag = json_decode($v['labels'], true); + $tag2 = json_decode($v['siteLabels'], true); + if (!empty($tag)) { + $tags = array_merge($tags, $tag); + } + if (!empty($tag2)) { + $siteLabels = array_merge($siteLabels, $tag2); + } + } + $tags = array_unique($tags); + $tags = array_values($tags); + $siteLabels = array_unique($siteLabels); + $siteLabels = array_values($siteLabels); + return ResponseHelper::success(['wechat' => $tags, 'siteLabels' => $siteLabels]); + } + + } \ No newline at end of file diff --git a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php index feed3dee..2dbded4d 100644 --- a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php +++ b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php @@ -155,7 +155,7 @@ class Adapter implements WeChatServiceInterface { $task = Db::name('customer_acquisition_task') ->where(['status' => 1,'deleteTime' => 0]) - ->whereRaw("id % $process_count_for_status_0 = {$current_worker_id}") +// ->whereRaw("id % $process_count_for_status_0 = {$current_worker_id}") ->order('id desc') ->select(); @@ -179,7 +179,6 @@ class Adapter implements WeChatServiceInterface $taskData = array_merge($taskData, $tasks); } - if ($taskData) { foreach ($taskData as $task) { @@ -197,9 +196,9 @@ class Adapter implements WeChatServiceInterface $friendAddTaskCreated = false; foreach ($wechatIdAccountIdMap as $accountId => $wechatId) { - // 是否已经是好友的判断,如果已经是好友,直接break; 但状态还是维持1,让另外一个进程处理发消息的逻辑 - $isFriend = $this->checkIfIsWeChatFriendByPhone($wechatId, $task['phone']); + $wechatTags = json_decode($task['tags'], true); + $isFriend = $this->checkIfIsWeChatFriendByPhone($wechatId, $task['phone'],$task['siteTags']); if (!empty($isFriend)) { $friendAddTaskCreated = true; $task['processed_wechat_ids'] = $task['processed_wechat_ids'] . ',' . $wechatId; // 处理失败任务用,用于过滤已处理的微信号 @@ -221,10 +220,15 @@ class Adapter implements WeChatServiceInterface // 采取乐观尝试的策略,假设第一个可以添加的人可以添加成功的; 回头再另外一个任务进程去判断 // 创建好友添加任务, 对接触客宝 - $tags = array_merge($task_info['tagConf']['customTags'],$task_info['tagConf']['scenarioTags']); + if (!empty($wechatTags)){ + $tags = array_merge($tags,$wechatTags); + } + $tags = array_unique($tags); + $tags = array_values($tags); $conf = array_merge($task_info['reqConf'], ['task_name' => $task_info['name'],'tags' => $tags]); + $this->createFriendAddTask($accountId, $task['phone'], $conf); $friendAddTaskCreated = true; $task['processed_wechat_ids'] = $task['processed_wechat_ids'] . ',' . $wechatId; // 处理失败任务用,用于过滤已处理的微信号 @@ -459,21 +463,36 @@ class Adapter implements WeChatServiceInterface } // 检查是否是好友关系 - public function checkIfIsWeChatFriendByPhone(string $wxId, string $phone): bool + public function checkIfIsWeChatFriendByPhone(string $wxId, string $phone,string $siteTags): bool { if (empty($wxId) || empty($phone)) { return false; } try { - $id = Db::table('s2_wechat_friend') + $friend = Db::table('s2_wechat_friend') ->where('ownerWechatId', $wxId) ->where(['isPassed' => 1,'isDeleted' => 0]) ->where('phone|alias|wechatId', 'like', $phone . '%') ->order('createTime', 'desc') - ->value('id'); - - return (bool)$id; + ->find(); + if (!empty($friend)) { + if (!empty($siteTags)) { + $siteTags = json_decode($siteTags, true); + $siteLabels = json_decode($friend['siteLabels'], true); + $tags = array_merge($siteTags,$siteLabels); + $tags = array_unique($tags); + $tags = array_values($tags); + if (empty($tags)){ + $tags = []; + } + $tags = json_encode($tags,256); + Db::table('s2_wechat_friend')->where(['id' => $friend['id']])->update(['siteLabels' => $tags,'updateTime' => time()]); + } + return true; + }else{ + return false; + } } catch (\Exception $e) { Log::error("Error in checkIfIsWeChatFriendByPhone (wxId: {$wxId}, phone: {$phone}): " . $e->getMessage()); return false; From 60dd035c571efe45f777fb58f476433e9917b888 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Wed, 30 Jul 2025 16:07:15 +0800 Subject: [PATCH 12/93] =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cunkebao/controller/plan/PosterWeChatMiniProgram.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php b/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php index 7539e66e..85bd2e06 100644 --- a/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php +++ b/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php @@ -150,8 +150,8 @@ class PosterWeChatMiniProgram extends Controller $sceneConf = json_decode($task['sceneConf'], true); - if(isset($sceneConf['posters'][0]['url'])) { - $posterUrl = $sceneConf['posters'][0]['url']; + if(isset($sceneConf['posters'][0]['url']) || isset($sceneConf['posters'][0]['preview'])) { + $posterUrl = !empty($sceneConf['posters'][0]['url']) ? $sceneConf['posters'][0]['url'] : $sceneConf['posters'][0]['preview']; } else { $posterUrl = ''; } From 90b3367afb650789136595ae3f1fdc9bae53938b Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Wed, 30 Jul 2025 16:07:41 +0800 Subject: [PATCH 13/93] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../device/GetAddResultedV1Controller.php | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/Server/application/cunkebao/controller/device/GetAddResultedV1Controller.php b/Server/application/cunkebao/controller/device/GetAddResultedV1Controller.php index c2cb8477..4a49b89e 100644 --- a/Server/application/cunkebao/controller/device/GetAddResultedV1Controller.php +++ b/Server/application/cunkebao/controller/device/GetAddResultedV1Controller.php @@ -8,6 +8,7 @@ use app\common\model\User as UserModel; use app\cunkebao\controller\BaseController; use library\ResponseHelper; use think\Db; +use think\facade\Cache; /** * 设备控制器 @@ -94,12 +95,14 @@ class GetAddResultedV1Controller extends BaseController */ protected function getCkbDeviceCount(): int { - return DeviceModel::where( - [ - 'companyId' => $this->getUserInfo('companyId') - ] - ) - ->count('*'); + $companyId = $this->getUserInfo('companyId'); + $cacheKey = 'deviceNum_'.$companyId; + $deviceNum = Cache::get($cacheKey); + if (empty($deviceNum)) { + $deviceNum = DeviceModel::where(['companyId' => $companyId])->count('*'); + Cache::set($cacheKey,$deviceNum,120); + } + return $deviceNum; } /** @@ -110,6 +113,7 @@ class GetAddResultedV1Controller extends BaseController */ protected function getAddResulted(int $accountId): bool { + $deviceNum = $this->getCkbDeviceCount(); $result = (new ApiDeviceController())->getlist( [ 'accountId' => $accountId, @@ -118,13 +122,22 @@ class GetAddResultedV1Controller extends BaseController ], true ); - $result = json_decode($result, true); $result = $result['data']['results'] ?? false; - return $result ? ( - count($result) > $this->getCkbDeviceCount() - ) : false; + if (empty($result)){ + return false; + }else{ + if (count($result) > $deviceNum){ + $companyId = $this->getUserInfo('companyId'); + $cacheKey = 'deviceNum_'.$companyId; + Cache::rm($cacheKey); + return true; + }else{ + return false; + } + } + } /** From 0ea5aff9d320a86968fc00d7f772c05978ba3ccf Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Wed, 30 Jul 2025 16:15:26 +0800 Subject: [PATCH 14/93] =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cunkebao/controller/plan/PosterWeChatMiniProgram.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php b/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php index 85bd2e06..aa5c2b53 100644 --- a/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php +++ b/Server/application/cunkebao/controller/plan/PosterWeChatMiniProgram.php @@ -150,10 +150,10 @@ class PosterWeChatMiniProgram extends Controller $sceneConf = json_decode($task['sceneConf'], true); - if(isset($sceneConf['posters'][0]['url']) || isset($sceneConf['posters'][0]['preview'])) { - $posterUrl = !empty($sceneConf['posters'][0]['url']) ? $sceneConf['posters'][0]['url'] : $sceneConf['posters'][0]['preview']; + if(isset($sceneConf['posters']['url'])) { + $posterUrl = !empty($sceneConf['posters']['url']); } else { - $posterUrl = ''; + $posterUrl = 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%92%A8%E8%AF%A2-FTiyAMAPop2g9LvjLOLDz0VwPg3KVu.gif'; } From fef109b472ed9195559bd1aaea7226a154402d6a Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Wed, 30 Jul 2025 17:51:18 +0800 Subject: [PATCH 15/93] =?UTF-8?q?=E6=B5=81=E9=87=8F=E6=B1=A0=E8=AF=A6?= =?UTF-8?q?=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../device/GetAddResultedV1Controller.php | 4 + ...PotentialListWithInCompanyV1Controller.php | 130 ++++++++++++++---- 2 files changed, 108 insertions(+), 26 deletions(-) diff --git a/Server/application/cunkebao/controller/device/GetAddResultedV1Controller.php b/Server/application/cunkebao/controller/device/GetAddResultedV1Controller.php index 4a49b89e..a2c78b05 100644 --- a/Server/application/cunkebao/controller/device/GetAddResultedV1Controller.php +++ b/Server/application/cunkebao/controller/device/GetAddResultedV1Controller.php @@ -149,6 +149,10 @@ class GetAddResultedV1Controller extends BaseController { $accountId = $this->request->param('accountId/d'); + if (empty($accountId)){ + return ResponseHelper::error('参数缺失'); + } + $isAdded = $this->getAddResulted($accountId); $isAdded && $this->migrateData($accountId); diff --git a/Server/application/cunkebao/controller/traffic/GetPotentialListWithInCompanyV1Controller.php b/Server/application/cunkebao/controller/traffic/GetPotentialListWithInCompanyV1Controller.php index 0bcd7b10..9a68d3c7 100644 --- a/Server/application/cunkebao/controller/traffic/GetPotentialListWithInCompanyV1Controller.php +++ b/Server/application/cunkebao/controller/traffic/GetPotentialListWithInCompanyV1Controller.php @@ -151,57 +151,135 @@ class GetPotentialListWithInCompanyV1Controller extends BaseController { $userId = $this->request->param('userId', ''); + $companyId = $this->getUserInfo('companyId'); + if (empty($userId)) { return json_encode(['code' => 500, 'msg' => '用户id不能为空']); } + $total = [ + 'msg' => 0, + 'money' => 0, + 'isFriend' => false, + 'percentage' => '0.00%', + ]; - $item = TrafficPoolModel::alias('p') - ->field( - [ - 'p.id', 'p.identifier', 'p.mobile', 'p.wechatId', 'p.identifier', - 's.fromd', 's.status', 's.createTime', 's.companyId', 's.sourceId', 's.type', - 'wa.nickname', 'wa.avatar', 'wa.gender', 'wa.phone', - ] - ) - ->join('traffic_source s', 'p.identifier=s.identifier', 'left') + + $data = TrafficPoolModel::alias('p') + ->field(['p.id', 'p.identifier', 'p.wechatId', + 'wa.nickname', 'wa.avatar', 'wa.gender', 'wa.phone','wa.alias']) ->join('wechat_account wa', 'p.identifier=wa.wechatId', 'left') - ->join('traffic_source_package_item tspi', 'p.identifier = tspi.identifier AND s.companyId = tspi.companyId', 'left') - ->join('traffic_source_package tsp', 'tspi.packageId=tsp.id', 'left') - ->join('device_wechat_login d', 's.sourceId=d.wechatId', 'left') - ->order('p.id DESC,s.id DESC') + ->order('p.id DESC') ->where(['p.id' => $userId]) ->group('p.identifier') ->find(); + $data['lastMsgTime'] = ''; - //流量池筛选 + + //来源 + $source = Db::name('traffic_source')->alias('ts') + ->field(['wa.nickname', 'wa.avatar', 'wa.gender','wa.phone','wa.wechatId','wa.alias', + 'ts.createTime', + 'wf.id as friendId','wf.wechatAccountId']) + ->join('wechat_account wa', 'ts.sourceId=wa.wechatId', 'left') + ->join(['s2_wechat_friend' => 'wf'], 'wa.wechatId=wf.ownerWechatId', 'left') + ->where(['ts.companyId' => $companyId,'ts.identifier' => $data['identifier'],'wf.wechatId' => $data['wechatId']]) + ->order('ts.createTime DESC') + ->select(); + + $wechatFriendId = []; + if (!empty($source)) { + $total['isFriend'] = true; + foreach ($source as &$v) { + $wechatFriendId[] = $v['friendId']; + //最后消息 + $v['createTime'] = date('Y-m-d H:i:s', $v['createTime']); + $lastMsgTime = Db::table('s2_wechat_message') + ->where(['wechatFriendId' => $v['friendId'],'wechatAccountId' => $v['wechatAccountId']]) + ->value('wechatTime'); + $v['lastMsgTime'] = !empty($lastMsgTime) ? date('Y-m-d H:i:s', $lastMsgTime) : ''; + + //设备信息 + $device = Db::name('device_wechat_login')->alias('dwl') + ->join('device d','d.id=dwl.deviceId') + ->where(['dwl.wechatId' => $v['wechatId']]) + ->field('d.id,d.memo,d.imei,d.brand,d.extra,d.alive') + ->order('dwl.id DESC') + ->find(); + $extra = json_decode($device['extra'],true); + unset($device['extra']); + $device['address'] = !empty($extra['address']) ? $extra['address'] : ''; + $v['device'] = $device; + } + unset($v); + } + $data['source'] = $source; + + + //流量池 $package = Db::name('traffic_source_package_item')->alias('tspi') ->join('traffic_source_package p', 'tspi.packageId=p.id AND tspi.companyId=p.companyId') - ->where(['tspi.companyId' => $item->companyId, 'tspi.identifier' => $item->identifier]) + ->where(['tspi.companyId' => $companyId, 'tspi.identifier' => $data['identifier']]) ->column('p.name'); - $package2 = Db::name('traffic_source_package_item')->alias('tspi') ->join('traffic_source_package p', 'tspi.packageId=p.id') - ->where(['tspi.companyId' => $item->companyId, 'tspi.identifier' => $item->identifier, 'p.isSys' => 1]) + ->where(['tspi.companyId' => $companyId, 'tspi.identifier' => $data['identifier']]) ->column('p.name'); $packages = array_merge($package, $package2); - $item['packages'] = $packages; + $data['packages'] = $packages; - if ($item->type == 1) { - $tag = Db::name('wechat_friendship')->where(['wechatId' => $item->wechatId])->column('tags'); - $tags = []; - foreach ($tag as $k => $v) { - $v = json_decode($v, true); - if (!empty($v)) { - $tags = array_merge($tags, $v); + if (!empty($wechatFriendId)){ + //消息统计 + $msgTotal = Db::table('s2_wechat_message') + ->whereIn('wechatFriendId', $wechatFriendId) + ->count(); + $total['msg'] = $msgTotal; + + //金额计算 + $money = Db::table('s2_wechat_message') + ->whereIn('wechatFriendId', $wechatFriendId) + ->where(['isSend' => 1,'msgType' => 419430449]) + ->select(); + if (!empty($money)){ + foreach ($money as $v){ + $content = json_decode($v['content'],true); + if ($content['paysubtype'] == 1){ + $number = number_format(str_replace("¥", "", $content['feedesc']), 2); + $floatValue = floatval($number); + $total['money'] += $floatValue; + } } } - $item['tags'] = $tags; } + $taskNum = Db::name('task_customer')->alias('tc') + ->join('customer_acquisition_task t','tc.task_id=t.id') + ->where(['t.companyId' => $companyId,'t.deleteTime' => 0]) + ->whereIn('tc.phone',[$data['phone'], $data['wechatId'], $data['alias']]) + ->count(); + $passNum = Db::name('task_customer')->alias('tc') + ->join('customer_acquisition_task t','tc.task_id=t.id') + ->where(['t.companyId' => $companyId,'t.deleteTime' => 0,'tc.status' => 4]) + ->whereIn('tc.phone',[$data['phone'], $data['wechatId'], $data['alias']]) + ->count(); + + if (!empty($taskNum) && !empty($passNum)){ + $percentage = number_format(($taskNum / $passNum) * 100, 2); + $total['percentage'] = $percentage; + } + + + + $data['total'] = $total; + $data['rmm'] = [ + 'r' => 0, + 'f' => 0, + 'm' => 0, + ]; + return ResponseHelper::success($data); } From de0f8fafda53158f881d7181133e816c022e0a7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Fri, 1 Aug 2025 16:44:35 +0800 Subject: [PATCH 16/93] =?UTF-8?q?=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/vite.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/nkebao/vite.config.ts b/nkebao/vite.config.ts index 921473b6..eb0ce0a3 100644 --- a/nkebao/vite.config.ts +++ b/nkebao/vite.config.ts @@ -11,6 +11,7 @@ export default defineConfig({ }, server: { open: true, + port: 3000, }, build: { chunkSizeWarningLimit: 2000, From 6ed19f7a8457b473b97e6027c757b64502ff1463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Mon, 4 Aug 2025 10:34:59 +0800 Subject: [PATCH 17/93] =?UTF-8?q?FEAT=20=3D>=20=E6=9C=AC=E6=AC=A1=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=A1=B9=E7=9B=AE=E4=B8=BA=EF=BC=9A=20store=E6=8C=81?= =?UTF-8?q?=E4=B9=85=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/api/request.ts | 4 +- nkebao/src/store/README.md | 572 +++++++++++++++++++++++++ nkebao/src/store/createPersistStore.ts | 249 ++++++++++- nkebao/src/store/examples.ts | 325 ++++++++++++++ nkebao/src/store/index.ts | 77 +++- nkebao/src/store/module/app.ts | 13 +- nkebao/src/store/module/settings.ts | 13 +- nkebao/src/store/module/user.ts | 17 +- nkebao/src/store/persistUtils.ts | 424 ++++++++++++++++++ 9 files changed, 1675 insertions(+), 19 deletions(-) create mode 100644 nkebao/src/store/README.md create mode 100644 nkebao/src/store/examples.ts create mode 100644 nkebao/src/store/persistUtils.ts diff --git a/nkebao/src/api/request.ts b/nkebao/src/api/request.ts index 2235a552..6394d22e 100644 --- a/nkebao/src/api/request.ts +++ b/nkebao/src/api/request.ts @@ -5,7 +5,8 @@ import axios, { AxiosResponse, } from "axios"; import { Toast } from "antd-mobile"; - +import { useUserStore } from "@/store/module/user"; +const { token } = useUserStore.getState(); const DEFAULT_DEBOUNCE_GAP = 1000; const debounceMap = new Map(); @@ -18,7 +19,6 @@ const instance: AxiosInstance = axios.create({ }); instance.interceptors.request.use((config: any) => { - const token = localStorage.getItem("token"); if (token) { config.headers = config.headers || {}; config.headers["Authorization"] = `Bearer ${token}`; diff --git a/nkebao/src/store/README.md b/nkebao/src/store/README.md new file mode 100644 index 00000000..71345a4d --- /dev/null +++ b/nkebao/src/store/README.md @@ -0,0 +1,572 @@ +# Store 持久化使用指南 + +## 概述 + +本项目使用 Zustand 作为状态管理库,并实现了完整的持久化功能。所有 store 都支持数据持久化,确保用户数据在页面刷新后不会丢失。 + +## 🚀 新功能特性 + +### 高级持久化功能 + +- **数据压缩**: 减少存储空间占用 +- **数据加密**: 保护敏感数据安全 +- **TTL支持**: 自动过期机制 +- **批量操作**: 提高性能 +- **存储监控**: 实时监控存储使用情况 +- **数据迁移**: 版本管理和数据升级 +- **备份恢复**: 完整的数据备份和恢复功能 + +## 已实现的持久化 Store + +### 1. User Store (`user.ts`) + +- **存储内容**: 用户信息、登录状态、token +- **存储位置**: localStorage +- **持久化键**: `user-store` + +```typescript +import { useUserStore } from "@/store"; + +// 使用用户状态 +const { user, isLoggedIn, login, logout } = useUserStore(); + +// 登录 +login(token, userInfo, deviceTotal); + +// 登出 +logout(); +``` + +### 2. App Store (`app.ts`) + +- **存储内容**: 应用状态、主题设置、调试模式等 +- **存储位置**: localStorage +- **持久化键**: `app-store` + +```typescript +import { useAppStore } from "@/store"; + +// 使用应用状态 +const { app, setTheme, setDebugMode } = useAppStore(); + +// 设置主题 +setTheme("dark"); + +// 切换调试模式 +setDebugMode(true); +``` + +### 3. Settings Store (`settings.ts`) + +- **存储内容**: 应用设置、语言、时区等 +- **存储位置**: localStorage +- **持久化键**: `settings-store` + +```typescript +import { useSettingsStore } from "@/store"; + +// 使用设置状态 +const { settings, updateSetting } = useSettingsStore(); + +// 更新设置 +updateSetting("language", "en-US"); +``` + +## 创建新的持久化 Store + +### 基础持久化 Store + +```typescript +import { createPersistStore } from "@/store"; + +interface MyState { + data: any[]; + loading: boolean; + setData: (data: any[]) => void; + setLoading: (loading: boolean) => void; +} + +export const useMyStore = createPersistStore( + set => ({ + data: [], + loading: false, + setData: data => set({ data }), + setLoading: loading => set({ loading }), + }), + { + name: "my-store", + partialize: state => ({ + data: state.data, + }), + onRehydrateStorage: () => state => { + console.log("My store hydrated:", state); + }, + }, +); +``` + +### 便利函数创建 Store + +```typescript +import { + createLocalStorageStore, + createSessionStorageStore, + createEncryptedStore, + createCompressedStore, + createTTLStore, +} from "@/store"; + +// 创建 localStorage 持久化 store +export const useLocalStore = createLocalStorageStore( + set => ({ + // state and actions + }), + "local-store", + state => ({ + /* partialize */ + }), + { ttl: 24 * 60 * 60 * 1000 }, // 24小时TTL +); + +// 创建 sessionStorage 持久化 store +export const useSessionStore = createSessionStorageStore( + set => ({ + // state and actions + }), + "session-store", + state => ({ + /* partialize */ + }), +); + +// 创建加密持久化 store +export const useEncryptedStore = createEncryptedStore( + set => ({ + // state and actions + }), + "encrypted-store", + "your-secret-key", + state => ({ + /* partialize */ + }), +); + +// 创建压缩持久化 store +export const useCompressedStore = createCompressedStore( + set => ({ + // state and actions + }), + "compressed-store", + state => ({ + /* partialize */ + }), +); + +// 创建TTL持久化 store +export const useTTLStore = createTTLStore( + set => ({ + // state and actions + }), + "ttl-store", + 24 * 60 * 60 * 1000, // 24小时TTL + state => ({ + /* partialize */ + }), +); +``` + +## 高级持久化功能 + +### 数据压缩 + +```typescript +// 启用压缩以减少存储空间 +const useCompressedStore = createPersistStore(createState, { + name: "compressed-store", + compress: true, // 启用压缩 +}); +``` + +### 数据加密 + +```typescript +// 启用加密以保护敏感数据 +const useEncryptedStore = createPersistStore(createState, { + name: "encrypted-store", + encrypt: true, + encryptionKey: "your-secret-key", +}); +``` + +### TTL (Time To Live) + +```typescript +// 设置数据自动过期时间 +const useTTLStore = createPersistStore(createState, { + name: "ttl-store", + ttl: 24 * 60 * 60 * 1000, // 24小时后自动过期 +}); +``` + +### 数据迁移 + +```typescript +// 版本管理和数据迁移 +const useMigratedStore = createPersistStore(createState, { + name: "migrated-store", + version: 2, + migrate: (persistedState, version) => { + if (version === 1) { + // 从版本1迁移到版本2 + return { + ...persistedState, + newField: "default-value", + }; + } + return persistedState; + }, +}); +``` + +## 持久化工具函数 + +### 数据管理 + +```typescript +import { + getPersistedData, + setPersistedData, + removePersistedData, + clearAllPersistedData, + getStorageUsage, + cleanupExpiredData, +} from "@/store"; + +// 获取持久化数据 +const data = getPersistedData("user-store", "localStorage"); + +// 设置持久化数据(带配置) +setPersistedData("my-key", { value: "test" }, "localStorage", { + compress: true, + encrypt: true, + ttl: 24 * 60 * 60 * 1000, +}); + +// 移除持久化数据 +removePersistedData("my-key", "localStorage"); + +// 清除所有持久化数据 +clearAllPersistedData(); + +// 获取存储使用情况 +const usage = getStorageUsage("localStorage"); +console.log( + `使用: ${usage.used} bytes, 总计: ${usage.total} bytes, 使用率: ${usage.percentage}%`, +); + +// 清理过期数据 +const cleanedCount = cleanupExpiredData(); +console.log(`清理了 ${cleanedCount} 个过期数据`); +``` + +### 批量操作 + +```typescript +import { + batchSetPersistedData, + batchGetPersistedData, + batchMigrateData, +} from "@/store"; + +// 批量设置数据 +const results = batchSetPersistedData( + { + key1: { value: "data1" }, + key2: { value: "data2" }, + key3: { value: "data3" }, + }, + "localStorage", + { compress: true }, +); + +// 批量获取数据 +const data = batchGetPersistedData(["key1", "key2", "key3"], "localStorage"); + +// 批量迁移数据 +const migrationResults = batchMigrateData({ + "user-store": oldData => ({ ...oldData, version: "2.0" }), + "app-store": oldData => ({ ...oldData, newField: "default" }), +}); +``` + +### 数据迁移 + +```typescript +import { migratePersistedData } from "@/store"; + +// 迁移数据 +migratePersistedData("user-store", "localStorage", oldData => { + // 转换旧数据格式到新格式 + return { + ...oldData, + newField: "default-value", + }; +}); +``` + +### 数据备份和恢复 + +```typescript +import { + backupPersistedData, + restorePersistedData, + exportPersistedData, + importPersistedData, +} from "@/store"; + +// 备份所有持久化数据 +const backup = backupPersistedData(); + +// 恢复持久化数据 +restorePersistedData(backup); + +// 导出备份数据为JSON字符串 +const jsonData = exportPersistedData(); + +// 从JSON字符串导入备份数据 +const success = importPersistedData(jsonData); +``` + +## Store 状态管理 + +### 获取 Store 状态 + +```typescript +import { + getStores, + getUserStore, + getAppStore, + getSettingsStore, +} from "@/store"; + +// 获取所有store状态 +const allStores = getStores(); + +// 获取特定store状态 +const userState = getUserStore(); +const appState = getAppStore(); +const settingsState = getSettingsStore(); +``` + +### Store 订阅 + +```typescript +import { + subscribeToUserStore, + subscribeToAppStore, + subscribeToSettingsStore, + subscribeToAllStores, +} from "@/store"; + +// 订阅单个store +const unsubscribeUser = subscribeToUserStore(state => { + console.log("User store changed:", state); +}); + +// 订阅所有store +const unsubscribeAll = subscribeToAllStores(state => { + console.log("Any store changed:", state); +}); + +// 取消订阅 +unsubscribeUser(); +unsubscribeAll(); +``` + +## 配置选项 + +### PersistConfig 接口 + +```typescript +interface PersistConfig { + name: string; // 存储键名 + partialize?: (state: T) => any; // 选择性持久化 + storage?: Storage; // 存储类型 (localStorage/sessionStorage) + version?: number; // 版本号,用于数据迁移 + migrate?: (persistedState: any, version: number) => any; // 迁移函数 + onRehydrateStorage?: (state: any) => void; // 重新水合回调 + skipHydration?: boolean; // 跳过水合 + compress?: boolean; // 启用压缩 + encrypt?: boolean; // 启用加密 + ttl?: number; // 生存时间(毫秒) + encryptionKey?: string; // 加密密钥 +} +``` + +### StorageConfig 接口 + +```typescript +interface StorageConfig { + compress: boolean; // 是否压缩 + encrypt: boolean; // 是否加密 + ttl?: number; // 生存时间 +} +``` + +## 最佳实践 + +### 1. 选择性持久化 + +只持久化必要的数据,避免存储过大的状态: + +```typescript +partialize: state => ({ + user: state.user, + token: state.token, + // 不持久化临时状态 + // loading: state.loading, +}); +``` + +### 2. 数据迁移 + +当数据结构发生变化时,使用迁移函数: + +```typescript +{ + name: "user-store", + version: 2, + migrate: (persistedState, version) => { + if (version === 1) { + // 从版本1迁移到版本2 + return { + ...persistedState, + newField: "default-value", + }; + } + return persistedState; + }, +} +``` + +### 3. 错误处理 + +持久化操作会自动处理错误,但可以添加自定义错误处理: + +```typescript +onRehydrateStorage: () => state => { + if (state) { + console.log("Store hydrated successfully"); + } else { + console.warn("Failed to hydrate store"); + } +}, +``` + +### 4. 存储空间管理 + +定期清理不需要的持久化数据: + +```typescript +// 检查数据大小 +const size = getPersistedDataSize("user-store", "localStorage"); +if (size > 1024 * 1024) { + // 1MB + console.warn("Persisted data is too large"); +} + +// 监控存储使用情况 +const usage = getStorageUsage("localStorage"); +if (usage.percentage > 80) { + console.warn("Storage usage is high:", usage.percentage + "%"); +} +``` + +### 5. 性能优化 + +使用批量操作提高性能: + +```typescript +// 批量设置数据 +const results = batchSetPersistedData( + { + key1: data1, + key2: data2, + key3: data3, + }, + "localStorage", +); + +// 批量获取数据 +const data = batchGetPersistedData(["key1", "key2", "key3"], "localStorage"); +``` + +### 6. 安全考虑 + +对敏感数据使用加密: + +```typescript +// 加密敏感数据 +const useSensitiveStore = createEncryptedStore( + createState, + "sensitive-store", + "your-secret-key", + partialize, +); +``` + +## 注意事项 + +1. **存储限制**: localStorage 通常有 5-10MB 限制 +2. **同步操作**: 持久化操作是同步的,避免阻塞主线程 +3. **隐私模式**: 在隐私模式下,存储可能不可用 +4. **数据安全**: 敏感数据应该加密后再存储 +5. **版本管理**: 及时更新版本号,确保数据迁移正常工作 +6. **压缩权衡**: 压缩可以减少存储空间,但会增加CPU开销 +7. **TTL设置**: 合理设置TTL,避免数据过期影响用户体验 +8. **错误恢复**: 实现错误恢复机制,处理存储失败的情况 + +## 实际应用场景 + +### 电商购物车 + +```typescript +const useCartStore = createLocalStorageStore( + createCartState, + "cart-store", + state => ({ items: state.items, total: state.total }), + { ttl: 30 * 24 * 60 * 60 * 1000 }, // 30天TTL +); +``` + +### 用户偏好设置 + +```typescript +const usePreferencesStore = createEncryptedStore( + createPreferencesState, + "preferences-store", + "user-secret-key", + state => ({ theme: state.theme, language: state.language }), +); +``` + +### 临时会话数据 + +```typescript +const useSessionStore = createTTLStore( + createSessionState, + "session-store", + 24 * 60 * 60 * 1000, // 24小时TTL + state => ({ sessionId: state.sessionId, lastActivity: state.lastActivity }), +); +``` + +### 大数据缓存 + +```typescript +const useCacheStore = createCompressedStore( + createCacheState, + "cache-store", + state => ({ data: state.data, timestamp: state.timestamp }), +); +``` diff --git a/nkebao/src/store/createPersistStore.ts b/nkebao/src/store/createPersistStore.ts index 79d7b634..80242dee 100644 --- a/nkebao/src/store/createPersistStore.ts +++ b/nkebao/src/store/createPersistStore.ts @@ -2,15 +2,260 @@ import { create } from "zustand"; import { persist, PersistOptions } from "zustand/middleware"; +export interface PersistConfig { + name: string; + partialize?: (state: any) => any; + storage?: Storage; + version?: number; + migrate?: (persistedState: any, version: number) => any; + onRehydrateStorage?: (state: any) => void; + skipHydration?: boolean; + compress?: boolean; + encrypt?: boolean; + ttl?: number; // 生存时间(毫秒) + encryptionKey?: string; +} + +// 默认配置 +const DEFAULT_CONFIG = { + storage: localStorage, + version: 1, + skipHydration: false, + compress: false, + encrypt: false, +}; + +// 简单的数据压缩 +function compressData(data: any): string { + try { + const jsonString = JSON.stringify(data); + return btoa(encodeURIComponent(jsonString)); + } catch { + return JSON.stringify(data); + } +} + +// 简单的数据解压 +function decompressData(compressedData: string): any { + try { + const jsonString = decodeURIComponent(atob(compressedData)); + return JSON.parse(jsonString); + } catch { + return JSON.parse(compressedData); + } +} + +// 简单的数据加密 +function encryptData(data: string, key: string = "default-key"): string { + let result = ""; + for (let i = 0; i < data.length; i++) { + result += String.fromCharCode( + data.charCodeAt(i) ^ key.charCodeAt(i % key.length), + ); + } + return btoa(result); +} + +// 简单的数据解密 +function decryptData( + encryptedData: string, + key: string = "default-key", +): string { + try { + const data = atob(encryptedData); + let result = ""; + for (let i = 0; i < data.length; i++) { + result += String.fromCharCode( + data.charCodeAt(i) ^ key.charCodeAt(i % key.length), + ); + } + return result; + } catch { + return encryptedData; + } +} + +// 检查数据是否过期 +function isDataExpired(timestamp: number, ttl: number): boolean { + return Date.now() - timestamp > ttl; +} + export function createPersistStore( createState: (set: any, get: any) => T, - name: string, - partialize?: (state: T) => Partial, + config: PersistConfig, ) { + const { + name, + partialize, + storage = DEFAULT_CONFIG.storage, + version = DEFAULT_CONFIG.version, + migrate, + onRehydrateStorage, + skipHydration = DEFAULT_CONFIG.skipHydration, + compress = DEFAULT_CONFIG.compress, + encrypt = DEFAULT_CONFIG.encrypt, + ttl, + encryptionKey = "default-key", + } = config; + return create()( persist(createState, { name, partialize, + storage: { + getItem: (name: string) => { + try { + const item = storage.getItem(name); + if (!item) return null; + + let data: any; + try { + data = JSON.parse(item); + } catch { + return null; + } + + // 检查TTL + if (data.timestamp && ttl && isDataExpired(data.timestamp, ttl)) { + storage.removeItem(name); + return null; + } + + let value = data.value; + + // 解密 + if (encrypt && typeof value === "string") { + value = decryptData(value, encryptionKey); + } + + // 解压 + if (compress && typeof value === "string") { + value = decompressData(value); + } + + return value; + } catch (error) { + console.warn(`Failed to get item ${name} from storage:`, error); + return null; + } + }, + setItem: (name: string, value: any) => { + try { + let processedValue = value; + + // 压缩 + if (compress) { + processedValue = compressData(processedValue); + } + + // 加密 + if (encrypt && typeof processedValue === "string") { + processedValue = encryptData(processedValue, encryptionKey); + } + + const storageData = { + value: processedValue, + timestamp: Date.now(), + config: { compress, encrypt, ttl }, + }; + + storage.setItem(name, JSON.stringify(storageData)); + } catch (error) { + console.warn(`Failed to set item ${name} to storage:`, error); + } + }, + removeItem: (name: string) => { + try { + storage.removeItem(name); + } catch (error) { + console.warn(`Failed to remove item ${name} from storage:`, error); + } + }, + }, + version, + migrate, + onRehydrateStorage, + skipHydration, } as PersistOptions), ); } + +// 便利函数:创建localStorage持久化store +export function createLocalStorageStore( + createState: (set: any, get: any) => T, + name: string, + partialize?: (state: T) => Partial, + options?: Partial>, +) { + return createPersistStore(createState, { + name, + partialize, + storage: localStorage, + ...options, + }); +} + +// 便利函数:创建sessionStorage持久化store +export function createSessionStorageStore( + createState: (set: any, get: any) => T, + name: string, + partialize?: (state: T) => Partial, + options?: Partial>, +) { + return createPersistStore(createState, { + name, + partialize, + storage: sessionStorage, + ...options, + }); +} + +// 便利函数:创建加密持久化store +export function createEncryptedStore( + createState: (set: any, get: any) => T, + name: string, + encryptionKey: string, + partialize?: (state: T) => Partial, + options?: Partial< + Omit + >, +) { + return createPersistStore(createState, { + name, + partialize, + encrypt: true, + encryptionKey, + ...options, + }); +} + +// 便利函数:创建压缩持久化store +export function createCompressedStore( + createState: (set: any, get: any) => T, + name: string, + partialize?: (state: T) => Partial, + options?: Partial>, +) { + return createPersistStore(createState, { + name, + partialize, + compress: true, + ...options, + }); +} + +// 便利函数:创建带TTL的持久化store +export function createTTLStore( + createState: (set: any, get: any) => T, + name: string, + ttl: number, // 生存时间(毫秒) + partialize?: (state: T) => Partial, + options?: Partial>, +) { + return createPersistStore(createState, { + name, + partialize, + ttl, + ...options, + }); +} diff --git a/nkebao/src/store/examples.ts b/nkebao/src/store/examples.ts new file mode 100644 index 00000000..99ff48ee --- /dev/null +++ b/nkebao/src/store/examples.ts @@ -0,0 +1,325 @@ +// src/store/examples.ts +// 高级持久化使用示例 + +import { + createLocalStorageStore, + createSessionStorageStore, + createEncryptedStore, + createCompressedStore, + createTTLStore, + createPersistStore, +} from "./createPersistStore"; + +// 示例1: 基础localStorage持久化store +interface BasicState { + count: number; + name: string; + increment: () => void; + setName: (name: string) => void; +} + +export const useBasicStore = createLocalStorageStore( + set => ({ + count: 0, + name: "", + increment: () => set(state => ({ count: state.count + 1 })), + setName: name => set({ name }), + }), + "basic-store", + state => ({ count: state.count, name: state.name }), // 只持久化数据,不持久化方法 +); + +// 示例2: sessionStorage持久化store(会话级存储) +interface SessionState { + sessionId: string; + lastActivity: number; + updateActivity: () => void; +} + +export const useSessionStore = createSessionStorageStore( + set => ({ + sessionId: "", + lastActivity: Date.now(), + updateActivity: () => set({ lastActivity: Date.now() }), + }), + "session-store", + state => ({ sessionId: state.sessionId, lastActivity: state.lastActivity }), +); + +// 示例3: 加密持久化store(敏感数据) +interface SensitiveState { + apiKey: string; + privateData: any; + setApiKey: (key: string) => void; + setPrivateData: (data: any) => void; +} + +export const useSensitiveStore = createEncryptedStore( + set => ({ + apiKey: "", + privateData: null, + setApiKey: key => set({ apiKey: key }), + setPrivateData: data => set({ privateData: data }), + }), + "sensitive-store", + "your-secret-key-here", // 加密密钥 + state => ({ apiKey: state.apiKey, privateData: state.privateData }), +); + +// 示例4: 压缩持久化store(大数据) +interface LargeDataState { + largeArray: number[]; + largeObject: Record; + addToArray: (item: number) => void; + updateObject: (key: string, value: any) => void; +} + +export const useLargeDataStore = createCompressedStore( + set => ({ + largeArray: [], + largeObject: {}, + addToArray: item => + set(state => ({ largeArray: [...state.largeArray, item] })), + updateObject: (key, value) => + set(state => ({ largeObject: { ...state.largeObject, [key]: value } })), + }), + "large-data-store", + state => ({ largeArray: state.largeArray, largeObject: state.largeObject }), +); + +// 示例5: TTL持久化store(临时数据,自动过期) +interface TemporaryState { + tempData: any; + expiresAt: number; + setTempData: (data: any, ttlMs: number) => void; +} + +export const useTemporaryStore = createTTLStore( + set => ({ + tempData: null, + expiresAt: 0, + setTempData: (data, ttlMs) => + set({ tempData: data, expiresAt: Date.now() + ttlMs }), + }), + "temporary-store", + 24 * 60 * 60 * 1000, // 24小时TTL + state => ({ tempData: state.tempData, expiresAt: state.expiresAt }), +); + +// 示例6: 高级配置持久化store +interface AdvancedState { + complexData: { + nested: { + deep: { + value: string; + }; + }; + array: any[]; + }; + metadata: { + version: string; + createdAt: number; + updatedAt: number; + }; + updateComplexData: (data: any) => void; + updateMetadata: (metadata: any) => void; +} + +export const useAdvancedStore = createPersistStore( + set => ({ + complexData: { + nested: { + deep: { + value: "", + }, + }, + array: [], + }, + metadata: { + version: "1.0.0", + createdAt: Date.now(), + updatedAt: Date.now(), + }, + updateComplexData: data => + set(state => ({ + complexData: { ...state.complexData, ...data }, + metadata: { ...state.metadata, updatedAt: Date.now() }, + })), + updateMetadata: metadata => + set(state => ({ + metadata: { ...state.metadata, ...metadata, updatedAt: Date.now() }, + })), + }), + { + name: "advanced-store", + partialize: state => ({ + complexData: state.complexData, + metadata: state.metadata, + }), + version: 2, + migrate: (persistedState, version) => { + if (version === 1) { + // 从版本1迁移到版本2 + return { + ...persistedState, + metadata: { + version: "2.0.0", + createdAt: Date.now(), + updatedAt: Date.now(), + }, + }; + } + return persistedState; + }, + onRehydrateStorage: () => state => { + console.log("Advanced store hydrated:", state); + }, + compress: true, // 启用压缩 + ttl: 7 * 24 * 60 * 60 * 1000, // 7天TTL + }, +); + +// 示例7: 购物车持久化store(电商场景) +interface CartState { + items: Array<{ + id: string; + name: string; + price: number; + quantity: number; + }>; + total: number; + addItem: (item: any) => void; + removeItem: (id: string) => void; + updateQuantity: (id: string, quantity: number) => void; + clearCart: () => void; + calculateTotal: () => void; +} + +export const useCartStore = createLocalStorageStore( + (set, get) => ({ + items: [], + total: 0, + addItem: item => { + const state = get(); + const existingItem = state.items.find(i => i.id === item.id); + + if (existingItem) { + set({ + items: state.items.map(i => + i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i, + ), + }); + } else { + set({ items: [...state.items, { ...item, quantity: 1 }] }); + } + + get().calculateTotal(); + }, + removeItem: id => { + const state = get(); + set({ items: state.items.filter(i => i.id !== id) }); + get().calculateTotal(); + }, + updateQuantity: (id, quantity) => { + const state = get(); + set({ + items: state.items.map(i => + i.id === id ? { ...i, quantity: Math.max(0, quantity) } : i, + ), + }); + get().calculateTotal(); + }, + clearCart: () => set({ items: [], total: 0 }), + calculateTotal: () => { + const state = get(); + const total = state.items.reduce( + (sum, item) => sum + item.price * item.quantity, + 0, + ); + set({ total }); + }, + }), + "cart-store", + state => ({ items: state.items, total: state.total }), + { + ttl: 30 * 24 * 60 * 60 * 1000, // 30天TTL + }, +); + +// 示例8: 用户偏好设置store +interface PreferencesState { + theme: "light" | "dark" | "auto"; + language: string; + notifications: { + email: boolean; + push: boolean; + sms: boolean; + }; + privacy: { + shareData: boolean; + analytics: boolean; + }; + setTheme: (theme: "light" | "dark" | "auto") => void; + setLanguage: (language: string) => void; + toggleNotification: (type: keyof PreferencesState["notifications"]) => void; + updatePrivacy: (settings: Partial) => void; +} + +export const usePreferencesStore = createEncryptedStore( + set => ({ + theme: "auto", + language: "zh-CN", + notifications: { + email: true, + push: true, + sms: false, + }, + privacy: { + shareData: false, + analytics: true, + }, + setTheme: theme => set({ theme }), + setLanguage: language => set({ language }), + toggleNotification: type => + set(state => ({ + notifications: { + ...state.notifications, + [type]: !state.notifications[type], + }, + })), + updatePrivacy: settings => + set(state => ({ + privacy: { ...state.privacy, ...settings }, + })), + }), + "preferences-store", + "user-preferences-key", + state => ({ + theme: state.theme, + language: state.language, + notifications: state.notifications, + privacy: state.privacy, + }), +); + +// 使用示例 +export function useStoreExamples() { + // 基础store使用 + const basic = useBasicStore(); + + // 敏感数据store使用 + const sensitive = useSensitiveStore(); + + // 购物车store使用 + const cart = useCartStore(); + + // 偏好设置store使用 + const preferences = usePreferencesStore(); + + return { + basic, + sensitive, + cart, + preferences, + }; +} diff --git a/nkebao/src/store/index.ts b/nkebao/src/store/index.ts index 4eca24e3..14263a67 100644 --- a/nkebao/src/store/index.ts +++ b/nkebao/src/store/index.ts @@ -1,2 +1,77 @@ +// 导出所有store模块 export * from "./module/user"; -// 未来可继续合并其他模块 +export * from "./module/app"; +export * from "./module/settings"; + +// 导入store实例 +import { useUserStore } from "./module/user"; +import { useAppStore } from "./module/app"; +import { useSettingsStore } from "./module/settings"; + +// 导出持久化store创建函数 +export { + createPersistStore, + createLocalStorageStore, + createSessionStorageStore, + createEncryptedStore, + createCompressedStore, + createTTLStore, +} from "./createPersistStore"; + +// 导出持久化工具函数 +export * from "./persistUtils"; + +// 导入工具函数 +import { + clearAllPersistedData as clearAllData, + PERSIST_KEYS, +} from "./persistUtils"; + +// Store类型定义 +export interface StoreState { + user: ReturnType; + app: ReturnType; + settings: ReturnType; +} + +// 便利的store访问函数 +export const getStores = (): StoreState => ({ + user: useUserStore.getState(), + app: useAppStore.getState(), + settings: useSettingsStore.getState(), +}); + +// 获取特定store状态 +export const getUserStore = () => useUserStore.getState(); +export const getAppStore = () => useAppStore.getState(); +export const getSettingsStore = () => useSettingsStore.getState(); + +// 清除所有持久化数据(使用工具函数) +export const clearAllPersistedData = clearAllData; + +// 获取所有持久化键名 +export const getPersistKeys = () => Object.values(PERSIST_KEYS); + +// Store状态监听器 +export const subscribeToUserStore = useUserStore.subscribe; +export const subscribeToAppStore = useAppStore.subscribe; +export const subscribeToSettingsStore = useSettingsStore.subscribe; + +// 组合订阅函数 +export const subscribeToAllStores = (callback: (state: StoreState) => void) => { + const unsubscribeUser = useUserStore.subscribe(() => { + callback(getStores()); + }); + const unsubscribeApp = useAppStore.subscribe(() => { + callback(getStores()); + }); + const unsubscribeSettings = useSettingsStore.subscribe(() => { + callback(getStores()); + }); + + return () => { + unsubscribeUser(); + unsubscribeApp(); + unsubscribeSettings(); + }; +}; diff --git a/nkebao/src/store/module/app.ts b/nkebao/src/store/module/app.ts index 9b6e73a4..e0eaa0af 100644 --- a/nkebao/src/store/module/app.ts +++ b/nkebao/src/store/module/app.ts @@ -77,10 +77,15 @@ export const useAppStore = createPersistStore( resetAppState: () => set({ app: defaultAppState }), }), - "app-store", - state => ({ - app: state.app, - }), + { + name: "app-store", + partialize: state => ({ + app: state.app, + }), + onRehydrateStorage: () => state => { + console.log("App store hydrated:", state); + }, + }, ); // 应用状态工具函数 diff --git a/nkebao/src/store/module/settings.ts b/nkebao/src/store/module/settings.ts index af884e36..ad971d84 100644 --- a/nkebao/src/store/module/settings.ts +++ b/nkebao/src/store/module/settings.ts @@ -50,10 +50,15 @@ export const useSettingsStore = createPersistStore( settings: { ...state.settings, [key]: value }, })), }), - "settings-store", - state => ({ - settings: state.settings, - }), + { + name: "settings-store", + partialize: state => ({ + settings: state.settings, + }), + onRehydrateStorage: () => state => { + // console.log("Settings store hydrated:", state); + }, + }, ); // 设置工具函数 diff --git a/nkebao/src/store/module/user.ts b/nkebao/src/store/module/user.ts index bee4f4c6..ae4f66d2 100644 --- a/nkebao/src/store/module/user.ts +++ b/nkebao/src/store/module/user.ts @@ -67,10 +67,15 @@ export const useUserStore = createPersistStore( set({ user: null, token: null, isLoggedIn: false }); }, }), - "user-store", - state => ({ - user: state.user, - token: state.token, - isLoggedIn: state.isLoggedIn, - }), + { + name: "user-store", + partialize: state => ({ + user: state.user, + token: state.token, + isLoggedIn: state.isLoggedIn, + }), + onRehydrateStorage: () => state => { + // console.log("User store hydrated:", state); + }, + }, ); diff --git a/nkebao/src/store/persistUtils.ts b/nkebao/src/store/persistUtils.ts new file mode 100644 index 00000000..abfb1e8c --- /dev/null +++ b/nkebao/src/store/persistUtils.ts @@ -0,0 +1,424 @@ +// src/store/persistUtils.ts + +// 持久化存储键名常量 +export const PERSIST_KEYS = { + USER_STORE: "user-store", + APP_STORE: "app-store", + SETTINGS_STORE: "settings-store", +} as const; + +// 存储类型 +export type StorageType = "localStorage" | "sessionStorage"; + +// 持久化配置接口 +export interface PersistConfig { + key: string; + storage: StorageType; + version?: number; + migrate?: (persistedState: any, version: number) => any; + compress?: boolean; + encrypt?: boolean; + ttl?: number; // 生存时间(毫秒) +} + +// 存储配置 +export interface StorageConfig { + compress: boolean; + encrypt: boolean; + ttl?: number; +} + +// 默认配置 +const DEFAULT_CONFIG: StorageConfig = { + compress: false, + encrypt: false, +}; + +// 获取存储实例 +export function getStorage(type: StorageType): Storage { + return type === "localStorage" ? localStorage : sessionStorage; +} + +// 检查存储是否可用 +export function isStorageAvailable(type: StorageType): boolean { + try { + const storage = getStorage(type); + const testKey = "__storage_test__"; + storage.setItem(testKey, "test"); + storage.removeItem(testKey); + return true; + } catch { + return false; + } +} + +// 简单的数据压缩(Base64编码) +function compressData(data: any): string { + try { + const jsonString = JSON.stringify(data); + return btoa(encodeURIComponent(jsonString)); + } catch { + return JSON.stringify(data); + } +} + +// 简单的数据解压 +function decompressData(compressedData: string): any { + try { + const jsonString = decodeURIComponent(atob(compressedData)); + return JSON.parse(jsonString); + } catch { + return JSON.parse(compressedData); + } +} + +// 简单的数据加密(XOR加密) +function encryptData(data: string, key: string = "default-key"): string { + let result = ""; + for (let i = 0; i < data.length; i++) { + result += String.fromCharCode( + data.charCodeAt(i) ^ key.charCodeAt(i % key.length), + ); + } + return btoa(result); +} + +// 简单的数据解密 +function decryptData( + encryptedData: string, + key: string = "default-key", +): string { + try { + const data = atob(encryptedData); + let result = ""; + for (let i = 0; i < data.length; i++) { + result += String.fromCharCode( + data.charCodeAt(i) ^ key.charCodeAt(i % key.length), + ); + } + return result; + } catch { + return encryptedData; + } +} + +// 检查数据是否过期 +function isDataExpired(timestamp: number, ttl: number): boolean { + return Date.now() - timestamp > ttl; +} + +// 安全地获取持久化数据 +export function getPersistedData( + key: string, + storage: StorageType, + config: Partial = {}, +): T | null { + try { + if (!isStorageAvailable(storage)) { + console.warn(`Storage ${storage} is not available`); + return null; + } + + const storageInstance = getStorage(storage); + const item = storageInstance.getItem(key); + + if (!item) return null; + + const finalConfig = { ...DEFAULT_CONFIG, ...config }; + let data: any; + + // 解析数据 + try { + data = JSON.parse(item); + } catch { + return null; + } + + // 检查TTL + if ( + data.timestamp && + finalConfig.ttl && + isDataExpired(data.timestamp, finalConfig.ttl) + ) { + removePersistedData(key, storage); + return null; + } + + let value = data.value; + + // 解密 + if (finalConfig.encrypt && typeof value === "string") { + value = decryptData(value); + } + + // 解压 + if (finalConfig.compress && typeof value === "string") { + value = decompressData(value); + } + + return value; + } catch (error) { + console.warn(`Failed to get persisted data for key ${key}:`, error); + return null; + } +} + +// 安全地设置持久化数据 +export function setPersistedData( + key: string, + data: T, + storage: StorageType, + config: Partial = {}, +): boolean { + try { + if (!isStorageAvailable(storage)) { + console.warn(`Storage ${storage} is not available`); + return false; + } + + const storageInstance = getStorage(storage); + const finalConfig = { ...DEFAULT_CONFIG, ...config }; + + let value: any = data; + + // 压缩 + if (finalConfig.compress) { + value = compressData(value); + } + + // 加密 + if (finalConfig.encrypt && typeof value === "string") { + value = encryptData(value); + } + + const storageData = { + value, + timestamp: Date.now(), + config: finalConfig, + }; + + storageInstance.setItem(key, JSON.stringify(storageData)); + return true; + } catch (error) { + console.warn(`Failed to set persisted data for key ${key}:`, error); + return false; + } +} + +// 安全地移除持久化数据 +export function removePersistedData( + key: string, + storage: StorageType, +): boolean { + try { + if (!isStorageAvailable(storage)) { + console.warn(`Storage ${storage} is not available`); + return false; + } + + const storageInstance = getStorage(storage); + storageInstance.removeItem(key); + return true; + } catch (error) { + console.warn(`Failed to remove persisted data for key ${key}:`, error); + return false; + } +} + +// 清除所有持久化数据 +export function clearAllPersistedData(): void { + Object.values(PERSIST_KEYS).forEach(key => { + removePersistedData(key, "localStorage"); + removePersistedData(key, "sessionStorage"); + }); +} + +// 获取持久化数据大小 +export function getPersistedDataSize( + key: string, + storage: StorageType, +): number { + try { + const data = getPersistedData(key, storage); + return data ? JSON.stringify(data).length : 0; + } catch { + return 0; + } +} + +// 检查持久化数据是否存在 +export function hasPersistedData(key: string, storage: StorageType): boolean { + try { + const storageInstance = getStorage(storage); + return storageInstance.getItem(key) !== null; + } catch { + return false; + } +} + +// 获取所有持久化键 +export function getAllPersistedKeys(storage: StorageType): string[] { + try { + const storageInstance = getStorage(storage); + return Object.keys(storageInstance).filter(key => + Object.values(PERSIST_KEYS).includes(key as any), + ); + } catch { + return []; + } +} + +// 获取存储使用情况 +export function getStorageUsage(storage: StorageType): { + used: number; + total: number; + percentage: number; +} { + try { + const storageInstance = getStorage(storage); + let used = 0; + + // 计算已使用空间 + for (let i = 0; i < storageInstance.length; i++) { + const key = storageInstance.key(i); + if (key) { + used += storageInstance.getItem(key)?.length || 0; + } + } + + // 估算总空间(localStorage通常为5-10MB) + const total = + storage === "localStorage" ? 5 * 1024 * 1024 : 5 * 1024 * 1024; + const percentage = (used / total) * 100; + + return { used, total, percentage }; + } catch { + return { used: 0, total: 0, percentage: 0 }; + } +} + +// 清理过期数据 +export function cleanupExpiredData(): number { + let cleanedCount = 0; + + Object.values(PERSIST_KEYS).forEach(key => { + ["localStorage", "sessionStorage"].forEach(storageType => { + const data = getPersistedData(key, storageType as StorageType); + if (data === null) { + cleanedCount++; + } + }); + }); + + return cleanedCount; +} + +// 持久化数据迁移工具 +export function migratePersistedData( + key: string, + storage: StorageType, + migrateFn: (data: any) => any, +): boolean { + try { + const data = getPersistedData(key, storage); + if (data) { + const migratedData = migrateFn(data); + return setPersistedData(key, migratedData, storage); + } + return false; + } catch (error) { + console.warn(`Failed to migrate persisted data for key ${key}:`, error); + return false; + } +} + +// 批量迁移数据 +export function batchMigrateData( + migrationMap: Record any>, + storage: StorageType = "localStorage", +): Record { + const results: Record = {}; + + Object.entries(migrationMap).forEach(([key, migrateFn]) => { + results[key] = migratePersistedData(key, storage, migrateFn); + }); + + return results; +} + +// 持久化数据备份和恢复 +export function backupPersistedData(): Record { + const backup: Record = {}; + + Object.values(PERSIST_KEYS).forEach(key => { + const localData = getPersistedData(key, "localStorage"); + const sessionData = getPersistedData(key, "sessionStorage"); + + if (localData || sessionData) { + backup[key] = { + localStorage: localData, + sessionStorage: sessionData, + }; + } + }); + + return backup; +} + +export function restorePersistedData(backup: Record): void { + Object.entries(backup).forEach(([key, data]) => { + if (data.localStorage) { + setPersistedData(key, data.localStorage, "localStorage"); + } + if (data.sessionStorage) { + setPersistedData(key, data.sessionStorage, "sessionStorage"); + } + }); +} + +// 导出备份数据 +export function exportPersistedData(): string { + const backup = backupPersistedData(); + return JSON.stringify(backup, null, 2); +} + +// 导入备份数据 +export function importPersistedData(jsonData: string): boolean { + try { + const backup = JSON.parse(jsonData); + restorePersistedData(backup); + return true; + } catch (error) { + console.warn("Failed to import persisted data:", error); + return false; + } +} + +// 性能优化的批量操作 +export function batchSetPersistedData( + dataMap: Record, + storage: StorageType = "localStorage", + config: Partial = {}, +): Record { + const results: Record = {}; + + Object.entries(dataMap).forEach(([key, data]) => { + results[key] = setPersistedData(key, data, storage, config); + }); + + return results; +} + +export function batchGetPersistedData( + keys: string[], + storage: StorageType = "localStorage", + config: Partial = {}, +): Record { + const results: Record = {}; + + keys.forEach(key => { + results[key] = getPersistedData(key, storage, config); + }); + + return results; +} From 02a42dd4b2681bdad06136fe5bfeb7dfb1ec5816 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Mon, 4 Aug 2025 10:35:35 +0800 Subject: [PATCH 18/93] =?UTF-8?q?FEAT=20=3D>=20=E6=9C=AC=E6=AC=A1=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=A1=B9=E7=9B=AE=E4=B8=BA=EF=BC=9A=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E7=8E=AF=E5=A2=83=E5=8F=98=E9=87=8F=E5=8F=8A=E6=A0=87=E9=A2=98?= =?UTF-8?q?=E4=B8=BA=E2=80=9C=E5=AD=98=E5=AE=A2=E5=AE=9D=E2=80=9D=EF=BC=8C?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=BA=94=E7=94=A8=E6=A8=A1=E5=BC=8F=E6=A0=87?= =?UTF-8?q?=E8=AF=86=EF=BC=8C=E4=BC=98=E5=8C=96iOS=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E5=AE=89=E5=85=A8=E5=8C=BA=E5=9F=9F=E9=AB=98=E5=BA=A6=E8=AE=A1?= =?UTF-8?q?=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/.env.development | 2 +- nkebao/.env.production | 3 +-- nkebao/index.html | 2 +- nkebao/src/pages/iframe/init.tsx | 1 + nkebao/src/utils/common.ts | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/nkebao/.env.development b/nkebao/.env.development index 9ac98215..7afdd84c 100644 --- a/nkebao/.env.development +++ b/nkebao/.env.development @@ -1,4 +1,4 @@ # 基础环境变量示例 # VITE_API_BASE_URL=http://www.yishi.com VITE_API_BASE_URL=https://ckbapi.quwanzhi.com -VITE_APP_TITLE=Nkebao Base +VITE_APP_TITLE=存客宝 diff --git a/nkebao/.env.production b/nkebao/.env.production index fe189d22..1c009a77 100644 --- a/nkebao/.env.production +++ b/nkebao/.env.production @@ -1,4 +1,3 @@ # 基础环境变量示例 VITE_API_BASE_URL=https://ckbapi.quwanzhi.com -VITE_APP_TITLE=Nkebao Base - +VITE_APP_TITLE=存客宝 diff --git a/nkebao/index.html b/nkebao/index.html index 22f819b6..92ab92a7 100644 --- a/nkebao/index.html +++ b/nkebao/index.html @@ -3,7 +3,7 @@ - Nkebao Base + 存客宝 + + ); +}; + +export default UpdateNotification; diff --git a/nkebao/src/pages/iframe/init.tsx b/nkebao/src/pages/iframe/init.tsx index 51185a38..b9e1fda0 100644 --- a/nkebao/src/pages/iframe/init.tsx +++ b/nkebao/src/pages/iframe/init.tsx @@ -4,7 +4,13 @@ import Layout from "@/components/Layout/Layout"; import NavCommon from "@/components/NavCommon"; import { Input } from "antd"; import { useNavigate } from "react-router-dom"; - +import { useSettingsStore } from "@/store/module/settings"; +import { + sendMessageToParent, + parseUrlMessage, + Message, + TYPE_EMUE, +} from "@/utils/postApp"; // 声明全局的 uni 对象 declare global { interface Window { @@ -12,55 +18,18 @@ declare global { } } -interface Message { - type: number; // 数据类型:0数据交互 1App功能调用 - data: any; -} - -const TYPE_EMUE = { - CONNECT: 0, - DATA: 1, - FUNCTION: 2, - CONFIG: 3, -}; const IframeDebugPage: React.FC = () => { + const { setSettings } = useSettingsStore(); const [receivedMessages, setReceivedMessages] = useState([]); const [messageId, setMessageId] = useState(0); const [inputMessage, setInputMessage] = useState(""); const navigate = useNavigate(); // 解析 URL 参数中的消息 - const parseUrlMessage = () => { - const search = window.location.search.substring(1); - let messageParam = null; - - if (search) { - const pairs = search.split("&"); - for (const pair of pairs) { - const [key, value] = pair.split("="); - if (key === "message" && value) { - messageParam = decodeURIComponent(value); - break; - } - } + parseUrlMessage().then(message => { + if (message) { + handleReceivedMessage(message); } - - if (messageParam) { - try { - const message = JSON.parse(decodeURIComponent(messageParam)); - console.log("[存客宝]ReceiveMessage=>\n" + JSON.stringify(message)); - handleReceivedMessage(message); - // 清除URL中的message参数 - const newUrl = - window.location.pathname + - window.location.search - .replace(/[?&]message=[^&]*/, "") - .replace(/^&/, "?"); - window.history.replaceState({}, "", newUrl); - } catch (e) { - console.error("解析URL消息失败:", e); - } - } - }; + }); useEffect(() => { parseUrlMessage(); @@ -71,35 +40,19 @@ const IframeDebugPage: React.FC = () => { const handleReceivedMessage = (message: Message) => { const messageText = `[${new Date().toLocaleTimeString()}] 收到: ${JSON.stringify(message)}`; setReceivedMessages(prev => [...prev, messageText]); - console.log("message.type", message.type); if ([TYPE_EMUE.CONFIG].includes(message.type)) { - localStorage.setItem("paddingTop", message.data.paddingTop); - localStorage.setItem("isAppMode", "true"); + const { paddingTop, appId, appName, appVersion } = message.data; + setSettings({ + paddingTop, + appId, + appName, + appVersion, + isAppMode: true, + }); navigate("/"); } }; - // 向 App 发送消息 - const sendMessageToParent = (message: Message) => { - if (window.uni && window.uni.postMessage) { - try { - window.uni.postMessage({ - data: message, - }); - console.log("[存客宝]SendMessage=>\n" + JSON.stringify(message)); - } catch (e) { - console.error( - "[存客宝]SendMessage=>\n" + JSON.stringify(message) + "发送失败:", - e, - ); - } - } else { - console.error( - "[存客宝]SendMessage=>\n" + JSON.stringify(message) + "无法发送消息", - ); - } - }; - // 发送自定义消息到 App const sendCustomMessage = () => { if (!inputMessage.trim()) return; @@ -107,17 +60,14 @@ const IframeDebugPage: React.FC = () => { const newMessageId = messageId + 1; setMessageId(newMessageId); - const message: Message = { - type: TYPE_EMUE.DATA, // 数据交互 - data: { - id: newMessageId, - content: inputMessage, - source: "存客宝消息源", - timestamp: Date.now(), - }, + const message = { + id: newMessageId, + content: inputMessage, + source: "存客宝消息源", + timestamp: Date.now(), }; - sendMessageToParent(message); + sendMessageToParent(message, TYPE_EMUE.DATA); setInputMessage(""); }; @@ -126,33 +76,27 @@ const IframeDebugPage: React.FC = () => { const newMessageId = messageId + 1; setMessageId(newMessageId); - const message: Message = { - type: TYPE_EMUE.DATA, // 数据交互 - data: { - id: newMessageId, - action: "ping", - content: `存客宝测试消息 ${newMessageId}`, - random: Math.random(), - }, + const message = { + id: newMessageId, + action: "ping", + content: `存客宝测试消息 ${newMessageId}`, + random: Math.random(), }; - sendMessageToParent(message); + sendMessageToParent(message, TYPE_EMUE.DATA); }; // 发送App功能调用消息 const sendAppFunctionCall = () => { - const message: Message = { - type: 1, // App功能调用 - data: { - action: "showToast", - params: { - title: "来自H5的功能调用", - icon: "success", - }, + const message = { + action: "showToast", + params: { + title: "来自H5的功能调用", + icon: "success", }, }; - sendMessageToParent(message); + sendMessageToParent(message, TYPE_EMUE.FUNCTION); }; // 清空消息列表 diff --git a/nkebao/src/pages/login/login.tsx b/nkebao/src/pages/login/login.tsx index c9d83114..6617e26b 100644 --- a/nkebao/src/pages/login/login.tsx +++ b/nkebao/src/pages/login/login.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from "react"; -import { useNavigate, useSearchParams } from "react-router-dom"; + import { Form, Input, Button, Toast, Checkbox } from "antd-mobile"; import { EyeInvisibleOutline, @@ -9,8 +9,6 @@ import { import { useUserStore } from "@/store/module/user"; import { loginWithPassword, loginWithCode, sendVerificationCode } from "./api"; import style from "./login.module.scss"; -import Layout from "@/components/Layout/Layout"; -import NavCommon from "@/components/NavCommon"; const Login: React.FC = () => { const [form] = Form.useForm(); @@ -20,8 +18,6 @@ const Login: React.FC = () => { const [showPassword, setShowPassword] = useState(false); const [agreeToTerms, setAgreeToTerms] = useState(false); - const navigate = useNavigate(); - const [searchParams] = useSearchParams(); const { login } = useUserStore(); // 倒计时效果 @@ -32,16 +28,6 @@ const Login: React.FC = () => { } }, [countdown]); - // 检查URL是否为登录页面 - const isLoginPage = (url: string) => { - try { - const urlObj = new URL(url, window.location.origin); - return urlObj.pathname === "/login" || urlObj.pathname.endsWith("/login"); - } catch { - return false; - } - }; - // 发送验证码 const handleSendVerificationCode = async () => { const account = form.getFieldValue("account"); @@ -95,24 +81,12 @@ const Login: React.FC = () => { } else { response = await loginWithCode(loginParams); } - console.log(response, "response"); // 获取设备总数 const deviceTotal = response.deviceTotal || 0; - console.log(deviceTotal, "deviceTotal"); // 更新状态管理(token会自动存储到localStorage,用户信息存储在状态管理中) login(response.token, response.member, deviceTotal); - - Toast.show({ content: "登录成功", position: "top" }); - - // 根据设备数量判断跳转 - if (deviceTotal > 0) { - navigate("/"); - } else { - // 没有设备,跳转到引导页面 - navigate("/guide"); - } } catch (error: any) { // 错误已在request中处理,这里不需要额外处理 } finally { diff --git a/nkebao/src/pages/mobile/home/index.tsx b/nkebao/src/pages/mobile/home/index.tsx index 3293455c..60244b37 100644 --- a/nkebao/src/pages/mobile/home/index.tsx +++ b/nkebao/src/pages/mobile/home/index.tsx @@ -18,6 +18,7 @@ import { getDashboard, } from "./api"; import style from "./index.module.scss"; +import UpdateNotification from "@/components/UpdateNotification"; interface DashboardData { deviceNum?: number; @@ -253,6 +254,7 @@ const Home: React.FC = () => { + ); }; diff --git a/nkebao/src/pages/mobile/mine/devices/index.tsx b/nkebao/src/pages/mobile/mine/devices/index.tsx index 444cfea3..6778bc7c 100644 --- a/nkebao/src/pages/mobile/mine/devices/index.tsx +++ b/nkebao/src/pages/mobile/mine/devices/index.tsx @@ -250,7 +250,7 @@ const Devices: React.FC = () => { } footer={ -
+
{
- logo + logo
diff --git a/nkebao/src/pages/mobile/mine/setting/index.tsx b/nkebao/src/pages/mobile/mine/setting/index.tsx index 8090bdc2..a15424bf 100644 --- a/nkebao/src/pages/mobile/mine/setting/index.tsx +++ b/nkebao/src/pages/mobile/mine/setting/index.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; -import { NavBar, List, Switch, Button, Dialog, Toast, Card } from "antd-mobile"; +import { List, Switch, Button, Dialog, Toast, Card } from "antd-mobile"; import { UserOutlined, SafetyOutlined, @@ -8,14 +8,16 @@ import { LogoutOutlined, SettingOutlined, LockOutlined, - HeartOutlined, - StarOutlined, + ReloadOutlined, } from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; import { useUserStore } from "@/store/module/user"; import { useSettingsStore } from "@/store/module/settings"; import style from "./index.module.scss"; import NavCommon from "@/components/NavCommon"; +import { sendMessageToParent, TYPE_EMUE } from "@/utils/postApp"; +import { updateChecker } from "@/utils/updateChecker"; + interface SettingItem { id: string; title: string; @@ -32,7 +34,7 @@ interface SettingItem { const Setting: React.FC = () => { const navigate = useNavigate(); const { user, logout } = useUserStore(); - const { settings, updateSetting } = useSettingsStore(); + const { settings } = useSettingsStore(); const [showLogoutDialog, setShowLogoutDialog] = useState(false); const [avatarError, setAvatarError] = useState(false); @@ -57,13 +59,30 @@ const Setting: React.FC = () => { Dialog.confirm({ content: "确定要清除缓存吗?这将清除所有本地数据。", onConfirm: () => { - localStorage.clear(); - sessionStorage.clear(); + sendMessageToParent( + { + action: "clearCache", + }, + TYPE_EMUE.FUNCTION, + ); + }, + }); + }; + + // 在设置页面添加手动检查更新功能 + const handleCheckUpdate = () => { + updateChecker.checkForUpdate().then(result => { + if (result.hasUpdate) { Toast.show({ - content: "缓存已清除", + content: "发现新版本,请刷新页面", position: "top", }); - }, + } else { + Toast.show({ + content: "当前已是最新版本", + position: "top", + }); + } }); }; @@ -114,6 +133,15 @@ const Setting: React.FC = () => { color: "var(--primary-color)", badge: "2.3MB", }, + { + id: "checkUpdate", + title: "检查更新", + description: "检查应用是否有新版本", + icon: , + type: "button", + onClick: handleCheckUpdate, + color: "var(--primary-color)", + }, ], }, { @@ -258,12 +286,14 @@ const Setting: React.FC = () => {
- +
存客宝
-
版本 3.0.0
-
Build 2025-7-30
+
+ 版本 {settings.appVersion} +
+
Build 2025-08-04
diff --git a/nkebao/src/pages/mobile/scenarios/plan/list/index.tsx b/nkebao/src/pages/mobile/scenarios/plan/list/index.tsx index cac548d8..230bf532 100644 --- a/nkebao/src/pages/mobile/scenarios/plan/list/index.tsx +++ b/nkebao/src/pages/mobile/scenarios/plan/list/index.tsx @@ -65,23 +65,6 @@ const ScenarioList: React.FC = () => { const [total, setTotal] = useState(0); const pageSize = 20; - // 获取渠道中文名称 - const getChannelName = (channel: string) => { - const channelMap: Record = { - douyin: "抖音直播获客", - kuaishou: "快手直播获客", - xiaohongshu: "小红书种草获客", - weibo: "微博话题获客", - haibao: "海报扫码获客", - phone: "电话号码获客", - gongzhonghao: "公众号引流获客", - weixinqun: "微信群裂变获客", - payment: "付款码获客", - api: "API接口获客", - }; - return channelMap[channel] || `${channel}获客`; - }; - // 获取计划列表数据 const fetchPlanList = async (page: number, isLoadMore: boolean = false) => { if (!scenarioId) return; @@ -409,7 +392,7 @@ const ScenarioList: React.FC = () => { } loading={loading} footer={ -
+
( diff --git a/nkebao/src/store/module/user.ts b/nkebao/src/store/module/user.ts index ae4f66d2..07838d87 100644 --- a/nkebao/src/store/module/user.ts +++ b/nkebao/src/store/module/user.ts @@ -1,4 +1,5 @@ import { createPersistStore } from "@/store/createPersistStore"; +import { Toast } from "antd-mobile"; export interface User { id: number; @@ -60,6 +61,16 @@ export const useUserStore = createPersistStore( deviceTotal: deviceTotal, }; set({ user, token, isLoggedIn: true }); + + Toast.show({ content: "登录成功", position: "top" }); + + // 根据设备数量判断跳转 + if (deviceTotal > 0) { + window.location.href = "/"; + } else { + // 没有设备,跳转到引导页面 + window.location.href = "/guide"; + } }, logout: () => { // 清除localStorage中的token diff --git a/nkebao/src/styles/global.scss b/nkebao/src/styles/global.scss index ca4e01d0..6e4995b9 100644 --- a/nkebao/src/styles/global.scss +++ b/nkebao/src/styles/global.scss @@ -264,3 +264,43 @@ button { align-items: center; gap: 6px; } +.pagination-container { + display: flex; + justify-content: center; + padding: 14px 0; + background: white; + border-radius: 12px; + margin-top: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + + :global(.ant-pagination) { + .ant-pagination-item { + border-radius: 6px; + border: 1px solid #d9d9d9; + + &:hover { + border-color: var(--primary-color); + } + + &.ant-pagination-item-active { + background: var(--primary-color); + border-color: var(--primary-color); + + a { + color: white; + } + } + } + + .ant-pagination-prev, + .ant-pagination-next { + border-radius: 6px; + border: 1px solid #d9d9d9; + + &:hover { + border-color: var(--primary-color); + color: var(--primary-color); + } + } + } +} diff --git a/nkebao/src/utils/common.ts b/nkebao/src/utils/common.ts index ebba36f1..995b9c05 100644 --- a/nkebao/src/utils/common.ts +++ b/nkebao/src/utils/common.ts @@ -1,5 +1,5 @@ import { Modal } from "antd-mobile"; - +import { getSetting } from "@/store/module/settings"; /** * 通用js调用弹窗,Promise风格 * @param content 弹窗内容 @@ -49,7 +49,7 @@ export function getSafeAreaHeight() { // 2. 设备检测 const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); const isAndroid = /Android/.test(navigator.userAgent); - const isAppMode = Boolean(localStorage.getItem("isAppMode")); + const isAppMode = getSetting("isAppMode"); if (isIOS && isAppMode) { // iOS 设备 const isIPhoneX = window.screen.height >= 812; diff --git a/nkebao/src/utils/postApp.ts b/nkebao/src/utils/postApp.ts new file mode 100644 index 00000000..0ed8ab2d --- /dev/null +++ b/nkebao/src/utils/postApp.ts @@ -0,0 +1,72 @@ +export interface Message { + type: number; // 数据类型:0数据交互 1App功能调用 + data: any; +} +export const TYPE_EMUE = { + CONNECT: 0, + DATA: 1, + FUNCTION: 2, + CONFIG: 3, +}; +// 向 App 发送消息 +export const sendMessageToParent = (message: any, type: number) => { + const params: Message = { + type: type, + data: message, + }; + + if (window.uni && window.uni.postMessage) { + try { + window.uni.postMessage({ + data: params, + }); + console.log("[存客宝]SendMessage=>\n" + JSON.stringify(params)); + } catch (e) { + console.error( + "[存客宝]SendMessage=>\n" + JSON.stringify(params) + "发送失败:", + e, + ); + } + } else { + console.error( + "[存客宝]SendMessage=>\n" + JSON.stringify(params) + "无法发送消息", + ); + } +}; +// 解析 URL 参数中的消息 +export const parseUrlMessage = (): Promise => { + return new Promise((resolve, reject) => { + const search = window.location.search.substring(1); + let messageParam = null; + + if (search) { + const pairs = search.split("&"); + for (const pair of pairs) { + const [key, value] = pair.split("="); + if (key === "message" && value) { + messageParam = decodeURIComponent(value); + break; + } + } + } + + if (messageParam) { + try { + const message = JSON.parse(decodeURIComponent(messageParam)); + console.log("[存客宝]ReceiveMessage=>\n" + JSON.stringify(message)); + resolve(message); + // 清除URL中的message参数 + const newUrl = + window.location.pathname + + window.location.search + .replace(/[?&]message=[^&]*/, "") + .replace(/^&/, "?"); + window.history.replaceState({}, "", newUrl); + } catch (e) { + console.error("解析URL消息失败:", e); + reject(e); + } + } + reject(null); + }); +}; diff --git a/nkebao/src/utils/updateChecker.ts b/nkebao/src/utils/updateChecker.ts new file mode 100644 index 00000000..aca1700c --- /dev/null +++ b/nkebao/src/utils/updateChecker.ts @@ -0,0 +1,217 @@ +/** + * 应用更新检测工具 + */ + +interface UpdateInfo { + hasUpdate: boolean; + version?: string; + timestamp?: number; +} + +class UpdateChecker { + private currentVersion: string; + private checkInterval: number = 1000; // 1秒检查一次(用于测试) + private intervalId: NodeJS.Timeout | null = null; + private updateCallbacks: ((info: UpdateInfo) => void)[] = []; + private currentHashes: string[] = []; + + constructor() { + // 从package.json获取版本号 + this.currentVersion = import.meta.env.VITE_APP_VERSION || "1.0.0"; + // 初始化当前哈希值 + this.initCurrentHashes(); + } + + /** + * 初始化当前哈希值 + */ + private initCurrentHashes() { + // 从当前页面的资源中提取哈希值 + const scripts = document.querySelectorAll("script[src]"); + const links = document.querySelectorAll("link[href]"); + + const scriptHashes = Array.from(scripts) + .map(script => script.getAttribute("src")) + .filter( + src => src && (src.includes("assets/") || src.includes("/assets/")), + ) + .map(src => { + // 修改正则表达式,匹配包含字母、数字和下划线的哈希值 + const match = src?.match(/[a-zA-Z0-9_-]{8,}/); + return match ? match[0] : ""; + }) + .filter(hash => hash); + + const linkHashes = Array.from(links) + .map(link => link.getAttribute("href")) + .filter( + href => href && (href.includes("assets/") || href.includes("/assets/")), + ) + .map(href => { + // 修改正则表达式,匹配包含字母、数字和下划线的哈希值 + const match = href?.match(/[a-zA-Z0-9_-]{8,}/); + return match ? match[0] : ""; + }) + .filter(hash => hash); + + this.currentHashes = [...new Set([...scriptHashes, ...linkHashes])]; + } + + /** + * 开始检测更新 + */ + start() { + if (this.intervalId) { + return; + } + + // 立即检查一次 + this.checkForUpdate(); + + // 设置定时检查 + this.intervalId = setInterval(() => { + this.checkForUpdate(); + }, this.checkInterval); + } + + /** + * 停止检测更新 + */ + stop() { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + } + + /** + * 检查更新 + */ + async checkForUpdate(): Promise { + try { + // 获取新的manifest文件 + let manifestResponse; + let manifestPath = "/.vite/manifest.json"; + + try { + manifestResponse = await fetch(manifestPath, { + cache: "no-cache", + headers: { + "Cache-Control": "no-cache", + Pragma: "no-cache", + }, + }); + } catch (error) { + // 如果.vite路径失败,尝试根路径 + manifestPath = "/manifest.json"; + manifestResponse = await fetch(manifestPath, { + cache: "no-cache", + headers: { + "Cache-Control": "no-cache", + Pragma: "no-cache", + }, + }); + } + + if (!manifestResponse.ok) { + return { hasUpdate: false }; + } + + const manifest = await manifestResponse.json(); + + // 从Vite manifest中提取文件哈希 + const newHashes: string[] = []; + + Object.values(manifest).forEach((entry: any) => { + if (entry.file && entry.file.includes("assets/")) { + // console.log("处理manifest entry file:", entry.file); + // 修改正则表达式,匹配包含字母、数字和下划线的哈希值 + const match = entry.file.match(/[a-zA-Z0-9_-]{8,}/); + if (match) { + const hash = match[0]; + newHashes.push(hash); + } + } + // 也检查CSS文件 + if (entry.css) { + entry.css.forEach((cssFile: string) => { + if (cssFile.includes("assets/")) { + // console.log("处理manifest entry css:", cssFile); + // 修改正则表达式,匹配包含字母、数字和下划线的哈希值 + const match = cssFile.match(/[a-zA-Z0-9_-]{8,}/); + if (match) { + const hash = match[0]; + // console.log("提取的manifest css哈希:", hash); + newHashes.push(hash); + } + } + }); + } + }); + + // 去重新哈希值数组 + const uniqueNewHashes = [...new Set(newHashes)]; + + // 比较哈希值 + const hasUpdate = this.compareHashes(this.currentHashes, uniqueNewHashes); + + const updateInfo: UpdateInfo = { + hasUpdate, + version: manifest.version || this.currentVersion, + timestamp: Date.now(), + }; + + // 通知所有回调 + this.updateCallbacks.forEach(callback => callback(updateInfo)); + + return updateInfo; + } catch (error) { + return { hasUpdate: false }; + } + } + + /** + * 比较哈希值 + */ + private compareHashes(current: string[], newHashes: string[]): boolean { + if (current.length !== newHashes.length) { + return true; + } + + // 对两个数组进行排序后比较,忽略顺序 + const sortedCurrent = [...current].sort(); + const sortedNewHashes = [...newHashes].sort(); + + const hasUpdate = sortedCurrent.some((hash, index) => { + return hash !== sortedNewHashes[index]; + }); + + return hasUpdate; + } + + /** + * 注册更新回调 + */ + onUpdate(callback: (info: UpdateInfo) => void) { + this.updateCallbacks.push(callback); + } + + /** + * 移除更新回调 + */ + offUpdate(callback: (info: UpdateInfo) => void) { + const index = this.updateCallbacks.indexOf(callback); + if (index > -1) { + this.updateCallbacks.splice(index, 1); + } + } + + /** + * 强制刷新页面 + */ + forceReload() { + window.location.reload(); + } +} + +export const updateChecker = new UpdateChecker(); diff --git a/nkebao/vite.config.ts b/nkebao/vite.config.ts index eb0ce0a3..d03fd648 100644 --- a/nkebao/vite.config.ts +++ b/nkebao/vite.config.ts @@ -38,5 +38,13 @@ export default defineConfig({ minify: "esbuild", // 启用源码映射(可选,生产环境可以关闭) sourcemap: false, + // 生成manifest文件 + manifest: true, + }, + define: { + // 注入版本信息 + "import.meta.env.VITE_APP_VERSION": JSON.stringify( + process.env.npm_package_version, + ), }, }); From dca72fac8c5fd4308f5d2e34e1c0e76b17156169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Tue, 5 Aug 2025 12:01:22 +0800 Subject: [PATCH 30/93] =?UTF-8?q?FEAT=20=3D>=20=E6=9C=AC=E6=AC=A1=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=A1=B9=E7=9B=AE=E4=B8=BA=EF=BC=9A=20=E6=96=B0?= =?UTF-8?q?=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/components/UpdateNotification/index.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nkebao/src/components/UpdateNotification/index.tsx b/nkebao/src/components/UpdateNotification/index.tsx index 86480615..529929d3 100644 --- a/nkebao/src/components/UpdateNotification/index.tsx +++ b/nkebao/src/components/UpdateNotification/index.tsx @@ -64,7 +64,7 @@ const UpdateNotification: React.FC = ({ right: 0, bottom: 0, zIndex: 99999, - background: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)", + background: "linear-gradient(135deg, #1890ff 0%, #096dd9 100%)", color: "white", display: "flex", flexDirection: "column", @@ -131,15 +131,15 @@ const UpdateNotification: React.FC = ({ } - > - 自动建群 - + /> {/* 搜索栏 */}
diff --git a/nkebao/src/pages/mobile/workspace/auto-like/list/data.ts b/nkebao/src/pages/mobile/workspace/auto-like/list/data.ts new file mode 100644 index 00000000..de39bd28 --- /dev/null +++ b/nkebao/src/pages/mobile/workspace/auto-like/list/data.ts @@ -0,0 +1,119 @@ +// 自动点赞任务状态 +export type LikeTaskStatus = 1 | 2; // 1: 开启, 2: 关闭 + +// 内容类型 +export type ContentType = "text" | "image" | "video" | "link"; + +// 设备信息 +export interface Device { + id: string; + name: string; + status: "online" | "offline"; + lastActive: string; +} + +// 好友信息 +export interface Friend { + id: string; + nickname: string; + wechatId: string; + avatar: string; + tags: string[]; + region: string; + source: string; +} + +// 点赞记录 +export interface LikeRecord { + id: string; + workbenchId: string; + momentsId: string; + snsId: string; + wechatAccountId: string; + wechatFriendId: string; + likeTime: string; + content: string; + resUrls: string[]; + momentTime: string; + userName: string; + operatorName: string; + operatorAvatar: string; + friendName: string; + friendAvatar: string; +} + +// 自动点赞任务 +export interface LikeTask { + id: string; + name: string; + status: LikeTaskStatus; + deviceCount: number; + targetGroup: string; + likeCount: number; + lastLikeTime: string; + createTime: string; + creator: string; + likeInterval: number; + maxLikesPerDay: number; + timeRange: { start: string; end: string }; + contentTypes: ContentType[]; + targetTags: string[]; + devices: string[]; + friends: string[]; + friendMaxLikes: number; + friendTags: string; + enableFriendTags: boolean; + todayLikeCount: number; + totalLikeCount: number; + updateTime: string; +} + +// 创建任务数据 +export interface CreateLikeTaskData { + name: string; + interval: number; + maxLikes: number; + startTime: string; + endTime: string; + contentTypes: ContentType[]; + devices: string[]; + friends?: string[]; + friendMaxLikes: number; + friendTags?: string; + enableFriendTags: boolean; + targetTags: string[]; +} + +// 更新任务数据 +export interface UpdateLikeTaskData extends CreateLikeTaskData { + id: string; +} + +// 任务配置 +export interface TaskConfig { + interval: number; + maxLikes: number; + startTime: string; + endTime: string; + contentTypes: ContentType[]; + devices: string[]; + friends: string[]; + friendMaxLikes: number; + friendTags: string; + enableFriendTags: boolean; +} + +// API响应类型 +export interface ApiResponse { + code: number; + msg: string; + data: T; +} + +// 分页响应类型 +export interface PaginatedResponse { + list: T[]; + total: number; + page: number; + limit: number; +} diff --git a/nkebao/src/pages/mobile/workspace/auto-like/list/index.tsx b/nkebao/src/pages/mobile/workspace/auto-like/list/index.tsx index a4910269..073603ee 100644 --- a/nkebao/src/pages/mobile/workspace/auto-like/list/index.tsx +++ b/nkebao/src/pages/mobile/workspace/auto-like/list/index.tsx @@ -14,7 +14,6 @@ import { MoreOutlined, LikeOutlined, } from "@ant-design/icons"; -import { ArrowLeftOutlined } from "@ant-design/icons"; import Layout from "@/components/Layout/Layout"; import { @@ -23,7 +22,7 @@ import { toggleAutoLikeTask, copyAutoLikeTask, } from "./api"; -import { LikeTask } from "@/pages/workspace/auto-like/record/data"; +import { LikeTask } from "./data"; import style from "./index.module.scss"; // 卡片菜单组件 diff --git a/nkebao/src/pages/mobile/workspace/group-push/list/index.api.ts b/nkebao/src/pages/mobile/workspace/group-push/list/index.api.ts index 0295e2f9..2a6c1153 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/list/index.api.ts +++ b/nkebao/src/pages/mobile/workspace/group-push/list/index.api.ts @@ -1,37 +1,13 @@ import request from "@/api/request"; -export interface GroupPushTask { - id: string; - name: string; - status: number; // 1: 运行中, 2: 已暂停 - deviceCount: number; - targetGroups: string[]; - pushCount: number; - successCount: number; - lastPushTime: string; - createTime: string; - creator: string; - pushInterval: number; - maxPushPerDay: number; - timeRange: { start: string; end: string }; - messageType: "text" | "image" | "video" | "link"; - messageContent: string; - targetTags: string[]; - pushMode: "immediate" | "scheduled"; - scheduledTime?: string; -} - interface ApiResponse { code: number; message: string; data: T; } -export async function fetchGroupPushTasks(): Promise { - const response = await request("/v1/workbench/list", { type: 3 }, "GET"); - if (Array.isArray(response)) return response; - if (response && Array.isArray(response.data)) return response.data; - return []; +export async function fetchGroupPushTasks() { + return request("/v1/workbench/list", { type: 3 }, "GET"); } export async function deleteGroupPushTask(id: string): Promise { diff --git a/nkebao/src/pages/mobile/workspace/group-push/list/index.tsx b/nkebao/src/pages/mobile/workspace/group-push/list/index.tsx index 7634c769..9bfea7b7 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/list/index.tsx +++ b/nkebao/src/pages/mobile/workspace/group-push/list/index.tsx @@ -36,7 +36,6 @@ import { deleteGroupPushTask, toggleGroupPushTask, copyGroupPushTask, - GroupPushTask, } from "./index.api"; import styles from "./index.module.scss"; @@ -44,14 +43,14 @@ const GroupPush: React.FC = () => { const navigate = useNavigate(); const [expandedTaskId, setExpandedTaskId] = useState(null); const [searchTerm, setSearchTerm] = useState(""); - const [tasks, setTasks] = useState([]); + const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(false); const fetchTasks = async () => { setLoading(true); try { - const list = await fetchGroupPushTasks(); - setTasks(list); + const result = await fetchGroupPushTasks(); + setTasks(result.list); } finally { setLoading(false); } @@ -180,13 +179,7 @@ const GroupPush: React.FC = () => { allowClear size="large" /> - -
@@ -228,39 +221,35 @@ const GroupPush: React.FC = () => { onChange={() => toggleTaskStatus(task.id)} /> - } - onClick={() => handleView(task.id)} - > - 查看 - - } - onClick={() => handleEdit(task.id)} - > - 编辑 - - } - onClick={() => handleCopy(task.id)} - > - 复制 - - } - onClick={() => handleDelete(task.id)} - danger - > - 删除 - - - } + menu={{ + items: [ + { + key: "view", + icon: , + label: "查看", + onClick: () => handleView(task.id), + }, + { + key: "edit", + icon: , + label: "编辑", + onClick: () => handleEdit(task.id), + }, + { + key: "copy", + icon: , + label: "复制", + onClick: () => handleCopy(task.id), + }, + { + key: "delete", + icon: , + label: "删除", + danger: true, + onClick: () => handleDelete(task.id), + }, + ], + }} trigger={["click"]} >
-
执行设备:{task.deviceCount} 个
-
目标群组:{task.targetGroups.length} 个
+
执行设备:{task.deviceCount || 1} 个
+
目标群组:{task.config?.groups?.length || 0} 个
- 推送成功:{task.successCount}/{task.pushCount} + 推送成功:{task.successCount || 0}/{task.pushCount || 0}
-
创建人:{task.creator}
-
-
-
推送成功率
- +
创建人:{task.creatorName || task.creator}
+
- 上次推送:{task.lastPushTime} -
-
- 创建时间:{task.createTime} -
+
创建时间:{task.createTime}
- {expandedTaskId === task.id && ( -
-
-
- 基本设置 -
推送间隔:{task.pushInterval} 秒
-
每日最大推送数:{task.maxPushPerDay} 条
-
- 执行时间段:{task.timeRange.start} -{" "} - {task.timeRange.end} -
-
- 推送模式: - {task.pushMode === "immediate" - ? "立即推送" - : "定时推送"} -
- {task.scheduledTime && ( -
定时时间:{task.scheduledTime}
- )} -
-
- 目标群组 -
- {task.targetGroups.map(group => ( - - ))} -
-
-
- 消息内容 -
- 消息类型:{getMessageTypeText(task.messageType)} -
-
- {task.messageContent} -
-
-
- 执行进度 -
- 今日已推送:{task.pushCount} / {task.maxPushPerDay} -
- - {task.targetTags.length > 0 && ( -
-
目标标签:
-
- {task.targetTags.map(tag => ( - - ))} -
-
- )} -
-
-
- )} )) )} From 2deddf6fd4149d3cdf1eda02a5005ddf93458147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Tue, 5 Aug 2025 18:12:32 +0800 Subject: [PATCH 35/93] =?UTF-8?q?FEAT=20=3D>=20=E6=9C=AC=E6=AC=A1=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=A1=B9=E7=9B=AE=E4=B8=BA=EF=BC=9A=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/utils/updateChecker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nkebao/src/utils/updateChecker.ts b/nkebao/src/utils/updateChecker.ts index aca1700c..c0022f18 100644 --- a/nkebao/src/utils/updateChecker.ts +++ b/nkebao/src/utils/updateChecker.ts @@ -10,7 +10,7 @@ interface UpdateInfo { class UpdateChecker { private currentVersion: string; - private checkInterval: number = 1000; // 1秒检查一次(用于测试) + private checkInterval: number = 1000 * 60 * 5; // 5分钟检查一次 private intervalId: NodeJS.Timeout | null = null; private updateCallbacks: ((info: UpdateInfo) => void)[] = []; private currentHashes: string[] = []; From 070d6b70d6f903427c142aa19a82c4971c8fba10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Wed, 6 Aug 2025 11:01:54 +0800 Subject: [PATCH 36/93] sj --- ckApp/manifest.json | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/ckApp/manifest.json b/ckApp/manifest.json index bb255e1e..297a2ced 100644 --- a/ckApp/manifest.json +++ b/ckApp/manifest.json @@ -45,7 +45,39 @@ "dSYMs" : false }, /* SDK配置 */ - "sdkConfigs" : {} + "sdkConfigs" : {}, + "icons" : { + "android" : { + "hdpi" : "unpackage/res/icons/72x72.png", + "xhdpi" : "unpackage/res/icons/96x96.png", + "xxhdpi" : "unpackage/res/icons/144x144.png", + "xxxhdpi" : "unpackage/res/icons/192x192.png" + }, + "ios" : { + "appstore" : "unpackage/res/icons/1024x1024.png", + "ipad" : { + "app" : "unpackage/res/icons/76x76.png", + "app@2x" : "unpackage/res/icons/152x152.png", + "notification" : "unpackage/res/icons/20x20.png", + "notification@2x" : "unpackage/res/icons/40x40.png", + "proapp@2x" : "unpackage/res/icons/167x167.png", + "settings" : "unpackage/res/icons/29x29.png", + "settings@2x" : "unpackage/res/icons/58x58.png", + "spotlight" : "unpackage/res/icons/40x40.png", + "spotlight@2x" : "unpackage/res/icons/80x80.png" + }, + "iphone" : { + "app@2x" : "unpackage/res/icons/120x120.png", + "app@3x" : "unpackage/res/icons/180x180.png", + "notification@2x" : "unpackage/res/icons/40x40.png", + "notification@3x" : "unpackage/res/icons/60x60.png", + "settings@2x" : "unpackage/res/icons/58x58.png", + "settings@3x" : "unpackage/res/icons/87x87.png", + "spotlight@2x" : "unpackage/res/icons/80x80.png", + "spotlight@3x" : "unpackage/res/icons/120x120.png" + } + } + } } }, /* 快应用特有相关 */ From b53929cb225e84fbfb6c3fcdca1532f36afdadc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Wed, 6 Aug 2025 17:22:01 +0800 Subject: [PATCH 37/93] =?UTF-8?q?FEAT=20=3D>=20=E6=9C=AC=E6=AC=A1=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=A1=B9=E7=9B=AE=E4=B8=BA=EF=BC=9A=20=E5=85=85?= =?UTF-8?q?=E5=80=BC=E8=AE=B0=E5=BD=95=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../recharge/{ => index}/index.module.scss | 0 .../mine/recharge/{ => index}/index.tsx | 4 +- .../pages/mobile/mine/recharge/order/api.ts | 197 ++++++++++ .../pages/mobile/mine/recharge/order/data.ts | 40 ++ .../mine/recharge/order/index.module.scss | 242 ++++++++++++ .../mobile/mine/recharge/order/index.tsx | 344 ++++++++++++++++++ .../src/pages/mobile/mine/setting/Privacy.tsx | 18 +- nkebao/src/router/module/mine.tsx | 6 + 8 files changed, 835 insertions(+), 16 deletions(-) rename nkebao/src/pages/mobile/mine/recharge/{ => index}/index.module.scss (100%) rename nkebao/src/pages/mobile/mine/recharge/{ => index}/index.tsx (98%) create mode 100644 nkebao/src/pages/mobile/mine/recharge/order/api.ts create mode 100644 nkebao/src/pages/mobile/mine/recharge/order/data.ts create mode 100644 nkebao/src/pages/mobile/mine/recharge/order/index.module.scss create mode 100644 nkebao/src/pages/mobile/mine/recharge/order/index.tsx diff --git a/nkebao/src/pages/mobile/mine/recharge/index.module.scss b/nkebao/src/pages/mobile/mine/recharge/index/index.module.scss similarity index 100% rename from nkebao/src/pages/mobile/mine/recharge/index.module.scss rename to nkebao/src/pages/mobile/mine/recharge/index/index.module.scss diff --git a/nkebao/src/pages/mobile/mine/recharge/index.tsx b/nkebao/src/pages/mobile/mine/recharge/index/index.tsx similarity index 98% rename from nkebao/src/pages/mobile/mine/recharge/index.tsx rename to nkebao/src/pages/mobile/mine/recharge/index/index.tsx index e322aca2..9507b73c 100644 --- a/nkebao/src/pages/mobile/mine/recharge/index.tsx +++ b/nkebao/src/pages/mobile/mine/recharge/index/index.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; -import { Card, Button, Toast, NavBar, Tabs } from "antd-mobile"; +import { Card, Button, Toast, Tabs } from "antd-mobile"; import { useUserStore } from "@/store/module/user"; import style from "./index.module.scss"; import { @@ -338,7 +338,7 @@ const Recharge: React.FC = () => { right={
navigate("/mine/consumption-records")} + onClick={() => navigate("/recharge/order")} >  记录 diff --git a/nkebao/src/pages/mobile/mine/recharge/order/api.ts b/nkebao/src/pages/mobile/mine/recharge/order/api.ts new file mode 100644 index 00000000..11d4573e --- /dev/null +++ b/nkebao/src/pages/mobile/mine/recharge/order/api.ts @@ -0,0 +1,197 @@ +import { + RechargeOrdersResponse, + RechargeOrderDetail, + RechargeOrderParams, +} from "./data"; + +// 模拟数据 +const mockOrders = [ + { + id: "1", + orderNo: "RC20241201001", + amount: 100.0, + paymentMethod: "wechat", + status: "success" as const, + createTime: "2024-12-01T10:30:00Z", + payTime: "2024-12-01T10:32:15Z", + description: "账户充值", + balance: 150.0, + }, + { + id: "2", + orderNo: "RC20241201002", + amount: 200.0, + paymentMethod: "alipay", + status: "pending" as const, + createTime: "2024-12-01T14:20:00Z", + description: "账户充值", + balance: 350.0, + }, + { + id: "3", + orderNo: "RC20241130001", + amount: 50.0, + paymentMethod: "bank", + status: "success" as const, + createTime: "2024-11-30T09:15:00Z", + payTime: "2024-11-30T09:18:30Z", + description: "账户充值", + balance: 50.0, + }, + { + id: "4", + orderNo: "RC20241129001", + amount: 300.0, + paymentMethod: "wechat", + status: "failed" as const, + createTime: "2024-11-29T16:45:00Z", + description: "账户充值", + }, + { + id: "5", + orderNo: "RC20241128001", + amount: 150.0, + paymentMethod: "alipay", + status: "cancelled" as const, + createTime: "2024-11-28T11:20:00Z", + description: "账户充值", + }, + { + id: "6", + orderNo: "RC20241127001", + amount: 80.0, + paymentMethod: "wechat", + status: "success" as const, + createTime: "2024-11-27T13:10:00Z", + payTime: "2024-11-27T13:12:45Z", + description: "账户充值", + balance: 80.0, + }, + { + id: "7", + orderNo: "RC20241126001", + amount: 120.0, + paymentMethod: "bank", + status: "success" as const, + createTime: "2024-11-26T08:30:00Z", + payTime: "2024-11-26T08:33:20Z", + description: "账户充值", + balance: 120.0, + }, + { + id: "8", + orderNo: "RC20241125001", + amount: 250.0, + paymentMethod: "alipay", + status: "pending" as const, + createTime: "2024-11-25T15:45:00Z", + description: "账户充值", + balance: 370.0, + }, +]; + +// 模拟延迟 +const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +// 获取充值记录列表 +export async function getRechargeOrders( + params: RechargeOrderParams, +): Promise { + await delay(800); // 模拟网络延迟 + + let filteredOrders = [...mockOrders]; + + // 状态筛选 + if (params.status && params.status !== "all") { + filteredOrders = filteredOrders.filter( + order => order.status === params.status, + ); + } + + // 时间筛选 + if (params.startTime) { + filteredOrders = filteredOrders.filter( + order => new Date(order.createTime) >= new Date(params.startTime!), + ); + } + if (params.endTime) { + filteredOrders = filteredOrders.filter( + order => new Date(order.createTime) <= new Date(params.endTime!), + ); + } + + // 分页 + const startIndex = (params.page - 1) * params.limit; + const endIndex = startIndex + params.limit; + const paginatedOrders = filteredOrders.slice(startIndex, endIndex); + + return { + list: paginatedOrders, + total: filteredOrders.length, + page: params.page, + limit: params.limit, + }; +} + +// 获取充值记录详情 +export async function getRechargeOrderDetail( + id: string, +): Promise { + await delay(500); + + const order = mockOrders.find(o => o.id === id); + if (!order) { + throw new Error("订单不存在"); + } + + return { + ...order, + paymentChannel: + order.paymentMethod === "wechat" + ? "微信支付" + : order.paymentMethod === "alipay" + ? "支付宝" + : "银行转账", + transactionId: `TX${order.orderNo}`, + }; +} + +// 取消充值订单 +export async function cancelRechargeOrder(id: string): Promise { + await delay(1000); + + const orderIndex = mockOrders.findIndex(o => o.id === id); + if (orderIndex === -1) { + throw new Error("订单不存在"); + } + + if (mockOrders[orderIndex].status !== "pending") { + throw new Error("只能取消处理中的订单"); + } + + // 模拟更新订单状态 + (mockOrders[orderIndex] as any).status = "cancelled"; +} + +// 申请退款 +export async function refundRechargeOrder( + id: string, + reason: string, +): Promise { + await delay(1200); + + const orderIndex = mockOrders.findIndex(o => o.id === id); + if (orderIndex === -1) { + throw new Error("订单不存在"); + } + + if (mockOrders[orderIndex].status !== "success") { + throw new Error("只能对成功的订单申请退款"); + } + + // 模拟添加退款信息 + const order = mockOrders[orderIndex]; + (order as any).refundAmount = order.amount; + (order as any).refundTime = new Date().toISOString(); + (order as any).refundReason = reason; +} diff --git a/nkebao/src/pages/mobile/mine/recharge/order/data.ts b/nkebao/src/pages/mobile/mine/recharge/order/data.ts new file mode 100644 index 00000000..95b9a0e9 --- /dev/null +++ b/nkebao/src/pages/mobile/mine/recharge/order/data.ts @@ -0,0 +1,40 @@ +// 充值记录类型定义 +export interface RechargeOrder { + id: string; + orderNo: string; + amount: number; + paymentMethod: string; + status: "success" | "pending" | "failed" | "cancelled"; + createTime: string; + payTime?: string; + description?: string; + remark?: string; + operator?: string; + balance?: number; +} + +// API响应类型 +export interface RechargeOrdersResponse { + list: RechargeOrder[]; + total: number; + page: number; + limit: number; +} + +// 充值记录详情 +export interface RechargeOrderDetail extends RechargeOrder { + paymentChannel?: string; + transactionId?: string; + refundAmount?: number; + refundTime?: string; + refundReason?: string; +} + +// 查询参数 +export interface RechargeOrderParams { + page: number; + limit: number; + status?: string; + startTime?: string; + endTime?: string; +} diff --git a/nkebao/src/pages/mobile/mine/recharge/order/index.module.scss b/nkebao/src/pages/mobile/mine/recharge/order/index.module.scss new file mode 100644 index 00000000..64031e92 --- /dev/null +++ b/nkebao/src/pages/mobile/mine/recharge/order/index.module.scss @@ -0,0 +1,242 @@ +.recharge-orders-page { + padding: 16px; + background: #f5f5f5; + min-height: 100vh; + + .orders-list { + .order-card { + margin-bottom: 12px; + border-radius: 12px; + background: #fff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + overflow: hidden; + + .order-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + border-bottom: 1px solid #f0f0f0; + + .order-info { + flex: 1; + + .order-no { + font-size: 14px; + color: #333; + font-weight: 500; + margin-bottom: 4px; + } + + .order-time { + font-size: 12px; + color: #999; + display: flex; + align-items: center; + gap: 4px; + } + } + + .order-amount { + text-align: right; + + .amount-text { + font-size: 18px; + font-weight: 600; + color: #52c41a; + margin-bottom: 4px; + } + + .status-tag { + font-size: 12px; + padding: 2px 8px; + border-radius: 10px; + } + } + } + + .order-details { + padding: 12px 16px; + + .detail-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + font-size: 14px; + + &:last-child { + margin-bottom: 0; + } + + .label { + color: #666; + } + + .value { + color: #333; + font-weight: 500; + } + } + + .payment-method { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + + .method-icon { + width: 20px; + height: 20px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + color: #fff; + } + + .method-text { + font-size: 14px; + color: #333; + } + } + + .balance-info { + background: #f8f9fa; + padding: 8px 12px; + border-radius: 6px; + font-size: 12px; + color: #666; + margin-top: 8px; + } + } + + .order-actions { + padding: 12px 16px; + border-top: 1px solid #f0f0f0; + display: flex; + gap: 8px; + + .action-btn { + flex: 1; + height: 32px; + border-radius: 6px; + font-size: 12px; + border: none; + cursor: pointer; + transition: all 0.2s; + + &.primary { + background: #1677ff; + color: #fff; + + &:hover { + background: #0958d9; + } + } + + &.secondary { + background: #f5f5f5; + color: #666; + border: 1px solid #d9d9d9; + + &:hover { + background: #e6e6e6; + } + } + + &.danger { + background: #ff4d4f; + color: #fff; + + &:hover { + background: #cf1322; + } + } + } + } + } + } + + .empty-state { + padding: 60px 20px; + text-align: center; + + .empty-icon { + font-size: 48px; + color: #d9d9d9; + margin-bottom: 16px; + } + + .empty-text { + font-size: 14px; + color: #999; + } + } + + .loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 20px; + + .loading-text { + margin-top: 12px; + font-size: 14px; + color: #666; + } + } + + .load-more { + text-align: center; + padding: 16px; + color: #1677ff; + font-size: 14px; + cursor: pointer; + background: #fff; + border-radius: 8px; + margin-top: 12px; + + &:hover { + background: #f0f8ff; + } + } + + .filter-bar { + background: #fff; + border-radius: 12px; + padding: 16px; + margin-bottom: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + + .filter-tabs { + display: flex; + gap: 8px; + + .filter-tab { + flex: 1; + height: 32px; + border-radius: 16px; + font-size: 12px; + border: 1px solid #d9d9d9; + background: #fff; + color: #666; + cursor: pointer; + transition: all 0.2s; + + &.active { + background: #1677ff; + color: #fff; + border-color: #1677ff; + } + + &:hover:not(.active) { + border-color: #1677ff; + color: #1677ff; + } + } + } + } +} diff --git a/nkebao/src/pages/mobile/mine/recharge/order/index.tsx b/nkebao/src/pages/mobile/mine/recharge/order/index.tsx new file mode 100644 index 00000000..7e5260e6 --- /dev/null +++ b/nkebao/src/pages/mobile/mine/recharge/order/index.tsx @@ -0,0 +1,344 @@ +import React, { useState, useEffect, useCallback } from "react"; +import { useNavigate } from "react-router-dom"; +import { Card, SpinLoading, Empty, Toast, Dialog } from "antd-mobile"; +import { + WalletOutlined, + ClockCircleOutlined, + WechatOutlined, + AlipayCircleOutlined, + BankOutlined, +} from "@ant-design/icons"; +import NavCommon from "@/components/NavCommon"; +import Layout from "@/components/Layout/Layout"; +import { + getRechargeOrders, + cancelRechargeOrder, + refundRechargeOrder, +} from "./api"; +import { RechargeOrder } from "./data"; +import style from "./index.module.scss"; + +const RechargeOrders: React.FC = () => { + const navigate = useNavigate(); + const [orders, setOrders] = useState([]); + const [loading, setLoading] = useState(true); + const [hasMore, setHasMore] = useState(true); + const [page, setPage] = useState(1); + const [statusFilter, setStatusFilter] = useState("all"); + + const loadOrders = async (reset = false) => { + setLoading(true); + try { + const currentPage = reset ? 1 : page; + const params = { + page: currentPage, + limit: 20, + ...(statusFilter !== "all" && { status: statusFilter }), + }; + + const response = await getRechargeOrders(params); + const newOrders = response.list || []; + setOrders(prev => (reset ? newOrders : [...prev, ...newOrders])); + setHasMore(newOrders.length === 20); + if (reset) setPage(1); + else setPage(currentPage + 1); + } catch (error) { + console.error("加载充值记录失败:", error); + Toast.show({ content: "加载失败,请重试", position: "top" }); + } finally { + setLoading(false); + } + }; + + // 初始化加载 + useEffect(() => { + loadOrders(true); + }, []); + + // 筛选条件变化时重新加载 + const handleFilterChange = (newStatus: string) => { + setStatusFilter(newStatus); + setPage(1); + setOrders([]); + loadOrders(true); + }; + + const getStatusText = (status: string) => { + switch (status) { + case "success": + return "充值成功"; + case "pending": + return "处理中"; + case "failed": + return "充值失败"; + case "cancelled": + return "已取消"; + default: + return "未知状态"; + } + }; + + const getStatusColor = (status: string) => { + switch (status) { + case "success": + return "#52c41a"; + case "pending": + return "#faad14"; + case "failed": + return "#ff4d4f"; + case "cancelled": + return "#999"; + default: + return "#666"; + } + }; + + const getPaymentMethodIcon = (method: string) => { + switch (method.toLowerCase()) { + case "wechat": + return ; + case "alipay": + return ; + case "bank": + return ; + default: + return ; + } + }; + + const getPaymentMethodColor = (method: string) => { + switch (method.toLowerCase()) { + case "wechat": + return "#07c160"; + case "alipay": + return "#1677ff"; + case "bank": + return "#722ed1"; + default: + return "#666"; + } + }; + + const formatTime = (timeStr: string) => { + const date = new Date(timeStr); + const now = new Date(); + const diff = now.getTime() - date.getTime(); + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + + if (days === 0) { + return date.toLocaleTimeString("zh-CN", { + hour: "2-digit", + minute: "2-digit", + }); + } else if (days === 1) { + return ( + "昨天 " + + date.toLocaleTimeString("zh-CN", { + hour: "2-digit", + minute: "2-digit", + }) + ); + } else if (days < 7) { + return `${days}天前`; + } else { + return date.toLocaleDateString("zh-CN"); + } + }; + + const handleCancelOrder = async (orderId: string) => { + const result = await Dialog.confirm({ + content: "确定要取消这个充值订单吗?", + confirmText: "确定取消", + cancelText: "再想想", + }); + + if (result) { + try { + await cancelRechargeOrder(orderId); + Toast.show({ content: "订单已取消", position: "top" }); + loadOrders(true); + } catch (error) { + console.error("取消订单失败:", error); + Toast.show({ content: "取消失败,请重试", position: "top" }); + } + } + }; + + const handleRefundOrder = async (orderId: string) => { + const result = await Dialog.confirm({ + content: "确定要申请退款吗?退款将在1-3个工作日内处理。", + confirmText: "申请退款", + cancelText: "取消", + }); + + if (result) { + try { + await refundRechargeOrder(orderId, "用户主动申请退款"); + Toast.show({ content: "退款申请已提交", position: "top" }); + loadOrders(true); + } catch (error) { + console.error("申请退款失败:", error); + Toast.show({ content: "申请失败,请重试", position: "top" }); + } + } + }; + + const renderOrderItem = (order: RechargeOrder) => ( + +
+
+
订单号:{order.orderNo}
+
+ + {formatTime(order.createTime)} +
+
+
+
+ ¥{order.amount.toFixed(2)} +
+
+ {getStatusText(order.status)} +
+
+
+ +
+
+
+ {getPaymentMethodIcon(order.paymentMethod)} +
+
{order.paymentMethod}
+
+ + {order.description && ( +
+ 备注 + {order.description} +
+ )} + + {order.payTime && ( +
+ 支付时间 + {formatTime(order.payTime)} +
+ )} + + {order.balance !== undefined && ( +
+ 充值后余额: ¥{order.balance.toFixed(2)} +
+ )} +
+ + {order.status === "pending" && ( +
+ +
+ )} + + {order.status === "success" && ( +
+ + +
+ )} + + {order.status === "failed" && ( +
+ +
+ )} +
+ ); + + const filterTabs = [ + { key: "all", label: "全部" }, + { key: "success", label: "成功" }, + { key: "pending", label: "处理中" }, + { key: "failed", label: "失败" }, + { key: "cancelled", label: "已取消" }, + ]; + + return ( + } + loading={loading && page === 1} + > +
+
+
+ {filterTabs.map(tab => ( + + ))} +
+
+ + {orders.length === 0 && !loading ? ( + } + /> + ) : ( +
+ {orders.map(renderOrderItem)} + {loading && page > 1 && ( +
+ +
加载中...
+
+ )} + {!loading && hasMore && ( +
loadOrders()}> + 加载更多 +
+ )} +
+ )} +
+
+ ); +}; + +export default RechargeOrders; diff --git a/nkebao/src/pages/mobile/mine/setting/Privacy.tsx b/nkebao/src/pages/mobile/mine/setting/Privacy.tsx index ab3f0737..4a3ea570 100644 --- a/nkebao/src/pages/mobile/mine/setting/Privacy.tsx +++ b/nkebao/src/pages/mobile/mine/setting/Privacy.tsx @@ -1,27 +1,17 @@ import React from "react"; -import { useNavigate } from "react-router-dom"; -import { NavBar, Card } from "antd-mobile"; +import { Card } from "antd-mobile"; import Layout from "@/components/Layout/Layout"; import style from "./index.module.scss"; +import NavCommon from "@/components/NavCommon"; const Privacy: React.FC = () => { - const navigate = useNavigate(); - return ( - navigate(-1)} style={{ background: "#fff" }}> - - 用户隐私协议 - - - } - > + }>

用户隐私协议

-

更新时间:2024年12月1日

+

更新时间:2025年8月1日

1. 信息收集

diff --git a/nkebao/src/router/module/mine.tsx b/nkebao/src/router/module/mine.tsx index 431c3da2..9bdb1ac3 100644 --- a/nkebao/src/router/module/mine.tsx +++ b/nkebao/src/router/module/mine.tsx @@ -6,6 +6,7 @@ import TrafficPoolDetail from "@/pages/mobile/mine/traffic-pool/detail/index"; import WechatAccounts from "@/pages/mobile/mine/wechat-accounts/list/index"; import WechatAccountDetail from "@/pages/mobile/mine/wechat-accounts/detail/index"; import Recharge from "@/pages/mobile/mine/recharge/index"; +import RechargeOrder from "@/pages/mobile/mine/recharge/order/index"; import Setting from "@/pages/mobile/mine/setting/index"; import SecuritySetting from "@/pages/mobile/mine/setting/SecuritySetting"; import About from "@/pages/mobile/mine/setting/About"; @@ -54,6 +55,11 @@ const routes = [ element: , auth: true, }, + { + path: "/recharge/order", + element: , + auth: true, + }, { path: "/settings", element: , From f59ce8809d25e074686c4cfd9c31836c92def5dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Wed, 6 Aug 2025 17:36:15 +0800 Subject: [PATCH 38/93] =?UTF-8?q?FEAT=20=3D>=20=E6=9C=AC=E6=AC=A1=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=A1=B9=E7=9B=AE=E4=B8=BA=EF=BC=9A=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E4=BA=86=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plan/new/steps/BasicSettings.tsx | 171 +----------------- .../scenarios/plan/new/steps/base.data.ts | 38 ++++ 2 files changed, 40 insertions(+), 169 deletions(-) create mode 100644 nkebao/src/pages/mobile/scenarios/plan/new/steps/base.data.ts diff --git a/nkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx b/nkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx index e88abbc3..08c5e113 100644 --- a/nkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx +++ b/nkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx @@ -1,6 +1,5 @@ import React, { useState, useEffect, useRef } from "react"; -import { Form, Input, Button, Tag, Switch, Modal, Spin } from "antd"; -import { Button as ButtonMobile } from "antd-mobile"; +import { Input, Button, Tag, Switch, Modal, Spin } from "antd"; import { PlusOutlined, EyeOutlined, @@ -11,6 +10,7 @@ import { } from "@ant-design/icons"; import { uploadFile } from "@/api/common"; import styles from "./base.module.scss"; +import { posterTemplates } from "./base.data"; interface BasicSettingsProps { isEdit: boolean; @@ -20,12 +20,6 @@ interface BasicSettingsProps { sceneLoading: boolean; } -interface Account { - id: string; - nickname: string; - avatar: string; -} - interface Material { id: string; name: string; @@ -33,53 +27,6 @@ interface Material { preview: string; } -const posterTemplates = [ - { - id: "poster-1", - name: "点击领取", - preview: - "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E9%A2%86%E5%8F%961-tipd1HI7da6qooY5NkhxQnXBnT5LGU.gif", - }, - { - id: "poster-2", - name: "点击合作", - preview: - "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%90%88%E4%BD%9C-LPlMdgxtvhqCSr4IM1bZFEFDBF3ztI.gif", - }, - { - id: "poster-3", - name: "点击咨询", - preview: - "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%92%A8%E8%AF%A2-FTiyAMAPop2g9LvjLOLDz0VwPg3KVu.gif", - }, - { - id: "poster-4", - name: "点击签到", - preview: - "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E7%AD%BE%E5%88%B0-94TZIkjLldb4P2jTVlI6MkSDg0NbXi.gif", - }, - { - id: "poster-5", - name: "点击了解", - preview: - "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E4%BA%86%E8%A7%A3-6GCl7mQVdO4WIiykJyweSubLsTwj71.gif", - }, - { - id: "poster-6", - name: "点击报名", - preview: - "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E6%8A%A5%E5%90%8D-Mj0nnva0BiASeDAIhNNaRRAbjPgjEj.gif", - }, -]; - -const generateRandomAccounts = (count: number): Account[] => { - return Array.from({ length: count }, (_, index) => ({ - id: `account-${index + 1}`, - nickname: `账号-${Math.random().toString(36).substring(2, 7)}`, - avatar: `/placeholder.svg?height=40&width=40&text=${index + 1}`, - })); -}; - const generatePosterMaterials = (): Material[] => { return posterTemplates.map(template => ({ id: template.id, @@ -96,30 +43,11 @@ const BasicSettings: React.FC = ({ sceneList, sceneLoading, }) => { - const [isAccountDialogOpen, setIsAccountDialogOpen] = useState(false); - const [isMaterialDialogOpen, setIsMaterialDialogOpen] = useState(false); const [isPreviewOpen, setIsPreviewOpen] = useState(false); - const [isPhoneSettingsOpen, setIsPhoneSettingsOpen] = useState(false); - const [accounts] = useState(generateRandomAccounts(50)); const [materials] = useState(generatePosterMaterials()); - const [selectedAccounts, setSelectedAccounts] = useState( - formData.accounts?.length > 0 ? formData.accounts : [], - ); const [selectedMaterials, setSelectedMaterials] = useState( formData.materials?.length > 0 ? formData.materials : [], ); - // showAllScenarios 默认为 true - const [showAllScenarios, setShowAllScenarios] = useState(true); - const [isImportDialogOpen, setIsImportDialogOpen] = useState(false); - const [importedTags, setImportedTags] = useState< - Array<{ - phone: string; - wechat: string; - source?: string; - orderAmount?: number; - orderDate?: string; - }> - >(formData.importedTags || []); // 自定义标签相关状态 const [customTagInput, setCustomTagInput] = useState(""); @@ -152,28 +80,6 @@ const BasicSettings: React.FC = ({ const uploadInputRef = useRef(null); const uploadOrderInputRef = useRef(null); - // 更新电话获客设置 - const handlePhoneSettingsUpdate = () => { - onChange({ ...formData, phoneSettings }); - setIsPhoneSettingsOpen(false); - }; - - // 处理标签选择 - const handleTagToggle = (tagId: string) => { - const newTags = selectedScenarioTags.includes(tagId) - ? selectedScenarioTags.filter((id: string) => id !== tagId) - : [...selectedScenarioTags, tagId]; - - setSelectedScenarioTags(newTags); - onChange({ ...formData, scenarioTags: newTags }); - }; - - // 处理通话类型选择 - const handleCallTypeChange = (type: string) => { - // setPhoneCallType(type) // This line was removed as per the edit hint. - onChange({ ...formData, phoneCallType: type }); - }; - // 初始化时,如果没有选择场景,默认选择海报获客 useEffect(() => { if (!formData.scenario) { @@ -287,60 +193,6 @@ const BasicSettings: React.FC = ({ setIsPreviewOpen(true); }; - // 账号多选切换 - const handleAccountToggle = (account: Account) => { - const isSelected = selectedAccounts.some( - (a: Account) => a.id === account.id, - ); - let newSelected; - if (isSelected) { - newSelected = selectedAccounts.filter( - (a: Account) => a.id !== account.id, - ); - } else { - newSelected = [...selectedAccounts, account]; - } - setSelectedAccounts(newSelected); - onChange({ ...formData, accounts: newSelected }); - }; - - // 移除已选账号 - const handleRemoveAccount = (id: string) => { - const newSelected = selectedAccounts.filter((a: Account) => a.id !== id); - setSelectedAccounts(newSelected); - onChange({ ...formData, accounts: newSelected }); - }; - - // 处理文件导入 - const handleFileImport = (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - if (file) { - const reader = new FileReader(); - reader.onload = e => { - try { - const content = e.target?.result as string; - const rows = content.split("\n").filter(row => row.trim()); - const tags = rows.slice(1).map(row => { - const [phone, wechat, source, orderAmount, orderDate] = - row.split(","); - return { - phone: phone?.trim(), - wechat: wechat?.trim(), - source: source?.trim(), - orderAmount: orderAmount ? Number(orderAmount) : undefined, - orderDate: orderDate?.trim(), - }; - }); - setImportedTags(tags); - onChange({ ...formData, importedTags: tags }); - } catch (error) { - // 可用 toast 提示 - } - }; - reader.readAsText(file); - } - }; - // 下载模板 const handleDownloadTemplate = () => { const template = @@ -378,25 +230,6 @@ const BasicSettings: React.FC = ({ } }; - // 账号弹窗关闭时清理搜索等状态 - const handleAccountDialogClose = () => { - setIsAccountDialogOpen(false); - // 可在此清理账号搜索等临时状态 - }; - // 素材弹窗关闭时清理搜索等状态 - const handleMaterialDialogClose = () => { - setIsMaterialDialogOpen(false); - // 可在此清理素材搜索等临时状态 - }; - // 订单导入弹窗关闭时清理文件输入等状态 - const handleImportDialogClose = () => { - setIsImportDialogOpen(false); - // 可在此清理文件输入等临时状态 - }; - // 电话获客弹窗关闭 - const handlePhoneSettingsDialogClose = () => { - setIsPhoneSettingsOpen(false); - }; // 图片预览关闭 const handleImagePreviewClose = () => { setIsPreviewOpen(false); diff --git a/nkebao/src/pages/mobile/scenarios/plan/new/steps/base.data.ts b/nkebao/src/pages/mobile/scenarios/plan/new/steps/base.data.ts new file mode 100644 index 00000000..c85d9c29 --- /dev/null +++ b/nkebao/src/pages/mobile/scenarios/plan/new/steps/base.data.ts @@ -0,0 +1,38 @@ +export const posterTemplates = [ + { + id: "poster-1", + name: "点击领取", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E9%A2%86%E5%8F%961-tipd1HI7da6qooY5NkhxQnXBnT5LGU.gif", + }, + { + id: "poster-2", + name: "点击合作", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%90%88%E4%BD%9C-LPlMdgxtvhqCSr4IM1bZFEFDBF3ztI.gif", + }, + { + id: "poster-3", + name: "点击咨询", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E5%92%A8%E8%AF%A2-FTiyAMAPop2g9LvjLOLDz0VwPg3KVu.gif", + }, + { + id: "poster-4", + name: "点击签到", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E7%AD%BE%E5%88%B0-94TZIkjLldb4P2jTVlI6MkSDg0NbXi.gif", + }, + { + id: "poster-5", + name: "点击了解", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E4%BA%86%E8%A7%A3-6GCl7mQVdO4WIiykJyweSubLsTwj71.gif", + }, + { + id: "poster-6", + name: "点击报名", + preview: + "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/%E7%82%B9%E5%87%BB%E6%8A%A5%E5%90%8D-Mj0nnva0BiASeDAIhNNaRRAbjPgjEj.gif", + }, +]; From 7ebece0491b3caea24eb27406ca8cbde9e239744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Wed, 6 Aug 2025 17:54:41 +0800 Subject: [PATCH 39/93] =?UTF-8?q?FEAT=20=3D>=20=E6=9C=AC=E6=AC=A1=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=A1=B9=E7=9B=AE=E4=B8=BA=EF=BC=9A=E5=84=AA=E5=8C=96?= =?UTF-8?q?=E5=9F=BA=E6=9C=AC=E8=A8=AD=E7=BD=AE=E9=A0=81=E9=9D=A2=EF=BC=8C?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=96=87=E4=BB=B6=E4=B8=8A=E5=82=B3=E7=B5=84?= =?UTF-8?q?=E4=BB=B6=EF=BC=8C=E8=AA=BF=E6=95=B4=E6=A8=A3=E5=BC=8F=E5=8F=8A?= =?UTF-8?q?=E9=A1=AF=E7=A4=BA=E9=82=8F=E8=BC=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plan/new/steps/BasicSettings.tsx | 115 +++++++----------- .../scenarios/plan/new/steps/base.module.scss | 2 +- 2 files changed, 47 insertions(+), 70 deletions(-) diff --git a/nkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx b/nkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx index 08c5e113..0b9fbd7a 100644 --- a/nkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx +++ b/nkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx @@ -5,12 +5,12 @@ import { EyeOutlined, CloseOutlined, DownloadOutlined, - UploadOutlined, - CheckOutlined, } from "@ant-design/icons"; import { uploadFile } from "@/api/common"; import styles from "./base.module.scss"; import { posterTemplates } from "./base.data"; +import GroupSelection from "@/components/GroupSelection"; +import FileUpload from "@/components/Upload/FileUpload"; interface BasicSettingsProps { isEdit: boolean; @@ -78,18 +78,12 @@ const BasicSettings: React.FC = ({ // 新增:用于文件选择的ref const uploadInputRef = useRef(null); - const uploadOrderInputRef = useRef(null); // 初始化时,如果没有选择场景,默认选择海报获客 useEffect(() => { if (!formData.scenario) { onChange({ ...formData, scenario: "haibao" }); } - - // 检查是否已经有上传的订单文件 - if (formData.orderFileUploaded) { - setOrderUploaded(true); - } }, [formData, onChange]); useEffect(() => { @@ -208,28 +202,6 @@ const BasicSettings: React.FC = ({ window.URL.revokeObjectURL(url); }; - // 修改订单表格上传逻辑,使用 uploadFile 公共方法 - const [orderUploaded, setOrderUploaded] = useState(false); - - const handleOrderFileUpload = async ( - event: React.ChangeEvent, - ) => { - const file = event.target.files?.[0]; - if (file) { - try { - await uploadFile(file); // 默认接口即可 - setOrderUploaded(true); - onChange({ ...formData, orderFileUploaded: true }); - // 可用 toast 或其它方式提示成功 - // alert('上传成功'); - } catch (err) { - // 可用 toast 或其它方式提示失败 - // alert('上传失败'); - } - event.target.value = ""; - } - }; - // 图片预览关闭 const handleImagePreviewClose = () => { setIsPreviewOpen(false); @@ -331,18 +303,23 @@ const BasicSettings: React.FC = ({ 添加
- {/* 输入获客成功提示 */} -
- { - setTips(e.target.value); - onChange({ ...formData, tips: e.target.value }); - }} - placeholder="请输入获客成功提示" - /> -
+ {/* 输入获客成功提示 - 只有海报场景才显示 */} + {formData.scenario === 1 && ( + <> +
请输入获客成功提示
+
+ { + setTips(e.target.value); + onChange({ ...formData, tips: e.target.value }); + }} + placeholder="请输入获客成功提示" + /> +
+ + )} {/* 选素材 */}
选择海报
@@ -467,7 +444,22 @@ const BasicSettings: React.FC = ({ )}
- {/* 订单导入区块优化 */} + {/* 群选择 - 只有微信群场景才显示 */} + {formData.scenario === 7 && ( +
+
选择群聊
+ + onChange({ ...formData, groupSelected: groups }) + } + placeholder="请选择微信群" + className={styles["basic-group-selector"]} + /> +
+ )} + + {/* 订单导入区块 - 使用FileUpload组件 */}
订单表格上传
@@ -477,34 +469,19 @@ const BasicSettings: React.FC = ({ > 下载模板 - +
+
+ onChange({ ...formData, orderFileUrl: url })} + acceptTypes={["excel"]} + maxCount={1} + maxSize={10} + showPreview={false} + />
- 支持 CSV、Excel 格式,上传后将文件保存到服务器 + 支持 Excel 格式,上传后将文件保存到服务器
{/* 电话获客设置区块,仅在选择电话获客场景时显示 */} diff --git a/nkebao/src/pages/mobile/scenarios/plan/new/steps/base.module.scss b/nkebao/src/pages/mobile/scenarios/plan/new/steps/base.module.scss index 8d85666a..aac76ca4 100644 --- a/nkebao/src/pages/mobile/scenarios/plan/new/steps/base.module.scss +++ b/nkebao/src/pages/mobile/scenarios/plan/new/steps/base.module.scss @@ -49,10 +49,10 @@ .basic-custom-tag-input { display: flex; gap: 8px; + margin-bottom: 16px; } .basic-success-tip { display: flex; - padding-top: 16px; } .basic-materials { margin: 16px 0; From d4c10de30a8d3c3d85b399249f6507485fcfced4 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Wed, 6 Aug 2025 17:57:24 +0800 Subject: [PATCH 40/93] =?UTF-8?q?=E7=BE=A4=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php index 95358eeb..a3751975 100644 --- a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php +++ b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php @@ -1321,9 +1321,10 @@ class Adapter implements WeChatServiceInterface public function syncWechatGroup() { - $sql = "insert into ck_wechat_group(`id`,`chatroomId`,`name`,`avatar`,`companyId`,`ownerWechatId`,`createTime`,`updateTime`,`deleteTime`) + $sql = "insert into ck_wechat_group(`id`,`wechatAccountId`,`chatroomId`,`name`,`avatar`,`companyId`,`ownerWechatId`,`createTime`,`updateTime`,`deleteTime`) SELECT g.id id, + g.wechatAccountId wechatAccountId, g.chatroomId chatroomId, g.nickname name, g.chatroomAvatar avatar, From 7a2553940c4fc07c6f7eee5b1d1e6e069587dc90 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Wed, 6 Aug 2025 18:00:50 +0800 Subject: [PATCH 41/93] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...PotentialListWithInCompanyV1Controller.php | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/Server/application/cunkebao/controller/traffic/GetPotentialListWithInCompanyV1Controller.php b/Server/application/cunkebao/controller/traffic/GetPotentialListWithInCompanyV1Controller.php index 9a68d3c7..ec35f503 100644 --- a/Server/application/cunkebao/controller/traffic/GetPotentialListWithInCompanyV1Controller.php +++ b/Server/application/cunkebao/controller/traffic/GetPotentialListWithInCompanyV1Controller.php @@ -270,7 +270,7 @@ class GetPotentialListWithInCompanyV1Controller extends BaseController $percentage = number_format(($taskNum / $passNum) * 100, 2); $total['percentage'] = $percentage; } - + $data['total'] = $total; @@ -322,14 +322,20 @@ class GetPotentialListWithInCompanyV1Controller extends BaseController public function getUserTags() { $userId = $this->request->param('userId', ''); + $companyId = $this->getUserInfo('companyId'); if (empty($userId)) { return json_encode(['code' => 500, 'msg' => '用户id不能为空']); } $data = Db::name('traffic_pool')->alias('tp') - ->join(['s2_wechat_friend' => 'wf'], 'tp.wechatId=wf.wechatId', 'left') + ->join('wechat_friendship f', 'tp.wechatId=f.wechatId AND f.companyId='.$companyId, 'left') + ->join(['s2_wechat_friend' => 'wf'], 'f.wechatId=wf.wechatId', 'left') ->where(['tp.id' => $userId]) ->order('tp.createTime desc') ->column('wf.id,wf.labels,wf.siteLabels'); + if (empty($data)) { + return ResponseHelper::success(['wechat' => [], 'siteLabels' => []]); + } + $tags = []; $siteLabels = []; @@ -350,5 +356,31 @@ class GetPotentialListWithInCompanyV1Controller extends BaseController return ResponseHelper::success(['wechat' => $tags, 'siteLabels' => $siteLabels]); } + /* public function editUserTags() + { + $userId = $this->request->param('userId', ''); + if (empty($userId)) { + return json_encode(['code' => 500, 'msg' => '用户id不能为空']); + } + $tags = $this->request->param('tags', []); + $tags = $this->request->param('tags', []); + $isWechat = $this->request->param('isWechat', false); + $companyId = $this->getUserInfo('companyId'); + + $friend = Db::name('traffic_pool')->alias('tp') + ->join('wechat_friendship f', 'tp.wechatId=f.wechatId AND f.companyId='.$companyId, 'left') + ->join(['s2_wechat_friend' => 'wf'], 'f.wechatId=wf.wechatId', 'left') + ->where(['tp.id' => $userId]) + ->order('tp.createTime desc') + ->column('wf.id,wf.accountId,wf.labels,wf.siteLabels'); + if (empty($data)) { + return ResponseHelper::error('该用户不存在'); + } + + + }*/ + + + } \ No newline at end of file From 94c484dbe474ed2ba3822a46350dbff51eb6171c Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Wed, 6 Aug 2025 18:02:03 +0800 Subject: [PATCH 42/93] =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E5=8F=B0-=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E7=BE=A4=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/controller/WebSocketController.php | 362 +++++++++--------- Server/application/command.php | 1 + Server/application/cunkebao/config/route.php | 3 + .../controller/WorkbenchController.php | 34 ++ Server/crontab_tasks.md | 3 + 5 files changed, 215 insertions(+), 188 deletions(-) diff --git a/Server/application/api/controller/WebSocketController.php b/Server/application/api/controller/WebSocketController.php index 5090a578..24d67fa6 100644 --- a/Server/application/api/controller/WebSocketController.php +++ b/Server/application/api/controller/WebSocketController.php @@ -9,12 +9,11 @@ use think\Db; use think\facade\Log; use WebSocket\Client; use think\facade\Env; -use app\api\model\WechatFriendModel as WechatFriend; +use app\api\model\WechatFriendModel as WechatFriend; use app\api\model\WechatMomentsModel as WechatMoments; use think\facade\Cache; - class WebSocketController extends BaseController { protected $authorized; @@ -27,7 +26,7 @@ class WebSocketController extends BaseController /************************************ * 初始化相关功能 ************************************/ - + /** * 构造函数 - 初始化WebSocket连接 * @param array $userData 用户数据 @@ -44,39 +43,39 @@ class WebSocketController extends BaseController */ protected function initConnection($userData = []) { - if(!empty($userData) && count($userData)){ + if (!empty($userData) && count($userData)) { if (empty($userData['userName']) || empty($userData['password'])) { - return json_encode(['code'=>400,'msg'=>'参数缺失']); + return json_encode(['code' => 400, 'msg' => '参数缺失']); } // 检查缓存中是否存在有效的token $cacheKey = 'websocket_token_' . $userData['userName']; - $cachedToken = Cache::get($cacheKey); - + $cachedToken = Cache::get($cacheKey); + if ($cachedToken) { $this->authorized = $cachedToken; $this->accountId = $userData['accountId']; } else { - $params = [ - 'grant_type' => 'password', - 'username' => $userData['userName'], - 'password' => $userData['password'] - ]; + $params = [ + 'grant_type' => 'password', + 'username' => $userData['userName'], + 'password' => $userData['password'] + ]; - // 调用登录接口获取token - $headerData = ['client:kefu-client']; - $header = setHeader($headerData, '', 'plain'); - $result = requestCurl('https://kf.quwanzhi.com:9991/token', $params, 'POST', $header); - $result_array = handleApiResponse($result); + // 调用登录接口获取token + $headerData = ['client:kefu-client']; + $header = setHeader($headerData, '', 'plain'); + $result = requestCurl('https://kf.quwanzhi.com:9991/token', $params, 'POST', $header); + $result_array = handleApiResponse($result); - if (isset($result_array['access_token']) && !empty($result_array['access_token'])) { - $this->authorized = $result_array['access_token']; - $this->accountId = $userData['accountId']; - - // 将token存入缓存,有效期5分钟 - Cache::set($cacheKey, $this->authorized, 300); - } else { - return json_encode(['code'=>400,'msg'=>'获取系统授权信息失败']); + if (isset($result_array['access_token']) && !empty($result_array['access_token'])) { + $this->authorized = $result_array['access_token']; + $this->accountId = $userData['accountId']; + + // 将token存入缓存,有效期5分钟 + Cache::set($cacheKey, $this->authorized, 300); + } else { + return json_encode(['code' => 400, 'msg' => '获取系统授权信息失败']); } } } else { @@ -85,7 +84,7 @@ class WebSocketController extends BaseController } if (empty($this->authorized) || empty($this->accountId)) { - return json_encode(['code'=>400,'msg'=>'缺失关键参数']); + return json_encode(['code' => 400, 'msg' => '缺失关键参数']); } $this->connect(); @@ -97,40 +96,40 @@ class WebSocketController extends BaseController protected function connect() { try { - //证书 - $context = stream_context_create(); - stream_context_set_option($context, 'ssl', 'verify_peer', false); - stream_context_set_option($context, 'ssl', 'verify_peer_name', false); - - //开启WS链接 - $result = [ - "accessToken" => $this->authorized, - "accountId" => $this->accountId, - "client" => "kefu-client", - "cmdType" => "CmdSignIn", - "seq" => 1, - ]; + //证书 + $context = stream_context_create(); + stream_context_set_option($context, 'ssl', 'verify_peer', false); + stream_context_set_option($context, 'ssl', 'verify_peer_name', false); - $content = json_encode($result); - $this->client = new Client("wss://kf.quwanzhi.com:9993", - [ - 'filter' => ['text', 'binary', 'ping', 'pong', 'close','receive', 'send'], - 'context' => $context, - 'headers' => [ - 'Sec-WebSocket-Protocol' => 'soap', - 'origin' => 'localhost', - ], - 'timeout' => 86400, - ] - ); - - $this->client->send($content); + //开启WS链接 + $result = [ + "accessToken" => $this->authorized, + "accountId" => $this->accountId, + "client" => "kefu-client", + "cmdType" => "CmdSignIn", + "seq" => 1, + ]; + + $content = json_encode($result); + $this->client = new Client("wss://kf.quwanzhi.com:9993", + [ + 'filter' => ['text', 'binary', 'ping', 'pong', 'close', 'receive', 'send'], + 'context' => $context, + 'headers' => [ + 'Sec-WebSocket-Protocol' => 'soap', + 'origin' => 'localhost', + ], + 'timeout' => 86400, + ] + ); + + $this->client->send($content); $this->isConnected = true; $this->lastHeartbeatTime = time(); - + // 启动心跳检测 //$this->startHeartbeat(); - + } catch (\Exception $e) { Log::error("WebSocket连接失败:" . $e->getMessage()); $this->isConnected = false; @@ -143,7 +142,7 @@ class WebSocketController extends BaseController protected function startHeartbeat() { // 使用定时器发送心跳 - \Swoole\Timer::tick($this->heartbeatInterval * 1000, function() { + \Swoole\Timer::tick($this->heartbeatInterval * 1000, function () { if ($this->isConnected) { $this->sendHeartbeat(); } @@ -160,10 +159,10 @@ class WebSocketController extends BaseController "cmdType" => "CmdHeartbeat", "seq" => time() ]; - + $this->client->send(json_encode($heartbeat)); $this->lastHeartbeatTime = time(); - + } catch (\Exception $e) { Log::error("发送心跳包失败:" . $e->getMessage()); $this->reconnect(); @@ -204,7 +203,7 @@ class WebSocketController extends BaseController protected function sendMessage($data) { $this->checkConnection(); - + try { $this->client->send(json_encode($data)); $response = $this->client->receive(); @@ -235,9 +234,9 @@ class WebSocketController extends BaseController $currentPage = 1; // 当前页码 $allMoments = []; // 存储所有朋友圈数据 - //过滤消息 + //过滤消息 if (empty($wechatAccountId)) { - return json_encode(['code'=>400,'msg'=>'指定账号不能为空']); + return json_encode(['code' => 400, 'msg' => '指定账号不能为空']); } try { @@ -263,21 +262,21 @@ class WebSocketController extends BaseController sleep(10); continue; } - + // 检查返回结果 if (!isset($message['result']) || empty($message['result']) || !is_array($message['result'])) { break; } - + // 检查是否遇到旧数据 $hasOldData = false; foreach ($message['result'] as $moment) { $momentId = WechatMoments::where('snsId', $moment['snsId']) ->where('wechatAccountId', $wechatAccountId) ->value('id'); - + if (!empty($momentId)) { $hasOldData = true; break; @@ -330,31 +329,31 @@ class WebSocketController extends BaseController return json_encode($result); } catch (\Exception $e) { - return json_encode(['code'=>500,'msg'=>$e->getMessage()]); + return json_encode(['code' => 500, 'msg' => $e->getMessage()]); } } - /** + /** * 朋友圈点赞 * @return \think\response\Json */ public function momentInteract($data = []) { - + $snsId = !empty($data['snsId']) ? $data['snsId'] : ''; $wechatAccountId = !empty($data['wechatAccountId']) ? $data['wechatAccountId'] : ''; $wechatFriendId = !empty($data['wechatFriendId']) ? $data['wechatFriendId'] : 0; //过滤消息 - if (empty($snsId)) { - return json_encode(['code'=>400,'msg'=>'snsId不能为空']); + if (empty($snsId)) { + return json_encode(['code' => 400, 'msg' => 'snsId不能为空']); } - if (empty($wechatAccountId)) { - return json_encode(['code'=>400,'msg'=>'微信id不能为空']); + if (empty($wechatAccountId)) { + return json_encode(['code' => 400, 'msg' => '微信id不能为空']); } - - try { + + try { $result = [ "cmdType" => "CmdMomentInteract", "momentInteractType" => 1, @@ -362,16 +361,16 @@ class WebSocketController extends BaseController "snsId" => $snsId, "wechatAccountId" => $wechatAccountId, "wechatFriendId" => $wechatFriendId, - ]; + ]; $message = $this->sendMessage($result); - return json_encode(['code'=>200,'msg'=>'点赞成功','data'=>$message]); - } catch (\Exception $e) { - return json_encode(['code'=>500,'msg'=>$e->getMessage()]); + return json_encode(['code' => 200, 'msg' => '点赞成功', 'data' => $message]); + } catch (\Exception $e) { + return json_encode(['code' => 500, 'msg' => $e->getMessage()]); } } - /** + /** * 朋友圈取消点赞 * @return \think\response\Json */ @@ -381,36 +380,36 @@ class WebSocketController extends BaseController $data = $this->request->param(); if (empty($data)) { - return json_encode(['code'=>400,'msg'=>'参数缺失']); + return json_encode(['code' => 400, 'msg' => '参数缺失']); } //过滤消息 if (empty($data['snsId'])) { - return json_encode(['code'=>400,'msg'=>'snsId不能为空']); + return json_encode(['code' => 400, 'msg' => 'snsId不能为空']); } if (empty($data['wechatAccountId'])) { - return json_encode(['code'=>400,'msg'=>'微信id不能为空']); + return json_encode(['code' => 400, 'msg' => '微信id不能为空']); } - + try { - $result = [ - "CommentId2" => '', - "CommentTime" => 0, - "cmdType" => "CmdMomentCancelInteract", - "optType" => 1, - "seq" => time(), - "snsId" => $data['snsId'], - "wechatAccountId" => $data['wechatAccountId'], - "wechatFriendId" => 0, - ]; + $result = [ + "CommentId2" => '', + "CommentTime" => 0, + "cmdType" => "CmdMomentCancelInteract", + "optType" => 1, + "seq" => time(), + "snsId" => $data['snsId'], + "wechatAccountId" => $data['wechatAccountId'], + "wechatFriendId" => 0, + ]; $message = $this->sendMessage($result); - return json_encode(['code'=>200,'msg'=>'取消点赞成功','data'=>$message]); + return json_encode(['code' => 200, 'msg' => '取消点赞成功', 'data' => $message]); } catch (\Exception $e) { - return json_encode(['code'=>500,'msg'=>$e->getMessage()]); + return json_encode(['code' => 500, 'msg' => $e->getMessage()]); } } else { - return json_encode(['code'=>400,'msg'=>'非法请求']); + return json_encode(['code' => 400, 'msg' => '非法请求']); } } @@ -462,19 +461,19 @@ class WebSocketController extends BaseController // 发送请求 $this->client->send(json_encode($params)); - + // 接收响应 $response = $this->client->receive(); $message = json_decode($response, true); - if(empty($message)){ - return json_encode(['code'=>500,'msg'=>'获取朋友圈资源链接失败']); + if (empty($message)) { + return json_encode(['code' => 500, 'msg' => '获取朋友圈资源链接失败']); } - if($message['cmdType'] == 'CmdDownloadMomentImagesResult' && is_array($message['urls']) && count($message['urls']) > 0){ - $urls = json_encode($message['urls'],256); - Db::table('s2_wechat_moments')->where('snsId',$data['snsId'])->update(['resUrls'=>$urls]); + if ($message['cmdType'] == 'CmdDownloadMomentImagesResult' && is_array($message['urls']) && count($message['urls']) > 0) { + $urls = json_encode($message['urls'], 256); + Db::table('s2_wechat_moments')->where('snsId', $data['snsId'])->update(['resUrls' => $urls]); } - return json_encode(['code'=>200,'msg'=>'获取朋友圈资源链接成功','data'=>$message]); + return json_encode(['code' => 200, 'msg' => '获取朋友圈资源链接成功', 'data' => $message]); } catch (\Exception $e) { // 记录错误日志 Log::error('获取朋友圈资源链接异常:' . $e->getMessage()); @@ -507,19 +506,18 @@ class WebSocketController extends BaseController return false; } - + try { foreach ($momentList as $moment) { // 提取momentEntity中的数据 $momentEntity = $moment['momentEntity'] ?? []; - + // 检查朋友圈数据是否已存在 $momentId = WechatMoments::where('snsId', $moment['snsId']) ->where('wechatAccountId', $wechatAccountId) ->value('id'); - $dataToSave = [ 'commentList' => json_encode($moment['commentList'] ?? [], 256), 'createTime' => $moment['createTime'] ?? 0, @@ -543,7 +541,7 @@ class WebSocketController extends BaseController // 如果已存在,则更新数据 Db::table('s2_wechat_moments')->where('id', $momentId)->update($dataToSave); } else { - if(empty($wechatFriendId)){ + if (empty($wechatFriendId)) { $wechatFriendId = WechatFriend::where('wechatAccountId', $wechatAccountId)->where('wechatId', $momentEntity['userName'])->value('id'); } // 如果不存在,则插入新数据 @@ -588,7 +586,7 @@ class WebSocketController extends BaseController if (empty($wechatFriendId)) { return json_encode(['code' => 400, 'msg' => '好友ID不能为空']); } - + if (empty($wechatAccountId)) { return json_encode(['code' => 400, 'msg' => '微信账号ID不能为空']); } @@ -609,7 +607,7 @@ class WebSocketController extends BaseController // 发送请求并获取响应 $message = $this->sendMessage($params); - + // 记录日志 Log::info('修改好友标签:' . json_encode($params, 256)); Log::info('修改好友标签结果:' . json_encode($message, 256)); @@ -619,7 +617,7 @@ class WebSocketController extends BaseController } catch (\Exception $e) { // 记录错误日志 Log::error('修改好友标签失败:' . $e->getMessage()); - + // 返回错误响应 return json_encode(['code' => 500, 'msg' => '修改标签失败:' . $e->getMessage()]); } @@ -653,24 +651,23 @@ class WebSocketController extends BaseController // 消息拼接 msgType(1:文本 3:图片 43:视频 47:动图表情包(gif、其他表情包) 49:小程序/其他:图文、文件) // 当前,type 为文本、图片、动图表情包的时候,content为string, 其他情况为对象 {type: 'file/link/...', url: '', title: '', thunmbPath: '', desc: ''} $params = [ - "cmdType" => "CmdSendMessage", - "content" => $dataArray['content'], - "msgSubType" => 0, - "msgType" => $dataArray['msgType'], - "seq" => time(), - "wechatAccountId" => $dataArray['wechatAccountId'], - "wechatChatroomId" => 0, - "wechatFriendId" => $dataArray['wechatFriendId'], - ]; + "cmdType" => "CmdSendMessage", + "content" => $dataArray['content'], + "msgSubType" => 0, + "msgType" => $dataArray['msgType'], + "seq" => time(), + "wechatAccountId" => $dataArray['wechatAccountId'], + "wechatChatroomId" => 0, + "wechatFriendId" => $dataArray['wechatFriendId'], + ]; // 发送请求 $this->client->send(json_encode($params)); // 接收响应 $response = $this->client->receive(); $message = json_decode($response, true); - - if(!empty($message)){ - return json_encode(['code'=>500,'msg'=>'信息发送成功','data'=>$message]); + if (!empty($message)) { + return json_encode(['code' => 200, 'msg' => '信息发送成功', 'data' => $message]); } } @@ -678,65 +675,55 @@ class WebSocketController extends BaseController * 发送群消息 * @return \think\response\Json */ - public function sendCommunity() + public function sendCommunity($dataArray = []) { - if ($this->request->isPost()) { - $data = $this->request->post(); - if (empty($data)) { - return json_encode(['code'=>400,'msg'=>'参数缺失']); - } - $dataArray = $data; - if (!is_array($dataArray)) { - return json_encode(['code'=>400,'msg'=>'数据格式错误']); - } - - //过滤消息 - if (empty($dataArray['content'])) { - return json_encode(['code'=>400,'msg'=>'内容缺失']); - } - if (empty($dataArray['wechatAccountId'])) { - return json_encode(['code'=>400,'msg'=>'微信id不能为空']); - } - - if (empty($dataArray['msgType'])) { - return json_encode(['code'=>400,'msg'=>'类型缺失']); - } - if (empty($dataArray['wechatChatroomId'])) { - return json_encode(['code'=>400,'msg'=>'群id不能为空']); - } - - $msg = '消息成功发送'; - $message = []; - try { - //消息拼接 msgType(1:文本 3:图片 43:视频 47:动图表情包 49:小程序) - $result = [ - "cmdType" => "CmdSendMessage", - "content" => htmlspecialchars_decode($dataArray['content']), - "msgSubType" => 0, - "msgType" => $dataArray['msgType'], - "seq" => time(), - "wechatAccountId" => $dataArray['wechatAccountId'], - "wechatChatroomId" => $dataArray['wechatChatroomId'], - "wechatFriendId" => 0, - ]; - - $result = json_encode($result); - $this->client->send($result); - $message = $this->client->receive(); - //关闭WS链接 - $this->client->close(); - //Log::write('WS群消息发送'); - //Log::write($message); - $message = json_decode($message, 1); - } catch (\Exception $e) { - $msg = $e->getMessage(); - } - return json_encode(['code'=>200,'msg'=>$msg,'data'=>$message]); - - } else { - return json_encode(['code'=>400,'msg'=>'非法请求']); - //return errorJson('非法请求'); + if (!is_array($dataArray)) { + return json_encode(['code' => 400, 'msg' => '数据格式错误']); } + + //过滤消息 + if (empty($dataArray['content'])) { + return json_encode(['code' => 400, 'msg' => '内容缺失']); + } + if (empty($dataArray['wechatAccountId'])) { + return json_encode(['code' => 400, 'msg' => '微信id不能为空']); + } + + if (empty($dataArray['msgType'])) { + return json_encode(['code' => 400, 'msg' => '类型缺失']); + } + if (empty($dataArray['wechatChatroomId'])) { + return json_encode(['code' => 400, 'msg' => '群id不能为空']); + } + + $message = []; + try { + //消息拼接 msgType(1:文本 3:图片 43:视频 47:动图表情包 49:小程序) + $params = [ + "cmdType" => "CmdSendMessage", + "content" => htmlspecialchars_decode($dataArray['content']), + "msgSubType" => 0, + "msgType" => $dataArray['msgType'], + "seq" => time(), + "wechatAccountId" => $dataArray['wechatAccountId'], + "wechatChatroomId" => $dataArray['wechatChatroomId'], + "wechatFriendId" => 0, + ]; + + // 发送请求 + $this->client->send(json_encode($params)); + // 接收响应 + $response = $this->client->receive(); + $message = json_decode($response, true); + if (!empty($message)) { + return json_encode(['code' => 200, 'msg' => '信息发送成功', 'data' => $message]); + } + } catch (\Exception $e) { + $msg = $e->getMessage(); + return json_encode(['code' => 400, 'msg' => $msg, 'data' => $message]); + } + + } /** @@ -747,26 +734,26 @@ class WebSocketController extends BaseController public function sendCommunitys($data = []) { if (empty($data)) { - return json_encode(['code'=>400,'msg'=>'参数缺失']); + return json_encode(['code' => 400, 'msg' => '参数缺失']); } $dataArray = $data; if (!is_array($dataArray)) { - return json_encode(['code'=>400,'msg'=>'数据格式错误']); + return json_encode(['code' => 400, 'msg' => '数据格式错误']); } //过滤消息 if (empty($dataArray['content'])) { - return json_encode(['code'=>400,'msg'=>'内容缺失']); + return json_encode(['code' => 400, 'msg' => '内容缺失']); } if (empty($dataArray['wechatAccountId'])) { - return json_encode(['code'=>400,'msg'=>'微信id不能为空']); + return json_encode(['code' => 400, 'msg' => '微信id不能为空']); } if (empty($dataArray['msgType'])) { - return json_encode(['code'=>400,'msg'=>'类型缺失']); + return json_encode(['code' => 400, 'msg' => '类型缺失']); } if (empty($dataArray['wechatChatroomId'])) { - return json_encode(['code'=>400,'msg'=>'群id不能为空']); + return json_encode(['code' => 400, 'msg' => '群id不能为空']); } $msg = '消息成功发送'; @@ -796,11 +783,10 @@ class WebSocketController extends BaseController $msg = $e->getMessage(); } - return json_encode(['code'=>200,'msg'=>$msg,'data'=>$message]); + return json_encode(['code' => 200, 'msg' => $msg, 'data' => $message]); } - /** * 邀请好友入群 * @param array $data 请求参数 @@ -843,11 +829,11 @@ class WebSocketController extends BaseController Log::info('邀请好友入群请求:' . json_encode($params, 256)); $message = $this->sendMessage($params); - return json_encode(['code'=>200,'msg'=>'邀请成功','data'=>$message]); + return json_encode(['code' => 200, 'msg' => '邀请成功', 'data' => $message]); } catch (\Exception $e) { // 记录错误日志 Log::error('邀请好友入群异常:' . $e->getMessage()); - // 返回错误响应 + // 返回错误响应 return json_encode(['code' => 500, 'msg' => '邀请好友入群异常:' . $e->getMessage()]); } } diff --git a/Server/application/command.php b/Server/application/command.php index 570c76a4..17904a8a 100644 --- a/Server/application/command.php +++ b/Server/application/command.php @@ -32,5 +32,6 @@ return [ 'sync:wechatData' => 'app\command\SyncWechatDataToCkbTask', // 同步微信数据到存客宝 'sync:allFriends' => 'app\command\SyncAllFriendsCommand', // 同步所有在线好友 'workbench:trafficDistribute' => 'app\command\WorkbenchTrafficDistributeCommand', // 工作台流量分发任务 + 'workbench:groupPush' => 'app\command\WorkbenchGroupPushCommand', // 工作台群组同步任务 'switch:friends' => 'app\command\SwitchFriendsCommand', ]; diff --git a/Server/application/cunkebao/config/route.php b/Server/application/cunkebao/config/route.php index 392d73e6..2a45e9f6 100644 --- a/Server/application/cunkebao/config/route.php +++ b/Server/application/cunkebao/config/route.php @@ -94,6 +94,9 @@ Route::group('v1/', function () { Route::get('device-labels', 'app\cunkebao\controller\WorkbenchController@getDeviceLabels'); // 获取设备微信好友标签统计 Route::get('group-list', 'app\cunkebao\controller\WorkbenchController@getGroupList'); // 获取群列表 Route::get('account-list', 'app\cunkebao\controller\WorkbenchController@getAccountList'); // 获取账号列表 + + Route::get('getJdSocialMedia', 'app\cunkebao\controller\WorkbenchController@getJdSocialMedia'); // 获取京东联盟导购媒体 + Route::get('getJdPromotionSite', 'app\cunkebao\controller\WorkbenchController@getJdPromotionSite'); // 获取京东联盟广告位 }); // 内容库相关 diff --git a/Server/application/cunkebao/controller/WorkbenchController.php b/Server/application/cunkebao/controller/WorkbenchController.php index 5bf682a6..95ec91a9 100644 --- a/Server/application/cunkebao/controller/WorkbenchController.php +++ b/Server/application/cunkebao/controller/WorkbenchController.php @@ -1500,4 +1500,38 @@ class WorkbenchController extends Controller } + /** + * 获取京东联盟导购媒体 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function getJdSocialMedia() + { + $data = Db::name('jd_social_media')->order('id DESC')->select(); + return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]); + } + + /** + * 获取京东联盟广告位 + * @return \think\response\Json + * @throws \think\db\exception\DataNotFoundException + * @throws \think\db\exception\ModelNotFoundException + * @throws \think\exception\DbException + */ + public function getJdPromotionSite() + { + $id = $this->request->param('id', ''); + if (empty($id)) { + return json(['code' => 500, 'msg' => '参数缺失']); + } + + $data = Db::name('jd_promotion_site')->where('jdSocialMediaId',$id)->order('id DESC')->select(); + return json(['code' => 200, 'msg' => '获取成功', 'data' => $data]); + } + + + + } \ No newline at end of file diff --git a/Server/crontab_tasks.md b/Server/crontab_tasks.md index 00aadff3..1442f0c6 100644 --- a/Server/crontab_tasks.md +++ b/Server/crontab_tasks.md @@ -57,6 +57,9 @@ # 同步微信数据到存客宝 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 2 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:groupPush >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_groupPush.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 From cd88e444645a1055607f4208c51a5218b0bd15fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Wed, 6 Aug 2025 18:29:12 +0800 Subject: [PATCH 43/93] =?UTF-8?q?FEAT=20=3D>=20=E6=9C=AC=E6=AC=A1=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=A1=B9=E7=9B=AE=E4=B8=BA=EF=BC=9A=20=E5=90=8C?= =?UTF-8?q?=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auto-like/record/record.module.scss | 4 +- .../group-push/list/index.module.scss | 68 +++++- .../workspace/group-push/list/index.tsx | 224 +++++++++--------- nkebao/src/router/module/workspace.tsx | 2 +- 4 files changed, 177 insertions(+), 121 deletions(-) diff --git a/nkebao/src/pages/mobile/workspace/auto-like/record/record.module.scss b/nkebao/src/pages/mobile/workspace/auto-like/record/record.module.scss index 16faab6e..a6df2c00 100644 --- a/nkebao/src/pages/mobile/workspace/auto-like/record/record.module.scss +++ b/nkebao/src/pages/mobile/workspace/auto-like/record/record.module.scss @@ -3,7 +3,7 @@ display: flex; align-items: center; gap: 8px; - padding: 16px; + padding: 0 16px; } .headerSearchInputWrap { position: relative; @@ -62,7 +62,7 @@ padding-bottom: 80px; } .contentWrap { - padding: 12px; + padding: 0 16px; display: flex; flex-direction: column; gap: 16px; diff --git a/nkebao/src/pages/mobile/workspace/group-push/list/index.module.scss b/nkebao/src/pages/mobile/workspace/group-push/list/index.module.scss index c1923d45..9ea092a0 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/list/index.module.scss +++ b/nkebao/src/pages/mobile/workspace/group-push/list/index.module.scss @@ -10,10 +10,15 @@ } .refresh-btn { - height: 38px; - width: 40px; - padding: 0; - border-radius: 8px; + // 只针对当前模块的refresh-btn按钮进行样式设置 + &.ant-btn { + height: 38px !important; + width: 40px !important; + padding: 0 !important; + border-radius: 8px !important; + min-width: 40px !important; + flex-shrink: 0 !important; + } } .taskList { @@ -59,12 +64,11 @@ } .taskInfoGrid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 8px 16px; font-size: 13px; color: #666; margin-bottom: 12px; + display: flex; + justify-content: space-between; } .progressBlock { @@ -100,6 +104,56 @@ gap: 16px; } +// CardMenu 样式 +.menu-btn { + background: none; + border: none; + padding: 4px; + cursor: pointer; + border-radius: 4px; + color: #666; + + &:hover { + background: #f5f5f5; + } +} + +.menu-dropdown { + position: absolute; + right: 0; + top: 28px; + background: white; + border-radius: 6px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + z-index: 100; + min-width: 120px; + padding: 4px; + border: 1px solid #e5e5e5; +} + +.menu-item { + padding: 8px 12px; + cursor: pointer; + display: flex; + align-items: center; + border-radius: 4px; + font-size: 14px; + gap: 8px; + transition: background 0.2s; + + &:hover { + background: #f5f5f5; + } + + &.danger { + color: #ff4d4f; + + &:hover { + background: #fff2f0; + } + } +} + @media (max-width: 600px) { .taskCard { padding: 12px 6px 8px 6px; diff --git a/nkebao/src/pages/mobile/workspace/group-push/list/index.tsx b/nkebao/src/pages/mobile/workspace/group-push/list/index.tsx index 9bfea7b7..b12dedc5 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/list/index.tsx +++ b/nkebao/src/pages/mobile/workspace/group-push/list/index.tsx @@ -1,8 +1,7 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { useNavigate } from "react-router-dom"; -import { NavBar } from "antd-mobile"; import { - ArrowLeftOutlined, + TeamOutlined, PlusOutlined, SearchOutlined, ReloadOutlined, @@ -12,25 +11,12 @@ import { DeleteOutlined, EyeOutlined, CopyOutlined, - DownOutlined, - UpOutlined, - SettingOutlined, - CalendarOutlined, - TeamOutlined, - MessageOutlined, SendOutlined, + CarryOutOutlined, } from "@ant-design/icons"; -import { - Card, - Button, - Input, - Badge, - Switch, - Progress, - Dropdown, - Menu, -} from "antd"; +import { Card, Button, Input, Badge, Switch } from "antd"; import Layout from "@/components/Layout/Layout"; +import NavCommon from "@/components/NavCommon"; import { fetchGroupPushTasks, deleteGroupPushTask, @@ -39,6 +25,71 @@ import { } from "./index.api"; import styles from "./index.module.scss"; +// 卡片菜单组件 +interface CardMenuProps { + onView: () => void; + onEdit: () => void; + onCopy: () => void; + onDelete: () => void; +} + +const CardMenu: React.FC = ({ onEdit, onCopy, onDelete }) => { + const [open, setOpen] = useState(false); + const menuRef = useRef(null); + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setOpen(false); + } + } + if (open) document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, [open]); + + return ( +
+ + {open && ( +
+
{ + onEdit(); + setOpen(false); + }} + className={styles["menu-item"]} + > + + 编辑 +
+
{ + onCopy(); + setOpen(false); + }} + className={styles["menu-item"]} + > + + 复制 +
+
{ + onDelete(); + setOpen(false); + }} + className={`${styles["menu-item"]} ${styles["danger"]}`} + > + + 删除 +
+
+ )} +
+ ); +}; + const GroupPush: React.FC = () => { const navigate = useNavigate(); const [expandedTaskId, setExpandedTaskId] = useState(null); @@ -71,7 +122,7 @@ const GroupPush: React.FC = () => { }; const handleEdit = (taskId: string) => { - navigate(`/workspace/group-push/${taskId}/edit`); + navigate(`/workspace/group-push/edit/${taskId}`); }; const handleView = (taskId: string) => { @@ -121,68 +172,45 @@ const GroupPush: React.FC = () => { } }; - const getMessageTypeText = (type: string) => { - switch (type) { - case "text": - return "文字"; - case "image": - return "图片"; - case "video": - return "视频"; - case "link": - return "链接"; - default: - return "未知"; - } - }; - - const getSuccessRate = (pushCount: number, successCount: number) => { - if (pushCount === 0) return 0; - return Math.round((successCount / pushCount) * 100); - }; - return ( - navigate(-1)} - /> -
- } - style={{ background: "#fff" }} - right={ + <> + } + onClick={handleCreateNew} + > + 创建任务 + + } + /> + +
+ setSearchTerm(e.target.value)} + prefix={} + allowClear + size="large" + /> - } - > - 群消息推送 - +
+ } >
-
- setSearchTerm(e.target.value)} - prefix={} - allowClear - size="large" - /> - -
{filteredTasks.length === 0 ? ( @@ -220,49 +248,23 @@ const GroupPush: React.FC = () => { checked={task.status === 1} onChange={() => toggleTaskStatus(task.id)} /> - , - label: "查看", - onClick: () => handleView(task.id), - }, - { - key: "edit", - icon: , - label: "编辑", - onClick: () => handleEdit(task.id), - }, - { - key: "copy", - icon: , - label: "复制", - onClick: () => handleCopy(task.id), - }, - { - key: "delete", - icon: , - label: "删除", - danger: true, - onClick: () => handleDelete(task.id), - }, - ], - }} - trigger={["click"]} - > -
-
执行设备:{task.deviceCount || 1} 个
-
目标群组:{task.config?.groups?.length || 0} 个
- 推送成功:{task.successCount || 0}/{task.pushCount || 0} + + 推送目标:{task.config?.groups?.length || 0} 个社群 +
+
+ 推送内容: + {task.config?.content || 0} 个
-
创建人:{task.creatorName || task.creator}
diff --git a/nkebao/src/router/module/workspace.tsx b/nkebao/src/router/module/workspace.tsx index 9b938fe4..5c010122 100644 --- a/nkebao/src/router/module/workspace.tsx +++ b/nkebao/src/router/module/workspace.tsx @@ -83,7 +83,7 @@ const workspaceRoutes = [ auth: true, }, { - path: "/workspace/group-push/:id/edit", + path: "/workspace/group-push/edit/:id", element: , auth: true, }, From f78cac645cc0c188efbbf249971e79c5f07a8210 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Thu, 7 Aug 2025 09:22:39 +0800 Subject: [PATCH 44/93] =?UTF-8?q?=E7=BE=A4=E6=8E=A8=E9=80=81=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../command/WorkbenchGroupPushCommand.php | 76 +++++++++++++++++++ Server/crontab_tasks.md | 2 +- 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 Server/application/command/WorkbenchGroupPushCommand.php diff --git a/Server/application/command/WorkbenchGroupPushCommand.php b/Server/application/command/WorkbenchGroupPushCommand.php new file mode 100644 index 00000000..4fa7dfd7 --- /dev/null +++ b/Server/application/command/WorkbenchGroupPushCommand.php @@ -0,0 +1,76 @@ +setName('workbench:groupPush') + ->setDescription('工作台群组同步任务队列') + ->addOption('jobId', null, Option::VALUE_OPTIONAL, '任务ID,用于区分不同实例', date('YmdHis') . rand(1000, 9999)); + } + + protected function execute(Input $input, Output $output) + { + $output->writeln('开始处理工作台群组同步任务...'); + + try { + // 获取任务ID + $jobId = $input->getOption('jobId'); + + $output->writeln('任务ID: ' . $jobId); + + // 检查队列是否已经在运行 + $queueLockKey = "queue_lock:{$this->queueName}"; + Cache::rm($queueLockKey); + if (Cache::get($queueLockKey)) { + $output->writeln("队列 {$this->queueName} 已经在运行中,跳过执行"); + Log::warning("队列 {$this->queueName} 已经在运行中,跳过执行"); + return false; + } + + // 设置队列运行锁,有效期1小时 + Cache::set($queueLockKey, $jobId, 3600); + $output->writeln("已设置队列运行锁,键名:{$queueLockKey},值:{$jobId},有效期:1小时"); + + // 将任务添加到队列 + $this->addToQueue($jobId, $queueLockKey); + + $output->writeln('工作台群组同步任务已添加到队列'); + } catch (\Exception $e) { + Log::error('工作台群组同步任务添加失败:' . $e->getMessage()); + $output->writeln('工作台群组同步任务添加失败:' . $e->getMessage()); + return false; + } + + return true; + } + + /** + * 添加任务到队列 + * @param string $jobId 任务ID + * @param string $queueLockKey 队列锁键名 + */ + public function addToQueue($jobId = '', $queueLockKey = '') + { + $data = [ + 'jobId' => $jobId, + 'queueLockKey' => $queueLockKey + ]; + + // 添加到队列,设置任务名为 workbench_groupPush + Queue::push(WorkbenchGroupPushJob::class, $data, $this->queueName); + } +} \ No newline at end of file diff --git a/Server/crontab_tasks.md b/Server/crontab_tasks.md index 1442f0c6..f6852e58 100644 --- a/Server/crontab_tasks.md +++ b/Server/crontab_tasks.md @@ -58,7 +58,7 @@ 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 2 * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:groupPush >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_groupPush.log 2>&1 +*/2 * * * * cd /www/wwwroot/mckb_quwanzhi_com/Server && php think workbench:groupPush >> /www/wwwroot/mckb_quwanzhi_com/Server/runtime/log/workbench_groupPush.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 From b33231b3b44b164c7bcb3d7369e436baa4424b97 Mon Sep 17 00:00:00 2001 From: wong <106998207@qq.com> Date: Thu, 7 Aug 2025 11:56:04 +0800 Subject: [PATCH 45/93] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 11 + .../command/SyncWechatDataToCkbTask.php | 5 + .../command/WorkbenchGroupPushCommand.php | 10 +- .../PostCreateAddFriendPlanV1Controller.php | 7 +- .../application/job/WorkbenchGroupPushJob.php | 401 ++++++++++++++++++ .../Adapters/ChuKeBao/Adapter.php | 33 ++ nkebao/.env.development | 2 +- 7 files changed, 462 insertions(+), 7 deletions(-) create mode 100644 .gitignore create mode 100644 Server/application/job/WorkbenchGroupPushJob.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4af24f06 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.idea/ +Cunkebao/.next/ +Store_vue/node_modules/ +*.zip +Cunkebao/.specstory/ +*.cursorindexingignore +Server/.specstory/ +Store_vue/.specstory/ +Store_vue/unpackage/ +Store_vue/.vscode/ +SuperAdmin/.specstory/ diff --git a/Server/application/command/SyncWechatDataToCkbTask.php b/Server/application/command/SyncWechatDataToCkbTask.php index fe95c1c0..420040d9 100644 --- a/Server/application/command/SyncWechatDataToCkbTask.php +++ b/Server/application/command/SyncWechatDataToCkbTask.php @@ -52,6 +52,7 @@ class SyncWechatDataToCkbTask extends Command $this->syncWechatDevice($ChuKeBaoAdapter); $this->syncWechatCustomer($ChuKeBaoAdapter); $this->syncWechatGroup($ChuKeBaoAdapter); + $this->syncWechatGroupCustomer($ChuKeBaoAdapter); $this->syncWechatFriendToTrafficPoolBatch($ChuKeBaoAdapter); $this->syncTrafficSourceUser($ChuKeBaoAdapter); $this->syncTrafficSourceGroup($ChuKeBaoAdapter); @@ -113,6 +114,10 @@ class SyncWechatDataToCkbTask extends Command { return $ChuKeBaoAdapter->syncWechatGroup(); } + protected function syncWechatGroupCustomer(ChuKeBaoAdapter $ChuKeBaoAdapter) + { + return $ChuKeBaoAdapter->syncWechatGroupCustomer(); + } } \ No newline at end of file diff --git a/Server/application/command/WorkbenchGroupPushCommand.php b/Server/application/command/WorkbenchGroupPushCommand.php index 4fa7dfd7..59c70ddc 100644 --- a/Server/application/command/WorkbenchGroupPushCommand.php +++ b/Server/application/command/WorkbenchGroupPushCommand.php @@ -18,13 +18,13 @@ class WorkbenchGroupPushCommand extends Command protected function configure() { $this->setName('workbench:groupPush') - ->setDescription('工作台群组同步任务队列') + ->setDescription('工作台群发同步任务队列') ->addOption('jobId', null, Option::VALUE_OPTIONAL, '任务ID,用于区分不同实例', date('YmdHis') . rand(1000, 9999)); } protected function execute(Input $input, Output $output) { - $output->writeln('开始处理工作台群组同步任务...'); + $output->writeln('开始处理工作台群发同步任务...'); try { // 获取任务ID @@ -48,10 +48,10 @@ class WorkbenchGroupPushCommand extends Command // 将任务添加到队列 $this->addToQueue($jobId, $queueLockKey); - $output->writeln('工作台群组同步任务已添加到队列'); + $output->writeln('工作台群发同步任务已添加到队列'); } catch (\Exception $e) { - Log::error('工作台群组同步任务添加失败:' . $e->getMessage()); - $output->writeln('工作台群组同步任务添加失败:' . $e->getMessage()); + Log::error('工作台群发同步任务添加失败:' . $e->getMessage()); + $output->writeln('工作台群发同步任务添加失败:' . $e->getMessage()); return false; } diff --git a/Server/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php b/Server/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php index a6d0cec2..3429f4b7 100644 --- a/Server/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php +++ b/Server/application/cunkebao/controller/plan/PostCreateAddFriendPlanV1Controller.php @@ -127,7 +127,6 @@ class PostCreateAddFriendPlanV1Controller extends Controller ]; - try { Db::startTrans(); // 插入数据 @@ -263,6 +262,12 @@ class PostCreateAddFriendPlanV1Controller extends Controller } } + //群获客 + if($params['sceneId'] == 7){ + + } + + Db::commit(); return ResponseHelper::success(['planId' => $planId], '添加计划任务成功'); diff --git a/Server/application/job/WorkbenchGroupPushJob.php b/Server/application/job/WorkbenchGroupPushJob.php new file mode 100644 index 00000000..cf9e77c2 --- /dev/null +++ b/Server/application/job/WorkbenchGroupPushJob.php @@ -0,0 +1,401 @@ +logJobStart($jobId, $queueLockKey); + $this->execute(); + $this->handleJobSuccess($job, $queueLockKey); + return true; + } catch (\Exception $e) { + return $this->handleJobError($e, $job, $queueLockKey); + } + } + + /** + * 执行任务 + * @throws \Exception + */ + public function execute() + { + try { + // 获取所有工作台 + $workbenches = Workbench::where(['status' => 1, 'type' => 3, 'isDel' => 0])->order('id desc')->select(); + foreach ($workbenches as $workbench) { + // 获取工作台配置 + $config = WorkbenchGroupPush::where('workbenchId', $workbench->id)->find(); + if (!$config) { + continue; + } + + //判断是否推送 + $isPush = $this->isPush($workbench, $config); + if (empty($isPush)) { + continue; + } + + // 获取内容库 + $contentLibrary = $this->getContentLibrary($workbench, $config); + if (empty($contentLibrary)) { + continue; + } + // 处理内容发送 + $this->sendMsgToGroup($workbench, $config, $contentLibrary); + } + } catch (\Exception $e) { + Log::error("消息群发任务异常: " . $e->getMessage()); + throw $e; + } + } + + + // 发微信个人消息 + public function sendMsgToGroup($workbench, $config, $msgConf) + { + // 消息拼接 msgType(1:文本 3:图片 43:视频 47:动图表情包(gif、其他表情包) 49:小程序/其他:图文、文件) + // 当前,type 为文本、图片、动图表情包的时候,content为string, 其他情况为对象 {type: 'file/link/...', url: '', title: '', thunmbPath: '', desc: ''} + // $result = [ + // "content" => $dataArray['content'], + // "msgSubType" => 0, + // "msgType" => $dataArray['msgType'], + // "seq" => time(), + // "wechatAccountId" => $dataArray['wechatAccountId'], + // "wechatChatroomId" => 0, + // "wechatFriendId" => $dataArray['wechatFriendId'], + // ]; + + + $groups = json_decode($config['groups'], true); + $groupsData = Db::name('wechat_group')->whereIn('id', $groups)->field('id,wechatAccountId,chatroomId,companyId,ownerWechatId')->select(); + if (empty($groupsData)) { + return false; + } + + $toAccountId = ''; + $username = Env::get('api.username', ''); + $password = Env::get('api.password', ''); + if (!empty($username) || !empty($password)) { + $toAccountId = Db::name('users')->where('account', $username)->value('s2_accountId'); + } + // 建立WebSocket + $wsController = new WebSocketController(['userName' => $username, 'password' => $password, 'accountId' => $toAccountId]); + foreach ($msgConf as $content) { + $sendData = []; + $sqlData = []; + + foreach ($groupsData as $groups) { + // msgType(1:文本 3:图片 43:视频 47:动图表情包(gif、其他表情包) 49:小程序/其他:图文、文件) + $sqlData[] = [ + 'workbenchId' => $workbench['id'], + 'contentId' => $content['id'], + 'groupId' => $groups['id'], + 'wechatAccountId' => $groups['wechatAccountId'], + 'createTime' => time() + ]; + + //内容 + if (!empty($content['content'])) { + $sendData[] = [ + 'content' => $content['content'], + 'msgType' => 1, + 'wechatAccountId' => $groups['wechatAccountId'], + 'wechatChatroomId' => $groups['id'], + ]; + } + + switch ($content['contentType']) { + case 1: + //图片解析 + $imgs = json_decode($content['resUrls'], true); + if (!empty($imgs)) { + foreach ($imgs as $img) { + $sendData[] = [ + 'content' => $img, + 'msgType' => 3, + 'wechatAccountId' => $groups['wechatAccountId'], + 'wechatChatroomId' => $groups['id'], + ]; + } + } + break; + case 2: + //链接解析 + $url = json_decode($content['urls'], true); + if (!empty($url[0])) { + $url = $url[0]; + $sendData[] = [ + 'content' => [ + 'desc' => '', + 'thumbPath' => $url['image'], + 'title' => $url['desc'], + 'type' => 'link', + 'url' => $url['url'], + ], + 'msgType' => 49, + 'wechatAccountId' => $groups['wechatAccountId'], + 'wechatChatroomId' => $groups['id'], + ]; + } + + break; + case 3: + //视频解析 + $video = json_decode($content['urls'], true); + if (!empty($video)) { + $video = $video[0]; + } + $sendData[] = [ + 'content' => $video, + 'msgType' => 43, + 'wechatAccountId' => $groups['wechatAccountId'], + 'wechatChatroomId' => $groups['id'], + ]; + break; + } + + if (empty($sendData)) { + continue; + } + + //发送消息 + foreach ($sendData as $send) { + $wsController->sendCommunity($send); + } + //插入发送记录 + Db::name('workbench_group_push_item')->insertAll($sqlData); + } + } + } + + + /** + * 记录发送历史 + * @param Workbench $workbench + * @param array $devices + * @param array $contentLibrary + */ + protected function recordSendHistory($workbench, $devices, $contentLibrary) + { + $now = time(); + $data = []; + foreach ($devices as $device) { + $data = [ + 'workbenchId' => $workbench->id, + 'deviceId' => $device['deviceId'], + 'contentId' => $contentLibrary['id'], + 'wechatAccountId' => $device['wechatAccountId'], + 'createTime' => $now, + ]; + Db::name('workbench_group_push_item')->insert($data); + } + + } + + /** + * 获取设备列表 + * @param Workbench $workbench 工作台 + * @param WorkbenchGroupPush $config 配置 + * @return array|bool + */ + protected function isPush($workbench, $config) + { + // 检查发送间隔(新逻辑:根据startTime、endTime、maxPerDay动态计算) + $today = date('Y-m-d'); + $startTimestamp = strtotime($today . ' ' . $config['startTime'] . ':00'); + $endTimestamp = strtotime($today . ' ' . $config['endTime'] . ':00'); + + // 如果时间不符,则跳过 + if (($startTimestamp > time() || $endTimestamp < time()) && empty($config['pushType'])) { + return false; + } + + $totalSeconds = $endTimestamp - $startTimestamp; + if ($totalSeconds <= 0 || empty($config['maxPerDay'])) { + return false; + } + $interval = floor($totalSeconds / $config['maxPerDay']); + + + // 查询今日已同步次数 + $count = Db::name('workbench_group_push_item') + ->where('workbenchId', $workbench->id) + ->whereTime('createTime', 'between', [$startTimestamp, $endTimestamp]) + ->count(); + if ($count >= $config['maxPerDay']) { + return false; + } + + // 计算本次同步的最早允许时间 + $nextSyncTime = $startTimestamp + $count * $interval; + if (time() < $nextSyncTime) { + return false; + } + return true; + } + + /** + * 获取内容库 + * @param Workbench $workbench 工作台 + * @param WorkbenchGroupPush $config 配置 + * @return array|bool + */ + protected function getContentLibrary($workbench, $config) + { + $contentids = json_decode($config['contentLibraries'], true); + if (empty($contentids)) { + return false; + } + + if ($config['pushType'] == 1) { + $limit = 10; + } else { + $limit = 1; + } + + + //推送顺序 + if ($config['pushOrder'] == 1) { + $order = 'ci.sendTime desc, ci.id asc'; + } else { + $order = 'ci.sendTime desc, ci.id desc'; + } + + // 基础查询 + $query = Db::name('content_library')->alias('cl') + ->join('content_item ci', 'ci.libraryId = cl.id') + ->join('workbench_group_push_item wgpi', 'wgpi.contentId = ci.id and wgpi.workbenchId = ' . $workbench->id, 'left') + ->where(['cl.isDel' => 0, 'ci.isDel' => 0]) + ->where('ci.sendTime <= ' . (time() + 60)) + ->whereIn('cl.id', $contentids) + ->field([ + 'ci.id', + 'ci.libraryId', + 'ci.contentType', + 'ci.title', + 'ci.content', + 'ci.resUrls', + 'ci.urls', + 'ci.comment', + 'ci.sendTime' + ]); + // 复制 query + $query2 = clone $query; + $query3 = clone $query; + // 根据accountType处理不同的发送逻辑 + if ($config['isLoop'] == 1) { + // 可以循环发送 + // 1. 优先获取未发送的内容 + $unsentContent = $query->where('wgpi.id', 'null') + ->order($order) + ->limit(0, $limit) + ->select(); + + if (!empty($unsentContent)) { + return $unsentContent; + } + $lastSendData = Db::name('workbench_group_push_item')->where('workbenchId', $workbench->id)->order('id desc')->find(); + $fastSendData = Db::name('workbench_group_push_item')->where('workbenchId', $workbench->id)->order('id asc')->find(); + + $sentContent = $query2->where('wgpi.contentId', '<', $lastSendData['contentId'])->order('wgpi.id ASC')->group('wgpi.contentId')->limit(0, $limit)->select(); + + if (empty($sentContent)) { + $sentContent = $query3->where('wgpi.contentId', '=', $fastSendData['contentId'])->order('wgpi.id ASC')->group('wgpi.contentId')->limit(0, $limit)->select(); + } + return $sentContent; + } else { + // 不能循环发送,只获取未发送的内容 + $list = $query->where('wgpi.id', 'null') + ->order($order) + ->limit(0, $limit) + ->select(); + return $list; + } + } + + /** + * 记录任务开始 + * @param string $jobId + * @param string $queueLockKey + */ + protected function logJobStart($jobId, $queueLockKey) + { + Log::info('开始处理工作台消息群发任务: ' . json_encode([ + 'jobId' => $jobId, + 'queueLockKey' => $queueLockKey + ])); + } + + /** + * 处理任务成功 + * @param Job $job + * @param string $queueLockKey + */ + protected function handleJobSuccess($job, $queueLockKey) + { + $job->delete(); + Cache::rm($queueLockKey); + Log::info('工作台消息群发任务执行成功'); + } + + /** + * 处理任务错误 + * @param \Exception $e + * @param Job $job + * @param string $queueLockKey + * @return bool + */ + protected function handleJobError(\Exception $e, $job, $queueLockKey) + { + Log::error('工作台消息群发任务异常:' . $e->getMessage()); + + if (!empty($queueLockKey)) { + Cache::rm($queueLockKey); + Log::info("由于异常释放队列锁: {$queueLockKey}"); + } + + if ($job->attempts() > self::MAX_RETRY_ATTEMPTS) { + $job->delete(); + } else { + $job->release(Config::get('queue.failed_delay', 10)); + } + + return false; + } +} \ No newline at end of file diff --git a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php index a3751975..5c4159ad 100644 --- a/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php +++ b/Server/extend/WeChatDeviceApi/Adapters/ChuKeBao/Adapter.php @@ -1356,6 +1356,39 @@ class Adapter implements WeChatServiceInterface } while ($affected > 0); } + public function syncWechatGroupCustomer() + { + $sql = "insert into ck_wechat_group_member(`identifier`,`chatroomId`,`companyId`,`groupId`,`createTime`) + SELECT + m.wechatId identifier, + g.chatroomId chatroomId, + c.departmentId companyId, + g.id groupId, + m.createTime createTime + FROM + s2_wechat_chatroom_member m + LEFT JOIN s2_wechat_chatroom g ON g.chatroomId = m.chatroomId + LEFT JOIN s2_company_account c ON g.accountId = c.id + ORDER BY m.id DESC + LIMIT ?, ? + ON DUPLICATE KEY UPDATE + identifier=VALUES(identifier), + chatroomId=VALUES(chatroomId), + companyId=VALUES(companyId), + groupId=VALUES(groupId)"; + + $offset = 0; + $limit = 2000; + $usleepTime = 50000; + do { + $affected = Db::execute($sql, [$offset, $limit]); + $offset += $limit; + if ($affected > 0) { + usleep($usleepTime); + } + } while ($affected > 0); + } + } diff --git a/nkebao/.env.development b/nkebao/.env.development index 7afdd84c..b9f53856 100644 --- a/nkebao/.env.development +++ b/nkebao/.env.development @@ -1,4 +1,4 @@ # 基础环境变量示例 # VITE_API_BASE_URL=http://www.yishi.com -VITE_API_BASE_URL=https://ckbapi.quwanzhi.com +VITE_API_BASE_URL=http://www.yishi.com VITE_APP_TITLE=存客宝 From 662a192b811c23091274eec7fb80e969bd35d40d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Thu, 7 Aug 2025 14:52:06 +0800 Subject: [PATCH 46/93] =?UTF-8?q?FEAT=20=3D>=20=E6=9C=AC=E6=AC=A1=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=A1=B9=E7=9B=AE=E4=B8=BA=EF=BC=9A=20=E7=BE=A4?= =?UTF-8?q?=E7=BB=84=E9=80=89=E6=8B=A9=E6=9E=84=E5=BB=BA=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/components/GroupSelection/data.ts | 43 +++ .../GroupSelection/index.module.scss | 46 +-- .../src/components/GroupSelection/index.tsx | 299 +++--------------- .../GroupSelection/selectionPopup.tsx | 220 +++++++++++++ .../plan/new/steps/BasicSettings.tsx | 35 +- nkebao/src/pages/mobile/test/select.tsx | 63 ++-- .../workspace/group-push/detail/index.tsx | 1 - .../form/components/BasicSettings.tsx | 8 +- .../form/components/GroupSelector.tsx | 255 +++------------ .../workspace/group-push/list/index.tsx | 1 - 10 files changed, 403 insertions(+), 568 deletions(-) create mode 100644 nkebao/src/components/GroupSelection/data.ts create mode 100644 nkebao/src/components/GroupSelection/selectionPopup.tsx diff --git a/nkebao/src/components/GroupSelection/data.ts b/nkebao/src/components/GroupSelection/data.ts new file mode 100644 index 00000000..1bee0032 --- /dev/null +++ b/nkebao/src/components/GroupSelection/data.ts @@ -0,0 +1,43 @@ +// 群组接口类型 +export interface WechatGroup { + id: string; + chatroomId: string; + name: string; + avatar: string; + ownerWechatId: string; + ownerNickname: string; + ownerAvatar: string; +} + +export interface GroupSelectionItem { + id: string; + avatar: string; + chatroomId?: string; + createTime?: number; + identifier?: string; + name: string; + ownerAlias?: string; + ownerAvatar?: string; + ownerNickname?: string; + ownerWechatId?: string; + [key: string]: any; +} + +// 组件属性接口 +export interface GroupSelectionProps { + selectedGroups: GroupSelectionItem[]; + onSelect: (groups: GroupSelectionItem[]) => void; + onSelectDetail?: (groups: WechatGroup[]) => void; + placeholder?: string; + className?: string; + visible?: boolean; + onVisibleChange?: (visible: boolean) => void; + selectedListMaxHeight?: number; + showInput?: boolean; + showSelectedList?: boolean; + readonly?: boolean; + onConfirm?: ( + selectedIds: string[], + selectedItems: GroupSelectionItem[], + ) => void; // 新增 +} diff --git a/nkebao/src/components/GroupSelection/index.module.scss b/nkebao/src/components/GroupSelection/index.module.scss index 8eb2b72e..bedba3ef 100644 --- a/nkebao/src/components/GroupSelection/index.module.scss +++ b/nkebao/src/components/GroupSelection/index.module.scss @@ -17,6 +17,21 @@ font-size: 16px; background: #f8f9fa; } +.selectedListRow { + padding: 8px; + border-bottom: 1px solid #f0f0f0; + font-size: 14px; +} +.selectedListRowContent { + flex: 1; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} +.selectedListRowContentText { + flex: 1; +} .popupContainer { display: flex; @@ -77,42 +92,11 @@ align-items: center; padding: 16px 24px; border-bottom: 1px solid #f0f0f0; - cursor: pointer; transition: background 0.2s; &:hover { background: #f5f6fa; } } -.radioWrapper { - margin-right: 12px; - display: flex; - align-items: center; - justify-content: center; -} -.radioSelected { - width: 20px; - height: 20px; - border-radius: 50%; - border: 2px solid #1890ff; - display: flex; - align-items: center; - justify-content: center; -} -.radioUnselected { - width: 20px; - height: 20px; - border-radius: 50%; - border: 2px solid #e5e6eb; - display: flex; - align-items: center; - justify-content: center; -} -.radioDot { - width: 12px; - height: 12px; - border-radius: 50%; - background: #1890ff; -} .groupInfo { display: flex; align-items: center; diff --git a/nkebao/src/components/GroupSelection/index.tsx b/nkebao/src/components/GroupSelection/index.tsx index 423cf142..decb8288 100644 --- a/nkebao/src/components/GroupSelection/index.tsx +++ b/nkebao/src/components/GroupSelection/index.tsx @@ -1,40 +1,10 @@ -import React, { useState, useEffect } from "react"; +import React, { useState } from "react"; import { SearchOutlined, DeleteOutlined } from "@ant-design/icons"; import { Button, Input } from "antd"; -import { Popup } from "antd-mobile"; -import { getGroupList } from "./api"; +import { Avatar } from "antd-mobile"; import style from "./index.module.scss"; -import Layout from "@/components/Layout/Layout"; -import PopupHeader from "@/components/PopuLayout/header"; -import PopupFooter from "@/components/PopuLayout/footer"; - -// 群组接口类型 -interface WechatGroup { - id: string; - chatroomId: string; - name: string; - avatar: string; - ownerWechatId: string; - ownerNickname: string; - ownerAvatar: string; -} - -// 组件属性接口 -interface GroupSelectionProps { - selectedGroups: string[]; - onSelect: (groups: string[]) => void; - onSelectDetail?: (groups: WechatGroup[]) => void; - placeholder?: string; - className?: string; - visible?: boolean; - onVisibleChange?: (visible: boolean) => void; - selectedListMaxHeight?: number; - showInput?: boolean; - showSelectedList?: boolean; - readonly?: boolean; - onConfirm?: (selectedIds: string[], selectedItems: WechatGroup[]) => void; // 新增 -} - +import SelectionPopup from "./selectionPopup"; +import { GroupSelectionProps } from "./data"; export default function GroupSelection({ selectedGroups, onSelect, @@ -50,22 +20,11 @@ export default function GroupSelection({ onConfirm, }: GroupSelectionProps) { const [popupVisible, setPopupVisible] = useState(false); - const [groups, setGroups] = useState([]); - const [searchQuery, setSearchQuery] = useState(""); - const [currentPage, setCurrentPage] = useState(1); - const [totalPages, setTotalPages] = useState(1); - const [totalGroups, setTotalGroups] = useState(0); - const [loading, setLoading] = useState(false); - - // 获取已选群聊详细信息 - const selectedGroupObjs = groups.filter(group => - selectedGroups.includes(group.id), - ); // 删除已选群聊 const handleRemoveGroup = (id: string) => { if (readonly) return; - onSelect(selectedGroups.filter(g => g !== id)); + onSelect(selectedGroups.filter(g => g.id !== id)); }; // 受控弹窗逻辑 @@ -78,73 +37,7 @@ export default function GroupSelection({ // 打开弹窗 const openPopup = () => { if (readonly) return; - setCurrentPage(1); - setSearchQuery(""); setRealVisible(true); - fetchGroups(1, ""); - }; - - // 当页码变化时,拉取对应页数据(弹窗已打开时) - useEffect(() => { - if (realVisible && currentPage !== 1) { - fetchGroups(currentPage, searchQuery); - } - }, [currentPage, realVisible, searchQuery]); - - // 搜索防抖 - useEffect(() => { - if (!realVisible) return; - const timer = setTimeout(() => { - setCurrentPage(1); - fetchGroups(1, searchQuery); - }, 500); - - return () => clearTimeout(timer); - }, [searchQuery, realVisible]); - - // 获取群聊列表API - const fetchGroups = async (page: number, keyword: string = "") => { - setLoading(true); - try { - const params: any = { - page, - limit: 20, - }; - - if (keyword.trim()) { - params.keyword = keyword.trim(); - } - - const response = await getGroupList(params); - if (response && response.list) { - setGroups(response.list); - setTotalGroups(response.total || 0); - setTotalPages(Math.ceil((response.total || 0) / 20)); - } - } catch (error) { - console.error("获取群聊列表失败:", error); - } finally { - setLoading(false); - } - }; - - // 处理群聊选择 - const handleGroupToggle = (groupId: string) => { - if (readonly) return; - - const newSelectedGroups = selectedGroups.includes(groupId) - ? selectedGroups.filter(id => id !== groupId) - : [...selectedGroups, groupId]; - - onSelect(newSelectedGroups); - - // 如果有 onSelectDetail 回调,传递完整的群聊对象 - if (onSelectDetail) { - const selectedGroupObjs = groups.filter(group => - newSelectedGroups.includes(group.id), - ); - onSelectDetail(selectedGroupObjs); - } }; // 获取显示文本 @@ -153,14 +46,6 @@ export default function GroupSelection({ return `已选择 ${selectedGroups.length} 个群聊`; }; - // 确认选择 - const handleConfirm = () => { - if (onConfirm) { - onConfirm(selectedGroups, selectedGroupObjs); - } - setRealVisible(false); - }; - return ( <> {/* 输入框 */} @@ -182,7 +67,7 @@ export default function GroupSelection({
)} {/* 已选群聊列表窗口 */} - {showSelectedList && selectedGroupObjs.length > 0 && ( + {showSelectedList && selectedGroups.length > 0 && (
- {selectedGroupObjs.map(group => ( -
-
- {group.name || group.chatroomId || group.id} + {selectedGroups.map(group => ( +
+
+ +
+
{group.name}
+
{group.chatroomId}
+
+ {!readonly && ( +
- {!readonly && ( -
))}
)} {/* 弹窗 */} - setRealVisible(false)} - position="bottom" - bodyStyle={{ height: "100vh" }} - > - fetchGroups(currentPage, searchQuery)} - /> - } - footer={ - setRealVisible(false)} - onConfirm={handleConfirm} - /> - } - > -
- {loading ? ( -
-
加载中...
-
- ) : groups.length > 0 ? ( -
- {groups.map(group => ( - - ))} -
- ) : ( -
-
- {searchQuery - ? `没有找到包含"${searchQuery}"的群聊` - : "没有找到群聊"} -
-
- )} -
-
-
+ ); } diff --git a/nkebao/src/components/GroupSelection/selectionPopup.tsx b/nkebao/src/components/GroupSelection/selectionPopup.tsx new file mode 100644 index 00000000..de7fa8af --- /dev/null +++ b/nkebao/src/components/GroupSelection/selectionPopup.tsx @@ -0,0 +1,220 @@ +import React, { useState, useEffect } from "react"; +import { Popup, Checkbox } from "antd-mobile"; + +import { getGroupList } from "./api"; +import style from "./index.module.scss"; +import Layout from "@/components/Layout/Layout"; +import PopupHeader from "@/components/PopuLayout/header"; +import PopupFooter from "@/components/PopuLayout/footer"; +import { GroupSelectionItem } from "./data"; +// 群组接口类型 +interface WechatGroup { + id: string; + name: string; + avatar: string; + chatroomId?: string; + ownerWechatId?: string; + ownerNickname?: string; + ownerAvatar?: string; +} + +// 弹窗属性接口 +interface SelectionPopupProps { + visible: boolean; + onVisibleChange: (visible: boolean) => void; + selectedGroups: GroupSelectionItem[]; + onSelect: (groups: GroupSelectionItem[]) => void; + onSelectDetail?: (groups: WechatGroup[]) => void; + readonly?: boolean; + onConfirm?: ( + selectedIds: string[], + selectedItems: GroupSelectionItem[], + ) => void; +} + +export default function SelectionPopup({ + visible, + onVisibleChange, + selectedGroups, + onSelect, + onSelectDetail, + readonly = false, + onConfirm, +}: SelectionPopupProps) { + const [groups, setGroups] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [totalGroups, setTotalGroups] = useState(0); + const [loading, setLoading] = useState(false); + + // 获取群聊列表API + const fetchGroups = async (page: number, keyword: string = "") => { + setLoading(true); + try { + const params: any = { + page, + limit: 20, + }; + + if (keyword.trim()) { + params.keyword = keyword.trim(); + } + + const response = await getGroupList(params); + if (response && response.list) { + setGroups(response.list); + setTotalGroups(response.total || 0); + setTotalPages(Math.ceil((response.total || 0) / 20)); + } + } catch (error) { + console.error("获取群聊列表失败:", error); + } finally { + setLoading(false); + } + }; + + // 处理群聊选择 + const handleGroupToggle = (group: GroupSelectionItem) => { + if (readonly) return; + + const newSelectedGroups = selectedGroups.some(g => g.id === group.id) + ? selectedGroups.filter(g => g.id !== group.id) + : selectedGroups.concat(group); + + onSelect(newSelectedGroups); + + // 如果有 onSelectDetail 回调,传递完整的群聊对象 + if (onSelectDetail) { + const selectedGroupObjs = groups.filter(group => + newSelectedGroups.some(g => g.id === group.id), + ); + onSelectDetail(selectedGroupObjs); + } + }; + + // 确认选择 + const handleConfirm = () => { + if (onConfirm) { + onConfirm( + selectedGroups.map(g => g.id), + selectedGroups, + ); + } + onVisibleChange(false); + }; + + // 弹窗打开时初始化数据(只执行一次) + useEffect(() => { + if (visible) { + setCurrentPage(1); + setSearchQuery(""); + fetchGroups(1, ""); + } + }, [visible]); + + // 搜索防抖(只在弹窗打开且搜索词变化时执行) + useEffect(() => { + if (!visible || searchQuery === "") return; + + const timer = setTimeout(() => { + setCurrentPage(1); + fetchGroups(1, searchQuery); + }, 500); + + return () => clearTimeout(timer); + }, [searchQuery, visible]); + + // 页码变化时请求数据(只在弹窗打开且页码不是1时执行) + useEffect(() => { + if (!visible || currentPage === 1) return; + fetchGroups(currentPage, searchQuery); + }, [currentPage, visible, searchQuery]); + + return ( + onVisibleChange(false)} + position="bottom" + bodyStyle={{ height: "100vh" }} + > + fetchGroups(currentPage, searchQuery)} + /> + } + footer={ + onVisibleChange(false)} + onConfirm={handleConfirm} + /> + } + > +
+ {loading ? ( +
+
加载中...
+
+ ) : groups.length > 0 ? ( +
+ {groups.map(group => ( +
+ g.id === group.id)} + onChange={() => !readonly && handleGroupToggle(group)} + disabled={readonly} + style={{ marginRight: 12 }} + /> +
+
+ {group.avatar ? ( + {group.name} + ) : ( + group.name.charAt(0) + )} +
+
+
{group.name}
+
+ 群ID: {group.chatroomId} +
+ {group.ownerNickname && ( +
+ 群主: {group.ownerNickname} +
+ )} +
+
+
+ ))} +
+ ) : ( +
+
+ {searchQuery + ? `没有找到包含"${searchQuery}"的群聊` + : "没有找到群聊"} +
+
+ )} +
+
+
+ ); +} diff --git a/nkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx b/nkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx index 0b9fbd7a..b26279fd 100644 --- a/nkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx +++ b/nkebao/src/pages/mobile/scenarios/plan/new/steps/BasicSettings.tsx @@ -86,12 +86,6 @@ const BasicSettings: React.FC = ({ } }, [formData, onChange]); - useEffect(() => { - const today = new Date().toLocaleDateString("zh-CN").replace(/\//g, ""); - const sceneItem = sceneList.find(v => formData.scenario === v.id); - onChange({ ...formData, name: `${sceneItem?.name || "海报"}${today}` }); - }, [isEdit]); - useEffect(() => { setTips(formData.tips || ""); }, [formData.tips]); @@ -202,11 +196,6 @@ const BasicSettings: React.FC = ({ window.URL.revokeObjectURL(url); }; - // 图片预览关闭 - const handleImagePreviewClose = () => { - setIsPreviewOpen(false); - }; - // 当前选中的场景对象 const currentScene = sceneList.find(s => s.id === formData.scenario); //打开订单 @@ -537,29 +526,7 @@ const BasicSettings: React.FC = ({
)} - {/* 微信群设置区块,仅在选择微信群场景时显示 */} - {formData.scenario === 7 && ( -
-
- onChange({ ...formData, weixinqunName })} - /> -
-
- onChange({ ...formData, weixinqunNotice })} - /> -
-
- )} +
是否启用 { - const navigate = useNavigate(); - const [activeTab, setActiveTab] = useState("devices"); + const [activeTab, setActiveTab] = useState("groups"); // 设备选择状态 const [selectedDevices, setSelectedDevices] = useState([]); @@ -22,7 +20,9 @@ const ComponentTest: React.FC = () => { const [selectedFriends, setSelectedFriends] = useState([]); // 群组选择状态 - const [selectedGroups, setSelectedGroups] = useState([]); + const [selectedGroups, setSelectedGroups] = useState( + [], + ); // 内容库选择状态 const [selectedLibraries, setSelectedLibraries] = useState([]); @@ -41,6 +41,32 @@ const ComponentTest: React.FC = () => { )} + +
+

GroupSelection 组件测试

+ +
+ 已选群组: {selectedGroups.length} 个 +
+ 群组ID:{" "} + {selectedGroups.map(g => g.id).join(", ") || "无"} +
+
+
+

DeviceSelection 组件测试

@@ -91,31 +117,6 @@ const ComponentTest: React.FC = () => {
- -
-

GroupSelection 组件测试

- -
- 已选群组: {selectedGroups.length} 个 -
- 群组ID: {selectedGroups.join(", ") || "无"} -
-
-
-

diff --git a/nkebao/src/pages/mobile/workspace/group-push/detail/index.tsx b/nkebao/src/pages/mobile/workspace/group-push/detail/index.tsx index ab2a9a0f..c38ba67e 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/detail/index.tsx +++ b/nkebao/src/pages/mobile/workspace/group-push/detail/index.tsx @@ -140,7 +140,6 @@ const Detail: React.FC = () => { 群发推送详情

} - footer={} >
diff --git a/nkebao/src/pages/mobile/workspace/group-push/form/components/BasicSettings.tsx b/nkebao/src/pages/mobile/workspace/group-push/form/components/BasicSettings.tsx index 34ba3f9e..2180c9d7 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/form/components/BasicSettings.tsx +++ b/nkebao/src/pages/mobile/workspace/group-push/form/components/BasicSettings.tsx @@ -53,7 +53,7 @@ const BasicSettings: React.FC = ({ return (
-
+
{/* 任务名称 */}
*任务名称: @@ -123,11 +123,12 @@ const BasicSettings: React.FC = ({ {/* 推送顺序 */}
推送顺序: - +
@@ -135,10 +136,11 @@ const BasicSettings: React.FC = ({ type={values.pushOrder === "latest" ? "primary" : "default"} onClick={() => handleChange("pushOrder", "latest")} disabled={loading} + style={{ borderRadius: "0 6px 6px 0", marginLeft: -1 }} > 按最新 - +
{/* 是否循环推送 */}
void; + selectedGroups: string[]; + onGroupsChange: (groups: string[]) => void; onPrevious: () => void; onNext: () => void; onSave: () => void; @@ -23,69 +12,6 @@ interface GroupSelectorProps { loading?: boolean; } -const mockGroups: WechatGroup[] = [ - { - id: "1", - name: "VIP客户群", - avatar: "https://via.placeholder.com/40", - serviceAccount: { - id: "1", - name: "客服小美", - avatar: "https://via.placeholder.com/32", - }, - }, - { - id: "2", - name: "潜在客户群", - avatar: "https://via.placeholder.com/40", - serviceAccount: { - id: "1", - name: "客服小美", - avatar: "https://via.placeholder.com/32", - }, - }, - { - id: "3", - name: "活动群", - avatar: "https://via.placeholder.com/40", - serviceAccount: { - id: "2", - name: "推广专员", - avatar: "https://via.placeholder.com/32", - }, - }, - { - id: "4", - name: "推广群", - avatar: "https://via.placeholder.com/40", - serviceAccount: { - id: "2", - name: "推广专员", - avatar: "https://via.placeholder.com/32", - }, - }, - { - id: "5", - name: "新客户群", - avatar: "https://via.placeholder.com/40", - serviceAccount: { - id: "3", - name: "销售小王", - avatar: "https://via.placeholder.com/32", - }, - }, - { - id: "6", - name: "体验群", - avatar: "https://via.placeholder.com/40", - serviceAccount: { - id: "3", - name: "销售小王", - avatar: "https://via.placeholder.com/32", - }, - }, -]; - const GroupSelector: React.FC = ({ selectedGroups, onGroupsChange, @@ -95,150 +21,59 @@ const GroupSelector: React.FC = ({ onCancel, loading = false, }) => { - const [searchTerm, setSearchTerm] = useState(""); - const [groups] = useState(mockGroups); + // 将WechatGroup转换为GroupSelection需要的格式 + // 群组选择状态 - const filteredGroups = groups.filter( - group => - group.name.toLowerCase().includes(searchTerm.toLowerCase()) || - group.serviceAccount.name - .toLowerCase() - .includes(searchTerm.toLowerCase()), - ); - - const handleGroupToggle = (group: WechatGroup, checked: boolean) => { - if (checked) { - onGroupsChange([...selectedGroups, group]); - } else { - onGroupsChange(selectedGroups.filter(g => g.id !== group.id)); - } - }; - - const handleSelectAll = () => { - if (selectedGroups.length === filteredGroups.length) { - onGroupsChange([]); - } else { - onGroupsChange(filteredGroups); - } - }; - - const isGroupSelected = (groupId: string) => { - return selectedGroups.some(group => group.id === groupId); + const handleGroupSelect = (groupIds: string[]) => { + onGroupsChange(groupIds); }; return ( -
- -
-
- 搜索群组: - } - placeholder="搜索群组名称或客服名称" - value={searchTerm} - onChange={e => setSearchTerm(e.target.value)} - disabled={loading} - style={{ marginTop: 4 }} - /> -
-
- 0 - } - onChange={handleSelectAll} - disabled={loading} - > - 全选 ({selectedGroups.length}/{filteredGroups.length}) - -
-
- {filteredGroups.map(group => ( -
- handleGroupToggle(group, e.target.checked)} - disabled={loading} - style={{ marginRight: 8 }} - /> - } - style={{ marginRight: 8 }} - /> -
-
{group.name}
-
- - {group.serviceAccount.name} -
-
-
- ))} - {filteredGroups.length === 0 && ( -
- - 没有找到匹配的群组 -
- )} -
-
-
+
+
+

+ 选择推送群组 +

+

+ 请选择要推送消息的微信群组 +

+
+ + +
- - - +
+ + + +
); diff --git a/nkebao/src/pages/mobile/workspace/group-push/list/index.tsx b/nkebao/src/pages/mobile/workspace/group-push/list/index.tsx index b12dedc5..e578a82e 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/list/index.tsx +++ b/nkebao/src/pages/mobile/workspace/group-push/list/index.tsx @@ -9,7 +9,6 @@ import { ClockCircleOutlined, EditOutlined, DeleteOutlined, - EyeOutlined, CopyOutlined, SendOutlined, CarryOutOutlined, From 67c76d8b0483e777b771a47ed0fb69b2f5756da5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Thu, 7 Aug 2025 16:15:45 +0800 Subject: [PATCH 47/93] =?UTF-8?q?FEAT=20=3D>=20=E6=9C=AC=E6=AC=A1=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=A1=B9=E7=9B=AE=E4=B8=BA=EF=BC=9A=20=E5=AD=98?= =?UTF-8?q?=E4=B8=AA=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/pages/mobile/test/select.tsx | 58 ++--- .../form/components/BasicSettings.tsx | 36 +-- .../form/components/ContentSelector.tsx | 239 +++--------------- .../form/components/GroupSelector.tsx | 47 +--- .../workspace/group-push/form/index.tsx | 149 +++++++---- nkebao/src/styles/global.scss | 12 + 6 files changed, 188 insertions(+), 353 deletions(-) diff --git a/nkebao/src/pages/mobile/test/select.tsx b/nkebao/src/pages/mobile/test/select.tsx index d1a1d63c..6255ffce 100644 --- a/nkebao/src/pages/mobile/test/select.tsx +++ b/nkebao/src/pages/mobile/test/select.tsx @@ -11,7 +11,7 @@ import { isDevelopment } from "@/utils/env"; import { GroupSelectionItem } from "@/components/GroupSelection/data"; const ComponentTest: React.FC = () => { - const [activeTab, setActiveTab] = useState("groups"); + const [activeTab, setActiveTab] = useState("libraries"); // 设备选择状态 const [selectedDevices, setSelectedDevices] = useState([]); @@ -41,6 +41,34 @@ const ComponentTest: React.FC = () => { )} + +
+

+ ContentLibrarySelection 组件测试 +

+ +
+ 已选内容库: {selectedLibraries.length} 个 +
+ 内容库ID:{" "} + {selectedLibraries.join(", ") || "无"} +
+
+
+

GroupSelection 组件测试

@@ -117,34 +145,6 @@ const ComponentTest: React.FC = () => {
- -
-

- ContentLibrarySelection 组件测试 -

- -
- 已选内容库: {selectedLibraries.length} 个 -
- 内容库ID:{" "} - {selectedLibraries.join(", ") || "无"} -
-
-
-

AccountSelection 组件测试

diff --git a/nkebao/src/pages/mobile/workspace/group-push/form/components/BasicSettings.tsx b/nkebao/src/pages/mobile/workspace/group-push/form/components/BasicSettings.tsx index 2180c9d7..255842e4 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/form/components/BasicSettings.tsx +++ b/nkebao/src/pages/mobile/workspace/group-push/form/components/BasicSettings.tsx @@ -15,7 +15,6 @@ interface BasicSettingsProps { }; onNext: (values: any) => void; onSave: (values: any) => void; - onCancel: () => void; loading?: boolean; } @@ -32,7 +31,6 @@ const BasicSettings: React.FC = ({ }, onNext, onSave, - onCancel, loading = false, }) => { const [values, setValues] = useState(defaultValues); @@ -123,7 +121,7 @@ const BasicSettings: React.FC = ({ {/* 推送顺序 */}
推送顺序: -
+
-
- - - -
); }; diff --git a/nkebao/src/pages/mobile/workspace/group-push/form/components/ContentSelector.tsx b/nkebao/src/pages/mobile/workspace/group-push/form/components/ContentSelector.tsx index ebcb1b52..1c6f7634 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/form/components/ContentSelector.tsx +++ b/nkebao/src/pages/mobile/workspace/group-push/form/components/ContentSelector.tsx @@ -1,6 +1,5 @@ import React, { useState } from "react"; -import { Button, Card, Input, Checkbox, Avatar } from "antd"; -import { FileTextOutlined, SearchOutlined } from "@ant-design/icons"; +import ContentLibrarySelection from "@/components/ContentLibrarySelection"; interface ContentLibrary { id: string; @@ -17,226 +16,60 @@ interface ContentSelectorProps { onPrevious: () => void; onNext: () => void; onSave: () => void; - onCancel: () => void; loading?: boolean; } -const mockLibraries: ContentLibrary[] = [ - { - id: "1", - name: "产品推广内容库", - targets: [ - { id: "1", avatar: "https://via.placeholder.com/32" }, - { id: "2", avatar: "https://via.placeholder.com/32" }, - { id: "3", avatar: "https://via.placeholder.com/32" }, - ], - }, - { - id: "2", - name: "活动宣传内容库", - targets: [ - { id: "4", avatar: "https://via.placeholder.com/32" }, - { id: "5", avatar: "https://via.placeholder.com/32" }, - ], - }, - { - id: "3", - name: "客户服务内容库", - targets: [ - { id: "6", avatar: "https://via.placeholder.com/32" }, - { id: "7", avatar: "https://via.placeholder.com/32" }, - { id: "8", avatar: "https://via.placeholder.com/32" }, - { id: "9", avatar: "https://via.placeholder.com/32" }, - ], - }, - { - id: "4", - name: "节日问候内容库", - targets: [ - { id: "10", avatar: "https://via.placeholder.com/32" }, - { id: "11", avatar: "https://via.placeholder.com/32" }, - ], - }, - { - id: "5", - name: "新品发布内容库", - targets: [ - { id: "12", avatar: "https://via.placeholder.com/32" }, - { id: "13", avatar: "https://via.placeholder.com/32" }, - { id: "14", avatar: "https://via.placeholder.com/32" }, - ], - }, -]; - const ContentSelector: React.FC = ({ selectedLibraries, onLibrariesChange, onPrevious, onNext, onSave, - onCancel, loading = false, }) => { - const [searchTerm, setSearchTerm] = useState(""); - const [libraries] = useState(mockLibraries); + // 将 ContentLibrary[] 转换为 string[] 用于 ContentLibrarySelection + const selectedLibraryIds = selectedLibraries.map(lib => lib.id); - const filteredLibraries = libraries.filter(library => - library.name.toLowerCase().includes(searchTerm.toLowerCase()), - ); - - const handleLibraryToggle = (library: ContentLibrary, checked: boolean) => { - if (checked) { - onLibrariesChange([...selectedLibraries, library]); - } else { - onLibrariesChange(selectedLibraries.filter(l => l.id !== library.id)); - } + // 处理选择变化 + const handleLibrariesChange = (libraryIds: string[]) => { + // 这里需要根据选中的ID重新构建ContentLibrary对象 + // 由于ContentLibrarySelection只返回ID,我们需要从原始数据中获取完整信息 + // 暂时使用简化的处理方式 + const newSelectedLibraries = libraryIds.map(id => ({ + id, + name: `内容库 ${id}`, // 这里应该从API获取完整信息 + targets: [], // 这里应该从API获取完整信息 + })); + onLibrariesChange(newSelectedLibraries); }; - const handleSelectAll = () => { - if (selectedLibraries.length === filteredLibraries.length) { - onLibrariesChange([]); - } else { - onLibrariesChange(filteredLibraries); - } - }; - - const isLibrarySelected = (libraryId: string) => { - return selectedLibraries.some(library => library.id === libraryId); + // 处理选择详情变化 + const handleSelectDetail = (libraries: any[]) => { + // 将API返回的数据转换为ContentLibrary格式 + const convertedLibraries = libraries.map(lib => ({ + id: lib.id, + name: lib.name, + targets: [], // 这里需要根据实际情况获取targets数据 + })); + onLibrariesChange(convertedLibraries); }; return (
- -
-
- 搜索内容库: - } - placeholder="搜索内容库名称" - value={searchTerm} - onChange={e => setSearchTerm(e.target.value)} - disabled={loading} - style={{ marginTop: 4 }} - /> -
-
- 0 - } - onChange={handleSelectAll} - disabled={loading} - > - 全选 ({selectedLibraries.length}/{filteredLibraries.length}) - -
-
- {filteredLibraries.map(library => ( -
- handleLibraryToggle(library, e.target.checked)} - disabled={loading} - style={{ marginRight: 8 }} - /> - } - size={40} - style={{ - marginRight: 8, - background: "#e6f7ff", - color: "#1890ff", - }} - /> -
-
{library.name}
-
- 包含 {library.targets.length} 条内容 -
-
-
- {library.targets.slice(0, 3).map(target => ( - - ))} - {library.targets.length > 3 && ( -
- +{library.targets.length - 3} -
- )} -
-
- ))} - {filteredLibraries.length === 0 && ( -
- - 没有找到匹配的内容库 -
- )} -
+
+
+ 选择内容库:
- -
- - - - +
); diff --git a/nkebao/src/pages/mobile/workspace/group-push/form/components/GroupSelector.tsx b/nkebao/src/pages/mobile/workspace/group-push/form/components/GroupSelector.tsx index 7c18cee3..b77cecaf 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/form/components/GroupSelector.tsx +++ b/nkebao/src/pages/mobile/workspace/group-push/form/components/GroupSelector.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; -import { Button } from "antd"; import GroupSelection from "@/components/GroupSelection"; +import { GroupSelectionItem } from "@/components/GroupSelection/data"; interface GroupSelectorProps { selectedGroups: string[]; @@ -8,7 +8,6 @@ interface GroupSelectorProps { onPrevious: () => void; onNext: () => void; onSave: () => void; - onCancel: () => void; loading?: boolean; } @@ -18,13 +17,19 @@ const GroupSelector: React.FC = ({ onPrevious, onNext, onSave, - onCancel, loading = false, }) => { - // 将WechatGroup转换为GroupSelection需要的格式 - // 群组选择状态 + // 将string[]转换为GroupSelectionItem[] + const selectedGroupItems: GroupSelectionItem[] = selectedGroups.map(id => ({ + id, + name: `群组 ${id}`, + avatar: "", + chatroomId: id, + })); - const handleGroupSelect = (groupIds: string[]) => { + const handleGroupSelect = (groupItems: GroupSelectionItem[]) => { + // 将GroupSelectionItem[]转换回string[] + const groupIds = groupItems.map(item => item.id); onGroupsChange(groupIds); }; @@ -40,41 +45,13 @@ const GroupSelector: React.FC = ({
- -
- -
- - - -
-
); }; diff --git a/nkebao/src/pages/mobile/workspace/group-push/form/index.tsx b/nkebao/src/pages/mobile/workspace/group-push/form/index.tsx index 198b8ab0..71d3b2ae 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/form/index.tsx +++ b/nkebao/src/pages/mobile/workspace/group-push/form/index.tsx @@ -1,16 +1,14 @@ -import React, { useState } from "react"; -import { useNavigate } from "react-router-dom"; -import { Button } from "antd-mobile"; -import { NavBar } from "antd-mobile"; -import { ArrowLeftOutlined } from "@ant-design/icons"; +import React, { useState, useEffect } from "react"; +import { useNavigate, useParams } from "react-router-dom"; +import { Button } from "antd"; import { createGroupPushTask } from "./index.api"; import Layout from "@/components/Layout/Layout"; import StepIndicator from "@/components/StepIndicator"; import BasicSettings from "./components/BasicSettings"; import GroupSelector from "./components/GroupSelector"; import ContentSelector from "./components/ContentSelector"; -import type { WechatGroup, ContentLibrary, FormData } from "./index.data"; - +import type { ContentLibrary, FormData } from "./index.data"; +import NavCommon from "@/components/NavCommon"; const steps = [ { id: 1, title: "步骤 1", subtitle: "基础设置" }, { id: 2, title: "步骤 2", subtitle: "选择社群" }, @@ -19,6 +17,7 @@ const steps = [ ]; const NewGroupPush: React.FC = () => { + const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const [currentStep, setCurrentStep] = useState(1); const [loading, setLoading] = useState(false); @@ -36,13 +35,28 @@ const NewGroupPush: React.FC = () => { }); const [isEditMode, setIsEditMode] = useState(false); - const handleBasicSettingsNext = (values: Partial) => { + useEffect(() => { + if (!id) return; + setIsEditMode(true); + }, [id]); + + const handleBasicSettingsChange = (values: Partial) => { setFormData(prev => ({ ...prev, ...values })); - setCurrentStep(2); }; - const handleGroupsChange = (groups: WechatGroup[]) => { - setFormData(prev => ({ ...prev, groups })); + const handleGroupsChange = (groups: string[]) => { + // 将string[]转换为WechatGroup[] + const convertedGroups = groups.map(id => ({ + id, + name: `群组 ${id}`, + avatar: "", + serviceAccount: { + id: "", + name: "", + avatar: "", + }, + })); + setFormData(prev => ({ ...prev, groups: convertedGroups })); }; const handleLibrariesChange = (contentLibraries: ContentLibrary[]) => { @@ -99,30 +113,78 @@ const NewGroupPush: React.FC = () => { } }; - const handleCancel = () => { - navigate("/workspace/group-push"); + const handlePrevious = () => { + if (currentStep > 1) { + setCurrentStep(currentStep - 1); + } + }; + + const handleNext = () => { + if (currentStep < 4) { + setCurrentStep(currentStep + 1); + } + }; + + const canGoNext = () => { + switch (currentStep) { + case 1: { + return formData.name.trim() !== ""; + } + case 2: { + // 选择社群:检查是否选择了群组 + const groupsValid = + formData.groups.length > 0 && formData.groups.length <= 50; // 添加上限检查 + return groupsValid; + } + case 3: { + // 选择内容库:检查是否选择了内容库 + const librariesValid = + formData.contentLibraries.length > 0 && + formData.contentLibraries.length <= 20; // 添加上限检查 + return librariesValid; + } + case 4: { + // 京东联盟:可以进入下一步(保存) + // 这里可以添加京东联盟相关的验证逻辑 + return true; + } + default: + return false; + } + }; + + const renderFooter = () => { + if (currentStep === 4) { + return ( +
+ + +
+ ); + } + + return ( +
+ {currentStep > 1 && ( + + )} + +
+ ); }; return ( - navigate(-1)} - /> -
- } - > - - {isEditMode ? "编辑任务" : "新建任务"} - - - } + header={} + footer={renderFooter()} >
@@ -139,20 +201,18 @@ const NewGroupPush: React.FC = () => { isImmediatePush: formData.isImmediatePush, isEnabled: formData.isEnabled, }} - onNext={handleBasicSettingsNext} + onNext={handleBasicSettingsChange} onSave={handleSave} - onCancel={handleCancel} loading={loading} /> )} {currentStep === 2 && ( g.id)} onGroupsChange={handleGroupsChange} onPrevious={() => setCurrentStep(1)} onNext={() => setCurrentStep(3)} onSave={handleSave} - onCancel={handleCancel} loading={loading} /> )} @@ -163,31 +223,12 @@ const NewGroupPush: React.FC = () => { onPrevious={() => setCurrentStep(2)} onNext={() => setCurrentStep(4)} onSave={handleSave} - onCancel={handleCancel} loading={loading} /> )} {currentStep === 4 && (
京东联盟设置(此步骤为占位,实际功能待开发) -
- - - -
)}
diff --git a/nkebao/src/styles/global.scss b/nkebao/src/styles/global.scss index 6e4995b9..f822f8eb 100644 --- a/nkebao/src/styles/global.scss +++ b/nkebao/src/styles/global.scss @@ -304,3 +304,15 @@ button { } } } + +//底部下一步上一步组合按钮 +.footer-btn-group { + padding: 12px; + display: flex; + gap: 8px; + justify-content: center; + background-color: #fff; + .ant-btn { + flex: 1; + } +} From 504e8ff9b61b5ee2f7f3fb36550e42e275c19672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Thu, 7 Aug 2025 17:43:20 +0800 Subject: [PATCH 48/93] =?UTF-8?q?FEAT=20=3D>=20=E6=9C=AC=E6=AC=A1=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=A1=B9=E7=9B=AE=E4=B8=BA=EF=BC=9A=20=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E7=A4=BE=E7=BE=A4=E6=9E=84=E5=BB=BA=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../form/components/BasicSettings.tsx | 372 +++++++++--------- .../form/components/ContentSelector.tsx | 159 +++++--- .../form/components/GroupSelector.tsx | 130 +++--- .../workspace/group-push/form/index.data.ts | 3 +- .../workspace/group-push/form/index.tsx | 152 +++---- 5 files changed, 463 insertions(+), 353 deletions(-) diff --git a/nkebao/src/pages/mobile/workspace/group-push/form/components/BasicSettings.tsx b/nkebao/src/pages/mobile/workspace/group-push/form/components/BasicSettings.tsx index 255842e4..e83332ad 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/form/components/BasicSettings.tsx +++ b/nkebao/src/pages/mobile/workspace/group-push/form/components/BasicSettings.tsx @@ -1,6 +1,5 @@ -import React, { useState } from "react"; -import { Input, Button, Card, Switch } from "antd"; -import { MinusOutlined, PlusOutlined } from "@ant-design/icons"; +import React, { useImperativeHandle, forwardRef } from "react"; +import { Input, Button, Card, Switch, Form, InputNumber } from "antd"; interface BasicSettingsProps { defaultValues?: { @@ -18,194 +17,199 @@ interface BasicSettingsProps { loading?: boolean; } -const BasicSettings: React.FC = ({ - defaultValues = { - name: "", - pushTimeStart: "06:00", - pushTimeEnd: "23:59", - dailyPushCount: 20, - pushOrder: "latest", - isLoopPush: false, - isImmediatePush: false, - isEnabled: false, - }, - onNext, - onSave, - loading = false, -}) => { - const [values, setValues] = useState(defaultValues); +export interface BasicSettingsRef { + validate: () => Promise; + getValues: () => any; +} - const handleChange = (field: string, value: any) => { - setValues(prev => ({ ...prev, [field]: value })); - }; +const BasicSettings = forwardRef( + ( + { + defaultValues = { + name: "", + pushTimeStart: "06:00", + pushTimeEnd: "23:59", + dailyPushCount: 20, + pushOrder: "latest", + isLoopPush: false, + isImmediatePush: false, + isEnabled: false, + }, + }, + ref, + ) => { + const [form] = Form.useForm(); - const handleCountChange = (increment: boolean) => { - setValues(prev => ({ - ...prev, - dailyPushCount: increment - ? prev.dailyPushCount + 1 - : Math.max(1, prev.dailyPushCount - 1), + // 暴露方法给父组件 + useImperativeHandle(ref, () => ({ + validate: async () => { + try { + await form.validateFields(); + return true; + } catch (error) { + console.log("BasicSettings 表单验证失败:", error); + return false; + } + }, + getValues: () => { + return form.getFieldsValue(); + }, })); - }; - return ( -
- -
- {/* 任务名称 */} -
- *任务名称: - handleChange("name", e.target.value)} - placeholder="请输入任务名称" - style={{ marginTop: 4 }} - /> -
- {/* 允许推送的时间段 */} -
- 允许推送的时间段: -
- handleChange("pushTimeStart", e.target.value)} - style={{ width: 120 }} - /> - - handleChange("pushTimeEnd", e.target.value)} - style={{ width: 120 }} - /> -
-
- {/* 每日推送 */} -
- 每日推送: -
+ +
{ + // 可以在这里处理表单值变化 + }} + > + {/* 任务名称 */} + -
-
- {/* 推送顺序 */} -
- 推送顺序: -
- - -
-
- {/* 是否循环推送 */} -
- 是否循环推送: - handleChange("isLoopPush", checked)} - disabled={loading} - /> -
- {/* 是否立即推送 */} -
- 是否立即推送: - handleChange("isImmediatePush", checked)} - disabled={loading} - /> -
- {values.isImmediatePush && ( -
+ + {/* 推送顺序 */} + - 如果启用,系统会把内容库里所有的内容按顺序推送到指定的社群 -
- )} - {/* 是否启用 */} -
- 是否启用: - handleChange("isEnabled", checked)} - disabled={loading} - /> -
-
-
-
- ); -}; +
+ + +
+ + + {/* 是否循环推送 */} + + + + + {/* 是否立即推送 */} + + + + + {/* 是否启用 */} + + + + + {/* 立即推送提示 */} + + {() => { + const isImmediatePush = form.getFieldValue("isImmediatePush"); + return isImmediatePush ? ( +
+ 如果启用,系统会把内容库里所有的内容按顺序推送到指定的社群 +
+ ) : null; + }} +
+ + +
+ ); + }, +); + +BasicSettings.displayName = "BasicSettings"; export default BasicSettings; diff --git a/nkebao/src/pages/mobile/workspace/group-push/form/components/ContentSelector.tsx b/nkebao/src/pages/mobile/workspace/group-push/form/components/ContentSelector.tsx index 1c6f7634..e96c6e14 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/form/components/ContentSelector.tsx +++ b/nkebao/src/pages/mobile/workspace/group-push/form/components/ContentSelector.tsx @@ -1,4 +1,5 @@ -import React, { useState } from "react"; +import React, { useImperativeHandle, forwardRef } from "react"; +import { Form, Card } from "antd"; import ContentLibrarySelection from "@/components/ContentLibrarySelection"; interface ContentLibrary { @@ -19,60 +20,116 @@ interface ContentSelectorProps { loading?: boolean; } -const ContentSelector: React.FC = ({ - selectedLibraries, - onLibrariesChange, - onPrevious, - onNext, - onSave, - loading = false, -}) => { - // 将 ContentLibrary[] 转换为 string[] 用于 ContentLibrarySelection - const selectedLibraryIds = selectedLibraries.map(lib => lib.id); +export interface ContentSelectorRef { + validate: () => Promise; + getValues: () => any; +} - // 处理选择变化 - const handleLibrariesChange = (libraryIds: string[]) => { - // 这里需要根据选中的ID重新构建ContentLibrary对象 - // 由于ContentLibrarySelection只返回ID,我们需要从原始数据中获取完整信息 - // 暂时使用简化的处理方式 - const newSelectedLibraries = libraryIds.map(id => ({ - id, - name: `内容库 ${id}`, // 这里应该从API获取完整信息 - targets: [], // 这里应该从API获取完整信息 +const ContentSelector = forwardRef( + ( + { + selectedLibraries, + onLibrariesChange, + onPrevious, + onNext, + onSave, + loading = false, + }, + ref, + ) => { + const [form] = Form.useForm(); + + // 暴露方法给父组件 + useImperativeHandle(ref, () => ({ + validate: async () => { + try { + await form.validateFields(); + return true; + } catch (error) { + console.log("ContentSelector 表单验证失败:", error); + return false; + } + }, + getValues: () => { + return form.getFieldsValue(); + }, })); - onLibrariesChange(newSelectedLibraries); - }; - // 处理选择详情变化 - const handleSelectDetail = (libraries: any[]) => { - // 将API返回的数据转换为ContentLibrary格式 - const convertedLibraries = libraries.map(lib => ({ - id: lib.id, - name: lib.name, - targets: [], // 这里需要根据实际情况获取targets数据 - })); - onLibrariesChange(convertedLibraries); - }; + // 将 ContentLibrary[] 转换为 string[] 用于 ContentLibrarySelection + const selectedLibraryIds = selectedLibraries.map(lib => lib.id); - return ( -
-
-
- 选择内容库: -
- + // 处理选择变化 + const handleLibrariesChange = (libraryIds: string[]) => { + // 这里需要根据选中的ID重新构建ContentLibrary对象 + // 由于ContentLibrarySelection只返回ID,我们需要从原始数据中获取完整信息 + // 暂时使用简化的处理方式 + const newSelectedLibraries = libraryIds.map(id => ({ + id, + name: `内容库 ${id}`, // 这里应该从API获取完整信息 + targets: [], // 这里应该从API获取完整信息 + })); + onLibrariesChange(newSelectedLibraries); + form.setFieldValue("contentLibraries", libraryIds); + }; + + // 处理选择详情变化 + const handleSelectDetail = (libraries: any[]) => { + // 将API返回的数据转换为ContentLibrary格式 + const convertedLibraries = libraries.map(lib => ({ + id: lib.id, + name: lib.name, + targets: [], // 这里需要根据实际情况获取targets数据 + })); + onLibrariesChange(convertedLibraries); + form.setFieldValue( + "contentLibraries", + libraries.map(lib => lib.id), + ); + }; + + return ( +
+ +
+
+

+ 选择内容库 +

+

+ 请选择要推送的内容库 +

+
+ + + + +
+
-
- ); -}; + ); + }, +); + +ContentSelector.displayName = "ContentSelector"; export default ContentSelector; diff --git a/nkebao/src/pages/mobile/workspace/group-push/form/components/GroupSelector.tsx b/nkebao/src/pages/mobile/workspace/group-push/form/components/GroupSelector.tsx index b77cecaf..f744295e 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/form/components/GroupSelector.tsx +++ b/nkebao/src/pages/mobile/workspace/group-push/form/components/GroupSelector.tsx @@ -1,59 +1,95 @@ -import React, { useState } from "react"; +import React, { useImperativeHandle, forwardRef } from "react"; +import { Form, Card } from "antd"; import GroupSelection from "@/components/GroupSelection"; import { GroupSelectionItem } from "@/components/GroupSelection/data"; interface GroupSelectorProps { - selectedGroups: string[]; - onGroupsChange: (groups: string[]) => void; + selectedGroups: GroupSelectionItem[]; onPrevious: () => void; - onNext: () => void; - onSave: () => void; - loading?: boolean; + onNext: (data: { + wechatGroups: string[]; + wechatGroupsOptions: GroupSelectionItem[]; + }) => void; } -const GroupSelector: React.FC = ({ - selectedGroups, - onGroupsChange, - onPrevious, - onNext, - onSave, - loading = false, -}) => { - // 将string[]转换为GroupSelectionItem[] - const selectedGroupItems: GroupSelectionItem[] = selectedGroups.map(id => ({ - id, - name: `群组 ${id}`, - avatar: "", - chatroomId: id, - })); +export interface GroupSelectorRef { + validate: () => Promise; + getValues: () => any; +} - const handleGroupSelect = (groupItems: GroupSelectionItem[]) => { - // 将GroupSelectionItem[]转换回string[] - const groupIds = groupItems.map(item => item.id); - onGroupsChange(groupIds); - }; +const GroupSelector = forwardRef( + ({ selectedGroups, onNext }, ref) => { + const [form] = Form.useForm(); - return ( -
-
-

- 选择推送群组 -

-

- 请选择要推送消息的微信群组 -

-
+ // 暴露方法给父组件 + useImperativeHandle(ref, () => ({ + validate: async () => { + try { + form.setFieldsValue({ + wechatGroups: selectedGroups.map(item => item.id), + }); + await form.validateFields(); + return true; + } catch (error) { + console.log("GroupSelector 表单验证失败:", error); + return false; + } + }, + getValues: () => { + return form.getFieldsValue(); + }, + })); - -
- ); -}; + // 群组选择 + const handleGroupSelect = (wechatGroupsOptions: GroupSelectionItem[]) => { + const wechatGroups = wechatGroupsOptions.map(item => item.id); + form.setFieldValue("wechatGroups", wechatGroups); + onNext({ wechatGroups, wechatGroupsOptions }); + }; + + return ( + +
+
+

+ 选择推送群组 +

+

+ 请选择要推送消息的微信群组 +

+
+ + + + +
+
+ ); + }, +); + +GroupSelector.displayName = "GroupSelector"; export default GroupSelector; diff --git a/nkebao/src/pages/mobile/workspace/group-push/form/index.data.ts b/nkebao/src/pages/mobile/workspace/group-push/form/index.data.ts index 1604aef2..8d7e81a3 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/form/index.data.ts +++ b/nkebao/src/pages/mobile/workspace/group-push/form/index.data.ts @@ -27,6 +27,7 @@ export interface FormData { isLoopPush: boolean; isImmediatePush: boolean; isEnabled: boolean; - groups: WechatGroup[]; contentLibraries: ContentLibrary[]; + wechatGroups: string[]; + [key: string]: any; } diff --git a/nkebao/src/pages/mobile/workspace/group-push/form/index.tsx b/nkebao/src/pages/mobile/workspace/group-push/form/index.tsx index 71d3b2ae..158574b6 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/form/index.tsx +++ b/nkebao/src/pages/mobile/workspace/group-push/form/index.tsx @@ -1,14 +1,17 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { Button } from "antd"; import { createGroupPushTask } from "./index.api"; import Layout from "@/components/Layout/Layout"; import StepIndicator from "@/components/StepIndicator"; -import BasicSettings from "./components/BasicSettings"; -import GroupSelector from "./components/GroupSelector"; -import ContentSelector from "./components/ContentSelector"; +import BasicSettings, { BasicSettingsRef } from "./components/BasicSettings"; +import GroupSelector, { GroupSelectorRef } from "./components/GroupSelector"; +import ContentSelector, { + ContentSelectorRef, +} from "./components/ContentSelector"; import type { ContentLibrary, FormData } from "./index.data"; import NavCommon from "@/components/NavCommon"; +import { GroupSelectionItem } from "@/components/GroupSelection/data"; const steps = [ { id: 1, title: "步骤 1", subtitle: "基础设置" }, { id: 2, title: "步骤 2", subtitle: "选择社群" }, @@ -21,6 +24,9 @@ const NewGroupPush: React.FC = () => { const navigate = useNavigate(); const [currentStep, setCurrentStep] = useState(1); const [loading, setLoading] = useState(false); + const [wechatGroupsOptions, setWechatGroupsOptions] = useState< + GroupSelectionItem[] + >([]); const [formData, setFormData] = useState({ name: "", pushTimeStart: "06:00", @@ -30,11 +36,16 @@ const NewGroupPush: React.FC = () => { isLoopPush: false, isImmediatePush: false, isEnabled: false, - groups: [], + wechatGroups: [], contentLibraries: [], }); const [isEditMode, setIsEditMode] = useState(false); + // 创建子组件的ref + const basicSettingsRef = useRef(null); + const groupSelectorRef = useRef(null); + const contentSelectorRef = useRef(null); + useEffect(() => { if (!id) return; setIsEditMode(true); @@ -44,19 +55,16 @@ const NewGroupPush: React.FC = () => { setFormData(prev => ({ ...prev, ...values })); }; - const handleGroupsChange = (groups: string[]) => { - // 将string[]转换为WechatGroup[] - const convertedGroups = groups.map(id => ({ - id, - name: `群组 ${id}`, - avatar: "", - serviceAccount: { - id: "", - name: "", - avatar: "", - }, + //群组选择 + const handleGroupsChange = (data: { + wechatGroups: string[]; + wechatGroupsOptions: GroupSelectionItem[]; + }) => { + setFormData(prev => ({ + ...prev, + wechatGroups: data.wechatGroups, })); - setFormData(prev => ({ ...prev, groups: convertedGroups })); + setWechatGroupsOptions(data.wechatGroupsOptions); }; const handleLibrariesChange = (contentLibraries: ContentLibrary[]) => { @@ -119,64 +127,66 @@ const NewGroupPush: React.FC = () => { } }; - const handleNext = () => { + const handleNext = async () => { if (currentStep < 4) { - setCurrentStep(currentStep + 1); - } - }; + try { + let isValid = false; - const canGoNext = () => { - switch (currentStep) { - case 1: { - return formData.name.trim() !== ""; + switch (currentStep) { + case 1: + // 调用 BasicSettings 的表单校验 + isValid = (await basicSettingsRef.current?.validate()) || false; + if (isValid) { + const values = basicSettingsRef.current?.getValues(); + if (values) { + handleBasicSettingsChange(values); + } + setCurrentStep(2); + } + break; + + case 2: + // 调用 GroupSelector 的表单校验 + isValid = (await groupSelectorRef.current?.validate()) || false; + if (isValid) { + setCurrentStep(3); + } + break; + + case 3: + // 调用 ContentSelector 的表单校验 + isValid = (await contentSelectorRef.current?.validate()) || false; + if (isValid) { + setCurrentStep(4); + } + break; + + default: + setCurrentStep(currentStep + 1); + } + } catch (error) { + console.log("表单验证失败:", error); } - case 2: { - // 选择社群:检查是否选择了群组 - const groupsValid = - formData.groups.length > 0 && formData.groups.length <= 50; // 添加上限检查 - return groupsValid; - } - case 3: { - // 选择内容库:检查是否选择了内容库 - const librariesValid = - formData.contentLibraries.length > 0 && - formData.contentLibraries.length <= 20; // 添加上限检查 - return librariesValid; - } - case 4: { - // 京东联盟:可以进入下一步(保存) - // 这里可以添加京东联盟相关的验证逻辑 - return true; - } - default: - return false; } }; const renderFooter = () => { - if (currentStep === 4) { - return ( -
- - -
- ); - } - return (
{currentStep > 1 && ( - )} - + {currentStep === 4 ? ( + + ) : ( + + )}
); }; @@ -186,11 +196,14 @@ const NewGroupPush: React.FC = () => { header={} footer={renderFooter()} > -
- -
+
+
+ +
+
{currentStep === 1 && ( { )} {currentStep === 2 && ( g.id)} - onGroupsChange={handleGroupsChange} + ref={groupSelectorRef} + selectedGroups={wechatGroupsOptions} onPrevious={() => setCurrentStep(1)} - onNext={() => setCurrentStep(3)} - onSave={handleSave} - loading={loading} + onNext={handleGroupsChange} /> )} {currentStep === 3 && ( setCurrentStep(2)} From bb06d3e4f8a35379200c09a783c346b8eb35bfa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Thu, 7 Aug 2025 18:21:35 +0800 Subject: [PATCH 49/93] =?UTF-8?q?FEAT=20=3D>=20=E6=9C=AC=E6=AC=A1=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=A1=B9=E7=9B=AE=E4=B8=BA=EF=BC=9A=20=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E5=BA=93=E5=A4=84=E7=90=86=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api.ts | 0 .../src/components/ContentSelection/data.ts | 21 +++++ .../index.module.scss | 0 .../index.tsx | 79 +++++-------------- nkebao/src/pages/mobile/test/select.tsx | 20 +++-- .../form/components/ContentSelector.tsx | 55 ++++--------- .../workspace/group-push/form/index.data.ts | 3 +- .../workspace/group-push/form/index.tsx | 24 +++--- .../workspace/moments-sync/new/index.tsx | 2 +- 9 files changed, 84 insertions(+), 120 deletions(-) rename nkebao/src/components/{ContentLibrarySelection => ContentSelection}/api.ts (100%) create mode 100644 nkebao/src/components/ContentSelection/data.ts rename nkebao/src/components/{ContentLibrarySelection => ContentSelection}/index.module.scss (100%) rename nkebao/src/components/{ContentLibrarySelection => ContentSelection}/index.tsx (81%) diff --git a/nkebao/src/components/ContentLibrarySelection/api.ts b/nkebao/src/components/ContentSelection/api.ts similarity index 100% rename from nkebao/src/components/ContentLibrarySelection/api.ts rename to nkebao/src/components/ContentSelection/api.ts diff --git a/nkebao/src/components/ContentSelection/data.ts b/nkebao/src/components/ContentSelection/data.ts new file mode 100644 index 00000000..5ea44b90 --- /dev/null +++ b/nkebao/src/components/ContentSelection/data.ts @@ -0,0 +1,21 @@ +// 内容库接口类型 +export interface ContentItem { + id: number; + name: string; + [key: string]: any; +} + +// 组件属性接口 +export interface ContentSelectionProps { + selectedContent: ContentItem[]; + onSelect: (selectedItems: ContentItem[]) => void; + placeholder?: string; + className?: string; + visible?: boolean; + onVisibleChange?: (visible: boolean) => void; + selectedListMaxHeight?: number; + showInput?: boolean; + showSelectedList?: boolean; + readonly?: boolean; + onConfirm?: (selectedItems: ContentItem[]) => void; +} diff --git a/nkebao/src/components/ContentLibrarySelection/index.module.scss b/nkebao/src/components/ContentSelection/index.module.scss similarity index 100% rename from nkebao/src/components/ContentLibrarySelection/index.module.scss rename to nkebao/src/components/ContentSelection/index.module.scss diff --git a/nkebao/src/components/ContentLibrarySelection/index.tsx b/nkebao/src/components/ContentSelection/index.tsx similarity index 81% rename from nkebao/src/components/ContentLibrarySelection/index.tsx rename to nkebao/src/components/ContentSelection/index.tsx index b053f1d9..9c022e80 100644 --- a/nkebao/src/components/ContentLibrarySelection/index.tsx +++ b/nkebao/src/components/ContentSelection/index.tsx @@ -7,17 +7,7 @@ import Layout from "@/components/Layout/Layout"; import PopupHeader from "@/components/PopuLayout/header"; import PopupFooter from "@/components/PopuLayout/footer"; import { getContentLibraryList } from "./api"; - -// 内容库接口类型 -interface ContentLibraryItem { - id: string; - name: string; - description?: string; - sourceType?: number; // 1=文本 2=图片 3=视频 - creatorName?: string; - updateTime?: string; - [key: string]: any; -} +import { ContentItem, ContentSelectionProps } from "./data"; // 类型标签文本 const getTypeText = (type?: number) => { @@ -43,29 +33,9 @@ const formatDate = (dateStr?: string) => { .padStart(2, "0")}`; }; -// 组件属性接口 -interface ContentLibrarySelectionProps { - selectedLibraries: (string | number)[]; - onSelect: (libraries: string[]) => void; - onSelectDetail?: (libraries: ContentLibraryItem[]) => void; - placeholder?: string; - className?: string; - visible?: boolean; - onVisibleChange?: (visible: boolean) => void; - selectedListMaxHeight?: number; - showInput?: boolean; - showSelectedList?: boolean; - readonly?: boolean; - onConfirm?: ( - selectedIds: string[], - selectedItems: ContentLibraryItem[], - ) => void; -} - -export default function ContentLibrarySelection({ - selectedLibraries, +export default function ContentSelection({ + selectedContent, onSelect, - onSelectDetail, placeholder = "选择内容库", className = "", visible, @@ -75,24 +45,19 @@ export default function ContentLibrarySelection({ showSelectedList = true, readonly = false, onConfirm, -}: ContentLibrarySelectionProps) { +}: ContentSelectionProps) { const [popupVisible, setPopupVisible] = useState(false); - const [libraries, setLibraries] = useState([]); + const [libraries, setLibraries] = useState([]); const [searchQuery, setSearchQuery] = useState(""); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [totalLibraries, setTotalLibraries] = useState(0); const [loading, setLoading] = useState(false); - // 获取已选内容库详细信息 - const selectedLibraryObjs = libraries.filter(item => - selectedLibraries.includes(item.id), - ); - // 删除已选内容库 - const handleRemoveLibrary = (id: string) => { + const handleRemoveLibrary = (id: number) => { if (readonly) return; - onSelect(selectedLibraries.filter(g => g !== id)); + onSelect(selectedContent.filter(c => c.id !== id)); }; // 受控弹窗逻辑 @@ -153,30 +118,24 @@ export default function ContentLibrarySelection({ }; // 处理内容库选择 - const handleLibraryToggle = (libraryId: string) => { + const handleLibraryToggle = (library: ContentItem) => { if (readonly) return; - const newSelected = selectedLibraries.includes(libraryId) - ? selectedLibraries.filter(id => id !== libraryId) - : [...selectedLibraries, libraryId]; + const newSelected = selectedContent.some(c => c.id === library.id) + ? selectedContent.filter(c => c.id !== library.id) + : [...selectedContent, library]; onSelect(newSelected); - if (onSelectDetail) { - const selectedObjs = libraries.filter(item => - newSelected.includes(item.id), - ); - onSelectDetail(selectedObjs); - } }; // 获取显示文本 const getDisplayText = () => { - if (selectedLibraries.length === 0) return ""; - return `已选择 ${selectedLibraries.length} 个内容库`; + if (selectedContent.length === 0) return ""; + return `已选择 ${selectedContent.length} 个内容库`; }; // 确认选择 const handleConfirm = () => { if (onConfirm) { - onConfirm(selectedLibraries, selectedLibraryObjs); + onConfirm(selectedContent); } setRealVisible(false); }; @@ -202,7 +161,7 @@ export default function ContentLibrarySelection({
)} {/* 已选内容库列表窗口 */} - {showSelectedList && selectedLibraryObjs.length > 0 && ( + {showSelectedList && selectedContent.length > 0 && (
- {selectedLibraryObjs.map(item => ( + {selectedContent.map(item => (
setRealVisible(false)} onConfirm={handleConfirm} @@ -301,8 +260,8 @@ export default function ContentLibrarySelection({ {libraries.map(item => (
diff --git a/nkebao/src/pages/mobile/workspace/group-push/form/components/ContentSelector.tsx b/nkebao/src/pages/mobile/workspace/group-push/form/components/ContentSelector.tsx index e96c6e14..1e3adff0 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/form/components/ContentSelector.tsx +++ b/nkebao/src/pages/mobile/workspace/group-push/form/components/ContentSelector.tsx @@ -1,23 +1,15 @@ import React, { useImperativeHandle, forwardRef } from "react"; import { Form, Card } from "antd"; -import ContentLibrarySelection from "@/components/ContentLibrarySelection"; - -interface ContentLibrary { - id: string; - name: string; - targets: Array<{ - id: string; - avatar: string; - }>; -} +import ContentSelection from "@/components/ContentSelection"; +import { ContentItem } from "@/components/ContentSelection/data"; interface ContentSelectorProps { - selectedLibraries: ContentLibrary[]; - onLibrariesChange: (libraries: ContentLibrary[]) => void; + selectedContent: ContentItem[]; onPrevious: () => void; - onNext: () => void; - onSave: () => void; - loading?: boolean; + onNext: (data: { + contentGroups: string[]; + contentGroupsOptions: ContentItem[]; + }) => void; } export interface ContentSelectorRef { @@ -26,17 +18,7 @@ export interface ContentSelectorRef { } const ContentSelector = forwardRef( - ( - { - selectedLibraries, - onLibrariesChange, - onPrevious, - onNext, - onSave, - loading = false, - }, - ref, - ) => { + ({ selectedContent, onNext }, ref) => { const [form] = Form.useForm(); // 暴露方法给父组件 @@ -55,20 +37,17 @@ const ContentSelector = forwardRef( }, })); - // 将 ContentLibrary[] 转换为 string[] 用于 ContentLibrarySelection - const selectedLibraryIds = selectedLibraries.map(lib => lib.id); - // 处理选择变化 - const handleLibrariesChange = (libraryIds: string[]) => { - // 这里需要根据选中的ID重新构建ContentLibrary对象 - // 由于ContentLibrarySelection只返回ID,我们需要从原始数据中获取完整信息 - // 暂时使用简化的处理方式 - const newSelectedLibraries = libraryIds.map(id => ({ + const handleLibrariesChange = (contentGroups: string[]) => { + const newSelectedLibraries = contentGroups.map(id => ({ id, name: `内容库 ${id}`, // 这里应该从API获取完整信息 targets: [], // 这里应该从API获取完整信息 })); - onLibrariesChange(newSelectedLibraries); + onNext({ + contentGroups: libraryIds, + contentGroupsOptions: newSelectedLibraries, + }); form.setFieldValue("contentLibraries", libraryIds); }; @@ -80,7 +59,7 @@ const ContentSelector = forwardRef( name: lib.name, targets: [], // 这里需要根据实际情况获取targets数据 })); - onLibrariesChange(convertedLibraries); + onNext(convertedLibraries); form.setFieldValue( "contentLibraries", libraries.map(lib => lib.id), @@ -112,8 +91,8 @@ const ContentSelector = forwardRef( { type: "array", max: 20, message: "最多只能选择20个内容库" }, ]} > - { const [wechatGroupsOptions, setWechatGroupsOptions] = useState< GroupSelectionItem[] >([]); + const [contentGroupsOptions, setContentGroupsOptions] = useState< + ContentItem[] + >([]); + const [formData, setFormData] = useState({ name: "", pushTimeStart: "06:00", @@ -37,7 +42,7 @@ const NewGroupPush: React.FC = () => { isImmediatePush: false, isEnabled: false, wechatGroups: [], - contentLibraries: [], + contentGroups: [], }); const [isEditMode, setIsEditMode] = useState(false); @@ -66,9 +71,13 @@ const NewGroupPush: React.FC = () => { })); setWechatGroupsOptions(data.wechatGroupsOptions); }; - - const handleLibrariesChange = (contentLibraries: ContentLibrary[]) => { - setFormData(prev => ({ ...prev, contentLibraries })); + //内容库选择 + const handleLibrariesChange = (data: { + contentGroups: string[]; + contentGroupsOptions: ContentItem[]; + }) => { + setFormData(prev => ({ ...prev, contentGroups: data.contentGroups })); + setContentGroupsOptions(data.contentGroupsOptions); }; const handleSave = async () => { @@ -230,12 +239,9 @@ const NewGroupPush: React.FC = () => { {currentStep === 3 && ( setCurrentStep(2)} - onNext={() => setCurrentStep(4)} - onSave={handleSave} - loading={loading} + onNext={handleLibrariesChange} /> )} {currentStep === 4 && ( diff --git a/nkebao/src/pages/mobile/workspace/moments-sync/new/index.tsx b/nkebao/src/pages/mobile/workspace/moments-sync/new/index.tsx index 8c6dea8e..32ac8209 100644 --- a/nkebao/src/pages/mobile/workspace/moments-sync/new/index.tsx +++ b/nkebao/src/pages/mobile/workspace/moments-sync/new/index.tsx @@ -13,7 +13,7 @@ import { getMomentsSyncDetail, } from "./api"; import DeviceSelection from "@/components/DeviceSelection"; -import ContentLibrarySelection from "@/components/ContentLibrarySelection"; +import ContentLibrarySelection from "@/components/ContentSelection"; import NavCommon from "@/components/NavCommon"; const steps = [ From b06a0e16b030f9dc9a63c3ea6f4aa6adaec5ec7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Thu, 7 Aug 2025 18:27:37 +0800 Subject: [PATCH 50/93] =?UTF-8?q?FEAT=20=3D>=20=E6=9C=AC=E6=AC=A1=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=A1=B9=E7=9B=AE=E4=B8=BA=EF=BC=9A=20=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E9=80=89=E6=8B=A9=E6=9E=84=E5=BB=BA=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../form/components/ContentSelector.tsx | 38 +++++-------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/nkebao/src/pages/mobile/workspace/group-push/form/components/ContentSelector.tsx b/nkebao/src/pages/mobile/workspace/group-push/form/components/ContentSelector.tsx index 1e3adff0..7b1e12fd 100644 --- a/nkebao/src/pages/mobile/workspace/group-push/form/components/ContentSelector.tsx +++ b/nkebao/src/pages/mobile/workspace/group-push/form/components/ContentSelector.tsx @@ -38,32 +38,13 @@ const ContentSelector = forwardRef( })); // 处理选择变化 - const handleLibrariesChange = (contentGroups: string[]) => { - const newSelectedLibraries = contentGroups.map(id => ({ - id, - name: `内容库 ${id}`, // 这里应该从API获取完整信息 - targets: [], // 这里应该从API获取完整信息 - })); + const handleLibrariesChange = (contentGroupsOptions: ContentItem[]) => { + const contentGroups = contentGroupsOptions.map(c => c.id.toString()); onNext({ - contentGroups: libraryIds, - contentGroupsOptions: newSelectedLibraries, + contentGroups, + contentGroupsOptions, }); - form.setFieldValue("contentLibraries", libraryIds); - }; - - // 处理选择详情变化 - const handleSelectDetail = (libraries: any[]) => { - // 将API返回的数据转换为ContentLibrary格式 - const convertedLibraries = libraries.map(lib => ({ - id: lib.id, - name: lib.name, - targets: [], // 这里需要根据实际情况获取targets数据 - })); - onNext(convertedLibraries); - form.setFieldValue( - "contentLibraries", - libraries.map(lib => lib.id), - ); + form.setFieldValue("contentGroups", contentGroups); }; return ( @@ -72,7 +53,7 @@ const ContentSelector = forwardRef(
c.id) }} >

@@ -84,7 +65,7 @@ const ContentSelector = forwardRef(

( ]} > From a339dd898b454c8f5059252e46f3781894e1e752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Fri, 8 Aug 2025 10:10:28 +0800 Subject: [PATCH 51/93] =?UTF-8?q?FEAT=20=3D>=20=E6=9C=AC=E6=AC=A1=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=A1=B9=E7=9B=AE=E4=B8=BA=EF=BC=9A=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E9=81=B8=E6=93=87=E6=B8=AC=E8=A9=A6=E7=B5=84=E4=BB=B6=EF=BC=8C?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=92=B0=E5=A2=83=E8=AE=8A=E6=95=B8=EF=BC=8C?= =?UTF-8?q?=E5=84=AA=E5=8C=96=E5=85=A7=E5=AE=B9=E8=A1=A8=E5=96=AE=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E5=A5=BD=E5=8F=8B=E5=92=8C=E7=BE=A4=E7=B5=84=E9=81=B8?= =?UTF-8?q?=E6=93=87=E9=82=8F=E8=BC=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/.env.development | 4 +- nkebao/src/components/FriendSelection/data.ts | 7 ++ nkebao/src/components/SelectionTest.tsx | 67 ------------------- .../src/pages/mobile/content/form/index.tsx | 36 ++++++++-- nkebao/src/pages/mobile/test/select.tsx | 42 +++++------- nkebao/src/router/module/other.tsx | 11 --- 6 files changed, 54 insertions(+), 113 deletions(-) create mode 100644 nkebao/src/components/FriendSelection/data.ts delete mode 100644 nkebao/src/components/SelectionTest.tsx delete mode 100644 nkebao/src/router/module/other.tsx diff --git a/nkebao/.env.development b/nkebao/.env.development index 7afdd84c..c008d630 100644 --- a/nkebao/.env.development +++ b/nkebao/.env.development @@ -1,4 +1,4 @@ # 基础环境变量示例 -# VITE_API_BASE_URL=http://www.yishi.com -VITE_API_BASE_URL=https://ckbapi.quwanzhi.com +VITE_API_BASE_URL=http://www.yishi.com +# VITE_API_BASE_URL=https://ckbapi.quwanzhi.com VITE_APP_TITLE=存客宝 diff --git a/nkebao/src/components/FriendSelection/data.ts b/nkebao/src/components/FriendSelection/data.ts new file mode 100644 index 00000000..5a7e2cbd --- /dev/null +++ b/nkebao/src/components/FriendSelection/data.ts @@ -0,0 +1,7 @@ +export interface FriendSelectionItem { + id: number; + wechatId: string; + nickname: string; + avatar: string; + [key: string]: any; +} diff --git a/nkebao/src/components/SelectionTest.tsx b/nkebao/src/components/SelectionTest.tsx deleted file mode 100644 index 08bc8be2..00000000 --- a/nkebao/src/components/SelectionTest.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React, { useState } from "react"; -import DeviceSelection from "./DeviceSelection"; -import FriendSelection from "./FriendSelection"; -import GroupSelection from "./GroupSelection"; -import { Button, Space } from "antd-mobile"; - -export default function SelectionTest() { - // 设备选择 - const [selectedDevices, setSelectedDevices] = useState([]); - const [deviceDialogOpen, setDeviceDialogOpen] = useState(false); - - // 好友选择 - const [selectedFriends, setSelectedFriends] = useState([]); - const [friendDialogOpen, setFriendDialogOpen] = useState(false); - - // 群组选择 - const [selectedGroups, setSelectedGroups] = useState([]); - const [groupDialogOpen, setGroupDialogOpen] = useState(false); - - return ( -
-

选择弹窗测试

- -
- DeviceSelection(内嵌输入框+弹窗) - -
-
- FriendSelection - - -
-
- GroupSelection - - -
-
-
-
已选设备ID: {selectedDevices.join(", ")}
-
已选好友ID: {selectedFriends.join(", ")}
-
已选群组ID: {selectedGroups.join(", ")}
-
-
- ); -} diff --git a/nkebao/src/pages/mobile/content/form/index.tsx b/nkebao/src/pages/mobile/content/form/index.tsx index 7df91566..1eb91a06 100644 --- a/nkebao/src/pages/mobile/content/form/index.tsx +++ b/nkebao/src/pages/mobile/content/form/index.tsx @@ -9,6 +9,8 @@ import Layout from "@/components/Layout/Layout"; import style from "./index.module.scss"; import request from "@/api/request"; import { getContentLibraryDetail, updateContentLibrary } from "./api"; +import { GroupSelectionItem } from "@/components/GroupSelection/data"; +import { FriendSelectionItem } from "@/components/FriendSelection/data"; const { TextArea } = AntdInput; @@ -27,8 +29,14 @@ export default function ContentForm() { const isEdit = !!id; const [sourceType, setSourceType] = useState<"friends" | "groups">("friends"); const [name, setName] = useState(""); - const [selectedFriends, setSelectedFriends] = useState([]); + const [friendsGroups, setSelectedFriends] = useState([]); + const [friendsGroupsOptions, setSelectedFriendsOptions] = useState< + FriendSelectionItem[] + >([]); const [selectedGroups, setSelectedGroups] = useState([]); + const [selectedGroupsOptions, setSelectedGroupsOptions] = useState< + GroupSelectionItem[] + >([]); const [useAI, setUseAI] = useState(false); const [aiPrompt, setAIPrompt] = useState(""); const [enabled, setEnabled] = useState(true); @@ -52,7 +60,11 @@ export default function ContentForm() { setName(data.name || ""); setSourceType(data.sourceType === 1 ? "friends" : "groups"); setSelectedFriends(data.sourceFriends || []); - setSelectedGroups(data.sourceGroups || []); + setSelectedGroups(data.selectedGroups || []); + setSelectedGroupsOptions(data.selectedGroupsOptions || []); + + setSelectedFriendsOptions(data.sourceFriendsOptions || []); + setKeywordsInclude((data.keywordInclude || []).join(",")); setKeywordsExclude((data.keywordExclude || []).join(",")); setAIPrompt(data.aiPrompt || ""); @@ -87,7 +99,7 @@ export default function ContentForm() { const payload = { name, sourceType: sourceType === "friends" ? 1 : 2, - friends: selectedFriends, + friends: friendsGroups, groups: selectedGroups, groupMembers: {}, keywordInclude: keywordsInclude @@ -122,6 +134,16 @@ export default function ContentForm() { } }; + const handleGroupsChange = (groups: GroupSelectionItem[]) => { + setSelectedGroups(groups.map(g => g.id.toString())); + setSelectedGroupsOptions(groups); + }; + + const handleFriendsChange = (friends: FriendSelectionItem[]) => { + setSelectedFriends(friends.map(f => f.id.toString())); + setSelectedFriendsOptions(friends); + }; + return ( } @@ -173,15 +195,15 @@ export default function ContentForm() { > diff --git a/nkebao/src/pages/mobile/test/select.tsx b/nkebao/src/pages/mobile/test/select.tsx index 1b6ae862..3665a917 100644 --- a/nkebao/src/pages/mobile/test/select.tsx +++ b/nkebao/src/pages/mobile/test/select.tsx @@ -10,6 +10,7 @@ import AccountSelection from "@/components/AccountSelection"; import { isDevelopment } from "@/utils/env"; import { GroupSelectionItem } from "@/components/GroupSelection/data"; import { ContentItem } from "@/components/ContentSelection/data"; +import { FriendSelectionItem } from "@/components/FriendSelection/data"; const ComponentTest: React.FC = () => { const [activeTab, setActiveTab] = useState("libraries"); @@ -28,7 +29,9 @@ const ComponentTest: React.FC = () => { const [selectedContent, setSelectedContent] = useState([]); const [selectedAccounts, setSelectedAccounts] = useState([]); - + const [selectedFriendsOptions, setSelectedFriendsOptions] = useState< + FriendSelectionItem[] + >([]); return ( }>
@@ -41,6 +44,18 @@ const ComponentTest: React.FC = () => { )} + +
+

FriendSelection 组件测试

+ +
+

ContentSelection 组件测试

@@ -118,31 +133,6 @@ const ComponentTest: React.FC = () => {
- -
-

FriendSelection 组件测试

- -
- 已选好友: {selectedFriends.length} 个 -
- 好友ID: {selectedFriends.join(", ") || "无"} -
-
-
-

AccountSelection 组件测试

diff --git a/nkebao/src/router/module/other.tsx b/nkebao/src/router/module/other.tsx deleted file mode 100644 index faedbee3..00000000 --- a/nkebao/src/router/module/other.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import SelectionTest from "@/components/SelectionTest"; - -const otherRoutes = [ - { - path: "/selection-test", - element: , - auth: false, - }, -]; - -export default otherRoutes; From 08d851ec811846b6188a148983045592c16633c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Fri, 8 Aug 2025 10:37:43 +0800 Subject: [PATCH 52/93] =?UTF-8?q?FEAT=20=3D>=20=E6=9C=AC=E6=AC=A1=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=A1=B9=E7=9B=AE=E4=B8=BA=EF=BC=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nkebao/src/components/DeviceSelection/data.ts | 2 +- nkebao/src/components/DeviceSelection/index.tsx | 14 +++++++------- .../components/DeviceSelection/selectionPopup.tsx | 14 +++++++------- .../plan/new/steps/FriendRequestSettings.tsx | 2 +- nkebao/src/pages/mobile/test/select.tsx | 2 +- .../mobile/workspace/auto-like/new/NewAutoLike.tsx | 2 +- .../pages/mobile/workspace/auto-like/new/index.tsx | 2 +- .../mobile/workspace/moments-sync/new/index.tsx | 8 ++++---- 8 files changed, 23 insertions(+), 23 deletions(-) diff --git a/nkebao/src/components/DeviceSelection/data.ts b/nkebao/src/components/DeviceSelection/data.ts index 45b8a088..e8a09b5f 100644 --- a/nkebao/src/components/DeviceSelection/data.ts +++ b/nkebao/src/components/DeviceSelection/data.ts @@ -12,7 +12,7 @@ export interface DeviceSelectionItem { // 组件属性接口 export interface DeviceSelectionProps { - selectedDevices: string[]; + selectedOptions: string[]; onSelect: (devices: string[]) => void; placeholder?: string; className?: string; diff --git a/nkebao/src/components/DeviceSelection/index.tsx b/nkebao/src/components/DeviceSelection/index.tsx index 6612e9dc..c358e0ff 100644 --- a/nkebao/src/components/DeviceSelection/index.tsx +++ b/nkebao/src/components/DeviceSelection/index.tsx @@ -7,7 +7,7 @@ import SelectionPopup from "./selectionPopup"; import style from "./index.module.scss"; const DeviceSelection: React.FC = ({ - selectedDevices, + selectedOptions, onSelect, placeholder = "选择设备", className = "", @@ -36,14 +36,14 @@ const DeviceSelection: React.FC = ({ // 获取显示文本 const getDisplayText = () => { - if (selectedDevices.length === 0) return ""; - return `已选择 ${selectedDevices.length} 个设备`; + if (selectedOptions.length === 0) return ""; + return `已选择 ${selectedOptions.length} 个设备`; }; // 删除已选设备 const handleRemoveDevice = (id: string) => { if (readonly) return; - onSelect(selectedDevices.filter(d => d !== id)); + onSelect(selectedOptions.filter(d => d !== id)); }; return ( @@ -67,7 +67,7 @@ const DeviceSelection: React.FC = ({
)} {/* 已选设备列表窗口 */} - {mode === "input" && showSelectedList && selectedDevices.length > 0 && ( + {mode === "input" && showSelectedList && selectedOptions.length > 0 && (
= ({ background: "#fff", }} > - {selectedDevices.map(deviceId => ( + {selectedOptions.map(deviceId => (
= ({ setRealVisible(false)} - selectedDevices={selectedDevices} + selectedOptions={selectedOptions} onSelect={onSelect} /> diff --git a/nkebao/src/components/DeviceSelection/selectionPopup.tsx b/nkebao/src/components/DeviceSelection/selectionPopup.tsx index fc3ea3c2..49a933e1 100644 --- a/nkebao/src/components/DeviceSelection/selectionPopup.tsx +++ b/nkebao/src/components/DeviceSelection/selectionPopup.tsx @@ -20,7 +20,7 @@ interface DeviceSelectionItem { interface SelectionPopupProps { visible: boolean; onClose: () => void; - selectedDevices: string[]; + selectedOptions: string[]; onSelect: (devices: string[]) => void; } @@ -29,7 +29,7 @@ const PAGE_SIZE = 20; const SelectionPopup: React.FC = ({ visible, onClose, - selectedDevices, + selectedOptions, onSelect, }) => { // 设备数据 @@ -113,10 +113,10 @@ const SelectionPopup: React.FC = ({ // 处理设备选择 const handleDeviceToggle = (deviceId: string) => { - if (selectedDevices.includes(deviceId)) { - onSelect(selectedDevices.filter(id => id !== deviceId)); + if (selectedOptions.includes(deviceId)) { + onSelect(selectedOptions.filter(id => id !== deviceId)); } else { - onSelect([...selectedDevices, deviceId]); + onSelect([...selectedOptions, deviceId]); } }; @@ -155,7 +155,7 @@ const SelectionPopup: React.FC = ({ currentPage={currentPage} totalPages={totalPages} loading={loading} - selectedCount={selectedDevices.length} + selectedCount={selectedOptions.length} onPageChange={setCurrentPage} onCancel={onClose} onConfirm={onClose} @@ -172,7 +172,7 @@ const SelectionPopup: React.FC = ({ {filteredDevices.map(device => (