WeChatミニプログラムを活用したオンライン注文システムの技術実装

技術概要

本稿では、WeChatミニプログラムを基盤としたオンライン注文システムの技術実装について解説します。このシステムは、モバイルユーザーに便利な食品注文体験を提供するためのソリューションです。

使用技術スタック

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

Spring Bootは、Javaベースのアプリケーション開発を簡素化する強力なフレームワークです。組み込みのTomcatサーバーを備えており、追加のインストールや設定なしで直接利用できます。自動設定機能により、プロジェクトの依存関係に基づいてアプリケーションを自動的に構成します。

以下に基本的なSpring Bootアプリケーションの例を示します:

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 RestaurantApplication {

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

    @GetMapping("/api/health")
    public String checkHealth() {
        return "システムは正常に稼働しています";
    }
}

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

コントローラ内では、checkHealthメソッドを定義し、@GetMappingアノテーションで"/api/health"パスにマッピングしています。このパスにアクセスすると、"システムは正常に稼働しています"という文字列が返されます。

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

Vue.jsは、モダンなJavaScriptフレームワークの一つです。仮想DOM技術を核心に据え、効率的なDOM操作を実現します。応答型データバインディング、コンポーネントベースのアーキテクチャを提供し、開発者がデータとUIの関係を簡単に管理できるようにします。

Vue.jsの基本的な使用例を以下に示します:

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Vue.js 注文システムデモ</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
</head>
<body>
  <div id="orderApp">
    <h2>{{ restaurantName }} - 注文フォーム</h2>
    <div>
      <label>メニューを選択してください:</label>
      <select v-model="selectedMenu">
        <option v-for="menu in menuItems" :value="menu.id">{{ menu.name }} - ¥{{ menu.price }}</option>
      </select>
    </div>
    <div v-if="selectedMenu">
      <p>選択されたメニュー: {{ selectedMenuName }}</p>
      <p>価格: ¥{{ selectedMenuPrice }}</p>
      <div>
        <label>数量:</label>
        <input type="number" v-model.number="quantity" min="1" max="10">
      </div>
      <button @click="addToCart">カートに追加</button>
    </div>
    <div v-if="cart.length > 0">
      <h3>カート内容</h3>
      - {{ item.name }} - ¥{{ item.price }} × {{ item.quantity }}
      <p>合計: ¥{{ totalPrice }}</p>
    </div>
  </div>

  <script>
    new Vue({
      el: '#orderApp',
      data: {
        restaurantName: '美味しいレストラン',
        menuItems: [
          { id: 1, name: '定食A', price: 1200 },
          { id: 2, name: '定食B', price: 1500 },
          { id: 3, name: 'セットC', price: 1800 },
          { id: 4, name: 'ドリンク', price: 500 }
        ],
        selectedMenu: null,
        quantity: 1,
        cart: []
      },
      computed: {
        selectedMenuName() {
          const menu = this.menuItems.find(item => item.id === this.selectedMenu);
          return menu ? menu.name : '';
        },
        selectedMenuPrice() {
          const menu = this.menuItems.find(item => item.id === this.selectedMenu);
          return menu ? menu.price : 0;
        },
        totalPrice() {
          return this.cart.reduce((total, item) => total + (item.price * item.quantity), 0);
        }
      },
      methods: {
        addToCart() {
          if (this.selectedMenu && this.quantity > 0) {
            const menuItem = this.menuItems.find(item => item.id === this.selectedMenu);
            if (menuItem) {
              // カートに同じ商品が既にあるか確認
              const existingItem = this.cart.find(item => item.id === this.selectedMenu);
              if (existingItem) {
                existingItem.quantity += this.quantity;
              } else {
                this.cart.push({
                  id: menuItem.id,
                  name: menuItem.name,
                  price: menuItem.price,
                  quantity: this.quantity
                });
              }
              this.quantity = 1;
              this.selectedMenu = null;
            }
          }
        }
      }
    });
  </script>
</body>
</html>

この例では、Vue.jsを使用して注文フォームを実装しています。メニューの選択、数量の指定、カートへの追加機能を提供しています。Vue.jsの応答型データバインディングにより、UIが自動的に更新されます。

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

MyBatisは、データベース操作を簡素化するオープンソースの永続化フレームワークです。SQL文とJavaコードを分離することで、データアクセスレイヤーの分離と柔軟性を実現します。

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

  1. データベース操作の簡素化:強力なSQLマッピング機能により、Javaオブジェクトとデータベーステーブルのマッピングが容易になります。
  2. 柔軟なSQL制御:動的SQLをサポートし、条件やロジックに基づいてSQL文を動的に生成できます。
  3. キャッシュサポート:一次キャッシュと二次キャッシュをサポートし、データベースアクセスを減少させます。
  4. 高い拡張性:プラグイン機構を採用しており、カスタム機能の拡張や要件に応じた調整が容易です。

