_gen_inventory.py 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004
  1. # -*- coding: utf-8 -*-
  2. """Generate a complete JeeSite MES inventory for RuoYi-Vue migration."""
  3. from __future__ import annotations
  4. import os
  5. import re
  6. import sys
  7. from collections import defaultdict
  8. from datetime import datetime
  9. from pathlib import Path
  10. ROOT = Path(r"D:\IdeaProjects\mes-199-0Y7A\mescloud")
  11. JAVA_MES = ROOT / "src/main/java/com/jeesite/modules/mes"
  12. JAVA_UTILS = ROOT / "src/main/java/com/jeesite/modules/utils"
  13. VIEWS = ROOT / "src/main/resources/views/modules/mes"
  14. WEB = JAVA_MES / "web"
  15. ENTITY = JAVA_MES / "entity"
  16. SQL = ROOT / "mes_cloud_199_0y7a.sql"
  17. APP = ROOT / "src/main/java/com/jeesite/modules/Application.java"
  18. STANDARD_CRUD = {"list", "listData", "form", "save", "delete", "disable", "enable"}
  19. def read_text(p: Path) -> str:
  20. return p.read_text(encoding="utf-8", errors="replace")
  21. def match_parens(s: str, start: int) -> int:
  22. """start points at '('; return index after matching ')'."""
  23. depth = 0
  24. i = start
  25. in_str = False
  26. quote = ""
  27. while i < len(s):
  28. c = s[i]
  29. if in_str:
  30. if c == "\\" and i + 1 < len(s):
  31. i += 2
  32. continue
  33. if c == quote:
  34. in_str = False
  35. i += 1
  36. continue
  37. if c in ('"', "'"):
  38. in_str = True
  39. quote = c
  40. i += 1
  41. continue
  42. if c == "(":
  43. depth += 1
  44. elif c == ")":
  45. depth -= 1
  46. if depth == 0:
  47. return i + 1
  48. i += 1
  49. return -1
  50. def match_braces(s: str, start: int) -> int:
  51. depth = 0
  52. i = start
  53. in_str = False
  54. quote = ""
  55. while i < len(s):
  56. c = s[i]
  57. if in_str:
  58. if c == "\\" and i + 1 < len(s):
  59. i += 2
  60. continue
  61. if c == quote:
  62. in_str = False
  63. i += 1
  64. continue
  65. if c in ('"', "'"):
  66. in_str = True
  67. quote = c
  68. i += 1
  69. continue
  70. if c == "{":
  71. depth += 1
  72. elif c == "}":
  73. depth -= 1
  74. if depth == 0:
  75. return i + 1
  76. i += 1
  77. return -1
  78. def attr_str(block: str, key: str) -> str | None:
  79. m = re.search(rf'{key}\s*=\s*"([^"]*)"', block)
  80. return m.group(1) if m else None
  81. def attr_ident(block: str, key: str) -> str | None:
  82. m = re.search(rf"{key}\s*=\s*([A-Za-z0-9_.]+)", block)
  83. return m.group(1) if m else None
  84. def parse_mapping_value(ann: str) -> str:
  85. """Parse Spring mapping annotation body into a display string."""
  86. body = ann.strip()
  87. if body.startswith("(") and body.endswith(")"):
  88. body = body[1:-1].strip()
  89. if not body:
  90. return ""
  91. m = re.search(r"value\s*=\s*", body)
  92. if m:
  93. rest = body[m.end() :].lstrip()
  94. if rest.startswith("{"):
  95. end = match_braces(rest, 0)
  96. inner = rest[1 : end - 1]
  97. parts = re.findall(r'"([^"]*)"', inner)
  98. return "{" + ", ".join(f'"{p}"' for p in parts) + "}"
  99. m2 = re.match(r'"([^"]*)"', rest)
  100. if m2:
  101. return m2.group(1)
  102. return rest.split(",")[0].strip()
  103. m2 = re.match(r'"([^"]*)"', body)
  104. if m2:
  105. return m2.group(1)
  106. if body.startswith("{"):
  107. end = match_braces(body, 0)
  108. inner = body[1 : end - 1]
  109. parts = re.findall(r'"([^"]*)"', inner)
  110. return "{" + ", ".join(f'"{p}"' for p in parts) + "}"
  111. return body.split(",")[0].strip()
  112. def mapping_paths(val: str) -> list[str]:
  113. if not val:
  114. return [""]
  115. if val.startswith("{") and val.endswith("}"):
  116. return re.findall(r'"([^"]*)"', val) or [""]
  117. return [val]
  118. # ---------------------------------------------------------------------------
  119. # 1. Views
  120. # ---------------------------------------------------------------------------
  121. def collect_views():
  122. groups = {"List": [], "Form": [], "Info": [], "Screen": [], "other": []}
  123. files = sorted(VIEWS.rglob("*.html"))
  124. for f in files:
  125. rel = f.relative_to(VIEWS).as_posix()
  126. name = f.name
  127. # Screen dashboards: mesScreen.html / mesScreenN.html (not mesScreenPlan*)
  128. if re.fullmatch(r"mesScreen(\d*)\.html", name, re.I):
  129. groups["Screen"].append(rel)
  130. elif "List" in name:
  131. groups["List"].append(rel)
  132. elif "Form" in name:
  133. groups["Form"].append(rel)
  134. elif "Info" in name:
  135. groups["Info"].append(rel)
  136. else:
  137. groups["other"].append(rel)
  138. return files, groups
  139. # ---------------------------------------------------------------------------
  140. # 2. Controllers
  141. # ---------------------------------------------------------------------------
  142. ANN_RE = re.compile(
  143. r"@(RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)\s*(\([^;{]*?\)|(?=\s))",
  144. re.S,
  145. )
  146. def strip_java_comments(src: str) -> str:
  147. src = re.sub(r"/\*.*?\*/", lambda m: "\n" * m.group(0).count("\n"), src, flags=re.S)
  148. src = re.sub(r"//.*?$", "", src, flags=re.M)
  149. return src
  150. def parse_controller(path: Path) -> dict:
  151. raw = read_text(path)
  152. src = strip_java_comments(raw)
  153. class_m = re.search(r"public\s+class\s+(\w+)", src)
  154. if not class_m:
  155. return None
  156. cls = class_m.group(1)
  157. class_annos = src[: class_m.start()]
  158. class_path = ""
  159. for m in re.finditer(r"@RequestMapping\s*(\([^)]*(?:\([^)]*\)[^)]*)*\)|)", class_annos):
  160. ann = m.group(1) or ""
  161. # handle nested parens better
  162. start = m.start()
  163. if src[m.end() - 1 if m.group(1) else m.end() :]:
  164. pass
  165. at = src.find("@RequestMapping", 0)
  166. # more reliable: find last @RequestMapping before class
  167. last = None
  168. for m in re.finditer(r"@RequestMapping", class_annos):
  169. last = m
  170. if last:
  171. i = last.end()
  172. while i < len(src) and src[i].isspace():
  173. i += 1
  174. if i < len(src) and src[i] == "(":
  175. end = match_parens(src, i)
  176. class_path = parse_mapping_value(src[i:end])
  177. else:
  178. class_path = ""
  179. methods = []
  180. # Find mapping annotations followed eventually by a public method
  181. i = class_m.end()
  182. while True:
  183. m = re.search(
  184. r"@(RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)\b",
  185. src[i:],
  186. )
  187. if not m:
  188. break
  189. abs_start = i + m.start()
  190. kind = m.group(1)
  191. j = i + m.end()
  192. while j < len(src) and src[j].isspace():
  193. j += 1
  194. if j < len(src) and src[j] == "(":
  195. end = match_parens(src, j)
  196. val = parse_mapping_value(src[j:end])
  197. after = end
  198. else:
  199. val = ""
  200. after = j
  201. # look ahead for public method (skip other annotations)
  202. ahead = src[after : after + 800]
  203. mm = re.search(
  204. r"public\s+(?:static\s+)?(?:[\w.<>,\s?\[\]]+)\s+(\w+)\s*\(",
  205. ahead,
  206. )
  207. if mm:
  208. name = mm.group(1)
  209. # skip constructors
  210. if name != cls:
  211. flag = name not in STANDARD_CRUD
  212. methods.append(
  213. {
  214. "name": name,
  215. "http": kind,
  216. "path": val,
  217. "business": flag,
  218. }
  219. )
  220. i = after
  221. return {"class": cls, "path": class_path, "file": path.name, "methods": methods}
  222. def collect_controllers():
  223. out = []
  224. for f in sorted(WEB.glob("*.java")):
  225. c = parse_controller(f)
  226. if c:
  227. out.append(c)
  228. return out
  229. # ---------------------------------------------------------------------------
  230. # 3. Entities
  231. # ---------------------------------------------------------------------------
  232. def split_top_level_annos(block: str, prefix: str) -> list[str]:
  233. """Split `@Xxx(...)` occurrences at top level of a `{...}` array."""
  234. items = []
  235. i = 0
  236. token = "@" + prefix
  237. while True:
  238. j = block.find(token, i)
  239. if j < 0:
  240. break
  241. k = j + len(token)
  242. while k < len(block) and block[k].isspace():
  243. k += 1
  244. if k < len(block) and block[k] == "(":
  245. end = match_parens(block, k)
  246. items.append(block[j:end])
  247. i = end
  248. else:
  249. i = k
  250. return items
  251. def parse_column_ann(ann: str) -> dict:
  252. body = ann[ann.find("(") :] if "(" in ann else ""
  253. return {
  254. "name": attr_str(body, "name") or "",
  255. "attrName": attr_str(body, "attrName") or "",
  256. "label": attr_str(body, "label") or "",
  257. }
  258. def parse_join_ann(ann: str) -> dict:
  259. body = ann[ann.find("(") :] if "(" in ann else ann
  260. entity = attr_ident(body, "entity") or ""
  261. alias = attr_str(body, "alias") or ""
  262. on = attr_str(body, "on") or ""
  263. typ = (attr_ident(body, "type") or "").replace("Type.", "").replace("JoinTable.", "")
  264. cols = []
  265. cm = re.search(r"columns\s*=\s*\{", body)
  266. if cm:
  267. end = match_braces(body, cm.end() - 1)
  268. inner = body[cm.end() - 1 : end]
  269. for c in split_top_level_annos(inner, "Column"):
  270. cols.append(parse_column_ann(c))
  271. return {
  272. "type": typ.replace("Type.", ""),
  273. "entity": entity.replace(".class", ""),
  274. "alias": alias,
  275. "on": on,
  276. "columns": cols,
  277. }
  278. def parse_entity(path: Path) -> dict:
  279. src = read_text(path)
  280. class_m = re.search(r"public\s+class\s+(\w+)", src)
  281. cls = class_m.group(1) if class_m else path.stem
  282. rec = {
  283. "class": cls,
  284. "file": path.name,
  285. "table": None,
  286. "label": None,
  287. "alias": None,
  288. "columns": [],
  289. "joins": [],
  290. "has_table": False,
  291. }
  292. tpos = src.find("@Table")
  293. if tpos < 0:
  294. return rec
  295. i = tpos + len("@Table")
  296. while i < len(src) and src[i].isspace():
  297. i += 1
  298. if i >= len(src) or src[i] != "(":
  299. return rec
  300. end = match_parens(src, i)
  301. body = src[i:end]
  302. rec["has_table"] = True
  303. rec["table"] = attr_str(body, "name")
  304. rec["label"] = attr_str(body, "label")
  305. rec["alias"] = attr_str(body, "alias")
  306. # columns at Table level: first columns={ that is not nested in joinTable
  307. # Extract joinTable first, then columns outside it
  308. join_m = re.search(r"joinTable\s*=\s*\{", body)
  309. join_span = None
  310. if join_m:
  311. jend = match_braces(body, join_m.end() - 1)
  312. join_span = (join_m.start(), jend)
  313. inner = body[join_m.end() - 1 : jend]
  314. for j in split_top_level_annos(inner, "JoinTable"):
  315. rec["joins"].append(parse_join_ann(j))
  316. # find columns= that is NOT inside join_span
  317. for cm in re.finditer(r"columns\s*=\s*\{", body):
  318. if join_span and join_span[0] <= cm.start() < join_span[1]:
  319. continue
  320. cend = match_braces(body, cm.end() - 1)
  321. inner = body[cm.end() - 1 : cend]
  322. for c in split_top_level_annos(inner, "Column"):
  323. rec["columns"].append(parse_column_ann(c))
  324. break
  325. return rec
  326. def collect_entities():
  327. out = []
  328. for f in sorted(ENTITY.glob("*.java")):
  329. out.append(parse_entity(f))
  330. return out
  331. # ---------------------------------------------------------------------------
  332. # 4. SQL menus + tables
  333. # ---------------------------------------------------------------------------
  334. def split_sql_values(s: str) -> list[str]:
  335. vals = []
  336. i = 0
  337. n = len(s)
  338. while i < n:
  339. while i < n and s[i] in " \t\r\n":
  340. i += 1
  341. if i >= n:
  342. break
  343. if s[i] == "'":
  344. i += 1
  345. buf = []
  346. while i < n:
  347. if s[i] == "'" and i + 1 < n and s[i + 1] == "'":
  348. buf.append("'")
  349. i += 2
  350. continue
  351. if s[i] == "'":
  352. i += 1
  353. break
  354. buf.append(s[i])
  355. i += 1
  356. vals.append("".join(buf))
  357. while i < n and s[i] in " \t\r\n":
  358. i += 1
  359. if i < n and s[i] == ",":
  360. i += 1
  361. continue
  362. # unquoted
  363. j = i
  364. while j < n and s[j] != ",":
  365. j += 1
  366. token = s[i:j].strip()
  367. vals.append(None if token.upper() == "NULL" else token)
  368. i = j + 1 if j < n and s[j] == "," else j
  369. return vals
  370. def collect_sql():
  371. tables = []
  372. menus = []
  373. if not SQL.exists():
  374. return tables, menus
  375. # Stream line by line — dump can be large
  376. with SQL.open(encoding="utf-8", errors="replace") as fh:
  377. for line in fh:
  378. if line.startswith("CREATE TABLE `mes_"):
  379. m = re.match(r"CREATE TABLE `(mes_[^`]+)`", line)
  380. if m:
  381. tables.append({"name": m.group(1), "comment": None})
  382. continue
  383. if tables and tables[-1]["comment"] is None:
  384. cm = re.search(r"COMMENT\s*=\s*'([^']*)'", line)
  385. if cm:
  386. tables[-1]["comment"] = cm.group(1)
  387. if line.startswith(")") or line.startswith("CREATE TABLE") or line.startswith("INSERT"):
  388. if tables[-1]["comment"] is None:
  389. tables[-1]["comment"] = ""
  390. if "INSERT INTO `js_sys_menu`" in line:
  391. m = re.search(r"VALUES\s*\((.*)\)\s*;?\s*$", line)
  392. if not m:
  393. continue
  394. rec = parse_menu_values_tuple(m.group(1))
  395. if rec:
  396. rec["_source"] = "mes_cloud_199_0y7a.sql"
  397. menus.append(rec)
  398. return tables, menus
  399. def parse_menu_values_tuple(inner: str) -> dict | None:
  400. vals = split_sql_values(inner.strip().rstrip(";").strip())
  401. if len(vals) < 20:
  402. return None
  403. return {
  404. "menu_code": vals[0] or "",
  405. "parent_code": vals[1] or "",
  406. "parent_codes": vals[2] or "",
  407. "tree_sort": vals[3],
  408. "tree_leaf": vals[5],
  409. "tree_level": vals[6],
  410. "tree_names": vals[7] or "",
  411. "menu_name": vals[8] or "",
  412. "menu_type": str(vals[9] or "").strip("'"),
  413. "menu_href": vals[10] or "",
  414. "menu_target": vals[11] or "",
  415. "menu_icon": vals[12] or "",
  416. "menu_color": vals[13] or "",
  417. "menu_title": vals[14] or "",
  418. "permission": vals[15] or "",
  419. "weight": vals[16],
  420. "is_show": str(vals[17] or "").strip("'"),
  421. "sys_code": vals[18] or "",
  422. "module_codes": vals[19] or "",
  423. "status": vals[22] if len(vals) > 22 else "",
  424. "_source": "dump",
  425. }
  426. def collect_incremental_menus(existing_codes: set[str]):
  427. extra = []
  428. ddl_dir = ROOT / "db" / "mysql"
  429. if not ddl_dir.exists():
  430. return extra
  431. skip = {"mes_cloud-2023-6-10.sql"}
  432. for f in sorted(ddl_dir.glob("*.sql")):
  433. if f.name in skip:
  434. continue
  435. txt = read_text(f)
  436. for m in re.finditer(
  437. r"INSERT INTO `js_sys_menu`(?:\s*\([^)]+\))?\s*VALUES\s*",
  438. txt,
  439. re.I,
  440. ):
  441. i = m.end()
  442. while i < len(txt) and txt[i].isspace():
  443. i += 1
  444. while i < len(txt) and txt[i] == "(":
  445. end = match_parens(txt, i)
  446. rec = parse_menu_values_tuple(txt[i + 1 : end - 1])
  447. if rec and rec["menu_code"] not in existing_codes:
  448. rec["_source"] = f.name
  449. extra.append(rec)
  450. existing_codes.add(rec["menu_code"])
  451. i = end
  452. while i < len(txt) and txt[i] in " \t\r\n":
  453. i += 1
  454. if i < len(txt) and txt[i] == ",":
  455. i += 1
  456. while i < len(txt) and txt[i].isspace():
  457. i += 1
  458. else:
  459. break
  460. for m in re.finditer(
  461. r"INSERT INTO `js_sys_menu`\s*SELECT\s+",
  462. txt,
  463. re.I,
  464. ):
  465. rest = txt[m.end() :]
  466. fm = re.search(r"\bFROM\b", rest, re.I)
  467. if not fm:
  468. continue
  469. rec = parse_menu_values_tuple(rest[: fm.start()])
  470. if rec and rec["menu_code"] not in existing_codes:
  471. rec["_source"] = f.name
  472. extra.append(rec)
  473. existing_codes.add(rec["menu_code"])
  474. return extra
  475. def is_mes_menu(m: dict) -> bool:
  476. mc = (m.get("module_codes") or "").lower()
  477. href = (m.get("menu_href") or "").lower()
  478. perm = (m.get("permission") or "").lower()
  479. parts = [p.strip() for p in mc.replace(";", ",").split(",") if p.strip()]
  480. if "mes" in parts:
  481. return True
  482. if "/mes/" in href or href.startswith("mes/") or href.startswith("/mes"):
  483. return True
  484. if "mes:" in perm:
  485. return True
  486. return False
  487. def is_visible_menu(m: dict) -> bool:
  488. return str(m.get("menu_type")) == "1" and str(m.get("is_show")) == "1"
  489. def sort_menu_key(m: dict):
  490. try:
  491. ts = float(m.get("tree_sort") or 0)
  492. except (TypeError, ValueError):
  493. ts = 0
  494. try:
  495. lv = float(m.get("tree_level") or 0)
  496. except (TypeError, ValueError):
  497. lv = 0
  498. return (lv, ts, m.get("menu_name") or "")
  499. def build_menu_tree(all_menus, mes_menus):
  500. """Nest visible MES menus under ancestor folders (folders often use module_codes=app)."""
  501. by_code = {m["menu_code"]: m for m in all_menus}
  502. visible_mes = [m for m in mes_menus if is_visible_menu(m)]
  503. included = {m["menu_code"]: m for m in visible_mes}
  504. for m in list(visible_mes):
  505. pc = m.get("parent_code") or "0"
  506. while pc and pc != "0" and pc in by_code:
  507. if pc not in included:
  508. parent = by_code[pc]
  509. if is_visible_menu(parent):
  510. included[pc] = parent
  511. parent["_ancestor_only"] = not is_mes_menu(parent)
  512. else:
  513. break
  514. pc = by_code[pc].get("parent_code") or "0"
  515. tree_nodes = list(included.values())
  516. visible_codes = set(included.keys())
  517. children = defaultdict(list)
  518. roots = []
  519. for m in sorted(tree_nodes, key=sort_menu_key):
  520. parent = m.get("parent_code") or "0"
  521. if parent in visible_codes:
  522. children[parent].append(m)
  523. else:
  524. roots.append(m)
  525. for k in children:
  526. children[k] = sorted(children[k], key=sort_menu_key)
  527. roots = sorted(roots, key=sort_menu_key)
  528. return roots, children, by_code, visible_mes
  529. def render_tree(roots, children, by_code, indent=0):
  530. lines = []
  531. def rec(nodes, level):
  532. for n in nodes:
  533. pad = " " * level
  534. parent_name = ""
  535. p = by_code.get(n["parent_code"])
  536. if p:
  537. parent_name = p["menu_name"]
  538. href = n["menu_href"] or "-"
  539. perm = n["permission"] or "-"
  540. icon = n["menu_icon"] or "-"
  541. mark = " *(folder ancestor)*" if n.get("_ancestor_only") else ""
  542. lines.append(
  543. f"{pad}- **{n['menu_name']}**{mark} href=`{href}` permission=`{perm}` icon=`{icon}` parent=`{parent_name or n['parent_code']}`"
  544. )
  545. rec(children.get(n["menu_code"], []), level + 1)
  546. rec(roots, 0)
  547. return lines
  548. # ---------------------------------------------------------------------------
  549. # 5. Packages
  550. # ---------------------------------------------------------------------------
  551. def list_java(dirpath: Path):
  552. if not dirpath.exists():
  553. return []
  554. return sorted(p.name for p in dirpath.glob("*.java"))
  555. def list_packages():
  556. pkgs = {}
  557. if JAVA_MES.exists():
  558. for d in sorted(p for p in JAVA_MES.iterdir() if p.is_dir()):
  559. pkgs[d.name] = list_java(d)
  560. return pkgs
  561. # ---------------------------------------------------------------------------
  562. # Extra DDL tables not in dump
  563. # ---------------------------------------------------------------------------
  564. def extra_ddl_tables():
  565. extra = []
  566. ddl_dir = ROOT / "db" / "mysql"
  567. if not ddl_dir.exists():
  568. return extra
  569. for f in sorted(ddl_dir.glob("*.sql")):
  570. txt = read_text(f)
  571. for m in re.finditer(r"CREATE TABLE(?:\s+IF NOT EXISTS)?\s+`?(mes_[a-z0-9_]+)`?", txt, re.I):
  572. extra.append((m.group(1).lower(), f.name))
  573. for m in re.finditer(r"ALTER TABLE\s+`?(mes_[a-z0-9_]+)`?", txt, re.I):
  574. extra.append((m.group(1).lower() + " (ALTER)", f.name))
  575. return extra
  576. def parse_netty():
  577. ports = []
  578. notes = []
  579. if APP.exists():
  580. src = read_text(APP)
  581. for m in re.finditer(r"new\s+(NettyServer|LdNettyServer)\s*\(\s*(\d+)\s*\)", src):
  582. commented = False
  583. # check if this occurrence is inside a line comment or block
  584. line_start = src.rfind("\n", 0, m.start()) + 1
  585. line = src[line_start : src.find("\n", m.start())]
  586. if line.lstrip().startswith("//") or "/*" in src[max(0, m.start() - 80) : m.start()]:
  587. # crude: if // before on same line
  588. before = src[line_start : m.start()]
  589. if "//" in before:
  590. commented = True
  591. ports.append(
  592. {
  593. "class": m.group(1),
  594. "port": int(m.group(2)),
  595. "active": not commented,
  596. "line": line.strip(),
  597. }
  598. )
  599. if "new LdNettyServer(7890)" in src:
  600. # specifically noted even if commented
  601. pass
  602. return ports
  603. # ---------------------------------------------------------------------------
  604. # Markdown
  605. # ---------------------------------------------------------------------------
  606. def md_escape(s: str) -> str:
  607. return (s or "").replace("|", "\\|")
  608. def write_md(path: Path, data: dict):
  609. views_files, view_groups = data["views"]
  610. controllers = data["controllers"]
  611. entities = data["entities"]
  612. tables, menus = data["sql"]
  613. mes_menus = [m for m in menus if is_mes_menu(m)]
  614. roots, children, by_code, visible = build_menu_tree(menus, mes_menus)
  615. pkgs = data["packages"]
  616. netty = data["netty"]
  617. extra = data["extra_ddl"]
  618. utils = data["utils"]
  619. biz_methods = 0
  620. for c in controllers:
  621. biz_methods += sum(1 for m in c["methods"] if m["business"])
  622. with_table = [e for e in entities if e["has_table"]]
  623. without_table = [e for e in entities if not e["has_table"]]
  624. dump_names = {t["name"] for t in tables}
  625. extra_new = sorted({n for n, _ in extra if not n.endswith("(ALTER)") and n not in dump_names})
  626. lines = []
  627. a = lines.append
  628. a("# JeeSite MES Inventory (mescloud)")
  629. a("")
  630. a(f"- Source: `{ROOT}`")
  631. a(f"- Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
  632. a("- Purpose: rebuild this MES on RuoYi-Vue")
  633. a("")
  634. a("## Summary counts")
  635. a("")
  636. a("| Item | Count |")
  637. a("|---|---|")
  638. a(f"| Controllers (`mes.web`) | {len(controllers)} |")
  639. a(f"| Controller methods (mapped) | {sum(len(c['methods']) for c in controllers)} |")
  640. a(f"| BUSINESS/CLIENT API methods | {biz_methods} |")
  641. a(f"| Entity classes | {len(entities)} |")
  642. a(f"| Entities with `@Table` | {len(with_table)} |")
  643. a(f"| HTML views (modules/mes) | {len(views_files)} |")
  644. a(f"| List pages | {len(view_groups['List'])} |")
  645. a(f"| Form pages | {len(view_groups['Form'])} |")
  646. a(f"| Info pages | {len(view_groups['Info'])} |")
  647. a(f"| Screen pages (`mesScreen*.html` dashboards) | {len(view_groups['Screen'])} |")
  648. a(f"| Other HTML | {len(view_groups['other'])} |")
  649. a(f"| `mes_` tables in SQL dump | {len(tables)} |")
  650. a(f"| MES-related `js_sys_menu` rows | {len(mes_menus)} |")
  651. a(f"| Visible MES menus (`menu_type=1`, `is_show=1`) | {len(visible)} |")
  652. a(f"| DAO classes | {len(pkgs.get('dao', []))} |")
  653. a(f"| Service classes | {len(pkgs.get('service', []))} |")
  654. a(f"| req DTOs | {len(pkgs.get('req', []))} |")
  655. a(f"| resp DTOs | {len(pkgs.get('resp', []))} |")
  656. a(f"| mes.util | {len(pkgs.get('util', []))} |")
  657. a(f"| com.jeesite.modules.utils (Netty etc.) | {len(utils)} |")
  658. a("")
  659. a("### Netty ports (`Application.java`)")
  660. a("")
  661. if netty:
  662. for n in netty:
  663. state = "ACTIVE" if n["active"] else "COMMENTED"
  664. a(f"- `{n['class']}({n['port']})` — **{state}** — `{n['line']}`")
  665. else:
  666. a("- (none parsed)")
  667. a("")
  668. a("- Active TCP: **NettyServer on port 3000** (client communications).")
  669. a("- Commented: `LdNettyServer(7890)` (laser engraving / 镭雕机 TCP).")
  670. a("")
  671. # ------------------------------------------------------------------
  672. a("## 1. HTML views (`src/main/resources/views/modules/mes/`)")
  673. a("")
  674. a(f"Total: **{len(views_files)}**. Grouped by filename convention (Screen = `mesScreen.html` / `mesScreenN.html` only; `mesScreenPlan*` goes to List/Form).")
  675. a("")
  676. for g in ("List", "Form", "Info", "Screen", "other"):
  677. a(f"### 1.{['List','Form','Info','Screen','other'].index(g)+1} {g} ({len(view_groups[g])})")
  678. a("")
  679. for rel in view_groups[g]:
  680. a(f"- `{rel}`")
  681. a("")
  682. # ------------------------------------------------------------------
  683. a("## 2. Controllers (`com.jeesite.modules.mes.web`)")
  684. a("")
  685. a("Standard CRUD method names: `list`, `listData`, `form`, `save`, `delete`, `disable`, `enable`.")
  686. a("Anything else is flagged **BUSINESS/CLIENT API**.")
  687. a("")
  688. a(f"Total controllers: **{len(controllers)}**.")
  689. a("")
  690. for c in controllers:
  691. a(f"### `{c['class']}`")
  692. a("")
  693. a(f"- File: `{c['file']}`")
  694. a(f"- Class `@RequestMapping`: `{c['path'] or '(none)'}`")
  695. a(f"- Methods: {len(c['methods'])}")
  696. a("")
  697. if c["methods"]:
  698. a("| Method | HTTP | Mapping | Flag |")
  699. a("|---|---|---|---|")
  700. for m in c["methods"]:
  701. flag = "BUSINESS/CLIENT API" if m["business"] else "CRUD"
  702. a(f"| `{m['name']}` | {m['http']} | `{md_escape(m['path'])}` | {flag} |")
  703. a("")
  704. else:
  705. a("_No mapped public methods found._")
  706. a("")
  707. a("### 2.x BUSINESS/CLIENT API index")
  708. a("")
  709. a("| Controller | Method | Mapping |")
  710. a("|---|---|---|")
  711. for c in controllers:
  712. for m in c["methods"]:
  713. if m["business"]:
  714. a(f"| `{c['class']}` | `{m['name']}` | `{md_escape(m['path'])}` |")
  715. a("")
  716. # ------------------------------------------------------------------
  717. a("## 3. Entities (`com.jeesite.modules.mes.entity`)")
  718. a("")
  719. a(f"Total classes: **{len(entities)}**. With `@Table`: **{len(with_table)}**. Without `@Table` (DTO/export): **{len(without_table)}**.")
  720. a("")
  721. if without_table:
  722. a("### 3.0 Entities without `@Table`")
  723. a("")
  724. for e in without_table:
  725. a(f"- `{e['class']}` (`{e['file']}`)")
  726. a("")
  727. for e in with_table:
  728. a(f"### `{e['class']}`")
  729. a("")
  730. a(f"- Table: `{e['table'] or '?'}`")
  731. a(f"- Label: {e['label'] or '-'}")
  732. a(f"- Alias: `{e['alias'] or '-'}`")
  733. a("")
  734. if e["columns"]:
  735. a("| Column | attrName | label |")
  736. a("|---|---|---|")
  737. for col in e["columns"]:
  738. a(f"| `{col['name']}` | `{col['attrName']}` | {md_escape(col['label'])} |")
  739. a("")
  740. else:
  741. a("_No `@Column` parsed._")
  742. a("")
  743. if e["joins"]:
  744. a("Join tables:")
  745. a("")
  746. for j in e["joins"]:
  747. a(f"- type=`{j['type']}` entity=`{j['entity']}` alias=`{j['alias']}` on=`{j['on']}`")
  748. if j["columns"]:
  749. a(" | Column | attrName | label |")
  750. a(" |---|---|---|")
  751. for col in j["columns"]:
  752. a(f" | `{col['name']}` | `{col['attrName']}` | {md_escape(col['label'])} |")
  753. a("")
  754. # ------------------------------------------------------------------
  755. a("## 4. MES menus (`js_sys_menu`)")
  756. a("")
  757. a("Filter: `module_codes` contains `mes`, **or** `menu_href` contains `/mes/`, **or** `permission` contains `mes:`.")
  758. a("Top-level folders (生产管理, 设备管理, 系统管理, …) often have `module_codes=app`/`core` and empty href; they are pulled into the tree as **folder ancestors** so nesting is preserved.")
  759. a("Also merged incremental `INSERT INTO js_sys_menu` from `db/mysql/*.sql` (except the old 2023 dump).")
  760. a("")
  761. a(f"- All menu INSERT rows parsed: **{len(menus)}**")
  762. a(f"- MES-related rows: **{len(mes_menus)}**")
  763. a(f"- Visible MES menus (`menu_type=1` AND `is_show=1`): **{len(visible)}**")
  764. a("")
  765. a("### 4.1 Visible menu tree")
  766. a("")
  767. tree_lines = render_tree(roots, children, by_code)
  768. if tree_lines:
  769. lines.extend(tree_lines)
  770. else:
  771. a("_No visible MES menus._")
  772. a("")
  773. a("### 4.2 All MES-related menu rows")
  774. a("")
  775. a("| menu_code | parent | name | type | href | permission | icon | is_show | module_codes | source |")
  776. a("|---|---|---|---|---|---|---|---|---|---|")
  777. for m in sorted(mes_menus, key=sort_menu_key):
  778. a(
  779. "| `{code}` | `{parent}` | {name} | {typ} | `{href}` | `{perm}` | `{icon}` | {show} | `{mod}` | {src} |".format(
  780. code=md_escape(m["menu_code"]),
  781. parent=md_escape(m["parent_code"]),
  782. name=md_escape(m["menu_name"]),
  783. typ=m["menu_type"],
  784. href=md_escape(m["menu_href"] or ""),
  785. perm=md_escape(m["permission"] or ""),
  786. icon=md_escape(m["menu_icon"] or ""),
  787. show=m["is_show"],
  788. mod=md_escape(m["module_codes"] or ""),
  789. src=md_escape(m.get("_source") or ""),
  790. )
  791. )
  792. a("")
  793. # ------------------------------------------------------------------
  794. a("## 5. Packages")
  795. a("")
  796. a("### 5.1 `com.jeesite.modules.mes` subpackages")
  797. a("")
  798. for name, files in pkgs.items():
  799. a(f"#### `{name}/` ({len(files)})")
  800. a("")
  801. for fn in files:
  802. a(f"- `{fn}`")
  803. a("")
  804. a("### 5.2 `com.jeesite.modules.utils` (Netty / protocol)")
  805. a("")
  806. a(f"Count: **{len(utils)}**")
  807. a("")
  808. for fn in utils:
  809. a(f"- `{fn}`")
  810. a("")
  811. # ------------------------------------------------------------------
  812. a("## 6. `mes_` tables (`CREATE TABLE` in SQL dump)")
  813. a("")
  814. a(f"Count: **{len(tables)}**")
  815. a("")
  816. a("| Table | Comment |")
  817. a("|---|---|")
  818. for t in tables:
  819. a(f"| `{t['name']}` | {md_escape(t['comment'] or '')} |")
  820. a("")
  821. a("### 6.1 Extra DDL under `db/mysql/` (not necessarily in dump)")
  822. a("")
  823. if extra:
  824. for name, src in extra:
  825. a(f"- `{name}` — `{src}`")
  826. else:
  827. a("- (none)")
  828. a("")
  829. if extra_new:
  830. a("Tables created in incremental DDL but **absent from dump**:")
  831. a("")
  832. for n in extra_new:
  833. a(f"- `{n}`")
  834. a("")
  835. # ------------------------------------------------------------------
  836. a("## 7. Screen pages and Netty")
  837. a("")
  838. a("### 7.1 Screen HTML (`mesScreen*.html` dashboards)")
  839. a("")
  840. for rel in view_groups["Screen"]:
  841. a(f"- `{rel}`")
  842. a("")
  843. a("Related (not dashboards): `mesScreenPlanList.html`, `mesScreenPlanForm.html` (plan CRUD).")
  844. a("")
  845. a("### 7.2 Netty")
  846. a("")
  847. a("| Server class | Port | Status | Source |")
  848. a("|---|---|---|---|")
  849. a("| `com.jeesite.modules.utils.NettyServer` | 3000 | **ACTIVE** | `Application.run()` starts `new NettyServer(3000).run()` in a thread |")
  850. a("| `com.jeesite.modules.utils.LdNettyServer` | 7890 | commented out | `Application.java` (镭雕机 TCP) |")
  851. a("")
  852. a("HTTP server port is configured separately (`BaseConfig.server_port` default `8980` / `server.port`).")
  853. a("")
  854. a("---")
  855. a("")
  856. a("_End of inventory._")
  857. path.parent.mkdir(parents=True, exist_ok=True)
  858. path.write_text("\n".join(lines) + "\n", encoding="utf-8")
  859. return {
  860. "controllers": len(controllers),
  861. "entities": len(entities),
  862. "list": len(view_groups["List"]),
  863. "form": len(view_groups["Form"]),
  864. "info": len(view_groups["Info"]),
  865. "screen": len(view_groups["Screen"]),
  866. "other_html": len(view_groups["other"]),
  867. "views": len(views_files),
  868. "tables": len(tables),
  869. "menus_all_mes": len(mes_menus),
  870. "menus_visible": len(visible),
  871. "biz": biz_methods,
  872. "mapped_methods": sum(len(c["methods"]) for c in controllers),
  873. "dao": len(pkgs.get("dao", [])),
  874. "service": len(pkgs.get("service", [])),
  875. "bytes": path.stat().st_size,
  876. "path": str(path),
  877. }
  878. def main():
  879. print("Collecting views...", flush=True)
  880. views = collect_views()
  881. print(f" views={len(views[0])}", flush=True)
  882. print("Collecting controllers...", flush=True)
  883. controllers = collect_controllers()
  884. print(f" controllers={len(controllers)}", flush=True)
  885. print("Collecting entities...", flush=True)
  886. entities = collect_entities()
  887. print(f" entities={len(entities)}", flush=True)
  888. print("Parsing SQL dump...", flush=True)
  889. tables, menus = collect_sql()
  890. extra_menus = collect_incremental_menus({m["menu_code"] for m in menus})
  891. menus.extend(extra_menus)
  892. print(f" tables={len(tables)} menus={len(menus)} extra_menus={len(extra_menus)}", flush=True)
  893. data = {
  894. "views": views,
  895. "controllers": controllers,
  896. "entities": entities,
  897. "sql": (tables, menus),
  898. "packages": list_packages(),
  899. "utils": list_java(JAVA_UTILS),
  900. "netty": parse_netty(),
  901. "extra_ddl": extra_ddl_tables(),
  902. }
  903. targets = [
  904. Path(r"D:\mes\_migrate\INVENTORY.md"),
  905. Path(r"D:\IdeaProjects\mes-199-0Y7A\mescloud\_migrate_inventory.md"),
  906. ]
  907. stats = None
  908. for t in targets:
  909. try:
  910. stats = write_md(t, data)
  911. print(f"Wrote {t} ({stats['bytes']} bytes)", flush=True)
  912. except Exception as e:
  913. print(f"FAILED writing {t}: {e}", flush=True)
  914. print("STATS", stats)
  915. return 0
  916. if __name__ == "__main__":
  917. sys.exit(main())