fix: 人员数据治理迁移3
This commit is contained in:
+5
@@ -140,6 +140,11 @@ public class PersonApiConfigServiceImpl extends BaseServiceImpl<PersonApiConfig>
|
|||||||
}
|
}
|
||||||
Assert.isTrue(List.of("HTTP_API", "SYSTEM_TABLE").contains(config.getSourceType()), "同步方式不正确");
|
Assert.isTrue(List.of("HTTP_API", "SYSTEM_TABLE").contains(config.getSourceType()), "同步方式不正确");
|
||||||
mappings = mappings == null ? new ArrayList<>() : mappings;
|
mappings = mappings == null ? new ArrayList<>() : mappings;
|
||||||
|
if (Boolean.TRUE.equals(config.getEnabled())) {
|
||||||
|
Assert.hasText(config.getTargetTable(), "启用同步任务前请选择目标表");
|
||||||
|
Assert.isTrue(mappings.stream().anyMatch(mapping -> Strings.isNotBlank(mapping.getSourceField())
|
||||||
|
&& Strings.isNotBlank(mapping.getTargetField())), "启用同步任务前请至少完成一条来源字段与目标字段的映射");
|
||||||
|
}
|
||||||
PersonApiFieldMapping identity = mappings.stream()
|
PersonApiFieldMapping identity = mappings.stream()
|
||||||
.filter(v -> Boolean.TRUE.equals(v.getIdentityFlag()))
|
.filter(v -> Boolean.TRUE.equals(v.getIdentityFlag()))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
|
|||||||
@@ -166,7 +166,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="person-governance-mapping-toolbar-item person-governance-mapping-switch">
|
<div class="person-governance-mapping-toolbar-item person-governance-mapping-switch">
|
||||||
<label>启用同步任务</label>
|
<label>启用同步任务</label>
|
||||||
<el-switch v-model="form.enabled" :disabled="mappings.length===0||cronValidating" @change="handleEnabledChange"></el-switch>
|
<el-switch v-model="form.enabled" :disabled="!hasMappedField||cronValidating" @change="handleEnabledChange"></el-switch>
|
||||||
</div>
|
</div>
|
||||||
<div class="person-governance-mapping-toolbar-item person-governance-task-cron">
|
<div class="person-governance-mapping-toolbar-item person-governance-task-cron">
|
||||||
<label>任务周期</label>
|
<label>任务周期</label>
|
||||||
@@ -258,8 +258,9 @@
|
|||||||
<template v-slot="{row}"><el-switch v-model="row.required"></el-switch></template>
|
<template v-slot="{row}"><el-switch v-model="row.required"></el-switch></template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="唯一标识" width="100">
|
<el-table-column label="唯一标识" width="100">
|
||||||
<template v-slot="{row}">
|
<template v-slot="{row,$index}">
|
||||||
<el-radio v-model="identityRow" :label="row">设为标识</el-radio>
|
<el-radio :value="mappings.indexOf(identityRow)" :label="$index"
|
||||||
|
:disabled="!isIdentityMapping(row)" @input="identityRow=mappings[$event]">设为标识</el-radio>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="80">
|
<el-table-column label="操作" width="80">
|
||||||
@@ -576,6 +577,11 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
isIdentityMapping(row) {
|
||||||
|
return Boolean(row && row.sourceField && row.sourceField.trim()
|
||||||
|
&& row.targetField && row.targetField.trim()
|
||||||
|
&& !['id', 'pullTime'].includes(row.targetField))
|
||||||
|
},
|
||||||
// 弹窗使用独立草稿;启停状态不限制周期编辑,取消不修改配置,未设置时预选每5分钟。
|
// 弹窗使用独立草稿;启停状态不限制周期编辑,取消不修改配置,未设置时预选每5分钟。
|
||||||
openCronDialog() {
|
openCronDialog() {
|
||||||
if (this.formLoading || this.cronValidating || this.cronDialogVisible) {
|
if (this.formLoading || this.cronValidating || this.cronDialogVisible) {
|
||||||
@@ -928,6 +934,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
save() {
|
save() {
|
||||||
|
if (this.form.syncMode && this.form.syncMode !== 'APPEND'
|
||||||
|
&& (!this.mappings.includes(this.identityRow) || !this.isIdentityMapping(this.identityRow))) {
|
||||||
|
this.notifyWarning('请在唯一标识列选择完整的业务字段映射(例如工号 gh);目标字段不能是 id 或 pullTime')
|
||||||
|
this.$set(this, 'activeTab', 'mapping')
|
||||||
|
return
|
||||||
|
}
|
||||||
const formRef = this.form.sourceType === 'SYSTEM_TABLE'
|
const formRef = this.form.sourceType === 'SYSTEM_TABLE'
|
||||||
? this.$refs.systemForm
|
? this.$refs.systemForm
|
||||||
: this.$refs.httpForm
|
: this.$refs.httpForm
|
||||||
@@ -1033,9 +1045,6 @@
|
|||||||
this.$set(this, 'activeTab', 'mapping')
|
this.$set(this, 'activeTab', 'mapping')
|
||||||
}
|
}
|
||||||
this.$set(this, 'mappings', res.data.mappings || [])
|
this.$set(this, 'mappings', res.data.mappings || [])
|
||||||
if (!this.hasMappedField) {
|
|
||||||
this.$set(this.form, 'enabled', false)
|
|
||||||
}
|
|
||||||
const params = (res.data.params || []).map((v) => {
|
const params = (res.data.params || []).map((v) => {
|
||||||
if (!v.requestScope) {
|
if (!v.requestScope) {
|
||||||
this.$set(v, 'requestScope', 'DATA')
|
this.$set(v, 'requestScope', 'DATA')
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
'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');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user