feat: 羚牛 BI 报表服务初始版本
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful

- Hono + TypeScript 后端,连接 MySQL 数据库
- React + Vite + Tailwind 前端
- 车辆资产实时汇总(按车型/品牌型号分组)
- 本周交车/还车/替换统计(关联业务单据)
- 车牌号详情弹窗
- Dockerfile + Woodpecker CI 流水线

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-03-26 14:02:49 +08:00
commit 0cc5024132
23 changed files with 5783 additions and 0 deletions

View File

@@ -0,0 +1,40 @@
import dotenv from 'dotenv';
dotenv.config();
import mysql from 'mysql2/promise';
async function main() {
const pool = mysql.createPool({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
});
const [rows] = await pool.query(`
SELECT
CONCAT(
IFNULL(dic_brand.dic_name,''),
'-',
IFNULL(dic_type.dic_name,''),
'-',
IFNULL(truck.color,''),
IF(dic_asc.dic_name='外租', IFNULL(truck.rent_from_company,''), '')
) AS tag,
COUNT(*) AS cnt
FROM tab_truck truck
LEFT JOIN tab_dic dic_type ON dic_type.parent_code='dic_truck_type' AND dic_type.dic_code=truck.model AND dic_type.is_deleted=0
LEFT JOIN tab_dic dic_brand ON dic_brand.parent_code='dic_vehicle_brand' AND dic_brand.dic_code=truck.brand AND dic_brand.is_deleted=0
LEFT JOIN tab_dic dic_asc ON dic_asc.parent_code='dic_truck_ascription_status' AND dic_asc.dic_code=truck.ascription_status AND dic_asc.is_deleted=0
WHERE truck.is_deleted=0 AND truck.is_operation=1
GROUP BY tag
ORDER BY cnt DESC
`);
for (const r of rows as any[]) {
console.log(`[${String(r.cnt).padStart(3)}] ${r.tag}`);
}
await pool.end();
}
main();

54
scripts/check-schema.ts Normal file
View File

@@ -0,0 +1,54 @@
import dotenv from 'dotenv';
dotenv.config();
import mysql from 'mysql2/promise';
async function main() {
const pool = mysql.createPool({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
});
// Check tab_truck columns for time-related fields
const [truckCols] = await pool.query(`
SELECT COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA='lingniu_prod3' AND TABLE_NAME='tab_truck'
AND (COLUMN_NAME LIKE '%time%' OR COLUMN_NAME LIKE '%date%' OR COLUMN_NAME LIKE '%status%' OR COLUMN_NAME LIKE '%create%' OR COLUMN_NAME LIKE '%update%' OR COLUMN_NAME LIKE '%delete%' OR COLUMN_NAME LIKE '%operation%')
ORDER BY ORDINAL_POSITION
`);
console.log('=== tab_truck time/status columns ===');
for (const c of truckCols as any[]) {
console.log(` ${c.COLUMN_NAME} (${c.DATA_TYPE}) — ${c.COLUMN_COMMENT || ''}`);
}
// Check for status change/history tables
const [tables] = await pool.query(`
SELECT TABLE_NAME, TABLE_COMMENT
FROM information_schema.TABLES
WHERE TABLE_SCHEMA='lingniu_prod3'
AND (TABLE_NAME LIKE '%log%' OR TABLE_NAME LIKE '%history%' OR TABLE_NAME LIKE '%change%' OR TABLE_NAME LIKE '%record%' OR TABLE_NAME LIKE '%status%')
ORDER BY TABLE_NAME
`);
console.log('\n=== Related history/log tables ===');
for (const t of tables as any[]) {
console.log(` ${t.TABLE_NAME}${t.TABLE_COMMENT || ''}`);
}
// Check tab_truck_status_info structure
const [statusCols] = await pool.query(`
SELECT COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA='lingniu_prod3' AND TABLE_NAME='tab_truck_status_info'
ORDER BY ORDINAL_POSITION
`);
console.log('\n=== tab_truck_status_info columns ===');
for (const c of statusCols as any[]) {
console.log(` ${c.COLUMN_NAME} (${c.DATA_TYPE}) — ${c.COLUMN_COMMENT || ''}`);
}
await pool.end();
}
main();

66
scripts/check-status.ts Normal file
View File

