Merge pull request #2932 from fatelei/issue-2917

fix: change balance to pointer type
This commit is contained in:
Wesley Liddick 2026-06-01 11:37:33 +08:00 committed by GitHub
commit a0057f44f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 83 additions and 16 deletions

View File

@ -48,14 +48,14 @@ func NewUserHandler(
// CreateUserRequest represents admin create user request // CreateUserRequest represents admin create user request
type CreateUserRequest struct { type CreateUserRequest struct {
Email string `json:"email" binding:"required,email"` Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"` Password string `json:"password" binding:"required,min=6"`
Username string `json:"username"` Username string `json:"username"`
Notes string `json:"notes"` Notes string `json:"notes"`
Balance float64 `json:"balance"` Balance *float64 `json:"balance"`
Concurrency int `json:"concurrency"` Concurrency int `json:"concurrency"`
RPMLimit int `json:"rpm_limit"` RPMLimit int `json:"rpm_limit"`
AllowedGroups []int64 `json:"allowed_groups"` AllowedGroups []int64 `json:"allowed_groups"`
} }
// UpdateUserRequest represents admin update user request // UpdateUserRequest represents admin update user request

View File

@ -129,7 +129,7 @@ type CreateUserInput struct {
Password string Password string
Username string Username string
Notes string Notes string
Balance float64 Balance *float64
Concurrency int Concurrency int
RPMLimit int RPMLimit int
AllowedGroups []int64 AllowedGroups []int64
@ -680,12 +680,19 @@ func (s *adminServiceImpl) GetUserIncludeDeleted(ctx context.Context, id int64)
} }
func (s *adminServiceImpl) CreateUser(ctx context.Context, input *CreateUserInput) (*User, error) { func (s *adminServiceImpl) CreateUser(ctx context.Context, input *CreateUserInput) (*User, error) {
balance := 0.0
if input.Balance != nil {
balance = *input.Balance
} else if s.settingService != nil {
balance = s.settingService.GetDefaultBalance(ctx)
}
user := &User{ user := &User{
Email: input.Email, Email: input.Email,
Username: input.Username, Username: input.Username,
Notes: input.Notes, Notes: input.Notes,
Role: RoleUser, // Always create as regular user, never admin Role: RoleUser, // Always create as regular user, never admin
Balance: input.Balance, Balance: balance,
Concurrency: input.Concurrency, Concurrency: input.Concurrency,
RPMLimit: input.RPMLimit, RPMLimit: input.RPMLimit,
Status: StatusActive, Status: StatusActive,

View File

@ -14,13 +14,14 @@ import (
func TestAdminService_CreateUser_Success(t *testing.T) { func TestAdminService_CreateUser_Success(t *testing.T) {
repo := &userRepoStub{nextID: 10} repo := &userRepoStub{nextID: 10}
svc := &adminServiceImpl{userRepo: repo} svc := &adminServiceImpl{userRepo: repo}
balance := 12.5
input := &CreateUserInput{ input := &CreateUserInput{
Email: "user@test.com", Email: "user@test.com",
Password: "strong-pass", Password: "strong-pass",
Username: "tester", Username: "tester",
Notes: "note", Notes: "note",
Balance: 12.5, Balance: &balance,
Concurrency: 7, Concurrency: 7,
AllowedGroups: []int64{3, 5}, AllowedGroups: []int64{3, 5},
} }
@ -32,7 +33,7 @@ func TestAdminService_CreateUser_Success(t *testing.T) {
require.Equal(t, input.Email, user.Email) require.Equal(t, input.Email, user.Email)
require.Equal(t, input.Username, user.Username) require.Equal(t, input.Username, user.Username)
require.Equal(t, input.Notes, user.Notes) require.Equal(t, input.Notes, user.Notes)
require.Equal(t, input.Balance, user.Balance) require.Equal(t, balance, user.Balance)
require.Equal(t, input.Concurrency, user.Concurrency) require.Equal(t, input.Concurrency, user.Concurrency)
require.Equal(t, input.AllowedGroups, user.AllowedGroups) require.Equal(t, input.AllowedGroups, user.AllowedGroups)
require.Equal(t, RoleUser, user.Role) require.Equal(t, RoleUser, user.Role)
@ -42,6 +43,56 @@ func TestAdminService_CreateUser_Success(t *testing.T) {
require.Equal(t, user, repo.created[0]) require.Equal(t, user, repo.created[0])
} }
func TestAdminService_CreateUser_UsesDefaultBalanceWhenBalanceOmitted(t *testing.T) {
repo := &userRepoStub{nextID: 11}
cfg := &config.Config{
Default: config.DefaultConfig{
UserBalance: 0,
},
}
settingService := NewSettingService(&settingRepoStub{values: map[string]string{
SettingKeyDefaultBalance: "0.02",
}}, cfg)
svc := &adminServiceImpl{userRepo: repo, settingService: settingService}
user, err := svc.CreateUser(context.Background(), &CreateUserInput{
Email: "default-balance@test.com",
Password: "strong-pass",
})
require.NoError(t, err)
require.NotNil(t, user)
require.Equal(t, 0.02, user.Balance)
require.Len(t, repo.created, 1)
require.Equal(t, 0.02, repo.created[0].Balance)
}
func TestAdminService_CreateUser_ExplicitZeroBalanceOverridesDefault(t *testing.T) {
repo := &userRepoStub{nextID: 12}
cfg := &config.Config{
Default: config.DefaultConfig{
UserBalance: 0,
},
}
settingService := NewSettingService(&settingRepoStub{values: map[string]string{
SettingKeyDefaultBalance: "0.02",
}}, cfg)
svc := &adminServiceImpl{userRepo: repo, settingService: settingService}
balance := 0.0
user, err := svc.CreateUser(context.Background(), &CreateUserInput{
Email: "zero-balance@test.com",
Password: "strong-pass",
Balance: &balance,
})
require.NoError(t, err)
require.NotNil(t, user)
require.Equal(t, 0.0, user.Balance)
require.Len(t, repo.created, 1)
require.Equal(t, 0.0, repo.created[0].Balance)
}
func TestAdminService_CreateUser_EmailExists(t *testing.T) { func TestAdminService_CreateUser_EmailExists(t *testing.T) {
repo := &userRepoStub{createErr: ErrEmailExists} repo := &userRepoStub{createErr: ErrEmailExists}
svc := &adminServiceImpl{userRepo: repo} svc := &adminServiceImpl{userRepo: repo}

View File

@ -117,8 +117,11 @@ export async function getById(id: number, includeDeleted = false): Promise<Admin
export async function create(userData: { export async function create(userData: {
email: string email: string
password: string password: string
username?: string
notes?: string
balance?: number balance?: number
concurrency?: number concurrency?: number
rpm_limit?: number
allowed_groups?: number[] | null allowed_groups?: number[] | null
}): Promise<AdminUser> { }): Promise<AdminUser> {
const { data } = await apiClient.post<AdminUser>('/admin/users', userData) const { data } = await apiClient.post<AdminUser>('/admin/users', userData)

View File

@ -28,7 +28,7 @@
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div> <div>
<label class="input-label">{{ t('admin.users.columns.balance') }}</label> <label class="input-label">{{ t('admin.users.columns.balance') }}</label>
<input v-model.number="form.balance" type="number" step="any" class="input" /> <input v-model="form.balance" type="number" step="any" class="input" />
</div> </div>
<div> <div>
<label class="input-label">{{ t('admin.users.columns.concurrency') }}</label> <label class="input-label">{{ t('admin.users.columns.concurrency') }}</label>
@ -69,18 +69,24 @@ import Icon from '@/components/icons/Icon.vue'
const props = defineProps<{ show: boolean }>() const props = defineProps<{ show: boolean }>()
const emit = defineEmits(['close', 'success']); const { t } = useI18n() const emit = defineEmits(['close', 'success']); const { t } = useI18n()
const form = reactive({ email: '', password: '', username: '', notes: '', balance: 0, concurrency: 1, rpm_limit: 0 }) const form = reactive({ email: '', password: '', username: '', notes: '', balance: '', concurrency: 1, rpm_limit: 0 })
const { loading, submit } = useForm({ const { loading, submit } = useForm({
form, form,
submitFn: async (data) => { submitFn: async (data) => {
await adminAPI.users.create(data) const { balance: rawBalance, ...rest } = data
const balance = String(rawBalance).trim()
const payload: typeof rest & { balance?: number } = { ...rest }
if (balance !== '') {
payload.balance = Number(balance)
}
await adminAPI.users.create(payload)
emit('success'); emit('close') emit('success'); emit('close')
}, },
successMsg: t('admin.users.userCreated') successMsg: t('admin.users.userCreated')
}) })
watch(() => props.show, (v) => { if(v) Object.assign(form, { email: '', password: '', username: '', notes: '', balance: 0, concurrency: 1, rpm_limit: 0 }) }) watch(() => props.show, (v) => { if(v) Object.assign(form, { email: '', password: '', username: '', notes: '', balance: '', concurrency: 1, rpm_limit: 0 }) })
const generateRandomPassword = () => { const generateRandomPassword = () => {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789!@#$%^&*' const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789!@#$%^&*'