package handler import ( "encoding/json" "net/http" "strconv" "github.com/enterprise-ai-platform/server/internal/middleware" "github.com/enterprise-ai-platform/server/internal/response" "github.com/go-chi/chi/v5" "github.com/jackc/pgx/v5/pgxpool" ) type StoreHandler struct { pool *pgxpool.Pool } func NewStoreHandler(pool *pgxpool.Pool) *StoreHandler { return &StoreHandler{pool: pool} } func (h *StoreHandler) ListCategories(w http.ResponseWriter, r *http.Request) { orgID := r.URL.Query().Get("org_id") query := `SELECT c.id, c.name, c.slug, c.icon, c.description, c.sort_order, COALESCE((SELECT COUNT(*) FROM applications a WHERE a.category_id = c.id AND a.status = 'approved'), 0) AS app_count FROM categories c WHERE c.status = 'active'` var args []any if orgID != "" { query += ` AND (c.org_id = $1 OR c.org_id IS NULL)` args = append(args, orgID) } query += ` ORDER BY c.sort_order ASC` rows, err := h.pool.Query(r.Context(), query, args...) if err != nil { response.InternalError(w, "查询分类失败") return } defer rows.Close() var cats []map[string]any for rows.Next() { var id, name, slug string var icon, desc *string var sortOrder int var appCount int if err := rows.Scan(&id, &name, &slug, &icon, &desc, &sortOrder, &appCount); err != nil { continue } cats = append(cats, map[string]any{ "id": id, "name": name, "slug": slug, "icon": icon, "description": desc, "sort_order": sortOrder, "app_count": appCount, }) } response.JSON(w, http.StatusOK, cats) } func (h *StoreHandler) ListApps(w http.ResponseWriter, r *http.Request) { q := r.URL.Query() page, _ := strconv.Atoi(q.Get("page")) if page < 1 { page = 1 } pageSize, _ := strconv.Atoi(q.Get("page_size")) if pageSize < 1 || pageSize > 50 { pageSize = 20 } offset := (page - 1) * pageSize search := q.Get("q") category := q.Get("category") sort := q.Get("sort") orgFilter := q.Get("org_id") if sort == "" { sort = "popular" } query := ` SELECT a.id, a.name, a.slug, a.description, a.icon_url, c.name as category_name, c.slug as category_slug, u.name as creator_name, a.usage_count, a.favorite_count, a.avg_rating, a.rating_count, a.dify_app_type, a.welcome_message, a.published_at::text FROM applications a LEFT JOIN categories c ON a.category_id = c.id LEFT JOIN users u ON a.creator_id = u.id WHERE a.status = 'approved' AND a.visibility = 'public'` args := []any{} argIdx := 1 // 按机构过滤:显示指定机构的应用 + 无机构归属的全局应用 if orgFilter != "" { query += ` AND (a.org_id = $` + strconv.Itoa(argIdx) + ` OR a.org_id IS NULL)` args = append(args, orgFilter) argIdx++ } if search != "" { query += ` AND to_tsvector('simple', a.name || ' ' || COALESCE(a.description, '')) @@ plainto_tsquery('simple', $` + strconv.Itoa(argIdx) + `)` args = append(args, search) argIdx++ } if category != "" { query += ` AND c.slug = $` + strconv.Itoa(argIdx) args = append(args, category) argIdx++ } switch sort { case "rating": query += ` ORDER BY a.avg_rating DESC, a.usage_count DESC` case "latest": query += ` ORDER BY a.published_at DESC NULLS LAST` default: query += ` ORDER BY a.usage_count DESC` } query += ` LIMIT $` + strconv.Itoa(argIdx) + ` OFFSET $` + strconv.Itoa(argIdx+1) args = append(args, pageSize, offset) rows, err := h.pool.Query(r.Context(), query, args...) if err != nil { response.InternalError(w, "查询应用失败") return } defer rows.Close() var apps []map[string]any for rows.Next() { var ( id, name, slug string desc, iconURL *string catName, catSlug *string creatorName *string usageCount int64 favCount, ratingCt int avgRating float32 difyType, welcome *string publishedAt *string ) if err := rows.Scan(&id, &name, &slug, &desc, &iconURL, &catName, &catSlug, &creatorName, &usageCount, &favCount, &avgRating, &ratingCt, &difyType, &welcome, &publishedAt); err != nil { continue } apps = append(apps, map[string]any{ "id": id, "name": name, "slug": slug, "description": desc, "icon_url": iconURL, "category_name": catName, "category_slug": catSlug, "creator_name": creatorName, "usage_count": usageCount, "favorite_count": favCount, "avg_rating": avgRating, "rating_count": ratingCt, "dify_app_type": difyType, "welcome_message": welcome, "published_at": publishedAt, }) } if apps == nil { apps = []map[string]any{} } response.JSON(w, http.StatusOK, map[string]any{ "items": apps, "page": page, "page_size": pageSize, }) } func (h *StoreHandler) GetApp(w http.ResponseWriter, r *http.Request) { slug := chi.URLParam(r, "slug") var ( id, name, appSlug string desc, longDesc *string iconURL *string catName, catSlug *string creatorName *string usageCount int64 favCount, ratingCt int avgRating float32 difyType, welcome *string suggestedPrompts *string appConfig *string publishedAt *string version string ) err := h.pool.QueryRow(r.Context(), ` SELECT a.id, a.name, a.slug, a.description, a.long_description, a.icon_url, c.name, c.slug, u.name, a.usage_count, a.favorite_count, a.avg_rating, a.rating_count, a.dify_app_type, a.welcome_message, a.suggested_prompts::text, a.app_config::text, a.published_at::text, a.version FROM applications a LEFT JOIN categories c ON a.category_id = c.id LEFT JOIN users u ON a.creator_id = u.id WHERE a.slug = $1 AND a.status = 'approved'`, slug, ).Scan(&id, &name, &appSlug, &desc, &longDesc, &iconURL, &catName, &catSlug, &creatorName, &usageCount, &favCount, &avgRating, &ratingCt, &difyType, &welcome, &suggestedPrompts, &appConfig, &publishedAt, &version) if err != nil { response.NotFound(w, "应用不存在") return } // Check if user favorited this app isFavorited := false userID := middleware.GetUserID(r.Context()) if userID.String() != "00000000-0000-0000-0000-000000000000" { _ = h.pool.QueryRow(r.Context(), `SELECT EXISTS(SELECT 1 FROM app_favorites WHERE user_id = $1 AND app_id = $2)`, userID, id).Scan(&isFavorited) } // Parse app_config as JSON if present var configData any if appConfig != nil { _ = json.Unmarshal([]byte(*appConfig), &configData) } response.JSON(w, http.StatusOK, map[string]any{ "id": id, "name": name, "slug": appSlug, "description": desc, "long_description": longDesc, "icon_url": iconURL, "category_name": catName, "category_slug": catSlug, "creator_name": creatorName, "usage_count": usageCount, "favorite_count": favCount, "avg_rating": avgRating, "rating_count": ratingCt, "dify_app_type": difyType, "welcome_message": welcome, "suggested_prompts": suggestedPrompts, "app_config": configData, "published_at": publishedAt, "version": version, "is_favorited": isFavorited, }) } func (h *StoreHandler) Featured(w http.ResponseWriter, r *http.Request) { orgID := r.URL.Query().Get("org_id") query := ` SELECT a.id, a.name, a.slug, a.description, a.icon_url, c.name as category_name, c.slug as category_slug, a.usage_count, a.avg_rating, a.rating_count, a.dify_app_type FROM applications a LEFT JOIN categories c ON a.category_id = c.id WHERE a.is_featured = true AND a.status = 'approved' AND a.visibility = 'public'` var args []any if orgID != "" { query += ` AND (a.org_id = $1 OR a.org_id IS NULL)` args = append(args, orgID) } query += ` ORDER BY a.usage_count DESC LIMIT 4` rows, err := h.pool.Query(r.Context(), query, args...) if err != nil { response.InternalError(w, "查询精选应用失败") return } defer rows.Close() apps := scanAppList(rows) response.JSON(w, http.StatusOK, apps) } func (h *StoreHandler) Rankings(w http.ResponseWriter, r *http.Request) { orgID := r.URL.Query().Get("org_id") query := ` SELECT a.id, a.name, a.slug, a.description, a.icon_url, c.name as category_name, c.slug as category_slug, a.usage_count, a.avg_rating, a.rating_count, a.dify_app_type FROM applications a LEFT JOIN categories c ON a.category_id = c.id WHERE a.status = 'approved' AND a.visibility = 'public'` var args []any if orgID != "" { query += ` AND (a.org_id = $1 OR a.org_id IS NULL)` args = append(args, orgID) } query += ` ORDER BY a.usage_count DESC LIMIT 50` rows, err := h.pool.Query(r.Context(), query, args...) if err != nil { response.InternalError(w, "查询排行榜失败") return } defer rows.Close() apps := scanAppList(rows) response.JSON(w, http.StatusOK, apps) } func (h *StoreHandler) Recent(w http.ResponseWriter, r *http.Request) { userID := middleware.GetUserID(r.Context()) rows, err := h.pool.Query(r.Context(), ` SELECT DISTINCT ON (a.id) a.id, a.name, a.slug, a.description, a.icon_url, c.name as category_name, c.slug as category_slug, a.usage_count, a.avg_rating, a.rating_count, a.dify_app_type FROM app_usage_logs l JOIN applications a ON l.app_id = a.id LEFT JOIN categories c ON a.category_id = c.id WHERE l.user_id = $1 ORDER BY a.id, l.created_at DESC LIMIT 10`, userID) if err != nil { response.InternalError(w, "查询最近使用失败") return } defer rows.Close() apps := scanAppList(rows) response.JSON(w, http.StatusOK, apps) } func scanAppList(rows interface { Next() bool Scan(dest ...any) error }) []map[string]any { var apps []map[string]any for rows.Next() { var ( id, name, slug string desc, iconURL *string catName, catSlug *string usageCount int64 avgRating float32 ratingCt int difyType *string ) if err := rows.Scan(&id, &name, &slug, &desc, &iconURL, &catName, &catSlug, &usageCount, &avgRating, &ratingCt, &difyType); err != nil { continue } apps = append(apps, map[string]any{ "id": id, "name": name, "slug": slug, "description": desc, "icon_url": iconURL, "category_name": catName, "category_slug": catSlug, "usage_count": usageCount, "avg_rating": avgRating, "rating_count": ratingCt, "dify_app_type": difyType, }) } if apps == nil { apps = []map[string]any{} } return apps }