fix: 修复域能管理操作无响应及公户账务错误
- 修复前端 energy.js api.post/get 回调调用为 Promise 链式调用,解决操作无响应 - 修复 AdminAdjust system_operation 模式错误扣减公户余额,现不碰公户 - 修复 AdminAdjust admin_transfer 模式公户按用户数量扣款(amount * len(userIDs)) - 修复 AdminAdjust 余额检查移入事务内并加行锁(SELECT FOR UPDATE),防 TOCTOU 竞态 - FundStore 接口及 FundRepo 新增 LockAndGetBalance 方法
This commit is contained in:
@ -5,6 +5,7 @@ import (
|
|||||||
"metazone.cc/metalab/internal/service"
|
"metazone.cc/metalab/internal/service"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FundRepo 公户数据访问
|
// FundRepo 公户数据访问
|
||||||
@ -32,6 +33,16 @@ func (r *FundRepo) GetBalance() (int, error) {
|
|||||||
return fund.Balance, nil
|
return fund.Balance, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LockAndGetBalance 加行锁查询公户余额(事务内使用,防止 TOCTOU 竞态)
|
||||||
|
func (r *FundRepo) LockAndGetBalance() (int, error) {
|
||||||
|
var fund model.CommunityFund
|
||||||
|
err := r.db.Clauses(clause.Locking{Strength: "UPDATE"}).First(&fund, 1).Error
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return fund.Balance, nil
|
||||||
|
}
|
||||||
|
|
||||||
// IncrBalance 原子增减公户余额
|
// IncrBalance 原子增减公户余额
|
||||||
func (r *FundRepo) IncrBalance(amount int) error {
|
func (r *FundRepo) IncrBalance(amount int) error {
|
||||||
return r.db.Model(&model.CommunityFund{}).
|
return r.db.Model(&model.CommunityFund{}).
|
||||||
|
|||||||
@ -379,34 +379,26 @@ func (s *EnergyService) DeductOnDeletePost(authorUserID uint, postID uint) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AdminAdjust 后台调整用户域能
|
// AdminAdjust 后台调整用户域能
|
||||||
// mode: "admin_transfer"(受公户余额约束)| "system_operation"(不受约束)
|
// mode: "admin_transfer"(受公户余额约束)| "system_operation"(不受约束,不碰公户)
|
||||||
func (s *EnergyService) AdminAdjust(operatorUID uint, userIDs []uint, amount int, description string, mode string) error {
|
func (s *EnergyService) AdminAdjust(operatorUID uint, userIDs []uint, amount int, description string, mode string) error {
|
||||||
if mode == "" {
|
if mode == "" {
|
||||||
mode = model.FundTypeAdminTransfer
|
mode = model.FundTypeAdminTransfer
|
||||||
}
|
}
|
||||||
|
|
||||||
// admin_transfer 出账时检查公户余额
|
|
||||||
if mode == model.FundTypeAdminTransfer && amount > 0 && s.fundRepo != nil {
|
|
||||||
balance, err := s.fundRepo.GetBalance()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("查询公户余额失败: %w", err)
|
|
||||||
}
|
|
||||||
if balance < amount {
|
|
||||||
return common.ErrInsufficientFund
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||||
txRepo := s.repo.WithTx(tx)
|
txRepo := s.repo.WithTx(tx)
|
||||||
var txFundRepo FundStore
|
|
||||||
if s.fundRepo != nil {
|
|
||||||
txFundRepo = s.fundRepo.WithTx(tx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 公户余额调整(给用户 energy=+amount → 公户 -amount,扣用户 energy=-amount → 公户 +amount)
|
// admin_transfer:公户出账需检查余额(行锁防 TOCTOU 竞态)
|
||||||
if txFundRepo != nil {
|
isAdminTransfer := mode == model.FundTypeAdminTransfer
|
||||||
if err := txFundRepo.IncrBalance(-amount); err != nil {
|
if isAdminTransfer && amount > 0 && s.fundRepo != nil {
|
||||||
return fmt.Errorf("公户余额调整失败: %w", err)
|
txFundRepo := s.fundRepo.WithTx(tx)
|
||||||
|
totalCost := amount * len(userIDs)
|
||||||
|
balance, err := txFundRepo.LockAndGetBalance()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("查询公户余额失败: %w", err)
|
||||||
|
}
|
||||||
|
if balance < totalCost {
|
||||||
|
return common.ErrInsufficientFund
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -428,14 +420,20 @@ func (s *EnergyService) AdminAdjust(operatorUID uint, userIDs []uint, amount int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 公户流水
|
// admin_transfer:公户扣款 + 公户流水(system_operation 不碰公户)
|
||||||
if txFundRepo != nil {
|
if isAdminTransfer && s.fundRepo != nil {
|
||||||
amountDisplay := float64(amount) / 10
|
txFundRepo := s.fundRepo.WithTx(tx)
|
||||||
|
totalCost := amount * len(userIDs)
|
||||||
|
if err := txFundRepo.IncrBalance(-totalCost); err != nil {
|
||||||
|
return fmt.Errorf("公户余额调整失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
totalDisplay := float64(totalCost) / 10
|
||||||
fundLog := &model.FundLog{
|
fundLog := &model.FundLog{
|
||||||
Amount: -amount,
|
Amount: -totalCost,
|
||||||
Type: mode,
|
Type: mode,
|
||||||
OperatorUID: &operatorUID,
|
OperatorUID: &operatorUID,
|
||||||
Description: fmt.Sprintf("%s:调整用户域能 %.1f,说明:%s", getFundModeName(mode), amountDisplay, description),
|
Description: fmt.Sprintf("%s:调整 %d 名用户域能 %.1f,说明:%s", getFundModeName(mode), len(userIDs), totalDisplay, description),
|
||||||
}
|
}
|
||||||
if len(userIDs) > 0 {
|
if len(userIDs) > 0 {
|
||||||
fundLog.RelatedType = "user"
|
fundLog.RelatedType = "user"
|
||||||
|
|||||||
@ -114,6 +114,7 @@ type energyStore = EnergyStore
|
|||||||
// FundStore 公户仓储接口(ISP)
|
// FundStore 公户仓储接口(ISP)
|
||||||
type FundStore interface {
|
type FundStore interface {
|
||||||
GetBalance() (int, error)
|
GetBalance() (int, error)
|
||||||
|
LockAndGetBalance() (int, error)
|
||||||
IncrBalance(amount int) error
|
IncrBalance(amount int) error
|
||||||
CreateLog(log *model.FundLog) error
|
CreateLog(log *model.FundLog) error
|
||||||
ListLogs(logType string, offset, limit int) ([]model.FundLog, int64, error)
|
ListLogs(logType string, offset, limit int) ([]model.FundLog, int64, error)
|
||||||
|
|||||||
@ -46,13 +46,12 @@
|
|||||||
amount: amount,
|
amount: amount,
|
||||||
description: desc,
|
description: desc,
|
||||||
mode: mode
|
mode: mode
|
||||||
}, function (res) {
|
}).then(function (res) {
|
||||||
if (submitBtn) submitBtn.disabled = false;
|
if (submitBtn) submitBtn.disabled = false;
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
showMsg(msgEl, res.message || '调整成功', 'success');
|
showMsg(msgEl, res.message || '调整成功', 'success');
|
||||||
// 刷新公户余额
|
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
api.get('/api/admin/energy/fund-balance', function (r) {
|
api.get('/api/admin/energy/fund-balance').then(function (r) {
|
||||||
if (r.success && r.data) {
|
if (r.success && r.data) {
|
||||||
var balEl = document.getElementById('fund-balance');
|
var balEl = document.getElementById('fund-balance');
|
||||||
if (balEl) {
|
if (balEl) {
|
||||||
@ -76,7 +75,7 @@
|
|||||||
|
|
||||||
function loadLogs() {
|
function loadLogs() {
|
||||||
var logType = typeFilter ? typeFilter.value : '';
|
var logType = typeFilter ? typeFilter.value : '';
|
||||||
api.get('/api/admin/energy/logs?page=' + page + '&page_size=20&type=' + encodeURIComponent(logType), function (res) {
|
api.get('/api/admin/energy/logs?page=' + page + '&page_size=20&type=' + encodeURIComponent(logType)).then(function (res) {
|
||||||
if (!res.success) return;
|
if (!res.success) return;
|
||||||
renderLogTable('energy-log-tbody', res.data.items, function (item) {
|
renderLogTable('energy-log-tbody', res.data.items, function (item) {
|
||||||
return [
|
return [
|
||||||
@ -112,7 +111,7 @@
|
|||||||
|
|
||||||
function loadLogs() {
|
function loadLogs() {
|
||||||
var logType = typeFilter ? typeFilter.value : '';
|
var logType = typeFilter ? typeFilter.value : '';
|
||||||
api.get('/api/admin/energy/fund-logs?page=' + page + '&page_size=20&type=' + encodeURIComponent(logType), function (res) {
|
api.get('/api/admin/energy/fund-logs?page=' + page + '&page_size=20&type=' + encodeURIComponent(logType)).then(function (res) {
|
||||||
if (!res.success) return;
|
if (!res.success) return;
|
||||||
renderLogTable('fund-log-tbody', res.data.items, function (item) {
|
renderLogTable('fund-log-tbody', res.data.items, function (item) {
|
||||||
return [
|
return [
|
||||||
|
|||||||
Reference in New Issue
Block a user