セント レジス テイクアウト プロジェクトの詳細な分析ノートとすべての機能補足コード

目次

プロジェクト分析の概要

#2022 年末に、学習プロジェクトの実践体験とメモを記録します。
これはセント レジス テイクアウト プロジェクトです。ビデオ内で定義されていない関数を追加し、関数実装のロジックに関するいくつかのメモを記録します。あくまで学習用の参考であり、私のコードは標準化されていない可能性があります。, 間違って書いていて気付かなかった可能性もありますが、自分で関数をテストする分には問題ありません; 読んでいただきありがとうございます、修正してくださいご質問がございましたら、私に連絡してください。不足があれば追加してください


テクノロジースタック

関連するテクノロジーは、Spring、Springboot、Mybatis-plus、MySQL、Redis、Linux、Git、Spring Cache、Sharding-JDBC、Nginx、Swagger です。(Apifox のこれらのツールはテクノロジーとみなされるべきではないため、使用されているツールはリストされません)


プロジェクト紹介

本プロジェクトは、店舗側に料理のパッケージ管理サービスを提供するバックグラウンド管理端末と、ユーザーに注文機能を提供するモバイル端末の2つの側面から開発されたテイクアウト注文システムです。食器。最後に、git でプロジェクトを管理し、nginx でフロントエンドをデプロイし、tomcat でバックエンドをデプロイし、mysql マスター/スレーブ レプリケーションを使用し、ライブラリから読み取り、メイン ライブラリに書き込み、シェル スクリプトでサーバーにデプロイします。


プロジェクトのソースコード

プロジェクトコードクラウドアドレス: https://gitee.com/dkgk8/reggie-git


1. 構造物の構築

1. プロジェクト構造を初期化する

新しい springboot プロジェクトの
pom import 座標を作成します

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>

		<dependency>
			<groupId>com.github.xiaoymin</groupId>
			<artifactId>knife4j-spring-boot-starter</artifactId>
			<version>3.0.2</version>
		</dependency>

<!--		<dependency>-->
<!--			<groupId>org.apache.shardingsphere</groupId>-->
<!--			<artifactId>sharding-jdbc-spring-boot-starter</artifactId>-->
<!--			<version>4.1.1</version>-->
<!--		</dependency>-->

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-cache</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
			<scope>compile</scope>
		</dependency>

		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.4.2</version>
		</dependency>

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.20</version>
		</dependency>

		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.76</version>
		</dependency>

		<dependency>
			<groupId>commons-lang</groupId>
			<artifactId>commons-lang</artifactId>
			<version>2.6</version>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>

		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid-spring-boot-starter</artifactId>
			<version>1.1.23</version>
		</dependency>

		<dependency>
			<groupId>com.aliyun</groupId>
			<artifactId>aliyun-java-sdk-core</artifactId>
			<version>4.5.16</version>
		</dependency>
		<dependency>
			<groupId>com.aliyun</groupId>
			<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
			<version>2.1.0</version>
		</dependency>

yml設定ファイルに追加された情報

server:
  port: 8080
spring:
#  application:
#    name: reggie_take_out
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/reggie?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: 123456
  redis:
    host: localhost
    port: 6379
    database: 0
  cache:
    redis:
      time-to-live: 1800000  #ms ->30min

mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: ASSIGN_ID
reggie:
  path: D:\SpringBoot_Reggie\reggie_take_out\src\main\resources\static\front\hello\

プロジェクトは後でサーバー上で実行するので、マルチ環境開発を使用しました。ローカルで実行する場合は、この手順を気にする必要はありません。プロジェクトの一般的な構造は次のとおりです
ここに画像の説明を挿入

ここに画像の説明を挿入

mybatis-plusを使ってみると、
エンティティクラス→マッパー→サービス→serviceImpl→コントローラーという流れで
プログラムを書いている気がします。

2. データベーステーブル構造の設計

ここに画像の説明を挿入
ここに画像の説明を挿入
すべてのテーブルが示されているわけではありません。ここでは典型的な従業員テーブルを示します。
ここに画像の説明を挿入

3. プロジェクトの基本構成情報を追加する

フロントエンド リソースのインポート
デフォルト ページとフロント ページの場合、これら 2 つをリソース ディレクトリに直接ドラッグして直接アクセスすると、mvc フレームワークによってインターセプトされるため、アクセスできません。静的ディレクトリですが、フロントエンド ページに直接アクセスすることはまだできないため、ここで静的を直接解放することもできるため、
これらのリソースを解放するためのマッピング クラスを記述する必要があります
WebMvcConfig クラス
ここに画像の説明を挿入

パブリックフィールドの自動入力

ここに画像の説明を挿入

これについては別の記事で詳しく書きました。リンク:パブリックフィールドを自動的に入力する

グローバル例外処理クラス

try-catch を使用して例外を処理することもできますが、コード量が多くなると、try-catch が多くなり、コードが簡潔でなく、読みにくくなります。そのため、グローバル例外処理を使用します。共通パッケージの
ここに画像の説明を挿入
カスタム例外クラス
ここに画像の説明を挿入

結果によってカプセル化されたエンティティ クラスを返します。

フロントエンドとバックエンドのデータ転送を容易にするために、データをオブジェクトの形式でカプセル化することがより適切です。

@Data
public class R<T> implements Serializable {
    
    

    private Integer code; //编码:1成功,0和其它数字为失败

    private String msg; //错误信息

    private T data; //数据

    private Map map = new HashMap(); //动态数据

    public static <T> R<T> success(T object) {
    
    
        R<T> r = new R<T>();
        r.data = object;
        r.code = 1;
        return r;
    }

    public static <T> R<T> error(String msg) {
    
    
        R r = new R();
        r.msg = msg;
        r.code = 0;
        return r;
    }

    public R<T> add(String key, Object value) {
    
    
        this.map.put(key, value);
        return this;
    }

}

2. マネジメント事業の展開

1. スタッフマネジメント関連事業

1.1 スタッフログイン

ログインロジックは次のとおりです
ここに画像の説明を挿入

    @PostMapping("/login")
    public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee){
    
    
        //1.将页面提交的明文密码进行md5加密
        String password = employee.getPassword();
        password = DigestUtils.md5DigestAsHex(password.getBytes());
        //2.根据页面提交的用户名username查数据库
        LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Employee::getUsername,employee.getUsername());
        Employee emp = employeeService.getOne(queryWrapper);
        //3.如果没有查询到则返回登录失败结果
        if (emp == null){
    
    
            return R.error("登录失败");
        }
        //4.密码比对,如果不一致则返回登录失败结果
        if (!emp.getPassword().equals(password)){
    
    
            return R.error("登录失败");
        }
        //5.查看员工账号状态是否锁定,若是禁用状态返回禁用信息
        if (emp.getStatus() == 0){
    
    
            return R.error("账号异常,已锁定");
        }
        //6.登录成功,将员工id存入Session  并返回登录成功结果
        request.getSession().setAttribute("employee",emp.getId());
        return R.success(emp);
    }

1.2 従業員の退職

従業員がログインするときにセッションに保存されている従業員 ID をクリアします。

    @PostMapping("/logout")
    public R<String> logout(HttpServletRequest request){
    
    
        //1.清理Session中保存的当前登录员工id
        request.getSession().removeAttribute("employee");
        return R.success("退出成功");
    }

1.3 フィルタの遮断

現在はフィルターがありません。ユーザーはログインする必要がありません。URL+リソース名を通じて気軽にアクセスできます。そのため、フィルターを追加して、ログインがない場合はリクエストをインターセプトし、アクセスを与えず、自動的にログイン ページ フィルタ処理ロジックが
スタートアップクラスに追加されます
ここに画像の説明を挿入
アノテーション @ServletComponentScan
フィルタ設定クラス アノテーション @WebFilter(filterName = "インターセプタ クラス名の最初の小文字", urlPartten = "/* などのインターセプトされるパス")

ユーザーがログインしたかどうかを判断するには、以前のセッションにemployeeという名前のオブジェクトがあり、ユーザーIDがそこに保存されているため、getAttributeを使用してセッションで取得したデータがnullかどうかを確認するだけでわかります。彼がログイン状態であるかどうか

ここで言っておきますが
、Springコアパッケージの文字列マッチングクラスのオブジェクトを呼び出し、パスをマッチングさせて比較結果を返し、
等しい場合はtrueとなります。

public static Final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

ここに画像の説明を挿入
コード上で直接

/**
 * 检查用户是否登录的过滤器
 */

@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
    
    
    //路径匹配器,支持通配符
    public static final AntPathMatcher PATH_MATCHER =new AntPathMatcher();

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    
    

        HttpServletRequest request=(HttpServletRequest) servletRequest;
        HttpServletResponse response=(HttpServletResponse) servletResponse;

        //1.获取本次请求uri
        String requestURI = request.getRequestURI();
        //定义不需要处理的请求路径
        String[] urls=new String[]{
    
    
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**",
                "/common/**",
                "/user/sendMsg",
                "/user/login",
                "/doc.html",
                "/webjars/**",
                "/swagger-resources",
                "/v2/api-docs"
        };
        //2.判断本次请求是否需要处理
        boolean check = check(urls, requestURI);
        //3.如果不需要处理则直接放行
        if (check){
    
    
            filterChain.doFilter(request,response);
            return;
        }
        //4-1.判断登录状态,如果已经登录,则直接放行
        if (request.getSession().getAttribute("employee")!=null){
    
    
            Long empId = (Long) request.getSession().getAttribute("employee");
            BaseContext.setCurrentId(empId);
            filterChain.doFilter(request,response);
            return;
        }
        //4-2.判断移动端登录状态,如果已经登录,则直接放行
        if (request.getSession().getAttribute("user")!=null){
    
    
            Long userId = (Long) request.getSession().getAttribute("user");
            BaseContext.setCurrentId(userId);
            filterChain.doFilter(request,response);
            return;
        }
        //5如果未登录则,通过输出流方式向客户端页面响应数据
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        return;

    }

    /**
     * 路径匹配,检查本次请求是否需要放行
     */
    public boolean check(String[] urls,String requestURI){
    
    
        //遍历的同时调用PATH_MATCHER来对路径进行匹配
        for (String url : urls){
    
    
            boolean match = PATH_MATCHER.match(url,requestURI);
            if (match){
    
    
                //匹配到了可以放行的路径,直接放行
                return true;
            }
        }
        return false;
    }
}

1.4 従業員情報の変更

従業員ステータスの変更
ここに画像の説明を挿入

問題が発生しました。スノーフレーク アルゴリズムによれば、データベース ID は 19 桁であり、js は Long データを処理するときに精度が失われ、最初の 16 桁しか保証できません。 解決策: サーバーが JSON データでページに応答するときに、Long データを変換します
。 data to String は
ここに画像の説明を挿入
ここに画像の説明を挿入
Long 型 ID を String 型データに変換します

/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
 * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
 */
public class JacksonObjectMapper extends ObjectMapper {
    
    

    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    public JacksonObjectMapper() {
    
    
        super();
        //收到未知属性时不报异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

        //反序列化时,属性不存在的兼容处理
        this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);


        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))

                .addSerializer(BigInteger.class, ToStringSerializer.instance)
                .addSerializer(Long.class, ToStringSerializer.instance)
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}

MVC 構成クラスのメッセージ コンバーターを拡張する

    /**
     * 扩展mvc框架的消息转换器
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    
    
        //创建消息转换器对象
        MappingJackson2HttpMessageConverter messageConverter=new MappingJackson2HttpMessageConverter();
        //设置对象转换器,底层使用Jackson将Java对象转为json
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        //将上面的消息转换器对象追加到mvc框架的转换器集合中
        converters.add(0,messageConverter);
    }

従業員情報の変更
変更ロジックはデータエコーとデータストレージに分かれており、
データエコーは渡された従業員IDに基づいて従業員情報をクエリし、従業員オブジェクトを返すものです。

    /**
     * 回显用户信息到修改框
     */
    @GetMapping("/{id}")
    public R<Employee> getById(@PathVariable Long id){
    
    
        Employee employee = employeeService.getById(id);
        if (employee!=null){
    
    

            return R.success(employee);
        }else {
    
    
            return R.error("没查到该员工");
        }
    }

データ保存とは、変更されたデータを従業員テーブルに更新することです

    /**
     * 修改员工信息
     */
    @PutMapping
    public R<String> update(@RequestBody Employee employee){
    
    
        log.info(employee.toString());

        employeeService.updateById(employee);
        return R.success("员工信息修改成功");
    }

試験結果
ここに画像の説明を挿入

1.5 従業員情報のページングクエリ

ページング クエリ、一般的
ページング クエリのビジネス ロジック
ここに画像の説明を挿入
ブラウザによって送信されるURL
ここに画像の説明を挿入
ページング プラグイン構成クラス
まず MP ページング プラグイン構成クラスを取得し、構成パッケージの下に MybatisPlusConfig クラスを作成します

/**
 * 配置MP的分页插件
 */
@Configuration
public class MybatisPlusConfig {
    
    

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
    
    
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

ここに画像の説明を挿入
ページオブジェクト内

ここに画像の説明を挿入

上位コード

    /**
     * 员工信息的分页查询
     */
    @GetMapping("/page")
    public R<Page> page(int page, int pageSize, String name){
    
    
        log.info("page={},pageSize={},name={}",page,pageSize,name);

        //构造分页构造器
        Page pageInfo = new Page(page,pageSize);
        //构造条件构造器
        LambdaQueryWrapper<Employee> queryWrapper=new LambdaQueryWrapper();
        //添加过滤条件
        queryWrapper.like(StringUtils.isNotBlank(name),Employee::getName,name);
        //添加排序条件
        queryWrapper.orderByDesc(Employee::getUpdateTime);

        //执行查询
        employeeService.page(pageInfo,queryWrapper);
        return R.success(pageInfo);
    }

1.6 新入社員

フロントエンドによって渡されるデータ。ここでは従業員オブジェクトを使用してすべてのデータを受信できます。
リクエスト URL: http://localhost:9001/employee (POST リクエスト)
ここに画像の説明を挿入

基本的に、これは mp によってカプセル化された CRUD です。save メソッドを直接呼び出すだけです。ここで Employee エンティティ クラスを変更する必要はありません。ID の追加には、一般的な ID スノーフレーク自己インクリメント アルゴリズムが使用されます。以下は、最初に yml で設定済みであるためです。下図に示すように、ID の追加にはスノーフレークの自己インクリメント アルゴリズムが使用されます。もちろん、2 つの方法のいずれかを選択することもできます。
ここに画像の説明を挿入
ここに画像の説明を挿入

    /**
     * 新增员工
     */
    @PostMapping
    public R<String> save(@RequestBody Employee employee){
    
    
        log.info("新增员工,员工信息:{}",employee.toString());
        //设置初始密码123456,需要进行md5加密处理
        employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));

        employeeService.save(employee);
        return R.success("新增员工成功");
    }

2. 機密管理関連業務

2.1 分類のページネーションクエリ

ここに画像の説明を挿入
まだその数歩、古いことわざ

1. ページ コンストラクターを作成します。 Page pageInfo = new Page(page, pageSize);
2. 条件付きフィルターがある場合は、条件付きフィルター LambaQueryWarpper を追加します。
3. 挿入されたサービス オブジェクト (MP BaseMapper インターフェイスを継承している) は、Page オブジェクトを呼び出します。
4. サービス object.page (ページング情報、条件フィルター) は
結果を返します

    @GetMapping("/page")
    public R<Page> page(int page,int pageSize){
    
    
        //分页构造器
        Page<Category> pageInfo = new Page<>(page,pageSize);
        //条件构造器
        LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
        //添加排序条件,根据sort进行排序
        queryWrapper.orderByAsc(Category::getSort);
        //进行分页查询
        categoryService.page(pageInfo,queryWrapper);
        return R.success(pageInfo);
    }

2.2 新しいカテゴリー

ここに画像の説明を挿入
フロントエンドから送信されたリクエストに従い、送信されたデータを受信し、mpでパッケージ化されたcrudを呼び出し、データベースのテーブルにデータを挿入するだけなので言うことはありません。

    @PostMapping
    public R<String> save(@RequestBody Category category){
    
    
        log.info("category:{}",category);
        categoryService.save(category);
        return R.success("新增分类成功");
    }

2.3 料理または定食の分類変更

変更は古いルーチンです。最初にデータをエコーし​​、次にデータを変更します。
ここに画像の説明を挿入
フロント エンドによって送信されるリクエストは
ここに画像の説明を挿入
2 段階のプロセスであり、mp カプセル化を呼び出す方法です。

2.4 料理やパッケージのカテゴリ削除

ここに画像の説明を挿入
改善するには、現在の料理カテゴリに料理がある場合、削除することはできません
。削除する前に判断する必要があります。現在のカテゴリに料理がある場合は、例外をスローする必要があります。
例外情報を返すカテゴリがないため、プロンプトを表示します。ここでは、
にグローバル例外ハンドラーを作成しており、例外をインターセプトする必要があるため、それを使用する必要があるため、特に例外情報を返すカスタム クラス CustomerException を作成します。一律に扱う
ここに画像の説明を挿入

    /**
     * 根据id删除分类,删除之前需要进行判断是否由关联
     * @param id
     */
    @Override
    public void remove(Long id) {
    
    
        LambdaQueryWrapper<Dish> dishLambdaQueryWrapper=new LambdaQueryWrapper<>();
        //添加查询条件,根据分类id进行查询
        dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
        int count1 = dishService.count(dishLambdaQueryWrapper);
        //查询当前分类是否关联了菜品,如果已经关联,抛出业务异常
        if (count1>0){
    
    
            //已关联菜品,抛出一个业务异常
            throw new CustomException("当前分类下关联了菜品,不能删除");
        }

        //查询当前分类是否关联了套餐,如果已经关联,抛出业务异常
        LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper=new LambdaQueryWrapper<>();
        setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
        int count2 = setmealService.count(setmealLambdaQueryWrapper);
        if (count2>0){
    
    
            //已关联套餐,抛出一个业务异常
            throw new CustomException("当前分类下关联了套餐,不能删除");
        }
        //正常删除分类
        super.removeById(id);
    }

3. 食器管理関連事業

3.1 ページングクエリ

このページごとのクエリは決まり文句ではなく、料理テーブルのページごとのクエリである場合、最後のページごとのクエリの料理カテゴリの列が空白であることがわかります。フロントエンドが必要とする料理カテゴリ名のデータが料理テーブルのページ単位のクエリデータに存在しないため、DishDtoのページングクエリと分類IDに応じた条件付きクエリを使用する必要があります。食器テーブルの
ここに画像の説明を挿入

料理テーブルを開くと、料理カテゴリ ID フィールドのみがあり、料理名がないことがわかります。
ここに画像の説明を挿入

DishDtoクラスを作成する
ここに画像の説明を挿入

ここに画像の説明を挿入

これは古典的な Dto ページネーション クエリであり、上記のコードです。

    /**
     * 菜品管理的分页查询
     */
    @GetMapping("/page")
    public R<Page> page(int page, int pageSize, String name){
    
    
        //构造分页构造器对象
        Page<Dish> pageInfo = new Page<>(page,pageSize);
        Page<DishDto> dishDtoPage = new Page<>();
        //条件构造器
        LambdaQueryWrapper<Dish> queryWrapper=new LambdaQueryWrapper<>();
        //添加过滤条件
        queryWrapper.like(name!=null,Dish::getName,name);
        //添加排序条件
        queryWrapper.orderByDesc(Dish::getUpdateTime);
        dishService.page(pageInfo,queryWrapper);

        BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
        List<Dish> records = pageInfo.getRecords();
        List<DishDto> list = records.stream().map((item)->{
    
    
            DishDto dishDto = new DishDto();
            BeanUtils.copyProperties(item,dishDto);
            Long categoryId = item.getCategoryId();
            Category category=categoryService.getById(categoryId);
            if(category!=null){
    
    
                String categoryName=category.getName();
                dishDto.setCategoryName(categoryName);
            }
            return dishDto;
        }).collect(Collectors.toList());
        dishDtoPage.setRecords(list);
        return R.success(dishDtoPage);
    }

3.2 画像のアップロードとダウンロード

ここに画像の説明を挿入

特定のストレージ パスは構成ファイルに書き込まれ、@Value を使用してビジネスに挿入できます。

ここに画像の説明を挿入
ここに画像の説明を挿入
ここに画像の説明を挿入

ここに画像の説明を挿入

このとき、写真をアップロードした後、写真は一時的な場所に保存されます。ブラウザを閉じると、写真ファイルは存在せず、再度閲覧することはできません。アップロードした写真をローカル ディスク ストレージにダウンロードする必要があるため、画像はブラウザ上でエコーできますが、
ここに画像の説明を挿入
フロントエンド表示コードにアクセスして画像リクエストを送信した場合にのみ画像が表示されます。

ここに画像の説明を挿入

ここに画像の説明を挿入

I/Oの入出力ストリームが使用される、これはレビューです

    /**
     * 文件下载,图片回显浏览器
     */
    @GetMapping("/download")
    public void download(String name, HttpServletResponse response){
    
    
        try {
    
    
            //输入流,通过输入流读取文件内容
            FileInputStream fileInputStream = new FileInputStream(new File(basePath+name));
            //输出流,通过输出流将文件写回浏览器,在浏览器展示图片
            ServletOutputStream outputStream = response.getOutputStream();

            response.setContentType("image/jpeg");//设置响应的文件类型
            int len = 0;
            byte[] bytes = new byte[1024];
            while ((len = fileInputStream.read(bytes)) != -1){
    
    //用while循环一直写,写到-1证明写完了
                outputStream.write(bytes,0,len);
                outputStream.flush();
            }
            //关闭资源
            outputStream.close();
            fileInputStream.close();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }

3.3 新しい料理

シナリオ記述
ここに画像の説明を挿入
開発ロジック
ここに画像の説明を挿入
ここに画像の説明を挿入
タイプ1は料理、タイプ2は定食
ここに画像の説明を挿入

    /**
     * 根据条件查询分类数据,返回到菜品管理的下拉框里去
     */
    @GetMapping("/list")
    public R<List<Category>> list(Category category){
    
    
        //条件构造器
        LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
        //添加条件 type为1是菜品,为2是套餐
        queryWrapper.eq(category.getType()!= null,Category::getType,category.getType());
        //添加排序条件
        queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);

        List<Category> list = categoryService.list(queryWrapper);
        return R.success(list);
    }

次のステップはマルチテーブルストレージです。mp は対応する API インターフェイスを提供していないため、自分で記述する必要があります。古いルーチン、サービスインターフェイスの宣言メソッド、実装クラスはビジネスを実現するのに最適です、そしてコントローラーは制御層が直接呼び出す

注: 複数のテーブル操作が同じ成功または失敗で崩壊するのを防ぐための制御のために、トランザクションもここに追加する必要があります。@Transactional トランザクションを開始します;
@EnableTransactionManagement は、トランザクション開始をサポートするためにスタートアップ クラスに追加されます
フロントエンド転送データによると、DishDto オブジェクトを使用して
2 つのテーブルに受信して格納できます。最初に基本情報をディッシュ テーブルに格納します次に、フレーバー情報をdish_flavorテーブルに格納します
。dish_flavorのテーブル構造に従って、dish_idもdish_flavorテーブルに格納する必要があります。
ここに画像の説明を挿入

    /**
     * 新增菜品,同时保存对应的口味数据
     * @param dishDto
     */
    @Override
    public void saveWithFlavor(DishDto dishDto) {
    
    
        //保存菜品的基本信息到菜品表dish中
        this.save(dishDto);

        Long dishId = dishDto.getId();
        //菜品口味
        List<DishFlavor> flavors = dishDto.getFlavors();
        flavors = flavors.stream().map((item)->{
    
    
            item.setDishId(dishId);//把dish_id存入dish_flavor表中
            return item;
        }).collect(Collectors.toList());
        //保存菜品口味数据到菜品表中去dish_flavor
        dishFlavorService.saveBatch(flavors);
    }

3.4 料理を変更する

ディッシュを変更する最初のステップはデータをエコーすることであり、2 番目のステップはデータを更新することです

ここでのエコーデータには複数テーブルの結合クエリが含まれており、まずフロントエンドから渡された料理IDに従って料理の基本情報をクエリし、その料理情報をdishDtoオブジェクトにコピーし、次にそれに応じてdish_flavorテーブルに対して条件付きクエリを実行します。料理IDに味を問い合わせ、その情報はdishDtoオブジェクトにもコピーされ、最終的にdishDtoが返されます。

データを更新すると、2 つのテーブルも個別に更新されます。最初に mp の updateById メソッドを呼び出して、dish テーブルのデータを更新します。次に、dish_flavor テーブルで、まず料理の下のフレーバー情報をクリアしてから、変更されたフレーバー情報をフロントエンドから渡されないフィールドデータについて、自分で設定する必要がある場合、dish_flavorテーブルを更新します。
ここに画像の説明を挿入

ここに画像の説明を挿入
エコーコード

    @Override
    public DishDto getByIdWithFlavor(Long id) {
    
    
        //查询菜品基本信息,从dish表查询
        Dish dish = this.getById(id);
        DishDto dishDto = new DishDto();
        //将菜品基本信息拷贝到dishDto中
        BeanUtils.copyProperties(dish,dishDto);
        //查询当前菜品对应的口味信息,从dish_flavor表查询
        LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(DishFlavor::getDishId,dish.getId());
        //将该菜品的口味信息查询出来存入list集合中
        List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);
        //set到dto的属性里
        dishDto.setFlavors(flavors);
        return dishDto;
    }

更新コード

    @Override
    @Transactional
    public void updateWithFlavor(DishDto dishDto) {
    
    

        //更新dish表的基本信息
        this.updateById(dishDto);
        //清理当前菜品对应口味数据---dish_flavor表的delete操作
        LambdaQueryWrapper<DishFlavor> queryWrapper=new LambdaQueryWrapper<>();
        queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());

        dishFlavorService.remove(queryWrapper);
        //添加当前提交过来的口味数据---dish_flavor表的insert操作
        List<DishFlavor> flavors = dishDto.getFlavors();

        flavors = flavors.stream().map((item)->{
    
    
            item.setId(IdWorker.getId());
            item.setDishId(dishDto.getId());
            return item;
        }).collect(Collectors.toList());

        dishFlavorService.saveBatch(flavors);//批量保存
    }

3.5 料理を削除する

フロントエンドがリクエストを送信する URL
ここに画像の説明を挿入

DishFlavorエンティティクラスにおいて、mybatis-plusが提供する論理削除であることを示すプライベートInteger isDeleted;フィールドに@TableLogicアノテーションを追加し、削除前に料理の販売状況を判断する必要があるため、removeメソッドここで抽出してサービス実装クラスを再度書きます

   /**
     *套餐批量删除和单个删除
     * @param ids
     */
    @Override
    @Transactional
    public void deleteByIds(List<Long> ids) {
    
    

        //构造条件查询器
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
        //先查询该菜品是否在售卖,如果是则抛出业务异常
        queryWrapper.in(ids!=null,Dish::getId,ids);
        List<Dish> list = this.list(queryWrapper);
        for (Dish dish : list) {
    
    
            Integer status = dish.getStatus();
            //如果不是在售卖,则可以删除
            if (status == 0){
    
    
                this.removeById(dish.getId());
            }else {
    
    
                //此时应该回滚,因为可能前面的删除了,但是后面的是正在售卖
                throw new CustomException("删除菜品中有正在售卖菜品,无法全部删除");
            }
        }

    }

コントローラーはサービス内のメソッドを直接呼び出すことができます

    /**
     * 套餐批量删除和单个删除
     * @return
     */
    @DeleteMapping
    public R<String> delete(@RequestParam("ids") List<Long> ids){
    
    
        //删除菜品  这里的删除是逻辑删除
        dishService.deleteByIds(ids);
        //删除菜品对应的口味  也是逻辑删除
        LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.in(DishFlavor::getDishId,ids);
        dishFlavorService.remove(queryWrapper);

        //清理所有菜品的缓存数据
        Set keys = redisTemplate.keys("dish_*");
        redisTemplate.delete(keys);
        return R.success("菜品删除成功");
    }

3.6 料理の休止と再開(補足)

フロントエンドから送信されるURL
ここに画像の説明を挿入

ビジネス ロジック
フロント エンドから渡された ID セットを使用して料理の状態をクエリし、クエリされたデータ セットを走査し、フロント エンドから渡されたステータスを各料理オブジェクトに直接設定して、料理ステータスの変更を完了します。

    /**
     * 对菜品批量或者是单个 进行停售或者是起售
     * @return
     */
    @PostMapping("/status/{status}")
//这个参数这里一定记得加注解才能获取到参数,否则这里非常容易出问题
    public R<String> status(@PathVariable("status") Integer status,@RequestParam List<Long> ids){
    
    
        //log.info("status:{}",status);
        //log.info("ids:{}",ids);
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper();
        queryWrapper.in(ids !=null,Dish::getId,ids);
        //根据传入的id集合进行批量查询
        List<Dish> list = dishService.list(queryWrapper);

        for (Dish dish : list) {
    
    
            if (dish != null){
    
    
                dish.setStatus(status);
                dishService.updateById(dish);
            }
        }
        //清理所有菜品的缓存数据
        Set keys = redisTemplate.keys("dish_*");
        redisTemplate.delete(keys);

        return R.success("售卖状态修改成功");
    }

4. パッケージ管理に関する業務

4.1 ページネーションクエリ

