From ece9dcc7eebf7809f83a14214a942923dfa87458 Mon Sep 17 00:00:00 2001 From: Pine Date: Tue, 1 Sep 2026 22:31:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(incubator):=20=E5=9B=AD=E5=8C=BA=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE+=E4=BC=81=E4=B8=9A/=E6=88=90=E5=91=98=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E8=87=AA=E5=8A=A8=E8=AF=86=E5=88=AB=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 IncubatorSetting 模型与迁移 0042:开放区域说明/申请注意事项/月报说明 - GET/PUT /incubator/settings(管理方维护,提交方只读) - GET /incubator/company(提交方企业资料,月报/申请表单自动带出) - GET /incubator/company/members(企业绑定成员,联系人自动填入) --- alembic/versions/0042_incubator_settings.py | 34 ++++++ app/incubator/routers.py | 56 +++++++++ app/incubator/service.py | 122 ++++++++++++++++++++ app/infrastructure/models.py | 18 +++ 4 files changed, 230 insertions(+) create mode 100644 alembic/versions/0042_incubator_settings.py diff --git a/alembic/versions/0042_incubator_settings.py b/alembic/versions/0042_incubator_settings.py new file mode 100644 index 0000000..5a22298 --- /dev/null +++ b/alembic/versions/0042_incubator_settings.py @@ -0,0 +1,34 @@ +"""incubator settings (园区级设置) + +Revision ID: 0042_incubator_settings +Revises: 0041_direct_messages +Create Date: 2026-09-01 +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +revision: str = "0042_incubator_settings" +down_revision: Union[str, None] = "0041_direct_messages" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "incubator_settings", + sa.Column("id", sa.String(), primary_key=True), + sa.Column("tenant_id", sa.String(), nullable=False, index=True), + sa.Column("reservation_notice", sa.Text(), nullable=False, default=""), + sa.Column("report_notice", sa.Text(), nullable=False, default=""), + sa.Column("open_summary", sa.Text(), nullable=False, default=""), + sa.Column("updated_by", sa.String(), nullable=False, default=""), + sa.Column("updated_at", sa.String(), nullable=False, default=""), + sa.UniqueConstraint("tenant_id", name="uq_incubator_setting_tenant"), + ) + + +def downgrade() -> None: + op.drop_table("incubator_settings") diff --git a/app/incubator/routers.py b/app/incubator/routers.py index ddd401d..73f29b7 100644 --- a/app/incubator/routers.py +++ b/app/incubator/routers.py @@ -104,6 +104,12 @@ class ReviewReservationRequest(BaseModel): record_handler: str = "" # 备案经办人 +class SettingsUpdateRequest(BaseModel): + reservation_notice: str = "" # 共享区域申请注意事项/须知 + report_notice: str = "" # 月报填报说明 + open_summary: str = "" # 开放区域总览说明 + + # --------------------------------------------------------------------------- # 公共:身份 / 概览 # --------------------------------------------------------------------------- @@ -482,3 +488,53 @@ async def incubator_finish_reservation( raise HTTPException(status_code=403, detail=str(exc)) from exc except ValueError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc + + +@router.get("/settings", summary="园区设置(注意事项/开放说明/月报说明)") +async def incubator_get_settings( + db: Database = Depends(get_db), + user: dict = Depends(get_current_user), +): + ident = await resolve_identity(db, user) + if not ident.is_manager and not ident.company_id: + raise HTTPException(status_code=403, detail="当前账号不属于昆明市大学生创业园,无法使用本应用") + return await service.get_settings(db, ident) + + +@router.put("/settings", summary="更新园区设置(管理方)") +async def incubator_update_settings( + body: SettingsUpdateRequest, + db: Database = Depends(get_db), + user: dict = Depends(get_current_user), +): + ident = await resolve_identity(db, user) + try: + return await service.update_settings(db, ident, body.model_dump()) + except LookupError as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + except PermissionError as exc: + raise HTTPException(status_code=403, detail=str(exc)) from exc + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + +@router.get("/company", summary="提交方企业基本信息(表单自动识别填入)") +async def incubator_company_profile( + db: Database = Depends(get_db), + user: dict = Depends(get_current_user), +): + ident = await resolve_identity(db, user) + if not ident.is_manager and not ident.company_id: + raise HTTPException(status_code=403, detail="当前账号不属于昆明市大学生创业园,无法使用本应用") + return await service.get_company_profile(db, ident) + + +@router.get("/company/members", summary="企业绑定成员列表(联系人自动填入)") +async def incubator_company_members( + db: Database = Depends(get_db), + user: dict = Depends(get_current_user), +): + ident = await resolve_identity(db, user) + if not ident.is_manager and not ident.company_id: + raise HTTPException(status_code=403, detail="当前账号不属于昆明市大学生创业园,无法使用本应用") + return await service.list_company_members(db, ident) diff --git a/app/incubator/service.py b/app/incubator/service.py index 5d9b31c..261e2fa 100644 --- a/app/incubator/service.py +++ b/app/incubator/service.py @@ -25,9 +25,11 @@ from ..infrastructure.models import ( CompanyMember, IncubatorAreaReservation, IncubatorMonthlyReport, + IncubatorSetting, IncubatorSharedArea, ParkCompany, ParkTenant, + User, ) from ..infrastructure.repositories import utcnow_iso @@ -655,3 +657,123 @@ async def finish_reservation(db, identity: Identity, rsv_id: str) -> dict: r.updated_at = _now() await db.session.commit() return _rsv_row(r) + + +# =========================================================================== +# 园区设置(开放区域说明 / 申请注意事项 / 月报说明) +# =========================================================================== +def _setting_row(r: IncubatorSetting | None) -> dict: + return { + "reservation_notice": (r.reservation_notice if r else ""), + "report_notice": (r.report_notice if r else ""), + "open_summary": (r.open_summary if r else ""), + "updated_by": (r.updated_by if r else ""), + "updated_at": (r.updated_at if r else ""), + } + + +async def get_settings(db, identity: Identity) -> dict: + """园区级设置:管理方与提交方都可读(管理方见空则给默认引导文案)。""" + require_identity(identity) + r = (await db.session.execute( + select(IncubatorSetting).where( + IncubatorSetting.tenant_id == identity.tenant_id) + )).scalar_one_or_none() + return _setting_row(r) + + +async def update_settings(db, identity: Identity, data: dict) -> dict: + """更新园区级设置(管理方)。""" + if not identity.is_manager: + raise PermissionError("仅管理方可维护园区设置") + r = (await db.session.execute( + select(IncubatorSetting).where( + IncubatorSetting.tenant_id == identity.tenant_id) + )).scalar_one_or_none() + if r is None: + r = IncubatorSetting(id=f"set_{secrets.token_hex(8)}", + tenant_id=identity.tenant_id) + db.session.add(r) + if "reservation_notice" in data and data["reservation_notice"] is not None: + r.reservation_notice = str(data["reservation_notice"]) + if "report_notice" in data and data["report_notice"] is not None: + r.report_notice = str(data["report_notice"]) + if "open_summary" in data and data["open_summary"] is not None: + r.open_summary = str(data["open_summary"]) + r.updated_by = identity.user_id + r.updated_at = _now() + await db.session.commit() + return _setting_row(r) + + +# =========================================================================== +# 企业基本信息 / 绑定成员(表单自动识别填入) +# =========================================================================== +async def get_company_profile(db, identity: Identity) -> dict: + """提交方企业资料:名称/区域/单元/法人/联系人/地址/行业等(申请与月报自动带出)。 + + 管理方不绑定企业 → 返回空 profile(管理方代申请时需手动指定)。 + """ + require_identity(identity) + if not identity.company_id: + return {"company_id": "", "company_name": identity.company_name, + "zone": "", "room": "", "industry": "", + "legal_person": "", "legal_phone": "", + "contact_phone": "", "contact_name": "", + "address": "", "registered_capital": "", + "company_type": "", "founder": "", "founded_at": "", + "employees": None, "credit_code": ""} + comp = await db.session.get(ParkCompany, identity.company_id) + if comp is None: + return {"company_id": identity.company_id, "company_name": identity.company_name, + "zone": "", "room": "", "industry": "", + "legal_person": "", "legal_phone": "", + "contact_phone": "", "contact_name": "", + "address": "", "registered_capital": "", + "company_type": "", "founder": "", "founded_at": "", + "employees": None, "credit_code": ""} + # 统一信用代码:ParkCompany 无该字段,尝试从该企业最近一次月报带出 + credit_code = "" + latest = (await db.session.execute( + select(IncubatorMonthlyReport).where( + IncubatorMonthlyReport.company_id == comp.id, + IncubatorMonthlyReport.credit_code != "", + ).order_by(IncubatorMonthlyReport.report_month.desc()).limit(1) + )).scalar_one_or_none() + if latest is not None: + credit_code = latest.credit_code + return { + "company_id": comp.id, "company_name": comp.name, + "zone": comp.zone, "room": comp.room, "industry": comp.industry, + "legal_person": comp.legal_person, "legal_phone": comp.legal_phone, + "contact_phone": comp.contact_phone, "contact_name": comp.founder or "", + "address": comp.address, "registered_capital": comp.registered_capital, + "company_type": comp.company_type, "founder": comp.founder, + "founded_at": comp.founded_at, "employees": comp.employees, + "credit_code": credit_code, + } + + +async def list_company_members(db, identity: Identity) -> dict: + """企业绑定成员列表(联系人自动填入用):姓名/手机/职务/是否管理员。""" + require_identity(identity) + if not identity.company_id: + return {"items": [], "total": 0} + rows = (await db.session.execute( + select(CompanyMember).where( + CompanyMember.company_id == identity.company_id, + CompanyMember.status == "active", + ) + )).scalars().all() + items = [] + for m in rows: + u = await db.session.get(User, m.user_id) + items.append({ + "user_id": m.user_id, + "name": (u.nickname if u else "") or (u.username if u else ""), + "phone": (u.phone if u else ""), + "account": (u.account if u else ""), + "is_admin": bool(m.is_admin), + "member_type": m.member_type, + }) + return {"items": items, "total": len(items)} diff --git a/app/infrastructure/models.py b/app/infrastructure/models.py index a2a0ff6..660abb4 100644 --- a/app/infrastructure/models.py +++ b/app/infrastructure/models.py @@ -1494,3 +1494,21 @@ class DmMessage(Base): content: Mapped[str] = mapped_column(Text, default="") read_at: Mapped[str] = mapped_column(String, default="") created_at: Mapped[str] = mapped_column(String, default="") + + +class IncubatorSetting(Base): + """创业园应用园区级设置(申请注意事项 / 开放说明 / 月报说明)。 + + 一园区一行(tenant_id 唯一)。管理方在应用"园区设置"页维护, + 提交方读取展示在申请/填报页面顶部。 + """ + __tablename__ = "incubator_settings" + __table_args__ = (UniqueConstraint("tenant_id", name="uq_incubator_setting_tenant"),) + + id: Mapped[str] = mapped_column(String, primary_key=True) # set_ + tenant_id: Mapped[str] = mapped_column(String, default="", index=True) + reservation_notice: Mapped[str] = mapped_column(Text, default="") # 共享区域申请注意事项/须知 + report_notice: Mapped[str] = mapped_column(Text, default="") # 月报填报说明 + open_summary: Mapped[str] = mapped_column(Text, default="") # 开放区域总览说明 + updated_by: Mapped[str] = mapped_column(String, default="") + updated_at: Mapped[str] = mapped_column(String, default="")