SSM アーキテクチャを採用した病院入院業務管理システムの実装

システム概要と背景

現代社会においてインターネット技術は急速に発展し、各種産業への浸透が加速しています。医療分野においても、情報のデジタル化は不可欠な課題となっています。従来の紙ベースや局所的なデータ管理では、入院記録の誤入力が頻発し、効率的な統計分析が困難であり、プライバシー保護の観点からもリスクが存在しました。これらの課題を解決するため、統合された情報管理プラットフォームの導入が求められています。

本プロジェクトは、Spring、SpringMVC、MyBatis(通称 SSM)の組み合わせを基盤とした Web アプリケーションです。これにより、管理者、医師、看護師といった異なる役割を持つユーザーに対して、階層化されたアクセス制御と専用の操作インターフェースを提供します。データの一元管理により、コスト削減と業務処理の高速化を実現します。

技術スタック

開発環境および採用技術は以下の通り設定されています。

  • バックエンド言語: Java (JDK 1.8 以上)
  • フレームワーク: Spring Framework, SpringMVC, MyBatis
  • フロントエンド: JSP, jQuery, Bootstrap
  • データ永続化: MySQL 5.7+
  • ビルドツール: Apache Maven
  • Web サーバー: Apache Tomcat 9.x
  • IDE: IntelliJ IDEA / Eclipse

機能モジュール詳細

システムはロールベースの権限制御を導入しており、各担当者には必要な最小権限のみが付与されます。

1. 管理者機能

システム全体の運用を担当するアカウントです。

  • 職員管理: 医療スタッフ(医師・看護師)の登録、変更、削除を行います。
  • 薬品在庫: 医薬品の価格更新や画像データのメンテナンスが可能です。
  • 統計ダッシュボード: 入院患者の消費金額集計レポートを視覚的に確認できます。
  • 設備管理: 病床の空き状況と割り当てを管理します。

2. 医師機能

治療計画および患者ケアの中核となります。

  • 患者登録: 入院手続きを行い、疾患種別や担当医情報を記録します。
  • 診療指示: 処方箋や検査オーダー(医嘱)の発行・変更を行います。
  • カルテ更新: 患者の症状経過や家族連絡先を更新できます。

3. 看護師機能

日々の療養サポートと事務処理を担当します。

  • 会計処理: 入院中の消耗品費や処置料などの収支入力を行います。
  • 指示確認: 医師から出された治療命令を確認し、タスクを遂行します。
  • 退院確認: 患者の入院期間や退院状況を照会します。

コア機能の実装サンプル

ここでは、セキュリティに関する重要な部分であるリクエスト認証とユーザーセッション管理のコード構造を示します。

アクセス制御フィルターの設計

CORS ポリシーの適用と、API トークンの検証を行うフィルタークラスです。すべてのリクエストに対して実行され、認証情報が正当か判定します。

package jp.med.core.security;

import com.med.service.AuthTokenService;
import com.med.utils.ResponseHandler;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.Map;

/**
 * リクエスト間の権限チェックを実装するインタセプター
 */
@Component
public class SessionValidationFilter extends HandlerInterceptorAdapter {

    private static final String AUTHORIZATION_HEADER = "X-API-KEY";
    
    @Autowired
    private AuthTokenService tokenProvider;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        
        // Cross-Origin Resource Sharing のヘッダー設定
        setCorsHeaders(response, request);

        // 認証スキップが必要なエンドポイントのチェック
        if (handler instanceof HandlerMethod) {
            HandlerMethod method = (HandlerMethod) handler;
            if (method.hasMethodAnnotation(AuthorizedIgnore.class)) {
                return true;
            }
        } else {
            return true;
        }

        // ヘッダーからのトークン抽出
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        
        if (StringUtils.isEmpty(bearerToken)) {
            sendUnauthorizedResponse(response, "認証トークンが見つかりません");
            return false;
        }

        // トークン有効性の確認とセッション属性への設定
        Map<String, Object> userInfo = tokenProvider.validate(bearerToken);
        
        if (userInfo == null) {
            sendUnauthorizedResponse(response, "トークンが無効または期限切れです");
            return false;
        }

        request.setAttribute("CURRENT_USER_ID", userInfo.get("uid"));
        request.setAttribute("CURRENT_ROLE", userInfo.get("role"));
        request.setAttribute("DB_TABLE_NAME", userInfo.get("source_table"));

