Files

736 lines
19 KiB
Go
Raw Permalink 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.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
}