app.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. /** @jsxImportSource https://esm.d8d.fun/hono@4.7.4/jsx */
  2. import { Hono } from 'hono'
  3. import { Auth } from '@d8d-appcontainer/auth'
  4. import type { User as AuthUser } from '@d8d-appcontainer/auth'
  5. import React from 'hono/jsx'
  6. import type { FC } from 'hono/jsx'
  7. import { cors } from 'hono/cors'
  8. import type { Context as HonoContext } from 'hono'
  9. import { serveStatic } from 'hono/deno'
  10. import { APIClient } from '@d8d-appcontainer/api'
  11. import debug from "debug"
  12. import dayjs from 'dayjs';
  13. import utc from 'dayjs/plugin/utc';
  14. import type { SystemSettingRecord, GlobalConfig } from '../client/share/types.ts';
  15. import { SystemSettingKey, OssType, MapMode } from '../client/share/types.ts';
  16. import {
  17. createKnowInfoRoutes,
  18. createFileCategoryRoutes,
  19. createFileUploadRoutes,
  20. createThemeRoutes,
  21. createSystemSettingsRoutes,
  22. } from "./routes_sys.ts";
  23. import {
  24. createMapRoutes,
  25. } from "./routes_maps.ts";
  26. import {
  27. createChartRoutes,
  28. } from "./routes_charts.ts";
  29. import {
  30. createClassRoomRoutes
  31. } from "./routes_classroom.ts"
  32. // 导入基础路由
  33. import { createAuthRoutes } from "./routes_auth.ts";
  34. import { createUserRoutes } from "./routes_users.ts";
  35. import { createMessagesRoutes } from "./routes_messages.ts";
  36. import { createMigrationsRoutes } from "./routes_migrations.ts";
  37. import { createHomeRoutes } from "./routes_home.ts";
  38. dayjs.extend(utc)
  39. // 初始化debug实例
  40. const log = {
  41. app: debug('app:server'),
  42. auth: debug('auth:server'),
  43. api: debug('api:server'),
  44. debug: debug('debug:server')
  45. }
  46. const GLOBAL_CONFIG: GlobalConfig = {
  47. OSS_BASE_URL: Deno.env.get('OSS_BASE_URL') || 'https://d8d-appcontainer-user.oss-cn-beijing.aliyuncs.com',
  48. OSS_TYPE: Deno.env.get('OSS_TYPE') === OssType.MINIO ? OssType.MINIO : OssType.ALIYUN,
  49. API_BASE_URL: '/api',
  50. APP_NAME: Deno.env.get('APP_NAME') || '应用Starter',
  51. ENV: Deno.env.get('ENV') || 'development',
  52. DEFAULT_THEME: 'light', // 默认主题
  53. MAP_CONFIG: {
  54. KEY: Deno.env.get('AMAP_KEY') || '您的地图API密钥',
  55. VERSION: '2.0',
  56. PLUGINS: ['AMap.ToolBar', 'AMap.Scale', 'AMap.HawkEye', 'AMap.MapType', 'AMap.Geolocation'],
  57. MAP_MODE: Deno.env.get('MAP_MODE') === MapMode.OFFLINE ? MapMode.OFFLINE : MapMode.ONLINE,
  58. },
  59. CHART_THEME: 'default', // 图表主题
  60. ENABLE_THEME_CONFIG: false, // 主题配置开关
  61. THEME: null
  62. };
  63. log.app.enabled = true
  64. log.auth.enabled = true
  65. log.api.enabled = true
  66. log.debug.enabled = true
  67. // 定义自定义上下文类型
  68. export interface Variables {
  69. auth: Auth
  70. user?: AuthUser
  71. apiClient: APIClient
  72. moduleDir: string
  73. systemSettings?: SystemSettingRecord
  74. }
  75. // 定义登录历史类型
  76. interface LoginHistory {
  77. id: number
  78. user_id: number
  79. login_time: string
  80. ip_address?: string
  81. user_agent?: string
  82. }
  83. // 定义仪表盘数据类型
  84. interface DashboardData {
  85. lastLogin: string
  86. loginCount: number
  87. fileCount: number
  88. userCount: number
  89. systemInfo: {
  90. version: string
  91. lastUpdate: string
  92. }
  93. }
  94. interface EsmScriptConfig {
  95. src: string
  96. href: string
  97. denoJson: string
  98. refresh: boolean
  99. prodPath?: string
  100. prodSrc?: string
  101. }
  102. // Auth实例
  103. let authInstance: Auth | null = null
  104. // 初始化Auth实例
  105. const initAuth = async (apiClient: APIClient) => {
  106. try {
  107. if (authInstance) {
  108. return authInstance
  109. }
  110. log.auth('正在初始化Auth实例')
  111. authInstance = new Auth(apiClient as any, {
  112. jwtSecret: Deno.env.get("JWT_SECRET") || 'your-jwt-secret-key',
  113. initialUsers: [],
  114. storagePrefix: '',
  115. userTable: 'users',
  116. fieldNames: {
  117. id: 'id',
  118. username: 'username',
  119. password: 'password',
  120. phone: 'phone',
  121. email: 'email',
  122. is_disabled: 'is_disabled',
  123. is_deleted: 'is_deleted'
  124. },
  125. tokenExpiry: 24 * 60 * 60,
  126. refreshTokenExpiry: 7 * 24 * 60 * 60
  127. })
  128. log.auth('Auth实例初始化完成')
  129. return authInstance
  130. } catch (error) {
  131. log.auth('Auth初始化失败:', error)
  132. throw error
  133. }
  134. }
  135. // 初始化系统设置
  136. const initSystemSettings = async (apiClient: APIClient) => {
  137. try {
  138. const systemSettings = await apiClient.database.table('system_settings')
  139. .select()
  140. // 将系统设置转换为键值对形式
  141. const settings = systemSettings.reduce((acc: Record<string, any>, setting: any) => {
  142. acc[setting.key] = setting.value
  143. return acc
  144. }, {}) as SystemSettingRecord
  145. // 更新全局配置
  146. if (settings[SystemSettingKey.SITE_NAME]) {
  147. GLOBAL_CONFIG.APP_NAME = String(settings[SystemSettingKey.SITE_NAME])
  148. }
  149. // 设置其他全局配置项
  150. if (settings[SystemSettingKey.SITE_FAVICON]) {
  151. GLOBAL_CONFIG.DEFAULT_THEME = String(settings[SystemSettingKey.SITE_FAVICON])
  152. }
  153. if (settings[SystemSettingKey.SITE_LOGO]) {
  154. GLOBAL_CONFIG.MAP_CONFIG.KEY = String(settings[SystemSettingKey.SITE_LOGO])
  155. }
  156. if (settings[SystemSettingKey.SITE_DESCRIPTION]) {
  157. GLOBAL_CONFIG.CHART_THEME = String(settings[SystemSettingKey.SITE_DESCRIPTION])
  158. }
  159. // 设置主题配置开关
  160. if (settings[SystemSettingKey.ENABLE_THEME_CONFIG]) {
  161. GLOBAL_CONFIG.ENABLE_THEME_CONFIG = settings[SystemSettingKey.ENABLE_THEME_CONFIG] === 'true'
  162. }
  163. // 查询ID1管理员的主题配置
  164. const adminTheme = await apiClient.database.table('theme_settings')
  165. .where('user_id', 1)
  166. .first()
  167. if (adminTheme) {
  168. GLOBAL_CONFIG.THEME = adminTheme.settings
  169. }
  170. return settings
  171. } catch (error) {
  172. log.app('获取系统设置失败:', error)
  173. return {} as SystemSettingRecord
  174. }
  175. }
  176. // 中间件:验证认证
  177. const withAuth = async (c: HonoContext<{ Variables: Variables }>, next: () => Promise<void>) => {
  178. try {
  179. const auth = c.get('auth')
  180. const token = c.req.header('Authorization')?.replace('Bearer ', '')
  181. if (token) {
  182. const userData = await auth.verifyToken(token)
  183. if (userData) {
  184. c.set('user', userData)
  185. await next()
  186. return
  187. }
  188. }
  189. return c.json({ error: '未授权' }, 401)
  190. } catch (error) {
  191. log.auth('认证失败:', error)
  192. return c.json({ error: '无效凭证' }, 401)
  193. }
  194. }
  195. // 导出withAuth类型定义
  196. export type WithAuth = typeof withAuth;
  197. // 定义模块参数接口
  198. interface ModuleParams {
  199. apiClient: APIClient
  200. app: Hono
  201. moduleDir: string
  202. }
  203. export default function({ apiClient, app, moduleDir }: ModuleParams) {
  204. const honoApp = app
  205. // 添加CORS中间件
  206. honoApp.use('/*', cors())
  207. // 创建API路由
  208. const api = new Hono<{ Variables: Variables }>()
  209. // // 使用数据库中间件
  210. // api.use('/*', withDatabase)
  211. // 设置环境变量
  212. api.use('*', async (c, next) => {
  213. c.set('apiClient', apiClient)
  214. c.set('moduleDir', moduleDir)
  215. c.set('auth', await initAuth(apiClient))
  216. c.set('systemSettings', await initSystemSettings(apiClient))
  217. await next()
  218. })
  219. // 查询仪表盘数据
  220. api.get('/dashboard', withAuth, async (c) => {
  221. try {
  222. const user = c.get('user')!
  223. const apiClient = c.get('apiClient')
  224. const lastLogin = await apiClient.database.table('login_history')
  225. .where('user_id', user.id)
  226. .orderBy('login_time', 'desc')
  227. .limit(1)
  228. .first()
  229. // 获取登录总次数
  230. const loginCount = await apiClient.database.table('login_history')
  231. .where('user_id', user.id)
  232. .count()
  233. // 获取系统数据统计
  234. const fileCount = await apiClient.database.table('file_library')
  235. .where('is_deleted', 0)
  236. .count()
  237. const userCount = await apiClient.database.table('users')
  238. .where('is_deleted', 0)
  239. .count()
  240. // 返回仪表盘数据
  241. const dashboardData: DashboardData = {
  242. lastLogin: lastLogin ? lastLogin.login_time : new Date().toISOString(),
  243. loginCount: loginCount,
  244. fileCount: Number(fileCount),
  245. userCount: Number(userCount),
  246. systemInfo: {
  247. version: '1.0.0',
  248. lastUpdate: new Date().toISOString()
  249. }
  250. }
  251. return c.json(dashboardData)
  252. } catch (error) {
  253. log.api('获取仪表盘数据失败:', error)
  254. return c.json({ error: '获取仪表盘数据失败' }, 500)
  255. }
  256. })
  257. // 注册基础路由
  258. api.route('/auth', createAuthRoutes(withAuth))
  259. api.route('/users', createUserRoutes(withAuth))
  260. api.route('/know-infos', createKnowInfoRoutes(withAuth))
  261. api.route('/upload', createFileUploadRoutes(withAuth)) // 添加文件上传路由
  262. api.route('/file-categories', createFileCategoryRoutes(withAuth)) // 添加文件分类管理路由
  263. api.route('/theme', createThemeRoutes(withAuth)) // 添加主题设置路由
  264. api.route('/charts', createChartRoutes(withAuth)) // 添加图表数据路由
  265. api.route('/map', createMapRoutes(withAuth)) // 添加地图数据路由
  266. api.route('/settings', createSystemSettingsRoutes(withAuth)) // 添加系统设置路由
  267. api.route('/messages', createMessagesRoutes(withAuth)) // 添加消息路由
  268. api.route('/migrations', createMigrationsRoutes(withAuth)) // 添加数据库迁移路由
  269. api.route('/home', createHomeRoutes(withAuth)) // 添加首页路由
  270. api.route('/classroom', createClassRoomRoutes(withAuth)) // 添加课堂路由
  271. // 注册API路由
  272. honoApp.route('/api', api)
  273. // 首页路由 - SSR
  274. honoApp.get('/', async (c: HonoContext) => {
  275. const systemName = GLOBAL_CONFIG.APP_NAME
  276. return c.html(
  277. <html>
  278. <head>
  279. <title>{systemName}</title>
  280. <meta charset="UTF-8" />
  281. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  282. <script src="https://cdn.tailwindcss.com"></script>
  283. </head>
  284. <body>
  285. <div className="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
  286. <div className="max-w-md w-full space-y-8">
  287. {/* 系统介绍区域 */}
  288. <div className="text-center">
  289. <h1 className="text-4xl font-bold text-gray-900 mb-4">
  290. {systemName}
  291. </h1>
  292. <p className="text-lg text-gray-600 mb-8">
  293. 全功能应用Starter
  294. </p>
  295. <p className="text-base text-gray-500 mb-8">
  296. 这是一个基于Hono和React的应用Starter,提供了用户认证、文件管理、图表分析、地图集成和主题切换等常用功能。
  297. </p>
  298. </div>
  299. {/* 管理入口按钮 */}
  300. <div className="space-y-4">
  301. <a
  302. href="/admin"
  303. 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"
  304. >
  305. 进入管理后台
  306. </a>
  307. {/* 移动端入口按钮 */}
  308. <a
  309. href="/mobile"
  310. className="w-full flex justify-center py-3 px-4 border border-blue-600 rounded-md shadow-sm text-lg font-medium text-blue-600 bg-white hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
  311. >
  312. 进入移动端
  313. </a>
  314. {/* 迁移管理入口按钮 */}
  315. <a
  316. href="/migrations"
  317. className="w-full flex justify-center py-3 px-4 border border-red-600 rounded-md shadow-sm text-lg font-medium text-red-600 bg-white hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
  318. >
  319. 数据库迁移
  320. </a>
  321. {/* 股票训练入口按钮 */}
  322. <a
  323. href="/stock"
  324. className="w-full flex justify-center py-3 px-4 border border-blue-600 rounded-md shadow-sm text-lg font-medium text-blue-600 bg-white hover:bg-blue-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
  325. >
  326. 进入股票训练
  327. </a>
  328. </div>
  329. </div>
  330. </div>
  331. </body>
  332. </html>
  333. )
  334. })
  335. // 创建一个函数,用于生成包含全局配置的HTML页面
  336. const createHtmlWithConfig = (scriptConfig: EsmScriptConfig, title = '应用Starter') => {
  337. return (c: HonoContext) => {
  338. const isProd = GLOBAL_CONFIG.ENV === 'production';
  339. const isLocalDeploy = Deno.env.get('IS_LOCAL_DEPLOY') === 'true';
  340. return c.html(
  341. <html lang="zh-CN">
  342. <head>
  343. <meta charset="UTF-8" />
  344. <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  345. {isProd && <meta name="version" content={Deno.env.get('VERSION') || '0.1.0'} />}
  346. <title>{title}</title>
  347. {isLocalDeploy ? (
  348. <script type="module" src={scriptConfig.prodSrc || `/client_dist/${scriptConfig.prodPath}`}></script>
  349. ) : (
  350. <script src={scriptConfig.src} href={scriptConfig.href} deno-json={scriptConfig.denoJson}
  351. {...isProd ? {}:{ refresh: true }}
  352. ></script>
  353. )}
  354. {isLocalDeploy ? (<script src="/tailwindcss@3.4.16/index.js"></script>) : (<script src="https://cdn.tailwindcss.com"></script>)}
  355. <script dangerouslySetInnerHTML={{ __html: `window.CONFIG = ${JSON.stringify(GLOBAL_CONFIG)};` }} />
  356. {!isProd && (
  357. <>
  358. <script src="https://ai-oss.d8d.fun/umd/vconsole.3.15.1.min.js"></script>
  359. <script dangerouslySetInnerHTML={{ __html: `
  360. const urlParams = new URLSearchParams(window.location.search);
  361. if (urlParams.has('vconsole')) {
  362. var vConsole = new VConsole({
  363. theme: urlParams.get('vconsole_theme') || 'light',
  364. onReady: function() {
  365. console.log('vConsole is ready');
  366. }
  367. });
  368. }
  369. `}} />
  370. </>
  371. )}
  372. <script src="https://g.alicdn.com/apsara-media-box/imp-interaction/1.6.1/alivc-im.iife.js"></script>
  373. </head>
  374. <body className="bg-gray-50">
  375. <div id="root"></div>
  376. </body>
  377. </html>
  378. )
  379. }
  380. }
  381. // 后台管理路由
  382. honoApp.get('/admin', createHtmlWithConfig({
  383. src: "https://esm.d8d.fun/xb",
  384. href: "/client/admin/web_app.tsx",
  385. denoJson: "/deno.json",
  386. refresh: true,
  387. prodPath: "admin/web_app.js"
  388. }, GLOBAL_CONFIG.APP_NAME))
  389. honoApp.get('/admin/*', createHtmlWithConfig({
  390. src: "https://esm.d8d.fun/xb",
  391. href: "/client/admin/web_app.tsx",
  392. denoJson: "/deno.json",
  393. refresh: true,
  394. prodPath: "admin/web_app.js"
  395. }, GLOBAL_CONFIG.APP_NAME))
  396. // 移动端路由
  397. honoApp.get('/mobile', createHtmlWithConfig({
  398. src: "https://esm.d8d.fun/xb",
  399. href: "/client/mobile/mobile_app.tsx",
  400. denoJson: "/deno.json",
  401. refresh: true,
  402. prodPath: "mobile/mobile_app.js"
  403. }, GLOBAL_CONFIG.APP_NAME))
  404. honoApp.get('/mobile/*', createHtmlWithConfig({
  405. src: "https://esm.d8d.fun/xb",
  406. href: "/client/mobile/mobile_app.tsx",
  407. denoJson: "/deno.json",
  408. refresh: true,
  409. prodPath: "mobile/mobile_app.js"
  410. }, GLOBAL_CONFIG.APP_NAME))
  411. // 迁移管理路由
  412. honoApp.get('/migrations', createHtmlWithConfig({
  413. src: "https://esm.d8d.fun/xb",
  414. href: "/client/migrations/migrations_app.tsx",
  415. denoJson: "/deno.json",
  416. refresh: true,
  417. prodPath: "migrations/migrations_app.js"
  418. }, GLOBAL_CONFIG.APP_NAME))
  419. honoApp.get('/migrations/*', createHtmlWithConfig({
  420. src: "https://esm.d8d.fun/xb",
  421. href: "/client/migrations/migrations_app.tsx",
  422. denoJson: "/deno.json",
  423. refresh: true,
  424. prodPath: "migrations/migrations_app.js"
  425. }, GLOBAL_CONFIG.APP_NAME))
  426. honoApp.get('/stock/*', createHtmlWithConfig({
  427. src: "https://esm.d8d.fun/xb",
  428. href: "/client/stock/stock_app.tsx",
  429. denoJson: "/deno.json",
  430. refresh: true,
  431. prodPath: "stock/stock_app.js"
  432. }, GLOBAL_CONFIG.APP_NAME))
  433. const staticRoutes = serveStatic({
  434. root: moduleDir,
  435. onFound: async (path: string, c: HonoContext) => {
  436. const fileExt = path.split('.').pop()?.toLowerCase()
  437. if (fileExt === 'tsx' || fileExt === 'ts') {
  438. c.header('Content-Type', 'text/typescript; charset=utf-8')
  439. } else if (fileExt === 'js' || fileExt === 'mjs') {
  440. c.header('Content-Type', 'application/javascript; charset=utf-8')
  441. } else if (fileExt === 'json') {
  442. c.header('Content-Type', 'application/json; charset=utf-8')
  443. } else if (fileExt === 'html') {
  444. c.header('Content-Type', 'text/html; charset=utf-8')
  445. } else if (fileExt === 'css') {
  446. c.header('Content-Type', 'text/css; charset=utf-8')
  447. } else if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(fileExt || '')) {
  448. c.header('Content-Type', `image/${fileExt}`)
  449. }
  450. const fileInfo = await Deno.stat(path)
  451. c.header('Last-Modified', fileInfo.mtime?.toUTCString() ?? new Date().toUTCString())
  452. },
  453. })
  454. // 静态资源路由
  455. honoApp.get('/deno.json', staticRoutes)
  456. honoApp.get('/client/*', staticRoutes)
  457. honoApp.get('/amap/*', staticRoutes)
  458. honoApp.get('/tailwindcss@3.4.16/*', staticRoutes)
  459. honoApp.get('/client_dist/*', staticRoutes)
  460. return honoApp
  461. }