宇恒一号官网

This commit is contained in:
whm
2026-03-17 00:59:32 +08:00
commit eb56519df7
105 changed files with 10783 additions and 0 deletions

199
server/handlers/auth.go Normal file
View File

@@ -0,0 +1,199 @@
package handlers
import (
"context"
"errors"
"net/http"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"yh_web/server/config"
"yh_web/server/models"
"yh_web/server/pkg/logger"
"yh_web/server/utils"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
const jwtSecret = "yh_web_admin_jwt_secret_change_in_production"
const jwtExpire = 24 * time.Hour
type LoginInput struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type Claims struct {
UserID string `json:"user_id"`
Username string `json:"username"`
RoleID int `json:"role_id"`
Role string `json:"role"`
jwt.RegisteredClaims
}
// Login 后台登录,仅 role_id=9527 超级管理员可登录
func Login(c *gin.Context) {
var input LoginInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "用户名和密码不能为空"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("users")
var user models.User
err := coll.FindOne(ctx, bson.M{"username": input.Username}).Decode(&user)
if err != nil {
if errors.Is(err, mongo.ErrNoDocuments) {
// 尝试用手机号登录
err = coll.FindOne(ctx, bson.M{"mobile": input.Username}).Decode(&user)
}
if err != nil {
if errors.Is(err, mongo.ErrNoDocuments) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
return
}
logger.Err("handlers/auth", "[Login] FindOne error: %v", err)
resp := gin.H{"error": "登录失败,请稍后重试"}
if gin.Mode() == gin.DebugMode {
resp["debug"] = err.Error()
}
c.JSON(http.StatusInternalServerError, resp)
return
}
}
// 超级管理员(9527)或超级用户(role_id=0, role=admin)可登录后台
if user.RoleID != models.RoleIDSuperAdmin && !(user.RoleID == models.RoleIDSuperUser && user.Role == "admin") {
c.JSON(http.StatusForbidden, gin.H{"error": "无后台访问权限"})
return
}
roleID := user.RoleID
if roleID == 0 && user.Role == "admin" {
roleID = models.RoleIDSuperAdmin
}
hashed := utils.HashPassword(input.Password)
if hashed != user.Password {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
return
}
claims := Claims{
UserID: user.ID.Hex(),
Username: user.Username,
RoleID: roleID,
Role: user.Role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(jwtExpire)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenStr, err := token.SignedString([]byte(jwtSecret))
if err != nil {
logger.Err("handlers/auth", "JWT SignedString error: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "生成令牌失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"token": tokenStr,
"user": gin.H{
"id": user.ID.Hex(),
"username": user.Username,
"role_id": roleID,
"role": user.Role,
},
"expires_in": int64(jwtExpire.Seconds()),
})
}
// AuthRequired 鉴权中间件,要求 role_id=9527
func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
tokenStr := c.GetHeader("Authorization")
if tokenStr == "" {
tokenStr = c.Query("token")
}
if len(tokenStr) > 7 && tokenStr[:7] == "Bearer " {
tokenStr = tokenStr[7:]
}
if tokenStr == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
c.Abort()
return
}
var claims Claims
token, err := jwt.ParseWithClaims(tokenStr, &claims, func(t *jwt.Token) (interface{}, error) {
return []byte(jwtSecret), nil
})
if err != nil || !token.Valid {
c.JSON(http.StatusUnauthorized, gin.H{"error": "登录已过期,请重新登录"})
c.Abort()
return
}
// 仅超级管理员或超级用户(role_id=0, role=admin)可访问后台
if claims.RoleID != models.RoleIDSuperAdmin && !(claims.RoleID == models.RoleIDSuperUser && claims.Role == "admin") {
c.JSON(http.StatusForbidden, gin.H{"error": "无后台访问权限"})
c.Abort()
return
}
c.Set("user_id", claims.UserID)
c.Set("username", claims.Username)
c.Set("role_id", claims.RoleID)
c.Set("role", claims.Role)
c.Next()
}
}
// SuperUserAuthRequired 超级用户鉴权:仅 role_id=0 且 role=admin 可访问(如短信平台配置)
// 集团超级用户 username=admin 只能配置集团信息,不能配置短信
func SuperUserAuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
tokenStr := c.GetHeader("Authorization")
if tokenStr == "" {
tokenStr = c.Query("token")
}
if len(tokenStr) > 7 && tokenStr[:7] == "Bearer " {
tokenStr = tokenStr[7:]
}
if tokenStr == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
c.Abort()
return
}
var claims Claims
token, err := jwt.ParseWithClaims(tokenStr, &claims, func(t *jwt.Token) (interface{}, error) {
return []byte(jwtSecret), nil
})
if err != nil || !token.Valid {
c.JSON(http.StatusUnauthorized, gin.H{"error": "登录已过期,请重新登录"})
c.Abort()
return
}
// 仅 role_id=9527 且 role=admin 可配置短信平台
if claims.RoleID != models.RoleIDSuperAdmin || claims.Role != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "仅超级管理员可配置短信平台"})
c.Abort()
return
}
c.Set("user_id", claims.UserID)
c.Set("username", claims.Username)
c.Set("role_id", claims.RoleID)
c.Set("role", claims.Role)
c.Next()
}
}

View File

@@ -0,0 +1,67 @@
package handlers
import (
"context"
"net/http"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo/options"
"yh_web/server/config"
"github.com/gin-gonic/gin"
)
func GetConversations(c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("conversations")
page := 1
pageSize := 20
if p := c.Query("page"); p != "" {
if v, err := parseInt(p); err == nil && v > 0 {
page = v
}
}
if ps := c.Query("page_size"); ps != "" {
if v, err := parseInt(ps); err == nil && v > 0 && v <= 100 {
pageSize = v
}
}
skip := (page - 1) * pageSize
opts := options.Find().SetSkip(int64(skip)).SetLimit(int64(pageSize)).SetSort(bson.D{{Key: "_id", Value: -1}})
filter := bson.M{}
if userID := c.Query("user_id"); userID != "" {
filter["user_id"] = userID
}
if workspaceID := c.Query("workspace_id"); workspaceID != "" {
filter["workspace_id"] = workspaceID
}
cursor, err := coll.Find(ctx, filter, opts)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer cursor.Close(ctx)
var list []map[string]interface{}
if err = cursor.All(ctx, &list); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
total, _ := coll.CountDocuments(ctx, filter)
c.JSON(http.StatusOK, gin.H{
"list": list,
"total": total,
"page": page,
"page_size": pageSize,
})
}

View File

@@ -0,0 +1,7 @@
package handlers
import "strconv"
func parseInt(s string) (int, error) {
return strconv.Atoi(s)
}

345
server/handlers/homepage.go Normal file
View File

