mirror of
https://github.com/skyle1995/NetworkAuth.git
synced 2026-05-25 02:24:05 +08:00
New warehouse
This commit is contained in:
388
controllers/admin/app.go
Normal file
388
controllers/admin/app.go
Normal file
@@ -0,0 +1,388 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"networkDev/database"
|
||||
"networkDev/models"
|
||||
"networkDev/utils"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// AppsFragmentHandler 应用列表页面片段处理器
|
||||
func AppsFragmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
utils.RenderTemplate(w, "apps.html", map[string]interface{}{
|
||||
"Title": "应用管理",
|
||||
})
|
||||
}
|
||||
|
||||
// AppsListHandler 应用列表API处理器
|
||||
func AppsListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取分页参数
|
||||
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
// 获取搜索参数
|
||||
search := strings.TrimSpace(r.URL.Query().Get("search"))
|
||||
|
||||
// 构建查询
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get database connection")
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var apps []models.App
|
||||
var total int64
|
||||
|
||||
query := db.Model(&models.App{})
|
||||
|
||||
// 如果有搜索条件
|
||||
if search != "" {
|
||||
query = query.Where("name LIKE ? OR uuid LIKE ?", "%"+search+"%", "%"+search+"%")
|
||||
}
|
||||
|
||||
// 获取总数
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to count apps")
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (page - 1) * limit
|
||||
if err := query.Order("created_at DESC").Offset(offset).Limit(limit).Find(&apps).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to query apps")
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 返回结果
|
||||
response := map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"count": total,
|
||||
"data": apps,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// AppCreateHandler 创建应用API处理器
|
||||
func AppCreateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Status int `json:"status"`
|
||||
DownloadType int `json:"download_type"`
|
||||
ForceUpdate int `json:"force_update"`
|
||||
DownloadURL string `json:"download_url"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
logrus.WithError(err).Error("Failed to decode JSON request")
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if strings.TrimSpace(req.Name) == "" {
|
||||
logrus.Error("App name is empty")
|
||||
http.Error(w, "应用名称不能为空", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if req.Version == "" {
|
||||
req.Version = "1.0.0"
|
||||
}
|
||||
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"name": req.Name,
|
||||
"version": req.Version,
|
||||
"status": req.Status,
|
||||
"download_type": req.DownloadType,
|
||||
"download_url": req.DownloadURL,
|
||||
"force_update": req.ForceUpdate,
|
||||
}).Info("Received app create request")
|
||||
|
||||
// 创建应用
|
||||
app := models.App{
|
||||
Name: strings.TrimSpace(req.Name),
|
||||
Version: req.Version,
|
||||
Status: req.Status,
|
||||
DownloadType: req.DownloadType,
|
||||
DownloadURL: strings.TrimSpace(req.DownloadURL),
|
||||
ForceUpdate: req.ForceUpdate,
|
||||
}
|
||||
|
||||
// 确保UUID和Secret被设置(虽然BeforeCreate钩子应该处理这些,但为了保险起见)
|
||||
if app.UUID == "" {
|
||||
app.UUID = uuid.New().String()
|
||||
}
|
||||
if app.Secret == "" {
|
||||
// 生成32位大写16进制随机字符
|
||||
bytes := make([]byte, 16) // 16字节 = 32位16进制字符
|
||||
rand.Read(bytes)
|
||||
app.Secret = strings.ToUpper(hex.EncodeToString(bytes))
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get database connection")
|
||||
http.Error(w, "数据库连接失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Create(&app).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to create app")
|
||||
http.Error(w, "创建应用失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "创建成功",
|
||||
"data": app,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// AppUpdateHandler 更新应用API处理器
|
||||
func AppUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Status int `json:"status"`
|
||||
DownloadType int `json:"download_type"`
|
||||
DownloadURL string `json:"download_url"`
|
||||
ForceUpdate int `json:"force_update"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if req.ID == 0 {
|
||||
http.Error(w, "应用ID不能为空", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Name) == "" {
|
||||
http.Error(w, "应用名称不能为空", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get database connection")
|
||||
http.Error(w, "数据库连接失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 查找应用
|
||||
var app models.App
|
||||
if err := db.First(&app, req.ID).Error; err != nil {
|
||||
http.Error(w, "应用不存在", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新字段
|
||||
app.Name = strings.TrimSpace(req.Name)
|
||||
app.Version = req.Version
|
||||
app.Status = req.Status
|
||||
app.DownloadType = req.DownloadType
|
||||
app.DownloadURL = strings.TrimSpace(req.DownloadURL)
|
||||
app.ForceUpdate = req.ForceUpdate
|
||||
|
||||
if err := db.Save(&app).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to update app")
|
||||
http.Error(w, "更新应用失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "更新成功",
|
||||
"data": app,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// AppDeleteHandler 删除应用API处理器
|
||||
func AppDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
ID uint `json:"id"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.ID == 0 {
|
||||
http.Error(w, "应用ID不能为空", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get database connection")
|
||||
http.Error(w, "数据库连接失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 删除应用
|
||||
if err := db.Delete(&models.App{}, req.ID).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to delete app")
|
||||
http.Error(w, "删除应用失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "删除成功",
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// AppsBatchDeleteHandler 批量删除应用API处理器
|
||||
func AppsBatchDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.IDs) == 0 {
|
||||
http.Error(w, "请选择要删除的应用", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get database connection")
|
||||
http.Error(w, "数据库连接失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
if err := db.Delete(&models.App{}, req.IDs).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to batch delete apps")
|
||||
http.Error(w, "批量删除失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "批量删除成功",
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// AppsBatchUpdateStatusHandler 批量更新应用状态API处理器
|
||||
func AppsBatchUpdateStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
IDs []uint `json:"ids"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.IDs) == 0 {
|
||||
http.Error(w, "请选择要更新的应用", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Status != 0 && req.Status != 1 {
|
||||
http.Error(w, "状态值无效", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("Failed to get database connection")
|
||||
http.Error(w, "数据库连接失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 批量更新状态
|
||||
if err := db.Model(&models.App{}).Where("id IN ?", req.IDs).Update("status", req.Status).Error; err != nil {
|
||||
logrus.WithError(err).Error("Failed to batch update app status")
|
||||
http.Error(w, "批量更新状态失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
statusText := "禁用"
|
||||
if req.Status == 1 {
|
||||
statusText = "启用"
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"code": 0,
|
||||
"msg": "批量" + statusText + "成功",
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
314
controllers/admin/auth.go
Normal file
314
controllers/admin/auth.go
Normal file
@@ -0,0 +1,314 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"networkDev/database"
|
||||
"networkDev/models"
|
||||
"networkDev/utils"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// LoginPageHandler 管理员登录页渲染处理器
|
||||
// - 如果已登录则重定向到 /admin
|
||||
// - 否则渲染 web/template/admin/login.html 模板
|
||||
func LoginPageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// 已登录直接跳转到后台布局
|
||||
if IsAdminAuthenticated(r) {
|
||||
http.Redirect(w, r, "/admin", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
data := utils.GetDefaultTemplateData()
|
||||
data["Title"] = "管理员登录"
|
||||
|
||||
utils.RenderTemplate(w, "login.html", data)
|
||||
}
|
||||
|
||||
// LoginHandler 管理员登录接口
|
||||
// - 接收JSON: {username, password}
|
||||
// - 验证用户存在与密码正确性
|
||||
// - 仅允许 Role=0 的管理员登录
|
||||
// - 成功后设置简单的会话Cookie(后续可切换为JWT或更完善的Session)
|
||||
func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "请求参数错误", nil)
|
||||
return
|
||||
}
|
||||
if body.Username == "" || body.Password == "" {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "用户名和密码不能为空", nil)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
dbErr := db.Where("username = ?", body.Username).First(&user).Error
|
||||
if dbErr != nil {
|
||||
utils.JsonResponse(w, http.StatusUnauthorized, false, "用户不存在或密码错误", nil)
|
||||
return
|
||||
}
|
||||
if user.Role != 0 {
|
||||
utils.JsonResponse(w, http.StatusForbidden, false, "非管理员账号不可登录后台", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 使用盐值验证密码
|
||||
if !utils.VerifyPasswordWithSalt(body.Password, user.PasswordSalt, user.Password) {
|
||||
utils.JsonResponse(w, http.StatusUnauthorized, false, "用户不存在或密码错误", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 生成JWT令牌
|
||||
token, err := generateJWTToken(user)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成令牌失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置JWT Cookie(HttpOnly,安全)
|
||||
cookie := &http.Cookie{
|
||||
Name: "admin_session",
|
||||
Value: token,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: false, // 生产环境应设置为true(HTTPS)
|
||||
MaxAge: 24 * 60 * 60, // 24小时
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
|
||||
utils.JsonResponse(w, http.StatusOK, true, "登录成功", map[string]interface{}{
|
||||
"redirect": "/admin",
|
||||
})
|
||||
}
|
||||
|
||||
// LogoutHandler 管理员登出
|
||||
// - 清理JWT Cookie会话
|
||||
// - 确保令牌完全失效
|
||||
func LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// 清理JWT Cookie
|
||||
cookie := &http.Cookie{
|
||||
Name: "admin_session",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: false, // 生产环境应设置为true
|
||||
MaxAge: -1, // 立即失效
|
||||
Expires: time.Unix(0, 0), // 确保过期
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
|
||||
// 可选:将JWT令牌加入黑名单(需要Redis或数据库支持)
|
||||
// 这里可以实现JWT黑名单机制
|
||||
|
||||
utils.JsonResponse(w, http.StatusOK, true, "已退出登录", map[string]interface{}{
|
||||
"redirect": "/admin/login",
|
||||
})
|
||||
}
|
||||
|
||||
// JWT密钥(生产环境应从配置文件或环境变量读取)
|
||||
var jwtSecret = []byte(viper.GetString("security.jwt_secret"))
|
||||
|
||||
// JWTClaims JWT载荷结构
|
||||
type JWTClaims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Role int `json:"role"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// generateJWTToken 生成JWT令牌
|
||||
// - 包含用户ID、用户名、角色信息
|
||||
// - 设置24小时过期时间
|
||||
// - 使用HMAC-SHA256签名
|
||||
func generateJWTToken(user models.User) (string, error) {
|
||||
claims := JWTClaims{
|
||||
UserID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: "凌动技术",
|
||||
Subject: strconv.Itoa(int(user.ID)),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(jwtSecret)
|
||||
}
|
||||
|
||||
// parseJWTToken 解析并验证JWT令牌
|
||||
// - 验证签名有效性
|
||||
// - 检查过期时间
|
||||
// - 返回用户信息
|
||||
func parseJWTToken(tokenString string) (*JWTClaims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return jwtSecret, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid token")
|
||||
}
|
||||
|
||||
// IsAdminAuthenticated 判断管理员是否已认证(导出)
|
||||
// - 检查admin_session Cookie中的JWT令牌
|
||||
// - 验证令牌签名、过期时间和用户角色
|
||||
func IsAdminAuthenticated(r *http.Request) bool {
|
||||
cookie, err := r.Cookie("admin_session")
|
||||
if err != nil || cookie.Value == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 解析并验证JWT令牌
|
||||
claims, err := parseJWTToken(cookie.Value)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证用户角色(只允许管理员角色=0)
|
||||
if claims.Role != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 可选:进一步验证用户是否仍然存在且有效
|
||||
// 这里可以添加数据库查询来验证用户状态
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// GetCurrentAdminUser 获取当前登录的管理员用户信息
|
||||
// - 从JWT令牌中提取用户信息
|
||||
// - 自动刷新接近过期的令牌(剩余时间少于6小时时刷新)
|
||||
// - 返回用户ID、用户名和角色
|
||||
func GetCurrentAdminUser(r *http.Request) (*JWTClaims, error) {
|
||||
cookie, err := r.Cookie("admin_session")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("未找到会话信息")
|
||||
}
|
||||
|
||||
claims, err := parseJWTToken(cookie.Value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无效的会话信息")
|
||||
}
|
||||
|
||||
if claims.Role != 0 {
|
||||
return nil, fmt.Errorf("权限不足")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
// GetCurrentAdminUserWithRefresh 获取当前登录的管理员用户信息并自动刷新令牌
|
||||
// - 从JWT令牌中提取用户信息
|
||||
// - 自动刷新接近过期的令牌(剩余时间少于6小时时刷新)
|
||||
// - 返回用户ID、用户名、角色和是否刷新了令牌
|
||||
func GetCurrentAdminUserWithRefresh(w http.ResponseWriter, r *http.Request) (*JWTClaims, bool, error) {
|
||||
cookie, err := r.Cookie("admin_session")
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("未找到会话信息")
|
||||
}
|
||||
|
||||
claims, err := parseJWTToken(cookie.Value)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("无效的会话信息")
|
||||
}
|
||||
|
||||
if claims.Role != 0 {
|
||||
return nil, false, fmt.Errorf("权限不足")
|
||||
}
|
||||
|
||||
// 检查是否需要刷新令牌(根据配置的阈值)
|
||||
refreshed := false
|
||||
refreshThreshold := time.Duration(viper.GetInt("security.jwt_refresh_threshold_hours")) * time.Hour
|
||||
if time.Until(claims.ExpiresAt.Time) < refreshThreshold {
|
||||
// 生成新的JWT令牌
|
||||
user := models.User{
|
||||
ID: claims.UserID,
|
||||
Username: claims.Username,
|
||||
Role: claims.Role,
|
||||
}
|
||||
newToken, err := generateJWTToken(user)
|
||||
if err == nil {
|
||||
// 更新Cookie
|
||||
newCookie := &http.Cookie{
|
||||
Name: "admin_session",
|
||||
Value: newToken,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: false, // 生产环境应设置为true(HTTPS)
|
||||
MaxAge: 24 * 60 * 60, // 24小时
|
||||
}
|
||||
http.SetCookie(w, newCookie)
|
||||
refreshed = true
|
||||
|
||||
// 更新claims的过期时间
|
||||
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(24 * time.Hour))
|
||||
claims.IssuedAt = jwt.NewNumericDate(time.Now())
|
||||
}
|
||||
}
|
||||
|
||||
return claims, refreshed, nil
|
||||
}
|
||||
|
||||
// AdminAuthRequired 管理员认证拦截中间件
|
||||
// - 未登录:重定向到 /admin/login
|
||||
// - 已登录:自动刷新接近过期的令牌,然后放行到后续处理器
|
||||
func AdminAuthRequired(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// 尝试获取用户信息并自动刷新令牌
|
||||
claims, refreshed, err := GetCurrentAdminUserWithRefresh(w, r)
|
||||
if err != nil {
|
||||
// 中文注释:区分普通页面请求与AJAX/JSON请求
|
||||
// - 对 AJAX/JSON:直接返回 401 JSON,便于前端处理(如提示重新登录)
|
||||
// - 对普通页面:保持原有重定向到登录页
|
||||
accept := r.Header.Get("Accept")
|
||||
xrw := strings.ToLower(strings.TrimSpace(r.Header.Get("X-Requested-With")))
|
||||
if strings.Contains(accept, "application/json") || xrw == "xmlhttprequest" {
|
||||
utils.JsonResponse(w, http.StatusUnauthorized, false, "未登录或会话已过期", nil)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin/login", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果令牌被刷新,可以在这里记录日志(可选)
|
||||
if refreshed {
|
||||
// 可以添加日志记录令牌刷新事件
|
||||
_ = claims // 避免未使用变量警告
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
650
controllers/admin/card.go
Normal file
650
controllers/admin/card.go
Normal file
@@ -0,0 +1,650 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
// 移除 CSV 导出,改为自定义分隔符文本导出
|
||||
// "encoding/csv"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"networkDev/database"
|
||||
"networkDev/models"
|
||||
"networkDev/utils"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// 生成指定长度的十六进制随机字符串
|
||||
// 入参 n 表示需要的随机字符数(非字节数);返回小写十六进制字符串
|
||||
func genRandomHex(n int) string {
|
||||
if n <= 0 {
|
||||
return ""
|
||||
}
|
||||
// 由于 hex 每个字节会转成 2 个字符,因此需要 (n+1)/2 个字节
|
||||
byteLen := (n + 1) / 2
|
||||
b := make([]byte, byteLen)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return ""
|
||||
}
|
||||
s := hex.EncodeToString(b)
|
||||
if len(s) > n {
|
||||
s = s[:n]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// 根据前缀和总长度构建卡号
|
||||
// - totalLen <= 0 时按 18 处理
|
||||
// - 若前缀长度 >= totalLen,则自动扩展为 前缀长度+18
|
||||
// - uppercase=true 表示最终结果转为大写;false 表示小写
|
||||
func buildCardNumber(prefix string, totalLen int, uppercase bool) string {
|
||||
if totalLen <= 0 {
|
||||
totalLen = 18
|
||||
}
|
||||
if len(prefix) >= totalLen {
|
||||
totalLen = len(prefix) + 18
|
||||
}
|
||||
rest := totalLen - len(prefix)
|
||||
s := prefix + genRandomHex(rest)
|
||||
if uppercase {
|
||||
return strings.ToUpper(s)
|
||||
}
|
||||
return strings.ToLower(s)
|
||||
}
|
||||
|
||||
// CardsFragmentHandler 卡密管理片段渲染
|
||||
// - 渲染 cards.html 列表与表单界面
|
||||
func CardsFragmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
utils.RenderTemplate(w, "cards.html", map[string]interface{}{})
|
||||
}
|
||||
|
||||
// CardsListHandler 获取卡密列表
|
||||
// - 支持GET
|
||||
// - 支持分页查询参数:page、page_size
|
||||
// - 支持筛选参数:card_type_id、status、batch、keyword(卡号/备注/批次模糊匹配)
|
||||
// - 返回卡密列表和分页信息
|
||||
func CardsListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取查询参数
|
||||
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||
pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size"))
|
||||
cardTypeIDStr := r.URL.Query().Get("card_type_id")
|
||||
statusStr := r.URL.Query().Get("status")
|
||||
batch := r.URL.Query().Get("batch")
|
||||
// 中文注释:keyword 支持在 card_number、remark、batch 三个字段上进行模糊匹配
|
||||
keyword := strings.TrimSpace(r.URL.Query().Get("keyword"))
|
||||
|
||||
// 设置默认分页参数
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 构建查询条件(去除无效的 Preload,前端已通过 card_type_id 自行映射类型名称)
|
||||
query := db.Model(&models.Card{})
|
||||
|
||||
// 筛选条件
|
||||
if cardTypeIDStr != "" {
|
||||
if cardTypeID, err := strconv.Atoi(cardTypeIDStr); err == nil && cardTypeID > 0 {
|
||||
query = query.Where("card_type_id = ?", cardTypeID)
|
||||
}
|
||||
}
|
||||
if statusStr != "" {
|
||||
if status, err := strconv.Atoi(statusStr); err == nil {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
}
|
||||
if batch != "" {
|
||||
query = query.Where("batch LIKE ?", "%"+batch+"%")
|
||||
}
|
||||
// 中文注释:当提供 keyword 时,在卡号、备注、批次三个字段上进行 OR 模糊匹配
|
||||
if keyword != "" {
|
||||
kw := "%" + keyword + "%"
|
||||
query = query.Where("(card_number LIKE ? OR remark LIKE ? OR batch LIKE ?)", kw, kw, kw)
|
||||
}
|
||||
|
||||
// 计算总数
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "统计总数失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
var cards []models.Card
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("id desc").Offset(offset).Limit(pageSize).Find(&cards).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "查询失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 中文注释:为每条卡记录补充类型名称,避免前端依赖异步类型映射导致显示“未知类型”
|
||||
// 1) 先查询类型列表并构建 id->name 的映射表
|
||||
var cardTypeList []models.CardType
|
||||
_ = db.Model(&models.CardType{}).Find(&cardTypeList).Error
|
||||
typeNameMap := make(map[uint]string, len(cardTypeList))
|
||||
for _, t := range cardTypeList {
|
||||
typeNameMap[t.ID] = t.Name
|
||||
}
|
||||
// 2) 将卡列表转换为通用 map 列表,并附加 card_type_name 字段
|
||||
items := make([]map[string]interface{}, 0, len(cards))
|
||||
for _, c := range cards {
|
||||
items = append(items, map[string]interface{}{
|
||||
"id": c.ID,
|
||||
"card_number": c.CardNumber,
|
||||
"card_type_id": c.CardTypeID,
|
||||
"card_type_name": typeNameMap[c.CardTypeID],
|
||||
"status": c.Status,
|
||||
"batch": c.Batch,
|
||||
"remark": c.Remark,
|
||||
"used_at": c.UsedAt,
|
||||
"created_at": c.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
// 返回分页数据
|
||||
result := map[string]interface{}{
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"pages": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "ok", result)
|
||||
}
|
||||
|
||||
// CardCreateHandler 新增卡密
|
||||
// - 接收JSON: {card_type_id, status, remark, prefix, length, uppercase, count}
|
||||
// - card_number 与 batch 不再由前端传入,后端将自动生成:
|
||||
// 1. 卡号:按 prefix 与 length 生成随机十六进制字符串,支持大小写控制(uppercase,默认小写)
|
||||
// 2. 批次:基于设置表 card_batch_counter 自增,格式为 YYYYMMDD-000001
|
||||
// 3. 生成数量:通过 count 控制一次生成的数量,默认1,最大500
|
||||
func CardCreateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
type reqBody struct {
|
||||
CardTypeID uint `json:"card_type_id"`
|
||||
Status int `json:"status"`
|
||||
Remark string `json:"remark"`
|
||||
Prefix string `json:"prefix"`
|
||||
Length int `json:"length"`
|
||||
Uppercase bool `json:"uppercase"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
var body reqBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "请求体错误", nil)
|
||||
return
|
||||
}
|
||||
if body.CardTypeID == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "卡密类型ID不能为空", nil)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查卡密类型是否存在且启用
|
||||
var cardType models.CardType
|
||||
if err := db.First(&cardType, body.CardTypeID).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "卡密类型不存在", nil)
|
||||
return
|
||||
}
|
||||
// 检查卡密类型是否被禁用
|
||||
if cardType.Status != 1 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "卡密类型已被禁用,无法创建卡密", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 规范化长度与大小写、生成数量参数
|
||||
if body.Length <= 0 {
|
||||
body.Length = 18
|
||||
}
|
||||
if body.Count <= 0 {
|
||||
body.Count = 1
|
||||
}
|
||||
if body.Count > 500 {
|
||||
body.Count = 500
|
||||
}
|
||||
|
||||
// 生成批次(基于设置表 card_batch_counter 自增)
|
||||
// 格式:YYYYMMDD-000001(每天不重置,仅简单自增计数)
|
||||
var batch string
|
||||
var setting models.Settings
|
||||
if err := db.Where("name = ?", "card_batch_counter").First(&setting).Error; err != nil {
|
||||
// 若不存在该设置项,则创建并从 1 开始
|
||||
setting = models.Settings{Name: "card_batch_counter", Value: "1", Description: "卡密批次号计数器(用于记录上次生成批次号的序号,自增使用)"}
|
||||
if e := db.Create(&setting).Error; e != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "初始化批次计数器失败", nil)
|
||||
return
|
||||
}
|
||||
batch = time.Now().Format("20060102") + "-" + fmt.Sprintf("%06d", 1)
|
||||
} else {
|
||||
cnt, _ := strconv.Atoi(setting.Value)
|
||||
cnt++
|
||||
newVal := strconv.Itoa(cnt)
|
||||
if e := db.Model(&models.Settings{}).Where("id = ?", setting.ID).Update("value", newVal).Error; e != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "更新批次计数器失败", nil)
|
||||
return
|
||||
}
|
||||
batch = time.Now().Format("20060102") + "-" + fmt.Sprintf("%06d", cnt)
|
||||
}
|
||||
|
||||
// 中文注释:计算合法状态值(1=已使用,2=禁用,其它按未使用0处理)
|
||||
safeStatus := body.Status
|
||||
if safeStatus != 1 && safeStatus != 2 {
|
||||
safeStatus = 0
|
||||
}
|
||||
|
||||
// 中文注释:循环生成 count 条卡密,若单条创建失败则重试最多5次
|
||||
success := 0
|
||||
for i := 0; i < body.Count; i++ {
|
||||
card := models.Card{
|
||||
CardNumber: buildCardNumber(body.Prefix, body.Length, body.Uppercase),
|
||||
CardTypeID: body.CardTypeID,
|
||||
Status: safeStatus,
|
||||
Batch: batch,
|
||||
Remark: body.Remark,
|
||||
}
|
||||
var createErr error
|
||||
for j := 0; j < 5; j++ {
|
||||
createErr = db.Create(&card).Error
|
||||
if createErr == nil {
|
||||
success++
|
||||
break
|
||||
}
|
||||
// 失败则重新生成一次卡号后重试
|
||||
card.CardNumber = buildCardNumber(body.Prefix, body.Length, body.Uppercase)
|
||||
}
|
||||
}
|
||||
|
||||
if success == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "创建失败,可能是卡密号码重复", nil)
|
||||
return
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"created": success,
|
||||
"batch": batch,
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, fmt.Sprintf("创建成功:%d条", success), result)
|
||||
}
|
||||
|
||||
// CardUpdateHandler 更新卡密
|
||||
// - 接收JSON: {id, card_number(可选), card_type_id(可选), status, batch(可选), remark}
|
||||
// - 说明:card_number 与 batch 若未提供或为空,则不会更新对应字段
|
||||
func CardUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
type reqBody struct {
|
||||
ID uint `json:"id"`
|
||||
CardNumber string `json:"card_number"`
|
||||
CardTypeID uint `json:"card_type_id"`
|
||||
Status int `json:"status"`
|
||||
Batch string `json:"batch"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
var body reqBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "请求体错误", nil)
|
||||
return
|
||||
}
|
||||
if body.ID == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "缺少ID", nil)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查卡密类型是否存在且启用(如果提供了新的卡密类型ID)
|
||||
if body.CardTypeID > 0 {
|
||||
var cardType models.CardType
|
||||
if err := db.First(&cardType, body.CardTypeID).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "卡密类型不存在", nil)
|
||||
return
|
||||
}
|
||||
// 检查卡密类型是否被禁用
|
||||
if cardType.Status != 1 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "卡密类型已被禁用,无法更新为此类型", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 中文注释:若尝试将状态置为未使用(0),则直接允许
|
||||
if body.Status == 0 {
|
||||
var existing models.Card
|
||||
if err := db.First(&existing, body.ID).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "卡密不存在", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 构建更新字段
|
||||
updates := map[string]interface{}{}
|
||||
if body.CardNumber != "" {
|
||||
updates["card_number"] = body.CardNumber
|
||||
}
|
||||
if body.CardTypeID > 0 {
|
||||
updates["card_type_id"] = body.CardTypeID
|
||||
}
|
||||
updates["status"] = body.Status
|
||||
// 仅当提供非空 batch 时才更新,防止被清空
|
||||
if strings.TrimSpace(body.Batch) != "" {
|
||||
updates["batch"] = body.Batch
|
||||
}
|
||||
updates["remark"] = body.Remark
|
||||
|
||||
if err := db.Model(&models.Card{}).Where("id = ?", body.ID).Updates(updates).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "更新失败,可能是卡密号码重复", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "更新成功", nil)
|
||||
}
|
||||
|
||||
// CardDeleteHandler 删除单个卡密
|
||||
// - 接收JSON: {id}
|
||||
func CardDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
ID uint `json:"id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.ID == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "参数错误", nil)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
if err := db.Delete(&models.Card{}, body.ID).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "删除失败", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "删除成功", nil)
|
||||
}
|
||||
|
||||
// CardsBatchDeleteHandler 批量删除卡密
|
||||
// - 接收JSON: {ids: []}
|
||||
func CardsBatchDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || len(body.IDs) == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "参数错误", nil)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
if err := db.Delete(&models.Card{}, body.IDs).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "批量删除失败", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "批量删除成功", nil)
|
||||
}
|
||||
|
||||
// CardsBatchUpdateStatusHandler 批量更新卡密状态
|
||||
// - 接收JSON: {ids: [], status: int}
|
||||
// - status: 0=未使用,1=已使用,2=禁用
|
||||
func CardsBatchUpdateStatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
IDs []uint `json:"ids"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || len(body.IDs) == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "参数错误", nil)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
// 中文注释:允许批量重置为未使用(0)
|
||||
if err := db.Model(&models.Card{}).Where("id IN ?", body.IDs).Update("status", body.Status).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "批量更新失败", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "操作成功", nil)
|
||||
}
|
||||
|
||||
// GetCardTypesHandler 获取卡密类型列表(供前端下拉选择)
|
||||
// - 仅支持GET请求
|
||||
// - 只返回启用状态的卡密类型,用于前端下拉选择
|
||||
func GetCardTypesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
var cardTypes []models.CardType
|
||||
// 中文注释:根据可选参数 all 决定是否仅返回启用类型
|
||||
// - 未提供或为其它值:仅返回启用(status=1)
|
||||
// - all=1/true/yes:返回所有状态的类型(用于筛选下拉场景)
|
||||
q := db.Model(&models.CardType{})
|
||||
all := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("all")))
|
||||
if !(all == "1" || all == "true" || all == "yes") {
|
||||
q = q.Where("status = ?", 1)
|
||||
}
|
||||
if err := q.Order("id asc").Find(&cardTypes).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "查询失败", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "ok", cardTypes)
|
||||
}
|
||||
|
||||
// CardsExportHandler 导出卡密为文本文件
|
||||
// - 支持GET
|
||||
// - 筛选参数:card_type_id、status、batch、remark
|
||||
// - 导出字段(按顺序):卡号、状态、创建时间;使用“----”分隔
|
||||
func CardsExportHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析筛选参数
|
||||
cardTypeIDStr := strings.TrimSpace(r.URL.Query().Get("card_type_id"))
|
||||
statusStr := strings.TrimSpace(r.URL.Query().Get("status"))
|
||||
batch := strings.TrimSpace(r.URL.Query().Get("batch"))
|
||||
remark := strings.TrimSpace(r.URL.Query().Get("remark"))
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
http.Error(w, "数据库连接失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 构建查询
|
||||
query := db.Model(&models.Card{})
|
||||
if cardTypeIDStr != "" {
|
||||
if id, err := strconv.Atoi(cardTypeIDStr); err == nil && id > 0 {
|
||||
query = query.Where("card_type_id = ?", id)
|
||||
}
|
||||
}
|
||||
if statusStr != "" {
|
||||
if s, err := strconv.Atoi(statusStr); err == nil {
|
||||
query = query.Where("status = ?", s)
|
||||
}
|
||||
}
|
||||
if batch != "" {
|
||||
query = query.Where("batch LIKE ?", "%"+batch+"%")
|
||||
}
|
||||
if remark != "" {
|
||||
query = query.Where("remark LIKE ?", "%"+remark+"%")
|
||||
}
|
||||
|
||||
// 查询数据(按ID倒序)
|
||||
var cards []models.Card
|
||||
if err := query.Order("id desc").Find(&cards).Error; err != nil {
|
||||
http.Error(w, "查询失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应头(文本下载)
|
||||
now := time.Now().Format("20060102150405")
|
||||
filename := fmt.Sprintf("cards_%s.txt", now)
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
|
||||
|
||||
// 写入UTF-8 BOM,避免Excel/记事本中文乱码
|
||||
_, _ = w.Write([]byte{0xEF, 0xBB, 0xBF})
|
||||
|
||||
// 写入表头
|
||||
_, _ = w.Write([]byte("卡号----状态----创建时间\n"))
|
||||
|
||||
// 时间格式
|
||||
const tf = "2006-01-02 15:04:05"
|
||||
|
||||
// 状态转文字
|
||||
statusText := func(s int) string {
|
||||
switch s {
|
||||
case 0:
|
||||
return "未使用"
|
||||
case 1:
|
||||
return "已使用"
|
||||
default:
|
||||
return "禁用"
|
||||
}
|
||||
}
|
||||
|
||||
// 写入数据行(以“----”分隔)
|
||||
for _, c := range cards {
|
||||
record := []string{
|
||||
c.CardNumber,
|
||||
statusText(c.Status),
|
||||
c.CreatedAt.Format(tf),
|
||||
}
|
||||
line := strings.Join(record, "----") + "\n"
|
||||
if _, err := w.Write([]byte(line)); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CardsExportSelectedHandler 导出选中的卡密为文本文件
|
||||
// - 支持GET
|
||||
// - 参数:ids(逗号分隔的卡密ID列表)
|
||||
// - 导出字段(按顺序):卡号、状态、创建时间;使用"----"分隔
|
||||
func CardsExportSelectedHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析选中的卡密ID列表
|
||||
idsStr := strings.TrimSpace(r.URL.Query().Get("ids"))
|
||||
if idsStr == "" {
|
||||
http.Error(w, "请提供要导出的卡密ID列表", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析ID列表
|
||||
idStrings := strings.Split(idsStr, ",")
|
||||
var ids []uint
|
||||
for _, idStr := range idStrings {
|
||||
if id, err := strconv.Atoi(strings.TrimSpace(idStr)); err == nil && id > 0 {
|
||||
ids = append(ids, uint(id))
|
||||
}
|
||||
}
|
||||
|
||||
if len(ids) == 0 {
|
||||
http.Error(w, "无效的卡密ID列表", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
http.Error(w, "数据库连接失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 查询选中的卡密数据(按ID倒序)
|
||||
var cards []models.Card
|
||||
if err := db.Where("id IN ?", ids).Order("id desc").Find(&cards).Error; err != nil {
|
||||
logrus.WithError(err).Error("查询选中卡密失败")
|
||||
http.Error(w, "查询卡密数据失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if len(cards) == 0 {
|
||||
http.Error(w, "未找到指定的卡密数据", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置响应头,触发下载
|
||||
filename := fmt.Sprintf("selected_cards_%s.txt", time.Now().Format("20060102_150405"))
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
|
||||
|
||||
// 写入数据
|
||||
tf := "2006-01-02 15:04:05"
|
||||
for _, c := range cards {
|
||||
// 状态转换
|
||||
var statusText string
|
||||
switch c.Status {
|
||||
case 0:
|
||||
statusText = "未使用"
|
||||
case 1:
|
||||
statusText = "已使用"
|
||||
default:
|
||||
statusText = "禁用"
|
||||
}
|
||||
|
||||
// 格式:卡号----状态----创建时间
|
||||
record := []string{
|
||||
c.CardNumber,
|
||||
statusText,
|
||||
c.CreatedAt.Format(tf),
|
||||
}
|
||||
line := strings.Join(record, "----") + "\n"
|
||||
if _, err := w.Write([]byte(line)); err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
181
controllers/admin/card_stats.go
Normal file
181
controllers/admin/card_stats.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"networkDev/constants"
|
||||
"networkDev/database"
|
||||
"networkDev/models"
|
||||
"networkDev/utils"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CardStatsOverviewHandler 卡密统计概览API
|
||||
// - 返回当日和所有卡密的统计信息
|
||||
// - 包括:总数、使用/未使用/禁用状态分布
|
||||
func CardStatsOverviewHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当日统计
|
||||
today := time.Now().Format("2006-01-02")
|
||||
todayStart := today + " 00:00:00"
|
||||
todayEnd := today + " 23:59:59"
|
||||
|
||||
// 当日卡密统计
|
||||
var todayTotal int64
|
||||
var todayByStatus = make(map[int]int64)
|
||||
|
||||
// 当日总数
|
||||
db.Model(&models.Card{}).Where("created_at >= ? AND created_at <= ?", todayStart, todayEnd).Count(&todayTotal)
|
||||
|
||||
// 当日按状态分布
|
||||
var todayStatusCounts []struct {
|
||||
Status int `json:"status"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
db.Model(&models.Card{}).
|
||||
Select("status, count(*) as count").
|
||||
Where("created_at >= ? AND created_at <= ?", todayStart, todayEnd).
|
||||
Group("status").
|
||||
Find(&todayStatusCounts)
|
||||
|
||||
for _, sc := range todayStatusCounts {
|
||||
todayByStatus[sc.Status] = sc.Count
|
||||
}
|
||||
|
||||
// 所有卡密统计
|
||||
var allTotal int64
|
||||
var allByStatus = make(map[int]int64)
|
||||
|
||||
// 总数
|
||||
db.Model(&models.Card{}).Count(&allTotal)
|
||||
|
||||
// 按状态分布
|
||||
var allStatusCounts []struct {
|
||||
Status int `json:"status"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
db.Model(&models.Card{}).
|
||||
Select("status, count(*) as count").
|
||||
Group("status").
|
||||
Find(&allStatusCounts)
|
||||
|
||||
for _, sc := range allStatusCounts {
|
||||
allByStatus[sc.Status] = sc.Count
|
||||
}
|
||||
|
||||
// 构建响应数据
|
||||
data := map[string]interface{}{
|
||||
"today": map[string]interface{}{
|
||||
"total": todayTotal,
|
||||
"by_status": todayByStatus,
|
||||
},
|
||||
"all": map[string]interface{}{
|
||||
"total": allTotal,
|
||||
"by_status": allByStatus,
|
||||
},
|
||||
}
|
||||
|
||||
utils.JsonResponse(w, http.StatusOK, true, "获取成功", data)
|
||||
}
|
||||
|
||||
// CardStatsTrend30DaysHandler 卡密30天趋势API
|
||||
// - 返回近30天的卡密创建和使用趋势
|
||||
func CardStatsTrend30DaysHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 生成近30天的日期列表
|
||||
var dates []string
|
||||
var totalCounts []int64
|
||||
var usedCounts []int64
|
||||
var unusedCounts []int64
|
||||
|
||||
for i := 29; i >= 0; i-- {
|
||||
date := time.Now().AddDate(0, 0, -i).Format("2006-01-02")
|
||||
dates = append(dates, date)
|
||||
|
||||
dayStart := date + " 00:00:00"
|
||||
dayEnd := date + " 23:59:59"
|
||||
|
||||
// 当天创建的卡密总数
|
||||
var totalCount int64
|
||||
db.Model(&models.Card{}).Where("created_at >= ? AND created_at <= ?", dayStart, dayEnd).Count(&totalCount)
|
||||
totalCounts = append(totalCounts, totalCount)
|
||||
|
||||
// 当天创建且已使用的卡密数
|
||||
var usedCount int64
|
||||
db.Model(&models.Card{}).
|
||||
Where("created_at >= ? AND created_at <= ? AND status = ?", dayStart, dayEnd, constants.CardStatusUsed).
|
||||
Count(&usedCount)
|
||||
usedCounts = append(usedCounts, usedCount)
|
||||
|
||||
// 当天创建且未使用的卡密数
|
||||
var unusedCount int64
|
||||
db.Model(&models.Card{}).
|
||||
Where("created_at >= ? AND created_at <= ? AND status = ?", dayStart, dayEnd, constants.CardStatusUnused).
|
||||
Count(&unusedCount)
|
||||
unusedCounts = append(unusedCounts, unusedCount)
|
||||
}
|
||||
|
||||
// 构建响应数据
|
||||
data := map[string]interface{}{
|
||||
"dates": dates,
|
||||
"total": totalCounts,
|
||||
"used": usedCounts,
|
||||
"unused": unusedCounts,
|
||||
}
|
||||
|
||||
utils.JsonResponse(w, http.StatusOK, true, "获取成功", data)
|
||||
}
|
||||
|
||||
// CardStatsSimpleHandler 简单卡密统计API
|
||||
// - 返回卡密的基本统计信息:总数、已使用、未使用、禁用
|
||||
func CardStatsSimpleHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 统计各状态的卡密数量
|
||||
var total int64
|
||||
var used int64
|
||||
var unused int64
|
||||
var disabled int64
|
||||
|
||||
db.Model(&models.Card{}).Count(&total)
|
||||
db.Model(&models.Card{}).Where("status = ?", constants.CardStatusUsed).Count(&used)
|
||||
db.Model(&models.Card{}).Where("status = ?", constants.CardStatusUnused).Count(&unused)
|
||||
db.Model(&models.Card{}).Where("status = ?", constants.CardStatusDisabled).Count(&disabled)
|
||||
|
||||
data := map[string]interface{}{
|
||||
"total": total,
|
||||
"used": used,
|
||||
"unused": unused,
|
||||
"disabled": disabled,
|
||||
}
|
||||
|
||||
utils.JsonResponse(w, http.StatusOK, true, "获取成功", data)
|
||||
}
|
||||
428
controllers/admin/card_type.go
Normal file
428
controllers/admin/card_type.go
Normal file
@@ -0,0 +1,428 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"networkDev/database"
|
||||
"networkDev/models"
|
||||
"networkDev/utils"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CardTypesFragmentHandler 卡密类型管理片段渲染
|
||||
// - 渲染 card_types.html 列表与表单界面
|
||||
func CardTypesFragmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
utils.RenderTemplate(w, "card_types.html", map[string]interface{}{})
|
||||
}
|
||||
|
||||
// CardTypesListHandler 获取卡密类型列表
|
||||
// - 支持GET
|
||||
// - 支持分页和筛选
|
||||
func CardTypesListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取查询参数
|
||||
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||
pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size"))
|
||||
keyword := r.URL.Query().Get("keyword")
|
||||
statusStr := r.URL.Query().Get("status")
|
||||
|
||||
// 设置默认分页参数
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 构建查询条件
|
||||
query := db.Model(&models.CardType{})
|
||||
|
||||
// 筛选条件
|
||||
if keyword != "" {
|
||||
query = query.Where("name LIKE ?", "%"+keyword+"%")
|
||||
}
|
||||
if statusStr != "" {
|
||||
if status, err := strconv.Atoi(statusStr); err == nil {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算总数
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "统计总数失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
var items []models.CardType
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("id asc").Offset(offset).Limit(pageSize).Find(&items).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "查询失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 返回分页数据
|
||||
result := map[string]interface{}{
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"pages": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "ok", result)
|
||||
}
|
||||
|
||||
// CardTypeCreateHandler 新增卡密类型
|
||||
// - 接收JSON: {name, status, login_types}
|
||||
// - Name 必填且唯一
|
||||
func CardTypeCreateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
type reqBody struct {
|
||||
Name string `json:"name"`
|
||||
Status int `json:"status"`
|
||||
LoginTypes string `json:"login_types"`
|
||||
}
|
||||
var body reqBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "请求体错误", nil)
|
||||
return
|
||||
}
|
||||
if body.Name == "" {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "名称不能为空", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 校验登录方式ID是否存在
|
||||
if errMsg := validateLoginTypes(body.LoginTypes); errMsg != "" {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, errMsg, nil)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
item := models.CardType{
|
||||
Name: body.Name,
|
||||
Status: body.Status,
|
||||
LoginTypes: body.LoginTypes,
|
||||
}
|
||||
if item.Status != 0 {
|
||||
item.Status = 1
|
||||
}
|
||||
if err := db.Create(&item).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "创建失败,可能是名称重复", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "创建成功", item)
|
||||
}
|
||||
|
||||
// checkCardTypeInUse 检查卡密类型是否被卡密使用
|
||||
// - 通过 cards 表中 card_type_id 外键计数
|
||||
// - 返回是否被使用以及被使用的数量
|
||||
func checkCardTypeInUse(cardTypeID uint) (bool, int64, error) {
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
var count int64
|
||||
if err := db.Model(&models.Card{}).Where("card_type_id = ?", cardTypeID).Count(&count).Error; err != nil {
|
||||
return false, 0, err
|
||||
}
|
||||
return count > 0, count, nil
|
||||
}
|
||||
|
||||
// CardTypeUpdateHandler 更新卡密类型
|
||||
// - 接收JSON: {id, name, status, login_types}
|
||||
func CardTypeUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
type reqBody struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status int `json:"status"`
|
||||
LoginTypes string `json:"login_types"`
|
||||
}
|
||||
var body reqBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "请求体错误", nil)
|
||||
return
|
||||
}
|
||||
if body.ID == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "缺少ID", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 校验登录方式名称是否存在且未被禁用
|
||||
if errMsg := validateLoginTypes(body.LoginTypes); errMsg != "" {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, errMsg, nil)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 查询原始记录,便于后续在用校验(重命名/禁用)
|
||||
var original models.CardType
|
||||
if err := db.First(&original, body.ID).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "卡密类型不存在", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果名称发生变化且该卡密类型已被卡密使用,则不允许修改名称
|
||||
if body.Name != "" && body.Name != original.Name {
|
||||
inUse, count, err := checkCardTypeInUse(body.ID)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "检查使用状态失败", nil)
|
||||
return
|
||||
}
|
||||
if inUse {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "该卡密类型已被卡密使用(数量:"+strconv.FormatInt(count, 10)+"),无法修改名称", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 当尝试禁用(status=0)且原状态不是禁用时,如该类型已被卡密使用则禁止禁用
|
||||
if body.Status == 0 && original.Status != 0 {
|
||||
inUse, count, err := checkCardTypeInUse(body.ID)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "检查使用状态失败", nil)
|
||||
return
|
||||
}
|
||||
if inUse {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "该卡密类型已被卡密使用(数量:"+strconv.FormatInt(count, 10)+"),无法禁用", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 构建更新字段
|
||||
updates := map[string]interface{}{}
|
||||
if body.Name != "" {
|
||||
updates["name"] = body.Name
|
||||
}
|
||||
updates["status"] = body.Status
|
||||
updates["login_types"] = body.LoginTypes
|
||||
if err := db.Model(&models.CardType{}).Where("id = ?", body.ID).Updates(updates).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "更新失败,可能是名称重复", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "更新成功", nil)
|
||||
}
|
||||
|
||||
// CardTypeDeleteHandler 删除单个卡密类型
|
||||
// - 接收JSON: {id}
|
||||
func CardTypeDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
ID uint `json:"id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.ID == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "参数错误", nil)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 在用校验
|
||||
inUse, count, err := checkCardTypeInUse(body.ID)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "检查使用状态失败", nil)
|
||||
return
|
||||
}
|
||||
if inUse {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "该卡密类型已被卡密使用(数量:"+strconv.FormatInt(count, 10)+"),无法删除", nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Delete(&models.CardType{}, body.ID).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "删除失败", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "删除成功", nil)
|
||||
}
|
||||
|
||||
// CardTypesBatchDeleteHandler 批量删除卡密类型
|
||||
// - 接收JSON: {ids: []}
|
||||
func CardTypesBatchDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || len(body.IDs) == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "参数错误", nil)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 批量在用校验
|
||||
var blocking []string
|
||||
for _, id := range body.IDs {
|
||||
inUse, count, err := checkCardTypeInUse(id)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "检查使用状态失败", nil)
|
||||
return
|
||||
}
|
||||
if inUse {
|
||||
var ct models.CardType
|
||||
if db.First(&ct, id).Error == nil {
|
||||
blocking = append(blocking, ct.Name+"(数量:"+strconv.FormatInt(count, 10)+")")
|
||||
} else {
|
||||
blocking = append(blocking, strconv.FormatUint(uint64(id), 10)+"(数量:"+strconv.FormatInt(count, 10)+")")
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(blocking) > 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "以下卡密类型已被卡密使用,无法删除:"+strings.Join(blocking, ";"), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Delete(&models.CardType{}, body.IDs).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "批量删除失败", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "批量删除成功", nil)
|
||||
}
|
||||
|
||||
// validateLoginTypes 校验登录方式名称是否存在且未被禁用
|
||||
// - 接收逗号分隔的登录方式名称字符串
|
||||
// - 检查登录方式是否存在且状态为启用(status=1)
|
||||
// - 返回错误信息,如果所有名称都存在且启用则返回空字符串
|
||||
func validateLoginTypes(loginTypesStr string) string {
|
||||
if loginTypesStr == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 分割登录方式名称字符串
|
||||
nameStrs := strings.Split(loginTypesStr, ",")
|
||||
var names []string
|
||||
|
||||
// 去重并清理空格
|
||||
nameSet := make(map[string]bool)
|
||||
for _, nameStr := range nameStrs {
|
||||
nameStr = strings.TrimSpace(nameStr)
|
||||
if nameStr == "" {
|
||||
continue
|
||||
}
|
||||
nameSet[nameStr] = true
|
||||
}
|
||||
|
||||
// 转换为切片
|
||||
for name := range nameSet {
|
||||
names = append(names, name)
|
||||
}
|
||||
|
||||
if len(names) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// 查询数据库检查名称是否存在
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
return "数据库连接失败"
|
||||
}
|
||||
|
||||
// 查询所有匹配的登录方式,包括状态信息
|
||||
var loginTypes []models.LoginType
|
||||
if err := db.Where("name IN ?", names).Find(&loginTypes).Error; err != nil {
|
||||
return "查询登录方式失败"
|
||||
}
|
||||
|
||||
// 检查是否有不存在的名称和被禁用的登录方式
|
||||
existingSet := make(map[string]bool)
|
||||
disabledNames := []string{}
|
||||
for _, loginType := range loginTypes {
|
||||
existingSet[loginType.Name] = true
|
||||
// 检查登录方式是否被禁用 (status != 1 表示禁用)
|
||||
if loginType.Status != 1 {
|
||||
disabledNames = append(disabledNames, loginType.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查不存在的名称
|
||||
var notFoundNames []string
|
||||
for _, name := range names {
|
||||
if !existingSet[name] {
|
||||
notFoundNames = append(notFoundNames, name)
|
||||
}
|
||||
}
|
||||
|
||||
// 返回错误信息
|
||||
if len(notFoundNames) > 0 {
|
||||
return "以下登录方式名称不存在: " + strings.Join(notFoundNames, ", ")
|
||||
}
|
||||
if len(disabledNames) > 0 {
|
||||
return "以下登录方式已被禁用,无法使用: " + strings.Join(disabledNames, ", ")
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// CardTypesBatchEnableHandler 批量启用
|
||||
// - 接收JSON: {ids: []}
|
||||
func CardTypesBatchEnableHandler(w http.ResponseWriter, r *http.Request) {
|
||||
batchUpdateStatus(w, r, 1)
|
||||
}
|
||||
|
||||
// CardTypesBatchDisableHandler 批量禁用
|
||||
// - 接收JSON: {ids: []}
|
||||
func CardTypesBatchDisableHandler(w http.ResponseWriter, r *http.Request) {
|
||||
batchUpdateStatus(w, r, 0)
|
||||
}
|
||||
|
||||
// batchUpdateStatus 批量更新状态的通用函数
|
||||
// - status: 1 启用,0 禁用
|
||||
func batchUpdateStatus(w http.ResponseWriter, r *http.Request, status int) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || len(body.IDs) == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "参数错误", nil)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
if err := db.Model(&models.CardType{}).Where("id IN ?", body.IDs).Update("status", status).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "批量更新失败", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "操作成功", nil)
|
||||
}
|
||||
91
controllers/admin/handlers.go
Normal file
91
controllers/admin/handlers.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"networkDev/database"
|
||||
"networkDev/services"
|
||||
"networkDev/utils"
|
||||
"networkDev/utils/timeutil"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// AdminIndexHandler /admin 与 /admin/ 根路径入口
|
||||
// - 未登录:重定向到 /admin/login
|
||||
// - 已登录:渲染后台布局页(或重定向到 /admin/layout)
|
||||
func AdminIndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if IsAdminAuthenticated(r) {
|
||||
// 直接渲染布局页,保持URL为 /admin
|
||||
AdminLayoutHandler(w, r)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/admin/login", http.StatusFound)
|
||||
}
|
||||
|
||||
// AdminLayoutHandler 后台布局页渲染
|
||||
// - 渲染 layout.html,包含顶部导航、侧边栏与动态内容容器
|
||||
func AdminLayoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data := utils.GetDefaultTemplateData()
|
||||
|
||||
// 从数据库读取站点标题
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
data["Title"] = "凌动技术"
|
||||
} else {
|
||||
siteTitle, err := services.FindSettingByName("site_title", db)
|
||||
if err != nil || siteTitle == nil {
|
||||
data["Title"] = "凌动技术"
|
||||
} else {
|
||||
data["Title"] = siteTitle.Value
|
||||
}
|
||||
}
|
||||
|
||||
utils.RenderTemplate(w, "layout.html", data)
|
||||
}
|
||||
|
||||
// DashboardFragmentHandler 仪表盘片段渲染
|
||||
// - 展示系统信息:版本、运行模式、数据库类型、启动时长
|
||||
func DashboardFragmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
version := "1.0.0"
|
||||
mode := viper.GetString("server.mode")
|
||||
dbType := viper.GetString("database.type")
|
||||
if dbType == "" {
|
||||
dbType = "sqlite"
|
||||
}
|
||||
uptime := timeutil.GetServerUptimeString()
|
||||
|
||||
data := map[string]interface{}{
|
||||
"Version": version,
|
||||
"Mode": mode,
|
||||
"DBType": dbType,
|
||||
"Uptime": uptime,
|
||||
}
|
||||
|
||||
utils.RenderTemplate(w, "dashboard.html", data)
|
||||
}
|
||||
|
||||
// SystemInfoHandler 系统信息API接口
|
||||
// - 返回系统运行状态的JSON数据,用于前端定时刷新
|
||||
func SystemInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
version := "1.0.0"
|
||||
mode := viper.GetString("server.mode")
|
||||
dbType := viper.GetString("database.type")
|
||||
if dbType == "" {
|
||||
dbType = "sqlite"
|
||||
}
|
||||
uptime := timeutil.GetServerUptimeString()
|
||||
|
||||
data := map[string]interface{}{
|
||||
"version": version,
|
||||
"mode": mode,
|
||||
"db_type": dbType,
|
||||
"uptime": uptime,
|
||||
}
|
||||
|
||||
utils.JsonResponse(w, http.StatusOK, true, "ok", data)
|
||||
}
|
||||
394
controllers/admin/login_type.go
Normal file
394
controllers/admin/login_type.go
Normal file
@@ -0,0 +1,394 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"networkDev/database"
|
||||
"networkDev/models"
|
||||
"networkDev/utils"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LoginTypesFragmentHandler 登录方式管理片段渲染
|
||||
// - 渲染 login_types.html 列表与表单界面
|
||||
func LoginTypesFragmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
utils.RenderTemplate(w, "login_types.html", map[string]interface{}{})
|
||||
}
|
||||
|
||||
// LoginTypesListHandler 获取登录方式列表
|
||||
// - 支持GET
|
||||
// - 支持分页和筛选
|
||||
func LoginTypesListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取查询参数
|
||||
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||
pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size"))
|
||||
keyword := r.URL.Query().Get("keyword")
|
||||
statusStr := r.URL.Query().Get("status")
|
||||
|
||||
// 设置默认分页参数
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 构建查询条件
|
||||
query := db.Model(&models.LoginType{})
|
||||
|
||||
// 筛选条件
|
||||
if keyword != "" {
|
||||
query = query.Where("name LIKE ?", "%"+keyword+"%")
|
||||
}
|
||||
if statusStr != "" {
|
||||
if status, err := strconv.Atoi(statusStr); err == nil {
|
||||
query = query.Where("status = ?", status)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算总数
|
||||
var total int64
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "统计总数失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
var items []models.LoginType
|
||||
offset := (page - 1) * pageSize
|
||||
if err := query.Order("id asc").Offset(offset).Limit(pageSize).Find(&items).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "查询失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 返回分页数据
|
||||
result := map[string]interface{}{
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
"pages": (total + int64(pageSize) - 1) / int64(pageSize),
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "ok", result)
|
||||
}
|
||||
|
||||
// LoginTypeCreateHandler 新增登录方式
|
||||
// - 接收JSON: {name, description, status}
|
||||
// - Name 必填且唯一
|
||||
func LoginTypeCreateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
type reqBody struct {
|
||||
Name string `json:"name"`
|
||||
VerifyTypes string `json:"verify_types"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
var body reqBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "请求体错误", nil)
|
||||
return
|
||||
}
|
||||
if body.Name == "" {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "名称不能为空", nil)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
item := models.LoginType{
|
||||
Name: body.Name,
|
||||
Status: body.Status,
|
||||
VerifyTypes: body.VerifyTypes,
|
||||
}
|
||||
if item.Status != 0 {
|
||||
item.Status = 1
|
||||
}
|
||||
if err := db.Create(&item).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "创建失败,可能是名称重复", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "创建成功", item)
|
||||
}
|
||||
|
||||
// checkLoginTypeInUse 检查登录类型是否被卡密类型使用
|
||||
// - 检查 card_types 表中的 login_types 字段是否包含该登录类型名称
|
||||
// - 返回是否被使用和使用该登录类型的卡密类型名称列表
|
||||
func checkLoginTypeInUse(loginTypeName string) (bool, []string, error) {
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
var cardTypes []models.CardType
|
||||
// 查询包含该登录类型名称的卡密类型
|
||||
if err := db.Where("login_types LIKE ?", "%"+loginTypeName+"%").Find(&cardTypes).Error; err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
var usingCardTypes []string
|
||||
for _, cardType := range cardTypes {
|
||||
// 精确匹配登录类型名称(避免部分匹配)
|
||||
loginTypes := strings.Split(cardType.LoginTypes, ",")
|
||||
for _, lt := range loginTypes {
|
||||
if strings.TrimSpace(lt) == loginTypeName {
|
||||
usingCardTypes = append(usingCardTypes, cardType.Name)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return len(usingCardTypes) > 0, usingCardTypes, nil
|
||||
}
|
||||
|
||||
// checkLoginTypesByIDsInUse 批量检查登录类型ID是否被使用
|
||||
// - 先查询登录类型ID对应的名称,再检查是否被使用
|
||||
func checkLoginTypesByIDsInUse(loginTypeIDs []uint) (bool, map[uint][]string, error) {
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
// 查询登录类型名称
|
||||
var loginTypes []models.LoginType
|
||||
if err := db.Where("id IN ?", loginTypeIDs).Find(&loginTypes).Error; err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
hasUsage := false
|
||||
usageMap := make(map[uint][]string)
|
||||
|
||||
for _, loginType := range loginTypes {
|
||||
inUse, usingCardTypes, err := checkLoginTypeInUse(loginType.Name)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
if inUse {
|
||||
hasUsage = true
|
||||
usageMap[loginType.ID] = usingCardTypes
|
||||
}
|
||||
}
|
||||
|
||||
return hasUsage, usageMap, nil
|
||||
}
|
||||
|
||||
// LoginTypeUpdateHandler 更新登录方式
|
||||
// - 接收JSON: {id, name, description, status}
|
||||
func LoginTypeUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
type reqBody struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
VerifyTypes string `json:"verify_types"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
var body reqBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "请求体错误", nil)
|
||||
return
|
||||
}
|
||||
if body.ID == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "缺少ID", nil)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 始终查询原始记录,便于后续校验(重命名/禁用)
|
||||
var originalLoginType models.LoginType
|
||||
if err := db.First(&originalLoginType, body.ID).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "登录类型不存在", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果名称发生变化,检查原名称是否被使用(与删除逻辑一致)
|
||||
if body.Name != "" && originalLoginType.Name != body.Name {
|
||||
inUse, usingCardTypes, err := checkLoginTypeInUse(originalLoginType.Name)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "检查使用状态失败", nil)
|
||||
return
|
||||
}
|
||||
if inUse {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "该登录类型正在被以下卡密类型使用,无法修改名称:"+strings.Join(usingCardTypes, "、"), nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 当尝试禁用(status=0)时,如被卡密类型使用则禁止禁用
|
||||
if body.Status == 0 && originalLoginType.Status != 0 {
|
||||
inUse, usingCardTypes, err := checkLoginTypeInUse(originalLoginType.Name)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "检查使用状态失败", nil)
|
||||
return
|
||||
}
|
||||
if inUse {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "该登录类型正在被以下卡密类型使用,无法禁用:"+strings.Join(usingCardTypes, "、"), nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{}
|
||||
if body.Name != "" {
|
||||
updates["name"] = body.Name
|
||||
}
|
||||
updates["status"] = body.Status
|
||||
updates["verify_types"] = body.VerifyTypes
|
||||
if err := db.Model(&models.LoginType{}).Where("id = ?", body.ID).Updates(updates).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "更新失败,可能是名称重复", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "更新成功", nil)
|
||||
}
|
||||
|
||||
// LoginTypeDeleteHandler 删除单个登录方式
|
||||
// - 接收JSON: {id}
|
||||
func LoginTypeDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
ID uint `json:"id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.ID == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "参数错误", nil)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 查询登录类型名称
|
||||
var loginType models.LoginType
|
||||
if dbErr := db.First(&loginType, body.ID).Error; dbErr != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "登录类型不存在", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否被卡密类型使用
|
||||
inUse, usingCardTypes, err := checkLoginTypeInUse(loginType.Name)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "检查使用状态失败", nil)
|
||||
return
|
||||
}
|
||||
if inUse {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "该登录类型正在被以下卡密类型使用,无法删除:"+strings.Join(usingCardTypes, "、"), nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Delete(&models.LoginType{}, body.ID).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "删除失败", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "删除成功", nil)
|
||||
}
|
||||
|
||||
// LoginTypesBatchDeleteHandler 批量删除登录方式
|
||||
// - 接收JSON: {ids: []}
|
||||
func LoginTypesBatchDeleteHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || len(body.IDs) == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "参数错误", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查批量删除的登录类型是否被使用
|
||||
hasUsage, usageMap, err := checkLoginTypesByIDsInUse(body.IDs)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "检查使用状态失败", nil)
|
||||
return
|
||||
}
|
||||
if hasUsage {
|
||||
// 构建详细的错误信息
|
||||
var errorMessages []string
|
||||
db, _ := database.GetDB()
|
||||
for loginTypeID, usingCardTypes := range usageMap {
|
||||
var loginType models.LoginType
|
||||
if db.First(&loginType, loginTypeID).Error == nil {
|
||||
errorMessages = append(errorMessages, loginType.Name+"(被"+strings.Join(usingCardTypes, "、")+"使用)")
|
||||
}
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "以下登录类型正在被使用,无法删除:"+strings.Join(errorMessages, ";"), nil)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
if err := db.Delete(&models.LoginType{}, body.IDs).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "批量删除失败", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "批量删除成功", nil)
|
||||
}
|
||||
|
||||
// LoginTypesBatchEnableHandler 批量启用
|
||||
// - 接收JSON: {ids: []}
|
||||
func LoginTypesBatchEnableHandler(w http.ResponseWriter, r *http.Request) {
|
||||
batchUpdateLoginTypeStatus(w, r, 1)
|
||||
}
|
||||
|
||||
// LoginTypesBatchDisableHandler 批量禁用
|
||||
// - 接收JSON: {ids: []}
|
||||
func LoginTypesBatchDisableHandler(w http.ResponseWriter, r *http.Request) {
|
||||
batchUpdateLoginTypeStatus(w, r, 0)
|
||||
}
|
||||
|
||||
// batchUpdateLoginTypeStatus 批量更新登录方式状态的通用函数
|
||||
// - status: 1 启用,0 禁用
|
||||
func batchUpdateLoginTypeStatus(w http.ResponseWriter, r *http.Request, status int) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || len(body.IDs) == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "参数错误", nil)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
if err := db.Model(&models.LoginType{}).Where("id IN ?", body.IDs).Update("status", status).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "批量更新失败", nil)
|
||||
return
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "操作成功", nil)
|
||||
}
|
||||
147
controllers/admin/settings.go
Normal file
147
controllers/admin/settings.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"networkDev/database"
|
||||
"networkDev/models"
|
||||
"networkDev/utils"
|
||||
|
||||
// 新增:用于刷新内存缓存
|
||||
"networkDev/services"
|
||||
// 新增:用于RedisDel上下文
|
||||
"context"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// SettingsFragmentHandler 设置片段渲染
|
||||
// - 渲染设置表单(通过前端JS调用API加载/保存)
|
||||
func SettingsFragmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
utils.RenderTemplate(w, "settings.html", map[string]interface{}{})
|
||||
}
|
||||
|
||||
// SettingsQueryHandler 设置查询API
|
||||
// - 返回所有设置项的 name:value 映射
|
||||
func SettingsQueryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
var list []models.Settings
|
||||
if err := db.Find(&list).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "查询失败", nil)
|
||||
return
|
||||
}
|
||||
res := map[string]string{}
|
||||
for _, s := range list {
|
||||
res[s.Name] = s.Value
|
||||
}
|
||||
utils.JsonResponse(w, http.StatusOK, true, "ok", res)
|
||||
}
|
||||
|
||||
// SettingsUpdateHandler 更新系统设置处理器
|
||||
// - 接收JSON格式的设置数据,支持两种格式:
|
||||
// 1. 直接字段格式: {"site_title": "值", "site_keywords": "值"}
|
||||
// 2. 嵌套格式: {"settings": {"site_title": "值", "site_keywords": "值"}}
|
||||
//
|
||||
// - 自动创建不存在的设置项
|
||||
// - 更新已存在的设置项
|
||||
// - 更新完成后:
|
||||
// 1. 删除对应的Redis缓存键,确保后续读取走数据库并重建缓存
|
||||
// 2. 刷新SettingsService内存缓存
|
||||
func SettingsUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 先尝试解析为直接字段格式
|
||||
var directBody map[string]interface{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&directBody); err != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "请求体错误", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 提取设置数据
|
||||
var settingsData map[string]string
|
||||
|
||||
// 检查是否为嵌套格式(包含settings字段)
|
||||
if settings, exists := directBody["settings"]; exists {
|
||||
if settingsMap, ok := settings.(map[string]interface{}); ok {
|
||||
settingsData = make(map[string]string)
|
||||
for k, v := range settingsMap {
|
||||
if str, ok := v.(string); ok {
|
||||
settingsData[k] = str
|
||||
}
|
||||
}
|
||||
} else {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "settings字段格式错误", nil)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// 直接字段格式
|
||||
settingsData = make(map[string]string)
|
||||
for k, v := range directBody {
|
||||
if str, ok := v.(string); ok {
|
||||
settingsData[k] = str
|
||||
} else if v != nil {
|
||||
// 转换其他类型为字符串
|
||||
settingsData[k] = fmt.Sprintf("%v", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(settingsData) == 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "无设置项", nil)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 记录需要失效的缓存键,统一删除,减少与Redis交互次数
|
||||
keysToDel := make([]string, 0, len(settingsData))
|
||||
|
||||
// 批量处理设置项
|
||||
for k, v := range settingsData {
|
||||
var s models.Settings
|
||||
if err := db.Where("name = ?", k).First(&s).Error; err != nil {
|
||||
// 不存在则创建
|
||||
s = models.Settings{Name: k, Value: v}
|
||||
if err := db.Create(&s).Error; err != nil {
|
||||
logrus.WithError(err).WithField("setting_name", k).Error("创建设置失败")
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, fmt.Sprintf("保存设置 %s 失败", k), nil)
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
// 存在则更新
|
||||
if err := db.Model(&models.Settings{}).Where("id = ?", s.ID).Update("value", v).Error; err != nil {
|
||||
logrus.WithError(err).WithField("setting_name", k).Error("更新设置失败")
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, fmt.Sprintf("更新设置 %s 失败", k), nil)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
// 收集对应的Redis缓存键(与services/query.go中的键命名保持一致)
|
||||
keysToDel = append(keysToDel, fmt.Sprintf("setting:%s", k))
|
||||
}
|
||||
|
||||
// 删除Redis缓存键(如果Redis不可用则静默跳过)
|
||||
_ = utils.RedisDel(context.Background(), keysToDel...)
|
||||
|
||||
// 刷新内存中的设置缓存,保证后续读取一致
|
||||
services.GetSettingsService().RefreshCache()
|
||||
|
||||
utils.JsonResponse(w, http.StatusOK, true, "保存成功", nil)
|
||||
}
|
||||
238
controllers/admin/user.go
Normal file
238
controllers/admin/user.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"networkDev/database"
|
||||
"networkDev/models"
|
||||
"networkDev/utils"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// UserFragmentHandler 个人资料片段渲染
|
||||
// - 渲染个人资料与修改密码表单
|
||||
func UserFragmentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
utils.RenderTemplate(w, "user.html", map[string]interface{}{})
|
||||
}
|
||||
|
||||
// UserProfileQueryHandler 查询当前登录管理员的基本信息
|
||||
// - 返回 id/username/role 三个字段
|
||||
// - 自动刷新接近过期的JWT令牌
|
||||
func UserProfileQueryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
claims, _, err := GetCurrentAdminUserWithRefresh(w, r)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusUnauthorized, false, "未登录或会话已过期", nil)
|
||||
return
|
||||
}
|
||||
|
||||
utils.JsonResponse(w, http.StatusOK, true, "ok", map[string]interface{}{
|
||||
"id": claims.UserID,
|
||||
"username": claims.Username,
|
||||
"role": claims.Role,
|
||||
})
|
||||
}
|
||||
|
||||
// UserPasswordUpdateHandler 修改当前登录管理员的密码
|
||||
// - 接收 JSON: {old_password, new_password, confirm_password}
|
||||
// - 校验旧密码正确性、新密码与确认一致性
|
||||
// - 成功后更新密码哈希
|
||||
// - 自动刷新接近过期的JWT令牌
|
||||
func UserPasswordUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
claims, _, err := GetCurrentAdminUserWithRefresh(w, r)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusUnauthorized, false, "未登录或会话已过期", nil)
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
OldPassword string `json:"old_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
ConfirmPassword string `json:"confirm_password"`
|
||||
}
|
||||
var decodeErr error
|
||||
if decodeErr = json.NewDecoder(r.Body).Decode(&body); decodeErr != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "请求参数错误", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 基础校验
|
||||
if body.OldPassword == "" || body.NewPassword == "" || body.ConfirmPassword == "" {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "旧密码/新密码/确认密码均不能为空", nil)
|
||||
return
|
||||
}
|
||||
if len(body.NewPassword) < 6 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "新密码长度不能少于6位", nil)
|
||||
return
|
||||
}
|
||||
if body.NewPassword != body.ConfirmPassword {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "两次输入的新密码不一致", nil)
|
||||
return
|
||||
}
|
||||
if body.NewPassword == body.OldPassword {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "新密码不能与旧密码相同", nil)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 查询当前用户
|
||||
var user models.User
|
||||
if dbErr := db.First(&user, claims.UserID).Error; dbErr != nil {
|
||||
utils.JsonResponse(w, http.StatusNotFound, false, "用户不存在", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 校验旧密码(使用盐值验证)
|
||||
if !utils.VerifyPasswordWithSalt(body.OldPassword, user.PasswordSalt, user.Password) {
|
||||
utils.JsonResponse(w, http.StatusUnauthorized, false, "旧密码不正确", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 生成新的密码盐值
|
||||
newSalt, err := utils.GenerateRandomSalt()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成密码盐失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 使用新盐值生成密码哈希
|
||||
hash, err := utils.HashPasswordWithSalt(body.NewPassword, newSalt)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成密码哈希失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新密码和盐值
|
||||
if err := db.Model(&models.User{}).Where("id = ?", claims.UserID).Updates(map[string]interface{}{
|
||||
"password": hash,
|
||||
"password_salt": newSalt,
|
||||
}).Error; err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "更新密码失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 可选:安全起见,通知前端跳转到登录页
|
||||
utils.JsonResponse(w, http.StatusOK, true, "密码修改成功,请重新登录", map[string]interface{}{
|
||||
"redirect": "/admin/login",
|
||||
})
|
||||
}
|
||||
|
||||
// UserProfileUpdateHandler 修改当前登录管理员的用户名
|
||||
// - 接收 JSON: {username}
|
||||
// - 校验用户名非空、长度与唯一性
|
||||
// - 更新数据库后重新签发JWT并写入 Cookie,保持前端展示的一致性
|
||||
// - 自动刷新接近过期的JWT令牌
|
||||
func UserProfileUpdateHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
claims, _, err := GetCurrentAdminUserWithRefresh(w, r)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusUnauthorized, false, "未登录或会话已过期", nil)
|
||||
return
|
||||
}
|
||||
|
||||
var body struct {
|
||||
Username string `json:"username"`
|
||||
OldPassword string `json:"old_password"`
|
||||
}
|
||||
if decodeErr := json.NewDecoder(r.Body).Decode(&body); decodeErr != nil {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "请求参数错误", nil)
|
||||
return
|
||||
}
|
||||
|
||||
username := strings.TrimSpace(body.Username)
|
||||
if username == "" {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "用户名不能为空", nil)
|
||||
return
|
||||
}
|
||||
if len(username) > 64 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "用户名长度不能超过64字符", nil)
|
||||
return
|
||||
}
|
||||
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "数据库连接失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查唯一性:排除当前用户ID
|
||||
var cnt int64
|
||||
if dbErr := db.Model(&models.User{}).Where("username = ? AND id <> ?", username, claims.UserID).Count(&cnt).Error; dbErr != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "检查用户名唯一性失败", nil)
|
||||
return
|
||||
}
|
||||
if cnt > 0 {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "用户名已存在,请更换", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 如果未变化则直接返回成功(无需校验旧密码)
|
||||
if strings.EqualFold(username, claims.Username) {
|
||||
utils.JsonResponse(w, http.StatusOK, true, "保存成功", map[string]interface{}{
|
||||
"username": username,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 修改用户名需要进行当前密码校验
|
||||
if strings.TrimSpace(body.OldPassword) == "" {
|
||||
utils.JsonResponse(w, http.StatusBadRequest, false, "修改用户名需要提供当前密码", nil)
|
||||
return
|
||||
}
|
||||
// 查询当前用户并校验旧密码
|
||||
var user models.User
|
||||
if dbErr := db.First(&user, claims.UserID).Error; dbErr != nil {
|
||||
utils.JsonResponse(w, http.StatusNotFound, false, "用户不存在", nil)
|
||||
return
|
||||
}
|
||||
// 使用盐值验证当前密码
|
||||
if !utils.VerifyPasswordWithSalt(body.OldPassword, user.PasswordSalt, user.Password) {
|
||||
utils.JsonResponse(w, http.StatusUnauthorized, false, "当前密码不正确", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 执行更新
|
||||
if dbErr := db.Model(&models.User{}).Where("id = ?", claims.UserID).Update("username", username).Error; dbErr != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "更新用户名失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 重新签发JWT并写入Cookie
|
||||
newUser := models.User{ID: claims.UserID, Username: username, Role: claims.Role}
|
||||
token, err := generateJWTToken(newUser)
|
||||
if err != nil {
|
||||
utils.JsonResponse(w, http.StatusInternalServerError, false, "生成新令牌失败", nil)
|
||||
return
|
||||
}
|
||||
cookie := &http.Cookie{
|
||||
Name: "admin_session",
|
||||
Value: token,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: false,
|
||||
MaxAge: 24 * 60 * 60,
|
||||
}
|
||||
http.SetCookie(w, cookie)
|
||||
|
||||
utils.JsonResponse(w, http.StatusOK, true, "保存成功", map[string]interface{}{
|
||||
"username": username,
|
||||
})
|
||||
}
|
||||
73
controllers/home/home.go
Normal file
73
controllers/home/home.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package home
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"networkDev/database"
|
||||
"networkDev/models"
|
||||
"networkDev/services"
|
||||
"networkDev/utils"
|
||||
)
|
||||
|
||||
// RootHandler 主页处理器
|
||||
func RootHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取数据库连接
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
http.Error(w, "数据库连接失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 从数据库获取站点标题和页脚文本
|
||||
siteTitle, err := services.FindSettingByName("site_title", db)
|
||||
if err != nil {
|
||||
siteTitle = &models.Settings{Value: "凌动技术"}
|
||||
}
|
||||
|
||||
footerText, err := services.FindSettingByName("footer_text", db)
|
||||
if err != nil {
|
||||
footerText = &models.Settings{Value: "© 2025 凌动技术 保留所有权利"}
|
||||
}
|
||||
|
||||
// 从数据库获取备案信息
|
||||
icpRecord, err := services.FindSettingByName("icp_record", db)
|
||||
if err != nil {
|
||||
icpRecord = &models.Settings{Value: ""}
|
||||
}
|
||||
|
||||
icpRecordLink, err := services.FindSettingByName("icp_record_link", db)
|
||||
if err != nil {
|
||||
icpRecordLink = &models.Settings{Value: "https://beian.miit.gov.cn"}
|
||||
}
|
||||
|
||||
// 从数据库获取公安备案信息
|
||||
psbRecord, err := services.FindSettingByName("psb_record", db)
|
||||
if err != nil {
|
||||
psbRecord = &models.Settings{Value: ""}
|
||||
}
|
||||
|
||||
psbRecordLink, err := services.FindSettingByName("psb_record_link", db)
|
||||
if err != nil {
|
||||
psbRecordLink = &models.Settings{Value: "https://www.beian.gov.cn"}
|
||||
}
|
||||
|
||||
// 准备模板数据
|
||||
data := map[string]interface{}{
|
||||
"SystemName": siteTitle.Value,
|
||||
"FooterText": footerText.Value,
|
||||
"ICPRecord": icpRecord.Value,
|
||||
"ICPRecordLink": icpRecordLink.Value,
|
||||
"PSBRecord": psbRecord.Value,
|
||||
"PSBRecordLink": psbRecordLink.Value,
|
||||
"title": "主页",
|
||||
}
|
||||
|
||||
if err := utils.RenderTemplate(w, "index.html", data); err != nil {
|
||||
http.Error(w, "页面加载失败", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user