98-微服务项目的编写(下篇)

微服务项目的编写

这里是续写97章博客(上一章博客)的,所以若没有看的话,最好看完再来:
接着续写:再创建子项目支付微服务edu-pay-boot(8006):
最终成果:

98-微服务项目的编写(下篇)_第1张图片

对应的依赖:

<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>com.lagougroupId>
		<artifactId>edu-lagouartifactId>
		<version>1.0-SNAPSHOTversion>
	parent>
	<groupId>com.lagougroupId>
	<artifactId>edu-pay-bootartifactId>
	<version>0.0.1-SNAPSHOTversion>
	<name>edu-pay-bootname>
	<description>edu-pay-bootdescription>
	<properties>
		<java.version>11java.version>
	properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-webartifactId>
		dependency>
		<dependency>
			
			<groupId>com.github.wxpaygroupId>
			<artifactId>wxpay-sdkartifactId>
			<version>0.0.3version>
		dependency>
		<dependency>
			<groupId>org.apache.httpcomponentsgroupId>
			<artifactId>httpclientartifactId>
			<version>4.5.12version>
		dependency>
		<dependency>
			
			
			<groupId>com.jfinalgroupId>
			<artifactId>jfinalartifactId>
			<version>3.5version>
		dependency>
		<dependency>
			<groupId>org.springframework.cloudgroupId>
			<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
		dependency>
	dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.bootgroupId>
				<artifactId>spring-boot-maven-pluginartifactId>
			plugin>
		plugins>
	build>

project>

将配置文件后缀修改成yml,并加上如下内容:
server:
  port: 8006
spring:
  application:
    name: edu-pay-boot
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka
    register-with-eureka: true
    fetch-registry: true
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}

启动类如下:
package com.lagou;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient // 注册到中心的客户端
public class EduPayBootApplication {

	public static void main(String[] args) {
		SpringApplication.run(EduPayBootApplication.class, args);
	}

}

现在我们在启动类所在的包下,创建commons.PayConfig类:
package com.lagou.commons;

/**
 *
 */
public class PayConfig {
    
    //当然,下面的自然是操作不了的,只是测试数据
    
    //企业公众号ID
    public static String appid = "wx8397f8696b538317";
    // 财付通平台的商户帐号
    public static String partner = "1473426802";
    // 财付通平台的商户密钥
    public static String partnerKey = "8A627A4578ACE384017C997F12D68B23";
    // 回调URL,一般是通知地址,当然,他并不影响总体流程,只是给出一个数据而已
    //后面的注释可以不看,因为可能并不对
    //通常是异步的,所以基本可以随便写,如果需要对应官方的数据,那么可以自己写上方法
    //实际上回调地址如果官方找不到
    //其中没有上线,通常只会给出数据返回(可能如此),然后前端再次的拿着数据访问本机的对应的路径,从而得到数据
    //而不会使得直接的拿数据,访问本机的对应路径
    //我们通常也称这个地址为回调地址,类似于微信登录的那个地址,实际上也是这样的操作的
    
    //所以不是上线的,不会直接去访问,如果能够直接去访问
    //那么他基本是异步访问的,而不是只返回数据,一般都需要在外网可以访问的网站(直接的来说)
    //当然了,我们自己内网的网站别人是访问不了的,只有外网才可
    //即我们可以访问外网的,但不能访问内网的(除非是他自己访问)
    /*
    这里说明一下外网和内网,我们学习通常使用的是内网
    外网(广域网,也可以认为是公网),就是我们通常所说的Internet,它是一个遍及全世界的网络
    内网(局域网),相对于外网而言,主要指在小范围内的计算机互联网络
    外网上的每一台电脑都有一个或多个广域网IP地址,广域网IP地址要到ISP处交费之后才能申请到
    广域网IP地址不能重复
    内网上的每一台电脑都有一个或多个局域网IP地址,局域网IP地址是局域网内部分配的
    不同局域网的IP地址可以重复,不会相互影响
    */
    
    //通常我们有时也会将他称为通知地址
    
    public static String notifyurl="http://localhost:8006/pay/wxCallback";

}

然后直接创建controller.WxPayController类:
package com.lagou.controller;

import com.github.wxpay.sdk.WXPayUtil;
import com.jfinal.kit.HttpKit;
import com.lagou.commons.PayConfig;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

/**
 *
 */
@RestController
@RequestMapping("pay")
@CrossOrigin
public class WxPayController {

    @GetMapping("createCode")
    public Object createCode(String courseid,String coursename,String price) throws Exception {

        coursename = new String(coursename.getBytes("ISO-8859-1"),"UTF-8");

        //编写商户信息写入到map中
        Map<String,String> map = new HashMap<>();
        map.put("appid", PayConfig.appid); //公众账号ID,微信分配的公众账号ID(企业号corpid即为此appid)
        map.put("mch_id",PayConfig.partner); //商户号,微信支付分配的商户号
        map.put("nonce_str", WXPayUtil.generateNonceStr()); //随机字符串,不长于32位
        map.put("body",coursename); //商品的描述,这里就传递对应的课程名称
        map.put("out_trade_no",WXPayUtil.generateNonceStr()); //随机生成的商户订单号
        map.put("total_fee",price); //订单金额,说明:订单总金额,单位为分,只能为整数
        map.put("spbill_create_ip","127.0.0.1");
        map.put("notify_url",PayConfig.notifyurl); //通知地址(回调URL)
        map.put("trade_type","NATIVE"); //交易类型

        System.out.println("商户信息:" +map);

        //生成数字签名,并把商户信息转换成xml格式
        String xml = WXPayUtil.generateSignedXml(map, PayConfig.partnerKey);

        System.out.println("商户的xml信息:" +xml);

        //将xml数据发送给微信支付平台,从而生成订单
        String url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
        //发送请求,并返回一个xml格式的字符串
        String post = HttpKit.post(url, xml); 
        //将对应的xml信息给微信支付平台(因为一般他们通常操作xml信息)

        System.out.println(post);
        //到这里,由于是测试数据,那么基本会显示


        //微信支付平台返回xml格式的数据,将其转换成map格式并返回给前端
        Map<String, String> map1 = WXPayUtil.xmlToMap(post);
        map1.put("orderId",map.get("out_trade_no"));

        System.out.println(map1);


        return map1;
    }

    @GetMapping("checkOrderStatus")
    public Object createCode(String orderId) throws Exception {

        //编写商户信息
        Map<String,String> map = new HashMap<>();
        map.put("appid", PayConfig.appid); //公众账号ID,微信分配的公众账号ID(企业号corpid即为此appid)
        map.put("mch_id",PayConfig.partner); //商户号,微信支付分配的商户号
        map.put("out_trade_no", orderId); //商户订单号
        map.put("nonce_str",WXPayUtil.generateNonceStr()); //随机字符串,不长于32位
        System.out.println(orderId);

        String xml = WXPayUtil.generateSignedXml(map, PayConfig.partnerKey);

        //发送查询请求给微信支付平台
        String url = "https://api.mch.weixin.qq.com/pay/orderquery";

        //设置订单状态的开始时间点
        long l = System.currentTimeMillis();

        //不停的去微信支付平台询问是否支付成功
        while(true) {
            String post = HttpKit.post(url, xml);

            //对微信支付平台返回的查询结果进行处理
            Map<String, String> map1 = WXPayUtil.xmlToMap(post);


            if(map1.get("trade_state").equalsIgnoreCase("SUCCESS")) {
                return map1;
            }


            if(System.currentTimeMillis() - l>30*1000){
                return map1;
            }
            Thread.sleep(3000);
        }

    }

    @RequestMapping("wxCallback")
    public String wxCallBack(HttpServletRequest request, HttpServletResponse response) throws IOException {
        InputStream inStream = null;
        ByteArrayOutputStream outSteam = null;
        String resultxml = null;
        try {
            inStream = request.getInputStream();
            outSteam = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = inStream.read(buffer)) != -1) {
                outSteam.write(buffer, 0, len);
            }
            resultxml = new String(outSteam.toByteArray(), "utf-8");
        } catch (Exception e) {
            System.out.println("回调处理失败");
        }finally {
            if(null != outSteam) {
                outSteam.close();
            }
            if(null != inStream) {
                inStream.close();
            }
        }
        System.out.println("wxCallback - 回调请求参数:"+ resultxml);
        return resultxml;
    }

}
当然,需要支付,自然首先需要订单,所以这里先写到这里(即先不启动这个微服务)
然后创建子项目订单微服务edu-order-boot(8007):
最终成果:

98-微服务项目的编写(下篇)_第2张图片

98-微服务项目的编写(下篇)_第3张图片

对应的依赖:

<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>com.lagougroupId>
		<artifactId>edu-lagouartifactId>
		<version>1.0-SNAPSHOTversion>
	parent>
	<groupId>com.lagougroupId>
	<artifactId>edu-order-bootartifactId>
	<version>0.0.1-SNAPSHOTversion>
	<name>edu-order-bootname>
	<description>edu-order-bootdescription>
	<properties>
		<java.version>11java.version>
	properties>
	<dependencies>
		
		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-webartifactId>
		dependency>
		
		<dependency>
			<groupId>org.springframework.cloudgroupId>
			<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
		dependency>
		
		<dependency>
			<groupId>com.baomidougroupId>
			<artifactId>mybatis-plus-boot-starterartifactId>
			<version>3.3.2version>
		dependency>
		
		<dependency>
			<groupId>mysqlgroupId>
			<artifactId>mysql-connector-javaartifactId>
			<scope>runtimescope>
		dependency>
		
		<dependency>
			<groupId>javax.persistencegroupId>
			<artifactId>javax.persistence-apiartifactId>
			<version>2.2version>
		dependency>

		<dependency>
			<groupId>org.projectlombokgroupId>
			<artifactId>lombokartifactId>
			<version>1.18.12version>
		dependency>

	dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.bootgroupId>
				<artifactId>spring-boot-maven-pluginartifactId>
			plugin>
		plugins>
	build>

project>

将配置文件后缀修改成yml,内容如下:
server:
  port: 8007
spring:
  application:
    name: edu-order-boot
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.164.128:3306/edu_order?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: QiDian@666
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka
    register-with-eureka: true
    fetch-registry: true
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}

对应的启动类:
package com.lagou;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient // 注册到中心的客户端
@MapperScan("com.lagou.mapper")  // 扫描mapper包
public class EduOrderBootApplication {

	public static void main(String[] args) {
		SpringApplication.run(EduOrderBootApplication.class, args);
	}

}

然后在启动类所在的包下,创建entity包,该包下,创建如下实体类:
package com.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.io.Serializable;
import java.util.Date;

/**
 *
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
//并不是非要实现序列化接口(除非需要用到),只是一种规范而已,但也因为规范我们也通常需要写上,这里可以测试一下
//而之所以有这个规范,是因为,大多数(或者说有些框架会操作序列化,比如88章博客对序列化的说明,在操作redis那里)
public class UserCourseOrder /*implements Serializable*/ {
//    private static final long serialVersionUID = -77239403959527764L;
    /**
     * 主键
     */
    private Long id;
    /**
     * 订单号
     */
    private String orderNo;
    /**
     * 用户id
     */
    private Object userId;
    /**
     * 课程id,根据订单中的课程类型来选择
     */
    private Object courseId;
    /**
     * 活动课程id
     */
    private Integer activityCourseId;
    /**
     * 订单来源类型: 1 用户下单购买 2 后台添加专栏
     */
    private Object sourceType;
    /**
     * 当前状态: 0已创建 10未支付 20已支付 30已取消 40已过期
     */
    private Object status;
    /**
     * 创建时间
     */
    private Date createTime;
    /**
     * 更新时间
     */
    private Date updateTime;
    /**
     * 是否删除
     */
    private Object isDel;



}

创建mapper.OrderDao接口:
package com.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.entity.UserCourseOrder;


/**
 *
 */

public interface OrderDao extends BaseMapper<UserCourseOrder> {
}
创建service.OrderService接口及其实现类:
package com.service;

import com.entity.UserCourseOrder;

import java.util.List;

/**
 *
 */
public interface OrderService {
    /**
     *
     * @param orderNo 订单编号
     * @param user_id 用户编号
     * @param course_id 课程编号
     * @param activity_course_id 活动课程编号
     * @param source_type 订单来源类型
     */
    public void saveOrder(String orderNo,String user_id, String course_id, String activity_course_id, String source_type);

    /**
     *
     * @param orderNo 订单编号
     * @param status 订单状态 0已创建 10未支付 20已支付 30已取消 40已过期
     * @return 受影响的行数
     */
    Integer updateOrder(String orderNo,Integer status);
    /**
     * 删除订单
     * @param orderNo 订单编号
     * @return
     */
    Integer deleteOrder(String orderNo);
    /**
     * 查询登录用户的全部订单,即用户的全部订单,只是需要登录后才可以看到,这是一定的
     * @param userId 用户编号
     * @return 所有订单
     */
    List<UserCourseOrder> getOrdersByUserId(String userId);


}

package com.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.entity.UserCourseOrder;
import com.mapper.OrderDao;
import com.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.List;

/**
 *
 */
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderDao orderDao;

    @Override
    public void saveOrder(String orderNo, String user_id, String course_id, String activity_course_id, String source_type) {
        UserCourseOrder order = new UserCourseOrder();
        order.setOrderNo(orderNo);
        order.setUserId(user_id);
        order.setCourseId(course_id);
        order.setActivityCourseId(Integer.parseInt(activity_course_id));
        order.setSourceType(source_type);
        order.setStatus(0);
        order.setIsDel(0);
        order.setCreateTime(new Date() );
        order.setUpdateTime(new Date() );

        orderDao.insert(order);
    }

    @Override
    public Integer updateOrder(String orderNo, Integer status) {
        UserCourseOrder order = new UserCourseOrder();
        order.setStatus(status);

        QueryWrapper q = new QueryWrapper();
        q.eq("order_no", orderNo);

        return orderDao.update(order,q);
    }

    @Override
    public Integer deleteOrder(String orderNo) {
        QueryWrapper q = new QueryWrapper();
        q.eq("order_no", orderNo);

        return orderDao.delete(q);
    }

    @Override
    public List<UserCourseOrder> getOrdersByUserId(String userId) {
        QueryWrapper q = new QueryWrapper();
        q.eq("user_id", userId); // 该用户
        return orderDao.selectList(q);
    }
}

创建controller.OrderController类:
package com.entity.controller;

import com.entity.UserCourseOrder;
import com.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 *
 */
@RestController
@RequestMapping("/order")
@CrossOrigin
public class OrderController {

    @Autowired 
    private OrderService orderService;

    @GetMapping("saveOrder/{userid}/{courseid}/{acid}/{stype}")
    public String saveOrder(String orderNo, @PathVariable("userid") String userid, @PathVariable("courseid") String courseid, @PathVariable("acid") String acid, @PathVariable("stype") String stype) {

        //String orderNo = UUID.randomUUID().toString();
        orderService.saveOrder(orderNo, userid, courseid, acid, stype);
        return orderNo;

    }

    @GetMapping("updateOrder/{orderno}//{status}")
    public Integer updateOrder(@PathVariable("orderno") String orderno, @PathVariable("status") Integer status) {

        Integer integer = orderService.updateOrder(orderno, status);
        return integer;

    }

    @GetMapping("deleteOrder/{orderno}")
    public Integer deleteOrder(@PathVariable("orderno") String orderno) {

        Integer integer = orderService.deleteOrder(orderno);
        return integer;

    }

    @GetMapping("getOrdersByUserId/{userId}")
    public List<UserCourseOrder> getOrdersByUserId(@PathVariable("userId") String userId) {

        List<UserCourseOrder> ordersByUserId = orderService.getOrdersByUserId(userId);

        return ordersByUserId;

    }
}
至此,项目初步完成
现在我们继续在entity包下,创建如下的类:
package com.entity;

import lombok.Data;

import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.Date;


@Data
@Table(name = "pay_order") //支付订单信息表
public class PayOrder implements Serializable {
    private static final long serialVersionUID = 777308790778683330L;

    /**
     * 主键
     */
    @Id
    private Long id;
    private String order_no;
    private String user_id;
    private String product_id;
    private String product_name;
    private Double amount;
    private Integer count;
    private String currency;
    private String channel;
    private Integer status;
    private Integer channel_status;
    private Integer order_type;
    private Integer source;
    private String client_ip;
    private String buy_id;
    private String out_trade_no;
    private Date created_time;
    private Date updated_time;
    private Date pay_time;
    private String extra;
    private String goods_order_no;
    private Integer platform;
    private Integer wx_type;
}
package com.entity;

import lombok.Data;

import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.Date;


@Data
@Table(name = "pay_order_record") //支付订单状态日志表
public class PayOrderRecord implements Serializable {
    private static final long serialVersionUID = 777308790778683330L;

    /**
     * 主键
     */
    @Id
    private Long id;
    private String order_no;
    private String type;
    private String from_status;
    private String to_status;
    private Double paid_amount;
    private String remark;
    private String created_by;
    private Date created_at;
    //上面的@Id和@Table实际上并没有具体使用到,所以可以删除,但也不会影响mp,所以不删除也可以
}
然后到mapper包下创建如下接口:
package com.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.entity.PayOrder;




public interface PayOrderDao extends BaseMapper<PayOrder> {
}

package com.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.entity.PayOrderRecord;




public interface PayOrderRecordDao extends BaseMapper<PayOrderRecord> {
}

然后在OrderService接口里添加如下方法:
 /**
     * 保存订单信息
     * @param payOrder
     */
    void saveOrderInfo(PayOrder payOrder);

    /**
     * 保存订单记录信息
     * @param payOrderRecord
     */
    void saveOrderRecord(PayOrderRecord payOrderRecord);
然后对应的实现类里也添加如下:
   @Autowired
    private PayOrderDao payOrderDao;
    @Autowired
    private PayOrderRecordDao payOrderRecordDao;

    @Override
    public void saveOrderInfo(PayOrder payOrder) {
        payOrderDao.insert(payOrder);
    }

    @Override
    public void saveOrderRecord(PayOrderRecord payOrderRecord) {
        payOrderRecordDao.insert(payOrderRecord);
    }
