feat: 产品/客户/订单批量删除;接入 GeoLite2 城市库(地理分布/激活记录/审计日志显示归属地);仪表盘趋势图美化
This commit is contained in:
1 parent
252ae14aef
commit
473dcd0aab
24 files changed
+431
-34
No files matched your search
@@ -4,6 +4,7 @@ import (
|
||||
"backend/internal/dto"
|
||||
customerservice "backend/internal/service/customer"
|
||||
"backend/utils"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -120,3 +121,22 @@ func (ctrl *CustomerController) UpdateStatus(c *gin.Context) {
|
||||
utils.LogAction(ctrl.db, c, "update_status", "customer", uint(id), "", "", "", "切换客户状态")
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "状态更新成功"})
|
||||
}
|
||||
|
||||
// BatchDelete 批量删除客户(名下有 License 的自动跳过) // Logan.Li
|
||||
func (ctrl *CustomerController) BatchDelete(c *gin.Context) {
|
||||
var req struct {
|
||||
IDs []uint `json:"ids" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
||||
return
|
||||
}
|
||||
result, err := ctrl.svc.BatchDelete(req.IDs)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, "batch_delete", "customer", 0, "", "", "",
|
||||
fmt.Sprintf("批量删除客户 %d 条,跳过 %d 条", result.Deleted, result.Skipped))
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "批量删除完成", "data": result})
|
||||
}
|
||||
@@ -529,3 +529,22 @@ func (ctrl *OrderController) CheckPurchaseStatus(c *gin.Context) {
|
||||
resp, _ := ctrl.svc.CheckPurchaseStatus(req.CustomerEmail, req.ProductID)
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "data": resp})
|
||||
}
|
||||
|
||||
// BatchDelete 批量删除订单(已支付为财务凭证自动跳过) // Logan.Li
|
||||
func (ctrl *OrderController) BatchDelete(c *gin.Context) {
|
||||
var req struct {
|
||||
IDs []uint `json:"ids" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
||||
return
|
||||
}
|
||||
result, err := ctrl.svc.BatchDelete(req.IDs)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, "batch_delete", "order", 0, "", "", "",
|
||||
fmt.Sprintf("批量删除订单 %d 条,跳过 %d 条", result.Deleted, result.Skipped))
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "批量删除完成", "data": result})
|
||||
}
|
||||
@@ -144,3 +144,22 @@ func (ctrl *ProductController) UpdateStatus(c *gin.Context) {
|
||||
utils.LogAction(ctrl.db, c, "update_status", "product", uint(id), "", "", "", fmt.Sprintf("切换状态为 %d", req.Status))
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "状态更新成功"})
|
||||
}
|
||||
|
||||
// BatchDelete 批量删除产品(被 License 引用的自动跳过) // Logan.Li
|
||||
func (ctrl *ProductController) BatchDelete(c *gin.Context) {
|
||||
var req struct {
|
||||
IDs []uint `json:"ids" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "参数错误"})
|
||||
return
|
||||
}
|
||||
result, err := ctrl.svc.BatchDelete(req.IDs)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"code": 1, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
utils.LogAction(ctrl.db, c, "batch_delete", "product", 0, "", "", "",
|
||||
fmt.Sprintf("批量删除产品 %d 条,跳过 %d 条", result.Deleted, result.Skipped))
|
||||
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "批量删除完成", "data": result})
|
||||
}
|
||||
@@ -184,5 +184,6 @@ type AuditLogResponse struct {
|
||||
Detail string `json:"detail"`
|
||||
Result string `json:"result"`
|
||||
IP string `json:"ip"`
|
||||
Location string `json:"location,omitempty"` // IP 归属地(城市库识别) // Logan.Li
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
@@ -119,6 +119,7 @@ type ActivationResponse struct {
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceInfo string `json:"device_info"`
|
||||
IP string `json:"ip"`
|
||||
Location string `json:"location,omitempty"` // IP 归属地(国家/省/市,中文) // Logan.Li
|
||||
Status string `json:"status"`
|
||||
IsOnline bool `json:"is_online"`
|
||||
ActivatedAt string `json:"activated_at"`
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"backend/utils"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -208,7 +210,8 @@ func (s *AnalyticsService) GetProductDistribution() ([]dto.ProductDistribution,
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetGeoDistribution 地理/网络分布(device_info.country 优先,否则按 IP 粗分) // Logan.Li
|
||||
// GetGeoDistribution 地理分布:GeoLite2 城市库按 IP 识别(国家/省/市,中文优先),
|
||||
// device_info 显式声明国家时优先;识别失败回退 公网/内网/未知 // Logan.Li
|
||||
func (s *AnalyticsService) GetGeoDistribution() ([]dto.GeoDistribution, error) {
|
||||
var acts []model.Activation
|
||||
s.db.Where("status = ?", "active").Find(&acts)
|
||||
@@ -261,6 +264,9 @@ func regionFromActivation(a model.Activation) string {
|
||||
if ip == "" {
|
||||
return "未知"
|
||||
}
|
||||
if loc := utils.LookupIPLocation(ip); loc != "" {
|
||||
return loc
|
||||
}
|
||||
parsed := net.ParseIP(ip)
|
||||
if parsed == nil {
|
||||
return "未知"
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package customer
|
||||
|
||||
import "backend/internal/model"
|
||||
|
||||
// BatchDeleteResult 批量删除结果 // Logan.Li
|
||||
type BatchDeleteResult struct {
|
||||
Deleted int `json:"deleted"`
|
||||
Skipped int `json:"skipped"`
|
||||
Failed int `json:"failed"`
|
||||
SkippedIDs []uint `json:"skipped_ids,omitempty"`
|
||||
}
|
||||
|
||||
// BatchDelete 批量删除客户:名下有 License 的跳过;其余软删除并清理产品关联 // Logan.Li
|
||||
func (s *CustomerService) BatchDelete(ids []uint) (*BatchDeleteResult, error) {
|
||||
result := &BatchDeleteResult{}
|
||||
seen := map[uint]struct{}{}
|
||||
for _, id := range ids {
|
||||
if id == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
|
||||
var licCount int64
|
||||
s.db.Model(&model.License{}).Where("customer_id = ?", id).Count(&licCount)
|
||||
if licCount > 0 {
|
||||
result.Skipped++
|
||||
result.SkippedIDs = append(result.SkippedIDs, id)
|
||||
continue
|
||||
}
|
||||
if err := s.db.Delete(&model.Customer{}, id).Error; err != nil {
|
||||
result.Failed++
|
||||
continue
|
||||
}
|
||||
s.db.Where("customer_id = ?", id).Delete(&model.CustomerProduct{})
|
||||
result.Deleted++
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -454,6 +454,7 @@ func toActivationResponse(a model.Activation) dto.ActivationResponse {
|
||||
DeviceName: a.DeviceName,
|
||||
DeviceInfo: a.DeviceInfo,
|
||||
IP: a.IP,
|
||||
Location: utils.LookupIPLocation(a.IP),
|
||||
Status: a.Status,
|
||||
IsOnline: a.IsOnline,
|
||||
ActivatedAt: a.ActivatedAt.Format("2006-01-02 15:04:05"),
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package order
|
||||
|
||||
import "backend/internal/model"
|
||||
|
||||
// BatchDeleteResult 批量删除结果 // Logan.Li
|
||||
type BatchDeleteResult struct {
|
||||
Deleted int `json:"deleted"`
|
||||
Skipped int `json:"skipped"`
|
||||
Failed int `json:"failed"`
|
||||
SkippedIDs []uint `json:"skipped_ids,omitempty"`
|
||||
}
|
||||
|
||||
// BatchDelete 批量删除订单:已支付(paid)为财务凭证默认跳过,其余状态可删 // Logan.Li
|
||||
func (s *OrderService) BatchDelete(ids []uint) (*BatchDeleteResult, error) {
|
||||
result := &BatchDeleteResult{}
|
||||
seen := map[uint]struct{}{}
|
||||
for _, id := range ids {
|
||||
if id == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
|
||||
var order model.Order
|
||||
if err := s.db.First(&order, id).Error; err != nil {
|
||||
result.Failed++
|
||||
continue
|
||||
}
|
||||
if order.Status == "paid" {
|
||||
result.Skipped++
|
||||
result.SkippedIDs = append(result.SkippedIDs, id)
|
||||
continue
|
||||
}
|
||||
if err := s.db.Delete(&model.Order{}, id).Error; err != nil {
|
||||
result.Failed++
|
||||
continue
|
||||
}
|
||||
result.Deleted++
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package product
|
||||
|
||||
import "backend/internal/model"
|
||||
|
||||
// BatchDeleteResult 批量删除结果 // Logan.Li
|
||||
type BatchDeleteResult struct {
|
||||
Deleted int `json:"deleted"`
|
||||
Skipped int `json:"skipped"`
|
||||
Failed int `json:"failed"`
|
||||
SkippedIDs []uint `json:"skipped_ids,omitempty"`
|
||||
}
|
||||
|
||||
// BatchDelete 批量删除产品:被 License 引用的跳过;未引用的连同其定价计划一并删除 // Logan.Li
|
||||
func (s *ProductService) BatchDelete(ids []uint) (*BatchDeleteResult, error) {
|
||||
result := &BatchDeleteResult{}
|
||||
seen := map[uint]struct{}{}
|
||||
for _, id := range ids {
|
||||
if id == 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
|
||||
var licCount int64
|
||||
s.db.Model(&model.License{}).Where("product_id = ?", id).Count(&licCount)
|
||||
if licCount > 0 {
|
||||
result.Skipped++
|
||||
result.SkippedIDs = append(result.SkippedIDs, id)
|
||||
continue
|
||||
}
|
||||
if err := s.db.Delete(&model.Product{}, id).Error; err != nil {
|
||||
result.Failed++
|
||||
continue
|
||||
}
|
||||
s.db.Where("product_id = ?", id).Delete(&model.Plan{})
|
||||
result.Deleted++
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"backend/internal/dto"
|
||||
"backend/internal/model"
|
||||
"backend/internal/service/payment"
|
||||
"backend/utils"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
@@ -728,6 +729,7 @@ func (s *SettingService) ListAuditLogs(page, pageSize int, action, targetType st
|
||||
Detail: l.Detail,
|
||||
Result: l.Result,
|
||||
IP: l.IP,
|
||||
Location: utils.LookupIPLocation(l.IP),
|
||||
CreatedAt: l.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user