メニュー ページングと同様に、ページごとにパッケージ情報をクエリし、ストリーミングを通じてパッケージ情報を SetmealDto にコピーし、次にパッケージ ID に従ってパッケージ分類オブジェクトをクエリし、パッケージ分類情報を SetmealDto にコピーして、最後に dtoPage を返し、ページネーションを行います。料理の管理はほぼ同じなのでコードはありません

4.2 新しいパッケージ

ここに画像の説明を挿入

新たに追加された料理と同様に、フロントエンドから送信されたURLリクエストに応じて、ここでもマルチテーブル操作が行われます。setmealテーブルとsetmeal_dishテーブルは別々に操作され、フロントエンドから送信されたデータはsetmeal_dishに保存されますテーブルに設定することでetmeal_dishテーブルのデータが完成します。

4.3 パッケージの変更

古いルーチンは、setmeal テーブルと setmeal_dish テーブルのデータをクエリしてエコーし、その後 2 つのテーブルの内容をそれぞれ更新します。食器の皿の管理と同じルーチンなので
繰り返しませんが、どちらもサービス実装クラスに落とし込んで書きました。

    @Override
    public SetmealDto getByIdWithDishes(Long id) {
    
    
        //查询套餐基本信息,从setmeal表查询
        Setmeal setmeal = this.getById(id);
        SetmealDto setmealDto = new SetmealDto();
        BeanUtils.copyProperties(setmeal,setmealDto);
        //查询当前套餐对应的菜品信息,从setmeal_dish表查询
        LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SetmealDish::getSetmealId,setmeal.getId());
        List<SetmealDish> dishes = setmealDishService.list(queryWrapper);
        setmealDto.setSetmealDishes(dishes);
        return setmealDto;
    }

    @Override
    @Transactional
    public void updateWithDishes(SetmealDto setmealDto) {
    
    
        //更新setmeal表的基本信息
        this.updateById(setmealDto);
        //清理当前套餐对应的菜品数据---setmeal_dish表的delete操作
        LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SetmealDish::getSetmealId,setmealDto.getId());
        setmealDishService.remove(queryWrapper);
        //添加当前提交过来的菜品数据---setmeal_dish表的insert操作
        List<SetmealDish> dishes = setmealDto.getSetmealDishes();
        dishes = dishes.stream().map((item)->{
    
    
            item.setId(IdWorker.getId());
            item.setSetmealId(setmealDto.getId());
            return item;
        }).collect(Collectors.toList());
        setmealDishService.saveBatch(dishes);
    }

4.4 パッケージの削除

料理の削除と同様に、最初に定食の状態を判断する必要があり、削除する場合は、定食配下の関連関係も削除し、setmealとsetmeal_dishの2つのテーブルの処理が必要です

   /**
     * 删除套餐,同时删除套餐和菜品关联数据
     * @param ids
     */
    @Transactional
    @Override
    public void removeWithDish(List<Long> ids) {
    
    
        //select count(*) from setmeal where id in (1,2,3) and status = 1;
        //查询套餐状态,确定是否可以删除
        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.in(Setmeal::getId,ids);
        queryWrapper.eq(Setmeal::getStatus,1);

        int count = this.count(queryWrapper);
        if (count>0){
    
    
            //如果不能删除,抛出一个业务异常
            throw new CustomException("套餐正在售卖中,不能删除");
        }
        //如果可以删除,先删除套餐表中的数据——setmeal
        this.removeByIds(ids);

        //delete from setmeal_dish where setmeal_id in (1,2,3)
        LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
        lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);
        //删除关系表中的数据——setmeal_dish
    }

4.5 パッケージの一時停止と起動(補足)

フロントエンドによって送信される URL リクエストは、
ここに画像の説明を挿入
料理の開始と停止に似ています。

    /**
     * 批量起售停售
     */
    @PostMapping("/status/{status}")
    @CacheEvict(value = "setmealCache", allEntries = true)
    public R<String> status(@PathVariable("status") Integer status,@RequestParam List<Long> ids) {
    
    
        LambdaQueryWrapper<Setmeal> queryWrapper=new LambdaQueryWrapper<>();
        queryWrapper.in(ids!=null,Setmeal::getId,ids);
        List<Setmeal> setmeals = setmealService.list(queryWrapper);
        for (Setmeal setmeal : setmeals) {
    
    
            if (setmeal!=null){
    
    
                setmeal.setStatus(status);
                setmealService.updateById(setmeal);
            }
        }
        return R.success("售卖状态修改成功");
    }

5. 注文内容(補足)

バックグラウンド管理端末の注文明細から送信されるURLから、注文テーブルのページングクエリであると判断できる
ここに画像の説明を挿入

ここに画像の説明を挿入
ここに画像の説明を挿入

実際、これは非常に単純で、順序を指定した単一のテーブル ページング クエリだけです。

    /**
     * 后台显示订单信息
     */
    @GetMapping("/page")
    public R<Page> page(int page, int pageSize, Long number, String beginTime, String endTime) {
    
    
        log.info("page={},pageSize={},number={},beginTime={},endTime={}",page,pageSize,number,beginTime,endTime);
        //分页构造器对象
        Page<Orders> pageInfo = new Page<>(page,pageSize);
        //构造条件查询对象
        LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
        //链式编程写查询条件
        queryWrapper.like(number!=null,Orders::getNumber,number)
                //前面加上判定条件是十分必要的,用户没有填写该数据,查询条件上就不添加它
                .gt(StringUtils.isNotBlank(beginTime),Orders::getOrderTime,beginTime)//大于起始时间
                .lt(StringUtils.isNotBlank(endTime),Orders::getOrderTime,endTime);//小于结束时间
        ordersService.page(pageInfo,queryWrapper);
        return R.success(pageInfo);
    }

しかし、問題が見つかりました。バックエンド表示データにはユーザー名がありません。
ここに画像の説明を挿入

実際、注文テーブルにはユーザー名フィールドがないため、ユーザー名がないのが普通です。したがって当然わかりませんが、注文テーブルには user_id フィールドがあることがわかりますが、ユーザー名はありません。方法
ここに画像の説明を挿入
1: まずユーザー テーブルにユーザー名フィールドを追加し、バックグラウンドで注文情報を表示する このページング クエリはデータ ユーザー名を取得します

方法 2: シンプル (怠惰) で、注文テーブルの荷受人 (荷受人) の名前をページング クエリ ページのユーザー名として直接取り出します。

ここに画像の説明を挿入
ここで、フロントエンド ユーザー名を荷受人に置き換えてユーザー名を表示します。
ここに画像の説明を挿入

ここに画像の説明を挿入

バックグラウンド注文ステータスの変更

ここに画像の説明を挿入

搬送パラメータはステータスであるため、非常に明確です。注文 ID に従って注文のステータスを変更することです。これは変更操作です。
ここに画像の説明を挿入

    /**
     * 修改订单状态
     */
    @PutMapping
    public R<String> orderStatusChange(@RequestBody Map<String,String> map){
    
    

        String id = map.get("id");
        Long orderId = Long.parseLong(id);//将接收到的id转为Long型
        Integer status = Integer.parseInt(map.get("status"));//转为Integer型

        if(orderId == null || status==null){
    
    
            return R.error("传入信息非法");
        }
        Orders orders = ordersService.getById(orderId);//根据订单id查询订单数据
        orders.setStatus(status);//修改订单对象里的数据
        ordersService.updateById(orders);

        return R.success("订单状态修改成功");

    }

3. モバイル事業の展開

1. ユーザーのログインとログアウト (終了は補足です)

ユーザーログイン
「ログイン」をクリックしてリクエストを送信します
ここに画像の説明を挿入

ペイロードは携帯電話番号と検証コードを一緒に送信し、マップのキーと値の形式を使用してそれを受信できます。キーは電話番号、値はコード code です
ここに画像の説明を挿入

ここに画像の説明を挿入

ユーザー出口、要求された URL に従って出口関数を作成します。
ここに画像の説明を挿入

    /**
     * 用户退出
     * @param request
     * @return
     */
    @PostMapping("/loginout")
    public R<String> loginout(HttpServletRequest request){
    
    
        request.getSession().removeAttribute("user");
        return R.success("退出成功");
    }

ここに画像の説明を挿入

2. Alibaba Cloud SMS 認証コード

これら 2 つのツール クラスを utils パッケージの下にインポートします。実際、Alibaba Cloud API ドキュメントに移動してコピーすることもできます。
ここに画像の説明を挿入

ここに画像の説明を挿入

accessKeyIdの取得方法は以下のとおりです。
ここに画像の説明を挿入

ここに画像の説明を挿入
ここに画像の説明を挿入
ここに画像の説明を挿入
新しいユーザーに権限を追加する
ここに画像の説明を挿入

UserControllerの認証コード送信メソッドでは、Alibaba Cloudが提供するSMSで送信されるAPIを呼び出します。

ここに画像の説明を挿入

@PostMapping("/sendMsg")
    public R<String> sendMsg(@RequestBody User user, HttpSession session){
    
    
        //获取手机号
        String phone = user.getPhone();
        if (StringUtils.isNotBlank(phone)){
    
    
            //生成随机的4位验证码
            String code = ValidateCodeUtils.generateValidateCode(4).toString();
            log.info("code={}",code);
            //调用阿里云提供的短信服务API完成发送短信
            SMSUtils.sendMessage("你自己的签名","你自己的模板code",phone,code);

            //需要将生成的验证码保存到Session
            //session.setAttribute(phone,code);

            //将将生成的验证码保存到Redis中,并且设置有效期为5分钟   phone是key,code是value
            redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES);

            return R.success("手机验证码短信发送成功");
        }
        return R.error("手机验证码短信发送失败");
    }

ここに画像の説明を挿入

最終的な効果は以下の通り
ここに画像の説明を挿入

3. 配送先住所