然后在OrderController类里修改如下:
 @GetMapping("saveOrder")
    public String saveOrder(String orderNo,String user_id , String course_id,String activity_course_id,String source_type,Double price) {
        // 保存支付订单
        orderService.saveOrder(orderNo, user_id, course_id, activity_course_id, source_type);
        // 保存订单的记录日志
        PayOrderRecord record = new PayOrderRecord();
        record.setOrder_no(orderNo);
        record.setType("CREATE");
        record.setFrom_status("0");
        record.setTo_status("1");
        record.setPaid_amount(price);
        record.setCreated_by(user_id);
        record.setCreated_at(new Date());
        System.out.println("创建订单记录 = " + orderNo);
        orderService.saveOrderRecord(record);

        return orderNo;
    }

   @GetMapping("updateOrder")
    public Integer updateOrder(HttpServletRequest request,String orderNo , Integer status,String user_id,String course_id,String course_name,Double price,String phone) {
        System.out.println("订单编号 = " + orderNo);
        System.out.println("状态编码 = " + status);
        Integer integer = orderService.updateOrder(orderNo, status);
        System.out.println("订单更新 = " + integer);

        //一般只有受影响,才会返回1,多次操作相同的,则认为不受影响,所以会返回0,或者没有操作(条数)
        if(integer == 1){
            // 记录订单支付的详情
            PayOrder po = new PayOrder();
            po.setOrder_no(orderNo);
            po.setUser_id(user_id);
            po.setProduct_id(course_id);
            po.setProduct_name(course_name);
            po.setAmount(price);
            po.setCount(1);
            po.setCurrency("cny");
            po.setChannel("weChat");
            po.setStatus(2); //2,代表支付成功,这里我们认为是成功的,所以我们就没有设置其他的方式了
            //当然,通常是状态码的问题,在后端可以使得出现对应的状态,使得改变
            //这里,我们没有操作(后端只操作成功的)
            //所以基本到这里来,因为是测试的数据,如果是好的数据,你可以判断改变一下
            //这里就不多说了
            
            
            po.setOrder_type(1);
            po.setSource(3);
            //po.setClient_ip("192.168.1.1"); // 自行解决:获取客户端ip
            //我的解决方式如下:
            String ip = request.getRemoteAddr();
            if("0:0:0:0:0:0:0:1".equals(request.getRemoteAddr())){
                ip = "127.0.0.1";
            }
            po.setClient_ip(ip); 
            //这里只得到ip了,就不得端口了,因为字段的长度有限(sql设置的)
            //注意:如果他的值是0:0:0等等开头的,那么对应通常是操作ipv6的,而不是ipv4
            //其实只要不操作域名,而是直接的输入地址,基本就不会这样的
            //当然,通常是只有localhost这个域名会这样,其他的基本不会
            //所以上面的我就加了判断,使得变成对应的ipv4的地址
            //其实就算是ipv6的,但是也是可以转换成ipv4的,具体看百度操作
            //即方式很多,看你如何操作
            
            po.setCreated_time(new Date());
            po.setUpdated_time(new Date());

            orderService.saveOrderInfo(po);

            // 记录支付操作的日志
            PayOrderRecord record = new PayOrderRecord();
            record.setOrder_no(orderNo);
            record.setType("PAY"); //代表支付
            record.setFrom_status("1"); //从0,变成1
            record.setTo_status("2"); //从1,变成2
            //很明显,保存订单的同时,若我们支付了,那么该订单会出现成功的记录
            record.setPaid_amount(price);
            record.setCreated_by(user_id);
            record.setCreated_at(new Date());
            System.out.println("创建订单记录 = " + orderNo);

            orderService.saveOrderRecord(record);

           
        }
        return integer;
    }
启动该项目,访问localhost:8007/order/saveOrder?orderNo=1&user_id=2&course_id=3&activity_course_id=4&source_type=5&price=6
但是会发现,报错了,没有对应的表,我们可以看看数据库,发现是水平分表的,所以单纯的指定的表是不存在的
看错误就知道了,即出现了Table ‘edu_order.user_course_order’ doesn’t exist的错误,所以的确是表不存在
所以我们需要分库分表,但是为了测试,我们先创建一个user_course_order表,代码如下:
CREATE TABLE `user_course_order` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `order_no` VARCHAR(64) DEFAULT NULL COMMENT '订单号',
  `user_id` INT(11) UNSIGNED NOT NULL DEFAULT '0' COMMENT '用户id',
  `course_id` INT(11) UNSIGNED NOT NULL DEFAULT '0' COMMENT '课程id,根据订单中的课程类型来选择',
  `activity_course_id` INT(11) DEFAULT '0' COMMENT '活动课程id',
  `source_type` TINYINT(5) UNSIGNED NOT NULL DEFAULT '0' COMMENT '订单来源类型: 1 用户下单购买 2 后台添加专栏',
  `status` TINYINT(2) UNSIGNED NOT NULL DEFAULT '0' COMMENT '当前状态: 0已创建 10未支付 20已支付 30已取消 40已过期 ',
  `create_time` DATETIME NOT NULL DEFAULT '1971-01-01 00:00:00' COMMENT '创建时间',
  `update_time` DATETIME NOT NULL DEFAULT '1971-01-01 00:00:00' COMMENT '更新时间',
  `is_del` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0' COMMENT '是否删除',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `uniq_userId_sourceType_refDataId_courseId` (`user_id`,`source_type`,`course_id`) USING BTREE
) ENGINE=INNODB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='用户课程订单表'
然后继续访问localhost:8007/order/saveOrder?orderNo=1&user_id=2&course_id=3&activity_course_id=4&source_type=5&price=6
查看数据表,若出现了数据,代表操作成功
现在我们操作分库分表,在95章博客里就操作过了,所以我们利用95章博客的知识来操作分库分表,首先,先导入具体依赖:
<dependency>
    <groupId>org.apache.shardingspheregroupId>
    <artifactId>sharding-jdbc-spring-boot-starterartifactId>
    <version>4.1.0version>
dependency>
<dependency>
    <groupId>com.alibabagroupId>
    <artifactId>druidartifactId>
    <version>1.2.1version>
dependency>
然后修改配置文件,内容如下:
server:
  port: 8007
spring:
  application:
    name: edu-order-boot
  shardingsphere:
    datasource:
      names: ds0   #配置库的名字,可以随意写,也可以指定多个(可以到95章博客查看如何指定,但这里必须至少指定一个数据库和表,比如这个要加上actualDataNodes并指定数据库和表,否则可能启动不了,与95章的配置不同,这里是必须的,这是spring boot对该分表配置的规定,而95章博客中,甚至可以只有主键生成和分片策略,且在博客里是优先说明的)
      #一般情况下,我们的spring boot单独的操作数据源时(不是分表的配置,分表的可以操作多个数据源),可能操作不了多个数据源
      #这是有原因的,可以百度查看,比如可以到这里查看:https://blog.csdn.net/jinrucsdn/article/details/106539916
      #通过他的介绍,可以得出,spring boot只操作单个数据源,如果需要多个,那么我们需要手动配置(既然要手动配置,自然需要操作设置数据源对应的属性,否则可能启动不了,因为mp需要数据源来操作,所以我们需要手动设置数据源对应的属性),具体配置就可以到66章博客查看,虽然是变量操作,但也可以操作定义多个数据源的信息,实际上是spring boot只会识别一个对应变量,所以导致spring boot基本只能是单数据源
      
      #注意:在某些时候,yml和yaml(与yml基本一样,或者说就是一样,如果有区别,那么也只是某些区别)与properties不一样(大多数情况下他们可以互通,但是有些时候不可以,比如这里)
      #只是大致相同,看下面的配置就知道,有些是不同的,下面的某些属性(或者说大多数,通常是全部)
      #相当于properties的对应属性去掉"_",然后对应"_"后面的字母大写
      #由这样的操作组成的,对比95章博客的内容就知道了
      ds0:
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.164.128:3306/edu_order?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
        username: root
        password: QiDian@666
    sharding:
      tables:
        user_course_order:   # 指定user_course_order表的数据分布情况,配置数据节点
          actualDataNodes: ds0.user_course_order_$->{0..2}
          tableStrategy:
            inline:   # 指定user_course_order表的分片策略,分片策略包括分片键和分片算法
              shardingColumn: id
              algorithmExpression: user_course_order_$->{id % 3}
          keyGenerator:   # 指定user_course_order表的主键生成策略为SNOWFLAKE
            type: SNOWFLAKE  #主键生成策略为SNOWFLAKE
            column: id  #指定主键
  props:
    sql:
      show: true
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka
    register-with-eureka: true
    fetch-registry: true
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
重新启动项目,访问localhost:8007/order/saveOrder?orderNo=1&user_id=2&course_id=3&activity_course_id=4&source_type=5&price=6
查看数据库表内容,若对应的表有数据,代表操作成功(没有保存在创建的那个表里面了)
接下来,我们回到前端的Course.vue组件中,找到如下:
 
        <el-dialog :visible.sync="dialogFormVisible" :before-close="cancelOrder" :modal="true" :close-on-click-modal="false" style="width:800px;margin:0px auto;" >
          <h1 style="font-size:30px;color:#00B38A" >微信扫一扫支付h1>
          <div id="qrcode" style="width:210px;margin:20px auto;">div>
          <h2 id="statusText">h2>
          <p id="closeText">p>
        el-dialog>
我们找到如下:
 // 取消订单(支付前,关闭二维码窗口视为不购买,可取消订单)
    cancelOrder(){
      this.dialogFormVisible= false;

      return this.axios
        .get("http://localhost:8007/order/deleteOrder",{
          params:{
            orderno:this.orderNo,
          }
        })
        .then((result) => {
          console.log("取消订单");
        })
        .catch( (error)=>{
          this.$message.error("取消订单失败!");
      });
    },
那么我们修改项目的OrderController类的如下方法(像这样说明的修改,自然是将对应的修改成如下写的):
@GetMapping("deleteOrder")
    public Integer deleteOrder(String orderno) {

        Integer integer = orderService.deleteOrder(orderno);
        return integer;

    }
然后重启项目,再找到如下:
// 生成二维码
    createCode(){
      // 将上一次的二维码清除,防止出现多个二维码
      document.getElementById("qrcode").innerHTML = "";
      // 去获取支付连接
      this.axios
        .get("http://localhost:8006/pay/createCode",{
          params:{
            courseid: this.course.id,
            coursename: this.course.courseName,
            price:1, //测试支付金额固定为1分钱,真实上线环境再改回此真实价钱:this.course.discounts
          }
        })
        .then((result) => {
          console.log(result);
          // QRCode(存放二维码的dom元素id,二维码的属性参数)
          let qrcode = new QRCode('qrcode',{
            width:200,
            height:200,
            //text:result.data.code_url  // 将返回的数据嵌入到二维码中
            text:"1" //由于对应的数据本来就是错误的,所以是得不到支付链接的,这里我们认为是成功的
              //如果你有正确的数据,那么可以去改变支付微服务的PayConfig类的内容就可以了
          });

          this.orderNo = result.data.orderId;

          // 保存订单, 状态为:已创建 0
          this.saveOrder();

          // 检查支付状态
          this.axios
            .get("http://localhost:8006/pay/checkOrderStatus",{
              params:{
                orderId: result.data.orderId // 传递 订单编号 进行查询
              }
            })
            .then((result) => {
              
           	  if(result.data.trade_state=="SUCCESS"){
                document.getElementById("statusText").innerHTML = " 支付成功!";
                // 支付成功
                this.updateOrder(20);

                // 3秒后关闭二维码窗口
                let s = 3;
                this.closeQRForm(s);
              }
              
            })
            .catch( (error)=>{
              this.$message.error("查询订单失败!");
            });
        })
        .catch( (error)=>{
          this.$message.error("生成二维码失败!");
        });
    },
然后启动支付微服务edu-pay-boot(8006,前面说的先不启动的项目)
再找到如下:
 // 保存订单
    saveOrder(){
      return this.axios
        .get("http://localhost:8007/order/saveOrder",{
          params:{
            orderNo:this.orderNo,
            user_id: this.user.userid,
            course_id:this.course.id,
            activity_course_id:this.course.id,
            source_type:1,
            price:1
          }
        })
        .then((result) => {
          // console.log(result);
          console.log("保存订单");
        })
        .catch( (error)=>{
          this.$message.error("保存订单失败!");
      });
    },
  // 更新订单的状态
    updateOrder(statusCode){
      return this.axios
        .get("http://localhost:8007/order/updateOrder",{
          params:{
            orderNo:this.orderNo,
            status:statusCode,
            user_id:this.user.userid,
            course_id:this.course.id,
            course_name:this.course.name,
            price:1,
            // phone:"17600870878" // 购买成功后短信通知用的手机号
          }
        })
        .then((result) => {
           console.log("更新订单【"+this.orderNo+"】状态:" + statusCode);
        }).catch( (error)=>{
           this.$message.error("更新订单失败!");
        });
    },
如果对应的代码没有问题,那么我们可以进行测试,首先删除所有相关的表,即edu_order数据库里面的表信息都删除
这里给出一个方便的操作,如图所示:

98-微服务项目的编写(下篇)_第4张图片

其中,可以点击字段左边,使得打上勾勾(点击只有一个记录的左边的勾勾也会使得他打上勾勾,因为只有一个相当于是全部了)
然后点击删除图标,那么就是删除当前表的所有数据了
如果只点击一个,或者点击其中的那条记录(默认是点击第一条的,除非你再次的点击,那么就是你点击的地方)某个地方
都可以点击删除图标进行删除该一条数据,但也只是一条
这里要注意:如果有勾勾的情况下(无论点击全部,还是单个记录的勾勾)
点击某个地方(没有勾勾的)是不会使得他删除的(而只删除勾勾的)
没有勾勾(完全没有,一个都没有)的情况下,会删除,即勾勾优先
接着上面要说明的测试,我们测试代码时,会发现,测试不了,问题如下:
第一,保存订单的问题,因为我们是测试,所以可以多次的使得用户购买一个课程,但是由于表的设置是符合实际情况的
即由于对应的是使用的唯一索引(总体,即只要他们都相同,那么添加不了,自然会保存失败)
那么添加(操作)到同一个表时,自然添加不了,即保存失败
具体解决方式:我们只需要多次的删除即可,所以这个问题并不是很严重,因为再实际情况下,基本是不可能多次的购买的
所以通常不会考虑,也基本只有在测试的时候才会出现
第二:对应的支付状态的问题,对应的出现问题的代码如下(在支付微服务的WxPayController类的createCode方法):
  if(map1.get("trade_state").equalsIgnoreCase("SUCCESS")) {
                return map1;
            }
//因为我们是错误的数据,自然map1.get("trade_state")得到的是null,所以很明显,是空指针异常,他也的确如此
所以为了可以使得认为成功,我们改变他的代码,使得他不报错,从而不会执行前端的catch里面的内容(方法)
修改如下:
    //不停的去微信支付平台询问是否支付成功
        while(true) {
            String post = HttpKit.post(url, xml);

            //对微信支付平台返回的查询结果进行处理
            Map<String, String> map1 = WXPayUtil.xmlToMap(post);
return null;
//            if(map1.get("trade_state").equalsIgnoreCase("SUCCESS")) {
//                return map1;
//            }


//            if(System.currentTimeMillis() - l>30*1000){
//                return map1;
//            }
//            Thread.sleep(3000);
        }

返回空数据即可,然后修改前端如下部分:
   .then((result) => {
         
           	  // if(result.data.trade_state=="SUCCESS"){
                document.getElementById("statusText").innerHTML = " 支付成功!";
                // 支付成功
                this.updateOrder(20);

                // 3秒后关闭二维码窗口
                let s = 3;
                this.closeQRForm(s);
              // }
              
            })
