parent
742bcab6a7
commit
85b2729a04
|
@ -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();
|
||||||
|
|
|
@ -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("*"); // 允许所有来源
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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("无权操作此直播间");
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//}
|
|
|
@ -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; // 发送时间
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -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; // 最长连续阅读天数
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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> {
|
||||||
|
}
|
|
@ -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> {
|
||||||
|
}
|
|
@ -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> {
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -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`);
|
|
@ -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>
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -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>
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue