Quellcode durchsuchen

refactor: 合并后端构建脚本为统一入口

- 新增 build.py 替代 build_v6~v11(javac编译 + fat jar重打包 + 资源注入 + 新增类补打)
- 新增 start.sh 替代 start_v7~v11(自动检测最新版本 + PORT环境变量支持)
- 删除所有过时版本脚本:build_v*.py, start_v*.sh, repack*.py, extract_deps.py
- 更新 AGENTS.md 和 backend/README.md 文档说明
- 清理 backend/.gitignore 重复条目
C vor 1 Monat
Ursprung
Commit
ad31bc1b9b
5 geänderte Dateien mit 329 neuen und 7 gelöschten Zeilen
  1. 31 2
      AGENTS.md
  2. 1 5
      backend/.gitignore
  3. 7 0
      backend/README.md
  4. 250 0
      backend/build.py
  5. 40 0
      backend/start.sh

+ 31 - 2
AGENTS.md

@@ -31,15 +31,44 @@
 
 ## 构建与启动命令
 
+### 方式一:Maven(推荐,需 JDK 17 + Maven)
+
 ```bash
 # 数据库初始化
 # 执行 sql/schema.sql
 
-# 后端
+# 后端(开发模式,热重载)
 cd backend
 mvn spring-boot:run
 
-# 前端
+# 或打包后运行
+mvn clean package -DskipTests
+java -jar target/resource-platform.jar
+```
+
+### 方式二:离线构建脚本(无 Maven 环境时使用)
+
+```bash
+cd backend
+
+# 构建(自动基于最新 jar 递增版本号)
+python build.py
+
+# 指定版本号
+python build.py --version 12
+
+# 启动(自动检测最新版本)
+bash start.sh
+
+# 启动指定版本
+bash start.sh 11
+```
+
+> **构建脚本说明:** `build.py` 是统一入口,合并了历史版本 build_v6~v11 的全部逻辑(javac 编译 + fat jar 重打包 + 资源注入 + 新增类补打)。`start.sh` 是统一启动脚本,支持 `PORT` 环境变量自定义端口。
+
+### 前端
+
+```bash
 cd frontend
 npm install
 npm run dev

+ 1 - 5
backend/.gitignore

@@ -3,10 +3,6 @@ target/
 .idea/
 *.iml
 application-dev.yml
-target/
-*.class
-.idea/
-*.iml
-# 离线重编译的依赖缓存与编译产物(非源码, 由 build_v*.py 生成)
+# 离线重编译的依赖缓存与编译产物(非源码, 由 build.py 生成)
 lib/
 build_tmp_classes/

+ 7 - 0
backend/README.md

@@ -20,6 +20,13 @@
    ```
    或者直接用IDE运行 `ResourcePlatformApplication`。
 
+   **无 Maven 环境时**可用离线构建脚本:
+   ```bash
+   python build.py          # 构建(自动递增版本号)
+   bash start.sh            # 启动(自动检测最新版本)
+   ```
+   详见根目录 `AGENTS.md`。
+
 4. **默认账号**
    - 账号:`admin`
    - 密码:`admin123`

+ 250 - 0
backend/build.py

