代码推送
This commit is contained in:
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{
|
||||
|
||||
Reference in New Issue
Block a user