|
|
@@ -36,7 +36,7 @@ from app.schemas.mapping import (
|
|
|
MappingStrategy,
|
|
|
ImportLogResponse
|
|
|
)
|
|
|
-from app.schemas.user import UserSyncRequest
|
|
|
+from app.schemas.user import UserSyncRequest, UserSyncList
|
|
|
from app.services.mapping_service import MappingService
|
|
|
from app.services.sms_service import SmsService
|
|
|
from app.services.log_service import LogService
|
|
|
@@ -672,6 +672,104 @@ def delete_mapping(
|
|
|
|
|
|
return {"message": "删除成功"}
|
|
|
|
|
|
+@router.post("/{app_id}/sync-users", summary="同步所有用户")
|
|
|
+def sync_users_to_app(
|
|
|
+ *,
|
|
|
+ db: Session = Depends(deps.get_db),
|
|
|
+ app_id: int,
|
|
|
+ current_user: User = Depends(deps.get_current_active_user),
|
|
|
+):
|
|
|
+ """
|
|
|
+ 一键导入用户管理中的用户数据到应用映射中。
|
|
|
+ 规则:如果用户已在映射中(基于手机号/User ID),则跳过。
|
|
|
+ 否则创建映射,使用英文名称作为映射账号(如果为空则使用手机号)。
|
|
|
+ """
|
|
|
+ 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="权限不足")
|
|
|
+
|
|
|
+ # Get all active users
|
|
|
+ users = db.query(User).filter(User.is_deleted == 0).all()
|
|
|
+
|
|
|
+ # Get existing mappings (user_ids)
|
|
|
+ existing_mappings = db.query(AppUserMapping).filter(AppUserMapping.app_id == app_id).all()
|
|
|
+ mapped_user_ids = {m.user_id for m in existing_mappings}
|
|
|
+
|
|
|
+ new_mappings = []
|
|
|
+
|
|
|
+ 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,
|
|
|
+ mapped_key=mapped_key,
|
|
|
+ mapped_email=None,
|
|
|
+ is_active=True
|
|
|
+ )
|
|
|
+ new_mappings.append(mapping)
|
|
|
+
|
|
|
+ if new_mappings:
|
|
|
+ try:
|
|
|
+ db.bulk_save_objects(new_mappings)
|
|
|
+ db.commit()
|
|
|
+ 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.
|
|
|
+ # Fallback: try one by one
|
|
|
+ success_count = 0
|
|
|
+ for m in new_mappings:
|
|
|
+ try:
|
|
|
+ db.add(m)
|
|
|
+ db.commit()
|
|
|
+ success_count += 1
|
|
|
+ except:
|
|
|
+ db.rollback()
|
|
|
+
|
|
|
+ LogService.create_log(
|
|
|
+ db=db,
|
|
|
+ app_id=app.id,
|
|
|
+ operator_id=current_user.id,
|
|
|
+ action_type=ActionType.IMPORT,
|
|
|
+ details={"message": "Sync all users (partial)", "attempted": len(new_mappings), "success": success_count}
|
|
|
+ )
|
|
|
+ return {"message": f"同步完成,成功 {success_count} 个,失败 {len(new_mappings) - success_count} 个 (可能是账号冲突)"}
|
|
|
+
|
|
|
+ # Log success
|
|
|
+ LogService.create_log(
|
|
|
+ db=db,
|
|
|
+ app_id=app.id,
|
|
|
+ operator_id=current_user.id,
|
|
|
+ action_type=ActionType.IMPORT,
|
|
|
+ details={"message": "Sync all users", "count": len(new_mappings)}
|
|
|
+ )
|
|
|
+ return {"message": f"同步成功,新增 {len(new_mappings)} 个用户映射"}
|
|
|
+
|
|
|
+ return {"message": "没有需要同步的用户"}
|
|
|
+
|
|
|
@router.get("/{app_id}/mappings/export", summary="导出映射")
|
|
|
def export_mappings(
|
|
|
*,
|
|
|
@@ -790,6 +888,26 @@ async def import_mapping(
|
|
|
|
|
|
return result
|
|
|
|
|
|
+@router.get("/mapping/users", response_model=UserSyncList, summary="获取全量用户(M2M)")
|
|
|
+def get_all_users_m2m(
|
|
|
+ *,
|
|
|
+ db: Session = Depends(deps.get_db),
|
|
|
+ skip: int = 0,
|
|
|
+ limit: int = 100,
|
|
|
+ current_app: Application = Depends(deps.get_current_app),
|
|
|
+):
|
|
|
+ """
|
|
|
+ 开发者拉取全量用户接口。
|
|
|
+ 仅返回:手机号、姓名、英文名。
|
|
|
+ 需要应用访问令牌 (Authorization Bearer JWT 或 X-App-Access-Token)。
|
|
|
+ """
|
|
|
+ query = db.query(User).filter(User.is_deleted == 0)
|
|
|
+
|
|
|
+ total = query.count()
|
|
|
+ users = query.order_by(User.id).offset(skip).limit(limit).all()
|
|
|
+
|
|
|
+ return {"total": total, "items": users}
|
|
|
+
|
|
|
@router.post("/mapping/sync", response_model=MappingResponse, summary="同步映射 (M2M)")
|
|
|
def sync_mapping(
|
|
|
*,
|