- Visión General del Sistema
1.1 Objetivos de Diseño
- Gestionar el ciclo de vida completo de contratos (borrador → aprobación → asignación → cumplimeinto).
- Soportar arquitectura multiinquilino (cada empresa con su configuración independiente).
- Permitir configuración dinámica de la lógica de negocio mediante plantillas JSON.
- Implementar un motor de ejecución flexible basado en reflexión.
1.2 Principios Fundamentales
Separación de reglas de negocio y código rígido, utilizando configuración externa para lograr adaptabilidad sin reinicios.
- Arquitectura del Sistema
2.1 Módulos Principales
| Módulo | Función | Tecnología |
|---|---|---|
| Motor de Plantillas | Interpreta JSON y construye modelos de negocio | Jackson + anotaciones personalizadas |
| Ejecutor por Reflexión | Carga dinámicamente clases de implementación según configuración JSON | Reflexión Java + caché optimizada |
| Gestión de Diccionarios | Mantiene configuraciones de diccionario de negocio, soporta multinivel y personalización | Caché Redis |
| Motor de Flujo de Aprobación | Interpreta dinámicamente configuraciones de flujo | No implementado actualmente |
- Especificación de Plantilla JSON
3.1 Ejemplo de Estructura de Plantilla
{
"baseProperties": [
{
"componentType": "input:string/pop-up",
"propertyName": "contractName",
"displayName": "Nombre del contrato",
"validator": {
"type": "without",
"content": ""
},
"autoLoadClass": {
"rendering": {
"className": "crear.comun.COMMON_RENDERING",
"methodName": "CREATE_ADMIN_EDIT"
},
"save": {
"className": "crear.comun.raiz.CONTRACT_NAME_",
"methodName": "SAVE"
},
"verify": {
"className": "crear.comun.raiz.CONTRACT_NAME_",
"methodName": "VERIFY"
}
}
}
],
"projectProperties": [
{
"displayName": "Campo de negocio oculto - evento de objeto",
"propertyName": "projects",
"autoLoadClass": {
"save": {
"className": "crear.comun.proyecto.PROJECTS_",
"methodName": "SAVE"
}
}
},
{
"displayName": "Número de registro de proyecto adjudicado",
"propertyName": "bidProjectRegisterNo",
"componentType": "input:string/pop-up",
"validator": {
"type": "without",
"content": ""
},
"autoLoadClass": {
"rendering": {
"className": "crear.comun.proyecto.BID_PROJECT_REGISTER_NO_",
"methodName": "RENDERING"
},
"verify": {
"className": "crear.comun.proyecto.BID_PROJECT_REGISTER_NO_",
"methodName": "VERIFY"
},
"save": {
"className": "crear.comun.proyecto.BID_PROJECT_REGISTER_NO_",
"methodName": "SAVE"
}
}
}
]
}
3.2 Puntos Clave de Diseño
- Extensión del sistema de tipos: Soporte para tipos de formulario personalizados mediante el campo
componentType; validación interna mediantevalidator; soporte para estructuras anidadas. - Mecanismo de validación: Clases de validación, visualización y guardado personalizables.
- Inyección de dependencias mediante reflexión:
@SneakyThrows
public static void ejecutar(Plantilla plantilla, String tipo, Class<?>[] tiposParam, Object... argumentos) {
Map<String, ClaseAutoCarga> mapa = plantilla.getClaseAutoCarga();
if (mapa != null && mapa.containsKey(tipo)) {
ClaseAutoCarga carga = mapa.get(tipo);
String nombreClase = "com.nari.microsvc.marketing.workbench.handover.dynamic_code." + carga.getClassName();
MethodHandle manejador;
if (!CACHE_CLASE.get().getOrDefault(new ConcurrentHashMap<>()).containsKey(nombreClase)) {
Class<?> clazz = Class.forName(nombreClase);
manejador = MethodHandles.lookup().findConstructor(clazz, MethodType.methodType(void.class));
CACHE_CLASE.get().putIfAbsent(nombreClase, manejador);
} else {
manejador = CACHE_CLASE.get().get(nombreClase);
}
ConcurrentMap<String, Method> metodos = CACHE_METODO.get().getOrDefault(nombreClase, new ConcurrentHashMap<>());
try {
Method metodo = metodos.get(carga.getMethodName());
if (metodo != null) {
metodo.invoke(manejador.invoke(), argumentos);
} else {
Object instancia = manejador.invoke();
Method m = instancia.getClass().getMethod(carga.getMethodName(), tiposParam);
m.invoke(instancia, argumentos);
metodos.put(carga.getMethodName(), m);
}
} catch (InvocationTargetException e) {
Throwable causa = e.getTargetException();
if (causa instanceof CreateContractException) throw (CreateContractException) causa;
if (causa instanceof SubmitVerifyException) throw (SubmitVerifyException) causa;
throw new RuntimeException(e);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}
- Diseño del Módulo de Consulta de Listas
4.1 Patrones de Diseño Aplicados
- Cadena de responsabilidad: Procesamiento en pipeline mediante la clase abstracta
PageDataAbstract, cada paso delega al siguiente. - Estrategia: Ensamblaje dinámico de condiciones de búsqueda mdeiante la interfaz
CustomConditionStrategy. - Fábrica: Enrutamiento de fuentes de datos según el tipo de página (
handover,performance).
4.2 Optimización de Rendimiento
@Cacheable(value = "cacheContratos", key = "#dto.page+':'+#dto.pageSize+':'+#dto.sortField")
public ResultadoPagina obtenerContratos(ConsultaDTO dto) { ... }
@CacheEvict(value = "cacheContratos", allEntries = true)
public void refrescarCache() { ... }
4.3 Metadatos de Campos Optimizados
{
"escenario": "marketing_contract_desk",
"fields": {
"contractName": {
"type": "STRING",
"label": "Nombre del contrato",
"search": {"type": "text"},
"table": {"width": "350px"}
},
"contractNumber": {
"type": "STRING",
"label": "Número de contrato",
"search": {"type": "text", "matches": "/^[A-Za-z0-9-]+$/"}
}
},
"tableColumns": [1,2,3],
"searchFields": [1,2]
}
- Módulo de Detalle de Contrato
5.1 Patrón de Cadena de Responsabilidad
La clase abstracta HandleDetails define la interfaz y mantiene un sucesor. Cada paso (datos, plantilla, externo, campos) se ejecuta en orden configurable.
public abstract class HandleDetails {
protected HandleDetails sucesor;
public void setSucesor(HandleDetails s) { this.sucesor = s; }
public void ejecutar(ContratoConstruccion qry, ModeloVistaBasico vista) {
procesar(qry, vista);
if (sucesor != null) sucesor.ejecutar(qry, vista);
}
public abstract void procesar(ContratoConstruccion qry, ModeloVistaBasico vista);
}
5.2 Patrón Composite
Se utiliza Tree para construir flujos ramificados según el tipo de contrato.
@Data
public class Tree {
private String nombre;
private HandleDetails raiz;
private List<HandleDetails> listaNodos;
}
- Sistema de Campos Extendidos de Negocio
6.1 Diseño de Tablas
CREATE TABLE IF NOT EXISTS `TRANSFER_BIZ_EXTEND_FIELD` (
`ID` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`BIZ_MODULE` VARCHAR(64) NOT NULL,
`BIZ_ENTITY_TYPE` VARCHAR(64) NOT NULL,
`FIELD_CODE` VARCHAR(64) NOT NULL,
`FIELD_NAME` VARCHAR(128) NOT NULL,
`DATA_TYPE` VARCHAR(32) NOT NULL,
`LENGTH` INT UNSIGNED DEFAULT 255,
`PRECISION` INT UNSIGNED DEFAULT 19,
`SCALE` INT UNSIGNED DEFAULT 4,
`IS_REQUIRED` TINYINT UNSIGNED NOT NULL DEFAULT 0,
`DEFAULT_VALUE` VARCHAR(512) DEFAULT NULL,
`IS_SEARCHABLE` TINYINT UNSIGNED DEFAULT 0,
`IS_FILTERABLE` TINYINT UNSIGNED DEFAULT 0,
`IS_SORTABLE` TINYINT UNSIGNED DEFAULT 0,
`STATUS` TINYINT UNSIGNED DEFAULT 1,
`EXT_CONFIG` JSON DEFAULT NULL,
`CREATE_TIME` DATETIME DEFAULT CURRENT_TIMESTAMP,
`UPDATE_TIME` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`CREATOR` VARCHAR(64),
`UPDATER` VARCHAR(64),
PRIMARY KEY (`ID`),
UNIQUE KEY `UNIQ_FIELD_CODE` (`FIELD_CODE`),
INDEX `IDX_BIZ_MODULE_ENTITY` (`BIZ_MODULE`, `BIZ_ENTITY_TYPE`),
INDEX `IDX_STATUS` (`STATUS`)
) ENGINE=INNODB DEFAULT CHARSET=UTF8MB4;
CREATE TABLE IF NOT EXISTS `TRANSFER_BIZ_EXTEND_VALUE` (
`ID` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`BIZ_MODULE` VARCHAR(64),
`BIZ_ENTITY_TYPE` VARCHAR(64),
`BIZ_ENTITY_ID` BIGINT UNSIGNED NOT NULL,
`FIELD_CODE` VARCHAR(64),
`FIELD_VALUE` TEXT,
`CREATE_TIME` DATETIME DEFAULT CURRENT_TIMESTAMP,
`UPDATE_TIME` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`ID`),
INDEX `IDX_ENTITY` (`BIZ_MODULE`, `BIZ_ENTITY_TYPE`, `BIZ_ENTITY_ID`),
UNIQUE KEY `UNIQ_ENTITY_FIELD` (`BIZ_MODULE`, `BIZ_ENTITY_TYPE`, `BIZ_ENTITY_ID`, `FIELD_CODE`)
) ENGINE=INNODB DEFAULT CHARSET=UTF8MB4;
6.2 Patrón Estrategia para Manejo de Tipos
public class FabricaManejadorCampo {
private static final Map<String, ManejadorValorCampo> manejadores = new HashMap<>();
static {
manejadores.put(EnumTipoDato.STRING.name(), new ManejadorString());
// otros tipos...
}
public static ManejadorValorCampo obtener(String tipoDato) {
return manejadores.get(tipoDato);
}
}
- Sistema de Diccionario Multiinquilino
7.1 Estructura de Tablas
CREATE TABLE `sys_dict_type` (
`id` bigint AUTO_INCREMENT,
`tenant_id` bigint NOT NULL,
`type_code` varchar(64) NOT NULL,
`type_name` varchar(128),
`is_system` tinyint DEFAULT 0,
`is_shared` tinyint DEFAULT 0,
`status` tinyint DEFAULT 1,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_type_code` (`tenant_id`,`type_code`)
) ENGINE=InnoDB;
CREATE TABLE `sys_dict_data` (
`id` bigint AUTO_INCREMENT,
`tenant_id` bigint NOT NULL,
`type_id` bigint NOT NULL,
`parent_id` bigint DEFAULT 0,
`data_code` varchar(64),
`data_name` varchar(128),
`data_value` varchar(512),
`path` varchar(255),
`level` int DEFAULT 1,
`sort` int DEFAULT 0,
`status` tinyint DEFAULT 1,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_type_data_code` (`type_id`,`data_code`),
KEY `idx_path` (`path`)
) ENGINE=InnoDB;
7.2 Campos Extendidos de Diccionario
Se definen tablas separadas para definición de campos extendidos y sus valores, con soporte para múltiples tipos (string, number, boolean, date, json).
// Ejemplo de definición de campo extendido
POST /api/dict/extend/define
{
"typeId": 5,
"fieldCode": "longitud",
"fieldName": "Longitud",
"fieldType": "number"
}
- Sistema de Botones Dinámicos
8.1 Arquitectura General
- Analizador de configuración (Singleton): Interpreta YAML/JSON y genera objetos
ButtonConfig. - Administrador de contexto (Singleton): Mantiene el contexto de ejecución (rol, escenario) con
ThreadLocal. - Fábrica de estrategias (Fábrica + Estrategia): Decide visibilidad y comportamiento según configuración y contexto.
- Ejecutor de acciones (Comando): Ejecuta la acción definida (llamada API, redirección, popup).
- Observador de recarga en caliente: Escucha cambios en archivos de configuración y refresca las estrategias sin reinicio.
- Evolución de la Arquitectura
9.1 Desde MVC hasta Configuración por Plantillas JSON
- 1.0 (MVC tradicional): Lógica de negocio rígida, alta dependencia código-base de datos, difícil extensión.
- 2.0 (DDD): Mejor organización por dominios, pero aún acoplamiento con cambios de negocio.
- 3.0 (Configuración basada en JSON): Las reglas de negocio se externalizan en plantillas JSON, se cargan dinámicamente mediante reflexión y estrategias, permitiendo adaptación sin reinicio y soporte multiinquilino.
9.2 Lecciones Aprendidas
- El negocio es el motor principal de la arquitectuar; la elección debe coincidir con la complejidad real.
- El desacoplamiento (capas, dominios, reglas vs código) es la clave para la escalabilidad.
- La configuración externa es el camino hacia sistemas verdaderamente adaptables.