225 lines
8.8 KiB
JavaScript
225 lines
8.8 KiB
JavaScript
'use strict';
|
||
|
||
/*
|
||
* 人员数据治理 API 配置测试服务,仅使用 Node.js 内置模块。
|
||
* 启动此文件后默认监听 http://127.0.0.1:18080,可通过 HOST、PORT 环境变量修改。
|
||
* 所有人员信息均为模拟数据,每次启动生成相同的 100 条记录,不连接数据库。
|
||
*
|
||
* API 配置示例(地址中的主机需保证 Java 服务能够访问):
|
||
* Token 地址:http://127.0.0.1:18080/api/token,POST,JSON 请求体 {}
|
||
* Access_Token 路径:data->access_token
|
||
* Expires_in 路径:data->expires_in,单位:SECONDS
|
||
* 数据地址:http://127.0.0.1:18080/api/persons,GET 或 POST
|
||
* 返回码路径:code,成功返回值:0,消息路径:msg
|
||
* 人员数组路径:data->list,总数路径:data->total
|
||
* 数据字段按人员接口字段表生成,全部为字符串;码值为模拟值。
|
||
* 字段映射示例:gh→loginname、xm→username、xb→sex、sjhm→mobile、yx→email、lxrq→arrivalAtSchoolDate。
|
||
* gh、grbsm 均为唯一标识,可按实际目标中间表字段配置。
|
||
* 分页 Query 参数:pageNumber=${页码}、pageSize=${数量},例如起始页码 1、数量 20。
|
||
* POST JSON 也可使用 {"pageNumber":"${页码}","pageSize":"${数量}"}。
|
||
* 不配置分页参数时一次返回全部 100 条数据。
|
||
* 默认不要求认证;MOCK_REQUIRE_TOKEN=1 时,数据接口要求以下任意一种凭证:
|
||
* Header Authorization: Bearer ${Access_Token}
|
||
* Query 或 JSON 参数 access_token: ${Access_Token}
|
||
*/
|
||
|
||
const http = require('node:http');
|
||
const { URL } = require('node:url');
|
||
|
||
const HOST = process.env.HOST || '127.0.0.1';
|
||
const PORT = Number(process.env.PORT || 18080);
|
||
const REQUIRE_TOKEN = process.env.MOCK_REQUIRE_TOKEN === '1';
|
||
const TOKEN = 'person-governance-mock-token';
|
||
const MAX_BODY_BYTES = 1024 * 1024;
|
||
|
||
const persons = Array.from({ length: 100 }, (_, index) => {
|
||
const number = index + 1;
|
||
const suffix = String(number).padStart(3, '0');
|
||
const department = index % 5;
|
||
const departmentCode = `MOCK-D${department + 1}`;
|
||
const departmentName = ['计算机学院', '机械工程学院', '经济管理学院', '外国语学院', '教务处'][department];
|
||
const female = number % 2 === 0;
|
||
const retired = number % 10 === 0;
|
||
const birthYear = retired ? 1960 + index % 5 : 1980 + index % 15;
|
||
const month = String(index % 12 + 1).padStart(2, '0');
|
||
const day = String(index % 28 + 1).padStart(2, '0');
|
||
const leader = number % 20 === 0;
|
||
return {
|
||
xm: `测试人员${suffix}`,
|
||
gh: `MOCK${suffix}`,
|
||
bmmz: departmentCode,
|
||
bm: departmentName,
|
||
zcszbm: departmentName,
|
||
zcszbmmz: departmentCode,
|
||
nl: String(2026 - birthYear),
|
||
jg: '江苏南京',
|
||
jgmz: '320100',
|
||
xb: female ? '女' : '男',
|
||
xbmz: female ? '2' : '1',
|
||
csrq: `${birthYear}-${month}-${day}`,
|
||
mz: '汉族',
|
||
mzmz: '01',
|
||
gjmz: '156',
|
||
gj: '中国',
|
||
zzmm: number % 3 === 0 ? '中共党员' : '群众',
|
||
zzmmmz: number % 3 === 0 ? '01' : '13',
|
||
zjlx: '测试证件',
|
||
zjlxmz: 'MOCK',
|
||
zjhm: `MOCK-ID-${suffix}`,
|
||
lxrq: `2020-${month}-01`,
|
||
zgqk: retired ? '退休' : '在岗',
|
||
zgqkmz: retired ? '02' : '01',
|
||
ryxz: retired ? '离退休人员' : '在职人员',
|
||
ryxzmz: retired ? '02' : '01',
|
||
yrxs: '事业编制',
|
||
yrxsmz: '01',
|
||
qzjz: '全职',
|
||
qzjzmz: '01',
|
||
xrzw: leader ? '部门负责人' : '教师',
|
||
zwjb: leader ? '正处级' : '无',
|
||
zwjbmz: leader ? '01' : '00',
|
||
rxzsj: '2022-09-01',
|
||
zgxl: number % 3 === 0 ? '硕士研究生' : '博士研究生',
|
||
zgxlmz: number % 3 === 0 ? '14' : '11',
|
||
rkxymz: departmentCode,
|
||
rkxy: departmentName,
|
||
gwlb: '专业技术岗',
|
||
gwlbmz: '01',
|
||
gwdj: '专业技术七级',
|
||
gwdjmz: '07',
|
||
gwmc: '教学科研岗',
|
||
gwmcmz: '01',
|
||
grjsdj: '无',
|
||
grjsdjmz: '00',
|
||
grdjsj: '',
|
||
sjhm: `00000000${suffix}`,
|
||
rysfmz: '01',
|
||
rysf: '教职工',
|
||
daszdw: '测试学校档案馆',
|
||
grbsm: `MOCK-PERSON-${suffix}`,
|
||
gwxz: '固定岗位',
|
||
gwxzmz: '01',
|
||
dezc: '无',
|
||
dezcmz: '00',
|
||
dezcjb: '无',
|
||
dezcjbmz: '00',
|
||
sfbmfzr: leader ? '是' : '否',
|
||
sfbmfzrmz: leader ? '1' : '0',
|
||
xldfgbm: '',
|
||
xldfgbmmz: '',
|
||
zcldfzbmmz: leader ? departmentCode : '',
|
||
zcldfzbm: leader ? departmentName : '',
|
||
zjsj: '2022-09-01',
|
||
xzw2: '',
|
||
zw2rzsj: '',
|
||
xzw3: '',
|
||
xzw4: '',
|
||
fdylb: '非辅导员',
|
||
fdylbmz: '00',
|
||
yx: `mock${suffix}@example.com`,
|
||
lxdh: `00000000${suffix}`,
|
||
qq: String(900000000 + number),
|
||
gz: '教学'
|
||
};
|
||
});
|
||
|
||
function reply(response, status, code, msg, data = null) {
|
||
response.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
|
||
response.end(JSON.stringify({ code, msg, data }));
|
||
}
|
||
|
||
function readBody(request) {
|
||
return new Promise((resolve, reject) => {
|
||
const chunks = [];
|
||
let bytes = 0;
|
||
let tooLarge = false;
|
||
request.on('data', chunk => {
|
||
bytes += chunk.length;
|
||
if (bytes > MAX_BODY_BYTES) {
|
||
if (!tooLarge) {
|
||
tooLarge = true;
|
||
reject(Object.assign(new Error('请求体超过 1MB'), { status: 413 }));
|
||
}
|
||
return;
|
||
}
|
||
chunks.push(chunk);
|
||
});
|
||
request.on('error', reject);
|
||
request.on('end', () => {
|
||
if (tooLarge) return;
|
||
const body = Buffer.concat(chunks).toString('utf8');
|
||
if (!body.trim()) return resolve({});
|
||
try {
|
||
const contentType = (request.headers['content-type'] || '').split(';')[0].trim();
|
||
const value = contentType === 'application/x-www-form-urlencoded'
|
||
? Object.fromEntries(new URLSearchParams(body)) : JSON.parse(body);
|
||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||
throw new Error('请求体必须为对象');
|
||
}
|
||
resolve(value);
|
||
} catch (error) {
|
||
reject(Object.assign(new Error('请求体须为 JSON 对象或表单参数'), { status: 400 }));
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function positiveInteger(value, fallback, name) {
|
||
if (value === undefined) return fallback;
|
||
if (!/^\d+$/.test(String(value)) || !Number.isSafeInteger(Number(value)) || Number(value) < 1) {
|
||
throw Object.assign(new Error(`${name} 必须为正整数`), { status: 400 });
|
||
}
|
||
return Number(value);
|
||
}
|
||
|
||
const server = http.createServer(async (request, response) => {
|
||
try {
|
||
const url = new URL(request.url, 'http://localhost');
|
||
if (!['/api/token', '/api/persons'].includes(url.pathname)) {
|
||
request.resume();
|
||
return reply(response, 404, 404, '接口不存在');
|
||
}
|
||
if (!['GET', 'POST'].includes(request.method)) {
|
||
request.resume();
|
||
response.setHeader('Allow', 'GET, POST');
|
||
return reply(response, 405, 405, '仅支持 GET 和 POST');
|
||
}
|
||
const body = await readBody(request);
|
||
const params = { ...Object.fromEntries(url.searchParams), ...body };
|
||
if (url.pathname === '/api/token') {
|
||
return reply(response, 200, 0, '成功', {
|
||
access_token: TOKEN, expires_in: 7200, token_type: 'Bearer'
|
||
});
|
||
}
|
||
if (REQUIRE_TOKEN && request.headers.authorization !== `Bearer ${TOKEN}`
|
||
&& params.access_token !== TOKEN) {
|
||
return reply(response, 401, 401, 'Token 无效或未提供');
|
||
}
|
||
const pageNumber = positiveInteger(params.pageNumber ?? params.page, 1, 'pageNumber');
|
||
const pageSize = positiveInteger(params.pageSize, 100, 'pageSize');
|
||
if (pageSize > 1000) {
|
||
return reply(response, 400, 400, 'pageSize 不能超过 1000');
|
||
}
|
||
const start = (pageNumber - 1) * pageSize;
|
||
return reply(response, 200, 0, '成功', {
|
||
list: persons.slice(start, start + pageSize),
|
||
total: persons.length,
|
||
pageNumber,
|
||
pageSize
|
||
});
|
||
} catch (error) {
|
||
const status = error.status || 500;
|
||
reply(response, status, status, status === 500 ? '测试服务处理失败' : error.message);
|
||
}
|
||
});
|
||
|
||
server.on('error', error => {
|
||
console.error(`测试服务启动失败:${error.message}`);
|
||
process.exitCode = 1;
|
||
});
|
||
|
||
server.listen(PORT, HOST, () => {
|
||
console.log(`人员治理测试服务:http://${HOST}:${PORT},已预填充 ${persons.length} 条模拟数据`);
|
||
console.log('Token 接口:/api/token;人员接口:/api/persons');
|
||
});
|