Browse Source

手动新增账号添加验证

liuq 3 months ago
parent
commit
4d2edc6d06

+ 120 - 25
backend/app/api/v1/endpoints/apps.py

@@ -26,6 +26,8 @@ from app.schemas.mapping import (
     MappingList,
     MappingResponse,
     MappingCreate,
+    MappingUpdate,
+    MappingDelete,
     MappingPreviewResponse,
     MappingImportSummary,
     MappingStrategy
@@ -275,20 +277,33 @@ def create_mapping(
     if current_user.role != "SUPER_ADMIN" and app.owner_id != current_user.id:
         raise HTTPException(status_code=403, detail="权限不足")
 
+    # Verify Password
+    if not security.verify_password(mapping_in.password, current_user.password_hash):
+        raise HTTPException(status_code=401, detail="密码错误")
+
+    # Normalize input: treat empty strings as None to avoid unique constraint violations
+    mapped_key = mapping_in.mapped_key if mapping_in.mapped_key else None
+    mapped_email = mapping_in.mapped_email if mapping_in.mapped_email else None
+
     # 1. Find User or Create
     user = db.query(User).filter(User.mobile == mapping_in.mobile, User.is_deleted == 0).first()
+    new_user_created = False
+    generated_password = None
+
     if not user:
         # Auto create user
-        password = secrets.token_urlsafe(8) # Random password
+        password_plain = security.generate_alphanumeric_password(8) # Random password letters+digits
         user = User(
             mobile=mapping_in.mobile,
-            password_hash=security.get_password_hash(password),
+            password_hash=security.get_password_hash(password_plain),
             status="ACTIVE",
-            role="DEVELOPER"
+            role="ORDINARY_USER"
         )
         db.add(user)
         db.commit()
         db.refresh(user)
+        new_user_created = True
+        generated_password = password_plain
 
     # 2. Check if mapping exists
     existing = db.query(AppUserMapping).filter(
@@ -299,29 +314,29 @@ def create_mapping(
         raise HTTPException(status_code=400, detail="该用户的映射已存在")
 
     # 3. Check Uniqueness for mapped_email (if provided)
-    if mapping_in.mapped_email:
+    if mapped_email:
         email_exists = db.query(AppUserMapping).filter(
             AppUserMapping.app_id == app_id,
-            AppUserMapping.mapped_email == mapping_in.mapped_email
+            AppUserMapping.mapped_email == mapped_email
         ).first()
         if email_exists:
-            raise HTTPException(status_code=400, detail=f"该应用下邮箱 {mapping_in.mapped_email} 已被使用")
+            raise HTTPException(status_code=400, detail=f"该应用下邮箱 {mapped_email} 已被使用")
 
     # 4. Check Uniqueness for mapped_key
-    if mapping_in.mapped_key:
+    if mapped_key:
         key_exists = db.query(AppUserMapping).filter(
             AppUserMapping.app_id == app_id,
-            AppUserMapping.mapped_key == mapping_in.mapped_key
+            AppUserMapping.mapped_key == mapped_key
         ).first()
         if key_exists:
-            raise HTTPException(status_code=400, detail=f"该应用下账号 {mapping_in.mapped_key} 已被使用")
+            raise HTTPException(status_code=400, detail=f"该应用下账号 {mapped_key} 已被使用")
         
     # 5. Create
     mapping = AppUserMapping(
         app_id=app_id,
         user_id=user.id,
-        mapped_key=mapping_in.mapped_key,
-        mapped_email=mapping_in.mapped_email
+        mapped_key=mapped_key,
+        mapped_email=mapped_email
     )
     db.add(mapping)
     db.commit()
@@ -333,7 +348,78 @@ def create_mapping(
         user_id=mapping.user_id,
         mapped_key=mapping.mapped_key,
         mapped_email=mapping.mapped_email,
-        user_mobile=user.mobile
+        user_mobile=user.mobile,
+        new_user_created=new_user_created,
+        generated_password=generated_password
+    )
+
+@router.put("/{app_id}/mappings/{mapping_id}", response_model=MappingResponse, summary="更新映射")
+def update_mapping(
+    *,
+    db: Session = Depends(deps.get_db),
+    app_id: int,
+    mapping_id: int,
+    mapping_in: MappingUpdate,
+    current_user: User = Depends(deps.get_current_active_user),
+):
+    """
+    更新映射信息。
+    """
+    app = db.query(Application).filter(Application.id == app_id).first()
+    if not app:
+        raise HTTPException(status_code=404, detail="应用未找到")
+    
+    if current_user.role != "SUPER_ADMIN" and app.owner_id != current_user.id:
+        raise HTTPException(status_code=403, detail="权限不足")
+
+    # Verify Password
+    if not security.verify_password(mapping_in.password, current_user.password_hash):
+        raise HTTPException(status_code=401, detail="密码错误")
+
+    mapping = db.query(AppUserMapping).filter(
+        AppUserMapping.id == mapping_id,
+        AppUserMapping.app_id == app_id
+    ).first()
+    if not mapping:
+        raise HTTPException(status_code=404, detail="映射未找到")
+
+    # Check Uniqueness for mapped_key
+    if mapping_in.mapped_key is not None and mapping_in.mapped_key != mapping.mapped_key:
+        if mapping_in.mapped_key:
+            key_exists = db.query(AppUserMapping).filter(
+                AppUserMapping.app_id == app_id,
+                AppUserMapping.mapped_key == mapping_in.mapped_key
+            ).first()
+            if key_exists:
+                raise HTTPException(status_code=400, detail=f"该应用下账号 {mapping_in.mapped_key} 已被使用")
+
+    # Check Uniqueness for mapped_email
+    if mapping_in.mapped_email is not None and mapping_in.mapped_email != mapping.mapped_email:
+        if mapping_in.mapped_email:
+            email_exists = db.query(AppUserMapping).filter(
+                AppUserMapping.app_id == app_id,
+                AppUserMapping.mapped_email == mapping_in.mapped_email
+            ).first()
+            if email_exists:
+                raise HTTPException(status_code=400, detail=f"该应用下邮箱 {mapping_in.mapped_email} 已被使用")
+
+    if mapping_in.mapped_key is not None:
+        mapping.mapped_key = mapping_in.mapped_key
+    if mapping_in.mapped_email is not None:
+        mapping.mapped_email = mapping_in.mapped_email
+
+    db.add(mapping)
+    db.commit()
+    db.refresh(mapping)
+
+    return MappingResponse(
+        id=mapping.id,
+        app_id=mapping.app_id,
+        user_id=mapping.user_id,
+        mapped_key=mapping.mapped_key,
+        mapped_email=mapping.mapped_email,
+        user_mobile=mapping.user.mobile if mapping.user else "Deleted User",
+        is_active=mapping.is_active
     )
 
 @router.delete("/{app_id}/mappings/{mapping_id}", summary="删除映射")
@@ -342,11 +428,16 @@ def delete_mapping(
     db: Session = Depends(deps.get_db),
     app_id: int,
     mapping_id: int,
+    req: MappingDelete,
     current_user: User = Depends(deps.get_current_active_user),
 ):
     """
-    删除映射关系。
+    删除映射关系。需验证密码。
     """
+    # Verify Password
+    if not security.verify_password(req.password, current_user.password_hash):
+        raise HTTPException(status_code=401, detail="密码错误")
+
     mapping = db.query(AppUserMapping).filter(
         AppUserMapping.id == mapping_id,
         AppUserMapping.app_id == app_id
@@ -470,16 +561,20 @@ def sync_mapping(
     只同步映射关系,不创建或更新用户本身。
     需要应用访问令牌 (Authorization Bearer JWT 或 X-App-Access-Token)。
     """
+    # Normalize input: treat empty strings as None
+    mapped_key = sync_in.mapped_key if sync_in.mapped_key else None
+    mapped_email = sync_in.mapped_email if sync_in.mapped_email else None
+
     # 1. Find User or Create
     user = db.query(User).filter(User.mobile == sync_in.mobile).first()
     if not user:
         # Auto create user
-        password = secrets.token_urlsafe(8) # Random password
+        password = security.generate_alphanumeric_password(8) # Random password letters+digits
         user = User(
             mobile=sync_in.mobile,
             password_hash=security.get_password_hash(password),
             status="ACTIVE",
-            role="DEVELOPER"
+            role="ORDINARY_USER"
         )
         db.add(user)
         db.commit()
@@ -492,38 +587,38 @@ def sync_mapping(
     ).first()
 
     # Check Uniqueness for mapped_key (if changing or new, and provided)
-    if sync_in.mapped_key and (not mapping or mapping.mapped_key != sync_in.mapped_key):
+    if mapped_key and (not mapping or mapping.mapped_key != mapped_key):
         key_exists = db.query(AppUserMapping).filter(
             AppUserMapping.app_id == current_app.id,
-            AppUserMapping.mapped_key == sync_in.mapped_key
+            AppUserMapping.mapped_key == mapped_key
         ).first()
         if key_exists:
-             raise HTTPException(status_code=400, detail=f"该应用下账号 {sync_in.mapped_key} 已被使用")
+             raise HTTPException(status_code=400, detail=f"该应用下账号 {mapped_key} 已被使用")
 
     # Check Uniqueness for mapped_email (if changing or new, and provided)
-    if sync_in.mapped_email and (not mapping or mapping.mapped_email != sync_in.mapped_email):
+    if mapped_email and (not mapping or mapping.mapped_email != mapped_email):
         email_exists = db.query(AppUserMapping).filter(
             AppUserMapping.app_id == current_app.id,
-            AppUserMapping.mapped_email == sync_in.mapped_email
+            AppUserMapping.mapped_email == mapped_email
         ).first()
         if email_exists:
-             raise HTTPException(status_code=400, detail=f"该应用下邮箱 {sync_in.mapped_email} 已被使用")
+             raise HTTPException(status_code=400, detail=f"该应用下邮箱 {mapped_email} 已被使用")
 
     if mapping:
         # Update existing mapping
         if sync_in.mapped_key is not None:
-            mapping.mapped_key = sync_in.mapped_key
+            mapping.mapped_key = mapped_key
         if sync_in.is_active is not None:
             mapping.is_active = sync_in.is_active
         if sync_in.mapped_email is not None:
-            mapping.mapped_email = sync_in.mapped_email
+            mapping.mapped_email = mapped_email
     else:
         # Create new mapping
         mapping = AppUserMapping(
             app_id=current_app.id,
             user_id=user.id,
-            mapped_key=sync_in.mapped_key,
-            mapped_email=sync_in.mapped_email,
+            mapped_key=mapped_key,
+            mapped_email=mapped_email,
             is_active=sync_in.is_active if sync_in.is_active is not None else True
         )
         db.add(mapping)

+ 2 - 2
backend/app/api/v1/endpoints/simple_auth.py

@@ -152,8 +152,8 @@ def admin_reset_password(
     if not target_user:
         raise HTTPException(status_code=404, detail="用户未找到")
 
-    # Generate random password
-    new_pwd = security.generate_random_password()
+    # Generate random password (alphanumeric only)
+    new_pwd = security.generate_alphanumeric_password(8)
     target_user.password_hash = security.get_password_hash(new_pwd)
     db.add(target_user)
     db.commit()

+ 10 - 0
backend/app/core/security.py

@@ -34,3 +34,13 @@ def generate_random_password(length: int = 8) -> str:
                 and any(c.isdigit() for c in password)
                 and any(c in "!@#$%^&*" for c in password)):
             return password
+
+def generate_alphanumeric_password(length: int = 8) -> str:
+    """Generate a random password containing letters and digits only."""
+    alphabet = string.ascii_letters + string.digits
+    while True:
+        password = ''.join(secrets.choice(alphabet) for _ in range(length))
+        if (any(c.islower() for c in password)
+                and any(c.isupper() for c in password)
+                and any(c.isdigit() for c in password)):
+            return password

+ 11 - 0
backend/app/schemas/mapping.py

@@ -39,6 +39,15 @@ class MappingCreate(BaseModel):
     mobile: str
     mapped_key: Optional[str] = None
     mapped_email: Optional[str] = None
+    password: str # Admin password for verification
+
+class MappingUpdate(BaseModel):
+    mapped_key: Optional[str] = None
+    mapped_email: Optional[str] = None
+    password: str  # Required for verification
+
+class MappingDelete(BaseModel):
+    password: str
 
 class MappingResponse(BaseModel):
     id: int
@@ -48,6 +57,8 @@ class MappingResponse(BaseModel):
     mapped_email: Optional[str] = None
     user_mobile: str  # Convenient to have
     is_active: bool = True
+    new_user_created: bool = False
+    generated_password: Optional[str] = None
 
     class Config:
         from_attributes = True

+ 3 - 2
backend/app/services/mapping_service.py

@@ -80,7 +80,8 @@ class MappingService:
             mapped_key = str(row['mapped_key']).strip()
             mapped_email = None
             if 'mapped_email' in df.columns and not pd.isna(row['mapped_email']):
-                 mapped_email = str(row['mapped_email']).strip()
+                 val = str(row['mapped_email']).strip()
+                 mapped_email = val if val else None
             
             row_preview = MappingRowPreview(
                 row_index=index + 1, # 1-based index for UI
@@ -147,7 +148,7 @@ class MappingService:
                 
                 if row.status == MappingRowStatus.AUTO_CREATE_USER:
                     # Create User
-                    pwd = security.generate_random_password()
+                    pwd = security.generate_alphanumeric_password(8)
                     new_user = User(
                         mobile=row.mobile,
                         password_hash=security.get_password_hash(pwd),

+ 10 - 3
frontend/src/api/apps.ts

@@ -36,7 +36,10 @@ export interface MappingResponse {
   app_id: number
   user_id: number
   mapped_key: string
+  mapped_email?: string
   user_mobile: string
+  new_user_created?: boolean
+  generated_password?: string
 }
 
 export interface MappingListResponse {
@@ -73,12 +76,16 @@ export const getMappings = (appId: number, skip = 0, limit = 10) => {
   return api.get<MappingListResponse>(`/apps/${appId}/mappings`, { params: { skip, limit } })
 }
 
-export const createMapping = (appId: number, data: { mobile: string, mapped_key: string }) => {
+export const createMapping = (appId: number, data: { mobile: string, mapped_key?: string, mapped_email?: string, password: string }) => {
   return api.post<MappingResponse>(`/apps/${appId}/mappings`, data)
 }
 
-export const deleteMapping = (appId: number, mappingId: number) => {
-  return api.delete(`/apps/${appId}/mappings/${mappingId}`)
+export const updateMapping = (appId: number, mappingId: number, data: { mapped_key?: string, mapped_email?: string, password: string }) => {
+  return api.put<MappingResponse>(`/apps/${appId}/mappings/${mappingId}`, data)
+}
+
+export const deleteMapping = (appId: number, mappingId: number, password: string) => {
+  return api.delete(`/apps/${appId}/mappings/${mappingId}`, { data: { password } })
 }
 
 export const exportMappings = (appId: number) => {

+ 431 - 156
frontend/src/views/Help.vue

@@ -1,71 +1,76 @@
 <template>
   <div class="help-container">
-    <h1>统一认证平台 - 简易认证 (Simple Auth) 集成指南</h1>
-    <p class="intro">本指南适用于需要使用自定义登录页面(而非跳转到认证中心标准页面),并通过后端 API 直接进行用户认证的场景。</p>
-
-    <div class="section">
-      <h2>1. 核心流程图</h2>
-      <div class="flow-chart">
-        <div class="actor-row">
-          <div class="actor">用户 (User)</div>
-          <div class="actor">客户端 (Client)</div>
-          <div class="actor">统一认证平台 (UAP)</div>
-        </div>
-        
-        <div class="step">
-          <div class="arrow user-to-client">1. 输入账号/密码</div>
-        </div>
-        <div class="step">
-          <div class="self-action">2. 生成签名 (Sign)</div>
-        </div>
-        <div class="step">
-          <div class="arrow client-to-uap">3. POST /login (账号+密码+签名)</div>
-        </div>
-        <div class="step">
-          <div class="arrow uap-to-client dashed">4. 返回 Ticket (票据)</div>
-        </div>
-        <div class="step">
-          <div class="self-action">5. 内部逻辑处理</div>
-        </div>
-        <div class="step">
-          <div class="arrow client-to-uap">6. POST /validate (Ticket+签名)</div>
-        </div>
-        <div class="step">
-          <div class="arrow uap-to-client dashed">7. 返回 用户信息 (ID, Mobile...)</div>
-        </div>
-        <div class="step">
-          <div class="arrow client-to-user dashed">8. 登录成功</div>
-        </div>
-      </div>
-    </div>
-
-    <div class="section">
-      <h2>2. 前置准备</h2>
-      <p>在调用 API 之前,请确保您已在平台注册应用并获取以下信息:</p>
-      <ul>
-        <li><strong>App ID (<code>app_id</code>)</strong>: 应用唯一标识。</li>
-        <li><strong>App Secret (<code>app_secret</code>)</strong>: 应用密钥(<strong class="danger">严禁泄露给前端</strong>)。</li>
-      </ul>
-      <el-alert title="安全警告" type="error" :closable="false" show-icon class="alert-box">
-        <div>由于生成签名需要使用 <code>App Secret</code>,建议登录请求由您的<strong>应用后端</strong>发起,或者使用后端代理(BFF模式)。如果在前端(浏览器 JS)直接存储 Secret 并计算签名,极易导致密钥泄露。</div>
-      </el-alert>
-    </div>
-
-    <div class="section">
-      <h2>3. 签名算法 (Signature)</h2>
-      <p>所有涉及安全的接口都需要校验签名。</p>
-      <p><strong>签名生成步骤:</strong></p>
-      <ol>
-        <li><strong>准备参数</strong>:收集所有请求参数(不包括 <code>sign</code> 本身)。</li>
-        <li><strong>排序</strong>:按照参数名(key)的 ASCII 码从小到大排序。</li>
-        <li><strong>拼接</strong>:将排序后的参数拼接成 <code>key1=value1&key2=value2...</code> 格式的字符串。</li>
-        <li><strong>计算 HMAC</strong>:使用 <code>App Secret</code> 作为密钥,对拼接字符串进行 <strong>HMAC-SHA256</strong> 计算。</li>
-        <li><strong>转十六进制</strong>:将计算结果转换为 Hex 字符串即为签名。</li>
-      </ol>
-
-      <p><strong>Python 示例代码:</strong></p>
-      <div class="code-block">
-        <pre>
+    <h1>使用帮助</h1>
+    
+    <el-tabs v-model="activeTab" class="help-tabs">
+      <el-tab-pane label="自定义登录页面" name="custom-login">
+        <div class="help-content">
+          <h2>统一认证平台 - 简易认证 (Simple Auth) 集成指南</h2>
+          <p class="intro">本指南适用于需要使用自定义登录页面(而非跳转到认证中心标准页面),并通过后端 API 直接进行用户认证的场景。</p>
+
+          <div class="section">
+            <h3>1. 核心流程图</h3>
+            <div class="flow-chart">
+              <div class="actor-row">
+                <div class="actor">用户 (User)</div>
+                <div class="actor">客户端 (Client)</div>
+                <div class="actor">统一认证平台 (UAP)</div>
+              </div>
+              
+              <div class="step">
+                <div class="arrow user-to-client">1. 输入账号/密码</div>
+              </div>
+              <div class="step">
+                <div class="self-action">2. 生成签名 (Sign)</div>
+              </div>
+              <div class="step">
+                <div class="arrow client-to-uap">3. POST /login (账号+密码+签名)</div>
+              </div>
+              <div class="step">
+                <div class="arrow uap-to-client dashed">4. 返回 Ticket (票据)</div>
+              </div>
+              <div class="step">
+                <div class="self-action">5. 内部逻辑处理</div>
+              </div>
+              <div class="step">
+                <div class="arrow client-to-uap">6. POST /validate (Ticket+签名)</div>
+              </div>
+              <div class="step">
+                <div class="arrow uap-to-client dashed">7. 返回 用户信息 (ID, Mobile...)</div>
+              </div>
+              <div class="step">
+                <div class="arrow client-to-user dashed">8. 登录成功</div>
+              </div>
+            </div>
+          </div>
+
+          <div class="section">
+            <h3>2. 前置准备</h3>
+            <p>在调用 API 之前,请确保您已在平台注册应用并获取以下信息:</p>
+            <ul>
+              <li><strong>App ID (<code>app_id</code>)</strong>: 应用唯一标识。</li>
+              <li><strong>App Secret (<code>app_secret</code>)</strong>: 应用密钥(<strong class="danger">严禁泄露给前端</strong>)。</li>
+            </ul>
+            <el-alert title="安全警告" type="error" :closable="false" show-icon class="alert-box">
+              <div>由于生成签名需要使用 <code>App Secret</code>,建议登录请求由您的<strong>应用后端</strong>发起,或者使用后端代理(BFF模式)。如果在前端(浏览器 JS)直接存储 Secret 并计算签名,极易导致密钥泄露。</div>
+            </el-alert>
+          </div>
+
+          <div class="section">
+            <h3>3. 签名算法 (Signature)</h3>
+            <p>所有涉及安全的接口都需要校验签名。</p>
+            <p><strong>签名生成步骤:</strong></p>
+            <ol>
+              <li><strong>准备参数</strong>:收集所有请求参数(不包括 <code>sign</code> 本身)。</li>
+              <li><strong>排序</strong>:按照参数名(key)的 ASCII 码从小到大排序。</li>
+              <li><strong>拼接</strong>:将排序后的参数拼接成 <code>key1=value1&key2=value2...</code> 格式的字符串。</li>
+              <li><strong>计算 HMAC</strong>:使用 <code>App Secret</code> 作为密钥,对拼接字符串进行 <strong>HMAC-SHA256</strong> 计算。</li>
+              <li><strong>转十六进制</strong>:将计算结果转换为 Hex 字符串即为签名。</li>
+            </ol>
+
+            <p><strong>Python 示例代码:</strong></p>
+            <div class="code-block">
+              <pre>
 import hmac
 import hashlib
 import time
@@ -88,37 +93,37 @@ def generate_signature(secret: str, params: dict) -> str:
     ).hexdigest()
     
     return signature
-        </pre>
-      </div>
-    </div>
-
-    <div class="section">
-      <h2>4. 接口开发详解</h2>
-
-      <h3>4.1 第一步:密码登录获取票据 (Login)</h3>
-      <p>用户在界面输入账号密码后,调用此接口获取临时票据(Ticket)。</p>
-      <ul>
-        <li><strong>接口地址</strong>: <code>POST /api/v1/simple/login</code></li>
-        <li><strong>Content-Type</strong>: <code>application/json</code></li>
-      </ul>
-
-      <p><strong>请求参数 (JSON Body):</strong></p>
-      <table class="param-table">
-        <thead>
-          <tr><th>字段</th><th>类型</th><th>必填</th><th>说明</th></tr>
-        </thead>
-        <tbody>
-          <tr><td><code>app_id</code></td><td>string</td><td>是</td><td>您的应用 ID</td></tr>
-          <tr><td><code>identifier</code></td><td>string</td><td>是</td><td>用户标识(手机号、用户名或邮箱)</td></tr>
-          <tr><td><code>password</code></td><td>string</td><td>是</td><td>用户明文密码</td></tr>
-          <tr><td><code>timestamp</code></td><td>int</td><td>是</td><td>当前时间戳(秒),有效期 300秒</td></tr>
-          <tr><td><code>sign</code></td><td>string</td><td>是</td><td>签名字符串</td></tr>
-        </tbody>
-      </table>
-
-      <p><strong>请求示例:</strong></p>
-      <div class="code-block">
-        <pre>
+              </pre>
+            </div>
+          </div>
+
+          <div class="section">
+            <h3>4. 接口开发详解</h3>
+
+            <h4>4.1 第一步:密码登录获取票据 (Login)</h4>
+            <p>用户在界面输入账号密码后,调用此接口获取临时票据(Ticket)。</p>
+            <ul>
+              <li><strong>接口地址</strong>: <code>POST /api/v1/simple/login</code></li>
+              <li><strong>Content-Type</strong>: <code>application/json</code></li>
+            </ul>
+
+            <p><strong>请求参数 (JSON Body):</strong></p>
+            <table class="param-table">
+              <thead>
+                <tr><th>字段</th><th>类型</th><th>必填</th><th>说明</th></tr>
+              </thead>
+              <tbody>
+                <tr><td><code>app_id</code></td><td>string</td><td>是</td><td>您的应用 ID</td></tr>
+                <tr><td><code>identifier</code></td><td>string</td><td>是</td><td>用户标识(手机号、用户名或邮箱)</td></tr>
+                <tr><td><code>password</code></td><td>string</td><td>是</td><td>用户明文密码</td></tr>
+                <tr><td><code>timestamp</code></td><td>int</td><td>是</td><td>当前时间戳(秒),有效期 300秒</td></tr>
+                <tr><td><code>sign</code></td><td>string</td><td>是</td><td>签名字符串</td></tr>
+              </tbody>
+            </table>
+
+            <p><strong>请求示例:</strong></p>
+            <div class="code-block">
+              <pre>
 {
   "app_id": "your_app_id",
   "identifier": "13800138000",
@@ -126,56 +131,56 @@ def generate_signature(secret: str, params: dict) -> str:
   "timestamp": 1709876543,
   "sign": "a1b2c3d4e5..." 
 }
-        </pre>
-      </div>
+              </pre>
+            </div>
 
-      <p><strong>响应成功 (200 OK):</strong></p>
-      <div class="code-block">
-        <pre>
+            <p><strong>响应成功 (200 OK):</strong></p>
+            <div class="code-block">
+              <pre>
 {
   "ticket": "TICKET-7f8e9d0a-..." 
 }
-        </pre>
-      </div>
-
-      <el-divider />
-
-      <h3>4.2 第二步:验证票据并获取用户信息 (Validate)</h3>
-      <p>拿到 <code>ticket</code> 后,立即调用此接口解析出用户身份。</p>
-      <ul>
-        <li><strong>接口地址</strong>: <code>POST /api/v1/simple/validate</code></li>
-        <li><strong>Content-Type</strong>: <code>application/json</code></li>
-      </ul>
-
-      <p><strong>请求参数 (JSON Body):</strong></p>
-      <table class="param-table">
-        <thead>
-          <tr><th>字段</th><th>类型</th><th>必填</th><th>说明</th></tr>
-        </thead>
-        <tbody>
-          <tr><td><code>app_id</code></td><td>string</td><td>是</td><td>您的应用 ID</td></tr>
-          <tr><td><code>ticket</code></td><td>string</td><td>是</td><td>上一步获取到的票据</td></tr>
-          <tr><td><code>timestamp</code></td><td>int</td><td>是</td><td>当前时间戳</td></tr>
-          <tr><td><code>sign</code></td><td>string</td><td>是</td><td>签名(注意参数变化,需重新计算)</td></tr>
-        </tbody>
-      </table>
-
-      <p><strong>请求示例:</strong></p>
-      <div class="code-block">
-        <pre>
+              </pre>
+            </div>
+
+            <el-divider />
+
+            <h4>4.2 第二步:验证票据并获取用户信息 (Validate)</h4>
+            <p>拿到 <code>ticket</code> 后,立即调用此接口解析出用户身份。</p>
+            <ul>
+              <li><strong>接口地址</strong>: <code>POST /api/v1/simple/validate</code></li>
+              <li><strong>Content-Type</strong>: <code>application/json</code></li>
+            </ul>
+
+            <p><strong>请求参数 (JSON Body):</strong></p>
+            <table class="param-table">
+              <thead>
+                <tr><th>字段</th><th>类型</th><th>必填</th><th>说明</th></tr>
+              </thead>
+              <tbody>
+                <tr><td><code>app_id</code></td><td>string</td><td>是</td><td>您的应用 ID</td></tr>
+                <tr><td><code>ticket</code></td><td>string</td><td>是</td><td>上一步获取到的票据</td></tr>
+                <tr><td><code>timestamp</code></td><td>int</td><td>是</td><td>当前时间戳</td></tr>
+                <tr><td><code>sign</code></td><td>string</td><td>是</td><td>签名(注意参数变化,需重新计算)</td></tr>
+              </tbody>
+            </table>
+
+            <p><strong>请求示例:</strong></p>
+            <div class="code-block">
+              <pre>
 {
   "app_id": "your_app_id",
   "ticket": "TICKET-7f8e9d0a-...",
   "timestamp": 1709876545,
   "sign": "f9e8d7c6b5..."
 }
-        </pre>
-      </div>
+              </pre>
+            </div>
 
-      <p><strong>响应成功 (200 OK):</strong></p>
-      <p>此接口返回 <code>valid: true</code> 表示票据有效,并附带用户数据。</p>
-      <div class="code-block">
-        <pre>
+            <p><strong>响应成功 (200 OK):</strong></p>
+            <p>此接口返回 <code>valid: true</code> 表示票据有效,并附带用户数据。</p>
+            <div class="code-block">
+              <pre>
 {
   "valid": true,
   "user_id": 1001,
@@ -183,24 +188,27 @@ def generate_signature(secret: str, params: dict) -> str:
   "mapped_key": "user_zhangsan",  // 第三方映射ID(如果有)
   "mapped_email": "zhangsan@example.com" // 映射邮箱(如果有)
 }
-        </pre>
-      </div>
+              </pre>
+            </div>
 
-      <p><strong>响应失败 (票据无效或过期):</strong></p>
-      <div class="code-block">
-        <pre>
+            <p><strong>响应失败 (票据无效或过期):</strong></p>
+            <div class="code-block">
+              <pre>
 {
   "valid": false
 }
-        </pre>
-      </div>
-    </div>
-
-    <div class="section">
-      <h2>5. 完整调用示例 (Python)</h2>
-      <p>这是一个模拟客户端的完整脚本,演示如何登录并获取数据:</p>
-      <div class="code-block">
-        <pre>
+              </pre>
+            </div>
+          </div>
+
+          <div class="section">
+            <h3>5. 多语言调用示例</h3>
+            <p>以下提供了多种编程语言计算签名并发起请求的示例代码。</p>
+            
+            <el-tabs type="border-card" class="code-tabs">
+              <el-tab-pane label="Python">
+                <div class="code-block">
+                  <pre>
 import requests
 import time
 import hmac
@@ -264,14 +272,273 @@ def main():
 
 if __name__ == "__main__":
     main()
-        </pre>
-      </div>
-    </div>
+                  </pre>
+                </div>
+              </el-tab-pane>
+              
+              <el-tab-pane label="Java">
+                <div class="code-block">
+                  <pre>
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import java.util.*;
+import java.nio.charset.StandardCharsets;
+
+public class AuthExample {
+    private static final String SECRET = "secret_key_abc123";
+
+    public static String generateSign(Map&lt;String, String&gt; params) {
+        try {
+            // 1. 排序
+            List&lt;String&gt; sortedKeys = new ArrayList&lt;&gt;(params.keySet());
+            Collections.sort(sortedKeys);
+            
+            // 2. 拼接
+            StringBuilder sb = new StringBuilder();
+            for (String key : sortedKeys) {
+                if (!key.equals("sign") && params.get(key) != null) {
+                    if (sb.length() > 0) sb.append("&");
+                    sb.append(key).append("=").append(params.get(key));
+                }
+            }
+            
+            // 3. HMAC-SHA256
+            Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
+            SecretKeySpec secret_key = new SecretKeySpec(SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
+            sha256_HMAC.init(secret_key);
+            
+            byte[] bytes = sha256_HMAC.doFinal(sb.toString().getBytes(StandardCharsets.UTF_8));
+            
+            // 4. Hex
+            StringBuilder hex = new StringBuilder();
+            for (byte b : bytes) {
+                hex.append(String.format("%02x", b));
+            }
+            return hex.toString();
+        } catch (Exception e) {
+            e.printStackTrace();
+            return "";
+        }
+    }
+    
+    public static void main(String[] args) {
+        Map&lt;String, String&gt; params = new HashMap&lt;&gt;();
+        params.put("app_id", "test_app_001");
+        params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
+        
+        System.out.println("Sign: " + generateSign(params));
+    }
+}
+                  </pre>
+                </div>
+              </el-tab-pane>
+
+              <el-tab-pane label="Android (Kotlin)">
+                <div class="code-block">
+                  <pre>
+import javax.crypto.Mac
+import javax.crypto.spec.SecretKeySpec
+
+object AuthUtils {
+    private const val APP_SECRET = "secret_key_abc123"
+
+    fun generateSign(params: Map&lt;String, String&gt;): String {
+        // 1. 过滤 & 排序
+        val sortedKeys = params.keys.filter { it != "sign" }.sorted()
+
+        // 2. 拼接
+        val queryString = sortedKeys.joinToString("&") { key ->
+            "$key=${params[key]}"
+        }
+
+        // 3. HMAC-SHA256
+        val hmacSha256 = "HmacSHA256"
+        val secretKeySpec = SecretKeySpec(APP_SECRET.toByteArray(Charsets.UTF_8), hmacSha256)
+        val mac = Mac.getInstance(hmacSha256)
+        mac.init(secretKeySpec)
+        
+        val bytes = mac.doFinal(queryString.toByteArray(Charsets.UTF_8))
+        
+        // 4. Hex
+        return bytes.joinToString("") { "%02x".format(it) }
+    }
+}
+
+// Usage Example
+fun main() {
+    val params = mapOf(
+        "app_id" to "test_app_001",
+        "identifier" to "13800000001",
+        "timestamp" to (System.currentTimeMillis() / 1000).toString()
+    )
+    val sign = AuthUtils.generateSign(params)
+    println("Signature: $sign")
+}
+                  </pre>
+                </div>
+              </el-tab-pane>
+
+              <el-tab-pane label="JavaScript (Node.js)">
+                <div class="code-block">
+                  <pre>
+const crypto = require('crypto');
+const axios = require('axios'); // npm install axios
+
+const APP_ID = 'test_app_001';
+const APP_SECRET = 'secret_key_abc123';
+const BASE_URL = 'http://localhost:8000/api/v1/simple';
+
+function getSign(params) {
+  // 1. 过滤 & 排序
+  const keys = Object.keys(params)
+    .filter(k => k !== 'sign' && params[k] !== undefined)
+    .sort();
+  
+  // 2. 拼接 Query String
+  const queryString = keys.map(k => `${k}=${params[k]}`).join('&');
+  
+  // 3. HMAC-SHA256
+  return crypto.createHmac('sha256', APP_SECRET)
+    .update(queryString)
+    .digest('hex');
+}
+
+async function login() {
+  const timestamp = Math.floor(Date.now() / 1000);
+  const payload = {
+    app_id: APP_ID,
+    identifier: '13800000001',
+    password: 'password123',
+    timestamp: timestamp
+  };
+  
+  payload.sign = getSign(payload);
+  
+  try {
+    console.log('Sending login request...');
+    const res = await axios.post(`${BASE_URL}/login`, payload);
+    console.log('Ticket:', res.data.ticket);
+  } catch (error) {
+    console.error('Login Failed:', error.response?.data || error.message);
+  }
+}
+
+login();
+                  </pre>
+                </div>
+              </el-tab-pane>
+
+              <el-tab-pane label="Go">
+                <div class="code-block">
+                  <pre>
+package main
+
+import (
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/hex"
+	"fmt"
+	"sort"
+	"strings"
+	"time"
+)
+
+func GetSign(secret string, params map[string]interface{}) string {
+	// 1. 提取 Key
+	var keys []string
+	for k := range params {
+		if k != "sign" {
+			keys = append(keys, k)
+		}
+	}
+	// 2. 排序
+	sort.Strings(keys)
+
+	// 3. 拼接
+	var parts []string
+	for _, k := range keys {
+		val := fmt.Sprintf("%v", params[k])
+		parts = append(parts, fmt.Sprintf("%s=%s", k, val))
+	}
+	query := strings.Join(parts, "&")
+
+	// 4. HMAC-SHA256
+	h := hmac.New(sha256.New, []byte(secret))
+	h.Write([]byte(query))
+	return hex.EncodeToString(h.Sum(nil))
+}
+
+func main() {
+    params := map[string]interface{}{
+        "app_id":    "test_app_001",
+        "identifier": "13800000001",
+        "password":   "123456",
+        "timestamp":  time.Now().Unix(),
+    }
+    
+    secret := "secret_key_abc123"
+    sign := GetSign(secret, params)
+    fmt.Printf("Signature: %s\n", sign)
+}
+                  </pre>
+                </div>
+              </el-tab-pane>
+
+              <el-tab-pane label="Swift">
+                <div class="code-block">
+                  <pre>
+import Foundation
+import CommonCrypto
+
+// 注意:需要添加 Bridging Header 引入 CommonCrypto 或直接在 Linux 环境使用
+
+func hmac(string: String, key: String) -> String {
+    var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
+    CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), key, key.count, string, string.count, &digest)
+    let data = Data(digest)
+    return data.map { String(format: "%02hhx", $0) }.joined()
+}
+
+func generateSign(secret: String, params: [String: Any]) -> String {
+    // 1. 过滤 & 排序
+    let sortedKeys = params.keys.filter { $0 != "sign" }.sorted()
+    
+    // 2. 拼接
+    let queryParts = sortedKeys.map { key in
+        return "\(key)=\(params[key]!)"
+    }
+    let queryString = queryParts.joined(separator: "&")
+    
+    // 3. HMAC
+    return hmac(string: queryString, key: secret)
+}
+
+// Usage
+let params: [String: Any] = [
+    "app_id": "test_app_001",
+    "timestamp": Int(Date().timeIntervalSince1970)
+]
+let sign = generateSign(secret: "secret_123", params: params)
+print(sign)
+                  </pre>
+                </div>
+              </el-tab-pane>
+            </el-tabs>
+          </div>
+        </div>
+      </el-tab-pane>
+
+      <el-tab-pane label="其他帮助 (待定)" name="tbd">
+        <el-empty description="更多帮助文档敬请期待..." />
+      </el-tab-pane>
+    </el-tabs>
   </div>
 </template>
 
 <script setup lang="ts">
-// 这是一个纯静态展示页面,不需要额外的脚本逻辑
+import { ref } from 'vue'
+
+const activeTab = ref('custom-login')
 </script>
 
 <style scoped>
@@ -297,7 +564,7 @@ h1 {
 
 h2 {
   font-size: 22px;
-  margin-top: 35px;
+  margin-top: 20px;
   margin-bottom: 20px;
   color: #1f2f3d;
   font-weight: 600;
@@ -313,6 +580,14 @@ h3 {
   font-weight: 600;
 }
 
+h4 {
+  font-size: 16px;
+  margin-top: 20px;
+  margin-bottom: 10px;
+  color: #303133;
+  font-weight: bold;
+}
+
 p {
   font-size: 15px;
   line-height: 1.7;
@@ -507,4 +782,4 @@ li {
     display: none; /* 手机端太窄,建议隐藏或换图 */
   }
 }
-</style>
+</style>

+ 47 - 0
frontend/src/views/UserList.vue

@@ -91,6 +91,11 @@
                         >
                             变更角色
                         </el-dropdown-item>
+                        <el-dropdown-item 
+                            @click="handleResetPassword(scope.row)"
+                        >
+                            重置密码
+                        </el-dropdown-item>
                       </el-dropdown-menu>
                     </template>
                 </el-dropdown>
@@ -135,6 +140,23 @@
         </template>
     </el-dialog>
 
+    <!-- Reset Password Dialog -->
+    <el-dialog v-model="resetPasswordDialogVisible" title="重置密码成功" width="400px">
+      <div style="text-align: center;">
+        <p>用户 <b>{{ resetPasswordUserMobile }}</b> 的新密码为:</p>
+        <div style="margin: 20px 0; font-size: 24px; font-weight: bold; color: #409EFF; background: #f4f4f5; padding: 10px; border-radius: 4px;">
+          {{ newPassword }}
+        </div>
+        <p style="color: #f56c6c; font-size: 12px;">请立即复制并保存,此密码只显示一次!</p>
+      </div>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="copyPassword">复制密码</el-button>
+          <el-button type="primary" @click="resetPasswordDialogVisible = false">关闭</el-button>
+        </span>
+      </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">
@@ -301,6 +323,31 @@ const handleStatus = async (user: User, newStatus: string) => {
   }
 }
 
+// Reset Password Logic
+const resetPasswordDialogVisible = ref(false)
+const newPassword = ref('')
+const resetPasswordUserMobile = ref('')
+
+const handleResetPassword = async (user: User) => {
+  try {
+    const res = await api.post('/simple/admin/reset-password', { user_id: user.id })
+    newPassword.value = res.data.new_password
+    resetPasswordUserMobile.value = user.mobile
+    resetPasswordDialogVisible.value = true
+  } catch (e) {
+    // handled
+  }
+}
+
+const copyPassword = async () => {
+  try {
+    await navigator.clipboard.writeText(newPassword.value)
+    ElMessage.success('密码已复制到剪贴板')
+  } catch (err) {
+    ElMessage.error('复制失败,请手动复制')
+  }
+}
+
 // Change Role Logic
 const changeRoleDialogVisible = ref(false)
 const roleTarget = ref<User | null>(null)

+ 44 - 0
frontend/src/views/admin/UserList.vue

@@ -67,6 +67,8 @@
               启用
             </el-button>
             <el-divider direction="vertical" />
+            <el-button type="primary" size="small" @click="handleResetPassword(scope.row)">重置密码</el-button>
+            <el-divider direction="vertical" />
             <el-popconfirm title="确定要删除该用户吗?(逻辑删除)" @confirm="handleDelete(scope.row)">
               <template #reference>
                 <el-button type="danger" size="small">删除</el-button>
@@ -76,6 +78,23 @@
         </template>
       </el-table-column>
     </el-table>
+
+    <!-- Reset Password Dialog -->
+    <el-dialog v-model="resetPasswordDialogVisible" title="重置密码成功" width="400px">
+      <div style="text-align: center;">
+        <p>用户 <b>{{ resetPasswordUserMobile }}</b> 的新密码为:</p>
+        <div style="margin: 20px 0; font-size: 24px; font-weight: bold; color: #409EFF; background: #f4f4f5; padding: 10px; border-radius: 4px;">
+          {{ newPassword }}
+        </div>
+        <p style="color: #f56c6c; font-size: 12px;">请立即复制并保存,此密码只显示一次!</p>
+      </div>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="copyPassword">复制密码</el-button>
+          <el-button type="primary" @click="resetPasswordDialogVisible = false">关闭</el-button>
+        </span>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
@@ -97,6 +116,11 @@ const loading = ref(false)
 const filterStatus = ref('')
 const filterRole = ref('')
 
+// Reset Password
+const resetPasswordDialogVisible = ref(false)
+const newPassword = ref('')
+const resetPasswordUserMobile = ref('')
+
 const fetchUsers = async () => {
   loading.value = true
   try {
@@ -132,6 +156,26 @@ const handleUpdateStatus = async (user: User, newStatus: string) => {
   }
 }
 
+const handleResetPassword = async (user: User) => {
+  try {
+    const res = await api.post('/simple/admin/reset-password', { user_id: user.id })
+    newPassword.value = res.data.new_password
+    resetPasswordUserMobile.value = user.mobile
+    resetPasswordDialogVisible.value = true
+  } catch (e) {
+    // handled
+  }
+}
+
+const copyPassword = async () => {
+  try {
+    await navigator.clipboard.writeText(newPassword.value)
+    ElMessage.success('密码已复制到剪贴板')
+  } catch (err) {
+    ElMessage.error('复制失败,请手动复制')
+  }
+}
+
 const handleDelete = async (user: User) => {
   try {
     await api.delete(`/users/${user.id}`)

+ 127 - 16
frontend/src/views/apps/MappingImport.vue

@@ -19,8 +19,9 @@
           <el-table-column prop="user_mobile" label="用户手机号" />
           <el-table-column prop="mapped_key" label="第三方系统账号 (Key)" />
           <el-table-column prop="mapped_email" label="第三方系统邮箱" />
-          <el-table-column label="操作" width="120">
+          <el-table-column label="操作" width="180">
             <template #default="scope">
+              <el-button type="primary" size="small" icon="Edit" @click="handleEdit(scope.row)">编辑</el-button>
               <el-button type="danger" size="small" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
             </template>
           </el-table-column>
@@ -117,7 +118,7 @@
     <!-- Manual Add Dialog -->
     <el-dialog v-model="addDialogVisible" title="添加映射" width="400px">
       <el-form :model="addForm" label-width="100px">
-        <el-form-item label="用户手机号">
+        <el-form-item label="用户手机号" required>
           <el-input v-model="addForm.mobile" placeholder="系统内用户手机号" />
         </el-form-item>
         <el-form-item label="映射账号">
@@ -126,6 +127,9 @@
         <el-form-item label="映射邮箱">
           <el-input v-model="addForm.mapped_email" placeholder="第三方系统邮箱(可选)" />
         </el-form-item>
+        <el-form-item label="管理员密码" required>
+          <el-input v-model="addForm.password" type="password" placeholder="请输入您的登录密码确认" show-password />
+        </el-form-item>
       </el-form>
       <template #footer>
         <span class="dialog-footer">
@@ -134,15 +138,39 @@
         </span>
       </template>
     </el-dialog>
+
+    <!-- Edit Dialog -->
+    <el-dialog v-model="editDialogVisible" title="编辑映射" width="400px">
+      <el-form :model="editForm" label-width="100px">
+        <el-form-item label="用户手机号">
+          <el-input v-model="editForm.mobile" disabled />
+        </el-form-item>
+        <el-form-item label="映射账号">
+          <el-input v-model="editForm.mapped_key" placeholder="第三方系统账号ID" />
+        </el-form-item>
+        <el-form-item label="映射邮箱">
+          <el-input v-model="editForm.mapped_email" placeholder="第三方系统邮箱(可选)" />
+        </el-form-item>
+        <el-form-item label="管理员密码" required>
+          <el-input v-model="editForm.password" type="password" placeholder="请输入您的登录密码确认" show-password />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <span class="dialog-footer">
+          <el-button @click="editDialogVisible = false">取消</el-button>
+          <el-button type="primary" @click="confirmEdit" :loading="editing">确定</el-button>
+        </span>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup lang="ts">
 import { ref, onMounted, reactive } from 'vue'
 import { useRoute } from 'vue-router'
-import { UploadFilled, ArrowLeft, Plus, Download, Delete } from '@element-plus/icons-vue'
+import { UploadFilled, ArrowLeft, Plus, Download, Delete, Edit } from '@element-plus/icons-vue'
 import { previewMapping, importMapping } from '../../api/mapping'
-import { getMappings, createMapping, deleteMapping, exportMappings, MappingResponse } from '../../api/apps'
+import { getMappings, createMapping, deleteMapping, updateMapping, exportMappings, MappingResponse } from '../../api/apps'
 import { ElMessage, ElMessageBox } from 'element-plus'
 
 const route = useRoute()
@@ -183,12 +211,27 @@ const handleCurrentChange = (val: number) => {
 }
 
 const handleDelete = (row: MappingResponse) => {
-  ElMessageBox.confirm(`确定要删除 ${row.user_mobile} 的映射吗?`, '警告', {
+  ElMessageBox.prompt(`请输入您的登录密码以确认删除 ${row.user_mobile} 的映射`, '安全验证', {
+    confirmButtonText: '删除',
+    cancelButtonText: '取消',
+    inputType: 'password',
+    inputPattern: /.+/,
+    inputErrorMessage: '密码不能为空',
     type: 'warning'
-  }).then(async () => {
-    await deleteMapping(appId, row.id)
-    ElMessage.success('删除成功')
-    fetchMappings()
+  }).then(async ({ value }) => {
+    try {
+      await deleteMapping(appId, row.id, value)
+      ElMessage.success('删除成功')
+      fetchMappings()
+    } catch (e: any) {
+      if (e.response && e.response.status === 401) {
+        ElMessage.error('密码错误')
+      } else {
+        ElMessage.error('删除失败')
+      }
+    }
+  }).catch(() => {
+    // cancelled
   })
 }
 
@@ -214,34 +257,102 @@ const adding = ref(false)
 const addForm = reactive({
   mobile: '',
   mapped_key: '',
-  mapped_email: ''
+  mapped_email: '',
+  password: ''
 })
 
 const openAddDialog = () => {
   addForm.mobile = ''
   addForm.mapped_key = ''
   addForm.mapped_email = ''
+  addForm.password = ''
   addDialogVisible.value = true
 }
 
 const confirmAdd = async () => {
-  if (!addForm.mobile || !addForm.mapped_key) {
-    ElMessage.warning('请填写完整')
+  if (!addForm.mobile || !addForm.mapped_key || !addForm.password) {
+    ElMessage.warning('请填写完整,包括管理员密码')
     return
   }
   adding.value = true
   try {
-    await createMapping(appId, addForm)
-    ElMessage.success('添加成功')
+    const res = await createMapping(appId, addForm)
     addDialogVisible.value = false
     fetchMappings()
-  } catch (e) {
-    // handled
+    
+    if (res.data.new_user_created && res.data.generated_password) {
+      ElMessageBox.alert(
+        `
+        <div>已自动为您创建统一认证账号:</div>
+        <div style="margin-top: 10px;"><b>手机号:</b> ${res.data.user_mobile}</div>
+        <div style="margin-top: 5px;"><b>密码:</b> <span style="color: #f56c6c; font-weight: bold;">${res.data.generated_password}</span></div>
+        <div style="margin-top: 10px; color: #909399; font-size: 12px;">请妥善保存密码。</div>
+        `,
+        '账号创建成功',
+        {
+          dangerouslyUseHTMLString: true,
+          confirmButtonText: '知道了',
+          type: 'success'
+        }
+      )
+    } else {
+      ElMessage.success('映射添加成功 (统一认证账号已存在)')
+    }
+  } catch (e: any) {
+    if (e.response && e.response.status === 401) {
+      ElMessage.error('密码错误')
+    }
+    // other errors handled by interceptor
   } finally {
     adding.value = false
   }
 }
 
+// --- Edit Logic ---
+const editDialogVisible = ref(false)
+const editing = ref(false)
+const editForm = reactive({
+  id: 0,
+  mobile: '',
+  mapped_key: '',
+  mapped_email: '',
+  password: ''
+})
+
+const handleEdit = (row: MappingResponse) => {
+  editForm.id = row.id
+  editForm.mobile = row.user_mobile
+  editForm.mapped_key = row.mapped_key
+  editForm.mapped_email = row.mapped_email || ''
+  editForm.password = ''
+  editDialogVisible.value = true
+}
+
+const confirmEdit = async () => {
+  if (!editForm.password) {
+    ElMessage.warning('请输入密码')
+    return
+  }
+  editing.value = true
+  try {
+    await updateMapping(appId, editForm.id, {
+      mapped_key: editForm.mapped_key || undefined,
+      mapped_email: editForm.mapped_email || undefined,
+      password: editForm.password
+    })
+    ElMessage.success('更新成功')
+    editDialogVisible.value = false
+    fetchMappings()
+  } catch (e: any) {
+    if (e.response && e.response.status === 401) {
+      ElMessage.error('密码错误')
+    }
+    // Other errors handled by interceptor
+  } finally {
+    editing.value = false
+  }
+}
+
 // --- Import Logic ---
 const previewData = ref<any>(null)
 const selectedFile = ref<File | null>(null)