liuq пре 2 месеци
родитељ
комит
c601b69c78

+ 105 - 4
backend/app/api/v1/endpoints/apps.py

@@ -15,6 +15,7 @@ from app.core import security
 from app.models.application import Application
 from app.models.user import User
 from app.models.mapping import AppUserMapping
+from app.core.utils import generate_english_name
 from app.schemas.application import (
     ApplicationCreate,
     ApplicationUpdate,
@@ -1014,32 +1015,132 @@ def sync_mapping(
 ):
     """
     从外部平台同步用户映射关系(机器对机器)。
-    只同步映射关系,不创建或更新用户本身。
+    支持增删改查:
+    - UPSERT (默认): 创建或更新映射及用户。
+    - DELETE: 仅删除应用与用户的映射关系,不删除用户。
     需要应用访问令牌 (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
+    in_name = sync_in.name if sync_in.name else None
+    in_english_name = sync_in.english_name if sync_in.english_name else None
+
+    # ==========================================
+    # 1. Handle DELETE Action
+    # ==========================================
+    if sync_in.sync_action == "DELETE":
+        # 查找用户
+        user = db.query(User).filter(User.mobile == sync_in.mobile).first()
+        if not user:
+            # 用户不存在,无法删除映射,直接抛出404或视作成功
+            raise HTTPException(status_code=404, detail="用户不存在")
+
+        # 查找映射
+        mapping = db.query(AppUserMapping).filter(
+            AppUserMapping.app_id == current_app.id,
+            AppUserMapping.user_id == user.id
+        ).first()
+
+        if not mapping:
+            raise HTTPException(status_code=404, detail="映射关系不存在")
+
+        # 构造返回数据(删除前快照,将状态置为 False)
+        resp_data = 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=user.mobile,
+            user_status=user.status,
+            is_active=False # 标记为非活跃/已删除
+        )
+
+        # 执行物理删除 (只删映射,不删用户)
+        db.delete(mapping)
+        db.commit()
+
+        # 记录日志
+        LogService.create_log(
+            db=db, 
+            app_id=current_app.id, 
+            operator_id=current_app.owner_id, 
+            action_type=ActionType.DELETE, 
+            target_user_id=user.id,
+            target_mobile=user.mobile,
+            details={"mapped_key": mapping.mapped_key, "action": "M2M_DELETE"}
+        )
+
+        return resp_data
+
+    # ==========================================
+    # 2. Handle UPSERT Action (Existing Logic)
+    # ==========================================
+
+    # 0. Check Uniqueness for Name and English Name (Global Check)
+    # We exclude the current user (by mobile) to allow updates to self without conflict
+    if in_name:
+        name_conflict = db.query(User).filter(
+            User.name == in_name, 
+            User.mobile != sync_in.mobile
+        ).first()
+        if name_conflict:
+            raise HTTPException(status_code=400, detail=f"姓名 '{in_name}' 已存在")
+
+    if in_english_name:
+        en_name_conflict = db.query(User).filter(
+            User.english_name == in_english_name, 
+            User.mobile != sync_in.mobile
+        ).first()
+        if en_name_conflict:
+            raise HTTPException(status_code=400, detail=f"英文名 '{in_english_name}' 已存在")
 
     # 1. Find User or Create
     user = db.query(User).filter(User.mobile == sync_in.mobile).first()
     new_user_created = False
+    
     if not user:
+        # Create New User
+        
+        # Auto-generate English name if missing but Chinese name is present
+        if in_name and not in_english_name:
+             in_english_name = generate_english_name(in_name)
+
+        # Validation: Name and English Name are required for new users
+        if not in_name or not in_english_name:
+            raise HTTPException(status_code=400, detail="新建用户必须提供姓名和英文名称")
+
         # Auto create user
         password = security.generate_alphanumeric_password(8) # Random password letters+digits
-        random_suffix = security.generate_alphanumeric_password(6)
+        
         user = User(
             mobile=sync_in.mobile,
             password_hash=security.get_password_hash(password),
             status="ACTIVE",
             role="ORDINARY_USER",
-            name=f"用户{random_suffix}",
-            english_name=mapped_key
+            name=in_name,
+            english_name=in_english_name
         )
         db.add(user)
         db.commit()
         db.refresh(user)
         new_user_created = True
+    else:
+        # Update Existing User (if fields provided)
+        updated = False
+        if in_name is not None and user.name != in_name:
+            user.name = in_name
+            updated = True
+        
+        if in_english_name is not None and user.english_name != in_english_name:
+            user.english_name = in_english_name
+            updated = True
+            
+        if updated:
+            db.add(user)
+            db.commit()
+            db.refresh(user)
 
     # 2. Handle Mapping
     mapping = db.query(AppUserMapping).filter(

+ 3 - 0
backend/app/schemas/user.py

@@ -42,11 +42,14 @@ class UserUpdate(BaseModel):
 
 class UserSyncRequest(BaseModel):
     mobile: str
+    name: Optional[str] = None
+    english_name: Optional[str] = None
     password: Optional[str] = None
     status: Optional[str] = None
     mapped_key: Optional[str] = None # External User ID
     mapped_email: Optional[str] = None # External User Email
     is_active: Optional[bool] = None # True=Active, False=Disabled. None=No Change
+    sync_action: Optional[str] = "UPSERT"
 
 class UserInDBBase(UserBase):
     id: int

+ 30 - 7
frontend/public/docs/account_sync.md

@@ -11,32 +11,55 @@
 - Token 可在平台管理界面的应用详情页查看。
 
 ## 3. 同步接口 (Sync)
-创建或更新用户,并建立应用映射关系。
+创建、更新或删除用户映射关系。
 
 - **URL**: `POST /apps/mapping/sync`
 - **Content-Type**: `application/json`
 
 ### 逻辑
-1. 根据 `mobile` 查找用户。若不存在则**自动创建**(随机密码)。
-2. 更新该用户与当前应用的映射 (`mapped_key`, `mapped_email`)。
+1. **增改模式 (UPSERT)**:
+   - 根据 `mobile` 查找用户。
+   - **新建用户**: 若用户不存在,必须提供 `name` 和 `english_name`,否则报错。
+   - **已有用户**: 若提供了 `name` 或 `english_name` 且不为空,则更新用户信息;否则保留原值。
+   - 建立或更新该用户与当前应用的映射 (`mapped_key`, `mapped_email`)。
+2. **删除模式 (DELETE)**:
+   - 仅删除该用户在当前应用下的映射关系。
+   - **不删除**平台上的用户账号。
 
 ### Request Body
 | Field | Type | Required | Description |
 |---|---|---|---|
 | `mobile` | string | Yes | 用户手机号 (平台唯一标识) |
+| `name` | string | Conditional | 用户姓名 (新建用户必填,已有用户选填) |
+| `english_name` | string | Conditional | 英文名/拼音 (新建用户必填,全局唯一) |
 | `mapped_key` | string | No | 外部系统用户ID |
 | `mapped_email` | string | No | 外部系统邮箱 |
 | `is_active` | boolean | No | 映射状态 (默认 true) |
+| `sync_action` | string | No | 操作类型: `UPSERT` (增改, 默认) 或 `DELETE` (删除映射) |
 
-### 示例
+### 示例 1: 新增或更新用户 (UPSERT)
 ```bash
 curl -X POST "{{API_BASE_URL}}/apps/mapping/sync" \
      -H "Content-Type: application/json" \
      -H "X-App-Access-Token: eyJhbGci..." \
      -d '{
            "mobile": "13800138000",
+           "name": "张三",
+           "english_name": "zhangsan",
            "mapped_key": "user_1001",
-           "mapped_email": "zhangsan@example.com"
+           "mapped_email": "zhangsan@example.com",
+           "sync_action": "UPSERT"
+         }'
+```
+
+### 示例 2: 删除映射 (DELETE)
+```bash
+curl -X POST "{{API_BASE_URL}}/apps/mapping/sync" \
+     -H "Content-Type: application/json" \
+     -H "X-App-Access-Token: eyJhbGci..." \
+     -d '{
+           "mobile": "13800138000",
+           "sync_action": "DELETE"
          }'
 ```
 
@@ -47,11 +70,11 @@ curl -X POST "{{API_BASE_URL}}/apps/mapping/sync" \
   "user_id": 456,      // 平台用户 ID
   "user_mobile": "13800138000",
   "mapped_key": "user_1001",
-  "is_active": true
+  "is_active": true    // 删除模式下会返回 false
 }
 ```
 
 ## 4. 错误码
-- `400 Bad Request`: 参数错误或映射关系冲突(如 mapped_key 已被占用)
+- `400 Bad Request`: 参数错误、姓名/英文名已存在、映射关系冲突
 - `403 Forbidden`: Token 无效。
 

+ 33 - 7
frontend/src/views/Help.vue

@@ -1427,16 +1427,26 @@ window.location.href = redirect_url;  // 直接跳转
             <ul>
               <li><strong>接口地址</strong>: <code>POST /api/v1/apps/mapping/sync</code></li>
               <li><strong>认证方式</strong>: 请求头需包含 <code>X-App-Access-Token</code>(可在应用详情页查看)。</li>
+              <li><strong>支持操作</strong>: 增改 (UPSERT) 和 删除 (DELETE)。</li>
             </ul>
             
             <div class="feature-card">
-              <h4>🛠️ 接口逻辑</h4>
+              <h4>🛠️ 接口逻辑 (UPSERT 模式)</h4>
               <ol>
-                <li>根据 <code>mobile</code> 查找用户。如果用户不存在,<strong>自动创建新用户</strong>(生成随机密码,默认激活)。</li>
+                <li>根据 <code>mobile</code> 查找用户。</li>
+                <li><strong>新建用户</strong>:若用户不存在,必须提供 <code>name</code> 和 <code>english_name</code>,否则报错。</li>
+                <li><strong>已有用户</strong>:若提供了 <code>name</code> 或 <code>english_name</code> 且不为空,则更新用户信息;否则保留原值。</li>
                 <li>将该用户与当前应用建立映射关系(绑定 <code>mapped_key</code> 和 <code>mapped_email</code>)。</li>
-                <li>如果映射已存在,则更新映射信息。</li>
               </ol>
             </div>
+
+            <div class="feature-card">
+              <h4>🗑️ 删除逻辑 (DELETE 模式)</h4>
+              <ul>
+                <li>仅删除用户在当前应用下的映射关系。</li>
+                <li><strong>不删除</strong>平台上的用户账号。</li>
+              </ul>
+            </div>
           </div>
 
           <div class="section">
@@ -1447,15 +1457,16 @@ window.location.href = redirect_url;  // 直接跳转
               </thead>
               <tbody>
                 <tr><td><code>mobile</code></td><td>string</td><td><span class="tag-required">是</span></td><td>用户手机号(平台唯一标识)</td></tr>
+                <tr><td><code>name</code></td><td>string</td><td><span class="tag-conditional">条件</span></td><td>姓名(新建用户必填)</td></tr>
+                <tr><td><code>english_name</code></td><td>string</td><td><span class="tag-conditional">条件</span></td><td>英文名(新建用户必填,全局唯一)</td></tr>
                 <tr><td><code>mapped_key</code></td><td>string</td><td><span class="tag-optional">否</span></td><td>外部系统中的用户ID(在该应用下唯一)</td></tr>
                 <tr><td><code>mapped_email</code></td><td>string</td><td><span class="tag-optional">否</span></td><td>外部系统中的邮箱(在该应用下唯一)</td></tr>
                 <tr><td><code>is_active</code></td><td>boolean</td><td><span class="tag-optional">否</span></td><td>映射关系状态(<code>true</code>启用,<code>false</code>禁用)</td></tr>
-                <tr><td><code>password</code></td><td>string</td><td><span class="tag-optional">否</span></td><td>(忽略)仅用于占位,不会更新用户密码</td></tr>
-                <tr><td><code>status</code></td><td>string</td><td><span class="tag-optional">否</span></td><td>(忽略)仅用于占位</td></tr>
+                <tr><td><code>sync_action</code></td><td>string</td><td><span class="tag-optional">否</span></td><td><code>UPSERT</code> (默认) 或 <code>DELETE</code></td></tr>
               </tbody>
             </table>
 
-            <p><strong>请求示例 (Curl):</strong></p>
+            <p><strong>请求示例 (UPSERT):</strong></p>
             <div class="code-block">
               <pre>
 curl -X POST "http://your-uap-domain/api/v1/apps/mapping/sync" \
@@ -1463,9 +1474,24 @@ curl -X POST "http://your-uap-domain/api/v1/apps/mapping/sync" \
      -H "X-App-Access-Token: YOUR_APP_ACCESS_TOKEN" \
      -d '{
            "mobile": "13800138000",
+           "name": "张三",
+           "english_name": "zhangsan",
            "mapped_key": "user_1001",
            "mapped_email": "zhangsan@example.com",
-           "is_active": true
+           "sync_action": "UPSERT"
+         }'
+              </pre>
+            </div>
+
+            <p><strong>请求示例 (DELETE):</strong></p>
+            <div class="code-block">
+              <pre>
+curl -X POST "http://your-uap-domain/api/v1/apps/mapping/sync" \
+     -H "Content-Type: application/json" \
+     -H "X-App-Access-Token: YOUR_APP_ACCESS_TOKEN" \
+     -d '{
+           "mobile": "13800138000",
+           "sync_action": "DELETE"
          }'
               </pre>
             </div>