From cbe0d03357ca1c69d1f6bbffcd463117be55afe3 Mon Sep 17 00:00:00 2001 From: Pine Date: Wed, 26 Aug 2026 11:59:54 +0800 Subject: [PATCH] =?UTF-8?q?feat(admission):=20=E5=9B=AD=E5=8C=BA=E5=85=A5?= =?UTF-8?q?=E9=A9=BB=E7=94=B3=E8=AF=B7=20=E2=80=94=E2=80=94=20park=5Fadmis?= =?UTF-8?q?sions=E8=A1=A8+=E8=AE=AD=E7=BB=83=E5=AD=90=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E7=AB=AF=E7=82=B9+=E4=B8=8A=E4=BC=A0=E6=94=BE=E5=AE=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 模型迁移0012 park_admissions(tenant/user/form_json/docs_json/status) - app/training: GET /api/park/tenants(公开) / POST /api/park-admission / mine / 运营列表+review - /api/upload 放宽到 图片/pdf/doc,20MB --- alembic/versions/0012_park_admissions.py | 41 ++++++++ app/infrastructure/models.py | 27 +++++ app/training/main.py | 127 ++++++++++++++++++++++- 3 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 alembic/versions/0012_park_admissions.py diff --git a/alembic/versions/0012_park_admissions.py b/alembic/versions/0012_park_admissions.py new file mode 100644 index 0000000..7321df2 --- /dev/null +++ b/alembic/versions/0012_park_admissions.py @@ -0,0 +1,41 @@ +"""park_admissions 表(园区入驻申请) + +Revision ID: 0012_park_admissions +Revises: 0011_unique_user_identifiers +Create Date: 2026-08-26 +""" +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op + +revision = "0012_park_admissions" +down_revision = "0011_unique_user_identifiers" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "park_admissions", + sa.Column("id", sa.String(), primary_key=True), + sa.Column("tenant_id", sa.String(), default="", index=True), + sa.Column("tenant_name", sa.String(), default=""), + sa.Column("user_id", sa.String(), default="", index=True), + sa.Column("username", sa.String(), default=""), + sa.Column("applicant_name", sa.String(), default=""), + sa.Column("contact_phone", sa.String(), default=""), + sa.Column("status", sa.String(), default="pending"), + sa.Column("form_json", sa.Text(), default="{}"), + sa.Column("docs_json", sa.Text(), default="{}"), + sa.Column("source", sa.String(), default="miniprogram"), + sa.Column("review_comment", sa.String(), default=""), + sa.Column("reviewed_by", sa.String(), default=""), + sa.Column("reviewed_at", sa.String(), default=""), + sa.Column("created_at", sa.String(), default=""), + sa.Column("updated_at", sa.String(), default=""), + ) + + +def downgrade() -> None: + op.drop_table("park_admissions") diff --git a/app/infrastructure/models.py b/app/infrastructure/models.py index b6bbe12..88b7051 100644 --- a/app/infrastructure/models.py +++ b/app/infrastructure/models.py @@ -838,6 +838,33 @@ class ParkSetting(Base): updated_at: Mapped[str] = mapped_column(String, default="") +class ParkAdmission(Base): + """园区入驻申请(C端 小程序/网页提交,园区/运营端审核)。 + + 表单字段以 JSON 存入 form_json(申请人/企业/项目/入驻需求), + 上传资料 URL 存 docs_json(创业计划书/身份证/学历/执照/资质等)。 + """ + + __tablename__ = "park_admissions" + + id: Mapped[str] = mapped_column(String, primary_key=True) + tenant_id: Mapped[str] = mapped_column(String, default="") # 所选园区 id + tenant_name: Mapped[str] = mapped_column(String, default="") # 冗余园区名(展示) + user_id: Mapped[str] = mapped_column(String, default="", index=True) # 申请人平台用户 id + username: Mapped[str] = mapped_column(String, default="") # 申请人登录名 + applicant_name: Mapped[str] = mapped_column(String, default="") # 申请人姓名(快速列表展示) + contact_phone: Mapped[str] = mapped_column(String, default="") # 联系电话(列表展示) + status: Mapped[str] = mapped_column(String, default="pending") # pending/reviewing/approved/rejected + form_json: Mapped[str] = mapped_column(Text, default="{}") # 结构化表单(申请人/企业/项目/需求) + docs_json: Mapped[str] = mapped_column(Text, default="{}") # 上传资料 URL 集合 + source: Mapped[str] = mapped_column(String, default="miniprogram") # miniprogram|web + review_comment: Mapped[str] = mapped_column(String, default="") + reviewed_by: Mapped[str] = mapped_column(String, default="") + reviewed_at: Mapped[str] = mapped_column(String, default="") + created_at: Mapped[str] = mapped_column(String, default="") + updated_at: Mapped[str] = mapped_column(String, default="") + + class ParkScreen(Base): __tablename__ = "park_screens" id: Mapped[str] = mapped_column(String, primary_key=True) diff --git a/app/training/main.py b/app/training/main.py index 9c860ae..a127252 100644 --- a/app/training/main.py +++ b/app/training/main.py @@ -947,14 +947,133 @@ async def upload(request: Request, file: UploadFile = File(...), authorization: if not file.filename: raise HTTPException(400, "未选择文件") ext = os.path.splitext(file.filename or "")[1].lower() - if ext not in (".jpg", ".jpeg", ".png", ".webp", ".gif"): - raise HTTPException(400, "仅支持 jpg/png/webp/gif 图片") + if ext not in (".jpg", ".jpeg", ".png", ".webp", ".gif", ".pdf", ".doc", ".docx"): + raise HTTPException(400, "仅支持 图片 / pdf / word 文件") fname = f"up_{int(time.time())}_{os.urandom(4).hex()}{ext}" dest = os.path.join(UPLOAD_DIR, fname) content = await file.read() - if len(content) > 5 * 1024 * 1024: - raise HTTPException(400, "文件过大(>5MB)") + if len(content) > 20 * 1024 * 1024: + raise HTTPException(400, "文件过大(>20MB)") with open(dest, "wb") as f: f.write(content) url = f"/uploads/{fname}" return {"ok": True, "url": abs_url(url)} + + +# ================= 园区入驻申请 ================= + +def _park_tenants_public(): + """平台可见园区列表(C端选园区用):id/name/intro。""" + out = [] + for t in db.list_all("park_tenants"): + try: + intro = json.loads(t.get("intro_json") or "[]") + except Exception: + intro = [] + if not isinstance(intro, list): + intro = [] + out.append({"id": t.get("id"), "name": t.get("name"), "intro": intro}) + return out + + +def _admission_view(row: dict) -> dict: + v = dict(row) + try: + v["form"] = json.loads(v.get("form_json") or "{}") + except Exception: + v["form"] = {} + try: + v["docs"] = json.loads(v.get("docs_json") or "{}") + except Exception: + v["docs"] = {} + v.pop("form_json", None) + v.pop("docs_json", None) + return v + + +@app.get("/api/park/tenants") +async def park_tenants_public(): + """选择园区:平台可见园区列表(公开,C端选园区)。""" + return {"ok": True, "tenants": _park_tenants_public()} + + +@app.post("/api/park-admission") +async def park_admission_submit(request: Request, authorization: str = Header(default="")): + """提交园区入驻申请(需登录)。form=申请人/企业/项目/需求,docs=上传资料URL集合。""" + auth = require_auth(authorization) + body = await request.json() + tenant_id = str(body.get("tenant_id") or "").strip() + if not tenant_id: + raise HTTPException(400, "请选择意向园区") + tenant = db.fetch_one("park_tenants", id=tenant_id) + if not tenant: + raise HTTPException(404, "园区不存在") + form = body.get("form") if isinstance(body.get("form"), dict) else {} + docs = body.get("docs") if isinstance(body.get("docs"), dict) else {} + u = _current_user(auth) + now = now_iso() + rec = { + "id": f"adm_{secrets.token_hex(8)}", + "tenant_id": tenant_id, + "tenant_name": tenant.get("name", ""), + "user_id": str(auth.get("user_id") or (u.get("id") if u else "") or ""), + "username": auth.get("username", ""), + "applicant_name": str(form.get("name") or (u.get("nickname") if u else "") or ""), + "contact_phone": str(form.get("contact_phone") or (u.get("phone") if u else "") or ""), + "status": "pending", + "form_json": json.dumps(form, ensure_ascii=False), + "docs_json": json.dumps(docs, ensure_ascii=False), + "source": (request.headers.get("X-Client", "") or "miniprogram"), + "review_comment": "", "reviewed_by": "", "reviewed_at": "", + "created_at": now, "updated_at": now, + } + db.insert("park_admissions", rec) + return {"ok": True, "id": rec["id"], "status": "pending"} + + +@app.get("/api/park-admission/mine") +async def park_admission_mine(authorization: str = Header(default="")): + """我的入驻申请列表。""" + auth = require_auth(authorization) + mine = [] + for i in db.list_all("park_admissions"): + if str(i.get("user_id")) == str(auth.get("user_id")) or str(i.get("username")) == str(auth.get("username")): + mine.append(_admission_view(i)) + mine.sort(key=lambda x: x.get("created_at", ""), reverse=True) + return {"ok": True, "items": mine} + + +@app.get("/api/park-admission") +async def park_admission_list(authorization: str = Header(default="")): + """运营方:全部入驻申请(审核列表)。""" + auth = require_auth(authorization) + u = _current_user(auth) + if not u or u.get("role") != "operator": + raise HTTPException(403, "仅运营方可查看") + items = [_admission_view(i) for i in db.list_all("park_admissions")] + items.sort(key=lambda x: x.get("created_at", ""), reverse=True) + return {"ok": True, "items": items} + + +@app.post("/api/park-admission/{aid}/review") +async def park_admission_review(aid: str, request: Request, authorization: str = Header(default="")): + """运营方:审核入驻申请(approved/rejected/reviewing + 意见)。""" + auth = require_auth(authorization) + u = _current_user(auth) + if not u or u.get("role") != "operator": + raise HTTPException(403, "仅运营方可审核") + target = db.fetch_by_id("park_admissions", aid) + if not target: + raise HTTPException(404, "申请不存在") + body = await request.json() + status = str(body.get("status") or "") + if status not in ("approved", "rejected", "reviewing"): + raise HTTPException(400, "status 需为 approved/rejected/reviewing") + db.update_row("park_admissions", aid, { + "status": status, + "review_comment": str(body.get("comment", "")), + "reviewed_by": auth.get("username", ""), + "reviewed_at": now_iso(), + "updated_at": now_iso(), + }) + return {"ok": True, "id": aid, "status": status}