迁移 orm 依赖到 drizzle
This commit is contained in:
@@ -1,58 +1,57 @@
|
||||
'use server'
|
||||
import prisma, { User } from '@/lib/prisma' // 使用统一的prisma实例
|
||||
import drizzle, { eq, sessions, users } from '@/lib/drizzle'
|
||||
import { compare } from 'bcryptjs'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { cookies } from 'next/headers'
|
||||
import { z } from 'zod'
|
||||
|
||||
const loginSchema = z.object({
|
||||
account: z.string().min(3, '账号至少需要3个字符'),
|
||||
password: z.string().min(6, '密码至少需要6个字符'),
|
||||
const loginProps = z.object({
|
||||
account: z.string().min(3, '账号至少需要3个字符').trim(),
|
||||
password: z.string().min(6, '密码至少需要6个字符').trim(),
|
||||
})
|
||||
|
||||
export async function login(props: {
|
||||
account: string
|
||||
password: string
|
||||
}) {
|
||||
export async function login(rawParams: z.infer<typeof loginProps>) {
|
||||
try {
|
||||
const result = loginSchema.parse(props)
|
||||
const params = loginProps.parse(rawParams)
|
||||
|
||||
const user: User | null = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ account: result.account.trim() },
|
||||
{ password: result.password.trim() },
|
||||
],
|
||||
},
|
||||
// 查找用户
|
||||
const user = await drizzle.query.users.findFirst({
|
||||
where: eq(users.account, params.account),
|
||||
})
|
||||
if (!user) {
|
||||
return {
|
||||
success: false,
|
||||
error: '用户不存在或密码未设置',
|
||||
error: '用户不存在或密码错误',
|
||||
}
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
const passwordMatch = await compare(result.password, user.password || '')
|
||||
const passwordMatch = await compare(params.password, user.password || '')
|
||||
|
||||
if (!passwordMatch) {
|
||||
return {
|
||||
success: false,
|
||||
error: '密码错误',
|
||||
error: '用户不存在或密码错误',
|
||||
}
|
||||
}
|
||||
|
||||
// 创建会话
|
||||
const sessionToken = crypto.randomUUID()
|
||||
await prisma.session.create({
|
||||
data: {
|
||||
id: sessionToken,
|
||||
userId: user.id,
|
||||
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
||||
},
|
||||
const sessionToken = randomUUID()
|
||||
await drizzle.insert(sessions).values({
|
||||
id: sessionToken,
|
||||
userid: user.id,
|
||||
expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
||||
})
|
||||
|
||||
// 设置cookie
|
||||
const response = {
|
||||
const cookieStore = await cookies()
|
||||
cookieStore.set('session', sessionToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
user: {
|
||||
id: user.id,
|
||||
@@ -60,15 +59,6 @@ export async function login(props: {
|
||||
name: user.name,
|
||||
},
|
||||
}
|
||||
|
||||
const cookieStore = await cookies()
|
||||
cookieStore.set('session', sessionToken, {
|
||||
httpOnly: true,
|
||||
// secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
catch (error) {
|
||||
console.error('登录错误:', error)
|
||||
@@ -86,24 +76,19 @@ export async function logout() {
|
||||
|
||||
// 删除数据库中的session(如果存在)
|
||||
if (sessionToken) {
|
||||
await prisma.session.deleteMany({
|
||||
where: { id: sessionToken },
|
||||
}).catch(() => {
|
||||
// 忽略删除错误,确保cookie被清除
|
||||
})
|
||||
await drizzle.delete(sessions).where(eq(sessions.id, sessionToken))
|
||||
}
|
||||
|
||||
// 清除cookie
|
||||
const response = { success: true }
|
||||
cookieStore.set('session', '', {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'lax',
|
||||
maxAge: 0, // 立即过期
|
||||
path: '/',
|
||||
})
|
||||
|
||||
return response
|
||||
return {
|
||||
success: true,
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('退出错误:', error)
|
||||
|
||||
@@ -1,18 +1,47 @@
|
||||
'use server'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { log } from 'console'
|
||||
|
||||
import drizzle, { eq, gateway } from '@/lib/drizzle'
|
||||
import z from 'zod'
|
||||
|
||||
export async function findConfigs(params: {
|
||||
macaddr: string
|
||||
}) {
|
||||
try {
|
||||
return await prisma.gateway.findMany({
|
||||
where: {
|
||||
macaddr: params.macaddr,
|
||||
},
|
||||
return await drizzle.query.gateway.findMany({
|
||||
where: eq(gateway.macaddr, params.macaddr),
|
||||
})
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error('查询配置失败: ' + (e as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
const pageConfigsParams = z.object({
|
||||
macaddr: z.string().min(1, 'MAC地址不能为空').trim(),
|
||||
page: z.number().min(1).default(1),
|
||||
size: z.number().min(10).max(250).default(100),
|
||||
})
|
||||
|
||||
export async function pageConfigs(rawParams: z.infer<typeof pageConfigsParams>) {
|
||||
try {
|
||||
const params = pageConfigsParams.parse(rawParams)
|
||||
const offset = (params.page - 1) * params.size
|
||||
const limit = params.size
|
||||
|
||||
const [data, total] = await Promise.all([
|
||||
drizzle.select().from(gateway).where(eq(gateway.macaddr, params.macaddr)).offset(offset).limit(limit),
|
||||
drizzle.$count(gateway, eq(gateway.macaddr, params.macaddr)),
|
||||
])
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
total,
|
||||
page: params.page,
|
||||
size: params.size,
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error('获取配置失败: ' + (e as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,48 @@
|
||||
'use server'
|
||||
import prisma from '@/lib/prisma'
|
||||
|
||||
import { Page, Res } from '@/lib/api'
|
||||
import drizzle, { change, cityhash, count, desc, edge, eq, gateway, is, sql, token } from '@/lib/drizzle'
|
||||
|
||||
export type AllocationStatus = {
|
||||
city: string
|
||||
count: bigint
|
||||
assigned: bigint
|
||||
count: number
|
||||
assigned: number
|
||||
}
|
||||
|
||||
// 城市分配状态
|
||||
export async function getAllocationStatus(hours: number) {
|
||||
export async function getAllocationStatus(hours: number = 24) {
|
||||
try {
|
||||
const hoursNum = hours || 24
|
||||
// 使用参数化查询防止SQL注入
|
||||
const result: AllocationStatus[] = await prisma.$queryRaw`
|
||||
SELECT
|
||||
city,
|
||||
c1.count AS count,
|
||||
c2.assigned AS assigned
|
||||
FROM
|
||||
cityhash
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
city_id,
|
||||
COUNT(*) AS count
|
||||
FROM
|
||||
edge
|
||||
WHERE
|
||||
active = 1
|
||||
GROUP BY
|
||||
city_id
|
||||
) c1 ON c1.city_id = cityhash.id
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
city AS city_id,
|
||||
COUNT(*) AS assigned
|
||||
FROM
|
||||
\`change\`
|
||||
WHERE
|
||||
time > NOW() - INTERVAL ${hoursNum} HOUR
|
||||
GROUP BY
|
||||
city
|
||||
) c2 ON c2.city_id = cityhash.id
|
||||
WHERE
|
||||
cityhash.macaddr IS NOT NULL;
|
||||
`
|
||||
const c1 = drizzle
|
||||
.select({
|
||||
cityId: edge.cityId,
|
||||
count: count().as('count'),
|
||||
})
|
||||
.from(edge)
|
||||
.where(eq(edge.active, 1))
|
||||
.groupBy(edge.cityId)
|
||||
.as('c1')
|
||||
|
||||
const c2 = drizzle
|
||||
.select({
|
||||
cityId: change.city,
|
||||
assigned: count().as('assigned'),
|
||||
})
|
||||
.from(change)
|
||||
.where(sql`time > NOW() - INTERVAL ${hours} HOUR`)
|
||||
.groupBy(change.city)
|
||||
.as('c2')
|
||||
|
||||
const result = await drizzle
|
||||
.select({
|
||||
city: cityhash.city,
|
||||
count: sql<number>`c1.count`,
|
||||
assigned: sql<number>`ifnull(c2.assigned, 0)`,
|
||||
})
|
||||
.from(cityhash)
|
||||
.leftJoin(c1, eq(c1.cityId, cityhash.id))
|
||||
.leftJoin(c2, eq(c2.cityId, cityhash.id))
|
||||
.where(sql`cityhash.macaddr is not null`)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
@@ -70,11 +69,16 @@ export type GatewayInfo = {
|
||||
// 获取网关基本信息
|
||||
export async function getGatewayInfo() {
|
||||
try {
|
||||
const result: GatewayInfo[] = await prisma.$queryRaw`
|
||||
SELECT macaddr, inner_ip, setid, enable
|
||||
FROM token
|
||||
ORDER BY macaddr
|
||||
`
|
||||
const result = await drizzle
|
||||
.select({
|
||||
macaddr: token.macaddr,
|
||||
inner_ip: token.innerIp,
|
||||
setid: token.setid,
|
||||
enable: token.enable,
|
||||
})
|
||||
.from(token)
|
||||
.orderBy(token.macaddr)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
@@ -91,88 +95,63 @@ export async function getGatewayInfo() {
|
||||
}
|
||||
|
||||
export type GatewayConfig = {
|
||||
city: string
|
||||
city: string | null
|
||||
edge: string
|
||||
user: string
|
||||
public: string
|
||||
public: string | null
|
||||
inner_ip: string
|
||||
ischange: number
|
||||
isonline: number
|
||||
}
|
||||
|
||||
// 网关配置
|
||||
export async function getGatewayConfig(off: number, itemsPerPage: number, mac?: string) {
|
||||
export async function getGatewayConfig(page?: number, mac?: string): Promise<Res<Page<GatewayConfig>>> {
|
||||
try {
|
||||
const offset = off || 0
|
||||
const limit = itemsPerPage || 100
|
||||
|
||||
// 获取总数
|
||||
let totalCountQuery = ''
|
||||
let totalCountParams: (string | number)[] = []
|
||||
|
||||
if (mac) {
|
||||
totalCountQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM gateway
|
||||
LEFT JOIN cityhash ON cityhash.hash = gateway.cityhash
|
||||
LEFT JOIN edge ON edge.macaddr = gateway.edge
|
||||
WHERE gateway.macaddr = ?
|
||||
`
|
||||
totalCountParams = [mac]
|
||||
}
|
||||
else {
|
||||
totalCountQuery = `
|
||||
SELECT COUNT(*) as total
|
||||
FROM gateway
|
||||
`
|
||||
if (!page && !mac) {
|
||||
throw new Error('页码和MAC地址不能同时为空')
|
||||
}
|
||||
|
||||
const totalCountResult = await prisma.$queryRawUnsafe<[{ total: bigint }]>(
|
||||
totalCountQuery,
|
||||
...totalCountParams,
|
||||
)
|
||||
const totalCount = Number(totalCountResult[0]?.total || 0)
|
||||
|
||||
// 获取分页数据
|
||||
let query = `
|
||||
select edge, city, user, public, inner_ip, ischange, isonline
|
||||
from
|
||||
gateway
|
||||
left join cityhash
|
||||
on cityhash.hash = gateway.cityhash
|
||||
left join edge
|
||||
on edge.macaddr = gateway.edge
|
||||
`
|
||||
let params: (string | number)[] = []
|
||||
|
||||
if (mac) {
|
||||
query += ' WHERE gateway.macaddr = ?'
|
||||
params = [mac]
|
||||
}
|
||||
else {
|
||||
query += ' LIMIT ? OFFSET ?'
|
||||
params.push(limit, offset)
|
||||
}
|
||||
|
||||
// 指定返回类型
|
||||
const result: GatewayConfig[] = await prisma.$queryRawUnsafe<GatewayConfig[]>(query, ...params)
|
||||
page = mac ? 1 : Math.max(1, page || 1)
|
||||
const [total, result] = await Promise.all([
|
||||
drizzle.$count(gateway, mac ? eq(gateway.macaddr, mac) : undefined),
|
||||
drizzle
|
||||
.select({
|
||||
city: cityhash.city,
|
||||
edge: gateway.edge,
|
||||
user: gateway.user,
|
||||
public: edge.public,
|
||||
inner_ip: gateway.network,
|
||||
ischange: gateway.ischange,
|
||||
isonline: gateway.isonline,
|
||||
})
|
||||
.from(gateway)
|
||||
.leftJoin(cityhash, eq(cityhash.hash, gateway.cityhash))
|
||||
.leftJoin(edge, eq(edge.macaddr, gateway.edge))
|
||||
.where(mac ? eq(gateway.macaddr, mac) : undefined)
|
||||
.orderBy(gateway.macaddr, sql`cast(regexp_replace(gateway.network, '172.30.168.', '') as unsigned)`)
|
||||
.offset((page - 1) * 250)
|
||||
.limit(250),
|
||||
])
|
||||
|
||||
return {
|
||||
data: result,
|
||||
totalCount: totalCount,
|
||||
currentPage: Math.floor(offset / limit) + 1,
|
||||
totalPages: Math.ceil(totalCount / limit),
|
||||
success: true,
|
||||
data: {
|
||||
total,
|
||||
page,
|
||||
size: 250,
|
||||
items: result,
|
||||
},
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Gateway config query error:', error)
|
||||
return {
|
||||
success: false,
|
||||
data: [],
|
||||
error: '查询网关配置失败',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type CityNode = {
|
||||
city: string
|
||||
count: number
|
||||
@@ -184,25 +163,29 @@ export type CityNode = {
|
||||
// 城市节点数量分布
|
||||
export async function getCityNodeCount() {
|
||||
try {
|
||||
const result: CityNode[] = await prisma.$queryRaw`
|
||||
select c.city, c.hash, c.label, e.count, c.\`offset\`
|
||||
from
|
||||
cityhash c
|
||||
left join (
|
||||
select city_id, count(*) as count
|
||||
from
|
||||
edge
|
||||
where
|
||||
edge.active is true
|
||||
group by
|
||||
city_id
|
||||
) e
|
||||
on c.id = e.city_id
|
||||
group by
|
||||
c.hash
|
||||
order by
|
||||
count desc
|
||||
`
|
||||
const e = drizzle
|
||||
.select({
|
||||
cityId: edge.cityId,
|
||||
count: count().as('count'),
|
||||
})
|
||||
.from(edge)
|
||||
.where(eq(edge.active, 1))
|
||||
.groupBy(edge.cityId)
|
||||
.as('e')
|
||||
|
||||
const result = await drizzle
|
||||
.select({
|
||||
city: cityhash.city,
|
||||
hash: cityhash.hash,
|
||||
label: cityhash.label,
|
||||
count: sql<number>`ifnull(e.count, 0)`,
|
||||
offset: cityhash.offset,
|
||||
})
|
||||
.from(cityhash)
|
||||
.leftJoin(e, eq(e.cityId, cityhash.id))
|
||||
.groupBy(cityhash.hash)
|
||||
.orderBy(desc(sql`e.count`))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result,
|
||||
@@ -219,32 +202,39 @@ export async function getCityNodeCount() {
|
||||
}
|
||||
|
||||
// 获取节点信息
|
||||
export async function getEdgeNodes(off: number, itemsPerPage: number) {
|
||||
export async function getEdgeNodes(page: number, size: number) {
|
||||
try {
|
||||
const offset = off || 0
|
||||
const limit = itemsPerPage || 100
|
||||
// 获取总数 - 使用类型断言
|
||||
const totalCountResult = await prisma.$queryRaw<[{ total: bigint }]>`
|
||||
SELECT COUNT(*) as total
|
||||
FROM edge
|
||||
WHERE active = true
|
||||
`
|
||||
const totalCount = Number(totalCountResult[0]?.total || 0)
|
||||
const offset = Math.max(0, (page - 1)) * size
|
||||
const limit = Math.min(100, Math.max(10, size))
|
||||
|
||||
const [total, data] = await Promise.all([
|
||||
drizzle.$count(edge, eq(edge.active, 1)),
|
||||
drizzle
|
||||
.select({
|
||||
id: edge.id,
|
||||
macaddr: edge.macaddr,
|
||||
city: cityhash.city,
|
||||
public: edge.public,
|
||||
isp: edge.isp,
|
||||
single: edge.single,
|
||||
sole: edge.sole,
|
||||
arch: edge.arch,
|
||||
online: edge.online,
|
||||
})
|
||||
.from(edge)
|
||||
.leftJoin(cityhash, eq(cityhash.id, edge.cityId))
|
||||
.where(eq(edge.active, 1))
|
||||
.orderBy(edge.id)
|
||||
.offset(offset)
|
||||
.limit(limit),
|
||||
|
||||
])
|
||||
|
||||
// 获取分页数据
|
||||
const result = await prisma.$queryRaw`
|
||||
SELECT edge.id, edge.macaddr, city, public, isp, single, sole, arch, online
|
||||
FROM edge
|
||||
LEFT JOIN cityhash ON cityhash.id = edge.city_id
|
||||
WHERE edge.active = true
|
||||
ORDER BY edge.id
|
||||
LIMIT ${limit} OFFSET ${offset}
|
||||
`
|
||||
return {
|
||||
data: result,
|
||||
totalCount: totalCount,
|
||||
data,
|
||||
totalCount: total,
|
||||
currentPage: Math.floor(offset / limit) + 1,
|
||||
totalPages: Math.ceil(totalCount / limit),
|
||||
totalPages: Math.ceil(total / limit),
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use server'
|
||||
import prisma from '@/lib/prisma'
|
||||
import drizzle, { desc, eq, users } from '@/lib/drizzle'
|
||||
import { first } from '@/lib/utils'
|
||||
import { hash } from 'bcryptjs'
|
||||
|
||||
type User = {
|
||||
@@ -17,22 +18,20 @@ export async function findUsers(): Promise<{
|
||||
error: string
|
||||
}> {
|
||||
try {
|
||||
const users = await prisma.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
account: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
})
|
||||
const result = await drizzle
|
||||
.select({
|
||||
id: users.id,
|
||||
account: users.account,
|
||||
name: users.name,
|
||||
createdAt: users.createdat,
|
||||
updatedAt: users.updatedat,
|
||||
})
|
||||
.from(users)
|
||||
.orderBy(desc(users.createdat))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: users,
|
||||
data: result,
|
||||
error: '',
|
||||
}
|
||||
}
|
||||
@@ -54,11 +53,10 @@ export async function createUser(params: {
|
||||
}) {
|
||||
try {
|
||||
// 检查用户是否已存在
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { account: params.account },
|
||||
const user = await drizzle.query.users.findFirst({
|
||||
where: eq(users.account, params.account),
|
||||
})
|
||||
|
||||
if (existingUser) {
|
||||
if (user) {
|
||||
return {
|
||||
success: false,
|
||||
error: '用户账号已存在',
|
||||
@@ -69,20 +67,31 @@ export async function createUser(params: {
|
||||
const hashedPassword = await hash(params.password, 10)
|
||||
|
||||
// 创建用户
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
account: params.account,
|
||||
password: hashedPassword,
|
||||
name: params.name || params.account,
|
||||
},
|
||||
})
|
||||
const id = first(
|
||||
await drizzle
|
||||
.insert(users)
|
||||
.values({
|
||||
account: params.account,
|
||||
password: hashedPassword,
|
||||
name: params.name || params.account,
|
||||
}).$returningId(),
|
||||
r => r.id,
|
||||
)
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
error: '创建用户失败',
|
||||
}
|
||||
}
|
||||
|
||||
// 不返回密码字段
|
||||
const { password: _, ...userWithoutPassword } = user
|
||||
|
||||
return {
|
||||
success: true,
|
||||
user: userWithoutPassword,
|
||||
user: {
|
||||
id,
|
||||
account: params.account,
|
||||
name: params.name || params.account,
|
||||
},
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
@@ -97,38 +106,10 @@ export async function createUser(params: {
|
||||
// 删除用户
|
||||
export async function removeUser(id: number) {
|
||||
try {
|
||||
if (!id) {
|
||||
return {
|
||||
success: false,
|
||||
error: '用户ID不能为空',
|
||||
}
|
||||
}
|
||||
await drizzle
|
||||
.delete(users)
|
||||
.where(eq(users.id, id))
|
||||
|
||||
// const userId = parseInt(id)
|
||||
if (isNaN(id)) {
|
||||
return {
|
||||
success: false,
|
||||
error: '无效的用户ID',
|
||||
}
|
||||
}
|
||||
|
||||
// 检查用户是否存在
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { id: id },
|
||||
})
|
||||
|
||||
if (!existingUser) {
|
||||
return {
|
||||
status: 404,
|
||||
success: false,
|
||||
error: '用户不存在',
|
||||
}
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
await prisma.user.delete({
|
||||
where: { id: id },
|
||||
})
|
||||
return {
|
||||
success: true,
|
||||
message: '用户删除成功',
|
||||
|
||||
@@ -40,11 +40,11 @@ export default function AllocationStatus({ detailed = false }: { detailed?: bool
|
||||
// 数据验证
|
||||
const validatedData = (result.data).map(item => ({
|
||||
city: item.city || '未知',
|
||||
count: item.count || BigInt(0),
|
||||
assigned: item.assigned || BigInt(0),
|
||||
count: item.count,
|
||||
assigned: item.assigned,
|
||||
}))
|
||||
|
||||
const sortedData = validatedData.sort((a, b) => Number(b.count - a.count))
|
||||
const sortedData = validatedData.sort((a, b) => b.count - a.count)
|
||||
|
||||
setData(sortedData)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { validateNumber } from '@/lib/utils'
|
||||
import { Pagination } from '@/components/ui/pagination'
|
||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
||||
import { getEdgeNodes } from '@/actions/stats'
|
||||
@@ -56,15 +55,15 @@ export default function Edge() {
|
||||
}
|
||||
|
||||
const validatedData = (result.data as ResultEdge[]).map(item => ({
|
||||
id: validateNumber(item.id),
|
||||
id: item.id,
|
||||
macaddr: item.macaddr || '',
|
||||
city: item.city || '',
|
||||
public: item.public || '',
|
||||
isp: item.isp || '',
|
||||
single: item.single,
|
||||
sole: item.sole,
|
||||
arch: validateNumber(item.arch),
|
||||
online: validateNumber(item.online),
|
||||
arch: item.arch,
|
||||
online: item.online,
|
||||
}))
|
||||
|
||||
setData(validatedData)
|
||||
|
||||
@@ -4,79 +4,56 @@ import { useSearchParams } from 'next/navigation'
|
||||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table'
|
||||
import { Pagination } from '@/components/ui/pagination'
|
||||
import { getGatewayConfig, type GatewayConfig } from '@/actions/stats'
|
||||
|
||||
// interface GatewayConfig {
|
||||
// city: string
|
||||
// edge: string
|
||||
// user: string
|
||||
// public: string
|
||||
// inner_ip: string
|
||||
// ischange: number
|
||||
// isonline: number
|
||||
// }
|
||||
import { toast } from 'sonner'
|
||||
|
||||
function GatewayConfigContent() {
|
||||
const [data, setData] = useState<GatewayConfig[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [macAddress, setMacAddress] = useState('')
|
||||
const [error, setError] = useState('')
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
// 分页状态
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [itemsPerPage, setItemsPerPage] = useState(100)
|
||||
const [totalItems, setTotalItems] = useState(0)
|
||||
|
||||
// 判断是否为MAC地址查询(用于控制分页显示)
|
||||
const isMacQuery = !!macAddress
|
||||
const [page, setPage] = useState(1)
|
||||
const [total, setTotal] = useState(0)
|
||||
|
||||
// 监听URL的mac参数变化
|
||||
useEffect(() => {
|
||||
const urlMac = searchParams.get('mac')
|
||||
if (urlMac) {
|
||||
setMacAddress(urlMac)
|
||||
setCurrentPage(1) // 重置到第一页
|
||||
fetchData(urlMac, 1, itemsPerPage)
|
||||
setPage(1) // 重置到第一页
|
||||
fetchData(urlMac, 1)
|
||||
}
|
||||
else {
|
||||
setMacAddress('')
|
||||
setCurrentPage(1) // 重置到第一页
|
||||
fetchData('', 1, itemsPerPage)
|
||||
setPage(1) // 重置到第一页
|
||||
fetchData('', 1)
|
||||
}
|
||||
}, [searchParams])
|
||||
|
||||
const fetchData = async (mac: string, page: number = 1, limit: number = itemsPerPage) => {
|
||||
const fetchData = async (mac: string, page: number = 1) => {
|
||||
setLoading(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
// 计算偏移量
|
||||
const offset = (page - 1) * limit
|
||||
|
||||
const result = await getGatewayConfig(offset, limit, mac)
|
||||
|
||||
// 检查返回的数据是否有效
|
||||
if (!result.data || result.data.length === 0) {
|
||||
if (mac.trim()) {
|
||||
setError(`未找到MAC地址为 ${mac} 的网关配置信息`)
|
||||
}
|
||||
else {
|
||||
setError('未找到任何网关配置信息')
|
||||
}
|
||||
setData([])
|
||||
setTotalItems(0)
|
||||
return
|
||||
const result = await getGatewayConfig(page, mac)
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || '查询网关配置失败')
|
||||
}
|
||||
|
||||
setData(result.data)
|
||||
const shrink = ['黔东南', '延边']
|
||||
result.data.items.forEach((item) => {
|
||||
shrink.forEach((s) => {
|
||||
if (item.city?.startsWith(s)) {
|
||||
item.city = s
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
setTotalItems(result.totalCount || 0)
|
||||
setData(result.data.items)
|
||||
setTotal(result.data.total)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('获取网关配置失败:', error)
|
||||
setError(error instanceof Error ? error.message : '获取网关配置失败')
|
||||
setData([])
|
||||
setTotalItems(0)
|
||||
toast.error((error as Error).message || '获取网关配置失败')
|
||||
}
|
||||
finally {
|
||||
setLoading(false)
|
||||
@@ -85,28 +62,26 @@ function GatewayConfigContent() {
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setCurrentPage(1) // 重置到第一页
|
||||
fetchData(macAddress, 1, itemsPerPage)
|
||||
setPage(1) // 重置到第一页
|
||||
fetchData(macAddress, 1)
|
||||
}
|
||||
|
||||
// 处理页码变化
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page)
|
||||
fetchData(macAddress, page, itemsPerPage)
|
||||
setPage(page)
|
||||
fetchData(macAddress, page)
|
||||
}
|
||||
|
||||
// 处理每页显示数量变化
|
||||
const handleSizeChange = (size: number) => {
|
||||
setItemsPerPage(size)
|
||||
setCurrentPage(1)
|
||||
fetchData(macAddress, 1, size)
|
||||
setPage(1)
|
||||
fetchData(macAddress, 1)
|
||||
}
|
||||
|
||||
const getStatusBadge = (value: number, trueText: string = '是', falseText: string = '否') => {
|
||||
// 0是正常1是更新,正常(绿)+ 更新(红)
|
||||
return (
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
value === 0 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${value === 0 ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
||||
{value === 0 ? trueText : falseText}
|
||||
</span>
|
||||
)
|
||||
@@ -139,7 +114,7 @@ function GatewayConfigContent() {
|
||||
value={macAddress}
|
||||
onChange={e => setMacAddress(e.target.value)}
|
||||
placeholder="输入MAC地址查询"
|
||||
className="px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
className="px-4 py-2 h-10 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
@@ -148,17 +123,6 @@ function GatewayConfigContent() {
|
||||
查询
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-md">
|
||||
<div className="flex items-center text-red-800">
|
||||
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
|
||||
{loading ? (
|
||||
@@ -173,11 +137,11 @@ function GatewayConfigContent() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-gray-50">
|
||||
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">城市</TableHead>
|
||||
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">MAC地址</TableHead>
|
||||
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">IP地址</TableHead>
|
||||
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">线路</TableHead>
|
||||
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">端口</TableHead>
|
||||
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">线路</TableHead>
|
||||
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">城市</TableHead>
|
||||
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">节点MAC</TableHead>
|
||||
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">节点IP</TableHead>
|
||||
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">配置更新</TableHead>
|
||||
<TableHead className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">在用状态</TableHead>
|
||||
</TableRow>
|
||||
@@ -185,21 +149,11 @@ function GatewayConfigContent() {
|
||||
<TableBody>
|
||||
{data.map((item, index) => (
|
||||
<TableRow key={index} className={index % 2 === 0 ? 'bg-white' : 'bg-gray-50'}>
|
||||
<TableCell>
|
||||
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded-full text-xs">
|
||||
{item.city}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="font-mono text-sm text-blue-600 font-medium">{item.edge}</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="font-mono text-sm text-green-600">{item.public}</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-gray-900">{item.user}</TableCell>
|
||||
<TableCell>
|
||||
<div className="font-mono text-sm text-purple-600">{item.inner_ip}</div>
|
||||
</TableCell>
|
||||
<TableCell>{item.inner_ip}</TableCell>
|
||||
<TableCell>{item.user}</TableCell>
|
||||
<TableCell>{item.city}</TableCell>
|
||||
<TableCell>{item.edge}</TableCell>
|
||||
<TableCell>{item.public}</TableCell>
|
||||
<TableCell>
|
||||
{getStatusBadge(item.ischange, '正常', '更新')}
|
||||
</TableCell>
|
||||
@@ -213,7 +167,7 @@ function GatewayConfigContent() {
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-4 mb-6">
|
||||
<div className="bg-blue-50 p-4 rounded-lg border border-blue-100">
|
||||
<div className="text-2xl font-bold text-blue-600">{totalItems}</div>
|
||||
<div className="text-2xl font-bold text-blue-600">{total}</div>
|
||||
<div className="text-sm text-blue-800">配置条数</div>
|
||||
</div>
|
||||
<div className="bg-green-50 p-4 rounded-lg border border-green-100">
|
||||
@@ -231,12 +185,13 @@ function GatewayConfigContent() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 分页组件 - 仅在非MAC查询时显示 */}
|
||||
{!isMacQuery && (
|
||||
{/* 分页组件 */}
|
||||
{!macAddress && (
|
||||
<Pagination
|
||||
page={currentPage}
|
||||
size={itemsPerPage}
|
||||
total={totalItems}
|
||||
total={total}
|
||||
page={page}
|
||||
size={250}
|
||||
sizeOptions={[250]}
|
||||
onPageChange={handlePageChange}
|
||||
onSizeChange={handleSizeChange}
|
||||
className="mt-4"
|
||||
|
||||
16
src/lib/api.ts
Normal file
16
src/lib/api.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export type Res<T> = {
|
||||
success: true
|
||||
data: T
|
||||
} | {
|
||||
success: false
|
||||
error: string
|
||||
}
|
||||
|
||||
export type Page<T> = {
|
||||
total: number
|
||||
page: number
|
||||
size: number
|
||||
items: T[]
|
||||
}
|
||||
|
||||
export type ResData<T extends (...args: never) => unknown> = Awaited<ReturnType<T>> extends Res<infer D> ? D : never
|
||||
21
src/lib/drizzle/index.ts
Normal file
21
src/lib/drizzle/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'dotenv/config'
|
||||
import { drizzle as client } from 'drizzle-orm/mysql2'
|
||||
import * as schema from './schema'
|
||||
|
||||
declare global {
|
||||
var drizzle: ReturnType<typeof client<typeof schema>> | undefined
|
||||
}
|
||||
|
||||
const { DATABASE_URL } = process.env
|
||||
if (!DATABASE_URL) {
|
||||
throw new Error('DATABASE_URL is not set')
|
||||
}
|
||||
|
||||
const drizzle = global.drizzle || client(DATABASE_URL, { mode: 'default', schema })
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
global.drizzle = drizzle
|
||||
}
|
||||
|
||||
export default drizzle
|
||||
export * from './schema'
|
||||
export * from 'drizzle-orm'
|
||||
95
src/lib/drizzle/schema.ts
Normal file
95
src/lib/drizzle/schema.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { mysqlTable, int, timestamp, varchar, datetime, text, tinyint } from 'drizzle-orm/mysql-core'
|
||||
|
||||
export const change = mysqlTable('change', {
|
||||
id: int().autoincrement().notNull().primaryKey(),
|
||||
time: timestamp({ mode: 'date' }),
|
||||
city: int(),
|
||||
macaddr: varchar({ length: 20 }).notNull(),
|
||||
edgeNew: varchar('edge_new', { length: 20 }).notNull(),
|
||||
edgeOld: varchar('edge_old', { length: 20 }),
|
||||
info: varchar({ length: 500 }).notNull(),
|
||||
network: varchar({ length: 20 }).notNull(),
|
||||
createtime: datetime({ mode: 'date' }).default(new Date()).notNull(),
|
||||
})
|
||||
|
||||
export const cityhash = mysqlTable('cityhash', {
|
||||
id: int().autoincrement().notNull().primaryKey(),
|
||||
index: int(),
|
||||
macaddr: varchar({ length: 20 }),
|
||||
city: varchar({ length: 20 }).notNull(),
|
||||
num: int().notNull(),
|
||||
hash: varchar({ length: 100 }).notNull(),
|
||||
label: varchar({ length: 20 }),
|
||||
count: int().default(0).notNull(),
|
||||
offset: int().default(0).notNull(),
|
||||
createtime: datetime({ mode: 'date' }).default(new Date()).notNull(),
|
||||
updatetime: datetime({ mode: 'date' }).default(new Date()).notNull(),
|
||||
})
|
||||
|
||||
export const edge = mysqlTable('edge', {
|
||||
id: int().autoincrement().notNull().primaryKey(),
|
||||
macaddr: varchar({ length: 17 }).notNull(),
|
||||
public: varchar({ length: 255 }).notNull(),
|
||||
isp: varchar({ length: 255 }).notNull(),
|
||||
single: tinyint().notNull(),
|
||||
sole: tinyint().notNull(),
|
||||
arch: tinyint().notNull(),
|
||||
online: int().default(0).notNull(),
|
||||
cityId: int('city_id').notNull(),
|
||||
active: tinyint().notNull(),
|
||||
})
|
||||
|
||||
export const gateway = mysqlTable('gateway', {
|
||||
id: int().autoincrement().notNull().primaryKey(),
|
||||
macaddr: varchar({ length: 20 }).notNull(),
|
||||
table: int().notNull(),
|
||||
edge: varchar({ length: 20 }).notNull(),
|
||||
network: varchar({ length: 20 }).notNull(),
|
||||
cityhash: varchar({ length: 100 }).notNull(),
|
||||
label: varchar({ length: 20 }),
|
||||
user: varchar({ length: 20 }).notNull(),
|
||||
innerIp: varchar('inner_ip', { length: 20 }).notNull(),
|
||||
ischange: tinyint().default(0).notNull(),
|
||||
isonline: tinyint().default(0).notNull(),
|
||||
onlinenum: int().default(0).notNull(),
|
||||
createtime: datetime({ mode: 'date' }).default(new Date()).notNull(),
|
||||
updatetime: datetime({ mode: 'date' }).default(new Date()).notNull(),
|
||||
})
|
||||
|
||||
export const sessions = mysqlTable('sessions', {
|
||||
id: varchar({ length: 191 }).notNull().primaryKey(),
|
||||
expires: datetime({ mode: 'date' }).notNull(),
|
||||
userid: int().notNull().references(() => users.id, { onDelete: 'cascade', onUpdate: 'cascade' }),
|
||||
createdat: datetime({ mode: 'date' }).default(new Date()).notNull(),
|
||||
})
|
||||
|
||||
export const submit = mysqlTable('submit', {
|
||||
id: int().autoincrement().notNull().primaryKey(),
|
||||
time: datetime({ mode: 'date' }).notNull(),
|
||||
gateway: varchar({ length: 20 }).notNull(),
|
||||
config: text(),
|
||||
})
|
||||
|
||||
export const token = mysqlTable('token', {
|
||||
id: int().autoincrement().notNull().primaryKey(),
|
||||
setid: int().default(1).notNull(),
|
||||
changeCount: int('change_count').notNull(),
|
||||
limitCount: int('limit_count').default(32000).notNull(),
|
||||
token: varchar({ length: 1000 }).notNull(),
|
||||
macaddr: varchar({ length: 100 }).notNull(),
|
||||
tokenTime: datetime('token_time', { mode: 'date' }).notNull(),
|
||||
innerIp: varchar('inner_ip', { length: 20 }).notNull(),
|
||||
l2Ip: varchar({ length: 20 }),
|
||||
enable: tinyint().default(1).notNull(),
|
||||
createtime: datetime({ mode: 'date' }).default(new Date()).notNull(),
|
||||
updatetime: datetime({ mode: 'date' }).default(new Date()).notNull(),
|
||||
})
|
||||
|
||||
export const users = mysqlTable('users', {
|
||||
id: int().autoincrement().notNull().primaryKey(),
|
||||
name: varchar({ length: 191 }),
|
||||
password: varchar({ length: 191 }).notNull(),
|
||||
account: varchar({ length: 191 }).notNull(),
|
||||
createdat: datetime({ mode: 'date' }).default(new Date()).notNull(),
|
||||
updatedat: datetime({ mode: 'date' }).default(new Date()).notNull(),
|
||||
})
|
||||
@@ -1,15 +0,0 @@
|
||||
import 'server-only'
|
||||
import { PrismaClient } from '@/generated/prisma/client'
|
||||
|
||||
const globalForPrisma = global as unknown as {
|
||||
prisma: PrismaClient | undefined
|
||||
}
|
||||
|
||||
const prisma = globalForPrisma.prisma ?? new PrismaClient()
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
globalForPrisma.prisma = prisma
|
||||
}
|
||||
|
||||
export default prisma
|
||||
export * from '@/generated/prisma/client'
|
||||
@@ -6,12 +6,11 @@ export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
// 数据验证函数
|
||||
export const validateNumber = (value: unknown): number => {
|
||||
if (typeof value === 'number') return value
|
||||
if (typeof value === 'string') {
|
||||
const num = parseInt(value)
|
||||
return isNaN(num) ? 0 : num
|
||||
}
|
||||
return 0
|
||||
// 取数组第一个元素,支持映射
|
||||
export function first<T>(array: T[]): T | undefined
|
||||
export function first<T, I>(array: T[], select: (item: T) => I): I | undefined
|
||||
|
||||
export function first<T, I>(array: T[], select?: (item: T) => I) {
|
||||
const item = array.length > 0 ? array[0] : undefined
|
||||
return select && item ? select(item) : item
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user