diff --git a/alembic/versions/0056_task_system_v2.py b/alembic/versions/0056_task_system_v2.py new file mode 100644 index 0000000..9b1d2aa --- /dev/null +++ b/alembic/versions/0056_task_system_v2.py @@ -0,0 +1,138 @@ +"""任务系统 v2:发布字段扩展 + 交付/里程碑/验收/发票表 + 标书扩展 + 评价唯一约束 + +Revision ID: 0056 +Revises: 0055 +""" +from alembic import op +import sqlalchemy as sa + +revision = "0056" +down_revision = "0055" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ── tasks 扩展字段(0056)── + op.add_column("tasks", sa.Column("resource_support_json", sa.Text(), nullable=False)) + op.add_column("tasks", sa.Column("raw_materials_json", sa.Text(), nullable=False)) + op.add_column("tasks", sa.Column("deliver_way", sa.String(length=20), nullable=False, server_default="online")) + op.add_column("tasks", sa.Column("deliver_items", sa.Text(), nullable=False)) + op.add_column("tasks", sa.Column("accept_standard", sa.Text(), nullable=False)) + op.add_column("tasks", sa.Column("price_type", sa.String(length=20), nullable=False, server_default="fixed")) + op.add_column("tasks", sa.Column("pay_type", sa.String(length=20), nullable=False, server_default="escrow")) + op.add_column("tasks", sa.Column("escrow_ratio", sa.Integer(), nullable=False, server_default="100")) + op.add_column("tasks", sa.Column("invoice_req", sa.Text(), nullable=False)) + op.add_column("tasks", sa.Column("milestone_json", sa.Text(), nullable=False)) + op.add_column("tasks", sa.Column("attachments_json", sa.Text(), nullable=False)) + op.add_column("tasks", sa.Column("deliver_at", sa.String(length=40), nullable=False, server_default="")) + op.add_column("tasks", sa.Column("accepted_at", sa.String(length=40), nullable=False, server_default="")) + op.add_column("tasks", sa.Column("reject_count", sa.Integer(), nullable=False, server_default="0")) + op.add_column("tasks", sa.Column("settle_status", sa.String(length=20), nullable=False, server_default="")) + op.add_column("tasks", sa.Column("settled_at", sa.String(length=40), nullable=False, server_default="")) + op.add_column("tasks", sa.Column("on_time", sa.Boolean(), nullable=True)) + op.add_column("tasks", sa.Column("contract_status", sa.String(length=20), nullable=False, server_default="")) + + # ── bids 标书扩展 ── + op.add_column("bids", sa.Column("deliver_days", sa.Integer(), nullable=False, server_default="0")) + op.add_column("bids", sa.Column("team_json", sa.Text(), nullable=False)) + op.add_column("bids", sa.Column("cases_json", sa.Text(), nullable=False)) + op.add_column("bids", sa.Column("commit_json", sa.Text(), nullable=False)) + op.add_column("bids", sa.Column("score_total", sa.Float(), nullable=False, server_default="0")) + op.add_column("bids", sa.Column("score_json", sa.Text(), nullable=False)) + op.add_column("bids", sa.Column("review_note", sa.String(length=500), nullable=False, server_default="")) + + # ── ratings:字段 + 唯一约束(先清理历史重复,保留最早一条)── + op.add_column("ratings", sa.Column("rating_type", sa.String(length=20), nullable=False, server_default="fulfill")) + op.add_column("ratings", sa.Column("dims_json", sa.Text(), nullable=False)) + op.add_column("ratings", sa.Column("tags_json", sa.Text(), nullable=False)) + op.execute( + "DELETE r1 FROM ratings r1 " + "INNER JOIN ratings r2 " + "ON r1.task_id = r2.task_id AND r1.from_id = r2.from_id AND r1.id > r2.id" + ) + op.create_unique_constraint("uq_rating_task_from", "ratings", ["task_id", "from_id"]) + + # ── 新表:task_deliveries / task_milestones / task_acceptances / invoices ── + op.create_table( + "task_deliveries", + sa.Column("id", sa.String(length=64), primary_key=True), + sa.Column("task_id", sa.String(length=64), nullable=False, index=True), + sa.Column("milestone_id", sa.String(length=64), nullable=False, server_default=""), + sa.Column("version", sa.Integer(), nullable=False, server_default="1"), + sa.Column("attachments_json", sa.Text(), nullable=False), + sa.Column("note", sa.String(length=1000), nullable=False, server_default=""), + sa.Column("created_at", sa.String(length=40), nullable=False, server_default=""), + sa.ForeignKeyConstraint(["task_id"], ["tasks.id"], ondelete="CASCADE"), + ) + op.create_table( + "task_milestones", + sa.Column("id", sa.String(length=64), primary_key=True), + sa.Column("task_id", sa.String(length=64), nullable=False, index=True), + sa.Column("index", sa.Integer(), nullable=False, server_default="1"), + sa.Column("name", sa.String(length=200), nullable=False, server_default=""), + sa.Column("desc", sa.String(length=1000), nullable=False, server_default=""), + sa.Column("days", sa.Integer(), nullable=False, server_default="0"), + sa.Column("deliverables_json", sa.Text(), nullable=False), + sa.Column("payment_ratio", sa.Float(), nullable=False, server_default="0"), + sa.Column("status", sa.String(length=20), nullable=False, server_default="pending"), + sa.Column("delivered_at", sa.String(length=40), nullable=False, server_default=""), + sa.Column("accepted_at", sa.String(length=40), nullable=False, server_default=""), + sa.Column("created_at", sa.String(length=40), nullable=False, server_default=""), + sa.ForeignKeyConstraint(["task_id"], ["tasks.id"], ondelete="CASCADE"), + ) + op.create_table( + "task_acceptances", + sa.Column("id", sa.String(length=64), primary_key=True), + sa.Column("task_id", sa.String(length=64), nullable=False, index=True), + sa.Column("delivery_id", sa.String(length=64), nullable=False, server_default=""), + sa.Column("milestone_id", sa.String(length=64), nullable=False, server_default=""), + sa.Column("checklist_json", sa.Text(), nullable=False), + sa.Column("score_json", sa.Text(), nullable=False), + sa.Column("result", sa.String(length=20), nullable=False, server_default="pass"), + sa.Column("comment", sa.String(length=1000), nullable=False, server_default=""), + sa.Column("reviewer_id", sa.String(length=64), nullable=False, server_default=""), + sa.Column("created_at", sa.String(length=40), nullable=False, server_default=""), + sa.ForeignKeyConstraint(["task_id"], ["tasks.id"], ondelete="CASCADE"), + ) + op.create_table( + "invoices", + sa.Column("id", sa.String(length=64), primary_key=True), + sa.Column("task_id", sa.String(length=64), nullable=False, index=True), + sa.Column("escrow_id", sa.String(length=64), nullable=False, server_default=""), + sa.Column("issuer_id", sa.String(length=64), nullable=False, server_default=""), + sa.Column("issuer_name", sa.String(length=200), nullable=False, server_default=""), + sa.Column("payer_name", sa.String(length=200), nullable=False, server_default=""), + sa.Column("amount", sa.Integer(), nullable=False, server_default="0"), + sa.Column("tax_rate", sa.Float(), nullable=False, server_default="0.01"), + sa.Column("invoice_type", sa.String(length=20), nullable=False, server_default="personal"), + sa.Column("status", sa.String(length=20), nullable=False, server_default="applied"), + sa.Column("invoice_no", sa.String(length=64), nullable=False, server_default=""), + sa.Column("created_at", sa.String(length=40), nullable=False, server_default=""), + sa.Column("issued_at", sa.String(length=40), nullable=False, server_default=""), + ) + + +def downgrade() -> None: + op.drop_table("invoices") + op.drop_table("task_acceptances") + op.drop_table("task_milestones") + op.drop_table("task_deliveries") + op.drop_constraint("uq_rating_task_from", "ratings", type_="unique") + op.drop_column("ratings", "tags_json") + op.drop_column("ratings", "dims_json") + op.drop_column("ratings", "rating_type") + op.drop_column("bids", "review_note") + op.drop_column("bids", "score_json") + op.drop_column("bids", "score_total") + op.drop_column("bids", "commit_json") + op.drop_column("bids", "cases_json") + op.drop_column("bids", "team_json") + op.drop_column("bids", "deliver_days") + for col in ( + "contract_status", "on_time", "settled_at", "settle_status", "reject_count", + "accepted_at", "deliver_at", "attachments_json", "milestone_json", + "invoice_req", "escrow_ratio", "pay_type", "price_type", "accept_standard", + "deliver_items", "deliver_way", "raw_materials_json", "resource_support_json", + ): + op.drop_column("tasks", col) diff --git a/app/api/routers/auth.py b/app/api/routers/auth.py index 17b67ec..9180383 100644 --- a/app/api/routers/auth.py +++ b/app/api/routers/auth.py @@ -167,6 +167,10 @@ async def _profile_for( region_id = (identity or user).get("region_id") profile = {f: user.get(f, "") for f in _PROFILE_FIELDS} profile.update(username=user.get("username", "")) + # 头像等资源字段出口统一补全为绝对直链(登录/更新资料响应可直接显示) + from ...infrastructure.oss import resolve_url + profile["avatar"] = resolve_url(user.get("avatar", "")) + profile["company_avatar"] = resolve_url(user.get("company_avatar", "")) profile["name"] = user.get("nickname") or user.get("username", "") profile["phone"] = user.get("phone", "") profile["phoneBound"] = bool(user.get("phone", "")) diff --git a/app/api/routers/invite.py b/app/api/routers/invite.py index a27114e..142f2b1 100644 --- a/app/api/routers/invite.py +++ b/app/api/routers/invite.py @@ -83,13 +83,14 @@ async def get_invitees( db: Database = Depends(get_db), ): invitees = await db.users.get_invitees(user["id"]) + from ...infrastructure.oss import resolve_url return { "total": len(invitees), "list": [ { "id": u["id"], "nickname": u.get("nickname", ""), - "avatar": u.get("avatar", ""), + "avatar": resolve_url(u.get("avatar", "")), "phone": u.get("phone", ""), "invited_at": u.get("invited_at", ""), "created_at": u.get("created_at", ""), diff --git a/app/api/routers/rbac_hall.py b/app/api/routers/rbac_hall.py index 8d3d9d6..58f3713 100644 --- a/app/api/routers/rbac_hall.py +++ b/app/api/routers/rbac_hall.py @@ -164,6 +164,17 @@ async def hall_publish_task( "bid_quota": int(body.get("bidQuota") or body.get("bid_quota") or 0), "win_quota": int(body.get("winQuota") or body.get("win_quota") or 0), "eligibility": json.dumps(body.get("eligibility") or {}, ensure_ascii=False), + # ── 任务系统 v2 发布字段(0056)── + "resource_support_json": json.dumps(body.get("resourceSupport") or {}, ensure_ascii=False), + "raw_materials_json": json.dumps(body.get("rawMaterials") or [], ensure_ascii=False), + "deliver_way": str(body.get("deliverWay") or body.get("deliver_way") or "online"), + "deliver_items": json.dumps(body.get("deliverItems") or [], ensure_ascii=False), + "accept_standard": str(body.get("acceptStandard") or body.get("accept_standard") or ""), + "price_type": str(body.get("priceType") or body.get("price_type") or "fixed"), + "pay_type": str(body.get("payType") or body.get("pay_type") or "escrow"), + "escrow_ratio": int(body.get("escrowRatio") or body.get("escrow_ratio") or 100), + "invoice_req": json.dumps(body.get("invoiceReq") or {}, ensure_ascii=False), + "milestone_json": json.dumps(body.get("milestones") or [], ensure_ascii=False), "status": status, "published_at": utcnow_iso() if status == "published" else "", "publisher_id": user.get("id"), @@ -835,7 +846,7 @@ def _ensure_task_owner(task: dict, user: dict) -> None: raise HTTPException(status_code=403, detail="仅发包方或运营端可操作") -@router.post("/tasks/{task_id}/win", summary="评标定标(发包方/运营端;bid → win,任务 → in_progress)") +@router.post("/tasks/{task_id}/win", summary="评标定标(发包方/运营端;bid → win,任务 → claimed)") async def hall_task_win(task_id: str, body: dict, request: Request, db: Database = Depends(get_db), user: dict = Depends(require_cap("opc_member"))): @@ -844,12 +855,30 @@ async def hall_task_win(task_id: str, body: dict, request: Request, if task is None: raise HTTPException(status_code=404, detail="任务不存在") _ensure_task_owner(task, user) - updated = await TaskService(db).win_bid(task_id, str(body.get("bidId", ""))) + updated = await TaskService(db).win_bid(task_id, str(body.get("bidId", "")), user) await write_audit(db, action="hall.task.win", resource="task", resource_id=task_id, user=user, request=request) return {"ok": True, "item": updated} +@router.post("/tasks/{task_id}/review-bid", summary="招标评审评分(发包方/运营端;综合得分写入标书)") +async def hall_task_review_bid(task_id: str, body: dict, request: Request, + db: Database = Depends(get_db), + user: dict = Depends(require_cap("opc_member"))): + from ...services.task_service import TaskService + task = await db.tasks.get(task_id) + if task is None: + raise HTTPException(status_code=404, detail="任务不存在") + _ensure_task_owner(task, user) + bid = await TaskService(db).review_bid( + task_id, str(body.get("bidId", "")), user, + scores=body.get("scores"), note=str(body.get("note", "")), + ) + await write_audit(db, action="hall.task.review_bid", resource="bid", resource_id=bid["id"], + user=user, request=request) + return {"ok": True, "item": bid} + + @router.post("/tasks/{task_id}/assign", summary="指派接单人(发包方/运营端;assign 模式)") async def hall_task_assign(task_id: str, body: dict, request: Request, db: Database = Depends(get_db), @@ -880,17 +909,17 @@ async def hall_task_select(task_id: str, body: dict, request: Request, return {"ok": True, "item": updated} -@router.post("/tasks/{task_id}/cancel", summary="取消任务(发包方/运营端;未接单可取消)") +@router.post("/tasks/{task_id}/cancel", summary="取消任务(发包方/运营端;published/claimed 可取消)") async def hall_task_cancel(task_id: str, request: Request, db: Database = Depends(get_db), user: dict = Depends(require_cap("opc_member"))): + from ...services.task_service import TaskService + task = await db.tasks.get(task_id) if task is None: raise HTTPException(status_code=404, detail="任务不存在") _ensure_task_owner(task, user) - if task.get("status") != "published": - raise HTTPException(status_code=400, detail="任务已被接单,不可取消") - updated = await db.tasks.set_status(task_id, "cancelled") + updated = await TaskService(db).cancel(task_id, user) await write_audit(db, action="hall.task.cancel", resource="task", resource_id=task_id, user=user, request=request) return {"ok": True, "item": updated} @@ -934,15 +963,22 @@ async def hall_task_grab_by_code( return {"ok": True, "item": updated} -@router.post("/tasks/{task_id}/bid", summary="投标(bid 模式,报价+方案)") +@router.post("/tasks/{task_id}/bid", summary="投标(bid 模式,六段标书:报价/方案/材料/工期/团队/案例/承诺)") async def hall_task_bid(task_id: str, body: dict, request: Request, db: Database = Depends(get_db), user: dict = Depends(require_cap("opc_member"))): from ...services.task_service import TaskService - bid = await TaskService(db).bid(task_id, user, - int(body.get("quote") or 0), - str(body.get("plan") or "")) + bid = await TaskService(db).bid( + task_id, user, + int(body.get("quote") or 0), + str(body.get("plan") or ""), + material=str(body.get("material") or ""), + deliver_days=int(body.get("deliverDays") or body.get("deliver_days") or 0), + team_json=json.dumps(body.get("team") or [], ensure_ascii=False), + cases_json=json.dumps(body.get("cases") or [], ensure_ascii=False), + commit_json=json.dumps(body.get("commit") or {}, ensure_ascii=False), + ) await write_audit(db, action="hall.task.bid", resource="bid", resource_id=bid["id"], user=user, request=request) return {"ok": True, "item": bid} @@ -960,37 +996,179 @@ async def hall_task_doing(task_id: str, request: Request, return {"ok": True, "item": updated} -@router.post("/tasks/{task_id}/complete", summary="完成做单") +@router.post("/tasks/{task_id}/complete", summary="完成做单(v3 语义:立即交付)") async def hall_task_complete(task_id: str, request: Request, db: Database = Depends(get_db), user: dict = Depends(require_cap("opc_member"))): from ...services.task_service import TaskService updated = await TaskService(db).complete(task_id, user) - # 信用事件:按时交付 +10(简化:complete 视为按时;逾期判定后续细化) - if updated and updated.get("status") == "completed": - from ...services.credit_engine import on_credit_event - await on_credit_event( - db, user_id=user["id"], event_code="task.complete.on_time", - ref_type="task", ref_id=task_id, reason="任务按时完成", - ) await write_audit(db, action="hall.task.complete", resource="task", resource_id=task_id, user=user, request=request) return {"ok": True, "item": updated} -@router.post("/tasks/{task_id}/deliver", summary="交付成果") -async def hall_task_deliver(task_id: str, request: Request, +@router.post("/tasks/{task_id}/deliver", summary="交付成果(版本化;附件 + 说明 + 里程碑交付)") +async def hall_task_deliver(task_id: str, body: dict, request: Request, db: Database = Depends(get_db), user: dict = Depends(require_cap("opc_member"))): from ...services.task_service import TaskService - updated = await TaskService(db).deliver(task_id, user) + updated = await TaskService(db).deliver( + task_id, user, + attachments=body.get("attachments") or [], + note=str(body.get("note") or ""), + milestone_id=str(body.get("milestoneId") or ""), + ) await write_audit(db, action="hall.task.deliver", resource="task", resource_id=task_id, user=user, request=request) return {"ok": True, "item": updated} +@router.post("/tasks/{task_id}/accept", summary="验收通过(发包方;delivered → completed)") +async def hall_task_accept(task_id: str, body: dict, request: Request, + db: Database = Depends(get_db), + user: dict = Depends(require_cap("opc_member"))): + from ...services.task_service import TaskService + + updated = await TaskService(db).accept( + task_id, user, + checklist=body.get("checklist") or [], + score=body.get("score"), + comment=str(body.get("comment") or ""), + milestone_id=str(body.get("milestoneId") or ""), + ) + await write_audit(db, action="hall.task.accept", resource="task", resource_id=task_id, + user=user, request=request) + return {"ok": True, "item": updated} + + +@router.post("/tasks/{task_id}/reject", summary="验收不通过/返工(发包方;delivered → doing,上限 3 次)") +async def hall_task_reject(task_id: str, body: dict, request: Request, + db: Database = Depends(get_db), + user: dict = Depends(require_cap("opc_member"))): + from ...services.task_service import TaskService + + updated = await TaskService(db).reject( + task_id, user, comment=str(body.get("comment") or ""), + ) + await write_audit(db, action="hall.task.reject", resource="task", resource_id=task_id, + user=user, request=request) + return {"ok": True, "item": updated} + + +@router.post("/tasks/{task_id}/withdraw", summary="接单人退出(claimed → published 释放)") +async def hall_task_withdraw(task_id: str, request: Request, + db: Database = Depends(get_db), + user: dict = Depends(require_cap("opc_member"))): + from ...services.task_service import TaskService + + updated = await TaskService(db).withdraw(task_id, user) + await write_audit(db, action="hall.task.withdraw", resource="task", resource_id=task_id, + user=user, request=request) + return {"ok": True, "item": updated} + + +@router.get("/tasks/{task_id}/deliveries", summary="任务交付历史 + 验收记录 + 里程碑(任务相关方可见)") +async def hall_task_deliveries(task_id: str, + db: Database = Depends(get_db), + user: dict = Depends(require_cap("opc_member"))): + task = await db.tasks.get(task_id) + if task is None: + raise HTTPException(status_code=404, detail="任务不存在") + caps = user.get("capabilities") or [] + if (task.get("publisher_id") or "") != user.get("id") and task.get("claimed_by") != user.get("id") \ + and not (set(caps) & {"operator", "carrier", "enterprise"}): + raise HTTPException(status_code=403, detail="仅任务相关方可查看") + return { + "deliveries": await db.task_deliveries.list_by_task(task_id), + "acceptances": await db.task_acceptances.list_by_task(task_id), + "milestones": await db.task_milestones.list_by_task(task_id), + } + + +@router.post("/tasks/{task_id}/escrow", summary="发包方自助托管(escrow 建档;全额/阶段)") +async def hall_task_escrow(task_id: str, body: dict, request: Request, + db: Database = Depends(get_db), + user: dict = Depends(require_cap("opc_member"))): + task = await db.tasks.get(task_id) + if task is None: + raise HTTPException(status_code=404, detail="任务不存在") + if (task.get("publisher_id") or "") != user.get("id"): + raise HTTPException(status_code=403, detail="仅发包方可托管资金") + if task["status"] not in ("claimed", "doing"): + raise HTTPException(status_code=400, detail="任务当前状态不可托管") + amount = int(body.get("amount") or 0) + if amount <= 0: + amount = task.get("budget_max") or task.get("budget_min") or 0 + if amount <= 0: + raise HTTPException(status_code=400, detail="请指定托管金额") + escrow = await db.escrows.create( + task_id=task_id, task_title=task.get("title") or "", amount=amount, + ) + await db.tasks.set_settle(task_id, "pending") + await write_audit(db, action="hall.task.escrow", resource="escrow", resource_id=escrow["id"], + user=user, request=request) + return {"ok": True, "escrow": escrow} + + +@router.post("/tasks/{task_id}/settle", summary="结算(验收通过后;微信分账或台账降级)") +async def hall_task_settle(task_id: str, request: Request, + db: Database = Depends(get_db), + user: dict = Depends(require_cap("opc_member"))): + from ...services.settlement_service import SettlementService + + task = await db.tasks.get(task_id) + if task is None: + raise HTTPException(status_code=404, detail="任务不存在") + if (task.get("publisher_id") or "") != user.get("id"): + raise HTTPException(status_code=403, detail="仅发包方可发起结算") + if task["status"] != "completed": + raise HTTPException(status_code=400, detail="任务未验收通过,无法结算") + if task.get("settle_status") == "released": + raise HTTPException(status_code=400, detail="任务已结算") + result = await SettlementService(db).release_escrow(task_id) + await db.tasks.set_settle(task_id, "released") + await write_audit(db, action="hall.task.settle", resource="task", resource_id=task_id, + user=user, request=request) + return {"ok": True, "result": result} + + +@router.post("/tasks/{task_id}/invoice", summary="申请开票(接单人;结算后)") +async def hall_task_invoice(task_id: str, body: dict, request: Request, + db: Database = Depends(get_db), + user: dict = Depends(require_cap("opc_member"))): + task = await db.tasks.get(task_id) + if task is None: + raise HTTPException(status_code=404, detail="任务不存在") + if task.get("claimed_by") != user.get("id"): + raise HTTPException(status_code=403, detail="仅接单人可申请开票") + if task["status"] != "completed": + raise HTTPException(status_code=400, detail="任务完成后才可开票") + amount = int(body.get("amount") or 0) or (task.get("budget_max") or 0) + if amount <= 0: + raise HTTPException(status_code=400, detail="请指定开票金额") + inv = await db.invoices.create( + task_id=task_id, + escrow_id=str(body.get("escrowId") or ""), + issuer_id=user["id"], + issuer_name=user.get("nickname") or user.get("username", ""), + payer_name=str(body.get("payerName") or task.get("publisher_name") or ""), + amount=amount, + tax_rate=float(body.get("taxRate") or 0.01), + invoice_type=str(body.get("invoiceType") or "personal"), + ) + await write_audit(db, action="hall.task.invoice", resource="invoice", resource_id=inv["id"], + user=user, request=request) + return {"ok": True, "invoice": inv} + + +@router.get("/invoices/mine", summary="我的开票记录") +async def hall_my_invoices(db: Database = Depends(get_db), + user: dict = Depends(require_cap("opc_member"))): + return {"items": await db.invoices.list_by_issuer(user["id"])} + + @router.post("/tasks/{task_id}/recommend", summary="推荐候选人(发包方/运营端)") async def hall_task_recommend(task_id: str, body: dict, request: Request, db: Database = Depends(get_db), diff --git a/app/api/routers/rbac_operator.py b/app/api/routers/rbac_operator.py index 2393940..bee7707 100644 --- a/app/api/routers/rbac_operator.py +++ b/app/api/routers/rbac_operator.py @@ -204,6 +204,20 @@ async def operator_escrow_deposit( return esc +@router.post("/tasks/scan-pending", summary="任务超时扫描(内部/定时任务入口)") +async def operator_task_scan( + db: Database = Depends(get_db), + _u: dict = Depends(require_roles("operator")), +): + """v3 定时任务:24h 未开始释放 / 72h 自动验收 / 截止过期关闭。""" + from ...services.task_service import TaskService + + stats = await TaskService(db).scan_pending() + await write_audit(db, action="hall.task.scan", resource="task", resource_id="", + detail=str(stats), user=_u) + return {"ok": True, "stats": stats} + + @router.post("/tasks/{task_id}/release", summary="运营结算(验收通过,微信分账+解冻剩余 或 台账降级)") async def operator_escrow_release( task_id: str, diff --git a/app/im/client.py b/app/im/client.py index bbfa748..5f64e44 100644 --- a/app/im/client.py +++ b/app/im/client.py @@ -57,9 +57,36 @@ async def _request(*, method: str, path: str, headers: dict[str, str] | None = N async def forward(api_path: str, *, auth_header: str, method: str = "GET", body: dict | None = None, params: dict | None = None) -> dict: - """把用户 REST 请求转发到 im-service /api/v1/*,透传 JWT。""" + """把用户 REST 请求转发到 im-service /api/v1/*,透传 JWT。 + + 出口统一把 ``*avatar*`` 字段 resolve 为可访问直链(im-service 存的是 + users 表原始对象路径 /oss/...,桌面/Web 无法直接渲染)。 + """ headers = {"Authorization": auth_header} if auth_header else {} - return await _request(method=method, path=f"/api/v1{api_path}", headers=headers, json_body=body, params=params) + data = await _request(method=method, path=f"/api/v1{api_path}", headers=headers, json_body=body, params=params) + return resolve_avatar_fields(data) + + +def resolve_avatar_fields(obj): + """递归把 dict/list 中所有键含 ``avatar`` 的字符串字段补全为 CDN 直链。 + + 覆盖 im-service 返回的:conversation.avatar / peer.avatar / members[].avatar / + message.sender_avatar / friend.from_avatar 等。非 /oss 与完整 URL 由 resolve_url 原样处理。 + """ + from ..infrastructure.oss import resolve_url + if isinstance(obj, dict): + out = {} + for k, v in obj.items(): + if isinstance(k, str) and "avatar" in k.lower() and isinstance(v, str): + out[k] = resolve_url(v) + elif isinstance(v, (dict, list)): + out[k] = resolve_avatar_fields(v) + else: + out[k] = v + return out + if isinstance(obj, list): + return [resolve_avatar_fields(i) for i in obj] + return obj # ── 内部同步(业务路由调用,fire-and-forget 不阻断主流程) ───── diff --git a/app/im/router.py b/app/im/router.py index 8832762..b034a78 100644 --- a/app/im/router.py +++ b/app/im/router.py @@ -111,9 +111,17 @@ async def im_ws_bridge(websocket: WebSocket) -> None: pass async def im_to_client() -> None: + import json as _json + from .client import resolve_avatar_fields try: while True: raw = await im_ws.recv() + try: + parsed = _json.loads(raw) + if isinstance(parsed, (dict, list)): + raw = _json.dumps(resolve_avatar_fields(parsed), ensure_ascii=False) + except (ValueError, TypeError): + pass # 非 JSON 帧(如 ping)原样透传 await websocket.send_text(raw) except (websockets.ConnectionClosed, Exception): # noqa: BLE001 pass diff --git a/app/infrastructure/models.py b/app/infrastructure/models.py index 54d340f..09efd8d 100644 --- a/app/infrastructure/models.py +++ b/app/infrastructure/models.py @@ -287,6 +287,25 @@ class Task(Base): register_quota: Mapped[int] = mapped_column(Integer, default=0) # 报名模式人数上限(0=不限)(0035) eligibility: Mapped[str] = mapped_column(Text, default="{}") # 接单资格条件(JSON 集) settle_mode: Mapped[str] = mapped_column(String, default="operator_escrow") # 资金托管方式 + # ── 任务系统 v2 扩展字段(0056)── + resource_support_json: Mapped[str] = mapped_column(Text, default="{}") # 提供的支持(算力/数据/场地/工具/对接/资金) + raw_materials_json: Mapped[str] = mapped_column(Text, default="[]") # 原始资料/素材附件 URL + deliver_way: Mapped[str] = mapped_column(String, default="online") # online/offline/mixed + deliver_items: Mapped[str] = mapped_column(Text, default="[]") # 交付内容清单(JSON 数组) + accept_standard: Mapped[str] = mapped_column(Text, default="") # 验收标准(富文本/清单) + price_type: Mapped[str] = mapped_column(String, default="fixed") # fixed/hourly/unit/revshare + pay_type: Mapped[str] = mapped_column(String, default="escrow") # escrow/stage/offline + escrow_ratio: Mapped[int] = mapped_column(Integer, default=100) # 托管比例(%) + invoice_req: Mapped[str] = mapped_column(Text, default="{}") # 发票要求(JSON) + milestone_json: Mapped[str] = mapped_column(Text, default="[]") # 里程碑配置(JSON) + attachments_json: Mapped[str] = mapped_column(Text, default="[]") # 交付物附件(JSON) + deliver_at: Mapped[str] = mapped_column(String, default="") # 交付时间 + accepted_at: Mapped[str] = mapped_column(String, default="") # 验收通过时间 + reject_count: Mapped[int] = mapped_column(Integer, default=0) # 返工次数 + settle_status: Mapped[str] = mapped_column(String, default="") # none/pending/released + settled_at: Mapped[str] = mapped_column(String, default="") # 结算时间 + on_time: Mapped[bool | None] = mapped_column(Boolean, nullable=True) # 是否按时交付(验收时判定) + contract_status: Mapped[str] = mapped_column(String, default="") # none/pending/signed created_at: Mapped[str] = mapped_column(String, default="") updated_at: Mapped[str] = mapped_column(String, default="") @@ -560,7 +579,15 @@ class Bid(Base): quote: Mapped[int] = mapped_column(Integer, default=0) plan: Mapped[str] = mapped_column(String, default="") material: Mapped[str] = mapped_column(String, default="") # 投标/报名材料(URL,0035) - status: Mapped[str] = mapped_column(String, default="submitted") # submitted/win/lost + # ── 标书 v2 扩展(0056)── + deliver_days: Mapped[int] = mapped_column(Integer, default=0) # 承诺交付周期(天) + team_json: Mapped[str] = mapped_column(Text, default="[]") # 团队构成(JSON) + cases_json: Mapped[str] = mapped_column(Text, default="[]") # 相关案例(JSON) + commit_json: Mapped[str] = mapped_column(Text, default="{}") # 服务承诺(售后/修改/响应) + score_total: Mapped[float] = mapped_column(Float, default=0) # 评审综合得分 + score_json: Mapped[str] = mapped_column(Text, default="{}") # 各维度得分快照 + review_note: Mapped[str] = mapped_column(String, default="") # 评审备注 + status: Mapped[str] = mapped_column(String, default="submitted") # submitted/win/lost/closed created_at: Mapped[str] = mapped_column(String, default="") updated_at: Mapped[str] = mapped_column(String, default="") @@ -693,9 +720,10 @@ class Dispute(Base): class Rating(Base): - """任务互评。""" + """任务互评(一人一评:task_id+from_id 唯一)。""" __tablename__ = "ratings" + __table_args__ = (UniqueConstraint("task_id", "from_id", name="uq_rating_task_from"),) id: Mapped[str] = mapped_column(String, primary_key=True) task_id: Mapped[str] = mapped_column( @@ -703,11 +731,92 @@ class Rating(Base): ) from_id: Mapped[str] = mapped_column(String, default="") to_id: Mapped[str] = mapped_column(String, default="") + rating_type: Mapped[str] = mapped_column(String, default="fulfill") # fulfill=评接单人 collab=评发包方 score: Mapped[int] = mapped_column(Integer, default=5) # 1..5 + dims_json: Mapped[str] = mapped_column(Text, default="{}") # 维度星级 + tags_json: Mapped[str] = mapped_column(Text, default="[]") # 评价标签 comment: Mapped[str] = mapped_column(String, default="") created_at: Mapped[str] = mapped_column(String, default="") +# ── 任务系统 v2:交付 / 里程碑 / 验收 / 发票(0056)──────────────────────── + +class TaskDelivery(Base): + """任务交付历史(版本化:返工后新版本)。""" + + __tablename__ = "task_deliveries" + + id: Mapped[str] = mapped_column(String, primary_key=True) + task_id: Mapped[str] = mapped_column( + ForeignKey("tasks.id", ondelete="CASCADE"), nullable=False, index=True, + ) + milestone_id: Mapped[str] = mapped_column(String, default="") # 关联里程碑(空=整体交付) + version: Mapped[int] = mapped_column(Integer, default=1) + attachments_json: Mapped[str] = mapped_column(Text, default="[]") + note: Mapped[str] = mapped_column(String, default="") + created_at: Mapped[str] = mapped_column(String, default="") + + +class TaskMilestone(Base): + """任务里程碑实例(配置来自 tasks.milestone_json)。""" + + __tablename__ = "task_milestones" + + id: Mapped[str] = mapped_column(String, primary_key=True) + task_id: Mapped[str] = mapped_column( + ForeignKey("tasks.id", ondelete="CASCADE"), nullable=False, index=True, + ) + index: Mapped[int] = mapped_column(Integer, default=1) + name: Mapped[str] = mapped_column(String, default="") + desc: Mapped[str] = mapped_column(String, default="") + days: Mapped[int] = mapped_column(Integer, default=0) + deliverables_json: Mapped[str] = mapped_column(Text, default="[]") + payment_ratio: Mapped[float] = mapped_column(Float, default=0) + status: Mapped[str] = mapped_column(String, default="pending") # pending/doing/delivered/accepted + delivered_at: Mapped[str] = mapped_column(String, default="") + accepted_at: Mapped[str] = mapped_column(String, default="") + created_at: Mapped[str] = mapped_column(String, default="") + + +class TaskAcceptance(Base): + """任务/里程碑验收记录。""" + + __tablename__ = "task_acceptances" + + id: Mapped[str] = mapped_column(String, primary_key=True) + task_id: Mapped[str] = mapped_column( + ForeignKey("tasks.id", ondelete="CASCADE"), nullable=False, index=True, + ) + delivery_id: Mapped[str] = mapped_column(String, default="") + milestone_id: Mapped[str] = mapped_column(String, default="") + checklist_json: Mapped[str] = mapped_column(Text, default="[]") # 验收清单核对结果 + score_json: Mapped[str] = mapped_column(Text, default="{}") # 量化指标打分 + result: Mapped[str] = mapped_column(String, default="pass") # pass/reject + comment: Mapped[str] = mapped_column(String, default="") + reviewer_id: Mapped[str] = mapped_column(String, default="") + created_at: Mapped[str] = mapped_column(String, default="") + + +class Invoice(Base): + """任务发票(结算后接单人开票申请 → 台账 → 电子发票)。""" + + __tablename__ = "invoices" + + id: Mapped[str] = mapped_column(String, primary_key=True) + task_id: Mapped[str] = mapped_column(String, default="", index=True) + escrow_id: Mapped[str] = mapped_column(String, default="") + issuer_id: Mapped[str] = mapped_column(String, default="") # 开票方(接单人) + issuer_name: Mapped[str] = mapped_column(String, default="") + payer_name: Mapped[str] = mapped_column(String, default="") # 抬头(发包方) + amount: Mapped[int] = mapped_column(Integer, default=0) + tax_rate: Mapped[float] = mapped_column(Float, default=0.01) + invoice_type: Mapped[str] = mapped_column(String, default="personal") # personal/vat + status: Mapped[str] = mapped_column(String, default="applied") # applied/issued/rejected + invoice_no: Mapped[str] = mapped_column(String, default="") + created_at: Mapped[str] = mapped_column(String, default="") + issued_at: Mapped[str] = mapped_column(String, default="") + + class Notification(Base): """站内通知。""" diff --git a/app/infrastructure/repositories.py b/app/infrastructure/repositories.py index ef6fc19..8db62b9 100644 --- a/app/infrastructure/repositories.py +++ b/app/infrastructure/repositories.py @@ -64,8 +64,11 @@ from .models import ( SubsidyApplication, SystemConfig, Task, + TaskAcceptance, TaskCategory, TaskClaim, + TaskDelivery, + TaskMilestone, TaskParkVisibility, TrainingEnrollment, TrainingCourse, @@ -74,6 +77,7 @@ from .models import ( TrainingCourseEnrollment, TrainingLessonProgress, TrainingCertificate, + Invoice, User, UserBadge, ) @@ -1377,6 +1381,16 @@ class TaskRepository: register_quota=fields.get("register_quota", 0), eligibility=fields.get("eligibility", "{}"), settle_mode=fields.get("settle_mode", "operator_escrow"), + resource_support_json=fields.get("resource_support_json", "{}"), + raw_materials_json=fields.get("raw_materials_json", "[]"), + deliver_way=fields.get("deliver_way", "online"), + deliver_items=fields.get("deliver_items", "[]"), + accept_standard=fields.get("accept_standard", ""), + price_type=fields.get("price_type", "fixed"), + pay_type=fields.get("pay_type", "escrow"), + escrow_ratio=fields.get("escrow_ratio", 100), + invoice_req=fields.get("invoice_req", "{}"), + milestone_json=fields.get("milestone_json", "[]"), created_at=now, updated_at=now) self.session.add(t) await self.session.commit() @@ -1395,6 +1409,9 @@ class TaskRepository: "assign_type", "assign_park_id", "assign_opc_id", "park_id", "park_released", "bid_quota", "win_quota", "register_quota", "eligibility", "settle_mode", + "resource_support_json", "raw_materials_json", "deliver_way", + "deliver_items", "accept_standard", "price_type", "pay_type", + "escrow_ratio", "invoice_req", "milestone_json", ): if key in fields: setattr(t, key, fields.get(key)) @@ -1445,6 +1462,121 @@ class TaskRepository: await self.session.commit() return self._to_dict(t) + # ── 任务系统 v2 生命周期(0056)───────────────────────────── + + async def set_delivered(self, task_id: str, note: str = "") -> dict | None: + """doing → delivered:记录交付时间与交付说明。""" + t = await self.session.get(Task, task_id) + if t is None: + return None + t.status = "delivered" + t.deliver_at = utcnow_iso() + t.updated_at = utcnow_iso() + await self.session.commit() + return self._to_dict(t) + + async def set_accepted(self, task_id: str, on_time: bool | None = None) -> dict | None: + """delivered → completed:验收通过(结算与互评入口)。""" + t = await self.session.get(Task, task_id) + if t is None: + return None + t.status = "completed" + t.accepted_at = utcnow_iso() + if on_time is not None: + t.on_time = on_time + t.updated_at = utcnow_iso() + await self.session.commit() + return self._to_dict(t) + + async def reject(self, task_id: str, reject_count: int) -> dict | None: + """delivered → doing:验收不通过返工(计数递增)。""" + t = await self.session.get(Task, task_id) + if t is None: + return None + t.status = "doing" + t.reject_count = reject_count + t.updated_at = utcnow_iso() + await self.session.commit() + return self._to_dict(t) + + async def release_claim(self, task_id: str) -> dict | None: + """claimed → published:接单人退出/超时释放,清空 claimed_by。""" + t = await self.session.get(Task, task_id) + if t is None: + return None + t.status = "published" + t.claimed_by = None + t.claimed_at = "" + t.updated_at = utcnow_iso() + await self.session.commit() + return self._to_dict(t) + + async def set_expired(self, task_id: str) -> dict | None: + """published → cancelled(expired):报名/抢单截止后无人接。""" + t = await self.session.get(Task, task_id) + if t is None: + return None + t.status = "cancelled" + t.updated_at = utcnow_iso() + await self.session.commit() + return self._to_dict(t) + + async def set_contract(self, task_id: str, status: str) -> dict | None: + t = await self.session.get(Task, task_id) + if t is None: + return None + t.contract_status = status + t.updated_at = utcnow_iso() + await self.session.commit() + return self._to_dict(t) + + async def set_settle(self, task_id: str, status: str = "released") -> dict | None: + t = await self.session.get(Task, task_id) + if t is None: + return None + t.settle_status = status + t.settled_at = utcnow_iso() + t.updated_at = utcnow_iso() + await self.session.commit() + return self._to_dict(t) + + def _to_dict(self, t: Task) -> dict: + return { + "id": t.id, "task_code": t.task_code, "title": t.title, + "category": t.category, "category_id": t.category_id, + "sub_category": t.sub_category, "description": t.description, + "tags": t.tags, "mode": t.mode, + "budget_min": t.budget_min, "budget_max": t.budget_max, + "deadline": t.deadline, "delivery_days": t.delivery_days, + "headcount": t.headcount, "exclusive": t.exclusive, + "display_priority": t.display_priority, "status": t.status, + "published_at": t.published_at, "publisher_id": t.publisher_id, + "publisher_org_id": t.publisher_org_id, "publisher_name": t.publisher_name, + "publisher_role": t.publisher_role, "visibility": t.visibility, + "c_visible": t.c_visible, "park_public": t.park_public, + "assign_type": t.assign_type, "assign_park_id": t.assign_park_id, + "assign_opc_id": t.assign_opc_id, "park_id": t.park_id, + "park_released": t.park_released, + "bid_quota": t.bid_quota, "win_quota": t.win_quota, + "register_quota": t.register_quota, + "eligibility": t.eligibility, "settle_mode": t.settle_mode, + "resource_support_json": t.resource_support_json, + "raw_materials_json": t.raw_materials_json, + "deliver_way": t.deliver_way, "deliver_items": t.deliver_items, + "accept_standard": t.accept_standard, + "price_type": t.price_type, "pay_type": t.pay_type, + "escrow_ratio": t.escrow_ratio, "invoice_req": t.invoice_req, + "milestone_json": t.milestone_json, + "attachments_json": t.attachments_json, + "deliver_at": t.deliver_at, "accepted_at": t.accepted_at, + "reject_count": t.reject_count, + "settle_status": t.settle_status, "settled_at": t.settled_at, + "on_time": t.on_time, "contract_status": t.contract_status, + "claimed_by": t.claimed_by, "claimed_at": t.claimed_at, + "doing_at": t.doing_at, "created_at": t.created_at, + "updated_at": t.updated_at, + } + class TaskClaimRepository: def __init__(self, session: AsyncSession): @@ -1498,6 +1630,192 @@ class TaskClaimRepository: await self.session.commit() return self._to_dict(c) + async def get_by_task_and_user(self, task_id: str, user_id: str) -> dict | None: + q = select(TaskClaim).where( + TaskClaim.task_id == task_id, + TaskClaim.claimer_user_id == user_id, + ).order_by(TaskClaim.created_at.desc()) + c = (await self.session.scalars(q)).first() + return self._to_dict(c) if c else None + + async def list_active_by_task(self, task_id: str) -> list[dict]: + """在接/已派/做单/已交付中的 claim(用于双写同步)。""" + q = select(TaskClaim).where( + TaskClaim.task_id == task_id, + TaskClaim.status.in_(["claimed", "assigned", "doing", "delivered"]), + ) + return [self._to_dict(c) for c in await self.session.scalars(q)] + + async def release_by_task(self, task_id: str, keep_status: str = "cancelled") -> int: + """把任务下所有 active claim 置为指定状态(退出/释放/完成时同步)。""" + q = update(TaskClaim).where( + TaskClaim.task_id == task_id, + TaskClaim.status.in_(["claimed", "assigned", "doing", "delivered"]), + ).values(status=keep_status, updated_at=utcnow_iso()) + result = await self.session.execute(q) + await self.session.commit() + return result.rowcount or 0 + + +# ── 任务系统 v2 仓库(0056):交付 / 里程碑 / 验收 / 发票 ────────────────── + +class TaskDeliveryRepository: + def __init__(self, session: AsyncSession): + self.session = session + + @staticmethod + def _to_dict(d: TaskDelivery) -> dict: + return {"id": d.id, "task_id": d.task_id, "milestone_id": d.milestone_id, + "version": d.version, "attachments_json": d.attachments_json, + "note": d.note, "created_at": d.created_at} + + async def create(self, task_id: str, version: int, attachments_json: str = "[]", + note: str = "", milestone_id: str = "") -> dict: + d = TaskDelivery(id=new_id("deli"), task_id=task_id, milestone_id=milestone_id, + version=version, attachments_json=attachments_json, + note=note, created_at=utcnow_iso()) + self.session.add(d) + await self.session.commit() + return self._to_dict(d) + + async def list_by_task(self, task_id: str) -> list[dict]: + q = select(TaskDelivery).where(TaskDelivery.task_id == task_id).order_by(TaskDelivery.version.asc()) + return [self._to_dict(d) for d in await self.session.scalars(q)] + + async def next_version(self, task_id: str) -> int: + q = select(func.max(TaskDelivery.version)).where(TaskDelivery.task_id == task_id) + v = await self.session.scalar(q) + return int(v or 0) + 1 + + +class TaskMilestoneRepository: + def __init__(self, session: AsyncSession): + self.session = session + + @staticmethod + def _to_dict(m: TaskMilestone) -> dict: + return {"id": m.id, "task_id": m.task_id, "index": m.index, "name": m.name, + "desc": m.desc, "days": m.days, "deliverables_json": m.deliverables_json, + "payment_ratio": m.payment_ratio, "status": m.status, + "delivered_at": m.delivered_at, "accepted_at": m.accepted_at, + "created_at": m.created_at} + + async def ensure_from_config(self, task_id: str, config: list) -> list[dict]: + """按任务 milestone_json 配置物化里程碑实例(幂等)。""" + existing = (await self.session.scalars( + select(TaskMilestone).where(TaskMilestone.task_id == task_id) + )).all() + if existing: + return [self._to_dict(m) for m in existing] + created = [] + for idx, item in enumerate(config or [], start=1): + m = TaskMilestone( + id=new_id("mile"), task_id=task_id, index=idx, + name=item.get("name", f"里程碑{idx}"), + desc=item.get("desc", ""), + days=int(item.get("days", 0)), + deliverables_json=json.dumps(item.get("deliverables", []), ensure_ascii=False), + payment_ratio=float(item.get("payment_ratio", 0)), + status="pending", created_at=utcnow_iso(), + ) + self.session.add(m) + created.append(m) + await self.session.commit() + return [self._to_dict(m) for m in created] + + async def list_by_task(self, task_id: str) -> list[dict]: + q = select(TaskMilestone).where(TaskMilestone.task_id == task_id).order_by(TaskMilestone.index.asc()) + return [self._to_dict(m) for m in await self.session.scalars(q)] + + async def set_status(self, milestone_id: str, status: str, + delivered_at: str = "", accepted_at: str = "") -> dict | None: + m = await self.session.get(TaskMilestone, milestone_id) + if m is None: + return None + m.status = status + if delivered_at: + m.delivered_at = delivered_at + if accepted_at: + m.accepted_at = accepted_at + await self.session.commit() + return self._to_dict(m) + + +class TaskAcceptanceRepository: + def __init__(self, session: AsyncSession): + self.session = session + + @staticmethod + def _to_dict(a: TaskAcceptance) -> dict: + return {"id": a.id, "task_id": a.task_id, "delivery_id": a.delivery_id, + "milestone_id": a.milestone_id, "checklist_json": a.checklist_json, + "score_json": a.score_json, "result": a.result, "comment": a.comment, + "reviewer_id": a.reviewer_id, "created_at": a.created_at} + + async def create(self, task_id: str, delivery_id: str, milestone_id: str, + checklist_json: str, score_json: str, result: str, + comment: str, reviewer_id: str) -> dict: + a = TaskAcceptance(id=new_id("acc"), task_id=task_id, delivery_id=delivery_id, + milestone_id=milestone_id, checklist_json=checklist_json, + score_json=score_json, result=result, comment=comment, + reviewer_id=reviewer_id, created_at=utcnow_iso()) + self.session.add(a) + await self.session.commit() + return self._to_dict(a) + + async def list_by_task(self, task_id: str) -> list[dict]: + q = select(TaskAcceptance).where(TaskAcceptance.task_id == task_id).order_by(TaskAcceptance.created_at.desc()) + return [self._to_dict(a) for a in await self.session.scalars(q)] + + +class InvoiceRepository: + def __init__(self, session: AsyncSession): + self.session = session + + @staticmethod + def _to_dict(v: Invoice) -> dict: + return {"id": v.id, "task_id": v.task_id, "escrow_id": v.escrow_id, + "issuer_id": v.issuer_id, "issuer_name": v.issuer_name, + "payer_name": v.payer_name, "amount": v.amount, + "tax_rate": v.tax_rate, "invoice_type": v.invoice_type, + "status": v.status, "invoice_no": v.invoice_no, + "created_at": v.created_at, "issued_at": v.issued_at} + + async def create(self, task_id: str, escrow_id: str, issuer_id: str, issuer_name: str, + payer_name: str, amount: int, tax_rate: float = 0.01, + invoice_type: str = "personal") -> dict: + v = Invoice(id=new_id("inv"), task_id=task_id, escrow_id=escrow_id, + issuer_id=issuer_id, issuer_name=issuer_name, payer_name=payer_name, + amount=amount, tax_rate=tax_rate, invoice_type=invoice_type, + status="applied", created_at=utcnow_iso()) + self.session.add(v) + await self.session.commit() + return self._to_dict(v) + + async def list_by_issuer(self, issuer_id: str) -> list[dict]: + q = select(Invoice).where(Invoice.issuer_id == issuer_id).order_by(Invoice.created_at.desc()) + return [self._to_dict(v) for v in await self.session.scalars(q)] + + async def list_all(self) -> list[dict]: + q = select(Invoice).order_by(Invoice.created_at.desc()) + return [self._to_dict(v) for v in await self.session.scalars(q)] + + async def get(self, invoice_id: str) -> dict | None: + v = await self.session.get(Invoice, invoice_id) + return self._to_dict(v) if v else None + + async def set_status(self, invoice_id: str, status: str, invoice_no: str = "") -> dict | None: + v = await self.session.get(Invoice, invoice_id) + if v is None: + return None + v.status = status + if invoice_no: + v.invoice_no = invoice_no + if status == "issued": + v.issued_at = utcnow_iso() + await self.session.commit() + return self._to_dict(v) + class TaskCategoryRepository: def __init__(self, session: AsyncSession): @@ -2212,10 +2530,15 @@ class BidRepository: row = await self.session.get(Bid, bid_id) return self._to_dict(row) if row else None - async def create(self, task_id: str, opc_id: str, opc_name: str, quote: int, plan: str = "") -> dict: + async def create(self, task_id: str, opc_id: str, opc_name: str, quote: int, + plan: str = "", material: str = "", + deliver_days: int = 0, team_json: str = "[]", + cases_json: str = "[]", commit_json: str = "{}") -> dict: row = Bid( id=new_id("bid"), task_id=task_id, opc_id=opc_id, opc_name=opc_name, - quote=quote, plan=plan, status="submitted", + quote=quote, plan=plan, material=material, status="submitted", + deliver_days=deliver_days, team_json=team_json, cases_json=cases_json, + commit_json=commit_json, created_at=utcnow_iso(), updated_at=utcnow_iso(), ) self.session.add(row) @@ -2231,10 +2554,26 @@ class BidRepository: await self.session.commit() return self._to_dict(row) + async def set_review(self, bid_id: str, score_total: float, score_json: str, + review_note: str) -> dict | None: + row = await self.session.get(Bid, bid_id) + if row is None: + return None + row.score_total = score_total + row.score_json = score_json + row.review_note = review_note + row.updated_at = utcnow_iso() + await self.session.commit() + return self._to_dict(row) + @staticmethod def _to_dict(b: Bid) -> dict: return {"id": b.id, "task_id": b.task_id, "opc_id": b.opc_id, "opc_name": b.opc_name, - "quote": b.quote, "plan": b.plan, "status": b.status, + "quote": b.quote, "plan": b.plan, "material": b.material, "status": b.status, + "deliver_days": b.deliver_days, "team_json": b.team_json, + "cases_json": b.cases_json, "commit_json": b.commit_json, + "score_total": b.score_total, "score_json": b.score_json, + "review_note": b.review_note, "created_at": b.created_at, "updated_at": b.updated_at} @@ -2469,13 +2808,22 @@ class RatingRepository: return [{"id": r.id, "task_id": r.task_id, "from_id": r.from_id, "score": r.score, "comment": r.comment, "created_at": r.created_at} for r in rows] - async def create(self, task_id: str, from_id: str, to_id: str, score: int, comment: str = "") -> dict: + async def create(self, task_id: str, from_id: str, to_id: str, score: int, comment: str = "", + rating_type: str = "fulfill", dims_json: str = "{}", + tags_json: str = "[]") -> dict: row = Rating(id=new_id("rate"), task_id=task_id, from_id=from_id, to_id=to_id, - score=max(1, min(5, score)), comment=comment, created_at=utcnow_iso()) + rating_type=rating_type, score=max(1, min(5, score)), + dims_json=dims_json, tags_json=tags_json, comment=comment, + created_at=utcnow_iso()) self.session.add(row) await self.session.commit() return {"id": row.id, "score": row.score, "comment": row.comment} + async def has_rated(self, task_id: str, from_id: str) -> bool: + q = select(func.count(Rating.id)).where( + Rating.task_id == task_id, Rating.from_id == from_id) + return int((await self.session.scalar(q)) or 0) > 0 + class NotificationRepository: def __init__(self, session: AsyncSession): @@ -2638,6 +2986,10 @@ class Database: self.tasks = TaskRepository(self.session) self.task_claims = TaskClaimRepository(self.session) self.task_categories = TaskCategoryRepository(self.session) + self.task_deliveries = TaskDeliveryRepository(self.session) + self.task_milestones = TaskMilestoneRepository(self.session) + self.task_acceptances = TaskAcceptanceRepository(self.session) + self.invoices = InvoiceRepository(self.session) self.providers = ProviderRepository(self.session) self.content = ContentRepository(self.session) self.config = ConfigRepository(self.session) diff --git a/app/market/routers.py b/app/market/routers.py index 776b095..c090115 100644 --- a/app/market/routers.py +++ b/app/market/routers.py @@ -319,7 +319,12 @@ async def admin_create_item( raise HTTPException(status_code=400, detail="item_type 仅支持 skill/plugin/app") if not await categories.valid_category(db, body.item_type, body.category): raise HTTPException(status_code=400, detail="分类不属于该类型,请先维护分类字典") - slug = _normalize_slug(body.slug, body.name) + # plugin/app 标识不允许手工设置:由 zip 上传时解析(覆盖此占位值); + # 此处不强制组织前缀,避免产生与插件内部 id 不一致的误导性标识。 + if body.item_type == "skill": + slug = _normalize_slug(body.slug, body.name) + else: + slug = (body.slug or "").strip().strip("/") try: item = await service.create_item( db, item_type=body.item_type, slug=slug, name=body.name, @@ -357,7 +362,11 @@ async def admin_update_item( raise HTTPException(status_code=404, detail="商品不存在") if not await categories.valid_category(db, existing["item_type"], body.category): raise HTTPException(status_code=400, detail="分类不属于该类型,请先维护分类字典") - slug = _normalize_slug(body.slug, body.name, existing["slug"]) + # plugin/app 标识由 zip 解析、不允许手工设置:编辑时保持已解析的标识不变(仅 skill 可改)。 + if existing["item_type"] == "skill": + slug = _normalize_slug(body.slug, body.name, existing["slug"]) + else: + slug = existing["slug"] try: item = await service.update_item( db, item_id, slug=slug, name=body.name, description=body.description, version=body.version, @@ -625,14 +634,20 @@ async def admin_upload_bundle( await db.session.commit() else: # plugin / app:zip 落 OSS + # 标识不允许手工设置:必须从 zip 内 plugin.json 的 id 解析,覆盖创建时的占位 slug + # (避免服务端组织前缀与插件内部 id 不一致,导致桌面端无法识别已安装插件/应用)。 + manifest = _parse_manifest(data) + manifest_id = "" + if manifest: + manifest_id = str(manifest.get("id") or "").strip().strip("/") + item.bundle_json = json.dumps({"manifest": manifest}, ensure_ascii=False) + if manifest_id: + item.slug = manifest_id slug = (item.slug or item.id).replace("/", "-") key = f"market/{item.item_type}/{slug}-{item.version}.zip" path = await oss.upload(key, data, content_type="application/zip") item.bundle_url = path item.bundle_sha256 = sha256 - manifest = _parse_manifest(data) - if manifest: - item.bundle_json = json.dumps({"manifest": manifest}, ensure_ascii=False) await db.session.commit() await _audit(db, user, "market.upload", item_id, f"{len(data)}B sha256={sha256[:12]}") return {"ok": True, "item_id": item_id, "bundle_sha256": sha256, "bundle_url": item.bundle_url} diff --git a/app/park/routers.py b/app/park/routers.py index 44fa004..db7f942 100644 --- a/app/park/routers.py +++ b/app/park/routers.py @@ -1524,10 +1524,11 @@ async def park_users(keyword: str = "", company_id: str = "", db=Depends(_carrie ).order_by(ParkMember.created_at.desc()) )).all() items = [] + from ..infrastructure.oss import resolve_url as _resolve_avatar for u, pm in rows: d = dict(u.__dict__) if False else { "id": u.id, "username": u.username, "nickname": u.nickname or "", - "phone": u.phone or "", "avatar": u.avatar or "", "role": u.role or "", + "phone": u.phone or "", "avatar": _resolve_avatar(u.avatar or ""), "role": u.role or "", "member_type": pm.member_type, "company": u.company or "", "park_company_id": u.park_company_id or "", "certification_status": u.certification_status or "uncertified", diff --git a/app/park/tenants.py b/app/park/tenants.py index 5fe4780..82e6e8e 100644 --- a/app/park/tenants.py +++ b/app/park/tenants.py @@ -535,12 +535,13 @@ async def park_members(tenant_id: str, status: str = "active") -> list[dict]: *[ParkMember.status == status] if status else [], ).order_by(ParkMember.created_at) )).all() + from ..infrastructure.oss import resolve_url as _resolve_avatar return [{ "id": u.id, "username": u.username, "nickname": u.nickname or "", "phone": u.phone or "", "role": u.role or "", "member_type": pm.member_type, "membership_status": pm.status, "certification_status": u.certification_status or "uncertified", - "company": u.company or "", "avatar": u.avatar or "", + "company": u.company or "", "avatar": _resolve_avatar(u.avatar or ""), "gender": u.gender or "", "created_at": u.created_at or "", } for u, pm in rows] diff --git a/app/services/avatar_pool.py b/app/services/avatar_pool.py new file mode 100644 index 0000000..2eead04 --- /dev/null +++ b/app/services/avatar_pool.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +"""默认头像池:新用户未提供头像时,从头像池随机分配一个默认头像。 + +头像池 = 对象存储桶 avatar/images/images/ 目录下的 1321 张头像 +(对象 key 清单见 ``serverdata/avatar_pool.json``,由 code/images/images_data/images +本地目录与桶内对象一一对应生成)。 + +数据库只存对象相对路径 ``/oss/avatar/images/images/``, +读取时经 ``resolve_url`` 补全为 CDN 直链(见 app/infrastructure/oss.py)。 + +语义约定: +- 新用户创建时若未显式提供头像(avatar 为空)→ 随机分配一个默认头像; +- 用户一旦自行更换头像(上传真实头像)→ 走 update 流程,不再回退/重随机。 +""" +from __future__ import annotations + +import json +import random +from pathlib import Path +from typing import List + +from .. import config + +_POOL_CACHE: List[str] | None = None +_POOL_PATH: Path = config.SERVERDATA_DIR / "avatar_pool.json" + + +def _load_pool() -> List[str]: + """读取头像池对象 key 清单(内存缓存)。文件缺失/损坏时返回空池。""" + global _POOL_CACHE + if _POOL_CACHE is not None: + return _POOL_CACHE + try: + raw = json.loads(_POOL_PATH.read_text(encoding="utf-8")) + keys = [ + str(k).strip() + for k in raw + if isinstance(k, str) and k.strip() + ] + except Exception: # noqa: BLE001 + keys = [] + _POOL_CACHE = keys + return keys + + +def random_default_avatar() -> str: + """从默认头像池随机返回一个对象路径(``/oss/...``);池为空时返回空串。""" + pool = _load_pool() + if not pool: + return "" + return "/oss/" + random.choice(pool) diff --git a/app/services/invite_service.py b/app/services/invite_service.py index 017f87a..4beda6c 100644 --- a/app/services/invite_service.py +++ b/app/services/invite_service.py @@ -139,6 +139,7 @@ async def get_invite_stats(db: Database, user_id: str) -> dict: scan_count = await db.invite_records.count_by(inviter_id=user_id, action="scan") # 注册成功数 register_count = len(invitees) + from ..infrastructure.oss import resolve_url return { "invite_code": code, "share_count": share_count, @@ -148,7 +149,7 @@ async def get_invite_stats(db: Database, user_id: str) -> dict: { "id": u["id"], "nickname": u.get("nickname", ""), - "avatar": u.get("avatar", ""), + "avatar": resolve_url(u.get("avatar", "")), "invited_at": u.get("invited_at", ""), } for u in invitees diff --git a/app/services/task_service.py b/app/services/task_service.py index 9080966..45f1bbe 100644 --- a/app/services/task_service.py +++ b/app/services/task_service.py @@ -1,21 +1,53 @@ # -*- coding: utf-8 -*- -"""业务层 · 任务状态机(抢单 / 投标 / 交付 / 中标 / 验收)。 +"""业务层 · 任务状态机 v3(抢单 / 报名 / 招标 / 指派 / 推荐 → 统一履约链)。 -业务层只处理业务逻辑;不感知 HTTP 之外的框架细节。依赖基础设施层 -Repository(经 Database 门面),不反向依赖接口层。 +v3 核心变更(0056): +1. claim 双写:所有匹配方式(grab/register_select/assign/select_recommend/win_bid) + 统一落 TaskClaim + 翻转 tasks 状态;履约动作(start/deliver/accept)同步 claim。 +2. 归属校验:start_doing / deliver / accept / withdraw 必须校验操作者为接单人。 +3. 统一状态机:claimed → doing → delivered → accepted(completed);旧 in_progress 语义并入 doing。 +4. 验收链路:delivered → accepted(completed) / reject 返工(doing),返工上限 3 次。 +5. 退出与超时:claimed 可退出释放;扫描任务处理 24h 未开始释放 / 72h 自动验收 / 截止过期。 +6. 资格预检扩展:信用等级下限、认证、发票能力、企业专属。 """ from __future__ import annotations import asyncio import json +from datetime import datetime, timedelta, timezone from fastapi import HTTPException from ..infrastructure.repositories import Database +MAX_REJECT = 3 + + +def _utcnow() -> str: + return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" + + +def _parse_ts(ts: str) -> datetime | None: + if not ts: + return None + try: + d = datetime.fromisoformat(ts.replace("Z", "+00:00")) + if d.tzinfo is None: + d = d.replace(tzinfo=timezone.utc) + return d + except Exception: # noqa: BLE001 + return None + + +def _hours_since(ts: str) -> float | None: + t = _parse_ts(ts) + if t is None: + return None + return (datetime.now(timezone.utc) - t).total_seconds() / 3600 + def _sync_task_group(task_id: str) -> None: - """任务群后台同步(fire-and-forget,不阻断主流程;IM 服务不可用时静默降级)。""" + """任务群后台同步(fire-and-forget,IM 服务不可用时静默降级)。""" try: from ..im import client as im_client @@ -24,46 +56,83 @@ def _sync_task_group(task_id: str) -> None: pass +async def _credit_event(db: Database, user_id: str, code: str, reason: str, + ref_type: str = "", ref_id: str = "") -> None: + """信用事件统一入口(引擎不可用时静默降级,不阻断业务)。""" + try: + from .credit_engine import on_credit_event + + await on_credit_event(db, code, { + "user_id": user_id, "reason": reason, + "ref_type": ref_type, "ref_id": ref_id, + }) + except Exception: # noqa: BLE001 + pass + + +async def _notify(db: Database, user_id: str, ntype: str, title: str, content: str) -> None: + try: + await db.notifications.create(user_id, ntype, title, content) + except Exception: # noqa: BLE001 + pass + + class TaskService: - """统一任务状态流转:grab/bid/deliver/win/review。""" + """统一任务状态流转 v3。""" def __init__(self, db: Database): self.db = db - async def can_accept(self, task: dict | None, actor: dict | None) -> dict: - """接单资格判定:可见性(仅被指派) + 归属园区(专属) + eligibility 条件集。 + # ── 资格预检引擎 v2 ───────────────────────────────────────── - 返回 {ok, reasons[]};reasons 为空即通过。 + async def _actor_profile(self, actor: dict) -> dict: + uid = actor.get("id") + if not uid: + return {} + full = (await self.db.users.get_by_id(uid)) or {} + credit = await self.db.credit_scores.get(uid) + return { + **full, + "credit_level": (credit or {}).get("level", "L1"), + "credit_score": (credit or {}).get("total_score", 0), + } + + async def can_accept(self, task: dict | None, actor: dict | None) -> dict: + """接单资格判定 v2:可见性 + 园区 + eligibility 扩展条件集。 + + eligibility 硬性条件(一票否决):opc_certified / region / gender / + years_min / field / skill / case_req / credit_level_min / certified / + invoice_ok / enterprise_only / deposit。 + 返回 {ok, reasons[], soft[]};reasons 为空即通过。 """ if not task: - return {"ok": False, "reasons": ["任务不存在"]} + return {"ok": False, "reasons": ["任务不存在"], "soft": []} reasons: list[str] = [] + soft: list[str] = [] actor = actor or {} - full = ((await self.db.users.get_by_id(actor["id"])) or {}) if actor.get("id") else {} + full = await self._actor_profile(actor) user_park = full.get("park_company_id") or full.get("park_id") or actor.get("park_id") uid = actor.get("id") - # 仅被指派可见 → 只有被指派对象(OPC 或园区)可接 if uid and task.get("visibility") == "assigned_only": target = task.get("assign_opc_id") or task.get("assign_park_id") if target and target != uid and target != user_park: reasons.append("仅被指派方可接") - # 园区专属任务 → 仅本园 if task.get("park_id") and user_park != task.get("park_id"): reasons.append("任务为本园区专属") - # 指派给园区但尚未发单 → 需园区端先操作 if task.get("assign_type") == "park" and not task.get("park_released") and task.get("assign_park_id") != user_park: reasons.append("由园区分派") - # eligibility 条件集 try: cond = json.loads(task.get("eligibility") or "{}") - except Exception: + except Exception: # noqa: BLE001 cond = {} if not isinstance(cond, dict): cond = {} + + # 硬性一票否决 if cond.get("opc_certified") and not full.get("opc_certified") and not full.get("opc_cert"): reasons.append("需 OPC 认证") if cond.get("region") and cond["region"] != "any" and (full.get("region_id") or "") != cond["region"]: @@ -78,26 +147,57 @@ class TaskService: reasons.append("专长不符") if cond.get("case_req") and (int(full.get("case_count") or 0) < int(cond["case_req"])): reasons.append(f"需 {cond['case_req']} 例服务案例") + if cond.get("credit_level_min"): + required = str(cond["credit_level_min"]).upper().strip() + level = (full.get("credit_level") or "L1").upper().strip() + order = {"L1": 1, "L2": 2, "L3": 3, "L4": 4, "L5": 5, "L6": 6, "L7": 7, "L8": 8} + if order.get(level, 1) < order.get(required, 8): + reasons.append(f"需信用等级 {required} 及以上") + if cond.get("certified") and not full.get("certifications"): + reasons.append("需完成认证") + if cond.get("invoice_ok") and not full.get("can_invoice"): + reasons.append("需具备开票能力") + if cond.get("enterprise_only") and (full.get("account_type") or "") not in ("enterprise", "company"): + reasons.append("仅企业用户可接") + if cond.get("deposit") and int(cond["deposit"] or 0) > 0: + reasons.append("需缴纳接单保证金") + + # 软性提示(不阻断) + if cond.get("price_range") and isinstance(cond["price_range"], dict): + pr = cond["price_range"] + low = int(pr.get("min") or 0) + if low > 0 and (task.get("budget_max") or 0) < low: + soft.append("预算低于习惯报价区间") + if full.get("credit_score") is not None and (full.get("credit_score") or 0) < 50: + soft.append("信用分较低,接单竞争力弱") reasons = [r for r in reasons if r] - return {"ok": not reasons, "reasons": reasons} + return {"ok": not reasons, "reasons": reasons, "soft": soft} + + # ── 统一匹配落单(claim 双写)──────────────────────────────── + + async def _match(self, task: dict, actor: dict, taker_id: str, taker_name: str, + source: str, claim_status: str = "claimed") -> dict: + """匹配成功统一动作:任务 → claimed + TaskClaim 流水 + IM 同步 + 信用事件。""" + updated = await self.db.tasks.claim(task["id"], taker_id) + await self.db.task_claims.create( + task["id"], taker_id, taker_name, source=source, status=claim_status, + ) + _sync_task_group(task["id"]) + await _credit_event(self.db, taker_id, "task_match", "任务匹配成功", + ref_type="task", ref_id=task["id"]) + return updated async def grab(self, task_id: str, actor: dict) -> dict: - """抢单(兼容旧入口):published + grab → claimed,记录接单流水。""" return await self.claim_by_id(task_id, actor, source="grab") async def claim(self, task_code: str, actor: dict, source: str = "scan") -> dict: - """扫码/按短码接单:解析 task_code 后领单。""" task = await self.db.tasks.get_by_code(task_code) if task is None: raise HTTPException(status_code=404, detail="任务不存在") return await self.claim_by_id(task["id"], actor, source=source) async def claim_by_id(self, task_id: str, actor: dict, source: str) -> dict: - """published + grab 模式 → claimed(claimed_by/claimed_at + TaskClaim 流水)。 - - 校验接单限制:独占(exclusive) 仅一人可接;headcount>0 时达上限拒绝。 - """ task = await self.db.tasks.get(task_id) if task is None: raise HTTPException(status_code=404, detail="任务不存在") @@ -105,6 +205,9 @@ class TaskService: raise HTTPException(status_code=400, detail="任务不可接单") if task["mode"] != "grab": raise HTTPException(status_code=400, detail="任务不支持扫码接单") + existing = await self.db.task_claims.get_by_task_and_user(task_id, actor["id"]) + if existing and existing.get("status") not in ("withdrawn", "cancelled"): + raise HTTPException(status_code=400, detail="你已接取该任务") active = await self.db.task_claims.count_active_by_task(task_id) if task.get("exclusive") and active >= 1: raise HTTPException(status_code=400, detail="该任务为独占,已被接单") @@ -113,16 +216,11 @@ class TaskService: acc = await self.can_accept(task, actor) if not acc["ok"]: raise HTTPException(status_code=403, detail="不满足接单条件:" + ";".join(acc["reasons"])) - updated = await self.db.tasks.claim(task_id, actor["id"]) - await self.db.task_claims.create( - task_id, actor["id"], - actor.get("nickname") or actor.get("username", ""), source, - ) - _sync_task_group(task_id) - return updated + return await self._match(task, actor, actor["id"], + actor.get("nickname") or actor.get("username", ""), + source) async def assign(self, task_id: str, taker_user_id: str, actor: dict) -> dict: - """指派:发包方直接指派某人 → claimed(claimed_by=taker)。仅 published + assign 模式。""" task = await self.db.tasks.get(task_id) if task is None: raise HTTPException(status_code=404, detail="任务不存在") @@ -132,15 +230,10 @@ class TaskService: raise HTTPException(status_code=400, detail="任务不支持指派") taker = await self.db.users.get_by_id(taker_user_id) taker_name = (taker or {}).get("nickname") or (taker or {}).get("username") or taker_user_id - updated = await self.db.tasks.claim(task_id, taker_user_id) - await self.db.task_claims.create( - task_id, taker_user_id, taker_name, source="assign", status="assigned", - ) - _sync_task_group(task_id) - return updated + return await self._match(task, actor, taker_user_id, taker_name, + source="assign", claim_status="assigned") async def recommend(self, task_id: str, candidates: list[str], actor: dict) -> list[dict]: - """推荐:发包方/系统推荐候选人,逐个写 TaskClaim(status=recommended)。仅 published + recommend。""" task = await self.db.tasks.get(task_id) if task is None: raise HTTPException(status_code=404, detail="任务不存在") @@ -159,7 +252,6 @@ class TaskService: return out async def select_recommend(self, task_id: str, taker_user_id: str, actor: dict) -> dict: - """选定推荐人:published → claimed(claimed_by=所选);该推荐 assigned、其余 withdrawn。""" task = await self.db.tasks.get(task_id) if task is None: raise HTTPException(status_code=404, detail="任务不存在") @@ -168,67 +260,19 @@ class TaskService: taker = await self.db.users.get_by_id(taker_user_id) taker_name = (taker or {}).get("nickname") or (taker or {}).get("username") or taker_user_id for rec in await self.db.task_claims.list_by_task(task_id): - target = "assigned" if rec["claimer_user_id"] == taker_user_id else "withdrawn" - await self.db.task_claims.set_status(rec["id"], target) - updated = await self.db.tasks.claim(task_id, taker_user_id) - if not any(t["claimer_user_id"] == taker_user_id - for t in await self.db.task_claims.list_by_task(task_id)): - await self.db.task_claims.create( - task_id, taker_user_id, taker_name, source="recommend", status="assigned", - ) - _sync_task_group(task_id) - return updated - - async def start_doing(self, task_id: str, actor: dict) -> dict: - """claimed → doing。""" - task = await self.db.tasks.get(task_id) - if task is None: - raise HTTPException(status_code=404, detail="任务不存在") - if task["status"] != "claimed": - raise HTTPException(status_code=400, detail="任务未接单,无法开始") - return await self.db.tasks.set_doing(task_id) - - async def complete(self, task_id: str, actor: dict) -> dict: - """doing → completed。""" - task = await self.db.tasks.get(task_id) - if task is None: - raise HTTPException(status_code=404, detail="任务不存在") - if task["status"] != "doing": - raise HTTPException(status_code=400, detail="任务未在进行中,无法完成") - return await self.db.tasks.set_status(task_id, "completed") - - async def bid(self, task_id: str, actor: dict, quote: int, plan: str) -> dict: - """投标:仅 published + bid 模式可投;校验报名人数上限 + 接单条件。""" - task = await self.db.tasks.get(task_id) - if task is None or task["status"] != "published" or task["mode"] != "bid": - raise HTTPException(status_code=400, detail="任务不可投标") - acc = await self.can_accept(task, actor) - if not acc["ok"]: - raise HTTPException(status_code=403, detail="不符合报名条件:" + ";".join(acc["reasons"])) - if task.get("bid_quota"): - existing = await self.db.bids.list_for_task(task_id) - if len(existing) >= task["bid_quota"]: - raise HTTPException(status_code=400, detail="该竞标报名人数已达上限") - for b in await self.db.bids.list_for_task(task_id): - if b.get("opc_id") == actor["id"]: - raise HTTPException(status_code=400, detail="你已报名该竞标") - return await self.db.bids.create( - task_id, actor["id"], actor.get("nickname") or actor["username"], - quote, plan, - ) + if rec.get("claimer_user_id") == taker_user_id: + await self.db.task_claims.set_status(rec["id"], "assigned") + elif rec.get("status") == "recommended": + await self.db.task_claims.set_status(rec["id"], "withdrawn") + return await self._match(task, actor, taker_user_id, taker_name, + source="recommend", claim_status="assigned") async def register(self, task_id: str, actor: dict) -> dict: - """报名(大厅 register 模式,0035):报名即占位、可多人、无报价。 - - 与 bid 的区别——不报价、不评标,先到先得;register_quota>0 时达上限即拒绝。 - 资格判定与 grab 共用 can_accept;落 TaskClaim(status=registered, source=register)。 - 任务保持 published(多报名不翻转任务状态,由发包方/运营端选定后转 claimed)。 - """ task = await self.db.tasks.get(task_id) if task is None or task["status"] != "published" or task["mode"] != "register": raise HTTPException(status_code=400, detail="任务不可报名") for c in await self.db.task_claims.list_by_task(task_id): - if c.get("claimer_user_id") == actor["id"] and c.get("status") not in ("withdrawn",): + if c.get("claimer_user_id") == actor["id"] and c.get("status") not in ("withdrawn", "cancelled"): raise HTTPException(status_code=400, detail="你已报名该任务") quota = task.get("register_quota") or 0 if quota > 0: @@ -245,48 +289,323 @@ class TaskService: ) async def register_select(self, task_id: str, claim_id: str, actor: dict) -> dict: - """报名模式选定:所选 registered → assigned(任务 → claimed),其余 → withdrawn。""" task = await self.db.tasks.get(task_id) if task is None: raise HTTPException(status_code=404, detail="任务不存在") for c in await self.db.task_claims.list_by_task(task_id): - target = "assigned" if c["id"] == claim_id else "withdrawn" if c.get("claim_source") == "register" and c.get("status") == "registered": + target = "assigned" if c["id"] == claim_id else "withdrawn" await self.db.task_claims.set_status(c["id"], target) chosen = await self.db.task_claims.get(claim_id) if chosen is None: raise HTTPException(status_code=404, detail="报名记录不存在") - result = await self.db.tasks.claim(task_id, chosen.get("claimer_user_id") or "") - _sync_task_group(task_id) - return result + return await self._match(task, actor, chosen.get("claimer_user_id") or "", + chosen.get("claimer_name") or "", + source="register", claim_status="assigned") - async def deliver(self, task_id: str, actor: dict) -> dict: - """交付:任务存在且处于进行中(in_progress)才可交付 → delivered。 + async def bid(self, task_id: str, actor: dict, quote: int, plan: str, + material: str = "", deliver_days: int = 0, + team_json: str = "[]", cases_json: str = "[]", + commit_json: str = "{}") -> dict: + """投标 v2:六段标书(报价/方案/材料/工期/团队/案例/承诺)。""" + task = await self.db.tasks.get(task_id) + if task is None or task["status"] != "published" or task["mode"] != "bid": + raise HTTPException(status_code=400, detail="任务不可投标") + acc = await self.can_accept(task, actor) + if not acc["ok"]: + raise HTTPException(status_code=403, detail="不符合报名条件:" + ";".join(acc["reasons"])) + if task.get("bid_quota"): + existing = await self.db.bids.list_for_task(task_id) + if len(existing) >= task["bid_quota"]: + raise HTTPException(status_code=400, detail="该竞标报名人数已达上限") + for b in await self.db.bids.list_for_task(task_id): + if b.get("opc_id") == actor["id"]: + raise HTTPException(status_code=400, detail="你已报名该竞标") + return await self.db.bids.create( + task_id, actor["id"], actor.get("nickname") or actor["username"], + quote, plan, material, deliver_days, team_json, cases_json, commit_json, + ) - 补齐原实现「无任何状态预检」的缺陷,防随意交付。 + async def review_bid(self, task_id: str, bid_id: str, actor: dict, + scores: dict | None = None, note: str = "") -> dict: + """评审评分(招标):综合得分 = 报价30% + 方案35% + 工期10% + 团队10% + 信用15%。 + + scores 可选各维度 0-100;缺省维度从标书推导(报价按预算区间、工期按 delivery_days)。 """ task = await self.db.tasks.get(task_id) if task is None: - raise HTTPException(status_code=404, detail="Task not found") - if task["status"] != "in_progress": + raise HTTPException(status_code=404, detail="任务不存在") + bid = await self.db.bids.get(bid_id) + if bid is None or bid["task_id"] != task_id: + raise HTTPException(status_code=404, detail="竞标不存在") + scores = scores or {} + quote_score = float(scores.get("quote", 0)) + plan_score = float(scores.get("plan", 0)) + days_score = float(scores.get("days", 0)) + team_score = float(scores.get("team", 0)) + credit_score = float(scores.get("credit", 0)) + # 缺省推导 + if quote_score <= 0: + q = bid.get("quote") or 0 + bmax = task.get("budget_max") or 0 + bmin = task.get("budget_min") or 0 + if bmax > bmin > 0: + quote_score = max(0, min(100, round(100 * (1 - (q - bmin) / (bmax - bmin))))) + elif bmax > 0: + quote_score = max(0, min(100, round(100 * (1 - q / bmax)))) + else: + quote_score = 70 + if plan_score <= 0: + plan_score = 80 if bid.get("plan") else 60 + if days_score <= 0: + dd = bid.get("deliver_days") or 0 + tdd = task.get("delivery_days") or 0 + days_score = 80 if (tdd == 0 or (dd and dd <= tdd)) else 55 + if team_score <= 0: + team_score = 80 if (bid.get("team_json") or "[]") != "[]" else 60 + if credit_score <= 0: + credit_score = 70 + total = round(quote_score * 0.30 + plan_score * 0.35 + days_score * 0.10 + + team_score * 0.10 + credit_score * 0.15, 2) + score_json = json.dumps({ + "quote": round(quote_score, 1), "plan": round(plan_score, 1), + "days": round(days_score, 1), "team": round(team_score, 1), + "credit": round(credit_score, 1), + }, ensure_ascii=False) + return await self.db.bids.set_review(bid_id, total, score_json, note) + + async def win_bid(self, task_id: str, bid_id: str, actor: dict) -> dict: + """定标中标:bid → win,其余 → lost;生成 claim(双写)→ claimed。""" + task = await self.db.tasks.get(task_id) + if task is None: + raise HTTPException(status_code=404, detail="任务不存在") + bid = await self.db.bids.get(bid_id) + if bid is None or bid["task_id"] != task_id: + raise HTTPException(status_code=404, detail="竞标不存在") + await self.db.bids.set_status(bid_id, "win") + for b in await self.db.bids.list_for_task(task_id): + if b["id"] != bid_id and b.get("status") == "submitted": + await self.db.bids.set_status(b["id"], "lost") + return await self._match(task, actor, bid["opc_id"], bid["opc_name"], + source="bid", claim_status="claimed") + + # ── 履约链:归属校验 + claim 双写 ──────────────────────────── + + async def _ensure_taker(self, task: dict, actor: dict) -> dict: + """校验操作者是否为当前接单人(claimed_by 优先,兼容 claim 流水)。""" + uid = actor.get("id") + if task.get("claimed_by") and task["claimed_by"] == uid: + claims = await self.db.task_claims.list_active_by_task(task["id"]) + return claims[0] if claims else None + claim = await self.db.task_claims.get_by_task_and_user(task["id"], uid or "") + if claim and claim.get("status") in ("claimed", "assigned", "doing", "delivered"): + return claim + raise HTTPException(status_code=403, detail="仅任务接单人可执行该操作") + + async def start_doing(self, task_id: str, actor: dict) -> dict: + task = await self.db.tasks.get(task_id) + if task is None: + raise HTTPException(status_code=404, detail="任务不存在") + if task["status"] != "claimed": + raise HTTPException(status_code=400, detail="任务未接单,无法开始") + claim = await self._ensure_taker(task, actor) + await self.db.tasks.set_doing(task_id) + if claim: + await self.db.task_claims.set_status(claim["id"], "doing") + # 里程碑物化 + try: + config = json.loads(task.get("milestone_json") or "[]") + except Exception: # noqa: BLE001 + config = [] + if isinstance(config, list) and config: + await self.db.task_milestones.ensure_from_config(task_id, config) + await _credit_event(self.db, actor["id"], "task_start", "开始执行任务", + ref_type="task", ref_id=task_id) + return await self.db.tasks.get(task_id) + + async def deliver(self, task_id: str, actor: dict, attachments: list | None = None, + note: str = "", milestone_id: str = "") -> dict: + """交付 v2:doing → delivered(版本化 TaskDelivery 记录 + claim 双写)。""" + task = await self.db.tasks.get(task_id) + if task is None: + raise HTTPException(status_code=404, detail="任务不存在") + if task["status"] != "doing": raise HTTPException(status_code=400, detail="任务未在进行中,无法交付") - return await self.db.tasks.set_status(task_id, "delivered") + claim = await self._ensure_taker(task, actor) + version = await self.db.task_deliveries.next_version(task_id) + await self.db.task_deliveries.create( + task_id, version, + attachments_json=json.dumps(attachments or [], ensure_ascii=False), + note=note, milestone_id=milestone_id, + ) + if milestone_id: + await self.db.task_milestones.set_status( + milestone_id, "delivered", delivered_at=_utcnow()) + await self.db.tasks.set_delivered(task_id, note) + if claim: + await self.db.task_claims.set_status(claim["id"], "delivered") + await _notify(self.db, task.get("publisher_id") or "", "task", + "任务已交付", f"「{task.get('title')}」已交付,请及时验收") + return await self.db.tasks.get(task_id) + + async def accept(self, task_id: str, actor: dict, checklist: list | None = None, + score: dict | None = None, comment: str = "", + milestone_id: str = "") -> dict: + """验收通过:delivered → completed(on_time 判定 + claim 双写 + 信用)。""" + task = await self.db.tasks.get(task_id) + if task is None: + raise HTTPException(status_code=404, detail="任务不存在") + if task["status"] != "delivered": + raise HTTPException(status_code=400, detail="任务未处于待验收状态") + if (task.get("publisher_id") or "") != actor.get("id"): + raise HTTPException(status_code=403, detail="仅发包方可验收") + on_time = True + if task.get("delivery_days"): + delivered_at = _parse_ts(task.get("deliver_at") or "") + started_at = _parse_ts(task.get("doing_at") or "") + if delivered_at and started_at: + span_hours = (delivered_at - started_at).total_seconds() / 3600 + on_time = span_hours <= int(task["delivery_days"]) * 24 + delivery_id = "" + deliveries = await self.db.task_deliveries.list_by_task(task_id) + if deliveries: + delivery_id = deliveries[-1]["id"] + await self.db.task_acceptances.create( + task_id, delivery_id, milestone_id, + checklist_json=json.dumps(checklist or [], ensure_ascii=False), + score_json=json.dumps(score or {}, ensure_ascii=False), + result="pass", comment=comment, reviewer_id=actor["id"], + ) + if milestone_id: + await self.db.task_milestones.set_status(milestone_id, "accepted", accepted_at=_utcnow()) + await self.db.tasks.set_accepted(task_id, on_time=on_time) + claims = await self.db.task_claims.list_active_by_task(task_id) + for c in claims: + await self.db.task_claims.set_status(c["id"], "completed") + await _credit_event(self.db, task.get("claimed_by") or "", "task_accept", + "任务验收通过", ref_type="task", ref_id=task_id) + await _credit_event(self.db, task.get("publisher_id") or "", "task_publish_success", + "任务完成", ref_type="task", ref_id=task_id) + await _notify(self.db, task.get("claimed_by") or "", "task", + "任务验收通过", f"「{task.get('title')}」已通过验收,可进行互评与结算") + return await self.db.tasks.get(task_id) + + async def reject(self, task_id: str, actor: dict, comment: str = "") -> dict: + """验收不通过:delivered → doing(返工),返工上限 MAX_REJECT。""" + task = await self.db.tasks.get(task_id) + if task is None: + raise HTTPException(status_code=404, detail="任务不存在") + if task["status"] != "delivered": + raise HTTPException(status_code=400, detail="任务未处于待验收状态") + if (task.get("publisher_id") or "") != actor.get("id"): + raise HTTPException(status_code=403, detail="仅发包方可验收") + count = int(task.get("reject_count") or 0) + 1 + if count > MAX_REJECT: + raise HTTPException(status_code=400, detail=f"返工已达上限({MAX_REJECT} 次),请发起争议") + deliveries = await self.db.task_deliveries.list_by_task(task_id) + delivery_id = deliveries[-1]["id"] if deliveries else "" + await self.db.task_acceptances.create( + task_id, delivery_id, "", + checklist_json="[]", score_json="{}", result="reject", + comment=comment, reviewer_id=actor["id"], + ) + await self.db.tasks.reject(task_id, count) + claims = await self.db.task_claims.list_active_by_task(task_id) + for c in claims: + await self.db.task_claims.set_status(c["id"], "doing") + await _notify(self.db, task.get("claimed_by") or "", "task", + "交付未通过验收", f"「{task.get('title')}」需返工(第 {count} 次):{comment}") + return await self.db.tasks.get(task_id) + + async def withdraw(self, task_id: str, actor: dict, reason: str = "") -> dict: + """接单人退出:claimed → published(释放);doing 阶段需发包方同意(暂仅 claimed)。""" + task = await self.db.tasks.get(task_id) + if task is None: + raise HTTPException(status_code=404, detail="任务不存在") + if task["status"] not in ("claimed", "doing"): + raise HTTPException(status_code=400, detail="当前状态不可退出") + if task["status"] == "doing": + raise HTTPException(status_code=400, detail="已开始任务需联系发包方或发起争议退出") + claim = await self._ensure_taker(task, actor) + await self.db.tasks.release_claim(task_id) + if claim: + await self.db.task_claims.set_status(claim["id"], "withdrawn") + await _credit_event(self.db, actor["id"], "task_withdraw", "接单后退出", + ref_type="task", ref_id=task_id) + await _notify(self.db, task.get("publisher_id") or "", "task", + "接单人退出", f"「{task.get('title')}」接单人已退出,任务重新开放") + return await self.db.tasks.get(task_id) + + async def cancel(self, task_id: str, actor: dict, reason: str = "") -> dict: + """发包方取消:published / claimed → cancelled。""" + task = await self.db.tasks.get(task_id) + if task is None: + raise HTTPException(status_code=404, detail="任务不存在") + if (task.get("publisher_id") or "") != actor.get("id"): + raise HTTPException(status_code=403, detail="仅发包方可取消任务") + if task["status"] not in ("published", "claimed"): + raise HTTPException(status_code=400, detail="当前状态不可取消") + await self.db.tasks.set_status(task_id, "cancelled") + await self.db.task_claims.release_by_task(task_id, "cancelled") + await _notify(self.db, task.get("claimed_by") or "", "task", + "任务已取消", f"「{task.get('title')}」已被发包方取消") + return await self.db.tasks.get(task_id) + + # ── 定时扫描(cron 入口)──────────────────────────────────── + + async def scan_pending(self) -> dict: + """定时任务:处理超时判定。 + + 1. claimed 超 24h 未开始 → 释放回 published(接单人信用扣分)。 + 2. delivered 超 72h 未验收 → 自动验收通过。 + 3. published 且已过 deadline → cancelled(expired)。 + """ + stats = {"released": 0, "auto_accepted": 0, "expired": 0} + for t in await self.db.tasks.list(): + status = t.get("status") + if status == "claimed": + h = _hours_since(t.get("claimed_at") or "") + if h is not None and h > 24: + await self.db.tasks.release_claim(t["id"]) + claims = await self.db.task_claims.list_active_by_task(t["id"]) + for c in claims: + await self.db.task_claims.set_status(c["id"], "withdrawn") + await _credit_event(self.db, c["claimer_user_id"], "task_overdue", + "接单后超时未开始", ref_type="task", ref_id=t["id"]) + stats["released"] += 1 + elif status == "delivered": + h = _hours_since(t.get("deliver_at") or "") + if h is not None and h > 72: + await self.db.tasks.set_accepted(t["id"], on_time=False) + claims = await self.db.task_claims.list_active_by_task(t["id"]) + for c in claims: + await self.db.task_claims.set_status(c["id"], "completed") + await _credit_event(self.db, t.get("claimed_by") or "", "task_accept", + "交付超时自动验收通过", ref_type="task", ref_id=t["id"]) + stats["auto_accepted"] += 1 + elif status == "published": + dl = _parse_ts(t.get("deadline") or "") + if dl is not None and dl < datetime.now(timezone.utc): + await self.db.tasks.set_expired(t["id"]) + claims = await self.db.task_claims.list_by_task(t["id"]) + for c in claims: + if c.get("status") == "registered": + await self.db.task_claims.set_status(c["id"], "withdrawn") + stats["expired"] += 1 + return stats + + # ── 兼容旧入口 ────────────────────────────────────────────── + + async def complete(self, task_id: str, actor: dict) -> dict: + """doing → delivered 的旧语义迁移为「立即交付」:保留兼容。""" + return await self.deliver(task_id, actor) async def enterprise_submit(self, task_id: str) -> dict: """企业提交审核:pending 任务 → published。""" return await self.db.tasks.set_status(task_id, "published") - async def win_bid(self, task_id: str, bid_id: str) -> dict: - """企业评标中标:bid → win,task → in_progress(双状态联动)。""" - bid = await self.db.bids.get(bid_id) - if bid is None or bid["task_id"] != task_id: - raise HTTPException(status_code=404, detail="竞标不存在") - await self.db.bids.set_status(bid_id, "win") - result = await self.db.tasks.set_status(task_id, "in_progress") - _sync_task_group(task_id) - return result - async def review(self, task_id: str, accept: bool) -> dict: - """企业验收:accept → completed;reject → in_progress。""" - target = "completed" if accept else "in_progress" - return await self.db.tasks.set_status(task_id, target) + """企业验收(旧入口,无 actor):仅承接已完成状态迁移。""" + if accept: + return await self.db.tasks.set_accepted(task_id) + return await self.db.tasks.set_status(task_id, "doing") diff --git a/scripts/sync_default_avatars.py b/scripts/sync_default_avatars.py new file mode 100644 index 0000000..bfe6ab2 --- /dev/null +++ b/scripts/sync_default_avatars.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""存量用户默认头像同步脚本。 + +给「头像为空」的存量用户随机分配一个默认头像(来自 avatar_pool.json 池)。 +已有真实头像(非空、非默认池前缀)的用户不会被覆盖。 + +用法: + python scripts/sync_default_avatars.py # 实际执行 + python scripts/sync_default_avatars.py --dry-run # 只预览不写库 +""" +from __future__ import annotations + +import argparse +import asyncio +from pathlib import Path + +from dotenv import load_dotenv + +load_dotenv(Path(__file__).resolve().parent.parent / ".env") + +from sqlalchemy import text # noqa: E402 + +from app.infrastructure.db import AsyncSessionLocal # noqa: E402 +from app.infrastructure.repositories import utcnow_iso # noqa: E402 +from app.services.avatar_pool import random_default_avatar # noqa: E402 + +DEFAULT_POOL_PREFIX = "/oss/avatar/images/images/" + + +async def main(dry_run: bool) -> int: + async with AsyncSessionLocal() as session: + # 仅处理头像为空(或 NULL)的存量用户;已设置真实头像的不动 + rows = ( + await session.execute( + text( + "SELECT id, nickname, username FROM users " + "WHERE avatar IS NULL OR avatar = ''" + ) + ) + ).all() + total = len(rows) + print(f"待同步用户数(头像为空): {total}") + if total == 0: + print("无需同步。") + return 0 + + updated = 0 + for row in rows: + avatar = random_default_avatar() + if not avatar: + print("警告:默认头像池为空,跳过(请检查 serverdata/avatar_pool.json)") + return 1 + print( + f" {'[DRY] ' if dry_run else '[set] '}{row.id} " + f"({row.nickname or row.username or ''}) -> {avatar}" + ) + if not dry_run: + await session.execute( + text("UPDATE users SET avatar = :av, updated_at = :now WHERE id = :uid"), + {"av": avatar, "uid": row.id, "now": utcnow_iso()}, + ) + updated += 1 + + if not dry_run: + await session.commit() + print(f"完成:{'(dry-run 未写库)' if dry_run else ''}计划/已更新 {updated} 人。") + return 0 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="存量用户默认头像同步") + parser.add_argument("--dry-run", action="store_true", help="只预览不写库") + args = parser.parse_args() + raise SystemExit(asyncio.run(main(args.dry_run))) diff --git a/serverdata/avatar_pool.json b/serverdata/avatar_pool.json new file mode 100644 index 0000000..9eb3210 --- /dev/null +++ b/serverdata/avatar_pool.json @@ -0,0 +1,1323 @@ +[ + "avatar/images/images/0094e9dd8b6f0521-lantern-5.jpg", + "avatar/images/images/00db1caee6079485-clothespin-3.jpg", + "avatar/images/images/0161901e72b5b974-hamster-2.jpg", + "avatar/images/images/016afd1decfd2bf0-bushbaby.jpg", + "avatar/images/images/0177e98389bbd26d-pale-belly-hamster.jpg", + "avatar/images/images/0178c13827065f35-amber-golem.jpg", + "avatar/images/images/01b5b387352499a3-maltese-dog.jpg", + "avatar/images/images/01f19eeec392dac2-friendly-ghost-2.jpg", + "avatar/images/images/020b8807b77401f4-tomato.jpg", + "avatar/images/images/020eaedcef603273-pretzel.jpg", + "avatar/images/images/022a590b7e0a42e6-sidneyia.jpg", + "avatar/images/images/02f7a553d1fb75e5-teapot-14.jpg", + "avatar/images/images/032346148c899175-pumpkin-vine-sprout.jpg", + "avatar/images/images/03341fa5ffc7ad4e-orangutan-3.jpg", + "avatar/images/images/0343713fd0d21820-harbor-seal-5.jpg", + "avatar/images/images/03686862e43aed8a-happy-fox.jpg", + "avatar/images/images/03925c309dec930b-giant-panda-bear.jpg", + "avatar/images/images/03b3e6caf479a8cc-dugong-4.jpg", + "avatar/images/images/04163cd096dae25f-watering-can-6.jpg", + "avatar/images/images/045337f175aa6676-typewriter-2.jpg", + "avatar/images/images/0587431e21244ceb-headphones-2.jpg", + "avatar/images/images/05dc2f56d12c27d0-warm-service-teapot.jpg", + "avatar/images/images/063aae41ff3c543b-cloud-2.jpg", + "avatar/images/images/0657274e41c1fe1e-yellow-chick.jpg", + "avatar/images/images/067ace7db9cd4493-observatory-dome-2.jpg", + "avatar/images/images/06836dab5611dc88-titan-arum.jpg", + "avatar/images/images/06bab6c3b8f19a4f-elephant-lavender-bg.jpg", + "avatar/images/images/06de92575cef6fac-weather-station-robot-2.jpg", + "avatar/images/images/06e83b885b7d3338-camel.jpg", + "avatar/images/images/074df406ea29af25-pangolin-11.jpg", + "avatar/images/images/077d991cf96a5867-otter-oval-head-left.jpg", + "avatar/images/images/07a1cef551598683-manta-ray-9.jpg", + "avatar/images/images/07a2ec0efe75dd4a-pincushion-4.jpg", + "avatar/images/images/07cb622730979d05-baby-monstera.jpg", + "avatar/images/images/07cd0dbc48213116-snow-globe.jpg", + "avatar/images/images/0804270b8e59f3c6-cream-lop-rabbit.jpg", + "avatar/images/images/081ecab9bd13042b-deep-red-fox.jpg", + "avatar/images/images/083616f5b490f7d9-jelly-cup.jpg", + "avatar/images/images/08d849bcd6195b2d-bat-6.jpg", + "avatar/images/images/08e3358e4e2e2dc2-snail-9.jpg", + "avatar/images/images/08e73f1e6ff3bafc-seal-5.jpg", + "avatar/images/images/08fd468efca5384b-duckling-2.jpg", + "avatar/images/images/090b6068c5f1c289-gentle-fox.jpg", + "avatar/images/images/092b18799d2e9ecf-moon-rover.jpg", + "avatar/images/images/0933abc70949f4b2-metronome-4.jpg", + "avatar/images/images/096d89e155868dc1-happy-owl.jpg", + "avatar/images/images/096fb7202a126560-peanut.jpg", + "avatar/images/images/098805d8ffbc4150-a1-cloud-messenger.jpg", + "avatar/images/images/099c8e2a09391d33-rhino.jpg", + "avatar/images/images/0a19a9ab72075f1d-luck-capsule.jpg", + "avatar/images/images/0a2b4236e80059eb-deer-2.jpg", + "avatar/images/images/0a4146b9e711bfcb-sprout.jpg", + "avatar/images/images/0a70e8d0ee6505bc-coral-dragon.jpg", + "avatar/images/images/0a7f583351a32930-standard-poodle-dog.jpg", + "avatar/images/images/0b0fa62e785b5729-oven-mitt.jpg", + "avatar/images/images/0b14e8c031917782-coffee-bean-2.jpg", + "avatar/images/images/0b1d45608961c209-hummingbird.jpg", + "avatar/images/images/0b20379ccff4abbb-spray-bottle.jpg", + "avatar/images/images/0b76d10e1a8bf5a8-sea-pig-5.jpg", + "avatar/images/images/0ba2b216e191fd29-hamster-gentle-small-muzzle-right.jpg", + "avatar/images/images/0bce06d5baaa2232-ink-blob-oracle.jpg", + "avatar/images/images/0bef4b58409c453f-raccoon-tiny-mouth-left.jpg", + "avatar/images/images/0c13c196b7ee463b-yeti-crab.jpg", + "avatar/images/images/0c29917e4e99ed52-fox-high-face-right.jpg", + "avatar/images/images/0c29cb57da8b4b0c-olm.jpg", + "avatar/images/images/0c9a5ae4b19d3cc8-bdelloid-rotifer.jpg", + "avatar/images/images/0ccfbd7bfae1cace-tea-genie.jpg", + "avatar/images/images/0cff0e84f9b78b06-rabbit-broad-forehead-right.jpg", + "avatar/images/images/0d1cbf9151eadcda-tiger-2.jpg", + "avatar/images/images/0d2144e32e5f69bc-citrus-reamer.jpg", + "avatar/images/images/0d525022dfb795d6-toucan.jpg", + "avatar/images/images/0d5e4b9a85342b09-siberian-cat-2.jpg", + "avatar/images/images/0dd1e024a5e889f9-monkey-cheerful.jpg", + "avatar/images/images/0e038a950ab3f47a-happy-chick.jpg", + "avatar/images/images/0e0d1c8d29ffdaf1-round-face-panda.jpg", + "avatar/images/images/0e10b34d1066aded-gentle-squirrel.jpg", + "avatar/images/images/0e259b9c98212e9d-amoeba.jpg", + "avatar/images/images/0e60eca2cc806c0c-sugar-glider-3.jpg", + "avatar/images/images/0e673008d54e0050-teapot-3.jpg", + "avatar/images/images/0ee743597f62fea9-toaster-robot.jpg", + "avatar/images/images/0f172b46e5536515-cassowary-2.jpg", + "avatar/images/images/0f8641ee97fe58d4-candle-spirit-2.jpg", + "avatar/images/images/0f914ec8a4758f07-hermit-crab-6.jpg", + "avatar/images/images/0f95ba973841e9d0-fern-guardian.jpg", + "avatar/images/images/0fbccb5533b16dc5-cream-arctic-fox.jpg", + "avatar/images/images/0fd8d1a524b89176-sea-bunny-nudibranch.jpg", + "avatar/images/images/100997932d863061-ladybird-beetle.jpg", + "avatar/images/images/1016d470f9f5e353-cornish-rex-cat-2.jpg", + "avatar/images/images/10188e30d2885ab3-bear-small-ears-right.jpg", + "avatar/images/images/1024c2a47fa1aa5e-acorn-wizard-2.jpg", + "avatar/images/images/10318a7194caa95c-cockapoo-dog.jpg", + "avatar/images/images/103fd565ea06c692-whale-13.jpg", + "avatar/images/images/107108967fa54592-blue-whale-3.jpg", + "avatar/images/images/10b4b55b2c08e5d1-praying-mantis.jpg", + "avatar/images/images/10b6d3b3eded4b3f-canele-2.jpg", + "avatar/images/images/1145257057086d62-purple-frog.jpg", + "avatar/images/images/1156fb6c14410452-sea-otter-3.jpg", + "avatar/images/images/115982e25472e8b1-tuxedo-cat.jpg", + "avatar/images/images/11796ee29e72220a-willow-tree-spirit.jpg", + "avatar/images/images/117bc2ce5f1da1e2-bear.jpg", + "avatar/images/images/118ca52057721884-coffee-grinder.jpg", + "avatar/images/images/119b71071ca9a761-pottery-wheel.jpg", + "avatar/images/images/11bb026e0f6586a2-tambourine.jpg", + "avatar/images/images/11c9a65ff28a3f8b-cream-face-deer.jpg", + "avatar/images/images/11cada57c1370dd7-silky-anteater.jpg", + "avatar/images/images/11d668868a0b9501-owl-5.jpg", + "avatar/images/images/123deac8ef5162a0-platypus-6.jpg", + "avatar/images/images/12af801ecd3f51e1-turtle-sleepy-soft.jpg", + "avatar/images/images/130fed3ee97b93e9-comet-whale.jpg", + "avatar/images/images/1318831fd92a150e-round-face-seal.jpg", + "avatar/images/images/13283408406ec266-mailbox-5.jpg", + "avatar/images/images/132b2d274f15df18-dumpling-5.jpg", + "avatar/images/images/13af073ba0571791-crocodile-3.jpg", + "avatar/images/images/14379e9c2c3de568-wide-muzzle-capybara.jpg", + "avatar/images/images/1455769972434e82-meerkat-2.jpg", + "avatar/images/images/145da7c9aa0d4ad0-mushroom-5.jpg", + "avatar/images/images/146cfaa6d89c97be-sea-turtle-3.jpg", + "avatar/images/images/14f7b38c57ef3ac3-wide-bill-duckling.jpg", + "avatar/images/images/14f92645095f2c2a-sneaker-2.jpg", + "avatar/images/images/153ca67647ba13bd-cyanobacterium.jpg", + "avatar/images/images/15574560c3105952-spatula.jpg", + "avatar/images/images/15f8503ef04783a0-papyrus-plant.jpg", + "avatar/images/images/160d142a11e8a290-kimberella.jpg", + "avatar/images/images/163f7549f553683d-hamster-soft-wide-cheeks-right.jpg", + "avatar/images/images/16f6a923cfb12719-west-highland-white-terrier-dog.jpg", + "avatar/images/images/16fac5dafd442ac9-ladybug-2.jpg", + "avatar/images/images/17270c38a718f075-southern-pudu.jpg", + "avatar/images/images/174f1a0e44ed0716-croissant-2.jpg", + "avatar/images/images/1787dd8d7c548a11-dog.jpg", + "avatar/images/images/1798fcf670fe4134-handpan.jpg", + "avatar/images/images/17f11f02e6cac355-zebra-2.jpg", + "avatar/images/images/1801bc2c513d348d-panda-serene-broad-muzzle-left.jpg", + "avatar/images/images/1872ac80caabeabb-manatee-12.jpg", + "avatar/images/images/1880c2e92bf02119-bubble-tea-cup.jpg", + "avatar/images/images/18d4c783b35f6287-rhino-2.jpg", + "avatar/images/images/18fa23cd370718ce-bumblebee-2.jpg", + "avatar/images/images/19a6de73b3e6424d-cream-chick.jpg", + "avatar/images/images/19b51d3b2c207e20-bento-box-3.jpg", + "avatar/images/images/19ed3667b7e53db7-rocket-2.jpg", + "avatar/images/images/1a233c9e27251ef5-moka-pot.jpg", + "avatar/images/images/1a25c5342ecb0b3f-flashlight.jpg", + "avatar/images/images/1a3ab8777310ed96-binoculars-6.jpg", + "avatar/images/images/1a3b17599dcb4f84-tamandua-2.jpg", + "avatar/images/images/1a5fe76822e74187-sieve.jpg", + "avatar/images/images/1a73dfcc78b303db-lightbulb-3.jpg", + "avatar/images/images/1a7f7b593a51ac9a-deer-bright-compact-face-left.jpg", + "avatar/images/images/1b03e7ff794c62be-bicolor-cat.jpg", + "avatar/images/images/1b28b80d7fa39bee-salamander-2.jpg", + "avatar/images/images/1b5000585e00ab0f-kelpie-foal.jpg", + "avatar/images/images/1b50ad7f5242c7a6-odaraia.jpg", + "avatar/images/images/1bc20edf631bd781-coccolithophore-2.jpg", + "avatar/images/images/1bc2bd68ca34e552-baby-dragon-3.jpg", + "avatar/images/images/1bcc46ea3d286b12-fig.jpg", + "avatar/images/images/1be54a0c5f625750-lotus-guardian.jpg", + "avatar/images/images/1c1ca7e5d7d459ab-goat-2.jpg", + "avatar/images/images/1c2a2825f77cf1d2-manta-ray-13.jpg", + "avatar/images/images/1c4be7e5b2f1997e-horse-shy-peek.jpg", + "avatar/images/images/1c6920913d06b2b3-chocolate-brown-bunny.jpg", + "avatar/images/images/1c9371c9fc0d7a6c-pear.jpg", + "avatar/images/images/1cad36f23a820e1b-squirrel-cheerful-wide-face-right.jpg", + "avatar/images/images/1d139c772d91fd34-seal-6.jpg", + "avatar/images/images/1d1b29f6ec04f545-red-blood-cell.jpg", + "avatar/images/images/1d6b015c5a1dee4c-tuatara.jpg", + "avatar/images/images/1dad6d0e34df8a2f-bear-glaucous.jpg", + "avatar/images/images/1dec9c0b3cf8b9bc-bench-vise-2.jpg", + "avatar/images/images/1e4dc5bbd90ab071-dewdrop-fairy.jpg", + "avatar/images/images/1eafe14938633a00-resilient-cactus.jpg", + "avatar/images/images/1eb150bb84729b4a-big-tail-squirrel.jpg", + "avatar/images/images/1eb527801f7ce45f-slide-projector-2.jpg", + "avatar/images/images/1eefeb7db82955a2-whale-6.jpg", + "avatar/images/images/1f14e9111d9493bb-axolotl.jpg", + "avatar/images/images/1f5c2b60d0de76b0-popcorn.jpg", + "avatar/images/images/1fb3a99e21bae651-kettlebell.jpg", + "avatar/images/images/1fcba03e58a62e80-dandelion-ghost.jpg", + "avatar/images/images/1fd3513e9612955e-round-face-deer.jpg", + "avatar/images/images/1fea4bd8535a97d8-orca-ochre.jpg", + "avatar/images/images/20263c116e347669-bassoon.jpg", + "avatar/images/images/2099ff740ee4fe66-accordion.jpg", + "avatar/images/images/20e5834d715545ca-moss-ball.jpg", + "avatar/images/images/20ea331202cdcbf9-acorn-wizard.jpg", + "avatar/images/images/210afaafb76a3779-mailbox-6.jpg", + "avatar/images/images/211a468f3705bc71-cat-5.jpg", + "avatar/images/images/219cd7883ee73069-cloud-shepherd.jpg", + "avatar/images/images/21dca69696f871c9-penguin-puffin.jpg", + "avatar/images/images/21e5d90846e5afd3-orange-red-fox.jpg", + "avatar/images/images/222581951a3781ba-dumbo-octopus-2.jpg", + "avatar/images/images/22af83f09a00da1e-woodpecker.jpg", + "avatar/images/images/22b2b9b5d1de7466-happy-penguin.jpg", + "avatar/images/images/22c084fa425c72e5-garden-snail.jpg", + "avatar/images/images/22d659d5d507ce20-acorn-cup-fungus.jpg", + "avatar/images/images/22f118fc111d56cc-kelp-sprite.jpg", + "avatar/images/images/22ffe1467a26c7ac-cloud-snail.jpg", + "avatar/images/images/231c0aedaa9d6067-chameleon-4.jpg", + "avatar/images/images/2358ede57599f6e2-cloud-dragon.jpg", + "avatar/images/images/23a899cc35c97cdf-yorkshire-terrier-dog-2.jpg", + "avatar/images/images/23c49e8c537e7d57-spoon-worm.jpg", + "avatar/images/images/240ad4f19c951504-geiger-counter-2.jpg", + "avatar/images/images/241b4e2e793b5530-tuning-fork.jpg", + "avatar/images/images/242a5d1ddb7b1e27-poodle.jpg", + "avatar/images/images/248985dcfc63f832-wifi-signal.jpg", + "avatar/images/images/24aa5210acae0f0d-phoenix-chick.jpg", + "avatar/images/images/24cde589d652a054-robot-2.jpg", + "avatar/images/images/2578f1d01b76f618-turtle-2.jpg", + "avatar/images/images/25bb724ec3abb7df-raccoon-small-ears-right.jpg", + "avatar/images/images/25bcda32b2ef213e-pincushion-2.jpg", + "avatar/images/images/25cd9fb7b8a366d9-fox.jpg", + "avatar/images/images/26134112943cd18b-plasma-firetruck.jpg", + "avatar/images/images/26527ced222f9a28-hamster-curious-large-eyes-left.jpg", + "avatar/images/images/2675955966d799ce-street-sweeper.jpg", + "avatar/images/images/269e0794edc4b082-thimble-6.jpg", + "avatar/images/images/26c625180ebda982-deep-sea-sampling-robot.jpg", + "avatar/images/images/271b456aa7fe55b2-dewdrop-beetle-spirit.jpg", + "avatar/images/images/272165114aaec508-gentle-chick.jpg", + "avatar/images/images/2727368f06889024-puffin-3.jpg", + "avatar/images/images/27471b7ba9cfe8a9-coffee-bean.jpg", + "avatar/images/images/275d52eed40ecc2a-mole-6.jpg", + "avatar/images/images/276677f5dd49760c-owl.jpg", + "avatar/images/images/27aaf42b83809076-butterwort.jpg", + "avatar/images/images/282431be9192e662-wooden-mallet.jpg", + "avatar/images/images/283bf5fd4d2a9380-pinecone.jpg", + "avatar/images/images/285471d062129851-familiar-robot.jpg", + "avatar/images/images/285ff1782c83666f-padlock.jpg", + "avatar/images/images/2885971611d78710-french-bulldog-dog-2.jpg", + "avatar/images/images/28862493ae2178dc-kakapo.jpg", + "avatar/images/images/2897783fd181eee1-lion-soft-peek.jpg", + "avatar/images/images/28a44fd3c234fedf-happy-red-panda.jpg", + "avatar/images/images/29592cee61ec9074-norwegian-forest-cat-2.jpg", + "avatar/images/images/29623f494a0a5dcf-tulip.jpg", + "avatar/images/images/29648588b5ce5b4c-horseshoe-magnet.jpg", + "avatar/images/images/29cff0f56e431aab-dog-tall-crop-left.jpg", + "avatar/images/images/29f8a4b5858c57bb-lantern-polisher-robot.jpg", + "avatar/images/images/2a14cb1e4c7470c8-apothecary-jar.jpg", + "avatar/images/images/2a1968336105a75a-raccoon-denim-blue.jpg", + "avatar/images/images/2a44a2dde40bdb50-bear-broad-cheeks-right.jpg", + "avatar/images/images/2aa9490920a375ab-parking-meter.jpg", + "avatar/images/images/2ac36cb72275309d-octopus-3.jpg", + "avatar/images/images/2ace38f2e676d20e-round-face-chick.jpg", + "avatar/images/images/2b269cea1b078d1d-wobbegong-shark.jpg", + "avatar/images/images/2b2794b0a8e45cfd-nudibranch.jpg", + "avatar/images/images/2b2f69d95a02ffad-alarm-clock-4.jpg", + "avatar/images/images/2b3e0a116feb810a-hermit-crab-2.jpg", + "avatar/images/images/2b62a91de04123f3-clock-tower.jpg", + "avatar/images/images/2b74f64737f1c8df-hamster-calm-round-face-right.jpg", + "avatar/images/images/2b7a9857b923da43-magnetic-stirrer.jpg", + "avatar/images/images/2bd3e5cfbd85068d-warm-brown-deer.jpg", + "avatar/images/images/2bf18969a41bebe9-tidepool-robot.jpg", + "avatar/images/images/2bf47b2193544bcb-donkey-shy-peek.jpg", + "avatar/images/images/2c0705ae7567e23c-boxer-dog-2.jpg", + "avatar/images/images/2c0965694c5abcc6-vicuna.jpg", + "avatar/images/images/2c67795c4c7d86a0-eagle.jpg", + "avatar/images/images/2c7ed591b3179dad-cockatrice.jpg", + "avatar/images/images/2cdc330229a01120-moss-golem.jpg", + "avatar/images/images/2cde518057cc7600-golden-squirrel.jpg", + "avatar/images/images/2d192aff9e5cfd2f-alarm-clock.jpg", + "avatar/images/images/2d6ad2d4a6ec564f-sheep-sleepy-soft-2.jpg", + "avatar/images/images/2d72f76cd8b50f1d-label-maker.jpg", + "avatar/images/images/2d8cfc6d1190c578-meerkat.jpg", + "avatar/images/images/2dd0763010ac1b8c-golden-retriever.jpg", + "avatar/images/images/2ddcfad1deff597d-pig-2.jpg", + "avatar/images/images/2e0b6c5a080f435c-pig.jpg", + "avatar/images/images/2e96de13884b105d-horse-playful.jpg", + "avatar/images/images/2e99eb62f01d03de-fennec-fox-2.jpg", + "avatar/images/images/2ee65312ff168688-venus-flytrap.jpg", + "avatar/images/images/2f20f68c6803d33c-strawberry.jpg", + "avatar/images/images/2f25df557d35aac5-lantern-moth-spirit.jpg", + "avatar/images/images/2f266d7034d0e5e2-bento-box-2.jpg", + "avatar/images/images/2f38d28e52f99d2b-welsh-corgi.jpg", + "avatar/images/images/2f6368084b8736e8-blobfish.jpg", + "avatar/images/images/2f63907d4524ce6d-desman.jpg", + "avatar/images/images/2f7dfe3cc061b676-music-box-robot-2.jpg", + "avatar/images/images/2fd51aa6b225bec7-axolotl-glaucous.jpg", + "avatar/images/images/3023eba2cc2491c6-slide-carousel.jpg", + "avatar/images/images/30aa9d9e2139f050-bernese-mountain-dog.jpg", + "avatar/images/images/31065822e6033754-robot.jpg", + "avatar/images/images/3126dfc6c9d67ebb-magic-caravan.jpg", + "avatar/images/images/31299af771f32443-coconut.jpg", + "avatar/images/images/3133d42260817d7d-saturn.jpg", + "avatar/images/images/31392f8996899083-barometer-2.jpg", + "avatar/images/images/31533375d66602d9-bumblebee-plum.jpg", + "avatar/images/images/31616c635f42e4e0-mushroom-knight-2.jpg", + "avatar/images/images/31842ec74cbcec28-black-cat-2.jpg", + "avatar/images/images/31a506b384b979f6-pear-5.jpg", + "avatar/images/images/31ab1028835219c9-snowdrift-spirit.jpg", + "avatar/images/images/32101f85d2a434a1-salp-2.jpg", + "avatar/images/images/3265e71420862be0-headphones-3.jpg", + "avatar/images/images/327d444de3dead3e-bakery-robot.jpg", + "avatar/images/images/32acee90246cc432-nebula-satellite.jpg", + "avatar/images/images/32f91c9f141a5970-warm-brown-squirrel.jpg", + "avatar/images/images/331ec4ec84021c9e-library-gargoyle-2.jpg", + "avatar/images/images/33d49e57d307d46e-happy-duckling.jpg", + "avatar/images/images/33f5b31d039796bb-pear-3.jpg", + "avatar/images/images/3437e3d7f97a676a-cloud-3.jpg", + "avatar/images/images/3475a228e953aef8-rift-anchor.jpg", + "avatar/images/images/347f25b0160ca682-red-panda-11.jpg", + "avatar/images/images/348e8e118c367245-giraffe-2.jpg", + "avatar/images/images/348f2477198babce-peach-eared-white-bunny.jpg", + "avatar/images/images/34c4a3326c0b184a-baby-seal-2.jpg", + "avatar/images/images/34efda81550b71b9-manta-ray-2.jpg", + "avatar/images/images/35695a85497d8211-round-face-raccoon.jpg", + "avatar/images/images/358f0d10bfe90be6-toucan-7.jpg", + "avatar/images/images/35a53186b4911e51-baby-goat-calm.jpg", + "avatar/images/images/35e64ecc3c69e373-eraser-2.jpg", + "avatar/images/images/3605592cc501ae84-squirrel-2.jpg", + "avatar/images/images/3639831dc7250069-lion-gentle-smile.jpg", + "avatar/images/images/3646a8619299e5c6-window-washing-robot.jpg", + "avatar/images/images/3654a58fef533ee1-coelacanth-2.jpg", + "avatar/images/images/36768d34e4123b48-pug.jpg", + "avatar/images/images/36825a0185e99228-alpaca-8.jpg", + "avatar/images/images/368a5da2430fb61f-emeraldella.jpg", + "avatar/images/images/3694c82eefa1c277-pebble-troll-3.jpg", + "avatar/images/images/3696d4ef9c1de101-cow-calm.jpg", + "avatar/images/images/36e0a8e508185f10-rubber-stamp-2.jpg", + "avatar/images/images/37c0f6b382d73437-white-duckling.jpg", + "avatar/images/images/37d6f69718e49735-airplane.jpg", + "avatar/images/images/37feecce12db98a2-quantum-rover.jpg", + "avatar/images/images/383f9eb4fc67d3a9-jellyfish-14.jpg", + "avatar/images/images/388091f222839862-tapir-3.jpg", + "avatar/images/images/39344a391cbc02f9-tidepool-dragon-2.jpg", + "avatar/images/images/3973cd8b87aabb97-phonograph-2.jpg", + "avatar/images/images/399b6bd875832ebc-mushroom-4.jpg", + "avatar/images/images/39aa94b473bfe5f9-round-ear-otter.jpg", + "avatar/images/images/39b8b68dc711c1ac-bear-low-ears-right.jpg", + "avatar/images/images/39bb39b334b85bcd-rhinoceros-cheerful.jpg", + "avatar/images/images/39cb90acd54c755e-mushroom-15.jpg", + "avatar/images/images/3a03135e0811597e-artichoke-4.jpg", + "avatar/images/images/3a0e2f9be21ab328-glockenspiel.jpg", + "avatar/images/images/3a2443e846b3c8f9-cuttlefish-2.jpg", + "avatar/images/images/3a72b4d2c1a37f86-acorn-worm.jpg", + "avatar/images/images/3a752d82b406660f-moon-flower.jpg", + "avatar/images/images/3aadd8a529b9382e-dog-low-ears-left.jpg", + "avatar/images/images/3b1f8480bc7a37a1-lichen-pebble-spirit.jpg", + "avatar/images/images/3b241692b15ff6e0-delivery-van.jpg", + "avatar/images/images/3babd6090622f471-beehive-skep.jpg", + "avatar/images/images/3c3558ea38ddc6ad-comb-jelly-3.jpg", + "avatar/images/images/3c7cf76e1f1a9478-chubby-deer.jpg", + "avatar/images/images/3c8c9ab28b6a63f4-cream-face-panda.jpg", + "avatar/images/images/3ced8c1f4f5be60d-decorator-crab.jpg", + "avatar/images/images/3dbf031e816c426f-fountain-pen-nib.jpg", + "avatar/images/images/3e241e8530772759-dog-broad-cheeks-left.jpg", + "avatar/images/images/3e4a48ff9042c859-red-panda-2.jpg", + "avatar/images/images/3e9dcf74fc4700fe-golden-retriever-dog.jpg", + "avatar/images/images/3efcb3aaff137264-sandwich-cookie.jpg", + "avatar/images/images/3f01ef741785996e-seahorse-2.jpg", + "avatar/images/images/3f753fbb78c2999e-moonbase-habitat.jpg", + "avatar/images/images/3f7f5bec96d83ccb-gravy-boat.jpg", + "avatar/images/images/3fa92aa38435efe1-courier-robot.jpg", + "avatar/images/images/400d95a93c2267d6-dog-oval-head-right.jpg", + "avatar/images/images/401c5399f8a713c7-sunflower-lion-cub.jpg", + "avatar/images/images/4082b26aadaa04cb-selkie-pup.jpg", + "avatar/images/images/40d1c52435510175-potential-acorn.jpg", + "avatar/images/images/412a3fe4295fb23d-tidepool-dragon.jpg", + "avatar/images/images/413a77f3d1776aa4-bamboo.jpg", + "avatar/images/images/4165ca57ed5394dc-bilby.jpg", + "avatar/images/images/4175f519dc1faff8-desk-lamp-6.jpg", + "avatar/images/images/422dd33c4c29c242-spirit-level-3.jpg", + "avatar/images/images/4231d4f39d38087f-happy-deer.jpg", + "avatar/images/images/425989f988f037cd-spotted-tail-quoll.jpg", + "avatar/images/images/4275fbadd7a9e3d8-ciabatta-roll.jpg", + "avatar/images/images/42d5c2bd11a03a20-raccoon-low-face-left.jpg", + "avatar/images/images/43130c35035ed41a-frog-sleepy-soft.jpg", + "avatar/images/images/4384a78137c761bf-bagel.jpg", + "avatar/images/images/43d04d4394e98994-capybara-10.jpg", + "avatar/images/images/4484a25b2472f600-neon-bathysphere.jpg", + "avatar/images/images/44abc9d44510667e-sleepy-owl.jpg", + "avatar/images/images/44dd6cebc1fbdfa1-pitcher-plant-spirit.jpg", + "avatar/images/images/44e0e27de8ade22a-bear-3.jpg", + "avatar/images/images/454e0d80f29d183e-barking-deer.jpg", + "avatar/images/images/455703466b4689e5-soft-mask-panda.jpg", + "avatar/images/images/4572e1117a7f0c05-pager-2.jpg", + "avatar/images/images/457ba46729ce5c5b-labrador-retriever-dog.jpg", + "avatar/images/images/45ce456d311a2641-lantern-fairy.jpg", + "avatar/images/images/45f06f252ea750d3-ram-lime-cream-2.jpg", + "avatar/images/images/45fef6834e64fe0d-stag-beetle-2.jpg", + "avatar/images/images/460379e60ea7bde1-walkie-talkie.jpg", + "avatar/images/images/46b2e2ab820fbec3-cassette-3.jpg", + "avatar/images/images/46b5404a8edd0f62-moon-moth-spirit.jpg", + "avatar/images/images/46ba8019398cf7bd-armadillo-3.jpg", + "avatar/images/images/46c806c2124cd57c-skunk.jpg", + "avatar/images/images/4723d948560b4901-night-router.jpg", + "avatar/images/images/472dd27d9dc09cee-wombat-8.jpg", + "avatar/images/images/47334457c84add3f-owl-scout.jpg", + "avatar/images/images/477eb5c56cd88587-schnauzer-2.jpg", + "avatar/images/images/47807d5423290f2b-friendly-robot-2.jpg", + "avatar/images/images/479ce770ea4d8c08-bharal.jpg", + "avatar/images/images/47bcc123014232dd-sphynx-cat-2.jpg", + "avatar/images/images/47e471dce0905af7-cinnamon-fox-spirit.jpg", + "avatar/images/images/47fb25c19de97f23-lemur-2.jpg", + "avatar/images/images/4835705980c6df63-leafy-seadragon.jpg", + "avatar/images/images/48a9450e42e43a89-sushi-roll.jpg", + "avatar/images/images/48ae3192cb304c90-gentle-deer.jpg", + "avatar/images/images/48f45093542b6386-chubby-penguin.jpg", + "avatar/images/images/49496acce5b9368f-mountain-spirit.jpg", + "avatar/images/images/4aa07cf43db94c9a-cowbell.jpg", + "avatar/images/images/4ad2c094c9f24e9f-nautilus.jpg", + "avatar/images/images/4ae1759e1f3d7256-yeti-crab-2.jpg", + "avatar/images/images/4ae2da5a61ca0172-siamese-cat.jpg", + "avatar/images/images/4af1a0b4a5bee826-american-curl-cat-2.jpg", + "avatar/images/images/4b240b211a7b6745-frog-bright-eyed.jpg", + "avatar/images/images/4bb7586c78bfa45d-raccoon-8.jpg", + "avatar/images/images/4c09964a1bdaca15-mana-battery.jpg", + "avatar/images/images/4c211899d16f0eb1-tortoise-2.jpg", + "avatar/images/images/4c3434b827346ec8-aye-aye.jpg", + "avatar/images/images/4c6f50b7d2c696e0-wrench-2.jpg", + "avatar/images/images/4ce8c0efd462f163-lysosome.jpg", + "avatar/images/images/4d34b59bda00b196-dwarf-hamster.jpg", + "avatar/images/images/4db75d434b90e5e7-focaccia.jpg", + "avatar/images/images/4dca77fdd53eda80-whisk.jpg", + "avatar/images/images/4de258c57339b5e3-bombay-cat-2.jpg", + "avatar/images/images/4e662e8903a9073d-bandicoot.jpg", + "avatar/images/images/4e72cfb216996d78-elephant-cheerful.jpg", + "avatar/images/images/4e826115782f622c-weather-vane.jpg", + "avatar/images/images/4e8c36ad9f7a3fe4-harp.jpg", + "avatar/images/images/4ec0c51b79c04c5f-mixed-breed-pointy-ear-dog.jpg", + "avatar/images/images/4f33f5862a388af0-periscope.jpg", + "avatar/images/images/4fa0f5c41427869d-coton-de-tulear-dog.jpg", + "avatar/images/images/4fd6d5d88440cae6-desk-lamp-3.jpg", + "avatar/images/images/502a090ac55e3fa4-rabbit.jpg", + "avatar/images/images/5050916dbb402662-rock-hyrax-3.jpg", + "avatar/images/images/5076c7a4341104b2-ghost-deep-twilight.jpg", + "avatar/images/images/5130b62fd3171055-abacus-2.jpg", + "avatar/images/images/513d096f5388cb9a-waffle-iron.jpg", + "avatar/images/images/51447f9b49bfd09a-flashlight-3.jpg", + "avatar/images/images/5243572ce2705d33-cassette-2.jpg", + "avatar/images/images/524c027263023ec5-conga-drum.jpg", + "avatar/images/images/52b7e85c01202978-wombat-7.jpg", + "avatar/images/images/52ee36583bb3a48e-loom-robot.jpg", + "avatar/images/images/53146d476807afce-baby-goat-bright-eyed.jpg", + "avatar/images/images/5330a4ae47ea8640-bat-5.jpg", + "avatar/images/images/5386e6419bae1f76-sea-bunny-2.jpg", + "avatar/images/images/5387da0d46be765e-moon-gate.jpg", + "avatar/images/images/53e7a4745716863e-thunderbird-chick.jpg", + "avatar/images/images/53f8e0abb6cec4d6-c-clamp-2.jpg", + "avatar/images/images/5442b9c9e765fa69-signal-wormhole.jpg", + "avatar/images/images/5456cbd39c0643d0-seismograph.jpg", + "avatar/images/images/54911bec8551ed01-tabby-cat.jpg", + "avatar/images/images/54b10e9fc0435ea7-luna-moth-2.jpg", + "avatar/images/images/552966175473bf98-rose-submersible.jpg", + "avatar/images/images/55365b67403bbd88-beagle-2.jpg", + "avatar/images/images/55374032b3dc8565-seahorse-7.jpg", + "avatar/images/images/55590448025a8060-rhinoceros-beetle.jpg", + "avatar/images/images/555af2fdb6dce586-jellyfish-11.jpg", + "avatar/images/images/55689b4f0a8fdb54-cobblers-last.jpg", + "avatar/images/images/558675cc31f6f052-naan-bread.jpg", + "avatar/images/images/558b2611fdeda92d-serow.jpg", + "avatar/images/images/55a73eb7921f059c-rubber-duck.jpg", + "avatar/images/images/55c876b13a6a69b9-walrus-6.jpg", + "avatar/images/images/560855000879c861-pembroke-corgi-dog.jpg", + "avatar/images/images/562e56deb1a120d3-warm-brown-raccoon.jpg", + "avatar/images/images/5631e68516c8c4bb-pallas-cat.jpg", + "avatar/images/images/567ad7cf6487609c-vampire-squid.jpg", + "avatar/images/images/56c37e1026446249-sunken-drone.jpg", + "avatar/images/images/57162d9399ef9e4b-astrolabe-2.jpg", + "avatar/images/images/5730b457e8b07c70-greater-kudu.jpg", + "avatar/images/images/57b40737b865bb32-raccoon-apricot.jpg", + "avatar/images/images/57f6faba85fe8f8e-mangosteen.jpg", + "avatar/images/images/5818ec3c13d09248-black-cat.jpg", + "avatar/images/images/584db12ca075872f-floppy-ear-mixed-dog.jpg", + "avatar/images/images/587a4ab2b7fd1285-theodolite.jpg", + "avatar/images/images/588c0c67ec3557a2-lunch-tiffin.jpg", + "avatar/images/images/58a12b913eecc8c9-ginkgo-leaf-spirit.jpg", + "avatar/images/images/58b586d75fde59d4-fox-oval-head-left.jpg", + "avatar/images/images/59597d2f80c5cb12-public-fountain.jpg", + "avatar/images/images/598b7606464033a9-strawberry-3.jpg", + "avatar/images/images/59a9ebe6e18dc63b-puffball-mushroom-2.jpg", + "avatar/images/images/59ae348df8ff463f-pig-playful-2.jpg", + "avatar/images/images/59b57ede4ba46dcb-classic-penguin.jpg", + "avatar/images/images/59e129d3edfa1251-french-bulldog-dog.jpg", + "avatar/images/images/5a755dada6cfb065-moonrat-2.jpg", + "avatar/images/images/5a7c68eb5ac84573-silverfish.jpg", + "avatar/images/images/5aaa2cce49739cdd-jelly-robot.jpg", + "avatar/images/images/5ae7f16a7a574e6f-acorn-goblin.jpg", + "avatar/images/images/5aea393db821c996-lyre.jpg", + "avatar/images/images/5aefbcfab43f54a4-cymbals.jpg", + "avatar/images/images/5b3fb7df3a9e535a-comet-penguin.jpg", + "avatar/images/images/5b9bef66632e0a53-sundew-plant.jpg", + "avatar/images/images/5bb0f6d64063d14a-musk-ox.jpg", + "avatar/images/images/5c0c860bbd7c0b8f-turkish-van-cat-2.jpg", + "avatar/images/images/5c1fc64eb0211bba-tarsier.jpg", + "avatar/images/images/5c573b797b0b5565-anglerfish-2.jpg", + "avatar/images/images/5c5bec516a456370-custard-pudding.jpg", + "avatar/images/images/5c8649c0542efb92-keycap.jpg", + "avatar/images/images/5c9989212adff0ad-tiny-bonsai.jpg", + "avatar/images/images/5cb781ec5a95ea5b-bear-bean-head-right.jpg", + "avatar/images/images/5d38bd357bf79406-binoculars-5.jpg", + "avatar/images/images/5d38c4ffe98dd5db-anemometer-2.jpg", + "avatar/images/images/5d715fec5cc45312-saiga-antelope-2.jpg", + "avatar/images/images/5d71889356a5ed3d-sea-turtle-6.jpg", + "avatar/images/images/5daf5a16dd2125eb-aurora-seal-spirit.jpg", + "avatar/images/images/5df13bb6dad6f27b-velvet-worm.jpg", + "avatar/images/images/5e6466429d75b031-loriciferan.jpg", + "avatar/images/images/5e6ee01b6d657a19-water-tower-2.jpg", + "avatar/images/images/5e8c57ad14873d7e-fluffy-white-rabbit.jpg", + "avatar/images/images/5eacae05352bc327-c-clamp.jpg", + "avatar/images/images/5ecfbf79133161a7-kiwi-bird-8.jpg", + "avatar/images/images/5ee0d28147bce865-paint-roller.jpg", + "avatar/images/images/5fe9d3bdd1624eeb-sea-lion.jpg", + "avatar/images/images/603a82c44b50cd3c-round-face-hamster.jpg", + "avatar/images/images/606d48c5bb314176-orca-3.jpg", + "avatar/images/images/60b65b9e1fa6357e-happy-brown-bear.jpg", + "avatar/images/images/60c6d35bb4b2c5fc-pager.jpg", + "avatar/images/images/60f3e505b5a7184c-pomeranian.jpg", + "avatar/images/images/611c78b65d5636c0-curious-panda.jpg", + "avatar/images/images/61b349663dab3bfb-horse-2.jpg", + "avatar/images/images/6205aadbcae9f743-canele-pastry.jpg", + "avatar/images/images/621bb2d33a5ed082-otter-low-ears-right.jpg", + "avatar/images/images/6250cb0872dc4e1e-sitar.jpg", + "avatar/images/images/625f245624373c45-small-ear-fox.jpg", + "avatar/images/images/626bce2d832f6267-rhodesian-ridgeback-dog.jpg", + "avatar/images/images/62784f2bd38fb9b7-pig-gentle-smile.jpg", + "avatar/images/images/6286d2ca355c505f-chloroplast.jpg", + "avatar/images/images/629d87141d453f08-tortoiseshell-inspired-cat.jpg", + "avatar/images/images/62b44d7e0eb26d28-alpaca-4.jpg", + "avatar/images/images/62e3c367e5a64d05-electric-kettle-2.jpg", + "avatar/images/images/62e9834334b3ac95-cassette-cream-2.jpg", + "avatar/images/images/62fc00e7e564dea3-doorbell.jpg", + "avatar/images/images/630b50da9340fd21-shiba-inu-puppy.jpg", + "avatar/images/images/6327a1e9fcb1a589-lantern-jellyfish-charcoal.jpg", + "avatar/images/images/63b1a2bbfae3a37c-lunar-lander.jpg", + "avatar/images/images/63e6e896232b9075-gorilla.jpg", + "avatar/images/images/63fd00538ea516be-somali-cat-2.jpg", + "avatar/images/images/64481f2da7374a86-seal-2.jpg", + "avatar/images/images/64ca70ab712d5c36-river-otter.jpg", + "avatar/images/images/64ef47d6f511b48d-robot-vacuum.jpg", + "avatar/images/images/651b6c2756e93034-dobsonfly.jpg", + "avatar/images/images/6521e033592ab40c-teapot-10.jpg", + "avatar/images/images/65926da5802d25ef-basket-star-2.jpg", + "avatar/images/images/6604ed17fca0867b-rabbit-2.jpg", + "avatar/images/images/662f36b0cd242fba-mushroom-8.jpg", + "avatar/images/images/66497957d1015d95-seashell-moon-rabbit.jpg", + "avatar/images/images/6661e0df0b8f897e-paint-roller-2.jpg", + "avatar/images/images/669259666116750a-bear-round-face-left.jpg", + "avatar/images/images/66b348e6b8391e44-ghost-9.jpg", + "avatar/images/images/66c9edf49ce30c23-snowball-yeti-2.jpg", + "avatar/images/images/66ea2403127560b9-blue-whale-4.jpg", + "avatar/images/images/6750ef9a4ffb6910-rune-hoverboard.jpg", + "avatar/images/images/6763be2e026107d5-deer-serene-broad-muzzle-right.jpg", + "avatar/images/images/676a44fa4c154580-hope-parachute.jpg", + "avatar/images/images/677356eda4c23941-rolling-pin-4.jpg", + "avatar/images/images/68206a39145ee51d-handbell-2.jpg", + "avatar/images/images/683bebebd511e484-isoxys.jpg", + "avatar/images/images/683ffd79b7f0864b-whale-warm-gray.jpg", + "avatar/images/images/68518755e7fc8583-tabletop-telescope-2.jpg", + "avatar/images/images/687d82d28ab4246b-cane-corso-dog.jpg", + "avatar/images/images/6880a31a6f8039c5-giant-panda.jpg", + "avatar/images/images/6891c8570c66e825-miniature-schnauzer-dog-2.jpg", + "avatar/images/images/68b71416c89914b6-long-eared-jerboa-2.jpg", + "avatar/images/images/6914d12e83558586-hibbertopterus.jpg", + "avatar/images/images/692ad705d58399a0-toad.jpg", + "avatar/images/images/6952707fc6f3aac0-manta-ray-5.jpg", + "avatar/images/images/6983d88c53709be3-deer-shy-soft-smile-right.jpg", + "avatar/images/images/698c2beda5f0a40a-pitcher-plant-2.jpg", + "avatar/images/images/69c9390c2a636c55-mailbox-3.jpg", + "avatar/images/images/69d6c78689e7d397-dachshund-dog-2.jpg", + "avatar/images/images/6a0dd5d7cb713792-dik-dik.jpg", + "avatar/images/images/6a53b94c92d71a33-acorn-3.jpg", + "avatar/images/images/6a864152caeb25b7-baby-crab.jpg", + "avatar/images/images/6a8a1b955ae4df28-thunderbird-chick-2.jpg", + "avatar/images/images/6a961bad25ce6bca-pitcher-plant-sprite.jpg", + "avatar/images/images/6abda03163e8b251-jellyfish.jpg", + "avatar/images/images/6b1652589995ff02-squirrel-serene-broad-muzzle-left.jpg", + "avatar/images/images/6b1f2066131e878d-kiwi-bird-4.jpg", + "avatar/images/images/6b2c009965e3f9ae-pangolin-5.jpg", + "avatar/images/images/6b43d3a48664c8b9-domestic-shorthair-tabby-cat.jpg", + "avatar/images/images/6b53a3cb1f2086ba-fox-4.jpg", + "avatar/images/images/6b6f1a4a22450ed8-nectocaris.jpg", + "avatar/images/images/6b739c0fa681fae3-manatee-6.jpg", + "avatar/images/images/6b777b187bc2fa21-swan-3.jpg", + "avatar/images/images/6bc781bd6e63ee0a-duck.jpg", + "avatar/images/images/6be88279df3fee3c-bigfin-reef-squid.jpg", + "avatar/images/images/6bee9c934e42cc15-ghost-6.jpg", + "avatar/images/images/6c7aaba53ebcea91-jack-russell-terrier.jpg", + "avatar/images/images/6d38dc719e1bb440-rabbit-muted-burgundy.jpg", + "avatar/images/images/6d718b9725c771ca-cow.jpg", + "avatar/images/images/6de3117a12e17302-wide-face-owl.jpg", + "avatar/images/images/6deef935f08f601e-ravioli.jpg", + "avatar/images/images/6df27622060a9d27-clothespin-2.jpg", + "avatar/images/images/6dfeb299da3cf287-cream-tabby-cat.jpg", + "avatar/images/images/6e05ed5c269ee42a-resurrection-plant.jpg", + "avatar/images/images/6ed054b24e63501d-moon-deep-twilight-2.jpg", + "avatar/images/images/6f0b24309f85e7c1-oven-mitt-2.jpg", + "avatar/images/images/6f27062b3203d54e-iron-wisp.jpg", + "avatar/images/images/6f33b67742f15c4b-hamster-4.jpg", + "avatar/images/images/6f41eb4ffc8f1795-round-face-squirrel.jpg", + "avatar/images/images/6f5bbdccf56f4500-bengal-inspired-cat.jpg", + "avatar/images/images/6f609c1d4c86a676-round-moss-ball.jpg", + "avatar/images/images/6f657e887c788cc9-turkish-van-cat.jpg", + "avatar/images/images/6f756e486d3d324e-dolphin-calf.jpg", + "avatar/images/images/6f9caf0977f351b6-moth.jpg", + "avatar/images/images/6f9d62e3f0e600ac-lotus-bud-2.jpg", + "avatar/images/images/6fefb833be49da4a-otter-tiny-mouth-left.jpg", + "avatar/images/images/6ffc889a7aea8e68-gecko.jpg", + "avatar/images/images/703a913c8da2e05b-gargoyle.jpg", + "avatar/images/images/7055678005065338-lacewing.jpg", + "avatar/images/images/70e90bf9614d87f6-penguin-friendly-low-features-right.jpg", + "avatar/images/images/70f48ce3751f3022-friendly-ghost.jpg", + "avatar/images/images/71114373f9e7096e-eclipse-owl-spirit-2.jpg", + "avatar/images/images/71889219f8d0696f-barn-owl-2.jpg", + "avatar/images/images/71e74fc189d283e5-brown-duckling.jpg", + "avatar/images/images/720b403505f00f26-raincloud-sprite.jpg", + "avatar/images/images/72a61efe85bb2bdd-lion-ochre.jpg", + "avatar/images/images/72bf152afd1940b6-puffin.jpg", + "avatar/images/images/7300ddcec637e601-desert-rain-frog.jpg", + "avatar/images/images/730296000d5e2649-submarine.jpg", + "avatar/images/images/733ba2895f3bb7bf-bubble-wyvern.jpg", + "avatar/images/images/7349e53d57596ba0-tiny-yeti.jpg", + "avatar/images/images/734dbc4ed39214ce-starship-acorn.jpg", + "avatar/images/images/736b0636b03e0cd1-leanchoilia.jpg", + "avatar/images/images/736e4530af08c4b1-tapir-7.jpg", + "avatar/images/images/7372dd631353dae0-gerenuk.jpg", + "avatar/images/images/73a1c67a84250822-slide-projector.jpg", + "avatar/images/images/73e4e770315b9c17-elephant-seal.jpg", + "avatar/images/images/73f39f5f8e13f3c4-comet-creature-2.jpg", + "avatar/images/images/745c5114028e3478-cloud-puff-spirit.jpg", + "avatar/images/images/747eab15261c0c0a-lion-sleepy-soft.jpg", + "avatar/images/images/7489231bba5f6a81-megaphone.jpg", + "avatar/images/images/748de12b9a206123-armadillo-2.jpg", + "avatar/images/images/74b5526dea5068cb-salp.jpg", + "avatar/images/images/74dd77f350b5341d-jellyfish-17.jpg", + "avatar/images/images/74deb70b9d110a64-pangolin-7.jpg", + "avatar/images/images/750f5698c0dd52e3-ivory-face-panda.jpg", + "avatar/images/images/75583fc95ed1504a-robot-8.jpg", + "avatar/images/images/756791e29ee7f315-sea-pig-4.jpg", + "avatar/images/images/75d90dea6f29b5e3-baby-goat-cheerful.jpg", + "avatar/images/images/76029942b8681e20-fennec-fox-3.jpg", + "avatar/images/images/76104295576bcb72-wide-mask-red-panda.jpg", + "avatar/images/images/76f3a5ec00518058-phoenix-glaucous-2.jpg", + "avatar/images/images/76f4e084d1451cb4-corkscrew.jpg", + "avatar/images/images/76fe18a6c7572ea3-buttery-croissant.jpg", + "avatar/images/images/7798c0b7c0f4c380-parrot-3.jpg", + "avatar/images/images/779bbbdb559c02cb-analog-voltmeter.jpg", + "avatar/images/images/77a9caf21643a0bf-fox-close-ears-right.jpg", + "avatar/images/images/77f9cce19527a48b-birman-cat-2.jpg", + "avatar/images/images/780f39f1e2be6862-suitcase.jpg", + "avatar/images/images/7813b82881368492-japanese-bobtail-cat.jpg", + "avatar/images/images/7831537aa4a8381e-binturong-5.jpg", + "avatar/images/images/785230fb058d2551-pagoda.jpg", + "avatar/images/images/786ff7bfb925e66f-sphynx-cat.jpg", + "avatar/images/images/787b0a434a92e45a-onion.jpg", + "avatar/images/images/78d60e5975db64c0-rubber-stamp-3.jpg", + "avatar/images/images/78e9f1c35a22713e-turtle-shy-peek.jpg", + "avatar/images/images/7949c05a0e06717a-british-shorthair-kitten.jpg", + "avatar/images/images/795ad688d0869b3d-coral-creature.jpg", + "avatar/images/images/796bbf77017effa4-bright-eyed-owl.jpg", + "avatar/images/images/7a12aac44b4536e0-basset-hound-dog.jpg", + "avatar/images/images/7a1b87ab8a6088b4-observatory-dome.jpg", + "avatar/images/images/7a4692fa9efc24a9-ladle.jpg", + "avatar/images/images/7a58126fb6cfda58-oven-mitt-3.jpg", + "avatar/images/images/7a5935ebf1b4070a-crumpet.jpg", + "avatar/images/images/7a93c58bd013cea9-spirit-level.jpg", + "avatar/images/images/7b14d28e19a2ba43-tuba.jpg", + "avatar/images/images/7b21ecc4b80c832c-miniature-pinscher-dog.jpg", + "avatar/images/images/7b4271c5e71018a2-windmill.jpg", + "avatar/images/images/7b5c817cd0f2c609-submarine-robot.jpg", + "avatar/images/images/7b9bd41c1f800fb8-owl-9.jpg", + "avatar/images/images/7bb3b617d4788437-siberian-musk-deer-2.jpg", + "avatar/images/images/7bba623a4a4001d2-picnic-hamper.jpg", + "avatar/images/images/7bcedfbd11c59d2b-raindrop.jpg", + "avatar/images/images/7c70db889e3ab112-backpack-3.jpg", + "avatar/images/images/7c80f00aadc4b3c0-heart-glider.jpg", + "avatar/images/images/7c93b6b2b1e14526-taco-3.jpg", + "avatar/images/images/7cac7610a208eea0-tea-kettle.jpg", + "avatar/images/images/7d03ca5a86639f00-smooth-cheek-fox.jpg", + "avatar/images/images/7d180caa7098fd89-harmonica.jpg", + "avatar/images/images/7d1873b48077c96d-pig-gentle-smile-2.jpg", + "avatar/images/images/7d1dd5ed33fb1709-badger-4.jpg", + "avatar/images/images/7d2674462b784ca2-pita-pocket.jpg", + "avatar/images/images/7d30480cf81de859-small-ear-raccoon.jpg", + "avatar/images/images/7d982b8f79c08a3d-blanket-octopus.jpg", + "avatar/images/images/7db351d4cfddf7a3-iguana-2.jpg", + "avatar/images/images/7e48cfe0dedb52f1-yellow-duckling.jpg", + "avatar/images/images/7eaebc42e6b56bce-archive-orb.jpg", + "avatar/images/images/7f00b30bc3fb46ef-cavalier-king-charles-spaniel-dog-2.jpg", + "avatar/images/images/7f3aef643a727bf5-bright-eyed-deer.jpg", + "avatar/images/images/7f6b335f59639e37-compact-duckling.jpg", + "avatar/images/images/7fa499966eea1114-submarine-sonar.jpg", + "avatar/images/images/7fb25b03cfcd03ff-cloud-forge.jpg", + "avatar/images/images/7fd2ac41fcfd27fc-horseshoe-magnet-2.jpg", + "avatar/images/images/8007025a339f130b-colorpoint-cat.jpg", + "avatar/images/images/80141ab0317af2f0-baobab-sprout-spirit.jpg", + "avatar/images/images/80959289943a5bc6-classic-red-panda.jpg", + "avatar/images/images/80bd420f2664b0e0-okapi-2.jpg", + "avatar/images/images/80f389cf5b796af4-chihuahua-dog.jpg", + "avatar/images/images/813b08f47f7f0980-rabbit-small-nose-left.jpg", + "avatar/images/images/81b4628804e9e8d4-penguin-calm-round-face-left.jpg", + "avatar/images/images/820d5c6a2f86a340-duckling-3.jpg", + "avatar/images/images/82a1ad10487375b6-hand-plane.jpg", + "avatar/images/images/82d2ba7a09e29d99-octopus-8.jpg", + "avatar/images/images/82da3921a32984cd-jellyfish-13.jpg", + "avatar/images/images/8302695c34027e1e-sky-ferry.jpg", + "avatar/images/images/83535bd5caf7af6b-sugar-glider.jpg", + "avatar/images/images/8371df2cb0d080b7-bold-mask-raccoon.jpg", + "avatar/images/images/83cd21b6f8ba2618-chameleon-6.jpg", + "avatar/images/images/83cee4b8271d8d4b-froe.jpg", + "avatar/images/images/83e8f5ef1d60efb7-toucan-5.jpg", + "avatar/images/images/84456d4dea35e320-soft-charcoal-bear.jpg", + "avatar/images/images/84ad02643a171465-cow-bright-eyed.jpg", + "avatar/images/images/84d73e131408f001-otter-compact-body-left.jpg", + "avatar/images/images/850428a212db7070-squirrel-soft-wide-cheeks-left.jpg", + "avatar/images/images/850779495211f96f-knight.jpg", + "avatar/images/images/852cd32f324680a7-pomeranian-dog-2.jpg", + "avatar/images/images/8530a9f51ca0f3f6-cactus-pad-spirit.jpg", + "avatar/images/images/8551d5dccbc46036-avocado-half.jpg", + "avatar/images/images/85ab4c42d3f46fa2-cassette-player-2.jpg", + "avatar/images/images/85b94803f1ff9032-beluga-whale-2.jpg", + "avatar/images/images/862208f805570876-axolotl-7.jpg", + "avatar/images/images/862fcb9b341e231e-horseshoe-crab-3.jpg", + "avatar/images/images/8630eb090d2b9538-ocicat-inspired-cat-2.jpg", + "avatar/images/images/86ae9a5b9e36eaef-salamander.jpg", + "avatar/images/images/871ed7d609c24229-cassette.jpg", + "avatar/images/images/874e0a8f24ed7783-peach.jpg", + "avatar/images/images/87d5e5ff48ff432c-deer-gentle-small-muzzle-right.jpg", + "avatar/images/images/87e75206cca85016-french-bulldog-2.jpg", + "avatar/images/images/88cc30e86297ef4f-portal-key.jpg", + "avatar/images/images/891b5d077ab9aafc-fern-fiddlehead-2.jpg", + "avatar/images/images/8924830120f63b0e-gentle-penguin.jpg", + "avatar/images/images/8924d09d8e1373c9-raccoon-low-ears-right.jpg", + "avatar/images/images/89268f1a74a4f745-honey-jar.jpg", + "avatar/images/images/892c5829de9cb3a4-sugar-glider-2.jpg", + "avatar/images/images/895a07874deb7473-greater-bilby-2.jpg", + "avatar/images/images/896c1e19033f9d7d-quern-stone.jpg", + "avatar/images/images/89c92419c6cac531-sloth-3.jpg", + "avatar/images/images/89eeaaf5d598345a-tardigrade-3.jpg", + "avatar/images/images/89fcc0a537b2ae23-baby-frog.jpg", + "avatar/images/images/8a11ac38fc22ab19-round-ear-hamster.jpg", + "avatar/images/images/8a518e05ee7a74d0-submarine-5.jpg", + "avatar/images/images/8a51a16b3d157f2f-peryton.jpg", + "avatar/images/images/8a5a66b3ad8858ab-automation-robot.jpg", + "avatar/images/images/8abb603ee4fb138c-fennec-fox.jpg", + "avatar/images/images/8af39044cdb82b8f-triops.jpg", + "avatar/images/images/8b0f65e6b4b25631-mailbox-2.jpg", + "avatar/images/images/8b7af5b234772a58-manta-ray-7.jpg", + "avatar/images/images/8b8d48a0934eb9ae-barn-owl.jpg", + "avatar/images/images/8bf8cdc765eeb7b0-bundt-cake.jpg", + "avatar/images/images/8c0ce13a6e239920-thimble-2.jpg", + "avatar/images/images/8c44885573045df8-baby-wombat.jpg", + "avatar/images/images/8c577cf31a2d399f-gentle-panda.jpg", + "avatar/images/images/8d1ed4cfc436cb8d-newt.jpg", + "avatar/images/images/8dc2e417bdab6a7c-dragonfly-drone.jpg", + "avatar/images/images/8df99079e61f42ee-cassowary.jpg", + "avatar/images/images/8e0de6e387b65f2c-snow-leopard.jpg", + "avatar/images/images/8e7495c85912d335-butterfly.jpg", + "avatar/images/images/8eb79d8a655ba9b5-cat-2.jpg", + "avatar/images/images/8efb8bc3457ba173-cuttlefish-3.jpg", + "avatar/images/images/8f010a1ebc0b41db-sprouting-acorn-2.jpg", + "avatar/images/images/8f565b3ccfd2cd3b-clamp-meter.jpg", + "avatar/images/images/8f6960f8058b5752-mushroom-sage.jpg", + "avatar/images/images/8f837d498bee8fdd-flame-elemental.jpg", + "avatar/images/images/8fbf026a4b435224-padlock-3.jpg", + "avatar/images/images/8fd7cc99c8f52e8c-lotus-seed-pod-2.jpg", + "avatar/images/images/8fde295d85314854-mushroom-6.jpg", + "avatar/images/images/8ff59a2aa52402cb-pale-mask-raccoon.jpg", + "avatar/images/images/909481b69997feb2-dumbo-octopus.jpg", + "avatar/images/images/909d6e7788a642e5-round-face-capybara.jpg", + "avatar/images/images/90aeda766431bd5e-flamingo.jpg", + "avatar/images/images/90e28fc9895a60c5-frost-giant.jpg", + "avatar/images/images/90edec9aad73508f-bison-3.jpg", + "avatar/images/images/90f70f1c29c16649-firefly-deep-twilight-2.jpg", + "avatar/images/images/9116cfd88e186abd-raccoon-7.jpg", + "avatar/images/images/91230d67798895d2-fly-agaric.jpg", + "avatar/images/images/91f87a0c7f046d38-otter.jpg", + "avatar/images/images/9250cda147ac0f32-small-wing-owl.jpg", + "avatar/images/images/92ac70e7b53d2abd-penguin-serene-broad-muzzle-left.jpg", + "avatar/images/images/92de374acaae87aa-pebble-golem-2.jpg", + "avatar/images/images/92f2bd999b24a58d-koala-8.jpg", + "avatar/images/images/9309bb2f767340c2-kitchen-whisk.jpg", + "avatar/images/images/930bb5aacdfae396-paper-airplane-creature.jpg", + "avatar/images/images/9371d449158cb146-shetland-sheepdog.jpg", + "avatar/images/images/93c2a448c9491c06-xylophone-2.jpg", + "avatar/images/images/93e70a18d4f260e6-moose.jpg", + "avatar/images/images/9408733637b05626-sea-otter.jpg", + "avatar/images/images/944787878edeefcb-giraffe-3.jpg", + "avatar/images/images/944fc6e13575a565-sewing-machine-3.jpg", + "avatar/images/images/94692733c1015bc5-panda-content-closed-eyes-left.jpg", + "avatar/images/images/9475a7a24a9257f6-sailboat.jpg", + "avatar/images/images/948843a6321049ab-cloud-navy.jpg", + "avatar/images/images/94b1a9ef488c0847-siberian-musk-deer.jpg", + "avatar/images/images/957b33e5b1acc2b7-classic-gray-seal.jpg", + "avatar/images/images/958afe2579455284-banana-plant-sprout.jpg", + "avatar/images/images/95ac2df4c44fa87d-bollard.jpg", + "avatar/images/images/95bbff297e1512ba-barrel-cactus-pup.jpg", + "avatar/images/images/95bee3479144f51b-yarn-ball.jpg", + "avatar/images/images/95e71ebc45a7747d-moon.jpg", + "avatar/images/images/962fff234544ec6f-coelacanth.jpg", + "avatar/images/images/963415f45fd5528e-momentum-wheel.jpg", + "avatar/images/images/966a5e2f89168be6-pufferfish-2.jpg", + "avatar/images/images/96716d02071b34fd-crab-6.jpg", + "avatar/images/images/967e69b3a5009b87-acoustic-guitar.jpg", + "avatar/images/images/96906a9f22164fa0-tayra.jpg", + "avatar/images/images/96ae84e9da9bfa2d-dark-sea-otter.jpg", + "avatar/images/images/96b6b31023dc97f5-white-cat-2.jpg", + "avatar/images/images/971849ef5cafe242-forge-drone.jpg", + "avatar/images/images/974f79013942be3b-alarm-clock-creature.jpg", + "avatar/images/images/9777fa26a9546560-glass-frog-2.jpg", + "avatar/images/images/97b3236dda36fa9d-eclipse-owl-spirit.jpg", + "avatar/images/images/9805e3e6ac3cbf75-tea-caddy.jpg", + "avatar/images/images/9839d2b3aed49a05-flapjack-octopus-3.jpg", + "avatar/images/images/988758580d784440-donut-2.jpg", + "avatar/images/images/9889d26d4c73193d-astrolabe.jpg", + "avatar/images/images/988eb4054d2412bd-numbat.jpg", + "avatar/images/images/98e0445e9bfa0d96-cassette-cream.jpg", + "avatar/images/images/990d02996c04e29f-colorpoint-cat-2.jpg", + "avatar/images/images/9980377ea24bf075-russian-blue-cat-2.jpg", + "avatar/images/images/99870bf06951bc9f-snowglobe-spirit.jpg", + "avatar/images/images/99a1f98d6e654a33-tullimonstrum.jpg", + "avatar/images/images/99e352516e68f76d-moon-cactus.jpg", + "avatar/images/images/9a0702bb91d1ed4d-chameleon.jpg", + "avatar/images/images/9aa3d228190bf9a4-lotus-spirit.jpg", + "avatar/images/images/9aaa68097991dd29-octopus-5.jpg", + "avatar/images/images/9ab5af528c74e775-canyon-spirit.jpg", + "avatar/images/images/9ae2df4c4926a165-lantern-6.jpg", + "avatar/images/images/9afcb3dcb0d5ec43-birds-nest-fungus.jpg", + "avatar/images/images/9b31974b4cb84d1e-dolphin-4.jpg", + "avatar/images/images/9b3b47a5c767b349-tapir-5.jpg", + "avatar/images/images/9b4cb24a305365c3-bell.jpg", + "avatar/images/images/9be73829b6343503-smooth-cheek-raccoon.jpg", + "avatar/images/images/9c1a0451ad8f995b-coral-2.jpg", + "avatar/images/images/9c248f1d1d2d0837-padlock-guardian.jpg", + "avatar/images/images/9c3e1bc13d49f994-airship.jpg", + "avatar/images/images/9c523789c0f266de-frog-4.jpg", + "avatar/images/images/9c5259475333c8d5-scissors-2.jpg", + "avatar/images/images/9c879a95097f50a3-sprout-2.jpg", + "avatar/images/images/9c9e04955e782569-pufferfish-3.jpg", + "avatar/images/images/9cd87f8a5c78a44b-hermit-crab-4.jpg", + "avatar/images/images/9cf3caf284317d20-leaf-sheep-sea-slug-4.jpg", + "avatar/images/images/9cf63df3ce9392e5-fox-compact-muzzle-left.jpg", + "avatar/images/images/9d1ccb1dece07997-quokka-5.jpg", + "avatar/images/images/9d3db3194e777d87-trivet.jpg", + "avatar/images/images/9dc2e151a484833f-kettle-bot.jpg", + "avatar/images/images/9de7b6f370a772a9-tunicate.jpg", + "avatar/images/images/9e41e08f9668a43b-siberian-husky-dog.jpg", + "avatar/images/images/9ec099e6551af906-moss-golem-2.jpg", + "avatar/images/images/9f43288b69f92a9d-bobtail-squid-2.jpg", + "avatar/images/images/9f7546cd7eeec682-frog.jpg", + "avatar/images/images/9f88dda6a91a00ae-octopus-4.jpg", + "avatar/images/images/9fab3729f2e640f3-raccoon-close-ears-right.jpg", + "avatar/images/images/9fc917da5f9a8f61-brown-bear.jpg", + "avatar/images/images/9fd5dbb36439f764-potters-wheel.jpg", + "avatar/images/images/a01078b9c0e215e1-pangolin-cream.jpg", + "avatar/images/images/a0265d1c789c69c7-rain-puddle-spirit.jpg", + "avatar/images/images/a03aee862af6ab17-pig-bright-eyed.jpg", + "avatar/images/images/a08b34ac762aae81-hatchetfish.jpg", + "avatar/images/images/a0a6770a56474ce1-mushroom-giant.jpg", + "avatar/images/images/a0ae41dae276c0c4-linsang.jpg", + "avatar/images/images/a0b159a74faf49d6-over-ear-headphones.jpg", + "avatar/images/images/a0d2021ada020069-mortar-and-pestle.jpg", + "avatar/images/images/a128ee1e3b8366da-game-controller-4.jpg", + "avatar/images/images/a1756ce2fd379ec9-teapot-6.jpg", + "avatar/images/images/a19d7e95fbdbe7bf-candle.jpg", + "avatar/images/images/a1a2b20a7a502ca2-crystal-sprout.jpg", + "avatar/images/images/a1c6218f8d357091-basilisk-hatchling.jpg", + "avatar/images/images/a20739a5fef52b6b-hammerhead-shark-2.jpg", + "avatar/images/images/a229b582e07ad7c8-cabasa.jpg", + "avatar/images/images/a22db9c9aa1b4007-typewriter-3.jpg", + "avatar/images/images/a2812afc04332221-round-wing-owl.jpg", + "avatar/images/images/a29183e5b6170407-bunny-succulent.jpg", + "avatar/images/images/a2bd1531513a131b-round-ghost.jpg", + "avatar/images/images/a2f18143a8b230a0-elephant-3.jpg", + "avatar/images/images/a2f6f6b3baf42e08-starfish.jpg", + "avatar/images/images/a33a6b7b63a6add9-thimble-4.jpg", + "avatar/images/images/a3724c808d0d583a-tiger-playful.jpg", + "avatar/images/images/a378c4b797975733-colander-2.jpg", + "avatar/images/images/a3aff2748e5aa7fa-cupcake-2.jpg", + "avatar/images/images/a3fa6eb34764e74d-lotus-bud-spirit.jpg", + "avatar/images/images/a40c4671a23acf25-backpack.jpg", + "avatar/images/images/a428236657550fb5-round-face-koala.jpg", + "avatar/images/images/a436a550ebdb2b24-resurrection-plant-2.jpg", + "avatar/images/images/a44877fb118b4263-cacomistle.jpg", + "avatar/images/images/a4d77647a51caab5-jack-russell-terrier-dog.jpg", + "avatar/images/images/a4e8cc9939ded819-chef-hat.jpg", + "avatar/images/images/a5393cd008a7cc7f-duck-white-bg.jpg", + "avatar/images/images/a55bd7270ebbd6d3-persian-cat-2.jpg", + "avatar/images/images/a583d007bc6fb52d-schnauzer-puppy.jpg", + "avatar/images/images/a627df6a454ce913-octopus-deep-twilight.jpg", + "avatar/images/images/a6316adbf8c8419d-moose-2.jpg", + "avatar/images/images/a636f9da1ce11ff3-otter-close-set-eyes-left.jpg", + "avatar/images/images/a6a0407ee6e54878-lanternfly.jpg", + "avatar/images/images/a6ecf8e07bd6a56b-grand-piano.jpg", + "avatar/images/images/a6f5f92aa1606845-balloon-whisk-2.jpg", + "avatar/images/images/a746787047a05c50-quokka-2.jpg", + "avatar/images/images/a76d140cc6cb801c-toaster.jpg", + "avatar/images/images/a7774d80a7f38d95-label-maker-2.jpg", + "avatar/images/images/a7898f290546073c-dung-beetle.jpg", + "avatar/images/images/a79ed011ef3a1bf9-lightbulb-2.jpg", + "avatar/images/images/a7d1df9259bb71ec-dolphin-2.jpg", + "avatar/images/images/a7f9a6dcfc3c9e17-lavender-sprig.jpg", + "avatar/images/images/a814e69fc9966df7-guinea-pig.jpg", + "avatar/images/images/a815509324a9a068-numbat-3.jpg", + "avatar/images/images/a885f371ab6244d1-coral-fungus.jpg", + "avatar/images/images/a88ebc131b49e792-bear-compact-body-left.jpg", + "avatar/images/images/a8a74a9fe1991fe3-narwhal-5.jpg", + "avatar/images/images/a8cac16034e52213-water-tower.jpg", + "avatar/images/images/a8f1557db3d29c1a-harbor-seal-2.jpg", + "avatar/images/images/a920a2fecc2389ea-ring-tailed-coati.jpg", + "avatar/images/images/aa0b522992333eb2-portal-keeper.jpg", + "avatar/images/images/aa1f3a106610b7e9-hedgehog-4.jpg", + "avatar/images/images/aa657346c748bf1d-raccoon-close-set-eyes-left.jpg", + "avatar/images/images/ab0ad64b27100373-potoo.jpg", + "avatar/images/images/ab15b1873e62c84e-peacock.jpg", + "avatar/images/images/ab47ec85b65f48e7-corn-cob.jpg", + "avatar/images/images/ab714148daf8f2cf-white-upright-rabbit.jpg", + "avatar/images/images/ab7da280f4949ef3-goose.jpg", + "avatar/images/images/ac195a1ba19e0a6b-lava-skimmer.jpg", + "avatar/images/images/ac35a529737bb688-fox-small-nose-left.jpg", + "avatar/images/images/ac5bf69496aaa111-jaguarundi.jpg", + "avatar/images/images/ac68eb11603c9e29-exotic-shorthair-cat-2.jpg", + "avatar/images/images/ac7ab8ee27554ed5-munchkin-cat-2.jpg", + "avatar/images/images/ac8bb8d1b47a7e58-brown-upright-rabbit.jpg", + "avatar/images/images/ad21828211ea7fd6-dugong.jpg", + "avatar/images/images/ad83c96f7aed7ed8-caddisfly.jpg", + "avatar/images/images/adec1a30c069ac06-lotus-spirit-3.jpg", + "avatar/images/images/ae031d494530ae77-beagle-puppy.jpg", + "avatar/images/images/ae51717b4411b8c9-cave-crystal-spirit.jpg", + "avatar/images/images/ae5adf8bf17c07dc-gentle-red-panda.jpg", + "avatar/images/images/ae699f5c81e75200-lotus-3.jpg", + "avatar/images/images/ae8f7ff6eaddb819-koi-4.jpg", + "avatar/images/images/aea1c29ff5f472e2-tiger-soft-peek.jpg", + "avatar/images/images/aef17e0b87b9c770-tab-ghost.jpg", + "avatar/images/images/aefc42d30421eb74-library-gargoyle.jpg", + "avatar/images/images/af1767a17cf8f147-bluebell.jpg", + "avatar/images/images/af2415998a5ce062-moss-golem-3.jpg", + "avatar/images/images/af3f81d1b2f639a6-horsetail-shoot.jpg", + "avatar/images/images/af4a0b5d7e661a6a-rambutan.jpg", + "avatar/images/images/afcc019cdd9d0559-manatee-7.jpg", + "avatar/images/images/afd0703f41b744e2-globe.jpg", + "avatar/images/images/afeee038b50f63a1-polar-bear-2.jpg", + "avatar/images/images/b0550e3f6f08f8cc-croissant-4.jpg", + "avatar/images/images/b10ef6cf64469685-deep-exploration-submarine.jpg", + "avatar/images/images/b11073e5d3cca9d4-hand-drill.jpg", + "avatar/images/images/b116ba4d52a60842-dugong-3.jpg", + "avatar/images/images/b1534599159b4853-fox-wide-set-eyes-right.jpg", + "avatar/images/images/b178e7cedcd1e33c-parrot-2.jpg", + "avatar/images/images/b1982a32e98c0956-baby-axolotl.jpg", + "avatar/images/images/b1d797e93e12918f-bumblebee-4.jpg", + "avatar/images/images/b1ddabc3784dd04b-hot-air-balloon.jpg", + "avatar/images/images/b1f5de0a65b53595-comet-creature.jpg", + "avatar/images/images/b20e0d1d494e00f4-roller-skate.jpg", + "avatar/images/images/b230814dc8789355-rotifer.jpg", + "avatar/images/images/b28208770b025ce5-hippo.jpg", + "avatar/images/images/b293458016f1151e-pelican-6.jpg", + "avatar/images/images/b2e409bf8e12109e-lantern-2.jpg", + "avatar/images/images/b33ddef057c1ad34-sea-pig.jpg", + "avatar/images/images/b34ca295453295c6-otter-small-nose-left.jpg", + "avatar/images/images/b3e044b06bfb60d1-challah-loaf.jpg", + "avatar/images/images/b3fcb8470245e35c-classic-gray-raccoon.jpg", + "avatar/images/images/b40267e5ec67aeaf-orangutan-2.jpg", + "avatar/images/images/b4090006582f13ec-sleepy-hamster.jpg", + "avatar/images/images/b4555dfb2cd3808f-toucan-2.jpg", + "avatar/images/images/b47a6169fd3fbf51-whale-terracotta-no-blowhole.jpg", + "avatar/images/images/b4c19baf4ffeb170-panda.jpg", + "avatar/images/images/b4ee9bd95f2f2cbe-happy-otter.jpg", + "avatar/images/images/b4fefd9f4649a7da-elephant-soft-peek.jpg", + "avatar/images/images/b52c89000be27fc9-acorn-oracle.jpg", + "avatar/images/images/b5979142dff4b50e-comet-2.jpg", + "avatar/images/images/b62c2aaabcbd2345-rock-hyrax-2.jpg", + "avatar/images/images/b634d1144ba3e75b-white-hamster.jpg", + "avatar/images/images/b635a24562bb7660-baby-elephant-2.jpg", + "avatar/images/images/b64d0c9b76c28192-miniature-australian-shepherd-dog.jpg", + "avatar/images/images/b6b516e0189a7feb-raccoon-tall-crop-right.jpg", + "avatar/images/images/b70a0b751e6cf4b1-spark-dragon.jpg", + "avatar/images/images/b745d8a40bdf19a4-chihuahua-puppy.jpg", + "avatar/images/images/b7b248f5c5f06f52-caterpillar.jpg", + "avatar/images/images/b809914e65b6cd79-maltese-dog-2.jpg", + "avatar/images/images/b8825dec2a4b3eb6-maltese.jpg", + "avatar/images/images/b882c845a130b545-bottle-opener.jpg", + "avatar/images/images/b8a5e10cd6790ba0-angelfish.jpg", + "avatar/images/images/b8a8cf2692fa9940-fire-hydrant.jpg", + "avatar/images/images/b8f2a6aa9f2dba4a-cream-tabby-cat-2.jpg", + "avatar/images/images/b8fa8d5c022157b3-tiger-curious-peek.jpg", + "avatar/images/images/b93ec8e5798b962d-bear-small-nose-left.jpg", + "avatar/images/images/b94a6417affe7383-accordion-3.jpg", + "avatar/images/images/b94bdf85e8c585b9-grasshopper.jpg", + "avatar/images/images/b95595d2ee7e4807-fluffy-cheek-raccoon.jpg", + "avatar/images/images/b960242cce25e8fd-volt-canoe.jpg", + "avatar/images/images/b9e36df5e8463004-quoll-2.jpg", + "avatar/images/images/ba3d934c08e9544d-sextant-3.jpg", + "avatar/images/images/baa80dc3b8bb5641-gray-owl.jpg", + "avatar/images/images/bad1e272ca41ff71-lion-curious-peek.jpg", + "avatar/images/images/bae34f8ccf5c262d-red-lipped-batfish.jpg", + "avatar/images/images/bae452b148da3aa9-radish.jpg", + "avatar/images/images/bb2abbe25dd23f7c-snail.jpg", + "avatar/images/images/bb2dc8d1d39cb171-seahorse.jpg", + "avatar/images/images/bb7f0af9d7b4f947-raccoon-broad-cheeks-right.jpg", + "avatar/images/images/bbaa31f2bfb62fb9-balinese-cat-2.jpg", + "avatar/images/images/bbfcdfef963bd29b-ginkgo-leaf.jpg", + "avatar/images/images/bbff5426b62f2c9c-small-ear-deer.jpg", + "avatar/images/images/bc2dd8dcd42bee12-penguin-violet-bg.jpg", + "avatar/images/images/bcbc294d7e57bc96-red-panda-6.jpg", + "avatar/images/images/bcc598ffedeece87-rabbit-low-face-left.jpg", + "avatar/images/images/bd01baf4db5b130f-capybara.jpg", + "avatar/images/images/bd28bb5fd5852be8-ram-lime-cream.jpg", + "avatar/images/images/bd3ad25d3b242b69-jelly-alien.jpg", + "avatar/images/images/bd467214d5a5566d-chihuahua.jpg", + "avatar/images/images/be4bad4be293e581-bear-oval-head-left.jpg", + "avatar/images/images/be82364b7c716d3a-pig-round-face.jpg", + "avatar/images/images/bea8826fae9b15c4-brush-tailed-bettong.jpg", + "avatar/images/images/bf4955740a007063-frog-curious-peek.jpg", + "avatar/images/images/bf85ae28a2fbd21d-viscacha.jpg", + "avatar/images/images/bfcc905866bcb7cc-flapjack-octopus.jpg", + "avatar/images/images/bffd03fcaa84f67b-stentor-2.jpg", + "avatar/images/images/c02c543d5f09bf39-cow-sleepy-soft.jpg", + "avatar/images/images/c03216013362e013-ramen-bowl.jpg", + "avatar/images/images/c08b56ace5e933ff-solar-sled.jpg", + "avatar/images/images/c0aaf5d5c8137532-horse.jpg", + "avatar/images/images/c0ab854320fd5b11-hippo-4.jpg", + "avatar/images/images/c0afd3973fc13123-ladybug.jpg", + "avatar/images/images/c124c11060935401-tiny-submarine.jpg", + "avatar/images/images/c15bb016530e0a4c-cell-nucleus.jpg", + "avatar/images/images/c1c42485b2631438-baby-manatee.jpg", + "avatar/images/images/c22bdfd8e98697bc-round-wing-penguin.jpg", + "avatar/images/images/c2390ba8f9c6254a-panda-curator.jpg", + "avatar/images/images/c27578597c41164f-xenophyophore.jpg", + "avatar/images/images/c27dbd153d989567-saxophone-2.jpg", + "avatar/images/images/c2c72508f72b9008-aloe-cluster.jpg", + "avatar/images/images/c2cd710ee22b6692-lotus-robot.jpg", + "avatar/images/images/c360eeface43a4c0-great-dane-dog.jpg", + "avatar/images/images/c36715110466c98f-lemur.jpg", + "avatar/images/images/c386d19bba9d817d-pyrenean-desman.jpg", + "avatar/images/images/c406730676838bff-atlas-moth.jpg", + "avatar/images/images/c4189bd4b57e4172-ice-cream-creature.jpg", + "avatar/images/images/c45878832e0476fd-dark-charcoal-raccoon.jpg", + "avatar/images/images/c45f0df356defb16-jackrabbit.jpg", + "avatar/images/images/c47215c0ec06968d-squirrel-peaceful-tall-crop-left.jpg", + "avatar/images/images/c476762e7b4fc25c-coral-guardian.jpg", + "avatar/images/images/c4a946ea6d913a46-crocodile.jpg", + "avatar/images/images/c533e55ea3c57b60-hand-spinning-wheel.jpg", + "avatar/images/images/c551e39b0a258494-shadow-courier.jpg", + "avatar/images/images/c5665664cd8b54c2-rabbit-wide-muzzle-right.jpg", + "avatar/images/images/c58f8123e9ace027-toaster-2.jpg", + "avatar/images/images/c59def90960fc583-cornish-rex-cat.jpg", + "avatar/images/images/c5aac09b4a3a077b-sea-otter-5.jpg", + "avatar/images/images/c5ae2fd3e98b909b-chef-hat-2.jpg", + "avatar/images/images/c5fe0fe9627e69b8-dragonfly.jpg", + "avatar/images/images/c6121eacb8c815e4-sheep-calm.jpg", + "avatar/images/images/c63a1e5b95a853d1-sleepy-penguin.jpg", + "avatar/images/images/c65ea1ef8c29fdc3-wombat-5.jpg", + "avatar/images/images/c68cedb61a2df229-emperor-penguin.jpg", + "avatar/images/images/c6ad4349e05adbce-markhor-2.jpg", + "avatar/images/images/c6b92511313ef1b7-snail-6.jpg", + "avatar/images/images/c6f2d77a4c8eb291-bottle-tree-sprout-2.jpg", + "avatar/images/images/c6f7da7cc9a06ab0-jellyfish-16.jpg", + "avatar/images/images/c7158c37d5aeefe4-moonstone-mole-fae.jpg", + "avatar/images/images/c7392b55c27f5f8d-maple-seed-fox.jpg", + "avatar/images/images/c76f10e34de9ca43-chipmunk.jpg", + "avatar/images/images/c793b40a19a357fd-scarab-beetle.jpg", + "avatar/images/images/c7b47023b51339f7-spell-wheel.jpg", + "avatar/images/images/c7f23763e5bc49df-desk-lamp-4.jpg", + "avatar/images/images/c8663dc22b208d04-border-collie-dog.jpg", + "avatar/images/images/c886fe2f9cf788a1-maltese-2.jpg", + "avatar/images/images/c8b467169569828e-sleepy-deer.jpg", + "avatar/images/images/c8f10a97710f03d7-lobster-2.jpg", + "avatar/images/images/c901440302ad4d80-metronome-3.jpg", + "avatar/images/images/c92e43b53c90c768-flapjack-octopus-2.jpg", + "avatar/images/images/c94e7c09c754fa89-brioche-loaf.jpg", + "avatar/images/images/c957a75fd86bbe11-seal-8.jpg", + "avatar/images/images/c95ce4076aaa59bf-panda-2.jpg", + "avatar/images/images/c95ec1653cebf0d2-compact-raccoon.jpg", + "avatar/images/images/c9a2848b3b513e3a-chameleon-5.jpg", + "avatar/images/images/c9d69c00ffb5e1b3-heart-reactor.jpg", + "avatar/images/images/c9ef50c52aa8b914-sea-turtle-4.jpg", + "avatar/images/images/c9f9dcebafbf814f-matschies-tree-kangaroo.jpg", + "avatar/images/images/ca0f6d2dcaa9389f-coral-crown-turtle.jpg", + "avatar/images/images/ca37e0ffbf4e7227-manta-ray-10.jpg", + "avatar/images/images/ca51c9ed01fb8b84-fox-large-ears-left.jpg", + "avatar/images/images/ca70ff0893fe8c69-owl-6.jpg", + "avatar/images/images/cab7503248b5a755-koi.jpg", + "avatar/images/images/cae35d993352967d-pig-playful.jpg", + "avatar/images/images/cb4f0bb832e4cb21-cream-belly-penguin.jpg", + "avatar/images/images/cb5c5f0b3d287eb2-honey-jar-2.jpg", + "avatar/images/images/cbcc27c0a5b8f54c-egyptian-mongoose.jpg", + "avatar/images/images/cbd404fa8a1f19d3-tape-dispenser.jpg", + "avatar/images/images/cbf2d04033c6ddeb-orchid-mantis-3.jpg", + "avatar/images/images/cc640a27df3e2b35-pancake-stack.jpg", + "avatar/images/images/cc935eb1a8c1701c-music-box-robot.jpg", + "avatar/images/images/cc986dd2a40269cf-diamond.jpg", + "avatar/images/images/cca33a018dc8edc2-archway.jpg", + "avatar/images/images/ccc229c85164b1a1-mouse-2.jpg", + "avatar/images/images/ccd26d65c5ceb2fd-sanctacaris.jpg", + "avatar/images/images/ccf767f7b46a9339-classic-red-squirrel.jpg", + "avatar/images/images/cd46b63ba62b300a-acorn-sprout.jpg", + "avatar/images/images/cd46b8619ab0e75c-maidenhair-fern.jpg", + "avatar/images/images/cd886929a8b029ef-giant-isopod-2.jpg", + "avatar/images/images/cd8d0a4e29972994-aloe-vera.jpg", + "avatar/images/images/cdb55eff36680fb6-shih-tzu-dog.jpg", + "avatar/images/images/cde26e4da6a1f143-cactus-11.jpg", + "avatar/images/images/cdf370aa00b72dce-mushroom-knight-3.jpg", + "avatar/images/images/ce1e3b5dbafbc574-patagonian-mara.jpg", + "avatar/images/images/ce5486ab60123bef-seedling-nurse-robot.jpg", + "avatar/images/images/ce87f4fd76627d80-saiga-antelope.jpg", + "avatar/images/images/ce99bbf46ed8e363-fox-compact-body-left.jpg", + "avatar/images/images/ced2c7b840614b46-key-2.jpg", + "avatar/images/images/cee01d898bb96e96-pink-fairy-armadillo-2.jpg", + "avatar/images/images/cf2799ac4d1c7527-coral.jpg", + "avatar/images/images/cf27b95b45d48b23-rhinoceros.jpg", + "avatar/images/images/cf33b5ef204d6b6c-lunar-rover-robot.jpg", + "avatar/images/images/cf58138cd530fc40-honey-comet-bear-2.jpg", + "avatar/images/images/cf8270218e3c2057-small-wing-penguin.jpg", + "avatar/images/images/cfc2c76eca93b477-rain-cloud.jpg", + "avatar/images/images/cfd5de479b88ccc7-frilled-shark.jpg", + "avatar/images/images/d035ecf1d36fd178-pebble-golem.jpg", + "avatar/images/images/d056947a568e0703-volcano-sprite.jpg", + "avatar/images/images/d0690ce2ba5daf25-dog-cheek-emphasis-right.jpg", + "avatar/images/images/d0944edc68be5cc9-rock-tumbler.jpg", + "avatar/images/images/d0cfbf11943ed50c-star-nosed-mole-2.jpg", + "avatar/images/images/d0e0597dce531eba-acorn-spirit.jpg", + "avatar/images/images/d11dbec6835deb67-carding-paddle.jpg", + "avatar/images/images/d177e8062e82d8a4-pangolin-8.jpg", + "avatar/images/images/d1b555f27019ea28-camel-spider.jpg", + "avatar/images/images/d1e0e834a0c6ad29-classic-river-otter.jpg", + "avatar/images/images/d1ea54375839c516-muntjac-2.jpg", + "avatar/images/images/d21f13960621f7f0-qilin.jpg", + "avatar/images/images/d258ab023b8794ad-giraffe-sleepy-soft.jpg", + "avatar/images/images/d2af973bfc888dba-pufferfish-7.jpg", + "avatar/images/images/d3135dcbaa5524e3-sheep-bright-eyed.jpg", + "avatar/images/images/d35680b7a2cb1611-coffee-mug-4.jpg", + "avatar/images/images/d36cc4067fcefd19-giraffe-curious-peek.jpg", + "avatar/images/images/d40bca4a3c73f02f-ink-bottle-3.jpg", + "avatar/images/images/d50bdf4b6f6c5424-fiddlehead-fern.jpg", + "avatar/images/images/d51ac789c5862b26-dragonfruit.jpg", + "avatar/images/images/d56ae2c23753535c-lychee.jpg", + "avatar/images/images/d58542f648595483-hog-nosed-skunk.jpg", + "avatar/images/images/d5b1f016aecb7e31-chameleon-2.jpg", + "avatar/images/images/d5b8e7f27da108e0-raccoon-compact-body-left.jpg", + "avatar/images/images/d5d8d72aec39d995-book-spirit.jpg", + "avatar/images/images/d61c28b23d983dd8-rice-cooker-2.jpg", + "avatar/images/images/d62fa299c5b401c4-alarm-clock-magenta.jpg", + "avatar/images/images/d63a4fd752d63c68-wide-cheek-fox.jpg", + "avatar/images/images/d64088a26afe2f66-scottish-fold-cat-2.jpg", + "avatar/images/images/d64e0eebed5a9e01-orange-tabby-cat.jpg", + "avatar/images/images/d693a248deb48948-pepper-mill.jpg", + "avatar/images/images/d6f2293ec693ac1f-elephant-curious-peek.jpg", + "avatar/images/images/d720e801dc1c8ebf-desk-lamp.jpg", + "avatar/images/images/d746b8bc5198fc88-salad-spinner.jpg", + "avatar/images/images/d7716365f66fce20-tortoise.jpg", + "avatar/images/images/d7889ce9ba566fee-koala.jpg", + "avatar/images/images/d7c9d431dfef8b55-anvil.jpg", + "avatar/images/images/d7f4b39238053b19-phantom-tugboat.jpg", + "avatar/images/images/d8171d9e20f404ea-cassette-player.jpg", + "avatar/images/images/d821015948c463ae-addax.jpg", + "avatar/images/images/d836b94d8e4481bc-giraffe-warm-smile.jpg", + "avatar/images/images/d84cc4546c68fa60-leatherworkers-maul.jpg", + "avatar/images/images/d8b7ad9fe9907b4d-smooth-cheek-bear.jpg", + "avatar/images/images/d8eb0c2bb1e807c3-tardigrade.jpg", + "avatar/images/images/d902e65a8079514d-sea-apple.jpg", + "avatar/images/images/d94a89f59b8e59fd-round-face-brown-bear.jpg", + "avatar/images/images/d9856e85c81351de-ram-6.jpg", + "avatar/images/images/d9a9fe8b6372aa0b-vacuum-cleaner.jpg", + "avatar/images/images/d9ff7dfcddd20744-sea-angel-2.jpg", + "avatar/images/images/da0ec5666c5ef292-badger-3.jpg", + "avatar/images/images/da86913c637c7750-radio.jpg", + "avatar/images/images/dae4e057a3af44ec-puffin-5.jpg", + "avatar/images/images/dae5525194c23723-genet.jpg", + "avatar/images/images/db4c12a2bfdc24dc-kalimba.jpg", + "avatar/images/images/db6ca7f9aaff55e6-orca-4.jpg", + "avatar/images/images/db6cfc27b7b6d59b-film-projector.jpg", + "avatar/images/images/db92787197705c6f-bison.jpg", + "avatar/images/images/db9de4e88e97cdd6-bathysphere-robot.jpg", + "avatar/images/images/db9f1ab936c1ecd6-phonograph-horn.jpg", + "avatar/images/images/dbb0244c0f4aebb0-basset-hound.jpg", + "avatar/images/images/dbcef1d975b2759c-calculator-2.jpg", + "avatar/images/images/dbe3f9a9b0d0799d-bubble-phoenix-2.jpg", + "avatar/images/images/dc0a4b8bb9ec7255-chinese-crested-dog.jpg", + "avatar/images/images/dd46604c258debcd-abacus.jpg", + "avatar/images/images/de998d9eee66a705-sourdough-boule.jpg", + "avatar/images/images/dec860aab7c8afbd-ginkgo-leaf-5.jpg", + "avatar/images/images/dee370b9e6198c1e-compact-hamster.jpg", + "avatar/images/images/dee9ca1ad93b326d-void-compass.jpg", + "avatar/images/images/def0049904d69bd4-artichoke.jpg", + "avatar/images/images/df1e67ecd225ccca-gentle-koala.jpg", + "avatar/images/images/df85b238e7350e8f-alpaca-2.jpg", + "avatar/images/images/df9a0b5f546128a9-chicken.jpg", + "avatar/images/images/dfaa1dfd7cce32bc-mole-5.jpg", + "avatar/images/images/e00a996b650552a6-pangolin-6.jpg", + "avatar/images/images/e00fc05e3b85ccfd-curious-penguin.jpg", + "avatar/images/images/e0299eaf778b30a4-frost-spirit.jpg", + "avatar/images/images/e059f867778111cc-dalmatian-inspired-dog.jpg", + "avatar/images/images/e06255d34bf65a92-bookend.jpg", + "avatar/images/images/e06e17016e2a7363-whale-3.jpg", + "avatar/images/images/e07b23719623bdef-cloud.jpg", + "avatar/images/images/e09a45bae626216b-mitochondrion.jpg", + "avatar/images/images/e0b7d7eda25f9f02-alpaca-3.jpg", + "avatar/images/images/e0be61024c265dc4-raincloud-sprite-2.jpg", + "avatar/images/images/e0da4ede308f86b7-ember-observatory.jpg", + "avatar/images/images/e118c09851972473-british-shorthair-cat-2.jpg", + "avatar/images/images/e11cb136ae8e8d8c-monstera.jpg", + "avatar/images/images/e11f9fafb573b261-tabletop-globe.jpg", + "avatar/images/images/e12f4e78f3478e23-pit-bull-type-dog.jpg", + "avatar/images/images/e157290ce7fe2913-mandrake-sprout-2.jpg", + "avatar/images/images/e157a01322d0bbd8-cassette-tape-2.jpg", + "avatar/images/images/e1e46118fc556d96-phoenix-violin.jpg", + "avatar/images/images/e233a40e7b063d80-star-forge.jpg", + "avatar/images/images/e2bfd26097eb4574-teacup-chimera.jpg", + "avatar/images/images/e3062372dc8ac007-ram-4.jpg", + "avatar/images/images/e3269606a9ccf0e9-pine-sapling.jpg", + "avatar/images/images/e3af5acdc26ad09c-ice-cream-sandwich.jpg", + "avatar/images/images/e3b5ab5ae6e1e941-starfish-mage.jpg", + "avatar/images/images/e3bfdb2472ea4aec-elephant.jpg", + "avatar/images/images/e3cf643c3aed070d-koi-3.jpg", + "avatar/images/images/e3e58a0fd0bf8442-plunger.jpg", + "avatar/images/images/e4078d02e4f701e4-chameleon-burgundy.jpg", + "avatar/images/images/e427b447008cffda-crab-4.jpg", + "avatar/images/images/e46226ed23305e77-grain-winnowing-basket.jpg", + "avatar/images/images/e4a814eb9f5c8bf6-fluffy-cream-rabbit.jpg", + "avatar/images/images/e4d66da1f950dbea-sneaker.jpg", + "avatar/images/images/e4e26e3b6f9e32d4-snowshoe-cat-2.jpg", + "avatar/images/images/e519e41d449cdd8a-brown-lop-rabbit.jpg", + "avatar/images/images/e528db91b14a5dd4-printing-brayer.jpg", + "avatar/images/images/e5305b77fff3941f-moth-plum.jpg", + "avatar/images/images/e553e725b63c7fb0-mythic-headphones.jpg", + "avatar/images/images/e5577ae1a5ca68d8-gray-lop-rabbit.jpg", + "avatar/images/images/e558a98ecd49b02c-oracle-camera.jpg", + "avatar/images/images/e5710e07d8a22664-yapok.jpg", + "avatar/images/images/e59c5a1c29701485-chocolate-chip-cookie.jpg", + "avatar/images/images/e5dd4f1261967572-rabbit-wide-set-eyes-right.jpg", + "avatar/images/images/e60557e50b79116b-erhu.jpg", + "avatar/images/images/e61463acffed19d4-rainbow-elemental.jpg", + "avatar/images/images/e6a91772c29e4d38-starlight-ram-2.jpg", + "avatar/images/images/e6b18946a05f91c6-rabbit-large-ears-left.jpg", + "avatar/images/images/e6edc53fd373fb40-pig-shy-peek.jpg", + "avatar/images/images/e6fb22a1f5a7245c-sea-otter-2.jpg", + "avatar/images/images/e764e781d531dd17-galaxy-wheelbarrow.jpg", + "avatar/images/images/e7840b9cf5fb7766-yorkshire-terrier.jpg", + "avatar/images/images/e78ca4f37297fb10-bumblebee-3.jpg", + "avatar/images/images/e78f1ce8a68c40cf-clarinet.jpg", + "avatar/images/images/e7b3e5258b77c3fb-maned-wolf-pup.jpg", + "avatar/images/images/e7c144bccc888a52-otter-broad-forehead-right.jpg", + "avatar/images/images/e7f7d2cd52b7dcbf-abacus-3.jpg", + "avatar/images/images/e8567c804d3dcc2c-artichoke-2.jpg", + "avatar/images/images/e94eea7972abfc77-baby-dragon.jpg", + "avatar/images/images/e9a9c4cec7a28e87-jellyfish-9.jpg", + "avatar/images/images/e9cda38d933ec33f-pseudoscorpion.jpg", + "avatar/images/images/ea05bd706504d78a-ghost-pipe-flower.jpg", + "avatar/images/images/ea1652e0479c3c3f-capybara-16.jpg", + "avatar/images/images/ea79d3bc4d2eab93-microscope-3.jpg", + "avatar/images/images/eaebf22318ae5c46-moon-jellyfish.jpg", + "avatar/images/images/eb58ec3ce0f42a5e-bubble-phoenix.jpg", + "avatar/images/images/ebae49671f83ff8b-serow-2.jpg", + "avatar/images/images/ebfe7669fb2afe1f-snakefly.jpg", + "avatar/images/images/ec302e1712885b15-oyster-mushroom.jpg", + "avatar/images/images/ec3990a6cc444991-cloud-weaver-robot.jpg", + "avatar/images/images/ec79217ea4bddb9c-platypus.jpg", + "avatar/images/images/ec98fb407f3949b3-bench-vise.jpg", + "avatar/images/images/ec991fbe51d26ad3-birman-cat.jpg", + "avatar/images/images/ecbb0696f4a72452-axolotl-9.jpg", + "avatar/images/images/edf196f80674efbf-pangolin-9.jpg", + "avatar/images/images/ee1f5b8c0c7eddaa-sichuan-takin-2.jpg", + "avatar/images/images/ee5725d6f2ed25e6-chubby-owl.jpg", + "avatar/images/images/ee6f10e0b523f4e7-whale-fog-blue.jpg", + "avatar/images/images/ee73e412645437c7-covered-bridge.jpg", + "avatar/images/images/ef3aba9aaaa33d79-velvet-mite.jpg", + "avatar/images/images/ef8fe8f964d8b4a7-lotus-spirit-2.jpg", + "avatar/images/images/efd371ef2a773cb3-navy-owl.jpg", + "avatar/images/images/f03eff1d965f5a3b-river-stone-golem.jpg", + "avatar/images/images/f061b240887653cf-harbor-seal-4.jpg", + "avatar/images/images/f086d018809de4d8-capuchin.jpg", + "avatar/images/images/f0cbf2835e994a59-honey-fungus.jpg", + "avatar/images/images/f0e431b3afd59bb5-taco-4.jpg", + "avatar/images/images/f1136fd60ffcf836-beaver-4.jpg", + "avatar/images/images/f19ea56503129031-australian-cattle-dog.jpg", + "avatar/images/images/f1bd273aaf6b409f-peach-2.jpg", + "avatar/images/images/f1cd02aab13b7439-living-card-stack.jpg", + "avatar/images/images/f216e4f2d5d360fa-acorn.jpg", + "avatar/images/images/f21b1e99d27e97e9-dormouse.jpg", + "avatar/images/images/f2767deea661a472-cupcake-4.jpg", + "avatar/images/images/f2878c3ccaa6b7d5-cow-shy-peek.jpg", + "avatar/images/images/f29187b9998b8b0d-bento-box.jpg", + "avatar/images/images/f29838dc0c17c7aa-siamese-cat-3.jpg", + "avatar/images/images/f2af7017bc7f75aa-cipher-mothership.jpg", + "avatar/images/images/f2c0721162162797-aardwolf.jpg", + "avatar/images/images/f33992f29a37cdc7-cactus-oracle.jpg", + "avatar/images/images/f37ad3dd63c7f724-labrador-retriever-dog-2.jpg", + "avatar/images/images/f3ac3f4a40603b1d-koala-6.jpg", + "avatar/images/images/f3bf6c1d4cc26cfd-baby-goat.jpg", + "avatar/images/images/f3d69cb8cf95137a-dragonfly-3.jpg", + "avatar/images/images/f3f9f1f559233577-monstera-leaf-spirit.jpg", + "avatar/images/images/f3fd69f537b4fab8-mole-3.jpg", + "avatar/images/images/f43bce7ca8c4b45c-otter-4.jpg", + "avatar/images/images/f49f628c5276f124-pangolin-cream-2.jpg", + "avatar/images/images/f4f3ce5cdeb98598-decorator-crab-2.jpg", + "avatar/images/images/f57f4d00ca1bbfa0-dog-single-face-patch-right.jpg", + "avatar/images/images/f5829e75c522e974-book-2.jpg", + "avatar/images/images/f5edaad0ec82978f-manatee-10.jpg", + "avatar/images/images/f64f4d758f4d30a8-friendly-satellite.jpg", + "avatar/images/images/f65771726dba2716-pug-dog.jpg", + "avatar/images/images/f68dc34b341362a2-snowshoe-cat.jpg", + "avatar/images/images/f73af8a7b91f11e1-yak-3.jpg", + "avatar/images/images/f78b89199c6d517e-key.jpg", + "avatar/images/images/f79a2ae559627ea2-dog-compact-body-right.jpg", + "avatar/images/images/f7a6e57228779291-sonar-robot.jpg", + "avatar/images/images/f7adb1d2c6259b73-aloe-pup-spirit.jpg", + "avatar/images/images/f7d872012bd3eebf-brown-hamster.jpg", + "avatar/images/images/f7ebe604d18c9794-rabbit-round-face-left.jpg", + "avatar/images/images/f7f76243224fbd58-harmony-tuning-fork.jpg", + "avatar/images/images/f82c27df4a19f54c-silky-anteater-2.jpg", + "avatar/images/images/f82c2d3a6cc13c60-mushroom-knight.jpg", + "avatar/images/images/f8395faa2bef4996-sawfish.jpg", + "avatar/images/images/f889ba567a954819-mountain-viscacha.jpg", + "avatar/images/images/f91201f81ed633b7-whale-17.jpg", + "avatar/images/images/f9222ea9a5d1b55d-manatee-9.jpg", + "avatar/images/images/f928b47dcc573b27-tractor.jpg", + "avatar/images/images/f9a810784ef71fb2-bear-close-set-eyes-left.jpg", + "avatar/images/images/fa02d12353b9ae69-giant-panda-2.jpg", + "avatar/images/images/fa2e38a31674990e-sushi-roll-3.jpg", + "avatar/images/images/fa6531ec05aecd30-binoculars-2.jpg", + "avatar/images/images/fb0ddb306a083830-pumpkin-2.jpg", + "avatar/images/images/fb3c56899934ddcf-corgi-3.jpg", + "avatar/images/images/fb43a24d4c561251-croissant-3.jpg", + "avatar/images/images/fc42d17ef748fa60-owl-2.jpg", + "avatar/images/images/fcd969321ce9662f-sichuan-takin.jpg", + "avatar/images/images/fce40d0369f7533d-french-bulldog-puppy.jpg", + "avatar/images/images/fd4d7fce196e3252-watering-can-7.jpg", + "avatar/images/images/fd4f63cbf1f97535-coral-griffin.jpg", + "avatar/images/images/fd63ea69f45a0d92-southern-gerenuk-2.jpg", + "avatar/images/images/fdc1aa5287b30bae-danish-pastry.jpg", + "avatar/images/images/fdd8255fa470f7b2-frog-5.jpg", + "avatar/images/images/fde3b977810e9fda-solenodon-2.jpg", + "avatar/images/images/fe2675659d8da373-a2-insight-owl.jpg", + "avatar/images/images/fea762c1852010d1-tulip-bud.jpg", + "avatar/images/images/fec792b4d4c25bac-calico-inspired-cat.jpg", + "avatar/images/images/ff759a49938aab76-otter-low-face-left.jpg", + "avatar/images/images/ff93946ab72d3a42-lion.jpg", + "avatar/images/images/ff9a2f640f72b7ec-shiba-inu-dog.jpg" +] \ No newline at end of file