添加管理端和移动端的多个新功能模块,包括文件上传、在线地图、用户认证、系统设置等,优化代码结构,提升可维护性和用户体验。

This commit is contained in:
zyh
2025-04-10 02:25:25 +00:00
parent 01dea6efa1
commit b1a6b608c6
31 changed files with 1366 additions and 46 deletions

221
server/README.md Normal file
View File

@@ -0,0 +1,221 @@
# 管理端与移动端启动模板 (Admin-Mobile Starter)
## 项目概述
这是一个基于 Deno 和 Hono 框架开发的管理系统与移动端应用启动模板,提供了完整的用户认证、权限管理、系统设置、文件上传、地图组件、图表组件等功能,可以快速构建企业级应用。
## 技术栈
- **后端框架**Deno + Hono
- **前端框架**React 19 + Ant Design 5
- **状态管理**TanStack Query
- **认证系统**@d8d-appcontainer/auth
- **API客户端**@d8d-appcontainer/api
- **地图组件**:高德地图(支持在线/离线模式)
- **图表组件**Ant Design Charts
- **日期处理**Day.js
- **网络请求**Axios
## 功能模块
- **用户认证与管理** - 登录、注册、用户信息管理
- **系统设置** - 站点信息、主题配置、全局参数设置
- **文件管理** - 文件上传、分类管理
- **地图组件** - 在线/离线地图、位置标记、地图交互
- **图表组件** - 数据可视化图表
- **移动端适配** - 响应式设计,支持移动端访问
## 目录结构
- `asset/` - 前端资源文件
- `admin/` - 管理端资源
- `mobile/` - 移动端资源
- `share/` - 共享资源和类型定义
- `routes_*.ts` - 各模块路由定义文件
- `app.tsx` - 应用主入口
- `migrations.ts` - 数据库迁移
- `deno.json` - Deno配置文件
## 在D8D(多八多)平台运行
本应用在 [D8D(多八多)开发者平台](https://www.d8d.fun) 上可以直接运行,无需复杂部署:
1. 访问 [www.d8d.fun](https://www.d8d.fun) 网站并注册账号
2. 登录后进入开发者控制台
3. 点击"创建应用"按钮创建新应用
4. 选择"管理端与移动端启动模板"作为应用模板
5. 配置应用基本信息(名称、描述等)
6. 完成创建后,直接点击"预览"按钮即可运行应用,无需额外部署步骤
7. 系统会自动初始化并启动应用,可直接在浏览器中访问和使用
### D8D(多八多)平台专属配置
在D8D(多八多)平台运行时,可以通过平台的"应用配置"面板设置以下内容:
- 应用资源限制CPU、内存等
- 公网访问设置
- 域名绑定
- 自动备份
- 日志记录级别
## 本地开发指南
### 环境要求
- Deno 2.2.8 或更高版本
- 数据库(由 @d8d-appcontainer/api 支持的数据库)
### 环境变量配置
在启动应用前,可配置以下环境变量:
```
# 应用配置
APP_NAME=应用名称
ENV=development
JWT_SECRET=your-jwt-secret-key
# OSS配置
OSS_TYPE=aliyun # 可选值: aliyun, minio
OSS_BASE_URL=https://your-oss-url.com
# 地图配置
MAP_MODE=online # 可选值: online, offline
AMAP_KEY=您的地图API密钥
# API客户端配置
SERVER_URL=https://app-server.d8d.fun
WORKSPACE_KEY=您的工作空间密钥 # 在多八多(www.d8d.fun)平台注册开通工作空间后获取
```
### 本地启动应用
要在本地运行此应用,需要创建一个启动文件:
1. 创建一个名为`run_app.ts`的新文件(文件名可自定义)
2. 将下面的代码复制到该文件中:
```typescript
// 导入所需模块
import { Hono } from 'hono'
import { APIClient } from '@d8d-appcontainer/api'
import debug from "debug"
import { cors } from 'hono/cors'
// 初始化debug实例
const log = {
app: debug('app:server'),
auth: debug('auth:server'),
api: debug('api:server'),
debug: debug('debug:server')
}
// 启用所有日志
Object.values(log).forEach(logger => logger.enabled = true)
// 初始化 API Client
const getApiClient = async (workspaceKey: string, serverUrl?: string) => {
try {
log.api('正在初始化API Client实例')
const apiClient = await APIClient.getInstance({
scope: 'user',
config: {
serverUrl: serverUrl || Deno.env.get('SERVER_URL') || 'https://app-server.d8d.fun',
workspaceKey: workspaceKey,
type: 'http',
}
})
log.api('API Client初始化成功')
return apiClient
} catch (error) {
log.api('API Client初始化失败:', error)
throw error
}
}
// 创建Hono应用实例
const app = new Hono()
// 注册CORS中间件
app.use('/*', cors())
// 动态加载并运行模板
const runTemplate = async () => {
try {
// 创建基础app实例
const moduleApp = new Hono()
// 初始化API Client
// 注意WORKSPACE_KEY 需要在 多八多(www.d8d.fun) 平台注册并开通工作空间后获取
const workspaceKey = Deno.env.get('WORKSPACE_KEY') || ''
if (!workspaceKey) {
console.warn('未设置WORKSPACE_KEY请前往 多八多(www.d8d.fun) 注册并开通工作空间以获取密钥')
}
const apiClient = await getApiClient(workspaceKey)
// 导入模板主模块
const templateModule = await import('./app.tsx')
if (templateModule.default) {
// 传入必要参数并初始化应用
const appInstance = templateModule.default({
apiClient: apiClient,
app: moduleApp,
moduleDir: './admin-mobile-starter'
})
// 启动服务器
Deno.serve({ port: 8000 }, appInstance.fetch)
console.log('应用已启动,监听端口: 8000')
}
} catch (error) {
console.error('模板加载失败:', error)
}
}
// 执行模板
runTemplate()
```
3. 运行该文件:
```bash
deno run -A run_app.ts
```
> **注意**上述代码用于本地运行app.tsx。SERVER_URL默认值为`app-server.d8d.fun`WORKSPACE_KEY需要在 [多八多(www.d8d.fun)](https://www.d8d.fun) 平台注册并开通工作空间后获取。
## 系统配置说明
系统配置可通过环境变量或数据库中的系统设置进行管理,支持以下配置项:
- 站点名称、图标、Logo
- 主题设置(明/暗模式)
- 地图模式(在线/离线)
- 图表主题
- API 基础路径
- 文件存储方式
## 数据库迁移
系统首次启动时会自动执行数据库迁移,创建必要的表结构和初始数据。
## 开发者扩展指南
### 添加新路由
`routes_*.ts` 文件中定义新的路由处理函数,然后在 `app.tsx` 中引入并注册。
### 前端开发
前端资源位于 `asset/` 目录下,区分为管理端和移动端,可根据需要进行修改和扩展。
## 许可证
[License] - 请参阅LICENSE文件了解详情
---
© 2025 多八多(D8D). 保留所有权利。

471
server/app.tsx Normal file
View File

@@ -0,0 +1,471 @@
/** @jsxImportSource https://esm.d8d.fun/hono@4.7.4/jsx */
import { Hono } from 'hono'
import { Auth } from '@d8d-appcontainer/auth'
import type { User as AuthUser } from '@d8d-appcontainer/auth'
import React from 'hono/jsx'
import type { FC } from 'hono/jsx'
import { cors } from 'hono/cors'
import type { Context as HonoContext } from 'hono'
import { serveStatic } from 'hono/deno'
import { APIClient } from '@d8d-appcontainer/api'
import debug from "debug"
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc';
import type { SystemSettingRecord, GlobalConfig } from '../client/share/types.ts';
import { SystemSettingKey, OssType, MapMode } from '../client/share/types.ts';
import {
createKnowInfoRoutes,
createFileCategoryRoutes,
createFileUploadRoutes,
createThemeRoutes,
createSystemSettingsRoutes,
} from "./routes_sys.ts";
import {
createMapRoutes,
} from "./routes_maps.ts";
import {
createChartRoutes,
} from "./routes_charts.ts";
import { migrations } from './migrations.ts';
// 导入基础路由
import { createAuthRoutes } from "./routes_auth.ts";
import { createUserRoutes } from "./routes_users.ts";
dayjs.extend(utc)
// 初始化debug实例
const log = {
app: debug('app:server'),
auth: debug('auth:server'),
api: debug('api:server'),
debug: debug('debug:server')
}
const GLOBAL_CONFIG: GlobalConfig = {
OSS_BASE_URL: Deno.env.get('OSS_BASE_URL') || 'https://d8d-appcontainer-user.oss-cn-beijing.aliyuncs.com',
OSS_TYPE: Deno.env.get('OSS_TYPE') === OssType.MINIO ? OssType.MINIO : OssType.ALIYUN,
API_BASE_URL: '/api',
APP_NAME: Deno.env.get('APP_NAME') || '应用Starter',
ENV: Deno.env.get('ENV') || 'development',
DEFAULT_THEME: 'light', // 默认主题
MAP_CONFIG: {
KEY: Deno.env.get('AMAP_KEY') || '您的地图API密钥',
VERSION: '2.0',
PLUGINS: ['AMap.ToolBar', 'AMap.Scale', 'AMap.HawkEye', 'AMap.MapType', 'AMap.Geolocation'],
MAP_MODE: Deno.env.get('MAP_MODE') === MapMode.OFFLINE ? MapMode.OFFLINE : MapMode.ONLINE,
},
CHART_THEME: 'default', // 图表主题
ENABLE_THEME_CONFIG: false, // 主题配置开关
THEME: null
};
log.app.enabled = true
log.auth.enabled = true
log.api.enabled = true
log.debug.enabled = true
// 定义自定义上下文类型
export interface Variables {
auth: Auth
user?: AuthUser
apiClient: APIClient
moduleDir: string
systemSettings?: SystemSettingRecord
}
// 定义登录历史类型
interface LoginHistory {
id: number
user_id: number
login_time: string
ip_address?: string
user_agent?: string
}
// 定义仪表盘数据类型
interface DashboardData {
lastLogin: string
loginCount: number
fileCount: number
userCount: number
systemInfo: {
version: string
lastUpdate: string
}
}
interface EsmScriptConfig {
src: string
href: string
denoJson: string
refresh: boolean
prodPath?: string
prodSrc?: string
}
// Auth实例
let authInstance: Auth | null = null
// 初始化Auth实例
const initAuth = async (apiClient: APIClient) => {
try {
if (authInstance) {
return authInstance
}
log.auth('正在初始化Auth实例')
authInstance = new Auth(apiClient as any, {
jwtSecret: Deno.env.get("JWT_SECRET") || 'your-jwt-secret-key',
initialUsers: [],
storagePrefix: '',
userTable: 'users',
fieldNames: {
id: 'id',
username: 'username',
password: 'password',
phone: 'phone',
email: 'email',
is_disabled: 'is_disabled',
is_deleted: 'is_deleted'
},
tokenExpiry: 24 * 60 * 60,
refreshTokenExpiry: 7 * 24 * 60 * 60
})
log.auth('Auth实例初始化完成')
return authInstance
} catch (error) {
log.auth('Auth初始化失败:', error)
throw error
}
}
// 初始化系统设置
const initSystemSettings = async (apiClient: APIClient) => {
try {
const systemSettings = await apiClient.database.table('system_settings')
.select()
// 将系统设置转换为键值对形式
const settings = systemSettings.reduce((acc: Record<string, any>, setting: any) => {
acc[setting.key] = setting.value
return acc
}, {}) as SystemSettingRecord
// 更新全局配置
if (settings[SystemSettingKey.SITE_NAME]) {
GLOBAL_CONFIG.APP_NAME = String(settings[SystemSettingKey.SITE_NAME])
}
// 设置其他全局配置项
if (settings[SystemSettingKey.SITE_FAVICON]) {
GLOBAL_CONFIG.DEFAULT_THEME = String(settings[SystemSettingKey.SITE_FAVICON])
}
if (settings[SystemSettingKey.SITE_LOGO]) {
GLOBAL_CONFIG.MAP_CONFIG.KEY = String(settings[SystemSettingKey.SITE_LOGO])
}
if (settings[SystemSettingKey.SITE_DESCRIPTION]) {
GLOBAL_CONFIG.CHART_THEME = String(settings[SystemSettingKey.SITE_DESCRIPTION])
}
// 设置主题配置开关
if (settings[SystemSettingKey.ENABLE_THEME_CONFIG]) {
GLOBAL_CONFIG.ENABLE_THEME_CONFIG = settings[SystemSettingKey.ENABLE_THEME_CONFIG] === 'true'
}
// 查询ID1管理员的主题配置
const adminTheme = await apiClient.database.table('theme_settings')
.where('user_id', 1)
.first()
if (adminTheme) {
GLOBAL_CONFIG.THEME = adminTheme.settings
}
return settings
} catch (error) {
log.app('获取系统设置失败:', error)
return {} as SystemSettingRecord
}
}
// 初始化数据库
const initDatabase = async (apiClient: APIClient) => {
try {
log.app('正在执行数据库迁移...')
const migrationsResult = await apiClient.database.executeLiveMigrations(migrations)
// log.app('数据库迁移完成 %O',migrationsResult)
log.app('数据库迁移完成')
} catch (error) {
log.app('数据库迁移失败:', error)
}
}
// 中间件:数据库初始化
const withDatabase = async (c: HonoContext<{ Variables: Variables }>, next: () => Promise<void>) => {
try {
const apiClient = c.get('apiClient')
await initDatabase(apiClient)
await next()
} catch (error) {
log.api('数据库操作失败:', error)
return c.json({ error: '数据库操作失败' }, 500)
}
}
// 中间件:验证认证
const withAuth = async (c: HonoContext<{ Variables: Variables }>, next: () => Promise<void>) => {
try {
const auth = c.get('auth')
const token = c.req.header('Authorization')?.replace('Bearer ', '')
if (token) {
const userData = await auth.verifyToken(token)
if (userData) {
c.set('user', userData)
await next()
return
}
}
return c.json({ error: '未授权' }, 401)
} catch (error) {
log.auth('认证失败:', error)
return c.json({ error: '无效凭证' }, 401)
}
}
// 导出withAuth类型定义
export type WithAuth = typeof withAuth;
// 定义模块参数接口
interface ModuleParams {
apiClient: APIClient
app: Hono<{ Variables: Variables }>
moduleDir: string
}
export default function({ apiClient, app, moduleDir }: ModuleParams) {
const honoApp = app
// 添加CORS中间件
honoApp.use('/*', cors())
// 创建API路由
const api = new Hono<{ Variables: Variables }>()
// 设置环境变量
api.use('*', async (c, next) => {
c.set('apiClient', apiClient)
c.set('moduleDir', moduleDir)
c.set('auth', await initAuth(apiClient))
c.set('systemSettings', await initSystemSettings(apiClient))
await next()
})
// 使用数据库中间件
api.use('/', withDatabase)
// 查询仪表盘数据
api.get('/dashboard', withAuth, async (c) => {
try {
const user = c.get('user')!
const apiClient = c.get('apiClient')
const lastLogin = await apiClient.database.table('login_history')
.where('user_id', user.id)
.orderBy('login_time', 'desc')
.limit(1)
.first()
// 获取登录总次数
const loginCount = await apiClient.database.table('login_history')
.where('user_id', user.id)
.count()
// 获取系统数据统计
const fileCount = await apiClient.database.table('file_library')
.where('is_deleted', 0)
.count()
const userCount = await apiClient.database.table('users')
.where('is_deleted', 0)
.count()
// 返回仪表盘数据
const dashboardData: DashboardData = {
lastLogin: lastLogin ? lastLogin.login_time : new Date().toISOString(),
loginCount: loginCount,
fileCount: Number(fileCount),
userCount: Number(userCount),
systemInfo: {
version: '1.0.0',
lastUpdate: new Date().toISOString()
}
}
return c.json(dashboardData)
} catch (error) {
log.api('获取仪表盘数据失败:', error)
return c.json({ error: '获取仪表盘数据失败' }, 500)
}
})
// 注册基础路由
api.route('/auth', createAuthRoutes(withAuth))
api.route('/users', createUserRoutes(withAuth))
api.route('/know-info', createKnowInfoRoutes(withAuth))
api.route('/upload', createFileUploadRoutes(withAuth)) // 添加文件上传路由
api.route('/file-categories', createFileCategoryRoutes(withAuth)) // 添加文件分类管理路由
api.route('/theme', createThemeRoutes(withAuth)) // 添加主题设置路由
api.route('/charts', createChartRoutes(withAuth)) // 添加图表数据路由
api.route('/map', createMapRoutes(withAuth)) // 添加地图数据路由
api.route('/settings', createSystemSettingsRoutes(withAuth)) // 添加系统设置路由
// 注册API路由
honoApp.route('/api', api)
// 首页路由 - SSR
honoApp.get('/', async (c: HonoContext) => {
const systemName = GLOBAL_CONFIG.APP_NAME
return c.html(
<html>
<head>
<title>{systemName}</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div className="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
{/* 系统介绍区域 */}
<div className="text-center">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
{systemName}
</h1>
<p className="text-lg text-gray-600 mb-8">
Starter
</p>
<p className="text-base text-gray-500 mb-8">
Hono和React的应用Starter
</p>
</div>
{/* 管理入口按钮 */}
<div>
<a
href="/admin"
className="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-lg font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
</a>
</div>
</div>
</div>
</body>
</html>
)
})
// 创建一个函数用于生成包含全局配置的HTML页面
const createHtmlWithConfig = (scriptConfig: EsmScriptConfig, title = '应用Starter') => {
return (c: HonoContext) => {
const isProd = GLOBAL_CONFIG.ENV === 'production';
return c.html(
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
{isProd ? (
<script type="module" src={scriptConfig.prodSrc || `/client_dist/${scriptConfig.prodPath}`}></script>
) : (
<script src={scriptConfig.src} href={scriptConfig.href} deno-json={scriptConfig.denoJson} refresh={scriptConfig.refresh}></script>
)}
{isProd ? (<script src="/tailwindcss@3.4.16/index.js"></script>) : (<script src="https://cdn.tailwindcss.com"></script>)}
<script dangerouslySetInnerHTML={{ __html: `window.CONFIG = ${JSON.stringify(GLOBAL_CONFIG)};` }} />
{!isProd && (
<>
<script src="https://ai-oss.d8d.fun/umd/vconsole.3.15.1.min.js"></script>
<script dangerouslySetInnerHTML={{ __html: `
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('vconsole')) {
var vConsole = new VConsole({
theme: urlParams.get('vconsole_theme') || 'light',
onReady: function() {
console.log('vConsole is ready');
}
});
}
`}} />
</>
)}
</head>
<body className="bg-gray-50">
<div id="root"></div>
</body>
</html>
)
}
}
// 后台管理路由
honoApp.get('/admin', createHtmlWithConfig({
src: "https://esm.d8d.fun/xb",
href: "/client/admin/web_app.tsx",
denoJson: "/client/admin/deno.json",
refresh: true,
prodPath: "admin/web_app.js"
}, GLOBAL_CONFIG.APP_NAME))
honoApp.get('/admin/*', createHtmlWithConfig({
src: "https://esm.d8d.fun/xb",
href: "/client/admin/web_app.tsx",
denoJson: "/client/admin/deno.json",
refresh: true,
prodPath: "admin/web_app.js"
}, GLOBAL_CONFIG.APP_NAME))
const staticRoutes = serveStatic({
root: moduleDir,
onFound: async (path: string, c: HonoContext) => {
const fileExt = path.split('.').pop()?.toLowerCase()
if (fileExt === 'tsx' || fileExt === 'ts') {
c.header('Content-Type', 'text/typescript; charset=utf-8')
} else if (fileExt === 'js' || fileExt === 'mjs') {
c.header('Content-Type', 'application/javascript; charset=utf-8')
} else if (fileExt === 'json') {
c.header('Content-Type', 'application/json; charset=utf-8')
} else if (fileExt === 'html') {
c.header('Content-Type', 'text/html; charset=utf-8')
} else if (fileExt === 'css') {
c.header('Content-Type', 'text/css; charset=utf-8')
} else if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(fileExt || '')) {
c.header('Content-Type', `image/${fileExt}`)
}
const fileInfo = await Deno.stat(path)
c.header('Last-Modified', fileInfo.mtime?.toUTCString() ?? new Date().toUTCString())
},
})
// 静态资源路由
honoApp.get('/client/*', staticRoutes)
honoApp.get('/amap/*', staticRoutes)
honoApp.get('/tailwindcss@3.4.16/*', staticRoutes)
honoApp.get('/client_dist/*', staticRoutes)
return honoApp
}

33
server/deno.json Normal file
View File

@@ -0,0 +1,33 @@
{
"imports": {
"hono": "https://esm.d8d.fun/hono@4.7.4",
"hono/jsx": "https://esm.d8d.fun/hono@4.7.4/jsx",
"hono/jsx/jsx-runtime": "https://esm.d8d.fun/hono@4.7.4/jsx/jsx-runtime",
"hono/cors": "https://esm.d8d.fun/hono@4.7.4/cors",
"hono/serve-static": "https://esm.d8d.fun/hono@4.7.4/serve-static",
"hono/deno": "https://esm.d8d.fun/hono@4.7.4/deno",
"@d8d-appcontainer/auth": "https://esm.d8d.fun/@d8d-appcontainer/auth@0.0.14",
"debug": "https://esm.d8d.fun/debug@4.4.0",
"dayjs/plugin/utc": "https://esm.d8d.fun/dayjs@1.11.13/plugin/utc",
"react": "https://esm.d8d.fun/react@19.0.0",
"react-dom": "https://esm.d8d.fun/react-dom@19.0.0",
"react-dom/client": "https://esm.d8d.fun/react-dom@19.0.0/client",
"react-router": "https://esm.d8d.fun/react-router@7.3.0?deps=react@19.0.0,react-dom@19.0.0",
"antd": "https://esm.d8d.fun/antd@5.24.5?standalone&deps=react@19.0.0,react-dom@19.0.0",
"antd/locale/zh_CN": "https://esm.d8d.fun/antd@5.24.5/locale/zh_CN?standalone&deps=react@19.0.0,react-dom@19.0.0",
"@ant-design/icons": "https://esm.d8d.fun/@ant-design/icons@5.6.1?standalone&deps=react@19.0.0,react-dom@19.0.0",
"@tanstack/react-query": "https://esm.d8d.fun/@tanstack/react-query@5.67.1?deps=react@19.0.0,react-dom@19.0.0",
"axios": "https://esm.d8d.fun/axios@1.6.2",
"dayjs": "https://esm.d8d.fun/dayjs@1.11.13",
"dayjs/locale/zh-cn": "https://esm.d8d.fun/dayjs@1.11.13/locale/zh-cn",
"dayjs/plugin/weekday": "https://esm.d8d.fun/dayjs@1.11.13/plugin/weekday",
"dayjs/plugin/localeData": "https://esm.d8d.fun/dayjs@1.11.13/plugin/localeData",
"@d8d-appcontainer/types": "https://esm.d8d.fun/@d8d-appcontainer/types@3.0.47",
"@d8d-appcontainer/api": "https://esm.d8d.fun/@d8d-appcontainer/api@3.0.47",
"@ant-design/plots": "https://esm.d8d.fun/@ant-design/plots@2.1.13?deps=react@19.0.0,react-dom@19.0.0",
"lodash": "https://esm.d8d.fun/lodash@4.17.21"
},
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext", "deno.ns"]
}
}

209
server/deno.lock generated Normal file
View File

@@ -0,0 +1,209 @@
{
"version": "4",
"redirects": {
"https://esm.d8d.fun/@deno/shim-deno-test@^0.5.0?target=denonext": "https://esm.d8d.fun/@deno/shim-deno-test@0.5.0?target=denonext",
"https://esm.d8d.fun/@deno/shim-deno@~0.18.0?target=denonext": "https://esm.d8d.fun/@deno/shim-deno@0.18.2?target=denonext",
"https://esm.d8d.fun/@socket.io/component-emitter@~3.1.0?target=denonext": "https://esm.d8d.fun/@socket.io/component-emitter@3.1.2?target=denonext",
"https://esm.d8d.fun/asynckit@^0.4.0?target=denonext": "https://esm.d8d.fun/asynckit@0.4.0?target=denonext",
"https://esm.d8d.fun/axios@^1.7.2?target=denonext": "https://esm.d8d.fun/axios@1.8.4?target=denonext",
"https://esm.d8d.fun/bufferutil@^4.0.1?target=denonext": "https://esm.d8d.fun/bufferutil@4.0.9?target=denonext",
"https://esm.d8d.fun/combined-stream@^1.0.8?target=denonext": "https://esm.d8d.fun/combined-stream@1.0.8?target=denonext",
"https://esm.d8d.fun/debug?target=denonext": "https://esm.d8d.fun/debug@4.4.0?target=denonext",
"https://esm.d8d.fun/delayed-stream@~1.0.0?target=denonext": "https://esm.d8d.fun/delayed-stream@1.0.0?target=denonext",
"https://esm.d8d.fun/engine.io-client@~6.6.1?target=denonext": "https://esm.d8d.fun/engine.io-client@6.6.3?target=denonext",
"https://esm.d8d.fun/engine.io-parser@~5.2.1?target=denonext": "https://esm.d8d.fun/engine.io-parser@5.2.3?target=denonext",
"https://esm.d8d.fun/follow-redirects@^1.15.6?target=denonext": "https://esm.d8d.fun/follow-redirects@1.15.9?target=denonext",
"https://esm.d8d.fun/form-data@^4.0.0?target=denonext": "https://esm.d8d.fun/form-data@4.0.2?target=denonext",
"https://esm.d8d.fun/isexe@^3.1.1?target=denonext": "https://esm.d8d.fun/isexe@3.1.1?target=denonext",
"https://esm.d8d.fun/jsonwebtoken@^9.0.2?target=denonext": "https://esm.d8d.fun/jsonwebtoken@9.0.2?target=denonext",
"https://esm.d8d.fun/jwa@^1.4.1?target=denonext": "https://esm.d8d.fun/jwa@1.4.1?target=denonext",
"https://esm.d8d.fun/jws@^3.2.2?target=denonext": "https://esm.d8d.fun/jws@3.2.2?target=denonext",
"https://esm.d8d.fun/lodash.includes@^4.3.0?target=denonext": "https://esm.d8d.fun/lodash.includes@4.3.0?target=denonext",
"https://esm.d8d.fun/lodash.isboolean@^3.0.3?target=denonext": "https://esm.d8d.fun/lodash.isboolean@3.0.3?target=denonext",
"https://esm.d8d.fun/lodash.isinteger@^4.0.4?target=denonext": "https://esm.d8d.fun/lodash.isinteger@4.0.4?target=denonext",
"https://esm.d8d.fun/lodash.isnumber@^3.0.3?target=denonext": "https://esm.d8d.fun/lodash.isnumber@3.0.3?target=denonext",
"https://esm.d8d.fun/lodash.isplainobject@^4.0.6?target=denonext": "https://esm.d8d.fun/lodash.isplainobject@4.0.6?target=denonext",
"https://esm.d8d.fun/lodash.isstring@^4.0.1?target=denonext": "https://esm.d8d.fun/lodash.isstring@4.0.1?target=denonext",
"https://esm.d8d.fun/lodash.once@^4.0.0?target=denonext": "https://esm.d8d.fun/lodash.once@4.1.1?target=denonext",
"https://esm.d8d.fun/mime-types@^2.1.12?target=denonext": "https://esm.d8d.fun/mime-types@2.1.35?target=denonext",
"https://esm.d8d.fun/ms@^2.1.1?target=denonext": "https://esm.d8d.fun/ms@2.1.3?target=denonext",
"https://esm.d8d.fun/ms@^2.1.3?target=denonext": "https://esm.d8d.fun/ms@2.1.3?target=denonext",
"https://esm.d8d.fun/nanoid@^5.1.2?target=denonext": "https://esm.d8d.fun/nanoid@5.1.5?target=denonext",
"https://esm.d8d.fun/node-gyp-build@^4.3.0?target=denonext": "https://esm.d8d.fun/node-gyp-build@4.8.4?target=denonext",
"https://esm.d8d.fun/proxy-from-env@^1.1.0?target=denonext": "https://esm.d8d.fun/proxy-from-env@1.1.0?target=denonext",
"https://esm.d8d.fun/safe-buffer@^5.0.1?target=denonext": "https://esm.d8d.fun/safe-buffer@5.2.1?target=denonext",
"https://esm.d8d.fun/semver@^7.5.4?target=denonext": "https://esm.d8d.fun/semver@7.7.1?target=denonext",
"https://esm.d8d.fun/socket.io-client@^4.7.2?target=denonext": "https://esm.d8d.fun/socket.io-client@4.8.1?target=denonext",
"https://esm.d8d.fun/socket.io-parser@~4.2.4?target=denonext": "https://esm.d8d.fun/socket.io-parser@4.2.4?target=denonext",
"https://esm.d8d.fun/supports-color?target=denonext": "https://esm.d8d.fun/supports-color@10.0.0?target=denonext",
"https://esm.d8d.fun/utf-8-validate@%3E=5.0.2?target=denonext": "https://esm.d8d.fun/utf-8-validate@6.0.5?target=denonext",
"https://esm.d8d.fun/which@^4.0.0?target=denonext": "https://esm.d8d.fun/which@4.0.0?target=denonext",
"https://esm.d8d.fun/ws@~8.17.1?target=denonext": "https://esm.d8d.fun/ws@8.17.1?target=denonext",
"https://esm.d8d.fun/xmlhttprequest-ssl@~2.1.1?target=denonext": "https://esm.d8d.fun/xmlhttprequest-ssl@2.1.2?target=denonext"
},
"remote": {
"https://deno.land/std@0.150.0/media_types/_util.ts": "ce9b4fc4ba1c447dafab619055e20fd88236ca6bdd7834a21f98bd193c3fbfa1",
"https://deno.land/std@0.150.0/media_types/mod.ts": "2d4b6f32a087029272dc59e0a55ae3cc4d1b27b794ccf528e94b1925795b3118",
"https://deno.land/std@0.150.0/media_types/vendor/mime-db.v1.52.0.ts": "724cee25fa40f1a52d3937d6b4fbbfdd7791ff55e1b7ac08d9319d5632c7f5af",
"https://deno.land/x/xhr@0.3.0/mod.ts": "094aacd627fd9635cd942053bf8032b5223b909858fa9dc8ffa583752ff63b20",
"https://esm.d8d.fun/@d8d-appcontainer/api@3.0.47": "6f44e26f9101c9c00c374a01defa883ae9db7c851ef2b8b130cb6c2d51a41b59",
"https://esm.d8d.fun/@d8d-appcontainer/api@3.0.47/denonext/api.mjs": "778329c130f21a547d6726e8e13fa680a74faecd96e4d953acb481724f8db7be",
"https://esm.d8d.fun/@d8d-appcontainer/auth@0.0.14": "4107b05a0631cc0440dce8b653ccf8e37010e67a7cb80263ac127e1a97c77dc3",
"https://esm.d8d.fun/@d8d-appcontainer/auth@0.0.14/denonext/auth.mjs": "fafb3b08f1bfe7b0a5ecbd2856935288e8b564157d11e5157c7f217ac3f73b43",
"https://esm.d8d.fun/@d8d-appcontainer/types@3.0.47/denonext/types.mjs": "44efd25cb0ad7942ad29d520b5f3296a908d7cefbe85382bcc45e3b3c6c9624c",
"https://esm.d8d.fun/@deno/shim-deno-test@0.5.0/denonext/shim-deno-test.mjs": "21298ee12e8add3e8efe527aa1dd2a4fd029eb1876f5a59107bbe62b3969e282",
"https://esm.d8d.fun/@deno/shim-deno-test@0.5.0?target=denonext": "503b73de1a14bd33782220e11fa2b33e9c87d574ac793e7addf1466c5436e66a",
"https://esm.d8d.fun/@deno/shim-deno@0.18.2/denonext/shim-deno.mjs": "819d8ac34fdaf60658cf03d137f14adaff3f13a279ffd79cd8797d84a6ac46ab",
"https://esm.d8d.fun/@deno/shim-deno@0.18.2?target=denonext": "ffa3ca347bb6b6530720158f307a2e31b16728fbb52e6432254a07d52fcbc404",
"https://esm.d8d.fun/@socket.io/component-emitter@3.1.2/denonext/component-emitter.mjs": "3c6c5f2d64d4933b577a7117df1d8855c51ff01ab3dea8f42af1adcb1a5989e7",
"https://esm.d8d.fun/@socket.io/component-emitter@3.1.2?target=denonext": "f6ff0f94ae3c9850a2c3a925cc2b236ec03a80fc2298d0ca48c2a90b10487db3",
"https://esm.d8d.fun/asynckit@0.4.0/denonext/asynckit.mjs": "4ef3be6eb52c104699b90ca5524db55ec15bc76b361432f05c16b6106279ba72",
"https://esm.d8d.fun/asynckit@0.4.0?target=denonext": "c6bd8832d6d16b648e22d124a16d33c3a7f7076e92be9444f2e4f6b27545708d",
"https://esm.d8d.fun/axios@1.8.4/denonext/axios.mjs": "a0c3d648353c6a1b9864f1067ff9d366b91ccf2c8a413ec30a6b073dea05366b",
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/adapters/http.mjs": "1c7e3b34ddafb39f9b36111dc81ab83a4b1dfed316efc7c9971a534f0c161e7f",
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/adapters/xhr.mjs": "443f6f99410af813126a7b3b004a7fe4d3ce1eb51fb940b15a3fb0932773ff78",
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/cancel/CanceledError.mjs": "6432ce6e9d09faff4439c0c5dfa1d44a79cea9901eb5c1894c7c7497e791e0fd",
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/core/AxiosError.mjs": "2366d9c8250a030e6d82cf72db361d0df9a4311e785e2dd5dd34364c4690cbd3",
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/core/AxiosHeaders.mjs": "f05eb0c07bf1f6d418dad3a3e310070822e5d7059e07c9cc232f365813d1f58e",
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/core/mergeConfig.mjs": "271e93496a6e07b99694817bbcd619c4862b839f5c25538d736168697d09b846",
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/defaults/transitional.mjs": "cf97aea57cebc35857e915fa922468b267947fbd8c8ee6cecc3d9878d2429b4e",
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/env/data.mjs": "b7d5dde239c8a22e820d1e42b045a19d981681e0d7b3eebd3b05f8caf1523455",
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/helpers/bind.mjs": "6f9b25a0abdcbdee99eea549869d4357151d21d8a004174fe5c135f82fc61412",
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/helpers/parseProtocol.mjs": "3268a599f6fa29d6ce6b5e8ee2a61999351b5e414b42cc584c7ee80eae248802",
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/helpers/progressEventReducer.mjs": "308616bf82ad4621ed466b2355025d108d40daac8e95831b56018fbe96c2ac1a",
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/helpers/resolveConfig.mjs": "5e7d2668d1b6d93c6f860aba237e92c2b07b6f0dfedca3b9e304f1dd4578e224",
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/helpers/toFormData.mjs": "9f8a95c8edd57d5e0acb598c56bfac9efae1e5ed39d6c73f94cebc603222b74d",
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/platform/index.mjs": "8022c68893946f6714646f45cb87e5a8d58ac966e1d801a7abeb89f40adbb2fc",
"https://esm.d8d.fun/axios@1.8.4/denonext/lib/platform/node/classes/FormData.mjs": "3af5a3b503dafe0b26a94454685a1a68da3b78540fa1ca65a85814022d725d14",
"https://esm.d8d.fun/axios@1.8.4/denonext/unsafe/core/buildFullPath.mjs": "bd725f6f2e86698888721717727752302d0ae5153bf6216ba5eb6a716692fee2",
"https://esm.d8d.fun/axios@1.8.4/denonext/unsafe/core/settle.mjs": "131ebbb5c8592f9505988f7cc8d33b3905a5ea06425db72723d0164be0e19f0c",
"https://esm.d8d.fun/axios@1.8.4/denonext/unsafe/helpers/buildURL.mjs": "07eee5bbb02b63ca69bda19d9724480724b82fdc8a75a47c46a1f6dfe985f159",
"https://esm.d8d.fun/axios@1.8.4/denonext/unsafe/helpers/combineURLs.mjs": "a2c79317fdc707709b83dc7d3166cab69c3f9dca5ad5c4612ff1e6b29912dee8",
"https://esm.d8d.fun/axios@1.8.4/denonext/unsafe/helpers/isAbsoluteURL.mjs": "df75312b93206485ee501d2b51a5784b1cbf76527cb29803a7fadf1238898881",
"https://esm.d8d.fun/axios@1.8.4/denonext/unsafe/utils.mjs": "ba8669ad91f8b94b0796e13f03605a620d7fe2d05419670cfa2f6793fd5707cf",
"https://esm.d8d.fun/axios@1.8.4?target=denonext": "3652480f46bbae591b9617f4f039537675c72ed4d8b7b44019d7bfdf173102ae",
"https://esm.d8d.fun/buffer-equal-constant-time@1.0.1/denonext/buffer-equal-constant-time.mjs": "c83a9e435334af6863aef9f50bd7d1a855e95055ba8af6dde63eaf674791f64b",
"https://esm.d8d.fun/bufferutil@4.0.9/denonext/bufferutil.mjs": "13dca4d5bb2c68cbe119f880fa3bd785b9a81a8e02e0834dae604b4b85295cd8",
"https://esm.d8d.fun/bufferutil@4.0.9?target=denonext": "e32574569ab438facfcc3f412c659b0719bbf05477136ca176938c9a3ac45125",
"https://esm.d8d.fun/combined-stream@1.0.8/denonext/combined-stream.mjs": "364b91aa4c33e5f0b4075949d93a3407b21a8695031e7c2be29999d588f9ca2c",
"https://esm.d8d.fun/combined-stream@1.0.8?target=denonext": "a0c89b8b29494e966774c7a708e33cc2df16a0bbe2279c841d088e169e7ab3c4",
"https://esm.d8d.fun/dayjs@1.11.13": "89c34b8b3f7b970708114b4d264c9430c30eb0c2eab1419410c77ffefa18fe2c",
"https://esm.d8d.fun/dayjs@1.11.13/denonext/dayjs.mjs": "a6d8258bec464149ab2c9ae26e4bd3736897828586b03f8fea45403080bf8a80",
"https://esm.d8d.fun/dayjs@1.11.13/denonext/plugin/utc.mjs": "01c663b7318d6daa10a2377306a878808a535d6dc4056fa5b60a8d31c5d2254f",
"https://esm.d8d.fun/dayjs@1.11.13/plugin/utc": "2e41a0673e6e7c7c962983f1680911ef6feb27ded6007bc7705787ac1b2637b7",
"https://esm.d8d.fun/debug@4.4.0": "dc29873ca5518385fcbddb2b2fa0f3b31dc6463ba52bdd790818683b9dbdc6ad",
"https://esm.d8d.fun/debug@4.4.0/denonext/debug.mjs": "3077d1ff15cfc5b7baee65b0c00b3200aef8ab51ddddfa960972957c347c1cee",
"https://esm.d8d.fun/debug@4.4.0?target=denonext": "dc29873ca5518385fcbddb2b2fa0f3b31dc6463ba52bdd790818683b9dbdc6ad",
"https://esm.d8d.fun/delayed-stream@1.0.0/denonext/delayed-stream.mjs": "051a3501b7b3d3c593b78a2c7305093a8e363c518cd156f1a77117185e312abe",
"https://esm.d8d.fun/delayed-stream@1.0.0?target=denonext": "d363b81e01f4c886114df14aa660c1a938bbb4be851ff12132260bed0db6126e",
"https://esm.d8d.fun/ecdsa-sig-formatter@1.0.11/denonext/ecdsa-sig-formatter.mjs": "08370379943e48e31cda373711fe5528f60b3d3d992fae7c0d520ef55607640e",
"https://esm.d8d.fun/engine.io-client@6.6.3/denonext/engine.io-client.mjs": "d127a167771015e459e79fb0eb38ee99601ae96ae98924ee407dd77d0ee8be0a",
"https://esm.d8d.fun/engine.io-client@6.6.3?target=denonext": "d97129d74541438ec8167b8232ff764b408b8bf4c065924c60823795fa3e038d",
"https://esm.d8d.fun/engine.io-parser@5.2.3/denonext/engine.io-parser.mjs": "dfb40060c00806566e236f3112b950f43fa6b5e3a142f14ba2e83ad651f4a451",
"https://esm.d8d.fun/engine.io-parser@5.2.3?target=denonext": "1dd633858ff4fd2affd2343c0f16f4d0727524919f53f0a5cf240baefd3c91fd",
"https://esm.d8d.fun/follow-redirects@1.15.9/denonext/follow-redirects.mjs": "90866d22d80eface74d3161906835600fbb1d5c9ded05dc72fd55b40960cfce7",
"https://esm.d8d.fun/follow-redirects@1.15.9?target=denonext": "c8028ec9d980a1974362377614a4978d1556ba85a75bc32526e8917bede535d1",
"https://esm.d8d.fun/form-data@4.0.2/denonext/form-data.mjs": "ad3c492eef1c6153bcfa02eb3041894e8bc8e4aa241ad4d9bd53a385f36d2c6f",
"https://esm.d8d.fun/form-data@4.0.2?target=denonext": "83cf111a2e6f0f7b6c045508635ceae449c5d43d8d22df23693e5b996cc65899",
"https://esm.d8d.fun/hono@4.7.4": "174d520e05dd0a3a1405b1435ee8a8cfe08b16ea80a8363879a88b8a6ff14f69",
"https://esm.d8d.fun/hono@4.7.4/cors": "7fae6007a9ce0f5e471d3e7a9244648df6060438c74d8da418bf0cea735e17c4",
"https://esm.d8d.fun/hono@4.7.4/deno": "8aa4c5e0f121e7cf1f754b905a40b3369315951ca2d83897b076be8752e77c09",
"https://esm.d8d.fun/hono@4.7.4/denonext/cors.mjs": "b7d3ae27185d20108a146cb6f6eb3dc2d3f1010a1d5516534609a370d1e14e7e",
"https://esm.d8d.fun/hono@4.7.4/denonext/deno.mjs": "55170c10ac727e9b09384d9fc05dc9d1524e89e885cd5b8bb05b2b42906866bb",
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/client/utils.mjs": "e334a8ad22f831c6eda17c09ac4bc26bc8cfe16ce2d7bb7727a3519d500ec057",
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/compose.mjs": "1196166f399a52036503ee70ed098af79ea34e1268233970859b2ff355bcffd1",
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/context.mjs": "6eb4bfb07b2b129ec5df08ccb4ddeae28e39c91cc0a2dae5ce8e4151548d218f",
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/hono.mjs": "b2701b3ab66ab91d39b5d521109c4e0cb78d88a71cdbb094ff4a5b8832548e0c",
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/base.mjs": "33d27345ea175980efe2a0d4a7bf12e7c521fe9506ec081f165baf39c55a6311",
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/children.mjs": "8c8ec600c04c1c7d585af870bc417ee1415dec57a83c7cc5b19c7ed779977787",
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/components.mjs": "329a18bfd2823e3925aad171634839a500f88dc45cec74651046009d6ade9a89",
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/constants.mjs": "a5d4f0c5a118dae37998fe58a430354c89ee5eb9206dde0b8bb0f15d3d0a3822",
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/context.mjs": "ea0aeb1b21b339f96151e94418f2bf32fb5e50f320b9d8ab6273927d19e06105",
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/dom/components.mjs": "637aa2d623ae9f5ebc3109e507f8f27ef6c1ae7d4c9bbfb14ecfe8d8ce2151db",
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/dom/context.mjs": "191930e2b2129d28e9a53612d26bf6a99867370396c71d7ecb1caea3d7f7990c",
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/dom/hooks/index.mjs": "fbed9585c8f693edf92a18f2307ce7fafe36955d6899c52720d30b225d8e16a8",
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/dom/render.mjs": "44bd0364afe7b6b46df2a548e06b070777c61fc03252e7d2ae9424a50c9e21b6",
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/hooks/index.mjs": "145266fb802e8fda823514b085e9653c1d0fb07ce6f67a29adbd9d16438fef23",
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/intrinsic-element/common.mjs": "859e3739f0c2637d291a5aaf76f02ab0a5566a1fb693432f95f19235077849b3",
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/jsx/utils.mjs": "169e8732e31a70ed1eecf31a7a1cef6c3882ad32581493d2d7812e3e62caef50",
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/utils/body.mjs": "9459ea130b33427fd1f67d9dc96900528e6ab747b296836a524c01ad6f2eb806",
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/utils/compress.mjs": "c480cdfc4b4236868fda55aba9b012ac91b4bbf237d855da2d40a8b905ee965d",
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/utils/constants.mjs": "b68b3ccd4d516d19a31f1b56a4fb65529b033deb9c8ff34d4b2e44e6e259ef72",
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/utils/handler.mjs": "bded9286b7e46345840114eb8e2a8e627ee4fffda3044163fd78d1578b82f26c",
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/utils/html.mjs": "9949836effcbff234d47fed244e8fa2c3d0a00b89c55767a16c15b7b35e12006",
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/utils/mime.mjs": "0f1f71d357f5b2b15dbe026c4d65219a78d3dd922d799673aed798f1ff18e981",
"https://esm.d8d.fun/hono@4.7.4/denonext/dist/utils/url.mjs": "aa55b9a667876655d972b7e086ff11c05ae9327ef29f174b87d2ce87791fa0fd",
"https://esm.d8d.fun/hono@4.7.4/denonext/hono-base.mjs": "29af9473270ed537bced9d32bc38a858353fe2dba647c1e7ba63dfa98d33838f",
"https://esm.d8d.fun/hono@4.7.4/denonext/hono.mjs": "4d667dd609e6792e5447ea24c718ea906617b8d150675895a04ee4ba7e5e1c52",
"https://esm.d8d.fun/hono@4.7.4/denonext/html.mjs": "02391a2fc736bf5b27588e34948b63581526effc738be6d05721bb7ac334d51a",
"https://esm.d8d.fun/hono@4.7.4/denonext/jsx.mjs": "7e884cd71ecba137dbeda14ba974dd7eecd870240816803883e2a7f6c06f4c2b",
"https://esm.d8d.fun/hono@4.7.4/denonext/jsx/dom/jsx-dev-runtime.mjs": "a02348636d71685c20a9186e66e5f455bdb1999d2bc980e4a4f1bc83dcd7484f",
"https://esm.d8d.fun/hono@4.7.4/denonext/jsx/dom/jsx-runtime.mjs": "32f78a1287f90a8afdf0c0d316af28e9b892401b19734bcabfd59f4e5fe9218c",
"https://esm.d8d.fun/hono@4.7.4/denonext/jsx/jsx-dev-runtime.mjs": "9ce39e3bc5cd0188ed1ef8f4e71ab18560b29a3ea179cd647a404749a6bd0576",
"https://esm.d8d.fun/hono@4.7.4/denonext/jsx/jsx-runtime.mjs": "e84ac4b5216e98c72733b1754acd23349be44a28c929f23ee121263fd11317d5",
"https://esm.d8d.fun/hono@4.7.4/denonext/jsx/streaming.mjs": "a0745c3b7b37d44be9139b4879e7ccfcaaeb12ec10e7880a5c66829c0f59928e",
"https://esm.d8d.fun/hono@4.7.4/denonext/request.mjs": "5e2499f74cad11ede4fd777c22479bfb2161c86e81435f20b205b9d3d02223b4",
"https://esm.d8d.fun/hono@4.7.4/denonext/router.mjs": "51c7a9987e81971338d9e342a305a4c2ce231e7ab6d398aa6a0b74f46443f63b",
"https://esm.d8d.fun/hono@4.7.4/denonext/router/reg-exp-router.mjs": "56a9c48e0ec3a264170db8de1c0342bec844ace172cab882223fcf809c0ce0f6",
"https://esm.d8d.fun/hono@4.7.4/denonext/router/smart-router.mjs": "69dec2229dadfda412d6736719104ed3dceabe667ddff6804a028cb9650b990b",
"https://esm.d8d.fun/hono@4.7.4/denonext/router/trie-router.mjs": "13d18e17ea528e9bfb85cec0221a2da76d9c18b9fe44db8caee98cb51b357234",
"https://esm.d8d.fun/hono@4.7.4/denonext/serve-static.mjs": "0e233f7b5b78374defa5281a05469d326444a8c7894ac36c1ae99e5b1412619d",
"https://esm.d8d.fun/hono@4.7.4/denonext/ssg.mjs": "c9e8cf200027b7c69c8a97d7bf96b64b09ca3b810198958c3f69a3dc18492730",
"https://esm.d8d.fun/hono@4.7.4/denonext/ws.mjs": "896fdef949b92bb10c16ebb1b7977babc3c87d3db43e329a66fcd9a786f8c933",
"https://esm.d8d.fun/hono@4.7.4/jsx": "8fb76b6158726b27e0e9be1db08b831967bd1bd8175d98f75652440254f2b0cf",
"https://esm.d8d.fun/hono@4.7.4/jsx/jsx-runtime": "add6d23136559fd64a92cab007bc2afa9450ad1773e0cd8e8cfdbf0c97d339d3",
"https://esm.d8d.fun/isexe@3.1.1/denonext/isexe.mjs": "59b58a950d33368749f8b2f0df8377ded09f5f30c276f79239a2542029e77a43",
"https://esm.d8d.fun/isexe@3.1.1/denonext/posix.mjs": "f99f8d2aacd0b5424625cee480f36b47639cfbad44c64b7b21cbba18ad77a1b2",
"https://esm.d8d.fun/isexe@3.1.1/denonext/win32.mjs": "f52981ee6555549c246db8e9e6c0ee1e2947a35367c3bcec0ba31834387991c0",
"https://esm.d8d.fun/isexe@3.1.1?target=denonext": "b3c61e7e70b9d56865de461fbcdae702ebf93743143457079a15a60e30dfcf83",
"https://esm.d8d.fun/jsonwebtoken@9.0.2/denonext/jsonwebtoken.mjs": "4365e66f5fa04a6903d106e17166fd93ddef582caf66f19d281dc066711c4f6e",
"https://esm.d8d.fun/jsonwebtoken@9.0.2?target=denonext": "76a17587b04be9dfee91197bb0119b0be3a6a53ec05cc63d44292077bc03c0f7",
"https://esm.d8d.fun/jwa@1.4.1/denonext/jwa.mjs": "fb2de82e653ed6ed5432adb25567772ea9044f241d209c569b23eee07ab50176",
"https://esm.d8d.fun/jwa@1.4.1?target=denonext": "10b18f9707bf3a08b740be74a3bf349ad0e483cf3c5bfa305a023a8c5cb0056e",
"https://esm.d8d.fun/jws@3.2.2/denonext/jws.mjs": "ed1560f6cd91af6f57bdf5132ffc6a126619d003c5d5e7680fb0b310dbf61ad8",
"https://esm.d8d.fun/jws@3.2.2?target=denonext": "e5b49eed4c39c26f02dafda1f75776a1ed468df4b36f8eb0f5a36e036a7af0f2",
"https://esm.d8d.fun/lodash.includes@4.3.0/denonext/lodash.includes.mjs": "7891d8e1db31f196dad13cfea5d8c9cd7b57e556e852bde44a32b9c95f52f769",
"https://esm.d8d.fun/lodash.includes@4.3.0?target=denonext": "52552ec9e8b0ee8dff8bd6743edb4ff57c1c1dc31fe13061c1fd6b023e26c0dd",
"https://esm.d8d.fun/lodash.isboolean@3.0.3/denonext/lodash.isboolean.mjs": "62346e63e84456d408ad9cdd38dc122ea4fecef67b7682c9ef311a9d9376ebb4",
"https://esm.d8d.fun/lodash.isboolean@3.0.3?target=denonext": "df362bed13151ed14dc4fea8a56582f7d5165ab057225f20f7a19933bc030603",
"https://esm.d8d.fun/lodash.isinteger@4.0.4/denonext/lodash.isinteger.mjs": "661e55e1960486dc985a2dd833c942f79874d2c3d82d89b62d92a7c2d4fb1976",
"https://esm.d8d.fun/lodash.isinteger@4.0.4?target=denonext": "1355063df9b6e29a874a0f22874b86abcb7520182efcd5e543ded4209ff4485c",
"https://esm.d8d.fun/lodash.isnumber@3.0.3/denonext/lodash.isnumber.mjs": "406375117efebf18a311f7b9a9cde5b961f2b96589c6e70e7805ae8f9b233186",
"https://esm.d8d.fun/lodash.isnumber@3.0.3?target=denonext": "129cd84408b3f202f339cfbde411d7e3573414128574123b94375b330cfd02b4",
"https://esm.d8d.fun/lodash.isplainobject@4.0.6/denonext/lodash.isplainobject.mjs": "52baba5dc85a16aa38f54fcbbbf47ffe8b3c9d84e788a5199a20eab9135dd35a",
"https://esm.d8d.fun/lodash.isplainobject@4.0.6?target=denonext": "aaf731d53ff27671d17a5e700371eb7d073496e7c099a537c05dd1df6606a54e",
"https://esm.d8d.fun/lodash.isstring@4.0.1/denonext/lodash.isstring.mjs": "27f803808b3f189fa994d1b3784e13b2978976bf17c06050ee311e7f54b0db32",
"https://esm.d8d.fun/lodash.isstring@4.0.1?target=denonext": "dc33fab7958cad340a76cc58bd2f30ad22ee63b00eac0bc2d9a4110049cdfddc",
"https://esm.d8d.fun/lodash.once@4.1.1/denonext/lodash.once.mjs": "d98a09cbf4f8520f256be230ba50e9cae609b73bc8ffd2f6bf388099ea2f50b1",
"https://esm.d8d.fun/lodash.once@4.1.1?target=denonext": "46682cfe5d75f572d14c6a3c4c5da23d597d80d0552fea124b72a2359bb4b37d",
"https://esm.d8d.fun/mime-db@1.52.0/denonext/mime-db.mjs": "f93feb3d7150014b71bd0d06c5bd819db56a089b31b8b79a3b0466bb37ef005e",
"https://esm.d8d.fun/mime-types@2.1.35/denonext/mime-types.mjs": "704bdb318816fe1360c90a196f7cb3ba6e25fe207707cc2df873f890ad2e5f44",
"https://esm.d8d.fun/mime-types@2.1.35?target=denonext": "e4cc9a1aabecc1be22d194375ec3b99cc9d51700cc4629ab689975451c0a8ce5",
"https://esm.d8d.fun/ms@2.1.3/denonext/ms.mjs": "9039464da1f4ae1c2042742d335c82556c048bbe49449b5d0cd5198193afa147",
"https://esm.d8d.fun/ms@2.1.3?target=denonext": "36f5aa7503ff0ff44ce9e3155a60362d8d3ae5db8db048be5764a3a515b6a263",
"https://esm.d8d.fun/nanoid@5.1.5/denonext/nanoid.mjs": "dc919f2d7339a244f732a0cf02e3962dd1289535668026f52fb26bd593e9358b",
"https://esm.d8d.fun/nanoid@5.1.5?target=denonext": "33ad5b17f1290cb850164cfcf30f642d9dad489ba19909bc3cfd9eb78369f451",
"https://esm.d8d.fun/node-gyp-build@4.8.4/denonext/node-gyp-build.mjs": "9a86f2d044fc77bd60aaa3d697c2ba1b818da5fb1b9aaeedec59a40b8e908803",
"https://esm.d8d.fun/node-gyp-build@4.8.4?target=denonext": "261a6cedf1fdbf159798141ba1e2311ac1510682c5c8b55dacc8cf5fdee4aa06",
"https://esm.d8d.fun/proxy-from-env@1.1.0/denonext/proxy-from-env.mjs": "f60f9c79fc3baa07c13c800798d645ae70d1b2059b8d593dcd4f8c5710b50333",
"https://esm.d8d.fun/proxy-from-env@1.1.0?target=denonext": "bf02a050a1a6aa56ddba25dbea2c355da294630e5c5520fddea4b2f30a9292bc",
"https://esm.d8d.fun/safe-buffer@5.2.1/denonext/safe-buffer.mjs": "51b088d69d0bbf6d7ce4179853887e105715df40e432a3bff0e9575cc2285276",
"https://esm.d8d.fun/safe-buffer@5.2.1?target=denonext": "34028b9647c849fa96dfd3d9f217a3adca8b43b13409820ac3f43fb15eba3e20",
"https://esm.d8d.fun/semver@7.7.1/denonext/semver.mjs": "f1d8c45a097d5f2da9662be5ff2087f1e4af9ebeb9b0c2eeeb0c90d74fa7a14c",
"https://esm.d8d.fun/semver@7.7.1?target=denonext": "7d6e1f9de61981f17d0e5153d48b77475e3433225ce9265ad77206afe216c5c8",
"https://esm.d8d.fun/socket.io-client@4.8.1/denonext/socket.io-client.mjs": "b902dafad93171849d6d6e9e98bfa5357513089e43b0fbf9268d394f0839f372",
"https://esm.d8d.fun/socket.io-client@4.8.1?target=denonext": "f5543108c5018ca5904af75985dc9ff7b7210334782408cf87bdf091ce1fbf2e",
"https://esm.d8d.fun/socket.io-parser@4.2.4/denonext/socket.io-parser.mjs": "a989568a92fa45870a4ae74fb731c5e554ef6c901b97f154d8c84267f7d5aaba",
"https://esm.d8d.fun/socket.io-parser@4.2.4?target=denonext": "95bc48ccd83940940fb68cf3401280667a8bad2b6abc8a4c7bb5c39ec59aff16",
"https://esm.d8d.fun/supports-color@10.0.0/denonext/supports-color.mjs": "239cd39d0828e1a018dee102748da869b1b75c38fe6a9c0c8f0bd4ffbd3e1ea1",
"https://esm.d8d.fun/supports-color@10.0.0?target=denonext": "4895255248e4ba0cbcce9437003dccf3658b1ac1d1e8eba5225fb8194c454ee1",
"https://esm.d8d.fun/utf-8-validate@6.0.5/denonext/utf-8-validate.mjs": "90c0c88a13bc4749b497361480d618bf4809153f5d5ba694fac79ae9dbf634a9",
"https://esm.d8d.fun/utf-8-validate@6.0.5?target=denonext": "071bc33ba1a58297e23a34d69dd589fd06df04b0f373b382ff5da544a623f271",
"https://esm.d8d.fun/which@4.0.0/denonext/which.mjs": "9f47207c6dc9684fe3d852f2290c474577babaeabf60616652630c0b90421a53",
"https://esm.d8d.fun/which@4.0.0?target=denonext": "50b06c1a68e3ef88dc8e2c68c17b732a6d1917000d5d59637496da3b61549c8e",
"https://esm.d8d.fun/ws@8.17.1/denonext/ws.mjs": "7b349f9bcf5af35a422b01ece5189ac693f84f07cc2e9be12023ec818a18ba71",
"https://esm.d8d.fun/ws@8.17.1?target=denonext": "3c5e4dca1be73c0e7776cb033809d16c2421b785cd1b93827b76a43c5b59a0bd",
"https://esm.d8d.fun/xmlhttprequest-ssl@2.1.2/denonext/xmlhttprequest-ssl.mjs": "5cb537aeb44e2971f9d84c4e22e0d24ea0554eb6c33a5d10a46cf163debf60ec",
"https://esm.d8d.fun/xmlhttprequest-ssl@2.1.2?target=denonext": "5a4574293c501f0f0da3ddd653bd5d9ac00ea59647e3b20694cc05ed02e7a22f"
}
}

403
server/migrations.ts Normal file
View File

@@ -0,0 +1,403 @@
import type { MigrationLiveDefinition } from '@d8d-appcontainer/types'
import {
EnableStatus, DeleteStatus,
AuditStatus, ThemeMode, FontSize, CompactMode,
SystemSettingKey,
SystemSettingGroup,
ALLOWED_FILE_TYPES,
} from '../client/share/types.ts';
// 定义用户表迁移
const createUsersTable: MigrationLiveDefinition = {
name: "create_users_table",
up: async (api) => {
await api.schema.createTable('users', (table) => {
table.increments('id').primary();
table.string('username').unique().notNullable();
table.string('password').notNullable();
table.string('phone').unique();
table.string('email').unique();
table.string('nickname');
table.string('name');
table.integer('is_disabled').defaultTo(0);
table.integer('is_deleted').defaultTo(0);
table.timestamps(true, true);
// 添加索引
table.index('username');
table.index('is_disabled');
table.index('is_deleted');
});
},
down: async (api) => {
await api.schema.dropTable('users');
}
}
// 定义登录历史表迁移
const createLoginHistoryTable: MigrationLiveDefinition = {
name: "create_login_history_table",
up: async (api) => {
await api.schema.createTable('login_history', (table) => {
table.increments('id').primary()
table.integer('user_id').unsigned().references('id').inTable('users').onDelete('CASCADE')
table.timestamp('login_time').defaultTo(api.fn.now())
table.string('ip_address')
table.text('user_agent')
table.decimal('longitude', 10, 6).nullable() // 经度
table.decimal('latitude', 10, 6).nullable() // 纬度
table.string('location_name').nullable() // 地点名称
// 添加索引
table.index('user_id');
table.index('login_time');
// table.index(['longitude', 'latitude']);
})
},
down: async (api) => {
await api.schema.dropTable('login_history')
}
}
// 定义知识库文章表迁移
const createKnowInfoTable: MigrationLiveDefinition = {
name: "create_know_info_table",
up: async (api) => {
await api.schema.createTable('know_info', (table) => {
table.increments('id').primary();
table.string('title').comment('文章标题');
table.string('tags').comment('文章标签');
table.text('content').comment('文章内容');
table.string('author').comment('作者');
table.string('category').comment('分类');
table.string('cover_url').comment('封面图片URL');
table.integer('audit_status').defaultTo(AuditStatus.PENDING).comment('审核状态');
table.integer('is_deleted').defaultTo(0).comment('是否被删除 (0否 1是)');
table.timestamps(true, true);
// 添加索引
table.index('title');
table.index('tags');
table.index('author');
table.index('category');
table.index('audit_status');
table.index('is_deleted');
});
},
down: async (api) => {
await api.schema.dropTable('know_info');
}
};
// 定义文件分类表迁移
const createFileCategoryTable: MigrationLiveDefinition = {
name: "create_file_category_table",
up: async (api) => {
await api.schema.createTable('file_categories', (table) => {
table.increments('id').primary();
table.string('name').notNullable().comment('分类名称');
table.string('code').notNullable().unique().comment('分类编码');
table.text('description').comment('分类描述');
table.integer('is_deleted').defaultTo(DeleteStatus.NOT_DELETED).comment('是否被删除 (0否 1是)');
table.timestamps(true, true);
// 添加索引
table.index('name');
table.index('code');
table.index('is_deleted');
});
},
down: async (api) => {
await api.schema.dropTable('file_categories');
}
};
// 定义文件库表迁移
const createFileLibraryTable: MigrationLiveDefinition = {
name: "create_file_library_table",
up: async (api) => {
await api.schema.createTable('file_library', (table) => {
table.increments('id').primary();
table.string('file_name').notNullable().comment('文件名称');
table.string('original_filename').comment('原始文件名');
table.string('file_path').notNullable().comment('文件路径');
table.string('file_type').comment('文件类型');
table.integer('file_size').unsigned().comment('文件大小(字节)');
table.integer('uploader_id').unsigned().references('id').inTable('users').onDelete('SET NULL').comment('上传用户ID');
table.string('uploader_name').comment('上传者名称');
table.integer('category_id').unsigned().references('id').inTable('file_categories').onDelete('SET NULL').comment('文件分类');
table.string('tags').comment('文件标签');
table.text('description').comment('文件描述');
table.integer('download_count').defaultTo(0).comment('下载次数');
table.integer('is_disabled').defaultTo(EnableStatus.DISABLED).comment('是否禁用 (0否 1是)');
table.integer('is_deleted').defaultTo(DeleteStatus.NOT_DELETED).comment('是否被删除 (0否 1是)');
table.timestamps(true, true);
// 添加索引
table.index('file_name');
table.index('file_type');
table.index('category_id');
table.index('uploader_id');
table.index('is_deleted');
});
},
down: async (api) => {
await api.schema.dropTable('file_library');
}
};
// 定义主题设置表迁移
const createThemeSettingsTable: MigrationLiveDefinition = {
name: "create_theme_settings_table",
up: async (api) => {
await api.schema.createTable('theme_settings', (table) => {
table.increments('id').primary();
table.integer('user_id').unsigned().references('id').inTable('users').onDelete('CASCADE');
table.jsonb('settings').comment('主题设置');
table.timestamps(true, true);
// 添加索引
table.index('user_id');
});
},
down: async (api) => {
await api.schema.dropTable('theme_settings');
}
};
// 定义系统设置表迁移
const createSystemSettingsTable: MigrationLiveDefinition = {
name: "create_system_settings_table",
up: async (api) => {
await api.schema.createTable('system_settings', (table) => {
table.increments('id').primary();
table.string('key').notNullable().unique().comment('设置键');
table.text('value').notNullable().comment('设置值');
table.string('description').nullable().comment('设置描述');
table.string('group').notNullable().comment('设置分组');
table.timestamps(true, true);
// 添加索引
table.index('key');
table.index('group');
});
},
down: async (api) => {
await api.schema.dropTable('system_settings');
}
};
// 初始测试数据迁移
const seedInitialData: MigrationLiveDefinition = {
name: "seed_initial_data",
up: async (api) => {
// 1. 添加默认用户
const defaultUser = {
username: 'admin',
password: 'admin123', // 实际应用中应使用加密后的密码
email: 'admin@example.com',
nickname: '系统管理员',
name: '管理员',
is_disabled: EnableStatus.ENABLED,
is_deleted: DeleteStatus.NOT_DELETED
};
const [userId] = await api.table('users').insert(defaultUser);
// 2. 添加默认主题设置
await api.table('theme_settings').insert({
user_id: userId,
settings: {
theme_mode: ThemeMode.LIGHT,
primary_color: '#1890ff',
font_size: FontSize.MEDIUM,
is_compact: CompactMode.NORMAL
},
created_at: api.fn.now(),
updated_at: api.fn.now()
});
// 3. 添加知识库文章示例
await api.table('know_info').insert([
{
title: '欢迎使用应用Starter',
tags: 'starter,指南',
content: '# 欢迎使用应用Starter\n\n这是一个基础的应用Starter提供了用户认证、文件管理、知识库、主题管理等功能。\n\n## 主要功能\n\n- 用户认证与管理\n- 文件上传与管理\n- 知识库文章管理\n- 主题设置(暗黑模式/明亮模式)\n- 图表数据统计\n- 地图集成\n\n更多功能请参考文档...',
author: '系统管理员',
category: '使用指南',
audit_status: AuditStatus.APPROVED,
is_deleted: DeleteStatus.NOT_DELETED
},
{
title: '如何使用文件管理',
tags: '文件,上传,管理',
content: '# 文件管理使用指南\n\n文件管理模块可以帮助您上传、分类和管理各种文件。\n\n## 上传文件\n\n1. 点击"上传文件"按钮\n2. 选择要上传的文件\n3. 填写文件信息(分类、标签等)\n4. 点击"确定"完成上传\n\n## 文件分类\n\n您可以创建自定义的文件分类方便管理不同类型的文件...',
author: '系统管理员',
category: '使用指南',
audit_status: AuditStatus.APPROVED,
is_deleted: DeleteStatus.NOT_DELETED
},
{
title: '主题设置指南',
tags: '主题,设置,外观',
content: '# 主题设置指南\n\n主题设置允许您自定义应用的外观和感觉包括颜色模式、字体大小等。\n\n## 颜色模式\n\n您可以选择明亮模式或暗黑模式适应不同的工作环境和个人偏好。\n\n## 主题颜色\n\n可以选择主题的主色调系统会根据选择自动生成配色方案。\n\n## 字体大小\n\n提供小、中、大三种字体大小选项满足不同用户的阅读需求。',
author: '系统管理员',
category: '使用指南',
audit_status: AuditStatus.APPROVED,
is_deleted: DeleteStatus.NOT_DELETED
},
{
title: '数据分析功能介绍',
tags: '分析,图表,数据',
content: '# 数据分析功能介绍\n\n数据分析模块提供了多种图表和可视化工具帮助您理解和分析数据。\n\n## 图表类型\n\n支持柱状图、折线图、饼图等多种图表类型适用于不同的数据展示需求。\n\n## 数据筛选\n\n可以根据时间范围、数据类型等条件筛选数据获得更精确的分析结果。',
author: '系统管理员',
category: '使用指南',
audit_status: AuditStatus.APPROVED,
is_deleted: DeleteStatus.NOT_DELETED
}
]);
// 4. 添加文件分类示例
await api.table('file_categories').insert([
{
name: '文档',
code: 'doc',
description: '各类文档文件',
is_deleted: DeleteStatus.NOT_DELETED
},
{
name: '图片',
code: 'image',
description: '各类图片文件',
is_deleted: DeleteStatus.NOT_DELETED
},
{
name: '视频',
code: 'video',
description: '各类视频文件',
is_deleted: DeleteStatus.NOT_DELETED
},
{
name: '音频',
code: 'audio',
description: '各类音频文件',
is_deleted: DeleteStatus.NOT_DELETED
},
{
name: '其他',
code: 'other',
description: '其他类型文件',
is_deleted: DeleteStatus.NOT_DELETED
}
]);
// 5. 添加系统设置示例
await api.table('system_settings').insert([
{
key: SystemSettingKey.SITE_NAME,
value: '应用管理系统',
description: '站点名称',
group: SystemSettingGroup.BASIC
},
{
key: SystemSettingKey.SITE_DESCRIPTION,
value: '一个功能完善的应用管理系统',
description: '站点描述',
group: SystemSettingGroup.BASIC
},
{
key: SystemSettingKey.SITE_KEYWORDS,
value: '应用,管理,系统',
description: '站点关键词',
group: SystemSettingGroup.BASIC
},
{
key: SystemSettingKey.ENABLE_REGISTER,
value: true,
description: '是否开启注册功能',
group: SystemSettingGroup.FEATURE
},
{
key: SystemSettingKey.ENABLE_CAPTCHA,
value: true,
description: '是否开启验证码',
group: SystemSettingGroup.FEATURE
},
{
key: SystemSettingKey.LOGIN_ATTEMPTS,
value: 5,
description: '允许的登录尝试次数',
group: SystemSettingGroup.FEATURE
},
{
key: SystemSettingKey.SESSION_TIMEOUT,
value: 120,
description: '会话超时时间(分钟)',
group: SystemSettingGroup.FEATURE
},
{
key: SystemSettingKey.UPLOAD_MAX_SIZE,
value: 10,
description: '最大上传大小(MB)',
group: SystemSettingGroup.UPLOAD
},
{
key: SystemSettingKey.ALLOWED_FILE_TYPES,
value: ALLOWED_FILE_TYPES,
description: '允许上传的文件类型',
group: SystemSettingGroup.UPLOAD
},
{
key: SystemSettingKey.IMAGE_COMPRESS,
value: true,
description: '是否压缩图片',
group: SystemSettingGroup.UPLOAD
},
{
key: SystemSettingKey.IMAGE_MAX_WIDTH,
value: 1920,
description: '图片最大宽度',
group: SystemSettingGroup.UPLOAD
},
{
key: SystemSettingKey.NOTIFY_ON_LOGIN,
value: true,
description: '是否开启登录通知',
group: SystemSettingGroup.NOTIFICATION
},
{
key: SystemSettingKey.NOTIFY_ON_UPLOAD,
value: true,
description: '是否开启上传通知',
group: SystemSettingGroup.NOTIFICATION
},
{
key: SystemSettingKey.NOTIFY_ON_ERROR,
value: true,
description: '是否开启错误通知',
group: SystemSettingGroup.NOTIFICATION
}
]);
},
down: async (api) => {
// 删除初始数据
await api.table('login_history').where('user_id', 1).delete();
await api.table('theme_settings').where('user_id', 1).delete();
await api.table('know_info').delete();
await api.table('file_categories').delete();
await api.table('users').where('username', 'admin').delete();
}
};
// 导出所有迁移
export const migrations = [
createUsersTable,
createLoginHistoryTable,
createKnowInfoTable,
createFileCategoryTable,
createFileLibraryTable,
createThemeSettingsTable,
createSystemSettingsTable,
seedInitialData
];

103
server/routes_auth.ts Normal file
View File

@@ -0,0 +1,103 @@
import { Hono } from 'hono'
import type { Variables } from './app.tsx'
import type { WithAuth } from './app.tsx'
export function createAuthRoutes(withAuth: WithAuth) {
const authRoutes = new Hono<{ Variables: Variables }>()
// 登录状态检查
authRoutes.get('/status', async (c) => {
try {
const auth = c.get('auth')
const token = c.req.header('Authorization')?.replace('Bearer ', '')
if (!token) {
return c.json({ isValid: false }, 200)
}
const status = await auth.checkLoginStatus(token)
return c.json(status)
} catch (error) {
console.error('登录状态检查失败:', error)
return c.json({ isValid: false, error: '登录状态检查失败' }, 500)
}
})
// 注册
authRoutes.post('/register', async (c) => {
try {
const auth = c.get('auth')
const { username, email, password } = await c.req.json()
if (!username || !password) {
return c.json({ error: '用户名和密码不能为空' }, 400)
}
try {
await auth.createUser({ username, password, email })
const result = await auth.authenticate(username, password)
return c.json({
message: '注册成功',
user: result.user
}, 201)
} catch (authError) {
return c.json({ error: '用户已存在或注册失败' }, 400)
}
} catch (error) {
console.error('注册失败:', error)
return c.json({ error: '注册失败' }, 500)
}
})
// 登录
authRoutes.post('/login', async (c) => {
try {
const auth = c.get('auth')
const { username, password } = await c.req.json()
if (!username || !password) {
return c.json({ error: '用户名和密码不能为空' }, 400)
}
try {
const result = await auth.authenticate(username, password)
if (result.user) {
const apiClient = c.get('apiClient')
await apiClient.database.insert('login_history', {
user_id: result.user.id,
login_time: apiClient.database.fn.now(),
ip_address: c.req.header('x-forwarded-for') || '未知',
user_agent: c.req.header('user-agent') || '未知'
})
}
return c.json({
message: '登录成功',
token: result.token,
refreshToken: result.refreshToken,
user: result.user
})
} catch (authError) {
return c.json({ error: '用户名或密码错误' }, 401)
}
} catch (error) {
console.error('登录失败:', error)
return c.json({ error: '登录失败' }, 500)
}
})
// 获取当前用户信息
authRoutes.get('/me', withAuth, (c) => {
const user = c.get('user')
return c.json(user)
})
// 登出
authRoutes.post('/logout', async (c) => {
return c.json({ message: '登出成功' })
})
return authRoutes
}

180
server/routes_charts.ts Normal file
View File

@@ -0,0 +1,180 @@
import { Hono } from "hono";
import debug from "debug";
import {
DeleteStatus,
} from "../client/share/types.ts";
import type { Variables, WithAuth } from "./app.tsx";
const log = {
api: debug("api:sys"),
};
// 创建图表数据路由
export function createChartRoutes(withAuth: WithAuth) {
const chartRoutes = new Hono<{ Variables: Variables }>();
// 获取用户活跃度图表数据
chartRoutes.get("/user-activity", withAuth, async (c) => {
try {
const apiClient = c.get('apiClient');
// 获取过去30天的数据
const days = 30;
const result = [];
// 当前日期
const currentDate = new Date();
// 生成过去30天的日期范围
for (let i = days - 1; i >= 0; i--) {
const date = new Date();
date.setDate(currentDate.getDate() - i);
// 格式化日期为 YYYY-MM-DD
const formattedDate = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
// 查询当天的登录次数
const loginCount = await apiClient.database
.table('login_history')
.whereRaw(`DATE(login_time) = ?`, [formattedDate])
.count();
result.push({
date: formattedDate,
count: Number(loginCount),
});
}
return c.json({
message: "获取用户活跃度数据成功",
data: result,
});
} catch (error) {
log.api("获取用户活跃度数据失败:", error);
return c.json({ error: "获取用户活跃度数据失败" }, 500);
}
});
// 获取文件上传统计图表数据
chartRoutes.get("/file-uploads", withAuth, async (c) => {
try {
const apiClient = c.get('apiClient');
// 获取过去12个月的数据
const months = 12;
const result = [];
// 当前日期
const currentDate = new Date();
// 生成过去12个月的月份范围
for (let i = months - 1; i >= 0; i--) {
const date = new Date();
date.setMonth(currentDate.getMonth() - i);
// 获取年月
const year = date.getFullYear();
const month = date.getMonth() + 1;
// 月份标签
const monthLabel = `${year}-${String(month).padStart(2, '0')}`;
// 查询当月的文件上传数量
const fileCount = await apiClient.database
.table('file_library')
.whereRaw(`YEAR(created_at) = ? AND MONTH(created_at) = ?`, [year, month])
.count();
result.push({
month: monthLabel,
count: Number(fileCount),
});
}
return c.json({
message: "获取文件上传统计数据成功",
data: result,
});
} catch (error) {
log.api("获取文件上传统计数据失败:", error);
return c.json({ error: "获取文件上传统计数据失败" }, 500);
}
});
// 获取文件类型分布图表数据
chartRoutes.get("/file-types", withAuth, async (c) => {
try {
const apiClient = c.get('apiClient');
// 查询不同文件类型的数量
const fileTypeStats = await apiClient.database
.table('file_library')
.select('file_type',apiClient.database.raw('count(id) as count'))
.where('is_deleted', DeleteStatus.NOT_DELETED)
.groupBy('file_type');
// 将结果转换为饼图所需格式
const result = fileTypeStats.map(item => ({
type: item.file_type || '未知',
value: Number(item.count),
}));
return c.json({
message: "获取文件类型分布数据成功",
data: result,
});
} catch (error) {
log.api("获取文件类型分布数据失败:", error);
return c.json({ error: "获取文件类型分布数据失败" }, 500);
}
});
// 获取仪表盘概览数据
chartRoutes.get("/dashboard-overview", withAuth, async (c) => {
try {
const apiClient = c.get('apiClient');
// 获取用户总数
const userCount = await apiClient.database
.table('users')
.where('is_deleted', DeleteStatus.NOT_DELETED)
.count();
// 获取文件总数
const fileCount = await apiClient.database
.table('file_library')
.where('is_deleted', DeleteStatus.NOT_DELETED)
.count();
// 获取知识库文章总数
const articleCount = await apiClient.database
.table('know_info')
.where('is_deleted', DeleteStatus.NOT_DELETED)
.count();
// 获取今日登录次数
const today = new Date().toISOString().split('T')[0];
const todayLoginCount = await apiClient.database
.table('login_history')
.whereRaw(`DATE(login_time) = ?`, [today])
.count();
return c.json({
message: "获取仪表盘概览数据成功",
data: {
userCount: Number(userCount),
fileCount: Number(fileCount),
articleCount: Number(articleCount),
todayLoginCount: Number(todayLoginCount),
},
});
} catch (error) {
log.api("获取仪表盘概览数据失败:", error);
return c.json({ error: "获取仪表盘概览数据失败" }, 500);
}
});
return chartRoutes;
}

161
server/routes_maps.ts Normal file
View File

@@ -0,0 +1,161 @@
import { Hono } from "hono";
import debug from "debug";
import type { Variables, WithAuth } from "./app.tsx";
const log = {
api: debug("api:sys"),
};
// 创建地图数据路由
export function createMapRoutes(withAuth: WithAuth) {
const mapRoutes = new Hono<{ Variables: Variables }>();
// 获取地图标记点数据
mapRoutes.get("/markers", withAuth, async (c) => {
try {
const apiClient = c.get('apiClient');
// 从登录历史表中查询有经纬度的登录记录
const locations = await apiClient.database
.table('login_history')
.select(
'id',
'user_id',
'location_name',
'longitude',
'latitude',
'login_time',
'ip_address'
)
.whereNotNull('longitude')
.whereNotNull('latitude')
.orderBy('login_time', 'desc')
.limit(100); // 限制返回最近100条记录
// 获取相关用户信息
const userIds = [...new Set(locations.map(loc => loc.user_id))];
const users = await apiClient.database
.table('users')
.select('id', 'username', 'nickname')
.whereIn('id', userIds);
// 构建用户信息映射
const userMap = new Map(users.map(user => [user.id, user]));
// 转换为地图标记点数据格式
const markers = locations.map(location => ({
id: location.id,
name: location.location_name || '未知地点',
longitude: location.longitude,
latitude: location.latitude,
loginTime: location.login_time,
ipAddress: location.ip_address,
user: userMap.get(location.user_id)
}));
return c.json({
message: "获取登录位置数据成功",
data: markers,
});
} catch (error) {
log.api("获取登录位置数据失败:", error);
return c.json({ error: "获取登录位置数据失败" }, 500);
}
});
// 获取登录位置详情数据
mapRoutes.get("/location/:id", withAuth, async (c) => {
try {
const id = Number(c.req.param("id"));
if (!id || isNaN(id)) {
return c.json({ error: "无效的登录记录ID" }, 400);
}
const apiClient = c.get('apiClient');
// 查询登录记录详情
const location = await apiClient.database
.table('login_history')
.where('id', id)
.first();
if (!location) {
return c.json({ error: "登录记录不存在" }, 404);
}
// 获取用户信息
const [user] = await apiClient.database
.table('users')
.select('id', 'username', 'nickname')
.where('id', location.user_id);
return c.json({
message: "获取登录位置详情成功",
data: {
...location,
user
},
});
} catch (error) {
log.api("获取登录位置详情失败:", error);
return c.json({ error: "获取登录位置详情失败" }, 500);
}
});
// 更新登录位置信息
mapRoutes.put("/location/:id", withAuth, async (c) => {
try {
const id = Number(c.req.param("id"));
if (!id || isNaN(id)) {
return c.json({ error: "无效的登录记录ID" }, 400);
}
const apiClient = c.get('apiClient');
const data = await c.req.json();
// 验证经纬度
if (!data.longitude || !data.latitude) {
return c.json({ error: "经度和纬度不能为空" }, 400);
}
// 检查登录记录是否存在
const location = await apiClient.database
.table('login_history')
.where('id', id)
.first();
if (!location) {
return c.json({ error: "登录记录不存在" }, 404);
}
// 更新位置信息
await apiClient.database
.table('login_history')
.where('id', id)
.update({
longitude: data.longitude,
latitude: data.latitude,
location_name: data.location_name
});
// 获取更新后的登录记录
const updatedLocation = await apiClient.database
.table('login_history')
.where('id', id)
.first();
return c.json({
message: "登录位置信息更新成功",
data: updatedLocation,
});
} catch (error) {
log.api("更新登录位置信息失败:", error);
return c.json({ error: "更新登录位置信息失败" }, 500);
}
});
return mapRoutes;
}

1063
server/routes_sys.ts Normal file

File diff suppressed because it is too large Load Diff

241
server/routes_users.ts Normal file
View File

@@ -0,0 +1,241 @@
import { Hono } from 'hono'
import type { Variables } from './app.tsx'
import type { WithAuth } from './app.tsx'
export function createUserRoutes(withAuth: WithAuth) {
const usersRoutes = new Hono<{ Variables: Variables }>()
// 获取用户列表
usersRoutes.get('/', withAuth, async (c) => {
try {
const apiClient = c.get('apiClient')
const page = Number(c.req.query('page')) || 1
const pageSize = Number(c.req.query('pageSize')) || 10
const offset = (page - 1) * pageSize
const search = c.req.query('search') || ''
let query = apiClient.database.table('users')
.orderBy('id', 'desc')
if (search) {
query = query.where((builder) => {
builder.where('username', 'like', `%${search}%`)
.orWhere('nickname', 'like', `%${search}%`)
.orWhere('email', 'like', `%${search}%`)
})
}
const total = await query.clone().count()
const users = await query.select('id', 'username', 'nickname', 'email', 'phone', 'role', 'created_at')
.limit(pageSize).offset(offset)
return c.json({
data: users,
pagination: {
total: Number(total),
current: page,
pageSize
}
})
} catch (error) {
console.error('获取用户列表失败:', error)
return c.json({ error: '获取用户列表失败' }, 500)
}
})
// 获取单个用户详情
usersRoutes.get('/:id', withAuth, async (c) => {
try {
const id = Number(c.req.param('id'))
if (!id || isNaN(id)) {
return c.json({ error: '无效的用户ID' }, 400)
}
const apiClient = c.get('apiClient')
const user = await apiClient.database.table('users')
.where('id', id)
.select('id', 'username', 'nickname', 'email', 'phone', 'role', 'created_at')
.first()
if (!user) {
return c.json({ error: '用户不存在' }, 404)
}
return c.json({
data: user,
message: '获取用户详情成功'
})
} catch (error) {
console.error('获取用户详情失败:', error)
return c.json({ error: '获取用户详情失败' }, 500)
}
})
// 创建用户
usersRoutes.post('/', withAuth, async (c) => {
try {
const apiClient = c.get('apiClient')
const body = await c.req.json()
// 验证必填字段
const { username, nickname, email, password, role } = body
if (!username || !nickname || !email || !password || !role) {
return c.json({ error: '缺少必要的用户信息' }, 400)
}
// 检查用户名是否已存在
const existingUser = await apiClient.database.table('users')
.where('username', username)
.first()
if (existingUser) {
return c.json({ error: '用户名已存在' }, 400)
}
// 创建用户
const [id] = await apiClient.database.table('users').insert({
username,
nickname,
email,
password: password, // 加密密码
role,
created_at: new Date(),
updated_at: new Date()
})
const newUser = await apiClient.database.table('users')
.where('id', id)
.select('id', 'username', 'nickname', 'email', 'role', 'created_at')
.first()
return c.json({
data: newUser,
message: '创建用户成功'
})
} catch (error) {
console.error('创建用户失败:', error)
return c.json({ error: '创建用户失败' }, 500)
}
})
// 更新用户
usersRoutes.put('/:id', withAuth, async (c) => {
try {
const id = Number(c.req.param('id'))
if (!id || isNaN(id)) {
return c.json({ error: '无效的用户ID' }, 400)
}
const apiClient = c.get('apiClient')
const body = await c.req.json()
// 验证必填字段
const { username, nickname, email, role } = body
if (!username || !nickname || !email || !role) {
return c.json({ error: '缺少必要的用户信息' }, 400)
}
// 检查用户是否存在
const existingUser = await apiClient.database.table('users')
.where('id', id)
.first()
if (!existingUser) {
return c.json({ error: '用户不存在' }, 404)
}
// 如果修改了用户名,检查新用户名是否已被使用
if (username !== existingUser.username) {
const userWithSameName = await apiClient.database.table('users')
.where('username', username)
.whereNot('id', id.toString())
.first()
if (userWithSameName) {
return c.json({ error: '用户名已存在' }, 400)
}
}
// 更新用户信息
const updateData: any = {
username,
nickname,
email,
role,
updated_at: new Date()
}
// 如果提供了新密码,则更新密码
if (body.password) {
updateData.password = body.password
}
await apiClient.database.table('users')
.where('id', id)
.update(updateData)
const updatedUser = await apiClient.database.table('users')
.where('id', id)
.select('id', 'username', 'nickname', 'email', 'role', 'created_at')
.first()
return c.json({
data: updatedUser,
message: '更新用户成功'
})
} catch (error) {
console.error('更新用户失败:', error)
return c.json({ error: '更新用户失败' }, 500)
}
})
// 删除用户
usersRoutes.delete('/:id', withAuth, async (c) => {
try {
const id = Number(c.req.param('id'))
if (!id || isNaN(id)) {
return c.json({ error: '无效的用户ID' }, 400)
}
const apiClient = c.get('apiClient')
// 检查用户是否存在
const existingUser = await apiClient.database.table('users')
.where('id', id)
.first()
if (!existingUser) {
return c.json({ error: '用户不存在' }, 404)
}
// 检查是否为最后一个管理员
if (existingUser.role === 'admin') {
const adminCount = await apiClient.database.table('users')
.where('role', 'admin')
.count()
if (Number(adminCount) <= 1) {
return c.json({ error: '不能删除最后一个管理员' }, 400)
}
}
// 删除用户
await apiClient.database.table('users')
.where('id', id)
.delete()
return c.json({
message: '删除用户成功',
id
})
} catch (error) {
console.error('删除用户失败:', error)
return c.json({ error: '删除用户失败' }, 500)
}
})
return usersRoutes
}