feat: add book admin display fields

This commit is contained in:
2026-04-27 13:53:37 +08:00
parent 67c33d06be
commit 2fa15625b0
20 changed files with 472 additions and 70 deletions

View File

@@ -26,6 +26,7 @@
| 入参包含分页、筛选、排序、动作语义、组合参数 | 必须使用 `request` |
| 返回实体原样或实体列表 | 可直接返回实体 |
| 返回包含展示、聚合、联表、树、嵌套结构 | 必须使用 `response` |
| 关联表列表/详情需要给前端展示关联对象名称、标题、label | 必须使用 `response` 联表展示字段,实体只保留外键 ID |
| 多个接口 contract 完全一致 | 复用同一个 `request/response` |
| `admin/app` 字段、校验、分页、展示口径不同 | 按端拆分 `request/response` |
@@ -38,6 +39,12 @@
- 同一业务模块存在 `admin/app` 两套接口时,实体只保留一份;仅 `request/response` 按端拆分。
- 改实体字段时,必须同步检查 `doc-sql``Service` 查询/写入、`API` 绑定/返回。
-`request/response` 或接口 contract 时,必须同步检查并更新 `doc-api``API``Service`
- 关联表实体只能承载自身字段和外键 ID禁止把关联对象的 `name/title/label` 等展示字段写入实体或 `doc-sql`
- 关联表的 admin 列表响应默认必须补充主要关联对象展示字段,字段名使用 `<关联对象语义><Name/Title/Label>`,例如 `authorName``bookTitle``typeLabel`
- 关联表详情响应如果用于前端详情页、编辑弹窗或审阅页面,也必须返回与列表一致的关联展示字段;纯内部读取接口可只返回实体。
- 展示字段来源必须由 `Service` 通过 `JOIN`、预加载或批量查询组装到 `response`,禁止要求前端拿到 ID 后再逐条反查名称。
- 关联展示字段只读;`Create/Update` 请求仍只接收外键 ID 和可编辑业务字段,禁止接收或信任前端传入的展示名称。
- 若列表支持按关联展示字段搜索或排序,`total` 统计查询必须同步使用等价关联条件,避免分页总数和列表数据不一致。
## 推荐做法
@@ -47,6 +54,7 @@
| 动作入参 | `<Action><StructName>Req` | `ChangePasswordReq` |
| 详情出参 | `<StructName>Response` | `BookResponse` |
| 列表项出参 | `<StructName>ListItem` | `BookListItem` |
| 关联展示字段 | `<Related> + Name/Title/Label` | `authorName``bookTitle` |
| 选项出参 | `<StructName>Option` | `AuthorOption` |
| 文件 | `model/<module>/<resource>.go``model/<module>/request/<resource>.go``model/<module>/response/<resource>.go` | `model/book/book.go` |

View File

@@ -131,7 +131,9 @@
| bookAuthorRelation | object | 关系实体 |
| bookAuthorRelation.id | uint | 关系 ID |
| bookAuthorRelation.bookId | uint | 书籍 ID |
| bookAuthorRelation.bookTitle | string | 书籍标题展示字段,来源于 `book.title AS book_title` |
| bookAuthorRelation.authorId | uint | 作者 ID |
| bookAuthorRelation.authorName | string | 作者名称展示字段,来源于 `book_author.name AS author_name` |
| bookAuthorRelation.authorSort | int | 作者展示顺序 |
| bookAuthorRelation.createdAt | string | 创建时间 |
| bookAuthorRelation.updatedAt | string | 更新时间 |
@@ -160,9 +162,10 @@
| list | array | `bookRes.BookAuthorRelationListItem` 列表 |
| list[].id | uint | 关系 ID |
| list[].bookId | uint | 书籍 ID |
| list[].bookTitle | string | 书籍标题展示字段,来源于 `book.title AS book_title` |
| list[].authorId | uint | 作者 ID |
| list[].authorSort | int | 作者展示顺序 |
| list[].authorName | string | 作者名称展示字段 |
| list[].authorName | string | 作者名称展示字段,来源于 `book_author.name AS author_name` |
| total | int64 | 总数 |
| page | int | 当前页 |
| pageSize | int | 每页数量 |
@@ -170,6 +173,7 @@
#### 规则
- 列表默认排序:`bookId asc, authorSort asc, id desc`
- 列表通过 `book_author_relation` 联表 `book``book_author` 补充 `bookTitle/authorName` 展示字段;写接口仍只接收 `bookId/authorId/authorSort`
## 规则
@@ -177,4 +181,5 @@
- `bookId + authorId` 唯一,对应数据库唯一索引 `uk_book_author_relation_book_id_author_id`
- `authorSort` 必须大于 `0`
- 创建时 `authorSort` 未传或为 `0` 时默认 `1`
- `bookTitle/authorName` 为只读展示字段,禁止写入实体表结构,禁止作为创建或更新入参。
- 删除策略为硬删。

View File

@@ -11,7 +11,7 @@
- 实体模型:`book.BookChapter`
- 搜索入参:`bookReq.BookChapterSearch`
- 详情响应:`bookRes.BookChapterResponse`
- 列表项:`book.BookChapter`
- 列表项:`bookRes.BookChapterListItem`
- 返回:写操作 `response.Response{msg}`;详情 `response.Response{data}`;列表 `response.PageResult`
- 删除策略:硬删
@@ -143,6 +143,7 @@
| bookChapter.createdAt | string | 创建时间 |
| bookChapter.updatedAt | string | 更新时间 |
| bookChapter.bookId | uint | 所属书籍 ID |
| bookChapter.bookTitle | string | 书籍标题展示字段,来源于 `book.title AS book_title` |
| bookChapter.title | string | 章节标题 |
| bookChapter.chapterNo | int | 同书内章节顺序编号 |
| bookChapter.isReadable | bool | 是否对 app 端开放阅读 |
@@ -173,11 +174,12 @@
| 字段 | 类型 | 说明 |
|:---|:---|:---|
| list | array | `book.BookChapter` 列表 |
| list | array | `bookRes.BookChapterListItem` 列表 |
| list[].id | uint | 章节 ID |
| list[].createdAt | string | 创建时间 |
| list[].updatedAt | string | 更新时间 |
| list[].bookId | uint | 所属书籍 ID |
| list[].bookTitle | string | 书籍标题展示字段,来源于 `book.title AS book_title` |
| list[].title | string | 章节标题 |
| list[].chapterNo | int | 同书内章节顺序编号 |
| list[].isReadable | bool | 是否对 app 端开放阅读 |
@@ -191,6 +193,7 @@
#### 规则
- 列表默认排序:`book_id asc, chapter_no asc`
- 列表通过 `book_chapter` 联表 `book` 补充 `bookTitle` 展示字段;写接口仍只接收 `bookId`
## 规则
@@ -199,3 +202,4 @@
- `chapterNo` 必须大于 `0`
- `totalLines` 不能小于 `0``isReadable=true``totalLines` 必须大于 `0`
- 创建、更新复用实体 `book.BookChapter`;列表查询复用 `bookReq.BookChapterSearch`;详情返回 `bookRes.BookChapterResponse`
- `bookTitle` 为只读展示字段,禁止写入实体表结构,禁止作为创建或更新入参。

