Files
nj/scripts/dev.mjs
iven 4cd381295a
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled
fix(app): Flutter Web 开发模式默认连接 localhost:3000 API
- app.dart: kDebugMode 下使用 AppConfig.dev (localhost:3000)
- dev.mjs: flutter run 传入 --dart-define API_BASE_URL/SSE_BASE_URL
2026-06-03 17:50:55 +08:00

443 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 暖记开发环境启动脚本 — 跨平台 (Windows/macOS/Linux)
*
* 用法:
* pnpm start:dev # 启动全部 (后端+管理端+学生端)
* pnpm start:dev:backend # 只启动后端
* pnpm start:dev:admin # 只启动管理端前端
* pnpm start:dev:app # 只启动学生端 Flutter
* pnpm start:stop # 停止所有服务
*/
import { spawn, execSync } from "node:child_process";
import { createRequire } from "node:module";
import { existsSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, "..");
const IS_WIN = process.platform === "win32";
// ===== 配置 =====
const PORTS = {
backend: 3000,
admin: 5174,
app: 8080,
};
const PG = {
host: "localhost",
port: 5432,
user: "postgres",
pass: "123123",
db: "nuanji",
};
// ===== 颜色输出 =====
const color = {
red: (s) => `\x1b[31m${s}\x1b[0m`,
green: (s) => `\x1b[32m${s}\x1b[0m`,
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
blue: (s) => `\x1b[34m${s}\x1b[0m`,
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
bold: (s) => `\x1b[1m${s}\x1b[0m`,
gray: (s) => `\x1b[90m${s}\x1b[0m`,
};
const log = {
info: (msg) => console.log(color.blue("[INFO]") + " " + msg),
ok: (msg) => console.log(color.green("[OK]") + " " + msg),
warn: (msg) => console.log(color.yellow("[WARN]") + " " + msg),
err: (msg) => console.log(color.red("[ERROR]") + " " + msg),
};
// ===== 工具函数 =====
/** 查找占用指定端口的进程 PID */
function findPidsOnPort(port) {
try {
if (IS_WIN) {
const out = execSync(
`netstat -ano | findstr :${port} | findstr LISTENING`,
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
);
return [
...new Set(
out
.trim()
.split("\n")
.map((l) => l.trim().split(/\s+/).pop())
.filter((p) => p && p !== "0")
),
];
}
// macOS / Linux
const out = execSync(`lsof -ti :${port}`, {
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
});
return out
.trim()
.split("\n")
.filter(Boolean);
} catch {
return [];
}
}
/** 杀死指定 PID */
function killPid(pid) {
try {
if (IS_WIN) {
execSync(`taskkill /F /PID ${pid}`, { stdio: "pipe" });
} else {
execSync(`kill -9 ${pid}`, { stdio: "pipe" });
}
return true;
} catch {
return false;
}
}
/** 清理端口上所有进程 */
function killPort(port, name) {
const pids = findPidsOnPort(port);
if (pids.length === 0) {
log.info(`${name} 端口 ${port} 空闲`);
return;
}
for (const pid of pids) {
if (killPid(pid)) {
log.ok(`已停止 ${name} (PID: ${pid}, 端口: ${port})`);
} else {
log.warn(`无法停止 ${name} (PID: ${pid})`);
}
}
}
/** 检查端口是否已被占用 */
function isPortInUse(port) {
return findPidsOnPort(port).length > 0;
}
/** 等待端口可达 */
function waitForPort(port, name, maxAttempts = 30, intervalMs = 2000) {
return new Promise((resolve) => {
let attempts = 0;
const check = () => {
attempts++;
if (isPortInUse(port)) {
log.ok(`${name} 已就绪 → http://localhost:${port}`);
resolve(true);
return;
}
if (attempts >= maxAttempts) {
log.err(`${name} 启动超时 (等待了 ${maxAttempts * intervalMs / 1000}s)`);
resolve(false);
return;
}
setTimeout(check, intervalMs);
};
check();
});
}
// ===== 前置检查 =====
function checkPostgreSQL() {
try {
const psqlPath = IS_WIN ? "D:\\postgreSQL\\bin\\psql.exe" : "psql";
execSync(
`"${psqlPath}" -U ${PG.user} -h ${PG.host} -p ${PG.port} -d ${PG.db} -c "SELECT 1"`,
{ stdio: "pipe", env: { ...process.env, PGPASSWORD: PG.pass } }
);
log.ok(`PostgreSQL 已连接 (${PG.db}@${PG.host}:${PG.port})`);
return true;
} catch {
log.err(`无法连接 PostgreSQL (${PG.db}@${PG.host}:${PG.port})`);
log.err("请确保 PostgreSQL 已启动且数据库存在");
return false;
}
}
function checkRedis() {
try {
execSync("redis-cli ping", { stdio: "pipe" });
log.ok("Redis 已连接");
return true;
} catch {
log.err("无法连接 Redis (localhost:6379)");
return false;
}
}
function checkFlutter() {
const flutterBin = IS_WIN ? "D:\\flutter\\bin\\flutter.bat" : "flutter";
try {
execSync(`${flutterBin} --version`, { stdio: "pipe" });
log.ok("Flutter SDK 可用");
return true;
} catch {
log.err("Flutter SDK 不可用");
return false;
}
}
function runChecks(services) {
const needPg = services.includes("backend");
const needRedis = services.includes("backend");
const needFlutter = services.includes("app");
if (needPg && !checkPostgreSQL()) return false;
if (needRedis && !checkRedis()) return false;
if (needFlutter && !checkFlutter()) return false;
return true;
}
// ===== 启动服务 =====
/** 启动后端 (Rust/Axum) */
async function startBackend() {
log.info("清理旧后端进程...");
killPort(PORTS.backend, "后端");
// Windows: 额外清理 erp-server.exe
if (IS_WIN) {
try {
execSync("taskkill /F /IM erp-server.exe", { stdio: "pipe" });
} catch {
// 忽略,可能没有运行
}
}
log.info("编译并启动后端 (diary feature)...");
const env = {
...process.env,
ERP__DATABASE__URL: `postgres://${PG.user}:${PG.pass}@${PG.host}:${PG.port}/${PG.db}`,
ERP__REDIS__URL: "redis://localhost:6379",
ERP__JWT__SECRET: "nuanji-dev-jwt-secret-2024-warm-notes-hmac-key-32b",
ERP__AUTH__SUPER_ADMIN_PASSWORD: "admin123",
ERP__WECHAT__APPID: "wx_dev_placeholder",
ERP__WECHAT__SECRET: "wx_dev_secret_placeholder",
ERP__WECHAT__DEV_MODE: "true",
ERP__CRYPTO__KEK: "0000000000000000000000000000000000000000000000000000000000000000",
};
const child = spawn("cargo run -p erp-server --features diary", {
cwd: ROOT,
env,
shell: true,
stdio: "pipe",
});
child.stdout?.on("data", (data) => {
const lines = data.toString().trim().split("\n");
for (const line of lines) {
if (line.trim()) console.log(color.cyan("[backend]") + " " + line);
}
});
child.stderr?.on("data", (data) => {
const lines = data.toString().trim().split("\n");
for (const line of lines) {
if (line.trim()) console.log(color.cyan("[backend]") + " " + line);
}
});
child.on("error", (err) => {
log.err(`后端启动失败: ${err.message}`);
});
child.on("exit", (code) => {
if (code !== 0 && code !== null) {
log.warn(`后端进程退出 (code: ${code})`);
}
});
return waitForPort(PORTS.backend, "后端", 60, 3000);
}
/** 启动管理端前端 (React + Vite) */
async function startAdmin() {
log.info("清理旧管理端进程...");
killPort(PORTS.admin, "管理端前端");
log.info("启动管理端前端 (React + Vite)...");
const adminDir = resolve(ROOT, "apps", "web");
// 先检查依赖是否安装
if (!existsSync(resolve(adminDir, "node_modules"))) {
log.info("管理端依赖未安装,正在安装...");
execSync("pnpm install", { cwd: adminDir, stdio: "inherit" });
}
const child = spawn("pnpm dev", {
cwd: adminDir,
shell: true,
stdio: "pipe",
});
child.stdout?.on("data", (data) => {
const lines = data.toString().trim().split("\n");
for (const line of lines) {
if (line.trim()) console.log(color.green("[admin]") + " " + line);
}
});
child.stderr?.on("data", (data) => {
const lines = data.toString().trim().split("\n");
for (const line of lines) {
if (line.trim()) console.log(color.green("[admin]") + " " + line);
}
});
child.on("error", (err) => {
log.err(`管理端启动失败: ${err.message}`);
});
return waitForPort(PORTS.admin, "管理端", 20, 2000);
}
/** 启动学生端 Flutter Web */
async function startApp() {
log.info("清理旧学生端进程...");
killPort(PORTS.app, "学生端前端");
log.info("编译并启动 Flutter Web...");
const appDir = resolve(ROOT, "app");
const flutterBin = IS_WIN ? "D:\\flutter\\bin\\flutter.bat" : "flutter";
const child = spawn(`${flutterBin} run -d chrome --web-port=${PORTS.app} --dart-define=API_BASE_URL=http://localhost:${PORTS.backend}/api/v1 --dart-define=SSE_BASE_URL=http://localhost:${PORTS.backend}/api/v1`, {
cwd: appDir,
shell: true,
stdio: "pipe",
});
child.stdout?.on("data", (data) => {
const lines = data.toString().trim().split("\n");
for (const line of lines) {
if (line.trim()) console.log(color.yellow("[app]") + " " + line);
}
});
child.stderr?.on("data", (data) => {
const lines = data.toString().trim().split("\n");
for (const line of lines) {
if (line.trim()) console.log(color.yellow("[app]") + " " + line);
}
});
child.on("error", (err) => {
log.err(`学生端启动失败: ${err.message}`);
});
return waitForPort(PORTS.app, "学生端", 40, 3000);
}
// ===== 主流程 =====
function printBanner(services) {
console.log();
console.log(
color.bold(color.cyan(" ╔══════════════════════════════════════════╗"))
);
console.log(
color.bold(color.cyan(" ║ 暖记 — 开发环境启动 ║"))
);
console.log(
color.bold(color.cyan(" ╚══════════════════════════════════════════╝"))
);
console.log();
}
function printSummary() {
console.log();
console.log(color.bold(color.green(" ═══ 暖记开发环境已启动 ═══")));
console.log();
console.log(` 后端 API: http://localhost:${PORTS.backend}`);
console.log(` 管理端: http://localhost:${PORTS.admin} (admin/admin123)`);
console.log(` 学生端: http://localhost:${PORTS.app}`);
console.log();
console.log(color.gray(" 停止所有服务: pnpm start:stop"));
console.log();
}
async function main() {
const arg = process.argv[2] || "all";
const services =
arg === "all"
? ["backend", "admin", "app"]
: [arg];
const validServices = ["backend", "admin", "app", "all"];
if (!validServices.includes(arg)) {
log.err(`未知参数: ${arg}`);
console.log("用法: pnpm start:dev [all|backend|admin|app]");
process.exit(1);
}
printBanner(services);
// 前置检查
log.info("检查依赖...");
if (!runChecks(services)) {
log.err("前置检查失败,请修复后重试");
process.exit(1);
}
// 按顺序启动(后端先启动,前端依赖 API
if (services.includes("backend")) {
const ok = await startBackend();
if (!ok) {
log.err("后端启动失败,终止");
process.exit(1);
}
}
// 管理端和学生端可以并行启动
const frontendPromises = [];
if (services.includes("admin")) {
frontendPromises.push(startAdmin());
}
if (services.includes("app")) {
frontendPromises.push(startApp());
}
if (frontendPromises.length > 0) {
const results = await Promise.all(frontendPromises);
const failed = results.filter((r) => !r).length;
if (failed > 0) {
log.warn(`${failed} 个前端服务启动失败`);
}
}
// 打印汇总
if (arg === "all") {
printSummary();
}
// 保持进程存活(不退出 Node让子进程继续运行
// Ctrl+C 会触发 SIGINT我们在下面处理清理
process.on("SIGINT", () => {
console.log();
log.info("收到 SIGINT正在停止所有服务...");
killPort(PORTS.backend, "后端");
killPort(PORTS.admin, "管理端前端");
killPort(PORTS.app, "学生端前端");
if (IS_WIN) {
try { execSync("taskkill /F /IM erp-server.exe", { stdio: "pipe" }); } catch {}
}
log.ok("所有服务已停止");
process.exit(0);
});
// 保持 Node 进程不退出
setInterval(() => {}, 60_000);
}
main().catch((err) => {
log.err(`启动失败: ${err.message}`);
process.exit(1);
});