feat(core): 优化路径处理和配置端口并添加项目规范文档
- 将所有文件路径转换为 URL 友好格式,统一使用正斜杠,确保跨平台兼容 - 修改默认服务器监听端口为 :8989,提升默认配置适用性 - 数据库初始化时创建数据库文件夹,避免路径不存在导致错误 - 新增 AGENTS.md 文档,详细规范项目开发流程、代码风格、架构设计和安全性能指导 - 修正头像路径显示逻辑,确保头像 URL 正确展示 - 增加应用启动和开发的标准操作指南,提升团队协作效率
This commit is contained in:
285
AGENTS.md
Normal file
285
AGENTS.md
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
# AGENTS.md - lv8girl Project Guidelines
|
||||||
|
|
||||||
|
This document provides guidelines for AI agents working on the lv8girl project - a Go-based forum system.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
**lv8girl** is a Go + Gin + SQLite forum system with user registration, posts, comments, likes, private messages, and admin dashboard.
|
||||||
|
|
||||||
|
## Build & Development Commands
|
||||||
|
|
||||||
|
### Basic Commands
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
go run ./cmd/server
|
||||||
|
|
||||||
|
# Build binary
|
||||||
|
go build -o lv8girl ./cmd/server
|
||||||
|
|
||||||
|
# Run tests (if any)
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
# Run specific test
|
||||||
|
go test ./internal/services -v
|
||||||
|
|
||||||
|
# Test coverage
|
||||||
|
go test ./... -cover
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database Operations
|
||||||
|
```bash
|
||||||
|
# Database is automatically created at: data/lv8girl.db
|
||||||
|
# Auto-migration runs on application start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
```bash
|
||||||
|
# Format code
|
||||||
|
go fmt ./...
|
||||||
|
|
||||||
|
# Vet code for suspicious constructs
|
||||||
|
go vet ./...
|
||||||
|
|
||||||
|
# Static analysis
|
||||||
|
go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||||
|
staticcheck ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Style Guidelines
|
||||||
|
|
||||||
|
### General Principles
|
||||||
|
- Follow **Go standard conventions** and **effective Go guidelines**
|
||||||
|
- Use **layered architecture**: Controllers → Services → Repositories → Models
|
||||||
|
- Keep functions small and focused (single responsibility)
|
||||||
|
- Prefer composition over inheritance
|
||||||
|
- Handle errors properly - never ignore them
|
||||||
|
|
||||||
|
### Imports Organization
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
// Standard library
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
// Third-party packages
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
// Local packages (project-specific)
|
||||||
|
"lv8girl/internal/models"
|
||||||
|
"lv8girl/internal/services"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
- **Packages**: lowercase, single word, descriptive (e.g., `controllers`, `services`)
|
||||||
|
- **Interfaces**: `-er` suffix (e.g., `Repository`, `Handler`)
|
||||||
|
- **Variables**: camelCase, descriptive names
|
||||||
|
- **Constants**: PascalCase or ALL_CAPS for exported constants
|
||||||
|
- **Methods**: camelCase, verbs for actions (e.g., `GetUserByID`, `CreatePost`)
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
```go
|
||||||
|
// Always check errors
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("context: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use custom error types for domain errors
|
||||||
|
type ValidationError struct {
|
||||||
|
Field string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ValidationError) Error() string {
|
||||||
|
return fmt.Sprintf("%s: %s", e.Field, e.Message)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Types and Structs
|
||||||
|
```go
|
||||||
|
// Use JSON tags for API responses
|
||||||
|
type User struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use GORM tags for database models
|
||||||
|
type Discussion struct {
|
||||||
|
ID uint `gorm:"primaryKey" json:"id"`
|
||||||
|
Title string `gorm:"size:200;not null" json:"title"`
|
||||||
|
Content string `gorm:"type:text;not null" json:"content"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Function Signatures
|
||||||
|
```go
|
||||||
|
// Return errors as last parameter
|
||||||
|
func GetUserByID(id uint) (*models.User, error)
|
||||||
|
|
||||||
|
// Use context for cancellation and timeouts
|
||||||
|
func GetUser(ctx context.Context, id uint) (*models.User, error)
|
||||||
|
|
||||||
|
// Receiver names should be short (1-2 letters)
|
||||||
|
func (c *Controller) HandleRequest(ctx *gin.Context)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Project Structure Patterns
|
||||||
|
```
|
||||||
|
cmd/server/main.go # Application entry point
|
||||||
|
internal/config/ # Configuration management
|
||||||
|
internal/controllers/ # HTTP request handlers (Gin)
|
||||||
|
internal/services/ # Business logic layer
|
||||||
|
internal/repositories/ # Data access layer (GORM)
|
||||||
|
internal/models/ # Data models
|
||||||
|
internal/middleware/ # HTTP middleware
|
||||||
|
internal/routes/ # Route definitions
|
||||||
|
internal/utils/ # Utility functions
|
||||||
|
```
|
||||||
|
|
||||||
|
### Controller Patterns
|
||||||
|
```go
|
||||||
|
// Use dependency injection
|
||||||
|
type AuthController struct {
|
||||||
|
authService *services.AuthService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthController() *AuthController {
|
||||||
|
return &AuthController{
|
||||||
|
authService: services.NewAuthService(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle HTTP requests with Gin
|
||||||
|
func (c *AuthController) Login(ctx *gin.Context) {
|
||||||
|
// Parse input
|
||||||
|
// Call service
|
||||||
|
// Return response
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Layer Patterns
|
||||||
|
```go
|
||||||
|
// Services handle business logic
|
||||||
|
type AuthService struct {
|
||||||
|
userRepo *repositories.UserRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AuthService) Login(login, password string) LoginResult {
|
||||||
|
// Business logic
|
||||||
|
// Call repositories
|
||||||
|
// Return result
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Repository Patterns
|
||||||
|
```go
|
||||||
|
// Repositories handle database operations
|
||||||
|
type UserRepository struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *UserRepository) FindByID(id uint) (*models.User, error) {
|
||||||
|
var user models.User
|
||||||
|
result := r.db.First(&user, id)
|
||||||
|
return &user, result.Error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing Guidelines
|
||||||
|
```go
|
||||||
|
// When adding tests, follow these patterns:
|
||||||
|
func TestUserService_CreateUser(t *testing.T) {
|
||||||
|
// Arrange
|
||||||
|
// Act
|
||||||
|
// Assert
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use table-driven tests
|
||||||
|
func TestValidateUser(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
user models.User
|
||||||
|
expected error
|
||||||
|
}{
|
||||||
|
// test cases
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security Considerations
|
||||||
|
- Never commit secrets (session keys, passwords)
|
||||||
|
- Use environment variables for sensitive configuration
|
||||||
|
- Validate all user input
|
||||||
|
- Use prepared statements (GORM handles this)
|
||||||
|
- Hash passwords with bcrypt/scrypt
|
||||||
|
|
||||||
|
### Performance Guidelines
|
||||||
|
- Use connection pooling (GORM handles this)
|
||||||
|
- Implement pagination for lists
|
||||||
|
- Cache frequently accessed data
|
||||||
|
- Use indexes on frequently queried columns
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- Document exported functions and types
|
||||||
|
- Use clear, concise comments for complex logic
|
||||||
|
- Update README.md for new features
|
||||||
|
- Keep API documentation in sync with code
|
||||||
|
|
||||||
|
## Common Tasks
|
||||||
|
|
||||||
|
### Adding a New Feature
|
||||||
|
1. Add model in `internal/models/`
|
||||||
|
2. Add repository methods in `internal/repositories/`
|
||||||
|
3. Add service logic in `internal/services/`
|
||||||
|
4. Add controller in `internal/controllers/`
|
||||||
|
5. Add routes in `internal/routes/routes.go`
|
||||||
|
6. Add templates in `templates/` (if needed)
|
||||||
|
|
||||||
|
### Database Migrations
|
||||||
|
- Models are auto-migrated on startup
|
||||||
|
- For manual migrations, create migration files
|
||||||
|
- Test migrations before applying to production
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
- Use RESTful conventions where appropriate
|
||||||
|
- Return appropriate HTTP status codes
|
||||||
|
- Use JSON for API responses
|
||||||
|
- Document API changes
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
1. **Before starting**: Run `go mod tidy` to ensure dependencies are current
|
||||||
|
2. **During development**: Use `go run ./cmd/server` for testing
|
||||||
|
3. **Before committing**: Run `go fmt ./...` and `go vet ./...`
|
||||||
|
4. **Testing**: Write tests for new functionality
|
||||||
|
5. **Documentation**: Update relevant documentation
|
||||||
|
|
||||||
|
## Project-Specific Notes
|
||||||
|
|
||||||
|
- Default admin credentials: `admin` / `admin123`
|
||||||
|
- Session secret is hardcoded in config - change for production
|
||||||
|
- Database path: `data/lv8girl.db`
|
||||||
|
- Server runs on port `:8989` by default
|
||||||
|
- Uses Gin web framework with HTML templates
|
||||||
|
- Uses GORM with SQLite (no CGO required)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- **Database issues**: Check `data/` directory permissions
|
||||||
|
- **Import errors**: Run `go mod tidy`
|
||||||
|
- **Build errors**: Ensure Go 1.21+ is installed
|
||||||
|
- **Runtime errors**: Check logs for detailed error messages
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Go Documentation](https://golang.org/doc/)
|
||||||
|
- [Gin Framework](https://gin-gonic.com/docs/)
|
||||||
|
- [GORM Documentation](https://gorm.io/docs/)
|
||||||
|
- [Effective Go](https://golang.org/doc/effective_go)
|
||||||
@@ -31,7 +31,7 @@ type AppConfig struct {
|
|||||||
|
|
||||||
var AppConfigInstance = &Config{
|
var AppConfigInstance = &Config{
|
||||||
Server: ServerConfig{
|
Server: ServerConfig{
|
||||||
Port: ":8080",
|
Port: ":8989",
|
||||||
ReadTimeout: 10 * time.Second,
|
ReadTimeout: 10 * time.Second,
|
||||||
WriteTimeout: 10 * time.Second,
|
WriteTimeout: 10 * time.Second,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ func (c *DiscussionController) CreatePost(ctx *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 将路径转换为 URL 友好的格式
|
||||||
|
imagePath = filepath.ToSlash(imagePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.discussionSvc.CreatePost(userID, title, content, imagePath); err != nil {
|
if err := c.discussionSvc.CreatePost(userID, title, content, imagePath); err != nil {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -40,6 +41,14 @@ func (c *HomeController) Index(ctx *gin.Context) {
|
|||||||
|
|
||||||
posts, _ := c.discussionSvc.GetApprovedPosts(30)
|
posts, _ := c.discussionSvc.GetApprovedPosts(30)
|
||||||
|
|
||||||
|
// 处理头像路径,确保使用正斜杠并添加前导斜杠
|
||||||
|
for i := range posts {
|
||||||
|
if posts[i].Avatar != "" {
|
||||||
|
avatarPath := filepath.ToSlash(posts[i].Avatar)
|
||||||
|
posts[i].Avatar = "/" + avatarPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var postCount, userCount, onlineCount int64
|
var postCount, userCount, onlineCount int64
|
||||||
postCount, _ = c.discussionRepo.CountByStatus("approved")
|
postCount, _ = c.discussionRepo.CountByStatus("approved")
|
||||||
userCount, onlineCount, _ = c.userSvc.GetUserStats()
|
userCount, onlineCount, _ = c.userSvc.GetUserStats()
|
||||||
@@ -93,7 +102,9 @@ func (c *HomeController) ShowPost(ctx *gin.Context) {
|
|||||||
|
|
||||||
avatar := ""
|
avatar := ""
|
||||||
if detail.Post.User.Avatar != "" {
|
if detail.Post.User.Avatar != "" {
|
||||||
avatar = "/" + detail.Post.User.Avatar
|
// 确保路径使用正斜杠,并添加前导斜杠
|
||||||
|
avatarPath := filepath.ToSlash(detail.Post.User.Avatar)
|
||||||
|
avatar = "/" + avatarPath
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, "post.html", gin.H{
|
ctx.HTML(http.StatusOK, "post.html", gin.H{
|
||||||
|
|||||||
@@ -47,7 +47,9 @@ func (c *UserController) ShowProfile(ctx *gin.Context) {
|
|||||||
|
|
||||||
avatar := ""
|
avatar := ""
|
||||||
if profile.User.Avatar != "" {
|
if profile.User.Avatar != "" {
|
||||||
avatar = "/" + profile.User.Avatar
|
// 确保路径使用正斜杠,并添加前导斜杠
|
||||||
|
avatarPath := filepath.ToSlash(profile.User.Avatar)
|
||||||
|
avatar = "/" + avatarPath
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, "profile.html", gin.H{
|
ctx.HTML(http.StatusOK, "profile.html", gin.H{
|
||||||
@@ -93,6 +95,8 @@ func (c *UserController) UploadAvatar(ctx *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.userSvc.UpdateAvatar(userID, imagePath)
|
// 将路径转换为 URL 友好的格式(使用正斜杠)
|
||||||
|
avatarPath := filepath.ToSlash(imagePath)
|
||||||
|
c.userSvc.UpdateAvatar(userID, avatarPath)
|
||||||
ctx.Redirect(http.StatusFound, "/profile?success=头像更新成功")
|
ctx.Redirect(http.StatusFound, "/profile?success=头像更新成功")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
package repositories
|
package repositories
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"lv8girl/internal/models"
|
"lv8girl/internal/models"
|
||||||
|
|
||||||
@@ -13,6 +14,12 @@ import (
|
|||||||
var DB *gorm.DB
|
var DB *gorm.DB
|
||||||
|
|
||||||
func Init(databasePath string) error {
|
func Init(databasePath string) error {
|
||||||
|
// 确保数据库文件所在目录存在
|
||||||
|
dir := filepath.Dir(databasePath)
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
DB, err = gorm.Open(sqlite.Open(databasePath), &gorm.Config{
|
DB, err = gorm.Open(sqlite.Open(databasePath), &gorm.Config{
|
||||||
Logger: logger.Default.LogMode(logger.Info),
|
Logger: logger.Default.LogMode(logger.Info),
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"lv8girl/internal/models"
|
"lv8girl/internal/models"
|
||||||
"lv8girl/internal/repositories"
|
"lv8girl/internal/repositories"
|
||||||
)
|
)
|
||||||
@@ -68,7 +70,9 @@ func (s *MessageService) GetConversations(userID uint) ([]ConversationView, erro
|
|||||||
|
|
||||||
avatar := ""
|
avatar := ""
|
||||||
if otherUser.Avatar != "" {
|
if otherUser.Avatar != "" {
|
||||||
avatar = "/" + otherUser.Avatar
|
// 确保路径使用正斜杠,并添加前导斜杠
|
||||||
|
avatarPath := filepath.ToSlash(otherUser.Avatar)
|
||||||
|
avatar = "/" + avatarPath
|
||||||
}
|
}
|
||||||
|
|
||||||
conversations = append(conversations, ConversationView{
|
conversations = append(conversations, ConversationView{
|
||||||
|
|||||||
Reference in New Issue
Block a user