feat(图书初步): 图书初步

图书初步
This commit is contained in:
ovo 2024-12-13 17:56:54 +08:00
parent 742bcab6a7
commit 85b2729a04
32 changed files with 1230 additions and 126 deletions

View File

@ -11,7 +11,6 @@ import org.springframework.context.annotation.Configuration;
@EsMapperScan("com.guwan.backend.es.mapper") @EsMapperScan("com.guwan.backend.es.mapper")
@EnableConfigurationProperties(EasyEsConfigProperties.class) @EnableConfigurationProperties(EasyEsConfigProperties.class)
public class EasyEsConfig { public class EasyEsConfig {
@Bean @Bean
public IndexStrategyFactory indexStrategyFactory() { public IndexStrategyFactory indexStrategyFactory() {
return new IndexStrategyFactory(); return new IndexStrategyFactory();

View File

@ -0,0 +1,18 @@
package com.guwan.backend.config;
import com.guwan.backend.websocket.ChatWebSocketHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new ChatWebSocketHandler(), "/ws/chat")
.setAllowedOrigins("*"); // 允许所有来源
}
}

View File

@ -20,6 +20,12 @@ public class SecurityConstants {
"/api/user/getEmailCode", // 获取邮箱验证码 "/api/user/getEmailCode", // 获取邮箱验证码
"/api/user/getPhoneCode", // 获取手机验证码 "/api/user/getPhoneCode", // 获取手机验证码
"/chat.html", "/chat.html",
"/polling-chat.html",
"/ws/chat/**",
"/api/polling-chat/**",
"/v3/api-docs/**", // Swagger API文档 "/v3/api-docs/**", // Swagger API文档
"/swagger-ui/**", // Swagger UI "/swagger-ui/**", // Swagger UI
"/swagger-ui.html", // Swagger UI HTML "/swagger-ui.html", // Swagger UI HTML

View File

@ -0,0 +1,106 @@
package com.guwan.backend.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.guwan.backend.common.Result;
import com.guwan.backend.entity.Book;
import com.guwan.backend.service.BookService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j
@Tag(name = "图书管理", description = "图书相关接口")
@RestController
@RequestMapping("/api/books")
@RequiredArgsConstructor
public class BookController {
private final BookService bookService;
@Operation(summary = "添加图书")
@PostMapping
public Result<Book> addBook(@RequestBody Book book) {
try {
return Result.success(bookService.addBook(book));
} catch (Exception e) {
log.error("添加图书失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "更新图书信息")
@PutMapping("/{id}")
public Result<Book> updateBook(@PathVariable Long id, @RequestBody Book book) {
try {
book.setId(id);
return Result.success(bookService.updateBook(book));
} catch (Exception e) {
log.error("更新图书失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "删除图书")
@DeleteMapping("/{id}")
public Result<Void> deleteBook(@PathVariable Long id) {
try {
bookService.deleteBook(id);
return Result.success();
} catch (Exception e) {
log.error("删除图书失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "获取图书详情")
@GetMapping("/{id}")
public Result<Book> getBook(@PathVariable Long id) {
try {
return Result.success(bookService.getBookById(id));
} catch (Exception e) {
log.error("获取图书详情失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "根据ISBN获取图书")
@GetMapping("/isbn/{isbn}")
public Result<Book> getBookByIsbn(@PathVariable String isbn) {
try {
return Result.success(bookService.getBookByIsbn(isbn));
} catch (Exception e) {
log.error("根据ISBN获取图书失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "分页查询图书列表")
@GetMapping
public Result<IPage<Book>> getBookList(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String keyword) {
try {
return Result.success(bookService.getBookList(pageNum, pageSize, keyword));
} catch (Exception e) {
log.error("查询图书列表失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "根据分类获取图书")
@GetMapping("/category/{category}")
public Result<IPage<Book>> getBooksByCategory(
@PathVariable String category,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
try {
return Result.success(bookService.getBooksByCategory(category, pageNum, pageSize));
} catch (Exception e) {
log.error("根据分类获取图书失败", e);
return Result.error(e.getMessage());
}
}
}

View File

@ -60,7 +60,7 @@ public class DemoController {
(bucketName, minioUtil.uploadFile(bucketName, file)))); (bucketName, minioUtil.uploadFile(bucketName, file))));
} }
@GetMapping("demo111") @GetMapping("/demo111")
public int saveTenPerson() { public int saveTenPerson() {
try { try {

View File

@ -1,82 +0,0 @@
//package com.guwan.backend.controller;
//
//import com.guwan.backend.entity.LiveRoom;
//import com.guwan.backend.entity.LiveRoomDTO;
//import com.guwan.backend.service.LiveService;
//import com.guwan.backend.util.Result;
//import com.guwan.backend.util.SecurityUtil;
//import lombok.RequiredArgsConstructor;
//import lombok.extern.slf4j.Slf4j;
//import org.springframework.web.bind.annotation.*;
//import io.swagger.v3.oas.annotations.Operation;
//import io.swagger.v3.oas.annotations.media.Schema;
//import io.swagger.v3.oas.annotations.security.SecurityRequirement;
//import io.swagger.v3.oas.annotations.tags.Tag;
//
//@Slf4j
//@Tag(name = "直播管理", description = "直播相关接口")
//@RestController
//@RequestMapping("/api/live")
//@RequiredArgsConstructor
//public class LiveController {
//
// private final LiveService liveService;
// private final SecurityUtil securityUtil;
//
// @Operation(summary = "创建直播间")
// @SecurityRequirement(name = "bearer-jwt")
// @PostMapping("/room")
// public Result<LiveRoom> createLiveRoom(@RequestBody LiveRoomDTO dto) {
// try {
// return Result.success(liveService.createLiveRoom(dto));
// } catch (Exception e) {
// log.error("创建直播间失败", e);
// return Result.error(e.getMessage());
// }
// }
//
// @Operation(summary = "开始直播")
// @SecurityRequirement(name = "bearer-jwt")
// @PostMapping("/room/{id}/start")
// public Result<Void> startLive(@PathVariable Long id) {
// try {
// // 检查权限
// checkPermission(id);
// liveService.startLive(id);
// return Result.success();
// } catch (Exception e) {
// log.error("开始直播失败", e);
// return Result.error(e.getMessage());
// }
// }
//
// @Operation(summary = "结束直播")
// @SecurityRequirement(name = "bearer-jwt")
// @PostMapping("/room/{id}/end")
// public Result<Void> endLive(@PathVariable Long id) {
// try {
// // 检查权限
// checkPermission(id);
// liveService.endLive(id);
// return Result.success();
// } catch (Exception e) {
// log.error("结束直播失败", e);
// return Result.error(e.getMessage());
// }
// }
//
// /**
// * 检查权限
// */
// private void checkPermission(Long roomId) {
// LiveRoom room = liveService.getLiveRoom(roomId);
// if (room == null) {
// throw new IllegalArgumentException("直播间不存在");
// }
//
// Long currentUserId = securityUtil.getCurrentUserId();
// if (!room.getUserId().equals(currentUserId)) {
// throw new IllegalStateException("无权操作此直播间");
// }
// }
//}

View File

@ -0,0 +1,60 @@
package com.guwan.backend.controller;
import com.guwan.backend.common.Result;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@Slf4j
@RestController
@RequestMapping("/api/polling-chat")
public class PollingChatController {
// 使用线程安全的列表存储消息
private static final List<Message> messages = new CopyOnWriteArrayList<>();
private static final int MAX_MESSAGES = 100; // 最多保存100条消息
// 发送消息
@PostMapping("/send")
public Result<Void> send(@RequestBody Message message) {
message.setTime(LocalDateTime.now());
messages.add(message);
// 只保留最近的消息
if (messages.size() > MAX_MESSAGES) {
messages.subList(0, messages.size() - MAX_MESSAGES).clear();
}
log.info("新消息: {}", message);
return Result.success();
}
// 获取消息(轮询)
@GetMapping("/messages")
public Result<List<Message>> getMessages(
@RequestParam(required = false) Integer lastIndex) {
if (lastIndex == null) {
// 首次请求,返回所有消息
return Result.success(new ArrayList<>(messages));
} else if (lastIndex < messages.size()) {
// 返回新消息
return Result.success(messages.subList(lastIndex, messages.size()));
} else {
// 没有新消息
return Result.success(Collections.emptyList());
}
}
@Data
public static class Message {
private String content; // 消息内容
private String sender; // 发送者
private LocalDateTime time; // 发送时间
}
}

View File

@ -0,0 +1,127 @@
package com.guwan.backend.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.guwan.backend.common.Result;
import com.guwan.backend.entity.ReadingNote;
import com.guwan.backend.service.ReadingNoteService;
import com.guwan.backend.util.SecurityUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j
@Tag(name = "阅读笔记", description = "阅读笔记相关接口")
@RestController
@RequestMapping("/api/reading-notes")
@RequiredArgsConstructor
public class ReadingNoteController {
private final ReadingNoteService noteService;
private final SecurityUtil securityUtil;
@Operation(summary = "添加笔记")
@PostMapping
public Result<ReadingNote> addNote(@RequestBody ReadingNote note) {
try {
note.setUserId(securityUtil.getCurrentUserId());
return Result.success(noteService.addNote(note));
} catch (Exception e) {
log.error("添加笔记失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "更新笔记")
@PutMapping("/{id}")
public Result<ReadingNote> updateNote(@PathVariable Long id, @RequestBody ReadingNote note) {
try {
note.setId(id);
note.setUserId(securityUtil.getCurrentUserId());
return Result.success(noteService.updateNote(note));
} catch (Exception e) {
log.error("更新笔记失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "删除笔记")
@DeleteMapping("/{id}")
public Result<Void> deleteNote(@PathVariable Long id) {
try {
noteService.deleteNote(id);
return Result.success();
} catch (Exception e) {
log.error("删除笔记失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "获取笔记详情")
@GetMapping("/{id}")
public Result<ReadingNote> getNote(@PathVariable Long id) {
try {
return Result.success(noteService.getNoteById(id));
} catch (Exception e) {
log.error("获取笔记详情失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "获取用户的所有笔记")
@GetMapping
public Result<IPage<ReadingNote>> getUserNotes(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
try {
Long userId = securityUtil.getCurrentUserId();
return Result.success(noteService.getUserNotes(userId, pageNum, pageSize));
} catch (Exception e) {
log.error("获取用户笔记失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "获取书籍的所有公开笔记")
@GetMapping("/book/{bookId}")
public Result<IPage<ReadingNote>> getBookNotes(
@PathVariable Long bookId,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
try {
return Result.success(noteService.getBookNotes(bookId, pageNum, pageSize));
} catch (Exception e) {
log.error("获取书籍笔记失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "获取用户在特定书籍上的笔记")
@GetMapping("/book/{bookId}/my")
public Result<IPage<ReadingNote>> getUserBookNotes(
@PathVariable Long bookId,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
try {
Long userId = securityUtil.getCurrentUserId();
return Result.success(noteService.getUserBookNotes(userId, bookId, pageNum, pageSize));
} catch (Exception e) {
log.error("获取用户书籍笔记失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "获取所有公开笔记")
@GetMapping("/public")
public Result<IPage<ReadingNote>> getPublicNotes(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
try {
return Result.success(noteService.getPublicNotes(pageNum, pageSize));
} catch (Exception e) {
log.error("获取公开笔记失败", e);
return Result.error(e.getMessage());
}
}
}

View File

@ -0,0 +1,104 @@
package com.guwan.backend.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.guwan.backend.common.Result;
import com.guwan.backend.dto.ReadingStatistics;
import com.guwan.backend.entity.ReadingProgress;
import com.guwan.backend.service.ReadingProgressService;
import com.guwan.backend.util.SecurityUtil;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
@Slf4j
@Tag(name = "阅读进度", description = "阅读进度相关接口")
@RestController
@RequestMapping("/api/reading-progress")
@RequiredArgsConstructor
public class ReadingProgressController {
private final ReadingProgressService progressService;
private final SecurityUtil securityUtil;
@Operation(summary = "更新阅读进度")
@PostMapping
public Result<ReadingProgress> updateProgress(@RequestBody ReadingProgress progress) {
try {
progress.setUserId(securityUtil.getCurrentUserId());
return Result.success(progressService.updateProgress(progress));
} catch (Exception e) {
log.error("更新阅读进度失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "获取书籍阅读进度")
@GetMapping("/book/{bookId}")
public Result<ReadingProgress> getProgress(@PathVariable Long bookId) {
try {
Long userId = securityUtil.getCurrentUserId();
return Result.success(progressService.getProgress(userId, bookId));
} catch (Exception e) {
log.error("获取阅读进度失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "获取用户的所有阅读进度")
@GetMapping
public Result<IPage<ReadingProgress>> getUserProgress(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
try {
Long userId = securityUtil.getCurrentUserId();
return Result.success(progressService.getUserProgress(userId, pageNum, pageSize));
} catch (Exception e) {
log.error("获取用户阅读进度失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "获取特定状态的书籍")
@GetMapping("/status/{status}")
public Result<IPage<ReadingProgress>> getProgressByStatus(
@PathVariable String status,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
try {
Long userId = securityUtil.getCurrentUserId();
return Result.success(progressService.getProgressByStatus(userId, status, pageNum, pageSize));
} catch (Exception e) {
log.error("获取阅读状态失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "更新阅读时长")
@PostMapping("/{bookId}/reading-time")
public Result<Void> updateReadingTime(
@PathVariable Long bookId,
@RequestParam Integer minutes) {
try {
Long userId = securityUtil.getCurrentUserId();
progressService.updateReadingTime(userId, bookId, minutes);
return Result.success();
} catch (Exception e) {
log.error("更新阅读时长失败", e);
return Result.error(e.getMessage());
}
}
@Operation(summary = "获取阅读统计")
@GetMapping("/statistics")
public Result<ReadingStatistics> getReadingStatistics() {
try {
Long userId = securityUtil.getCurrentUserId();
return Result.success(progressService.getReadingStatistics(userId));
} catch (Exception e) {
log.error("获取阅读统计失败", e);
return Result.error(e.getMessage());
}
}
}

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,13 @@
package com.guwan.backend.dto;
import lombok.Data;
@Data
public class ReadingStatistics {
private Integer totalBooks; // 总阅读书籍数
private Integer finishedBooks; // 已完成书籍数
private Integer totalReadingTime; // 总阅读时长(分钟)
private Integer dailyAverage; // 日均阅读时长(分钟)
private Integer currentStreak; // 当前连续阅读天数
private Integer longestStreak; // 最长连续阅读天数
}

View File

@ -0,0 +1,30 @@
package com.guwan.backend.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("book")
public class Book {
@TableId(type = IdType.AUTO)
private Long id;
private String isbn; // ISBN编号
private String title; // 书名
private String author; // 作者
private String publisher; // 出版社
private String description; // 描述
private String coverUrl; // 封面图片URL
private String category; // 分类
private String tags; // 标签(逗号分隔)
private Integer totalPages; // 总页数
private String language; // 语言
private LocalDateTime publishDate; // 出版日期
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
}

View File

@ -0,0 +1,27 @@
package com.guwan.backend.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("reading_note")
public class ReadingNote {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId; // 用户ID
private Long bookId; // 书籍ID
private String type; // 笔记类型:NOTE,HIGHLIGHT,THOUGHT
private String content; // 笔记内容
private String chapter; // 章节
private Integer pageNumber; // 页码
private String audioUrl; // 语音笔记URL
private Boolean isPublic; // 是否公开
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
}

View File

@ -0,0 +1,26 @@
package com.guwan.backend.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("reading_progress")
public class ReadingProgress {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId; // 用户ID
private Long bookId; // 书籍ID
private String status; // 状态:WANT_TO_READ,READING,FINISHED
private Integer currentPage; // 当前页码
private Double percentage; // 阅读进度(0-100)
private Integer readingTime; // 阅读时长(分钟)
private LocalDateTime lastReadTime; // 最后阅读时间
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
}

View File

@ -0,0 +1,9 @@
package com.guwan.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.guwan.backend.entity.Book;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface BookMapper extends BaseMapper<Book> {
}

View File

@ -0,0 +1,9 @@
package com.guwan.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.guwan.backend.entity.ReadingNote;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ReadingNoteMapper extends BaseMapper<ReadingNote> {
}

View File

@ -0,0 +1,9 @@
package com.guwan.backend.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.guwan.backend.entity.ReadingProgress;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ReadingProgressMapper extends BaseMapper<ReadingProgress> {
}

View File

@ -0,0 +1,27 @@
package com.guwan.backend.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.guwan.backend.entity.Book;
public interface BookService {
// 添加书籍
Book addBook(Book book);
// 更新书籍信息
Book updateBook(Book book);
// 删除书籍
void deleteBook(Long id);
// 获取书籍详情
Book getBookById(Long id);
// 根据ISBN获取书籍
Book getBookByIsbn(String isbn);
// 分页查询书籍列表
IPage<Book> getBookList(Integer pageNum, Integer pageSize, String keyword);
// 根据分类获取书籍
IPage<Book> getBooksByCategory(String category, Integer pageNum, Integer pageSize);
}

View File

@ -0,0 +1,30 @@
package com.guwan.backend.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.guwan.backend.entity.ReadingNote;
public interface ReadingNoteService {
// 添加笔记
ReadingNote addNote(ReadingNote note);
// 更新笔记
ReadingNote updateNote(ReadingNote note);
// 删除笔记
void deleteNote(Long id);
// 获取笔记详情
ReadingNote getNoteById(Long id);
// 获取用户的所有笔记
IPage<ReadingNote> getUserNotes(Long userId, Integer pageNum, Integer pageSize);
// 获取书籍的所有笔记
IPage<ReadingNote> getBookNotes(Long bookId, Integer pageNum, Integer pageSize);
// 获取用户在特定书籍上的笔记
IPage<ReadingNote> getUserBookNotes(Long userId, Long bookId, Integer pageNum, Integer pageSize);
// 获取公开的笔记
IPage<ReadingNote> getPublicNotes(Integer pageNum, Integer pageSize);
}

View File

@ -0,0 +1,24 @@
package com.guwan.backend.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.guwan.backend.entity.ReadingProgress;
public interface ReadingProgressService {
// 创建或更新阅读进度
ReadingProgress updateProgress(ReadingProgress progress);
// 获取用户的阅读进度
ReadingProgress getProgress(Long userId, Long bookId);
// 获取用户的所有阅读进度
IPage<ReadingProgress> getUserProgress(Long userId, Integer pageNum, Integer pageSize);
// 获取用户特定状态的书籍
IPage<ReadingProgress> getProgressByStatus(Long userId, String status, Integer pageNum, Integer pageSize);
// 更新阅读时长
void updateReadingTime(Long userId, Long bookId, Integer minutes);
// 获取用户的阅读统计
ReadingStatistics getReadingStatistics(Long userId);
}

View File

@ -0,0 +1,73 @@
package com.guwan.backend.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.guwan.backend.entity.Book;
import com.guwan.backend.mapper.BookMapper;
import com.guwan.backend.service.BookService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
public class BookServiceImpl implements BookService {
private final BookMapper bookMapper;
@Override
@Transactional
public Book addBook(Book book) {
bookMapper.insert(book);
return book;
}
@Override
@Transactional
public Book updateBook(Book book) {
bookMapper.updateById(book);
return book;
}
@Override
@Transactional
public void deleteBook(Long id) {
bookMapper.deleteById(id);
}
@Override
public Book getBookById(Long id) {
return bookMapper.selectById(id);
}
@Override
public Book getBookByIsbn(String isbn) {
return bookMapper.selectOne(
new LambdaQueryWrapper<Book>()
.eq(Book::getIsbn, isbn)
);
}
@Override
public IPage<Book> getBookList(Integer pageNum, Integer pageSize, String keyword) {
LambdaQueryWrapper<Book> wrapper = new LambdaQueryWrapper<>();
if (keyword != null && !keyword.isEmpty()) {
wrapper.like(Book::getTitle, keyword)
.or()
.like(Book::getAuthor, keyword)
.or()
.like(Book::getDescription, keyword);
}
return bookMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);
}
@Override
public IPage<Book> getBooksByCategory(String category, Integer pageNum, Integer pageSize) {
LambdaQueryWrapper<Book> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Book::getCategory, category);
return bookMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);
}
}

View File

@ -0,0 +1,87 @@
package com.guwan.backend.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.guwan.backend.entity.ReadingNote;
import com.guwan.backend.mapper.ReadingNoteMapper;
import com.guwan.backend.service.ReadingNoteService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
public class ReadingNoteServiceImpl implements ReadingNoteService {
private final ReadingNoteMapper noteMapper;
@Override
@Transactional
public ReadingNote addNote(ReadingNote note) {
noteMapper.insert(note);
return note;
}
@Override
@Transactional
public ReadingNote updateNote(ReadingNote note) {
noteMapper.updateById(note);
return note;
}
@Override
@Transactional
public void deleteNote(Long id) {
noteMapper.deleteById(id);
}
@Override
public ReadingNote getNoteById(Long id) {
return noteMapper.selectById(id);
}
@Override
public IPage<ReadingNote> getUserNotes(Long userId, Integer pageNum, Integer pageSize) {
return noteMapper.selectPage(
new Page<>(pageNum, pageSize),
new LambdaQueryWrapper<ReadingNote>()
.eq(ReadingNote::getUserId, userId)
.orderByDesc(ReadingNote::getCreatedTime)
);
}
@Override
public IPage<ReadingNote> getBookNotes(Long bookId, Integer pageNum, Integer pageSize) {
return noteMapper.selectPage(
new Page<>(pageNum, pageSize),
new LambdaQueryWrapper<ReadingNote>()
.eq(ReadingNote::getBookId, bookId)
.eq(ReadingNote::getIsPublic, true)
.orderByDesc(ReadingNote::getCreatedTime)
);
}
@Override
public IPage<ReadingNote> getUserBookNotes(Long userId, Long bookId, Integer pageNum, Integer pageSize) {
return noteMapper.selectPage(
new Page<>(pageNum, pageSize),
new LambdaQueryWrapper<ReadingNote>()
.eq(ReadingNote::getUserId, userId)
.eq(ReadingNote::getBookId, bookId)
.orderByDesc(ReadingNote::getCreatedTime)
);
}
@Override
public IPage<ReadingNote> getPublicNotes(Integer pageNum, Integer pageSize) {
return noteMapper.selectPage(
new Page<>(pageNum, pageSize),
new LambdaQueryWrapper<ReadingNote>()
.eq(ReadingNote::getIsPublic, true)
.orderByDesc(ReadingNote::getCreatedTime)
);
}
}

View File

@ -0,0 +1,85 @@
package com.guwan.backend.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.guwan.backend.entity.ReadingProgress;
import com.guwan.backend.mapper.ReadingProgressMapper;
import com.guwan.backend.service.ReadingProgressService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
@Slf4j
@Service
@RequiredArgsConstructor
public class ReadingProgressServiceImpl implements ReadingProgressService {
private final ReadingProgressMapper progressMapper;
@Override
@Transactional
public ReadingProgress updateProgress(ReadingProgress progress) {
progress.setLastReadTime(LocalDateTime.now());
// 检查是否存在记录
ReadingProgress existing = getProgress(progress.getUserId(), progress.getBookId());
if (existing != null) {
progress.setId(existing.getId());
progressMapper.updateById(progress);
} else {
progressMapper.insert(progress);
}
return progress;
}
@Override
public ReadingProgress getProgress(Long userId, Long bookId) {
return progressMapper.selectOne(
new LambdaQueryWrapper<ReadingProgress>()
.eq(ReadingProgress::getUserId, userId)
.eq(ReadingProgress::getBookId, bookId)
);
}
@Override
public IPage<ReadingProgress> getUserProgress(Long userId, Integer pageNum, Integer pageSize) {
return progressMapper.selectPage(
new Page<>(pageNum, pageSize),
new LambdaQueryWrapper<ReadingProgress>()
.eq(ReadingProgress::getUserId, userId)
.orderByDesc(ReadingProgress::getLastReadTime)
);
}
@Override
public IPage<ReadingProgress> getProgressByStatus(Long userId, String status, Integer pageNum, Integer pageSize) {
return progressMapper.selectPage(
new Page<>(pageNum, pageSize),
new LambdaQueryWrapper<ReadingProgress>()
.eq(ReadingProgress::getUserId, userId)
.eq(ReadingProgress::getStatus, status)
.orderByDesc(ReadingProgress::getLastReadTime)
);
}
@Override
@Transactional
public void updateReadingTime(Long userId, Long bookId, Integer minutes) {
ReadingProgress progress = getProgress(userId, bookId);
if (progress != null) {
progress.setReadingTime(progress.getReadingTime() + minutes);
progress.setLastReadTime(LocalDateTime.now());
progressMapper.updateById(progress);
}
}
@Override
public ReadingStatistics getReadingStatistics(Long userId) {
// TODO: 实现阅读统计逻辑
return new ReadingStatistics();
}
}

View File

@ -0,0 +1,47 @@
package com.guwan.backend.websocket;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Component
public class ChatWebSocketHandler extends TextWebSocketHandler {
// 存储所有连接的会话
private static final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
String sessionId = session.getId();
sessions.put(sessionId, session);
log.info("新的连接: {}", sessionId);
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
log.info("收到消息: {}", payload);
// 广播消息给所有连接的客户端
TextMessage response = new TextMessage(payload);
for (WebSocketSession webSocketSession : sessions.values()) {
if (webSocketSession.isOpen()) {
webSocketSession.sendMessage(response);
}
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
String sessionId = session.getId();
sessions.remove(sessionId);
log.info("连接关闭: {}", sessionId);
}
}

View File

@ -0,0 +1,71 @@
-- 图书表
CREATE TABLE `book` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '图书ID',
`isbn` varchar(20) DEFAULT NULL COMMENT 'ISBN编号',
`title` varchar(100) NOT NULL COMMENT '书名',
`author` varchar(100) DEFAULT NULL COMMENT '作者',
`publisher` varchar(100) DEFAULT NULL COMMENT '出版社',
`description` text COMMENT '描述',
`cover_url` varchar(255) DEFAULT NULL COMMENT '封面图片URL',
`category` varchar(50) DEFAULT NULL COMMENT '分类',
`tags` varchar(255) DEFAULT NULL COMMENT '标签(逗号分隔)',
`total_pages` int DEFAULT NULL COMMENT '总页数',
`language` varchar(20) DEFAULT NULL COMMENT '语言',
`publish_date` datetime DEFAULT NULL COMMENT '出版日期',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_isbn` (`isbn`),
KEY `idx_category` (`category`),
KEY `idx_created_time` (`created_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='图书表';
-- 阅读进度表
CREATE TABLE `reading_progress` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '进度ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`book_id` bigint NOT NULL COMMENT '图书ID',
`status` varchar(20) NOT NULL COMMENT '状态:WANT_TO_READ,READING,FINISHED',
`current_page` int DEFAULT NULL COMMENT '当前页码',
`percentage` decimal(5,2) DEFAULT NULL COMMENT '阅读进度(0-100)',
`reading_time` int DEFAULT '0' COMMENT '阅读时长(分钟)',
`last_read_time` datetime DEFAULT NULL COMMENT '最后阅读时间',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_book` (`user_id`,`book_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_book_id` (`book_id`),
KEY `idx_status` (`status`),
KEY `idx_last_read_time` (`last_read_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='阅读进度表';
-- 阅读笔记表
CREATE TABLE `reading_note` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '笔记ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`book_id` bigint NOT NULL COMMENT '图书ID',
`type` varchar(20) NOT NULL COMMENT '笔记类型:NOTE,HIGHLIGHT,THOUGHT',
`content` text NOT NULL COMMENT '笔记内容',
`chapter` varchar(100) DEFAULT NULL COMMENT '章节',
`page_number` int DEFAULT NULL COMMENT '页码',
`audio_url` varchar(255) DEFAULT NULL COMMENT '语音笔记URL',
`is_public` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否公开',
`created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_book_id` (`book_id`),
KEY `idx_type` (`type`),
KEY `idx_is_public` (`is_public`),
KEY `idx_created_time` (`created_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='阅读笔记表';
-- 添加外键约束
ALTER TABLE `reading_progress`
ADD CONSTRAINT `fk_progress_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
ADD CONSTRAINT `fk_progress_book` FOREIGN KEY (`book_id`) REFERENCES `book` (`id`);
ALTER TABLE `reading_note`
ADD CONSTRAINT `fk_note_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
ADD CONSTRAINT `fk_note_book` FOREIGN KEY (`book_id`) REFERENCES `book` (`id`);

View File

@ -1,67 +1,65 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<title>WebSocket Chat</title> <meta charset="UTF-8">
<title>WebSocket 聊天室</title>
<style> <style>
#messageArea { height: 300px; overflow-y: scroll; border: 1px solid #ccc; } #messageArea {
height: 300px;
overflow-y: scroll;
border: 1px solid #ccc;
margin-bottom: 10px;
padding: 10px;
}
</style> </style>
</head> </head>
<body> <body>
<div id="messageArea"></div> <div id="messageArea"></div>
<input type="text" id="username" placeholder="Your name"> <input type="text" id="messageInput" placeholder="输入消息...">
<input type="text" id="message" placeholder="Type a message..."> <button onclick="sendMessage()">发送</button>
<button onclick="sendMessage()">Send</button>
<script> <script>
let ws; let ws;
let username;
function connect() { function connect() {
username = document.getElementById('username').value; ws = new WebSocket('ws://localhost:8084/ws/chat');
if (!username) {
alert('Please enter your name');
return;
}
ws = new WebSocket('ws://localhost:8086/ws/chat');
ws.onopen = () => { ws.onopen = () => {
// 发送连接消息 addMessage('系统', '连接成功');
ws.send(JSON.stringify({
type: 'CONNECT',
from: username,
timestamp: Date.now()
}));
}; };
ws.onmessage = (event) => { ws.onmessage = (event) => {
const message = JSON.parse(event.data); addMessage('收到消息', event.data);
const messageArea = document.getElementById('messageArea'); };
messageArea.innerHTML += `<p><strong>${message.from}:</strong> ${message.content}</p>`;
messageArea.scrollTop = messageArea.scrollHeight; ws.onclose = () => {
addMessage('系统', '连接断开');
}; };
} }
function sendMessage() { function sendMessage() {
const content = document.getElementById('message').value; const input = document.getElementById('messageInput');
if (!content) return; const message = input.value;
if (message) {
ws.send(JSON.stringify({ ws.send(message);
type: 'CHAT', input.value = '';
from: username, }
content: content,
timestamp: Date.now()
}));
document.getElementById('message').value = '';
} }
// 页面加载完成后连接 function addMessage(from, text) {
window.onload = () => { const messageArea = document.getElementById('messageArea');
document.getElementById('username').onchange = connect; messageArea.innerHTML += `<p><strong>${from}:</strong> ${text}</p>`;
document.getElementById('message').onkeypress = (e) => { messageArea.scrollTop = messageArea.scrollHeight;
if (e.key === 'Enter') sendMessage(); }
};
// 连接WebSocket
connect();
// 支持按回车发送
document.getElementById('messageInput').onkeypress = function(e) {
if (e.key === 'Enter') {
sendMessage();
}
}; };
</script> </script>
</body> </body>

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,128 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>轮询聊天室</title>
<style>
body {
max-width: 800px;
margin: 20px auto;
padding: 0 20px;
font-family: Arial, sans-serif;
}
#messageArea {
height: 400px;
overflow-y: scroll;
border: 1px solid #ccc;
margin-bottom: 10px;
padding: 10px;
background: #f9f9f9;
}
.input-area {
display: flex;
gap: 10px;
}
#messageInput {
flex: 1;
padding: 8px;
}
button {
padding: 8px 20px;
cursor: pointer;
}
.message {
margin: 5px 0;
padding: 5px;
border-radius: 4px;
}
.message:nth-child(odd) {
background: #fff;
}
.time {
color: #999;
font-size: 0.9em;
}
</style>
</head>
<body>
<h2>轮询聊天室</h2>
<div id="messageArea"></div>
<div class="input-area">
<input type="text" id="nameInput" placeholder="你的名字" style="width: 100px;">
<input type="text" id="messageInput" placeholder="输入消息...">
<button onclick="sendMessage()">发送</button>
</div>
<script>
let lastIndex = 0;
// 发送消息
async function sendMessage() {
const nameInput = document.getElementById('nameInput');
const messageInput = document.getElementById('messageInput');
const name = nameInput.value || '匿名';
const content = messageInput.value;
if (!content) return;
try {
await fetch('/api/polling-chat/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
sender: name,
content: content
})
});
messageInput.value = '';
} catch (error) {
console.error('发送失败:', error);
}
}
// 轮询获取消息
async function pollMessages() {
try {
const response = await fetch(`/api/polling-chat/messages?lastIndex=${lastIndex}`);
const result = await response.json();
if (result.data && result.data.length > 0) {
result.data.forEach(addMessage);
lastIndex += result.data.length;
}
} catch (error) {
console.error('获取消息失败:', error);
}
// 继续轮询
setTimeout(pollMessages, 1000);
}
function addMessage(message) {
const messageArea = document.getElementById('messageArea');
const time = new Date(message.time).toLocaleTimeString();
const div = document.createElement('div');
div.className = 'message';
div.innerHTML = `
<span class="time">[${time}]</span>
<strong>${message.sender}:</strong>
${message.content}
`;
messageArea.appendChild(div);
messageArea.scrollTop = messageArea.scrollHeight;
}
// 支持按回车发送
document.getElementById('messageInput').onkeypress = function(e) {
if (e.key === 'Enter') {
sendMessage();
}
};
// 开始轮询
pollMessages();
</script>
</body>
</html>

View File

@ -0,0 +1,68 @@
package com.guwan.backend.service;
import com.guwan.backend.entity.Book;
import com.guwan.backend.mapper.BookMapper;
import com.guwan.backend.service.impl.BookServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.time.LocalDateTime;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
class BookServiceTest {
@Mock
private BookMapper bookMapper;
@InjectMocks
private BookServiceImpl bookService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void addBook_Success() {
// 准备测试数据
Book book = new Book();
book.setTitle("测试书籍");
book.setAuthor("测试作者");
book.setIsbn("9787000000000");
when(bookMapper.insert(any(Book.class))).thenReturn(1);
// 执行测试
Book result = bookService.addBook(book);
// 验证结果
assertNotNull(result);
assertEquals("测试书籍", result.getTitle());
verify(bookMapper, times(1)).insert(any(Book.class));
}
@Test
void getBookByIsbn_Success() {
// 准备测试数据
Book book = new Book();
book.setId(1L);
book.setTitle("测试书籍");
book.setIsbn("9787000000000");
when(bookMapper.selectOne(any())).thenReturn(book);
// 执行测试
Book result = bookService.getBookByIsbn("9787000000000");
// 验证结果
assertNotNull(result);
assertEquals("测试书籍", result.getTitle());
assertEquals("9787000000000", result.getIsbn());
}
}