第 5 阶段:纠纷、通知与后台-1

This commit is contained in:
yml
2026-05-22 16:34:32 +08:00
parent 9ebdfab078
commit 99f9df7bcd
32 changed files with 1711 additions and 8 deletions
@@ -0,0 +1,154 @@
package systemconfig
import (
"encoding/json"
"errors"
"hfb_sys/backend/internal/model"
"gorm.io/datatypes"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type Repository struct {
db *gorm.DB
}
type AuditMeta struct {
IP string
UserAgent string
}
type defaultConfig struct {
Key string
Value string
Description string
}
var defaultConfigs = []defaultConfig{
{Key: "handoff.owner_submit_timeout_minutes", Value: "30", Description: "号主待交接超时分钟数"},
{Key: "handoff.renter_confirm_timeout_minutes", Value: "30", Description: "租客待确认收号超时分钟数"},
{Key: "handoff.owner_return_confirm_timeout_minutes", Value: "120", Description: "号主待确认归还超时分钟数"},
{Key: "order.return_overdue_grace_minutes", Value: "10", Description: "租期到期后归还宽限分钟数"},
{Key: "deposit.min_amount", Value: "50", Description: "发布租号最低押金"},
{Key: "risk.sms_limit_per_phone_hour", Value: "5", Description: "单手机号每小时短信验证码次数"},
{Key: "risk.sms_limit_per_ip_hour", Value: "20", Description: "单 IP 每小时短信验证码次数"},
{Key: "realname.required_for_order", Value: "false", Description: "下单是否必须完成实名认证"},
{Key: "settlement.platform_fee_rate", Value: "0.00", Description: "平台抽成比例"},
{Key: "settlement.owner_cycle_days", Value: "0", Description: "号主结算周期天数"},
{Key: "withdraw.min_amount", Value: "100", Description: "提现最低金额预留"},
}
func NewRepository(db *gorm.DB) *Repository {
return &Repository{db: db}
}
func (r *Repository) List() ([]ConfigDTO, error) {
if err := r.ensureDefaults(); err != nil {
return nil, err
}
var rows []model.SystemConfig
if err := r.db.Order("`key` ASC").Find(&rows).Error; err != nil {
return nil, err
}
items := make([]ConfigDTO, 0, len(rows))
for _, row := range rows {
items = append(items, toDTO(row))
}
return items, nil
}
func (r *Repository) Update(actorID uint64, key string, req UpdateRequest, meta AuditMeta) (*ConfigDTO, error) {
var row model.SystemConfig
err := r.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("`key` = ?", key).First(&row).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
row = model.SystemConfig{
Key: key,
Value: req.Value,
Description: req.Description,
UpdatedBy: &actorID,
}
if err := tx.Create(&row).Error; err != nil {
return err
}
} else {
return err
}
} else {
before := row.Value
row.Value = req.Value
if req.Description != "" {
row.Description = req.Description
}
row.UpdatedBy = &actorID
if err := tx.Save(&row).Error; err != nil {
return err
}
if err := appendAuditLog(tx, actorID, "system_config.update", row.ID, meta, map[string]any{
"key": row.Key,
"before": before,
"after": row.Value,
}); err != nil {
return err
}
return nil
}
return appendAuditLog(tx, actorID, "system_config.create", row.ID, meta, map[string]any{
"key": row.Key,
"value": row.Value,
})
})
if err != nil {
return nil, err
}
dto := toDTO(row)
return &dto, nil
}
func (r *Repository) ensureDefaults() error {
return r.db.Transaction(func(tx *gorm.DB) error {
for _, item := range defaultConfigs {
row := model.SystemConfig{
Key: item.Key,
Value: item.Value,
Description: item.Description,
}
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&row).Error; err != nil {
return err
}
}
return nil
})
}
func appendAuditLog(tx *gorm.DB, actorID uint64, action string, bizID uint64, meta AuditMeta, detail map[string]any) error {
raw, err := json.Marshal(detail)
if err != nil {
return err
}
row := model.AuditLog{
ActorType: "admin",
ActorID: actorID,
Action: action,
BizType: "system_config",
BizID: &bizID,
IP: meta.IP,
UserAgent: meta.UserAgent,
Detail: datatypes.JSON(raw),
}
return tx.Create(&row).Error
}
func toDTO(row model.SystemConfig) ConfigDTO {
return ConfigDTO{
ID: row.ID,
Key: row.Key,
Value: row.Value,
Description: row.Description,
UpdatedBy: row.UpdatedBy,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}
}