From 2d5e334a5c8944849d0cf0c373259d16cde85beb Mon Sep 17 00:00:00 2001 From: luorijun Date: Tue, 23 Sep 2025 11:40:12 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20debug=20=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 10 ++++ bun.lock | 15 +++++ docker-compose.yaml | 10 +++- package.json | 1 + prisma/init.sql | 30 +--------- src/actions/config.ts | 18 ++++++ src/actions/remote.ts | 96 ++++++++++++++++++++++++++++++ src/app/debug/config/page.tsx | 108 ++++++++++++++++++++++++++++++++++ src/app/debug/layout.tsx | 11 ++++ src/lib/prisma.ts | 1 + src/lib/redis.ts | 9 +++ 11 files changed, 279 insertions(+), 30 deletions(-) create mode 100644 .env.example create mode 100644 src/actions/config.ts create mode 100644 src/actions/remote.ts create mode 100644 src/app/debug/config/page.tsx create mode 100644 src/app/debug/layout.tsx create mode 100644 src/lib/redis.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b932ddb --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +# 数据库连接字符串 +DATABASE_URL= + +# Redis 连接字符串 +REDIS_URL= + +# 京东网关配置 +JD_BASE=https://smart.jdbox.xyz:58001 +JD_USERNAME= +JD_PASSWORD= \ No newline at end of file diff --git a/bun.lock b/bun.lock index 13577bc..c759d09 100644 --- a/bun.lock +++ b/bun.lock @@ -19,6 +19,7 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-hook-form": "^7.62.0", + "redis": "^5.8.2", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "zustand": "^5.0.8", @@ -302,6 +303,16 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + "@redis/bloom": ["@redis/bloom@5.8.2", "https://registry.npmmirror.com/@redis/bloom/-/bloom-5.8.2.tgz", { "peerDependencies": { "@redis/client": "^5.8.2" } }, "sha512-855DR0ChetZLarblio5eM0yLwxA9Dqq50t8StXKp5bAtLT0G+rZ+eRzzqxl37sPqQKjUudSYypz55o6nNhbz0A=="], + + "@redis/client": ["@redis/client@5.8.2", "https://registry.npmmirror.com/@redis/client/-/client-5.8.2.tgz", { "dependencies": { "cluster-key-slot": "1.1.2" } }, "sha512-WtMScno3+eBpTac1Uav2zugXEoXqaU23YznwvFgkPwBQVwEHTDgOG7uEAObtZ/Nyn8SmAMbqkEubJaMOvnqdsQ=="], + + "@redis/json": ["@redis/json@5.8.2", "https://registry.npmmirror.com/@redis/json/-/json-5.8.2.tgz", { "peerDependencies": { "@redis/client": "^5.8.2" } }, "sha512-uxpVfas3I0LccBX9rIfDgJ0dBrUa3+0Gc8sEwmQQH0vHi7C1Rx1Qn8Nv1QWz5bohoeIXMICFZRcyDONvum2l/w=="], + + "@redis/search": ["@redis/search@5.8.2", "https://registry.npmmirror.com/@redis/search/-/search-5.8.2.tgz", { "peerDependencies": { "@redis/client": "^5.8.2" } }, "sha512-cNv7HlgayavCBXqPXgaS97DRPVWFznuzsAmmuemi2TMCx5scwLiP50TeZvUS06h/MG96YNPe6A0Zt57yayfxwA=="], + + "@redis/time-series": ["@redis/time-series@5.8.2", "https://registry.npmmirror.com/@redis/time-series/-/time-series-5.8.2.tgz", { "peerDependencies": { "@redis/client": "^5.8.2" } }, "sha512-g2NlHM07fK8H4k+613NBsk3y70R2JIM2dPMSkhIjl2Z17SYvaYKdusz85d7VYOrZBWtDrHV/WD2E3vGu+zni8A=="], + "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], "@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.12.0", "", {}, "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw=="], @@ -490,6 +501,8 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "cluster-key-slot": ["cluster-key-slot@1.1.2", "https://registry.npmmirror.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], + "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -908,6 +921,8 @@ "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "redis": ["redis@5.8.2", "https://registry.npmmirror.com/redis/-/redis-5.8.2.tgz", { "dependencies": { "@redis/bloom": "5.8.2", "@redis/client": "5.8.2", "@redis/json": "5.8.2", "@redis/search": "5.8.2", "@redis/time-series": "5.8.2" } }, "sha512-31vunZj07++Y1vcFGcnNWEf5jPoTkGARgfWI4+Tk55vdwHxhAvug8VEtW7Cx+/h47NuJTEg/JL77zAwC6E0OeA=="], + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], diff --git a/docker-compose.yaml b/docker-compose.yaml index 58c79c5..00f329f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,4 +1,5 @@ services: + mariadb: image: mariadb:10 environment: @@ -7,4 +8,11 @@ services: ports: - "23306:3306" volumes: - - .volumes/mysql:/var/lib/mysql \ No newline at end of file + - .volumes/mysql:/var/lib/mysql + + redis: + image: redis:7 + ports: + - "26379:6379" + volumes: + - .volumes/redis:/data \ No newline at end of file diff --git a/package.json b/package.json index e8ae8f0..8b55614 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-hook-form": "^7.62.0", + "redis": "^5.8.2", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "zustand": "^5.0.8" diff --git a/prisma/init.sql b/prisma/init.sql index 78ba5b8..0a8fc7b 100644 --- a/prisma/init.sql +++ b/prisma/init.sql @@ -1,21 +1,3 @@ --- jdbox.accounts definition -CREATE TABLE `accounts` ( - `id` varchar(191) NOT NULL, - `user_id` int(11) NOT NULL, - `type` varchar(191) NOT NULL, - `provider` varchar(191) NOT NULL, - `provider_account_id` varchar(191) NOT NULL, - `refresh_token` text DEFAULT NULL, - `access_token` text DEFAULT NULL, - `expires_at` int(11) DEFAULT NULL, - `token_type` varchar(191) DEFAULT NULL, - `scope` varchar(191) DEFAULT NULL, - `id_token` text DEFAULT NULL, - `session_state` varchar(191) DEFAULT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `accounts_provider_provider_account_id_key` (`provider`, `provider_account_id`), - KEY `accounts_user_id_fkey` (`user_id`) USING BTREE -) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; -- jdbox.sessions definition CREATE TABLE `sessions` ( `id` varchar(191) NOT NULL, @@ -25,6 +7,7 @@ CREATE TABLE `sessions` ( PRIMARY KEY (`id`), KEY `sessions_userId_idx` (`userId`) USING BTREE ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + -- jdbox.users definition CREATE TABLE `users` ( `id` int(11) NOT NULL AUTO_INCREMENT, @@ -36,17 +19,6 @@ CREATE TABLE `users` ( PRIMARY KEY (`id`), UNIQUE KEY `users_phone_key` (`phone`) ) ENGINE = InnoDB AUTO_INCREMENT = 2 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; --- jdbox.verification_codes definition -CREATE TABLE `verification_codes` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `phone` varchar(191) NOT NULL, - `code` varchar(191) NOT NULL, - `type` varchar(191) NOT NULL, - `expiresAt` datetime(3) NOT NULL, - `createdAt` datetime(3) NOT NULL DEFAULT current_timestamp(3), - PRIMARY KEY (`id`), - KEY `verification_codes_phone_type_idx` (`phone`, `type`) USING BTREE -) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; -- 插入初始用户 INSERT INTO users(phone, password, name) diff --git a/src/actions/config.ts b/src/actions/config.ts new file mode 100644 index 0000000..4c31e8b --- /dev/null +++ b/src/actions/config.ts @@ -0,0 +1,18 @@ +'use server' +import prisma from '@/lib/prisma' +import { log } from 'console' + +export async function findConfigs(params: { + macaddr: string +}) { + try { + return await prisma.gateway.findMany({ + where: { + macaddr: params.macaddr, + }, + }) + } + catch (e) { + throw new Error('查询配置失败: ' + (e as Error).message) + } +} diff --git a/src/actions/remote.ts b/src/actions/remote.ts new file mode 100644 index 0000000..68990ab --- /dev/null +++ b/src/actions/remote.ts @@ -0,0 +1,96 @@ +'use server' +import redis from '@/lib/redis' + +const base = process.env.JD_BASE +const username = process.env.JD_USERNAME +const password = process.env.JD_PASSWORD + +type JdResp = { + code: number + meta: string + data: T +} + +async function post(path: string, data: unknown) { + try { + if (!base) throw new Error('JD_BASE 环境变量未设置') + if (!username) throw new Error('JD_USERNAME 环境变量未设置') + if (!password) throw new Error('JD_PASSWORD 环境变量未设置') + + // 获取令牌 + let token = await redis.get('token') + if (!token) { + const resp = await fetch(`${base}/client/auth`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + username, + password, + }), + }) + + const json = await resp.json() + if (json.code !== 0) { + throw new Error('响应失败: ' + json.meta) + } + + token = json.data + if (!token) { + throw new Error('响应中缺少 token') + } + + await redis.set('token', token, { + expiration: { type: 'EX', value: 6 * 24 * 3600 }, + }) + } + + // 发起请求 + const resp = await fetch(base + path, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Token': token, + }, + body: JSON.stringify(data), + }) + if (resp.status === 401) { + await redis.del('token') + throw new Error('令牌无效,已删除缓存,请重试') + } + + return await resp.json() as JdResp + } + catch (e) { + throw new Error('请求失败: ' + (e as Error).message) + } +} + +export async function gatewayConfigGet(params: { + macaddr: string +}) { + try { + const resp = await post('/gateway/config/get', params) + if (resp.code !== 0) { + throw new Error('响应失败: ' + resp.meta) + } + if (!resp.data) { + throw new Error('响应中缺少 data') + } + + return JSON.parse(atob(resp.data)) as { + id: number + rules: { + table: number + enable: boolean + edge: string[] + network: string[] + cityhash: string + }[] + } + } + catch (e) { + throw new Error('获取远程配置失败: ' + (e as Error).message) + } +} diff --git a/src/app/debug/config/page.tsx b/src/app/debug/config/page.tsx new file mode 100644 index 0000000..5fd3a83 --- /dev/null +++ b/src/app/debug/config/page.tsx @@ -0,0 +1,108 @@ +'use client' +import { findConfigs } from '@/actions/config' +import { gatewayConfigGet } from '@/actions/remote' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import { useState } from 'react' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' + +type EdgeConfig = { + port?: string + edge?: string + city?: string + _index: number +} + +export default function DebugConfigPage() { + const [macaddr, setMacaddr] = useState('') + const [remotes, setRemotes] = useState([]) + const [locals, setLocals] = useState([]) + + const fetch = async (macaddr: string) => { + try { + console.log('fetch', macaddr) + if (!macaddr) return + + const rawLocal = await findConfigs({ macaddr }) + console.log('raw local', rawLocal) + + const rawRemote = await gatewayConfigGet({ macaddr }) + console.log('raw remote', rawRemote) + + setLocals(rawLocal.map(rule => ({ + port: rule.network, + edge: rule.edge, + city: rule.cityhash, + _index: parseInt(rule.network.split('.')[3] || '0'), + })).sort((a, b) => a._index - b._index)) + + setRemotes(rawRemote.rules.map((rule) => { + const port = rule.network.find(n => !!n) + return ({ + port: port, + edge: rule.edge.find(n => !!n), + city: rule.cityhash, + _index: port ? parseInt(port.split('.')[3]) : 0, + }) + }).sort((a, b) => a._index - b._index)) + } + catch (e) { + console.error('数据获取失败', e) + } + } + + return ( +
+
+ setMacaddr(e.target.value)} className="flex-none basis-60" /> + +
+ + + + 端口 + 节点 + 城市 + + + + {!locals.length || !remotes.length ? ( + + 获取数据为空 + + ) : locals.length !== remotes.length ? ( + + 本地和远程规则数量不匹配 + + ) : ( + locals.map((item, index) => ( + + + {item.port === remotes[index].port ? ( + {item.port} + ) : ( + {item.port} : {remotes[index].port} + )} + + + {item.edge === remotes[index].edge ? ( + {item.edge} + ) : ( + {item.edge} : {remotes[index].edge} + )} + + + {item.city === remotes[index].city ? ( + {item.city} + ) : ( + {item.city} : {remotes[index].city} + )} + + + )) + )} + +
+
+ ) +} diff --git a/src/app/debug/layout.tsx b/src/app/debug/layout.tsx new file mode 100644 index 0000000..4adfd0d --- /dev/null +++ b/src/app/debug/layout.tsx @@ -0,0 +1,11 @@ +import { ReactNode } from 'react' + +export default async function DebugLayout(props: { + children: ReactNode +}) { + return ( +
+ {props.children} +
+ ) +} diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index ab0c930..157791e 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -1,3 +1,4 @@ +import 'server-only' import { PrismaClient } from '@/generated/prisma/client' const globalForPrisma = global as unknown as { diff --git a/src/lib/redis.ts b/src/lib/redis.ts new file mode 100644 index 0000000..4db706c --- /dev/null +++ b/src/lib/redis.ts @@ -0,0 +1,9 @@ +import 'server-only' +import { createClient } from 'redis' + +const client = createClient({ + url: process.env.REDIS_URL, +}) + +const redis = await client.connect() +export default redis