1.调整导出逻辑

2.增加导出导入日志管理页面
3.增加配置表
This commit is contained in:
2026-07-14 17:19:05 +08:00
parent e83c92ec66
commit d27cf48571
131 changed files with 5056 additions and 6428 deletions

View File

@@ -0,0 +1,31 @@
package com.njcn.relational.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
/**
* MyBatis-Plus 分页插件配置
* 必须配置,否则分页不生效
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件(根据数据库类型选择)
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.ORACLE);
// 或 DbType.MYSQL / DbType.POSTGRE_SQL 等
// 设置最大单页限制默认500-1表示不限制
paginationInnerInterceptor.setMaxLimit(1000L);
// 设置溢出总页数后是否跳转到第一页默认false
paginationInnerInterceptor.setOverflow(false);
interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}
}

View File

@@ -16,4 +16,5 @@ public class Security extends WebSecurityConfigurerAdapter {
}
}

View File

@@ -3,22 +3,21 @@ package com.njcn.relational.controller;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.util.StrUtil;
import com.njcn.relational.service.SyncTableConfigService;
import com.njcn.relational.service.SyncTableParseService;
import com.njcn.relational.service.SyncTwoDmExportService;
import com.njcn.relational.service.SyncThreeDmParseService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -34,15 +33,19 @@ import java.util.Map;
@Slf4j
public class DataTransportController {
private final SyncTableConfigService syncTableConfigService;
private final SyncTwoDmExportService syncTwoDmExportService;
private final SyncTableParseService syncTableParseService;
private final SyncThreeDmParseService syncThreeDmParseService;
@Value("${spring.profiles.active:query}")
private String serverType;
@GetMapping("export")
public void export(@RequestParam(required = false)String date){
if(!serverType.contains("query")){
return;
}
if(StrUtil.isBlank(date)){
date = LocalDate.now().minusDays(1).format(DateTimeFormatter.ofPattern(DatePattern.PURE_DATE_PATTERN));
}else {
@@ -51,11 +54,14 @@ public class DataTransportController {
return;
}
}
syncTableConfigService.syncAllFullTables(date);
syncTwoDmExportService.syncAllFullTables(date);
}
@GetMapping("import")
public void importData(@RequestParam(required = false)String date){
if(!serverType.contains("insert")){
return;
}
if(StrUtil.isBlank(date)){
date = LocalDate.now().minusDays(1).format(DateTimeFormatter.ofPattern(DatePattern.PURE_DATE_PATTERN));
}else {
@@ -65,7 +71,7 @@ public class DataTransportController {
}
}
try {
syncTableParseService.syncFullTables(date);
syncThreeDmParseService.syncFullTables(date);
} catch (Exception e) {
e.printStackTrace();
}
@@ -73,18 +79,24 @@ public class DataTransportController {
@GetMapping("test")
public void test(){
syncTableConfigService.test();
if(!serverType.contains("query")){
return;
}
syncTwoDmExportService.test();
}
@GetMapping("testR")
public void testR(){
syncTableParseService.testR();
if(!serverType.contains("insert")){
return;
}
syncThreeDmParseService.testR();
}
@GetMapping("getRemoteTwoAll")
public List<String> getRemoteTwoAll(){
try {
return syncTableConfigService.getPendingFiles();
return syncTwoDmExportService.getPendingFiles();
} catch (Exception e) {
log.error("最终捕获二区异常",e);
throw new RuntimeException(e);
@@ -94,17 +106,24 @@ public class DataTransportController {
@GetMapping("getRemoteAll")
public List<String> getAll(){
try {
return syncTableParseService.getPendingFiles();
if("query_up".equals(serverType)){
return syncTwoDmExportService.getPendingFiles();
}else if("insert_up".equals(serverType)){
return syncThreeDmParseService.getPendingFiles();
}else {
return new ArrayList<>();
}
} catch (Exception e) {
log.error("最终捕获异常",e);
throw new RuntimeException(e);
return new ArrayList<>();
}
}
@GetMapping("removeTwo")
public String removeTwo(@RequestParam Integer a){
try {
syncTableConfigService.cleanLocalFiles(a);
syncTwoDmExportService.cleanLocalFiles(a);
} catch (Exception e) {
throw new RuntimeException(e);
}
@@ -115,7 +134,7 @@ public class DataTransportController {
@GetMapping("removeThree")
public String removeThree(@RequestParam Integer a){
try {
syncTableParseService.cleanLocalFiles(a);
syncThreeDmParseService.cleanLocalFiles(a);
} catch (Exception e) {
throw new RuntimeException(e);
}

View File

@@ -1,5 +1,6 @@
package com.njcn.relational.controller;
import com.njcn.relational.pojo.dto.DcloudSyncQuery;
import com.njcn.relational.service.DcloudBusService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
@@ -18,8 +19,27 @@ public class DcloudBusController {
private final DcloudBusService dcloudBusService;
@GetMapping("/sync")
@GetMapping("/sync1")
public void syncData() {
dcloudBusService.syncDataToDb();
DcloudSyncQuery query = DcloudSyncQuery.buildPlantDefault(null, null, null, null);
dcloudBusService.syncDataToDb(query);
}
@GetMapping("/sync2")
public void syncData2() {
DcloudSyncQuery query = DcloudSyncQuery.buildPlantChangZhan(null, null, null, null);
dcloudBusService.syncDataToDb(query);
}
@GetMapping("/sync3")
public void syncData3() {
DcloudSyncQuery query = DcloudSyncQuery.buildPlantBdB(null, null, null, null);
dcloudBusService.syncDataToDb(query);
}
@GetMapping("/sync4")
public void syncData4() {
DcloudSyncQuery query = DcloudSyncQuery.buildPlantBdC(null, null, null, null);
dcloudBusService.syncDataToDb(query);
}
}

View File

@@ -0,0 +1,37 @@
package com.njcn.relational.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* @Author: cdf
* @CreateTime: 2026-06-28
* @Description:
*/
@Controller
@Slf4j
public class IndexController {
@GetMapping("/")
public String index() {
log.info("访问根路径,跳转到首页");
return "forward:/pages/index.html";
}
@GetMapping("/index")
public String indexPage() {
log.info("访问 /index跳转到首页");
return "forward:/pages/index.html";
}
@GetMapping("/config")
public String configPage() {
return "forward:/pages/config.html";
}
@GetMapping("/logs")
public String logs() {
return "forward:/pages/logs.html";
}
}

View File

@@ -0,0 +1,86 @@
package com.njcn.relational.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.relational.pojo.po.SGConPlantB;
import com.njcn.relational.service.SGConPlantBService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/plant")
@RequiredArgsConstructor
public class SGConPlantBController {
private final SGConPlantBService sgConPlantBService;
/**
* 新增场站
*/
@PostMapping("/add")
public Boolean add(@RequestBody SGConPlantB entity) {
return sgConPlantBService.save(entity);
}
/**
* 根据ID删除
*/
@DeleteMapping("/remove/{id}")
public Boolean remove(@PathVariable String id) {
return sgConPlantBService.removeById(id);
}
/**
* 批量删除
*/
@DeleteMapping("/batch")
public Boolean batchRemove(@RequestBody List<String> idList) {
return sgConPlantBService.removeByIds(idList);
}
/**
* 修改场站
*/
@PutMapping("/update")
public Boolean update(@RequestBody SGConPlantB entity) {
return sgConPlantBService.updateById(entity);
}
/**
* 根据ID查询单条
*/
@GetMapping("/get/{id}")
public SGConPlantB getById(@PathVariable String id) {
return sgConPlantBService.getById(id);
}
/**
* 分页查询
*/
@GetMapping("/page")
public IPage<SGConPlantB> page(
@RequestParam(defaultValue = "1") long pageNum,
@RequestParam(defaultValue = "10") long pageSize,
SGConPlantB query
) {
Page<SGConPlantB> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<SGConPlantB> wrapper = new LambdaQueryWrapper<>();
// 可根据业务自行拼接模糊/等值条件示例
wrapper.like(query.getName() != null, SGConPlantB::getName, query.getName());
wrapper.eq(query.getPlantType() != null, SGConPlantB::getPlantType, query.getPlantType());
wrapper.eq(query.getConnectivePgId() != null, SGConPlantB::getConnectivePgId, query.getConnectivePgId());
return sgConPlantBService.page(page, wrapper);
}
/**
* 查询全部列表(不分页)
*/
@GetMapping("/list")
public List<SGConPlantB> list(SGConPlantB query) {
LambdaQueryWrapper<SGConPlantB> wrapper = new LambdaQueryWrapper<>();
wrapper.like(query.getName() != null, SGConPlantB::getName, query.getName());
return sgConPlantBService.list(wrapper);
}
}

View File

@@ -0,0 +1,91 @@
package com.njcn.relational.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.relational.pojo.po.SGConSubstationB;
import com.njcn.relational.service.SGConSubstationBService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/substation")
@RequiredArgsConstructor
public class SGConSubstationBController {
private final SGConSubstationBService substationBService;
/**
* 新增变电站
*/
@PostMapping("/add")
public Boolean add(@RequestBody SGConSubstationB entity) {
return substationBService.save(entity);
}
/**
* 根据ID删除
*/
@DeleteMapping("/remove/{id}")
public Boolean remove(@PathVariable String id) {
return substationBService.removeById(id);
}
/**
* 批量删除
*/
@DeleteMapping("/batch")
public Boolean batchRemove(@RequestBody List<String> idList) {
return substationBService.removeByIds(idList);
}
/**
* 修改变电站
*/
@PutMapping("/update")
public Boolean update(@RequestBody SGConSubstationB entity) {
return substationBService.updateById(entity);
}
/**
* 根据ID单条查询
*/
@GetMapping("/get/{id}")
public SGConSubstationB getById(@PathVariable String id) {
return substationBService.getById(id);
}
/**
* 分页查询
*/
@GetMapping("/page")
public IPage<SGConSubstationB> page(
@RequestParam(defaultValue = "1") long pageNum,
@RequestParam(defaultValue = "10") long pageSize,
SGConSubstationB query
) {
Page<SGConSubstationB> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<SGConSubstationB> wrapper = new LambdaQueryWrapper<>();
// 常用查询条件示例
wrapper.like(query.getName() != null, SGConSubstationB::getName, query.getName());
wrapper.eq(query.getDccId() != null, SGConSubstationB::getDccId, query.getDccId());
wrapper.eq(query.getPgId() != null, SGConSubstationB::getPgId, query.getPgId());
wrapper.eq(query.getTopAcVoltageType() != null, SGConSubstationB::getTopAcVoltageType, query.getTopAcVoltageType());
wrapper.eq(query.getOperateState() != null, SGConSubstationB::getOperateState, query.getOperateState());
return substationBService.page(page, wrapper);
}
/**
* 不分页全量列表
*/
@GetMapping("/list")
public List<SGConSubstationB> list(SGConSubstationB query) {
LambdaQueryWrapper<SGConSubstationB> wrapper = new LambdaQueryWrapper<>();
wrapper.like(query.getName() != null, SGConSubstationB::getName, query.getName());
wrapper.eq(query.getDccId() != null, SGConSubstationB::getDccId, query.getDccId());
return substationBService.list(wrapper);
}
}

View File

@@ -0,0 +1,94 @@
package com.njcn.relational.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.relational.pojo.po.SGConSubstationBind;
import com.njcn.relational.service.impl.SGConSubstationCServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/substation/map")
@RequiredArgsConstructor
public class SGConSubstationCController {
private final SGConSubstationCServiceImpl substationCService;
/**
* 新增多系统映射记录
*/
@PostMapping("/add")
public Boolean add(@RequestBody SGConSubstationBind entity) {
// 新增默认未删除
entity.setIsDelete(0);
return substationCService.save(entity);
}
/**
* 根据联合主键查询
*/
@GetMapping("/get")
public SGConSubstationBind get(
@RequestParam String dccId,
@RequestParam String dcloudId
) {
return substationCService.getByUnionKey(dccId, dcloudId);
}
/**
* 修改映射记录
*/
@PutMapping("/update")
public Boolean update(@RequestBody SGConSubstationBind entity) {
return substationCService.updateById(entity);
}
/**
* 逻辑删除(软删)
*/
@DeleteMapping("/logicRemove")
public Boolean logicRemove(
@RequestParam String dccId,
@RequestParam String dcloudId
) {
return substationCService.logicRemoveByUnionKey(dccId, dcloudId);
}
/**
* 分页查询映射关系
*/
@GetMapping("/page")
public IPage<SGConSubstationBind> page(
@RequestParam(defaultValue = "1") long pageNum,
@RequestParam(defaultValue = "10") long pageSize,
SGConSubstationBind query
) {
Page<SGConSubstationBind> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<SGConSubstationBind> wrapper = new LambdaQueryWrapper<>();
// 默认只查正常未删除数据
wrapper.eq(SGConSubstationBind::getIsDelete, 0);
wrapper.eq(query.getDccId() != null, SGConSubstationBind::getDccId, query.getDccId());
wrapper.eq(query.getDcloudId() != null, SGConSubstationBind::getDcloudId, query.getDcloudId());
wrapper.like(query.getDcloudName() != null, SGConSubstationBind::getDcloudName, query.getDcloudName());
wrapper.eq(query.getDcloudVoltagelevel() != null, SGConSubstationBind::getDcloudVoltagelevel, query.getDcloudVoltagelevel());
wrapper.eq(query.getD5000Id() != null, SGConSubstationBind::getD5000Id, query.getD5000Id());
wrapper.eq(query.getOmsId() != null, SGConSubstationBind::getOmsId, query.getOmsId());
wrapper.eq(query.getStatus() != null, SGConSubstationBind::getStatus, query.getStatus());
return substationCService.page(page, wrapper);
}
/**
* 不分页列表
*/
@GetMapping("/list")
public List<SGConSubstationBind> list(SGConSubstationBind query) {
LambdaQueryWrapper<SGConSubstationBind> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SGConSubstationBind::getIsDelete, 0);
wrapper.eq(query.getDccId() != null, SGConSubstationBind::getDccId, query.getDccId());
return substationCService.list(wrapper);
}
}

View File

@@ -0,0 +1,121 @@
package com.njcn.relational.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.njcn.relational.pojo.po.SyncTableConfig;
import com.njcn.relational.pojo.vo.HttpResult;
import com.njcn.relational.service.SyncTableConfigService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.List;
/**
* 同步表配置控制器
*
* @author cdf
* @date 2026/6/28
*/
@RestController
@RequestMapping("/sync-table-config")
@RequiredArgsConstructor
public class SyncTableConfigController {
private final SyncTableConfigService syncTableConfigService;
/**
* 分页查询
*/
@GetMapping("/page")
public HttpResult<IPage<SyncTableConfig>> page(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) String tableName,
@RequestParam(required = false) String syncMode,
@RequestParam(required = false) Integer enabled) {
Page<SyncTableConfig> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<SyncTableConfig> wrapper = new LambdaQueryWrapper<>();
if (tableName != null && !tableName.isEmpty()) {
wrapper.like(SyncTableConfig::getTableName, tableName);
}
if (syncMode != null && !syncMode.isEmpty()) {
wrapper.eq(SyncTableConfig::getSyncMode, syncMode);
}
if (enabled != null) {
wrapper.eq(SyncTableConfig::getEnabled, enabled);
}
wrapper.orderByAsc(SyncTableConfig::getSortOrder);
IPage<SyncTableConfig> result = syncTableConfigService.page(page, wrapper);
return HttpResult.success(result);
}
/**
* 查询所有启用的配置
*/
@GetMapping("/list")
public HttpResult<List<SyncTableConfig>> list() {
LambdaQueryWrapper<SyncTableConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SyncTableConfig::getEnabled, 1)
.orderByAsc(SyncTableConfig::getSortOrder);
return HttpResult.success(syncTableConfigService.list(wrapper));
}
/**
* 根据ID查询
*/
@GetMapping("/{tableName}")
public HttpResult<SyncTableConfig> getById(@PathVariable String tableName) {
return HttpResult.success(syncTableConfigService.getById(tableName));
}
/**
* 新增
*/
@PostMapping
public HttpResult<Boolean> add(@RequestBody SyncTableConfig config) {
return HttpResult.success(syncTableConfigService.save(config));
}
/**
* 更新
*/
@PutMapping
public HttpResult<Boolean> update(@RequestBody SyncTableConfig config) {
return HttpResult.success(syncTableConfigService.updateById(config));
}
/**
* 删除
*/
@DeleteMapping("/{tableName}")
public HttpResult<Boolean> delete(@PathVariable String tableName) {
return HttpResult.success(syncTableConfigService.removeById(tableName));
}
/**
* 批量删除
*/
@DeleteMapping("/batch")
public HttpResult<Boolean> deleteBatch(@RequestBody List<String> tableNames) {
return HttpResult.success(syncTableConfigService.removeByIds(tableNames));
}
/**
* 更新启用状态
*/
@PutMapping("/status/{tableName}")
public HttpResult<Boolean> updateStatus(@PathVariable String tableName, @RequestParam Integer enabled) {
SyncTableConfig config = new SyncTableConfig();
config.setTableName(tableName);
config.setEnabled(enabled);
return HttpResult.success(syncTableConfigService.updateById(config));
}
}

View File

@@ -0,0 +1,249 @@
package com.njcn.relational.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.njcn.relational.pojo.po.SyncTableLogs;
import com.njcn.relational.pojo.vo.HttpResult;
import com.njcn.relational.service.SyncTableLogsService;
import lombok.RequiredArgsConstructor;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
* 二三区数据转移日志 Controller
*
* @author cdf
* @date 2026-06-28
*/
@RestController
@RequestMapping("/sync-logs")
@RequiredArgsConstructor
public class SyncTableLogsController {
private final SyncTableLogsService syncTableLogsService;
// ========== 查询 ==========
/**
* 分页查询日志
* GET /api/sync-logs/page?pageNum=1&pageSize=10&timeId=2026-06-28&result=1
*/
@GetMapping("/page")
public HttpResult<IPage<SyncTableLogs>> page(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate timeId,
@RequestParam(required = false) Integer result) {
IPage<SyncTableLogs> page = syncTableLogsService.pageQuery(pageNum, pageSize, timeId, result);
return HttpResult.success(page);
}
/**
* 根据日期查询日志列表
* GET /api/sync-logs/list?timeId=2026-06-28
*/
@GetMapping("/list")
public HttpResult<List<SyncTableLogs>> listByTimeId(
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate timeId) {
List<SyncTableLogs> list = syncTableLogsService.listByTimeId(timeId);
return HttpResult.success(list);
}
/**
* 根据日期和结果查询
* GET /api/sync-logs/list/condition?timeId=2026-06-28&result=1
*/
@GetMapping("/list/condition")
public HttpResult<List<SyncTableLogs>> listByTimeIdAndResult(
@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate timeId,
@RequestParam Integer result) {
List<SyncTableLogs> list = syncTableLogsService.listByTimeIdAndResult(timeId, result);
return HttpResult.success(list);
}
/**
* 根据ID查询单条记录
* GET /api/sync-logs/2026-06-28/1
*/
@GetMapping("/{timeId}/{result}")
public HttpResult<SyncTableLogs> getById(
@PathVariable @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate timeId,
@PathVariable Integer result) {
LambdaQueryWrapper<SyncTableLogs> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SyncTableLogs::getTimeId, timeId)
.eq(SyncTableLogs::getResult, result);
SyncTableLogs log = syncTableLogsService.getOne(wrapper);
return HttpResult.success(log);
}
/**
* 检查某日期是否存在记录
* GET /api/sync-logs/exists/2026-06-28
*/
@GetMapping("/exists/{timeId}")
public HttpResult<Boolean> existsByTimeId(
@PathVariable @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate timeId) {
boolean exists = syncTableLogsService.existsByTimeId(timeId);
return HttpResult.success(exists);
}
/**
* 统计某日期的成功/失败数量
* GET /api/sync-logs/count/2026-06-28/1
*/
@GetMapping("/count/{timeId}/{result}")
public HttpResult<Integer> countByTimeIdAndResult(
@PathVariable @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate timeId,
@PathVariable Integer result) {
int count = syncTableLogsService.countByTimeIdAndResult(timeId, result);
return HttpResult.success(count);
}
/**
* 查询所有日志
* GET /api/sync-logs/all
*/
@GetMapping("/all")
public HttpResult<List<SyncTableLogs>> listAll() {
List<SyncTableLogs> list = syncTableLogsService.list();
return HttpResult.success(list);
}
// ========== 新增 ==========
/**
* 新增日志
* POST /api/sync-logs
*/
@PostMapping
public HttpResult<Boolean> add(@RequestBody SyncTableLogs log) {
// 设置创建时间
if (log.getCreateTime() == null) {
log.setCreateTime(LocalDateTime.now());
}
boolean success = syncTableLogsService.save(log);
return success ? HttpResult.success(true) : HttpResult.error("新增日志失败");
}
/**
* 批量新增日志
* POST /api/sync-logs/batch
*/
@PostMapping("/batch")
public HttpResult<Boolean> batchAdd(@RequestBody List<SyncTableLogs> logs) {
boolean success = syncTableLogsService.batchInsert(logs);
return success ? HttpResult.success(true) : HttpResult.error("批量新增失败");
}
/**
* 保存或更新(存在则更新,不存在则插入)
* POST /api/sync-logs/saveOrUpdate
*/
@PostMapping("/saveOrUpdate")
public HttpResult<Boolean> saveOrUpdate(@RequestBody SyncTableLogs log) {
boolean success = syncTableLogsService.saveOrUpdateLog(log);
return success ? HttpResult.success(true) : HttpResult.error("保存或更新失败");
}
// ========== 更新 ==========
/**
* 更新日志
* PUT /api/sync-logs
* 注意:由于 timeId 和 result 是复合主键,更新时不能修改这两个字段
*/
@PutMapping
public HttpResult<Boolean> update(@RequestBody SyncTableLogs log) {
// 使用条件更新
LambdaQueryWrapper<SyncTableLogs> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SyncTableLogs::getTimeId, log.getTimeId())
.eq(SyncTableLogs::getResult, log.getResult());
// 只更新 localResult 字段(如果有其他字段需要更新,在这里设置)
SyncTableLogs update = new SyncTableLogs();
update.setLocalResult(log.getLocalResult());
boolean success = syncTableLogsService.update(update, wrapper);
return success ? HttpResult.success(true) : HttpResult.error("更新失败");
}
/**
* 更新本地处理结果
* PUT /api/sync-logs/localResult/2026-06-28/1?localResult=1
*/
@PutMapping("/localResult/{timeId}/{result}")
public HttpResult<Boolean> updateLocalResult(
@PathVariable @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate timeId,
@PathVariable Integer result,
@RequestParam Integer localResult) {
LambdaQueryWrapper<SyncTableLogs> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SyncTableLogs::getTimeId, timeId)
.eq(SyncTableLogs::getResult, result);
SyncTableLogs update = new SyncTableLogs();
update.setLocalResult(localResult);
boolean success = syncTableLogsService.update(update, wrapper);
return success ? HttpResult.success(true) : HttpResult.error("更新本地处理结果失败");
}
// ========== 删除 ==========
/**
* 根据日期和结果删除
* DELETE /api/sync-logs/2026-06-28/1
*/
@DeleteMapping("/{timeId}/{result}")
public HttpResult<Boolean> delete(
@PathVariable @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate timeId,
@PathVariable Integer result) {
boolean success = syncTableLogsService.deleteByTimeIdAndResult(timeId, result);
return success ? HttpResult.success(true) : HttpResult.error("删除失败");
}
/**
* 根据日期删除所有日志
* DELETE /api/sync-logs/timeId/2026-06-28
*/
@DeleteMapping("/timeId/{timeId}")
public HttpResult<Boolean> deleteByTimeId(
@PathVariable @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate timeId) {
boolean success = syncTableLogsService.deleteByTimeId(timeId);
return success ? HttpResult.success(true) : HttpResult.error("删除失败");
}
/**
* 批量删除
* DELETE /api/sync-logs/batch
*/
@DeleteMapping("/batch")
public HttpResult<Boolean> deleteBatch(@RequestBody List<SyncTableLogs> logs) {
if (logs == null || logs.isEmpty()) {
return HttpResult.error("请选择要删除的记录");
}
// 构建删除条件
LambdaQueryWrapper<SyncTableLogs> wrapper = new LambdaQueryWrapper<>();
for (SyncTableLogs log : logs) {
wrapper.or(w -> w.eq(SyncTableLogs::getTimeId, log.getTimeId())
.eq(SyncTableLogs::getResult, log.getResult()));
}
boolean success = syncTableLogsService.remove(wrapper);
return success ? HttpResult.success(true) : HttpResult.error("批量删除失败");
}
}

