Spring BootとVue.jsを使用した学生インターンシップ・就職管理システム:WeChatミニプログラムによるBSモデル実装

システム概要

本システムは、WeChatミニプログラムをベースとした学生のインターンシップと就職管理を目的としたBS(Browser/Server)モデルのアプリケーションです。Spring Bootをバックエンドに、Vue.jsをフロントエンドに採用し、効率的なデータ管理とユーザーエクスペリエンスを実現しています。

技術スタック

バックエンドフレームワーク:Spring Boot

Spring BootはSpring Frameworkを基盤とした開発フレームワークで、多くの利点を持っています。Tomcat、Jetty、Undertowなどのサーバーが内蔵されており、追加のインストールや設定なしで直接使用できます。最も重要な特徴は、強力な自動設定機能です。プロジェクトの依存関係に基づいてアプリケーションを自動設定し、開発プロセスを大幅に簡素化します。また、Spring Data、Spring Security、Spring Cloudなどの豊富なすぐに使える機能とプラグインを提供し、開発者がより迅速にアプリケーションを構築し、他の技術を容易に拡張・統合できるようにします。

以下に核心的なコード例を示します:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class ApplicationInitializer {

    public static void main(String[] args) {
        SpringApplication.run(ApplicationInitializer.class, args);
    }

    @GetMapping("/greeting")
    public String displayGreeting() {
        return "ようこそ、システムへ!";
    }
}

このコードはSpring Bootアプリケーションのエントリーポイントクラス`ApplicationInitializer`を定義しています。`@SpringBootApplication`アノテーションでSpring Bootアプリケーションとしてマークし、`@RestController`アノテーションでRESTfulコントローラーとしてマークしています。

コントローラー内では、`displayGreeting`メソッドを定義し、`@GetMapping`アノテーションで"/greeting"パスにマッピングしています。このパスにアクセスすると、メソッドは「ようこそ、システムへ!」という文字列を返します。

フロントエンドフレームワーク:Vue.js

Vue.jsは人気のあるJavaScriptフレームワークで、多くの利点を持っています。その中でも、Vue.jsの核心的な利点の一つは仮想DOM技術です。仮想DOMはメモリ内のデータ構造であり、効率的なDOM操作の実現に重要な役割を果たします。

Vue.jsは応答性の高いデータバインディング、仮想DOM、コンポーネント化などの現代的な技術を採用し、開発者に柔軟で効率的、保守しやすい開発モデルを提供します。データが変更されると、Vue.jsはUIを自動的に更新し、開発者は手動でUIを更新する必要がなく、データ処理に専念できます。

以下にVue.jsの核心機能を示すサンプルコードを示します:

<!DOCTYPE html>
<html>
<head>
  <title>Vue.js デモ</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
</head>
<body>
  <div id="app">
    <h2>{{ message }}</h2>
    <button v-on:click="updateMessage">メッセージ変更</button>
  </div>

  <script>
    var app = new Vue({
      el: '#app',
      data: {
        message: 'こんにちは、Vue.js!'
      },
      methods: {
        updateMessage: function() {
          this.message = 'Vue.jsは素晴らしい!';
        }
      }
    });
  </script>
</body>
</html>

この例では、Vueインスタンスを作成し、ページ上の要素(idが"app")にバインドしています。`data`プロパティを通じて、`message`という名前の変数を定義し、その初期値を「こんにちは、Vue.js!」に設定しています。ページ上では、二重中括弧構文(`{{ message }}`)を使用して`message`の値を表示しています。`methods`プロパティを通じて、`updateMessage`という名前のメソッドを定義し、ボタンがクリックされるとこのメソッドが`message`の値を変更します。Vue.jsの応答性の高いデータバインディングメカニズムにより、`message`の値が変更されると、ページに表示される内容も自動的に更新されます。

永続化フレームワーク:MyBatis

MyBatisはオープンソースの永続化フレームワークで、開発者がデータベース操作の作成と管理を簡素化するのに役立ちます。MyBatisの核心的な考え方はSQLステートメントとJavaコードを分離し、XMLまたはアノテーションを通じてデータベース操作を記述することで、データアクセス層の分離と柔軟性を実現することです。

MyBatisの主な利点は以下の通りです:

  1. データベース操作の簡素化:MyBatisは強力なSQLマッピング機能を提供し、Javaオブジェクトとデータベーステーブルをマッピングできるため、開発者は煩雑なSQLステートメントを手動で作成する必要がなく、データベース操作の作成と保守を大幅に簡素化します。
  2. 柔軟なSQL制御:MyBatisは動的SQLをサポートし、異なる条件やロジックに基づいてSQLステートメントを動的に生成できるため、クエリ、更新などの操作がより柔軟で制御可能になります。
  3. キャッシュサポート:MyBatisは一次キャッシュと二次キャッシュのサポートを提供し、データベースへのアクセス回数を効果的に削減し、システムパフォーマンスを向上させます。
  4. 高い拡張性:MyBatisはプラグインメカニズムを採用しており、独自の機能を容易に拡張・カスタマイズでき、さまざまなビジネスニーズに対応できます。

