Conception et implémentation du backend pour une plateforme de consultation juridique

Schéma de la base de données

Pour commencer, nous définissons la structure des données pour stocker les informations des utilisateurs. La table t_utilisateur est conçue pour gérer les identités, les rôles (utilisateur, avocat, administrateur) et l'état du compte.

DROP TABLE IF EXISTS `t_utilisateur`;
CREATE TABLE `t_utilisateur` (
    `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'Identifiant unique',
    `nom_affichage` VARCHAR(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'Nom d\'affichage',
    `identifiant` VARCHAR(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'Identifiant de connexion',
    `url_avatar` VARCHAR(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'URL de la photo de profil',
    `genre` TINYINT NULL DEFAULT NULL COMMENT 'Genre (0: inconnu, 1: homme, 2: femme)',
    `mot_de_passe` VARCHAR(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'Mot de passe haché',
    `telephone` VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'Numéro de téléphone',
    `courriel` VARCHAR(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'Adresse e-mail',
    `statut` INT NOT NULL DEFAULT 0 COMMENT 'État du compte (0: normal)',
    `date_creation` DATETIME NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Date de création',
    `date_modification` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'Date de mise à jour',
    `est_supprime` TINYINT NOT NULL DEFAULT 0 COMMENT 'Marqueur de suppression logique',
    `role` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Rôle (0: Client, 1: Avocat, 2: Admin)',
    PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

Configuration et Structure du Projet

1. Définition des codes d'erreur

Pour une gestion centralisée des erreurs, nous utilisons une énumération qui définit les codes HTTP, les messages et les descriptions détaillées.

public enum CodeStatut {
    PARAMETRE_INVALIDE(40000, "Paramètres de requête invalides", ""),
    DONNEE_MANQUANTE(40001, "Les données de la requête sont vides", ""),
    NON_AUTHENTIFIE(40100, "Utilisateur non connecté", ""),
    ACCES_REFUSE(40101, "Autorisation insuffisante", ""),
    ERREUR_DB(40102, "Échec de l'opération de base de données", ""),
    ERREUR_SYSTEME(50000, "Erreur interne du système", "");

    private final int code;
    private final String message;
    private final String description;

    CodeStatut(int code, String message, String description) {
        this.code = code;
        this.message = message;
        this.description = description;
    }

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

    public String getDescription() {
        return description;
    }
}

2. Objet de réponse uniforme

Cette classe générique encapsule toutes les réponses envoyées au frontend, assurant une structure cohérente pour le succès et l'échec.

import lombok.Data;
import java.io.Serializable;

@Data
public class ReponseApi<T> implements Serializable {
    private int code;
    private T donnees;
    private String message;
    private String description;

    public ReponseApi(int code, T donnees, String message, String description) {
        this.code = code;
        this.donnees = donnees;
        this.message = message;
        this.description = description;
    }

    public ReponseApi(int code, T donnees) {
        this(code, donnees, "Ok", "");
    }

    public ReponseApi(CodeStatut codeStatut) {
        this(codeStatut.getCode(), null, codeStatut.getMessage(), codeStatut.getDescription());
    }
}

3. Utilitaire de construction des réponses

Pour simplifier l'écriture des contrôleurs, une classe utilitaire permet d'instancier rapidement les réponses.

public class FabriqueReponse {
    public static <T> ReponseApi<T> succes(T donnees) {
        return new ReponseApi<>(0, donnees);
    }

    public static ReponseApi erreur(CodeStatut codeStatut) {
        return new ReponseApi(codeStatut);
    }

    public static ReponseApi erreur(int code, String message, String description) {
        return new ReponseApi(code, null, message, description);
    }
}

4. Constantes de sécurité

public interface ConstantesSecurite {
    String SECRET_SALT = "sel_secret_jwt_v1";
}

5. Gestion des exceptions métier

public class ExceptionOperationnelle extends RuntimeException {
    private final int code;
    private final String description;

    public ExceptionOperationnelle(String message, int code, String description) {
        super(message);
        this.code = code;
        this.description = description;
    }

    public ExceptionOperationnelle(CodeStatut codeStatut) {
        super(codeStatut.getMessage());
        this.code = codeStatut.getCode();
        this.description = codeStatut.getDescription();
    }
    
    public int getCode() {
        return code;
    }
    
    public String getDescription() {
        return description;
    }
}

6. Gestionnaire global des exceptions

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
@Slf4j
public class GestionnaireExceptionsGlobal {

    @ExceptionHandler(ExceptionOperationnelle.class)
    public ReponseApi<?> gererExceptionMetier(ExceptionOperationnelle e) {
        log.error("Erreur métier : " + e.getMessage(), e);
        return FabriqueReponse.erreur(e.getCode(), e.getMessage(), e.getDescription());
    }

    @ExceptionHandler(RuntimeException.class)
    public ReponseApi<?> gererExceptionRuntime(RuntimeException e) {
        log.error("Erreur inattendue : ", e);
        return FabriqueReponse.erreur(CodeStatut.ERREUR_SYSTEME.getCode(), e.getMessage());
    }
}

7. Intégration de la documentation API (Swagger)

Ajout de la dépendance Maven et configuration pour OpenAPI.

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.3.0</version>
</dependency>

import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
@Profile({"dev", "test"})
public class ConfigurationApi {

    @Bean
    public OpenAPI apiPublique() {
        return new OpenAPI()
                .info(new Info()
                        .title("API Plateforme Juridique")
                        .description("Interface backend pour les services juridiques")
                        .version("1.0"))
                .externalDocs(new ExternalDocumentation()
                        .description("Documentation Swagger")
                        .url("/swagger-ui.html"));
    }
}

Développement de l'API

Service d'inscription

La logique d'inscription inclut la validation des entrées, la vérification de l'unicité de l'identifiant et le hachage du mot de passe.

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

@Service
public class ServiceUtilisateurImpl extends ServiceImpl<MappeurUtilisateur, Utilisateur> implements ServiceUtilisateur {

    @Override
    public long enregistrerUtilisateur(String identifiant, String motDePasse, String confirmationMdp) {
        // 1. Validation de base
        if (!StringUtils.hasText(identifiant) || !StringUtils.hasText(motDePasse)) {
            throw new ExceptionOperationnelle("Paramètres manquants", CodeStatut.PARAMETRE_INVALIDE.getCode(), "");
        }
        if (identifiant.length() < 5) {
             throw new ExceptionOperationnelle("L'identifiant doit comporter au moins 5 caractères", CodeStatut.PARAMETRE_INVALIDE.getCode(), "");
        }
        if (motDePasse.length() < 8) {
             throw new ExceptionOperationnelle("Le mot de passe est trop court", CodeStatut.PARAMETRE_INVALIDE.getCode(), "");
        }

        // 2. Vérification de l'unicité
        QueryWrapper<Utilisateur> wrapper = new QueryWrapper<>();
        wrapper.eq("identifiant", identifiant);
        long compte = this.baseMapper.selectCount(wrapper);
        if (compte > 0) {
            throw new ExceptionOperationnelle("Cet identifiant est déjà pris", CodeStatut.PARAMETRE_INVALIDE.getCode(), "");
        }

        // 3. Validation des caractères interdits
        String motifRegEx = "[`~!@#$%^&*()+=|{}':;',\\[\\].<>/?~!@#¥%……&*()——+|{}【】‘;:""’。,、?]";
        if (java.util.regex.Pattern.compile(motifRegEx).matcher(identifiant).find()) {
            throw new ExceptionOperationnelle("Caractères spéciaux interdits dans l'identifiant", CodeStatut.PARAMETRE_INVALIDE.getCode(), "");
        }

        // 4. Correspondance des mots de passe
        if (!motDePasse.equals(confirmationMdp)) {
            throw new ExceptionOperationnelle("Les mots de passe ne correspondent pas", CodeStatut.PARAMETRE_INVALIDE.getCode(), "");
        }

        // 5. Chiffrement du mot de passe
        String mdpChiffre = DigestUtils.md5Hex((ConstantesSecurite.SECRET_SALT + motDePasse).getBytes());

        // 6. Persistance
        Utilisateur nouvelUtilisateur = new Utilisateur();
        nouvelUtilisateur.setIdentifiant(identifiant);
        nouvelUtilisateur.setMotDePasse(mdpChiffre);
        
        boolean resultatSauvegarde = this.save(nouvelUtilisateur);
        if (!resultatSauvegarde) {
            throw new ExceptionOperationnelle("Échec de l'enregistrement", CodeStatut.ERREUR_DB.getCode(), "");
        }
        
        return nouvelUtilisateur.getId();
    }
}

