diff --git a/Cunkebao/app/layout.tsx b/Cunkebao/app/layout.tsx index b678c2de..1a8a4b89 100644 --- a/Cunkebao/app/layout.tsx +++ b/Cunkebao/app/layout.tsx @@ -21,7 +21,7 @@ export default function RootLayout({ }) { return ( - + diff --git a/Cunkebao/app/wechat-accounts/[id]/page.tsx b/Cunkebao/app/wechat-accounts/[id]/page.tsx index d29cb16a..7126c424 100644 --- a/Cunkebao/app/wechat-accounts/[id]/page.tsx +++ b/Cunkebao/app/wechat-accounts/[id]/page.tsx @@ -527,31 +527,33 @@ export default function WechatAccountDetailPage() { // 获取账号概览数据 const fetchSummaryData = useCallback(async () => { + if (!account?.wechatId || isLoading) return; + try { setIsLoading(true); - const response = await fetchWechatAccountSummary(account?.wechatId || ''); + const response = await fetchWechatAccountSummary(account.wechatId); if (response.code === 200) { setAccountSummary(response.data); - } else { - toast({ + } else { + toast({ title: "获取账号概览失败", description: response.msg || "请稍后再试", - variant: "destructive" - }); - } - } catch (error) { - console.error("获取账号概览失败:", error); - toast({ - title: "获取账号概览失败", - description: "请检查网络连接或稍后再试", variant: "destructive" + }); + } + } catch (error) { + console.error("获取账号概览失败:", error); + toast({ + title: "获取账号概览失败", + description: "请检查网络连接或稍后再试", + variant: "destructive" }); - } finally { + } finally { setIsLoading(false); } - }, [account]); + }, [account?.wechatId]); - // 在页面加载时获取账号概览数据 + // 统一在账号数据加载完成后获取概览数据 useEffect(() => { if (account?.wechatId) { fetchSummaryData(); @@ -724,7 +726,7 @@ export default function WechatAccountDetailPage() { 账号概览 - 好友列表{activeTab === "friends" && friendsTotal > 0 ? ` (${friendsTotal})` : ''} + 好友列表{activeTab === "friends" && friendsTotal > 0 ? ` (${friendsTotal.toLocaleString()})` : ''} @@ -849,7 +851,7 @@ export default function WechatAccountDetailPage() {
根据当前账号权重({accountSummary.accountWeight.scope}分),每日最多可添加{" "} - {accountSummary.statistics.addLimit}{" "} + {accountSummary.statistics.addLimit.toLocaleString()}{" "} 个好友
@@ -886,110 +888,80 @@ export default function WechatAccountDetailPage() { - - -
- {/* 搜索栏 */} -
-
- - setSearchQuery(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleSearch()} - className="pl-9" - /> -
- + +
+ {/* 搜索栏 */} +
+
+ + setSearchQuery(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} + className="pl-9 bg-white border-gray-200 focus:border-blue-500" + />
+ +
- {/* 好友列表 */} -
- {isFetchingFriends && friends.length === 0 ? ( -
- -
- ) : friends.length === 0 && hasFriendLoadError ? ( -
-

加载好友失败,请重试

- -
- ) : friends.length === 0 ? ( -
未找到匹配的好友
- ) : ( - <> - {friends.map((friend) => ( + {/* 好友列表 */} +
+ {isFetchingFriends && friends.length === 0 ? ( +
+ +
+ ) : friends.length === 0 ? ( +
未找到匹配的好友
+ ) : ( + <> + {friends.map((friend) => (
handleFriendClick(friend)} > - {friend.nickname?.[0] || 'U'} + {friend.nickname?.[0] || 'U'}
-
+
{friend.nickname} - {friend.remark && ({friend.remark})} + {friend.remark && ({friend.remark})}
{friend.wechatId}
- {friend.tags.slice(0, 3).map((tag: FriendTag) => ( - - {tag.name} + {friend.tags?.map((tag, index) => ( + + {typeof tag === 'string' ? tag : tag.name} ))} - {friend.tags.length > 3 && ( - - +{friend.tags.length - 3} - - )}
- ))} - - {/* 懒加载指示器 */} - {hasMoreFriends && ( -
- {isFetchingFriends && } -
- )} - - )} -
- - {/* 显示加载状态和总数 */} -
- {friendsTotal > 0 ? ( - - 已加载 {Math.min(friends.length, friendsTotal)} / {friendsTotal} 条记录 - - ) : !isFetchingFriends && !hasFriendLoadError && account ? ( - - 共 {account.friendCount} 条记录 - - ) : null} -
+ ))} + {hasMoreFriends && ( +
+ +
+ )} + + )}
- +
diff --git a/Cunkebao/components/AuthCheck.tsx b/Cunkebao/components/AuthCheck.tsx new file mode 100644 index 00000000..61bc91e4 --- /dev/null +++ b/Cunkebao/components/AuthCheck.tsx @@ -0,0 +1,43 @@ +"use client" + +import { useEffect } from 'react' +import { useRouter, usePathname } from 'next/navigation' +import { useAuth } from '@/hooks/useAuth' + +// 不需要登录的公共页面路径 +const PUBLIC_PATHS = [ + '/login', + '/register', + '/forgot-password', + '/reset-password', + '/404', + '/500' +] + +export function AuthCheck({ children }: { children: React.ReactNode }) { + const router = useRouter() + const pathname = usePathname() + const { isAuthenticated, isLoading } = useAuth() + + useEffect(() => { + if (!isLoading && !isAuthenticated && !PUBLIC_PATHS.includes(pathname)) { + // 保存当前URL,登录后可以重定向回来 + const returnUrl = encodeURIComponent(window.location.href) + router.push(`/login?returnUrl=${returnUrl}`) + } + }, [isAuthenticated, isLoading, pathname, router]) + + if (isLoading) { + return ( +
+
+
+ ) + } + + if (!isAuthenticated && !PUBLIC_PATHS.includes(pathname)) { + return null + } + + return <>{children} +} \ No newline at end of file diff --git a/Cunkebao/hooks/useAuth.ts b/Cunkebao/hooks/useAuth.ts new file mode 100644 index 00000000..33994766 --- /dev/null +++ b/Cunkebao/hooks/useAuth.ts @@ -0,0 +1,46 @@ +import { useState, useEffect } from 'react' + +export interface AuthState { + isAuthenticated: boolean + isLoading: boolean + user: any | null +} + +export function useAuth(): AuthState { + const [state, setState] = useState({ + isAuthenticated: false, + isLoading: true, + user: null + }) + + useEffect(() => { + const checkAuth = async () => { + try { + // 检查本地存储的token + const token = localStorage.getItem('token') + if (!token) { + setState({ isAuthenticated: false, isLoading: false, user: null }) + return + } + + // TODO: 这里可以添加token验证的API调用 + // const response = await validateToken(token) + // if (response.valid) { + // setState({ isAuthenticated: true, isLoading: false, user: response.user }) + // } else { + // setState({ isAuthenticated: false, isLoading: false, user: null }) + // } + + // 临时:仅检查token存在性 + setState({ isAuthenticated: true, isLoading: false, user: { token } }) + } catch (error) { + console.error('Auth check failed:', error) + setState({ isAuthenticated: false, isLoading: false, user: null }) + } + } + + checkAuth() + }, []) + + return state +} \ No newline at end of file diff --git a/Cunkebao/next.config.mjs b/Cunkebao/next.config.mjs index a4c88a09..7d39de61 100644 --- a/Cunkebao/next.config.mjs +++ b/Cunkebao/next.config.mjs @@ -22,6 +22,9 @@ const nextConfig = { parallelServerCompiles: true, }, reactStrictMode: false, + compiler: { + styledComponents: true, + }, } mergeConfig(nextConfig, userConfig) diff --git a/Server/index.html b/Server/index.html deleted file mode 100644 index 86aeca22..00000000 --- a/Server/index.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - - 恭喜,站点创建成功! - - - -
-

恭喜, 站点创建成功!

-

这是默认index.html,本页面由系统自动生成

-
    -
  • 本页面在FTP根目录下的index.html
  • -
  • 您可以修改、删除或覆盖本页面
  • -
  • FTP相关信息,请到“面板系统后台 > FTP” 查看
  • -
-
- - \ No newline at end of file diff --git a/Server/scripts/README.md b/Server/scripts/README.md deleted file mode 100644 index 2077eeae..00000000 --- a/Server/scripts/README.md +++ /dev/null @@ -1,180 +0,0 @@ -# 定时任务和队列使用说明 - -## 一、环境准备 - -### 1. 安装Redis -确保服务器已安装Redis,并正常运行。 - -```bash -# Ubuntu/Debian系统 -sudo apt-get update -sudo apt-get install redis-server - -# CentOS系统 -sudo yum install redis -sudo systemctl start redis -``` - -### 2. 安装PHP的Redis扩展 - -```bash -sudo pecl install redis -``` - -### 3. 修改队列配置 - -编辑 `config/queue.php` 文件,根据实际环境修改Redis连接信息。 - -## 二、系统组件说明 - -### 1. 命令行任务 -已实现的命令行任务: -- `device:list`: 获取设备列表命令 - -### 2. 队列任务 -已实现的队列任务: -- `DeviceListJob`: 处理设备列表获取并自动翻页的任务 - -## 三、配置步骤 - -### 1. 确保API连接信息正确 -在 `.env` 文件中确保以下配置正确: -- `api.wechat_url`: API基础URL地址 -- `api.username`: API登录用户名 -- `api.password`: API登录密码 - -## 四、系统架构说明 - -### 1. 授权服务 -系统使用了公共授权服务 `app\common\service\AuthService`,用于管理API授权信息: -- 提供静态方法 `getSystemAuthorization()`,可被所有定时任务和控制器复用 -- 从环境变量(.env文件)中读取API连接信息 -- 自动缓存授权Token,有效期为10分钟,避免频繁请求API -- 缓存失效后自动重新获取授权信息 - -### 2. 队列任务 -队列任务统一放置在 `application/job` 目录中: -- 目前已实现 `DeviceListJob` 用于获取设备列表 -- 每个任务类需实现 `fire` 方法来处理队列任务 -- 任务可以通过 `think queue:work` 命令处理 -- 失败任务会自动重试,最多3次 - -### 3. 定时命令 -定时命令统一放置在 `application/common/command` 目录中: -- 继承自 `BaseCommand` 基类 -- 需要在 `application/command.php` 中注册命令 -- 可通过 `php think` 命令调用 - -## 五、配置定时任务 - -### 1. 编辑crontab配置 - -```bash -crontab -e -``` - -### 2. 直接配置PHP命令执行定时任务 - -``` -# 每5分钟执行一次设备列表获取任务 -*/5 * * * * cd /www/wwwroot/yi.54iis.com && php think device:list >> /www/wwwroot/yi.54iis.com/logs/device_list.log 2>&1 -``` - -说明: -- `cd /www/wwwroot/yi.54iis.com`: 切换到项目目录 -- `php think device:list`: 执行设备列表命令 -- `>> /www/wwwroot/yi.54iis.com/logs/device_list.log 2>&1`: 将输出和错误信息追加到日志文件 - -### 3. 创建日志目录 - -```bash -# 确保日志目录存在 -mkdir -p /www/wwwroot/yi.54iis.com/logs -chmod 755 /www/wwwroot/yi.54iis.com/logs -``` - -## 六、配置队列处理进程 - -### 1. 使用crontab监控队列进程 - -``` -# 每分钟检查队列进程,如果不存在则启动 -* * * * * ps aux | grep "php think queue:work" | grep -v grep > /dev/null || (cd /www/wwwroot/yi.54iis.com && nohup php think queue:work --queue device_list --tries 3 --sleep 3 >> /www/wwwroot/yi.54iis.com/logs/queue_worker.log 2>&1 &) -``` - -说明: -- `ps aux | grep "php think queue:work" | grep -v grep > /dev/null`: 检查队列进程是否存在 -- `||`: 如果前面的命令失败(进程不存在),则执行后面的命令 -- `(cd /www/wwwroot/yi.54iis.com && nohup...)`: 进入项目目录并启动队列处理进程 - -### 2. 或者使用supervisor管理队列进程(推荐) - -如果服务器上安装了supervisor,可以创建配置文件 `/etc/supervisor/conf.d/device_queue.conf`: - -```ini -[program:device_queue] -process_name=%(program_name)s_%(process_num)02d -command=php /www/wwwroot/yi.54iis.com/think queue:work --queue device_list --tries 3 --sleep 3 -autostart=true -autorestart=true -user=www -numprocs=1 -redirect_stderr=true -stdout_logfile=/www/wwwroot/yi.54iis.com/logs/queue_worker.log -``` - -然后重新加载supervisor配置: - -```bash -sudo supervisorctl reread -sudo supervisorctl update -sudo supervisorctl start device_queue:* -``` - -## 七、测试 - -### 1. 手动执行命令 - -```bash -# 进入项目目录 -cd /www/wwwroot/yi.54iis.com - -# 执行设备列表获取命令 -php think device:list -``` - -### 2. 查看日志 - -```bash -# 查看定时任务日志 -cat /www/wwwroot/yi.54iis.com/logs/device_list.log - -# 查看队列处理日志 -cat /www/wwwroot/yi.54iis.com/logs/queue_worker.log -``` - -## 八、新增定时任务流程 - -如需添加新的定时任务,请按照以下步骤操作: - -1. 在 `application/common/command` 目录下创建新的命令类 -2. 在 `application/job` 目录下创建对应的队列任务处理类 -3. 在 `application/command.php` 文件中注册新命令 -4. 更新crontab配置,添加新的命令执行计划 - -示例:添加一个每天凌晨2点执行的数据备份任务 - -``` -0 2 * * * cd /www/wwwroot/yi.54iis.com && php think backup:data >> /www/wwwroot/yi.54iis.com/logs/backup_data.log 2>&1 -``` - -新增的定时任务可直接使用 `AuthService::getSystemAuthorization()` 获取授权信息,无需重复实现授权逻辑。 - -## 九、注意事项 - -1. 确保PHP命令可以正常执行(如果默认PHP版本不匹配,可能需要使用完整路径,例如 `/www/server/php/74/bin/php`) -2. 确保Redis服务正常运行 -3. 确保API连接信息配置正确 -4. 确保日志目录存在且有写入权限 -5. 定时任务执行用户需要有项目目录的读写权限 -6. 如果使用宝塔面板,可以在【计划任务】中配置上述crontab任务 \ No newline at end of file diff --git a/Server/scripts/init-menu.php b/Server/scripts/init-menu.php deleted file mode 100644 index d1ed8ad5..00000000 --- a/Server/scripts/init-menu.php +++ /dev/null @@ -1,173 +0,0 @@ - Env::get('database.type', 'mysql'), - 'hostname' => Env::get('database.hostname', '127.0.0.1'), - 'database' => Env::get('database.database', 'database'), - 'username' => Env::get('database.username', 'root'), - 'password' => Env::get('database.password', 'root'), - 'hostport' => Env::get('database.hostport', '3306'), - 'charset' => Env::get('database.charset', 'utf8mb4'), - 'prefix' => Env::get('database.prefix', 'tk_'), -]; - -// 连接数据库 -try { - $dsn = "{$dbConfig['type']}:host={$dbConfig['hostname']};port={$dbConfig['hostport']};dbname={$dbConfig['database']};charset={$dbConfig['charset']}"; - $pdo = new PDO($dsn, $dbConfig['username'], $dbConfig['password']); - $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - echo "数据库连接成功!\n"; -} catch (PDOException $e) { - die("数据库连接失败: " . $e->getMessage() . "\n"); -} - -// 创建菜单表SQL -$createTableSql = " -CREATE TABLE IF NOT EXISTS `{$dbConfig['prefix']}menus` ( - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '菜单ID', - `title` varchar(50) NOT NULL COMMENT '菜单名称', - `path` varchar(100) NOT NULL COMMENT '路由路径', - `icon` varchar(50) DEFAULT NULL COMMENT '图标名称', - `parent_id` int(11) NOT NULL DEFAULT '0' COMMENT '父菜单ID,0表示顶级菜单', - `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态:1启用,0禁用', - `sort` int(11) NOT NULL DEFAULT '0' COMMENT '排序,数值越小越靠前', - `create_time` int(11) DEFAULT NULL COMMENT '创建时间', - `update_time` int(11) DEFAULT NULL COMMENT '更新时间', - PRIMARY KEY (`id`), - KEY `idx_parent_id` (`parent_id`), - KEY `idx_status` (`status`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统菜单表'; -"; - -// 执行创建表SQL -try { - $pdo->exec($createTableSql); - echo "菜单表创建成功!\n"; -} catch (PDOException $e) { - echo "菜单表创建失败: " . $e->getMessage() . "\n"; -} - -// 检查表中是否已有数据 -$checkSql = "SELECT COUNT(*) FROM `{$dbConfig['prefix']}menus`"; -try { - $count = $pdo->query($checkSql)->fetchColumn(); - if ($count > 0) { - echo "菜单表中已有 {$count} 条数据,跳过数据初始化\n"; - exit(0); - } -} catch (PDOException $e) { - echo "检查数据失败: " . $e->getMessage() . "\n"; - exit(1); -} - -// 插入顶级菜单数据 -$topMenus = [ - ['仪表盘', '/dashboard', 'LayoutDashboard', 0, 1, 10], - ['项目管理', '/dashboard/projects', 'FolderKanban', 0, 1, 20], - ['客户池', '/dashboard/customers', 'Users', 0, 1, 30], - ['管理员权限', '/dashboard/admins', 'Settings', 0, 1, 40], - ['系统设置', '/settings', 'Cog', 0, 1, 50], -]; - -$insertTopMenuSql = "INSERT INTO `{$dbConfig['prefix']}menus` - (`title`, `path`, `icon`, `parent_id`, `status`, `sort`, `create_time`, `update_time`) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; - -$timestamp = time(); -$insertStmt = $pdo->prepare($insertTopMenuSql); - -$pdo->beginTransaction(); -try { - foreach ($topMenus as $index => $menu) { - $insertStmt->execute([ - $menu[0], // title - $menu[1], // path - $menu[2], // icon - $menu[3], // parent_id - $menu[4], // status - $menu[5], // sort - $timestamp, - $timestamp - ]); - } - $pdo->commit(); - echo "顶级菜单数据插入成功!\n"; -} catch (PDOException $e) { - $pdo->rollBack(); - echo "顶级菜单数据插入失败: " . $e->getMessage() . "\n"; - exit(1); -} - -// 查询刚插入的顶级菜单ID -$menuIds = []; -$queryTopMenuSql = "SELECT id, title FROM `{$dbConfig['prefix']}menus` WHERE parent_id = 0"; -try { - $topMenusResult = $pdo->query($queryTopMenuSql)->fetchAll(PDO::FETCH_ASSOC); - foreach ($topMenusResult as $menu) { - $menuIds[$menu['title']] = $menu['id']; - } -} catch (PDOException $e) { - echo "查询顶级菜单失败: " . $e->getMessage() . "\n"; - exit(1); -} - -// 插入子菜单数据 -$subMenus = [ - ['项目列表', '/dashboard/projects', 'List', $menuIds['项目管理'], 1, 21], - ['新建项目', '/dashboard/projects/new', 'PlusCircle', $menuIds['项目管理'], 1, 22], - ['客户管理', '/dashboard/customers', 'Users', $menuIds['客户池'], 1, 31], - ['客户分析', '/dashboard/customers/analytics', 'BarChart', $menuIds['客户池'], 1, 32], - ['管理员列表', '/dashboard/admins', 'UserCog', $menuIds['管理员权限'], 1, 41], - ['角色管理', '/dashboard/admins/roles', 'ShieldCheck', $menuIds['管理员权限'], 1, 42], - ['权限设置', '/dashboard/admins/permissions', 'Lock', $menuIds['管理员权限'], 1, 43], - ['基本设置', '/settings/general', 'Settings', $menuIds['系统设置'], 1, 51], - ['安全设置', '/settings/security', 'Shield', $menuIds['系统设置'], 1, 52], -]; - -$insertSubMenuSql = "INSERT INTO `{$dbConfig['prefix']}menus` - (`title`, `path`, `icon`, `parent_id`, `status`, `sort`, `create_time`, `update_time`) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; - -$pdo->beginTransaction(); -try { - $insertStmt = $pdo->prepare($insertSubMenuSql); - foreach ($subMenus as $menu) { - $insertStmt->execute([ - $menu[0], // title - $menu[1], // path - $menu[2], // icon - $menu[3], // parent_id - $menu[4], // status - $menu[5], // sort - $timestamp, - $timestamp - ]); - } - $pdo->commit(); - echo "子菜单数据插入成功!\n"; -} catch (PDOException $e) { - $pdo->rollBack(); - echo "子菜单数据插入失败: " . $e->getMessage() . "\n"; - exit(1); -} - -echo "菜单初始化完成!\n"; \ No newline at end of file