Files
novel_server/internal/logic/book/book.go
2025-07-16 15:16:40 +08:00

765 lines
20 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package book
import (
"context"
"server/internal/dao"
"server/internal/model"
"server/internal/model/do"
"server/internal/service"
"server/utility/ecode"
"github.com/gogf/gf/v2/database/gdb"
)
type sBook struct{}
func New() service.IBook {
return &sBook{}
}
func init() {
service.RegisterBook(New())
}
// List retrieves a paginated list of books
func (s *sBook) List(ctx context.Context, in *model.BookListIn) (out *model.BookListOut, err error) {
out = &model.BookListOut{}
m := dao.Books.Ctx(ctx)
if in.Title != "" {
m = m.Where(dao.Books.Columns().Title+" like ?", "%"+in.Title+"%")
}
if in.CategoryId != 0 {
m = m.Where(dao.Books.Columns().CategoryId, in.CategoryId)
}
if in.AuthorId != 0 {
m = m.Where(dao.Books.Columns().AuthorId, in.AuthorId)
}
if in.Status != 0 {
m = m.Where(dao.Books.Columns().Status, in.Status)
}
if in.IsRecommended != 0 {
m = m.Where(dao.Books.Columns().IsRecommended, in.IsRecommended)
}
if in.Sort != "" {
m = m.Order(in.Sort)
}
if err = m.Page(in.Page, in.Size).WithAll().ScanAndCount(&out.List, &out.Total, false); err != nil {
return
}
return
}
func (s *sBook) Create(ctx context.Context, in *model.BookAddIn) (out *model.BookCRUDOut, err error) {
exist, err := dao.Books.Ctx(ctx).
Where(dao.Books.Columns().Title, in.Title).
Exist()
if err != nil {
return nil, ecode.Fail.Sub("book_query_failed")
}
if exist {
return nil, ecode.Params.Sub("book_exists")
}
if _, err := dao.Books.Ctx(ctx).Data(do.Books{
AuthorId: in.AuthorId,
CategoryId: in.CategoryId,
Title: in.Title,
CoverUrl: in.CoverUrl,
Description: in.Description,
Status: in.Status,
Tags: in.Tags,
IsRecommended: in.IsRecommended,
}).Insert(); err != nil {
return nil, ecode.Fail.Sub("book_create_failed")
}
return &model.BookCRUDOut{
Success: true,
}, nil
}
func (s *sBook) Update(ctx context.Context, in *model.BookEditIn) (out *model.BookCRUDOut, err error) {
exist, err := dao.Books.Ctx(ctx).
WherePri(in.Id).
Exist()
if err != nil {
return nil, ecode.Fail.Sub("book_query_failed")
}
if !exist {
return nil, ecode.NotFound.Sub("book_not_found")
}
exist, err = dao.Books.Ctx(ctx).
Where(dao.Books.Columns().Title, in.Title).
Where("id != ?", in.Id).
Exist()
if err != nil {
return nil, ecode.Fail.Sub("book_query_failed")
}
if exist {
return nil, ecode.Params.Sub("book_exists")
}
_, err = dao.Books.Ctx(ctx).
WherePri(in.Id).
Data(do.Books{
AuthorId: in.AuthorId,
CategoryId: in.CategoryId,
Title: in.Title,
CoverUrl: in.CoverUrl,
Description: in.Description,
Status: in.Status,
Tags: in.Tags,
IsRecommended: in.IsRecommended,
}).Update()
if err != nil {
return nil, ecode.Fail.Sub("book_update_failed")
}
return &model.BookCRUDOut{
Success: true,
}, nil
}
func (s *sBook) Delete(ctx context.Context, in *model.BookDelIn) (out *model.BookCRUDOut, err error) {
exist, err := dao.Books.Ctx(ctx).
WherePri(in.Id).
Exist()
if err != nil {
return nil, ecode.Fail.Sub("book_query_failed")
}
if !exist {
return nil, ecode.NotFound.Sub("book_not_found")
}
// 开启事务,删除书籍及相关数据
err = dao.Books.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
// 1. 删除书籍相关的章节
_, err := dao.Chapters.Ctx(ctx).TX(tx).
Where(dao.Chapters.Columns().BookId, in.Id).
Delete()
if err != nil {
return ecode.Fail.Sub("chapter_delete_failed")
}
// 2. 删除书籍相关的用户阅读记录
_, err = dao.UserReadRecords.Ctx(ctx).TX(tx).
Where(dao.UserReadRecords.Columns().BookId, in.Id).
Delete()
if err != nil {
return ecode.Fail.Sub("read_record_delete_failed")
}
// 3. 删除书籍相关的用户阅读历史
_, err = dao.UserReadHistory.Ctx(ctx).TX(tx).
Where(dao.UserReadHistory.Columns().BookId, in.Id).
Delete()
if err != nil {
return ecode.Fail.Sub("history_delete_failed")
}
// 4. 删除书籍相关的用户书架
_, err = dao.Bookshelves.Ctx(ctx).TX(tx).
Where(dao.Bookshelves.Columns().BookId, in.Id).
Delete()
if err != nil {
return ecode.Fail.Sub("bookshelf_delete_failed")
}
// 5. 删除书籍相关的用户评分
_, err = dao.BookRatings.Ctx(ctx).TX(tx).
Where(dao.BookRatings.Columns().BookId, in.Id).
Delete()
if err != nil {
return ecode.Fail.Sub("rating_delete_failed")
}
// 6. 删除书籍相关的章节购买记录
_, err = dao.UserChapterPurchases.Ctx(ctx).TX(tx).
Where(dao.UserChapterPurchases.Columns().BookId, in.Id).
Delete()
if err != nil {
return ecode.Fail.Sub("purchase_delete_failed")
}
// 7. 最后删除书籍
_, err = dao.Books.Ctx(ctx).TX(tx).WherePri(in.Id).Delete()
if err != nil {
return ecode.Fail.Sub("book_delete_failed")
}
return nil
})
if err != nil {
return nil, err
}
return &model.BookCRUDOut{
Success: true,
}, nil
}
// AppList retrieves book list for app
func (s *sBook) AppList(ctx context.Context, in *model.BookAppListIn) (out *model.BookAppListOut, err error) {
out = &model.BookAppListOut{}
// 构建查询条件使用WithAll查询关联的作者信息
m := dao.Books.Ctx(ctx).WithAll()
// 书名模糊搜索
if in.Title != "" {
m = m.Where(dao.Books.Columns().Title+" like ?", "%"+in.Title+"%")
}
// 分类筛选
if in.CategoryId != 0 {
m = m.Where(dao.Books.Columns().CategoryId, in.CategoryId)
}
// 推荐筛选
if in.IsRecommended {
m = m.Where(dao.Books.Columns().IsRecommended, 1)
}
// 最新筛选(按创建时间倒序)
if in.IsLatest == 1 {
m = m.OrderDesc(dao.Books.Columns().CreatedAt)
}
if in.AuthorId != 0 {
m = m.Where(dao.Books.Columns().AuthorId, in.AuthorId)
}
if in.IsFeatured {
m = m.Where(dao.Books.Columns().IsFeatured, 1)
}
if in.Sort != "" {
m = m.Order(in.Sort)
}
// 执行分页查询
if err = m.Page(in.Page, in.Size).ScanAndCount(&out.List, &out.Total, false); err != nil {
return nil, ecode.Fail.Sub("book_query_failed")
}
// 用户评分批量查询
if in.UserId > 0 && len(out.List) > 0 {
bookIds := make([]int64, 0, len(out.List))
for _, item := range out.List {
bookIds = append(bookIds, item.Id)
}
// 查询用户对这些书的评分
type ratingRow struct {
BookId int64 `json:"bookId"`
Score float64 `json:"score"`
}
ratings := make([]ratingRow, 0)
err = dao.BookRatings.Ctx(ctx).
Fields("book_id, score").
Where(dao.BookRatings.Columns().UserId, in.UserId).
WhereIn(dao.BookRatings.Columns().BookId, bookIds).
Scan(&ratings)
if err == nil && len(ratings) > 0 {
ratingMap := make(map[int64]float64, len(ratings))
for _, r := range ratings {
ratingMap[r.BookId] = r.Score
}
for i := range out.List {
if score, ok := ratingMap[out.List[i].Id]; ok {
out.List[i].HasRated = true
out.List[i].MyRating = score
} else {
out.List[i].HasRated = false
out.List[i].MyRating = 0
}
}
} else {
for i := range out.List {
out.List[i].HasRated = false
out.List[i].MyRating = 0
}
}
}
return out, nil
}
// AppRate rates a book for app
func (s *sBook) AppRate(ctx context.Context, in *model.BookAppRateIn) (out *model.BookAppRateOut, err error) {
out = &model.BookAppRateOut{}
// 必须指定用户ID
if in.UserId == 0 {
return nil, ecode.Fail.Sub("user_id_required")
}
// 必须指定书籍ID
if in.BookId == 0 {
return nil, ecode.Fail.Sub("book_id_required")
}
// 验证评分范围1-10分
if in.Rating < 1 || in.Rating > 10 {
return nil, ecode.Fail.Sub("rating_invalid")
}
// 检查书籍是否存在
exist, err := dao.Books.Ctx(ctx).Where(dao.Books.Columns().Id, in.BookId).Exist()
if err != nil {
return nil, ecode.Fail.Sub("book_query_failed")
}
if !exist {
return nil, ecode.NotFound.Sub("book_not_found")
}
// 检查用户是否存在
exist, err = dao.Users.Ctx(ctx).Where(dao.Users.Columns().Id, in.UserId).Exist()
if err != nil {
return nil, ecode.Fail.Sub("user_query_failed")
}
if !exist {
return nil, ecode.NotFound.Sub("user_not_found")
}
// 开启事务处理评分
if err := dao.BookRatings.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
// 检查是否已经评分过
exist, err := dao.BookRatings.Ctx(ctx).TX(tx).
Where(dao.BookRatings.Columns().UserId, in.UserId).
Where(dao.BookRatings.Columns().BookId, in.BookId).
Exist()
if err != nil {
return ecode.Fail.Sub("rating_query_failed")
}
if exist {
// 更新现有评分
_, err = dao.BookRatings.Ctx(ctx).TX(tx).
Where(dao.BookRatings.Columns().UserId, in.UserId).
Where(dao.BookRatings.Columns().BookId, in.BookId).
Data(do.BookRatings{
Score: in.Rating,
}).Update()
if err != nil {
return ecode.Fail.Sub("rating_update_failed")
}
} else {
// 创建新评分记录
_, err = dao.BookRatings.Ctx(ctx).TX(tx).Data(do.BookRatings{
UserId: in.UserId,
BookId: in.BookId,
Score: in.Rating,
}).Insert()
if err != nil {
return ecode.Fail.Sub("rating_create_failed")
}
}
// 重新计算书籍平均评分
var avgRating float64
err = dao.BookRatings.Ctx(ctx).TX(tx).
Where(dao.BookRatings.Columns().BookId, in.BookId).
Fields("AVG(score) as avg_rating").
Scan(&avgRating)
if err != nil {
return ecode.Fail.Sub("rating_calculation_failed")
}
// 更新书籍的平均评分
_, err = dao.Books.Ctx(ctx).TX(tx).
Where(dao.Books.Columns().Id, in.BookId).
Data(do.Books{
Rating: avgRating,
}).Update()
if err != nil {
return ecode.Fail.Sub("book_rating_update_failed")
}
return nil
}); err != nil {
return nil, err
}
out.Success = true
return out, nil
}
// AppDetail retrieves book detail for app
func (s *sBook) AppDetail(ctx context.Context, in *model.BookAppDetailIn) (out *model.BookAppDetailOut, err error) {
out = &model.BookAppDetailOut{}
// 必须指定书籍ID
if in.Id == 0 {
return nil, ecode.Fail.Sub("book_id_required")
}
// 构建查询条件
m := dao.Books.Ctx(ctx)
m = m.Where(dao.Books.Columns().Id, in.Id)
// 执行查询
if err = m.Scan(out); err != nil {
return nil, ecode.Fail.Sub("book_query_failed")
}
// 检查书籍是否存在
if out.Id == 0 {
return nil, ecode.NotFound.Sub("book_not_found")
}
// 如果用户已登录,查询阅读进度
if in.UserId > 0 {
// 查询用户对该书籍的历史记录
var historyRecord struct {
ChapterId int64 `json:"chapterId"`
ReadAt string `json:"readAt"`
}
err = dao.UserReadHistory.Ctx(ctx).
Fields("chapter_id, read_at").
Where(dao.UserReadHistory.Columns().UserId, in.UserId).
Where(dao.UserReadHistory.Columns().BookId, in.Id).
OrderDesc(dao.UserReadHistory.Columns().ReadAt).
Scan(&historyRecord)
if err == nil && historyRecord.ChapterId > 0 {
// 用户读过这本书
out.HasRead = true
out.LastChapterId = historyRecord.ChapterId
out.LastReadAt = historyRecord.ReadAt
// 计算阅读进度:总章节数 vs 已读章节数
totalChapters, err := dao.Chapters.Ctx(ctx).
Where(dao.Chapters.Columns().BookId, in.Id).
Count()
if err == nil && totalChapters > 0 {
readChapters, err := dao.UserReadRecords.Ctx(ctx).
Where(dao.UserReadRecords.Columns().UserId, in.UserId).
Where(dao.UserReadRecords.Columns().BookId, in.Id).
Where(dao.UserReadRecords.Columns().ChapterId, ">", 0). // 只统计有章节ID的记录
Count()
if err == nil {
out.ReadProgress = int(float64(readChapters) / float64(totalChapters) * 100)
if out.ReadProgress > 100 {
out.ReadProgress = 100
}
}
}
}
}
return out, nil
}
func (s *sBook) MyList(ctx context.Context, in *model.MyBookListIn) (out *model.MyBookListOut, err error) {
out = &model.MyBookListOut{}
// 验证用户ID
if in.UserId == 0 {
return nil, ecode.Fail.Sub("user_id_required")
}
// 验证类型参数
if in.Type < 1 || in.Type > 3 {
return nil, ecode.Fail.Sub("type_invalid")
}
var list []model.MyBookItem
var total int
switch in.Type {
case 1: // 正在读
// 查询书架表中read_status=1的记录
var bookshelves []struct {
BookId int64 `json:"bookId"`
LastReadPercent float64 `json:"lastReadPercent"`
LastReadAt string `json:"lastReadAt"`
}
if in.Sort != "" {
err = dao.Bookshelves.Ctx(ctx).
Fields("book_id, last_read_percent, last_read_at").
Where(dao.Bookshelves.Columns().UserId, in.UserId).
Where(dao.Bookshelves.Columns().ReadStatus, 1).
Order(in.Sort).
Page(in.Page, in.Size).
ScanAndCount(&bookshelves, &total, false)
} else {
err = dao.Bookshelves.Ctx(ctx).
Fields("book_id, last_read_percent, last_read_at").
Where(dao.Bookshelves.Columns().UserId, in.UserId).
Where(dao.Bookshelves.Columns().ReadStatus, 1).
Page(in.Page, in.Size).
ScanAndCount(&bookshelves, &total, false)
}
if err != nil {
return nil, ecode.Fail.Sub("bookshelf_query_failed")
}
// 获取书籍详细信息
if len(bookshelves) > 0 {
bookIds := make([]int64, 0, len(bookshelves))
for _, bs := range bookshelves {
bookIds = append(bookIds, bs.BookId)
}
var books []model.Book
err = dao.Books.Ctx(ctx).
WhereIn(dao.Books.Columns().Id, bookIds).
Scan(&books)
if err != nil {
return nil, ecode.Fail.Sub("book_query_failed")
}
// 构建书籍ID到书架信息的映射
bookshelfMap := make(map[int64]struct {
LastReadPercent float64
LastReadAt string
})
for _, bs := range bookshelves {
bookshelfMap[bs.BookId] = struct {
LastReadPercent float64
LastReadAt string
}{
LastReadPercent: bs.LastReadPercent,
LastReadAt: bs.LastReadAt,
}
}
// 构建返回列表
for _, book := range books {
bsInfo := bookshelfMap[book.Id]
list = append(list, model.MyBookItem{
Id: book.Id,
Title: book.Title,
CoverUrl: book.CoverUrl,
Description: book.Description,
Progress: int(bsInfo.LastReadPercent),
IsInShelf: true,
LastReadAt: bsInfo.LastReadAt,
Status: book.Status,
AuthorId: book.AuthorId,
CategoryId: book.CategoryId,
})
}
}
case 2: // 已读完
// 查询书架表中read_status=2的记录
var bookshelves []struct {
BookId int64 `json:"bookId"`
LastReadPercent float64 `json:"lastReadPercent"`
LastReadAt string `json:"lastReadAt"`
}
if in.Sort != "" {
err = dao.Bookshelves.Ctx(ctx).
Fields("book_id, last_read_percent, last_read_at").
Where(dao.Bookshelves.Columns().UserId, in.UserId).
Where(dao.Bookshelves.Columns().ReadStatus, 2).
Order(in.Sort).
Page(in.Page, in.Size).
ScanAndCount(&bookshelves, &total, false)
} else {
err = dao.Bookshelves.Ctx(ctx).
Fields("book_id, last_read_percent, last_read_at").
Where(dao.Bookshelves.Columns().UserId, in.UserId).
Where(dao.Bookshelves.Columns().ReadStatus, 2).
Page(in.Page, in.Size).
ScanAndCount(&bookshelves, &total, false)
}
if err != nil {
return nil, ecode.Fail.Sub("bookshelf_query_failed")
}
// 获取书籍详细信息
if len(bookshelves) > 0 {
bookIds := make([]int64, 0, len(bookshelves))
for _, bs := range bookshelves {
bookIds = append(bookIds, bs.BookId)
}
var books []model.Book
err = dao.Books.Ctx(ctx).
WhereIn(dao.Books.Columns().Id, bookIds).
Scan(&books)
if err != nil {
return nil, ecode.Fail.Sub("book_query_failed")
}
// 构建书籍ID到书架信息的映射
bookshelfMap := make(map[int64]struct {
LastReadPercent float64
LastReadAt string
})
for _, bs := range bookshelves {
bookshelfMap[bs.BookId] = struct {
LastReadPercent float64
LastReadAt string
}{
LastReadPercent: bs.LastReadPercent,
LastReadAt: bs.LastReadAt,
}
}
// 构建返回列表
for _, book := range books {
bsInfo := bookshelfMap[book.Id]
list = append(list, model.MyBookItem{
Id: book.Id,
Title: book.Title,
CoverUrl: book.CoverUrl,
Description: book.Description,
Progress: int(bsInfo.LastReadPercent),
IsInShelf: true,
LastReadAt: bsInfo.LastReadAt,
Status: book.Status,
AuthorId: book.AuthorId,
CategoryId: book.CategoryId,
})
}
}
case 3: // 历史记录
// 查询user_read_history表
var histories []struct {
BookId int64 `json:"bookId"`
ChapterId int64 `json:"chapterId"`
ReadAt string `json:"readAt"`
}
if in.Sort != "" {
err = dao.UserReadHistory.Ctx(ctx).
Fields("book_id, chapter_id, read_at").
Where(dao.UserReadHistory.Columns().UserId, in.UserId).
Order(in.Sort).
Page(in.Page, in.Size).
ScanAndCount(&histories, &total, false)
} else {
err = dao.UserReadHistory.Ctx(ctx).
Fields("book_id, chapter_id, read_at").
Where(dao.UserReadHistory.Columns().UserId, in.UserId).
OrderDesc(dao.UserReadHistory.Columns().ReadAt).
Page(in.Page, in.Size).
ScanAndCount(&histories, &total, false)
}
if err != nil {
return nil, ecode.Fail.Sub("history_query_failed")
}
// 获取书籍详细信息
if len(histories) > 0 {
bookIds := make([]int64, 0, len(histories))
for _, history := range histories {
bookIds = append(bookIds, history.BookId)
}
var books []model.Book
err = dao.Books.Ctx(ctx).
WhereIn(dao.Books.Columns().Id, bookIds).
Scan(&books)
if err != nil {
return nil, ecode.Fail.Sub("book_query_failed")
}
// 构建书籍ID到历史信息的映射
historyMap := make(map[int64]struct {
ChapterId int64
ReadAt string
})
for _, history := range histories {
historyMap[history.BookId] = struct {
ChapterId int64
ReadAt string
}{
ChapterId: history.ChapterId,
ReadAt: history.ReadAt,
}
}
// 检查是否在书架中
var bookshelves []struct {
BookId int64 `json:"bookId"`
}
err = dao.Bookshelves.Ctx(ctx).
Fields("book_id").
Where(dao.Bookshelves.Columns().UserId, in.UserId).
WhereIn(dao.Bookshelves.Columns().BookId, bookIds).
Scan(&bookshelves)
if err != nil {
return nil, ecode.Fail.Sub("bookshelf_query_failed")
}
// 构建书架ID集合
shelfBookIds := make(map[int64]bool)
for _, bs := range bookshelves {
shelfBookIds[bs.BookId] = true
}
// 构建返回列表
for _, book := range books {
historyInfo := historyMap[book.Id]
list = append(list, model.MyBookItem{
Id: book.Id,
Title: book.Title,
CoverUrl: book.CoverUrl,
Description: book.Description,
Progress: 0, // 历史记录不显示进度
IsInShelf: shelfBookIds[book.Id],
LastReadAt: historyInfo.ReadAt,
Status: book.Status,
AuthorId: book.AuthorId,
CategoryId: book.CategoryId,
})
}
}
default:
// 返回空列表
}
out = &model.MyBookListOut{
Total: total,
List: list,
}
return
}
// SetFeatured: 单独修改书籍的精选状态
func (s *sBook) SetFeatured(ctx context.Context, in *model.BookSetFeaturedIn) (out *model.BookCRUDOut, err error) {
// 检查书籍是否存在
exist, err := dao.Books.Ctx(ctx).WherePri(in.Id).Exist()
if err != nil {
return nil, ecode.Fail.Sub("book_query_failed")
}
if !exist {
return nil, ecode.NotFound.Sub("book_not_found")
}
_, err = dao.Books.Ctx(ctx).WherePri(in.Id).Data(do.Books{
IsFeatured: in.IsFeatured,
}).Update()
if err != nil {
return nil, ecode.Fail.Sub("book_update_failed")
}
return &model.BookCRUDOut{Success: true}, nil
}
// SetRecommended: 单独修改书籍的推荐状态
func (s *sBook) SetRecommended(ctx context.Context, in *model.BookSetRecommendedIn) (out *model.BookCRUDOut, err error) {
// 检查书籍是否存在
exist, err := dao.Books.Ctx(ctx).WherePri(in.Id).Exist()
if err != nil {
return nil, ecode.Fail.Sub("book_query_failed")
}
if !exist {
return nil, ecode.NotFound.Sub("book_not_found")
}
_, err = dao.Books.Ctx(ctx).WherePri(in.Id).Data(do.Books{
IsRecommended: in.IsRecommended,
}).Update()
if err != nil {
return nil, ecode.Fail.Sub("book_update_failed")
}
return &model.BookCRUDOut{Success: true}, nil
}