认为他支付成功,然后我们再次的点击购买,查看数据库,若有数据了,代表操作完成
当然,如果你是正常的数据,那么如果你支付成功了,自然对应的值会得到(循环的)
然后给前端自然也会成功,如果没有支付,自然他会一直循环的,直到你支付完成,然后then里面的内容(方法)才会执行
之后为了全部优化,最好修改如下:
  <button
            @click="buy(course.id)"
            type="button"
            class="weui-btn purchase-button weui-btn_mini weui-btn_primary"
            style="width:155px;height:45px;font-size:17px;"
          >
            立即购买
            
          button>




    
         // 倒计时关闭二维码窗口
    closeQRForm( s ){
      // alert(that)
      // console.log(this)
       let that = this; //这个必须加上,因为后面使用了that
        //当然后面的使用this也行(需要在普通的js的情况下使用setInterval,setTimeout这是一样的,所以他们赋值给的是Window)
        //这里就不修改了,下面以打印this为例
        //普通的js,外面和里面都是Window,而vue的话,外面是Vue(简称),里面的Window(简称),所以这里需要加上一个变量来使用
        //即let that = this;,当然,并不是非要that,比如a,b等等都可以
        //只要是变量即可(如果改变的话,后面的that记得也要改变哦)

        //因为作用域的原因,所以后面可以clearInterval(a);,通常情况下,没有前缀的,基本都是操作作用域
        //否则会报错,即不存在该变量,即没有定义的错误,比如没有加上let that = this;
        //那么就会报错:that is not defined
        //自然后面的不会执行(报错的话就不会执行,无论你是异步还是同步,都不会执行了,即相当于后面没有代码)
        
        //这里我需要提一下,html中如果有id="a"的值,那么我们直接的输出console.log(a)
        //就相当于输出console.log(document.getElementById("a")),也就是说,会默认将id的值定义一个变量
        //这样需要注意:所以上面的that在这种情况下,可能并不会出现问题,这里只是提一下,注意即可
   
      var a = window.setInterval(function(){
      
        document.getElementById("closeText").innerHTML = "( "+ s-- +" ) 秒后关闭本窗口";
              console.log(s)
                   console.log(this)
       if(s <= 0){
           document.getElementById("closeText").innerHTML = ""
          clearInterval(a); // 停止计时器
          console.log(990)
          that.dialogFormVisible = false; // 二维码窗口隐藏
          that.isBuy = true; // 修改购买状态(已购买)
        }
      }, 1000);
    },
        
        
        //部分修改(记得以后在Index.vue那里也这样的操作,如果需要的话,就操作即可):
          // 购买课程
    buy(courseid) {
    
      if(this.user != null){
        
        //alert("购买第【" + courseid + "】门课程成功,加油!");
        this.dialogFormVisible = true; //显示提示框
document.getElementById("closeText").innerHTML = "" 
          //需要放在后面,否则是没有innerHTML对应的属性的地方的
          //因为我们没有打开,但这里最好不要写,因为有些时候,可能他是先执行,在某个点,所以会出现报错
          //反正上面的closeQRForm已经写了,所以为了防止这种情况这里最好不写,虽然看起来他是后执行
          //但是可能显示提示框中,有上面操作是异步的,所以可能会出现报错吧,自己测试就知道了
          
          
        //但也因为是异步,所以有时候,放在后面可能也会报错,即最好是再循环时,进行赋值,所以这里最好也注释掉
至此微信支付操作完成,但是这里有个问题,购买后,对应的按钮还是立即购买,这个问题在后面解决
因为我们先解决完对应的已购操作
在这之前,我们都认为对应的表是分开的,实际上也是如此,所以可以分析如下
已购操作需要分析,可以有如下方式:
第一种:根据用户id去课程微服务里找到对应的所有已购课程的信息(关联的)
但是我们不操作关联且对应的表是分开的,不在一个数据库,所以这种方式可以去除
第二种:根据用户id去订单微服务里面找到所有的已购课程id,然后根据这些id
去课程微服务里面找到对应的课程信息,但是很明显,至少需要访问两次,所以基本不考虑
第三种:根据用户id去订单微服务里面找到所有的已购课程id,然后订单微服务帮我们访问对应的课程微服务得到其对应的信息
这个可以只需要访问两次就可以了,因为可以操作sql的in操作,所以我们通常考虑这个
那么你可能会有疑惑,第三种不是有耦合了吗,那么这里有个问题,第二种是否有耦合,通常我们可能认为他并没有
但实际上与第三种是一样的有耦合,只是这种耦合在前端进行连接了,所以实际上耦合的程度中,第二种和第三种是差不多的
但是第二种还是要少点,可是在时间上降低了很多效率
且第三种他不需要取出对应的值来访问了,且可以一次访问都获取(因为sql的in),且可以操作Spring Cloud的一些组件
所以各有利弊,但通常我们使用第三种(利大于弊),因为无论在数据量大的情况下
还是数据量少的情况下,第三种都非常好,即最好不要多次的访问(因为循环取出值,或者多次访问需要大量时间)
所以我们最好使用第三种(时间上效率高)
那么微服务之间如何访问呢,我们可以使用89章博客知识中的Feign远程调用组件来操作:
在这之前,我们先回到前端的Index.vue组件中,找到如下(部分代码):
 .then( (result)=>{
        console.log(  result );
        if(result.data.state == 4){
          this.isLogin = true;
          this.setCookie("user",token,600);
          this.user = jwtDecode(token);
          // console.log(  this.user );
          // 获取已购买的课程列表
          this.getMyCourseList(); //这里写上,等我们先编写好对应的方法后,再来操作这个,即先写上
        }
      })
回到课程微服务,找到CourseController类里的getCourseByUserId方法修改成如下:
   @GetMapping("getCoursesByUserId")
    public List<CourseDTO> getCoursesByUserId( Integer userid) {
        System.out.println("userid = " + userid);
        return courseService.getCoursesByUserId(userid);
    }
添加依赖:
<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
然后在启动类上添加如下:
package com.lagou;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableEurekaClient // 注册到中心的客户端
@MapperScan("com.lagou.mapper")  // 扫描mapper包
@EnableFeignClients //这个加上,使得表示启用Fegin客户端功能
public class EduCourseBootApplication {

	public static void main(String[] args) {
		SpringApplication.run(EduCourseBootApplication.class, args);
	}

}

然后创建remote.OrderRemoteService接口:
package com.lagou.remote;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

/**
 *
 */
@FeignClient(name = "edu-order-boot",path = "order") //path:在下面的目录之前,加上一个(该)目录
public interface OrderRemoteService {
    @GetMapping("getOKOrderCourseIds")
    List<Object> getOKOrderCourseIds(@RequestParam(value = "userid") Integer userid );
}
修改CourseService接口及其实现类的对应getCourseByUserId方法:
 List<CourseDTO> getCoursesByUserId(Integer userid);
//之前的getAllCourse方法里面修改部分:
List<Course> initCourse = getInitCourse(null);

// 初始化基本的全部课程
    private List<Course> getInitCourse(List<Object> ids){
        QueryWrapper q = new QueryWrapper();
        q.eq("status", 1);// 已上架
        q.eq("is_del", Boolean.FALSE);// 未删除
       
         if(null != ids ){
            if(0 != ids.size()) {
                     //防止为空的数据(即大小为0,因为在sql中in里面没有值会报错,所以必须是大小不为0的才可进入,且in对应的参数不能是null,因为自动的原因,会执行某些方法,自动的基本都会,所以通常null在当成in参数时会报错,即空指针异常)
            //当然这里也可以写上两个如下:即if(null != ids && 0 == ids.size()){ 
                //和if(null != ids && 0 != ids.size()){ 
                //而之所以在后面,是防止后面执行null时,使得出现空指针异常
                
            
                q.in("id", ids); // where id in(1,2,3),可以是集合参数,点击进去就知道了
            }else{
                q.eq("id",0); //当大小为0时,自然,对应的已购数据需要是没有的
                //所以我们需要使得他返回空的数据,来保证已购操作是没有数据的,而正是因为是自增,所以0代表操作自增,所以基本是不会出现0的(有0的情况下,基本是添加不了自增的)
                //一般来说,数据库返回空数据,在后端,认为是空集合(而不是null)
            }
        }
        q.orderByDesc(" sort_num ");// 排序
        return this.courseMapper.selectList(q);
    }

//因为@FeignClient注解,所以会扫描得到
@Autowired
    private OrderRemoteService orderRemoteService;
    @Override
//修改的方法
    public List<CourseDTO> getCoursesByUserId(Integer userid) {
        // 根据用户id获取已经购买的课程id集合
        List<Object> ids = orderRemoteService.getOKOrderCourseIds(userid);
        System.out.println("ids = " + ids);
        // 根据课程id集合,查询相应的课程信息集合
        return this.getMyCourses(ids);
        
    }

 // 查询已购买的课程
    private List<CourseDTO> getMyCourses(List<Object> ids) {
        //将redis内存中的序列化的集合名称用String重新命名(增加可读性)
        RedisSerializer rs = new StringRedisSerializer();
        redisTemplate.setKeySerializer(rs);

        // 1、先去redis中查询
        System.out.println("***查询redis***");
        // 课程dto集合
        List<CourseDTO> courseDTOS = ( List<CourseDTO>)redisTemplate.opsForValue().get("myCourses");
        // 2、redis中没有,才回去mysql查询
        if(null == courseDTOS){
            synchronized (this) {
                courseDTOS = ( List<CourseDTO>)redisTemplate.opsForValue().get("myCourses");
                if(null == courseDTOS){
                    System.out.println("===查询mysql===");
                    // 查询全部课程
                    List<Course> courses = getInitCourse(ids);
                    courseDTOS = new ArrayList<>();
                    for(Course course : courses){
                        CourseDTO dto = new CourseDTO();
                        // course将属性全部赋给给courseDTO对象
                        BeanUtils.copyProperties(course, dto);
                        courseDTOS.add(dto);
                        // 设置老师
                        setTeacher(dto);
                        // 设置前两节课
                        setTop2Lesson(dto);
                    }
                    redisTemplate.opsForValue().set("myCourses", courseDTOS, 10, TimeUnit.MINUTES);
                }
            }
        }
        return courseDTOS;
    }
至此,获取已购课程的操作完毕,接下来,我们来补全getOKOrderCourseIds方法:
回到订单微服务,在OrderController类里加上如下:
@GetMapping("getOKOrderCourseIds")
    public List<Object> getOKOrderCourseIds(Integer userid){
        List<UserCourseOrder> list = orderService.getOKOrderCourseIds(userid);
        List<Object> ids = new ArrayList<>();
        for(UserCourseOrder order : list){
            ids.add( order.getCourseId() );
        }
        return ids;
    }
然后在OrderService接口及其实现类里加上如下:
List<UserCourseOrder> getOKOrderCourseIds(Integer userId);
@Override
    public List<UserCourseOrder> getOKOrderCourseIds(Integer userId) {
        QueryWrapper q = new QueryWrapper();
        q.eq("status", 20); //购买成功
        q.eq("is_del", Boolean.FALSE); // 未删除
        q.eq("user_id", userId); // 该用户
        return orderDao.selectList(q);
    }
现在我们将课程微服务和订单微服务都重启,然后访问localhost:8007/order/getOKOrderCourseIds?userid=100030011
你可以适当的添加订单,如果添加再次访问后,得到了数据,代表操作成功
那么这样就说明订单微服务的改动没有问题,现在,我们来测试课程微服务的改动是否有问题
我们直接访问localhost:8004/course/getCoursesByUserId?userid=100030011
只要返回了数据(空数据也算数据,只是没有关联而已,之前没有说明,这里说明一下),就代表操作成功
至此课程微服务的改动也没有问题
现在我们回到前端,找到Index.vue组件,找到如下:
 .then( (result)=>{
        console.log(  result );
        if(result.data.state == 4){
          this.isLogin = true;
          this.setCookie("user",token,600);
          this.user = jwtDecode(token);
          // console.log(  this.user );
          // 获取已购买的课程列表
          this.getMyCourseList();
        }
      })
对应的方法如下:
  // 获取已购的课程列表
    getMyCourseList(){
      return this.axios
      .get("http://localhost:8004/course/getCoursesByUserId",{
          params:{
            userid:this.user.userid
          }
      }).then((result) => {
          console.log(result);
          this.myCourseList = result.data;
      }).catch( (error)=>{
          this.$message.error("获取已购买的课程信息失败!");
      });
    },
很明显,他的确是查询我们用户已购买的信息,前端的对应的已购,自然使用的是this.myCourseList
而普通的全部是使用的this.courseList,这是不同的,我们可以添加一条记录,来测试
若发现,出现了对应的已购课程,那么操作成功
接下来,我们操作前端中,立即购买的地方,首先再data里加上如下:
//记得引入如下
import QRCode from 'qrcodejs2'; // 引入qrcodejs  


data() {
    return {
      activeName: "allLesson",
      courseList:[],  // 课程集合
      myCourseList:[], // 我购买过的课程列表
      isLogin:false, //登录状态
      user:null, // 已登录的用户对象信息
      adList:null, //广告集合
        //下面的基本都加上
      course:null, //这里也加上
      dialogFormVisible:false, //默认false:隐藏,true:显示
    };
  },
对应的前端中,有如下:
 
          <el-dialog :visible.sync="dialogFormVisible" :before-close="cancelOrder" :modal="true" :close-on-click-modal="false" style="width:800px;margin:0px auto;" >
            <h1 style="font-size:30px;color:#00B38A" >微信扫一扫支付h1>
            <div id="qrcode" style="width:210px;margin:20px auto;">div>
            <h2 id="statusText">h2>
            <p id="closeText">p>
          el-dialog>


添加如下方法:
  // 购买课程
    buy(course) {

this.course = course
      if(this.user != null){
        // document.getElementById("closeText").innerHTML = ""
        //alert("购买第【" + courseid + "】门课程成功,加油!");
   
        this.dialogFormVisible = true; //显示提示框
// document.getElementById("closeText").innerHTML = ""
// alert(1)
        // 待dom更新之后再用二维码渲染其内容
        this.$nextTick(function(){
            this.createCode(); // 直接调用会报错:TypeError: Cannot read property 'appendChild' of null
        });
      }else{
        this.$message.error("请先登录,再来购买课程!");
      }
      
      
    },
      // 生成二维码
    createCode(){
      // 将上一次的二维码清除,防止出现多个二维码
      document.getElementById("qrcode").innerHTML = "";
      // 去获取支付连接
      this.axios
        .get("http://localhost:8006/pay/createCode",{
          params:{
            courseid: this.course.id,
            coursename: this.course.courseName,
            price:1, //测试支付金额固定为1分钱,真实上线环境再改回此真实价钱:this.course.discounts
          }
        })
        .then((result) => {
  
          console.log(8787)
          console.log(result);

          // QRCode(存放二维码的dom元素id,二维码的属性参数)
          let qrcode = new QRCode('qrcode',{
            width:200,
            height:200,
            text:"1"  // 将返回的数据嵌入到二维码中
          });

          this.orderNo = result.data.orderId;

          // 保存订单, 状态为:已创建 0
          this.saveOrder();

          // 检查支付状态
          this.axios
            .get("http://localhost:8006/pay/checkOrderStatus",{
              params:{
                orderId: result.data.orderId // 传递 订单编号 进行查询
              }
            })
            .then((result) => {

           	   //if(result.data.trade_state=="SUCCESS"){
                document.getElementById("statusText").innerHTML = " 支付成功!";
                // 支付成功
                this.updateOrder(20);

                // 3秒后关闭二维码窗口
                let s = 3;
                this.closeQRForm(s);
              // }
              
            })
            .catch( (error)=>{
              this.$message.error("查询订单失败!");
            });
        })
        .catch( (error)=>{
          this.$message.error("生成二维码失败!");
        });
    },
    // 倒计时关闭二维码窗口
    closeQRForm( s ){
      // alert(that)
      // console.log(this)
       let that = this;
var b = "1";
console.log(77)
console.log(this.b)
      var a = setInterval(function(){
      
        document.getElementById("closeText").innerHTML = "( "+ s-- +" ) 秒后关闭本窗口";
              console.log(s)
                   console.log(this)
       if(s <= 0){

       document.getElementById("closeText").innerHTML = ""
          clearInterval(a); // 停止计时器
          console.log(990)
          that.dialogFormVisible = false; // 二维码窗口隐藏
      
          that.isBuy = true; // 修改购买状态(已购买)
        }
      }, 1000);
    },
    // 保存订单
    saveOrder(){
   
      return this.axios
        .get("http://localhost:8007/order/saveOrder",{
          params:{
            orderNo:this.orderNo,
            user_id: this.user.userid,
            course_id:this.course.id,
            activity_course_id:this.course.id,
            source_type:1,
            price:1
          }
        })
        .then((result) => {
          // console.log(result);
          console.log("保存订单");
        })
        .catch( (error)=>{
          this.$message.error("保存订单失败!");
      });
    },
    // 更新订单的状态
    updateOrder(statusCode){
      return this.axios
        .get("http://localhost:8007/order/updateOrder",{
          params:{
            orderNo:this.orderNo,
            status:statusCode,
            user_id:this.user.userid,
            course_id:this.course.id,
            course_name:this.course.name,
            price:1,
            // phone:"17600870878" // 购买成功后短信通知用的手机号
          }
        })
        .then((result) => {
           console.log("更新订单【"+this.orderNo+"】状态:" + statusCode);
        }).catch( (error)=>{
           this.$message.error("更新订单失败!");
        });
    },
    // 取消订单(支付前,关闭二维码窗口视为不购买,可取消订单)
    cancelOrder(){
      this.dialogFormVisible= false;

      return this.axios
        .get("http://localhost:8007/order/deleteOrder",{
          params:{
            orderno:this.orderNo,
          }
        })
        .then((result) => {
          console.log("取消订单");
        })
        .catch( (error)=>{
          this.$message.error("取消订单失败!");
      });
    },
然后点击对应的立即购买,如果出现二维码,并自动的操作了,说明我们操作成功,但是现在有个问题
立即购买这个字需要改变吗,或者说,如果要改变,我们怎么改变,这里,我们最好进行改变,主要是为了更好的观察
修改前端代码:
   <div class="btn btn-green btn-offset" @click="buy(item)">立即购买div>
                 
在操作后面之前,我们首先需要修改订单微服务的保存订单的OrderController类里面的updateOrder方法(他使得认为已支付)
为什么这里要修改呢:
很明显,保存订单中,只要我们已经支付了,肯定会使得获取的已购课程进行改变,可是,对应的已购是操作缓存的
在前面我们说过,写的操作最好需要删除对应的缓存,所以我们需要删除redis的缓存(他是内容,通常我们认为是程序的缓存)
修改如下:
在订单微服务里添加如下依赖:
	<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-data-redisartifactId>
		dependency>
//部分代码如下:

@Autowired
    private RedisTemplate<Object,Object> redisTemplate;

 @GetMapping("updateOrder")
    public Integer updateOrder(HttpServletRequest request,String orderNo , Integer status,String user_id,String course_id,String course_name,Double price,String phone) {
        System.out.println("订单编号 = " + orderNo);
        System.out.println("状态编码 = " + status);
        Integer integer = orderService.updateOrder(orderNo, status);
        System.out.println("订单更新 = " + integer);
        RedisSerializer rs = new StringRedisSerializer();
        redisTemplate.setKeySerializer(rs);
        redisTemplate.delete("myCourses");

        if(integer == 1){
            // 记录订单支付的详情
对应的配置记得加上(按照自己的地址来操作):
spring:
  redis:
    host: 192.168.164.128
    port: 6379
重新启动订单微服务项目,然后在前端(Index.vue组件)修改如下方法:
 // 倒计时关闭二维码窗口
    closeQRForm( s ){
      // alert(that)
      // console.log(this)
       let that = this;
var b = "1";
console.log(77)
console.log(this.b)
      var a = setInterval(function(){
      
        document.getElementById("closeText").innerHTML = "( "+ s-- +" ) 秒后关闭本窗口";
              console.log(s)
                   console.log(this)
       if(s <= 0){

       document.getElementById("closeText").innerHTML = ""
          clearInterval(a); // 停止计时器
          console.log(990)
          that.dialogFormVisible = false; // 二维码窗口隐藏
      
                  that.$router.go(0); //这里我们直接的刷新,使得获取已购课程信息
        }
      }, 1000);
    },
        
//然后操作修改如下(因为这里是点击的,所以通常是可以得到数据的,即数据通常是完整,而不会因为异步操作先操作了):
        // 购买课程
    buy(course) {
    console.log(course)
    console.log(this.myCourseList.length)
     console.log(this.myCourseList)
      var b = true;
      if(this.myCourseList.length!=0){
for(var a =0;a<this.myCourseList.length;a++){
if(course.id == this.myCourseList[a].id){
b = false
}
}
      }
        
        //只要我们有已购课程,并且,当前的课程是已购,就不会操作订单的支付操作
if(b == true){

this.course = course
      if(this.user != null){
        // document.getElementById("closeText").innerHTML = ""
        //alert("购买第【" + courseid + "】门课程成功,加油!");
   
        this.dialogFormVisible = true; //显示提示框
// document.getElementById("closeText").innerHTML = ""
// alert(1)

        // 待dom更新之后再用二维码渲染其内容
        this.$nextTick(function(){
  this.createCode(); // 直接调用会报错:TypeError: Cannot read property 'appendChild' of null
        });
      }else{
        this.$message.error("请先登录,再来购买课程!");
      }
}else{
  this.$message.error("课程已经购买了!");
}
      
      
    },
然后看前端显示,若的确是符合要求的,那么操作成功,接下来我们来改变具体的显示内容
首先,我们来分析一下,我们在改变时,肯定是改变某个部分,而不是全部,那么自然需要有具体的课程的唯一
所以在正常情况下(没有随意的添加标签),可以这样修改:
<div :id="item.id" class="btn btn-green btn-offset" @click="buy(item)">立即购买div>

那么就可以这样修改:
   // 获取已购的课程列表
    getMyCourseList(){
      return this.axios
      .get("http://localhost:8004/course/getCoursesByUserId",{
          params:{
            userid:this.user.userid
          }
      }).then((result) => {
        console.log(5555)
          console.log(result);
          this.myCourseList = result.data;
          //由于异步的问题,这里我们等待一会执行,当然
          //若你解决了this.axios的异步(可以百度查看,就与jq的解决异步类似)
          //但是,我们也最好不要同步,因为在大多数的情况下,都会有延迟
          //而同步会出现更多的延迟,所以最好不要同步
          //其实只要我们获得的数据的时间与这里的执行延迟时间来比较修改即可(一般执行延迟时间要低于获取数据的时间,才基本不会出现得不到数据)
          //这样既可以节省时间,也不会出现数据得不到的情况

          //如果后端返回空集合(不是null),那么这里也算空集合(也不是null)
          var that = this
          var b = null;
          setTimeout(function (){              
      for(var a =0;a<that.myCourseList.length;a++){
        b = document.getElementById(that.myCourseList[a].id)
        b.setAttribute("class","btn btn-yellow btn-offset");
        b.innerHTML = '好好学习'
        
          }
   }, 1000);
       
  
      }).catch( (error)=>{
          this.$message.error("获取已购买的课程信息失败!");
      });
    },
至此,我们修改了显示内容,我们继续分析,实际上更新操作,可能也会出现问题,为什么,我们看代码:
 // 保存订单, 状态为:已创建 0
          this.saveOrder();

          // 检查支付状态
          this.axios
            .get("http://localhost:8006/pay/checkOrderStatus",{
              params:{
                orderId: result.data.orderId // 传递 订单编号 进行查询
              }
            })
            .then((result) => {

           	   //if(result.data.trade_state=="SUCCESS"){
                document.getElementById("statusText").innerHTML = " 支付成功!";
                // 支付成功
                this.updateOrder(20);
console.log(90)
                // 3秒后关闭二维码窗口
                let s = 3;
                this.closeQRForm(s);
              // }
              
            })
我们发现,由于异步的关系且我们没有判断,所以通常更新操作是可能会先操作的(对保存来说),虽然这种情况很小
而后面的关闭二维码窗口因为有时间,所以更新操作基本会先操作完毕(对更新来说),即我们需要这样的修改:
  .then((result) => {

           	   //if(result.data.trade_state=="SUCCESS"){
                document.getElementById("statusText").innerHTML = " 支付成功!";
               let that = this
                setTimeout(function (){              
      // 支付成功
                that.updateOrder(20);
console.log(90)
                // 3秒后关闭二维码窗口
                let s = 3;
                that.closeQRForm(s);
              // }
   }, 500);
              
              
            })
500毫秒通常够了,至此,我们也基本解决了这个问题,但是,在实际情况中,我们并不建议这样,因为500是固定的
当然,实际情况,也基本是有判断的(因为后端循环,所以也通常不会这样操作)
我们再次的分析,由于我们点击取消,即框框的叉叉(x),可以取消订单
那么这里就有个问题,如果支付完成后,点击取消订单,那么订单删除,又因为查询已购是查询订单的
那么会认为我们没有购买,所以相当于我们使用了小钱钱,但是,我们却没有购买,所以这是很严重的现象,那么如何解决呢
第一种:没有取消订单的操作,但是这样,并不友好,因为,如果没有取消订单
对于这里的数据库来说,他是不能出现相同的订单的,所以必须取消,否则可能添加不了订单了
第二种:在适当的时间,使得取消订单,不会操作,很明显,我们需要这一种,而适当的时间
自然是当我们会更新或者认为我们已经支付了的时候,所以代码修改如下:
  data() {
    return {
      activeName: "allLesson",
      courseList:[],  // 课程集合
      myCourseList:[], // 我购买过的课程列表
      isLogin:false, //登录状态
      user:null, // 已登录的用户对象信息
      adList:null, //广告集合
      course:null,
      dialogFormVisible:false, //默认false:隐藏,true:显示
      isBuy:false, //是否购买了
    };
  },
   document.getElementById("statusText").innerHTML = " 支付成功!";
               this.isBuy = true //到这里,自然是认为支付的了
               let that = this
                setTimeout(function (){              
      // 支付成功
                that.updateOrder(20);
console.log(90)
                // 3秒后关闭二维码窗口
 // 取消订单(支付前,关闭二维码窗口视为不购买,可取消订单)
    cancelOrder(){
      if(this.isBuy == false){
      this.dialogFormVisible= false;

      return this.axios
        .get("http://localhost:8007/order/deleteOrder",{
          params:{
            orderno:this.orderNo,
          }
        })
        .then((result) => {
          console.log("取消订单");
        })
        .catch( (error)=>{
          this.$message.error("取消订单失败!");
      });
      }else{
        this.$message.error("已经购买完毕,订单不能取消了,因为需要操作已购");
      }
    },
现在我们再次的进行测试,发现,问题解决,而如果我们取消了,那么自然在后端,返回的结果是不受影响的
所以也就没有后续的操作,虽然会打印信息
至此,分析完毕,但是这是Index.vue组件里面的,我们到Course.vue组件进行改变
因为他也有立即购买的显示以及这里的某些问题,所以我们也需要改变他的代码:
     if(result.data.state == 4){
          
          this.isLogin = true;
          this.setCookie("user",token,600);
          this.user = jwtDecode(token);
          // console.log(  this.user );
           this.getMyCourseList(); //这里加上
          this.userid = this.user.userid;
          
        }
我们可以找到他这个方法:
 // 查询当前用户购买过的全部课程
    getMyCourseList(){ 
      return this.axios
       .get("http://localhost:8004/course/getCoursesByUserId",{
          params:{
            userid:this.user.userid
          }
       })
      .then((result) => {
        console.log(result);
        this.myCourseList = result.data;

        // 检测当前的课程是否购买过
        for(let i = 0; i<this.myCourseList.length ; i++){
          if( this.myCourseList[i].id == this.course.id ){
            this.isBuy = true; // 标记购买过本课程
            break;
          }
        }

      }).catch( (error)=>{
        this.$message.error("获取课程信息失败!");
      } );
    },
很明显,他使用this.isBuy来保证是否购买,所以我们到对于的如下代码进行修改:
  <div v-if="isBuy == false">立即购买div>
             <div v-if="isBuy == true">已购买div>
 // 购买课程
    buy(courseid) {
    if(this.isBuy == false){
      if(this.user != null){
        // document.getElementById("closeText").innerHTML = ""
        //alert("购买第【" + courseid + "】门课程成功,加油!");
        this.dialogFormVisible = true; //显示提示框
// document.getElementById("closeText").innerHTML = ""
// alert(1)
        // 待dom更新之后再用二维码渲染其内容
        this.$nextTick(function(){
            this.createCode(); // 直接调用会报错:TypeError: Cannot read property 'appendChild' of null
        });
      }else{
        this.$message.error("请先登录,再来购买课程!");
      }
    }else{
      this.$message.error("已经购买过了!");
    }
      
      
    },
至此,我们可以看下前端页面,若改变了,代表操作成功,接下来,我们来解决取消订单问题:
 data() {
    return {
      activeName: "intro",
      course:null,
      totalLessons:0, // 本门课程的总节数
      commentList:null,  // 所有留言
      isLogin:false, // false 未登录
      isBuy:false, // false 未购买
      user:null, // 当前用户
      userid:666666,
      myCourseList:[], // 当前用户购买过的所有课程
      comment:null, // 待发表的留言内容
      dialogFormVisible:false, //默认false:隐藏,true:显示
      time:null,// 计时对象
      orderNo:"",// 订单编号
      isOrder:false //这里添加上
    };
     
     //修改如下:
       .then((result) => {

           	   //if(result.data.trade_state=="SUCCESS"){
                document.getElementById("statusText").innerHTML = " 支付成功!";
           
   this.isOrder = true
               let that = this
                setTimeout(function (){              
      // 支付成功
                that.updateOrder(20);
console.log(90)
                // 3秒后关闭二维码窗口
                let s = 3;
                that.closeQRForm(s);
              // }
   }, 500);

              // }
              
            })
     
     
     //再修改如下:
       // 取消订单(支付前,关闭二维码窗口视为不购买,可取消订单)
    cancelOrder(){
      if(this.isOrder == false){
      this.dialogFormVisible= false;

      return this.axios
        .get("http://localhost:8007/order/deleteOrder",{
          params:{
            orderno:this.orderNo,
          }
        })
        .then((result) => {
          console.log("取消订单");
        })
        .catch( (error)=>{
          this.$message.error("取消订单失败!");
      });
      }else{
        this.$message.error("已经购买完毕,订单不能取消了,因为需要操作已购");
      }
    },
     
至此,取消订单问题解决了,顺便也解决了部分异步问题(500毫秒即可)
到这里,基本上没有什么问题了,现在,我们来添加一个功能:
当我们购买后,需要一个短信的通知,即订单支付成功后,进行短信通知
那么如果你的通知有很多个,那么最好使用消息队列,看如图:

98-微服务项目的编写(下篇)_第5张图片

所以如果按照传统的方式,必须都需要执行完,才可返回,而使用消息队列,那么我们只需要写入消息即可
虽然他并不是在返回数据的时候,才发送,但是通知,是可以延迟的或者不通知的
这对用户的体验影响并不大,所以这里就使用消息队列
接下来我们需要mq的知识,如果你没有学习过,那么可以看81章博客进行学习
这里就使用该博客说明的RabbitMQ了(使用了他里面操作的用户)
然后我们启动RabbitMQ,创建虚拟主机lagou,再创建他的消息队列order_queue
然后添加对应的依赖(在订单微服务里面):
<dependency>
    
	<groupId>org.springframework.bootgroupId>
	<artifactId>spring-boot-starter-amqpartifactId>
dependency>
然后在配置文件里添加如下:
spring:
  rabbitmq:
    host: 192.168.164.128
    port: 5672
    username: laosun
    password: 123123
    virtual-host: lagou
    queue: order_queue
为了进行测试,我们到OrderController类里加上如下内容:
  @Autowired
    private RabbitTemplate rabbitTemplate;

    //导入import org.springframework.beans.factory.annotation.Value;这个
    @Value("${spring.rabbitmq.queue}")
    private String queue;
    // mq的生产端
    @GetMapping("sendMQ")
    public void sendMQ(){
        String msg = "你好啊,mq!";
        rabbitTemplate.convertAndSend(queue,msg);
    }
然后重启项目,访问http://localhost:8007/order/sendMQ,查看消息队列,若出现了消息,代表操作成功
现在,我们创建rabbit.OrderRever类:
package com.lagou.rabbit;

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired; //这里是测试的时候造成的,虽然这里没有使用到,大多数的情况下,出现这样的情况,基本都是我自己测试时或者复制代码后,删除造成的,写上也没有什么影响,只是代码多点(多操作而已,在大型项目中,最好要避免这种情况,因为或多或少可能在部署时或者运行时慢点,因为占用了资源,虽然很少,但也占用了(通常也并不重要,但若造成服务器缓慢,即运行时慢点,也会影响用户,所以最好删除无关的导入)
import org.springframework.stereotype.Component;

/**
 *
 */
@Component
public class OrderRever {

    @RabbitListener( queues = "${spring.rabbitmq.queue}") //监视队列
    public void process(String msg){ //msg是得到的消息
        System.out.println("得到通知,开始发送 = " + msg);


    }
}
再次的重新启动,访问http://localhost:8007/order/sendMQ,看看打印信息,若出现了打印信息,则代表操作成功
然后加上这个依赖:
   <dependency>
          
            <groupId>com.alibabagroupId>
            <artifactId>fastjsonartifactId>
            <version>1.2.47version>
        dependency>
        <dependency>
            
            <groupId>com.aliyungroupId>
            <artifactId>aliyun-java-sdk-coreartifactId>
            <version>4.5.3version>
        dependency>
然后再配置文件里加上:
spring:
  application:
    name: edu-order-boot
ali:
  sms:
    signName: 大佬孙
    templateCode: SMS_177536068
    assessKeyId: LTAI4FwKDkeZ6StZvRxg5RDf
    assessKeySecret: 09IMDRUia2uIC7HMXpSmM5CiXuUgvf
现在,我们创建sms.SmsService类来完成我们的短信通知:
package com.lagou.sms;

import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 *
 */
@Component
public class SmsService {

    @Value("${ali.sms.signName}")
    private String signName;
    @Value("${ali.sms.templateCode}")
    private String templateCode;
    @Value("${ali.sms.assessKeyId}")
    private String accessKeyId;
    @Value("${ali.sms.assessKeySecret}")
    private String assessKeySecret;

    public Object sendSms(String phoneNumber,String courseName) {
        DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, assessKeySecret);
        IAcsClient client = new DefaultAcsClient(profile);

        CommonRequest request = new CommonRequest();
        request.setSysMethod(MethodType.POST);
        request.setSysDomain("dysmsapi.aliyuncs.com");
        request.setSysVersion("2017-05-25");
        request.setSysAction("SendSms");
        request.putQueryParameter("RegionId", "cn-hangzhou");
        request.putQueryParameter("PhoneNumbers", phoneNumber);
        request.putQueryParameter("SignName", signName);
        request.putQueryParameter("TemplateCode", templateCode);

        request.putQueryParameter("TemplateParam", "{\"phone\":\"" + phoneNumber + "\",\"courseName\":\""+courseName+"\"}");
        try {
            CommonResponse response = client.getCommonResponse(request);
            String jsonStr = response.getData();
            System.out.println("jsonStr = " + jsonStr);

        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

}
再在entity包下创建SmsVo类:
package com.lagou.entity;

import lombok.Data;

/**
 *
 */
@Data
public class SmsVo implements Serializable{ //需要实现序列化接口,来操作消息队列,否则会报错
    //即后面的rabbitTemplate.convertAndSend(queue,smsVo);这里报错
    private String phone;
    private String courseName;
}

然后我们修改OrderController类的updateOrder方法(部分添加):
 System.out.println("创建订单记录 = " + orderNo);

            orderService.saveOrderRecord(record);

            // 发送短信成功的通知
            SmsVo smsVo = new SmsVo();
            smsVo.setPhone(phone);// 手机号码
            smsVo.setCourseName(course_name);
            rabbitTemplate.convertAndSend(queue,smsVo);
然后改变OrderRever类:
package com.lagou.rabbit;

import com.lagou.entity.SmsVo;
import com.lagou.sms.SmsService;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 *
 */
@Component
public class OrderRever {
    @Autowired
    private SmsService smsService;
    @RabbitListener( queues = "${spring.rabbitmq.queue}") //监视队列
    public void process(SmsVo smsVo){
        System.out.println("得到通知,开始发送 = " + smsVo.getCourseName());

        // 调用发送短信
        smsService.sendSms(smsVo.getPhone(), smsVo.getCourseName());
    }
}
然后修改这里(Index.vue和Course.vue都进行修改):
 .get("http://localhost:8007/order/updateOrder",{
          params:{
            orderNo:this.orderNo,
            status:statusCode,
            user_id:this.user.userid,
            course_id:this.course.id,
            course_name:this.course.courseName, //这里记得要改变
            price:1,
            phone:"17600870878" // 购买成功后短信通知用的手机号,这里解除注释或者删除掉
          }
        })
至此,操作完成,重启项目进行测试,看打印信息即可(这里如果显示格式不对或者没有匹配,前面说明过的错误)
则代表操作完成,因为的确是发送了
改造播放组件:
前端中,我们使用的是html的
只能播放而已,无法保证视频资源的安全性,容易被爬取,总不能我是收费的,但是你投机取巧使得得到免费的了吧
所以我们采用阿里云视频点播方案
阿里官网:https://www.alibabacloud.com/help/zh/doc-detail/51236.htm?spm=a2c63.p38356.b99.2.28213799QTbeE3
高端的额外服务是按照流量收取费用的,且需要某些参数(相当于微信支付需要某些参数,比如需要公司注册等等)
所以这里只是给出部分的演示,具体到工作中,可以去操作
现在改造前端项目,加入阿里播放组件
在vue项目的public目录中的index.html文件中引入css和js
<link rel="stylesheet" href="https://g.alicdn.com/de/prismplayer/2.8.2/skins/default/aliplayer-min.css" />

<script charset="utf-8" type="text/javascript" src="https://g.alicdn.com/de/prismplayer/2.8.2/aliplayer-min.js">script>
在videoDetail.vue组件中引入如下:
<div class="prism-player" id="J_prismPlayer" >div>


  <div 
                class="video" id="player-box">
                
                <div class="prism-player" id="J_prismPlayer" style="height:850px;min-width:720px; max-height:940px;overflow-x: hidden;overflow-y: hidden;">div>
                
              div>
然后添加如下:
 var player = new Aliplayer({
  id: 'J_prismPlayer',
  width: '100%',
  height: '900px',
  autoplay: true,
  //支持播放地址播放,此播放优先级最高
  source : 'https://video.pearvideo.com/mp4/short/20200914/cont-1697119-15382138-hd.mp4',
});
 
 //比如放在这里
 mounted() {

    this.myvideo = new Aliplayer({ //修改一下变量名称
      id: 'J_prismPlayer',
      width: '100%',
      height: '880px',
      autoplay: true,
      //支持播放地址播放,此播放优先级最高
      source : 'https://video.pearvideo.com/mp4/short/20200914/cont-1697119-15382138-hd.mp4',
    });
  },
为了解决登录的问题(videoDetail.vue组件得不到登录是否成功的信息),修改如下:
  created() { 
    // 判断登录(暂无)
  // 从url中获取token参数
    let token  = this.getValueByUrlParams('token');

    if(token == null || token == ""){
      // 从cookie中获取user的token
      token = this.getCookie("user");
    }


    this.token = token;
   
    console.log("刷新页面token=>"+token);
    if(token != null || token != ""){
      // 将token发送到sso进行校验
         
      this.axios
      .get("http://localhost:80/user/checkToken",{
        params:{
          token:token
        }
      })
      .then( (result)=>{
        console.log(  result );
    console.log(666)
        if(result.data.state == 4){
          
          this.isLogin = true; //主要是这个
          this.setCookie("user",token,600);
          this.user = jwtDecode(token); //如果不需要信息,可以删除
         
          
        }
      })
      .catch( (error)=>{
       });
    }
 

    // 从上一级页面的请求中获得课程对象和小节编号
    this.course = this.$route.params.course;
    this.lessonid = this.$route.params.lessonid;
    this.isBuy = this.$route.params.isBuy;
    
  },
然后添加如下方法(vue添加方法时,基本都是在methods里面添加方法):
 getValueByUrlParams(paramKey) {
      // http://localhost:8080/#/?token=1&id=2
      var url = location.href;
      var paraString = url.substring(url.indexOf("?") + 1, url.length).split("&");
      var paraObj = {}
      var i, j
      for (i = 0; j = paraString[i]; i++) {
          paraObj[j.substring(0, j.indexOf("=")).toLowerCase()] = j.substring(j.indexOf("=") + 1, j.length);
      }
      var returnValue = paraObj[paramKey.toLowerCase()];
      if (typeof(returnValue) == "undefined") {
          return "";
      } else {
          return returnValue;
      }
    },
           //从cookie中获取token
    getCookie(key){
      var name = key + "=";
      if(document.cookie.indexOf(';') > 0){
        var ca = document.cookie.split(';');
        for(var i=0; i<ca.length; i++) {
            var c = ca[i].trim();
            if (c.indexOf(name)==0) { 
              return c.substring(name.length,c.length); 
            }
        }
      }else{
  
          var ca = document.cookie
         //console.log(ca)
           if (ca.indexOf(name)==0) { 
             console.log(9)
             console.log(ca.substring(name.length,ca.length))
              return ca.substring(name.length,ca.length); 
            }
      }
      // return "";
    },
        
         //设置cookie
    setCookie(key,value,expires){
      var exp = new Date();
      exp.setTime(exp.getTime() + expires*1000);
      document.cookie = key + "=" + escape (value) + ";expires=" + exp.toGMTString();
    },
然后导入如下:
<script>
import jwtDecode from 'jwt-decode'; //这个需要导入
export default {
  name: "videoDetail",
至此登录问题解决,但是他总是一开始给出固定的地址
即https://video.pearvideo.com/mp4/short/20200914/cont-1697119-15382138-hd.mp4
然后查看如下:
  playNow(lesson){

      this.lessonid = lesson.id; // 当前播放的视频,就是我点击的课
      //this.myvideo.source = lesson.courseMedia.fileEdk; // 切换播放器的播放地址

      // 防止视频叠加,每次播放之前,应该先将原来的播放器对象删除,重新创建播放器的容器div
      document.getElementById("J_prismPlayer").remove(); 
      //单纯的remove基本没有操作,即需要remove
      //删除本身的标签(可以认为是dom,通常说成dom节点),可以自己测试

      var pdiv = document.createElement("div");  
      pdiv.setAttribute("class","prism-player");
      pdiv.setAttribute("id","J_prismPlayer");

      document.getElementById("player-box").appendChild(pdiv);

      this.myvideo = new Aliplayer({
        id: 'J_prismPlayer',
        width: '100%',
        height: '900px',
        autoplay: true,
        //支持播放地址播放,此播放优先级最高,修改这个地址,再次的操作通常并不能完成切换,即在后面执行中
        source : lesson.courseMedia.fileEdk,
      });
      
    },
        
            // 初始化时播放的视频
    initplay(){

      //1.在课程信息中查找即将播放的小节视频的编号
      for( let i = 0 ; i< this.course.courseSections.length;i++ ){
          let section = this.course.courseSections[i];
          for(let j = 0; j<section.courseLessons.length ; j++){
              let lesson = section.courseLessons[j]; // 每节课
              if(lesson.courseMedia!=null){
                if(this.lessonid == lesson.courseMedia.lessonId){
                  console.log("视频地址:" + lesson.courseMedia.fileEdk);
                  this.lessonName = lesson.theme;
                  //2.将小节视频的地址 赋值 给播放器,进行播放,这里直接给出课时信息即可,即执行方法即可
                 playNow(lesson);
                  return;
                }
              }
          }
      }
    }


//这里添加上:
  // 从上一级页面的请求中获得课程对象和小节编号
    this.course = this.$route.params.course;
    this.lessonid = this.$route.params.lessonid;
    this.isBuy = this.$route.params.isBuy;
    this.initplay(); //这里添加上
接下来有个问题,那么视频怎么存放呢,在小视频的情况下可以使用fastdfs
虽然他没有合并的操作(即并不是很适合操作大的视频),但这里,我们使用fastdfs也并不是很友好
因为他传递过来的链接容易被盗,即安全性不是很高
使得其他人可以直接的访问我们的服务器的视频(知道地址的情况下,甚至该链接包含了地址),而不用付费了
说到安全,这里就需要提一下,一般第三方公开的框架,也最好不要使用,因为如果发现漏洞
自然会对公司造成大的影响,所以一般大公司都是自研框架的,中小型公司使用的多点第三方公开的框架
第三方框架:简称其他不是自己的框架
大多数时候将第三方认为是其他人,也通常是这样认为,所以也基本没有什么第一方和第二方
虽然我们可以解决fastdfs的安全性问题,但是耗时耗力,难度通常比较大,具体可以百度
那么除了fastdfs,我们还能使用什么来解决文件的存储且能够保证安全呢:
阿里云OSS:
我们要做文件服务,阿里云oss是一个很好的分布式文件服务系统,所以我们只需要集成阿里云oss即可
但这里也要提一下,基本上,没有绝对的安全,比如说,如果我们不能直接的盗取视频
但是确可以录视频,就算有防止录的操作,比如不能执行某些录频软件,或者过一段时间在视频上滑过学员的信息
但还是有方式可以盗取,比如直接用手机录取,然后使用剪辑或者暂停来使得取消掉滑过的学员信息或者等待他滑过等等
所以没有绝对的安全,当然,盗取视频虽然并不是好的操作,但只要传播了,就算违法(看你是否被发现吧)
所以只要不传播,只留给自己基本不会有影响
回归正题,现在我们进入该地址:https://www.aliyun.com/,然后找到如下:

98-微服务项目的编写(下篇)_第6张图片

点击对象存储OSS,或者你可以搜索他,然后进入如下:

98-微服务项目的编写(下篇)_第7张图片

往下面滑,可以看到收费的项目,通常开通不会收取费用,而使用容量,或者特殊容量(如同城冗余存储)都需要收费
通常也会提示的(信息提示,或者打印提示,或者控制台提示的,通常是打印提示)
现在我们开通"对象存储OSS"服务(通常默认是小容量,自然只要根据默认的操作,使用时是用收费的,即我们上传文件需要收取保存的费用,到后面你就知道了):
点击上面的立即开通:
申请阿里云账号并实名认证(前面已经操作过了),从这里我们也可也看出,一个账号在多个网站使用
实际上类似于操作相同的token,或者使用类似于redis保存登录信息的操作
然后开通"对象存储OSS"服务(只需要一路点击即可,很容易知道的)
然后到这里:

98-微服务项目的编写(下篇)_第8张图片

点击并进入管理控制台即可,也就是https://oss.console.aliyun.com/overview这个网站
可以不加overview(因为自动会跳转到加上这个路径的地址)
然后到如下:

98-微服务项目的编写(下篇)_第9张图片

然后往下滑,找到如下:

98-微服务项目的编写(下篇)_第10张图片

点击"创建Bucket",到如下:

98-微服务项目的编写(下篇)_第11张图片

内容我们填写如下:
可以看到oss-cn-beijing.aliyuncs.com地址,也就是帮我们的快速找到存放文件的服务器地址
相当于下载地址的镜像(类似于Linux和maven的镜像等等)

98-微服务项目的编写(下篇)_第12张图片

低频访问存储,通常是操作身份证图片这些信息,如实名认证等等
他的对应存储费用通常比较低,他也一般存放不怎么频繁访问的图片,这里我们操作标准存储即可

98-微服务项目的编写(下篇)_第13张图片

还有一个同城冗余存储,这里没有给出了,实际上他可以认为是备份的作用,可是需要对应的更多存储费用
即不同的存储费用是不同的收取方式的,但是总体来说,还是量的问题,因为同城冗余存储
需要更多的容量,所以实际上相同的量费用相同,主要看使用了多少容量了

98-微服务项目的编写(下篇)_第14张图片

其余的不用改变,当然,这里并没有看到什么费用,但是有些需要收费
如使用容量需要收费,特殊操作需要收费(如同城冗余存储)等等
自然我们这些测试的,基本都只是小容量,如果要收费时,会提示的(不操作程序,一般给出短信通知)
然后点击确定,到如下:

98-微服务项目的编写(下篇)_第15张图片

往下滑可以到这里:

98-微服务项目的编写(下篇)_第16张图片

很明显,实际上地域节点,是帮我们去访问Bucket域名的
因为我们自己访问可能会很慢(因为路由选择需要更多的时间,即转发表索引很慢出来,虽然通常也可人工干预)
然后点击这里:

98-微服务项目的编写(下篇)_第17张图片

点击上传文件,到如下:

98-微服务项目的编写(下篇)_第18张图片

之后,自己操作上传文件,即可,很简单的,这里就不多说,比如我这里操作好后(我上传的是图片),出现如下:

98-微服务项目的编写(下篇)_第19张图片

因为我们上传文件了,那么就使用了容量,所以会出现费用,一般没有余额会有通知,通常是短信(没有操作程序的打印的)
我们可以点击他后面的详情(上面图没有显示出来),到如下:

98-微服务项目的编写(下篇)_第20张图片

我们来访问https://hasa.oss-cn-beijing.aliyuncs.com/1.png,就可以得到图片信息了
他是直接下载的(好像,下载的太快了,没有时间取消,或者可能取消不了,但大文件通常可以取消的,会提示,比如视频,大图片可能没有提示,但可能一般也有提示,因为够大,但这些都只是浏览器的原因,所以并不需要理会)
而正是因为直接下载,所以他基本不会改变原来的url,自己测试就知道了
那么这里为什么没有使用对应的地域节点的地址(oss-cn-beijing.aliyuncs.com)呢
实际上可以使用,但是再浏览器上,访问没有上面作用,需要在程序上操作,也规定了这样,使得更快操作(如保存和读取)
因为浏览器基本不会操作镜像操作,而通常只操作域名,所以这里没有使用地域节点的地址
至此,我们操作完毕,现在我们来操作程序使得上传文件:
通常来说,我们看官方文档比较好,比如到如下:

98-微服务项目的编写(下篇)_第21张图片

往下滑,找到如下(随着时间的推移,界面的显示可能与这里不同,但通常可以找到,到那时,自己去找吧)

98-微服务项目的编写(下篇)_第22张图片

点击"对象存储OSS学习路径"进入,并找到如下:

98-微服务项目的编写(下篇)_第23张图片

点击"Java SDK"进入:

98-微服务项目的编写(下篇)_第24张图片

现在就看自己的咯,好吧,这里还是给出具体的代码:
我们创建子项目,通用的公共子模块edu-api:
最终成果:

98-微服务项目的编写(下篇)_第25张图片

依赖如下:

<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>com.lagougroupId>
        <artifactId>edu-lagouartifactId>
        <version>1.0-SNAPSHOTversion>
        <relativePath/> 
    parent>
    <groupId>com.lagougroupId>
    <artifactId>edu-apiartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <name>edu-apiname>
    <description>edu-apidescription>
    <properties>
        <java.version>11java.version>
    properties>
    <dependencies>
   		<dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            
            <groupId>joda-timegroupId>
            <artifactId>joda-timeartifactId>
            <version>2.9.4version>
        dependency>
        <dependency>
            
            <groupId>com.aliyun.ossgroupId>
            <artifactId>aliyun-sdk-ossartifactId>
            <version>3.15.1version>
        dependency>

    dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>

project>

修改对应配置文件后缀为yml,内容如下:
server:
  port: 8081
aliyun:
  oss:
    #需要是自己的数据,这里我肯定不会给出具体的正确的值的,防止你们恶搞(●ˇ∀ˇ●)
    endpoint: oss-cn-beijing.aliyuncs.com
    accessKeyId: LTGI5t98MKFDrsyegTYozxaV
    secret: 74ZcCcxGWVqGZMgDM5MQYIRbZ3ZOsB
    bucket: hasa
    
    #中间的accessKeyId和secret是之前操作的用户(再短信验证码那里有说明,按照那里的步骤即可)
    #endpoint就是前面说明的地域节点,而bucket就是前面设置的bucket名称,即程序使得用户操作,就如前面说的
    #用户来操作短信给我们,但是这里是用户得到我们的数据(文件数据)
    #来进行对对应的bucket的地方进行保存(使用地域镜像,来操作快速的保存)
    #即,我们需要借用用户来操作,单纯的添加是不行的,就如我们也需要手动的操作一样
    #虽然大于5GB的需要命令行工具或者API,或者其他操作等
    #所以这里的程序就可以操作大于5GB的(在手动上传文件中,可以看到该提示)
    
    #因为这里需要写,所以需要身份验证,虽然就算公开,通常也规定这样的,所以后面的实现类是那样的操作
    
    #所以这里保存(写的话,基本无论你的读写权限是什么,都可以操作,因为有用户的身份,即验证成功了)
    #但是浏览器访问,则需要操作读,那么就不能是私有的,否则访问不了,返回对应的提示信息(看打印就知道了)
    #打印的信息(与后面的用户没有权限是类似的)
注意:因为后面需要他web依赖里面的内容,比如MultipartFile类(导入的依赖包含了需要的依赖),所以导入了web依赖
虽然操作测试是不会操作端口的(即不会占用端口),但这里还是操作访问,即web
在启动类所在的包下,创建service.FileService接口及其实现类:
package com.lagou.eduapi.service;

import org.springframework.web.multipart.MultipartFile;

/**
 *
 */
public interface FileService {
    public String upload(MultipartFile file);
}

package com.lagou.eduapi.service.impl;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.lagou.eduapi.service.FileService;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;

/**
 *
 */
@Service
public class FileServiceImpl implements FileService {

    @Value("${aliyun.oss.endpoint}")
    private String endpoint;
    @Value("${aliyun.oss.accessKeyId}")
    private String accessKeyId;
    @Value("${aliyun.oss.secret}")
    private String accessKeySecret;
    @Value("${aliyun.oss.bucket}")
    private String bucket;

    @Override
    public String upload(MultipartFile file) {
        try {
            // 创建OSSClient实例,很明显,给他用户的两个参数,以及域名节点信息,来初始化
            OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
            // 上传文件流
            InputStream inputStream = file.getInputStream();
            //获取完整名称,包括文件名和后缀名,自然也包括中间的" . "(点)
            String fileName = file.getOriginalFilename(); 
            // 生成随机唯一值,使用uuid,添加到文件名称里面
            String uuid = UUID.randomUUID().toString().replaceAll("-","");
            fileName = uuid+fileName;
            //按照当前日期,创建文件夹,上传到创建文件夹里面
            
            String timeUrl = new DateTime().toString("yyyy/MM/dd");
            fileName = timeUrl+"/"+fileName; //相当于这样:2021/02/02/23fads85rj4hka01.mp4
            // 调用方法实现上传,文件名为fileName,在OSS中会识别的
            ossClient.putObject(bucket, fileName, inputStream);
            // 关闭OSSClient。
            ossClient.shutdown();
            // 上传之后文件路径
            // https://lagou-laosun.oss-cn-beijing.aliyuncs.com/01.jpg
            String url = "https://"+bucket+"."+endpoint+"/"+fileName;
            // 返回
            return url;
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }
}
然后创建controller.FileController类:
package com.lagou.eduapi.controller;

import com.lagou.eduapi.service.FileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

/**
 *
 */
@RestController
@RequestMapping("file")
public class FileController {
    @Autowired
    private FileService fileService;

    //上传文件到阿里云oss
    @PostMapping("fileUpload")
    public String fileUpload(@RequestParam("file") MultipartFile file) {
        //获取上传文件
        String url = fileService.upload(file);
        return url;
    }
}
启动项目,在访问之前,首先检查一下你用户的访问权限(前面操作过的权限)
比如添加这个权限(不要是只读那个,他不能保存)
还有,解除权限和添加权限都有延迟的,所以通常需要等待一会作用,且可能你的访问会使得来回跳动,因为可能没有被拦截
自己测试来回切换解除和添加权限就知道了,可以出现,当访问成功后
下次的访问竟然是失败的,拦截的问题,比如没有被拦截,相当于抢占cpu资源
只是权重小点而已(而不是与多线程一样是平等的)
使得后续的可能会放行,但是只要延迟结束,就不会出现这样的问题了

98-微服务项目的编写(下篇)_第26张图片

即上面的已添加的这个部分权限,需要你去给用户添加
否则你是操作不了的,即一般在代码中的ossClient.putObject(bucket, fileName, inputStream);这里报错
然后操作如下访问:

98-微服务项目的编写(下篇)_第27张图片

执行后,查看我们的文件列表,如果有数据代表操作成功
你也可也访问返回的地址,若也操作了对应的文件(如下载),也代表操作成功
至此,我们的文件操作完毕,但是你会发现,他是下载的,那么请问一下,前端的标签可以获取吗
答:可以,可以放在img标签的src里面进行访问,那么为什么没有下载呢,实际上这是浏览器的问题
浏览器访问的话,那么他会下载到我们的本地,无论是否是小文件还是大文件都会(这里就与fastdfs是不同的,fastdfs使用浏览器基本只能下载大文件,虽然他们都有提示,小文件是直接的显示,而OSS确会下载小文件)
注意:虽然我说的是大文件,但是,某些后缀也会看出大文件的,无论是否是大的空间
比如将小文件的后缀修改成mp4,那么基本就有提示你是否下载了,即看成大文件了
而标签,即代码,默认为给浏览器来显示,即可以认为是只读
虽然没有下载到我们的本地,但是他用来显示了,退出自然也就没有(也就是一个窗口占用系统资源的,退出则释放)
所以OSS的确也操作了fastdfs的功能,虽然容量(上传后文件的保存,根据大小来收费,即容量需要收费)需要收费
但他还是有其他功能的(比如加密),虽然可能某些加密要收费,但也有免费的
具体看创建bucket时的介绍即可
Config 分布式配置中心:
分布式配置中心应用场景
往往,我们使用配置文件管理一些配置信息,比如application.yml
单体应用架构:配置信息的管理、维护并不会显得特别麻烦,手动操作就可以,因为就一个工程
微服务架构:因为我们的分布式集群环境中可能有很多个微服务,我们不可能一个一个去修改配置然后重启生效
在一定场景下我们还需要在运行期间动态调整配置信息
比如:根据各个微服务的负载情况,动态调整数据源连接池大小,我们希望配置内容发生变化的时候,微服务可以自动更新
场景总结如下:
1:集中配置管理,一个微服务架构中可能有成百上千个微服务,所以集中配置管理是很重要的(一次修改、到处生效)
2:不同环境不同配置,比如数据源配置在不同环境(开发dev,测试test,生产prod)中是不同的
3:运行期间可动态调整,例如,可根据各个微服务的负载情况,动态调整数据源连接池大小等配置修改后可自动更新
4:如配置内容发生变化,微服务可以自动更新配置(一般都是有延迟的,这是肯定的,就如访问也需要延迟)
那么,我们就需要对配置文件进行集中式管理,这也是分布式配置中心的作用
Spring Cloud Config 是一个分布式配置管理方案,包含了 Server端和 Client端两个部分
一句话理解:将所有的配置文件统一保存到云端(比如github),通过服务端应用去获取,再分发给对应的每一个微服务
云端包括:
1:github (如果访问不了或者访问缓慢,可以使用代理网址:https://hub.fastgit.org 等价于 https://github.com)
现在好像已经改变成了hub.fastgit.xyz(http://hub.fastgit.xyz/),所以这里需要注意,当然代理网址(http://hub.fastgit.xyz/)也并不是一定可以访问(甚至比https://github.com还要差,即可能出现https://github.com可以访问,而http://hub.fastgit.xyz/不能访问)
所以有时候 https://github.com也行,如果需要好的解决,可以看这个文章https://zhuanlan.zhihu.com/p/358183268
2:gitee 码云(国产github,很好用,用法和github一样)

98-微服务项目的编写(下篇)_第28张图片

服务端配置:
在这之前,如果你不会使用或者只是大致了解github,可以访问这个网站来学习
https://docs.github.com/cn/get-started/quickstart/hello-world
在GitHub上新建一个库,名称是yaml,然后将项目中,所有的yml文件都推送该库里面去(他们都修改名称为,“项目名-dev.yml”)
具体实操,可以查看上面的这个网站,或者可以看看73章博客中,操作的具体流程
具体推送文件如下:

98-微服务项目的编写(下篇)_第29张图片

当然,可能有些不会操作,但上传也并没有坏处,用到了就知道吧
创建服务端工程:
创建子项目,edu-config-boot配置中心(8008):
最终成果:

98-微服务项目的编写(下篇)_第30张图片

对应的依赖:

<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>com.lagougroupId>
        <artifactId>edu-lagouartifactId>
        <version>1.0-SNAPSHOTversion>
        <relativePath/> 
    parent>
    <groupId>com.lagougroupId>
    <artifactId>edu-config-bootartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <name>edu-config-bootname>
    <description>edu-config-bootdescription>
    <properties>
        <java.version>11java.version>
    properties>
    <dependencies>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-config-serverartifactId>
        dependency>
        
        	
		<dependency>
			<groupId>org.springframework.cloudgroupId>
			<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
		dependency>
    dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>

project>

将配置文件后缀修改成yml,内容如下:
server:
  port: 8008
spring:
  application:
    name: edu-config-boot
  cloud:
    config:
      server:
        git:
          uri: https://github.com/wobushigoudao/yaml.git  #配置git服务地址
          username: [email protected] #配置git用户名
          password: Sunguoan123 #配置git密码,记得使用自己的,这里就给一个错误的
          search-paths:
            - yaml  #仓库名
          default-label: master # 使用的默认分支,默认为 master
          clone-on-start: true
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka
    register-with-eureka: true
    fetch-registry: true
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
对应的启动类:
package com.lagou.educonfigboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.server.EnableConfigServer;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
@EnableConfigServer // 配置中心
public class EduConfigBootApplication {

    public static void main(String[] args) {
        SpringApplication.run(EduConfigBootApplication.class, args);
    }

}

启动该项目
访问localhost:8008/master/edu-ad-boot-dev.yml(master可以不加,因为默认是到该分支的,且这里也是设置为该分支,所以这里可以不行,否则如果设置的是其他分支,那么通常需要指定,因为这时如果不指定的话,可能会获取不到文件信息)
查看结果,如果出现了结果,且基本对应,代表操作成功
注意:如果出现了启动不了,无缘无故停止,但实际上并没有什么问题(有一定可能是因为超时原因,但通常也不是)
通常可以关闭项目重新打开来解决
一般这样的情况端口自动的没有清除干净,idea默认为占用的端口,但基本很少出现这种情况(电脑好的情况下)
或者因为太卡的原因以及内存的原因造成这样的
如果有问题的,那么自然是某些配置问题啦,通常也会出现日志,那么自己看日志错误提示吧
现在,我们再创建客户端工程配置(以广告微服务edu-ad-boot为例):
因为application.yml配置都推送到git(github或者gitee,gitee即码云,这里是github)上
所以以往的application.yml文件里面的内容都可以不写了
但是要将application.yml文件更名为bootstrap.yml
因为bootstrap.yml文件启动优先级更高
这里提一下:一般来说,该文件是属于cloud操作的而不是boot操作的,他也有后缀,也符合boot的那三个后缀优先级
他在启动时就加载,加载就去edu-config-boot中找属于自己的配置信息,edu-config-boot得到请求后,会去git中找
bootstrap.yml的内容(也就是说,我们只需要下面的配置即可,我们将所有的配置内容删除,然后加上下面的配置即可):
spring:
  cloud:
    config:
      name: edu-ad-boot #github上去掉yml的资源名称
      profile: dev   #本次访问的配置项
      label: master
      uri: http://localhost:8008 # 去8008配置服务中心找属于自己的配置信息,靠上面的name和profile值查找
      #很明显,他们组成的结果是localhost:8008/master/edu-ad-boot-dev
      #正好是上面我们访问localhost:8008/master/edu-ad-boot-dev.yml的名称
然后加上依赖:
  <dependency> 
            
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-config-clientartifactId>
        dependency>
因为springboot本身是不支持bootstrap文件的,需要结合springcloud的组件一起使用
通常需要spring-cloud-context依赖,使得支持该bootstrap文件
大多数的spring-cloud的主要依赖都有他spring-cloud-context依赖
比如spring-cloud-starter-netflix-eureka-client依赖里面就有spring-cloud-context依赖
且从Spring Boot 2.4版本开始,配置文件加载方式进行了重构
这个重构,导致我们还需要加上如下依赖,使得加载bootstrap文件,否则的话
一般不会进行加载该bootstrap文件,那么可能会出现没有什么属性的问题,比如数据源没有加上(如没有sql的url):

<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-bootstrapartifactId>
dependency>
在Spring Boot 2.4版本之前,通常可以不加
现在我们重启广告微服务,然后访问http://localhost:8001/ad/getAdsBySpaceId/1
如果出现了数据,代表他的确读取了文件,即操作成功
这里因为读取缓存,导致我们修改git的内容,项目的配置内容并不会改变,所以需要刷新,比如手动刷新和自动刷新
具体的刷新机制可以到89章博客查看,这里就不多说了
IDEA集成Docker部署微服务:
回顾docker,引入大多数网上的内容(如果需要具体的学习,可以看看90章博客):
我想要盖房子,于是我搬砖、砍木头、画图纸、和水泥,一顿操作猛如虎,终于把这个房子盖好了

98-微服务项目的编写(下篇)_第31张图片

住了一段时间后,心血来潮想搬回东北老家,这时候按之前的办法
我只能回到东北后,再次搬砖、砍木头、画图纸、和水泥、盖房子

98-微服务项目的编写(下篇)_第32张图片

突然,降临一位神仙姐姐教了我一种法术,这个法术可以把我的房子复制一份,做成镜像,并可以放在百宝箱里

98-微服务项目的编写(下篇)_第33张图片

抱着百宝箱就回了东北,就用这个镜像,复制一套房子,完美复刻,拎包入住
是不是很神奇,对应到我们的项目中来,房子就是项目本身,镜像就是项目的复制,百宝箱就是镜像仓库
如果要动态扩容,从仓库中取出项目镜像,随便复制就可以了
不用再关注版本、兼容、部署等问题,彻底解决了开发完美,上线就崩,无终止排查环境的尴尬
安装docker:
#安装docker
yum -y install docker #由于他本身是没有的,现在的docker的安装,可能需要修改vi /etc/sysconfig/docker
#加上或者修改如下即可:
#OPTIONS='--selinux-enabled=false --log-driver=journald --signature-verification=false'
#否则可能启动不了(通常情况下是需要的,当然,并不绝对,如果出现了,加上或者修改即可)
#启动docker
systemctl start docker
#查看docker的运行状态
systemctl status docker
开启远程访问:
Docker默认是不允许远程访问的,基本只能是docker所在的自己的服务器访问(所以在不允许之前,通常操作端口映射的)
# 修改配置文件
vim /lib/systemd/system/docker.service
修改成这样:

在这里插入图片描述

即修改在这个地方(可能版本不同,修改的地方或者操作不同,到那时,还是去百度吧,这里我是直接执行yum -y install docker的,当然,可能随着时间的推移,得到的版本可能就不同了,那时候就去百度查看解决吧):
ExecStart=/usr/bin/dockerd-current -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock \
#注意:最后需要加上"\"(可以不用空格隔开),否则可能启动不了docker,甚至可能创建不了容器,所以若在idea操作时,这里要加上,否则打包docker一般都会报错
# 重新加载配置文件
systemctl daemon-reload
# 重启docker
service docker restart
# 查看端口是否开启
netstat -nlpt
# 验证端口是否生效
curl http://192.168.164.128:2375/info #192.168.164.128是当前服务器的地址,我这里是这个,这里写上你自己的
#如果出现了很多数据,通常代表操作成功
IDEA集成插件:
在Plugins中搜索Docker,并安装

98-微服务项目的编写(下篇)_第34张图片

也就是第一个,其实一般大多数的idea会自带docker插件的,但看一下也没有关系
然后点击并输入如下:

98-微服务项目的编写(下篇)_第35张图片

只要出现如下,就代表连接成功:

98-微服务项目的编写(下篇)_第36张图片

即出现了Connection successful这个,就代表操作成功
注意:记得关闭服务器的防火墙,比如输入如下:
systemctl stop firewalld.service
否则可能是连接失败的
然后到如下(这个与docker打包并没有什么关系,即可以不配置,他只是代表自己仓库的镜像,当然,如果推送镜像到自己的仓库的操作以及从自己的仓库里拉取镜像的操作,可以百度查看操作,自己的仓库也就是在https://hub.docker.com/这个地址里注册并创建的仓库):

98-微服务项目的编写(下篇)_第37张图片

记得输入自己docker的用户名和密码,docker的地址如下:https://hub.docker.com/,在这里注册即可
若出现了出现了Connection successful这个,代表操作成功
然后点击ok退出窗口(也可也点击应用,但是反正要点击ok,ok包含应用)
然后可以在这里看到docker相关的选项了:

98-微服务项目的编写(下篇)_第38张图片

但是这里可能会有一个问题,在idea中,他可能并不能获取docker的一些信息,但是实际上还是操作了docker
可能与docker的文件开放有关,或者与idea本身,以及docker插件有关,也有可能是某些特殊原因,具体可以百度
Docker的Maven插件:
传统的过程中,要经历打包,部署,上传到linux
然后编写Dockerfile(从这里开始的步骤,具体实现,可以百度查看),构建镜像,创建容器等步骤
Dockerfile是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明
如果需要学习操作Dockerfile,可以百度找学习资源,这里就不多说了
docker-maven-plugin就是帮助我们在开发构成中,自动生成镜像并推送到仓库中
docker打包项目的插件有两种:docker-maven-plugin,dockerfile
我们通常需要都进行打包,这里以edu-eureka-boot服务中心(7001)为例,使得进行打包
在打包之前,安装相关的项目(前面说明过了),因为他是单独(因为这里的单独的打包会报错的)
所以需要先安装(主要是安装父项目),才可以打包
在该项目中,加上或者修改如下依赖:
<build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>

            <plugin>
                 <dependency>
                     
                <groupId>javax.activationgroupId>
                <artifactId>activationartifactId>
                <version>1.1.1version>
            dependency>
                <groupId>com.spotifygroupId>
                <artifactId>docker-maven-pluginartifactId>
                <version>1.0.0version>
                <configuration>
                    
                    <imageName>laosun/${project.artifactId}imageName>
                    
                    <imageTags>
                        <imageTag>latestimageTag>
                    imageTags>
                    
                    <baseImage>javabaseImage>
                    
                    <maintainer>laosun [email protected]maintainer>

                    
                    <entryPoint>["java", "-jar", "/${project.build.finalName}.jar"]entryPoint>
                    
                    <dockerHost>http://192.168.164.128:2375dockerHost>

                    
                    <resources>
                        <resource>
                            <targetPath>/targetPath>
                            
                            <directory>${project.build.directory}directory>
                            
                            <include>${project.build.finalName}.jarinclude>
                        resource>
                    resources>
                configuration>
            plugin>
        plugins>
    build>


执行命令:
对项目进行打包,并构建镜像到docker上
第一次执行多等一会,因为要拉取java的环境(初始化,以及拉取镜像的操作)等等操作
注意:构建镜像,要将项目中用到的localhost改为docker所在服务器的ip,然后再进行构建镜像
当然,如果都是同一个机器(这里代表容器,而不是服务器)里面,可以不用改变
但是这里的操作,通常自然不会到一个容器里面,所以也最好改变
那么有个问题,没有端口映射,是怎么互相访问的,实际上需要我们自己来操作端口映射,即需要我们自己对应
他虽然是docker里面的,但是无论是他访问别人,还是别人访问他,都需要宿主机的地址来操作的
相当于别人访问宿主机,或者宿主机访问别人的作用,这时端口映射的功劳

98-微服务项目的编写(下篇)_第39张图片

记得改变上面的localhost(是eureka,不用注意图片,这是我中途修改的,图片没有换了)变成对应服务器的地址,比如我就需要变成192.168.164.128即可
现在我们点击如下:

98-微服务项目的编写(下篇)_第40张图片

不给你看我的用户(嘿嘿(●ˇ∀ˇ●)),然后执行即可,命令是:mvn clean package docker:build(不用手写啦)
代表清除,打包,然后操作docker的打包,当然,可以先打包,然后直接执行mvn docker:build也可以
这里需要注意:如果你在docker上启动不了,那么这里自然也不会操作打包
因为我们需要操作内容,自然要启动的,所以这里也会出现对应类似的报错
至此命令执行完成,会自动将jar包镜像推送到docker
在idea的docker界面,根据镜像创建容器即可,如果没有显示,可以到Linux服务器上进行创建
这里给出显示的操作图片示例

98-微服务项目的编写(下篇)_第41张图片

98-微服务项目的编写(下篇)_第42张图片

当然了,一个镜像可以创建N个容器
相当于多个服务器,只是宿主机端口不同而已,当然,一个容器也可以对应多个宿主机端口
现在,我们启动后,查看启动的容器:
docker ps
然后我们访问http://192.168.164.128:7001/,如果出现了页面,代表操作成功
至此,你可以自己进行测试其他微服务了,这里就不多说了
现在,我们继续分析之前的事务
前面已经说明了("因为不同数据库之间的事务基本是不会共享的,在后面会说明该问题"就是这句话)
所以我们需要分布式的事务,那么我们该如何操作呢:
分布式事务解决方案-Seata:
为什么选择seata:
1:我们的项目是微服务项目,多数据源,因此很多业务操作避免不了跨数据源(库)操作,传统的事务是不能解决跨数据源的
因此我们需要寻求分布式事务的解决方案
2:Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务
3:Seata 是 Simple Extensible Autonomous Transaction Architecture 的简写,Fescar 品牌升级,更名为 Seata
4:在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色
并能帮助经济体平稳的度过历年的双11,可以说性能十分强大
5:在2019年1月份,为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源
6:使用seata需要服务端(官网提供下载)和客户端(微服务)配合来完成
通用案例:
订单保存成功 & 累计账户积分+10
账户微服务和订单微服务,都各自有独立的数据库
因为数据库不同,所以不同数据库事务之间不会操作,这就是主要解决的问题
为了进行测试,我们操作如下,这三个微服务与前面的微服务不同,如果出现名称相同的,并不需要理会
比如下面的订单微服务,他与前面的订单微服务是不同的,注意即可
在操作之前,记得启动服务中心edu-eureka-boot(7001),如果没有启动,启动即可
如果已经启动了,那么就不需要启动了(不用变)
现在开始操作:
创建父项目test-seata
最终成果:

98-微服务项目的编写(下篇)_第43张图片

依赖如下:

<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">
    <modelVersion>4.0.0modelVersion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.1.6.RELEASEversion>
        <relativePath/> 
    parent>
    <groupId>com.lagougroupId>
    <artifactId>test-seataartifactId>
    <version>1.0-SNAPSHOTversion>
    <packaging>pompackaging>
    <properties>
        <maven.compiler.source>11maven.compiler.source>
        <maven.compiler.target>11maven.compiler.target>
    properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starterartifactId>
        dependency>

        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintagegroupId>
                    <artifactId>junit-vintage-engineartifactId>
                exclusion>
            exclusions>
        dependency>
    dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-dependenciesartifactId>
                <version>Greenwich.RELEASEversion>
                <type>pomtype>
                <scope>importscope>
            dependency>
            <dependency>
                <groupId>com.alibaba.cloudgroupId>
                <artifactId>spring-cloud-alibaba-dependenciesartifactId>
                <version>2.1.0.RELEASEversion>
                <type>pomtype>
                <scope>importscope>
            dependency>
        dependencies>
    dependencyManagement>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>
project>
然后创建子项目(路径是子的)
创建账户微服务test-account(8100):
最终成果:

98-微服务项目的编写(下篇)_第44张图片

数据库(记得是后面的数据源哦,即自己的数据库,那么后面的数据源自然也是对应的数据库地址,后面的就不提示了):
CREATE DATABASE /*!32312 IF NOT EXISTS*/`test-account` /*!40100 DEFAULT CHARACTER SET utf8 */;

USE `test-account`;

/*Table structure for table `taccount` */

DROP TABLE IF EXISTS `taccount`;

CREATE TABLE `taccount` (
  `id` int(11) unsigned zerofill NOT NULL AUTO_INCREMENT COMMENT '账户编号',
  `name` varchar(64) NOT NULL COMMENT '昵称',
  `score` int(11) NOT NULL DEFAULT '0' COMMENT '积分',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

/*Data for the table `taccount` */

insert  into `taccount`(`id`,`name`,`score`) values 
(00000000001,'吕布',0),
(00000000002,'赵云',0),
(00000000003,'典韦',0);
-- 00000000001相当于1
对应依赖如下:

<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>
        <artifactId>test-seataartifactId>
        <groupId>com.lagougroupId>
        <version>1.0-SNAPSHOTversion>

    parent>
    <groupId>com.lagougroupId>
    <artifactId>test-accountartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <name>test-accountname>
    <description>test-accountdescription>
    <properties>
        <java.version>11java.version>
    properties>
    <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.12version>
        dependency>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>
        
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-boot-starterartifactId>
            <version>3.3.2version>
        dependency>
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <scope>runtimescope>
        dependency>
    
    dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>

project>

将配置文件后缀修改成yml,内容如下:
server:
  port: 8100
spring:
  application:
    name: test-account
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.164.128:3306/test-account?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: QiDian@666
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka
    register-with-eureka: true
    fetch-registry: true
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
在启动类所在的包下,创建entity.Taccount类:
package com.lagou.entity;

/**
 *
 */
import lombok.Data;

@Data
public class Taccount {

    private long id;
    private String name;
    private long score;



}
然后创建mapper.AccountDao接口:
package com.lagou.mapper;

/**
 *
 */
public interface AccountDao extends BaseMapper<Taccount> {
}

再创建service.AccountService接口及其实现类:
package com.lagou.service;

/**
 *
 */
public interface AccountService {
    public int updateAccountScore(int userid, int score);
}

package com.lagou.service.impl;

import com.lagou.entity.Taccount;
import com.lagou.mapper.AccountDao;
import com.lagou.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 *
 */
@Service
public class AccountServiceImpl implements AccountService {
    @Autowired
    private AccountDao accountDao;
    @Override
    public int updateAccountScore(int userid, int score) {
        Taccount account = accountDao.selectById(userid);
        // 在原来的积分之上,再增加10分
        account.setScore(account.getScore()+score);
        return accountDao.updateById(account);
    }
}
创建controller.AccountController类:
package com.lagou.controller;

import com.lagou.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 *
 */
@RestController
@RequestMapping("account") //这里就不加跨域了,因为只是测试用,测试用的基本不会写,后面的不写的基本都是如此
//不操作跨域的,那么也是不写的,虽然写了也没有问题,只是没起到作用而已
public class AccountController {

    @Autowired
    private AccountService accountService;

    @GetMapping("update")
    public int updateAccountScore(){
        return accountService.updateAccountScore(1, 10);
    }
}
对应的启动类:
package com.lagou;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
@MapperScan("com.lagou.mapper")
public class TestUserApplication {

    public static void main(String[] args) {
        SpringApplication.run(TestUserApplication.class, args);
    }

}

然后启动项目,访问localhost:8100/account/update,查看数据库数据是否变化,若发生了变化,则操作成功
创建订单微服务test-order(8101):
最终成果:

98-微服务项目的编写(下篇)_第45张图片

数据库:
CREATE DATABASE /*!32312 IF NOT EXISTS*/`test-order` /*!40100 DEFAULT CHARACTER SET utf8 */;

USE `test-order`;

/*Table structure for table `torder` */

DROP TABLE IF EXISTS `torder`;

CREATE TABLE `torder` (
  `id` varchar(128) NOT NULL COMMENT '订单编号',
  `uid` int(11) NOT NULL DEFAULT '0' COMMENT '账户编号',
  `pid` int(11) NOT NULL DEFAULT '0' COMMENT '商品编号',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
对应的依赖:

<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>
		<artifactId>test-seataartifactId>
		<groupId>com.lagougroupId>
		<version>1.0-SNAPSHOTversion>
	parent>
	<groupId>com.lagougroupId>
	<artifactId>test-orderartifactId>
	<version>0.0.1-SNAPSHOTversion>
	<name>test-ordername>
	<description>test-orderdescription>
	<properties>
		<java.version>11java.version>
	properties>

	<dependencies>
		
		<dependency>
			<groupId>org.springframework.bootgroupId>
			<artifactId>spring-boot-starter-webartifactId>
		dependency>
		<dependency>
			<groupId>org.projectlombokgroupId>
			<artifactId>lombokartifactId>
			<version>1.18.12version>
		dependency>
		
		<dependency>
			<groupId>org.springframework.cloudgroupId>
			<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
		dependency>
		
		<dependency>
			<groupId>com.baomidougroupId>
			<artifactId>mybatis-plus-boot-starterartifactId>
			<version>3.3.2version>
		dependency>
		
		<dependency>
			<groupId>mysqlgroupId>
			<artifactId>mysql-connector-javaartifactId>
			<scope>runtimescope>
		dependency>
		
	dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.bootgroupId>
				<artifactId>spring-boot-maven-pluginartifactId>
			plugin>
		plugins>
	build>

project>

将配置文件后缀修改成yml,内容如下:
server:
  port: 8101
spring:
  application:
    name: test-order
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.164.128:3306/test-order?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: QiDian@666
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka
    register-with-eureka: true
    fetch-registry: true
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
在启动类所在的包下,创建entity.Torder类:
package com.lagou.entity;
import lombok.Data;

@Data
public class Torder {

  private String id;
  private long uid;
  private long pid;



}

然后创建mapper.OrderDao接口(mapper和dao可以互相变化,具体看你自己,比如dao包或者OrderMapper类):
package com.lagou.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.lagou.entity.Torder;

/**
 *
 */
public interface OrderDao extends BaseMapper<Torder> {
}
创建service.OrderService接口及其实现类:
package com.lagou.service;

import com.lagou.entity.Torder;

/**
 *
 */
public interface OrderService {
    public int saveOrder(Torder order);
}

package com.lagou.service.impl;

import com.lagou.entity.Torder;
import com.lagou.mapper.OrderDao;
import com.lagou.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 *
 */
@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderDao orderDao;

    @Override
    public int saveOrder(Torder order) {
        return orderDao.insert(order);
    }
}
创建controller.OrderController类:
package com.lagou.controller;

import com.lagou.entity.Torder;
import com.lagou.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

/**
 *
 */
@RestController
@RequestMapping("order")
public class OrderController {

    @Autowired
    private OrderService orderService;

    @GetMapping("save")
    public int save(){
        Torder order = new Torder();
        order.setId(UUID.randomUUID().toString().replaceAll("-", ""));
        order.setUid(1);
        order.setPid(11);
        return orderService.saveOrder(order);
    }
}
对应的启动类:
package com.lagou;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient
@MapperScan("com.lagou.mapper")
public class TestOrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(TestOrderApplication.class, args);
    }

}

然后启动项目,访问localhost:8101/order/save,查看数据库,若有数据添加了,代表操作成功
创建业务入口微服务:test-front(8102):
最终成果:

98-微服务项目的编写(下篇)_第46张图片

对应的依赖:

<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>
		<artifactId>test-seataartifactId>
		<groupId>com.lagougroupId>
		<version>1.0-SNAPSHOTversion>
	parent>
	<groupId>com.lagougroupId>
	<artifactId>test-frontartifactId>
	<version>0.0.1-SNAPSHOTversion>
	<name>test-frontname>
	<description>test-frontdescription>
	<properties>
		<java.version>11java.version>
	properties>
	<dependencies>
		
		<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.cloudgroupId>
			<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
		dependency>
		
	dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.bootgroupId>
				<artifactId>spring-boot-maven-pluginartifactId>
			plugin>
		plugins>
	build>

project>

将配置文件后缀修改成yml,内容如下:
server:
  port: 8102
spring:
  application:
    name: test-front
eureka:
  client:
    service-url:
      defaultZone: http://localhost:7001/eureka
    register-with-eureka: true
    fetch-registry: true
  instance:
    prefer-ip-address: true
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
在启动类所在的包下,创建remote.AccountRemoteService接口:
package com.lagou.remote;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

/**
 *
 */
@FeignClient(name = "test-account",path = "account")
public interface AccountRemoteService {
    @GetMapping("update")
    int update();
}
再在该remote包下创建OrderRemoteService接口:
@FeignClient(name = "test-order",path = "order")
public interface OrderRemoteService {
    @GetMapping("save")
    int save();
}
创建service.BusinessService接口及其实现类:
package com.lagou.service;

/**
 *
 */
public interface BusinessService {
    public boolean business();
}

package com.lagou.service.impl;

import com.lagou.remote.AccountRemoteService;
import com.lagou.remote.OrderRemoteService;
import com.lagou.service.BusinessService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 *
 */
@Service
public class BusinessServiceImpl implements BusinessService {
    @Autowired
    private AccountRemoteService accountRemoteService;

    @Autowired
    private OrderRemoteService orderRemoteService;

    @Override
    public boolean business() {
        accountRemoteService.update();
        // int i = 10/0;
        orderRemoteService.save();
        return true;
    }
}
创建controller.frontController类:
package com.lagou.controller;

import com.lagou.service.BusinessService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 *
 */
@RestController
@RequestMapping("front")
public class frontController {

    @Autowired
    private BusinessService businessService;

    @GetMapping("business")
    public boolean business(){
        return  businessService.business();
    }

}
对应的启动类:
package com.lagou;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableEurekaClient // 注册到中心的客户端
@EnableFeignClients
public class TestFrontApplication {

    public static void main(String[] args) {
        SpringApplication.run(TestFrontApplication.class, args);
    }

}

然后我们启动项目,访问localhost:8102/front/business,查看两个数据库
若对应数据库数据发生改变(变化)了,且操作添加了,代表操作成功
至此,基本项目部署基本完毕,接下来我们需要操作分布式事务了
首先,我们修改业务入口微服务的BusinessServiceImpl类的如下:
  @Override
    public boolean business() {
        accountRemoteService.update();
         int i = 10/0;
        orderRemoteService.save();
        return true;
    }
重启项目,访问localhost:8102/front/business,查看对应两个数据库,会发现,我们操作了变化数据,但是没有添加数据
接下来,我们来验证,数据库事务是否传递(事务传递:也就是不同数据库之间的事务基本是不会共享的),操作如下:
加上如下依赖:
   <dependency>
            
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-jdbcartifactId>
            <version>5.1.5.RELEASEversion>
            dependency>
<dependency>
        
        <groupId>mysqlgroupId>
        <artifactId>mysql-connector-javaartifactId>
   
    dependency>
修改配置文件:
spring:
  application:
    name: test-front
  datasource:
  #随便取他们两个之一的配置
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.164.128:3306/test-account?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: QiDian@666
然后修改BusinessServiceImpl类的如下:
@Override
    @Transactional()
    public boolean business() {
        accountRemoteService.update();
   
        orderRemoteService.save();
        int i = 10/0; //放在后面可以更好的得出结果,看后面的解释就知道了,虽然这里并没有什么用
        //当然,你放中间可以来操作实际业务,但这里只是测试
        //所以就放在后面来看后面的具体解释(因为方便,可以使用"都没有发生"这个词语来解释)了
        return true;
    }
重启项目,继续访问localhost:8102/front/business,查看是否操作事务传递
我们可以发现,数据变化了,且添加了,即并没有操作回滚,为什么呢
这是因为对应操作的是其他微服务的连接(相当于http访问一样)
虽然,数据源相同,但是连接不同(连接不同,也可以认为是窗口不同)
所以事务也并没有操作,所以我们需要在他们的微服务里面操作事务,先注释掉上面的int i = 10/0;代码和@Transactional()代码
然后操作如下,以账户微服务为例:
修改AccountServiceImpl类的如下:
 @Override
    @Transactional() //mp依赖有对应的依赖,所以不需要导入其对应的依赖了
    public int updateAccountScore(int userid, int score) {
        Taccount account = accountDao.selectById(userid);
        // 在原来的积分之上,再增加10分
        account.setScore(account.getScore()+score);
        int i = 10/0;
        return accountDao.updateById(account);
    }
重新启动账户微服务,和业务入口微服务,继续访问localhost:8102/front/business
发现,在业务入口微服务的accountRemoteService.update();报错
也就是说,虽然类似于http访问,但是对方报错了,我们也会受影响
且我们查看数据库,发现,都没有发生改变或者添加,我们将业务入口微服务的BusinessServiceImpl类,修改如下:
   @Override
//    @Transactional()
    public boolean business() {
        

        int save = orderRemoteService.save();
        System.out.println(save);
        accountRemoteService.update();
//        int i = 10/0;
        return true;
    }
我们换一下代码位置(测试完后,可以选择不换回来,因为我们只是测试事务而已,不需要具体业务顺序)
重启业务入口微服务,继续访问localhost:8102/front/business
查看数据库,我们可以很明显的看到,他只是添加了数据,但是没有修改数据
也就是说,对应的注解@Transactional() ,并没有操作其他微服务的事务。只是操作当前的
所以通过上面的多个测试,所以的确,不同数据库(不同连接一般也算,一般是针对不同项目,因为相同的,可能通常都是一个连接,虽然前面也并没有说明过,这里说明一下,因为sql连接的默认隔离,基本不会出现什么隔离问题,这里就不考虑了)
事务的确不会共享,所以我们的确需要一种分布式的事务来使得他们可以认为都在一个事务中
而不是具体的一个微服务的事务(数据库或者连接)
至此测试完毕,将AccountServiceImpl类的updateAccountScore方法的对应的int i = 10/0;以及@Transactional()注解注释掉吧
然后重启账户微服务吧
现在我们来完成分布式事务的具体操作:
下载与安装:
官网:http://seata.io/zh-cn/index.html

98-微服务项目的编写(下篇)_第47张图片

点击上面的下载,找到如下:
尽量不要选择最新的版本因为通常"不太稳定",我们选择(点击)下载 1.2.0 (2020-04-20)的binary

98-微服务项目的编写(下篇)_第48张图片

这样就可以下载对应的zip文件了,当然,你也可以到如下地址下载:
链接:https://pan.baidu.com/s/1fTDRwt6PyVApNRRDZ9m7tw
提取码:alsk
我们将得到的seata-server-1.2.0.zip(可能随时间,该名称会改变)上传到linux,并解压
配置服务端:
操作解压:
unzip seata-server-1.2.0.zip
cd seata/conf
#如果没有unzip命令,那么可以执行yum install unzip,进行下载安装
#一般"ll(或者相关的,比如ls -l,实际上他们是一样的,只是命令名称不同而已,但操作基本相同,即可以认为他们的操作是等价的)"命令显示的文件中,如果我们修改了文件
#那么通常在1年以内(可能是月份,或者天数,具体可以百度)对应会显示时间,而不是年份
#只是解压,移动等等,一般不算修改,操作内部文件也算(比如创建目录,如mkdir a)
主要配置两个文件:
file.conf:配置数据库
registry.conf:配置注册中心
file.conf:
在mysql里创建一个seata库,需要执行下面的脚本代码,因为seata需要在操作mysql事务时,需要用到这些表
否则可能不会操作事务,或者事务不起作用,也有可能项目启动时会报错,这些等等原因
一般情况下,下面的脚本代码需要在seata的1.0之前(通常不包括1.0)的版本中,从db_store.sql(一般是这个名称)中获取
seata的1.0之后(通常包括1.0)取消了这个脚本文件,所以我们复制下面代码
CREATE DATABASE /*!32312 IF NOT EXISTS*/`seata` /*!40100 DEFAULT CHARACTER SET utf8 */;

USE `seata`;

/*Table structure for table `branch_table` */

DROP TABLE IF EXISTS `branch_table`;

CREATE TABLE `branch_table` (
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(128) NOT NULL,
  `transaction_id` bigint(20) DEFAULT NULL,
  `resource_group_id` varchar(32) DEFAULT NULL,
  `resource_id` varchar(256) DEFAULT NULL,
  `lock_key` varchar(128) DEFAULT NULL,
  `branch_type` varchar(8) DEFAULT NULL,
  `status` tinyint(4) DEFAULT NULL,
  `client_id` varchar(64) DEFAULT NULL,
  `application_data` varchar(2000) DEFAULT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`branch_id`),
  KEY `idx_xid` (`xid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*Data for the table `branch_table` */

/*Table structure for table `global_table` */

DROP TABLE IF EXISTS `global_table`;

CREATE TABLE `global_table` (
  `xid` varchar(128) NOT NULL,
  `transaction_id` bigint(20) DEFAULT NULL,
  `status` tinyint(4) NOT NULL,
  `application_id` varchar(32) DEFAULT NULL,
  `transaction_service_group` varchar(32) DEFAULT NULL,
  `transaction_name` varchar(128) DEFAULT NULL,
  `timeout` int(11) DEFAULT NULL,
  `begin_time` bigint(20) DEFAULT NULL,
  `application_data` varchar(2000) DEFAULT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`xid`),
  KEY `idx_gmt_modified_status` (`gmt_modified`,`status`),
  KEY `idx_transaction_id` (`transaction_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*Data for the table `global_table` */

/*Table structure for table `lock_table` */

DROP TABLE IF EXISTS `lock_table`;

CREATE TABLE `lock_table` (
  `row_key` varchar(128) NOT NULL,
  `xid` varchar(96) DEFAULT NULL,
  `transaction_id` mediumtext,
  `branch_id` mediumtext,
  `resource_id` varchar(256) DEFAULT NULL,
  `table_name` varchar(32) DEFAULT NULL,
  `pk` varchar(36) DEFAULT NULL,
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`row_key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*Data for the table `lock_table` */

/*Table structure for table `undo_log` */

DROP TABLE IF EXISTS `undo_log`;

CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
然后在mysql中执行即可
现在打开file.conf文件,修改两处:

98-微服务项目的编写(下篇)_第49张图片

记得改成自己的数据源即可,注意改变url这个地方
很明显,他是操作我们创建的seata数据库的
再打开registry.conf:
修改如下:

98-微服务项目的编写(下篇)_第50张图片

注意:不要写localhost,因为他是在虚拟机的
所以需要我们本机的具体地址(在cmd命令提示符那里,输入ipconfig查看即可,比如我的就是192.168.164.1)
因为localhost在虚拟机那里就代表虚拟机了,而不是我们的本机了
很明显他也是操作了服务中心edu-eureka-boot(7001),而之所以这样
是为了确定并操作微服务的事务管理,在最后我会解释为什么要这样做,现在你先认为这样
启动:
先启动eureka
再启动seata的服务端
cd /opt/seata/bin/
./seata-server.sh -p 9099 -m db #如果出现了没有对应文件或者目录的错误,创建对应文件或者目录即可
#一般是没有目录或者说只需要创建目录就行,文件在启动时会自动创建
#且seata也有心跳,所以先启动seata也行

#ctrl+c退出(关闭seata)seata,这样在eureka里面就不会显示出来(出现)了
然后我们访问http://localhost:7001/,查看他是否注册(显示),如果出现了,则代表操作成功(即服务端启动成功)
配置客户端:
在账户微服务里加上如下依赖:

        
          <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-alibaba-seataartifactId>

        dependency>
        <dependency>
            <groupId>io.seatagroupId>
            <artifactId>seata-allartifactId>
            <version>1.2.0version>
        dependency>


 



然后在资源文件夹下,加上三个配置文件:
分别是file.conf,registry.conf,seata.conf(通常需要自己写)
其中file.conf和registry.conf可以去github示例项目中获取:
https://github.com/seata/seata-samples/tree/master/springboot-dubbo-seata/samples-business/src/main/resources
这里我直接给出具体内容,当然,可能随着时间的推移,内容会改变,所以最好看这地址里面的内容:
file.conf:
transport {
  # tcp udt unix-domain-socket
  type = "TCP"
  #NIO NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  # the client batch send request enable
  enableClientBatchSendRequest = true
  #thread factory for netty
  threadFactory {
    bossThreadPrefix = "NettyBoss"
    workerThreadPrefix = "NettyServerNIOWorker"
    serverExecutorThread-prefix = "NettyServerBizHandler"
    shareBossWorker = false
    clientSelectorThreadPrefix = "NettyClientSelector"
    clientSelectorThreadSize = 1
    clientWorkerThreadPrefix = "NettyClientWorkerThread"
    # netty boss thread size,will not be used for UDT
    bossThreadSize = 1
    #auto default pin or 8
    workerThreadSize = "default"
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}
service {
#vgroupMapping.自定义名称= "分布式seata applicationName"
#分布式seata applicationName.grouplist = "192.168.164.128:9099"

  # 自定义的事务组由 seata服务端 管理
  vgroupMapping.my_tx_group = "seata-server"
  # 服务端的ip和端口
  seata-server.grouplist = "192.168.164.128:9099"
  #degrade, current not support
  enableDegrade = false
  #disable seata
  disableGlobalTransaction = false
}

client {
  rm {
    asyncCommitBufferLimit = 10000
    lock {
      retryInterval = 10
      retryTimes = 30
      retryPolicyBranchRollbackOnConflict = true
    }
    reportRetryCount = 5
    tableMetaCheckEnable = false
    reportSuccessEnable = false
  }
  tm {
    commitRetryCount = 5
    rollbackRetryCount = 5
  }
  undo {
    dataValidation = true
    logSerialization = "jackson"
    logTable = "undo_log"
  }
  log {
    exceptionRate = 100
  }
}
registry.conf:
registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "eureka"  # 注册中心

  nacos {
    application = "seata-server"
    serverAddr = "localhost"
    namespace = ""
    username = ""
    password = ""
  }
  #这个地方地址要对,这里我们不用修改
  eureka {
    serviceUrl = "http://localhost:7001/eureka"   # 注册中心的地址
    application = "seata-server"  # 服务端名称
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = "0"
    password = ""
    timeout = "0"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3、springCloudConfig
  type = "file"

  nacos {
    serverAddr = "localhost"
    namespace = ""
    group = "SEATA_GROUP"
    username = ""
    password = ""
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    appId = "seata-server"
    apolloMeta = "http://192.168.1.204:8801"
    namespace = "application"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}
seata.conf:
client {
    application.id = test-account   # 客户端项目的名字
    transaction.service.group = my_tx_group   # 自定义的事务组
}
我们发现,定义了注册中心地址(为什么已经注册了,还需要定义呢,实际上他是若在seata服务端,那么认为是注册加上具体地址,但是若在客户端,则只认为是具体地址,为什么这样说,可以认为,我们id所在的事务组中,需要指定一个服务端在注册中心的名称,也就是说的具体地址当成操作区域),且定义了一个seata客户端项目名称(用作事务id)
然后定义了对应的事务组由seata管理(即seata-server,指定了其在服务器的地址)
且我们的事务id由该事务组管理,那么自然由seata管理,所以简单来说就是:
file.conf文件指定一个seata服务端地址,然后创建一个事务组给他管理,也就是提供了唯一操作者(事务组)
registry.conf文件,指定seata服务端在注册中心的名称,因为都在同一个注册中心
所以可以操作事务共享(需要对应的代理数据源)
也就是说,我们操作的具体地址都需要是一样的,这样,事务才可操作,也就是提供了唯一区域(没有区域怎么操作呢)
seata.conf文件,定义了对应微服务在指定事务组里面的唯一标识
因为同一个事务组里面是事务共享的,且在一个区域操作,导致我们可以操作事务共享(需要对应的代理数据源)
至此,我们完成了一个微服务,现在,我们在订单微服务和业务入口微服务里也加上对应的依赖
然后在资源文件夹下,都加上上面的三个文件吧(业务入口加上,是为了确定组名称以及区域,在后面的@GlobalTransactional注解就是这样的作用),即seata依赖会读取这三个文件,其中名称不要改变咯,否则可能操作不了了
唯一要改变的就是seata.conf文件的application.id属性,自己操作吧,
可能要修改启动类:
//如果需要多数据源,可以修改成
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) // 排除 数据库自动注入
//在前面的分片配置那里已经说明了他的作用了
//即主要是这个地址:https://blog.csdn.net/jinrucsdn/article/details/106539916

//这里我们并不需要多数据源,所以启动类不需要改变,但是如果需要后面的代理数据源的话,那么就需要加上了,来使得不会有多个实例(DataSource,没有操作@Primary之前得到的实例)
//当然,业务入口微服务不需要加,因为并不需要代理数据源


配置类(记得订单微服务和账户微服务都加上该类,业务入口微服务不需要,因为他并不操作数据库交互):
创建config.SeataConfig类:
package com.lagou.config;

import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

/**
 *
 */
@Configuration
public class SeataConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource") //可以从spring boot的对应的三个配置文件里取得以spring.datasource开头的对应属性和属性值,当然他肯定是spring boot的注解,看他的导入就知道了,他会操作一定的连接的依赖,比如他就会操作DruidDataSource,将对应属性和属性值设置为DruidDataSource对应的属性,方便我们不用手动设置了
    public DataSource druidDataSource(){
        DruidDataSource ds = new DruidDataSource();
        return ds;
    }
/*
从seata0.9 开始(包括0.9),下面的操作,可以自动完成的(但通常不能,所以最好加上),如果可行,那么也就是说,我们只需要编写上面的代码即可,如果那时候你非要编写下面的代码,可能是报错的(除非是seata0.9之前的版本),因为有两个对应的实例了(之所以可能报错,是因为可能会覆盖,但一般不会)
*/
@Primary
    @Bean("dataSource")
    public DataSourceProxy dataSource(DataSource druidDataSource){
        return new DataSourceProxy(druidDataSource);
    }
    /*
    DataSourceProxy是seata的类,因为导入:import io.seata.rm.datasource.DataSourceProxy;
    
    其中DataSourceProxy是DataSource的子类,为什么可以在0.9之前编写呢
    难道并不会出现相同注入的实例吗(父类可以指向子类),如果是手动的话,我们可以避免(注入的类变得更加子类即可),但是如果是操作框架(比如mybatis),他里面可能是固定需要DataSource的,那么会出现相同的实例了,那么他是为什么可以编写呢,这是因为@Primary注解的存在
    @Primary代表有相同实例时,不会使得报错,而是以该@Primary注解表示的实例为主
    但是如果对应的相同实例都加上@Primary,那么会报错(不能有多个相同的为主)
    否则不会(除了这里的DataSourceProxy,可能某些底层原因吧,导致不能有相同实例,无论是否加上@Primary)
*/
    
    
    /*
    注意:因为这里是手动的操作,所以我们可以加上
    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})来使得不操作自动配置
   这里最好加上
    因为他们的自动配置,一般也是连接池实例,即无论是否指定类型,都是同样的DataSource,所以如果不加的话,上面的参数里面的DataSource可以得到两个,从而启动报错或者找不到对应的客户端,一般都是直接启动报错,找不到对应客户端,即找不到事务id,可能是因为,后操作的原因,或者其他原因,具体可以百度,所以我们最好加上,实际上找不到对应客户端可能是因为是没有加载完毕,如果是这样,那么我们只需要多次执行或者等待执行即可,多次执行实际上也就是等等执行,只是执行当成等待时间了而已,虽然会导致服务器浪费点资源,但是结果更快出现,当然,因为是测试,所以一般是多次执行,否则在上线的项目中,最好是等待执行,除非你的资源足够,那么无论什么情况都可以多次执行
    但是上面是建立在他是一个实例的基础上,实际上大多数自动配置都只是创建对象,而不是创建实例,所以通常可以不加
    
    通常来说,没有指定类型的话
    一般默认是使用mybatis自带的连接池,也可以指定(在95章博客有操作指定druid的连接池)
    
    如果我们没有操作自动配置,那么mybatis使用的就是我们实例的数据源了,也就是不操作他的变量赋值产生的实例(通常是对象,而不是实例),而是我们手动的操作实例了
    一般默认代理也是操作DataSource的(因为是他DataSource的子类),所以上面才需要@Primary注解
    
    这里最好加上,因为虽然我们有事务id,事务组,和区域,但是我们也只是定义而已,其中,事务组和区域已经定义完毕
    事务id也定义了,但是如果我们操作事务,那么事务之间的共享,或者说事务的库需要指定吧,如果使用原来的连接
    他并不知道事务id是干什么的,也就是说,不会使用事务id,那么对应的回滚就不会影响到不加的微服务,所以就会改变数据库数据,而不会回滚了,而使用事务id的就会操作回滚(比如在后面操作完成后,可以试着将账户微服务不加,然后将错误代码放在主要代码之后,即在账户微服务和订单微服务之后,自然要在return之前,否则启动会报错的,然后重启访问,会发现,账户微服务操作了数据库,而订单微服务回滚了)
    而这个代理数据源DataSourceProxy就知道会使用事务id,以及知道自己是那个组和区域
    从而,决定了多个微服务直接事务共享的操作,只要一个事务id出现问题,该组进行回滚
    简单来说,他操作的数据源执行的语句,就会进行日志保存,从而操作事务,并且来决定对应的事务组的回滚
    那么对应事务id所对应的该代理数据源的日志事务会进行回滚了,从而使得操作了事务共享
    即他就是用来操作对应的日志交给事务组的(如可以回滚操作),但是产生日志需要事务id才可
    所以他们是一个整体,事务id也要,代理数据源也要,即事务id可以看成是一个mysql数据库的事务(这是平常来说的,具体来说的,就是他对应数据源的日志产生者)
    而对应的事务日志,就在对应数据库表里面(而不是mysql的自带的地方)
    所以简单来说,他们的事务,只是被事务组这个大事务包含了
    
    那么为什么事务组可以操作事务呢,那么就要说明sql的事务是如果操作的了
    你可以这样认为,我们只是将sql的事务的原理,在事务组里面实现了而已,只不过范围更大
    
    
    
    最后要注意:如果启动不了,可能是因为spring-cloud-starter-netflix-eureka-client依赖与这个代理数据源产生了冲突,在高版本下,通常会使得启动不了,比如boot和cloud的版本分别是2.4.2和2020.0.0,那么该依赖是3.0.0版本,该版本会与这个代理数据源发生冲突,如果是2.1.6.RELEASE和Greenwich.RELEASE组合,那么该依赖是2.1.0.RELEASE,那么通常不会发生冲突,但是我们又要使用该代理而不能不写这个:
    @Primary
    @Bean("dataSource")
    public DataSourceProxy dataSource(DataSource druidDataSource){
        return new DataSourceProxy(druidDataSource);
    }
    那么我们可以手动指定版本,即
    
			org.springframework.cloud
			spring-cloud-starter-netflix-eureka-client
			2.1.0.RELEASE
		
		
		从这里我们可以发现,新的版本(如2.4.2和2020.0.0)组合的冲突实在太多,而我们使用了2.1.6.RELEASE和Greenwich.RELEASE组合,基本没有什么大的问题,但是也要注意:如果对于的依赖不同,可能启动访问后,也是会报错的,比如
		
		
			org.springframework.cloud
			spring-cloud-starter-openfeign
			
		
		后面的不要加,否则启动后,访问可能会失败
		
			org.springframework.cloud
			spring-cloud-openfeign-core
			2.2.6.RELEASE
		
		
			org.springframework.cloud
			spring-cloud-starter-netflix-ribbon
			2.2.6.RELEASE
		
    */

    //至此,在出现新的组合或者依赖版本时,我们最好等他变成有RELEASE后缀,那么我们才可以尝试去使用
    //这样可以避免大多数的冲突
}
然后在订单微服务和账户微服务以及业务入口微服务的配置文件上,添加如下:
spring:
  cloud:
    alibaba:
      seata:
        tx-service-group: my_tx_group
        #再次的确认该组,否则可能操作不了,因为对应的事务组是放在eata服务端的,而这里指定操作那个事务组
        #而业务入口加上,是为了@GlobalTransactional注解,即也需要指定操作那个事务组,所以业务入口微服务,虽然并没有操作数据源,但对应的三个配置也要与他们(其他两个微服务)基本一样的,主要是给@GlobalTransactional注解的原因
事务id之间的事务共享是需要数据库的,也就是代理数据源DataSourceProxy需要一个数据库或者表来进行事务之间的共享
相当于我们事务操作的日志
我们在订单微服务和账户微服务对应的数据源的数据库里,执行如下sql语句:
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `branch_id` bigint(20) NOT NULL,
  `xid` varchar(100) NOT NULL,
  `context` varchar(128) NOT NULL,
  `rollback_info` longblob NOT NULL,
  `log_status` int(11) NOT NULL,
  `log_created` datetime NOT NULL,
  `log_modified` datetime NOT NULL,
  `ext` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
然后我们在业务入口微服务中,将BusinessServiceImpl类的该方法修改如下:
  @Override
    @GlobalTransactional //需要spring-cloud-alibaba-seata这个依赖,才可有使用该注解的操作
    public boolean business() {
        accountRemoteService.update();
		int i = 1/0;
        int save = orderRemoteService.save();


        return true;
    }
然后我们重启订单微服务和账户微服务以及业务入口微服务,然后访问localhost:8102/front/business
查看数据库,如果对应的数据没有修改,且报错信息是(/ by zero),那么也就是说操作了回滚,即分布式事务操作成功
接下来,我们来看看有没有多余的代码,首先删除对应三个微服务的如下配置:
cloud:
    alibaba:
      seata:
        tx-service-group: my_tx_group
然后将他们都重启,那么我经过测试,他们都需要加上,其中,业务入口的数据源和不操作自动配置都可以不加,因为并不操作
最后注意:可能访问时会有超时的错误,我们再次的执行即可,大概与电脑卡顿有关,因为他并不是错误,只是访问慢而已
最后,还要验证一下,seata是否有类似于心跳的机制,那么我们关闭seata服务端(在服务器上的)
然后再次的启动,再访问,如果回滚了,代表,他们之间的确有心跳的机制(在项目之间的心跳机制一般也代表循环获取,虽然大多数框架内的心跳机制是每隔一段时间发送一个包,在一定时间后,会断开,但项目之间是基本是没有断开的,cloud那里也有类似的这个,不如eureka的客户端和服务端)
经过测试,他们的确有心跳机制(基本是不断开的,可以自己测试)
这里也要提一下,最好不要在对应微服务里面加上@Transactional()操作事务,会冲突的
使得访问时会报错,即可能会导致找不到对应客户端,即找不到事务id的错误,直接报错,而不会执行sql语句
即执行到那里就报错了,这里还需要提一下,业务入口微服务实际本身也是一个事务id
即对应的三个配置不只是给@GlobalTransactional注解,也给了他自己的seata,其他两个也都给了自己的seata
如果他也操作数据源,那么对应的@GlobalTransactional操作的方法里面如果有自己对数据源的操作,那么也会认为在事务组里面
而不会发生冲突,所以真正的事务入口,实际上就是@GlobalTransactional注解,而不是该项目本身
且自己也可以操作数据源,当成其他两个微服务一样的,即不会冲突自己(也操作事务组回滚)
你可以试着将账户微服务的相关数据源操作放在业务入口微服务里面
然后再@GlobalTransactional注解的对应方法里面加上操作数据库的代码,然后重启,并访问
也会发现,进行了回滚,所以不会冲突(与是否同一个数据源无关,因为是看事务id的,虽然连接也基本不会相同)
但并不建议这样做,因为这样耦合度比较高,我们需要要具体分工,才好进行维护
所以一般有@GlobalTransactional注解的微服务只提供访问(用户访问他,然后他访问其他微服务,而不会让自己去操作数据源)
如果事务id相同怎么办,答:继续操作,相同并没有什么关系,可以认为他一个事务id指定多个数据源
虽然之前我们也认为一个事务id对应一个mysql,但是这是平常的
而实际上我们可以认为事务id只是操作mysql事务的日志产生者,所以实际上只是来记录日志的
而事务组就结合他们的日志(有时间顺序的)来进行回滚,所以事务id相同并没有什么关系,只是他一个人干了两个日志产生而已
至此,分布式事务的seata大致介绍完毕,具体的坑也大致说明完毕
实际上一个项目基本不可能将所有的技术进行覆盖,所以具体问题或者业务,只要有能够解决他们的技术,那么就使用即可
即具体问题,具体分析,然后具体使用技术

你可能感兴趣的:(笔记,apache,java,服务器,微服务项目,分布式事务)