351 lines
10 KiB
Go
351 lines
10 KiB
Go
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
|
|
}
|