@@ -0,0 +1,66 @@
import dotenv from 'dotenv';
dotenv.config();
import mysql from 'mysql2/promise';
async function main() {
const pool = mysql.createPool({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
});
const [statusRows] = await pool.query(`
SELECT dic_status.dic_name AS status_label, truck.truck_rent_status AS status_code, COUNT(*) AS cnt
FROM tab_truck truck
LEFT JOIN tab_dic dic_status
ON dic_status.parent_code = 'dic_truck_rent_status'
AND dic_status.dic_code = truck.truck_rent_status
AND dic_status.is_deleted = 0
WHERE truck.is_deleted = 0 AND truck.is_operation = 1
GROUP BY dic_status.dic_name, truck.truck_rent_status
`);
console.log('=== Rental Status ===');
console.log(JSON.stringify(statusRows, null, 2));
const [ownerRows] = await pool.query(`
SELECT dic.dic_name AS label, truck.ascription_status AS code, COUNT(*) AS cnt
FROM tab_truck truck
LEFT JOIN tab_dic dic
ON dic.parent_code = 'dic_truck_ascription_status'
AND dic.dic_code = truck.ascription_status
AND dic.is_deleted = 0
WHERE truck.is_deleted = 0 AND truck.is_operation = 1
GROUP BY dic.dic_name, truck.ascription_status
`);
console.log('=== Ownership Status ===');
console.log(JSON.stringify(ownerRows, null, 2));
const [regionRows] = await pool.query(`
SELECT info.province, info.city, COUNT(*) AS cnt
FROM tab_truck truck
LEFT JOIN tab_truck_remote_sync_realtime_info info ON info.id = truck.id
WHERE truck.is_deleted = 0 AND truck.is_operation = 1
GROUP BY info.province, info.city
ORDER BY cnt DESC
LIMIT 20
`);
console.log('=== Top Regions ===');
console.log(JSON.stringify(regionRows, null, 2));
const [modelRows] = await pool.query(`
SELECT dic_type.dic_name AS model_label, dic_brand.dic_name AS brand_label, COUNT(*) AS cnt
FROM tab_truck truck
LEFT JOIN tab_dic dic_type ON dic_type.parent_code = 'dic_truck_type' AND dic_type.dic_code = truck.model AND dic_type.is_deleted = 0
LEFT JOIN tab_dic dic_brand ON dic_brand.parent_code = 'dic_vehicle_brand' AND dic_brand.dic_code = truck.brand AND dic_brand.is_deleted = 0
WHERE truck.is_deleted = 0 AND truck.is_operation = 1
GROUP BY dic_type.dic_name, dic_brand.dic_name
ORDER BY cnt DESC
`);
console.log('=== Models ===');
console.log(JSON.stringify(modelRows, null, 2));
await pool.end();
}
main();

42
scripts/check-tags.ts Normal file
View File

@@ -0,0 +1,42 @@
import dotenv from 'dotenv';
dotenv.config();
import mysql from 'mysql2/promise';
async function main() {
const pool = mysql.createPool({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
});
const [rows] = await pool.query(`
SELECT
CONCAT(
IFNULL(dic_brand.dic_name,''),
'-',
IFNULL(dic_type.dic_name,''),
'-',
IFNULL(truck.color,''),
IF(dic_asc.dic_name='外租', IFNULL(truck.rent_from_company,''), '')
) AS tag,
dic_brand.dic_name AS brand,
dic_type.dic_name AS model_label,
truck.color AS color,
dic_asc.dic_name AS ownership,
truck.rent_from_company AS rent_company,
COUNT(*) AS cnt
FROM tab_truck truck
LEFT JOIN tab_dic dic_type ON dic_type.parent_code='dic_truck_type' AND dic_type.dic_code=truck.model AND dic_type.is_deleted=0
LEFT JOIN tab_dic dic_brand ON dic_brand.parent_code='dic_vehicle_brand' AND dic_brand.dic_code=truck.brand AND dic_brand.is_deleted=0
LEFT JOIN tab_dic dic_asc ON dic_asc.parent_code='dic_truck_ascription_status' AND dic_asc.dic_code=truck.ascription_status AND dic_asc.is_deleted=0
WHERE truck.is_deleted=0 AND truck.is_operation=1
GROUP BY tag, dic_brand.dic_name, dic_type.dic_name, truck.color, dic_asc.dic_name, truck.rent_from_company
ORDER BY dic_type.dic_name, cnt DESC
`);
console.log(JSON.stringify(rows, null, 2));
await pool.end();
}
main();

87
scripts/check-weekly.ts Normal file
View File