まず、このアドレス管理によって実現される機能を解析します。ここでは、すべてのアドレスを表示する必要があるため、すべての機能に対するクエリが必要です。次に、アドレスの追加とアドレスの変更のための新しい操作があります。のロジックです。アドレスの変更は表示と変更に分割され、エコーは渡された ID に従ってクエリを実行し、基本的な SQL は mp が提供するメソッドを通じて直接呼び出すことができます。
: このクエリはすべてであり、mp が提供するリスト メソッドをクエリに直接使用することはできません。条件付きクエリを使用する必要があります。これは、アドレス テーブル内のすべてのデータを表示するのではなく、user_id に基づくクエリであるためです。

ここに画像の説明を挿入
次のようにすべてのコードをクエリします。

@GetMapping("/list")
    public R<List<AddressBook>> list(AddressBook addressBook) {
    
    
        addressBook.setUserId(BaseContext.getCurrentId());
        log.info("addressBook:{}", addressBook);

        //条件构造器
        LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(null != addressBook.getUserId(), AddressBook::getUserId, addressBook.getUserId());
        queryWrapper.orderByDesc(AddressBook::getUpdateTime);

        //SQL:select * from address_book where user_id = ? order by update_time desc
        return R.success(addressBookService.list(queryWrapper));
    }

もう 1 つは、アドレスをデフォルト アドレスとして設定することです。実際には、アドレス テーブルの is_default フィールドを条件に応じて変更します。これを 1 に設定するとデフォルト アドレスになりますが、デフォルト アドレスは 1 つだけです。ロジックデフォルトのアドレスを変更したい場合は、ユーザーのすべてのアドレスの is_default フィールドを 0 に更新し、ユーザーがデフォルトとして設定するアドレス ID を送信した後、アドレスの is_default フィールドを 1 に変更します。住所。

    @PutMapping("default")
    public R<AddressBook> setDefault(@RequestBody AddressBook addressBook) {
    
    
        log.info("addressBook:{}", addressBook);
        LambdaUpdateWrapper<AddressBook> wrapper = new LambdaUpdateWrapper<>();
        wrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
        wrapper.set(AddressBook::getIsDefault, 0);
        //SQL:update address_book set is_default = 0 where user_id = ?
        addressBookService.update(wrapper);

        addressBook.setIsDefault(1);
        //SQL:update address_book set is_default = 1 where id = ?
        addressBookService.updateById(addressBook);
        return R.success(addressBook);
    }

以下はデフォルトのアドレスをクエリするコードです。

    /**
     * 查询默认地址
     */
    @GetMapping("default")
    public R<AddressBook> getDefault() {
    
    
        LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(AddressBook::getUserId, BaseContext.getCurrentId());
        queryWrapper.eq(AddressBook::getIsDefault, 1);

        //SQL:select * from address_book where user_id = ? and is_default = 1
        AddressBook addressBook = addressBookService.getOne(queryWrapper);

        if (null == addressBook) {
    
    
            return R.error("没有找到该对象");
        } else {
    
    
            return R.success(addressBook);
        }
    }

4. 料理やパッケージの展示

このとき、バックエンドはデータを渡しており、ロードからは送られたjsonが見えるのですが、フロントエンドでは表示されません
ここに画像の説明を挿入

ここに画像の説明を挿入

ここに画像の説明を挿入

しかし、現在、バックグラウンド管理端末に書かれたすべての料理をクエリするコードは適用できなくなりました。モバイル端末も分類名と味のデータを表示する必要があるため、ここでのクエリでは料理オブジェクトを返すことができなくなり、DishDto が返される必要があります。汎用の DishDto リスト コレクションを返すために使用される場合は、コードを変更する必要があります

    @GetMapping("/list")
    public R<List<DishDto>> list(Dish dish){
    
    
        List<DishDto> dishDtoList = null;

        //动态构造key
        String key = "dish_" + dish.getCategoryId() + "_" + dish.getStatus();//dish_13494852934_1
        //从redis中获取缓存数据(移动端使用redis缓存,将每个分类下查询的数据都放到缓存,避免重复查询,降低服务器压力)
        dishDtoList = (List<DishDto>) redisTemplate.opsForValue().get(key);
        //如果从redis获取的数据不为空,证明redis缓存了该数据,直接取出来返回,就无需查询数据库
        if (dishDtoList != null){
    
    
            return R.success(dishDtoList);
        }

        //构造查询条件
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(dish.getCategoryId()!=null,Dish::getCategoryId,dish.getCategoryId());
        //添加条件,查询状态为1(起售状态)的菜品
        queryWrapper.eq(Dish::getStatus,1);
        //添加排序条件
        queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
        List<Dish> list = dishService.list(queryWrapper);

        dishDtoList = list.stream().map((item)->{
    
    
            DishDto dishDto = new DishDto();
            BeanUtils.copyProperties(item,dishDto);
            Long dishId = item.getId();
            LambdaQueryWrapper<DishFlavor> lambdaQueryWrapper=new LambdaQueryWrapper<>();
            lambdaQueryWrapper.eq(DishFlavor::getDishId,dishId);
            //SQL: select * from dish_flavor where dish_id = ?
            List<DishFlavor> dishFlavorList = dishFlavorService.list(lambdaQueryWrapper);
            dishDto.setFlavors(dishFlavorList);
            return dishDto;
        }).collect(Collectors.toList());

        //如果redis不存在该数据,需要查询数据库,将查询菜品数据缓存到redis中
        redisTemplate.opsForValue().set(key,dishDtoList,60, TimeUnit.MINUTES);
        return R.success(dishDtoList);
    }

パッケージデータの表示も同様です 送られてきたURLに従いsetmealオブジェクトを使ってデータを受け取りバックエンドコードを記述します 表示されたパッケージには味などのデータは無いのでSetmealのリストコレクションを直接返すだけです。
ここに画像の説明を挿入

    @GetMapping("/list")
    @Cacheable(value = "setmealCache", key = "#setmeal.categoryId + '_' + #setmeal.status")
    public R<List<Setmeal>> list(Setmeal setmeal){
    
    
        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(setmeal.getCategoryId()!=null,Setmeal::getCategoryId,setmeal.getCategoryId());
        queryWrapper.eq(setmeal.getStatus()!=null,Setmeal::getStatus,setmeal.getStatus());
        queryWrapper.orderByDesc(Setmeal::getUpdateTime);
        List<Setmeal> list = setmealService.list(queryWrapper);
        return R.success(list);
    }

5.料理の選択仕様

料理の横にある選択仕様をクリックすると、選択フレーバー データのポップアップ ウィンドウが表示され、次のコードをリスト クエリ メソッドに追加する必要があります。
ここに画像の説明を挿入

ここに画像の説明を挿入

コードデバッグ デバッグで確認できること
ここに画像の説明を挿入

6. パッケージクリック表示(補足)

定食をクリックするとgetリクエストが送信されます。URLは以下の通りです。おそらく、定食の料理データが表示されるはずです。この表示には数字も含まれているので、DishDtoを返すことができます。 f12 の応答によると、このデータは料理だけでは表示できないことが列からわかるので、DishDto オブジェクトを返します。
ここに画像の説明を挿入

ここに画像の説明を挿入

    /**
     * 点击查看套餐中的菜品
     */
    @GetMapping("/dish/{id}")
    public R<List<DishDto>> dish(@PathVariable("id") Long SetmealId) {
    
    
        LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SetmealDish::getSetmealId, SetmealId);
        //获取套餐里面的所有菜品  这个就是SetmealDish表里面的数据
        List<SetmealDish> list = setmealDishService.list(queryWrapper);

        List<DishDto> dishDtos = list.stream().map((setmealDish) -> {
    
    
            DishDto dishDto = new DishDto();
            //将套餐菜品关系表中的数据拷贝到dishDto中
            BeanUtils.copyProperties(setmealDish, dishDto);
            //这里是为了把套餐中的菜品的基本信息填充到dto中,比如菜品描述,菜品图片等菜品的基本信息
            Long dishId = setmealDish.getDishId();
            Dish dish = dishService.getById(dishId);
            //将菜品信息拷贝到dishDto中
            BeanUtils.copyProperties(dish, dishDto);

            return dishDto;
        }).collect(Collectors.toList());

        return R.success(dishDtos);
    }

ここに画像の説明を挿入

7. ショッピングカート

ショッピングカートのテーブル構造
ここに画像の説明を挿入

事前に main.js の下のコメントを開いて、料理セットを表示するときにコメントすることを忘れないでください。
ここに画像の説明を挿入

需要分析は
下図のようになります。まず、新しい操作であるショッピング カートに追加し、ショッピング カート テーブルにデータを保存し、ショッピング カートを表示します。以下のショッピング カート内のすべてのデータをクエリします。ユーザーをユーザー ID でクエリし、合計を加算します。 減算は、対応するテーブルの数値フィールドを変更することです。

ただし、追加するときに、ショッピング カートにデータがない場合は保存する必要があることに注意してください。データがある場合は、数値フィールドを直接変更して 1 を追加できます。データがない場合は、数値フィールドを直接変更して 1 を減算します。

    /**
     * 从购物车中减掉
     */
    @PostMapping("/sub")
    public R<String> remove(@RequestBody ShoppingCart shoppingCart){
    
    
        //设置用户id,指定当前时哪个用户的购物车数据
        Long currentId = BaseContext.getCurrentId();
        shoppingCart.setUserId(currentId);
        //查询当前菜品或者套餐是否在购物车中
        Long dishId = shoppingCart.getDishId();
        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ShoppingCart::getUserId,currentId);

        if (dishId!=null){
    
    
            //添加到购物车的是菜品
            queryWrapper.eq(ShoppingCart::getDishId,dishId);
        }else {
    
    
            //添加到购物车的是套餐
            queryWrapper.eq(ShoppingCart::getSetmealId,shoppingCart.getSetmealId());
        }
        //SQL:select * from shopping_cart where user_id = ? and dish_id = ?
        ShoppingCart cartServiceOne = shoppingCartService.getOne(queryWrapper);

        if (cartServiceOne.getNumber()>1){
    
    
            //如果已经存在,就在原来数量基础上减去一
            Integer number = cartServiceOne.getNumber();
            cartServiceOne.setNumber(number-1);
            shoppingCartService.updateById(cartServiceOne);
        }else {
    
    

            shoppingCartService.remove(queryWrapper);
        }
        return R.success("减去成功");
    }

