admin.py 90 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603
  1. """运营端管理接口(租户、角色下拉等)"""
  2. import os
  3. import uuid
  4. from datetime import datetime
  5. from typing import Any, List, Optional
  6. from fastapi import APIRouter, Depends, HTTPException, Query, Request, UploadFile, File, Form
  7. from pydantic import BaseModel, ConfigDict, Field
  8. from sqlalchemy import and_, func, or_
  9. from sqlalchemy.orm import Session
  10. from app.core.config import settings
  11. from app.core.data_scope import apply_data_scope, assert_row_in_data_scope
  12. from app.core.database import get_db
  13. from app.core.menu_guard import require_admin_or_super_admin, require_menu_code
  14. from app.core.response import success_response, error_response
  15. from app.core.attachment_biz import resolve_upload_folder
  16. from app.core.storage import storage
  17. from app.crud import tenant as tenant_svc
  18. from app.crud import dict_type as dict_type_svc
  19. from app.crud import dict_data as dict_data_svc
  20. from app.services import dict_ensure as dict_ensure_svc
  21. from app.crud import dept as dept_svc
  22. from app.crud import site as site_svc
  23. from app.crud import post as post_svc
  24. from app.crud import menu as menu_svc
  25. from app.crud import role as role_svc
  26. from app.crud import user as user_svc
  27. from app.crud import role as role_svc
  28. from app.crud import user as user_svc
  29. from app.crud import oss as oss_svc
  30. from app.models.models import SysDept, SysDictData, SysDictType, SysPost, SysRole, SysTenant, SysUser, SysUserRole
  31. from app.schemas.menu_favorite import (
  32. MenuFavoriteCreateBody,
  33. MenuFavoriteDeleteBody,
  34. MenuFavoriteReorderBody,
  35. )
  36. from app.services.web_auth_roles import _map_role_name_to_web
  37. from app.services.column_facet import list_column_facet_values
  38. from app.utils.excel_export import (
  39. EXPORT_MAX_ROWS,
  40. ExportColumn,
  41. ExportValidationError,
  42. export_rows_to_xlsx_response,
  43. filter_export_columns,
  44. )
  45. router = APIRouter(prefix="/web/admin", tags=["运营端管理"])
  46. # ---------------------------------------------------------------------------
  47. # 角色下拉(用户弹窗等)
  48. # ---------------------------------------------------------------------------
  49. @router.get("/roles")
  50. def list_roles_for_select(
  51. _user: dict = Depends(require_admin_or_super_admin),
  52. db: Session = Depends(get_db),
  53. ):
  54. """返回运营端角色下拉选项(label=库中角色名,value=Web 角色码)。"""
  55. rows: List[SysRole] = (
  56. db.query(SysRole)
  57. .filter(SysRole.del_flag == 0, SysRole.status == "1")
  58. .order_by(SysRole.create_time.desc())
  59. .all()
  60. )
  61. seen: set[str] = set()
  62. out: List[dict] = []
  63. for r in rows:
  64. name = (r.role_name or "").strip()
  65. if not name:
  66. continue
  67. code = _map_role_name_to_web(name)
  68. key = f"{code}:{name}"
  69. if key in seen:
  70. continue
  71. seen.add(key)
  72. out.append({"label": name, "value": code})
  73. if not out:
  74. out = [
  75. {"label": "超级管理员", "value": "super_admin"},
  76. {"label": "运营管理员", "value": "admin"},
  77. ]
  78. return success_response(out)
  79. @router.get("/users/options")
  80. def list_users_for_select(
  81. user: dict = Depends(require_admin_or_super_admin),
  82. db: Session = Depends(get_db),
  83. ):
  84. """返回用户下拉选项(label=昵称,value=用户ID,附带 nickName/userNo)。
  85. 供申请人/审批人等选择场景使用,返回本租户全部有效用户,
  86. 不做业务数据范围过滤(否则「仅本人」角色会按 create_by 滤空,连自己都选不到)。
  87. """
  88. tenant_no = _get_tenant_no(db, user)
  89. q = db.query(SysUser).filter(SysUser.del_flag == 0, SysUser.status == "1")
  90. q = q.filter(SysUser.tenant_no == tenant_no)
  91. rows = q.order_by(SysUser.create_time.desc()).all()
  92. out = [
  93. {
  94. "label": (r.nick_name or r.user_no or ""),
  95. "value": r.id,
  96. "nickName": r.nick_name or "",
  97. "userNo": r.user_no or "",
  98. }
  99. for r in rows
  100. ]
  101. return success_response(out)
  102. @router.get("/roles/options")
  103. def list_roles_for_designer(
  104. user: dict = Depends(require_admin_or_super_admin),
  105. db: Session = Depends(get_db),
  106. ):
  107. """返回角色下拉选项(label=角色名,value=角色ID UUID),供流程设计器审批人选择使用。
  108. 与 /roles 不同,此端点返回角色 UUID 作为 value(引擎解析审批人需要 UUID)。
  109. 角色为租户级主数据选项,不做 create_by 数据范围过滤。
  110. """
  111. tenant_no = _get_tenant_no(db, user)
  112. q = db.query(SysRole).filter(SysRole.del_flag == 0, SysRole.status == "1")
  113. q = q.filter(SysRole.tenant_no == tenant_no)
  114. rows = q.order_by(SysRole.create_time.desc()).all()
  115. out = [{"label": (r.role_name or "").strip(), "value": r.id} for r in rows if (r.role_name or "").strip()]
  116. return success_response(out)
  117. # ---------------------------------------------------------------------------
  118. # 租户管理
  119. # ---------------------------------------------------------------------------
  120. class TenantCreateBody(BaseModel):
  121. model_config = ConfigDict(populate_by_name=True)
  122. tenant_no: str = Field(..., alias="tenantNo")
  123. tenant_name: str = Field(..., alias="tenantName")
  124. contact_name: Optional[str] = Field(None, alias="contactName")
  125. contact_phone: Optional[str] = Field(None, alias="contactPhone")
  126. order_num: Optional[int] = Field(0, alias="orderNum")
  127. remark: Optional[str] = None
  128. class TenantUpdateBody(BaseModel):
  129. model_config = ConfigDict(populate_by_name=True)
  130. tenant_no: Optional[str] = Field(None, alias="tenantNo")
  131. tenant_name: Optional[str] = Field(None, alias="tenantName")
  132. contact_name: Optional[str] = Field(None, alias="contactName")
  133. contact_phone: Optional[str] = Field(None, alias="contactPhone")
  134. order_num: Optional[int] = Field(None, alias="orderNum")
  135. remark: Optional[str] = None
  136. @router.get("/tenants")
  137. def list_tenants(
  138. request: Request,
  139. page: int = Query(1, ge=1),
  140. pageSize: int = Query(10, ge=1, le=500, alias="pageSize"),
  141. keyword: Optional[str] = None,
  142. _user: dict = Depends(require_admin_or_super_admin),
  143. db: Session = Depends(get_db),
  144. ):
  145. items, total = tenant_svc.list_tenants(
  146. db,
  147. page=page,
  148. page_size=pageSize,
  149. keyword=keyword,
  150. tenant_no_like=request.query_params.get("tenantNo"),
  151. tenant_name=request.query_params.get("tenantName"),
  152. contact_name=request.query_params.get("contactName"),
  153. contact_phone=request.query_params.get("contactPhone"),
  154. tenant_no_in=request.query_params.get("tenantNoIn"),
  155. tenant_name_in=request.query_params.get("tenantNameIn"),
  156. contact_name_in=request.query_params.get("contactNameIn"),
  157. contact_phone_in=request.query_params.get("contactPhoneIn"),
  158. user_info=_user,
  159. )
  160. total_pages = tenant_svc.total_pages(total, pageSize)
  161. return success_response(
  162. {
  163. "list": items,
  164. "total": total,
  165. "page": page,
  166. "pageSize": pageSize,
  167. "totalPages": total_pages,
  168. }
  169. )
  170. class TenantColumnFacetBody(BaseModel):
  171. """租户列表列头可选值(去重;可附带列表上下文)"""
  172. model_config = ConfigDict(populate_by_name=True, extra="allow")
  173. field: str = Field(..., description="tenantNo | tenantName | contactName | contactPhone")
  174. keyword: Optional[str] = Field(default=None, description="可选值关键字")
  175. limit: Optional[int] = Field(default=500, ge=1, le=500, description="返回条数上限")
  176. _TENANT_FACET_COLUMNS = {
  177. "tenantNo": SysTenant.tenant_no,
  178. "tenantName": SysTenant.tenant_name,
  179. "contactName": SysTenant.contact_name,
  180. "contactPhone": SysTenant.contact_phone,
  181. }
  182. @router.post("/tenants/column-facet-values")
  183. def tenant_column_facet_values(
  184. body: TenantColumnFacetBody,
  185. _user: dict = Depends(require_admin_or_super_admin),
  186. db: Session = Depends(get_db),
  187. ):
  188. """租户列表列头可选值:在列表上下文(排除本列)下去重。"""
  189. from app.services.pl.facet_context import strip_simple_facet_self_params
  190. column = _TENANT_FACET_COLUMNS.get(body.field)
  191. if column is None:
  192. return error_response(400, "不支持的列")
  193. try:
  194. lim = min(max(1, int(body.limit or 500)), 500)
  195. list_keyword = (body.keyword or "").strip() or None
  196. params = strip_simple_facet_self_params(_facet_body_params(body), body.field)
  197. q = (
  198. db.query(column.label("value"), func.count(SysTenant.id).label("cnt"))
  199. .filter(
  200. SysTenant.del_flag == 0,
  201. column.isnot(None),
  202. column != "",
  203. )
  204. )
  205. q = apply_data_scope(q, SysTenant, _user, db)
  206. kw = list_keyword or str(_param_first(params, "keyword") or "").strip()
  207. if kw:
  208. like = f"%{kw}%"
  209. q = q.filter(
  210. or_(
  211. SysTenant.tenant_no.like(like),
  212. SysTenant.tenant_name.like(like),
  213. )
  214. )
  215. for col, camel, snake in (
  216. (SysTenant.tenant_no, "tenantNo", "tenant_no"),
  217. (SysTenant.tenant_name, "tenantName", "tenant_name"),
  218. (SysTenant.contact_name, "contactName", "contact_name"),
  219. (SysTenant.contact_phone, "contactPhone", "contact_phone"),
  220. ):
  221. values = _split_csv_param(params, f"{camel}In", f"{snake}_in")
  222. if values:
  223. q = q.filter(col.in_(values))
  224. else:
  225. single = str(_param_first(params, camel, snake) or "").strip()
  226. if single:
  227. q = q.filter(col.like(f"%{single}%"))
  228. status_values = _split_csv_param(params, "statusIn", "status_in")
  229. if not status_values:
  230. st = _param_first(params, "status")
  231. if st:
  232. status_values = [str(st).strip()]
  233. if status_values:
  234. q = q.filter(SysTenant.status.in_(status_values))
  235. rows = q.group_by(column).order_by(column.asc()).limit(lim).all()
  236. out = []
  237. for row in rows:
  238. value = str(row.value).strip() if row.value is not None else ""
  239. if not value:
  240. continue
  241. out.append({"value": value, "label": value, "count": int(row.cnt or 0)})
  242. return success_response({"list": out})
  243. except ValueError as e:
  244. return error_response(400, str(e))
  245. except Exception as e:
  246. return error_response(500, str(e))
  247. TENANT_EXPORT_COLUMNS = [
  248. ExportColumn(title="租户编号", field="tenantNo"),
  249. ExportColumn(title="租户名称", field="tenantName"),
  250. ExportColumn(title="联系人", field="contactName"),
  251. ExportColumn(title="联系电话", field="contactPhone"),
  252. ExportColumn(title="创建时间", field="createTime"),
  253. ]
  254. class TenantExportBody(BaseModel):
  255. keyword: Optional[str] = None
  256. ids: Optional[List[str]] = None
  257. columns: Optional[List[str]] = None
  258. @router.post("/tenants/export")
  259. def export_tenants(
  260. body: TenantExportBody,
  261. _user: dict = Depends(require_admin_or_super_admin),
  262. db: Session = Depends(get_db),
  263. ):
  264. """导出租户列表(当前筛选全量,或 ids 已选)。"""
  265. try:
  266. items, _ = tenant_svc.list_tenants(
  267. db,
  268. page=1,
  269. page_size=EXPORT_MAX_ROWS,
  270. keyword=body.keyword,
  271. ids=body.ids,
  272. max_page_size=EXPORT_MAX_ROWS,
  273. user_info=_user,
  274. )
  275. cols = filter_export_columns(TENANT_EXPORT_COLUMNS, body.columns)
  276. date_str = datetime.now().strftime("%Y%m%d")
  277. return export_rows_to_xlsx_response(
  278. db,
  279. tenant_no=None,
  280. sheet_title="租户列表",
  281. columns=cols,
  282. rows=items,
  283. filename_stem=f"租户列表_{date_str}",
  284. )
  285. except ExportValidationError as e:
  286. return error_response(400, str(e))
  287. except Exception as e:
  288. return error_response(500, str(e))
  289. @router.post("/tenants")
  290. def create_tenant(
  291. body: TenantCreateBody,
  292. user: dict = Depends(require_admin_or_super_admin),
  293. db: Session = Depends(get_db),
  294. ):
  295. try:
  296. obj = tenant_svc.create_tenant(
  297. db,
  298. tenant_no=body.tenant_no,
  299. tenant_name=body.tenant_name,
  300. contact_name=body.contact_name,
  301. contact_phone=body.contact_phone,
  302. order_num=body.order_num,
  303. remark=body.remark,
  304. user_id=user.get("user_id"),
  305. )
  306. return success_response(tenant_svc._row_to_dict(obj), message="创建成功")
  307. except ValueError as e:
  308. return error_response(400, str(e))
  309. @router.put("/tenants/{tenant_id}")
  310. def update_tenant(
  311. tenant_id: str,
  312. body: TenantUpdateBody,
  313. user: dict = Depends(require_admin_or_super_admin),
  314. db: Session = Depends(get_db),
  315. ):
  316. try:
  317. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  318. obj = tenant_svc.update_tenant(
  319. db,
  320. tenant_id,
  321. tenant_no=payload.get("tenant_no"),
  322. tenant_name=payload.get("tenant_name"),
  323. contact_name=payload.get("contact_name"),
  324. contact_phone=payload.get("contact_phone"),
  325. order_num=payload.get("order_num"),
  326. remark=payload.get("remark"),
  327. user_id=user.get("user_id"),
  328. )
  329. return success_response(tenant_svc._row_to_dict(obj), message="更新成功")
  330. except ValueError as e:
  331. return error_response(400, str(e))
  332. @router.delete("/tenants/{tenant_id}")
  333. def delete_tenant(
  334. tenant_id: str,
  335. _user: dict = Depends(require_admin_or_super_admin),
  336. db: Session = Depends(get_db),
  337. ):
  338. try:
  339. tenant_svc.soft_delete_tenant(db, tenant_id, user_info=_user)
  340. return success_response(None, message="删除成功")
  341. except ValueError as e:
  342. return error_response(400, str(e))
  343. # ---------------------------------------------------------------------------
  344. # Helper: resolve tenant_no from user context
  345. # ---------------------------------------------------------------------------
  346. def _get_tenant_no(db: Session, user: dict) -> str:
  347. user_id = user.get("user_id")
  348. if not user_id:
  349. return "shanxia2.0"
  350. sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
  351. return (sys_user.tenant_no if sys_user else None) or "shanxia2.0"
  352. # ---------------------------------------------------------------------------
  353. # 用户管理
  354. # ---------------------------------------------------------------------------
  355. def _facet_body_params(body) -> dict:
  356. data = body.model_dump(by_alias=True, exclude_none=True)
  357. extra = getattr(body, "model_extra", None) or {}
  358. for key, value in extra.items():
  359. if key not in data and value not in (None, ""):
  360. data[key] = value
  361. return data
  362. def _split_csv_param(params: dict, *keys: str):
  363. for key in keys:
  364. raw = params.get(key)
  365. if raw is None or raw == "":
  366. continue
  367. return [s.strip() for s in str(raw).split(",") if s.strip()]
  368. return []
  369. def _param_first(params: dict, *keys: str):
  370. for key in keys:
  371. if key in params and params[key] not in (None, ""):
  372. return params[key]
  373. return None
  374. class UserCreateBody(BaseModel):
  375. model_config = ConfigDict(populate_by_name=True)
  376. user_no: str = Field(..., alias="userNo")
  377. nick_name: str = Field(..., alias="nickName")
  378. password: str = Field(..., alias="password")
  379. dept_id: Optional[str] = Field(None, alias="deptId")
  380. email: Optional[str] = Field("", alias="email")
  381. phone_no: Optional[str] = Field("", alias="phoneNo")
  382. sex: Optional[str] = Field("0", alias="sex")
  383. avatar: Optional[str] = Field(None, alias="avatar")
  384. status: Optional[str] = Field("1", alias="status")
  385. order_num: Optional[int] = Field(None, alias="orderNum")
  386. remark: Optional[str] = None
  387. role_ids: Optional[List[str]] = Field(None, alias="roleIds")
  388. post_ids: Optional[List[str]] = Field(None, alias="postIds")
  389. class UserUpdateBody(BaseModel):
  390. model_config = ConfigDict(populate_by_name=True)
  391. nick_name: Optional[str] = Field(None, alias="nickName")
  392. dept_id: Optional[str] = Field(None, alias="deptId")
  393. email: Optional[str] = Field(None, alias="email")
  394. phone_no: Optional[str] = Field(None, alias="phoneNo")
  395. sex: Optional[str] = Field(None, alias="sex")
  396. avatar: Optional[str] = Field(None, alias="avatar")
  397. status: Optional[str] = Field(None, alias="status")
  398. order_num: Optional[int] = Field(None, alias="orderNum")
  399. remark: Optional[str] = None
  400. role_ids: Optional[List[str]] = Field(None, alias="roleIds")
  401. post_ids: Optional[List[str]] = Field(None, alias="postIds")
  402. USER_EXPORT_COLUMNS = [
  403. ExportColumn(title="用户账号", field="userNo"),
  404. ExportColumn(title="用户名", field="nickName"),
  405. ExportColumn(title="所属组织", field="deptName"),
  406. ExportColumn(title="角色", field="roles"),
  407. ExportColumn(title="手机号", field="phoneNo"),
  408. ExportColumn(title="状态", field="status", dict_type="sys_status"),
  409. ExportColumn(title="创建时间", field="createTime"),
  410. ]
  411. class UserExportBody(BaseModel):
  412. model_config = ConfigDict(populate_by_name=True)
  413. keyword: Optional[str] = None
  414. dept_id: Optional[str] = Field(None, alias="deptId")
  415. status: Optional[str] = None
  416. ids: Optional[List[str]] = None
  417. columns: Optional[List[str]] = None
  418. @router.get("/users")
  419. def list_users(
  420. request: Request,
  421. page: int = Query(1, ge=1),
  422. pageSize: int = Query(10, ge=1, le=500, alias="pageSize"),
  423. keyword: Optional[str] = None,
  424. deptId: Optional[str] = Query(None, alias="deptId"),
  425. status: Optional[str] = None,
  426. user: dict = Depends(require_menu_code("system:user:list")),
  427. db: Session = Depends(get_db),
  428. ):
  429. tenant_no = _get_tenant_no(db, user)
  430. query = db.query(SysUser).filter(
  431. SysUser.del_flag == 0,
  432. SysUser.tenant_no == tenant_no,
  433. )
  434. kw = (keyword or "").strip()
  435. if kw:
  436. like = f"%{kw}%"
  437. query = query.filter(
  438. or_(
  439. SysUser.user_no.like(like),
  440. SysUser.nick_name.like(like),
  441. SysUser.phone_no.like(like),
  442. )
  443. )
  444. user_no = (request.query_params.get("userNo") or "").strip()
  445. if user_no:
  446. query = query.filter(SysUser.user_no.like(f"%{user_no}%"))
  447. nick_name = (request.query_params.get("nickName") or "").strip()
  448. if nick_name:
  449. query = query.filter(SysUser.nick_name.like(f"%{nick_name}%"))
  450. phone_no = (request.query_params.get("phoneNo") or "").strip()
  451. if phone_no:
  452. query = query.filter(SysUser.phone_no.like(f"%{phone_no}%"))
  453. user_no_in = (request.query_params.get("userNoIn") or "").strip()
  454. user_no_values = [s.strip() for s in user_no_in.split(",") if s.strip()]
  455. if user_no_values:
  456. query = query.filter(SysUser.user_no.in_(user_no_values))
  457. nick_name_in = (request.query_params.get("nickNameIn") or "").strip()
  458. nick_name_values = [s.strip() for s in nick_name_in.split(",") if s.strip()]
  459. if nick_name_values:
  460. query = query.filter(SysUser.nick_name.in_(nick_name_values))
  461. phone_no_in = (request.query_params.get("phoneNoIn") or "").strip()
  462. phone_no_values = [s.strip() for s in phone_no_in.split(",") if s.strip()]
  463. if phone_no_values:
  464. query = query.filter(SysUser.phone_no.in_(phone_no_values))
  465. if deptId:
  466. query = query.filter(SysUser.dept_id == deptId)
  467. status_in = (request.query_params.get("statusIn") or "").strip()
  468. status_values = [s.strip() for s in status_in.split(",") if s.strip()]
  469. if not status_values and status:
  470. status_values = [status]
  471. if status_values:
  472. query = query.filter(SysUser.status.in_(status_values))
  473. query = apply_data_scope(query, SysUser, user, db, tenant_no)
  474. total = query.count()
  475. items = query.order_by(SysUser.create_time.desc()).offset((page - 1) * pageSize).limit(pageSize).all()
  476. dept_ids = list({r.dept_id for r in items if r.dept_id})
  477. dept_map: dict[str, str] = {}
  478. if dept_ids:
  479. for d in db.query(SysDept).filter(SysDept.id.in_(dept_ids), SysDept.del_flag == 0).all():
  480. dept_map[d.id] = d.dept_name or ""
  481. result_list = [
  482. user_svc._row_to_dict(r, dept_name=dept_map.get(r.dept_id or "", "")) for r in items
  483. ]
  484. user_ids = [d["id"] for d in result_list if d.get("id")]
  485. role_map: dict[str, list[str]] = {uid: [] for uid in user_ids}
  486. if user_ids:
  487. for ur in (
  488. db.query(SysUserRole)
  489. .filter(
  490. SysUserRole.user_id.in_(user_ids),
  491. SysUserRole.del_flag == 0,
  492. )
  493. .all()
  494. ):
  495. role_map.setdefault(ur.user_id, []).append(ur.role_id)
  496. for d in result_list:
  497. d["roleIds"] = role_map.get(d["id"], [])
  498. tp = user_svc.total_pages(total, pageSize)
  499. return success_response({"list": result_list, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  500. class UserColumnFacetBody(BaseModel):
  501. """用户列表列头可选值(去重;可附带列表上下文)"""
  502. model_config = ConfigDict(populate_by_name=True, extra="allow")
  503. field: str = Field(..., description="userNo | nickName | phoneNo")
  504. keyword: Optional[str] = Field(default=None, description="可选值关键字")
  505. limit: Optional[int] = Field(default=500, ge=1, le=500, description="返回条数上限")
  506. _USER_FACET_COLUMNS = {
  507. "userNo": SysUser.user_no,
  508. "nickName": SysUser.nick_name,
  509. "phoneNo": SysUser.phone_no,
  510. }
  511. @router.post("/users/column-facet-values")
  512. def user_column_facet_values(
  513. body: UserColumnFacetBody,
  514. user: dict = Depends(require_menu_code("system:user:list")),
  515. db: Session = Depends(get_db),
  516. ):
  517. """用户列表列头可选值:在列表上下文(排除本列)下去重。"""
  518. from app.services.pl.facet_context import strip_simple_facet_self_params
  519. column = _USER_FACET_COLUMNS.get(body.field)
  520. if column is None:
  521. return error_response(400, "不支持的列")
  522. try:
  523. tenant_no = _get_tenant_no(db, user)
  524. list_keyword = (body.keyword or "").strip() or None
  525. params = strip_simple_facet_self_params(_facet_body_params(body), body.field)
  526. extra = []
  527. kw = list_keyword or (_param_first(params, "keyword") or "")
  528. kw = str(kw).strip()
  529. if kw:
  530. like = f"%{kw}%"
  531. extra.append(
  532. or_(
  533. SysUser.user_no.like(like),
  534. SysUser.nick_name.like(like),
  535. SysUser.phone_no.like(like),
  536. )
  537. )
  538. for col, camel, snake in (
  539. (SysUser.user_no, "userNo", "user_no"),
  540. (SysUser.nick_name, "nickName", "nick_name"),
  541. (SysUser.phone_no, "phoneNo", "phone_no"),
  542. ):
  543. values = _split_csv_param(params, f"{camel}In", f"{snake}_in")
  544. if values:
  545. extra.append(col.in_(values))
  546. else:
  547. single = (_param_first(params, camel, snake) or "")
  548. if str(single).strip():
  549. extra.append(col.like(f"%{str(single).strip()}%"))
  550. dept_id = _param_first(params, "deptId", "dept_id")
  551. if dept_id:
  552. extra.append(SysUser.dept_id == dept_id)
  553. status_values = _split_csv_param(params, "statusIn", "status_in")
  554. if not status_values:
  555. st = _param_first(params, "status")
  556. if st:
  557. status_values = [str(st).strip()]
  558. if status_values:
  559. extra.append(SysUser.status.in_(status_values))
  560. rows = list_column_facet_values(
  561. db,
  562. SysUser,
  563. column,
  564. tenant_no=tenant_no,
  565. keyword=None,
  566. limit=body.limit if body.limit is not None else 500,
  567. apply_scope=lambda q: apply_data_scope(q, SysUser, user, db, tenant_no),
  568. extra_filters=extra or None,
  569. )
  570. return success_response({"list": rows})
  571. except ValueError as e:
  572. return error_response(400, str(e))
  573. except Exception as e:
  574. return error_response(500, str(e))
  575. @router.post("/users/export")
  576. def export_users(
  577. body: UserExportBody,
  578. user: dict = Depends(require_menu_code("system:user:list")),
  579. db: Session = Depends(get_db),
  580. ):
  581. """导出用户列表(当前筛选全量,或 ids 已选)。"""
  582. try:
  583. tenant_no = _get_tenant_no(db, user)
  584. query = db.query(SysUser).filter(
  585. SysUser.del_flag == 0,
  586. SysUser.tenant_no == tenant_no,
  587. )
  588. kw = (body.keyword or "").strip()
  589. if kw:
  590. like = f"%{kw}%"
  591. query = query.filter(
  592. or_(
  593. SysUser.user_no.like(like),
  594. SysUser.nick_name.like(like),
  595. SysUser.phone_no.like(like),
  596. )
  597. )
  598. if body.dept_id:
  599. query = query.filter(SysUser.dept_id == body.dept_id)
  600. if body.status:
  601. query = query.filter(SysUser.status == body.status)
  602. if body.ids:
  603. query = query.filter(SysUser.id.in_(body.ids))
  604. query = apply_data_scope(query, SysUser, user, db, tenant_no)
  605. rows_orm = query.order_by(SysUser.create_time.desc()).all()
  606. dept_ids = list({r.dept_id for r in rows_orm if r.dept_id})
  607. dept_map: dict[str, str] = {}
  608. if dept_ids:
  609. for d in db.query(SysDept).filter(SysDept.id.in_(dept_ids), SysDept.del_flag == 0).all():
  610. dept_map[d.id] = d.dept_name or ""
  611. result_list = [
  612. user_svc._row_to_dict(r, dept_name=dept_map.get(r.dept_id or "", "")) for r in rows_orm
  613. ]
  614. user_ids = [d["id"] for d in result_list if d.get("id")]
  615. role_map: dict[str, list[str]] = {uid: [] for uid in user_ids}
  616. role_name_by_id: dict[str, str] = {}
  617. if user_ids:
  618. for ur in (
  619. db.query(SysUserRole)
  620. .filter(
  621. SysUserRole.user_id.in_(user_ids),
  622. SysUserRole.del_flag == 0,
  623. )
  624. .all()
  625. ):
  626. role_map.setdefault(ur.user_id, []).append(ur.role_id)
  627. role_ids = list({rid for ids in role_map.values() for rid in ids})
  628. if role_ids:
  629. for role in db.query(SysRole).filter(SysRole.id.in_(role_ids), SysRole.del_flag == 0).all():
  630. role_name_by_id[role.id] = role.role_name or role.id
  631. for d in result_list:
  632. names = [role_name_by_id.get(rid, rid) for rid in role_map.get(d["id"], [])]
  633. d["roles"] = "、".join(names)
  634. cols = filter_export_columns(USER_EXPORT_COLUMNS, body.columns)
  635. date_str = datetime.now().strftime("%Y%m%d")
  636. return export_rows_to_xlsx_response(
  637. db,
  638. tenant_no=tenant_no,
  639. sheet_title="用户列表",
  640. columns=cols,
  641. rows=result_list,
  642. filename_stem=f"用户列表_{date_str}",
  643. )
  644. except ExportValidationError as e:
  645. return error_response(400, str(e))
  646. except Exception as e:
  647. return error_response(500, str(e))
  648. @router.get("/users/{user_id}")
  649. def get_user_by_id(
  650. user_id: str,
  651. user: dict = Depends(require_menu_code("system:user:list")),
  652. db: Session = Depends(get_db),
  653. ):
  654. tenant_no = _get_tenant_no(db, user)
  655. row = user_svc.get_user(db, user_id)
  656. if not row:
  657. return error_response(404, "用户不存在")
  658. if row.tenant_no != tenant_no and user.get("role") != "super_admin":
  659. return error_response(403, "无权查看该用户")
  660. try:
  661. assert_row_in_data_scope(db, row, SysUser, user, tenant_no)
  662. except HTTPException:
  663. return error_response(403, "无权查看该用户")
  664. data = user_svc.get_user_with_relations(db, user_id)
  665. return success_response(data)
  666. @router.post("/users")
  667. def create_user(
  668. body: UserCreateBody,
  669. user: dict = Depends(require_admin_or_super_admin),
  670. db: Session = Depends(get_db),
  671. ):
  672. try:
  673. tenant_no = _get_tenant_no(db, user)
  674. if user_svc.user_no_exists(db, body.user_no, tenant_no):
  675. return error_response(400, "用户账号已存在")
  676. obj = user_svc.create_user(
  677. db,
  678. user_no=body.user_no,
  679. nick_name=body.nick_name,
  680. password=body.password,
  681. dept_id=body.dept_id,
  682. email=body.email,
  683. phone_no=body.phone_no,
  684. sex=body.sex,
  685. avatar=body.avatar,
  686. status=body.status,
  687. tenant_no=tenant_no,
  688. order_num=body.order_num,
  689. remark=body.remark,
  690. user_id=user.get("user_id"),
  691. role_ids=body.role_ids,
  692. post_ids=body.post_ids,
  693. )
  694. return success_response(user_svc._row_to_dict(obj), message="创建成功")
  695. except ValueError as e:
  696. return error_response(400, str(e))
  697. @router.put("/users/{user_id}")
  698. def update_user(
  699. user_id: str,
  700. body: UserUpdateBody,
  701. user: dict = Depends(require_admin_or_super_admin),
  702. db: Session = Depends(get_db),
  703. ):
  704. try:
  705. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  706. obj = user_svc.update_user(
  707. db,
  708. user_id,
  709. nick_name=payload.get("nick_name"),
  710. dept_id=payload.get("dept_id"),
  711. email=payload.get("email"),
  712. phone_no=payload.get("phone_no"),
  713. sex=payload.get("sex"),
  714. avatar=payload.get("avatar"),
  715. status=payload.get("status"),
  716. order_num=payload.get("order_num"),
  717. remark=payload.get("remark"),
  718. user_id_updater=user.get("user_id"),
  719. role_ids=payload.get("role_ids"),
  720. post_ids=payload.get("post_ids"),
  721. )
  722. return success_response(user_svc._row_to_dict(obj), message="更新成功")
  723. except ValueError as e:
  724. return error_response(400, str(e))
  725. @router.delete("/users/{user_id}")
  726. def delete_user(
  727. user_id: str,
  728. _user: dict = Depends(require_admin_or_super_admin),
  729. db: Session = Depends(get_db),
  730. ):
  731. try:
  732. user_svc.soft_delete_user(db, user_id, user_info=_user)
  733. return success_response(None, message="删除成功")
  734. except ValueError as e:
  735. return error_response(400, str(e))
  736. @router.post("/users/{user_id}/reset-password")
  737. def reset_user_password(
  738. user_id: str,
  739. _user: dict = Depends(require_admin_or_super_admin),
  740. db: Session = Depends(get_db),
  741. ):
  742. try:
  743. user_svc.reset_password(db, user_id, "111111")
  744. return success_response(None, message="密码已重置为 111111")
  745. except ValueError as e:
  746. return error_response(400, str(e))
  747. # ---------------------------------------------------------------------------
  748. # 字典类型管理
  749. # ---------------------------------------------------------------------------
  750. class DictTypeCreateBody(BaseModel):
  751. model_config = ConfigDict(populate_by_name=True)
  752. dict_name: str = Field(..., alias="dictName")
  753. dict_type: str = Field(..., alias="dictType")
  754. order_num: Optional[int] = Field(0, alias="orderNum")
  755. remark: Optional[str] = None
  756. class DictTypeUpdateBody(BaseModel):
  757. model_config = ConfigDict(populate_by_name=True)
  758. dict_name: Optional[str] = Field(None, alias="dictName")
  759. dict_type: Optional[str] = Field(None, alias="dictType")
  760. order_num: Optional[int] = Field(None, alias="orderNum")
  761. remark: Optional[str] = None
  762. @router.get("/dict-types")
  763. def list_dict_types(
  764. request: Request,
  765. page: int = Query(1, ge=1),
  766. pageSize: int = Query(10, ge=1, le=500, alias="pageSize"),
  767. keyword: Optional[str] = None,
  768. user: dict = Depends(require_menu_code("system:dict:list")),
  769. db: Session = Depends(get_db),
  770. ):
  771. tenant_no = _get_tenant_no(db, user)
  772. items, total = dict_type_svc.list_dict_types(
  773. db,
  774. page=page,
  775. page_size=pageSize,
  776. keyword=keyword,
  777. dict_name=request.query_params.get("dictName"),
  778. dict_type=request.query_params.get("dictType"),
  779. dict_name_in=request.query_params.get("dictNameIn"),
  780. dict_type_in=request.query_params.get("dictTypeIn"),
  781. tenant_no=tenant_no,
  782. user_info=user,
  783. )
  784. tp = dict_type_svc.total_pages(total, pageSize)
  785. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  786. class DictTypeColumnFacetBody(BaseModel):
  787. """字典类型列表列头可选值(去重;可附带列表上下文)"""
  788. model_config = ConfigDict(populate_by_name=True, extra="allow")
  789. field: str = Field(..., description="dictName | dictType")
  790. keyword: Optional[str] = Field(default=None, description="可选值关键字")
  791. limit: Optional[int] = Field(default=500, ge=1, le=500, description="返回条数上限")
  792. _DICT_TYPE_FACET_COLUMNS = {
  793. "dictName": SysDictType.dict_name,
  794. "dictType": SysDictType.dict_type,
  795. }
  796. @router.post("/dict-types/column-facet-values")
  797. def dict_type_column_facet_values(
  798. body: DictTypeColumnFacetBody,
  799. user: dict = Depends(require_menu_code("system:dict:list")),
  800. db: Session = Depends(get_db),
  801. ):
  802. """字典类型列表列头可选值:在列表上下文(排除本列)下去重。"""
  803. from app.services.pl.facet_context import strip_simple_facet_self_params
  804. column = _DICT_TYPE_FACET_COLUMNS.get(body.field)
  805. if column is None:
  806. return error_response(400, "不支持的列")
  807. try:
  808. tenant_no = _get_tenant_no(db, user)
  809. list_keyword = (body.keyword or "").strip() or None
  810. params = strip_simple_facet_self_params(_facet_body_params(body), body.field)
  811. extra = []
  812. kw = list_keyword or str(_param_first(params, "keyword") or "").strip()
  813. if kw:
  814. like = f"%{kw}%"
  815. extra.append(or_(SysDictType.dict_name.like(like), SysDictType.dict_type.like(like)))
  816. for col, camel, snake in (
  817. (SysDictType.dict_name, "dictName", "dict_name"),
  818. (SysDictType.dict_type, "dictType", "dict_type"),
  819. ):
  820. values = _split_csv_param(params, f"{camel}In", f"{snake}_in")
  821. if values:
  822. extra.append(col.in_(values))
  823. else:
  824. single = str(_param_first(params, camel, snake) or "").strip()
  825. if single:
  826. extra.append(col.like(f"%{single}%"))
  827. rows = list_column_facet_values(
  828. db,
  829. SysDictType,
  830. column,
  831. tenant_no=tenant_no,
  832. keyword=None,
  833. limit=body.limit if body.limit is not None else 500,
  834. extra_filters=extra or None,
  835. )
  836. return success_response({"list": rows})
  837. except ValueError as e:
  838. return error_response(400, str(e))
  839. except Exception as e:
  840. return error_response(500, str(e))
  841. DICT_TYPE_EXPORT_COLUMNS = [
  842. ExportColumn(title="字典名称", field="dictName"),
  843. ExportColumn(title="字典类型编码", field="dictType"),
  844. ExportColumn(title="备注", field="remark"),
  845. ExportColumn(title="创建时间", field="createTime"),
  846. ]
  847. class DictTypeExportBody(BaseModel):
  848. keyword: Optional[str] = None
  849. ids: Optional[List[str]] = None
  850. columns: Optional[List[str]] = None
  851. @router.post("/dict-types/export")
  852. def export_dict_types(
  853. body: DictTypeExportBody,
  854. user: dict = Depends(require_menu_code("system:dict:list")),
  855. db: Session = Depends(get_db),
  856. ):
  857. """导出字典类型列表(当前筛选全量,或 ids 已选)。"""
  858. try:
  859. tenant_no = _get_tenant_no(db, user)
  860. items, _ = dict_type_svc.list_dict_types(
  861. db,
  862. page=1,
  863. page_size=EXPORT_MAX_ROWS,
  864. keyword=body.keyword,
  865. tenant_no=tenant_no,
  866. ids=body.ids,
  867. max_page_size=EXPORT_MAX_ROWS,
  868. user_info=user,
  869. )
  870. cols = filter_export_columns(DICT_TYPE_EXPORT_COLUMNS, body.columns)
  871. date_str = datetime.now().strftime("%Y%m%d")
  872. return export_rows_to_xlsx_response(
  873. db,
  874. tenant_no=tenant_no,
  875. sheet_title="字典类型",
  876. columns=cols,
  877. rows=items,
  878. filename_stem=f"字典类型_{date_str}",
  879. )
  880. except ExportValidationError as e:
  881. return error_response(400, str(e))
  882. except Exception as e:
  883. return error_response(500, str(e))
  884. @router.post("/dict-types")
  885. def create_dict_type(
  886. body: DictTypeCreateBody,
  887. user: dict = Depends(require_admin_or_super_admin),
  888. db: Session = Depends(get_db),
  889. ):
  890. try:
  891. tenant_no = _get_tenant_no(db, user)
  892. obj = dict_type_svc.create_dict_type(
  893. db,
  894. dict_name=body.dict_name,
  895. dict_type=body.dict_type,
  896. tenant_no=tenant_no,
  897. order_num=body.order_num,
  898. remark=body.remark,
  899. user_id=user.get("user_id"),
  900. )
  901. return success_response(dict_type_svc._row_to_dict(obj), message="创建成功")
  902. except ValueError as e:
  903. return error_response(400, str(e))
  904. @router.put("/dict-types/{dict_type_id}")
  905. def update_dict_type(
  906. dict_type_id: str,
  907. body: DictTypeUpdateBody,
  908. user: dict = Depends(require_admin_or_super_admin),
  909. db: Session = Depends(get_db),
  910. ):
  911. try:
  912. tenant_no = _get_tenant_no(db, user)
  913. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  914. obj = dict_type_svc.update_dict_type(
  915. db,
  916. dict_type_id,
  917. dict_name=payload.get("dict_name"),
  918. dict_type=payload.get("dict_type"),
  919. order_num=payload.get("order_num"),
  920. remark=payload.get("remark"),
  921. user_id=user.get("user_id"),
  922. tenant_no=tenant_no,
  923. )
  924. return success_response(dict_type_svc._row_to_dict(obj), message="更新成功")
  925. except ValueError as e:
  926. return error_response(400, str(e))
  927. @router.delete("/dict-types/{dict_type_id}")
  928. def delete_dict_type(
  929. dict_type_id: str,
  930. _user: dict = Depends(require_admin_or_super_admin),
  931. db: Session = Depends(get_db),
  932. ):
  933. try:
  934. dict_type_svc.soft_delete_dict_type(db, dict_type_id, user_info=_user)
  935. return success_response(None, message="删除成功")
  936. except ValueError as e:
  937. return error_response(400, str(e))
  938. # ---------------------------------------------------------------------------
  939. # 字典数据管理
  940. # ---------------------------------------------------------------------------
  941. class DictDataCreateBody(BaseModel):
  942. model_config = ConfigDict(populate_by_name=True)
  943. dict_label: str = Field(..., alias="dictLabel")
  944. dict_value: str = Field(..., alias="dictValue")
  945. dict_type: str = Field(..., alias="dictType")
  946. tag_type: Optional[str] = Field("", alias="tagType")
  947. order_num: Optional[int] = Field(0, alias="orderNum")
  948. remark: Optional[str] = None
  949. class DictDataUpdateBody(BaseModel):
  950. model_config = ConfigDict(populate_by_name=True)
  951. dict_label: Optional[str] = Field(None, alias="dictLabel")
  952. dict_value: Optional[str] = Field(None, alias="dictValue")
  953. tag_type: Optional[str] = Field(None, alias="tagType")
  954. order_num: Optional[int] = Field(None, alias="orderNum")
  955. remark: Optional[str] = None
  956. @router.get("/dict-data")
  957. def list_dict_data(
  958. page: int = Query(1, ge=1),
  959. pageSize: int = Query(10, ge=1, le=500, alias="pageSize"),
  960. dictType: Optional[str] = None,
  961. keyword: Optional[str] = None,
  962. user: dict = Depends(require_menu_code("system:dict:list")),
  963. db: Session = Depends(get_db),
  964. ):
  965. tenant_no = _get_tenant_no(db, user)
  966. items, total = dict_data_svc.list_dict_data(
  967. db, page=page, page_size=pageSize, dict_type_code=dictType, keyword=keyword, tenant_no=tenant_no, user_info=user
  968. )
  969. tp = dict_data_svc.total_pages(total, pageSize)
  970. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  971. @router.post("/dict-data")
  972. def create_dict_data(
  973. body: DictDataCreateBody,
  974. user: dict = Depends(require_admin_or_super_admin),
  975. db: Session = Depends(get_db),
  976. ):
  977. try:
  978. tenant_no = _get_tenant_no(db, user)
  979. obj = dict_data_svc.create_dict_data(
  980. db,
  981. dict_label=body.dict_label,
  982. dict_value=body.dict_value,
  983. dict_type=body.dict_type,
  984. tag_type=body.tag_type,
  985. tenant_no=tenant_no,
  986. order_num=body.order_num,
  987. remark=body.remark,
  988. user_id=user.get("user_id"),
  989. )
  990. return success_response(dict_data_svc._row_to_dict(obj), message="创建成功")
  991. except ValueError as e:
  992. return error_response(400, str(e))
  993. @router.put("/dict-data/{data_id}")
  994. def update_dict_data(
  995. data_id: str,
  996. body: DictDataUpdateBody,
  997. user: dict = Depends(require_admin_or_super_admin),
  998. db: Session = Depends(get_db),
  999. ):
  1000. try:
  1001. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  1002. obj = dict_data_svc.update_dict_data(
  1003. db,
  1004. data_id,
  1005. dict_label=payload.get("dict_label"),
  1006. dict_value=payload.get("dict_value"),
  1007. tag_type=payload.get("tag_type"),
  1008. order_num=payload.get("order_num"),
  1009. remark=payload.get("remark"),
  1010. user_id=user.get("user_id"),
  1011. )
  1012. return success_response(dict_data_svc._row_to_dict(obj), message="更新成功")
  1013. except ValueError as e:
  1014. return error_response(400, str(e))
  1015. @router.delete("/dict-data/{data_id}")
  1016. def delete_dict_data(
  1017. data_id: str,
  1018. _user: dict = Depends(require_admin_or_super_admin),
  1019. db: Session = Depends(get_db),
  1020. ):
  1021. try:
  1022. dict_data_svc.soft_delete_dict_data(db, data_id, user_info=_user)
  1023. return success_response(None, message="删除成功")
  1024. except ValueError as e:
  1025. return error_response(400, str(e))
  1026. @router.get("/dict-data/options")
  1027. def dict_data_options(
  1028. dictType: str = Query(..., alias="dictType"),
  1029. user: dict = Depends(require_menu_code("system:dict:list")),
  1030. db: Session = Depends(get_db),
  1031. ):
  1032. tenant_no = _get_tenant_no(db, user)
  1033. # 字典选项为租户主数据,表单共用;不做数据范围过滤
  1034. items = dict_data_svc.list_dict_data_options(
  1035. db, dict_type_code=dictType, tenant_no=tenant_no, user_info=None
  1036. )
  1037. return success_response(items)
  1038. @router.get("/dict-data/all")
  1039. def dict_data_all(
  1040. user: dict = Depends(require_admin_or_super_admin),
  1041. db: Session = Depends(get_db),
  1042. ):
  1043. tenant_no = _get_tenant_no(db, user)
  1044. q = db.query(SysDictData).filter(
  1045. and_(SysDictData.tenant_no == tenant_no, SysDictData.del_flag == 0)
  1046. )
  1047. rows = q.order_by(SysDictData.order_num.asc()).all()
  1048. grouped = {}
  1049. for r in rows:
  1050. key = r.dict_type
  1051. if key not in grouped:
  1052. grouped[key] = []
  1053. grouped[key].append({
  1054. "label": r.dict_label,
  1055. "value": r.dict_value,
  1056. "tagType": r.tag_type or ""
  1057. })
  1058. return success_response(grouped)
  1059. class DictEnsureBody(BaseModel):
  1060. model_config = ConfigDict(populate_by_name=True)
  1061. dict_type: str = Field(..., alias="dictType")
  1062. label: str
  1063. preferred_value: Optional[str] = Field(None, alias="preferredValue")
  1064. class DictEnsureRemoveBody(BaseModel):
  1065. model_config = ConfigDict(populate_by_name=True)
  1066. dict_type: str = Field(..., alias="dictType")
  1067. value: str
  1068. @router.post("/dict-data/ensure")
  1069. def ensure_dict_data_api(
  1070. body: DictEnsureBody,
  1071. user: dict = Depends(require_admin_or_super_admin),
  1072. db: Session = Depends(get_db),
  1073. ):
  1074. try:
  1075. tenant_no = _get_tenant_no(db, user)
  1076. data = dict_ensure_svc.ensure_dict_data(
  1077. db,
  1078. tenant_no=tenant_no,
  1079. dict_type=body.dict_type,
  1080. label=body.label,
  1081. user_id=user.get("user_id") or user.get("id") or "",
  1082. preferred_value=body.preferred_value,
  1083. )
  1084. db.commit()
  1085. return success_response(data, message="已保存")
  1086. except dict_ensure_svc.DictEnsureError as e:
  1087. db.rollback()
  1088. return error_response(400, str(e))
  1089. @router.post("/dict-data/ensure-remove")
  1090. def ensure_remove_dict_data_api(
  1091. body: DictEnsureRemoveBody,
  1092. user: dict = Depends(require_admin_or_super_admin),
  1093. db: Session = Depends(get_db),
  1094. ):
  1095. try:
  1096. tenant_no = _get_tenant_no(db, user)
  1097. dict_ensure_svc.ensure_remove_dict_data(
  1098. db,
  1099. tenant_no=tenant_no,
  1100. dict_type=body.dict_type,
  1101. value=body.value,
  1102. user_id=user.get("user_id") or user.get("id") or "",
  1103. )
  1104. db.commit()
  1105. return success_response(None, message="已删除")
  1106. except dict_ensure_svc.DictEnsureError as e:
  1107. db.rollback()
  1108. return error_response(400, str(e))
  1109. # ---------------------------------------------------------------------------
  1110. # 组织管理
  1111. # ---------------------------------------------------------------------------
  1112. class DeptCreateBody(BaseModel):
  1113. model_config = ConfigDict(populate_by_name=True)
  1114. parent_id: str = Field("0", alias="parentId")
  1115. dept_name: str = Field(..., alias="deptName")
  1116. dept_category_code: Optional[str] = Field(None, alias="deptCategoryCode")
  1117. status: Optional[str] = Field("1")
  1118. leader: Optional[str] = None
  1119. leader_phone: Optional[str] = Field(None, alias="leaderPhone")
  1120. order_num: Optional[int] = Field(0, alias="orderNum")
  1121. remark: Optional[str] = None
  1122. class DeptUpdateBody(BaseModel):
  1123. model_config = ConfigDict(populate_by_name=True)
  1124. dept_name: Optional[str] = Field(None, alias="deptName")
  1125. dept_category_code: Optional[str] = Field(None, alias="deptCategoryCode")
  1126. status: Optional[str] = None
  1127. leader: Optional[str] = None
  1128. leader_phone: Optional[str] = Field(None, alias="leaderPhone")
  1129. order_num: Optional[int] = Field(None, alias="orderNum")
  1130. remark: Optional[str] = None
  1131. @router.get("/depts")
  1132. def list_depts(
  1133. keyword: Optional[str] = None,
  1134. user: dict = Depends(require_menu_code("system:dept:list")),
  1135. db: Session = Depends(get_db),
  1136. ):
  1137. tenant_no = _get_tenant_no(db, user)
  1138. tree = dept_svc.list_depts_tree(db, keyword=keyword, tenant_no=tenant_no, user_info=user)
  1139. return success_response(tree)
  1140. DEPT_EXPORT_COLUMNS = [
  1141. ExportColumn(title="组织名称", field="deptName"),
  1142. ExportColumn(title="组织类别", field="deptCategoryCode"),
  1143. ExportColumn(title="状态", field="status", dict_type="sys_status"),
  1144. ExportColumn(title="负责人", field="leader"),
  1145. ExportColumn(title="联系电话", field="leaderPhone"),
  1146. ExportColumn(title="创建时间", field="createTime"),
  1147. ]
  1148. class DeptExportBody(BaseModel):
  1149. keyword: Optional[str] = None
  1150. ids: Optional[List[str]] = None
  1151. columns: Optional[List[str]] = None
  1152. @router.post("/depts/export")
  1153. def export_depts(
  1154. body: DeptExportBody,
  1155. user: dict = Depends(require_menu_code("system:dept:list")),
  1156. db: Session = Depends(get_db),
  1157. ):
  1158. """导出组织列表(扁平行;当前筛选或已选)。"""
  1159. try:
  1160. tenant_no = _get_tenant_no(db, user)
  1161. rows = dept_svc.list_depts(db, keyword=body.keyword, tenant_no=tenant_no, user_info=user)
  1162. if body.ids:
  1163. idset = set(body.ids)
  1164. rows = [r for r in rows if r.get("id") in idset]
  1165. cols = filter_export_columns(DEPT_EXPORT_COLUMNS, body.columns)
  1166. date_str = datetime.now().strftime("%Y%m%d")
  1167. return export_rows_to_xlsx_response(
  1168. db,
  1169. tenant_no=tenant_no,
  1170. sheet_title="组织列表",
  1171. columns=cols,
  1172. rows=rows,
  1173. filename_stem=f"组织列表_{date_str}",
  1174. )
  1175. except ExportValidationError as e:
  1176. return error_response(400, str(e))
  1177. except Exception as e:
  1178. return error_response(500, str(e))
  1179. @router.post("/depts")
  1180. def create_dept(
  1181. body: DeptCreateBody,
  1182. user: dict = Depends(require_admin_or_super_admin),
  1183. db: Session = Depends(get_db),
  1184. ):
  1185. try:
  1186. tenant_no = _get_tenant_no(db, user)
  1187. obj = dept_svc.create_dept(
  1188. db,
  1189. parent_id=body.parent_id,
  1190. dept_name=body.dept_name,
  1191. dept_category_code=body.dept_category_code,
  1192. status=body.status,
  1193. leader=body.leader,
  1194. leader_phone=body.leader_phone,
  1195. tenant_no=tenant_no,
  1196. order_num=body.order_num,
  1197. remark=body.remark,
  1198. user_id=user.get("user_id"),
  1199. )
  1200. return success_response(dept_svc._row_to_dict(obj), message="创建成功")
  1201. except ValueError as e:
  1202. return error_response(400, str(e))
  1203. @router.put("/depts/{dept_id}")
  1204. def update_dept(
  1205. dept_id: str,
  1206. body: DeptUpdateBody,
  1207. user: dict = Depends(require_admin_or_super_admin),
  1208. db: Session = Depends(get_db),
  1209. ):
  1210. try:
  1211. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  1212. obj = dept_svc.update_dept(
  1213. db,
  1214. dept_id,
  1215. dept_name=payload.get("dept_name"),
  1216. dept_category_code=payload.get("dept_category_code"),
  1217. status=payload.get("status"),
  1218. leader=payload.get("leader"),
  1219. leader_phone=payload.get("leader_phone"),
  1220. order_num=payload.get("order_num"),
  1221. remark=payload.get("remark"),
  1222. user_id=user.get("user_id"),
  1223. )
  1224. return success_response(dept_svc._row_to_dict(obj), message="更新成功")
  1225. except ValueError as e:
  1226. return error_response(400, str(e))
  1227. @router.delete("/depts/{dept_id}")
  1228. def delete_dept(
  1229. dept_id: str,
  1230. _user: dict = Depends(require_admin_or_super_admin),
  1231. db: Session = Depends(get_db),
  1232. ):
  1233. try:
  1234. dept_svc.soft_delete_dept(db, dept_id, user_info=_user)
  1235. return success_response(None, message="删除成功")
  1236. except ValueError as e:
  1237. return error_response(400, str(e))
  1238. # ---------------------------------------------------------------------------
  1239. # 站点管理
  1240. # ---------------------------------------------------------------------------
  1241. class SiteCreateBody(BaseModel):
  1242. model_config = ConfigDict(populate_by_name=True)
  1243. site_name: str = Field(..., alias="siteName")
  1244. site_code: str = Field(..., alias="siteCode")
  1245. site_type: Optional[str] = Field(None, alias="siteType")
  1246. status: Optional[str] = Field("1")
  1247. address: Optional[str] = None
  1248. contact_name: Optional[str] = Field(None, alias="contactName")
  1249. contact_phone: Optional[str] = Field(None, alias="contactPhone")
  1250. business_scope: Optional[str] = Field(None, alias="businessScope")
  1251. order_num: Optional[int] = Field(0, alias="orderNum")
  1252. remark: Optional[str] = None
  1253. class SiteUpdateBody(BaseModel):
  1254. model_config = ConfigDict(populate_by_name=True)
  1255. site_name: Optional[str] = Field(None, alias="siteName")
  1256. site_code: Optional[str] = Field(None, alias="siteCode")
  1257. site_type: Optional[str] = Field(None, alias="siteType")
  1258. status: Optional[str] = None
  1259. address: Optional[str] = None
  1260. contact_name: Optional[str] = Field(None, alias="contactName")
  1261. contact_phone: Optional[str] = Field(None, alias="contactPhone")
  1262. business_scope: Optional[str] = Field(None, alias="businessScope")
  1263. order_num: Optional[int] = Field(None, alias="orderNum")
  1264. remark: Optional[str] = None
  1265. @router.get("/sites")
  1266. def list_sites(
  1267. page: int = Query(1, ge=1),
  1268. pageSize: int = Query(10, ge=1, le=500, alias="pageSize"),
  1269. keyword: Optional[str] = None,
  1270. user: dict = Depends(require_menu_code("system:site:list")),
  1271. db: Session = Depends(get_db),
  1272. ):
  1273. tenant_no = _get_tenant_no(db, user)
  1274. items, total = site_svc.list_sites(db, page=page, page_size=pageSize, keyword=keyword, tenant_no=tenant_no, user_info=user)
  1275. tp = site_svc.total_pages(total, pageSize)
  1276. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  1277. SITE_EXPORT_COLUMNS = [
  1278. ExportColumn(title="站点名称", field="siteName"),
  1279. ExportColumn(title="站点编码", field="siteCode"),
  1280. ExportColumn(title="站点类型", field="siteType"),
  1281. ExportColumn(title="状态", field="status", dict_type="sys_status"),
  1282. ExportColumn(title="地址", field="address"),
  1283. ExportColumn(title="联系人", field="contactName"),
  1284. ExportColumn(title="联系电话", field="contactPhone"),
  1285. ExportColumn(title="创建时间", field="createTime"),
  1286. ]
  1287. class SiteExportBody(BaseModel):
  1288. keyword: Optional[str] = None
  1289. ids: Optional[List[str]] = None
  1290. columns: Optional[List[str]] = None
  1291. @router.post("/sites/export")
  1292. def export_sites(
  1293. body: SiteExportBody,
  1294. user: dict = Depends(require_menu_code("system:site:list")),
  1295. db: Session = Depends(get_db),
  1296. ):
  1297. """导出站点列表(当前筛选全量,或 ids 已选)。"""
  1298. try:
  1299. tenant_no = _get_tenant_no(db, user)
  1300. items, _ = site_svc.list_sites(
  1301. db,
  1302. page=1,
  1303. page_size=EXPORT_MAX_ROWS,
  1304. keyword=body.keyword,
  1305. tenant_no=tenant_no,
  1306. ids=body.ids,
  1307. max_page_size=EXPORT_MAX_ROWS,
  1308. user_info=user,
  1309. )
  1310. cols = filter_export_columns(SITE_EXPORT_COLUMNS, body.columns)
  1311. date_str = datetime.now().strftime("%Y%m%d")
  1312. return export_rows_to_xlsx_response(
  1313. db,
  1314. tenant_no=tenant_no,
  1315. sheet_title="站点列表",
  1316. columns=cols,
  1317. rows=items,
  1318. filename_stem=f"站点列表_{date_str}",
  1319. )
  1320. except ExportValidationError as e:
  1321. return error_response(400, str(e))
  1322. except Exception as e:
  1323. return error_response(500, str(e))
  1324. @router.post("/sites")
  1325. def create_site(
  1326. body: SiteCreateBody,
  1327. user: dict = Depends(require_admin_or_super_admin),
  1328. db: Session = Depends(get_db),
  1329. ):
  1330. try:
  1331. tenant_no = _get_tenant_no(db, user)
  1332. obj = site_svc.create_site(
  1333. db,
  1334. site_name=body.site_name,
  1335. site_code=body.site_code,
  1336. site_type=body.site_type,
  1337. status=body.status,
  1338. address=body.address,
  1339. contact_name=body.contact_name,
  1340. contact_phone=body.contact_phone,
  1341. business_scope=body.business_scope,
  1342. tenant_no=tenant_no,
  1343. order_num=body.order_num,
  1344. remark=body.remark,
  1345. user_id=user.get("user_id"),
  1346. )
  1347. return success_response(site_svc._row_to_dict(obj), message="创建成功")
  1348. except ValueError as e:
  1349. return error_response(400, str(e))
  1350. @router.put("/sites/{site_id}")
  1351. def update_site(
  1352. site_id: str,
  1353. body: SiteUpdateBody,
  1354. user: dict = Depends(require_admin_or_super_admin),
  1355. db: Session = Depends(get_db),
  1356. ):
  1357. try:
  1358. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  1359. obj = site_svc.update_site(
  1360. db,
  1361. site_id,
  1362. site_name=payload.get("site_name"),
  1363. site_code=payload.get("site_code"),
  1364. site_type=payload.get("site_type"),
  1365. status=payload.get("status"),
  1366. address=payload.get("address"),
  1367. contact_name=payload.get("contact_name"),
  1368. contact_phone=payload.get("contact_phone"),
  1369. business_scope=payload.get("business_scope"),
  1370. order_num=payload.get("order_num"),
  1371. remark=payload.get("remark"),
  1372. user_id=user.get("user_id"),
  1373. )
  1374. return success_response(site_svc._row_to_dict(obj), message="更新成功")
  1375. except ValueError as e:
  1376. return error_response(400, str(e))
  1377. @router.delete("/sites/{site_id}")
  1378. def delete_site(
  1379. site_id: str,
  1380. _user: dict = Depends(require_admin_or_super_admin),
  1381. db: Session = Depends(get_db),
  1382. ):
  1383. try:
  1384. site_svc.soft_delete_site(db, site_id, user_info=_user)
  1385. return success_response(None, message="删除成功")
  1386. except ValueError as e:
  1387. return error_response(400, str(e))
  1388. # ---------------------------------------------------------------------------
  1389. # 职位管理
  1390. # ---------------------------------------------------------------------------
  1391. class PostCreateBody(BaseModel):
  1392. model_config = ConfigDict(populate_by_name=True)
  1393. post_name: str = Field(..., alias="postName")
  1394. post_code: str = Field(..., alias="postCode")
  1395. status: Optional[str] = Field("1")
  1396. order_num: Optional[int] = Field(0, alias="orderNum")
  1397. remark: Optional[str] = None
  1398. class PostUpdateBody(BaseModel):
  1399. model_config = ConfigDict(populate_by_name=True)
  1400. post_name: Optional[str] = Field(None, alias="postName")
  1401. post_code: Optional[str] = Field(None, alias="postCode")
  1402. status: Optional[str] = None
  1403. order_num: Optional[int] = Field(None, alias="orderNum")
  1404. remark: Optional[str] = None
  1405. @router.get("/posts")
  1406. def list_posts(
  1407. request: Request,
  1408. page: int = Query(1, ge=1),
  1409. pageSize: int = Query(10, ge=1, le=500, alias="pageSize"),
  1410. keyword: Optional[str] = None,
  1411. user: dict = Depends(require_menu_code("system:post:list")),
  1412. db: Session = Depends(get_db),
  1413. ):
  1414. tenant_no = _get_tenant_no(db, user)
  1415. items, total = post_svc.list_posts(
  1416. db,
  1417. page=page,
  1418. page_size=pageSize,
  1419. keyword=keyword,
  1420. post_name=request.query_params.get("postName"),
  1421. post_code=request.query_params.get("postCode"),
  1422. post_name_in=request.query_params.get("postNameIn"),
  1423. post_code_in=request.query_params.get("postCodeIn"),
  1424. status=request.query_params.get("status"),
  1425. status_in=request.query_params.get("statusIn"),
  1426. tenant_no=tenant_no,
  1427. user_info=user,
  1428. )
  1429. tp = post_svc.total_pages(total, pageSize)
  1430. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  1431. class PostColumnFacetBody(BaseModel):
  1432. """职位列表列头可选值(去重;可附带列表上下文)"""
  1433. model_config = ConfigDict(populate_by_name=True, extra="allow")
  1434. field: str = Field(..., description="postName | postCode")
  1435. keyword: Optional[str] = Field(default=None, description="可选值关键字")
  1436. limit: Optional[int] = Field(default=500, ge=1, le=500, description="返回条数上限")
  1437. _POST_FACET_COLUMNS = {
  1438. "postName": SysPost.post_name,
  1439. "postCode": SysPost.post_code,
  1440. }
  1441. @router.post("/posts/column-facet-values")
  1442. def post_column_facet_values(
  1443. body: PostColumnFacetBody,
  1444. user: dict = Depends(require_menu_code("system:post:list")),
  1445. db: Session = Depends(get_db),
  1446. ):
  1447. """职位列表列头可选值:在列表上下文(排除本列)下去重。"""
  1448. from app.services.pl.facet_context import strip_simple_facet_self_params
  1449. column = _POST_FACET_COLUMNS.get(body.field)
  1450. if column is None:
  1451. return error_response(400, "不支持的列")
  1452. try:
  1453. tenant_no = _get_tenant_no(db, user)
  1454. list_keyword = (body.keyword or "").strip() or None
  1455. params = strip_simple_facet_self_params(_facet_body_params(body), body.field)
  1456. extra = []
  1457. kw = list_keyword or str(_param_first(params, "keyword") or "").strip()
  1458. if kw:
  1459. like = f"%{kw}%"
  1460. extra.append(or_(SysPost.post_name.like(like), SysPost.post_code.like(like)))
  1461. for col, camel, snake in (
  1462. (SysPost.post_name, "postName", "post_name"),
  1463. (SysPost.post_code, "postCode", "post_code"),
  1464. ):
  1465. values = _split_csv_param(params, f"{camel}In", f"{snake}_in")
  1466. if values:
  1467. extra.append(col.in_(values))
  1468. else:
  1469. single = str(_param_first(params, camel, snake) or "").strip()
  1470. if single:
  1471. extra.append(col.like(f"%{single}%"))
  1472. status_values = _split_csv_param(params, "statusIn", "status_in")
  1473. if not status_values:
  1474. st = _param_first(params, "status")
  1475. if st:
  1476. status_values = [str(st).strip()]
  1477. if status_values:
  1478. extra.append(SysPost.status.in_(status_values))
  1479. rows = list_column_facet_values(
  1480. db,
  1481. SysPost,
  1482. column,
  1483. tenant_no=tenant_no,
  1484. keyword=None,
  1485. limit=body.limit if body.limit is not None else 500,
  1486. extra_filters=extra or None,
  1487. )
  1488. return success_response({"list": rows})
  1489. except ValueError as e:
  1490. return error_response(400, str(e))
  1491. except Exception as e:
  1492. return error_response(500, str(e))
  1493. POST_EXPORT_COLUMNS = [
  1494. ExportColumn(title="职位名称", field="postName"),
  1495. ExportColumn(title="职位编码", field="postCode"),
  1496. ExportColumn(title="状态", field="status", dict_type="sys_status"),
  1497. ExportColumn(title="创建时间", field="createTime"),
  1498. ]
  1499. class PostExportBody(BaseModel):
  1500. keyword: Optional[str] = None
  1501. ids: Optional[List[str]] = None
  1502. columns: Optional[List[str]] = None
  1503. @router.post("/posts/export")
  1504. def export_posts(
  1505. body: PostExportBody,
  1506. user: dict = Depends(require_menu_code("system:post:list")),
  1507. db: Session = Depends(get_db),
  1508. ):
  1509. """导出职位列表(当前筛选全量,或 ids 已选)。"""
  1510. try:
  1511. tenant_no = _get_tenant_no(db, user)
  1512. items, _ = post_svc.list_posts(
  1513. db,
  1514. page=1,
  1515. page_size=EXPORT_MAX_ROWS,
  1516. keyword=body.keyword,
  1517. tenant_no=tenant_no,
  1518. ids=body.ids,
  1519. max_page_size=EXPORT_MAX_ROWS,
  1520. user_info=user,
  1521. )
  1522. cols = filter_export_columns(POST_EXPORT_COLUMNS, body.columns)
  1523. date_str = datetime.now().strftime("%Y%m%d")
  1524. return export_rows_to_xlsx_response(
  1525. db,
  1526. tenant_no=tenant_no,
  1527. sheet_title="职位列表",
  1528. columns=cols,
  1529. rows=items,
  1530. filename_stem=f"职位列表_{date_str}",
  1531. )
  1532. except ExportValidationError as e:
  1533. return error_response(400, str(e))
  1534. except Exception as e:
  1535. return error_response(500, str(e))
  1536. @router.post("/posts")
  1537. def create_post(
  1538. body: PostCreateBody,
  1539. user: dict = Depends(require_admin_or_super_admin),
  1540. db: Session = Depends(get_db),
  1541. ):
  1542. try:
  1543. tenant_no = _get_tenant_no(db, user)
  1544. obj = post_svc.create_post(
  1545. db,
  1546. post_name=body.post_name,
  1547. post_code=body.post_code,
  1548. status=body.status,
  1549. tenant_no=tenant_no,
  1550. order_num=body.order_num,
  1551. remark=body.remark,
  1552. user_id=user.get("user_id"),
  1553. )
  1554. return success_response(post_svc._row_to_dict(obj), message="创建成功")
  1555. except ValueError as e:
  1556. return error_response(400, str(e))
  1557. @router.put("/posts/{post_id}")
  1558. def update_post(
  1559. post_id: str,
  1560. body: PostUpdateBody,
  1561. user: dict = Depends(require_admin_or_super_admin),
  1562. db: Session = Depends(get_db),
  1563. ):
  1564. try:
  1565. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  1566. obj = post_svc.update_post(
  1567. db,
  1568. post_id,
  1569. post_name=payload.get("post_name"),
  1570. post_code=payload.get("post_code"),
  1571. status=payload.get("status"),
  1572. order_num=payload.get("order_num"),
  1573. remark=payload.get("remark"),
  1574. user_id=user.get("user_id"),
  1575. )
  1576. return success_response(post_svc._row_to_dict(obj), message="更新成功")
  1577. except ValueError as e:
  1578. return error_response(400, str(e))
  1579. @router.delete("/posts/{post_id}")
  1580. def delete_post(
  1581. post_id: str,
  1582. _user: dict = Depends(require_admin_or_super_admin),
  1583. db: Session = Depends(get_db),
  1584. ):
  1585. try:
  1586. post_svc.soft_delete_post(db, post_id, user_info=_user)
  1587. return success_response(None, message="删除成功")
  1588. except ValueError as e:
  1589. return error_response(400, str(e))
  1590. class PostUsersUpdateBody(BaseModel):
  1591. model_config = ConfigDict(populate_by_name=True)
  1592. user_ids: List[str] = Field(default_factory=list, alias="userIds")
  1593. @router.get("/posts/{post_id}/users")
  1594. def list_post_bound_users(
  1595. post_id: str,
  1596. user: dict = Depends(require_menu_code("system:post:list")),
  1597. db: Session = Depends(get_db),
  1598. ):
  1599. tenant_no = _get_tenant_no(db, user)
  1600. post = post_svc.get_post(db, post_id)
  1601. if not post or post.tenant_no != tenant_no:
  1602. return error_response(404, "职位不存在")
  1603. ids = user_svc.list_user_ids_bound_to_post(db, post_id, tenant_no)
  1604. return success_response({"userIds": ids})
  1605. @router.put("/posts/{post_id}/users")
  1606. def update_post_bound_users(
  1607. post_id: str,
  1608. body: PostUsersUpdateBody,
  1609. user: dict = Depends(require_admin_or_super_admin),
  1610. db: Session = Depends(get_db),
  1611. ):
  1612. tenant_no = _get_tenant_no(db, user)
  1613. post = post_svc.get_post(db, post_id)
  1614. if not post or post.tenant_no != tenant_no:
  1615. return error_response(404, "职位不存在")
  1616. try:
  1617. user_svc.sync_post_user_bindings(
  1618. db, post_id, tenant_no, body.user_ids, user.get("user_id")
  1619. )
  1620. return success_response(None, message="保存成功")
  1621. except ValueError as e:
  1622. return error_response(400, str(e))
  1623. # ---------------------------------------------------------------------------
  1624. # 菜单管理
  1625. # ---------------------------------------------------------------------------
  1626. class MenuCreateBody(BaseModel):
  1627. model_config = ConfigDict(populate_by_name=True)
  1628. menu_name: str = Field(..., alias="menuName")
  1629. parent_id: str = Field("0", alias="parentId")
  1630. menu_type: str = Field("M", alias="menuType")
  1631. path: Optional[str] = Field("", alias="path")
  1632. component: Optional[str] = Field(None, alias="component")
  1633. perms: Optional[str] = Field(None, alias="perms")
  1634. is_frame: Optional[int] = Field(1, alias="isFrame")
  1635. visible: Optional[str] = Field("1", alias="visible")
  1636. status: Optional[str] = Field("1", alias="status")
  1637. order_num: Optional[int] = Field(None, alias="orderNum")
  1638. remark: Optional[str] = None
  1639. class MenuUpdateBody(BaseModel):
  1640. model_config = ConfigDict(populate_by_name=True)
  1641. menu_name: Optional[str] = Field(None, alias="menuName")
  1642. parent_id: Optional[str] = Field(None, alias="parentId")
  1643. menu_type: Optional[str] = Field(None, alias="menuType")
  1644. path: Optional[str] = Field(None, alias="path")
  1645. component: Optional[str] = Field(None, alias="component")
  1646. perms: Optional[str] = Field(None, alias="perms")
  1647. is_frame: Optional[int] = Field(None, alias="isFrame")
  1648. visible: Optional[str] = Field(None, alias="visible")
  1649. status: Optional[str] = Field(None, alias="status")
  1650. order_num: Optional[int] = Field(None, alias="orderNum")
  1651. remark: Optional[str] = None
  1652. @router.get("/menus")
  1653. def list_menus(
  1654. keyword: Optional[str] = None,
  1655. user: dict = Depends(require_admin_or_super_admin),
  1656. db: Session = Depends(get_db),
  1657. ):
  1658. tenant_no = _get_tenant_no(db, user)
  1659. tree = menu_svc.list_menus_tree(db, keyword=keyword, tenant_no=tenant_no, user_info=user)
  1660. return success_response(tree)
  1661. MENU_EXPORT_COLUMNS = [
  1662. ExportColumn(title="菜单名称", field="menuName"),
  1663. ExportColumn(title="菜单类型", field="menuType"),
  1664. ExportColumn(title="路由", field="path"),
  1665. ExportColumn(title="组件", field="component"),
  1666. ExportColumn(title="权限标识", field="perms"),
  1667. ExportColumn(title="状态", field="status", dict_type="sys_status"),
  1668. ExportColumn(title="可见", field="visible"),
  1669. ExportColumn(title="创建时间", field="createTime"),
  1670. ]
  1671. class MenuExportBody(BaseModel):
  1672. keyword: Optional[str] = None
  1673. ids: Optional[List[str]] = None
  1674. columns: Optional[List[str]] = None
  1675. @router.post("/menus/export")
  1676. def export_menus(
  1677. body: MenuExportBody,
  1678. user: dict = Depends(require_admin_or_super_admin),
  1679. db: Session = Depends(get_db),
  1680. ):
  1681. """导出菜单列表(扁平行;当前筛选或已选)。"""
  1682. try:
  1683. tenant_no = _get_tenant_no(db, user)
  1684. # list flat via tree then flatten, or use underlying list if exists
  1685. from app.crud import menu as menu_mod
  1686. if hasattr(menu_mod, 'list_menus'):
  1687. rows = menu_mod.list_menus(db, keyword=body.keyword, tenant_no=tenant_no, user_info=user)
  1688. else:
  1689. tree = menu_svc.list_menus_tree(db, keyword=body.keyword, tenant_no=tenant_no, user_info=user)
  1690. rows = []
  1691. def walk(nodes):
  1692. for n in nodes or []:
  1693. item = {k: v for k, v in n.items() if k != 'children'}
  1694. rows.append(item)
  1695. walk(n.get('children') or [])
  1696. walk(tree)
  1697. if body.ids:
  1698. idset = set(body.ids)
  1699. rows = [r for r in rows if r.get("id") in idset]
  1700. cols = filter_export_columns(MENU_EXPORT_COLUMNS, body.columns)
  1701. date_str = datetime.now().strftime("%Y%m%d")
  1702. return export_rows_to_xlsx_response(
  1703. db,
  1704. tenant_no=tenant_no,
  1705. sheet_title="菜单列表",
  1706. columns=cols,
  1707. rows=rows,
  1708. filename_stem=f"菜单列表_{date_str}",
  1709. )
  1710. except ExportValidationError as e:
  1711. return error_response(400, str(e))
  1712. except Exception as e:
  1713. return error_response(500, str(e))
  1714. @router.post("/menus")
  1715. def create_menu(
  1716. body: MenuCreateBody,
  1717. user: dict = Depends(require_admin_or_super_admin),
  1718. db: Session = Depends(get_db),
  1719. ):
  1720. try:
  1721. tenant_no = _get_tenant_no(db, user)
  1722. obj = menu_svc.create_menu(
  1723. db,
  1724. menu_name=body.menu_name,
  1725. parent_id=body.parent_id,
  1726. menu_type=body.menu_type,
  1727. path=body.path,
  1728. component=body.component,
  1729. perms=body.perms,
  1730. is_frame=body.is_frame,
  1731. visible=body.visible,
  1732. status=body.status,
  1733. tenant_no=tenant_no,
  1734. order_num=body.order_num,
  1735. remark=body.remark,
  1736. user_id=user.get("user_id"),
  1737. )
  1738. return success_response(menu_svc._row_to_dict(obj), message="创建成功")
  1739. except ValueError as e:
  1740. return error_response(400, str(e))
  1741. @router.put("/menus/{menu_id}")
  1742. def update_menu(
  1743. menu_id: str,
  1744. body: MenuUpdateBody,
  1745. user: dict = Depends(require_admin_or_super_admin),
  1746. db: Session = Depends(get_db),
  1747. ):
  1748. try:
  1749. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  1750. obj = menu_svc.update_menu(
  1751. db,
  1752. menu_id,
  1753. menu_name=payload.get("menu_name"),
  1754. parent_id=payload.get("parent_id"),
  1755. menu_type=payload.get("menu_type"),
  1756. path=payload.get("path"),
  1757. component=payload.get("component"),
  1758. perms=payload.get("perms"),
  1759. is_frame=payload.get("is_frame"),
  1760. visible=payload.get("visible"),
  1761. status=payload.get("status"),
  1762. order_num=payload.get("order_num"),
  1763. remark=payload.get("remark"),
  1764. user_id=user.get("user_id"),
  1765. )
  1766. return success_response(menu_svc._row_to_dict(obj), message="更新成功")
  1767. except ValueError as e:
  1768. return error_response(400, str(e))
  1769. @router.delete("/menus/{menu_id}")
  1770. def delete_menu(
  1771. menu_id: str,
  1772. _user: dict = Depends(require_admin_or_super_admin),
  1773. db: Session = Depends(get_db),
  1774. ):
  1775. try:
  1776. menu_svc.soft_delete_menu(db, menu_id, user_info=_user)
  1777. return success_response(None, message="删除成功")
  1778. except ValueError as e:
  1779. return error_response(400, str(e))
  1780. # ---------------------------------------------------------------------------
  1781. # 角色管理
  1782. # ---------------------------------------------------------------------------
  1783. class RoleCreateBody(BaseModel):
  1784. model_config = ConfigDict(populate_by_name=True)
  1785. role_name: str = Field(..., alias="roleName")
  1786. data_scope: Optional[str] = Field("4", alias="dataScope")
  1787. status: Optional[str] = Field("1", alias="status")
  1788. order_num: Optional[int] = Field(None, alias="orderNum")
  1789. remark: Optional[str] = None
  1790. class RoleUpdateBody(BaseModel):
  1791. model_config = ConfigDict(populate_by_name=True)
  1792. role_name: Optional[str] = Field(None, alias="roleName")
  1793. data_scope: Optional[str] = Field(None, alias="dataScope")
  1794. status: Optional[str] = Field(None, alias="status")
  1795. order_num: Optional[int] = Field(None, alias="orderNum")
  1796. remark: Optional[str] = None
  1797. class RoleMenuBody(BaseModel):
  1798. menu_ids: List[str] = Field(..., alias="menuIds")
  1799. class RoleDataScopeBody(BaseModel):
  1800. model_config = ConfigDict(populate_by_name=True)
  1801. data_scope: str = Field(..., alias="dataScope")
  1802. scope_details: Optional[List[dict]] = Field(None, alias="scopeDetails")
  1803. @router.get("/role-management")
  1804. def list_role_management(
  1805. request: Request,
  1806. page: int = Query(1, ge=1),
  1807. pageSize: int = Query(10, ge=1, le=500, alias="pageSize"),
  1808. keyword: Optional[str] = None,
  1809. user: dict = Depends(require_menu_code("system:role:list")),
  1810. db: Session = Depends(get_db),
  1811. ):
  1812. tenant_no = _get_tenant_no(db, user)
  1813. items, total = role_svc.list_roles(
  1814. db,
  1815. page=page,
  1816. page_size=pageSize,
  1817. keyword=keyword,
  1818. role_name=request.query_params.get("roleName"),
  1819. role_name_in=request.query_params.get("roleNameIn"),
  1820. status=request.query_params.get("status"),
  1821. status_in=request.query_params.get("statusIn"),
  1822. tenant_no=tenant_no,
  1823. user_info=user,
  1824. )
  1825. tp = role_svc.total_pages(total, pageSize)
  1826. return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})
  1827. class RoleColumnFacetBody(BaseModel):
  1828. """角色列表列头可选值(去重;可附带列表上下文)"""
  1829. model_config = ConfigDict(populate_by_name=True, extra="allow")
  1830. field: str = Field(..., description="roleName")
  1831. keyword: Optional[str] = Field(default=None, description="可选值关键字")
  1832. limit: Optional[int] = Field(default=500, ge=1, le=500, description="返回条数上限")
  1833. _ROLE_FACET_COLUMNS = {
  1834. "roleName": SysRole.role_name,
  1835. }
  1836. @router.post("/role-management/column-facet-values")
  1837. def role_column_facet_values(
  1838. body: RoleColumnFacetBody,
  1839. user: dict = Depends(require_menu_code("system:role:list")),
  1840. db: Session = Depends(get_db),
  1841. ):
  1842. """角色列表列头可选值:在列表上下文(排除本列)下去重。"""
  1843. from app.services.pl.facet_context import strip_simple_facet_self_params
  1844. column = _ROLE_FACET_COLUMNS.get(body.field)
  1845. if column is None:
  1846. return error_response(400, "不支持的列")
  1847. try:
  1848. tenant_no = _get_tenant_no(db, user)
  1849. list_keyword = (body.keyword or "").strip() or None
  1850. params = strip_simple_facet_self_params(_facet_body_params(body), body.field)
  1851. extra = []
  1852. kw = list_keyword or str(_param_first(params, "keyword") or "").strip()
  1853. if kw:
  1854. extra.append(SysRole.role_name.like(f"%{kw}%"))
  1855. values = _split_csv_param(params, "roleNameIn", "role_name_in")
  1856. if values:
  1857. extra.append(SysRole.role_name.in_(values))
  1858. else:
  1859. single = str(_param_first(params, "roleName", "role_name") or "").strip()
  1860. if single:
  1861. extra.append(SysRole.role_name.like(f"%{single}%"))
  1862. status_values = _split_csv_param(params, "statusIn", "status_in")
  1863. if not status_values:
  1864. st = _param_first(params, "status")
  1865. if st:
  1866. status_values = [str(st).strip()]
  1867. if status_values:
  1868. extra.append(SysRole.status.in_(status_values))
  1869. rows = list_column_facet_values(
  1870. db,
  1871. SysRole,
  1872. column,
  1873. tenant_no=tenant_no,
  1874. keyword=None,
  1875. limit=body.limit if body.limit is not None else 500,
  1876. apply_scope=lambda q: apply_data_scope(q, SysRole, user, db, tenant_no),
  1877. extra_filters=extra or None,
  1878. )
  1879. return success_response({"list": rows})
  1880. except ValueError as e:
  1881. return error_response(400, str(e))
  1882. except Exception as e:
  1883. return error_response(500, str(e))
  1884. ROLE_EXPORT_COLUMNS = [
  1885. ExportColumn(title="角色名称", field="roleName"),
  1886. ExportColumn(title="数据权限", field="dataScope", dict_type="sys_data_scope"),
  1887. ExportColumn(title="状态", field="status", dict_type="sys_status"),
  1888. ExportColumn(title="创建时间", field="createTime"),
  1889. ]
  1890. class RoleExportBody(BaseModel):
  1891. keyword: Optional[str] = None
  1892. ids: Optional[List[str]] = None
  1893. columns: Optional[List[str]] = None
  1894. @router.post("/role-management/export")
  1895. def export_role_management(
  1896. body: RoleExportBody,
  1897. user: dict = Depends(require_menu_code("system:role:list")),
  1898. db: Session = Depends(get_db),
  1899. ):
  1900. """导出角色列表(当前筛选全量,或 ids 已选)。"""
  1901. try:
  1902. tenant_no = _get_tenant_no(db, user)
  1903. items, _ = role_svc.list_roles(
  1904. db,
  1905. page=1,
  1906. page_size=EXPORT_MAX_ROWS,
  1907. keyword=body.keyword,
  1908. tenant_no=tenant_no,
  1909. ids=body.ids,
  1910. max_page_size=EXPORT_MAX_ROWS,
  1911. user_info=user,
  1912. )
  1913. cols = filter_export_columns(ROLE_EXPORT_COLUMNS, body.columns)
  1914. date_str = datetime.now().strftime("%Y%m%d")
  1915. return export_rows_to_xlsx_response(
  1916. db,
  1917. tenant_no=tenant_no,
  1918. sheet_title="角色列表",
  1919. columns=cols,
  1920. rows=items,
  1921. filename_stem=f"角色列表_{date_str}",
  1922. )
  1923. except ExportValidationError as e:
  1924. return error_response(400, str(e))
  1925. except Exception as e:
  1926. return error_response(500, str(e))
  1927. @router.post("/role-management")
  1928. def create_role_management(
  1929. body: RoleCreateBody,
  1930. user: dict = Depends(require_admin_or_super_admin),
  1931. db: Session = Depends(get_db),
  1932. ):
  1933. try:
  1934. tenant_no = _get_tenant_no(db, user)
  1935. obj = role_svc.create_role(
  1936. db,
  1937. role_name=body.role_name,
  1938. data_scope=body.data_scope,
  1939. status=body.status,
  1940. tenant_no=tenant_no,
  1941. order_num=body.order_num,
  1942. remark=body.remark,
  1943. user_id=user.get("user_id"),
  1944. )
  1945. return success_response(role_svc._row_to_dict(obj), message="创建成功")
  1946. except ValueError as e:
  1947. return error_response(400, str(e))
  1948. @router.get("/role-management/{role_id}")
  1949. def get_role_management(
  1950. role_id: str,
  1951. user: dict = Depends(require_admin_or_super_admin),
  1952. db: Session = Depends(get_db),
  1953. ):
  1954. tenant_no = _get_tenant_no(db, user)
  1955. obj = role_svc.get_role(db, role_id)
  1956. if not obj or obj.tenant_no != tenant_no:
  1957. return error_response(404, "角色不存在")
  1958. try:
  1959. assert_row_in_data_scope(db, obj, SysRole, user, tenant_no)
  1960. except HTTPException:
  1961. return error_response(403, "无权查看该角色")
  1962. return success_response(role_svc._row_to_dict(obj))
  1963. @router.put("/role-management/{role_id}")
  1964. def update_role_management(
  1965. role_id: str,
  1966. body: RoleUpdateBody,
  1967. user: dict = Depends(require_admin_or_super_admin),
  1968. db: Session = Depends(get_db),
  1969. ):
  1970. try:
  1971. payload: dict[str, Any] = body.model_dump(exclude_unset=True, by_alias=False)
  1972. obj = role_svc.update_role(
  1973. db,
  1974. role_id,
  1975. role_name=payload.get("role_name"),
  1976. data_scope=payload.get("data_scope"),
  1977. status=payload.get("status"),
  1978. order_num=payload.get("order_num"),
  1979. remark=payload.get("remark"),
  1980. user_id=user.get("user_id"),
  1981. )
  1982. return success_response(role_svc._row_to_dict(obj), message="更新成功")
  1983. except ValueError as e:
  1984. return error_response(400, str(e))
  1985. @router.delete("/role-management/{role_id}")
  1986. def delete_role_management(
  1987. role_id: str,
  1988. _user: dict = Depends(require_admin_or_super_admin),
  1989. db: Session = Depends(get_db),
  1990. ):
  1991. try:
  1992. role_svc.soft_delete_role(db, role_id, user_info=_user)
  1993. return success_response(None, message="删除成功")
  1994. except ValueError as e:
  1995. return error_response(400, str(e))
  1996. @router.get("/role-management/{role_id}/menus")
  1997. def get_role_menus(
  1998. role_id: str,
  1999. _user: dict = Depends(require_admin_or_super_admin),
  2000. db: Session = Depends(get_db),
  2001. ):
  2002. menu_ids = role_svc.get_role_menu_ids(db, role_id)
  2003. return success_response({"menuIds": menu_ids})
  2004. @router.put("/role-management/{role_id}/menus")
  2005. def update_role_menus(
  2006. role_id: str,
  2007. body: RoleMenuBody,
  2008. user: dict = Depends(require_admin_or_super_admin),
  2009. db: Session = Depends(get_db),
  2010. ):
  2011. try:
  2012. tenant_no = _get_tenant_no(db, user)
  2013. role_svc.update_role_menus(db, role_id, body.menu_ids, tenant_no, user_id=user.get("user_id"))
  2014. return success_response(None, message="更新成功")
  2015. except ValueError as e:
  2016. return error_response(400, str(e))
  2017. @router.get("/role-management/{role_id}/data-scope")
  2018. def get_role_data_scope(
  2019. role_id: str,
  2020. _user: dict = Depends(require_admin_or_super_admin),
  2021. db: Session = Depends(get_db),
  2022. ):
  2023. role = role_svc.get_role(db, role_id)
  2024. if not role:
  2025. return error_response(404, "角色不存在")
  2026. details = role_svc.get_role_data_scope(db, role_id)
  2027. return success_response({"dataScope": role.data_scope or "1", "scopeDetails": details})
  2028. @router.put("/role-management/{role_id}/data-scope")
  2029. def update_role_data_scope(
  2030. role_id: str,
  2031. body: RoleDataScopeBody,
  2032. user: dict = Depends(require_admin_or_super_admin),
  2033. db: Session = Depends(get_db),
  2034. ):
  2035. try:
  2036. tenant_no = _get_tenant_no(db, user)
  2037. role_svc.update_role_data_scope(
  2038. db,
  2039. role_id,
  2040. body.data_scope,
  2041. body.scope_details,
  2042. tenant_no=tenant_no,
  2043. user_id=user.get("user_id"),
  2044. )
  2045. return success_response(None, message="更新成功")
  2046. except ValueError as e:
  2047. return error_response(400, str(e))
  2048. # ---------------------------------------------------------------------------
  2049. # 附件上传
  2050. # ---------------------------------------------------------------------------
  2051. def _format_file_size(size_bytes: int) -> str:
  2052. if size_bytes < 1024:
  2053. return f"{size_bytes} B"
  2054. elif size_bytes < 1024 * 1024:
  2055. return f"{size_bytes / 1024:.1f} KB"
  2056. else:
  2057. return f"{size_bytes / (1024 * 1024):.1f} MB"
  2058. @router.post("/attachment/upload")
  2059. def upload_attachment(
  2060. file: UploadFile = File(...),
  2061. folder: Optional[str] = Form(""),
  2062. biz_type: Optional[str] = Form(None, alias="bizType"),
  2063. user: dict = Depends(require_admin_or_super_admin),
  2064. db: Session = Depends(get_db),
  2065. ):
  2066. if not file or not file.filename:
  2067. return error_response(400, "请选择文件")
  2068. try:
  2069. effective_folder = resolve_upload_folder(
  2070. folder=folder or "", biz_type=biz_type or ""
  2071. )
  2072. file_content = file.file.read()
  2073. full_url = storage.upload_file(file_content, file.filename, effective_folder)
  2074. base_url = settings.MINIO_BASE_URL.rstrip("/") if settings.MINIO_BASE_URL else ""
  2075. if base_url and full_url.startswith(base_url + "/"):
  2076. file_url = full_url[len(base_url) + 1:]
  2077. else:
  2078. file_url = full_url
  2079. file_size = len(file_content)
  2080. file_ext = os.path.splitext(file.filename)[1].lstrip(".") if file.filename else ""
  2081. file_type = file.content_type or ""
  2082. tenant_no = _get_tenant_no(db, user)
  2083. record = oss_svc.create_oss(
  2084. db,
  2085. tenant_no=tenant_no,
  2086. url=file_url,
  2087. original_name=file.filename,
  2088. file_suffix=file_ext,
  2089. file_size=file_size,
  2090. file_type=file_type,
  2091. biz_type=(biz_type or "").strip(),
  2092. user_id=user.get("user_id"),
  2093. ext1=effective_folder or None,
  2094. )
  2095. return success_response({
  2096. "id": record.id,
  2097. "fileName": record.original_name,
  2098. "fileUrl": record.url,
  2099. "fileSize": record.file_size,
  2100. "fileSizeDisplay": _format_file_size(record.file_size or 0),
  2101. "fileType": record.file_type,
  2102. "bizType": record.biz_type or "",
  2103. })
  2104. except Exception as e:
  2105. db.rollback()
  2106. return error_response(500, f"上传失败: {str(e)}")
  2107. class AttachmentDownloadUrlBody(BaseModel):
  2108. model_config = ConfigDict(populate_by_name=True)
  2109. file_url: str = Field(..., alias="fileUrl")
  2110. file_name: Optional[str] = Field(None, alias="fileName")
  2111. @router.post("/attachment/download-url")
  2112. def attachment_download_url(
  2113. body: AttachmentDownloadUrlBody,
  2114. user: dict = Depends(require_admin_or_super_admin),
  2115. db: Session = Depends(get_db),
  2116. ):
  2117. """返回带 Content-Disposition 的下载 URL,保证浏览器使用原始文件名。"""
  2118. file_url = (body.file_url or "").strip()
  2119. if not file_url:
  2120. return error_response(400, "文件地址不能为空")
  2121. tenant_no = _get_tenant_no(db, user)
  2122. file_url, att = oss_svc.resolve_storage_path(db, tenant_no, file_url)
  2123. if not att:
  2124. att = oss_svc.get_oss_by_url(db, tenant_no, file_url)
  2125. download_name = (body.file_name or "").strip()
  2126. if not download_name and att:
  2127. download_name = (att.original_name or "").strip()
  2128. if not download_name:
  2129. download_name = file_url.rsplit("/", 1)[-1] or "download"
  2130. try:
  2131. url = storage.presigned_download_url(file_url, download_name)
  2132. return success_response({"downloadUrl": url})
  2133. except Exception as e:
  2134. return error_response(500, f"获取下载链接失败: {str(e)}")
  2135. @router.post("/attachment/preview-url")
  2136. def attachment_preview_url(
  2137. body: AttachmentDownloadUrlBody,
  2138. user: dict = Depends(require_admin_or_super_admin),
  2139. db: Session = Depends(get_db),
  2140. ):
  2141. """返回用于浏览器内联预览的 URL(PDF iframe 等)。"""
  2142. file_url = (body.file_url or "").strip()
  2143. if not file_url:
  2144. return error_response(400, "文件地址不能为空")
  2145. tenant_no = _get_tenant_no(db, user)
  2146. file_url, _att = oss_svc.resolve_storage_path(db, tenant_no, file_url)
  2147. try:
  2148. url = storage.presigned_preview_url(file_url)
  2149. return success_response({"previewUrl": url})
  2150. except Exception as e:
  2151. return error_response(500, f"获取预览链接失败: {str(e)}")
  2152. # ---------------------------------------------------------------------------
  2153. # 用户菜单收藏
  2154. # ---------------------------------------------------------------------------
  2155. @router.get("/menu-favorites")
  2156. def list_menu_favorites(
  2157. user: dict = Depends(require_admin_or_super_admin),
  2158. db: Session = Depends(get_db),
  2159. ):
  2160. user_id = user.get("user_id")
  2161. if not user_id:
  2162. return error_response(401, "用户无效")
  2163. tenant_no = _get_tenant_no(db, user)
  2164. from app.crud import menu_favorite as fav_svc
  2165. return success_response(fav_svc.list_favorites(db, user_id=user_id, tenant_no=tenant_no))
  2166. @router.post("/menu-favorites")
  2167. def create_menu_favorite(
  2168. body: MenuFavoriteCreateBody,
  2169. user: dict = Depends(require_admin_or_super_admin),
  2170. db: Session = Depends(get_db),
  2171. ):
  2172. from app.crud import menu_favorite as fav_svc
  2173. user_id = user.get("user_id")
  2174. if not user_id:
  2175. return error_response(401, "用户无效")
  2176. tenant_no = _get_tenant_no(db, user)
  2177. try:
  2178. data = fav_svc.add_favorite(
  2179. db,
  2180. user_id=user_id,
  2181. tenant_no=tenant_no,
  2182. menu_path=body.path,
  2183. create_dept_id=str(user.get("dept_id") or ""),
  2184. )
  2185. return success_response(data)
  2186. except ValueError as e:
  2187. return error_response(400, str(e))
  2188. @router.put("/menu-favorites/order")
  2189. def reorder_menu_favorites(
  2190. body: MenuFavoriteReorderBody,
  2191. user: dict = Depends(require_admin_or_super_admin),
  2192. db: Session = Depends(get_db),
  2193. ):
  2194. from app.crud import menu_favorite as fav_svc
  2195. user_id = user.get("user_id")
  2196. if not user_id:
  2197. return error_response(401, "用户无效")
  2198. tenant_no = _get_tenant_no(db, user)
  2199. try:
  2200. data = fav_svc.reorder_favorites(
  2201. db, user_id=user_id, tenant_no=tenant_no, paths=body.paths or []
  2202. )
  2203. return success_response(data)
  2204. except ValueError as e:
  2205. return error_response(400, str(e))
  2206. @router.delete("/menu-favorites")
  2207. def delete_menu_favorite(
  2208. body: MenuFavoriteDeleteBody,
  2209. user: dict = Depends(require_admin_or_super_admin),
  2210. db: Session = Depends(get_db),
  2211. ):
  2212. from app.crud import menu_favorite as fav_svc
  2213. user_id = user.get("user_id")
  2214. if not user_id:
  2215. return error_response(401, "用户无效")
  2216. tenant_no = _get_tenant_no(db, user)
  2217. fav_svc.soft_delete_favorite(
  2218. db, user_id=user_id, tenant_no=tenant_no, menu_path=body.path
  2219. )
  2220. return success_response({"path": body.path})
  2221. # ---------------------------------------------------------------------------
  2222. # Demo 模板页:Mock 数据由前端组装行后走统一 xlsx 导出(无 Demo 业务表)
  2223. # ---------------------------------------------------------------------------
  2224. class DemoXlsxColumnSpec(BaseModel):
  2225. model_config = ConfigDict(populate_by_name=True)
  2226. title: str
  2227. field: str
  2228. class DemoXlsxExportBody(BaseModel):
  2229. model_config = ConfigDict(populate_by_name=True)
  2230. sheet_title: str = Field(default="导出", alias="sheetTitle")
  2231. filename_stem: str = Field(default="export", alias="filenameStem")
  2232. columns: List[DemoXlsxColumnSpec]
  2233. rows: List[dict]
  2234. @router.post("/demo/xlsx-export")
  2235. def demo_xlsx_export(
  2236. body: DemoXlsxExportBody,
  2237. user: dict = Depends(require_admin_or_super_admin),
  2238. db: Session = Depends(get_db),
  2239. ):
  2240. """Demo 列表导出:接收已筛选行与列定义,生成 xlsx。"""
  2241. try:
  2242. rows = body.rows or []
  2243. if not rows:
  2244. raise ExportValidationError("没有可导出的数据")
  2245. if len(rows) > EXPORT_MAX_ROWS:
  2246. raise ExportValidationError(f"导出超过 {EXPORT_MAX_ROWS} 条,请缩小筛选范围")
  2247. if not body.columns:
  2248. raise ExportValidationError("没有可导出的列")
  2249. cols = [ExportColumn(title=c.title, field=c.field) for c in body.columns]
  2250. tenant_no = _get_tenant_no(db, user)
  2251. date_str = datetime.now().strftime("%Y%m%d")
  2252. stem = (body.filename_stem or "export").strip() or "export"
  2253. return export_rows_to_xlsx_response(
  2254. db,
  2255. tenant_no=tenant_no,
  2256. sheet_title=body.sheet_title or "导出",
  2257. columns=cols,
  2258. rows=rows,
  2259. filename_stem=f"{stem}_{date_str}",
  2260. )
  2261. except ExportValidationError as e:
  2262. return error_response(400, str(e))