1.项目亮点
2.技术架构
3.在本地配置虚假域名映射
修改本地的的host文件(配置小工具)
如果直接输入域名就能访问,就是不用输入端口号的话,就需要配置nginx
80是naginx的默认端口号监听即可(配置nginx.conf),一个server代表一个虚拟主机
4.持续集成自动化部署(Jenkins)
5.nginx部署前端
6.关于maven的小知识
maven模块的聚合,在父工程聚合
maven的继承,子模块继承父工程
关于maven依赖冲突问题的解决,以及版本的不兼容
7.idea远程调试微服务
记录一个判断bug
原因:
底层源码
8.Git的分支管理规范
9.项目的介绍
10.项目各个模块的业务流程
1.我的课程模块
、
11.MP代码生成器
先下载插件
配置数据源
配置相关包信息,还能设置swagger注解
12.枚举类的使用讲解
枚举类的好处就是不用在写业务代码的时候定义过多的常量,让代码更加的简洁
使用枚举的时候必须要明确两个区别,jackson返回前端的是value值1,2,3,fastjson是返回desc比如学习中,以及他们对Null值得处理
13.使用Mq进行消息通知
消息的幂等性处理就是在课程表中添加两个唯一约束,比如说课程id和用户id设置成唯一键。也能防止消息发送消息的重复。整体的流程是:生产者->交换机->队列->消费者
1.添加依赖
在生产模块和消费模块都要添加
2.配置yml
3.在生产者
配置类:生命交换机以及队列并对其持久化操作,绑定以及发送消息失败之后的回调
@Slf4j
@Configuration
public class PayNotifyConfig implements ApplicationContextAware {
//交换机
public static final String PAYNOTIFY_EXCHANGE_FANOUT = "paynotify_exchange_fanout";
//支付结果通知消息类型
public static final String MESSAGE_TYPE = "payresult_notify";
//支付通知队列
public static final String PAYNOTIFY_QUEUE = "paynotify_queue";
//声明交换机,且持久化
@Bean(PAYNOTIFY_EXCHANGE_FANOUT)
public FanoutExchange paynotify_exchange_fanout() {
// 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除
return new FanoutExchange(PAYNOTIFY_EXCHANGE_FANOUT, true, false);
}
//支付通知队列,且持久化
@Bean(PAYNOTIFY_QUEUE)
public Queue course_publish_queue() {
return QueueBuilder.durable(PAYNOTIFY_QUEUE).build();
}
//交换机和支付通知队列绑定
@Bean
public Binding binding_course_publish_queue(@Qualifier(PAYNOTIFY_QUEUE) Queue queue, @Qualifier(PAYNOTIFY_EXCHANGE_FANOUT) FanoutExchange exchange) {
return BindingBuilder.bind(queue).to(exchange);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 获取RabbitTemplate
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
//消息处理service
MqMessageService mqMessageService = applicationContext.getBean(MqMessageService.class);
// 设置ReturnCallback
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
// 投递失败,记录日志
log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}",
replyCode, replyText, exchange, routingKey, message.toString());
MqMessage mqMessage = JSON.parseObject(message.toString(), MqMessage.class);
//将消息再添加到消息表
mqMessageService.addMessage(mqMessage.getMessageType(), mqMessage.getBusinessKey1(), mqMessage.getBusinessKey2(), mqMessage.getBusinessKey3());
});
}
}
发送消息的方法
//发送通知消息
@Override
public void notifyPayResult(MqMessage message) {
//1.0构造一个消息
String msg = JSON.toJSONString(message);//消息体转为json
//设置消息持久化
Message msgObj = MessageBuilder.withBody(msg.getBytes(StandardCharsets.UTF_8)).setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();
//2.0全局唯一的消息ID,需要封装到CorrelationData中,就是一个回调方法
CorrelationData correlationData = new CorrelationData(message.getId().toString());
correlationData.getFuture().addCallback(
result -> {
if(result.isAck()){
// 3.1.ack,消息成功
log.debug("通知支付结果消息发送成功, ID:{}", correlationData.getId());
//删除消息表中的记录
mqMessageService.completed(message.getId());
}else{
// 3.2.nack,消息失败
log.error("通知支付结果消息发送失败, ID:{}, 原因{}",correlationData.getId(), result.getReason());
}
},
ex -> log.error("消息发送异常, ID:{}, 原因{}",correlationData.getId(),ex.getMessage())
);
//3.0发送消息,广播模式
rabbitTemplate.convertAndSend(PayNotifyConfig.PAYNOTIFY_EXCHANGE_FANOUT,"",msgObj,correlationData);
}
4.在消费者
最好也声明交换机这些,无论生产者先启动还是消费先启动都能保证先声明交换机和队列
@Slf4j
@Configuration
public class PayNotifyConfig implements ApplicationContextAware {
//交换机
public static final String PAYNOTIFY_EXCHANGE_FANOUT = "paynotify_exchange_fanout";
//支付结果通知消息类型
public static final String MESSAGE_TYPE = "payresult_notify";
//支付通知队列
public static final String PAYNOTIFY_QUEUE = "paynotify_queue";
//声明交换机,且持久化
@Bean(PAYNOTIFY_EXCHANGE_FANOUT)
public FanoutExchange paynotify_exchange_fanout() {
// 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除
return new FanoutExchange(PAYNOTIFY_EXCHANGE_FANOUT, true, false);
}
//支付通知队列,且持久化
@Bean(PAYNOTIFY_QUEUE)
public Queue course_publish_queue() {
return QueueBuilder.durable(PAYNOTIFY_QUEUE).build();
}
//交换机和支付通知队列绑定
@Bean
public Binding binding_course_publish_queue(@Qualifier(PAYNOTIFY_QUEUE) Queue queue, @Qualifier(PAYNOTIFY_EXCHANGE_FANOUT) FanoutExchange exchange) {
return BindingBuilder.bind(queue).to(exchange);
}
}
消费者接受消息
@Slf4j
@Service
public class ReceivePayNotifyService {
@Autowired
MyCourseTableService myCourseTableService;
//监听消息队列接收支付结果通知
@RabbitListener(queues = PayNotifyConfig.PAYNOTIFY_QUEUE)
public void receive(Message message, Channel channel){
//要是抛出异常别让代码执行的这么迅速
try{
Thread.sleep(5000);
}catch (InterruptedException ex){
ex.printStackTrace();
}
//解析消息
byte[] body = message.getBody();
String JsonString = new String(body);
//转成java对象
MqMessage mqMessage = JSON.parseObject(JsonString, MqMessage.class);
log.debug("学习中心服务接收支付的结果:{}",mqMessage);
//消息的类型
String messageType = mqMessage.getMessageType();
//订单类型
String businessKey2 = mqMessage.getBusinessKey2();
//这里只处理支付结果通知
if(PayNotifyConfig.MESSAGE_TYPE.equals(messageType)&& "60201".equals(businessKey2)){
//根据消息的内容,更新选课记录,向我的课程表中插入数据
//选课记录id
String chooseCourseId = mqMessage.getBusinessKey1();
boolean b = myCourseTableService.saveChooseCourseSuccess(chooseCourseId);
if(!b){
//添加选课失败,抛出异常,消息重回队列
XueChengPlusException.cast("收到支付结果,添加选课失败");
}
}
}
}
这个是有意思的一个接口常量类
package com.tianji.common.constants;
public interface MqConstants {
interface Exchange{
/*课程有关的交换机*/
String COURSE_EXCHANGE = "course.topic";
/*订单有关的交换机*/
String ORDER_EXCHANGE = "order.topic";
/*学习有关的交换机*/
String LEARNING_EXCHANGE = "learning.topic";
/*信息中心短信相关的交换机*/
String SMS_EXCHANGE = "sms.direct";
/*异常信息的交换机*/
String ERROR_EXCHANGE = "error.topic";
/*支付有关的交换机*/
String PAY_EXCHANGE = "pay.topic";
/*交易服务延迟任务交换机*/
String TRADE_DELAY_EXCHANGE = "trade.delay.topic";
/*点赞记录有关的交换机*/
String LIKE_RECORD_EXCHANGE = "like.record.topic";
/*优惠促销有关的交换机*/
String PROMOTION_EXCHANGE = "promotion.topic";
}
interface Queue {
String ERROR_QUEUE_TEMPLATE = "error.{}.queue";
}
interface Key{
/*课程有关的 RoutingKey*/
String COURSE_NEW_KEY = "course.new";
String COURSE_UP_KEY = "course.up";
String COURSE_DOWN_KEY = "course.down";
String COURSE_EXPIRE_KEY = "course.expire";
String COURSE_DELETE_KEY = "course.delete";
/*订单有关的RoutingKey*/
String ORDER_PAY_KEY = "order.pay";
String ORDER_REFUND_KEY = "order.refund";
/*积分相关RoutingKey*/
/* 写回答 */
String WRITE_REPLY = "reply.new";
/* 签到 */
String SIGN_IN = "sign.in";
/* 学习视频 */
String LEARN_SECTION = "section.learned";
/* 写笔记 */
String WRITE_NOTE = "note.new";
/* 笔记被采集 */
String NOTE_GATHERED = "note.gathered";
/*点赞的RoutingKey*/
String LIKED_TIMES_KEY_TEMPLATE = "{}.times.changed";
/*问答*/
String QA_LIKED_TIMES_KEY = "QA.times.changed";
/*笔记*/
String NOTE_LIKED_TIMES_KEY = "NOTE.times.changed";
/*短信系统发送短信*/
String SMS_MESSAGE = "sms.message";
/*异常RoutingKey的前缀*/
String ERROR_KEY_PREFIX = "error.";
String DEFAULT_ERROR_KEY = "error.#";
/*支付有关的key*/
String PAY_SUCCESS = "pay.success";
String REFUND_CHANGE = "refund.status.change";
String ORDER_DELAY_KEY = "delay.order.query";
/*领取优惠券的key*/
String COUPON_RECEIVE = "coupon.receive";
}
}
14远程调用其他模块Feign
在公用模块创建一个client
@FeignClient(contextId = "course", value = "course-service")
public interface CourseClient {
/**
* 根据老师id列表获取老师出题数据和讲课数据
* @param teacherIds 老师id列表
* @return 老师id和老师对应的出题数和教课数
*/
@GetMapping("/course/infoByTeacherIds")
List<SubNumAndCourseNumDTO> infoByTeacherIds(@RequestParam("teacherIds") Iterable<Long> teacherIds);
/**
* 根据小节id获取小节对应的mediaId和课程id
*
* @param sectionId 小节id
* @return 小节对应的mediaId和课程id
*/
@GetMapping("/course/section/{id}")
SectionInfoDTO sectionInfo(@PathVariable("id") Long sectionId);
/**
* 根据媒资Id列表查询媒资被引用的次数
*
* @param mediaIds 媒资id列表
* @return 媒资id和媒资被引用的次数的列表
*/
@GetMapping("/course/media/useInfo")
List<MediaQuoteDTO> mediaUserInfo(@RequestParam("mediaIds") Iterable<Long> mediaIds);
/**
* 根据课程id查询索引库需要的数据
*
* @param id 课程id
* @return 索引库需要的数据
*/
@GetMapping("/course/{id}/searchInfo")
CourseSearchDTO getSearchInfo(@PathVariable("id") Long id);
/**
* 根据课程id集合查询课程简单信息
* @param ids id集合
* @return 课程简单信息的列表
*/
@GetMapping("/courses