Files
2026-10-15 08:50:25 +08:00

359 lines
13 KiB
Python

"""请假申请 API — 工作流测试"""
from __future__ import annotations
import logging
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, ConfigDict, Field
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.core.menu_guard import require_admin_or_super_admin
from app.core.database import get_db
from app.core.response import success_response, error_response
from app.crud import leave as leave_crud
from app.crud.leave import total_pages
from app.schemas.leave import LeaveCreate, LeaveUpdate
from app.utils.excel_export import (
EXPORT_MAX_ROWS,
ExportColumn,
ExportValidationError,
export_rows_to_xlsx_response,
filter_export_columns,
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/web/admin/leave", tags=["请假管理"])
_LEAVE_TYPE_MAP = {"personal": "事假", "sick": "病假", "annual": "年假", "other": "其他"}
def _leave_local_callback(db: Session, business_id: str, event: str) -> None:
if not business_id:
return
if event == "completed":
leave_crud.update_leave_status(db, business_id, "approved")
elif event == "rejected":
leave_crud.update_leave_status(db, business_id, "rejected")
db.commit()
from app.services.workflow_callback import register_local_callback
register_local_callback("/api/admin/leave/callback", _leave_local_callback)
@router.get("")
def list_leaves(
page: int = Query(1, ge=1),
pageSize: int = Query(10, ge=1, le=500),
keyword: Optional[str] = Query(None),
status: Optional[str] = Query(None),
reasonIn: Optional[str] = Query(None),
_user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _user.get("tenant_no", "")
user_id = _user.get("user_id") or _user.get("id", "")
items, total = leave_crud.list_leaves(
db, page=page, page_size=pageSize, keyword=keyword, status=status,
reason_in=reasonIn,
tenant_no=tenant_no, create_by=user_id, user_info=_user,
)
for item in items:
item["leaveTypeLabel"] = _LEAVE_TYPE_MAP.get(item.get("leaveType", ""), item.get("leaveType", ""))
return success_response({
"list": items, "total": total, "page": page, "pageSize": pageSize,
"totalPages": total_pages(total, pageSize),
})
class LeaveColumnFacetBody(BaseModel):
"""请假列表列头可选值(去重;可附带列表上下文)"""
model_config = ConfigDict(populate_by_name=True, extra="allow")
field: str = Field(..., description="reason")
keyword: Optional[str] = Field(default=None, description="列表关键字(原因/类型)")
limit: Optional[int] = Field(default=500, ge=1, le=500, description="返回条数上限")
@router.post("/column-facet-values")
def leave_column_facet_values(
body: LeaveColumnFacetBody,
_user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
"""请假列表列头可选值:在列表上下文(排除本列)下去重。"""
from app.models.models import BizLeaveRequest
from app.services.column_facet import list_column_facet_values
from app.services.pl.facet_context import strip_simple_facet_self_params
if body.field != "reason":
return error_response(400, "不支持的列")
try:
tenant_no = _user.get("tenant_no", "")
user_id = _user.get("user_id") or _user.get("id", "")
data = body.model_dump(by_alias=True, exclude_none=True)
extra = getattr(body, "model_extra", None) or {}
for key, value in extra.items():
if key not in data and value not in (None, ""):
data[key] = value
list_keyword = (body.keyword or "").strip() or None
params = strip_simple_facet_self_params(data, body.field)
filters = []
kw = list_keyword or str(params.get("keyword") or "").strip()
status_in = params.get("statusIn") or params.get("status_in")
if status_in:
values = [s.strip() for s in str(status_in).split(",") if s.strip()]
if values:
filters.append(BizLeaveRequest.status.in_(values))
leave_type_in = params.get("leaveTypeIn") or params.get("leave_type_in")
if leave_type_in:
values = [s.strip() for s in str(leave_type_in).split(",") if s.strip()]
if values:
filters.append(BizLeaveRequest.leave_type.in_(values))
rows = list_column_facet_values(
db,
BizLeaveRequest,
BizLeaveRequest.reason,
tenant_no=tenant_no,
keyword=None,
limit=body.limit if body.limit is not None else 500,
extra_filters=filters or None,
apply_scope=lambda query: leave_crud.apply_leave_list_filters(
query, db, keyword=kw,
status=None if status_in else params.get("status"),
tenant_no=tenant_no, create_by=user_id, user_info=_user,
),
)
return success_response({"list": rows})
except ValueError as e:
return error_response(400, str(e))
except Exception as e:
return error_response(500, str(e))
_LEAVE_STATUS_MAP = {"draft": "草稿", "pending": "审批中", "approved": "已通过", "rejected": "已驳回"}
LEAVE_EXPORT_COLUMNS = [
ExportColumn(title="请假类型", field="leaveTypeLabel"),
ExportColumn(title="开始日期", field="startDate"),
ExportColumn(title="结束日期", field="endDate"),
ExportColumn(title="天数", field="days"),
ExportColumn(title="请假原因", field="reason"),
ExportColumn(title="状态", field="statusLabel"),
ExportColumn(title="创建时间", field="createTime"),
]
class LeaveExportBody(BaseModel):
keyword: Optional[str] = None
status: Optional[str] = None
reason_in: Optional[str] = Field(default=None, alias="reasonIn")
ids: Optional[List[str]] = None
columns: Optional[List[str]] = None
@router.post("/export")
def export_leaves(
body: LeaveExportBody,
_user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
"""导出请假列表(当前筛选全量,或 ids 已选)。"""
try:
tenant_no = _user.get("tenant_no", "")
user_id = _user.get("user_id") or _user.get("id", "")
items, _ = leave_crud.list_leaves(
db,
page=1,
page_size=EXPORT_MAX_ROWS,
keyword=body.keyword,
status=body.status,
reason_in=body.reason_in,
tenant_no=tenant_no,
create_by=user_id,
ids=body.ids,
max_page_size=EXPORT_MAX_ROWS,
user_info=_user,
)
for item in items:
item["leaveTypeLabel"] = _LEAVE_TYPE_MAP.get(item.get("leaveType", ""), item.get("leaveType", ""))
item["statusLabel"] = _LEAVE_STATUS_MAP.get(item.get("status", ""), item.get("status", ""))
# map frontend props status/leaveType to label fields when columns requested
requested = None
if body.columns:
requested = []
for key in body.columns:
if key == "status":
requested.append("statusLabel")
elif key == "leaveType":
requested.append("leaveTypeLabel")
else:
requested.append(key)
cols = filter_export_columns(LEAVE_EXPORT_COLUMNS, requested)
date_str = datetime.now().strftime("%Y%m%d")
return export_rows_to_xlsx_response(
db,
tenant_no=tenant_no or None,
sheet_title="请假列表",
columns=cols,
rows=items,
filename_stem=f"请假列表_{date_str}",
)
except ExportValidationError as e:
return error_response(400, str(e))
except Exception as e:
return error_response(500, str(e))
@router.post("")
def create_leave(
body: LeaveCreate,
_user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
tenant_no = _user.get("tenant_no", "")
user_id = _user.get("user_id") or _user.get("id", "")
dept_id = _user.get("region_id", "")
row = leave_crud.create_leave(
db,
leave_type=body.leaveType,
start_date=body.startDate,
end_date=body.endDate,
days=body.days,
reason=body.reason or "",
tenant_no=tenant_no,
user_id=user_id,
dept_id=dept_id,
remark=body.remark or "",
)
db.commit()
return success_response(leave_crud._row_to_dict(row))
@router.post("/{leave_id}/submit")
def submit_leave(
leave_id: str,
_user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
from app.models.models import WfProcessDef
from app.services.workflow_engine import WorkflowEngine
tenant_no = _user.get("tenant_no", "")
user_id = _user.get("user_id") or _user.get("id", "")
leave = leave_crud.get_leave(db, leave_id)
if not leave:
return error_response(404, "请假记录不存在")
if leave["status"] not in ("draft", "rejected"):
return error_response(400, "只有草稿或已驳回的请假单可以提交")
defn = db.query(WfProcessDef).filter(
WfProcessDef.process_code == "leave_request",
WfProcessDef.del_flag == 0,
WfProcessDef.status == "1",
).first()
if not defn:
return error_response(400, "未找到请假审批流程(leave_request),请先在流程配置中创建并发布")
variables = {
"days": leave["days"],
"leave_days": leave["days"],
"leaveType": leave["leaveType"],
"applicant": user_id,
}
try:
engine = WorkflowEngine(db)
instance = engine.start_process(
process_def_id=defn.id,
business_type="leave",
business_id=leave_id,
starter_user_id=user_id,
variables=variables,
callback_url="/api/admin/leave/callback",
tenant_no=tenant_no,
user_id=user_id,
)
leave_crud.update_leave_status(db, leave_id, "pending", wf_instance_id=instance.id, user_id=user_id)
db.commit()
return success_response({"instanceId": instance.id, "status": "pending"})
except Exception as e:
db.rollback()
logger.exception("发起请假审批失败: %s", e)
return error_response(500, f"发起审批失败: {e}")
@router.post("/callback")
def leave_workflow_callback(
body: dict,
db: Session = Depends(get_db),
):
logger.info("请假工作流回调: %s", body)
business_id = body.get("businessId", "")
event = body.get("event", "")
if not business_id:
return success_response({"received": True})
if event == "completed":
leave_crud.update_leave_status(db, business_id, "approved")
elif event == "rejected":
leave_crud.update_leave_status(db, business_id, "rejected")
db.commit()
return success_response({"received": True})
@router.get("/{leave_id}")
def get_leave(
leave_id: str,
_user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
item = leave_crud.get_leave(db, leave_id)
if not item:
return error_response(404, "请假记录不存在")
item["leaveTypeLabel"] = _LEAVE_TYPE_MAP.get(item.get("leaveType", ""), item.get("leaveType", ""))
return success_response(item)
@router.delete("/{leave_id}")
def delete_leave(
leave_id: str,
_user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
user_id = _user.get("user_id") or _user.get("id", "")
leave = leave_crud.get_leave(db, leave_id)
if not leave:
return error_response(404, "请假记录不存在")
if leave["status"] == "pending":
return error_response(400, "审批中的请假单不能删除")
leave_crud.soft_delete_leave(db, leave_id, user_id=user_id, user_info=_user)
db.commit()
return success_response(None)
@router.put("/{leave_id}")
def update_leave(
leave_id: str,
body: LeaveUpdate,
_user: dict = Depends(require_admin_or_super_admin),
db: Session = Depends(get_db),
):
user_id = _user.get("user_id") or _user.get("id", "")
leave = leave_crud.get_leave(db, leave_id)
if not leave:
return error_response(404, "请假记录不存在")
if leave["status"] not in ("draft", "rejected"):
return error_response(400, "只有草稿或已驳回状态可以编辑")
leave_crud.update_leave(db, leave_id, user_id=user_id, **body.model_dump(exclude_none=True))
db.commit()
return success_response(leave_crud.get_leave(db, leave_id))