@@ -0,0 +1,345 @@
package handlers
import (
"context"
"encoding/json"
"net/http"
"strings"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
"yh_web/server/config"
"yh_web/server/models"
"github.com/gin-gonic/gin"
)
const homepageSlug = "index"
const officialSiteConfigID = "official_site_id"
type officialSiteDoc struct {
ID string `bson:"_id"`
SiteID string `bson:"site_id"`
}
// getOfficialSiteID 从 system_config 读取官网站点 ID未设置则返回第一个站点的 ID
func getOfficialSiteID(ctx context.Context) string {
coll := config.GetDB(config.DBName).Collection("system_config")
var doc officialSiteDoc
err := coll.FindOne(ctx, bson.M{"_id": officialSiteConfigID}).Decode(&doc)
if err == nil && doc.SiteID != "" {
return doc.SiteID
}
sitesColl := config.GetDB(config.DBName).Collection("sites")
opts := options.FindOne().SetSort(bson.D{{Key: "created_at", Value: -1}})
var site models.Site
if err := sitesColl.FindOne(ctx, bson.M{}, opts).Decode(&site); err == nil {
return site.ID.Hex()
}
return ""
}
// GetWebHomepage 前台:获取官网站点首页数据(无需鉴权)
func GetWebHomepage(c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
siteID := getOfficialSiteID(ctx)
if siteID == "" {
c.JSON(http.StatusOK, defaultHomepageData())
return
}
coll := config.GetDB(config.DBName).Collection("pages")
var page models.Page
err := coll.FindOne(ctx, bson.M{"site_id": siteID, "slug": homepageSlug}).Decode(&page)
if err != nil {
if err == mongo.ErrNoDocuments {
c.JSON(http.StatusOK, defaultHomepageData())
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var data models.HomepageData
if page.Content != "" {
if err := json.Unmarshal([]byte(page.Content), &data); err != nil {
c.JSON(http.StatusOK, defaultHomepageData())
return
}
} else {
data = defaultHomepageData()
}
c.JSON(http.StatusOK, data)
}
// GetHomepage 获取站点首页数据
func GetHomepage(c *gin.Context) {
siteID := c.Param("site_id")
if siteID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("pages")
var page models.Page
err := coll.FindOne(ctx, bson.M{"site_id": siteID, "slug": homepageSlug}).Decode(&page)
if err != nil {
if err == mongo.ErrNoDocuments {
c.JSON(http.StatusOK, defaultHomepageData())
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
var data models.HomepageData
if page.Content != "" {
if err := json.Unmarshal([]byte(page.Content), &data); err != nil {
c.JSON(http.StatusOK, defaultHomepageData())
return
}
} else {
data = defaultHomepageData()
}
c.JSON(http.StatusOK, data)
}
// UpdateHomepage 更新站点首页数据
func UpdateHomepage(c *gin.Context) {
siteID := c.Param("site_id")
if siteID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
return
}
var data models.HomepageData
if err := c.ShouldBindJSON(&data); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
body, err := json.Marshal(data)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("pages")
now := time.Now().Format(time.RFC3339)
filter := bson.M{"site_id": siteID, "slug": homepageSlug}
update := bson.M{
"$set": bson.M{
"site_id": siteID,
"slug": homepageSlug,
"title": data.Title,
"type": "homepage",
"content": string(body),
"updated_at": now,
},
}
opts := options.UpdateOne().SetUpsert(true)
_, err = coll.UpdateOne(ctx, filter, update, opts)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "保存成功"})
}
// DownloadHomepage 下载首页 HTML
func DownloadHomepage(c *gin.Context) {
siteID := c.Param("site_id")
if siteID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("pages")
var page models.Page
var data models.HomepageData
err := coll.FindOne(ctx, bson.M{"site_id": siteID, "slug": homepageSlug}).Decode(&page)
if err == nil && page.Content != "" {
_ = json.Unmarshal([]byte(page.Content), &data)
}
if err != nil || page.Content == "" {
data = defaultHomepageData()
}
html := renderHomepageHTML(&data)
c.Header("Content-Disposition", "attachment; filename=index.html")
c.Data(http.StatusOK, "text/html; charset=utf-8", []byte(html))
}
func defaultHomepageData() models.HomepageData {
return models.HomepageData{
LogoText: "YUHENG ONE",
NavLinks: []models.NavLink{{Label: "MISSION", URL: "#"}, {Label: "DOWNLOAD", URL: "#"}, {Label: "CONTACT", URL: "#"}},
Title: "宇恒一号",
Subtitle: "INTERSTELLAR EXPLORER EDITION",
Description: "跨越星际的智能伙伴 · 探索无限可能<br>\n 引领您进入前所未有的数字宇宙",
DownloadText: "START EXPLORING",
DownloadURL: "#",
Platforms: []models.PlatformItem{
{Name: "WINDOWS", URL: "#"},
{Name: "MACOS", URL: "#"},
{Name: "LINUX", URL: "#"},
{Name: "IOS", URL: "#"},
{Name: "ANDROID", URL: "#"},
},
Version: "VERSION 3.2.1",
LaunchYear: "LAUNCH: 2024",
BadgeText: "FREE ACCESS",
Features: []models.FeatureItem{
{Title: "星际导航", Desc: "先进的AI导航系统精准定位您的需求引领探索之旅"},
{Title: "量子同步", Desc: "跨维度数据同步技术,您的数据在多宇宙中保持一致"},
{Title: "星际防护", Desc: "来自未来的安全加密协议,守护您的数字资产安全"},
},
FooterText: "© 2024 YUHENG ONE // STELLAR EXPLORATION INITIATIVE",
}
}
// renderHomepageHTML 根据数据生成首页 HTML简化版保留原样式与结构
func renderHomepageHTML(d *models.HomepageData) string {
if d == nil {
d = &models.HomepageData{}
}
titleChars := splitTitle(d.Title)
navHTML := ""
for _, l := range d.NavLinks {
navHTML += `<a href="` + escape(l.URL) + `" style="color: rgba(255,255,255,0.5); text-decoration: none; transition: color 0.3s;">` + escape(l.Label) + `</a>`
}
platformsHTML := ""
for _, p := range d.Platforms {
platformsHTML += `<div class="orbit-platform"><svg viewBox="0 0 24 24"><path d="M20 18c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2H0v2h24v-2h-4zM4 6h16v10H4V6z"/></svg><span>` + escape(p.Name) + `</span></div>`
}
featuresHTML := ""
for i, f := range d.Features {
iconPath := []string{
"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z",
"M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM19 18H6c-2.21 0-4-1.79-4-4s1.79-4 4-4h.71C7.37 7.69 9.48 6 12 6c3.04 0 5.5 2.46 5.5 5.5v.5H19c1.66 0 3 1.34 3 3s-1.34 3-3 3z",
"M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm0 10.99h7c-.53 4.12-3.28 7.79-7 8.94V12H5V6.3l7-3.11v8.8z",
}
if i >= len(iconPath) {
iconPath = append(iconPath, iconPath[0])
}
path := iconPath[i%3]
featuresHTML += `<div class="feature-space"><div class="feature-icon"><svg viewBox="0 0 24 24"><path d="` + path + `"/></svg></div><h3>` + escape(f.Title) + `</h3><p>` + escape(f.Desc) + `</p></div>`
}
sb := &strings.Builder{}
sb.WriteString("<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>")
sb.WriteString(escape(d.Title))
sb.WriteString(" - 星际探索版</title>\n")
sb.WriteString(homepageCSS)
sb.WriteString("</head>\n<body>\n<div class=\"space-bg\"></div>\n<div class=\"stars\" id=\"stars\"></div>\n<div class=\"planet planet-1\"></div>\n<div class=\"planet planet-2\"></div>\n<nav class=\"navbar\"><div class=\"logo-space\">")
sb.WriteString(escape(d.LogoText))
sb.WriteString("</div>\n<div style=\"display: flex; gap: 35px; font-family: 'Exo 2', sans-serif; font-size: 12px; letter-spacing: 2px;\">")
sb.WriteString(navHTML)
sb.WriteString("</div>\n</nav>\n<section class=\"hero\">\n<div class=\"title-container\"><h1 class=\"title-3d\">")
for _, ch := range titleChars {
sb.WriteString("<span>" + escape(ch) + "</span>")
}
sb.WriteString("</h1>\n</div>\n<p class=\"subtitle-space\">")
sb.WriteString(escape(d.Subtitle))
sb.WriteString("</p>\n<p class=\"description-space\">")
sb.WriteString(strings.ReplaceAll(escape(d.Description), "\n", "<br>\n"))
sb.WriteString("</p>\n<div class=\"download-warp\">\n<div class=\"warp-effect\"></div>\n<a href=\"")
sb.WriteString(escape(d.DownloadURL))
sb.WriteString("\" class=\"warp-btn\">\n<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z\"/></svg>\n")
sb.WriteString(escape(d.DownloadText))
sb.WriteString("\n</a>\n</div>\n<div class=\"orbit-platforms\">")
sb.WriteString(platformsHTML)
sb.WriteString("</div>\n<div class=\"mission-info\">\n<span class=\"mission-badge\">")
sb.WriteString(escape(d.Version))
sb.WriteString("</span>\n<span>🚀 ")
sb.WriteString(escape(d.LaunchYear))
sb.WriteString("</span>\n<span>⚡ ")
sb.WriteString(escape(d.BadgeText))
sb.WriteString("</span>\n</div>\n<div class=\"features-space\">")
sb.WriteString(featuresHTML)
sb.WriteString("</div>\n</section>\n<footer>\n<p>")
sb.WriteString(escape(d.FooterText))
sb.WriteString("</p>\n</footer>\n<script>\nconst starsContainer = document.getElementById('stars');\nfor (let i = 0; i < 200; i++) {\nconst star = document.createElement('div');\nstar.className = 'star';\nstar.style.left = Math.random() * 100 + '%';\nstar.style.top = Math.random() * 100 + '%';\nstar.style.width = (Math.random() * 2 + 1) + 'px';\nstar.style.height = star.style.width;\nstar.style.setProperty('--duration', (Math.random() * 3 + 2) + 's');\nstar.style.setProperty('--min-opacity', Math.random() * 0.3 + 0.1);\nstarsContainer.appendChild(star);\n}\n</script>\n</body>\n</html>")
return sb.String()
}
func splitTitle(s string) []string {
var out []rune
for _, r := range s {
out = append(out, r)
}
if len(out) == 0 {
return []string{"宇", "恒", "一", "号"}
}
result := make([]string, len(out))
for i, r := range out {
result[i] = string(r)
}
return result
}
func escape(s string) string {
s = strings.ReplaceAll(s, "&", "&amp;")
s = strings.ReplaceAll(s, "<", "&lt;")
s = strings.ReplaceAll(s, ">", "&gt;")
s = strings.ReplaceAll(s, "\"", "&quot;")
return s
}
const homepageCSS = `<style>
@import url('https://fonts.googleapis.com/css2?family=Exo+2:wght@300;400;700;900&family=Noto+Sans+SC:wght@300;400;700&display=swap');
*{margin:0;padding:0;box-sizing:border-box;}
:root{--space-dark:#0a0a12;--space-blue:#1e3a5f;--nebula-purple:#4a1a6b;--star-white:#fff;--plasma-cyan:#00d4ff;--plasma-pink:#ff2d95;}
body{font-family:'Noto Sans SC',sans-serif;background:var(--space-dark);color:var(--star-white);min-height:100vh;overflow-x:hidden;}
.space-bg{position:fixed;top:0;left:0;width:100%;height:100%;z-index:0;background:radial-gradient(ellipse at 20% 80%,rgba(74,26,107,0.3) 0%,transparent 50%),radial-gradient(ellipse at 80% 20%,rgba(30,58,95,0.3) 0%,transparent 50%);}
.stars{position:fixed;top:0;left:0;width:100%;height:100%;z-index:1;pointer-events:none;}
.star{position:absolute;background:#fff;border-radius:50%;animation:twinkle 3s ease-in-out infinite;}
@keyframes twinkle{0%,100%{opacity:0.3;transform:scale(1);}50%{opacity:1;transform:scale(1.2);}}
.planet{position:fixed;border-radius:50%;z-index:2;pointer-events:none;}
.planet-1{width:300px;height:300px;top:10%;right:-100px;background:linear-gradient(135deg,var(--nebula-purple),#1a0a2e);opacity:0.6;}
.planet-2{width:150px;height:150px;bottom:20%;left:-50px;background:linear-gradient(135deg,var(--space-blue),#0a1520);opacity:0.5;}
.navbar{position:fixed;top:0;left:0;right:0;padding:25px 50px;display:flex;justify-content:space-between;align-items:center;z-index:100;background:linear-gradient(180deg,rgba(10,10,18,0.9) 0%,transparent 100%);}
.logo-space{font-family:'Exo 2',sans-serif;font-size:26px;font-weight:900;background:linear-gradient(90deg,var(--plasma-cyan),var(--star-white),var(--plasma-pink));-webkit-background-clip:text;-webkit-text-fill-color:transparent;letter-spacing:4px;}
.hero{min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:100px 20px;position:relative;z-index:10;}
.title-container{perspective:1000px;margin-bottom:30px;}
.title-3d{font-family:'Exo 2',sans-serif;font-size:clamp(60px,12vw,150px);font-weight:900;color:var(--star-white);text-shadow:0 0 20px rgba(0,212,255,0.3);}
.subtitle-space{font-family:'Exo 2',sans-serif;font-size:clamp(16px,3vw,24px);letter-spacing:12px;color:var(--plasma-cyan);margin-bottom:25px;}
.description-space{max-width:650px;text-align:center;color:rgba(255,255,255,0.6);line-height:2;font-size:16px;margin-bottom:50px;}
.download-warp{position:relative;display:inline-block;padding:0;background:transparent;border:none;cursor:pointer;}
.download-warp .warp-btn{display:flex;align-items:center;gap:15px;padding:22px 45px;font-size:16px;font-weight:700;color:var(--space-dark);background:linear-gradient(135deg,var(--plasma-cyan),var(--plasma-pink));border:none;font-family:'Exo 2',sans-serif;letter-spacing:2px;text-decoration:none;clip-path:polygon(20px 0,100% 0,100% calc(100% - 20px),calc(100% - 20px) 100%,0 100%,0 20px);transition:all 0.4s;}
.download-warp .warp-btn:hover{transform:scale(1.05);box-shadow:0 0 30px rgba(0,212,255,0.6);}
.warp-effect{position:absolute;top:50%;left:50%;width:0;height:0;background:radial-gradient(circle,rgba(255,255,255,0.8) 0%,transparent 70%);border-radius:50%;transform:translate(-50%,-50%);animation:warp-drive 1.5s ease-out infinite;}
@keyframes warp-drive{0%{width:0;height:0;opacity:1;}100%{width:300px;height:300px;opacity:0;}}
.orbit-platforms{display:flex;gap:25px;margin-top:70px;flex-wrap:wrap;justify-content:center;}
.orbit-platform{width:90px;height:90px;background:rgba(30,58,95,0.3);border:1px solid rgba(0,212,255,0.3);border-radius:50%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;cursor:pointer;transition:all 0.4s;}
.orbit-platform:hover{background:rgba(0,212,255,0.2);border-color:var(--plasma-cyan);transform:translateY(-10px);}
.orbit-platform svg{width:28px;height:28px;fill:var(--star-white);}
.orbit-platform span{font-size:10px;color:rgba(255,255,255,0.6);}
.mission-info{margin-top:60px;display:flex;gap:40px;flex-wrap:wrap;justify-content:center;font-size:13px;color:rgba(255,255,255,0.4);}
.mission-info .mission-badge{padding:4px 12px;background:rgba(0,212,255,0.1);border:1px solid rgba(0,212,255,0.3);border-radius:20px;color:var(--plasma-cyan);font-size:11px;}
.features-space{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:30px;max-width:1100px;margin:80px auto 0;padding:0 20px;}
.feature-space{background:linear-gradient(135deg,rgba(30,58,95,0.2),rgba(10,10,18,0.8));border:1px solid rgba(0,212,255,0.1);border-radius:20px;padding:35px;transition:all 0.5s;}
.feature-space:hover{transform:translateY(-15px);border-color:rgba(0,212,255,0.4);}
.feature-icon{width:55px;height:55px;background:linear-gradient(135deg,var(--plasma-cyan),var(--plasma-pink));border-radius:15px;display:flex;align-items:center;justify-content:center;margin-bottom:20px;}
.feature-icon svg{width:28px;height:28px;fill:var(--space-dark);}
.feature-space h3{font-family:'Exo 2',sans-serif;font-size:18px;color:var(--star-white);margin-bottom:12px;}
.feature-space p{color:rgba(255,255,255,0.5);font-size:14px;line-height:1.7;}
footer{padding:40px;text-align:center;border-top:1px solid rgba(255,255,255,0.05);margin-top:80px;}
footer p{color:rgba(255,255,255,0.3);font-size:12px;}
@media(max-width:768px){.planet{display:none;}}
</style>
`

View File

@@ -0,0 +1,145 @@
package handlers
import (
"context"
"net/http"
"os"
"path/filepath"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo/options"
"yh_web/server/config"
"yh_web/server/models"
"github.com/gin-gonic/gin"
)
const uploadDir = "uploads"
// ListSiteAssets 站点功能模块/上传文件列表
func ListSiteAssets(c *gin.Context) {
siteID := c.Param("site_id")
if siteID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("site_assets")
opts := options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}})
cursor, err := coll.Find(ctx, bson.M{"site_id": siteID}, opts)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer cursor.Close(ctx)
var list []models.SiteAsset
if err = cursor.All(ctx, &list); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
total, _ := coll.CountDocuments(ctx, bson.M{"site_id": siteID})
c.JSON(http.StatusOK, gin.H{"list": list, "total": total})
}
// UploadSiteAsset 上传功能模块/文件
func UploadSiteAsset(c *gin.Context) {
siteID := c.Param("site_id")
if siteID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
return
}
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择要上传的文件"})
return
}
baseDir := filepath.Join(uploadDir, "sites", siteID)
if err := os.MkdirAll(baseDir, 0755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建目录失败"})
return
}
// 避免覆盖:加时间戳
name := file.Filename
ext := filepath.Ext(name)
nameNoExt := name[:len(name)-len(ext)]
saveName := nameNoExt + "_" + time.Now().Format("20060102150405") + ext
relPath := filepath.Join("sites", siteID, saveName)
destPath := filepath.Join(uploadDir, relPath)
if err := c.SaveUploadedFile(file, destPath); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
doc := models.SiteAsset{
SiteID: siteID,
Name: file.Filename,
FilePath: relPath,
Size: file.Size,
ContentType: file.Header.Get("Content-Type"),
CreatedAt: time.Now().Format(time.RFC3339),
}
res, err := config.GetDB(config.DBName).Collection("site_assets").InsertOne(ctx, bson.M{
"site_id": doc.SiteID,
"name": doc.Name,
"file_path": doc.FilePath,
"size": doc.Size,
"content_type": doc.ContentType,
"created_at": doc.CreatedAt,
})
if err != nil {
os.Remove(destPath)
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"id": res.InsertedID, "file_path": doc.FilePath, "message": "上传成功"})
}
// DeleteSiteAsset 删除站点资源
func DeleteSiteAsset(c *gin.Context) {
siteID := c.Param("site_id")
idStr := c.Param("asset_id")
if siteID == "" || idStr == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
oid, err := bson.ObjectIDFromHex(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("site_assets")
var asset models.SiteAsset
err = coll.FindOne(ctx, bson.M{"_id": oid, "site_id": siteID}).Decode(&asset)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "资源不存在"})
return
}
fullPath := filepath.Join(uploadDir, asset.FilePath)
os.Remove(fullPath)
_, err = coll.DeleteOne(ctx, bson.M{"_id": oid})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}