View File

@@ -1,11 +1,9 @@
package com.njcn.relational.job;
import cn.hutool.core.date.DatePattern;
import com.njcn.relational.service.SyncTableConfigService;
import com.njcn.relational.service.SyncTableParseService;
import com.njcn.relational.service.SyncThreeDmParseService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Profile;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@@ -24,7 +22,7 @@ import java.time.format.DateTimeFormatter;
public class importJob {
private final SyncTableParseService syncTableParseService;
private final SyncThreeDmParseService syncThreeDmParseService;
@@ -38,7 +36,7 @@ public class importJob {
@Scheduled(cron = "0 55 4 * * ?")
public void threeImport(){
String date = LocalDate.now().minusDays(1).format(DateTimeFormatter.ofPattern(DatePattern.PURE_DATE_PATTERN));
syncTableParseService.syncFullTables(date);
syncThreeDmParseService.syncFullTables(date);
}
@@ -52,7 +50,7 @@ public class importJob {
@Scheduled(cron = "0 44 5 * * ?")
@Profile("insert_up")
public void removeThreeLocal(){
syncTableParseService.cleanLocalFiles(7);
syncThreeDmParseService.cleanLocalFiles(7);
}
}

View File

@@ -1,11 +1,9 @@
package com.njcn.relational.job;
import cn.hutool.core.date.DatePattern;
import com.njcn.relational.service.SyncTableConfigService;
import com.njcn.relational.service.SyncTableParseService;
import com.njcn.relational.service.SyncTwoDmExportService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Profile;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@@ -23,7 +21,7 @@ import java.time.format.DateTimeFormatter;
@Profile("query_up")
public class reportJob {
private final SyncTableConfigService syncTableConfigService;
private final SyncTwoDmExportService syncTwoDmExportService;
/**
@@ -35,7 +33,7 @@ public class reportJob {
@Scheduled(cron = "0 40 4 * * ?")
public void twoExport(){
String date = LocalDate.now().minusDays(1).format(DateTimeFormatter.ofPattern(DatePattern.PURE_DATE_PATTERN));
syncTableConfigService.syncAllFullTables(date);
syncTwoDmExportService.syncAllFullTables(date);
}
@@ -48,7 +46,7 @@ public class reportJob {
*/
@Scheduled(cron = "0 33 5 * * ?")
public void removeTwoLocal(){
syncTableConfigService.cleanLocalFiles(7);
syncTwoDmExportService.cleanLocalFiles(7);
}

View File

@@ -1,16 +0,0 @@
package com.njcn.relational.mapper;
import com.github.jeffreyning.mybatisplus.base.MppBaseMapper;
import com.njcn.relational.pojo.po.RStatDataFlickerD;
/**
* <p>
* Mapper 接口
* </p>
*/
public interface RStatDataFlickerRelationMapper extends MppBaseMapper<RStatDataFlickerD> {
}

View File

@@ -1,16 +0,0 @@
package com.njcn.relational.mapper;
import com.github.jeffreyning.mybatisplus.base.MppBaseMapper;
import com.njcn.relational.pojo.po.RStatDataFlucD;
/**
* <p>
* Mapper 接口
* </p>
*/
public interface RStatDataFlucRelationMapper extends MppBaseMapper<RStatDataFlucD> {
}

View File

@@ -1,16 +0,0 @@
package com.njcn.relational.mapper;
import com.github.jeffreyning.mybatisplus.base.MppBaseMapper;
import com.njcn.relational.pojo.po.RStatDataHarmPhasicID;
/**
* <p>
* Mapper 接口
* </p>
*/
public interface RStatDataHarmPhasicIRelationMapper extends MppBaseMapper<RStatDataHarmPhasicID> {
}

View File

@@ -1,16 +0,0 @@
package com.njcn.relational.mapper;
import com.github.jeffreyning.mybatisplus.base.MppBaseMapper;
import com.njcn.relational.pojo.po.RStatDataHarmPhasicVD;
/**
* <p>
* Mapper 接口
* </p>
*/
public interface RStatDataHarmPhasicVRelationMapper extends MppBaseMapper<RStatDataHarmPhasicVD> {
}

View File

@@ -1,16 +0,0 @@
package com.njcn.relational.mapper;
import com.github.jeffreyning.mybatisplus.base.MppBaseMapper;
import com.njcn.relational.pojo.po.RStatDataHarmPowerPD;
/**
* <p>
* Mapper 接口
* </p>
*/
public interface RStatDataHarmPowerPRelationMapper extends MppBaseMapper<RStatDataHarmPowerPD> {
}

View File

@@ -1,16 +0,0 @@
package com.njcn.relational.mapper;
import com.github.jeffreyning.mybatisplus.base.MppBaseMapper;
import com.njcn.relational.pojo.po.RStatDataHarmPowerQD;
/**
* <p>
* Mapper 接口
* </p>
*/
public interface RStatDataHarmPowerQRelationMapper extends MppBaseMapper<RStatDataHarmPowerQD> {
}

View File

@@ -1,16 +0,0 @@
package com.njcn.relational.mapper;
import com.github.jeffreyning.mybatisplus.base.MppBaseMapper;
import com.njcn.relational.pojo.po.RStatDataHarmPowerSD;
/**
* <p>
* Mapper 接口
* </p>
*/
public interface RStatDataHarmPowerSRelationMapper extends MppBaseMapper<RStatDataHarmPowerSD> {
}

View File

@@ -1,16 +0,0 @@
package com.njcn.relational.mapper;
import com.github.jeffreyning.mybatisplus.base.MppBaseMapper;
import com.njcn.relational.pojo.po.RStatDataHarmRateID;
/**
* <p>
* Mapper 接口
* </p>
*/
public interface RStatDataHarmRateIRelationMapper extends MppBaseMapper<RStatDataHarmRateID> {
}

View File

@@ -1,16 +0,0 @@
package com.njcn.relational.mapper;
import com.github.jeffreyning.mybatisplus.base.MppBaseMapper;
import com.njcn.relational.pojo.po.RStatDataHarmRateVD;
/**
* <p>
* Mapper 接口
* </p>
*/
public interface RStatDataHarmRateVRelationMapper extends MppBaseMapper<RStatDataHarmRateVD> {
}

View File

@@ -1,16 +0,0 @@
package com.njcn.relational.mapper;
import com.github.jeffreyning.mybatisplus.base.MppBaseMapper;
import com.njcn.relational.pojo.po.RStatDataID;
/**
* <p>
* Mapper 接口
* </p>
*/
public interface RStatDataIRelationMapper extends MppBaseMapper<RStatDataID> {
}

View File

@@ -1,16 +0,0 @@
package com.njcn.relational.mapper;
import com.github.jeffreyning.mybatisplus.base.MppBaseMapper;
import com.njcn.relational.pojo.po.RStatDataInHarmID;
/**
* <p>
* Mapper 接口
* </p>
*/
public interface RStatDataInHarmIRelationMapper extends MppBaseMapper<RStatDataInHarmID> {
}

View File

@@ -1,16 +0,0 @@
package com.njcn.relational.mapper;
import com.github.jeffreyning.mybatisplus.base.MppBaseMapper;
import com.njcn.relational.pojo.po.RStatDataInHarmVD;
/**
* <p>
* Mapper 接口
* </p>
*/
public interface RStatDataInHarmVRelationMapper extends MppBaseMapper<RStatDataInHarmVD> {
}

View File

@@ -1,16 +0,0 @@
package com.njcn.relational.mapper;
import com.github.jeffreyning.mybatisplus.base.MppBaseMapper;
import com.njcn.relational.pojo.po.RStatDataPltD;
/**
* <p>
* Mapper 接口
* </p>
*/
public interface RStatDataPltRelationMapper extends MppBaseMapper<RStatDataPltD> {
}

View File

@@ -1,16 +0,0 @@
package com.njcn.relational.mapper;
import com.github.jeffreyning.mybatisplus.base.MppBaseMapper;
import com.njcn.relational.pojo.po.RStatDataVD;
/**
* <p>
* Mapper 接口
* </p>
*/
public interface RStatDataVRelationMapper extends MppBaseMapper<RStatDataVD> {
}

View File

@@ -4,6 +4,7 @@ package com.njcn.relational.service;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.njcn.relational.pojo.dto.DcloudSyncQuery;
import dcloud.common.InnerServiceBus.ServiceBus;
import dcloud.model.common.ConModel;
import lombok.RequiredArgsConstructor;
@@ -13,6 +14,6 @@ import org.springframework.stereotype.Service;
public interface DcloudBusService {
boolean syncDataToDb();
boolean syncDataToDb(DcloudSyncQuery query);
}

View File

@@ -1,17 +0,0 @@
package com.njcn.relational.service;
import java.util.List;
/**
* Description:
* Date: 2024/11/18 11:17【需求编号】
*
* @author clam
* @version V1.0.0
*/
public interface InsertIDataFlicker {
}

View File

@@ -1,17 +0,0 @@
package com.njcn.relational.service;
import java.util.List;
/**
* Description:
* Date: 2024/11/18 11:17【需求编号】
*
* @author clam
* @version V1.0.0
*/
public interface InsertIDataFluc {
}

View File

@@ -1,13 +0,0 @@
package com.njcn.relational.service;
import java.util.List;
/**
* @author xy
*/
public interface InsertIDataHarmRateI {
}

View File

@@ -1,14 +0,0 @@
package com.njcn.relational.service;
import java.util.List;
/**
* @author xy
*/
public interface InsertIDataHarmRateV {
}

View File

@@ -1,19 +0,0 @@
package com.njcn.relational.service;
import java.util.List;
/**
* Description:
* Date: 2024/11/18 11:17【需求编号】
*
* @author clam
* @version V1.0.0
*/
public interface InsertIDataHarmphasicI {
}

View File

@@ -1,17 +0,0 @@
package com.njcn.relational.service;
import java.util.List;
/**
* Description:
* Date: 2024/11/18 11:17【需求编号】
*
* @author clam
* @version V1.0.0
*/
public interface InsertIDataHarmphasicV {
}

View File

@@ -1,18 +0,0 @@
package com.njcn.relational.service;
import java.util.List;
/**
* Description:
* Date: 2024/11/18 11:17【需求编号】
*
* @author clam
* @version V1.0.0
*/
public interface InsertIDataHarmpowerP {
}

View File

@@ -1,18 +0,0 @@
package com.njcn.relational.service;
import java.util.List;
/**
* Description:
* Date: 2024/11/18 11:17【需求编号】
*
* @author clam
* @version V1.0.0
*/
public interface InsertIDataHarmpowerQ {
}

View File

@@ -1,17 +0,0 @@
package com.njcn.relational.service;
import java.util.List;
/**
* Description:
* Date: 2024/11/18 11:17【需求编号】
*
* @author clam
* @version V1.0.0
*/
public interface InsertIDataHarmpowerS {
}

View File

@@ -1,17 +0,0 @@
package com.njcn.relational.service;
import java.util.List;
/**
* Description:
* Date: 2024/11/18 11:17【需求编号】
*
* @author clam
* @version V1.0.0
*/
public interface InsertIDataI {
}

View File

@@ -1,17 +0,0 @@
package com.njcn.relational.service;
import java.util.List;
/**
* Description:
* Date: 2024/11/18 13:27【需求编号】
*
* @author clam
* @version V1.0.0
*/
public interface InsertIDataInharmI {
}

View File

@@ -1,17 +0,0 @@
package com.njcn.relational.service;
import java.util.List;
/**
* Description:
* Date: 2024/11/18 13:27【需求编号】
*
* @author clam
* @version V1.0.0
*/
public interface InsertIDataInharmV {
}

View File

@@ -1,15 +0,0 @@
package com.njcn.relational.service;
import java.util.List;
/**
* Description:
* Date: 2024/11/18 11:17【需求编号】
*
* @author clam
* @version V1.0.0
*/
public interface InsertIDataPlt {
}

View File

@@ -1,19 +0,0 @@
package com.njcn.relational.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.njcn.relational.pojo.po.RStatDataID;
import com.njcn.relational.pojo.po.RStatDataVD;
import java.util.List;
/**
* @author hongawen
* @version 1.0
* @data 2024/11/7 10:54
*/
public interface InsertIDataV extends IService<RStatDataID> {
}

View File

@@ -0,0 +1,15 @@
package com.njcn.relational.service;
import com.alibaba.fastjson.JSONArray;
import com.baomidou.mybatisplus.extension.service.IService;
import com.njcn.relational.pojo.po.SGConPlantB;
public interface SGConPlantBService extends IService<SGConPlantB> {
/**
* 云平台批量同步场站数据入库
* @param dataArr 云平台分页返回JSON数组
* @return 同步结果
*/
boolean batchSyncFromDcloud(JSONArray dataArr);
}

View File

@@ -0,0 +1,15 @@
package com.njcn.relational.service;
import com.alibaba.fastjson.JSONArray;
import com.baomidou.mybatisplus.extension.service.IService;
import com.njcn.relational.pojo.po.SGConSubstationB;
public interface SGConSubstationBService extends IService<SGConSubstationB> {
/**
* 云平台批量同步场站数据入库
* @param dataArr 云平台分页返回JSON数组
* @return 同步结果
*/
boolean batchSyncFromDcloud(JSONArray dataArr) throws Exception;
}

View File

@@ -0,0 +1,15 @@
package com.njcn.relational.service;
import com.alibaba.fastjson.JSONArray;
import com.baomidou.mybatisplus.extension.service.IService;
import com.njcn.relational.pojo.po.SGConSubstationBind;
public interface SGConSubstationCService extends IService<SGConSubstationBind> {
/**
* 云平台批量同步场站数据入库
* @param dataArr 云平台分页返回JSON数组
* @return 同步结果
*/
boolean batchSyncFromDcloud(JSONArray dataArr) throws Exception;
}

View File

@@ -1,490 +1,7 @@
package com.njcn.relational.service;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.njcn.relational.mapper.DynamicSyncMapper;
import com.njcn.relational.mapper.SyncTableConfigMapper;
import com.njcn.relational.pojo.bo.UploadResult;
import com.njcn.relational.pojo.po.SyncTableConfig;
import com.njcn.relational.serializer.BlobSerializer;
import com.njcn.relational.serializer.ClobSerializer;
import com.njcn.relational.utils.SftpUploadUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import java.io.*;
import java.sql.Blob;
import java.sql.Clob;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
public class SyncTableConfigService extends ServiceImpl<SyncTableConfigMapper, SyncTableConfig> {
@Autowired
private DynamicSyncMapper dynamicSyncMapper;
@Value("${sync.export.remotePath}")
private String remotePath;
@Value("${sync.export.localPath}")
private String localPath;
@Value("${sync.ip}")
private String ip;
@Value("${sync.port:22}")
private Integer port;
@Value("${sync.username}")
private String username;
@Value("${sync.password}")
private String password;
// 新增:表名 -> 时间字段 缓存
private Map<String, String> timeColumnCache = new HashMap<>();
private static final int BATCH_SIZE = 10000;
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final DateTimeFormatter FILE_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
// JSON序列化工具自动处理时间、换行符等
private final ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.registerModule(new SimpleModule() {{
addSerializer(Blob.class, new BlobSerializer());
addSerializer(Clob.class, new ClobSerializer());
}})
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.enable(SerializationFeature.INDENT_OUTPUT)
.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));; // 格式化输出,可读性好
// 缓存当前Schema
private String currentSchema;
// 记录本次生成的所有文件
private final List<String> generatedFiles = new ArrayList<>();
@PostConstruct
public void init() {
currentSchema = dynamicSyncMapper.getCurrentUser();
log.info("当前数据库Schema: {}", currentSchema);
}
/**
* 获取所有启用的全量表(从配置表)
*/
public List<SyncTableConfig> getEnabledFullTables() {
LambdaQueryWrapper<SyncTableConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SyncTableConfig::getEnabled, 1)
.eq(SyncTableConfig::getSyncMode, "FULL")
.orderByAsc(SyncTableConfig::getSortOrder);
return this.list(wrapper);
}
/**
* 获取所有启用的增量表(从配置表)
*/
public List<SyncTableConfig> getEnabledIncrementTables() {
LambdaQueryWrapper<SyncTableConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SyncTableConfig::getEnabled, 1)
.eq(SyncTableConfig::getSyncMode, "INC")
.orderByAsc(SyncTableConfig::getSortOrder);
return this.list(wrapper);
}
/**
* 确保本地目录存在
*/
private void ensureLocalDirectory() {
File dir = new File(localPath);
if (!dir.exists()) {
dir.mkdirs();
log.info("创建目录: {}", localPath);
}
}
/**
* 全量同步所有表
*/
public void syncAllFullTables(String dateStr) {
List<SyncTableConfig> tables = getEnabledFullTables();
log.info("========== 全量同步开始,共 {} 张表 ==========", tables.size());
// 确保目录存在
ensureLocalDirectory();
generatedFiles.clear();
for (SyncTableConfig config : tables) {
String tableName = config.getTableName();
try {
long startTime = System.currentTimeMillis();
/*IService<?> service = registry.getServiceByTableName(tableName);
if (service != null) {
String filePath = exportFullTableWithEntity(service, tableName, dateStr);
log.info("表 {} 使用实体方式导出完成", tableName);
} else {*/
String filePath = exportFullTableToJson(tableName, dateStr);
log.info("表 {} 使用Map方式导出完成", tableName);
// }
long duration = System.currentTimeMillis() - startTime;
log.info("表 {} 全量导出完成,耗时 {} ms", tableName, duration);
} catch (Exception e) {
log.error("表 {} 全量导出失败", tableName, e);
}
}
syncAllIncrementTables(dateStr);
}
/**
* 增量同步所有表
*/
public void syncAllIncrementTables(String dateStr) {
List<SyncTableConfig> tables = getEnabledIncrementTables();
timeColumnCache = tables.stream().filter(it-> StrUtil.isNotBlank(it.getTimeColumn())).collect(Collectors.toMap(SyncTableConfig::getTableName,SyncTableConfig::getTimeColumn));
log.info("========== 增量同步开始,共 {} 张表 ==========", tables.size());
for (SyncTableConfig config : tables) {
String tableName = config.getTableName();
try {
long startTime = System.currentTimeMillis();
/*IService<?> service = registry.getServiceByTableName(tableName);
if (service != null) {
String filePath = exportIncrementWithEntity(service, tableName, dateStr);
log.info("表 {} 使用实体方式导出完成", tableName);
} else {*/
String filePath = exportTodayDataToJson(tableName, dateStr);
log.info("表 {} 使用Map方式导出完成", tableName);
// }
long duration = System.currentTimeMillis() - startTime;
log.info("表 {} 增量导出完成,耗时 {} ms", tableName, duration);
} catch (Exception e) {
log.error("表 {} 增量同步失败", tableName, e);
}
}
// 推送所有文件到横向隔离设备
if (!generatedFiles.isEmpty()) {
uploadAllFiles();
}
}
/**
* 上传所有生成的文件(批量上传)
*/
private void uploadAllFiles() {
if (generatedFiles.isEmpty()) {
log.info("没有文件需要上传");
return;
}
try {
List<UploadResult> resultList = SftpUploadUtil.batchUploadFiles(
ip, port, username, password, generatedFiles, remotePath
);
// 统计上传结果
long successCount = resultList.stream().filter(UploadResult::isSuccess).count();
long failCount = resultList.size() - successCount;
log.info("批量上传完成 - 成功: {}, 失败: {}, 总计: {}", successCount, failCount, resultList.size());
// 记录失败的文件
for (UploadResult result : resultList) {
if (!result.isSuccess()) {
log.error("上传失败文件: {}, 原因: {}", result.getLocalFilePath(), result.getErrorMessage());
}
}
} catch (Exception e) {
log.error("批量上传失败", e);
}
}
/**
* Map方式全量导出为JSON保存为.txt文件
*/
@Transactional
public String exportFullTableToJson(String tableName, String dateStr) throws IOException {
String fileName = String.format("%s_FULL_%s.txt", tableName, dateStr);
String filePath = localPath + fileName;
log.info("开始导出全量表: {} -> {}", tableName, filePath);
List<Map<String, Object>> allData = new ArrayList<>();
int offset = 0;
while (true) {
List<Map<String, Object>> batchData = dynamicSyncMapper.selectPage(
tableName, BATCH_SIZE, offset);
if (batchData.isEmpty()) {
break;
}
allData.addAll(batchData);
log.info("表 {} 已读取 {} 条记录", tableName, allData.size());
if (batchData.size() < BATCH_SIZE) {
break;
}
offset += BATCH_SIZE;
}
// 写入JSON格式到.txt文件
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath))) {
objectMapper.writeValue(bos, allData);
}
// 记录生成的文件
generatedFiles.add(filePath);
log.info("表 {} 全量导出完成,共 {} 条记录,文件: {}", tableName, allData.size(), filePath);
return filePath;
}
/**
* Map方式增量导出为JSON保存为.txt文件
*/
@Transactional
public String exportTodayDataToJson(String tableName, String dateStr) throws IOException {
// 1. 判断表属于哪种周期D/M/Q/Y
String periodType = getTablePeriodType(tableName);
if (periodType == null) {
log.warn("表 {} 无法识别周期类型,跳过增量导出", tableName);
return null;
}
// 2. 根据周期构建 开始/结束 时间
Map<String, String> timeRange = buildPeriodTimeRange(periodType, dateStr);
String dateStart = timeRange.get("start");
String dateEnd = timeRange.get("end");
log.info("导出表 {} 数据: {} 至 {}", tableName, dateStart, dateEnd);
String timeColumn = getTimeColumn(tableName);
List<Map<String, Object>> queryData = dynamicSyncMapper.selectTodayData(
tableName, timeColumn, dateStart, dateEnd);
if (queryData.isEmpty()) {
log.info("表 {} {} - {}无数据", tableName,dateStart,dateEnd);
return null;
}
String fileName = String.format("%s_INC_%s.txt", tableName, dateStr);
String filePath = localPath + fileName;
// 写入JSON格式到.txt文件
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath))) {
objectMapper.writeValue(bos, queryData);
}
// 记录生成的文件
generatedFiles.add(filePath);
log.info("表 {} {}数据导出完成,共 {} 条记录,文件: {}", tableName,dateStr, queryData.size(), filePath);
return filePath;
}
/**
* 从表名识别周期类型
* D=天, M=月, Q=季, Y=年
*/
private String getTablePeriodType(String tableName) {
if (tableName.endsWith("_D") || tableName.equalsIgnoreCase("R_MP_V_THD")) return "D";
if (tableName.endsWith("_M")) return "M";
if (tableName.endsWith("_Q")) return "Q";
if (tableName.endsWith("_Y")) return "Y";
return null;
}
/**
* 根据周期 & 入参日期计算【当前周期】时间范围JDK8 兼容)
*/
private Map<String, String> buildPeriodTimeRange(String periodType, String dateStr) {
DateTime baseDate = DateUtil.parse(dateStr, DatePattern.PURE_DATE_PATTERN);
String start;
String end;
// JDK8 标准 switch
switch (periodType) {
case "D":
// 入参日期 当天
start = DateUtil.format(DateUtil.beginOfDay(baseDate), DatePattern.NORM_DATETIME_PATTERN);
end = DateUtil.format(DateUtil.endOfDay(baseDate), DatePattern.NORM_DATETIME_PATTERN);
break;
case "M":
// 入参日期 所在整月
start = DateUtil.format(DateUtil.beginOfMonth(baseDate), DatePattern.NORM_DATETIME_PATTERN);
end = DateUtil.format(DateUtil.endOfMonth(baseDate), DatePattern.NORM_DATETIME_PATTERN);
break;
case "Q":
// 入参日期 所在整季度
start = DateUtil.format(DateUtil.beginOfQuarter(baseDate), DatePattern.NORM_DATETIME_PATTERN);
end = DateUtil.format(DateUtil.endOfQuarter(baseDate), DatePattern.NORM_DATETIME_PATTERN);
break;
case "Y":
// 入参日期 所在整年
start = DateUtil.format(DateUtil.beginOfYear(baseDate), DatePattern.NORM_DATETIME_PATTERN);
end = DateUtil.format(DateUtil.endOfYear(baseDate), DatePattern.NORM_DATETIME_PATTERN);
break;
default:
throw new IllegalArgumentException("不支持的周期类型:" + periodType);
}
Map<String, String> map = new HashMap<>(2);
map.put("start", start);
map.put("end", end);
return map;
}
/**
* 获取时间字段
*/
private String getTimeColumn(String tableName) {
String createTitle = timeColumnCache.get(tableName);
if(Objects.isNull(createTitle)){
switch (tableName) {
case "R_STAT_DATA_FLICKER_D":
case "R_STAT_DATA_FLUC_D":
case "R_STAT_DATA_HARMPHASIC_I_D":
case "R_STAT_DATA_HARMPHASIC_V_D":
case "R_STAT_DATA_HARMPOWER_P_D":
case "R_STAT_DATA_HARMPOWER_Q_D":
case "R_STAT_DATA_HARMPOWER_S_D":
case "R_STAT_DATA_HARMRATE_I_D":
case "R_STAT_DATA_HARMRATE_V_D":
case "R_STAT_DATA_INHARM_I_D":
case "R_STAT_DATA_INHARM_V_D":
case "R_STAT_DATA_I_D":
case "R_STAT_DATA_PLT_D":
case "R_STAT_DATA_V_D":
return "TIME";
// 新增表
case "R_STAT_COMASSES_D":
case "R_STAT_ASSES_D":
case "R_STAT_LIMIT_RATE_D":
case "R_STAT_ONLINERATE_D":
case "R_STAT_INTEGRITY_D":
case "R_STAT_ORG_INTEGRITY_D":
case "R_STAT_LIMIT_RATE_DETAIL_D":
case "R_STAT_LIMIT_TARGET_D":
return "TIME_ID";
case "R_MP_POLLUTION_D":
case "R_MP_V_THD":
case "R_STAT_POLLUTION_ORG_D":
case "R_STAT_POLLUTION_ORG_M":
case "R_STAT_POLLUTION_ORG_Q":
case "R_STAT_POLLUTION_ORG_Y":
case "R_STAT_POLLUTION_SUBSTATION_D":
case "R_STAT_POLLUTION_SUBSTATION_M":
case "R_STAT_POLLUTION_SUBSTATION_Q":
case "R_STAT_POLLUTION_SUBSTATION_Y":
return "DATA_DATE";
default:
log.warn("表 {} 未配置时间字段,跳过", tableName);
return "CREATE_TIME";
}
}else {
return createTitle;
}
}
public void test(){
try {
// 确保目录存在
File localDir = new File(localPath);
if (!localDir.exists()) {
localDir.mkdirs();
}
// 生成txt文件
String filePath = localPath + "test.txt";
try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) {
writer.write("test");
}
// 上传到SFTP服务器
SftpUploadUtil.uploadFile(ip, port, username, password, filePath, remotePath);
log.info("test.txt文件生成并上传成功");
} catch (Exception e) {
log.error("失败", e);
}
}
public List<String> getPendingFiles() {
List<String> matchedFiles = new ArrayList<>();
Vector<?> files;
try {
// 列目录也纳入异常捕获
files = SftpUploadUtil.listFiles(ip, port, username, password, remotePath);
for (Object obj : files) {
if (obj instanceof com.jcraft.jsch.ChannelSftp.LsEntry) {
com.jcraft.jsch.ChannelSftp.LsEntry entry = (com.jcraft.jsch.ChannelSftp.LsEntry) obj;
String fileName = entry.getFilename();
matchedFiles.add(fileName);
}
}
} catch (Exception e) {
log.error("获取待处理文件列表失败,远端目录:{}", remotePath, e);
return new ArrayList<>();
}
return matchedFiles;
}
/**
* 清理本地已处理过的文件
*/
public void cleanLocalFiles(int daysToKeep) {
File localDir = new File(localPath);
if (!localDir.exists()) {
return;
}
long cutoffTime = System.currentTimeMillis() - (daysToKeep * 24L * 60 * 60 * 1000);
File[] files = localDir.listFiles();
if (files != null) {
int deletedCount = 0;
for (File file : files) {
if (file.isFile() && file.lastModified() < cutoffTime) {
file.delete();
deletedCount++;
}
}
log.info("清理了10.11.7.5二区服务器 {} 个七天前的本地txt文件", deletedCount);
}
}
public interface SyncTableConfigService extends IService<SyncTableConfig> {
}