        return true;
    }

    private void setCorsHeaders(HttpServletResponse resp, HttpServletRequest req) {
        resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
        resp.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        resp.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-KEY");
        resp.setHeader("Access-Control-Max-Age", "3600");
    }

    private void sendUnauthorizedResponse(HttpServletResponse resp, String msg) throws Exception {
        resp.setContentType("application/json;charset=UTF-8");
        PrintWriter writer = resp.getWriter();
        writer.write(ResponseHandler.error(401, msg));
        writer.flush();
        writer.close();
    }
}

認証コントローラーの実装

ユーザーのログイン、ログアウト、および基本情報取得を行うコントローラーです。RESTful なレスポンス形式を採用しています。

package jp.med.controller.api;

import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.med.entity.AccountInfo;
import com.med.service.AccountService;
import com.med.service.AuthTokenService;
import com.med.utils.MD5Encrypt;
import com.med.utils.PageResult;
import com.med.utils.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

/**
 * ユーザー認証およびセッション管理 API
 */
@RestController
@RequestMapping("/v1/accounts")
public class AccountSessionController {

    @Autowired
    private AccountService accountRepository;
    
    @Autowired
    private AuthTokenService tokenManager;

    /**
     * ログイン処理
     */
    @PostMapping("/signin")
    public Result executeLogin(@RequestBody Map<String, String> payload) {
        String userId = payload.get("username");
        String rawPass = payload.get("password");
        
        EntityWrapper<AccountInfo> query = new EntityWrapper<>();
        query.eq("username", userId);
        
        AccountInfo foundUser = accountRepository.selectOne(query);
        
        if (foundUser == null || !foundUser.getPassword().equals(rawPass)) {
            return Result.fail("認証エラー: ID またはパスワードが一致しません");
        }

        // JWT トークン発行
        String sessionToken = tokenManager.create(foundUser.getId(), foundUser.getUsername(), foundUser.getRole());
        return Result.success(Map.of("access_token", sessionToken));
    }

    /**
     * 新しいユーザー登録
     */
    @PostMapping("/signup")
    public Result registerNewUser(@RequestBody AccountInfo newAccount) {
        EntityWrapper<AccountInfo> check = new EntityWrapper<>();
        check.eq("username", newAccount.getUsername());
        
        if (accountRepository.count(check) > 0) {
            return Result.fail("重複エラー: このユーザー名は既に使用されています");
        }
        
        accountRepository.insert(newAccount);
        return Result.success();
    }

    /**
     * セッション終了(ログアウト)
     */
    @GetMapping("/signout")
    public Result terminateSession(HttpServletRequest currentRequest) {
        currentRequest.getSession().invalidate();
        return Result.success("正常にログアウトしました");
    }

    /**
     * パスワード初期化
     */
    @PostMapping("/reset-password")
    public Result resetCredentials(@RequestParam String username) {
        EntityWrapper<AccountInfo> target = new EntityWrapper<>();
        target.eq("username", username);
        
        AccountInfo user = accountRepository.selectOne(target);
        if (user == null) {
            return Result.fail("該当するアカウントは見つかりませんでした");
        }
        
        user.setPassword("default123"); // デフォルト値へ設定
        accountRepository.updateById(user);
        return Result.success("仮パスワードが発行されました: default123");
    }

    /**
     * 条件付きユーザーリスト取得(ページング対応)
     */
    @GetMapping("/list")
    public Result searchUsers(@RequestParam Map<String, Object> filters) {
        PageResult pageData = accountRepository.findPagedPage(filters);
        return Result.success(pageData);
    }
    
    /**
     * 特定ユーザーの情報を取得
     */
    @GetMapping("/{id}")
    public Result fetchProfile(@PathVariable String id) {
        AccountInfo data = accountRepository.selectById(id);
        return Result.success(data);
    }

    /**
     * バッチ削除処理
     */
    @DeleteMapping("/batch")
    public Result removeBatch(@RequestBody List<String> ids) {
        accountService.deleteBatchIds(ids);
        return Result.success();
    }
}

タグ: Java SpringMVC MyBatis HospitalManagementSystem REST API

5月31日 20:21 投稿