"""运营端管理接口(租户、角色下拉等)""" import os import uuid from typing import Any, List, Optional from fastapi import APIRouter, Depends, Query, UploadFile, File, Form from pydantic import BaseModel, ConfigDict, Field from sqlalchemy import and_, or_ from sqlalchemy.orm import Session from app.core.config import settings from app.core.data_scope import apply_data_scope from app.core.database import get_db from app.core.menu_guard import require_admin_or_super_admin, require_menu_code from app.core.response import success_response, error_response from app.core.attachment_biz import resolve_upload_folder from app.core.storage import storage from app.crud import tenant as tenant_svc from app.crud import dict_type as dict_type_svc from app.crud import dict_data as dict_data_svc from app.crud import dept as dept_svc from app.crud import site as site_svc from app.crud import post as post_svc from app.crud import menu as menu_svc from app.crud import role as role_svc from app.crud import user as user_svc from app.crud import role as role_svc from app.crud import user as user_svc from app.crud import oss as oss_svc from app.models.models import SysDept, SysDictData, SysRole, SysUser from app.services.web_auth_roles import _map_role_name_to_web router = APIRouter(prefix="/web/admin", tags=["运营端管理"]) # --------------------------------------------------------------------------- # 角色下拉(用户弹窗等) # --------------------------------------------------------------------------- @router.get("/roles") async def list_roles_for_select( _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): """返回运营端角色下拉选项(label=库中角色名,value=Web 角色码)。""" rows: List[SysRole] = ( db.query(SysRole) .filter(SysRole.del_flag == 0, SysRole.status == "1") .order_by(SysRole.create_time.desc()) .all() ) seen: set[str] = set() out: List[dict] = [] for r in rows: name = (r.role_name or "").strip() if not name: continue code = _map_role_name_to_web(name) key = f"{code}:{name}" if key in seen: continue seen.add(key) out.append({"label": name, "value": code}) if not out: out = [ {"label": "超级管理员", "value": "super_admin"}, {"label": "运营管理员", "value": "admin"}, ] return success_response(out) @router.get("/users/options") async def list_users_for_select( _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): """返回用户下拉选项(label=昵称,value=用户ID,附带 nickName/userNo),供审批人选择等场景使用。""" tenant_no = _user.get("tenant_no", "") q = db.query(SysUser).filter(SysUser.del_flag == 0, SysUser.status == "1") if tenant_no: q = q.filter(SysUser.tenant_no == tenant_no) rows = q.order_by(SysUser.create_time.desc()).all() out = [ { "label": (r.nick_name or r.user_no or ""), "value": r.id, "nickName": r.nick_name or "", "userNo": r.user_no or "", } for r in rows ] return success_response(out) @router.get("/roles/options") async def list_roles_for_designer( _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): """返回角色下拉选项(label=角色名,value=角色ID UUID),供流程设计器审批人选择使用。 与 /roles 不同,此端点返回角色 UUID 作为 value(引擎解析审批人需要 UUID)。""" tenant_no = _user.get("tenant_no", "") q = db.query(SysRole).filter(SysRole.del_flag == 0, SysRole.status == "1") if tenant_no: q = q.filter(SysRole.tenant_no == tenant_no) rows = q.order_by(SysRole.create_time.desc()).all() out = [{"label": (r.role_name or "").strip(), "value": r.id} for r in rows if (r.role_name or "").strip()] return success_response(out) # --------------------------------------------------------------------------- # 租户管理 # --------------------------------------------------------------------------- class TenantCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) tenant_no: str = Field(..., alias="tenantNo") tenant_name: str = Field(..., alias="tenantName") contact_name: Optional[str] = Field(None, alias="contactName") contact_phone: Optional[str] = Field(None, alias="contactPhone") order_num: Optional[int] = Field(0, alias="orderNum") remark: Optional[str] = None class TenantUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) tenant_no: Optional[str] = Field(None, alias="tenantNo") tenant_name: Optional[str] = Field(None, alias="tenantName") contact_name: Optional[str] = Field(None, alias="contactName") contact_phone: Optional[str] = Field(None, alias="contactPhone") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None @router.get("/tenants") async def list_tenants( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): items, total = tenant_svc.list_tenants(db, page=page, page_size=pageSize, keyword=keyword, user_info=_user) total_pages = tenant_svc.total_pages(total, pageSize) return success_response( { "list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": total_pages, } ) @router.post("/tenants") async def create_tenant( body: TenantCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: obj = tenant_svc.create_tenant( db, tenant_no=body.tenant_no, tenant_name=body.tenant_name, contact_name=body.contact_name, contact_phone=body.contact_phone, order_num=body.order_num, remark=body.remark, user_id=user.get("user_id"), ) return success_response(tenant_svc._row_to_dict(obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.put("/tenants/{tenant_id}") async def update_tenant( tenant_id: str, body: TenantUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False) obj = tenant_svc.update_tenant( db, tenant_id, tenant_no=payload.get("tenant_no"), tenant_name=payload.get("tenant_name"), contact_name=payload.get("contact_name"), contact_phone=payload.get("contact_phone"), order_num=payload.get("order_num"), remark=payload.get("remark"), user_id=user.get("user_id"), ) return success_response(tenant_svc._row_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/tenants/{tenant_id}") async def delete_tenant( tenant_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_svc.soft_delete_tenant(db, tenant_id) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # Helper: resolve tenant_no from user context # --------------------------------------------------------------------------- def _get_tenant_no(db: Session, user: dict) -> str: user_id = user.get("user_id") if not user_id: return "shanxia2.0" sys_user = db.query(SysUser).filter(SysUser.id == user_id).first() return (sys_user.tenant_no if sys_user else None) or "shanxia2.0" # --------------------------------------------------------------------------- # 用户管理 # --------------------------------------------------------------------------- class UserCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) user_no: str = Field(..., alias="userNo") nick_name: str = Field(..., alias="nickName") password: str = Field(..., alias="password") dept_id: Optional[str] = Field(None, alias="deptId") email: Optional[str] = Field("", alias="email") phone_no: Optional[str] = Field("", alias="phoneNo") sex: Optional[str] = Field("0", alias="sex") avatar: Optional[str] = Field(None, alias="avatar") status: Optional[str] = Field("1", alias="status") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None role_ids: Optional[List[str]] = Field(None, alias="roleIds") post_ids: Optional[List[str]] = Field(None, alias="postIds") class UserUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) nick_name: Optional[str] = Field(None, alias="nickName") dept_id: Optional[str] = Field(None, alias="deptId") email: Optional[str] = Field(None, alias="email") phone_no: Optional[str] = Field(None, alias="phoneNo") sex: Optional[str] = Field(None, alias="sex") avatar: Optional[str] = Field(None, alias="avatar") status: Optional[str] = Field(None, alias="status") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None role_ids: Optional[List[str]] = Field(None, alias="roleIds") post_ids: Optional[List[str]] = Field(None, alias="postIds") @router.get("/users") async def list_users( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, deptId: Optional[str] = Query(None, alias="deptId"), status: Optional[str] = None, user: dict = Depends(require_menu_code("system:user:list")), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) query = db.query(SysUser).filter( SysUser.del_flag == 0, SysUser.tenant_no == tenant_no, ) kw = (keyword or "").strip() if kw: like = f"%{kw}%" query = query.filter( or_( SysUser.user_no.like(like), SysUser.nick_name.like(like), SysUser.phone_no.like(like), ) ) if deptId: query = query.filter(SysUser.dept_id == deptId) if status: query = query.filter(SysUser.status == status) query = apply_data_scope(query, SysUser, user, db, tenant_no) total = query.count() items = query.order_by(SysUser.create_time.desc()).offset((page - 1) * pageSize).limit(pageSize).all() dept_ids = list({r.dept_id for r in items if r.dept_id}) dept_map: dict[str, str] = {} if dept_ids: for d in db.query(SysDept).filter(SysDept.id.in_(dept_ids), SysDept.del_flag == 0).all(): dept_map[d.id] = d.dept_name or "" result_list = [ user_svc._row_to_dict(r, dept_name=dept_map.get(r.dept_id or "", "")) for r in items ] tp = user_svc.total_pages(total, pageSize) return success_response({"list": result_list, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.get("/users/{user_id}") async def get_user_by_id( user_id: str, user: dict = Depends(require_menu_code("system:user:list")), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) row = user_svc.get_user(db, user_id) if not row: return error_response(404, "用户不存在") if row.tenant_no != tenant_no and user.get("role") != "super_admin": return error_response(403, "无权查看该用户") data = user_svc.get_user_with_relations(db, user_id) return success_response(data) @router.post("/users") async def create_user( body: UserCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) if user_svc.user_no_exists(db, body.user_no, tenant_no): return error_response(400, "用户账号已存在") obj = user_svc.create_user( db, user_no=body.user_no, nick_name=body.nick_name, password=body.password, dept_id=body.dept_id, email=body.email, phone_no=body.phone_no, sex=body.sex, avatar=body.avatar, status=body.status, tenant_no=tenant_no, order_num=body.order_num, remark=body.remark, user_id=user.get("user_id"), role_ids=body.role_ids, post_ids=body.post_ids, ) return success_response(user_svc._row_to_dict(obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.put("/users/{user_id}") async def update_user( user_id: str, body: UserUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False) obj = user_svc.update_user( db, user_id, nick_name=payload.get("nick_name"), dept_id=payload.get("dept_id"), email=payload.get("email"), phone_no=payload.get("phone_no"), sex=payload.get("sex"), avatar=payload.get("avatar"), status=payload.get("status"), order_num=payload.get("order_num"), remark=payload.get("remark"), user_id_updater=user.get("user_id"), role_ids=payload.get("role_ids"), post_ids=payload.get("post_ids"), ) return success_response(user_svc._row_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/users/{user_id}") async def delete_user( user_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: user_svc.soft_delete_user(db, user_id) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) @router.post("/users/{user_id}/reset-password") async def reset_user_password( user_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: user_svc.reset_password(db, user_id, "111111") return success_response(None, message="密码已重置为 111111") except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 字典类型管理 # --------------------------------------------------------------------------- class DictTypeCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) dict_name: str = Field(..., alias="dictName") dict_type: str = Field(..., alias="dictType") order_num: Optional[int] = Field(0, alias="orderNum") remark: Optional[str] = None class DictTypeUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) dict_name: Optional[str] = Field(None, alias="dictName") dict_type: Optional[str] = Field(None, alias="dictType") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None @router.get("/dict-types") async def list_dict_types( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, user: dict = Depends(require_menu_code("system:dict:list")), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, total = dict_type_svc.list_dict_types( db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user ) tp = dict_type_svc.total_pages(total, pageSize) return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.post("/dict-types") async def create_dict_type( body: DictTypeCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) obj = dict_type_svc.create_dict_type( db, dict_name=body.dict_name, dict_type=body.dict_type, tenant_no=tenant_no, order_num=body.order_num, remark=body.remark, user_id=user.get("user_id"), ) return success_response(dict_type_svc._row_to_dict(obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.put("/dict-types/{dict_type_id}") async def update_dict_type( dict_type_id: str, body: DictTypeUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False) obj = dict_type_svc.update_dict_type( db, dict_type_id, dict_name=payload.get("dict_name"), dict_type=payload.get("dict_type"), order_num=payload.get("order_num"), remark=payload.get("remark"), user_id=user.get("user_id"), tenant_no=tenant_no, ) return success_response(dict_type_svc._row_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/dict-types/{dict_type_id}") async def delete_dict_type( dict_type_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: dict_type_svc.soft_delete_dict_type(db, dict_type_id) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 字典数据管理 # --------------------------------------------------------------------------- class DictDataCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) dict_label: str = Field(..., alias="dictLabel") dict_value: str = Field(..., alias="dictValue") dict_type: str = Field(..., alias="dictType") tag_type: Optional[str] = Field("", alias="tagType") order_num: Optional[int] = Field(0, alias="orderNum") remark: Optional[str] = None class DictDataUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) dict_label: Optional[str] = Field(None, alias="dictLabel") dict_value: Optional[str] = Field(None, alias="dictValue") tag_type: Optional[str] = Field(None, alias="tagType") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None @router.get("/dict-data") async def list_dict_data( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), dictType: Optional[str] = None, keyword: Optional[str] = None, user: dict = Depends(require_menu_code("system:dict:list")), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, total = dict_data_svc.list_dict_data( db, page=page, page_size=pageSize, dict_type_code=dictType, keyword=keyword, tenant_no=tenant_no, user_info=user ) tp = dict_data_svc.total_pages(total, pageSize) return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.post("/dict-data") async def create_dict_data( body: DictDataCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) obj = dict_data_svc.create_dict_data( db, dict_label=body.dict_label, dict_value=body.dict_value, dict_type=body.dict_type, tag_type=body.tag_type, tenant_no=tenant_no, order_num=body.order_num, remark=body.remark, user_id=user.get("user_id"), ) return success_response(dict_data_svc._row_to_dict(obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.put("/dict-data/{data_id}") async def update_dict_data( data_id: str, body: DictDataUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False) obj = dict_data_svc.update_dict_data( db, data_id, dict_label=payload.get("dict_label"), dict_value=payload.get("dict_value"), tag_type=payload.get("tag_type"), order_num=payload.get("order_num"), remark=payload.get("remark"), user_id=user.get("user_id"), ) return success_response(dict_data_svc._row_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/dict-data/{data_id}") async def delete_dict_data( data_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: dict_data_svc.soft_delete_dict_data(db, data_id) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) @router.get("/dict-data/options") async def dict_data_options( dictType: str = Query(..., alias="dictType"), user: dict = Depends(require_menu_code("system:dict:list")), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items = dict_data_svc.list_dict_data_options(db, dict_type_code=dictType, tenant_no=tenant_no) return success_response(items) @router.get("/dict-data/all") async def dict_data_all( user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) rows = db.query(SysDictData).filter( and_(SysDictData.tenant_no == tenant_no, SysDictData.del_flag == 0) ).order_by(SysDictData.order_num.asc()).all() grouped = {} for r in rows: key = r.dict_type if key not in grouped: grouped[key] = [] grouped[key].append({ "label": r.dict_label, "value": r.dict_value, "tagType": r.tag_type or "" }) return success_response(grouped) # --------------------------------------------------------------------------- # 组织管理 # --------------------------------------------------------------------------- class DeptCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) parent_id: str = Field("0", alias="parentId") dept_name: str = Field(..., alias="deptName") dept_category_code: Optional[str] = Field(None, alias="deptCategoryCode") status: Optional[str] = Field("1") leader: Optional[str] = None leader_phone: Optional[str] = Field(None, alias="leaderPhone") order_num: Optional[int] = Field(0, alias="orderNum") remark: Optional[str] = None class DeptUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) dept_name: Optional[str] = Field(None, alias="deptName") dept_category_code: Optional[str] = Field(None, alias="deptCategoryCode") status: Optional[str] = None leader: Optional[str] = None leader_phone: Optional[str] = Field(None, alias="leaderPhone") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None @router.get("/depts") async def list_depts( keyword: Optional[str] = None, user: dict = Depends(require_menu_code("system:dept:list")), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) tree = dept_svc.list_depts_tree(db, keyword=keyword, tenant_no=tenant_no, user_info=user) return success_response(tree) @router.post("/depts") async def create_dept( body: DeptCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) obj = dept_svc.create_dept( db, parent_id=body.parent_id, dept_name=body.dept_name, dept_category_code=body.dept_category_code, status=body.status, leader=body.leader, leader_phone=body.leader_phone, tenant_no=tenant_no, order_num=body.order_num, remark=body.remark, user_id=user.get("user_id"), ) return success_response(dept_svc._row_to_dict(obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.put("/depts/{dept_id}") async def update_dept( dept_id: str, body: DeptUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False) obj = dept_svc.update_dept( db, dept_id, dept_name=payload.get("dept_name"), dept_category_code=payload.get("dept_category_code"), status=payload.get("status"), leader=payload.get("leader"), leader_phone=payload.get("leader_phone"), order_num=payload.get("order_num"), remark=payload.get("remark"), user_id=user.get("user_id"), ) return success_response(dept_svc._row_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/depts/{dept_id}") async def delete_dept( dept_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: dept_svc.soft_delete_dept(db, dept_id) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 站点管理 # --------------------------------------------------------------------------- class SiteCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) site_name: str = Field(..., alias="siteName") site_code: str = Field(..., alias="siteCode") site_type: Optional[str] = Field(None, alias="siteType") status: Optional[str] = Field("1") address: Optional[str] = None contact_name: Optional[str] = Field(None, alias="contactName") contact_phone: Optional[str] = Field(None, alias="contactPhone") business_scope: Optional[str] = Field(None, alias="businessScope") order_num: Optional[int] = Field(0, alias="orderNum") remark: Optional[str] = None class SiteUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) site_name: Optional[str] = Field(None, alias="siteName") site_code: Optional[str] = Field(None, alias="siteCode") site_type: Optional[str] = Field(None, alias="siteType") status: Optional[str] = None address: Optional[str] = None contact_name: Optional[str] = Field(None, alias="contactName") contact_phone: Optional[str] = Field(None, alias="contactPhone") business_scope: Optional[str] = Field(None, alias="businessScope") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None @router.get("/sites") async def list_sites( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, user: dict = Depends(require_menu_code("system:site:list")), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, total = site_svc.list_sites(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user) tp = site_svc.total_pages(total, pageSize) return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.post("/sites") async def create_site( body: SiteCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) obj = site_svc.create_site( db, site_name=body.site_name, site_code=body.site_code, site_type=body.site_type, status=body.status, address=body.address, contact_name=body.contact_name, contact_phone=body.contact_phone, business_scope=body.business_scope, tenant_no=tenant_no, order_num=body.order_num, remark=body.remark, user_id=user.get("user_id"), ) return success_response(site_svc._row_to_dict(obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.put("/sites/{site_id}") async def update_site( site_id: str, body: SiteUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False) obj = site_svc.update_site( db, site_id, site_name=payload.get("site_name"), site_code=payload.get("site_code"), site_type=payload.get("site_type"), status=payload.get("status"), address=payload.get("address"), contact_name=payload.get("contact_name"), contact_phone=payload.get("contact_phone"), business_scope=payload.get("business_scope"), order_num=payload.get("order_num"), remark=payload.get("remark"), user_id=user.get("user_id"), ) return success_response(site_svc._row_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/sites/{site_id}") async def delete_site( site_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: site_svc.soft_delete_site(db, site_id) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 职位管理 # --------------------------------------------------------------------------- class PostCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) post_name: str = Field(..., alias="postName") post_code: str = Field(..., alias="postCode") status: Optional[str] = Field("1") order_num: Optional[int] = Field(0, alias="orderNum") remark: Optional[str] = None class PostUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) post_name: Optional[str] = Field(None, alias="postName") post_code: Optional[str] = Field(None, alias="postCode") status: Optional[str] = None order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None @router.get("/posts") async def list_posts( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, user: dict = Depends(require_menu_code("system:post:list")), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, total = post_svc.list_posts(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user) tp = post_svc.total_pages(total, pageSize) return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.post("/posts") async def create_post( body: PostCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) obj = post_svc.create_post( db, post_name=body.post_name, post_code=body.post_code, status=body.status, tenant_no=tenant_no, order_num=body.order_num, remark=body.remark, user_id=user.get("user_id"), ) return success_response(post_svc._row_to_dict(obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.put("/posts/{post_id}") async def update_post( post_id: str, body: PostUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False) obj = post_svc.update_post( db, post_id, post_name=payload.get("post_name"), post_code=payload.get("post_code"), status=payload.get("status"), order_num=payload.get("order_num"), remark=payload.get("remark"), user_id=user.get("user_id"), ) return success_response(post_svc._row_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/posts/{post_id}") async def delete_post( post_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: post_svc.soft_delete_post(db, post_id) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) class PostUsersUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) user_ids: List[str] = Field(default_factory=list, alias="userIds") @router.get("/posts/{post_id}/users") async def list_post_bound_users( post_id: str, user: dict = Depends(require_menu_code("system:post:list")), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) post = post_svc.get_post(db, post_id) if not post or post.tenant_no != tenant_no: return error_response(404, "职位不存在") ids = user_svc.list_user_ids_bound_to_post(db, post_id, tenant_no) return success_response({"userIds": ids}) @router.put("/posts/{post_id}/users") async def update_post_bound_users( post_id: str, body: PostUsersUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) post = post_svc.get_post(db, post_id) if not post or post.tenant_no != tenant_no: return error_response(404, "职位不存在") try: user_svc.sync_post_user_bindings( db, post_id, tenant_no, body.user_ids, user.get("user_id") ) return success_response(None, message="保存成功") except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 菜单管理 # --------------------------------------------------------------------------- class MenuCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) menu_name: str = Field(..., alias="menuName") parent_id: str = Field("0", alias="parentId") menu_type: str = Field("M", alias="menuType") path: Optional[str] = Field("", alias="path") component: Optional[str] = Field(None, alias="component") perms: Optional[str] = Field(None, alias="perms") is_frame: Optional[int] = Field(1, alias="isFrame") visible: Optional[str] = Field("1", alias="visible") status: Optional[str] = Field("1", alias="status") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None class MenuUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) menu_name: Optional[str] = Field(None, alias="menuName") parent_id: Optional[str] = Field(None, alias="parentId") menu_type: Optional[str] = Field(None, alias="menuType") path: Optional[str] = Field(None, alias="path") component: Optional[str] = Field(None, alias="component") perms: Optional[str] = Field(None, alias="perms") is_frame: Optional[int] = Field(None, alias="isFrame") visible: Optional[str] = Field(None, alias="visible") status: Optional[str] = Field(None, alias="status") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None @router.get("/menus") async def list_menus( keyword: Optional[str] = None, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) tree = menu_svc.list_menus_tree(db, keyword=keyword, tenant_no=tenant_no, user_info=user) return success_response(tree) @router.post("/menus") async def create_menu( body: MenuCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) obj = menu_svc.create_menu( db, menu_name=body.menu_name, parent_id=body.parent_id, menu_type=body.menu_type, path=body.path, component=body.component, perms=body.perms, is_frame=body.is_frame, visible=body.visible, status=body.status, tenant_no=tenant_no, order_num=body.order_num, remark=body.remark, user_id=user.get("user_id"), ) return success_response(menu_svc._row_to_dict(obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.put("/menus/{menu_id}") async def update_menu( menu_id: str, body: MenuUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False) obj = menu_svc.update_menu( db, menu_id, menu_name=payload.get("menu_name"), parent_id=payload.get("parent_id"), menu_type=payload.get("menu_type"), path=payload.get("path"), component=payload.get("component"), perms=payload.get("perms"), is_frame=payload.get("is_frame"), visible=payload.get("visible"), status=payload.get("status"), order_num=payload.get("order_num"), remark=payload.get("remark"), user_id=user.get("user_id"), ) return success_response(menu_svc._row_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/menus/{menu_id}") async def delete_menu( menu_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: menu_svc.soft_delete_menu(db, menu_id) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 角色管理 # --------------------------------------------------------------------------- class RoleCreateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) role_name: str = Field(..., alias="roleName") data_scope: Optional[str] = Field("4", alias="dataScope") status: Optional[str] = Field("1", alias="status") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None class RoleUpdateBody(BaseModel): model_config = ConfigDict(populate_by_name=True) role_name: Optional[str] = Field(None, alias="roleName") data_scope: Optional[str] = Field(None, alias="dataScope") status: Optional[str] = Field(None, alias="status") order_num: Optional[int] = Field(None, alias="orderNum") remark: Optional[str] = None class RoleMenuBody(BaseModel): menu_ids: List[str] = Field(..., alias="menuIds") class RoleDataScopeBody(BaseModel): model_config = ConfigDict(populate_by_name=True) data_scope: str = Field(..., alias="dataScope") scope_details: Optional[List[dict]] = Field(None, alias="scopeDetails") @router.get("/role-management") async def list_role_management( page: int = Query(1, ge=1), pageSize: int = Query(10, ge=1, le=100, alias="pageSize"), keyword: Optional[str] = None, user: dict = Depends(require_menu_code("system:role:list")), db: Session = Depends(get_db), ): tenant_no = _get_tenant_no(db, user) items, total = role_svc.list_roles(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user) tp = role_svc.total_pages(total, pageSize) return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp}) @router.post("/role-management") async def create_role_management( body: RoleCreateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) obj = role_svc.create_role( db, role_name=body.role_name, data_scope=body.data_scope, status=body.status, tenant_no=tenant_no, order_num=body.order_num, remark=body.remark, user_id=user.get("user_id"), ) return success_response(role_svc._row_to_dict(obj), message="创建成功") except ValueError as e: return error_response(400, str(e)) @router.put("/role-management/{role_id}") async def update_role_management( role_id: str, body: RoleUpdateBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False) obj = role_svc.update_role( db, role_id, role_name=payload.get("role_name"), data_scope=payload.get("data_scope"), status=payload.get("status"), order_num=payload.get("order_num"), remark=payload.get("remark"), user_id=user.get("user_id"), ) return success_response(role_svc._row_to_dict(obj), message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.delete("/role-management/{role_id}") async def delete_role_management( role_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: role_svc.soft_delete_role(db, role_id) return success_response(None, message="删除成功") except ValueError as e: return error_response(400, str(e)) @router.get("/role-management/{role_id}/menus") async def get_role_menus( role_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): menu_ids = role_svc.get_role_menu_ids(db, role_id) return success_response({"menuIds": menu_ids}) @router.put("/role-management/{role_id}/menus") async def update_role_menus( role_id: str, body: RoleMenuBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) role_svc.update_role_menus(db, role_id, body.menu_ids, tenant_no, user_id=user.get("user_id")) return success_response(None, message="更新成功") except ValueError as e: return error_response(400, str(e)) @router.get("/role-management/{role_id}/data-scope") async def get_role_data_scope( role_id: str, _user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): role = role_svc.get_role(db, role_id) if not role: return error_response(404, "角色不存在") details = role_svc.get_role_data_scope(db, role_id) return success_response({"dataScope": role.data_scope or "1", "scopeDetails": details}) @router.put("/role-management/{role_id}/data-scope") async def update_role_data_scope( role_id: str, body: RoleDataScopeBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): try: tenant_no = _get_tenant_no(db, user) role_svc.update_role_data_scope( db, role_id, body.data_scope, body.scope_details, tenant_no=tenant_no, user_id=user.get("user_id"), ) return success_response(None, message="更新成功") except ValueError as e: return error_response(400, str(e)) # --------------------------------------------------------------------------- # 附件上传 # --------------------------------------------------------------------------- def _format_file_size(size_bytes: int) -> str: if size_bytes < 1024: return f"{size_bytes} B" elif size_bytes < 1024 * 1024: return f"{size_bytes / 1024:.1f} KB" else: return f"{size_bytes / (1024 * 1024):.1f} MB" @router.post("/attachment/upload") async def upload_attachment( file: UploadFile = File(...), folder: Optional[str] = Form(""), biz_type: Optional[str] = Form(None, alias="bizType"), user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): if not file or not file.filename: return error_response(400, "请选择文件") try: effective_folder = resolve_upload_folder( folder=folder or "", biz_type=biz_type or "" ) file_content = await file.read() full_url = storage.upload_file(file_content, file.filename, effective_folder) base_url = settings.MINIO_BASE_URL.rstrip("/") if settings.MINIO_BASE_URL else "" if base_url and full_url.startswith(base_url + "/"): file_url = full_url[len(base_url) + 1:] else: file_url = full_url file_size = len(file_content) file_ext = os.path.splitext(file.filename)[1].lstrip(".") if file.filename else "" file_type = file.content_type or "" tenant_no = _get_tenant_no(db, user) record = oss_svc.create_oss( db, tenant_no=tenant_no, url=file_url, original_name=file.filename, file_suffix=file_ext, file_size=file_size, file_type=file_type, biz_type=(biz_type or "").strip(), user_id=user.get("user_id"), ext1=effective_folder or None, ) return success_response({ "id": record.id, "fileName": record.original_name, "fileUrl": record.url, "fileSize": record.file_size, "fileSizeDisplay": _format_file_size(record.file_size or 0), "fileType": record.file_type, "bizType": record.biz_type or "", }) except Exception as e: db.rollback() return error_response(500, f"上传失败: {str(e)}") class AttachmentDownloadUrlBody(BaseModel): model_config = ConfigDict(populate_by_name=True) file_url: str = Field(..., alias="fileUrl") file_name: Optional[str] = Field(None, alias="fileName") @router.post("/attachment/download-url") async def attachment_download_url( body: AttachmentDownloadUrlBody, user: dict = Depends(require_admin_or_super_admin), db: Session = Depends(get_db), ): """返回带 Content-Disposition 的下载 URL,保证浏览器使用原始文件名。""" file_url = (body.file_url or "").strip() if not file_url: return error_response(400, "文件地址不能为空") tenant_no = _get_tenant_no(db, user) att = oss_svc.get_oss_by_url(db, tenant_no, file_url) download_name = (body.file_name or "").strip() if not download_name and att: download_name = (att.original_name or "").strip() if not download_name: download_name = file_url.rsplit("/", 1)[-1] or "download" try: url = storage.presigned_download_url(file_url, download_name) return success_response({"downloadUrl": url}) except Exception as e: return error_response(500, f"获取下载链接失败: {str(e)}")