Files
opsx-licence/backend/internal/service/setting/setting_service.go
T
Logan.Li 7c146cb639 fix: 修复支付页测试/生产 Product 映射串用与无法保存
切换环境时先暂存再切换;后端 Profile 不再串用另一侧商品;支持一键填充 OpsXshell 与跨环境复制,调试使用当前页映射。
2026-08-12 23:35:27 +08:00

740 lines
21 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package setting
import (
"backend/internal/dto"
"backend/internal/model"
"backend/internal/service/payment"
"errors"
"fmt"
"net/url"
"regexp"
"strings"
"time"
"gorm.io/gorm"
)
// SettingService 系统设置业务逻辑层 // Logan.Li
type SettingService struct {
db *gorm.DB
}
// NewSettingService 创建 service 实例 // Logan.Li
func NewSettingService(db *gorm.DB) *SettingService {
return &SettingService{db: db}
}
// Get 获取系统设置(单行) // Logan.Li
func (s *SettingService) Get() (*model.Setting, error) {
var setting model.Setting
if err := s.db.First(&setting).Error; err != nil {
return nil, errors.New("系统设置不存在")
}
return &setting, nil
}
// Update 更新系统设置 // Logan.Li
func (s *SettingService) Update(req dto.SettingUpdate) (*model.Setting, error) {
var setting model.Setting
if err := s.db.First(&setting).Error; err != nil {
return nil, errors.New("系统设置不存在")
}
updates := map[string]interface{}{}
if req.SystemName != nil {
updates["system_name"] = *req.SystemName
}
if req.DefaultDuration != nil {
updates["default_duration"] = *req.DefaultDuration
}
if req.DefaultMaxActivations != nil {
updates["default_max_activations"] = *req.DefaultMaxActivations
}
if req.ExpireReminderDays != nil {
updates["expire_reminder_days"] = *req.ExpireReminderDays
}
if req.APIRateLimit != nil {
updates["api_rate_limit"] = *req.APIRateLimit
}
if req.LogRetentionDays != nil {
updates["log_retention_days"] = *req.LogRetentionDays
}
if len(updates) > 0 {
if err := s.db.Model(&setting).Updates(updates).Error; err != nil {
return nil, errors.New("更新系统设置失败")
}
}
s.db.First(&setting)
return &setting, nil
}
// GetWaffo 获取 Waffo 支付集成配置(私钥仅返回是否已配置) // Logan.Li
func (s *SettingService) GetWaffo() (*dto.WaffoSettingsResponse, error) {
cfg := payment.LoadWaffoConfig(s.db)
var gw model.PaymentGatewayConfig
dbHas := s.db.Where("gateway = ?", "waffo").First(&gw).Error == nil
enabled := true
if dbHas {
enabled = gw.Enabled
}
hint := "env"
if dbHas && strings.TrimSpace(gw.PublicKey) != "" {
hint = "db"
} else if dbHas {
hint = "mixed"
}
meta := payment.ParseWaffoPublicMeta(gw.PublicKey)
env := payment.NormalizeWaffoEnvironment(cfg.Environment)
meta.ApplyEnvProfile(env)
mappings := buildProductMappings(cfg, meta.ProductMappings)
productIDs := cloneProductIDs(cfg.ProductIDs)
bag := payment.ParseWaffoSecretBag(gw.SecretKey, env)
profiles := map[string]dto.WaffoEnvUI{
"test": buildEnvUI(meta, bag, "test"),
"live": buildEnvUI(meta, bag, "live"),
}
return &dto.WaffoSettingsResponse{
Enabled: enabled,
MerchantID: cfg.MerchantID,
StoreID: cfg.StoreID,
StoreSlug: cfg.StoreSlug,
Environment: env,
SandboxMode: env == "test",
Currency: cfg.Currency,
ProductID: firstNonEmpty(meta.ProductID, cfg.ProductID),
ProductIDYearly: firstNonEmpty(meta.ProductIDYearly, cfg.ProductIDYearly),
ProductIDPerpetual: firstNonEmpty(meta.ProductIDPerpetual, cfg.ProductIDPerpetual),
ProductIDs: productIDs,
ProductMappings: mappings,
SuccessURL: cfg.SuccessURL,
WebhookURL: payment.DefaultWebhookURL,
PrivateKeyConfigured: bag.SecretForEnv(env) != "",
PrivateKeyByEnv: map[string]bool{
"test": bag.SecretForEnv("test") != "",
"live": bag.SecretForEnv("live") != "",
},
Profiles: profiles,
SourceHint: hint,
}, nil
}
// buildEnvUI 组装单环境预览(不得串用另一环境顶层映射) // Logan.Li
func buildEnvUI(meta payment.WaffoPublicMeta, bag payment.WaffoSecretBag, env string) dto.WaffoEnvUI {
cp := meta
// 先清空商品字段,再叠该环境 Profile,避免未配置环境显示成另一侧数据 // Logan.Li
cp.ProductID = ""
cp.ProductIDYearly = ""
cp.ProductIDPerpetual = ""
cp.ProductIDs = nil
cp.ProductMappings = nil
cp.ApplyEnvProfile(env)
cfgLike := payment.WaffoConfig{
ProductID: cp.ProductID,
ProductIDYearly: cp.ProductIDYearly,
ProductIDPerpetual: cp.ProductIDPerpetual,
ProductIDs: cloneProductIDs(cp.ProductIDs),
}
if len(cp.ProductMappings) > 0 {
if cfgLike.ProductIDs == nil {
cfgLike.ProductIDs = map[string]string{}
}
for _, m := range cp.ProductMappings {
k := strings.TrimSpace(strings.ToLower(m.PlanKey))
v := strings.TrimSpace(m.ProductID)
if k != "" && v != "" {
cfgLike.ProductIDs[k] = v
}
}
}
return dto.WaffoEnvUI{
ProductMappings: buildProductMappings(cfgLike, cp.ProductMappings),
ProductID: firstNonEmpty(cp.ProductID, cfgLike.ProductIDs["perpetual"], cfgLike.ProductIDs["lifetime"], cfgLike.ProductIDs["yearly"]),
ProductIDYearly: firstNonEmpty(cp.ProductIDYearly, cfgLike.ProductIDs["yearly"]),
ProductIDPerpetual: firstNonEmpty(cp.ProductIDPerpetual, cfgLike.ProductIDs["perpetual"], cfgLike.ProductIDs["lifetime"]),
PrivateKeyConfigured: bag.SecretForEnv(env) != "",
}
}
// UpdateWaffo 保存 Waffo 配置到 payment_gateway_configs(后台优先于 .env) // Logan.Li
func (s *SettingService) UpdateWaffo(req dto.WaffoSettingsUpdate) (*dto.WaffoSettingsResponse, error) {
var gw model.PaymentGatewayConfig
err := s.db.Where("gateway = ?", "waffo").First(&gw).Error
if err != nil {
gw = model.PaymentGatewayConfig{
Gateway: "waffo",
Enabled: true,
SandboxMode: true,
Currency: "CNY",
Priority: 0,
}
}
oldMeta := payment.ParseWaffoPublicMeta(gw.PublicKey)
oldEnv := payment.NormalizeWaffoEnvironment(oldMeta.Environment)
if oldEnv == "test" && !gw.SandboxMode {
oldEnv = "live"
}
productIDs, mappings := mergeProductIDInputs(req)
newEnv := payment.NormalizeWaffoEnvironment(req.Environment)
meta := oldMeta
if meta.EnvProfiles == nil {
meta.EnvProfiles = map[string]payment.WaffoEnvProfile{}
}
meta.MerchantID = strings.TrimSpace(req.MerchantID)
meta.StoreID = strings.TrimSpace(req.StoreID)
meta.StoreSlug = strings.TrimSpace(req.StoreSlug)
meta.SuccessURL = strings.TrimSpace(req.SuccessURL)
meta.Currency = strings.TrimSpace(req.Currency)
if meta.Currency == "" {
meta.Currency = "CNY"
}
meta.CurrencyYearly = firstNonEmpty(strings.TrimSpace(req.CurrencyYearly), currencyFromMappings(mappings, "yearly"), meta.Currency)
meta.CurrencyPerpetual = firstNonEmpty(strings.TrimSpace(req.CurrencyPerpetual), currencyFromMappings(mappings, "perpetual"), currencyFromMappings(mappings, "lifetime"), meta.Currency)
meta.Environment = newEnv
meta.ProductID = strings.TrimSpace(req.ProductID)
meta.ProductIDYearly = strings.TrimSpace(req.ProductIDYearly)
meta.ProductIDPerpetual = strings.TrimSpace(req.ProductIDPerpetual)
meta.ProductIDs = productIDs
meta.ProductMappings = toPaymentMappings(mappings)
// 兼容字段与可扩展映射对齐 // Logan.Li
if meta.ProductIDYearly == "" {
meta.ProductIDYearly = productIDs["yearly"]
}
if meta.ProductIDPerpetual == "" {
meta.ProductIDPerpetual = firstNonEmpty(productIDs["perpetual"], productIDs["lifetime"])
}
if meta.ProductID == "" {
meta.ProductID = firstNonEmpty(meta.ProductIDPerpetual, meta.ProductIDYearly, productIDs["default"])
}
// 当前环境映射写入 Profile,保留另一环境配置 // Logan.Li
meta.CaptureEnvProfile(newEnv)
if newEnv == "live" {
gw.SandboxMode = false
} else {
gw.SandboxMode = true
}
encoded, err := payment.EncodeWaffoPublicMeta(meta)
if err != nil {
return nil, err
}
gw.PublicKey = encoded
gw.Currency = meta.Currency
gw.Priority = 0
if req.Enabled != nil {
gw.Enabled = *req.Enabled
} else {
gw.Enabled = true
}
bag := payment.ParseWaffoSecretBag(gw.SecretKey, oldEnv)
if req.ClearPrivateKey {
bag.ClearSecretForEnv(newEnv)
} else if pk := strings.TrimSpace(req.PrivateKey); pk != "" {
if _, err := payment.ParseMerchantPrivateKey(pk); err != nil {
return nil, errors.New("私钥无法解析: " + err.Error())
}
bag.SetSecretForEnv(newEnv, pk)
}
encodedSecret, err := payment.EncodeWaffoSecretBag(bag)
if err != nil {
return nil, errors.New("保存私钥失败")
}
gw.SecretKey = encodedSecret
if err := s.db.Save(&gw).Error; err != nil {
return nil, errors.New("保存 Waffo 配置失败")
}
_ = s.db.Model(&model.PaymentGatewayConfig{}).
Where("gateway <> ? AND enabled = ?", "waffo", true).
Update("priority", 100).Error
return s.GetWaffo()
}
// DebugWaffo 用当前 DB/env 配置轻量探测 create-session(不回显私钥) // Logan.Li
func (s *SettingService) DebugWaffo(req dto.WaffoDebugRequest) *dto.WaffoDebugResponse {
cfg := payment.LoadWaffoConfig(s.db)
// 页面未保存的环境 / 映射优先覆盖,避免「填了映射却仍用旧 Product」 // Logan.Li
if strings.TrimSpace(req.Environment) != "" {
env := payment.NormalizeWaffoEnvironment(req.Environment)
cfg.Environment = env
var gw model.PaymentGatewayConfig
if err := s.db.Where("gateway = ?", "waffo").First(&gw).Error; err == nil {
bag := payment.ParseWaffoSecretBag(gw.SecretKey, env)
cfg.PrivateKey = bag.SecretForEnv(env)
}
}
applyDebugProductOverrides(&cfg, req)
planType := strings.TrimSpace(strings.ToLower(req.PlanType))
if planType == "" {
planType = "yearly"
}
productID := strings.TrimSpace(req.ProductID)
if productID == "" {
productID = payment.ResolveProductID(cfg, planType)
}
authMode := "none"
if cfg.HasAPIKeyAuth() {
authMode = "api_key"
} else if strings.TrimSpace(cfg.StoreSlug) != "" {
authMode = "store_slug"
}
out := &dto.WaffoDebugResponse{
AuthMode: authMode,
Environment: cfg.Environment,
PlanType: planType,
ProductIDUsed: productID,
PrivateKeyPresent: strings.TrimSpace(cfg.PrivateKey) != "",
MerchantIDPresent: strings.TrimSpace(cfg.MerchantID) != "",
StoreSlugPresent: strings.TrimSpace(cfg.StoreSlug) != "",
}
if productID == "" {
out.OK = false
out.ErrorSummary = "未配置可用的 Waffo Product ID(请在当前环境映射表填写后先保存,或点「填充 OpsXshell 商品」)"
return out
}
if authMode == "none" {
out.OK = false
out.ErrorSummary = "未配置商户 ID/私钥,且缺少 Store Slug"
return out
}
buyer := strings.TrimSpace(req.BuyerEmail)
if buyer == "" {
buyer = "debug@opsxshell.local"
}
orderNo := fmt.Sprintf("debug-%d", time.Now().Unix())
successURL := cfg.SuccessURL
if successURL == "" {
successURL = payment.DefaultSuccessURL
}
currency := payment.ResolveCurrency(cfg, planType)
if c := strings.TrimSpace(req.Currency); c != "" && planType != "yearly" && planType != "perpetual" && planType != "lifetime" {
currency = c
}
session, err := payment.CreateCheckoutSession(cfg, payment.CreateSessionRequest{
ProductID: productID,
ProductType: "onetime",
Currency: currency,
BuyerEmail: buyer,
SuccessURL: successURL,
OrderMerchantExternalID: orderNo,
Metadata: map[string]string{
"debug": "true",
"planType": planType,
"orderNo": orderNo,
},
ExpiresInSeconds: 300,
StoreSlug: cfg.StoreSlug,
Environment: cfg.Environment,
})
if err != nil {
out.OK = false
out.ErrorSummary = sanitizeDebugError(err.Error())
out.HTTPStatus = extractHTTPStatus(err.Error())
if strings.Contains(out.ErrorSummary, "not approved for production") {
out.ErrorSummary += "(Waffo 门店尚未开通生产收款:请先在测试环境验证;生产需在 Waffo Dashboard 完成 Go-Live)"
}
return out
}
out.OK = true
out.HTTPStatus = 200
out.SessionID = session.SessionID
if u, err := url.Parse(session.CheckoutURL); err == nil && u.Host != "" {
out.CheckoutURLHost = u.Host
}
return out
}
// applyDebugProductOverrides 把调试请求中的映射叠到运行时配置 // Logan.Li
func applyDebugProductOverrides(cfg *payment.WaffoConfig, req dto.WaffoDebugRequest) {
if cfg == nil {
return
}
if cfg.ProductIDs == nil {
cfg.ProductIDs = map[string]string{}
}
for _, m := range req.ProductMappings {
k := strings.TrimSpace(strings.ToLower(m.PlanKey))
v := strings.TrimSpace(m.ProductID)
if k == "" || v == "" {
continue
}
cfg.ProductIDs[k] = v
switch k {
case "yearly", "monthly":
cfg.ProductIDYearly = v
if c := strings.TrimSpace(m.Currency); c != "" {
cfg.CurrencyYearly = c
}
case "perpetual", "lifetime":
cfg.ProductIDPerpetual = v
cfg.ProductID = v
if c := strings.TrimSpace(m.Currency); c != "" {
cfg.CurrencyPerpetual = c
}
}
}
if y := strings.TrimSpace(req.ProductIDYearly); y != "" {
cfg.ProductIDYearly = y
cfg.ProductIDs["yearly"] = y
}
if p := strings.TrimSpace(req.ProductIDPerpetual); p != "" {
cfg.ProductIDPerpetual = p
cfg.ProductID = p
cfg.ProductIDs["perpetual"] = p
}
if c := strings.TrimSpace(req.Currency); c != "" {
cfg.Currency = c
}
if c := strings.TrimSpace(req.CurrencyYearly); c != "" {
cfg.CurrencyYearly = c
}
if c := strings.TrimSpace(req.CurrencyPerpetual); c != "" {
cfg.CurrencyPerpetual = c
}
}
func mergeProductIDInputs(req dto.WaffoSettingsUpdate) (map[string]string, []dto.WaffoProductMapping) {
ids := map[string]string{}
var mappings []dto.WaffoProductMapping
if len(req.ProductMappings) > 0 {
for _, m := range req.ProductMappings {
k := strings.TrimSpace(strings.ToLower(m.PlanKey))
v := strings.TrimSpace(m.ProductID)
if k == "" {
continue
}
mappings = append(mappings, dto.WaffoProductMapping{
PlanKey: k,
Label: strings.TrimSpace(m.Label),
ProductID: v,
Currency: strings.ToUpper(strings.TrimSpace(m.Currency)),
})
if v != "" {
ids[k] = v
}
}
}
for k, v := range req.ProductIDs {
k = strings.TrimSpace(strings.ToLower(k))
v = strings.TrimSpace(v)
if k == "" || v == "" {
continue
}
ids[k] = v
}
if y := strings.TrimSpace(req.ProductIDYearly); y != "" {
ids["yearly"] = y
}
if p := strings.TrimSpace(req.ProductIDPerpetual); p != "" {
ids["perpetual"] = p
if _, ok := ids["lifetime"]; !ok {
ids["lifetime"] = p
}
}
if d := strings.TrimSpace(req.ProductID); d != "" {
if _, ok := ids["default"]; !ok {
ids["default"] = d
}
if _, ok := ids["perpetual"]; !ok {
ids["perpetual"] = d
}
}
if len(mappings) == 0 {
mappings = mappingsFromIDs(ids, nil, nil, payment.WaffoConfig{})
} else {
// 用最终 ids 回填表格行 product_id,避免只改了兼容字段 // Logan.Li
for i := range mappings {
if v := ids[mappings[i].PlanKey]; v != "" {
mappings[i].ProductID = v
}
}
}
return ids, mappings
}
func buildProductMappings(cfg payment.WaffoConfig, stored []payment.WaffoProductMapping) []dto.WaffoProductMapping {
labelHint := map[string]string{}
currencyHint := map[string]string{}
for _, m := range stored {
k := strings.TrimSpace(strings.ToLower(m.PlanKey))
if k == "" {
continue
}
if l := strings.TrimSpace(m.Label); l != "" {
labelHint[k] = l
}
if c := strings.TrimSpace(m.Currency); c != "" {
currencyHint[k] = strings.ToUpper(c)
}
}
// 若 stored 有完整行(含空 product 也应展示),优先按 stored 顺序回显 // Logan.Li
if len(stored) > 0 {
out := make([]dto.WaffoProductMapping, 0, len(stored))
seen := map[string]bool{}
for _, m := range stored {
k := strings.TrimSpace(strings.ToLower(m.PlanKey))
if k == "" || seen[k] {
continue
}
seen[k] = true
pid := strings.TrimSpace(m.ProductID)
if pid == "" {
pid = strings.TrimSpace(cfg.ProductIDs[k])
}
cur := strings.TrimSpace(m.Currency)
if cur == "" {
cur = currencyHint[k]
}
if cur == "" {
switch k {
case "yearly", "monthly":
cur = cfg.CurrencyYearly
case "perpetual", "lifetime":
cur = cfg.CurrencyPerpetual
}
}
out = append(out, dto.WaffoProductMapping{
PlanKey: k,
Label: firstNonEmpty(strings.TrimSpace(m.Label), labelHint[k]),
ProductID: pid,
Currency: strings.ToUpper(strings.TrimSpace(cur)),
})
}
if len(out) > 0 {
return out
}
}
return mappingsFromIDs(cfg.ProductIDs, labelHint, currencyHint, cfg)
}
func mappingsFromIDs(ids map[string]string, labels, currencies map[string]string, cfg payment.WaffoConfig) []dto.WaffoProductMapping {
if len(ids) == 0 {
return []dto.WaffoProductMapping{
{PlanKey: "yearly", Label: "包年", Currency: "CNY"},
{PlanKey: "perpetual", Label: "永久", Currency: "USD"},
}
}
defaultLabels := map[string]string{
"yearly": "包年",
"monthly": "包月",
"perpetual": "永久",
"lifetime": "终身",
"trial": "试用",
"default": "默认",
}
// 稳定顺序:常用 key 优先,其余按字典序 // Logan.Li
order := []string{"trial", "yearly", "monthly", "perpetual", "lifetime", "default"}
seen := map[string]bool{}
out := make([]dto.WaffoProductMapping, 0, len(ids))
appendOne := func(k string) {
v := strings.TrimSpace(ids[k])
if v == "" || seen[k] {
return
}
seen[k] = true
label := ""
if labels != nil {
label = labels[k]
}
if label == "" {
label = defaultLabels[k]
}
cur := ""
if currencies != nil {
cur = currencies[k]
}
if cur == "" {
switch k {
case "yearly", "monthly":
cur = cfg.CurrencyYearly
case "perpetual", "lifetime":
cur = cfg.CurrencyPerpetual
default:
cur = cfg.Currency
}
}
out = append(out, dto.WaffoProductMapping{
PlanKey: k,
Label: label,
ProductID: v,
Currency: strings.ToUpper(strings.TrimSpace(cur)),
})
}
for _, k := range order {
appendOne(k)
}
rest := make([]string, 0)
for k := range ids {
k = strings.TrimSpace(strings.ToLower(k))
if k == "" || seen[k] || strings.TrimSpace(ids[k]) == "" {
continue
}
rest = append(rest, k)
}
for i := 0; i < len(rest); i++ {
for j := i + 1; j < len(rest); j++ {
if rest[j] < rest[i] {
rest[i], rest[j] = rest[j], rest[i]
}
}
}
for _, k := range rest {
appendOne(k)
}
if len(out) == 0 {
return []dto.WaffoProductMapping{
{PlanKey: "yearly", Label: "包年", Currency: "CNY"},
{PlanKey: "perpetual", Label: "永久", Currency: "USD"},
}
}
return out
}
func toPaymentMappings(in []dto.WaffoProductMapping) []payment.WaffoProductMapping {
out := make([]payment.WaffoProductMapping, 0, len(in))
for _, m := range in {
k := strings.TrimSpace(strings.ToLower(m.PlanKey))
if k == "" {
continue
}
out = append(out, payment.WaffoProductMapping{
PlanKey: k,
Label: strings.TrimSpace(m.Label),
ProductID: strings.TrimSpace(m.ProductID),
Currency: strings.ToUpper(strings.TrimSpace(m.Currency)),
})
}
return out
}
// currencyFromMappings 从映射表取指定 plan 的货币 // Logan.Li
func currencyFromMappings(mappings []dto.WaffoProductMapping, plan string) string {
plan = strings.TrimSpace(strings.ToLower(plan))
for _, m := range mappings {
if strings.TrimSpace(strings.ToLower(m.PlanKey)) == plan {
return strings.ToUpper(strings.TrimSpace(m.Currency))
}
}
return ""
}
func cloneProductIDs(in map[string]string) map[string]string {
if len(in) == 0 {
return map[string]string{}
}
out := make(map[string]string, len(in))
for k, v := range in {
k = strings.TrimSpace(strings.ToLower(k))
v = strings.TrimSpace(v)
if k == "" || v == "" {
continue
}
out[k] = v
}
return out
}
func firstNonEmpty(values ...string) string {
for _, v := range values {
if s := strings.TrimSpace(v); s != "" {
return s
}
}
return ""
}
var httpStatusRe = regexp.MustCompile(`\((\d{3})\)`)
func extractHTTPStatus(msg string) int {
m := httpStatusRe.FindStringSubmatch(msg)
if len(m) < 2 {
return 0
}
var n int
fmt.Sscanf(m[1], "%d", &n)
return n
}
func sanitizeDebugError(msg string) string {
msg = strings.TrimSpace(msg)
lower := strings.ToLower(msg)
// 防止误回显 PEM / 私钥片段 // Logan.Li
if strings.Contains(lower, "private key") || strings.Contains(msg, "BEGIN ") {
if idx := strings.Index(lower, "private"); idx >= 0 {
return "私钥相关错误(详情已隐藏)"
}
}
if len(msg) > 300 {
return msg[:300]
}
return msg
}
// ============ 审计日志 ============
// ListAuditLogs 分页查询审计日志 // Logan.Li
func (s *SettingService) ListAuditLogs(page, pageSize int, action, targetType string) ([]dto.AuditLogResponse, int64, error) {
var logs []model.AuditLog
var total int64
query := s.db.Model(&model.AuditLog{})
if action != "" {
query = query.Where("action = ?", action)
}
if targetType != "" {
query = query.Where("target_type = ?", targetType)
}
query.Count(&total)
if err := query.Order("created_at DESC").
Offset((page - 1) * pageSize).Limit(pageSize).
Find(&logs).Error; err != nil {
return nil, 0, err
}
results := make([]dto.AuditLogResponse, 0, len(logs))
for _, l := range logs {
results = append(results, dto.AuditLogResponse{
ID: l.ID,
Username: l.Username,
Action: l.Action,
TargetType: l.TargetType,
TargetID: l.TargetID,
TargetName: l.TargetName,
Detail: l.Detail,
Result: l.Result,
IP: l.IP,
CreatedAt: l.CreatedAt.Format("2006-01-02 15:04:05"),
})
}
return results, total, nil
}
// ClearAuditLogs 清空审计日志 // Logan.Li
func (s *SettingService) ClearAuditLogs() error {
return s.db.Exec("DELETE FROM audit_logs").Error
}