宇恒一号官网
This commit is contained in:
199
server/handlers/auth.go
Normal file
199
server/handlers/auth.go
Normal 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()
|
||||
}
|
||||
}
|
||||
67
server/handlers/conversation.go
Normal file
67
server/handlers/conversation.go
Normal 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,
|
||||
})
|
||||
}
|
||||
7
server/handlers/helpers.go
Normal file
7
server/handlers/helpers.go
Normal 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
345
server/handlers/homepage.go
Normal 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, "&", "&")
|
||||
s = strings.ReplaceAll(s, "<", "<")
|
||||
s = strings.ReplaceAll(s, ">", ">")
|
||||
s = strings.ReplaceAll(s, "\"", """)
|
||||
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>
|
||||
`
|
||||
145
server/handlers/module_upload.go
Normal file
145
server/handlers/module_upload.go
Normal 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": "删除成功"})
|
||||
}
|
||||
50
server/handlers/official_site.go
Normal file
50
server/handlers/official_site.go
Normal 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
175
server/handlers/page.go
Normal 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": "删除成功"})
|
||||
}
|
||||
128
server/handlers/payment_config.go
Normal file
128
server/handlers/payment_config.go
Normal 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": "配置已保存"})
|
||||
}
|
||||
96
server/handlers/permission.go
Normal file
96
server/handlers/permission.go
Normal 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_id(JWT 等可能解码为 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
294
server/handlers/register.go
Normal 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
|
||||
}
|
||||
109
server/handlers/role_permission.go
Normal file
109
server/handlers/role_permission.go
Normal 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
165
server/handlers/site.go
Normal 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": "删除成功"})
|
||||
}
|
||||
94
server/handlers/sms_config.go
Normal file
94
server/handlers/sms_config.go
Normal 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
34
server/handlers/stats.go
Normal 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
234
server/handlers/user.go
Normal 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": "删除成功"})
|
||||
}
|
||||
64
server/handlers/workspace.go
Normal file
64
server/handlers/workspace.go
Normal 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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user