固定文章编码

This commit is contained in:
2026-07-21 15:30:33 +08:00
parent d961538027
commit 83d38956ca
7 changed files with 149 additions and 24 deletions

View File

@@ -31,12 +31,12 @@ func GetArticle(c *fiber.Ctx) error {
return err
}
var req core.IdReq
var req s.GetArticleReq
if err := g.Validator.ParseBody(c, &req); err != nil {
return err
}
article, err := s.Article.GetPublic(req.Id)
article, err := s.Article.GetPublic(req)
if err != nil {
return err
}

View File

@@ -6,6 +6,7 @@ import "platform/web/core"
type Article struct {
core.Model
GroupID int32 `json:"group_id" gorm:"column:group_id"` // 分组ID
Code *string `json:"code,omitempty" gorm:"column:code"` // 文章编码,用于固定引用链接
Title string `json:"title" gorm:"column:title"` // 文章标题
Content *string `json:"content,omitempty" gorm:"column:content"` // 文章内容
Sort int32 `json:"sort" gorm:"column:sort"` // 文章排序

View File

@@ -32,6 +32,7 @@ func newArticle(db *gorm.DB, opts ...gen.DOOption) article {
_article.UpdatedAt = field.NewTime(tableName, "updated_at")
_article.DeletedAt = field.NewField(tableName, "deleted_at")
_article.GroupID = field.NewInt32(tableName, "group_id")
_article.Code = field.NewString(tableName, "code")
_article.Title = field.NewString(tableName, "title")
_article.Content = field.NewString(tableName, "content")
_article.Sort = field.NewInt32(tableName, "sort")
@@ -56,6 +57,7 @@ type article struct {
UpdatedAt field.Time
DeletedAt field.Field
GroupID field.Int32
Code field.String
Title field.String
Content field.String
Sort field.Int32
@@ -82,6 +84,7 @@ func (a *article) updateTableName(table string) *article {
a.UpdatedAt = field.NewTime(table, "updated_at")
a.DeletedAt = field.NewField(table, "deleted_at")
a.GroupID = field.NewInt32(table, "group_id")
a.Code = field.NewString(table, "code")
a.Title = field.NewString(table, "title")
a.Content = field.NewString(table, "content")
a.Sort = field.NewInt32(table, "sort")
@@ -102,12 +105,13 @@ func (a *article) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
}
func (a *article) fillFieldMap() {
a.fieldMap = make(map[string]field.Expr, 10)
a.fieldMap = make(map[string]field.Expr, 11)
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["code"] = a.Code
a.fieldMap["title"] = a.Title
a.fieldMap["content"] = a.Content
a.fieldMap["sort"] = a.Sort

View File

@@ -14,6 +14,7 @@ import (
"platform/web/core"
m "platform/web/models"
q "platform/web/queries"
"regexp"
"strings"
"time"
@@ -33,6 +34,8 @@ var articleUploadMimeExt = map[string]string{
"image/webp": ".webp",
}
var articleCodePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9_-]{0,63}$`)
type ArticleUploadResult struct {
URL string `json:"url"`
Path string `json:"path"`
@@ -151,7 +154,13 @@ func formatUploadSizeLimit(bytes int) string {
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 + "%"))
do = do.Where(
q.Article.Where(
q.Article.Title.Like("%" + *req.Keyword + "%"),
).Or(
q.Article.Code.Like("%" + *req.Keyword + "%"),
),
)
}
if req.GroupID != nil {
do = do.Where(q.Article.GroupID.Eq(*req.GroupID))
@@ -194,17 +203,28 @@ func (s *articleService) Create(data CreateArticleData) error {
return err
}
return q.Article.Create(&m.Article{
code, err := optionalArticleCode(data.Code)
if err != nil {
return err
}
err = q.Article.Create(&m.Article{
GroupID: data.GroupID,
Code: code,
Title: data.Title,
Content: data.Content,
Sort: u.Else(data.Sort, 0),
Status: u.Else(data.Status, m.ArticleStatusEnabled),
})
if errors.Is(err, gorm.ErrDuplicatedKey) {
return core.NewBizErr("文档编码已存在")
}
return err
}
type CreateArticleData struct {
GroupID int32 `json:"group_id" validate:"required"`
Code *string `json:"code"`
Title string `json:"title" validate:"required"`
Content *string `json:"content"`
Sort *int32 `json:"sort"`
@@ -222,6 +242,13 @@ func (s *articleService) Update(data UpdateArticleData) error {
if data.GroupID != nil {
do = append(do, q.Article.GroupID.Value(*data.GroupID))
}
if data.Code != nil {
code, err := normalizeArticleCode(*data.Code)
if err != nil {
return err
}
do = append(do, q.Article.Code.Value(code))
}
if data.Title != nil {
do = append(do, q.Article.Title.Value(*data.Title))
}
@@ -239,6 +266,9 @@ func (s *articleService) Update(data UpdateArticleData) error {
}
r, err := q.Article.Where(q.Article.ID.Eq(data.ID)).UpdateSimple(do...)
if errors.Is(err, gorm.ErrDuplicatedKey) {
return core.NewBizErr("文档编码已存在")
}
if err != nil {
return err
}
@@ -251,6 +281,7 @@ func (s *articleService) Update(data UpdateArticleData) error {
type UpdateArticleData struct {
ID int32 `json:"id" validate:"required"`
GroupID *int32 `json:"group_id"`
Code *string `json:"code"`
Title *string `json:"title"`
Content *string `json:"content"`
Sort *int32 `json:"sort"`
@@ -314,6 +345,7 @@ func (s *articleService) Nav() ([]*ArticleNavGroup, error) {
}
group.Articles = append(group.Articles, &ArticleNavArticle{
ID: article.ID,
Code: article.Code,
Title: article.Title,
UpdatedAt: article.UpdatedAt,
})
@@ -331,18 +363,40 @@ type ArticleNavGroup struct {
type ArticleNavArticle struct {
ID int32 `json:"id"`
Code *string `json:"code,omitempty"`
Title string `json:"title"`
UpdatedAt time.Time `json:"updated_at"`
}
func (s *articleService) GetPublic(id int32) (*ArticlePublicDetail, error) {
article, err := q.Article.
type GetArticleReq struct {
ID *int32 `json:"id"`
Code *string `json:"code"`
}
func (s *articleService) GetPublic(req GetArticleReq) (*ArticlePublicDetail, error) {
do := q.Article.
Preload(q.Article.Group).
Where(
q.Article.ID.Eq(id),
q.Article.Status.Eq(int(m.ArticleStatusEnabled)),
).
Take()
Where(q.Article.Status.Eq(int(m.ArticleStatusEnabled)))
switch {
case req.ID != nil && req.Code != nil:
return nil, core.NewBizErr("文档 ID 和编码不能同时提供")
case req.ID != nil:
if *req.ID <= 0 {
return nil, core.NewBizErr("文档 ID 不正确")
}
do = do.Where(q.Article.ID.Eq(*req.ID))
case req.Code != nil:
code, err := normalizeArticleCode(*req.Code)
if err != nil {
return nil, err
}
do = do.Where(q.Article.Code.Eq(code))
default:
return nil, core.NewBizErr("必须提供文档 ID 或编码")
}
article, err := do.Take()
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, core.NewBizErr("文档不存在")
}
@@ -355,6 +409,7 @@ func (s *articleService) GetPublic(id int32) (*ArticlePublicDetail, error) {
return &ArticlePublicDetail{
ID: article.ID,
Code: article.Code,
Title: article.Title,
Content: article.Content,
UpdatedAt: article.UpdatedAt,
@@ -368,6 +423,7 @@ func (s *articleService) GetPublic(id int32) (*ArticlePublicDetail, error) {
type ArticlePublicDetail struct {
ID int32 `json:"id"`
Code *string `json:"code,omitempty"`
Title string `json:"title"`
Content *string `json:"content,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
@@ -387,3 +443,22 @@ func (s *articleService) ensureGroupExists(groupID int32) error {
}
return err
}
func optionalArticleCode(code *string) (*string, error) {
if code == nil || strings.TrimSpace(*code) == "" {
return nil, nil
}
normalized, err := normalizeArticleCode(*code)
if err != nil {
return nil, err
}
return &normalized, nil
}
func normalizeArticleCode(code string) (string, error) {
normalized := strings.TrimSpace(code)
if !articleCodePattern.MatchString(normalized) {
return "", core.NewBizErr("文档编码只能包含小写字母、数字、下划线或连字符,长度为 1-64 位")
}
return normalized, nil
}