Java项目实战笔记--基于SpringBoot3.0开发仿12306高并发售票系统--(二)项目实现-第三篇-基础业务开发

本文参考自

Springboot3+微服务实战12306高性能售票系统 - 慕课网 (imooc.com)

本文是仿12306项目实战第(二)章——项目实现 的第三篇,本篇为12306基础功能的实现,包括会员数据,火车基础数据等的增删改查;讲解如何使用自制前后端代码生成器提高开发效率

一、会员基础功能的实现

1.详解乘车人表的设计

  • 增加乘车人表

    sql/member.sql

    drop table if exists `passenger`;
    create table `passenger` (
                                 `id` bigint not null comment 'id',
                                 `member_id` bigint not null comment '会员id',
                                 `name` varchar(20) not null comment '姓名',
                                 `id_card` varchar(18) not null comment '身份证',
                                 `type` char(1) not null comment '旅客类型|枚举[PassengerTypeEnum]',
                                 `create_time` datetime(3) comment '新增时间',
                                 `update_time` datetime(3) comment '修改时间',
                                 primary key (`id`),
                                 index `member_id_index` (`member_id`)
    ) engine=innodb default charset=utf8mb4 comment='乘车人';
    

    注意:这里旅客类型type,在后面代码会使用到一个枚举类去表示旅客类型

  • 生成代码

    修改生成器配置

    generator/src/main/resources/generator-config-member.xml

    <!--        <table tableName="member" domainObjectName="Member"/>-->
            <table tableName="passenger" domainObjectName="Passenger"/>
    
  • 配置maven命令快捷指令

在这里插入图片描述

在这里插入图片描述

点击生成代码:

在这里插入图片描述

  • 增加乘车人类型枚举类

    com.neilxu.train.member.enums.PassengerTypeEnum

    package com.neilxu.train.member.enums;
    
    import java.util.ArrayList;
    import java.util.EnumSet;
    import java.util.HashMap;
    import java.util.List;
    
    public enum PassengerTypeEnum {
    
        ADULT("1", "成人"),
        CHILD("2", "儿童"),
        STUDENT("3", "学生");
    
        private String code;
    
        private String desc;
    
        PassengerTypeEnum(String code, String desc) {
            this.code = code;
            this.desc = desc;
        }
    
        public String getCode() {
            return code;
        }
    
        public void setCode(String code) {
            this.code = code;
        }
    
        public void setDesc(String desc) {
            this.desc = desc;
        }
    
        public String getDesc() {
            return desc;
        }
    
        public static List<HashMap<String,String>> getEnumList() {
            List<HashMap<String, String>> list = new ArrayList<>();
            for (PassengerTypeEnum anEnum : EnumSet.allOf(PassengerTypeEnum.class)) {
                HashMap<String, String> map = new HashMap<>();
                map.put("code",anEnum.code);
                map.put("desc",anEnum.desc);
                list.add(map);
            }
            return list;
        }
    }
    

2.增加乘车人新增接口

  • 增加请求实体类

    com.neilxu.train.member.req.PassengerSaveReq

    lombok和自己写的方法重名时,自己写的优先级更高

    package com.neilxu.train.member.req;
    
    import jakarta.validation.constraints.NotBlank;
    import jakarta.validation.constraints.NotNull;
    import lombok.Data;
    
    import java.util.Date;
    
    @Data
    public class PassengerSaveReq {
        private Long id;
    
        @NotNull(message = "【会员ID】不能为空")
        private Long memberId;
    
        @NotBlank(message = "【名字】不能为空")
        private String name;
    
        @NotBlank(message = "【身份证】不能为空")
        private String idCard;
    
        @NotBlank(message = "【旅客类型】不能为空")
        private String type;
    
        private Date createTime;
    
        private Date updateTime;
    
        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append(getClass().getSimpleName());
            sb.append(" [");
            sb.append("Hash = ").append(hashCode());
            sb.append(", id=").append(id);
            sb.append(", memberId=").append(memberId);
            sb.append(", name=").append(name);
            sb.append(", idCard=").append(idCard);
            sb.append(", type=").append(type);
            sb.append(", createTime=").append(createTime);
            sb.append(", updateTime=").append(updateTime);
            sb.append("]");
            return sb.toString();
        }
    }
    
  • service

    com.neilxu.train.member.service.PassengerService

    package com.neilxu.train.member.service;
    
    import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.date.DateTime;
    import com.neilxu.train.common.util.SnowUtil;
    import com.neilxu.train.member.domain.Passenger;
    import com.neilxu.train.member.mapper.PassengerMapper;
    import com.neilxu.train.member.req.PassengerSaveReq;
    import jakarta.annotation.Resource;
    import org.springframework.stereotype.Service;
    
    @Service
    public class PassengerService {
    
        @Resource
        private PassengerMapper passengerMapper;
    
        public void save(PassengerSaveReq req) {
            DateTime now = DateTime.now();
            Passenger passenger = BeanUtil.copyProperties(req, Passenger.class);
            passenger.setId(SnowUtil.getSnowflakeNextId());
            passenger.setCreateTime(now);
            passenger.setUpdateTime(now);
            passengerMapper.insert(passenger);
        }
    }
    

    这里注意请求参数没有带的值,后端插入表之前要先set

  • controller

    com.neilxu.train.member.controller.PassengerController

    package com.neilxu.train.member.controller;
    
    import com.neilxu.train.common.resp.CommonResp;
    import com.neilxu.train.member.req.PassengerSaveReq;
    import com.neilxu.train.member.service.PassengerService;
    import jakarta.annotation.Resource;
    import jakarta.validation.Valid;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/passenger")
    public class PassengerController {
    
        @Resource
        private PassengerService passengerService;
    
        @PostMapping("/save")
        public CommonResp<Object> save(@Valid @RequestBody PassengerSaveReq req) {
            passengerService.save(req);
            return new CommonResp<>();
        }
    
    }
    
  • 测试

    http/member-passenger.http

    POST http://localhost:8001/member/passenger/save
    Content-Type: application/json
    
    {
      "memberId": "1",
      "name": "test",
      "idCard": "123321",
      "type": "1"
    }
    
    ###
    

在这里插入图片描述

在这里插入图片描述

3.使用IDEA自带的HttpClient记住登录信息

  • 增加全局变量

    登录后保存token到全局变量token

    http/member-test.http

    POST http://localhost:8000/member/member/login
    Content-Type: application/json
    
    {
      "mobile": "13000000001",
      "code": "8888"
    }
    
    > {%
    client.log(JSON.stringify(response.body));
    client.log(JSON.stringify(response.body.content.token));
    client.global.set("token", response.body.content.token);
    %}
    
    ###
    
  • 修改其他需要登录的测试接口

    {{token}} 获取全局变量token

    POST http://localhost:8000/member/passenger/save
    Content-Type: application/json
    token: {{token}}
    
    {
      "memberId": "1",
      "name": "test",
      "idCard": "123321",
      "type": "1"
    }
    
    ###
    
  • 测试

    先调用登录

在这里插入图片描述

网关8000端口访问save接口

在这里插入图片描述

4.使用线程本地变量存储会员信息

  • common新建类来使用本地变量

    com.neilxu.train.common.context.LoginMemberContext

    package com.neilxu.train.common.context;
    
    import com.neilxu.train.common.resp.MemberLoginResp;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    public class LoginMemberContext {
        private static final Logger LOG = LoggerFactory.getLogger(LoginMemberContext.class);
    
        private static ThreadLocal<MemberLoginResp> member = new ThreadLocal<>();
    
        public static MemberLoginResp getMember() {
            return member.get();
        }
    
        public static void setMember(MemberLoginResp member) {
            LoginMemberContext.member.set(member);
        }
    
        public static Long getId() {
            try {
                return member.get().getId();
            } catch (Exception e) {
                LOG.error("获取登录会员信息异常", e);
                throw e;
            }
        }
    
    }
    

    **注意:**这里将MemberLoginResp.java复制了一份到common中

  • 配置拦截器

    • 新增拦截器类

      com.neilxu.train.common.interceptor.MemberInterceptor

      package com.neilxu.train.common.interceptor;
      
      import cn.hutool.core.util.StrUtil;
      import cn.hutool.json.JSONObject;
      import cn.hutool.json.JSONUtil;
      import com.neilxu.train.common.util.JwtUtil;
      import com.neilxu.train.common.context.LoginMemberContext;
      import com.neilxu.train.common.resp.MemberLoginResp;
      import jakarta.servlet.http.HttpServletRequest;
      import jakarta.servlet.http.HttpServletResponse;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      import org.springframework.stereotype.Component;
      import org.springframework.web.servlet.HandlerInterceptor;
      
      /**
       * 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印
       */
      @Component
      public class MemberInterceptor implements HandlerInterceptor {
      
          private static final Logger LOG = LoggerFactory.getLogger(MemberInterceptor.class);
      
          @Override
          public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
              //获取header的token参数
              String token = request.getHeader("token");
              if (StrUtil.isNotBlank(token)) {
                  LOG.info("获取会员登录token:{}", token);
                  JSONObject loginMember = JwtUtil.getJSONObject(token);
                  LOG.info("当前登录会员:{}", loginMember);
                  MemberLoginResp member = JSONUtil.toBean(loginMember, MemberLoginResp.class);
                  LoginMemberContext.setMember(member);
              }
              return true;
          }
      
      }
      
    • 开启拦截器

      com.neilxu.train.common.config.SpringMvcConfig

      package com.neilxu.train.common.config;
      
      import com.neilxu.train.common.interceptor.MemberInterceptor;
      import jakarta.annotation.Resource;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
      import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
      
      @Configuration
      public class SpringMvcConfig implements WebMvcConfigurer {
      
          @Resource
          MemberInterceptor memberInterceptor;
      
          @Override
          public void addInterceptors(InterceptorRegistry registry) {
              registry.addInterceptor(memberInterceptor)
                      .addPathPatterns("/**")
                      .excludePathPatterns(
                              "/member/hello",
                              "/member/member/send-code",
                              "/member/member/login"
                      );
          }
      }
      
    • 测试

      最终效果:save乘客的时候,无需再传递memberId,而是从线程本地变量获取

      http/member-passenger.http

      POST http://localhost:8000/member/passenger/save
      Content-Type: application/json
      token: {{token}}
      
      {
        "name": "test",
        "idCard": "123321",
        "type": "1"
      }
      
      ###
      

在这里插入图片描述

  • 解决拦截器没有日志流水号的问题

    由于拦截器先拦截,之后再走的aop,所以没有日志流水号

    解决办法:增加日志拦截器

    com.neilxu.train.common.interceptor.LogInterceptor

    package com.neilxu.train.common.interceptor;
    
    import cn.hutool.core.util.RandomUtil;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import org.slf4j.MDC;
    import org.springframework.stereotype.Component;
    import org.springframework.web.servlet.HandlerInterceptor;
    
    /**
     * 日志拦截器
     */
    @Component
    public class LogInterceptor implements HandlerInterceptor {
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 增加日志流水号
            MDC.put("LOG_ID", System.currentTimeMillis() + RandomUtil.randomString(3));
            return true;
        }
    
    }
    

    开启日志拦截器

    com.neilxu.train.common.config.SpringMvcConfig

    package com.neilxu.train.common.config;
    
    import com.neilxu.train.common.interceptor.LogInterceptor;
    import com.neilxu.train.common.interceptor.MemberInterceptor;
    import jakarta.annotation.Resource;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    public class SpringMvcConfig implements WebMvcConfigurer {
    
        @Resource
        LogInterceptor logInterceptor;
    
        @Resource
        MemberInterceptor memberInterceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(logInterceptor);
    
            registry.addInterceptor(memberInterceptor)
                    .addPathPatterns("/**")
                    .excludePathPatterns(
                            "/member/hello",
                            "/member/member/send-code",
                            "/member/member/login"
                    );
        }
    }
    

    去掉aop里的增加日志流水号逻辑

    com.neilxu.train.common.aspect.LogAspect

    //        // 增加日志流水号
    //        MDC.put("LOG_ID", System.currentTimeMillis() + RandomUtil.randomString(3));
    

    测试效果:

在这里插入图片描述

5.前端二级路由页面开发

  • 制作欢迎页面

    • 修改路由

      web/src/router/index.js

      const routes = [
        {
          path: '/login',
          component: () => import('../views/login.vue')
        },
        {
          path: '/',
          component: () => import('../views/main.vue'),
          meta: {
            loginRequire: true
          },
          children: [{
            path: 'welcome',
            component: () => import('../views/main/welcome.vue'),
          }]
        },
        {
          path: '',
          redirect: '/welcome'
        },
      ]
      
    • 修改main.vue,只保留header和sider,内容空出,放动态页面

      web/src/views/main.vue

      <template>
        <a-layout id="components-layout-demo-top-side-2">
          <the-header-view></the-header-view>
          <a-layout>
            <the-sider-view></the-sider-view>
            <a-layout-content
                :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
            >
              <router-view></router-view>
            </a-layout-content>
          </a-layout>
        </a-layout>
      </template>
      <script>
      import { defineComponent } from 'vue';
      import TheHeaderView from "@/components/the-header";
      import TheSiderView from "@/components/the-sider";
      
      export default defineComponent({
        components: {
          TheSiderView,
          TheHeaderView,
        },
        setup() {
          return {
          };
        },
      });
      </script>
      <style>
      #components-layout-demo-top-side-2 .logo {
        float: left;
        width: 120px;
        height: 31px;
        margin: 16px 24px 16px 0;
        background: rgba(255, 255, 255, 0.3);
      }
      
      .ant-row-rtl #components-layout-demo-top-side-2 .logo {
        float: right;
        margin: 16px 0 16px 24px;
      }
      
      .site-layout-background {
        background: #fff;
      }
      </style>
      
    • 增加welcome.vue

      web/src/views/main/welcome.vue

      <template>
        <h1>欢迎使用甲蛙12306售票系统</h1>
      </template>
      <script>
      import { defineComponent } from 'vue';
      
      export default defineComponent({
        setup() {
          return {
          };
        },
      });
      </script>
      <style>
      </style>
      
    • 修改login.vue

      web/src/views/login.vue

      const login = () => {
        axios.post("/member/member/login", loginForm).then(response => {
          let data = response.data;
          if (data.success) {
            notification.success({ description: '登录成功!' });
            // 登录成功,跳到控台主页
            router.push("/welcome");
            // store保存登录信息
            store.commit("setMember", data.content);
          } else {
            notification.error({ description: data.message });
          }
        })
      };
      

      这里改不改都可以(上面已经将访问根路径“/”重定向到了welcom)

    • 测试效果

在这里插入图片描述

  • 增加乘车人管理页面

    • 修改路由

      web/src/router/index.js

      import { createRouter, createWebHistory } from 'vue-router'
      import store from "@/store";
      import {notification} from "ant-design-vue";
      
      const routes = [{
        path: '/login',
        component: () => import('../views/login.vue')
      }, {
        path: '/',
        component: () => import('../views/main.vue'),
        meta: {
          loginRequire: true
        },
        children: [{
          path: 'welcome',
          component: () => import('../views/main/welcome.vue'),
        }, {
          path: 'passenger',
          component: () => import('../views/main/passenger.vue'),
        }]
      }, {
        path: '',
        redirect: '/welcome'
      }];
      
      const router = createRouter({
        history: createWebHistory(process.env.BASE_URL),
        routes
      })
      
      // 路由登录拦截
      router.beforeEach((to, from, next) => {
        // 要不要对meta.loginRequire属性做监控拦截
        if (to.matched.some(function (item) {
          console.log(item, "是否需要登录校验:", item.meta.loginRequire || false);
          return item.meta.loginRequire
        })) {
          const _member = store.state.member;
          console.log("页面登录校验开始:", _member);
          if (!_member.token) {
            console.log("用户未登录或登录超时!");
            notification.error({ description: "未登录或登录超时" });
            next('/login');
          } else {
            next();
          }
        } else {
          next();
        }
      });
      
      export default router
      
    • 增加passenger.vue

      web/src/views/main/passenger.vue

      <template>
        <h1>乘车人管理</h1>
      </template>
      <script>
      import { defineComponent } from 'vue';
      
      export default defineComponent({
        setup() {
          return {
          };
        },
      });
      </script>
      <style>
      </style>
      
    • 测试效果

在这里插入图片描述

  • 修改菜单,实现页面切换

    • 修改the-header.vue

      <template>
        <a-layout-header class="header">
          <div class="logo" />
          <div style="float: right; color: white;">
            您好:{{member.mobile}} &nbsp;&nbsp;
            <router-link to="/login" style="color: white;">
              退出登录
            </router-link>
          </div>
          <a-menu
              theme="dark"
              mode="horizontal"
              :style="{ lineHeight: '64px' }"
          >
            <a-menu-item key="/welcome">
              <router-link to="/welcome">
                <coffee-outlined /> &nbsp; 欢迎
              </router-link>
            </a-menu-item>
            <a-menu-item key="/passenger">
              <router-link to="/passenger">
                <user-outlined /> &nbsp; 乘车人管理
              </router-link>
            </a-menu-item>
          </a-menu>
        </a-layout-header>
      </template>
      
      <script>
      import {defineComponent} from 'vue';
      import store from "@/store";
      
      export default defineComponent({
        name: "the-header-view",
        setup() {
          let member = store.state.member;
      
          return {
            member
          };
        },
      });
      </script>
      
      <!-- Add "scoped" attribute to limit CSS to this component only -->
      <style scoped>
      
      </style>
      
    • 修改the-sider.vue

      <template>
        <a-layout-sider width="200" style="background: #fff">
          <a-menu
              mode="inline"
              :style="{ height: '100%', borderRight: 0 }"
          >
            <a-menu-item key="/welcome">
              <router-link to="/welcome">
                <coffee-outlined /> &nbsp; 欢迎
              </router-link>
            </a-menu-item>
            <a-menu-item key="/passenger">
              <router-link to="/passenger">
                <user-outlined /> &nbsp; 乘车人管理
              </router-link>
            </a-menu-item>
          </a-menu>
        </a-layout-sider>
      </template>
      
      <script>
      import {defineComponent} from 'vue';
      
      export default defineComponent({
        name: "the-sider-view",
        setup() {
      
          return {
          };
        },
      });
      </script>
      
      <!-- Add "scoped" attribute to limit CSS to this component only -->
      <style scoped>
      
      </style>
      
    • 测试效果

在这里插入图片描述

问题:可以跳转,但是导航栏和侧边栏的激活状态不同步
  • 实现菜单同步激活

    声明响应式变量:

    ref用来声明基本的数据类型
    reactive用来声明对象或对象数组

    对ref变量的取值、赋值都必须加:.value

    **原理:**监听路由变化,来给selectedKeys赋值

    • the-header.vue

      <template>
        <a-layout-header class="header">
          <div class="logo" />
          <div style="float: right; color: white;">
            您好:{{member.mobile}} &nbsp;&nbsp;
            <router-link to="/login" style="color: white;">
              退出登录
            </router-link>
          </div>
          <a-menu
              v-model:selectedKeys="selectedKeys"
              theme="dark"
              mode="horizontal"
              :style="{ lineHeight: '64px' }"
          >
            <a-menu-item key="/welcome">
              <router-link to="/welcome">
                <coffee-outlined /> &nbsp; 欢迎
              </router-link>
            </a-menu-item>
            <a-menu-item key="/passenger">
              <router-link to="/passenger">
                <user-outlined /> &nbsp; 乘车人管理
              </router-link>
            </a-menu-item>
          </a-menu>
        </a-layout-header>
      </template>
      
      <script>
      import {defineComponent, ref, watch} from 'vue';
      import store from "@/store";
      import router from '@/router'
      
      export default defineComponent({
        name: "the-header-view",
        setup() {
          let member = store.state.member;
          const selectedKeys = ref([]);
      
          watch(() => router.currentRoute.value.path, (newValue) => {
            console.log('watch', newValue);
            selectedKeys.value = [];
            selectedKeys.value.push(newValue);
          }, {immediate: true});
          return {
            member,
            selectedKeys
          };
        },
      });
      </script>
      
      <!-- Add "scoped" attribute to limit CSS to this component only -->
      <style scoped>
      
      </style>
      
    • the-sider.vue

      <template>
        <a-layout-sider width="200" style="background: #fff">
          <a-menu
              v-model:selectedKeys="selectedKeys"
              mode="inline"
              :style="{ height: '100%', borderRight: 0 }"
          >
            <a-menu-item key="/welcome">
              <router-link to="/welcome">
                <coffee-outlined /> &nbsp; 欢迎
              </router-link>
            </a-menu-item>
            <a-menu-item key="/passenger">
              <router-link to="/passenger">
                <user-outlined /> &nbsp; 乘车人管理
              </router-link>
            </a-menu-item>
          </a-menu>
        </a-layout-sider>
      </template>
      
      <script>
      import {defineComponent, ref, watch} from 'vue';
      import router from "@/router";
      
      export default defineComponent({
        name: "the-sider-view",
        setup() {
          const selectedKeys = ref([]);
      
          watch(() => router.currentRoute.value.path, (newValue) => {
            console.log('watch', newValue);
            selectedKeys.value = [];
            selectedKeys.value.push(newValue);
          }, {immediate: true});
          return {
            selectedKeys
          };
        },
      });
      </script>
      
      <!-- Add "scoped" attribute to limit CSS to this component only -->
      <style scoped>
      
      </style>
      
    • 测试效果

在这里插入图片描述

6.乘车人新增界面开发

此节为纯前端,直接放代码

web/src/views/main/passenger.vue

<template>
  <a-button type="primary" @click="showModal">新增</a-button>
  <a-modal v-model:visible="visible" title="乘车人" @ok="handleOk"
           ok-text="确认" cancel-text="取消">
    <a-form :model="passenger" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
      <a-form-item label="姓名">
        <a-input v-model:value="passenger.name" />
      </a-form-item>
      <a-form-item label="身份证">
        <a-input v-model:value="passenger.idCard" />
      </a-form-item>
      <a-form-item label="类型">
        <a-select v-model:value="passenger.type">
          <a-select-option value="1">成人</a-select-option>
          <a-select-option value="2">儿童</a-select-option>
          <a-select-option value="3">学生</a-select-option>
        </a-select>
      </a-form-item>
    </a-form>
  </a-modal>
</template>
<script>
import { defineComponent, ref, reactive } from 'vue';
import {notification} from "ant-design-vue";
import axios from "axios";

export default defineComponent({
  setup() {
    const visible = ref(false);
    const passenger = reactive({
      id: undefined,
      memberId: undefined,
      name: undefined,
      idCard: undefined,
      type: undefined,
      createTime: undefined,
      updateTime: undefined,
    });

    const showModal = () => {
      visible.value = true;
    };

    const handleOk = () => {
      axios.post("/member/passenger/save", passenger).then((response) => {
        let data = response.data;
        if (data.success) {
          notification.success({description: "保存成功!"});
          visible.value = false;
        } else {
          notification.error({description: data.message});
        }
      });
    };

    return {
      passenger,
      visible,
      showModal,
      handleOk,
    };
  },
});
</script>
<style>
</style>

效果

在这里插入图片描述

在这里插入图片描述

