feat(incubator): 园区设置+企业/成员信息自动识别接口

- 新增 IncubatorSetting 模型与迁移 0042:开放区域说明/申请注意事项/月报说明
- GET/PUT /incubator/settings(管理方维护,提交方只读)
- GET /incubator/company(提交方企业资料,月报/申请表单自动带出)
- GET /incubator/company/members(企业绑定成员,联系人自动填入)
This commit is contained in:
Pine
2026-09-01 22:31:53 +08:00
parent 684f4253b9
commit ece9dcc7ee
4 changed files with 230 additions and 0 deletions
@@ -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")
+56
View File
@@ -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)
+122
View File
@@ -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)}
+18
View File
@@ -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_<hex>
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="")