Creación de un Sistema de Actualización de Aplicaciones Móviles con Spring Boot y UniApp

La actualización de versiones es un aspecto crucial en el desarrollo de aplicaciones móviles. Los procesos de revisión de las tiendas de aplicaciones tradicionales pueden ser lentos y complicados. Alternativamente, el servicio oficial uni-upgrade-center requiere depender de uniCloud, lo que puede añadir complejidad y costos de mantenimiento para equipos que ya poseen una arquitectura de backend robusta. Este artículo te guiará en la construcción de tu propio sistema de actualización de aplicaciones, ofreciéndote control total y agilidad.

¿Por Qué Desarrollar un Sistema de Actualización Propio?

Existen principalmente tres enfoques para la actualización de aplicaciones móviles:

  • Actualizaciones a través de Tiendas de Aplicaciones: Ofrecen confianza al usuario y soporte oficial, pero el proceso de revisión puede ser prolongado, impidiendo la corrección rápida de errores críticos. Son ideales para actualizaciones de versiones mayores.
  • uni-upgrade-center Oficial: Es fácil de implementar y usar directamente, pero depende de uniCloud y no permite una personalización profunda de la lógica. Adecuado para proyectos pequeños y desarrollo rápido.
  • Sistema de Actualización Propio: Proporciona control total y respuestas rápidas. Requiere una inversión inicial en desarrollo, pero es ideal para proyectos medianos a grandes y cuando ya se dispone de un servicio backend.

Las ventajas de un sistema propio incluyen:

  • Unificación Tecnológica: Aprovecha tu infraestructura SpringBoot existente, reduciendo la complejidad y los costos de mantenimiento asociados a múltiples pilas tecnológicas.
  • Flexibilidad: Permite diseñar estrategias de actualización adaptadas a tus necesidades específicas.
  • Optimización de Costos: Elimina la necesidad de servicios externos de pago.
  • Rapidez: Facilita la distribución de correcciones urgentes a los usuarios en cuestión de minutos.

Diseño de la Arquitectura del Sistema

Un sistema de actualización de aplicaciones requiere una colaboración fluida entre el frontend y el backend. La siguiente arquitectura describe cómo interactuarán:


Frontend (UniApp) <--- HTTP/HTTPS ---> Backend (SpringBoot)
    ↑                               ↑
    |                               |
Detección de Versión           Gestión de Versiones
Gestión de Descargas         Almacenamiento de Archivos
Gestión de Instalación        Análisis de Estadísticas

Diseño de Tablas Clave en el Backend

Para soportar la funcionalidad de actualización, necesitaremos al menos dos tablas en nuestra base de datos:

Tabla de Configuración de Versiones (biz_version_config)

Esta tabla almacenará la información detallada de cada versión de la aplicación.


