システム概要
本システムは、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枚のチケットが正常に購入される |