迁移 orm 依赖到 drizzle

This commit is contained in:
2025-09-27 10:37:27 +08:00
parent 7fa2fe67ca
commit 72ea29f435
19 changed files with 538 additions and 523 deletions

View File

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

View File

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

View File

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

View File

@@ -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: '用户删除成功',

View File

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

View File

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

View File

@@ -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
View 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
View 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
View 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(),
})

View File

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

View File

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