@@ -0,0 +1,87 @@
import dotenv from 'dotenv';
dotenv.config();
import mysql from 'mysql2/promise';
async function main() {
const pool = mysql.createPool({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
});
// Check rent_status_check table structure
const [checkCols] = await pool.query(`
SELECT COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA='lingniu_prod3' AND TABLE_NAME='tab_truck_rent_status_check'
ORDER BY ORDINAL_POSITION
`);
console.log('=== tab_truck_rent_status_check columns ===');
for (const c of checkCols as any[]) {
console.log(` ${c.COLUMN_NAME} (${c.DATA_TYPE}) — ${c.COLUMN_COMMENT || ''}`);
}
// Weekly stats: is_operation set to 1 this week (newly added to operation)
const [newOp] = await pool.query(`
SELECT COUNT(*) AS cnt FROM tab_truck
WHERE is_deleted=0 AND is_operation=1
AND create_time >= DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE())+2 DAY)
`);
console.log('\n=== This week new (by create_time, since last Saturday) ===');
console.log(JSON.stringify(newOp));
// Pending delivery count (status=7)
const [pending] = await pool.query(`
SELECT COUNT(*) AS cnt FROM tab_truck
WHERE is_deleted=0 AND is_operation=1 AND truck_rent_status=7
`);
console.log('\n=== Pending delivery (status=7) ===');
console.log(JSON.stringify(pending));
// Weekly deliveries (take_date this week)
const [delivered] = await pool.query(`
SELECT COUNT(*) AS cnt FROM tab_truck_status_info
WHERE is_deleted=0
AND take_date >= DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE())+2 DAY)
AND take_date < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE())+2 DAY), INTERVAL 7 DAY)
`);
console.log('\n=== This week delivered (by take_date) ===');
console.log(JSON.stringify(delivered));
// Weekly returns (return_date this week)
const [returned] = await pool.query(`
SELECT COUNT(*) AS cnt FROM tab_truck_status_info
WHERE is_deleted=0
AND return_date >= DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE())+2 DAY)
AND return_date < DATE_ADD(DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE())+2 DAY), INTERVAL 7 DAY)
`);
console.log('\n=== This week returned (by return_date) ===');
console.log(JSON.stringify(returned));
// return_change_record values
const [rcValues] = await pool.query(`
SELECT return_change_record, COUNT(*) AS cnt
FROM tab_truck_status_info
WHERE is_deleted=0
GROUP BY return_change_record
`);
console.log('\n=== return_change_record values ===');
console.log(JSON.stringify(rcValues));
// Check aa_temp table
const [tempCols] = await pool.query(`
SELECT COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA='lingniu_prod3' AND TABLE_NAME='tab_aa_temp_truck_rent_status'
ORDER BY ORDINAL_POSITION
`);
console.log('\n=== tab_aa_temp_truck_rent_status columns ===');
for (const c of tempCols as any[]) {
console.log(` ${c.COLUMN_NAME} (${c.DATA_TYPE}) — ${c.COLUMN_COMMENT || ''}`);
}
await pool.end();
}
main();

66
scripts/check-weekly2.ts Normal file
View File

@@ -0,0 +1,66 @@
import dotenv from 'dotenv';
dotenv.config();
import mysql from 'mysql2/promise';
async function main() {
const pool = mysql.createPool({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
});
// return_change_record dic
const [rcDic] = await pool.query(`
SELECT dic_code, dic_name FROM tab_dic
WHERE parent_code LIKE '%change%' OR parent_code LIKE '%return%'
AND is_deleted=0
ORDER BY parent_code, dic_code
`);
console.log('=== return/change dic ===');
console.log(JSON.stringify(rcDic, null, 2));
// Weekly removed: is_operation changed to 0 this week, or is_deleted set to 1
const [removed] = await pool.query(`
SELECT COUNT(*) AS cnt FROM tab_truck
WHERE (is_deleted=1 OR is_operation=0)
AND update_time >= DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE())+2 DAY)
`);
console.log('\n=== This week removed (is_operation=0 or deleted, by update_time) ===');
console.log(JSON.stringify(removed));
// Weekly new: is_operation set to 1 this week
const [newByUpdate] = await pool.query(`
SELECT COUNT(*) AS cnt FROM tab_truck
WHERE is_deleted=0 AND is_operation=1
AND buy_time >= DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE())+2 DAY)
`);
console.log('\n=== This week new (by buy_time) ===');
console.log(JSON.stringify(newByUpdate));
// Replacements this week: return_change_record=3 means replacement?
const [replaced] = await pool.query(`
SELECT return_change_record, COUNT(*) AS cnt FROM tab_truck_status_info
WHERE is_deleted=0
AND return_change_record IN (2, 3)
AND update_time >= DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE())+2 DAY)
GROUP BY return_change_record
`);
console.log('\n=== This week replace records (rc=2,3 by update_time) ===');
console.log(JSON.stringify(replaced));
// Sample recent take_date
const [recentTake] = await pool.query(`
SELECT si.truck_id, t.plate_number, si.take_date, si.return_date, si.return_change_record
FROM tab_truck_status_info si
JOIN tab_truck t ON t.id = si.truck_id
WHERE si.is_deleted=0 AND si.take_date IS NOT NULL
ORDER BY si.take_date DESC LIMIT 5
`);
console.log('\n=== Recent deliveries ===');
console.log(JSON.stringify(recentTake, null, 2));
await pool.end();
}
main();