feat: Chatgpt

This commit is contained in:
irony 2025-12-06 19:46:34 +08:00
parent 844dbba527
commit e4c6b14576
4 changed files with 81 additions and 31 deletions

View File

@ -1,6 +1,5 @@
package com.novelreader.config;
import com.novelreader.model.ChapterDO;
import com.novelreader.model.NovelDO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -26,14 +25,17 @@ public class MongoIndexConfig {
@EventListener(ApplicationReadyEvent.class)
public void ensureIndexes() {
log.info("开始检查和创建MongoDB索引...");
try {
// Chapter集合索引 - 这是最关键的因为有2800万条数据
createChapterIndexes();
// Novel集合索引
createNovelIndexes();
// History集合索引
createHistoryIndexes();
log.info("MongoDB索引检查完成");
} catch (Exception e) {
log.error("创建索引失败", e);
@ -42,39 +44,60 @@ public class MongoIndexConfig {
private void createChapterIndexes() {
IndexOperations indexOps = mongoTemplate.indexOps("chapterDO");
// 复合索引: novelId + index (最重要的索引用于章节查询和排序)
// 复合索引: novelId index (最重要的索引用于章节查询和排序)
indexOps.ensureIndex(new Index()
.on("novelId", Sort.Direction.ASC)
.on("index", Sort.Direction.ASC)
.named("idx_novelId_index")
.background()); // 后台创建不阻塞
log.info("Chapter索引已确保: idx_novelId_index");
}
private void createNovelIndexes() {
IndexOperations indexOps = mongoTemplate.indexOps(NovelDO.class);
// 名称索引 (用于搜索)
indexOps.ensureIndex(new Index()
.on("name", Sort.Direction.ASC)
.named("idx_name")
.background());
// 作者索引 (用于搜索)
indexOps.ensureIndex(new Index()
.on("author", Sort.Direction.ASC)
.named("idx_author")
.background());
// 标签索引 (用于标签筛选)
indexOps.ensureIndex(new Index()
.on("tags", Sort.Direction.ASC)
.named("idx_tags")
.background());
log.info("Novel索引已确保: idx_name, idx_author, idx_tags");
}
private void createHistoryIndexes() {
IndexOperations indexOps = mongoTemplate.indexOps("history");
// 唯一索引: userId novelId防止重复历史记录
indexOps.ensureIndex(new Index()
.on("userId", Sort.Direction.ASC)
.on("novelId", Sort.Direction.ASC)
.unique()
.named("ux_user_novel")
.background());
// 辅助索引按时间排序
indexOps.ensureIndex(new Index()
.on("userId", Sort.Direction.ASC)
.on("lastReadAt", Sort.Direction.DESC)
.named("idx_user_lastReadAt")
.background());
log.info("History索引已确保: ux_user_novel, idx_user_lastReadAt");
}
}

View File

@ -6,6 +6,11 @@ import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.mongodb.core.FindAndModifyOptions;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
import java.time.Instant;
@ -14,36 +19,41 @@ import java.util.Optional;
@Service
@RequiredArgsConstructor
public class HistoryService {
private final HistoryRepository historyRepository;
private final MongoTemplate mongoTemplate;
private static final String DEFAULT_USER_ID = "default_user";
public Page<History> getHistory(String userId, int page, int size) {
String uid = userId != null ? userId : DEFAULT_USER_ID;
Pageable pageable = PageRequest.of(page, size);
return historyRepository.findByUserIdOrderByLastReadAtDesc(uid, pageable);
}
public Optional<History> getHistoryByNovel(String userId, String novelId) {
String uid = userId != null ? userId : DEFAULT_USER_ID;
return historyRepository.findByUserIdAndNovelId(uid, novelId);
}
public History updateHistory(String userId, String novelId, String chapterId, Integer position) {
String uid = userId != null ? userId : DEFAULT_USER_ID;
Optional<History> existing = historyRepository.findByUserIdAndNovelId(uid, novelId);
History history = existing.orElse(new History());
history.setUserId(uid);
history.setNovelId(novelId);
history.setChapterId(chapterId);
history.setPosition(position != null ? position : 0);
history.setLastReadAt(Instant.now());
return historyRepository.save(history);
Query query = new Query(Criteria.where("userId").is(uid).and("novelId").is(novelId));
Update update = new Update()
.set("userId", uid)
.set("novelId", novelId)
.set("chapterId", chapterId)
.set("position", position != null ? position : 0)
.set("lastReadAt", Instant.now());
return mongoTemplate.findAndModify(
query,
update,
FindAndModifyOptions.options().returnNew(true).upsert(true),
History.class
);
}
public void deleteHistory(String userId, String novelId) {
String uid = userId != null ? userId : DEFAULT_USER_ID;
historyRepository.deleteByUserIdAndNovelId(uid, novelId);

View File

@ -13,8 +13,17 @@ const App = (function() {
handleRoute();
}
// 记录上一个路由,用于返回
let lastHash = '#/shelf';
let lastNonReadHash = '#/shelf';
function handleRoute() {
const hash = window.location.hash || '#/shelf';
const prevHash = lastHash;
lastHash = hash;
if (!hash.includes('#/read/')) {
lastNonReadHash = hash;
}
const [path, ...params] = hash.slice(2).split('/');
// 更新导航状态
@ -52,6 +61,9 @@ const App = (function() {
case 'read':
if (params[0]) {
const chapterIndex = parseInt(params[1]) || 0;
// 记录进入阅读页前的路由,用于返回时回到详情页或来源页
const backRoute = prevHash && !prevHash.includes('#/read/') ? prevHash : lastNonReadHash;
Store.setState('prevRouteBeforeRead', backRoute || '#/shelf');
showPage('reader');
Reader.init(params[0], chapterIndex);
}

View File

@ -507,12 +507,17 @@ const Reader = (function() {
// 解绑事件
unbindEvents();
// 返回详情页showPage会处理导航栏显示状态
if (currentNovelId) {
window.location.hash = '#/novel/' + currentNovelId;
// 使用记录的前一页面替换当前阅读页,避免回退再次进入阅读
const prevRoute = Store.getState('prevRouteBeforeRead');
if (prevRoute) {
window.location.replace(prevRoute);
} else if (currentNovelId) {
window.location.replace('#/novel/' + currentNovelId);
} else {
window.location.hash = '#/shelf';
window.location.replace('#/shelf');
}
// 清理记录
Store.setState('prevRouteBeforeRead', null);
}
function scrollToTop() {