From a2cee7c27afdad03daf014a6ed5ac48323533835 Mon Sep 17 00:00:00 2001 From: abearxiong Date: Mon, 24 Nov 2025 18:58:32 +0800 Subject: [PATCH] update --- backend/bun.config.mjs | 21 + backend/package.json | 24 +- backend/readme.md | 6 +- backend/src/app.ts | 2 +- backend/src/index.ts | 4 + backend/src/main.ts | 13 +- backend/src/module/db.ts | 5 +- backend/src/module/schema.ts | 4 + backend/src/router/daily-task.ts | 31 +- backend/src/router/daily/index.ts | 34 +- backend/src/router/index.ts | 15 +- backend/src/router/library-task.ts | 3 +- backend/src/router/library/index.ts | 12 +- backend/zip.ts | 93 +++++ frontend/copy.ts | 34 ++ frontend/package.json | 3 + frontend/src/apps/daily/index.tsx | 12 +- frontend/src/apps/footer.tsx | 50 +++ frontend/src/apps/library/index.tsx | 9 +- frontend/src/apps/today/index.tsx | 23 +- frontend/src/apps/today/store.ts | 13 +- frontend/src/data/docs/01-frontend.md | 2 +- frontend/src/data/docs/02-downlocal.md | 16 + frontend/src/lib/json.ts | 12 + frontend/src/modules/basename.ts | 10 +- frontend/src/modules/query.ts | 11 +- frontend/src/pages/docs/index.astro | 5 +- pnpm-lock.yaml | 545 +++++++++++++++++++++++-- 28 files changed, 931 insertions(+), 81 deletions(-) create mode 100644 backend/bun.config.mjs create mode 100644 backend/src/index.ts create mode 100644 backend/zip.ts create mode 100644 frontend/copy.ts create mode 100644 frontend/src/apps/footer.tsx create mode 100644 frontend/src/data/docs/02-downlocal.md create mode 100644 frontend/src/lib/json.ts diff --git a/backend/bun.config.mjs b/backend/bun.config.mjs new file mode 100644 index 0000000..5abf321 --- /dev/null +++ b/backend/bun.config.mjs @@ -0,0 +1,21 @@ +// @ts-check +import { resolvePath } from '@kevisual/use-config'; +import { execSync } from 'node:child_process'; + +const entry = 'src/index.ts'; +const naming = 'app'; +const external = ['pm2']; +/** + * @type {import('bun').BuildConfig} + */ +await Bun.build({ + target: 'node', + format: 'esm', + entrypoints: [resolvePath(entry, { meta: import.meta })], + outdir: resolvePath('./dist', { meta: import.meta }), + naming: { + entry: `${naming}.js`, + }, + external, + env: 'KEVISUAL_*', +}); \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index eca135e..edbf4a6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -3,12 +3,27 @@ "version": "0.0.1", "description": "", "main": "index.js", + "basename": "/root/daily-question-server", + "app": { + "type": "system-app", + "key": "daily-question-server", + "entry": "app.js", + "runtime": [ + "server" + ] + }, + "files": [ + "dist" + ], "scripts": { "dev": "bun --watch src/main.ts ", - "build": "rimraf dist && bun run bun.config.mjs", - "compile": "bun build --compile ./src/main.ts --outfile myapp", - "compile:win": "bun build --compile ./src/main.ts --target=bun-windows-x64 --outfile myapp.exe", + "build": "rimraf dist && rimraf pack-dist && bun run bun.config.mjs", + "postbuild": "ev pack", + "compile": "bun build --compile ./src/main.ts --outfile daily-question", + "compile:win": "bun build --compile ./src/main.ts --target=bun-windows-x64 --outfile daily-question.exe", + "postcompile:win": "bun zip.ts", "clean": "rm -rf dist", + "sc": "rimraf *.zip && rimraf *.exe", "pub": "envision pack -p -u" }, "keywords": [], @@ -18,7 +33,7 @@ "type": "module", "dependencies": { "@kevisual/ai": "^0.0.12", - "@kevisual/local-proxy": "^0.0.6", + "@kevisual/local-proxy": "^0.0.8", "@kevisual/query": "^0.0.29", "@kevisual/router": "0.0.33", "@kevisual/use-config": "^1.0.21", @@ -34,6 +49,7 @@ "@types/better-sqlite3": "^7.6.13", "@types/bun": "^1.3.3", "@types/node": "^24.10.1", + "archiver": "^7.0.1", "drizzle-kit": "^0.31.7" } } \ No newline at end of file diff --git a/backend/readme.md b/backend/readme.md index 9879c66..0ecebea 100644 --- a/backend/readme.md +++ b/backend/readme.md @@ -1,4 +1,8 @@ -## 后端功能 +## 使用方法 + +运行exe后,浏览器访问 http://localhost:9000/ + +## 后端功能提示词 使用db,better-sqite3 diff --git a/backend/src/app.ts b/backend/src/app.ts index 4e71f58..5a32ef2 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,6 +1,6 @@ import { App } from "@kevisual/router"; import { useContextKey } from "@kevisual/use-config/context"; -export const app = useContextKey('app', new App({ +export const app: App = useContextKey('app', new App({ serverOptions: { path: '/client/router' } diff --git a/backend/src/index.ts b/backend/src/index.ts new file mode 100644 index 0000000..f0a6d58 --- /dev/null +++ b/backend/src/index.ts @@ -0,0 +1,4 @@ +import { app } from './app.ts' +import './router/index.ts'; + +export { app } \ No newline at end of file diff --git a/backend/src/main.ts b/backend/src/main.ts index d256a3c..e0e2bc8 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -1,16 +1,15 @@ import { app } from './app.ts' -import './router/daily/index.ts'; -import './router/library/index.ts'; import './router/index.ts'; + import { proxyRoute, initProxy } from '@kevisual/local-proxy/proxy.ts'; -initProxy({ - pagesDir: './public', - watch: true, - home: '/root/daily-question' -}); export { app } if (require.main === module) { + initProxy({ + pagesDir: './public', + watch: true, + home: '/root/daily-question' + }); app.listen(9000, () => { console.log('Server is running on http://localhost:9000'); }); diff --git a/backend/src/module/db.ts b/backend/src/module/db.ts index e9cfe4d..27d8c80 100644 --- a/backend/src/module/db.ts +++ b/backend/src/module/db.ts @@ -23,7 +23,6 @@ export const getDb = () => { export const init = async () => { console.log('Database path:', dbPath); const database = getDb(); - // 使用 Drizzle ORM 创建表 (如果不存在) if (sqliteDb) { sqliteDb.run(` CREATE TABLE IF NOT EXISTS question_library ( @@ -34,6 +33,8 @@ export const init = async () => { repeat INTEGER DEFAULT 0, isUse INTEGER DEFAULT 0, usedAt TEXT, + uid TEXT, + data TEXT, createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL ) @@ -49,6 +50,8 @@ export const init = async () => { createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL, date TEXT NOT NULL, + uid TEXT, + data TEXT, qid TEXT, FOREIGN KEY (qid) REFERENCES question_library(id) ) diff --git a/backend/src/module/schema.ts b/backend/src/module/schema.ts index e9ed0b9..44059dc 100644 --- a/backend/src/module/schema.ts +++ b/backend/src/module/schema.ts @@ -12,8 +12,10 @@ export const questionLibrary = sqliteTable( repeat: integer({ mode: 'boolean' }).default(false), isUse: integer({ mode: 'boolean' }).default(false), usedAt: text(), // YYYY-MM-DD format + uid: text().notNull().default(''), // 用户ID,默认系统用户 createdAt: text().notNull().default(new Date().toISOString()), updatedAt: text().notNull().default(new Date().toISOString()).$onUpdate(() => new Date().toISOString()), + data: text(), // JSON string }, (table) => ({ isUseIdx: index('idx_library_isUse').on(table.isUse), @@ -33,7 +35,9 @@ export const dailyQuestions = sqliteTable( createdAt: text().notNull().default(new Date().toISOString()), updatedAt: text().notNull().default(new Date().toISOString()).$onUpdate(() => new Date().toISOString()), date: text().notNull(), // YYYY-MM-DD format + uid: text().notNull().default(''), // 用户ID,默认系统用户 qid: text().references(() => questionLibrary.id), + data: text(), // JSON string }, (table) => ({ dateIdx: index('idx_daily_date').on(table.date), diff --git a/backend/src/router/daily-task.ts b/backend/src/router/daily-task.ts index 70fa60b..534a1c2 100644 --- a/backend/src/router/daily-task.ts +++ b/backend/src/router/daily-task.ts @@ -2,12 +2,13 @@ import { generateId, getTodayDate } from '../module/utils.ts'; import { app } from '../app.ts'; import { getDb } from '../module/db.ts'; import { dailyQuestions, questionLibrary } from '../module/schema.ts'; -import { eq } from 'drizzle-orm'; +import { desc, eq } from 'drizzle-orm'; app.route({ path: 'daily', key: 'random', - description: '随机获取一条未使用的问题' + description: '随机获取一条未使用的问题', + middleware: ['auth'] }).define(async (ctx) => { const force = ctx.query.force ?? false; const db = getDb(); @@ -37,14 +38,18 @@ app.route({ .from(questionLibrary) .where(eq(questionLibrary.isUse, false)); + let selectedQuestion; if (unusedQuestions.length === 0) { - ctx.throw(404, '没有未使用的问题'); - return; + selectedQuestion = { + title: '今天发生了什么有趣的事情?', + description: '请描述一下你今天经历的有趣事件,分享你的感受和想法。', + tags: JSON.stringify(['生活', '分享']), + } + } else { + const randomIndex = Math.floor(Math.random() * unusedQuestions.length); + selectedQuestion = unusedQuestions[randomIndex]; } - const randomIndex = Math.floor(Math.random() * unusedQuestions.length); - const selectedQuestion = unusedQuestions[randomIndex]; - // 更新questionLibrary中的isUse和usedAt字段 await db .update(questionLibrary) @@ -60,8 +65,9 @@ app.route({ .insert(dailyQuestions) .values({ id: generateId(), - qid: selectedQuestion.id, + qid: selectedQuestion.id || '', title: selectedQuestion.title, + summary: selectedQuestion?.description, description: '', tags: selectedQuestion.tags, date: day, @@ -78,11 +84,12 @@ app.route({ app.route({ description: '获取今天的问题', path: 'daily', - key: 'today' + key: 'today', + middleware: ['auth'] }).define(async (ctx) => { const db = getDb(); const day = getTodayDate(); - + const token = ctx.query?.token; try { const todayQuestion = await db .select() @@ -95,11 +102,15 @@ app.route({ const res = await ctx.call({ path: 'daily', key: 'random', + payload: { + token: token, + } }); if (res.code === 200) { ctx.body = res.body; return; } + console.error(res.message); ctx.throw(500, '获取今天的问题失败'); } diff --git a/backend/src/router/daily/index.ts b/backend/src/router/daily/index.ts index 6f872f9..12024ae 100644 --- a/backend/src/router/daily/index.ts +++ b/backend/src/router/daily/index.ts @@ -2,22 +2,41 @@ import { generateId } from '../../module/utils.ts'; import { app } from '../../app.ts'; import { getDb } from '../../module/db.ts'; import { dailyQuestions } from '../../module/schema.ts'; -import { eq } from 'drizzle-orm'; +import { eq, and, or, like } from 'drizzle-orm'; // 列出每日问题 app.route({ description: '列出每日问题', path: 'daily', - key: 'list' + key: 'list', + middleware: ['auth'] }).define(async (ctx) => { const query = ctx.query; const page = query.page ?? 1; const pageSize = query.pageSize ?? 99999; const db = getDb(); + const search = query.search ?? ''; + const id = query.id ?? ''; try { const offset = (page - 1) * pageSize; - const allResults = await db.select().from(dailyQuestions); + const allResults = await db.select().from(dailyQuestions).where(() => { + const conditions = []; + + if (search) { + conditions.push(like(dailyQuestions.title, `%${search}%`)); + } + + if (id) { + conditions.push(eq(dailyQuestions.id, id)); + } + + // 如果需要 OR 逻辑(search 或 id 任一匹配) + return conditions.length > 0 ? or(...conditions) : undefined; + + // 如果需要 AND 逻辑(search 和 id 都要匹配) + // return conditions.length > 0 ? and(...conditions) : undefined; + }); // Sort by createdAt in descending order (newest first) const sortedResults = allResults.sort((a, b) => @@ -47,7 +66,8 @@ app.route({ app.route({ description: '更新每日问题', path: 'daily', - key: 'update' + key: 'update', + middleware: ['auth'] }).define(async (ctx) => { const query = ctx.query; const id = query.id; @@ -102,7 +122,8 @@ app.route({ app.route({ description: '删除每日问题', path: 'daily', - key: 'delete' + key: 'delete', + middleware: ['auth'] }).define(async (ctx) => { const query = ctx.query; const id = query.id; @@ -128,7 +149,8 @@ app.route({ app.route({ description: '获取每日问题详情', path: 'daily', - key: 'detail' + key: 'detail', + middleware: ['auth'] }).define(async (ctx) => { const query = ctx.query; const id = query.id; diff --git a/backend/src/router/index.ts b/backend/src/router/index.ts index a2b15d6..9f4d014 100644 --- a/backend/src/router/index.ts +++ b/backend/src/router/index.ts @@ -1,4 +1,17 @@ import './daily/index.ts' import './library/index.ts' import './daily-task.ts' -import './library-task.ts' \ No newline at end of file +import './library-task.ts' +import { app } from '../app.ts'; +const hasAuth = app.router.routes.some(r => r.id === 'auth'); +if (!hasAuth) { + console.log('添加认证中间件路由'); + app.route({ + path: 'auth', + key: 'auth', + description: '用户认证', + id: 'auth' + }).define(async (ctx) => { + // 这里可以添加实际的认证逻辑 + }).addTo(app); +} \ No newline at end of file diff --git a/backend/src/router/library-task.ts b/backend/src/router/library-task.ts index 6ea25e3..158d45c 100644 --- a/backend/src/router/library-task.ts +++ b/backend/src/router/library-task.ts @@ -7,7 +7,8 @@ import { eq } from 'drizzle-orm'; app.route({ path: 'library', key: 'setAllUnused', - description: '将所有问题设置为未使用' + description: '将所有问题设置为未使用', + middleware: ['auth'] }).define(async (ctx) => { const db = getDb(); try { diff --git a/backend/src/router/library/index.ts b/backend/src/router/library/index.ts index 6ae3c14..f25396c 100644 --- a/backend/src/router/library/index.ts +++ b/backend/src/router/library/index.ts @@ -8,7 +8,8 @@ import { eq } from 'drizzle-orm'; app.route({ description: '列出问题库', path: 'library', - key: 'list' + key: 'list', + middleware: ['auth'] }).define(async (ctx) => { const query = ctx.query; const page = query.page ?? 1; @@ -47,7 +48,8 @@ app.route({ app.route({ description: '更新问题库', path: 'library', - key: 'update' + key: 'update', + middleware: ['auth'] }).define(async (ctx) => { const query = ctx.query; const id = query.id; @@ -104,7 +106,8 @@ app.route({ app.route({ description: '删除问题库', path: 'library', - key: 'delete' + key: 'delete', + middleware: ['auth'] }).define(async (ctx) => { const query = ctx.query; const id = query.id; @@ -130,7 +133,8 @@ app.route({ app.route({ description: '获取问题库详情', path: 'library', - key: 'detail' + key: 'detail', + middleware: ['auth'] }).define(async (ctx) => { const query = ctx.query; const id = query.id; diff --git a/backend/zip.ts b/backend/zip.ts new file mode 100644 index 0000000..87dd5ed --- /dev/null +++ b/backend/zip.ts @@ -0,0 +1,93 @@ +import archiver from 'archiver'; +import fs from 'fs'; +import path from 'path'; +import * as pkgs from './package.json'; +/** + * 创建 ZIP 压缩包 + * @param outputPath - 输出的 zip 文件路径 + * @param files - 要压缩的文件列表 + * @param directories - 要压缩的目录列表 + */ +export async function createZip( + outputPath: string, + files: string[] = [], + directories: string[] = [] +): Promise { + return new Promise((resolve, reject) => { + // 创建输出流 + const output = fs.createWriteStream(outputPath); + const archive = archiver('zip', { + zlib: { level: 9 } // 最高压缩级别 + }); + + // 监听完成事件 + output.on('close', () => { + console.log(`✅ 压缩完成: ${archive.pointer()} bytes`); + console.log(`📦 输出文件: ${outputPath}`); + resolve(); + }); + + // 监听警告 + archive.on('warning', (err) => { + if (err.code === 'ENOENT') { + console.warn('⚠️ 警告:', err); + } else { + reject(err); + } + }); + + // 监听错误 + archive.on('error', (err) => { + reject(err); + }); + + // 连接输出流 + archive.pipe(output); + + // 添加文件 + for (const file of files) { + if (fs.existsSync(file)) { + archive.file(file, { name: path.basename(file) }); + console.log(`📄 添加文件: ${file}`); + } else { + console.warn(`⚠️ 文件不存在: ${file}`); + } + } + + // 添加目录 + for (const dir of directories) { + if (fs.existsSync(dir)) { + archive.directory(dir, path.basename(dir)); + console.log(`📁 添加目录: ${dir}`); + } else { + console.warn(`⚠️ 目录不存在: ${dir}`); + } + } + + // 完成归档 + archive.finalize(); + }); +} + +/** + * 为项目创建发布包 + */ +export async function createReleaseZip(): Promise { + const files = [ + 'daily-question.exe', + 'readme.md' + ]; + + const directories = [ + 'public' + ]; + + await createZip(`daily-question-${pkgs.version}.zip`, files, directories); +} + +// 如果直接运行此文件 +if (import.meta.main) { + createReleaseZip() + .then(() => console.log('🎉 打包成功!')) + .catch((err) => console.error('❌ 打包失败:', err)); +} \ No newline at end of file diff --git a/frontend/copy.ts b/frontend/copy.ts new file mode 100644 index 0000000..31c27b0 --- /dev/null +++ b/frontend/copy.ts @@ -0,0 +1,34 @@ +/** + * 将 dist 目录的内容复制到 backend/public 目录, 使用bun运行 + * + * @tags build, deploy, copy + * @description 构建后将前端静态资源复制到后端公共目录,复制前会删除已有内容 + * @title 前端资源复制脚本 + * @createdAt 2025-11-24 + */ +import { existsSync, rmSync, cpSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const distPath = resolve(__dirname, 'dist'); +const backendPath = resolve(__dirname, '../backend/public'); +const frontendPath = resolve(backendPath, 'root/daily-question'); +// 删除已有的 backend/public 目录内容 +if (existsSync(backendPath)) { + console.log('删除已有的 backend/public 目录...'); + rmSync(backendPath, { recursive: true, force: true }); + console.log('✓ 已删除旧内容'); +} + +// 复制 dist 内容到 backend/public +if (existsSync(distPath)) { + console.log('复制 dist 内容到 backend/public...'); + cpSync(distPath, frontendPath, { recursive: true }); + console.log('✓ 复制完成'); +} else { + console.error('错误: dist 目录不存在,请先运行构建命令'); + process.exit(1); +} diff --git a/frontend/package.json b/frontend/package.json index 9f23124..7c6b454 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,6 +7,7 @@ "scripts": { "dev": "astro dev", "build": "astro build", + "postbuild": "bun copy.ts", "preview": "astro preview", "pub": "envision deploy ./dist -k daily-question -v 0.0.1 -u", "ui": "pnpm dlx shadcn@latest add " @@ -48,11 +49,13 @@ }, "devDependencies": { "@kevisual/types": "^0.0.10", + "@types/node": "^24.10.1", "@types/react": "^19.2.6", "@types/react-dom": "^19.2.3", "@vitejs/plugin-basic-ssl": "^2.1.0", "dotenv": "^17.2.3", "tailwindcss": "^4.1.17", + "tsx": "^4.20.6", "tw-animate-css": "^1.4.0" }, "packageManager": "pnpm@10.23.0", diff --git a/frontend/src/apps/daily/index.tsx b/frontend/src/apps/daily/index.tsx index 1aa77d1..2278087 100644 --- a/frontend/src/apps/daily/index.tsx +++ b/frontend/src/apps/daily/index.tsx @@ -5,6 +5,8 @@ import { useShallow } from 'zustand/shallow'; import { useEffect } from 'react'; import { AppModal } from './modal.tsx' import dayjs from 'dayjs'; +import { Footer } from '../footer.tsx'; +import { exportJson } from '@/lib/json.ts'; const render = (dateStr: string) => { return dayjs(dateStr).format('YYYY-MM-DD HH:mm:ss'); @@ -22,6 +24,10 @@ export const App = () => { store.getList(); }, []) const columns = [ + { + title: 'ID', + dataIndex: 'id', + }, { title: '日期', dataIndex: 'date', @@ -80,7 +86,6 @@ export const App = () => { @@ -96,6 +101,7 @@ export const AppProvider = () => { +