システムテスト

テストの目的

システムテストは、開発サイクルにおいて重要な段階です。システムの品質と信頼性を確保するための最終的なチェックポイントであり、開発プロセス全体の最終確認となります。

テストの主な目的は、ユーザーが使用中に問題に遭遇することを避け、ユーザーエクスペリエンスを向上させることです。多角的な視点からシステムが抱える可能性のある問題を考慮し、さまざまなシナリオをシミュレートして欠陥を発見し、解決します。

機能テスト

機能テストは、システムの各機能モジュールをテストするプロセスです。以下にログイン機能のテスト例を示します:

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

テストの結論

本システムは主にブラックボックステストを採用し、ユーザーがシステムを使用して各機能を実装するためのテストケースを作成し、テストを実行しました。システムのフローの正確性を確保します。システムテストは不可欠であり、システムをより完成度の高いものにし、使用可能性を高めます。

本システムのテストは、システムの機能モジュールが当初の設計理念を満たしているかどうかを検証し、各機能モジュールのロジックが正しいかどうかを検証することを目的としています。このシステムは、ユーザーが操作しやすいように複雑なロジック処理を必要としません。テストの最終目的もユーザーの使用を中心に展開されます。

認証と認可の実装例

以下に、認証とトークン生成のJavaコード例を示します:

// 認証を無視するアノテーション
@SkipAuth
@PostMapping("/api/auth/login")
public ResponseEntity<ApiResponse<UserSession>> authenticateUser(
    @RequestParam String username, 
    @RequestParam String password, 
    @RequestParam String captcha,
    HttpServletRequest request) {
    
    // ユーザー情報をクエリ
    UserEntity user = userService.findByUsername(username);
    
    // ユーザーの存在とパスワードの正誤を確認
    if(user == null || !PasswordEncoder.matches(password, user.getPassword())) {
        return ResponseEntity.badRequest()
            .body(new ApiResponse<>("error", "ユーザー名またはパスワードが正しくありません"));
    }
    
    // セッションを作成
    UserSession session = new UserSession();
    session.setUserId(user.getId());
    session.setUsername(user.getUsername());
    session.setRole(user.getRole());
    
    // JWTトークンを生成
    String jwtToken = jwtService.generateToken(session);
    session.setToken(jwtToken);
    
    // セッションを保存
    sessionService.createSession(session);
    
    return ResponseEntity.ok()
        .body(new ApiResponse<>("success", "認証に成功しました", session));
}

// JWTトークンを生成
@Override
public String generateToken(UserSession session) {
    // トークンの有効期限を設定(1時間)
    Date expiration = new Date(System.currentTimeMillis() + 3600000);
    
    // JWTトークンを構築
    JwtBuilder builder = Jwts.builder()
        .setSubject(session.getUsername())
        .claim("userId", session.getUserId())
        .claim("role", session.getRole())
        .setExpiration(expiration)
        .signWith(SignatureAlgorithm.HS512, secretKey);
    
    return builder.compact();
}

/**
 * 認証(トークン)検証インターセプター
 */
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {

    // リクエストヘッダーでのトークンのキー名を定義
    public static final String AUTH_TOKEN_HEADER = "Authorization";
    
    @Autowired
    private SessionService sessionService;
    
    @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, Content-Type, cache-control");
        response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
        
        // クロスドメイン時には最初にOPTIONSリクエストが送信されるため、正常なステータスを返す
        if (request.getMethod().equals(RequestMethod.OPTIONS.name())) {
            response.setStatus(HttpStatus.OK.value());
            return false;
        }
        
        // ハンドラーメソッドのアノテーションを取得
        SkipAuth skipAuthAnnotation;
        if (handler instanceof HandlerMethod) {
            skipAuthAnnotation = ((HandlerMethod) handler).getMethodAnnotation(SkipAuth.class);
        } else {
            return true;
        }
        
        // ヘッダーからトークンを取得
        String token = request.getHeader(AUTH_TOKEN_HEADER);
        
        /**
         * 認証を必要としないメソッドはそのまま通過
         */
        if(skipAuthAnnotation != null) {
            return true;
        }
        
        // トークンに基づいてセッション情報を取得
        UserSession session = null;
        if(StringUtils.isNotBlank(token)) {
            session = sessionService.getSessionByToken(token);
        }
        
        if(session != null && !sessionService.isTokenExpired(token)) {
            // ユーザー情報をリクエスト属性に保存
            request.setAttribute("currentUser", session);
            return true;
        }
        
        // 認証に失敗した場合、401エラーとメッセージを返す
        response.setContentType("application/json;charset=UTF-8");
        response.setCharacterEncoding("UTF-8");
        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.getWriter().write("{\"status\":\"error\",\"message\":\"認証が必要です\"}");
        return false;
    }
}

