package chapter import ( "context" "server/internal/dao" "server/internal/model" "server/internal/model/do" "server/internal/model/entity" "server/internal/service" "server/utility/ecode" "github.com/gogf/gf/v2/database/gdb" "github.com/gogf/gf/v2/os/gtime" ) type sChapter struct{} func New() service.IChapter { return &sChapter{} } func init() { service.RegisterChapter(New()) } // List retrieves a paginated list of chapters func (s *sChapter) List(ctx context.Context, in *model.ChapterListIn) (out *model.ChapterListOut, err error) { out = &model.ChapterListOut{} m := dao.Chapters.Ctx(ctx) if in.BookId != 0 { m = m.Where(dao.Chapters.Columns().BookId, in.BookId) } if in.Title != "" { m = m.Where(dao.Chapters.Columns().Title+" like ?", "%"+in.Title+"%") } if in.IsLocked != 0 { m = m.Where(dao.Chapters.Columns().IsLocked, in.IsLocked) } if err = m.Page(in.Page, in.Size).ScanAndCount(&out.List, &out.Total, false); err != nil { return } return } // Create creates a new chapter func (s *sChapter) Create(ctx context.Context, in *model.ChapterAddIn) (out *model.ChapterCRUDOut, err error) { if _, err := dao.Chapters.Ctx(ctx).Data(do.Chapters{ BookId: in.BookId, Title: in.Title, Content: in.Content, WordCount: in.WordCount, Sort: in.Sort, IsLocked: in.IsLocked, RequiredScore: in.RequiredScore, }).Insert(); err != nil { return nil, ecode.Fail.Sub("chapter_create_failed") } return &model.ChapterCRUDOut{Success: true}, nil } // Update updates an existing chapter func (s *sChapter) Update(ctx context.Context, in *model.ChapterEditIn) (out *model.ChapterCRUDOut, err error) { exist, err := dao.Chapters.Ctx(ctx). WherePri(in.Id). Exist() if err != nil { return nil, ecode.Fail.Sub("chapter_query_failed") } if !exist { return nil, ecode.NotFound.Sub("chapter_not_found") } _, err = dao.Chapters.Ctx(ctx). WherePri(in.Id). Data(do.Chapters{ BookId: in.BookId, Title: in.Title, Content: in.Content, WordCount: in.WordCount, Sort: in.Sort, IsLocked: in.IsLocked, RequiredScore: in.RequiredScore, }).Update() if err != nil { return nil, ecode.Fail.Sub("chapter_update_failed") } return &model.ChapterCRUDOut{Success: true}, nil } // Delete deletes a chapter by ID func (s *sChapter) Delete(ctx context.Context, in *model.ChapterDelIn) (out *model.ChapterCRUDOut, err error) { exist, err := dao.Chapters.Ctx(ctx). WherePri(in.Id). Exist() if err != nil { return nil, ecode.Fail.Sub("chapter_query_failed") } if !exist { return nil, ecode.NotFound.Sub("chapter_not_found") } // 开启事务,删除章节及相关数据 err = dao.Chapters.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { // 1. 删除章节相关的用户阅读记录 _, err := dao.UserReadRecords.Ctx(ctx).TX(tx). Where(dao.UserReadRecords.Columns().ChapterId, in.Id). Delete() if err != nil { return ecode.Fail.Sub("read_record_delete_failed") } // 2. 删除章节相关的购买记录 _, err = dao.UserChapterPurchases.Ctx(ctx).TX(tx). Where(dao.UserChapterPurchases.Columns().ChapterId, in.Id). Delete() if err != nil { return ecode.Fail.Sub("purchase_delete_failed") } // 3. 最后删除章节 _, err = dao.Chapters.Ctx(ctx).TX(tx).WherePri(in.Id).Delete() if err != nil { return ecode.Fail.Sub("chapter_delete_failed") } return nil }) if err != nil { return nil, err } return &model.ChapterCRUDOut{Success: true}, nil } // AppList retrieves chapter list for app without content func (s *sChapter) AppList(ctx context.Context, in *model.ChapterAppListIn) (out *model.ChapterAppListOut, err error) { out = &model.ChapterAppListOut{} // 构建查询条件 m := dao.Chapters.Ctx(ctx) // 必须指定 bookId if in.BookId == 0 { return nil, ecode.Fail.Sub("chapter_book_id_required") } m = m.Where(dao.Chapters.Columns().BookId, in.BookId) // 根据 isDesc 字段排序 if in.IsDesc { m = m.OrderDesc(dao.Chapters.Columns().Sort) } // 执行分页查询,获取列表和总数 if err = m.Page(in.Page, in.Size).ScanAndCount(&out.List, &out.Total, false); err != nil { return nil, ecode.Fail.Sub("chapter_query_failed") } // 如果指定了用户ID,查询阅读进度 if in.UserId > 0 { // 获取所有章节ID chapterIds := make([]int64, 0, len(out.List)) for _, item := range out.List { chapterIds = append(chapterIds, item.Id) } if len(chapterIds) > 0 { // 查询阅读记录 readRecords := make([]struct { ChapterId int64 `json:"chapterId"` ReadAt *gtime.Time `json:"readAt"` }, 0) err = dao.UserReadRecords.Ctx(ctx). Fields("chapter_id, read_at"). Where(dao.UserReadRecords.Columns().UserId, in.UserId). Where(dao.UserReadRecords.Columns().BookId, in.BookId). WhereIn(dao.UserReadRecords.Columns().ChapterId, chapterIds). Scan(&readRecords) if err != nil { return nil, ecode.Fail.Sub("read_record_query_failed") } // 构建阅读记录映射 readMap := make(map[int64]*gtime.Time) for _, record := range readRecords { readMap[record.ChapterId] = record.ReadAt } // 为每个章节设置阅读进度 for i := range out.List { if readAt, exists := readMap[out.List[i].Id]; exists { out.List[i].ReadAt = readAt out.List[i].ReadProgress = 100 // 有阅读记录表示已读 } else { out.List[i].ReadProgress = 0 // 未读 out.List[i].ReadAt = nil } } } } return out, nil } // AppDetail retrieves chapter detail for app func (s *sChapter) AppDetail(ctx context.Context, in *model.ChapterAppDetailIn) (out *model.ChapterAppDetailOut, err error) { out = &model.ChapterAppDetailOut{} // 构建查询条件 m := dao.Chapters.Ctx(ctx) // 必须指定章节ID if in.Id == 0 { return nil, ecode.Fail.Sub("chapter_id_required") } m = m.Where(dao.Chapters.Columns().Id, in.Id) // 执行查询 if err = m.Scan(out); err != nil { return nil, ecode.Fail.Sub("chapter_query_failed") } // 检查章节是否存在 if out.Id == 0 { return nil, ecode.NotFound.Sub("chapter_not_found") } return out, nil } // AppPurchase purchases chapter for app func (s *sChapter) AppPurchase(ctx context.Context, in *model.ChapterAppPurchaseIn) (out *model.ChapterAppPurchaseOut, err error) { out = &model.ChapterAppPurchaseOut{} // 必须指定章节ID if in.Id == 0 { return nil, ecode.Fail.Sub("chapter_id_required") } // 必须指定用户ID if in.UserId == 0 { return nil, ecode.Fail.Sub("user_id_required") } // 查询章节信息 chapter := &entity.Chapters{} err = dao.Chapters.Ctx(ctx).Where(dao.Chapters.Columns().Id, in.Id).Scan(chapter) if err != nil { return nil, ecode.Fail.Sub("chapter_query_failed") } // 检查章节是否存在 if chapter.Id == 0 { return nil, ecode.NotFound.Sub("chapter_not_found") } // 检查章节是否锁定 if chapter.IsLocked == 0 { // 章节免费,直接返回成功 out.Success = true return out, nil } // 查询用户信息 user := &entity.Users{} err = dao.Users.Ctx(ctx).Where(dao.Users.Columns().Id, in.UserId).Scan(user) if err != nil { return nil, ecode.Fail.Sub("user_query_failed") } // 检查用户是否存在 if user.Id == 0 { return nil, ecode.NotFound.Sub("user_not_found") } // 检查用户积分是否足够 if user.Points < uint64(chapter.RequiredScore) { return nil, ecode.Fail.Sub("insufficient_points") } // 检查是否已经购买过该章节 exist, err := dao.UserChapterPurchases.Ctx(ctx). Where(dao.UserChapterPurchases.Columns().UserId, in.UserId). Where(dao.UserChapterPurchases.Columns().ChapterId, in.Id). Exist() if err != nil { return nil, ecode.Fail.Sub("purchase_query_failed") } if exist { // 已经购买过,直接返回成功 out.Success = true return out, nil } // 开启事务处理 if err := dao.UserChapterPurchases.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { // 扣除用户积分 _, err := dao.Users.Ctx(ctx).TX(tx).Where(dao.Users.Columns().Id, in.UserId).Decrement(dao.Users.Columns().Points, chapter.RequiredScore) if err != nil { return ecode.Fail.Sub("score_deduction_failed") } // 记录购买记录 purchaseId, err := dao.UserChapterPurchases.Ctx(ctx).TX(tx).Data(do.UserChapterPurchases{ UserId: in.UserId, ChapterId: in.Id, BookId: chapter.BookId, PointsUsed: chapter.RequiredScore, PurchaseTime: gtime.Now(), }).InsertAndGetId() if err != nil { return ecode.Fail.Sub("purchase_record_failed") } // 记录用户积分日志 _, err = dao.UserPointsLogs.Ctx(ctx).TX(tx).Data(do.UserPointsLogs{ UserId: in.UserId, ChangeType: 1, // 消费 PointsChange: -chapter.RequiredScore, RelatedOrderId: purchaseId, Description: "购买章节:" + chapter.Title, CreatedAt: gtime.Now(), }).Insert() if err != nil { return ecode.Fail.Sub("point_log_failed") } return nil }); err != nil { return nil, err } out.Success = true return out, nil } // AppProgress uploads reading progress for app func (s *sChapter) AppProgress(ctx context.Context, in *model.ChapterAppProgressIn) (out *model.ChapterAppProgressOut, err error) { out = &model.ChapterAppProgressOut{} // 必须指定用户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") } // 必须指定章节ID if in.ChapterId == 0 { return nil, ecode.Fail.Sub("chapter_id_required") } // 验证进度范围 if in.Progress < 0 || in.Progress > 100 { return nil, ecode.Fail.Sub("progress_invalid") } // 检查章节是否存在 exist, err := dao.Chapters.Ctx(ctx).Where(dao.Chapters.Columns().Id, in.ChapterId).Exist() if err != nil { return nil, ecode.Fail.Sub("chapter_query_failed") } if !exist { return nil, ecode.NotFound.Sub("chapter_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.UserReadRecords.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { // 1. 更新或创建阅读记录 exist, err := dao.UserReadRecords.Ctx(ctx).TX(tx). Where(dao.UserReadRecords.Columns().UserId, in.UserId). Where(dao.UserReadRecords.Columns().BookId, in.BookId). Where(dao.UserReadRecords.Columns().ChapterId, in.ChapterId). Exist() if err != nil { return ecode.Fail.Sub("read_record_query_failed") } if exist { // 更新现有记录 _, err = dao.UserReadRecords.Ctx(ctx).TX(tx). Where(dao.UserReadRecords.Columns().UserId, in.UserId). Where(dao.UserReadRecords.Columns().BookId, in.BookId). Where(dao.UserReadRecords.Columns().ChapterId, in.ChapterId). Data(do.UserReadRecords{ Progress: in.Progress, ReadAt: gtime.Now(), }).Update() if err != nil { return ecode.Fail.Sub("read_record_update_failed") } } else { // 创建新记录 _, err = dao.UserReadRecords.Ctx(ctx).TX(tx).Data(do.UserReadRecords{ UserId: in.UserId, BookId: in.BookId, ChapterId: in.ChapterId, Progress: in.Progress, ReadAt: gtime.Now(), }).Insert() if err != nil { return ecode.Fail.Sub("read_record_create_failed") } } // 2. 更新或创建历史记录 exist, err = dao.UserReadHistory.Ctx(ctx).TX(tx). Where(dao.UserReadHistory.Columns().UserId, in.UserId). Where(dao.UserReadHistory.Columns().BookId, in.BookId). Exist() if err != nil { return ecode.Fail.Sub("history_query_failed") } if exist { // 更新现有历史记录 _, err = dao.UserReadHistory.Ctx(ctx).TX(tx). Where(dao.UserReadHistory.Columns().UserId, in.UserId). Where(dao.UserReadHistory.Columns().BookId, in.BookId). Data(do.UserReadHistory{ ChapterId: in.ChapterId, ReadAt: gtime.Now(), }).Update() if err != nil { return ecode.Fail.Sub("history_update_failed") } } else { // 创建新历史记录 _, err = dao.UserReadHistory.Ctx(ctx).TX(tx).Data(do.UserReadHistory{ UserId: in.UserId, BookId: in.BookId, ChapterId: in.ChapterId, ReadAt: gtime.Now(), }).Insert() if err != nil { return ecode.Fail.Sub("history_create_failed") } } // 3. 更新书架记录(如果存在) exist, err = dao.Bookshelves.Ctx(ctx).TX(tx). Where(dao.Bookshelves.Columns().UserId, in.UserId). Where(dao.Bookshelves.Columns().BookId, in.BookId). Exist() if err != nil { return ecode.Fail.Sub("bookshelf_query_failed") } if exist { // 计算阅读进度百分比 totalChapters, err := dao.Chapters.Ctx(ctx).TX(tx). Where(dao.Chapters.Columns().BookId, in.BookId). Count() if err != nil { return ecode.Fail.Sub("chapter_count_failed") } var readChapters int if totalChapters > 0 { readChapters, err = dao.UserReadRecords.Ctx(ctx).TX(tx). Where(dao.UserReadRecords.Columns().UserId, in.UserId). Where(dao.UserReadRecords.Columns().BookId, in.BookId). Where(dao.UserReadRecords.Columns().ChapterId, ">", 0). Count() if err != nil { return ecode.Fail.Sub("read_chapter_count_failed") } } readPercent := 0.0 if totalChapters > 0 { readPercent = float64(readChapters) / float64(totalChapters) * 100 if readPercent > 100 { readPercent = 100 } } // 判断是否为最后一章 lastChapter, err := dao.Chapters.Ctx(ctx).TX(tx). Where(dao.Chapters.Columns().BookId, in.BookId). OrderDesc(dao.Chapters.Columns().Sort). One() if err != nil { return ecode.Fail.Sub("chapter_query_failed") } readStatus := 1 // 默认为正在读 if lastChapter != nil && lastChapter["id"].Int64() == in.ChapterId { readStatus = 2 // 如果是最后一章,标记为已读完 } // 更新书架记录 _, err = dao.Bookshelves.Ctx(ctx).TX(tx). Where(dao.Bookshelves.Columns().UserId, in.UserId). Where(dao.Bookshelves.Columns().BookId, in.BookId). Data(do.Bookshelves{ LastReadChapterId: in.ChapterId, LastReadPercent: readPercent, LastReadAt: gtime.Now(), ReadStatus: readStatus, }).Update() if err != nil { return ecode.Fail.Sub("bookshelf_update_failed") } } return nil }); err != nil { return nil, err } out.Success = true return out, nil }