View File

@@ -0,0 +1,50 @@
package handlers
import (
"context"
"net/http"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo/options"
"yh_web/server/config"
"github.com/gin-gonic/gin"
)
// GetOfficialSite 获取官网站点 ID
func GetOfficialSite(c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
siteID := getOfficialSiteID(ctx)
c.JSON(http.StatusOK, gin.H{"site_id": siteID})
}
// SetOfficialSiteInput 设置官网站点
type SetOfficialSiteInput struct {
SiteID string `json:"site_id" binding:"required"`
}
// SetOfficialSite 设置官网站点
func SetOfficialSite(c *gin.Context) {
var input SetOfficialSiteInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("system_config")
doc := officialSiteDoc{ID: officialSiteConfigID, SiteID: input.SiteID}
opts := options.UpdateOne().SetUpsert(true)
_, err := coll.UpdateOne(ctx, bson.M{"_id": officialSiteConfigID}, bson.M{"$set": doc}, opts)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "已设为官网站点", "site_id": input.SiteID})
}

175
server/handlers/page.go Normal file
View File

@@ -0,0 +1,175 @@
package handlers
import (
"context"
"net/http"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo/options"
"yh_web/server/config"
"yh_web/server/models"
"github.com/gin-gonic/gin"
)
// GetPages 网页列表(按站点)
func GetPages(c *gin.Context) {
siteID := c.Query("site_id")
if siteID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "请提供 site_id"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("pages")
opts := options.Find().SetSort(bson.D{{Key: "updated_at", Value: -1}})
cursor, err := coll.Find(ctx, bson.M{"site_id": siteID}, opts)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer cursor.Close(ctx)
var list []models.Page
if err = cursor.All(ctx, &list); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
total, _ := coll.CountDocuments(ctx, bson.M{"site_id": siteID})
c.JSON(http.StatusOK, gin.H{"list": list, "total": total})
}
// GetPageByID 单页
func GetPageByID(c *gin.Context) {
idStr := c.Param("id")
oid, err := bson.ObjectIDFromHex(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的页面ID"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var page models.Page
err = config.GetDB(config.DBName).Collection("pages").FindOne(ctx, bson.M{"_id": oid}).Decode(&page)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "页面不存在"})
return
}
c.JSON(http.StatusOK, page)
}
// CreatePageInput 创建网页
type CreatePageInput struct {
SiteID string `json:"site_id" binding:"required"`
Slug string `json:"slug" binding:"required"`
Title string `json:"title" binding:"required"`
Type string `json:"type"` // homepage, page
Content string `json:"content"`
}
// CreatePage 创建网页
func CreatePage(c *gin.Context) {
var input CreatePageInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写 site_id、slug、title"})
return
}
if input.Type == "" {
input.Type = "page"
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("pages")
now := time.Now().Format(time.RFC3339)
doc := bson.M{
"site_id": input.SiteID,
"slug": input.Slug,
"title": input.Title,
"type": input.Type,
"content": input.Content,
"updated_at": now,
}
res, err := coll.InsertOne(ctx, doc)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"id": res.InsertedID, "message": "创建成功"})
}
// UpdatePageInput 更新网页
type UpdatePageInput struct {
Slug *string `json:"slug"`
Title *string `json:"title"`
Type *string `json:"type"`
Content *string `json:"content"`
}
// UpdatePage 更新网页
func UpdatePage(c *gin.Context) {
idStr := c.Param("id")
oid, err := bson.ObjectIDFromHex(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的页面ID"})
return
}
var input UpdatePageInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
set := bson.M{"updated_at": time.Now().Format(time.RFC3339)}
if input.Slug != nil {
set["slug"] = *input.Slug
}
if input.Title != nil {
set["title"] = *input.Title
}
if input.Type != nil {
set["type"] = *input.Type
}
if input.Content != nil {
set["content"] = *input.Content
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err = config.GetDB(config.DBName).Collection("pages").UpdateOne(ctx, bson.M{"_id": oid}, bson.M{"$set": set})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "更新成功"})
}
// DeletePage 删除网页
func DeletePage(c *gin.Context) {
idStr := c.Param("id")
oid, err := bson.ObjectIDFromHex(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的页面ID"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err = config.GetDB(config.DBName).Collection("pages").DeleteOne(ctx, bson.M{"_id": oid})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}

View File

@@ -0,0 +1,128 @@
package handlers
import (
"context"
"net/http"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
"yh_web/server/config"
"yh_web/server/models"
"github.com/gin-gonic/gin"
)
const paymentConfigDocID = "payment"
// GetPaymentConfig 获取支付配置(仅 role_id=9527, role=admin
func GetPaymentConfig(c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("system_config")
var cfg models.PaymentConfig
err := coll.FindOne(ctx, bson.M{"_id": paymentConfigDocID}).Decode(&cfg)
if err != nil {
if err == mongo.ErrNoDocuments {
c.JSON(http.StatusOK, models.PaymentConfig{})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, cfg)
}
// PaymentConfigUpdateInput 支付配置更新
type PaymentConfigUpdateInput struct {
Wechat *WechatPayUpdateInput `json:"wechat"`
Alipay *AlipayUpdateInput `json:"alipay"`
}
type WechatPayUpdateInput struct {
AppID string `json:"app_id"`
MchID string `json:"mch_id"`
APIKey string `json:"api_key"`
APIKeyV3 string `json:"api_key_v3"`
Enabled *bool `json:"enabled"`
}
type AlipayUpdateInput struct {
AppID string `json:"app_id"`
PrivateKey string `json:"private_key"`
AlipayPublicKey string `json:"alipay_public_key"`
Enabled *bool `json:"enabled"`
}
// UpdatePaymentConfig 更新支付配置
func UpdatePaymentConfig(c *gin.Context) {
var input PaymentConfigUpdateInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("system_config")
set := bson.M{}
if input.Wechat != nil {
w := bson.M{}
if input.Wechat.AppID != "" {
w["app_id"] = input.Wechat.AppID
}
if input.Wechat.MchID != "" {
w["mch_id"] = input.Wechat.MchID
}
if input.Wechat.APIKey != "" {
w["api_key"] = input.Wechat.APIKey
}
if input.Wechat.APIKeyV3 != "" {
w["api_key_v3"] = input.Wechat.APIKeyV3
}
if input.Wechat.Enabled != nil {
w["enabled"] = *input.Wechat.Enabled
}
for k, v := range w {
set["wechat."+k] = v
}
}
if input.Alipay != nil {
a := bson.M{}
if input.Alipay.AppID != "" {
a["app_id"] = input.Alipay.AppID
}
if input.Alipay.PrivateKey != "" {
a["private_key"] = input.Alipay.PrivateKey
}
if input.Alipay.AlipayPublicKey != "" {
a["alipay_public_key"] = input.Alipay.AlipayPublicKey
}
if input.Alipay.Enabled != nil {
a["enabled"] = *input.Alipay.Enabled
}
for k, v := range a {
set["alipay."+k] = v
}
}
if len(set) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "无有效更新字段"})
return
}
opts := options.UpdateOne().SetUpsert(true)
_, err := coll.UpdateOne(ctx, bson.M{"_id": paymentConfigDocID}, bson.M{"$set": set}, opts)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "配置已保存"})
}

