diff --git a/.gitea/workflows/README.md b/.gitea/workflows/README.md new file mode 100644 index 0000000..b574c20 --- /dev/null +++ b/.gitea/workflows/README.md @@ -0,0 +1,47 @@ +# Gitea Actions 部署配置说明 + +## 前提条件 + +- Gitea Runner 已配置为 `self-hosted` 模式,直接在宿主机上运行 +- 宿主机上已安装 `bun` 和 `pm2` +- Runner 用户有权限访问 `/home/score-backend` 目录 + +## 工作流说明 + +- **触发条件**: + - 推送到 `dev` 或 `main` 分支时自动触发 + - 也可以手动触发(workflow_dispatch) + +- **部署步骤**: + 1. 检出代码到 runner 工作目录 + 2. 使用 rsync 同步文件到 `/home/score-backend`(排除不需要的文件) + 3. 在目标目录执行 `bun install` 安装依赖 + 4. 使用 `pm2 restart media-backend` 重启应用(如果应用不存在则启动) + +## 文件排除规则 + +以下文件/目录不会被同步到服务器: +- `.git` - Git 仓库文件 +- `node_modules` - 依赖包(会在服务器上重新安装) +- `.DS_Store` - macOS 系统文件 +- `*.db` - 数据库文件 +- `bun.lockb` - Bun 锁文件 +- `.env` / `.env.local` - 环境变量文件(保留服务器上的配置) +- `.gitea` - Gitea 工作流文件 + +## PM2 配置 + +如果 PM2 中还没有 `media-backend` 应用,工作流会自动创建。你也可以手动配置: + +```bash +cd /home/score-backend +pm2 start main.ts --name media-backend +pm2 save +pm2 startup # 设置开机自启 +``` + +## 注意事项 + +- 确保 Runner 用户有写入 `/home/score-backend` 的权限 +- 确保环境变量在服务器上已正确配置(不会覆盖 `.env` 文件) +- 数据库文件不会被覆盖,确保数据安全 diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..4dc4298 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,42 @@ +name: Deploy to Server + +on: + push: + branches: + - dev + - main + workflow_dispatch: + +jobs: + deploy: + runs-on: host + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Copy files to deployment directory + run: | + # 创建目标目录(如果不存在) + mkdir -p /home/score-backend + + # 使用 rsync 同步文件(排除不需要的文件) + rsync -av \ + --exclude='.git' \ + --exclude='node_modules' \ + --exclude='.DS_Store' \ + --exclude='*.db' \ + --exclude='bun.lockb' \ + --exclude='.env' \ + --exclude='.env.local' \ + --exclude='.gitea' \ + ./ /home/score-backend/ + + - name: Install dependencies + working-directory: /home/score-backend + run: | + bun install + + - name: Restart PM2 application + run: | + cd /home/score-backend + pm2 restart media-backend || pm2 start main.ts --name media-backend diff --git a/.gitignore b/.gitignore index e3568c1..b2b72fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ .vscode .DS_Store **/*.db +node_modules/ +bun.lockb +.env +.env.local \ No newline at end of file diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..69cad86 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,137 @@ +# SQLite 到 PostgreSQL 迁移指南 + +本文档说明如何将项目从 SQLite 迁移到 PostgreSQL。 + +## 主要变更 + +### 1. 依赖更新 +- 添加了 `postgres` 库用于 PostgreSQL 连接 +- 移除了 `node:sqlite` 依赖 + +### 2. 数据库连接 +数据库连接现在通过环境变量配置,支持以下方式: + +**方式一:使用 DATABASE_URL** +```bash +export DATABASE_URL="postgresql://user:password@localhost:5432/dbname" +``` + +**方式二:使用独立的环境变量** +```bash +export DB_USER=postgres +export DB_PASSWORD=postgres +export DB_HOST=localhost +export DB_PORT=5432 +export DB_NAME=media +``` + +### 3. SQL 语法变更 + +#### 占位符 +- **SQLite**: 使用 `?` 作为占位符 +- **PostgreSQL**: 使用 `$1, $2, $3...` 作为占位符 +- **解决方案**: 代码中已创建适配器自动转换 + +#### 日期时间函数 +- **SQLite**: `datetime('now')` +- **PostgreSQL**: `NOW()` 或 `CURRENT_TIMESTAMP` +- **已更新**: 所有 `datetime('now')` 已替换为 `NOW()` + +#### 获取插入的 ID +- **SQLite**: `result.lastInsertRowid` +- **PostgreSQL**: 使用 `RETURNING id` 子句 +- **已更新**: INSERT 语句已添加 `RETURNING id` + +### 4. 数据库 Schema + +需要创建以下表结构: + +#### media 表 +```sql +CREATE TABLE media ( + id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + type VARCHAR(50) NOT NULL, + rating INTEGER, + notes TEXT, + platform VARCHAR(100), + date DATE, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); +``` + +#### users 表 +```sql +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + username VARCHAR(100) UNIQUE NOT NULL, + password VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT NOW() +); +``` + +## 迁移步骤 + +### 1. 安装 PostgreSQL +确保已安装并运行 PostgreSQL 数据库。 + +### 2. 创建数据库 +```bash +createdb media +# 或使用 psql +psql -U postgres +CREATE DATABASE media; +``` + +### 3. 创建表结构 +使用上面提供的 SQL 语句创建表。 + +### 4. 迁移数据(可选) +如果需要从 SQLite 迁移现有数据,可以使用以下方法: + +```bash +# 导出 SQLite 数据 +sqlite3 media.db .dump > data.sql + +# 手动转换并导入到 PostgreSQL +# 注意:需要修改 SQL 语法以兼容 PostgreSQL +``` + +### 5. 配置环境变量 +设置数据库连接信息(见上面的环境变量配置)。 + +### 6. 测试连接 +运行应用并测试数据库连接是否正常。 + +## 注意事项 + +1. **数据类型差异**: + - SQLite 的 `INTEGER` 对应 PostgreSQL 的 `INTEGER` 或 `SERIAL` + - SQLite 的 `TEXT` 对应 PostgreSQL 的 `VARCHAR` 或 `TEXT` + - SQLite 的 `REAL` 对应 PostgreSQL 的 `REAL` 或 `DOUBLE PRECISION` + +2. **事务处理**: + - PostgreSQL 支持更完善的事务和并发控制 + - 代码中的适配器已处理基本的异步操作 + +3. **性能优化**: + - PostgreSQL 支持索引优化 + - 建议为常用查询字段添加索引: + ```sql + CREATE INDEX idx_media_type ON media(type); + CREATE INDEX idx_media_rating ON media(rating); + CREATE INDEX idx_media_date ON media(date); + ``` + +4. **连接池**: + - `postgres` 库默认使用连接池 + - 可以通过配置选项调整连接池大小 + +## 回滚方案 + +如果需要回滚到 SQLite: +1. 恢复 `db/index.ts` 中的 SQLite 连接代码 +2. 恢复所有 SQL 查询中的 `datetime('now')` +3. 移除 INSERT 语句中的 `RETURNING id` +4. 更新 `deno.json` 移除 postgres 依赖 diff --git a/README.md b/README.md index d29ae32..87999f1 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,49 @@ +## 安装依赖 + +```bash +bun install ``` -deno task start + +## 运行项目 + +```bash +# 开发模式(带热重载) +bun run dev + +# 生产模式 +bun run start ``` + +## 数据库初始化 + +首次运行前,需要初始化数据库: + +```bash +# 方式一:使用初始化脚本(推荐,会自动创建数据库和表) +bun run init-db + +# 方式二:手动创建数据库和表 +# 1. 创建数据库 +createdb media +# 或使用 psql +psql -U postgres -c "CREATE DATABASE media;" + +# 2. 创建表结构 +psql -U postgres -d media -f db/schema.sql +``` + +## 环境变量 + +配置以下环境变量: + +- `PORT`: 服务器端口(默认: 8000) +- `AUTH_SECRET`: JWT 密钥(默认: it-is-a-secret) +- `DATABASE_URL`: PostgreSQL 连接字符串 +- 或使用独立变量: + - `DB_USER`: 数据库用户(默认: postgres) + - `DB_PASSWORD`: 数据库密码(默认: postgres) + - `DB_HOST`: 数据库主机(默认: localhost) + - `DB_PORT`: 数据库端口(默认: 5432) + - `DB_NAME`: 数据库名称(默认: media) + +**注意**:应用启动时会自动检查并创建表结构(如果不存在),但不会自动创建数据库。请确保数据库已存在。 diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..e413970 --- /dev/null +++ b/bun.lock @@ -0,0 +1,29 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "my-score-backend", + "dependencies": { + "hono": "^4.7.11", + "postgres": "^3.4.3", + }, + "devDependencies": { + "@types/bun": "latest", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], + + "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + + "bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], + + "hono": ["hono@4.11.3", "", {}, "sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w=="], + + "postgres": ["postgres@3.4.8", "", {}, "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + } +} diff --git a/db/index.ts b/db/index.ts index 2225144..9aa1b43 100644 --- a/db/index.ts +++ b/db/index.ts @@ -4,13 +4,143 @@ * @LastEditTime: 2025-06-13 14:23:03 * @FilePath: /my-score/honoback/db/index.ts */ -import { DatabaseSync } from "node:sqlite" -import { join, dirname } from 'node:path' -import { fileURLToPath } from 'node:url' +import postgres from "postgres"; +import { readFileSync } from "fs"; +import { join, dirname } from "path"; -const __dirname = dirname(fileURLToPath(import.meta.url)) +// 获取当前文件所在目录 +const __dirname = + typeof import.meta.dir !== "undefined" + ? import.meta.dir + : dirname(new URL(import.meta.url).pathname); -// 初始化数据库连接 -const db = new DatabaseSync(join(__dirname, 'media.db')) +// 从环境变量获取数据库连接信息 +const DATABASE_URL = + process.env.DATABASE_URL || + `postgresql://${process.env.DB_USER || "postgres"}:${ + process.env.DB_PASSWORD || "postgres" + }@${process.env.DB_HOST || "localhost"}:${process.env.DB_PORT || "5432"}/${ + process.env.DB_NAME || "media" + }`; -export { db } +// 初始化 PostgreSQL 连接 +const sql = postgres(DATABASE_URL); + +// 自动初始化表结构(如果表不存在) +async function initTables() { + try { + // 检查 media 表是否存在 + const tableExists = await sql` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'media' + ) + `; + + // 如果表不存在,创建表 + if (!tableExists[0].exists) { + console.log("Initializing database tables..."); + const schemaPath = join(__dirname, "schema.sql"); + const schema = readFileSync(schemaPath, "utf-8"); + await sql.unsafe(schema); + console.log("Database tables initialized successfully"); + } + } catch (error: any) { + // 如果是因为数据库不存在,尝试创建数据库 + if ( + error.message?.includes("database") && + error.message?.includes("does not exist") + ) { + console.error("Database does not exist. Please create it first:"); + console.error(" createdb media"); + console.error(" or: psql -U postgres -c 'CREATE DATABASE media;'"); + throw error; + } + // 其他错误也抛出 + throw error; + } +} + +// 在模块加载时初始化表(但不阻塞) +initTables().catch((error) => { + console.error("Failed to initialize tables:", error.message); + // 不抛出错误,让应用继续运行,但会在第一次查询时失败 +}); + +// 将 SQLite 的 ? 占位符转换为 PostgreSQL 的 $1, $2, $3... +function convertQuery(query: string, params: any[]): [string, any[]] { + let pgQuery = query; + let paramIndex = 1; + const pgParams: any[] = []; + + // 替换 ? 为 $1, $2, $3... + pgQuery = pgQuery.replace(/\?/g, () => { + if (paramIndex <= params.length) { + pgParams.push(params[paramIndex - 1]); + } + return `$${paramIndex++}`; + }); + + return [pgQuery, pgParams]; +} + +// 创建一个兼容的数据库接口,模拟 SQLite 的 prepare 方法 +class DatabaseAdapter { + private sql: ReturnType; + + constructor(sql: ReturnType) { + this.sql = sql; + } + + prepare(query: string) { + return { + // all() 方法用于查询多条记录 + all: async (...params: any[]) => { + const [pgQuery, pgParams] = convertQuery(query, params); + const result = await this.sql.unsafe(pgQuery, pgParams); + return result; + }, + // get() 方法用于查询单条记录 + get: async (...params: any[]) => { + const [pgQuery, pgParams] = convertQuery(query, params); + const result = await this.sql.unsafe(pgQuery, pgParams); + return result[0] || null; + }, + // run() 方法用于执行 INSERT/UPDATE/DELETE + run: async (...params: any[]) => { + const [pgQuery, pgParams] = convertQuery(query, params); + const result = await this.sql.unsafe(pgQuery, pgParams); + + // 如果是 INSERT 语句,返回类似 SQLite 的 lastInsertRowid + if (query.trim().toUpperCase().startsWith("INSERT")) { + // PostgreSQL 使用 RETURNING 子句获取插入的 ID + if (query.includes("RETURNING")) { + return { + lastInsertRowid: result[0]?.id || null, + changes: result.length, + }; + } else { + // 如果没有 RETURNING,需要查询最后插入的 ID + // 注意:这需要表有 id 列且是 SERIAL 类型 + const lastIdResult = await this.sql.unsafe( + "SELECT lastval() as id" + ); + return { + lastInsertRowid: lastIdResult[0]?.id || null, + changes: result.length || 1, + }; + } + } + + return { + changes: result.length || 0, + }; + }, + }; + } +} + +const db = new DatabaseAdapter(sql); + +export { db, sql }; diff --git a/db/init.ts b/db/init.ts new file mode 100644 index 0000000..4b882c2 --- /dev/null +++ b/db/init.ts @@ -0,0 +1,83 @@ +/* + * 数据库初始化脚本 + * 自动创建数据库和表结构 + */ +import postgres from "postgres"; +import { readFileSync } from "fs"; +import { join, dirname } from "path"; + +// 获取当前文件所在目录 +const __dirname = + typeof import.meta.dir !== "undefined" + ? import.meta.dir + : dirname(new URL(import.meta.url).pathname); + +// 从环境变量获取数据库连接信息(不包含数据库名) +const getBaseConnection = () => { + const user = process.env.DB_USER || "postgres"; + const password = process.env.DB_PASSWORD || "postgres"; + const host = process.env.DB_HOST || "localhost"; + const port = process.env.DB_PORT || "5432"; + return `postgresql://${user}:${password}@${host}:${port}`; +}; + +// 初始化数据库 +export async function initDatabase() { + const dbName = process.env.DB_NAME || "media"; + const baseUrl = getBaseConnection(); + + try { + // 连接到 PostgreSQL 服务器(使用默认的 postgres 数据库) + const adminSql = postgres(`${baseUrl}/postgres`); + + // 检查数据库是否存在 + const dbExists = await adminSql` + SELECT 1 FROM pg_database WHERE datname = ${dbName} + `; + + // 如果数据库不存在,创建它 + if (dbExists.length === 0) { + console.log(`Creating database: ${dbName}`); + await adminSql.unsafe(`CREATE DATABASE ${dbName}`); + console.log(`Database ${dbName} created successfully`); + } else { + console.log(`Database ${dbName} already exists`); + } + + await adminSql.end(); + + // 连接到新创建的数据库 + const sql = postgres(`${baseUrl}/${dbName}`); + + // 读取并执行 schema.sql + const schemaPath = join(__dirname, "schema.sql"); + const schema = readFileSync(schemaPath, "utf-8"); + + // 执行 schema 中的 SQL 语句 + console.log("Creating tables..."); + await sql.unsafe(schema); + console.log("Tables created successfully"); + + await sql.end(); + console.log("Database initialization completed"); + } catch (error: any) { + console.error("Database initialization failed:", error.message); + throw error; + } +} + +// 如果直接运行此文件,执行初始化 +if ( + import.meta.main || + (typeof Bun !== "undefined" && Bun.main === import.meta.path) +) { + initDatabase() + .then(() => { + console.log("Initialization complete"); + process.exit(0); + }) + .catch((error) => { + console.error("Initialization failed:", error); + process.exit(1); + }); +} diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 0000000..d2a08ee --- /dev/null +++ b/db/schema.sql @@ -0,0 +1,29 @@ +-- PostgreSQL 数据库 Schema +-- 用于创建 media 和 users 表 + +-- 创建 media 表 +CREATE TABLE IF NOT EXISTS media ( + id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + type VARCHAR(50) NOT NULL, + rating INTEGER, + notes TEXT, + platform VARCHAR(100), + date DATE, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +-- 创建 users 表 +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + username VARCHAR(100) UNIQUE NOT NULL, + password VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT NOW() +); + +-- 创建索引以优化查询性能 +CREATE INDEX IF NOT EXISTS idx_media_type ON media(type); +CREATE INDEX IF NOT EXISTS idx_media_rating ON media(rating); +CREATE INDEX IF NOT EXISTS idx_media_date ON media(date); +CREATE INDEX IF NOT EXISTS idx_users_username ON users(username); diff --git a/deno.json b/deno.json deleted file mode 100644 index 0f5660f..0000000 --- a/deno.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "imports": { - "hono": "jsr:@hono/hono@^4.7.11" - }, - "tasks": { - "start": "deno run --allow-net --allow-read --allow-write --allow-env --watch main.ts" - }, - "compilerOptions": { - "jsx": "precompile", - "jsxImportSource": "hono/jsx" - } -} \ No newline at end of file diff --git a/deno.lock b/deno.lock deleted file mode 100644 index 72a9f4a..0000000 --- a/deno.lock +++ /dev/null @@ -1,121 +0,0 @@ -{ - "version": "5", - "specifiers": { - "jsr:@hono/hono@^4.7.11": "4.7.11", - "npm:@types/node@*": "22.15.15" - }, - "jsr": { - "@hono/hono@4.7.11": { - "integrity": "52b9395e4f2448fbaf82a6bd7925d15e59f4055037e74d6c2f657f3618b11193" - } - }, - "npm": { - "@types/node@22.15.15": { - "integrity": "sha512-R5muMcZob3/Jjchn5LcO8jdKwSCbzqmPB6ruBxMcf9kbxtniZHP327s6C37iOfuw8mbKK3cAQa7sEl7afLrQ8A==", - "dependencies": [ - "undici-types" - ] - }, - "undici-types@6.21.0": { - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" - } - }, - "redirects": { - "https://deno.land/std/path/mod.ts": "https://deno.land/std@0.224.0/path/mod.ts" - }, - "remote": { - "https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834", - "https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917", - "https://deno.land/std@0.224.0/path/_common/assert_path.ts": "dbdd757a465b690b2cc72fc5fb7698c51507dec6bfafce4ca500c46b76ff7bd8", - "https://deno.land/std@0.224.0/path/_common/basename.ts": "569744855bc8445f3a56087fd2aed56bdad39da971a8d92b138c9913aecc5fa2", - "https://deno.land/std@0.224.0/path/_common/common.ts": "ef73c2860694775fe8ffcbcdd387f9f97c7a656febf0daa8c73b56f4d8a7bd4c", - "https://deno.land/std@0.224.0/path/_common/constants.ts": "dc5f8057159f4b48cd304eb3027e42f1148cf4df1fb4240774d3492b5d12ac0c", - "https://deno.land/std@0.224.0/path/_common/dirname.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", - "https://deno.land/std@0.224.0/path/_common/format.ts": "92500e91ea5de21c97f5fe91e178bae62af524b72d5fcd246d6d60ae4bcada8b", - "https://deno.land/std@0.224.0/path/_common/from_file_url.ts": "d672bdeebc11bf80e99bf266f886c70963107bdd31134c4e249eef51133ceccf", - "https://deno.land/std@0.224.0/path/_common/glob_to_reg_exp.ts": "6cac16d5c2dc23af7d66348a7ce430e5de4e70b0eede074bdbcf4903f4374d8d", - "https://deno.land/std@0.224.0/path/_common/normalize.ts": "684df4aa71a04bbcc346c692c8485594fc8a90b9408dfbc26ff32cf3e0c98cc8", - "https://deno.land/std@0.224.0/path/_common/normalize_string.ts": "33edef773c2a8e242761f731adeb2bd6d683e9c69e4e3d0092985bede74f4ac3", - "https://deno.land/std@0.224.0/path/_common/relative.ts": "faa2753d9b32320ed4ada0733261e3357c186e5705678d9dd08b97527deae607", - "https://deno.land/std@0.224.0/path/_common/strip_trailing_separators.ts": "7024a93447efcdcfeaa9339a98fa63ef9d53de363f1fbe9858970f1bba02655a", - "https://deno.land/std@0.224.0/path/_common/to_file_url.ts": "7f76adbc83ece1bba173e6e98a27c647712cab773d3f8cbe0398b74afc817883", - "https://deno.land/std@0.224.0/path/_interface.ts": "8dfeb930ca4a772c458a8c7bbe1e33216fe91c253411338ad80c5b6fa93ddba0", - "https://deno.land/std@0.224.0/path/_os.ts": "8fb9b90fb6b753bd8c77cfd8a33c2ff6c5f5bc185f50de8ca4ac6a05710b2c15", - "https://deno.land/std@0.224.0/path/basename.ts": "7ee495c2d1ee516ffff48fb9a93267ba928b5a3486b550be73071bc14f8cc63e", - "https://deno.land/std@0.224.0/path/common.ts": "03e52e22882402c986fe97ca3b5bb4263c2aa811c515ce84584b23bac4cc2643", - "https://deno.land/std@0.224.0/path/constants.ts": "0c206169ca104938ede9da48ac952de288f23343304a1c3cb6ec7625e7325f36", - "https://deno.land/std@0.224.0/path/dirname.ts": "85bd955bf31d62c9aafdd7ff561c4b5fb587d11a9a5a45e2b01aedffa4238a7c", - "https://deno.land/std@0.224.0/path/extname.ts": "593303db8ae8c865cbd9ceec6e55d4b9ac5410c1e276bfd3131916591b954441", - "https://deno.land/std@0.224.0/path/format.ts": "6ce1779b0980296cf2bc20d66436b12792102b831fd281ab9eb08fa8a3e6f6ac", - "https://deno.land/std@0.224.0/path/from_file_url.ts": "911833ae4fd10a1c84f6271f36151ab785955849117dc48c6e43b929504ee069", - "https://deno.land/std@0.224.0/path/glob_to_regexp.ts": "7f30f0a21439cadfdae1be1bf370880b415e676097fda584a63ce319053b5972", - "https://deno.land/std@0.224.0/path/is_absolute.ts": "4791afc8bfd0c87f0526eaa616b0d16e7b3ab6a65b62942e50eac68de4ef67d7", - "https://deno.land/std@0.224.0/path/is_glob.ts": "a65f6195d3058c3050ab905705891b412ff942a292bcbaa1a807a74439a14141", - "https://deno.land/std@0.224.0/path/join.ts": "ae2ec5ca44c7e84a235fd532e4a0116bfb1f2368b394db1c4fb75e3c0f26a33a", - "https://deno.land/std@0.224.0/path/join_globs.ts": "5b3bf248b93247194f94fa6947b612ab9d3abd571ca8386cf7789038545e54a0", - "https://deno.land/std@0.224.0/path/mod.ts": "f6bd79cb08be0e604201bc9de41ac9248582699d1b2ee0ab6bc9190d472cf9cd", - "https://deno.land/std@0.224.0/path/normalize.ts": "4155743ccceeed319b350c1e62e931600272fad8ad00c417b91df093867a8352", - "https://deno.land/std@0.224.0/path/normalize_glob.ts": "cc89a77a7d3b1d01053b9dcd59462b75482b11e9068ae6c754b5cf5d794b374f", - "https://deno.land/std@0.224.0/path/parse.ts": "77ad91dcb235a66c6f504df83087ce2a5471e67d79c402014f6e847389108d5a", - "https://deno.land/std@0.224.0/path/posix/_util.ts": "1e3937da30f080bfc99fe45d7ed23c47dd8585c5e473b2d771380d3a6937cf9d", - "https://deno.land/std@0.224.0/path/posix/basename.ts": "d2fa5fbbb1c5a3ab8b9326458a8d4ceac77580961b3739cd5bfd1d3541a3e5f0", - "https://deno.land/std@0.224.0/path/posix/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4", - "https://deno.land/std@0.224.0/path/posix/constants.ts": "93481efb98cdffa4c719c22a0182b994e5a6aed3047e1962f6c2c75b7592bef1", - "https://deno.land/std@0.224.0/path/posix/dirname.ts": "76cd348ffe92345711409f88d4d8561d8645353ac215c8e9c80140069bf42f00", - "https://deno.land/std@0.224.0/path/posix/extname.ts": "e398c1d9d1908d3756a7ed94199fcd169e79466dd88feffd2f47ce0abf9d61d2", - "https://deno.land/std@0.224.0/path/posix/format.ts": "185e9ee2091a42dd39e2a3b8e4925370ee8407572cee1ae52838aed96310c5c1", - "https://deno.land/std@0.224.0/path/posix/from_file_url.ts": "951aee3a2c46fd0ed488899d024c6352b59154c70552e90885ed0c2ab699bc40", - "https://deno.land/std@0.224.0/path/posix/glob_to_regexp.ts": "76f012fcdb22c04b633f536c0b9644d100861bea36e9da56a94b9c589a742e8f", - "https://deno.land/std@0.224.0/path/posix/is_absolute.ts": "cebe561ad0ae294f0ce0365a1879dcfca8abd872821519b4fcc8d8967f888ede", - "https://deno.land/std@0.224.0/path/posix/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", - "https://deno.land/std@0.224.0/path/posix/join.ts": "7fc2cb3716aa1b863e990baf30b101d768db479e70b7313b4866a088db016f63", - "https://deno.land/std@0.224.0/path/posix/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25", - "https://deno.land/std@0.224.0/path/posix/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604", - "https://deno.land/std@0.224.0/path/posix/normalize.ts": "baeb49816a8299f90a0237d214cef46f00ba3e95c0d2ceb74205a6a584b58a91", - "https://deno.land/std@0.224.0/path/posix/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6", - "https://deno.land/std@0.224.0/path/posix/parse.ts": "09dfad0cae530f93627202f28c1befa78ea6e751f92f478ca2cc3b56be2cbb6a", - "https://deno.land/std@0.224.0/path/posix/relative.ts": "3907d6eda41f0ff723d336125a1ad4349112cd4d48f693859980314d5b9da31c", - "https://deno.land/std@0.224.0/path/posix/resolve.ts": "08b699cfeee10cb6857ccab38fa4b2ec703b0ea33e8e69964f29d02a2d5257cf", - "https://deno.land/std@0.224.0/path/posix/to_file_url.ts": "7aa752ba66a35049e0e4a4be5a0a31ac6b645257d2e031142abb1854de250aaf", - "https://deno.land/std@0.224.0/path/posix/to_namespaced_path.ts": "28b216b3c76f892a4dca9734ff1cc0045d135532bfd9c435ae4858bfa5a2ebf0", - "https://deno.land/std@0.224.0/path/relative.ts": "ab739d727180ed8727e34ed71d976912461d98e2b76de3d3de834c1066667add", - "https://deno.land/std@0.224.0/path/resolve.ts": "a6f977bdb4272e79d8d0ed4333e3d71367cc3926acf15ac271f1d059c8494d8d", - "https://deno.land/std@0.224.0/path/to_file_url.ts": "88f049b769bce411e2d2db5bd9e6fd9a185a5fbd6b9f5ad8f52bef517c4ece1b", - "https://deno.land/std@0.224.0/path/to_namespaced_path.ts": "b706a4103b104cfadc09600a5f838c2ba94dbcdb642344557122dda444526e40", - "https://deno.land/std@0.224.0/path/windows/_util.ts": "d5f47363e5293fced22c984550d5e70e98e266cc3f31769e1710511803d04808", - "https://deno.land/std@0.224.0/path/windows/basename.ts": "6bbc57bac9df2cec43288c8c5334919418d784243a00bc10de67d392ab36d660", - "https://deno.land/std@0.224.0/path/windows/common.ts": "26f60ccc8b2cac3e1613000c23ac5a7d392715d479e5be413473a37903a2b5d4", - "https://deno.land/std@0.224.0/path/windows/constants.ts": "5afaac0a1f67b68b0a380a4ef391bf59feb55856aa8c60dfc01bd3b6abb813f5", - "https://deno.land/std@0.224.0/path/windows/dirname.ts": "33e421be5a5558a1346a48e74c330b8e560be7424ed7684ea03c12c21b627bc9", - "https://deno.land/std@0.224.0/path/windows/extname.ts": "165a61b00d781257fda1e9606a48c78b06815385e7d703232548dbfc95346bef", - "https://deno.land/std@0.224.0/path/windows/format.ts": "bbb5ecf379305b472b1082cd2fdc010e44a0020030414974d6029be9ad52aeb6", - "https://deno.land/std@0.224.0/path/windows/from_file_url.ts": "ced2d587b6dff18f963f269d745c4a599cf82b0c4007356bd957cb4cb52efc01", - "https://deno.land/std@0.224.0/path/windows/glob_to_regexp.ts": "e45f1f89bf3fc36f94ab7b3b9d0026729829fabc486c77f414caebef3b7304f8", - "https://deno.land/std@0.224.0/path/windows/is_absolute.ts": "4a8f6853f8598cf91a835f41abed42112cebab09478b072e4beb00ec81f8ca8a", - "https://deno.land/std@0.224.0/path/windows/is_glob.ts": "8a8b08c08bf731acf2c1232218f1f45a11131bc01de81e5f803450a5914434b9", - "https://deno.land/std@0.224.0/path/windows/join.ts": "8d03530ab89195185103b7da9dfc6327af13eabdcd44c7c63e42e27808f50ecf", - "https://deno.land/std@0.224.0/path/windows/join_globs.ts": "a9475b44645feddceb484ee0498e456f4add112e181cb94042cdc6d47d1cdd25", - "https://deno.land/std@0.224.0/path/windows/mod.ts": "2301fc1c54a28b349e20656f68a85f75befa0ee9b6cd75bfac3da5aca9c3f604", - "https://deno.land/std@0.224.0/path/windows/normalize.ts": "78126170ab917f0ca355a9af9e65ad6bfa5be14d574c5fb09bb1920f52577780", - "https://deno.land/std@0.224.0/path/windows/normalize_glob.ts": "9c87a829b6c0f445d03b3ecadc14492e2864c3ebb966f4cea41e98326e4435c6", - "https://deno.land/std@0.224.0/path/windows/parse.ts": "08804327b0484d18ab4d6781742bf374976de662f8642e62a67e93346e759707", - "https://deno.land/std@0.224.0/path/windows/relative.ts": "3e1abc7977ee6cc0db2730d1f9cb38be87b0ce4806759d271a70e4997fc638d7", - "https://deno.land/std@0.224.0/path/windows/resolve.ts": "8dae1dadfed9d46ff46cc337c9525c0c7d959fb400a6308f34595c45bdca1972", - "https://deno.land/std@0.224.0/path/windows/to_file_url.ts": "40e560ee4854fe5a3d4d12976cef2f4e8914125c81b11f1108e127934ced502e", - "https://deno.land/std@0.224.0/path/windows/to_namespaced_path.ts": "4ffa4fb6fae321448d5fe810b3ca741d84df4d7897e61ee29be961a6aac89a4c", - "https://deno.land/x/sqlite@v3.8/build/sqlite.js": "72f63689fffcb9bb5ae10b1e8f7db09ea845cdf713e0e3a9693d8416a28f92a6", - "https://deno.land/x/sqlite@v3.8/build/vfs.js": "08533cc78fb29b9d9bd62f6bb93e5ef333407013fed185776808f11223ba0e70", - "https://deno.land/x/sqlite@v3.8/mod.ts": "e09fc79d8065fe222578114b109b1fd60077bff1bb75448532077f784f4d6a83", - "https://deno.land/x/sqlite@v3.8/src/constants.ts": "90f3be047ec0a89bcb5d6fc30db121685fc82cb00b1c476124ff47a4b0472aa9", - "https://deno.land/x/sqlite@v3.8/src/db.ts": "7d3251021756fa80f382c3952217c7446c5c8c1642b63511da0938fe33562663", - "https://deno.land/x/sqlite@v3.8/src/error.ts": "f7a15cb00d7c3797da1aefee3cf86d23e0ae92e73f0ba3165496c3816ab9503a", - "https://deno.land/x/sqlite@v3.8/src/function.ts": "e4c83b8ec64bf88bafad2407376b0c6a3b54e777593c70336fb40d43a79865f2", - "https://deno.land/x/sqlite@v3.8/src/query.ts": "d58abda928f6582d77bad685ecf551b1be8a15e8e38403e293ec38522e030cad", - "https://deno.land/x/sqlite@v3.8/src/wasm.ts": "e79d0baa6e42423257fb3c7cc98091c54399254867e0f34a09b5bdef37bd9487" - }, - "workspace": { - "dependencies": [ - "jsr:@hono/hono@^4.7.11" - ] - } -} diff --git a/deploy.sh b/deploy.sh index ba508a4..a173e69 100644 --- a/deploy.sh +++ b/deploy.sh @@ -2,7 +2,7 @@ ### # @Date: 2025-06-13 16:11:38 # @LastEditors: 陈子健 - # @LastEditTime: 2025-06-23 16:56:58 + # @LastEditTime: 2025-06-23 17:24:22 # @FilePath: /my-score/honoback/deploy.sh ### @@ -13,18 +13,23 @@ REMOTE_DIR="/home/media-backend" # 本地构建 echo "Building project..." -deno cache main.ts +bun install # 创建远程目录 echo "Creating remote directory..." ssh $USER@$SERVER "mkdir -p $REMOTE_DIR" -# 同步文件到服务器 -echo "Syncing files to server..." -rsync -avz --exclude 'db/media.db' \ - --exclude '.git' \ - ./ $USER@$SERVER:$REMOTE_DIR/ + +# 1. 远程备份数据库 +ssh $USER@$SERVER "cp $REMOTE_DIR/db/media.db $REMOTE_DIR/db/media.db.bak 2>/dev/null || true" + +# 2. 同步代码到服务器(只同步白名单内容,且排除 media.db) +rsync -avz --exclude='.git/' --exclude='node_modules/' --exclude='db/media.db' \ + --include='*.ts' --include='*.json' --include='*.sh' --include='*.service' \ + --include='package.json' --include='package-lock.json' --include='bun.lockb' \ + --include='db/' --include='db/*.ts' --include='routes/' --include='routes/*.ts' \ + --exclude='*' ./ $USER@$SERVER:$REMOTE_DIR # 在服务器上安装依赖并重启服务 -echo "Installing and starting systemd service..." -ssh $USER@$SERVER "systemctl restart my-score" \ No newline at end of file +echo "Installing dependencies and restarting service..." +ssh $USER@$SERVER "cd $REMOTE_DIR && bun install && systemctl restart my-score" \ No newline at end of file diff --git a/main.ts b/main.ts index 14e84bc..e70bd42 100644 --- a/main.ts +++ b/main.ts @@ -13,7 +13,7 @@ import auth from './routes/auth.ts' const app = new Hono<{ Variables: JwtVariables }>() -const AUTH_SECRET = Deno.env.get('AUTH_SECRET') || 'it-is-a-secret' +const AUTH_SECRET = process.env.AUTH_SECRET || 'it-is-a-secret' // 添加请求日志中间件 app.use('*', async (c, next) => { @@ -41,10 +41,12 @@ app.notFound((c) => { }) // 获取端口配置 -const port = parseInt(Deno.env.get('PORT') || '8000') +const port = parseInt(process.env.PORT || '8000') // 启动服务器 -if (import.meta.main) { - Deno.serve({ port }, app.fetch) - console.log(`Server running on port ${port}`) -} +Bun.serve({ + port, + fetch: app.fetch, +}) + +console.log(`Server running on port ${port}`) diff --git a/my-score.service b/my-score.service index 872db8b..5e66317 100644 --- a/my-score.service +++ b/my-score.service @@ -1,5 +1,5 @@ [Unit] -Description=My Score Deno Service +Description=My Score Bun Service After=network.target [Service] @@ -9,7 +9,7 @@ WorkingDirectory=/home/media-backend Environment=PATH=/usr/local/bin:/usr/bin:/bin Environment=PORT=8000 Environment=AUTH_SECRET=it-is-a-secret -ExecStart=/usr/local/bin/deno run --allow-net --allow-read --allow-write --allow-env main.ts +ExecStart=/usr/local/bin/bun run main.ts Restart=always RestartSec=10 StandardOutput=journal diff --git a/package.json b/package.json new file mode 100644 index 0000000..7960c31 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "my-score-backend", + "version": "1.0.0", + "description": "My Score Backend API", + "type": "module", + "main": "main.ts", + "scripts": { + "start": "bun run main.ts", + "dev": "bun --watch main.ts", + "init-db": "bun run db/init.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "hono": "^4.7.11", + "postgres": "^3.4.3" + }, + "devDependencies": { + "@types/bun": "latest" + } +} \ No newline at end of file diff --git a/routes/media.ts b/routes/media.ts index ef7ccbd..5eeb0eb 100644 --- a/routes/media.ts +++ b/routes/media.ts @@ -1,157 +1,267 @@ -import { Hono } from 'hono' -import { db } from '../db/index.ts' -import type { JwtVariables } from 'hono/jwt' -import { authMiddleware } from '../auth.ts' +import { Hono } from "hono"; +import { db } from "../db/index.ts"; +import type { JwtVariables } from "hono/jwt"; +import { authMiddleware } from "../auth.ts"; -const media = new Hono<{ Variables: JwtVariables }>() +const media = new Hono<{ Variables: JwtVariables }>(); -media.use('/*', authMiddleware) +// 查询满分(10分)作品,不需要JWT验证 +media.get("/perfect", async (c) => { + try { + const perfectList = await db + .prepare("SELECT * FROM media WHERE rating = 10") + .all(); + return c.json({ + code: 0, + data: perfectList, + message: "Success", + }); + } catch (error: any) { + return c.json({ code: 1, data: {}, message: error.message }, 500); + } +}); + +media.use("/*", authMiddleware); // 获取所有媒体记录 -media.get('/list', (c) => { +media.get("/list", async (c) => { try { - const mediaList = db.prepare('SELECT * FROM media').all() + const mediaList = await db.prepare("SELECT * FROM media").all(); return c.json({ code: 0, data: mediaList, - message: 'Success' - }) + message: "Success", + }); } catch (error: any) { - return c.json({ code: 1, data: {}, message: error.message }, 500) + return c.json({ code: 1, data: {}, message: error.message }, 500); } -}) +}); // 创建新的媒体记录 -media.post('/create', async (c) => { +media.post("/create", async (c) => { try { - const data = await c.req.json() - const { title, type, rating, notes, platform, date } = data + const data = await c.req.json(); + const { title, type, rating, notes, platform, date } = data; - const result = db.prepare(` + const result = await db + .prepare( + ` INSERT INTO media (title, type, rating, notes, platform, date, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, datetime('now'), datetime('now')) - `).run(title, type, rating, notes, platform, date) + VALUES (?, ?, ?, ?, ?, ?, NOW(), NOW()) + RETURNING id + ` + ) + .run(title, type, rating, notes, platform, date); - const newMedia = db.prepare('SELECT * FROM media WHERE id = ?').get(result.lastInsertRowid) + const newMedia = await db + .prepare("SELECT * FROM media WHERE id = ?") + .get(result.lastInsertRowid); - return c.json({ - code: 0, - data: newMedia, - message: 'Created successfully' - }, 201) + return c.json( + { + code: 0, + data: newMedia, + message: "Created successfully", + }, + 201 + ); } catch (error: any) { - return c.json({ code: 2, data: {}, message: error.message }, 500) + return c.json({ code: 2, data: {}, message: error.message }, 500); } -}) +}); // 更新媒体记录 -media.put('/updateById/:id', async (c) => { +media.put("/updateById/:id", async (c) => { try { - const id = c.req.param('id') - const data = await c.req.json() - const { title, type, rating, notes, platform, date } = data + const id = c.req.param("id"); + const data = await c.req.json(); + const { title, type, rating, notes, platform, date } = data; - db.prepare(` + await db + .prepare( + ` UPDATE media - SET title = ?, type = ?, rating = ?, notes = ?, platform = ?, date = ?, updated_at = datetime('now') + SET title = ?, type = ?, rating = ?, notes = ?, platform = ?, date = ?, updated_at = NOW() WHERE id = ? - `).run(title, type, rating, notes, platform, date, id) + ` + ) + .run(title, type, rating, notes, platform, date, id); - const updatedMedia = db.prepare('SELECT * FROM media WHERE id = ?').get(id) + const updatedMedia = await db + .prepare("SELECT * FROM media WHERE id = ?") + .get(id); if (!updatedMedia) { - return c.json({ code: 1, data: {}, message: 'Media not found' }, 404) + return c.json({ code: 1, data: {}, message: "Media not found" }, 404); } return c.json({ code: 0, data: updatedMedia, - message: 'Updated successfully' - }) + message: "Updated successfully", + }); } catch (error: any) { - return c.json({ code: 2, data: {}, message: error.message }, 500) + return c.json({ code: 2, data: {}, message: error.message }, 500); } -}) +}); // 删除媒体记录 -media.delete('/deleteById/:id', async (c) => { +media.delete("/deleteById/:id", async (c) => { try { - const id = c.req.param('id') - db.prepare('DELETE FROM media WHERE id = ?').run(id) - return c.json({ code: 0, data: {}, message: 'Deleted successfully' }) + const id = c.req.param("id"); + await db.prepare("DELETE FROM media WHERE id = ?").run(id); + return c.json({ code: 0, data: {}, message: "Deleted successfully" }); } catch (error: any) { - return c.json({ code: 2, data: {}, message: error.message }, 500) + return c.json({ code: 2, data: {}, message: error.message }, 500); } -}) +}); // 分页查询媒体记录 -media.get('/page', async (c) => { +media.get("/page", async (c) => { try { - const type = c.req.query('type') - const currentPage = parseInt(c.req.query('currentPage') || '1') - const pageSize = parseInt(c.req.query('pageSize') || '10') - const title = c.req.query('title') || '' - const startDate = c.req.query('startDate') - const endDate = c.req.query('endDate') - const sortBy = c.req.query('sortBy') || 'date' - const sortType = c.req.query('sortType') || 'desc' + const type = c.req.query("type"); + const currentPage = parseInt(c.req.query("currentPage") || "1"); + const pageSize = parseInt(c.req.query("pageSize") || "10"); + const title = c.req.query("title") || ""; + const startDate = c.req.query("startDate"); + const endDate = c.req.query("endDate"); + const sortBy = c.req.query("sortBy") || "date"; + const sortType = c.req.query("sortType") || "desc"; if (!type) { - return c.json({ code: 1, data: {}, message: 'Type is required' }, 400) + return c.json({ code: 1, data: {}, message: "Type is required" }, 400); } - let query = 'SELECT * FROM media WHERE type = ?' - const params = [type] + let query = "SELECT * FROM media WHERE type = ?"; + const params = [type]; if (title) { - query += ' AND title LIKE ?' - params.push(`%${title}%`) + query += " AND title LIKE ?"; + params.push(`%${title}%`); } if (startDate) { - query += ' AND date >= ?' - params.push(startDate) + query += " AND date >= ?"; + params.push(startDate); } if (endDate) { - query += ' AND date <= ?' - params.push(endDate) + query += " AND date <= ?"; + params.push(endDate); } // 添加排序 - if (sortBy === 'date') { - query += ` ORDER BY date ${sortType.toUpperCase()}` - } else if (sortBy === 'score') { - query += ` ORDER BY rating ${sortType.toUpperCase()}` + if (sortBy === "date") { + query += ` ORDER BY date ${sortType.toUpperCase()}`; + } else if (sortBy === "score") { + query += ` ORDER BY rating ${sortType.toUpperCase()}`; } // 添加分页 - const offset = (currentPage - 1) * pageSize - query += ' LIMIT ? OFFSET ?' - params.push(pageSize.toString(), offset.toString()) + const offset = (currentPage - 1) * pageSize; + query += " LIMIT ? OFFSET ?"; + params.push(pageSize.toString(), offset.toString()); // 获取总数 const countQuery = query - .replace('SELECT *', 'SELECT COUNT(*) as total') - .replace(/ORDER BY.*$/, '') // 移除 ORDER BY 子句 - .replace(/LIMIT.*$/, '') // 移除 LIMIT 和 OFFSET 子句 - const totalResult = db.prepare(countQuery).get(...params.slice(0, -2)) - const total = totalResult?.total || 0 + .replace("SELECT *", "SELECT COUNT(*) as total") + .replace(/ORDER BY.*$/, "") // 移除 ORDER BY 子句 + .replace(/LIMIT.*$/, ""); // 移除 LIMIT 和 OFFSET 子句 + const totalResult = await db + .prepare(countQuery) + .get(...params.slice(0, -2)); + const total = totalResult?.total || 0; // 获取分页数据 - const mediaList = db.prepare(query).all(...params) + const mediaList = await db.prepare(query).all(...params); + + // 格式化 date 字段,只返回日期部分(YYYY-MM-DD) + const formattedList = mediaList.map((item: any) => { + if (item.date) { + // 如果是 Date 对象或字符串,格式化为 YYYY-MM-DD + const date = new Date(item.date); + if (!isNaN(date.getTime())) { + item.date = date.toISOString().split("T")[0]; + } + } + return item; + }); return c.json({ code: 0, data: { - list: mediaList, + list: formattedList, total, currentPage, - pageSize + pageSize, }, - message: 'Success' - }) + message: "Success", + }); } catch (error: any) { - return c.json({ code: 1, data: {}, message: error.message }, 500) + return c.json({ code: 1, data: {}, message: error.message }, 500); } -}) +}); -export default media +// 获取媒体统计信息 +media.get("/stats", async (c) => { + try { + // 获取总数 + const totalResult = await db + .prepare("SELECT COUNT(*) as total FROM media") + .get(); + const total = totalResult?.total || 0; + + // 获取满分作品数 + const perfectResult = await db + .prepare("SELECT COUNT(*) as count FROM media WHERE rating = 10") + .get(); + const perfectCount = perfectResult?.count || 0; + + // 按类型统计 + const typeStats = await db + .prepare( + "SELECT type, COUNT(*) as count FROM media GROUP BY type ORDER BY count DESC" + ) + .all(); + + // 获取平均评分 + const avgRatingResult = await db + .prepare("SELECT AVG(rating) as avg FROM media WHERE rating IS NOT NULL") + .get(); + const avgRating = avgRatingResult?.avg + ? parseFloat(avgRatingResult.avg).toFixed(2) + : "0.00"; + + // 获取最新添加的作品 + const latestResult = await db + .prepare( + "SELECT title, type, rating, date FROM media ORDER BY created_at DESC LIMIT 5" + ) + .all(); + + // 格式化最新作品的日期 + const latest = latestResult.map((item: any) => { + if (item.date) { + const date = new Date(item.date); + if (!isNaN(date.getTime())) { + item.date = date.toISOString().split("T")[0]; + } + } + return item; + }); + + return c.json({ + code: 0, + data: { + total, + perfectCount, + avgRating, + typeStats, + latest, + }, + message: "Success", + }); + } catch (error: any) { + return c.json({ code: 1, data: {}, message: error.message }, 500); + } +}); + +export default media; diff --git a/routes/user.ts b/routes/user.ts index dde1bd0..3c7d8e9 100644 --- a/routes/user.ts +++ b/routes/user.ts @@ -4,36 +4,40 @@ * @LastEditTime: 2025-06-16 11:17:09 * @FilePath: /my-score/honoback/routes/user.ts */ -import { Hono } from 'hono' -import { sign } from 'hono/jwt' -import type { JwtVariables } from 'hono/jwt' -import { db } from '../db/index.ts' +import { Hono } from "hono"; +import { sign } from "hono/jwt"; +import type { JwtVariables } from "hono/jwt"; +import { db } from "../db/index.ts"; -const user = new Hono<{ Variables: JwtVariables }>() +const user = new Hono<{ Variables: JwtVariables }>(); -const AUTH_SECRET = 'it-is-a-secret' +const AUTH_SECRET = "it-is-a-secret"; // 登录路由 -user.post('/login', async (c) => { - const { username, password } = await c.req.json() - +user.post("/login", async (c) => { + const { username, password } = await c.req.json(); + // 从数据库验证用户 - const user = db.prepare('SELECT * FROM users WHERE username = ? AND password = ?') - .get(username, password) - - if (user) { - const token = await sign({ username: user.username }, AUTH_SECRET) - return c.json({ + const userRecord = await db + .prepare("SELECT * FROM users WHERE username = ? AND password = ?") + .get(username, password); + + if (userRecord) { + const token = await sign({ username: userRecord.username }, AUTH_SECRET); + return c.json({ code: 200, data: { token }, - message: '登录成功' - }) + message: "登录成功", + }); } - - return c.json({ - code: 401, - message: '用户名或密码错误' - }, 401) -}) -export default user + return c.json( + { + code: 401, + message: "用户名或密码错误", + }, + 401 + ); +}); + +export default user;