Estructura, componentes y flujo de datos del sistema
El sistema ayuntamientosmart.com sigue una arquitectura de tres capas sin frameworks, optimizada para simplicidad operativa y bajo acoplamiento:
Responsable de la interfaz de usuario y la interacción con el usuario final. Compuesta por:
Implementa las reglas de negocio, validaciones y orquestación de operaciones. Ubicada en:
Gestiona el almacenamiento, recuperación y persistencia de datos:
La estructura de directorios del sistema está organizada de forma lógica y predecible:
/
├── index.php # Punto de entrada: login y dashboard
├── ajax.php # Manejador de peticiones AJAX
├── api.php # Gateway API REST con autenticación JWT
├── cron.php # Tareas programadas (backups, notificaciones)
├── inc/ # Componentes core del sistema
│ ├── config.php # Constantes: paths, keys, timeouts
│ ├── core.php # Utilidades base, sesiones, tenant
│ ├── json_db.php # Motor de base de datos JSON
│ ├── hash.php # Hash partitioning (256 buckets)
│ ├── auth.php # Autenticación Argon2id, 2FA, brute-force
│ ├── permisos.php # RBAC con 6 roles y wildcards
│ ├── cifrado.php # AES-256-GCM para campos sensibles
│ ├── log.php # Auditoría con hash SHA256
│ └── validacion.php # Sanitización de inputs
├── modulos/ # Módulos funcionales (25 módulos)
│ ├── denuncias/
│ │ ├── index.php # Listado de denuncias
│ │ ├── ver.php # Detalle de denuncia
│ │ ├── nuevo.php # Crear denuncia
│ │ ├── editar.php # Editar denuncia
│ │ ├── ajax.php # AJAX específico del módulo
│ │ └── exportar.php # Exportar a PDF/Excel
│ ├── sanciones/
│ ├── atestados/
│ └── .../
├── data/ # Datos persistentes
│ ├── {tenant_hash}/ # Datos por tenant (multi-tenancy)
│ │ ├── _config.json # Configuración del tenant
│ │ ├── denuncias/ # Módulo denuncias
│ │ │ ├── 2026/ # Año
│ │ │ │ ├── 00.json # Bucket 00 (hash partition)
│ │ │ │ ├── 01.json # Bucket 01
│ │ │ │ ├── .../
│ │ │ │ └── ff.json # Bucket ff (256 buckets)
│ │ ├── sanciones/
│ │ └── .../
├── assets/ # Recursos estáticos
│ ├── css/
│ ├── js/
│ └── img/
└── vendor/ # Librerías externas (Composer)
El sistema utiliza almacenamiento basado en archivos JSON en lugar de bases de datos tradicionales (MySQL, PostgreSQL). Esta decisión arquitectónica tiene ventajas importantes:
Para evitar archivos JSON excesivamente grandes, se utiliza hash partitioning con 256 buckets (00-ff):
function obtener_bucket($id) {
$hash = md5($id);
return substr($hash, 0, 2); // Primeros 2 caracteres hex: 00-ff
}
// Ejemplo: ID 'DEN-2026-00123'
// MD5: '5f2a8b...' → Bucket: '5f'
// Archivo: /data/{tenant}/denuncias/2026/5f.json
Con 256 buckets, si tenemos 25.600 registros, cada archivo contiene ~100 registros. Archivos de 100 registros son rápidos de leer/escribir.
Para evitar corrupción de datos con escrituras concurrentes, se usa flock():
$fp = fopen($archivo, 'c+');
if (flock($fp, LOCK_EX)) { // Lock exclusivo
$datos = json_decode(fread($fp, filesize($archivo)), true);
// Modificar $datos
ftruncate($fp, 0);
rewind($fp);
fwrite($fp, json_encode($datos, JSON_PRETTY_PRINT));
fflush($fp);
flock($fp, LOCK_UN); // Liberar lock
}
fclose($fp);
Para mejorar el rendimiento, se cachean en memoria (APCu o Redis) los archivos JSON accedidos frecuentemente. La caché se invalida automáticamente cuando se modifica el archivo.
Define todas las constantes del sistema:
// Paths
define('ROOT_PATH', __DIR__ . '/..');
define('DATA_PATH', ROOT_PATH . '/data');
define('INC_PATH', ROOT_PATH . '/inc');
// Seguridad
define('ENCRYPTION_KEY', '...'); // AES-256 key (32 bytes)
define('SALT_TENANT', '...'); // Salt para hash de tenant
define('HASH_ALGO', 'sha256'); // Algoritmo hash para auditoría
// Sesiones
define('SESSION_TIMEOUT', 3600); // 1 hora
define('SESSION_NAME', 'AYUNTAMIENTOSMART_SID');
// Intentos de login
define('MAX_LOGIN_ATTEMPTS', 5);
define('LOGIN_LOCKOUT_TIME', 900); // 15 minutos
Funciones de utilidad general:
API de alto nivel para operaciones CRUD:
// Leer registros
$registros = db_find('denuncias', ['estado' => 'registrada'], 2026);
// Crear registro
$id = db_insert('denuncias', $datos, 2026);
// Actualizar registro
db_update('denuncias', $id, $cambios, 2026);
// Eliminar registro (soft delete)
db_delete('denuncias', $id, 2026);
// Contar registros
$total = db_count('denuncias', ['tipo' => 'hechos'], 2026);
Sistema de autenticación robusto:
Sistema de permisos basado en roles con wildcards:
// 6 roles predefinidos
$ROLES = [
'admin' => ['*'], // Acceso total
'jefe_servicio' => ['denuncias.*', 'sanciones.*', 'estadisticas.*'],
'concejal' => ['denuncias.ver', 'denuncias.crear', 'sanciones.*'],
'tecnico' => ['denuncias.crear', 'sanciones.crear'],
'administrativo'=> ['denuncias.ver', 'sanciones.tramitar'],
'consulta' => ['*.ver']
];
// Comprobar permiso
if (tiene_permiso($usuario, 'denuncias.crear')) {
// Permitir operación
}
Cifrado AES-256-GCM para campos sensibles (DNI, dirección, teléfono, datos médicos):
// Cifrar
$cifrado = cifrar_campo($dni); // Devuelve base64 del texto cifrado + IV + tag
// Descifrar
$dni = descifrar_campo($cifrado);
// Campos cifrados por módulo (constante)
$CAMPOS_CIFRADOS = [
'denuncias' => ['denunciante.dni', 'denunciante.direccion', 'denunciante.telefono'],
'sanciones' => ['conductor.dni', 'conductor.direccion'],
'atestados' => ['detenido.dni', 'detenido.antecedentes']
];
Logging completo de todas las operaciones:
log_auditoria(
$modulo, // 'denuncias', 'sanciones', etc.
$accion, // 'crear', 'modificar', 'eliminar', 'ver'
$registro_id, // ID del registro afectado
$usuario_id, // Usuario que realiza la acción
$datos_extra // Array con datos adicionales
);
// Cada entrada de log incluye:
// - Timestamp exacto (microsegundos)
// - Usuario y rol
// - IP de origen
// - Acción realizada
// - Datos antes/después (para modificaciones)
// - Hash SHA256 de la entrada anterior (cadena inmutable)
El sistema soporta múltiples tenants (ayuntamientos) en la misma instalación mediante subdominios:
// URL: https://madrid.ayuntamientosmart.com/
$subdomain = 'madrid';
$tenant_hash = substr(hash('sha256', $subdomain . SALT_TENANT), 0, 8);
// tenant_hash: 'a3f5c2d1' (8 caracteres hex)
// Los datos del tenant se almacenan en:
// /data/a3f5c2d1/
Cada tenant tiene su propio directorio en /data/. No hay forma de que un tenant acceda a datos de otro. El hash del tenant se calcula con un salt secreto, por lo que no es predecible.
Cada tenant tiene su archivo _config.json con configuración específica:
{
"municipio": "Ayuntamiento de Madrid",
"cif": "P2807900B",
"direccion": "Plaza de la Villa, 1",
"logo": "madrid.png",
"integraciones": {
"dgt_testra": true,
"lexnet": true,
"viogen": false
},
"limites": {
"usuarios_max": 50,
"almacenamiento_mb": 5000
}
}El sistema es altamente modular. Cada módulo (denuncias, sanciones, etc.) es autocontenido en su directorio y sigue la misma estructura:
/modulos/denuncias/
├── index.php # Listado con filtros y paginación
├── ver.php # Vista detalle de un registro
├── nuevo.php # Formulario de creación
├── editar.php # Formulario de edición
├── ajax.php # Endpoints AJAX específicos del módulo
└── exportar.php # Exportación a PDF/Excel
Ventajas de esta arquitectura modular: