This commit is contained in:
2025-10-23 08:49:33 +08:00
commit 45fb8dfeb8
6 changed files with 404 additions and 0 deletions

228
api/Blog.api Normal file
View File

@@ -0,0 +1,228 @@
syntax = "v1"
info (
title: "Blog API"
desc: "Blog service API with file upload/download and CRUD operations"
author: "cialloo"
date: "2025-10-22"
version: "v1"
)
// ============= File Upload/Download Types =============
type (
// Request for generating presigned upload URL
GenerateUploadUrlReq {
FileName string `json:"file_name"` // Original file name
FileSize int64 `json:"file_size"` // File size in bytes
}
GenerateUploadUrlResp {
FileId int64 `json:"file_id"` // Generated file ID
PresignedUrl string `json:"presigned_url"` // S3 presigned upload URL
FileKey string `json:"file_key"` // S3 file key
ExpiresIn int `json:"expires_in"` // URL expiration time in seconds
}
// Request for generating presigned download URL
GenerateDownloadUrlReq {
FileId int64 `path:"file_id"` // File ID to download
}
GenerateDownloadUrlResp {
FileName string `json:"file_name"` // Original file name
PresignedUrl string `json:"presigned_url"` // S3 presigned download URL
FileSize int64 `json:"file_size"` // File size in bytes
ExpiresIn int `json:"expires_in"` // URL expiration time in seconds
}
// Confirm upload completion
ConfirmUploadReq {
FileId int64 `json:"file_id"` // File ID to confirm
}
ConfirmUploadResp {
Success bool `json:"success"` // Upload confirmation status
Message string `json:"message"` // Status message
}
)
// ============= Post Types =============
type (
// Create post request
CreatePostReq {
Title string `json:"title"` // Post title
Content string `json:"content"` // Post content
Hashtags []string `json:"hashtags,optional"` // Associated hashtags
}
CreatePostResp {
PostId int64 `json:"post_id"` // Created post ID
}
// Update post request
UpdatePostReq {
PostId int64 `path:"post_id"` // Post ID to update
Title string `json:"title,optional"` // New title
Content string `json:"content,optional"` // New content
Hashtags []string `json:"hashtags,optional"` // New hashtags (replaces all)
}
UpdatePostResp {
Success bool `json:"success"` // Update status
}
// Delete post request
DeletePostReq {
PostId int64 `path:"post_id"` // Post ID to delete
}
DeletePostResp {
Success bool `json:"success"` // Delete status
}
// Get post detail request
GetPostReq {
PostId int64 `path:"post_id"` // Post ID to retrieve
}
PostDetail {
PostId int64 `json:"post_id"` // Post ID
Title string `json:"title"` // Post title
Content string `json:"content"` // Post content
ViewCount int64 `json:"view_count"` // View count
Hashtags []Hashtag `json:"hashtags"` // Associated hashtags
CreatedAt string `json:"created_at"` // Creation timestamp
UpdatedAt string `json:"updated_at"` // Last update timestamp
}
GetPostResp {
Post PostDetail `json:"post"` // Post detail
}
// List posts request
ListPostsReq {
Page int `form:"page,default=1"` // Page number
PageSize int `form:"page_size,default=10"` // Items per page
Hashtag string `form:"hashtag,optional"` // Filter by hashtag name
SortBy string `form:"sort_by,default=created_at,options=created_at|view_count|updated_at"` // Sort field
}
PostItem {
PostId int64 `json:"post_id"` // Post ID
Title string `json:"title"` // Post title
ViewCount int64 `json:"view_count"` // View count
Hashtags []Hashtag `json:"hashtags"` // Associated hashtags
CreatedAt string `json:"created_at"` // Creation timestamp
UpdatedAt string `json:"updated_at"` // Last update timestamp
}
ListPostsResp {
Posts []PostItem `json:"posts"` // List of posts
Total int64 `json:"total"` // Total count
Page int `json:"page"` // Current page
PageSize int `json:"page_size"` // Items per page
TotalPages int `json:"total_pages"` // Total pages
}
)
// ============= Hashtag Types =============
type (
Hashtag {
HashtagId int64 `json:"hashtag_id"` // Hashtag ID
Name string `json:"name"` // Hashtag name
UsageCount int64 `json:"usage_count"` // Usage count
}
// List hashtags request
ListHashtagsReq {
Page int `form:"page,default=1"` // Page number
PageSize int `form:"page_size,default=20"` // Items per page
SortBy string `form:"sort_by,default=usage_count,options=usage_count|created_at|name"` // Sort field
}
ListHashtagsResp {
Hashtags []Hashtag `json:"hashtags"` // List of hashtags
Total int64 `json:"total"` // Total count
Page int `json:"page"` // Current page
PageSize int `json:"page_size"` // Items per page
TotalPages int `json:"total_pages"` // Total pages
}
// Get popular hashtags
GetPopularHashtagsReq {
Limit int `form:"limit,default=10"` // Number of hashtags to return
}
GetPopularHashtagsResp {
Hashtags []Hashtag `json:"hashtags"` // Popular hashtags
}
)
// ============= Service Definition =============
@server (
prefix: /api/v1/blog
group: file
)
service Blog {
@doc "Generate presigned URL for file upload"
@handler generateUploadUrl
post /files/upload/url (GenerateUploadUrlReq) returns (GenerateUploadUrlResp)
@doc "Generate presigned URL for file download"
@handler generateDownloadUrl
get /files/download/:file_id (GenerateDownloadUrlReq) returns (GenerateDownloadUrlResp)
@doc "Confirm file upload completion"
@handler confirmUpload
post /files/upload/confirm (ConfirmUploadReq) returns (ConfirmUploadResp)
}
@server (
prefix: /api/v1/blog
group: post
)
service Blog {
@doc "Create a new blog post"
@handler createPost
post /posts (CreatePostReq) returns (CreatePostResp)
@doc "Update an existing blog post"
@handler updatePost
put /posts/:post_id (UpdatePostReq) returns (UpdatePostResp)
@doc "Delete a blog post"
@handler deletePost
delete /posts/:post_id (DeletePostReq) returns (DeletePostResp)
@doc "Get blog post detail"
@handler getPost
get /posts/:post_id (GetPostReq) returns (GetPostResp)
@doc "List blog posts with pagination and filtering"
@handler listPosts
get /posts (ListPostsReq) returns (ListPostsResp)
}
@server (
prefix: /api/v1/blog
group: hashtag
)
service Blog {
@doc "List all hashtags with pagination"
@handler listHashtags
get /hashtags (ListHashtagsReq) returns (ListHashtagsResp)
@doc "Get popular hashtags"
@handler getPopularHashtags
get /hashtags/popular (GetPopularHashtagsReq) returns (GetPopularHashtagsResp)
}
@server (
prefix: /api/v1/blog
group: common
)
service Blog {
@doc "Health check endpoint"
@handler ping
get /ping
}