ショッピング カートを空にします。つまり、user_id に従ってユーザーの下のすべてのデータを削除します。

ここに画像の説明を挿入

ここに画像の説明を挿入

    @GetMapping("/list")
    public R<List<ShoppingCart>> list(){
    
    
        log.info("查看购物车");
        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
        queryWrapper.orderByAsc(ShoppingCart::getCreateTime);
        List<ShoppingCart> list = shoppingCartService.list(queryWrapper);
        return R.success(list);
    }
    /**
     * 清空购物车
     */
    @DeleteMapping("/clean")
    public R<String> clean(){
    
    
        //SQL:delete from shopping_cart where user_id = ?
        LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
        shoppingCartService.remove(queryWrapper);
        return R.success("清空购物车成功");
    }

8. 注文する

需要分析
ここに画像の説明を挿入

ここに画像の説明を挿入

クリックして支払い送信された URL リクエストに従って、フロントエンドによって渡されたデータに従って、注文オブジェクトを使用してそれを受け取ることができます。
ここに画像の説明を挿入
渡されたデータは 3 つだけなので、注文オブジェクトの他の属性を完了する必要がありますそして注文テーブルに挿入されます。
ここに画像の説明を挿入

具体的なコーディング手順は次のとおりです。
1. 現在のユーザー ID を取得します
。 2. 現在のユーザーのショッピング カート データをクエリします。
3. ユーザー データをクエリします
。 4. 住所データをクエリし
ます。 5. データを注文テーブルに 1 つのデータとして挿入します。 end は、addressBookId、payMethod、remark の 3 つのデータのみを渡します。その他のデータは、上でクエリしたショッピング カート、ユーザー、住所のデータを注文オブジェクトに設定し、mp の save メソッドを呼び出して注文をデータベースに保存する必要があります) 6. 複数のデータを注文リストに挿入します
(上記と同様、手動でデータを orderDetails オブジェクトに保存し、mp の saveBatch メソッドを呼び出して orderDetails をテーブルに挿入する必要があります) 7. ショッピングを空にし
ますカートデータ

9. 配送先住所の削除(補足)

ブラウザから送信された URL によると、おそらく ID に基づいてアドレス テーブルを削除する操作であることはわかりますが、この ID は RESTful スタイルではなく、直接スプライシングの形式であるため、値仮パラメータのは @RequestParam(“ids”) Long id
ここに画像の説明を挿入

    @DeleteMapping()
    public R<String> detele(@RequestParam("ids") Long id){
    
    
        log.info("id={}",id);

//        if (id == null){
    
    
//            return R.error("请求异常");
//        }  //感觉这个判断没太大必要,前端传的id必不能为空,为空的话地址就不会展示出来,更不会有这个删除按钮存在,简单说为空的话,连删除的机会都没有,所以判断没太大必要

//        LambdaQueryWrapper<AddressBook> queryWrapper = new LambdaQueryWrapper<>();
//        queryWrapper.eq(AddressBook::getId,id).eq(AddressBook::getUserId,BaseContext.getCurrentId());
//        addressBookService.remove(queryWrapper);

        //别人说直接使用这个removeById不太严谨,但我个人认为就是没登录状态进入该页面,是执行不了删除操作的,别说删除连查询,这个地址信息都不会展示,全被过滤器拦截了
        //所以用上面的条件查询好像意义不大,当然你也可以放弃这个简单的removeById,用上面注释的条件删除
        addressBookService.removeById(id);
        return R.success("删除成功");
    }

ここに画像の説明を挿入

10. 利用者決済後の注文確認(補足)

ブラウザから送信される URL は次のとおりで、order_detail テーブルのデータを注文 ID に従ってクエリするもので、単純な単一テーブルのページング クエリだと思っていましたが、実は落とし穴でした。

orderDetailのテーブル構造
ここに画像の説明を挿入

order.html フロントエンド ページには、バックエンドから渡す必要がある次のデータも必要です。これは単なる単一テーブルのページごとのクエリです。orderDetail オブジェクトには注文名やその他のデータはありません。 。

ここに画像の説明を挿入

ここに画像の説明を挿入

したがって、ここでは OrderDto オブジェクトを使用して、注文データと注文詳細データを OrderDto オブジェクトに保存し、Dto ページネーション クエリのデータを返す必要があります。
OrderDto を作成します。

@Data
public class OrderDto extends Orders {
    
    
    private List<OrderDetail> orderDetails;
}

OrderService インターフェイスは、注文 ID に従って注文の詳細データをクエリするメソッドを宣言します。

public List<OrderDetail> getOrderDetailListByOrderId(Long orderId);

この条件付きクエリ メソッドを OrdersServiceImpl 実装クラスに実装します。

    public List<OrderDetail> getOrderDetailListByOrderId(Long orderId){
    
    
        LambdaQueryWrapper<OrderDetail> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(OrderDetail::getOrderId, orderId);
        //根据order表的条件查询出order_detail的数据,因为一个订单可能有多条菜品数据
        List<OrderDetail> orderDetailList = orderDetailService.list(queryWrapper);
        return orderDetailList;
    }

ここに画像の説明を挿入
注文データのページングクエリ
ここに画像の説明を挿入

OrderControllerクラス配下に、決済後の注文機能を確認するコード

    @GetMapping("/userPage")
    public R<Page> page(int page, int pageSize){
    
    
        //分页构造器对象
        Page<Orders> pageInfo = new Page<>(page,pageSize);
        Page<OrderDto> pageDto = new Page<>(page,pageSize);
        //构造条件查询对象
        LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(Orders::getUserId, BaseContext.getCurrentId());
        //这里是直接把当前用户分页的全部结果查询出来,要添加用户id作为查询条件,否则会出现用户可以查询到其他用户的订单情况
        //添加排序条件,根据更新时间降序排列
        queryWrapper.orderByDesc(Orders::getOrderTime);
        //这里是把所有的订单分页查询出来
        ordersService.page(pageInfo,queryWrapper);

        //对OrderDto进行属性赋值
        List<Orders> records = pageInfo.getRecords();
        List<OrderDto> orderDtoList = records.stream().map((item) ->{
    
    //item其实就是分页查询出来的每个订单对象
            OrderDto orderDto = new OrderDto();
            //此时的orderDto对象里面orderDetails属性还是空 下面准备为它赋值
            Long orderId = item.getId();//获取订单id
            //调用根据订单id条件查询订单明细数据的方法,把查询出来订单明细数据存入orderDetailList
            List<OrderDetail> orderDetailList = ordersService.getOrderDetailListByOrderId(orderId);

            BeanUtils.copyProperties(item,orderDto);//把订单对象的数据复制到orderDto中
            //对orderDto进行OrderDetails属性的赋值
            orderDto.setOrderDetails(orderDetailList);
            return orderDto;
        }).collect(Collectors.toList());

        //将订单分页查询的订单数据以外的内容复制到pageDto中,不清楚可以对着图看
        BeanUtils.copyProperties(pageInfo,pageDto,"records");
        pageDto.setRecords(orderDtoList);
        return R.success(pageDto);
    }

フロントエンド ページがページング データを送信するため、バックエンドは OrderDto のページネーション クエリを実現します。実際、現在の支払い注文のみをクエリすることも考えました。注文 ID に従って OrderDto をクエリすることは難しくありません。クエリされた注文データと注文の詳細を DishDto オブジェクトに保存し、DishDto オブジェクトを返します。

ただし、フロントエンドから渡されるデータはページサイズとページクエリであり、明らかにページクエリであり、オーダーIDパラメータが渡されていないため、現在のオーダーに対応するIDを判断できず、クエリを実行できません。注文IDに応じた注文詳細データ。
ここに画像の説明を挿入
ここに画像の説明を挿入

11. もう一つ注文(補足)

ここに画像の説明を挿入

渡されるのは注文IDです

ここに画像の説明を挿入
①上記で渡したorderIdで注文内容のデータを取得します
。 ②注文内容のデータをショッピングカートテーブルに挿入しますが、その前にショッピングカートテーブルのデータをクリアする必要があります(現在ログインしているユーザーの買い物はクリア済み) 車両テーブルのデータ)

    /**
     * 再来一单
     * @param map
     * @return
     */
    @PostMapping("/again")
    public R<String> againSubmit(@RequestBody Map<String,String> map){
    
    
        String ids = map.get("id");

        long id = Long.parseLong(ids);

        LambdaQueryWrapper<OrderDetail> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(OrderDetail::getOrderId,id);
        //获取该订单对应的所有的订单明细表
        List<OrderDetail> orderDetailList = orderDetailService.list(queryWrapper);

        //通过用户id把原来的购物车给清空,这里的clean方法就是之前的购物车清空方法,我给写到service中去了,这样可以通过接口复用代码
        shoppingCartService.clean();

        //获取用户id
        Long userId = BaseContext.getCurrentId();
        List<ShoppingCart> shoppingCartList = orderDetailList.stream().map((item) -> {
    
    
            //把从order表中和order_details表中获取到的数据赋值给这个购物车对象
            ShoppingCart shoppingCart = new ShoppingCart();
            shoppingCart.setUserId(userId);
            shoppingCart.setImage(item.getImage());
            Long dishId = item.getDishId();
            Long setmealId = item.getSetmealId();
            if (dishId != null) {
    
    
                //如果是菜品那就添加菜品的查询条件
                shoppingCart.setDishId(dishId);
            } else {
    
    
                //添加到购物车的是套餐
                shoppingCart.setSetmealId(setmealId);
            }
            shoppingCart.setName(item.getName());
            shoppingCart.setDishFlavor(item.getDishFlavor());
            shoppingCart.setNumber(item.getNumber());
            shoppingCart.setAmount(item.getAmount());
            shoppingCart.setCreateTime(LocalDateTime.now());
            return shoppingCart;
        }).collect(Collectors.toList());

        //把携带数据的购物车批量插入购物车表  这个批量保存的方法要使用熟练!!!
        shoppingCartService.saveBatch(shoppingCartList);

        return R.success("操作成功");
    }

