Modelo de base de datos

Esquema de datos, relaciones y diccionario de tablas

Version 1.0 AYUNTAMIENTOSMART Generado: 29/05/2026 23:39

Capítulo 1: Visión general del modelo de datos

El modelo de datos de ayuntamientosmart.com sigue un diseño relacional normalizado (hasta 3NF) adaptado al almacenamiento JSON. Las principales características son:

Entidades principales

Metadatos automáticos

Todos los registros incluyen automáticamente estos campos de metadatos:

{
    "_id": "DEN-2026-00001",              // ID único del registro
    "_created": "2026-01-10T10:30:00Z",  // Fecha de creación
    "_modified": "2026-01-10T11:45:00Z", // Última modificación
    "_created_by": "usr-123",            // Usuario que creó
    "_modified_by": "usr-456",           // Usuario que modificó
    "_version": 3,                        // Número de versión (para control de concurrencia)
    "_hash": "sha256:abc123...",         // Hash del contenido
    "_deleted": null                      // Timestamp de eliminación (soft delete)
}

Capítulo 2: Entidad Denuncias

La entidad Denuncias almacena todas las denuncias municipales registradas en el sistema.

Estructura JSON

{
    "_id": "DEN-2026-00001",
    "tipo": "hechos",
    "estado": "registrada",
    "numero": "DEN-2026-00001",
    "fecha_registro": "2026-01-10T10:30:00Z",
    "agente_tramitador": "usr-123",
    
    "denunciante": {
        "persona_id": "per-456",
        "dni": "ENCRYPTED:abc123...",
        "nombre": "María García López",
        "fecha_nacimiento": "1985-05-15",
        "nacionalidad": "ESP",
        "direccion": "ENCRYPTED:def456...",
        "telefono": "ENCRYPTED:ghi789...",
        "email": "maria@example.com"
    },
    
    "hechos": {
        "fecha": "2026-01-09",
        "hora": "18:30",
        "lugar": "Calle Mayor 15, Madrid",
        "coordenadas": {
            "lat": 40.4168,
            "lon": -3.7038
        },
        "descripcion": "Me sustrajeron el teléfono móvil mientras caminaba..."
    },
    
    "denunciados": [
        {
            "persona_id": "per-789",
            "dni": "ENCRYPTED:jkl012...",
            "nombre": "Juan Pérez",
            "relacion_hechos": "Presunto autor de la sustracción"
        }
    ],
    
    "testigos": [
        {
            "persona_id": "per-012",
            "nombre": "Pedro Sánchez",
            "telefono": "ENCRYPTED:mno345..."
        }
    ],
    
    "objetos": [
        {
            "tipo": "telefono",
            "marca": "Apple",
            "modelo": "iPhone 14 Pro",
            "color": "Azul",
            "imei": "123456789012345",
            "valor_estimado": 1000.00
        }
    ],
    
    "adjuntos": [
        {
            "id": "att-001",
            "nombre": "foto_danos.jpg",
            "tipo": "image/jpeg",
            "tamano": 2548672,
            "ruta": "/data/{tenant}/adjuntos/2026/01/att-001.jpg",
            "hash_sha256": "abc123..."
        }
    ],
    
    "tramitacion": {
        "estado_actual": "registrada",
        "fecha_notificacion": "2026-01-10T12:00:00Z",
        "metodo_notificacion": "email",
        "remitida_a": null,
        "fecha_archivo": null,
        "motivo_archivo": null
    },
    
    "firma": {
        "firmada": true,
        "fecha_firma": "2026-01-10T10:35:00Z",
        "usuario_firma": "usr-123",
        "hash_documento": "sha256:def456...",
        "certificado_digital": false
    },
    
    "_created": "2026-01-10T10:30:00Z",
    "_modified": "2026-01-10T10:35:00Z",
    "_created_by": "usr-123",
    "_modified_by": "usr-123",
    "_version": 1,
    "_hash": "sha256:xyz789...",
    "_deleted": null
}

Diccionario de campos

CampoTipoDescripciónValores
tipostringTipo de denunciahechos, perdida, trafico, ruidos, urbanistica
estadostringEstado de tramitaciónborrador, registrada, en_investigacion, remitida, archivada, anulada
denunciante.dnistringDNI/NIE cifradoCifrado AES-256-GCM
hechos.coordenadasobjectGeolocalización del lugar{lat: float, lon: float}
objetosarrayObjetos sustraídos/perdidosArray de objetos

Capítulo 3: Entidad Sanciones

Almacena sanciones de tráfico y multas administrativas.

Estructura JSON

{
    "_id": "SAN-2026-00001",
    "tipo": "in_situ",
    "estado": "notificada",
    "numero_boletin": "SAN-2026-00001",
    "fecha_imposicion": "2026-01-10T15:30:00Z",
    "agente_denunciante": "usr-123",
    
    "vehiculo": {
        "matricula": "1234ABC",
        "marca": "SEAT",
        "modelo": "IBIZA",
        "color": "Blanco",
        "bastidor": "VF1XXXXXXXXXXXX",
        "titular": {
            "dni": "ENCRYPTED:abc123...",
            "nombre": "José Martínez García",
            "direccion": "ENCRYPTED:def456..."
        }
    },
    
    "conductor": {
        "es_titular": true,
        "dni": "ENCRYPTED:abc123...",
        "nombre": "José Martínez García",
        "permiso": "ES987654321",
        "clases_permiso": ["B"],
        "puntos_disponibles": 12
    },
    
    "infraccion": {
        "codigo": "3.1",
        "descripcion": "Circular en sentido contrario al establecido",
        "articulo_ley": "Artículo 29 RGC",
        "calificacion": "grave",
        "fecha": "2026-01-10",
        "hora": "15:15",
        "lugar": "Calle Alcalá 100, Madrid",
        "coordenadas": {"lat": 40.4200, "lon": -3.6900},
        "circunstancias": "Vehículo circulando por carril contrario en calle de sentido único"
    },
    
    "sancion": {
        "importe": 200.00,
        "importe_reducido": 100.00,
        "puntos": 4,
        "fecha_limite_descuento": "2026-01-30",
        "pagada": false,
        "fecha_pago": null,
        "importe_pagado": null,
        "forma_pago": null
    },
    
    "notificacion": {
        "metodo": "in_situ",
        "fecha": "2026-01-10T15:30:00Z",
        "firmada_conductor": true,
        "negativa_firma": false,
        "acuse_recibo": true
    },
    
    "alegaciones": {
        "presentadas": false,
        "fecha_presentacion": null,
        "texto": null,
        "adjuntos": [],
        "resolucion": null,
        "fecha_resolucion": null
    },
    
    "dgt": {
        "enviado": true,
        "fecha_envio": "2026-01-10T16:00:00Z",
        "numero_boletin_dgt": "DGT123456789",
        "acuse_recibo": true
    },
    
    "_created": "2026-01-10T15:30:00Z",
    "_modified": "2026-01-10T16:00:00Z",
    "_created_by": "usr-123",
    "_modified_by": "usr-123",
    "_version": 2,
    "_hash": "sha256:abc123...",
    "_deleted": null
}

Campos específicos de sanciones

CampoTipoDescripciónValores
tipostringTipo de sanciónin_situ, formulada
infraccion.calificacionstringGravedadleve, grave, muy_grave
sancion.puntosintegerPuntos a detraer0-6
conductor.es_titularboolean¿El conductor es el titular?true/false

Capítulo 4: Entidad Personas

Registro unificado de todas las personas que interactúan con el sistema.

Estructura JSON

