250 lines
6.8 KiB
Go
250 lines
6.8 KiB
Go
package systemconfig
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
|
|
"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: "提现最低金额预留"},
|
|
{Key: homeAnnouncementsConfigKey, Value: defaultHomeAnnouncementsConfigValue(), Description: "移动端首页公告 JSON 数组"},
|
|
{Key: homeBannersConfigKey, Value: defaultHomeBannersConfigValue(), Description: "移动端首页轮播图 JSON 数组"},
|
|
{Key: publishOptionsConfigKey, Value: defaultPublishOptionsConfigValue(), Description: "发布页选项配置 JSON"},
|
|
}
|
|
|
|
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) FindValue(key string) (string, error) {
|
|
if err := r.ensureDefaults(); err != nil {
|
|
return "", err
|
|
}
|
|
var row model.SystemConfig
|
|
if err := r.db.Where("`key` = ?", key).First(&row).Error; err != nil {
|
|
return "", err
|
|
}
|
|
return row.Value, 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
|
|
}
|
|
if item.Key == publishOptionsConfigKey {
|
|
if err := updateLegacyPublishOptions(tx, item); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func updateLegacyPublishOptions(tx *gorm.DB, item defaultConfig) error {
|
|
var row model.SystemConfig
|
|
if err := tx.Where("`key` = ?", item.Key).First(&row).Error; err != nil {
|
|
return err
|
|
}
|
|
if isLegacyPublishOptionsValue(row.Value) {
|
|
row.Value = item.Value
|
|
row.Description = item.Description
|
|
return tx.Save(&row).Error
|
|
}
|
|
var options PublishOptionsDTO
|
|
if err := json.Unmarshal([]byte(row.Value), &options); err != nil {
|
|
row.Value = item.Value
|
|
row.Description = item.Description
|
|
return tx.Save(&row).Error
|
|
}
|
|
before := row.Value
|
|
isOldPublishOptionsSchema := !strings.Contains(before, `"ban_record_options"`)
|
|
normalizePublishOptions(&options)
|
|
if isOldPublishOptionsSchema {
|
|
applyConditionalBanEvidenceDefault(&options)
|
|
}
|
|
raw, err := json.Marshal(options)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
row.Value = string(raw)
|
|
row.Description = item.Description
|
|
if row.Value == before {
|
|
return nil
|
|
}
|
|
return tx.Save(&row).Error
|
|
}
|
|
|
|
func applyConditionalBanEvidenceDefault(options *PublishOptionsDTO) {
|
|
defaults := DefaultPublishOptions()
|
|
defaultSlots := make(map[string]PublishScreenshotSlot, len(defaults.ScreenshotSlots))
|
|
for _, item := range defaults.ScreenshotSlots {
|
|
defaultSlots[item.Key] = item
|
|
}
|
|
for index, item := range options.ScreenshotSlots {
|
|
if item.Key != "tencentSecurity" {
|
|
continue
|
|
}
|
|
if next, ok := defaultSlots[item.Key]; ok {
|
|
options.ScreenshotSlots[index] = next
|
|
}
|
|
}
|
|
}
|
|
|
|
func isLegacyPublishOptionsValue(value string) bool {
|
|
legacyMarkers := []string{
|
|
`"kit5"`,
|
|
"5级全装套",
|
|
"6级子弹",
|
|
"6头数量",
|
|
"6甲数量",
|
|
"0.26元/发",
|
|
"0.8元/发",
|
|
"çº",
|
|
"å¼",
|
|
"å…",
|
|
"å¤",
|
|
"ä½",
|
|
"é«",
|
|
"ï¼",
|
|
}
|
|
for _, marker := range legacyMarkers {
|
|
if strings.Contains(value, marker) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|