このようなフロントエンド コードは order.html で確認できます。


<div class="btn" v-if="order.status === 4">  //状态是4才会让你点击下面这个再来一单
     <div class="btnAgain" @click="addOrderAgain(order)">再来一单
     </div>
</div>

ここに画像の説明を挿入
バックグラウンドでの注文確認機能がないため、データベースを通じて注文ステータスを変更することでテストは完了します。
ここに画像の説明を挿入

テスト結果、ショッピング カートがデータをエコーする
ここに画像の説明を挿入

4. プロジェクトの最適化

1. Redisキャッシュを使用する

1.1 キャッシュ検証コード

1. Redis 座標を pom にインポートします

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>

2. 対応する操作を UserController に追加し、
最初に RedisTemplate を自動的にアセンブルします

@Autowired
    private RedisTemplate redisTemplate;

次に、前の確認コードをセッションに入力し、redis に置き換えます。

redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES);

ここに画像の説明を挿入

次に、ユーザー ログイン メソッドで、redis から検証コードを取得し、ログイン成功後に検証コードを削除する必要があります。

Object codeInSession = redisTemplate.opsForValue().get(phone);
            //如果登录成功,删除redis中的验证码
            redisTemplate.delete(phone);

ここに画像の説明を挿入

Redis 構成クラスを作成し、キーをシリアル化します。そうしないと、キーが作成したキーの名前ではなく、\xAC\xED\x00\x05t\x00\key name (おそらくこのタイプである) であることが Redis クライアントで確認できます。 、読みにくいです。シリアル化させます。最終的にはアイデアがシリアル化するため、値をシリアル化する必要はありません。

@Configuration
public class RedisConfig extends CachingConfigurerSupport {
    
    

    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
    
    
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();

        //默认的Key序列化器为:JdkSerializationRedisSerializer
        redisTemplate.setKeySerializer(new StringRedisSerializer()); // key序列化
        //redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // value序列化

        redisTemplate.setConnectionFactory(connectionFactory);
        return redisTemplate;
    }
}

ここに画像の説明を挿入

1.2 ディッシュクエリデータのキャッシュ

まず、モバイル端末上の料理のクエリを分析します。湖南料理や四川料理などのカテゴリごとに、クリックするたびにデータベースを再クエリする必要があります。 では、ライフ サイクルを 30 分に設定します。クリックして再度表示すると、データベースに再度クエリを実行することはなく、redis から直接データを取得するため、サーバーの負荷が軽減され、リソースの無駄が回避されます。

キャッシュロジックでは、
まず一意のキー値を動的に構築し、そのキーに応じて値を取得し、値が空かどうかを判断し、空でない場合は、redis にそのカテゴリにデータがあることを意味し、直接返す; 空の場合は、データベースにアクセスしてデータをクエリし、クエリされたデータを Redis キャッシュに入れて、次回はデータベースに再度クエリせずに Redis キャッシュに直接クエリする必要があります。

ここに画像の説明を挿入

ここに画像の説明を挿入

Redis がディッシュ情報をキャッシュしていることがわかります。

ここに画像の説明を挿入

ただし、キャッシュを使用する場合は、変更、追加、および削除のためにキャッシュをクリアする必要があります。そうしないと、バックエンド データが変更された場合でも、再度クエリを実行するとキャッシュにアクセスすることになり、キャッシュされたデータは変更されていない 最新のデータが見つからない データの不一致 したがって、変更、追加、削除メソッドでキャッシュをクリーンアップします。

        //清理所有菜品的缓存数据
        //Set keys = redisTemplate.keys("dish_*");
        //redisTemplate.delete(keys);

        //只清理该修改菜品的分类下缓存的数据,精确清理,因为redis可能已经缓存了好几个分类下的数据,全删太浪费
        String key = "dish_" + dishDto.getCategoryId() + "_1";
        redisTemplate.delete(key);

ここに画像の説明を挿入

1.3Spring Cacheのキャッシュパッケージデータ

1. Spring Cache 座標を pom ファイルにインポートします

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-cache</artifactId>
		</dependency>

2. スタートアップクラスにアノテーションを追加してキャッシュ機能を有効にする

@EnableCaching

ここに画像の説明を挿入

3. Spring Cache アノテーションを使用してキャッシュを有効にする
ここに画像の説明を挿入

: このメソッドによって返される結果 R のクラスはシリアル化インターフェイスを実装する必要があります。実装しないとエラーが報告され、キャッシュできません。
ここに画像の説明を挿入

ここに画像の説明を挿入

キャッシュクエリのパッケージ情報

@Cacheable(value = "setmealCache", key = "#setmeal.categoryId + '_' + #setmeal.status")

ここに画像の説明を挿入

キャッシュをクリアするには、このアノテーションを new、modify、delete メソッドに追加する必要があります。

@CacheEvict(value = "setmealCache", allEntries = true)

ここに画像の説明を挿入

もう一度アクセスすると、redis の下にすでにパッケージ データがあり、パッケージ分類に従ってまだキャッシュされていることがわかります。
ここに画像の説明を挿入

2. 読み取りと書き込みの分離

読み取りと書き込みのすべての負荷が 1 つのデータベースに負担され、負荷が高く、データベース サーバーのディスクが損傷するかデータが失われ、単一障害点が発生します。
ここに画像の説明を挿入

2.1mysql マスター/スレーブ レプリケーション

MySQL のマスター/スレーブ レプリケーションは、非同期レプリケーション プロセスです。スレーブ ライブラリのスレーブは、マスター ライブラリのマスターからログをコピーし、ログを解析してそれ自体に適用し、最終的にスレーブ ライブラリのデータがマスター ライブラリのデータと一致していることを認識します。MySQL のマスター/スレーブ レプリケーションは、MySQL データベースの組み込み機能です。
ここに画像の説明を挿入

注: MySQL がインストールされ、正常に起動されるサーバーが少なくとも 2 つある必要があります。仮想マシンを使用して、別のサーバーをスレーブ ライブラリとしてクローン作成できます。

1. マスターは、レコードをバイナリ ログ バイナリ ログに変更して、
MySQL データベースの構成ファイルを変更し、vim /etc/my.cnf
[mysqld]に次のコードを追加します。

log-bin=mysql-bin  #启用二进制日志
server-id=100  #id作为服务器唯一标识,不一定要100,只要不重复即可

ここに画像の説明を挿入
保存して終了した後、mysql サービスを再起動します。

systemctl restart mysqld

スレーブがこのユーザーを通じて記録したログ ファイルをコピーできるように、マスターの下にユーザーを作成し、そのユーザーに権限を付与する必要があります。まず mysql にログインします。

mysql -uroot -p

xiaoming という名前のユーザーを作成し、パスワードは Root@123456 で、そのユーザーに REPLICATION SLAVE 権限を付与します。

GRANT REPLICATION SLAVE ON *.* to 'xiaoming'@'%' identified by 'Root@123456';

mysql8エラーで
ここに画像の説明を挿入
認証前にユ​​ーザーを作成する必要がある場合は、次のコードを使用します。

create user xiaoming identified by 'Root@123456';
grant replication slave on *.* to xiaoming;

メインライブラリのステータスを表示する

show master status;

ここに画像の説明を挿入
次に、メインライブラリを操作する必要はありませんが、操作を行うと記録場所が変更され、698ではなくなります。この場所とファイル名は、後ほどスレーブライブラリで使用されます。

2. スレーブはマスターのバイナリ ログをリレー ログにコピーします。
まず、MySQL データベースの構成ファイルを変更し、vim /etc/my.cnf
[mysqld]に次のコードを追加します。

server-id=101 #必须是唯一的id,不能重复

ここに画像の説明を挿入

保存して終了した後、mysql サービスを再起動します。

systemctl restart mysqld

MySQL にログインし、次のコードを実行します。

mysql -uroot -p
change master to master_host='填入master的ip',master_user='上面创建的用户',master_password='上面设置的',master_log_file='主库刚查的日志名称',master_log_pos=刚查的记录位置;

スレーブを開始する

start slave

ここに画像の説明を挿入

スレーブのステータスを表示する

show slave status\G

MySQL8 を使用していて、エラー メッセージが次の場合は、
ここに画像の説明を挿入
この解決策を参照できますhttps://www.modb.pro/db/29919

Slave_IO_Running と Slave_SQL_Running は両方とも「いいえ」です。この解決策をご覧くださいhttps://www.cnblogs.com/MENGSHIYU/p/11978489.html
終了後は必ず MySQL サービスを再起動してください

systemctl restart mysqld

変更後、スレーブ ライブラリの 2 つの IOS はすべて Yes になります。
ここに画像の説明を挿入

3. スレーブはリレー ログ内のイベントをやり直し、変更を自身のデータベースに適用します。

Navicat では、IP に従って 2 つのマスター/スレーブ接続が作成されます。マスター データベースがデータベースとテーブルを作成した後、スレーブ データベースはそれらを更新して直接表示します。マスター データベースによるテーブルの変更操作はテーブル F5 に表示されます
ここに画像の説明を挿入
。スレーブデータベースの。

2.2 シャーディング-JDBC による読み書き分離の実現

メイン ライブラリは書き込み操作を実行し、スレーブ ライブラリは読み取り操作を実行します。Sharding-JDBC は、Java の JDBC 層でサービスを提供する軽量の Java フレームワークです。カプセル化された API を使用するには、座標をインポートするだけで済み、簡単に実現できます。データベースの読み取りと書き込みは別個です。
1. pom ファイルはシャーディング JDBC 座標をインポートします

		<dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-jdbc-spring-boot-starter</artifactId>  
        </dependency>

2. yml 構成ファイルに構成情報を追加し、ip を独自のマスター/スレーブ データベースの IP に置き換え、パスワードを独自のパスワードに置き換えます。

