From f68a5b360b4a198c889b57d2d2d68a9e168b6884 Mon Sep 17 00:00:00 2001 From: denghui <1016848185@qq.com> Date: Wed, 16 Jul 2025 15:16:40 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/admin/admin.go | 4 +- api/admin/v1/admin.go | 12 +- api/author/author.go | 21 + api/author/v1/author.go | 75 +++ api/book/book.go | 16 +- api/book/v1/book.go | 208 ++++-- api/category/category.go | 8 +- api/category/v1/category.go | 44 +- api/chapter/chapter.go | 12 +- api/chapter/v1/chapter.go | 88 ++- api/feedback/feedback.go | 4 +- api/feedback/v1/feedback.go | 22 +- api/user/user.go | 2 +- api/user/v1/user.go | 10 +- build_x86_64.sh | 1 + go.mod | 18 +- go.sum | 40 +- internal/cmd/cmd.go | 2 + .../admin/admin_v1_admin_edit_pass.go | 17 - .../controller/admin/admin_v1_edit_pass.go | 14 + ...dmin_v1_admin_info.go => admin_v1_info.go} | 5 +- internal/controller/author/author.go | 5 + internal/controller/author/author_new.go | 15 + internal/controller/author/author_v1_add.go | 24 + internal/controller/author/author_v1_del.go | 21 + .../controller/author/author_v1_detail.go | 17 + internal/controller/author/author_v1_edit.go | 24 + .../controller/author/author_v1_follow.go | 31 + internal/controller/author/author_v1_list.go | 25 + .../controller/author/author_v1_unfollow.go | 27 + .../{book_v1_book_add.go => book_v1_add.go} | 14 +- .../controller/book/book_v1_app_detail.go | 44 ++ internal/controller/book/book_v1_app_list.go | 35 + internal/controller/book/book_v1_app_rate.go | 26 + .../book/book_v1_book_set_featured.go | 14 + .../book/book_v1_book_set_recommended.go | 14 + .../{book_v1_book_del.go => book_v1_del.go} | 10 +- .../{book_v1_book_edit.go => book_v1_edit.go} | 16 +- .../{book_v1_book_list.go => book_v1_list.go} | 19 +- internal/controller/book/book_v1_my_list.go | 32 + internal/controller/book/book_v1_shelf_add.go | 31 + .../controller/book/book_v1_shelf_remove.go | 31 + ..._v1_category_add.go => category_v1_add.go} | 10 +- .../category/category_v1_category_list.go | 25 - ..._v1_category_del.go => category_v1_del.go} | 4 +- ...1_category_edit.go => category_v1_edit.go} | 12 +- .../controller/category/category_v1_list.go | 30 + ...er_v1_chapter_add.go => chapter_v1_add.go} | 4 +- .../chapter/chapter_v1_app_detail.go | 29 + .../controller/chapter/chapter_v1_app_list.go | 29 + .../chapter/chapter_v1_app_progress.go | 27 + .../chapter/chapter_v1_app_purchase.go | 25 + ...er_v1_chapter_del.go => chapter_v1_del.go} | 4 +- ..._v1_chapter_edit.go => chapter_v1_edit.go} | 14 +- ..._v1_chapter_list.go => chapter_v1_list.go} | 4 +- ..._v1_feedback_add.go => feedback_v1_add.go} | 6 +- ...1_feedback_list.go => feedback_v1_list.go} | 6 +- internal/controller/user/user_v1_info.go | 32 + internal/controller/user/user_v1_user_info.go | 24 - internal/dao/internal/books.go | 72 +- internal/dao/internal/bookshelves.go | 2 + internal/dao/internal/categories.go | 4 +- internal/dao/internal/user_points_logs.go | 4 +- internal/dao/internal/user_read_history.go | 81 +++ .../{read_records.go => user_read_records.go} | 46 +- internal/dao/read_records.go | 27 - internal/dao/user_read_history.go | 27 + internal/dao/user_read_records.go | 27 + internal/logic/author/author.go | 268 ++++++++ internal/logic/book/book.go | 632 +++++++++++++++++- internal/logic/bookshelve/bookshelve.go | 57 ++ internal/logic/category/category.go | 47 +- internal/logic/chapter/chapter.go | 433 +++++++++++- internal/logic/feedback/feedback.go | 2 +- internal/logic/logic.go | 6 +- internal/logic/read_record/read_record.go | 68 -- internal/logic/tag/tag.go | 119 ---- internal/logic/user/user.go | 116 +++- .../user_follow_author/user_follow_author.go | 32 + .../user_read_record/user_read_record.go | 97 +++ internal/model/author.go | 60 ++ internal/model/book.go | 216 +++++- internal/model/bookshelve.go | 24 + internal/model/category.go | 28 +- internal/model/chapter.go | 91 ++- internal/model/do/books.go | 38 +- internal/model/do/bookshelves.go | 1 + internal/model/do/categories.go | 2 +- internal/model/do/user_points_logs.go | 4 +- internal/model/do/user_read_history.go | 20 + .../{read_records.go => user_read_records.go} | 9 +- internal/model/entity/books.go | 36 +- internal/model/entity/bookshelves.go | 15 +- internal/model/entity/categories.go | 12 +- internal/model/entity/user_points_logs.go | 14 +- .../{read_records.go => user_read_history.go} | 14 +- internal/model/entity/user_read_records.go | 21 + internal/model/feedback.go | 17 +- internal/model/read_record.go | 36 - internal/model/tag.go | 6 +- internal/model/upload.go | 46 -- internal/model/user.go | 14 + internal/model/user_follow_author.go | 8 +- internal/model/user_read_history.go | 27 + internal/model/user_read_record.go | 39 ++ internal/packed/packed.go | 1 - internal/service/author.go | 42 ++ internal/service/book.go | 11 + internal/service/bookshelve.go | 35 + internal/service/chapter.go | 11 + internal/service/read_record.go | 37 - internal/service/user_follow_author.go | 39 ++ internal/service/user_read_history.go | 35 + internal/service/user_read_record.go | 37 + novel.sql | 503 ++++++++++++++ noveltest | Bin 0 -> 34038802 bytes utility/i18n/i18n.go | 108 ++- utility/mqtt/amazon_sqs/amazon_sqs.go | 60 ++ utility/mqtt/mqtt.go | 18 + utility/myCasbin/casbin.go | 52 +- utility/oss/aliyun/aliyun.go | 99 --- utility/oss/amazon_s3/amazon-s3.go | 114 ++++ utility/oss/oss.go | 24 +- 123 files changed, 4643 insertions(+), 931 deletions(-) create mode 100644 api/author/author.go create mode 100644 api/author/v1/author.go create mode 100755 build_x86_64.sh delete mode 100644 internal/controller/admin/admin_v1_admin_edit_pass.go create mode 100644 internal/controller/admin/admin_v1_edit_pass.go rename internal/controller/admin/{admin_v1_admin_info.go => admin_v1_info.go} (73%) create mode 100644 internal/controller/author/author.go create mode 100644 internal/controller/author/author_new.go create mode 100644 internal/controller/author/author_v1_add.go create mode 100644 internal/controller/author/author_v1_del.go create mode 100644 internal/controller/author/author_v1_detail.go create mode 100644 internal/controller/author/author_v1_edit.go create mode 100644 internal/controller/author/author_v1_follow.go create mode 100644 internal/controller/author/author_v1_list.go create mode 100644 internal/controller/author/author_v1_unfollow.go rename internal/controller/book/{book_v1_book_add.go => book_v1_add.go} (71%) create mode 100644 internal/controller/book/book_v1_app_detail.go create mode 100644 internal/controller/book/book_v1_app_list.go create mode 100644 internal/controller/book/book_v1_app_rate.go create mode 100644 internal/controller/book/book_v1_book_set_featured.go create mode 100644 internal/controller/book/book_v1_book_set_recommended.go rename internal/controller/book/{book_v1_book_del.go => book_v1_del.go} (53%) rename internal/controller/book/{book_v1_book_edit.go => book_v1_edit.go} (71%) rename internal/controller/book/{book_v1_book_list.go => book_v1_list.go} (72%) create mode 100644 internal/controller/book/book_v1_my_list.go create mode 100644 internal/controller/book/book_v1_shelf_add.go create mode 100644 internal/controller/book/book_v1_shelf_remove.go rename internal/controller/category/{category_v1_category_add.go => category_v1_add.go} (53%) delete mode 100644 internal/controller/category/category_v1_category_list.go rename internal/controller/category/{category_v1_category_del.go => category_v1_del.go} (65%) rename internal/controller/category/{category_v1_category_edit.go => category_v1_edit.go} (50%) create mode 100644 internal/controller/category/category_v1_list.go rename internal/controller/chapter/{chapter_v1_chapter_add.go => chapter_v1_add.go} (76%) create mode 100644 internal/controller/chapter/chapter_v1_app_detail.go create mode 100644 internal/controller/chapter/chapter_v1_app_list.go create mode 100644 internal/controller/chapter/chapter_v1_app_progress.go create mode 100644 internal/controller/chapter/chapter_v1_app_purchase.go rename internal/controller/chapter/{chapter_v1_chapter_del.go => chapter_v1_del.go} (58%) rename internal/controller/chapter/{chapter_v1_chapter_edit.go => chapter_v1_edit.go} (77%) rename internal/controller/chapter/{chapter_v1_chapter_list.go => chapter_v1_list.go} (72%) rename internal/controller/feedback/{feedback_v1_feedback_add.go => feedback_v1_add.go} (67%) rename internal/controller/feedback/{feedback_v1_feedback_list.go => feedback_v1_list.go} (70%) create mode 100644 internal/controller/user/user_v1_info.go delete mode 100644 internal/controller/user/user_v1_user_info.go create mode 100644 internal/dao/internal/user_read_history.go rename internal/dao/internal/{read_records.go => user_read_records.go} (51%) delete mode 100644 internal/dao/read_records.go create mode 100644 internal/dao/user_read_history.go create mode 100644 internal/dao/user_read_records.go create mode 100644 internal/logic/author/author.go create mode 100644 internal/logic/bookshelve/bookshelve.go delete mode 100644 internal/logic/read_record/read_record.go delete mode 100644 internal/logic/tag/tag.go create mode 100644 internal/logic/user_read_record/user_read_record.go create mode 100644 internal/model/author.go create mode 100644 internal/model/bookshelve.go create mode 100644 internal/model/do/user_read_history.go rename internal/model/do/{read_records.go => user_read_records.go} (61%) rename internal/model/entity/{read_records.go => user_read_history.go} (66%) create mode 100644 internal/model/entity/user_read_records.go delete mode 100644 internal/model/read_record.go create mode 100644 internal/model/user_read_history.go create mode 100644 internal/model/user_read_record.go create mode 100644 internal/service/author.go create mode 100644 internal/service/bookshelve.go delete mode 100644 internal/service/read_record.go create mode 100644 internal/service/user_follow_author.go create mode 100644 internal/service/user_read_history.go create mode 100644 internal/service/user_read_record.go create mode 100644 novel.sql create mode 100755 noveltest create mode 100644 utility/mqtt/amazon_sqs/amazon_sqs.go create mode 100644 utility/mqtt/mqtt.go delete mode 100644 utility/oss/aliyun/aliyun.go create mode 100644 utility/oss/amazon_s3/amazon-s3.go diff --git a/api/admin/admin.go b/api/admin/admin.go index 3d66157..4efb658 100644 --- a/api/admin/admin.go +++ b/api/admin/admin.go @@ -11,6 +11,6 @@ import ( ) type IAdminV1 interface { - AdminInfo(ctx context.Context, req *v1.AdminInfoReq) (res *v1.AdminInfoRes, err error) - AdminEditPass(ctx context.Context, req *v1.AdminEditPassReq) (res *v1.AdminEditPassRes, err error) + Info(ctx context.Context, req *v1.InfoReq) (res *v1.InfoRes, err error) + EditPass(ctx context.Context, req *v1.EditPassReq) (res *v1.EditPassRes, err error) } diff --git a/api/admin/v1/admin.go b/api/admin/v1/admin.go index ad0adc5..68b0a42 100644 --- a/api/admin/v1/admin.go +++ b/api/admin/v1/admin.go @@ -4,21 +4,21 @@ import ( "github.com/gogf/gf/v2/frame/g" ) -type AdminInfoReq struct { - g.Meta `path:"/admin/info" tags:"Admin" method:"get" summary:"管理员信息"` +type InfoReq struct { + g.Meta `path:"/admin/info" tags:"Backend/Admin" method:"get" summary:"管理员信息"` } -type AdminInfoRes struct { +type InfoRes struct { g.Meta `mime:"application/json"` AdminId int64 `json:"adminId"` Username string `json:"username"` } -type AdminEditPassReq struct { - g.Meta `path:"/admin/editPass" tags:"Admin" method:"post" summary:"修改密码"` +type EditPassReq struct { + g.Meta `path:"/admin/editPass" tags:"Backend/Admin" method:"post" summary:"修改密码"` OldPass string `json:"oldPass" v:"required" dc:"旧密码"` NewPass string `json:"newPass" v:"required" dc:"新密码"` } -type AdminEditPassRes struct { +type EditPassRes struct { g.Meta `mime:"application/json"` Success bool } diff --git a/api/author/author.go b/api/author/author.go new file mode 100644 index 0000000..f53f444 --- /dev/null +++ b/api/author/author.go @@ -0,0 +1,21 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package author + +import ( + "context" + + "server/api/author/v1" +) + +type IAuthorV1 interface { + List(ctx context.Context, req *v1.ListReq) (res *v1.ListRes, err error) + Add(ctx context.Context, req *v1.AddReq) (res *v1.AddRes, err error) + Edit(ctx context.Context, req *v1.EditReq) (res *v1.EditRes, err error) + Del(ctx context.Context, req *v1.DelReq) (res *v1.DelRes, err error) + Follow(ctx context.Context, req *v1.FollowReq) (res *v1.FollowRes, err error) + Unfollow(ctx context.Context, req *v1.UnfollowReq) (res *v1.UnfollowRes, err error) + Detail(ctx context.Context, req *v1.DetailReq) (res *v1.DetailRes, err error) +} diff --git a/api/author/v1/author.go b/api/author/v1/author.go new file mode 100644 index 0000000..bb8bf48 --- /dev/null +++ b/api/author/v1/author.go @@ -0,0 +1,75 @@ +package v1 + +import ( + "github.com/gogf/gf/v2/frame/g" + "server/internal/model" +) + +type ListReq struct { + g.Meta `path:"/author" tags:"Backend/Admin" method:"get" summary:"获取作者列表"` + Page int `json:"page" dc:"页码"` + Size int `json:"size" dc:"每页数量"` + PenName string `json:"penName" dc:"笔名(模糊搜索)"` + Status int `json:"status" dc:"状态:1正常,2禁用"` +} +type ListRes struct { + Total int `json:"total" dc:"总数"` + List interface{} `json:"list" dc:"作者列表"` +} + +type AddReq struct { + g.Meta `path:"/author" tags:"Backend/Admin" method:"post" summary:"新增作者"` + UserId int64 `json:"userId" dc:"用户ID" v:"required"` + PenName string `json:"penName" dc:"笔名" v:"required"` + Bio string `json:"bio" dc:"作者简介"` + Status int `json:"status" dc:"状态:1正常,2禁用"` +} +type AddRes struct { + Success bool `json:"success" dc:"是否成功"` +} + +type EditReq struct { + g.Meta `path:"/author" tags:"Backend/Admin" method:"put" summary:"编辑作者"` + Id int64 `json:"id" dc:"作者ID" v:"required"` + PenName string `json:"penName" dc:"笔名" v:"required"` + Bio string `json:"bio" dc:"作者简介"` + Status int `json:"status" dc:"状态:1正常,2禁用"` +} +type EditRes struct { + Success bool `json:"success" dc:"是否成功"` +} + +type DelReq struct { + g.Meta `path:"/author" tags:"Backend/Admin" method:"delete" summary:"删除作者"` + Id int64 `json:"id" dc:"作者ID" v:"required"` +} +type DelRes struct { + Success bool `json:"success" dc:"是否成功"` +} + +// 关注作者 +// ============================= +type FollowReq struct { + g.Meta `path:"/author/follow" tags:"APP" method:"post" summary:"关注作者"` + AuthorId int64 `json:"authorId" dc:"作者ID" v:"required"` +} +type FollowRes struct { + Success bool `json:"success" dc:"是否成功"` +} + +// 取消关注作者 +// ============================= +type UnfollowReq struct { + g.Meta `path:"/author/unfollow" tags:"APP" method:"post" summary:"取消关注作者"` + AuthorId int64 `json:"authorId" dc:"作者ID" v:"required"` +} +type UnfollowRes struct { + Success bool `json:"success" dc:"是否成功"` +} +type DetailReq struct { + g.Meta `path:"/author/detail" tags:"APP" method:"get" summary:"作者详情"` + AuthorId int64 `json:"authorId" dc:"作者ID" v:"required"` +} +type DetailRes struct { + Author *model.AuthorDetailOut `json:"author" dc:"作者信息"` +} diff --git a/api/book/book.go b/api/book/book.go index 8afb24e..f74b6b7 100644 --- a/api/book/book.go +++ b/api/book/book.go @@ -11,8 +11,16 @@ import ( ) type IBookV1 interface { - BookList(ctx context.Context, req *v1.BookListReq) (res *v1.BookListRes, err error) - BookAdd(ctx context.Context, req *v1.BookAddReq) (res *v1.BookAddRes, err error) - BookEdit(ctx context.Context, req *v1.BookEditReq) (res *v1.BookEditRes, err error) - BookDel(ctx context.Context, req *v1.BookDelReq) (res *v1.BookDelRes, err error) + List(ctx context.Context, req *v1.ListReq) (res *v1.ListRes, err error) + Add(ctx context.Context, req *v1.AddReq) (res *v1.AddRes, err error) + Edit(ctx context.Context, req *v1.EditReq) (res *v1.EditRes, err error) + Del(ctx context.Context, req *v1.DelReq) (res *v1.DelRes, err error) + ShelfAdd(ctx context.Context, req *v1.ShelfAddReq) (res *v1.ShelfAddRes, err error) + ShelfRemove(ctx context.Context, req *v1.ShelfRemoveReq) (res *v1.ShelfRemoveRes, err error) + AppList(ctx context.Context, req *v1.AppListReq) (res *v1.AppListRes, err error) + AppDetail(ctx context.Context, req *v1.AppDetailReq) (res *v1.AppDetailRes, err error) + AppRate(ctx context.Context, req *v1.AppRateReq) (res *v1.AppRateRes, err error) + MyList(ctx context.Context, req *v1.MyListReq) (res *v1.MyListRes, err error) + BookSetFeatured(ctx context.Context, req *v1.BookSetFeaturedReq) (res *v1.BookSetFeaturedRes, err error) + BookSetRecommended(ctx context.Context, req *v1.BookSetRecommendedReq) (res *v1.BookSetRecommendedRes, err error) } diff --git a/api/book/v1/book.go b/api/book/v1/book.go index 3974543..14ab8f4 100644 --- a/api/book/v1/book.go +++ b/api/book/v1/book.go @@ -6,56 +6,180 @@ import ( "github.com/gogf/gf/v2/frame/g" ) -type BookListReq struct { - g.Meta `path:"/book" tags:"Book" method:"get" summary:"获取小说列表"` - Page int `json:"page"` - Size int `json:"size"` - Title string `json:"title"` - CategoryId int64 `json:"categoryId"` - AuthorId int64 `json:"authorId"` - Status int `json:"status"` - IsRecommended int `json:"isRecommended"` +type ListReq struct { + g.Meta `path:"/book" tags:"Backend/Author" method:"get" summary:"获取小说列表"` + Page int `json:"page" dc:"页码"` + Size int `json:"size" dc:"每页数量"` + Title string `json:"title" dc:"书名模糊搜索"` + CategoryId int64 `json:"categoryId" dc:"分类ID"` + AuthorId int64 `json:"authorId" dc:"作者ID"` + Status int `json:"status" dc:"状态"` + IsRecommended int `json:"isRecommended" dc:"是否推荐"` + Sort string `json:"sort" dc:"排序字段"` } -type BookListRes struct { - Total int `json:"total"` - List []model.Book `json:"list"` +type ListRes struct { + Total int `json:"total" dc:"总数"` + List []model.Book `json:"list" dc:"书籍列表"` } -type BookAddReq struct { - g.Meta `path:"/book" tags:"Book" method:"post" summary:"新增小说"` - AuthorId int64 `json:"authorId"` - CategoryId int64 `json:"categoryId"` - Title string `json:"title"` - CoverUrl string `json:"coverUrl"` - Description string `json:"description"` - Status int `json:"status"` - Tags string `json:"tags"` - IsRecommended int `json:"isRecommended"` +type AddReq struct { + g.Meta `path:"/book" tags:"Backend/Author" method:"post" summary:"新增小说"` + AuthorId int64 `json:"authorId" dc:"作者ID" v:"required"` + CategoryId int64 `json:"categoryId" dc:"分类ID" v:"required"` + Title string `json:"title" dc:"书名" v:"required"` + CoverUrl string `json:"coverUrl" dc:"封面图"` + Description string `json:"description" dc:"简介"` + Status int `json:"status" dc:"状态"` + Tags string `json:"tags" dc:"标签"` + IsRecommended int `json:"isRecommended" dc:"是否推荐"` + IsFeatured int `json:"isFeatured" dc:"是否精选"` + Language string `json:"language" dc:"语言"` } -type BookAddRes struct { - Success bool `json:"success"` +type AddRes struct { + Success bool `json:"success" dc:"是否成功"` } -type BookEditReq struct { - g.Meta `path:"/book" tags:"Book" method:"put" summary:"编辑小说"` - Id int64 `json:"id"` - AuthorId int64 `json:"authorId"` - CategoryId int64 `json:"categoryId"` - Title string `json:"title"` - CoverUrl string `json:"coverUrl"` - Description string `json:"description"` - Status int `json:"status"` - Tags string `json:"tags"` - IsRecommended int `json:"isRecommended"` +type EditReq struct { + g.Meta `path:"/book" tags:"Backend/Author" method:"put" summary:"编辑小说"` + Id int64 `json:"id" dc:"书籍ID" v:"required"` + AuthorId int64 `json:"authorId" dc:"作者ID" v:"required"` + CategoryId int64 `json:"categoryId" dc:"分类ID" v:"required"` + Title string `json:"title" dc:"书名" v:"required"` + CoverUrl string `json:"coverUrl" dc:"封面图"` + Description string `json:"description" dc:"简介"` + Status int `json:"status" dc:"状态"` + Tags string `json:"tags" dc:"标签"` + IsRecommended int `json:"isRecommended" dc:"是否推荐"` + IsFeatured int `json:"isFeatured" dc:"是否精选"` + Language string `json:"language" dc:"语言"` } -type BookEditRes struct { - Success bool `json:"success"` +type EditRes struct { + Success bool `json:"success" dc:"是否成功"` } -type BookDelReq struct { - g.Meta `path:"/book" tags:"Book" method:"delete" summary:"删除小说"` - Id int64 `json:"id"` +type DelReq struct { + g.Meta `path:"/book" tags:"Backend/Author" method:"delete" summary:"删除小说"` + Id int64 `json:"id" dc:"书籍ID" v:"required"` } -type BookDelRes struct { - Success bool `json:"success"` +type DelRes struct { + Success bool `json:"success" dc:"是否成功"` +} + +// 加入书架 +type ShelfAddReq struct { + g.Meta `path:"/book/shelf/add" tags:"APP" method:"post" summary:"加入书架"` + BookId int64 `json:"bookId" dc:"小说ID" v:"required"` +} +type ShelfAddRes struct { + Success bool `json:"success" dc:"是否成功"` +} + +// 移除书架 +type ShelfRemoveReq struct { + g.Meta `path:"/book/shelf/remove" tags:"APP" method:"post" summary:"移除书架(支持批量)"` + BookIds []int64 `json:"bookIds" dc:"小说ID列表" v:"required"` +} +type ShelfRemoveRes struct { + Success bool `json:"success" dc:"是否成功"` +} + +// App 获取书籍列表 +type AppListReq struct { + g.Meta `path:"/book/app/list" tags:"APP" method:"get" summary:"App获取书籍列表"` + Page int `json:"page" dc:"页码"` + Size int `json:"size" dc:"每页数量"` + IsRecommended bool `json:"isRecommended" dc:"是否推荐"` + IsFeatured bool `json:"isFeatured" dc:"是否精选"` + IsLatest int `json:"isLatest" dc:"是否最新"` + CategoryId int64 `json:"categoryId" dc:"分类ID"` + Title string `json:"title" dc:"书名模糊搜索"` + AuthorId int `json:"authorId" dc:"作者ID"` + Language string `json:"language" dc:"语言"` + Sort string `json:"sort" dc:"排序字段"` +} +type AppListRes struct { + Total int `json:"total" dc:"总数"` + List []model.BookAppItem `json:"list" dc:"书籍列表"` +} + +// App 获取书籍详情 +type AppDetailReq struct { + g.Meta `path:"/book/app/detail" tags:"APP" method:"get" summary:"App获取书籍详情"` + Id int64 `json:"id" dc:"书籍ID" v:"required"` +} +type AppDetailRes struct { + Id int64 `json:"id" dc:"书籍ID"` + AuthorId int64 `json:"authorId" dc:"作者ID"` + CategoryId int64 `json:"categoryId" dc:"分类ID"` + Title string `json:"title" dc:"书名"` + CoverUrl string `json:"coverUrl" dc:"封面图"` + Description string `json:"description" dc:"简介"` + Status int `json:"status" dc:"状态"` + Tags string `json:"tags" dc:"标签"` + IsRecommended int `json:"isRecommended" dc:"是否推荐"` + IsFeatured int `json:"isFeatured" dc:"是否精选"` + Language string `json:"language" dc:"语言"` + Rating float64 `json:"rating" dc:"评分"` + CurrentReaders int64 `json:"currentReaders" dc:"在读人数"` + CreatedAt string `json:"createdAt" dc:"创建时间"` + UpdatedAt string `json:"updatedAt" dc:"更新时间"` + HasRead bool `json:"hasRead" dc:"是否读过"` + ReadProgress int `json:"readProgress" dc:"阅读进度百分比"` + LastChapterId int64 `json:"lastChapterId" dc:"最近阅读章节ID"` + LastReadAt string `json:"lastReadAt" dc:"最近阅读时间"` +} + +// App 用户评分 +type AppRateReq struct { + g.Meta `path:"/book/app/rate" tags:"APP" method:"post" summary:"App用户评分"` + BookId int64 `json:"bookId" dc:"书籍ID" v:"required"` + Rating float64 `json:"rating" dc:"评分(1-10分)" v:"required"` +} +type AppRateRes struct { + Success bool `json:"success" dc:"是否成功"` +} + +type BookAppItem struct { + Id int64 `json:"id" dc:"书籍ID"` + CoverUrl string `json:"coverUrl" dc:"封面图"` + Rating float64 `json:"rating" dc:"评分"` + Title string `json:"title" dc:"标题"` + Description string `json:"description" dc:"简介"` + HasRated bool `json:"hasRated" dc:"当前用户是否已评分"` + MyRating float64 `json:"myRating" dc:"当前用户评分(未评分为0)"` +} + +// 我的书籍列表 +// ============================= +type MyListReq struct { + g.Meta `path:"/book/app/my-books" tags:"APP" method:"get" summary:"获取我的书籍列表"` + Type int `json:"type" dc:"类型:1-正在读 2-已读完 3-历史记录" v:"required"` + Page int `json:"page" dc:"页码"` + Size int `json:"size" dc:"每页数量"` + Sort string `json:"sort" dc:"排序字段"` +} +type MyListRes struct { + Total int `json:"total" dc:"总数"` + List []model.MyBookItem `json:"list" dc:"书籍列表"` +} + +// ============================= +// 新增:单独修改精选状态和推荐状态 +// ============================= +type BookSetFeaturedReq struct { + g.Meta `path:"/book/set-featured" tags:"Backend/Book" method:"post" summary:"设置书籍精选状态"` + Id int64 `json:"id" dc:"书籍ID" v:"required"` + IsFeatured int `json:"isFeatured" dc:"是否精选" v:"required"` +} +type BookSetFeaturedRes struct { + Success bool `json:"success" dc:"是否成功"` +} + +type BookSetRecommendedReq struct { + g.Meta `path:"/book/set-recommended" tags:"Backend/Book" method:"post" summary:"设置书籍推荐状态"` + Id int64 `json:"id" dc:"书籍ID" v:"required"` + IsRecommended int `json:"isRecommended" dc:"是否推荐" v:"required"` +} +type BookSetRecommendedRes struct { + Success bool `json:"success" dc:"是否成功"` } diff --git a/api/category/category.go b/api/category/category.go index 8cefcb1..86ad538 100644 --- a/api/category/category.go +++ b/api/category/category.go @@ -11,8 +11,8 @@ import ( ) type ICategoryV1 interface { - CategoryList(ctx context.Context, req *v1.CategoryListReq) (res *v1.CategoryListRes, err error) - CategoryAdd(ctx context.Context, req *v1.CategoryAddReq) (res *v1.CategoryAddRes, err error) - CategoryEdit(ctx context.Context, req *v1.CategoryEditReq) (res *v1.CategoryEditRes, err error) - CategoryDel(ctx context.Context, req *v1.CategoryDelReq) (res *v1.CategoryDelRes, err error) + List(ctx context.Context, req *v1.ListReq) (res *v1.ListRes, err error) + Add(ctx context.Context, req *v1.AddReq) (res *v1.AddRes, err error) + Edit(ctx context.Context, req *v1.EditReq) (res *v1.EditRes, err error) + Del(ctx context.Context, req *v1.DelReq) (res *v1.DelRes, err error) } diff --git a/api/category/v1/category.go b/api/category/v1/category.go index 01aeaf9..a72c951 100644 --- a/api/category/v1/category.go +++ b/api/category/v1/category.go @@ -6,41 +6,41 @@ import ( "github.com/gogf/gf/v2/frame/g" ) -type CategoryListReq struct { - g.Meta `path:"/category" tags:"Category" method:"get" summary:"获取分类列表"` - Page int `json:"page" dc:"页码"` - Size int `json:"size" dc:"每页数量"` - Name string `json:"name" dc:"分类名称(模糊搜索)"` - Type int `json:"type" dc:"类型,1男频,2女频"` +type ListReq struct { + g.Meta `path:"/category" tags:"APP" method:"get" summary:"获取分类列表"` + Page int `json:"page" dc:"页码"` + Size int `json:"size" dc:"每页数量"` + Name string `json:"name" dc:"分类名称(模糊搜索)"` + Channel int `json:"channel" dc:"频道类型:1=男频,2=女频"` } -type CategoryListRes struct { +type ListRes struct { Total int `json:"total" dc:"总数"` List []model.Category `json:"list" dc:"分类列表"` } -type CategoryAddReq struct { - g.Meta `path:"/category" tags:"Category" method:"post" summary:"新增分类"` - Name string `json:"name" dc:"分类名称"` - Type int `json:"type" dc:"类型,1男频,2女频"` +type AddReq struct { + g.Meta `path:"/category" tags:"Backend/Admin" method:"post" summary:"新增分类"` + Name string `json:"name" dc:"分类名称" v:"required"` + Channel int `json:"channel" dc:"频道类型:1=男频,2=女频" v:"required"` } -type CategoryAddRes struct { +type AddRes struct { Success bool `json:"success" dc:"是否成功"` } -type CategoryEditReq struct { - g.Meta `path:"/category" tags:"Category" method:"put" summary:"编辑分类"` - Id int `json:"id" dc:"分类ID"` - Name string `json:"name" dc:"分类名称"` - Type int `json:"type" dc:"类型,1男频,2女频"` +type EditReq struct { + g.Meta `path:"/category" tags:"Backend/Admin" method:"put" summary:"编辑分类"` + Id int64 `json:"id" dc:"分类ID" v:"required"` + Name string `json:"name" dc:"分类名称" v:"required"` + Channel int `json:"channel" dc:"频道类型:1=男频,2=女频" v:"required"` } -type CategoryEditRes struct { +type EditRes struct { Success bool `json:"success" dc:"是否成功"` } -type CategoryDelReq struct { - g.Meta `path:"/category" tags:"Category" method:"delete" summary:"删除分类"` - Id int `json:"id" dc:"分类ID"` +type DelReq struct { + g.Meta `path:"/category" tags:"Backend/Admin" method:"delete" summary:"删除分类"` + Id int64 `json:"id" dc:"分类ID" v:"required"` } -type CategoryDelRes struct { +type DelRes struct { Success bool `json:"success" dc:"是否成功"` } diff --git a/api/chapter/chapter.go b/api/chapter/chapter.go index 71ae89f..b7019fc 100644 --- a/api/chapter/chapter.go +++ b/api/chapter/chapter.go @@ -11,8 +11,12 @@ import ( ) type IChapterV1 interface { - ChapterList(ctx context.Context, req *v1.ChapterListReq) (res *v1.ChapterListRes, err error) - ChapterAdd(ctx context.Context, req *v1.ChapterAddReq) (res *v1.ChapterAddRes, err error) - ChapterEdit(ctx context.Context, req *v1.ChapterEditReq) (res *v1.ChapterEditRes, err error) - ChapterDel(ctx context.Context, req *v1.ChapterDelReq) (res *v1.ChapterDelRes, err error) + List(ctx context.Context, req *v1.ListReq) (res *v1.ListRes, err error) + Add(ctx context.Context, req *v1.AddReq) (res *v1.AddRes, err error) + Edit(ctx context.Context, req *v1.EditReq) (res *v1.EditRes, err error) + Del(ctx context.Context, req *v1.DelReq) (res *v1.DelRes, err error) + AppList(ctx context.Context, req *v1.AppListReq) (res *v1.AppListRes, err error) + AppDetail(ctx context.Context, req *v1.AppDetailReq) (res *v1.AppDetailRes, err error) + AppPurchase(ctx context.Context, req *v1.AppPurchaseReq) (res *v1.AppPurchaseRes, err error) + AppProgress(ctx context.Context, req *v1.AppProgressReq) (res *v1.AppProgressRes, err error) } diff --git a/api/chapter/v1/chapter.go b/api/chapter/v1/chapter.go index 6c42daa..8851760 100644 --- a/api/chapter/v1/chapter.go +++ b/api/chapter/v1/chapter.go @@ -6,52 +6,98 @@ import ( "github.com/gogf/gf/v2/frame/g" ) -type ChapterListReq struct { - g.Meta `path:"/chapter" tags:"Chapter" method:"get" summary:"获取章节列表"` +type ListReq struct { + g.Meta `path:"/chapter" tags:"Backend/Author" method:"get" summary:"获取章节列表"` Page int `json:"page" dc:"页码"` Size int `json:"size" dc:"每页数量"` BookId int64 `json:"bookId" dc:"小说ID"` Title string `json:"title" dc:"章节标题(模糊搜索)"` IsLocked int `json:"isLocked" dc:"是否锁定:0免费,1需积分解锁"` } -type ChapterListRes struct { +type ListRes struct { Total int `json:"total" dc:"总数"` List []model.Chapter `json:"list" dc:"章节列表"` } -type ChapterAddReq struct { - g.Meta `path:"/chapter" tags:"Chapter" method:"post" summary:"新增章节"` - BookId int64 `json:"bookId" dc:"小说ID"` - Title string `json:"title" dc:"章节标题"` - Content string `json:"content" dc:"章节内容"` +type AddReq struct { + g.Meta `path:"/chapter" tags:"Backend/Author" method:"post" summary:"新增章节"` + BookId int64 `json:"bookId" dc:"小说ID" v:"required"` + Title string `json:"title" dc:"章节标题" v:"required"` + Content string `json:"content" dc:"章节内容" v:"required"` WordCount int `json:"wordCount" dc:"章节字数"` Sort int `json:"sort" dc:"排序序号"` IsLocked int `json:"isLocked" dc:"是否锁定:0免费,1需积分解锁"` RequiredScore int `json:"requiredScore" dc:"解锁所需积分"` } -type ChapterAddRes struct { +type AddRes struct { Success bool `json:"success" dc:"是否成功"` } -type ChapterEditReq struct { - g.Meta `path:"/chapter" tags:"Chapter" method:"put" summary:"编辑章节"` +type EditReq struct { + g.Meta `path:"/chapter" tags:"Backend/Author" method:"put" summary:"编辑章节"` + Id int64 `json:"id" dc:"章节ID" v:"required"` + BookId int64 `json:"bookId" dc:"小说ID" v:"required"` + Title string `json:"title" dc:"章节标题" v:"required"` + Content string `json:"content" dc:"章节内容" v:"required"` + WordCount int `json:"wordCount" dc:"章节字数"` + Sort int `json:"sort" dc:"排序序号"` + IsLocked int `json:"isLocked" dc:"是否锁定:0免费,1需积分解锁"` + RequiredScore int `json:"requiredScore" dc:"解锁所需积分"` +} +type EditRes struct { + Success bool `json:"success" dc:"是否成功"` +} + +type DelReq struct { + g.Meta `path:"/chapter" tags:"Backend/Author" method:"delete" summary:"删除章节"` + Id int64 `json:"id" dc:"章节ID" v:"required"` +} +type DelRes struct { + Success bool `json:"success" dc:"是否成功"` +} + +type AppListReq struct { + g.Meta `path:"/chapter/app/list" tags:"APP" method:"get" summary:"App获取章节列表"` + BookId int64 `json:"bookId" dc:"书籍ID" v:"required"` + IsDesc bool `json:"isDesc" dc:"是否逆序排列"` + Page int `json:"page" dc:"页码"` + Size int `json:"size" dc:"每页数量"` +} +type AppListRes struct { + Total int `json:"total" dc:"总数"` + List []model.ChapterAppItem `json:"list" dc:"章节列表"` +} + +type AppDetailReq struct { + g.Meta `path:"/chapter/app/detail" tags:"APP" method:"get" summary:"App获取章节详情"` + Id int64 `json:"id" dc:"章节ID" v:"required"` +} +type AppDetailRes struct { Id int64 `json:"id" dc:"章节ID"` - BookId int64 `json:"bookId" dc:"小说ID"` + BookId int64 `json:"bookId" dc:"书籍ID"` Title string `json:"title" dc:"章节标题"` Content string `json:"content" dc:"章节内容"` - WordCount int `json:"wordCount" dc:"章节字数"` - Sort int `json:"sort" dc:"排序序号"` - IsLocked int `json:"isLocked" dc:"是否锁定:0免费,1需积分解锁"` - RequiredScore int `json:"requiredScore" dc:"解锁所需积分"` + WordCount int `json:"wordCount" dc:"字数"` + Sort int `json:"sort" dc:"排序"` + IsLocked int `json:"isLocked" dc:"是否锁定"` + RequiredScore int `json:"requiredScore" dc:"所需积分"` + UpdatedAt string `json:"updatedAt" dc:"更新时间"` } -type ChapterEditRes struct { + +type AppPurchaseReq struct { + g.Meta `path:"/chapter/app/purchase" tags:"APP" method:"post" summary:"App购买章节"` + Id int64 `json:"id" dc:"章节ID" v:"required"` +} +type AppPurchaseRes struct { Success bool `json:"success" dc:"是否成功"` } -type ChapterDelReq struct { - g.Meta `path:"/chapter" tags:"Chapter" method:"delete" summary:"删除章节"` - Id int64 `json:"id" dc:"章节ID"` +type AppProgressReq struct { + g.Meta `path:"/chapter/app/progress" tags:"APP" method:"post" summary:"App上传阅读进度"` + BookId int64 `json:"bookId" dc:"书籍ID" v:"required"` + ChapterId int64 `json:"chapterId" dc:"章节ID" v:"required"` + Progress int `json:"progress" dc:"阅读进度百分比(0-100)"` } -type ChapterDelRes struct { +type AppProgressRes struct { Success bool `json:"success" dc:"是否成功"` } diff --git a/api/feedback/feedback.go b/api/feedback/feedback.go index b8f023e..2f8a3b5 100644 --- a/api/feedback/feedback.go +++ b/api/feedback/feedback.go @@ -11,6 +11,6 @@ import ( ) type IFeedbackV1 interface { - FeedbackList(ctx context.Context, req *v1.FeedbackListReq) (res *v1.FeedbackListRes, err error) - FeedbackAdd(ctx context.Context, req *v1.FeedbackAddReq) (res *v1.FeedbackAddRes, err error) + List(ctx context.Context, req *v1.ListReq) (res *v1.ListRes, err error) + Add(ctx context.Context, req *v1.AddReq) (res *v1.AddRes, err error) } diff --git a/api/feedback/v1/feedback.go b/api/feedback/v1/feedback.go index 0e96c86..8912861 100644 --- a/api/feedback/v1/feedback.go +++ b/api/feedback/v1/feedback.go @@ -6,22 +6,22 @@ import ( "github.com/gogf/gf/v2/frame/g" ) -type FeedbackListReq struct { - g.Meta `path:"/feedback" tags:"Feedback" method:"get" summary:"获取反馈列表"` - Page int `json:"page" dc:"页码"` - Size int `json:"size" dc:"每页数量"` - UserId int64 `json:"userId" dc:"用户ID"` - Status int `json:"status" dc:"处理状态:1未处理,2处理中,3已处理"` +type ListReq struct { + g.Meta `path:"/feedback" tags:"Backend/Admin" method:"get" summary:"获取反馈列表"` + Page int `json:"page" dc:"页码"` + Size int `json:"size" dc:"每页数量"` + UserId int64 `json:"userId" dc:"用户ID"` + Status int `json:"status" dc:"处理状态:1未处理,2处理中,3已处理"` } -type FeedbackListRes struct { +type ListRes struct { Total int `json:"total" dc:"总数"` List []model.Feedback `json:"list" dc:"反馈列表"` } -type FeedbackAddReq struct { - g.Meta `path:"/feedback" tags:"APP/Feedback" method:"post" summary:"新增反馈"` - Content string `json:"content" dc:"反馈内容"` +type AddReq struct { + g.Meta `path:"/feedback" tags:"APP" method:"post" summary:"新增反馈"` + Content string `json:"content" dc:"反馈内容" v:"required"` } -type FeedbackAddRes struct { +type AddRes struct { Success bool `json:"success" dc:"是否成功"` } diff --git a/api/user/user.go b/api/user/user.go index 3883b6b..e92c739 100644 --- a/api/user/user.go +++ b/api/user/user.go @@ -11,7 +11,7 @@ import ( ) type IUserV1 interface { - UserInfo(ctx context.Context, req *v1.UserInfoReq) (res *v1.UserInfoRes, err error) + Info(ctx context.Context, req *v1.InfoReq) (res *v1.InfoRes, err error) Delete(ctx context.Context, req *v1.DeleteReq) (res *v1.DeleteRes, err error) Logout(ctx context.Context, req *v1.LogoutReq) (res *v1.LogoutRes, err error) } diff --git a/api/user/v1/user.go b/api/user/v1/user.go index 2bdd916..51a2927 100644 --- a/api/user/v1/user.go +++ b/api/user/v1/user.go @@ -4,10 +4,10 @@ import ( "github.com/gogf/gf/v2/frame/g" ) -type UserInfoReq struct { - g.Meta `path:"/user/info" tags:"APP/User" method:"get" summary:"获取用户信息"` +type InfoReq struct { + g.Meta `path:"/user/info" tags:"APP" method:"get" summary:"获取用户信息"` } -type UserInfoRes struct { +type InfoRes struct { g.Meta `mime:"application/json"` UserId int64 `json:"userId"` Username string `json:"username"` // 用户名 @@ -17,7 +17,7 @@ type UserInfoRes struct { } type DeleteReq struct { - g.Meta `path:"/user/delete" tags:"APP/User" method:"post" summary:"删除用户"` + g.Meta `path:"/user/delete" tags:"APP" method:"post" summary:"删除用户"` Password string `json:"password" v:"required" dc:"密码"` } @@ -26,7 +26,7 @@ type DeleteRes struct { } type LogoutReq struct { - g.Meta `path:"/user/logout" tags:"APP/User" method:"post" summary:"登出"` + g.Meta `path:"/user/logout" tags:"APP" method:"post" summary:"登出"` } type LogoutRes struct { Success bool `json:"success" dc:"是否成功"` diff --git a/build_x86_64.sh b/build_x86_64.sh new file mode 100755 index 0000000..c42dc82 --- /dev/null +++ b/build_x86_64.sh @@ -0,0 +1 @@ +GOARCH=amd64 GOOS=linux CGO_ENABLED=0 go build -o ./noveltest ./main.go diff --git a/go.mod b/go.mod index a44bc69..1dd4135 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,10 @@ go 1.23.0 toolchain go1.24.3 require ( - github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible + github.com/aws/aws-sdk-go v1.55.7 + github.com/aws/aws-sdk-go-v2 v1.36.5 + github.com/aws/aws-sdk-go-v2/config v1.29.17 + github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8 github.com/casbin/casbin/v2 v2.108.0 github.com/gogf/gf v1.16.9 github.com/gogf/gf/contrib/drivers/mysql/v2 v2.9.0 @@ -19,6 +22,17 @@ require ( require ( github.com/BurntSushi/toml v1.4.0 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.70 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect + github.com/aws/smithy-go v1.22.4 // indirect github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect github.com/casbin/govaluate v1.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect @@ -34,6 +48,7 @@ require ( github.com/gomodule/redigo v1.8.5 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/grokify/html-strip-tags-go v0.1.0 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/magiconair/properties v1.8.9 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -48,6 +63,5 @@ require ( golang.org/x/net v0.32.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect - golang.org/x/time v0.12.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index c6c3839..4a617b8 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,36 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g= -github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= +github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= +github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0= +github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0= +github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0= +github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8= +github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0= +github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4= +github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8 h1:80dpSqWMwx2dAm30Ib7J6ucz1ZHfiv5OCRwN/EnCOXQ= +github.com/aws/aws-sdk-go-v2/service/sqs v1.38.8/go.mod h1:IzNt/udsXlETCdvBOL0nmyMe2t9cGmXmZgsdoZGYYhI= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E= +github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0= +github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w= +github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw= +github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= @@ -67,6 +95,10 @@ github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtg github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= github.com/hailaz/gf-casbin-adapter/v2 v2.8.1 h1:ZFIlfQAYmrL2Fe6/dZz6vCA5hYK0NxhnoKMQpbklgqc= github.com/hailaz/gf-casbin-adapter/v2 v2.8.1/go.mod h1:Jk91dRBZuMVjBMu2oQmqMRc6xuL5V+rzgHeFWvI+wlg= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -132,8 +164,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -141,6 +171,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 22180fa..cc02040 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -4,6 +4,7 @@ import ( "context" "server/internal/controller/admin" "server/internal/controller/auth" + "server/internal/controller/author" "server/internal/controller/book" "server/internal/controller/category" "server/internal/controller/chapter" @@ -35,6 +36,7 @@ var ( group.Middleware(middleware.Casbin) group.Bind( admin.NewV1(), + author.NewV1(), book.NewV1(), category.NewV1(), chapter.NewV1(), diff --git a/internal/controller/admin/admin_v1_admin_edit_pass.go b/internal/controller/admin/admin_v1_admin_edit_pass.go deleted file mode 100644 index 8f99345..0000000 --- a/internal/controller/admin/admin_v1_admin_edit_pass.go +++ /dev/null @@ -1,17 +0,0 @@ -package admin - -import ( - "context" - "server/internal/model" - "server/internal/service" - - "server/api/admin/v1" -) - -func (c *ControllerV1) AdminEditPass(ctx context.Context, req *v1.AdminEditPassReq) (res *v1.AdminEditPassRes, err error) { - out, err := service.Admin().EditPass(ctx, &model.AdminEditPassIn{NewPass: req.NewPass, OldPass: req.OldPass}) - if err != nil { - return nil, err - } - return &v1.AdminEditPassRes{Success: out.Success}, nil -} diff --git a/internal/controller/admin/admin_v1_edit_pass.go b/internal/controller/admin/admin_v1_edit_pass.go new file mode 100644 index 0000000..919a0d4 --- /dev/null +++ b/internal/controller/admin/admin_v1_edit_pass.go @@ -0,0 +1,14 @@ +package admin + +import ( + "context" + + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + + "server/api/admin/v1" +) + +func (c *ControllerV1) EditPass(ctx context.Context, req *v1.EditPassReq) (res *v1.EditPassRes, err error) { + return nil, gerror.NewCode(gcode.CodeNotImplemented) +} diff --git a/internal/controller/admin/admin_v1_admin_info.go b/internal/controller/admin/admin_v1_info.go similarity index 73% rename from internal/controller/admin/admin_v1_admin_info.go rename to internal/controller/admin/admin_v1_info.go index 883aac6..443aad8 100644 --- a/internal/controller/admin/admin_v1_admin_info.go +++ b/internal/controller/admin/admin_v1_info.go @@ -9,13 +9,14 @@ import ( "server/api/admin/v1" ) -func (c *ControllerV1) AdminInfo(ctx context.Context, req *v1.AdminInfoReq) (res *v1.AdminInfoRes, err error) { +func (c *ControllerV1) Info(ctx context.Context, req *v1.InfoReq) (res *v1.InfoRes, err error) { adminId := g.RequestFromCtx(ctx).GetCtxVar("id").Int64() out, err := service.Admin().Info(ctx, &model.AdminInfoIn{AdminId: adminId}) + if err != nil { return nil, err } - return &v1.AdminInfoRes{ + return &v1.InfoRes{ AdminId: out.AdminId, Username: out.Username, }, nil diff --git a/internal/controller/author/author.go b/internal/controller/author/author.go new file mode 100644 index 0000000..c6a519e --- /dev/null +++ b/internal/controller/author/author.go @@ -0,0 +1,5 @@ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package author diff --git a/internal/controller/author/author_new.go b/internal/controller/author/author_new.go new file mode 100644 index 0000000..27cefc9 --- /dev/null +++ b/internal/controller/author/author_new.go @@ -0,0 +1,15 @@ +// ================================================================================= +// This is auto-generated by GoFrame CLI tool only once. Fill this file as you wish. +// ================================================================================= + +package author + +import ( + "server/api/author" +) + +type ControllerV1 struct{} + +func NewV1() author.IAuthorV1 { + return &ControllerV1{} +} diff --git a/internal/controller/author/author_v1_add.go b/internal/controller/author/author_v1_add.go new file mode 100644 index 0000000..cb83a7f --- /dev/null +++ b/internal/controller/author/author_v1_add.go @@ -0,0 +1,24 @@ +package author + +import ( + "context" + v1 "server/api/author/v1" + "server/internal/model" + "server/internal/service" +) + +func (c *ControllerV1) Add(ctx context.Context, req *v1.AddReq) (res *v1.AddRes, err error) { + out, err := service.Author().Create(ctx, &model.AuthorAddIn{ + UserId: req.UserId, + PenName: req.PenName, + Bio: req.Bio, + Status: req.Status, + }) + if err != nil { + return nil, err + } + + return &v1.AddRes{ + Success: out.Success, + }, nil +} diff --git a/internal/controller/author/author_v1_del.go b/internal/controller/author/author_v1_del.go new file mode 100644 index 0000000..3c7031f --- /dev/null +++ b/internal/controller/author/author_v1_del.go @@ -0,0 +1,21 @@ +package author + +import ( + "context" + v1 "server/api/author/v1" + "server/internal/model" + "server/internal/service" +) + +func (c *ControllerV1) Del(ctx context.Context, req *v1.DelReq) (res *v1.DelRes, err error) { + out, err := service.Author().Delete(ctx, &model.AuthorDelIn{ + Id: req.Id, + }) + if err != nil { + return nil, err + } + + return &v1.DelRes{ + Success: out.Success, + }, nil +} diff --git a/internal/controller/author/author_v1_detail.go b/internal/controller/author/author_v1_detail.go new file mode 100644 index 0000000..fa67603 --- /dev/null +++ b/internal/controller/author/author_v1_detail.go @@ -0,0 +1,17 @@ +package author + +import ( + "context" + "server/internal/model" + "server/internal/service" + + "server/api/author/v1" +) + +func (c *ControllerV1) Detail(ctx context.Context, req *v1.DetailReq) (res *v1.DetailRes, err error) { + out, err := service.Author().Detail(ctx, &model.AuthorDetailIn{AuthorId: req.AuthorId}) + if err != nil { + return nil, err + } + return &v1.DetailRes{Author: out}, nil +} diff --git a/internal/controller/author/author_v1_edit.go b/internal/controller/author/author_v1_edit.go new file mode 100644 index 0000000..da7da77 --- /dev/null +++ b/internal/controller/author/author_v1_edit.go @@ -0,0 +1,24 @@ +package author + +import ( + "context" + v1 "server/api/author/v1" + "server/internal/model" + "server/internal/service" +) + +func (c *ControllerV1) Edit(ctx context.Context, req *v1.EditReq) (res *v1.EditRes, err error) { + out, err := service.Author().Update(ctx, &model.AuthorEditIn{ + Id: req.Id, + PenName: req.PenName, + Bio: req.Bio, + Status: req.Status, + }) + if err != nil { + return nil, err + } + + return &v1.EditRes{ + Success: out.Success, + }, nil +} diff --git a/internal/controller/author/author_v1_follow.go b/internal/controller/author/author_v1_follow.go new file mode 100644 index 0000000..9804df3 --- /dev/null +++ b/internal/controller/author/author_v1_follow.go @@ -0,0 +1,31 @@ +package author + +import ( + "context" + v1 "server/api/author/v1" + "server/internal/model" + "server/internal/service" + "server/utility/ecode" + + "github.com/gogf/gf/v2/frame/g" +) + +func (c *ControllerV1) Follow(ctx context.Context, req *v1.FollowReq) (res *v1.FollowRes, err error) { + // 从 ctx 获取用户ID + userId := g.RequestFromCtx(ctx).GetCtxVar("id").Int64() + if userId == 0 { + return nil, ecode.Logout + } + + out, err := service.UserFollowAuthor().Create(ctx, &model.UserFollowAuthorAddIn{ + UserId: userId, + AuthorId: req.AuthorId, + }) + if err != nil { + return nil, err + } + + return &v1.FollowRes{ + Success: out.Success, + }, nil +} diff --git a/internal/controller/author/author_v1_list.go b/internal/controller/author/author_v1_list.go new file mode 100644 index 0000000..06d04dc --- /dev/null +++ b/internal/controller/author/author_v1_list.go @@ -0,0 +1,25 @@ +package author + +import ( + "context" + v1 "server/api/author/v1" + "server/internal/model" + "server/internal/service" +) + +func (c *ControllerV1) List(ctx context.Context, req *v1.ListReq) (res *v1.ListRes, err error) { + out, err := service.Author().List(ctx, &model.AuthorListIn{ + Page: req.Page, + Size: req.Size, + PenName: req.PenName, + Status: req.Status, + }) + if err != nil { + return nil, err + } + + return &v1.ListRes{ + Total: out.Total, + List: out.List, + }, nil +} diff --git a/internal/controller/author/author_v1_unfollow.go b/internal/controller/author/author_v1_unfollow.go new file mode 100644 index 0000000..5a5e701 --- /dev/null +++ b/internal/controller/author/author_v1_unfollow.go @@ -0,0 +1,27 @@ +package author + +import ( + "context" + v1 "server/api/author/v1" + "server/internal/service" + "server/utility/ecode" + + "github.com/gogf/gf/v2/frame/g" +) + +func (c *ControllerV1) Unfollow(ctx context.Context, req *v1.UnfollowReq) (res *v1.UnfollowRes, err error) { + // 从 ctx 获取用户ID + userId := g.RequestFromCtx(ctx).GetCtxVar("id").Int64() + if userId == 0 { + return nil, ecode.Logout + } + + out, err := service.UserFollowAuthor().Unfollow(ctx, userId, req.AuthorId) + if err != nil { + return nil, err + } + + return &v1.UnfollowRes{ + Success: out.Success, + }, nil +} diff --git a/internal/controller/book/book_v1_book_add.go b/internal/controller/book/book_v1_add.go similarity index 71% rename from internal/controller/book/book_v1_book_add.go rename to internal/controller/book/book_v1_add.go index 11eb037..01e00a8 100644 --- a/internal/controller/book/book_v1_book_add.go +++ b/internal/controller/book/book_v1_add.go @@ -2,25 +2,27 @@ package book import ( "context" + v1 "server/api/book/v1" "server/internal/model" "server/internal/service" - - "server/api/book/v1" ) -func (c *ControllerV1) BookAdd(ctx context.Context, req *v1.BookAddReq) (res *v1.BookAddRes, err error) { +func (c *ControllerV1) Add(ctx context.Context, req *v1.AddReq) (res *v1.AddRes, err error) { out, err := service.Book().Create(ctx, &model.BookAddIn{ AuthorId: req.AuthorId, CategoryId: req.CategoryId, + Title: req.Title, CoverUrl: req.CoverUrl, Description: req.Description, - IsRecommended: req.IsRecommended, Status: req.Status, Tags: req.Tags, - Title: req.Title, + IsRecommended: req.IsRecommended, }) if err != nil { return nil, err } - return &v1.BookAddRes{Success: out.Success}, nil + + return &v1.AddRes{ + Success: out.Success, + }, nil } diff --git a/internal/controller/book/book_v1_app_detail.go b/internal/controller/book/book_v1_app_detail.go new file mode 100644 index 0000000..1dc79aa --- /dev/null +++ b/internal/controller/book/book_v1_app_detail.go @@ -0,0 +1,44 @@ +package book + +import ( + "context" + v1 "server/api/book/v1" + "server/internal/model" + "server/internal/service" + + "github.com/gogf/gf/v2/frame/g" +) + +func (c *ControllerV1) AppDetail(ctx context.Context, req *v1.AppDetailReq) (res *v1.AppDetailRes, err error) { + // 从 ctx 获取用户ID + userId := g.RequestFromCtx(ctx).GetCtxVar("id").Int64() + + out, err := service.Book().AppDetail(ctx, &model.BookAppDetailIn{ + Id: req.Id, + UserId: userId, + }) + if err != nil { + return nil, err + } + + return &v1.AppDetailRes{ + Id: out.Id, + AuthorId: out.AuthorId, + CategoryId: out.CategoryId, + Title: out.Title, + CoverUrl: out.CoverUrl, + Description: out.Description, + Status: out.Status, + Tags: out.Tags, + IsRecommended: out.IsRecommended, + Rating: out.Rating, + CurrentReaders: out.CurrentReaders, + CreatedAt: out.CreatedAt.String(), + UpdatedAt: out.UpdatedAt.String(), + // 添加阅读进度信息 + HasRead: out.HasRead, + ReadProgress: out.ReadProgress, + LastChapterId: out.LastChapterId, + LastReadAt: out.LastReadAt, + }, nil +} diff --git a/internal/controller/book/book_v1_app_list.go b/internal/controller/book/book_v1_app_list.go new file mode 100644 index 0000000..9c4a5e4 --- /dev/null +++ b/internal/controller/book/book_v1_app_list.go @@ -0,0 +1,35 @@ +package book + +import ( + "context" + v1 "server/api/book/v1" + "server/internal/model" + "server/internal/service" + + "github.com/gogf/gf/v2/frame/g" +) + +func (c *ControllerV1) AppList(ctx context.Context, req *v1.AppListReq) (res *v1.AppListRes, err error) { + userId := g.RequestFromCtx(ctx).GetCtxVar("id").Int64() + out, err := service.Book().AppList(ctx, &model.BookAppListIn{ + Page: req.Page, + Size: req.Size, + IsRecommended: req.IsRecommended, + IsFeatured: req.IsFeatured, + IsLatest: req.IsLatest, + CategoryId: req.CategoryId, + Title: req.Title, + AuthorId: req.AuthorId, + UserId: userId, + Language: req.Language, + Sort: req.Sort, + }) + if err != nil { + return nil, err + } + + return &v1.AppListRes{ + Total: out.Total, + List: out.List, + }, nil +} diff --git a/internal/controller/book/book_v1_app_rate.go b/internal/controller/book/book_v1_app_rate.go new file mode 100644 index 0000000..621689d --- /dev/null +++ b/internal/controller/book/book_v1_app_rate.go @@ -0,0 +1,26 @@ +package book + +import ( + "context" + v1 "server/api/book/v1" + "server/internal/model" + "server/internal/service" + + "github.com/gogf/gf/v2/frame/g" +) + +func (c *ControllerV1) AppRate(ctx context.Context, req *v1.AppRateReq) (res *v1.AppRateRes, err error) { + userId := g.RequestFromCtx(ctx).GetCtxVar("id").Int64() + out, err := service.Book().AppRate(ctx, &model.BookAppRateIn{ + BookId: req.BookId, + Rating: req.Rating, + UserId: userId, + }) + if err != nil { + return nil, err + } + + return &v1.AppRateRes{ + Success: out.Success, + }, nil +} diff --git a/internal/controller/book/book_v1_book_set_featured.go b/internal/controller/book/book_v1_book_set_featured.go new file mode 100644 index 0000000..fdf41b2 --- /dev/null +++ b/internal/controller/book/book_v1_book_set_featured.go @@ -0,0 +1,14 @@ +package book + +import ( + "context" + + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + + "server/api/book/v1" +) + +func (c *ControllerV1) BookSetFeatured(ctx context.Context, req *v1.BookSetFeaturedReq) (res *v1.BookSetFeaturedRes, err error) { + return nil, gerror.NewCode(gcode.CodeNotImplemented) +} diff --git a/internal/controller/book/book_v1_book_set_recommended.go b/internal/controller/book/book_v1_book_set_recommended.go new file mode 100644 index 0000000..e02499b --- /dev/null +++ b/internal/controller/book/book_v1_book_set_recommended.go @@ -0,0 +1,14 @@ +package book + +import ( + "context" + + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + + "server/api/book/v1" +) + +func (c *ControllerV1) BookSetRecommended(ctx context.Context, req *v1.BookSetRecommendedReq) (res *v1.BookSetRecommendedRes, err error) { + return nil, gerror.NewCode(gcode.CodeNotImplemented) +} diff --git a/internal/controller/book/book_v1_book_del.go b/internal/controller/book/book_v1_del.go similarity index 53% rename from internal/controller/book/book_v1_book_del.go rename to internal/controller/book/book_v1_del.go index 78efc66..89f52b9 100644 --- a/internal/controller/book/book_v1_book_del.go +++ b/internal/controller/book/book_v1_del.go @@ -2,18 +2,20 @@ package book import ( "context" + v1 "server/api/book/v1" "server/internal/model" "server/internal/service" - - "server/api/book/v1" ) -func (c *ControllerV1) BookDel(ctx context.Context, req *v1.BookDelReq) (res *v1.BookDelRes, err error) { +func (c *ControllerV1) Del(ctx context.Context, req *v1.DelReq) (res *v1.DelRes, err error) { out, err := service.Book().Delete(ctx, &model.BookDelIn{ Id: req.Id, }) if err != nil { return nil, err } - return &v1.BookDelRes{Success: out.Success}, nil + + return &v1.DelRes{ + Success: out.Success, + }, nil } diff --git a/internal/controller/book/book_v1_book_edit.go b/internal/controller/book/book_v1_edit.go similarity index 71% rename from internal/controller/book/book_v1_book_edit.go rename to internal/controller/book/book_v1_edit.go index 0a2e443..9608e7a 100644 --- a/internal/controller/book/book_v1_book_edit.go +++ b/internal/controller/book/book_v1_edit.go @@ -2,26 +2,28 @@ package book import ( "context" + v1 "server/api/book/v1" "server/internal/model" "server/internal/service" - - "server/api/book/v1" ) -func (c *ControllerV1) BookEdit(ctx context.Context, req *v1.BookEditReq) (res *v1.BookEditRes, err error) { +func (c *ControllerV1) Edit(ctx context.Context, req *v1.EditReq) (res *v1.EditRes, err error) { out, err := service.Book().Update(ctx, &model.BookEditIn{ + Id: req.Id, AuthorId: req.AuthorId, CategoryId: req.CategoryId, + Title: req.Title, CoverUrl: req.CoverUrl, Description: req.Description, - Id: req.Id, - IsRecommended: req.IsRecommended, Status: req.Status, Tags: req.Tags, - Title: req.Title, + IsRecommended: req.IsRecommended, }) if err != nil { return nil, err } - return &v1.BookEditRes{Success: out.Success}, nil + + return &v1.EditRes{ + Success: out.Success, + }, nil } diff --git a/internal/controller/book/book_v1_book_list.go b/internal/controller/book/book_v1_list.go similarity index 72% rename from internal/controller/book/book_v1_book_list.go rename to internal/controller/book/book_v1_list.go index 2fa3e2e..1c8f2f4 100644 --- a/internal/controller/book/book_v1_book_list.go +++ b/internal/controller/book/book_v1_list.go @@ -2,27 +2,28 @@ package book import ( "context" + v1 "server/api/book/v1" "server/internal/model" "server/internal/service" - - "server/api/book/v1" ) -func (c *ControllerV1) BookList(ctx context.Context, req *v1.BookListReq) (res *v1.BookListRes, err error) { +func (c *ControllerV1) List(ctx context.Context, req *v1.ListReq) (res *v1.ListRes, err error) { out, err := service.Book().List(ctx, &model.BookListIn{ - AuthorId: req.AuthorId, - CategoryId: req.CategoryId, - IsRecommended: req.IsRecommended, Page: req.Page, Size: req.Size, - Status: req.Status, Title: req.Title, + CategoryId: req.CategoryId, + AuthorId: req.AuthorId, + Status: req.Status, + IsRecommended: req.IsRecommended, + Sort: req.Sort, }) if err != nil { return nil, err } - return &v1.BookListRes{ - List: out.List, + + return &v1.ListRes{ Total: out.Total, + List: out.List, }, nil } diff --git a/internal/controller/book/book_v1_my_list.go b/internal/controller/book/book_v1_my_list.go new file mode 100644 index 0000000..ae1d7e7 --- /dev/null +++ b/internal/controller/book/book_v1_my_list.go @@ -0,0 +1,32 @@ +package book + +import ( + "context" + + v1 "server/api/book/v1" + "server/internal/model" + "server/internal/service" + + "github.com/gogf/gf/v2/net/ghttp" +) + +func (c *ControllerV1) MyList(ctx context.Context, req *v1.MyListReq) (res *v1.MyListRes, err error) { + // 从ctx获取userId + userId := ghttp.RequestFromCtx(ctx).GetCtxVar("id").Int64() + in := &model.MyBookListIn{ + Type: req.Type, + Page: req.Page, + Size: req.Size, + UserId: userId, + Sort: req.Sort, + } + result, err := service.Book().MyList(ctx, in) + if err != nil { + return nil, err + } + res = &v1.MyListRes{ + Total: result.Total, + List: result.List, + } + return +} diff --git a/internal/controller/book/book_v1_shelf_add.go b/internal/controller/book/book_v1_shelf_add.go new file mode 100644 index 0000000..574dc0e --- /dev/null +++ b/internal/controller/book/book_v1_shelf_add.go @@ -0,0 +1,31 @@ +package book + +import ( + "context" + v1 "server/api/book/v1" + "server/internal/model" + "server/internal/service" + "server/utility/ecode" + + "github.com/gogf/gf/v2/frame/g" +) + +func (c *ControllerV1) ShelfAdd(ctx context.Context, req *v1.ShelfAddReq) (res *v1.ShelfAddRes, err error) { + // 从 ctx 获取用户ID + userId := g.RequestFromCtx(ctx).GetCtxVar("id").Int64() + if userId == 0 { + return nil, ecode.Logout + } + + out, err := service.Bookshelve().Add(ctx, &model.BookshelveAddIn{ + UserId: userId, + BookId: req.BookId, + }) + if err != nil { + return nil, err + } + + return &v1.ShelfAddRes{ + Success: out.Success, + }, nil +} diff --git a/internal/controller/book/book_v1_shelf_remove.go b/internal/controller/book/book_v1_shelf_remove.go new file mode 100644 index 0000000..f9b0472 --- /dev/null +++ b/internal/controller/book/book_v1_shelf_remove.go @@ -0,0 +1,31 @@ +package book + +import ( + "context" + v1 "server/api/book/v1" + "server/internal/model" + "server/internal/service" + "server/utility/ecode" + + "github.com/gogf/gf/v2/frame/g" +) + +func (c *ControllerV1) ShelfRemove(ctx context.Context, req *v1.ShelfRemoveReq) (res *v1.ShelfRemoveRes, err error) { + // 从 ctx 获取用户ID + userId := g.RequestFromCtx(ctx).GetCtxVar("id").Int64() + if userId == 0 { + return nil, ecode.Logout + } + + out, err := service.Bookshelve().Delete(ctx, &model.BookshelveDelIn{ + UserId: userId, + BookIds: req.BookIds, + }) + if err != nil { + return nil, err + } + + return &v1.ShelfRemoveRes{ + Success: out.Success, + }, nil +} diff --git a/internal/controller/category/category_v1_category_add.go b/internal/controller/category/category_v1_add.go similarity index 53% rename from internal/controller/category/category_v1_category_add.go rename to internal/controller/category/category_v1_add.go index fc1f93e..2077711 100644 --- a/internal/controller/category/category_v1_category_add.go +++ b/internal/controller/category/category_v1_add.go @@ -5,18 +5,18 @@ import ( "server/internal/model" "server/internal/service" - "server/api/category/v1" + v1 "server/api/category/v1" ) -func (c *ControllerV1) CategoryAdd(ctx context.Context, req *v1.CategoryAddReq) (res *v1.CategoryAddRes, err error) { +func (c *ControllerV1) Add(ctx context.Context, req *v1.AddReq) (res *v1.AddRes, err error) { out, err := service.Category().Create(ctx, &model.CategoryAddIn{ - Name: req.Name, - Type: req.Type, + Name: req.Name, + Channel: req.Channel, }) if err != nil { return nil, err } - return &v1.CategoryAddRes{ + return &v1.AddRes{ Success: out.Success, }, nil } diff --git a/internal/controller/category/category_v1_category_list.go b/internal/controller/category/category_v1_category_list.go deleted file mode 100644 index 890ad90..0000000 --- a/internal/controller/category/category_v1_category_list.go +++ /dev/null @@ -1,25 +0,0 @@ -package category - -import ( - "context" - "server/internal/model" - "server/internal/service" - - "server/api/category/v1" -) - -func (c *ControllerV1) CategoryList(ctx context.Context, req *v1.CategoryListReq) (res *v1.CategoryListRes, err error) { - out, err := service.Category().List(ctx, &model.CategoryListIn{ - Name: req.Name, - Page: req.Page, - Size: req.Size, - Type: req.Type, - }) - if err != nil { - return nil, err - } - return &v1.CategoryListRes{ - List: out.List, - Total: out.Total, - }, nil -} diff --git a/internal/controller/category/category_v1_category_del.go b/internal/controller/category/category_v1_del.go similarity index 65% rename from internal/controller/category/category_v1_category_del.go rename to internal/controller/category/category_v1_del.go index 22407c0..f22a595 100644 --- a/internal/controller/category/category_v1_category_del.go +++ b/internal/controller/category/category_v1_del.go @@ -8,14 +8,14 @@ import ( "server/api/category/v1" ) -func (c *ControllerV1) CategoryDel(ctx context.Context, req *v1.CategoryDelReq) (res *v1.CategoryDelRes, err error) { +func (c *ControllerV1) Del(ctx context.Context, req *v1.DelReq) (res *v1.DelRes, err error) { out, err := service.Category().Delete(ctx, &model.CategoryDelIn{ Id: req.Id, }) if err != nil { return nil, err } - return &v1.CategoryDelRes{ + return &v1.DelRes{ Success: out.Success, }, nil } diff --git a/internal/controller/category/category_v1_category_edit.go b/internal/controller/category/category_v1_edit.go similarity index 50% rename from internal/controller/category/category_v1_category_edit.go rename to internal/controller/category/category_v1_edit.go index 440400a..0074447 100644 --- a/internal/controller/category/category_v1_category_edit.go +++ b/internal/controller/category/category_v1_edit.go @@ -5,19 +5,19 @@ import ( "server/internal/model" "server/internal/service" - "server/api/category/v1" + v1 "server/api/category/v1" ) -func (c *ControllerV1) CategoryEdit(ctx context.Context, req *v1.CategoryEditReq) (res *v1.CategoryEditRes, err error) { +func (c *ControllerV1) Edit(ctx context.Context, req *v1.EditReq) (res *v1.EditRes, err error) { out, err := service.Category().Update(ctx, &model.CategoryEditIn{ - Id: req.Id, - Name: req.Name, - Type: req.Type, + Id: req.Id, + Name: req.Name, + Channel: req.Channel, }) if err != nil { return nil, err } - return &v1.CategoryEditRes{ + return &v1.EditRes{ Success: out.Success, }, nil } diff --git a/internal/controller/category/category_v1_list.go b/internal/controller/category/category_v1_list.go new file mode 100644 index 0000000..18b5358 --- /dev/null +++ b/internal/controller/category/category_v1_list.go @@ -0,0 +1,30 @@ +package category + +import ( + "context" + + v1 "server/api/category/v1" + "server/internal/model" + "server/internal/service" +) + +func (c *ControllerV1) List(ctx context.Context, req *v1.ListReq) (res *v1.ListRes, err error) { + // 调用service层获取分类列表 + result, err := service.Category().List(ctx, &model.CategoryListIn{ + Page: req.Page, + Size: req.Size, + Name: req.Name, + Channel: req.Channel, + }) + if err != nil { + return nil, err + } + + // 构造响应 + res = &v1.ListRes{ + Total: result.Total, + List: result.List, + } + + return res, nil +} diff --git a/internal/controller/chapter/chapter_v1_chapter_add.go b/internal/controller/chapter/chapter_v1_add.go similarity index 76% rename from internal/controller/chapter/chapter_v1_chapter_add.go rename to internal/controller/chapter/chapter_v1_add.go index 920aafe..7f9c744 100644 --- a/internal/controller/chapter/chapter_v1_chapter_add.go +++ b/internal/controller/chapter/chapter_v1_add.go @@ -8,7 +8,7 @@ import ( "server/api/chapter/v1" ) -func (c *ControllerV1) ChapterAdd(ctx context.Context, req *v1.ChapterAddReq) (res *v1.ChapterAddRes, err error) { +func (c *ControllerV1) Add(ctx context.Context, req *v1.AddReq) (res *v1.AddRes, err error) { out, err := service.Chapter().Create(ctx, &model.ChapterAddIn{ BookId: req.BookId, Content: req.Content, @@ -21,7 +21,7 @@ func (c *ControllerV1) ChapterAdd(ctx context.Context, req *v1.ChapterAddReq) (r if err != nil { return nil, err } - return &v1.ChapterAddRes{ + return &v1.AddRes{ Success: out.Success, }, nil } diff --git a/internal/controller/chapter/chapter_v1_app_detail.go b/internal/controller/chapter/chapter_v1_app_detail.go new file mode 100644 index 0000000..f024105 --- /dev/null +++ b/internal/controller/chapter/chapter_v1_app_detail.go @@ -0,0 +1,29 @@ +package chapter + +import ( + "context" + v1 "server/api/chapter/v1" + "server/internal/model" + "server/internal/service" +) + +func (c *ControllerV1) AppDetail(ctx context.Context, req *v1.AppDetailReq) (res *v1.AppDetailRes, err error) { + out, err := service.Chapter().AppDetail(ctx, &model.ChapterAppDetailIn{ + Id: req.Id, + }) + if err != nil { + return nil, err + } + + return &v1.AppDetailRes{ + Id: out.Id, + BookId: out.BookId, + Title: out.Title, + Content: out.Content, + WordCount: out.WordCount, + Sort: out.Sort, + IsLocked: out.IsLocked, + RequiredScore: out.RequiredScore, + UpdatedAt: out.UpdatedAt.String(), + }, nil +} diff --git a/internal/controller/chapter/chapter_v1_app_list.go b/internal/controller/chapter/chapter_v1_app_list.go new file mode 100644 index 0000000..d455551 --- /dev/null +++ b/internal/controller/chapter/chapter_v1_app_list.go @@ -0,0 +1,29 @@ +package chapter + +import ( + "context" + v1 "server/api/chapter/v1" + "server/internal/model" + "server/internal/service" + + "github.com/gogf/gf/v2/frame/g" +) + +func (c *ControllerV1) AppList(ctx context.Context, req *v1.AppListReq) (res *v1.AppListRes, err error) { + userId := g.RequestFromCtx(ctx).GetCtxVar("id").Int64() + out, err := service.Chapter().AppList(ctx, &model.ChapterAppListIn{ + BookId: req.BookId, + IsDesc: req.IsDesc, + Page: req.Page, + Size: req.Size, + UserId: userId, + }) + if err != nil { + return nil, err + } + + return &v1.AppListRes{ + Total: out.Total, + List: out.List, + }, nil +} diff --git a/internal/controller/chapter/chapter_v1_app_progress.go b/internal/controller/chapter/chapter_v1_app_progress.go new file mode 100644 index 0000000..fbee63b --- /dev/null +++ b/internal/controller/chapter/chapter_v1_app_progress.go @@ -0,0 +1,27 @@ +package chapter + +import ( + "context" + v1 "server/api/chapter/v1" + "server/internal/model" + "server/internal/service" + + "github.com/gogf/gf/v2/frame/g" +) + +func (c *ControllerV1) AppProgress(ctx context.Context, req *v1.AppProgressReq) (res *v1.AppProgressRes, err error) { + userId := g.RequestFromCtx(ctx).GetCtxVar("id").Int64() + out, err := service.Chapter().AppProgress(ctx, &model.ChapterAppProgressIn{ + BookId: req.BookId, + ChapterId: req.ChapterId, + Progress: req.Progress, + UserId: userId, + }) + if err != nil { + return nil, err + } + + return &v1.AppProgressRes{ + Success: out.Success, + }, nil +} diff --git a/internal/controller/chapter/chapter_v1_app_purchase.go b/internal/controller/chapter/chapter_v1_app_purchase.go new file mode 100644 index 0000000..37e1e29 --- /dev/null +++ b/internal/controller/chapter/chapter_v1_app_purchase.go @@ -0,0 +1,25 @@ +package chapter + +import ( + "context" + v1 "server/api/chapter/v1" + "server/internal/model" + "server/internal/service" + + "github.com/gogf/gf/v2/frame/g" +) + +func (c *ControllerV1) AppPurchase(ctx context.Context, req *v1.AppPurchaseReq) (res *v1.AppPurchaseRes, err error) { + userId := g.RequestFromCtx(ctx).GetCtxVar("id").Int64() + out, err := service.Chapter().AppPurchase(ctx, &model.ChapterAppPurchaseIn{ + Id: req.Id, + UserId: userId, + }) + if err != nil { + return nil, err + } + + return &v1.AppPurchaseRes{ + Success: out.Success, + }, nil +} diff --git a/internal/controller/chapter/chapter_v1_chapter_del.go b/internal/controller/chapter/chapter_v1_del.go similarity index 58% rename from internal/controller/chapter/chapter_v1_chapter_del.go rename to internal/controller/chapter/chapter_v1_del.go index 850be4c..434a8e4 100644 --- a/internal/controller/chapter/chapter_v1_chapter_del.go +++ b/internal/controller/chapter/chapter_v1_del.go @@ -8,12 +8,12 @@ import ( "server/api/chapter/v1" ) -func (c *ControllerV1) ChapterDel(ctx context.Context, req *v1.ChapterDelReq) (res *v1.ChapterDelRes, err error) { +func (c *ControllerV1) Del(ctx context.Context, req *v1.DelReq) (res *v1.DelRes, err error) { out, err := service.Chapter().Delete(ctx, &model.ChapterDelIn{ Id: req.Id, }) if err != nil { return nil, err } - return &v1.ChapterDelRes{Success: out.Success}, nil + return &v1.DelRes{Success: out.Success}, nil } diff --git a/internal/controller/chapter/chapter_v1_chapter_edit.go b/internal/controller/chapter/chapter_v1_edit.go similarity index 77% rename from internal/controller/chapter/chapter_v1_chapter_edit.go rename to internal/controller/chapter/chapter_v1_edit.go index fc23813..1d33781 100644 --- a/internal/controller/chapter/chapter_v1_chapter_edit.go +++ b/internal/controller/chapter/chapter_v1_edit.go @@ -8,21 +8,21 @@ import ( "server/api/chapter/v1" ) -func (c *ControllerV1) ChapterEdit(ctx context.Context, req *v1.ChapterEditReq) (res *v1.ChapterEditRes, err error) { +func (c *ControllerV1) Edit(ctx context.Context, req *v1.EditReq) (res *v1.EditRes, err error) { out, err := service.Chapter().Update(ctx, &model.ChapterEditIn{ - BookId: req.BookId, - Content: req.Content, Id: req.Id, - IsLocked: req.IsLocked, - RequiredScore: req.RequiredScore, - Sort: req.Sort, + BookId: req.BookId, Title: req.Title, + Sort: req.Sort, + IsLocked: req.IsLocked, + Content: req.Content, WordCount: req.WordCount, + RequiredScore: req.RequiredScore, }) if err != nil { return nil, err } - return &v1.ChapterEditRes{ + return &v1.EditRes{ Success: out.Success, }, nil } diff --git a/internal/controller/chapter/chapter_v1_chapter_list.go b/internal/controller/chapter/chapter_v1_list.go similarity index 72% rename from internal/controller/chapter/chapter_v1_chapter_list.go rename to internal/controller/chapter/chapter_v1_list.go index 8dad245..18b2221 100644 --- a/internal/controller/chapter/chapter_v1_chapter_list.go +++ b/internal/controller/chapter/chapter_v1_list.go @@ -8,7 +8,7 @@ import ( "server/api/chapter/v1" ) -func (c *ControllerV1) ChapterList(ctx context.Context, req *v1.ChapterListReq) (res *v1.ChapterListRes, err error) { +func (c *ControllerV1) List(ctx context.Context, req *v1.ListReq) (res *v1.ListRes, err error) { out, err := service.Chapter().List(ctx, &model.ChapterListIn{ BookId: req.BookId, IsLocked: req.IsLocked, @@ -19,7 +19,7 @@ func (c *ControllerV1) ChapterList(ctx context.Context, req *v1.ChapterListReq) if err != nil { return nil, err } - return &v1.ChapterListRes{ + return &v1.ListRes{ List: out.List, Total: out.Total, }, nil diff --git a/internal/controller/feedback/feedback_v1_feedback_add.go b/internal/controller/feedback/feedback_v1_add.go similarity index 67% rename from internal/controller/feedback/feedback_v1_feedback_add.go rename to internal/controller/feedback/feedback_v1_add.go index 67627cf..be956ac 100644 --- a/internal/controller/feedback/feedback_v1_feedback_add.go +++ b/internal/controller/feedback/feedback_v1_add.go @@ -9,7 +9,7 @@ import ( "server/api/feedback/v1" ) -func (c *ControllerV1) FeedbackAdd(ctx context.Context, req *v1.FeedbackAddReq) (res *v1.FeedbackAddRes, err error) { +func (c *ControllerV1) Add(ctx context.Context, req *v1.AddReq) (res *v1.AddRes, err error) { userId := g.RequestFromCtx(ctx).GetCtxVar("id").Int64() out, err := service.Feedback().Create(ctx, &model.FeedbackAddIn{ Content: req.Content, @@ -18,7 +18,5 @@ func (c *ControllerV1) FeedbackAdd(ctx context.Context, req *v1.FeedbackAddReq) if err != nil { return nil, err } - return &v1.FeedbackAddRes{ - Success: out.Success, - }, nil + return &v1.AddRes{Success: out.Success}, nil } diff --git a/internal/controller/feedback/feedback_v1_feedback_list.go b/internal/controller/feedback/feedback_v1_list.go similarity index 70% rename from internal/controller/feedback/feedback_v1_feedback_list.go rename to internal/controller/feedback/feedback_v1_list.go index c61c4be..0dd2655 100644 --- a/internal/controller/feedback/feedback_v1_feedback_list.go +++ b/internal/controller/feedback/feedback_v1_list.go @@ -8,7 +8,7 @@ import ( "server/api/feedback/v1" ) -func (c *ControllerV1) FeedbackList(ctx context.Context, req *v1.FeedbackListReq) (res *v1.FeedbackListRes, err error) { +func (c *ControllerV1) List(ctx context.Context, req *v1.ListReq) (res *v1.ListRes, err error) { out, err := service.Feedback().List(ctx, &model.FeedbackListIn{ Page: req.Page, Size: req.Size, @@ -18,8 +18,8 @@ func (c *ControllerV1) FeedbackList(ctx context.Context, req *v1.FeedbackListReq if err != nil { return nil, err } - return &v1.FeedbackListRes{ - List: out.List, + return &v1.ListRes{ Total: out.Total, + List: out.List, }, nil } diff --git a/internal/controller/user/user_v1_info.go b/internal/controller/user/user_v1_info.go new file mode 100644 index 0000000..9c8a07b --- /dev/null +++ b/internal/controller/user/user_v1_info.go @@ -0,0 +1,32 @@ +package user + +import ( + "context" + v1 "server/api/user/v1" + "server/internal/model" + "server/internal/service" + "server/utility/ecode" + + "github.com/gogf/gf/v2/frame/g" +) + +func (c *ControllerV1) Info(ctx context.Context, req *v1.InfoReq) (res *v1.InfoRes, err error) { + // 从ctx获取userId + userId := g.RequestFromCtx(ctx).GetCtxVar("id").Int64() + if userId == 0 { + return nil, ecode.Logout + } + + out, err := service.User().Info(ctx, &model.UserInfoIn{UserId: userId}) + if err != nil { + return nil, err + } + + return &v1.InfoRes{ + UserId: out.UserId, + Username: out.Username, + Avatar: out.Avatar, + Email: out.Email, + Points: out.Points, + }, nil +} diff --git a/internal/controller/user/user_v1_user_info.go b/internal/controller/user/user_v1_user_info.go deleted file mode 100644 index 6e17ff4..0000000 --- a/internal/controller/user/user_v1_user_info.go +++ /dev/null @@ -1,24 +0,0 @@ -package user - -import ( - "context" - "github.com/gogf/gf/v2/frame/g" - "server/internal/model" - "server/internal/service" - - "server/api/user/v1" -) - -func (c *ControllerV1) UserInfo(ctx context.Context, req *v1.UserInfoReq) (res *v1.UserInfoRes, err error) { - userId := g.RequestFromCtx(ctx).GetCtxVar("id").Int64() - info, err := service.User().Info(ctx, &model.UserInfoIn{UserId: userId}) - if err != nil { - return nil, err - } - return &v1.UserInfoRes{ - Username: info.Username, - Avatar: info.Avatar, - Email: info.Email, - Points: info.Points, - }, nil -} diff --git a/internal/dao/internal/books.go b/internal/dao/internal/books.go index 6fed503..1b5fca3 100644 --- a/internal/dao/internal/books.go +++ b/internal/dao/internal/books.go @@ -20,44 +20,48 @@ type BooksDao struct { // BooksColumns defines and stores column names for the table books. type BooksColumns struct { - Id string // 小说ID - AuthorId string // 作者ID - CategoryId string // 分类ID - Title string // 小说标题 - CoverUrl string // 封面图片URL - Description string // 小说简介 - Status string // 状态:1=连载中,2=完结,3=下架 - WordsCount string // 字数 - ChaptersCount string // 章节数 - LatestChapterId string // 最新章节ID - Rating string // 评分(0.00~10.00) - ReadCount string // 阅读人数 - Tags string // 标签(逗号分隔) - CreatedAt string // 创建时间 - UpdatedAt string // 更新时间 - DeletedAt string // 软删除时间戳 - IsRecommended string // 是否推荐:0=否,1=是 + Id string // 小说ID + AuthorId string // 作者ID + CategoryId string // 分类ID + Title string // 小说标题 + CoverUrl string // 封面图片URL + Description string // 小说简介 + Status string // 状态:1=连载中,2=完结,3=下架 + WordsCount string // 字数 + ChaptersCount string // 章节数 + Rating string // 评分(0.00~10.00) + ReadCount string // 阅读人数 + CurrentReaders string // 在读人数 + Tags string // 标签(逗号分隔) + CreatedAt string // 创建时间 + UpdatedAt string // 更新时间 + DeletedAt string // 软删除时间戳 + IsRecommended string // 是否推荐:0=否,1=是 + IsFeatured string // 是否精选:0=否,1=是 + Language string // 语言,如 zh=中文,en=英文,jp=日文 } // booksColumns holds the columns for the table books. var booksColumns = BooksColumns{ - Id: "id", - AuthorId: "author_id", - CategoryId: "category_id", - Title: "title", - CoverUrl: "cover_url", - Description: "description", - Status: "status", - WordsCount: "words_count", - ChaptersCount: "chapters_count", - LatestChapterId: "latest_chapter_id", - Rating: "rating", - ReadCount: "read_count", - Tags: "tags", - CreatedAt: "created_at", - UpdatedAt: "updated_at", - DeletedAt: "deleted_at", - IsRecommended: "is_recommended", + Id: "id", + AuthorId: "author_id", + CategoryId: "category_id", + Title: "title", + CoverUrl: "cover_url", + Description: "description", + Status: "status", + WordsCount: "words_count", + ChaptersCount: "chapters_count", + Rating: "rating", + ReadCount: "read_count", + CurrentReaders: "current_readers", + Tags: "tags", + CreatedAt: "created_at", + UpdatedAt: "updated_at", + DeletedAt: "deleted_at", + IsRecommended: "is_recommended", + IsFeatured: "is_featured", + Language: "language", } // NewBooksDao creates and returns a new DAO object for table data access. diff --git a/internal/dao/internal/bookshelves.go b/internal/dao/internal/bookshelves.go index 237c2f5..6b2b23b 100644 --- a/internal/dao/internal/bookshelves.go +++ b/internal/dao/internal/bookshelves.go @@ -27,6 +27,7 @@ type BookshelvesColumns struct { LastReadChapterId string // 最后阅读章节ID LastReadPercent string // 阅读进度百分比(0.00~100.00) LastReadAt string // 最后阅读时间 + ReadStatus string // 阅读状态:1=正在读,2=已读完,3=已收藏 } // bookshelvesColumns holds the columns for the table bookshelves. @@ -38,6 +39,7 @@ var bookshelvesColumns = BookshelvesColumns{ LastReadChapterId: "last_read_chapter_id", LastReadPercent: "last_read_percent", LastReadAt: "last_read_at", + ReadStatus: "read_status", } // NewBookshelvesDao creates and returns a new DAO object for table data access. diff --git a/internal/dao/internal/categories.go b/internal/dao/internal/categories.go index 1760a15..70acac7 100644 --- a/internal/dao/internal/categories.go +++ b/internal/dao/internal/categories.go @@ -22,20 +22,20 @@ type CategoriesDao struct { type CategoriesColumns struct { Id string // 分类ID Name string // 分类名称 - Type string // 分类类型:1=男频, 2=女频 CreatedAt string // 创建时间 UpdatedAt string // 更新时间 DeletedAt string // 软删除时间戳 + Channel string // 频道类型:1=男频,2=女频 } // categoriesColumns holds the columns for the table categories. var categoriesColumns = CategoriesColumns{ Id: "id", Name: "name", - Type: "type", CreatedAt: "created_at", UpdatedAt: "updated_at", DeletedAt: "deleted_at", + Channel: "channel", } // NewCategoriesDao creates and returns a new DAO object for table data access. diff --git a/internal/dao/internal/user_points_logs.go b/internal/dao/internal/user_points_logs.go index e261c5d..fa6b50e 100644 --- a/internal/dao/internal/user_points_logs.go +++ b/internal/dao/internal/user_points_logs.go @@ -22,9 +22,9 @@ type UserPointsLogsDao struct { type UserPointsLogsColumns struct { Id string // 积分流水ID UserId string // 用户ID - ChangeType string // 变动类型,例如 earn、spend、refund 等 + ChangeType string // 变动类型,1=消费(spend), 2=收入(earn) PointsChange string // 积分变化数,正数增加,负数减少 - RelatedOrderId string // 关联订单ID + RelatedOrderId string // 关联ID:当change_type=1时,为chapter_purchases.id;当change_type=2时,为advertisement_records.id Description string // 变动说明 CreatedAt string // 变动时间 } diff --git a/internal/dao/internal/user_read_history.go b/internal/dao/internal/user_read_history.go new file mode 100644 index 0000000..c4fc68d --- /dev/null +++ b/internal/dao/internal/user_read_history.go @@ -0,0 +1,81 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// UserReadHistoryDao is the data access object for the table user_read_history. +type UserReadHistoryDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns UserReadHistoryColumns // columns contains all the column names of Table for convenient usage. +} + +// UserReadHistoryColumns defines and stores column names for the table user_read_history. +type UserReadHistoryColumns struct { + Id string // 历史记录ID + UserId string // 用户ID + BookId string // 小说ID + ChapterId string // 最后阅读章节ID + ReadAt string // 最后阅读时间 +} + +// userReadHistoryColumns holds the columns for the table user_read_history. +var userReadHistoryColumns = UserReadHistoryColumns{ + Id: "id", + UserId: "user_id", + BookId: "book_id", + ChapterId: "chapter_id", + ReadAt: "read_at", +} + +// NewUserReadHistoryDao creates and returns a new DAO object for table data access. +func NewUserReadHistoryDao() *UserReadHistoryDao { + return &UserReadHistoryDao{ + group: "default", + table: "user_read_history", + columns: userReadHistoryColumns, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *UserReadHistoryDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *UserReadHistoryDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *UserReadHistoryDao) Columns() UserReadHistoryColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *UserReadHistoryDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *UserReadHistoryDao) Ctx(ctx context.Context) *gdb.Model { + return dao.DB().Model(dao.table).Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *UserReadHistoryDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/read_records.go b/internal/dao/internal/user_read_records.go similarity index 51% rename from internal/dao/internal/read_records.go rename to internal/dao/internal/user_read_records.go index 226bf0e..80d9532 100644 --- a/internal/dao/internal/read_records.go +++ b/internal/dao/internal/user_read_records.go @@ -11,62 +11,68 @@ import ( "github.com/gogf/gf/v2/frame/g" ) -// ReadRecordsDao is the data access object for the table read_records. -type ReadRecordsDao struct { - table string // table is the underlying table name of the DAO. - group string // group is the database configuration group name of the current DAO. - columns ReadRecordsColumns // columns contains all the column names of Table for convenient usage. +// UserReadRecordsDao is the data access object for the table user_read_records. +type UserReadRecordsDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns UserReadRecordsColumns // columns contains all the column names of Table for convenient usage. } -// ReadRecordsColumns defines and stores column names for the table read_records. -type ReadRecordsColumns struct { +// UserReadRecordsColumns defines and stores column names for the table user_read_records. +type UserReadRecordsColumns struct { Id string // 记录ID UserId string // 用户ID BookId string // 小说ID ChapterId string // 章节ID + Progress string // 阅读进度百分比(0-100) ReadAt string // 阅读时间 + CreatedAt string // 创建时间 + UpdatedAt string // 更新时间 } -// readRecordsColumns holds the columns for the table read_records. -var readRecordsColumns = ReadRecordsColumns{ +// userReadRecordsColumns holds the columns for the table user_read_records. +var userReadRecordsColumns = UserReadRecordsColumns{ Id: "id", UserId: "user_id", BookId: "book_id", ChapterId: "chapter_id", + Progress: "progress", ReadAt: "read_at", + CreatedAt: "created_at", + UpdatedAt: "updated_at", } -// NewReadRecordsDao creates and returns a new DAO object for table data access. -func NewReadRecordsDao() *ReadRecordsDao { - return &ReadRecordsDao{ +// NewUserReadRecordsDao creates and returns a new DAO object for table data access. +func NewUserReadRecordsDao() *UserReadRecordsDao { + return &UserReadRecordsDao{ group: "default", - table: "read_records", - columns: readRecordsColumns, + table: "user_read_records", + columns: userReadRecordsColumns, } } // DB retrieves and returns the underlying raw database management object of the current DAO. -func (dao *ReadRecordsDao) DB() gdb.DB { +func (dao *UserReadRecordsDao) DB() gdb.DB { return g.DB(dao.group) } // Table returns the table name of the current DAO. -func (dao *ReadRecordsDao) Table() string { +func (dao *UserReadRecordsDao) Table() string { return dao.table } // Columns returns all column names of the current DAO. -func (dao *ReadRecordsDao) Columns() ReadRecordsColumns { +func (dao *UserReadRecordsDao) Columns() UserReadRecordsColumns { return dao.columns } // Group returns the database configuration group name of the current DAO. -func (dao *ReadRecordsDao) Group() string { +func (dao *UserReadRecordsDao) Group() string { return dao.group } // Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. -func (dao *ReadRecordsDao) Ctx(ctx context.Context) *gdb.Model { +func (dao *UserReadRecordsDao) Ctx(ctx context.Context) *gdb.Model { return dao.DB().Model(dao.table).Safe().Ctx(ctx) } @@ -76,6 +82,6 @@ func (dao *ReadRecordsDao) Ctx(ctx context.Context) *gdb.Model { // // Note: Do not commit or roll back the transaction in function f, // as it is automatically handled by this function. -func (dao *ReadRecordsDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { +func (dao *UserReadRecordsDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { return dao.Ctx(ctx).Transaction(ctx, f) } diff --git a/internal/dao/read_records.go b/internal/dao/read_records.go deleted file mode 100644 index bd38228..0000000 --- a/internal/dao/read_records.go +++ /dev/null @@ -1,27 +0,0 @@ -// ================================================================================= -// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. -// ================================================================================= - -package dao - -import ( - "server/internal/dao/internal" -) - -// internalReadRecordsDao is an internal type for wrapping the internal DAO implementation. -type internalReadRecordsDao = *internal.ReadRecordsDao - -// readRecordsDao is the data access object for the table read_records. -// You can define custom methods on it to extend its functionality as needed. -type readRecordsDao struct { - internalReadRecordsDao -} - -var ( - // ReadRecords is a globally accessible object for table read_records operations. - ReadRecords = readRecordsDao{ - internal.NewReadRecordsDao(), - } -) - -// Add your custom methods and functionality below. diff --git a/internal/dao/user_read_history.go b/internal/dao/user_read_history.go new file mode 100644 index 0000000..b7b067b --- /dev/null +++ b/internal/dao/user_read_history.go @@ -0,0 +1,27 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "server/internal/dao/internal" +) + +// internalUserReadHistoryDao is an internal type for wrapping the internal DAO implementation. +type internalUserReadHistoryDao = *internal.UserReadHistoryDao + +// userReadHistoryDao is the data access object for the table user_read_history. +// You can define custom methods on it to extend its functionality as needed. +type userReadHistoryDao struct { + internalUserReadHistoryDao +} + +var ( + // UserReadHistory is a globally accessible object for table user_read_history operations. + UserReadHistory = userReadHistoryDao{ + internal.NewUserReadHistoryDao(), + } +) + +// Add your custom methods and functionality below. diff --git a/internal/dao/user_read_records.go b/internal/dao/user_read_records.go new file mode 100644 index 0000000..64ec212 --- /dev/null +++ b/internal/dao/user_read_records.go @@ -0,0 +1,27 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "server/internal/dao/internal" +) + +// internalUserReadRecordsDao is an internal type for wrapping the internal DAO implementation. +type internalUserReadRecordsDao = *internal.UserReadRecordsDao + +// userReadRecordsDao is the data access object for the table user_read_records. +// You can define custom methods on it to extend its functionality as needed. +type userReadRecordsDao struct { + internalUserReadRecordsDao +} + +var ( + // UserReadRecords is a globally accessible object for table user_read_records operations. + UserReadRecords = userReadRecordsDao{ + internal.NewUserReadRecordsDao(), + } +) + +// Add your custom methods and functionality below. diff --git a/internal/logic/author/author.go b/internal/logic/author/author.go new file mode 100644 index 0000000..7d4b18d --- /dev/null +++ b/internal/logic/author/author.go @@ -0,0 +1,268 @@ +package author + +import ( + "context" + "server/internal/dao" + "server/internal/model" + "server/internal/model/do" + "server/internal/service" + "server/utility/ecode" + "server/utility/encrypt" + + "github.com/gogf/gf/v2/database/gdb" +) + +type sAuthor struct{} + +func New() service.IAuthor { + return &sAuthor{} +} +func init() { + service.RegisterAuthor(New()) +} + +// List retrieves a paginated list of authors +func (s *sAuthor) List(ctx context.Context, in *model.AuthorListIn) (out *model.AuthorListOut, err error) { + out = &model.AuthorListOut{} + m := dao.Authors.Ctx(ctx) + if in.PenName != "" { + m = m.Where(dao.Authors.Columns().PenName+" like ?", "%"+in.PenName+"%") + } + if in.Status != 0 { + m = m.Where(dao.Authors.Columns().Status, in.Status) + } + if err = m.Page(in.Page, in.Size).ScanAndCount(&out.List, &out.Total, false); err != nil { + return + } + return +} + +// Create adds a new author +func (s *sAuthor) Create(ctx context.Context, in *model.AuthorAddIn) (out *model.AuthorCRUDOut, err error) { + // 开启事务,确保用户和作者同时写入 + err = dao.Authors.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + // 检查该 userId 是否已存在作者 + exist, err := dao.Authors.Ctx(ctx).TX(tx). + Where(dao.Authors.Columns().UserId, in.UserId). + Exist() + if err != nil { + return ecode.Fail.Sub("author_query_failed") + } + if exist { + return ecode.Params.Sub("author_user_exists") + } + + // 检查用户是否存在 + userExist, err := dao.Users.Ctx(ctx).TX(tx). + Where(dao.Users.Columns().Id, in.UserId). + Exist() + if err != nil { + return ecode.Fail.Sub("author_query_failed") + } + if !userExist { + // 不存在则创建用户,用户名用笔名,密码默认 Aa123456 + hash, err := encrypt.EncryptPassword("Aa123456") + if err != nil { + return ecode.Fail.Sub("password_encryption_failed") + } + result, err := dao.Users.Ctx(ctx).TX(tx).Data(do.Users{ + Username: in.PenName, + PasswordHash: hash, + }).InsertAndGetId() + if err != nil { + return ecode.Fail.Sub("author_create_failed") + } + in.UserId = result + } + + // 创建作者 + _, err = dao.Authors.Ctx(ctx).TX(tx).Data(do.Authors{ + UserId: in.UserId, + PenName: in.PenName, + Bio: in.Bio, + Status: in.Status, + }).Insert() + if err != nil { + return ecode.Fail.Sub("author_create_failed") + } + return nil + }) + if err != nil { + return nil, err + } + return &model.AuthorCRUDOut{Success: true}, nil +} + +// Update edits an author +func (s *sAuthor) Update(ctx context.Context, in *model.AuthorEditIn) (out *model.AuthorCRUDOut, err error) { + exist, err := dao.Authors.Ctx(ctx). + WherePri(in.Id). + Exist() + if err != nil { + return nil, ecode.Fail.Sub("author_query_failed") + } + if !exist { + return nil, ecode.NotFound.Sub("author_not_found") + } + _, err = dao.Authors.Ctx(ctx). + WherePri(in.Id). + Data(do.Authors{ + PenName: in.PenName, + Bio: in.Bio, + Status: in.Status, + }).Update() + if err != nil { + return nil, ecode.Fail.Sub("author_update_failed") + } + return &model.AuthorCRUDOut{Success: true}, nil +} + +// Delete removes an author by id +func (s *sAuthor) Delete(ctx context.Context, in *model.AuthorDelIn) (out *model.AuthorCRUDOut, err error) { + exist, err := dao.Authors.Ctx(ctx). + WherePri(in.Id). + Exist() + if err != nil { + return nil, ecode.Fail.Sub("author_query_failed") + } + if !exist { + return nil, ecode.NotFound.Sub("author_not_found") + } + + // 开启事务,删除作者及相关数据 + err = dao.Authors.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + // 1. 删除作者相关的用户关注记录 + _, err := dao.UserFollowAuthors.Ctx(ctx).TX(tx). + Where(dao.UserFollowAuthors.Columns().AuthorId, in.Id). + Delete() + if err != nil { + return ecode.Fail.Sub("follow_author_delete_failed") + } + + // 2. 删除作者相关的书籍(这里需要递归删除书籍相关的所有数据) + // 先查询该作者的所有书籍ID + var bookIds []int64 + err = dao.Books.Ctx(ctx).TX(tx). + Where(dao.Books.Columns().AuthorId, in.Id). + Fields("id"). + Scan(&bookIds) + if err != nil { + return ecode.Fail.Sub("book_query_failed") + } + + if len(bookIds) > 0 { + // 删除书籍相关的章节 + _, err = dao.Chapters.Ctx(ctx).TX(tx). + WhereIn(dao.Chapters.Columns().BookId, bookIds). + Delete() + if err != nil { + return ecode.Fail.Sub("chapter_delete_failed") + } + + // 删除书籍相关的用户阅读记录 + _, err = dao.UserReadRecords.Ctx(ctx).TX(tx). + WhereIn(dao.UserReadRecords.Columns().BookId, bookIds). + Delete() + if err != nil { + return ecode.Fail.Sub("read_record_delete_failed") + } + + // 删除书籍相关的用户阅读历史 + _, err = dao.UserReadHistory.Ctx(ctx).TX(tx). + WhereIn(dao.UserReadHistory.Columns().BookId, bookIds). + Delete() + if err != nil { + return ecode.Fail.Sub("history_delete_failed") + } + + // 删除书籍相关的用户书架 + _, err = dao.Bookshelves.Ctx(ctx).TX(tx). + WhereIn(dao.Bookshelves.Columns().BookId, bookIds). + Delete() + if err != nil { + return ecode.Fail.Sub("bookshelf_delete_failed") + } + + // 删除书籍相关的用户评分 + _, err = dao.BookRatings.Ctx(ctx).TX(tx). + WhereIn(dao.BookRatings.Columns().BookId, bookIds). + Delete() + if err != nil { + return ecode.Fail.Sub("rating_delete_failed") + } + + // 删除书籍相关的章节购买记录 + _, err = dao.UserChapterPurchases.Ctx(ctx).TX(tx). + WhereIn(dao.UserChapterPurchases.Columns().BookId, bookIds). + Delete() + if err != nil { + return ecode.Fail.Sub("purchase_delete_failed") + } + + // 最后删除书籍 + _, err = dao.Books.Ctx(ctx).TX(tx). + Where(dao.Books.Columns().AuthorId, in.Id). + Delete() + if err != nil { + return ecode.Fail.Sub("book_delete_failed") + } + } + + // 3. 最后删除作者 + _, err = dao.Authors.Ctx(ctx).TX(tx).WherePri(in.Id).Delete() + if err != nil { + return ecode.Fail.Sub("author_delete_failed") + } + + return nil + }) + + if err != nil { + return nil, err + } + + return &model.AuthorCRUDOut{Success: true}, nil +} + +// Apply 允许用户申请成为作者 +func (s *sAuthor) Apply(ctx context.Context, in *model.AuthorApplyIn) (out *model.AuthorApplyOut, err error) { + userIdVal := ctx.Value("id") + userId, ok := userIdVal.(int64) + if !ok || userId == 0 { + return nil, ecode.Fail.Sub("user_id_invalid") + } + exist, err := dao.Authors.Ctx(ctx). + Where(dao.Authors.Columns().UserId, userId). + Exist() + if err != nil { + return nil, ecode.Fail.Sub("author_query_failed") + } + if exist { + return nil, ecode.Params.Sub("author_user_exists") + } + if _, err := dao.Authors.Ctx(ctx).Data(do.Authors{ + UserId: userId, + PenName: in.PenName, + Bio: in.Bio, + Status: 1, // 默认正常 + }).Insert(); err != nil { + return nil, ecode.Fail.Sub("author_create_failed") + } + return &model.AuthorApplyOut{Success: true}, nil +} +func (s *sAuthor) Detail(ctx context.Context, in *model.AuthorDetailIn) (out *model.AuthorDetailOut, err error) { + out = &model.AuthorDetailOut{} + exist, err := dao.Authors.Ctx(ctx). + WherePri(in.AuthorId). + Exist() + if err != nil { + return nil, ecode.Fail.Sub("author_query_failed") + } + if !exist { + return nil, ecode.NotFound.Sub("author_not_found") + } + if err = dao.Authors.Ctx(ctx).WherePri(in.AuthorId).WithAll().Scan(&out); err != nil { + return nil, ecode.Fail.Sub("author_query_failed") + } + return out, nil +} diff --git a/internal/logic/book/book.go b/internal/logic/book/book.go index 4cf6771..e646f55 100644 --- a/internal/logic/book/book.go +++ b/internal/logic/book/book.go @@ -7,6 +7,8 @@ import ( "server/internal/model/do" "server/internal/service" "server/utility/ecode" + + "github.com/gogf/gf/v2/database/gdb" ) type sBook struct{} @@ -38,7 +40,10 @@ func (s *sBook) List(ctx context.Context, in *model.BookListIn) (out *model.Book if in.IsRecommended != 0 { m = m.Where(dao.Books.Columns().IsRecommended, in.IsRecommended) } - if err = m.Page(in.Page, in.Size).ScanAndCount(&out.List, &out.Total, false); err != nil { + if in.Sort != "" { + m = m.Order(in.Sort) + } + if err = m.Page(in.Page, in.Size).WithAll().ScanAndCount(&out.List, &out.Total, false); err != nil { return } return @@ -127,12 +132,633 @@ func (s *sBook) Delete(ctx context.Context, in *model.BookDelIn) (out *model.Boo return nil, ecode.NotFound.Sub("book_not_found") } - _, err = dao.Books.Ctx(ctx).WherePri(in.Id).Delete() + // 开启事务,删除书籍及相关数据 + err = dao.Books.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + // 1. 删除书籍相关的章节 + _, err := dao.Chapters.Ctx(ctx).TX(tx). + Where(dao.Chapters.Columns().BookId, in.Id). + Delete() + if err != nil { + return ecode.Fail.Sub("chapter_delete_failed") + } + + // 2. 删除书籍相关的用户阅读记录 + _, err = dao.UserReadRecords.Ctx(ctx).TX(tx). + Where(dao.UserReadRecords.Columns().BookId, in.Id). + Delete() + if err != nil { + return ecode.Fail.Sub("read_record_delete_failed") + } + + // 3. 删除书籍相关的用户阅读历史 + _, err = dao.UserReadHistory.Ctx(ctx).TX(tx). + Where(dao.UserReadHistory.Columns().BookId, in.Id). + Delete() + if err != nil { + return ecode.Fail.Sub("history_delete_failed") + } + + // 4. 删除书籍相关的用户书架 + _, err = dao.Bookshelves.Ctx(ctx).TX(tx). + Where(dao.Bookshelves.Columns().BookId, in.Id). + Delete() + if err != nil { + return ecode.Fail.Sub("bookshelf_delete_failed") + } + + // 5. 删除书籍相关的用户评分 + _, err = dao.BookRatings.Ctx(ctx).TX(tx). + Where(dao.BookRatings.Columns().BookId, in.Id). + Delete() + if err != nil { + return ecode.Fail.Sub("rating_delete_failed") + } + + // 6. 删除书籍相关的章节购买记录 + _, err = dao.UserChapterPurchases.Ctx(ctx).TX(tx). + Where(dao.UserChapterPurchases.Columns().BookId, in.Id). + Delete() + if err != nil { + return ecode.Fail.Sub("purchase_delete_failed") + } + + // 7. 最后删除书籍 + _, err = dao.Books.Ctx(ctx).TX(tx).WherePri(in.Id).Delete() + if err != nil { + return ecode.Fail.Sub("book_delete_failed") + } + + return nil + }) + if err != nil { - return nil, ecode.Fail.Sub("book_delete_failed") + return nil, err } return &model.BookCRUDOut{ Success: true, }, nil } + +// AppList retrieves book list for app +func (s *sBook) AppList(ctx context.Context, in *model.BookAppListIn) (out *model.BookAppListOut, err error) { + out = &model.BookAppListOut{} + + // 构建查询条件,使用WithAll查询关联的作者信息 + m := dao.Books.Ctx(ctx).WithAll() + + // 书名模糊搜索 + if in.Title != "" { + m = m.Where(dao.Books.Columns().Title+" like ?", "%"+in.Title+"%") + } + + // 分类筛选 + if in.CategoryId != 0 { + m = m.Where(dao.Books.Columns().CategoryId, in.CategoryId) + } + + // 推荐筛选 + if in.IsRecommended { + m = m.Where(dao.Books.Columns().IsRecommended, 1) + } + + // 最新筛选(按创建时间倒序) + if in.IsLatest == 1 { + m = m.OrderDesc(dao.Books.Columns().CreatedAt) + } + if in.AuthorId != 0 { + m = m.Where(dao.Books.Columns().AuthorId, in.AuthorId) + } + if in.IsFeatured { + m = m.Where(dao.Books.Columns().IsFeatured, 1) + } + if in.Sort != "" { + m = m.Order(in.Sort) + } + + // 执行分页查询 + if err = m.Page(in.Page, in.Size).ScanAndCount(&out.List, &out.Total, false); err != nil { + return nil, ecode.Fail.Sub("book_query_failed") + } + + // 用户评分批量查询 + if in.UserId > 0 && len(out.List) > 0 { + bookIds := make([]int64, 0, len(out.List)) + for _, item := range out.List { + bookIds = append(bookIds, item.Id) + } + // 查询用户对这些书的评分 + type ratingRow struct { + BookId int64 `json:"bookId"` + Score float64 `json:"score"` + } + ratings := make([]ratingRow, 0) + err = dao.BookRatings.Ctx(ctx). + Fields("book_id, score"). + Where(dao.BookRatings.Columns().UserId, in.UserId). + WhereIn(dao.BookRatings.Columns().BookId, bookIds). + Scan(&ratings) + if err == nil && len(ratings) > 0 { + ratingMap := make(map[int64]float64, len(ratings)) + for _, r := range ratings { + ratingMap[r.BookId] = r.Score + } + for i := range out.List { + if score, ok := ratingMap[out.List[i].Id]; ok { + out.List[i].HasRated = true + out.List[i].MyRating = score + } else { + out.List[i].HasRated = false + out.List[i].MyRating = 0 + } + } + } else { + for i := range out.List { + out.List[i].HasRated = false + out.List[i].MyRating = 0 + } + } + } + + return out, nil +} + +// AppRate rates a book for app +func (s *sBook) AppRate(ctx context.Context, in *model.BookAppRateIn) (out *model.BookAppRateOut, err error) { + out = &model.BookAppRateOut{} + + // 必须指定用户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") + } + + // 验证评分范围(1-10分) + if in.Rating < 1 || in.Rating > 10 { + return nil, ecode.Fail.Sub("rating_invalid") + } + + // 检查书籍是否存在 + exist, err := dao.Books.Ctx(ctx).Where(dao.Books.Columns().Id, in.BookId).Exist() + if err != nil { + return nil, ecode.Fail.Sub("book_query_failed") + } + if !exist { + return nil, ecode.NotFound.Sub("book_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.BookRatings.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + // 检查是否已经评分过 + exist, err := dao.BookRatings.Ctx(ctx).TX(tx). + Where(dao.BookRatings.Columns().UserId, in.UserId). + Where(dao.BookRatings.Columns().BookId, in.BookId). + Exist() + if err != nil { + return ecode.Fail.Sub("rating_query_failed") + } + + if exist { + // 更新现有评分 + _, err = dao.BookRatings.Ctx(ctx).TX(tx). + Where(dao.BookRatings.Columns().UserId, in.UserId). + Where(dao.BookRatings.Columns().BookId, in.BookId). + Data(do.BookRatings{ + Score: in.Rating, + }).Update() + if err != nil { + return ecode.Fail.Sub("rating_update_failed") + } + } else { + // 创建新评分记录 + _, err = dao.BookRatings.Ctx(ctx).TX(tx).Data(do.BookRatings{ + UserId: in.UserId, + BookId: in.BookId, + Score: in.Rating, + }).Insert() + if err != nil { + return ecode.Fail.Sub("rating_create_failed") + } + } + + // 重新计算书籍平均评分 + var avgRating float64 + err = dao.BookRatings.Ctx(ctx).TX(tx). + Where(dao.BookRatings.Columns().BookId, in.BookId). + Fields("AVG(score) as avg_rating"). + Scan(&avgRating) + if err != nil { + return ecode.Fail.Sub("rating_calculation_failed") + } + + // 更新书籍的平均评分 + _, err = dao.Books.Ctx(ctx).TX(tx). + Where(dao.Books.Columns().Id, in.BookId). + Data(do.Books{ + Rating: avgRating, + }).Update() + if err != nil { + return ecode.Fail.Sub("book_rating_update_failed") + } + + return nil + }); err != nil { + return nil, err + } + + out.Success = true + return out, nil +} + +// AppDetail retrieves book detail for app +func (s *sBook) AppDetail(ctx context.Context, in *model.BookAppDetailIn) (out *model.BookAppDetailOut, err error) { + out = &model.BookAppDetailOut{} + + // 必须指定书籍ID + if in.Id == 0 { + return nil, ecode.Fail.Sub("book_id_required") + } + + // 构建查询条件 + m := dao.Books.Ctx(ctx) + m = m.Where(dao.Books.Columns().Id, in.Id) + + // 执行查询 + if err = m.Scan(out); err != nil { + return nil, ecode.Fail.Sub("book_query_failed") + } + + // 检查书籍是否存在 + if out.Id == 0 { + return nil, ecode.NotFound.Sub("book_not_found") + } + + // 如果用户已登录,查询阅读进度 + if in.UserId > 0 { + // 查询用户对该书籍的历史记录 + var historyRecord struct { + ChapterId int64 `json:"chapterId"` + ReadAt string `json:"readAt"` + } + err = dao.UserReadHistory.Ctx(ctx). + Fields("chapter_id, read_at"). + Where(dao.UserReadHistory.Columns().UserId, in.UserId). + Where(dao.UserReadHistory.Columns().BookId, in.Id). + OrderDesc(dao.UserReadHistory.Columns().ReadAt). + Scan(&historyRecord) + + if err == nil && historyRecord.ChapterId > 0 { + // 用户读过这本书 + out.HasRead = true + out.LastChapterId = historyRecord.ChapterId + out.LastReadAt = historyRecord.ReadAt + + // 计算阅读进度:总章节数 vs 已读章节数 + totalChapters, err := dao.Chapters.Ctx(ctx). + Where(dao.Chapters.Columns().BookId, in.Id). + Count() + + if err == nil && totalChapters > 0 { + readChapters, err := dao.UserReadRecords.Ctx(ctx). + Where(dao.UserReadRecords.Columns().UserId, in.UserId). + Where(dao.UserReadRecords.Columns().BookId, in.Id). + Where(dao.UserReadRecords.Columns().ChapterId, ">", 0). // 只统计有章节ID的记录 + Count() + + if err == nil { + out.ReadProgress = int(float64(readChapters) / float64(totalChapters) * 100) + if out.ReadProgress > 100 { + out.ReadProgress = 100 + } + } + } + } + } + + return out, nil +} + +func (s *sBook) MyList(ctx context.Context, in *model.MyBookListIn) (out *model.MyBookListOut, err error) { + out = &model.MyBookListOut{} + + // 验证用户ID + if in.UserId == 0 { + return nil, ecode.Fail.Sub("user_id_required") + } + + // 验证类型参数 + if in.Type < 1 || in.Type > 3 { + return nil, ecode.Fail.Sub("type_invalid") + } + + var list []model.MyBookItem + var total int + + switch in.Type { + case 1: // 正在读 + // 查询书架表中read_status=1的记录 + var bookshelves []struct { + BookId int64 `json:"bookId"` + LastReadPercent float64 `json:"lastReadPercent"` + LastReadAt string `json:"lastReadAt"` + } + + if in.Sort != "" { + err = dao.Bookshelves.Ctx(ctx). + Fields("book_id, last_read_percent, last_read_at"). + Where(dao.Bookshelves.Columns().UserId, in.UserId). + Where(dao.Bookshelves.Columns().ReadStatus, 1). + Order(in.Sort). + Page(in.Page, in.Size). + ScanAndCount(&bookshelves, &total, false) + } else { + err = dao.Bookshelves.Ctx(ctx). + Fields("book_id, last_read_percent, last_read_at"). + Where(dao.Bookshelves.Columns().UserId, in.UserId). + Where(dao.Bookshelves.Columns().ReadStatus, 1). + Page(in.Page, in.Size). + ScanAndCount(&bookshelves, &total, false) + } + if err != nil { + return nil, ecode.Fail.Sub("bookshelf_query_failed") + } + + // 获取书籍详细信息 + if len(bookshelves) > 0 { + bookIds := make([]int64, 0, len(bookshelves)) + for _, bs := range bookshelves { + bookIds = append(bookIds, bs.BookId) + } + + var books []model.Book + err = dao.Books.Ctx(ctx). + WhereIn(dao.Books.Columns().Id, bookIds). + Scan(&books) + if err != nil { + return nil, ecode.Fail.Sub("book_query_failed") + } + + // 构建书籍ID到书架信息的映射 + bookshelfMap := make(map[int64]struct { + LastReadPercent float64 + LastReadAt string + }) + for _, bs := range bookshelves { + bookshelfMap[bs.BookId] = struct { + LastReadPercent float64 + LastReadAt string + }{ + LastReadPercent: bs.LastReadPercent, + LastReadAt: bs.LastReadAt, + } + } + + // 构建返回列表 + for _, book := range books { + bsInfo := bookshelfMap[book.Id] + list = append(list, model.MyBookItem{ + Id: book.Id, + Title: book.Title, + CoverUrl: book.CoverUrl, + Description: book.Description, + Progress: int(bsInfo.LastReadPercent), + IsInShelf: true, + LastReadAt: bsInfo.LastReadAt, + Status: book.Status, + AuthorId: book.AuthorId, + CategoryId: book.CategoryId, + }) + } + } + + case 2: // 已读完 + // 查询书架表中read_status=2的记录 + var bookshelves []struct { + BookId int64 `json:"bookId"` + LastReadPercent float64 `json:"lastReadPercent"` + LastReadAt string `json:"lastReadAt"` + } + + if in.Sort != "" { + err = dao.Bookshelves.Ctx(ctx). + Fields("book_id, last_read_percent, last_read_at"). + Where(dao.Bookshelves.Columns().UserId, in.UserId). + Where(dao.Bookshelves.Columns().ReadStatus, 2). + Order(in.Sort). + Page(in.Page, in.Size). + ScanAndCount(&bookshelves, &total, false) + } else { + err = dao.Bookshelves.Ctx(ctx). + Fields("book_id, last_read_percent, last_read_at"). + Where(dao.Bookshelves.Columns().UserId, in.UserId). + Where(dao.Bookshelves.Columns().ReadStatus, 2). + Page(in.Page, in.Size). + ScanAndCount(&bookshelves, &total, false) + } + if err != nil { + return nil, ecode.Fail.Sub("bookshelf_query_failed") + } + + // 获取书籍详细信息 + if len(bookshelves) > 0 { + bookIds := make([]int64, 0, len(bookshelves)) + for _, bs := range bookshelves { + bookIds = append(bookIds, bs.BookId) + } + + var books []model.Book + err = dao.Books.Ctx(ctx). + WhereIn(dao.Books.Columns().Id, bookIds). + Scan(&books) + if err != nil { + return nil, ecode.Fail.Sub("book_query_failed") + } + + // 构建书籍ID到书架信息的映射 + bookshelfMap := make(map[int64]struct { + LastReadPercent float64 + LastReadAt string + }) + for _, bs := range bookshelves { + bookshelfMap[bs.BookId] = struct { + LastReadPercent float64 + LastReadAt string + }{ + LastReadPercent: bs.LastReadPercent, + LastReadAt: bs.LastReadAt, + } + } + + // 构建返回列表 + for _, book := range books { + bsInfo := bookshelfMap[book.Id] + list = append(list, model.MyBookItem{ + Id: book.Id, + Title: book.Title, + CoverUrl: book.CoverUrl, + Description: book.Description, + Progress: int(bsInfo.LastReadPercent), + IsInShelf: true, + LastReadAt: bsInfo.LastReadAt, + Status: book.Status, + AuthorId: book.AuthorId, + CategoryId: book.CategoryId, + }) + } + } + + case 3: // 历史记录 + // 查询user_read_history表 + var histories []struct { + BookId int64 `json:"bookId"` + ChapterId int64 `json:"chapterId"` + ReadAt string `json:"readAt"` + } + + if in.Sort != "" { + err = dao.UserReadHistory.Ctx(ctx). + Fields("book_id, chapter_id, read_at"). + Where(dao.UserReadHistory.Columns().UserId, in.UserId). + Order(in.Sort). + Page(in.Page, in.Size). + ScanAndCount(&histories, &total, false) + } else { + err = dao.UserReadHistory.Ctx(ctx). + Fields("book_id, chapter_id, read_at"). + Where(dao.UserReadHistory.Columns().UserId, in.UserId). + OrderDesc(dao.UserReadHistory.Columns().ReadAt). + Page(in.Page, in.Size). + ScanAndCount(&histories, &total, false) + } + if err != nil { + return nil, ecode.Fail.Sub("history_query_failed") + } + + // 获取书籍详细信息 + if len(histories) > 0 { + bookIds := make([]int64, 0, len(histories)) + for _, history := range histories { + bookIds = append(bookIds, history.BookId) + } + + var books []model.Book + err = dao.Books.Ctx(ctx). + WhereIn(dao.Books.Columns().Id, bookIds). + Scan(&books) + if err != nil { + return nil, ecode.Fail.Sub("book_query_failed") + } + + // 构建书籍ID到历史信息的映射 + historyMap := make(map[int64]struct { + ChapterId int64 + ReadAt string + }) + for _, history := range histories { + historyMap[history.BookId] = struct { + ChapterId int64 + ReadAt string + }{ + ChapterId: history.ChapterId, + ReadAt: history.ReadAt, + } + } + + // 检查是否在书架中 + var bookshelves []struct { + BookId int64 `json:"bookId"` + } + err = dao.Bookshelves.Ctx(ctx). + Fields("book_id"). + Where(dao.Bookshelves.Columns().UserId, in.UserId). + WhereIn(dao.Bookshelves.Columns().BookId, bookIds). + Scan(&bookshelves) + if err != nil { + return nil, ecode.Fail.Sub("bookshelf_query_failed") + } + + // 构建书架ID集合 + shelfBookIds := make(map[int64]bool) + for _, bs := range bookshelves { + shelfBookIds[bs.BookId] = true + } + + // 构建返回列表 + for _, book := range books { + historyInfo := historyMap[book.Id] + list = append(list, model.MyBookItem{ + Id: book.Id, + Title: book.Title, + CoverUrl: book.CoverUrl, + Description: book.Description, + Progress: 0, // 历史记录不显示进度 + IsInShelf: shelfBookIds[book.Id], + LastReadAt: historyInfo.ReadAt, + Status: book.Status, + AuthorId: book.AuthorId, + CategoryId: book.CategoryId, + }) + } + } + + default: + // 返回空列表 + } + + out = &model.MyBookListOut{ + Total: total, + List: list, + } + return +} + +// SetFeatured: 单独修改书籍的精选状态 +func (s *sBook) SetFeatured(ctx context.Context, in *model.BookSetFeaturedIn) (out *model.BookCRUDOut, err error) { + // 检查书籍是否存在 + exist, err := dao.Books.Ctx(ctx).WherePri(in.Id).Exist() + if err != nil { + return nil, ecode.Fail.Sub("book_query_failed") + } + if !exist { + return nil, ecode.NotFound.Sub("book_not_found") + } + _, err = dao.Books.Ctx(ctx).WherePri(in.Id).Data(do.Books{ + IsFeatured: in.IsFeatured, + }).Update() + if err != nil { + return nil, ecode.Fail.Sub("book_update_failed") + } + return &model.BookCRUDOut{Success: true}, nil +} + +// SetRecommended: 单独修改书籍的推荐状态 +func (s *sBook) SetRecommended(ctx context.Context, in *model.BookSetRecommendedIn) (out *model.BookCRUDOut, err error) { + // 检查书籍是否存在 + exist, err := dao.Books.Ctx(ctx).WherePri(in.Id).Exist() + if err != nil { + return nil, ecode.Fail.Sub("book_query_failed") + } + if !exist { + return nil, ecode.NotFound.Sub("book_not_found") + } + _, err = dao.Books.Ctx(ctx).WherePri(in.Id).Data(do.Books{ + IsRecommended: in.IsRecommended, + }).Update() + if err != nil { + return nil, ecode.Fail.Sub("book_update_failed") + } + return &model.BookCRUDOut{Success: true}, nil +} diff --git a/internal/logic/bookshelve/bookshelve.go b/internal/logic/bookshelve/bookshelve.go new file mode 100644 index 0000000..3424710 --- /dev/null +++ b/internal/logic/bookshelve/bookshelve.go @@ -0,0 +1,57 @@ +package bookshelve + +import ( + "context" + "server/internal/dao" + "server/internal/model" + "server/internal/model/do" + "server/internal/service" + "server/utility/ecode" +) + +type sBookshelve struct{} + +func New() service.IBookshelve { + return &sBookshelve{} +} + +func init() { + service.RegisterBookshelve(New()) +} + +// Add 添加书架 +func (s *sBookshelve) Add(ctx context.Context, in *model.BookshelveAddIn) (out *model.BookshelveCRUDOut, err error) { + exist, err := dao.Bookshelves.Ctx(ctx). + Where(dao.Bookshelves.Columns().UserId, in.UserId). + Where(dao.Bookshelves.Columns().BookId, in.BookId). + Exist() + if err != nil { + return nil, ecode.Fail.Sub("bookshelve_query_failed") + } + if exist { + return nil, ecode.Params.Sub("bookshelve_exists") + } + if _, err := dao.Bookshelves.Ctx(ctx).Data(do.Bookshelves{ + UserId: in.UserId, + BookId: in.BookId, + ReadStatus: 1, // 默认为正在读 + }).Insert(); err != nil { + return nil, ecode.Fail.Sub("bookshelve_create_failed") + } + return &model.BookshelveCRUDOut{Success: true}, nil +} + +// Delete 批量删除书架 +func (s *sBookshelve) Delete(ctx context.Context, in *model.BookshelveDelIn) (out *model.BookshelveCRUDOut, err error) { + if len(in.BookIds) == 0 { + return nil, ecode.Params.Sub("bookshelve_bookids_empty") + } + _, err = dao.Bookshelves.Ctx(ctx). + Where(dao.Bookshelves.Columns().UserId, in.UserId). + WhereIn(dao.Bookshelves.Columns().BookId, in.BookIds). + Delete() + if err != nil { + return nil, ecode.Fail.Sub("bookshelve_delete_failed") + } + return &model.BookshelveCRUDOut{Success: true}, nil +} diff --git a/internal/logic/category/category.go b/internal/logic/category/category.go index b47835f..0a64f6a 100644 --- a/internal/logic/category/category.go +++ b/internal/logic/category/category.go @@ -7,6 +7,8 @@ import ( "server/internal/model/do" "server/internal/service" "server/utility/ecode" + + "github.com/gogf/gf/v2/database/gdb" ) type sCategory struct { @@ -24,8 +26,8 @@ func init() { func (s *sCategory) List(ctx context.Context, in *model.CategoryListIn) (out *model.CategoryListOut, err error) { out = &model.CategoryListOut{} m := dao.Categories.Ctx(ctx) - if in.Type != 0 { - m = m.Where(dao.Categories.Columns().Type, in.Type) + if in.Channel != 0 { + m = m.Where(dao.Categories.Columns().Channel, in.Channel) } if err = m.Page(in.Page, in.Size).ScanAndCount(&out.List, &out.Total, false); err != nil { return @@ -34,9 +36,6 @@ func (s *sCategory) List(ctx context.Context, in *model.CategoryListIn) (out *mo } func (s *sCategory) Create(ctx context.Context, in *model.CategoryAddIn) (out *model.CategoryCRUDOut, err error) { - if in.Type != 1 && in.Type != 2 { - return nil, ecode.Params.Sub("category_type_invalid") - } exist, err := dao.Categories.Ctx(ctx). Where(dao.Categories.Columns().Name, in.Name). Exist() @@ -48,8 +47,8 @@ func (s *sCategory) Create(ctx context.Context, in *model.CategoryAddIn) (out *m } if _, err := dao.Categories.Ctx(ctx).Data(do.Categories{ - Name: in.Name, - Type: in.Type, + Name: in.Name, + Channel: in.Channel, }).Insert(); err != nil { return nil, ecode.Fail.Sub("category_create_failed") } @@ -60,9 +59,6 @@ func (s *sCategory) Create(ctx context.Context, in *model.CategoryAddIn) (out *m } func (s *sCategory) Update(ctx context.Context, in *model.CategoryEditIn) (out *model.CategoryCRUDOut, err error) { - if in.Type != 1 && in.Type != 2 { - return nil, ecode.Params.Sub("category_type_invalid") - } exist, err := dao.Categories.Ctx(ctx). WherePri(in.Id). Exist() @@ -88,8 +84,8 @@ func (s *sCategory) Update(ctx context.Context, in *model.CategoryEditIn) (out * _, err = dao.Categories.Ctx(ctx). WherePri(in.Id). Data(do.Categories{ - Name: in.Name, - Type: in.Type, + Name: in.Name, + Channel: in.Channel, }).Update() if err != nil { return nil, ecode.Fail.Sub("category_update_failed") @@ -111,10 +107,31 @@ func (s *sCategory) Delete(ctx context.Context, in *model.CategoryDelIn) (out *m return nil, ecode.NotFound.Sub("category_not_found") } - // Soft delete category - _, err = dao.Categories.Ctx(ctx).WherePri(in.Id).Delete() + // 开启事务,检查是否有书籍使用了这个分类 + err = dao.Categories.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + // 检查是否有书籍使用了这个分类 + bookCount, err := dao.Books.Ctx(ctx).TX(tx). + Where(dao.Books.Columns().CategoryId, in.Id). + Count() + if err != nil { + return ecode.Fail.Sub("book_query_failed") + } + + if bookCount > 0 { + return ecode.Fail.Sub("category_in_use") + } + + // 删除分类 + _, err = dao.Categories.Ctx(ctx).TX(tx).WherePri(in.Id).Delete() + if err != nil { + return ecode.Fail.Sub("category_delete_failed") + } + + return nil + }) + if err != nil { - return nil, ecode.Fail.Sub("category_delete_failed") + return nil, err } return &model.CategoryCRUDOut{ diff --git a/internal/logic/chapter/chapter.go b/internal/logic/chapter/chapter.go index cb2232f..ceccf96 100644 --- a/internal/logic/chapter/chapter.go +++ b/internal/logic/chapter/chapter.go @@ -5,8 +5,12 @@ import ( "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{} @@ -38,6 +42,7 @@ func (s *sChapter) List(ctx context.Context, in *model.ChapterListIn) (out *mode 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, @@ -53,6 +58,7 @@ func (s *sChapter) Create(ctx context.Context, in *model.ChapterAddIn) (out *mod 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). @@ -80,6 +86,7 @@ func (s *sChapter) Update(ctx context.Context, in *model.ChapterEditIn) (out *mo 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). @@ -90,9 +97,431 @@ func (s *sChapter) Delete(ctx context.Context, in *model.ChapterDelIn) (out *mod if !exist { return nil, ecode.NotFound.Sub("chapter_not_found") } - _, err = dao.Chapters.Ctx(ctx).WherePri(in.Id).Delete() + + // 开启事务,删除章节及相关数据 + 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, ecode.Fail.Sub("chapter_delete_failed") + 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 +} diff --git a/internal/logic/feedback/feedback.go b/internal/logic/feedback/feedback.go index 2f497c9..0978d51 100644 --- a/internal/logic/feedback/feedback.go +++ b/internal/logic/feedback/feedback.go @@ -28,7 +28,7 @@ func (s *sFeedback) List(ctx context.Context, in *model.FeedbackListIn) (out *mo if in.Status != 0 { m = m.Where(dao.Feedbacks.Columns().Status, in.Status) } - if err = m.Page(in.Page, in.Size).ScanAndCount(&out.List, &out.Total, false); err != nil { + if err = m.Page(in.Page, in.Size).WithAll().ScanAndCount(&out.List, &out.Total, false); err != nil { return } return diff --git a/internal/logic/logic.go b/internal/logic/logic.go index 781520c..fac6965 100644 --- a/internal/logic/logic.go +++ b/internal/logic/logic.go @@ -6,11 +6,13 @@ package logic import ( _ "server/internal/logic/admin" + _ "server/internal/logic/author" _ "server/internal/logic/book" + _ "server/internal/logic/bookshelve" _ "server/internal/logic/category" _ "server/internal/logic/chapter" _ "server/internal/logic/feedback" - _ "server/internal/logic/read_record" - _ "server/internal/logic/tag" _ "server/internal/logic/user" + _ "server/internal/logic/user_follow_author" + _ "server/internal/logic/user_read_record" ) diff --git a/internal/logic/read_record/read_record.go b/internal/logic/read_record/read_record.go deleted file mode 100644 index 78479f1..0000000 --- a/internal/logic/read_record/read_record.go +++ /dev/null @@ -1,68 +0,0 @@ -package read_record - -import ( - "context" - "server/internal/dao" - "server/internal/model" - "server/internal/model/do" - "server/internal/service" - "server/utility/ecode" -) - -type sReadRecord struct{} - -func New() service.IReadRecord { - return &sReadRecord{} -} -func init() { - service.RegisterReadRecord(New()) -} - -// List retrieves a paginated list of read records -func (s *sReadRecord) List(ctx context.Context, in *model.ReadRecordListIn) (out *model.ReadRecordListOut, err error) { - out = &model.ReadRecordListOut{} - m := dao.ReadRecords.Ctx(ctx) - if in.UserId != 0 { - m = m.Where(dao.ReadRecords.Columns().UserId, in.UserId) - } - if in.BookId != 0 { - m = m.Where(dao.ReadRecords.Columns().BookId, in.BookId) - } - if in.ChapterId != 0 { - m = m.Where(dao.ReadRecords.Columns().ChapterId, in.ChapterId) - } - if err = m.Page(in.Page, in.Size).ScanAndCount(&out.List, &out.Total, false); err != nil { - return - } - return -} - -// Create adds a new read record -func (s *sReadRecord) Create(ctx context.Context, in *model.ReadRecordAddIn) (out *model.ReadRecordCRUDOut, err error) { - if _, err := dao.ReadRecords.Ctx(ctx).Data(do.ReadRecords{ - UserId: in.UserId, - BookId: in.BookId, - ChapterId: in.ChapterId, - }).Insert(); err != nil { - return nil, ecode.Fail.Sub("read_record_create_failed") - } - return &model.ReadRecordCRUDOut{Success: true}, nil -} - -// Delete removes a read record by id -func (s *sReadRecord) Delete(ctx context.Context, in *model.ReadRecordDelIn) (out *model.ReadRecordCRUDOut, err error) { - exist, err := dao.ReadRecords.Ctx(ctx). - WherePri(in.Id). - Exist() - if err != nil { - return nil, ecode.Fail.Sub("read_record_query_failed") - } - if !exist { - return nil, ecode.NotFound.Sub("read_record_not_found") - } - _, err = dao.ReadRecords.Ctx(ctx).WherePri(in.Id).Delete() - if err != nil { - return nil, ecode.Fail.Sub("read_record_delete_failed") - } - return &model.ReadRecordCRUDOut{Success: true}, nil -} diff --git a/internal/logic/tag/tag.go b/internal/logic/tag/tag.go deleted file mode 100644 index 9390e39..0000000 --- a/internal/logic/tag/tag.go +++ /dev/null @@ -1,119 +0,0 @@ -package tag - -import ( - "context" - "server/internal/dao" - "server/internal/model" - "server/internal/model/do" - "server/internal/service" - "server/utility/ecode" -) - -type sTag struct{} - -func New() service.ITag { - return &sTag{} -} - -func init() { - service.RegisterTag(New()) -} - -// List retrieves a paginated list of tags -func (s *sTag) List(ctx context.Context, in *model.TagListIn) (out *model.TagListOut, err error) { - out = &model.TagListOut{} - m := dao.Tags.Ctx(ctx) - if in.Name != "" { - m = m.Where(dao.Tags.Columns().Name+" like ?", "%"+in.Name+"%") - } - if in.Type != 0 { - m = m.Where(dao.Tags.Columns().Type, in.Type) - } - if err = m.Page(in.Page, in.Size).ScanAndCount(&out.List, &out.Total, false); err != nil { - return - } - return -} - -func (s *sTag) Create(ctx context.Context, in *model.TagAddIn) (out *model.TagCRUDOut, err error) { - exist, err := dao.Tags.Ctx(ctx). - Where(dao.Tags.Columns().Name, in.Name). - Where(dao.Tags.Columns().Type, in.Type). - Exist() - if err != nil { - return nil, ecode.Fail.Sub("tag_query_failed") - } - if exist { - return nil, ecode.Params.Sub("tag_exists") - } - - if _, err := dao.Tags.Ctx(ctx).Data(do.Tags{ - Name: in.Name, - Type: in.Type, - }).Insert(); err != nil { - return nil, ecode.Fail.Sub("tag_create_failed") - } - - return &model.TagCRUDOut{ - Success: true, - }, nil -} - -func (s *sTag) Update(ctx context.Context, in *model.TagEditIn) (out *model.TagCRUDOut, err error) { - exist, err := dao.Tags.Ctx(ctx). - WherePri(in.Id). - Exist() - if err != nil { - return nil, ecode.Fail.Sub("tag_query_failed") - } - if !exist { - return nil, ecode.NotFound.Sub("tag_not_found") - } - - exist, err = dao.Tags.Ctx(ctx). - Where(dao.Tags.Columns().Name, in.Name). - Where(dao.Tags.Columns().Type, in.Type). - Where("id != ?", in.Id). - Exist() - if err != nil { - return nil, ecode.Fail.Sub("tag_query_failed") - } - if exist { - return nil, ecode.Params.Sub("tag_exists") - } - - _, err = dao.Tags.Ctx(ctx). - WherePri(in.Id). - Data(do.Tags{ - Name: in.Name, - Type: in.Type, - }).Update() - if err != nil { - return nil, ecode.Fail.Sub("tag_update_failed") - } - - return &model.TagCRUDOut{ - Success: true, - }, nil -} - -func (s *sTag) Delete(ctx context.Context, in *model.TagDelIn) (out *model.TagCRUDOut, err error) { - exist, err := dao.Tags.Ctx(ctx). - WherePri(in.Id). - Exist() - if err != nil { - return nil, ecode.Fail.Sub("tag_query_failed") - } - if !exist { - return nil, ecode.NotFound.Sub("tag_not_found") - } - - _, err = dao.Tags.Ctx(ctx).WherePri(in.Id).Delete() - if err != nil { - return nil, ecode.Fail.Sub("tag_delete_failed") - } - - return &model.TagCRUDOut{ - Success: true, - }, nil -} diff --git a/internal/logic/user/user.go b/internal/logic/user/user.go index 9f0c2ad..4401364 100644 --- a/internal/logic/user/user.go +++ b/internal/logic/user/user.go @@ -11,6 +11,8 @@ import ( "server/utility/encrypt" "server/utility/jwt" "strings" + + "github.com/gogf/gf/v2/database/gdb" ) type sUser struct{} @@ -37,7 +39,13 @@ func (s *sUser) Login(ctx context.Context, in *model.UserLoginIn) (out *model.Us if !encrypt.ComparePassword(entityUser.PasswordHash, in.Password) { return nil, ecode.Password // 密码不正确 } - token, err := jwt.GenerateToken(&jwt.TokenIn{UserId: entityUser.Id, Role: "user"}) + // 判断是否为作者 + author, _ := dao.Authors.Ctx(ctx).Where(do.Authors{UserId: entityUser.Id, Status: 1}).One() + role := "user" + if author != nil && !author.IsEmpty() { + role = "author" + } + token, err := jwt.GenerateToken(&jwt.TokenIn{UserId: entityUser.Id, Role: role}) if err != nil { return nil, ecode.Fail.Sub("token_generation_failed") } @@ -92,7 +100,111 @@ func (s *sUser) Info(ctx context.Context, in *model.UserInfoIn) (out *model.User } func (s *sUser) Delete(ctx context.Context, in *model.UserDeleteIn) (out *model.UserDeleteOut, err error) { - // FIXME + // 查询用户信息 + user, err := dao.Users.Ctx(ctx).Where(do.Users{Id: in.UserId}).One() + if err != nil { + return nil, ecode.Fail.Sub("database_query_failed") + } + if user == nil { + return nil, ecode.Auth.Sub("user_not_found") + } + + var entityUser entity.Users + if err = user.Struct(&entityUser); err != nil { + return nil, ecode.Fail.Sub("data_conversion_failed") + } + + // 验证密码 + if !encrypt.ComparePassword(entityUser.PasswordHash, in.Password) { + return nil, ecode.Password.Sub("password_incorrect") + } + + // 开启事务,删除用户及相关数据 + err = dao.Users.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { + // 1. 删除用户阅读记录 + _, err := dao.UserReadRecords.Ctx(ctx).TX(tx). + Where(dao.UserReadRecords.Columns().UserId, in.UserId). + Delete() + if err != nil { + return ecode.Fail.Sub("read_record_delete_failed") + } + + // 2. 删除用户阅读历史 + _, err = dao.UserReadHistory.Ctx(ctx).TX(tx). + Where(dao.UserReadHistory.Columns().UserId, in.UserId). + Delete() + if err != nil { + return ecode.Fail.Sub("history_delete_failed") + } + + // 3. 删除用户书架 + _, err = dao.Bookshelves.Ctx(ctx).TX(tx). + Where(dao.Bookshelves.Columns().UserId, in.UserId). + Delete() + if err != nil { + return ecode.Fail.Sub("bookshelf_delete_failed") + } + + // 4. 删除用户关注作者记录 + _, err = dao.UserFollowAuthors.Ctx(ctx).TX(tx). + Where(dao.UserFollowAuthors.Columns().UserId, in.UserId). + Delete() + if err != nil { + return ecode.Fail.Sub("follow_author_delete_failed") + } + + // 5. 删除用户评分记录 + _, err = dao.BookRatings.Ctx(ctx).TX(tx). + Where(dao.BookRatings.Columns().UserId, in.UserId). + Delete() + if err != nil { + return ecode.Fail.Sub("rating_delete_failed") + } + + // 6. 删除用户章节购买记录 + _, err = dao.UserChapterPurchases.Ctx(ctx).TX(tx). + Where(dao.UserChapterPurchases.Columns().UserId, in.UserId). + Delete() + if err != nil { + return ecode.Fail.Sub("purchase_delete_failed") + } + + // 7. 删除用户积分日志 + _, err = dao.UserPointsLogs.Ctx(ctx).TX(tx). + Where(dao.UserPointsLogs.Columns().UserId, in.UserId). + Delete() + if err != nil { + return ecode.Fail.Sub("points_log_delete_failed") + } + + // 8. 删除用户反馈 + _, err = dao.Feedbacks.Ctx(ctx).TX(tx). + Where(dao.Feedbacks.Columns().UserId, in.UserId). + Delete() + if err != nil { + return ecode.Fail.Sub("feedback_delete_failed") + } + + // 9. 删除作者信息(如果用户是作者) + _, err = dao.Authors.Ctx(ctx).TX(tx). + Where(dao.Authors.Columns().UserId, in.UserId). + Delete() + if err != nil { + return ecode.Fail.Sub("author_delete_failed") + } + + // 10. 最后删除用户(软删除) + _, err = dao.Users.Ctx(ctx).TX(tx).Where(do.Users{Id: in.UserId}).Delete() + if err != nil { + return ecode.Fail.Sub("user_delete_failed") + } + + return nil + }) + + if err != nil { + return nil, err + } return &model.UserDeleteOut{Success: true}, nil } diff --git a/internal/logic/user_follow_author/user_follow_author.go b/internal/logic/user_follow_author/user_follow_author.go index b39922e..c67407e 100644 --- a/internal/logic/user_follow_author/user_follow_author.go +++ b/internal/logic/user_follow_author/user_follow_author.go @@ -5,11 +5,19 @@ import ( "server/internal/dao" "server/internal/model" "server/internal/model/do" + "server/internal/service" "server/utility/ecode" ) type sUserFollowAuthor struct{} +func New() service.IUserFollowAuthor { + return &sUserFollowAuthor{} +} +func init() { + service.RegisterUserFollowAuthor(New()) +} + // List retrieves a paginated list of user follow authors func (s *sUserFollowAuthor) List(ctx context.Context, in *model.UserFollowAuthorListIn) (out *model.UserFollowAuthorListOut, err error) { out = &model.UserFollowAuthorListOut{} @@ -64,3 +72,27 @@ func (s *sUserFollowAuthor) Delete(ctx context.Context, in *model.UserFollowAuth } return &model.UserFollowAuthorCRUDOut{Success: true}, nil } + +// Unfollow removes a user follow author by userId and authorId +func (s *sUserFollowAuthor) Unfollow(ctx context.Context, userId int64, authorId int64) (out *model.UserFollowAuthorCRUDOut, err error) { + if userId == 0 || authorId == 0 { + return nil, ecode.Params.Sub("user_id_or_author_id_invalid") + } + // 查找关注记录 + var record struct{ Id int64 } + err = dao.UserFollowAuthors.Ctx(ctx). + Where(dao.UserFollowAuthors.Columns().UserId, userId). + Where(dao.UserFollowAuthors.Columns().AuthorId, authorId). + Fields("id").Scan(&record) + if err != nil { + return nil, ecode.Fail.Sub("user_follow_author_query_failed") + } + if record.Id == 0 { + return nil, ecode.NotFound.Sub("user_follow_author_not_found") + } + _, err = dao.UserFollowAuthors.Ctx(ctx).WherePri(record.Id).Delete() + if err != nil { + return nil, ecode.Fail.Sub("user_follow_author_delete_failed") + } + return &model.UserFollowAuthorCRUDOut{Success: true}, nil +} diff --git a/internal/logic/user_read_record/user_read_record.go b/internal/logic/user_read_record/user_read_record.go new file mode 100644 index 0000000..c65de66 --- /dev/null +++ b/internal/logic/user_read_record/user_read_record.go @@ -0,0 +1,97 @@ +package user_read_record + +import ( + "context" + "server/internal/dao" + "server/internal/model" + "server/internal/model/do" + "server/internal/service" + "server/utility/ecode" + + "github.com/gogf/gf/v2/os/gtime" +) + +type sUserReadRecord struct{} + +func New() service.IUserReadRecord { + return &sUserReadRecord{} +} +func init() { + service.RegisterUserReadRecord(New()) +} + +// List retrieves a paginated list of user read records +func (s *sUserReadRecord) List(ctx context.Context, in *model.UserReadRecordListIn) (out *model.UserReadRecordListOut, err error) { + out = &model.UserReadRecordListOut{} + m := dao.UserReadRecords.Ctx(ctx) + if in.UserId != 0 { + m = m.Where(dao.UserReadRecords.Columns().UserId, in.UserId) + } + if in.BookId != 0 { + m = m.Where(dao.UserReadRecords.Columns().BookId, in.BookId) + } + if in.ChapterId != 0 { + m = m.Where(dao.UserReadRecords.Columns().ChapterId, in.ChapterId) + } + if err = m.Page(in.Page, in.Size).ScanAndCount(&out.List, &out.Total, false); err != nil { + return + } + return +} + +// Create adds a new user read record +func (s *sUserReadRecord) Create(ctx context.Context, in *model.UserReadRecordAddIn) (out *model.UserReadRecordCRUDOut, err error) { + // 检查是否已存在相同的阅读记录 + exist, err := dao.UserReadRecords.Ctx(ctx). + 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 nil, ecode.Fail.Sub("read_record_query_failed") + } + + if exist { + // 如果记录已存在,更新进度和时间 + _, err = dao.UserReadRecords.Ctx(ctx). + 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() + } else { + // 创建新记录 + _, err = dao.UserReadRecords.Ctx(ctx).Data(do.UserReadRecords{ + UserId: in.UserId, + BookId: in.BookId, + ChapterId: in.ChapterId, + Progress: in.Progress, + ReadAt: gtime.Now(), + }).Insert() + } + + if err != nil { + return nil, ecode.Fail.Sub("read_record_create_failed") + } + return &model.UserReadRecordCRUDOut{Success: true}, nil +} + +// Delete removes user read records by userId and bookIds +func (s *sUserReadRecord) Delete(ctx context.Context, in *model.UserReadRecordDelIn) (out *model.UserReadRecordCRUDOut, err error) { + if len(in.BookIds) == 0 { + return nil, ecode.Params.Sub("bookshelve_bookids_empty") + } + + // 批量删除指定用户的指定书籍历史记录 + _, err = dao.UserReadRecords.Ctx(ctx). + Where(dao.UserReadRecords.Columns().UserId, in.UserId). + WhereIn(dao.UserReadRecords.Columns().BookId, in.BookIds). + Delete() + if err != nil { + return nil, ecode.Fail.Sub("read_record_delete_failed") + } + return &model.UserReadRecordCRUDOut{Success: true}, nil +} diff --git a/internal/model/author.go b/internal/model/author.go new file mode 100644 index 0000000..6dcbe74 --- /dev/null +++ b/internal/model/author.go @@ -0,0 +1,60 @@ +package model + +import "github.com/gogf/gf/v2/frame/g" + +type Author struct { + g.Meta `orm:"table:authors"` + Id int64 `json:"id" orm:"id"` + UserId int64 `json:"userId" orm:"user_id"` + PenName string `json:"penName" orm:"pen_name"` + Bio string `json:"bio" orm:"bio"` +} + +type AuthorListIn struct { + Page int + Size int + PenName string + Status int +} +type AuthorListOut struct { + Total int + List []Author +} + +type AuthorAddIn struct { + UserId int64 + PenName string + Bio string + Status int +} +type AuthorEditIn struct { + Id int64 + PenName string + Bio string + Status int +} +type AuthorDelIn struct { + Id int64 +} +type AuthorCRUDOut struct { + Success bool +} + +type AuthorApplyIn struct { + PenName string // 笔名 + Bio string // 作者简介 +} +type AuthorApplyOut struct { + Success bool +} +type AuthorDetailIn struct { + AuthorId int64 +} +type AuthorDetailOut struct { + g.Meta `orm:"table:authors"` + Id int64 `json:"id" orm:"id"` + UserId int64 `json:"userId" orm:"user_id"` + PenName string `json:"penName" orm:"pen_name"` + Bio string `json:"bio" orm:"bio"` + User User `json:"user" orm:"with:id = user_id"` +} diff --git a/internal/model/book.go b/internal/model/book.go index e4b2c96..3e45b8f 100644 --- a/internal/model/book.go +++ b/internal/model/book.go @@ -1,23 +1,41 @@ package model -import "github.com/gogf/gf/v2/frame/g" +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) type Book struct { g.Meta `orm:"table:books"` - Id int64 `json:"id"` - AuthorId int64 `json:"authorId"` - CategoryId int64 `json:"categoryId"` - Title string `json:"title"` - CoverUrl string `json:"coverUrl"` - Description string `json:"description"` - Status int `json:"status"` - WordsCount int `json:"wordsCount"` - ChaptersCount int `json:"chaptersCount"` - LatestChapterId int64 `json:"latestChapterId"` - Rating float64 `json:"rating"` - ReadCount int64 `json:"readCount"` - Tags string `json:"tags"` - IsRecommended int `json:"isRecommended"` + Id int64 `json:"id" orm:"id"` + AuthorId int64 `json:"authorId" orm:"author_id"` + Author Author `json:"author" orm:"with:id = author_id"` + CategoryId int64 `json:"categoryId" orm:"category_id"` + Category Category `json:"category" orm:"with:id = category_id"` + Title string `json:"title" orm:"title"` + CoverUrl string `json:"coverUrl" orm:"cover_url"` + Description string `json:"description" orm:"description"` + Status int `json:"status" orm:"status"` + WordsCount int `json:"wordsCount" orm:"words_count"` + ChaptersCount int `json:"chaptersCount" orm:"chapters_count"` + LatestChapterId int64 `json:"latestChapterId" orm:"latest_chapter_id"` + Rating float64 `json:"rating" orm:"rating"` + ReadCount int64 `json:"readCount" orm:"read_count"` + CurrentReaders int64 `json:"currentReaders" orm:"current_readers"` + Tags string `json:"tags" orm:"tags"` + IsRecommended int `json:"isRecommended" orm:"is_recommended"` + IsFeatured int `json:"isFeatured" orm:"is_featured"` + Language string `json:"language" orm:"language"` +} + +// App作者信息结构体 +type AppAuthor struct { + g.Meta `orm:"table:authors"` + Id int64 `json:"id" orm:"id"` + UserId int64 `json:"-" orm:"user_id"` + PenName string `json:"penName" orm:"pen_name"` + Bio string `json:"bio" orm:"bio"` + Status int `json:"status" orm:"status"` } type BookListIn struct { @@ -28,6 +46,7 @@ type BookListIn struct { AuthorId int64 Status int IsRecommended int + Sort string } type BookListOut struct { Total int @@ -43,6 +62,8 @@ type BookAddIn struct { Status int Tags string IsRecommended int + IsFeatured int + Language string } type BookEditIn struct { Id int64 @@ -54,6 +75,8 @@ type BookEditIn struct { Status int Tags string IsRecommended int + IsFeatured int + Language string } type BookDelIn struct { Id int64 @@ -62,3 +85,166 @@ type BookDelIn struct { type BookCRUDOut struct { Success bool } + +type BookHistoryRemoveOut struct { + Success bool `json:"success" dc:"是否成功"` +} + +// App 书籍列表项结构体 +type BookAppItem struct { + g.Meta `orm:"table:books"` + Id int64 `json:"id" dc:"书籍ID" orm:"id"` + CoverUrl string `json:"coverUrl" dc:"封面图" orm:"cover_url"` + Rating float64 `json:"rating" dc:"评分" orm:"rating"` + Title string `json:"title" dc:"标题" orm:"title"` + Description string `json:"description" dc:"简介" orm:"description"` + AuthorId int64 `json:"authorId" dc:"作者ID" orm:"author_id"` + Author AppAuthor `json:"author" dc:"作者信息" orm:"with:id = author_id"` + IsFeatured int `json:"isFeatured" dc:"是否精选" orm:"is_featured"` + Language string `json:"language" dc:"语言" orm:"language"` + CategoryId int64 `json:"categoryId" dc:"分类ID" orm:"category_id"` + Category Category `json:"category" dc:"分类信息" orm:"with:id = category_id"` + Status int `json:"status" dc:"状态" orm:"status"` + WordsCount int `json:"wordsCount" dc:"字数" orm:"words_count"` + ChaptersCount int `json:"chaptersCount" dc:"章节数" orm:"chapters_count"` + LatestChapterId int64 `json:"latestChapterId" dc:"最新章节ID" orm:"latest_chapter_id"` + ReadCount int64 `json:"readCount" dc:"阅读人数" orm:"read_count"` + CurrentReaders int64 `json:"currentReaders" dc:"在读人数" orm:"current_readers"` + Tags string `json:"tags" dc:"标签" orm:"tags"` + IsRecommended int `json:"isRecommended" dc:"是否推荐" orm:"is_recommended"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间" orm:"created_at"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间" orm:"updated_at"` + HasRated bool `json:"hasRated" dc:"当前用户是否已评分"` + MyRating float64 `json:"myRating" dc:"当前用户评分(未评分为0)"` +} + +// App 书籍列表查询输入参数 +type BookAppListIn struct { + Page int `json:"page" dc:"页码"` + Size int `json:"size" dc:"每页数量"` + IsRecommended bool `json:"isRecommended" dc:"是否推荐"` + IsLatest int `json:"isLatest" dc:"是否最新"` + CategoryId int64 `json:"categoryId" dc:"分类ID"` + Title string `json:"title" dc:"书名模糊搜索"` + UserId int64 `json:"userId" dc:"用户ID"` + AuthorId int `json:"authorId" dc:"作者ID"` + IsFeatured bool `json:"isFeatured" dc:"是否精选"` + Language string `json:"language" dc:"语言"` + Sort string `json:"sort" dc:"排序字段"` +} + +// App 书籍列表输出结构体 +type BookAppListOut struct { + Total int `json:"total" dc:"总数"` + List []BookAppItem `json:"list" dc:"书籍列表"` +} + +// App 书籍详情查询输入参数 +type BookAppDetailIn struct { + Id int64 `json:"id" dc:"书籍ID"` + UserId int64 `json:"userId" dc:"用户ID"` +} + +// App 书籍详情输出结构体 +type BookAppDetailOut struct { + Id int64 `json:"id" dc:"书籍ID"` + AuthorId int64 `json:"authorId" dc:"作者ID"` + CategoryId int64 `json:"categoryId" dc:"分类ID"` + Title string `json:"title" dc:"书名"` + CoverUrl string `json:"coverUrl" dc:"封面图"` + Description string `json:"description" dc:"简介"` + Status int `json:"status" dc:"状态"` + Tags string `json:"tags" dc:"标签"` + IsRecommended int `json:"isRecommended" dc:"是否推荐"` + IsFeatured int `json:"isFeatured" dc:"是否精选"` + Language string `json:"language" dc:"语言"` + Rating float64 `json:"rating" dc:"评分"` + CurrentReaders int64 `json:"currentReaders" dc:"在读人数"` + CreatedAt *gtime.Time `json:"createdAt" dc:"创建时间"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` + HasRead bool `json:"hasRead" dc:"是否读过"` + ReadProgress int `json:"readProgress" dc:"阅读进度百分比"` + LastChapterId int64 `json:"lastChapterId" dc:"最近阅读章节ID"` + LastReadAt string `json:"lastReadAt" dc:"最近阅读时间"` +} + +// App 用户评分输入参数 +type BookAppRateIn struct { + BookId int64 `json:"bookId" dc:"书籍ID"` + UserId int64 `json:"userId" dc:"用户ID"` + Rating float64 `json:"rating" dc:"评分(1-10分)"` +} + +// App 用户评分输出结构体 +type BookAppRateOut struct { + Success bool `json:"success" dc:"是否成功"` +} + +// 书籍评分模型 +type BookRating struct { + g.Meta `orm:"table:book_ratings"` + Id int64 `json:"id" orm:"id"` + UserId int64 `json:"userId" orm:"user_id"` + BookId int64 `json:"bookId" orm:"book_id"` + Score float64 `json:"score" orm:"score"` + Comment string `json:"comment" orm:"comment"` + CreatedAt *gtime.Time `json:"createdAt" orm:"created_at"` +} + +// 书籍评分输入参数 +type BookRatingAddIn struct { + UserId int64 `json:"userId" dc:"用户ID"` + BookId int64 `json:"bookId" dc:"书籍ID"` + Score float64 `json:"score" dc:"评分(1-10分)"` + Comment string `json:"comment" dc:"用户评论"` +} + +// 书籍评分输出结构体 +type BookRatingCRUDOut struct { + Success bool `json:"success" dc:"是否成功"` +} + +// 我的书籍列表项 +// ============================= +type MyBookItem struct { + Id int64 `json:"id"` + Title string `json:"title"` + CoverUrl string `json:"coverUrl"` + Description string `json:"description"` + Progress int `json:"progress" dc:"阅读进度百分比"` + IsInShelf bool `json:"isInShelf" dc:"是否在书架"` + LastReadAt string `json:"lastReadAt" dc:"最近阅读时间"` + Status int `json:"status" dc:"书籍状态"` + AuthorId int64 `json:"authorId"` + CategoryId int64 `json:"categoryId"` +} + +// 我的书籍列表查询参数 +// ============================= +type MyBookListIn struct { + Type int // 1-正在读 2-已读完 3-历史记录 + Page int + Size int + UserId int64 + Sort string +} + +// 我的书籍列表返回 +// ============================= +type MyBookListOut struct { + Total int + List []MyBookItem +} + +// ============================= +// 新增:单独修改精选状态和推荐状态 +// ============================= +type BookSetFeaturedIn struct { + Id int64 `json:"id" dc:"书籍ID"` + IsFeatured int `json:"isFeatured" dc:"是否精选"` +} + +type BookSetRecommendedIn struct { + Id int64 `json:"id" dc:"书籍ID"` + IsRecommended int `json:"isRecommended" dc:"是否推荐"` +} diff --git a/internal/model/bookshelve.go b/internal/model/bookshelve.go new file mode 100644 index 0000000..836a428 --- /dev/null +++ b/internal/model/bookshelve.go @@ -0,0 +1,24 @@ +package model + +import "github.com/gogf/gf/v2/frame/g" + +type Bookshelve struct { + g.Meta `orm:"table:bookshelves"` + Id int64 `json:"id" orm:"id"` + UserId int64 `json:"userId" orm:"user_id"` + BookId int64 `json:"bookId" orm:"book_id"` + AddedAt int64 `json:"addedAt" orm:"added_at"` + ReadStatus int `json:"readStatus" orm:"read_status"` +} + +type BookshelveAddIn struct { + UserId int64 + BookId int64 +} +type BookshelveDelIn struct { + UserId int64 + BookIds []int64 +} +type BookshelveCRUDOut struct { + Success bool +} diff --git a/internal/model/category.go b/internal/model/category.go index 8ebee78..00f62cb 100644 --- a/internal/model/category.go +++ b/internal/model/category.go @@ -3,17 +3,17 @@ package model import "github.com/gogf/gf/v2/frame/g" type Category struct { - g.Meta `orm:"table:categories"` - Id int64 `json:"id" orm:"id"` // 分类ID - Name string `json:"name" orm:"name"` // 分类名称 - Type int `json:type orm:"type"` // 类型,1 男频,2 女频 + g.Meta `orm:"table:categories"` + Id int64 `json:"id" orm:"id"` + Name string `json:"name" orm:"name"` + Channel int `json:"channel" orm:"channel"` } type CategoryListIn struct { - Page int - Size int - Name string - Type int + Page int + Size int + Name string + Channel int } type CategoryListOut struct { Total int @@ -21,16 +21,16 @@ type CategoryListOut struct { } type CategoryAddIn struct { - Name string - Type int // 类型,1 男频,2 女频 + Name string + Channel int } type CategoryEditIn struct { - Id int - Name string - Type int // 类型,1 男频,2 女频 + Id int64 + Name string + Channel int } type CategoryDelIn struct { - Id int + Id int64 } type CategoryCRUDOut struct { diff --git a/internal/model/chapter.go b/internal/model/chapter.go index 30a04af..de10771 100644 --- a/internal/model/chapter.go +++ b/internal/model/chapter.go @@ -1,17 +1,20 @@ package model -import "github.com/gogf/gf/v2/frame/g" +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) type Chapter struct { g.Meta `orm:"table:chapters"` - Id int64 `json:"id"` - BookId int64 `json:"bookId"` - Title string `json:"title"` - Content string `json:"content"` - WordCount int `json:"wordCount"` - Sort int `json:"sort"` - IsLocked int `json:"isLocked"` - RequiredScore int `json:"requiredScore"` + Id int64 `json:"id" orm:"id"` + BookId int64 `json:"bookId" orm:"book_id"` + Title string `json:"title" orm:"title"` + Content string `json:"content" orm:"content"` + WordCount int `json:"wordCount" orm:"word_count"` + Sort int `json:"sort" orm:"sort"` + IsLocked int `json:"isLocked" orm:"is_locked"` + RequiredScore int `json:"requiredScore" orm:"required_score"` } type ChapterListIn struct { @@ -52,3 +55,73 @@ type ChapterDelIn struct { type ChapterCRUDOut struct { Success bool } + +// App 章节列表查询输入参数 +type ChapterAppListIn struct { + BookId int64 `json:"bookId" dc:"书籍ID"` + IsDesc bool `json:"isDesc" dc:"是否逆序排列"` + Page int `json:"page" dc:"页码"` + Size int `json:"size" dc:"每页数量"` + UserId int64 `json:"userId" dc:"用户ID"` +} + +// App 章节列表输出结构体(不包含 content) +type ChapterAppItem struct { + g.Meta `orm:"table:chapters"` + Id int64 `json:"id" orm:"id" dc:"章节ID"` + Title string `json:"title" orm:"title" dc:"章节标题"` + WordCount int `json:"wordCount" orm:"word_count" dc:"字数"` + Sort int `json:"sort" orm:"sort" dc:"排序"` + IsLocked int `json:"isLocked" orm:"is_locked" dc:"是否锁定"` + RequiredScore int `json:"requiredScore" orm:"required_score" dc:"所需积分"` + UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" dc:"更新时间"` + ReadProgress int `json:"readProgress" dc:"阅读进度百分比"` + ReadAt *gtime.Time `json:"readAt" dc:"最后阅读时间"` +} + +type ChapterAppListOut struct { + List []ChapterAppItem + Total int +} + +// App 章节详情查询输入参数 +type ChapterAppDetailIn struct { + Id int64 `json:"id" dc:"章节ID"` +} + +// App 章节详情输出结构体 +type ChapterAppDetailOut struct { + Id int64 `json:"id" dc:"章节ID"` + BookId int64 `json:"bookId" dc:"书籍ID"` + Title string `json:"title" dc:"章节标题"` + Content string `json:"content" dc:"章节内容"` + WordCount int `json:"wordCount" dc:"字数"` + Sort int `json:"sort" dc:"排序"` + IsLocked int `json:"isLocked" dc:"是否锁定"` + RequiredScore int `json:"requiredScore" dc:"所需积分"` + UpdatedAt *gtime.Time `json:"updatedAt" dc:"更新时间"` +} + +// App 购买章节输入参数 +type ChapterAppPurchaseIn struct { + Id int64 `json:"id" dc:"章节ID"` + UserId int64 `json:"userId" dc:"用户ID"` +} + +// App 购买章节输出结构体 +type ChapterAppPurchaseOut struct { + Success bool `json:"success" dc:"是否成功"` +} + +// App 上传阅读进度输入参数 +type ChapterAppProgressIn struct { + BookId int64 `json:"bookId" dc:"书籍ID"` + ChapterId int64 `json:"chapterId" dc:"章节ID"` + Progress int `json:"progress" dc:"阅读进度百分比"` + UserId int64 `json:"userId" dc:"用户ID"` +} + +// App 上传阅读进度输出结构体 +type ChapterAppProgressOut struct { + Success bool `json:"success" dc:"是否成功"` +} diff --git a/internal/model/do/books.go b/internal/model/do/books.go index 2193b56..3974243 100644 --- a/internal/model/do/books.go +++ b/internal/model/do/books.go @@ -11,22 +11,24 @@ import ( // Books is the golang structure of table books for DAO operations like Where/Data. type Books struct { - g.Meta `orm:"table:books, do:true"` - Id interface{} // 小说ID - AuthorId interface{} // 作者ID - CategoryId interface{} // 分类ID - Title interface{} // 小说标题 - CoverUrl interface{} // 封面图片URL - Description interface{} // 小说简介 - Status interface{} // 状态:1=连载中,2=完结,3=下架 - WordsCount interface{} // 字数 - ChaptersCount interface{} // 章节数 - LatestChapterId interface{} // 最新章节ID - Rating interface{} // 评分(0.00~10.00) - ReadCount interface{} // 阅读人数 - Tags interface{} // 标签(逗号分隔) - CreatedAt *gtime.Time // 创建时间 - UpdatedAt *gtime.Time // 更新时间 - DeletedAt *gtime.Time // 软删除时间戳 - IsRecommended interface{} // 是否推荐:0=否,1=是 + g.Meta `orm:"table:books, do:true"` + Id interface{} // 小说ID + AuthorId interface{} // 作者ID + CategoryId interface{} // 分类ID + Title interface{} // 小说标题 + CoverUrl interface{} // 封面图片URL + Description interface{} // 小说简介 + Status interface{} // 状态:1=连载中,2=完结,3=下架 + WordsCount interface{} // 字数 + ChaptersCount interface{} // 章节数 + Rating interface{} // 评分(0.00~10.00) + ReadCount interface{} // 阅读人数 + CurrentReaders interface{} // 在读人数 + Tags interface{} // 标签(逗号分隔) + CreatedAt *gtime.Time // 创建时间 + UpdatedAt *gtime.Time // 更新时间 + DeletedAt *gtime.Time // 软删除时间戳 + IsRecommended interface{} // 是否推荐:0=否,1=是 + IsFeatured interface{} // 是否精选:0=否,1=是 + Language interface{} // 语言,如 zh=中文,en=英文,jp=日文 } diff --git a/internal/model/do/bookshelves.go b/internal/model/do/bookshelves.go index ba83e3f..30ad2a2 100644 --- a/internal/model/do/bookshelves.go +++ b/internal/model/do/bookshelves.go @@ -19,4 +19,5 @@ type Bookshelves struct { LastReadChapterId interface{} // 最后阅读章节ID LastReadPercent interface{} // 阅读进度百分比(0.00~100.00) LastReadAt *gtime.Time // 最后阅读时间 + ReadStatus interface{} // 阅读状态:1=正在读,2=已读完,3=已收藏 } diff --git a/internal/model/do/categories.go b/internal/model/do/categories.go index 933fee6..e804716 100644 --- a/internal/model/do/categories.go +++ b/internal/model/do/categories.go @@ -14,8 +14,8 @@ type Categories struct { g.Meta `orm:"table:categories, do:true"` Id interface{} // 分类ID Name interface{} // 分类名称 - Type interface{} // 分类类型:1=男频, 2=女频 CreatedAt *gtime.Time // 创建时间 UpdatedAt *gtime.Time // 更新时间 DeletedAt *gtime.Time // 软删除时间戳 + Channel interface{} // 频道类型:1=男频,2=女频 } diff --git a/internal/model/do/user_points_logs.go b/internal/model/do/user_points_logs.go index 996641e..7a346b1 100644 --- a/internal/model/do/user_points_logs.go +++ b/internal/model/do/user_points_logs.go @@ -14,9 +14,9 @@ type UserPointsLogs struct { g.Meta `orm:"table:user_points_logs, do:true"` Id interface{} // 积分流水ID UserId interface{} // 用户ID - ChangeType interface{} // 变动类型,例如 earn、spend、refund 等 + ChangeType interface{} // 变动类型,1=消费(spend), 2=收入(earn) PointsChange interface{} // 积分变化数,正数增加,负数减少 - RelatedOrderId interface{} // 关联订单ID + RelatedOrderId interface{} // 关联ID:当change_type=1时,为chapter_purchases.id;当change_type=2时,为advertisement_records.id Description interface{} // 变动说明 CreatedAt *gtime.Time // 变动时间 } diff --git a/internal/model/do/user_read_history.go b/internal/model/do/user_read_history.go new file mode 100644 index 0000000..68e70eb --- /dev/null +++ b/internal/model/do/user_read_history.go @@ -0,0 +1,20 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// UserReadHistory is the golang structure of table user_read_history for DAO operations like Where/Data. +type UserReadHistory struct { + g.Meta `orm:"table:user_read_history, do:true"` + Id interface{} // 历史记录ID + UserId interface{} // 用户ID + BookId interface{} // 小说ID + ChapterId interface{} // 最后阅读章节ID + ReadAt *gtime.Time // 最后阅读时间 +} diff --git a/internal/model/do/read_records.go b/internal/model/do/user_read_records.go similarity index 61% rename from internal/model/do/read_records.go rename to internal/model/do/user_read_records.go index 2b871a0..7d0c04a 100644 --- a/internal/model/do/read_records.go +++ b/internal/model/do/user_read_records.go @@ -9,12 +9,15 @@ import ( "github.com/gogf/gf/v2/os/gtime" ) -// ReadRecords is the golang structure of table read_records for DAO operations like Where/Data. -type ReadRecords struct { - g.Meta `orm:"table:read_records, do:true"` +// UserReadRecords is the golang structure of table user_read_records for DAO operations like Where/Data. +type UserReadRecords struct { + g.Meta `orm:"table:user_read_records, do:true"` Id interface{} // 记录ID UserId interface{} // 用户ID BookId interface{} // 小说ID ChapterId interface{} // 章节ID + Progress interface{} // 阅读进度百分比(0-100) ReadAt *gtime.Time // 阅读时间 + CreatedAt *gtime.Time // 创建时间 + UpdatedAt *gtime.Time // 更新时间 } diff --git a/internal/model/entity/books.go b/internal/model/entity/books.go index 21b2c27..190c63d 100644 --- a/internal/model/entity/books.go +++ b/internal/model/entity/books.go @@ -10,21 +10,23 @@ import ( // Books is the golang structure for table books. type Books struct { - Id int64 `json:"id" orm:"id" description:"小说ID"` // 小说ID - AuthorId int64 `json:"authorId" orm:"author_id" description:"作者ID"` // 作者ID - CategoryId int64 `json:"categoryId" orm:"category_id" description:"分类ID"` // 分类ID - Title string `json:"title" orm:"title" description:"小说标题"` // 小说标题 - CoverUrl string `json:"coverUrl" orm:"cover_url" description:"封面图片URL"` // 封面图片URL - Description string `json:"description" orm:"description" description:"小说简介"` // 小说简介 - Status int `json:"status" orm:"status" description:"状态:1=连载中,2=完结,3=下架"` // 状态:1=连载中,2=完结,3=下架 - WordsCount int `json:"wordsCount" orm:"words_count" description:"字数"` // 字数 - ChaptersCount int `json:"chaptersCount" orm:"chapters_count" description:"章节数"` // 章节数 - LatestChapterId int64 `json:"latestChapterId" orm:"latest_chapter_id" description:"最新章节ID"` // 最新章节ID - Rating float64 `json:"rating" orm:"rating" description:"评分(0.00~10.00)"` // 评分(0.00~10.00) - ReadCount int64 `json:"readCount" orm:"read_count" description:"阅读人数"` // 阅读人数 - Tags string `json:"tags" orm:"tags" description:"标签(逗号分隔)"` // 标签(逗号分隔) - CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"创建时间"` // 创建时间 - UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:"更新时间"` // 更新时间 - DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_at" description:"软删除时间戳"` // 软删除时间戳 - IsRecommended int `json:"isRecommended" orm:"is_recommended" description:"是否推荐:0=否,1=是"` // 是否推荐:0=否,1=是 + Id int64 `json:"id" orm:"id" description:"小说ID"` // 小说ID + AuthorId int64 `json:"authorId" orm:"author_id" description:"作者ID"` // 作者ID + CategoryId int64 `json:"categoryId" orm:"category_id" description:"分类ID"` // 分类ID + Title string `json:"title" orm:"title" description:"小说标题"` // 小说标题 + CoverUrl string `json:"coverUrl" orm:"cover_url" description:"封面图片URL"` // 封面图片URL + Description string `json:"description" orm:"description" description:"小说简介"` // 小说简介 + Status int `json:"status" orm:"status" description:"状态:1=连载中,2=完结,3=下架"` // 状态:1=连载中,2=完结,3=下架 + WordsCount int `json:"wordsCount" orm:"words_count" description:"字数"` // 字数 + ChaptersCount int `json:"chaptersCount" orm:"chapters_count" description:"章节数"` // 章节数 + Rating float64 `json:"rating" orm:"rating" description:"评分(0.00~10.00)"` // 评分(0.00~10.00) + ReadCount int64 `json:"readCount" orm:"read_count" description:"阅读人数"` // 阅读人数 + CurrentReaders int64 `json:"currentReaders" orm:"current_readers" description:"在读人数"` // 在读人数 + Tags string `json:"tags" orm:"tags" description:"标签(逗号分隔)"` // 标签(逗号分隔) + CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"创建时间"` // 创建时间 + UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:"更新时间"` // 更新时间 + DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_at" description:"软删除时间戳"` // 软删除时间戳 + IsRecommended int `json:"isRecommended" orm:"is_recommended" description:"是否推荐:0=否,1=是"` // 是否推荐:0=否,1=是 + IsFeatured int `json:"isFeatured" orm:"is_featured" description:"是否精选:0=否,1=是"` // 是否精选:0=否,1=是 + Language string `json:"language" orm:"language" description:"语言,如 zh=中文,en=英文,jp=日文"` // 语言,如 zh=中文,en=英文,jp=日文 } diff --git a/internal/model/entity/bookshelves.go b/internal/model/entity/bookshelves.go index d6f893b..b1d951a 100644 --- a/internal/model/entity/bookshelves.go +++ b/internal/model/entity/bookshelves.go @@ -10,11 +10,12 @@ import ( // Bookshelves is the golang structure for table bookshelves. type Bookshelves struct { - Id int64 `json:"id" orm:"id" description:"记录ID"` // 记录ID - UserId int64 `json:"userId" orm:"user_id" description:"用户ID"` // 用户ID - BookId int64 `json:"bookId" orm:"book_id" description:"小说ID"` // 小说ID - AddedAt *gtime.Time `json:"addedAt" orm:"added_at" description:"加入书架时间"` // 加入书架时间 - LastReadChapterId int64 `json:"lastReadChapterId" orm:"last_read_chapter_id" description:"最后阅读章节ID"` // 最后阅读章节ID - LastReadPercent float64 `json:"lastReadPercent" orm:"last_read_percent" description:"阅读进度百分比(0.00~100.00)"` // 阅读进度百分比(0.00~100.00) - LastReadAt *gtime.Time `json:"lastReadAt" orm:"last_read_at" description:"最后阅读时间"` // 最后阅读时间 + Id int64 `json:"id" orm:"id" description:"记录ID"` // 记录ID + UserId int64 `json:"userId" orm:"user_id" description:"用户ID"` // 用户ID + BookId int64 `json:"bookId" orm:"book_id" description:"小说ID"` // 小说ID + AddedAt *gtime.Time `json:"addedAt" orm:"added_at" description:"加入书架时间"` // 加入书架时间 + LastReadChapterId int64 `json:"lastReadChapterId" orm:"last_read_chapter_id" description:"最后阅读章节ID"` // 最后阅读章节ID + LastReadPercent float64 `json:"lastReadPercent" orm:"last_read_percent" description:"阅读进度百分比(0.00~100.00)"` // 阅读进度百分比(0.00~100.00) + LastReadAt *gtime.Time `json:"lastReadAt" orm:"last_read_at" description:"最后阅读时间"` // 最后阅读时间 + ReadStatus int `json:"readStatus" orm:"read_status" description:"阅读状态:1=正在读,2=已读完,3=已收藏"` // 阅读状态:1=正在读,2=已读完,3=已收藏 } diff --git a/internal/model/entity/categories.go b/internal/model/entity/categories.go index 2907560..7031597 100644 --- a/internal/model/entity/categories.go +++ b/internal/model/entity/categories.go @@ -10,10 +10,10 @@ import ( // Categories is the golang structure for table categories. type Categories struct { - Id int64 `json:"id" orm:"id" description:"分类ID"` // 分类ID - Name string `json:"name" orm:"name" description:"分类名称"` // 分类名称 - Type int `json:"type" orm:"type" description:"分类类型:1=男频, 2=女频"` // 分类类型:1=男频, 2=女频 - CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"创建时间"` // 创建时间 - UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:"更新时间"` // 更新时间 - DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_at" description:"软删除时间戳"` // 软删除时间戳 + Id int64 `json:"id" orm:"id" description:"分类ID"` // 分类ID + Name string `json:"name" orm:"name" description:"分类名称"` // 分类名称 + CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"创建时间"` // 创建时间 + UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:"更新时间"` // 更新时间 + DeletedAt *gtime.Time `json:"deletedAt" orm:"deleted_at" description:"软删除时间戳"` // 软删除时间戳 + Channel int `json:"channel" orm:"channel" description:"频道类型:1=男频,2=女频"` // 频道类型:1=男频,2=女频 } diff --git a/internal/model/entity/user_points_logs.go b/internal/model/entity/user_points_logs.go index 2d0ce38..fa12761 100644 --- a/internal/model/entity/user_points_logs.go +++ b/internal/model/entity/user_points_logs.go @@ -10,11 +10,11 @@ import ( // UserPointsLogs is the golang structure for table user_points_logs. type UserPointsLogs struct { - Id int64 `json:"id" orm:"id" description:"积分流水ID"` // 积分流水ID - UserId int64 `json:"userId" orm:"user_id" description:"用户ID"` // 用户ID - ChangeType string `json:"changeType" orm:"change_type" description:"变动类型,例如 earn、spend、refund 等"` // 变动类型,例如 earn、spend、refund 等 - PointsChange int `json:"pointsChange" orm:"points_change" description:"积分变化数,正数增加,负数减少"` // 积分变化数,正数增加,负数减少 - RelatedOrderId int64 `json:"relatedOrderId" orm:"related_order_id" description:"关联订单ID"` // 关联订单ID - Description string `json:"description" orm:"description" description:"变动说明"` // 变动说明 - CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"变动时间"` // 变动时间 + Id int64 `json:"id" orm:"id" description:"积分流水ID"` // 积分流水ID + UserId int64 `json:"userId" orm:"user_id" description:"用户ID"` // 用户ID + ChangeType int `json:"changeType" orm:"change_type" description:"变动类型,1=消费(spend), 2=收入(earn)"` // 变动类型,1=消费(spend), 2=收入(earn) + PointsChange int `json:"pointsChange" orm:"points_change" description:"积分变化数,正数增加,负数减少"` // 积分变化数,正数增加,负数减少 + RelatedOrderId int64 `json:"relatedOrderId" orm:"related_order_id" description:"关联ID:当change_type=1时,为chapter_purchases.id;当change_type=2时,为advertisement_records.id"` // 关联ID:当change_type=1时,为chapter_purchases.id;当change_type=2时,为advertisement_records.id + Description string `json:"description" orm:"description" description:"变动说明"` // 变动说明 + CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"变动时间"` // 变动时间 } diff --git a/internal/model/entity/read_records.go b/internal/model/entity/user_read_history.go similarity index 66% rename from internal/model/entity/read_records.go rename to internal/model/entity/user_read_history.go index 1f22b92..3cdd8a7 100644 --- a/internal/model/entity/read_records.go +++ b/internal/model/entity/user_read_history.go @@ -8,11 +8,11 @@ import ( "github.com/gogf/gf/v2/os/gtime" ) -// ReadRecords is the golang structure for table read_records. -type ReadRecords struct { - Id int64 `json:"id" orm:"id" description:"记录ID"` // 记录ID - UserId int64 `json:"userId" orm:"user_id" description:"用户ID"` // 用户ID - BookId int64 `json:"bookId" orm:"book_id" description:"小说ID"` // 小说ID - ChapterId int64 `json:"chapterId" orm:"chapter_id" description:"章节ID"` // 章节ID - ReadAt *gtime.Time `json:"readAt" orm:"read_at" description:"阅读时间"` // 阅读时间 +// UserReadHistory is the golang structure for table user_read_history. +type UserReadHistory struct { + Id int64 `json:"id" orm:"id" description:"历史记录ID"` // 历史记录ID + UserId int64 `json:"userId" orm:"user_id" description:"用户ID"` // 用户ID + BookId int64 `json:"bookId" orm:"book_id" description:"小说ID"` // 小说ID + ChapterId int64 `json:"chapterId" orm:"chapter_id" description:"最后阅读章节ID"` // 最后阅读章节ID + ReadAt *gtime.Time `json:"readAt" orm:"read_at" description:"最后阅读时间"` // 最后阅读时间 } diff --git a/internal/model/entity/user_read_records.go b/internal/model/entity/user_read_records.go new file mode 100644 index 0000000..248acd5 --- /dev/null +++ b/internal/model/entity/user_read_records.go @@ -0,0 +1,21 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// UserReadRecords is the golang structure for table user_read_records. +type UserReadRecords struct { + Id int64 `json:"id" orm:"id" description:"记录ID"` // 记录ID + UserId int64 `json:"userId" orm:"user_id" description:"用户ID"` // 用户ID + BookId int64 `json:"bookId" orm:"book_id" description:"小说ID"` // 小说ID + ChapterId int64 `json:"chapterId" orm:"chapter_id" description:"章节ID"` // 章节ID + Progress int `json:"progress" orm:"progress" description:"阅读进度百分比(0-100)"` // 阅读进度百分比(0-100) + ReadAt *gtime.Time `json:"readAt" orm:"read_at" description:"阅读时间"` // 阅读时间 + CreatedAt *gtime.Time `json:"createdAt" orm:"created_at" description:"创建时间"` // 创建时间 + UpdatedAt *gtime.Time `json:"updatedAt" orm:"updated_at" description:"更新时间"` // 更新时间 +} diff --git a/internal/model/feedback.go b/internal/model/feedback.go index 4c185f4..bfc825c 100644 --- a/internal/model/feedback.go +++ b/internal/model/feedback.go @@ -1,13 +1,18 @@ package model -import "github.com/gogf/gf/v2/frame/g" +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) type Feedback struct { - g.Meta `orm:"table:feedbacks"` - Id int64 `json:"id"` - UserId int64 `json:"userId"` - Content string `json:"content"` - Status int `json:"status"` + g.Meta `orm:"table:feedbacks"` + Id int64 `json:"id" orm:"id"` + UserId int64 `json:"userId" orm:"user_id"` + Content string `json:"content" orm:"content"` + Status int `json:"status" orm:"status"` + CreatedAt *gtime.Time `json:"createdAt" orm:"created_at"` + User User `json:"user" orm:"with:id=user_id"` } type FeedbackListIn struct { diff --git a/internal/model/read_record.go b/internal/model/read_record.go deleted file mode 100644 index 27f820d..0000000 --- a/internal/model/read_record.go +++ /dev/null @@ -1,36 +0,0 @@ -package model - -import "github.com/gogf/gf/v2/frame/g" - -type ReadRecord struct { - g.Meta `orm:"table:read_records"` - Id int64 `json:"id"` - UserId int64 `json:"userId"` - BookId int64 `json:"bookId"` - ChapterId int64 `json:"chapterId"` - ReadAt int64 `json:"readAt"` -} - -type ReadRecordListIn struct { - Page int - Size int - UserId int64 - BookId int64 - ChapterId int64 -} -type ReadRecordListOut struct { - Total int - List []ReadRecord -} - -type ReadRecordAddIn struct { - UserId int64 - BookId int64 - ChapterId int64 -} -type ReadRecordDelIn struct { - Id int64 -} -type ReadRecordCRUDOut struct { - Success bool -} diff --git a/internal/model/tag.go b/internal/model/tag.go index 3420d91..51d8f89 100644 --- a/internal/model/tag.go +++ b/internal/model/tag.go @@ -4,9 +4,9 @@ import "github.com/gogf/gf/v2/frame/g" type Tag struct { g.Meta `orm:"table:tags"` - Id int64 `json:"id"` - Name string `json:"name"` - Type int `json:"type"` // 1=主题, 2=角色, 3=情节 + Id int64 `json:"id" orm:"id"` + Name string `json:"name" orm:"name"` + Type int `json:"type" orm:"type"` // 1=主题, 2=角色, 3=情节 } type TagListIn struct { diff --git a/internal/model/upload.go b/internal/model/upload.go index 59d7292..8b53790 100644 --- a/internal/model/upload.go +++ b/internal/model/upload.go @@ -1,47 +1 @@ package model - -import "github.com/gogf/gf/v2/net/ghttp" - -type UploadIn struct { - File *ghttp.UploadFile - Type string -} - -type UploadOut struct { - Url string -} -type OssOutput struct { - Url string -} - -type OssBytesInput struct { - Bytes []byte - Name string -} - -type OssGetFileInput struct { - FilePath string - Name string -} - -type OssUploadFileInput struct { - Filename string - File *ghttp.UploadFile -} - -type SMSCodeIn struct { - Phone string - Code string -} - -type SMSCodeOut struct { - Success bool -} - -type CaptchaIn struct { - Name string -} - -type CaptchaOut struct { - Success bool -} diff --git a/internal/model/user.go b/internal/model/user.go index 87847f3..b8acdcd 100644 --- a/internal/model/user.go +++ b/internal/model/user.go @@ -1,5 +1,19 @@ package model +import "github.com/gogf/gf/v2/frame/g" + +type User struct { + g.Meta `orm:"table:users"` + Id int64 `json:"id" orm:"id"` + Email string `json:"email" orm:"email"` + Username string `json:"username" orm:"username"` + Avatar string `json:"avatar" orm:"avatar"` +} +type AppUser struct { + g.Meta `orm:"table:users"` + Id int64 `json:"id" orm:"id"` + Avatar string `json:"avatar" orm:"avatar"` +} type UserLoginIn struct { Email string // 用户名 Password string // 密码 diff --git a/internal/model/user_follow_author.go b/internal/model/user_follow_author.go index 1e6c380..567c371 100644 --- a/internal/model/user_follow_author.go +++ b/internal/model/user_follow_author.go @@ -4,10 +4,10 @@ import "github.com/gogf/gf/v2/frame/g" type UserFollowAuthor struct { g.Meta `orm:"table:user_follow_authors"` - Id int64 `json:"id"` - UserId int64 `json:"userId"` - AuthorId int64 `json:"authorId"` - FollowedAt int64 `json:"followedAt"` + Id int64 `json:"id" orm:"id"` + UserId int64 `json:"userId" orm:"user_id"` + AuthorId int64 `json:"authorId" orm:"author_id"` + FollowedAt int64 `json:"followedAt" orm:"followed_at"` } type UserFollowAuthorListIn struct { diff --git a/internal/model/user_read_history.go b/internal/model/user_read_history.go new file mode 100644 index 0000000..4abf5f0 --- /dev/null +++ b/internal/model/user_read_history.go @@ -0,0 +1,27 @@ +package model + +import "github.com/gogf/gf/v2/frame/g" + +type UserReadHistory struct { + g.Meta `orm:"table:user_read_history"` + Id int64 `json:"id" orm:"id"` + UserId int64 `json:"userId" orm:"user_id"` + BookId int64 `json:"bookId" orm:"book_id"` + ChapterId int64 `json:"chapterId" orm:"chapter_id"` + ReadAt int64 `json:"readAt" orm:"read_at"` +} + +type UserReadHistoryAddIn struct { + UserId int64 + BookId int64 + ChapterId int64 +} + +type UserReadHistoryDelIn struct { + UserId int64 + BookIds []int64 +} + +type UserReadHistoryCRUDOut struct { + Success bool +} diff --git a/internal/model/user_read_record.go b/internal/model/user_read_record.go new file mode 100644 index 0000000..e871b87 --- /dev/null +++ b/internal/model/user_read_record.go @@ -0,0 +1,39 @@ +package model + +import "github.com/gogf/gf/v2/frame/g" + +type UserReadRecordModel struct { + g.Meta `orm:"table:user_read_records"` + Id int64 `json:"id" orm:"id"` + UserId int64 `json:"userId" orm:"user_id"` + BookId int64 `json:"bookId" orm:"book_id"` + ChapterId int64 `json:"chapterId" orm:"chapter_id"` + Progress int `json:"progress" orm:"progress"` + ReadAt int64 `json:"readAt" orm:"read_at"` +} + +type UserReadRecordListIn struct { + Page int + Size int + UserId int64 + BookId int64 + ChapterId int64 +} +type UserReadRecordListOut struct { + Total int + List []UserReadRecordModel +} + +type UserReadRecordAddIn struct { + UserId int64 + BookId int64 + ChapterId int64 + Progress int +} +type UserReadRecordDelIn struct { + UserId int64 + BookIds []int64 +} +type UserReadRecordCRUDOut struct { + Success bool +} diff --git a/internal/packed/packed.go b/internal/packed/packed.go index 05d3470..22336da 100644 --- a/internal/packed/packed.go +++ b/internal/packed/packed.go @@ -2,7 +2,6 @@ package packed import ( _ "server/utility/myCasbin" - _ "server/utility/oss/aliyun" _ "github.com/gogf/gf/contrib/drivers/mysql/v2" _ "github.com/gogf/gf/contrib/nosql/redis/v2" diff --git a/internal/service/author.go b/internal/service/author.go new file mode 100644 index 0000000..d4cccfd --- /dev/null +++ b/internal/service/author.go @@ -0,0 +1,42 @@ +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "server/internal/model" +) + +type ( + IAuthor interface { + // List retrieves a paginated list of authors + List(ctx context.Context, in *model.AuthorListIn) (out *model.AuthorListOut, err error) + // Create adds a new author + Create(ctx context.Context, in *model.AuthorAddIn) (out *model.AuthorCRUDOut, err error) + // Update edits an author + Update(ctx context.Context, in *model.AuthorEditIn) (out *model.AuthorCRUDOut, err error) + // Delete removes an author by id + Delete(ctx context.Context, in *model.AuthorDelIn) (out *model.AuthorCRUDOut, err error) + // Apply 允许用户申请成为作者 + Apply(ctx context.Context, in *model.AuthorApplyIn) (out *model.AuthorApplyOut, err error) + Detail(ctx context.Context, in *model.AuthorDetailIn) (out *model.AuthorDetailOut, err error) + } +) + +var ( + localAuthor IAuthor +) + +func Author() IAuthor { + if localAuthor == nil { + panic("implement not found for interface IAuthor, forgot register?") + } + return localAuthor +} + +func RegisterAuthor(i IAuthor) { + localAuthor = i +} diff --git a/internal/service/book.go b/internal/service/book.go index 16ffa3c..8cdbe18 100644 --- a/internal/service/book.go +++ b/internal/service/book.go @@ -17,6 +17,17 @@ type ( Create(ctx context.Context, in *model.BookAddIn) (out *model.BookCRUDOut, err error) Update(ctx context.Context, in *model.BookEditIn) (out *model.BookCRUDOut, err error) Delete(ctx context.Context, in *model.BookDelIn) (out *model.BookCRUDOut, err error) + // AppList retrieves book list for app + AppList(ctx context.Context, in *model.BookAppListIn) (out *model.BookAppListOut, err error) + // AppRate rates a book for app + AppRate(ctx context.Context, in *model.BookAppRateIn) (out *model.BookAppRateOut, err error) + // AppDetail retrieves book detail for app + AppDetail(ctx context.Context, in *model.BookAppDetailIn) (out *model.BookAppDetailOut, err error) + MyList(ctx context.Context, in *model.MyBookListIn) (out *model.MyBookListOut, err error) + // SetFeatured: 单独修改书籍的精选状态 + SetFeatured(ctx context.Context, in *model.BookSetFeaturedIn) (out *model.BookCRUDOut, err error) + // SetRecommended: 单独修改书籍的推荐状态 + SetRecommended(ctx context.Context, in *model.BookSetRecommendedIn) (out *model.BookCRUDOut, err error) } ) diff --git a/internal/service/bookshelve.go b/internal/service/bookshelve.go new file mode 100644 index 0000000..e539105 --- /dev/null +++ b/internal/service/bookshelve.go @@ -0,0 +1,35 @@ +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "server/internal/model" +) + +type ( + IBookshelve interface { + // Add 添加书架 + Add(ctx context.Context, in *model.BookshelveAddIn) (out *model.BookshelveCRUDOut, err error) + // Delete 批量删除书架 + Delete(ctx context.Context, in *model.BookshelveDelIn) (out *model.BookshelveCRUDOut, err error) + } +) + +var ( + localBookshelve IBookshelve +) + +func Bookshelve() IBookshelve { + if localBookshelve == nil { + panic("implement not found for interface IBookshelve, forgot register?") + } + return localBookshelve +} + +func RegisterBookshelve(i IBookshelve) { + localBookshelve = i +} diff --git a/internal/service/chapter.go b/internal/service/chapter.go index e0f9247..58c8eae 100644 --- a/internal/service/chapter.go +++ b/internal/service/chapter.go @@ -14,9 +14,20 @@ type ( IChapter interface { // List retrieves a paginated list of chapters List(ctx context.Context, in *model.ChapterListIn) (out *model.ChapterListOut, err error) + // Create creates a new chapter Create(ctx context.Context, in *model.ChapterAddIn) (out *model.ChapterCRUDOut, err error) + // Update updates an existing chapter Update(ctx context.Context, in *model.ChapterEditIn) (out *model.ChapterCRUDOut, err error) + // Delete deletes a chapter by ID Delete(ctx context.Context, in *model.ChapterDelIn) (out *model.ChapterCRUDOut, err error) + // AppList retrieves chapter list for app without content + AppList(ctx context.Context, in *model.ChapterAppListIn) (out *model.ChapterAppListOut, err error) + // AppDetail retrieves chapter detail for app + AppDetail(ctx context.Context, in *model.ChapterAppDetailIn) (out *model.ChapterAppDetailOut, err error) + // AppPurchase purchases chapter for app + AppPurchase(ctx context.Context, in *model.ChapterAppPurchaseIn) (out *model.ChapterAppPurchaseOut, err error) + // AppProgress uploads reading progress for app + AppProgress(ctx context.Context, in *model.ChapterAppProgressIn) (out *model.ChapterAppProgressOut, err error) } ) diff --git a/internal/service/read_record.go b/internal/service/read_record.go deleted file mode 100644 index cc5ba4b..0000000 --- a/internal/service/read_record.go +++ /dev/null @@ -1,37 +0,0 @@ -// ================================================================================ -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. -// You can delete these comments if you wish manually maintain this interface file. -// ================================================================================ - -package service - -import ( - "context" - "server/internal/model" -) - -type ( - IReadRecord interface { - // List retrieves a paginated list of read records - List(ctx context.Context, in *model.ReadRecordListIn) (out *model.ReadRecordListOut, err error) - // Create adds a new read record - Create(ctx context.Context, in *model.ReadRecordAddIn) (out *model.ReadRecordCRUDOut, err error) - // Delete removes a read record by id - Delete(ctx context.Context, in *model.ReadRecordDelIn) (out *model.ReadRecordCRUDOut, err error) - } -) - -var ( - localReadRecord IReadRecord -) - -func ReadRecord() IReadRecord { - if localReadRecord == nil { - panic("implement not found for interface IReadRecord, forgot register?") - } - return localReadRecord -} - -func RegisterReadRecord(i IReadRecord) { - localReadRecord = i -} diff --git a/internal/service/user_follow_author.go b/internal/service/user_follow_author.go new file mode 100644 index 0000000..ff2d574 --- /dev/null +++ b/internal/service/user_follow_author.go @@ -0,0 +1,39 @@ +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "server/internal/model" +) + +type ( + IUserFollowAuthor interface { + // List retrieves a paginated list of user follow authors + List(ctx context.Context, in *model.UserFollowAuthorListIn) (out *model.UserFollowAuthorListOut, err error) + // Create adds a new user follow author + Create(ctx context.Context, in *model.UserFollowAuthorAddIn) (out *model.UserFollowAuthorCRUDOut, err error) + // Delete removes a user follow author by id + Delete(ctx context.Context, in *model.UserFollowAuthorDelIn) (out *model.UserFollowAuthorCRUDOut, err error) + // Unfollow removes a user follow author by userId and authorId + Unfollow(ctx context.Context, userId int64, authorId int64) (out *model.UserFollowAuthorCRUDOut, err error) + } +) + +var ( + localUserFollowAuthor IUserFollowAuthor +) + +func UserFollowAuthor() IUserFollowAuthor { + if localUserFollowAuthor == nil { + panic("implement not found for interface IUserFollowAuthor, forgot register?") + } + return localUserFollowAuthor +} + +func RegisterUserFollowAuthor(i IUserFollowAuthor) { + localUserFollowAuthor = i +} diff --git a/internal/service/user_read_history.go b/internal/service/user_read_history.go new file mode 100644 index 0000000..1bad384 --- /dev/null +++ b/internal/service/user_read_history.go @@ -0,0 +1,35 @@ +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "server/internal/model" +) + +type ( + IUserReadHistory interface { + // Add 添加历史记录 + Add(ctx context.Context, in *model.UserReadHistoryAddIn) (out *model.UserReadHistoryCRUDOut, err error) + // Delete 批量删除历史记录 + Delete(ctx context.Context, in *model.UserReadHistoryDelIn) (out *model.UserReadHistoryCRUDOut, err error) + } +) + +var ( + localUserReadHistory IUserReadHistory +) + +func UserReadHistory() IUserReadHistory { + if localUserReadHistory == nil { + panic("implement not found for interface IUserReadHistory, forgot register?") + } + return localUserReadHistory +} + +func RegisterUserReadHistory(i IUserReadHistory) { + localUserReadHistory = i +} diff --git a/internal/service/user_read_record.go b/internal/service/user_read_record.go new file mode 100644 index 0000000..ba5f3e9 --- /dev/null +++ b/internal/service/user_read_record.go @@ -0,0 +1,37 @@ +// ================================================================================ +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// You can delete these comments if you wish manually maintain this interface file. +// ================================================================================ + +package service + +import ( + "context" + "server/internal/model" +) + +type ( + IUserReadRecord interface { + // List retrieves a paginated list of user read records + List(ctx context.Context, in *model.UserReadRecordListIn) (out *model.UserReadRecordListOut, err error) + // Create adds a new user read record + Create(ctx context.Context, in *model.UserReadRecordAddIn) (out *model.UserReadRecordCRUDOut, err error) + // Delete removes user read records by userId and bookIds + Delete(ctx context.Context, in *model.UserReadRecordDelIn) (out *model.UserReadRecordCRUDOut, err error) + } +) + +var ( + localUserReadRecord IUserReadRecord +) + +func UserReadRecord() IUserReadRecord { + if localUserReadRecord == nil { + panic("implement not found for interface IUserReadRecord, forgot register?") + } + return localUserReadRecord +} + +func RegisterUserReadRecord(i IUserReadRecord) { + localUserReadRecord = i +} diff --git a/novel.sql b/novel.sql new file mode 100644 index 0000000..6c476ca --- /dev/null +++ b/novel.sql @@ -0,0 +1,503 @@ +/* + Navicat Premium Dump SQL + + Source Server : Mac + Source Server Type : MySQL + Source Server Version : 80041 (8.0.41) + Source Host : localhost:3306 + Source Schema : novel + + Target Server Type : MySQL + Target Server Version : 80041 (8.0.41) + File Encoding : 65001 + + Date: 16/07/2025 09:37:05 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for admins +-- ---------------------------- +DROP TABLE IF EXISTS `admins`; +CREATE TABLE `admins` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '管理员ID', + `username` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '管理员用户名', + `password_hash` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码哈希', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT NULL COMMENT '更新时间', + `deleted_at` timestamp NULL DEFAULT NULL COMMENT '软删除时间戳', + PRIMARY KEY (`id`), + KEY `idx_username_deleted` (`username`,`deleted_at`), + KEY `idx_email_deleted` (`deleted_at`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='系统管理员表'; + +-- ---------------------------- +-- Records of admins +-- ---------------------------- +BEGIN; +INSERT INTO `admins` (`id`, `username`, `password_hash`, `created_at`, `updated_at`, `deleted_at`) VALUES (1, 'admin', '$2a$10$cin1jkkEiKg0GW2blAWEb.1V8QeUCHV350yZ6sTbmIC/4fdbFqnTW', '2025-07-09 13:40:54', '2025-07-09 13:40:54', NULL); +COMMIT; + +-- ---------------------------- +-- Table structure for authors +-- ---------------------------- +DROP TABLE IF EXISTS `authors`; +CREATE TABLE `authors` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '作者ID', + `user_id` bigint NOT NULL COMMENT '用户ID', + `pen_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '笔名', + `bio` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '作者简介', + `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1=正常,2=禁用', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT NULL COMMENT '更新时间', + `deleted_at` timestamp NULL DEFAULT NULL COMMENT '软删除时间戳', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_id` (`user_id`), + KEY `idx_status` (`status`), + CONSTRAINT `fk_authors_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='作者表'; + +-- ---------------------------- +-- Records of authors +-- ---------------------------- +BEGIN; +INSERT INTO `authors` (`id`, `user_id`, `pen_name`, `bio`, `status`, `created_at`, `updated_at`, `deleted_at`) VALUES (1, 6, 'MoonScribe', 'A fantasy writer crafting magical worlds and epic quests.', 1, '2023-01-11 09:30:00', '2025-07-10 14:00:00', NULL); +INSERT INTO `authors` (`id`, `user_id`, `pen_name`, `bio`, `status`, `created_at`, `updated_at`, `deleted_at`) VALUES (2, 7, 'NebulaTale', 'Sci-fi author exploring interstellar adventures and cosmic mysteries.', 1, '2023-03-16 12:00:00', '2025-06-25 10:15:00', NULL); +INSERT INTO `authors` (`id`, `user_id`, `pen_name`, `bio`, `status`, `created_at`, `updated_at`, `deleted_at`) VALUES (3, 8, 'MistWalker', 'Mystery novelist weaving intricate plots in shadowy settings.', 1, '2023-05-21 14:00:00', '2025-05-10 16:20:00', NULL); +INSERT INTO `authors` (`id`, `user_id`, `pen_name`, `bio`, `status`, `created_at`, `updated_at`, `deleted_at`) VALUES (4, 9, 'LegendWeaver', 'Epic storyteller inspired by ancient myths and heroic sagas.', 1, '2023-07-26 10:30:00', '2025-07-01 12:30:00', NULL); +INSERT INTO `authors` (`id`, `user_id`, `pen_name`, `bio`, `status`, `created_at`, `updated_at`, `deleted_at`) VALUES (5, 10, 'StarBloom', 'Romantic writer penning heartfelt stories under the stars.', 1, '2023-09-11 15:30:00', NULL, NULL); +INSERT INTO `authors` (`id`, `user_id`, `pen_name`, `bio`, `status`, `created_at`, `updated_at`, `deleted_at`) VALUES (6, 11, 'NightShade', 'Thriller and horror author delving into the darkest corners of the mind.', 2, '2023-11-06 09:00:00', '2025-06-15 09:00:00', '2025-07-05 11:00:00'); +COMMIT; + +-- ---------------------------- +-- Table structure for book_ratings +-- ---------------------------- +DROP TABLE IF EXISTS `book_ratings`; +CREATE TABLE `book_ratings` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '评分ID', + `user_id` bigint NOT NULL COMMENT '用户ID', + `book_id` bigint NOT NULL COMMENT '小说ID', + `score` decimal(4,2) NOT NULL COMMENT '评分(0~10)', + `comment` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '用户评论', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_book` (`user_id`,`book_id`), + KEY `idx_book_id` (`book_id`), + CONSTRAINT `fk_book_ratings_book_id` FOREIGN KEY (`book_id`) REFERENCES `books` (`id`), + CONSTRAINT `fk_book_ratings_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='小说评分与评论表'; + +-- ---------------------------- +-- Records of book_ratings +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for books +-- ---------------------------- +DROP TABLE IF EXISTS `books`; +CREATE TABLE `books` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '小说ID', + `author_id` bigint NOT NULL COMMENT '作者ID', + `category_id` bigint NOT NULL COMMENT '分类ID', + `title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '小说标题', + `cover_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '封面图片URL', + `description` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '小说简介', + `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1=连载中,2=完结,3=下架', + `words_count` int DEFAULT '0' COMMENT '字数', + `chapters_count` int DEFAULT '0' COMMENT '章节数', + `rating` decimal(4,2) DEFAULT '0.00' COMMENT '评分(0.00~10.00)', + `read_count` bigint DEFAULT '0' COMMENT '阅读人数', + `current_readers` bigint DEFAULT '0' COMMENT '在读人数', + `tags` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '标签(逗号分隔)', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT NULL COMMENT '更新时间', + `deleted_at` timestamp NULL DEFAULT NULL COMMENT '软删除时间戳', + `is_recommended` tinyint NOT NULL DEFAULT '0' COMMENT '是否推荐:0=否,1=是', + `is_featured` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否精选:0=否,1=是', + `language` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'zh' COMMENT '语言,如 zh=中文,en=英文,jp=日文', + PRIMARY KEY (`id`), + KEY `idx_author_id` (`author_id`), + KEY `idx_category_id` (`category_id`), + KEY `idx_status` (`status`), + CONSTRAINT `fk_books_author_id` FOREIGN KEY (`author_id`) REFERENCES `authors` (`id`), + CONSTRAINT `fk_books_category_id` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=41 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='小说表'; + +-- ---------------------------- +-- Records of books +-- ---------------------------- +BEGIN; +INSERT INTO `books` (`id`, `author_id`, `category_id`, `title`, `cover_url`, `description`, `status`, `words_count`, `chapters_count`, `rating`, `read_count`, `current_readers`, `tags`, `created_at`, `updated_at`, `deleted_at`, `is_recommended`, `is_featured`, `language`) VALUES (1, 1, 1, 'Echoes of the Lost Realm', 'https://example.com/covers/echoes_realm.jpg', 'A young sorcerer uncovers secrets of a forgotten kingdom.', 1, 160000, 48, 8.60, 13000, 0, 'fantasy,magic,adventure', '2024-02-15 10:00:00', '2025-07-10 12:00:00', NULL, 1, 0, 'en'); +INSERT INTO `books` (`id`, `author_id`, `category_id`, `title`, `cover_url`, `description`, `status`, `words_count`, `chapters_count`, `rating`, `read_count`, `current_readers`, `tags`, `created_at`, `updated_at`, `deleted_at`, `is_recommended`, `is_featured`, `language`) VALUES (2, 2, 2, 'Void Wanderers', 'https://example.com/covers/void_wanderers.jpg', 'A crew of explorers navigates uncharted galaxies.', 2, 210000, 62, 9.00, 22000, 0, 'sci-fi,space,exploration', '2023-09-10 11:00:00', '2025-06-20 13:30:00', NULL, 1, 0, 'en'); +INSERT INTO `books` (`id`, `author_id`, `category_id`, `title`, `cover_url`, `description`, `status`, `words_count`, `chapters_count`, `rating`, `read_count`, `current_readers`, `tags`, `created_at`, `updated_at`, `deleted_at`, `is_recommended`, `is_featured`, `language`) VALUES (3, 3, 3, 'Shadows of Doubt', 'https://example.com/covers/shadows_doubt.jpg', 'A detective solves a murder in a city shrouded in secrets.', 1, 100000, 32, 8.20, 7000, 0, 'mystery,detective,crime', '2024-05-25 14:00:00', '2025-07-05 09:45:00', NULL, 0, 0, 'en'); +INSERT INTO `books` (`id`, `author_id`, `category_id`, `title`, `cover_url`, `description`, `status`, `words_count`, `chapters_count`, `rating`, `read_count`, `current_readers`, `tags`, `created_at`, `updated_at`, `deleted_at`, `is_recommended`, `is_featured`, `language`) VALUES (4, 4, 4, 'The Eternal Saga', 'https://example.com/covers/eternal_saga.jpg', 'Heroes battle to fulfill an ancient prophecy.', 2, 340000, 85, 9.40, 28000, 0, 'epic,fantasy,mythology', '2023-12-01 12:00:00', '2025-07-01 11:15:00', NULL, 1, 0, 'en'); +INSERT INTO `books` (`id`, `author_id`, `category_id`, `title`, `cover_url`, `description`, `status`, `words_count`, `chapters_count`, `rating`, `read_count`, `current_readers`, `tags`, `created_at`, `updated_at`, `deleted_at`, `is_recommended`, `is_featured`, `language`) VALUES (5, 5, 5, 'Love Beyond the Stars', 'https://example.com/covers/love_stars.jpg', 'A romance blossoms during an interstellar journey.', 1, 115000, 38, 8.50, 11000, 0, 'romance,sci-fi,drama', '2024-07-15 13:00:00', '2025-07-11 02:00:00', NULL, 0, 0, 'en'); +INSERT INTO `books` (`id`, `author_id`, `category_id`, `title`, `cover_url`, `description`, `status`, `words_count`, `chapters_count`, `rating`, `read_count`, `current_readers`, `tags`, `created_at`, `updated_at`, `deleted_at`, `is_recommended`, `is_featured`, `language`) VALUES (6, 6, 7, 'Curse of the Forgotten', NULL, 'A haunted village hides a terrifying secret.', 3, 65000, 18, 6.90, 2500, 0, 'horror,supernatural,mystery', '2024-03-10 16:00:00', '2025-07-11 02:00:00', '2025-07-05 10:00:00', 0, 0, 'en'); +INSERT INTO `books` (`id`, `author_id`, `category_id`, `title`, `cover_url`, `description`, `status`, `words_count`, `chapters_count`, `rating`, `read_count`, `current_readers`, `tags`, `created_at`, `updated_at`, `deleted_at`, `is_recommended`, `is_featured`, `language`) VALUES (7, 2, 6, 'Pulse of Danger', 'https://example.com/covers/pulse_danger.jpg', 'A rogue agent uncovers a global conspiracy.', 1, 140000, 40, 8.70, 9500, 0, 'thriller,action,conspiracy', '2024-04-20 09:30:00', '2025-06-30 15:00:00', NULL, 1, 0, 'en'); +COMMIT; + +-- ---------------------------- +-- Table structure for bookshelves +-- ---------------------------- +DROP TABLE IF EXISTS `bookshelves`; +CREATE TABLE `bookshelves` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '记录ID', + `user_id` bigint NOT NULL COMMENT '用户ID', + `book_id` bigint NOT NULL COMMENT '小说ID', + `added_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '加入书架时间', + `last_read_chapter_id` bigint DEFAULT NULL COMMENT '最后阅读章节ID', + `last_read_percent` decimal(5,2) DEFAULT '0.00' COMMENT '阅读进度百分比(0.00~100.00)', + `last_read_at` timestamp NULL DEFAULT NULL COMMENT '最后阅读时间', + `read_status` tinyint NOT NULL DEFAULT '1' COMMENT '阅读状态:1=正在读,2=已读完,3=已收藏', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_book` (`user_id`,`book_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_book_id` (`book_id`), + CONSTRAINT `fk_bookshelves_book_id` FOREIGN KEY (`book_id`) REFERENCES `books` (`id`), + CONSTRAINT `fk_bookshelves_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户书架表'; + +-- ---------------------------- +-- Records of bookshelves +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for casbin_rule +-- ---------------------------- +DROP TABLE IF EXISTS `casbin_rule`; +CREATE TABLE `casbin_rule` ( + `id` int NOT NULL AUTO_INCREMENT, + `p_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `v0` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `v1` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `v2` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `v3` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `v4` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + `v5` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '', + PRIMARY KEY (`id`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=43 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='路由权限表'; + +-- ---------------------------- +-- Records of casbin_rule +-- ---------------------------- +BEGIN; +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (1, 'g', 'user', 'guest', '', '', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (2, 'g', 'author', 'user', '', '', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (3, 'g', 'admin', 'author', '', '', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (4, 'p', 'guest', '/book/app/list', 'GET', 'App获取书籍列表', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (5, 'p', 'guest', '/book/app/detail', 'GET', 'App获取书籍详情', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (6, 'p', 'guest', '/chapter/app/list', 'GET', 'App获取章节列表', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (7, 'p', 'guest', '/chapter/app/detail', 'GET', 'App获取章节详情', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (8, 'p', 'guest', '/category', 'GET', '获取分类列表', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (9, 'p', 'user', '/book/shelf/add', 'POST', '加入书架', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (10, 'p', 'user', '/book/shelf/remove', 'POST', '移除书架', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (11, 'p', 'user', '/book/history/add', 'POST', '添加历史记录', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (12, 'p', 'user', '/book/history/remove', 'POST', '删除历史记录', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (13, 'p', 'user', '/book/app/rate', 'POST', 'App用户评分', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (14, 'p', 'user', '/chapter/app/purchase', 'POST', 'App购买章节', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (15, 'p', 'user', '/chapter/app/progress', 'POST', 'App上传阅读进度', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (16, 'p', 'user', '/feedback', 'POST', '新增反馈', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (17, 'p', 'user', '/user/info', 'GET', '获取用户信息', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (18, 'p', 'user', '/user/delete', 'POST', '删除用户', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (19, 'p', 'user', '/user/logout', 'POST', '用户登出', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (20, 'p', 'user', '/author/follow', 'POST', '关注作者', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (21, 'p', 'user', '/author/unfollow', 'POST', '取消关注作者', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (22, 'p', 'author', '/book', 'GET', '获取图书列表', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (23, 'p', 'author', '/book', 'POST', '新增图书', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (24, 'p', 'author', '/book', 'PUT', '编辑图书', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (25, 'p', 'author', '/book', 'DELETE', '删除图书', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (26, 'p', 'author', '/chapter', 'GET', '获取章节列表', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (27, 'p', 'author', '/chapter', 'POST', '创建章节', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (28, 'p', 'author', '/chapter', 'PUT', '更新章节', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (29, 'p', 'author', '/chapter', 'DELETE', '删除章节', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (30, 'p', 'admin', '/author', 'GET', '获取作者列表', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (31, 'p', 'admin', '/author', 'POST', '创建作者', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (32, 'p', 'admin', '/author', 'PUT', '更新作者', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (33, 'p', 'admin', '/author', 'DELETE', '删除作者', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (34, 'p', 'admin', '/feedback', 'GET', '获取反馈列表', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (35, 'p', 'admin', '/category', 'POST', '创建分类', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (36, 'p', 'admin', '/category', 'PUT', '更新分类', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (37, 'p', 'admin', '/category', 'DELETE', '删除分类', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (38, 'p', 'admin', '/admin/info', 'GET', '获取管理员用户信息', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (39, 'p', 'admin', '/admin/editPass', 'POST', '管理员修改密码', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (40, 'p', 'user', '/book/app/my-books', 'GET', '获取我的书籍列表', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (41, 'p', 'admin', '/book/set-featured', 'POST', '设置书籍精选状态', '', ''); +INSERT INTO `casbin_rule` (`id`, `p_type`, `v0`, `v1`, `v2`, `v3`, `v4`, `v5`) VALUES (42, 'p', 'admin', '/book/set-recommended', 'POST', '设置书籍推荐状态', '', ''); +COMMIT; + +-- ---------------------------- +-- Table structure for categories +-- ---------------------------- +DROP TABLE IF EXISTS `categories`; +CREATE TABLE `categories` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '分类ID', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '分类名称', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT NULL COMMENT '更新时间', + `deleted_at` timestamp NULL DEFAULT NULL COMMENT '软删除时间戳', + `channel` tinyint(1) NOT NULL DEFAULT '1' COMMENT '频道类型:1=男频,2=女频', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_name` (`name`) +) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='小说分类表'; + +-- ---------------------------- +-- Records of categories +-- ---------------------------- +BEGIN; +INSERT INTO `categories` (`id`, `name`, `created_at`, `updated_at`, `deleted_at`, `channel`) VALUES (1, 'Fantasy', '2023-01-10 09:00:00', '2025-07-10 15:30:00', NULL, 1); +INSERT INTO `categories` (`id`, `name`, `created_at`, `updated_at`, `deleted_at`, `channel`) VALUES (2, 'Science Fiction', '2023-02-15 10:30:00', '2025-06-20 12:00:00', NULL, 1); +INSERT INTO `categories` (`id`, `name`, `created_at`, `updated_at`, `deleted_at`, `channel`) VALUES (3, 'Mystery', '2023-03-20 14:00:00', '2025-05-15 09:45:00', NULL, 1); +INSERT INTO `categories` (`id`, `name`, `created_at`, `updated_at`, `deleted_at`, `channel`) VALUES (4, 'Epic', '2023-04-05 11:15:00', '2025-07-01 16:20:00', NULL, 1); +INSERT INTO `categories` (`id`, `name`, `created_at`, `updated_at`, `deleted_at`, `channel`) VALUES (5, 'Romance', '2023-05-12 08:45:00', NULL, NULL, 1); +INSERT INTO `categories` (`id`, `name`, `created_at`, `updated_at`, `deleted_at`, `channel`) VALUES (6, 'Thriller', '2023-06-18 13:20:00', '2025-04-10 10:10:00', NULL, 1); +INSERT INTO `categories` (`id`, `name`, `created_at`, `updated_at`, `deleted_at`, `channel`) VALUES (8, 'Modern Romance', '2025-07-15 21:00:00', NULL, NULL, 2); +INSERT INTO `categories` (`id`, `name`, `created_at`, `updated_at`, `deleted_at`, `channel`) VALUES (9, 'Historical Romance', '2025-07-15 21:00:00', NULL, NULL, 2); +INSERT INTO `categories` (`id`, `name`, `created_at`, `updated_at`, `deleted_at`, `channel`) VALUES (10, 'School Life', '2025-07-15 21:00:00', NULL, NULL, 2); +INSERT INTO `categories` (`id`, `name`, `created_at`, `updated_at`, `deleted_at`, `channel`) VALUES (11, 'CEO Romance', '2025-07-15 21:00:00', NULL, NULL, 2); +INSERT INTO `categories` (`id`, `name`, `created_at`, `updated_at`, `deleted_at`, `channel`) VALUES (12, 'Time Travel', '2025-07-15 21:00:00', NULL, NULL, 2); +INSERT INTO `categories` (`id`, `name`, `created_at`, `updated_at`, `deleted_at`, `channel`) VALUES (13, 'Fantasy Romance', '2025-07-15 21:00:00', NULL, NULL, 2); +COMMIT; + +-- ---------------------------- +-- Table structure for chapters +-- ---------------------------- +DROP TABLE IF EXISTS `chapters`; +CREATE TABLE `chapters` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '章节ID', + `book_id` bigint NOT NULL COMMENT '小说ID', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '章节标题', + `content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '章节内容', + `word_count` int DEFAULT '0' COMMENT '章节字数', + `sort` int DEFAULT '0' COMMENT '排序序号', + `is_locked` tinyint NOT NULL DEFAULT '0' COMMENT '是否锁定:0=免费,1=需积分解锁', + `required_score` int NOT NULL DEFAULT '0' COMMENT '解锁该章节所需积分', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT NULL COMMENT '更新时间', + `deleted_at` timestamp NULL DEFAULT NULL COMMENT '软删除时间戳', + PRIMARY KEY (`id`), + KEY `idx_book_id` (`book_id`), + CONSTRAINT `fk_chapters_book_id` FOREIGN KEY (`book_id`) REFERENCES `books` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='章节表'; + +-- ---------------------------- +-- Records of chapters +-- ---------------------------- +BEGIN; +INSERT INTO `chapters` (`id`, `book_id`, `title`, `content`, `word_count`, `sort`, `is_locked`, `required_score`, `created_at`, `updated_at`, `deleted_at`) VALUES (1, 1, 'The Fated Dawn', 'Aelar discovers a shimmering crystal in the ruins, pulsing with ancient magic...', 3700, 1, 0, 0, '2024-02-15 10:10:00', '2025-07-11 20:23:43', NULL); +INSERT INTO `chapters` (`id`, `book_id`, `title`, `content`, `word_count`, `sort`, `is_locked`, `required_score`, `created_at`, `updated_at`, `deleted_at`) VALUES (2, 1, 'The Oracle’s Vision', 'Aelar seeks guidance from a reclusive seer in the enchanted forest...', 4000, 2, 0, 0, '2024-03-05 11:45:00', '2024-03-05 11:45:00', NULL); +INSERT INTO `chapters` (`id`, `book_id`, `title`, `content`, `word_count`, `sort`, `is_locked`, `required_score`, `created_at`, `updated_at`, `deleted_at`) VALUES (3, 1, 'The Veiled Kingdom', 'Aelar battles a mystical guardian to enter the Lost Realm...', 4200, 3, 1, 70, '2025-07-10 12:00:00', '2025-07-10 12:00:00', NULL); +INSERT INTO `chapters` (`id`, `book_id`, `title`, `content`, `word_count`, `sort`, `is_locked`, `required_score`, `created_at`, `updated_at`, `deleted_at`) VALUES (4, 2, 'The Cosmic Leap', 'The Starfarer crew prepares for a daring jump into uncharted space...', 4400, 1, 0, 0, '2023-09-10 11:10:00', '2023-09-10 11:10:00', NULL); +INSERT INTO `chapters` (`id`, `book_id`, `title`, `content`, `word_count`, `sort`, `is_locked`, `required_score`, `created_at`, `updated_at`, `deleted_at`) VALUES (5, 2, 'Anomaly Detected', 'A mysterious signal causes chaos aboard the Starfarer...', 4700, 2, 0, 0, '2023-10-25 12:30:00', '2023-10-25 12:30:00', NULL); +INSERT INTO `chapters` (`id`, `book_id`, `title`, `content`, `word_count`, `sort`, `is_locked`, `required_score`, `created_at`, `updated_at`, `deleted_at`) VALUES (6, 2, 'The Starborn Legacy', 'The crew uncovers an alien artifact that holds the key to their mission...', 5000, 3, 1, 130, '2025-06-20 13:30:00', '2025-06-20 13:30:00', NULL); +INSERT INTO `chapters` (`id`, `book_id`, `title`, `content`, `word_count`, `sort`, `is_locked`, `required_score`, `created_at`, `updated_at`, `deleted_at`) VALUES (7, 3, 'The Cryptic Note', 'Detective Lila finds a hidden message that changes the case...', 3400, 1, 0, 0, '2024-05-25 14:10:00', '2024-05-25 14:10:00', NULL); +INSERT INTO `chapters` (`id`, `book_id`, `title`, `content`, `word_count`, `sort`, `is_locked`, `required_score`, `created_at`, `updated_at`, `deleted_at`) VALUES (8, 3, 'Suspect’s Secrets', 'Lila interrogates a key witness with a dark past...', 3600, 2, 1, 50, '2024-06-20 15:15:00', '2024-06-20 15:15:00', NULL); +INSERT INTO `chapters` (`id`, `book_id`, `title`, `content`, `word_count`, `sort`, `is_locked`, `required_score`, `created_at`, `updated_at`, `deleted_at`) VALUES (9, 3, 'The Shadow’s Trail', 'A late-night pursuit leads Lila to a shocking discovery...', 3800, 3, 1, 70, '2025-07-05 09:45:00', '2025-07-05 09:45:00', NULL); +INSERT INTO `chapters` (`id`, `book_id`, `title`, `content`, `word_count`, `sort`, `is_locked`, `required_score`, `created_at`, `updated_at`, `deleted_at`) VALUES (10, 4, 'The Oracle’s Call', 'Kael is summoned to lead the fight against an ancient evil...', 5200, 1, 0, 0, '2023-12-01 12:10:00', '2023-12-01 12:10:00', NULL); +INSERT INTO `chapters` (`id`, `book_id`, `title`, `content`, `word_count`, `sort`, `is_locked`, `required_score`, `created_at`, `updated_at`, `deleted_at`) VALUES (11, 4, 'The Great Clash', 'Kael’s forces face the dark army in a battle for the ages...', 5400, 2, 0, 0, '2024-01-25 13:15:00', '2024-01-25 13:15:00', NULL); +INSERT INTO `chapters` (`id`, `book_id`, `title`, `content`, `word_count`, `sort`, `is_locked`, `required_score`, `created_at`, `updated_at`, `deleted_at`) VALUES (12, 4, 'The Prophecy’s End', 'Kael’s final stand determines the fate of the world...', 5700, 3, 1, 200, '2025-07-01 11:15:00', '2025-07-01 11:15:00', NULL); +INSERT INTO `chapters` (`id`, `book_id`, `title`, `content`, `word_count`, `sort`, `is_locked`, `required_score`, `created_at`, `updated_at`, `deleted_at`) VALUES (13, 5, 'First Glance', 'Elara and Kian meet aboard the starship, their connection instant...', 3200, 1, 0, 0, '2024-07-15 13:10:00', '2024-07-15 13:10:00', NULL); +INSERT INTO `chapters` (`id`, `book_id`, `title`, `content`, `word_count`, `sort`, `is_locked`, `required_score`, `created_at`, `updated_at`, `deleted_at`) VALUES (14, 5, 'Under the Stars', 'A quiet evening reveals the depth of Elara and Kian’s feelings...', 3400, 2, 1, 60, '2024-08-30 14:15:00', '2024-08-30 14:15:00', NULL); +INSERT INTO `chapters` (`id`, `book_id`, `title`, `content`, `word_count`, `sort`, `is_locked`, `required_score`, `created_at`, `updated_at`, `deleted_at`) VALUES (15, 5, 'Tempest of Love', 'A galactic storm threatens to tear Elara and Kian apart...', 3700, 3, 1, 80, '2025-07-11 02:00:00', '2025-07-11 02:00:00', NULL); +INSERT INTO `chapters` (`id`, `book_id`, `title`, `content`, `word_count`, `sort`, `is_locked`, `required_score`, `created_at`, `updated_at`, `deleted_at`) VALUES (16, 6, 'The Fog Descends', 'A chilling mist engulfs the village, hiding unspeakable horrors...', 2900, 1, 0, 0, '2024-03-10 16:15:00', '2024-03-10 16:15:00', '2025-07-05 10:00:00'); +INSERT INTO `chapters` (`id`, `book_id`, `title`, `content`, `word_count`, `sort`, `is_locked`, `required_score`, `created_at`, `updated_at`, `deleted_at`) VALUES (17, 6, 'The Haunted Relic', 'A cursed artifact in the village square awakens dark forces...', 3100, 2, 0, 0, '2024-04-05 17:00:00', '2024-04-05 17:00:00', '2025-07-05 10:00:00'); +INSERT INTO `chapters` (`id`, `book_id`, `title`, `content`, `word_count`, `sort`, `is_locked`, `required_score`, `created_at`, `updated_at`, `deleted_at`) VALUES (18, 6, 'Whispers in the Dark', 'The villagers hear eerie voices as the curse tightens its grip...', 3300, 3, 0, 0, '2024-04-20 17:30:00', '2024-04-20 17:30:00', '2025-07-05 10:00:00'); +INSERT INTO `chapters` (`id`, `book_id`, `title`, `content`, `word_count`, `sort`, `is_locked`, `required_score`, `created_at`, `updated_at`, `deleted_at`) VALUES (19, 7, 'The Covert Operation', 'Agent Jace infiltrates a secret facility with a deadly agenda...', 3700, 1, 0, 0, '2024-04-20 09:45:00', '2024-04-20 09:45:00', NULL); +INSERT INTO `chapters` (`id`, `book_id`, `title`, `content`, `word_count`, `sort`, `is_locked`, `required_score`, `created_at`, `updated_at`, `deleted_at`) VALUES (20, 7, 'The Hidden Truth', 'Jace uncovers evidence of a global conspiracy...', 3900, 2, 1, 80, '2024-05-20 10:30:00', '2024-05-20 10:30:00', NULL); +INSERT INTO `chapters` (`id`, `book_id`, `title`, `content`, `word_count`, `sort`, `is_locked`, `required_score`, `created_at`, `updated_at`, `deleted_at`) VALUES (21, 7, 'The Final Countdown', 'Jace races to stop a catastrophic event...', 4100, 3, 1, 90, '2025-06-30 15:00:00', '2025-06-30 15:00:00', NULL); +COMMIT; + +-- ---------------------------- +-- Table structure for feedbacks +-- ---------------------------- +DROP TABLE IF EXISTS `feedbacks`; +CREATE TABLE `feedbacks` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '反馈ID', + `user_id` bigint NOT NULL COMMENT '用户ID', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '反馈内容', + `status` tinyint NOT NULL DEFAULT '1' COMMENT '处理状态:1=未处理,2=处理中,3=已处理', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '反馈时间', + `updated_at` timestamp NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + CONSTRAINT `fk_feedbacks_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户反馈表'; + +-- ---------------------------- +-- Records of feedbacks +-- ---------------------------- +BEGIN; +INSERT INTO `feedbacks` (`id`, `user_id`, `content`, `status`, `created_at`, `updated_at`) VALUES (1, 1, 'App crashes during photo upload.', 1, '2025-07-01 10:15:23', NULL); +INSERT INTO `feedbacks` (`id`, `user_id`, `content`, `status`, `created_at`, `updated_at`) VALUES (2, 2, 'Please add dark mode support.', 2, '2025-07-02 14:30:45', '2025-07-05 09:20:10'); +INSERT INTO `feedbacks` (`id`, `user_id`, `content`, `status`, `created_at`, `updated_at`) VALUES (3, 3, 'Slow login process needs optimization.', 3, '2025-07-03 08:45:12', '2025-07-06 16:00:00'); +INSERT INTO `feedbacks` (`id`, `user_id`, `content`, `status`, `created_at`, `updated_at`) VALUES (4, 4, 'More filter options would be great.', 1, '2025-07-04 19:22:33', NULL); +INSERT INTO `feedbacks` (`id`, `user_id`, `content`, `status`, `created_at`, `updated_at`) VALUES (5, 5, 'Payment section has a bug.', 2, '2025-07-05 11:10:50', '2025-07-07 13:45:22'); +COMMIT; + +-- ---------------------------- +-- Table structure for user_chapter_purchases +-- ---------------------------- +DROP TABLE IF EXISTS `user_chapter_purchases`; +CREATE TABLE `user_chapter_purchases` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '购买记录ID', + `user_id` bigint NOT NULL COMMENT '用户ID', + `book_id` bigint NOT NULL COMMENT '小说ID', + `chapter_id` bigint NOT NULL COMMENT '章节ID', + `points_used` int NOT NULL COMMENT '消耗积分数', + `purchase_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '购买时间', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_book_id` (`book_id`), + KEY `idx_chapter_id` (`chapter_id`), + CONSTRAINT `fk_user_chapter_purchases_book_id` FOREIGN KEY (`book_id`) REFERENCES `books` (`id`), + CONSTRAINT `fk_user_chapter_purchases_chapter_id` FOREIGN KEY (`chapter_id`) REFERENCES `chapters` (`id`), + CONSTRAINT `fk_user_chapter_purchases_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户章节购买记录表'; + +-- ---------------------------- +-- Records of user_chapter_purchases +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for user_follow_authors +-- ---------------------------- +DROP TABLE IF EXISTS `user_follow_authors`; +CREATE TABLE `user_follow_authors` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '关注ID', + `user_id` bigint NOT NULL COMMENT '用户ID', + `author_id` bigint NOT NULL COMMENT '作者ID', + `followed_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '关注时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_author` (`user_id`,`author_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_author_id` (`author_id`), + CONSTRAINT `fk_user_follow_authors_author_id` FOREIGN KEY (`author_id`) REFERENCES `authors` (`id`), + CONSTRAINT `fk_user_follow_authors_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户关注作者表'; + +-- ---------------------------- +-- Records of user_follow_authors +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for user_points_logs +-- ---------------------------- +DROP TABLE IF EXISTS `user_points_logs`; +CREATE TABLE `user_points_logs` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '积分流水ID', + `user_id` bigint NOT NULL COMMENT '用户ID', + `change_type` tinyint NOT NULL COMMENT '变动类型,1=消费(spend), 2=收入(earn)', + `points_change` int NOT NULL COMMENT '积分变化数,正数增加,负数减少', + `related_order_id` bigint DEFAULT NULL COMMENT '关联ID:当change_type=1时,为chapter_purchases.id;当change_type=2时,为advertisement_records.id', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '变动说明', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '变动时间', + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_related_order_id` (`related_order_id`), + CONSTRAINT `fk_user_points_logs_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户积分流水表'; + +-- ---------------------------- +-- Records of user_points_logs +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for user_read_history +-- ---------------------------- +DROP TABLE IF EXISTS `user_read_history`; +CREATE TABLE `user_read_history` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '历史记录ID', + `user_id` bigint NOT NULL COMMENT '用户ID', + `book_id` bigint NOT NULL COMMENT '小说ID', + `chapter_id` bigint NOT NULL COMMENT '最后阅读章节ID', + `read_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '最后阅读时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_book` (`user_id`,`book_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_book_id` (`book_id`), + KEY `fk_user_read_history_chapter_id` (`chapter_id`), + CONSTRAINT `fk_user_read_history_book_id` FOREIGN KEY (`book_id`) REFERENCES `books` (`id`), + CONSTRAINT `fk_user_read_history_chapter_id` FOREIGN KEY (`chapter_id`) REFERENCES `chapters` (`id`), + CONSTRAINT `fk_user_read_history_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户阅读历史记录表'; + +-- ---------------------------- +-- Records of user_read_history +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for user_read_records +-- ---------------------------- +DROP TABLE IF EXISTS `user_read_records`; +CREATE TABLE `user_read_records` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '记录ID', + `user_id` bigint NOT NULL COMMENT '用户ID', + `book_id` bigint NOT NULL COMMENT '小说ID', + `chapter_id` bigint NOT NULL COMMENT '章节ID', + `progress` int NOT NULL DEFAULT '0' COMMENT '阅读进度百分比(0-100)', + `read_at` datetime NOT NULL COMMENT '阅读时间', + `created_at` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_user_book_chapter` (`user_id`,`book_id`,`chapter_id`), + KEY `idx_user_id` (`user_id`), + KEY `idx_book_id` (`book_id`), + KEY `idx_chapter_id` (`chapter_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户阅读记录表'; + +-- ---------------------------- +-- Records of user_read_records +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for users +-- ---------------------------- +DROP TABLE IF EXISTS `users`; +CREATE TABLE `users` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID', + `username` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名', + `password_hash` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '密码哈希', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '头像URL', + `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '邮箱', + `points` bigint unsigned NOT NULL DEFAULT '0' COMMENT '积分', + `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间', + `updated_at` timestamp NULL DEFAULT NULL COMMENT '更新时间', + `deleted_at` timestamp NULL DEFAULT NULL COMMENT '软删除时间戳', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_username` (`username`), + KEY `idx_email_deleted` (`email`,`deleted_at`) +) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户表'; + +-- ---------------------------- +-- Records of users +-- ---------------------------- +BEGIN; +INSERT INTO `users` (`id`, `username`, `password_hash`, `avatar`, `email`, `points`, `created_at`, `updated_at`, `deleted_at`) VALUES (1, 'john_doe', '$2a$10$cin1jkkEiKg0GW2blAWEb.1V8QeUCHV350yZ6sTbmIC/4fdbFqnTW', 'https://example.com/avatars/john.jpg', 'john.doe@example.com', 150, '2025-06-01 09:00:00', '2025-07-05 14:20:30', NULL); +INSERT INTO `users` (`id`, `username`, `password_hash`, `avatar`, `email`, `points`, `created_at`, `updated_at`, `deleted_at`) VALUES (2, 'jane_smith', '$2a$10$cin1jkkEiKg0GW2blAWEb.1V8QeUCHV350yZ6sTbmIC/4fdbFqnTW', 'https://example.com/avatars/jane.jpg', 'jane.smith@example.com', 320, '2025-06-02 12:15:45', '2025-07-06 10:10:10', NULL); +INSERT INTO `users` (`id`, `username`, `password_hash`, `avatar`, `email`, `points`, `created_at`, `updated_at`, `deleted_at`) VALUES (3, 'alice_wong', '$2a$10$cin1jkkEiKg0GW2blAWEb.1V8QeUCHV350yZ6sTbmIC/4fdbFqnTW', NULL, 'alice.wong@example.com', 50, '2025-06-03 15:30:22', NULL, NULL); +INSERT INTO `users` (`id`, `username`, `password_hash`, `avatar`, `email`, `points`, `created_at`, `updated_at`, `deleted_at`) VALUES (4, 'bob_lee', '$2a$10$cin1jkkEiKg0GW2blAWEb.1V8QeUCHV350yZ6sTbmIC/4fdbFqnTW', 'https://example.com/avatars/bob.jpg', 'bob.lee@example.com', 200, '2025-06-04 08:45:00', '2025-07-07 16:00:00', NULL); +INSERT INTO `users` (`id`, `username`, `password_hash`, `avatar`, `email`, `points`, `created_at`, `updated_at`, `deleted_at`) VALUES (5, 'emma_brown', '$2a$10$cin1jkkEiKg0GW2blAWEb.1V8QeUCHV350yZ6sTbmIC/4fdbFqnTW', NULL, 'emma.brown@example.com', 0, '2025-06-05 11:20:15', NULL, NULL); +INSERT INTO `users` (`id`, `username`, `password_hash`, `avatar`, `email`, `points`, `created_at`, `updated_at`, `deleted_at`) VALUES (6, 'MoonScribe', '$2a$10$cin1jkkEiKg0GW2blAWEb.1V8QeUCHV350yZ6sTbmIC/4fdbFqnTW', 'https://example.com/avatars/moonscribe.jpg', 'moonscribe@example.com', 1800, '2023-01-10 09:00:00', '2025-07-10 14:00:00', NULL); +INSERT INTO `users` (`id`, `username`, `password_hash`, `avatar`, `email`, `points`, `created_at`, `updated_at`, `deleted_at`) VALUES (7, 'NebulaTale', '$2a$10$cin1jkkEiKg0GW2blAWEb.1V8QeUCHV350yZ6sTbmIC/4fdbFqnTW', 'https://example.com/avatars/nebulatale.jpg', 'nebulatale@example.com', 2500, '2023-03-15 11:30:00', '2025-06-25 10:15:00', NULL); +INSERT INTO `users` (`id`, `username`, `password_hash`, `avatar`, `email`, `points`, `created_at`, `updated_at`, `deleted_at`) VALUES (8, 'MistWalker', '$2a$10$cin1jkkEiKg0GW2blAWEb.1V8QeUCHV350yZ6sTbmIC/4fdbFqnTW', NULL, 'mistwalker@example.com', 900, '2023-05-20 13:45:00', '2025-05-10 16:20:00', NULL); +INSERT INTO `users` (`id`, `username`, `password_hash`, `avatar`, `email`, `points`, `created_at`, `updated_at`, `deleted_at`) VALUES (9, 'LegendWeaver', '$2a$10$cin1jkkEiKg0GW2blAWEb.1V8QeUCHV350yZ6sTbmIC/4fdbFqnTW', 'https://example.com/avatars/legendweaver.jpg', 'legendweaver@example.com', 3200, '2023-07-25 10:00:00', '2025-07-01 12:30:00', NULL); +INSERT INTO `users` (`id`, `username`, `password_hash`, `avatar`, `email`, `points`, `created_at`, `updated_at`, `deleted_at`) VALUES (10, 'StarBloom', '$2a$10$cin1jkkEiKg0GW2blAWEb.1V8QeUCHV350yZ6sTbmIC/4fdbFqnTW', 'https://example.com/avatars/starbloom.jpg', 'starbloom@example.com', 1400, '2023-09-10 15:00:00', NULL, NULL); +INSERT INTO `users` (`id`, `username`, `password_hash`, `avatar`, `email`, `points`, `created_at`, `updated_at`, `deleted_at`) VALUES (11, 'NightShade', '$2a$10$cin1jkkEiKg0GW2blAWEb.1V8QeUCHV350yZ6sTbmIC/4fdbFqnTW', NULL, 'nightshade@example.com', 700, '2023-11-05 08:30:00', '2025-06-15 09:00:00', '2025-07-05 11:00:00'); +COMMIT; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/noveltest b/noveltest new file mode 100755 index 0000000000000000000000000000000000000000..4fd67be111913981416c4fd4c7f2023d30bd72a9 GIT binary patch literal 34038802 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