From 0dfbbe5939d53542d5d2052a3cc4151701b231ec Mon Sep 17 00:00:00 2001 From: luorijun Date: Mon, 1 Jun 2026 15:31:25 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=96=87=E7=AB=A0=E4=B8=8E?= =?UTF-8?q?=E5=88=86=E7=BB=84=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 - cmd/gen/main.go | 2 + scripts/sql/fill.sql | 14 +- scripts/sql/init.sql | 60 +++++ web/core/scopes.go | 8 + web/handlers/article.go | 128 +++++++++ web/handlers/article_group.go | 90 +++++++ web/models/article.go | 23 ++ web/models/article_group.go | 20 ++ web/queries/article.gen.go | 442 +++++++++++++++++++++++++++++++ web/queries/article_group.gen.go | 347 ++++++++++++++++++++++++ web/queries/gen.go | 16 ++ web/routes.go | 20 ++ web/services/article.go | 257 ++++++++++++++++++ web/services/article_group.go | 128 +++++++++ 15 files changed, 1554 insertions(+), 3 deletions(-) create mode 100644 web/handlers/article.go create mode 100644 web/handlers/article_group.go create mode 100644 web/models/article.go create mode 100644 web/models/article_group.go create mode 100644 web/queries/article.gen.go create mode 100644 web/queries/article_group.gen.go create mode 100644 web/services/article.go create mode 100644 web/services/article_group.go diff --git a/README.md b/README.md index 071c989..84d0362 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ --- -用反射实现环境变量解析,以简化函数签名 - 错误提示增强,展示整链路信息 交易信息持久化 diff --git a/cmd/gen/main.go b/cmd/gen/main.go index 21f5fe4..e031068 100644 --- a/cmd/gen/main.go +++ b/cmd/gen/main.go @@ -35,6 +35,8 @@ func main() { m.Admin{}, m.AdminRole{}, m.Announcement{}, + m.Article{}, + m.ArticleGroup{}, m.Bill{}, m.Channel{}, m.Client{}, diff --git a/scripts/sql/fill.sql b/scripts/sql/fill.sql index 3f9a67b..833eeb0 100644 --- a/scripts/sql/fill.sql +++ b/scripts/sql/fill.sql @@ -128,7 +128,9 @@ insert into permission (name, description, sort) values ('bill', '账单', 13), ('balance_activity', '余额变动', 14), ('proxy', '代理', 15), - ('coupon_user', '已发放优惠券', 16); + ('coupon_user', '已发放优惠券', 16), + ('article', '文档', 17), + ('article_group', '文档分组', 18); -- -------------------------- -- level 2 @@ -215,6 +217,16 @@ insert into permission (parent_id, name, description, sort) values ((select id from permission where name = 'coupon_user' and deleted_at is null), 'coupon_user:read', '读取已发放优惠券列表', 1), ((select id from permission where name = 'coupon_user' and deleted_at is null), 'coupon_user:write', '编辑已发放优惠券', 2); +-- article 子权限 +insert into permission (parent_id, name, description, sort) values + ((select id from permission where name = 'article' and deleted_at is null), 'article:read', '读取文档列表', 1), + ((select id from permission where name = 'article' and deleted_at is null), 'article:write', '编辑文档', 2); + +-- article_group 子权限 +insert into permission (parent_id, name, description, sort) values + ((select id from permission where name = 'article_group' and deleted_at is null), 'article_group:read', '读取文档分组列表', 1), + ((select id from permission where name = 'article_group' and deleted_at is null), 'article_group:write', '编辑文档分组', 2); + -- -------------------------- -- level 3 -- -------------------------- diff --git a/scripts/sql/init.sql b/scripts/sql/init.sql index b29b906..7a98b2a 100644 --- a/scripts/sql/init.sql +++ b/scripts/sql/init.sql @@ -143,6 +143,62 @@ comment on column announcement.created_at is '创建时间'; comment on column announcement.updated_at is '更新时间'; comment on column announcement.deleted_at is '删除时间'; +-- article_group +drop table if exists article_group cascade; +create table article_group ( + id int generated by default as identity primary key, + name text not null, + code text not null, + sort int not null default 0, + status int not null default 1, + created_at timestamptz default current_timestamp, + updated_at timestamptz default current_timestamp, + deleted_at timestamptz +); +create unique index udx_article_group_code on article_group (code) where deleted_at is null; +create index idx_article_group_status on article_group (status) where deleted_at is null; +create index idx_article_group_sort on article_group (sort) where deleted_at is null; + +-- article_group表字段注释 +comment on table article_group is '文章分组表'; +comment on column article_group.id is '分组ID'; +comment on column article_group.name is '分组名称'; +comment on column article_group.code is '分组编码'; +comment on column article_group.sort is '分组排序'; +comment on column article_group.status is '分组状态:0-禁用,1-正常'; +comment on column article_group.created_at is '创建时间'; +comment on column article_group.updated_at is '更新时间'; +comment on column article_group.deleted_at is '删除时间'; + +-- article +drop table if exists article cascade; +create table article ( + id int generated by default as identity primary key, + group_id int not null, + title text not null, + content text, + sort int not null default 0, + status int not null default 1, + created_at timestamptz default current_timestamp, + updated_at timestamptz default current_timestamp, + deleted_at timestamptz +); +create index idx_article_group_id on article (group_id) where deleted_at is null; +create index idx_article_status on article (status) where deleted_at is null; +create index idx_article_sort on article (sort) where deleted_at is null; + +-- article表字段注释 +comment on table article is '文章表'; +comment on column article.id is '文章ID'; +comment on column article.group_id is '分组ID'; +comment on column article.title is '文章标题'; +comment on column article.content is '文章内容'; +comment on column article.sort is '文章排序'; +comment on column article.status is '文章状态:0-禁用,1-正常'; +comment on column article.created_at is '创建时间'; +comment on column article.updated_at is '更新时间'; +comment on column article.deleted_at is '删除时间'; + -- inquiry drop table if exists inquiry cascade; create table inquiry ( @@ -1228,6 +1284,10 @@ alter table coupon_user alter table coupon_user add constraint fk_coupon_user_coupon_id foreign key (coupon_id) references coupon (id) on delete cascade; +-- article表外键 +alter table article + add constraint fk_article_group_id foreign key (group_id) references article_group (id) on delete restrict; + -- product_sku表外键 alter table product_sku add constraint fk_product_sku_product_id foreign key (product_id) references product (id) on delete cascade; diff --git a/web/core/scopes.go b/web/core/scopes.go index a423d95..94f084f 100644 --- a/web/core/scopes.go +++ b/web/core/scopes.go @@ -74,6 +74,14 @@ const ( ScopeProxyWrite = string("proxy:write") // 写入代理 ScopeProxyWriteStatus = string("proxy:write:status") // 更改代理状态 + ScopeArticle = string("article") // 文档 + ScopeArticleRead = string("article:read") // 读取文档列表 + ScopeArticleWrite = string("article:write") // 写入文档 + + ScopeArticleGroup = string("article_group") // 文档分组 + ScopeArticleGroupRead = string("article_group:read") // 读取文档分组列表 + ScopeArticleGroupWrite = string("article_group:write") // 写入文档分组 + ScopeTrade = string("trade") // 交易 ScopeTradeRead = string("trade:read") // 读取交易列表 ScopeTradeReadOfUser = string("trade:read:of_user") // 读取指定用户的交易列表 diff --git a/web/handlers/article.go b/web/handlers/article.go new file mode 100644 index 0000000..aea8fef --- /dev/null +++ b/web/handlers/article.go @@ -0,0 +1,128 @@ +package handlers + +import ( + "platform/web/auth" + "platform/web/core" + g "platform/web/globals" + s "platform/web/services" + + "github.com/gofiber/fiber/v2" +) + +func NavArticle(c *fiber.Ctx) error { + _, err := auth.GetAuthCtx(c).PermitOfficialClient() + if err != nil { + return err + } + + list, err := s.Article.Nav() + if err != nil { + return err + } + + return c.JSON(list) +} + +func GetArticle(c *fiber.Ctx) error { + _, err := auth.GetAuthCtx(c).PermitOfficialClient() + if err != nil { + return err + } + + var req core.IdReq + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + article, err := s.Article.GetPublic(req.Id) + if err != nil { + return err + } + + return c.JSON(article) +} + +func PageArticleByAdmin(c *fiber.Ctx) error { + _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleRead) + if err != nil { + return err + } + + var req s.PageArticleReq + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + list, total, err := s.Article.Page(&req) + if err != nil { + return err + } + + return c.JSON(core.PageResp{ + Total: int(total), + Page: req.GetPage(), + Size: req.GetSize(), + List: list, + }) +} + +func GetArticleByAdmin(c *fiber.Ctx) error { + _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleRead) + if err != nil { + return err + } + + var req core.IdReq + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + article, err := s.Article.GetByAdmin(req.Id) + if err != nil { + return err + } + + return c.JSON(article) +} + +func CreateArticle(c *fiber.Ctx) error { + _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleWrite) + if err != nil { + return err + } + + var req s.CreateArticleData + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + return s.Article.Create(req) +} + +func UpdateArticle(c *fiber.Ctx) error { + _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleWrite) + if err != nil { + return err + } + + var req s.UpdateArticleData + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + return s.Article.Update(req) +} + +func DeleteArticle(c *fiber.Ctx) error { + _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleWrite) + if err != nil { + return err + } + + var req core.IdReq + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + return s.Article.Delete(req.Id) +} diff --git a/web/handlers/article_group.go b/web/handlers/article_group.go new file mode 100644 index 0000000..62d2ee7 --- /dev/null +++ b/web/handlers/article_group.go @@ -0,0 +1,90 @@ +package handlers + +import ( + "platform/web/auth" + "platform/web/core" + g "platform/web/globals" + s "platform/web/services" + + "github.com/gofiber/fiber/v2" +) + +func PageArticleGroupByAdmin(c *fiber.Ctx) error { + _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleGroupRead) + if err != nil { + return err + } + + var req s.PageArticleGroupReq + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + list, total, err := s.ArticleGroup.Page(&req) + if err != nil { + return err + } + + return c.JSON(core.PageResp{ + Total: int(total), + Page: req.GetPage(), + Size: req.GetSize(), + List: list, + }) +} + +func ListArticleGroupByAdmin(c *fiber.Ctx) error { + _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleGroupRead) + if err != nil { + return err + } + + list, err := s.ArticleGroup.All() + if err != nil { + return err + } + + return c.JSON(list) +} + +func CreateArticleGroup(c *fiber.Ctx) error { + _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleGroupWrite) + if err != nil { + return err + } + + var req s.CreateArticleGroupData + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + return s.ArticleGroup.Create(req) +} + +func UpdateArticleGroup(c *fiber.Ctx) error { + _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleGroupWrite) + if err != nil { + return err + } + + var req s.UpdateArticleGroupData + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + return s.ArticleGroup.Update(req) +} + +func DeleteArticleGroup(c *fiber.Ctx) error { + _, err := auth.GetAuthCtx(c).PermitAdmin(core.ScopeArticleGroupWrite) + if err != nil { + return err + } + + var req core.IdReq + if err := g.Validator.ParseBody(c, &req); err != nil { + return err + } + + return s.ArticleGroup.Delete(req.Id) +} diff --git a/web/models/article.go b/web/models/article.go new file mode 100644 index 0000000..078e3b1 --- /dev/null +++ b/web/models/article.go @@ -0,0 +1,23 @@ +package models + +import "platform/web/core" + +// Article 文章表 +type Article struct { + core.Model + GroupID int32 `json:"group_id" gorm:"column:group_id"` // 分组ID + Title string `json:"title" gorm:"column:title"` // 文章标题 + Content *string `json:"content,omitempty" gorm:"column:content"` // 文章内容 + Sort int32 `json:"sort" gorm:"column:sort"` // 文章排序 + Status ArticleStatus `json:"status" gorm:"column:status"` // 文章状态:0-禁用,1-正常 + + Group *ArticleGroup `json:"group,omitempty" gorm:"foreignKey:GroupID"` // 分组 +} + +// ArticleStatus 文章状态 +type ArticleStatus int + +const ( + ArticleStatusDisabled ArticleStatus = 0 // 禁用 + ArticleStatusEnabled ArticleStatus = 1 // 正常 +) diff --git a/web/models/article_group.go b/web/models/article_group.go new file mode 100644 index 0000000..cf18df5 --- /dev/null +++ b/web/models/article_group.go @@ -0,0 +1,20 @@ +package models + +import "platform/web/core" + +// ArticleGroup 文章分组表 +type ArticleGroup struct { + core.Model + Name string `json:"name" gorm:"column:name"` // 分组名称 + Code string `json:"code" gorm:"column:code"` // 分组编码 + Sort int32 `json:"sort" gorm:"column:sort"` // 分组排序 + Status ArticleGroupStatus `json:"status" gorm:"column:status"` // 分组状态:0-禁用,1-正常 +} + +// ArticleGroupStatus 分组状态 +type ArticleGroupStatus int + +const ( + ArticleGroupStatusDisabled ArticleGroupStatus = 0 // 禁用 + ArticleGroupStatusEnabled ArticleGroupStatus = 1 // 正常 +) diff --git a/web/queries/article.gen.go b/web/queries/article.gen.go new file mode 100644 index 0000000..617a515 --- /dev/null +++ b/web/queries/article.gen.go @@ -0,0 +1,442 @@ +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. + +package queries + +import ( + "context" + + "gorm.io/gorm" + "gorm.io/gorm/clause" + "gorm.io/gorm/schema" + + "gorm.io/gen" + "gorm.io/gen/field" + + "gorm.io/plugin/dbresolver" + + "platform/web/models" +) + +func newArticle(db *gorm.DB, opts ...gen.DOOption) article { + _article := article{} + + _article.articleDo.UseDB(db, opts...) + _article.articleDo.UseModel(&models.Article{}) + + tableName := _article.articleDo.TableName() + _article.ALL = field.NewAsterisk(tableName) + _article.ID = field.NewInt32(tableName, "id") + _article.CreatedAt = field.NewTime(tableName, "created_at") + _article.UpdatedAt = field.NewTime(tableName, "updated_at") + _article.DeletedAt = field.NewField(tableName, "deleted_at") + _article.GroupID = field.NewInt32(tableName, "group_id") + _article.Title = field.NewString(tableName, "title") + _article.Content = field.NewString(tableName, "content") + _article.Sort = field.NewInt32(tableName, "sort") + _article.Status = field.NewInt(tableName, "status") + _article.Group = articleBelongsToGroup{ + db: db.Session(&gorm.Session{}), + + RelationField: field.NewRelation("Group", "models.ArticleGroup"), + } + + _article.fillFieldMap() + + return _article +} + +type article struct { + articleDo + + ALL field.Asterisk + ID field.Int32 + CreatedAt field.Time + UpdatedAt field.Time + DeletedAt field.Field + GroupID field.Int32 + Title field.String + Content field.String + Sort field.Int32 + Status field.Int + Group articleBelongsToGroup + + fieldMap map[string]field.Expr +} + +func (a article) Table(newTableName string) *article { + a.articleDo.UseTable(newTableName) + return a.updateTableName(newTableName) +} + +func (a article) As(alias string) *article { + a.articleDo.DO = *(a.articleDo.As(alias).(*gen.DO)) + return a.updateTableName(alias) +} + +func (a *article) updateTableName(table string) *article { + a.ALL = field.NewAsterisk(table) + a.ID = field.NewInt32(table, "id") + a.CreatedAt = field.NewTime(table, "created_at") + a.UpdatedAt = field.NewTime(table, "updated_at") + a.DeletedAt = field.NewField(table, "deleted_at") + a.GroupID = field.NewInt32(table, "group_id") + a.Title = field.NewString(table, "title") + a.Content = field.NewString(table, "content") + a.Sort = field.NewInt32(table, "sort") + a.Status = field.NewInt(table, "status") + + a.fillFieldMap() + + return a +} + +func (a *article) GetFieldByName(fieldName string) (field.OrderExpr, bool) { + _f, ok := a.fieldMap[fieldName] + if !ok || _f == nil { + return nil, false + } + _oe, ok := _f.(field.OrderExpr) + return _oe, ok +} + +func (a *article) fillFieldMap() { + a.fieldMap = make(map[string]field.Expr, 10) + a.fieldMap["id"] = a.ID + a.fieldMap["created_at"] = a.CreatedAt + a.fieldMap["updated_at"] = a.UpdatedAt + a.fieldMap["deleted_at"] = a.DeletedAt + a.fieldMap["group_id"] = a.GroupID + a.fieldMap["title"] = a.Title + a.fieldMap["content"] = a.Content + a.fieldMap["sort"] = a.Sort + a.fieldMap["status"] = a.Status + +} + +func (a article) clone(db *gorm.DB) article { + a.articleDo.ReplaceConnPool(db.Statement.ConnPool) + a.Group.db = db.Session(&gorm.Session{Initialized: true}) + a.Group.db.Statement.ConnPool = db.Statement.ConnPool + return a +} + +func (a article) replaceDB(db *gorm.DB) article { + a.articleDo.ReplaceDB(db) + a.Group.db = db.Session(&gorm.Session{}) + return a +} + +type articleBelongsToGroup struct { + db *gorm.DB + + field.RelationField +} + +func (a articleBelongsToGroup) Where(conds ...field.Expr) *articleBelongsToGroup { + if len(conds) == 0 { + return &a + } + + exprs := make([]clause.Expression, 0, len(conds)) + for _, cond := range conds { + exprs = append(exprs, cond.BeCond().(clause.Expression)) + } + a.db = a.db.Clauses(clause.Where{Exprs: exprs}) + return &a +} + +func (a articleBelongsToGroup) WithContext(ctx context.Context) *articleBelongsToGroup { + a.db = a.db.WithContext(ctx) + return &a +} + +func (a articleBelongsToGroup) Session(session *gorm.Session) *articleBelongsToGroup { + a.db = a.db.Session(session) + return &a +} + +func (a articleBelongsToGroup) Model(m *models.Article) *articleBelongsToGroupTx { + return &articleBelongsToGroupTx{a.db.Model(m).Association(a.Name())} +} + +func (a articleBelongsToGroup) Unscoped() *articleBelongsToGroup { + a.db = a.db.Unscoped() + return &a +} + +type articleBelongsToGroupTx struct{ tx *gorm.Association } + +func (a articleBelongsToGroupTx) Find() (result *models.ArticleGroup, err error) { + return result, a.tx.Find(&result) +} + +func (a articleBelongsToGroupTx) Append(values ...*models.ArticleGroup) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Append(targetValues...) +} + +func (a articleBelongsToGroupTx) Replace(values ...*models.ArticleGroup) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Replace(targetValues...) +} + +func (a articleBelongsToGroupTx) Delete(values ...*models.ArticleGroup) (err error) { + targetValues := make([]interface{}, len(values)) + for i, v := range values { + targetValues[i] = v + } + return a.tx.Delete(targetValues...) +} + +func (a articleBelongsToGroupTx) Clear() error { + return a.tx.Clear() +} + +func (a articleBelongsToGroupTx) Count() int64 { + return a.tx.Count() +} + +func (a articleBelongsToGroupTx) Unscoped() *articleBelongsToGroupTx { + a.tx = a.tx.Unscoped() + return &a +} + +type articleDo struct{ gen.DO } + +func (a articleDo) Debug() *articleDo { + return a.withDO(a.DO.Debug()) +} + +func (a articleDo) WithContext(ctx context.Context) *articleDo { + return a.withDO(a.DO.WithContext(ctx)) +} + +func (a articleDo) ReadDB() *articleDo { + return a.Clauses(dbresolver.Read) +} + +func (a articleDo) WriteDB() *articleDo { + return a.Clauses(dbresolver.Write) +} + +func (a articleDo) Session(config *gorm.Session) *articleDo { + return a.withDO(a.DO.Session(config)) +} + +func (a articleDo) Clauses(conds ...clause.Expression) *articleDo { + return a.withDO(a.DO.Clauses(conds...)) +} + +func (a articleDo) Returning(value interface{}, columns ...string) *articleDo { + return a.withDO(a.DO.Returning(value, columns...)) +} + +func (a articleDo) Not(conds ...gen.Condition) *articleDo { + return a.withDO(a.DO.Not(conds...)) +} + +func (a articleDo) Or(conds ...gen.Condition) *articleDo { + return a.withDO(a.DO.Or(conds...)) +} + +func (a articleDo) Select(conds ...field.Expr) *articleDo { + return a.withDO(a.DO.Select(conds...)) +} + +func (a articleDo) Where(conds ...gen.Condition) *articleDo { + return a.withDO(a.DO.Where(conds...)) +} + +func (a articleDo) Order(conds ...field.Expr) *articleDo { + return a.withDO(a.DO.Order(conds...)) +} + +func (a articleDo) Distinct(cols ...field.Expr) *articleDo { + return a.withDO(a.DO.Distinct(cols...)) +} + +func (a articleDo) Omit(cols ...field.Expr) *articleDo { + return a.withDO(a.DO.Omit(cols...)) +} + +func (a articleDo) Join(table schema.Tabler, on ...field.Expr) *articleDo { + return a.withDO(a.DO.Join(table, on...)) +} + +func (a articleDo) LeftJoin(table schema.Tabler, on ...field.Expr) *articleDo { + return a.withDO(a.DO.LeftJoin(table, on...)) +} + +func (a articleDo) RightJoin(table schema.Tabler, on ...field.Expr) *articleDo { + return a.withDO(a.DO.RightJoin(table, on...)) +} + +func (a articleDo) Group(cols ...field.Expr) *articleDo { + return a.withDO(a.DO.Group(cols...)) +} + +func (a articleDo) Having(conds ...gen.Condition) *articleDo { + return a.withDO(a.DO.Having(conds...)) +} + +func (a articleDo) Limit(limit int) *articleDo { + return a.withDO(a.DO.Limit(limit)) +} + +func (a articleDo) Offset(offset int) *articleDo { + return a.withDO(a.DO.Offset(offset)) +} + +func (a articleDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *articleDo { + return a.withDO(a.DO.Scopes(funcs...)) +} + +func (a articleDo) Unscoped() *articleDo { + return a.withDO(a.DO.Unscoped()) +} + +func (a articleDo) Create(values ...*models.Article) error { + if len(values) == 0 { + return nil + } + return a.DO.Create(values) +} + +func (a articleDo) CreateInBatches(values []*models.Article, batchSize int) error { + return a.DO.CreateInBatches(values, batchSize) +} + +// Save : !!! underlying implementation is different with GORM +// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values) +func (a articleDo) Save(values ...*models.Article) error { + if len(values) == 0 { + return nil + } + return a.DO.Save(values) +} + +func (a articleDo) First() (*models.Article, error) { + if result, err := a.DO.First(); err != nil { + return nil, err + } else { + return result.(*models.Article), nil + } +} + +func (a articleDo) Take() (*models.Article, error) { + if result, err := a.DO.Take(); err != nil { + return nil, err + } else { + return result.(*models.Article), nil + } +} + +func (a articleDo) Last() (*models.Article, error) { + if result, err := a.DO.Last(); err != nil { + return nil, err + } else { + return result.(*models.Article), nil + } +} + +func (a articleDo) Find() ([]*models.Article, error) { + result, err := a.DO.Find() + return result.([]*models.Article), err +} + +func (a articleDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*models.Article, err error) { + buf := make([]*models.Article, 0, batchSize) + err = a.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error { + defer func() { results = append(results, buf...) }() + return fc(tx, batch) + }) + return results, err +} + +func (a articleDo) FindInBatches(result *[]*models.Article, batchSize int, fc func(tx gen.Dao, batch int) error) error { + return a.DO.FindInBatches(result, batchSize, fc) +} + +func (a articleDo) Attrs(attrs ...field.AssignExpr) *articleDo { + return a.withDO(a.DO.Attrs(attrs...)) +} + +func (a articleDo) Assign(attrs ...field.AssignExpr) *articleDo { + return a.withDO(a.DO.Assign(attrs...)) +} + +func (a articleDo) Joins(fields ...field.RelationField) *articleDo { + for _, _f := range fields { + a = *a.withDO(a.DO.Joins(_f)) + } + return &a +} + +func (a articleDo) Preload(fields ...field.RelationField) *articleDo { + for _, _f := range fields { + a = *a.withDO(a.DO.Preload(_f)) + } + return &a +} + +func (a articleDo) FirstOrInit() (*models.Article, error) { + if result, err := a.DO.FirstOrInit(); err != nil { + return nil, err + } else { + return result.(*models.Article), nil + } +} + +func (a articleDo) FirstOrCreate() (*models.Article, error) { + if result, err := a.DO.FirstOrCreate(); err != nil { + return nil, err + } else { + return result.(*models.Article), nil + } +} + +func (a articleDo) FindByPage(offset int, limit int) (result []*models.Article, count int64, err error) { + result, err = a.Offset(offset).Limit(limit).Find() + if err != nil { + return + } + + if size := len(result); 0 < limit && 0 < size && size < limit { + count = int64(size + offset) + return + } + + count, err = a.Offset(-1).Limit(-1).Count() + return +} + +func (a articleDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) { + count, err = a.Count() + if err != nil { + return + } + + err = a.Offset(offset).Limit(limit).Scan(result) + return +} + +func (a articleDo) Scan(result interface{}) (err error) { + return a.DO.Scan(result) +} + +func (a articleDo) Delete(models ...*models.Article) (result gen.ResultInfo, err error) { + return a.DO.Delete(models) +} + +func (a *articleDo) withDO(do gen.Dao) *articleDo { + a.DO = *do.(*gen.DO) + return a +} diff --git a/web/queries/article_group.gen.go b/web/queries/article_group.gen.go new file mode 100644 index 0000000..2890610 --- /dev/null +++ b/web/queries/article_group.gen.go @@ -0,0 +1,347 @@ +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. +// Code generated by gorm.io/gen. DO NOT EDIT. + +package queries + +import ( + "context" + + "gorm.io/gorm" + "gorm.io/gorm/clause" + "gorm.io/gorm/schema" + + "gorm.io/gen" + "gorm.io/gen/field" + + "gorm.io/plugin/dbresolver" + + "platform/web/models" +) + +func newArticleGroup(db *gorm.DB, opts ...gen.DOOption) articleGroup { + _articleGroup := articleGroup{} + + _articleGroup.articleGroupDo.UseDB(db, opts...) + _articleGroup.articleGroupDo.UseModel(&models.ArticleGroup{}) + + tableName := _articleGroup.articleGroupDo.TableName() + _articleGroup.ALL = field.NewAsterisk(tableName) + _articleGroup.ID = field.NewInt32(tableName, "id") + _articleGroup.CreatedAt = field.NewTime(tableName, "created_at") + _articleGroup.UpdatedAt = field.NewTime(tableName, "updated_at") + _articleGroup.DeletedAt = field.NewField(tableName, "deleted_at") + _articleGroup.Name = field.NewString(tableName, "name") + _articleGroup.Code = field.NewString(tableName, "code") + _articleGroup.Sort = field.NewInt32(tableName, "sort") + _articleGroup.Status = field.NewInt(tableName, "status") + + _articleGroup.fillFieldMap() + + return _articleGroup +} + +type articleGroup struct { + articleGroupDo + + ALL field.Asterisk + ID field.Int32 + CreatedAt field.Time + UpdatedAt field.Time + DeletedAt field.Field + Name field.String + Code field.String + Sort field.Int32 + Status field.Int + + fieldMap map[string]field.Expr +} + +func (a articleGroup) Table(newTableName string) *articleGroup { + a.articleGroupDo.UseTable(newTableName) + return a.updateTableName(newTableName) +} + +func (a articleGroup) As(alias string) *articleGroup { + a.articleGroupDo.DO = *(a.articleGroupDo.As(alias).(*gen.DO)) + return a.updateTableName(alias) +} + +func (a *articleGroup) updateTableName(table string) *articleGroup { + a.ALL = field.NewAsterisk(table) + a.ID = field.NewInt32(table, "id") + a.CreatedAt = field.NewTime(table, "created_at") + a.UpdatedAt = field.NewTime(table, "updated_at") + a.DeletedAt = field.NewField(table, "deleted_at") + a.Name = field.NewString(table, "name") + a.Code = field.NewString(table, "code") + a.Sort = field.NewInt32(table, "sort") + a.Status = field.NewInt(table, "status") + + a.fillFieldMap() + + return a +} + +func (a *articleGroup) GetFieldByName(fieldName string) (field.OrderExpr, bool) { + _f, ok := a.fieldMap[fieldName] + if !ok || _f == nil { + return nil, false + } + _oe, ok := _f.(field.OrderExpr) + return _oe, ok +} + +func (a *articleGroup) fillFieldMap() { + a.fieldMap = make(map[string]field.Expr, 8) + a.fieldMap["id"] = a.ID + a.fieldMap["created_at"] = a.CreatedAt + a.fieldMap["updated_at"] = a.UpdatedAt + a.fieldMap["deleted_at"] = a.DeletedAt + a.fieldMap["name"] = a.Name + a.fieldMap["code"] = a.Code + a.fieldMap["sort"] = a.Sort + a.fieldMap["status"] = a.Status +} + +func (a articleGroup) clone(db *gorm.DB) articleGroup { + a.articleGroupDo.ReplaceConnPool(db.Statement.ConnPool) + return a +} + +func (a articleGroup) replaceDB(db *gorm.DB) articleGroup { + a.articleGroupDo.ReplaceDB(db) + return a +} + +type articleGroupDo struct{ gen.DO } + +func (a articleGroupDo) Debug() *articleGroupDo { + return a.withDO(a.DO.Debug()) +} + +func (a articleGroupDo) WithContext(ctx context.Context) *articleGroupDo { + return a.withDO(a.DO.WithContext(ctx)) +} + +func (a articleGroupDo) ReadDB() *articleGroupDo { + return a.Clauses(dbresolver.Read) +} + +func (a articleGroupDo) WriteDB() *articleGroupDo { + return a.Clauses(dbresolver.Write) +} + +func (a articleGroupDo) Session(config *gorm.Session) *articleGroupDo { + return a.withDO(a.DO.Session(config)) +} + +func (a articleGroupDo) Clauses(conds ...clause.Expression) *articleGroupDo { + return a.withDO(a.DO.Clauses(conds...)) +} + +func (a articleGroupDo) Returning(value interface{}, columns ...string) *articleGroupDo { + return a.withDO(a.DO.Returning(value, columns...)) +} + +func (a articleGroupDo) Not(conds ...gen.Condition) *articleGroupDo { + return a.withDO(a.DO.Not(conds...)) +} + +func (a articleGroupDo) Or(conds ...gen.Condition) *articleGroupDo { + return a.withDO(a.DO.Or(conds...)) +} + +func (a articleGroupDo) Select(conds ...field.Expr) *articleGroupDo { + return a.withDO(a.DO.Select(conds...)) +} + +func (a articleGroupDo) Where(conds ...gen.Condition) *articleGroupDo { + return a.withDO(a.DO.Where(conds...)) +} + +func (a articleGroupDo) Order(conds ...field.Expr) *articleGroupDo { + return a.withDO(a.DO.Order(conds...)) +} + +func (a articleGroupDo) Distinct(cols ...field.Expr) *articleGroupDo { + return a.withDO(a.DO.Distinct(cols...)) +} + +func (a articleGroupDo) Omit(cols ...field.Expr) *articleGroupDo { + return a.withDO(a.DO.Omit(cols...)) +} + +func (a articleGroupDo) Join(table schema.Tabler, on ...field.Expr) *articleGroupDo { + return a.withDO(a.DO.Join(table, on...)) +} + +func (a articleGroupDo) LeftJoin(table schema.Tabler, on ...field.Expr) *articleGroupDo { + return a.withDO(a.DO.LeftJoin(table, on...)) +} + +func (a articleGroupDo) RightJoin(table schema.Tabler, on ...field.Expr) *articleGroupDo { + return a.withDO(a.DO.RightJoin(table, on...)) +} + +func (a articleGroupDo) Group(cols ...field.Expr) *articleGroupDo { + return a.withDO(a.DO.Group(cols...)) +} + +func (a articleGroupDo) Having(conds ...gen.Condition) *articleGroupDo { + return a.withDO(a.DO.Having(conds...)) +} + +func (a articleGroupDo) Limit(limit int) *articleGroupDo { + return a.withDO(a.DO.Limit(limit)) +} + +func (a articleGroupDo) Offset(offset int) *articleGroupDo { + return a.withDO(a.DO.Offset(offset)) +} + +func (a articleGroupDo) Scopes(funcs ...func(gen.Dao) gen.Dao) *articleGroupDo { + return a.withDO(a.DO.Scopes(funcs...)) +} + +func (a articleGroupDo) Unscoped() *articleGroupDo { + return a.withDO(a.DO.Unscoped()) +} + +func (a articleGroupDo) Create(values ...*models.ArticleGroup) error { + if len(values) == 0 { + return nil + } + return a.DO.Create(values) +} + +func (a articleGroupDo) CreateInBatches(values []*models.ArticleGroup, batchSize int) error { + return a.DO.CreateInBatches(values, batchSize) +} + +// Save : !!! underlying implementation is different with GORM +// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values) +func (a articleGroupDo) Save(values ...*models.ArticleGroup) error { + if len(values) == 0 { + return nil + } + return a.DO.Save(values) +} + +func (a articleGroupDo) First() (*models.ArticleGroup, error) { + if result, err := a.DO.First(); err != nil { + return nil, err + } else { + return result.(*models.ArticleGroup), nil + } +} + +func (a articleGroupDo) Take() (*models.ArticleGroup, error) { + if result, err := a.DO.Take(); err != nil { + return nil, err + } else { + return result.(*models.ArticleGroup), nil + } +} + +func (a articleGroupDo) Last() (*models.ArticleGroup, error) { + if result, err := a.DO.Last(); err != nil { + return nil, err + } else { + return result.(*models.ArticleGroup), nil + } +} + +func (a articleGroupDo) Find() ([]*models.ArticleGroup, error) { + result, err := a.DO.Find() + return result.([]*models.ArticleGroup), err +} + +func (a articleGroupDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*models.ArticleGroup, err error) { + buf := make([]*models.ArticleGroup, 0, batchSize) + err = a.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error { + defer func() { results = append(results, buf...) }() + return fc(tx, batch) + }) + return results, err +} + +func (a articleGroupDo) FindInBatches(result *[]*models.ArticleGroup, batchSize int, fc func(tx gen.Dao, batch int) error) error { + return a.DO.FindInBatches(result, batchSize, fc) +} + +func (a articleGroupDo) Attrs(attrs ...field.AssignExpr) *articleGroupDo { + return a.withDO(a.DO.Attrs(attrs...)) +} + +func (a articleGroupDo) Assign(attrs ...field.AssignExpr) *articleGroupDo { + return a.withDO(a.DO.Assign(attrs...)) +} + +func (a articleGroupDo) Joins(fields ...field.RelationField) *articleGroupDo { + for _, _f := range fields { + a = *a.withDO(a.DO.Joins(_f)) + } + return &a +} + +func (a articleGroupDo) Preload(fields ...field.RelationField) *articleGroupDo { + for _, _f := range fields { + a = *a.withDO(a.DO.Preload(_f)) + } + return &a +} + +func (a articleGroupDo) FirstOrInit() (*models.ArticleGroup, error) { + if result, err := a.DO.FirstOrInit(); err != nil { + return nil, err + } else { + return result.(*models.ArticleGroup), nil + } +} + +func (a articleGroupDo) FirstOrCreate() (*models.ArticleGroup, error) { + if result, err := a.DO.FirstOrCreate(); err != nil { + return nil, err + } else { + return result.(*models.ArticleGroup), nil + } +} + +func (a articleGroupDo) FindByPage(offset int, limit int) (result []*models.ArticleGroup, count int64, err error) { + result, err = a.Offset(offset).Limit(limit).Find() + if err != nil { + return + } + + if size := len(result); 0 < limit && 0 < size && size < limit { + count = int64(size + offset) + return + } + + count, err = a.Offset(-1).Limit(-1).Count() + return +} + +func (a articleGroupDo) ScanByPage(result interface{}, offset int, limit int) (count int64, err error) { + count, err = a.Count() + if err != nil { + return + } + + err = a.Offset(offset).Limit(limit).Scan(result) + return +} + +func (a articleGroupDo) Scan(result interface{}) (err error) { + return a.DO.Scan(result) +} + +func (a articleGroupDo) Delete(models ...*models.ArticleGroup) (result gen.ResultInfo, err error) { + return a.DO.Delete(models) +} + +func (a *articleGroupDo) withDO(do gen.Dao) *articleGroupDo { + a.DO = *do.(*gen.DO) + return a +} diff --git a/web/queries/gen.go b/web/queries/gen.go index 60bc218..65679e6 100644 --- a/web/queries/gen.go +++ b/web/queries/gen.go @@ -20,6 +20,8 @@ var ( Admin *admin AdminRole *adminRole Announcement *announcement + Article *article + ArticleGroup *articleGroup BalanceActivity *balanceActivity Bill *bill Channel *channel @@ -59,6 +61,8 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) { Admin = &Q.Admin AdminRole = &Q.AdminRole Announcement = &Q.Announcement + Article = &Q.Article + ArticleGroup = &Q.ArticleGroup BalanceActivity = &Q.BalanceActivity Bill = &Q.Bill Channel = &Q.Channel @@ -99,6 +103,8 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query { Admin: newAdmin(db, opts...), AdminRole: newAdminRole(db, opts...), Announcement: newAnnouncement(db, opts...), + Article: newArticle(db, opts...), + ArticleGroup: newArticleGroup(db, opts...), BalanceActivity: newBalanceActivity(db, opts...), Bill: newBill(db, opts...), Channel: newChannel(db, opts...), @@ -140,6 +146,8 @@ type Query struct { Admin admin AdminRole adminRole Announcement announcement + Article article + ArticleGroup articleGroup BalanceActivity balanceActivity Bill bill Channel channel @@ -182,6 +190,8 @@ func (q *Query) clone(db *gorm.DB) *Query { Admin: q.Admin.clone(db), AdminRole: q.AdminRole.clone(db), Announcement: q.Announcement.clone(db), + Article: q.Article.clone(db), + ArticleGroup: q.ArticleGroup.clone(db), BalanceActivity: q.BalanceActivity.clone(db), Bill: q.Bill.clone(db), Channel: q.Channel.clone(db), @@ -231,6 +241,8 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query { Admin: q.Admin.replaceDB(db), AdminRole: q.AdminRole.replaceDB(db), Announcement: q.Announcement.replaceDB(db), + Article: q.Article.replaceDB(db), + ArticleGroup: q.ArticleGroup.replaceDB(db), BalanceActivity: q.BalanceActivity.replaceDB(db), Bill: q.Bill.replaceDB(db), Channel: q.Channel.replaceDB(db), @@ -270,6 +282,8 @@ type queryCtx struct { Admin *adminDo AdminRole *adminRoleDo Announcement *announcementDo + Article *articleDo + ArticleGroup *articleGroupDo BalanceActivity *balanceActivityDo Bill *billDo Channel *channelDo @@ -309,6 +323,8 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx { Admin: q.Admin.WithContext(ctx), AdminRole: q.AdminRole.WithContext(ctx), Announcement: q.Announcement.WithContext(ctx), + Article: q.Article.WithContext(ctx), + ArticleGroup: q.ArticleGroup.WithContext(ctx), BalanceActivity: q.BalanceActivity.WithContext(ctx), Bill: q.Bill.WithContext(ctx), Channel: q.Channel.WithContext(ctx), diff --git a/web/routes.go b/web/routes.go index c0daf9b..578ba4b 100644 --- a/web/routes.go +++ b/web/routes.go @@ -160,6 +160,10 @@ func clientRouter(api fiber.Router) { channel := client.Group("/channel") channel.Post("/remove", handlers.RemoveChannels) + // 文章查看 + article := api.Group("/article") + article.Post("/nav", handlers.NavArticle) + article.Post("/get", handlers.GetArticle) } // 管理员接口路由 @@ -286,4 +290,20 @@ func adminRouter(api fiber.Router) { couponUser.Post("/create", handlers.CreateCouponUserByAdmin) couponUser.Post("/update", handlers.UpdateCouponUserByAdmin) couponUser.Post("/remove", handlers.DeleteCouponUserByAdmin) + + // article 文档 + var article = api.Group("/article") + article.Post("/page", handlers.PageArticleByAdmin) + article.Post("/get", handlers.GetArticleByAdmin) + article.Post("/create", handlers.CreateArticle) + article.Post("/update", handlers.UpdateArticle) + article.Post("/remove", handlers.DeleteArticle) + + // article-group 文档分组 + var articleGroup = api.Group("/article-group") + articleGroup.Post("/page", handlers.PageArticleGroupByAdmin) + articleGroup.Post("/list", handlers.ListArticleGroupByAdmin) + articleGroup.Post("/create", handlers.CreateArticleGroup) + articleGroup.Post("/update", handlers.UpdateArticleGroup) + articleGroup.Post("/remove", handlers.DeleteArticleGroup) } diff --git a/web/services/article.go b/web/services/article.go new file mode 100644 index 0000000..735c87b --- /dev/null +++ b/web/services/article.go @@ -0,0 +1,257 @@ +package services + +import ( + "errors" + "platform/pkg/u" + "platform/web/core" + m "platform/web/models" + q "platform/web/queries" + "time" + + "gorm.io/gen/field" + "gorm.io/gorm" +) + +var Article = &articleService{} + +type articleService struct{} + +func (s *articleService) Page(req *PageArticleReq) (result []*m.Article, count int64, err error) { + do := q.Article.Where() + if req.Keyword != nil && *req.Keyword != "" { + do = do.Where(q.Article.Title.Like("%" + *req.Keyword + "%")) + } + if req.GroupID != nil { + do = do.Where(q.Article.GroupID.Eq(*req.GroupID)) + } + if req.Status != nil { + do = do.Where(q.Article.Status.Eq(int(*req.Status))) + } + + return q.Article. + Preload(q.Article.Group). + Where(do). + Omit(q.Article.Content). + Order(q.Article.Sort, q.Article.CreatedAt). + FindByPage(req.GetOffset(), req.GetLimit()) +} + +type PageArticleReq struct { + core.PageReq + Keyword *string `json:"keyword,omitempty"` + GroupID *int32 `json:"group_id,omitempty"` + Status *m.ArticleStatus `json:"status,omitempty"` +} + +func (s *articleService) GetByAdmin(id int32) (*m.Article, error) { + article, err := q.Article. + Preload(q.Article.Group). + Where(q.Article.ID.Eq(id)). + Take() + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, core.NewBizErr("文档不存在") + } + if err != nil { + return nil, err + } + return article, nil +} + +func (s *articleService) Create(data CreateArticleData) error { + if err := s.ensureGroupExists(data.GroupID); err != nil { + return err + } + + return q.Article.Create(&m.Article{ + GroupID: data.GroupID, + Title: data.Title, + Content: data.Content, + Sort: u.Else(data.Sort, 0), + Status: u.Else(data.Status, m.ArticleStatusEnabled), + }) +} + +type CreateArticleData struct { + GroupID int32 `json:"group_id" validate:"required"` + Title string `json:"title" validate:"required"` + Content *string `json:"content"` + Sort *int32 `json:"sort"` + Status *m.ArticleStatus `json:"status"` +} + +func (s *articleService) Update(data UpdateArticleData) error { + if data.GroupID != nil { + if err := s.ensureGroupExists(*data.GroupID); err != nil { + return err + } + } + + do := make([]field.AssignExpr, 0) + if data.GroupID != nil { + do = append(do, q.Article.GroupID.Value(*data.GroupID)) + } + if data.Title != nil { + do = append(do, q.Article.Title.Value(*data.Title)) + } + if data.Content != nil { + do = append(do, q.Article.Content.Value(*data.Content)) + } + if data.Sort != nil { + do = append(do, q.Article.Sort.Value(*data.Sort)) + } + if data.Status != nil { + do = append(do, q.Article.Status.Value(int(*data.Status))) + } + if len(do) == 0 { + return nil + } + + r, err := q.Article.Where(q.Article.ID.Eq(data.ID)).UpdateSimple(do...) + if err != nil { + return err + } + if r.RowsAffected == 0 { + return core.NewBizErr("文档状态已过期") + } + return nil +} + +type UpdateArticleData struct { + ID int32 `json:"id" validate:"required"` + GroupID *int32 `json:"group_id"` + Title *string `json:"title"` + Content *string `json:"content"` + Sort *int32 `json:"sort"` + Status *m.ArticleStatus `json:"status"` +} + +func (s *articleService) Delete(id int32) error { + r, err := q.Article.Where(q.Article.ID.Eq(id)).UpdateColumn(q.Article.DeletedAt, time.Now()) + if err != nil { + return err + } + if r.RowsAffected == 0 { + return core.NewBizErr("文档状态已过期") + } + return nil +} + +func (s *articleService) Nav() ([]*ArticleNavGroup, error) { + groups, err := q.ArticleGroup. + Where(q.ArticleGroup.Status.Eq(int(m.ArticleGroupStatusEnabled))). + Order(q.ArticleGroup.Sort, q.ArticleGroup.CreatedAt). + Find() + if err != nil { + return nil, err + } + if len(groups) == 0 { + return []*ArticleNavGroup{}, nil + } + + groupIDs := make([]int32, 0, len(groups)) + result := make([]*ArticleNavGroup, 0, len(groups)) + groupMap := make(map[int32]*ArticleNavGroup, len(groups)) + for _, group := range groups { + groupIDs = append(groupIDs, group.ID) + item := &ArticleNavGroup{ + ID: group.ID, + Name: group.Name, + Code: group.Code, + Articles: []*ArticleNavArticle{}, + } + result = append(result, item) + groupMap[group.ID] = item + } + + articles, err := q.Article. + Where( + q.Article.GroupID.In(groupIDs...), + q.Article.Status.Eq(int(m.ArticleStatusEnabled)), + ). + Omit(q.Article.Content). + Order(q.Article.Sort, q.Article.CreatedAt). + Find() + if err != nil { + return nil, err + } + + for _, article := range articles { + group := groupMap[article.GroupID] + if group == nil { + continue + } + group.Articles = append(group.Articles, &ArticleNavArticle{ + ID: article.ID, + Title: article.Title, + UpdatedAt: article.UpdatedAt, + }) + } + + return result, nil +} + +type ArticleNavGroup struct { + ID int32 `json:"id"` + Name string `json:"name"` + Code string `json:"code"` + Articles []*ArticleNavArticle `json:"articles"` +} + +type ArticleNavArticle struct { + ID int32 `json:"id"` + Title string `json:"title"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (s *articleService) GetPublic(id int32) (*ArticlePublicDetail, error) { + article, err := q.Article. + Preload(q.Article.Group). + Where( + q.Article.ID.Eq(id), + q.Article.Status.Eq(int(m.ArticleStatusEnabled)), + ). + Take() + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, core.NewBizErr("文档不存在") + } + if err != nil { + return nil, err + } + if article.Group == nil || article.Group.Status != m.ArticleGroupStatusEnabled { + return nil, core.NewBizErr("文档不存在") + } + + return &ArticlePublicDetail{ + ID: article.ID, + Title: article.Title, + Content: article.Content, + UpdatedAt: article.UpdatedAt, + Group: &ArticlePublicGroup{ + ID: article.Group.ID, + Name: article.Group.Name, + Code: article.Group.Code, + }, + }, nil +} + +type ArticlePublicDetail struct { + ID int32 `json:"id"` + Title string `json:"title"` + Content *string `json:"content,omitempty"` + UpdatedAt time.Time `json:"updated_at"` + Group *ArticlePublicGroup `json:"group"` +} + +type ArticlePublicGroup struct { + ID int32 `json:"id"` + Name string `json:"name"` + Code string `json:"code"` +} + +func (s *articleService) ensureGroupExists(groupID int32) error { + _, err := q.ArticleGroup.Where(q.ArticleGroup.ID.Eq(groupID)).Take() + if errors.Is(err, gorm.ErrRecordNotFound) { + return core.NewBizErr("文档分组不存在") + } + return err +} diff --git a/web/services/article_group.go b/web/services/article_group.go new file mode 100644 index 0000000..ce2dcb8 --- /dev/null +++ b/web/services/article_group.go @@ -0,0 +1,128 @@ +package services + +import ( + "errors" + "platform/pkg/u" + "platform/web/core" + m "platform/web/models" + q "platform/web/queries" + "time" + + "gorm.io/gen/field" + "gorm.io/gorm" +) + +var ArticleGroup = &articleGroupService{} + +type articleGroupService struct{} + +func (s *articleGroupService) All() (result []*m.ArticleGroup, err error) { + return q.ArticleGroup. + Order(q.ArticleGroup.Sort, q.ArticleGroup.CreatedAt). + Find() +} + +func (s *articleGroupService) Page(req *PageArticleGroupReq) (result []*m.ArticleGroup, count int64, err error) { + do := q.ArticleGroup.Where() + if req.Keyword != nil && *req.Keyword != "" { + do = do.Where( + q.ArticleGroup.Where( + q.ArticleGroup.Name.Like("%" + *req.Keyword + "%"), + ).Or( + q.ArticleGroup.Code.Like("%" + *req.Keyword + "%"), + ), + ) + } + if req.Status != nil { + do = do.Where(q.ArticleGroup.Status.Eq(int(*req.Status))) + } + + return q.ArticleGroup. + Where(do). + Order(q.ArticleGroup.Sort, q.ArticleGroup.CreatedAt). + FindByPage(req.GetOffset(), req.GetLimit()) +} + +type PageArticleGroupReq struct { + core.PageReq + Keyword *string `json:"keyword,omitempty"` + Status *m.ArticleGroupStatus `json:"status,omitempty"` +} + +func (s *articleGroupService) Create(data CreateArticleGroupData) error { + err := q.ArticleGroup.Create(&m.ArticleGroup{ + Name: data.Name, + Code: data.Code, + Sort: u.Else(data.Sort, 0), + Status: u.Else(data.Status, m.ArticleGroupStatusEnabled), + }) + if errors.Is(err, gorm.ErrDuplicatedKey) { + return core.NewBizErr("文档分组编码已存在") + } + return err +} + +type CreateArticleGroupData struct { + Name string `json:"name" validate:"required"` + Code string `json:"code" validate:"required"` + Sort *int32 `json:"sort"` + Status *m.ArticleGroupStatus `json:"status"` +} + +func (s *articleGroupService) Update(data UpdateArticleGroupData) error { + do := make([]field.AssignExpr, 0) + if data.Name != nil { + do = append(do, q.ArticleGroup.Name.Value(*data.Name)) + } + if data.Code != nil { + do = append(do, q.ArticleGroup.Code.Value(*data.Code)) + } + if data.Sort != nil { + do = append(do, q.ArticleGroup.Sort.Value(*data.Sort)) + } + if data.Status != nil { + do = append(do, q.ArticleGroup.Status.Value(int(*data.Status))) + } + if len(do) == 0 { + return nil + } + + r, err := q.ArticleGroup.Where(q.ArticleGroup.ID.Eq(data.ID)).UpdateSimple(do...) + if errors.Is(err, gorm.ErrDuplicatedKey) { + return core.NewBizErr("文档分组编码已存在") + } + if err != nil { + return err + } + if r.RowsAffected == 0 { + return core.NewBizErr("文档分组状态已过期") + } + return nil +} + +type UpdateArticleGroupData struct { + ID int32 `json:"id" validate:"required"` + Name *string `json:"name"` + Code *string `json:"code"` + Sort *int32 `json:"sort"` + Status *m.ArticleGroupStatus `json:"status"` +} + +func (s *articleGroupService) Delete(id int32) error { + count, err := q.Article.Where(q.Article.GroupID.Eq(id)).Count() + if err != nil { + return err + } + if count > 0 { + return core.NewBizErr("分组下仍有关联文章,无法删除") + } + + r, err := q.ArticleGroup.Where(q.ArticleGroup.ID.Eq(id)).UpdateColumn(q.ArticleGroup.DeletedAt, time.Now()) + if err != nil { + return err + } + if r.RowsAffected == 0 { + return core.NewBizErr("文档分组状态已过期") + } + return nil +}