View File

@@ -0,0 +1,57 @@
package com.njcn.relational.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.njcn.relational.pojo.po.SyncTableLogs;
import java.time.LocalDate;
import java.util.List;
public interface SyncTableLogsService extends IService<SyncTableLogs> {
/**
* 分页查询日志
*/
IPage<SyncTableLogs> pageQuery(Integer pageNum, Integer pageSize, LocalDate timeId, Integer result);
/**
* 根据日期查询日志列表
*/
List<SyncTableLogs> listByTimeId(LocalDate timeId);
/**
* 根据日期和结果查询
*/
List<SyncTableLogs> listByTimeIdAndResult(LocalDate timeId, Integer result);
/**
* 查询某日期是否存在记录
*/
boolean existsByTimeId(LocalDate timeId);
/**
* 批量插入日志
*/
boolean batchInsert(List<SyncTableLogs> logs);
/**
* 根据日期删除日志
*/
boolean deleteByTimeId(LocalDate timeId);
/**
* 根据日期和结果删除
*/
boolean deleteByTimeIdAndResult(LocalDate timeId, Integer result);
/**
* 统计某日期的成功/失败数量
*/
int countByTimeIdAndResult(LocalDate timeId, Integer result);
/**
* 保存或更新日志(如果存在则更新,不存在则插入)
*/
boolean saveOrUpdateLog(SyncTableLogs log);
}

View File

