fix: 记账可靠性——关停排空队列、失败重试、扣费不再静默;余额改 SQL 原子递减
- Recorder.Close 改为停止接收+排空残留,channel 不再 close(杜绝 send-on-closed panic),Close 后的 Record 走同步落库 - flush 瞬时 DB 错误重试 3 次,不再整批丢弃;事务内扣费/流水/日聚合失败一律打日志,明细与扣费不再静默脱节 - 扣费与管理员调余额改 SQL 侧原子加减(SQLite 驱动不支持 FOR UPDATE,读-改-写并发会丢扣费);调余额 ref_id 改纳秒不再撞唯一索引 - 最后一个启用的管理员不可被降级/禁用(管理面锁死后 reset-admin-password 救不回)
This commit is contained in:
1 parent
7c597a6aa0
commit
984a31cbc5
3 files changed
+292
-65
No files matched your search
@@ -115,6 +115,34 @@ func (h *Handler) AdminPatchUser(c *gin.Context) {
|
||||
}
|
||||
updates["status"] = *req.Status
|
||||
}
|
||||
// 最后一个启用的管理员不可被降级/禁用:否则管理面被锁死,
|
||||
// 且 reset-admin-password CLI 只在 admin 角色里找目标,救不回来。
|
||||
if req.Role != nil || req.Status != nil {
|
||||
var target store.User
|
||||
if err := h.a.DB.First(&target, id).Error; err != nil {
|
||||
resp.Fail(c, http.StatusNotFound, "user not found")
|
||||
return
|
||||
}
|
||||
if target.Role == store.RoleAdmin && target.Status == store.UserStatusActive {
|
||||
newRole, newStatus := target.Role, target.Status
|
||||
if req.Role != nil {
|
||||
newRole = *req.Role
|
||||
}
|
||||
if req.Status != nil {
|
||||
newStatus = *req.Status
|
||||
}
|
||||
if newRole != store.RoleAdmin || newStatus != store.UserStatusActive {
|
||||
var admins int64
|
||||
h.a.DB.Model(&store.User{}).
|
||||
Where("role = ? AND status = ? AND id != ?", store.RoleAdmin, store.UserStatusActive, id).
|
||||
Count(&admins)
|
||||
if admins == 0 {
|
||||
resp.Fail(c, http.StatusBadRequest, "cannot demote or disable the last active admin")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 模型限制(jsonb):手动序列化
|
||||
if req.AllowedModels != nil {
|
||||
raw, _ := json.Marshal(*req.AllowedModels)
|
||||
@@ -166,16 +194,17 @@ func (h *Handler) AdminAdjustBalance(c *gin.Context) {
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&u, id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
newBalance := u.Balance + req.Amount
|
||||
if err := tx.Model(&store.User{}).Where("id = ?", id).Update("balance", newBalance).Error; err != nil {
|
||||
// SQL 侧原子加减:SQLite 驱动不支持行锁,Go 侧读-改-写会与记账扣费互相覆盖
|
||||
if err := tx.Model(&store.User{}).Where("id = ?", id).Update("balance", gorm.Expr("balance + ?", req.Amount)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
ref := "admin:" + strconv.FormatUint(u.ID, 10) + ":" + time.Now().Format("20060102150405")
|
||||
// ref_id 有单列唯一索引,秒级时间戳同一秒第二次调整必撞;纳秒粒度避免
|
||||
ref := "admin:" + strconv.FormatUint(u.ID, 10) + ":" + strconv.FormatInt(time.Now().UnixNano(), 10)
|
||||
_ = admin.ID // 流水里不冗余管理员 ID;需要时再加
|
||||
return tx.Create(&store.BalanceLog{
|
||||
UserID: u.ID,
|
||||
Change: req.Amount,
|
||||
BalanceAfter: newBalance,
|
||||
BalanceAfter: u.Balance + req.Amount,
|
||||
Type: store.BalanceTypeAdminAdjust,
|
||||
RefID: ref,
|
||||
Remark: req.Remark,
|
||||
|
||||
Reference in new issue
Block a user