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:
@@ -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")
|
||||
@@ -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)
|
||||
|
||||
+123
-4
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user