This commit is contained in:
2026-02-23 23:50:04 +08:00
commit 084d3b0faf
45 changed files with 4090 additions and 0 deletions

53
internal/config/config.go Normal file
View File

@@ -0,0 +1,53 @@
package config
import "time"
type Config struct {
Server ServerConfig
Database DatabaseConfig
Session SessionConfig
App AppConfig
}
type ServerConfig struct {
Port string
ReadTimeout time.Duration
WriteTimeout time.Duration
}
type DatabaseConfig struct {
Path string
}
type SessionConfig struct {
Secret string
Name string
}
type AppConfig struct {
Name string
Version string
}
var AppConfigInstance = &Config{
Server: ServerConfig{
Port: ":8080",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
},
Database: DatabaseConfig{
Path: "data/lv8girl.db",
},
Session: SessionConfig{
Secret: "lv8girl-secret-key-change-in-production",
Name: "lv8girl_session",
},
App: AppConfig{
Name: "lv8girl",
Version: "1.0.0",
},
}
func GetConfig() *Config {
return AppConfigInstance
}

View File

@@ -0,0 +1,179 @@
package controllers
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"lv8girl/internal/middleware"
"lv8girl/internal/services"
)
type AdminController struct {
adminSvc *services.AdminService
messageSvc *services.MessageService
}
func NewAdminController() *AdminController {
return &AdminController{
adminSvc: services.NewAdminService(),
messageSvc: services.NewMessageService(),
}
}
func (c *AdminController) Dashboard(ctx *gin.Context) {
username, _ := ctx.Get("username")
stats, _ := c.adminSvc.GetStats()
ctx.HTML(http.StatusOK, "admin_dashboard.html", gin.H{
"Username": username,
"Stats": stats,
"Page": "dashboard",
"Message": ctx.Query("msg"),
})
}
func (c *AdminController) PendingPosts(ctx *gin.Context) {
username, _ := ctx.Get("username")
posts, _ := c.adminSvc.GetPendingPosts()
ctx.HTML(http.StatusOK, "admin_pending_posts.html", gin.H{
"Username": username,
"Posts": posts,
"Page": "pending_posts",
"Message": ctx.Query("msg"),
})
}
func (c *AdminController) ApprovePost(ctx *gin.Context) {
postID := parseUint(ctx.Param("id"))
action := ctx.Param("action")
if action == "approve" {
c.adminSvc.ApprovePost(postID)
ctx.Redirect(http.StatusFound, "/admin/pending_posts?msg=帖子已通过审核")
} else if action == "reject" {
c.adminSvc.RejectPost(postID)
ctx.Redirect(http.StatusFound, "/admin/pending_posts?msg=帖子已拒绝")
} else {
ctx.Redirect(http.StatusFound, "/admin/pending_posts")
}
}
func (c *AdminController) PendingUsers(ctx *gin.Context) {
username, _ := ctx.Get("username")
users, _ := c.adminSvc.GetPendingUsers()
ctx.HTML(http.StatusOK, "admin_pending_users.html", gin.H{
"Username": username,
"Users": users,
"Page": "pending_users",
"Message": ctx.Query("msg"),
})
}
func (c *AdminController) ApproveUser(ctx *gin.Context) {
userID := parseUint(ctx.Param("id"))
action := ctx.Param("action")
adminID, _ := ctx.Get("user_id")
if action == "approve" {
c.adminSvc.ApproveUser(userID)
c.messageSvc.NotifyUserApproved(adminID.(uint), userID)
ctx.Redirect(http.StatusFound, "/admin/pending_users?msg=用户已通过审核")
} else if action == "reject" {
c.adminSvc.RejectUser(userID)
c.messageSvc.NotifyUserRejected(adminID.(uint), userID)
ctx.Redirect(http.StatusFound, "/admin/pending_users?msg=用户已拒绝")
} else {
ctx.Redirect(http.StatusFound, "/admin/pending_users")
}
}
func (c *AdminController) Posts(ctx *gin.Context) {
username, _ := ctx.Get("username")
posts, _ := c.adminSvc.GetAllPosts()
ctx.HTML(http.StatusOK, "admin_posts.html", gin.H{
"Username": username,
"Posts": posts,
"Page": "posts",
"Message": ctx.Query("msg"),
})
}
func (c *AdminController) DeletePost(ctx *gin.Context) {
postID := parseUint(ctx.Param("id"))
c.adminSvc.DeletePost(postID)
ctx.Redirect(http.StatusFound, "/admin/posts?msg=帖子已删除")
}
func (c *AdminController) Users(ctx *gin.Context) {
username, _ := ctx.Get("username")
currentUserID, _ := ctx.Get("user_id")
users, _ := c.adminSvc.GetAllUsers()
ctx.HTML(http.StatusOK, "admin_users.html", gin.H{
"Username": username,
"Users": users,
"CurrentUserID": currentUserID,
"Page": "users",
"Message": ctx.Query("msg"),
})
}
func (c *AdminController) UpdateUserRole(ctx *gin.Context) {
userIDStr := ctx.PostForm("user_id")
newRole := ctx.PostForm("new_role")
currentUserID, _ := ctx.Get("user_id")
userID, _ := strconv.ParseUint(userIDStr, 10, 32)
if uint(userID) == currentUserID.(uint) {
ctx.Redirect(http.StatusFound, "/admin/users?msg=不能修改自己的角色")
return
}
c.adminSvc.UpdateUserRole(uint(userID), newRole)
if newRole == "banned" {
c.messageSvc.NotifyUserBanned(currentUserID.(uint), uint(userID))
}
ctx.Redirect(http.StatusFound, "/admin/users?msg=用户角色已更新")
}
func (c *AdminController) DeleteUser(ctx *gin.Context) {
userID := parseUint(ctx.Param("id"))
currentUserID, _ := ctx.Get("user_id")
if userID == currentUserID.(uint) {
ctx.Redirect(http.StatusFound, "/admin/users?msg=不能删除自己")
return
}
c.adminSvc.DeleteUser(userID)
ctx.Redirect(http.StatusFound, "/admin/users?msg=用户已删除")
}
func (c *AdminController) Comments(ctx *gin.Context) {
username, _ := ctx.Get("username")
comments, _ := c.adminSvc.GetAllComments()
ctx.HTML(http.StatusOK, "admin_comments.html", gin.H{
"Username": username,
"Comments": comments,
"Page": "comments",
"Message": ctx.Query("msg"),
})
}
func (c *AdminController) DeleteComment(ctx *gin.Context) {
commentID := parseUint(ctx.Param("id"))
c.adminSvc.DeleteComment(commentID)
ctx.Redirect(http.StatusFound, "/admin/comments?msg=评论已删除")
}
func init() {
_ = middleware.GetCurrentUser
}

View File