@@ -1,5 +1,6 @@
package com.njcn.relational.service;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -9,6 +10,7 @@ import com.njcn.relational.mapper.DynamicSyncMapper;
import com.njcn.relational.mapper.SyncTableConfigMapper;
import com.njcn.relational.pojo.bo.DownloadResult;
import com.njcn.relational.pojo.po.SyncTableConfig;
import com.njcn.relational.pojo.po.SyncTableLogs;
import com.njcn.relational.utils.SftpUploadUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@@ -20,18 +22,23 @@ import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import java.io.*;
import java.nio.file.*;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.Timestamp;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Slf4j
@Service
public class SyncTableParseService {
public class SyncThreeDmParseService {
@Autowired
private DynamicSyncMapper dynamicSyncMapper;
@@ -39,6 +46,9 @@ public class SyncTableParseService {
@Autowired
private SyncTableConfigMapper syncTableConfigMapper;
@Autowired
private SyncTableLogsService syncTableLogsService;
// 缓存当前Schema
private String currentSchema;
@@ -78,16 +88,29 @@ public class SyncTableParseService {
log.info("当前数据库Schema: {}", currentSchema);
}
private Map<String, String> syncData(){
Map<String, String> tableBlobCache = new ConcurrentHashMap<>();
Map<String, String> tableClobCache = new ConcurrentHashMap<>();
private void syncData(){
// 加载表配置到缓存
Map<String, String> tableConfigCache = new HashMap<>();
tableBlobCache.clear();
tableClobCache.clear();
LambdaQueryWrapper<SyncTableConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SyncTableConfig::getEnabled, 1).isNotNull(SyncTableConfig::getBlobColumns);
wrapper.eq(SyncTableConfig::getEnabled, 1);
List<SyncTableConfig> configs = syncTableConfigMapper.selectList(wrapper);
for (SyncTableConfig config : configs) {
tableConfigCache.put(config.getTableName().toUpperCase(), config.getBlobColumns());
String tableUpper = config.getTableName().toUpperCase();
if(StrUtil.isNotBlank(config.getBlobColumns())) {
tableBlobCache.put(tableUpper, config.getBlobColumns());
}
if(StrUtil.isNotBlank(config.getClobColumns())){
tableClobCache.put(tableUpper, config.getClobColumns());
}
}
return tableConfigCache;
log.info("加载表配置BLOB缓存完成共 {} 张表", tableBlobCache.size());
log.info("加载表配置CLOB缓存完成共 {} 张表", tableClobCache.size());
}
/**
@@ -97,12 +120,14 @@ public class SyncTableParseService {
log.info("========== 开始批量同步数据{} ==========",LocalDateTime.now());
// 缓存表配置
Map<String, String> tableConfigCache = syncData();
log.info("加载表配置缓存完成,共 {} 张表", tableConfigCache.size());
syncData();
// 确保本地目录存在
ensureLocalDirectory();
boolean localFlag =true;
boolean resultFlag = true;
try {
// 1. 获取待下载的文件列表
List<String> remoteFilePaths = getRemoteFilePaths(dataStr);
@@ -116,8 +141,7 @@ public class SyncTableParseService {
// 2. 批量下载并删除远程文件
List<DownloadResult> downloadResults = SftpUploadUtil.batchDownloadAndDelete(
ip, port, username, password,
remoteFilePaths, localPath, true // true = 下载后删除远程文件
ip, port, username, password, remoteFilePaths, localPath, true // true = 下载后删除远程文件
);
// 3. 统计下载结果
@@ -128,6 +152,7 @@ public class SyncTableParseService {
// 记录下载失败的文件
for (DownloadResult result : downloadResults) {
if (!result.isSuccess()) {
localFlag = false;
log.error("下载失败: {}, 原因: {}", result.getRemoteFilePath(), result.getErrorMessage());
}
}
@@ -138,7 +163,8 @@ public class SyncTableParseService {
.collect(Collectors.toList());
if (successfulDownloads.isEmpty()) {
log.warn("没有成功下载的文件,停止处理");
log.error("没有成功下载的文件,停止处理");
localFlag = false;
return;
}
@@ -152,7 +178,7 @@ public class SyncTableParseService {
String localFilePath = downloadResult.getLocalFilePath();
log.info("开始导入表: {}, 文件: {}", tableName, localFilePath);
importJsonToTable(tableName, localFilePath,tableConfigCache);
importJsonToTable(tableName, localFilePath);
importSuccessCount++;
log.info("表 {} 导入成功", tableName);
@@ -161,6 +187,7 @@ public class SyncTableParseService {
deleteLocalFile(localFilePath);
} catch (Exception e) {
resultFlag = false;
importFailCount++;
log.error("导入失败: {}", downloadResult.getRemoteFilePath(), e);
}
@@ -171,7 +198,15 @@ public class SyncTableParseService {
} catch (Exception e) {
log.error("批量同步失败", e);
resultFlag = false;
}
SyncTableLogs syncTableLogs = new SyncTableLogs();
syncTableLogs.setLocalResult(localFlag?1:0);
syncTableLogs.setResult((resultFlag&&localFlag) ? 1:0);
syncTableLogs.setTimeId(LocalDate.parse(dataStr, DateTimeFormatter.ofPattern("yyyyMMdd")));
syncTableLogs.setCreateTime(LocalDateTime.now());
syncTableLogsService.saveOrUpdate(syncTableLogs);
}
/**
@@ -319,8 +354,8 @@ public class SyncTableParseService {
/**
* 从配置缓存中获取BLOB字段列表
*/
private List<String> getBlobColumnsFromCache(String tableName, Map<String, String> tableConfigCache) {
String blobColumnsStr = tableConfigCache.get(tableName.toUpperCase());
private List<String> getBlobColumnsFromCache(String tableName) {
String blobColumnsStr = tableBlobCache.get(tableName.toUpperCase());
if (blobColumnsStr == null || blobColumnsStr.trim().isEmpty()) {
return new ArrayList<>();
}
@@ -342,7 +377,7 @@ public class SyncTableParseService {
* 导入JSON数据到表支持BLOB字段
*/
@Transactional
public void importJsonToTable(String tableName, String filePath, Map<String, String> tableConfigCache) throws Exception {
public void importJsonToTable(String tableName, String filePath) throws Exception {
log.info("开始导入表: {}, 文件: {}", tableName, filePath);
File dataFile = new File(filePath);
@@ -384,7 +419,7 @@ public class SyncTableParseService {
List<String> timeColumns = getTimeColumns(tableName.toUpperCase());
// 从缓存配置中获取BLOB字段列表
List<String> blobColumns = getBlobColumnsFromCache(tableName, tableConfigCache);
List<String> blobColumns = getBlobColumnsFromCache(tableName);
// 转换数据Map key转大写并将时间字段转换为TimestampBLOB字段转换为byte[]
List<Map<String, Object>> convertedData = new ArrayList<>();
@@ -428,6 +463,19 @@ public class SyncTableParseService {
convertedData.add(convertedRow);
}
// 仅CLOB配置表 + 增量文件 才走先删当日再插入全量保留原有truncate逻辑
String tableUpper = tableName.toUpperCase();
if (tableClobCache.containsKey(tableUpper) && !isFull) {
log.info("表 {} 专用先删后插流程开始执行", tableName);
String syncDate = extractDateFromFileName(fileName);
// 从缓存拿当前表配置的timeColumn
SyncTableConfig cfg = getTableConfig(tableName);
String delTimeCol = cfg.getTimeColumn();
deleteDayThenInsert(tableName, columns, convertedData, syncDate, delTimeCol);
log.info("表 {} 专用先删后插流程执行完毕,删除时间字段:{},直接返回", tableName, delTimeCol);
return;
}
// ==============================================
// 核心修改增量使用MERGE INTO(存在更新/不存在插入)
// ==============================================
@@ -473,6 +521,16 @@ public class SyncTableParseService {
log.info("表 {} 导入完成,共 {} 条记录", tableName, totalCount);
}
private SyncTableConfig getTableConfig(String tableName) {
LambdaQueryWrapper<SyncTableConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SyncTableConfig::getTableName, tableName.toUpperCase());
SyncTableConfig config = syncTableConfigMapper.selectOne(wrapper);
if (config == null || StrUtil.isBlank(config.getTimeColumn())) {
throw new RuntimeException("表[" + tableName + "]未配置timeColumn无法执行按日期删除增量逻辑");
}
return config;
}
/**
* 将Base64字符串转换为byte[]
*/
@@ -518,6 +576,10 @@ public class SyncTableParseService {
}
}
/**
* 构建达梦MERGE INTO SQL存在更新不存在插入
* 根据表配置CLOB_COLUMN动态使用TO_CLOB(?)不再硬编码后缀_OVERTIME
*/
/**
* 构建达梦MERGE INTO SQL存在更新不存在插入
*/
@@ -558,7 +620,6 @@ public class SyncTableParseService {
sb.append(")");
return sb.toString();
}
/**
* 批量执行MERGE
*/
@@ -581,7 +642,7 @@ public class SyncTableParseService {
private List<String> getTimeColumns(String tableName) {
String sql = "SELECT COLUMN_NAME FROM ALL_TAB_COLUMNS " +
"WHERE TABLE_NAME = ? AND OWNER = ? " +
"AND DATA_TYPE IN ('DATE', 'TIMESTAMP')";
"AND DATA_TYPE IN ('DATE', 'TIMESTAMP','DATETIME')";
try {
List<String> timeColumns = targetJdbcTemplate.queryForList(sql, String.class, tableName, currentSchema);
log.debug("表 {} 的时间字段: {}", tableName, timeColumns);
@@ -596,59 +657,63 @@ public class SyncTableParseService {
* 将字符串转换为 java.sql.Timestamp增强版
*/
private Timestamp convertToTimestamp(String timeStr) {
if (timeStr == null || timeStr.isEmpty()) {
return null;
if (timeStr == null || StrUtil.isBlank(timeStr)) {
log.warn("时间字符串为空,使用当日零点兜底");
return Timestamp.valueOf(LocalDate.now(java.time.ZoneId.of("Asia/Shanghai")).atStartOfDay());
}
timeStr = timeStr.trim();
if (timeStr.startsWith("\"") && timeStr.endsWith("\"")) {
timeStr = timeStr.substring(1, timeStr.length() - 1);
}
// 1. 带时区 ISO 2026-06-10T14:53:23.853+08:00
try {
// 去除可能的引号
timeStr = timeStr.trim();
if (timeStr.startsWith("\"") && timeStr.endsWith("\"")) {
timeStr = timeStr.substring(1, timeStr.length() - 1);
}
java.time.OffsetDateTime offsetDateTime = java.time.OffsetDateTime.parse(timeStr);
LocalDateTime ldt = offsetDateTime.atZoneSameInstant(java.time.ZoneId.of("Asia/Shanghai")).toLocalDateTime();
return Timestamp.valueOf(ldt);
} catch (Exception ignored) {}
// 尝试多种解析器
// 1. ISO_OFFSET_DATE_TIME (支持 2025-04-17T03:42:04.000+00:00)
try {
java.time.OffsetDateTime offsetDateTime = java.time.OffsetDateTime.parse(timeStr);
return Timestamp.valueOf(offsetDateTime.toLocalDateTime());
} catch (Exception e) {
// 继续尝试其他格式
}
// 2. 本地带T 2026-06-10T14:53:23.853
try {
java.time.LocalDateTime localDateTime = java.time.LocalDateTime.parse(timeStr);
return Timestamp.valueOf(localDateTime);
} catch (Exception ignored) {}
// 2. ISO_LOCAL_DATE_TIME (支持 2025-04-17T03:42:04)
try {
java.time.LocalDateTime localDateTime = java.time.LocalDateTime.parse(timeStr);
return Timestamp.valueOf(localDateTime);
} catch (Exception e) {
// 继续尝试其他格式
}
// 3. yyyy-MM-dd HH:mm:ss.SSS
try {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
LocalDateTime ldt = LocalDateTime.parse(timeStr, formatter);
return Timestamp.valueOf(ldt);
} catch (Exception ignored) {}
// 3. 自定义格式: yyyy-MM-dd HH:mm:ss
try {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime ldt = LocalDateTime.parse(timeStr, formatter);
return Timestamp.valueOf(ldt);
} catch (Exception e) {
// 继续尝试其他格式
}
// 4. yyyy-MM-dd HH:mm:ss
try {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime ldt = LocalDateTime.parse(timeStr, formatter);
return Timestamp.valueOf(ldt);
} catch (Exception ignored) {}
// 4. 日期格式: yyyy-MM-dd
try {
LocalDate ld = LocalDate.parse(timeStr, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
return Timestamp.valueOf(ld.atStartOfDay());
} catch (Exception e) {
// 继续尝试其他格式
}
// 5. 最后尝试直接转换
return Timestamp.valueOf(timeStr);
// 5. 日期 yyyy-MM-dd
try {
LocalDate ld = LocalDate.parse(timeStr, DateTimeFormatter.ISO_LOCAL_DATE);
return Timestamp.valueOf(ld.atStartOfDay());
} catch (Exception ignored) {}
// 6. 纯时分秒 HH:mm:ss解决MINS_TIME数组内时间
try {
DateTimeFormatter timeFmt = DateTimeFormatter.ofPattern("HH:mm:ss");
LocalTime lt = LocalTime.parse(timeStr, timeFmt);
LocalDate today = LocalDate.now(java.time.ZoneId.of("Asia/Shanghai"));
LocalDateTime ldt = LocalDateTime.of(today, lt);
return Timestamp.valueOf(ldt);
} catch (Exception e) {
log.warn("时间转换失败: {}, 返回null", timeStr);
return null;
log.error("所有格式均无法解析时间字符串:[{}],使用当日零点兜底", timeStr, e);
// 兜底返回当日零点防止NOT NULL字段插入null报错
return Timestamp.valueOf(LocalDate.now(java.time.ZoneId.of("Asia/Shanghai")).atStartOfDay());
}
}
/**
* 构建INSERT语句
*/
@@ -668,6 +733,7 @@ public class SyncTableParseService {
return sql.toString();
}
/**
* 执行批量插入
*/
@@ -684,6 +750,105 @@ public class SyncTableParseService {
return Arrays.stream(results).sum();
}
/**
* 专用带动态时间字段先删当日再批量插入
* @param tableName 表名
* @param columns 全表字段
* @param dataList 待入库数据
* @param fileDate 文件名内日期 yyyyMMdd
* @param timeColumn 配置的时间字段如TIME_ID
*/
private void deleteDayThenInsert(String tableName, List<String> columns, List<Map<String, Object>> dataList, String fileDate, String timeColumn) throws Exception {
// 读取本表CLOB字段配置
String clobText = tableClobCache.get(tableName.toUpperCase());
List<String> clobFields = Arrays.stream(clobText.split(","))
.map(String::trim)
.map(String::toUpperCase)
.collect(Collectors.toList());
// 动态拼接删除SQL使用配置的timeColumn
String deleteSql = String.format("DELETE FROM %s WHERE TRUNC(%s) = TO_DATE(?, 'yyyyMMdd')", tableName, timeColumn);
// 2INSERT SQL ?占位符无TO_CLOB
StringBuilder colSb = new StringBuilder();
StringBuilder valSb = new StringBuilder();
for (String col : columns) {
if (colSb.length() > 0) {
colSb.append(",");
valSb.append(",");
}
colSb.append(col);
valSb.append("?");
}
String insertSql = "INSERT INTO " + tableName + "(" + colSb + ") VALUES(" + valSb + ")";
Connection conn = targetJdbcTemplate.getDataSource().getConnection();
conn.setAutoCommit(false);
PreparedStatement delStmt = null;
PreparedStatement insStmt = null;
final int BATCH_COMMIT = 1000;
int batchCnt = 0;
try {
// 执行删除当日数据
delStmt = conn.prepareStatement(deleteSql);
delStmt.setString(1, fileDate);
int delRows = delStmt.executeUpdate();
log.info("专用入库:根据字段{}删除{}日原有{}条记录", timeColumn, fileDate, delRows);
insStmt = conn.prepareStatement(insertSql);
for (Map<String, Object> row : dataList) {
for (int i = 0; i < columns.size(); i++) {
String col = columns.get(i);
Object val = row.get(col);
int paramIndex = i + 1;
// CLOB文本使用原生Clob完整存储不截断
if (clobFields.contains(col) && val instanceof String) {
Clob clob = conn.createClob();
clob.setString(1, (String) val);
insStmt.setClob(paramIndex, clob);
} else {
insStmt.setObject(paramIndex, val);
}
}
insStmt.addBatch();
batchCnt++;
// 分段提交
if (batchCnt >= BATCH_COMMIT) {
insStmt.executeBatch();
batchCnt = 0;
}
}
// 提交剩余批次
if (batchCnt > 0) {
insStmt.executeBatch();
}
conn.commit();
log.info("专用入库完成,共插入{}条数据", dataList.size());
} catch (Exception e) {
conn.rollback();
log.error("{} 入库失败,事务回滚", tableName, e);
throw new Exception("入库异常", e);
} finally {
if (delStmt != null) delStmt.close();
if (insStmt != null) insStmt.close();
if (conn != null) conn.close();
}
}
/**
* 从文件名提取同步日期 yyyyMMdd
*/
private String extractDateFromFileName(String fileName) {
Pattern pattern = Pattern.compile("_(FULL|INC)_(\\d{8})\\.txt");
Matcher matcher = pattern.matcher(fileName);
if (matcher.find()) {
return matcher.group(2);
}
throw new RuntimeException("文件无法提取同步日期:" + fileName);
}
/**
* 手动导入指定的远程文件单个文件处理
*/
@@ -708,13 +873,11 @@ public class SyncTableParseService {
String tableName = extractTableNameFromFile(remoteFileName);
try {
// 缓存表配置
Map<String, String> tableConfigCache = syncData();
log.info("加载表配置缓存完成,共 {} 张表", tableConfigCache.size());
syncData();
// 导入数据
importJsonToTable(tableName, result.getLocalFilePath(),tableConfigCache);
importJsonToTable(tableName, result.getLocalFilePath());
// 导入成功后删除本地文件
deleteLocalFile(result.getLocalFilePath());
@@ -773,6 +936,26 @@ public class SyncTableParseService {
}
}
/**
* 判断当前字段是否配置为CLOB大文本字段
* @param col 字段名大写
* @param tableName 表名大写
* @param tableConfigCache 表配置缓存 key=表名大写 value=逗号分隔CLOB字段大写
*/
private boolean isClobColumn(String col, String tableName, Map<String, String> tableConfigCache) {
String clobColStr = tableConfigCache.get(tableName);
if (StrUtil.isBlank(clobColStr)) {
return false;
}
// 分割配置的CLOB字段列表统一大写匹配
List<String> clobCols = Arrays.stream(clobColStr.split(","))
.map(String::trim)
.map(String::toUpperCase)
.collect(Collectors.toList());
return clobCols.contains(col.toUpperCase());
}
/**
* 测试方法
*/

View File

@@ -0,0 +1,523 @@
package com.njcn.relational.service;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.njcn.relational.mapper.DynamicSyncMapper;
import com.njcn.relational.mapper.SyncTableConfigMapper;
import com.njcn.relational.pojo.bo.UploadResult;
import com.njcn.relational.pojo.po.SyncTableConfig;
import com.njcn.relational.pojo.po.SyncTableLogs;
import com.njcn.relational.serializer.BlobSerializer;
import com.njcn.relational.serializer.ClobSerializer;
import com.njcn.relational.utils.SftpUploadUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import java.io.*;
import java.sql.Blob;
import java.sql.Clob;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Service
public class SyncTwoDmExportService extends ServiceImpl<SyncTableConfigMapper, SyncTableConfig> {
@Autowired
private SyncTableLogsService syncTableLogsService;
@Autowired
private DynamicSyncMapper dynamicSyncMapper;
@Value("${sync.export.remotePath}")
private String remotePath;
@Value("${sync.export.localPath}")
private String localPath;
@Value("${sync.ip}")
private String ip;
@Value("${sync.port:22}")
private Integer port;
@Value("${sync.username}")
private String username;
@Value("${sync.password}")
private String password;
// 新增:表名 -> 时间字段 缓存
private Map<String, String> timeColumnCache = new HashMap<>();
private Map<String, String> dataTimeTypeCache = new HashMap<>();
private static final int BATCH_SIZE = 10000;
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private static final DateTimeFormatter FILE_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
// JSON序列化工具自动处理时间、换行符等
private final ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.registerModule(new SimpleModule() {{
addSerializer(Blob.class, new BlobSerializer());
addSerializer(Clob.class, new ClobSerializer());
}})
.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"))
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.enable(SerializationFeature.INDENT_OUTPUT)
.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
; // 格式化输出,可读性好
// 缓存当前Schema
private String currentSchema;
// 记录本次生成的所有文件
private final List<String> generatedFiles = new ArrayList<>();
@PostConstruct
public void init() {
currentSchema = dynamicSyncMapper.getCurrentUser();
log.info("当前数据库Schema: {}", currentSchema);
}
/**
* 获取所有启用的全量表(从配置表)
*/
public List<SyncTableConfig> getEnabledFullTables() {
LambdaQueryWrapper<SyncTableConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SyncTableConfig::getEnabled, 1)
.eq(SyncTableConfig::getSyncMode, "FULL")
.orderByAsc(SyncTableConfig::getSortOrder);
return this.list(wrapper);
}
/**
* 获取所有启用的增量表(从配置表)
*/
public List<SyncTableConfig> getEnabledIncrementTables() {
LambdaQueryWrapper<SyncTableConfig> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SyncTableConfig::getEnabled, 1)
.eq(SyncTableConfig::getSyncMode, "INC")
.orderByAsc(SyncTableConfig::getSortOrder);
return this.list(wrapper);
}
/**
* 确保本地目录存在
*/
private void ensureLocalDirectory() {
File dir = new File(localPath);
if (!dir.exists()) {
dir.mkdirs();
log.info("创建目录: {}", localPath);
}
}
/**
* 全量同步所有表
*/
public void syncAllFullTables(String dateStr) {
List<SyncTableConfig> tables = getEnabledFullTables();
log.info("========== 全量同步开始,共 {} 张表 ==========", tables.size());
// 确保目录存在
ensureLocalDirectory();
generatedFiles.clear();
for (SyncTableConfig config : tables) {
String tableName = config.getTableName();
try {
long startTime = System.currentTimeMillis();
/*IService<?> service = registry.getServiceByTableName(tableName);
if (service != null) {
String filePath = exportFullTableWithEntity(service, tableName, dateStr);
log.info("表 {} 使用实体方式导出完成", tableName);
} else {*/
String filePath = exportFullTableToJson(tableName, dateStr);
log.info("表 {} 使用Map方式导出完成", tableName);
// }
long duration = System.currentTimeMillis() - startTime;
log.info("表 {} 全量导出完成,耗时 {} ms", tableName, duration);
} catch (Exception e) {
log.error("表 {} 全量导出失败", tableName, e);
}
}
syncAllIncrementTables(dateStr);
}
/**
* 增量同步所有表
*/
public void syncAllIncrementTables(String dateStr) {
List<SyncTableConfig> tables = getEnabledIncrementTables();
timeColumnCache = tables.stream().filter(it -> StrUtil.isNotBlank(it.getTimeColumn())).collect(Collectors.toMap(SyncTableConfig::getTableName, SyncTableConfig::getTimeColumn));
dataTimeTypeCache = tables.stream().filter(it -> StrUtil.isNotBlank(it.getDataTimeType())).collect(Collectors.toMap(SyncTableConfig::getTableName, SyncTableConfig::getDataTimeType));
log.info("========== 增量同步开始,共 {} 张表 ==========", tables.size());
boolean localFlag = true;
boolean resultFlag = true;
for (SyncTableConfig config : tables) {
String tableName = config.getTableName();
try {
long startTime = System.currentTimeMillis();
String filePath = exportTodayDataToJson(tableName, dateStr);
log.info("表 {} 使用Map方式导出完成", tableName);
long duration = System.currentTimeMillis() - startTime;
log.info("表 {} 增量导出完成,耗时 {} ms", tableName, duration);
} catch (Exception e) {
log.error("表 {} 增量同步失败", tableName, e);
localFlag = false;
}
}
// 推送所有文件到横向隔离设备
if (!generatedFiles.isEmpty()) {
try {
uploadAllFiles();
} catch (Exception e) {
log.error("批量上传失败", e);
resultFlag = false;
}
} else {
resultFlag = false;
}
SyncTableLogs syncTableLogs = new SyncTableLogs();
syncTableLogs.setLocalResult(localFlag?1:0);
syncTableLogs.setResult((resultFlag&&localFlag) ? 1:0);
syncTableLogs.setTimeId(LocalDate.parse(dateStr, DateTimeFormatter.ofPattern("yyyyMMdd")));
syncTableLogs.setCreateTime(LocalDateTime.now());
syncTableLogsService.saveOrUpdate(syncTableLogs);
}
/**
* 上传所有生成的文件(批量上传)
*/
private void uploadAllFiles() throws Exception {
if (generatedFiles.isEmpty()) {
log.info("没有文件需要上传");
return;
}
List<UploadResult> resultList = SftpUploadUtil.batchUploadFiles(
ip, port, username, password, generatedFiles, remotePath
);
// 统计上传结果
long successCount = resultList.stream().filter(UploadResult::isSuccess).count();
long failCount = resultList.size() - successCount;
log.info("批量上传完成 - 成功: {}, 失败: {}, 总计: {}", successCount, failCount, resultList.size());
// 记录失败的文件
for (UploadResult result : resultList) {
if (!result.isSuccess()) {
log.error("上传失败文件: {}, 原因: {}", result.getLocalFilePath(), result.getErrorMessage());
}
}
}
/**
* Map方式全量导出为JSON保存为.txt文件
*/
@Transactional
public String exportFullTableToJson(String tableName, String dateStr) throws IOException {
String fileName = String.format("%s_FULL_%s.txt", tableName, dateStr);
String filePath = localPath + fileName;
log.info("开始导出全量表: {} -> {}", tableName, filePath);
List<Map<String, Object>> allData = new ArrayList<>();
int offset = 0;
while (true) {
List<Map<String, Object>> batchData = dynamicSyncMapper.selectPage(
tableName, BATCH_SIZE, offset);
if (batchData.isEmpty()) {
break;
}
allData.addAll(batchData);
log.info("表 {} 已读取 {} 条记录", tableName, allData.size());
if (batchData.size() < BATCH_SIZE) {
break;
}
offset += BATCH_SIZE;
}
// 写入JSON格式到.txt文件
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath))) {
objectMapper.writeValue(bos, allData);
}
// 记录生成的文件
generatedFiles.add(filePath);
log.info("表 {} 全量导出完成,共 {} 条记录,文件: {}", tableName, allData.size(), filePath);
return filePath;
}
/**
* Map方式增量导出为JSON保存为.txt文件
*/
@Transactional
public String exportTodayDataToJson(String tableName, String dateStr) throws IOException {
// 1. 判断表属于哪种周期D/M/Q/Y
String periodType = getTablePeriodType(tableName);
if (periodType == null) {
log.error("表 {} 无法识别周期类型,跳过增量导出", tableName);
return null;
}
// 2. 根据周期构建 开始/结束 时间
Map<String, String> timeRange = buildPeriodTimeRange(periodType, dateStr);
String dateStart = timeRange.get("start");
String dateEnd = timeRange.get("end");
log.info("导出表 {} 数据: {} 至 {}", tableName, dateStart, dateEnd);
String timeColumn = getTimeColumn(tableName);
List<Map<String, Object>> queryData = dynamicSyncMapper.selectTodayData(
tableName, timeColumn, dateStart, dateEnd);
if (queryData.isEmpty()) {
log.info("表 {} {} - {}无数据", tableName, dateStart, dateEnd);
return null;
}
String fileName = String.format("%s_INC_%s.txt", tableName, dateStr);
String filePath = localPath + fileName;
// 写入JSON格式到.txt文件
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath))) {
objectMapper.writeValue(bos, queryData);
}
// 记录生成的文件
generatedFiles.add(filePath);
log.info("表 {} {}数据导出完成,共 {} 条记录,文件: {}", tableName, dateStr, queryData.size(), filePath);
return filePath;
}
/**
* 从表名识别周期类型
* D=天, M=月, Q=季, Y=年
*/
private String getTablePeriodType(String tableName) {
String dTypeCreateTitle = dataTimeTypeCache.get(tableName);
if (dTypeCreateTitle != null) {
if ("D".equals(dTypeCreateTitle) || "M".equalsIgnoreCase(tableName) || "Q".equals(tableName) || "Y".equals(tableName)) {
return dTypeCreateTitle;
}
} else {
if (tableName.endsWith("_D")) {
return "D";
}
if (tableName.endsWith("_M")) {
return "M";
}
if (tableName.endsWith("_Q")) {
return "Q";
}
if (tableName.endsWith("_Y")) {
return "Y";
}
}
return null;
}
/**
* 根据周期 & 入参日期计算【当前周期】时间范围JDK8 兼容)
*/
private Map<String, String> buildPeriodTimeRange(String periodType, String dateStr) {
DateTime baseDate = DateUtil.parse(dateStr, DatePattern.PURE_DATE_PATTERN);
String start;
String end;
// JDK8 标准 switch
switch (periodType) {
case "D":
// 入参日期 当天
start = DateUtil.format(DateUtil.beginOfDay(baseDate), DatePattern.NORM_DATETIME_PATTERN);
end = DateUtil.format(DateUtil.endOfDay(baseDate), DatePattern.NORM_DATETIME_PATTERN);
break;
case "M":
// 入参日期 所在整月
start = DateUtil.format(DateUtil.beginOfMonth(baseDate), DatePattern.NORM_DATETIME_PATTERN);
end = DateUtil.format(DateUtil.endOfMonth(baseDate), DatePattern.NORM_DATETIME_PATTERN);
break;
case "Q":
// 入参日期 所在整季度
start = DateUtil.format(DateUtil.beginOfQuarter(baseDate), DatePattern.NORM_DATETIME_PATTERN);
end = DateUtil.format(DateUtil.endOfQuarter(baseDate), DatePattern.NORM_DATETIME_PATTERN);
break;
case "Y":
// 入参日期 所在整年
start = DateUtil.format(DateUtil.beginOfYear(baseDate), DatePattern.NORM_DATETIME_PATTERN);
end = DateUtil.format(DateUtil.endOfYear(baseDate), DatePattern.NORM_DATETIME_PATTERN);
break;
default:
throw new IllegalArgumentException("不支持的周期类型:" + periodType);
}
Map<String, String> map = new HashMap<>(2);
map.put("start", start);
map.put("end", end);
return map;
}
/**
* 获取时间字段
*/
private String getTimeColumn(String tableName) {
String createTitle = timeColumnCache.get(tableName);
if (Objects.isNull(createTitle)) {
switch (tableName) {
case "R_STAT_DATA_FLICKER_D":
case "R_STAT_DATA_FLUC_D":
case "R_STAT_DATA_HARMPHASIC_I_D":
case "R_STAT_DATA_HARMPHASIC_V_D":
case "R_STAT_DATA_HARMPOWER_P_D":
case "R_STAT_DATA_HARMPOWER_Q_D":
case "R_STAT_DATA_HARMPOWER_S_D":
case "R_STAT_DATA_HARMRATE_I_D":
case "R_STAT_DATA_HARMRATE_V_D":
case "R_STAT_DATA_INHARM_I_D":
case "R_STAT_DATA_INHARM_V_D":
case "R_STAT_DATA_I_D":
case "R_STAT_DATA_PLT_D":
case "R_STAT_DATA_V_D":
return "TIME";
// 新增表
case "R_STAT_COMASSES_D":
case "R_STAT_ASSES_D":
case "R_STAT_LIMIT_RATE_D":
case "R_STAT_ONLINERATE_D":
case "R_STAT_INTEGRITY_D":
case "R_STAT_ORG_INTEGRITY_D":
case "R_STAT_LIMIT_RATE_DETAIL_D":
case "R_STAT_LIMIT_TARGET_D":
return "TIME_ID";
case "R_MP_POLLUTION_D":
case "R_MP_V_THD":
case "R_STAT_POLLUTION_ORG_D":
case "R_STAT_POLLUTION_ORG_M":
case "R_STAT_POLLUTION_ORG_Q":
case "R_STAT_POLLUTION_ORG_Y":
case "R_STAT_POLLUTION_SUBSTATION_D":
case "R_STAT_POLLUTION_SUBSTATION_M":
case "R_STAT_POLLUTION_SUBSTATION_Q":
case "R_STAT_POLLUTION_SUBSTATION_Y":
return "DATA_DATE";
default:
log.warn("表 {} 未配置时间字段,跳过", tableName);
return "CREATE_TIME";
}
} else {
return createTitle;
}
}
public void test() {
try {
// 确保目录存在
File localDir = new File(localPath);
if (!localDir.exists()) {
localDir.mkdirs();
}
// 生成txt文件
String filePath = localPath + "test.txt";
try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) {
writer.write("test");
}
// 上传到SFTP服务器
SftpUploadUtil.uploadFile(ip, port, username, password, filePath, remotePath);
log.info("test.txt文件生成并上传成功");
} catch (Exception e) {
log.error("失败", e);
}
}
public List<String> getPendingFiles() {
List<String> matchedFiles = new ArrayList<>();
Vector<?> files;
try {
// 列目录也纳入异常捕获
files = SftpUploadUtil.listFiles(ip, port, username, password, remotePath);
for (Object obj : files) {
if (obj instanceof com.jcraft.jsch.ChannelSftp.LsEntry) {
com.jcraft.jsch.ChannelSftp.LsEntry entry = (com.jcraft.jsch.ChannelSftp.LsEntry) obj;
String fileName = entry.getFilename();
matchedFiles.add(fileName);
}
}
} catch (Exception e) {
log.error("获取待处理文件列表失败,远端目录:{}", remotePath, e);
return new ArrayList<>();
}
return matchedFiles;
}
/**
* 清理本地已处理过的文件
*/
public void cleanLocalFiles(int daysToKeep) {
File localDir = new File(localPath);
if (!localDir.exists()) {
return;
}
long cutoffTime = System.currentTimeMillis() - (daysToKeep * 24L * 60 * 60 * 1000);
File[] files = localDir.listFiles();
if (files != null) {
int deletedCount = 0;
for (File file : files) {
if (file.isFile() && file.lastModified() < cutoffTime) {
file.delete();
deletedCount++;
}
}
log.info("清理了10.11.7.5二区服务器 {} 个七天前的本地txt文件", deletedCount);
}
}
}

View File

@@ -4,8 +4,7 @@ import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.njcn.relational.pojo.dto.DcloudSyncQuery;
import com.njcn.relational.service.DcloudBusService;
import com.njcn.relational.service.SgConPlantCService;
import com.njcn.relational.service.*;
import dcloud.common.InnerServiceBus.ServiceBus;
import dcloud.model.common.ConModel;
import lombok.RequiredArgsConstructor;
@@ -28,6 +27,37 @@ public class DcloudBusServiceImpl implements DcloudBusService {
private final SgConPlantCService sgConPlantCService;
private final SGConPlantBService sgConPlantBService;
private final SGConSubstationBService sgConSubstationBService;
private final SGConSubstationCService sgConSubstationCService;
/**
* 数据入库入口
*/
@Override
public boolean syncDataToDb(DcloudSyncQuery query) {
log.info("开始从云平台同步厂站数据到本地数据库");
ServiceBus serviceBus = initServiceBus();
if (serviceBus == null) {
log.error("ServiceBus初始化失败终止同步");
return false;
}
try {
handleConModelSync(serviceBus, query);
log.info("厂站数据同步完成");
return true;
} catch (Exception e) {
log.error("厂站数据同步失败", e);
return false;
}
}
/**
* 统一同步入口
*/
@@ -175,7 +205,20 @@ public class DcloudBusServiceImpl implements DcloudBusService {
log.info("[{}] 第{}页查询到 {} 条数据,开始入库", tableName, pageIndex, pageData.size());
// 分批入库(每页数据直接入库)
boolean batchResult = sgConPlantCService.batchSyncFromDcloud(pageData);
boolean batchResult = false;
if("SG_CON_PLANT_C".equals(tableName)) {
log.info("【全量同步入口】执行同步厂站基础数据---------");
batchResult = sgConPlantCService.batchSyncFromDcloud(pageData);
}else if("SG_CON_PLANT_B".equals(tableName)){
log.info("【全量同步入口】执行同步厂站绑定数据1111111111");
batchResult = sgConPlantBService.batchSyncFromDcloud(pageData);
}else if("SG_CON_SUBSTATION_B".equals(tableName)){
log.info("【全量同步入口】执行变电站站基础数据---------");
batchResult = sgConSubstationBService.batchSyncFromDcloud(pageData);
}else if("SG_CON_SUBSTATION_C".equals(tableName)){
log.info("【全量同步入口】执行变电站站绑定数据---------");
batchResult = sgConSubstationCService.batchSyncFromDcloud(pageData);
}
if (batchResult) {
successCount += pageData.size();
log.info("[{}] 第{}页入库成功,当前累计成功 {} 条", tableName, pageIndex, successCount);
@@ -192,30 +235,6 @@ public class DcloudBusServiceImpl implements DcloudBusService {
}
}
/**
* 数据入库入口
*/
@Override
public boolean syncDataToDb() {
log.info("开始从云平台同步厂站数据到本地数据库");
DcloudSyncQuery query = DcloudSyncQuery.buildPlantDefault(null, null, 1, DEFAULT_PAGE_SIZE);
ServiceBus serviceBus = initServiceBus();
if (serviceBus == null) {
log.error("ServiceBus初始化失败终止同步");
return false;
}
try {
handleConModelSync(serviceBus, query);
log.info("厂站数据同步完成");
return true;
} catch (Exception e) {
log.error("厂站数据同步失败", e);
return false;
}
}
// ===================== 便捷方法 =====================

View File

@@ -1,21 +0,0 @@
package com.njcn.relational.service.impl;
import com.njcn.relational.service.InsertIDataFlicker;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* Description:
* Date: 2024/11/18 11:17【需求编号】
*
* @author clam
* @version V1.0.0
*/
@Service
@RequiredArgsConstructor
public class InsertDataFlickerImpl implements InsertIDataFlicker {
}

View File

@@ -1,21 +0,0 @@
package com.njcn.relational.service.impl;
import com.njcn.relational.service.InsertIDataFluc;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* Description:
* Date: 2024/11/18 11:17【需求编号】
*
* @author clam
* @version V1.0.0
*/
@Service
@RequiredArgsConstructor
public class InsertDataFlucImpl implements InsertIDataFluc {
}

View File

@@ -1,18 +0,0 @@
package com.njcn.relational.service.impl;
import com.njcn.relational.service.InsertIDataHarmRateI;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* @author xy
*/
@Service
@RequiredArgsConstructor
public class InsertDataHarmRateIImpl implements InsertIDataHarmRateI {
}

View File

@@ -1,17 +0,0 @@
package com.njcn.relational.service.impl;
import com.njcn.relational.service.InsertIDataHarmRateV;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* @author xy
*/
@Service
@RequiredArgsConstructor
public class InsertDataHarmRateVImpl implements InsertIDataHarmRateV {
}

View File

@@ -1,22 +0,0 @@
package com.njcn.relational.service.impl;
import com.njcn.relational.service.InsertIDataHarmphasicI;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* Description:
* Date: 2024/11/18 11:17【需求编号】
*
* @author clam
* @version V1.0.0
*/
@Service
@RequiredArgsConstructor
public class InsertDataHarmphasicIImpl implements InsertIDataHarmphasicI {
}

View File

@@ -1,21 +0,0 @@
package com.njcn.relational.service.impl;
import com.njcn.relational.service.InsertIDataHarmphasicV;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* Description:
* Date: 2024/11/18 11:17【需求编号】
*
* @author clam
* @version V1.0.0
*/
@Service
@RequiredArgsConstructor
public class InsertDataHarmphasicVImpl implements InsertIDataHarmphasicV {
}

View File

@@ -1,21 +0,0 @@
package com.njcn.relational.service.impl;
import com.njcn.relational.service.InsertIDataHarmpowerP;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* Description:
* Date: 2024/11/18 11:17【需求编号】
*
* @author clam
* @version V1.0.0
*/
@Service
@RequiredArgsConstructor
public class InsertDataHarmpowerPImpl implements InsertIDataHarmpowerP {
}

View File

@@ -1,21 +0,0 @@
package com.njcn.relational.service.impl;
import com.njcn.relational.service.InsertIDataHarmpowerQ;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* Description:
* Date: 2024/11/18 11:17【需求编号】
*
* @author clam
* @version V1.0.0
*/
@Service
@RequiredArgsConstructor
public class InsertDataHarmpowerQImpl implements InsertIDataHarmpowerQ {
}

View File

@@ -1,22 +0,0 @@
package com.njcn.relational.service.impl;
import com.njcn.relational.service.InsertIDataHarmpowerS;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* Description:
* Date: 2024/11/18 11:17【需求编号】
*
* @author clam
* @version V1.0.0
*/
@Service
@RequiredArgsConstructor
public class InsertDataHarmpowerSImpl implements InsertIDataHarmpowerS {
}

View File

@@ -1,26 +0,0 @@
package com.njcn.relational.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import com.njcn.relational.service.InsertIDataI;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.time.ZoneId;
import java.util.ArrayList;
/**
* Description:
* Date: 2024/11/18 11:17【需求编号】
*
* @author clam
* @version V1.0.0
*/
@Service
@RequiredArgsConstructor
public class InsertDataIImpl implements InsertIDataI {
}

View File

@@ -1,21 +0,0 @@
package com.njcn.relational.service.impl;
import com.njcn.relational.service.InsertIDataInharmI;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* Description:
* Date: 2024/11/18 13:27【需求编号】
*
* @author clam
* @version V1.0.0
*/
@Service
@RequiredArgsConstructor
public class InsertDataInharmIImpl implements InsertIDataInharmI {
}

View File

@@ -1,22 +0,0 @@
package com.njcn.relational.service.impl;
import com.njcn.relational.service.InsertIDataInharmV;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* Description:
* Date: 2024/11/18 13:27【需求编号】
*
* @author clam
* @version V1.0.0
*/
@Service
@RequiredArgsConstructor
public class InsertDataInharmVImpl implements InsertIDataInharmV {
}

View File

@@ -1,20 +0,0 @@
package com.njcn.relational.service.impl;
import com.njcn.relational.service.InsertIDataPlt;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* Description:
* Date: 2024/11/18 11:17【需求编号】
*
* @author clam
* @version V1.0.0
*/
@Service
@RequiredArgsConstructor
public class InsertDataPltImpl implements InsertIDataPlt {
}

View File

@@ -1,24 +0,0 @@
package com.njcn.relational.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcn.relational.mapper.RStatDataVRelationMapper;
import com.njcn.relational.pojo.po.RStatDataVD;
import com.njcn.relational.service.InsertIDataI;
import com.njcn.relational.service.InsertIDataV;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* @author hongawen
* @version 1.0
* @data 2024/11/7 10:54
*/
@Service
@RequiredArgsConstructor
public class InsertDataVImpl extends ServiceImpl<RStatDataVRelationMapper, RStatDataVD> implements InsertIDataI {
}

View File

@@ -0,0 +1,94 @@
package com.njcn.relational.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcn.relational.mapper.SGConPlantBMapper;
import com.njcn.relational.pojo.po.SGConPlantB;
import com.njcn.relational.service.SGConPlantBService;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class SGConPlantBServiceImpl extends ServiceImpl<SGConPlantBMapper, SGConPlantB>
implements SGConPlantBService {
private static final int BATCH_SIZE = 500;
@Override
public boolean batchSyncFromDcloud(JSONArray dataArr) {
if (CollUtil.isEmpty(dataArr)) {
return true;
}
int batchSize = 500;
List<SGConPlantB> entityList = new ArrayList<>(dataArr.size());
for (Object obj : dataArr) {
JSONObject json = (JSONObject) obj;
SGConPlantB plant = new SGConPlantB();
// 主键必存在
if (json.containsKey("ID")) {
plant.setId(json.getString("ID"));
}
// 字符串字段,存在则赋值
if (json.containsKey("ADDRESS")) plant.setAddress(json.getString("ADDRESS"));
if (json.containsKey("CHECK_CODE")) plant.setCheckCode(json.getString("CHECK_CODE"));
if (json.containsKey("COMM_TAG")) plant.setCommTag(json.getString("COMM_TAG"));
if (json.containsKey("COMPANY_ID")) plant.setCompanyId(json.getString("COMPANY_ID"));
if (json.containsKey("CONNECTIVE_PG_ID")) plant.setConnectivePgId(json.getString("CONNECTIVE_PG_ID"));
if (json.containsKey("DCC_ID")) plant.setDccId(json.getString("DCC_ID"));
if (json.containsKey("FAX_NO")) plant.setFaxNo(json.getString("FAX_NO"));
if (json.containsKey("GENCOMPANY_ID")) plant.setGencompanyId(json.getString("GENCOMPANY_ID"));
if (json.containsKey("NAME")) plant.setName(json.getString("NAME"));
if (json.containsKey("NAME_ABBREVIATION")) plant.setNameAbbreviation(json.getString("NAME_ABBREVIATION"));
if (json.containsKey("OWNER")) plant.setOwner(json.getString("OWNER"));
if (json.containsKey("PHONE_NO")) plant.setPhoneNo(json.getString("PHONE_NO"));
if (json.containsKey("POSTCODE")) plant.setPostcode(json.getString("POSTCODE"));
if (json.containsKey("REGISTER_NAME")) plant.setRegisterName(json.getString("REGISTER_NAME"));
if (json.containsKey("REMARKS")) plant.setRemarks(json.getString("REMARKS"));
if (json.containsKey("SCS_TAG")) plant.setScsTag(json.getString("SCS_TAG"));
if (json.containsKey("STAMP")) plant.setStamp(json.getString("STAMP"));
if (json.containsKey("STATE_CODE")) plant.setStateCode(json.getString("STATE_CODE"));
if (json.containsKey("SYS_FLAG")) plant.setSysFlag(json.getString("SYS_FLAG"));
if (json.containsKey("TAG")) plant.setTag(json.getString("TAG"));
// Int数字字段
if (json.containsKey("ASSETS_OWNERSHIP")) plant.setAssetsOwnership(json.getInteger("ASSETS_OWNERSHIP"));
if (json.containsKey("LANDFORM")) plant.setLandform(json.getInteger("LANDFORM"));
if (json.containsKey("MAX_VOLTAGE_TYPE")) plant.setMaxVoltageType(json.getInteger("MAX_VOLTAGE_TYPE"));
if (json.containsKey("OPERATE_STATE")) plant.setOperateState(json.getInteger("OPERATE_STATE"));
if (json.containsKey("PLANT_DETAIL_TYPE")) plant.setPlantDetailType(json.getInteger("PLANT_DETAIL_TYPE"));
if (json.containsKey("PLANT_TYPE")) plant.setPlantType(json.getInteger("PLANT_TYPE"));
if (json.containsKey("REGION")) plant.setRegion(json.getInteger("REGION"));
// BigDecimal 浮点字段
if (json.containsKey("ALTITUDE")) plant.setAltitude(json.getBigDecimal("ALTITUDE"));
if (json.containsKey("LATITUDE")) plant.setLatitude(json.getBigDecimal("LATITUDE"));
if (json.containsKey("LONGITUDE")) plant.setLongitude(json.getBigDecimal("LONGITUDE"));
// 日期字段 字符串转Date
if (json.containsKey("EXPIRY_DATE")) {
String dateStr = json.getString("EXPIRY_DATE");
if (StrUtil.isNotBlank(dateStr)) {
plant.setExpiryDate(DateUtil.parseDate(dateStr));
}
}
if (json.containsKey("OPERATE_DATE")) {
String dateStr = json.getString("OPERATE_DATE");
if (StrUtil.isNotBlank(dateStr)) {
plant.setOperateDate(DateUtil.parseDate(dateStr));
}
}
entityList.add(plant);
}
// 根据主键ID存在更新不存在新增
return saveOrUpdateBatch(entityList, batchSize);
}
}

View File

@@ -0,0 +1,128 @@
package com.njcn.relational.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcn.relational.mapper.SGConSubstationBMapper;
import com.njcn.relational.pojo.po.SGConSubstationB;
import com.njcn.relational.service.SGConSubstationBService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
@Slf4j
public class SGConSubstationBServiceImpl extends ServiceImpl<SGConSubstationBMapper, SGConSubstationB>
implements SGConSubstationBService {
private static final int BATCH_SIZE = 500;
@Override
public boolean batchSyncFromDcloud(JSONArray dataArr) throws Exception{
if (CollUtil.isEmpty(dataArr)) {
return true;
}
List<SGConSubstationB> entityList = new ArrayList<>(dataArr.size());
for (Object obj : dataArr) {
JSONObject json = (JSONObject) obj;
SGConSubstationB sub = new SGConSubstationB();
// 主键ID必传
if (json.containsKey("ID")) {
sub.setId(json.getString("ID"));
}
// 字符串字段,存在才赋值
if (json.containsKey("ASSETS_OWNERSHIP_COM_ID")) {
sub.setAssetsOwnershipComId(json.getString("ASSETS_OWNERSHIP_COM_ID"));
}
if (json.containsKey("CHECK_CODE")) {
sub.setCheckCode(json.getString("CHECK_CODE"));
}
if (json.containsKey("COMM_TAG")) {
sub.setCommTag(json.getString("COMM_TAG"));
}
if (json.containsKey("DCC_ID")) {
sub.setDccId(json.getString("DCC_ID"));
}
if (json.containsKey("MANAGE_DEPT_ID")) {
sub.setManageDeptId(json.getString("MANAGE_DEPT_ID"));
}
if (json.containsKey("NAME")) {
sub.setName(json.getString("NAME"));
}
if (json.containsKey("OWNER")) {
sub.setOwner(json.getString("OWNER"));
}
if (json.containsKey("PG_ID")) {
sub.setPgId(json.getString("PG_ID"));
}
if (json.containsKey("SCS_TAG")) {
sub.setScsTag(json.getString("SCS_TAG"));
}
if (json.containsKey("STATE_CODE")) {
sub.setStateCode(json.getString("STATE_CODE"));
}
if (json.containsKey("SYS_FLAG")) {
sub.setSysFlag(json.getString("SYS_FLAG"));
}
if (json.containsKey("VIDEOSCENEID")) {
sub.setVideosceneid(json.getString("VIDEOSCENEID"));
}
// 整型数字字段
if (json.containsKey("ASSETS_OWNERSHIP")) {
sub.setAssetsOwnership(json.getInteger("ASSETS_OWNERSHIP"));
}
if (json.containsKey("DC_VOLTAGE_TYPE")) {
sub.setDcVoltageType(json.getInteger("DC_VOLTAGE_TYPE"));
}
if (json.containsKey("OPERATE_STATE")) {
sub.setOperateState(json.getInteger("OPERATE_STATE"));
}
if (json.containsKey("REGION")) {
sub.setRegion(json.getInteger("REGION"));
}
if (json.containsKey("TOP_AC_VOLTAGE_TYPE")) {
sub.setTopAcVoltageType(json.getInteger("TOP_AC_VOLTAGE_TYPE"));
}
if (json.containsKey("TYPE")) {
sub.setType(json.getInteger("TYPE"));
}
// 高精度小数
if (json.containsKey("ALTITUDE")) {
sub.setAltitude(json.getBigDecimal("ALTITUDE"));
}
if (json.containsKey("LATITUDE")) {
sub.setLatitude(json.getBigDecimal("LATITUDE"));
}
if (json.containsKey("LONGITUDE")) {
sub.setLongitude(json.getBigDecimal("LONGITUDE"));
}
// 日期字段 字符串转Date
if (json.containsKey("EXPIRY_DATE")) {
String dateStr = json.getString("EXPIRY_DATE");
if (StrUtil.isNotBlank(dateStr)) {
sub.setExpiryDate(DateUtil.parseDate(dateStr));
}
}
if (json.containsKey("OPERATE_DATE")) {
String dateStr = json.getString("OPERATE_DATE");
if (StrUtil.isNotBlank(dateStr)) {
sub.setOperateDate(DateUtil.parseDate(dateStr));
}
}
entityList.add(sub);
}
// 根据主键ID判断新增/更新
return saveOrUpdateBatch(entityList, BATCH_SIZE);
}
}

View File

@@ -0,0 +1,87 @@
package com.njcn.relational.service.impl;
import cn.hutool.core.collection.CollUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcn.relational.mapper.SGConSubstationCMapper;
import com.njcn.relational.pojo.po.SGConSubstationBind;
import com.njcn.relational.service.SGConSubstationCService;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class SGConSubstationCServiceImpl extends ServiceImpl<SGConSubstationCMapper, SGConSubstationBind>
implements SGConSubstationCService {
/**
* 根据联合主键(DCC_ID + DCLOUD_ID)查询单条映射
*/
public SGConSubstationBind getByUnionKey(String dccId, String dcloudId) {
LambdaQueryWrapper<SGConSubstationBind> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SGConSubstationBind::getDccId, dccId)
.eq(SGConSubstationBind::getDcloudId, dcloudId)
.eq(SGConSubstationBind::getIsDelete, 0);
return getOne(wrapper);
}
/**
* 逻辑删除:根据联合主键软删
*/
public boolean logicRemoveByUnionKey(String dccId, String dcloudId) {
LambdaUpdateWrapper<SGConSubstationBind> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(SGConSubstationBind::getIsDelete, 1)
.eq(SGConSubstationBind::getDccId, dccId)
.eq(SGConSubstationBind::getDcloudId, dcloudId);
return update(updateWrapper);
}
private static final int BATCH_SIZE = 500;
@Override
public boolean batchSyncFromDcloud(JSONArray dataArr) {
if (CollUtil.isEmpty(dataArr)) {
return true;
}
List<SGConSubstationBind> entityList = new ArrayList<>(dataArr.size());
for (Object obj : dataArr) {
JSONObject json = (JSONObject) obj;
SGConSubstationBind bind = new SGConSubstationBind();
// 联合主键两字段必存在
if (json.containsKey("DCC_ID")) {
bind.setDccId(json.getString("DCC_ID"));
}
if (json.containsKey("DCLOUD_ID")) {
bind.setDcloudId(json.getString("DCLOUD_ID"));
}
// 字符串字段存在key才赋值
if (json.containsKey("D5000_ID")) bind.setD5000Id(json.getString("D5000_ID"));
if (json.containsKey("D5000_NAME")) bind.setD5000Name(json.getString("D5000_NAME"));
if (json.containsKey("DCLOUD_NAME")) bind.setDcloudName(json.getString("DCLOUD_NAME"));
if (json.containsKey("DCLOUD_VOLTAGELEVEL")) bind.setDcloudVoltagelevel(json.getString("DCLOUD_VOLTAGELEVEL"));
if (json.containsKey("OMS_ID")) bind.setOmsId(json.getString("OMS_ID"));
if (json.containsKey("OMS_NAME")) bind.setOmsName(json.getString("OMS_NAME"));
if (json.containsKey("OWNER")) bind.setOwner(json.getString("OWNER"));
if (json.containsKey("PMS_ID")) bind.setPmsId(json.getString("PMS_ID"));
if (json.containsKey("PSDB_DEV_TYPE")) bind.setPsdbDevType(json.getString("PSDB_DEV_TYPE"));
if (json.containsKey("PSDB_ID")) bind.setPsdbId(json.getString("PSDB_ID"));
if (json.containsKey("PSDB_NAME")) bind.setPsdbName(json.getString("PSDB_NAME"));
if (json.containsKey("SETTING_ID")) bind.setSettingId(json.getString("SETTING_ID"));
if (json.containsKey("SETTING_NAME")) bind.setSettingName(json.getString("SETTING_NAME"));
if (json.containsKey("STATUS")) bind.setStatus(json.getString("STATUS"));
// 数字字段
if (json.containsKey("IS_DELETE")) bind.setIsDelete(json.getInteger("IS_DELETE"));
entityList.add(bind);
}
// 根据主键ID判断新增/更新
return saveOrUpdateBatch(entityList, BATCH_SIZE);
}
}

View File

@@ -0,0 +1,17 @@
package com.njcn.relational.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcn.relational.mapper.SyncTableConfigMapper;
import com.njcn.relational.pojo.po.SyncTableConfig;
import com.njcn.relational.service.SyncTableConfigService;
import org.springframework.stereotype.Service;
/**
* @Author: cdf
* @CreateTime: 2026-06-28
* @Description: 二区达梦数据导出服务实现类
*/
@Service
public class SyncTableConfigServiceImpl extends ServiceImpl<SyncTableConfigMapper, SyncTableConfig> implements SyncTableConfigService{
}

View File

@@ -0,0 +1,126 @@
package com.njcn.relational.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.njcn.relational.mapper.SyncTableLogsMapper;
import com.njcn.relational.pojo.po.SyncTableLogs;
import com.njcn.relational.service.SyncTableLogsService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Service
public class SyncTableLogsServiceImpl extends ServiceImpl<SyncTableLogsMapper, SyncTableLogs>
implements SyncTableLogsService {
@Override
public IPage<SyncTableLogs> pageQuery(Integer pageNum, Integer pageSize, LocalDate timeId, Integer result) {
Page<SyncTableLogs> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<SyncTableLogs> wrapper = new LambdaQueryWrapper<>();
if (timeId != null) {
wrapper.eq(SyncTableLogs::getTimeId, timeId);
}
if (result != null) {
wrapper.eq(SyncTableLogs::getResult, result);
}
// 按创建时间倒序
wrapper.orderByDesc(SyncTableLogs::getCreateTime);
return this.page(page, wrapper);
}
@Override
public List<SyncTableLogs> listByTimeId(LocalDate timeId) {
LambdaQueryWrapper<SyncTableLogs> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SyncTableLogs::getTimeId, timeId)
.orderByDesc(SyncTableLogs::getCreateTime);
return this.list(wrapper);
}
@Override
public List<SyncTableLogs> listByTimeIdAndResult(LocalDate timeId, Integer result) {
LambdaQueryWrapper<SyncTableLogs> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SyncTableLogs::getTimeId, timeId)
.eq(SyncTableLogs::getResult, result)
.orderByDesc(SyncTableLogs::getCreateTime);
return this.list(wrapper);
}
@Override
public boolean existsByTimeId(LocalDate timeId) {
LambdaQueryWrapper<SyncTableLogs> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SyncTableLogs::getTimeId, timeId);
return this.count(wrapper) > 0;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean batchInsert(List<SyncTableLogs> logs) {
if (logs == null || logs.isEmpty()) {
return false;
}
// 设置创建时间
logs.forEach(log -> {
if (log.getCreateTime() == null) {
log.setCreateTime(LocalDateTime.now());
}
});
return this.saveBatch(logs);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteByTimeId(LocalDate timeId) {
LambdaQueryWrapper<SyncTableLogs> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SyncTableLogs::getTimeId, timeId);
return this.remove(wrapper);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteByTimeIdAndResult(LocalDate timeId, Integer result) {
LambdaQueryWrapper<SyncTableLogs> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SyncTableLogs::getTimeId, timeId)
.eq(SyncTableLogs::getResult, result);
return this.remove(wrapper);
}
@Override
public int countByTimeIdAndResult(LocalDate timeId, Integer result) {
return 0;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean saveOrUpdateLog(SyncTableLogs log) {
if (log == null) {
return false;
}
// 先查询是否存在
LambdaQueryWrapper<SyncTableLogs> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SyncTableLogs::getTimeId, log.getTimeId())
.eq(SyncTableLogs::getResult, log.getResult());
SyncTableLogs exist = this.getOne(wrapper);
if (exist == null) {
// 不存在则新增
if (log.getCreateTime() == null) {
log.setCreateTime(LocalDateTime.now());
}
return this.save(log);
} else {
// 存在则更新(保留原创建时间)
log.setCreateTime(exist.getCreateTime());
return this.update(log, wrapper);
}
}
}

View File

@@ -0,0 +1,9 @@
package com.njcn.relational.utils;
/**
* @Author: cdf
* @CreateTime: 2026-06-26
* @Description:
*/
public class LnBusUtils {
}

View File

@@ -3,8 +3,8 @@ spring:
datasource:
druid:
driver-class-name: dm.jdbc.driver.DmDriver
url: jdbc:dm://127.0.0.1:5236/PQSADMIN?useUnicode=true&characterEncoding=utf-8
username: PQSADMIN
url: jdbc:dm://127.0.0.1:5236/PQSADMIN_LN?useUnicode=true&characterEncoding=utf-8
username: PQSADMIN_LN
password: Pqsadmin123
#初始化建立物理连接的个数、最小、最大连接数
initial-size: 5
@@ -29,7 +29,7 @@ sync:
password: dnzl@#001
import:
localPath: D:/data/import/
remotePath: /home/export/
remotePath: D:/data/export/
export:
localPath: D:/data/export/
remotePath: /home/export/

View File

@@ -3,7 +3,7 @@ spring:
datasource:
druid:
driver-class-name: dm.jdbc.driver.DmDriver
url: jdbc:dm://192.168.1.21:5236/PQSADMIN?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=CTT
url: jdbc:dm://127.0.0.1:5236/PQSADMIN?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=CTT
username: PQSADMIN
password: Pqsadmin123
#初始化建立物理连接的个数、最小、最大连接数
@@ -29,7 +29,7 @@ sync:
password: dnzl@#001
export:
localPath: D:/data/export/
remotePath: /home/export/
remotePath: D:/home/export/
import:
localPath: D:/data/export/
remotePath: /home/export/

View File

@@ -9,7 +9,7 @@ spring:
application:
name: dmTransport
profiles:
#active: query_up
#active: 二区:query_up 三区insert_up
active: insert_up
security:
user:

View File

@@ -0,0 +1,50 @@
<!-- 同步表配置对话框 -->
<el-dialog
:title="dialogTitle"
:visible.sync="dialogVisible"
width="600px"
@close="handleDialogClose"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="120px"
size="small"
>
<el-form-item label="表名" prop="tableName">
<el-input v-model="formData.tableName" placeholder="请输入表名" :disabled="isEdit"></el-input>
</el-form-item>
<el-form-item label="同步模式" prop="syncMode">
<el-select v-model="formData.syncMode" placeholder="请选择同步模式" style="width: 100%">
<el-option label="全量同步" value="FULL"></el-option>
<el-option label="增量同步" value="INCREMENTAL"></el-option>
<el-option label="实时同步" value="REAL_TIME"></el-option>
</el-select>
</el-form-item>
<el-form-item label="时间字段" prop="timeColumn">
<el-input v-model="formData.timeColumn" placeholder="请输入时间字段名create_time"></el-input>
<span class="form-tip">用于增量同步的时间查询字段</span>
</el-form-item>
<el-form-item label="BLOB字段" prop="blobColumns">
<el-input v-model="formData.blobColumns" placeholder="多个字段用逗号分隔content,data"></el-input>
<span class="form-tip">多个字段请用英文逗号分隔</span>
</el-form-item>
<el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="formData.sortOrder" :min="0" :max="9999" style="width: 100%"></el-input-number>
</el-form-item>
<el-form-item label="启用状态" prop="enabled">
<el-radio-group v-model="formData.enabled">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button size="small" @click="dialogVisible = false">取消</el-button>
<el-button type="primary" size="small" :loading="submitLoading" @click="handleSubmit">确定</el-button>
</span>
</el-dialog>

View File

@@ -0,0 +1,10 @@
<!-- 头部组件 -->
<div class="header">
<h1>
🗄️ 数据同步管理系统
<span :class="['environment-badge', isSourceServer ? 'badge-source' : 'badge-target']">
{{ serverTypeName }}
</span>
</h1>
<p> relational_migration - 数据导入导出管理平台 | 同步表配置管理</p>
</div>

View File

@@ -0,0 +1,15 @@
<!-- 日志组件 -->
<div class="card">
<div class="card-title">📋 操作日志</div>
<div class="log-area">
<div v-for="(log, index) in logs" :key="index" :class="['log-item', log.type]">
[{{ log.time }}] {{ log.message }}
</div>
<div v-if="logs.length === 0" class="log-info">
暂无操作日志
</div>
</div>
<el-button size="small" @click="clearLogs" style="margin-top: 10px;">
清空日志
</el-button>
</div>

View File

@@ -0,0 +1,19 @@
<!-- 统计信息组件 -->
<div class="stats">
<div class="stat-item">
<div class="stat-value">{{ fileCount }}</div>
<div class="stat-label">远程文件数量</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ operationCount }}</div>
<div class="stat-label">操作次数</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ tableCount }}</div>
<div class="stat-label">同步表配置数</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ lastOperationTime || '--' }}</div>
<div class="stat-label">最后操作时间</div>
</div>
</div>

