代码推送

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

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