このJavaコードは、認証機能の実装に関連しています。主なコンポーネントは以下の通りです:

  1. @SkipAuthアノテーション:認証を必要としないメソッドを識別するためのカスタムアノテーションです。
  2. authenticateUserメソッド:ユーザー認証を行い、JWTトークンを生成して返却します。
  3. generateTokenメソッド:JWTトークンを生成します。ユーザー情報を含み、有効期限を設定します。
  4. AuthenticationInterceptorクラス:リクエストの前に実行されるインターセプタで、認証を検証します。

データベース設計例

注文システムにおける商品テーブルのSQLスキーマ例を以下に示します:

-- 商品テーブルの構造
DROP TABLE IF EXISTS `menu_items`;
CREATE TABLE `menu_items` (
  `item_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主キー',
  `item_name` varchar(100) NOT NULL COMMENT '商品名',
  `category_id` bigint(20) NOT NULL COMMENT 'カテゴリID',
  `price` decimal(10, 2) NOT NULL COMMENT '価格',
  `description` varchar(500) DEFAULT NULL COMMENT '商品説明',
  `available_stock` int(11) NOT NULL DEFAULT '0' COMMENT '在庫数',
  `is_available` tinyint(1) NOT NULL DEFAULT '1' COMMENT '販売可能状態',
  `image_url` varchar(255) DEFAULT NULL COMMENT '画像URL',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '作成日時',
  `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新日時',
  PRIMARY KEY (`item_id`),
  KEY `idx_category` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品テーブル';

-- 注文テーブルの構造
DROP TABLE IF EXISTS `orders`;
CREATE TABLE `orders` (
  `order_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '注文ID',
  `user_id` bigint(20) NOT NULL COMMENT 'ユーザーID',
  `order_number` varchar(50) NOT NULL COMMENT '注文番号',
  `total_amount` decimal(10, 2) NOT NULL COMMENT '合計金額',
  `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '注文状態(0:未処理, 1:準備中, 2:完了, 3:キャンセル)',
  `order_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '注文日時',
  `delivery_address` varchar(255) NOT NULL COMMENT '配送先',
  `contact_phone` varchar(20) NOT NULL COMMENT '連絡先電話番号',
  `notes` text DEFAULT NULL COMMENT '注文メモ',
  PRIMARY KEY (`order_id`),
  UNIQUE KEY `uk_order_number` (`order_number`),
  KEY `idx_user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='注文テーブル';

-- 注明明細テーブルの構造
DROP TABLE IF EXISTS `order_items`;
CREATE TABLE `order_items` (
  `item_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '明細ID',
  `order_id` bigint(20) NOT NULL COMMENT '注文ID',
  `menu_item_id` bigint(20) NOT NULL COMMENT '商品ID',
  `quantity` int(11) NOT NULL COMMENT '数量',
  `unit_price` decimal(10, 2) NOT NULL COMMENT '単価',
  `subtotal` decimal(10, 2) NOT NULL COMMENT '小計',
  PRIMARY KEY (`item_id`),
  KEY `idx_order` (`order_id`),
  KEY `idx_menu_item` (`menu_item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='注文明細テーブル';

これらのテーブルは、注文システムの核となるデータ構造です。商品テーブルはメニュー項目を格納し、注文テーブルは顧客の注文情報を管理し、注文明細テーブルは各注文に含まれる商品の詳細を記録します。

商品テーブルへのデータ挿入例:

-- 商品データの挿入
INSERT INTO `menu_items` (`item_name`, `category_id`, `price`, `description`, `available_stock`, `image_url`) 
VALUES ('定食A', 1, 1200.00, 'ご飯、味噌汁、サラダ、主菜(焼き魚)のセット', 50, '/images/set_a.jpg');

INSERT INTO `menu_items` (`item_name`, `category_id`, `price`, `description`, `available_stock`, `image_url`) 
VALUES ('定食B', 1, 1500.00, 'ご飯、味噌汁、サラダ、主菜(ステーキ)のセット', 30, '/images/set_b.jpg');

INSERT INTO `menu_items` (`item_name`, `category_id`, `price`, `description`, `available_stock`, `image_url`) 
VALUES ('お寿司盛り合わせ', 2, 2500.00, '10種類のお寿司の盛り合わせ', 20, '/images/sushi_platter.jpg');

INSERT INTO `menu_items` (`item_name`, `category_id`, `price`, `description`, `available_stock`, `image_url`) 
VALUES ('ソフトドリンク', 3, 500.00, 'コーラ、オレンジジュース、レモネードの中から選択', 100, '/images/drinks.jpg');

タグ: WeChatミニプログラム Spring Boot vue.js MyBatis オンライン注文システム

7月13日 16:32 投稿