@@ -0,0 +1,99 @@
package controllers
import (
"net/http"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"lv8girl/internal/services"
)
type AuthController struct {
authService *services.AuthService
}
func NewAuthController() *AuthController {
return &AuthController{
authService: services.NewAuthService(),
}
}
func (c *AuthController) ShowLogin(ctx *gin.Context) {
session := sessions.Default(ctx)
if session.Get("user_id") != nil {
ctx.Redirect(http.StatusFound, "/")
return
}
ctx.HTML(http.StatusOK, "login.html", gin.H{"Error": ""})
}
func (c *AuthController) Login(ctx *gin.Context) {
session := sessions.Default(ctx)
if session.Get("user_id") != nil {
ctx.Redirect(http.StatusFound, "/")
return
}
login := ctx.PostForm("login")
password := ctx.PostForm("password")
if login == "" || password == "" {
ctx.HTML(http.StatusOK, "login.html", gin.H{"Error": "请输入用户名/邮箱和密码"})
return
}
result := c.authService.Login(login, password)
if !result.Success {
ctx.HTML(http.StatusOK, "login.html", gin.H{"Error": result.Error})
return
}
session.Set("user_id", result.User.ID)
session.Set("username", result.User.Username)
session.Set("user_role", result.User.Role)
session.Save()
ctx.Redirect(http.StatusFound, "/")
}
func (c *AuthController) ShowRegister(ctx *gin.Context) {
session := sessions.Default(ctx)
if session.Get("user_id") != nil {
ctx.Redirect(http.StatusFound, "/")
return
}
ctx.HTML(http.StatusOK, "register.html", gin.H{"Error": "", "Success": ""})
}
func (c *AuthController) Register(ctx *gin.Context) {
session := sessions.Default(ctx)
if session.Get("user_id") != nil {
ctx.Redirect(http.StatusFound, "/")
return
}
username := ctx.PostForm("username")
email := ctx.PostForm("email")
password := ctx.PostForm("password")
confirmPassword := ctx.PostForm("confirm_password")
if password != confirmPassword {
ctx.HTML(http.StatusOK, "register.html", gin.H{"Error": "两次输入的密码不一致", "Success": ""})
return
}
result := c.authService.Register(username, email, password)
if !result.Success {
ctx.HTML(http.StatusOK, "register.html", gin.H{"Error": result.Error, "Success": ""})
return
}
ctx.HTML(http.StatusOK, "register.html", gin.H{"Error": "", "Success": "注册成功!您的账号正在等待管理员审核,请耐心等待。"})
}
func (c *AuthController) Logout(ctx *gin.Context) {
session := sessions.Default(ctx)
session.Clear()
session.Save()
ctx.Redirect(http.StatusFound, "/")
}

View File

@@ -0,0 +1,121 @@
package controllers
import (
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
"lv8girl/internal/middleware"
"lv8girl/internal/services"
)
type DiscussionController struct {
discussionSvc *services.DiscussionService
}
func NewDiscussionController() *DiscussionController {
return &DiscussionController{
discussionSvc: services.NewDiscussionService(),
}
}
func (c *DiscussionController) ShowNewPost(ctx *gin.Context) {
userID, username, userRole, _ := middleware.GetCurrentUser(ctx)
ctx.HTML(http.StatusOK, "post_discussion.html", gin.H{
"IsLoggedIn": true,
"UserID": userID,
"Username": username,
"UserRole": userRole,
"Error": "",
"Success": "",
})
}
func (c *DiscussionController) CreatePost(ctx *gin.Context) {
userID, username, userRole, _ := middleware.GetCurrentUser(ctx)
title := strings.TrimSpace(ctx.PostForm("title"))
content := strings.TrimSpace(ctx.PostForm("content"))
if title == "" || content == "" {
ctx.HTML(http.StatusOK, "post_discussion.html", gin.H{
"IsLoggedIn": true,
"UserID": userID,
"Username": username,
"UserRole": userRole,
"Error": "标题和内容不能为空",
"Success": "",
})
return
}
var imagePath string
file, err := ctx.FormFile("image")
if err == nil {
if file.Size > 2*1024*1024 {
ctx.HTML(http.StatusOK, "post_discussion.html", gin.H{
"IsLoggedIn": true,
"UserID": userID,
"Username": username,
"UserRole": userRole,
"Error": "图片大小不能超过2MB",
"Success": "",
})
return
}
ext := strings.ToLower(filepath.Ext(file.Filename))
filename := "post_" + time.Now().Format("20060102150405") + "_" + randomString(8) + ext
uploadDir := "uploads/posts"
if _, err := os.Stat(uploadDir); os.IsNotExist(err) {
os.MkdirAll(uploadDir, 0755)
}
imagePath = filepath.Join(uploadDir, filename)
if err := ctx.SaveUploadedFile(file, imagePath); err != nil {
ctx.HTML(http.StatusOK, "post_discussion.html", gin.H{
"IsLoggedIn": true,
"UserID": userID,
"Username": username,
"UserRole": userRole,
"Error": "图片保存失败",
"Success": "",
})
return
}
}
if err := c.discussionSvc.CreatePost(userID, title, content, imagePath); err != nil {
ctx.HTML(http.StatusOK, "post_discussion.html", gin.H{
"IsLoggedIn": true,
"UserID": userID,
"Username": username,
"UserRole": userRole,
"Error": "发帖失败,请稍后重试",
"Success": "",
})
return
}
ctx.HTML(http.StatusOK, "post_discussion.html", gin.H{
"IsLoggedIn": true,
"UserID": userID,
"Username": username,
"UserRole": userRole,
"Error": "",
"Success": "帖子已提交,等待管理员审核。",
})
}
func randomString(n int) string {
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, n)
for i := range b {
b[i] = letters[time.Now().UnixNano()%int64(len(letters))]
}
return string(b)
}

View File