システムテスト

システムテストの目的

管理システムの開発ライフサイクルにおいて、システムテストは極めて重要な段階です。これはシステムの品質と信頼性を確保する最後の関門であり、開発プロセス全体の最終チェックでもあります。

システムテストの主な目的は、ユーザーが使用中に問題に遭遇することを防ぎ、ユーザーエクスペリエンスを向上させることです。複数の角度と視点から出発し、システムが遭遇する可能性のある問題を考慮し、さまざまなシナリオをシミュレートすることで欠陥を発見し、問題を解決します。テストプロセスを通じて、システムの品質状況を評価し、システム機能が完全であるか、ロジックがスムーズであるかを確認することもできます。成功したシステムテストは、システムの品質とユーザーエクスペリエンスを大幅に向上させます。

システム機能テスト

システム機能テストは、システムの機能モジュールに対して行われるテストプロセスです。クリック、境界値の入力、必須項目と非必須項目の検証などの方法によるブラックボックステストを実施します。テストケースを作成し、テストケースに基づいてテストを実行し、テスト結論を導き出します。

ログイン機能を例に取ると、本システムはログイン機能テストを行います。ユーザーがシステムにログインする必要がある場合、本システムはアカウントパスワードなどの機能ポイントを通じて検証します。ユーザーが入力する際は、データベースに保存されたデータと一致する必要があります。いずれかの入力が誤っている場合、システムは入力エラーを示します。このインターフェースでは役割権限も検証され、ユーザーが管理者役割を選択してログインすると、システムはエラーを報告します。以下はログイン機能のテストケース例です:

入力データ 期待される結果 実際の結果 結果分析
ユーザー名:admin パスワード:123456 検証コード:正しい入力 システムにログイン システムへのログインに成功 結果一致
ユーザー名:admin パスワード:111111 検証コード:正しい入力 パスワードエラー パスワードが間違っています。再入力してください 結果一致
ユーザー名:admin パスワード:123456 検証コード:誤った入力 検証コードエラー 検証コード情報が間違っています 結果一致
ユーザー名:空 パスワード:123456 検証コード:正しい入力 ユーザー名必須 ユーザー名を入力してください 結果一致
ユーザー名:admin パスワード:空 検証コード:正しい入力 パスワードエラー パスワードが間違っています。再入力してください 結果一致

また、本システムではユーザー管理機能のテストも行っています。ユーザー管理には、ユーザーの追加、編集、削除、検索などの機能が含まれます。本システムでは、これらの各機能のテストケースをテストしました:

1. ユーザー追加機能テストケース:

入力データ 期待される結果 実際の結果 結果分析
ユーザー名:user1 パスワード:123456 役割:一般ユーザー 追加成功し、ユーザーリストに表示 ユーザーリストにuser1が表示される 結果一致
ユーザー名:user2 パスワード:111111 役割:一般ユーザー 追加成功し、ユーザーリストに表示 ユーザーリストにuser2が表示される 結果一致
ユーザー名:user1 パスワード:123456 役割:一般ユーザー 追加失敗、ユーザー名が既に存在することを示す 追加失敗、ユーザー名が既に存在することを示す 結果一致
ユーザー名:空 パスワード:123456 役割:一般ユーザー 追加失敗、ユーザー名を空にできないことを示す 追加失敗、ユーザー名を空にできないことを示す 結果一致

実装コード例

認証処理の実装

// 権限検証をスキップするアノテーション
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BypassAuth {
}

// ログインコントローラー
@RestController
@RequestMapping("/api/auth")
public class AuthenticationController {

    @Autowired
    private UserService userService;
    
    @Autowired
    private TokenManager tokenManager;
    
    @BypassAuth
    @PostMapping("/authenticate")
    public ResponseEntity<?> authenticate(@RequestBody LoginRequest loginRequest) {
        // ユーザー情報を検索
        UserEntity user = userService.findByUsername(loginRequest.getUsername());
        
        // ユーザーが存在しないかパスワードが正しくないかを確認
        if(user == null || !user.getPassword().equals(encryptPassword(loginRequest.getPassword()))) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                    .body(new ApiResponse("アカウント名またはパスワードが正しくありません"));
        }
        
        // トークンを生成
        String token = tokenManager.createToken(user.getId(), user.getUsername(), user.getRole());
        
        return ResponseEntity.ok(new AuthResponse(token));
    }
}

// トークン管理サービス
@Service
public class TokenManager {
    
    @Autowired
    private TokenRepository tokenRepository;
    
    public String createToken(Long userId, String username, String role) {
        // 既存のトークンを確認
        TokenEntity existingToken = tokenRepository.findByUserIdAndRole(userId, role);
        
        // ランダムなトークン文字列を生成
        String tokenValue = generateRandomToken(32);
        
        // トークンの有効期限を1時間後に設定
        Date expirationTime = calculateExpirationTime(1);
        
        if(existingToken != null) {
            // トークン情報を更新
            existingToken.setTokenValue(tokenValue);
            existingToken.setExpirationTime(expirationTime);
            tokenRepository.save(existingToken);
        } else {
            // 新しいトークンレコードを作成
            TokenEntity newToken = new TokenEntity(userId, username, "users", role, tokenValue, expirationTime);
            tokenRepository.save(newToken);
        }
        
        return tokenValue;
    }
    
