Initial commit: GovAI 政务AI平台

This commit is contained in:
freedakgmail
2026-06-15 23:48:37 +08:00
commit 0f490f72a9
245 changed files with 51669 additions and 0 deletions
+350
View File
@@ -0,0 +1,350 @@
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
}