feat(auth): 添加认证模块和图片批注功能(项目初始化)

- 实现用户登录、登出、密码修改等认证功能
- 添加会话管理和权限控制中间件
- 创建图片批注组件和相关API路由
- 实现批注的增删改查功能
- 添加Docker和Git忽略配置文件
- 创建系统架构文档和开发约定说明
- 集成认证模块到前端应用路由中
This commit is contained in:
yuzhe
2026-07-21 15:28:55 +08:00
commit b0c498fbb6
81 changed files with 10826 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
import { database } from '../database.js';
import type { Annotation, CreateAnnotationRequest } from '../../shared/types.js';
type AnnotationRow = Annotation & { id: number | string; image_id: number | string };
function toAnnotation(row: AnnotationRow): Annotation { return { ...row, id: Number(row.id), image_id: Number(row.image_id) }; }
export const annotationsRepository = {
async listByImage(imageId: number): Promise<Annotation[]> {
return (await database.all<AnnotationRow>('SELECT id, image_id, x, y, content, author_name, status, created_at FROM annotations WHERE image_id = ? ORDER BY id ASC', [imageId])).map(toAnnotation);
},
async create(imageId: number, data: CreateAnnotationRequest): Promise<Annotation> {
const id = await database.insertId('INSERT INTO annotations (image_id, x, y, content, author_name) VALUES (?, ?, ?, ?, ?)', [imageId, data.x, data.y, data.content, data.author_name || '客户']);
return toAnnotation((await database.one<AnnotationRow>('SELECT id, image_id, x, y, content, author_name, status, created_at FROM annotations WHERE id = ?', [id]))!);
},
async remove(id: number): Promise<boolean> { return (await database.execute('DELETE FROM annotations WHERE id = ?', [id])).changes > 0; },
};

View File

@@ -0,0 +1,28 @@
import { database, withTransaction, type QueryContext } from '../database.js';
import type { NoteImage } from '../../shared/types.js';
interface ImageRow extends Omit<NoteImage, 'id' | 'note_id' | 'order_index'> { id: number | string; note_id: number | string; order_index: number | string }
function toImage(row: ImageRow): NoteImage { return { ...row, id: Number(row.id), note_id: Number(row.note_id), order_index: Number(row.order_index), url: /^https?:\/\//.test(row.url) || row.url.startsWith('/') ? row.url : `/uploads/${row.url}` }; }
export const imagesRepository = {
async listByNote(noteId: number, versionNumber?: number): Promise<NoteImage[]> {
const rows = await database.all<ImageRow>(`SELECT id, note_id, url, width, height, order_index, storage_provider, storage_key FROM images
WHERE note_id = ?${versionNumber ? ' AND version_number = ?' : ''} ORDER BY order_index ASC, id ASC`, versionNumber ? [noteId, versionNumber] : [noteId]);
return rows.map(toImage);
},
async createMany(noteId: number, images: { url: string; width: number; height: number; storageProvider: 'local' | 'tencent_cos'; storageKey: string }[], versionNumber = 1, existing?: QueryContext): Promise<number[]> {
const insert = async (tx: QueryContext) => {
const ids: number[] = [];
for (let index = 0; index < images.length; index += 1) {
const image = images[index];
ids.push(await tx.insertId('INSERT INTO images (note_id, url, width, height, order_index, storage_provider, storage_key, version_number) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [noteId, image.url, image.width, image.height, index, image.storageProvider, image.storageKey, versionNumber]));
}
return ids;
};
return existing ? insert(existing) : withTransaction(insert);
},
async findById(id: number): Promise<NoteImage | null> {
const row = await database.one<ImageRow>('SELECT id, note_id, url, width, height, order_index, storage_provider, storage_key FROM images WHERE id = ?', [id]);
return row ? toImage(row) : null;
},
};

View File

@@ -0,0 +1,53 @@
import { database } from '../database.js';
import type { Note, NoteListQuery, ReviewStatus } from '../../shared/types.js';
interface NoteRow {
id: number; collection_id: number; title: string; description: string; tags: string;
review_status: ReviewStatus; version_number: number; created_at: string;
image_count: number | string; annotation_count: number | string; comment_count: number | string; cover_url: string | null;
}
function toNote(row: NoteRow): Note {
return {
...row,
id: Number(row.id), collection_id: Number(row.collection_id), version_number: Number(row.version_number),
image_count: Number(row.image_count), annotation_count: Number(row.annotation_count), comment_count: Number(row.comment_count),
tags: JSON.parse(row.tags || '[]') as string[],
cover_image: row.cover_url ? (/^https?:\/\//.test(row.cover_url) || row.cover_url.startsWith('/') ? row.cover_url : `/uploads/${row.cover_url}`) : '',
};
}
const select = `
SELECT n.id, n.collection_id, n.title, n.description, n.tags, n.review_status, n.version_number, n.created_at,
(SELECT COUNT(*) FROM images i WHERE i.note_id = n.id AND i.version_number = n.version_number) AS image_count,
(SELECT COUNT(*) FROM annotations a JOIN images i ON i.id = a.image_id WHERE i.note_id = n.id AND i.version_number = n.version_number) AS annotation_count,
(SELECT COUNT(*) FROM work_comments wc WHERE wc.note_id = n.id) AS comment_count,
(SELECT url FROM images WHERE note_id = n.id AND version_number = n.version_number ORDER BY order_index, id LIMIT 1) AS cover_url
FROM notes n`;
export const notesRepository = {
async list(query: NoteListQuery = {}): Promise<Note[]> {
const conditions: string[] = []; const params: unknown[] = [];
if (query.collectionId) { conditions.push('n.collection_id = ?'); params.push(query.collectionId); }
if (query.status) { conditions.push('n.review_status = ?'); params.push(query.status); }
if (query.q?.trim()) { conditions.push('(n.title LIKE ? OR n.description LIKE ?)'); params.push(`%${query.q.trim()}%`, `%${query.q.trim()}%`); }
if (query.tag?.trim()) { conditions.push('n.tags LIKE ?'); params.push(`%"${query.tag.trim()}"%`); }
if (query.projectId) { conditions.push('n.collection_id IN (SELECT id FROM collections WHERE project_id = ?)'); params.push(query.projectId); }
if (query.groupId) { conditions.push('n.collection_id IN (SELECT c.id FROM collections c JOIN projects p ON p.id = c.project_id WHERE p.group_id = ?)'); params.push(query.groupId); }
const where = conditions.length ? ` WHERE ${conditions.join(' AND ')}` : '';
const order = query.order === 'asc' ? 'ASC' : 'DESC';
const sort = query.sort === 'annotations' ? 'annotation_count' : 'n.created_at';
return (await database.all<NoteRow>(`${select}${where} ORDER BY ${sort} ${order}, n.id DESC`, params)).map(toNote);
},
async findById(id: number): Promise<Note | null> {
const row = await database.one<NoteRow>(`${select} WHERE n.id = ?`, [id]);
return row ? toNote(row) : null;
},
async create(title: string, description: string, collectionId: number, tags: string[]): Promise<number> {
return database.insertId('INSERT INTO notes (title, description, collection_id, tags, review_status) VALUES (?, ?, ?, ?, ?)', [title, description, collectionId, JSON.stringify(tags), 'pending']);
},
async setStatus(id: number, status: ReviewStatus): Promise<boolean> {
return (await database.execute('UPDATE notes SET review_status = ? WHERE id = ?', [status, id])).changes > 0;
},
async remove(id: number): Promise<boolean> { return (await database.execute('DELETE FROM notes WHERE id = ?', [id])).changes > 0; },
};