Files

770 lines
21 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 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
}