diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..aca33cd --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,7 @@ +## PR Checklist + +- [ ] I have run `docker compose config` and the compose configuration resolves without errors. +- [ ] If I changed `BUS_GATEWAY_VERSION`, I verified whether `mysql8/initdb/03-cbus-init-table.sql` and `mysql8/initdb/04-cbus-init-data.sql` need to be updated for the new version. +- [ ] If I changed `TRACK_MAINTAIN_VERSION`, I verified whether `mysql8/initdb/01-maintain-init-table.sql` and `mysql8/initdb/02-maintain-init-data.sql` need to be updated for the new version. +- [ ] If I changed `mysql8/initdb/*.sql`, I ran `node scripts/check-init-sql.js` and fixed any column/value mismatches. +- [ ] I exported `docker compose config > compose-stack.yaml` and reviewed the diff before submitting. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..cc4fa06 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,117 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Repository Overview + +This is the Docker Compose configuration repository for TranscodeGroup's fleet/vehicle tracking platform. It does not contain application source code; it packages and orchestrates pre-built backend/frontend images and infrastructure services (MySQL, Redis, MongoDB, RabbitMQ, MinIO, nginx, SRS, etc.). + +The repo is deployed to `/home/docker` on target servers. Runtime configuration and compose overrides live in `/home/docker-compose`, and persistent data is written to `/data`. + +## Common Commands + +All compose commands are run from `/home/docker-compose`, where the top-level `compose.yaml` and `.env` files reside. + +```sh +# Validate compose configuration and required environment variables +docker compose config + +# Export resolved compose config for diff review +docker compose config > compose-stack.yaml + +# Start the stack in the background +docker compose up -d + +# Recreate a specific service after changing its config/image +docker compose up -d --force-recreate + +# Follow logs for a service +docker compose logs -f + +# Validate that initdb INSERT statements match CREATE TABLE column counts +node /home/docker/scripts/check-init-sql.js +``` + +When modifying `mysql8/initdb/*.sql`, run `check-init-sql.js` before committing to catch column/value mismatches. + +## High-Level Architecture + +### Modular Compose Layout + +Each top-level directory (`maintain/`, `jtt808/`, `bus/`, `video/`, `mysql8/`, `nginx/`, etc.) contains one or more focused `compose.yml` fragments. These fragments are assembled by the example deployments in `examples/` using Docker Compose `include`: + +- `examples/track-http/compose.yaml` — Tracker V2 single-node HTTP deployment. +- `examples/track-https/compose.yaml` — Tracker V2 single-node HTTPS deployment. +- `examples/bus-http/compose.yaml` / `examples/bus-https/compose.yaml` — Bus platform single-node deployments. +- `examples/video-storage/compose.yaml` / `examples/video-stream/compose.yaml` — Distributed video storage/streaming nodes. + +A deployment typically `include`s shared infrastructure (mysql8, redis, mongodb, rabbitmq, minio), one or more backend service fragments, nginx fragments, and optional video or web-downloader fragments. + +### Two Primary Products + +1. **Tracker V2 (track)** + - `maintain` — Spring Boot backend (`transcodegroup/maintain-server`), the main API and device registry. + - `jtt808` — JT/T 808 device gateway (`transcodegroup/jtt808-server`), handles terminal connections and forwards to `maintain`. + - `video` — SRS + RTP streaming stack for live video, playback, talk, and monitor. + - `nginx` — Reverse proxy and static frontend host for the `track` web app. + +2. **Bus** + - `gateway_808_2019` — JT/T 808 gateway for the Bus product (`transcodegroup/gateway-808-2019`). + - `gateway_web` — Spring Boot web backend (`transcodegroup/gateway-web`). + - `gateway_dispatch` — Spring Boot scheduler (`transcodegroup/gateway-dispatch`). + - `gateway_jsatl12` — Active-safety alarm file gateway (`transcodegroup/gateway-jsatl12`). + - `nginx` — Reverse proxy and static frontend host for the `bus` web app. + +Both products share the same infrastructure services but use different MySQL databases (`maintain` vs `cbus`). + +### Configuration and Data Layout + +- `/home/docker` — This repository. +- `/home/docker-compose/compose.yaml` — Deployment-specific composition, created from an `examples/` template. +- `/home/docker-compose/.env` — Runtime overrides, created from `default.env`. +- `/data` — All persistent runtime data (MySQL data, logs, uploaded files, nginx html, SRS recordings, etc.). +- `/home/docker-compose/bus-override/` and `/home/docker-compose/track-override/` — Frontend override directories. Files here are copied into `/data/nginx/html/` when nginx starts. Use these to override `_app.config.js`, `index-seo.html`, favicons, and logos instead of editing `/data/nginx/html/` directly. +- `/home/docker-compose/token/` — JWT RSA key pairs and `ip2region.xdb`. +- `/home/docker-compose/opt/` — Optional host binaries mounted into containers, e.g. `ffmpeg`, `ffprobe`, and `ifv2mp4` for the jtt808 video-conversion tools. + +### MySQL Initialization + +`mysql8/compose.yml` mounts `mysql8/initdb/` into `/docker-entrypoint-initdb.d`. Scripts run once on the first container startup, before the application connects. The directory is split into table-definition and seed-data files per logical database (`maintain`, `cbus`, etc.). When altering these schemas, update the corresponding seed `INSERT`s and validate with `scripts/check-init-sql.js`. + +### Networking and Service Discovery + +Services within the same compose project resolve each other by service name. Common aliases used in environment variables: + +- `mysql8`, `redis`, `mongodb`, `rabbitmq`, `minio`, `nginx`, `jtt808`, `maintain`, `srs`, `rtp`. + +External access is exposed through `nginx` (ports 80/443 by default) and, for device protocols, directly through `jtt808` / `gateway_808_2019` / `video` RTP ports. + +### Frontend Deployment + +Frontend artifacts are not built here. They are downloaded from CI artifacts using `scripts/teamcity-download-artifact.sh` or deployed from tagged GitHub releases using `scripts/distar-beta-deploy.sh`. See `scripts/README.md` and `jtt808/README.md` for exact commands. + +## Review Rules + +When a pull request changes the following version variables in `default.env` / `default.en.env`, the reviewer must confirm that the corresponding MySQL initialization scripts under `mysql8/initdb/` are still consistent with the new application version: + +- `BUS_GATEWAY_VERSION` — check `mysql8/initdb/03-cbus-init-table.sql` and `mysql8/initdb/04-cbus-init-data.sql`. +- `TRACK_MAINTAIN_VERSION` — check `mysql8/initdb/01-maintain-init-table.sql` and `mysql8/initdb/02-maintain-init-data.sql`. + +If the SQL init files are modified, run `node scripts/check-init-sql.js` to catch column/value mismatches. Any compose change must pass `docker compose config`. + +These rules are also encoded in `.github/pull_request_template.md`. + +## Important Notes + +- Most compose fragments rely on environment variables marked `{?required}` in their image tags or env vars. Always run `docker compose config` before `up` to catch missing values. +- MySQL init scripts only execute on a fresh data volume. To re-run them you must wipe `/data/mysql8/data`, which destroys all data. +- The `100-init-partition-table.sql` script uses `USE maintain;` and `USE cbus;` to create monthly partitions for large tables (position, alarm, depart_arrive). +- HTTPS deployments need a valid certificate path in `.env`. For testing, use the built-in placeholder cert at `${DOCKER_DIR}/nginx/ssl/placeholder`. +- GitHub Actions in `.github/workflows/` only manage project board automation and PR author assignment; they do not build or test this repo. + +## Related Documentation + +- `README.md` / `README.en.md` — Deployment quick-start. +- `scripts/README.md` — Frontend artifact download and distar deployment scripts. +- `jtt808/README.md` — FFmpeg and IFV-to-MP4 tooling setup. +- `default.env` / `default.en.env` — Full list of configurable environment variables. diff --git a/mysql8/initdb/02-maintain-init-data.sql b/mysql8/initdb/02-maintain-init-data.sql index a97842d..88cc087 100644 --- a/mysql8/initdb/02-maintain-init-data.sql +++ b/mysql8/initdb/02-maintain-init-data.sql @@ -218,8 +218,8 @@ INSERT INTO `maintain`.`device_manufacturer` VALUES (1, '43a610ca929d45dea574b11 -- Records of device_product -- ---------------------------- -INSERT INTO `maintain`.`device_product` VALUES (1, '7de049b26def4364a9f3dc3bc60cf029', 'TCG-MDVR', 'TCG-MDVR', '1d3b089c74ca496b8c17cfa77e13a65a', '[\"808-2011\",\"808-2013\",\"808-2016\",\"808-2019\",\"1078-2016\",\"safety-jiangsu\",\"tl\"]', '', 2, 4095, '43a610ca929d45dea574b1122e313e2b', 'TGC', NULL, '', 0, NULL, NULL, 0, '', '[\"TCG-MDVR\"]', 0, '2026-01-21 07:15:23', '2026-01-21 07:16:39'); -INSERT INTO `maintain`.`device_product` VALUES (2, 'bf56842a3d80445c96d705e91320a92a', 'GPS Tracker', 'GPS Tracker', 'ed4d3d9b5eda4dfe9a6cdb1327ec1690', '[\"808-2011\",\"808-2013\",\"808-2019\",\"tg\"]', '', 0, 288, '43a610ca929d45dea574b1122e313e2b', 'TGC', NULL, '', 0, NULL, NULL, 0, '', NULL, 0, '2026-01-21 08:02:53', '2026-01-21 08:02:53'); +INSERT INTO `maintain`.`device_product` VALUES (1, '7de049b26def4364a9f3dc3bc60cf029', 'TCG-MDVR', 'TCG-MDVR', '1d3b089c74ca496b8c17cfa77e13a65a', '[\"808-2011\",\"808-2013\",\"808-2016\",\"808-2019\",\"1078-2016\",\"safety-jiangsu\",\"tl\"]', '', 2, 4095, '43a610ca929d45dea574b1122e313e2b', 'TGC', NULL, '', 0, NULL, NULL, 0, '', '[\"TCG-MDVR\"]', 0, '', '', '2026-01-21 07:15:23', '2026-01-21 07:16:39'); +INSERT INTO `maintain`.`device_product` VALUES (2, 'bf56842a3d80445c96d705e91320a92a', 'GPS Tracker', 'GPS Tracker', 'ed4d3d9b5eda4dfe9a6cdb1327ec1690', '[\"808-2011\",\"808-2013\",\"808-2019\",\"tg\"]', '', 0, 288, '43a610ca929d45dea574b1122e313e2b', 'TGC', NULL, '', 0, NULL, NULL, 0, '', NULL, 0, '', '', '2026-01-21 08:02:53', '2026-01-21 08:02:53'); -- ---------------------------- -- Records of system_msg_template diff --git a/scripts/check-init-sql.js b/scripts/check-init-sql.js new file mode 100644 index 0000000..2516975 --- /dev/null +++ b/scripts/check-init-sql.js @@ -0,0 +1,186 @@ +#!/usr/bin/env node +/** + * 校验 mysql8/initdb 下所有初始化 SQL 的 INSERT VALUES 数量是否与对应 CREATE TABLE 的列数一致。 + * 用法:node scripts/check-init-sql.js + */ + +const fs = require("fs"); +const path = require("path"); + +const base = "mysql8/initdb"; +const files = fs.readdirSync(base).filter(f => f.endsWith(".sql")).sort().map(f => path.join(base, f)); + +// schema.table -> column count +const tables = {}; +const inserts = []; + +function norm(name) { + return name.replace(/`/g, "").toLowerCase(); +} + +function getDb(line, currentDb) { + const m = line.match(/^USE\s+`?(\w+)`?\s*;/i); + return m ? norm(m[1]) : currentDb; +} + +// Split body by top-level commas (not inside parentheses or quotes) +function splitTopLevel(text) { + const parts = []; + let current = ""; + let depth = 0; + for (let i = 0; i < text.length; i++) { + const ch = text[i]; + if (ch === "'") { + current += ch; + i++; + while (i < text.length && text[i] !== "'") { + if (text[i] === "\\" && i + 1 < text.length) { + current += text[i++]; + } + current += text[i++]; + } + if (i < text.length) current += text[i]; + } else if (ch === "(") { + depth++; + current += ch; + } else if (ch === ")") { + depth--; + current += ch; + } else if (ch === "," && depth === 0) { + parts.push(current.trim()); + current = ""; + } else { + current += ch; + } + } + if (current.trim()) parts.push(current.trim()); + return parts; +} + +function defaultDb(file) { + const baseName = path.basename(file); + if (baseName.includes("maintain")) return "maintain"; + if (baseName.includes("cbus")) return "cbus"; + if (baseName.includes("gps")) return "gps"; + if (baseName.includes("analytics")) return "analytics"; + return null; +} + +files.forEach(file => { + const text = fs.readFileSync(file, "utf-8"); + const lines = text.split(/\r?\n/); + + let currentDb = defaultDb(file); + + // Parse CREATE TABLE + const createRe = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([\w`.]+)\s*\((.*?)\)\s+ENGINE/sgi; + let m; + while ((m = createRe.exec(text)) !== null) { + const tableName = norm(m[1]); + const schema = tableName.includes(".") ? tableName.split(".")[0] : currentDb; + const shortName = tableName.includes(".") ? tableName.split(".")[1] : tableName; + const key = schema ? `${schema}.${shortName}` : shortName; + const body = m[2]; + const parts = splitTopLevel(body); + let colCount = 0; + for (const s of parts) { + if (!s) continue; + if (/^(PRIMARY\s+KEY|UNIQUE\s+KEY|UNIQUE\s+INDEX|KEY|INDEX|CONSTRAINT|FOREIGN\s+KEY)\b/i.test(s)) continue; + if (/^`\w+`\s+\w+/.test(s)) { + colCount++; + } + } + tables[key] = colCount; + } + + // Parse INSERT INTO (only without explicit column list) + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + currentDb = getDb(line, currentDb); + + const match = line.match(/^INSERT\s+INTO\s+([\w`.]+)\s+VALUES\s+(.*)/i); + if (!match) continue; + const tableName = norm(match[1]); + const schema = tableName.includes(".") ? tableName.split(".")[0] : currentDb; + const shortName = tableName.includes(".") ? tableName.split(".")[1] : tableName; + const key = schema ? `${schema}.${shortName}` : shortName; + + let rest = match[2]; + let j = i; + while (!rest.trim().endsWith(";") && j + 1 < lines.length) { + j++; + rest += " " + lines[j]; + currentDb = getDb(lines[j], currentDb); + } + rest = rest.trim().replace(/;$/, ""); + + // Extract top-level parentheses groups + const groups = []; + let start = -1; + let depth = 0; + for (let k = 0; k < rest.length; k++) { + const ch = rest[k]; + if (ch === "'") { + k++; + while (k < rest.length && rest[k] !== "'") { + if (rest[k] === "\\" && k + 1 < rest.length) k++; + k++; + } + } else if (ch === "(") { + if (depth === 0) start = k; + depth++; + } else if (ch === ")") { + depth--; + if (depth === 0 && start !== -1) { + groups.push(rest.slice(start + 1, k)); + start = -1; + } + } + } + + for (const values of groups) { + let count = 1; + depth = 0; + for (let k = 0; k < values.length; k++) { + const ch = values[k]; + if (ch === "'") { + k++; + while (k < values.length && values[k] !== "'") { + if (values[k] === "\\" && k + 1 < values.length) k++; + k++; + } + } else if (ch === "(") { + depth++; + } else if (ch === ")") { + depth--; + } else if (ch === "," && depth === 0) { + count++; + } + } + inserts.push({file, line: j + 1, table: key, vcount: count}); + } + } +}); + +const mismatches = inserts.filter(ins => tables[ins.table] && tables[ins.table] !== ins.vcount); +const grouped = {}; +mismatches.forEach(ins => { + const key = `${ins.file}:${ins.line}:${ins.table}`; + if (!grouped[key]) grouped[key] = ins; +}); + +if (Object.keys(grouped).length) { + console.log("发现列数不匹配的 INSERT:"); + Object.values(grouped).sort((a,b)=>`${a.file}:${a.line}`.localeCompare(`${b.file}:${b.line}`)).forEach(ins => { + console.log(` ${ins.file}:${ins.line} 表=${ins.table} 列数=${tables[ins.table]} VALUES数=${ins.vcount}`); + }); +} else { + console.log("未发现列数不匹配的 INSERT。"); +} + +const insertedTables = new Set(inserts.map(i => i.table)); +const unknown = [...insertedTables].filter(t => !tables[t]); +if (unknown.length) { + console.log("\n以下 INSERT 找不到对应 CREATE TABLE(可能跨库/表名解析问题):"); + unknown.sort().forEach(t => console.log(` ${t}`)); +}