View File

@@ -0,0 +1,96 @@
package handlers
import (
"context"
"net/http"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"yh_web/server/config"
"yh_web/server/models"
"github.com/gin-gonic/gin"
)
// roleIDFromContext 从 context 安全取 role_idJWT 等可能解码为 float64
func roleIDFromContext(c *gin.Context) (int, bool) {
roleIDVal, ok := c.Get("role_id")
if !ok {
return 0, false
}
switch v := roleIDVal.(type) {
case int:
return v, true
case float64:
return int(v), true
default:
return 0, false
}
}
// getPermissionsByRoleID 从 role_permissions 读取某角色的权限9527 默认拥有全部
func getPermissionsByRoleID(ctx context.Context, roleID int) []string {
if roleID == models.RoleIDSuperAdmin {
return allPermissionKeys()
}
coll := config.GetDB(config.DBName).Collection("role_permissions")
var doc models.RolePermissionsDoc
err := coll.FindOne(ctx, bson.M{"role_id": roleID}).Decode(&doc)
if err != nil {
if err == mongo.ErrNoDocuments {
return nil
}
return nil
}
return doc.Permissions
}
func allPermissionKeys() []string {
keys := make([]string, 0, len(models.AllPermissions))
for _, p := range models.AllPermissions {
keys = append(keys, p.Key)
}
return keys
}
// RequirePermission 要求当前用户拥有指定权限(在 AuthRequired 之后使用)
func RequirePermission(permission string) gin.HandlerFunc {
return func(c *gin.Context) {
roleID, ok := roleIDFromContext(c)
if !ok {
c.JSON(http.StatusForbidden, gin.H{"error": "无权限"})
c.Abort()
return
}
if roleID == models.RoleIDSuperAdmin {
c.Next()
return
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
perms := getPermissionsByRoleID(ctx, roleID)
for _, p := range perms {
if p == permission {
c.Next()
return
}
}
c.JSON(http.StatusForbidden, gin.H{"error": "无此操作权限"})
c.Abort()
}
}
// GetMyPermissions 返回当前用户权限列表(供前端菜单、按钮显隐)
func GetMyPermissions(c *gin.Context) {
roleID, ok := roleIDFromContext(c)
if !ok {
c.JSON(http.StatusOK, gin.H{"permissions": []string{}})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
perms := getPermissionsByRoleID(ctx, roleID)
c.JSON(http.StatusOK, gin.H{"permissions": perms})
}

294
server/handlers/register.go Normal file
View File

@@ -0,0 +1,294 @@
package handlers
import (
"context"
"errors"
"log"
"net/http"
"regexp"
"sync"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"yh_web/server/config"
"yh_web/server/models"
"yh_web/server/utils"
"github.com/gin-gonic/gin"
)
const (
testVerifyCode = "8888" // 测试验证码(未接入短信时使用)
codeExpire = 5 * time.Minute // 验证码有效期
)
var (
codeStore = make(map[string]codeEntry)
codeStoreMu sync.RWMutex
)
type codeEntry struct {
Code string
ExpireAt time.Time
}
// SendCodeInput 发送验证码请求
type SendCodeInput struct {
Mobile string `json:"mobile" binding:"required"`
}
// RegisterInput 手机注册请求
type RegisterInput struct {
Mobile string `json:"mobile" binding:"required"`
Code string `json:"code" binding:"required"`
Password string `json:"password" binding:"required"`
Username string `json:"username"` // 可选,默认用手机号
Email string `json:"email"` // 可选
}
// SendCode 发送验证码(测试阶段:任意手机号输入 8888 即可)
func SendCode(c *gin.Context) {
var input SendCodeInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请输入手机号"})
return
}
if !isValidMobile(input.Mobile) {
c.JSON(http.StatusBadRequest, gin.H{"error": "手机号格式不正确"})
return
}
// 未接入短信平台,使用测试验证码 8888
codeStoreMu.Lock()
codeStore[input.Mobile] = codeEntry{
Code: testVerifyCode,
ExpireAt: time.Now().Add(codeExpire),
}
codeStoreMu.Unlock()
c.JSON(http.StatusOK, gin.H{
"message": "验证码已发送(测试环境请输入 8888",
"expire": int(codeExpire.Seconds()),
})
}
// Register 手机号注册
func Register(c *gin.Context) {
var input RegisterInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写手机号、验证码和密码"})
return
}
if !isValidMobile(input.Mobile) {
c.JSON(http.StatusBadRequest, gin.H{"error": "手机号格式不正确"})
return
}
if len(input.Password) < 6 {
c.JSON(http.StatusBadRequest, gin.H{"error": "密码至少6位"})
return
}
if input.Email != "" && !isValidEmail(input.Email) {
c.JSON(http.StatusBadRequest, gin.H{"error": "邮箱格式不正确"})
return
}
// 校验验证码
codeStoreMu.RLock()
entry, ok := codeStore[input.Mobile]
codeStoreMu.RUnlock()
if !ok || entry.Code != input.Code {
c.JSON(http.StatusBadRequest, gin.H{"error": "验证码错误"})
return
}
if time.Now().After(entry.ExpireAt) {
codeStoreMu.Lock()
delete(codeStore, input.Mobile)
codeStoreMu.Unlock()
c.JSON(http.StatusBadRequest, gin.H{"error": "验证码已过期,请重新获取"})
return
}
// 删除已用验证码
codeStoreMu.Lock()
delete(codeStore, input.Mobile)
codeStoreMu.Unlock()
username := input.Username
if username == "" {
username = input.Mobile
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("users")
// 检查手机号是否已注册
var exist models.User
err := coll.FindOne(ctx, bson.M{"mobile": input.Mobile}).Decode(&exist)
if err == nil {
c.JSON(http.StatusConflict, gin.H{"error": "该手机号已注册"})
return
}
if !errors.Is(err, mongo.ErrNoDocuments) {
log.Printf("[Register] FindOne mobile error: %v", err)
resp := gin.H{"error": "注册失败,请稍后重试"}
if gin.Mode() == gin.DebugMode {
resp["debug"] = err.Error()
}
c.JSON(http.StatusInternalServerError, resp)
return
}
err = coll.FindOne(ctx, bson.M{"username": username}).Decode(&exist)
if err == nil {
c.JSON(http.StatusConflict, gin.H{"error": "用户名已存在"})
return
}
if !errors.Is(err, mongo.ErrNoDocuments) {
log.Printf("[Register] FindOne username error: %v", err)
resp := gin.H{"error": "注册失败,请稍后重试"}
if gin.Mode() == gin.DebugMode {
resp["debug"] = err.Error()
}
c.JSON(http.StatusInternalServerError, resp)
return
}
// 若提供了邮箱,检查是否已注册
if input.Email != "" {
err = coll.FindOne(ctx, bson.M{"email": input.Email}).Decode(&exist)
if err == nil {
c.JSON(http.StatusConflict, gin.H{"error": "该邮箱已注册"})
return
}
if !errors.Is(err, mongo.ErrNoDocuments) {
log.Printf("[Register] FindOne email error: %v", err)
resp := gin.H{"error": "注册失败,请稍后重试"}
if gin.Mode() == gin.DebugMode {
resp["debug"] = err.Error()
}
c.JSON(http.StatusInternalServerError, resp)
return
}
}
doc := bson.M{
"username": username,
"mobile": input.Mobile,
"password": utils.HashPassword(input.Password),
"role": "admin",
"role_id": models.RoleIDSuperAdmin,
}
if input.Email != "" {
doc["email"] = input.Email
}
_, err = coll.InsertOne(ctx, doc)
if err != nil {
log.Printf("[Register] InsertOne error: %v", err)
resp := gin.H{"error": "注册失败,请稍后重试"}
if gin.Mode() == gin.DebugMode {
resp["debug"] = err.Error()
}
c.JSON(http.StatusInternalServerError, resp)
return
}
c.JSON(http.StatusOK, gin.H{
"message": "注册成功",
})
}
// ResetPasswordInput 密码找回请求
type ResetPasswordInput struct {
Mobile string `json:"mobile" binding:"required"`
Code string `json:"code" binding:"required"`
NewPassword string `json:"new_password" binding:"required"`
}
// ResetPassword 密码找回(手机号+验证码)
func ResetPassword(c *gin.Context) {
var input ResetPasswordInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写手机号、验证码和新密码"})
return
}
if !isValidMobile(input.Mobile) {
c.JSON(http.StatusBadRequest, gin.H{"error": "手机号格式不正确"})
return
}
if len(input.NewPassword) < 6 {
c.JSON(http.StatusBadRequest, gin.H{"error": "新密码至少6位"})
return
}
// 校验验证码
codeStoreMu.RLock()
entry, ok := codeStore[input.Mobile]
codeStoreMu.RUnlock()
if !ok || entry.Code != input.Code {
c.JSON(http.StatusBadRequest, gin.H{"error": "验证码错误"})
return
}
if time.Now().After(entry.ExpireAt) {
codeStoreMu.Lock()
delete(codeStore, input.Mobile)
codeStoreMu.Unlock()
c.JSON(http.StatusBadRequest, gin.H{"error": "验证码已过期,请重新获取"})
return
}
codeStoreMu.Lock()
delete(codeStore, input.Mobile)
codeStoreMu.Unlock()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("users")
var user models.User
err := coll.FindOne(ctx, bson.M{"mobile": input.Mobile}).Decode(&user)
if err != nil {
if errors.Is(err, mongo.ErrNoDocuments) {
c.JSON(http.StatusNotFound, gin.H{"error": "该手机号未注册"})
return
}
log.Printf("[ResetPassword] FindOne error: %v", err)
resp := gin.H{"error": "操作失败"}
if gin.Mode() == gin.DebugMode {
resp["debug"] = err.Error()
}
c.JSON(http.StatusInternalServerError, resp)
return
}
_, err = coll.UpdateOne(ctx, bson.M{"_id": user.ID}, bson.M{"$set": bson.M{"password": utils.HashPassword(input.NewPassword)}})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "重置失败"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "密码已重置,请登录"})
}
func isValidMobile(mobile string) bool {
// 简单校验11位数字1开头
matched, _ := regexp.MatchString(`^1\d{10}$`, mobile)
return matched
}
func isValidEmail(email string) bool {
// 简单邮箱格式校验
matched, _ := regexp.MatchString(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`, email)
return matched
}

View File

@@ -0,0 +1,109 @@
package handlers
import (
"context"
"net/http"
"strconv"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo/options"
"yh_web/server/config"
"yh_web/server/models"
"github.com/gin-gonic/gin"
)
// 预定义角色(与 users.role_id 对应)
var roleMeta = []struct {
RoleID int `json:"role_id"`
RoleName string `json:"role_name"`
}{
{models.RoleIDSuperAdmin, "超级管理员"},
{models.RoleIDSuperUser, "超级用户"},
{models.RoleIDUser, "普通用户"},
}
// GetRolePermissionsList 返回所有角色及其权限(用于角色权限管理页)
func GetRolePermissionsList(c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("role_permissions")
cursor, err := coll.Find(ctx, bson.M{})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer cursor.Close(ctx)
var docs []models.RolePermissionsDoc
if err = cursor.All(ctx, &docs); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
permMap := make(map[int][]string)
for _, d := range docs {
permMap[d.RoleID] = d.Permissions
}
list := make([]gin.H, 0, len(roleMeta))
for _, r := range roleMeta {
perms := permMap[r.RoleID]
if perms == nil {
perms = []string{}
}
if r.RoleID == models.RoleIDSuperAdmin {
perms = allPermissionKeys()
}
list = append(list, gin.H{
"role_id": r.RoleID,
"role_name": r.RoleName,
"permissions": perms,
})
}
c.JSON(http.StatusOK, gin.H{
"list": list,
"all_permissions": models.AllPermissions,
})
}
// UpdateRolePermissionsInput 更新某角色权限
type UpdateRolePermissionsInput struct {
Permissions []string `json:"permissions"`
}
// UpdateRolePermissions 更新指定角色的权限
func UpdateRolePermissions(c *gin.Context) {
roleIDStr := c.Param("role_id")
roleID, err := strconv.Atoi(roleIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 role_id"})
return
}
if roleID == models.RoleIDSuperAdmin {
c.JSON(http.StatusBadRequest, gin.H{"error": "超级管理员权限不可修改"})
return
}
var input UpdateRolePermissionsInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("role_permissions")
filter := bson.M{"role_id": roleID}
update := bson.M{"$set": bson.M{"role_id": roleID, "permissions": input.Permissions}}
opts := options.UpdateOne().SetUpsert(true)
_, err = coll.UpdateOne(ctx, filter, update, opts)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "保存成功", "role_id": roleID, "permissions": input.Permissions})
}

165
server/handlers/site.go Normal file
View File

@@ -0,0 +1,165 @@
package handlers
import (
"context"
"net/http"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo/options"
"yh_web/server/config"
"yh_web/server/models"
"github.com/gin-gonic/gin"
)
// GetSites 站点列表
func GetSites(c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("sites")
opts := options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}})
cursor, err := coll.Find(ctx, bson.M{}, opts)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer cursor.Close(ctx)
var list []models.Site
if err = cursor.All(ctx, &list); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
total, _ := coll.CountDocuments(ctx, bson.M{})
c.JSON(http.StatusOK, gin.H{"list": list, "total": total})
}
// GetSiteByID 单个站点
func GetSiteByID(c *gin.Context) {
idStr := c.Param("site_id")
oid, err := bson.ObjectIDFromHex(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的站点ID"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var site models.Site
err = config.GetDB(config.DBName).Collection("sites").FindOne(ctx, bson.M{"_id": oid}).Decode(&site)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "站点不存在"})
return
}
c.JSON(http.StatusOK, site)
}
// CreateSiteInput 创建站点
type CreateSiteInput struct {
Name string `json:"name" binding:"required"`
Domain string `json:"domain"`
Description string `json:"description"`
}
// CreateSite 创建站点
func CreateSite(c *gin.Context) {
var input CreateSiteInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请填写站点名称"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("sites")
doc := bson.M{
"name": input.Name,
"domain": input.Domain,
"description": input.Description,
"created_at": time.Now().Format(time.RFC3339),
}
res, err := coll.InsertOne(ctx, doc)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"id": res.InsertedID, "message": "创建成功"})
}
// UpdateSiteInput 更新站点
type UpdateSiteInput struct {
Name *string `json:"name"`
Domain *string `json:"domain"`
Description *string `json:"description"`
}
// UpdateSite 更新站点
func UpdateSite(c *gin.Context) {
idStr := c.Param("site_id")
oid, err := bson.ObjectIDFromHex(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的站点ID"})
return
}
var input UpdateSiteInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
set := bson.M{}
if input.Name != nil {
set["name"] = *input.Name
}
if input.Domain != nil {
set["domain"] = *input.Domain
}
if input.Description != nil {
set["description"] = *input.Description
}
if len(set) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "无有效更新字段"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err = config.GetDB(config.DBName).Collection("sites").UpdateOne(ctx, bson.M{"_id": oid}, bson.M{"$set": set})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "更新成功"})
}
// DeleteSite 删除站点
func DeleteSite(c *gin.Context) {
idStr := c.Param("site_id")
oid, err := bson.ObjectIDFromHex(idStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的站点ID"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
db := config.GetDB(config.DBName)
_, err = db.Collection("sites").DeleteOne(ctx, bson.M{"_id": oid})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
db.Collection("pages").DeleteMany(ctx, bson.M{"site_id": idStr})
db.Collection("site_assets").DeleteMany(ctx, bson.M{"site_id": idStr})
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}

View File

@@ -0,0 +1,94 @@
package handlers
import (
"context"
"net/http"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
"yh_web/server/config"
"yh_web/server/models"
"github.com/gin-gonic/gin"
)
const smsConfigDocID = "sms_platform"
// GetSMSConfig 获取短信平台配置(仅超级用户 role_id=0, role=admin
func GetSMSConfig(c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("system_config")
var cfg models.SMSConfig
err := coll.FindOne(ctx, bson.M{"_id": smsConfigDocID}).Decode(&cfg)
if err != nil {
if err == mongo.ErrNoDocuments {
c.JSON(http.StatusOK, models.SMSConfig{})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, cfg)
}
// SMSConfigUpdateInput 短信配置更新
type SMSConfigUpdateInput struct {
Provider string `json:"provider"`
AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key"`
SignName string `json:"sign_name"`
TemplateID string `json:"template_id"`
Enabled *bool `json:"enabled"`
}
// UpdateSMSConfig 更新短信平台配置(仅超级用户)
func UpdateSMSConfig(c *gin.Context) {
var input SMSConfigUpdateInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("system_config")
update := bson.M{}
if input.Provider != "" {
update["provider"] = input.Provider
}
if input.AccessKey != "" {
update["access_key"] = input.AccessKey
}
if input.SecretKey != "" {
update["secret_key"] = input.SecretKey
}
if input.SignName != "" {
update["sign_name"] = input.SignName
}
if input.TemplateID != "" {
update["template_id"] = input.TemplateID
}
if input.Enabled != nil {
update["enabled"] = *input.Enabled
}
if len(update) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "无有效更新字段"})
return
}
opts := options.UpdateOne().SetUpsert(true)
_, err := coll.UpdateOne(ctx, bson.M{"_id": smsConfigDocID}, bson.M{"$set": update}, opts)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "配置已保存"})
}

