Merge pull request 'dev' (#1) from dev into main
All checks were successful
Deploy to Server / deploy (push) Successful in 5s
All checks were successful
Deploy to Server / deploy (push) Successful in 5s
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
47
.gitea/workflows/README.md
Normal file
47
.gitea/workflows/README.md
Normal file
@@ -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` 文件)
|
||||
- 数据库文件不会被覆盖,确保数据安全
|
||||
42
.gitea/workflows/deploy.yml
Normal file
42
.gitea/workflows/deploy.yml
Normal file
@@ -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
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,3 +1,7 @@
|
||||
.vscode
|
||||
.DS_Store
|
||||
**/*.db
|
||||
node_modules/
|
||||
bun.lockb
|
||||
.env
|
||||
.env.local
|
||||
137
MIGRATION.md
Normal file
137
MIGRATION.md
Normal file
@@ -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 依赖
|
||||
48
README.md
48
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)
|
||||
|
||||
**注意**:应用启动时会自动检查并创建表结构(如果不存在),但不会自动创建数据库。请确保数据库已存在。
|
||||
|
||||
29
bun.lock
Normal file
29
bun.lock
Normal file
@@ -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=="],
|
||||
}
|
||||
}
|
||||
144
db/index.ts
144
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<typeof postgres>;
|
||||
|
||||
constructor(sql: ReturnType<typeof postgres>) {
|
||||
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 };
|
||||
|
||||
83
db/init.ts
Normal file
83
db/init.ts
Normal file
@@ -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);
|
||||
});
|
||||
}
|
||||
29
db/schema.sql
Normal file
29
db/schema.sql
Normal file
@@ -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);
|
||||
12
deno.json
12
deno.json
@@ -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"
|
||||
}
|
||||
}
|
||||
121
deno.lock
generated
121
deno.lock
generated
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
23
deploy.sh
23
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"
|
||||
echo "Installing dependencies and restarting service..."
|
||||
ssh $USER@$SERVER "cd $REMOTE_DIR && bun install && systemctl restart my-score"
|
||||
14
main.ts
14
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}`)
|
||||
|
||||
@@ -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
|
||||
|
||||
22
package.json
Normal file
22
package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
276
routes/media.ts
276
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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user