CREATE TABLE `biz_version_config` (
  `id` INT AUTO_INCREMENT PRIMARY KEY,
  `version_code` VARCHAR(50) NOT NULL COMMENT 'Código de versión, ej. 1.0.0',
  `version_name` VARCHAR(100) COMMENT 'Nombre de la versión',
  `download_url` VARCHAR(255) NOT NULL COMMENT 'URL de descarga del paquete de instalación',
  `update_log` TEXT COMMENT 'Registro de cambios de la actualización',
  `file_size` BIGINT COMMENT 'Tamaño del archivo en bytes',
  `file_md5` VARCHAR(32) COMMENT 'Valor MD5 para verificación del archivo',
  `is_force` TINYINT DEFAULT 0 COMMENT 'Indica si la actualización es forzada (1) o no (0)',
  `is_current` TINYINT DEFAULT 0 COMMENT 'Indica si es la versión actual disponible (1) o no (0)',
  `platform` VARCHAR(20) COMMENT 'Plataforma: android, ios, all',
  `min_support_version` VARCHAR(50) COMMENT 'Versión mínima soportada para esta actualización',
  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
  `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX `idx_platform_current` (`platform`, `is_current`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Configuración de versiones de la APP';

Tabla de Registros de Actualización (biz_update_log)

Esta tabla registrará las acciones de actualización realizadas por los usuarios.


CREATE TABLE `biz_update_log` (
  `id` BIGINT AUTO_INCREMENT PRIMARY KEY,
  `user_id` VARCHAR(100) NOT NULL COMMENT 'Identificador del usuario',
  `version_code` VARCHAR(50) NOT NULL COMMENT 'Código de la versión que el usuario tenía',
  `new_version_code` VARCHAR(50) NOT NULL COMMENT 'Código de la nueva versión a la que se actualiza',
  `platform` VARCHAR(20) NOT NULL COMMENT 'Plataforma del dispositivo',
  `update_type` VARCHAR(50) COMMENT 'Tipo de actualización (ej. manual, automática, forzada)',
  `status` VARCHAR(50) COMMENT 'Estado de la actualización (ej. iniciado, completado, fallido)',
  `timestamp` DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Registro de actualizaciones de APP';

Lógica del Backend (SpringBoot)

El backend será responsable de gestionar las versiones y proveer los endpoints necesarios para la aplicación móvil.

Endpoint para Consultar Versiones

Este endpoint permitirá a la aplicación cliente verificar si hay una nueva versión disponible.


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/app/update")
public class AppUpdateController {

    @Autowired
    private AppVersionService appVersionService;

    /**
     * Consulta la última versión disponible para una plataforma específica.
     * @param platform Plataforma ('android' o 'ios').
     * @param currentVersionCode Código de la versión actual del cliente.
     * @return Información de la versión disponible.
     */
    @GetMapping("/check")
    public ResponseEntity<VersionInfo> checkUpdate(
            @RequestParam("platform") String platform,
            @RequestParam("currentVersionCode") String currentVersionCode) {

        // Validar plataforma y código de versión entrante
        if (platform == null || platform.isEmpty() || currentVersionCode == null || currentVersionCode.isEmpty()) {
            return ResponseEntity.badRequest().build();
        }

        VersionInfo latestVersion = appVersionService.getLatestVersion(platform, currentVersionCode);

        if (latestVersion != null) {
            // Registrar el evento de consulta (opcional, pero útil para estadísticas)
            // appVersionService.logVersionCheck(userId, currentVersionCode, platform);
            return ResponseEntity.ok(latestVersion);
        } else {
            return ResponseEntity.notFound().build(); // No hay actualización disponible o la versión actual es la más reciente compatible
        }
    }

    // Otras definiciones de endpoint como descarga, registro, etc.
}

Definición de la clase DTO VersionInfo (simplificada):


public class VersionInfo {
    private String versionCode;
    private String versionName;
    private String downloadUrl;
    private String updateLog;
    private long fileSize;
    private String fileMd5;
    private boolean isForce;
    private String minSupportVersion;

    // Getters y Setters...
}

Servicio AppVersionService (simplificado):


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

@Service
public class AppVersionService {

    @Autowired
    private AppVersionRepository versionRepository; // Asumiendo un Spring Data JPA Repository

    public VersionInfo getLatestVersion(String platform, String currentVersionCode) {
        // Obtener la versión más reciente marcada como 'current' para la plataforma dada
        // Y que además cumpla con la versión mínima soportada si aplica
        BizVersionConfig latestConfig = versionRepository.findLatestForPlatformAndCurrent(platform);

        if (latestConfig == null) {
            return null;
        }

        // Lógica de comparación de versiones (se puede usar una librería externa)
        boolean needsUpdate = compareVersions(latestConfig.getVersionCode(), currentVersionCode) > 0;
        // O también verificar si la versión actual del cliente es menor que la mínima soportada
        boolean isTooOld = compareVersions(currentVersionCode, latestConfig.getMinSupportVersion()) < 0;


        if (needsUpdate || isTooOld) {
             // Construir el objeto VersionInfo a partir de BizVersionConfig
            VersionInfo info = new VersionInfo();
            info.setVersionCode(latestConfig.getVersionCode());
            info.setVersionName(latestConfig.getVersionName());
            info.setDownloadUrl(latestConfig.getDownloadUrl());
            info.setUpdateLog(latestConfig.getUpdateLog());
            info.setFileSize(latestConfig.getFileSize());
            info.setFileMd5(latestConfig.getFileMd5());
            // Forzar actualización si es necesario O si la versión actual es muy antigua
            info.setForce(latestConfig.isForce() || isTooOld);
            info.setMinSupportVersion(latestConfig.getMinSupportVersion());
            return info;
        }

        return null; // No se necesita actualización
    }

    // Método auxiliar para comparar versiones (necesita implementación robusta)
    private int compareVersions(String version1, String version2) {
        if (version1 == null && version2 == null) return 0;
        if (version1 == null) return -1;
        if (version2 == null) return 1;

        String[] parts1 = version1.split("\\.");
        String[] parts2 = version2.split("\\.");

        int length = Math.max(parts1.length, parts2.length);
        for (int i = 0; i < length; i++) {
            int v1 = i < parts1.length ? Integer.parseInt(parts1[i]) : 0;
            int v2 = i < parts2.length ? Integer.parseInt(parts2[i]) : 0;
            if (v1 < v2) return -1;
            if (v1 > v2) return 1;
        }
        return 0; // Versiones iguales
    }

    // Métodos para registrar logs, etc.
}

Endpoint para Descargar el Paquete

Este endpoint servirá los archivos de actualización. Es crucial implementarlo de forma eficiente para manejar grandes archivos.


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;

@RestController
@RequestMapping("/api/app/update")
public class AppDownloadController {

    @Autowired
    private FileStorageService fileStorageService; // Servicio para manejar la carga/descarga de archivos

    /**
     * Descarga el paquete de actualización especificado por su nombre de archivo.
     * @param filename Nombre del archivo del paquete de actualización.
     * @return El archivo como un recurso de Spring.
     */
    @GetMapping("/download/{filename:.+}")
    public ResponseEntity<Resource> downloadFile(@PathVariable String filename) {
        try {
            Resource resource = fileStorageService.loadFileAsResource(filename);

            // Determinar el tipo de contenido MIME
            String contentType = "application/octet-stream"; // Tipo genérico por defecto
            if (filename.endsWith(".apk")) {
                contentType = "application/vnd.android.package-archive";
            } else if (filename.endsWith(".ipa")) { // Soporte para iOS, aunque la actualización nativa es diferente
                contentType = "application/octet-stream"; // O un tipo más específico si se maneja de forma diferente
            }

            return ResponseEntity.ok()
                    .contentType(MediaType.parseMediaType(contentType))
                    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
                    .body(resource);
        } catch (IOException ex) {
            // Manejar la excepción si el archivo no se encuentra o hay un error de lectura
            return ResponseEntity.notFound().build();
        }
    }

    // Implementación del FileStorageService (simplificada)
    // Aquí iría la lógica para leer archivos desde el disco o un almacenamiento S3, etc.
    public static class FileStorageService {
        // ... implementación para cargar recursos ...
        public Resource loadFileAsResource(String filename) throws IOException {
            // Lógica para obtener el Resource (ej. usando Path, Files.readAllBytes, etc.)
            // Retornar un objeto Resource adecuado
            return null; // Placeholder
        }
    }
}

Lógica del Frontend (UniApp)

La aplicación móvil (desarrollada con UniApp) será la encargada de interactuar con el backend para verificar, descargar e instalar las actualizaciones.

Módulo de Verificación y Actualización

Este módulo se ejecutará al inicio de la aplicación o periódicamente para comprobar si hay nuevas versiones.


// utils/updateManager.js

import {
    getAppVersionConfig, // Función para llamar al endpoint /api/app/update/check
    downloadAppPackage      // Función para llamar al endpoint /api/app/update/download
} from '@/api/updateApi.js'; // Asumiendo que tienes un archivo de configuración de API

// Necesitarás importar módulos nativos o usar plugins para la instalación
// Ejemplo usando un plugin común para actualizaciones (ej. uni-update-app)
// o llamando a API nativas si estás desarrollando para plataformas específicas.

const app = getApp();

export async function checkAndUpdate() {
    // Obtener la versión actual de la app
    const currentVersion = app.globalData.version; // Asumiendo que la versión está en globalData

    // Obtener la plataforma
    const platform = uni.getSystemInfoSync().platform; // 'ios' o 'android'
    const platformName = platform === 'ios' ? 'ios' : 'android';

    try {
        // 1. Consultar al backend por la última versión
        const response = await getAppVersionConfig(platformName, currentVersion);

        if (response.statusCode === 200 && response.data) {
            const updateInfo = response.data;

            // 2. Comparar versiones (lógica simple)
            if (compareVersions(updateInfo.versionCode, currentVersion) > 0) {
                // Hay una nueva versión disponible
                console.log('Nueva versión encontrada:', updateInfo.versionCode);

                // Mostrar diálogo al usuario
                uni.showModal({
                    title: 'Actualización Disponible',
                    content: `Versión ${updateInfo.versionCode} disponible.\n\n${updateInfo.updateLog || 'Actualiza para obtener las últimas mejoras.'}`,
                    showCancel: !updateInfo.isForce, // No mostrar botón de cancelar si es forzada
                    confirmText: updateInfo.isForce ? 'Actualizar Ahora' : 'Actualizar',
                    success: async (res) => {
                        if (res.confirm) {
                            // Iniciar la descarga
                            await downloadAndInstall(updateInfo);
                        } else if (!updateInfo.isForce) {
                            console.log('El usuario decidió no actualizar.');
                        }
                    }
                });
            } else {
                console.log('La aplicación está al día.');
            }
        } else if (response.statusCode === 404) {
            console.log('No se encontraron actualizaciones disponibles.');
        } else {
            console.error('Error al consultar actualizaciones:', response.statusCode, response.data);
            // Manejar otros códigos de estado si es necesario
        }
    } catch (error) {
        console.error('Error de red o del servidor al buscar actualizaciones:', error);
    }
}

async function downloadAndInstall(updateInfo) {
    uni.showLoading({
        title: 'Descargando actualización...'
    });

    try {
        // 3. Descargar el paquete
        // Nota: uni.downloadFile permite descargar archivos. Para instalaciones automáticas,
        // puede requerir nativas o plugins específicos, especialmente en Android.
        const downloadTask = uni.downloadFile({
            url: updateInfo.downloadUrl, // URL del backend que sirve el archivo
            success: async (res) => {
                if (res.statusCode === 200) {
                    const tempFilePath = res.tempFilePath;
                    console.log('Archivo descargado en:', tempFilePath);

                    // 4. Instalar el paquete
                    // La instalación varía significativamente entre plataformas.
                    // Para Android, puedes usar un plugin o solicitar permisos para instalar desde fuentes desconocidas.
                    // Para iOS, las actualizaciones de código nativo generalmente requieren reinstalar la app,
                    // pero las actualizaciones de contenido (si se manejan así) son diferentes.
                    // Este ejemplo asume un escenario simplificado o el uso de un plugin.

                    // Ejemplo con uni.installApp (puede no estar disponible o funcionar como se espera en todas las versiones/plataformas)
                    // O usando un plugin específico como uni-update-app o llamadas nativas.
                    // const installResult = await uni.installApp({ filePath: tempFilePath });
                    // console.log('Resultado de instalación:', installResult);

                    // Si se usa un plugin externo o API nativa, la llamada sería diferente.
                    // Por ahora, solo informamos al usuario.
                    uni.hideLoading();
                    uni.showToast({
                        title: 'Descarga completa. Por favor, instale manualmente.',
                        icon: 'none',
                        duration: 3000
                    });

                    // *** Lógica de instalación real ***
                    // Aquí deberías integrar con APIs nativas o plugins para iniciar la instalación.
                    // Ejemplo conceptual:
                    // if (platform === 'android') {
                    //     // Usar plugin para instalar APK
                    //     // InstallApkPlugin.install(tempFilePath);
                    // } else if (platform === 'ios') {
                    //     // iOS a menudo requiere que el usuario descargue desde la App Store,
                    //     // a menos que se implementen soluciones de actualización de contenido in-app.
                    // }

                } else {
                    console.error('Error al descargar el archivo:', res.statusCode);
                    uni.hideLoading();
                    uni.showToast({ title: 'Error en la descarga', icon: 'error' });
                }
            },
            fail: (err) => {
                console.error('Fallo al descargar el archivo:', err);
                uni.hideLoading();
                uni.showToast({ title: 'Fallo en la descarga', icon: 'error' });
            }
        });

        // Opcional: Mostrar progreso de descarga
        downloadTask.onProgressUpdate((res) => {
            console.log('Progreso de descarga: ' + res.progress);
            uni.showLoading({
                title: `Descargando: ${res.progress}%`
            });
        });

    } catch (error) {
        console.error('Error durante el proceso de descarga:', error);
        uni.hideLoading();
        uni.showToast({ title: 'Error de descarga', icon: 'error' });
    }
}

// Función auxiliar para comparar versiones (debe ser consistente con la del backend)
function compareVersions(versionA, versionB) {
    if (versionA === versionB) return 0;

    const partsA = versionA.split('.').map(Number);
    const partsB = versionB.split('.').map(Number);

    const maxLength = Math.max(partsA.length, partsB.length);

    for (let i = 0; i < maxLength; i++) {
        const valA = partsA[i] || 0;
        const valB = partsB[i] || 0;

        if (valA < valB) return -1;
        if (valA > valB) return 1;
    }
    return 0;
}

// Ejemplo de cómo llamar a la función de verificación (ej. en App.vue o una página principal)
// mounted() {
//     checkAndUpdate();
// }

API de Ejemplo (api/updateApi.js)


import request from '@/utils/request.js'; // Asumiendo un helper de petición HTTP

const BASE_URL = '/api/app/update'; // La URL base de tu API de actualización

/**
 * Consulta la información de la última versión disponible.
 * @param {string} platform - Plataforma ('android' o 'ios').
 * @param {string} currentVersionCode - Código de la versión actual.
 * @returns {Promise<object>} - Respuesta de la API.
 */
export function getAppVersionConfig(platform, currentVersionCode) {
    return request.get(`${BASE_URL}/check`, {
        params: {
            platform: platform,
            currentVersionCode: currentVersionCode
        }
    });
}

/**
 * Solicita la descarga de un paquete de actualización.
 * Nota: La descarga real se realiza a través de uni.downloadFile en el cliente,
 * este endpoint podría ser para obtener la URL o iniciar el proceso en el servidor
 * si se usa un método diferente. Aquí asumimos que solo se obtiene la URL.
 * @param {string} filename - Nombre del archivo a descargar.
 * @returns {Promise<object>} - Respuesta con la URL de descarga o datos del archivo.
 */
export function downloadAppPackage(filename) {
    // Si el backend solo proporciona la URL, esta función podría no ser necesaria o
    // ser diferente. Si el backend sirve el archivo directamente, esta función
    // llamaría a un endpoint que responde con el archivo.
    // En nuestro ejemplo de controller, el frontend llama directamente a /download/{filename}
    // Así que esta función de API podría solo devolver la URL base del servidor de descargas.
    const downloadServerUrl = '/api/app/update/download/'; // Asegúrate que coincida con tu backend
    return Promise.resolve({
        statusCode: 200,
        data: {
            downloadUrl: `${downloadServerUrl}${filename}` // La URL completa para uni.downloadFile
        }
    });
}
</object></object>

Consideraciones Adicionales

  • Seguridad: Asegúrate de que las URLs de descarga sean seguras (HTTPS) y considera la verificación de la integridad de los archivos descargados (usando MD5 o similar).
  • Experiencia de Usuario: Proporciona feedback claro al usuario sobre el progreso de la descarga e instalación. Maneja casos de error de manera elegante.
  • Rollback: Para actualizaciones críticas, considera tener un plan de rollback o la capacidad de revertir una versión si se descubren problemas graves.
  • Plataforma iOS: Las actualizaciones de aplicaciones iOS a través de la App Store son el método principal. Para actualizaciones de contenido o configuraciones sin necesidad de reinstalar la app completa, se requieren enfoques de desarrollo diferentes (ej. CDN para recursos, configuración remota). La actualización directa de binarios no es posible como en Android sin pasar por la App Store.
  • Pruebas: Realiza pruebas exhaustivas en diferentes dispositivos y versiones de sistema operativo.

Al implementar este sistema, obtendrás una solución de actualización ágil y adaptada a tus necesidades, liberándote de las limitaciones de los canales de distribución tradicionales para las actualizaciones menores y correcciones rápidas.

Etiquetas: SpringBoot uniapp Actualización de Aplicaciones Hot Update Desarrollo Móvil

Publicado el 6-24 04:12