前言:归因于学校的工程实践,要求小组完成一个项目,包括项目的开题报告、需求分析、数据库设计、概要设计、原型设计、详细设计、项目编码、功能测试、项目集成。作为组长,我想先为项目搭好整体的框架,包括后端的模块划分、依赖版本统一、技术栈,前端界面布局设计以及整体的一个风格等等。并且团队内开发采用gitee进行版本控制,这样做的好处是方便最后的整合,节省开发时间,并且为以后大家在公司内开发奠定基础,以后可能就是一个人负责一个或几个模块。当然让我自己写一个框架肯定是不行的,我一开始的想法就是使用网上大佬写的开源框架,然后对里面的代码进行修改和删减,“变成”我们自己的。我认为不论是在接外包还是完成学校的一些项目任务,都是可以使用一些开源框架的,比如易优、人人开源等,在修改和改错的过程中也是能学到技巧和知识的。多的不说,我就拿本次实践项目为例子从头开始搭建。
项目设计到vue和Spring Cloud技术,请提前安装Node和nacos
这次我就选择人人开源作为模板,我会使用到它的renren-fast-vue和renren-fast,其他的大家有兴趣可以点击去看看,特别是renren-generator——代码生成器,自动生成前和后端的代码,除了一般的controller层、service层等一些后端代码,还有对应的vue代码。
将renren-fas下载或者git拉取都可,打开项目,导入相关依赖。
根据db文件夹下的文件创建数据库,是MySQL就是拿MySQL,其他拿其他。
修改项目配置文件
启动没问题说明后端代码和环境没问题
初次打开项目会提示npm install
下载相干依赖,先别点击,先修改package.json
文件中的个别依赖版本号,避免某些依赖因为版本号冲突导不进去。
将原来的dependencies部分用下面的代码替换
"dependencies": {
"axios": "0.17.1",
"babel-plugin-component": "0.10.1",
"babel-polyfill": "6.26.0",
"element-ui": "^2.8.2",
"gulp": "4.0.2",
"gulp-concat": "2.6.1",
"gulp-load-plugins": "2.0.5",
"gulp-replace": "1.0.0",
"gulp-shell": "0.8.0",
"lodash": "4.17.5",
"node-sass": "^6.0.0",
"npm": "^6.9.0",
"pubsub-js": "^1.8.0",
"sass-loader": "6.0.6",
"svg-sprite-loader": "3.7.3",
"vue": "2.5.16",
"vue-cookie": "1.1.4",
"vue-router": "3.0.1",
"vuex": "3.0.1"
},
替换完后,再点击npm install
。
只要控制台中没有出现error
,说明依赖全部下载完成,控制台中输入npm run dev
运行
由于我搭建的团队框架中只使用了项目中的菜单管理模块,并且我删除按钮的操作,目录的添加相当于一整个大模块,而菜单则是模块下的各个服务,添加成功后我们只需要在它的项目文件夹view下的modules创建目录(可参照它的项目结构),这符合项目的模块化开发,我觉得这一块对于我们开发vue的开发节省了较多的时间,比较方便,其他功能后面需要可以参照它的后端代码实现,大家根据需求适当删减。
这是我们组决定的项目以及模块划分,大家可根据自身情况创建。
服务模块划分
zuke-data(数据模块)
zuke-hotel(酒店模块)
zuke-order(订单模块)
zuke-product(商品模块)
zuke-department(部门模块)
zuke-admin(管理员模块)
zuke-consumer(消费模块)
zuke-monitor(监控模块)
zuke-common(存放工具类,公共返回对象,全局异常等相关类和工具包。简言之每一个服务都需要的东西都
放在common里面)
zuke-gateway(网关)
首先创建一个maven
空项目作为父项目,作为聚合
接着在父项目下创建各个模块(服务),举个例子:
tips:版本号后期可以通过pom进行修改,因为我们项目是分模块开发的,也就是各个服务分开,后端的端口可能很多,所以这里使用到了Spring Cloud相关依赖,如果是单模块(后端只有一个端口),完全不用导入Spring Cloud相关依赖。
也可以像人人开源一样,将登录和菜单管理放在一个模块。大家搭建的时候创建这4个模块即可。
除了gateway模块,其他3个模块里面的内容基本和renren-fast中的差不多,不同的是删除了登录时需要的验证码、用户权限,下面我将改好的部分代码贴出,大家根据结构复制renren-fast中的代码即可。涉及到的数据库表只有sys_admin和sys_menu,可以单独创建一个数据库将这两张表的数据和结构复制到新数据库中
存放工具类,公共返回对象,全局异常等相关类和工具包
pom
<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>zukeartifactId>
<groupId>com.kt.zukegroupId>
<version>0.0.1-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<description>存放工具类,公共返回对象,全局异常等相关类和工具包description>
<artifactId>zuke-commonartifactId>
<dependencies>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.3version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.20version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.24version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.75version>
dependency>
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpclientartifactId>
<version>4.2.1version>
dependency>
<dependency>
<groupId>org.hibernate.validatorgroupId>
<artifactId>hibernate-validatorartifactId>
<version>6.2.0.Finalversion>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.1.2.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
project>
以下皆与renren-fast中对应类或接口相同,大家复制即可。
GlobalException.class
Constant.class
PageUtil.class
Result.class
AddGroup.interface
UpdateGroup.interface
需要在启动类ZukeGatewayApplication.class中加上@EnableDiscoveryClient注解
结构:
pom
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.12.RELEASEversion>
<relativePath/>
parent>
<groupId>com.cuit.zukegroupId>
<artifactId>zuke-gatewayartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>zuke-gatewayname>
<description>API网关description>
<properties>
<java.version>1.8java.version>
<spring-cloud.version>Hoxton.SR12spring-cloud.version>
properties>
<dependencies>
<dependency>
<groupId>com.kt.emallgroupId>
<artifactId>emall-commonartifactId>
<version>0.0.1-SNAPSHOTversion>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
exclusion>
<exclusion>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
CrosConfig
/**
* @program: zuke
* @description: 解决跨域
* @author: KamTang
* @create: 2022-04-02 09:03
**/
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsWebFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 配置跨域
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.setAllowCredentials(true);
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsWebFilter(source);
}
}
application
server:
port: 9000
spring:
application:
name: zuke-gateway
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
gateway:
routes:
- id: zuke-system
uri: lb://zuke-system
predicates:
- Path=/zukeApi/sys/**
filters:
- RewritePath=/zukeApi/(?>.*),/$\{segment}
- id: zuke-admin
# lb 负载均衡到zuke-admin服务
uri: lb://zuke-admin
# 断言规则
predicates:
- Path=/zukeApi/admin/**
filters:
- RewritePath=/zukeApi/(?>.*),/$\{segment}
需要在启动类ZukeAdminApplication.class中加上@EnableDiscoveryClient注解开启服务注册功能,将服务注册到注册中心
结构:
AdminController
/**
* @program: zuke
* @description: 管理员
* @author: KamTang
* @create: 2022-04-01 18:46
**/
@RestController
@RequestMapping("/admin")
public class AdminController extends AbstractController{
@Autowired
AdminService adminService;
@GetMapping("/list")
public Result list (@RequestParam Map<String, Object> params) {
return Result.ok().put("page", adminService.queryPage(params));
}
/**
* 获取登录的用户信息
*/
@GetMapping("/info")
public Result info(){
return Result.ok().put("user", getAdmin());
}
}
LoginController
/**
* @program: zuke
* @description: 登录
* @author: KamTang
* @create: 2022-04-01 19:29
**/
@RestController
@RequestMapping("/admin")
public class LoginController {
@Autowired
private AdminService adminService;
@Autowired
private TokenService tokenService;
/**
* 登录
*/
@PostMapping("/login")
public Map<String, Object> login(@RequestBody LoginVO loginVO) {
//用户信息
AdminEntity admin = adminService.queryByUserName(loginVO.getUsername());
//账号不存在、密码错误
if(admin == null || !admin.getPassword().equals(new Sha256Hash(loginVO.getPassword(), admin.getSalt()).toHex())) {
return Result.error("账号或密码不正确");
}
//账号锁定
if(admin.getStatus() == 0){
return Result.error("账号已被锁定,请联系管理员");
}
//生成token,并保存到数据库
return tokenService.createToken(admin.getUserId());
}
}
LoginVO
@Data
public class LoginVO {
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* uuid
*/
private String uuid;
}
application
server:
port: 9001
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://localhost:3306/zuke_admin?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
hikari:
# 连接池名
pool-name: DateHikariCP
# 最小空闲连接数
minimum-idle: 5
# 空闲连接存活最大时间,默认600000(10min)
idle-timeout: 180000
# 最大连接数,默认10
maximum-pool-size: 10
# 从连接池返回的连接的自动提交
auto-commit: true
# 连接最大存活时间,0代表永久存活,默认1800000(30min)
max-lifetime: 1800000
# 连接超时时间,默认30000(30s)
connection-timeout: 30000
# 测试连接是否可用的查询语句
connection-test-query: SELECT 1
# type: com.zaxxer.hikari.HikariDataSource
application:
name: zuke-admin
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
#mybatis
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
#实体扫描,多个package用逗号或者分号分隔
# typeAliasesPackage: io.renren.modules.*.entity
global-config:
#数据库相关配置
db-config:
#主键类型 AUTO:"数据库ID自增", INPUT:"用户输入ID", ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
id-type: AUTO
logic-delete-value: -1
logic-not-delete-value: 0
banner: false
需要在启动类ZukeSystemApplication.class中加上@EnableDiscoveryClient注解开启服务注册功能,将服务注册到注册中心
结构:
pom
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.3.12.RELEASEversion>
<relativePath/>
parent>
<groupId>com.cuit.systemgroupId>
<artifactId>zuke-systemartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>zuke-systemname>
<description>系统设置description>
<properties>
<java.version>1.8java.version>
<spring-cloud.version>Hoxton.SR12spring-cloud.version>
properties>
<dependencies>
<dependency>
<groupId>com.kt.zukegroupId>
<artifactId>zuke-commonartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
SysMenuController
/**
* @program: zuke
* @description: 系统菜单
* @author: KamTang
* @create: 2022-03-31 10:07
**/
@RestController
@RequestMapping("/sys/menu")
public class SysMenuController {
@Autowired
private SysMenuService sysMenuService;
/**
* 导航菜单,菜单的parent_id为0
*/
@GetMapping("/nav")
public Result nav() {
List<SysMenuEntity> menuList = sysMenuService.getNavMenu();
return Result.ok().put("menuList", menuList);
}
/**
* 所有菜单列表(封装成父子节点)
*/
@GetMapping("/list")
public List<SysMenuEntity> list() {
List<SysMenuEntity> menuList = sysMenuService.list();
HashMap<Long, SysMenuEntity> menuMap = new HashMap<>(12);
for (SysMenuEntity s : menuList) {
menuMap.put(s.getMenuId(), s);
}
for (SysMenuEntity s : menuList) {
SysMenuEntity parent = menuMap.get(s.getParentId());
if (Objects.nonNull(parent)) {
s.setParentName(parent.getName());
}
}
return menuList;
}
/**
* 选择菜单(添加、修改菜单)
*/
@GetMapping("/select")
public Result select(){
//查询列表数据
List<SysMenuEntity> menuList = sysMenuService.queryNotButtonList();
//添加顶级菜单
SysMenuEntity root = new SysMenuEntity();
root.setMenuId(0L);
root.setName("一级菜单");
root.setParentId(-1L);
root.setOpen(true);
menuList.add(root);
return Result.ok().put("menuList", menuList);
}
/**
* 菜单信息
*/
@GetMapping("/info/{menuId}")
public Result info(@PathVariable("menuId") Long menuId) {
SysMenuEntity menu = sysMenuService.getById(menuId);
return Result.ok().put("menu", menu);
}
/**
* 保存
*/
@PostMapping("/save")
public Result save(@RequestBody SysMenuEntity menu) {
//数据校验
verifyForm(menu);
sysMenuService.save(menu);
return Result.ok();
}
/**
* 修改
*/
@PostMapping("/update")
public Result update(@RequestBody SysMenuEntity menu) {
//数据校验
verifyForm(menu);
sysMenuService.updateById(menu);
return Result.ok();
}
/**
* 删除
*/
@PostMapping("/delete/{menuId}")
public Result delete(@PathVariable("menuId") long menuId){
if(menuId == 1){
return Result.error("系统菜单,不能删除");
}
//判断是否有子菜单或按钮
List<SysMenuEntity> menuList = sysMenuService.queryListParentId(menuId);
if(menuList.size() > 0){
return Result.error("请先删除子菜单或按钮");
}
sysMenuService.delete(menuId);
return Result.ok();
}
/**
* 验证参数是否正确
*/
private void verifyForm(SysMenuEntity menu) {
if (StringUtils.isBlank(menu.getName())) {
throw new GlobalException("菜单名称不能为空");
}
if (menu.getParentId() == null) {
throw new GlobalException("上级菜单不能为空");
}
//菜单
if (menu.getType() == Constant.MenuType.MENU.getValue()) {
if (StringUtils.isBlank(menu.getUrl())) {
throw new GlobalException("菜单URL不能为空");
}
}
//上级菜单类型
int parentType = Constant.MenuType.CATALOG.getValue();
if (menu.getParentId() != 0) {
SysMenuEntity parentMenu = sysMenuService.getById(menu.getParentId());
parentType = parentMenu.getType();
}
//目录、菜单
if (menu.getType() == Constant.MenuType.CATALOG.getValue() ||
menu.getType() == Constant.MenuType.MENU.getValue()) {
if (parentType != Constant.MenuType.CATALOG.getValue()) {
throw new GlobalException("上级菜单只能为目录类型");
}
return;
}
//按钮
if (menu.getType() == Constant.MenuType.BUTTON.getValue()) {
if (parentType != Constant.MenuType.MENU.getValue()) {
throw new GlobalException("上级菜单只能为菜单类型");
}
}
}
}
SysMenuDao
@Mapper
public interface SysMenuDao extends BaseMapper<SysMenuEntity> {
/**
* 根据父菜单,查询子菜单
* @param parentId 父菜单ID
* @return List
*/
List<SysMenuEntity> queryListParentId(Long parentId);
/**
* 获取不包含按钮的菜单列表
* @return List
*/
List<SysMenuEntity> queryNotButtonList();
}
SysMenuEntity
@Data
@TableName(value = "sys_menu")
public class SysMenuEntity implements Serializable {
private static final long serialVersionUID = 1L;
...
@TableField(exist=false)
private List<SysMenuEntity> list=new ArrayList<>();
}
SysMenuService
SysMenuServiceImpl
@Service("sysMenuService")
public class SysMenuServiceImpl extends ServiceImpl<SysMenuDao, SysMenuEntity> implements SysMenuService {
/**
* 获取导航菜单
*
* @return 树形输出所有
*/
@Override
public List<SysMenuEntity> getNavMenu() {
// 所有菜单
List<SysMenuEntity> sysMenuEntities = baseMapper.selectList(null);
// 获取所有父级菜单
List<SysMenuEntity> parentMenu = sysMenuEntities.stream().filter(sysMenu -> sysMenu.getParentId() == 0).collect(Collectors.toList());
// 封装成父子孙结构
List<SysMenuEntity> collect = parentMenu.stream().map((menu) -> {
menu.setList(getChildren(menu, sysMenuEntities));
return menu;
}).collect(Collectors.toList());
return collect;
}
/**
* 根据父菜单,查询子菜单
*
* @param parentId 父菜单ID
*/
@Override
public List<SysMenuEntity> queryListParentId(Long parentId) {
return baseMapper.queryListParentId(parentId);
}
/**
* 删除
*
* @param menuId 菜单id
*/
@Override
public void delete(Long menuId) {
baseMapper.deleteById(menuId);
}
/**
* 获取不包含按钮的菜单列表
*
* @return List<SysMenuEntity>
*/
@Override
public List<SysMenuEntity> queryNotButtonList() {
return baseMapper.queryNotButtonList();
}
/**
* 递归获取所有子节点
*
* @param sysMenuEntity
* @param sysMenuEntityList
* @return
*/
private List<SysMenuEntity> getChildren(SysMenuEntity sysMenuEntity, List<SysMenuEntity> sysMenuEntityList) {
List<SysMenuEntity> children = sysMenuEntityList.stream().filter(sysMenu -> {
return sysMenu.getParentId().equals(sysMenuEntity.getMenuId());
}).filter(sysMenu -> {
return sysMenu.getType() != 2;
}).map(sysMenu -> {
sysMenu.setList(getChildren(sysMenu, sysMenuEntityList));
return sysMenu;
}).collect(Collectors.toList());
return children;
}
}
最后将这个四个服务都启动,如果都没报错,说明后台搭建完成。
创建一个vue项目,过程这里我就不演示了,我直接来说明一下renren-fast-vue中部分文件的作用。
首先是package.json,它的作用相当于pom,这一块需要修改成自己项目信息。
新建的项目仅需要这些依赖:
"dependencies": {
"axios": "0.17.1",
"babel-plugin-component": "0.10.1",
"babel-polyfill": "6.26.0",
"element-ui": "^2.15.6",
"lodash": "4.17.5",
"node-sass": "^6.0.0",
"pubsub-js": "^1.8.0",
"sass-loader": "6.0.6",
"svg-sprite-loader": "3.7.3",
"vue": "^2.5.2",
"vue-cookie": "1.1.4",
"vue-router": "3.0.1",
"vuex": "3.0.1"
},
index.js
/**
* 开发环境
*/
;(function () {
window.SITE_CONFIG = {};
// api接口请求地址
window.SITE_CONFIG['baseUrl'] = 'http://localhost:9000/zukeApi';
// cdn地址 = 域名 + 版本号
window.SITE_CONFIG['domain'] = './'; // 域名
window.SITE_CONFIG['version'] = ''; // 版本号(年月日时分)
window.SITE_CONFIG['cdnUrl'] = window.SITE_CONFIG.domain + window.SITE_CONFIG.version;
})();
总结:后端整体采用了Mybatis-plus和Spring Boot、Spring Cloud那一套技术,对照renren-fast,我只采用了它的登录和菜单模块,并且删除了验证码和权限,保留了Token。而对于前端,除了login和menu模块以及一些公共视图模块,其他我都舍弃了。同样删除了大部分功能(权限、按钮相关),并且优化了添加目录后,左边导航栏不自动刷新问题。