项目概览
创建项目
数据库
模块
第三方模块renren-fast
代码生成
分布式
注册中心nacos
- namespace
- group
- data-id
通信openfeign
网关Gateway
修改路由,对应前端项目
跨域请求
方案
通过NGINX可以将前后端变成同一个域
添加一个bean
@Configuration
public class GuliMallCorsConfiguration {
@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);
}
}
功能
逻辑删除
mybatis-plus配置一下
mybatis-plus:
global-config:
db-config:
logic-delete-value: 0
logic-not-delete-value: 1
logic-delete-field: show-status
在entity中的对应字段上添加@TableLogic
注解
然后就可以直接调用basemapper
的delete
方法
逻辑删除的方法是在
com.baomidou.mybatisplus.core.injector.methods
包里面设置的
我们通过log
可以看到在查询的时候,默认是有where logic-delete-field = logic-not-delete-value
的条件
添加/修改
mybatis-plus
中的IService
中有saveOrUpdate
方法,他是根据主键判断是save还是update
卧槽,本来是觉得pms_category
表里的name
不能重复的,后来发现可以重复的,所以直接用saveorupdate
就行了
但是因为没什么用,我们的
update
代码和insert
代码没有多少重复的地方,所以我们还是选择用两个函数去实现,就不用saveOrUpdate
了
修改showStatus
showStatus
是我设置的@TableLogic
字段,所以mbp在自动生成MappedStatement
的时候,是这样的SQL:
<script>
UPDATE pms_brand <set>
<if test="et['name'] != null">name=#{et.name},</if>
<if test="et['logo'] != null">logo=#{et.logo},</if>
<if test="et['descript'] != null">descript=#{et.descript},</if>
<if test="et['firstLetter'] != null">first_letter=#{et.firstLetter},</if>
<if test="et['sort'] != null">sort=#{et.sort},</if>
</set> WHERE brand_id=#{et.brandId} AND show_status=1
</script>
可以在
MapperMethod
里的SqlCommand
的Constructor
中找到MappedStatement
这里面作为@TableLogic
的字段没有放在可修改的字段中,所以如果要单纯修改showStatus
的话,只传入id
,showStatus
产生的SQL语句就会发生以下的错误:
UPDATE pms_brand WHERE brand_id=1 AND show_status=1
就尼玛离谱
为什么我们要单独修改showStatus
他本身就是个逻辑删除字段,修改他就相当于删除他或恢复他
前端的bind
很多东西要v-bind
或者 :
,因为这样子是双向绑定,数据改变了,对应的DOM也会改变,否则就是单相绑定,数据改变了,不会使DOM也跟着改变
拖拽
我们有要求:总共只有三层分类级别,所以要判断是否能拖拽到这个地方。
就是要遍历被拖拽的节点,最深有多少层,然后判断能否拖拽到目的节点
前端实现方法可以参考element-ui
的tree
的draggable
属性
我的想法
在后端用同一个方法update
参数有catId
,parentCid
,catLevel
,sort
哦哦 ,老师的想法
他是想把顺序就按照拖拽后的顺序精确排序
他遍历了新的位置的所有节点,然后按照该顺序全部重新设置sort
要注意的一些地方:
传过去的是一个列表
dragNode
要传过去的数据有:catId
,sort
,parentCid
,catLevel
,productCount
siblings
要传过去的就是catId
,sort
,productCount
漏了个:
如果draggingNode
的catLevel
发生变化,那他的子节点的catLevel
也要变化
批量拖拽
因为是批量拖拽,我们拖来拖去,其中的data
没有实时更新,点击批量拖拽后才更新,所以我们应该使用node
而不是data
。
这样子会导致有很多的重复更新,所以要不设置一个阈值,超过这个阈值的时候,我们的点击批量拖拽后,直接将当前的整个
tree
里的catId
,sort
,catLevel
等等数据提交上去,而不是提交一个一直再增长的listForm
批量删除
MBP的SQL
由于批量删除我想着直接传一个Class过去能不能行,结果说我传的类型不对,不符合SQL语句
我就想找找看SQL语句在哪里
通过debug呢,我们发现SQL语句是在项目启动的时候就创建好的,在com.baomidou.mybatisplus.core.injector.methods.Insert
之类的方法中,有injectMappedStatement
方法,最后通过String.format
方法来得到SQL语句,并由此创建SqlSource
然后创建MappedStatement
运行的时候,在找
preparedStatement
的时候发现,还会有@Deprecated
的class,com.baomidou.mybatisplus.core.executor.MybatisSimpleExecutor
,存在于类似的类下面。各个方法中有stmt
变量
分页查询
原本我看到有个page
方法,我就直接调用了Page<PmsBrand> page = brandService.page(new Page<PmsBrand>(Long.parseLong((String) params.get("current")), Long.parseLong((String) params.get("size"))))
方法,结果却发现SQL语句只是单纯的select,没有什么条件
看了文档发现要添加配置:
//Spring boot方式
@Configuration
@MapperScan("com.baomidou.cloud.service.*.mapper*")
public class MybatisPlusConfig {
// 旧版
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
// paginationInterceptor.setOverflow(false);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
// paginationInterceptor.setLimit(500);
// 开启 count 的 join 优化,只针对部分 left join
paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
return paginationInterceptor;
}
// 最新版
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
return interceptor;
}
}
page
里的参数就是current 当前页
,size 页显示数
,total 总数
,
云存储
使用阿里云的OSS服务
逻辑
将文件上传给阿里云的方式:
- 先上传给服务器,再由服务器将流上传给阿里云(太慢了)
- 直接将阿里云的地址和密钥写在js,前端直接上传给阿里云(不安全)
- 上传给阿里云,但是由服务器来进行签名(选用)
我们选择第三种,如何实现,就是用户先向前端发送上传文件请求,前端再向后端要签名,签名就是服务器根据账号密码而生成的一串东西,然后前端带着我们的防伪签名和文件直接提交给阿里云
实现
后端实现
接收前端的强求,返回给他签名:
@RequestMapping("/policy")
public R policy(){
// String accessId = "<yourAccessKeyId>"; // 请填写您的AccessKeyId。
// String accessKey = "<yourAccessKeySecret>"; // 请填写您的AccessKeySecret。
String bucket = "mine-gulimall"; // 请填写您的 bucketname 。
String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
// callbackUrl为 上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。
String callbackUrl = "http://88.88.88.88:8888";
String dir = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); // 用户上传文件时指定的前缀。
Map<String, String> respMap = null;
try {
long expireTime = 30;
long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
Date expiration = new Date(expireEndTime);
// PostObject请求最大可支持的文件大小为5 GB,即CONTENT_LENGTH_RANGE为5*1024*1024*1024。
PolicyConditions policyConds = new PolicyConditions();
policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
byte[] binaryData = postPolicy.getBytes(StandardCharsets.UTF_8);
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
String postSignature = ossClient.calculatePostSignature(postPolicy);
respMap = new LinkedHashMap<String, String>();
respMap.put("accessid", accessId);
respMap.put("policy", encodedPolicy);
respMap.put("signature", postSignature);
respMap.put("dir", dir);
respMap.put("host", host);
respMap.put("expire", String.valueOf(expireEndTime / 1000));
// respMap.put("expire", formatISO8601Date(expiration));
} catch (Exception e) {
// Assert.fail(e.getMessage());
System.out.println(e.getMessage());
} finally {
ossClient.shutdown();
}
return R.ok().put("map", respMap);
}
前端实现
使用element-ui
的el-upload
组件
<el-upload
class="upload-demo"
action="https://mine-gulimall.oss-cn-shanghai.aliyuncs.com/"
:data="dataObj"
list-type="picture"
:on-preview="handlePreview"
:on-remove="handleRemove"
:before-remove="beforeRemove"
:before-upload="beforeUpload"
:limit="1"
:multiple="false"
:on-exceed="handleExceed"
:on-success="handleSuccess"
:file-list="fileList">
<el-button size="small" type="primary">Click to upload</el-button>
<div slot="tip" class="el-upload__tip">jpg/png files with a size less than 500kb</div>
</el-upload>
然后在父组件中使用他:
import multiUpload from '@/components/upload/multiUpload'
<el-form-item label="品牌logo地址" prop="logo">
<multi-upload v-model="dataForm.logo" placeholder="品牌logo"></multi-upload>
</el-form-item>
这里的v-model
我们要通过在子组件中设置 this.$emit('input', file.url)
,将需要传输的数据交给父组件。就设置在onSuccess
中:
handleSuccess (response, file, fileList) {
console.log('response: ', response)
console.log('file: ', file)
console.log('fielist: ', fileList)
this.$emit('input', file.url)
},
获取文件路径
上传Object后,如果没有设置callbackUrl
的话,oss服务器不会返回任何信息,只有个 200 OK,而如果要设置callbackurl
的话,就是让oss服务器向这个callbackurl
发送post请求,请求体是我们向oss服务器发送的callbackbody
里面要求的信息。然后再由callbackurl
对应的服务器将信息发给我们的前端。
这里面的问题就是,我们的callbackurl
不能是本地地址,因为发个本地地址,oss服务器也访问不到,那么我在测试的时候,就不能通过这种方法来得到文件路径了。
于是只能自己根据文件路径名逻辑
去编码出来了:
https://mine-gulimall.oss-cn-shanghai.aliyuncs.com/2021-04-01/01ca15a0-1201-42e6-8601-8f2edf55bb0dcungu.jpeg
也就是this.action + this.dataObj.key
Validation
在前端我们需要通过验证请求的合法性,同样,在后端我们也要验证
后端的验证我们需要添加
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
原本我们添加
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
就够了,但是在新版的spring boot中,只添加validation-api
是不够的,因为这个只提供了JSR
的规则,没有提供实现类
ExceptionHandler
不知道为什么我的默认异常处理很普通。我也懒得debug,直接搜了个处理方案
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, String> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return errors;
}
ExceptionHandler
是spring boot的包里的类。他会检查到MethodArgumentNotValidException
异常,并执行这个类
还是不行
发现自定义这个处理类后,只有一点点的错误信息,不希望每个错误都要自己写,想要用封装好的,然后发现封装好的错误信息里面message
字段为空。通过debug,发现:
在org.springframework.web.servlet.DispatcherServlet.processDispatchServlet
方法中,有个mv = this.processHandlerException(request, response, handler, exception);
,这个方法搜寻handlerExceptionResolvers
,如果我们没有定义的话,搜寻到最后,调用的是HandlerExceptionResolverComposite
,然后再去HandlerExceptionResolverComposite
找到handlerExceptionResolver
,有三个,其他的返回为null
所以取消,DefaultHandlerExceptionResolver
有返回,他的处理仅仅是:
protected ModelAndView handleMethodArgumentNotValidException(MethodArgumentNotValidException ex, HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {
response.sendError(400);
return new ModelAndView();
}
所以我们的ex
参数根本没有用到,也就是没有返回详细的错误信息。
自定义了ExceptionHandler
他有个ExceptionHandlerMethodResolver
,这里会找到我们自定义的Method。
这段代码差点没看懂
public static Set<Method> selectMethods(Class<?> targetType, MethodFilter methodFilter) {
return selectMethods(targetType, (method) -> {
return methodFilter.matches(method) ? Boolean.TRUE : null;
}).keySet();
}
是表示第二个selectMethods接收的第二个参数是一个接口,我们传入的方法是这个接口的重写方法,也可以说就是这个接口的匿名实现类。
不愧是我,没有记笔记也能及时反应过来
解决
哦行吧,是新版的spring-boot-autoconfigure
包的问题,debug到最后,他是交给了BasicErrorController
,他会根据this.errorProperties
的属性来确定最后的错误返回信息包含什么,这个ErrorProperties
类是在启动项目的时候就完成了的,他默认message
属性为NEVER
.所以我试试看在application.properties
中设置,结果就成功了,server.error.include-message=always
。当然也有on-param
这个选项,不过我也不知道传的param
有什么要求,还有怎么传。反正目前传入的parameters
是没有。
统一异常处理
就如一开始写的一样。通过添加@ExceptionHandler
可以将该方法放到HandlerExceptionResolverComposite
,然后可以对异常进行处理。
我们把异常处理方法放到com.joiller.gulimall.product.exception.PmsExceptionHandler
中,
@RestControllerAdvice
public class PmsExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public R argumentNotValid(MethodArgumentNotValidException e){
return R.error(BizCodeEnum.VALID_EXCEPTION).put("error", e.getMessage());
}
@ExceptionHandler(Throwable.class)
public R otherExcption(Throwable throwable) {
return R.error(BizCodeEnum.UNKOWN_EXCEPTION).put("error", throwable.getMessage());
}
}
这里的BizCodeEnum
是自己额外添加的Enum:
@AllArgsConstructor
@Getter
public enum BizCodeEnum {
UNKOWN_EXCEPTION(10000, "未知异常"),
VALID_EXCEPTION(10001, "参数格式异常");
private int code;
private String message;
}
然后给R.error()
方法添加一个BizCodeEnum
的参数overload
分组校验
@Valid
是一个空注解,
@Validated
接收一个Class<?>[]
作为参数。
在分组校验中,我们可以使用@Validated
进行分组
只要创建一个空接口,就可以当做一个group。
在字段的校验要求上添加@NotBlank(groups = {UpdateGroup.class, AddGroup.class})
就是说这个校验是在{UpdateGroup.class, AddGroup.class}
这两个组中,当@Validated()
中包含了这两个组的其中一个,那才会被校验到。如果@Validated
不添加参数,那么只有无组别的校验才会被校验到。
比如说:@Validated
对应@NotBlank
,@Validated(UpdateGroup.class)
对应@NotBlank(groups = {UpdateGroup.class, AddGroup.class})
自定义校验
我们要自己写个校验注解
然后给这个校验注解写个处理类
然后关联一下。
直接参照现有的校验注解去实现:
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
//@Repeatable(ListValue.List.class)
@Documented
@Constraint(
validatedBy = {ListIntegerValidator.class}
)
public @interface ListValue {
String message() default "{com.joiller.gulimall.common.valid.ListValue.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int[] value();
}
这里的message
属性是引用了ValidationMessages.properties
里的属性。我们也创建一个这个ValidationMessages.properties
文件,在其中添加一个关于我们注解的属性:
com.joiller.gulimall.common.valid.ListValue.message = must in list {value}
然后创建一个处理类:
public class ListIntegerValidator implements ConstraintValidator<ListValue, Integer> {
HashSet<Integer> set = new HashSet<>();
@Override
public void initialize(ListValue constraintAnnotation) {
int[] value = constraintAnnotation.value();
for (int i : value) {
set.add(i);
}
}
@Override
public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
return set.contains(integer);
}
}
initialize
方法就是初始化,接收这个注解,通过这个注解,我们将注解中的数据得到。
然后在isValid
方法中判断数据的合法性。第一个参数就是这个注解的Target
所传过来的参数,我们要判断这个参数是否满足我们的注解参数value
。
要注意判断integer
是否为null。因为当我们这个字段没有得到 参数的时候,也会进行校验,这个时候在isValid
方法中发生了NullPointerException
,就会抛出新的错误,然后在我们这个项目中会交给我们写的统一异常处理中的@ExceptionHandler(Throwable.class) public R otherExcption(Throwable throwable) { return R.error(BizCodeEnum.UNKOWN_EXCEPTION).put("error", >throwable.getMessage()); }
去处理
完整的首页菜单
首页菜单是根据gulimall_admin
.sys_menus
表格建立的,通过教程给的代码,生成了这个表格,然后可以看见首页的菜单增加了。
商品系统
品牌管理
spu (Standard Product Unit)和 sku(stock keeping unit)
电商里的概念
类目: 有手机 => 智能手机 => 苹果XS
SPU: 就是商品聚合信息的最小单位,就是一款产品他的公有信息,比如苹果XS
SKU: 就是具体的,确定的一种商品。比如在官网买苹果XS 的时候需要选择各种各样的属性,就是在确定具体哪一款,当然不包含商品序列号,因为SKU不是确定的一件,而是一款
属性分组
分页查询
前端
通过renren-generator
生成前端,不过我们的需要添加一个组件,那就是common.category.vue
,我们要求每个页面都是,左边是category,右边是具体信息。
这就需要我们在组件中引用category.vue
了,利用Vue的$emit
实现父子组件的交互
后端
这里我希望对分页查询作一个泛化
?我也不确定这种说法对不对。
在Service
中添加方法:
public <E extends IPage<PmsAttrGroup>> E page(E page, String key, Integer catelogId)
主要问题在Page
这个类,他有多个Constructor
,可以接收这么多的参数:
public Page(long current, long size) {
this(current, size, 0L);
}
public Page(long current, long size, long total) {
this(current, size, total, true);
}
public Page(long current, long size, boolean isSearchCount) {
this(current, size, 0L, isSearchCount);
}
public Page(long current, long size, long total, boolean isSearchCount) {
...
}
关键这些参数还都是primitive
,这样子就不能传入null
值了,否则会发生NullPointerException
而controller
传入的参数不一定全都有,就不确定选择哪一个Constructor
了。现在我想到的解决方案:
- 在
controller
中做if判断 - 创建一个新的
PageUtils
类,在PageUtils
类中做判断 - 创建新的
PageUtils
类,给参数作默认值初始化,然后有值的赋值,调用全参构造
选择了第三个方案
public class PageUtils<T> {
long total = 0L;
long current = 1L;
long size = 10L;
boolean isSearchCount = true;
public Page<T> page(Map<String, String> map) {
if (map.get("current") != null) {
current = Long.parseLong(map.get("current"));
}
if (map.get("size") != null) {
size = Long.parseLong(map.get("size"));
}
if (map.get("total") != null) {
total = Long.parseLong(map.get("total"));
}
if (map.get("isSearchCount") != null) {
isSearchCount = Boolean.parseBoolean(map.get("isSearchCount"));
}
return new Page<>(current, size, total, isSearchCount);
}
}
返回完整category路径
因为在前端使用的cascader
标签是要接收一个数组,里面包含依照父子关系从左至右的数组。
我们在后端选择通过categoryService
来获取数组,并且在attrBrandGroup
中添加一个字段来接收这个数组:
@Service
public class PmsCategoryServiceImpl extends ServiceImpl<PmsCategoryMapper, PmsCategory> implements IPmsCategoryService {
...
@Override
public List<Serializable> getPath(Serializable id) {
return getPath(id, new LinkedList<>());
}
private List<Serializable> getPath(Serializable id, LinkedList<Serializable> path){
path.addFirst(id);
PmsCategory temp = this.getById(id);
if (temp.getParentCid()==0){
return path;
}
return getPath(temp.getParentCid(), path);
}
}
@Data
@EqualsAndHashCode(callSuper = false)
public class PmsAttrGroup implements Serializable {
@TableField(exist = false)
private List<Serializable> catelogPath;
}
品牌分类关联与级联更新
品牌的查询的key
字段也加上先
关系
一个品牌关联多个分类,一个分类下有多个品牌,所以是Many to Many
表格:pms_category_brand_relation
表格。
API
其他表的修改
因为在关联表中有brand_name
,catelog_name
字段,所以当这两个改变的时候,关联表要同步改变。
我们在这两个表进行update的时候,判断是否更新name
字段,如果更新了,那么就将关联表中的name也更新。
有多种改法
可以在category service
中调用relation service
,然后创建relation entity
通过relation service
.update
,将relation entity
和update wrapper
传入。
或者可以在category service
中调用relation service
,然后创建新的relation service
.updateDetail
,updateDetail
接收category
类。
属性与属性分类的关联
attr
与 attr_attrgroup
attr
.save
的时候,我们要在attr_attrgroup_relation
中也添加相应的字段
@Override
public boolean save(AttrVo attrVo) {
PmsAttr attr = new PmsAttr();
BeanUtils.copyProperties(attrVo, attr);
this.save(attr);
PmsAttrAttrgroupRelation relation = new PmsAttrAttrgroupRelation();
relation.setAttrId(attr.getAttrId());
relation.setAttrGroupId(attrVo.getAttrGroupId());
relation.setAttrSort(0);
relationService.save(relation);
return true;
}
这里的VO是Value Object, 是一种概念,类似于DAO
VO在这里专门用来对前端的请求数据和后端的返回数据进行转换的。
因为我们的DAO经常不能满足前端的请求
这里我们准备在
relation
表格中添加新增的attr_id
的时候,可以用DAO的attr.getAttrId()
,刚刚save好了,这里的attrId
也会变成相应的id
。
功能
- 根据分组查询该分组下的属性
- 点击新增弹出可以添加至该分组的属性
- 添加属性时选择分组
逻辑较为简单,技术里用到了有mapper.xml
的模板技巧,通过@Param("pname")
来给mapper
中的语句传参:
public interface PmsAttrAttrgroupRelationMapper extends BaseMapper<PmsAttrAttrgroupRelation> {
boolean deleteBatchRelations(@Param("relations") PmsAttrAttrgroupRelation[] relations);
}
<delete id="deleteBatchRelations">
DELETE FROM `pms_attr_attrgroup_relation` WHERE
<foreach collection="relations" item="item" separator=" OR ">
attr_id = #{item.attrId} and attr_group_id = #{item.attrGroupId}
</foreach>
</delete>
前端的配置
关于vue cli中修改alias, 就是经常见到
@
这个符号,这个其实在@vue/cli
也就是3.x的vue cli中,可以在root path 下创建vue.config.js
,然后
module.exports = {
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
}
然而我们这个是老的
vue cli
,所以不会解析vue.config.js
,取而代之是会出现可见的webpack.base.conf.js, webpack.dev.conf.js , webpack.prod.conf.js
,其中base
是会在dev
和prod
环境下都解析的。我们只需要把上面的东西放入到对应的webpack.conf.js
中就行了。
是的,在新的vue cli
中,这些webpack
配置会隐藏
业务逻辑规范
这个老师说是controller
里面最好分三部分:
- 处理请求,接收和校验数据
- 调用
service
,让service
进行业务处理 - 接收
service
的数据,封装页面指定的vo
商品维护
发布商品
页面1
进入页面会请求/member/memberlevel/list
会员等级。第一个页面的选择分类有watcher
,当catelogPath
发生变化的时候,请求得到该分类下的品牌,/product/categorybrandrelation/brands/list
页面2
进入页面会请求/product/attrgroup/{catId}/withattr
,得到该三级分类下的所有分组,并且每个分组也要带上该分组下的所有属性
页面3
没什么要请求的,就是制定一些其他的属性。指定好后就会将数据打包成JSON发送给product/spuinfo/save
,数据比较多,我们将JSON复制到在线JSON处理工具中,自动生成JAVA BEAN
,就会方便很多
自动生成
自动生成的Javabean的type一般不准,我们需要手动修改这些type
调用其他微服务
这个页面生成了很多数据,有些数据不在product
数据库中,而在其他的数据库中,其他的数据库我们用的是其他的微服务。这时候就需要利用openfeign
来调用其他的服务了。
调用逻辑主要是:
- 将自己的请求数据转换为json
- 找到对方服务,给对方的网络接口发送请求,将上一步转换的json放在请求体中
- 对方服务收到请求,处理json数据
所以我们的请求体和对方接收的请求体不一定需要是同一个类,因为只要都能处理相同的json即可
怎么实现呢:
首先要各种enable
,然后在自己的微服务中创建一个接口:
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
@PostMapping("/coupon/sms-spu-bounds/save")
R saveSpuBounds(SpuBoundsTo spuBoundsTo);
@PostMapping("/coupon/sms-member-price/saveBatch")
R saveSkuMemberPrices(List<SkuMemberPriceTo> skuMemberPriceTos);
@PostMapping("/coupon/sms-sku-full-reduction/save")
R saveSkuFullReduction(SkuFullReductionTo skuFullReductionTo);
}
然后在相应的地方调用这个接口bean就可以了
Debug
由于这个业务很长,涉及很多的代码和数据库操作,难免会有bug,所以我们应该开启debug模式,一步一步的对照数据库
注意由于这个是一个事务,那么在事务结束前是不会commit的,如果想要看到没有commit的数据,就要执行以下的SQL语句:
Syntax Affected Characteristic | Scope |
---|---|
SET GLOBAL TRANSACTION transaction_characteristic | Global |
SET SESSION TRANSACTION transaction_characteristic | Session |
SET TRANSACTION transaction_characteristic | Next transaction only |
这边发现个bug,就是该service添加了
@Transcational
注解,但是他也调用了openfeign
,openfeign
调用的方法又不归这个springboot application
管,所以要如何令其他的微服务也和我们保持同一个transaction
,就是一个问题了,以后再说吧,老师也没说。
(D)TO (Data) Transer Object
类似于VO,DAO, 他就是个用来传输数据的,主要用在微服务的交互上。‘
不过我网上搜的各种用法和这个项目不一样,反正没什么难的,到时候具体项目具体规定吧。
继承父类后lombok.EqualsAndHashCode
这个注解是为我们的类添加equals
和hashCode
方法的,通过@EqualsAndHashCode.Exclude
可以将指定field
不被这两个方法考虑。设置@EqualsAndHashCode(callSuper = true)
就可以让这两个方法将父类的字段也考虑进去。但是如果没有继承什么父类的话(继承了Object),将callSuper=true
会是一个compile-time error
,也就是不会运行。因为这样子做的话,equals
和hashCode
会和父类一样,也就是只有是同一个对象的情况下才会equal
才会有相同的hashCode
循环依赖
当我们多个service
互相调用会出现这种问题,所以最好还是controller
调用service
,service
调用mapper
。同一层的东西不要互相调用。
不过spring会解决这个问题,首先开启allowCircularReferences
,那么spring会有两个池子,一个成品池子,一个半成品池子,在注入bean,向容器获取bean的时候,会优先向成品池子要,要不到,然后再问半成品池子要。
这里再说一下。
Autowired
是bytype,是构造注入Resource
是byname, 是setter注入
spu管理
分页查询
返回的时间戳,我们可以通过配置spring.jackson.date-format
来格式化json返回的日期格式
…歪日我怎么不行? 我先上床了
库存系统
分页插件
这边忘了件事情,发现调用mybatis-plus
的page
方法的时候,select
语句没有应用到page
里的东西,原来是分页插件忘记了,在官方文档里就有。不过问题是我想把他放到common里,但是由于他是springboot compopent
, springboot
只会找到自己主启动类同级的包,所以也就不方便找到common里的配置。如果给springboot配置一下component位置的话,也要给每个微服务都配置,现在就是想,如果可以在配置文件中配置component位置,那么就可以让每个微服务都有一个共享的配置文件,在这个配置文件上添加component位置。
logging
之前的product
微服务是用的logback
来配置日志的:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
<!-- <include resource="org/springframework/boot/logging/logback/file-appender.xml" />-->
<root level="DEBUG">
<appender-ref ref="CONSOLE" />
<!-- <appender-ref ref="FILE" />-->
</root>
<logger name="com.joiller.gulimall.product.mapper" level="DEBUG"/>
</configuration>
而spring boot给我们提供了简单点的方法,直接在配置文件中添加:
logging:
level:
com.joiller: debug
logging.level
接收一个map
仓库维护
分页查询
带上key
参数
商品库存
分页查询
采购单维护
逻辑
先有采购需求,然后合成为采购单,然后执行采购单,然后就会增加商品库存
合并到整单
采购需求: purchase_detail
采购单: purchase
就是将采购需求分配到采购单中,就是更新采购需求里的采购单id
如果没有指定采购单,就新建一个
领取/完成采购单
采购员领取采购单,采购单状态变为已领取,而且已领取的采购单中的采购需求的状态变为正在采购,也不能再被分配到别的采购单。
完成采购单,将采购单和采购需求状态变为已完成
这两个功能不是后台管理系统的功能,需要从postman来发送请求。
完成采购单:
除了更新采购单和采购需求的状态外,还要添加库存。
requestbody:
{
"id": 5,
"items": [
{
"itemId": 8,
"status": 3,
"reason": ""
},
{
"itemId": 9,
"status": 4,
"reason": "没货"
}
]
}
所以我们再添加VO:
@Data
public class PurchaseDoneVo {
@NotNull
private Integer id;
private List<PurchaseItemDoneVo> items;
}
@Data
public class PurchaseItemDoneVo {
@NotNull
private Long itemId;
private Integer status;
private String reason;
}
添加库存就需要这几个参数: 将哪个sku添加到哪个ware多少stock个
sku_id
,ware_id
,stock
基础篇 的一些BONUS
做完准备再给product
添加一个spu管理
的规格
功能的时候,发现根本无法访问,弹出来是404,原来是前端router.index.js
向后端发送/sys/menu/nav
的请求,得到所有可以访问的路径,然后其他路径都是转向给404,所以根据后端该路径,可以找到是数据库gulimall_admin.sys_menu
这里出现了遗漏,这个数据应该是可以通过前端页面的菜单管理
功能添加的,但是我才不搞那么麻烦,直接插入数据库就完事儿了,反正不是我写的,有什么bug也无所谓。
又添加了个规格更新功能,这个是更新对应spu的各个属性,逻辑居然是把原本的属性记录全部delete
,再将新的数据全部insert
。因为这里要update
的是多个,需要执行多次SQL,而insert
就可以只执行一次了。
基础篇的总结
分布式基础概念
微服务, 注册中心, 配置中心, 远程调用, 网关
基础开发
springboot2.0, springcloud, mybatis-plus, Vue组件化, 阿里云对象存储oss
环境
Linux, docker, MySQL, redis, 逆向工程, 人人开源
开发规范
数据校验JSR303 , 全局异常处理, 全局统一返回, 全局跨域处理
枚举状态/业务状态码,VO/TO/PO的划分,逻辑删除
Lombok @Data, @Slf4j