7.乘车人列表查询接口开发

  • 新增查询乘客请求实体类

    com.neilxu.train.member.req.PassengerQueryReq

    package com.neilxu.train.member.req;
    
    import lombok.Data;
    
    @Data
    public class PassengerQueryReq {
    
        private Long memberId;
        
    }
    
  • 新增查询乘客返回结果类

    com.neilxu.train.member.resp.PassengerQueryResp

    package com.neilxu.train.member.resp;
    
    import lombok.Data;
    
    import java.util.Date;
    
    @Data
    public class PassengerQueryResp {
        private Long id;
    
        private Long memberId;
    
        private String name;
    
        private String idCard;
    
        private String type;
    
        private Date createTime;
    
        private Date updateTime;
    
    }
    
  • service方法

    com.neilxu.train.member.service.PassengerService

    public List<PassengerQueryResp> queryList(PassengerQueryReq req) {
        PassengerExample passengerExample = new PassengerExample();
        PassengerExample.Criteria criteria = passengerExample.createCriteria();
        if (ObjectUtil.isNotNull(req.getMemberId())) {
            criteria.andMemberIdEqualTo(req.getMemberId());
        }
        List<Passenger> passengerList = passengerMapper.selectByExample(passengerExample);
        return BeanUtil.copyToList(passengerList, PassengerQueryResp.class);
    }
    

    这里注意passengerExample只能createCriteria一次,所以需要提取出来到if外面

  • controller

    com.neilxu.train.member.controller.PassengerController

    @GetMapping("/query-list")
    public CommonResp<List<PassengerQueryResp>> queryList(@Valid PassengerQueryReq req) {
        req.setMemberId(LoginMemberContext.getId());
        List<PassengerQueryResp> list = passengerService.queryList(req);
        return new CommonResp<>(list);
    }
    
  • 测试

    http/member-passenger.http

    GET http://localhost:8000/member/passenger/query-list
    Accept: application/json
    token: {{token}}
    
    ###
    

在这里插入图片描述
在这里插入图片描述

8.集成PageHelper实现后端分页,封装分页请求参数和返回结果

MyBatis PageHelper是一个用于在MyBatis中实现分页功能的工具库。它通过拦截器的方式来对SQL语句进行拦截和修改,从而实现分页功能。其原理主要包括以下几个步骤:

  1. 拦截器机制:PageHelper通过实现MyBatis的拦截器接口,在SQL语句执行前对其进行拦截和修改。这样可以在不修改业务代码的情况下,实现对SQL语句的分页处理。
  2. 解析分页参数:PageHelper拦截器在执行前会解析传入的分页参数,如页码和每页数据条数等,确定需要分页的SQL语句以及分页参数的值。
  3. 修改SQL语句:根据解析得到的分页参数,PageHelper会修改原始的SQL语句,添加对应的分页逻辑,通常是在原始SQL语句的末尾添加LIMIT(MySQL)或者ROWNUM(Oracle)等关键字来限制结果集的返回条数。
  4. 执行分页查询:修改后的SQL语句会被执行,从数据库中查询数据。由于修改后的SQL语句包含了分页逻辑,因此只会返回符合分页条件的数据。
  5. 返回分页结果:查询结果会被封装成分页对象,并返回给调用方。这个分页对象通常包含了总记录数、当前页码、总页数以及查询结果等信息,方便业务代码进行分页展示和处理。

总的来说,MyBatis PageHelper的原理就是通过拦截器机制,在SQL语句执行前对其进行修改,添加分页逻辑,从而实现对查询结果的分页处理。

​ -------------------来自ChatGPT的回答

  • 集成PageHelper,实现sql分页功能

    • common增加依赖

      父pom

      <dependency>
          <groupId>com.github.pagehelper</groupId>
          <artifactId>pagehelper-spring-boot-starter</artifactId>
          <version>1.4.6</version>
      </dependency>
      

      common的 pom文件

      <!-- 分页插件pagehelper -->
      <dependency>
          <groupId>com.github.pagehelper</groupId>
          <artifactId>pagehelper-spring-boot-starter</artifactId>
      </dependency>
      
    • 修改service方法

      com.neilxu.train.member.service.PassengerService

      public List<PassengerQueryResp> queryList(PassengerQueryReq req) {
          PassengerExample passengerExample = new PassengerExample();
          PassengerExample.Criteria criteria = passengerExample.createCriteria();
          if (ObjectUtil.isNotNull(req.getMemberId())) {
              criteria.andMemberIdEqualTo(req.getMemberId());
          }
          PageHelper.startPage(1, 2);
          List<Passenger> passengerList = passengerMapper.selectByExample(passengerExample);
          return BeanUtil.copyToList(passengerList, PassengerQueryResp.class);
      }
      
    • 测试

在这里插入图片描述

  • 封装分页请求参数

    • PageReq.java

      com.neilxu.train.common.req.PageReq

      package com.neilxu.train.common.req;
      
      import jakarta.validation.constraints.Max;
      import jakarta.validation.constraints.NotNull;
      
      public class PageReq {
      
          @NotNull(message = "【页码】不能为空")
          private Integer page;
      
          @NotNull(message = "【每页条数】不能为空")
          @Max(value = 100, message = "【每页条数】不能超过100")
          private Integer size;
      
          public Integer getPage() {
              return page;
          }
      
          public void setPage(Integer page) {
              this.page = page;
          }
      
          public Integer getSize() {
              return size;
          }
      
          public void setSize(Integer size) {
              this.size = size;
          }
      
          @Override
          public String toString() {
              return "PageReq{" +
                      "page=" + page +
                      ", size=" + size +
                      '}';
          }
      }
      
    • 修改PassengerQueryReq.java

      com.neilxu.train.member.req.PassengerQueryReq

      @Data
      public class PassengerQueryReq extends PageReq {
      
          private Long memberId;
      
      }
      
    • 修改service

      com.neilxu.train.member.service.PassengerService

      public List<PassengerQueryResp> queryList(PassengerQueryReq req) {
          PassengerExample passengerExample = new PassengerExample();
          PassengerExample.Criteria criteria = passengerExample.createCriteria();
          if (ObjectUtil.isNotNull(req.getMemberId())) {
              criteria.andMemberIdEqualTo(req.getMemberId());
          }
          PageHelper.startPage(req.getPage(), req.getSize());
          List<Passenger> passengerList = passengerMapper.selectByExample(passengerExample);
          return BeanUtil.copyToList(passengerList, PassengerQueryResp.class);
      }
      
    • 测试

      http/member-passenger.http

      GET http://localhost:8000/member/passenger/query-list?page=2&size=200
      Accept: application/json
      token: {{token}}
      
      ###
      

在这里插入图片描述

  • 封装分页返回结果

    • PageResp.java

      com.neilxu.train.common.resp.PageResp

      package com.neilxu.train.common.resp;
      
      import java.io.Serializable;
      import java.util.List;
      
      public class PageResp<T> implements Serializable {
      
          /**
           * 总条数
           */
          private Long total;
      
          /**
           * 当前页的列表
           */
          private List<T> list;
      
          public Long getTotal() {
              return total;
          }
      
          public void setTotal(Long total) {
              this.total = total;
          }
      
          public List<T> getList() {
              return list;
          }
      
          public void setList(List<T> list) {
              this.list = list;
          }
      
          @Override
          public String toString() {
              return "PageResp{" +
                      "total=" + total +
                      ", list=" + list +
                      '}';
          }
      }
      
    • 修改service

      com.neilxu.train.member.service.PassengerService

      public PageResp<PassengerQueryResp> queryList(PassengerQueryReq req) {
          PassengerExample passengerExample = new PassengerExample();
          PassengerExample.Criteria criteria = passengerExample.createCriteria();
          if (ObjectUtil.isNotNull(req.getMemberId())) {
              criteria.andMemberIdEqualTo(req.getMemberId());
          }
      
          LOG.info("查询页码:{}", req.getPage());
          LOG.info("每页条数:{}", req.getSize());
          PageHelper.startPage(req.getPage(), req.getSize());
          List<Passenger> passengerList = passengerMapper.selectByExample(passengerExample);
      
          PageInfo<Passenger> pageInfo = new PageInfo<>(passengerList);
          LOG.info("总行数:{}", pageInfo.getTotal());
          LOG.info("总页数:{}", pageInfo.getPages());
      
          List<PassengerQueryResp> list = BeanUtil.copyToList(passengerList, PassengerQueryResp.class);
      
          PageResp<PassengerQueryResp> pageResp = new PageResp<>();
          pageResp.setTotal(pageInfo.getTotal());
          pageResp.setList(list);
          return pageResp;
      }
      
    • 修改controller

      com.neilxu.train.member.controller.PassengerController

      @GetMapping("/query-list")
      public CommonResp<PageResp<PassengerQueryResp>> queryList(@Valid PassengerQueryReq req) {
          req.setMemberId(LoginMemberContext.getId());
          PageResp<PassengerQueryResp> list = passengerService.queryList(req);
          return new CommonResp<>(list);
      }
      
    • 测试

在这里插入图片描述

  • 格式化返回的日期字段

    修改PassengerQueryResp.java

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;
    
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date updateTime;
    

    效果:

在这里插入图片描述

变成

在这里插入图片描述

