feat: 产品/客户/订单批量删除;接入 GeoLite2 城市库(地理分布/激活记录/审计日志显示归属地);仪表盘趋势图美化

This commit is contained in:
Logan.Li committed 2026-09-14 22:48:14 +08:00
1 parent 252ae14aef
commit 473dcd0aab
24 files changed
+431 -34

No files matched your search

@@ -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"),
+43
View File
@@ -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
}
+41
View File
@@ -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"),
})
}