Ver código fonte

完善日志

liuq 2 meses atrás
pai
commit
b436d40b91

+ 69 - 37
backend/app/api/v1/endpoints/apps.py

@@ -3,12 +3,13 @@ import string
 import io
 import csv
 import pandas as pd
+import logging
 from typing import List
 from datetime import datetime
 from fastapi import APIRouter, Depends, HTTPException, Response, UploadFile, File, Form, Query
 from fastapi.responses import StreamingResponse
 from sqlalchemy.orm import Session
-from sqlalchemy import desc
+from sqlalchemy import desc, or_
 
 from app.api.v1 import deps
 from app.core import security
@@ -45,6 +46,7 @@ from app.services.log_service import LogService
 from app.schemas.operation_log import ActionType, OperationLogList, OperationLogResponse
 
 router = APIRouter()
+logger = logging.getLogger(__name__)
 
 def generate_access_token():
     return secrets.token_urlsafe(32)
@@ -76,7 +78,6 @@ def read_apps(
     
     if search:
         # Search by name or app_id
-        from sqlalchemy import or_
         query = query.filter(
             or_(
                 Application.app_name.ilike(f"%{search}%"),
@@ -140,6 +141,8 @@ def create_app(
     db.commit()
     db.refresh(db_app)
 
+    logger.info(f"应用创建成功: {app_in.app_name} (ID: {app_id}, Owner: {current_user.mobile})")
+    
     return ApplicationSecretDisplay(app_id=app_id, app_secret=app_secret, access_token=access_token)
 
 @router.put("/{app_id}", response_model=ApplicationResponse, summary="更新应用")
@@ -166,6 +169,7 @@ def update_app(
          raise HTTPException(status_code=400, detail="需要提供手机验证码")
 
     if not SmsService.verify_code(current_user.mobile, app_in.verification_code):
+        logger.warning(f"应用更新失败: 验证码错误 (User: {current_user.mobile})")
         raise HTTPException(status_code=400, detail="验证码无效或已过期")
         
     update_data = app_in.model_dump(exclude_unset=True)
@@ -188,6 +192,8 @@ def update_app(
         action_type=ActionType.UPDATE,
         details=update_data
     )
+    
+    logger.info(f"应用更新成功: {app.app_name} (ID: {app.app_id})")
 
     return app
 
@@ -212,6 +218,8 @@ def delete_app(
     app.is_deleted = True
     db.add(app)
     db.commit()
+    
+    logger.info(f"应用删除成功: {app.app_name} (ID: {app.app_id}, Operator: {current_user.mobile})")
     return app
 
 @router.post("/{app_id}/regenerate-secret", response_model=ApplicationSecretDisplay, summary="重新生成密钥")
@@ -235,9 +243,11 @@ def regenerate_secret(
         
     # Security Verification
     if not security.verify_password(req.password, current_user.password_hash):
+        logger.warning(f"重置密钥失败: 密码错误 (User: {current_user.mobile})")
         raise HTTPException(status_code=403, detail="密码错误")
 
     if not SmsService.verify_code(current_user.mobile, req.verification_code):
+        logger.warning(f"重置密钥失败: 验证码错误 (User: {current_user.mobile})")
         raise HTTPException(status_code=400, detail="验证码无效或已过期")
 
     _, new_secret = generate_app_credentials()
@@ -255,6 +265,8 @@ def regenerate_secret(
         details={"message": "Regenerated App Secret"}
     )
 
+    logger.info(f"应用密钥已重置: {app.app_name} (ID: {app.app_id})")
+
     return ApplicationSecretDisplay(app_id=app.app_id, app_secret=new_secret, access_token=app.access_token)
 
 @router.post("/{app_id}/view-secret", response_model=ApplicationSecretDisplay, summary="查看密钥")
@@ -270,6 +282,7 @@ def view_secret(
     """
     # 1. Verify Password
     if not security.verify_password(req.password, current_user.password_hash):
+        logger.warning(f"查看密钥失败: 密码错误 (User: {current_user.mobile})")
         raise HTTPException(status_code=403, detail="密码错误")
         
     app = db.query(Application).filter(Application.id == app_id).first()
@@ -288,6 +301,8 @@ def view_secret(
         action_type=ActionType.VIEW_SECRET,
         details={"message": "Viewed App Secret"}
     )
+    
+    logger.info(f"查看应用密钥: {app.app_name} (Operator: {current_user.mobile})")
 
     return ApplicationSecretDisplay(app_id=app.app_id, app_secret=app.app_secret, access_token=app.access_token)
 
@@ -313,10 +328,12 @@ def transfer_app(
 
     # 1. Verify Password
     if not security.verify_password(req.password, current_user.password_hash):
+        logger.warning(f"转让应用失败: 密码错误 (User: {current_user.mobile})")
         raise HTTPException(status_code=403, detail="密码错误")
 
     # 2. Verify SMS Code
     if not SmsService.verify_code(current_user.mobile, req.verification_code):
+        logger.warning(f"转让应用失败: 验证码错误 (User: {current_user.mobile})")
         raise HTTPException(status_code=400, detail="验证码无效或已过期")
 
     # 3. Verify Target User
@@ -355,6 +372,8 @@ def transfer_app(
         }
     )
     
+    logger.info(f"应用转让成功: {app.app_name} 从 {current_user.mobile} 转让给 {target_user.mobile}")
+    
     return app
 
 # ==========================================
@@ -422,6 +441,7 @@ def create_mapping(
 
     # Verify Password
     if not security.verify_password(mapping_in.password, current_user.password_hash):
+        logger.warning(f"创建映射失败: 密码错误 (User: {current_user.mobile})")
         raise HTTPException(status_code=403, detail="密码错误")
 
     # Normalize input: treat empty strings as None to avoid unique constraint violations
@@ -450,6 +470,7 @@ def create_mapping(
         db.refresh(user)
         new_user_created = True
         generated_password = password_plain
+        logger.info(f"自动创建用户: {user.mobile}")
 
     # 2. Check if mapping exists
     existing = db.query(AppUserMapping).filter(
@@ -502,6 +523,8 @@ def create_mapping(
             "new_user_created": new_user_created
         }
     )
+    
+    logger.info(f"映射创建成功: App {app_id} -> User {user.mobile} ({mapped_key})")
 
     return MappingResponse(
         id=mapping.id,
@@ -536,6 +559,7 @@ def update_mapping(
 
     # Verify Password
     if not security.verify_password(mapping_in.password, current_user.password_hash):
+        logger.warning(f"更新映射失败: 密码错误 (User: {current_user.mobile})")
         raise HTTPException(status_code=403, detail="密码错误")
 
     mapping = db.query(AppUserMapping).filter(
@@ -591,6 +615,8 @@ def update_mapping(
             "new": {"mapped_key": mapping.mapped_key, "mapped_email": mapping.mapped_email}
         }
     )
+    
+    logger.info(f"映射更新成功: App {app_id} User {mapping.user.mobile if mapping.user else 'unknown'}")
 
     return MappingResponse(
         id=mapping.id,
@@ -617,6 +643,7 @@ def delete_mapping(
     """
     # Verify Password
     if not security.verify_password(req.password, current_user.password_hash):
+        logger.warning(f"删除映射失败: 密码错误 (User: {current_user.mobile})")
         raise HTTPException(status_code=403, detail="密码错误")
 
     mapping = db.query(AppUserMapping).filter(
@@ -648,6 +675,8 @@ def delete_mapping(
         details={"mapping_id": mapping_id}
     )
     
+    logger.info(f"映射删除成功: App {app_id}, Mapping {mapping_id}")
+    
     return {"message": "删除成功"}
 
 @router.post("/{app_id}/sync-users", summary="同步所有用户")
@@ -659,8 +688,6 @@ def sync_users_to_app(
 ):
     """
     一键导入用户管理中的用户数据到应用映射中。
-    规则:如果用户已在映射中(基于手机号/User ID),则跳过。
-    否则创建映射,使用英文名称作为映射账号(如果为空则使用手机号)。
     """
     app = db.query(Application).filter(Application.id == app_id).first()
     if not app:
@@ -677,30 +704,15 @@ def sync_users_to_app(
     mapped_user_ids = {m.user_id for m in existing_mappings}
     
     new_mappings = []
+    logger.info(f"开始同步用户到应用 {app.app_name} (ID: {app_id})")
     
     for user in users:
         if user.id in mapped_user_ids:
             continue
             
         # Create mapping
-        # Rule: Use English name as mapped_key. Fallback to mobile.
         mapped_key = user.english_name if user.english_name else user.mobile
         
-        # Check if mapped_key is already used in this app (unlikely for User ID check passed, but mapped_key might collide if english names are dupes)
-        # However, bulk insert for thousands might be slow if we check one by one.
-        # Ideally we should fetch all existing mapped_keys too.
-        
-        # For simplicity in "one-click sync", we might just proceed. 
-        # But if mapped_key is not unique in AppUserMapping (uq_app_mapped_key), it will fail.
-        # Let's assume English Name is unique enough or we catch error? 
-        # Actually, let's verify if english_name is unique in User table. It's not (User.english_name is nullable and not unique).
-        # So multiple users might have same english_name.
-        # If so, we should probably append mobile or something to make it unique? 
-        # Or just use mobile if english_name is duplicate?
-        
-        # Re-reading: "没有存在则使用手机号码,英文名称作为账号映射" -> "If not exists, use phone number, English name as account mapping".
-        # This could mean: Use English Name.
-        
         mapping = AppUserMapping(
             app_id=app.id,
             user_id=user.id,
@@ -714,9 +726,11 @@ def sync_users_to_app(
         try:
             db.bulk_save_objects(new_mappings)
             db.commit()
+            logger.info(f"用户同步完成: 新增 {len(new_mappings)} 条映射")
         except Exception as e:
             db.rollback()
-            # If bulk fails (e.g. unique constraint on mapped_key), we might need to do row-by-row or handle it.
+            logger.error(f"批量同步用户失败: {e}。尝试逐条插入。")
+            
             # Fallback: try one by one
             success_count = 0
             for m in new_mappings:
@@ -724,8 +738,9 @@ def sync_users_to_app(
                     db.add(m)
                     db.commit()
                     success_count += 1
-                except:
+                except Exception as ex:
                     db.rollback()
+                    logger.warning(f"单个用户映射失败 (User: {m.user_id}): {ex}")
             
             LogService.create_log(
                 db=db,
@@ -746,6 +761,7 @@ def sync_users_to_app(
         )
         return {"message": f"同步成功,新增 {len(new_mappings)} 个用户映射"}
 
+    logger.info("用户同步: 没有需要同步的新用户")
     return {"message": "没有需要同步的用户"}
 
 @router.post("/{app_id}/sync-users-v2", summary="同步用户 (新版)")
@@ -770,6 +786,7 @@ def sync_users_to_app_v2(
 
     # 1. Verify SMS Code
     if not SmsService.verify_code(current_user.mobile, sync_req.verification_code):
+        logger.warning(f"同步用户失败: 验证码错误 (User: {current_user.mobile})")
         raise HTTPException(status_code=400, detail="验证码无效或已过期")
 
     # 2. Determine Target Users
@@ -799,11 +816,6 @@ def sync_users_to_app_v2(
         if user.id in mapped_user_ids:
             continue
             
-        # Determine mapped_key (English name)
-        # If english_name is missing, fallback to mobile? Or skip?
-        # User requirement: "账号就是英文名" (Account is English name)
-        # Assuming if english_name is empty, we might use mobile or some generation.
-        # Let's fallback to mobile if english_name is missing, but prefer english_name.
         mapped_key = user.english_name if user.english_name else user.mobile
         
         mapped_email = None
@@ -827,25 +839,21 @@ def sync_users_to_app_v2(
         return {"message": "所有选中的用户均已存在映射,无需同步"}
 
     # 4. Insert
-    # We'll try to insert one by one to handle potential unique constraint violations (e.g. same english name for different users)
-    # Or we can try bulk and catch. Given "User can check selection", maybe best effort is good.
+    logger.info(f"开始同步(v2)用户到应用 {app.app_name},计划新增 {len(new_mappings)} 条")
     
     success_count = 0
     fail_count = 0
     
     for m in new_mappings:
         try:
-            # Check unique key collision within this transaction if possible, 
-            # but db.add + commit per row is safer for partial success report.
-            
             # Additional check: uniqueness of mapped_key in this app
-            # (We already have uniqueness constraint in DB)
             db.add(m)
             db.commit()
             success_count += 1
-        except Exception:
+        except Exception as e:
             db.rollback()
             fail_count += 1
+            logger.warning(f"同步单个映射失败 (User: {m.user_id}): {e}")
             
     # 5. Log
     LogService.create_log(
@@ -862,6 +870,8 @@ def sync_users_to_app_v2(
         }
     )
     
+    logger.info(f"同步(v2)完成。成功: {success_count}, 失败: {fail_count}")
+
     msg = f"同步完成。成功: {success_count},失败: {fail_count}"
     if fail_count > 0:
         msg += " (失败原因可能是账号或邮箱冲突)"
@@ -912,6 +922,7 @@ def export_mappings(
     output.seek(0)
     
     filename = f"mappings_app_{app_id}.xlsx"
+    logger.info(f"导出映射成功: App {app_id}, Count {len(mappings)}")
     
     return StreamingResponse(
         output,
@@ -938,7 +949,11 @@ async def preview_mapping(
 
     contents = await file.read()
     filename = file.filename
-    return MappingService.preview_import(db, app_id, contents, filename)
+    try:
+        return MappingService.preview_import(db, app_id, contents, filename)
+    except Exception as e:
+        logger.error(f"导入预览失败: {e}", exc_info=True)
+        raise HTTPException(status_code=400, detail=f"解析文件失败: {str(e)}")
 
 @router.post("/send-import-verification-code", summary="发送导入验证码")
 def send_import_verification_code(
@@ -948,6 +963,7 @@ def send_import_verification_code(
     发送验证码给当前登录用户(用于敏感操作验证,如导入)。
     """
     SmsService.send_code(current_user.mobile)
+    logger.info(f"发送导入验证码: {current_user.mobile}")
     return {"message": "验证码已发送"}
 
 @router.post("/{app_id}/mapping/import", response_model=ImportLogResponse, summary="执行映射导入")
@@ -972,7 +988,13 @@ async def import_mapping(
     contents = await file.read()
     filename = file.filename
     
-    result = MappingService.execute_import(db, app_id, contents, filename, strategy, current_user.mobile, verification_code)
+    logger.info(f"开始执行映射导入: App {app_id}, File {filename}, Strategy {strategy}")
+
+    try:
+        result = MappingService.execute_import(db, app_id, contents, filename, strategy, current_user.mobile, verification_code)
+    except Exception as e:
+        logger.error(f"执行映射导入异常: {e}", exc_info=True)
+        raise e
     
     # LOGGING
     # For import, we log the summary and the logs structure
@@ -984,6 +1006,8 @@ async def import_mapping(
         details=result.model_dump(mode='json') # Store full result including logs
     )
     
+    logger.info(f"映射导入完成: 成功 {result.summary.inserted + result.summary.updated}, 失败 {result.summary.failed}")
+    
     return result
 
 @router.get("/mapping/users", response_model=UserSyncList, summary="获取全量用户(M2M)")
@@ -1026,6 +1050,8 @@ def sync_mapping(
     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
 
+    logger.info(f"收到 M2M 同步请求: App {current_app.app_id}, Mobile {sync_in.mobile}, Action {sync_in.sync_action}")
+
     # ==========================================
     # 1. Handle DELETE Action
     # ==========================================
@@ -1034,6 +1060,7 @@ def sync_mapping(
         user = db.query(User).filter(User.mobile == sync_in.mobile).first()
         if not user:
             # 用户不存在,无法删除映射,直接抛出404或视作成功
+            logger.warning(f"M2M 删除失败: 用户 {sync_in.mobile} 不存在")
             raise HTTPException(status_code=404, detail="用户不存在")
 
         # 查找映射
@@ -1043,6 +1070,7 @@ def sync_mapping(
         ).first()
 
         if not mapping:
+            logger.warning(f"M2M 删除失败: 映射不存在 (User {sync_in.mobile})")
             raise HTTPException(status_code=404, detail="映射关系不存在")
 
         # 构造返回数据(删除前快照,将状态置为 False)
@@ -1071,6 +1099,7 @@ def sync_mapping(
             target_mobile=user.mobile,
             details={"mapped_key": mapping.mapped_key, "action": "M2M_DELETE"}
         )
+        logger.info(f"M2M 删除成功: {sync_in.mobile}")
 
         return resp_data
 
@@ -1086,7 +1115,7 @@ def sync_mapping(
             User.mobile != sync_in.mobile
         ).first()
         if name_conflict:
-            raise HTTPException(status_code=400, detail=f"姓名 '{in_name}' 已存在")
+             raise HTTPException(status_code=400, detail=f"姓名 '{in_name}' 已存在")
 
     if in_english_name:
         en_name_conflict = db.query(User).filter(
@@ -1126,6 +1155,7 @@ def sync_mapping(
         db.commit()
         db.refresh(user)
         new_user_created = True
+        logger.info(f"M2M 自动创建用户: {sync_in.mobile}")
     else:
         # Update Existing User (if fields provided)
         updated = False
@@ -1205,6 +1235,8 @@ def sync_mapping(
             "new_mapping_created": new_mapping_created
         }
     )
+    
+    logger.info(f"M2M 同步成功: {sync_in.mobile} (Mapping: {mapping.id})")
 
     return MappingResponse(
         id=mapping.id,

+ 13 - 0
backend/app/api/v1/endpoints/oidc.py

@@ -1,4 +1,5 @@
 from typing import Any, List
+import logging
 from fastapi import APIRouter, Depends, HTTPException, Query
 from sqlalchemy.orm import Session
 
@@ -10,6 +11,7 @@ from app.schemas.token import Token, LoginRequest
 from app.services.hydra_service import hydra_service
 
 router = APIRouter()
+logger = logging.getLogger(__name__)
 
 @router.get("/login-request", summary="获取登录请求信息 (OIDC)")
 def get_login_request(challenge: str):
@@ -20,10 +22,12 @@ def get_login_request(challenge: str):
     try:
         req = hydra_service.get_login_request(challenge)
         if req.skip:
+            logger.info(f"Skipping login for challenge {challenge}, subject: {req.subject}")
             # If Hydra says skip, we just accept it immediately
             return hydra_service.accept_login_request(challenge, subject=req.subject)
         return req
     except Exception as e:
+        logger.exception(f"Failed to get login request for challenge: {challenge}")
         raise HTTPException(status_code=400, detail=str(e))
 
 @router.post("/login/accept", summary="接受登录请求 (OIDC)")
@@ -39,14 +43,18 @@ def accept_login(
     if not user or not security.verify_password(login_data.password, user.password_hash):
         # We don't reject the request immediately to allow retry,
         # but in a strict flow we might. Here we just return 401.
+        logger.warning(f"Login failed for user {login_data.mobile}: Invalid credentials")
         raise HTTPException(status_code=401, detail="手机号或密码错误")
     
     if user.status != "ACTIVE":
+        logger.warning(f"Login failed for user {login_data.mobile}: User not active")
         raise HTTPException(status_code=400, detail="用户状态不正常")
 
     try:
+        logger.info(f"Accepting login request for user {user.mobile} (ID: {user.id}), challenge: {challenge}")
         return hydra_service.accept_login_request(challenge, subject=str(user.id))
     except Exception as e:
+        logger.exception(f"Failed to accept login request for challenge: {challenge}")
         raise HTTPException(status_code=500, detail=str(e))
 
 @router.get("/consent-request", summary="获取同意请求信息 (OIDC)")
@@ -65,6 +73,7 @@ def get_consent_request(
         # But we still might want to refresh claims?
         # For simplicity, if skip, we accept with old scopes.
         if req.skip:
+             logger.info(f"Skipping consent for challenge {challenge}, subject: {req.subject}")
              return hydra_service.accept_consent_request(
                  challenge, 
                  grant_scope=req.requested_scope,
@@ -92,6 +101,9 @@ def get_consent_request(
             # You can standardize the claim name, e.g. "preferred_username" or "ext_id"
             id_token_claims["preferred_username"] = mapping.mapped_key
             id_token_claims["email"] = mapping.mapped_key # If it's email
+            logger.info(f"Injecting claims for user {user_id} in client {client_id}: {mapping.mapped_key}")
+        else:
+            logger.info(f"No mapping found for user {user_id} in client {client_id}, minimal claims injected.")
         
         # Also inject mobile number if needed
         user = db.query(User).filter(User.id == user_id).first()
@@ -105,4 +117,5 @@ def get_consent_request(
         )
 
     except Exception as e:
+        logger.exception(f"Failed to process consent request for challenge: {challenge}")
         raise HTTPException(status_code=400, detail=str(e))

+ 52 - 12
backend/app/api/v1/endpoints/simple_auth.py

@@ -1,7 +1,8 @@
 from typing import Optional, List
 import json
 from datetime import timedelta
-from fastapi import APIRouter, Depends, HTTPException, Body
+import logging
+from fastapi import APIRouter, Depends, HTTPException, Body, Request
 from sqlalchemy.orm import Session
 from pydantic import BaseModel
 
@@ -29,9 +30,9 @@ from app.services.login_log_service import LoginLogService
 from app.services.system_config_service import SystemConfigService
 from app.schemas.operation_log import ActionType
 from app.schemas.login_log import LoginLogCreate, LoginMethod, AuthType
-from fastapi import Request
 
 router = APIRouter()
+logger = logging.getLogger(__name__)
 
 @router.post("/login", response_model=PasswordLoginResponse, summary="密码登录")
 def login_with_password(
@@ -61,15 +62,14 @@ def login_with_password(
             log_create.is_success = 0
             log_create.failure_reason = "用户未找到"
             LoginLogService.create_log(db, log_create)
+            logger.warning(f"平台登录失败: 用户 {req.identifier} 未找到")
             raise HTTPException(status_code=404, detail="用户未找到")
         
         log_create.user_id = user.id
 
         is_valid = security.verify_password(req.password, user.password_hash)
         if not is_valid:
-            import logging
-            logger = logging.getLogger(__name__)
-            logger.error(f"Platform Login failed for user {user.mobile}")
+            logger.warning(f"平台登录失败: 用户 {user.mobile} 密码错误")
             
             log_create.is_success = 0
             log_create.failure_reason = "密码错误"
@@ -78,6 +78,7 @@ def login_with_password(
             raise HTTPException(status_code=401, detail="密码错误")
             
         if user.status != UserStatus.ACTIVE:
+            logger.warning(f"平台登录失败: 用户 {user.mobile} 已被禁用")
             log_create.is_success = 0
             log_create.failure_reason = "用户已禁用"
             LoginLogService.create_log(db, log_create)
@@ -95,6 +96,7 @@ def login_with_password(
         
         # Log Success
         LoginLogService.create_log(db, log_create)
+        logger.info(f"平台登录成功: 用户 {user.mobile} (ID: {user.id})")
 
         return {
             "access_token": access_token, 
@@ -117,6 +119,7 @@ def login_with_password(
         log_create.is_success = 0
         log_create.failure_reason = "应用未找到"
         LoginLogService.create_log(db, log_create)
+        logger.warning(f"应用登录失败: 应用ID {req.app_id} 未找到")
         raise HTTPException(status_code=404, detail="应用未找到")
 
     # 2. Verify Signature (Optional but recommended for server-side calls)
@@ -132,6 +135,7 @@ def login_with_password(
             log_create.is_success = 0
             log_create.failure_reason = "签名无效"
             LoginLogService.create_log(db, log_create)
+            logger.warning(f"应用登录失败: 应用 {req.app_id} 签名验证失败")
             raise HTTPException(status_code=400, detail="签名无效")
 
     # 3. Find User
@@ -158,6 +162,7 @@ def login_with_password(
         log_create.is_success = 0
         log_create.failure_reason = "用户未找到"
         LoginLogService.create_log(db, log_create)
+        logger.warning(f"应用登录失败: 用户 {req.identifier} 在应用 {req.app_id} 中未找到")
         raise HTTPException(status_code=404, detail="用户未找到")
 
     log_create.user_id = user.id
@@ -166,16 +171,14 @@ def login_with_password(
         log_create.is_success = 0
         log_create.failure_reason = "用户已禁用"
         LoginLogService.create_log(db, log_create)
+        logger.warning(f"应用登录失败: 用户 {user.mobile} 已被禁用")
         raise HTTPException(status_code=400, detail="用户已禁用")
 
     # 4. Verify Password
-    import logging
-    logger = logging.getLogger(__name__)
-    
     # DEBUG: Log password verification details
     is_valid = security.verify_password(req.password, user.password_hash)
     if not is_valid:
-        logger.error(f"Password verification failed for user {user.mobile}")
+        logger.warning(f"应用登录失败: 用户 {user.mobile} 密码验证失败 (App: {req.app_id})")
         
         log_create.is_success = 0
         log_create.failure_reason = "密码错误"
@@ -189,6 +192,7 @@ def login_with_password(
     # Log Success (AuthType is PASSWORD leading to TICKET generation, keeping PASSWORD is fine or TICKET)
     # User requirement: "包括...认证方式". Here the auth method was PASSWORD.
     LoginLogService.create_log(db, log_create)
+    logger.info(f"应用登录成功: 用户 {user.mobile} 获取 Ticket (App: {req.app_id})")
 
     return {"ticket": ticket}
 
@@ -210,6 +214,7 @@ def login_with_sms(
     mobile_enabled = SystemConfigService.get_config(db, "sms_login_mobile_enabled")
     
     if pc_enabled != "true" and mobile_enabled != "true":
+        logger.warning("短信登录尝试失败: 短信登录功能未开启")
         raise HTTPException(status_code=403, detail="短信登录功能未开启")
 
     # --- Platform Login ---
@@ -231,6 +236,7 @@ def login_with_sms(
              log_create.is_success = 0
              log_create.failure_reason = "验证码错误或已过期"
              LoginLogService.create_log(db, log_create)
+             logger.warning(f"平台短信登录失败: 手机号 {req.mobile} 验证码无效")
              raise HTTPException(status_code=400, detail="验证码错误或已过期")
 
         # 2. Find user
@@ -239,6 +245,7 @@ def login_with_sms(
             log_create.is_success = 0
             log_create.failure_reason = "用户未找到"
             LoginLogService.create_log(db, log_create)
+            logger.warning(f"平台短信登录失败: 手机号 {req.mobile} 未注册")
             raise HTTPException(status_code=404, detail="用户未找到")
         
         log_create.user_id = user.id
@@ -247,6 +254,7 @@ def login_with_sms(
             log_create.is_success = 0
             log_create.failure_reason = "用户已禁用"
             LoginLogService.create_log(db, log_create)
+            logger.warning(f"平台短信登录失败: 用户 {user.mobile} 已被禁用")
             raise HTTPException(status_code=400, detail="用户已禁用")
 
         # 3. Generate JWT Access Token
@@ -261,6 +269,7 @@ def login_with_sms(
         
         # Log Success
         LoginLogService.create_log(db, log_create)
+        logger.info(f"平台短信登录成功: 用户 {user.mobile} (ID: {user.id})")
 
         return {
             "access_token": access_token, 
@@ -283,6 +292,7 @@ def login_with_sms(
         log_create.is_success = 0
         log_create.failure_reason = "应用未找到"
         LoginLogService.create_log(db, log_create)
+        logger.warning(f"应用短信登录失败: 应用ID {req.app_id} 未找到")
         raise HTTPException(status_code=404, detail="应用未找到")
 
     # 2. Verify Signature (Optional)
@@ -298,6 +308,7 @@ def login_with_sms(
             log_create.is_success = 0
             log_create.failure_reason = "签名无效"
             LoginLogService.create_log(db, log_create)
+            logger.warning(f"应用短信登录失败: 应用 {req.app_id} 签名无效")
             raise HTTPException(status_code=400, detail="签名无效")
 
     # 3. Verify Code
@@ -308,18 +319,17 @@ def login_with_sms(
          log_create.is_success = 0
          log_create.failure_reason = "验证码错误或已过期"
          LoginLogService.create_log(db, log_create)
+         logger.warning(f"应用短信登录失败: 手机号 {req.mobile} 验证码无效")
          raise HTTPException(status_code=400, detail="验证码错误或已过期")
 
     # 4. Find User
     user = db.query(User).filter(User.mobile == req.mobile, User.is_deleted == 0).first()
     
-    # Check mapping if not found by mobile? No, SMS login implies mobile IS the identity.
-    # But maybe the user in UAP has a different mobile? No, SMS is sent to mobile.
-    
     if not user:
         log_create.is_success = 0
         log_create.failure_reason = "用户未找到"
         LoginLogService.create_log(db, log_create)
+        logger.warning(f"应用短信登录失败: 手机号 {req.mobile} 未注册")
         raise HTTPException(status_code=404, detail="用户未找到")
 
     log_create.user_id = user.id
@@ -328,6 +338,7 @@ def login_with_sms(
         log_create.is_success = 0
         log_create.failure_reason = "用户已禁用"
         LoginLogService.create_log(db, log_create)
+        logger.warning(f"应用短信登录失败: 用户 {user.mobile} 已被禁用")
         raise HTTPException(status_code=400, detail="用户已禁用")
 
     # 5. Generate Ticket (Self-Targeting)
@@ -338,6 +349,7 @@ def login_with_sms(
     
     # Log Success
     LoginLogService.create_log(db, log_create)
+    logger.info(f"应用短信登录成功: 用户 {user.mobile} 获取 Ticket (App: {req.app_id})")
 
     return {"ticket": ticket}
 
@@ -361,6 +373,7 @@ def register_user(
 
     existing_user = db.query(User).filter(User.mobile == req.mobile, User.is_deleted == 0).first()
     if existing_user:
+        logger.info(f"用户注册失败: 手机号 {req.mobile} 已存在")
         raise HTTPException(status_code=400, detail="手机号已注册")
 
     english_name = generate_english_name(req.name)
@@ -376,6 +389,8 @@ def register_user(
     db.add(new_user)
     db.commit()
     db.refresh(new_user)
+    
+    logger.info(f"用户注册成功: {req.mobile} (ID: {new_user.id})")
 
     # Auto-login after registration (return token)
     access_token = security.create_access_token(new_user.id)
@@ -401,6 +416,7 @@ def admin_reset_password(
 
     # Verify Admin Password
     if not security.verify_password(req.admin_password, current_user.password_hash):
+        logger.warning(f"管理员重置密码失败: 管理员 {current_user.mobile} 密码验证错误")
         raise HTTPException(status_code=401, detail="管理员密码错误")
 
     target_user = db.query(User).filter(User.id == req.user_id).first()
@@ -423,6 +439,8 @@ def admin_reset_password(
         ip_address=get_client_ip(request),
         details={}
     )
+    
+    logger.info(f"管理员重置用户密码成功: 目标用户 {target_user.mobile} (ID: {target_user.id})")
 
     return {"new_password": new_pwd}
 
@@ -442,10 +460,13 @@ def promote_user(
     if not target_user:
         raise HTTPException(status_code=404, detail="用户未找到")
         
+    old_role = target_user.role
     target_user.role = req.new_role
     db.add(target_user)
     db.commit()
     
+    logger.info(f"用户角色变更: 用户 {target_user.mobile} 从 {old_role} 变更为 {req.new_role} (操作者: {current_user.mobile})")
+    
     return {"message": "success"}
 
 @router.get("/me/mappings", response_model=MyMappingsResponse, summary="我的映射")
@@ -484,6 +505,7 @@ def change_my_password(
     current_user: User = Depends(deps.get_current_active_user),
 ):
     if not security.verify_password(req.old_password, current_user.password_hash):
+        logger.warning(f"用户修改密码失败: 用户 {current_user.mobile} 旧密码验证错误")
         raise HTTPException(status_code=400, detail="旧密码错误")
         
     if req.new_password:
@@ -496,6 +518,8 @@ def change_my_password(
     db.add(current_user)
     db.commit()
     
+    logger.info(f"用户修改密码成功: {current_user.mobile}")
+    
     return {"message": "密码修改成功"}
 
 @router.post("/exchange", response_model=TicketExchangeResponse, summary="票据交换")
@@ -509,6 +533,7 @@ def exchange_ticket(
     # 1. Verify Source App
     source_app = db.query(Application).filter(Application.app_id == req.app_id).first()
     if not source_app:
+        logger.warning(f"票据交换失败: 源应用 {req.app_id} 未找到")
         raise HTTPException(status_code=404, detail="源应用未找到")
         
     # 2. Verify Signature
@@ -522,6 +547,7 @@ def exchange_ticket(
     
     # Use the stored secret to verify
     if not SignatureService.verify_signature(source_app.app_secret, params, req.sign):
+        logger.warning(f"票据交换失败: 源应用 {req.app_id} 签名无效")
         raise HTTPException(status_code=400, detail="签名无效")
     
     # 3. Verify User Existence (Optional: Do we trust source app completely? Usually yes if signed.)
@@ -532,6 +558,7 @@ def exchange_ticket(
         # If user doesn't exist, we might auto-create OR fail.
         # Requirement: "Returns redirect_url". 
         # For simplicity, if user not found, we cannot map.
+        logger.warning(f"票据交换失败: 用户 {req.user_mobile} 未找到")
         raise HTTPException(status_code=404, detail="用户在 UAP 中未找到")
 
     # 4. Generate Ticket for Target App
@@ -541,6 +568,7 @@ def exchange_ticket(
     # 5. Get Target App URL
     target_app = db.query(Application).filter(Application.app_id == req.target_app_id).first()
     if not target_app:
+        logger.warning(f"票据交换失败: 目标应用 {req.target_app_id} 未找到")
         raise HTTPException(status_code=404, detail="目标应用未找到")
 
     # Construct redirect URL
@@ -568,6 +596,7 @@ def exchange_ticket(
 
     full_redirect_url = f"{redirect_base}?ticket={ticket}"
 
+    logger.info(f"票据交换成功: 用户 {req.user_mobile} 从 {req.app_id} -> {req.target_app_id}")
     return {
         "ticket": ticket,
         "redirect_url": full_redirect_url
@@ -604,12 +633,14 @@ def sso_login(
         log_create.is_success = 0
         log_create.failure_reason = "应用未找到"
         LoginLogService.create_log(db, log_create)
+        logger.warning(f"SSO登录失败: 应用 {req.app_id} 未找到")
         raise HTTPException(status_code=404, detail="应用未找到")
 
     if app.protocol_type != "SIMPLE_API":
          log_create.is_success = 0
          log_create.failure_reason = "协议不支持"
          LoginLogService.create_log(db, log_create)
+         logger.warning(f"SSO登录失败: 应用 {req.app_id} 协议类型不支持 ({app.protocol_type})")
          raise HTTPException(status_code=400, detail="SSO 登录仅支持简易 API 应用。OIDC 请使用标准流程。")
 
     user = None
@@ -643,12 +674,14 @@ def sso_login(
          log_create.is_success = 0
          log_create.failure_reason = "认证失败"
          LoginLogService.create_log(db, log_create)
+         logger.warning(f"SSO登录失败: 用户认证失败 (Username: {req.username})")
          raise HTTPException(status_code=401, detail="认证失败")
         
     if user.status != "ACTIVE":
         log_create.is_success = 0
         log_create.failure_reason = "用户已禁用"
         LoginLogService.create_log(db, log_create)
+        logger.warning(f"SSO登录失败: 用户 {user.mobile} 已被禁用")
         raise HTTPException(status_code=400, detail="用户已禁用")
 
     # 4. Generate Ticket
@@ -656,6 +689,7 @@ def sso_login(
     
     # Log Success
     LoginLogService.create_log(db, log_create)
+    logger.info(f"SSO登录成功: 用户 {user.mobile} 获取 Ticket (App: {req.app_id})")
     
     # 5. Get Redirect URL
     redirect_base = ""
@@ -672,6 +706,7 @@ def sso_login(
             redirect_base = app.redirect_uris.strip()
             
     if not redirect_base:
+         logger.error(f"SSO登录配置错误: 应用 {req.app_id} 未配置回调地址")
          raise HTTPException(status_code=400, detail="应用未配置重定向 URI")
 
     full_redirect_url = f"{redirect_base}?ticket={ticket}"
@@ -689,6 +724,7 @@ def validate_ticket(
     # 1. Verify App
     app = db.query(Application).filter(Application.app_id == req.app_id).first()
     if not app:
+        logger.warning(f"票据验证失败: 应用 {req.app_id} 未找到")
         raise HTTPException(status_code=404, detail="应用未找到")
 
     # 2. Verify Signature
@@ -699,12 +735,14 @@ def validate_ticket(
         "sign": req.sign
     }
     if not SignatureService.verify_signature(app.app_secret, params, req.sign):
+        logger.warning(f"票据验证失败: 应用 {req.app_id} 签名无效")
         raise HTTPException(status_code=400, detail="签名无效")
 
     # 3. Consume Ticket
     ticket_data = TicketService.consume_ticket(req.ticket, req.app_id)
     
     if not ticket_data:
+        logger.warning(f"票据验证失败: Ticket 无效或已过期 (App: {req.app_id})")
         return {"valid": False}
 
     user_id = ticket_data["user_id"]
@@ -719,6 +757,8 @@ def validate_ticket(
     
     mapped_key = mapping.mapped_key if mapping else None
     mapped_email = mapping.mapped_email if mapping else None
+    
+    logger.info(f"票据验证成功: 用户 {user.mobile} (App: {req.app_id})")
 
     return {
         "valid": True,

+ 17 - 3
backend/app/api/v1/endpoints/users.py

@@ -1,4 +1,5 @@
 from typing import List, Any
+import logging
 from fastapi import APIRouter, Depends, HTTPException, Body, BackgroundTasks, Request, Query
 from sqlalchemy.orm import Session
 from sqlalchemy import or_
@@ -16,6 +17,7 @@ from app.services.log_service import LogService
 from app.schemas.operation_log import ActionType
 
 router = APIRouter()
+logger = logging.getLogger(__name__)
 
 @router.get("/search", response_model=List[UserSchema], summary="搜索用户")
 def search_users(
@@ -115,6 +117,7 @@ def create_user(
 
     # Verify Admin Password
     if not user_in.admin_password or not security.verify_password(user_in.admin_password, current_user.password_hash):
+        logger.warning(f"管理员创建用户失败: 密码错误 (Admin: {current_user.mobile})")
         raise HTTPException(status_code=403, detail="管理员密码错误")
 
     user = db.query(User).filter(User.mobile == user_in.mobile).first()
@@ -178,6 +181,8 @@ def create_user(
         ip_address=get_client_ip(request),
         details={"role": db_user.role}
     )
+    
+    logger.info(f"管理员创建用户成功: {db_user.mobile} (Role: {db_user.role})")
 
     return db_user
 
@@ -199,6 +204,7 @@ def batch_reset_english_name(
 
     # Verify Admin Password
     if not security.verify_password(req.admin_password, current_user.password_hash):
+        logger.warning(f"批量重置英文名失败: 密码错误 (Admin: {current_user.mobile})")
         raise HTTPException(status_code=403, detail="管理员密码错误")
 
     if not req.user_ids:
@@ -218,9 +224,6 @@ def batch_reset_english_name(
         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):
@@ -256,6 +259,8 @@ def batch_reset_english_name(
             success_count += 1
             
     db.commit()
+    logger.info(f"批量重置英文名完成: 成功 {success_count} 个 (Admin: {current_user.mobile})")
+    
     return {"success": True, "count": success_count}
 
 @router.put("/{user_id}", response_model=UserSchema, summary="更新用户")
@@ -291,6 +296,7 @@ def update_user(
         else:
             # Require admin password for mobile change
             if not user_in.admin_password or not security.verify_password(user_in.admin_password, current_user.password_hash):
+                logger.warning(f"修改用户手机号失败: 管理员密码错误 (Target: {user.mobile})")
                 raise HTTPException(status_code=403, detail="管理员密码错误")
 
             # Check uniqueness
@@ -317,6 +323,7 @@ def update_user(
                     db.flush()
                 except IntegrityError:
                     db.rollback()
+                    logger.error(f"修改手机号失败: 映射冲突 ({old_mobile} -> {new_mobile})")
                     raise HTTPException(status_code=400, detail="修改失败:新手机号在某些应用中已存在映射关联")
 
                 actions.append((ActionType.UPDATE, {"field": "mobile", "old": old_mobile, "new": new_mobile}))
@@ -330,6 +337,7 @@ def update_user(
         else:
             # Require admin password for status change
             if not user_in.admin_password or not security.verify_password(user_in.admin_password, current_user.password_hash):
+                logger.warning(f"修改用户状态失败: 密码错误")
                 raise HTTPException(status_code=403, detail="管理员密码错误")
             
             # Add Log Action
@@ -345,6 +353,7 @@ def update_user(
         else:
              # Require admin password for role change
             if not user_in.admin_password or not security.verify_password(user_in.admin_password, current_user.password_hash):
+                logger.warning(f"修改用户角色失败: 密码错误")
                 raise HTTPException(status_code=403, detail="管理员密码错误")
             
             actions.append((ActionType.CHANGE_ROLE, {"old": user.role, "new": update_data["role"]}))
@@ -400,6 +409,7 @@ def update_user(
                 details=details
             )
 
+    logger.info(f"更新用户信息成功: {user.mobile} (ID: {user.id})")
     return user
 
 @router.post("/{user_id}/promote", response_model=UserSchema, summary="提升用户权限")
@@ -420,10 +430,12 @@ def promote_user(
     
     # 1. Verify Password
     if not security.verify_password(req.password, current_user.password_hash):
+        logger.warning(f"提升权限失败: 密码错误 (Admin: {current_user.mobile})")
         raise HTTPException(status_code=403, detail="密码错误")
         
     # 2. Verify Captcha
     if not CaptchaService.verify_captcha(req.captcha_id, req.captcha_code):
+        logger.warning(f"提升权限失败: 验证码错误")
         raise HTTPException(status_code=400, detail="验证码错误")
         
     user = db.query(User).filter(User.id == user_id).first()
@@ -449,6 +461,7 @@ def promote_user(
         details={"old": old_role, "new": "SUPER_ADMIN"}
     )
     
+    logger.info(f"提升用户为超管: {user.mobile}")
     return user
 
 @router.delete("/{user_id}", response_model=UserSchema, summary="删除用户")
@@ -491,6 +504,7 @@ def delete_user(
         details={"status": "DISABLED"}
     )
     
+    logger.info(f"删除用户成功: {user.mobile}")
     return user
 
 @router.get("/me", response_model=UserSchema, summary="获取当前用户信息")

+ 37 - 7
backend/app/services/hydra_service.py

@@ -1,3 +1,4 @@
+import logging
 import ory_hydra_client
 from ory_hydra_client.api import o_auth2_api
 from ory_hydra_client.models.accept_o_auth2_login_request import AcceptOAuth2LoginRequest
@@ -7,6 +8,8 @@ from ory_hydra_client.models.o_auth2_consent_session import OAuth2ConsentSession
 
 from app.core.hydra_config import hydra_settings
 
+logger = logging.getLogger(__name__)
+
 class HydraService:
     def __init__(self):
         configuration = ory_hydra_client.Configuration(
@@ -16,7 +19,11 @@ class HydraService:
         self.oauth2 = o_auth2_api.OAuth2Api(self.api_client)
 
     def get_login_request(self, challenge: str):
-        return self.oauth2.get_o_auth2_login_request(challenge)
+        try:
+            return self.oauth2.get_o_auth2_login_request(challenge)
+        except Exception as e:
+            logger.error(f"获取登录请求失败 (challenge: {challenge}): {e}")
+            raise
 
     def accept_login_request(self, challenge: str, subject: str):
         body = AcceptOAuth2LoginRequest(
@@ -24,17 +31,31 @@ class HydraService:
             remember=True,
             remember_for=3600,
         )
-        return self.oauth2.accept_o_auth2_login_request(challenge, accept_o_auth2_login_request=body)
+        try:
+            logger.info(f"接受登录请求 (subject: {subject}, challenge: {challenge})")
+            return self.oauth2.accept_o_auth2_login_request(challenge, accept_o_auth2_login_request=body)
+        except Exception as e:
+            logger.error(f"接受登录请求失败 (challenge: {challenge}): {e}")
+            raise
 
     def reject_login_request(self, challenge: str, error: str, error_description: str):
         body = RejectOAuth2Request(
             error=error,
             error_description=error_description
         )
-        return self.oauth2.reject_o_auth2_login_request(challenge, reject_o_auth2_request=body)
+        try:
+            logger.info(f"拒绝登录请求 (challenge: {challenge}, error: {error})")
+            return self.oauth2.reject_o_auth2_login_request(challenge, reject_o_auth2_request=body)
+        except Exception as e:
+            logger.error(f"拒绝登录请求失败 (challenge: {challenge}): {e}")
+            raise
 
     def get_consent_request(self, challenge: str):
-        return self.oauth2.get_o_auth2_consent_request(challenge)
+        try:
+            return self.oauth2.get_o_auth2_consent_request(challenge)
+        except Exception as e:
+            logger.error(f"获取同意请求失败 (challenge: {challenge}): {e}")
+            raise
 
     def accept_consent_request(self, challenge: str, grant_scope: list, id_token_claims: dict):
         body = AcceptOAuth2ConsentRequest(
@@ -46,14 +67,23 @@ class HydraService:
                 id_token=id_token_claims
             )
         )
-        return self.oauth2.accept_o_auth2_consent_request(challenge, accept_o_auth2_consent_request=body)
+        try:
+            logger.info(f"接受同意请求 (challenge: {challenge}, scope: {grant_scope})")
+            return self.oauth2.accept_o_auth2_consent_request(challenge, accept_o_auth2_consent_request=body)
+        except Exception as e:
+            logger.error(f"接受同意请求失败 (challenge: {challenge}): {e}")
+            raise
     
     def reject_consent_request(self, challenge: str, error: str, error_description: str):
         body = RejectOAuth2Request(
             error=error,
             error_description=error_description
         )
-        return self.oauth2.reject_o_auth2_consent_request(challenge, reject_o_auth2_request=body)
+        try:
+            logger.info(f"拒绝同意请求 (challenge: {challenge}, error: {error})")
+            return self.oauth2.reject_o_auth2_consent_request(challenge, reject_o_auth2_request=body)
+        except Exception as e:
+            logger.error(f"拒绝同意请求失败 (challenge: {challenge}): {e}")
+            raise
 
 hydra_service = HydraService()
-

+ 74 - 63
backend/app/services/mapping_service.py

@@ -1,5 +1,6 @@
 import io
 import pandas as pd
+import logging
 from datetime import datetime
 from typing import List, Tuple
 from sqlalchemy.orm import Session
@@ -18,6 +19,8 @@ from app.schemas.mapping import (
 from app.core import security
 from app.services.sms_service import SmsService
 
+logger = logging.getLogger(__name__)
+
 class MappingService:
     @staticmethod
     def process_excel_file(file_content: bytes) -> pd.DataFrame:
@@ -59,81 +62,87 @@ class MappingService:
             
             return df
         except Exception as e:
+            logger.error(f"文件解析失败: {e}", exc_info=True)
             raise ValueError(f"文件格式无效: {str(e)}")
 
     @staticmethod
     def preview_import(db: Session, app_id: int, file_content: bytes, filename: str = "") -> MappingPreviewResponse:
-        df = MappingService.process_excel_file(file_content)
-        
-        required_cols = {'mobile', 'mapped_key'}
-        if not required_cols.issubset(df.columns):
-            raise ValueError(f"缺少必要列。请确保文件包含: 手机号(mobile), 映射账号(mapped_key)")
+        try:
+            df = MappingService.process_excel_file(file_content)
+            
+            required_cols = {'mobile', 'mapped_key'}
+            if not required_cols.issubset(df.columns):
+                raise ValueError(f"缺少必要列。请确保文件包含: 手机号(mobile), 映射账号(mapped_key)")
 
-        preview_rows: List[MappingRowPreview] = []
-        stats = {"valid": 0, "error": 0, "new": 0, "update": 0}
+            preview_rows: List[MappingRowPreview] = []
+            stats = {"valid": 0, "error": 0, "new": 0, "update": 0}
 
-        # 1. Batch query all users involved
-        mobiles = df['mobile'].astype(str).unique().tolist()
-        users = db.query(User).filter(User.mobile.in_(mobiles)).all()
-        user_map = {u.mobile: u.id for u in users}
+            # 1. Batch query all users involved
+            mobiles = df['mobile'].astype(str).unique().tolist()
+            users = db.query(User).filter(User.mobile.in_(mobiles)).all()
+            user_map = {u.mobile: u.id for u in users}
 
-        # 2. Batch query existing mappings for this app
-        user_ids = list(user_map.values())
-        existing_mappings = db.query(AppUserMapping).filter(
-            AppUserMapping.app_id == app_id,
-            AppUserMapping.user_id.in_(user_ids)
-        ).all()
-        mapping_map = {m.user_id: m for m in existing_mappings}
+            # 2. Batch query existing mappings for this app
+            user_ids = list(user_map.values())
+            existing_mappings = db.query(AppUserMapping).filter(
+                AppUserMapping.app_id == app_id,
+                AppUserMapping.user_id.in_(user_ids)
+            ).all()
+            mapping_map = {m.user_id: m for m in existing_mappings}
 
-        # 3. Iterate rows
-        for index, row in df.iterrows():
-            mobile = str(row['mobile']).strip()
-            mapped_key = str(row['mapped_key']).strip()
-            mapped_email = None
-            if 'mapped_email' in df.columns and not pd.isna(row['mapped_email']):
-                 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
-                mobile=mobile, 
-                mapped_key=mapped_key, 
-                mapped_email=mapped_email,
-                status=MappingRowStatus.ERROR
-            )
-
-            if not mobile or not mapped_key or mobile == 'nan' or mapped_key == 'nan':
-                row_preview.message = "手机号或映射账号为空"
-                stats["error"] += 1
-            elif mobile not in user_map:
-                row_preview.status = MappingRowStatus.AUTO_CREATE_USER
-                row_preview.message = "用户不存在,将自动创建"
-                stats["new"] += 1
-                stats["valid"] += 1
-            else:
-                user_id = user_map[mobile]
-                row_preview.user_id = user_id
+            # 3. Iterate rows
+            for index, row in df.iterrows():
+                mobile = str(row['mobile']).strip()
+                mapped_key = str(row['mapped_key']).strip()
+                mapped_email = None
+                if 'mapped_email' in df.columns and not pd.isna(row['mapped_email']):
+                     val = str(row['mapped_email']).strip()
+                     mapped_email = val if val else None
                 
-                if user_id in mapping_map:
-                    row_preview.status = MappingRowStatus.UPDATE
-                    row_preview.message = f"当前映射: {mapping_map[user_id].mapped_key}"
-                    stats["update"] += 1
-                    stats["valid"] += 1
-                else:
-                    row_preview.status = MappingRowStatus.NEW
+                row_preview = MappingRowPreview(
+                    row_index=index + 1, # 1-based index for UI
+                    mobile=mobile, 
+                    mapped_key=mapped_key, 
+                    mapped_email=mapped_email,
+                    status=MappingRowStatus.ERROR
+                )
+
+                if not mobile or not mapped_key or mobile == 'nan' or mapped_key == 'nan':
+                    row_preview.message = "手机号或映射账号为空"
+                    stats["error"] += 1
+                elif mobile not in user_map:
+                    row_preview.status = MappingRowStatus.AUTO_CREATE_USER
+                    row_preview.message = "用户不存在,将自动创建"
                     stats["new"] += 1
                     stats["valid"] += 1
-            
-            preview_rows.append(row_preview)
+                else:
+                    user_id = user_map[mobile]
+                    row_preview.user_id = user_id
+                    
+                    if user_id in mapping_map:
+                        row_preview.status = MappingRowStatus.UPDATE
+                        row_preview.message = f"当前映射: {mapping_map[user_id].mapped_key}"
+                        stats["update"] += 1
+                        stats["valid"] += 1
+                    else:
+                        row_preview.status = MappingRowStatus.NEW
+                        stats["new"] += 1
+                        stats["valid"] += 1
+                
+                preview_rows.append(row_preview)
 
-        return MappingPreviewResponse(
-            total_rows=len(df),
-            valid_count=stats["valid"],
-            error_count=stats["error"],
-            new_count=stats["new"],
-            update_count=stats["update"],
-            preview_rows=preview_rows
-        )
+            logger.info(f"导入预览完成: App {app_id}, 文件 {filename}, 行数 {len(df)}")
+            return MappingPreviewResponse(
+                total_rows=len(df),
+                valid_count=stats["valid"],
+                error_count=stats["error"],
+                new_count=stats["new"],
+                update_count=stats["update"],
+                preview_rows=preview_rows
+            )
+        except Exception as e:
+            logger.error(f"导入预览异常: {e}", exc_info=True)
+            raise
 
     @staticmethod
     def execute_import(
@@ -148,6 +157,7 @@ class MappingService:
         
         # 0. Verify SMS Code
         if not SmsService.verify_code(current_user_mobile, verification_code):
+            logger.warning(f"导入执行失败: 验证码错误 (User: {current_user_mobile})")
             raise HTTPException(status_code=400, detail="验证码无效或已过期")
 
         preview = MappingService.preview_import(db, app_id, file_content, filename)
@@ -237,6 +247,7 @@ class MappingService:
                 summary.failed += 1
                 log_item.status = "Failed"
                 log_item.message = str(e)
+                logger.error(f"导入行失败 (Mobile: {row.mobile}): {e}")
             
             logs.append(log_item)