JavaとVue.jsを用いたコンサートチケット販売システムの技術解説

システム概要

本システムは、JavaベックエンドとVue.jsフロントエンドを組み合わせたコンサートチケット販売プラットフォームです。Spring Bootフレームワークをバックエンドとして使用し、UniAppを介してモバイルアプリケーションを提供します。MyBatisをデータアクセス層として利用し、効率的なデータベース操作を実現しています。

アーキテクチャ構成

バックエンド:Spring Boot

Spring Bootは、開発者が迅速に生産レベルのSpringアプリケーションを構築できるフレームワークです。デフォルト設定の提供により、設定ファイルの使用を最小限に抑え、開発者がアプリケーションのロジックに集中できる環境を提供します。内蔵サーバー(Tomcat、Jettyなど)の自動設定機能により、Webアプリケーションのデプロイプロセスが簡素化されます。

フロントエンド:Vue.js

Vue.jsは、仮想DOM技術を核とするモダンなJavaScriptフレームワークです。データバインディング、コンポーネントベースの開発、リアクティブなUI更新を特徴とし、開発者に効率的で保守性の高い開発体験を提供します。データ変更時にUIが自動的に更新されるため、開発者はビジネスロジックの実装に集中できます。

データアクセス層:MyBatis

MyBatisは、SQL文とJavaコードを分離することでデータベース操作を簡素化するORMフレームワークです。XMLまたはアノテーションを通じてデータベース操作を定義し、柔軟なSQL制御を実現します。動的SQLサポート、キャッシュ機能、プラグイン機構により、様々なビジネス要件に対応可能です。

認証・認可システムの実装

システムのセキュリティを確保するため、トークンベースの認証機構を実装しています。以下に主要なコード例を示します。


// 認証不要のアノテーション
@SkipAuthentication
@PostMapping(value = "/authenticate")
public Response authenticate(String userId, String password, String captcha, HttpServletRequest request) {
   // ユーザー情報の検索
   UserEntity user = userRepo.findOne(new Specification<UserEntity>().eq("userId", userId));
   // ユーザーの存在確認とパスワード検証
   if(user == null || !user.getPassword().equals(password)) {
      return Response.error("ユーザーIDまたはパスワードが正しくありません");
   }
   // トークンの生成
   String token = authService.generateToken(user.getId(), userId, "users", user.getRole());
   return Response.success().put("token", token);
}

// トークン生成処理
@Override
public String generateToken(Long userId, String userIdStr, String tableName, String role) {
   // 既存トークンの確認
   TokenEntity existingToken = this.findOne(new Specification<TokenEntity>()
      .eq("userId", userId).eq("role", role));
   
   // ランダムなトークン文字列の生成
   String newToken = TokenUtils.generateRandomToken(32);
   
   // トークンの有効期限を1時間後に設定
   Calendar calendar = Calendar.getInstance();   
   calendar.setTime(new Date());   
   calendar.add(Calendar.HOUR_OF_DAY, 1);
   
   if(existingToken != null) {
      // トークン情報の更新
      existingToken.setToken(newToken);
      existingToken.setExpirationTime(calendar.getTime());
      this.update(existingToken);
   } else {
      // 新規トークンレコードの作成
      this.insert(new TokenEntity(userId, userIdStr, tableName, role, newToken, calendar.getTime()));
   }
   return newToken;
}

/**
 * トークン認証インターセプター
 */
@Component
public class AuthInterceptor implements HandlerInterceptor {

    // リクエストヘッダーにおけるトークンのキー名
    public static final String AUTH_TOKEN_KEY = "Authorization";

    @Autowired
    private TokenService tokenService;
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // クロスオリジンリクエストのサポート
        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,Authorization, Origin,imgType, Content-Type, cache-control");
        response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));

        // プリフライトリクエストの処理
        if (request.getMethod().equals(RequestMethod.OPTIONS.name())) {
            response.setStatus(HttpStatus.OK.value());
            return false;
        }
        
        // ハンドラメソッドのアノテーション取得
        SkipAuthentication skipAuth;
        if (handler instanceof HandlerMethod) {
            skipAuth = ((HandlerMethod) handler).getMethodAnnotation(SkipAuthentication.class);
        } else {
            return true;
        }

        // ヘッダーからトークンを取得
        String token = request.getHeader(AUTH_TOKEN_KEY);
        
        /**
         * 認証不要のメソッドはそのまま通過
         */
        if(skipAuth != null) {
            return true;
        }
        
        // トークンに基づくトークンエンティティの取得
        TokenEntity tokenEntity = null;
        if(StringUtils.isNotBlank(token)) {
            tokenEntity = tokenService.validateToken(token);
        }
        
        if(tokenEntity != null) {
            // ユーザー情報をセッションに保存
            request.getSession().setAttribute("currentUserId", tokenEntity.getUserId());
            request.getSession().setAttribute("userRole", tokenEntity.getRole());
            request.getSession().setAttribute("userTable", tokenEntity.getTableName());
            request.getSession().setAttribute("userName", tokenEntity.getUserName());
            return true;
        }
        
        // 認証失敗時の処理
        PrintWriter writer = null;
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        try {
            writer = response.getWriter();
            writer.print(JSONObject.toJSONString(Response.error(401, "ログインが必要です")));
        } finally {
            if(writer != null){
                writer.close();
            }
        }
        return false;
    }
}