34
server/handlers/stats.go Normal file
View File

@@ -0,0 +1,34 @@
package handlers
import (
"context"
"net/http"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"yh_web/server/config"
"github.com/gin-gonic/gin"
)
func GetStats(c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
db := config.GetDB(config.DBName)
users, _ := db.Collection("users").CountDocuments(ctx, bson.M{})
workspaces, _ := db.Collection("workspaces").CountDocuments(ctx, bson.M{})
conversations, _ := db.Collection("conversations").CountDocuments(ctx, bson.M{})
messages, _ := db.Collection("messages").CountDocuments(ctx, bson.M{})
files, _ := db.Collection("files").CountDocuments(ctx, bson.M{})
c.JSON(http.StatusOK, gin.H{
"users": users,
"workspaces": workspaces,
"conversations": conversations,
"messages": messages,
"files": files,
})
}

234
server/handlers/user.go Normal file
View File

@@ -0,0 +1,234 @@
package handlers
import (
"context"
"errors"
"net/http"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo"
"go.mongodb.org/mongo-driver/v2/mongo/options"
"yh_web/server/config"
"yh_web/server/models"
"yh_web/server/utils"
"github.com/gin-gonic/gin"
)
func GetUsers(c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("users")
page := 1
pageSize := 20
if p := c.Query("page"); p != "" {
if v, err := parseInt(p); err == nil && v > 0 {
page = v
}
}
if ps := c.Query("page_size"); ps != "" {
if v, err := parseInt(ps); err == nil && v > 0 && v <= 100 {
pageSize = v
}
}
skip := (page - 1) * pageSize
opts := options.Find().SetSkip(int64(skip)).SetLimit(int64(pageSize)).SetSort(bson.D{{Key: "_id", Value: -1}})
cursor, err := coll.Find(ctx, bson.M{}, opts)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer cursor.Close(ctx)
var users []models.User
if err = cursor.All(ctx, &users); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
total, _ := coll.CountDocuments(ctx, bson.M{})
c.JSON(http.StatusOK, gin.H{
"list": users,
"total": total,
"page": page,
"page_size": pageSize,
})
}
func GetUserByID(c *gin.Context) {
id := c.Param("id")
oid, err := bson.ObjectIDFromHex(id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var user models.User
err = config.GetDB(config.DBName).Collection("users").FindOne(ctx, bson.M{"_id": oid}).Decode(&user)
if err != nil {
if errors.Is(err, mongo.ErrNoDocuments) {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, user)
}
func CreateUser(c *gin.Context) {
var input models.UserCreateInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("users")
// 检查用户名是否已存在
var exist models.User
if err := coll.FindOne(ctx, bson.M{"username": input.Username}).Decode(&exist); err == nil {
c.JSON(http.StatusConflict, gin.H{"error": "用户名已存在"})
return
}
if err := coll.FindOne(ctx, bson.M{"email": input.Email}).Decode(&exist); err == nil {
c.JSON(http.StatusConflict, gin.H{"error": "邮箱已存在"})
return
}
if input.Role == "" {
input.Role = "user"
}
roleID := input.RoleID
if roleID == 0 {
roleID = models.RoleIDUser
}
doc := bson.M{
"username": input.Username,
"email": input.Email,
"password": utils.HashPassword(input.Password),
"role": input.Role,
"role_id": roleID,
"is_beta": input.IsBeta,
}
if input.TrialStartDate != "" {
doc["trial_start_date"] = input.TrialStartDate
}
if input.TrialEndDate != "" {
doc["trial_end_date"] = input.TrialEndDate
}
if input.LLM != "" {
doc["llm"] = input.LLM
}
res, err := coll.InsertOne(ctx, doc)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"id": res.InsertedID, "message": "创建成功"})
}
func UpdateUser(c *gin.Context) {
id := c.Param("id")
oid, err := bson.ObjectIDFromHex(id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
return
}
var input models.UserUpdateInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
update := bson.M{}
if input.Username != nil {
update["username"] = *input.Username
}
if input.Email != nil {
update["email"] = *input.Email
}
if input.Password != nil && *input.Password != "" {
update["password"] = utils.HashPassword(*input.Password)
}
if input.Role != nil {
update["role"] = *input.Role
}
if input.IsBeta != nil {
update["is_beta"] = *input.IsBeta
}
if input.TrialStartDate != nil {
update["trial_start_date"] = *input.TrialStartDate
}
if input.TrialEndDate != nil {
update["trial_end_date"] = *input.TrialEndDate
}
if input.LLM != nil {
update["llm"] = *input.LLM
}
if input.RoleID != nil {
update["role_id"] = *input.RoleID
}
if len(update) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "无有效更新字段"})
return
}
res, err := config.GetDB(config.DBName).Collection("users").UpdateOne(ctx, bson.M{"_id": oid}, bson.M{"$set": update})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if res.MatchedCount == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "更新成功"})
}
func DeleteUser(c *gin.Context) {
id := c.Param("id")
oid, err := bson.ObjectIDFromHex(id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的 ID"})
return
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
res, err := config.GetDB(config.DBName).Collection("users").DeleteOne(ctx, bson.M{"_id": oid})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if res.DeletedCount == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
}

View File

@@ -0,0 +1,64 @@
package handlers
import (
"context"
"net/http"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"go.mongodb.org/mongo-driver/v2/mongo/options"
"yh_web/server/config"
"github.com/gin-gonic/gin"
)
func GetWorkspaces(c *gin.Context) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("workspaces")
page := 1
pageSize := 20
if p := c.Query("page"); p != "" {
if v, err := parseInt(p); err == nil && v > 0 {
page = v
}
}
if ps := c.Query("page_size"); ps != "" {
if v, err := parseInt(ps); err == nil && v > 0 && v <= 100 {
pageSize = v
}
}
skip := (page - 1) * pageSize
opts := options.Find().SetSkip(int64(skip)).SetLimit(int64(pageSize)).SetSort(bson.D{{Key: "_id", Value: -1}})
filter := bson.M{}
if userID := c.Query("user_id"); userID != "" {
filter["user_id"] = userID
}
cursor, err := coll.Find(ctx, filter, opts)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
defer cursor.Close(ctx)
var list []map[string]interface{}
if err = cursor.All(ctx, &list); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
total, _ := coll.CountDocuments(ctx, filter)
c.JSON(http.StatusOK, gin.H{
"list": list,
"total": total,
"page": page,
"page_size": pageSize,
})
}