diff --git a/common/common-generator/src/main/java/apelet/common/generator/service/IOnlFormHeadService.java b/common/common-generator/src/main/java/apelet/common/generator/service/IOnlFormHeadService.java index 0782aa4..0172fe3 100644 --- a/common/common-generator/src/main/java/apelet/common/generator/service/IOnlFormHeadService.java +++ b/common/common-generator/src/main/java/apelet/common/generator/service/IOnlFormHeadService.java @@ -119,4 +119,15 @@ public interface IOnlFormHeadService extends IService { * @return */ OnlFormHead getHeadTableNameCache(String tableName); + + /** + * 创建元数据(含默认字段)并同步数据库. 返回创建后的 OnlFormHead. + * + * @param tableName 表名 + * @param tableTxt 表描述 + * @param metaType 表类型: "0"=附表(分录), "1"=主表(业务单据), "2"=单表(基础资料) + * @param sortFiled 关联字段(附表时传主表表名) + * @return OnlFormHead + */ + OnlFormHead createMetaAndSync(String tableName, String tableTxt, String metaType, String sortFiled); } diff --git a/common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlFormHeadServiceImpl.java b/common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlFormHeadServiceImpl.java index 4f54f5a..a6915a5 100644 --- a/common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlFormHeadServiceImpl.java +++ b/common/common-generator/src/main/java/apelet/common/generator/service/impl/OnlFormHeadServiceImpl.java @@ -1660,4 +1660,42 @@ public class OnlFormHeadServiceImpl extends ServiceImpl list) { onlFormHeadMapper.saveEasDict(list); } + + @Override + @Transactional(rollbackFor = Exception.class) + public OnlFormHead createMetaAndSync(String tableName, String tableTxt, String metaType, String sortFiled) { + OnlFormHead head = new OnlFormHead(); + head.setId(idGenerator.nextLongId()); + head.setTableName(tableName); + head.setTableTxt(tableTxt); + head.setTableType(metaType); + head.setStatus("0"); // 未同步状态 + head.setLine(CustomUtil.MetaType_Entry.equals(metaType) ? 1 : 0); + if (StringUtils.isNotEmpty(sortFiled)) { + head.setSort(sortFiled); + } + + // 初始化审计字段(createUserId/updateUserId/createTime/updateTime/deletedFlag) + addOrUpdateBaseData(head, head.getId()); + + // Build DTO + OnlFormHeadDto dto = new OnlFormHeadDto(); + BeanUtil.copyProperties(head, dto); + + // Generate default fields + List defaultFields = new ArrayList<>(); + defaultData(defaultFields, null, null, metaType); + dto.setOnlFormFieldList(defaultFields); + + // Save + this.addAll(dto); + + // Reload to get full info + OnlFormHead saved = this.getById(head.getId()); + + // Sync database (type=0: CREATE TABLE or ALTER TABLE ADD COLUMN) + this.syncDB(saved, 0L); + + return saved; + } } diff --git a/common/common-online/src/main/java/apelet/common/online/controller/OnlineFormController.java b/common/common-online/src/main/java/apelet/common/online/controller/OnlineFormController.java index fa8dc0c..d075054 100644 --- a/common/common-online/src/main/java/apelet/common/online/controller/OnlineFormController.java +++ b/common/common-online/src/main/java/apelet/common/online/controller/OnlineFormController.java @@ -12,8 +12,12 @@ import apelet.common.core.util.MyPageUtil; import apelet.common.core.validator.UpdateGroup; import apelet.common.dbutil.object.SqlTable; import apelet.common.dbutil.object.SqlTableColumn; +import apelet.common.generator.model.OnlFormField; import apelet.common.generator.model.OnlFormHead; +import apelet.common.generator.service.IOnlFormFieldService; import apelet.common.generator.service.IOnlFormHeadService; +import apelet.common.generator.utils.CustomUtil; +import apelet.common.online.util.WidgetFieldTypeMapping; import apelet.common.log.annotation.OperationLog; import apelet.common.log.model.constant.SysOperationLogType; import apelet.common.online.config.OnlineProperties; @@ -49,6 +53,7 @@ import javax.annotation.Resource; import javax.validation.groups.Default; import java.lang.reflect.Method; import java.util.*; +import java.util.function.Consumer; import java.util.stream.Collectors; /** @@ -83,6 +88,8 @@ public class OnlineFormController { @Autowired IOnlFormHeadService onlFormHeadService; @Autowired + IOnlFormFieldService onlFormFieldService; + @Autowired OnlinePageService onlinePageService; @Autowired private OnlineProperties properties; @@ -326,6 +333,13 @@ public class OnlineFormController { onlineForm.setWidgetJson(jsonObject.toJSONString()); } + // ★ 新增: 处理新增字段和子表 + JSONObject updatedJson = syncNewFieldsAndSubTables(onlineForm, onlineFormDto, JSON.parseObject(onlineForm.getWidgetJson())); + if (updatedJson != null) { + onlineForm.setWidgetJson(updatedJson.toJSONString()); + onlineFormDto.setWidgetJson(updatedJson.toJSONString()); + } + if (!onlineFormService.update(onlineForm, originalOnlineForm, datasourceIdSet)) { redissonClient.getBucket(OnlineRedisKeyUtil.makeOnlineFormKey(onlineForm.getFormId())).delete(); return ResponseResult.error(ErrorCodeEnum.DATA_NOT_EXIST); @@ -685,4 +699,283 @@ public class OnlineFormController { } return ResponseResult.success(onlineFormList); } + + // ============ 字段diff+同步 相关私有方法 ============ + + /** + * 对比 widgetJson 与现有 OnlFormField / OnlFormHead 识别新增字段和子表, + * 创建 OnlFormField 记录、同步数据库、创建 OnlineColumn、回填 columnId/relationId。 + * + * @return 修改后的 widgetJson (含回填的 columnId/relationId),无变更则返回 null + */ + private JSONObject syncNewFieldsAndSubTables(OnlineForm form, OnlineFormDto dto, JSONObject widgetJson) { + // 1. 获取数据源 → OnlineTable → OnlFormHead + OnlineDatasource datasource = onlineDatasourceService.getById(dto.getDatasourceIdList().get(0)); + OnlineTable onlineTable = onlineTableService.getById(datasource.getMasterTableId()); + + OnlFormHead onlFormHead = onlFormHeadService.getOne( + new LambdaQueryWrapper() + .eq(OnlFormHead::getTableName, onlineTable.getTableName())); + if (onlFormHead == null) { + return null; + } + + boolean changed = false; + + // === Diff 新增字段 === + Set widgetColumnNames = collectAllBindColumnNames(widgetJson); + List existingFields = onlFormFieldService.list( + new LambdaQueryWrapper().eq(OnlFormField::getHeadId, onlFormHead.getId())); + Set existingFieldNames = existingFields.stream() + .map(OnlFormField::getFieldName).collect(Collectors.toSet()); + Set newColumnNames = new HashSet<>(widgetColumnNames); + newColumnNames.removeAll(existingFieldNames); + + if (!newColumnNames.isEmpty()) { + Map nameToWidget = mapColumnNameToWidget(widgetJson); + OnlineDblink dblink = onlineDblinkService.getById(datasource.getDblinkId()); + + // 预校验:过滤掉 widgetType 不在映射中的控件,避免 getFieldTypeId 抛异常导致部分数据已入库 + Set validColumnNames = new LinkedHashSet<>(); + for (String fieldName : newColumnNames) { + JSONObject widget = nameToWidget.get(fieldName); + Integer widgetType = widget != null ? widget.getInteger("widgetType") : null; + if (widgetType == null || !WidgetFieldTypeMapping.isDataWidget(widgetType)) { + continue; + } + validColumnNames.add(fieldName); + } + if (validColumnNames.isEmpty()) { + return changed ? widgetJson : null; + } + + for (String fieldName : validColumnNames) { + JSONObject widget = nameToWidget.get(fieldName); + Integer widgetType = widget.getInteger("widgetType"); + + OnlFormField field = new OnlFormField(); + field.setHeadId(onlFormHead.getId()); + field.setFieldName(fieldName); + field.setFieldType(WidgetFieldTypeMapping.getFieldTypeId(widgetType)); + Integer length = WidgetFieldTypeMapping.getDefaultLength(widgetType); + if (length != null) { + field.setFieldLength(length); + } + field.setIsEmpty(1); + field.setIsDefault(0); + field.setRowDisabled(0); + field.setSyncFlag(0); + + // 从 widget 的 bindData.columnComment 设置字段备注 + JSONObject bindData = widget.getJSONObject("bindData"); + if (bindData != null && bindData.containsKey("columnComment")) { + field.setFieldRemark(bindData.getString("columnComment")); + } + onlFormFieldService.save(field); + } + + // ALTER TABLE ADD COLUMN + onlFormHeadService.syncDB(onlFormHead, 0L); + + // 批量获取所有列信息,避免 N+1 查询 + List allColumns = onlineDblinkService.getDblinkTableColumnList( + dblink, onlineTable.getTableName()); + Map columnMap = new HashMap<>(); + if (CollUtil.isNotEmpty(allColumns)) { + for (SqlTableColumn col : allColumns) { + if (col.getColumnName() != null) { + columnMap.put(col.getColumnName().toLowerCase(), col); + } + } + } + + // 增量创建 OnlineColumn + 回填 columnId 到 widgetJson + for (String fieldName : validColumnNames) { + SqlTableColumn sqlCol = columnMap.get(fieldName.toLowerCase()); + if (sqlCol != null) { + OnlineColumn newCol = new OnlineColumn(); + newCol.setColumnName(sqlCol.getColumnName()); + newCol.setColumnType(sqlCol.getColumnType()); + newCol.setFullColumnType(sqlCol.getFullColumnType()); + if (sqlCol.getColumnDefault() != null) { + newCol.setColumnDefault(sqlCol.getColumnDefault().toString()); + } + newCol.setNullable(sqlCol.getNullable()); + newCol.setTableId(onlineTable.getTableId()); + onlineColumnService.save(newCol); + + // 回填 columnId 到 widgetJson + JSONObject widget = nameToWidget.get(fieldName); + if (widget != null) { + JSONObject bindData2 = widget.getJSONObject("bindData"); + if (bindData2 != null) { + bindData2.put("columnId", newCol.getColumnId()); + bindData2.put("tableId", onlineTable.getTableId()); + } + } + } + } + changed = true; + } + + // === Diff 新增子表 === + Set widgetSubTables = collectSubTableNames(widgetJson); + List existingSubTables = onlFormHeadService.list( + new LambdaQueryWrapper() + .eq(OnlFormHead::getSort, onlFormHead.getTableName()) + .eq(OnlFormHead::getLine, 1)); + Set existingSubTableNames = existingSubTables.stream() + .map(OnlFormHead::getTableName).collect(Collectors.toSet()); + Set newSubTableNames = new HashSet<>(widgetSubTables); + newSubTableNames.removeAll(existingSubTableNames); + + if (!newSubTableNames.isEmpty()) { + // 获取主表的主键字段(id)作为关联字段 + OnlineColumn masterPkColumn = onlineColumnService.getOne( + new LambdaQueryWrapper() + .eq(OnlineColumn::getTableId, onlineTable.getTableId()) + .eq(OnlineColumn::getColumnName, "id")); + + for (String subTableName : newSubTableNames) { + OnlFormHead subHead = onlFormHeadService.createMetaAndSync( + subTableName, + onlFormHead.getTableTxt() + "附表", + CustomUtil.MetaType_Entry, + onlFormHead.getTableName()); + + // 获取子表的 OnlineTable + OnlineTable subOnlineTable = onlineTableService.getOne( + new LambdaQueryWrapper() + .eq(OnlineTable::getTableName, subTableName.toLowerCase())); + if (subOnlineTable == null) { + // 从DB获取表结构并创建 OnlineTable + SqlTable subSqlTable = onlineDblinkService.getDblinkTable( + onlineDblinkService.getById(datasource.getDblinkId()), subTableName); + if (subSqlTable != null) { + subOnlineTable = onlineTableService.saveNewFromSqlTable(subSqlTable); + } + } + + // 创建子表数据源 + OnlineDatasource subDatasource = new OnlineDatasource(); + subDatasource.setDblinkId(datasource.getDblinkId()); + if (subOnlineTable != null) { + subDatasource.setMasterTableId(subOnlineTable.getTableId()); + } + onlineDatasourceService.save(subDatasource); + + // 创建 1:N 关系 + OnlineDatasourceRelation relation = new OnlineDatasourceRelation(); + relation.setDatasourceId(datasource.getDatasourceId()); + relation.setRelationType(1); + if (subOnlineTable != null) { + relation.setSlaveTableId(subOnlineTable.getTableId()); + } + if (masterPkColumn != null) { + relation.setMasterColumnId(masterPkColumn.getColumnId()); + } + onlineDatasourceRelationService.save(relation); + + backfillRelationId(widgetJson, subTableName, relation.getRelationId()); + } + changed = true; + } + + return changed ? widgetJson : null; + } + + /** + * 遍历 pc/mobile 的 widget 树,收集所有 bindData.columnName 值。 + */ + private Set collectAllBindColumnNames(JSONObject widgetJson) { + Set names = new HashSet<>(); + walkAllModes(widgetJson, widget -> { + JSONObject bindData = widget.getJSONObject("bindData"); + if (bindData != null && bindData.containsKey("columnName")) { + String name = bindData.getString("columnName"); + if (StrUtil.isNotEmpty(name)) { + names.add(name); + } + } + }); + return names; + } + + /** + * 遍历 pc/mobile 的 widget 树,收集 widgetType=403 (TableContainer) 的 props.tableName 值。 + */ + private Set collectSubTableNames(JSONObject widgetJson) { + Set names = new HashSet<>(); + walkAllModes(widgetJson, widget -> { + if (widget.getInteger("widgetType") != null && 403 == widget.getInteger("widgetType")) { + JSONObject props = widget.getJSONObject("props"); + if (props != null && props.containsKey("tableName")) { + String tn = props.getString("tableName"); + if (StrUtil.isNotEmpty(tn)) { + names.add(tn); + } + } + } + }); + return names; + } + + /** + * 构建 columnName → widget 的映射。 + */ + private Map mapColumnNameToWidget(JSONObject widgetJson) { + Map map = new HashMap<>(); + walkAllModes(widgetJson, widget -> { + JSONObject bindData = widget.getJSONObject("bindData"); + if (bindData != null && bindData.containsKey("columnName")) { + String name = bindData.getString("columnName"); + if (StrUtil.isNotEmpty(name)) { + map.put(name, widget); + } + } + }); + return map; + } + + /** + * 在 widgetJson 中查找匹配 tableName 的 TableContainer 控件,回填 relationId。 + */ + private void backfillRelationId(JSONObject widgetJson, String tableName, Long relationId) { + walkAllModes(widgetJson, widget -> { + if (widget.getInteger("widgetType") != null && 403 == widget.getInteger("widgetType")) { + JSONObject props = widget.getJSONObject("props"); + if (props != null && tableName.equals(props.getString("tableName"))) { + props.put("relationId", relationId); + } + } + }); + } + + /** + * 遍历 "pc" 和 "mobile" 两个模式的控件树。 + */ + private void walkAllModes(JSONObject widgetJson, Consumer visitor) { + for (String mode : new String[]{"pc", "mobile"}) { + JSONObject modeObj = widgetJson.getJSONObject(mode); + if (modeObj != null) { + walkWidgets(modeObj, visitor); + } + } + } + + /** + * 递归遍历控件树,访问每个控件及其子控件(widgetList)。 + */ + private void walkWidgets(JSONObject node, Consumer visitor) { + if (node == null) { + return; + } + visitor.accept(node); + JSONArray children = node.getJSONArray("widgetList"); + if (children != null) { + for (int i = 0; i < children.size(); i++) { + walkWidgets(children.getJSONObject(i), visitor); + } + } + } } diff --git a/common/common-online/src/main/java/apelet/common/online/controller/OnlinePageController.java b/common/common-online/src/main/java/apelet/common/online/controller/OnlinePageController.java index 666ea6d..0fd5efa 100644 --- a/common/common-online/src/main/java/apelet/common/online/controller/OnlinePageController.java +++ b/common/common-online/src/main/java/apelet/common/online/controller/OnlinePageController.java @@ -36,6 +36,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.dao.DuplicateKeyException; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; import javax.validation.groups.Default; @@ -82,41 +83,84 @@ public class OnlinePageController { */ @ApiOperationSupport(ignoreParameters = {"onlinePageDto.pageId"}) @OperationLog(type = SysOperationLogType.ADD) + @Transactional(rollbackFor = Exception.class) @PostMapping("/add") public ResponseResult add(@MyRequestBody OnlinePageDto onlinePageDto) { String errorMessage = MyCommonUtil.getModelValidationError(onlinePageDto); if (errorMessage != null) { return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); } + // Validate based on metaSource + String metaSource = onlinePageDto.getMetaSource(); + if ("new".equals(metaSource)) { + // New metadata flow: metaType and pageCode are required + if (StrUtil.isEmpty(onlinePageDto.getMetaType())) { + errorMessage = "数据验证失败,新建元数据时表类型(metaType)不能为空!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + if (StrUtil.isEmpty(onlinePageDto.getPageCode())) { + errorMessage = "数据验证失败,新建元数据时页面编码不能为空!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } else { + // Existing metadata flow: masterTableId is required + if (StrUtil.isEmpty(onlinePageDto.getMasterTableId())) { + errorMessage = "数据验证失败,表单元数据不能为空!"; + return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); + } + } OnlinePage onlinePage = MyModelUtil.copyTo(onlinePageDto, OnlinePage.class); if (onlinePageService.existByPageCode(onlinePage.getPageCode())) { errorMessage = "数据验证失败,页面编码已经存在!"; return ResponseResult.error(ErrorCodeEnum.DUPLICATED_UNIQUE_KEY, errorMessage); } + + Long masterTableId; + OnlFormHead onlFormHead; + + if ("new".equals(metaSource)) { + // === New branch: create fresh metadata === + String metaType = onlinePageDto.getMetaType(); + String tableName = onlinePageDto.getPageCode(); + String tableTxt = onlinePageDto.getPageName(); + + onlFormHead = iOnlFormHeadService.createMetaAndSync(tableName, tableTxt, metaType, null); + masterTableId = onlFormHead.getId(); + onlinePage.setMasterTableId(String.valueOf(masterTableId)); + onlinePage.setMasterTableName(onlFormHead.getTableName()); + + // For business documents (metaType="1"): auto-create sub-table + if (CustomUtil.MetaType_Main.equals(metaType)) { + String entryTableName = tableName + "_entry"; + iOnlFormHeadService.createMetaAndSync(entryTableName, tableTxt + "附表", CustomUtil.MetaType_Entry, tableName); + } + } else { + // === Existing branch: use existing metadata === + String formMetadata = onlinePageDto.getMasterTableId(); + onlFormHead = iOnlFormHeadService.getById(formMetadata); + masterTableId = Long.valueOf(formMetadata); + } + try { onlinePage = onlinePageService.saveNew(onlinePage); } catch (DuplicateKeyException e) { errorMessage = "数据验证失败,当前应用的页面编码已经存在!"; return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); } - //根据所传表单元数据id查出对应表单信息 - String formMetadata = onlinePageDto.getMasterTableId(); - //在线表单主表对象 - OnlFormHead onlFormHead = iOnlFormHeadService.getById(formMetadata); - //准备创建数据模型所需的数据 + + // 准备创建数据模型所需的数据 OnlineDatasourceDto onlineDatasourceDto = new OnlineDatasourceDto(); - //数据源标识-取表名驼峰 + // 数据源标识-取表名驼峰 onlineDatasourceDto.setVariableName(CustomUtil.UnderlineToCamel(onlFormHead.getTableName())); - //数据主表标识 + // 数据主表标识 onlineDatasourceDto.setMasterTableName(onlFormHead.getTableName()); - //数据源名称-取表名 + // 数据源名称-取表名 onlineDatasourceDto.setDatasourceName((onlFormHead.getTableName())); - - //目前系统只有一个数据源,所以默认1740180645717610505 + // 目前系统只有一个数据源,所以默认1740180645717610505 OnlineDblink onlineDblink = onlineDblinkService.getById(1740180645717610505L); onlineDatasourceDto.setDblinkId(1740180645717610505L); - //创建数据模型 + // 创建数据模型 SqlTable sqlTable = onlineDblinkService.getDblinkTable(onlineDblink, onlineDatasourceDto.getMasterTableName()); if (sqlTable == null) { errorMessage = "数据验证失败,指定的数据表名不存在!"; @@ -130,14 +174,11 @@ public class OnlinePageController { return ResponseResult.error(ErrorCodeEnum.DATA_VALIDATED_FAILED, errorMessage); } /** - *创建数据表关联关系 先查出改元数据表的所有附表关系 + * 创建数据表关联关系 先查出改元数据表的所有附表关系 * 一对多 - * */ - addOnlineDatasourceRelation(onlFormHead, onlineDatasource, errorMessage, onlineDblink); - return ResponseResult.success(onlinePage.getPageId()); } diff --git a/common/common-online/src/main/java/apelet/common/online/dto/OnlinePageDto.java b/common/common-online/src/main/java/apelet/common/online/dto/OnlinePageDto.java index 59c2529..afa6f86 100644 --- a/common/common-online/src/main/java/apelet/common/online/dto/OnlinePageDto.java +++ b/common/common-online/src/main/java/apelet/common/online/dto/OnlinePageDto.java @@ -60,13 +60,23 @@ public class OnlinePageDto { * 表单元数据。 */ @Schema(description = "表单元数据") - @NotNull(message = "数据验证失败,表单元数据不能为空!") private String masterTableId; /** * 表单元数据。 */ @Schema(description = "表单元数据表名") -// @NotNull(message = "数据验证失败,表单元数据不能为空!") private String masterTableName; + + /** + * 数据来源: "new"=新建元数据, "existing"=使用已有元数据。 + */ + @Schema(description = "数据来源: new=新建元数据, existing=使用已有元数据") + private String metaSource; + + /** + * 表类型: "2"=基础资料(单表), "1"=业务单据(主表+附表)。仅在metaSource="new"时有效。 + */ + @Schema(description = "表类型: 2=基础资料(单表), 1=业务单据(主表+附表)。仅在metaSource=new时有效") + private String metaType; }