From 45fb8dfeb81c0d4d33fec0fd85b5d22f8dd1e90e Mon Sep 17 00:00:00 2001 From: cialloo Date: Thu, 23 Oct 2025 08:49:33 +0800 Subject: [PATCH] update --- api/Blog.api | 228 ++++++++++++++++++++++++++++++++++++++++++ api/example.api | 75 ++++++++++++++ ddl/files.sql | 25 +++++ ddl/hashtags.sql | 23 +++++ ddl/post_hashtags.sql | 23 +++++ ddl/posts.sql | 30 ++++++ 6 files changed, 404 insertions(+) create mode 100644 api/Blog.api create mode 100644 api/example.api create mode 100644 ddl/files.sql create mode 100644 ddl/hashtags.sql create mode 100644 ddl/post_hashtags.sql create mode 100644 ddl/posts.sql diff --git a/api/Blog.api b/api/Blog.api new file mode 100644 index 0000000..d3abe7d --- /dev/null +++ b/api/Blog.api @@ -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 +} \ No newline at end of file diff --git a/api/example.api b/api/example.api new file mode 100644 index 0000000..8a8e4d1 --- /dev/null +++ b/api/example.api @@ -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) +} + diff --git a/ddl/files.sql b/ddl/files.sql new file mode 100644 index 0000000..fc482fc --- /dev/null +++ b/ddl/files.sql @@ -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'; \ No newline at end of file diff --git a/ddl/hashtags.sql b/ddl/hashtags.sql new file mode 100644 index 0000000..2a9ce98 --- /dev/null +++ b/ddl/hashtags.sql @@ -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'; \ No newline at end of file diff --git a/ddl/post_hashtags.sql b/ddl/post_hashtags.sql new file mode 100644 index 0000000..cdad062 --- /dev/null +++ b/ddl/post_hashtags.sql @@ -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'; \ No newline at end of file diff --git a/ddl/posts.sql b/ddl/posts.sql new file mode 100644 index 0000000..c7f7f26 --- /dev/null +++ b/ddl/posts.sql @@ -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'; \ No newline at end of file