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.IsHot { m = m.Where(dao.Books.Columns().IsHot, 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 } } // 查询用户书架 type shelfRow struct { BookId int64 `json:"bookId"` } shelves := make([]shelfRow, 0) err = dao.Bookshelves.Ctx(ctx). Fields("book_id"). Where(dao.Bookshelves.Columns().UserId, in.UserId). WhereIn(dao.Bookshelves.Columns().BookId, bookIds). Scan(&shelves) if err == nil && len(shelves) > 0 { shelfMap := make(map[int64]bool, len(shelves)) for _, s := range shelves { shelfMap[s.BookId] = true } for i := range out.List { out.List[i].IsInBookshelf = shelfMap[out.List[i].Id] } } else { for i := range out.List { out.List[i].IsInBookshelf = false } } } 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). 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). 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).Data(do.BookRatings{ UserId: in.UserId, BookId: in.BookId, Score: in.Rating, }).Insert() if err != nil { return ecode.Fail.Sub("rating_create_failed") } } // 重新计算书籍平均评分 var result struct { AvgRating float64 `json:"avg_rating"` } err = dao.BookRatings.Ctx(ctx). Where(dao.BookRatings.Columns().BookId, in.BookId). Fields("COALESCE(AVG(" + dao.BookRatings.Columns().Score + "), 0) as avg_rating"). Scan(&result) if err != nil { return ecode.Fail.Sub("rating_calculation_failed") } // 更新书籍的平均评分 _, err = dao.Books.Ctx(ctx). Where(dao.Books.Columns().Id, in.BookId). Data(do.Books{ Rating: result.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.WithAll().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 { // 查询用户是否已将本书加入书架 count, err := dao.Bookshelves.Ctx(ctx). Where(dao.Bookshelves.Columns().UserId, in.UserId). Where(dao.Bookshelves.Columns().BookId, in.Id). Count() if err == nil { out.IsInBookshelf = count > 0 } // 查询用户是否已评分 var ratingRecord struct { Score float64 `json:"score"` } err = dao.BookRatings.Ctx(ctx). Fields("score"). Where(dao.BookRatings.Columns().UserId, in.UserId). Where(dao.BookRatings.Columns().BookId, in.Id). Scan(&ratingRecord) if err == nil && ratingRecord.Score > 0 { out.HasRated = true out.MyRating = ratingRecord.Score } // 查询用户对该书籍的历史记录 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). WhereGT(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{} 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 ( ids []int64 extraMap map[int64]struct { Progress int LastReadAt string } total int ) switch in.Type { case 1, 2: var bookshelves []struct { BookId int64 `json:"bookId"` LastReadPercent float64 `json:"lastReadPercent"` LastReadAt string `json:"lastReadAt"` } readStatus := 1 if in.Type == 2 { readStatus = 2 } q := 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, readStatus) if in.Sort != "" { q = q.Order(in.Sort) } else { q = q } err = q.Page(in.Page, in.Size).ScanAndCount(&bookshelves, &total, false) if err != nil { return nil, ecode.Fail.Sub("bookshelf_query_failed") } ids = make([]int64, 0, len(bookshelves)) extraMap = make(map[int64]struct { Progress int LastReadAt string }, len(bookshelves)) for _, bs := range bookshelves { ids = append(ids, bs.BookId) extraMap[bs.BookId] = struct { Progress int LastReadAt string }{ Progress: int(bs.LastReadPercent), LastReadAt: bs.LastReadAt, } } case 3: var histories []struct { BookId int64 `json:"bookId"` ChapterId int64 `json:"chapterId"` ReadAt string `json:"readAt"` } q := dao.UserReadHistory.Ctx(ctx). Fields("book_id, chapter_id, read_at"). Where(dao.UserReadHistory.Columns().UserId, in.UserId) if in.Sort != "" { q = q.Order(in.Sort) } else { q = q.OrderDesc(dao.UserReadHistory.Columns().ReadAt) } err = q.Page(in.Page, in.Size).ScanAndCount(&histories, &total, false) if err != nil { return nil, ecode.Fail.Sub("history_query_failed") } ids = make([]int64, 0, len(histories)) extraMap = make(map[int64]struct { Progress int LastReadAt string }, len(histories)) for _, h := range histories { ids = append(ids, h.BookId) extraMap[h.BookId] = struct { Progress int LastReadAt string }{ Progress: 0, LastReadAt: h.ReadAt, } } } var books []model.MyBookItem if len(ids) > 0 { err = dao.Books.Ctx(ctx).WhereIn(dao.Books.Columns().Id, ids).Scan(&books) if err != nil { return nil, ecode.Fail.Sub("book_query_failed") } } // type3 需要查书架 shelfBookIds := map[int64]bool{} if in.Type == 3 && len(ids) > 0 { var bookshelves []struct{ BookId int64 } err = dao.Bookshelves.Ctx(ctx). Fields("book_id"). Where(dao.Bookshelves.Columns().UserId, in.UserId). WhereIn(dao.Bookshelves.Columns().BookId, ids). Scan(&bookshelves) if err != nil { return nil, ecode.Fail.Sub("bookshelf_query_failed") } for _, bs := range bookshelves { shelfBookIds[bs.BookId] = true } } for i := range books { if info, ok := extraMap[books[i].Id]; ok { books[i].Progress = info.Progress books[i].LastReadAt = info.LastReadAt } if in.Type == 1 || in.Type == 2 { books[i].IsInShelf = true } else if in.Type == 3 { books[i].IsInShelf = shelfBookIds[books[i].Id] } } // 查询用户评分信息 if len(books) > 0 { bookIds := make([]int64, 0, len(books)) for _, book := range books { bookIds = append(bookIds, book.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 books { if score, ok := ratingMap[books[i].Id]; ok { books[i].HasRated = true books[i].MyRating = score } else { books[i].HasRated = false books[i].MyRating = 0 } } } else { for i := range books { books[i].HasRated = false books[i].MyRating = 0 } } } out.Total = total out.List = books 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 } // SetHot: 单独修改书籍的热门状态 func (s *sBook) SetHot(ctx context.Context, in *model.BookSetHotIn) (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{ IsHot: in.IsHot, }).Update() if err != nil { return nil, ecode.Fail.Sub("book_update_failed") } return &model.BookCRUDOut{Success: true}, nil }