@@ -0,0 +1,150 @@
package controllers
import (
"net/http"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"lv8girl/internal/middleware"
"lv8girl/internal/repositories"
"lv8girl/internal/services"
)
type HomeController struct {
discussionSvc *services.DiscussionService
userSvc *services.UserService
messageSvc *services.MessageService
discussionRepo *repositories.DiscussionRepository
}
func NewHomeController() *HomeController {
return &HomeController{
discussionSvc: services.NewDiscussionService(),
userSvc: services.NewUserService(),
messageSvc: services.NewMessageService(),
discussionRepo: repositories.NewDiscussionRepository(),
}
}
func (c *HomeController) Index(ctx *gin.Context) {
session := sessions.Default(ctx)
userID, _ := session.Get("user_id").(uint)
username, _ := session.Get("username").(string)
userRole, _ := session.Get("user_role").(string)
isLoggedIn := userID != 0
if isLoggedIn {
userRepo := repositories.NewUserRepository()
userRepo.UpdateLastActive(userID)
}
posts, _ := c.discussionSvc.GetApprovedPosts(30)
var postCount, userCount, onlineCount int64
postCount, _ = c.discussionRepo.CountByStatus("approved")
userCount, onlineCount, _ = c.userSvc.GetUserStats()
var unreadCount int64
if isLoggedIn {
unreadCount, _ = c.messageSvc.GetUnreadCount(userID)
}
ctx.HTML(http.StatusOK, "index.html", gin.H{
"IsLoggedIn": isLoggedIn,
"UserID": userID,
"Username": username,
"UserRole": userRole,
"Posts": posts,
"PostCount": postCount,
"UserCount": userCount,
"OnlineCount": onlineCount,
"UnreadCount": unreadCount,
})
}
func (c *HomeController) ShowPost(ctx *gin.Context) {
postID := parseUint(ctx.Param("id"))
userID, username, userRole, isLoggedIn := middleware.GetCurrentUser(ctx)
session := sessions.Default(ctx)
viewedKey := "viewed_posts"
viewedPosts := session.Get(viewedKey)
var viewedMap map[uint]bool
if viewedPosts == nil {
viewedMap = make(map[uint]bool)
} else {
viewedMap = viewedPosts.(map[uint]bool)
}
if !viewedMap[postID] {
c.discussionSvc.IncrementViews(postID)
viewedMap[postID] = true
session.Set(viewedKey, viewedMap)
session.Save()
}
detail, err := c.discussionSvc.GetPostDetail(postID, userID)
if err != nil {
ctx.String(http.StatusNotFound, "帖子不存在")
return
}
comments, _ := c.discussionSvc.GetComments(postID)
avatar := ""
if detail.Post.User.Avatar != "" {
avatar = "/" + detail.Post.User.Avatar
}
ctx.HTML(http.StatusOK, "post.html", gin.H{
"IsLoggedIn": isLoggedIn,
"UserID": userID,
"Username": username,
"UserRole": userRole,
"Post": detail.Post,
"PostAvatar": avatar,
"Comments": comments,
"LikeCount": detail.LikeCount,
"UserLiked": detail.UserLiked,
"AuthorPostCount": detail.AuthorPostCount,
})
}
func (c *HomeController) LikePost(ctx *gin.Context) {
userID, _, _, isLoggedIn := middleware.GetCurrentUser(ctx)
if !isLoggedIn {
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
return
}
postID := parseUint(ctx.Param("id"))
c.discussionSvc.AddLike(postID, userID)
ctx.Redirect(http.StatusFound, "/post/"+ctx.Param("id"))
}
func (c *HomeController) AddComment(ctx *gin.Context) {
userID, _, _, isLoggedIn := middleware.GetCurrentUser(ctx)
if !isLoggedIn {
ctx.Redirect(http.StatusFound, "/login")
return
}
postID := parseUint(ctx.Param("id"))
content := ctx.PostForm("content")
if content != "" {
c.discussionSvc.AddComment(postID, userID, content)
}
ctx.Redirect(http.StatusFound, "/post/"+ctx.Param("id"))
}
func parseUint(s string) uint {
var result uint
for _, c := range s {
if c >= '0' && c <= '9' {
result = result*10 + uint(c-'0')
}
}
return result
}

View File

@@ -0,0 +1,89 @@
package controllers
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"lv8girl/internal/middleware"
"lv8girl/internal/repositories"
"lv8girl/internal/services"
)
type MessageController struct {
messageSvc *services.MessageService
userRepo *repositories.UserRepository
}
func NewMessageController() *MessageController {
return &MessageController{
messageSvc: services.NewMessageService(),
userRepo: repositories.NewUserRepository(),
}
}
func (c *MessageController) ShowMessages(ctx *gin.Context) {
currentUserID, currentUsername, currentUserRole, _ := middleware.GetCurrentUser(ctx)
conversations, _ := c.messageSvc.GetConversations(currentUserID)
ctx.HTML(http.StatusOK, "messages.html", gin.H{
"IsLoggedIn": true,
"UserID": currentUserID,
"Username": currentUsername,
"UserRole": currentUserRole,
"Conversations": conversations,
})
}
func (c *MessageController) ShowSendMessage(ctx *gin.Context) {
currentUserID, currentUsername, currentUserRole, _ := middleware.GetCurrentUser(ctx)
toUserID := parseUint(ctx.Query("to"))
if toUserID == 0 {
ctx.Redirect(http.StatusFound, "/")
return
}
receiver, err := c.userRepo.FindByID(toUserID)
if err != nil {
ctx.String(http.StatusNotFound, "用户不存在")
return
}
ctx.HTML(http.StatusOK, "send_message.html", gin.H{
"IsLoggedIn": true,
"UserID": currentUserID,
"Username": currentUsername,
"UserRole": currentUserRole,
"Receiver": receiver,
"Error": "",
"Success": "",
})
}
func (c *MessageController) SendMessage(ctx *gin.Context) {
currentUserID, _, _, _ := middleware.GetCurrentUser(ctx)
toUserID := parseUint(ctx.Query("to"))
content := strings.TrimSpace(ctx.PostForm("content"))
receiver, _ := c.userRepo.FindByID(toUserID)
if content == "" {
ctx.HTML(http.StatusOK, "send_message.html", gin.H{
"Receiver": receiver,
"Error": "消息内容不能为空",
"Success": "",
})
return
}
c.messageSvc.SendMessage(currentUserID, toUserID, content)
ctx.HTML(http.StatusOK, "send_message.html", gin.H{
"Receiver": receiver,
"Error": "",
"Success": "消息已发送!",
})
}

View File