View File

@@ -11,6 +11,7 @@
- 实体模型:`book.BookComment`
- 搜索入参:`bookReq.BookCommentSearch`
- 详情响应:`bookRes.BookCommentResponse`
- 列表项:`bookRes.BookCommentListItem`
- 返回:写操作 `response.Response{msg}`;详情 `response.Response{data}`;列表 `response.PageResult`
- 删除策略:硬删
@@ -143,7 +144,9 @@
| bookComment.updatedAt | time | 更新时间 |
| bookComment.memberUserId | uint | 评论用户的会员 ID |
| bookComment.bookId | uint | 所属书籍 ID |
| bookComment.bookTitle | string | 书籍标题展示字段,来源于 `book.title AS book_title` |
| bookComment.chapterId | uint | 评论目标章节 ID`0` 表示整本书 |
| bookComment.chapterTitle | string | 章节标题展示字段,来源于 `book_chapter.title AS chapter_title`;整书评论可为空 |
| bookComment.lineIndex | int | 评论目标文本行下标,`0` 表示整章或整本书 |
| bookComment.content | string | 评论正文内容 |
| bookComment.likeCount | int64 | 评论点赞聚合值 |
@@ -172,13 +175,15 @@
| 字段 | 类型 | 说明 |
|:---|:---|:---|
| list | array | `book.BookComment` 列表 |
| list | array | `bookRes.BookCommentListItem` 列表 |
| list[].id | uint | 评论 ID |
| list[].createdAt | time | 创建时间 |
| list[].updatedAt | time | 更新时间 |
| list[].memberUserId | uint | 评论用户的会员 ID |
| list[].bookId | uint | 所属书籍 ID |
| list[].bookTitle | string | 书籍标题展示字段,来源于 `book.title AS book_title` |
| list[].chapterId | uint | 评论目标章节 ID`0` 表示整本书 |
| list[].chapterTitle | string | 章节标题展示字段,来源于 `book_chapter.title AS chapter_title`;整书评论可为空 |
| list[].lineIndex | int | 评论目标文本行下标,`0` 表示整章或整本书 |
| list[].content | string | 评论正文内容 |
| list[].likeCount | int64 | 评论点赞聚合值 |
@@ -190,6 +195,7 @@
#### 规则
- 列表默认排序:`id desc`
- 列表通过 `book_comment` 联表 `book``book_chapter` 补充 `bookTitle/chapterTitle` 展示字段;写接口仍只接收 `bookId/chapterId`
## 规则
@@ -197,3 +203,4 @@
- `lineIndex/likeCount` 不能小于 `0`
- 整书评论 `chapterId=0``lineIndex` 必须为 `0`
- 删除策略为硬删。
- `bookTitle/chapterTitle` 为只读展示字段,禁止写入实体表结构,禁止作为创建或更新入参。

View File

@@ -11,7 +11,7 @@
- 实体模型:`book.BookCommentLikeRecord`
- 搜索入参:`bookReq.BookCommentLikeRecordSearch`
- 详情响应:`bookRes.BookCommentLikeRecordResponse`
- 列表项:`book.BookCommentLikeRecord`
- 列表项:`bookRes.BookCommentLikeRecordListItem`
- 返回:写操作 `response.Response{msg}`;详情 `response.Response{data}`;列表 `response.PageResult`
- 删除策略:硬删
@@ -133,6 +133,8 @@
| bookCommentLikeRecord.createdAt | string | 创建时间 |
| bookCommentLikeRecord.updatedAt | string | 更新时间 |
| bookCommentLikeRecord.commentId | uint | 被点赞评论 ID |
| bookCommentLikeRecord.commentContent | string | 被点赞评论内容展示字段,来源于 `book_comment.content AS comment_content` |
| bookCommentLikeRecord.bookTitle | string | 评论所属书籍标题展示字段,来源于 `book.title AS book_title` |
| bookCommentLikeRecord.memberUserId | uint | 点赞用户的会员 ID |
| bookCommentLikeRecord.likedAt | string | 点赞发生时间 |
@@ -157,11 +159,13 @@
| 字段 | 类型 | 说明 |
|:---|:---|:---|
| list | array | `book.BookCommentLikeRecord` 列表 |
| list | array | `bookRes.BookCommentLikeRecordListItem` 列表 |
| list[].id | uint | 点赞记录 ID |
| list[].createdAt | string | 创建时间 |
| list[].updatedAt | string | 更新时间 |
| list[].commentId | uint | 被点赞评论 ID |
| list[].commentContent | string | 被点赞评论内容展示字段,来源于 `book_comment.content AS comment_content` |
| list[].bookTitle | string | 评论所属书籍标题展示字段,来源于 `book.title AS book_title` |
| list[].memberUserId | uint | 点赞用户的会员 ID |
| list[].likedAt | string | 点赞发生时间 |
| total | int64 | 总数 |
@@ -171,6 +175,7 @@
#### 规则
- 列表默认排序:`id desc`
- 列表通过 `book_comment_like_record` 联表 `book_comment``book` 补充 `commentContent/bookTitle` 展示字段;写接口仍只接收 `commentId/memberUserId`
## 规则
@@ -178,3 +183,4 @@
- `commentId + memberUserId` 是幂等判断边界,必须保持唯一。
- 删除策略为硬删;删除后该点赞关系不再存在。
- 时间字段以接口实际 JSON 序列化格式为准,业务语义为 `liked_at`
- `commentContent/bookTitle` 为只读展示字段,禁止写入实体表结构,禁止作为创建或更新入参。

View File

