@@ -112,26 +112,66 @@ func (s *SettingService) Security(ctx context.Context) (SecuritySettings, error)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// UpdateSecurity 校验并保存安全设置,随后刷新内存快照立即生效。
|
||||
func (s *SettingService) UpdateSecurity(ctx context.Context, in SecuritySettings) error {
|
||||
if err := validateSecurity(&in); err != nil {
|
||||
// SecurityPatch 是安全设置的部分更新;nil 字段沿用现值,只落库出现的字段,
|
||||
// 并发编辑不同字段因此互不回滚(2026-07-22 审查 #18)。
|
||||
type SecurityPatch struct {
|
||||
LoginFailLimit *int `json:"loginFailLimit"`
|
||||
LoginLockMinutes *int `json:"loginLockMinutes"`
|
||||
IPRateRPS *int `json:"ipRateRps"`
|
||||
IPRateBurst *int `json:"ipRateBurst"`
|
||||
RealIPHeader *string `json:"realIpHeader"`
|
||||
AppURL *string `json:"appUrl"`
|
||||
}
|
||||
|
||||
// merge 把非 nil 字段覆盖到 dst,返回被触碰字段的存储键集合。
|
||||
func (p SecurityPatch) merge(dst *SecuritySettings) map[string]bool {
|
||||
touched := map[string]bool{}
|
||||
fields := []struct {
|
||||
key string
|
||||
set func()
|
||||
on bool
|
||||
}{
|
||||
{settingSecLoginFailLimit, func() { dst.LoginFailLimit = *p.LoginFailLimit }, p.LoginFailLimit != nil},
|
||||
{settingSecLoginLockMin, func() { dst.LoginLockMinutes = *p.LoginLockMinutes }, p.LoginLockMinutes != nil},
|
||||
{settingSecIPRateRPS, func() { dst.IPRateRPS = *p.IPRateRPS }, p.IPRateRPS != nil},
|
||||
{settingSecIPRateBurst, func() { dst.IPRateBurst = *p.IPRateBurst }, p.IPRateBurst != nil},
|
||||
{settingSecRealIPHeader, func() { dst.RealIPHeader = *p.RealIPHeader }, p.RealIPHeader != nil},
|
||||
{settingSecAppURL, func() { dst.AppURL = *p.AppURL }, p.AppURL != nil},
|
||||
}
|
||||
for _, f := range fields {
|
||||
if f.on {
|
||||
f.set()
|
||||
touched[f.key] = true
|
||||
}
|
||||
}
|
||||
return touched
|
||||
}
|
||||
|
||||
// UpdateSecurity 把补丁合并到现值上整体校验,只落库出现的字段,再重读刷新快照。
|
||||
func (s *SettingService) UpdateSecurity(ctx context.Context, p SecurityPatch) error {
|
||||
cur, err := s.Security(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
touched := p.merge(&cur)
|
||||
if err := validateSecurity(&cur); err != nil {
|
||||
return err
|
||||
}
|
||||
kv := map[string]string{
|
||||
settingSecLoginFailLimit: strconv.Itoa(in.LoginFailLimit),
|
||||
settingSecLoginLockMin: strconv.Itoa(in.LoginLockMinutes),
|
||||
settingSecIPRateRPS: strconv.Itoa(in.IPRateRPS),
|
||||
settingSecIPRateBurst: strconv.Itoa(in.IPRateBurst),
|
||||
settingSecRealIPHeader: in.RealIPHeader,
|
||||
settingSecAppURL: in.AppURL,
|
||||
settingSecLoginFailLimit: strconv.Itoa(cur.LoginFailLimit),
|
||||
settingSecLoginLockMin: strconv.Itoa(cur.LoginLockMinutes),
|
||||
settingSecIPRateRPS: strconv.Itoa(cur.IPRateRPS),
|
||||
settingSecIPRateBurst: strconv.Itoa(cur.IPRateBurst),
|
||||
settingSecRealIPHeader: cur.RealIPHeader,
|
||||
settingSecAppURL: cur.AppURL,
|
||||
}
|
||||
for key, value := range kv {
|
||||
if err := s.set(ctx, key, value); err != nil {
|
||||
for key := range touched {
|
||||
if err := s.set(ctx, key, kv[key]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.security.Store(in)
|
||||
return nil
|
||||
// 重读而非直接 Store 合并值:并发补丁各写各键,重读保证快照收敛到库内最终值
|
||||
return s.ReloadSecurity(ctx)
|
||||
}
|
||||
|
||||
// ReloadSecurity 从库加载安全设置到内存快照;进程启动时调用一次。
|
||||
|
||||
Reference in New Issue
Block a user