feat(incubator): 孵化器入驻与叶子节点管理
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
"""创业园应用新增企业成员请假表 incubator_leaves。
|
||||
|
||||
权限:企业成员提交本人请假;企业管理员(company_members.is_admin)
|
||||
或园区管理员(carrier)审批;状态机 pending→approved/rejected→cancelled。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision = "0068_incubator_leaves"
|
||||
down_revision = "0067_user_personal_balance"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
exists = bind.execute(
|
||||
sa.text(
|
||||
"SELECT COUNT(*) FROM information_schema.TABLES "
|
||||
"WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'incubator_leaves'"
|
||||
)
|
||||
).scalar()
|
||||
if exists:
|
||||
return
|
||||
op.create_table(
|
||||
"incubator_leaves",
|
||||
sa.Column("id", sa.String(64), primary_key=True),
|
||||
sa.Column("tenant_id", sa.String(64), nullable=False, index=True),
|
||||
sa.Column("company_id", sa.String(64), nullable=False, index=True),
|
||||
sa.Column("company_name", sa.String(255), nullable=False, default=""),
|
||||
sa.Column("user_id", sa.String(64), nullable=False, index=True),
|
||||
sa.Column("user_name", sa.String(128), nullable=False, default=""),
|
||||
sa.Column("leave_type", sa.String(32), nullable=False, default="personal"),
|
||||
sa.Column("start_at", sa.String(32), nullable=False, default=""),
|
||||
sa.Column("end_at", sa.String(32), nullable=False, default=""),
|
||||
sa.Column("reason", sa.Text, nullable=False, default=""),
|
||||
sa.Column("status", sa.String(32), nullable=False, default="pending", index=True),
|
||||
sa.Column("reviewer_id", sa.String(64), nullable=False, default=""),
|
||||
sa.Column("reviewer_name", sa.String(128), nullable=False, default=""),
|
||||
sa.Column("review_comment", sa.Text, nullable=False, default=""),
|
||||
sa.Column("reviewed_at", sa.String(32), nullable=False, default=""),
|
||||
sa.Column("created_at", sa.String(32), nullable=False, default=""),
|
||||
sa.Column("updated_at", sa.String(32), nullable=False, default=""),
|
||||
mysql_charset="utf8mb4",
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("incubator_leaves")
|
||||
@@ -661,3 +661,89 @@ async def incubator_area_schedule(
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
except PermissionError as exc:
|
||||
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 请假管理(企业成员提交 / 仅园区管理员审批)
|
||||
# ---------------------------------------------------------------------------
|
||||
class LeaveCreateRequest(BaseModel):
|
||||
leave_type: str = Field("out", description="out/personal/sick/annual/other")
|
||||
start_at: str = Field(..., description="开始时间 YYYY-MM-DD HH:MM")
|
||||
end_at: str = Field(..., description="结束时间 YYYY-MM-DD HH:MM")
|
||||
reason: str = ""
|
||||
|
||||
|
||||
class LeaveReviewRequest(BaseModel):
|
||||
action: str = Field(..., description="approve / reject")
|
||||
comment: str = ""
|
||||
|
||||
|
||||
@router.get("/leaves", summary="请假列表(按身份自动过滤:园区管理员=全园区/企业成员=本人)")
|
||||
async def incubator_leaves(
|
||||
status: str = "", company_id: str = "",
|
||||
db: Database = Depends(get_db),
|
||||
user: dict = Depends(get_current_user),
|
||||
):
|
||||
ident = await resolve_identity(db, user)
|
||||
try:
|
||||
return await service.list_leaves(db, ident, status=status, company_id=company_id)
|
||||
except PermissionError as exc:
|
||||
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/leaves/{leave_id}", summary="请假详情")
|
||||
async def incubator_leave_detail(
|
||||
leave_id: str,
|
||||
db: Database = Depends(get_db),
|
||||
user: dict = Depends(get_current_user),
|
||||
):
|
||||
ident = await resolve_identity(db, user)
|
||||
try:
|
||||
return await service.get_leave(db, ident, leave_id)
|
||||
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
|
||||
|
||||
|
||||
@router.post("/leaves", summary="提交请假(企业成员本人)")
|
||||
async def incubator_create_leave(
|
||||
body: LeaveCreateRequest,
|
||||
db: Database = Depends(get_db),
|
||||
user: dict = Depends(get_current_user),
|
||||
):
|
||||
ident = await resolve_identity(db, user)
|
||||
try:
|
||||
return await service.create_leave(db, ident, body.model_dump())
|
||||
except (ValueError, PermissionError) as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/leaves/{leave_id}/review", summary="审批请假(仅园区管理员)")
|
||||
async def incubator_review_leave(
|
||||
leave_id: str, body: LeaveReviewRequest,
|
||||
db: Database = Depends(get_db),
|
||||
user: dict = Depends(get_current_user),
|
||||
):
|
||||
ident = await resolve_identity(db, user)
|
||||
try:
|
||||
return await service.review_leave(db, ident, leave_id, body.action, body.comment)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
except (ValueError, PermissionError) as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
|
||||
@router.post("/leaves/{leave_id}/cancel", summary="撤回本人 pending 请假")
|
||||
async def incubator_cancel_leave(
|
||||
leave_id: str,
|
||||
db: Database = Depends(get_db),
|
||||
user: dict = Depends(get_current_user),
|
||||
):
|
||||
ident = await resolve_identity(db, user)
|
||||
try:
|
||||
return await service.cancel_leave(db, ident, leave_id)
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
||||
except (ValueError, PermissionError) as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
+143
-2
@@ -24,6 +24,7 @@ from sqlalchemy import select
|
||||
from ..infrastructure.models import (
|
||||
CompanyMember,
|
||||
IncubatorAreaReservation,
|
||||
IncubatorLeave,
|
||||
IncubatorMonthlyReport,
|
||||
IncubatorSetting,
|
||||
IncubatorSharedArea,
|
||||
@@ -46,7 +47,8 @@ class Identity:
|
||||
"""当前用户在本园区应用中的生效身份。"""
|
||||
|
||||
def __init__(self, *, is_manager: bool, tenant_id: str, company_id: str,
|
||||
company_name: str, role: str, user_id: str, user_name: str = ""):
|
||||
company_name: str, role: str, user_id: str, user_name: str = "",
|
||||
is_company_admin: bool = False, member_type: str = "staff"):
|
||||
self.is_manager = is_manager
|
||||
self.tenant_id = tenant_id
|
||||
self.company_id = company_id
|
||||
@@ -54,6 +56,10 @@ class Identity:
|
||||
self.role = role
|
||||
self.user_id = user_id
|
||||
self.user_name = user_name
|
||||
# 企业内角色:is_company_admin=企业管理员(可审批本企业请假等);
|
||||
# member_type=admin/staff/owner/migrated(company_members.member_type)
|
||||
self.is_company_admin = is_company_admin
|
||||
self.member_type = member_type
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
@@ -64,6 +70,8 @@ class Identity:
|
||||
"company_name": self.company_name,
|
||||
"user_id": self.user_id,
|
||||
"user_name": self.user_name,
|
||||
"is_company_admin": self.is_company_admin,
|
||||
"member_type": self.member_type,
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +125,9 @@ async def resolve_identity(db, user: dict) -> Identity:
|
||||
continue
|
||||
return Identity(is_manager=False, tenant_id=tid, company_id=comp.id,
|
||||
company_name=comp.name, role="opc_member", user_id=uid,
|
||||
user_name=user_name)
|
||||
user_name=user_name,
|
||||
is_company_admin=bool(m.is_admin),
|
||||
member_type=m.member_type or "staff")
|
||||
|
||||
# —— 无权限(非本园区账号) ——
|
||||
return Identity(is_manager=False, tenant_id="", company_id="",
|
||||
@@ -1081,3 +1091,134 @@ async def area_schedule(db, identity: Identity, area_id: str, *, days: int = 7)
|
||||
"today": today.strftime("%Y-%m-%d"),
|
||||
}
|
||||
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 请假管理(企业成员提交 / 企业管理员或园区管理员审批)
|
||||
# ===========================================================================
|
||||
LEAVE_TYPES = ("out", "personal", "sick", "annual", "other")
|
||||
LEAVE_STATUSES = ("pending", "approved", "rejected", "cancelled")
|
||||
|
||||
|
||||
async def list_leaves(db, identity: Identity, *, status: str = "",
|
||||
company_id: str = "") -> dict:
|
||||
"""请假列表:园区管理员=全园区;企业成员=本人。"""
|
||||
require_identity(identity)
|
||||
stmt = select(IncubatorLeave).where(IncubatorLeave.tenant_id == identity.tenant_id)
|
||||
if identity.is_manager:
|
||||
if company_id:
|
||||
stmt = stmt.where(IncubatorLeave.company_id == company_id)
|
||||
else:
|
||||
stmt = stmt.where(IncubatorLeave.user_id == identity.user_id)
|
||||
if status and status in LEAVE_STATUSES:
|
||||
stmt = stmt.where(IncubatorLeave.status == status)
|
||||
stmt = stmt.order_by(IncubatorLeave.created_at.desc())
|
||||
rows = (await db.session.execute(stmt)).scalars().all()
|
||||
items = [_leave_to_dict(r) for r in rows]
|
||||
return {"items": items, "total": len(items)}
|
||||
|
||||
|
||||
async def get_leave(db, identity: Identity, leave_id: str) -> dict:
|
||||
require_identity(identity)
|
||||
r = await db.session.get(IncubatorLeave, leave_id)
|
||||
if r is None or r.tenant_id != identity.tenant_id:
|
||||
raise LookupError("请假单不存在")
|
||||
if not identity.is_manager and r.user_id != identity.user_id:
|
||||
raise LookupError("请假单不存在")
|
||||
return _leave_to_dict(r)
|
||||
|
||||
|
||||
async def create_leave(db, identity: Identity, body: dict) -> dict:
|
||||
"""企业成员提交本人请假。"""
|
||||
require_identity(identity)
|
||||
if not identity.company_id:
|
||||
raise PermissionError("仅在园企业成员可提交请假")
|
||||
leave_type = (body.get("leave_type") or "personal").strip()
|
||||
if leave_type not in LEAVE_TYPES:
|
||||
raise ValueError(f"请假类型仅支持:{', '.join(LEAVE_TYPES)}")
|
||||
start_at = (body.get("start_at") or "").strip()
|
||||
end_at = (body.get("end_at") or "").strip()
|
||||
if not start_at or not end_at:
|
||||
raise ValueError("请假起止时间必填")
|
||||
if start_at >= end_at:
|
||||
raise ValueError("结束时间必须晚于开始时间")
|
||||
now = utcnow_iso()
|
||||
r = IncubatorLeave(
|
||||
id=f"leave_{secrets.token_hex(8)}",
|
||||
tenant_id=identity.tenant_id,
|
||||
company_id=identity.company_id,
|
||||
company_name=identity.company_name,
|
||||
user_id=identity.user_id,
|
||||
user_name=identity.user_name or identity.user_id,
|
||||
leave_type=leave_type,
|
||||
start_at=start_at,
|
||||
end_at=end_at,
|
||||
reason=(body.get("reason") or "").strip(),
|
||||
status="pending",
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
db.session.add(r)
|
||||
await db.session.commit()
|
||||
return _leave_to_dict(r)
|
||||
|
||||
|
||||
async def review_leave(db, identity: Identity, leave_id: str,
|
||||
action: str, comment: str = "") -> dict:
|
||||
"""审批请假:仅园区管理员(carrier / 园区绑定管理员)。"""
|
||||
require_identity(identity)
|
||||
if not identity.is_manager:
|
||||
raise PermissionError("仅园区管理员可审批请假")
|
||||
if action not in ("approve", "reject"):
|
||||
raise ValueError("action 仅支持 approve / reject")
|
||||
r = await db.session.get(IncubatorLeave, leave_id)
|
||||
if r is None or r.tenant_id != identity.tenant_id:
|
||||
raise LookupError("请假单不存在")
|
||||
if r.status != "pending":
|
||||
raise ValueError(f"当前状态 {r.status} 不可审批")
|
||||
r.status = "approved" if action == "approve" else "rejected"
|
||||
r.review_comment = comment.strip()
|
||||
r.reviewer_id = identity.user_id
|
||||
r.reviewer_name = identity.user_name or identity.user_id
|
||||
r.reviewed_at = utcnow_iso()
|
||||
r.updated_at = utcnow_iso()
|
||||
await db.session.commit()
|
||||
return _leave_to_dict(r)
|
||||
|
||||
|
||||
async def cancel_leave(db, identity: Identity, leave_id: str) -> dict:
|
||||
"""提交人撤回本人 pending 请假。"""
|
||||
require_identity(identity)
|
||||
r = await db.session.get(IncubatorLeave, leave_id)
|
||||
if r is None or r.tenant_id != identity.tenant_id:
|
||||
raise LookupError("请假单不存在")
|
||||
if r.user_id != identity.user_id:
|
||||
raise PermissionError("仅可撤回本人请假")
|
||||
if r.status != "pending":
|
||||
raise ValueError(f"当前状态 {r.status} 不可撤回")
|
||||
r.status = "cancelled"
|
||||
r.updated_at = utcnow_iso()
|
||||
await db.session.commit()
|
||||
return _leave_to_dict(r)
|
||||
|
||||
|
||||
def _leave_to_dict(r: IncubatorLeave) -> dict:
|
||||
return {
|
||||
"id": r.id,
|
||||
"tenant_id": r.tenant_id,
|
||||
"company_id": r.company_id,
|
||||
"company_name": r.company_name,
|
||||
"user_id": r.user_id,
|
||||
"user_name": r.user_name,
|
||||
"leave_type": r.leave_type,
|
||||
"start_at": r.start_at,
|
||||
"end_at": r.end_at,
|
||||
"reason": r.reason,
|
||||
"status": r.status,
|
||||
"reviewer_id": r.reviewer_id,
|
||||
"reviewer_name": r.reviewer_name,
|
||||
"review_comment": r.review_comment,
|
||||
"reviewed_at": r.reviewed_at,
|
||||
"created_at": r.created_at,
|
||||
"updated_at": r.updated_at,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user