代码推送

This commit is contained in:
2026-09-09 02:39:57 +08:00
parent a112893947
commit 5e47757c73
24 changed files with 1156 additions and 226 deletions

View File

@@ -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"`

View File

@@ -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
}

View File

@@ -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 号",

View File

@@ -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
View 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, " ")
}

View 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, "清空成功")
}

View 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)
}

View 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
}

View File

@@ -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{

View 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)
}()
}
}

View 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"`
}

View File

@@ -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"`

View File

@@ -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"`

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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=禁用

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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 列出指定模块下的所有文件(递归遍历子目录)