9.乘车人列表查询界面开发

  • 乘车人列表查询界面开发,引入表格组件

    修改passenger.vue

    web/src/views/main/passenger.vue

    <template>
      <p>
        <a-button type="primary" @click="showModal">新增</a-button>
      </p>
      <a-table :dataSource="dataSource" :columns="columns" />
      <a-modal v-model:visible="visible" title="乘车人" @ok="handleOk"
               ok-text="确认" cancel-text="取消">
        <a-form :model="passenger" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
          <a-form-item label="姓名">
            <a-input v-model:value="passenger.name" />
          </a-form-item>
          <a-form-item label="身份证">
            <a-input v-model:value="passenger.idCard" />
          </a-form-item>
          <a-form-item label="类型">
            <a-select v-model:value="passenger.type">
              <a-select-option value="1">成人</a-select-option>
              <a-select-option value="2">儿童</a-select-option>
              <a-select-option value="3">学生</a-select-option>
            </a-select>
          </a-form-item>
        </a-form>
      </a-modal>
    </template>
    <script>
    import { defineComponent, ref, reactive } from 'vue';
    import {notification} from "ant-design-vue";
    import axios from "axios";
    
    export default defineComponent({
      setup() {
        const visible = ref(false);
        const passenger = reactive({
          id: undefined,
          memberId: undefined,
          name: undefined,
          idCard: undefined,
          type: undefined,
          createTime: undefined,
          updateTime: undefined,
        });
        const dataSource = [{
          key: '1',
          name: '胡彦斌',
          age: 32,
          address: '西湖区湖底公园1号',
        }, {
          key: '2',
          name: '胡彦祖',
          age: 42,
          address: '西湖区湖底公园1号',
        }];
        const columns = [{
          title: '姓名',
          dataIndex: 'name',
          key: 'name',
        }, {
          title: '年龄',
          dataIndex: 'age',
          key: 'age',
        }, {
          title: '住址',
          dataIndex: 'address',
          key: 'address',
        }];
    
        const showModal = () => {
          visible.value = true;
        };
    
        const handleOk = () => {
          axios.post("/member/passenger/save", passenger).then((response) => {
            let data = response.data;
            if (data.success) {
              notification.success({description: "保存成功!"});
              visible.value = false;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        return {
          passenger,
          visible,
          showModal,
          handleOk,
          dataSource,
          columns
        };
      },
    });
    </script>
    <style>
    </style>
    

    效果

在这里插入图片描述

  • 乘车人列表查询界面开发,使用axios调用后端查询接口

    修改passenger.vue

    web/src/views/main/passenger.vue

    <template>
      <p>
        <a-button type="primary" @click="showModal">新增</a-button>
      </p>
      <a-table :dataSource="passengers" :columns="columns" />
      <a-modal v-model:visible="visible" title="乘车人" @ok="handleOk"
               ok-text="确认" cancel-text="取消">
        <a-form :model="passenger" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
          <a-form-item label="姓名">
            <a-input v-model:value="passenger.name" />
          </a-form-item>
          <a-form-item label="身份证">
            <a-input v-model:value="passenger.idCard" />
          </a-form-item>
          <a-form-item label="类型">
            <a-select v-model:value="passenger.type">
              <a-select-option value="1">成人</a-select-option>
              <a-select-option value="2">儿童</a-select-option>
              <a-select-option value="3">学生</a-select-option>
            </a-select>
          </a-form-item>
        </a-form>
      </a-modal>
    </template>
    <script>
    import { defineComponent, ref, reactive, onMounted } from 'vue';
    import {notification} from "ant-design-vue";
    import axios from "axios";
    
    export default defineComponent({
      setup() {
        const visible = ref(false);
        const passenger = reactive({
          id: undefined,
          memberId: undefined,
          name: undefined,
          idCard: undefined,
          type: undefined,
          createTime: undefined,
          updateTime: undefined,
        });
        const passengers = ref([]);
        const columns = [{
          title: '姓名',
          dataIndex: 'name',
          key: 'name',
        }, {
          title: '身份证',
          dataIndex: 'idCard',
          key: 'idCard',
        }, {
          title: '类型',
          dataIndex: 'type',
          key: 'type',
        }];
    
        const showModal = () => {
          visible.value = true;
        };
    
        const handleOk = () => {
          axios.post("/member/passenger/save", passenger).then((response) => {
            let data = response.data;
            if (data.success) {
              notification.success({description: "保存成功!"});
              visible.value = false;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleQuery = (param) => {
          axios.get("/member/passenger/query-list", {
            params: {
              page: param.page,
              size: param.size
            }
          }).then((response) => {
            let data = response.data;
            if (data.success) {
              passengers.value = data.content.list;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        onMounted(() => {
          handleQuery({
            page: 1,
            size: 2
          });
        });
    
        return {
          passenger,
          visible,
          showModal,
          handleOk,
          passengers,
          columns
        };
      },
    });
    </script>
    <style>
    </style>
    

    效果

在这里插入图片描述

  • 乘车人列表查询界面开发,显示分页页码

    修改passenger.vue

    web/src/views/main/passenger.vue

    <template>
      <p>
        <a-button type="primary" @click="showModal">新增</a-button>
      </p>
      <a-table :dataSource="passengers" :columns="columns" :pagination="pagination"/>
      <a-modal v-model:visible="visible" title="乘车人" @ok="handleOk"
               ok-text="确认" cancel-text="取消">
        <a-form :model="passenger" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
          <a-form-item label="姓名">
            <a-input v-model:value="passenger.name" />
          </a-form-item>
          <a-form-item label="身份证">
            <a-input v-model:value="passenger.idCard" />
          </a-form-item>
          <a-form-item label="类型">
            <a-select v-model:value="passenger.type">
              <a-select-option value="1">成人</a-select-option>
              <a-select-option value="2">儿童</a-select-option>
              <a-select-option value="3">学生</a-select-option>
            </a-select>
          </a-form-item>
        </a-form>
      </a-modal>
    </template>
    <script>
    import { defineComponent, ref, reactive, onMounted } from 'vue';
    import {notification} from "ant-design-vue";
    import axios from "axios";
    
    export default defineComponent({
      setup() {
        const visible = ref(false);
        const passenger = reactive({
          id: undefined,
          memberId: undefined,
          name: undefined,
          idCard: undefined,
          type: undefined,
          createTime: undefined,
          updateTime: undefined,
        });
        const passengers = ref([]);
        // 分页的三个属性名是固定的
        const pagination = reactive({
          total: 0,
          current: 1,
          pageSize: 2,
        });
        const columns = [{
          title: '姓名',
          dataIndex: 'name',
          key: 'name',
        }, {
          title: '身份证',
          dataIndex: 'idCard',
          key: 'idCard',
        }, {
          title: '类型',
          dataIndex: 'type',
          key: 'type',
        }];
    
        const showModal = () => {
          visible.value = true;
        };
    
        const handleOk = () => {
          axios.post("/member/passenger/save", passenger).then((response) => {
            let data = response.data;
            if (data.success) {
              notification.success({description: "保存成功!"});
              visible.value = false;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleQuery = (param) => {
          axios.get("/member/passenger/query-list", {
            params: {
              page: param.page,
              size: param.size
            }
          }).then((response) => {
            let data = response.data;
            if (data.success) {
              passengers.value = data.content.list;
              pagination.total = data.content.total;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        onMounted(() => {
          handleQuery({
            page: 1,
            size: 2
          });
        });
    
        return {
          passenger,
          visible,
          showModal,
          handleOk,
          passengers,
          pagination,
          columns
        };
      },
    });
    </script>
    <style>
    </style>
    

    效果

在这里插入图片描述

  • 乘车人列表查询界面开发,增加表格分页点击事件

    修改passenger.vue

    web/src/views/main/passenger.vue

    <template>
      <p>
        <a-button type="primary" @click="showModal">新增</a-button>
      </p>
      <a-table :dataSource="passengers" :columns="columns" :pagination="pagination" @change="handleTableChange"/>
      <a-modal v-model:visible="visible" title="乘车人" @ok="handleOk"
               ok-text="确认" cancel-text="取消">
        <a-form :model="passenger" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
          <a-form-item label="姓名">
            <a-input v-model:value="passenger.name" />
          </a-form-item>
          <a-form-item label="身份证">
            <a-input v-model:value="passenger.idCard" />
          </a-form-item>
          <a-form-item label="类型">
            <a-select v-model:value="passenger.type">
              <a-select-option value="1">成人</a-select-option>
              <a-select-option value="2">儿童</a-select-option>
              <a-select-option value="3">学生</a-select-option>
            </a-select>
          </a-form-item>
        </a-form>
      </a-modal>
    </template>
    <script>
    import { defineComponent, ref, reactive, onMounted } from 'vue';
    import {notification} from "ant-design-vue";
    import axios from "axios";
    
    export default defineComponent({
      setup() {
        const visible = ref(false);
        const passenger = reactive({
          id: undefined,
          memberId: undefined,
          name: undefined,
          idCard: undefined,
          type: undefined,
          createTime: undefined,
          updateTime: undefined,
        });
        const passengers = ref([]);
        // 分页的三个属性名是固定的
        const pagination = reactive({
          total: 0,
          current: 1,
          pageSize: 2,
        });
        const columns = [{
          title: '姓名',
          dataIndex: 'name',
          key: 'name',
        }, {
          title: '身份证',
          dataIndex: 'idCard',
          key: 'idCard',
        }, {
          title: '类型',
          dataIndex: 'type',
          key: 'type',
        }];
    
        const showModal = () => {
          visible.value = true;
        };
    
        const handleOk = () => {
          axios.post("/member/passenger/save", passenger).then((response) => {
            let data = response.data;
            if (data.success) {
              notification.success({description: "保存成功!"});
              visible.value = false;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleQuery = (param) => {
          axios.get("/member/passenger/query-list", {
            params: {
              page: param.page,
              size: param.size
            }
          }).then((response) => {
            let data = response.data;
            if (data.success) {
              passengers.value = data.content.list;
              // 设置分页控件的值
              pagination.current = param.page;
              pagination.total = data.content.total;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleTableChange = (pagination) => {
          // console.log("看看自带的分页参数都有啥:" + pagination);
          handleQuery({
            page: pagination.current,
            size: pagination.pageSize
          });
        };
    
        onMounted(() => {
          handleQuery({
            page: 1,
            size: pagination.pageSize
          });
        });
    
        return {
          passenger,
          visible,
          showModal,
          handleOk,
          passengers,
          pagination,
          columns,
          handleTableChange
        };
      },
    });
    </script>
    <style>
    </style>
    

    效果

在这里插入图片描述

  • 乘车人列表查询界面开发,增加刷新按钮,点击查询第1页

    同上

    <template>
      <p>
        <a-space>
          <a-button type="primary" @click="handleQuery()">刷新</a-button>
          <a-button type="primary" @click="showModal">新增</a-button>
        </a-space>
      </p>
      <a-table :dataSource="passengers" :columns="columns" :pagination="pagination" @change="handleTableChange"/>
      <a-modal v-model:visible="visible" title="乘车人" @ok="handleOk"
               ok-text="确认" cancel-text="取消">
        <a-form :model="passenger" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
          <a-form-item label="姓名">
            <a-input v-model:value="passenger.name" />
          </a-form-item>
          <a-form-item label="身份证">
            <a-input v-model:value="passenger.idCard" />
          </a-form-item>
          <a-form-item label="类型">
            <a-select v-model:value="passenger.type">
              <a-select-option value="1">成人</a-select-option>
              <a-select-option value="2">儿童</a-select-option>
              <a-select-option value="3">学生</a-select-option>
            </a-select>
          </a-form-item>
        </a-form>
      </a-modal>
    </template>
    <script>
    import { defineComponent, ref, reactive, onMounted } from 'vue';
    import {notification} from "ant-design-vue";
    import axios from "axios";
    
    export default defineComponent({
      setup() {
        const visible = ref(false);
        const passenger = reactive({
          id: undefined,
          memberId: undefined,
          name: undefined,
          idCard: undefined,
          type: undefined,
          createTime: undefined,
          updateTime: undefined,
        });
        const passengers = ref([]);
        // 分页的三个属性名是固定的
        const pagination = reactive({
          total: 0,
          current: 1,
          pageSize: 2,
        });
        const columns = [{
          title: '姓名',
          dataIndex: 'name',
          key: 'name',
        }, {
          title: '身份证',
          dataIndex: 'idCard',
          key: 'idCard',
        }, {
          title: '类型',
          dataIndex: 'type',
          key: 'type',
        }];
    
        const showModal = () => {
          visible.value = true;
        };
    
        const handleOk = () => {
          axios.post("/member/passenger/save", passenger).then((response) => {
            let data = response.data;
            if (data.success) {
              notification.success({description: "保存成功!"});
              visible.value = false;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleQuery = (param) => {
          if (!param) {
            param = {
              page: 1,
              size: pagination.pageSize
            };
          }
          axios.get("/member/passenger/query-list", {
            params: {
              page: param.page,
              size: param.size
            }
          }).then((response) => {
            let data = response.data;
            if (data.success) {
              passengers.value = data.content.list;
              // 设置分页控件的值
              pagination.current = param.page;
              pagination.total = data.content.total;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleTableChange = (pagination) => {
          // console.log("看看自带的分页参数都有啥:" + pagination);
          handleQuery({
            page: pagination.current,
            size: pagination.pageSize
          });
        };
    
        onMounted(() => {
          handleQuery({
            page: 1,
            size: pagination.pageSize
          });
        });
    
        return {
          passenger,
          visible,
          showModal,
          handleOk,
          passengers,
          pagination,
          columns,
          handleTableChange,
          handleQuery
        };
      },
    });
    </script>
    <style>
    </style>
    

    效果

在这里插入图片描述

  • 乘车人列表查询界面开发,增加loading效果

    同上

    <template>
      <p>
        <a-space>
          <a-button type="primary" @click="handleQuery()">刷新</a-button>
          <a-button type="primary" @click="showModal">新增</a-button>
        </a-space>
      </p>
      <a-table :dataSource="passengers"
               :columns="columns"
               :pagination="pagination"
               @change="handleTableChange"
               :loading="loading"/>
      <a-modal v-model:visible="visible" title="乘车人" @ok="handleOk"
               ok-text="确认" cancel-text="取消">
        <a-form :model="passenger" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
          <a-form-item label="姓名">
            <a-input v-model:value="passenger.name" />
          </a-form-item>
          <a-form-item label="身份证">
            <a-input v-model:value="passenger.idCard" />
          </a-form-item>
          <a-form-item label="类型">
            <a-select v-model:value="passenger.type">
              <a-select-option value="1">成人</a-select-option>
              <a-select-option value="2">儿童</a-select-option>
              <a-select-option value="3">学生</a-select-option>
            </a-select>
          </a-form-item>
        </a-form>
      </a-modal>
    </template>
    <script>
    import { defineComponent, ref, reactive, onMounted } from 'vue';
    import {notification} from "ant-design-vue";
    import axios from "axios";
    
    export default defineComponent({
      setup() {
        const visible = ref(false);
        const passenger = reactive({
          id: undefined,
          memberId: undefined,
          name: undefined,
          idCard: undefined,
          type: undefined,
          createTime: undefined,
          updateTime: undefined,
        });
        const passengers = ref([]);
        // 分页的三个属性名是固定的
        const pagination = reactive({
          total: 0,
          current: 1,
          pageSize: 2,
        });
        let loading = ref(false);
        const columns = [{
          title: '姓名',
          dataIndex: 'name',
          key: 'name',
        }, {
          title: '身份证',
          dataIndex: 'idCard',
          key: 'idCard',
        }, {
          title: '类型',
          dataIndex: 'type',
          key: 'type',
        }];
    
        const showModal = () => {
          visible.value = true;
        };
    
        const handleOk = () => {
          axios.post("/member/passenger/save", passenger).then((response) => {
            let data = response.data;
            if (data.success) {
              notification.success({description: "保存成功!"});
              visible.value = false;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleQuery = (param) => {
          if (!param) {
            param = {
              page: 1,
              size: pagination.pageSize
            };
          }
          loading.value = true;
          axios.get("/member/passenger/query-list", {
            params: {
              page: param.page,
              size: param.size
            }
          }).then((response) => {
            loading.value = false;
            let data = response.data;
            if (data.success) {
              passengers.value = data.content.list;
              // 设置分页控件的值
              pagination.current = param.page;
              pagination.total = data.content.total;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleTableChange = (pagination) => {
          // console.log("看看自带的分页参数都有啥:" + pagination);
          handleQuery({
            page: pagination.current,
            size: pagination.pageSize
          });
        };
    
        onMounted(() => {
          handleQuery({
            page: 1,
            size: pagination.pageSize
          });
        });
    
        return {
          passenger,
          visible,
          showModal,
          handleOk,
          passengers,
          pagination,
          columns,
          handleTableChange,
          handleQuery,
          loading
        };
      },
    });
    </script>
    <style>
    </style>
    

    效果

在这里插入图片描述

  • 乘车人列表查询界面开发,保存成功后刷新列表

    修改passenger.vue

    web/src/views/main/passenger.vue

    const handleOk = () => {
      axios.post("/member/passenger/save", passenger).then((response) => {
        let data = response.data;
        if (data.success) {
          notification.success({description: "保存成功!"});
          visible.value = false;
          handleQuery({
            page: pagination.current,
            size: pagination.pageSize
          });
        } else {
          notification.error({description: data.message});
        }
      });
    };
    

    修改service方法(按时间顺序倒序排列,方便测试效果)

    com.neilxu.train.member.service.PassengerService

    public PageResp<PassengerQueryResp> queryList(PassengerQueryReq req) {
        PassengerExample passengerExample = new PassengerExample();
        passengerExample.setOrderByClause("id desc");
    

    效果

在这里插入图片描述

在这里插入图片描述

10.解决Long类型精度丢失的问题

不同的语言,虽然都有int long等类型,但他们的精度不太一样,在数据传递时需要特别注意精度丢失。
解决方法:将long传成string

对于Long类型
在这里插入图片描述

  • 方式一:全局配置Long转成String

    这种方式前端会有警告,不推荐

    common增加 JacksonConfig.java

  • 方式二:为每个返回结果类单独配置

    修改PassengerQueryResp.java

    com.neilxu.train.member.resp.PassengerQueryResp

    package com.neilxu.train.member.resp;
    
    import com.fasterxml.jackson.annotation.JsonFormat;
    import com.fasterxml.jackson.databind.annotation.JsonSerialize;
    import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
    import lombok.Data;
    
    import java.util.Date;
    
    @Data
    public class PassengerQueryResp {
        @JsonSerialize(using= ToStringSerializer.class)
        private Long id;
    
        @JsonSerialize(using= ToStringSerializer.class)
        private Long memberId;
    
        private String name;
    
        private String idCard;
    
        private String type;
    
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
        private Date createTime;
    
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
        private Date updateTime;
    
    }
    

    效果:

在这里插入图片描述

11.乘车人编辑接口开发

  • 修改service方法

    判断前端有无传id,没有则是新增,否则是更新

    com.neilxu.train.member.service.PassengerService

    public void save(PassengerSaveReq req) {
        DateTime now = DateTime.now();
        Passenger passenger = BeanUtil.copyProperties(req, Passenger.class);
        if (ObjectUtil.isNull(passenger.getId())) {
            passenger.setMemberId(LoginMemberContext.getId());
            passenger.setId(SnowUtil.getSnowflakeNextId());
            passenger.setCreateTime(now);
            passenger.setUpdateTime(now);
            passengerMapper.insert(passenger);
        } else {
            passenger.setUpdateTime(now);
            passengerMapper.updateByPrimaryKey(passenger);
        }
    }
    
  • 测试

    POST http://localhost:8000/member/passenger/save
    Content-Type: application/json
    token: {{token}}
    
    {
      "id": 1768293540707831808,
      "memberId": 1767241446253006848,
      "name": "neil测试1",
      "idCard": "11111111111111111",
      "type": "1",
      "createTime": "2024-03-14 23:10:08"
    }
    
    ###
    

    注意点:

    需要修改保存请求实体类PassengerSaveReq.java,格式化接收的时间格式,否则上面的请求会报错

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date createTime;
    
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date updateTime;
    

    最终修改成功:

在这里插入图片描述

12.乘车人编辑界面开发

  • 修改passenger.vue

    web/src/views/main/passenger.vue

    <template>
      <p>
        <a-space>
          <a-button type="primary" @click="handleQuery()">刷新</a-button>
          <a-button type="primary" @click="onAdd">新增</a-button>
        </a-space>
      </p>
      <a-table :dataSource="passengers"
               :columns="columns"
               :pagination="pagination"
               @change="handleTableChange"
               :loading="loading">
        <template #bodyCell="{ column, record }">
          <template v-if="column.dataIndex === 'operation'">
            <a-space>
              <a @click="onEdit(record)">编辑</a>
            </a-space>
          </template>
        </template>
      </a-table>
      <a-modal v-model:visible="visible" title="乘车人" @ok="handleOk"
               ok-text="确认" cancel-text="取消">
        <a-form :model="passenger" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
          <a-form-item label="姓名">
            <a-input v-model:value="passenger.name" />
          </a-form-item>
          <a-form-item label="身份证">
            <a-input v-model:value="passenger.idCard" />
          </a-form-item>
          <a-form-item label="类型">
            <a-select v-model:value="passenger.type">
              <a-select-option value="1">成人</a-select-option>
              <a-select-option value="2">儿童</a-select-option>
              <a-select-option value="3">学生</a-select-option>
            </a-select>
          </a-form-item>
        </a-form>
      </a-modal>
    </template>
    <script>
    import { defineComponent, ref, onMounted } from 'vue';
    import {notification} from "ant-design-vue";
    import axios from "axios";
    
    export default defineComponent({
      setup() {
        const visible = ref(false);
        let passenger = ref({
          id: undefined,
          memberId: undefined,
          name: undefined,
          idCard: undefined,
          type: undefined,
          createTime: undefined,
          updateTime: undefined,
        });
        const passengers = ref([]);
        // 分页的三个属性名是固定的
        const pagination = ref({
          total: 0,
          current: 1,
          pageSize: 2,
        });
        let loading = ref(false);
        const columns = [{
          title: '姓名',
          dataIndex: 'name',
          key: 'name',
        }, {
          title: '身份证',
          dataIndex: 'idCard',
          key: 'idCard',
        }, {
          title: '类型',
          dataIndex: 'type',
          key: 'type',
        }, {
          title: '操作',
          dataIndex: 'operation'
        }];
    
        const onAdd = () => {
          visible.value = true;
        };
    
        const onEdit = (record) => {
          passenger.value = record;
          visible.value = true;
        };
    
        const handleOk = () => {
          axios.post("/member/passenger/save", passenger.value).then((response) => {
            let data = response.data;
            if (data.success) {
              notification.success({description: "保存成功!"});
              visible.value = false;
              handleQuery({
                page: pagination.value.current,
                size: pagination.value.pageSize
              });
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleQuery = (param) => {
          if (!param) {
            param = {
              page: 1,
              size: pagination.value.pageSize
            };
          }
          loading.value = true;
          axios.get("/member/passenger/query-list", {
            params: {
              page: param.page,
              size: param.size
            }
          }).then((response) => {
            loading.value = false;
            let data = response.data;
            if (data.success) {
              passengers.value = data.content.list;
              // 设置分页控件的值
              pagination.value.current = param.page;
              pagination.value.total = data.content.total;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleTableChange = (pagination) => {
          // console.log("看看自带的分页参数都有啥:" + pagination);
          handleQuery({
            page: pagination.current,
            size: pagination.pageSize
          });
        };
    
        onMounted(() => {
          handleQuery({
            page: 1,
            size: pagination.value.pageSize
          });
        });
    
        return {
          passenger,
          visible,
          onAdd,
          handleOk,
          passengers,
          pagination,
          columns,
          handleTableChange,
          handleQuery,
          loading,
          onEdit
        };
      },
    });
    </script>
    <style>
    </style>
    

    注意点:

    这里为了方便,将reactive改为ref,如果用reactive需要再封装一层属性才能实现双向绑定(或者对属性赋值)

    例如
    在这里插入图片描述

在这里插入图片描述

或者

在这里插入图片描述

在这里插入图片描述

效果

在这里插入图片描述

这里有修改了没点确定,但是页面数据已经变化的问题;还有点击新增,会把前面填写的表单历史内容赋值上

  • 解决上述问题

    • 引入tool.js

      web/public/js/tool.js

      Tool = {
          /**
           * 空校验 null或""都返回true
           */
          isEmpty: (obj) => {
              if ((typeof obj === 'string')) {
                  return !obj || obj.replace(/\s+/g, "") === ""
              } else {
                  return (!obj || JSON.stringify(obj) === "{}" || obj.length === 0);
              }
          },
      
          /**
           * 非空校验
           */
          isNotEmpty: (obj) => {
              return !Tool.isEmpty(obj);
          },
      
          /**
           * 对象复制
           * @param obj
           */
          copy: (obj) => {
              if (Tool.isNotEmpty(obj)) {
                  return JSON.parse(JSON.stringify(obj));
              }
          },
      
          /**
           * 使用递归将数组转为树形结构
           * 父ID属性为parent
           */
          array2Tree: (array, parentId) => {
              if (Tool.isEmpty(array)) {
                  return [];
              }
      
              const result = [];
              for (let i = 0; i < array.length; i++) {
                  const c = array[i];
                  // console.log(Number(c.parent), Number(parentId));
                  if (Number(c.parent) === Number(parentId)) {
                      result.push(c);
      
                      // 递归查看当前节点对应的子节点
                      const children = Tool.array2Tree(array, c.id);
                      if (Tool.isNotEmpty(children)) {
                          c.children = children;
                      }
                  }
              }
              return result;
          },
      
          /**
           * 随机生成[len]长度的[radix]进制数
           * @param len
           * @param radix 默认62
           * @returns {string}
           */
          uuid: (len, radix = 62) => {
              const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
              const uuid = [];
              radix = radix || chars.length;
      
              for (let i = 0; i < len; i++) {
                  uuid[i] = chars[0 | Math.random() * radix];
              }
      
              return uuid.join('');
          }
      };
      

      web/public/index.html

      <script src="<%= BASE_URL %>js/tool.js"></script>
      
    • 修改passenger.vue

      const onAdd = () => {
        passenger.value = {};
        visible.value = true;
      };
      
      const onEdit = (record) => {
        passenger.value = window.Tool.copy(record);
        visible.value = true;
      };
      

      新增的时候,先将passenger清空,则不会出现历史数据;编辑的时候,将行数据复制一个新的对象,修改弹窗里的新的对象,只要不点击确定,则不会修改页面的数据

    • 效果

在这里插入图片描述

13.乘车人删除接口开发

  • service

    com.neilxu.train.member.service.PassengerService

    public void delete(Long id) {
        passengerMapper.deleteByPrimaryKey(id);
    }
    
  • controller

    com.neilxu.train.member.controller.PassengerController

    @DeleteMapping("/delete/{id}")
    public CommonResp<Object> delete(@PathVariable Long id) {
        passengerService.delete(id);
        return new CommonResp<>();
    }
    
  • 测试

    DELETE http://localhost:8000/member/passenger/delete/1768484775556943872
    Accept: application/json
    token: {{token}}
    
    ###
    

    调用后成功删除

14.乘车人删除界面开发

  • 修改passenger.vue

    <template>
      <p>
        <a-space>
          <a-button type="primary" @click="handleQuery()">刷新</a-button>
          <a-button type="primary" @click="onAdd">新增</a-button>
        </a-space>
      </p>
      <a-table :dataSource="passengers"
               :columns="columns"
               :pagination="pagination"
               @change="handleTableChange"
               :loading="loading">
        <template #bodyCell="{ column, record }">
          <template v-if="column.dataIndex === 'operation'">
            <a-space>
              <a-popconfirm
                  title="删除后不可恢复,确认删除?"
                  @confirm="onDelete(record)"
                  ok-text="确认" cancel-text="取消">
                <a style="color: red">删除</a>
              </a-popconfirm>
              <a @click="onEdit(record)">编辑</a>
            </a-space>
          </template>
        </template>
      </a-table>
      <a-modal v-model:visible="visible" title="乘车人" @ok="handleOk"
               ok-text="确认" cancel-text="取消">
        <a-form :model="passenger" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
          <a-form-item label="姓名">
            <a-input v-model:value="passenger.name" />
          </a-form-item>
          <a-form-item label="身份证">
            <a-input v-model:value="passenger.idCard" />
          </a-form-item>
          <a-form-item label="类型">
            <a-select v-model:value="passenger.type">
              <a-select-option value="1">成人</a-select-option>
              <a-select-option value="2">儿童</a-select-option>
              <a-select-option value="3">学生</a-select-option>
            </a-select>
          </a-form-item>
        </a-form>
      </a-modal>
    </template>
    <script>
    import { defineComponent, ref, onMounted } from 'vue';
    import {notification} from "ant-design-vue";
    import axios from "axios";
    
    export default defineComponent({
      setup() {
        const visible = ref(false);
        let passenger = ref({
          id: undefined,
          memberId: undefined,
          name: undefined,
          idCard: undefined,
          type: undefined,
          createTime: undefined,
          updateTime: undefined,
        });
        const passengers = ref([]);
        // 分页的三个属性名是固定的
        const pagination = ref({
          total: 0,
          current: 1,
          pageSize: 2,
        });
        let loading = ref(false);
        const columns = [{
          title: '姓名',
          dataIndex: 'name',
          key: 'name',
        }, {
          title: '身份证',
          dataIndex: 'idCard',
          key: 'idCard',
        }, {
          title: '类型',
          dataIndex: 'type',
          key: 'type',
        }, {
          title: '操作',
          dataIndex: 'operation'
        }];
    
        const onAdd = () => {
          passenger.value = {};
          visible.value = true;
        };
    
        const onEdit = (record) => {
          passenger.value = window.Tool.copy(record);
          visible.value = true;
        };
    
        const onDelete = (record) => {
          axios.delete("/member/passenger/delete/" + record.id).then((response) => {
            const data = response.data;
            if (data.success) {
              notification.success({description: "删除成功!"});
              handleQuery({
                page: pagination.value.current,
                size: pagination.value.pageSize,
              });
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleOk = () => {
          axios.post("/member/passenger/save", passenger.value).then((response) => {
            let data = response.data;
            if (data.success) {
              notification.success({description: "保存成功!"});
              visible.value = false;
              handleQuery({
                page: pagination.value.current,
                size: pagination.value.pageSize
              });
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleQuery = (param) => {
          if (!param) {
            param = {
              page: 1,
              size: pagination.value.pageSize
            };
          }
          loading.value = true;
          axios.get("/member/passenger/query-list", {
            params: {
              page: param.page,
              size: param.size
            }
          }).then((response) => {
            loading.value = false;
            let data = response.data;
            if (data.success) {
              passengers.value = data.content.list;
              // 设置分页控件的值
              pagination.value.current = param.page;
              pagination.value.total = data.content.total;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleTableChange = (pagination) => {
          // console.log("看看自带的分页参数都有啥:" + pagination);
          handleQuery({
            page: pagination.current,
            size: pagination.pageSize
          });
        };
    
        onMounted(() => {
          handleQuery({
            page: 1,
            size: pagination.value.pageSize
          });
        });
    
        return {
          passenger,
          visible,
          onAdd,
          handleOk,
          passengers,
          pagination,
          columns,
          handleTableChange,
          handleQuery,
          loading,
          onEdit,
          onDelete
        };
      },
    });
    </script>
    <style>
    </style>
    
  • 效果

在这里插入图片描述

在这里插入图片描述

15.前端枚举展示的解决方案

目前问题:列表里乘客类型显示是数字,需要改成对应的中文名称;同时,前端枚举需要与后端枚举保持同步,所以需要做个关联

  • 将下拉框的值提取成常量数组

    修改passenger.vue

    <template>
      <p>
        <a-space>
          <a-button type="primary" @click="handleQuery()">刷新</a-button>
          <a-button type="primary" @click="onAdd">新增</a-button>
        </a-space>
      </p>
      <a-table :dataSource="passengers"
               :columns="columns"
               :pagination="pagination"
               @change="handleTableChange"
               :loading="loading">
        <template #bodyCell="{ column, record }">
          <template v-if="column.dataIndex === 'operation'">
            <a-space>
              <a-popconfirm
                  title="删除后不可恢复,确认删除?"
                  @confirm="onDelete(record)"
                  ok-text="确认" cancel-text="取消">
                <a style="color: red">删除</a>
              </a-popconfirm>
              <a @click="onEdit(record)">编辑</a>
            </a-space>
          </template>
        </template>
      </a-table>
      <a-modal v-model:visible="visible" title="乘车人" @ok="handleOk"
               ok-text="确认" cancel-text="取消">
        <a-form :model="passenger" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
          <a-form-item label="姓名">
            <a-input v-model:value="passenger.name" />
          </a-form-item>
          <a-form-item label="身份证">
            <a-input v-model:value="passenger.idCard" />
          </a-form-item>
          <a-form-item label="类型">
            <a-select v-model:value="passenger.type">
              <a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.key" :value="item.key">{{item.value}}</a-select-option>
            </a-select>
          </a-form-item>
        </a-form>
      </a-modal>
    </template>
    <script>
    import { defineComponent, ref, onMounted } from 'vue';
    import {notification} from "ant-design-vue";
    import axios from "axios";
    
    export default defineComponent({
      setup() {
        const PASSENGER_TYPE_ARRAY = [{key: "1", value: "成人1"}, {key: "2", value: "儿童"}, {key: "3", value: "学生"}];
        const visible = ref(false);
        let passenger = ref({
          id: undefined,
          memberId: undefined,
          name: undefined,
          idCard: undefined,
          type: undefined,
          createTime: undefined,
          updateTime: undefined,
        });
        const passengers = ref([]);
        // 分页的三个属性名是固定的
        const pagination = ref({
          total: 0,
          current: 1,
          pageSize: 2,
        });
        let loading = ref(false);
        const columns = [{
          title: '姓名',
          dataIndex: 'name',
          key: 'name',
        }, {
          title: '身份证',
          dataIndex: 'idCard',
          key: 'idCard',
        }, {
          title: '类型',
          dataIndex: 'type',
          key: 'type',
        }, {
          title: '操作',
          dataIndex: 'operation'
        }];
    
        const onAdd = () => {
          passenger.value = {};
          visible.value = true;
        };
    
        const onEdit = (record) => {
          passenger.value = window.Tool.copy(record);
          visible.value = true;
        };
    
        const onDelete = (record) => {
          axios.delete("/member/passenger/delete/" + record.id).then((response) => {
            const data = response.data;
            if (data.success) {
              notification.success({description: "删除成功!"});
              handleQuery({
                page: pagination.value.current,
                size: pagination.value.pageSize,
              });
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleOk = () => {
          axios.post("/member/passenger/save", passenger.value).then((response) => {
            let data = response.data;
            if (data.success) {
              notification.success({description: "保存成功!"});
              visible.value = false;
              handleQuery({
                page: pagination.value.current,
                size: pagination.value.pageSize
              });
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleQuery = (param) => {
          if (!param) {
            param = {
              page: 1,
              size: pagination.value.pageSize
            };
          }
          loading.value = true;
          axios.get("/member/passenger/query-list", {
            params: {
              page: param.page,
              size: param.size
            }
          }).then((response) => {
            loading.value = false;
            let data = response.data;
            if (data.success) {
              passengers.value = data.content.list;
              // 设置分页控件的值
              pagination.value.current = param.page;
              pagination.value.total = data.content.total;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleTableChange = (pagination) => {
          // console.log("看看自带的分页参数都有啥:" + pagination);
          handleQuery({
            page: pagination.current,
            size: pagination.pageSize
          });
        };
    
        onMounted(() => {
          handleQuery({
            page: 1,
            size: pagination.value.pageSize
          });
        });
    
        return {
          PASSENGER_TYPE_ARRAY,
          passenger,
          visible,
          onAdd,
          handleOk,
          passengers,
          pagination,
          columns,
          handleTableChange,
          handleQuery,
          loading,
          onEdit,
          onDelete
        };
      },
    });
    </script>
    <style>
    </style>
    

    这里是演示将写死的下拉框改成从一个常量中获取

  • 解决表格中枚举字段的显示

    修改passenger.vue

    <template>
      <p>
        <a-space>
          <a-button type="primary" @click="handleQuery()">刷新</a-button>
          <a-button type="primary" @click="onAdd">新增</a-button>
        </a-space>
      </p>
      <a-table :dataSource="passengers"
               :columns="columns"
               :pagination="pagination"
               @change="handleTableChange"
               :loading="loading">
        <template #bodyCell="{ column, record }">
          <template v-if="column.dataIndex === 'operation'">
            <a-space>
              <a-popconfirm
                  title="删除后不可恢复,确认删除?"
                  @confirm="onDelete(record)"
                  ok-text="确认" cancel-text="取消">
                <a style="color: red">删除</a>
              </a-popconfirm>
              <a @click="onEdit(record)">编辑</a>
            </a-space>
          </template>
          <template v-else-if="column.dataIndex === 'type'">
            <span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.key">
              <span v-if="item.key === record.type">
                {{item.value}}
              </span>
            </span>
          </template>
        </template>
      </a-table>
      <a-modal v-model:visible="visible" title="乘车人" @ok="handleOk"
               ok-text="确认" cancel-text="取消">
        <a-form :model="passenger" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
          <a-form-item label="姓名">
            <a-input v-model:value="passenger.name" />
          </a-form-item>
          <a-form-item label="身份证">
            <a-input v-model:value="passenger.idCard" />
          </a-form-item>
          <a-form-item label="类型">
            <a-select v-model:value="passenger.type">
              <a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.key" :value="item.key">{{item.value}}</a-select-option>
            </a-select>
          </a-form-item>
        </a-form>
      </a-modal>
    </template>
    <script>
    import { defineComponent, ref, onMounted } from 'vue';
    import {notification} from "ant-design-vue";
    import axios from "axios";
    
    export default defineComponent({
      setup() {
        const PASSENGER_TYPE_ARRAY = [{key: "1", value: "成人"}, {key: "2", value: "儿童"}, {key: "3", value: "学生"}];
        const visible = ref(false);
        let passenger = ref({
          id: undefined,
          memberId: undefined,
          name: undefined,
          idCard: undefined,
          type: undefined,
          createTime: undefined,
          updateTime: undefined,
        });
        const passengers = ref([]);
        // 分页的三个属性名是固定的
        const pagination = ref({
          total: 0,
          current: 1,
          pageSize: 2,
        });
        let loading = ref(false);
        const columns = [{
          title: '姓名',
          dataIndex: 'name',
          key: 'name',
        }, {
          title: '身份证',
          dataIndex: 'idCard',
          key: 'idCard',
        }, {
          title: '类型',
          dataIndex: 'type',
          key: 'type',
        }, {
          title: '操作',
          dataIndex: 'operation'
        }];
    
        const onAdd = () => {
          passenger.value = {};
          visible.value = true;
        };
    
        const onEdit = (record) => {
          passenger.value = window.Tool.copy(record);
          visible.value = true;
        };
    
        const onDelete = (record) => {
          axios.delete("/member/passenger/delete/" + record.id).then((response) => {
            const data = response.data;
            if (data.success) {
              notification.success({description: "删除成功!"});
              handleQuery({
                page: pagination.value.current,
                size: pagination.value.pageSize,
              });
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleOk = () => {
          axios.post("/member/passenger/save", passenger.value).then((response) => {
            let data = response.data;
            if (data.success) {
              notification.success({description: "保存成功!"});
              visible.value = false;
              handleQuery({
                page: pagination.value.current,
                size: pagination.value.pageSize
              });
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleQuery = (param) => {
          if (!param) {
            param = {
              page: 1,
              size: pagination.value.pageSize
            };
          }
          loading.value = true;
          axios.get("/member/passenger/query-list", {
            params: {
              page: param.page,
              size: param.size
            }
          }).then((response) => {
            loading.value = false;
            let data = response.data;
            if (data.success) {
              passengers.value = data.content.list;
              // 设置分页控件的值
              pagination.value.current = param.page;
              pagination.value.total = data.content.total;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleTableChange = (pagination) => {
          // console.log("看看自带的分页参数都有啥:" + pagination);
          handleQuery({
            page: pagination.current,
            size: pagination.pageSize
          });
        };
    
        onMounted(() => {
          handleQuery({
            page: 1,
            size: pagination.value.pageSize
          });
        });
    
        return {
          PASSENGER_TYPE_ARRAY,
          passenger,
          visible,
          onAdd,
          handleOk,
          passengers,
          pagination,
          columns,
          handleTableChange,
          handleQuery,
          loading,
          onEdit,
          onDelete
        };
      },
    });
    </script>
    <style>
    </style>
    

    效果

在这里插入图片描述

  • 为了后续能通过后端生成枚举常量给前端,现在再进一步将常量写成一个独立的js文件

    • 新增enums.js

      web/src/assets/js/enums.js

      PASSENGER_TYPE_ARRAY = [{key: "1", value: "成人"}, {key: "2", value: "儿童"}, {key: "3", value: "学生"}];
      
    • 引入文件

      • 修改main.js

        import './assets/js/enums';
        
      • 修改package.json

        "rules": {
            "vue/multi-word-component-names": 0,
            "no-undef": 0
        }
        

        这里是解决后面passenger.vue使用window.PASSENGER_TYPE_ARRAY;获取对象报错的问题

        自定义的js放在src下和放在public下的区别:

        放在src下会统一打包成一个js,

        public下的则独立打包

    • 修改passenger.vue

      <template>
        <p>
          <a-space>
            <a-button type="primary" @click="handleQuery()">刷新</a-button>
            <a-button type="primary" @click="onAdd">新增</a-button>
          </a-space>
        </p>
        <a-table :dataSource="passengers"
                 :columns="columns"
                 :pagination="pagination"
                 @change="handleTableChange"
                 :loading="loading">
          <template #bodyCell="{ column, record }">
            <template v-if="column.dataIndex === 'operation'">
              <a-space>
                <a-popconfirm
                    title="删除后不可恢复,确认删除?"
                    @confirm="onDelete(record)"
                    ok-text="确认" cancel-text="取消">
                  <a style="color: red">删除</a>
                </a-popconfirm>
                <a @click="onEdit(record)">编辑</a>
              </a-space>
            </template>
            <template v-else-if="column.dataIndex === 'type'">
              <span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.key">
                <span v-if="item.key === record.type">
                  {{item.value}}
                </span>
              </span>
            </template>
          </template>
        </a-table>
        <a-modal v-model:visible="visible" title="乘车人" @ok="handleOk"
                 ok-text="确认" cancel-text="取消">
          <a-form :model="passenger" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
            <a-form-item label="姓名">
              <a-input v-model:value="passenger.name" />
            </a-form-item>
            <a-form-item label="身份证">
              <a-input v-model:value="passenger.idCard" />
            </a-form-item>
            <a-form-item label="类型">
              <a-select v-model:value="passenger.type">
                <a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.key" :value="item.key">{{item.value}}</a-select-option>
              </a-select>
            </a-form-item>
          </a-form>
        </a-modal>
      </template>
      <script>
      import { defineComponent, ref, onMounted } from 'vue';
      import {notification} from "ant-design-vue";
      import axios from "axios";
      
      export default defineComponent({
        setup() {
          const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;
          const visible = ref(false);
          let passenger = ref({
            id: undefined,
            memberId: undefined,
            name: undefined,
            idCard: undefined,
            type: undefined,
            createTime: undefined,
            updateTime: undefined,
          });
          const passengers = ref([]);
          // 分页的三个属性名是固定的
          const pagination = ref({
            total: 0,
            current: 1,
            pageSize: 2,
          });
          let loading = ref(false);
          const columns = [{
            title: '姓名',
            dataIndex: 'name',
            key: 'name',
          }, {
            title: '身份证',
            dataIndex: 'idCard',
            key: 'idCard',
          }, {
            title: '类型',
            dataIndex: 'type',
            key: 'type',
          }, {
            title: '操作',
            dataIndex: 'operation'
          }];
      
          const onAdd = () => {
            passenger.value = {};
            visible.value = true;
          };
      
          const onEdit = (record) => {
            passenger.value = window.Tool.copy(record);
            visible.value = true;
          };
      
          const onDelete = (record) => {
            axios.delete("/member/passenger/delete/" + record.id).then((response) => {
              const data = response.data;
              if (data.success) {
                notification.success({description: "删除成功!"});
                handleQuery({
                  page: pagination.value.current,
                  size: pagination.value.pageSize,
                });
              } else {
                notification.error({description: data.message});
              }
            });
          };
      
          const handleOk = () => {
            axios.post("/member/passenger/save", passenger.value).then((response) => {
              let data = response.data;
              if (data.success) {
                notification.success({description: "保存成功!"});
                visible.value = false;
                handleQuery({
                  page: pagination.value.current,
                  size: pagination.value.pageSize
                });
              } else {
                notification.error({description: data.message});
              }
            });
          };
      
          const handleQuery = (param) => {
            if (!param) {
              param = {
                page: 1,
                size: pagination.value.pageSize
              };
            }
            loading.value = true;
            axios.get("/member/passenger/query-list", {
              params: {
                page: param.page,
                size: param.size
              }
            }).then((response) => {
              loading.value = false;
              let data = response.data;
              if (data.success) {
                passengers.value = data.content.list;
                // 设置分页控件的值
                pagination.value.current = param.page;
                pagination.value.total = data.content.total;
              } else {
                notification.error({description: data.message});
              }
            });
          };
      
          const handleTableChange = (pagination) => {
            // console.log("看看自带的分页参数都有啥:" + pagination);
            handleQuery({
              page: pagination.current,
              size: pagination.pageSize
            });
          };
      
          onMounted(() => {
            handleQuery({
              page: 1,
              size: pagination.value.pageSize
            });
          });
      
          return {
            PASSENGER_TYPE_ARRAY,
            passenger,
            visible,
            onAdd,
            handleOk,
            passengers,
            pagination,
            columns,
            handleTableChange,
            handleQuery,
            loading,
            onEdit,
            onDelete
          };
        },
      });
      </script>
      <style>
      </style>
      
    • 效果

在这里插入图片描述

二、自制前后端代码生成器,提高开发效率

以乘车人增删改查为模板,自制单表管理,前后端代码生成器,生成controller,service,req,resp,enum,vue

实际项目中目前使用mybatis-plus相关插件也是可以做到类似的生成后端代码效果,这里学习下课程里的方法

在这里插入图片描述

  • 生成器原理

    使用freemarker,利用模板,生成java、vue等项目文件。
    freemarker是老牌模板引擎,以前常用于页面开发,和thymeleaf类似,有需要批量生成格式固定的一类文件的需求,都可以使用freemarker来完成。

    冷门知识点:excel可以另存为xml。
    复杂excel导出:可以先设计好复制excel,转成xml,用xml来制作模板,再生成excel

1.引入依赖,测试使用freemarker生成代码

  • 修改generator下的pom文件

    <dependencies>
        <!-- 模板引擎freemarker -->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
        </dependency>
    </dependencies>
    
  • 新增FreemarkerUtil.java

    com.neilxu.train.generator.util.FreemarkerUtil

    package com.neilxu.train.generator.util;
    
    import freemarker.template.Configuration;
    import freemarker.template.DefaultObjectWrapper;
    import freemarker.template.Template;
    import freemarker.template.TemplateException;
    
    import java.io.BufferedWriter;
    import java.io.File;
    import java.io.FileWriter;
    import java.io.IOException;
    import java.util.Map;
    
    public class FreemarkerUtil {
    
        static String ftlPath = "generator\\src\\main\\java\\com\\neilxu\\train\\generator\\ftl\\";
    
        static Template temp;
    
        /**
         * 读模板
         */
        public static void initConfig(String ftlName) throws IOException {
            Configuration cfg = new Configuration(Configuration.VERSION_2_3_31);
            cfg.setDirectoryForTemplateLoading(new File(ftlPath));
            cfg.setObjectWrapper(new DefaultObjectWrapper(Configuration.VERSION_2_3_31));
            temp = cfg.getTemplate(ftlName);
        }
    
        /**
         * 根据模板,生成文件
         */
        public static void generator(String fileName, Map<String, Object> map) throws IOException, TemplateException {
            FileWriter fw = new FileWriter(fileName);
            BufferedWriter bw = new BufferedWriter(fw);
            temp.process(map, bw);
            bw.flush();
            fw.close();
        }
    }
    
  • 新增ServerGenerator.java

    com.neilxu.train.generator.server.ServerGenerator

    package com.neilxu.train.generator.server;
    
    import com.neilxu.train.generator.util.FreemarkerUtil;
    import freemarker.template.TemplateException;
    
    import java.io.File;
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Map;
    
    public class ServerGenerator {
        static String toPath = "generator\\src\\main\\java\\com\\neilxu\\train\\generator\\test\\";
        static {
            new File(toPath).mkdirs();
        }
    
        public static void main(String[] args) throws IOException, TemplateException {
            FreemarkerUtil.initConfig("test.ftl");
            Map<String, Object> param = new HashMap<>();
            param.put("domain", "Test1");
            FreemarkerUtil.generator(toPath + "Test1.java", param);
        }
    }
    
  • 新增ftl模板

    package com.neilxu.train.generator.test;
    
    public class ${domain} {
    
    private String name;
    }
    
  • 测试

    执行ServerGenerator的main方法

在这里插入图片描述

2.集成DOM4j读取xml

由于我们后续生成代码需要与原来的generator代码生成器xml配置文件相结合,所以这里引入dom4j来读取xml

  • 新增依赖

    修改 generator下的 pom.xml

    <!-- 读xml -->
    <dependency>
        <groupId>org.dom4j</groupId>
        <artifactId>dom4j</artifactId>
        <version>2.1.3</version>
    </dependency>
    
    <!-- https://mvnrepository.com/artifact/jaxen/jaxen -->
    <dependency>
        <groupId>jaxen</groupId>
        <artifactId>jaxen</artifactId>
        <version>1.2.0</version>
    </dependency>
    
    <plugin>
        <groupId>org.mybatis.generator</groupId>
        <artifactId>mybatis-generator-maven-plugin</artifactId>
        <version>1.4.0</version>
        <configuration>
            <!--这里注意后面会有其他模块的代码生成器配置文件,每次只能生成一个模块的-->
            <configurationFile>src/main/resources/generator-config-member.xml</configurationFile>
            <!--<configurationFile>src/main/resources/generator-config-business.xml</configurationFile>-->
            <overwrite>true</overwrite>
            <verbose>true</verbose>
        </configuration>
        <dependencies>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.22</version>
            </dependency>
        </dependencies>
    </plugin>
    
  • 先新增generator-config-business.xml,以后会用到

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE generatorConfiguration
            PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
            "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
    
    <generatorConfiguration>
        <context id="Mysql" targetRuntime="MyBatis3" defaultModelType="flat">
    
            <!-- 自动检查关键字,为关键字增加反引号 -->
            <property name="autoDelimitKeywords" value="true"/>
            <property name="beginningDelimiter" value="`"/>
            <property name="endingDelimiter" value="`"/>
    
            <!--覆盖生成XML文件-->
            <plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin" />
            <!-- 生成的实体类添加toString()方法 -->
            <plugin type="org.mybatis.generator.plugins.ToStringPlugin"/>
    
            <!-- 不生成注释 -->
            <commentGenerator>
                <property name="suppressAllComments" value="true"/>
            </commentGenerator>
    
            <!-- 配置数据源,需要根据自己的项目修改 -->
            <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                            connectionURL="jdbc:mysql://localhost/train_business?serverTimezone=Asia/Shanghai"
                            userId="train_business"
                            password="Business123">
            </jdbcConnection>
    
            <!-- domain类的位置 targetProject是相对pom.xml的路径-->
            <javaModelGenerator targetProject="..\member\src\main\java"
                                targetPackage="com.neilxu.train.member.domain"/>
    
            <!-- mapper xml的位置 targetProject是相对pom.xml的路径 -->
            <sqlMapGenerator targetProject="..\member\src\main\resources"
                             targetPackage="mapper"/>
    
            <!-- mapper类的位置 targetProject是相对pom.xml的路径 -->
            <javaClientGenerator targetProject="..\member\src\main\java"
                                 targetPackage="com.neilxu.train.member.mapper"
                                 type="XMLMAPPER"/>
    
            <!--<table tableName="member" domainObjectName="Member"/>-->
            <table tableName="passenger" domainObjectName="Passenger"/>
        </context>
    </generatorConfiguration>
    
  • 修改ServerGenerator.java

    com.neilxu.train.generator.server.ServerGenerator

    package com.neilxu.train.generator.server;
    
    import org.dom4j.Document;
    import org.dom4j.Node;
    import org.dom4j.io.SAXReader;
    
    import java.io.File;
    import java.util.HashMap;
    import java.util.Map;
    
    public class ServerGenerator {
        static String toPath = "generator\\src\\main\\java\\com\\neilxu\\train\\generator\\test\\";
        static String pomPath = "generator\\pom.xml";
        static {
            new File(toPath).mkdirs();
        }
    
        public static void main(String[] args) throws Exception {
            SAXReader saxReader = new SAXReader();
            Map<String, String> map = new HashMap<String, String>();
            map.put("pom", "http://maven.apache.org/POM/4.0.0");
            saxReader.getDocumentFactory().setXPathNamespaceURIs(map);
            Document document = saxReader.read(pomPath);
            Node node = document.selectSingleNode("//pom:configurationFile");
            System.out.println(node.getText());
    
            // FreemarkerUtil.initConfig("test.ftl");
            // Map<String, Object> param = new HashMap<>();
            // param.put("domain", "Test1");
            // FreemarkerUtil.generator(toPath + "Test1.java", param);
        }
    }
    

    执行main方法:

在这里插入图片描述

  • 继续修改ServerGenerator.java,读取当前持久层的xml文件,得到表名和实体名

    com.neilxu.train.generator.server.ServerGenerator

    package com.neilxu.train.generator.server;
    
    import org.dom4j.Document;
    import org.dom4j.DocumentException;
    import org.dom4j.Node;
    import org.dom4j.io.SAXReader;
    
    import java.io.File;
    import java.util.HashMap;
    import java.util.Map;
    
    public class ServerGenerator {
        static String toPath = "generator\\src\\main\\java\\com\\neilxu\\train\\generator\\test\\";
        static String pomPath = "generator\\pom.xml";
        static {
            new File(toPath).mkdirs();
        }
    
    
        public static void main(String[] args) throws Exception {
            String generatorPath = getGeneratorPath();
    
            Document document = new SAXReader().read("generator/" + generatorPath);
            Node table = document.selectSingleNode("//table");
            System.out.println(table);
            Node tableName = table.selectSingleNode("@tableName");
            Node domainObjectName = table.selectSingleNode("@domainObjectName");
            System.out.println(tableName.getText() + "/" + domainObjectName.getText());
    
            // FreemarkerUtil.initConfig("test.ftl");
            // Map<String, Object> param = new HashMap<>();
            // param.put("domain", "Test1");
            // FreemarkerUtil.generator(toPath + "Test1.java", param);
        }
    
        private static String getGeneratorPath() throws DocumentException {
            SAXReader saxReader = new SAXReader();
            Map<String, String> map = new HashMap<String, String>();
            map.put("pom", "http://maven.apache.org/POM/4.0.0");
            saxReader.getDocumentFactory().setXPathNamespaceURIs(map);
            Document document = saxReader.read(pomPath);
            Node node = document.selectSingleNode("//pom:configurationFile");
            System.out.println(node.getText());
            return node.getText();
        }
    }
    

    执行main方法:

在这里插入图片描述

3.详解Service生成器

  • 将现有的PassengerService.java改造成ftl模板文件

    generator/src/main/java/com/neilxu/train/generator/ftl/service.ftl

    package com.neilxu.train.member.service;
    
    import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.date.DateTime;
    import cn.hutool.core.util.ObjectUtil;
    import com.github.pagehelper.PageHelper;
    import com.github.pagehelper.PageInfo;
    import com.neilxu.train.common.context.LoginMemberContext;
    import com.neilxu.train.common.resp.PageResp;
    import com.neilxu.train.common.util.SnowUtil;
    import com.neilxu.train.member.domain.${Domain};
    import com.neilxu.train.member.domain.${Domain}Example;
    import com.neilxu.train.member.mapper.${Domain}Mapper;
    import com.neilxu.train.member.req.${Domain}QueryReq;
    import com.neilxu.train.member.req.${Domain}SaveReq;
    import com.neilxu.train.member.resp.${Domain}QueryResp;
    import jakarta.annotation.Resource;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Service;
    
    import java.util.List;
    
    @Service
    public class ${Domain}Service {
    
        private static final Logger LOG = LoggerFactory.getLogger(${Domain}Service.class);
    
        @Resource
        private ${Domain}Mapper ${domain}Mapper;
    
        public void save(${Domain}SaveReq req) {
            DateTime now = DateTime.now();
            ${Domain} ${domain} = BeanUtil.copyProperties(req, ${Domain}.class);
            if (ObjectUtil.isNull(${domain}.getId())) {
                ${domain}.setMemberId(LoginMemberContext.getId());
                ${domain}.setId(SnowUtil.getSnowflakeNextId());
                ${domain}.setCreateTime(now);
                ${domain}.setUpdateTime(now);
                ${domain}Mapper.insert(${domain});
            } else {
                ${domain}.setUpdateTime(now);
                ${domain}Mapper.updateByPrimaryKey(${domain});
            }
        }
    
        public PageResp<${Domain}QueryResp> queryList(${Domain}QueryReq req) {
            ${Domain}Example ${domain}Example = new ${Domain}Example();
            ${domain}Example.setOrderByClause("id desc");
            ${Domain}Example.Criteria criteria = ${domain}Example.createCriteria();
            if (ObjectUtil.isNotNull(req.getMemberId())) {
                criteria.andMemberIdEqualTo(req.getMemberId());
            }
    
            LOG.info("查询页码:{}", req.getPage());
            LOG.info("每页条数:{}", req.getSize());
            PageHelper.startPage(req.getPage(), req.getSize());
            List<${Domain}> ${domain}List = ${domain}Mapper.selectByExample(${domain}Example);
    
            PageInfo<${Domain}> pageInfo = new PageInfo<>(${domain}List);
            LOG.info("总行数:{}", pageInfo.getTotal());
            LOG.info("总页数:{}", pageInfo.getPages());
    
            List<${Domain}QueryResp> list = BeanUtil.copyToList(${domain}List, ${Domain}QueryResp.class);
    
            PageResp<${Domain}QueryResp> pageResp = new PageResp<>();
            pageResp.setTotal(pageInfo.getTotal());
            pageResp.setList(list);
            return pageResp;
        }
    
        public void delete(Long id) {
            ${domain}Mapper.deleteByPrimaryKey(id);
        }
    }
    
  • 修改ServerGenerator.java

    com.neilxu.train.generator.server.ServerGenerator

    package com.neilxu.train.generator.server;
    
    import com.neilxu.train.generator.util.FreemarkerUtil;
    import org.dom4j.Document;
    import org.dom4j.DocumentException;
    import org.dom4j.Node;
    import org.dom4j.io.SAXReader;
    
    import java.io.File;
    import java.util.HashMap;
    import java.util.Map;
    
    public class ServerGenerator {
        static String servicePath = "[module]/src/main/java/com/neilxu/train/[module]/service/";
        static String pomPath = "generator\\pom.xml";
        static {
            new File(servicePath).mkdirs();
        }
    
        public static void main(String[] args) throws Exception {
            // 获取mybatis-generator
            String generatorPath = getGeneratorPath();
            // 比如generator-config-member.xml,得到module = member
            String module = generatorPath.replace("src/main/resources/generator-config-", "").replace(".xml", "");
            System.out.println("module: " + module);
            servicePath = servicePath.replace("[module]", module);
            // new File(servicePath).mkdirs();
            System.out.println("servicePath: " + servicePath);
    
            // 读取table节点
            Document document = new SAXReader().read("generator/" + generatorPath);
            Node table = document.selectSingleNode("//table");
            System.out.println(table);
            Node tableName = table.selectSingleNode("@tableName");
            Node domainObjectName = table.selectSingleNode("@domainObjectName");
            System.out.println(tableName.getText() + "/" + domainObjectName.getText());
    
            // 示例:表名 jiawa_test
            // Domain = JiawaTest
            String Domain = domainObjectName.getText();
            // domain = jiawaTest
            String domain = Domain.substring(0, 1).toLowerCase() + Domain.substring(1);
            // do_main = jiawa-test
            String do_main = tableName.getText().replaceAll("_", "-");
    
            // 组装参数
            Map<String, Object> param = new HashMap<>();
            param.put("Domain", Domain);
            param.put("domain", domain);
            param.put("do_main", do_main);
            System.out.println("组装参数:" + param);
    
            FreemarkerUtil.initConfig("service.ftl");
            FreemarkerUtil.generator(servicePath + Domain + "Service.java", param);
        }
    
        private static String getGeneratorPath() throws DocumentException {
            SAXReader saxReader = new SAXReader();
            Map<String, String> map = new HashMap<String, String>();
            map.put("pom", "http://maven.apache.org/POM/4.0.0");
            saxReader.getDocumentFactory().setXPathNamespaceURIs(map);
            Document document = saxReader.read(pomPath);
            Node node = document.selectSingleNode("//pom:configurationFile");
            System.out.println(node.getText());
            return node.getText();
        }
    }
    
  • 执行效果

    改一下原service类

在这里插入图片描述

再试着重新生成这个类

在这里插入图片描述

生成覆盖后和原来一样,说明代码生成成功

4.详解Controller生成器

方法同service

  • 制作controller模板

    generator/src/main/java/com/neilxu/train/generator/ftl/controller.ftl

    package com.neilxu.train.member.controller;
    
    import com.neilxu.train.common.context.LoginMemberContext;
    import com.neilxu.train.common.resp.CommonResp;
    import com.neilxu.train.common.resp.PageResp;
    import com.neilxu.train.member.req.${Domain}QueryReq;
    import com.neilxu.train.member.req.${Domain}SaveReq;
    import com.neilxu.train.member.resp.${Domain}QueryResp;
    import com.neilxu.train.member.service.${Domain}Service;
    import jakarta.annotation.Resource;
    import jakarta.validation.Valid;
    import org.springframework.web.bind.annotation.*;
    
    @RestController
    @RequestMapping("/${do_main}")
    public class ${Domain}Controller {
    
        @Resource
        private ${Domain}Service ${domain}Service;
    
        @PostMapping("/save")
        public CommonResp<Object> save(@Valid @RequestBody ${Domain}SaveReq req) {
            ${domain}Service.save(req);
            return new CommonResp<>();
        }
    
        @GetMapping("/query-list")
        public CommonResp<PageResp<${Domain}QueryResp>> queryList(@Valid ${Domain}QueryReq req) {
            req.setMemberId(LoginMemberContext.getId());
            PageResp<${Domain}QueryResp> list = ${domain}Service.queryList(req);
            return new CommonResp<>(list);
        }
    
        @DeleteMapping("/delete/{id}")
        public CommonResp<Object> delete(@PathVariable Long id) {
            ${domain}Service.delete(id);
            return new CommonResp<>();
        }
    
    }
    
  • 修改ServerGenerator.java

    package com.neilxu.train.generator.server;
    
    import com.neilxu.train.generator.util.FreemarkerUtil;
    import freemarker.template.TemplateException;
    import org.dom4j.Document;
    import org.dom4j.DocumentException;
    import org.dom4j.Node;
    import org.dom4j.io.SAXReader;
    
    import java.io.File;
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Map;
    
    public class ServerGenerator {
        static String serverPath = "[module]/src/main/java/com/neilxu/train/[module]/";
        static String pomPath = "generator\\pom.xml";
        static {
            new File(serverPath).mkdirs();
        }
    
        public static void main(String[] args) throws Exception {
            // 获取mybatis-generator
            String generatorPath = getGeneratorPath();
            // 比如generator-config-member.xml,得到module = member
            String module = generatorPath.replace("src/main/resources/generator-config-", "").replace(".xml", "");
            System.out.println("module: " + module);
            serverPath = serverPath.replace("[module]", module);
            // new File(servicePath).mkdirs();
            System.out.println("servicePath: " + serverPath);
    
            // 读取table节点
            Document document = new SAXReader().read("generator/" + generatorPath);
            Node table = document.selectSingleNode("//table");
            System.out.println(table);
            Node tableName = table.selectSingleNode("@tableName");
            Node domainObjectName = table.selectSingleNode("@domainObjectName");
            System.out.println(tableName.getText() + "/" + domainObjectName.getText());
    
            // 示例:表名 neilxu_test
            // Domain = neilxuTest
            String Domain = domainObjectName.getText();
            // domain = neilxuTest
            String domain = Domain.substring(0, 1).toLowerCase() + Domain.substring(1);
            // do_main = neilxu-test
            String do_main = tableName.getText().replaceAll("_", "-");
    
            // 组装参数
            Map<String, Object> param = new HashMap<>();
            param.put("Domain", Domain);
            param.put("domain", domain);
            param.put("do_main", do_main);
            System.out.println("组装参数:" + param);
    
            gen(Domain, param, "service");
            gen(Domain, param, "controller");
        }
    
        private static void gen(String Domain, Map<String, Object> param, String target) throws IOException, TemplateException {
            FreemarkerUtil.initConfig(target + ".ftl");
            String toPath = serverPath + target + "/";
            new File(toPath).mkdirs();
            String Target = target.substring(0, 1).toUpperCase() + target.substring(1);
            String fileName = toPath + Domain + Target + ".java";
            System.out.println("开始生成:" + fileName);
            FreemarkerUtil.generator(fileName, param);
        }
    
        private static String getGeneratorPath() throws DocumentException {
            SAXReader saxReader = new SAXReader();
            Map<String, String> map = new HashMap<String, String>();
            map.put("pom", "http://maven.apache.org/POM/4.0.0");
            saxReader.getDocumentFactory().setXPathNamespaceURIs(map);
            Document document = saxReader.read(pomPath);
            Node node = document.selectSingleNode("//pom:configurationFile");
            System.out.println(node.getText());
            return node.getText();
        }
    }
    
  • 测试

在这里插入图片描述

生成后覆盖原有文件,并无变化,表示成功

5.增加DbUtil,获取表字段信息

  • generator增加依赖

    generator/pom.xml

    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>compile</scope>
    </dependency>
    
  • 新增Field.java

    这个类用来存储数据库字段的信息

    com.neilxu.train.generator.util.Field

    package com.neilxu.train.generator.util;
    
    import lombok.Data;
    
    @Data
    public class Field {
        private String name; // 字段名:course_id
        private String nameHump; // 字段名小驼峰:courseId
        private String nameBigHump; // 字段名大驼峰:CourseId
        private String nameCn; // 中文名:课程
        private String type; // 字段类型:char(8)
        private String javaType; // java类型:String
        private String comment; // 注释:课程|ID
        private Boolean nullAble; // 是否可为空
        private Integer length; // 字符串长度
        private Boolean enums; // 是否是枚举
        private String enumsConst; // 枚举常量 COURSE_LEVEL
    
    }
    

    注意其中的枚举常量属性enumsConst,这个后续会用在生成前端的文件

  • 新增DbUtil.java

    配置数据源,连接数据库之后,通过sql查询数据库信息,获取表名,获取字段信息映射到java类。

    package com.neilxu.train.generator.util;
    
    import cn.hutool.core.util.StrUtil;
    
    import java.sql.*;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    
    public class DbUtil {
    
        public static String url = "";
        public static String user = "";
        public static String password = "";
    
        public static Connection getConnection() {
            Connection conn = null;
            try {
                Class.forName("com.mysql.cj.jdbc.Driver");
                String url = DbUtil.url;
                String user = DbUtil.user;
                String password = DbUtil.password;
                conn = DriverManager.getConnection(url, user, password);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return conn;
        }
    
        /**
         * 获得表注释
         * @param tableName
         * @return
         * @throws Exception
         */
        public static String getTableComment(String tableName) throws Exception {
            Connection conn = getConnection();
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery("select table_comment from information_schema.tables Where table_name = '" + tableName + "'");
            String tableNameCH = "";
            if (rs != null) {
                while(rs.next()) {
                    tableNameCH = rs.getString("table_comment");
                    break;
                }
            }
            rs.close();
            stmt.close();
            conn.close();
            System.out.println("表名:" + tableNameCH);
            return tableNameCH;
        }
    
        /**
         * 获得所有列信息
         * @param tableName
         * @return
         * @throws Exception
         */
        public static List<Field> getColumnByTableName(String tableName) throws Exception {
            List<Field> fieldList = new ArrayList<>();
            Connection conn = getConnection();
            Statement stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery("show full columns from `" + tableName + "`");
            if (rs != null) {
                while(rs.next()) {
                    String columnName = rs.getString("Field");
                    String type = rs.getString("Type");
                    String comment = rs.getString("Comment");
                    String nullAble = rs.getString("Null"); //YES NO
                    Field field = new Field();
                    field.setName(columnName);
                    field.setNameHump(lineToHump(columnName));
                    field.setNameBigHump(lineToBigHump(columnName));
                    field.setType(type);
                    field.setJavaType(DbUtil.sqlTypeToJavaType(rs.getString("Type")));
                    field.setComment(comment);
                    if (comment.contains("|")) {
                        field.setNameCn(comment.substring(0, comment.indexOf("|")));
                    } else {
                        field.setNameCn(comment);
                    }
                    field.setNullAble("YES".equals(nullAble));
                    if (type.toUpperCase().contains("varchar".toUpperCase())) {
                        String lengthStr = type.substring(type.indexOf("(") + 1, type.length() - 1);
                        field.setLength(Integer.valueOf(lengthStr));
                    } else {
                        field.setLength(0);
                    }
                    if (comment.contains("枚举")) {
                        field.setEnums(true);
    
                        // 以课程等级为例:从注释中的“枚举[CourseLevelEnum]”,得到enumsConst = COURSE_LEVEL
                        int start = comment.indexOf("[");
                        int end = comment.indexOf("]");
                        String enumsName = comment.substring(start + 1, end); // CourseLevelEnum
                        String enumsConst = StrUtil.toUnderlineCase(enumsName)
                                .toUpperCase().replace("_ENUM", "");
                        field.setEnumsConst(enumsConst);
                    } else {
                        field.setEnums(false);
                    }
                    fieldList.add(field);
                }
            }
            rs.close();
            stmt.close();
            conn.close();
            System.out.println("列信息:" + fieldList);
            return fieldList;
        }
    
        /**
         * 下划线转小驼峰:member_id 转成 memberId
         */
        public static String lineToHump(String str){
            Pattern linePattern = Pattern.compile("_(\\w)");
            str = str.toLowerCase();
            Matcher matcher = linePattern.matcher(str);
            StringBuffer sb = new StringBuffer();
            while(matcher.find()){
                matcher.appendReplacement(sb, matcher.group(1).toUpperCase());
            }
            matcher.appendTail(sb);
            return sb.toString();
        }
    
        /**
         * 下划线转大驼峰:member_id 转成 MemberId
         */
        public static String lineToBigHump(String str){
            String s = lineToHump(str);
            return s.substring(0, 1).toUpperCase() + s.substring(1);
        }
    
        /**
         * 数据库类型转为Java类型
         */
        public static String sqlTypeToJavaType(String sqlType) {
            if (sqlType.toUpperCase().contains("varchar".toUpperCase())
                    || sqlType.toUpperCase().contains("char".toUpperCase())
                    || sqlType.toUpperCase().contains("text".toUpperCase())) {
                return "String";
            } else if (sqlType.toUpperCase().contains("datetime".toUpperCase())) {
                return "Date";
            } else if (sqlType.toUpperCase().contains("bigint".toUpperCase())) {
                return "Long";
            } else if (sqlType.toUpperCase().contains("int".toUpperCase())) {
                return "Integer";
            } else if (sqlType.toUpperCase().contains("long".toUpperCase())) {
                return "Long";
            } else if (sqlType.toUpperCase().contains("decimal".toUpperCase())) {
                return "BigDecimal";
            } else if (sqlType.toUpperCase().contains("boolean".toUpperCase())) {
                return "Boolean";
            } else {
                return "String";
            }
        }
    }
    
  • 修改ServerGenerator.java

    com.neilxu.train.generator.server.ServerGenerator

    package com.neilxu.train.generator.server;
    
    import com.neilxu.train.generator.util.DbUtil;
    import com.neilxu.train.generator.util.Field;
    import com.neilxu.train.generator.util.FreemarkerUtil;
    import freemarker.template.TemplateException;
    import org.dom4j.Document;
    import org.dom4j.DocumentException;
    import org.dom4j.Node;
    import org.dom4j.io.SAXReader;
    
    import java.io.File;
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    public class ServerGenerator {
        static String serverPath = "[module]/src/main/java/com/neilxu/train/[module]/";
        static String pomPath = "generator\\pom.xml";
        static {
            new File(serverPath).mkdirs();
        }
    
        public static void main(String[] args) throws Exception {
            // 获取mybatis-generator
            String generatorPath = getGeneratorPath();
            // 比如generator-config-member.xml,得到module = member
            String module = generatorPath.replace("src/main/resources/generator-config-", "").replace(".xml", "");
            System.out.println("module: " + module);
            serverPath = serverPath.replace("[module]", module);
            // new File(servicePath).mkdirs();
            System.out.println("servicePath: " + serverPath);
    
            // 读取table节点
            Document document = new SAXReader().read("generator/" + generatorPath);
            Node table = document.selectSingleNode("//table");
            System.out.println(table);
            Node tableName = table.selectSingleNode("@tableName");
            Node domainObjectName = table.selectSingleNode("@domainObjectName");
            System.out.println(tableName.getText() + "/" + domainObjectName.getText());
    
            // 为DbUtil设置数据源
            Node connectionURL = document.selectSingleNode("//@connectionURL");
            Node userId = document.selectSingleNode("//@userId");
            Node password = document.selectSingleNode("//@password");
            System.out.println("url: " + connectionURL.getText());
            System.out.println("user: " + userId.getText());
            System.out.println("password: " + password.getText());
            DbUtil.url = connectionURL.getText();
            DbUtil.user = userId.getText();
            DbUtil.password = password.getText();
    
            // 示例:表名 jiawa_test
            // Domain = JiawaTest
            String Domain = domainObjectName.getText();
            // domain = jiawaTest
            String domain = Domain.substring(0, 1).toLowerCase() + Domain.substring(1);
            // do_main = jiawa-test
            String do_main = tableName.getText().replaceAll("_", "-");
            // 表中文名
            String tableNameCn = DbUtil.getTableComment(tableName.getText());
            List<Field> fieldList = DbUtil.getColumnByTableName(tableName.getText());
    
            // 组装参数
            Map<String, Object> param = new HashMap<>();
            param.put("Domain", Domain);
            param.put("domain", domain);
            param.put("do_main", do_main);
            System.out.println("组装参数:" + param);
    
            gen(Domain, param, "service");
            gen(Domain, param, "controller");
        }
    
        private static void gen(String Domain, Map<String, Object> param, String target) throws IOException, TemplateException {
            FreemarkerUtil.initConfig(target + ".ftl");
            String toPath = serverPath + target + "/";
            new File(toPath).mkdirs();
            String Target = target.substring(0, 1).toUpperCase() + target.substring(1);
            String fileName = toPath + Domain + Target + ".java";
            System.out.println("开始生成:" + fileName);
            FreemarkerUtil.generator(fileName, param);
        }
    
        private static String getGeneratorPath() throws DocumentException {
            SAXReader saxReader = new SAXReader();
            Map<String, String> map = new HashMap<String, String>();
            map.put("pom", "http://maven.apache.org/POM/4.0.0");
            saxReader.getDocumentFactory().setXPathNamespaceURIs(map);
            Document document = saxReader.read(pomPath);
            Node node = document.selectSingleNode("//pom:configurationFile");
            System.out.println(node.getText());
            return node.getText();
        }
    }
    
  • 测试

在这里插入图片描述

6.详解实体类生成器

  • 优化DbUtil.java

    美化json打印

    com.neilxu.train.generator.util.DbUtil

    System.out.println("列信息:" + JSONUtil.toJsonPrettyStr(fieldList));
    
  • 新建模板saveReq.ftl

    package com.neilxu.train.member.req;
    
    <#list typeSet as type>
    <#if type=='Date'>
    import java.util.Date;
    import com.fasterxml.jackson.annotation.JsonFormat;
    </#if>
    <#if type=='BigDecimal'>
    import java.math.BigDecimal;
    </#if>
    </#list>
    
    import jakarta.validation.constraints.NotBlank;
    import jakarta.validation.constraints.NotNull;
    
    @Data
    public class ${Domain}SaveReq {
    
        <#list fieldList as field>
        /**
         * ${field.comment}
         */
        <#if field.javaType=='Date'>
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
        </#if>
        <#if field.name!="id" && field.nameHump!="createdAt" && field.nameHump!="updatedAt">
            <#if !field.nullAble>
                <#if field.javaType=='String'>
        @NotBlank(message = "【${field.nameCn}】不能为空")
                <#else>
        @NotNull(message = "【${field.nameCn}】不能为空")
                </#if>
            </#if>
        </#if>
        private ${field.javaType} ${field.nameHump};
    
        </#list>
    
        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append(getClass().getSimpleName());
            sb.append(" [");
            sb.append("Hash = ").append(hashCode());
            <#list fieldList as field>
            sb.append(", ${field.nameHump}=").append(${field.nameHump});
            </#list>
            sb.append("]");
            return sb.toString();
        }
    }
    
  • 修改ServerGenerator.java

    com.neilxu.train.generator.server.ServerGenerator

    package com.neilxu.train.generator.server;
    
    import com.neilxu.train.generator.util.DbUtil;
    import com.neilxu.train.generator.util.Field;
    import com.neilxu.train.generator.util.FreemarkerUtil;
    import freemarker.template.TemplateException;
    import org.dom4j.Document;
    import org.dom4j.DocumentException;
    import org.dom4j.Node;
    import org.dom4j.io.SAXReader;
    
    import java.io.File;
    import java.io.IOException;
    import java.util.*;
    
    public class ServerGenerator {
        static String serverPath = "[module]/src/main/java/com/neilxu/train/[module]/";
        static String pomPath = "generator\\pom.xml";
        static {
            new File(serverPath).mkdirs();
        }
    
        public static void main(String[] args) throws Exception {
            // 获取mybatis-generator
            String generatorPath = getGeneratorPath();
            // 比如generator-config-member.xml,得到module = member
            String module = generatorPath.replace("src/main/resources/generator-config-", "").replace(".xml", "");
            System.out.println("module: " + module);
            serverPath = serverPath.replace("[module]", module);
            // new File(servicePath).mkdirs();
            System.out.println("servicePath: " + serverPath);
    
            // 读取table节点
            Document document = new SAXReader().read("generator/" + generatorPath);
            Node table = document.selectSingleNode("//table");
            System.out.println(table);
            Node tableName = table.selectSingleNode("@tableName");
            Node domainObjectName = table.selectSingleNode("@domainObjectName");
            System.out.println(tableName.getText() + "/" + domainObjectName.getText());
    
            // 为DbUtil设置数据源
            Node connectionURL = document.selectSingleNode("//@connectionURL");
            Node userId = document.selectSingleNode("//@userId");
            Node password = document.selectSingleNode("//@password");
            System.out.println("url: " + connectionURL.getText());
            System.out.println("user: " + userId.getText());
            System.out.println("password: " + password.getText());
            DbUtil.url = connectionURL.getText();
            DbUtil.user = userId.getText();
            DbUtil.password = password.getText();
    
            // 示例:表名 neilxu_test
            // Domain = neilxuTest
            String Domain = domainObjectName.getText();
            // domain = neilxuTest
            String domain = Domain.substring(0, 1).toLowerCase() + Domain.substring(1);
            // do_main = neilxu-test
            String do_main = tableName.getText().replaceAll("_", "-");
            // 表中文名
            String tableNameCn = DbUtil.getTableComment(tableName.getText());
            List<Field> fieldList = DbUtil.getColumnByTableName(tableName.getText());
            Set<String> typeSet = getJavaTypes(fieldList);
    
            // 组装参数
            Map<String, Object> param = new HashMap<>();
            param.put("Domain", Domain);
            param.put("domain", domain);
            param.put("do_main", do_main);
            param.put("tableNameCn", tableNameCn);
            param.put("fieldList", fieldList);
            param.put("typeSet", typeSet);
            System.out.println("组装参数:" + param);
    
            gen(Domain, param, "service", "service");
            gen(Domain, param, "controller", "controller");
            gen(Domain, param, "req", "saveReq");
        }
    
        private static void gen(String Domain, Map<String, Object> param, String packageName, String target) throws IOException, TemplateException {
            FreemarkerUtil.initConfig(target + ".ftl");
            String toPath = serverPath + packageName + "/";
            new File(toPath).mkdirs();
            String Target = target.substring(0, 1).toUpperCase() + target.substring(1);
            String fileName = toPath + Domain + Target + ".java";
            System.out.println("开始生成:" + fileName);
            FreemarkerUtil.generator(fileName, param);
        }
    
        private static String getGeneratorPath() throws DocumentException {
            SAXReader saxReader = new SAXReader();
            Map<String, String> map = new HashMap<String, String>();
            map.put("pom", "http://maven.apache.org/POM/4.0.0");
            saxReader.getDocumentFactory().setXPathNamespaceURIs(map);
            Document document = saxReader.read(pomPath);
            Node node = document.selectSingleNode("//pom:configurationFile");
            System.out.println(node.getText());
            return node.getText();
        }
    
        /**
         * 获取所有的Java类型,使用Set去重
         */
        private static Set<String> getJavaTypes(List<Field> fieldList) {
            Set<String> set = new HashSet<>();
            for (int i = 0; i < fieldList.size(); i++) {
                Field field = fieldList.get(i);
                set.add(field.getJavaType());
            }
            return set;
        }
    }
    
  • 测试

在这里插入图片描述

生成的req实体类和原来对比,代码逻辑不变,加了注释,这里注意memberId不需要校验,单独去掉

在这里插入图片描述

7.优化模版,按模块生成

  • controller.ftl

    package com.neilxu.train.${module}.controller;
    
    import com.neilxu.train.common.context.LoginMemberContext;
    import com.neilxu.train.common.resp.CommonResp;
    import com.neilxu.train.common.resp.PageResp;
    import com.neilxu.train.${module}.req.${Domain}QueryReq;
    import com.neilxu.train.${module}.req.${Domain}SaveReq;
    import com.neilxu.train.${module}.resp.${Domain}QueryResp;
    import com.neilxu.train.${module}.service.${Domain}Service;
    import jakarta.annotation.Resource;
    import jakarta.validation.Valid;
    import org.springframework.web.bind.annotation.*;
    
  • saveReq.ftl

    package com.neilxu.train.${module}.req;
    
  • service.ftl

    package com.neilxu.train.${module}.service;
    
    import cn.hutool.core.bean.BeanUtil;
    import cn.hutool.core.date.DateTime;
    import cn.hutool.core.util.ObjectUtil;
    import com.github.pagehelper.PageHelper;
    import com.github.pagehelper.PageInfo;
    import com.neilxu.train.common.context.LoginMemberContext;
    import com.neilxu.train.common.resp.PageResp;
    import com.neilxu.train.common.util.SnowUtil;
    import com.neilxu.train.${module}.domain.${Domain};
    import com.neilxu.train.${module}.domain.${Domain}Example;
    import com.neilxu.train.${module}.mapper.${Domain}Mapper;
    import com.neilxu.train.${module}.req.${Domain}QueryReq;
    import com.neilxu.train.${module}.req.${Domain}SaveReq;
    import com.neilxu.train.${module}.resp.${Domain}QueryResp;
    import jakarta.annotation.Resource;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Service;
    
    import java.util.List;
    
  • 修改ServerGenerator.java

    // 组装参数
    Map<String, Object> param = new HashMap<>();
    param.put("module", module);
    param.put("Domain", Domain);
    param.put("domain", domain);
    param.put("do_main", do_main);
    param.put("tableNameCn", tableNameCn);
    param.put("fieldList", fieldList);
    param.put("typeSet", typeSet);
    System.out.println("组装参数:" + param);
    
  • 测试

    重新生成一次,覆盖后代码不变

8.制作queryReq.ftl模板

  • queryReq.ftl

    generator/src/main/java/com/neilxu/train/generator/ftl/queryReq.ftl

    package com.neilxu.train.${module}.req;
    
    import com.neilxu.train.common.req.PageReq;
    
    @Data
    public class ${Domain}QueryReq extends PageReq {
    
    }
    
  • 减少service.ftl里特殊字段的部分,使其变得通用

在这里插入图片描述

9.制作queryResp.ftl模板

  • queryResp.ftl

    generator/src/main/java/com/neilxu/train/generator/ftl/queryResp.ftl

    package com.neilxu.train.${module}.resp;
    
    import com.fasterxml.jackson.databind.annotation.JsonSerialize;
    import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
    <#list typeSet as type>
    <#if type=='Date'>
    import java.util.Date;
    import com.fasterxml.jackson.annotation.JsonFormat;
    </#if>
    <#if type=='BigDecimal'>
    import java.math.BigDecimal;
    </#if>
    </#list>
    
    public class ${Domain}QueryResp {
    
        <#list fieldList as field>
        /**
         * ${field.comment}
         */
        <#if field.javaType=='Date'>
            <#if field.type=='time'>
        @JsonFormat(pattern = "HH:mm:ss",timezone = "GMT+8")
            <#elseif field.type=='date'>
        @JsonFormat(pattern = "yyyy-MM-dd",timezone = "GMT+8")
            <#else>
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
            </#if>
        </#if>
        <#if field.name=='id' || field.name?ends_with('_id')>
        @JsonSerialize(using= ToStringSerializer.class)
        </#if>
        private ${field.javaType} ${field.nameHump};
    
        </#list>
    
    }
    
  • 修改ServerGenerator.java

    gen(Domain, param, "req", "queryReq");
    gen(Domain, param, "resp", "queryResp");
    
  • 测试

    生成都没问题,这里把新的resp(有注释)保留

10.详解vue界面生成器

  • 制作vue.ftl模板

    generator/src/main/java/com/neilxu/train/generator/ftl/vue.ftl

    <template>
      <p>
        <a-space>
          <a-button type="primary" @click="handleQuery()">刷新</a-button>
          <#if !readOnly><a-button type="primary" @click="onAdd">新增</a-button></#if>
        </a-space>
      </p>
      <a-table :dataSource="${domain}s"
               :columns="columns"
               :pagination="pagination"
               @change="handleTableChange"
               :loading="loading">
        <template #bodyCell="{ column, record }">
          <template v-if="column.dataIndex === 'operation'">
            <#if !readOnly>
            <a-space>
              <a-popconfirm
                  title="删除后不可恢复,确认删除?"
                  @confirm="onDelete(record)"
                  ok-text="确认" cancel-text="取消">
                <a style="color: red">删除</a>
              </a-popconfirm>
              <a @click="onEdit(record)">编辑</a>
            </a-space>
            </#if>
          </template>
          <#list fieldList as field>
            <#if field.enums>
          <template v-else-if="column.dataIndex === '${field.nameHump}'">
            <span v-for="item in ${field.enumsConst}_ARRAY" :key="item.key">
              <span v-if="item.key === record.${field.nameHump}">
                {{item.value}}
              </span>
            </span>
          </template>
            </#if>
          </#list>
        </template>
      </a-table>
      <#if !readOnly>
      <a-modal v-model:visible="visible" title="${tableNameCn}" @ok="handleOk"
               ok-text="确认" cancel-text="取消">
        <a-form :model="${domain}" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
          <#list fieldList as field>
            <#if field.name!="id" && field.nameHump!="createTime" && field.nameHump!="updateTime">
          <a-form-item label="${field.nameCn}">
            <#if field.enums>
            <a-select v-model:value="${domain}.${field.nameHump}">
              <a-select-option v-for="item in ${field.enumsConst}_ARRAY" :key="item.key" :value="item.key">
                {{item.value}}
              </a-select-option>
            </a-select>
            <#elseif field.javaType=='Date'>
              <#if field.type=='time'>
            <a-time-picker v-model:value="${domain}.${field.nameHump}" valueFormat="HH:mm:ss" placeholder="请选择时间" />
              <#elseif field.type=='date'>
            <a-date-picker v-model:value="${domain}.${field.nameHump}" valueFormat="YYYY-MM-DD" placeholder="请选择日期" />
              <#else>
            <a-date-picker v-model:value="${domain}.${field.nameHump}" valueFormat="YYYY-MM-DD HH:mm:ss" show-time placeholder="请选择日期" />
              </#if>
            <#else>
            <a-input v-model:value="${domain}.${field.nameHump}" />
            </#if>
          </a-form-item>
            </#if>
          </#list>
        </a-form>
      </a-modal>
      </#if>
    </template>
    
    <script>
    import { defineComponent, ref, onMounted } from 'vue';
    import {notification} from "ant-design-vue";
    import axios from "axios";
    
    export default defineComponent({
      name: "${do_main}-view",
      setup() {
        <#list fieldList as field>
        <#if field.enums>
        const ${field.enumsConst}_ARRAY = window.${field.enumsConst}_ARRAY;
        </#if>
        </#list>
        const visible = ref(false);
        let ${domain} = ref({
          <#list fieldList as field>
          ${field.nameHump}: undefined,
          </#list>
        });
        const ${domain}s = ref([]);
        // 分页的三个属性名是固定的
        const pagination = ref({
          total: 0,
          current: 1,
          pageSize: 10,
        });
        let loading = ref(false);
        const columns = [
        <#list fieldList as field>
          <#if field.name!="id" && field.nameHump!="createTime" && field.nameHump!="updateTime">
        {
          title: '${field.nameCn}',
          dataIndex: '${field.nameHump}',
          key: '${field.nameHump}',
        },
          </#if>
        </#list>
        <#if !readOnly>
        {
          title: '操作',
          dataIndex: 'operation'
        }
        </#if>
        ];
    
        <#if !readOnly>
        const onAdd = () => {
          ${domain}.value = {};
          visible.value = true;
        };
    
        const onEdit = (record) => {
          ${domain}.value = window.Tool.copy(record);
          visible.value = true;
        };
    
        const onDelete = (record) => {
          axios.delete("/${module}/${do_main}/delete/" + record.id).then((response) => {
            const data = response.data;
            if (data.success) {
              notification.success({description: "删除成功!"});
              handleQuery({
                page: pagination.value.current,
                size: pagination.value.pageSize,
              });
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleOk = () => {
          axios.post("/${module}/${do_main}/save", ${domain}.value).then((response) => {
            let data = response.data;
            if (data.success) {
              notification.success({description: "保存成功!"});
              visible.value = false;
              handleQuery({
                page: pagination.value.current,
                size: pagination.value.pageSize
              });
            } else {
              notification.error({description: data.message});
            }
          });
        };
        </#if>
    
        const handleQuery = (param) => {
          if (!param) {
            param = {
              page: 1,
              size: pagination.value.pageSize
            };
          }
          loading.value = true;
          axios.get("/${module}/${do_main}/query-list", {
            params: {
              page: param.page,
              size: param.size
            }
          }).then((response) => {
            loading.value = false;
            let data = response.data;
            if (data.success) {
              ${domain}s.value = data.content.list;
              // 设置分页控件的值
              pagination.value.current = param.page;
              pagination.value.total = data.content.total;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleTableChange = (pagination) => {
          // console.log("看看自带的分页参数都有啥:" + pagination);
          handleQuery({
            page: pagination.current,
            size: pagination.pageSize
          });
        };
    
        onMounted(() => {
          handleQuery({
            page: 1,
            size: pagination.value.pageSize
          });
        });
    
        return {
          <#list fieldList as field>
          <#if field.enums>
          ${field.enumsConst}_ARRAY,
          </#if>
          </#list>
          ${domain},
          visible,
          ${domain}s,
          pagination,
          columns,
          handleTableChange,
          handleQuery,
          loading,
          <#if !readOnly>
          onAdd,
          handleOk,
          onEdit,
          onDelete
          </#if>
        };
      },
    });
    </script>
    
  • 修改ServerGenerator.java

    package com.neilxu.train.generator.server;
    
    import com.neilxu.train.generator.util.DbUtil;
    import com.neilxu.train.generator.util.Field;
    import com.neilxu.train.generator.util.FreemarkerUtil;
    import freemarker.template.TemplateException;
    import org.dom4j.Document;
    import org.dom4j.DocumentException;
    import org.dom4j.Node;
    import org.dom4j.io.SAXReader;
    
    import java.io.File;
    import java.io.IOException;
    import java.util.*;
    
    public class ServerGenerator {
        static boolean readOnly = false;
        static String vuePath = "web/src/views/main/";
        static String serverPath = "[module]/src/main/java/com/neilxu/train/[module]/";
        static String pomPath = "generator\\pom.xml";
        static {
            new File(serverPath).mkdirs();
        }
    
        public static void main(String[] args) throws Exception {
            // 获取mybatis-generator
            String generatorPath = getGeneratorPath();
            // 比如generator-config-member.xml,得到module = member
            String module = generatorPath.replace("src/main/resources/generator-config-", "").replace(".xml", "");
            System.out.println("module: " + module);
            serverPath = serverPath.replace("[module]", module);
            // new File(servicePath).mkdirs();
            System.out.println("servicePath: " + serverPath);
    
            // 读取table节点
            Document document = new SAXReader().read("generator/" + generatorPath);
            Node table = document.selectSingleNode("//table");
            System.out.println(table);
            Node tableName = table.selectSingleNode("@tableName");
            Node domainObjectName = table.selectSingleNode("@domainObjectName");
            System.out.println(tableName.getText() + "/" + domainObjectName.getText());
    
            // 为DbUtil设置数据源
            Node connectionURL = document.selectSingleNode("//@connectionURL");
            Node userId = document.selectSingleNode("//@userId");
            Node password = document.selectSingleNode("//@password");
            System.out.println("url: " + connectionURL.getText());
            System.out.println("user: " + userId.getText());
            System.out.println("password: " + password.getText());
            DbUtil.url = connectionURL.getText();
            DbUtil.user = userId.getText();
            DbUtil.password = password.getText();
    
            // 示例:表名 neilxu_test
            // Domain = neilxuTest
            String Domain = domainObjectName.getText();
            // domain = neilxuTest
            String domain = Domain.substring(0, 1).toLowerCase() + Domain.substring(1);
            // do_main = neilxu-test
            String do_main = tableName.getText().replaceAll("_", "-");
            // 表中文名
            String tableNameCn = DbUtil.getTableComment(tableName.getText());
            List<Field> fieldList = DbUtil.getColumnByTableName(tableName.getText());
            Set<String> typeSet = getJavaTypes(fieldList);
    
            // 组装参数
            Map<String, Object> param = new HashMap<>();
            param.put("module", module);
            param.put("Domain", Domain);
            param.put("domain", domain);
            param.put("do_main", do_main);
            param.put("tableNameCn", tableNameCn);
            param.put("fieldList", fieldList);
            param.put("typeSet", typeSet);
            param.put("readOnly", readOnly);
            System.out.println("组装参数:" + param);
    
    //        gen(Domain, param, "service", "service");
    //        gen(Domain, param, "controller", "controller");
    //        gen(Domain, param, "req", "saveReq");
    //        gen(Domain, param, "req", "queryReq");
    //        gen(Domain, param, "resp", "queryResp");
            genVue(do_main, param);
        }
    
        private static void gen(String Domain, Map<String, Object> param, String packageName, String target) throws IOException, TemplateException {
            FreemarkerUtil.initConfig(target + ".ftl");
            String toPath = serverPath + packageName + "/";
            new File(toPath).mkdirs();
            String Target = target.substring(0, 1).toUpperCase() + target.substring(1);
            String fileName = toPath + Domain + Target + ".java";
            System.out.println("开始生成:" + fileName);
            FreemarkerUtil.generator(fileName, param);
        }
    
        private static void genVue(String do_main, Map<String, Object> param) throws IOException, TemplateException {
            FreemarkerUtil.initConfig("vue.ftl");
            new File(vuePath).mkdirs();
            String fileName = vuePath + do_main + ".vue";
            System.out.println("开始生成:" + fileName);
            FreemarkerUtil.generator(fileName, param);
        }
    
        private static String getGeneratorPath() throws DocumentException {
            SAXReader saxReader = new SAXReader();
            Map<String, String> map = new HashMap<String, String>();
            map.put("pom", "http://maven.apache.org/POM/4.0.0");
            saxReader.getDocumentFactory().setXPathNamespaceURIs(map);
            Document document = saxReader.read(pomPath);
            Node node = document.selectSingleNode("//pom:configurationFile");
            System.out.println(node.getText());
            return node.getText();
        }
    
        /**
         * 获取所有的Java类型,使用Set去重
         */
        private static Set<String> getJavaTypes(List<Field> fieldList) {
            Set<String> set = new HashSet<>();
            for (int i = 0; i < fieldList.size(); i++) {
                Field field = fieldList.get(i);
                set.add(field.getJavaType());
            }
            return set;
        }
    }
    
  • 测试

    生成vue页面,覆盖原来的文件

    web/src/views/main/passenger.vue

    <template>
      <p>
        <a-space>
          <a-button type="primary" @click="handleQuery()">刷新</a-button>
          <a-button type="primary" @click="onAdd">新增</a-button>
        </a-space>
      </p>
      <a-table :dataSource="passengers"
               :columns="columns"
               :pagination="pagination"
               @change="handleTableChange"
               :loading="loading">
        <template #bodyCell="{ column, record }">
          <template v-if="column.dataIndex === 'operation'">
            <a-space>
              <a-popconfirm
                  title="删除后不可恢复,确认删除?"
                  @confirm="onDelete(record)"
                  ok-text="确认" cancel-text="取消">
                <a style="color: red">删除</a>
              </a-popconfirm>
              <a @click="onEdit(record)">编辑</a>
            </a-space>
          </template>
          <template v-else-if="column.dataIndex === 'type'">
            <span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.key">
              <span v-if="item.key === record.type">
                {{item.value}}
              </span>
            </span>
          </template>
        </template>
      </a-table>
      <a-modal v-model:visible="visible" title="乘车人" @ok="handleOk"
               ok-text="确认" cancel-text="取消">
        <a-form :model="passenger" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
          <a-form-item label="姓名">
            <a-input v-model:value="passenger.name" />
          </a-form-item>
          <a-form-item label="身份证">
            <a-input v-model:value="passenger.idCard" />
          </a-form-item>
          <a-form-item label="旅客类型">
            <a-select v-model:value="passenger.type">
              <a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.key" :value="item.key">
                {{item.value}}
              </a-select-option>
            </a-select>
          </a-form-item>
        </a-form>
      </a-modal>
    </template>
    
    <script>
    import { defineComponent, ref, onMounted } from 'vue';
    import {notification} from "ant-design-vue";
    import axios from "axios";
    
    export default defineComponent({
      name: "passenger-view",
      setup() {
        const PASSENGER_TYPE_ARRAY = window.PASSENGER_TYPE_ARRAY;
        const visible = ref(false);
        let passenger = ref({
          id: undefined,
          memberId: undefined,
          name: undefined,
          idCard: undefined,
          type: undefined,
          createTime: undefined,
          updateTime: undefined,
        });
        const passengers = ref([]);
        // 分页的三个属性名是固定的
        const pagination = ref({
          total: 0,
          current: 1,
          pageSize: 10,
        });
        let loading = ref(false);
        const columns = [
        {
          title: '姓名',
          dataIndex: 'name',
          key: 'name',
        },
        {
          title: '身份证',
          dataIndex: 'idCard',
          key: 'idCard',
        },
        {
          title: '旅客类型',
          dataIndex: 'type',
          key: 'type',
        },
        {
          title: '操作',
          dataIndex: 'operation'
        }
        ];
    
        const onAdd = () => {
          passenger.value = {};
          visible.value = true;
        };
    
        const onEdit = (record) => {
          passenger.value = window.Tool.copy(record);
          visible.value = true;
        };
    
        const onDelete = (record) => {
          axios.delete("/member/passenger/delete/" + record.id).then((response) => {
            const data = response.data;
            if (data.success) {
              notification.success({description: "删除成功!"});
              handleQuery({
                page: pagination.value.current,
                size: pagination.value.pageSize,
              });
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleOk = () => {
          axios.post("/member/passenger/save", passenger.value).then((response) => {
            let data = response.data;
            if (data.success) {
              notification.success({description: "保存成功!"});
              visible.value = false;
              handleQuery({
                page: pagination.value.current,
                size: pagination.value.pageSize
              });
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleQuery = (param) => {
          if (!param) {
            param = {
              page: 1,
              size: pagination.value.pageSize
            };
          }
          loading.value = true;
          axios.get("/member/passenger/query-list", {
            params: {
              page: param.page,
              size: param.size
            }
          }).then((response) => {
            loading.value = false;
            let data = response.data;
            if (data.success) {
              passengers.value = data.content.list;
              // 设置分页控件的值
              pagination.value.current = param.page;
              pagination.value.total = data.content.total;
            } else {
              notification.error({description: data.message});
            }
          });
        };
    
        const handleTableChange = (pagination) => {
          // console.log("看看自带的分页参数都有啥:" + pagination);
          handleQuery({
            page: pagination.current,
            size: pagination.pageSize
          });
        };
    
        onMounted(() => {
          handleQuery({
            page: 1,
            size: pagination.value.pageSize
          });
        });
    
        return {
          PASSENGER_TYPE_ARRAY,
          passenger,
          visible,
          passengers,
          pagination,
          columns,
          handleTableChange,
          handleQuery,
          loading,
          onAdd,
          handleOk,
          onEdit,
          onDelete
        };
      },
    });
    </script>
    

    效果

在这里插入图片描述

测试功能没有问题

11.详解前端枚举代码生成器

  • 修改前端枚举,将key/value改成code/desc,保持和后端一致

    • vue.ftl

      <template>
        <p>
          <a-space>
            <a-button type="primary" @click="handleQuery()">刷新</a-button>
            <#if !readOnly><a-button type="primary" @click="onAdd">新增</a-button></#if>
          </a-space>
        </p>
        <a-table :dataSource="${domain}s"
                 :columns="columns"
                 :pagination="pagination"
                 @change="handleTableChange"
                 :loading="loading">
          <template #bodyCell="{ column, record }">
            <template v-if="column.dataIndex === 'operation'">
              <#if !readOnly>
              <a-space>
                <a-popconfirm
                    title="删除后不可恢复,确认删除?"
                    @confirm="onDelete(record)"
                    ok-text="确认" cancel-text="取消">
                  <a style="color: red">删除</a>
                </a-popconfirm>
                <a @click="onEdit(record)">编辑</a>
              </a-space>
              </#if>
            </template>
            <#list fieldList as field>
              <#if field.enums>
            <template v-else-if="column.dataIndex === '${field.nameHump}'">
              <span v-for="item in ${field.enumsConst}_ARRAY" :key="item.code">
                <span v-if="item.code === record.${field.nameHump}">
                  {{item.desc}}
                </span>
              </span>
            </template>
              </#if>
            </#list>
          </template>
        </a-table>
        <#if !readOnly>
        <a-modal v-model:visible="visible" title="${tableNameCn}" @ok="handleOk"
                 ok-text="确认" cancel-text="取消">
          <a-form :model="${domain}" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
            <#list fieldList as field>
              <#if field.name!="id" && field.nameHump!="createTime" && field.nameHump!="updateTime">
            <a-form-item label="${field.nameCn}">
              <#if field.enums>
              <a-select v-model:value="${domain}.${field.nameHump}">
                <a-select-option v-for="item in ${field.enumsConst}_ARRAY" :key="item.code" :value="item.code">
                  {{item.desc}}
                </a-select-option>
              </a-select>
              <#elseif field.javaType=='Date'>
                <#if field.type=='time'>
              <a-time-picker v-model:value="${domain}.${field.nameHump}" valueFormat="HH:mm:ss" placeholder="请选择时间" />
                <#elseif field.type=='date'>
              <a-date-picker v-model:value="${domain}.${field.nameHump}" valueFormat="YYYY-MM-DD" placeholder="请选择日期" />
                <#else>
              <a-date-picker v-model:value="${domain}.${field.nameHump}" valueFormat="YYYY-MM-DD HH:mm:ss" show-time placeholder="请选择日期" />
                </#if>
              <#else>
              <a-input v-model:value="${domain}.${field.nameHump}" />
              </#if>
            </a-form-item>
              </#if>
            </#list>
          </a-form>
        </a-modal>
        </#if>
      </template>
      
      <script>
      import { defineComponent, ref, onMounted } from 'vue';
      import {notification} from "ant-design-vue";
      import axios from "axios";
      
      export default defineComponent({
        name: "${do_main}-view",
        setup() {
          <#list fieldList as field>
          <#if field.enums>
          const ${field.enumsConst}_ARRAY = window.${field.enumsConst}_ARRAY;
          </#if>
          </#list>
          const visible = ref(false);
          let ${domain} = ref({
            <#list fieldList as field>
            ${field.nameHump}: undefined,
            </#list>
          });
          const ${domain}s = ref([]);
          // 分页的三个属性名是固定的
          const pagination = ref({
            total: 0,
            current: 1,
            pageSize: 10,
          });
          let loading = ref(false);
          const columns = [
          <#list fieldList as field>
            <#if field.name!="id" && field.nameHump!="createTime" && field.nameHump!="updateTime">
          {
            title: '${field.nameCn}',
            dataIndex: '${field.nameHump}',
            key: '${field.nameHump}',
          },
            </#if>
          </#list>
          <#if !readOnly>
          {
            title: '操作',
            dataIndex: 'operation'
          }
          </#if>
          ];
      
          <#if !readOnly>
          const onAdd = () => {
            ${domain}.value = {};
            visible.value = true;
          };
      
          const onEdit = (record) => {
            ${domain}.value = window.Tool.copy(record);
            visible.value = true;
          };
      
          const onDelete = (record) => {
            axios.delete("/${module}/${do_main}/delete/" + record.id).then((response) => {
              const data = response.data;
              if (data.success) {
                notification.success({description: "删除成功!"});
                handleQuery({
                  page: pagination.value.current,
                  size: pagination.value.pageSize,
                });
              } else {
                notification.error({description: data.message});
              }
            });
          };
      
          const handleOk = () => {
            axios.post("/${module}/${do_main}/save", ${domain}.value).then((response) => {
              let data = response.data;
              if (data.success) {
                notification.success({description: "保存成功!"});
                visible.value = false;
                handleQuery({
                  page: pagination.value.current,
                  size: pagination.value.pageSize
                });
              } else {
                notification.error({description: data.message});
              }
            });
          };
          </#if>
      
          const handleQuery = (param) => {
            if (!param) {
              param = {
                page: 1,
                size: pagination.value.pageSize
              };
            }
            loading.value = true;
            axios.get("/${module}/${do_main}/query-list", {
              params: {
                page: param.page,
                size: param.size
              }
            }).then((response) => {
              loading.value = false;
              let data = response.data;
              if (data.success) {
                ${domain}s.value = data.content.list;
                // 设置分页控件的值
                pagination.value.current = param.page;
                pagination.value.total = data.content.total;
              } else {
                notification.error({description: data.message});
              }
            });
          };
      
          const handleTableChange = (pagination) => {
            // console.log("看看自带的分页参数都有啥:" + pagination);
            handleQuery({
              page: pagination.current,
              size: pagination.pageSize
            });
          };
      
          onMounted(() => {
            handleQuery({
              page: 1,
              size: pagination.value.pageSize
            });
          });
      
          return {
            <#list fieldList as field>
            <#if field.enums>
            ${field.enumsConst}_ARRAY,
            </#if>
            </#list>
            ${domain},
            visible,
            ${domain}s,
            pagination,
            columns,
            handleTableChange,
            handleQuery,
            loading,
            <#if !readOnly>
            onAdd,
            handleOk,
            onEdit,
            onDelete
            </#if>
          };
        },
      });
      </script>
      
    • web/src/assets/js/enums.js

      PASSENGER_TYPE_ARRAY = [{code: "1", desc: "成人"}, {code: "2", desc: "儿童"}, {code: "3", desc: "学生"}];
      
  • 增加枚举生成器EnumGenerator.java

    • generator引入member依赖

      <dependency>
          <groupId>com.neilxu</groupId>
          <artifactId>member</artifactId>
          <version>0.0.1-SNAPSHOT</version>
          <scope>compile</scope>
      </dependency>
      
    • EnumGenerator.java

      package com.neilxu.train.generator.gen;
      
      import cn.hutool.core.util.StrUtil;
      import com.neilxu.train.member.enums.PassengerTypeEnum;
      
      import java.io.FileOutputStream;
      import java.io.OutputStreamWriter;
      import java.lang.reflect.Method;
      
      public class EnumGenerator {
          static String path = "web/src/assets/js/enums.js";
      
          public static void main(String[] args) {
              StringBuffer bufferObject = new StringBuffer();
              StringBuffer bufferArray = new StringBuffer();
              long begin = System.currentTimeMillis();
              try {
                  toJson(PassengerTypeEnum.class, bufferObject, bufferArray);
      
                  StringBuffer buffer = bufferObject.append("\r\n").append(bufferArray);
                  writeJs(buffer);
              } catch (Exception e) {
                  e.printStackTrace();
              }
              long end = System.currentTimeMillis();
              System.out.println("执行耗时:" + (end - begin) + " 毫秒");
          }
      
          private static void toJson(Class clazz, StringBuffer bufferObject, StringBuffer bufferArray) throws Exception {
              // enumConst:将YesNoEnum变成YES_NO
              String enumConst = StrUtil.toUnderlineCase(clazz.getSimpleName())
                      .toUpperCase().replace("_ENUM", "");
              Object[] objects = clazz.getEnumConstants();
              Method name = clazz.getMethod("name");
              Method getDesc = clazz.getMethod("getDesc");
              Method getCode = clazz.getMethod("getCode");
      
              // 生成对象
              bufferObject.append(enumConst).append("={");
              for (int i = 0; i < objects.length; i++) {
                  Object obj = objects[i];
                  bufferObject.append(name.invoke(obj)).append(":{code:\"").append(getCode.invoke(obj)).append("\", desc:\"").append(getDesc.invoke(obj)).append("\"}");
                  if (i < objects.length - 1) {
                      bufferObject.append(",");
                  }
              }
              bufferObject.append("};\r\n");
      
              // 生成数组
              bufferArray.append(enumConst).append("_ARRAY=[");
              for (int i = 0; i < objects.length; i++) {
                  Object obj = objects[i];
                  bufferArray.append("{code:\"").append(getCode.invoke(obj)).append("\", desc:\"").append(getDesc.invoke(obj)).append("\"}");
                  if (i < objects.length - 1) {
                      bufferArray.append(",");
                  }
              }
              bufferArray.append("];\r\n");
          }
      
          /**
           * 写文件
           * @param stringBuffer
           */
          public static void writeJs(StringBuffer stringBuffer) {
              FileOutputStream out = null;
              try {
                  out = new FileOutputStream(path);
                  OutputStreamWriter osw = new OutputStreamWriter(out, "UTF-8");
                  System.out.println(path);
                  osw.write(stringBuffer.toString());
                  osw.close();
              } catch (Exception e) {
                  e.printStackTrace();
              }
              finally {
                  try {
                      out.close();
                  } catch (Exception e) {
                      e.printStackTrace();
                  }
      
              }
          }
      
      }
      
    • 测试

      生成新的enums.js

      PASSENGER_TYPE={ADULT:{code:"1", desc:"成人"},CHILD:{code:"2", desc:"儿童"},STUDENT:{code:"3", desc:"学生"}};
      
      PASSENGER_TYPE_ARRAY=[{code:"1", desc:"成人"},{code:"2", desc:"儿童"},{code:"3", desc:"学生"}];
      

三、利用代码生成器实现:火车基础数据管理功能

完成火车基础数据管理功能:车站、车次、车箱、座位

1.更换远程代码仓库

课程讲解了如何更换本地所关联的远程仓库,这里做下笔记

在这里插入图片描述

先移除原绑定的远程仓库,然后再绑定新的远程仓库

在这里插入图片描述

然后再将本地git push上去

在这里插入图片描述

2.项目中增加admin控台模块

后台一般不做注册,只做登录

  • 新建admin文件夹

    复制web文件夹除了library的部分到admin

    然后修改几处地方 name是web的为admin,如图所示

在这里插入图片描述

最后npm install

在这里插入图片描述

  • 测试启动

    修改admin/package.json,改变端口

    "scripts": {
      "serve-dev": "vue-cli-service serve --mode dev --port 9001",
      "serve-prod": "vue-cli-service serve --mode prod --port 9001",
      "build": "vue-cli-service build",
      "lint": "vue-cli-service lint"
    },
    

在这里插入图片描述

  • admin去掉登录页面,去掉member和登录相关的拦截,去掉乘车人页面

    • the-header.vue

      <template>
        <a-layout-header class="header">
          <div class="logo" />
          <div style="float: right; color: white;">
            欢迎使用管理控台
          </div>
          <a-menu
              v-model:selectedKeys="selectedKeys"
              theme="dark"
              mode="horizontal"
              :style="{ lineHeight: '64px' }"
          >
            <a-menu-item key="/welcome">
              <router-link to="/welcome">
                <coffee-outlined /> &nbsp; 欢迎
              </router-link>
            </a-menu-item>
            <a-menu-item key="/about">
              <router-link to="/about">
                <user-outlined /> &nbsp; 关于
              </router-link>
            </a-menu-item>
          </a-menu>
        </a-layout-header>
      </template>
      
      <script>
      import {defineComponent, ref, watch} from 'vue';
      import router from '@/router'
      
      export default defineComponent({
        name: "the-header-view",
        setup() {
          const selectedKeys = ref([]);
      
          watch(() => router.currentRoute.value.path, (newValue) => {
            console.log('watch', newValue);
            selectedKeys.value = [];
            selectedKeys.value.push(newValue);
          }, {immediate: true});
          return {
            selectedKeys
          };
        },
      });
      </script>
      
      <!-- Add "scoped" attribute to limit CSS to this component only -->
      <style scoped>
      
      </style>
      
    • the-sider.vue

      <template>
        <a-layout-sider width="200" style="background: #fff">
          <a-menu
              v-model:selectedKeys="selectedKeys"
              mode="inline"
              :style="{ height: '100%', borderRight: 0 }"
          >
            <a-menu-item key="/welcome">
              <router-link to="/welcome">
                <coffee-outlined /> &nbsp; 欢迎
              </router-link>
            </a-menu-item>
            <a-menu-item key="/about">
              <router-link to="/about">
                <user-outlined /> &nbsp; 关于
              </router-link>
            </a-menu-item>
          </a-menu>
        </a-layout-sider>
      </template>
      
      <script>
      import {defineComponent, ref, watch} from 'vue';
      import router from "@/router";
      
      export default defineComponent({
        name: "the-sider-view",
        setup() {
          const selectedKeys = ref([]);
      
          watch(() => router.currentRoute.value.path, (newValue) => {
            console.log('watch', newValue);
            selectedKeys.value = [];
            selectedKeys.value.push(newValue);
          }, {immediate: true});
          return {
            selectedKeys
          };
        },
      });
      </script>
      
      <!-- Add "scoped" attribute to limit CSS to this component only -->
      <style scoped>
      
      </style>
      
    • main.js

      import { createApp } from 'vue'
      import App from './App.vue'
      import router from './router'
      import store from './store'
      import Antd from 'ant-design-vue';
      import 'ant-design-vue/dist/antd.css';
      import * as Icons from '@ant-design/icons-vue';
      import axios from 'axios';
      import './assets/js/enums';
      
      const app = createApp(App);
      app.use(Antd).use(store).use(router).mount('#app');
      
      // 全局使用图标
      const icons = Icons;
      for (const i in icons) {
          app.component(i, icons[i]);
      }
      
      /**
       * axios拦截器
       */
      axios.interceptors.request.use(function (config) {
          console.log('请求参数:', config);
          return config;
      }, error => {
          return Promise.reject(error);
      });
      axios.interceptors.response.use(function (response) {
          console.log('返回结果:', response);
          return response;
      }, error => {
          console.log('返回错误:', error);
          return Promise.reject(error);
      });
      axios.defaults.baseURL = process.env.VUE_APP_SERVER;
      console.log('环境:', process.env.NODE_ENV);
      console.log('服务端:', process.env.VUE_APP_SERVER);
      
    • admin/src/router/index.js

      import { createRouter, createWebHistory } from 'vue-router'
      
      const routes = [{
        path: '/',
        component: () => import('../views/main.vue'),
        children: [{
          path: 'welcome',
          component: () => import('../views/main/welcome.vue'),
        }, {
          path: 'about',
          component: () => import('../views/main/about.vue'),
        }]
      }, {
        path: '',
        redirect: '/welcome'
      }];
      
      const router = createRouter({
        history: createWebHistory(process.env.BASE_URL),
        routes
      })
      
      export default router
      
    • about.vue

      <template>
        <h1>关于neilxu 12306</h1>
      </template>
      <script>
      import { defineComponent } from 'vue';
      
      export default defineComponent({
        setup() {
          return {
          };
        },
      });
      </script>
      <style>
      </style>
      
    • admin/src/router/index.js

      import { createStore } from 'vuex'
      
      export default createStore({
        state: {
        },
        getters: {
        },
        mutations: {
        },
        actions: {
        },
        modules: {
        }
      })
      
    • 效果

在这里插入图片描述

  • 去掉没用的HelloWorld组件,修改logo区域

    • the-header.vue

      <template>
        <a-layout-header class="header">
          <div class="logo">
            <router-link to="/welcome" style="color: white; font-size: 18px">
              neilxu 12306控台
            </router-link>
          </div>
          <div style="float: right; color: white;">
            欢迎使用管理控台
          </div>
          <a-menu
              v-model:selectedKeys="selectedKeys"
              theme="dark"
              mode="horizontal"
              :style="{ lineHeight: '64px' }"
          >
            <a-menu-item key="/welcome">
              <router-link to="/welcome">
                <coffee-outlined /> &nbsp; 欢迎
              </router-link>
            </a-menu-item>
            <a-menu-item key="/about">
              <router-link to="/about">
                <user-outlined /> &nbsp; 关于
              </router-link>
            </a-menu-item>
          </a-menu>
        </a-layout-header>
      </template>
      
      <script>
      import {defineComponent, ref, watch} from 'vue';
      import router from '@/router'
      
      export default defineComponent({
        name: "the-header-view",
        setup() {
          const selectedKeys = ref([]);
      
          watch(() => router.currentRoute.value.path, (newValue) => {
            console.log('watch', newValue);
            selectedKeys.value = [];
            selectedKeys.value.push(newValue);
          }, {immediate: true});
          return {
            selectedKeys
          };
        },
      });
      </script>
      
      <!-- Add "scoped" attribute to limit CSS to this component only -->
      <style scoped>
      .logo {
        float: left;
        height: 31px;
        width: 150px;
        color: white;
        font-size: 20px;
      }
      </style>
      
    • main.vue

      <template>
        <a-layout id="components-layout-demo-top-side-2">
          <the-header-view></the-header-view>
          <a-layout>
            <the-sider-view></the-sider-view>
            <a-layout-content
                :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
            >
              <router-view></router-view>
            </a-layout-content>
          </a-layout>
        </a-layout>
      </template>
      <script>
      import { defineComponent } from 'vue';
      import TheHeaderView from "@/components/the-header";
      import TheSiderView from "@/components/the-sider";
      
      export default defineComponent({
        components: {
          TheSiderView,
          TheHeaderView,
        },
        setup() {
          return {
          };
        },
      });
      </script>
      <style>
      
      </style>
      
    • 效果

在这里插入图片描述

3.项目中增加business业务模块

  • 新建business模块

在这里插入图片描述

  • 修改pom

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <parent>
            <artifactId>train</artifactId>
            <groupId>com.neilxu</groupId>
            <version>0.0.1-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
    
        <artifactId>business</artifactId>
    
        <properties>
            <maven.compiler.source>17</maven.compiler.source>
            <maven.compiler.target>17</maven.compiler.target>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>com.neilxu</groupId>
                <artifactId>common</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-devtools</artifactId>
                <scope>runtime</scope>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    
    </project>
    
  • 新增启动类

    com.neilxu.train.business.config.BusinessApplication

    package com.neilxu.train.business.config;
    
    import org.mybatis.spring.annotation.MapperScan;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.core.env.Environment;
    
    @SpringBootApplication
    @ComponentScan("com.neilxu")
    @MapperScan("com.neilxu.train.*.mapper")
    public class BusinessApplication {
    
        private static final Logger LOG = LoggerFactory.getLogger(BusinessApplication.class);
    
        public static void main(String[] args) {
            SpringApplication app = new SpringApplication(BusinessApplication.class);
            Environment env = app.run(args).getEnvironment();
            LOG.info("启动成功!!");
            LOG.info("测试地址: \thttp://127.0.0.1:{}{}/hello", env.getProperty("server.port"), env.getProperty("server.servlet.context-path"));
        }
    }
    
  • 新增TestController.java

    com.neilxu.train.business.controller.TestController

    package com.neilxu.train.business.controller;
    
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class TestController {
        @GetMapping("/hello")
        public String hello() {
            return "Hello World! Business!";
        }
    }
    
  • 新增配置文件

    business/src/main/resources/application.properties

    server.port=8002
    server.servlet.context-path=/business
    
    # 数据库连接
    spring.datasource.url=jdbc:mysql://localhost/train_business?characterEncoding=UTF8&autoReconnect=true&serverTimezone=Asia/Shanghai
    spring.datasource.username=train_business
    spring.datasource.password=train_business
    spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    # mybatis xml路径
    mybatis.mapper-locations=classpath:/mapper/**/*.xml
    logging.level.com.neilxu.train.business.mapper=trace
    

    business/src/main/resources/logback-spring.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
        <!-- 修改一下路径-->
        <property name="PATH" value="./log/business"></property>
    
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <!--            <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %blue(%-50logger{50}:%-4line) %thread %green(%-18X{LOG_ID}) %msg%n</Pattern>-->
                <Pattern>%d{mm:ss.SSS} %highlight(%-5level) %blue(%-30logger{30}:%-4line) %thread %green(%-18X{LOG_ID}) %msg%n</Pattern>
            </encoder>
        </appender>
    
        <appender name="TRACE_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>${PATH}/trace.log</file>
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <FileNamePattern>${PATH}/trace.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
                <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <maxFileSize>10MB</maxFileSize>
                </timeBasedFileNamingAndTriggeringPolicy>
            </rollingPolicy>
            <layout>
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n</pattern>
            </layout>
        </appender>
    
        <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>${PATH}/error.log</file>
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <FileNamePattern>${PATH}/error.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
                <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <maxFileSize>10MB</maxFileSize>
                </timeBasedFileNamingAndTriggeringPolicy>
            </rollingPolicy>
            <layout>
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %-50logger{50}:%-4line %green(%-18X{LOG_ID}) %msg%n</pattern>
            </layout>
            <filter class="ch.qos.logback.classic.filter.LevelFilter">
                <level>ERROR</level>
                <onMatch>ACCEPT</onMatch>
                <onMismatch>DENY</onMismatch>
            </filter>
        </appender>
    
        <root level="ERROR">
            <appender-ref ref="ERROR_FILE" />
        </root>
    
        <root level="TRACE">
            <appender-ref ref="TRACE_FILE" />
        </root>
    
        <root level="INFO">
            <appender-ref ref="STDOUT" />
        </root>
    </configuration>
    
  • 修改gateway的application.properties

    # 路由转发,将/business/...的请求转发了business模块
    spring.cloud.gateway.routes[1].id=business
    spring.cloud.gateway.routes[1].uri=http://127.0.0.1:8002
    #spring.cloud.gateway.routes[1].uri=lb://business
    spring.cloud.gateway.routes[1].predicates[0]=Path=/business/**
    
  • 测试

    http/business-test.http

    GET http://localhost:8002/business/hello
    Accept: application/json
    
    ###
    
    GET http://localhost:8000/business/hello
    Accept: application/json
    
    ###
    

在这里插入图片描述

4.为business模块配置持久层生成器

  • 新增train_business数据库

在这里插入图片描述

  • 执行sql

    sql/business.sql

    drop table if exists `member`;
    create table `member` (
      `id` bigint not null comment 'id',
      `mobile` varchar(11) comment '手机号',
      primary key (`id`),
      unique key `mobile_unique` (`mobile`)
    ) engine=innodb default charset=utf8mb4 comment='会员';
    
  • 修改generator/pom.xml

    <!--<configurationFile>src/main/resources/generator-config-member.xml</configurationFile>-->
    <configurationFile>src/main/resources/generator-config-business.xml</configurationFile>
    
  • 修改generator-config-business.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE generatorConfiguration
            PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
            "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
    
    <generatorConfiguration>
        <context id="Mysql" targetRuntime="MyBatis3" defaultModelType="flat">
    
            <!-- 自动检查关键字,为关键字增加反引号 -->
            <property name="autoDelimitKeywords" value="true"/>
            <property name="beginningDelimiter" value="`"/>
            <property name="endingDelimiter" value="`"/>
    
            <!--覆盖生成XML文件-->
            <plugin type="org.mybatis.generator.plugins.UnmergeableXmlMappersPlugin" />
            <!-- 生成的实体类添加toString()方法 -->
            <plugin type="org.mybatis.generator.plugins.ToStringPlugin"/>
    
            <!-- 不生成注释 -->
            <commentGenerator>
                <property name="suppressAllComments" value="true"/>
            </commentGenerator>
    
            <!-- 配置数据源,需要根据自己的项目修改 -->
            <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                            connectionURL="jdbc:mysql://localhost/train_business?serverTimezone=Asia/Shanghai"
                            userId="train_business"
                            password="train_business">
            </jdbcConnection>
    
            <!-- domain类的位置 targetProject是相对pom.xml的路径-->
            <javaModelGenerator targetProject="..\business\src\main\java"
                                targetPackage="com.neilxu.train.business.domain"/>
    
            <!-- mapper xml的位置 targetProject是相对pom.xml的路径 -->
            <sqlMapGenerator targetProject="..\business\src\main\resources"
                             targetPackage="mapper"/>
    
            <!-- mapper类的位置 targetProject是相对pom.xml的路径 -->
            <javaClientGenerator targetProject="..\business\src\main\java"
                                 targetPackage="com.neilxu.train.business.mapper"
                                 type="XMLMAPPER"/>
    
            <table tableName="member" domainObjectName="Member"/>
        </context>
    </generatorConfiguration>
    
  • 测试

    这里注意:

在这里插入图片描述

如果member install 错误,先install train,再install member

执行代码生成:

在这里插入图片描述

5.快速生成车站基础数据

  • 增加车站表,生成持久层代码及前后端代码

    先将上节生成的member相关的类删除

    • business.sql

      drop table if exists `station`;
      create table `station` (
        `id` bigint not null comment 'id',
        `name` varchar(20) not null comment '站名',
        `name_pinyin` varchar(50) not null comment '站名拼音',
        `name_py` varchar(50) not null comment '站名拼音首字母',
        `create_time` datetime(3) comment '新增时间',
        `update_time` datetime(3) comment '修改时间',
        primary key (`id`),
        unique key `name_unique` (`name`)
      ) engine=innodb default charset=utf8mb4 comment='车站';
      
    • 修改generator-config-business.xml

      <!--        <table tableName="member" domainObjectName="Member"/>-->
              <table tableName="station" domainObjectName="Station"/>
      
    • 修改controller.ftl(注意后面会改回来,新增一个adminController.ftl)

      后面生成的接口是admin的,所以不需要memberId查询条件

      下面的去掉

在这里插入图片描述

  • 修改ServerGenerator.java

    //    static String vuePath = "web/src/views/main/";
        static String vuePath = "admin/src/views/main/";
    
    gen(Domain, param, "service", "service");
    gen(Domain, param, "controller", "controller");
    gen(Domain, param, "req", "saveReq");
    gen(Domain, param, "req", "queryReq");
    gen(Domain, param, "resp", "queryResp");
    genVue(do_main, param);
    
  • 生成持久层代码和前后端代码

在这里插入图片描述

在这里插入图片描述

  • 手动改下前端代码,加上路由和侧边菜单

    • admin/src/components/the-sider.vue

      <a-menu-item key="/station">
        <router-link to="/station">
          <user-outlined /> &nbsp; 车站管理
        </router-link>
      </a-menu-item>
      
    • admin/src/router/index.js

      const routes = [{
        path: '/',
        component: () => import('../views/main.vue'),
        children: [{
          path: 'welcome',
          component: () => import('../views/main/welcome.vue'),
        }, {
          path: 'about',
          component: () => import('../views/main/about.vue'),
        }, {
          path: 'station',
          component: () => import('../views/main/station.vue'),
        }]
      }, {
        path: '',
        redirect: '/welcome'
      }];
      
  • 修改生成器,生成admin相关的代码,测试成功

    发现进去后后端报错没有登录,原因是这些被拦截了,但是实际目前是需要将这些接口归入admin下的,不被之前LoginMemberFilter.java拦截才对

在这里插入图片描述

- **修改controller.ftl,新增generator/src/main/java/com/neilxu/train/generator/ftl/adminController.ftl**

  generator/src/main/java/com/neilxu/train/generator/ftl/controller.ftl

  改回去

  ```
  package com.neilxu.train.${module}.controller;
  
  import com.neilxu.train.common.context.LoginMemberContext;
  import com.neilxu.train.common.resp.CommonResp;
  import com.neilxu.train.common.resp.PageResp;
  import com.neilxu.train.${module}.req.${Domain}QueryReq;
  import com.neilxu.train.${module}.req.${Domain}SaveReq;
  import com.neilxu.train.${module}.resp.${Domain}QueryResp;
  import com.neilxu.train.${module}.service.${Domain}Service;
  import jakarta.annotation.Resource;
  import jakarta.validation.Valid;
  import org.springframework.web.bind.annotation.*;
  
  @RestController
  @RequestMapping("/${do_main}")
  public class ${Domain}Controller {
  
      @Resource
      private ${Domain}Service ${domain}Service;
  
      @PostMapping("/save")
      public CommonResp<Object> save(@Valid @RequestBody ${Domain}SaveReq req) {
          ${domain}Service.save(req);
          return new CommonResp<>();
      }
  
      @GetMapping("/query-list")
      public CommonResp<PageResp<${Domain}QueryResp>> queryList(@Valid ${Domain}QueryReq req) {
          req.setMemberId(LoginMemberContext.getId());
          PageResp<${Domain}QueryResp> list = ${domain}Service.queryList(req);
          return new CommonResp<>(list);
      }
  
      @DeleteMapping("/delete/{id}")
      public CommonResp<Object> delete(@PathVariable Long id) {
          ${domain}Service.delete(id);
          return new CommonResp<>();
      }
  }
  ```

  generator/src/main/java/com/neilxu/train/generator/ftl/adminController.ftl

  ```
  package com.neilxu.train.${module}.controller.admin;;
  
  import com.neilxu.train.common.context.LoginMemberContext;
  import com.neilxu.train.common.resp.CommonResp;
  import com.neilxu.train.common.resp.PageResp;
  import com.neilxu.train.${module}.req.${Domain}QueryReq;
  import com.neilxu.train.${module}.req.${Domain}SaveReq;
  import com.neilxu.train.${module}.resp.${Domain}QueryResp;
  import com.neilxu.train.${module}.service.${Domain}Service;
  import jakarta.annotation.Resource;
  import jakarta.validation.Valid;
  import org.springframework.web.bind.annotation.*;
  
  @RestController
  @RequestMapping("/admin/${do_main}")
  public class ${Domain}AdminController {
  
      @Resource
      private ${Domain}Service ${domain}Service;
  
      @PostMapping("/save")
      public CommonResp<Object> save(@Valid @RequestBody ${Domain}SaveReq req) {
          ${domain}Service.save(req);
          return new CommonResp<>();
      }
  
      @GetMapping("/query-list")
      public CommonResp<PageResp<${Domain}QueryResp>> queryList(@Valid ${Domain}QueryReq req) {
          PageResp<${Domain}QueryResp> list = ${domain}Service.queryList(req);
          return new CommonResp<>(list);
      }
  
      @DeleteMapping("/delete/{id}")
      public CommonResp<Object> delete(@PathVariable Long id) {
          ${domain}Service.delete(id);
          return new CommonResp<>();
      }
  }
  ```

- **修改vue.ftl**

  ```
  <template>
    <p>
      <a-space>
        <a-button type="primary" @click="handleQuery()">刷新</a-button>
        <#if !readOnly><a-button type="primary" @click="onAdd">新增</a-button></#if>
      </a-space>
    </p>
    <a-table :dataSource="${domain}s"
             :columns="columns"
             :pagination="pagination"
             @change="handleTableChange"
             :loading="loading">
      <template #bodyCell="{ column, record }">
        <template v-if="column.dataIndex === 'operation'">
          <#if !readOnly>
          <a-space>
            <a-popconfirm
                title="删除后不可恢复,确认删除?"
                @confirm="onDelete(record)"
                ok-text="确认" cancel-text="取消">
              <a style="color: red">删除</a>
            </a-popconfirm>
            <a @click="onEdit(record)">编辑</a>
          </a-space>
          </#if>
        </template>
        <#list fieldList as field>
          <#if field.enums>
        <template v-else-if="column.dataIndex === '${field.nameHump}'">
          <span v-for="item in ${field.enumsConst}_ARRAY" :key="item.code">
            <span v-if="item.code === record.${field.nameHump}">
              {{item.desc}}
            </span>
          </span>
        </template>
          </#if>
        </#list>
      </template>
    </a-table>
    <#if !readOnly>
    <a-modal v-model:visible="visible" title="${tableNameCn}" @ok="handleOk"
             ok-text="确认" cancel-text="取消">
      <a-form :model="${domain}" :label-col="{span: 4}" :wrapper-col="{ span: 20 }">
        <#list fieldList as field>
          <#if field.name!="id" && field.nameHump!="createTime" && field.nameHump!="updateTime">
        <a-form-item label="${field.nameCn}">
          <#if field.enums>
          <a-select v-model:value="${domain}.${field.nameHump}">
            <a-select-option v-for="item in ${field.enumsConst}_ARRAY" :key="item.code" :value="item.code">
              {{item.desc}}
            </a-select-option>
          </a-select>
          <#elseif field.javaType=='Date'>
            <#if field.type=='time'>
          <a-time-picker v-model:value="${domain}.${field.nameHump}" valueFormat="HH:mm:ss" placeholder="请选择时间" />
            <#elseif field.type=='date'>
          <a-date-picker v-model:value="${domain}.${field.nameHump}" valueFormat="YYYY-MM-DD" placeholder="请选择日期" />
            <#else>
          <a-date-picker v-model:value="${domain}.${field.nameHump}" valueFormat="YYYY-MM-DD HH:mm:ss" show-time placeholder="请选择日期" />
            </#if>
          <#else>
          <a-input v-model:value="${domain}.${field.nameHump}" />
          </#if>
        </a-form-item>
          </#if>
        </#list>
      </a-form>
    </a-modal>
    </#if>
  </template>
  
  <script>
  import { defineComponent, ref, onMounted } from 'vue';
  import {notification} from "ant-design-vue";
  import axios from "axios";
  
  export default defineComponent({
    name: "${do_main}-view",
    setup() {
      <#list fieldList as field>
      <#if field.enums>
      const ${field.enumsConst}_ARRAY = window.${field.enumsConst}_ARRAY;
      </#if>
      </#list>
      const visible = ref(false);
      let ${domain} = ref({
        <#list fieldList as field>
        ${field.nameHump}: undefined,
        </#list>
      });
      const ${domain}s = ref([]);
      // 分页的三个属性名是固定的
      const pagination = ref({
        total: 0,
        current: 1,
        pageSize: 10,
      });
      let loading = ref(false);
      const columns = [
      <#list fieldList as field>
        <#if field.name!="id" && field.nameHump!="createTime" && field.nameHump!="updateTime">
      {
        title: '${field.nameCn}',
        dataIndex: '${field.nameHump}',
        key: '${field.nameHump}',
      },
        </#if>
      </#list>
      <#if !readOnly>
      {
        title: '操作',
        dataIndex: 'operation'
      }
      </#if>
      ];
  
      <#if !readOnly>
      const onAdd = () => {
        ${domain}.value = {};
        visible.value = true;
      };
  
      const onEdit = (record) => {
        ${domain}.value = window.Tool.copy(record);
        visible.value = true;
      };
  
      const onDelete = (record) => {
        axios.delete("/${module}/admin/${do_main}/delete/" + record.id).then((response) => {
          const data = response.data;
          if (data.success) {
            notification.success({description: "删除成功!"});
            handleQuery({
              page: pagination.value.current,
              size: pagination.value.pageSize,
            });
          } else {
            notification.error({description: data.message});
          }
        });
      };
  
      const handleOk = () => {
        axios.post("/${module}/admin/${do_main}/save", ${domain}.value).then((response) => {
          let data = response.data;
          if (data.success) {
            notification.success({description: "保存成功!"});
            visible.value = false;
            handleQuery({
              page: pagination.value.current,
              size: pagination.value.pageSize
            });
          } else {
            notification.error({description: data.message});
          }
        });
      };
      </#if>
  
      const handleQuery = (param) => {
        if (!param) {
          param = {
            page: 1,
            size: pagination.value.pageSize
          };
        }
        loading.value = true;
        axios.get("/${module}/admin/${do_main}/query-list", {
          params: {
            page: param.page,
            size: param.size
          }
        }).then((response) => {
          loading.value = false;
          let data = response.data;
          if (data.success) {
            ${domain}s.value = data.content.list;
            // 设置分页控件的值
            pagination.value.current = param.page;
            pagination.value.total = data.content.total;
          } else {
            notification.error({description: data.message});
          }
        });
      };
  
      const handleTableChange = (pagination) => {
        // console.log("看看自带的分页参数都有啥:" + pagination);
        handleQuery({
          page: pagination.current,
          size: pagination.pageSize
        });
      };
  
      onMounted(() => {
        handleQuery({
          page: 1,
          size: pagination.value.pageSize
        });
      });
  
      return {
        <#list fieldList as field>
        <#if field.enums>
        ${field.enumsConst}_ARRAY,
        </#if>
        </#list>
        ${domain},
        visible,
        ${domain}s,
        pagination,
        columns,
        handleTableChange,
        handleQuery,
        loading,
        <#if !readOnly>
        onAdd,
        handleOk,
        onEdit,
        onDelete
        </#if>
      };
    },
  });
  </script>
  ```

- **修改ServerGenerator.java**

  ```java
          gen(Domain, param, "service", "service");
          gen(Domain, param, "controller/admin", "adminController");
  //        gen(Domain, param, "controller", "controller");
          gen(Domain, param, "req", "saveReq");
          gen(Domain, param, "req", "queryReq");
          gen(Domain, param, "resp", "queryResp");
          genVue(do_main, param);
  ```

- **测试**

  重新生成前后端代码

  生成成功

在这里插入图片描述

在这里插入图片描述

  增删改查都正常

5.1.注意:导入了common依赖,common加了lombok,这里却导不进来lombok的问题

在这里插入图片描述

将父pom的lombok 去掉,不然刷新maven刷新不进来,具体原因还不太清楚

在这里插入图片描述

6.快速生成火车基础数据管理功能

这里可以先把business库里的member表删除,已经没用了

  • 增加火车表,生成持久层代码及前后端代码

    • business.sql

      drop table if exists `train`;
      create table `train` (
          `id` bigint not null comment 'id',
          `code` varchar(20) not null comment '车次编号',
          `type` char(1) not null comment '车次类型|枚举[TrainTypeEnum]',
          `start` varchar(20) not null comment '始发站',
          `start_pinyin` varchar(50) not null comment '始发站拼音',
          `start_time` time not null comment '出发时间',
          `end` varchar(20) not null comment '终点站',
          `end_pinyin` varchar(50) not null comment '终点站拼音',
          `end_time` time not null comment '到站时间',
          `create_time` datetime(3) comment '新增时间',
          `update_time` datetime(3) comment '修改时间',
          primary key (`id`),
          unique key `code_unique` (`code`)
      ) engine=innodb default charset=utf8mb4 comment='车次';
      
    • 新增TrainTypeEnum.java

      com.neilxu.train.business.enums.TrainTypeEnum

      package com.neilxu.train.business.enums;
      
      import java.math.BigDecimal;
      import java.util.ArrayList;
      import java.util.EnumSet;
      import java.util.HashMap;
      import java.util.List;
      
      public enum TrainTypeEnum {
      
          G("G", "高铁", new BigDecimal("1.2")),
          D("D", "动车", new BigDecimal("1")),
          K("K", "快速", new BigDecimal("0.8"));
      
          private String code;
      
          private String desc;
      
          /**
           * 票价比例,例:1.1,则票价 = 1.1 * 每公里单价(SeatTypeEnum.price) * 公里(station.km)
           */
          private BigDecimal priceRate;
      
          TrainTypeEnum(String code, String desc, BigDecimal priceRate) {
              this.code = code;
              this.desc = desc;
              this.priceRate = priceRate;
          }
      
          @Override
          public String toString() {
              return "TrainTypeEnum{" +
                      "code='" + code + '\'' +
                      ", desc='" + desc + '\'' +
                      ", priceRate=" + priceRate +
                      "} " + super.toString();
          }
      
          public String getCode() {
              return code;
          }
      
          public void setCode(String code) {
              this.code = code;
          }
      
          public void setDesc(String desc) {
              this.desc = desc;
          }
      
          public String getDesc() {
              return desc;
          }
      
          public BigDecimal getPriceRate() {
              return priceRate;
          }
      
          public void setPriceRate(BigDecimal priceRate) {
              this.priceRate = priceRate;
          }
      
          public static List<HashMap<String,String>> getEnumList() {
              List<HashMap<String, String>> list = new ArrayList<>();
              for (TrainTypeEnum anEnum : EnumSet.allOf(TrainTypeEnum.class)) {
                  HashMap<String, String> map = new HashMap<>();
                  map.put("code",anEnum.code);
                  map.put("desc",anEnum.desc);
                  list.add(map);
              }
              return list;
          }
      }
      
    • 修改EnumGenerator.java

      generator 的pom先引入business

      <dependency>
          <groupId>com.neilxu</groupId>
          <artifactId>business</artifactId>
          <version>0.0.1-SNAPSHOT</version>
          <scope>compile</scope>
      </dependency>
      

      com.neilxu.train.generator.gen.EnumGenerator

      package com.neilxu.train.generator.gen;
      
      import cn.hutool.core.util.StrUtil;
      import com.neilxu.train.business.enums.TrainTypeEnum;
      import com.neilxu.train.member.enums.PassengerTypeEnum;
      
      import java.io.FileOutputStream;
      import java.io.OutputStreamWriter;
      import java.lang.reflect.Method;
      
      public class EnumGenerator {
      //    static String path = "web/src/assets/js/enums.js";
          static String path = "admin/src/assets/js/enums.js";
      
          public static void main(String[] args) {
              StringBuffer bufferObject = new StringBuffer();
              StringBuffer bufferArray = new StringBuffer();
              long begin = System.currentTimeMillis();
              try {
                  toJson(PassengerTypeEnum.class, bufferObject, bufferArray);
                  toJson(TrainTypeEnum.class, bufferObject, bufferArray);
      
                  StringBuffer buffer = bufferObject.append("\r\n").append(bufferArray);
                  writeJs(buffer);
              } catch (Exception e) {
                  e.printStackTrace();
              }
              long end = System.currentTimeMillis();
              System.out.println("执行耗时:" + (end - begin) + " 毫秒");
          }
      
          private static void toJson(Class clazz, StringBuffer bufferObject, StringBuffer bufferArray) throws Exception {
              // enumConst:将YesNoEnum变成YES_NO
              String enumConst = StrUtil.toUnderlineCase(clazz.getSimpleName())
                      .toUpperCase().replace("_ENUM", "");
              Object[] objects = clazz.getEnumConstants();
              Method name = clazz.getMethod("name");
              Method getDesc = clazz.getMethod("getDesc");
              Method getCode = clazz.getMethod("getCode");
      
              // 生成对象
              bufferObject.append(enumConst).append("={");
              for (int i = 0; i < objects.length; i++) {
                  Object obj = objects[i];
                  bufferObject.append(name.invoke(obj)).append(":{code:\"").append(getCode.invoke(obj)).append("\", desc:\"").append(getDesc.invoke(obj)).append("\"}");
                  if (i < objects.length - 1) {
                      bufferObject.append(",");
                  }
              }
              bufferObject.append("};\r\n");
      
              // 生成数组
              bufferArray.append(enumConst).append("_ARRAY=[");
              for (int i = 0; i < objects.length; i++) {
                  Object obj = objects[i];
                  bufferArray.append("{code:\"").append(getCode.invoke(obj)).append("\", desc:\"").append(getDesc.invoke(obj)).append("\"}");
                  if (i < objects.length - 1) {
                      bufferArray.append(",");
                  }
              }
              bufferArray.append("];\r\n");
          }
      
          /**
           * 写文件
           * @param stringBuffer
           */
          public static void writeJs(StringBuffer stringBuffer) {
              FileOutputStream out = null;
              try {
                  out = new FileOutputStream(path);
                  OutputStreamWriter osw = new OutputStreamWriter(out, "UTF-8");
                  System.out.println(path);
                  osw.write(stringBuffer.toString());
                  osw.close();
              } catch (Exception e) {
                  e.printStackTrace();
              }
              finally {
                  try {
                      out.close();
                  } catch (Exception e) {
                      e.printStackTrace();
                  }
      
              }
          }
      
      }
      

      生成admin/src/assets/js/enums.js

      PASSENGER_TYPE={ADULT:{code:"1", desc:"成人"},CHILD:{code:"2", desc:"儿童"},STUDENT:{code:"3", desc:"学生"}};
      TRAIN_TYPE={G:{code:"G", desc:"高铁"},D:{code:"D", desc:"动车"},K:{code:"K", desc:"快速"}};
      
      PASSENGER_TYPE_ARRAY=[{code:"1", desc:"成人"},{code:"2", desc:"儿童"},{code:"3", desc:"学生"}];
      TRAIN_TYPE_ARRAY=[{code:"G", desc:"高铁"},{code:"D", desc:"动车"},{code:"K", desc:"快速"}];
      
  • 修改generator-config-business.xml

    generator/src/main/resources/generator-config-business.xml

    <!--        <table tableName="member" domainObjectName="Member"/>-->
    <!--        <table tableName="station" domainObjectName="Station"/>-->
            <table tableName="train" domainObjectName="Train"/>
    
  • 生成持久层代码和前后端代码

在这里插入图片描述

这里执行又报错,解决办法:先install train,再install business,再执行

在这里插入图片描述

这里注意到生成的代码一直需要import lombok,我们修改下模板,将这句话也写进去

在这里插入图片描述

生成成功

在这里插入图片描述

  • 手动修改前端代码

    同上,加路由、侧边栏

    const routes = [{
      path: '/',
      component: () => import('../views/main.vue'),
      children: [{
        path: 'welcome',
        component: () => import('../views/main/welcome.vue'),
      }, {
        path: 'about',
        component: () => import('../views/main/about.vue'),
      }, {
        path: 'station',
        component: () => import('../views/main/station.vue'),
      }, {
        path: 'train',
        component: () => import('../views/main/train.vue'),
      }]
    }, {
      path: '',
      redirect: '/welcome'
    }];
    
    <a-menu-item key="/train">
      <router-link to="/train">
        <user-outlined /> &nbsp; 火车管理
      </router-link>
    </a-menu-item>
    
  • 优化:针对date和time类型修改生成器

    • 修改DbUtil.java

      public static String sqlTypeToJavaType(String sqlType) {
          if (sqlType.toUpperCase().contains("varchar".toUpperCase())
                  || sqlType.toUpperCase().contains("char".toUpperCase())
                  || sqlType.toUpperCase().contains("text".toUpperCase())) {
              return "String";
          } else if (sqlType.toUpperCase().contains("datetime".toUpperCase())) {
              return "Date";
          } else if (sqlType.toUpperCase().contains("time".toUpperCase())) {
              return "Date";
          } else if (sqlType.toUpperCase().contains("date".toUpperCase())) {
              return "Date";
          } else if (sqlType.toUpperCase().contains("bigint".toUpperCase())) {
              return "Long";
          } else if (sqlType.toUpperCase().contains("int".toUpperCase())) {
              return "Integer";
          } else if (sqlType.toUpperCase().contains("long".toUpperCase())) {
              return "Long";
          } else if (sqlType.toUpperCase().contains("decimal".toUpperCase())) {
              return "BigDecimal";
          } else if (sqlType.toUpperCase().contains("boolean".toUpperCase())) {
              return "Boolean";
          } else {
              return "String";
          }
      }
      
    • 修改saveReq.ftl

      <#if field.javaType=='Date'>
          <#if field.type=='time'>
      @JsonFormat(pattern = "HH:mm:ss",timezone = "GMT+8")
          <#elseif field.type=='date'>
      @JsonFormat(pattern = "yyyy-MM-dd",timezone = "GMT+8")
          <#else>
      @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
          </#if>
      </#if>
      
    • 重新生成前后端代码

在这里插入图片描述

  • 测试

在这里插入图片描述

7.快速生成火车车站基础数据管理功能

火车——车站关联的功能

  • 新增火车车站表

    drop table if exists `train_station`;
    create table `train_station` (
      `id` bigint not null comment 'id',
      `train_code` varchar(20) not null comment '车次编号',
      `index` int not null comment '站序',
      `name` varchar(20) not null comment '站名',
      `name_pinyin` varchar(50) not null comment '站名拼音',
      `in_time` time comment '进站时间',
      `out_time` time comment '出站时间',
      `stop_time` time comment '停站时长',
      `km` decimal(8, 2) not null comment '里程(公里)|从上一站到本站的距离',
      `create_time` datetime(3) comment '新增时间',
      `update_time` datetime(3) comment '修改时间',
      primary key (`id`),
      unique key `train_code_index_unique` (`train_code`, `index`),
      unique key `train_code_name_unique` (`train_code`, `name`)
    ) engine=innodb default charset=utf8mb4 comment='火车车站';
    
  • 修改generator-config-business.xml

    <!--<table tableName="station" domainObjectName="Station"/>-->
    <!--<table tableName="train" domainObjectName="Train"/>-->
    <table tableName="train_station" domainObjectName="TrainStation"/>
    
  • 生成持久层代码和前后端代码

在这里插入图片描述

  • 修改路由、菜单

    同上

8.快速生成火车车厢基础数据管理功能

  • 新增表

    drop table if exists `train_carriage`;
    create table `train_carriage` (
      `id` bigint not null comment 'id',
      `train_code` varchar(20) not null comment '车次编号',
      `index` int not null comment '厢号',
      `seat_type` char(1) not null comment '座位类型|枚举[SeatTypeEnum]',
      `seat_count` int not null comment '座位数',
      `row_count` int not null comment '排数',
      `col_count` int not null comment '列数',
      `create_time` datetime(3) comment '新增时间',
      `update_time` datetime(3) comment '修改时间',
      unique key `train_code_index_unique` (`train_code`, `index`),
      primary key (`id`)
    ) engine=innodb default charset=utf8mb4 comment='火车车厢';
    

    注意:

    column是mysql的关键字,使用他来做列号的话,正常使用mybatis是没问题,mybatis会自动为关键字加反引号``,但是在使用分布式事务seata时会有问题,所以还是避免使用关键字

  • 新增枚举类

    com.neilxu.train.business.enums.SeatTypeEnum

    package com.neilxu.train.business.enums;
    
    import java.math.BigDecimal;
    import java.util.ArrayList;
    import java.util.EnumSet;
    import java.util.HashMap;
    import java.util.List;
    
    public enum SeatTypeEnum {
    
        YDZ("1", "一等座", new BigDecimal("0.4")),
        EDZ("2", "二等座", new BigDecimal("0.3")),
        RW("3", "软卧", new BigDecimal("0.6")),
        YW("4", "硬卧", new BigDecimal("0.5"));
    
        private String code;
    
        private String desc;
    
        /**
         * 基础票价 N元/公里,0.4即为0.4元/公里
         */
        private BigDecimal price;
    
        SeatTypeEnum(String code, String desc, BigDecimal price) {
            this.code = code;
            this.desc = desc;
            this.price = price;
        }
    
        public String getCode() {
            return code;
        }
    
        public String getDesc() {
            return desc;
        }
    
        public BigDecimal getPrice() {
            return price;
        }
    
        public void setPrice(BigDecimal price) {
            this.price = price;
        }
    
        public static List<HashMap<String,String>> getEnumList() {
            List<HashMap<String, String>> list = new ArrayList<>();
            for (SeatTypeEnum anEnum : EnumSet.allOf(SeatTypeEnum.class)) {
                HashMap<String, String> map = new HashMap<>();
                map.put("code",anEnum.code);
                map.put("desc",anEnum.desc);
                list.add(map);
            }
            return list;
        }
    
        public static SeatTypeEnum getEnumByCode(String code) {
            for (SeatTypeEnum enums : SeatTypeEnum.values()) {
                if (enums.getCode().equalsIgnoreCase(code)) {
                    return enums;
                }
            }
            return null;
        }
    }
    
  • 生成持久层、前后端、枚举js

    修改好generator-config-business.xml和EnumGenerator.java

    生成步骤同上

  • 修改路由、侧边栏

    同上

9.快速生成火车座位基础数据管理功能

  • 新增表

    drop table if exists `train_seat`;
    create table `train_seat` (
      `id` bigint not null comment 'id',
      `train_code` varchar(20) not null comment '车次编号',
      `carriage_index` int not null comment '厢序',
      `row` char(2) not null comment '排号|01, 02',
      `col` char(1) not null comment '列号|枚举[SeatColEnum]',
      `seat_type` char(1) not null comment '座位类型|枚举[SeatTypeEnum]',
      `carriage_seat_index` int not null comment '同车厢座序',
      `create_time` datetime(3) comment '新增时间',
      `update_time` datetime(3) comment '修改时间',
      primary key (`id`)
    ) engine=innodb default charset=utf8mb4 comment='座位';
    

    同车厢座序:每个车厢座位都按1,2,3,4…排序

  • 新增枚举类

    com.neilxu.train.business.enums.SeatColEnum

    package com.neilxu.train.business.enums;
    
    import java.util.ArrayList;
    import java.util.EnumSet;
    import java.util.List;
    
    public enum SeatColEnum {
    
        YDZ_A("A", "A", "1"),
        YDZ_C("C", "C", "1"),
        YDZ_D("D", "D", "1"),
        YDZ_F("F", "F", "1"),
        EDZ_A("A", "A", "2"),
        EDZ_B("B", "B", "2"),
        EDZ_C("C", "C", "2"),
        EDZ_D("D", "D", "2"),
        EDZ_F("F", "F", "2");
    
        private String code;
    
        private String desc;
    
        /**
         * 对应SeatTypeEnum.code
         */
        private String type;
    
        SeatColEnum(String code, String desc, String type) {
            this.code = code;
            this.desc = desc;
            this.type = type;
        }
    
        public String getCode() {
            return code;
        }
    
        public void setCode(String code) {
            this.code = code;
        }
    
        public void setDesc(String desc) {
            this.desc = desc;
        }
    
        public String getDesc() {
            return desc;
        }
    
        public String getType() {
            return type;
        }
    
        public void setType(String type) {
            this.type = type;
        }
    
        /**
         * 根据车箱的座位类型,筛选出所有的列,比如车箱类型是一等座,则筛选出columnList={ACDF}
         */
        public static List<SeatColEnum> getColsByType(String seatType) {
            List<SeatColEnum> colList = new ArrayList<>();
            EnumSet<SeatColEnum> seatColEnums = EnumSet.allOf(SeatColEnum.class);
            for (SeatColEnum anEnum : seatColEnums) {
                if (seatType.equals(anEnum.getType())) {
                    colList.add(anEnum);
                }
            }
            return colList;
        }
    
    }
    
  • 生成持久层、前后端、枚举js

    同上

  • 修改路由、侧边栏

    同上

由于篇幅太长,还差一截补在下篇

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值