    private Date calculateExpirationTime(int hours) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(new Date());
        calendar.add(Calendar.HOUR_OF_DAY, hours);
        return calendar.getTime();
    }
    
    private String generateRandomToken(int length) {
        // ランダムな文字列を生成するロジック
        return UUID.randomUUID().toString().replace("-", "").substring(0, length);
    }
}

// 認証インターセプター
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {

    public static final String AUTH_TOKEN_HEADER = "X-Auth-Token";
    
    @Autowired
    private TokenManager tokenManager;
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // CORS対応
        setupCorsHeaders(response, request);
        
        // OPTIONSリクエストの処理
        if(request.getMethod().equals(HttpMethod.OPTIONS.name())) {
            response.setStatus(HttpStatus.OK.value());
            return false;
        }
        
        // ハンドラーメソッドのアノテーションを取得
        BypassAuth bypassAuth = getBypassAuthAnnotation(handler);
        
        // 認証が不要なメソッドはそのまま通過
        if(bypassAuth != null) {
            return true;
        }
        
        // ヘッダーからトークンを取得
        String token = request.getHeader(AUTH_TOKEN_HEADER);
        
        // トークンの検証
        if(StringUtils.isEmpty(token)) {
            sendUnauthorizedResponse(response);
            return false;
        }
        
        TokenEntity tokenEntity = tokenManager.validateToken(token);
        if(tokenEntity == null) {
            sendUnauthorizedResponse(response);
            return false;
        }
        
        // ユーザー情報をセッションに保存
        request.getSession().setAttribute("userId", tokenEntity.getUserId());
        request.getSession().setAttribute("role", tokenEntity.getRole());
        request.getSession().setAttribute("tableName", tokenEntity.getTableName());
        request.getSession().setAttribute("username", tokenEntity.getUsername());
        
        return true;
    }
    
    private void setupCorsHeaders(HttpServletResponse response, HttpServletRequest request) {
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Headers", "x-requested-with,request-source,X-Auth-Token, Origin,imgType, Content-Type, cache-control,postman-token,Cookie, Accept,authorization");
        response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
    }
    
    private BypassAuth getBypassAuthAnnotation(Object handler) {
        if(handler instanceof HandlerMethod) {
            return ((HandlerMethod) handler).getMethodAnnotation(BypassAuth.class);
        }
        return null;
    }
    
    private void sendUnauthorizedResponse(HttpServletResponse response) {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        
        try(PrintWriter writer = response.getWriter()) {
            writer.write(new ObjectMapper().writeValueAsString(
                new ApiResponse(401, "ログインしてください")));
        } catch (IOException e) {
            // エラーハンドリング
        }
    }
}

データベース設計例

以下に、学生情報を管理するためのデータベーステーブルの設計例を示します:

-- ----------------------------
-- 学生情報テーブルの構造
-- ----------------------------
DROP TABLE IF EXISTS `student_info`;
CREATE TABLE `student_info` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主キー',
  `student_id` varchar(20) NOT NULL COMMENT '学生ID',
  `name` varchar(50) NOT NULL COMMENT '氏名',
  `department` varchar(100) NOT NULL COMMENT '所属学科',
  `email` varchar(100) DEFAULT NULL COMMENT 'メールアドレス',
  `phone` varchar(20) DEFAULT NULL COMMENT '電話番号',
  `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT 'ステータス(0:在学中, 1:卒業, 2:休学)',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '作成時間',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_student_id` (`student_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='学生情報テーブル';

この学生情報テーブルには以下のフィールドが含まれます:

  • `id`:主キー、自動増分
  • `student_id`:学生ID、一意の識別子
  • `name`:学生の氏名
  • `department`:所属学科
  • `email`:メールアドレス
  • `phone`:電話番号
  • `status`:学生のステータス(0:在学中, 1:卒業, 2:休学)
  • `create_time`:レコード作成時間
  • `update_time`:レコード最終更新時間

学生情報テーブルへのデータ挿入例:

INSERT INTO `student_info` (`student_id`, `name`, `department`, `email`, `phone`, `status`)
VALUES ('S2023001', '田中太郎', '情報工学', 'tanaka@example.com', '090-1234-5678', 0);

INSERT INTO `student_info` (`student_id`, `name`, `department`, `email`, `phone`, `status`)
VALUES ('S2023002', '佐藤花子', '経営学', 'sato@example.com', '080-9876-5432', 0);

INSERT INTO `student_info` (`student_id`, `name`, `department`, `email`, `phone`, `status`)
VALUES ('S2022001', '鈴木一郎', '機械工学', 'suzuki@example.com', '070-1111-2222', 1);

タグ: SpringBoot vue.js WeChatミニプログラム MyBatis 学生管理システム

7月26日 02:09 投稿