feat: 添加用户余额充值/退款功能 (#17)
## 功能特性 ### 前端 - 在用户列表操作列添加充值和退款按钮 - 实现充值/退款对话框,支持输入金额和备注 - 从编辑用户表单中移除余额字段,防止直接修改 - 添加余额不足验证,实时显示操作后余额 - 优化备注提示词,提供多种场景示例 ### 后端 - 为 redeem_codes 表添加 notes 字段(迁移文件) - 在 UpdateUserBalance 接口添加 notes 参数支持 - 添加余额验证:金额必须大于0,操作后余额不能为负 - UpdateUser 接口移除 balance 字段处理,防止误操作 - 完整的审计日志和缓存管理 ## 安全保护 - 前端:余额不足时禁用提交按钮,实时提示 - 后端:双重验证(输入金额 > 0 + 结果余额 >= 0) - 权限:仅管理员可访问(AdminAuth 中间件) - 审计:所有操作记录到 redeem_codes 表 ## 修改文件 后端: - backend/migrations/004_add_redeem_code_notes.sql - backend/internal/model/redeem_code.go - backend/internal/service/admin_service.go - backend/internal/handler/admin/user_handler.go 前端: - frontend/src/views/admin/UsersView.vue - frontend/src/api/admin/users.ts - frontend/src/i18n/locales/zh.ts - frontend/src/i18n/locales/en.ts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -49,8 +49,9 @@ type UpdateUserRequest struct {
|
||||
|
||||
// UpdateBalanceRequest represents balance update request
|
||||
type UpdateBalanceRequest struct {
|
||||
Balance float64 `json:"balance" binding:"required"`
|
||||
Balance float64 `json:"balance" binding:"required,gt=0"`
|
||||
Operation string `json:"operation" binding:"required,oneof=set add subtract"`
|
||||
Notes string `json:"notes"`
|
||||
}
|
||||
|
||||
// List handles listing all users with pagination
|
||||
@@ -183,7 +184,7 @@ func (h *UserHandler) UpdateBalance(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.adminService.UpdateUserBalance(c.Request.Context(), userID, req.Balance, req.Operation)
|
||||
user, err := h.adminService.UpdateUserBalance(c.Request.Context(), userID, req.Balance, req.Operation, req.Notes)
|
||||
if err != nil {
|
||||
response.InternalError(c, "Failed to update balance: "+err.Error())
|
||||
return
|
||||
|
||||
@@ -14,6 +14,7 @@ type RedeemCode struct {
|
||||
Status string `gorm:"size:20;default:unused;not null" json:"status"` // unused/used
|
||||
UsedBy *int64 `gorm:"index" json:"used_by"`
|
||||
UsedAt *time.Time `json:"used_at"`
|
||||
Notes string `gorm:"type:text" json:"notes"`
|
||||
CreatedAt time.Time `gorm:"not null" json:"created_at"`
|
||||
|
||||
// 订阅类型专用字段
|
||||
|
||||
@@ -22,7 +22,7 @@ type AdminService interface {
|
||||
CreateUser(ctx context.Context, input *CreateUserInput) (*model.User, error)
|
||||
UpdateUser(ctx context.Context, id int64, input *UpdateUserInput) (*model.User, error)
|
||||
DeleteUser(ctx context.Context, id int64) error
|
||||
UpdateUserBalance(ctx context.Context, userID int64, balance float64, operation string) (*model.User, error)
|
||||
UpdateUserBalance(ctx context.Context, userID int64, balance float64, operation string, notes string) (*model.User, error)
|
||||
GetUserAPIKeys(ctx context.Context, userID int64, page, pageSize int) ([]model.ApiKey, int64, error)
|
||||
GetUserUsageStats(ctx context.Context, userID int64, period string) (any, error)
|
||||
|
||||
@@ -271,8 +271,6 @@ func (s *adminServiceImpl) UpdateUser(ctx context.Context, id int64, input *Upda
|
||||
return nil, errors.New("cannot disable admin user")
|
||||
}
|
||||
|
||||
// Track balance and concurrency changes for logging
|
||||
oldBalance := user.Balance
|
||||
oldConcurrency := user.Concurrency
|
||||
|
||||
if input.Email != "" {
|
||||
@@ -284,7 +282,6 @@ func (s *adminServiceImpl) UpdateUser(ctx context.Context, id int64, input *Upda
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户字段
|
||||
if input.Username != nil {
|
||||
user.Username = *input.Username
|
||||
}
|
||||
@@ -295,22 +292,14 @@ func (s *adminServiceImpl) UpdateUser(ctx context.Context, id int64, input *Upda
|
||||
user.Notes = *input.Notes
|
||||
}
|
||||
|
||||
// Role is not allowed to be changed via API to prevent privilege escalation
|
||||
if input.Status != "" {
|
||||
user.Status = input.Status
|
||||
}
|
||||
|
||||
// 只在指针非 nil 时更新 Balance(支持设置为 0)
|
||||
if input.Balance != nil {
|
||||
user.Balance = *input.Balance
|
||||
}
|
||||
|
||||
// 只在指针非 nil 时更新 Concurrency(支持设置为任意值)
|
||||
if input.Concurrency != nil {
|
||||
user.Concurrency = *input.Concurrency
|
||||
}
|
||||
|
||||
// 只在指针非 nil 时更新 AllowedGroups
|
||||
if input.AllowedGroups != nil {
|
||||
user.AllowedGroups = *input.AllowedGroups
|
||||
}
|
||||
@@ -319,41 +308,6 @@ func (s *adminServiceImpl) UpdateUser(ctx context.Context, id int64, input *Upda
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 余额变化时失效缓存
|
||||
if input.Balance != nil && *input.Balance != oldBalance {
|
||||
if s.billingCacheService != nil {
|
||||
go func() {
|
||||
cacheCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := s.billingCacheService.InvalidateUserBalance(cacheCtx, id); err != nil {
|
||||
log.Printf("invalidate user balance cache failed: user_id=%d err=%v", id, err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// Create adjustment records for balance/concurrency changes
|
||||
balanceDiff := user.Balance - oldBalance
|
||||
if balanceDiff != 0 {
|
||||
code, err := model.GenerateRedeemCode()
|
||||
if err != nil {
|
||||
log.Printf("failed to generate adjustment redeem code: %v", err)
|
||||
return user, nil
|
||||
}
|
||||
adjustmentRecord := &model.RedeemCode{
|
||||
Code: code,
|
||||
Type: model.AdjustmentTypeAdminBalance,
|
||||
Value: balanceDiff,
|
||||
Status: model.StatusUsed,
|
||||
UsedBy: &user.ID,
|
||||
}
|
||||
now := time.Now()
|
||||
adjustmentRecord.UsedAt = &now
|
||||
if err := s.redeemCodeRepo.Create(ctx, adjustmentRecord); err != nil {
|
||||
log.Printf("failed to create balance adjustment redeem code: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
concurrencyDiff := user.Concurrency - oldConcurrency
|
||||
if concurrencyDiff != 0 {
|
||||
code, err := model.GenerateRedeemCode()
|
||||
@@ -390,12 +344,14 @@ func (s *adminServiceImpl) DeleteUser(ctx context.Context, id int64) error {
|
||||
return s.userRepo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (s *adminServiceImpl) UpdateUserBalance(ctx context.Context, userID int64, balance float64, operation string) (*model.User, error) {
|
||||
func (s *adminServiceImpl) UpdateUserBalance(ctx context.Context, userID int64, balance float64, operation string, notes string) (*model.User, error) {
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
oldBalance := user.Balance
|
||||
|
||||
switch operation {
|
||||
case "set":
|
||||
user.Balance = balance
|
||||
@@ -405,11 +361,14 @@ func (s *adminServiceImpl) UpdateUserBalance(ctx context.Context, userID int64,
|
||||
user.Balance -= balance
|
||||
}
|
||||
|
||||
if user.Balance < 0 {
|
||||
return nil, fmt.Errorf("balance cannot be negative, current balance: %.2f, requested operation would result in: %.2f", oldBalance, user.Balance)
|
||||
}
|
||||
|
||||
if err := s.userRepo.Update(ctx, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 失效余额缓存
|
||||
if s.billingCacheService != nil {
|
||||
go func() {
|
||||
cacheCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
@@ -420,6 +379,30 @@ func (s *adminServiceImpl) UpdateUserBalance(ctx context.Context, userID int64,
|
||||
}()
|
||||
}
|
||||
|
||||
balanceDiff := user.Balance - oldBalance
|
||||
if balanceDiff != 0 {
|
||||
code, err := model.GenerateRedeemCode()
|
||||
if err != nil {
|
||||
log.Printf("failed to generate adjustment redeem code: %v", err)
|
||||
return user, nil
|
||||
}
|
||||
|
||||
adjustmentRecord := &model.RedeemCode{
|
||||
Code: code,
|
||||
Type: model.AdjustmentTypeAdminBalance,
|
||||
Value: balanceDiff,
|
||||
Status: model.StatusUsed,
|
||||
UsedBy: &user.ID,
|
||||
Notes: notes,
|
||||
}
|
||||
now := time.Now()
|
||||
adjustmentRecord.UsedAt = &now
|
||||
|
||||
if err := s.redeemCodeRepo.Create(ctx, adjustmentRecord); err != nil {
|
||||
log.Printf("failed to create balance adjustment redeem code: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
|
||||
6
backend/migrations/004_add_redeem_code_notes.sql
Normal file
6
backend/migrations/004_add_redeem_code_notes.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
-- 为 redeem_codes 表添加备注字段
|
||||
|
||||
ALTER TABLE redeem_codes
|
||||
ADD COLUMN IF NOT EXISTS notes TEXT DEFAULT NULL;
|
||||
|
||||
COMMENT ON COLUMN redeem_codes.notes IS '备注说明(管理员调整时的原因说明)';
|
||||
@@ -84,16 +84,19 @@ export async function deleteUser(id: number): Promise<{ message: string }> {
|
||||
* @param id - User ID
|
||||
* @param balance - New balance
|
||||
* @param operation - Operation type ('set', 'add', 'subtract')
|
||||
* @param notes - Optional notes for the balance adjustment
|
||||
* @returns Updated user
|
||||
*/
|
||||
export async function updateBalance(
|
||||
id: number,
|
||||
balance: number,
|
||||
operation: 'set' | 'add' | 'subtract' = 'set'
|
||||
operation: 'set' | 'add' | 'subtract' = 'set',
|
||||
notes?: string
|
||||
): Promise<User> {
|
||||
const { data } = await apiClient.post<User>(`/admin/users/${id}/balance`, {
|
||||
balance,
|
||||
operation,
|
||||
notes: notes || '',
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -520,6 +520,26 @@ export default {
|
||||
allowedGroupsUpdated: 'Allowed groups updated successfully',
|
||||
failedToLoadGroups: 'Failed to load groups',
|
||||
failedToUpdateAllowedGroups: 'Failed to update allowed groups',
|
||||
deposit: 'Deposit',
|
||||
withdraw: 'Withdraw',
|
||||
depositAmount: 'Deposit Amount',
|
||||
withdrawAmount: 'Withdraw Amount',
|
||||
currentBalance: 'Current Balance',
|
||||
depositNotesPlaceholder: 'e.g., New user registration bonus, promotional credit, compensation, etc.',
|
||||
withdrawNotesPlaceholder: 'e.g., Service issue refund, incorrect charge reversal, account closure refund, etc.',
|
||||
notesOptional: 'Notes are optional but helpful for record keeping',
|
||||
amountHint: 'Please enter a positive amount',
|
||||
newBalance: 'New Balance',
|
||||
depositing: 'Depositing...',
|
||||
withdrawing: 'Withdrawing...',
|
||||
confirmDeposit: 'Confirm Deposit',
|
||||
confirmWithdraw: 'Confirm Withdraw',
|
||||
depositSuccess: 'Deposit successful',
|
||||
withdrawSuccess: 'Withdraw successful',
|
||||
failedToDeposit: 'Failed to deposit',
|
||||
failedToWithdraw: 'Failed to withdraw',
|
||||
useDepositWithdrawButtons: 'Please use deposit/withdraw buttons to adjust balance',
|
||||
insufficientBalance: 'Insufficient balance, balance cannot be negative after withdrawal',
|
||||
},
|
||||
|
||||
// Groups
|
||||
|
||||
@@ -587,6 +587,25 @@ export default {
|
||||
allowedGroupsUpdated: '允许分组更新成功',
|
||||
failedToLoadGroups: '加载分组列表失败',
|
||||
failedToUpdateAllowedGroups: '更新允许分组失败',
|
||||
deposit: '充值',
|
||||
withdraw: '退款',
|
||||
depositAmount: '充值金额',
|
||||
withdrawAmount: '退款金额',
|
||||
depositNotesPlaceholder: '例如:新用户注册奖励、活动充值、补偿充值等',
|
||||
withdrawNotesPlaceholder: '例如:服务问题退款、错误充值退回、账户注销退款等',
|
||||
notesOptional: '备注为可选项,有助于未来查账',
|
||||
amountHint: '请输入正数金额',
|
||||
newBalance: '操作后余额',
|
||||
depositing: '充值中...',
|
||||
withdrawing: '退款中...',
|
||||
confirmDeposit: '确认充值',
|
||||
confirmWithdraw: '确认退款',
|
||||
depositSuccess: '充值成功',
|
||||
withdrawSuccess: '退款成功',
|
||||
failedToDeposit: '充值失败',
|
||||
failedToWithdraw: '退款失败',
|
||||
useDepositWithdrawButtons: '请使用充值/退款按钮调整余额',
|
||||
insufficientBalance: '余额不足,退款后余额不能为负数',
|
||||
},
|
||||
|
||||
// Groups Management
|
||||
|
||||
@@ -207,6 +207,26 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Deposit -->
|
||||
<button
|
||||
@click="handleDeposit(row)"
|
||||
class="p-2 rounded-lg hover:bg-emerald-50 dark:hover:bg-emerald-900/20 text-gray-500 hover:text-emerald-600 dark:hover:text-emerald-400 transition-colors"
|
||||
:title="t('admin.users.deposit')"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Withdraw -->
|
||||
<button
|
||||
@click="handleWithdraw(row)"
|
||||
class="p-2 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 text-gray-500 hover:text-red-600 dark:hover:text-red-400 transition-colors"
|
||||
:title="t('admin.users.withdraw')"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14" />
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Edit -->
|
||||
<button
|
||||
@click="handleEdit(row)"
|
||||
@@ -476,24 +496,13 @@
|
||||
></textarea>
|
||||
<p class="input-hint">{{ t('admin.users.notesHint') }}</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.users.columns.balance') }}</label>
|
||||
<input
|
||||
v-model.number="editForm.balance"
|
||||
type="number"
|
||||
step="any"
|
||||
class="input"
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.users.columns.concurrency') }}</label>
|
||||
<input
|
||||
v-model.number="editForm.concurrency"
|
||||
type="number"
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.users.columns.concurrency') }}</label>
|
||||
<input
|
||||
v-model.number="editForm.concurrency"
|
||||
type="number"
|
||||
class="input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
@@ -729,6 +738,114 @@
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<!-- Deposit/Withdraw Modal -->
|
||||
<Modal
|
||||
:show="showBalanceModal"
|
||||
:title="balanceOperation === 'add' ? t('admin.users.deposit') : t('admin.users.withdraw')"
|
||||
size="md"
|
||||
@close="closeBalanceModal"
|
||||
>
|
||||
<form v-if="balanceUser" @submit.prevent="handleBalanceSubmit" class="space-y-5">
|
||||
<div class="flex items-center gap-3 p-4 rounded-xl bg-gray-50 dark:bg-dark-700">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-primary-100 dark:bg-primary-900/30">
|
||||
<span class="text-lg font-medium text-primary-700 dark:text-primary-300">
|
||||
{{ balanceUser.email.charAt(0).toUpperCase() }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<p class="font-medium text-gray-900 dark:text-white">{{ balanceUser.email }}</p>
|
||||
<p class="text-sm text-gray-500 dark:text-dark-400">
|
||||
{{ t('admin.users.currentBalance') }}: ${{ balanceUser.balance.toFixed(2) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="input-label">
|
||||
{{ balanceOperation === 'add' ? t('admin.users.depositAmount') : t('admin.users.withdrawAmount') }}
|
||||
</label>
|
||||
<div class="relative">
|
||||
<div class="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 dark:text-dark-400 font-medium">
|
||||
$
|
||||
</div>
|
||||
<input
|
||||
v-model.number="balanceForm.amount"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0.01"
|
||||
required
|
||||
class="input pl-8"
|
||||
:placeholder="balanceOperation === 'add' ? '10.00' : '5.00'"
|
||||
/>
|
||||
</div>
|
||||
<p class="input-hint">
|
||||
{{ t('admin.users.amountHint') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="input-label">{{ t('admin.users.notes') }}</label>
|
||||
<textarea
|
||||
v-model="balanceForm.notes"
|
||||
rows="3"
|
||||
class="input"
|
||||
:placeholder="balanceOperation === 'add'
|
||||
? t('admin.users.depositNotesPlaceholder')
|
||||
: t('admin.users.withdrawNotesPlaceholder')"
|
||||
></textarea>
|
||||
<p class="input-hint">{{ t('admin.users.notesOptional') }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="balanceForm.amount > 0" class="p-4 rounded-xl bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800/50">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-blue-700 dark:text-blue-300">{{ t('admin.users.newBalance') }}:</span>
|
||||
<span class="font-bold text-blue-900 dark:text-blue-100">
|
||||
${{ calculateNewBalance().toFixed(2) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="balanceOperation === 'subtract' && calculateNewBalance() < 0" class="p-4 rounded-xl bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800/50">
|
||||
<div class="flex items-center gap-2 text-sm text-red-700 dark:text-red-300">
|
||||
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
||||
</svg>
|
||||
<span>{{ t('admin.users.insufficientBalance') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
<button
|
||||
@click="closeBalanceModal"
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
{{ t('common.cancel') }}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="balanceSubmitting || !balanceForm.amount || balanceForm.amount <= 0 || (balanceOperation === 'subtract' && calculateNewBalance() < 0)"
|
||||
class="btn"
|
||||
:class="balanceOperation === 'add' ? 'bg-emerald-600 hover:bg-emerald-700 text-white' : 'btn-danger'"
|
||||
>
|
||||
<svg
|
||||
v-if="balanceSubmitting"
|
||||
class="animate-spin -ml-1 mr-2 h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
{{ balanceSubmitting
|
||||
? (balanceOperation === 'add' ? t('admin.users.depositing') : t('admin.users.withdrawing'))
|
||||
: (balanceOperation === 'add' ? t('admin.users.confirmDeposit') : t('admin.users.confirmWithdraw'))
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
<!-- Delete Confirmation Dialog -->
|
||||
<ConfirmDialog
|
||||
:show="showDeleteDialog"
|
||||
@@ -828,6 +945,16 @@ const selectedGroupIds = ref<number[]>([])
|
||||
const loadingGroups = ref(false)
|
||||
const savingAllowedGroups = ref(false)
|
||||
|
||||
// Balance (Deposit/Withdraw) modal state
|
||||
const showBalanceModal = ref(false)
|
||||
const balanceUser = ref<User | null>(null)
|
||||
const balanceOperation = ref<'add' | 'subtract'>('add')
|
||||
const balanceSubmitting = ref(false)
|
||||
const balanceForm = reactive({
|
||||
amount: 0,
|
||||
notes: ''
|
||||
})
|
||||
|
||||
const createForm = reactive({
|
||||
email: '',
|
||||
password: '',
|
||||
@@ -844,7 +971,6 @@ const editForm = reactive({
|
||||
username: '',
|
||||
wechat: '',
|
||||
notes: '',
|
||||
balance: 0,
|
||||
concurrency: 1
|
||||
})
|
||||
const editPasswordCopied = ref(false)
|
||||
@@ -997,7 +1123,6 @@ const handleEdit = (user: User) => {
|
||||
editForm.username = user.username || ''
|
||||
editForm.wechat = user.wechat || ''
|
||||
editForm.notes = user.notes || ''
|
||||
editForm.balance = user.balance
|
||||
editForm.concurrency = user.concurrency
|
||||
editPasswordCopied.value = false
|
||||
showEditModal.value = true
|
||||
@@ -1015,13 +1140,11 @@ const handleUpdateUser = async () => {
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
// Build update data - only include password if not empty
|
||||
const updateData: Record<string, any> = {
|
||||
email: editForm.email,
|
||||
username: editForm.username,
|
||||
wechat: editForm.wechat,
|
||||
notes: editForm.notes,
|
||||
balance: editForm.balance,
|
||||
concurrency: editForm.concurrency
|
||||
}
|
||||
if (editForm.password.trim()) {
|
||||
@@ -1141,6 +1264,69 @@ const confirmDelete = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeposit = (user: User) => {
|
||||
balanceUser.value = user
|
||||
balanceOperation.value = 'add'
|
||||
balanceForm.amount = 0
|
||||
balanceForm.notes = ''
|
||||
showBalanceModal.value = true
|
||||
}
|
||||
|
||||
const handleWithdraw = (user: User) => {
|
||||
balanceUser.value = user
|
||||
balanceOperation.value = 'subtract'
|
||||
balanceForm.amount = 0
|
||||
balanceForm.notes = ''
|
||||
showBalanceModal.value = true
|
||||
}
|
||||
|
||||
const closeBalanceModal = () => {
|
||||
showBalanceModal.value = false
|
||||
balanceUser.value = null
|
||||
balanceForm.amount = 0
|
||||
balanceForm.notes = ''
|
||||
}
|
||||
|
||||
const calculateNewBalance = () => {
|
||||
if (!balanceUser.value) return 0
|
||||
if (balanceOperation.value === 'add') {
|
||||
return balanceUser.value.balance + balanceForm.amount
|
||||
} else {
|
||||
return balanceUser.value.balance - balanceForm.amount
|
||||
}
|
||||
}
|
||||
|
||||
const handleBalanceSubmit = async () => {
|
||||
if (!balanceUser.value || balanceForm.amount <= 0) return
|
||||
|
||||
balanceSubmitting.value = true
|
||||
try {
|
||||
await adminAPI.users.updateBalance(
|
||||
balanceUser.value.id,
|
||||
balanceForm.amount,
|
||||
balanceOperation.value,
|
||||
balanceForm.notes
|
||||
)
|
||||
|
||||
const successMsg = balanceOperation.value === 'add'
|
||||
? t('admin.users.depositSuccess')
|
||||
: t('admin.users.withdrawSuccess')
|
||||
|
||||
appStore.showSuccess(successMsg)
|
||||
closeBalanceModal()
|
||||
loadUsers()
|
||||
} catch (error: any) {
|
||||
const errorMsg = balanceOperation.value === 'add'
|
||||
? t('admin.users.failedToDeposit')
|
||||
: t('admin.users.failedToWithdraw')
|
||||
|
||||
appStore.showError(error.response?.data?.detail || errorMsg)
|
||||
console.error('Error updating balance:', error)
|
||||
} finally {
|
||||
balanceSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadUsers()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user