75
api/example.api Normal file
View File

@@ -0,0 +1,75 @@
syntax = "v1"
info (
title: "api 文件完整示例写法"
desc: "演示如何编写 api 文件"
author: "keson.an"
date: "2022 年 12 月 26 日"
version: "v1"
)
type UpdateReq {
Arg1 string `json:"arg1"`
}
type ListItem {
Value1 string `json:"value1"`
}
type LoginReq {
Username string `json:"username"`
Password string `json:"password"`
}
type LoginResp {
Name string `json:"name"`
}
type FormExampleReq {
Name string `form:"name"`
}
type PathExampleReq {
// path 标签修饰的 id 必须与请求路由中的片段对应,如
// id 在 service 语法块的请求路径上一定会有 :id 对应,见下文。
ID string `path:"id"`
}
type PathExampleResp {
Name string `json:"name"`
}
@server (
jwt: Auth // 对当前 Foo 语法块下的所有路由,开启 jwt 认证,不需要则请删除此行
prefix: /v1 // 对当前 Foo 语法块下的所有路由,新增 /v1 路由前缀,不需要则请删除此行
group: g1 // 对当前 Foo 语法块下的所有路由,路由归并到 g1 目录下,不需要则请删除此行
timeout: 3s // 对当前 Foo 语法块下的所有路由进行超时配置,不需要则请删除此行
middleware: AuthInterceptor // 对当前 Foo 语法块下的所有路由添加中间件,不需要则请删除此行
maxBytes: 1048576 // 对当前 Foo 语法块下的所有路由添加请求体大小控制,单位为 byte,goctl 版本 >= 1.5.0 才支持
)
service Foo {
// 定义没有请求体和响应体的接口,如 ping
@handler ping
get /ping
// 定义只有请求体的接口,如更新信息
@handler update
post /update (UpdateReq)
// 定义只有响应体的结构,如获取全部信息列表
@handler list
get /list returns ([]ListItem)
// 定义有结构体和响应体的接口,如登录
@handler login
post /login (LoginReq) returns (LoginResp)
// 定义表单请求
@handler formExample
post /form/example (FormExampleReq)
// 定义 path 参数
@handler pathExample
get /path/example/:id (PathExampleReq) returns (PathExampleResp)
}

25
ddl/files.sql Normal file
View File

