Browse Source

英文名问题

liuq 2 months ago
parent
commit
e99fd1e0b4

+ 2 - 5
backend/app/api/v1/endpoints/apps.py

@@ -161,11 +161,8 @@ def update_app(
         raise HTTPException(status_code=403, detail="权限不足")
 
     # Security Verification
-    if not app_in.password or not app_in.verification_code:
-         raise HTTPException(status_code=400, detail="需要提供密码和手机验证码")
-
-    if not security.verify_password(app_in.password, current_user.password_hash):
-        raise HTTPException(status_code=401, detail="密码错误")
+    if not app_in.verification_code:
+         raise HTTPException(status_code=400, detail="需要提供手机验证码")
 
     if not SmsService.verify_code(current_user.mobile, app_in.verification_code):
         raise HTTPException(status_code=400, detail="验证码无效或已过期")

+ 78 - 1
backend/app/api/v1/endpoints/users.py

@@ -9,7 +9,7 @@ from app.core import security
 from app.core.utils import generate_english_name
 from app.models.user import User, UserRole
 from app.models.mapping import AppUserMapping
-from app.schemas.user import User as UserSchema, UserCreate, UserUpdate, UserList, PromoteUserRequest
+from app.schemas.user import User as UserSchema, UserCreate, UserUpdate, UserList, PromoteUserRequest, BatchResetEnglishNameRequest
 from app.services.webhook_service import WebhookService
 from app.services.captcha_service import CaptchaService
 from app.services.log_service import LogService
@@ -177,6 +177,83 @@ def create_user(
 
     return db_user
 
+@router.post("/batch/reset-english-name", summary="批量重置用户英文名")
+def batch_reset_english_name(
+    *,
+    db: Session = Depends(deps.get_db),
+    req: BatchResetEnglishNameRequest,
+    request: Request,
+    current_user: User = Depends(deps.get_current_active_user),
+):
+    """
+    批量重置选中用户的英文名。
+    规则:根据姓名生成拼音,如有重复自动追加数字后缀。
+    需要管理员密码验证。
+    """
+    if current_user.role != "SUPER_ADMIN":
+        raise HTTPException(status_code=403, detail="权限不足")
+
+    # Verify Admin Password
+    if not security.verify_password(req.admin_password, current_user.password_hash):
+        raise HTTPException(status_code=401, detail="管理员密码错误")
+
+    if not req.user_ids:
+        raise HTTPException(status_code=400, detail="请选择用户")
+
+    success_count = 0
+    users = db.query(User).filter(User.id.in_(req.user_ids), User.is_deleted == 0).all()
+    
+    for user in users:
+        if not user.name:
+            continue
+            
+        old_english_name = user.english_name
+        new_english_name = generate_english_name(user.name)
+        
+        # Uniqueness check
+        if new_english_name:
+            original_base = new_english_name
+            counter = 1
+            # Check against DB (excluding self if it accidentally matches, though unlikely for reset)
+            # Actually for reset, even if it matches self, we might want to keep it or update it.
+            # But the goal is uniqueness.
+            
+            # Helper to check existence
+            def check_exists(name, current_id):
+                return db.query(User).filter(
+                    User.english_name == name, 
+                    User.is_deleted == 0, 
+                    User.id != current_id
+                ).first()
+
+            while check_exists(new_english_name, user.id):
+                new_english_name = f"{original_base}{counter}"
+                counter += 1
+        
+        if old_english_name != new_english_name:
+            user.english_name = new_english_name
+            db.add(user)
+            
+            # Log
+            LogService.create_log(
+                db=db,
+                operator_id=current_user.id,
+                action_type=ActionType.UPDATE,
+                target_user_id=user.id,
+                target_mobile=user.mobile,
+                ip_address=request.client.host,
+                details={
+                    "field": "english_name", 
+                    "old": old_english_name, 
+                    "new": new_english_name,
+                    "reason": "BATCH_RESET"
+                }
+            )
+            success_count += 1
+            
+    db.commit()
+    return {"success": True, "count": success_count}
+
 @router.put("/{user_id}", response_model=UserSchema, summary="更新用户")
 def update_user(
     *,

+ 3 - 3
backend/app/core/utils.py

@@ -14,8 +14,8 @@ def generate_english_name(chinese_name: str) -> str:
     surname = chinese_name[0]
     firstname = chinese_name[1:]
     
-    surname_initial = p.get_initials(surname, "").lower()
-    firstname_pinyin = p.get_pinyin(firstname, "").lower()
+    surname_pinyin = p.get_pinyin(surname, "").lower()
+    firstname_initial = p.get_initials(firstname, "").lower()
     
-    return f"{surname_initial}{firstname_pinyin}"
+    return f"{surname_pinyin}{firstname_initial}"
 

+ 5 - 1
backend/app/schemas/user.py

@@ -26,6 +26,10 @@ class PromoteUserRequest(BaseModel):
     captcha_id: str
     captcha_code: str
 
+class BatchResetEnglishNameRequest(BaseModel):
+    user_ids: List[int]
+    admin_password: str
+
 class UserUpdate(BaseModel):
     password: Optional[str] = None
     mobile: Optional[str] = None
@@ -72,4 +76,4 @@ class UserSyncSimple(BaseModel):
 
 class UserSyncList(BaseModel):
     total: int
-    items: List[UserSyncSimple]
+    items: List[UserSyncSimple]

+ 3 - 6
backend/app/services/import_service.py

@@ -9,6 +9,7 @@ from sqlalchemy import or_
 from app.models.user import User, UserStatus, UserRole
 from app.models.import_log import ImportLog
 from app.core.security import get_password_hash
+from app.core.utils import generate_english_name
 # from app.core.utils import generate_password 
 from typing import List, Dict, Any, Tuple
 from datetime import datetime
@@ -121,13 +122,9 @@ class UserImportService:
                     # 3. Handle Name and English Name Generation
                     if name:
                         if not en_name:
-                            # Use surname first letter + given name full pinyin
+                            # Use surname full pinyin + given name initials
                             try:
-                                surname = name[0]
-                                given = name[1:] if len(name) > 1 else ""
-                                s_initial = p.get_initials(surname, '').lower()
-                                g_pinyin = p.get_pinyin(given, '').lower()
-                                en_name = f"{s_initial}{g_pinyin}"
+                                en_name = generate_english_name(name)
                             except Exception:
                                 en_name = f"user_{_generate_random_string(6)}"
                     else:

+ 94 - 14
frontend/src/views/UserList.vue

@@ -14,7 +14,7 @@
             @keyup.enter="handleSearch"
         >
             <template #append>
-              <el-button icon="Search" @click="handleSearch" />
+              <el-button :icon="Search" @click="handleSearch" />
             </template>
         </el-input>
 
@@ -39,13 +39,24 @@
         <el-button type="primary" plain @click="showImportDialog = true">
           <el-icon style="margin-right: 4px"><Upload /></el-icon> Excel导入
         </el-button>
+        <el-button type="warning" plain @click="handleBatchResetClick" :disabled="selectedUsers.length === 0">
+          <el-icon style="margin-right: 4px"><EditPen /></el-icon> 批量重置英文名
+        </el-button>
         <el-button @click="openLogDrawer">
           <el-icon style="margin-right: 4px"><List /></el-icon> 操作日志
         </el-button>
       </div>
     </div>
 
-    <el-table :data="users" v-loading="loading" stripe border style="width: 100%">
+    <el-table 
+        :data="users" 
+        v-loading="loading" 
+        stripe 
+        border 
+        style="width: 100%"
+        @selection-change="handleSelectionChange"
+    >
+      <el-table-column type="selection" width="55" />
       <el-table-column prop="id" label="ID" width="80" />
       <el-table-column prop="mobile" label="手机号" min-width="120" />
       <el-table-column prop="name" label="姓名" min-width="100" />
@@ -73,10 +84,13 @@
       <el-table-column label="操作" fixed="right" width="220">
         <template #default="scope">
           <div class="action-buttons">
-            <!-- Super Admin cannot be managed here usually, but if needed -->
+            <!-- Allow Edit for everyone, including Super Admin -->
+            <el-button type="primary" link @click="handleEditUser(scope.row)">编辑</el-button>
+
+            <!-- Other actions restricted for Super Admin -->
             <template v-if="scope.row.role !== 'SUPER_ADMIN'">
-                <el-button type="primary" link @click="handleEditUser(scope.row)">编辑</el-button>
                 <el-divider direction="vertical" />
+                
                 <!-- PENDING Actions -->
                 <template v-if="scope.row.status === 'PENDING'">
                   <el-button type="primary" link @click="handleStatus(scope.row, 'ACTIVE')">通过</el-button>
@@ -115,9 +129,6 @@
                     </template>
                 </el-dropdown>
             </template>
-            <span v-else class="text-gray">
-                不可操作
-            </span>
           </div>
         </template>
       </el-table-column>
@@ -206,6 +217,36 @@
       </template>
     </el-dialog>
 
+    <!-- Batch Reset Dialog -->
+    <el-dialog v-model="batchResetDialogVisible" title="批量重置英文名" width="400px">
+        <div class="warning-text" style="margin-bottom: 20px; color: #e6a23c; display: flex; align-items: flex-start; gap: 8px;">
+            <el-icon style="margin-top: 2px"><Warning /></el-icon>
+            <span>
+                将根据用户的姓名自动生成拼音英文名。如有重复将自动添加数字后缀。
+                <br>
+                已选择 <strong>{{ selectedUsers.length }}</strong> 位用户。
+            </span>
+        </div>
+        
+        <el-form label-position="top">
+            <el-form-item label="管理员密码验证" required>
+                <el-input 
+                    v-model="batchAdminPassword" 
+                    type="password" 
+                    show-password 
+                    placeholder="请输入管理员密码" 
+                    :name="dynamicPwdField"
+                    autocomplete="new-password"
+                />
+            </el-form-item>
+        </el-form>
+
+        <template #footer>
+            <el-button @click="batchResetDialogVisible = false">取消</el-button>
+            <el-button type="primary" @click="confirmBatchReset" :loading="batchResetting">确认重置</el-button>
+        </template>
+    </el-dialog>
+
     <!-- Create User Dialog -->
     <el-dialog v-model="createDialogVisible" title="新增用户" width="500px">
       <el-form :model="createForm" :rules="createRules" ref="createFormRef" label-width="100px">
@@ -300,7 +341,7 @@
                 @keyup.enter="fetchLogsData"
             >
                 <template #append>
-                    <el-button icon="Search" @click="fetchLogsData" />
+                    <el-button :icon="Search" @click="fetchLogsData" />
                 </template>
             </el-input>
              <el-date-picker
@@ -365,7 +406,7 @@
 <script setup lang="ts">
 import { ref, onMounted, reactive } from 'vue'
 import { ElMessage, FormInstance, FormRules } from 'element-plus'
-import { Refresh, ArrowDown, Search, Plus, List, Upload } from '@element-plus/icons-vue'
+import { Refresh, ArrowDown, Search, Plus, List, Upload, EditPen, Warning } from '@element-plus/icons-vue'
 import api from '../utils/request'
 import { getLogs, OperationLog } from '../api/logs'
 import UserImportDialog from '../components/UserImportDialog.vue'
@@ -723,6 +764,50 @@ const confirmChangeRole = async () => {
     }
 }
 
+// Batch Reset Logic
+const batchResetDialogVisible = ref(false)
+const batchAdminPassword = ref('')
+const batchResetting = ref(false)
+const selectedUsers = ref<User[]>([])
+
+const handleSelectionChange = (val: User[]) => {
+    selectedUsers.value = val
+}
+
+const handleBatchResetClick = () => {
+    if (selectedUsers.value.length === 0) {
+        ElMessage.warning('请先选择用户')
+        return
+    }
+    batchAdminPassword.value = ''
+    refreshDynamicField()
+    batchResetDialogVisible.value = true
+}
+
+const confirmBatchReset = async () => {
+    if (!batchAdminPassword.value) {
+        ElMessage.warning('请输入管理员密码')
+        return
+    }
+    
+    batchResetting.value = true
+    try {
+        const res = await api.post('/users/batch/reset-english-name', {
+            user_ids: selectedUsers.value.map(u => u.id),
+            admin_password: batchAdminPassword.value
+        })
+        ElMessage.success(`操作成功,已重置 ${res.data.count} 位用户的英文名`)
+        batchResetDialogVisible.value = false
+        fetchUsers()
+        selectedUsers.value = [] 
+    } catch (e) {
+        // handled
+    } finally {
+        batchResetting.value = false
+    }
+}
+
+
 // Log Logic
 const logDrawerVisible = ref(false)
 const logLoading = ref(false)
@@ -753,11 +838,6 @@ const fetchLogsData = async () => {
             keyword: logFilter.keyword || undefined,
         }
         if (logFilter.dateRange && logFilter.dateRange.length === 2) {
-            params.start_date = logFilter.dateRange[0]
-            // Add time to end date to cover the whole day
-            // Or backend handles it? Standard is often strict inequality.
-            // Let's assume date string implies 00:00:00, so end date should be next day or use backend logic.
-            // Simple approach: pass as is
             params.start_date = logFilter.dateRange[0]
             params.end_date = logFilter.dateRange[1] + ' 23:59:59'
         }

+ 8 - 5
frontend/src/views/apps/AppList.vue

@@ -141,6 +141,9 @@
     <!-- Security Verification Dialog -->
     <el-dialog v-model="securityDialogVisible" title="安全验证" width="450px" :close-on-click-modal="false">
       <el-alert title="敏感操作需要验证身份" type="warning" :closable="false" style="margin-bottom: 20px;" />
+      <div v-if="securityAction === 'EDIT' && user && user.mobile" style="margin-bottom: 15px; color: #666; font-size: 14px; text-align: center;">
+          验证码将发送至您绑定的手机号:<strong>{{ user.mobile.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') }}</strong>
+      </div>
       <el-form :model="securityForm" label-width="100px">
         <el-form-item label="验证码" required>
             <div style="display: flex; gap: 10px; width: 100%;">
@@ -150,7 +153,7 @@
                 </el-button>
             </div>
         </el-form-item>
-        <el-form-item label="管理员密码" required>
+        <el-form-item label="管理员密码" required v-if="securityAction !== 'EDIT'">
           <el-input 
             v-model="securityForm.password" 
             type="password" 
@@ -454,18 +457,18 @@ const sendCode = async () => {
 }
 
 const confirmSecurityAction = async () => {
-    if (!securityForm.password || !securityForm.verificationCode) {
-        ElMessage.warning('请填写验证码和密码')
+    // Validate password only if not EDIT
+    if (!securityForm.verificationCode || (securityAction.value !== 'EDIT' && !securityForm.password)) {
+        ElMessage.warning(securityAction.value === 'EDIT' ? '请填写验证码' : '请填写验证码和密码')
         return
     }
     
     processing.value = true
     try {
         if (securityAction.value === 'EDIT' && currentId.value) {
-            // Merge form data with security data
+            // Merge form data with security data (exclude password)
             await updateApp(currentId.value, {
                 ...form,
-                password: securityForm.password,
                 verification_code: securityForm.verificationCode
             })
             ElMessage.success('更新成功')