apps.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637
  1. import secrets
  2. import string
  3. import io
  4. import csv
  5. import pandas as pd
  6. from typing import List
  7. from fastapi import APIRouter, Depends, HTTPException, Response
  8. from fastapi.responses import StreamingResponse
  9. from sqlalchemy.orm import Session
  10. from sqlalchemy import desc
  11. from app.api.v1 import deps
  12. from app.core import security
  13. from app.models.application import Application
  14. from app.models.user import User
  15. from app.models.mapping import AppUserMapping
  16. from app.schemas.application import (
  17. ApplicationCreate,
  18. ApplicationUpdate,
  19. ApplicationResponse,
  20. ApplicationList,
  21. ApplicationSecretDisplay,
  22. ViewSecretRequest
  23. )
  24. from app.schemas.mapping import (
  25. MappingList,
  26. MappingResponse,
  27. MappingCreate,
  28. MappingUpdate,
  29. MappingDelete,
  30. MappingPreviewResponse,
  31. MappingImportSummary,
  32. MappingStrategy
  33. )
  34. from app.schemas.user import UserSyncRequest
  35. from app.services.mapping_service import MappingService
  36. router = APIRouter()
  37. def generate_access_token():
  38. return secrets.token_urlsafe(32)
  39. def generate_app_credentials():
  40. # Generate a random 16-char App ID (hex or alphanumeric)
  41. app_id = "app_" + secrets.token_hex(8)
  42. # Generate a strong 32-char App Secret
  43. alphabet = string.ascii_letters + string.digits
  44. app_secret = ''.join(secrets.choice(alphabet) for i in range(32))
  45. return app_id, app_secret
  46. @router.get("/", response_model=ApplicationList, summary="获取应用列表")
  47. def read_apps(
  48. skip: int = 0,
  49. limit: int = 10,
  50. search: str = None,
  51. db: Session = Depends(deps.get_db),
  52. current_user: User = Depends(deps.get_current_active_user),
  53. ):
  54. """
  55. 获取应用列表(分页)。
  56. 超级管理员可以查看所有,开发者只能查看自己的应用。
  57. """
  58. query = db.query(Application).filter(Application.is_deleted == False)
  59. if current_user.role != "SUPER_ADMIN":
  60. query = query.filter(Application.owner_id == current_user.id)
  61. if search:
  62. # Search by name or app_id
  63. from sqlalchemy import or_
  64. query = query.filter(
  65. or_(
  66. Application.app_name.ilike(f"%{search}%"),
  67. Application.app_id.ilike(f"%{search}%")
  68. )
  69. )
  70. total = query.count()
  71. apps = query.order_by(desc(Application.id)).offset(skip).limit(limit).all()
  72. return {"total": total, "items": apps}
  73. @router.post("/", response_model=ApplicationSecretDisplay, summary="创建应用")
  74. def create_app(
  75. *,
  76. db: Session = Depends(deps.get_db),
  77. app_in: ApplicationCreate,
  78. current_user: User = Depends(deps.get_current_active_user),
  79. ):
  80. """
  81. 创建新应用。只会返回一次明文密钥。
  82. """
  83. # 1. Generate ID and Secret
  84. app_id, app_secret = generate_app_credentials()
  85. # 2. Generate Access Token
  86. access_token = generate_access_token()
  87. # 3. Store Secret (Plain text needed for HMAC verification)
  88. db_app = Application(
  89. app_id=app_id,
  90. app_secret=app_secret,
  91. access_token=access_token,
  92. app_name=app_in.app_name,
  93. icon_url=app_in.icon_url,
  94. protocol_type=app_in.protocol_type,
  95. redirect_uris=app_in.redirect_uris,
  96. notification_url=app_in.notification_url,
  97. owner_id=current_user.id # Assign owner
  98. )
  99. db.add(db_app)
  100. db.commit()
  101. db.refresh(db_app)
  102. return ApplicationSecretDisplay(app_id=app_id, app_secret=app_secret, access_token=access_token)
  103. @router.put("/{app_id}", response_model=ApplicationResponse, summary="更新应用")
  104. def update_app(
  105. *,
  106. db: Session = Depends(deps.get_db),
  107. app_id: int,
  108. app_in: ApplicationUpdate,
  109. current_user: User = Depends(deps.get_current_active_user),
  110. ):
  111. """
  112. 更新应用信息。
  113. """
  114. app = db.query(Application).filter(Application.id == app_id).first()
  115. if not app:
  116. raise HTTPException(status_code=404, detail="应用未找到")
  117. # Check ownership
  118. if current_user.role != "SUPER_ADMIN" and app.owner_id != current_user.id:
  119. raise HTTPException(status_code=403, detail="权限不足")
  120. update_data = app_in.model_dump(exclude_unset=True)
  121. for field, value in update_data.items():
  122. setattr(app, field, value)
  123. db.add(app)
  124. db.commit()
  125. db.refresh(app)
  126. return app
  127. @router.delete("/{app_id}", response_model=ApplicationResponse, summary="删除应用")
  128. def delete_app(
  129. *,
  130. db: Session = Depends(deps.get_db),
  131. app_id: int,
  132. current_user: User = Depends(deps.get_current_active_user),
  133. ):
  134. """
  135. 软删除应用。
  136. """
  137. app = db.query(Application).filter(Application.id == app_id).first()
  138. if not app:
  139. raise HTTPException(status_code=404, detail="应用未找到")
  140. # Check ownership
  141. if current_user.role != "SUPER_ADMIN" and app.owner_id != current_user.id:
  142. raise HTTPException(status_code=403, detail="权限不足")
  143. app.is_deleted = True
  144. db.add(app)
  145. db.commit()
  146. return app
  147. @router.post("/{app_id}/regenerate-secret", response_model=ApplicationSecretDisplay, summary="重新生成密钥")
  148. def regenerate_secret(
  149. *,
  150. db: Session = Depends(deps.get_db),
  151. app_id: int,
  152. current_user: User = Depends(deps.get_current_active_user),
  153. ):
  154. """
  155. 重新生成应用密钥。
  156. """
  157. app = db.query(Application).filter(Application.id == app_id).first()
  158. if not app:
  159. raise HTTPException(status_code=404, detail="应用未找到")
  160. # Check ownership
  161. if current_user.role != "SUPER_ADMIN" and app.owner_id != current_user.id:
  162. raise HTTPException(status_code=403, detail="权限不足")
  163. _, new_secret = generate_app_credentials()
  164. app.app_secret = new_secret
  165. db.add(app)
  166. db.commit()
  167. return ApplicationSecretDisplay(app_id=app.app_id, app_secret=new_secret, access_token=app.access_token)
  168. @router.post("/{app_id}/view-secret", response_model=ApplicationSecretDisplay, summary="查看密钥")
  169. def view_secret(
  170. *,
  171. db: Session = Depends(deps.get_db),
  172. app_id: int,
  173. req: ViewSecretRequest,
  174. current_user: User = Depends(deps.get_current_active_user),
  175. ):
  176. """
  177. 查看应用密钥。需要验证用户密码。
  178. """
  179. # 1. Verify Password
  180. if not security.verify_password(req.password, current_user.password_hash):
  181. raise HTTPException(status_code=401, detail="密码错误")
  182. app = db.query(Application).filter(Application.id == app_id).first()
  183. if not app:
  184. raise HTTPException(status_code=404, detail="应用未找到")
  185. # Check ownership
  186. if current_user.role != "SUPER_ADMIN" and app.owner_id != current_user.id:
  187. raise HTTPException(status_code=403, detail="权限不足")
  188. return ApplicationSecretDisplay(app_id=app.app_id, app_secret=app.app_secret, access_token=app.access_token)
  189. # ==========================================
  190. # Mappings
  191. # ==========================================
  192. @router.get("/{app_id}/mappings", response_model=MappingList, summary="获取应用映射列表")
  193. def read_mappings(
  194. *,
  195. db: Session = Depends(deps.get_db),
  196. app_id: int,
  197. skip: int = 0,
  198. limit: int = 10,
  199. current_user: User = Depends(deps.get_current_active_user),
  200. ):
  201. """
  202. 获取应用的账号映射列表。
  203. """
  204. app = db.query(Application).filter(Application.id == app_id).first()
  205. if not app:
  206. raise HTTPException(status_code=404, detail="应用未找到")
  207. if current_user.role != "SUPER_ADMIN" and app.owner_id != current_user.id:
  208. raise HTTPException(status_code=403, detail="权限不足")
  209. query = db.query(AppUserMapping).filter(AppUserMapping.app_id == app_id)
  210. total = query.count()
  211. mappings = query.order_by(desc(AppUserMapping.id)).offset(skip).limit(limit).all()
  212. # Enrich with user mobile (handled by ORM relation usually, but for Pydantic 'from_attributes')
  213. # We added `user_mobile` to MappingResponse, so we need to ensure it's populated.
  214. # The ORM `mapping.user` is lazy loaded, which is fine for sync code.
  215. result = []
  216. for m in mappings:
  217. result.append(MappingResponse(
  218. id=m.id,
  219. app_id=m.app_id,
  220. user_id=m.user_id,
  221. mapped_key=m.mapped_key,
  222. mapped_email=m.mapped_email,
  223. user_mobile=m.user.mobile if m.user else "Deleted User"
  224. ))
  225. return {"total": total, "items": result}
  226. @router.post("/{app_id}/mappings", response_model=MappingResponse, summary="创建映射")
  227. def create_mapping(
  228. *,
  229. db: Session = Depends(deps.get_db),
  230. app_id: int,
  231. mapping_in: MappingCreate,
  232. current_user: User = Depends(deps.get_current_active_user),
  233. ):
  234. """
  235. 手动创建映射。
  236. """
  237. app = db.query(Application).filter(Application.id == app_id).first()
  238. if not app:
  239. raise HTTPException(status_code=404, detail="应用未找到")
  240. if current_user.role != "SUPER_ADMIN" and app.owner_id != current_user.id:
  241. raise HTTPException(status_code=403, detail="权限不足")
  242. # Verify Password
  243. if not security.verify_password(mapping_in.password, current_user.password_hash):
  244. raise HTTPException(status_code=401, detail="密码错误")
  245. # Normalize input: treat empty strings as None to avoid unique constraint violations
  246. mapped_key = mapping_in.mapped_key if mapping_in.mapped_key else None
  247. mapped_email = mapping_in.mapped_email if mapping_in.mapped_email else None
  248. # 1. Find User or Create
  249. user = db.query(User).filter(User.mobile == mapping_in.mobile, User.is_deleted == 0).first()
  250. new_user_created = False
  251. generated_password = None
  252. if not user:
  253. # Auto create user
  254. password_plain = security.generate_alphanumeric_password(8) # Random password letters+digits
  255. user = User(
  256. mobile=mapping_in.mobile,
  257. password_hash=security.get_password_hash(password_plain),
  258. status="ACTIVE",
  259. role="ORDINARY_USER"
  260. )
  261. db.add(user)
  262. db.commit()
  263. db.refresh(user)
  264. new_user_created = True
  265. generated_password = password_plain
  266. # 2. Check if mapping exists
  267. existing = db.query(AppUserMapping).filter(
  268. AppUserMapping.app_id == app_id,
  269. AppUserMapping.user_id == user.id
  270. ).first()
  271. if existing:
  272. raise HTTPException(status_code=400, detail="该用户的映射已存在")
  273. # 3. Check Uniqueness for mapped_email (if provided)
  274. if mapped_email:
  275. email_exists = db.query(AppUserMapping).filter(
  276. AppUserMapping.app_id == app_id,
  277. AppUserMapping.mapped_email == mapped_email
  278. ).first()
  279. if email_exists:
  280. raise HTTPException(status_code=400, detail=f"该应用下邮箱 {mapped_email} 已被使用")
  281. # 4. Check Uniqueness for mapped_key
  282. if mapped_key:
  283. key_exists = db.query(AppUserMapping).filter(
  284. AppUserMapping.app_id == app_id,
  285. AppUserMapping.mapped_key == mapped_key
  286. ).first()
  287. if key_exists:
  288. raise HTTPException(status_code=400, detail=f"该应用下账号 {mapped_key} 已被使用")
  289. # 5. Create
  290. mapping = AppUserMapping(
  291. app_id=app_id,
  292. user_id=user.id,
  293. mapped_key=mapped_key,
  294. mapped_email=mapped_email
  295. )
  296. db.add(mapping)
  297. db.commit()
  298. db.refresh(mapping)
  299. return MappingResponse(
  300. id=mapping.id,
  301. app_id=mapping.app_id,
  302. user_id=mapping.user_id,
  303. mapped_key=mapping.mapped_key,
  304. mapped_email=mapping.mapped_email,
  305. user_mobile=user.mobile,
  306. new_user_created=new_user_created,
  307. generated_password=generated_password
  308. )
  309. @router.put("/{app_id}/mappings/{mapping_id}", response_model=MappingResponse, summary="更新映射")
  310. def update_mapping(
  311. *,
  312. db: Session = Depends(deps.get_db),
  313. app_id: int,
  314. mapping_id: int,
  315. mapping_in: MappingUpdate,
  316. current_user: User = Depends(deps.get_current_active_user),
  317. ):
  318. """
  319. 更新映射信息。
  320. """
  321. app = db.query(Application).filter(Application.id == app_id).first()
  322. if not app:
  323. raise HTTPException(status_code=404, detail="应用未找到")
  324. if current_user.role != "SUPER_ADMIN" and app.owner_id != current_user.id:
  325. raise HTTPException(status_code=403, detail="权限不足")
  326. # Verify Password
  327. if not security.verify_password(mapping_in.password, current_user.password_hash):
  328. raise HTTPException(status_code=401, detail="密码错误")
  329. mapping = db.query(AppUserMapping).filter(
  330. AppUserMapping.id == mapping_id,
  331. AppUserMapping.app_id == app_id
  332. ).first()
  333. if not mapping:
  334. raise HTTPException(status_code=404, detail="映射未找到")
  335. # Check Uniqueness for mapped_key
  336. if mapping_in.mapped_key is not None and mapping_in.mapped_key != mapping.mapped_key:
  337. if mapping_in.mapped_key:
  338. key_exists = db.query(AppUserMapping).filter(
  339. AppUserMapping.app_id == app_id,
  340. AppUserMapping.mapped_key == mapping_in.mapped_key
  341. ).first()
  342. if key_exists:
  343. raise HTTPException(status_code=400, detail=f"该应用下账号 {mapping_in.mapped_key} 已被使用")
  344. # Check Uniqueness for mapped_email
  345. if mapping_in.mapped_email is not None and mapping_in.mapped_email != mapping.mapped_email:
  346. if mapping_in.mapped_email:
  347. email_exists = db.query(AppUserMapping).filter(
  348. AppUserMapping.app_id == app_id,
  349. AppUserMapping.mapped_email == mapping_in.mapped_email
  350. ).first()
  351. if email_exists:
  352. raise HTTPException(status_code=400, detail=f"该应用下邮箱 {mapping_in.mapped_email} 已被使用")
  353. if mapping_in.mapped_key is not None:
  354. mapping.mapped_key = mapping_in.mapped_key
  355. if mapping_in.mapped_email is not None:
  356. mapping.mapped_email = mapping_in.mapped_email
  357. db.add(mapping)
  358. db.commit()
  359. db.refresh(mapping)
  360. return MappingResponse(
  361. id=mapping.id,
  362. app_id=mapping.app_id,
  363. user_id=mapping.user_id,
  364. mapped_key=mapping.mapped_key,
  365. mapped_email=mapping.mapped_email,
  366. user_mobile=mapping.user.mobile if mapping.user else "Deleted User",
  367. is_active=mapping.is_active
  368. )
  369. @router.delete("/{app_id}/mappings/{mapping_id}", summary="删除映射")
  370. def delete_mapping(
  371. *,
  372. db: Session = Depends(deps.get_db),
  373. app_id: int,
  374. mapping_id: int,
  375. req: MappingDelete,
  376. current_user: User = Depends(deps.get_current_active_user),
  377. ):
  378. """
  379. 删除映射关系。需验证密码。
  380. """
  381. # Verify Password
  382. if not security.verify_password(req.password, current_user.password_hash):
  383. raise HTTPException(status_code=401, detail="密码错误")
  384. mapping = db.query(AppUserMapping).filter(
  385. AppUserMapping.id == mapping_id,
  386. AppUserMapping.app_id == app_id
  387. ).first()
  388. if not mapping:
  389. raise HTTPException(status_code=404, detail="映射未找到")
  390. app = db.query(Application).filter(Application.id == app_id).first()
  391. if current_user.role != "SUPER_ADMIN" and app.owner_id != current_user.id:
  392. raise HTTPException(status_code=403, detail="权限不足")
  393. db.delete(mapping)
  394. db.commit()
  395. return {"message": "删除成功"}
  396. @router.get("/{app_id}/mappings/export", summary="导出映射")
  397. def export_mappings(
  398. *,
  399. db: Session = Depends(deps.get_db),
  400. app_id: int,
  401. current_user: User = Depends(deps.get_current_active_user),
  402. ):
  403. """
  404. 导出所有映射到 Excel (.xlsx)。
  405. """
  406. app = db.query(Application).filter(Application.id == app_id).first()
  407. if not app:
  408. raise HTTPException(status_code=404, detail="应用未找到")
  409. if current_user.role != "SUPER_ADMIN" and app.owner_id != current_user.id:
  410. raise HTTPException(status_code=403, detail="权限不足")
  411. mappings = db.query(AppUserMapping).filter(AppUserMapping.app_id == app_id).all()
  412. # Prepare data for DataFrame
  413. data = []
  414. for m in mappings:
  415. mobile = m.user.mobile if m.user else "Deleted User"
  416. data.append({
  417. '手机号': mobile,
  418. '映射账号': m.mapped_key,
  419. '映射邮箱': m.mapped_email or ''
  420. })
  421. # Create DataFrame
  422. df = pd.DataFrame(data)
  423. # If no data, create an empty DataFrame with columns
  424. if not data:
  425. df = pd.DataFrame(columns=['手机号', '映射账号', '映射邮箱'])
  426. # Write to Excel BytesIO
  427. output = io.BytesIO()
  428. with pd.ExcelWriter(output, engine='openpyxl') as writer:
  429. df.to_excel(writer, index=False)
  430. output.seek(0)
  431. filename = f"mappings_app_{app_id}.xlsx"
  432. return StreamingResponse(
  433. output,
  434. media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  435. headers={"Content-Disposition": f"attachment; filename={filename}"}
  436. )
  437. from fastapi import UploadFile, File
  438. @router.post("/{app_id}/mapping/preview", response_model=MappingPreviewResponse, summary="预览映射导入")
  439. async def preview_mapping(
  440. app_id: int,
  441. file: UploadFile = File(...),
  442. db: Session = Depends(deps.get_db),
  443. current_user: User = Depends(deps.get_current_active_user),
  444. ):
  445. """
  446. 预览 Excel/CSV 映射导入。
  447. """
  448. app = db.query(Application).filter(Application.id == app_id).first()
  449. if not app:
  450. raise HTTPException(status_code=404, detail="应用未找到")
  451. if current_user.role != "SUPER_ADMIN" and app.owner_id != current_user.id:
  452. raise HTTPException(status_code=403, detail="权限不足")
  453. contents = await file.read()
  454. filename = file.filename
  455. return MappingService.preview_import(db, app_id, contents, filename)
  456. @router.post("/{app_id}/mapping/import", response_model=MappingImportSummary, summary="执行映射导入")
  457. async def import_mapping(
  458. app_id: int,
  459. file: UploadFile = File(...),
  460. strategy: MappingStrategy = MappingStrategy.SKIP,
  461. db: Session = Depends(deps.get_db),
  462. current_user: User = Depends(deps.get_current_active_user),
  463. ):
  464. """
  465. 执行映射导入操作。
  466. """
  467. app = db.query(Application).filter(Application.id == app_id).first()
  468. if not app:
  469. raise HTTPException(status_code=404, detail="应用未找到")
  470. if current_user.role != "SUPER_ADMIN" and app.owner_id != current_user.id:
  471. raise HTTPException(status_code=403, detail="权限不足")
  472. contents = await file.read()
  473. filename = file.filename
  474. return MappingService.execute_import(db, app_id, contents, filename, strategy)
  475. @router.post("/mapping/sync", response_model=MappingResponse, summary="同步映射 (M2M)")
  476. def sync_mapping(
  477. *,
  478. db: Session = Depends(deps.get_db),
  479. sync_in: UserSyncRequest,
  480. current_app: Application = Depends(deps.get_current_app),
  481. ):
  482. """
  483. 从外部平台同步用户映射关系(机器对机器)。
  484. 只同步映射关系,不创建或更新用户本身。
  485. 需要应用访问令牌 (Authorization Bearer JWT 或 X-App-Access-Token)。
  486. """
  487. # Normalize input: treat empty strings as None
  488. mapped_key = sync_in.mapped_key if sync_in.mapped_key else None
  489. mapped_email = sync_in.mapped_email if sync_in.mapped_email else None
  490. # 1. Find User or Create
  491. user = db.query(User).filter(User.mobile == sync_in.mobile).first()
  492. if not user:
  493. # Auto create user
  494. password = security.generate_alphanumeric_password(8) # Random password letters+digits
  495. user = User(
  496. mobile=sync_in.mobile,
  497. password_hash=security.get_password_hash(password),
  498. status="ACTIVE",
  499. role="ORDINARY_USER"
  500. )
  501. db.add(user)
  502. db.commit()
  503. db.refresh(user)
  504. # 2. Handle Mapping
  505. mapping = db.query(AppUserMapping).filter(
  506. AppUserMapping.app_id == current_app.id,
  507. AppUserMapping.user_id == user.id
  508. ).first()
  509. # Check Uniqueness for mapped_key (if changing or new, and provided)
  510. if mapped_key and (not mapping or mapping.mapped_key != mapped_key):
  511. key_exists = db.query(AppUserMapping).filter(
  512. AppUserMapping.app_id == current_app.id,
  513. AppUserMapping.mapped_key == mapped_key
  514. ).first()
  515. if key_exists:
  516. raise HTTPException(status_code=400, detail=f"该应用下账号 {mapped_key} 已被使用")
  517. # Check Uniqueness for mapped_email (if changing or new, and provided)
  518. if mapped_email and (not mapping or mapping.mapped_email != mapped_email):
  519. email_exists = db.query(AppUserMapping).filter(
  520. AppUserMapping.app_id == current_app.id,
  521. AppUserMapping.mapped_email == mapped_email
  522. ).first()
  523. if email_exists:
  524. raise HTTPException(status_code=400, detail=f"该应用下邮箱 {mapped_email} 已被使用")
  525. if mapping:
  526. # Update existing mapping
  527. if sync_in.mapped_key is not None:
  528. mapping.mapped_key = mapped_key
  529. if sync_in.is_active is not None:
  530. mapping.is_active = sync_in.is_active
  531. if sync_in.mapped_email is not None:
  532. mapping.mapped_email = mapped_email
  533. else:
  534. # Create new mapping
  535. mapping = AppUserMapping(
  536. app_id=current_app.id,
  537. user_id=user.id,
  538. mapped_key=mapped_key,
  539. mapped_email=mapped_email,
  540. is_active=sync_in.is_active if sync_in.is_active is not None else True
  541. )
  542. db.add(mapping)
  543. db.commit()
  544. db.refresh(mapping)
  545. return MappingResponse(
  546. id=mapping.id,
  547. app_id=mapping.app_id,
  548. user_id=mapping.user_id,
  549. mapped_key=mapping.mapped_key,
  550. mapped_email=mapping.mapped_email,
  551. user_mobile=user.mobile,
  552. is_active=mapping.is_active
  553. )