@@ -0,0 +1,250 @@
+#!/usr/bin/env python3
+"""
+统一构建脚本 — 合并 build_v6~v11 为单入口。
+
+用法:
+    python build.py              # 基于最新 jar 自动递增版本号
+    python build.py --version 12 # 指定输出版本号
+    python build.py --base path/to/base.jar  # 指定基底 jar
+
+功能:
+    1. 收集 lib/ 下所有 jar 作为 classpath(首次运行自动从基底 jar 解压)
+    2. 用 javac 编译 src/main/java 下全部源码
+    3. 以基底 fat jar 为模板,替换 BOOT-INF/classes 下的 class 和资源文件,
+       并补打基底中不存在的新增类 / 资源
+    4. 输出 target/resource-platform-v{N}.jar
+"""
+
+import zipfile, os, subprocess, glob, sys, shutil, argparse, re
+
+# ── 路径常量 ──────────────────────────────────────────────
+BASE = os.path.dirname(os.path.abspath(__file__))
+SRC = os.path.join(BASE, "src", "main", "java")
+RES = os.path.join(BASE, "src", "main", "resources")
+OUT = os.path.join(BASE, "build_tmp_classes")
+LIB = os.path.join(BASE, "lib")
+TARGET = os.path.join(BASE, "target")
+
+JAVAC = r"D:\Program Files\Java\jdk-17\bin\javac.exe"
+LOMBOK = r"C:\Users\BXD-XTYW\.m2\repository\org\projectlombok\lombok\1.18.32\lombok-1.18.32.jar"
+
+# ── 辅助函数 ──────────────────────────────────────────────
+
+def find_latest_version():
+    """扫描 target/ 目录,返回已有的最高版本号;无则返回 0。"""
+    versions = []
+    pattern = re.compile(r"resource-platform-v(\d+)\.jar$")
+    if os.path.isdir(TARGET):
+        for f in os.listdir(TARGET):
+            m = pattern.match(f)
+            if m:
+                versions.append(int(m.group(1)))
+    return max(versions) if versions else 0
+
+
+def find_base_jar(current_version):
+    """
+    按优先级查找基底 jar:
+      1. target/resource-platform-v{N}.jar(最高版本)
+      2. target/resource-platform.jar(Maven 产出,无版本号)
+    """
+    if current_version > 0:
+        versioned = os.path.join(TARGET, f"resource-platform-v{current_version}.jar")
+        if os.path.isfile(versioned):
+            return versioned
+    # 回退到 Maven 产出的无版本号 jar
+    maven_jar = os.path.join(TARGET, "resource-platform.jar")
+    if os.path.isfile(maven_jar):
+        return maven_jar
+    return None
+
+
+def extract_libs_from_jar(jar_path):
+    """从 fat jar 解压 BOOT-INF/lib/*.jar 到 lib/ 目录。"""
+    os.makedirs(LIB, exist_ok=True)
+    with zipfile.ZipFile(jar_path) as z:
+        for name in z.namelist():
+            if name.startswith("BOOT-INF/lib/") and name.endswith(".jar"):
+                out = os.path.join(LIB, os.path.basename(name))
+                with open(out, "wb") as f:
+                    f.write(z.read(name))
+
+
+def ensure_lombok():
+    """确保 lombok jar 存在于 lib/ 目录。"""
+    dst = os.path.join(LIB, "lombok-1.18.32.jar")
+    if not os.path.exists(dst) and os.path.exists(LOMBOK):
+        shutil.copy(LOMBOK, dst)
+
+
+def collect_classpath():
+    """收集 lib/ 下所有 jar,拼成 classpath 字符串。"""
+    dep_jars = sorted(glob.glob(os.path.join(LIB, "*.jar")))
+    print(f"classpath entries: {len(dep_jars)}")
+    return ";".join(dep_jars)
+
+
+def compile_sources(cp):
+    """编译 src/main/java 下全部 .java 到 build_tmp_classes/。"""
+    sources = []
+    for root, _, files in os.walk(SRC):
+        for f in files:
+            if f.endswith(".java"):
+                sources.append(os.path.join(root, f))
+    print(f"source files: {len(sources)}")
+
+    os.makedirs(OUT, exist_ok=True)
+    cmd = [JAVAC, "-parameters", "-encoding", "UTF-8", "-cp", cp, "-d", OUT] + sources
+    r = subprocess.run(cmd, capture_output=True)
+    print(f"javac returncode: {r.returncode}")
+    if r.returncode != 0:
+        err = (r.stderr or b"").decode("utf-8", "replace")
+        print("=== STDERR (first 60 lines) ===")
+        print("\n".join(err.splitlines()[:60]))
+        sys.exit(1)
+    class_count = len(glob.glob(os.path.join(OUT, "**", "*.class"), recursive=True))
+    print(f"compiled classes: {class_count}")
+    return class_count
+
+
+def repackage(base_jar, dst_jar):
+    """
+    以 base_jar 为基底,替换 / 补打 class 和资源文件,生成 dst_jar。
+    逻辑合并自 build_v7(资源注入)+ build_v8(新增类补打)。
+    """
+    with zipfile.ZipFile(base_jar) as zin:
+        infos = zin.infolist()
+        all_names = set(zin.namelist())
+
+        def local_for(entry_name):
+            # 已编译 class: 优先用本地编译产物
+            if entry_name.startswith("BOOT-INF/classes/") and entry_name.endswith(".class"):
+                rel = entry_name[len("BOOT-INF/classes/"):]
+                local = os.path.join(OUT, rel)
+                if os.path.isfile(local):
+                    return local
+                return None
+            # 资源文件: 用 src/main/resources 中的版本
+            if entry_name.startswith("BOOT-INF/classes/"):
+                rel = entry_name[len("BOOT-INF/classes/"):]
+                local = os.path.join(RES, rel)
+                if os.path.isfile(local):
+                    return local
+            return None
+
+        manifest_name = "META-INF/MANIFEST.MF"
+
+        # 收集 src/main/resources 中新增的、基底 jar 里没有的资源条目
+        extra_resources = []
+        for root, _, files in os.walk(RES):
+            for f in files:
+                full = os.path.join(root, f)
+                rel = os.path.relpath(full, RES).replace("\\", "/")
+                entry = "BOOT-INF/classes/" + rel
+                if entry not in all_names:
+                    extra_resources.append((entry, full))
+
+        # 收集本地编译产物中新增的、基底 jar 里不存在的 class
+        extra_classes = []
+        for root, _, files in os.walk(OUT):
+            for f in files:
+                if f.endswith(".class"):
+                    full = os.path.join(root, f)
+                    rel = os.path.relpath(full, OUT).replace("\\", "/")
+                    entry = "BOOT-INF/classes/" + rel
+                    if entry not in all_names:
+                        extra_classes.append((entry, full))
+
+        with zipfile.ZipFile(dst_jar, "w", compression=zipfile.ZIP_DEFLATED) as zout:
+            # MANIFEST 必须第一个写入
+            mi = next(i for i in infos if i.filename == manifest_name)
+            zout.writestr(mi, zin.read(manifest_name))
+            for i in infos:
+                if i.filename == manifest_name:
+                    continue
+                local = local_for(i.filename)
+                if local:
+                    with open(local, "rb") as lf:
+                        data = lf.read()
+                    zi = zipfile.ZipInfo(i.filename)
+                    zi.external_attr = i.external_attr
+                    zi.date_time = i.date_time
+                    zout.writestr(zi, data)
+                else:
+                    zout.writestr(i, zin.read(i.filename))
+            # 补打新增资源
+            for entry, full in extra_resources:
+                with open(full, "rb") as lf:
+                    data = lf.read()
+                zout.writestr(zipfile.ZipInfo(entry), data)
+            # 补打新增 class
+            for entry, full in extra_classes:
+                with open(full, "rb") as lf:
+                    data = lf.read()
+                zout.writestr(zipfile.ZipInfo(entry), data)
+
+    # 校验
+    zm = zipfile.ZipFile(dst_jar)
+    print(f"manifest first: {zm.namelist()[0] == manifest_name}")
+    print(f"extra resources injected: {len(extra_resources)}")
+    print(f"extra classes injected: {len(extra_classes)}")
+    return zm
+
+
+# ── 主流程 ────────────────────────────────────────────────
+
+def main():
+    parser = argparse.ArgumentParser(description="统一构建脚本")
+    parser.add_argument("--version", type=int, help="指定输出版本号(默认自动递增)")
+    parser.add_argument("--base", help="指定基底 jar 路径(默认使用最新版本)")
+    args = parser.parse_args()
+
+    # 1. 确定版本号
+    latest = find_latest_version()
+    if args.version:
+        new_version = args.version
+    else:
+        new_version = latest + 1
+
+    # 2. 确定基底 jar
+    if args.base:
+        base_jar = args.base
+    else:
+        base_jar = find_base_jar(latest)
+
+    if not base_jar or not os.path.isfile(base_jar):
+        print(f"ERROR: 基底 jar 不存在: {base_jar}")
+        print("请先通过 'mvn clean package -DskipTests' 生成初始 jar,")
+        print("或使用 --base 参数指定基底 jar 路径。")
+        sys.exit(1)
+
+    dst_jar = os.path.join(TARGET, f"resource-platform-v{new_version}.jar")
+
+    print(f"base jar:  {base_jar}")
+    print(f"output:    {dst_jar}")
+    print(f"version:   {new_version}")
+    print()
+
+    # 3. 准备 classpath
+    os.makedirs(LIB, exist_ok=True)
+    if not glob.glob(os.path.join(LIB, "*.jar")):
+        print("lib/ 为空, 从基底 jar 解压依赖...")
+        extract_libs_from_jar(base_jar)
+    ensure_lombok()
+    cp = collect_classpath()
+    print()
+
+    # 4. 编译
+    compile_sources(cp)
+    print()
+
+    # 5. 重打包
+    zm = repackage(base_jar, dst_jar)
+    print()
+
+    print(f"built: {dst_jar}")
+    print(f"exists: {os.path.exists(dst_jar)}")
+
+
+if __name__ == "__main__":
+    main()