{
    "_id": "per-123",
    "dni": "ENCRYPTED:abc123...",
    "nombre": "María",
    "apellido1": "García",
    "apellido2": "López",
    "nombre_completo": "María García López",
    "fecha_nacimiento": "1985-05-15",
    "nacionalidad": "ESP",
    "sexo": "F",
    
    "contacto": {
        "direccion": "ENCRYPTED:def456...",
        "municipio": "Madrid",
        "provincia": "Madrid",
        "cp": "28001",
        "telefono_fijo": "ENCRYPTED:ghi789...",
        "telefono_movil": "ENCRYPTED:jkl012...",
        "email": "maria@example.com"
    },
    
    "documentacion": {
        "tipo_documento": "DNI",
        "numero_documento": "ENCRYPTED:abc123...",
        "fecha_expedicion": "2015-06-01",
        "fecha_caducidad": "2025-06-01"
    },
    
    "antecedentes": {
        "tiene_antecedentes": false,
        "denuncias_como_denunciante": 2,
        "denuncias_como_denunciado": 0,
        "sanciones": 1,
        "detenciones": 0
    },
    
    "notas": "Persona colaboradora, víctima habitual de hurtos",
    
    "_created": "2025-03-15T09:00:00Z",
    "_modified": "2026-01-10T10:30:00Z",
    "_created_by": "usr-100",
    "_modified_by": "usr-123",
    "_version": 5,
    "_hash": "sha256:xyz789...",
    "_deleted": null
}

Capítulo 5: Entidad Usuarios

Usuarios del sistema (agentes, administrativos, mandos).

Estructura JSON

{
    "_id": "usr-123",
    "usuario": "agente.garcia",
    "password_hash": "$argon2id$v=19$m=65536,t=4,p=3$...",
    "email": "garcia@policia.madrid.es",
    
    "perfil": {
        "tip": "12345",
        "nombre": "Juan",
        "apellidos": "García Pérez",
        "rango": "Técnico",
        "unidad": "AyuntamientoSmart Madrid",
        "telefono": "666123456",
        "foto": "/uploads/users/usr-123.jpg"
    },
    
    "rol": "agente",
    "permisos": [
        "denuncias.crear",
        "denuncias.ver",
        "sanciones.crear",
        "sanciones.ver",
        "vehiculos.consultar"
    ],
    
    "seguridad": {
        "2fa_habilitado": true,
        "2fa_secret": "ENCRYPTED:abc123...",
        "ultimo_acceso": "2026-01-10T09:00:00Z",
        "ip_ultimo_acceso": "192.168.1.50",
        "intentos_fallidos": 0,
        "bloqueado_hasta": null,
        "cambiar_password": false
    },
    
    "preferencias": {
        "idioma": "es",
        "zona_horaria": "Europe/Madrid",
        "notificaciones_email": true,
        "items_por_pagina": 20
    },
    
    "firma_digital": {
        "tiene_certificado": true,
        "certificado_cn": "Juan García Pérez",
        "certificado_issuer": "FNMT-RCM",
        "certificado_caducidad": "2027-06-01"
    },
    
    "activo": true,
    "fecha_alta": "2020-01-15",
    "fecha_baja": null,
    
    "_created": "2020-01-15T08:00:00Z",
    "_modified": "2026-01-10T09:00:00Z",
    "_created_by": "usr-001",
    "_modified_by": "usr-123",
    "_version": 45,
    "_hash": "sha256:def456...",
    "_deleted": null
}

Capítulo 6: Entidad Auditoría

Log inmutable de todas las operaciones del sistema.

Estructura JSON

{
    "_id": "log-2026-01-10-0001234",
    "timestamp": "2026-01-10T10:30:15.547Z",
    "usuario": {
        "id": "usr-123",
        "nombre": "Juan García",
        "rol": "agente"
    },
    "ip": "192.168.1.50",
    "modulo": "denuncias",
    "accion": "crear",
    "registro_id": "DEN-2026-00001",
    "datos_antes": null,
    "datos_despues": { /* Snapshot del registro creado */ },
    "cambios": [
        {"campo": "estado", "antes": "borrador", "despues": "registrada"}
    ],
    "hash_anterior": "sha256:abc123...",
    "hash_actual": "sha256:def456...",
    "metadata": {
        "user_agent": "Mozilla/5.0...",
        "duracion_ms": 145
    }
}

