# -*- coding: utf-8 -*- """Generate a complete JeeSite MES inventory for RuoYi-Vue migration.""" from __future__ import annotations import os import re import sys from collections import defaultdict from datetime import datetime from pathlib import Path ROOT = Path(r"D:\IdeaProjects\mes-199-0Y7A\mescloud") JAVA_MES = ROOT / "src/main/java/com/jeesite/modules/mes" JAVA_UTILS = ROOT / "src/main/java/com/jeesite/modules/utils" VIEWS = ROOT / "src/main/resources/views/modules/mes" WEB = JAVA_MES / "web" ENTITY = JAVA_MES / "entity" SQL = ROOT / "mes_cloud_199_0y7a.sql" APP = ROOT / "src/main/java/com/jeesite/modules/Application.java" STANDARD_CRUD = {"list", "listData", "form", "save", "delete", "disable", "enable"} def read_text(p: Path) -> str: return p.read_text(encoding="utf-8", errors="replace") def match_parens(s: str, start: int) -> int: """start points at '('; return index after matching ')'.""" depth = 0 i = start in_str = False quote = "" while i < len(s): c = s[i] if in_str: if c == "\\" and i + 1 < len(s): i += 2 continue if c == quote: in_str = False i += 1 continue if c in ('"', "'"): in_str = True quote = c i += 1 continue if c == "(": depth += 1 elif c == ")": depth -= 1 if depth == 0: return i + 1 i += 1 return -1 def match_braces(s: str, start: int) -> int: depth = 0 i = start in_str = False quote = "" while i < len(s): c = s[i] if in_str: if c == "\\" and i + 1 < len(s): i += 2 continue if c == quote: in_str = False i += 1 continue if c in ('"', "'"): in_str = True quote = c i += 1 continue if c == "{": depth += 1 elif c == "}": depth -= 1 if depth == 0: return i + 1 i += 1 return -1 def attr_str(block: str, key: str) -> str | None: m = re.search(rf'{key}\s*=\s*"([^"]*)"', block) return m.group(1) if m else None def attr_ident(block: str, key: str) -> str | None: m = re.search(rf"{key}\s*=\s*([A-Za-z0-9_.]+)", block) return m.group(1) if m else None def parse_mapping_value(ann: str) -> str: """Parse Spring mapping annotation body into a display string.""" body = ann.strip() if body.startswith("(") and body.endswith(")"): body = body[1:-1].strip() if not body: return "" m = re.search(r"value\s*=\s*", body) if m: rest = body[m.end() :].lstrip() if rest.startswith("{"): end = match_braces(rest, 0) inner = rest[1 : end - 1] parts = re.findall(r'"([^"]*)"', inner) return "{" + ", ".join(f'"{p}"' for p in parts) + "}" m2 = re.match(r'"([^"]*)"', rest) if m2: return m2.group(1) return rest.split(",")[0].strip() m2 = re.match(r'"([^"]*)"', body) if m2: return m2.group(1) if body.startswith("{"): end = match_braces(body, 0) inner = body[1 : end - 1] parts = re.findall(r'"([^"]*)"', inner) return "{" + ", ".join(f'"{p}"' for p in parts) + "}" return body.split(",")[0].strip() def mapping_paths(val: str) -> list[str]: if not val: return [""] if val.startswith("{") and val.endswith("}"): return re.findall(r'"([^"]*)"', val) or [""] return [val] # --------------------------------------------------------------------------- # 1. Views # --------------------------------------------------------------------------- def collect_views(): groups = {"List": [], "Form": [], "Info": [], "Screen": [], "other": []} files = sorted(VIEWS.rglob("*.html")) for f in files: rel = f.relative_to(VIEWS).as_posix() name = f.name # Screen dashboards: mesScreen.html / mesScreenN.html (not mesScreenPlan*) if re.fullmatch(r"mesScreen(\d*)\.html", name, re.I): groups["Screen"].append(rel) elif "List" in name: groups["List"].append(rel) elif "Form" in name: groups["Form"].append(rel) elif "Info" in name: groups["Info"].append(rel) else: groups["other"].append(rel) return files, groups # --------------------------------------------------------------------------- # 2. Controllers # --------------------------------------------------------------------------- ANN_RE = re.compile( r"@(RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)\s*(\([^;{]*?\)|(?=\s))", re.S, ) def strip_java_comments(src: str) -> str: src = re.sub(r"/\*.*?\*/", lambda m: "\n" * m.group(0).count("\n"), src, flags=re.S) src = re.sub(r"//.*?$", "", src, flags=re.M) return src def parse_controller(path: Path) -> dict: raw = read_text(path) src = strip_java_comments(raw) class_m = re.search(r"public\s+class\s+(\w+)", src) if not class_m: return None cls = class_m.group(1) class_annos = src[: class_m.start()] class_path = "" for m in re.finditer(r"@RequestMapping\s*(\([^)]*(?:\([^)]*\)[^)]*)*\)|)", class_annos): ann = m.group(1) or "" # handle nested parens better start = m.start() if src[m.end() - 1 if m.group(1) else m.end() :]: pass at = src.find("@RequestMapping", 0) # more reliable: find last @RequestMapping before class last = None for m in re.finditer(r"@RequestMapping", class_annos): last = m if last: i = last.end() while i < len(src) and src[i].isspace(): i += 1 if i < len(src) and src[i] == "(": end = match_parens(src, i) class_path = parse_mapping_value(src[i:end]) else: class_path = "" methods = [] # Find mapping annotations followed eventually by a public method i = class_m.end() while True: m = re.search( r"@(RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping|PatchMapping)\b", src[i:], ) if not m: break abs_start = i + m.start() kind = m.group(1) j = i + m.end() while j < len(src) and src[j].isspace(): j += 1 if j < len(src) and src[j] == "(": end = match_parens(src, j) val = parse_mapping_value(src[j:end]) after = end else: val = "" after = j # look ahead for public method (skip other annotations) ahead = src[after : after + 800] mm = re.search( r"public\s+(?:static\s+)?(?:[\w.<>,\s?\[\]]+)\s+(\w+)\s*\(", ahead, ) if mm: name = mm.group(1) # skip constructors if name != cls: flag = name not in STANDARD_CRUD methods.append( { "name": name, "http": kind, "path": val, "business": flag, } ) i = after return {"class": cls, "path": class_path, "file": path.name, "methods": methods} def collect_controllers(): out = [] for f in sorted(WEB.glob("*.java")): c = parse_controller(f) if c: out.append(c) return out # --------------------------------------------------------------------------- # 3. Entities # --------------------------------------------------------------------------- def split_top_level_annos(block: str, prefix: str) -> list[str]: """Split `@Xxx(...)` occurrences at top level of a `{...}` array.""" items = [] i = 0 token = "@" + prefix while True: j = block.find(token, i) if j < 0: break k = j + len(token) while k < len(block) and block[k].isspace(): k += 1 if k < len(block) and block[k] == "(": end = match_parens(block, k) items.append(block[j:end]) i = end else: i = k return items def parse_column_ann(ann: str) -> dict: body = ann[ann.find("(") :] if "(" in ann else "" return { "name": attr_str(body, "name") or "", "attrName": attr_str(body, "attrName") or "", "label": attr_str(body, "label") or "", } def parse_join_ann(ann: str) -> dict: body = ann[ann.find("(") :] if "(" in ann else ann entity = attr_ident(body, "entity") or "" alias = attr_str(body, "alias") or "" on = attr_str(body, "on") or "" typ = (attr_ident(body, "type") or "").replace("Type.", "").replace("JoinTable.", "") cols = [] cm = re.search(r"columns\s*=\s*\{", body) if cm: end = match_braces(body, cm.end() - 1) inner = body[cm.end() - 1 : end] for c in split_top_level_annos(inner, "Column"): cols.append(parse_column_ann(c)) return { "type": typ.replace("Type.", ""), "entity": entity.replace(".class", ""), "alias": alias, "on": on, "columns": cols, } def parse_entity(path: Path) -> dict: src = read_text(path) class_m = re.search(r"public\s+class\s+(\w+)", src) cls = class_m.group(1) if class_m else path.stem rec = { "class": cls, "file": path.name, "table": None, "label": None, "alias": None, "columns": [], "joins": [], "has_table": False, } tpos = src.find("@Table") if tpos < 0: return rec i = tpos + len("@Table") while i < len(src) and src[i].isspace(): i += 1 if i >= len(src) or src[i] != "(": return rec end = match_parens(src, i) body = src[i:end] rec["has_table"] = True rec["table"] = attr_str(body, "name") rec["label"] = attr_str(body, "label") rec["alias"] = attr_str(body, "alias") # columns at Table level: first columns={ that is not nested in joinTable # Extract joinTable first, then columns outside it join_m = re.search(r"joinTable\s*=\s*\{", body) join_span = None if join_m: jend = match_braces(body, join_m.end() - 1) join_span = (join_m.start(), jend) inner = body[join_m.end() - 1 : jend] for j in split_top_level_annos(inner, "JoinTable"): rec["joins"].append(parse_join_ann(j)) # find columns= that is NOT inside join_span for cm in re.finditer(r"columns\s*=\s*\{", body): if join_span and join_span[0] <= cm.start() < join_span[1]: continue cend = match_braces(body, cm.end() - 1) inner = body[cm.end() - 1 : cend] for c in split_top_level_annos(inner, "Column"): rec["columns"].append(parse_column_ann(c)) break return rec def collect_entities(): out = [] for f in sorted(ENTITY.glob("*.java")): out.append(parse_entity(f)) return out # --------------------------------------------------------------------------- # 4. SQL menus + tables # --------------------------------------------------------------------------- def split_sql_values(s: str) -> list[str]: vals = [] i = 0 n = len(s) while i < n: while i < n and s[i] in " \t\r\n": i += 1 if i >= n: break if s[i] == "'": i += 1 buf = [] while i < n: if s[i] == "'" and i + 1 < n and s[i + 1] == "'": buf.append("'") i += 2 continue if s[i] == "'": i += 1 break buf.append(s[i]) i += 1 vals.append("".join(buf)) while i < n and s[i] in " \t\r\n": i += 1 if i < n and s[i] == ",": i += 1 continue # unquoted j = i while j < n and s[j] != ",": j += 1 token = s[i:j].strip() vals.append(None if token.upper() == "NULL" else token) i = j + 1 if j < n and s[j] == "," else j return vals def collect_sql(): tables = [] menus = [] if not SQL.exists(): return tables, menus # Stream line by line — dump can be large with SQL.open(encoding="utf-8", errors="replace") as fh: for line in fh: if line.startswith("CREATE TABLE `mes_"): m = re.match(r"CREATE TABLE `(mes_[^`]+)`", line) if m: tables.append({"name": m.group(1), "comment": None}) continue if tables and tables[-1]["comment"] is None: cm = re.search(r"COMMENT\s*=\s*'([^']*)'", line) if cm: tables[-1]["comment"] = cm.group(1) if line.startswith(")") or line.startswith("CREATE TABLE") or line.startswith("INSERT"): if tables[-1]["comment"] is None: tables[-1]["comment"] = "" if "INSERT INTO `js_sys_menu`" in line: m = re.search(r"VALUES\s*\((.*)\)\s*;?\s*$", line) if not m: continue rec = parse_menu_values_tuple(m.group(1)) if rec: rec["_source"] = "mes_cloud_199_0y7a.sql" menus.append(rec) return tables, menus def parse_menu_values_tuple(inner: str) -> dict | None: vals = split_sql_values(inner.strip().rstrip(";").strip()) if len(vals) < 20: return None return { "menu_code": vals[0] or "", "parent_code": vals[1] or "", "parent_codes": vals[2] or "", "tree_sort": vals[3], "tree_leaf": vals[5], "tree_level": vals[6], "tree_names": vals[7] or "", "menu_name": vals[8] or "", "menu_type": str(vals[9] or "").strip("'"), "menu_href": vals[10] or "", "menu_target": vals[11] or "", "menu_icon": vals[12] or "", "menu_color": vals[13] or "", "menu_title": vals[14] or "", "permission": vals[15] or "", "weight": vals[16], "is_show": str(vals[17] or "").strip("'"), "sys_code": vals[18] or "", "module_codes": vals[19] or "", "status": vals[22] if len(vals) > 22 else "", "_source": "dump", } def collect_incremental_menus(existing_codes: set[str]): extra = [] ddl_dir = ROOT / "db" / "mysql" if not ddl_dir.exists(): return extra skip = {"mes_cloud-2023-6-10.sql"} for f in sorted(ddl_dir.glob("*.sql")): if f.name in skip: continue txt = read_text(f) for m in re.finditer( r"INSERT INTO `js_sys_menu`(?:\s*\([^)]+\))?\s*VALUES\s*", txt, re.I, ): i = m.end() while i < len(txt) and txt[i].isspace(): i += 1 while i < len(txt) and txt[i] == "(": end = match_parens(txt, i) rec = parse_menu_values_tuple(txt[i + 1 : end - 1]) if rec and rec["menu_code"] not in existing_codes: rec["_source"] = f.name extra.append(rec) existing_codes.add(rec["menu_code"]) i = end while i < len(txt) and txt[i] in " \t\r\n": i += 1 if i < len(txt) and txt[i] == ",": i += 1 while i < len(txt) and txt[i].isspace(): i += 1 else: break for m in re.finditer( r"INSERT INTO `js_sys_menu`\s*SELECT\s+", txt, re.I, ): rest = txt[m.end() :] fm = re.search(r"\bFROM\b", rest, re.I) if not fm: continue rec = parse_menu_values_tuple(rest[: fm.start()]) if rec and rec["menu_code"] not in existing_codes: rec["_source"] = f.name extra.append(rec) existing_codes.add(rec["menu_code"]) return extra def is_mes_menu(m: dict) -> bool: mc = (m.get("module_codes") or "").lower() href = (m.get("menu_href") or "").lower() perm = (m.get("permission") or "").lower() parts = [p.strip() for p in mc.replace(";", ",").split(",") if p.strip()] if "mes" in parts: return True if "/mes/" in href or href.startswith("mes/") or href.startswith("/mes"): return True if "mes:" in perm: return True return False def is_visible_menu(m: dict) -> bool: return str(m.get("menu_type")) == "1" and str(m.get("is_show")) == "1" def sort_menu_key(m: dict): try: ts = float(m.get("tree_sort") or 0) except (TypeError, ValueError): ts = 0 try: lv = float(m.get("tree_level") or 0) except (TypeError, ValueError): lv = 0 return (lv, ts, m.get("menu_name") or "") def build_menu_tree(all_menus, mes_menus): """Nest visible MES menus under ancestor folders (folders often use module_codes=app).""" by_code = {m["menu_code"]: m for m in all_menus} visible_mes = [m for m in mes_menus if is_visible_menu(m)] included = {m["menu_code"]: m for m in visible_mes} for m in list(visible_mes): pc = m.get("parent_code") or "0" while pc and pc != "0" and pc in by_code: if pc not in included: parent = by_code[pc] if is_visible_menu(parent): included[pc] = parent parent["_ancestor_only"] = not is_mes_menu(parent) else: break pc = by_code[pc].get("parent_code") or "0" tree_nodes = list(included.values()) visible_codes = set(included.keys()) children = defaultdict(list) roots = [] for m in sorted(tree_nodes, key=sort_menu_key): parent = m.get("parent_code") or "0" if parent in visible_codes: children[parent].append(m) else: roots.append(m) for k in children: children[k] = sorted(children[k], key=sort_menu_key) roots = sorted(roots, key=sort_menu_key) return roots, children, by_code, visible_mes def render_tree(roots, children, by_code, indent=0): lines = [] def rec(nodes, level): for n in nodes: pad = " " * level parent_name = "" p = by_code.get(n["parent_code"]) if p: parent_name = p["menu_name"] href = n["menu_href"] or "-" perm = n["permission"] or "-" icon = n["menu_icon"] or "-" mark = " *(folder ancestor)*" if n.get("_ancestor_only") else "" lines.append( f"{pad}- **{n['menu_name']}**{mark} href=`{href}` permission=`{perm}` icon=`{icon}` parent=`{parent_name or n['parent_code']}`" ) rec(children.get(n["menu_code"], []), level + 1) rec(roots, 0) return lines # --------------------------------------------------------------------------- # 5. Packages # --------------------------------------------------------------------------- def list_java(dirpath: Path): if not dirpath.exists(): return [] return sorted(p.name for p in dirpath.glob("*.java")) def list_packages(): pkgs = {} if JAVA_MES.exists(): for d in sorted(p for p in JAVA_MES.iterdir() if p.is_dir()): pkgs[d.name] = list_java(d) return pkgs # --------------------------------------------------------------------------- # Extra DDL tables not in dump # --------------------------------------------------------------------------- def extra_ddl_tables(): extra = [] ddl_dir = ROOT / "db" / "mysql" if not ddl_dir.exists(): return extra for f in sorted(ddl_dir.glob("*.sql")): txt = read_text(f) for m in re.finditer(r"CREATE TABLE(?:\s+IF NOT EXISTS)?\s+`?(mes_[a-z0-9_]+)`?", txt, re.I): extra.append((m.group(1).lower(), f.name)) for m in re.finditer(r"ALTER TABLE\s+`?(mes_[a-z0-9_]+)`?", txt, re.I): extra.append((m.group(1).lower() + " (ALTER)", f.name)) return extra def parse_netty(): ports = [] notes = [] if APP.exists(): src = read_text(APP) for m in re.finditer(r"new\s+(NettyServer|LdNettyServer)\s*\(\s*(\d+)\s*\)", src): commented = False # check if this occurrence is inside a line comment or block line_start = src.rfind("\n", 0, m.start()) + 1 line = src[line_start : src.find("\n", m.start())] if line.lstrip().startswith("//") or "/*" in src[max(0, m.start() - 80) : m.start()]: # crude: if // before on same line before = src[line_start : m.start()] if "//" in before: commented = True ports.append( { "class": m.group(1), "port": int(m.group(2)), "active": not commented, "line": line.strip(), } ) if "new LdNettyServer(7890)" in src: # specifically noted even if commented pass return ports # --------------------------------------------------------------------------- # Markdown # --------------------------------------------------------------------------- def md_escape(s: str) -> str: return (s or "").replace("|", "\\|") def write_md(path: Path, data: dict): views_files, view_groups = data["views"] controllers = data["controllers"] entities = data["entities"] tables, menus = data["sql"] mes_menus = [m for m in menus if is_mes_menu(m)] roots, children, by_code, visible = build_menu_tree(menus, mes_menus) pkgs = data["packages"] netty = data["netty"] extra = data["extra_ddl"] utils = data["utils"] biz_methods = 0 for c in controllers: biz_methods += sum(1 for m in c["methods"] if m["business"]) with_table = [e for e in entities if e["has_table"]] without_table = [e for e in entities if not e["has_table"]] dump_names = {t["name"] for t in tables} extra_new = sorted({n for n, _ in extra if not n.endswith("(ALTER)") and n not in dump_names}) lines = [] a = lines.append a("# JeeSite MES Inventory (mescloud)") a("") a(f"- Source: `{ROOT}`") a(f"- Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") a("- Purpose: rebuild this MES on RuoYi-Vue") a("") a("## Summary counts") a("") a("| Item | Count |") a("|---|---|") a(f"| Controllers (`mes.web`) | {len(controllers)} |") a(f"| Controller methods (mapped) | {sum(len(c['methods']) for c in controllers)} |") a(f"| BUSINESS/CLIENT API methods | {biz_methods} |") a(f"| Entity classes | {len(entities)} |") a(f"| Entities with `@Table` | {len(with_table)} |") a(f"| HTML views (modules/mes) | {len(views_files)} |") a(f"| List pages | {len(view_groups['List'])} |") a(f"| Form pages | {len(view_groups['Form'])} |") a(f"| Info pages | {len(view_groups['Info'])} |") a(f"| Screen pages (`mesScreen*.html` dashboards) | {len(view_groups['Screen'])} |") a(f"| Other HTML | {len(view_groups['other'])} |") a(f"| `mes_` tables in SQL dump | {len(tables)} |") a(f"| MES-related `js_sys_menu` rows | {len(mes_menus)} |") a(f"| Visible MES menus (`menu_type=1`, `is_show=1`) | {len(visible)} |") a(f"| DAO classes | {len(pkgs.get('dao', []))} |") a(f"| Service classes | {len(pkgs.get('service', []))} |") a(f"| req DTOs | {len(pkgs.get('req', []))} |") a(f"| resp DTOs | {len(pkgs.get('resp', []))} |") a(f"| mes.util | {len(pkgs.get('util', []))} |") a(f"| com.jeesite.modules.utils (Netty etc.) | {len(utils)} |") a("") a("### Netty ports (`Application.java`)") a("") if netty: for n in netty: state = "ACTIVE" if n["active"] else "COMMENTED" a(f"- `{n['class']}({n['port']})` — **{state}** — `{n['line']}`") else: a("- (none parsed)") a("") a("- Active TCP: **NettyServer on port 3000** (client communications).") a("- Commented: `LdNettyServer(7890)` (laser engraving / 镭雕机 TCP).") a("") # ------------------------------------------------------------------ a("## 1. HTML views (`src/main/resources/views/modules/mes/`)") a("") a(f"Total: **{len(views_files)}**. Grouped by filename convention (Screen = `mesScreen.html` / `mesScreenN.html` only; `mesScreenPlan*` goes to List/Form).") a("") for g in ("List", "Form", "Info", "Screen", "other"): a(f"### 1.{['List','Form','Info','Screen','other'].index(g)+1} {g} ({len(view_groups[g])})") a("") for rel in view_groups[g]: a(f"- `{rel}`") a("") # ------------------------------------------------------------------ a("## 2. Controllers (`com.jeesite.modules.mes.web`)") a("") a("Standard CRUD method names: `list`, `listData`, `form`, `save`, `delete`, `disable`, `enable`.") a("Anything else is flagged **BUSINESS/CLIENT API**.") a("") a(f"Total controllers: **{len(controllers)}**.") a("") for c in controllers: a(f"### `{c['class']}`") a("") a(f"- File: `{c['file']}`") a(f"- Class `@RequestMapping`: `{c['path'] or '(none)'}`") a(f"- Methods: {len(c['methods'])}") a("") if c["methods"]: a("| Method | HTTP | Mapping | Flag |") a("|---|---|---|---|") for m in c["methods"]: flag = "BUSINESS/CLIENT API" if m["business"] else "CRUD" a(f"| `{m['name']}` | {m['http']} | `{md_escape(m['path'])}` | {flag} |") a("") else: a("_No mapped public methods found._") a("") a("### 2.x BUSINESS/CLIENT API index") a("") a("| Controller | Method | Mapping |") a("|---|---|---|") for c in controllers: for m in c["methods"]: if m["business"]: a(f"| `{c['class']}` | `{m['name']}` | `{md_escape(m['path'])}` |") a("") # ------------------------------------------------------------------ a("## 3. Entities (`com.jeesite.modules.mes.entity`)") a("") a(f"Total classes: **{len(entities)}**. With `@Table`: **{len(with_table)}**. Without `@Table` (DTO/export): **{len(without_table)}**.") a("") if without_table: a("### 3.0 Entities without `@Table`") a("") for e in without_table: a(f"- `{e['class']}` (`{e['file']}`)") a("") for e in with_table: a(f"### `{e['class']}`") a("") a(f"- Table: `{e['table'] or '?'}`") a(f"- Label: {e['label'] or '-'}") a(f"- Alias: `{e['alias'] or '-'}`") a("") if e["columns"]: a("| Column | attrName | label |") a("|---|---|---|") for col in e["columns"]: a(f"| `{col['name']}` | `{col['attrName']}` | {md_escape(col['label'])} |") a("") else: a("_No `@Column` parsed._") a("") if e["joins"]: a("Join tables:") a("") for j in e["joins"]: a(f"- type=`{j['type']}` entity=`{j['entity']}` alias=`{j['alias']}` on=`{j['on']}`") if j["columns"]: a(" | Column | attrName | label |") a(" |---|---|---|") for col in j["columns"]: a(f" | `{col['name']}` | `{col['attrName']}` | {md_escape(col['label'])} |") a("") # ------------------------------------------------------------------ a("## 4. MES menus (`js_sys_menu`)") a("") a("Filter: `module_codes` contains `mes`, **or** `menu_href` contains `/mes/`, **or** `permission` contains `mes:`.") 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.") a("Also merged incremental `INSERT INTO js_sys_menu` from `db/mysql/*.sql` (except the old 2023 dump).") a("") a(f"- All menu INSERT rows parsed: **{len(menus)}**") a(f"- MES-related rows: **{len(mes_menus)}**") a(f"- Visible MES menus (`menu_type=1` AND `is_show=1`): **{len(visible)}**") a("") a("### 4.1 Visible menu tree") a("") tree_lines = render_tree(roots, children, by_code) if tree_lines: lines.extend(tree_lines) else: a("_No visible MES menus._") a("") a("### 4.2 All MES-related menu rows") a("") a("| menu_code | parent | name | type | href | permission | icon | is_show | module_codes | source |") a("|---|---|---|---|---|---|---|---|---|---|") for m in sorted(mes_menus, key=sort_menu_key): a( "| `{code}` | `{parent}` | {name} | {typ} | `{href}` | `{perm}` | `{icon}` | {show} | `{mod}` | {src} |".format( code=md_escape(m["menu_code"]), parent=md_escape(m["parent_code"]), name=md_escape(m["menu_name"]), typ=m["menu_type"], href=md_escape(m["menu_href"] or ""), perm=md_escape(m["permission"] or ""), icon=md_escape(m["menu_icon"] or ""), show=m["is_show"], mod=md_escape(m["module_codes"] or ""), src=md_escape(m.get("_source") or ""), ) ) a("") # ------------------------------------------------------------------ a("## 5. Packages") a("") a("### 5.1 `com.jeesite.modules.mes` subpackages") a("") for name, files in pkgs.items(): a(f"#### `{name}/` ({len(files)})") a("") for fn in files: a(f"- `{fn}`") a("") a("### 5.2 `com.jeesite.modules.utils` (Netty / protocol)") a("") a(f"Count: **{len(utils)}**") a("") for fn in utils: a(f"- `{fn}`") a("") # ------------------------------------------------------------------ a("## 6. `mes_` tables (`CREATE TABLE` in SQL dump)") a("") a(f"Count: **{len(tables)}**") a("") a("| Table | Comment |") a("|---|---|") for t in tables: a(f"| `{t['name']}` | {md_escape(t['comment'] or '')} |") a("") a("### 6.1 Extra DDL under `db/mysql/` (not necessarily in dump)") a("") if extra: for name, src in extra: a(f"- `{name}` — `{src}`") else: a("- (none)") a("") if extra_new: a("Tables created in incremental DDL but **absent from dump**:") a("") for n in extra_new: a(f"- `{n}`") a("") # ------------------------------------------------------------------ a("## 7. Screen pages and Netty") a("") a("### 7.1 Screen HTML (`mesScreen*.html` dashboards)") a("") for rel in view_groups["Screen"]: a(f"- `{rel}`") a("") a("Related (not dashboards): `mesScreenPlanList.html`, `mesScreenPlanForm.html` (plan CRUD).") a("") a("### 7.2 Netty") a("") a("| Server class | Port | Status | Source |") a("|---|---|---|---|") a("| `com.jeesite.modules.utils.NettyServer` | 3000 | **ACTIVE** | `Application.run()` starts `new NettyServer(3000).run()` in a thread |") a("| `com.jeesite.modules.utils.LdNettyServer` | 7890 | commented out | `Application.java` (镭雕机 TCP) |") a("") a("HTTP server port is configured separately (`BaseConfig.server_port` default `8980` / `server.port`).") a("") a("---") a("") a("_End of inventory._") path.parent.mkdir(parents=True, exist_ok=True) path.write_text("\n".join(lines) + "\n", encoding="utf-8") return { "controllers": len(controllers), "entities": len(entities), "list": len(view_groups["List"]), "form": len(view_groups["Form"]), "info": len(view_groups["Info"]), "screen": len(view_groups["Screen"]), "other_html": len(view_groups["other"]), "views": len(views_files), "tables": len(tables), "menus_all_mes": len(mes_menus), "menus_visible": len(visible), "biz": biz_methods, "mapped_methods": sum(len(c["methods"]) for c in controllers), "dao": len(pkgs.get("dao", [])), "service": len(pkgs.get("service", [])), "bytes": path.stat().st_size, "path": str(path), } def main(): print("Collecting views...", flush=True) views = collect_views() print(f" views={len(views[0])}", flush=True) print("Collecting controllers...", flush=True) controllers = collect_controllers() print(f" controllers={len(controllers)}", flush=True) print("Collecting entities...", flush=True) entities = collect_entities() print(f" entities={len(entities)}", flush=True) print("Parsing SQL dump...", flush=True) tables, menus = collect_sql() extra_menus = collect_incremental_menus({m["menu_code"] for m in menus}) menus.extend(extra_menus) print(f" tables={len(tables)} menus={len(menus)} extra_menus={len(extra_menus)}", flush=True) data = { "views": views, "controllers": controllers, "entities": entities, "sql": (tables, menus), "packages": list_packages(), "utils": list_java(JAVA_UTILS), "netty": parse_netty(), "extra_ddl": extra_ddl_tables(), } targets = [ Path(r"D:\mes\_migrate\INVENTORY.md"), Path(r"D:\IdeaProjects\mes-199-0Y7A\mescloud\_migrate_inventory.md"), ] stats = None for t in targets: try: stats = write_md(t, data) print(f"Wrote {t} ({stats['bytes']} bytes)", flush=True) except Exception as e: print(f"FAILED writing {t}: {e}", flush=True) print("STATS", stats) return 0 if __name__ == "__main__": sys.exit(main())