+ 40 - 0
backend/start.sh

@@ -0,0 +1,40 @@
+#!/bin/bash
+# 统一启动脚本 — 合并 start_v7~v11
+# 用法:
+#   ./start.sh              # 启动最新版本
+#   ./start.sh 11           # 启动指定版本
+#   PORT=9090 ./start.sh    # 自定义端口
+
+cd /d/demo/resource-platform/backend
+
+# 自动检测最新版本
+if [ -z "$1" ]; then
+  VERSION=$(ls target/resource-platform-v*.jar 2>/dev/null \
+    | sed 's/.*-v\([0-9]*\)\.jar/\1/' \
+    | sort -n | tail -1)
+  if [ -z "$VERSION" ]; then
+    echo "ERROR: target/ 下未找到任何版本 jar,请先运行 python build.py"
+    exit 1
+  fi
+else
+  VERSION=$1
+fi
+
+JAR="target/resource-platform-v${VERSION}.jar"
+
+if [ ! -f "$JAR" ]; then
+  echo "ERROR: $JAR 不存在"
+  exit 1
+fi
+
+PORT=${PORT:-8080}
+
+echo "Starting resource-platform v${VERSION} on port ${PORT}..."
+
+"D:/Program Files/Java/jdk-17/bin/java.exe" \
+  -Dspring.profiles.active=dev \
+  -Dserver.port="${PORT}" \
+  -DDB_URL="${DB_URL:-jdbc:mysql://rm-uf64611sxm4qe438g8o.mysql.rds.aliyuncs.com:33450/resource_platform?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false}" \
+  -DDB_USERNAME="${DB_USERNAME:-root}" \
+  -DDB_PASSWORD="${DB_PASSWORD:-Cmt123456}" \
+  -jar "$JAR"