优化后端构建方式,修改首页报表 bug
This commit is contained in:
@@ -11,22 +11,15 @@ WORKDIR /app
|
|||||||
# Create logs directory
|
# Create logs directory
|
||||||
RUN mkdir -p /app/logs
|
RUN mkdir -p /app/logs
|
||||||
|
|
||||||
# Install dependencies (including devDependencies for build)
|
# Install production dependencies only
|
||||||
COPY package.json pnpm-lock.yaml ./
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
RUN corepack enable pnpm && pnpm install --frozen-lockfile
|
RUN corepack enable pnpm && pnpm install --frozen-lockfile --prod
|
||||||
|
|
||||||
# Copy application code
|
# Copy pre-built application
|
||||||
COPY tsconfig.json drizzle.config.ts ./
|
COPY dist/ ./dist/
|
||||||
COPY src/ ./src/
|
|
||||||
|
|
||||||
# Build TypeScript → dist/
|
|
||||||
RUN pnpm build
|
|
||||||
|
|
||||||
# Remove devDependencies after build
|
|
||||||
RUN pnpm prune --prod
|
|
||||||
|
|
||||||
# Expose port
|
# Expose port
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
# Run the compiled application
|
# Run the compiled application
|
||||||
CMD ["node", "--conditions", "@zod/source", "dist/app.js"]
|
CMD ["node", "dist/app.js"]
|
||||||
|
|||||||
@@ -6,36 +6,36 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/app.js",
|
"main": "dist/app.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx watch --conditions @zod/source src/app.ts",
|
"dev": "tsx watch src/app.ts",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"start": "node --conditions @zod/source dist/app.js",
|
"start": "node dist/app.js",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:push": "drizzle-kit push",
|
"db:push": "drizzle-kit push",
|
||||||
"db:studio": "drizzle-kit studio"
|
"db:studio": "drizzle-kit studio"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^5.2",
|
"@aws-sdk/client-s3": "^3.1098",
|
||||||
|
"axios": "^1.19",
|
||||||
"cors": "^2.8",
|
"cors": "^2.8",
|
||||||
"mysql2": "^3.23",
|
"dotenv": "^17.4",
|
||||||
"drizzle-orm": "^0.45",
|
"drizzle-orm": "^0.45",
|
||||||
|
"express": "^5.2",
|
||||||
|
"ioredis": "^5.11",
|
||||||
"jsonwebtoken": "^9.0",
|
"jsonwebtoken": "^9.0",
|
||||||
"multer": "^2.2",
|
"multer": "^2.2",
|
||||||
"axios": "^1.19",
|
"mysql2": "^3.23",
|
||||||
"dotenv": "^17.4",
|
|
||||||
"zod": "^3.25",
|
|
||||||
"@aws-sdk/client-s3": "^3.1098",
|
|
||||||
"winston": "^3.19",
|
"winston": "^3.19",
|
||||||
"ioredis": "^5.11"
|
"zod": "^4.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tsx": "^4.23",
|
|
||||||
"typescript": "^7.0",
|
|
||||||
"drizzle-kit": "^0.31",
|
|
||||||
"@types/express": "^5.0",
|
|
||||||
"@types/cors": "^2.8",
|
"@types/cors": "^2.8",
|
||||||
|
"@types/express": "^5.0",
|
||||||
"@types/jsonwebtoken": "^9.0",
|
"@types/jsonwebtoken": "^9.0",
|
||||||
"@types/multer": "^2.2",
|
"@types/multer": "^2.2",
|
||||||
"@types/node": "^22.0"
|
"@types/node": "^22.0",
|
||||||
|
"drizzle-kit": "^0.31",
|
||||||
|
"tsx": "^4.23",
|
||||||
|
"typescript": "^7.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+5
-5
@@ -42,8 +42,8 @@ importers:
|
|||||||
specifier: ^3.19
|
specifier: ^3.19
|
||||||
version: 3.19.0
|
version: 3.19.0
|
||||||
zod:
|
zod:
|
||||||
specifier: ^3.25
|
specifier: ^4.4.3
|
||||||
version: 3.25.0
|
version: 4.4.3
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/cors':
|
'@types/cors':
|
||||||
specifier: ^2.8
|
specifier: ^2.8
|
||||||
@@ -1482,8 +1482,8 @@ packages:
|
|||||||
wrappy@1.0.2:
|
wrappy@1.0.2:
|
||||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||||
|
|
||||||
zod@3.25.0:
|
zod@4.4.3:
|
||||||
resolution: {integrity: sha512-ficnZKUW0mlNivqeJkosTEkGbJ6NKCtSaOHGx5aXbtfeWMdRyzXLbAIn19my4C/KB7WPY/p9vlGPt+qpOp6c4Q==}
|
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
@@ -2778,4 +2778,4 @@ snapshots:
|
|||||||
|
|
||||||
wrappy@1.0.2: {}
|
wrappy@1.0.2: {}
|
||||||
|
|
||||||
zod@3.25.0: {}
|
zod@4.4.3: {}
|
||||||
|
|||||||
@@ -46,4 +46,4 @@ echo "按 Ctrl+C 停止服务"
|
|||||||
echo "================================================"
|
echo "================================================"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
exec node --conditions @zod/source dist/app.js
|
exec node dist/app.js
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ router.post('/api/blacklist/', async (req: Request, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const parsed = BlackListCreateSchema.safeParse(req.body);
|
const parsed = BlackListCreateSchema.safeParse(req.body);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
const errors = parsed.error.errors.map((e) => e.message).join('; ');
|
const errors = parsed.error.issues.map((e: { message: string }) => e.message).join('; ');
|
||||||
res.json(ApiResponse.error(errors));
|
res.json(ApiResponse.error(errors));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -174,7 +174,7 @@ router.put('/api/blacklist/:id', async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
const parsed = BlackListUpdateSchema.safeParse(req.body);
|
const parsed = BlackListUpdateSchema.safeParse(req.body);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
const errors = parsed.error.errors.map((e) => e.message).join('; ');
|
const errors = parsed.error.issues.map((e: { message: string }) => e.message).join('; ');
|
||||||
res.json(ApiResponse.error(errors));
|
res.json(ApiResponse.error(errors));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,14 @@ import { authMiddleware } from '../middleware/auth.js';
|
|||||||
|
|
||||||
const router: RouterType = Router();
|
const router: RouterType = Router();
|
||||||
|
|
||||||
|
/** 格式化为本地日期字符串 YYYY-MM-DD(避免 toISOString 的 UTC 偏移) */
|
||||||
|
function toLocalDateStr(d: Date): string {
|
||||||
|
const y = d.getFullYear();
|
||||||
|
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(d.getDate()).padStart(2, '0');
|
||||||
|
return `${y}-${m}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
// 所有 dashboard 接口需登录
|
// 所有 dashboard 接口需登录
|
||||||
router.use(authMiddleware);
|
router.use(authMiddleware);
|
||||||
|
|
||||||
@@ -135,7 +143,7 @@ router.get('/api/dashboard/device-daily', async (req: Request, res: Response) =>
|
|||||||
const dateList: string[] = [];
|
const dateList: string[] = [];
|
||||||
const tmp = new Date(startDate);
|
const tmp = new Date(startDate);
|
||||||
while (tmp <= now) {
|
while (tmp <= now) {
|
||||||
dateList.push(tmp.toISOString().slice(0, 10));
|
dateList.push(toLocalDateStr(tmp));
|
||||||
tmp.setDate(tmp.getDate() + 1);
|
tmp.setDate(tmp.getDate() + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,14 +236,14 @@ router.get('/api/dashboard/active-daily', async (req: Request, res: Response) =>
|
|||||||
const dateList: string[] = [];
|
const dateList: string[] = [];
|
||||||
const tmp = new Date(startDate);
|
const tmp = new Date(startDate);
|
||||||
while (tmp <= now) {
|
while (tmp <= now) {
|
||||||
dateList.push(tmp.toISOString().slice(0, 10));
|
dateList.push(toLocalDateStr(tmp));
|
||||||
tmp.setDate(tmp.getDate() + 1);
|
tmp.setDate(tmp.getDate() + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelSet = new Set<string>();
|
const modelSet = new Set<string>();
|
||||||
const dataMap = new Map<string, Record<string, number>>();
|
const dataMap = new Map<string, Record<string, number>>();
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const dateStr = String(row.active_date).slice(0, 10);
|
const dateStr = toLocalDateStr(new Date(row.active_date));
|
||||||
modelSet.add(row.model);
|
modelSet.add(row.model);
|
||||||
if (!dataMap.has(dateStr)) dataMap.set(dateStr, {});
|
if (!dataMap.has(dateStr)) dataMap.set(dateStr, {});
|
||||||
dataMap.get(dateStr)![row.model] = Number(row.count);
|
dataMap.get(dateStr)![row.model] = Number(row.count);
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ router.post('/api/ota/', async (req: Request, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const parsed = OtaCreateSchema.safeParse(req.body);
|
const parsed = OtaCreateSchema.safeParse(req.body);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
const errors = parsed.error.errors.map((e) => e.message).join('; ');
|
const errors = parsed.error.issues.map((e: { message: string }) => e.message).join('; ');
|
||||||
res.json(ApiResponse.error(errors));
|
res.json(ApiResponse.error(errors));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -241,7 +241,7 @@ router.put('/api/ota/:ota_id', async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
const parsed = OtaUpdateSchema.safeParse(req.body);
|
const parsed = OtaUpdateSchema.safeParse(req.body);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
const errors = parsed.error.errors.map((e) => e.message).join('; ');
|
const errors = parsed.error.issues.map((e: { message: string }) => e.message).join('; ');
|
||||||
res.json(ApiResponse.error(errors));
|
res.json(ApiResponse.error(errors));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ router.post('/api/ota-target-device/', async (req: Request, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const parsed = OtaTargetDeviceCreateSchema.safeParse(req.body);
|
const parsed = OtaTargetDeviceCreateSchema.safeParse(req.body);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
const errors = parsed.error.errors.map((e) => e.message).join('; ');
|
const errors = parsed.error.issues.map((e: { message: string }) => e.message).join('; ');
|
||||||
res.json(ApiResponse.error(errors));
|
res.json(ApiResponse.error(errors));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -185,7 +185,7 @@ router.put('/api/ota-target-device/:id', async (req: Request, res: Response) =>
|
|||||||
|
|
||||||
const parsed = OtaTargetDeviceUpdateSchema.safeParse(req.body);
|
const parsed = OtaTargetDeviceUpdateSchema.safeParse(req.body);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
const errors = parsed.error.errors.map((e) => e.message).join('; ');
|
const errors = parsed.error.issues.map((e: { message: string }) => e.message).join('; ');
|
||||||
res.json(ApiResponse.error(errors));
|
res.json(ApiResponse.error(errors));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,4 +20,4 @@ echo "Building TypeScript..."
|
|||||||
pnpm build
|
pnpm build
|
||||||
|
|
||||||
echo "Starting server..."
|
echo "Starting server..."
|
||||||
exec node --conditions @zod/source dist/app.js
|
exec node dist/app.js
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"customConditions": ["@zod/source"],
|
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
|
|||||||
@@ -173,12 +173,7 @@ watch(deviceDays, () => {
|
|||||||
// ===== 设备日活趋势 =====
|
// ===== 设备日活趋势 =====
|
||||||
const activeDays = ref(30);
|
const activeDays = ref(30);
|
||||||
const activeLoading = ref(false);
|
const activeLoading = ref(false);
|
||||||
const activeLatest = ref({ date: '', total: 0 });
|
const activeTodayTotal = ref(0);
|
||||||
|
|
||||||
const activeLatestLabel = computed(() => {
|
|
||||||
if (activeDays.value === 1) return `${activeLatest.value.date} 总活跃`;
|
|
||||||
return `${activeLatest.value.date.slice(5)} 总日活`;
|
|
||||||
});
|
|
||||||
|
|
||||||
function buildActiveChartOptions(dates: string[], series: Array<{ name: string; data: number[] }>): ECOption {
|
function buildActiveChartOptions(dates: string[], series: Array<{ name: string; data: number[] }>): ECOption {
|
||||||
const isHourly = activeDays.value === 1;
|
const isHourly = activeDays.value === 1;
|
||||||
@@ -257,7 +252,13 @@ async function loadActiveDaily() {
|
|||||||
activeLoading.value = true;
|
activeLoading.value = true;
|
||||||
const { data, error } = await fetchGetActiveDaily(activeDays.value);
|
const { data, error } = await fetchGetActiveDaily(activeDays.value);
|
||||||
if (!error && data) {
|
if (!error && data) {
|
||||||
activeLatest.value = data.latest;
|
// 今天模式(按小时):累加所有小时;多天模式:取末尾(今天)
|
||||||
|
const isHourly = activeDays.value === 1;
|
||||||
|
const lastIdx = (data.dates?.length || 1) - 1;
|
||||||
|
activeTodayTotal.value = (data.series || []).reduce((sum: number, s: { data: number[] }) => {
|
||||||
|
if (isHourly) return sum + s.data.reduce((a: number, b: number) => a + b, 0);
|
||||||
|
return sum + (s.data[lastIdx] || 0);
|
||||||
|
}, 0);
|
||||||
lastActiveData.value = { dates: data.dates, series: data.series };
|
lastActiveData.value = { dates: data.dates, series: data.series };
|
||||||
await updateActiveChart(() => buildActiveChartOptions(data.dates, data.series));
|
await updateActiveChart(() => buildActiveChartOptions(data.dates, data.series));
|
||||||
}
|
}
|
||||||
@@ -359,7 +360,7 @@ onMounted(() => {
|
|||||||
<template #header>活跃趋势</template>
|
<template #header>活跃趋势</template>
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<NSpace align="center" :size="12">
|
<NSpace align="center" :size="12">
|
||||||
<NTag type="success" size="small">{{ activeLatestLabel }} {{ activeLatest.total }}</NTag>
|
<NTag type="success" size="small">今日日活 {{ activeTodayTotal }}</NTag>
|
||||||
<NRadioGroup v-model:value="activeDays" size="small">
|
<NRadioGroup v-model:value="activeDays" size="small">
|
||||||
<NRadioButton :value="1">今天</NRadioButton>
|
<NRadioButton :value="1">今天</NRadioButton>
|
||||||
<NRadioButton :value="7">近7天</NRadioButton>
|
<NRadioButton :value="7">近7天</NRadioButton>
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ FRONTEND_PATHS=(
|
|||||||
)
|
)
|
||||||
|
|
||||||
BACKEND_PATHS=(
|
BACKEND_PATHS=(
|
||||||
./backend/src/
|
./backend/dist/
|
||||||
./backend/package.json
|
./backend/package.json
|
||||||
./backend/pnpm-lock.yaml
|
./backend/pnpm-lock.yaml
|
||||||
|
./backend/pnpm-workspace.yaml
|
||||||
)
|
)
|
||||||
|
|
||||||
COMPOSE_PATHS=(
|
COMPOSE_PATHS=(
|
||||||
@@ -50,7 +51,7 @@ show_help() {
|
|||||||
|
|
||||||
目标:
|
目标:
|
||||||
frontend, f 同步前端 dist/(不含 nginx.conf)
|
frontend, f 同步前端 dist/(不含 nginx.conf)
|
||||||
backend, b 同步后端 src/、package.json、pnpm-lock.yaml(不含 Dockerfile)
|
backend, b 同步后端 dist/、package.json、pnpm-lock.yaml(不含 Dockerfile)
|
||||||
compose, c 同步 docker-compose.yml
|
compose, c 同步 docker-compose.yml
|
||||||
all, a 同步以上全部(默认)
|
all, a 同步以上全部(默认)
|
||||||
(路径) 同步指定文件或目录,可传多个
|
(路径) 同步指定文件或目录,可传多个
|
||||||
|
|||||||
Reference in New Issue
Block a user