# -*- coding: utf-8 -*- """造测试数据验证邀请列表"真实头像能不能连上"。 A 邀请 B1(上传真实头像)/B2(无头像→色块)/B3(设昵称→色块), 然后: ① 打印 A 的邀请码 + /invitees 返回(看 B1 有 avatar_url、B2 空、B3 昵称); ② 直接 HTTP 访问 B1 的头像地址,确认文件能取到(后端静态托管通)。 前端 AsyncImage 实际显示,装机后真机用 A 登录看。 跑: scripts/seed_invitee_avatar_test.py""" import io import struct import sys import zlib import httpx sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") BASE = "http://127.0.0.1:8770" def solid_png(size: int = 96, rgb: tuple = (80, 140, 240)) -> bytes: """生成一张纯色 PNG(蓝色 96x96),够真机看清、魔数是合法 PNG。""" row = b"\x00" + bytes(rgb) * size raw = row * size comp = zlib.compress(raw) def chunk(typ: bytes, data: bytes) -> bytes: c = typ + data return struct.pack(">I", len(data)) + c + struct.pack(">I", zlib.crc32(c) & 0xFFFFFFFF) sig = b"\x89PNG\r\n\x1a\n" ihdr = struct.pack(">IIBBBBB", size, size, 8, 2, 0, 0, 0) # 8bit, RGB return sig + chunk(b"IHDR", ihdr) + chunk(b"IDAT", comp) + chunk(b"IEND", b"") def login(c: httpx.Client, phone: str) -> str: c.post(f"{BASE}/api/v1/auth/sms/send", json={"phone": phone}) r = c.post(f"{BASE}/api/v1/auth/sms/login", json={"phone": phone, "code": "123456"}) r.raise_for_status() return r.json()["access_token"] def auth(tok: str) -> dict: return {"Authorization": f"Bearer {tok}"} with httpx.Client(timeout=15) as c: # A = 邀请人(真机用它登录看) A_PHONE = "13900008001" a_tok = login(c, A_PHONE) a_code = c.get(f"{BASE}/api/v1/invite/me", headers=auth(a_tok)).json()["invite_code"] print(f"邀请人 A: 手机号={A_PHONE} 验证码=123456 邀请码={a_code}") # B1: 绑 A 码 + 上传真实头像 b1 = login(c, "13900008002") c.post(f"{BASE}/api/v1/invite/bind", json={"invite_code": a_code}, headers=auth(b1)) r = c.post( f"{BASE}/api/v1/user/avatar", headers=auth(b1), files={"file": ("avatar.png", io.BytesIO(solid_png()), "image/png")}, ) print(f"B1 绑定 + 上传头像 -> HTTP {r.status_code}, avatar_url={r.json().get('avatar_url')!r}") # B2: 绑 A 码, 不传头像(测色块兜底) b2 = login(c, "13900008003") c.post(f"{BASE}/api/v1/invite/bind", json={"invite_code": a_code}, headers=auth(b2)) print("B2 绑定, 无头像(测色块)") # B3: 绑 A 码 + 设昵称(测昵称优先 + 色块) b3 = login(c, "13900008004") c.post(f"{BASE}/api/v1/invite/bind", json={"invite_code": a_code}, headers=auth(b3)) c.patch(f"{BASE}/api/v1/user/profile", json={"nickname": "测试昵称"}, headers=auth(b3)) print("B3 绑定 + 昵称='测试昵称'") # A 的邀请列表 inv = c.get(f"{BASE}/api/v1/invite/invitees", headers=auth(a_tok)).json() print(f"\n=== A 的 /invitees (共 {inv['total']} 人) ===") for it in inv["items"]: print(f" name={it['display_name']!r} avatar={it['avatar_url']!r} coins={it['coins']} time={it['invited_at']}") # 关键: 直接 HTTP 取 B1 的头像文件, 确认后端静态托管能 serve(= 前端拿这地址也能加载) b1_avatar = next((it["avatar_url"] for it in inv["items"] if it["avatar_url"]), None) print("\n=== 头像文件 HTTP 可访问性(后端静态托管) ===") if b1_avatar: rr = c.get(f"{BASE}{b1_avatar}") ok = rr.status_code == 200 and (rr.headers.get("content-type", "").startswith("image")) print(f" GET {BASE}{b1_avatar}") print(f" -> HTTP {rr.status_code}, content-type={rr.headers.get('content-type')}, {len(rr.content)} bytes {'✅ 能取到' if ok else '✗ 取不到'}") else: print(" 没有带头像的被邀请人?!")