This commit is contained in:
24kycj
2026-06-12 11:53:18 +08:00
parent 55a6029779
commit 23b958753d
6 changed files with 209 additions and 160 deletions
+4 -1
View File
@@ -17,9 +17,12 @@
```bat ```bat
docker\sync-config.ps1 :: 同步 local.js 与 backend local.php(改端口后必跑) docker\sync-config.ps1 :: 同步 local.js 与 backend local.php(改端口后必跑)
scripts\start.bat :: 启动 scripts\start.bat :: 启动(不销毁已有容器,适合日常开发)
scripts\start.bat stop :: 停止(保留数据库) scripts\start.bat stop :: 停止(保留数据库)
scripts\start.bat reset :: 停止并清空数据库卷 scripts\start.bat reset :: 停止并清空数据库卷
scripts\start.bat rebuild :: 重建 API 镜像并重启 API 容器
scripts\start.bat status :: 查看容器与 /health
scripts\start.bat logs :: 跟踪 API 日志
``` ```
生产环境编辑 `frontend-web/config/local.js`(从 `config/local.js.example` 复制);全量更新 frontend 时不要覆盖该文件。本地开发由 `docker/sync-config.ps1` 生成 `local.js` 生产环境编辑 `frontend-web/config/local.js`(从 `config/local.js.example` 复制);全量更新 frontend 时不要覆盖该文件。本地开发由 `docker/sync-config.ps1` 生成 `local.js`
+4 -1
View File
@@ -24,7 +24,10 @@ services:
start_period: 30s start_period: 30s
api: api:
image: php:8.2-cli-bookworm build:
context: ./php
dockerfile: Dockerfile.dev
image: soondesign-api:dev
container_name: soondesign-api container_name: soondesign-api
restart: unless-stopped restart: unless-stopped
ports: ports:
+8
View File
@@ -0,0 +1,8 @@
FROM php:8.2-cli-bookworm
RUN apt-get update -qq \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
libmariadb-dev-compat $PHPIZE_DEPS \
&& docker-php-ext-install -j"$(nproc)" pdo_mysql \
&& apt-get purge -y --auto-remove $PHPIZE_DEPS \
&& rm -rf /var/lib/apt/lists/*
+2 -10
View File
@@ -1,22 +1,13 @@
#!/bin/sh #!/bin/sh
set -e set -e
if ! php -m 2>/dev/null | grep -q pdo_mysql; then
echo "[api] installing pdo_mysql..."
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
libmariadb-dev-compat $PHPIZE_DEPS
docker-php-ext-install -j"$(nproc)" pdo_mysql
apt-get purge -y --auto-remove $PHPIZE_DEPS
rm -rf /var/lib/apt/lists/*
fi
host="${SOON_DB_HOST:-mysql}" host="${SOON_DB_HOST:-mysql}"
port="${SOON_DB_PORT:-3306}" port="${SOON_DB_PORT:-3306}"
user="${SOON_DB_USER:-soondesign}" user="${SOON_DB_USER:-soondesign}"
pass="${SOON_DB_PASS:-soondesign}" pass="${SOON_DB_PASS:-soondesign}"
db="${SOON_DB_NAME:-soondesign}" db="${SOON_DB_NAME:-soondesign}"
echo "[api] waiting for mysql..."
i=0 i=0
while [ "$i" -lt 60 ]; do while [ "$i" -lt 60 ]; do
if php -r " if php -r "
@@ -41,4 +32,5 @@ done
php /docker/php/migrate-dev-schema.php || true php /docker/php/migrate-dev-schema.php || true
php /docker/php/seed-dev-admin.php || true php /docker/php/seed-dev-admin.php || true
echo "[api] listening on :8080"
exec php -S 0.0.0.0:8080 -t /var/www/public /docker/php/router-dev.php exec php -S 0.0.0.0:8080 -t /var/www/public /docker/php/router-dev.php
+80 -41
View File
@@ -409,6 +409,7 @@ function addBackground() {
function finishBack(backImg) { function finishBack(backImg) {
if (backImg) { if (backImg) {
backImg.objectCaching = true;
soonPlaceBgOnCanvas(backImg, '#canvas2'); soonPlaceBgOnCanvas(backImg, '#canvas2');
background_image2 = backImg; background_image2 = backImg;
window.background_image2 = backImg; window.background_image2 = backImg;
@@ -426,6 +427,7 @@ function addBackground() {
if (!loadBg) return; if (!loadBg) return;
loadBg('bg_front_1.png', function (frontImg) { loadBg('bg_front_1.png', function (frontImg) {
if (!frontImg) return; if (!frontImg) return;
frontImg.objectCaching = true;
frontImg.scale(1, 1); frontImg.scale(1, 1);
soonPlaceBgOnCanvas(frontImg, '#canvas1'); soonPlaceBgOnCanvas(frontImg, '#canvas1');
background_image1 = frontImg; background_image1 = frontImg;
@@ -436,10 +438,10 @@ function addBackground() {
canvas.renderAll(); canvas.renderAll();
recordObjs1.push(JSON.stringify(objs1)); recordObjs1.push(JSON.stringify(objs1));
recordJson1.push(canvas1.toJSON(TO_JSON_PROPERTIES)); recordJson1.push(canvas1.toJSON(TO_JSON_PROPERTIES));
loadBg('bg_back.png', finishBack, function () { loadBg('bg_back.png', finishBack); }); loadBg('bg_back.png', finishBack, function () { loadBg('bg_back.png', finishBack, null, 2, true); }, 2, true);
}, function () { }, function () {
setTimeout(startLoad, 200); setTimeout(startLoad, 200);
}); }, 2, true);
} }
whenReady('#canvas1', startLoad); whenReady('#canvas1', startLoad);
@@ -1515,7 +1517,20 @@ function handleObjectMoving(e) {
obj.set("angle", angleDeg); obj.set("angle", angleDeg);
obj.set("flipped", flipped); obj.set("flipped", flipped);
} }
updateControls(); soonScheduleUpdateControls();
}
var _soonUpdateControlsRaf = 0;
function soonScheduleUpdateControls() {
if (_soonUpdateControlsRaf) return;
_soonUpdateControlsRaf = requestAnimationFrame(function () {
_soonUpdateControlsRaf = 0;
window.updateControls();
});
}
function soonDesign1SetInteracting(on) {
window._soonDesign1Interacting = !!on;
} }
// 辅助函数:检查对象是否被锁定 // 辅助函数:检查对象是否被锁定
@@ -1623,17 +1638,28 @@ function initCanvasEvents() {
_canvasEventsInitialized = true; _canvasEventsInitialized = true;
[canvas1, canvas2].forEach(currentCanvas => { [canvas1, canvas2].forEach(currentCanvas => {
var _mouseMoveRaf = 0;
var _mouseMovePending = null;
// 1. 鼠标移动 // 1. 鼠标移动
currentCanvas.on("mouse:move", (e) => { currentCanvas.on("mouse:move", (e) => {
let offsetX = 0, offsetY = 0; _mouseMovePending = { e: e, canvas: currentCanvas };
if (currentCanvas === canvas2 && typeof background_image !== 'undefined') { if (_mouseMoveRaf) return;
offsetX = background_image.left; _mouseMoveRaf = requestAnimationFrame(function () {
offsetY = background_image.top; _mouseMoveRaf = 0;
} var payload = _mouseMovePending;
if (e.pointer) { if (!payload) return;
$("#posText").text("X: " + (e.pointer.x - offsetX).toFixed(0) + " Y: " + (e.pointer.y - offsetY).toFixed(0)); var moveEvent = payload.e;
} var moveCanvas = payload.canvas;
updateRulerGuides(e.e); let offsetX = 0, offsetY = 0;
if (moveCanvas === canvas2 && typeof background_image !== 'undefined') {
offsetX = background_image.left;
offsetY = background_image.top;
}
if (moveEvent.pointer) {
$("#posText").text("X: " + (moveEvent.pointer.x - offsetX).toFixed(0) + " Y: " + (moveEvent.pointer.y - offsetY).toFixed(0));
}
updateRulerGuides(moveEvent.e);
});
}); });
// 2. 鼠标移出 // 2. 鼠标移出
@@ -1726,12 +1752,19 @@ function initCanvasEvents() {
return false; return false;
} }
} }
soonDesign1SetInteracting(true);
handleObjectMoving(e); handleObjectMoving(e);
}); });
// 6. 变换事件 // 6. 变换事件
currentCanvas.on("object:rotating", updateControls); currentCanvas.on("object:rotating", function () {
currentCanvas.on("object:scaling", updateControls); soonDesign1SetInteracting(true);
soonScheduleUpdateControls();
});
currentCanvas.on("object:scaling", function () {
soonDesign1SetInteracting(true);
soonScheduleUpdateControls();
});
currentCanvas.on("object:rotated", () => { window.recordState(); }); currentCanvas.on("object:rotated", () => { window.recordState(); });
currentCanvas.on('text:editing:entered', () => { _textEdit = true; }); currentCanvas.on('text:editing:entered', () => { _textEdit = true; });
currentCanvas.on('text:editing:exited', () => { _textEdit = false; }); currentCanvas.on('text:editing:exited', () => { _textEdit = false; });
@@ -1739,38 +1772,41 @@ function initCanvasEvents() {
// 7. 对象修改后 // 7. 对象修改后
currentCanvas.on('object:modified', function (event) { currentCanvas.on('object:modified', function (event) {
if (currentCanvas.getActiveObjects().length > 1) return; try {
let idx = getIndex(event.target); if (currentCanvas.getActiveObjects().length > 1) return;
if (idx <= 0) return; let idx = getIndex(event.target);
let objData = objs[idx - 1]; if (idx <= 0) return;
// 安全检查:确保 objData 存在且有 type 属性 let objData = objs[idx - 1];
if (!objData || typeof objData.type === 'undefined') { // 安全检查:确保 objData 存在且有 type 属性
if (!objData || typeof objData.type === 'undefined') {
// 更新对象列表以同步锁状态
if (typeof window.updateList === 'function') {
window.updateList();
}
return;
}
let target = event.target;
if ([3, 4, 10].includes(objData.type)) {
target.fontSize = (target.fontSize * target.scaleX).toFixed(0);
target.scaleX = 1; target.scaleY = 1;
target._clearCache();
if (objData.type == 3) $("#text_size").val(target.fontSize);
if (objData.type == 4) $("#out_text_size").val(target.fontSize);
if (objData.type == 10) $("#count_size").val(target.fontSize);
currentCanvas.renderAll();
} else if (objData.type == 5) {
target.width *= target.scaleX; target.height *= target.scaleY;
target.scaleX = 1; target.scaleY = 1;
}
window.recordState();
// 更新对象列表以同步锁状态 // 更新对象列表以同步锁状态
if (typeof window.updateList === 'function') { if (typeof window.updateList === 'function') {
window.updateList(); window.updateList();
} }
return; } finally {
} soonDesign1SetInteracting(false);
let target = event.target;
if ([3, 4, 10].includes(objData.type)) {
target.fontSize = (target.fontSize * target.scaleX).toFixed(0);
target.scaleX = 1; target.scaleY = 1;
target._clearCache();
if (objData.type == 3) $("#text_size").val(target.fontSize);
if (objData.type == 4) $("#out_text_size").val(target.fontSize);
if (objData.type == 10) $("#count_size").val(target.fontSize);
currentCanvas.renderAll();
updateControls(); updateControls();
} else if (objData.type == 5) {
target.width *= target.scaleX; target.height *= target.scaleY;
target.scaleX = 1; target.scaleY = 1;
updateControls();
}
window.recordState();
// 更新对象列表以同步锁状态
if (typeof window.updateList === 'function') {
window.updateList();
} }
}); });
// ================================================== // ==================================================
@@ -1829,6 +1865,8 @@ function initCanvasEvents() {
ctx.restore(); ctx.restore();
} }
if (window._soonDesign1Interacting) return;
// 在遮罩之后重新绘制辅助线,确保辅助线在最上层 // 在遮罩之后重新绘制辅助线,确保辅助线在最上层
const objects = currentCanvas.getObjects(); const objects = currentCanvas.getObjects();
objects.forEach(obj => { objects.forEach(obj => {
@@ -2042,6 +2080,7 @@ function initRulers() {
// --- 3. 绘制标尺 (核心绘制循环) --- // --- 3. 绘制标尺 (核心绘制循环) ---
function updateAndDraw() { function updateAndDraw() {
if (window._soonDesign1Interacting) return;
// 每次重新获取尺寸,确保使用最新值 // 每次重新获取尺寸,确保使用最新值
// 使用多种方法尝试获取实际尺寸,确保准确 // 使用多种方法尝试获取实际尺寸,确保准确
let currentCanvasWidth = canvasWrapper.width() || canvasWrapper.outerWidth() || canvasWrapper[0].getBoundingClientRect().width || 800; let currentCanvasWidth = canvasWrapper.width() || canvasWrapper.outerWidth() || canvasWrapper[0].getBoundingClientRect().width || 800;
+111 -107
View File
@@ -1,29 +1,34 @@
@echo off @echo off
setlocal EnableDelayedExpansion setlocal EnableDelayedExpansion
rem Double-click (cmd /c): re-launch under cmd /k so the window stays open set "SELF=%~f0"
if /i not "%~1"=="__run__" ( if /i "%~1"=="__run__" (
set "ACTION=%~2"
) else (
set "ACTION=%~1"
echo %CMDCMDLINE% | find /i " /c " >nul echo %CMDCMDLINE% | find /i " /c " >nul
if not errorlevel 1 ( if not errorlevel 1 (
cmd /k "%~f0" __run__ %* cmd /k "%SELF%" __run__ %*
exit /b exit /b
) )
) )
shift
set "ROOT=%~dp0.." set "ROOT=%~dp0.."
for %%I in ("%ROOT%") do set "ROOT=%%~fI"
cd /d "%ROOT%" || goto :fail_cd cd /d "%ROOT%" || goto :fail_cd
set "COMPOSE=docker\docker-compose.yml" set "COMPOSE=docker\docker-compose.yml"
set "ENV_FILE=docker\.env" set "ENV_FILE=docker\.env"
set "COMPOSE_PROJECT=soondesign" set "COMPOSE_PROJECT=soondesign"
set "ACTION=%~1"
set "WEB_PORT=8100" set "WEB_PORT=8100"
set "ELECTRON_PORT=8101" set "ELECTRON_PORT=8101"
set "BACKEND_PORT=8102" set "BACKEND_PORT=8102"
if /i "%ACTION%"=="stop" goto :do_stop if /i "%ACTION%"=="stop" goto :do_stop
if /i "%ACTION%"=="reset" goto :do_reset if /i "%ACTION%"=="reset" goto :do_reset
if /i "%ACTION%"=="rebuild" goto :do_rebuild
if /i "%ACTION%"=="logs" goto :do_logs
if /i "%ACTION%"=="status" goto :do_status
goto :do_start goto :do_start
:compose :compose
@@ -34,6 +39,66 @@ exit /b !ERRORLEVEL!
docker compose -p "%COMPOSE_PROJECT%" -f "%COMPOSE%" %* docker compose -p "%COMPOSE_PROJECT%" -f "%COMPOSE%" %*
exit /b !ERRORLEVEL! exit /b !ERRORLEVEL!
:load_ports
for /f "usebackq eol=# tokens=1,* delims==" %%A in ("%ENV_FILE%") do (
if /i "%%A"=="WEB_PORT" set "WEB_PORT=%%B"
if /i "%%A"=="ELECTRON_PORT" set "ELECTRON_PORT=%%B"
if /i "%%A"=="BACKEND_PORT" set "BACKEND_PORT=%%B"
)
exit /b 0
:ensure_env
if not exist "%ROOT%\docker\docker-compose.yml" (
echo [ERROR] Missing docker\docker-compose.yml under %ROOT%
exit /b 1
)
if not exist "%ROOT%\soonModels\" mkdir "%ROOT%\soonModels"
if not exist "%ENV_FILE%" (
if not exist "docker\.env.example" (
echo [ERROR] Missing docker\.env.example
exit /b 1
)
copy /Y "docker\.env.example" "%ENV_FILE%" >nul
)
powershell -NoProfile -ExecutionPolicy Bypass -File "docker\sync-config.ps1" -EnvFile "%ENV_FILE%"
if errorlevel 1 (
echo [ERROR] Config sync failed.
exit /b 1
)
call :load_ports
exit /b 0
:wait_api
echo [INFO] Waiting for API http://localhost:%BACKEND_PORT%/health ...
set /a RETRY=0
:wait_health
set /a RETRY+=1
curl.exe -sf "http://localhost:%BACKEND_PORT%/health" >nul 2>&1
if not errorlevel 1 exit /b 0
if !RETRY! GEQ 90 (
echo [WARN] API not ready after ~3 min. Run: scripts\start.bat logs
exit /b 1
)
set /a MOD=!RETRY! %% 5
if !MOD!==0 echo ... still waiting (!RETRY!/90)
ping -n 3 127.0.0.1 >nul
goto :wait_health
:print_urls
echo.
echo ========================================
echo Project: %COMPOSE_PROJECT%
echo Web: http://localhost:%WEB_PORT%/pages/index.web.html
echo Admin: http://localhost:%WEB_PORT%/pages/admin/index.html
echo Electron: http://localhost:%ELECTRON_PORT%/pages/index.html
echo API: http://localhost:%BACKEND_PORT%/health
echo ========================================
echo Login: admin@local.test / admin123
echo Desktop: cd frontend-electron ^&^& npm start
echo Stop: scripts\start.bat stop
echo Logs: scripts\start.bat logs
exit /b 0
:do_stop :do_stop
if exist "%ENV_FILE%" (call :compose down) else (call :compose_noenv down) if exist "%ENV_FILE%" (call :compose down) else (call :compose_noenv down)
echo. echo.
@@ -46,6 +111,33 @@ echo.
echo [OK] Project %COMPOSE_PROJECT% stopped. DB volume removed. echo [OK] Project %COMPOSE_PROJECT% stopped. DB volume removed.
exit /b 0 exit /b 0
:do_logs
docker logs -f --tail 80 soondesign-api
exit /b 0
:do_status
call :ensure_env
if errorlevel 1 goto :fail
docker ps --filter name=soondesign --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
echo.
curl.exe -sf "http://localhost:%BACKEND_PORT%/health" >nul 2>&1
if errorlevel 1 (
echo [WARN] API health check failed.
) else (
echo [OK] API health check passed.
)
exit /b 0
:do_rebuild
echo [SoonDesign] Rebuilding API image and restarting...
call :ensure_env
if errorlevel 1 goto :fail
if exist "%ENV_FILE%" (call :compose up -d --build --force-recreate api) else (call :compose_noenv up -d --build --force-recreate api)
if errorlevel 1 goto :fail
call :wait_api
call :print_urls
goto :done_ok
:do_start :do_start
echo [SoonDesign] Starting local Docker environment... echo [SoonDesign] Starting local Docker environment...
echo [INFO] Compose project: %COMPOSE_PROJECT% echo [INFO] Compose project: %COMPOSE_PROJECT%
@@ -57,121 +149,33 @@ if errorlevel 1 (
goto :fail goto :fail
) )
if not exist "soonModels\" mkdir soonModels call :ensure_env
if errorlevel 1 goto :fail
if not exist "docker\docker-compose.yml" (
echo [ERROR] Missing docker\docker-compose.yml
goto :fail
)
if not exist "%ENV_FILE%" (
if not exist "docker\.env.example" (
echo [ERROR] Missing docker\.env.example
goto :fail
)
copy /Y "docker\.env.example" "%ENV_FILE%" >nul
)
powershell -NoProfile -ExecutionPolicy Bypass -File "docker\sync-config.ps1" -EnvFile "%ENV_FILE%"
if errorlevel 1 (
echo [ERROR] Config sync failed.
goto :fail
)
for /f "usebackq eol=# tokens=1,* delims==" %%A in ("%ENV_FILE%") do (
if /i "%%A"=="WEB_PORT" set "WEB_PORT=%%B"
if /i "%%A"=="ELECTRON_PORT" set "ELECTRON_PORT=%%B"
if /i "%%A"=="BACKEND_PORT" set "BACKEND_PORT=%%B"
)
echo [INFO] Removing legacy compose project "docker" if present...
docker compose -p docker -f "%COMPOSE%" --env-file "%ENV_FILE%" down >nul 2>&1
docker network rm docker_default >nul 2>&1
call :compose down >nul 2>&1
echo [INFO] Ports: Web=%WEB_PORT% Electron=%ELECTRON_PORT% API=%BACKEND_PORT% echo [INFO] Ports: Web=%WEB_PORT% Electron=%ELECTRON_PORT% API=%BACKEND_PORT%
echo [INFO] Starting containers - project %COMPOSE_PROJECT%... echo [INFO] docker compose up -d (keeps running containers; use rebuild for fresh API image)
call :compose up -d if exist "%ENV_FILE%" (call :compose up -d) else (call :compose_noenv up -d)
set "DC_EXIT=!ERRORLEVEL!" if errorlevel 1 (
if not "!DC_EXIT!"=="0" ( echo [ERROR] docker compose failed. Is Docker Desktop running?
echo.
echo [ERROR] docker compose failed with exit code !DC_EXIT!.
echo Ensure Docker Desktop is running.
echo First API start may install pdo_mysql for about 1 minute.
goto :fail goto :fail
) )
echo [INFO] Waiting for API - first run may take up to 2 min for pdo_mysql install... call :wait_api
set /a RETRY=0 if errorlevel 1 goto :show_urls_warn
:wait_health
set /a RETRY+=1
curl.exe -sf "http://localhost:%BACKEND_PORT%/health" >nul 2>&1
if not errorlevel 1 goto :ready
if !RETRY! GEQ 60 goto :show_urls
ping -n 3 127.0.0.1 >nul
goto :wait_health
:ready
echo [OK] Backend is ready. echo [OK] Backend is ready.
:show_urls
echo.
echo ========================================
echo Project: %COMPOSE_PROJECT%
echo Web: http://localhost:%WEB_PORT%/pages/index.web.html
echo Admin: http://localhost:%WEB_PORT%/pages/admin/index.html
echo Electron: http://localhost:%ELECTRON_PORT%/pages/index.html
echo API: http://localhost:%BACKEND_PORT%/health
echo ========================================
echo Login: admin@local.test / admin123
echo Stop: scripts\start.bat stop
echo.
curl.exe -sf "http://localhost:%BACKEND_PORT%/health" >nul 2>&1
if errorlevel 1 (
echo [WARN] API not ready yet. Check: docker logs soondesign-api
goto :done_ok
)
curl.exe -sf "http://localhost:%BACKEND_PORT%/api/v1/settings" >nul 2>&1 curl.exe -sf "http://localhost:%BACKEND_PORT%/api/v1/settings" >nul 2>&1
if errorlevel 1 ( if errorlevel 1 (
echo [WARN] API routes not responding. Check: docker logs soondesign-api echo [WARN] API routes not responding. Run: scripts\start.bat logs
) else ( ) else (
echo [OK] API routes verified. echo [OK] API routes verified.
) )
call :print_urls
start "" "http://localhost:%WEB_PORT%/pages/index.web.html"
start "" "http://localhost:%WEB_PORT%/pages/admin/index.html"
call :launch_electron
goto :done_ok goto :done_ok
:launch_electron :show_urls_warn
where node >nul 2>&1 call :print_urls
if errorlevel 1 ( goto :done_ok
echo [WARN] Node.js not found. Open Electron preview in browser instead.
start "" "http://localhost:%ELECTRON_PORT%/pages/index.html"
exit /b 0
)
if not exist "frontend-electron\package.json" (
echo [WARN] frontend-electron not found. Opening browser preview.
start "" "http://localhost:%ELECTRON_PORT%/pages/index.html"
exit /b 0
)
if not exist "frontend-electron\node_modules\electron" (
echo [INFO] Installing Electron dependencies - first run only...
pushd frontend-electron
call npm install --no-audit --no-fund
if errorlevel 1 (
popd
echo [WARN] npm install failed. Opening browser preview.
start "" "http://localhost:%ELECTRON_PORT%/pages/index.html"
exit /b 0
)
popd
)
echo [INFO] Launching Electron desktop app...
start "SoonDesign Desktop" /D "%CD%\frontend-electron" cmd /c "npm start"
exit /b 0
:fail_cd :fail_cd
echo [ERROR] Cannot cd to project root: %ROOT% echo [ERROR] Cannot cd to project root: %ROOT%
@@ -184,5 +188,5 @@ exit /b 1
:done_ok :done_ok
echo. echo.
echo [DONE] SoonDesign is running under Docker project %COMPOSE_PROJECT%. echo [DONE] SoonDesign dev stack is up (project %COMPOSE_PROJECT%).
exit /b 0 exit /b 0