代码推送
This commit is contained in:
@@ -9,26 +9,27 @@ import (
|
||||
|
||||
// Config 全局配置根结构
|
||||
type Config struct {
|
||||
Server ServerConfig `yaml:"server"`
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
JWT JWTConfig `yaml:"jwt"`
|
||||
Upload UploadConfig `yaml:"upload"`
|
||||
Storage StorageConfig `yaml:"storage"`
|
||||
SMS SMSConfig `yaml:"sms"`
|
||||
Wechat WechatConfig `yaml:"wechat"`
|
||||
AI AIConfig `yaml:"ai"`
|
||||
CORS CORSConfig `yaml:"cors"`
|
||||
RateLimit RateLimitConfig `yaml:"rate_limit"`
|
||||
Env EnvConfig `yaml:"env"`
|
||||
Server ServerConfig `yaml:"server"`
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
JWT JWTConfig `yaml:"jwt"`
|
||||
Upload UploadConfig `yaml:"upload"`
|
||||
Storage StorageConfig `yaml:"storage"`
|
||||
SMS SMSConfig `yaml:"sms"`
|
||||
Wechat WechatConfig `yaml:"wechat"`
|
||||
WechatWebhook WechatWebhookConfig `yaml:"wechat_webhook"`
|
||||
AI AIConfig `yaml:"ai"`
|
||||
CORS CORSConfig `yaml:"cors"`
|
||||
RateLimit RateLimitConfig `yaml:"rate_limit"`
|
||||
Env EnvConfig `yaml:"env"`
|
||||
}
|
||||
|
||||
// ServerConfig 服务配置
|
||||
type ServerConfig struct {
|
||||
Port int `yaml:"port"`
|
||||
ReadTimeout int `yaml:"read_timeout"` // 读超时(秒)
|
||||
WriteTimeout int `yaml:"write_timeout"` // 写超时(秒)
|
||||
IdleTimeout int `yaml:"idle_timeout"` // 空闲超时(秒)
|
||||
ShutdownTimeout int `yaml:"shutdown_timeout"` // 优雅关机等待(秒)
|
||||
Port int `yaml:"port"`
|
||||
ReadTimeout int `yaml:"read_timeout"` // 读超时(秒)
|
||||
WriteTimeout int `yaml:"write_timeout"` // 写超时(秒)
|
||||
IdleTimeout int `yaml:"idle_timeout"` // 空闲超时(秒)
|
||||
ShutdownTimeout int `yaml:"shutdown_timeout"` // 优雅关机等待(秒)
|
||||
}
|
||||
|
||||
// DatabaseConfig 数据库配置
|
||||
@@ -152,6 +153,12 @@ type WechatConfig struct {
|
||||
NotifyURL string `yaml:"notify_url"`
|
||||
}
|
||||
|
||||
// WechatWebhookConfig 企业微信群机器人Webhook配置
|
||||
type WechatWebhookConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
URL string `yaml:"url"`
|
||||
}
|
||||
|
||||
// AIConfig AI 配置
|
||||
type AIConfig struct {
|
||||
APIKey string `yaml:"api_key"`
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"gotest/internal/model"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
@@ -61,7 +60,7 @@ func Init(cfg *config.DatabaseConfig) (*DB, error) {
|
||||
// auto_migrate 自动建表/更新表结构
|
||||
if cfg.AutoMigrate {
|
||||
if err := db.AutoMigrate(
|
||||
&model.User{}, &model.Config{},
|
||||
&model.User{}, &model.Config{}, &model.AccessLog{},
|
||||
&model.Company{}, &model.EnterpriseNews{}, &model.EnterpriseService{}, &model.TeamMember{},
|
||||
&model.ProductCategory{}, &model.Product{},
|
||||
&model.RestaurantCategory{}, &model.Restaurant{}, &model.MenuCategory{}, &model.MenuItem{},
|
||||
@@ -102,9 +101,9 @@ func Init(cfg *config.DatabaseConfig) (*DB, error) {
|
||||
return &DB{db}, nil
|
||||
}
|
||||
|
||||
// seedData 初始化基础数据,仅在用户表为空时执行
|
||||
// seedData 初始化基础数据
|
||||
func seedData(db *gorm.DB) error {
|
||||
// 检查是否已有用户,避免重复初始化
|
||||
// 检查是否已有用户
|
||||
var count int64
|
||||
if err := db.Model(&model.User{}).Count(&count).Error; err != nil {
|
||||
return err
|
||||
@@ -114,24 +113,7 @@ func seedData(db *gorm.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建默认管理员账号 admin / admin123
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("生成管理员密码哈希失败: %w", err)
|
||||
}
|
||||
|
||||
admin := model.User{
|
||||
Username: "admin",
|
||||
Password: string(hashedPassword),
|
||||
Role: "admin",
|
||||
Status: 1,
|
||||
}
|
||||
|
||||
if err := db.Create(&admin).Error; err != nil {
|
||||
return fmt.Errorf("创建默认管理员失败: %w", err)
|
||||
}
|
||||
log.Println("默认管理员账号已创建: admin / admin123(请及时修改密码)")
|
||||
|
||||
log.Println("未检测到管理员账号,请通过后台初始化页面创建管理员")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ func seedEnterpriseData(db *gorm.DB) error {
|
||||
Slogan: "创新驱动 · 智领未来",
|
||||
Description: "Nexus 科技集团成立于 2010 年,是一家专注于人工智能、云计算和物联网技术的高科技企业。我们致力于为全球客户提供数字化转型解决方案,业务覆盖智能制造、智慧城市、金融科技等多个领域。经过十余年发展,集团已拥有超过 2000 名员工,服务客户超过 5000 家,足迹遍及亚洲、欧洲和北美。",
|
||||
Vision: "成为全球领先的智能科技企业,用技术赋能每一个行业。",
|
||||
Mission: "以创新技术为核心驱动力,为客户创造价值,为社会承担责任,为员工提供平台。",
|
||||
Mission: "以创新技术为核心驱动力,为客户创造价值,为社会承担责任,为员工提供平台。",
|
||||
Phone: "400-888-9999",
|
||||
Email: "contact@nexus-tech.com",
|
||||
Address: "上海市浦东新区张江高科技园区博云路 2 号",
|
||||
|
||||
@@ -59,7 +59,10 @@ func seedFoodData(db *gorm.DB) error {
|
||||
}
|
||||
|
||||
// 为每个餐厅创建菜品分类和菜品
|
||||
type catSeed struct{ name string; items []model.MenuItem }
|
||||
type catSeed struct {
|
||||
name string
|
||||
items []model.MenuItem
|
||||
}
|
||||
restaurantMenus := map[uint][]catSeed{
|
||||
// 蜀香苑 川菜
|
||||
1: {
|
||||
|
||||
107
internal/geoip/geoip.go
Normal file
107
internal/geoip/geoip.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package geoip
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ipLocation 缓存的 IP 归属地信息
|
||||
type ipLocation struct {
|
||||
Location string
|
||||
FetchedAt time.Time
|
||||
}
|
||||
|
||||
// Service IP 归属地查询服务(基于 ip-api.com 免费接口 + 内存缓存)
|
||||
type Service struct {
|
||||
cache sync.Map
|
||||
}
|
||||
|
||||
// NewService 创建 IP 归属地查询服务
|
||||
func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
// ipApiResponse ip-api.com 返回结构
|
||||
type ipApiResponse struct {
|
||||
Status string `json:"status"`
|
||||
Country string `json:"country"`
|
||||
RegionName string `json:"regionName"`
|
||||
City string `json:"city"`
|
||||
ISP string `json:"isp"`
|
||||
Query string `json:"query"`
|
||||
}
|
||||
|
||||
// Lookup 查询 IP 归属地,带内存缓存(24小时有效)
|
||||
func (s *Service) Lookup(ip string) string {
|
||||
// 本地/内网 IP 直接返回
|
||||
if ip == "127.0.0.1" || ip == "::1" || ip == "" ||
|
||||
strings.HasPrefix(ip, "192.168.") ||
|
||||
strings.HasPrefix(ip, "10.") ||
|
||||
strings.HasPrefix(ip, "172.") {
|
||||
return "本地网络"
|
||||
}
|
||||
|
||||
// 检查缓存(24小时有效)
|
||||
if v, ok := s.cache.Load(ip); ok {
|
||||
loc := v.(*ipLocation)
|
||||
if time.Since(loc.FetchedAt) < 24*time.Hour {
|
||||
return loc.Location
|
||||
}
|
||||
s.cache.Delete(ip)
|
||||
}
|
||||
|
||||
// 查询 ip-api.com(免费接口,限制 45 次/分钟)
|
||||
location := s.queryIPAPI(ip)
|
||||
if location == "" {
|
||||
return "未知"
|
||||
}
|
||||
|
||||
// 写入缓存
|
||||
s.cache.Store(ip, &ipLocation{
|
||||
Location: location,
|
||||
FetchedAt: time.Now(),
|
||||
})
|
||||
|
||||
return location
|
||||
}
|
||||
|
||||
// queryIPAPI 调用 ip-api.com 查询
|
||||
func (s *Service) queryIPAPI(ip string) string {
|
||||
client := &http.Client{Timeout: 3 * time.Second}
|
||||
url := fmt.Sprintf("http://ip-api.com/json/%s?lang=zh-CN", ip)
|
||||
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result ipApiResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if result.Status != "success" {
|
||||
return ""
|
||||
}
|
||||
|
||||
parts := []string{}
|
||||
if result.Country != "" {
|
||||
parts = append(parts, result.Country)
|
||||
}
|
||||
if result.RegionName != "" {
|
||||
parts = append(parts, result.RegionName)
|
||||
}
|
||||
if result.City != "" {
|
||||
parts = append(parts, result.City)
|
||||
}
|
||||
if result.ISP != "" {
|
||||
parts = append(parts, result.ISP)
|
||||
}
|
||||
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
107
internal/handler/access_log.go
Normal file
107
internal/handler/access_log.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"gotest/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AccessLogHandler 访问日志处理器
|
||||
type AccessLogHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewAccessLogHandler 创建访问日志处理器
|
||||
func NewAccessLogHandler(db *gorm.DB) *AccessLogHandler {
|
||||
return &AccessLogHandler{db: db}
|
||||
}
|
||||
|
||||
// ListLogs 获取访问日志列表 GET /api/v1/admin/access-logs
|
||||
func (h *AccessLogHandler) ListLogs(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
||||
ip := c.Query("ip")
|
||||
path := c.Query("path")
|
||||
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
query := h.db.Model(&model.AccessLog{})
|
||||
|
||||
if ip != "" {
|
||||
query = query.Where("ip LIKE ?", "%"+ip+"%")
|
||||
}
|
||||
if path != "" {
|
||||
query = query.Where("path LIKE ?", "%"+path+"%")
|
||||
}
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
var logs []model.AccessLog
|
||||
if err := query.Order("id DESC").Offset(offset).Limit(pageSize).Find(&logs).Error; err != nil {
|
||||
FailInternal(c, "查询失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
Success(c, gin.H{
|
||||
"list": logs,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": pageSize,
|
||||
})
|
||||
}
|
||||
|
||||
// GetStats 获取访问统计 GET /api/v1/admin/access-logs/stats
|
||||
func (h *AccessLogHandler) GetStats(c *gin.Context) {
|
||||
var totalVisits int64
|
||||
h.db.Model(&model.AccessLog{}).Count(&totalVisits)
|
||||
|
||||
var todayVisits int64
|
||||
h.db.Model(&model.AccessLog{}).Where("created_at >= date('now')").Count(&todayVisits)
|
||||
|
||||
var uniqueIPs int64
|
||||
h.db.Model(&model.AccessLog{}).Distinct("ip").Count(&uniqueIPs)
|
||||
|
||||
// 最近 24 小时每小时访问量
|
||||
type hourlyCount struct {
|
||||
Hour string `json:"hour"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
var hourly []hourlyCount
|
||||
h.db.Model(&model.AccessLog{}).
|
||||
Select("strftime('%Y-%m-%d %H:00', created_at) as hour, count(*) as count").
|
||||
Where("created_at >= datetime('now', '-24 hours')").
|
||||
Group("hour").
|
||||
Order("hour ASC").
|
||||
Scan(&hourly)
|
||||
|
||||
// 最近 10 条日志
|
||||
var recent []model.AccessLog
|
||||
h.db.Order("id DESC").Limit(10).Find(&recent)
|
||||
|
||||
Success(c, gin.H{
|
||||
"total_visits": totalVisits,
|
||||
"today_visits": todayVisits,
|
||||
"unique_ips": uniqueIPs,
|
||||
"hourly": hourly,
|
||||
"recent": recent,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteLogs 清空访问日志 DELETE /api/v1/admin/access-logs
|
||||
func (h *AccessLogHandler) DeleteLogs(c *gin.Context) {
|
||||
if err := h.db.Where("1 = 1").Delete(&model.AccessLog{}).Error; err != nil {
|
||||
FailInternal(c, "清空失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
SuccessMsg(c, "清空成功")
|
||||
}
|
||||
82
internal/handler/config.go
Normal file
82
internal/handler/config.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"gotest/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ConfigHandler 系统配置处理器
|
||||
type ConfigHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewConfigHandler 创建系统配置处理器
|
||||
func NewConfigHandler(db *gorm.DB) *ConfigHandler {
|
||||
return &ConfigHandler{db: db}
|
||||
}
|
||||
|
||||
// GetConfig 获取配置 GET /api/v1/admin/config/:key
|
||||
func (h *ConfigHandler) GetConfig(c *gin.Context) {
|
||||
key := c.Param("key")
|
||||
var config model.Config
|
||||
if err := h.db.Where("`key` = ?", key).First(&config).Error; err != nil {
|
||||
Success(c, gin.H{"key": key, "value": "", "remark": ""})
|
||||
return
|
||||
}
|
||||
Success(c, config)
|
||||
}
|
||||
|
||||
// SetConfig 设置配置 PUT /api/v1/admin/config/:key
|
||||
func (h *ConfigHandler) SetConfig(c *gin.Context) {
|
||||
key := c.Param("key")
|
||||
var req struct {
|
||||
Value string `json:"value"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
FailBadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
var config model.Config
|
||||
result := h.db.Where("`key` = ?", key).First(&config)
|
||||
if result.Error == gorm.ErrRecordNotFound {
|
||||
// 创建新配置
|
||||
config = model.Config{
|
||||
Key: key,
|
||||
Value: req.Value,
|
||||
Remark: req.Remark,
|
||||
}
|
||||
if err := h.db.Create(&config).Error; err != nil {
|
||||
FailInternal(c, "创建失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
} else if result.Error == nil {
|
||||
// 更新已有配置
|
||||
config.Value = req.Value
|
||||
if req.Remark != "" {
|
||||
config.Remark = req.Remark
|
||||
}
|
||||
if err := h.db.Save(&config).Error; err != nil {
|
||||
FailInternal(c, "更新失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
FailInternal(c, "数据库错误: "+result.Error.Error())
|
||||
return
|
||||
}
|
||||
|
||||
Success(c, config)
|
||||
}
|
||||
|
||||
// ListConfigs 获取所有配置 GET /api/v1/admin/config
|
||||
func (h *ConfigHandler) ListConfigs(c *gin.Context) {
|
||||
var configs []model.Config
|
||||
if err := h.db.Order("id ASC").Find(&configs).Error; err != nil {
|
||||
FailInternal(c, "查询失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
Success(c, configs)
|
||||
}
|
||||
116
internal/handler/customer.go
Normal file
116
internal/handler/customer.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"gotest/internal/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CustomerHandler 客户信息收集处理器
|
||||
type CustomerHandler struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
// NewCustomerHandler 创建客户信息收集处理器
|
||||
func NewCustomerHandler(cfg *config.Config) *CustomerHandler {
|
||||
return &CustomerHandler{cfg: cfg}
|
||||
}
|
||||
|
||||
// CustomerRequest 客户信息请求
|
||||
type CustomerRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Phone string `json:"phone" binding:"required"`
|
||||
Company string `json:"company"`
|
||||
Email string `json:"email"`
|
||||
Needs string `json:"needs" binding:"required"`
|
||||
}
|
||||
|
||||
// Submit 提交客户信息 POST /api/v1/customer/submit
|
||||
func (h *CustomerHandler) Submit(c *gin.Context) {
|
||||
var req CustomerRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
FailBadRequest(c, "请填写完整的联系信息")
|
||||
return
|
||||
}
|
||||
|
||||
// 构建企业微信消息内容
|
||||
content := fmt.Sprintf("【新客户咨询】\n"+
|
||||
"━━━━━━━━━━━━━━━━━━━━\n"+
|
||||
"👤 姓名:%s\n"+
|
||||
"📱 电话:%s\n"+
|
||||
"📧 邮箱:%s\n"+
|
||||
"🏢 公司:%s\n"+
|
||||
"💼 需求:%s\n"+
|
||||
"⏰ 时间:%s\n"+
|
||||
"━━━━━━━━━━━━━━━━━━━━",
|
||||
req.Name, req.Phone, req.Email, req.Company, req.Needs, time.Now().Format("2006-01-02 15:04:05"))
|
||||
|
||||
// 发送到企业微信webhook
|
||||
if err := h.sendToWechatWebhook(content); err != nil {
|
||||
// 即使webhook失败,也记录下来并返回成功(避免暴露内部错误)
|
||||
fmt.Printf("发送企业微信webhook失败: %v\n", err)
|
||||
}
|
||||
|
||||
Success(c, gin.H{
|
||||
"message": "提交成功,我们会尽快联系您!",
|
||||
})
|
||||
}
|
||||
|
||||
// sendToWechatWebhook 发送消息到企业微信群机器人
|
||||
func (h *CustomerHandler) sendToWechatWebhook(content string) error {
|
||||
if h.cfg.WechatWebhook.URL == "" {
|
||||
return fmt.Errorf("企业微信webhook URL未配置")
|
||||
}
|
||||
|
||||
// 构建消息体
|
||||
message := map[string]interface{}{
|
||||
"msgtype": "text",
|
||||
"text": map[string]string{
|
||||
"content": content,
|
||||
},
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化消息失败: %w", err)
|
||||
}
|
||||
|
||||
// 发送POST请求
|
||||
resp, err := http.Post(h.cfg.WechatWebhook.URL, "application/json", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return fmt.Errorf("发送请求失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查响应状态
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("请求失败,状态码: %d,响应: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// 解析响应JSON
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return fmt.Errorf("解析响应失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查企业微信返回的错误码
|
||||
if errcode, ok := result["errcode"].(float64); ok && errcode != 0 {
|
||||
errmsg, _ := result["errmsg"].(string)
|
||||
return fmt.Errorf("企业微信返回错误: %v - %s", errcode, errmsg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -28,7 +28,7 @@ var tableRegistry = map[string]interface{}{
|
||||
"game_games": &model.Game{},
|
||||
"game_categories": &model.GameCategory{},
|
||||
// 应用模块
|
||||
"app_apps": &model.App{},
|
||||
"app_apps": &model.App{},
|
||||
"app_categories": &model.AppCategory{},
|
||||
// 音乐模块
|
||||
"music_songs": &model.Song{},
|
||||
@@ -37,22 +37,22 @@ var tableRegistry = map[string]interface{}{
|
||||
|
||||
// tableLabels 模块中文标签(用于前端展示)
|
||||
var tableLabels = map[string]string{
|
||||
"enterprise_company": "企业信息",
|
||||
"enterprise_news": "企业新闻",
|
||||
"enterprise_services": "企业服务",
|
||||
"enterprise_team": "团队成员",
|
||||
"shop_products": "商品管理",
|
||||
"shop_categories": "商品分类",
|
||||
"food_restaurants": "餐厅管理",
|
||||
"food_categories": "餐厅分类",
|
||||
"food_menu_items": "菜品管理",
|
||||
"enterprise_company": "企业信息",
|
||||
"enterprise_news": "企业新闻",
|
||||
"enterprise_services": "企业服务",
|
||||
"enterprise_team": "团队成员",
|
||||
"shop_products": "商品管理",
|
||||
"shop_categories": "商品分类",
|
||||
"food_restaurants": "餐厅管理",
|
||||
"food_categories": "餐厅分类",
|
||||
"food_menu_items": "菜品管理",
|
||||
"food_menu_categories": "菜品分类",
|
||||
"game_games": "游戏管理",
|
||||
"game_categories": "游戏分类",
|
||||
"app_apps": "应用管理",
|
||||
"app_categories": "应用分类",
|
||||
"music_songs": "歌曲管理",
|
||||
"music_categories": "音乐分类",
|
||||
"game_games": "游戏管理",
|
||||
"game_categories": "游戏分类",
|
||||
"app_apps": "应用管理",
|
||||
"app_categories": "应用分类",
|
||||
"music_songs": "歌曲管理",
|
||||
"music_categories": "音乐分类",
|
||||
}
|
||||
|
||||
// tablesWithSort 拥有 sort 列的表(用于排序)
|
||||
@@ -75,40 +75,40 @@ var tablesWithSort = map[string]bool{
|
||||
|
||||
// tablesWithName 拥有 name 列的表(用于关键词搜索)
|
||||
var tablesWithName = map[string]bool{
|
||||
"enterprise_company": true,
|
||||
"enterprise_team": true,
|
||||
"enterprise_services": false, // 有 title 无 name
|
||||
"enterprise_news": false,
|
||||
"shop_products": true,
|
||||
"shop_categories": true,
|
||||
"food_restaurants": true,
|
||||
"food_categories": true,
|
||||
"food_menu_items": true,
|
||||
"food_menu_categories": true,
|
||||
"game_games": true,
|
||||
"game_categories": true,
|
||||
"app_apps": true,
|
||||
"app_categories": true,
|
||||
"music_songs": false, // 有 title 无 name
|
||||
"music_categories": true,
|
||||
"enterprise_company": true,
|
||||
"enterprise_team": true,
|
||||
"enterprise_services": false, // 有 title 无 name
|
||||
"enterprise_news": false,
|
||||
"shop_products": true,
|
||||
"shop_categories": true,
|
||||
"food_restaurants": true,
|
||||
"food_categories": true,
|
||||
"food_menu_items": true,
|
||||
"food_menu_categories": true,
|
||||
"game_games": true,
|
||||
"game_categories": true,
|
||||
"app_apps": true,
|
||||
"app_categories": true,
|
||||
"music_songs": false, // 有 title 无 name
|
||||
"music_categories": true,
|
||||
}
|
||||
|
||||
// tablesWithTitle 拥有 title 列的表(用于关键词搜索)
|
||||
var tablesWithTitle = map[string]bool{
|
||||
"enterprise_news": true,
|
||||
"enterprise_services": true,
|
||||
"music_songs": true,
|
||||
"enterprise_news": true,
|
||||
"enterprise_services": true,
|
||||
"music_songs": true,
|
||||
}
|
||||
|
||||
// tablesWithStatus 拥有 status 列的表(用于状态过滤)
|
||||
var tablesWithStatus = map[string]bool{
|
||||
"enterprise_news": true,
|
||||
"shop_products": true,
|
||||
"food_restaurants": true,
|
||||
"food_menu_items": true,
|
||||
"game_games": true,
|
||||
"app_apps": true,
|
||||
"music_songs": true,
|
||||
"enterprise_news": true,
|
||||
"shop_products": true,
|
||||
"food_restaurants": true,
|
||||
"food_menu_items": true,
|
||||
"game_games": true,
|
||||
"app_apps": true,
|
||||
"music_songs": true,
|
||||
}
|
||||
|
||||
// DashboardHandler 管理后台仪表盘处理器(通用 CRUD + 统计)
|
||||
@@ -140,7 +140,7 @@ func (h *DashboardHandler) GetStats(c *gin.Context) {
|
||||
// 按业务线分组
|
||||
grouped := gin.H{
|
||||
"enterprise": gin.H{
|
||||
"label": "企业官网",
|
||||
"label": "企业官网",
|
||||
"items": gin.H{
|
||||
"company": stats["enterprise_company"],
|
||||
"news": stats["enterprise_news"],
|
||||
@@ -158,10 +158,10 @@ func (h *DashboardHandler) GetStats(c *gin.Context) {
|
||||
"food": gin.H{
|
||||
"label": "外卖平台",
|
||||
"items": gin.H{
|
||||
"restaurants": stats["food_restaurants"],
|
||||
"categories": stats["food_categories"],
|
||||
"menu_items": stats["food_menu_items"],
|
||||
"menu_categories": stats["food_menu_categories"],
|
||||
"restaurants": stats["food_restaurants"],
|
||||
"categories": stats["food_categories"],
|
||||
"menu_items": stats["food_menu_items"],
|
||||
"menu_categories": stats["food_menu_categories"],
|
||||
},
|
||||
},
|
||||
"game": gin.H{
|
||||
|
||||
78
internal/middleware/access_log.go
Normal file
78
internal/middleware/access_log.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gotest/internal/geoip"
|
||||
"gotest/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// responseWriter 包装 gin.ResponseWriter 以捕获状态码
|
||||
type responseWriter struct {
|
||||
gin.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (w *responseWriter) WriteHeader(code int) {
|
||||
w.statusCode = code
|
||||
w.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
// AccessLog 访问日志中间件,记录每次 HTTP 请求到数据库
|
||||
func AccessLog(db *gorm.DB, ipService *geoip.Service) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 跳过健康检查和静态资源
|
||||
path := c.Request.URL.Path
|
||||
if path == "/api/health" || strings.HasPrefix(path, "/shared/") || strings.HasPrefix(path, "/assets/") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
// 包装响应写入器以捕获状态码
|
||||
rw := &responseWriter{ResponseWriter: c.Writer, statusCode: http.StatusOK}
|
||||
c.Writer = rw
|
||||
|
||||
c.Next()
|
||||
|
||||
// 异步写入日志(不阻塞请求响应)
|
||||
go func() {
|
||||
cost := int(time.Since(start).Milliseconds())
|
||||
ua := c.Request.UserAgent()
|
||||
if len(ua) > 500 {
|
||||
ua = ua[:500]
|
||||
}
|
||||
referer := c.Request.Referer()
|
||||
if len(referer) > 500 {
|
||||
referer = referer[:500]
|
||||
}
|
||||
p := c.Request.URL.Path
|
||||
if len(p) > 500 {
|
||||
p = p[:500]
|
||||
}
|
||||
|
||||
// 查询 IP 归属地
|
||||
location := ipService.Lookup(clientIP)
|
||||
|
||||
log := model.AccessLog{
|
||||
IP: clientIP,
|
||||
Path: p,
|
||||
Method: c.Request.Method,
|
||||
UserAgent: ua,
|
||||
Referer: referer,
|
||||
Location: location,
|
||||
Status: rw.statusCode,
|
||||
Cost: cost,
|
||||
}
|
||||
|
||||
db.Create(&log)
|
||||
}()
|
||||
}
|
||||
}
|
||||
17
internal/model/access_log.go
Normal file
17
internal/model/access_log.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// AccessLog 网站访问日志
|
||||
type AccessLog struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
IP string `gorm:"size:45;index" json:"ip"`
|
||||
Path string `gorm:"size:500;index" json:"path"`
|
||||
Method string `gorm:"size:10" json:"method"`
|
||||
UserAgent string `gorm:"size:500" json:"user_agent"`
|
||||
Referer string `gorm:"size:500" json:"referer"`
|
||||
Location string `gorm:"size:255" json:"location"` // IP 归属地
|
||||
Status int `json:"status"`
|
||||
Cost int `json:"cost"` // 耗时(毫秒)
|
||||
CreatedAt time.Time `gorm:"index" json:"created_at"`
|
||||
}
|
||||
@@ -19,19 +19,19 @@ type App struct {
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
CategoryID uint `gorm:"index" json:"category_id"`
|
||||
CategoryName string `gorm:"size:50" json:"category_name"`
|
||||
Icon string `gorm:"size:255" json:"icon"` // 应用图标URL
|
||||
Developer string `gorm:"size:100" json:"developer"` // 开发者
|
||||
Icon string `gorm:"size:255" json:"icon"` // 应用图标URL
|
||||
Developer string `gorm:"size:100" json:"developer"` // 开发者
|
||||
Rating float64 `gorm:"type:decimal(2,1);default:5.0" json:"rating"`
|
||||
Reviews int `gorm:"default:0" json:"reviews"` // 评价数
|
||||
Downloads int `gorm:"default:0" json:"downloads"` // 下载量
|
||||
Size string `gorm:"size:50" json:"size"` // 应用大小
|
||||
Version string `gorm:"size:50" json:"version"` // 版本号
|
||||
UpdateDate string `gorm:"size:20" json:"update_date"` // 更新日期
|
||||
Platform string `gorm:"size:100" json:"platform"` // 平台
|
||||
Price string `gorm:"size:50" json:"price"` // 价格(免费/付费)
|
||||
Tags string `gorm:"type:text" json:"tags"` // JSON: 标签数组
|
||||
Screenshots string `gorm:"type:text" json:"screenshots"` // JSON: 截图描述数组
|
||||
Features string `gorm:"type:text" json:"features"` // JSON: 特性列表
|
||||
Reviews int `gorm:"default:0" json:"reviews"` // 评价数
|
||||
Downloads int `gorm:"default:0" json:"downloads"` // 下载量
|
||||
Size string `gorm:"size:50" json:"size"` // 应用大小
|
||||
Version string `gorm:"size:50" json:"version"` // 版本号
|
||||
UpdateDate string `gorm:"size:20" json:"update_date"` // 更新日期
|
||||
Platform string `gorm:"size:100" json:"platform"` // 平台
|
||||
Price string `gorm:"size:50" json:"price"` // 价格(免费/付费)
|
||||
Tags string `gorm:"type:text" json:"tags"` // JSON: 标签数组
|
||||
Screenshots string `gorm:"type:text" json:"screenshots"` // JSON: 截图描述数组
|
||||
Features string `gorm:"type:text" json:"features"` // JSON: 特性列表
|
||||
IsFeatured bool `gorm:"default:false" json:"is_featured"` // 精选
|
||||
IsNew bool `gorm:"default:false" json:"is_new"` // 新上架
|
||||
Sort int `gorm:"default:0" json:"sort"`
|
||||
|
||||
@@ -8,9 +8,9 @@ type Company struct {
|
||||
Name string `gorm:"size:100;not null" json:"name"`
|
||||
Logo string `gorm:"size:255" json:"logo"`
|
||||
Slogan string `gorm:"size:255" json:"slogan"`
|
||||
Description string `gorm:"type:text" json:"description"` // 关于我们
|
||||
Vision string `gorm:"type:text" json:"vision"` // 愿景
|
||||
Mission string `gorm:"type:text" json:"mission"` // 使命
|
||||
Description string `gorm:"type:text" json:"description"` // 关于我们
|
||||
Vision string `gorm:"type:text" json:"vision"` // 愿景
|
||||
Mission string `gorm:"type:text" json:"mission"` // 使命
|
||||
Phone string `gorm:"size:30" json:"phone"`
|
||||
Email string `gorm:"size:100" json:"email"`
|
||||
Address string `gorm:"size:255" json:"address"`
|
||||
@@ -41,8 +41,8 @@ type EnterpriseService struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Title string `gorm:"size:100;not null" json:"title"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
Icon string `gorm:"size:50" json:"icon"` // 图标标识
|
||||
Image string `gorm:"size:255" json:"image"` // 图片URL
|
||||
Icon string `gorm:"size:50" json:"icon"` // 图标标识
|
||||
Image string `gorm:"size:255" json:"image"` // 图片URL
|
||||
Features string `gorm:"type:text" json:"features"` // JSON: 特性列表
|
||||
Sort int `gorm:"default:0" json:"sort"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
@@ -14,25 +14,25 @@ type RestaurantCategory struct {
|
||||
|
||||
// Restaurant 餐厅模型(3 套外卖网站共用同一套数据)
|
||||
type Restaurant struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:100;not null" json:"name"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
CategoryID uint `gorm:"index" json:"category_id"`
|
||||
CategoryName string `gorm:"size:50" json:"category_name"`
|
||||
Cover string `gorm:"size:255" json:"cover"` // 封面图URL
|
||||
Logo string `gorm:"size:255" json:"logo"` // Logo URL
|
||||
Rating float64 `gorm:"type:decimal(2,1);default:4.5" json:"rating"`
|
||||
Sales int `gorm:"default:0" json:"sales"` // 月销量
|
||||
DeliveryFee float64 `gorm:"type:decimal(6,2);default:0" json:"delivery_fee"` // 配送费
|
||||
MinOrder float64 `gorm:"type:decimal(6,2);default:0" json:"min_order"` // 起送价
|
||||
DeliveryTime int `json:"delivery_time"` // 配送时间(分钟)
|
||||
Distance string `gorm:"size:50" json:"distance"` // 距离
|
||||
Tags string `gorm:"type:text" json:"tags"` // JSON: 标签数组
|
||||
Notice string `gorm:"type:text" json:"notice"` // 公告
|
||||
Status int `gorm:"default:1;not null" json:"status"` // 1=营业 0=休息
|
||||
Sort int `gorm:"default:0" json:"sort"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:100;not null" json:"name"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
CategoryID uint `gorm:"index" json:"category_id"`
|
||||
CategoryName string `gorm:"size:50" json:"category_name"`
|
||||
Cover string `gorm:"size:255" json:"cover"` // 封面图URL
|
||||
Logo string `gorm:"size:255" json:"logo"` // Logo URL
|
||||
Rating float64 `gorm:"type:decimal(2,1);default:4.5" json:"rating"`
|
||||
Sales int `gorm:"default:0" json:"sales"` // 月销量
|
||||
DeliveryFee float64 `gorm:"type:decimal(6,2);default:0" json:"delivery_fee"` // 配送费
|
||||
MinOrder float64 `gorm:"type:decimal(6,2);default:0" json:"min_order"` // 起送价
|
||||
DeliveryTime int `json:"delivery_time"` // 配送时间(分钟)
|
||||
Distance string `gorm:"size:50" json:"distance"` // 距离
|
||||
Tags string `gorm:"type:text" json:"tags"` // JSON: 标签数组
|
||||
Notice string `gorm:"type:text" json:"notice"` // 公告
|
||||
Status int `gorm:"default:1;not null" json:"status"` // 1=营业 0=休息
|
||||
Sort int `gorm:"default:0" json:"sort"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// MenuCategory 菜品分类(属于某个餐厅)
|
||||
@@ -47,19 +47,19 @@ type MenuCategory struct {
|
||||
|
||||
// MenuItem 菜品模型
|
||||
type MenuItem struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
RestaurantID uint `gorm:"index;not null" json:"restaurant_id"`
|
||||
CategoryID uint `gorm:"index" json:"category_id"` // 菜品分类ID
|
||||
Name string `gorm:"size:100;not null" json:"name"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
Price float64 `gorm:"type:decimal(8,2);not null" json:"price"`
|
||||
OriginalPrice float64 `gorm:"type:decimal(8,2)" json:"original_price"`
|
||||
Image string `gorm:"size:255" json:"image"`
|
||||
Rating float64 `gorm:"type:decimal(2,1);default:5.0" json:"rating"`
|
||||
Sales int `gorm:"default:0" json:"sales"`
|
||||
Tags string `gorm:"type:text" json:"tags"` // JSON: 标签数组
|
||||
Sort int `gorm:"default:0" json:"sort"`
|
||||
Status int `gorm:"default:1;not null" json:"status"` // 1=上架
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
RestaurantID uint `gorm:"index;not null" json:"restaurant_id"`
|
||||
CategoryID uint `gorm:"index" json:"category_id"` // 菜品分类ID
|
||||
Name string `gorm:"size:100;not null" json:"name"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
Price float64 `gorm:"type:decimal(8,2);not null" json:"price"`
|
||||
OriginalPrice float64 `gorm:"type:decimal(8,2)" json:"original_price"`
|
||||
Image string `gorm:"size:255" json:"image"`
|
||||
Rating float64 `gorm:"type:decimal(2,1);default:5.0" json:"rating"`
|
||||
Sales int `gorm:"default:0" json:"sales"`
|
||||
Tags string `gorm:"type:text" json:"tags"` // JSON: 标签数组
|
||||
Sort int `gorm:"default:0" json:"sort"`
|
||||
Status int `gorm:"default:1;not null" json:"status"` // 1=上架
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
@@ -14,28 +14,28 @@ type GameCategory struct {
|
||||
|
||||
// Game 游戏模型(3 套游戏网站共用同一套数据)
|
||||
type Game struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:200;not null" json:"name"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
CategoryID uint `gorm:"index" json:"category_id"`
|
||||
CategoryName string `gorm:"size:50" json:"category_name"`
|
||||
Cover string `gorm:"size:255" json:"cover"` // 封面图URL
|
||||
Developer string `gorm:"size:100" json:"developer"` // 开发商
|
||||
Publisher string `gorm:"size:100" json:"publisher"` // 发行商
|
||||
Rating float64 `gorm:"type:decimal(2,1);default:5.0" json:"rating"`
|
||||
Plays int `gorm:"default:0" json:"plays"` // 游玩次数
|
||||
Downloads int `gorm:"default:0" json:"downloads"` // 下载量
|
||||
Size string `gorm:"size:50" json:"size"` // 游戏大小
|
||||
Version string `gorm:"size:50" json:"version"` // 版本号
|
||||
ReleaseDate string `gorm:"size:20" json:"release_date"` // 发行日期
|
||||
Platform string `gorm:"size:100" json:"platform"` // 平台
|
||||
Tags string `gorm:"type:text" json:"tags"` // JSON: 标签数组
|
||||
Screenshots string `gorm:"type:text" json:"screenshots"` // JSON: 截图描述数组
|
||||
Features string `gorm:"type:text" json:"features"` // JSON: 特性列表
|
||||
IsHot bool `gorm:"default:false" json:"is_hot"` // 热门
|
||||
IsNew bool `gorm:"default:false" json:"is_new"` // 新游
|
||||
Sort int `gorm:"default:0" json:"sort"`
|
||||
Status int `gorm:"default:1;not null" json:"status"` // 1=上架
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:200;not null" json:"name"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
CategoryID uint `gorm:"index" json:"category_id"`
|
||||
CategoryName string `gorm:"size:50" json:"category_name"`
|
||||
Cover string `gorm:"size:255" json:"cover"` // 封面图URL
|
||||
Developer string `gorm:"size:100" json:"developer"` // 开发商
|
||||
Publisher string `gorm:"size:100" json:"publisher"` // 发行商
|
||||
Rating float64 `gorm:"type:decimal(2,1);default:5.0" json:"rating"`
|
||||
Plays int `gorm:"default:0" json:"plays"` // 游玩次数
|
||||
Downloads int `gorm:"default:0" json:"downloads"` // 下载量
|
||||
Size string `gorm:"size:50" json:"size"` // 游戏大小
|
||||
Version string `gorm:"size:50" json:"version"` // 版本号
|
||||
ReleaseDate string `gorm:"size:20" json:"release_date"` // 发行日期
|
||||
Platform string `gorm:"size:100" json:"platform"` // 平台
|
||||
Tags string `gorm:"type:text" json:"tags"` // JSON: 标签数组
|
||||
Screenshots string `gorm:"type:text" json:"screenshots"` // JSON: 截图描述数组
|
||||
Features string `gorm:"type:text" json:"features"` // JSON: 特性列表
|
||||
IsHot bool `gorm:"default:false" json:"is_hot"` // 热门
|
||||
IsNew bool `gorm:"default:false" json:"is_new"` // 新游
|
||||
Sort int `gorm:"default:0" json:"sort"`
|
||||
Status int `gorm:"default:1;not null" json:"status"` // 1=上架
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
@@ -15,23 +15,23 @@ type MusicCategory struct {
|
||||
// Song 歌曲模型(3 套音乐网站共用同一套数据)
|
||||
type Song struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Title string `gorm:"size:200;not null" json:"title"` // 歌曲名
|
||||
Artist string `gorm:"size:100;not null" json:"artist"` // 歌手
|
||||
Album string `gorm:"size:200" json:"album"` // 专辑
|
||||
Title string `gorm:"size:200;not null" json:"title"` // 歌曲名
|
||||
Artist string `gorm:"size:100;not null" json:"artist"` // 歌手
|
||||
Album string `gorm:"size:200" json:"album"` // 专辑
|
||||
CategoryID uint `gorm:"index" json:"category_id"`
|
||||
CategoryName string `gorm:"size:50" json:"category_name"` // 曲风
|
||||
Cover string `gorm:"size:255" json:"cover"` // 封面URL
|
||||
Duration int `gorm:"default:0" json:"duration"` // 时长(秒)
|
||||
PlayCount int `gorm:"default:0" json:"play_count"` // 播放量
|
||||
LikeCount int `gorm:"default:0" json:"like_count"` // 收藏量
|
||||
Lyrics string `gorm:"type:text" json:"lyrics"` // 歌词
|
||||
ReleaseDate string `gorm:"size:20" json:"release_date"` // 发行日期
|
||||
Language string `gorm:"size:50" json:"language"` // 语种
|
||||
Tags string `gorm:"type:text" json:"tags"` // JSON: 标签数组
|
||||
IsHot bool `gorm:"default:false" json:"is_hot"` // 热门
|
||||
IsNew bool `gorm:"default:false" json:"is_new"` // 新歌
|
||||
CategoryName string `gorm:"size:50" json:"category_name"` // 曲风
|
||||
Cover string `gorm:"size:255" json:"cover"` // 封面URL
|
||||
Duration int `gorm:"default:0" json:"duration"` // 时长(秒)
|
||||
PlayCount int `gorm:"default:0" json:"play_count"` // 播放量
|
||||
LikeCount int `gorm:"default:0" json:"like_count"` // 收藏量
|
||||
Lyrics string `gorm:"type:text" json:"lyrics"` // 歌词
|
||||
ReleaseDate string `gorm:"size:20" json:"release_date"` // 发行日期
|
||||
Language string `gorm:"size:50" json:"language"` // 语种
|
||||
Tags string `gorm:"type:text" json:"tags"` // JSON: 标签数组
|
||||
IsHot bool `gorm:"default:false" json:"is_hot"` // 热门
|
||||
IsNew bool `gorm:"default:false" json:"is_new"` // 新歌
|
||||
Sort int `gorm:"default:0" json:"sort"`
|
||||
Status int `gorm:"default:1;not null" json:"status"` // 1=上架
|
||||
Status int `gorm:"default:1;not null" json:"status"` // 1=上架
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Username string `gorm:"size:50;uniqueIndex;not null" json:"username"`
|
||||
Password string `gorm:"size:255;not null" json:"-"` // bcrypt 哈希,不返回前端
|
||||
Phone string `gorm:"size:20;index" json:"phone"` // 手机号
|
||||
Password string `gorm:"size:255;not null" json:"-"` // bcrypt 哈希,不返回前端
|
||||
Phone string `gorm:"size:20;index" json:"phone"` // 手机号
|
||||
Role string `gorm:"size:20;default:user;not null" json:"role"` // admin | user
|
||||
Avatar string `gorm:"size:255" json:"avatar"` // 头像 URL
|
||||
Status int `gorm:"default:1;not null" json:"status"` // 1=正常 0=禁用
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"gotest/internal/config"
|
||||
"gotest/internal/database"
|
||||
"gotest/internal/geoip"
|
||||
"gotest/internal/handler"
|
||||
"gotest/internal/middleware"
|
||||
|
||||
@@ -27,6 +28,10 @@ func Setup(
|
||||
appH *handler.AppHandler,
|
||||
musicH *handler.MusicHandler,
|
||||
dashboardH *handler.DashboardHandler,
|
||||
customerH *handler.CustomerHandler,
|
||||
accessLogH *handler.AccessLogHandler,
|
||||
configH *handler.ConfigHandler,
|
||||
ipLocationService *geoip.Service,
|
||||
) *gin.Engine {
|
||||
if cfg.Env.Name == "prod" {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
@@ -34,12 +39,13 @@ func Setup(
|
||||
|
||||
r := gin.New()
|
||||
|
||||
// 全局中间件链(顺序:RequestID → Recover → Logger → CORS → RateLimit)
|
||||
// 全局中间件链(顺序:RequestID → Recover → Logger → CORS → RateLimit → AccessLog)
|
||||
r.Use(middleware.RequestID())
|
||||
r.Use(middleware.Recover())
|
||||
r.Use(middleware.Logger())
|
||||
r.Use(middleware.CORS(&cfg.CORS))
|
||||
r.Use(middleware.RateLimit(&cfg.RateLimit))
|
||||
r.Use(middleware.AccessLog(db.DB, ipLocationService))
|
||||
|
||||
// 健康检查(含数据库就绪探针)
|
||||
r.GET("/api/health", func(c *gin.Context) {
|
||||
@@ -147,6 +153,15 @@ func Setup(
|
||||
music.GET("/new", musicH.ListNew)
|
||||
}
|
||||
|
||||
// 公开接口(无需认证)
|
||||
// ...
|
||||
|
||||
// 客户信息收集(公开,无需认证)
|
||||
customer := api.Group("/customer")
|
||||
{
|
||||
customer.POST("/submit", customerH.Submit)
|
||||
}
|
||||
|
||||
// 需要登录的路由
|
||||
authed := api.Group("")
|
||||
authed.Use(middleware.JWTAuth(cfg.JWT.Secret))
|
||||
@@ -180,6 +195,16 @@ func Setup(
|
||||
admin.POST("/dashboard/:table", dashboardH.CreateItem)
|
||||
admin.PUT("/dashboard/:table/:id", dashboardH.UpdateItem)
|
||||
admin.DELETE("/dashboard/:table/:id", dashboardH.DeleteItem)
|
||||
|
||||
// 访问日志
|
||||
admin.GET("/access-logs", accessLogH.ListLogs)
|
||||
admin.GET("/access-logs/stats", accessLogH.GetStats)
|
||||
admin.DELETE("/access-logs", accessLogH.DeleteLogs)
|
||||
|
||||
// 系统配置(webhook 等)
|
||||
admin.GET("/config", configH.ListConfigs)
|
||||
admin.GET("/config/:key", configH.GetConfig)
|
||||
admin.PUT("/config/:key", configH.SetConfig)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,9 +58,9 @@ func (s *FoodService) GetRestaurantMenu(restaurantID uint) (map[string]interface
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"restaurant": restaurant,
|
||||
"categories": categories,
|
||||
"menu_items": items,
|
||||
"restaurant": restaurant,
|
||||
"categories": categories,
|
||||
"menu_items": items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -76,11 +76,11 @@ func (s *UploadService) Upload(module string, filename string, contentType strin
|
||||
|
||||
// FileInfo 文件信息
|
||||
type FileInfo struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Size int64 `json:"size"`
|
||||
ModTime string `json:"mod_time"`
|
||||
IsDir bool `json:"is_dir"`
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
Size int64 `json:"size"`
|
||||
ModTime string `json:"mod_time"`
|
||||
IsDir bool `json:"is_dir"`
|
||||
}
|
||||
|
||||
// ListFiles 列出指定模块下的所有文件(递归遍历子目录)
|
||||
|
||||
Reference in New Issue
Block a user