package jnpf.onlinedev.service.impl;
|
|
import cn.hutool.core.bean.BeanUtil;
|
import jnpf.base.UserInfo;
|
import jnpf.base.entity.VisualdevReleaseEntity;
|
import jnpf.base.model.VisualLogModel;
|
import jnpf.base.service.SuperServiceImpl;
|
import jnpf.base.service.VisualdevReleaseService;
|
import jnpf.constant.JnpfConst;
|
import jnpf.consts.ProjectEventConst;
|
import jnpf.event.ProjectEventListener;
|
import jnpf.model.visualjson.FieLdsModel;
|
import jnpf.model.visualjson.FormDataModel;
|
import jnpf.model.visualjson.TableFields;
|
import jnpf.model.visualjson.TableModel;
|
import jnpf.model.visualjson.config.ConfigModel;
|
import jnpf.module.ProjectEventBuilder;
|
import jnpf.module.ProjectEventInstance;
|
import jnpf.onlinedev.entity.VisualLogEntity;
|
import jnpf.onlinedev.mapper.VisualLogMapper;
|
import jnpf.onlinedev.model.enums.OnlineDataTypeEnum;
|
import jnpf.onlinedev.model.log.VisualLogForm;
|
import jnpf.onlinedev.model.log.VisualLogPage;
|
import jnpf.onlinedev.service.VisualLogService;
|
import jnpf.onlinedev.util.OnlinePublicUtils;
|
import jnpf.onlinedev.util.OnlineSwapDataUtils;
|
import jnpf.util.IpUtil;
|
import jnpf.util.JsonUtil;
|
import jnpf.util.PublishEventUtil;
|
import jnpf.util.TableFeildsEnum;
|
import jnpf.util.UserProvider;
|
import jnpf.util.visiual.JnpfKeyConsts;
|
import lombok.RequiredArgsConstructor;
|
import lombok.extern.slf4j.Slf4j;
|
import org.apache.commons.collections4.CollectionUtils;
|
import org.apache.commons.collections4.MapUtils;
|
import org.springframework.stereotype.Service;
|
import org.springframework.web.context.request.RequestContextHolder;
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
|
import java.util.*;
|
import java.util.stream.Collectors;
|
|
/**
|
* 数据日志实现
|
*
|
* @author JNPF开发平台组
|
* @version v5.1.0
|
* @copyright 引迈信息技术有限公司
|
* @date 2024/8/27 18:24:42
|
*/
|
@Slf4j
|
@Service
|
@RequiredArgsConstructor
|
public class VisualLogServiceImpl extends SuperServiceImpl<VisualLogMapper, VisualLogEntity> implements VisualLogService {
|
|
private final VisualdevReleaseService visualdevReleaseService;
|
private final OnlineSwapDataUtils onlineSwapDataUtils;
|
|
/**
|
* 创建日志事件
|
*
|
* @param form 数据id
|
*
|
* <p>[LIMS PATCH 2026-05-15] 从 publishLocalEvent 改为 BROADCASTING,
|
* 使 jnpf-extend 等其它微服务能通过 RocketMQ 跨 JVM 监听 VSLOG_EVENT_KEY,
|
* 便于 LIMS 业务日志统一聚合到 lims_biz_log。
|
* 详见 tasks/jnpf-platform-patches.md 和 docs/release-checklist.md。</p>
|
*/
|
@Override
|
public void createEventLog(VisualLogForm form) {
|
ensureBoundaryListLog(form);
|
// AUDIT-P1(登记 tasks/jnpf-platform-patches.md):附带请求上下文供审计层0消费
|
try {
|
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
UserInfo user = UserProvider.getUser(); // 静态方法(终验 javap 核实:无实例 get()、无 userProvider 注入字段)
|
if (attrs != null && user != null) {
|
Map<String, Object> auditCtx = new HashMap<>();
|
auditCtx.put("userId", user.getUserId());
|
auditCtx.put("userName", user.getUserName());
|
auditCtx.put("tenantId", user.getTenantId());
|
auditCtx.put("ip", IpUtil.getIpAddr()); // 无参方法(终验 javap 核实:不存在接收 request 的重载)
|
auditCtx.put("userAgent", attrs.getRequest().getHeader("User-Agent"));
|
auditCtx.put("uri", attrs.getRequest().getRequestURI());
|
// AUDIT-P1 扩展(2026-07-31):操作入口菜单。header 由前端全局拦截器注入,
|
// 取不到就不放这个键——审计侧按"没带"处理并留痕,不猜。
|
String auditMenuId = attrs.getRequest().getHeader("Jnpf-Menu-Id");
|
if (auditMenuId != null && !auditMenuId.trim().isEmpty()) {
|
auditCtx.put("menuId", auditMenuId.trim());
|
}
|
auditCtx.put("appCode", attrs.getRequest().getHeader("App-Code"));
|
auditCtx.put("appScope", attrs.getRequest().getHeader("Jnpf-App-Scope"));
|
String auditBizSign = attrs.getRequest().getHeader("X-Audit-Biz-Sign");
|
if (auditBizSign != null && !auditBizSign.trim().isEmpty()) {
|
// 固化请求时的关联键;MQ 消费时已脱离 HTTP 线程,不能再读取请求头。
|
auditCtx.put("auditBizSign", auditBizSign.trim());
|
}
|
auditCtx.put("opTime", System.currentTimeMillis());
|
form.setAuditCtx(JsonUtil.getObjectToString(auditCtx)); // form = 该方法内既有的 VisualLogForm 实例
|
}
|
} catch (Throwable t) {
|
log.warn("AUDIT-P1 上下文采集失败, 不影响主流程", t);
|
}
|
try {
|
PublishEventUtil.publish(
|
new ProjectEventBuilder(JnpfConst.VSLOG_EVENT_KEY, form)
|
.setMessageModel(ProjectEventConst.EVENT_PUBLISH_MODEL_BROADCASTING));
|
} catch (Throwable t) {
|
// 日志事件是旁路能力,RocketMQ 不可用时不能影响在线表单主流程保存。
|
log.warn("[VisualLog] 发布表单日志事件失败,已跳过。modelId={}, dataId={}, type={}, error={}",
|
form == null ? null : form.getModelId(),
|
form == null ? null : form.getDataId(),
|
form == null ? null : form.getType(),
|
t.getMessage());
|
}
|
}
|
|
/**
|
* 新增和删除路径原本只发布整行快照,导致统一审计拿不到子表行级变化。
|
* 复用更新路径的 addLog 契约:新增以空数据为 before,删除以空数据为 after。
|
*/
|
private void ensureBoundaryListLog(VisualLogForm form) {
|
if (form == null || CollectionUtils.isNotEmpty(form.getListLog()) || form.getNewData() == null
|
|| (!Objects.equals(form.getType(), 0) && !Objects.equals(form.getType(), 2))) {
|
return;
|
}
|
Map<String, Object> snapshot = form.getNewData();
|
Map<String, Object> originalOld = form.getOldData();
|
try {
|
if (Objects.equals(form.getType(), 0)) {
|
form.setOldData(Collections.emptyMap());
|
} else {
|
form.setOldData(snapshot);
|
form.setNewData(Collections.emptyMap());
|
}
|
List<VisualLogModel> listLog = new ArrayList<>();
|
addLog(form, listLog);
|
form.setListLog(listLog);
|
} catch (Throwable t) {
|
log.warn("[AUDIT] 子表边界差异生成失败,主表审计继续。modelId={}, dataId={}, type={}, error={}",
|
form.getModelId(), form.getDataId(), form.getType(), t.getMessage());
|
} finally {
|
form.setOldData(originalOld);
|
form.setNewData(snapshot);
|
}
|
}
|
|
@Override
|
public List<VisualLogEntity> getList(VisualLogPage pagination) {
|
return this.baseMapper.getList(pagination);
|
}
|
|
/**
|
* 监听日志插入
|
*
|
* @param redisEvent
|
*/
|
@ProjectEventListener(channelRegex = JnpfConst.VSLOG_EVENT_KEY + ".*")
|
public void createLogByEvent(ProjectEventInstance redisEvent) {
|
// [LIMS PATCH 2026-05-15] BROADCASTING 模式下事件经 RocketMQ 跨 JVM 后 source 反序列化为 JSONObject,
|
// 不能直接 cast 成 VisualLogForm(@Builder 无无参构造器无法 parseObject 回去)。
|
// 这里安全跳过非 VisualLogForm 的 source —— 否则抛 ClassCastException 会阻断同一 channel 上后续 listener
|
// (如 jnpf-extend 的 BaseVisualLogListener)。base_visual_log 在跨 JVM 路径下不再被写入,由 lims_biz_log 接管。
|
Object source = redisEvent.getSource();
|
if (!(source instanceof VisualLogForm)) {
|
return;
|
}
|
VisualLogForm form = (VisualLogForm) source;
|
if (Boolean.TRUE.equals(form.getAuditOnly())) {
|
return;
|
}
|
VisualLogEntity visualLogEntity = new VisualLogEntity();
|
visualLogEntity.setType(form.getType());
|
visualLogEntity.setModelId(form.getModelId());
|
visualLogEntity.setDataId(form.getDataId());
|
|
//修改数据
|
if (Objects.equals(form.getType(), 1)) {
|
List<VisualLogModel> listLog = new ArrayList<>();
|
if (CollectionUtils.isNotEmpty(form.getListLog())) {
|
listLog = form.getListLog();
|
} else {
|
addLog(form, listLog);
|
}
|
visualLogEntity.setDataLog(JsonUtil.getObjectToString(listLog));
|
if (CollectionUtils.isNotEmpty(listLog)) {
|
this.save(visualLogEntity);
|
}
|
} else {
|
//新增数据
|
this.save(visualLogEntity);
|
}
|
}
|
|
public void addLog(VisualLogForm formSource, List<VisualLogModel> listLog) {
|
VisualLogForm form = BeanUtil.copyProperties(formSource, VisualLogForm.class);
|
VisualdevReleaseEntity visualdevEntity = visualdevReleaseService.getById(form.getModelId());
|
FormDataModel formData = JsonUtil.getJsonToBean(visualdevEntity.getFormData(), FormDataModel.class);
|
List<FieLdsModel> fieLdsModels = JsonUtil.getJsonToList(formData.getFields(), FieLdsModel.class);
|
List<TableModel> tableModels = JsonUtil.getJsonToList(visualdevEntity.getVisualTables(), TableModel.class);
|
|
List<FieLdsModel> fields = new ArrayList<>();
|
OnlinePublicUtils.recursionFields(fields, fieLdsModels);
|
|
List<Map<String, Object>> swapInfoOld = onlineSwapDataUtils.getSwapInfo(Arrays.asList(form.getOldData()), fields, visualdevEntity.getId(), false, null);
|
Map<String, Object> oldData = swapInfoOld.get(0);
|
List<Map<String, Object>> swapInfoNew = onlineSwapDataUtils.getSwapInfo(Arrays.asList(form.getNewData()), fields, visualdevEntity.getId(), false, null);
|
Map<String, Object> newData = swapInfoNew.get(0);
|
|
for (FieLdsModel item : fields) {
|
//移除系统字段
|
if (isSkipFields(item)) continue;
|
if (JnpfKeyConsts.CHILD_TABLE.equals(item.getConfig().getJnpfKey())) {
|
//子表字段
|
addChildTableLog(item, tableModels, listLog, oldData, newData);
|
} else {
|
addMainMastLog(item, listLog, oldData, newData);
|
}
|
}
|
}
|
|
/**
|
* 添加主附表字段日志
|
*
|
* @param item
|
* @param listLog
|
* @param oldData
|
* @param newData
|
*/
|
private void addMainMastLog(FieLdsModel item, List<VisualLogModel> listLog, Map<String, Object> oldData, Map<String, Object> newData) {
|
String vModel = item.getVModel();
|
ConfigModel config = item.getConfig();
|
String jnpfKey = config.getJnpfKey();
|
String label = config.getLabel();
|
String dataType = config.getDataType();
|
if (!Objects.equals(oldData.get(vModel), newData.get(vModel))) {
|
Integer actionType = 1;//0-新增,1-修改
|
if (oldData.get(vModel) == null || oldData.get(vModel).toString().trim().isEmpty()) {
|
actionType = 0;
|
}
|
boolean nameModified = false;
|
if (JnpfKeyConsts.getNameModified().contains(jnpfKey)
|
&& (JnpfKeyConsts.getNameModifiedNotDynamic().contains(jnpfKey) || OnlineDataTypeEnum.DYNAMIC.getType().equals(dataType))) {
|
nameModified = true;
|
}
|
|
String newValue = getValueByType(newData, vModel, jnpfKey);
|
String oldValue = getValueByType(oldData, vModel, jnpfKey);
|
VisualLogModel vlogModel = new VisualLogModel();
|
vlogModel.setField(vModel);
|
vlogModel.setFieldName(label);
|
vlogModel.setJnpfKey(jnpfKey);
|
vlogModel.setNewData(newValue);
|
vlogModel.setOldData(oldValue);
|
vlogModel.setType(actionType);
|
vlogModel.setNameModified(nameModified);
|
listLog.add(vlogModel);
|
}
|
}
|
|
/**
|
* 添加子表字段日志
|
*
|
* @param item
|
* @param tableModels
|
* @param listLog
|
* @param oldData
|
* @param newData
|
*/
|
private void addChildTableLog(FieLdsModel item, List<TableModel> tableModels, List<VisualLogModel> listLog, Map<String, Object> oldData, Map<String, Object> newData) {
|
ConfigModel config = item.getConfig();
|
String jnpfKey = config.getJnpfKey();
|
String label = config.getLabel();
|
TableModel tableModel = tableModels.stream().filter(t -> t.getTable().equals(config.getTableName())).findFirst().orElse(null);
|
TableFields mainField = tableModel.getFields().stream().filter(t ->
|
Objects.equals(t.getPrimaryKey(), 1) && !t.getField().equalsIgnoreCase(TableFeildsEnum.TENANTID.getField())).findFirst().orElse(null);
|
String mainKey = mainField.getField();
|
|
List<FieLdsModel> children = item.getConfig().getChildren();
|
String vModel = item.getVModel();
|
//子表数据
|
List<Map<String, Object>> childData = new ArrayList<>();
|
//子表表头
|
List<Map<String, Object>> childField = new ArrayList<>();
|
for (FieLdsModel childItem : children) {
|
String jnpfKeyChild = childItem.getConfig().getJnpfKey();
|
if (isSkipFields(childItem))
|
continue;
|
Map<String, Object> childItemMap = new HashMap<>();
|
childItemMap.put("prop", childItem.getVModel());
|
childItemMap.put("label", childItem.getConfig().getLabel());
|
childItemMap.put("jnpfKey", jnpfKeyChild);
|
boolean nameModified = false;
|
if (JnpfKeyConsts.getNameModified().contains(jnpfKeyChild)
|
&& (JnpfKeyConsts.getNameModifiedNotDynamic().contains(jnpfKeyChild) || OnlineDataTypeEnum.DYNAMIC.getType().equals(childItem.getConfig().getDataType()))) {
|
nameModified = true;
|
}
|
childItemMap.put("nameModified", nameModified);
|
childField.add(childItemMap);
|
}
|
|
List<Map<String, Object>> childOld = oldData.get(vModel) == null ? new ArrayList<>() : (List) oldData.get(vModel);
|
List<Map<String, Object>> childNew = newData.get(vModel) == null ? new ArrayList<>() : (List) newData.get(vModel);
|
List<Object> newIds = childNew.stream().map(t -> t.get(mainKey)).collect(Collectors.toList());
|
List<Map<String, Object>> deleteMap = childOld.stream().filter(t -> !newIds.contains(t.get(mainKey))).collect(Collectors.toList());
|
for (Map<String, Object> chilMap : deleteMap) {
|
doDelData(chilMap, children, childData);
|
}
|
for (Map<String, Object> chilMap : childNew) {
|
doNewData(chilMap, mainKey, childOld, children, childData);
|
}
|
if (CollectionUtils.isNotEmpty(childData)) {
|
VisualLogModel vlogModel = new VisualLogModel();
|
vlogModel.setField(vModel);
|
vlogModel.setFieldName(label);
|
vlogModel.setJnpfKey(jnpfKey);
|
vlogModel.setTargetTable(config.getTableName());
|
vlogModel.setChidData(childData);
|
vlogModel.setChidField(childField);
|
vlogModel.setType(1);
|
listLog.add(vlogModel);
|
}
|
}
|
|
private void doDelData(Map<String, Object> chilMap, List<FieLdsModel> children, List<Map<String, Object>> childData) {
|
Map<String, Object> childDataMap = new HashMap<>();
|
for (FieLdsModel childItem : children) {
|
String childJnpfKey = childItem.getConfig().getJnpfKey();
|
//移除系统字段
|
if (isSkipFields(childItem)) continue;
|
String childVmodel = childItem.getVModel();
|
String childVmodelOld = "jnpf_old_" + childItem.getVModel();
|
String oldValue = getValueByType(chilMap, childVmodel, childJnpfKey);
|
childDataMap.put(childVmodel, null);
|
childDataMap.put(childVmodelOld, oldValue);
|
childDataMap.put("jnpf_type", 2);
|
}
|
childData.add(childDataMap);
|
}
|
|
private void doNewData(Map<String, Object> chilMap, String mainKey, List<Map<String, Object>> childOld, List<FieLdsModel> children, List<Map<String, Object>> childData) {
|
Object mainId = chilMap.get(mainKey);
|
Map<String, Object> oldMap = childOld.stream().filter(t -> Objects.equals(t.get(mainKey), mainId)).findFirst().orElse(null);
|
Integer jnpfType = 1;
|
if (oldMap == null) {
|
jnpfType = 0;
|
}
|
Map<String, Object> childDataMap = new HashMap<>();
|
boolean hasChanged = false;
|
for (FieLdsModel childItem : children) {
|
String childJnpfKey = childItem.getConfig().getJnpfKey();
|
//移除系统字段
|
if (isSkipFields(childItem)) continue;
|
String childVmodel = childItem.getVModel();
|
String childVmodelOld = "jnpf_old_" + childItem.getVModel();
|
String newValue = getValueByType(chilMap, childVmodel, childJnpfKey);
|
String oldValue = getValueByType(oldMap, childVmodel, childJnpfKey);
|
if (!Objects.equals(newValue, oldValue)) {
|
hasChanged = true;
|
}
|
childDataMap.put(childVmodel, newValue);
|
childDataMap.put(childVmodelOld, oldValue);
|
childDataMap.put("jnpf_type", jnpfType);
|
}
|
if (hasChanged) {
|
childData.add(childDataMap);
|
}
|
}
|
|
/**
|
* 跳过字段不处理
|
*
|
* @param childItem
|
* @return
|
*/
|
private boolean isSkipFields(FieLdsModel childItem) {
|
//字段不处理直接跳过 :(JnpfKeyConsts.CALCULATE 计算公式仅展示也跳过)
|
List<String> skipFields = new ArrayList<>();
|
skipFields.addAll(JnpfKeyConsts.getSystemKey());
|
skipFields.add(JnpfKeyConsts.POPUPSELECT_ATTR);
|
skipFields.add(JnpfKeyConsts.RELATIONFORM_ATTR);
|
String jnpfKey = childItem.getConfig().getJnpfKey();
|
return skipFields.contains(jnpfKey) || (JnpfKeyConsts.CALCULATE.equals(jnpfKey) && Objects.equals(childItem.getIsStorage(), 0));
|
}
|
|
private String getValueByType(Map<String, Object> map, String vModel, String jnpfKey) {
|
if (map != null) {
|
Object o = map.get(vModel);
|
String value = o == null ? "" : o.toString();
|
switch (jnpfKey) {
|
case JnpfKeyConsts.UPLOADFZ:
|
List<Map<String, String>> listM = (List) o;
|
if (listM != null) {
|
StringJoiner sj = new StringJoiner(",");
|
listM.stream().forEach(t -> sj.add(t.get("name")));
|
value = sj.toString();
|
}
|
break;
|
case JnpfKeyConsts.LOCATION:
|
Map<String, Object> lcationMap = JsonUtil.stringToMap(value);
|
if (MapUtils.isNotEmpty(lcationMap)) {
|
value = lcationMap.get("fullAddress") != null ? lcationMap.get("fullAddress").toString() : "";
|
}
|
break;
|
default:
|
break;
|
}
|
return value;
|
}
|
return null;
|
}
|
}
|