@@ -0,0 +1,98 @@
package controllers
import (
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"lv8girl/internal/middleware"
"lv8girl/internal/services"
)
type UserController struct {
userSvc *services.UserService
}
func NewUserController() *UserController {
return &UserController{
userSvc: services.NewUserService(),
}
}
func (c *UserController) ShowProfile(ctx *gin.Context) {
currentUserID, currentUsername, currentUserRole, isLoggedIn := middleware.GetCurrentUser(ctx)
userIDStr := ctx.Param("id")
var userID uint
if userIDStr == "" {
userID = currentUserID
} else {
userID = parseUint(userIDStr)
}
if userID == 0 {
ctx.Redirect(http.StatusFound, "/")
return
}
profile, err := c.userSvc.GetUserProfile(currentUserID, userID)
if err != nil {
ctx.String(http.StatusNotFound, "用户不存在")
return
}
avatar := ""
if profile.User.Avatar != "" {
avatar = "/" + profile.User.Avatar
}
ctx.HTML(http.StatusOK, "profile.html", gin.H{
"IsLoggedIn": isLoggedIn,
"CurrentUserID": currentUserID,
"CurrentUsername": currentUsername,
"CurrentUserRole": currentUserRole,
"User": profile.User,
"UserAvatar": avatar,
"Posts": profile.Posts,
"PostCount": profile.PostCount,
"IsOwner": profile.IsOwner,
"UnreadCount": profile.UnreadCount,
"Message": "",
})
}
func (c *UserController) UploadAvatar(ctx *gin.Context) {
userID, _, _, _ := middleware.GetCurrentUser(ctx)
file, err := ctx.FormFile("avatar")
if err != nil {
ctx.Redirect(http.StatusFound, "/profile?error=上传失败")
return
}
if file.Size > 2*1024*1024 {
ctx.Redirect(http.StatusFound, "/profile?error=图片大小不能超过2MB")
return
}
ext := strings.ToLower(filepath.Ext(file.Filename))
filename := "avatar_" + strconv.FormatUint(uint64(userID), 10) + "_" + time.Now().Format("20060102150405") + ext
uploadDir := "uploads/avatars"
if _, err := os.Stat(uploadDir); os.IsNotExist(err) {
os.MkdirAll(uploadDir, 0755)
}
imagePath := filepath.Join(uploadDir, filename)
if err := ctx.SaveUploadedFile(file, imagePath); err != nil {
ctx.Redirect(http.StatusFound, "/profile?error=保存失败")
return
}
c.userSvc.UpdateAvatar(userID, imagePath)
ctx.Redirect(http.StatusFound, "/profile?success=头像更新成功")
}

View File

