完善功能

This commit is contained in:
2025-07-16 15:16:40 +08:00
parent b2871ec0d2
commit f68a5b360b
123 changed files with 4643 additions and 931 deletions

View File

@ -7,6 +7,8 @@ import (
"server/internal/model/do"
"server/internal/service"
"server/utility/ecode"
"github.com/gogf/gf/v2/database/gdb"
)
type sBook struct{}
@ -38,7 +40,10 @@ func (s *sBook) List(ctx context.Context, in *model.BookListIn) (out *model.Book
if in.IsRecommended != 0 {
m = m.Where(dao.Books.Columns().IsRecommended, in.IsRecommended)
}
if err = m.Page(in.Page, in.Size).ScanAndCount(&out.List, &out.Total, false); err != nil {
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
@ -127,12 +132,633 @@ func (s *sBook) Delete(ctx context.Context, in *model.BookDelIn) (out *model.Boo
return nil, ecode.NotFound.Sub("book_not_found")
}
_, err = dao.Books.Ctx(ctx).WherePri(in.Id).Delete()
// 开启事务,删除书籍及相关数据
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, ecode.Fail.Sub("book_delete_failed")
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
}