feat: add creator

This commit is contained in:
2026-06-16 11:05:56 -06:00
parent 33d5bfd8e9
commit 028ba0f2f9
4 changed files with 35 additions and 8 deletions

View File

@@ -6,6 +6,7 @@ export interface BookSummary {
name: string name: string
updatedAt: string updatedAt: string
lessonCount: number lessonCount: number
createdBy: string
} }
export interface BookRecord { export interface BookRecord {
@@ -26,6 +27,7 @@ interface BookRow {
name: string name: string
data: string data: string
updated_at: string updated_at: string
created_by: string
} }
type StoredTeachingBook = Omit<TeachingBook, 'selectedId'> & { type StoredTeachingBook = Omit<TeachingBook, 'selectedId'> & {
@@ -67,7 +69,8 @@ const SCHEMA = `
name TEXT NOT NULL, name TEXT NOT NULL,
data TEXT NOT NULL, data TEXT NOT NULL,
created_at TEXT NOT NULL, created_at TEXT NOT NULL,
updated_at TEXT NOT NULL updated_at TEXT NOT NULL,
created_by TEXT NOT NULL DEFAULT ''
); );
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
@@ -124,6 +127,14 @@ function parseBookData(data: string): TeachingBook {
return normalizeBookData(JSON.parse(data) as StoredTeachingBook).data return normalizeBookData(JSON.parse(data) as StoredTeachingBook).data
} }
function migrateBookOwnership(db: Database): void {
const admin = db
.query<{ id: string }, []>("SELECT id FROM users WHERE role = 'admin' LIMIT 1")
.get()
if (!admin) return
db.run("UPDATE books SET created_by = ? WHERE created_by = ''", [admin.id])
}
function migrateStoredBooks(db: Database): void { function migrateStoredBooks(db: Database): void {
const rows = db.query<{ id: string; data: string }, []>('SELECT id, data FROM books').all() const rows = db.query<{ id: string; data: string }, []>('SELECT id, data FROM books').all()
@@ -139,13 +150,25 @@ export function openDb(path: string): Database {
const db = new Database(path) const db = new Database(path)
db.run('PRAGMA foreign_keys = ON') db.run('PRAGMA foreign_keys = ON')
db.run(SCHEMA) db.run(SCHEMA)
try {
db.run("ALTER TABLE books ADD COLUMN created_by TEXT NOT NULL DEFAULT ''")
} catch {
// column already exists
}
migrateStoredBooks(db) migrateStoredBooks(db)
migrateBookOwnership(db)
return db return db
} }
export function listBooks(db: Database): BookSummary[] { export function listBooks(db: Database): BookSummary[] {
const rows = db const rows = db
.query<BookRow, []>('SELECT id, name, data, updated_at FROM books ORDER BY updated_at DESC') .query<BookRow & { creator_username: string }, []>(
`SELECT b.id, b.name, b.data, b.updated_at, b.created_by,
COALESCE(u.username, '') AS creator_username
FROM books b
LEFT JOIN users u ON b.created_by = u.id
ORDER BY b.updated_at DESC`,
)
.all() .all()
return rows.map((row) => ({ return rows.map((row) => ({
@@ -153,21 +176,23 @@ export function listBooks(db: Database): BookSummary[] {
name: row.name, name: row.name,
updatedAt: row.updated_at, updatedAt: row.updated_at,
lessonCount: parseBookData(row.data).designs.length, lessonCount: parseBookData(row.data).designs.length,
createdBy: row.creator_username,
})) }))
} }
export function createBook(db: Database, name: string): BookRecord { export function createBook(db: Database, name: string, userId = ''): BookRecord {
const id = crypto.randomUUID() const id = crypto.randomUUID()
const now = new Date().toISOString() const now = new Date().toISOString()
const data = createEmptyBook() const data = createEmptyBook()
data.updatedAt = now data.updatedAt = now
db.run('INSERT INTO books (id, name, data, created_at, updated_at) VALUES (?, ?, ?, ?, ?)', [ db.run('INSERT INTO books (id, name, data, created_at, updated_at, created_by) VALUES (?, ?, ?, ?, ?, ?)', [
id, id,
name, name,
JSON.stringify(data), JSON.stringify(data),
now, now,
now, now,
userId,
]) ])
return { id, name, updatedAt: now, data } return { id, name, updatedAt: now, data }

View File

@@ -2,9 +2,10 @@ import type { Database } from 'bun:sqlite'
import { Hono } from 'hono' import { Hono } from 'hono'
import type { TeachingBook } from '../../shared/domain/teachingDesign' import type { TeachingBook } from '../../shared/domain/teachingDesign'
import { createBook, deleteBook, getBook, listBooks, renameBook, saveBookData } from '../db' import { createBook, deleteBook, getBook, listBooks, renameBook, saveBookData } from '../db'
import type { AuthVariables } from '../middleware/bearerAuth'
export function createBooksRouter(db: Database): Hono { export function createBooksRouter(db: Database): Hono<{ Variables: AuthVariables }> {
const app = new Hono() const app = new Hono<{ Variables: AuthVariables }>()
app.get('/', (c) => { app.get('/', (c) => {
return c.json(listBooks(db)) return c.json(listBooks(db))
@@ -18,7 +19,7 @@ export function createBooksRouter(db: Database): Hono {
return c.json({ error: '请提供整本名称。' }, 400) return c.json({ error: '请提供整本名称。' }, 400)
} }
return c.json(createBook(db, name.trim())) return c.json(createBook(db, name.trim(), c.get('userId')))
}) })
app.get('/:id', (c) => { app.get('/:id', (c) => {

View File

@@ -156,7 +156,7 @@ async function removeBook(book: BookSummary): Promise<void> {
</template> </template>
<template v-else> <template v-else>
<span class="book-list-name">{{ book.name }}</span> <span class="book-list-name">{{ book.name }}</span>
<span class="book-list-meta">更新于 {{ formatCstUpdatedAt(book.updatedAt) }} · {{ book.lessonCount }} </span> <span class="book-list-meta">更新于 {{ formatCstUpdatedAt(book.updatedAt) }} · {{ book.lessonCount }} <template v-if="book.createdBy"> · 创建者{{ book.createdBy }}</template></span>
<button class="ui-button" type="button" :data-testid="`open-${book.id}`" @click="emit('open', book.id)"> <button class="ui-button" type="button" :data-testid="`open-${book.id}`" @click="emit('open', book.id)">
打开 打开
</button> </button>

View File

@@ -6,6 +6,7 @@ export interface BookSummary {
name: string name: string
updatedAt: string updatedAt: string
lessonCount: number lessonCount: number
createdBy: string
} }
export interface BookRecord { export interface BookRecord {