Relaciones entre entidades

Aunque el almacenamiento es en JSON (no relacional), el modelo lógico mantiene relaciones claras entre entidades mediante IDs de referencia.

Denuncias - Personas

Una denuncia puede tener:

  • 1 denunciante (relación 1:1 con Personas)
  • 0-N denunciados (relación 1:N con Personas)
  • 0-N testigos (relación 1:N con Personas)

La relación se establece mediante el campo persona_id que referencia el _id en la entidad Personas. Al cargar una denuncia, se puede hacer join en memoria para obtener los datos completos de las personas.

Sanciones - Vehículos

Una sanción está asociada a 1 vehículo (relación 1:1). El vehículo se identifica por matrícula. Los datos del vehículo pueden estar almacenados localmente en la entidad Vehículos o consultarse en tiempo real a la DGT mediante integración TESTRA.

Atestados - Denuncias

Un atestado puede originarse de 1 denuncia (relación 1:1 opcional). El campo origen_denuncia_id en Atestados referencia el _id de la denuncia original. Esto permite trazabilidad completa desde la denuncia inicial hasta el procedimiento judicial.

Registros - Usuarios

Todos los registros tienen relación con Usuarios mediante los campos _created_by y _modified_by. Esto permite auditoría completa de quién creó/modificó cada registro.

Índices y optimización de consultas

Para acelerar las consultas más frecuentes, se mantienen índices ligeros en archivos separados.

Índice por estado

Archivo: /data/{tenant}/denuncias/_index_estado.json

{
    "borrador": ["DEN-2026-00005", "DEN-2026-00012"],
    "registrada": ["DEN-2026-00001", "DEN-2026-00002", ...],
    "archivada": ["DEN-2025-00999", ...]
}

Permite filtrar por estado sin cargar todos los buckets.

Índice por fecha

Archivo: /data/{tenant}/denuncias/_index_fecha.json

{
    "2026-01-10": ["DEN-2026-00001", "DEN-2026-00002"],
    "2026-01-09": ["DEN-2025-00999"],
    ...
}

Acelera consultas de denuncias por rango de fechas.

Índice por persona

Archivo: /data/{tenant}/personas/_index_dni.json

{
    "DNI_HASH_12345678A": "per-123",
    "DNI_HASH_87654321B": "per-456",
    ...
}

Permite búsqueda rápida de personas por DNI sin descifrar todos los registros. Se indexa el hash del DNI, no el DNI en claro.

Mantenimiento de índices

Los índices se actualizan automáticamente cada vez que se crea/modifica/elimina un registro. Si un índice se corrompe o desincroniza, puede regenerarse completamente ejecutando el script de mantenimiento: php cron.php rebuild_indexes

Versionado y control de concurrencia

El sistema implementa control de concurrencia optimista mediante versionado de registros.

Campo _version

Cada registro tiene un campo _version que se incrementa en cada modificación. Al actualizar un registro:

  1. El cliente envía el registro con su _version actual (ej: version 5)
  2. El servidor comprueba que la versión en base de datos coincide
  3. Si coincide, actualiza y incrementa a versión 6
  4. Si no coincide (otro usuario modificó mientras tanto), rechaza la actualización con error 409 Conflict

Resolución de conflictos

Cuando se detecta un conflicto de concurrencia (versiones no coinciden), el sistema:

  1. Devuelve error 409 con los datos actuales de la base de datos
  2. El cliente muestra al usuario los cambios en conflicto
  3. El usuario decide: sobrescribir cambios ajenos, descartar sus cambios, o fusionar manualmente
  4. Se reintenta la actualización con la versión correcta

