Separate cross-team selection from attribution maintenance and apply the same scope to options, history filters and writes. Add an opt-in tenant permission without granting existing roles. Protect authorization mutation paths against self-grants, validate tenant targets and links, and include menu ancestors atomically. Cover role union, revocation, mobile writes and administration boundaries with regression tests.
1709 lines
61 KiB
Python
1709 lines
61 KiB
Python
"""运营端管理接口(租户、角色下拉等)"""
|
|
import asyncio
|
|
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 or_
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.config import settings
|
|
from app.core.data_scope import apply_data_scope, _get_child_dept_ids
|
|
from app.core.database import get_db
|
|
from app.core.menu_guard import require_admin_or_super_admin, require_menu_code, require_strict_menu_code
|
|
from app.core.response import success_response, error_response
|
|
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 item_unit as item_unit_svc
|
|
from app.crud import express_company as express_company_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.collection_protocol import _lookup_attachment_by_url
|
|
from app.models.models import BizAttachment, SysDept, SysRole, SysUser, SysMenu, SysPost
|
|
from app.services.permission_administration import validate_tenant_references
|
|
from app.services.web_auth_roles import (
|
|
_map_role_name_to_web,
|
|
list_user_ids_with_effective_role_name,
|
|
)
|
|
|
|
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(
|
|
roleName: Optional[str] = Query(None, alias="roleName"),
|
|
_user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""返回用户下拉选项(label=昵称,value=用户ID),供流程设计器审批人选择使用。
|
|
|
|
可选 roleName:按有效角色(直接绑定 ∪ 职位继承)精确过滤。
|
|
"""
|
|
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)
|
|
rn = (roleName or "").strip()
|
|
if rn:
|
|
matched_ids = list_user_ids_with_effective_role_name(
|
|
db, tenant_no=tenant_no or settings.DEFAULT_TENANT_NO, role_name=rn
|
|
)
|
|
if not matched_ids:
|
|
return success_response([])
|
|
q = q.filter(SysUser.id.in_(matched_ids))
|
|
rows = q.order_by(SysUser.create_time.desc()).distinct().all()
|
|
out = [{"label": (r.nick_name or r.user_no or ""), "value": r.id} 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 "zkxw"
|
|
sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
|
|
return (sys_user.tenant_no if sys_user else None) or "zkxw"
|
|
|
|
|
|
def _assert_user_tenant_access(db: Session, user: dict, target_user_id: str):
|
|
"""用户管理始终在当前认证租户内操作;返回目标用户行。"""
|
|
row = user_svc.get_user(db, target_user_id)
|
|
if not row:
|
|
raise ValueError("用户不存在")
|
|
tenant_no = _get_tenant_no(db, user)
|
|
if row.tenant_no != tenant_no:
|
|
raise ValueError("无权操作其他租户的用户")
|
|
return row
|
|
|
|
|
|
def _generate_temp_password(length: int = 12) -> str:
|
|
"""生成可读的临时密码(字母数字,避免易混淆字符)。"""
|
|
import secrets
|
|
|
|
alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"
|
|
return "".join(secrets.choice(alphabet) for _ in range(length))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 用户管理
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
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")
|
|
supervisor_user_id: Optional[str] = Field(None, alias="supervisorUserId")
|
|
email: Optional[str] = Field("", alias="email")
|
|
phone_no: Optional[str] = Field("", alias="phoneNo")
|
|
hire_date: Optional[str] = Field(None, alias="hireDate")
|
|
probation_months: Optional[int] = Field(None, alias="probationMonths")
|
|
last_work_day: Optional[str] = Field(None, alias="lastWorkDay")
|
|
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")
|
|
supervisor_user_id: Optional[str] = Field(None, alias="supervisorUserId")
|
|
email: Optional[str] = Field(None, alias="email")
|
|
phone_no: Optional[str] = Field(None, alias="phoneNo")
|
|
hire_date: Optional[str] = Field(None, alias="hireDate")
|
|
probation_months: Optional[int] = Field(None, alias="probationMonths")
|
|
last_work_day: Optional[str] = Field(None, alias="lastWorkDay")
|
|
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,
|
|
roleName: Optional[str] = Query(None, alias="roleName"),
|
|
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:
|
|
dept_ids = _get_child_dept_ids(db, deptId, tenant_no)
|
|
query = query.filter(SysUser.dept_id.in_(dept_ids))
|
|
if status:
|
|
query = query.filter(SysUser.status == status)
|
|
rn = (roleName or "").strip()
|
|
if rn:
|
|
matched_ids = list_user_ids_with_effective_role_name(
|
|
db, tenant_no=tenant_no, role_name=rn
|
|
)
|
|
if not matched_ids:
|
|
return success_response(
|
|
{"list": [], "total": 0, "page": page, "pageSize": pageSize, "totalPages": 0}
|
|
)
|
|
query = query.filter(SysUser.id.in_(matched_ids))
|
|
query = apply_data_scope(query, SysUser, user, db, tenant_no)
|
|
total = query.distinct().count()
|
|
items = (
|
|
query.distinct()
|
|
.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
|
|
]
|
|
user_svc.enrich_user_dicts_with_relations(db, result_list)
|
|
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_strict_menu_code("system:user:list", allow_super_admin=True)),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
validate_tenant_references(db, SysRole, body.role_ids, tenant_no)
|
|
validate_tenant_references(db, SysPost, body.post_ids, tenant_no)
|
|
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,
|
|
supervisor_user_id=body.supervisor_user_id,
|
|
email=body.email,
|
|
phone_no=body.phone_no,
|
|
hire_date=body.hire_date,
|
|
probation_months=body.probation_months,
|
|
last_work_day=body.last_work_day,
|
|
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_strict_menu_code("system:user:list", allow_super_admin=True)),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
_assert_user_tenant_access(db, user, user_id)
|
|
tenant_no = _get_tenant_no(db, user)
|
|
validate_tenant_references(db, SysRole, body.role_ids, tenant_no)
|
|
validate_tenant_references(db, SysPost, body.post_ids, tenant_no)
|
|
payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
|
|
update_kwargs: dict[str, Any] = {
|
|
"nick_name": payload.get("nick_name"),
|
|
"dept_id": payload.get("dept_id"),
|
|
"supervisor_user_id": payload.get("supervisor_user_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"),
|
|
}
|
|
if "hire_date" in payload:
|
|
update_kwargs["hire_date"] = payload.get("hire_date")
|
|
if "probation_months" in payload:
|
|
update_kwargs["probation_months"] = payload.get("probation_months")
|
|
if "last_work_day" in payload:
|
|
update_kwargs["last_work_day"] = payload.get("last_work_day")
|
|
obj = user_svc.update_user(db, user_id, **update_kwargs)
|
|
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_strict_menu_code("system:user:list", allow_super_admin=True)),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
_assert_user_tenant_access(db, user, user_id)
|
|
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_strict_menu_code("system:user:list", allow_super_admin=True)),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
_assert_user_tenant_access(db, user, user_id)
|
|
temp_password = _generate_temp_password(12)
|
|
user_svc.reset_password(db, user_id, temp_password)
|
|
return success_response(
|
|
{"tempPassword": temp_password},
|
|
message="密码已重置,请将临时密码告知用户并督促尽快修改",
|
|
)
|
|
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")
|
|
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")
|
|
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,
|
|
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"),
|
|
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)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 组织管理
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
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")
|
|
leader_user_id: Optional[str] = Field(None, alias="leaderUserId")
|
|
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")
|
|
leader_user_id: Optional[str] = Field(None, alias="leaderUserId")
|
|
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,
|
|
leader_user_id=body.leader_user_id,
|
|
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"),
|
|
leader_user_id=payload.get("leader_user_id"),
|
|
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")
|
|
site_image: Optional[str] = Field(None, alias="siteImage")
|
|
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")
|
|
site_image: Optional[str] = Field(None, alias="siteImage")
|
|
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,
|
|
site_image=body.site_image,
|
|
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"),
|
|
site_image=payload.get("site_image"),
|
|
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_strict_menu_code("system:post:list", allow_super_admin=True)),
|
|
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_strict_menu_code("system:post:list", allow_super_admin=True)),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
validate_tenant_references(db, SysPost, [post_id], _get_tenant_no(db, user))
|
|
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_strict_menu_code("system:post:list", allow_super_admin=True)),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
validate_tenant_references(db, SysPost, [post_id], _get_tenant_no(db, _user))
|
|
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_strict_menu_code("system:post:list", allow_super_admin=True)),
|
|
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 PostRolesUpdateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
role_ids: List[str] = Field(default_factory=list, alias="roleIds")
|
|
|
|
|
|
@router.get("/posts/{post_id}/roles")
|
|
async def list_post_bound_roles(
|
|
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 = post_svc.list_role_ids_for_post(db, post_id, tenant_no)
|
|
return success_response({"roleIds": ids})
|
|
|
|
|
|
@router.put("/posts/{post_id}/roles")
|
|
async def update_post_bound_roles(
|
|
post_id: str,
|
|
body: PostRolesUpdateBody,
|
|
user: dict = Depends(require_strict_menu_code("system:post:list", allow_super_admin=True)),
|
|
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:
|
|
post_svc.sync_post_role_bindings(
|
|
db, post_id, tenant_no, body.role_ids, user.get("user_id")
|
|
)
|
|
return success_response(None, message="保存成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 物品单位管理
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class ItemUnitCreateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
unit_code: str = Field(..., alias="unitCode")
|
|
unit_name: str = Field(..., alias="unitName")
|
|
order_num: Optional[int] = Field(0, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class ItemUnitUpdateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
unit_code: Optional[str] = Field(None, alias="unitCode")
|
|
unit_name: Optional[str] = Field(None, alias="unitName")
|
|
order_num: Optional[int] = Field(None, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
@router.get("/item-units")
|
|
async def list_item_units(
|
|
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:unit:list")),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = item_unit_svc.list_item_units(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
|
|
tp = item_unit_svc.total_pages(total, pageSize)
|
|
return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
|
|
|
|
|
|
@router.post("/item-units")
|
|
async def create_item_unit(
|
|
body: ItemUnitCreateBody,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
obj = item_unit_svc.create_item_unit(
|
|
db,
|
|
unit_code=body.unit_code,
|
|
unit_name=body.unit_name,
|
|
tenant_no=tenant_no,
|
|
order_num=body.order_num,
|
|
remark=body.remark,
|
|
user_id=user.get("user_id"),
|
|
)
|
|
return success_response(item_unit_svc._row_to_dict(obj), message="创建成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/item-units/{unit_id}")
|
|
async def update_item_unit(
|
|
unit_id: str,
|
|
body: ItemUnitUpdateBody,
|
|
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 = item_unit_svc.update_item_unit(
|
|
db,
|
|
unit_id,
|
|
unit_code=payload.get("unit_code"),
|
|
unit_name=payload.get("unit_name"),
|
|
order_num=payload.get("order_num"),
|
|
remark=payload.get("remark"),
|
|
user_id=user.get("user_id"),
|
|
)
|
|
return success_response(item_unit_svc._row_to_dict(obj), message="更新成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.delete("/item-units/{unit_id}")
|
|
async def delete_item_unit(
|
|
unit_id: str,
|
|
_user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
item_unit_svc.soft_delete_item_unit(db, unit_id)
|
|
return success_response(None, message="删除成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 快递公司管理
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class ExpressCompanyCreateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
express_code: str = Field(..., alias="expressCode")
|
|
express_name: str = Field(..., alias="expressName")
|
|
express_logo: Optional[str] = Field(None, alias="expressLogo")
|
|
order_num: Optional[int] = Field(0, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
class ExpressCompanyUpdateBody(BaseModel):
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
express_code: Optional[str] = Field(None, alias="expressCode")
|
|
express_name: Optional[str] = Field(None, alias="expressName")
|
|
express_logo: Optional[str] = Field(None, alias="expressLogo")
|
|
order_num: Optional[int] = Field(None, alias="orderNum")
|
|
remark: Optional[str] = None
|
|
|
|
|
|
@router.get("/express-companies")
|
|
async def list_express_companies(
|
|
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:express:list")),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
tenant_no = _get_tenant_no(db, user)
|
|
items, total = express_company_svc.list_express_companies(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
|
|
tp = express_company_svc.total_pages(total, pageSize)
|
|
return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
|
|
|
|
|
|
@router.post("/express-companies")
|
|
async def create_express_company(
|
|
body: ExpressCompanyCreateBody,
|
|
user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
obj = express_company_svc.create_express_company(
|
|
db,
|
|
express_code=body.express_code,
|
|
express_name=body.express_name,
|
|
express_logo=body.express_logo,
|
|
tenant_no=tenant_no,
|
|
order_num=body.order_num,
|
|
remark=body.remark,
|
|
user_id=user.get("user_id"),
|
|
)
|
|
return success_response(express_company_svc._row_to_dict(obj), message="创建成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.put("/express-companies/{company_id}")
|
|
async def update_express_company(
|
|
company_id: str,
|
|
body: ExpressCompanyUpdateBody,
|
|
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 = express_company_svc.update_express_company(
|
|
db,
|
|
company_id,
|
|
express_code=payload.get("express_code"),
|
|
express_name=payload.get("express_name"),
|
|
express_logo=payload.get("express_logo"),
|
|
order_num=payload.get("order_num"),
|
|
remark=payload.get("remark"),
|
|
user_id=user.get("user_id"),
|
|
)
|
|
return success_response(express_company_svc._row_to_dict(obj), message="更新成功")
|
|
except ValueError as e:
|
|
return error_response(400, str(e))
|
|
|
|
|
|
@router.delete("/express-companies/{company_id}")
|
|
async def delete_express_company(
|
|
company_id: str,
|
|
_user: dict = Depends(require_admin_or_super_admin),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
express_company_svc.soft_delete_express_company(db, company_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_strict_menu_code("system:menu:list", allow_super_admin=True)),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
tenant_no = _get_tenant_no(db, user)
|
|
if body.parent_id and body.parent_id != "0":
|
|
validate_tenant_references(db, SysMenu, [body.parent_id], _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_strict_menu_code("system:menu:list", allow_super_admin=True)),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
validate_tenant_references(db, SysMenu, [menu_id], _get_tenant_no(db, user))
|
|
payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
|
|
if body.parent_id and body.parent_id != "0":
|
|
validate_tenant_references(db, SysMenu, [body.parent_id], _get_tenant_no(db, user))
|
|
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_strict_menu_code("system:menu:list", allow_super_admin=True)),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
validate_tenant_references(db, SysMenu, [menu_id], _get_tenant_no(db, _user))
|
|
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_strict_menu_code("system:role:list", allow_super_admin=True)),
|
|
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_strict_menu_code("system:role:list", allow_super_admin=True)),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
validate_tenant_references(db, SysRole, [role_id], _get_tenant_no(db, user))
|
|
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_strict_menu_code("system:role:list", allow_super_admin=True)),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
validate_tenant_references(db, SysRole, [role_id], _get_tenant_no(db, _user))
|
|
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_strict_menu_code("system:role:list", allow_super_admin=True)),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
role = role_svc.get_role(db, role_id)
|
|
if not role or role.tenant_no != _get_tenant_no(db, _user):
|
|
return error_response(404, "角色不存在")
|
|
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_strict_menu_code("system:role:list", allow_super_admin=True)),
|
|
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_strict_menu_code("system:role:list", allow_super_admin=True)),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
role = role_svc.get_role(db, role_id)
|
|
if not role or role.tenant_no != _get_tenant_no(db, _user):
|
|
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_strict_menu_code("system:role:list", allow_super_admin=True)),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
try:
|
|
validate_tenant_references(db, SysRole, [role_id], _get_tenant_no(db, user))
|
|
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(""),
|
|
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:
|
|
file_content = await file.read()
|
|
# MinIO/本地写入是同步的。多张大图并行时若占住事件循环,其它上传会一直等不到响应,前端表现为超时后文件消失。
|
|
full_url = await asyncio.to_thread(
|
|
storage.upload_file, file_content, file.filename, folder or ""
|
|
)
|
|
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 = BizAttachment(
|
|
id=uuid.uuid4().hex,
|
|
original_name=file.filename,
|
|
file_ext=file_ext,
|
|
file_url=file_url,
|
|
file_size=file_size,
|
|
file_type=file_type,
|
|
tenant_no=tenant_no,
|
|
create_by=user.get("user_id"),
|
|
)
|
|
db.add(record)
|
|
db.commit()
|
|
db.refresh(record)
|
|
return success_response({
|
|
"id": record.id,
|
|
"fileName": record.original_name,
|
|
"fileUrl": record.file_url,
|
|
"fileSize": record.file_size,
|
|
"fileSizeDisplay": _format_file_size(record.file_size or 0),
|
|
"fileType": record.file_type,
|
|
})
|
|
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 = _lookup_attachment_by_url(db, file_url, tenant_no)
|
|
if not att:
|
|
return error_response(403, "无权下载该附件或附件未登记")
|
|
download_name = (body.file_name or "").strip()
|
|
if not download_name:
|
|
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)}")
|