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, } }