770 lines
21 KiB
Go
770 lines
21 KiB
Go
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")
|
||
}
|
||
|
||
// 查询购买记录
|
||
purchaseRecords := make([]struct {
|
||
ChapterId int64 `json:"chapterId"`
|
||
}, 0)
|
||
|
||
err = dao.UserChapterPurchases.Ctx(ctx).
|
||
Fields("chapter_id").
|
||
Where(dao.UserChapterPurchases.Columns().UserId, in.UserId).
|
||
Where(dao.UserChapterPurchases.Columns().BookId, in.BookId).
|
||
WhereIn(dao.UserChapterPurchases.Columns().ChapterId, chapterIds).
|
||
Scan(&purchaseRecords)
|
||
if err != nil {
|
||
return nil, ecode.Fail.Sub("purchase_record_query_failed")
|
||
}
|
||
|
||
// 构建阅读记录映射
|
||
readMap := make(map[int64]*gtime.Time)
|
||
for _, record := range readRecords {
|
||
readMap[record.ChapterId] = record.ReadAt
|
||
}
|
||
|
||
// 构建购买记录映射
|
||
purchaseMap := make(map[int64]bool)
|
||
for _, record := range purchaseRecords {
|
||
purchaseMap[record.ChapterId] = true
|
||
}
|
||
|
||
// 为每个章节设置阅读进度和购买状态
|
||
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
|
||
}
|
||
|
||
// 设置购买状态
|
||
if out.List[i].IsLocked == 0 {
|
||
// 免费章节,直接设置为已购买
|
||
out.List[i].IsPurchased = true
|
||
} else {
|
||
// 付费章节,根据购买记录设置
|
||
if purchaseMap[out.List[i].Id] {
|
||
out.List[i].IsPurchased = true
|
||
} else {
|
||
out.List[i].IsPurchased = false
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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).
|
||
WhereGT(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
|
||
}
|
||
|
||
// AppBatchProgress uploads batch reading progress for app
|
||
func (s *sChapter) AppBatchProgress(ctx context.Context, in *model.ChapterAppBatchProgressIn) (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")
|
||
}
|
||
|
||
// 必须指定章节列表
|
||
if len(in.Chapters) == 0 {
|
||
return nil, ecode.Fail.Sub("chapters_required")
|
||
}
|
||
|
||
// 检查用户是否存在
|
||
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")
|
||
}
|
||
|
||
// 验证所有章节进度
|
||
for _, chapter := range in.Chapters {
|
||
if chapter.ChapterId == 0 {
|
||
return nil, ecode.Fail.Sub("chapter_id_required")
|
||
}
|
||
if chapter.Progress < 0 || chapter.Progress > 100 {
|
||
return nil, ecode.Fail.Sub("progress_invalid")
|
||
}
|
||
}
|
||
|
||
// 检查所有章节是否存在
|
||
chapterIds := make([]int64, 0)
|
||
for _, chapter := range in.Chapters {
|
||
chapterIds = append(chapterIds, chapter.ChapterId)
|
||
}
|
||
|
||
exist, err = dao.Chapters.Ctx(ctx).
|
||
WhereIn(dao.Chapters.Columns().Id, chapterIds).
|
||
Where(dao.Chapters.Columns().BookId, in.BookId).
|
||
Exist()
|
||
if err != nil {
|
||
return nil, ecode.Fail.Sub("chapter_query_failed")
|
||
}
|
||
if !exist {
|
||
return nil, ecode.NotFound.Sub("chapter_not_found")
|
||
}
|
||
|
||
// 开启事务处理
|
||
if err := dao.UserReadRecords.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||
// 批量处理每个章节的进度
|
||
for _, chapter := range in.Chapters {
|
||
// 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, chapter.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, chapter.ChapterId).
|
||
Data(do.UserReadRecords{
|
||
Progress: chapter.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: chapter.ChapterId,
|
||
Progress: chapter.Progress,
|
||
ReadAt: gtime.Now(),
|
||
}).Insert()
|
||
if err != nil {
|
||
return ecode.Fail.Sub("read_record_create_failed")
|
||
}
|
||
}
|
||
}
|
||
|
||
// 2. 更新或创建历史记录(使用最后一个章节作为当前阅读章节)
|
||
lastChapter := in.Chapters[len(in.Chapters)-1]
|
||
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: lastChapter.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: lastChapter.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).
|
||
WhereGT(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
|
||
}
|
||
}
|
||
|
||
// 判断是否为最后一章
|
||
lastChapterInfo, 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 lastChapterInfo != nil && lastChapterInfo["id"].Int64() == lastChapter.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: lastChapter.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
|
||
}
|