spring:
  shardingsphere:
    datasource:
      names:
        master,slave
      # 主数据源
    master:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.231.128:3306/rw?characterEncoding=utf-8
        username: root
        password: 123456
      # 从数据源
    slave:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.231.129:3306/rw?characterEncoding=utf-8
        username: root
        password: 123456
    masterslave:
      # 读写分离配置
      load-balance-algorithm-type: round_robin  #轮询策略,负载均衡
      # 最终的数据源名称
      name: dataSource
      # 主库数据源名称
      master-data-source-name: master
      # 从库数据源名称列表,多个逗号分隔
      slave-data-source-names: slave
    props:
      sql:
        show: true #开启SQL显示,默认false


3. Sharding-JDBC と Druid の両方にデータ ソースによって定義された構成クラスがあり、どちらもデータ ソース オブジェクトを作成する必要があるため、構成ファイルで Bean 定義のオーバーライドを有効にします。その結果、競合が発生します。Bean のオーバーライドを有効にする必要があり、データソースは後で作成されます以前のものを上書きします。具体的なコードは次のとおりです。

spring:
  main:
    allow-bean-definition-overriding: true

構成が完了すると、自動的に読み取りと書き込み、スレーブ ライブラリへのクエリと移動、メイン ライブラリへの追加、削除、および変更が可能になります。

2.3 プロジェクトは読み取りと書き込みの分離を実現します

1. メインデータベースに新しい reggie データベースを作成し、先ほどの SQL ファイルを実行して更新すると、プロジェクトのテーブル構造とデータがインポートされていることがわかります。データベースから更新すると、データも出てきます。
2. 上と同様に、座標をガイドし、対応する設定情報を yml に追加し、データベース名を reggie に変更し、読み取りと書き込みの分離を完了します。

3. Nginxサーバーを使用する

Nginx は、軽量の Web サーバー/リバース プロキシ サーバーおよび電子メール プロキシ サーバーです。その利点は、メモリ占有量が少なく、同時実行性が高いことです
nginx 設定ファイルは 3 つの部分に分かれています
グローバル ブロック: イベント ブロックの前の設定と nginx の動作に関連するグローバル設定 イベント
ブロック: ネットワーク接続に関連する構成
http ブロック: プロキシ、キャッシュ、ロギング、仮想ホスト構成、通常は主にこのコンテンツを構成します

3.1 Nginx は静的リソースをデプロイします

静的リソースをnginxのhtmlディレクトリに直接置くだけで
ここに画像の説明を挿入
アクセスする際はip/page名になります

3.2 リバースプロキシ

フォワード プロキシは、簡単に言うと梯子です。クライアントはプロキシ サーバーを介してターゲット サーバーにアクセスします。これはクライアント上で行われ、クライアントはプロキシ サービスを認識します。

リバース プロキシは、プロキシ サーバーにアクセスするクライアントです。プロキシ サーバーはターゲット サーバーに転送しますが、これはサーバー側で行われます。クライアントはプロキシ サーバーの存在を認識しません。
フォワード プロキシはユーザーを隠し、リバース プロキシはサーバーを隠します。

ここに画像の説明を挿入

リバース プロキシを構成するには、リバース プロキシ サーバー上で nginx.conf 構成ファイルを構成し、次のコードを http ブロックに追加します。

    server {
    
    
        listen       82;
        server_name  localhost;
        location / {
    
    
                proxy_pass http://目标服务器ip:8080;#将请求转发到指定服务器
        }
    }

そして、プロキシサーバーのポート82を開き、ファイアウォールをリロードします

firewall-cmd --zone=public --add-port=82/tcp --permanent

firewall-cmd --reload

nginx をリロードし
、ここで nginx を環境変数として設定する必要があることに注意してください。詳細については、nginx の基本設定を参照してください。

nginx -s reload

アクセス時はリバースプロキシサーバーのipにアクセスし、リバースプロキシサーバーがリクエストを対象サーバーに転送します。

3.3 負荷分散

アプリケーション クラスタ:同じアプリケーションを複数のサーバに展開してアプリケーション クラスタを形成し、ロード バランサによって分散されたリクエストを受信し、業務処理を実行して応答データを返します。 ロード バランサ:対応する負荷分散アルゴリズムに従ってユーザ リクエストをアプリケーション クラスタに分散します。
サーバの
ここに画像の説明を挿入

負荷分散サーバーで nginx.conf 構成を構成する

ここに画像の説明を挿入
デフォルトはポーリング戦略です。このサーバーが初めて使用されるとき、2 回目にサーバーが削除されます。
構成が完了したら、nginx サービスをリロードします。

nginx -s reload

このように、クラスター モードは、単一サーバーへの負荷を軽減し、アクセス効率を向上させ、単一障害点の問題を回避するために使用されます。

4. フロントエンドとバックエンドの個別開発

プロジェクト構造が分割され、プロジェクト展開も変化
ここに画像の説明を挿入

開発プロセス
ここに画像の説明を挿入
インターフェイス (API インターフェイス) は http リクエスト アドレスであり、主にリクエスト パス、リクエスト メソッド、リクエスト パラメータ、レスポンス データなどを定義します。

4.1YApi

インターフェイス管理サービスを提供して、インターフェイス開発をより簡単かつ効率的にし、インターフェイス管理をより読みやすく、保守しやすくすることです。Apifox よりも利便性が低い気がするので、ここでは Apifox を使用します。

4.2闊歩する

1. POM は Swagger ソリューションの Knife4j 座標をインポートします

		<dependency>
			<groupId>com.github.xiaoymin</groupId>
			<artifactId>knife4j-spring-boot-starter</artifactId>
			<version>3.0.2</version>
		</dependency>

2. kknife4j 関連の設定 (WebMvcConfig) をインポートします
。WebMvcConfig 設定クラスの下の swagger ドキュメント関数を開き、次の注釈を追加します。

@EnableKnife4j
@Configuration

次の 2 つのメソッドを定義します

@Bean
    public Docket createRestApi() {
    
    
        // 文档类型
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.controller"))//扫描controller包下的所有api接口
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
    
    
        return new ApiInfoBuilder()
                .title("瑞吉外卖")
                .version("1.0")
                .description("瑞吉外卖接口文档")
                .build();
    }

3. 静的リソース マッピングを設定します。そうでないと、インターフェイス ドキュメント ページにアクセスできません。
次の 2 行のコードを addResourceHandlers メソッドに追加するだけです。

        registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");

ここに画像の説明を挿入

4. LoginCheckFilterのフィルタ設定でこれらのURLを許可し、処理不要なリクエストパスとして設定する
以下のURLをLoginCheckFilterのリリースリストに追加

                "/doc.html",
                "/webjars/**",
                "/swagger-resources",
                "/v2/api-docs"

プロジェクトが開始されたら、http://localhost:8080/doc.html に直接アクセスして確認できます。

ここに画像の説明を挿入

ここに画像の説明を挿入

ここに画像の説明を挿入

4.3 プロジェクトの展開

導入アーキテクチャ
![ここに画像の説明を挿入](https://img-blog.csdnimg.cn/5e6e464e0a164384a34bd15f01163891.png)

導入環境の手順、3 台のサーバー
導入環境の説明

フロントエンド プロジェクトのデプロイメント
1. フロントエンド サーバーに nginx をインストールし、nginx の下の html ディレクトリにフロントエンド リソースをアップロードします。

2. conf ディレクトリ内の nginx.conf ファイルを設定します。
設定情報は次のとおりです。

server{
    
    
  listen 80;
  server_name localhost;
#静态资源配置
  location /{
    
    
    root html/dist;
    index index.html;
  }
#请求转发代理,重写URL+转发
  location ^~ /api/{
    
    
          rewrite ^/api/(.*)$ /$1 break;
          proxy_pass http://后端服务ip:端口号;
  }
#其他
  error_page 500 502 503 504 /50x.html;
  location = /50x.html{
    
    
      root html;
  }
}

ここに画像の説明を挿入

リバースプロキシの構成分析
ここに画像の説明を挿入

バックエンド プロジェクトのデプロイメント
スクリプトを使用して自動的にデプロイメントします
。 1. バックエンド サーバーは、jdk、maven、git、mysql をインストールし、git ウェアハウスからプロジェクトのクローンを作成する必要があります。

git clone 远程仓库的url

2. reggieStart スクリプトを追加します。具体的なコードは次のとおりです。これを /usr/local/app ディレクトリに置き、スクリプトを実行してコードを自動的にプルし、パッケージ化してバックグラウンドでデプロイします。

#!/bin/sh
echo =================================
echo  自动化部署脚本启动
echo =================================

echo 停止原来运行中的工程
APP_NAME=reggie_take_out

tpid=`ps -ef|grep $APP_NAME|grep -v grep|grep -v kill|awk '{
    
    print $2}'`
if [ ${
    
    tpid} ]; then
    echo 'Stop Process...'
    kill -15 $tpid
fi
sleep 2
tpid=`ps -ef|grep $APP_NAME|grep -v grep|grep -v kill|awk '{
    
    print $2}'`
if [ ${
    
    tpid} ]; then
    echo 'Kill Process!'
    kill -9 $tpid
else
    echo 'Stop Success!'
fi

echo 准备从Git仓库拉取最新代码
cd /usr/local/app/reggie_take_out

echo 开始从Git仓库拉取最新代码
git pull
echo 代码拉取完成

echo 开始打包
output=`mvn clean package -Dmaven.test.skip=true`

cd target

echo 启动项目
nohup java -jar reggie_take_out-0.0.1-SNAPSHOT.jar &> server.log &
echo 项目启动完成

スクリプトに実行権限を付与します

chmod 777 reggieStart.sh

スクリプトを実行して、バックグラウンド プロジェクトのデプロイメントを完了します。特定の jar パッケージ名は、プロジェクト フォルダーの下のターゲット ディレクトリにあります
ここに画像の説明を挿入

知らず知らずのうちに5wも書いてしまいました。後からプロジェクトについて新たな理解があれば補足・改良していきます。個人レベルなので至らない点はご容赦ください。修正していただきありがとうございます。読む。
ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/giveupgivedown/article/details/128708122