View File

@@ -0,0 +1,189 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
background-color: #f5f7fa;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
/* 头部样式 */
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 8px;
margin-bottom: 30px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.header h1 {
font-size: 28px;
margin-bottom: 10px;
}
.header p {
font-size: 14px;
opacity: 0.9;
}
.environment-badge {
display: inline-block;
padding: 5px 15px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
margin-left: 15px;
}
.badge-source {
background: #e6a23c;
color: white;
}
.badge-target {
background: #409eff;
color: white;
}
/* 卡片样式 */
.card {
background: white;
border-radius: 8px;
padding: 25px;
margin-bottom: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.card-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #e4e7ed;
color: #303133;
}
/* 统计信息 */
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat-item {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 8px;
text-align: center;
}
.stat-value {
font-size: 32px;
font-weight: bold;
margin-bottom: 5px;
}
.stat-label {
font-size: 14px;
opacity: 0.9;
}
/* 按钮组 */
.button-group {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
/* 日期输入 */
.date-input {
width: 200px;
margin-right: 15px;
}
/* 文件列表 */
.file-list {
max-height: 400px;
overflow-y: auto;
}
/* 日志区域 */
.log-area {
background: #f5f7fa;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 15px;
max-height: 300px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
}
.log-item {
margin-bottom: 5px;
padding: 5px;
border-radius: 3px;
}
.log-success {
background: #f0f9ff;
color: #67c23a;
}
.log-error {
background: #fef0f0;
color: #f56c6c;
}
.log-info {
background: #f4f4f5;
color: #909399;
}
/* 表单提示 */
.form-tip {
font-size: 12px;
color: #999;
display: block;
margin-top: 4px;
}
/* BLOB标签 */
.blob-tag {
margin: 2px;
}
/* 分页 */
.pagination-container {
margin-top: 20px;
text-align: right;
}
/* 搜索卡片 */
.search-card {
background: #fafafa;
padding: 15px;
border-radius: 4px;
margin-bottom: 15px;
}
.batch-delete-btn {
margin-left: 10px;
}
/* Tab内容 */
.tab-content {
padding-top: 20px;
}

View File

@@ -19,7 +19,6 @@
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}

