# gitlearn **Repository Path**: kelez/gitlearn ## Basic Information - **Project Name**: gitlearn - **Description**: git学习 - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-04-27 - **Last Updated**: 2023-09-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 员工管理模块 2023/5/1 ### 添加员工信息 - 先检查前端的信息能否正确提交 - 为员工设置初始化密码 - 填充其他缺失字段 - 为了捕捉因为username重复抛出的异常,写了一个全局异常处理器。 ### 员工信息分页查询 - 需求说明 - 规定每页的页数 - 通过翻页能获取所有员工信息 - 为了进行分页查询,加入一个mybatisps的拦截器 ``` @Configuration public class MyBatisPlusConfig { @Bean // 这里忘记加注解,导致不能分页 public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor mi = new MybatisPlusInterceptor(); mi.addInnerInterceptor(new PaginationInnerInterceptor()); return mi; } } ``` 2023/5/3 - 这里没有使用rest风格,而是直接用的属性拼接的方式,比如:http://localhost:8080/employee/page?page=1&pageSize=10&name=zhangs - 后台的接收方法如下,在查询用户时,name才会用到 ``` @GetMapping("/page") public R page(int page, int pageSize, String name) { return null; } ``` - 出现bug - 忘记在拦截器上加bean注解,导致sql语句中没有出现limit分页 ### 员工账号的启用与禁用 需求说明 1. 只有管理员能够启动或者禁用其他员工的状态 处理步骤 1. 拿到前台传来的用户id和用户status 2. 根据id修改status 知识点: - js对数字的处理只能到16位,而数据库中19位用户id使用js保存时丢失精度,所以这里需要扩展**消息转换器** 将long数据保存为字符串格式,并保存正确的日期格式。具体修改见com/itheima/reggie/config/WebMvcConfig.java ### 编辑员工信息 需求说明 1. 员工列表页面点击编辑按钮,编辑员工信息并且保存提交。 2023/5/4 ## 分类管理模块 公共字段自动填充 - 代码中出现了很多重复,例如每次更新信息的时候都需要加上更新时间,更新人的信息。可以在实体类中使用@TableField(fill = FieldFill.INSERT)注解,结合MyMetaObjectHandler,在插入该数据或者更新该数据时候,完成自动填充 - 使用ThreadLocal的线程隔离效果,一个HTTP请求,为一个线程,所以可以通过一个**线程**,从而维护登录用户的id。具体见common下的BaseContext,使用如下代码,我们可以在过滤器中存放id,然后在自动填充时获取id。 ``` public class BaseContext { private static ThreadLocal threadLocal = new ThreadLocal<>(); public static void setThreadLocal(Long id) { threadLocal.set(id); } public static Long getThreadLocal() { threadLocal.get(); } } ``` 分类模块需求分析 - 用户需要添加菜品分类,然后向菜品分类中添加菜品。 框架搭建 - 创建Category相关的框架 - 创建Mapper - 借助mybatisplus,创建service与对应的impl - 创建controller 添加菜品查询菜品 - 查询所有菜品,并按照sort字段进行排序 删除菜品 - 举个🌰,若该菜品类型下面已经添加了菜品,这时候要进行提示,且不能将该类型进行删除。 - 实现方式 - 创建Dish和Setmeal的「Mapper、Service、Controller」。 - 若某个菜品类下存在dish,Setmeal,则抛出一个「自定义异常」,否则直接删除。 修改菜品 bug记录 - 程序显示找不到实体类,可能因为我的实体类复制粘贴进来的,重新构建一下项目,问题解决。 2023/5/5 ## 文件上传功能 文件上传要求 - 采用post提交方式,method="post" - 采用multipart格式上传文件,enctype="multipart/form-data" - 使用input的file控件上传,type="file" - 代码中MultipartFile参数名称要与前端页面的name名称一致 ## 菜品管理功能 ### 添加菜品 - 每个菜品都有属于自己的菜品分类,菜品分类是已经创建好的。 - 添加dish和dishFlavor的mapper,service,添加dish的controller同时处理dish与dishFlavor的服务。 - 因为dish中需要添加口味🍜,使得实体类与前端传输过来的json数据不能对应,需要用到DTO,数据传输对象。 ### 菜品信息分页查询 - 简单将菜品信息进行分页查询并返回,但是不能返回对应的菜品分类。 - 通过联查,显示菜品分类,详情见DishController.java的page方法。 ### 修改菜品信息 - 完成菜品信息的回显以及修改 - 因为返回的Dish中不存在DishFlavor,所以需要使用Dto ### 删除菜品、修改菜品状态(未实现) - 修改状态:感觉应该是直接修改标志位状态就行 - 删除:我这里直接删除了该条信息和对应的风味,还有对应的图片 ❓我的问题❓: - 每次上传图片就直接保存到服务器了,但我要是在新增菜品的时候点击了取消,那上传上去的图片不但没用上,而且还占用了一定的空间,这该怎么办 2023/5/6 ## 套餐管理 ### 新建套餐 - 套餐菜品添加按钮 CategoryController.list() - 套餐菜品添加 SetmealController.save() ### 分页查询套餐 - 两表联查显示套餐分类 ### 修改套餐 - 信息回显 ### 删除套餐 - 只有在停售状态下的套餐才能删除 - 逻辑删除(未实现) ### 套餐停售 - 菜品停售对应的套餐要停售 - 套餐起售菜品也要起售。 - 套餐删除要删除套餐表,套餐菜品表。 - 菜品删除要删除菜品表,口味表,套餐菜品表。 ### spring cache缓存注解 作为一个缓存的框加,根据缓存技术的不同实现不同的CacheManager,这里使用redis作为缓存。 ![](https://cdn.nlark.com/yuque/0/2023/png/38710874/1693739861227-550f05eb-c0e4-4626-a54e-69f537daa30f.png?x-oss-process=image%2Fresize%2Cw_750%2Climit_0#averageHue=%23eeedf5&from=url&id=ElsXR&originHeight=319&originWidth=750&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=stroke&title=) #### 本身的Map实现 [注解用法](https://blog.csdn.net/pengzhisen123/article/details/94409825) 缺点,使用map进行缓存存储在本地内存中,重启之后缓存就不存在。 #### redis实现 加入maven依赖 ```xml org.springframework.boot spring-boot-starter-data-redis org.springframework.boot spring-boot-starter-cache ``` yml配置,连接的数据 ```yaml redis: host: 192.168.174.130 port: 6379 database: 0 ``` 配置缓存数据的过期时间 ``` spring: cache: redis: time-to-live: 1800000 ``` 首先在application开启@EnableCaching注解 当存在该key时直接返回,不存在存入redis,把value看成一个目录,然后目录下面存放着键值对,键就是key,值是方法的返回值。 ```java @GetMapping("/list") @Cacheable(value = "setmealCache", key = "#setmeal.categoryId + '_' + #setmeal.getStatus") public R> list(Setmeal setmeal) { log.info("Setmeal=>{}", setmeal); LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(setmeal.getCategoryId() != null, Setmeal::getCategoryId, setmeal.getCategoryId()); queryWrapper.eq(setmeal.getStatus() != null, Setmeal::getStatus, setmeal.getStatus()); queryWrapper.orderByDesc(Setmeal::getUpdateTime); List list = setmealService.list(queryWrapper); return R.success(list); } ``` 缓存成功 ![image.png](https://cdn.nlark.com/yuque/0/2023/png/38710874/1693746336398-3c4f791c-cbaf-4718-8471-3eda76822713.png#averageHue=%23f6f6f5&clientId=u42db93b4-40b5-4&from=paste&height=160&id=hXwRv&originHeight=160&originWidth=470&originalType=binary&ratio=1&rotation=0&showTitle=false&size=10801&status=done&style=stroke&taskId=u3fa4dc7a-241b-490d-8604-cbea0de7b98&title=&width=470) 当有新数据或者数据发生修改时,allEntries = true表示将所有的缓存删除。 ```java @PostMapping @CacheEvict(value = "setmealCache", allEntries = true) public R save(@RequestBody SetmealDto setmealDto) { setmealService.saveWithDish(setmealDto); return R.success("添加套餐成功"); } ``` ## 手机验证码 - 可以直接使用[阿里云测试](https://dysms.console.aliyun.com/quickstart) ## 移动端页面登录 - 由于User对象中不存在code(验证码)属性,可以使用Map接收json 今日bug - 移动端页面登陆后直接退出,原因:没有设置session,直接被拦截 2023/5/7 - 用户地址簿(直接导入) - 用户地址删除、修改方法 2023/5/9 ## 点餐主页 - 查看套餐页面SetmealController.list() - 点击套餐,查看套餐下的餐品SetmealController.dishList() ## 购物车 - 创建购物车的mapper、service、controller - 添加菜品或套餐到购物车 - 点击购物车,查看购物车中的「菜品和套餐」 - 清空购物车 ## 订单 - 订单需要对三张表进行操作 - 前端传入addressBookId,获取用户id - 根据用户id获取购物车菜品信息 - 更新订单明细表 - 更新订单表 - 将原购物车中的菜品信息删除 今日遇见的小问题 - 前端传回json格式使用注解@RequestBody接受,若是使用「?参数」的形式,后端直接使用实体类接收就好 2023/5/10 ## 订单明细 - 后台 - 分页查询 - 查询订单编号 - 订单状态修改 - 前台 - 个人中心订单显示、历史订单显示 - 创建 OrderDto,存放订单明细 - 退出登录 - 再来一单 - 只做了个页面跳转,可能是要把当前订单明细中的所有菜品,都加到购物车再买一次。 ## MySQL主从复制 为了防止因为访问用户过多给MySQL带来的压力,我们设置主从复制,主库执行DML语句,从库主要进行select,然后完成主从之间的数据同步。 主从复制时一个异步的复制过程,底层是基于MySQL自带的二进制日志bin log功能。其中有一个主库「master」,一个或者多个从库「slave」,从库从主库进行日志的复制,解析日志,并应用到自身,最终实现主库和从库的数据保持一致。 ### 复制的过程 master将改变记录到bin log save中的IO进程将master的bin log拷贝到中继日志relay log slave重做中继日志中的事件,将改变应用到自己的数据库中。 ### 配置主库 1、打开配置文件,vi /etc/my.cnf > [mysqld] > log-bin=mysql-bin > server-id=100 2、重启mysql服务 > systemctl restart mysqld 3、授权从库,授权用户名为「xiaoming」密码为「Tute@123」的从库。 > grant replication slave on ***.*** to 'xiaoming'@'%' identified by 'Tute@123'; 4、主库执行show master status,显示正在使用的bin log,这里面会记录一DML语句,可以提供给从库使用,此时不要再执行任何命令,然后配置从库。 ```java mysql> show master status; +------------------+----------+--------------+------------------+-------------------+ | File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set | +------------------+----------+--------------+------------------+-------------------+ | mysql-bin.000001 | 154 | | | | +------------------+----------+--------------+------------------+-------------------+ ``` ### 配置从库 1、修改配置文件,两个id不能重复, > [mysqld] > server-id=101 2、登录登录MySQL,配置从库, 注意File和Position的对应关系 > change master to master_host='192.168.174.130',master_user='xiaoming',master_password='Tute@123',master_log_file='mysql-bin.000001',master_log_pos=154 3、启动从库 > start slave; - 如果原本存在slave的话,可能存在无法启动slave,此时需要执行stop slave - 如果出现不能加载delay.log,可以参考[MySQL主从复制,启动slave时报错](https://blog.csdn.net/weixin_37998647/article/details/79950133),执行reset slave,然后start slave、 4、查看状态 > show slave status \G; 显示 ```java Relay_Master_Log_File: mysql-bin.000001 Slave_IO_Running: Yes Slave_SQL_Running: Yes ``` ### 测试 在主库中执行DML和DDL,从库都会发生相同的变化 注意这里不要在从库发生修改,否则show slave status的时候slave sql running会变为No,这时候主库不要动,需要重启从库然后将上面的从库配置程序再执行一遍。 ## 读写分离Sharding-JDBC ```xml org.apache.shardingsphere sharding-jdbc-spring-boot-starter 4.0.0-RC1 ``` 将查询扔给从库,主库执行增删改。 ## Nginx ### 部署静态资源 server_name:匹配对应的域名,如localhost root为存放静态资源的跟目录,浏览器可以访问/www/server/phpmyadmin下的资源 ```bash server { listen 80; server_name localhost; index index.html index.htm index.php; root /www/server/nginx/html; #error_page 404 /404.html; include enable-php.conf; # location / { # root /www/server/nginx/html; # index index.html; # } access_log /www/wwwlogs/access.log; } include /www/server/panel/vhost/nginx/*.conf; } ``` ### 正向代理 梯子:我们无法直接获取外网服务器的内容,此时可以借助代理服务器,向代理发送一个请求并指定目标,然后代理向原服务器转交请求并获得服务器的内容返回给客户端,此时能够感知到代理服务器的存在 ![image.png](https://cdn.nlark.com/yuque/0/2023/png/38710874/1693828697260-ba8a3c5a-0350-4ec2-b8a7-ada836ba4741.png#averageHue=%23f7f5f9&clientId=u336a2e47-193c-4&from=paste&height=213&id=u71e20926&originHeight=266&originWidth=658&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=107477&status=done&style=shadow&taskId=ub25aa579-d2e2-44f1-9e9b-e680bb5f28b&title=&width=526.4) ### 反向代理 - 反向代理服务器位于用户和服务器之间,用户感知不到代理的存在, - 反向代理可以充当安全屏障,隐藏了后端服务器的真实IP地址,从而提高了安全性。它可以拦截和过滤恶意请求,提供额外的安全性层。 - 反向代理提供了单一的入口点,客户端可以通过一个域名或IP地址访问多个应用程序或服务。这简化了客户端的配置和管理。 配置反向代理: 反向代理服务器地址为192.168.138.100,通过监听82端口,将请求转发到web服务器 ![image.png](https://cdn.nlark.com/yuque/0/2023/png/38710874/1693831936408-2a78aa0b-e23c-4782-8b05-1e1783629038.png#averageHue=%23fdfbfd&clientId=ue6a6a639-276b-4&from=paste&height=465&id=u3f69ae31&originHeight=581&originWidth=961&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=171341&status=done&style=shadow&taskId=u5b3f2093-e904-49f1-9b16-3b38080e9d4&title=&width=768.8) ### 基于反向代理实现负载均衡 ```bash http { upstream haahha{ # 用于负载均衡,这里可以指定多个服务器,在这里只是转发到了haahha的网关地址 server 192.168.80.1:88; } include /etc/nginx/conf.d/*.conf; # 加载Server块的路径 server { listen 80; # 监听80端口 server_name gulimall.com *.gulimall.com 7791a33n49.goho.co:47831; # 接收域名访问 location /static { # 静态文件的存放目录 root /usr/share/nginx/html; } location / { proxy_pass http://haahha; # 不同于以往的ip地址,用作负载均衡,haahha与upstream后的名称相同 } } } ``` ![image.png](https://cdn.nlark.com/yuque/0/2023/png/38710874/1693833056971-08cc7329-ea92-4d1d-9f9b-4743635f23df.png#averageHue=%23c9d0e9&clientId=ue6a6a639-276b-4&from=paste&height=287&id=u9dcf5b58&originHeight=359&originWidth=947&originalType=binary&ratio=1.25&rotation=0&showTitle=false&size=129929&status=done&style=shadow&taskId=ua3455811-ba0a-4d26-8379-b33dd1d6adf&title=&width=757.6) ### 使用Nginx进行前后端分离。 ## Swagger 后端自动生成接口文档 ```xml com.github.xiaoymin knife4j-spring-boot-starter 3.0.3 ``` - 添加注解, - 设置静态资源映射,「注意META-INF别拼错了」 - 添加Docket的Bean, - 使用http://localhost:8080/doc.html进行访问。 ```java @Slf4j @Configuration @EnableSwagger2 @EnableKnife4j public class WebMvcConfig extends WebMvcConfigurationSupport { @Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { // log.info("开始静态资源映射"); registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/"); registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/"); } @Bean public Docket createRestApi() { // 文档类型 return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.itheima.reggie.controller")) .paths(PathSelectors.any()) .build(); } /** * 接口文档描述 * @return */ private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("xxx") .version("1.0") .description("xxx接口文档") .build(); } } ``` ## 项目部署linux 2023/5/16 Ubuntu与centerOS的防火墙不同。[Ubuntu](https://blog.csdn.net/weixin_33814002/article/details/116814303) 开放端口 > firewall-cmd --zone=public --add-port=8080/tcp --permanent 刷新使得立即生效 > firewall-cmd --reload 查看开放的端口 > firewall-cmd --zone=public --list-ports 1. 切换prod环境,将redis和mysql全部换成线上环境,看能不能跑通 2. 放置前端dist资源,设置配置文件 ```java location / { root /www/server/nginx/html/dist; index index.html; } location ^~ /api/ { rewrite ^/api/(.*)$ /$1 break; proxy_pass http://127.0.0.1:8080; } ``` 3. redis使用docker安装,然后开放TCP端口就能使用。 4. 安装JDK,将本地的java项目clean、compile、package,打成jar。 5. 启动 ```java /usr/local/btjdk/jdk8/bin/java -jar -Xmx1024M -Xms256M /www/wwwroot/default/demo-0.0.1-SNAPSHOT.jar --server.port=8080 --itheima.basepath=/www/wwwroot/default/imgs/ ``` ## 修改套餐信息 在前端页面,需要这些信息,若传入错误的话无法完成回显 ``` this.ruleForm = res.data this.ruleForm.status = res.data.status === '1' this.ruleForm.price = res.data.price / 100 this.imageUrl = `/common/download?name=${res.data.image}` this.checkList = res.data.setmealDishes this.dishTable = res.data.setmealDishes this.ruleForm.idType = res.data.categoryId ``` 所以在返回值时,需要借助「Dto」