@@ -0,0 +1,49 @@
package middleware
import (
"net/http"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
func AuthRequired() gin.HandlerFunc {
return func(c *gin.Context) {
session := sessions.Default(c)
userID := session.Get("user_id")
if userID == nil {
c.Redirect(http.StatusFound, "/login")
c.Abort()
return
}
c.Set("user_id", userID)
c.Set("username", session.Get("username"))
c.Set("user_role", session.Get("user_role"))
c.Next()
}
}
func AdminRequired() gin.HandlerFunc {
return func(c *gin.Context) {
session := sessions.Default(c)
userRole := session.Get("user_role")
if userRole != "admin" {
c.Redirect(http.StatusFound, "/")
c.Abort()
return
}
c.Set("user_id", session.Get("user_id"))
c.Set("username", session.Get("username"))
c.Set("user_role", userRole)
c.Next()
}
}
func GetCurrentUser(c *gin.Context) (uint, string, string, bool) {
session := sessions.Default(c)
userID := session.Get("user_id")
if userID == nil {
return 0, "", "", false
}
return userID.(uint), session.Get("username").(string), session.Get("user_role").(string), true
}

84
internal/models/models.go Normal file
View File

@@ -0,0 +1,84 @@
package models
import (
"time"
"gorm.io/gorm"
)
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"size:50;uniqueIndex;not null" json:"username"`
Email string `gorm:"size:100;uniqueIndex;not null" json:"email"`
PasswordHash string `gorm:"size:255;not null" json:"-"`
Avatar string `gorm:"size:255" json:"avatar"`
Role string `gorm:"size:20;default:user" json:"role"`
Status string `gorm:"size:20;default:pending" json:"status"`
LastActive *time.Time `json:"last_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (User) TableName() string {
return "users"
}
type Discussion struct {
ID uint `gorm:"primaryKey" json:"id"`
UserID uint `gorm:"not null;index" json:"user_id"`
User User `gorm:"foreignKey:UserID" json:"user"`
Title string `gorm:"size:200;not null" json:"title"`
Content string `gorm:"type:text;not null" json:"content"`
ImagePath string `gorm:"size:255" json:"image_path"`
Status string `gorm:"size:20;default:pending" json:"status"`
Views int `gorm:"default:0" json:"views"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (Discussion) TableName() string {
return "discussions"
}
type Comment struct {
ID uint `gorm:"primaryKey" json:"id"`
PostID uint `gorm:"not null;index" json:"post_id"`
UserID uint `gorm:"not null;index" json:"user_id"`
User User `gorm:"foreignKey:UserID" json:"user"`
Content string `gorm:"type:text;not null" json:"content"`
CreatedAt time.Time `json:"created_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (Comment) TableName() string {
return "comments"
}
type Like struct {
ID uint `gorm:"primaryKey" json:"id"`
PostID uint `gorm:"not null;uniqueIndex:idx_post_user" json:"post_id"`
UserID uint `gorm:"not null;uniqueIndex:idx_post_user" json:"user_id"`
CreatedAt time.Time `json:"created_at"`
}
func (Like) TableName() string {
return "likes"
}
type PrivateMessage struct {
ID uint `gorm:"primaryKey" json:"id"`
FromUserID uint `gorm:"not null;index" json:"from_user_id"`
FromUser User `gorm:"foreignKey:FromUserID" json:"from_user"`
ToUserID uint `gorm:"not null;index" json:"to_user_id"`
ToUser User `gorm:"foreignKey:ToUserID" json:"to_user"`
Content string `gorm:"type:text;not null" json:"content"`
IsRead bool `gorm:"default:false" json:"is_read"`
CreatedAt time.Time `json:"created_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
func (PrivateMessage) TableName() string {
return "private_messages"
}

View File

@@ -0,0 +1,47 @@
package repositories
import "lv8girl/internal/models"
type CommentRepository struct{}
func NewCommentRepository() *CommentRepository {
return &CommentRepository{}
}
func (r *CommentRepository) FindByPostID(postID uint) ([]models.Comment, error) {
var comments []models.Comment
err := DB.Preload("User").Where("post_id = ?", postID).Order("created_at DESC").Find(&comments).Error
return comments, err
}
func (r *CommentRepository) FindAll() ([]models.Comment, error) {
var comments []models.Comment
err := DB.Preload("User").Order("created_at DESC").Find(&comments).Error
return comments, err
}
func (r *CommentRepository) Create(comment *models.Comment) error {
return DB.Create(comment).Error
}
func (r *CommentRepository) Delete(id uint) error {
return DB.Delete(&models.Comment{}, id).Error
}
func (r *CommentRepository) Count() (int64, error) {
var count int64
err := DB.Model(&models.Comment{}).Count(&count).Error
return count, err
}
func (r *CommentRepository) CountByPostID(postID uint) (int64, error) {
var count int64
err := DB.Model(&models.Comment{}).Where("post_id = ?", postID).Count(&count).Error
return count, err
}
func (r *CommentRepository) CountByUserID(userID uint) (int64, error) {
var count int64
err := DB.Model(&models.Comment{}).Where("user_id = ?", userID).Count(&count).Error
return count, err
}

View File

@@ -0,0 +1,36 @@
package repositories
import (
"time"
"lv8girl/internal/models"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var DB *gorm.DB
func Init(databasePath string) error {
var err error
DB, err = gorm.Open(sqlite.Open(databasePath), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
return err
}
err = DB.AutoMigrate(
&models.User{},
&models.Discussion{},
&models.Comment{},
&models.Like{},
&models.PrivateMessage{},
)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,146 @@
package repositories
import (
"time"
"gorm.io/gorm"
"lv8girl/internal/models"
)
type DiscussionRepository struct{}
func NewDiscussionRepository() *DiscussionRepository {
return &DiscussionRepository{}
}
func (r *DiscussionRepository) FindByID(id uint) (*models.Discussion, error) {
var discussion models.Discussion
err := DB.Preload("User").First(&discussion, id).Error
return &discussion, err
}
func (r *DiscussionRepository) FindByIDWithStats(id uint) (*models.Discussion, int64, int64, error) {
discussion, err := r.FindByID(id)
if err != nil {
return nil, 0, 0, err
}
var likeCount, commentCount int64
DB.Model(&models.Like{}).Where("post_id = ?", id).Count(&likeCount)
DB.Model(&models.Comment{}).Where("post_id = ?", id).Count(&commentCount)
return discussion, likeCount, commentCount, nil
}
func (r *DiscussionRepository) FindApproved(limit int) ([]models.Discussion, error) {
var discussions []models.Discussion
err := DB.Preload("User").
Where("status = ?", "approved").
Order("created_at DESC").
Limit(limit).
Find(&discussions).Error
return discussions, err
}
func (r *DiscussionRepository) FindByUserID(userID uint) ([]models.Discussion, error) {
var discussions []models.Discussion
err := DB.Where("user_id = ? AND status = ?", userID, "approved").
Order("created_at DESC").
Find(&discussions).Error
return discussions, err
}
func (r *DiscussionRepository) FindPending() ([]models.Discussion, error) {
var discussions []models.Discussion
err := DB.Preload("User").
Where("status = ?", "pending").
Order("created_at DESC").
Find(&discussions).Error
return discussions, err
}
func (r *DiscussionRepository) FindAll() ([]models.Discussion, error) {
var discussions []models.Discussion
err := DB.Preload("User").Order("created_at DESC").Find(&discussions).Error
return discussions, err
}
func (r *DiscussionRepository) Create(discussion *models.Discussion) error {
return DB.Create(discussion).Error
}
func (r *DiscussionRepository) Update(discussion *models.Discussion) error {
return DB.Save(discussion).Error
}
func (r *DiscussionRepository) UpdateStatus(id uint, status string) error {
return DB.Model(&models.Discussion{}).Where("id = ?", id).Update("status", status).Error
}
func (r *DiscussionRepository) IncrementViews(id uint) error {
return DB.Model(&models.Discussion{}).Where("id = ?", id).
UpdateColumn("views", gorm.Expr("views + ?", 1)).Error
}
func (r *DiscussionRepository) Delete(id uint) error {
return DB.Delete(&models.Discussion{}, id).Error
}
func (r *DiscussionRepository) Count() (int64, error) {
var count int64
err := DB.Model(&models.Discussion{}).Count(&count).Error
return count, err
}
func (r *DiscussionRepository) CountByStatus(status string) (int64, error) {
var count int64
err := DB.Model(&models.Discussion{}).Where("status = ?", status).Count(&count).Error
return count, err
}
func (r *DiscussionRepository) CountByUserID(userID uint) (int64, error) {
var count int64
err := DB.Model(&models.Discussion{}).Where("user_id = ? AND status = ?", userID, "approved").Count(&count).Error
return count, err
}
type PostListItem struct {
ID uint
Title string
Content string
Username string
Avatar string
UserID uint
LikeCount int64
CommentCount int64
Views int
CreatedAt time.Time
}
func (r *DiscussionRepository) GetPostList(limit int) ([]PostListItem, error) {
discussions, err := r.FindApproved(limit)
if err != nil {
return nil, err
}
var posts []PostListItem
for _, d := range discussions {
var likeCount, commentCount int64
DB.Model(&models.Like{}).Where("post_id = ?", d.ID).Count(&likeCount)
DB.Model(&models.Comment{}).Where("post_id = ?", d.ID).Count(&commentCount)
posts = append(posts, PostListItem{
ID: d.ID,
Title: d.Title,
Content: d.Content,
Username: d.User.Username,
Avatar: d.User.Avatar,
UserID: d.User.ID,
LikeCount: likeCount,
CommentCount: commentCount,
Views: d.Views,
CreatedAt: d.CreatedAt,
})
}
return posts, nil
}

View File

@@ -0,0 +1,37 @@
package repositories
import "lv8girl/internal/models"
type LikeRepository struct{}
func NewLikeRepository() *LikeRepository {
return &LikeRepository{}
}
func (r *LikeRepository) FindByPostAndUser(postID, userID uint) (*models.Like, error) {
var like models.Like
err := DB.Where("post_id = ? AND user_id = ?", postID, userID).First(&like).Error
return &like, err
}
func (r *LikeRepository) Exists(postID, userID uint) bool {
var count int64
DB.Model(&models.Like{}).Where("post_id = ? AND user_id = ?", postID, userID).Count(&count)
return count > 0
}
func (r *LikeRepository) Create(like *models.Like) error {
return DB.Create(like).Error
}
func (r *LikeRepository) Count() (int64, error) {
var count int64
err := DB.Model(&models.Like{}).Count(&count).Error
return count, err
}
func (r *LikeRepository) CountByPostID(postID uint) (int64, error) {
var count int64
err := DB.Model(&models.Like{}).Where("post_id = ?", postID).Count(&count).Error
return count, err
}

View File

@@ -0,0 +1,69 @@
package repositories
import (
"time"
"lv8girl/internal/models"
)
type MessageRepository struct{}
func NewMessageRepository() *MessageRepository {
return &MessageRepository{}
}
func (r *MessageRepository) FindByID(id uint) (*models.PrivateMessage, error) {
var msg models.PrivateMessage
err := DB.First(&msg, id).Error
return &msg, err
}
func (r *MessageRepository) Create(message *models.PrivateMessage) error {
return DB.Create(message).Error
}
func (r *MessageRepository) MarkAsRead(id uint) error {
return DB.Model(&models.PrivateMessage{}).Where("id = ?", id).Update("is_read", true).Error
}
func (r *MessageRepository) CountUnread(userID uint) (int64, error) {
var count int64
err := DB.Model(&models.PrivateMessage{}).
Where("to_user_id = ? AND is_read = ?", userID, false).
Count(&count).Error
return count, err
}
func (r *MessageRepository) FindConversations(userID uint) ([]models.PrivateMessage, error) {
var messages []models.PrivateMessage
err := DB.Where("from_user_id = ? OR to_user_id = ?", userID, userID).
Order("created_at DESC").
Find(&messages).Error
return messages, err
}
func (r *MessageRepository) FindLastMessage(userID, otherUserID uint) (*models.PrivateMessage, error) {
var msg models.PrivateMessage
err := DB.Where(
"(from_user_id = ? AND to_user_id = ?) OR (from_user_id = ? AND to_user_id = ?)",
userID, otherUserID, otherUserID, userID,
).Order("created_at DESC").First(&msg).Error
return &msg, err
}
func (r *MessageRepository) CountUnreadFromUser(fromUserID, toUserID uint) (int64, error) {
var count int64
err := DB.Model(&models.PrivateMessage{}).
Where("from_user_id = ? AND to_user_id = ? AND is_read = ?", fromUserID, toUserID, false).
Count(&count).Error
return count, err
}
type ConversationSummary struct {
UserID uint
Username string
Avatar string
LastMsg string
Time time.Time
Unread int64
}

View File

@@ -0,0 +1,123 @@
package repositories
import (
"time"
"golang.org/x/crypto/bcrypt"
"lv8girl/internal/models"
)
type UserRepository struct{}
func NewUserRepository() *UserRepository {
return &UserRepository{}
}
func (r *UserRepository) FindByID(id uint) (*models.User, error) {
var user models.User
err := DB.First(&user, id).Error
return &user, err
}
func (r *UserRepository) FindByUsernameOrEmail(login string) (*models.User, error) {
var user models.User
err := DB.Where("username = ? OR email = ?", login, login).First(&user).Error
return &user, err
}
func (r *UserRepository) FindByUsername(username string) (*models.User, error) {
var user models.User
err := DB.Where("username = ?", username).First(&user).Error
return &user, err
}
func (r *UserRepository) FindByEmail(email string) (*models.User, error) {
var user models.User
err := DB.Where("email = ?", email).First(&user).Error
return &user, err
}
func (r *UserRepository) ExistsByUsernameOrEmail(username, email string) bool {
var count int64
DB.Model(&models.User{}).Where("username = ? OR email = ?", username, email).Count(&count)
return count > 0
}
func (r *UserRepository) Create(user *models.User) error {
return DB.Create(user).Error
}
func (r *UserRepository) Update(user *models.User) error {
return DB.Save(user).Error
}
func (r *UserRepository) UpdateField(id uint, field string, value interface{}) error {
return DB.Model(&models.User{}).Where("id = ?", id).Update(field, value).Error
}
func (r *UserRepository) UpdateLastActive(id uint) error {
now := time.Now()
return r.UpdateField(id, "last_active", now)
}
func (r *UserRepository) Delete(id uint) error {
return DB.Delete(&models.User{}, id).Error
}
func (r *UserRepository) Count() (int64, error) {
var count int64
err := DB.Model(&models.User{}).Count(&count).Error
return count, err
}
func (r *UserRepository) CountOnline() (int64, error) {
var count int64
fiveMinutesAgo := time.Now().Add(-5 * time.Minute)
err := DB.Model(&models.User{}).Where("last_active > ?", fiveMinutesAgo).Count(&count).Error
return count, err
}
func (r *UserRepository) CountByRole(role string) (int64, error) {
var count int64
err := DB.Model(&models.User{}).Where("role = ?", role).Count(&count).Error
return count, err
}
func (r *UserRepository) FindPending() ([]models.User, error) {
var users []models.User
err := DB.Where("status = ?", "pending").Order("created_at ASC").Find(&users).Error
return users, err
}
func (r *UserRepository) FindAll() ([]models.User, error) {
var users []models.User
err := DB.Order("id").Find(&users).Error
return users, err
}
func (r *UserRepository) HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
return string(bytes), err
}
func (r *UserRepository) CheckPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
func (r *UserRepository) CreateAdminIfNotExists() error {
var count int64
DB.Model(&models.User{}).Where("role = ?", "admin").Count(&count)
if count == 0 {
passwordHash, _ := r.HashPassword("admin123")
admin := models.User{
Username: "admin",
Email: "admin@lv8girl.local",
PasswordHash: passwordHash,
Role: "admin",
Status: "approved",
}
return DB.Create(&admin).Error
}
return nil
}

72
internal/routes/routes.go Normal file
View File

@@ -0,0 +1,72 @@
package routes
import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"lv8girl/internal/config"
"lv8girl/internal/controllers"
"lv8girl/internal/middleware"
)
func SetupRouter() *gin.Engine {
r := gin.Default()
cfg := config.GetConfig()
store := cookie.NewStore([]byte(cfg.Session.Secret))
r.Use(sessions.Sessions(cfg.Session.Name, store))
r.Static("/static", "./static")
r.Static("/uploads", "./uploads")
r.LoadHTMLGlob("templates/*")
authCtrl := controllers.NewAuthController()
homeCtrl := controllers.NewHomeController()
discussionCtrl := controllers.NewDiscussionController()
userCtrl := controllers.NewUserController()
messageCtrl := controllers.NewMessageController()
adminCtrl := controllers.NewAdminController()
r.GET("/", homeCtrl.Index)
r.GET("/login", authCtrl.ShowLogin)
r.POST("/login", authCtrl.Login)
r.GET("/register", authCtrl.ShowRegister)
r.POST("/register", authCtrl.Register)
r.GET("/logout", authCtrl.Logout)
r.GET("/post/:id", homeCtrl.ShowPost)
r.POST("/post/:id/like", middleware.AuthRequired(), homeCtrl.LikePost)
r.POST("/post/:id/comment", middleware.AuthRequired(), homeCtrl.AddComment)
r.GET("/new-post", middleware.AuthRequired(), discussionCtrl.ShowNewPost)
r.POST("/new-post", middleware.AuthRequired(), discussionCtrl.CreatePost)
r.GET("/profile", middleware.AuthRequired(), userCtrl.ShowProfile)
r.GET("/profile/:id", userCtrl.ShowProfile)
r.POST("/upload-avatar", middleware.AuthRequired(), userCtrl.UploadAvatar)
r.GET("/messages", middleware.AuthRequired(), messageCtrl.ShowMessages)
r.GET("/send-message", middleware.AuthRequired(), messageCtrl.ShowSendMessage)
r.POST("/send-message", middleware.AuthRequired(), messageCtrl.SendMessage)
admin := r.Group("/admin")
admin.Use(middleware.AdminRequired())
{
admin.GET("", adminCtrl.Dashboard)
admin.GET("/", adminCtrl.Dashboard)
admin.GET("/pending_posts", adminCtrl.PendingPosts)
admin.GET("/pending_posts/:action/:id", adminCtrl.ApprovePost)
admin.GET("/pending_users", adminCtrl.PendingUsers)
admin.GET("/pending_users/:action/:id", adminCtrl.ApproveUser)
admin.GET("/posts", adminCtrl.Posts)
admin.GET("/posts/delete/:id", adminCtrl.DeletePost)
admin.GET("/users", adminCtrl.Users)
admin.POST("/users/role", adminCtrl.UpdateUserRole)
admin.GET("/users/delete/:id", adminCtrl.DeleteUser)
admin.GET("/comments", adminCtrl.Comments)
admin.GET("/comments/delete/:id", adminCtrl.DeleteComment)
}
return r
}

View File

@@ -0,0 +1,98 @@
package services
import (
"lv8girl/internal/models"
"lv8girl/internal/repositories"
)
type AdminService struct {
userRepo *repositories.UserRepository
discussionRepo *repositories.DiscussionRepository
commentRepo *repositories.CommentRepository
likeRepo *repositories.LikeRepository
}
func NewAdminService() *AdminService {
return &AdminService{
userRepo: repositories.NewUserRepository(),
discussionRepo: repositories.NewDiscussionRepository(),
commentRepo: repositories.NewCommentRepository(),
likeRepo: repositories.NewLikeRepository(),
}
}
type Stats struct {
Posts int64
Users int64
Comments int64
Likes int64
Online int64
Approved int64
Rejected int64
Pending int64
}
func (s *AdminService) GetStats() (*Stats, error) {
stats := &Stats{}
stats.Posts, _ = s.discussionRepo.Count()
stats.Users, _ = s.userRepo.Count()
stats.Comments, _ = s.commentRepo.Count()
stats.Likes, _ = s.likeRepo.Count()
stats.Online, _ = s.userRepo.CountOnline()
stats.Approved, _ = s.discussionRepo.CountByStatus("approved")
stats.Rejected, _ = s.discussionRepo.CountByStatus("rejected")
stats.Pending, _ = s.discussionRepo.CountByStatus("pending")
return stats, nil
}
func (s *AdminService) GetPendingPosts() ([]models.Discussion, error) {
return s.discussionRepo.FindPending()
}
func (s *AdminService) GetPendingUsers() ([]models.User, error) {
return s.userRepo.FindPending()
}
func (s *AdminService) ApprovePost(id uint) error {
return s.discussionRepo.UpdateStatus(id, "approved")
}
func (s *AdminService) RejectPost(id uint) error {
return s.discussionRepo.UpdateStatus(id, "rejected")
}
func (s *AdminService) DeletePost(id uint) error {
return s.discussionRepo.Delete(id)
}
func (s *AdminService) ApproveUser(id uint) error {
return s.userRepo.UpdateField(id, "status", "approved")
}
func (s *AdminService) RejectUser(id uint) error {
return s.userRepo.UpdateField(id, "status", "rejected")
}
func (s *AdminService) UpdateUserRole(id uint, role string) error {
return s.userRepo.UpdateField(id, "role", role)
}
func (s *AdminService) DeleteUser(id uint) error {
return s.userRepo.Delete(id)
}
func (s *AdminService) GetAllPosts() ([]models.Discussion, error) {
return s.discussionRepo.FindAll()
}
func (s *AdminService) GetAllUsers() ([]models.User, error) {
return s.userRepo.FindAll()
}
func (s *AdminService) GetAllComments() ([]models.Comment, error) {
return s.commentRepo.FindAll()
}
func (s *AdminService) DeleteComment(id uint) error {
return s.commentRepo.Delete(id)
}

87
internal/services/auth.go Normal file
View File

@@ -0,0 +1,87 @@
package services
import (
"lv8girl/internal/models"
"lv8girl/internal/repositories"
)
type AuthService struct {
userRepo *repositories.UserRepository
}
func NewAuthService() *AuthService {
return &AuthService{
userRepo: repositories.NewUserRepository(),
}
}
type LoginResult struct {
Success bool
User *models.User
Error string
}
func (s *AuthService) Login(login, password string) LoginResult {
user, err := s.userRepo.FindByUsernameOrEmail(login)
if err != nil {
return LoginResult{Success: false, Error: "用户名/邮箱或密码错误"}
}
if !s.userRepo.CheckPassword(password, user.PasswordHash) {
return LoginResult{Success: false, Error: "用户名/邮箱或密码错误"}
}
switch user.Status {
case "pending":
return LoginResult{Success: false, Error: "您的账号正在等待管理员审核,请耐心等待。"}
case "rejected":
return LoginResult{Success: false, Error: "您的账号审核未通过,无法登录。如有疑问,请联系管理员。"}
case "approved":
if user.Role == "banned" {
return LoginResult{Success: false, Error: "您的账号已被封禁,请联系管理员"}
}
default:
return LoginResult{Success: false, Error: "账号状态异常,请联系管理员"}
}
s.userRepo.UpdateLastActive(user.ID)
return LoginResult{Success: true, User: user}
}
type RegisterResult struct {
Success bool
Error string
}
func (s *AuthService) Register(username, email, password string) RegisterResult {
if len(username) < 3 || len(username) > 20 {
return RegisterResult{Success: false, Error: "用户名长度必须在3-20个字符之间"}
}
if s.userRepo.ExistsByUsernameOrEmail(username, email) {
return RegisterResult{Success: false, Error: "用户名或邮箱已被注册"}
}
passwordHash, err := s.userRepo.HashPassword(password)
if err != nil {
return RegisterResult{Success: false, Error: "注册失败,请稍后重试"}
}
user := &models.User{
Username: username,
Email: email,
PasswordHash: passwordHash,
Role: "user",
Status: "pending",
}
if err := s.userRepo.Create(user); err != nil {
return RegisterResult{Success: false, Error: "注册失败,请稍后重试"}
}
return RegisterResult{Success: true}
}
func (s *AuthService) InitAdmin() error {
return s.userRepo.CreateAdminIfNotExists()
}

View File

@@ -0,0 +1,92 @@
package services
import (
"lv8girl/internal/models"
"lv8girl/internal/repositories"
)
type DiscussionService struct {
discussionRepo *repositories.DiscussionRepository
likeRepo *repositories.LikeRepository
commentRepo *repositories.CommentRepository
}
func NewDiscussionService() *DiscussionService {
return &DiscussionService{
discussionRepo: repositories.NewDiscussionRepository(),
likeRepo: repositories.NewLikeRepository(),
commentRepo: repositories.NewCommentRepository(),
}
}
type PostDetailView struct {
Post *models.Discussion
LikeCount int64
CommentCount int64
UserLiked bool
AuthorPostCount int64
}
func (s *DiscussionService) GetPostDetail(postID, userID uint) (*PostDetailView, error) {
post, err := s.discussionRepo.FindByID(postID)
if err != nil {
return nil, err
}
likeCount, _ := s.likeRepo.CountByPostID(postID)
commentCount, _ := s.commentRepo.CountByPostID(postID)
userLiked := s.likeRepo.Exists(postID, userID)
authorPostCount, _ := s.discussionRepo.CountByUserID(post.UserID)
return &PostDetailView{
Post: post,
LikeCount: likeCount,
CommentCount: commentCount,
UserLiked: userLiked,
AuthorPostCount: authorPostCount,
}, nil
}
func (s *DiscussionService) IncrementViews(postID uint) error {
return s.discussionRepo.IncrementViews(postID)
}
func (s *DiscussionService) AddLike(postID, userID uint) error {
if s.likeRepo.Exists(postID, userID) {
return nil
}
like := &models.Like{
PostID: postID,
UserID: userID,
}
return s.likeRepo.Create(like)
}
func (s *DiscussionService) AddComment(postID, userID uint, content string) error {
comment := &models.Comment{
PostID: postID,
UserID: userID,
Content: content,
}
return s.commentRepo.Create(comment)
}
func (s *DiscussionService) GetComments(postID uint) ([]models.Comment, error) {
return s.commentRepo.FindByPostID(postID)
}
func (s *DiscussionService) CreatePost(userID uint, title, content, imagePath string) error {
post := &models.Discussion{
UserID: userID,
Title: title,
Content: content,
ImagePath: imagePath,
Status: "pending",
}
return s.discussionRepo.Create(post)
}
func (s *DiscussionService) GetApprovedPosts(limit int) ([]repositories.PostListItem, error) {
return s.discussionRepo.GetPostList(limit)
}

View File

@@ -0,0 +1,97 @@
package services
import (
"lv8girl/internal/models"
"lv8girl/internal/repositories"
)
type MessageService struct {
messageRepo *repositories.MessageRepository
userRepo *repositories.UserRepository
}
func NewMessageService() *MessageService {
return &MessageService{
messageRepo: repositories.NewMessageRepository(),
userRepo: repositories.NewUserRepository(),
}
}
func (s *MessageService) SendMessage(fromUserID, toUserID uint, content string) error {
message := &models.PrivateMessage{
FromUserID: fromUserID,
ToUserID: toUserID,
Content: content,
IsRead: false,
}
return s.messageRepo.Create(message)
}
func (s *MessageService) GetUnreadCount(userID uint) (int64, error) {
return s.messageRepo.CountUnread(userID)
}
type ConversationView struct {
UserID uint
Username string
Avatar string
LastMsg string
Time string
Unread int64
}
func (s *MessageService) GetConversations(userID uint) ([]ConversationView, error) {
messages, err := s.messageRepo.FindConversations(userID)
if err != nil {
return nil, err
}
userMap := make(map[uint]bool)
for _, msg := range messages {
if msg.FromUserID != userID {
userMap[msg.FromUserID] = true
}
if msg.ToUserID != userID {
userMap[msg.ToUserID] = true
}
}
var conversations []ConversationView
for otherID := range userMap {
otherUser, err := s.userRepo.FindByID(otherID)
if err != nil {
continue
}
lastMsg, _ := s.messageRepo.FindLastMessage(userID, otherID)
unread, _ := s.messageRepo.CountUnreadFromUser(otherID, userID)
avatar := ""
if otherUser.Avatar != "" {
avatar = "/" + otherUser.Avatar
}
conversations = append(conversations, ConversationView{
UserID: otherUser.ID,
Username: otherUser.Username,
Avatar: avatar,
LastMsg: lastMsg.Content,
Time: lastMsg.CreatedAt.Format("2006-01-02 15:04"),
Unread: unread,
})
}
return conversations, nil
}
func (s *MessageService) NotifyUserApproved(adminID, userID uint) error {
return s.SendMessage(adminID, userID, "恭喜!您的账号已通过管理员审核,现在可以正常登录使用了。")
}
func (s *MessageService) NotifyUserRejected(adminID, userID uint) error {
return s.SendMessage(adminID, userID, "您的账号审核未通过。如有疑问,请联系管理员。")
}
func (s *MessageService) NotifyUserBanned(adminID, userID uint) error {
return s.SendMessage(adminID, userID, "您的账号已被管理员封禁。如有疑问,请联系管理员。")
}

94
internal/services/user.go Normal file
View File

@@ -0,0 +1,94 @@
package services
import (
"lv8girl/internal/models"
"lv8girl/internal/repositories"
)
type UserService struct {
userRepo *repositories.UserRepository
discussionRepo *repositories.DiscussionRepository
commentRepo *repositories.CommentRepository
messageRepo *repositories.MessageRepository
}
func NewUserService() *UserService {
return &UserService{
userRepo: repositories.NewUserRepository(),
discussionRepo: repositories.NewDiscussionRepository(),
commentRepo: repositories.NewCommentRepository(),
messageRepo: repositories.NewMessageRepository(),
}
}
type UserProfileView struct {
User *models.User
Posts []models.Discussion
PostCount int64
UnreadCount int64
IsOwner bool
}
func (s *UserService) GetUserProfile(viewerID, targetUserID uint) (*UserProfileView, error) {
user, err := s.userRepo.FindByID(targetUserID)
if err != nil {
return nil, err
}
posts, _ := s.discussionRepo.FindByUserID(targetUserID)
postCount, _ := s.discussionRepo.CountByUserID(targetUserID)
var unreadCount int64
if viewerID != 0 {
unreadCount, _ = s.messageRepo.CountUnread(viewerID)
}
return &UserProfileView{
User: user,
Posts: posts,
PostCount: postCount,
UnreadCount: unreadCount,
IsOwner: viewerID == targetUserID,
}, nil
}
func (s *UserService) UpdateAvatar(userID uint, avatarPath string) error {
user, err := s.userRepo.FindByID(userID)
if err != nil {
return err
}
return s.userRepo.UpdateField(user.ID, "avatar", avatarPath)
}
func (s *UserService) GetUserByID(id uint) (*models.User, error) {
return s.userRepo.FindByID(id)
}
func (s *UserService) UpdateUserStatus(id uint, status string) error {
return s.userRepo.UpdateField(id, "status", status)
}
func (s *UserService) UpdateUserRole(id uint, role string) error {
return s.userRepo.UpdateField(id, "role", role)
}
func (s *UserService) DeleteUser(id uint) error {
return s.userRepo.Delete(id)
}
func (s *UserService) GetPendingUsers() ([]models.User, error) {
return s.userRepo.FindPending()
}
func (s *UserService) GetAllUsers() ([]models.User, error) {
return s.userRepo.FindAll()
}
func (s *UserService) GetUserStats() (int64, int64, error) {
total, err := s.userRepo.Count()
if err != nil {
return 0, 0, err
}
online, err := s.userRepo.CountOnline()
return total, online, err
}

9
internal/utils/utils.go Normal file
View File

@@ -0,0 +1,9 @@
package utils
func Substring(s string, length int) string {
runes := []rune(s)
if len(runes) <= length {
return s
}
return string(runes[:length])
}