From b143a3807a224ac948df29305608a42fdbf46dfc Mon Sep 17 00:00:00 2001 From: yyg-max <175597134+yyg-max@users.noreply.github.com> Date: Tue, 30 Sep 2025 18:23:08 +0800 Subject: [PATCH 1/3] feat(routers): add pagination and search parameters for project receivers --- docs/docs.go | 21 +++++++++++++- docs/swagger.json | 21 +++++++++++++- docs/swagger.yaml | 15 +++++++++- internal/apps/project/routers.go | 48 ++++++++++++++++++++++++++------ 4 files changed, 94 insertions(+), 11 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index b4a8afa..8dddf13 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -600,10 +600,29 @@ const docTemplate = `{ "parameters": [ { "type": "string", - "description": "项目ID", + "description": "项目ID (Project ID)", "name": "id", "in": "path", "required": true + }, + { + "minimum": 1, + "type": "integer", + "name": "current", + "in": "query" + }, + { + "maxLength": 1024, + "type": "string", + "name": "search", + "in": "query" + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "name": "size", + "in": "query" } ], "responses": { diff --git a/docs/swagger.json b/docs/swagger.json index a43d02a..1729ec3 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -591,10 +591,29 @@ "parameters": [ { "type": "string", - "description": "项目ID", + "description": "项目ID (Project ID)", "name": "id", "in": "path", "required": true + }, + { + "minimum": 1, + "type": "integer", + "name": "current", + "in": "query" + }, + { + "maxLength": 1024, + "type": "string", + "name": "search", + "in": "query" + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "name": "size", + "in": "query" } ], "responses": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 22a7e03..2cc64f3 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -764,11 +764,24 @@ paths: consumes: - application/json parameters: - - description: 项目ID + - description: 项目ID (Project ID) in: path name: id required: true type: string + - in: query + minimum: 1 + name: current + type: integer + - in: query + maxLength: 1024 + name: search + type: string + - in: query + maximum: 100 + minimum: 1 + name: size + type: integer produces: - application/json responses: diff --git a/internal/apps/project/routers.go b/internal/apps/project/routers.go index 786ec11..600e6d4 100644 --- a/internal/apps/project/routers.go +++ b/internal/apps/project/routers.go @@ -345,29 +345,61 @@ func DeleteProject(c *gin.Context) { c.JSON(http.StatusOK, ProjectResponse{}) } +type ListProjectReceiversRequest struct { + Current int `json:"current" form:"current" binding:"min=1"` + Size int `json:"size" form:"size" binding:"min=1,max=100"` + Search string `json:"search" form:"search" binding:"max=1024"` +} + // ListProjectReceivers // @Tags project // @Accept json // @Produce json -// @Param id path string true "项目ID" +// @Param id path string true "项目ID (Project ID)" +// @Param request query ListProjectReceiversRequest true "request query" // @Success 200 {object} ProjectResponse // @Router /api/v1/projects/{id}/receivers [get] func ListProjectReceivers(c *gin.Context) { // load project project, _ := GetProjectFromContext(c) - // query db + // validate pagination request + req := &ListProjectReceiversRequest{} + if err := c.ShouldBindQuery(req); err != nil { + c.JSON(http.StatusBadRequest, ProjectResponse{ErrorMsg: err.Error()}) + return + } + offset := (req.Current - 1) * req.Size + + // build optimized query with proper indexing strategy + query := db.DB(c.Request.Context()). + Model(&ProjectItem{}). + Select("users.username, users.nickname, project_items.content"). + Joins("JOIN users ON users.id = project_items.receiver_id"). + Where("project_items.project_id = ?", project.ID) + + if req.Search != "" { + searchPattern := strings.TrimSpace(req.Search) + "%" + if len(searchPattern) <= 20 { + query = query.Where( + "users.username LIKE ? OR users.nickname LIKE ? OR project_items.content LIKE ?", + searchPattern, "%"+searchPattern, "%"+searchPattern) + } else { + query = query.Where( + "users.nickname LIKE ? OR project_items.content LIKE ?", + "%"+searchPattern, "%"+searchPattern) + } + } + + // query db with optimizations var receivers []struct { Username string `json:"username"` Nickname string `json:"nickname"` Content string `json:"content"` } - if err := db.DB(c.Request.Context()). - Model(&ProjectItem{}). - Select("users.username, users.nickname, project_items.content"). - Joins("JOIN users ON users.id = project_items.receiver_id"). - Where("project_items.project_id = ?", project.ID). - Order("project_items.received_at DESC"). + if err := query. + Offset(offset). + Limit(req.Size). Scan(&receivers).Error; err != nil { c.JSON(http.StatusInternalServerError, ProjectResponse{ErrorMsg: err.Error()}) return From 7f1559959f23b2f060541975ee5af5587f305644 Mon Sep 17 00:00:00 2001 From: yyg-max <175597134+yyg-max@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:42:53 +0800 Subject: [PATCH 2/3] feat: add pagination and search functionality for project receivers --- .../common/project/ReceiverDialog.tsx | 53 ++++++------------- .../components/common/project/constants.ts | 3 +- .../common/receive/ReceiveContent.tsx | 37 +++++++------ frontend/hooks/use-debounce.ts | 25 +++++++++ .../lib/services/project/project.service.ts | 30 +++++++++-- internal/apps/project/models.go | 2 +- internal/apps/project/routers.go | 10 ++-- 7 files changed, 97 insertions(+), 63 deletions(-) create mode 100644 frontend/hooks/use-debounce.ts diff --git a/frontend/components/common/project/ReceiverDialog.tsx b/frontend/components/common/project/ReceiverDialog.tsx index 899bb3d..edcf470 100644 --- a/frontend/components/common/project/ReceiverDialog.tsx +++ b/frontend/components/common/project/ReceiverDialog.tsx @@ -2,6 +2,7 @@ import {useState, useEffect, useCallback} from 'react'; import {useIsMobile} from '@/hooks/use-mobile'; +import {useDebounce} from '@/hooks/use-debounce'; import {toast} from 'sonner'; import {Button} from '@/components/ui/button'; import {Input} from '@/components/ui/input'; @@ -29,9 +30,9 @@ export function ReceiverDialog({ const isMobile = useIsMobile(); const [open, setOpen] = useState(false); const [loading, setLoading] = useState(false); - const [receivers, setReceivers] = useState([]); const [filteredReceivers, setFilteredReceivers] = useState([]); const [searchKeyword, setSearchKeyword] = useState(''); + const debouncedSearchKeyword = useDebounce(searchKeyword, 500); const [error, setError] = useState(null); const [copiedIndex, setCopiedIndex] = useState(null); @@ -43,10 +44,9 @@ export function ReceiverDialog({ setError(null); try { - const result = await services.project.getProjectReceiversSafe(projectId); + const result = await services.project.getProjectReceiversSafe(projectId, 1, 100, debouncedSearchKeyword); if (result.success) { - setReceivers(result.data || []); setFilteredReceivers(result.data || []); } else { setError(result.error || '获取领取人列表失败'); @@ -56,27 +56,20 @@ export function ReceiverDialog({ } finally { setLoading(false); } - }, [projectId]); - - /** - * 搜索功能 - */ - const handleSearch = useCallback((keyword: string) => { - const filtered = receivers.filter((receiver) => - receiver.username.toLowerCase().includes(keyword.toLowerCase()) || - receiver.nickname.toLowerCase().includes(keyword.toLowerCase()) || - receiver.content.toLowerCase().includes(keyword.toLowerCase()), - ); - - setFilteredReceivers(filtered); - }, [receivers]); + }, [projectId, debouncedSearchKeyword]); /** * 复制内容到剪贴板 */ const handleCopy = async (content: string, index: number) => { try { - await navigator.clipboard.writeText(content); + // 拆分内容并整理成多行格式 + const contentItems = content.split('$\n*'); + const cleanedItems = contentItems.map(item => + item.replace(/^[\u4e00-\u9fa5\w]+\d*:\s*/, '') + ); + const formattedContent = cleanedItems.join('\n'); + await navigator.clipboard.writeText(formattedContent); setCopiedIndex(index); toast.success('已复制到剪贴板'); @@ -102,17 +95,9 @@ export function ReceiverDialog({ useEffect(() => { if (open) { fetchReceivers(); - setSearchKeyword(''); } }, [open, fetchReceivers]); - /** - * 搜索关键词变化时过滤数据 - */ - useEffect(() => { - handleSearch(searchKeyword); - }, [searchKeyword, handleSearch]); - return ( @@ -146,16 +131,6 @@ export function ReceiverDialog({ /> - {/* 统计信息 */} - {!loading && !error && ( -
- - 共 {receivers.length} 人领取 - {searchKeyword && ` · 筛选出 ${filteredReceivers.length} 条结果`} - -
- )} - {/* 内容区域 */}
{loading ? ( @@ -194,7 +169,11 @@ export function ReceiverDialog({
{receiver.username} ({receiver.nickname}) - - {receiver.content} +
+ {receiver.content.split('$\n*').map((item, itemIndex) => ( +
{item}
+ ))} +
+
+ {contentItems.map((item, index) => ( +
+ + {item} + + +
+ ))}
); diff --git a/frontend/hooks/use-debounce.ts b/frontend/hooks/use-debounce.ts new file mode 100644 index 0000000..cd44737 --- /dev/null +++ b/frontend/hooks/use-debounce.ts @@ -0,0 +1,25 @@ +'use client'; + +import {useEffect, useState} from 'react'; + +/** + * 防抖钩子函数 + * @param value - 需要防抖的值 + * @param delay - 延迟时间,单位毫秒,默认为300ms + * @returns 防抖后的值 + */ +export function useDebounce(value: T, delay = 300): T { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(timer); + }; + }, [value, delay]); + + return debouncedValue; +} diff --git a/frontend/lib/services/project/project.service.ts b/frontend/lib/services/project/project.service.ts index 698791c..39dff9f 100644 --- a/frontend/lib/services/project/project.service.ts +++ b/frontend/lib/services/project/project.service.ts @@ -91,10 +91,24 @@ export class ProjectService extends BaseService { /** * 获取项目领取者列表(仅项目创建者可访问) * @param projectId - 项目ID + * @param current - 当前页码,默认为1 + * @param size - 每页大小,默认为10 + * @param search - 搜索关键词,可选 * @returns 项目领取者列表 */ - static async getProjectReceivers(projectId: string): Promise { - const response = await apiClient.get(`${this.basePath}/${projectId}/receivers`); + static async getProjectReceivers( + projectId: string, + current: number = 1, + size: number = 10, + search: string = '' + ): Promise { + const response = await apiClient.get(`${this.basePath}/${projectId}/receivers`,{ + params: { + current, + size, + search, + }, + }); if (response.data.error_msg) { throw new Error(response.data.error_msg); } @@ -478,15 +492,23 @@ export class ProjectService extends BaseService { /** * 获取项目领取者列表(带错误处理,仅项目创建者可访问) * @param projectId - 项目ID + * @param current - 当前页码,默认为1 + * @param size - 每页大小,默认为10 + * @param search - 搜索关键词,可选 * @returns 获取结果,包含成功状态、领取者列表和错误信息 */ - static async getProjectReceiversSafe(projectId: string): Promise<{ + static async getProjectReceiversSafe( + projectId: string, + current: number = 1, + size: number = 10, + search: string = '' + ): Promise<{ success: boolean; data?: ProjectReceiver[]; error?: string; }> { try { - const data = await this.getProjectReceivers(projectId); + const data = await this.getProjectReceivers(projectId, current, size, search); return { success: true, data, diff --git a/internal/apps/project/models.go b/internal/apps/project/models.go index 81625d6..5b76df3 100644 --- a/internal/apps/project/models.go +++ b/internal/apps/project/models.go @@ -439,7 +439,7 @@ type ProjectItem struct { Content string `json:"content" gorm:"size:1024"` CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` - ReceivedAt *time.Time `json:"received_at"` + ReceivedAt *time.Time `json:"received_at" gorm:"index"` } func (p *ProjectItem) Exact(tx *gorm.DB, id uint64) error { diff --git a/internal/apps/project/routers.go b/internal/apps/project/routers.go index 600e6d4..ffcc0c3 100644 --- a/internal/apps/project/routers.go +++ b/internal/apps/project/routers.go @@ -380,14 +380,14 @@ func ListProjectReceivers(c *gin.Context) { if req.Search != "" { searchPattern := strings.TrimSpace(req.Search) + "%" - if len(searchPattern) <= 20 { - query = query.Where( - "users.username LIKE ? OR users.nickname LIKE ? OR project_items.content LIKE ?", - searchPattern, "%"+searchPattern, "%"+searchPattern) - } else { + if len(searchPattern) > 21 { query = query.Where( "users.nickname LIKE ? OR project_items.content LIKE ?", "%"+searchPattern, "%"+searchPattern) + } else { + query = query.Where( + "users.username LIKE ? OR users.nickname LIKE ? OR project_items.content LIKE ?", + searchPattern, "%"+searchPattern, "%"+searchPattern) } } From 0e068bd5b619bc5ccb216e55346e94ea42d01632 Mon Sep 17 00:00:00 2001 From: yyg-max <175597134+yyg-max@users.noreply.github.com> Date: Fri, 10 Oct 2025 17:00:46 +0800 Subject: [PATCH 3/3] refactor(service, dialog): clean up parameter formatting and improve content handling --- .../common/project/ReceiverDialog.tsx | 4 ++-- .../lib/services/project/project.service.ts | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/frontend/components/common/project/ReceiverDialog.tsx b/frontend/components/common/project/ReceiverDialog.tsx index edcf470..17e0837 100644 --- a/frontend/components/common/project/ReceiverDialog.tsx +++ b/frontend/components/common/project/ReceiverDialog.tsx @@ -65,8 +65,8 @@ export function ReceiverDialog({ try { // 拆分内容并整理成多行格式 const contentItems = content.split('$\n*'); - const cleanedItems = contentItems.map(item => - item.replace(/^[\u4e00-\u9fa5\w]+\d*:\s*/, '') + const cleanedItems = contentItems.map((item) => + item.replace(/^[\u4e00-\u9fa5\w]+\d*:\s*/, ''), ); const formattedContent = cleanedItems.join('\n'); await navigator.clipboard.writeText(formattedContent); diff --git a/frontend/lib/services/project/project.service.ts b/frontend/lib/services/project/project.service.ts index 39dff9f..d36ebfc 100644 --- a/frontend/lib/services/project/project.service.ts +++ b/frontend/lib/services/project/project.service.ts @@ -97,12 +97,12 @@ export class ProjectService extends BaseService { * @returns 项目领取者列表 */ static async getProjectReceivers( - projectId: string, - current: number = 1, - size: number = 10, - search: string = '' + projectId: string, + current: number = 1, + size: number = 10, + search: string = '', ): Promise { - const response = await apiClient.get(`${this.basePath}/${projectId}/receivers`,{ + const response = await apiClient.get(`${this.basePath}/${projectId}/receivers`, { params: { current, size, @@ -498,10 +498,10 @@ export class ProjectService extends BaseService { * @returns 获取结果,包含成功状态、领取者列表和错误信息 */ static async getProjectReceiversSafe( - projectId: string, - current: number = 1, - size: number = 10, - search: string = '' + projectId: string, + current: number = 1, + size: number = 10, + search: string = '', ): Promise<{ success: boolean; data?: ProjectReceiver[];