vue2server 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/eslint.config.js +20 -0
- package/frontEnd/package-lock.json +18392 -0
- package/frontEnd/package.json +47 -0
- package/frontEnd/public/.gitkeep +0 -0
- package/frontEnd/public/index.html +17 -0
- package/frontEnd/src/App.vue +69 -0
- package/frontEnd/src/api/mock.js +5 -0
- package/frontEnd/src/assets/.gitkeep +0 -0
- package/frontEnd/src/components/AppMenu.vue +86 -0
- package/frontEnd/src/main.js +14 -0
- package/frontEnd/src/pages/LineChartPage.vue +573 -0
- package/frontEnd/src/pages/MedalIssueDialog.vue +558 -0
- package/frontEnd/src/pages/MedalSetting.vue +571 -0
- package/frontEnd/src/pages/MeetingListPage.vue +542 -0
- package/frontEnd/src/router/index.js +12 -0
- package/frontEnd/src/router/routes.js +53 -0
- package/frontEnd/src/utils/request.js +28 -0
- package/frontEnd/vue.config.js +8 -0
- package/package.json +54 -0
- package/src/app.ts +88 -0
- package/src/controllers/health.controller.ts +14 -0
- package/src/controllers/medal.controller.ts +46 -0
- package/src/controllers/mock.controller.ts +41 -0
- package/src/middlewares/response.middleware.ts +71 -0
- package/src/routes/api.route.ts +13 -0
- package/src/routes/index.route.ts +5 -0
- package/src/routes/modules/health.route.ts +8 -0
- package/src/routes/modules/medal.route.ts +10 -0
- package/src/routes/modules/mock.route.ts +9 -0
- package/src/services/medal.service.ts +159 -0
- package/src/services/mock.service.ts +46 -0
- package/src/utils/httpError.ts +13 -0
- package/test/health.test.js +12 -0
- package/test/trend.test.js +30 -0
- package/tsconfig.json +15 -0
package/src/app.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import "dotenv/config";
|
|
2
|
+
|
|
3
|
+
import cors from "cors";
|
|
4
|
+
import express, { NextFunction, Request, Response } from "express";
|
|
5
|
+
import helmet from "helmet";
|
|
6
|
+
import morgan from "morgan";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
|
|
10
|
+
import { responseMiddleware } from "./middlewares/response.middleware";
|
|
11
|
+
import router from "./routes/index.route";
|
|
12
|
+
import { HttpError } from "./utils/httpError";
|
|
13
|
+
|
|
14
|
+
const app = express();
|
|
15
|
+
|
|
16
|
+
app.set("trust proxy", true);
|
|
17
|
+
|
|
18
|
+
app.use(helmet());
|
|
19
|
+
app.use(cors({ origin: true }));
|
|
20
|
+
app.use(express.json({ limit: "1mb" }));
|
|
21
|
+
app.use(express.urlencoded({ extended: true }));
|
|
22
|
+
app.use(morgan(process.env.NODE_ENV === "production" ? "combined" : "dev"));
|
|
23
|
+
|
|
24
|
+
app.use(responseMiddleware);
|
|
25
|
+
app.use(router);
|
|
26
|
+
|
|
27
|
+
const distPublic = path.join(__dirname, "public");
|
|
28
|
+
const frontDist = path.join(process.cwd(), "frontEnd", "dist");
|
|
29
|
+
const staticDir = fs.existsSync(distPublic) ? distPublic : frontDist;
|
|
30
|
+
if (fs.existsSync(staticDir)) {
|
|
31
|
+
app.use(express.static(staticDir));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// SPA fallback for non-API GET requests
|
|
35
|
+
app.use((req: Request, res: Response, next: NextFunction) => {
|
|
36
|
+
if (req.method !== "GET") return next();
|
|
37
|
+
if (req.path.startsWith("/api")) return next();
|
|
38
|
+
if (fs.existsSync(path.join(staticDir, "index.html"))) {
|
|
39
|
+
res.sendFile(path.join(staticDir, "index.html"));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
next();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
app.use((req: Request, res: Response) => {
|
|
46
|
+
res.status(404).json({
|
|
47
|
+
error: {
|
|
48
|
+
code: "NOT_FOUND",
|
|
49
|
+
message: `Route not found: ${req.method} ${req.path}`
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => {
|
|
55
|
+
if (err instanceof HttpError) {
|
|
56
|
+
res.status(err.status).json({
|
|
57
|
+
error: {
|
|
58
|
+
code: err.code,
|
|
59
|
+
message: err.message,
|
|
60
|
+
details: err.details ?? null
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
res.status(500).json({
|
|
67
|
+
error: {
|
|
68
|
+
code: "INTERNAL_SERVER_ERROR",
|
|
69
|
+
message: "Unexpected error"
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const PORT = normalizePort(process.env.PORT ?? "3000");
|
|
75
|
+
|
|
76
|
+
if (require.main === module) {
|
|
77
|
+
app.listen(PORT, () => {
|
|
78
|
+
console.log(`服务已运行: http://localhost:${PORT}`);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export default app;
|
|
83
|
+
|
|
84
|
+
function normalizePort(value: string): number {
|
|
85
|
+
const parsed = Number.parseInt(value, 10);
|
|
86
|
+
if (Number.isNaN(parsed) || parsed <= 0) return 3000;
|
|
87
|
+
return parsed;
|
|
88
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Request, Response } from "express";
|
|
2
|
+
|
|
3
|
+
class HealthController {
|
|
4
|
+
getHealth = (_req: Request, res: Response): void => {
|
|
5
|
+
res.json({
|
|
6
|
+
status: "ok",
|
|
7
|
+
uptimeSeconds: Math.floor(process.uptime()),
|
|
8
|
+
timestamp: new Date().toISOString(),
|
|
9
|
+
env: process.env.NODE_ENV ?? "development"
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default new HealthController();
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from "express";
|
|
2
|
+
import * as medalService from "../services/medal.service";
|
|
3
|
+
|
|
4
|
+
class MedalController {
|
|
5
|
+
getMedalList = async (_req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
6
|
+
try {
|
|
7
|
+
const data = await medalService.getMedalList();
|
|
8
|
+
res.json({
|
|
9
|
+
data
|
|
10
|
+
});
|
|
11
|
+
} catch (error) {
|
|
12
|
+
next(error);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
getMedalUserList = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
17
|
+
try {
|
|
18
|
+
const { medalNo } = req.query;
|
|
19
|
+
const data = await medalService.getMedalUserList(medalNo as string);
|
|
20
|
+
res.json({
|
|
21
|
+
data
|
|
22
|
+
});
|
|
23
|
+
} catch (error) {
|
|
24
|
+
next(error);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
getCandidateUserPage = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
29
|
+
try {
|
|
30
|
+
const { page, pageSize, keyword, excludeUserNos } = req.query;
|
|
31
|
+
const data = await medalService.getCandidateUserPage({
|
|
32
|
+
page,
|
|
33
|
+
pageSize,
|
|
34
|
+
keyword,
|
|
35
|
+
excludeUserNos
|
|
36
|
+
});
|
|
37
|
+
res.json({
|
|
38
|
+
data
|
|
39
|
+
});
|
|
40
|
+
} catch (error) {
|
|
41
|
+
next(error);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default new MedalController();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Request, Response } from "express";
|
|
2
|
+
|
|
3
|
+
import { buildMockUser, buildYearTrend } from "../services/mock.service";
|
|
4
|
+
class MockController {
|
|
5
|
+
getMockUser = (_req: Request, res: Response): void => {
|
|
6
|
+
res.json({
|
|
7
|
+
data: buildMockUser()
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
getTrend = (req: Request, res: Response): void => {
|
|
11
|
+
const rawYear = (req.body as { year?: unknown })?.year;
|
|
12
|
+
const year = typeof rawYear === "string" ? Number.parseInt(rawYear, 10) : typeof rawYear === "number" ? rawYear : NaN;
|
|
13
|
+
|
|
14
|
+
if (!Number.isInteger(year) || year < 1970 || year > 9999) {
|
|
15
|
+
res.status(400).json({
|
|
16
|
+
code: "INVALID_YEAR",
|
|
17
|
+
message: "year必须是1970-9999之间的整数"
|
|
18
|
+
});
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const fullData = buildYearTrend(year);
|
|
23
|
+
const now = new Date();
|
|
24
|
+
const currentYear = now.getUTCFullYear();
|
|
25
|
+
if (year === currentYear) {
|
|
26
|
+
const y = currentYear;
|
|
27
|
+
//const m = String(now.getUTCMonth() + 1).padStart(2, "0");
|
|
28
|
+
const m = '02'
|
|
29
|
+
const d = String(now.getUTCDate()).padStart(2, "0");
|
|
30
|
+
const today = `${y}-${m}-${d}`;
|
|
31
|
+
console.log(today);
|
|
32
|
+
const truncated = fullData.filter((item) => item.label <= today);
|
|
33
|
+
res.json({ data: truncated });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
res.json({ data: fullData });
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default new MockController();
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { NextFunction, Request, Response } from "express";
|
|
2
|
+
|
|
3
|
+
type StandardResponse = {
|
|
4
|
+
code: number | string;
|
|
5
|
+
msg: string;
|
|
6
|
+
data: unknown;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function isStandardResponse(value: unknown): value is StandardResponse {
|
|
10
|
+
if (value === null || typeof value !== "object") return false;
|
|
11
|
+
return "code" in value && "msg" in value && "data" in value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function extractSuccessData(body: unknown): unknown {
|
|
15
|
+
if (body === null || typeof body !== "object") return body;
|
|
16
|
+
const record = body as Record<string, unknown>;
|
|
17
|
+
const keys = Object.keys(record);
|
|
18
|
+
if (keys.length === 1 && keys[0] === "data") return record.data;
|
|
19
|
+
return body;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildErrorResponse(statusCode: number, body: unknown): StandardResponse {
|
|
23
|
+
let code: number | string = statusCode;
|
|
24
|
+
let msg = "Request failed";
|
|
25
|
+
let data: unknown = null;
|
|
26
|
+
|
|
27
|
+
if (body && typeof body === "object") {
|
|
28
|
+
const record = body as Record<string, unknown>;
|
|
29
|
+
const error = record.error && typeof record.error === "object" ? (record.error as Record<string, unknown>) : null;
|
|
30
|
+
|
|
31
|
+
const rawCode = record.code ?? error?.code;
|
|
32
|
+
if (typeof rawCode === "string" || typeof rawCode === "number") code = rawCode;
|
|
33
|
+
|
|
34
|
+
const rawMsg = record.msg ?? record.message ?? error?.message;
|
|
35
|
+
if (typeof rawMsg === "string" && rawMsg.trim().length > 0) msg = rawMsg;
|
|
36
|
+
|
|
37
|
+
if ("data" in record) data = record.data;
|
|
38
|
+
else if ("details" in record) data = record.details;
|
|
39
|
+
else if (error && "details" in error) data = error.details;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { code, msg, data };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function responseMiddleware(_req: Request, res: Response, next: NextFunction): void {
|
|
46
|
+
const originalJson = res.json.bind(res);
|
|
47
|
+
const originalSend = res.send.bind(res);
|
|
48
|
+
|
|
49
|
+
res.json = ((body: unknown) => {
|
|
50
|
+
if (isStandardResponse(body)) return originalJson(body);
|
|
51
|
+
|
|
52
|
+
if (res.statusCode >= 400) {
|
|
53
|
+
return originalJson(buildErrorResponse(res.statusCode, body));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const wrapped: StandardResponse = {
|
|
57
|
+
code: 0,
|
|
58
|
+
msg: "success",
|
|
59
|
+
data: extractSuccessData(body)
|
|
60
|
+
};
|
|
61
|
+
return originalJson(wrapped);
|
|
62
|
+
}) as Response["json"];
|
|
63
|
+
|
|
64
|
+
res.send = ((body?: unknown) => {
|
|
65
|
+
if (body && typeof body === "object") return res.json(body);
|
|
66
|
+
return originalSend(body as never);
|
|
67
|
+
}) as Response["send"];
|
|
68
|
+
|
|
69
|
+
next();
|
|
70
|
+
}
|
|
71
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
|
|
3
|
+
import healthRouter from "./modules/health.route";
|
|
4
|
+
import mockRouter from "./modules/mock.route";
|
|
5
|
+
import medalRouter from "./modules/medal.route";
|
|
6
|
+
|
|
7
|
+
const router = Router();
|
|
8
|
+
|
|
9
|
+
router.use('/health',healthRouter);
|
|
10
|
+
router.use("/mock", mockRouter);
|
|
11
|
+
router.use("/medal", medalRouter);
|
|
12
|
+
|
|
13
|
+
export default router;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import medalController from "../../controllers/medal.controller";
|
|
3
|
+
|
|
4
|
+
const router = Router();
|
|
5
|
+
|
|
6
|
+
router.get("/list", medalController.getMedalList);
|
|
7
|
+
router.get("/users", medalController.getMedalUserList);
|
|
8
|
+
router.get("/candidates", medalController.getCandidateUserPage);
|
|
9
|
+
|
|
10
|
+
export default router;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
export const getMedalList = async () => {
|
|
2
|
+
return [
|
|
3
|
+
{
|
|
4
|
+
id: 1,
|
|
5
|
+
activaFlag: "Y",
|
|
6
|
+
confeMode: "4",
|
|
7
|
+
confeSubKno: "SORG",
|
|
8
|
+
medalObjName: "授予分行机构",
|
|
9
|
+
createTm: "2025-11-26T11:17:09.000Z",
|
|
10
|
+
creatorNo: "046083",
|
|
11
|
+
filePicUrl: "/getUrl?fileId=3d6457ca-3d05-463f-8acd-d5a56d773c40",
|
|
12
|
+
limitQtyFlag: "Y",
|
|
13
|
+
limitTmFlag: "Y",
|
|
14
|
+
medalDesc: "",
|
|
15
|
+
medalName: "阿萨德",
|
|
16
|
+
medalNo: "SORG419ABFE1ADE9",
|
|
17
|
+
medalQty: 13,
|
|
18
|
+
issuedQty: 2,
|
|
19
|
+
tmUnitType: "D",
|
|
20
|
+
updateStaffNo: "046083",
|
|
21
|
+
updateTm: "2025-12-30T13:10:44.000Z",
|
|
22
|
+
wearDurat: 12
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 2,
|
|
26
|
+
activaFlag: "Y",
|
|
27
|
+
confeMode: "4",
|
|
28
|
+
confeSubKno: "CU",
|
|
29
|
+
medalObjName: "授予客户经理",
|
|
30
|
+
createTm: "2025-11-26T11:17:09.000Z",
|
|
31
|
+
creatorNo: "046083",
|
|
32
|
+
filePicUrl: "/getUrl?fileId=7661069b-aeb9-450b-aa78-3192d53e4e10",
|
|
33
|
+
limitQtyFlag: "N",
|
|
34
|
+
limitTmFlag: "N",
|
|
35
|
+
medalDesc: "",
|
|
36
|
+
medalName: "1111",
|
|
37
|
+
medalNo: "CU41990D4501B5",
|
|
38
|
+
medalQty: 0,
|
|
39
|
+
issuedQty: 0,
|
|
40
|
+
tmUnitType: "",
|
|
41
|
+
updateStaffNo: "046083",
|
|
42
|
+
updateTm: "2025-12-30T13:10:44.000Z",
|
|
43
|
+
wearDurat: 0
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 3,
|
|
47
|
+
activaFlag: "N",
|
|
48
|
+
confeMode: "4",
|
|
49
|
+
confeSubKno: "CU",
|
|
50
|
+
medalObjName: "授予客户经理",
|
|
51
|
+
createTm: "2025-11-26T11:17:09.000Z",
|
|
52
|
+
creatorNo: "046083",
|
|
53
|
+
filePicUrl: "/getUrl?fileId=3d6457ca-3d05-463f-8acd-d5a56d773c40",
|
|
54
|
+
limitQtyFlag: "Y",
|
|
55
|
+
limitTmFlag: "Y",
|
|
56
|
+
medalDesc: "",
|
|
57
|
+
medalName: "测试勋章",
|
|
58
|
+
medalNo: "CU419917C23D9C",
|
|
59
|
+
medalQty: 999,
|
|
60
|
+
issuedQty: 0,
|
|
61
|
+
tmUnitType: "D",
|
|
62
|
+
updateStaffNo: "046083",
|
|
63
|
+
updateTm: "2025-11-26T11:10:44.000Z",
|
|
64
|
+
wearDurat: 12
|
|
65
|
+
}
|
|
66
|
+
];
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type MedalUser = {
|
|
70
|
+
id: string;
|
|
71
|
+
displayName: string;
|
|
72
|
+
userNo: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
let cachedUserPool: MedalUser[] | null = null;
|
|
76
|
+
|
|
77
|
+
function getUserPool(): MedalUser[] {
|
|
78
|
+
if (cachedUserPool) return cachedUserPool;
|
|
79
|
+
|
|
80
|
+
const surnames = ["赵", "钱", "孙", "李", "周", "吴", "郑", "王", "冯", "陈", "褚", "卫", "蒋", "沈", "韩", "杨", "朱", "秦", "尤", "许", "何", "吕", "施", "张", "孔", "曹", "严", "华", "金", "魏", "陶", "姜"];
|
|
81
|
+
const givenNames = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "子", "文", "宇", "浩", "欣", "婷", "磊", "楠", "敏", "静", "伟", "强", "明", "杰", "晨", "洋", "雪", "雨", "峰", "超"];
|
|
82
|
+
const list: MedalUser[] = [];
|
|
83
|
+
const count = 200;
|
|
84
|
+
for (let i = 0; i < count; i += 1) {
|
|
85
|
+
const displayName = `${surnames[i % surnames.length]}${givenNames[i % givenNames.length]}${givenNames[(i + 7) % givenNames.length]}`;
|
|
86
|
+
const userNo = String(200000 + i).padStart(6, "0");
|
|
87
|
+
list.push({
|
|
88
|
+
id: userNo,
|
|
89
|
+
displayName,
|
|
90
|
+
userNo
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
cachedUserPool = list;
|
|
95
|
+
return list;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const getMedalUserList = async (medalNo?: string) => {
|
|
99
|
+
// 这里可以根据 medalNo 过滤或获取特定数据,目前仅作为示例接收参数
|
|
100
|
+
// console.log("Fetching users for medal:", medalNo);
|
|
101
|
+
const list = getUserPool();
|
|
102
|
+
|
|
103
|
+
// 模拟已发放用户(取前10个)
|
|
104
|
+
const issuedUsers = list.slice(0, 10);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
allUsers: list,
|
|
108
|
+
issuedUsers
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
function toInt(value: unknown, fallback: number): number {
|
|
113
|
+
if (typeof value === "number" && Number.isFinite(value)) return Math.floor(value);
|
|
114
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
115
|
+
const parsed = Number.parseInt(value, 10);
|
|
116
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
117
|
+
}
|
|
118
|
+
return fallback;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function toStringArray(value: unknown): string[] {
|
|
122
|
+
if (Array.isArray(value)) return value.map(String).map((s) => s.trim()).filter(Boolean);
|
|
123
|
+
if (typeof value === "string") {
|
|
124
|
+
const trimmed = value.trim();
|
|
125
|
+
if (!trimmed) return [];
|
|
126
|
+
return trimmed.split(",").map((s) => s.trim()).filter(Boolean);
|
|
127
|
+
}
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const getCandidateUserPage = async (input: {
|
|
132
|
+
page?: unknown;
|
|
133
|
+
pageSize?: unknown;
|
|
134
|
+
keyword?: unknown;
|
|
135
|
+
excludeUserNos?: unknown;
|
|
136
|
+
}) => {
|
|
137
|
+
const pageSize = Math.max(1, Math.min(200, toInt(input.pageSize, 10)));
|
|
138
|
+
const page = Math.max(1, toInt(input.page, 1));
|
|
139
|
+
const keyword = typeof input.keyword === "string" ? input.keyword.trim() : "";
|
|
140
|
+
const excludeUserNos = new Set(toStringArray(input.excludeUserNos));
|
|
141
|
+
|
|
142
|
+
const pool = getUserPool();
|
|
143
|
+
const filtered = pool.filter((u) => {
|
|
144
|
+
if (excludeUserNos.has(u.userNo)) return false;
|
|
145
|
+
if (!keyword) return true;
|
|
146
|
+
return u.displayName.includes(keyword) || u.userNo.includes(keyword);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const total = filtered.length;
|
|
150
|
+
const start = (page - 1) * pageSize;
|
|
151
|
+
const list = filtered.slice(start, start + pageSize);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
list,
|
|
155
|
+
total,
|
|
156
|
+
page,
|
|
157
|
+
pageSize
|
|
158
|
+
};
|
|
159
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const FIRST_NAMES = ["Alex", "Sam", "Taylor", "Jordan", "Morgan", "Casey", "Riley", "Avery"];
|
|
4
|
+
const LAST_NAMES = ["Li", "Wang", "Zhang", "Chen", "Liu", "Yang", "Zhao", "Huang"];
|
|
5
|
+
|
|
6
|
+
export function buildMockUser(): { id: string; name: string; email: string; age: number; createdAt: string } {
|
|
7
|
+
const firstName = pick(FIRST_NAMES);
|
|
8
|
+
const lastName = pick(LAST_NAMES);
|
|
9
|
+
const age = randomInt(18, 60);
|
|
10
|
+
const createdAt = new Date(Date.now() - randomInt(0, 90) * 24 * 60 * 60 * 1000).toISOString();
|
|
11
|
+
const name = `${firstName} ${lastName}`;
|
|
12
|
+
const email = `${firstName.toLowerCase()}.${lastName.toLowerCase()}@example.com`;
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
id: randomUUID(),
|
|
16
|
+
name,
|
|
17
|
+
email,
|
|
18
|
+
age,
|
|
19
|
+
createdAt
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function buildYearTrend(year: number): Array<{ label: string; value: number | string }> {
|
|
24
|
+
const result: Array<{ label: string; value: number | string }> = [];
|
|
25
|
+
let current = new Date(Date.UTC(year, 0, 1));
|
|
26
|
+
while (current.getUTCFullYear() === year) {
|
|
27
|
+
const y = current.getUTCFullYear();
|
|
28
|
+
const m = String(current.getUTCMonth() + 1).padStart(2, "0");
|
|
29
|
+
const d = String(current.getUTCDate()).padStart(2, "0");
|
|
30
|
+
const label = `${y}-${m}-${d}`;
|
|
31
|
+
const value = Math.random() > 0.9 ? "_" : randomInt(0, 100000000000);
|
|
32
|
+
result.push({ label, value });
|
|
33
|
+
current.setUTCDate(current.getUTCDate() + 1);
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function pick<T>(items: T[]): T {
|
|
39
|
+
return items[Math.floor(Math.random() * items.length)];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function randomInt(minInclusive: number, maxInclusive: number): number {
|
|
43
|
+
const min = Math.ceil(minInclusive);
|
|
44
|
+
const max = Math.floor(maxInclusive);
|
|
45
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
46
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class HttpError extends Error {
|
|
2
|
+
readonly status: number;
|
|
3
|
+
readonly code: string;
|
|
4
|
+
readonly details?: unknown;
|
|
5
|
+
|
|
6
|
+
constructor(input: { status: number; code: string; message: string; details?: unknown }) {
|
|
7
|
+
super(input.message);
|
|
8
|
+
this.name = "HttpError";
|
|
9
|
+
this.status = input.status;
|
|
10
|
+
this.code = input.code;
|
|
11
|
+
this.details = input.details;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const test = require("node:test");
|
|
3
|
+
|
|
4
|
+
const request = require("supertest");
|
|
5
|
+
|
|
6
|
+
const app = require("../dist/app").default;
|
|
7
|
+
|
|
8
|
+
test("GET /health returns ok", async () => {
|
|
9
|
+
const res = await request(app).get("/api/health").expect(200);
|
|
10
|
+
assert.equal(res.body.code, 0);
|
|
11
|
+
assert.equal(res.body.data.status, "ok");
|
|
12
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const assert = require("node:assert/strict");
|
|
2
|
+
const test = require("node:test");
|
|
3
|
+
const request = require("supertest");
|
|
4
|
+
|
|
5
|
+
const app = require("../dist/app").default;
|
|
6
|
+
|
|
7
|
+
function isLeapYear(year) {
|
|
8
|
+
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
test("POST /api/mock/trend returns full year daily data", async () => {
|
|
12
|
+
const year = 2024;
|
|
13
|
+
const res = await request(app).post("/api/mock/trend").send({ year }).expect(200);
|
|
14
|
+
assert.equal(res.body.code, 0);
|
|
15
|
+
const data = res.body.data;
|
|
16
|
+
assert.ok(Array.isArray(data), "data should be an array");
|
|
17
|
+
assert.equal(data[0].label, `${year}-01-01`);
|
|
18
|
+
assert.equal(data[data.length - 1].label, `${year}-12-31`);
|
|
19
|
+
assert.equal(data.length, isLeapYear(year) ? 366 : 365);
|
|
20
|
+
// Check that all values are either number or "_"
|
|
21
|
+
for (const item of data) {
|
|
22
|
+
assert.ok(typeof item.value === "number" || item.value === "_", `Value ${item.value} at ${item.label} is invalid`);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("POST /api/mock/trend rejects invalid year", async () => {
|
|
27
|
+
const res = await request(app).post("/api/mock/trend").send({ year: "abc" }).expect(400);
|
|
28
|
+
assert.equal(res.body.code, "INVALID_YEAR");
|
|
29
|
+
});
|
|
30
|
+
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "CommonJS",
|
|
5
|
+
"moduleResolution": "Node",
|
|
6
|
+
"rootDir": "src",
|
|
7
|
+
"outDir": "dist",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"types": ["node"],
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*.ts"]
|
|
15
|
+
}
|