@@ -0,0 +1,25 @@
-- Files table: stores uploaded file metadata
CREATE TABLE IF NOT EXISTS files (
id BIGSERIAL PRIMARY KEY,
file_name VARCHAR(255) NOT NULL,
file_key VARCHAR(500) NOT NULL UNIQUE,
byte_size BIGINT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Index for looking up files by key (most common query)
CREATE INDEX IF NOT EXISTS idx_files_file_key ON files(file_key);
-- Index for sorting files by creation date
CREATE INDEX IF NOT EXISTS idx_files_created_at ON files(created_at DESC);
-- Index for file size queries
CREATE INDEX IF NOT EXISTS idx_files_byte_size ON files(byte_size);
-- Comments on columns
COMMENT ON TABLE files IS 'File metadata table for uploaded files';
COMMENT ON COLUMN files.id IS 'Primary key, auto-incrementing file ID';
COMMENT ON COLUMN files.file_name IS 'Original name of the uploaded file';
COMMENT ON COLUMN files.file_key IS 'Unique storage key/path for the file';
COMMENT ON COLUMN files.byte_size IS 'File size in bytes';
COMMENT ON COLUMN files.created_at IS 'Timestamp when the file was uploaded';

23
ddl/hashtags.sql Normal file
View File

@@ -0,0 +1,23 @@
-- Hashtags table: stores unique hashtag information
CREATE TABLE IF NOT EXISTS hashtags (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE,
usage_count BIGINT DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Index for looking up hashtags by name (most common query)
CREATE INDEX IF NOT EXISTS idx_hashtags_name ON hashtags(name);
-- Index for sorting hashtags by popularity
CREATE INDEX IF NOT EXISTS idx_hashtags_usage_count ON hashtags(usage_count DESC);
-- Index for trending hashtags (recent + popular)
CREATE INDEX IF NOT EXISTS idx_hashtags_created_usage ON hashtags(created_at DESC, usage_count DESC);
-- Comments on columns
COMMENT ON TABLE hashtags IS 'Hashtags table for categorizing posts';
COMMENT ON COLUMN hashtags.id IS 'Primary key, auto-incrementing hashtag ID';
COMMENT ON COLUMN hashtags.name IS 'Unique hashtag name';
COMMENT ON COLUMN hashtags.usage_count IS 'Number of times the hashtag has been used';
COMMENT ON COLUMN hashtags.created_at IS 'Timestamp when the hashtag was first created';

23
ddl/post_hashtags.sql Normal file
View File

@@ -0,0 +1,23 @@
-- Post_hashtags junction table: many-to-many relationship between posts and hashtags
CREATE TABLE IF NOT EXISTS post_hashtags (
post_id BIGINT NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
hashtag_id BIGINT NOT NULL REFERENCES hashtags(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (post_id, hashtag_id)
);
-- Index for finding all posts with a specific hashtag
CREATE INDEX IF NOT EXISTS idx_post_hashtags_hashtag_id ON post_hashtags(hashtag_id, post_id);
-- Index for finding all hashtags for a specific post
CREATE INDEX IF NOT EXISTS idx_post_hashtags_post_id ON post_hashtags(post_id, hashtag_id);
-- Index for sorting by creation date
CREATE INDEX IF NOT EXISTS idx_post_hashtags_created_at ON post_hashtags(created_at DESC);
-- Comments on columns
COMMENT ON TABLE post_hashtags IS 'Junction table linking posts and hashtags';
COMMENT ON COLUMN post_hashtags.post_id IS 'Foreign key referencing posts table';
COMMENT ON COLUMN post_hashtags.hashtag_id IS 'Foreign key referencing hashtags table';
COMMENT ON COLUMN post_hashtags.created_at IS 'Timestamp when the hashtag was associated with the post';

30
ddl/posts.sql Normal file
View File

@@ -0,0 +1,30 @@
-- Posts table: stores blog post information
CREATE TABLE IF NOT EXISTS posts (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
view_count BIGINT DEFAULT 0,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Index for sorting posts by creation date
CREATE INDEX IF NOT EXISTS idx_posts_created_at ON posts(created_at DESC);
-- Index for sorting posts by view count (popular posts)
CREATE INDEX IF NOT EXISTS idx_posts_view_count ON posts(view_count DESC);
-- Index for sorting posts by update date
CREATE INDEX IF NOT EXISTS idx_posts_updated_at ON posts(updated_at DESC);
-- Combined index for pagination queries
CREATE INDEX IF NOT EXISTS idx_posts_created_view ON posts(created_at DESC, view_count DESC);
-- Comments on columns
COMMENT ON TABLE posts IS 'Blog posts table containing article information';
COMMENT ON COLUMN posts.id IS 'Primary key, auto-incrementing post ID';
COMMENT ON COLUMN posts.title IS 'Post title';
COMMENT ON COLUMN posts.content IS 'Post content in text format';
COMMENT ON COLUMN posts.view_count IS 'Number of times the post has been viewed';
COMMENT ON COLUMN posts.created_at IS 'Timestamp when the post was created';
COMMENT ON COLUMN posts.updated_at IS 'Timestamp when the post was last updated';