Contrôleur d'inscripsion

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/utilisateur")
public class ControleurUtilisateur {

    @Autowired
    private ServiceUtilisateur serviceUtilisateur;

    @PostMapping("/inscription")
    public ReponseApi<Long> inscription(
            @RequestParam("identifiant") String identifiant,
            @RequestParam("motDePasse") String motDePasse,
            @RequestParam("confirmationMdp") String confirmationMdp) {
        
        if (!StringUtils.hasText(identifiant) || !StringUtils.hasText(motDePasse)) {
            return FabriqueReponse.erreur(CodeStatut.DONNEE_MANQUANTE);
        }
        
        long idUtilisateur = serviceUtilisateur.enregistrerUtilisateur(identifiant, motDePasse, confirmationMdp);
        return FabriqueReponse.succes(idUtilisateur);
    }
}

Résolution de problèmes techniques

Compatibilité MyBatis-Plus avec Spring Boot 3

Problème rencontré : Erreur de démarrage indiquant que le Bean MappeurUtilisateur n'a pas pu être défini ("Consider defining a bean...").

Solution : Avec Spring Boot 3, il est impératif d'utliiser le starter spécifique mybatis-plus-spring-boot3-starter au lieu de l'ancien artefact.

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
    <version>3.5.5</version>
</dependency>

Gestion des exceptions de persistance

Problème : Une MyBatisSystemException générique était levée lors des accès base de données, rendant le débogage difficile.

Soltuion : Vérifier la configuration de l'URL JDBC dans application.yml. Pour isoler la cause, un bloc try-catch spécifique a été ajouté autour des requêtes pour logger la pile d'exécution complète.

try {
    QueryWrapper<Utilisateur> wrapper = new QueryWrapper<>();
    wrapper.eq("identifiant", identifiant);
    long count = baseMapper.selectCount(wrapper);
} catch (org.apache.ibatis.exceptions.PersistenceException e) {
    e.printStackTrace();
    // Log l'erreur détaillée pour identifier le problème de configuration ou de requête
}

Publié le 2 septembre à 08h43