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

528 lines
14 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 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
}