View File

@@ -0,0 +1,73 @@
// API接口定义
var API = {
// ========== 系统配置 ==========
getConfig: function() {
return axios.get('source/getConfig');
},
// ========== 数据同步相关 ==========
exportData: function(date) {
var params = date ? { date: date } : {};
return axios.get('source/export', { params: params });
},
importData: function(date) {
var params = date ? { date: date } : {};
return axios.get('source/import', { params: params });
},
testSync: function() {
return axios.get('source/test');
},
testSyncR: function() {
return axios.get('source/testR');
},
getRemoteFiles: function() {
return axios.get('source/getRemoteAll');
},
cleanTwo: function(days) {
return axios.get('source/removeTwo', { params: { a: days } });
},
cleanThree: function(days) {
return axios.get('source/removeThree', { params: { a: days } });
},
// ========== 同步表配置相关 ==========
getTablePage: function(params) {
return axios.get('/api/sync-table-config/page', { params: params });
},
getTableList: function() {
return axios.get('/api/sync-table-config/list');
},
getTableById: function(tableName) {
return axios.get('/api/sync-table-config/' + tableName);
},
addTable: function(data) {
return axios.post('/api/sync-table-config', data);
},
updateTable: function(data) {
return axios.put('/api/sync-table-config', data);
},
deleteTable: function(tableName) {
return axios.delete('/api/sync-table-config/' + tableName);
},
deleteTables: function(tableNames) {
return axios.delete('/api/sync-table-config/batch', { data: tableNames });
},
updateTableStatus: function(tableName, enabled) {
return axios.put('/api/sync-table-config/status/' + tableName, null, {
params: { enabled: enabled }
});
}
};

View File

@@ -0,0 +1,22 @@
// 全局配置
var APP_CONFIG = {
// API基础路径
baseURL: '',
// 环境配置
serverType: 'source',
serverTypeName: '源服务器',
// 默认保留天数
defaultCleanDays: 7,
// 日志最大条数
maxLogs: 100,
// 分页配置
pageSizeOptions: [10, 20, 50, 100],
defaultPageSize: 10
};
// 同步模式映射
var SYNC_MODE = {
FULL: { label: '全量同步', type: 'info' },
INCREMENTAL: { label: '增量同步', type: 'warning' },
REAL_TIME: { label: '实时同步', type: 'danger' }
};

View File

@@ -0,0 +1,25 @@
// router.js
const routes = [
{ path: '/', component: MainPage },
{ path: '/config', component: ConfigPage }
];
const router = new VueRouter({
routes
});
// app.js
new Vue({
router,
data: {
// 全局共享数据
operationCount: 0,
lastOperationTime: '',
logs: []
},
methods: {
addLog(message, type) {
// 全局日志方法
}
}
}).$mount('#app');

View File

