feat(admission): 园区入驻申请 —— park_admissions表+训练子应用端点+上传放宽

- 模型迁移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
This commit is contained in:
Pine
2026-08-26 11:59:54 +08:00
parent 32ca580a06
commit cbe0d03357
3 changed files with 191 additions and 4 deletions
+41
View File
@@ -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")
+27
View File
@@ -838,6 +838,33 @@ class ParkSetting(Base):
updated_at: Mapped[str] = mapped_column(String, default="") 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): class ParkScreen(Base):
__tablename__ = "park_screens" __tablename__ = "park_screens"
id: Mapped[str] = mapped_column(String, primary_key=True) id: Mapped[str] = mapped_column(String, primary_key=True)
+123 -4
View File
@@ -947,14 +947,133 @@ async def upload(request: Request, file: UploadFile = File(...), authorization:
if not file.filename: if not file.filename:
raise HTTPException(400, "未选择文件") raise HTTPException(400, "未选择文件")
ext = os.path.splitext(file.filename or "")[1].lower() ext = os.path.splitext(file.filename or "")[1].lower()
if ext not in (".jpg", ".jpeg", ".png", ".webp", ".gif"): if ext not in (".jpg", ".jpeg", ".png", ".webp", ".gif", ".pdf", ".doc", ".docx"):
raise HTTPException(400, "仅支持 jpg/png/webp/gif 图片") raise HTTPException(400, "仅支持 图片 / pdf / word 文件")
fname = f"up_{int(time.time())}_{os.urandom(4).hex()}{ext}" fname = f"up_{int(time.time())}_{os.urandom(4).hex()}{ext}"
dest = os.path.join(UPLOAD_DIR, fname) dest = os.path.join(UPLOAD_DIR, fname)
content = await file.read() content = await file.read()
if len(content) > 5 * 1024 * 1024: if len(content) > 20 * 1024 * 1024:
raise HTTPException(400, "文件过大(>5MB") raise HTTPException(400, "文件过大(>20MB")
with open(dest, "wb") as f: with open(dest, "wb") as f:
f.write(content) f.write(content)
url = f"/uploads/{fname}" url = f"/uploads/{fname}"
return {"ok": True, "url": abs_url(url)} 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}