add article
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"tyapi-server/internal/domains/api/services/processors/jrzq"
|
||||
"tyapi-server/internal/domains/api/services/processors/qcxg"
|
||||
"tyapi-server/internal/domains/api/services/processors/qygl"
|
||||
"tyapi-server/internal/domains/api/services/processors/test"
|
||||
"tyapi-server/internal/domains/api/services/processors/yysy"
|
||||
"tyapi-server/internal/domains/product/services"
|
||||
"tyapi-server/internal/infrastructure/external/alicloud"
|
||||
@@ -162,6 +163,11 @@ func registerAllProcessors(combService *comb.CombService) {
|
||||
|
||||
// FLXG系列处理器 - 风险管控 (包含原FXHY功能)
|
||||
"FLXG8B4D": flxg.ProcessFLXG8B4DRequest,
|
||||
|
||||
// TEST系列处理器 - 测试用处理器
|
||||
"TEST001": test.ProcessTestRequest,
|
||||
"TEST002": test.ProcessTestErrorRequest,
|
||||
"TEST003": test.ProcessTestTimeoutRequest,
|
||||
}
|
||||
|
||||
// 批量注册到组合包服务
|
||||
|
||||
94
internal/domains/api/services/processors/test/README.md
Normal file
94
internal/domains/api/services/processors/test/README.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# 测试处理器使用说明
|
||||
|
||||
这个目录包含了用于测试的处理器,可以模拟各种API请求场景,帮助开发和测试人员验证系统功能。
|
||||
|
||||
## 处理器列表
|
||||
|
||||
### 1. ProcessTestRequest - 基础测试处理器
|
||||
- **功能**: 模拟正常的API请求处理
|
||||
- **用途**: 测试基本的请求处理流程、参数验证、响应生成等
|
||||
|
||||
#### 请求参数
|
||||
```json
|
||||
{
|
||||
"test_param": "测试参数值",
|
||||
"delay": 1000
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
```json
|
||||
{
|
||||
"message": "测试请求处理成功",
|
||||
"timestamp": "2024-01-01T12:00:00Z",
|
||||
"request_id": "test_20240101120000_000000000",
|
||||
"test_param": "测试参数值",
|
||||
"process_time_ms": 1005,
|
||||
"status": "success"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. ProcessTestErrorRequest - 错误测试处理器
|
||||
- **功能**: 模拟各种错误情况
|
||||
- **用途**: 测试错误处理机制、异常响应等
|
||||
|
||||
#### 支持的错误类型
|
||||
- `system_error`: 系统错误
|
||||
- `datasource_error`: 数据源错误
|
||||
- `not_found`: 资源未找到
|
||||
- `invalid_param`: 参数无效
|
||||
|
||||
#### 请求示例
|
||||
```json
|
||||
{
|
||||
"test_param": "system_error"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. ProcessTestTimeoutRequest - 超时测试处理器
|
||||
- **功能**: 模拟长时间处理导致的超时
|
||||
- **用途**: 测试超时处理、上下文取消等
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 开发阶段
|
||||
- 验证处理器框架是否正常工作
|
||||
- 测试参数验证逻辑
|
||||
- 验证错误处理机制
|
||||
|
||||
### 测试阶段
|
||||
- 性能测试(通过delay参数)
|
||||
- 超时测试
|
||||
- 错误场景测试
|
||||
- 集成测试
|
||||
|
||||
### 调试阶段
|
||||
- 快速验证API调用流程
|
||||
- 测试中间件功能
|
||||
- 验证日志记录
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **延迟参数**: `delay` 参数最大值为5000毫秒(5秒),避免测试时等待时间过长
|
||||
2. **上下文处理**: 所有处理器都正确处理上下文取消,支持超时控制
|
||||
3. **错误处理**: 遵循项目的错误处理规范,使用预定义的错误类型
|
||||
4. **参数验证**: 使用标准的参数验证机制,确保测试的真实性
|
||||
|
||||
## 集成到路由
|
||||
|
||||
要将测试处理器集成到API路由中,需要在相应的路由配置中添加:
|
||||
|
||||
```go
|
||||
// 在路由配置中添加测试端点
|
||||
router.POST("/api/test/basic", handlers.WrapProcessor(processors.ProcessTestRequest))
|
||||
router.POST("/api/test/error", handlers.WrapProcessor(processors.ProcessTestErrorRequest))
|
||||
router.POST("/api/test/timeout", handlers.WrapProcessor(processors.ProcessTestTimeoutRequest))
|
||||
```
|
||||
|
||||
## 测试建议
|
||||
|
||||
1. **基础功能测试**: 先使用 `ProcessTestRequest` 验证基本流程
|
||||
2. **错误场景测试**: 使用 `ProcessTestErrorRequest` 测试各种错误情况
|
||||
3. **性能测试**: 通过调整 `delay` 参数测试不同响应时间
|
||||
4. **超时测试**: 使用 `ProcessTestTimeoutRequest` 验证超时处理
|
||||
5. **压力测试**: 并发调用测试处理器的稳定性
|
||||
120
internal/domains/api/services/processors/test/test_processor.go
Normal file
120
internal/domains/api/services/processors/test/test_processor.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"tyapi-server/internal/domains/api/services/processors"
|
||||
)
|
||||
|
||||
// TestRequest 测试请求参数
|
||||
type TestRequest struct {
|
||||
TestParam string `json:"test_param" validate:"required"`
|
||||
Delay int `json:"delay" validate:"min=0,max=5000"` // 延迟毫秒数,最大5秒
|
||||
}
|
||||
|
||||
// TestResponse 测试响应数据
|
||||
type TestResponse struct {
|
||||
Message string `json:"message"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
RequestID string `json:"request_id"`
|
||||
TestParam string `json:"test_param"`
|
||||
ProcessTime int64 `json:"process_time_ms"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// ProcessTestRequest 测试处理器,用于模拟API请求
|
||||
func ProcessTestRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
startTime := time.Now()
|
||||
|
||||
// 解析请求参数
|
||||
var req TestRequest
|
||||
if err := json.Unmarshal(params, &req); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 参数验证
|
||||
if err := deps.Validator.ValidateStruct(req); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 模拟处理延迟
|
||||
if req.Delay > 0 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, errors.Join(processors.ErrSystem, ctx.Err())
|
||||
case <-time.After(time.Duration(req.Delay) * time.Millisecond):
|
||||
// 延迟完成
|
||||
}
|
||||
}
|
||||
|
||||
// 检查上下文是否已取消
|
||||
if ctx.Err() != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, ctx.Err())
|
||||
}
|
||||
|
||||
// 生成响应数据
|
||||
response := TestResponse{
|
||||
Message: "测试请求处理成功",
|
||||
Timestamp: time.Now(),
|
||||
RequestID: generateTestRequestID(),
|
||||
TestParam: req.TestParam,
|
||||
ProcessTime: time.Since(startTime).Milliseconds(),
|
||||
Status: "success",
|
||||
}
|
||||
|
||||
// 序列化响应
|
||||
result, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
return nil, errors.Join(processors.ErrSystem, err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ProcessTestErrorRequest 测试错误处理的处理器
|
||||
func ProcessTestErrorRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var req TestRequest
|
||||
if err := json.Unmarshal(params, &req); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 模拟不同类型的错误
|
||||
switch req.TestParam {
|
||||
case "system_error":
|
||||
return nil, processors.ErrSystem
|
||||
case "datasource_error":
|
||||
return nil, processors.ErrDatasource
|
||||
case "not_found":
|
||||
return nil, processors.ErrNotFound
|
||||
case "invalid_param":
|
||||
return nil, processors.ErrInvalidParam
|
||||
default:
|
||||
return nil, errors.Join(processors.ErrSystem, errors.New("未知错误类型"))
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessTestTimeoutRequest 测试超时处理的处理器
|
||||
func ProcessTestTimeoutRequest(ctx context.Context, params []byte, deps *processors.ProcessorDependencies) ([]byte, error) {
|
||||
var req TestRequest
|
||||
if err := json.Unmarshal(params, &req); err != nil {
|
||||
return nil, errors.Join(processors.ErrInvalidParam, err)
|
||||
}
|
||||
|
||||
// 模拟长时间处理
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, errors.Join(processors.ErrSystem, ctx.Err())
|
||||
case <-time.After(10 * time.Second): // 10秒超时
|
||||
// 这里通常不会执行到,因为上下文会先超时
|
||||
}
|
||||
|
||||
return nil, processors.ErrSystem
|
||||
}
|
||||
|
||||
// generateTestRequestID 生成测试用的请求ID
|
||||
func generateTestRequestID() string {
|
||||
return "test_" + time.Now().Format("20060102150405") + "_" + time.Now().Format("000000000")
|
||||
}
|
||||
@@ -38,10 +38,10 @@ func ProcessYYSY6F2ERequest(ctx context.Context, params []byte, deps *processors
|
||||
|
||||
reqData := map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"name": encryptedName,
|
||||
"idNo": encryptedIDCard,
|
||||
"phone": encryptedMobileNo,
|
||||
"phoneType": paramsDto.MobileType,
|
||||
"name": encryptedName,
|
||||
"idNo": encryptedIDCard,
|
||||
"phone": encryptedMobileNo,
|
||||
"phoneType": paramsDto.MobileType,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -55,4 +55,4 @@ func ProcessYYSY6F2ERequest(ctx context.Context, params []byte, deps *processors
|
||||
}
|
||||
|
||||
return respBytes, nil
|
||||
}
|
||||
}
|
||||
|
||||
195
internal/domains/article/entities/article.go
Normal file
195
internal/domains/article/entities/article.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ArticleStatus 文章状态枚举
|
||||
type ArticleStatus string
|
||||
|
||||
const (
|
||||
ArticleStatusDraft ArticleStatus = "draft" // 草稿
|
||||
ArticleStatusPublished ArticleStatus = "published" // 已发布
|
||||
ArticleStatusArchived ArticleStatus = "archived" // 已归档
|
||||
)
|
||||
|
||||
// Article 文章聚合根
|
||||
// 系统的核心内容实体,提供文章的完整生命周期管理
|
||||
// 支持草稿、发布、归档状态,实现Entity接口便于统一管理
|
||||
type Article struct {
|
||||
// 基础标识
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"文章唯一标识"`
|
||||
Title string `gorm:"type:varchar(200);not null" json:"title" comment:"文章标题"`
|
||||
Content string `gorm:"type:text;not null" json:"content" comment:"文章内容"`
|
||||
Summary string `gorm:"type:varchar(500)" json:"summary" comment:"文章摘要"`
|
||||
CoverImage string `gorm:"type:varchar(500)" json:"cover_image" comment:"封面图片"`
|
||||
|
||||
// 分类
|
||||
CategoryID string `gorm:"type:varchar(36)" json:"category_id" comment:"分类ID"`
|
||||
|
||||
// 状态管理
|
||||
Status ArticleStatus `gorm:"type:varchar(20);not null;default:'draft'" json:"status" comment:"文章状态"`
|
||||
IsFeatured bool `gorm:"default:false" json:"is_featured" comment:"是否推荐"`
|
||||
PublishedAt *time.Time `json:"published_at" comment:"发布时间"`
|
||||
ScheduledAt *time.Time `json:"scheduled_at" comment:"定时发布时间"`
|
||||
|
||||
// 统计信息
|
||||
ViewCount int `gorm:"default:0" json:"view_count" comment:"阅读量"`
|
||||
|
||||
// 时间戳字段
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
|
||||
|
||||
// 关联关系
|
||||
Category *Category `gorm:"foreignKey:CategoryID" json:"category,omitempty" comment:"分类信息"`
|
||||
Tags []Tag `gorm:"many2many:article_tag_relations;" json:"tags,omitempty" comment:"标签列表"`
|
||||
|
||||
// 领域事件 (不持久化)
|
||||
domainEvents []interface{} `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (Article) TableName() string {
|
||||
return "articles"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (a *Article) BeforeCreate(tx *gorm.DB) error {
|
||||
if a.ID == "" {
|
||||
a.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 实现 Entity 接口 - 提供统一的实体管理接口
|
||||
// GetID 获取实体唯一标识
|
||||
func (a *Article) GetID() string {
|
||||
return a.ID
|
||||
}
|
||||
|
||||
// GetCreatedAt 获取创建时间
|
||||
func (a *Article) GetCreatedAt() time.Time {
|
||||
return a.CreatedAt
|
||||
}
|
||||
|
||||
// GetUpdatedAt 获取更新时间
|
||||
func (a *Article) GetUpdatedAt() time.Time {
|
||||
return a.UpdatedAt
|
||||
}
|
||||
|
||||
// Validate 验证文章信息
|
||||
// 检查文章必填字段是否完整,确保数据的有效性
|
||||
func (a *Article) Validate() error {
|
||||
if a.Title == "" {
|
||||
return NewValidationError("文章标题不能为空")
|
||||
}
|
||||
if a.Content == "" {
|
||||
return NewValidationError("文章内容不能为空")
|
||||
}
|
||||
|
||||
// 验证标题长度
|
||||
if len(a.Title) > 200 {
|
||||
return NewValidationError("文章标题不能超过200个字符")
|
||||
}
|
||||
|
||||
// 验证摘要长度
|
||||
if a.Summary != "" && len(a.Summary) > 500 {
|
||||
return NewValidationError("文章摘要不能超过500个字符")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Publish 发布文章
|
||||
func (a *Article) Publish() error {
|
||||
if a.Status == ArticleStatusPublished {
|
||||
return NewValidationError("文章已经是发布状态")
|
||||
}
|
||||
|
||||
a.Status = ArticleStatusPublished
|
||||
now := time.Now()
|
||||
a.PublishedAt = &now
|
||||
a.ScheduledAt = nil // 清除定时发布时间
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SchedulePublish 定时发布文章
|
||||
func (a *Article) SchedulePublish(scheduledTime time.Time) error {
|
||||
if a.Status == ArticleStatusPublished {
|
||||
return NewValidationError("文章已经是发布状态")
|
||||
}
|
||||
|
||||
if scheduledTime.Before(time.Now()) {
|
||||
return NewValidationError("定时发布时间不能早于当前时间")
|
||||
}
|
||||
|
||||
a.Status = ArticleStatusDraft // 保持草稿状态,等待定时发布
|
||||
a.ScheduledAt = &scheduledTime
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsScheduled 判断是否已设置定时发布
|
||||
func (a *Article) IsScheduled() bool {
|
||||
return a.ScheduledAt != nil && a.Status == ArticleStatusDraft
|
||||
}
|
||||
|
||||
// GetScheduledTime 获取定时发布时间
|
||||
func (a *Article) GetScheduledTime() *time.Time {
|
||||
return a.ScheduledAt
|
||||
}
|
||||
|
||||
// Archive 归档文章
|
||||
func (a *Article) Archive() error {
|
||||
if a.Status == ArticleStatusArchived {
|
||||
return NewValidationError("文章已经是归档状态")
|
||||
}
|
||||
|
||||
a.Status = ArticleStatusArchived
|
||||
return nil
|
||||
}
|
||||
|
||||
// IncrementViewCount 增加阅读量
|
||||
func (a *Article) IncrementViewCount() {
|
||||
a.ViewCount++
|
||||
}
|
||||
|
||||
// SetFeatured 设置推荐状态
|
||||
func (a *Article) SetFeatured(featured bool) {
|
||||
a.IsFeatured = featured
|
||||
}
|
||||
|
||||
// IsPublished 判断是否已发布
|
||||
func (a *Article) IsPublished() bool {
|
||||
return a.Status == ArticleStatusPublished
|
||||
}
|
||||
|
||||
// IsDraft 判断是否为草稿
|
||||
func (a *Article) IsDraft() bool {
|
||||
return a.Status == ArticleStatusDraft
|
||||
}
|
||||
|
||||
// IsArchived 判断是否已归档
|
||||
func (a *Article) IsArchived() bool {
|
||||
return a.Status == ArticleStatusArchived
|
||||
}
|
||||
|
||||
// CanEdit 判断是否可以编辑
|
||||
func (a *Article) CanEdit() bool {
|
||||
return a.Status == ArticleStatusDraft
|
||||
}
|
||||
|
||||
// CanPublish 判断是否可以发布
|
||||
func (a *Article) CanPublish() bool {
|
||||
return a.Status == ArticleStatusDraft
|
||||
}
|
||||
|
||||
// CanArchive 判断是否可以归档
|
||||
func (a *Article) CanArchive() bool {
|
||||
return a.Status == ArticleStatusPublished
|
||||
}
|
||||
78
internal/domains/article/entities/category.go
Normal file
78
internal/domains/article/entities/category.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Category 文章分类实体
|
||||
// 用于对文章进行分类管理,支持层级结构和排序
|
||||
type Category struct {
|
||||
// 基础标识
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"分类唯一标识"`
|
||||
Name string `gorm:"type:varchar(100);not null" json:"name" comment:"分类名称"`
|
||||
Description string `gorm:"type:text" json:"description" comment:"分类描述"`
|
||||
SortOrder int `gorm:"default:0" json:"sort_order" comment:"排序"`
|
||||
|
||||
// 时间戳字段
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
|
||||
|
||||
// 关联关系
|
||||
Articles []Article `gorm:"foreignKey:CategoryID" json:"articles,omitempty" comment:"分类下的文章"`
|
||||
|
||||
// 领域事件 (不持久化)
|
||||
domainEvents []interface{} `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (Category) TableName() string {
|
||||
return "article_categories"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (c *Category) BeforeCreate(tx *gorm.DB) error {
|
||||
if c.ID == "" {
|
||||
c.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 实现 Entity 接口 - 提供统一的实体管理接口
|
||||
// GetID 获取实体唯一标识
|
||||
func (c *Category) GetID() string {
|
||||
return c.ID
|
||||
}
|
||||
|
||||
// GetCreatedAt 获取创建时间
|
||||
func (c *Category) GetCreatedAt() time.Time {
|
||||
return c.CreatedAt
|
||||
}
|
||||
|
||||
// GetUpdatedAt 获取更新时间
|
||||
func (c *Category) GetUpdatedAt() time.Time {
|
||||
return c.UpdatedAt
|
||||
}
|
||||
|
||||
// Validate 验证分类信息
|
||||
// 检查分类必填字段是否完整,确保数据的有效性
|
||||
func (c *Category) Validate() error {
|
||||
if c.Name == "" {
|
||||
return NewValidationError("分类名称不能为空")
|
||||
}
|
||||
|
||||
// 验证名称长度
|
||||
if len(c.Name) > 100 {
|
||||
return NewValidationError("分类名称不能超过100个字符")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetSortOrder 设置排序
|
||||
func (c *Category) SetSortOrder(order int) {
|
||||
c.SortOrder = order
|
||||
}
|
||||
21
internal/domains/article/entities/errors.go
Normal file
21
internal/domains/article/entities/errors.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package entities
|
||||
|
||||
// ValidationError 验证错误
|
||||
type ValidationError struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *ValidationError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// NewValidationError 创建验证错误
|
||||
func NewValidationError(message string) *ValidationError {
|
||||
return &ValidationError{Message: message}
|
||||
}
|
||||
|
||||
// IsValidationError 判断是否为验证错误
|
||||
func IsValidationError(err error) bool {
|
||||
_, ok := err.(*ValidationError)
|
||||
return ok
|
||||
}
|
||||
102
internal/domains/article/entities/tag.go
Normal file
102
internal/domains/article/entities/tag.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package entities
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Tag 文章标签实体
|
||||
// 用于对文章进行标签化管理,支持颜色配置
|
||||
type Tag struct {
|
||||
// 基础标识
|
||||
ID string `gorm:"primaryKey;type:varchar(36)" json:"id" comment:"标签唯一标识"`
|
||||
Name string `gorm:"type:varchar(50);not null" json:"name" comment:"标签名称"`
|
||||
Color string `gorm:"type:varchar(20);default:'#1890ff'" json:"color" comment:"标签颜色"`
|
||||
|
||||
// 时间戳字段
|
||||
CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at" comment:"创建时间"`
|
||||
UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at" comment:"更新时间"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-" comment:"软删除时间"`
|
||||
|
||||
// 关联关系
|
||||
Articles []Article `gorm:"many2many:article_tag_relations;" json:"articles,omitempty" comment:"标签下的文章"`
|
||||
|
||||
// 领域事件 (不持久化)
|
||||
domainEvents []interface{} `gorm:"-" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 指定表名
|
||||
func (Tag) TableName() string {
|
||||
return "article_tags"
|
||||
}
|
||||
|
||||
// BeforeCreate GORM钩子:创建前自动生成UUID
|
||||
func (t *Tag) BeforeCreate(tx *gorm.DB) error {
|
||||
if t.ID == "" {
|
||||
t.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 实现 Entity 接口 - 提供统一的实体管理接口
|
||||
// GetID 获取实体唯一标识
|
||||
func (t *Tag) GetID() string {
|
||||
return t.ID
|
||||
}
|
||||
|
||||
// GetCreatedAt 获取创建时间
|
||||
func (t *Tag) GetCreatedAt() time.Time {
|
||||
return t.CreatedAt
|
||||
}
|
||||
|
||||
// GetUpdatedAt 获取更新时间
|
||||
func (t *Tag) GetUpdatedAt() time.Time {
|
||||
return t.UpdatedAt
|
||||
}
|
||||
|
||||
// Validate 验证标签信息
|
||||
// 检查标签必填字段是否完整,确保数据的有效性
|
||||
func (t *Tag) Validate() error {
|
||||
if t.Name == "" {
|
||||
return NewValidationError("标签名称不能为空")
|
||||
}
|
||||
|
||||
// 验证名称长度
|
||||
if len(t.Name) > 50 {
|
||||
return NewValidationError("标签名称不能超过50个字符")
|
||||
}
|
||||
|
||||
// 验证颜色格式
|
||||
if t.Color != "" && !isValidColor(t.Color) {
|
||||
return NewValidationError("标签颜色格式无效")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetColor 设置标签颜色
|
||||
func (t *Tag) SetColor(color string) error {
|
||||
if color != "" && !isValidColor(color) {
|
||||
return NewValidationError("标签颜色格式无效")
|
||||
}
|
||||
t.Color = color
|
||||
return nil
|
||||
}
|
||||
|
||||
// isValidColor 验证颜色格式
|
||||
func isValidColor(color string) bool {
|
||||
// 简单的颜色格式验证,支持 #RRGGBB 格式
|
||||
if len(color) == 7 && color[0] == '#' {
|
||||
for i := 1; i < 7; i++ {
|
||||
if !((color[i] >= '0' && color[i] <= '9') ||
|
||||
(color[i] >= 'a' && color[i] <= 'f') ||
|
||||
(color[i] >= 'A' && color[i] <= 'F')) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tyapi-server/internal/domains/article/entities"
|
||||
"tyapi-server/internal/domains/article/repositories/queries"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// ArticleRepository 文章仓储接口
|
||||
type ArticleRepository interface {
|
||||
interfaces.Repository[entities.Article]
|
||||
|
||||
// 自定义查询方法
|
||||
FindByAuthorID(ctx context.Context, authorID string) ([]*entities.Article, error)
|
||||
FindByCategoryID(ctx context.Context, categoryID string) ([]*entities.Article, error)
|
||||
FindByStatus(ctx context.Context, status entities.ArticleStatus) ([]*entities.Article, error)
|
||||
FindFeatured(ctx context.Context) ([]*entities.Article, error)
|
||||
Search(ctx context.Context, query *queries.SearchArticleQuery) ([]*entities.Article, int64, error)
|
||||
ListArticles(ctx context.Context, query *queries.ListArticleQuery) ([]*entities.Article, int64, error)
|
||||
|
||||
// 统计方法
|
||||
CountByCategoryID(ctx context.Context, categoryID string) (int64, error)
|
||||
CountByStatus(ctx context.Context, status entities.ArticleStatus) (int64, error)
|
||||
|
||||
// 更新统计信息
|
||||
IncrementViewCount(ctx context.Context, articleID string) error
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tyapi-server/internal/domains/article/entities"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// CategoryRepository 分类仓储接口
|
||||
type CategoryRepository interface {
|
||||
interfaces.Repository[entities.Category]
|
||||
|
||||
// 自定义查询方法
|
||||
FindActive(ctx context.Context) ([]*entities.Category, error)
|
||||
FindBySortOrder(ctx context.Context) ([]*entities.Category, error)
|
||||
|
||||
// 统计方法
|
||||
CountActive(ctx context.Context) (int64, error)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package queries
|
||||
|
||||
import "tyapi-server/internal/domains/article/entities"
|
||||
|
||||
// ListArticleQuery 文章列表查询
|
||||
type ListArticleQuery struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Status entities.ArticleStatus `json:"status"`
|
||||
CategoryID string `json:"category_id"`
|
||||
TagID string `json:"tag_id"`
|
||||
Title string `json:"title"`
|
||||
Summary string `json:"summary"`
|
||||
IsFeatured *bool `json:"is_featured"`
|
||||
OrderBy string `json:"order_by"`
|
||||
OrderDir string `json:"order_dir"`
|
||||
}
|
||||
|
||||
// SearchArticleQuery 文章搜索查询
|
||||
type SearchArticleQuery struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Keyword string `json:"keyword"`
|
||||
CategoryID string `json:"category_id"`
|
||||
AuthorID string `json:"author_id"`
|
||||
Status entities.ArticleStatus `json:"status"`
|
||||
OrderBy string `json:"order_by"`
|
||||
OrderDir string `json:"order_dir"`
|
||||
}
|
||||
|
||||
// GetArticleQuery 获取文章详情查询
|
||||
type GetArticleQuery struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// GetArticlesByAuthorQuery 获取作者文章查询
|
||||
type GetArticlesByAuthorQuery struct {
|
||||
AuthorID string `json:"author_id"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
|
||||
// GetArticlesByCategoryQuery 获取分类文章查询
|
||||
type GetArticlesByCategoryQuery struct {
|
||||
CategoryID string `json:"category_id"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"tyapi-server/internal/domains/article/entities"
|
||||
"tyapi-server/internal/shared/interfaces"
|
||||
)
|
||||
|
||||
// TagRepository 标签仓储接口
|
||||
type TagRepository interface {
|
||||
interfaces.Repository[entities.Tag]
|
||||
|
||||
// 自定义查询方法
|
||||
FindByArticleID(ctx context.Context, articleID string) ([]*entities.Tag, error)
|
||||
FindByName(ctx context.Context, name string) (*entities.Tag, error)
|
||||
|
||||
// 关联方法
|
||||
AddTagToArticle(ctx context.Context, articleID string, tagID string) error
|
||||
RemoveTagFromArticle(ctx context.Context, articleID string, tagID string) error
|
||||
GetArticleTags(ctx context.Context, articleID string) ([]*entities.Tag, error)
|
||||
}
|
||||
94
internal/domains/article/services/article_service.go
Normal file
94
internal/domains/article/services/article_service.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"tyapi-server/internal/domains/article/entities"
|
||||
)
|
||||
|
||||
// ArticleService 文章领域服务
|
||||
// 处理文章相关的业务逻辑,包括验证、状态管理等
|
||||
type ArticleService struct{}
|
||||
|
||||
// NewArticleService 创建文章领域服务
|
||||
func NewArticleService() *ArticleService {
|
||||
return &ArticleService{}
|
||||
}
|
||||
|
||||
// ValidateArticle 验证文章
|
||||
// 检查文章是否符合业务规则
|
||||
func (s *ArticleService) ValidateArticle(article *entities.Article) error {
|
||||
// 1. 基础验证
|
||||
if err := article.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 业务规则验证
|
||||
// 标题不能包含敏感词
|
||||
if s.containsSensitiveWords(article.Title) {
|
||||
return entities.NewValidationError("文章标题包含敏感词")
|
||||
}
|
||||
|
||||
// 内容不能包含敏感词
|
||||
if s.containsSensitiveWords(article.Content) {
|
||||
return entities.NewValidationError("文章内容包含敏感词")
|
||||
}
|
||||
|
||||
// 摘要长度不能超过内容长度
|
||||
if article.Summary != "" && len(article.Summary) >= len(article.Content) {
|
||||
return entities.NewValidationError("文章摘要不能超过内容长度")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanPublish 检查是否可以发布
|
||||
func (s *ArticleService) CanPublish(article *entities.Article) error {
|
||||
if !article.CanPublish() {
|
||||
return entities.NewValidationError("文章状态不允许发布")
|
||||
}
|
||||
|
||||
// 检查必填字段
|
||||
if article.Title == "" {
|
||||
return entities.NewValidationError("文章标题不能为空")
|
||||
}
|
||||
if article.Content == "" {
|
||||
return entities.NewValidationError("文章内容不能为空")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanEdit 检查是否可以编辑
|
||||
func (s *ArticleService) CanEdit(article *entities.Article) error {
|
||||
if !article.CanEdit() {
|
||||
return entities.NewValidationError("文章状态不允许编辑")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// containsSensitiveWords 检查是否包含敏感词
|
||||
func (s *ArticleService) containsSensitiveWords(text string) bool {
|
||||
// TODO: 实现敏感词检查逻辑
|
||||
// 这里可以集成敏感词库或调用外部服务
|
||||
sensitiveWords := []string{
|
||||
"敏感词1",
|
||||
"敏感词2",
|
||||
"敏感词3",
|
||||
}
|
||||
|
||||
for _, word := range sensitiveWords {
|
||||
if len(word) > 0 && len(text) > 0 {
|
||||
// 简单的字符串包含检查
|
||||
// 实际项目中应该使用更复杂的算法
|
||||
if len(text) >= len(word) {
|
||||
for i := 0; i <= len(text)-len(word); i++ {
|
||||
if text[i:i+len(word)] == word {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user