| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603 |
- """运营端管理接口(租户、角色下拉等)"""
- import os
- import uuid
- from datetime import datetime
- from typing import Any, List, Optional
- from fastapi import APIRouter, Depends, HTTPException, Query, Request, UploadFile, File, Form
- from pydantic import BaseModel, ConfigDict, Field
- from sqlalchemy import and_, func, or_
- from sqlalchemy.orm import Session
- from app.core.config import settings
- from app.core.data_scope import apply_data_scope, assert_row_in_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.services import dict_ensure as dict_ensure_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, SysDictType, SysPost, SysRole, SysTenant, SysUser, SysUserRole
- from app.schemas.menu_favorite import (
- MenuFavoriteCreateBody,
- MenuFavoriteDeleteBody,
- MenuFavoriteReorderBody,
- )
- from app.services.web_auth_roles import _map_role_name_to_web
- from app.services.column_facet import list_column_facet_values
- from app.utils.excel_export import (
- EXPORT_MAX_ROWS,
- ExportColumn,
- ExportValidationError,
- export_rows_to_xlsx_response,
- filter_export_columns,
- )
- router = APIRouter(prefix="/web/admin", tags=["运营端管理"])
- # ---------------------------------------------------------------------------
- # 角色下拉(用户弹窗等)
- # ---------------------------------------------------------------------------
- @router.get("/roles")
- 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")
- def list_users_for_select(
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- """返回用户下拉选项(label=昵称,value=用户ID,附带 nickName/userNo)。
- 供申请人/审批人等选择场景使用,返回本租户全部有效用户,
- 不做业务数据范围过滤(否则「仅本人」角色会按 create_by 滤空,连自己都选不到)。
- """
- tenant_no = _get_tenant_no(db, user)
- q = db.query(SysUser).filter(SysUser.del_flag == 0, SysUser.status == "1")
- 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")
- 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)。
- 角色为租户级主数据选项,不做 create_by 数据范围过滤。
- """
- tenant_no = _get_tenant_no(db, user)
- q = db.query(SysRole).filter(SysRole.del_flag == 0, SysRole.status == "1")
- 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")
- def list_tenants(
- request: Request,
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=500, 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,
- tenant_no_like=request.query_params.get("tenantNo"),
- tenant_name=request.query_params.get("tenantName"),
- contact_name=request.query_params.get("contactName"),
- contact_phone=request.query_params.get("contactPhone"),
- tenant_no_in=request.query_params.get("tenantNoIn"),
- tenant_name_in=request.query_params.get("tenantNameIn"),
- contact_name_in=request.query_params.get("contactNameIn"),
- contact_phone_in=request.query_params.get("contactPhoneIn"),
- 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,
- }
- )
- class TenantColumnFacetBody(BaseModel):
- """租户列表列头可选值(去重;可附带列表上下文)"""
- model_config = ConfigDict(populate_by_name=True, extra="allow")
- field: str = Field(..., description="tenantNo | tenantName | contactName | contactPhone")
- keyword: Optional[str] = Field(default=None, description="可选值关键字")
- limit: Optional[int] = Field(default=500, ge=1, le=500, description="返回条数上限")
- _TENANT_FACET_COLUMNS = {
- "tenantNo": SysTenant.tenant_no,
- "tenantName": SysTenant.tenant_name,
- "contactName": SysTenant.contact_name,
- "contactPhone": SysTenant.contact_phone,
- }
- @router.post("/tenants/column-facet-values")
- def tenant_column_facet_values(
- body: TenantColumnFacetBody,
- _user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- """租户列表列头可选值:在列表上下文(排除本列)下去重。"""
- from app.services.pl.facet_context import strip_simple_facet_self_params
- column = _TENANT_FACET_COLUMNS.get(body.field)
- if column is None:
- return error_response(400, "不支持的列")
- try:
- lim = min(max(1, int(body.limit or 500)), 500)
- list_keyword = (body.keyword or "").strip() or None
- params = strip_simple_facet_self_params(_facet_body_params(body), body.field)
- q = (
- db.query(column.label("value"), func.count(SysTenant.id).label("cnt"))
- .filter(
- SysTenant.del_flag == 0,
- column.isnot(None),
- column != "",
- )
- )
- q = apply_data_scope(q, SysTenant, _user, db)
- kw = list_keyword or str(_param_first(params, "keyword") or "").strip()
- if kw:
- like = f"%{kw}%"
- q = q.filter(
- or_(
- SysTenant.tenant_no.like(like),
- SysTenant.tenant_name.like(like),
- )
- )
- for col, camel, snake in (
- (SysTenant.tenant_no, "tenantNo", "tenant_no"),
- (SysTenant.tenant_name, "tenantName", "tenant_name"),
- (SysTenant.contact_name, "contactName", "contact_name"),
- (SysTenant.contact_phone, "contactPhone", "contact_phone"),
- ):
- values = _split_csv_param(params, f"{camel}In", f"{snake}_in")
- if values:
- q = q.filter(col.in_(values))
- else:
- single = str(_param_first(params, camel, snake) or "").strip()
- if single:
- q = q.filter(col.like(f"%{single}%"))
- status_values = _split_csv_param(params, "statusIn", "status_in")
- if not status_values:
- st = _param_first(params, "status")
- if st:
- status_values = [str(st).strip()]
- if status_values:
- q = q.filter(SysTenant.status.in_(status_values))
- rows = q.group_by(column).order_by(column.asc()).limit(lim).all()
- out = []
- for row in rows:
- value = str(row.value).strip() if row.value is not None else ""
- if not value:
- continue
- out.append({"value": value, "label": value, "count": int(row.cnt or 0)})
- return success_response({"list": out})
- except ValueError as e:
- return error_response(400, str(e))
- except Exception as e:
- return error_response(500, str(e))
- TENANT_EXPORT_COLUMNS = [
- ExportColumn(title="租户编号", field="tenantNo"),
- ExportColumn(title="租户名称", field="tenantName"),
- ExportColumn(title="联系人", field="contactName"),
- ExportColumn(title="联系电话", field="contactPhone"),
- ExportColumn(title="创建时间", field="createTime"),
- ]
- class TenantExportBody(BaseModel):
- keyword: Optional[str] = None
- ids: Optional[List[str]] = None
- columns: Optional[List[str]] = None
- @router.post("/tenants/export")
- def export_tenants(
- body: TenantExportBody,
- _user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- """导出租户列表(当前筛选全量,或 ids 已选)。"""
- try:
- items, _ = tenant_svc.list_tenants(
- db,
- page=1,
- page_size=EXPORT_MAX_ROWS,
- keyword=body.keyword,
- ids=body.ids,
- max_page_size=EXPORT_MAX_ROWS,
- user_info=_user,
- )
- cols = filter_export_columns(TENANT_EXPORT_COLUMNS, body.columns)
- date_str = datetime.now().strftime("%Y%m%d")
- return export_rows_to_xlsx_response(
- db,
- tenant_no=None,
- sheet_title="租户列表",
- columns=cols,
- rows=items,
- filename_stem=f"租户列表_{date_str}",
- )
- except ExportValidationError as e:
- return error_response(400, str(e))
- except Exception as e:
- return error_response(500, str(e))
- @router.post("/tenants")
- 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}")
- 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}")
- 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, user_info=_user)
- 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"
- # ---------------------------------------------------------------------------
- # 用户管理
- # ---------------------------------------------------------------------------
- def _facet_body_params(body) -> dict:
- data = body.model_dump(by_alias=True, exclude_none=True)
- extra = getattr(body, "model_extra", None) or {}
- for key, value in extra.items():
- if key not in data and value not in (None, ""):
- data[key] = value
- return data
- def _split_csv_param(params: dict, *keys: str):
- for key in keys:
- raw = params.get(key)
- if raw is None or raw == "":
- continue
- return [s.strip() for s in str(raw).split(",") if s.strip()]
- return []
- def _param_first(params: dict, *keys: str):
- for key in keys:
- if key in params and params[key] not in (None, ""):
- return params[key]
- return None
- 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")
- USER_EXPORT_COLUMNS = [
- ExportColumn(title="用户账号", field="userNo"),
- ExportColumn(title="用户名", field="nickName"),
- ExportColumn(title="所属组织", field="deptName"),
- ExportColumn(title="角色", field="roles"),
- ExportColumn(title="手机号", field="phoneNo"),
- ExportColumn(title="状态", field="status", dict_type="sys_status"),
- ExportColumn(title="创建时间", field="createTime"),
- ]
- class UserExportBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- keyword: Optional[str] = None
- dept_id: Optional[str] = Field(None, alias="deptId")
- status: Optional[str] = None
- ids: Optional[List[str]] = None
- columns: Optional[List[str]] = None
- @router.get("/users")
- def list_users(
- request: Request,
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=500, 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),
- )
- )
- user_no = (request.query_params.get("userNo") or "").strip()
- if user_no:
- query = query.filter(SysUser.user_no.like(f"%{user_no}%"))
- nick_name = (request.query_params.get("nickName") or "").strip()
- if nick_name:
- query = query.filter(SysUser.nick_name.like(f"%{nick_name}%"))
- phone_no = (request.query_params.get("phoneNo") or "").strip()
- if phone_no:
- query = query.filter(SysUser.phone_no.like(f"%{phone_no}%"))
- user_no_in = (request.query_params.get("userNoIn") or "").strip()
- user_no_values = [s.strip() for s in user_no_in.split(",") if s.strip()]
- if user_no_values:
- query = query.filter(SysUser.user_no.in_(user_no_values))
- nick_name_in = (request.query_params.get("nickNameIn") or "").strip()
- nick_name_values = [s.strip() for s in nick_name_in.split(",") if s.strip()]
- if nick_name_values:
- query = query.filter(SysUser.nick_name.in_(nick_name_values))
- phone_no_in = (request.query_params.get("phoneNoIn") or "").strip()
- phone_no_values = [s.strip() for s in phone_no_in.split(",") if s.strip()]
- if phone_no_values:
- query = query.filter(SysUser.phone_no.in_(phone_no_values))
- if deptId:
- query = query.filter(SysUser.dept_id == deptId)
- status_in = (request.query_params.get("statusIn") or "").strip()
- status_values = [s.strip() for s in status_in.split(",") if s.strip()]
- if not status_values and status:
- status_values = [status]
- if status_values:
- query = query.filter(SysUser.status.in_(status_values))
- 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
- ]
- user_ids = [d["id"] for d in result_list if d.get("id")]
- role_map: dict[str, list[str]] = {uid: [] for uid in user_ids}
- if user_ids:
- for ur in (
- db.query(SysUserRole)
- .filter(
- SysUserRole.user_id.in_(user_ids),
- SysUserRole.del_flag == 0,
- )
- .all()
- ):
- role_map.setdefault(ur.user_id, []).append(ur.role_id)
- for d in result_list:
- d["roleIds"] = role_map.get(d["id"], [])
- tp = user_svc.total_pages(total, pageSize)
- return success_response({"list": result_list, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
- class UserColumnFacetBody(BaseModel):
- """用户列表列头可选值(去重;可附带列表上下文)"""
- model_config = ConfigDict(populate_by_name=True, extra="allow")
- field: str = Field(..., description="userNo | nickName | phoneNo")
- keyword: Optional[str] = Field(default=None, description="可选值关键字")
- limit: Optional[int] = Field(default=500, ge=1, le=500, description="返回条数上限")
- _USER_FACET_COLUMNS = {
- "userNo": SysUser.user_no,
- "nickName": SysUser.nick_name,
- "phoneNo": SysUser.phone_no,
- }
- @router.post("/users/column-facet-values")
- def user_column_facet_values(
- body: UserColumnFacetBody,
- user: dict = Depends(require_menu_code("system:user:list")),
- db: Session = Depends(get_db),
- ):
- """用户列表列头可选值:在列表上下文(排除本列)下去重。"""
- from app.services.pl.facet_context import strip_simple_facet_self_params
- column = _USER_FACET_COLUMNS.get(body.field)
- if column is None:
- return error_response(400, "不支持的列")
- try:
- tenant_no = _get_tenant_no(db, user)
- list_keyword = (body.keyword or "").strip() or None
- params = strip_simple_facet_self_params(_facet_body_params(body), body.field)
- extra = []
- kw = list_keyword or (_param_first(params, "keyword") or "")
- kw = str(kw).strip()
- if kw:
- like = f"%{kw}%"
- extra.append(
- or_(
- SysUser.user_no.like(like),
- SysUser.nick_name.like(like),
- SysUser.phone_no.like(like),
- )
- )
- for col, camel, snake in (
- (SysUser.user_no, "userNo", "user_no"),
- (SysUser.nick_name, "nickName", "nick_name"),
- (SysUser.phone_no, "phoneNo", "phone_no"),
- ):
- values = _split_csv_param(params, f"{camel}In", f"{snake}_in")
- if values:
- extra.append(col.in_(values))
- else:
- single = (_param_first(params, camel, snake) or "")
- if str(single).strip():
- extra.append(col.like(f"%{str(single).strip()}%"))
- dept_id = _param_first(params, "deptId", "dept_id")
- if dept_id:
- extra.append(SysUser.dept_id == dept_id)
- status_values = _split_csv_param(params, "statusIn", "status_in")
- if not status_values:
- st = _param_first(params, "status")
- if st:
- status_values = [str(st).strip()]
- if status_values:
- extra.append(SysUser.status.in_(status_values))
- rows = list_column_facet_values(
- db,
- SysUser,
- column,
- tenant_no=tenant_no,
- keyword=None,
- limit=body.limit if body.limit is not None else 500,
- apply_scope=lambda q: apply_data_scope(q, SysUser, user, db, tenant_no),
- extra_filters=extra or None,
- )
- return success_response({"list": rows})
- except ValueError as e:
- return error_response(400, str(e))
- except Exception as e:
- return error_response(500, str(e))
- @router.post("/users/export")
- def export_users(
- body: UserExportBody,
- user: dict = Depends(require_menu_code("system:user:list")),
- db: Session = Depends(get_db),
- ):
- """导出用户列表(当前筛选全量,或 ids 已选)。"""
- try:
- tenant_no = _get_tenant_no(db, user)
- query = db.query(SysUser).filter(
- SysUser.del_flag == 0,
- SysUser.tenant_no == tenant_no,
- )
- kw = (body.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 body.dept_id:
- query = query.filter(SysUser.dept_id == body.dept_id)
- if body.status:
- query = query.filter(SysUser.status == body.status)
- if body.ids:
- query = query.filter(SysUser.id.in_(body.ids))
- query = apply_data_scope(query, SysUser, user, db, tenant_no)
- rows_orm = query.order_by(SysUser.create_time.desc()).all()
- dept_ids = list({r.dept_id for r in rows_orm 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 rows_orm
- ]
- user_ids = [d["id"] for d in result_list if d.get("id")]
- role_map: dict[str, list[str]] = {uid: [] for uid in user_ids}
- role_name_by_id: dict[str, str] = {}
- if user_ids:
- for ur in (
- db.query(SysUserRole)
- .filter(
- SysUserRole.user_id.in_(user_ids),
- SysUserRole.del_flag == 0,
- )
- .all()
- ):
- role_map.setdefault(ur.user_id, []).append(ur.role_id)
- role_ids = list({rid for ids in role_map.values() for rid in ids})
- if role_ids:
- for role in db.query(SysRole).filter(SysRole.id.in_(role_ids), SysRole.del_flag == 0).all():
- role_name_by_id[role.id] = role.role_name or role.id
- for d in result_list:
- names = [role_name_by_id.get(rid, rid) for rid in role_map.get(d["id"], [])]
- d["roles"] = "、".join(names)
- cols = filter_export_columns(USER_EXPORT_COLUMNS, body.columns)
- date_str = datetime.now().strftime("%Y%m%d")
- return export_rows_to_xlsx_response(
- db,
- tenant_no=tenant_no,
- sheet_title="用户列表",
- columns=cols,
- rows=result_list,
- filename_stem=f"用户列表_{date_str}",
- )
- except ExportValidationError as e:
- return error_response(400, str(e))
- except Exception as e:
- return error_response(500, str(e))
- @router.get("/users/{user_id}")
- 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, "无权查看该用户")
- try:
- assert_row_in_data_scope(db, row, SysUser, user, tenant_no)
- except HTTPException:
- return error_response(403, "无权查看该用户")
- data = user_svc.get_user_with_relations(db, user_id)
- return success_response(data)
- @router.post("/users")
- 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}")
- 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}")
- 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, user_info=_user)
- return success_response(None, message="删除成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.post("/users/{user_id}/reset-password")
- 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")
- def list_dict_types(
- request: Request,
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=500, 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,
- dict_name=request.query_params.get("dictName"),
- dict_type=request.query_params.get("dictType"),
- dict_name_in=request.query_params.get("dictNameIn"),
- dict_type_in=request.query_params.get("dictTypeIn"),
- 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})
- class DictTypeColumnFacetBody(BaseModel):
- """字典类型列表列头可选值(去重;可附带列表上下文)"""
- model_config = ConfigDict(populate_by_name=True, extra="allow")
- field: str = Field(..., description="dictName | dictType")
- keyword: Optional[str] = Field(default=None, description="可选值关键字")
- limit: Optional[int] = Field(default=500, ge=1, le=500, description="返回条数上限")
- _DICT_TYPE_FACET_COLUMNS = {
- "dictName": SysDictType.dict_name,
- "dictType": SysDictType.dict_type,
- }
- @router.post("/dict-types/column-facet-values")
- def dict_type_column_facet_values(
- body: DictTypeColumnFacetBody,
- user: dict = Depends(require_menu_code("system:dict:list")),
- db: Session = Depends(get_db),
- ):
- """字典类型列表列头可选值:在列表上下文(排除本列)下去重。"""
- from app.services.pl.facet_context import strip_simple_facet_self_params
- column = _DICT_TYPE_FACET_COLUMNS.get(body.field)
- if column is None:
- return error_response(400, "不支持的列")
- try:
- tenant_no = _get_tenant_no(db, user)
- list_keyword = (body.keyword or "").strip() or None
- params = strip_simple_facet_self_params(_facet_body_params(body), body.field)
- extra = []
- kw = list_keyword or str(_param_first(params, "keyword") or "").strip()
- if kw:
- like = f"%{kw}%"
- extra.append(or_(SysDictType.dict_name.like(like), SysDictType.dict_type.like(like)))
- for col, camel, snake in (
- (SysDictType.dict_name, "dictName", "dict_name"),
- (SysDictType.dict_type, "dictType", "dict_type"),
- ):
- values = _split_csv_param(params, f"{camel}In", f"{snake}_in")
- if values:
- extra.append(col.in_(values))
- else:
- single = str(_param_first(params, camel, snake) or "").strip()
- if single:
- extra.append(col.like(f"%{single}%"))
- rows = list_column_facet_values(
- db,
- SysDictType,
- column,
- tenant_no=tenant_no,
- keyword=None,
- limit=body.limit if body.limit is not None else 500,
- extra_filters=extra or None,
- )
- return success_response({"list": rows})
- except ValueError as e:
- return error_response(400, str(e))
- except Exception as e:
- return error_response(500, str(e))
- DICT_TYPE_EXPORT_COLUMNS = [
- ExportColumn(title="字典名称", field="dictName"),
- ExportColumn(title="字典类型编码", field="dictType"),
- ExportColumn(title="备注", field="remark"),
- ExportColumn(title="创建时间", field="createTime"),
- ]
- class DictTypeExportBody(BaseModel):
- keyword: Optional[str] = None
- ids: Optional[List[str]] = None
- columns: Optional[List[str]] = None
- @router.post("/dict-types/export")
- def export_dict_types(
- body: DictTypeExportBody,
- user: dict = Depends(require_menu_code("system:dict:list")),
- db: Session = Depends(get_db),
- ):
- """导出字典类型列表(当前筛选全量,或 ids 已选)。"""
- try:
- tenant_no = _get_tenant_no(db, user)
- items, _ = dict_type_svc.list_dict_types(
- db,
- page=1,
- page_size=EXPORT_MAX_ROWS,
- keyword=body.keyword,
- tenant_no=tenant_no,
- ids=body.ids,
- max_page_size=EXPORT_MAX_ROWS,
- user_info=user,
- )
- cols = filter_export_columns(DICT_TYPE_EXPORT_COLUMNS, body.columns)
- date_str = datetime.now().strftime("%Y%m%d")
- return export_rows_to_xlsx_response(
- db,
- tenant_no=tenant_no,
- sheet_title="字典类型",
- columns=cols,
- rows=items,
- filename_stem=f"字典类型_{date_str}",
- )
- except ExportValidationError as e:
- return error_response(400, str(e))
- except Exception as e:
- return error_response(500, str(e))
- @router.post("/dict-types")
- 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}")
- 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}")
- 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, user_info=_user)
- 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")
- def list_dict_data(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=500, 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")
- 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}")
- 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}")
- 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, user_info=_user)
- return success_response(None, message="删除成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.get("/dict-data/options")
- 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, user_info=None
- )
- return success_response(items)
- @router.get("/dict-data/all")
- def dict_data_all(
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- q = db.query(SysDictData).filter(
- and_(SysDictData.tenant_no == tenant_no, SysDictData.del_flag == 0)
- )
- rows = q.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 DictEnsureBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- dict_type: str = Field(..., alias="dictType")
- label: str
- preferred_value: Optional[str] = Field(None, alias="preferredValue")
- class DictEnsureRemoveBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- dict_type: str = Field(..., alias="dictType")
- value: str
- @router.post("/dict-data/ensure")
- def ensure_dict_data_api(
- body: DictEnsureBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- data = dict_ensure_svc.ensure_dict_data(
- db,
- tenant_no=tenant_no,
- dict_type=body.dict_type,
- label=body.label,
- user_id=user.get("user_id") or user.get("id") or "",
- preferred_value=body.preferred_value,
- )
- db.commit()
- return success_response(data, message="已保存")
- except dict_ensure_svc.DictEnsureError as e:
- db.rollback()
- return error_response(400, str(e))
- @router.post("/dict-data/ensure-remove")
- def ensure_remove_dict_data_api(
- body: DictEnsureRemoveBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- try:
- tenant_no = _get_tenant_no(db, user)
- dict_ensure_svc.ensure_remove_dict_data(
- db,
- tenant_no=tenant_no,
- dict_type=body.dict_type,
- value=body.value,
- user_id=user.get("user_id") or user.get("id") or "",
- )
- db.commit()
- return success_response(None, message="已删除")
- except dict_ensure_svc.DictEnsureError as e:
- db.rollback()
- return error_response(400, str(e))
- # ---------------------------------------------------------------------------
- # 组织管理
- # ---------------------------------------------------------------------------
- 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")
- 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)
- DEPT_EXPORT_COLUMNS = [
- ExportColumn(title="组织名称", field="deptName"),
- ExportColumn(title="组织类别", field="deptCategoryCode"),
- ExportColumn(title="状态", field="status", dict_type="sys_status"),
- ExportColumn(title="负责人", field="leader"),
- ExportColumn(title="联系电话", field="leaderPhone"),
- ExportColumn(title="创建时间", field="createTime"),
- ]
- class DeptExportBody(BaseModel):
- keyword: Optional[str] = None
- ids: Optional[List[str]] = None
- columns: Optional[List[str]] = None
- @router.post("/depts/export")
- def export_depts(
- body: DeptExportBody,
- user: dict = Depends(require_menu_code("system:dept:list")),
- db: Session = Depends(get_db),
- ):
- """导出组织列表(扁平行;当前筛选或已选)。"""
- try:
- tenant_no = _get_tenant_no(db, user)
- rows = dept_svc.list_depts(db, keyword=body.keyword, tenant_no=tenant_no, user_info=user)
- if body.ids:
- idset = set(body.ids)
- rows = [r for r in rows if r.get("id") in idset]
- cols = filter_export_columns(DEPT_EXPORT_COLUMNS, body.columns)
- date_str = datetime.now().strftime("%Y%m%d")
- return export_rows_to_xlsx_response(
- db,
- tenant_no=tenant_no,
- sheet_title="组织列表",
- columns=cols,
- rows=rows,
- filename_stem=f"组织列表_{date_str}",
- )
- except ExportValidationError as e:
- return error_response(400, str(e))
- except Exception as e:
- return error_response(500, str(e))
- @router.post("/depts")
- 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}")
- 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}")
- 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, user_info=_user)
- 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")
- def list_sites(
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=500, 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})
- SITE_EXPORT_COLUMNS = [
- ExportColumn(title="站点名称", field="siteName"),
- ExportColumn(title="站点编码", field="siteCode"),
- ExportColumn(title="站点类型", field="siteType"),
- ExportColumn(title="状态", field="status", dict_type="sys_status"),
- ExportColumn(title="地址", field="address"),
- ExportColumn(title="联系人", field="contactName"),
- ExportColumn(title="联系电话", field="contactPhone"),
- ExportColumn(title="创建时间", field="createTime"),
- ]
- class SiteExportBody(BaseModel):
- keyword: Optional[str] = None
- ids: Optional[List[str]] = None
- columns: Optional[List[str]] = None
- @router.post("/sites/export")
- def export_sites(
- body: SiteExportBody,
- user: dict = Depends(require_menu_code("system:site:list")),
- db: Session = Depends(get_db),
- ):
- """导出站点列表(当前筛选全量,或 ids 已选)。"""
- try:
- tenant_no = _get_tenant_no(db, user)
- items, _ = site_svc.list_sites(
- db,
- page=1,
- page_size=EXPORT_MAX_ROWS,
- keyword=body.keyword,
- tenant_no=tenant_no,
- ids=body.ids,
- max_page_size=EXPORT_MAX_ROWS,
- user_info=user,
- )
- cols = filter_export_columns(SITE_EXPORT_COLUMNS, body.columns)
- date_str = datetime.now().strftime("%Y%m%d")
- return export_rows_to_xlsx_response(
- db,
- tenant_no=tenant_no,
- sheet_title="站点列表",
- columns=cols,
- rows=items,
- filename_stem=f"站点列表_{date_str}",
- )
- except ExportValidationError as e:
- return error_response(400, str(e))
- except Exception as e:
- return error_response(500, str(e))
- @router.post("/sites")
- 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}")
- 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}")
- 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, user_info=_user)
- 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")
- def list_posts(
- request: Request,
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=500, 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,
- post_name=request.query_params.get("postName"),
- post_code=request.query_params.get("postCode"),
- post_name_in=request.query_params.get("postNameIn"),
- post_code_in=request.query_params.get("postCodeIn"),
- status=request.query_params.get("status"),
- status_in=request.query_params.get("statusIn"),
- 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})
- class PostColumnFacetBody(BaseModel):
- """职位列表列头可选值(去重;可附带列表上下文)"""
- model_config = ConfigDict(populate_by_name=True, extra="allow")
- field: str = Field(..., description="postName | postCode")
- keyword: Optional[str] = Field(default=None, description="可选值关键字")
- limit: Optional[int] = Field(default=500, ge=1, le=500, description="返回条数上限")
- _POST_FACET_COLUMNS = {
- "postName": SysPost.post_name,
- "postCode": SysPost.post_code,
- }
- @router.post("/posts/column-facet-values")
- def post_column_facet_values(
- body: PostColumnFacetBody,
- user: dict = Depends(require_menu_code("system:post:list")),
- db: Session = Depends(get_db),
- ):
- """职位列表列头可选值:在列表上下文(排除本列)下去重。"""
- from app.services.pl.facet_context import strip_simple_facet_self_params
- column = _POST_FACET_COLUMNS.get(body.field)
- if column is None:
- return error_response(400, "不支持的列")
- try:
- tenant_no = _get_tenant_no(db, user)
- list_keyword = (body.keyword or "").strip() or None
- params = strip_simple_facet_self_params(_facet_body_params(body), body.field)
- extra = []
- kw = list_keyword or str(_param_first(params, "keyword") or "").strip()
- if kw:
- like = f"%{kw}%"
- extra.append(or_(SysPost.post_name.like(like), SysPost.post_code.like(like)))
- for col, camel, snake in (
- (SysPost.post_name, "postName", "post_name"),
- (SysPost.post_code, "postCode", "post_code"),
- ):
- values = _split_csv_param(params, f"{camel}In", f"{snake}_in")
- if values:
- extra.append(col.in_(values))
- else:
- single = str(_param_first(params, camel, snake) or "").strip()
- if single:
- extra.append(col.like(f"%{single}%"))
- status_values = _split_csv_param(params, "statusIn", "status_in")
- if not status_values:
- st = _param_first(params, "status")
- if st:
- status_values = [str(st).strip()]
- if status_values:
- extra.append(SysPost.status.in_(status_values))
- rows = list_column_facet_values(
- db,
- SysPost,
- column,
- tenant_no=tenant_no,
- keyword=None,
- limit=body.limit if body.limit is not None else 500,
- extra_filters=extra or None,
- )
- return success_response({"list": rows})
- except ValueError as e:
- return error_response(400, str(e))
- except Exception as e:
- return error_response(500, str(e))
- POST_EXPORT_COLUMNS = [
- ExportColumn(title="职位名称", field="postName"),
- ExportColumn(title="职位编码", field="postCode"),
- ExportColumn(title="状态", field="status", dict_type="sys_status"),
- ExportColumn(title="创建时间", field="createTime"),
- ]
- class PostExportBody(BaseModel):
- keyword: Optional[str] = None
- ids: Optional[List[str]] = None
- columns: Optional[List[str]] = None
- @router.post("/posts/export")
- def export_posts(
- body: PostExportBody,
- user: dict = Depends(require_menu_code("system:post:list")),
- db: Session = Depends(get_db),
- ):
- """导出职位列表(当前筛选全量,或 ids 已选)。"""
- try:
- tenant_no = _get_tenant_no(db, user)
- items, _ = post_svc.list_posts(
- db,
- page=1,
- page_size=EXPORT_MAX_ROWS,
- keyword=body.keyword,
- tenant_no=tenant_no,
- ids=body.ids,
- max_page_size=EXPORT_MAX_ROWS,
- user_info=user,
- )
- cols = filter_export_columns(POST_EXPORT_COLUMNS, body.columns)
- date_str = datetime.now().strftime("%Y%m%d")
- return export_rows_to_xlsx_response(
- db,
- tenant_no=tenant_no,
- sheet_title="职位列表",
- columns=cols,
- rows=items,
- filename_stem=f"职位列表_{date_str}",
- )
- except ExportValidationError as e:
- return error_response(400, str(e))
- except Exception as e:
- return error_response(500, str(e))
- @router.post("/posts")
- 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}")
- 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}")
- 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, user_info=_user)
- 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")
- 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")
- 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")
- 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)
- MENU_EXPORT_COLUMNS = [
- ExportColumn(title="菜单名称", field="menuName"),
- ExportColumn(title="菜单类型", field="menuType"),
- ExportColumn(title="路由", field="path"),
- ExportColumn(title="组件", field="component"),
- ExportColumn(title="权限标识", field="perms"),
- ExportColumn(title="状态", field="status", dict_type="sys_status"),
- ExportColumn(title="可见", field="visible"),
- ExportColumn(title="创建时间", field="createTime"),
- ]
- class MenuExportBody(BaseModel):
- keyword: Optional[str] = None
- ids: Optional[List[str]] = None
- columns: Optional[List[str]] = None
- @router.post("/menus/export")
- def export_menus(
- body: MenuExportBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- """导出菜单列表(扁平行;当前筛选或已选)。"""
- try:
- tenant_no = _get_tenant_no(db, user)
- # list flat via tree then flatten, or use underlying list if exists
- from app.crud import menu as menu_mod
- if hasattr(menu_mod, 'list_menus'):
- rows = menu_mod.list_menus(db, keyword=body.keyword, tenant_no=tenant_no, user_info=user)
- else:
- tree = menu_svc.list_menus_tree(db, keyword=body.keyword, tenant_no=tenant_no, user_info=user)
- rows = []
- def walk(nodes):
- for n in nodes or []:
- item = {k: v for k, v in n.items() if k != 'children'}
- rows.append(item)
- walk(n.get('children') or [])
- walk(tree)
- if body.ids:
- idset = set(body.ids)
- rows = [r for r in rows if r.get("id") in idset]
- cols = filter_export_columns(MENU_EXPORT_COLUMNS, body.columns)
- date_str = datetime.now().strftime("%Y%m%d")
- return export_rows_to_xlsx_response(
- db,
- tenant_no=tenant_no,
- sheet_title="菜单列表",
- columns=cols,
- rows=rows,
- filename_stem=f"菜单列表_{date_str}",
- )
- except ExportValidationError as e:
- return error_response(400, str(e))
- except Exception as e:
- return error_response(500, str(e))
- @router.post("/menus")
- 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}")
- 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}")
- 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, user_info=_user)
- 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")
- def list_role_management(
- request: Request,
- page: int = Query(1, ge=1),
- pageSize: int = Query(10, ge=1, le=500, 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,
- role_name=request.query_params.get("roleName"),
- role_name_in=request.query_params.get("roleNameIn"),
- status=request.query_params.get("status"),
- status_in=request.query_params.get("statusIn"),
- 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})
- class RoleColumnFacetBody(BaseModel):
- """角色列表列头可选值(去重;可附带列表上下文)"""
- model_config = ConfigDict(populate_by_name=True, extra="allow")
- field: str = Field(..., description="roleName")
- keyword: Optional[str] = Field(default=None, description="可选值关键字")
- limit: Optional[int] = Field(default=500, ge=1, le=500, description="返回条数上限")
- _ROLE_FACET_COLUMNS = {
- "roleName": SysRole.role_name,
- }
- @router.post("/role-management/column-facet-values")
- def role_column_facet_values(
- body: RoleColumnFacetBody,
- user: dict = Depends(require_menu_code("system:role:list")),
- db: Session = Depends(get_db),
- ):
- """角色列表列头可选值:在列表上下文(排除本列)下去重。"""
- from app.services.pl.facet_context import strip_simple_facet_self_params
- column = _ROLE_FACET_COLUMNS.get(body.field)
- if column is None:
- return error_response(400, "不支持的列")
- try:
- tenant_no = _get_tenant_no(db, user)
- list_keyword = (body.keyword or "").strip() or None
- params = strip_simple_facet_self_params(_facet_body_params(body), body.field)
- extra = []
- kw = list_keyword or str(_param_first(params, "keyword") or "").strip()
- if kw:
- extra.append(SysRole.role_name.like(f"%{kw}%"))
- values = _split_csv_param(params, "roleNameIn", "role_name_in")
- if values:
- extra.append(SysRole.role_name.in_(values))
- else:
- single = str(_param_first(params, "roleName", "role_name") or "").strip()
- if single:
- extra.append(SysRole.role_name.like(f"%{single}%"))
- status_values = _split_csv_param(params, "statusIn", "status_in")
- if not status_values:
- st = _param_first(params, "status")
- if st:
- status_values = [str(st).strip()]
- if status_values:
- extra.append(SysRole.status.in_(status_values))
- rows = list_column_facet_values(
- db,
- SysRole,
- column,
- tenant_no=tenant_no,
- keyword=None,
- limit=body.limit if body.limit is not None else 500,
- apply_scope=lambda q: apply_data_scope(q, SysRole, user, db, tenant_no),
- extra_filters=extra or None,
- )
- return success_response({"list": rows})
- except ValueError as e:
- return error_response(400, str(e))
- except Exception as e:
- return error_response(500, str(e))
- ROLE_EXPORT_COLUMNS = [
- ExportColumn(title="角色名称", field="roleName"),
- ExportColumn(title="数据权限", field="dataScope", dict_type="sys_data_scope"),
- ExportColumn(title="状态", field="status", dict_type="sys_status"),
- ExportColumn(title="创建时间", field="createTime"),
- ]
- class RoleExportBody(BaseModel):
- keyword: Optional[str] = None
- ids: Optional[List[str]] = None
- columns: Optional[List[str]] = None
- @router.post("/role-management/export")
- def export_role_management(
- body: RoleExportBody,
- user: dict = Depends(require_menu_code("system:role:list")),
- db: Session = Depends(get_db),
- ):
- """导出角色列表(当前筛选全量,或 ids 已选)。"""
- try:
- tenant_no = _get_tenant_no(db, user)
- items, _ = role_svc.list_roles(
- db,
- page=1,
- page_size=EXPORT_MAX_ROWS,
- keyword=body.keyword,
- tenant_no=tenant_no,
- ids=body.ids,
- max_page_size=EXPORT_MAX_ROWS,
- user_info=user,
- )
- cols = filter_export_columns(ROLE_EXPORT_COLUMNS, body.columns)
- date_str = datetime.now().strftime("%Y%m%d")
- return export_rows_to_xlsx_response(
- db,
- tenant_no=tenant_no,
- sheet_title="角色列表",
- columns=cols,
- rows=items,
- filename_stem=f"角色列表_{date_str}",
- )
- except ExportValidationError as e:
- return error_response(400, str(e))
- except Exception as e:
- return error_response(500, str(e))
- @router.post("/role-management")
- 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.get("/role-management/{role_id}")
- def get_role_management(
- role_id: str,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- tenant_no = _get_tenant_no(db, user)
- obj = role_svc.get_role(db, role_id)
- if not obj or obj.tenant_no != tenant_no:
- return error_response(404, "角色不存在")
- try:
- assert_row_in_data_scope(db, obj, SysRole, user, tenant_no)
- except HTTPException:
- return error_response(403, "无权查看该角色")
- return success_response(role_svc._row_to_dict(obj))
- @router.put("/role-management/{role_id}")
- 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}")
- 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, user_info=_user)
- return success_response(None, message="删除成功")
- except ValueError as e:
- return error_response(400, str(e))
- @router.get("/role-management/{role_id}/menus")
- 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")
- 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")
- 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")
- 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")
- 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 = file.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")
- 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)
- file_url, att = oss_svc.resolve_storage_path(db, tenant_no, file_url)
- if not att:
- 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)}")
- @router.post("/attachment/preview-url")
- def attachment_preview_url(
- body: AttachmentDownloadUrlBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- """返回用于浏览器内联预览的 URL(PDF iframe 等)。"""
- file_url = (body.file_url or "").strip()
- if not file_url:
- return error_response(400, "文件地址不能为空")
- tenant_no = _get_tenant_no(db, user)
- file_url, _att = oss_svc.resolve_storage_path(db, tenant_no, file_url)
- try:
- url = storage.presigned_preview_url(file_url)
- return success_response({"previewUrl": url})
- except Exception as e:
- return error_response(500, f"获取预览链接失败: {str(e)}")
- # ---------------------------------------------------------------------------
- # 用户菜单收藏
- # ---------------------------------------------------------------------------
- @router.get("/menu-favorites")
- def list_menu_favorites(
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- user_id = user.get("user_id")
- if not user_id:
- return error_response(401, "用户无效")
- tenant_no = _get_tenant_no(db, user)
- from app.crud import menu_favorite as fav_svc
- return success_response(fav_svc.list_favorites(db, user_id=user_id, tenant_no=tenant_no))
- @router.post("/menu-favorites")
- def create_menu_favorite(
- body: MenuFavoriteCreateBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- from app.crud import menu_favorite as fav_svc
- user_id = user.get("user_id")
- if not user_id:
- return error_response(401, "用户无效")
- tenant_no = _get_tenant_no(db, user)
- try:
- data = fav_svc.add_favorite(
- db,
- user_id=user_id,
- tenant_no=tenant_no,
- menu_path=body.path,
- create_dept_id=str(user.get("dept_id") or ""),
- )
- return success_response(data)
- except ValueError as e:
- return error_response(400, str(e))
- @router.put("/menu-favorites/order")
- def reorder_menu_favorites(
- body: MenuFavoriteReorderBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- from app.crud import menu_favorite as fav_svc
- user_id = user.get("user_id")
- if not user_id:
- return error_response(401, "用户无效")
- tenant_no = _get_tenant_no(db, user)
- try:
- data = fav_svc.reorder_favorites(
- db, user_id=user_id, tenant_no=tenant_no, paths=body.paths or []
- )
- return success_response(data)
- except ValueError as e:
- return error_response(400, str(e))
- @router.delete("/menu-favorites")
- def delete_menu_favorite(
- body: MenuFavoriteDeleteBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- from app.crud import menu_favorite as fav_svc
- user_id = user.get("user_id")
- if not user_id:
- return error_response(401, "用户无效")
- tenant_no = _get_tenant_no(db, user)
- fav_svc.soft_delete_favorite(
- db, user_id=user_id, tenant_no=tenant_no, menu_path=body.path
- )
- return success_response({"path": body.path})
- # ---------------------------------------------------------------------------
- # Demo 模板页:Mock 数据由前端组装行后走统一 xlsx 导出(无 Demo 业务表)
- # ---------------------------------------------------------------------------
- class DemoXlsxColumnSpec(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- title: str
- field: str
- class DemoXlsxExportBody(BaseModel):
- model_config = ConfigDict(populate_by_name=True)
- sheet_title: str = Field(default="导出", alias="sheetTitle")
- filename_stem: str = Field(default="export", alias="filenameStem")
- columns: List[DemoXlsxColumnSpec]
- rows: List[dict]
- @router.post("/demo/xlsx-export")
- def demo_xlsx_export(
- body: DemoXlsxExportBody,
- user: dict = Depends(require_admin_or_super_admin),
- db: Session = Depends(get_db),
- ):
- """Demo 列表导出:接收已筛选行与列定义,生成 xlsx。"""
- try:
- rows = body.rows or []
- if not rows:
- raise ExportValidationError("没有可导出的数据")
- if len(rows) > EXPORT_MAX_ROWS:
- raise ExportValidationError(f"导出超过 {EXPORT_MAX_ROWS} 条,请缩小筛选范围")
- if not body.columns:
- raise ExportValidationError("没有可导出的列")
- cols = [ExportColumn(title=c.title, field=c.field) for c in body.columns]
- tenant_no = _get_tenant_no(db, user)
- date_str = datetime.now().strftime("%Y%m%d")
- stem = (body.filename_stem or "export").strip() or "export"
- return export_rows_to_xlsx_response(
- db,
- tenant_no=tenant_no,
- sheet_title=body.sheet_title or "导出",
- columns=cols,
- rows=rows,
- filename_stem=f"{stem}_{date_str}",
- )
- except ExportValidationError as e:
- return error_response(400, str(e))
|