Historial de versiones

Opcionalmente (según configuración), se puede activar el guardado de todas las versiones históricas de un registro en archivos separados:

/data/{tenant}/denuncias/_historial/DEN-2026-00001/
├── v1.json  # Versión 1
├── v2.json  # Versión 2
├── v3.json  # Versión 3
└── v4.json  # Versión actual

Esto permite auditoría completa y recuperación de versiones anteriores.

Soft delete y recuperación

Los registros no se eliminan físicamente de la base de datos. En su lugar, se marcan como eliminados (soft delete).

Marcado como eliminado

Al eliminar un registro, se establece el campo _deleted con el timestamp de eliminación:

{
    "_id": "DEN-2026-00001",
    /* ... resto de campos ... */
    "_deleted": "2026-01-15T16:30:00Z"
}

Las consultas normales excluyen automáticamente registros con _deleted != null. Solo usuarios con permisos especiales pueden ver registros eliminados.

Recuperación de registros eliminados

Un registro eliminado puede recuperarse estableciendo _deleted a null:

db_restore('denuncias', 'DEN-2026-00001');

El registro vuelve a aparecer en consultas normales. La recuperación queda registrada en auditoría.

Purga definitiva

Según política de retención (típicamente 3 años), los registros eliminados se purgan definitivamente mediante un proceso batch nocturno:

// Purgar registros eliminados hace más de 3 años
$fecha_limite = date('Y-m-d', strtotime('-3 years'));
db_purge('denuncias', $fecha_limite);

La purga elimina físicamente los archivos. Este proceso es irreversible.

Cifrado de campos sensibles

Ciertos campos contienen datos personales sensibles que deben cifrarse según RGPD y LO 7/2021.

Campos cifrados

Se cifran con AES-256-GCM los siguientes tipos de datos:

  • Identificación: DNI, NIE, pasaporte, número de seguridad social
  • Contacto: Dirección postal completa, teléfono, email (opcional)
  • Salud: Datos médicos, enfermedades, tratamientos
  • Antecedentes: Antecedentes penales, sanciones previas
  • Datos sensibles: Orientación sexual, creencias religiosas, afiliación sindical

Formato de campo cifrado

Los campos cifrados se almacenan como strings con el prefijo ENCRYPTED:

"dni": "ENCRYPTED:AeB4f7G9..."

El valor después de ENCRYPTED: es base64 de: IV (16 bytes) + ciphertext + tag (16 bytes). Al leer, el sistema detecta el prefijo ENCRYPTED: y descifra automáticamente si el usuario tiene permisos.

Búsqueda en campos cifrados

No se puede buscar directamente en campos cifrados (el cifrado impide búsqueda de texto). Para permitir búsquedas, se mantiene un índice con hash del campo:

// Índice de DNIs
{
    "sha256:abc123...": "per-123",  // Hash de DNI 12345678A
    "sha256:def456...": "per-456"
}

Al buscar por DNI, se calcula su hash y se consulta el índice.

Rotación de claves

Periódicamente (cada 2 años), se debe rotar la clave de cifrado por seguridad. El proceso es:

  1. Generar nueva clave de cifrado
  2. Descifrar todos los campos con clave antigua
  3. Cifrar con clave nueva
  4. Actualizar constante ENCRYPTION_KEY
  5. Destruir clave antigua de forma segura

El sistema incluye script de rotación: php cron.php rotate_encryption_key

Historial de cambios

Version 1.0 - 2026-01-10
  • Versión inicial del modelo de base de datos
  • Documentación completa de entidades principales
  • Estructura JSON de denuncias, sanciones, personas, usuarios
  • Diccionario de campos con tipos y restricciones
  • Relaciones entre entidades
  • Sistema de índices para optimización
  • Versionado y control de concurrencia
  • Soft delete y recuperación
  • Cifrado de campos sensibles