@@ -11,7 +11,7 @@
- 实体模型:`book.BookFavoriteRecord`
- 搜索入参:`bookReq.BookFavoriteRecordSearch`
- 详情响应:`bookRes.BookFavoriteRecordResponse`
- 列表项:`book.BookFavoriteRecord`
- 列表项:`bookRes.BookFavoriteRecordListItem`
- 返回:写操作 `response.Response{msg}`;详情 `response.Response{data}`;列表 `response.PageResult`
- 删除策略:硬删
@@ -134,6 +134,7 @@
| bookFavoriteRecord.updatedAt | string | 更新时间 |
| bookFavoriteRecord.memberUserId | uint | 收藏用户的会员 ID |
| bookFavoriteRecord.bookId | uint | 收藏书籍 ID |
| bookFavoriteRecord.bookTitle | string | 书籍标题展示字段,来源于 `book.title AS book_title` |
| bookFavoriteRecord.favoritedAt | string | 收藏发生时间 |
### 分页查询书籍收藏记录列表
@@ -157,12 +158,13 @@
| 字段 | 类型 | 说明 |
|:---|:---|:---|
| list | array | `book.BookFavoriteRecord` 列表 |
| list | array | `bookRes.BookFavoriteRecordListItem` 列表 |
| list[].id | uint | 收藏记录 ID |
| list[].createdAt | string | 创建时间 |
| list[].updatedAt | string | 更新时间 |
| list[].memberUserId | uint | 收藏用户的会员 ID |
| list[].bookId | uint | 收藏书籍 ID |
| list[].bookTitle | string | 书籍标题展示字段,来源于 `book.title AS book_title` |
| list[].favoritedAt | string | 收藏发生时间 |
| total | int64 | 总数 |
| page | int | 当前页 |
@@ -171,6 +173,7 @@
#### 规则
- 列表默认排序:`id desc`
- 列表通过 `book_favorite_record` 联表 `book` 补充 `bookTitle` 展示字段;写接口仍只接收 `memberUserId/bookId`
## 规则
@@ -178,3 +181,4 @@
- 写操作挂载 `middleware.OperationRecord()`;详情和分页列表不挂操作审计。
- `memberUserId + bookId` 唯一约束由数据库索引 `uk_book_favorite_record_member_user_id_book_id` 保证。
- 删除策略为硬删,实体使用 `HardDeleteModel`,不维护 `deletedAt`
- `bookTitle` 为只读展示字段,禁止写入实体表结构,禁止作为创建或更新入参。

View File

@@ -11,7 +11,7 @@
- 实体模型:`book.BookReadRecord`
- 搜索入参:`bookReq.BookReadRecordSearch`
- 详情响应:`bookRes.BookReadRecordResponse`
- 列表项:`book.BookReadRecord`
- 列表项:`bookRes.BookReadRecordListItem`
- 返回:写操作 `response.Response{msg}`;详情 `response.Response{data}`;列表 `response.PageResult`
- 删除策略:硬删
@@ -147,6 +147,7 @@
| bookReadRecord.bookTitleSnapshot | string | 阅读书籍标题快照 |
| bookReadRecord.readProgress | float | 阅读进度百分比 |
| bookReadRecord.chapterId | uint | 当前续读章节 ID |
| bookReadRecord.chapterTitle | string | 章节标题展示字段,来源于 `book_chapter.title AS chapter_title` |
| bookReadRecord.lineIndex | int | 当前续读文本行下标,正文首行为 `1` |
| bookReadRecord.lastReadAt | string | 最近一次阅读时间 |
@@ -171,7 +172,7 @@
| 字段 | 类型 | 说明 |
|:---|:---|:---|
| list | array | `book.BookReadRecord` 列表 |
| list | array | `bookRes.BookReadRecordListItem` 列表 |
| list[].id | uint | 阅读记录 ID |
| list[].createdAt | string | 创建时间 |
| list[].updatedAt | string | 更新时间 |
@@ -180,6 +181,7 @@
| list[].bookTitleSnapshot | string | 阅读书籍标题快照 |
| list[].readProgress | float | 阅读进度百分比 |
| list[].chapterId | uint | 当前续读章节 ID |
| list[].chapterTitle | string | 章节标题展示字段,来源于 `book_chapter.title AS chapter_title` |
| list[].lineIndex | int | 当前续读文本行下标,正文首行为 `1` |
| list[].lastReadAt | string | 最近一次阅读时间 |
| total | int64 | 总数 |
@@ -189,6 +191,7 @@
#### 规则
- 列表默认排序:`last_read_at desc, id desc`
- 列表通过 `book_read_record` 联表 `book_chapter` 补充 `chapterTitle` 展示字段;书籍标题使用记录内 `bookTitleSnapshot`
## 规则
@@ -197,3 +200,4 @@
- 删除策略为硬删,删除后不保留软删标记。
- `memberUserId + bookId` 是业务唯一键。
- `readProgress` 范围为 `0-100``chapterId` 必须大于 `0``lineIndex` 必须大于 `0`
- `chapterTitle` 为只读展示字段,禁止写入实体表结构,禁止作为创建或更新入参。

View File

@@ -222,6 +222,8 @@
- Query 字段:取 `request/Search``common.GetById``common.IdsReq`
- Detail 响应:取 `response` 包装结构和实体 JSON 字段。
- List 响应:取 `ListItem``Select/Scan` 字段、额外展示字段。
- 关联表响应:列表默认写明关联对象展示字段;详情用于前端展示或编辑时同步写明同口径展示字段。
- 关联展示字段:来源于关联表 `JOIN`、预加载或批量查询组装;字段名使用 `<关联对象语义><Name/Title/Label>`,例如 `authorName``bookTitle`
- 字段规则:取 `doc-sql` 非空/默认值/唯一索引/check叠加 `doc-dict` 值域和 `Service` 校验。
- 字段名:使用实际 `json/form` 名称,禁止使用 Go 字段名替代。
- 端点鉴权:默认继承 `## 基本信息`;只有端点鉴权不同才在端点内单独写。

View File

@@ -12,28 +12,57 @@ type BookAuthorListItem struct {
book.BookAuthor `gorm:"embedded"`
AuthorName string `json:"authorName" gorm:"column:author_name"`
}
type BookAuthorRelationResponse struct {
BookAuthorRelation book.BookAuthorRelation `json:"bookAuthorRelation"`
}
type BookAuthorRelationListItem struct {
type BookAuthorRelationDisplay struct {
book.BookAuthorRelation `gorm:"embedded"`
BookTitle string `json:"bookTitle" gorm:"column:book_title"`
AuthorName string `json:"authorName" gorm:"column:author_name"`
}
type BookAuthorRelationResponse struct {
BookAuthorRelation BookAuthorRelationDisplay `json:"bookAuthorRelation"`
}
type BookAuthorRelationListItem = BookAuthorRelationDisplay
type BookChapterDisplay struct {
book.BookChapter `gorm:"embedded"`
BookTitle string `json:"bookTitle" gorm:"column:book_title"`
}
type BookChapterResponse struct {
BookChapter book.BookChapter `json:"bookChapter"`
BookChapter BookChapterDisplay `json:"bookChapter"`
}
type BookChapterListItem = BookChapterDisplay
type BookCommentDisplay struct {
book.BookComment `gorm:"embedded"`
BookTitle string `json:"bookTitle" gorm:"column:book_title"`
ChapterTitle string `json:"chapterTitle" gorm:"column:chapter_title"`
}
type BookCommentResponse struct {
BookComment book.BookComment `json:"bookComment"`
BookComment BookCommentDisplay `json:"bookComment"`
}
type BookCommentListItem = BookCommentDisplay
type BookCommentLikeRecordDisplay struct {
book.BookCommentLikeRecord `gorm:"embedded"`
BookTitle string `json:"bookTitle" gorm:"column:book_title"`
CommentContent string `json:"commentContent" gorm:"column:comment_content"`
}
type BookCommentLikeRecordResponse struct {
BookCommentLikeRecord book.BookCommentLikeRecord `json:"bookCommentLikeRecord"`
BookCommentLikeRecord BookCommentLikeRecordDisplay `json:"bookCommentLikeRecord"`
}
type BookCommentLikeRecordListItem = BookCommentLikeRecordDisplay
type BookFavoriteRecordDisplay struct {
book.BookFavoriteRecord `gorm:"embedded"`
BookTitle string `json:"bookTitle" gorm:"column:book_title"`
}
type BookFavoriteRecordResponse struct {
BookFavoriteRecord book.BookFavoriteRecord `json:"bookFavoriteRecord"`
BookFavoriteRecord BookFavoriteRecordDisplay `json:"bookFavoriteRecord"`
}
type BookFavoriteRecordListItem = BookFavoriteRecordDisplay
type BookReadRecordDisplay struct {
book.BookReadRecord `gorm:"embedded"`
ChapterTitle string `json:"chapterTitle" gorm:"column:chapter_title"`
}
type BookReadRecordResponse struct {
BookReadRecord book.BookReadRecord `json:"bookReadRecord"`
BookReadRecord BookReadRecordDisplay `json:"bookReadRecord"`
}
type BookReadRecordListItem = BookReadRecordDisplay
type BookSeriesResponse struct {
BookSeries book.BookSeries `json:"bookSeries"`
}

View File

@@ -14,7 +14,7 @@ func (s *BookAuthorService) CreateBookAuthor(item book.BookAuthor) error {
if err := validateBookAuthor(item); err != nil {
return err
}
return global.GVA_DB.Save(&item).Error
return createWithExplicitIsEnabled(&item, item.IsEnabled)
}
func (s *BookAuthorService) DeleteBookAuthor(item book.BookAuthor) error {

View File

@@ -1,6 +1,7 @@
package book
import (
"encoding/json"
"testing"
"github.com/flipped-aurora/gin-vue-admin/server/global"
@@ -17,7 +18,7 @@ func setupBookAuthorListTestDB(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(&book.BookAuthor{}, &book.BookAuthorRelation{}))
require.NoError(t, db.AutoMigrate(&book.Book{}, &book.BookAuthor{}, &book.BookAuthorRelation{}))
global.GVA_DB = db
t.Cleanup(func() {
@@ -43,26 +44,74 @@ func TestBookAuthorService_GetBookAuthorInfoListReturnsAuthorName(t *testing.T)
require.Equal(t, "鲁迅", list[0].AuthorName)
}
func TestBookAuthorRelationService_GetBookAuthorRelationInfoListReturnsAuthorName(t *testing.T) {
func TestBookAuthorRelationService_GetBookAuthorRelationInfoListReturnsDisplayFields(t *testing.T) {
setupBookAuthorListTestDB(t)
bookItem := book.Book{
Title: "边城",
BookType: "novel",
EraTag: book.BookEraTagModern,
CompletionStatus: book.BookCompletionStatusCompleted,
PublishStatus: book.BookPublishStatusOnShelf,
}
require.NoError(t, global.GVA_DB.Create(&bookItem).Error)
author := book.BookAuthor{Name: "沈从文", IsEnabled: true}
require.NoError(t, global.GVA_DB.Create(&author).Error)
require.NoError(t, global.GVA_DB.Create(&book.BookAuthorRelation{
BookID: 11,
BookID: bookItem.ID,
AuthorID: author.ID,
AuthorSort: 1,
}).Error)
list, total, err := (&BookAuthorRelationService{}).GetBookAuthorRelationInfoList(bookReq.BookAuthorRelationSearch{
PageInfo: commonReq.PageInfo{Page: 1, PageSize: 10},
BookID: uintPtr(11),
BookID: uintPtr(bookItem.ID),
})
require.NoError(t, err)
require.EqualValues(t, 1, total)
require.Len(t, list, 1)
require.Equal(t, bookItem.ID, list[0].BookID)
require.Equal(t, author.ID, list[0].AuthorID)
require.Equal(t, "沈从文", list[0].AuthorName)
var got map[string]any
payload, err := json.Marshal(list[0])
require.NoError(t, err)
require.NoError(t, json.Unmarshal(payload, &got))
require.Equal(t, "边城", got["bookTitle"])
}
func TestBookAuthorRelationService_GetBookAuthorRelationReturnsDisplayFields(t *testing.T) {
setupBookAuthorListTestDB(t)
bookItem := book.Book{
Title: "边城",
BookType: "novel",
EraTag: book.BookEraTagModern,
CompletionStatus: book.BookCompletionStatusCompleted,
PublishStatus: book.BookPublishStatusOnShelf,
}
require.NoError(t, global.GVA_DB.Create(&bookItem).Error)
author := book.BookAuthor{Name: "沈从文", IsEnabled: true}
require.NoError(t, global.GVA_DB.Create(&author).Error)
relation := book.BookAuthorRelation{
BookID: bookItem.ID,
AuthorID: author.ID,
AuthorSort: 1,
}
require.NoError(t, global.GVA_DB.Create(&relation).Error)
item, err := (&BookAuthorRelationService{}).GetBookAuthorRelation(relation.ID)
require.NoError(t, err)
var got map[string]any
payload, err := json.Marshal(item)
require.NoError(t, err)
require.NoError(t, json.Unmarshal(payload, &got))
require.Equal(t, "边城", got["bookTitle"])
require.Equal(t, "沈从文", got["authorName"])
}
func uintPtr(v uint) *uint {

View File

@@ -6,10 +6,13 @@ import (
bookReq "github.com/flipped-aurora/gin-vue-admin/server/model/book/request"
bookRes "github.com/flipped-aurora/gin-vue-admin/server/model/book/response"
commonReq "github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
"gorm.io/gorm"
)
type BookAuthorRelationService struct{}
const bookAuthorRelationDisplaySelect = "bar.*, b.title AS book_title, ba.name AS author_name"
func (s *BookAuthorRelationService) CreateBookAuthorRelation(item book.BookAuthorRelation) error {
item = applyBookAuthorRelationDefaults(item)
if err := validateBookAuthorRelation(item); err != nil {
@@ -33,13 +36,16 @@ func (s *BookAuthorRelationService) UpdateBookAuthorRelation(item book.BookAutho
return global.GVA_DB.Save(&item).Error
}
func (s *BookAuthorRelationService) GetBookAuthorRelation(id uint) (item book.BookAuthorRelation, err error) {
err = global.GVA_DB.Where("id = ?", id).First(&item).Error
func (s *BookAuthorRelationService) GetBookAuthorRelation(id uint) (item bookRes.BookAuthorRelationDisplay, err error) {
err = bookAuthorRelationDisplayDB().
Select(bookAuthorRelationDisplaySelect).
Where("bar.id = ?", id).
Take(&item).Error
return
}
func (s *BookAuthorRelationService) GetBookAuthorRelationInfoList(info bookReq.BookAuthorRelationSearch) (list []bookRes.BookAuthorRelationListItem, total int64, err error) {
db := global.GVA_DB.Table("book_author_relation AS bar")
db := bookAuthorRelationDisplayDB()
if info.BookID != nil {
db = db.Where("bar.book_id = ?", *info.BookID)
}
@@ -50,10 +56,15 @@ func (s *BookAuthorRelationService) GetBookAuthorRelationInfoList(info bookReq.B
if err != nil {
return
}
err = db.Select("bar.*, ba.name AS author_name").
Joins("LEFT JOIN book_author AS ba ON ba.id = bar.author_id").
err = db.Select(bookAuthorRelationDisplaySelect).
Scopes(paginate(info.PageInfo)).
Order("bar.book_id asc, bar.author_sort asc, bar.id desc").
Scan(&list).Error
return
}
func bookAuthorRelationDisplayDB() *gorm.DB {
return global.GVA_DB.Table("book_author_relation AS bar").
Joins("LEFT JOIN book AS b ON b.id = bar.book_id").
Joins("LEFT JOIN book_author AS ba ON ba.id = bar.author_id")
}

View File

@@ -4,16 +4,20 @@ import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/book"
bookReq "github.com/flipped-aurora/gin-vue-admin/server/model/book/request"
bookRes "github.com/flipped-aurora/gin-vue-admin/server/model/book/response"
commonReq "github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
"gorm.io/gorm"
)
type BookChapterService struct{}
const bookChapterDisplaySelect = "bc.*, b.title AS book_title"
func (s *BookChapterService) CreateBookChapter(item book.BookChapter) error {
if err := validateBookChapter(item); err != nil {
return err
}
return global.GVA_DB.Save(&item).Error
return createWithExplicitIsEnabled(&item, item.IsEnabled)
}
func (s *BookChapterService) DeleteBookChapter(item book.BookChapter) error {
@@ -31,29 +35,37 @@ func (s *BookChapterService) UpdateBookChapter(item book.BookChapter) error {
return global.GVA_DB.Save(&item).Error
}
func (s *BookChapterService) GetBookChapter(id uint) (item book.BookChapter, err error) {
err = global.GVA_DB.Where("id = ?", id).First(&item).Error
func (s *BookChapterService) GetBookChapter(id uint) (item bookRes.BookChapterDisplay, err error) {
err = bookChapterDisplayDB().
Select(bookChapterDisplaySelect).
Where("bc.id = ?", id).
Take(&item).Error
return
}
func (s *BookChapterService) GetBookChapterInfoList(info bookReq.BookChapterSearch) (list []book.BookChapter, total int64, err error) {
db := global.GVA_DB.Model(&book.BookChapter{})
func (s *BookChapterService) GetBookChapterInfoList(info bookReq.BookChapterSearch) (list []bookRes.BookChapterListItem, total int64, err error) {
db := bookChapterDisplayDB()
if info.BookID != nil {
db = db.Where("book_id = ?", *info.BookID)
db = db.Where("bc.book_id = ?", *info.BookID)
}
if info.IsReadable != nil {
db = db.Where("is_readable = ?", *info.IsReadable)
db = db.Where("bc.is_readable = ?", *info.IsReadable)
}
if info.IsEnabled != nil {
db = db.Where("is_enabled = ?", *info.IsEnabled)
db = db.Where("bc.is_enabled = ?", *info.IsEnabled)
}
if info.Keyword != "" {
db = db.Where("title LIKE ?", "%"+info.Keyword+"%")
db = db.Where("bc.title LIKE ?", "%"+info.Keyword+"%")
}
err = db.Count(&total).Error
if err != nil {
return
}
err = db.Scopes(paginate(info.PageInfo)).Order("book_id asc, chapter_no asc").Find(&list).Error
err = db.Select(bookChapterDisplaySelect).Scopes(paginate(info.PageInfo)).Order("bc.book_id asc, bc.chapter_no asc").Scan(&list).Error
return
}
func bookChapterDisplayDB() *gorm.DB {
return global.GVA_DB.Table("book_chapter AS bc").
Joins("LEFT JOIN book AS b ON b.id = bc.book_id")
}

View File

@@ -4,11 +4,15 @@ import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/book"
bookReq "github.com/flipped-aurora/gin-vue-admin/server/model/book/request"
bookRes "github.com/flipped-aurora/gin-vue-admin/server/model/book/response"
commonReq "github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
"gorm.io/gorm"
)
type BookCommentService struct{}
const bookCommentDisplaySelect = "bc.*, b.title AS book_title, ch.title AS chapter_title"
func (s *BookCommentService) CreateBookComment(item book.BookComment) error {
if err := validateBookComment(item); err != nil {
return err
@@ -31,29 +35,38 @@ func (s *BookCommentService) UpdateBookComment(item book.BookComment) error {
return global.GVA_DB.Save(&item).Error
}
func (s *BookCommentService) GetBookComment(id uint) (item book.BookComment, err error) {
err = global.GVA_DB.Where("id = ?", id).First(&item).Error
func (s *BookCommentService) GetBookComment(id uint) (item bookRes.BookCommentDisplay, err error) {
err = bookCommentDisplayDB().
Select(bookCommentDisplaySelect).
Where("bc.id = ?", id).
Take(&item).Error
return
}
func (s *BookCommentService) GetBookCommentInfoList(info bookReq.BookCommentSearch) (list []book.BookComment, total int64, err error) {
db := global.GVA_DB.Model(&book.BookComment{})
func (s *BookCommentService) GetBookCommentInfoList(info bookReq.BookCommentSearch) (list []bookRes.BookCommentListItem, total int64, err error) {
db := bookCommentDisplayDB()
if info.MemberUserID != nil {
db = db.Where("member_user_id = ?", *info.MemberUserID)
db = db.Where("bc.member_user_id = ?", *info.MemberUserID)
}
if info.BookID != nil {
db = db.Where("book_id = ?", *info.BookID)
db = db.Where("bc.book_id = ?", *info.BookID)
}
if info.ChapterID != nil {
db = db.Where("chapter_id = ?", *info.ChapterID)
db = db.Where("bc.chapter_id = ?", *info.ChapterID)
}
if info.CommentStatus != "" {
db = db.Where("comment_status = ?", info.CommentStatus)
db = db.Where("bc.comment_status = ?", info.CommentStatus)
}
err = db.Count(&total).Error
if err != nil {
return
}
err = db.Scopes(paginate(info.PageInfo)).Order("id desc").Find(&list).Error
err = db.Select(bookCommentDisplaySelect).Scopes(paginate(info.PageInfo)).Order("bc.id desc").Scan(&list).Error
return
}
func bookCommentDisplayDB() *gorm.DB {
return global.GVA_DB.Table("book_comment AS bc").
Joins("LEFT JOIN book AS b ON b.id = bc.book_id").
Joins("LEFT JOIN book_chapter AS ch ON ch.id = bc.chapter_id")
}

View File

@@ -4,11 +4,15 @@ import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/book"
bookReq "github.com/flipped-aurora/gin-vue-admin/server/model/book/request"
bookRes "github.com/flipped-aurora/gin-vue-admin/server/model/book/response"
commonReq "github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
"gorm.io/gorm"
)
type BookCommentLikeRecordService struct{}
const bookCommentLikeRecordDisplaySelect = "bclr.*, b.title AS book_title, bc.content AS comment_content"
func (s *BookCommentLikeRecordService) CreateBookCommentLikeRecord(item book.BookCommentLikeRecord) error {
return global.GVA_DB.Create(&item).Error
}
@@ -25,23 +29,32 @@ func (s *BookCommentLikeRecordService) UpdateBookCommentLikeRecord(item book.Boo
return global.GVA_DB.Save(&item).Error
}
func (s *BookCommentLikeRecordService) GetBookCommentLikeRecord(id uint) (item book.BookCommentLikeRecord, err error) {
err = global.GVA_DB.Where("id = ?", id).First(&item).Error
func (s *BookCommentLikeRecordService) GetBookCommentLikeRecord(id uint) (item bookRes.BookCommentLikeRecordDisplay, err error) {
err = bookCommentLikeRecordDisplayDB().
Select(bookCommentLikeRecordDisplaySelect).
Where("bclr.id = ?", id).
Take(&item).Error
return
}
func (s *BookCommentLikeRecordService) GetBookCommentLikeRecordInfoList(info bookReq.BookCommentLikeRecordSearch) (list []book.BookCommentLikeRecord, total int64, err error) {
db := global.GVA_DB.Model(&book.BookCommentLikeRecord{})
func (s *BookCommentLikeRecordService) GetBookCommentLikeRecordInfoList(info bookReq.BookCommentLikeRecordSearch) (list []bookRes.BookCommentLikeRecordListItem, total int64, err error) {
db := bookCommentLikeRecordDisplayDB()
if info.CommentID != nil {
db = db.Where("comment_id = ?", *info.CommentID)
db = db.Where("bclr.comment_id = ?", *info.CommentID)
}
if info.MemberUserID != nil {
db = db.Where("member_user_id = ?", *info.MemberUserID)
db = db.Where("bclr.member_user_id = ?", *info.MemberUserID)
}
err = db.Count(&total).Error
if err != nil {
return
}
err = db.Scopes(paginate(info.PageInfo)).Order("id desc").Find(&list).Error
err = db.Select(bookCommentLikeRecordDisplaySelect).Scopes(paginate(info.PageInfo)).Order("bclr.id desc").Scan(&list).Error
return
}
func bookCommentLikeRecordDisplayDB() *gorm.DB {
return global.GVA_DB.Table("book_comment_like_record AS bclr").
Joins("LEFT JOIN book_comment AS bc ON bc.id = bclr.comment_id").
Joins("LEFT JOIN book AS b ON b.id = bc.book_id")
}

View File

@@ -0,0 +1,199 @@
package book
import (
"encoding/json"
"testing"
"time"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/book"
bookReq "github.com/flipped-aurora/gin-vue-admin/server/model/book/request"
commonReq "github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
"github.com/glebarez/sqlite"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
func setupBookDisplayTestDB(t *testing.T) {
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, db.AutoMigrate(
&book.Book{},
&book.BookChapter{},
&book.BookComment{},
&book.BookCommentLikeRecord{},
&book.BookFavoriteRecord{},
&book.BookReadRecord{},
))
global.GVA_DB = db
t.Cleanup(func() {
global.GVA_DB = nil
})
}
func createBookDisplayFixture(t *testing.T) (book.Book, book.BookChapter, book.BookComment) {
t.Helper()
bookItem := book.Book{
Title: "边城",
BookType: "novel",
EraTag: book.BookEraTagModern,
CompletionStatus: book.BookCompletionStatusCompleted,
PublishStatus: book.BookPublishStatusOnShelf,
}
require.NoError(t, global.GVA_DB.Create(&bookItem).Error)
chapter := book.BookChapter{
BookID: bookItem.ID,
Title: "第一章 茶峒",
ChapterNo: 1,
ContentFileUrl: "chapters/biancheng-1.txt",
IsEnabled: true,
}
require.NoError(t, global.GVA_DB.Create(&chapter).Error)
comment := book.BookComment{
MemberUserID: 7,
BookID: bookItem.ID,
ChapterID: chapter.ID,
Content: "这段很有画面感",
CommentStatus: book.BookCommentStatusNormal,
}
require.NoError(t, global.GVA_DB.Create(&comment).Error)
return bookItem, chapter, comment
}
func requireJSONField(t *testing.T, value any, key string, expected any) {
t.Helper()
payload, err := json.Marshal(value)
require.NoError(t, err)
var got map[string]any
require.NoError(t, json.Unmarshal(payload, &got))
require.Equal(t, expected, got[key])
}
func TestBookChapterServiceReturnsBookTitle(t *testing.T) {
setupBookDisplayTestDB(t)
bookItem, chapter, _ := createBookDisplayFixture(t)
list, total, err := (&BookChapterService{}).GetBookChapterInfoList(bookReq.BookChapterSearch{
PageInfo: commonReq.PageInfo{Page: 1, PageSize: 10},
BookID: uintPtr(bookItem.ID),
})
require.NoError(t, err)
require.EqualValues(t, 1, total)
require.Len(t, list, 1)
require.Equal(t, chapter.ID, list[0].ID)
requireJSONField(t, list[0], "bookTitle", "边城")
detail, err := (&BookChapterService{}).GetBookChapter(chapter.ID)
require.NoError(t, err)
requireJSONField(t, detail, "bookTitle", "边城")
}
func TestBookCommentServiceReturnsBookAndChapterTitles(t *testing.T) {
setupBookDisplayTestDB(t)
bookItem, _, comment := createBookDisplayFixture(t)
list, total, err := (&BookCommentService{}).GetBookCommentInfoList(bookReq.BookCommentSearch{
PageInfo: commonReq.PageInfo{Page: 1, PageSize: 10},
BookID: uintPtr(bookItem.ID),
})
require.NoError(t, err)
require.EqualValues(t, 1, total)
require.Len(t, list, 1)
require.Equal(t, comment.ID, list[0].ID)
requireJSONField(t, list[0], "bookTitle", "边城")
requireJSONField(t, list[0], "chapterTitle", "第一章 茶峒")
detail, err := (&BookCommentService{}).GetBookComment(comment.ID)
require.NoError(t, err)
requireJSONField(t, detail, "bookTitle", "边城")
requireJSONField(t, detail, "chapterTitle", "第一章 茶峒")
}
func TestBookCommentLikeRecordServiceReturnsCommentDisplayFields(t *testing.T) {
setupBookDisplayTestDB(t)
_, _, comment := createBookDisplayFixture(t)
record := book.BookCommentLikeRecord{
CommentID: comment.ID,
MemberUserID: 8,
LikedAt: time.Now(),
}
require.NoError(t, global.GVA_DB.Create(&record).Error)
list, total, err := (&BookCommentLikeRecordService{}).GetBookCommentLikeRecordInfoList(bookReq.BookCommentLikeRecordSearch{
PageInfo: commonReq.PageInfo{Page: 1, PageSize: 10},
CommentID: uintPtr(comment.ID),
})
require.NoError(t, err)
require.EqualValues(t, 1, total)
require.Len(t, list, 1)
require.Equal(t, record.ID, list[0].ID)
requireJSONField(t, list[0], "bookTitle", "边城")
requireJSONField(t, list[0], "commentContent", "这段很有画面感")
detail, err := (&BookCommentLikeRecordService{}).GetBookCommentLikeRecord(record.ID)
require.NoError(t, err)
requireJSONField(t, detail, "bookTitle", "边城")
requireJSONField(t, detail, "commentContent", "这段很有画面感")
}
func TestBookFavoriteRecordServiceReturnsBookTitle(t *testing.T) {
setupBookDisplayTestDB(t)
bookItem, _, _ := createBookDisplayFixture(t)
record := book.BookFavoriteRecord{
MemberUserID: 9,
BookID: bookItem.ID,
FavoritedAt: time.Now(),
}
require.NoError(t, global.GVA_DB.Create(&record).Error)
list, total, err := (&BookFavoriteRecordService{}).GetBookFavoriteRecordInfoList(bookReq.BookFavoriteRecordSearch{
PageInfo: commonReq.PageInfo{Page: 1, PageSize: 10},
BookID: uintPtr(bookItem.ID),
})
require.NoError(t, err)
require.EqualValues(t, 1, total)
require.Len(t, list, 1)
require.Equal(t, record.ID, list[0].ID)
requireJSONField(t, list[0], "bookTitle", "边城")
detail, err := (&BookFavoriteRecordService{}).GetBookFavoriteRecord(record.ID)
require.NoError(t, err)
requireJSONField(t, detail, "bookTitle", "边城")
}
func TestBookReadRecordServiceReturnsChapterTitle(t *testing.T) {
setupBookDisplayTestDB(t)
bookItem, chapter, _ := createBookDisplayFixture(t)
record := book.BookReadRecord{
MemberUserID: 10,
BookID: bookItem.ID,
BookTitleSnapshot: bookItem.Title,
ReadProgress: 12.5,
ChapterID: chapter.ID,
LineIndex: 3,
LastReadAt: time.Now(),
}
require.NoError(t, global.GVA_DB.Create(&record).Error)
list, total, err := (&BookReadRecordService{}).GetBookReadRecordInfoList(bookReq.BookReadRecordSearch{
PageInfo: commonReq.PageInfo{Page: 1, PageSize: 10},
BookID: uintPtr(bookItem.ID),
})
require.NoError(t, err)
require.EqualValues(t, 1, total)
require.Len(t, list, 1)
require.Equal(t, record.ID, list[0].ID)
requireJSONField(t, list[0], "chapterTitle", "第一章 茶峒")
detail, err := (&BookReadRecordService{}).GetBookReadRecord(record.ID)
require.NoError(t, err)
requireJSONField(t, detail, "chapterTitle", "第一章 茶峒")
}

View File

@@ -4,11 +4,15 @@ import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/book"
bookReq "github.com/flipped-aurora/gin-vue-admin/server/model/book/request"
bookRes "github.com/flipped-aurora/gin-vue-admin/server/model/book/response"
commonReq "github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
"gorm.io/gorm"
)
type BookFavoriteRecordService struct{}
const bookFavoriteRecordDisplaySelect = "bfr.*, b.title AS book_title"
func (s *BookFavoriteRecordService) CreateBookFavoriteRecord(item book.BookFavoriteRecord) error {
return global.GVA_DB.Create(&item).Error
}
@@ -25,23 +29,31 @@ func (s *BookFavoriteRecordService) UpdateBookFavoriteRecord(item book.BookFavor
return global.GVA_DB.Save(&item).Error
}
func (s *BookFavoriteRecordService) GetBookFavoriteRecord(id uint) (item book.BookFavoriteRecord, err error) {
err = global.GVA_DB.Where("id = ?", id).First(&item).Error
func (s *BookFavoriteRecordService) GetBookFavoriteRecord(id uint) (item bookRes.BookFavoriteRecordDisplay, err error) {
err = bookFavoriteRecordDisplayDB().
Select(bookFavoriteRecordDisplaySelect).
Where("bfr.id = ?", id).
Take(&item).Error
return
}
func (s *BookFavoriteRecordService) GetBookFavoriteRecordInfoList(info bookReq.BookFavoriteRecordSearch) (list []book.BookFavoriteRecord, total int64, err error) {
db := global.GVA_DB.Model(&book.BookFavoriteRecord{})
func (s *BookFavoriteRecordService) GetBookFavoriteRecordInfoList(info bookReq.BookFavoriteRecordSearch) (list []bookRes.BookFavoriteRecordListItem, total int64, err error) {
db := bookFavoriteRecordDisplayDB()
if info.MemberUserID != nil {
db = db.Where("member_user_id = ?", *info.MemberUserID)
db = db.Where("bfr.member_user_id = ?", *info.MemberUserID)
}
if info.BookID != nil {
db = db.Where("book_id = ?", *info.BookID)
db = db.Where("bfr.book_id = ?", *info.BookID)
}
err = db.Count(&total).Error
if err != nil {
return
}
err = db.Scopes(paginate(info.PageInfo)).Order("id desc").Find(&list).Error
err = db.Select(bookFavoriteRecordDisplaySelect).Scopes(paginate(info.PageInfo)).Order("bfr.id desc").Scan(&list).Error
return
}
func bookFavoriteRecordDisplayDB() *gorm.DB {
return global.GVA_DB.Table("book_favorite_record AS bfr").
Joins("LEFT JOIN book AS b ON b.id = bfr.book_id")
}

View File

@@ -4,11 +4,15 @@ import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/book"
bookReq "github.com/flipped-aurora/gin-vue-admin/server/model/book/request"
bookRes "github.com/flipped-aurora/gin-vue-admin/server/model/book/response"
commonReq "github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
"gorm.io/gorm"
)
type BookReadRecordService struct{}
const bookReadRecordDisplaySelect = "brr.*, ch.title AS chapter_title"
func (s *BookReadRecordService) CreateBookReadRecord(item book.BookReadRecord) error {
if err := validateBookReadRecord(item); err != nil {
return err
@@ -31,23 +35,31 @@ func (s *BookReadRecordService) UpdateBookReadRecord(item book.BookReadRecord) e
return global.GVA_DB.Save(&item).Error
}
func (s *BookReadRecordService) GetBookReadRecord(id uint) (item book.BookReadRecord, err error) {
err = global.GVA_DB.Where("id = ?", id).First(&item).Error
func (s *BookReadRecordService) GetBookReadRecord(id uint) (item bookRes.BookReadRecordDisplay, err error) {
err = bookReadRecordDisplayDB().
Select(bookReadRecordDisplaySelect).
Where("brr.id = ?", id).
Take(&item).Error
return
}
func (s *BookReadRecordService) GetBookReadRecordInfoList(info bookReq.BookReadRecordSearch) (list []book.BookReadRecord, total int64, err error) {
db := global.GVA_DB.Model(&book.BookReadRecord{})
func (s *BookReadRecordService) GetBookReadRecordInfoList(info bookReq.BookReadRecordSearch) (list []bookRes.BookReadRecordListItem, total int64, err error) {
db := bookReadRecordDisplayDB()
if info.MemberUserID != nil {
db = db.Where("member_user_id = ?", *info.MemberUserID)
db = db.Where("brr.member_user_id = ?", *info.MemberUserID)
}
if info.BookID != nil {
db = db.Where("book_id = ?", *info.BookID)
db = db.Where("brr.book_id = ?", *info.BookID)
}
err = db.Count(&total).Error
if err != nil {
return
}
err = db.Scopes(paginate(info.PageInfo)).Order("last_read_at desc, id desc").Find(&list).Error
err = db.Select(bookReadRecordDisplaySelect).Scopes(paginate(info.PageInfo)).Order("brr.last_read_at desc, brr.id desc").Scan(&list).Error
return
}
func bookReadRecordDisplayDB() *gorm.DB {
return global.GVA_DB.Table("book_read_record AS brr").
Joins("LEFT JOIN book_chapter AS ch ON ch.id = brr.chapter_id")
}

View File

@@ -10,7 +10,7 @@ import (
type BookSeriesService struct{}
func (s *BookSeriesService) CreateBookSeries(item book.BookSeries) error {
return global.GVA_DB.Save(&item).Error
return createWithExplicitIsEnabled(&item, item.IsEnabled)
}
func (s *BookSeriesService) DeleteBookSeries(item book.BookSeries) error {

View File

@@ -11,3 +11,15 @@ func paginate(info commonReq.PageInfo) func(db *gorm.DB) *gorm.DB { return (&inf
func deleteByIDs[T any](ids []int) error {
return global.GVA_DB.Where("id in ?", ids).Delete(new(T)).Error
}
func createWithExplicitIsEnabled[T any](item *T, isEnabled bool) error {
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(item).Error; err != nil {
return err
}
if isEnabled {
return nil
}
return tx.Model(item).Update("is_enabled", false).Error
})
}