223 lines
6.4 KiB
JavaScript
223 lines
6.4 KiB
JavaScript
/**
|
||
* ═══════════════════════════════════════════════════════════
|
||
* 团队书库引擎 · Library Engine Service
|
||
* ═══════════════════════════════════════════════════════════
|
||
*
|
||
* 智库节点 Phase 3 · ZY-SVR-006
|
||
* 守护: 铸渊 · ICE-GL-ZY001
|
||
* 版权: 国作登字-2026-A-00037559
|
||
*
|
||
* 功能:
|
||
* - 团队共享书库(男频/女频分区)
|
||
* - 去重存储:同一本书只存一份,成员书架用引用
|
||
* - 书籍分类管理(标签/分区/推荐)
|
||
* - 书籍评分与评论
|
||
*
|
||
* ═══════════════════════════════════════════════════════════
|
||
*/
|
||
|
||
'use strict';
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const crypto = require('crypto');
|
||
|
||
const DATA_DIR = path.join(__dirname, '..', 'data');
|
||
const LIBRARY_FILE = path.join(DATA_DIR, 'team-library.json');
|
||
|
||
const CATEGORIES = ['男频', '女频', '无CP', '百合', '耽美', '其他'];
|
||
|
||
// 内存数据 (null-prototype 防原型链污染)
|
||
let libraryData = Object.create(null);
|
||
|
||
function sanitizeId(id) {
|
||
if (!id || typeof id !== 'string') return null;
|
||
if (id === '__proto__' || id === 'constructor' || id === 'prototype') return null;
|
||
if (id.includes('..') || id.includes('/') || id.includes('\\')) return null;
|
||
return id;
|
||
}
|
||
|
||
function init() {
|
||
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
|
||
load();
|
||
}
|
||
|
||
function load() {
|
||
try {
|
||
if (fs.existsSync(LIBRARY_FILE)) {
|
||
const raw = JSON.parse(fs.readFileSync(LIBRARY_FILE, 'utf8'));
|
||
libraryData = Object.create(null);
|
||
if (raw.books) libraryData.books = raw.books;
|
||
else libraryData.books = [];
|
||
} else {
|
||
libraryData.books = [];
|
||
}
|
||
} catch {
|
||
libraryData.books = [];
|
||
}
|
||
}
|
||
|
||
function save() {
|
||
try {
|
||
fs.writeFileSync(LIBRARY_FILE, JSON.stringify(libraryData, null, 2), 'utf8');
|
||
} catch (err) {
|
||
console.error('[LibraryEngine] 保存失败:', err.message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 添加书籍到团队书库(去重:同书名+同作者视为重复)
|
||
*/
|
||
function addBook({ book_name, author, category, tags, source_file, added_by }) {
|
||
if (!book_name) throw new Error('book_name 为必填项');
|
||
|
||
// 去重检查
|
||
const existing = libraryData.books.find(
|
||
b => b.book_name === book_name.trim() && b.author === (author || '未知').trim()
|
||
);
|
||
if (existing) {
|
||
return { added: false, message: '书库中已存在同名同作者书籍', book: existing };
|
||
}
|
||
|
||
const bookId = `LIB-${Date.now()}-${crypto.randomBytes(3).toString('hex')}`;
|
||
const book = {
|
||
book_id: bookId,
|
||
book_name: book_name.trim(),
|
||
author: (author || '未知').trim(),
|
||
category: CATEGORIES.includes(category) ? category : '其他',
|
||
tags: Array.isArray(tags) ? tags.slice(0, 10) : [],
|
||
source_file: source_file || null,
|
||
added_by: added_by || 'system',
|
||
rating: 0,
|
||
rating_count: 0,
|
||
ref_count: 0,
|
||
reviews: [],
|
||
created_at: new Date().toISOString(),
|
||
updated_at: new Date().toISOString()
|
||
};
|
||
|
||
libraryData.books.push(book);
|
||
save();
|
||
|
||
return { added: true, book };
|
||
}
|
||
|
||
/**
|
||
* 列表(支持分区过滤、搜索、排序)
|
||
*/
|
||
function listBooks({ category, search, sort, limit } = {}) {
|
||
let result = [...libraryData.books];
|
||
|
||
if (category && CATEGORIES.includes(category)) {
|
||
result = result.filter(b => b.category === category);
|
||
}
|
||
|
||
if (search) {
|
||
const q = search.toLowerCase();
|
||
result = result.filter(b =>
|
||
b.book_name.toLowerCase().includes(q) ||
|
||
b.author.toLowerCase().includes(q) ||
|
||
(b.tags || []).some(t => t.toLowerCase().includes(q))
|
||
);
|
||
}
|
||
|
||
if (sort === 'rating') {
|
||
result.sort((a, b) => b.rating - a.rating);
|
||
} else if (sort === 'popular') {
|
||
result.sort((a, b) => b.ref_count - a.ref_count);
|
||
} else {
|
||
result.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||
}
|
||
|
||
if (limit) result = result.slice(0, limit);
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 获取单本书详情
|
||
*/
|
||
function getBook(bookId) {
|
||
if (!sanitizeId(bookId)) return null;
|
||
return libraryData.books.find(b => b.book_id === bookId) || null;
|
||
}
|
||
|
||
/**
|
||
* 添加评分
|
||
*/
|
||
function rateBook(bookId, score, memberId) {
|
||
if (!sanitizeId(bookId)) throw new Error('非法 bookId');
|
||
const book = libraryData.books.find(b => b.book_id === bookId);
|
||
if (!book) throw new Error('书籍不存在');
|
||
if (score < 1 || score > 5) throw new Error('评分范围 1-5');
|
||
|
||
// 简化:每人只算最新评分
|
||
const total = book.rating * book.rating_count + score;
|
||
book.rating_count++;
|
||
book.rating = Math.round((total / book.rating_count) * 10) / 10;
|
||
book.updated_at = new Date().toISOString();
|
||
save();
|
||
|
||
return { rating: book.rating, rating_count: book.rating_count };
|
||
}
|
||
|
||
/**
|
||
* 添加评论
|
||
*/
|
||
function addReview(bookId, { member_id, content }) {
|
||
if (!sanitizeId(bookId)) throw new Error('非法 bookId');
|
||
const book = libraryData.books.find(b => b.book_id === bookId);
|
||
if (!book) throw new Error('书籍不存在');
|
||
if (!content || content.trim().length === 0) throw new Error('评论内容不能为空');
|
||
|
||
const review = {
|
||
member_id: member_id || 'anonymous',
|
||
content: content.trim().slice(0, 500),
|
||
created_at: new Date().toISOString()
|
||
};
|
||
|
||
if (!book.reviews) book.reviews = [];
|
||
book.reviews.push(review);
|
||
book.updated_at = new Date().toISOString();
|
||
save();
|
||
|
||
return review;
|
||
}
|
||
|
||
/**
|
||
* 引用计数(成员添加到个人书架时+1)
|
||
*/
|
||
function incrementRef(bookId) {
|
||
const book = libraryData.books.find(b => b.book_id === bookId);
|
||
if (book) {
|
||
book.ref_count = (book.ref_count || 0) + 1;
|
||
save();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 统计
|
||
*/
|
||
function getStats() {
|
||
const books = libraryData.books;
|
||
const catCounts = {};
|
||
for (const cat of CATEGORIES) {
|
||
catCounts[cat] = books.filter(b => b.category === cat).length;
|
||
}
|
||
return {
|
||
total_books: books.length,
|
||
categories: catCounts,
|
||
total_reviews: books.reduce((s, b) => s + (b.reviews ? b.reviews.length : 0), 0),
|
||
avg_rating: books.length > 0
|
||
? Math.round(books.reduce((s, b) => s + b.rating, 0) / books.length * 10) / 10
|
||
: 0
|
||
};
|
||
}
|
||
|
||
init();
|
||
|
||
module.exports = {
|
||
addBook, listBooks, getBook, rateBook, addReview,
|
||
incrementRef, getStats, CATEGORIES
|
||
};
|