本文参考自
本文是仿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}} <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 /> 欢迎 </router-link> </a-menu-item> <a-menu-item key="/passenger"> <router-link to="/passenger"> <user-outlined /> 乘车人管理 </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 /> 欢迎 </router-link> </a-menu-item> <a-menu-item key="/passenger"> <router-link to="/passenger"> <user-outlined /> 乘车人管理 </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}} <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 /> 欢迎 </router-link> </a-menu-item> <a-menu-item key="/passenger"> <router-link to="/passenger"> <user-outlined /> 乘车人管理 </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 /> 欢迎 </router-link> </a-menu-item> <a-menu-item key="/passenger"> <router-link to="/passenger"> <user-outlined /> 乘车人管理 </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语句进行拦截和修改,从而实现分页功能。其原理主要包括以下几个步骤:
- 拦截器机制:PageHelper通过实现MyBatis的拦截器接口,在SQL语句执行前对其进行拦截和修改。这样可以在不修改业务代码的情况下,实现对SQL语句的分页处理。
- 解析分页参数:PageHelper拦截器在执行前会解析传入的分页参数,如页码和每页数据条数等,确定需要分页的SQL语句以及分页参数的值。
- 修改SQL语句:根据解析得到的分页参数,PageHelper会修改原始的SQL语句,添加对应的分页逻辑,通常是在原始SQL语句的末尾添加
LIMIT
(MySQL)或者ROWNUM
(Oracle)等关键字来限制结果集的返回条数。- 执行分页查询:修改后的SQL语句会被执行,从数据库中查询数据。由于修改后的SQL语句包含了分页逻辑,因此只会返回符合分页条件的数据。
- 返回分页结果:查询结果会被封装成分页对象,并返回给调用方。这个分页对象通常包含了总记录数、当前页码、总页数以及查询结果等信息,方便业务代码进行分页展示和处理。
总的来说,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 /> 欢迎 </router-link> </a-menu-item> <a-menu-item key="/about"> <router-link to="/about"> <user-outlined /> 关于 </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 /> 欢迎 </router-link> </a-menu-item> <a-menu-item key="/about"> <router-link to="/about"> <user-outlined /> 关于 </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 /> 欢迎 </router-link> </a-menu-item> <a-menu-item key="/about"> <router-link to="/about"> <user-outlined /> 关于 </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 /> 车站管理 </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 /> 火车管理 </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
同上
-
修改路由、侧边栏
同上
由于篇幅太长,还差一截补在下篇