@@ -0,0 +1,65 @@
// 工具函数
var Utils = {
// 获取昨天的日期
getYesterdayDate: function() {
var date = new Date();
date.setDate(date.getDate() - 1);
var y = date.getFullYear();
var m = (date.getMonth() + 1).toString().padStart(2, '0');
var d = date.getDate().toString().padStart(2, '0');
return y + m + d;
},
// 格式化时间
formatTime: function(date) {
if (!date) return '--';
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
},
// 解析BLOB字段
parseBlobColumns: function(columns) {
if (!columns) return [];
return columns.split(',').map(function(s) {
return s.trim();
}).filter(function(s) {
return s !== '';
});
},
// 获取同步模式标签
getSyncModeLabel: function(mode) {
var map = {
'FULL': '全量同步',
'INCREMENTAL': '增量同步',
'REAL_TIME': '实时同步'
};
return map[mode] || mode;
},
// 获取同步模式类型
getSyncModeType: function(mode) {
var map = {
'FULL': 'info',
'INCREMENTAL': 'warning',
'REAL_TIME': 'danger'
};
return map[mode] || '';
},
// 深拷贝对象
deepClone: function(obj) {
return JSON.parse(JSON.stringify(obj));
},
// 生成唯一ID
generateId: function() {
return Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
};

View File

@@ -0,0 +1,621 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>传输配置管理 - 同步表配置</title>
<!-- Element UI 样式 -->
<link rel="stylesheet" href="js/element-ui.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Microsoft YaHei", sans-serif;
background-color: #f0f2f5;
padding: 20px;
}
.container {
margin: 0 auto;
}
/* 头部 */
.header {
background: linear-gradient(135deg, #409eff 0%, #36a3f7 100%);
color: white;
padding: 20px 30px;
border-radius: 8px;
margin-bottom: 24px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
.header h1 {
font-size: 24px;
font-weight: 500;
}
.header .sub {
font-size: 14px;
opacity: 0.85;
margin-top: 4px;
}
.header-actions .el-button {
border-color: rgba(255,255,255,0.4);
color: white;
background: rgba(255,255,255,0.1);
}
.header-actions .el-button:hover {
background: rgba(255,255,255,0.25);
border-color: rgba(255,255,255,0.6);
}
/* 卡片 */
.card {
background: #fff;
border-radius: 8px;
padding: 20px 24px;
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
margin-bottom: 20px;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 16px;
padding-bottom: 10px;
border-bottom: 1px solid #e4e7ed;
}
.search-bar {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
margin-bottom: 16px;
}
.search-bar .el-form-item {
margin-bottom: 0;
}
.table-wrap {
margin-top: 8px;
}
.blob-tag {
background: #ecf5ff;
color: #409eff;
border-radius: 4px;
padding: 0 8px;
font-size: 12px;
line-height: 22px;
display: inline-block;
margin: 2px 4px 2px 0;
}
.status-tag {
font-weight: 500;
}
.dialog-footer {
text-align: right;
padding-top: 12px;
}
.empty-tip {
color: #909399;
font-size: 13px;
text-align: center;
padding: 30px 0;
}
.back-link {
cursor: pointer;
color: #fff;
font-size: 14px;
display: inline-flex;
align-items: center;
gap: 4px;
}
.back-link:hover {
text-decoration: underline;
}
.el-table th {
background-color: #f5f7fa !important;
}
.el-dialog .el-form-item {
margin-bottom: 18px;
}
/* 分页样式优化 */
.pagination-wrap {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<!-- 头部 -->
<div class="header">
<div>
<h1>⚙️ 传输配置管理 <span style="font-size: 14px; margin-left: 12px; font-weight: 400;">SYNC_TABLE_CONFIG</span></h1>
<div class="sub">管理同步表配置 · 支持BLOB字段、时间列、启用状态等</div>
</div>
<div class="header-actions">
<el-button type="text" icon="el-icon-back" @click="goBack" style="color: white; font-size: 14px;">
返回主页面
</el-button>
<el-button type="text" icon="el-icon-refresh" @click="fetchPage" style="color: white; font-size: 14px;">
刷新
</el-button>
</div>
</div>
<!-- 搜索 & 操作栏 -->
<div class="card">
<div class="card-title">🔍 查询 & 操作</div>
<div class="search-bar">
<el-form :inline="true" size="small" @submit.native.prevent>
<el-form-item label="表名">
<el-input v-model="search.tableName" placeholder="模糊搜索" clearable style="width:180px;"></el-input>
</el-form-item>
<el-form-item label="同步模式">
<el-select v-model="search.syncMode" placeholder="全部" clearable style="width:140px;">
<el-option label="全量" value="FULL"></el-option>
<el-option label="增量" value="INCREMENTAL"></el-option>
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="search.enabled" placeholder="全部" clearable style="width:120px;">
<el-option label="启用" :value="1"></el-option>
<el-option label="停用" :value="0"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="fetchPage">查询</el-button>
<el-button icon="el-icon-refresh" @click="resetSearch">重置</el-button>
</el-form-item>
<el-form-item style="margin-left: auto;">
<el-button type="success" icon="el-icon-plus" @click="openAddDialog">新增配置</el-button>
<el-button type="danger" icon="el-icon-delete" plain :disabled="selectedRows.length===0" @click="batchDelete">批量删除</el-button>
</el-form-item>
</el-form>
</div>
</div>
<!-- 表格 -->
<div class="card">
<div class="card-title">📋 配置列表 <span style="font-size:13px; font-weight:400; color:#909399; margin-left:12px;">共 {{ total }} 条</span></div>
<div class="table-wrap">
<el-table
:data="tableData"
stripe
border
v-loading="loading"
style="width:100%;"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" align="center"></el-table-column>
<el-table-column prop="tableName" label="表名" width="200" fixed></el-table-column>
<el-table-column prop="syncMode" label="同步模式" width="90" align="center">
<template slot-scope="{ row }">
<el-tag :type="row.syncMode === 'FULL' ? 'warning' : 'success'" size="small">
{{ row.syncMode === 'FULL' ? '全量' : '增量' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="timeColumn" label="时间查询列" min-width="130" show-overflow-tooltip></el-table-column>
<el-table-column prop="blobColumns" label="BLOB列" min-width="120">
<template slot-scope="{ row }">
<span v-if="row.blobColumns">
<span v-for="col in row.blobColumns.split(',')" :key="col" class="blob-tag">{{ col.trim() }}</span>
</span>
<span v-else style="color:#c0c4cc;">-</span>
</template>
</el-table-column>
<el-table-column prop="dataTimeType" label="增量数据时间类型" min-width="110" show-overflow-tooltip></el-table-column>
<el-table-column prop="enabled" label="状态" width="90" align="center">
<template slot-scope="{ row }">
<el-switch
v-model="row.enabled"
:active-value="1"
:inactive-value="0"
active-color="#13ce66"
inactive-color="#ff4949"
@change="handleStatusChange(row)"
></el-switch>
</template>
</el-table-column>
<el-table-column prop="sortOrder" label="排序" width="70" align="center"></el-table-column>
<el-table-column prop="remark" label="备注" min-width="120" show-overflow-tooltip></el-table-column>
<el-table-column prop="createTime" label="创建时间" width="140" sortable>
<template slot-scope="{ row }">{{ row.createTime ? formatDate(row.createTime) : '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template slot-scope="{ row }">
<el-button size="mini" type="primary" plain @click="openEditDialog(row)">编辑</el-button>
<el-button size="mini" type="danger" plain @click="deleteSingle(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrap">
<el-pagination
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageNum"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</div>
</div>
</div>
<!-- ========== 新增/编辑对话框 ========== -->
<el-dialog
:title="dialogTitle"
:visible.sync="dialogVisible"
width="620px"
:close-on-click-modal="false"
@closed="resetForm"
>
<el-form :model="form" :rules="rules" ref="configForm" label-width="120px" size="small">
<el-form-item label="表名" prop="tableName">
<el-input v-model="form.tableName" placeholder="请输入表名 (主键)" :disabled="isEdit"></el-input>
</el-form-item>
<el-form-item label="同步模式" prop="syncMode">
<el-select v-model="form.syncMode" placeholder="请选择" style="width:100%;">
<el-option label="全量" value="FULL"></el-option>
<el-option label="增量" value="INCREMENTAL"></el-option>
</el-select>
</el-form-item>
<el-form-item label="启用状态" prop="enabled">
<el-switch v-model="form.enabled" :active-value="1" :inactive-value="0" active-color="#13ce66" inactive-color="#ff4949"></el-switch>
<span style="margin-left:10px; color:#909399; font-size:13px;">{{ form.enabled === 1 ? '启用' : '停用' }}</span>
</el-form-item>
<el-form-item label="排序值" prop="sortOrder">
<el-input-number v-model="form.sortOrder" :min="0" :max="9999" style="width:100%;"></el-input-number>
</el-form-item>
<el-form-item label="时间查询列" prop="timeColumn">
<el-input v-model="form.timeColumn" placeholder="例如: create_time, update_time"></el-input>
<span style="color:#909399; font-size:12px;">用于增量同步的时间字段</span>
</el-form-item>
<el-form-item label="BLOB列" prop="blobColumns">
<el-input v-model="form.blobColumns" placeholder="多个逗号分隔,如: content, data"></el-input>
<span style="color:#909399; font-size:12px;">需要特殊处理的BLOB字段</span>
</el-form-item>
<el-form-item label="增量数据时间类型" prop="dataTimeType">
<el-select v-model="form.dataTimeType" placeholder="请选择" style="width:100%;">
<el-option label="日" value="D"></el-option>
<el-option label="月" value="M"></el-option>
<el-option label="季" value="Q"></el-option>
<el-option label="年" value="Y"></el-option>
</el-select>
<span style="color:#909399; font-size:12px;">该字段用于配置日表、月表、年表、季表</span>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" rows="2" placeholder="可选备注"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm" :loading="submitLoading">确定</el-button>
</span>
</el-dialog>
</div>
<!-- 引入 Vue & Element UI & Axios -->
<script src="js/vue.min.js"></script>
<script src="js/element-ui-index.js"></script>
<script src="js/axios.min.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
// 分页参数
pageNum: 1,
pageSize: 10,
total: 0,
// 搜索条件
search: {
tableName: '',
syncMode: '',
enabled: null
},
// 表格数据
tableData: [],
loading: false,
selectedRows: [],
// 对话框
dialogVisible: false,
dialogTitle: '新增配置',
isEdit: false,
submitLoading: false,
form: {
tableName: '',
syncMode: 'FULL',
enabled: 1,
sortOrder: 0,
timeColumn: '',
blobColumns: '',
dataTimeType: '',
remark: ''
},
// 表单校验
rules: {
tableName: [
{ required: true, message: '请输入表名', trigger: 'blur' },
{ pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, message: '表名格式不正确', trigger: 'blur' }
],
syncMode: [
{ required: true, message: '请选择同步模式', trigger: 'change' }
],
sortOrder: [
{ required: true, message: '请输入排序值', trigger: 'blur' }
]
}
};
},
mounted() {
this.fetchPage();
},
methods: {
// ---------- 数据请求 ----------
fetchPage() {
this.loading = true;
const params = {
pageNum: this.pageNum,
pageSize: this.pageSize,
tableName: this.search.tableName || undefined,
syncMode: this.search.syncMode || undefined,
enabled: this.search.enabled !== null && this.search.enabled !== '' ? this.search.enabled : undefined
};
axios.get('sync-table-config/page', { params })
.then(res => {
// 兼容不同的返回结构
let responseData = res.data;
// 如果data是HttpResult包装的
if (responseData.code !== undefined) {
if (responseData.code === 200 || responseData.code === 0) {
responseData = responseData.data;
} else {
this.$message.error(responseData.msg || '获取列表失败');
this.tableData = [];
this.total = 0;
return;
}
}
// 处理分页数据 - 兼容多种结构
if (responseData) {
// 结构1: { records: [], total: 0 }
if (responseData.records !== undefined) {
this.tableData = responseData.records || [];
this.total = responseData.total || 0;
}
// 结构2: { list: [], total: 0 }
else if (responseData.list !== undefined) {
this.tableData = responseData.list || [];
this.total = responseData.total || 0;
}
// 结构3: 直接是数组total从其他地方获取
else if (Array.isArray(responseData)) {
this.tableData = responseData;
this.total = responseData.length;
}
// 结构4: 嵌套在 data 中
else if (responseData.data && responseData.data.records) {
this.tableData = responseData.data.records || [];
this.total = responseData.data.total || 0;
}
else {
this.tableData = [];
this.total = 0;
}
} else {
this.tableData = [];
this.total = 0;
}
})
.catch(err => {
this.$message.error('请求失败: ' + (err.response?.data?.msg || err.message));
this.tableData = [];
this.total = 0;
})
.finally(() => {
this.loading = false;
});
},
resetSearch() {
this.search.tableName = '';
this.search.syncMode = '';
this.search.enabled = null;
this.pageNum = 1;
this.fetchPage();
},
// ---------- 分页 ----------
handleSizeChange(val) {
this.pageSize = val;
this.pageNum = 1;
this.fetchPage();
},
handleCurrentChange(val) {
this.pageNum = val;
this.fetchPage();
},
// ---------- 选中 ----------
handleSelectionChange(rows) {
this.selectedRows = rows;
},
// ---------- 状态切换 ----------
handleStatusChange(row) {
axios.put(`sync-table-config/status/${row.tableName}?enabled=${row.enabled}`)
.then(res => {
let data = res.data;
if (data.code === 200 || data.code === 0 || data.success) {
this.$message.success(`状态已${row.enabled === 1 ? '启用' : '停用'}`);
} else {
// 回退
row.enabled = row.enabled === 1 ? 0 : 1;
this.$message.error(data.msg || '状态更新失败');
}
})
.catch(() => {
row.enabled = row.enabled === 1 ? 0 : 1;
this.$message.error('状态更新失败');
});
},
// ---------- 新增 ----------
openAddDialog() {
this.dialogTitle = '新增配置';
this.isEdit = false;
this.form = {
tableName: '',
syncMode: 'FULL',
enabled: 1,
sortOrder: 0,
timeColumn: '',
blobColumns: '',
remark: ''
};
this.dialogVisible = true;
this.$nextTick(() => {
if (this.$refs.configForm) this.$refs.configForm.clearValidate();
});
},
// ---------- 编辑 ----------
openEditDialog(row) {
this.dialogTitle = '编辑配置';
this.isEdit = true;
// 深拷贝
this.form = {
tableName: row.tableName,
syncMode: row.syncMode || 'FULL',
enabled: row.enabled,
sortOrder: row.sortOrder || 0,
timeColumn: row.timeColumn || '',
blobColumns: row.blobColumns || '',
remark: row.remark || ''
};
this.dialogVisible = true;
this.$nextTick(() => {
if (this.$refs.configForm) this.$refs.configForm.clearValidate();
});
},
// ---------- 提交表单 ----------
submitForm() {
this.$refs.configForm.validate((valid) => {
if (!valid) return;
this.submitLoading = true;
const url = 'sync-table-config';
const method = this.isEdit ? 'put' : 'post';
axios[method](url, this.form)
.then(res => {
let data = res.data;
if (data.code === 200 || data.code === 0 || data.success) {
this.$message.success(this.isEdit ? '更新成功' : '新增成功');
this.dialogVisible = false;
this.fetchPage();
} else {
this.$message.error(data.msg || '操作失败');
}
})
.catch(err => {
this.$message.error('请求失败: ' + (err.response?.data?.msg || err.message));
})
.finally(() => {
this.submitLoading = false;
});
});
},
resetForm() {
// 关闭时重置
this.form = {
tableName: '',
syncMode: 'FULL',
enabled: 1,
sortOrder: 0,
timeColumn: '',
blobColumns: '',
remark: ''
};
this.isEdit = false;
},
// ---------- 删除单个 ----------
deleteSingle(row) {
this.$confirm(`确定删除表 "${row.tableName}" 的配置吗?`, '提示', {
type: 'warning'
}).then(() => {
axios.delete(`sync-table-config/${row.tableName}`)
.then(res => {
let data = res.data;
if (data.code === 200 || data.code === 0 || data.success) {
this.$message.success('删除成功');
this.fetchPage();
} else {
this.$message.error(data.msg || '删除失败');
}
})
.catch(err => {
this.$message.error('删除失败: ' + (err.response?.data?.msg || err.message));
});
}).catch(() => {});
},
// ---------- 批量删除 ----------
batchDelete() {
if (this.selectedRows.length === 0) {
this.$message.warning('请先选择要删除的配置');
return;
}
const names = this.selectedRows.map(r => r.tableName);
this.$confirm(`确定删除选中的 ${names.length} 个配置吗?`, '批量删除', {
type: 'warning'
}).then(() => {
axios.delete('sync-table-config/batch', { data: names })
.then(res => {
let data = res.data;
if (data.code === 200 || data.code === 0 || data.success) {
this.$message.success('批量删除成功');
this.fetchPage();
} else {
this.$message.error(data.msg || '批量删除失败');
}
})
.catch(err => {
this.$message.error('批量删除失败: ' + (err.response?.data?.msg || err.message));
});
}).catch(() => {});
},
// ---------- 返回主页面 ----------
goBack() {
window.location.href = 'index';
},
// ---------- 工具函数 ----------
formatDate(dateStr) {
if (!dateStr) return '-';
const d = new Date(dateStr);
if (isNaN(d)) return dateStr;
const pad = n => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,622 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据同步管理系统</title>
<!-- 引入 Element UI 样式 -->
<link rel="stylesheet" href="js/element-ui.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
background-color: #f5f7fa;
}
.container {
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 10px;
border-radius: 8px;
margin-bottom: 10px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
.header-left {
display: flex;
flex-direction: column;
}
.header-left h1 {
font-size: 28px;
margin-bottom: 10px;
}
.header-left p {
font-size: 14px;
opacity: 0.9;
}
.header-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center;
}
.header-actions .el-button {
border-color: rgba(255,255,255,0.4);
color: white;
background: rgba(255,255,255,0.1);
}
.header-actions .el-button:hover {
background: rgba(255,255,255,0.25);
border-color: rgba(255,255,255,0.6);
}
.card {
background: white;
border-radius: 8px;
padding: 25px;
margin-bottom: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.card-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #e4e7ed;
color: #303133;
}
.button-group {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.button-item {
flex: 1;
min-width: 200px;
}
.date-input {
width: 200px;
margin-right: 15px;
}
.file-list {
max-height: 400px;
overflow-y: auto;
}
.log-area {
background: #f5f7fa;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 15px;
max-height: 300px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
}
.log-item {
margin-bottom: 5px;
padding: 5px;
border-radius: 3px;
}
.log-success {
background: #f0f9ff;
color: #67c23a;
}
.log-error {
background: #fef0f0;
color: #f56c6c;
}
.log-info {
background: #f4f4f5;
color: #909399;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat-item {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 8px;
text-align: center;
}
.stat-value {
font-size: 32px;
font-weight: bold;
margin-bottom: 5px;
}
.stat-label {
font-size: 14px;
opacity: 0.9;
}
.environment-badge {
display: inline-block;
padding: 5px 15px;
border-radius: 20px;
font-size: 12px;
font-weight: bold;
margin-left: 15px;
}
.badge-source {
background: #e6a23c;
color: white;
}
.badge-target {
background: #409eff;
color: white;
}
/* 管理配置页面(独立跳转,此处仅为样式预留) */
.config-page-link {
cursor: pointer;
font-weight: 500;
}
.config-page-link:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<!-- 头部:增加右侧按钮组 -->
<div class="header">
<div class="header-left">
<h1>
🗄️ 数据同步管理系统
<span style="font-size: large" :class="['environment-badge', isSourceServer ? 'badge-source' : 'badge-target']">
{{ serverTypeName }}
</span>
</h1>
<p> relational_migration - 数据导入导出管理平台</p>
</div>
<div class="header-actions">
<!-- 新增:传输配置按钮,点击跳转至管理配置页面 -->
<el-button type="text" icon="el-icon-setting" @click="goToLogsPage" style="color: white; font-size: 16px;">
传输日志
</el-button>
<el-button type="text" icon="el-icon-setting" @click="goToConfigPage" style="color: white; font-size: 16px;">
传输配置
</el-button>
<el-button type="text" icon="el-icon-refresh" @click="refreshAll" style="color: white; font-size: 14px;">
刷新
</el-button>
</div>
</div>
<!-- 统计信息 -->
<div class="stats">
<div class="stat-item">
<div class="stat-value">{{ fileCount }}</div>
<div class="stat-label">远程文件数量</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ operationCount }}</div>
<div class="stat-label">操作次数</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ lastOperationTime || '--' }}</div>
<div class="stat-label">最后操作时间</div>
</div>
</div>
<!-- 环境配置卡片(可选显示) -->
<div class="card" v-if="showEnvSwitch">
<div class="card-title">⚙️ 环境切换(调试用)</div>
<el-alert
title="当前环境由配置文件控制,如需切换请修改 application.yml 中的 app.server-type 配置"
type="warning"
:closable="false"
show-icon
style="margin-bottom: 15px;">
</el-alert>
<el-form :inline="true">
<el-form-item label="当前配置">
<el-tag :type="isSourceServer ? 'warning' : 'primary'" size="medium">
{{ serverTypeName }}
</el-tag>
</el-form-item>
</el-form>
</div>
<!-- 数据操作卡片 -->
<div class="card">
<div class="card-title">📊 数据操作</div>
<el-form :inline="true" class="demo-form-inline">
<el-form-item label="日期">
<el-date-picker
v-model="selectedDate"
type="date"
placeholder="选择日期"
format="yyyyMMdd"
value-format="yyyyMMdd"
class="date-input">
</el-date-picker>
</el-form-item>
<el-form-item>
<div class="button-group">
<el-button
v-if="isSourceServer"
type="primary"
icon="el-icon-download"
@click="handleExport"
:loading="loading.export">
从二区库导出数据并上传横向隔离设备
</el-button>
<el-button
v-if="isTargetServer"
type="success"
icon="el-icon-upload2"
@click="handleImport"
:loading="loading.import">
从横向隔离设备拉取数据并入三区库
</el-button>
<el-button v-if="isSourceServer" type="warning" icon="el-icon-refresh" @click="handleTest" :loading="loading.test">
测试二区发送数据至横向隔离设备
</el-button>
<el-button v-if="isTargetServer" type="info" icon="el-icon-refresh-right" @click="handleTestR" :loading="loading.testR">
测试三区从横向隔离设备消费数据
</el-button>
</div>
</el-form-item>
</el-form>
</div>
<!-- 文件管理卡片 -->
<div class="card">
<div class="card-title">📁 远程文件管理</div>
<el-button type="primary" icon="el-icon-search" @click="getRemoteFiles" :loading="loading.getRemoteAll" style="margin-bottom: 15px;">
获取横向隔离设备远程文件列表
</el-button>
<div class="file-list" v-if="remoteFiles.length > 0">
<el-table :data="remoteFiles" stripe style="width: 100%">
<el-table-column type="index" label="序号" width="60"></el-table-column>
<el-table-column prop="name" label="文件名"></el-table-column>
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<!-- <el-button size="mini" type="danger" @click="removeFile(scope.row)">
删除
</el-button>-->
</template>
</el-table-column>
</el-table>
</div>
<el-empty v-else description="暂无远程文件" :image-size="100"></el-empty>
</div>
<!-- 清理文件卡片 -->
<div class="card">
<div class="card-title">🗑️ 本地文件清理</div>
<el-form :inline="true">
<el-form-item label="保留天数">
<el-input-number v-model="cleanDays" :min="1" :max="365" label="请输入保留天数"></el-input-number>
</el-form-item>
<el-form-item>
<el-button v-if="isSourceServer" type="danger" icon="el-icon-delete" @click="cleanTwo" :loading="loading.removeTwo">
清理二区服务器上生成的数据
</el-button>
<el-button v-if="isTargetServer" type="danger" icon="el-icon-delete" @click="cleanThree" :loading="loading.removeThree">
清理三区服务器保留的数据
</el-button>
</el-form-item>
</el-form>
</div>
<!-- 操作日志卡片 -->
<div class="card">
<div class="card-title">📋 操作日志</div>
<div class="log-area">
<div v-for="(log, index) in logs" :key="index" :class="['log-item', log.type]">
[{{ log.time }}] {{ log.message }}
</div>
<div v-if="logs.length === 0" class="log-info">
暂无操作日志
</div>
</div>
<el-button size="small" @click="clearLogs" style="margin-top: 10px;">
清空日志
</el-button>
</div>
</div>
</div>
<!-- 引入 Vue -->
<script src="js/vue.min.js"></script>
<!-- 引入 Element UI -->
<script src="js/element-ui-index.js"></script>
<!-- 引入 Axios -->
<script src="js/axios.min.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
selectedDate: '',
cleanDays: 7,
remoteFiles: [],
logs: [],
loading: {
export: false,
import: false,
test: false,
testR: false,
getRemoteAll: false,
removeTwo: false,
removeThree: false
},
fileCount: 0,
operationCount: 0,
lastOperationTime: '',
serverType: 'source',
serverTypeName: '源服务器',
showEnvSwitch: false,
// 默认选中 昨天
selectedDate: this.getYesterdayDate()
}
},
computed: {
isSourceServer() {
return this.serverType === 'source';
},
isTargetServer() {
return this.serverType === 'target';
}
},
mounted() {
this.loadServerConfig();
},
methods: {
// 获取昨天的日期,格式 yyyyMMdd
getYesterdayDate() {
const date = new Date();
date.setDate(date.getDate() - 1); // 减去一天 = 昨天
const y = date.getFullYear();
const m = (date.getMonth() + 1).toString().padStart(2, '0');
const d = date.getDate().toString().padStart(2, '0');
return `${y}${m}${d}`; // 输出 20250613 这种格式
},
// 加载服务器配置
async loadServerConfig() {
try {
const response = await axios.get('source/getConfig');
this.serverType = response.data.serverType;
this.serverTypeName = response.data.serverTypeName;
this.addLog(`系统初始化完成,当前环境:${this.serverTypeName}`, 'success');
this.getRemoteFiles();
} catch (error) {
console.error('加载配置失败:', error);
this.addLog('加载配置失败,使用默认配置', 'error');
this.getRemoteFiles();
}
},
// 添加日志
addLog(message, type = 'info') {
const now = new Date();
const timeStr = now.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
this.logs.unshift({
time: timeStr,
message: message,
type: type
});
// 保持最多100条日志
if (this.logs.length > 100) {
this.logs.pop();
}
},
// 清空日志
clearLogs() {
this.logs = [];
this.$message.info('日志已清空');
},
// 导出数据
async handleExport() {
this.loading.export = true;
try {
const params = this.selectedDate ? { date: this.selectedDate } : {};
await axios.get('source/export', { params });
this.$message.success('二区数据导出上传成功');
this.addLog('二区数据导出上传成功', 'success');
this.operationCount++;
this.lastOperationTime = new Date().toLocaleTimeString();
} catch (error) {
this.$message.error('数据导出失败: ' + (error.response?.data || error.message));
this.addLog('数据导出失败: ' + error.message, 'error');
} finally {
this.loading.export = false;
}
},
// 导入数据
async handleImport() {
this.loading.import = true;
try {
const params = this.selectedDate ? { date: this.selectedDate } : {};
await axios.get('source/import', { params });
this.$message.success('三区数据拉取导入成功');
this.addLog('三区数据拉取导入成功', 'success');
this.operationCount++;
this.lastOperationTime = new Date().toLocaleTimeString();
} catch (error) {
this.$message.error('数据导入失败: ' + (error.response?.data || error.message));
this.addLog('数据导入失败: ' + error.message, 'error');
} finally {
this.loading.import = false;
}
},
// 测试连接
async handleTest() {
this.loading.test = true;
try {
await axios.get('source/test');
this.$message.success('测试二区数据推送横向隔离设备成功');
this.addLog('测试二区数据推送横向隔离设备成功', 'success');
this.operationCount++;
this.lastOperationTime = new Date().toLocaleTimeString();
} catch (error) {
this.$message.error('测试二区数据推送横向隔离设备失败: ' + (error.response?.data || error.message));
this.addLog('测试二区数据推送横向隔离设备失败: ' + error.message, 'error');
} finally {
this.loading.test = false;
}
},
// 测试远程
async handleTestR() {
this.loading.testR = true;
try {
await axios.get('source/testR');
this.$message.success('三区拉取测试数据成功');
this.addLog('三区拉取测试数据成功', 'success');
this.operationCount++;
this.lastOperationTime = new Date().toLocaleTimeString();
} catch (error) {
this.$message.error('三区拉取测试数据失败: ' + (error.response?.data || error.message));
this.addLog('三区拉取测试数据失败: ' + error.message, 'error');
} finally {
this.loading.testR = false;
}
},
// 获取远程文件列表
async getRemoteFiles() {
this.loading.getRemoteAll = true;
try {
const response = await axios.get('source/getRemoteAll');
this.remoteFiles = response.data.map((name, index) => ({
id: index,
name: name
}));
this.fileCount = this.remoteFiles.length;
this.$message.success(`获取到 ${this.fileCount} 个远程文件`);
this.addLog(`获取远程文件列表成功,共 ${this.fileCount} 个文件`, 'success');
} catch (error) {
this.$message.error('获取远程文件失败: ' + (error.response?.data || error.message));
this.addLog('获取远程文件失败: ' + error.message, 'error');
} finally {
this.loading.getRemoteAll = false;
}
},
// 清理二区服务器历史导出数据
async cleanTwo() {
this.loading.removeTwo = true;
try {
const response = await axios.get('source/removeTwo', {
params: { a: this.cleanDays }
});
this.$message.success(response.data);
this.addLog(`清理二区服务器历史导出数据成功,保留${this.cleanDays}天内的文件`, 'success');
this.operationCount++;
this.lastOperationTime = new Date().toLocaleTimeString();
// 刷新文件列表
//this.getRemoteFiles();
} catch (error) {
this.$message.error('清理失败: ' + (error.response?.data || error.message));
this.addLog('清理二区服务器历史导出数据失败: ' + error.message, 'error');
} finally {
this.loading.removeTwo = false;
}
},
// 清理三区拉取数据
async cleanThree() {
this.loading.removeThree = true;
try {
const response = await axios.get('source/removeThree', {
params: { a: this.cleanDays }
});
this.$message.success(response.data);
this.addLog(`清理三区拉取数据成功,保留${this.cleanDays}天内的文件`, 'success');
this.operationCount++;
this.lastOperationTime = new Date().toLocaleTimeString();
// 刷新文件列表
//this.getRemoteFiles();
} catch (error) {
this.$message.error('清理失败: ' + (error.response?.data || error.message));
this.addLog('清理三区拉取数据失败: ' + error.message, 'error');
} finally {
this.loading.removeThree = false;
}
},
// ========== 新增:跳转至管理配置页面 ==========
goToConfigPage() {
// 方式1当前窗口跳转独立页面
window.location.href = 'config';
// 方式2新窗口打开可选
// window.open('config.html', '_blank');
},
goToLogsPage() {
// 方式1当前窗口跳转独立页面
window.location.href = 'logs';
// 方式2新窗口打开可选
// window.open('config.html', '_blank');
},
// 刷新当前页面数据
refreshAll() {
this.getRemoteFiles();
this.addLog('手动刷新', 'info');
this.$message.success('已刷新');
}
}
})
</script>
</body>
</html>