データベース設計

システムの主要なデータベーステーブル構造を以下に示します。


DROP TABLE IF EXISTS `shipping_address`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `shipping_address` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主キー',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '作成日時',
  `user_id` bigint(20) NOT NULL COMMENT 'ユーザーID',
  `address_detail` varchar(200) NOT NULL COMMENT '住所詳細',
  `recipient_name` varchar(200) NOT NULL COMMENT '受取人名',
  `phone_number` varchar(200) NOT NULL COMMENT '電話番号',
  `is_default` varchar(200) NOT NULL COMMENT 'デフォルト住所[はい/いいえ]',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8 COMMENT '配送先住所';

DROP TABLE IF EXISTS `discussion_forum`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `discussion_forum` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主キー',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '作成日時',
  `post_title` varchar(200) DEFAULT NULL COMMENT '投稿タイトル',
  `post_content` longtext NOT NULL COMMENT '投稿内容',
  `parent_post_id` bigint(20) DEFAULT NULL COMMENT '親投稿ID',
  `user_id` bigint(20) NOT NULL COMMENT 'ユーザーID',
  `user_name` varchar(200) DEFAULT NULL COMMENT 'ユーザー名',
  `avatar_url` longtext COMMENT 'アバターURL',
  `post_status` varchar(200) DEFAULT NULL COMMENT 'ステータス',
  `is_pinned` int(11) DEFAULT '0' COMMENT '固定表示フラグ',
  `pinned_time` datetime DEFAULT NULL COMMENT '固定表示時刻',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=79 DEFAULT CHARSET=utf8 COMMENT '交流フォーラム';

DROP TABLE IF EXISTS `customer`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `customer` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主キー',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '作成日時',
  `account_id` varchar(200) NOT NULL COMMENT 'アカウントID',
  `password` varchar(200) NOT NULL COMMENT 'パスワード',
  `full_name` varchar(200) NOT NULL COMMENT '氏名',
  `gender` varchar(200) DEFAULT NULL COMMENT '性別',
  `mobile_phone` varchar(200) DEFAULT NULL COMMENT '携帯電話番号',
  `profile_image` longtext COMMENT 'プロフィール画像',
  `points` double DEFAULT '0' COMMENT 'ポイント',
  `balance` double DEFAULT '0' COMMENT '残高',
  PRIMARY KEY (`id`),
  UNIQUE KEY `account_id` (`account_id`)
) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8 COMMENT '顧客';

サンプルデータ挿入


INSERT INTO `concert` (`name`, `price`, `description`, `available_seats`)
VALUES ('J-POPコンサート2024', 15000.00, '人気アーティットによる最新ライブ', 500);

INSERT INTO `concert` (`name`, `price`, `description`, `available_seats`)
VALUES ('ロックフェスティバル', 12000.00, '多ジャンルのアーティストによる野外フェス', 1000);

INSERT INTO `concert` (`name`, `price`, `description`, `available_seats`)
VALUES ('ジャズナイト', 8000.00, '大人の雰囲気を味わうジャズコンサート', 200);

テストケース

システムの主要機能に対するテストケースを以下に示します。

入力データ 期待結果 実際の結果 結果分析
ユーザー名:admin パスワード:123456 キャプチャ:正しい入力 システムへのログイン システムへの正常なログイン 結果が一致
ユーザー名:admin パスワード:111111 キャプチャ:正しい入力 パスワードエラー パスワードエラー、再入力を求められる 結果が一致
ユーザー名:admin パスワード:123456 キャプチャ:誤った入力 キャプチャエラー キャプチャ情報が正しくない 結果が一致

チケット購入機能のテストケース

シナリオ 操作手順 期待結果 実際の結果
在庫があるチケットの購入 1. ログイン
2. チケット詳細ページへ移動
3. 購入ボタンをクリック
4. 支払い情報を入力
購入成功、チケットが予約される 購入成功、チケット予約が完了
在庫がないチケットの購入 1. ログイン
2. 売り切れチケットの詳細ページへ移動
3. 購入ボタンをクリック
エラーメッセージが表示される 「在庫切れ」のエラーメッセージが表示される
複数枚のチケット購入 1. ログイン
2. チケット詳細ページへ移動
3. 数量を4に変更
4. 購入ボタンをクリック
4枚のチケットが購入される 4枚のチケットが正常に購入される

タグ: Java SpringBoot vue.js UniApp MyBatis

7月31日 22:43 投稿