固定文章编码
This commit is contained in:
25
README.md
25
README.md
@@ -1,10 +1,11 @@
|
||||
## TODO
|
||||
|
||||
长短效合并
|
||||
清除代理的并发问题,redis 归还后,还有可能 gost 没有完成解除占用,导致端口冲突,考虑先等几秒再释放 redis
|
||||
|
||||
现在我需要提供一套首页接口,用来展示一些统计信息。检查 [README.md](README.md) 的后台首页接口部分,确保你正确理解了每个接口的含义,如何进行统计。如果有不清除的接口,先提出问题,我会补充相关信息。在没有问题后,给出实施计划
|
||||
|
||||
后台首页接口:
|
||||
|
||||
- 待认领客户数量
|
||||
- 活跃代理数
|
||||
- 今日交易额
|
||||
@@ -31,7 +32,7 @@
|
||||
|
||||
错误提示增强,展示整链路信息
|
||||
|
||||
交易信息持久化
|
||||
交易信息持久化
|
||||
|
||||
订单关闭问题,在前端关闭窗口后直接调用了全部订单接口,应改成先确认再关闭
|
||||
|
||||
@@ -53,20 +54,24 @@
|
||||
1. 创建 model 文件
|
||||
2. 将 model 按照格式添加声明到 `cmd/gen/main.go` 中
|
||||
3. 编辑 `scripts/sql/init.sql` 文件,参照原有格式,需要注意:
|
||||
- 先写 drop table if exists 语句,确保脚本可以幂等执行
|
||||
- 为有必要的字段添加索引
|
||||
- 为数据表及其字段添加注释
|
||||
- 在文件末尾创建数据表流程全部结束后,为字段添加外键
|
||||
|
||||
- 先写 drop table if exists 语句,确保脚本可以幂等执行
|
||||
- 为有必要的字段添加索引
|
||||
- 为数据表及其字段添加注释
|
||||
- 在文件末尾创建数据表流程全部结束后,为字段添加外键
|
||||
|
||||
4. 调用 `go run ./cmd/gen/main.go` 生成查询文件
|
||||
|
||||
### 更新数据表流程
|
||||
|
||||
1. 更新 model 文件
|
||||
2. 编辑 `scripts/sql/init.sql` 文件,参照原有格式,需要注意:
|
||||
- 先写 drop table if exists 语句,确保脚本可以幂等执行
|
||||
- 为有必要的字段添加索引
|
||||
- 为数据表及其字段添加注释
|
||||
- 在文件末尾创建数据表流程全部结束后,为字段添加外键
|
||||
|
||||
- 先写 drop table if exists 语句,确保脚本可以幂等执行
|
||||
- 为有必要的字段添加索引
|
||||
- 为数据表及其字段添加注释
|
||||
- 在文件末尾创建数据表流程全部结束后,为字段添加外键
|
||||
|
||||
3. 调用 `go run ./cmd/gen/main.go` 更新查询文件
|
||||
|
||||
### 新增接口或修改接口权限
|
||||
|
||||
@@ -1944,17 +1944,39 @@ components:
|
||||
properties:
|
||||
keyword:
|
||||
type: string
|
||||
description: 按文章标题或编码模糊查询
|
||||
group_id:
|
||||
type: integer
|
||||
status:
|
||||
type: integer
|
||||
enum: [0, 1]
|
||||
|
||||
GetArticleRequest:
|
||||
type: object
|
||||
description: `id` 与 `code` 必须且只能提供一个。按 `id` 查询用于兼容旧文章链接,按 `code` 查询用于固定引用链接。
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
code:
|
||||
type: string
|
||||
minLength: 1
|
||||
maxLength: 64
|
||||
pattern: "^[a-z0-9][a-z0-9_-]{0,63}$"
|
||||
oneOf:
|
||||
- required: [id]
|
||||
- required: [code]
|
||||
|
||||
CreateArticleRequest:
|
||||
type: object
|
||||
properties:
|
||||
group_id:
|
||||
type: integer
|
||||
code:
|
||||
type: string
|
||||
description: 可选的文章编码,用于固定引用链接;在未删除文章中唯一
|
||||
minLength: 1
|
||||
maxLength: 64
|
||||
pattern: "^[a-z0-9][a-z0-9_-]{0,63}$"
|
||||
title:
|
||||
type: string
|
||||
content:
|
||||
@@ -1975,6 +1997,12 @@ components:
|
||||
type: integer
|
||||
group_id:
|
||||
type: integer
|
||||
code:
|
||||
type: string
|
||||
description: 可修改的文章编码;在未删除文章中唯一
|
||||
minLength: 1
|
||||
maxLength: 64
|
||||
pattern: "^[a-z0-9][a-z0-9_-]{0,63}$"
|
||||
title:
|
||||
type: string
|
||||
content:
|
||||
@@ -2057,6 +2085,9 @@ components:
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
code:
|
||||
type: string
|
||||
description: 旧文章可能没有编码
|
||||
title:
|
||||
type: string
|
||||
updated_at:
|
||||
@@ -2105,6 +2136,9 @@ components:
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
code:
|
||||
type: string
|
||||
description: 旧文章可能没有编码
|
||||
title:
|
||||
type: string
|
||||
content:
|
||||
@@ -2468,6 +2502,9 @@ components:
|
||||
properties:
|
||||
group_id:
|
||||
type: integer
|
||||
code:
|
||||
type: string
|
||||
description: 旧文章可能没有编码
|
||||
title:
|
||||
type: string
|
||||
sort:
|
||||
@@ -3834,7 +3871,7 @@ paths:
|
||||
post:
|
||||
tags: [client/article]
|
||||
summary: 获取文档详情
|
||||
description: 仅官方客户端可调用。
|
||||
description: 仅官方客户端可调用。支持按文章 ID 或编码查询,二者必须且只能提供一个。
|
||||
security:
|
||||
- basicAuth: []
|
||||
- bearerAuth: []
|
||||
@@ -3843,7 +3880,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/IdRequest"
|
||||
$ref: "#/components/schemas/GetArticleRequest"
|
||||
responses:
|
||||
"200":
|
||||
description: 文档详情
|
||||
|
||||
@@ -175,6 +175,7 @@ drop table if exists article cascade;
|
||||
create table article (
|
||||
id int generated by default as identity primary key,
|
||||
group_id int not null,
|
||||
code text,
|
||||
title text not null,
|
||||
content text,
|
||||
sort int not null default 0,
|
||||
@@ -183,6 +184,7 @@ create table article (
|
||||
updated_at timestamptz default current_timestamp,
|
||||
deleted_at timestamptz
|
||||
);
|
||||
create unique index udx_article_code on article (code) where code is not null and deleted_at is null;
|
||||
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;
|
||||
@@ -191,6 +193,7 @@ create index idx_article_sort on article (sort) where deleted_at is null;
|
||||
comment on table article is '文章表';
|
||||
comment on column article.id is '文章ID';
|
||||
comment on column article.group_id is '分组ID';
|
||||
comment on column article.code is '文章编码,用于固定引用链接';
|
||||
comment on column article.title is '文章标题';
|
||||
comment on column article.content is '文章内容';
|
||||
comment on column article.sort is '文章排序';
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"` // 文章排序
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user