View File

@@ -0,0 +1,588 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数据同步日志管理</title>
<!-- Element UI 样式 -->
<link rel="stylesheet" href="js/element-ui.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Microsoft YaHei", sans-serif;
background-color: #f0f2f5;
padding: 20px;
}
.container {
margin: 0 auto;
}
/* 头部 */
.header {
background: linear-gradient(135deg, #67c23a 0%, #5daf34 100%);
color: white;
padding: 20px 30px;
border-radius: 8px;
margin-bottom: 24px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
}
.header h1 {
font-size: 24px;
font-weight: 500;
}
.header .sub {
font-size: 14px;
opacity: 0.85;
margin-top: 4px;
}
.header-actions .el-button {
border-color: rgba(255,255,255,0.4);
color: white;
background: rgba(255,255,255,0.1);
}
.header-actions .el-button:hover {
background: rgba(255,255,255,0.25);
border-color: rgba(255,255,255,0.6);
}
/* 卡片 */
.card {
background: #fff;
border-radius: 8px;
padding: 20px 24px;
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
margin-bottom: 20px;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 16px;
padding-bottom: 10px;
border-bottom: 1px solid #e4e7ed;
}
.search-bar {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
margin-bottom: 16px;
}
.search-bar .el-form-item {
margin-bottom: 0;
}
.table-wrap {
margin-top: 8px;
}
.status-tag {
font-weight: 500;
}
.dialog-footer {
text-align: right;
padding-top: 12px;
}
.el-table th {
background-color: #f5f7fa !important;
}
.el-dialog .el-form-item {
margin-bottom: 18px;
}
.pagination-wrap {
margin-top: 16px;
display: flex;
justify-content: flex-end;
}
.result-success {
color: #67c23a;
}
.result-fail {
color: #f56c6c;
}
.stat-summary {
display: flex;
gap: 30px;
flex-wrap: wrap;
padding: 10px 0;
}
.stat-summary .stat-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.stat-summary .stat-item .label {
color: #909399;
}
.stat-summary .stat-item .value {
font-weight: 600;
font-size: 18px;
}
.stat-summary .stat-item .value.success {
color: #67c23a;
}
.stat-summary .stat-item .value.fail {
color: #f56c6c;
}
.stat-summary .stat-item .value.total {
color: #409eff;
}
.back-link {
cursor: pointer;
color: #fff;
font-size: 14px;
display: inline-flex;
align-items: center;
gap: 4px;
}
.back-link:hover {
text-decoration: underline;
}
.local-result-tag {
font-size: 12px;
padding: 2px 10px;
border-radius: 10px;
}
.retry-btn {
margin-left: 4px;
}
</style>
</head>
<body>
<div id="app">
<div class="container">
<!-- 头部 -->
<div class="header">
<div>
<h1>📋 数据同步日志管理 <span style="font-size: 14px; margin-left: 12px; font-weight: 400;">SYNC_TABLE_LOGS</span></h1>
<div class="sub">二三区数据转移日志 · 查看同步记录与状态</div>
</div>
<div class="header-actions">
<el-button type="text" icon="el-icon-back" @click="goBack" style="color: white; font-size: 14px;">
返回主页面
</el-button>
<el-button type="text" icon="el-icon-refresh" @click="fetchPage" style="color: white; font-size: 14px;">
刷新
</el-button>
</div>
</div>
<!-- 搜索 & 统计 -->
<div class="card">
<div class="card-title">🔍 查询 & 统计</div>
<div class="search-bar">
<el-form :inline="true" size="small" @submit.native.prevent>
<el-form-item label="日期">
<el-date-picker
v-model="search.timeId"
type="date"
placeholder="选择日期"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
clearable
style="width:160px;">
</el-date-picker>
</el-form-item>
<el-form-item label="同步结果">
<el-select v-model="search.result" placeholder="全部" clearable style="width:120px;">
<el-option label="成功" :value="1"></el-option>
<el-option label="失败" :value="0"></el-option>
</el-select>
</el-form-item>
<el-form-item label="本地处理">
<el-select v-model="search.localResult" placeholder="全部" clearable style="width:120px;">
<el-option label="成功" :value="1"></el-option>
<el-option label="失败" :value="0"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="fetchPage">查询</el-button>
<el-button icon="el-icon-refresh" @click="resetSearch">重置</el-button>
</el-form-item>
<el-form-item style="margin-left: auto;">
<el-button type="danger" icon="el-icon-delete" plain :disabled="selectedRows.length===0" @click="batchDelete">批量删除</el-button>
</el-form-item>
</el-form>
</div>
<!-- 统计信息 -->
<div class="stat-summary" v-if="total > 0">
<div class="stat-item">
<span class="label">📊 总记录数:</span>
<span class="value total">{{ total }}</span>
</div>
<div class="stat-item">
<span class="label">✅ 同步成功:</span>
<span class="value success">{{ successCount }}</span>
</div>
<div class="stat-item">
<span class="label">❌ 同步失败:</span>
<span class="value fail">{{ failCount }}</span>
</div>
<div class="stat-item" v-if="search.timeId">
<span class="label">📅 日期:</span>
<span>{{ search.timeId }}</span>
</div>
</div>
</div>
<!-- 表格 -->
<div class="card">
<div class="card-title">📋 日志列表 <span style="font-size:13px; font-weight:400; color:#909399; margin-left:12px;">共 {{ total }} 条</span></div>
<div class="table-wrap">
<el-table
:data="tableData"
stripe
border
v-loading="loading"
style="width:100%;"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="90" align="center"></el-table-column>
<el-table-column prop="timeId" label="日期" width="130" sortable>
<template slot-scope="{ row }">
{{ row.timeId || '-' }}
</template>
</el-table-column>
<el-table-column prop="result" label="同步结果" width="110" align="center">
<template slot-scope="{ row }">
<el-tag :type="row.result === 1 ? 'success' : 'danger'" size="small">
{{ row.result === 1 ? '✅ 成功' : '❌ 失败' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="localResult" label="本地处理" width="130" align="center">
<template slot-scope="{ row }">
<el-tag :type="row.localResult === 1 ? 'success' : 'danger'" size="small" v-if="row.localResult !== undefined && row.localResult !== null">
{{ row.localResult === 1 ? '✅ 成功' : '❌ 失败' }}
</el-tag>
<span v-else style="color:#c0c4cc;">-</span>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="170" sortable>
<template slot-scope="{ row }">{{ row.createTime ? formatDate(row.createTime) : '-' }}</template>
</el-table-column>
<el-table-column label="操作" fixed="right">
<template slot-scope="{ row }">
<el-button size="mini" type="primary" plain @click="openDetailDialog(row)">详情</el-button>
<!-- 重试按钮:只有 result=0 且 localResult=0 时才显示 -->
<el-button
v-if="row.result === 0 && row.localResult === 0"
size="mini"
type="warning"
plain
class="retry-btn"
@click="handleRetry(row)"
:loading="retryLoading[row.timeId + '_' + row.result]"
>
重试
</el-button>
<el-button size="mini" type="danger" plain @click="deleteSingle(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrap">
<el-pagination
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageNum"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</div>
</div>
</div>
<!-- ========== 详情对话框 ========== -->
<el-dialog
title="📄 日志详情"
:visible.sync="detailVisible"
width="500px"
:close-on-click-modal="false"
>
<el-descriptions :column="1" border>
<el-descriptions-item label="日期">
{{ detailData.timeId || '-' }}
</el-descriptions-item>
<el-descriptions-item label="同步结果">
<el-tag :type="detailData.result === 1 ? 'success' : 'danger'" size="small">
{{ detailData.result === 1 ? '✅ 成功' : '❌ 失败' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="本地处理结果">
<el-tag :type="detailData.localResult === 1 ? 'success' : 'danger'" size="small" v-if="detailData.localResult !== undefined && detailData.localResult !== null">
{{ detailData.localResult === 1 ? '✅ 成功' : '❌ 失败' }}
</el-tag>
<span v-else style="color:#c0c4cc;">未处理</span>
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ detailData.createTime ? formatDate(detailData.createTime) : '-' }}
</el-descriptions-item>
</el-descriptions>
<span slot="footer" class="dialog-footer">
<el-button @click="detailVisible = false">关闭</el-button>
</span>
</el-dialog>
</div>
<!-- 引入 Vue & Element UI & Axios -->
<script src="js/vue.min.js"></script>
<script src="js/element-ui-index.js"></script>
<script src="js/axios.min.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
// 分页参数
pageNum: 1,
pageSize: 10,
total: 0,
// 搜索条件
search: {
timeId: '',
result: null,
localResult: null
},
// 表格数据
tableData: [],
loading: false,
selectedRows: [],
// 统计
successCount: 0,
failCount: 0,
// 详情对话框
detailVisible: false,
detailData: {},
serverType: '',
// 重试加载状态
retryLoading: {}
};
},
mounted() {
this.loadServerConfig();
this.fetchPage();
},
methods: {
// ---------- 数据请求 ----------
fetchPage() {
this.loading = true;
const params = {
pageNum: this.pageNum,
pageSize: this.pageSize,
timeId: this.search.timeId || undefined,
result: this.search.result !== null && this.search.result !== '' ? this.search.result : undefined,
localResult: this.search.localResult !== null && this.search.localResult !== '' ? this.search.localResult : undefined
};
axios.get('sync-logs/page', { params })
.then(res => {
let responseData = res.data;
if (responseData.code !== undefined) {
if (responseData.code === 200 || responseData.code === 0) {
responseData = responseData.data;
} else {
this.$message.error(responseData.msg || '获取列表失败');
this.tableData = [];
this.total = 0;
return;
}
}
if (responseData) {
if (responseData.records !== undefined) {
this.tableData = responseData.records || [];
this.total = responseData.total || 0;
} else if (responseData.list !== undefined) {
this.tableData = responseData.list || [];
this.total = responseData.total || 0;
} else if (Array.isArray(responseData)) {
this.tableData = responseData;
this.total = responseData.length;
} else if (responseData.data && responseData.data.records) {
this.tableData = responseData.data.records || [];
this.total = responseData.data.total || 0;
} else {
this.tableData = [];
this.total = 0;
}
} else {
this.tableData = [];
this.total = 0;
}
// 统计成功/失败数量
this.calcStatistics();
})
.catch(err => {
this.$message.error('请求失败: ' + (err.response?.data?.msg || err.message));
this.tableData = [];
this.total = 0;
})
.finally(() => {
this.loading = false;
});
},
calcStatistics() {
this.successCount = this.tableData.filter(item => item.result === 1).length;
this.failCount = this.tableData.filter(item => item.result === 0).length;
},
resetSearch() {
this.search.timeId = '';
this.search.result = null;
this.search.localResult = null;
this.pageNum = 1;
this.fetchPage();
},
// ---------- 分页 ----------
handleSizeChange(val) {
this.pageSize = val;
this.pageNum = 1;
this.fetchPage();
},
handleCurrentChange(val) {
this.pageNum = val;
this.fetchPage();
},
// ---------- 选中 ----------
handleSelectionChange(rows) {
this.selectedRows = rows;
},
// ---------- 详情 ----------
openDetailDialog(row) {
this.detailData = { ...row };
this.detailVisible = true;
},
// ---------- 重试 ----------
async handleRetry(row) {
const key = row.timeId + '_' + row.result;
// 设置加载状态
this.$set(this.retryLoading, key, true);
try {
// 根据 serverType 调用不同接口
// source: 调用 source/import (二区导入)
// target: 调用 source/export (三区导出)
let url = '';
if (this.serverType === 'source') {
url = 'source/export';
} else if (this.serverType === 'target') {
url = 'source/import';
} else {
this.$message.error('未知的服务器类型: ' + this.serverType);
return;
}
// 调用接口,传递日期参数
const params = {
date: row.timeId.replace(/-/g, "")
};
await axios.get(url, { params });
this.$message.success(`重试成功 (${this.serverType === 'source' ? '二区导入' : '三区导出'})`);
// 刷新列表
this.fetchPage();
} catch (error) {
this.$message.error('重试失败: ' + (error.response?.data || error.message));
} finally {
// 清除加载状态
this.$set(this.retryLoading, key, false);
}
},
// ---------- 删除单个 ----------
deleteSingle(row) {
this.$confirm(`确定删除日期 "${row.timeId}" 的日志吗?`, '提示', {
type: 'warning'
}).then(() => {
axios.delete(`sync-logs/${row.timeId}/${row.result}`)
.then(res => {
let data = res.data;
if (data.code === 200 || data.code === 0 || data.success) {
this.$message.success('删除成功');
this.fetchPage();
} else {
this.$message.error(data.msg || '删除失败');
}
})
.catch(err => {
this.$message.error('删除失败: ' + (err.response?.data?.msg || err.message));
});
}).catch(() => {});
},
// ---------- 批量删除 ----------
batchDelete() {
if (this.selectedRows.length === 0) {
this.$message.warning('请先选择要删除的日志');
return;
}
const logs = this.selectedRows.map(r => ({
timeId: r.timeId,
result: r.result
}));
this.$confirm(`确定删除选中的 ${logs.length} 条日志吗?`, '批量删除', {
type: 'warning'
}).then(() => {
axios.delete('sync-logs/batch', { data: logs })
.then(res => {
let data = res.data;
if (data.code === 200 || data.code === 0 || data.success) {
this.$message.success('批量删除成功');
this.fetchPage();
} else {
this.$message.error(data.msg || '批量删除失败');
}
})
.catch(err => {
this.$message.error('批量删除失败: ' + (err.response?.data?.msg || err.message));
});
}).catch(() => {});
},
// ---------- 返回主页面 ----------
goBack() {
window.location.href = 'index';
},
// ---------- 工具函数 ----------
formatDate(dateStr) {
if (!dateStr) return '-';
const d = new Date(dateStr);
if (isNaN(d)) return dateStr;
const pad = n => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
},
// ---------- 加载服务器配置 ----------
async loadServerConfig() {
try {
const response = await axios.get('source/getConfig');
this.serverType = response.data.serverType;
} catch (error) {
console.error('加载配置失败:', error);
// 默认设为 source
this.serverType = 'source';
}
}
}
});
</script>
</body>
</html>