1. 增加 XXL-Job starter

2. 迁移 pay 服务的 Job 逻辑
pull/4/MERGE
YunaiV 2020-11-30 18:47:57 +08:00
parent 04f53da686
commit efaeb5b39d
50 changed files with 655 additions and 439 deletions

View File

@ -34,6 +34,12 @@
> >
> 迫切希望,有前端能力不错的小伙伴,加入我们,一起来完善「芋道商城」。 > 迫切希望,有前端能力不错的小伙伴,加入我们,一起来完善「芋道商城」。
## 管理后台
体验传送门:<http://dashboard.shop.iocoder.cn>
![GIF 图-耐心等待](https://raw.githubusercontent.com/YunaiV/Blog/master/Mall/onemall-admin-min.gif)
## H5 商城 ## H5 商城
体验传送门:<http://h5.shop.iocoder.cn> 体验传送门:<http://h5.shop.iocoder.cn>
@ -42,14 +48,6 @@
![GIF 图-耐心等待](https://raw.githubusercontent.com/YunaiV/Blog/master/Mall/onemall-h5-min.gif) ![GIF 图-耐心等待](https://raw.githubusercontent.com/YunaiV/Blog/master/Mall/onemall-h5-min.gif)
## 管理后台
体验传送门:<http://dashboard.shop.iocoder.cn>
*2M 带宽小水管,访问略微有点慢*
![GIF 图-耐心等待](https://raw.githubusercontent.com/YunaiV/Blog/master/Mall/onemall-admin-min.gif)
## 其它演示 ## 其它演示
下面,我们会提供目前用到的中间件的管理平台。 下面,我们会提供目前用到的中间件的管理平台。
@ -81,10 +79,10 @@
**XXL-Job Console** **XXL-Job Console**
* 地址:<http://job.shop.iocoder.cn> * 地址:<http://xxl-job.shop.iocoder.cn>
* 管理员账号admin / 233666 * 管理员账号admin / 123456
> 教程:[《芋道 RocketMQ 安装部署》](http://www.iocoder.cn/XXL-JOB/install/?onemall) > 教程:[《芋道 XXL-Job 安装部署》](http://www.iocoder.cn/XXL-JOB/install/?onemall)
**Sentinel Console** **Sentinel Console**
@ -107,33 +105,35 @@ TODO 此处应有一个架构图的装逼 JPG 图。
| 模块 | 名称 | 端口 | | | 模块 | 名称 | 端口 | |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `admin-web` | 【前端】管理后台 | HTTP 8080 | | | [`admin-dashboard-vue`](https://github.com/YunaiV/onemall-web/tree/master/admin-dashboard-vue) | 【前端】管理后台 | HTTP 9527 | |
| `mobile-web` | 【前端】商城 H5 | HTTP 8000 | | | [`user-dashboard-vue`](https://github.com/YunaiV/onemall-web/tree/master/user-h5-vue) | 【前端】商城平台 | HTTP 8080 | |
| `system-application` | 管理员 HTTP 服务 | HTTP 18083 | [接口文档](http://api.shop.iocoder.cn/admin-api/doc.html) | | | | |
| `user-application` | 用户 HTTP 服务 | HTTP 18082 | [接口文档](http://api.shop.iocoder.cn/user-api/doc.html) | | | | |
| `product-application` | 商品 HTTP 服务 | HTTP 18081 | [接口文档](http://api.shop.iocoder.cn/product-api/doc.html) | | `management-web-app` | 【后端】管理平台 HTTP 服务 | HTTP 18083 | [接口文档](http://api-dashboard.shop.iocoder.cn/management-api/doc.html) |
| `pay-application` | 支付 HTTP 服务 | HTTP 18084 | [接口文档](http://api.shop.iocoder.cn/pay-api/doc.html) | | `shop-web-app` | 【后端】商城平台 HTTP 服务 | HTTP 18084 | [接口文档](http://api-h5.shop.iocoder.cn/shop-api/doc.html) |
| `promotion-application` | 促销 HTTP 服务 | HTTP 18085 | [接口文档](http://api.shop.iocoder.cn/promotion-api/doc.html) | | | | |
| `search-application` | 搜索 HTTP 服务 | HTTP 18086 | [接口文档](http://api.shop.iocoder.cn/search-api/doc.html) | | | | |
| `order-application` | 订单 HTTP 服务 | HTTP 18088 | [接口文档](http://api.shop.iocoder.cn/order-api/doc.html) | | `system-service-project` | 系统 RPC 服务 | 随机 |
| `user-service-project` | 用户 RPC 服务 | 随机 | |
| `promotion-service-project` | 营销 RPC 服务 | 随机 | |
| `pay-service-project` | 支付 RPC 服务 | 随机 | |
| `trade-service-project` | 交易 RPC 服务 | 随机 | |
| `product-service-project` | 商品 RPC 服务 | 随机 | |
| `search-service-project` | 搜索å RPC 服务 | 随机 | |
------- -------
后端项目,目前的项目结构如下: 后端项目,目前的项目结构如下:
```Java ```Java
[-] xxx [-] xxx-web-app // 提供对外 HTTP API。
├──[-] xxx-application // 提供对外 HTTP API 。
├──[-] xxx-service-api // 提供 Dubbo 服务 API 。 [-] xxx-service-project
├──[-] xxx-service-impl // 提供 Dubbo 服务 Service 实现。 ├──[-] xxx-service-api // 提供对内 RPC API 。
├──[-] xxx-service-app // 提供对内 RPC 实现。
├──[-] xxx-service-integration-test // 集成测试。
``` ```
考虑到大多数公司,无需拆分的特别细,并且过多 JVM 带来的服务器成本。所以目前的设定是:
* `xxx-service-impl` 内嵌在 `xxx-application` 中运行。
* MQ 消费者、定时器执行器,内嵌在 `xxx-service-impl` 中运行。
也就是说,一个 `xxx-application` 启动后,该模块就完整启动了。
## 技术栈 ## 技术栈
@ -165,8 +165,6 @@ TODO 此处应有一个架构图的装逼 JPG 图。
### 前端 ### 前端
商城 H5 和管理后台,分别采用了 Vue 和 React ,基于其适合的场景考虑。具体的,可以看看 [《为什么 React 比 Vue 更适合大型应用?》](https://www.zhihu.com/question/314761485/answer/615318460) 的讨论。
**商城 H5** **商城 H5**
| 框架 | 说明 | 版本 | | 框架 | 说明 | 版本 |
@ -178,8 +176,8 @@ TODO 此处应有一个架构图的装逼 JPG 图。
| 框架 | 说明 | 版本 | | 框架 | 说明 | 版本 |
| --- | --- | --- | | --- | --- | --- |
| [React](https://reactjs.org/) | JavaScript 框架 | 16.7.0 | | [Vue](https://cn.vuejs.org/index.html) | JavaScript 框架 | 2.5.17 |
| [Ant Design](https://ant.design/docs/react/introduce-cn) | React UI 组件库 | 3.13.0 | | [Vue Element Admin](https://ant.design/docs/react/introduce-cn) | 后台前端解决方案 | - |
### 监控 ### 监控

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>common</artifactId>
<groupId>cn.iocoder.mall</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>mall-spring-boot-starter-xxl-job</artifactId>
<dependencies>
<!-- Spring 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<optional>true</optional>
</dependency>
<!-- Job 相关 -->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,56 @@
package cn.iocoder.mall.xxljob.config;
import com.xxl.job.core.executor.XxlJobExecutor;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Objects;
/**
* XXL-Job
*/
@Configuration
@ConditionalOnClass(XxlJobSpringExecutor.class)
@ConditionalOnProperty(prefix = "xxl.job", name = "enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties({XxlJobProperties.class})
public class XxlJobAutoConfiguration {
private static final Logger LOGGER = LoggerFactory.getLogger(XxlJobAutoConfiguration.class);
private final XxlJobProperties properties;
public XxlJobAutoConfiguration(XxlJobProperties properties) {
this.properties = properties;
}
@Bean
@ConditionalOnMissingBean
public XxlJobExecutor xxlJobExecutor() {
LOGGER.info("初始化 XXL-Job 执行器的配置");
// 参数校验
XxlJobProperties.AdminProperties admin = this.properties.getAdmin();
XxlJobProperties.ExecutorProperties executor = this.properties.getExecutor();
Objects.requireNonNull(admin, "xxl job admin properties must not be null.");
Objects.requireNonNull(executor, "xxl job executor properties must not be null.");
// 初始化执行器
XxlJobExecutor xxlJobExecutor = new XxlJobSpringExecutor();
xxlJobExecutor.setIp(executor.getIp());
xxlJobExecutor.setPort(executor.getPort());
xxlJobExecutor.setAppname(executor.getAppName());
xxlJobExecutor.setLogPath(executor.getLogPath());
xxlJobExecutor.setLogRetentionDays(executor.getLogRetentionDays());
xxlJobExecutor.setAdminAddresses(admin.getAddresses());
xxlJobExecutor.setAccessToken(this.properties.getAccessToken());
return xxlJobExecutor;
}
}

View File

@ -0,0 +1,172 @@
package cn.iocoder.mall.xxljob.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* XXL-Job
*/
@ConfigurationProperties("xxl.job")
public class XxlJobProperties {
/**
* true
*/
private Boolean enabled = true;
/**
* 访
*/
private String accessToken;
/**
*
*/
private AdminProperties admin;
/**
*
*/
private ExecutorProperties executor;
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
if (enabled != null) {
this.enabled = enabled;
}
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
if (accessToken != null && accessToken.trim().length() > 0) {
this.accessToken = accessToken;
}
}
public AdminProperties getAdmin() {
return admin;
}
public void setAdmin(AdminProperties admin) {
this.admin = admin;
}
public ExecutorProperties getExecutor() {
return executor;
}
public void setExecutor(ExecutorProperties executor) {
this.executor = executor;
}
/**
* XXL-Job
*/
public static class AdminProperties {
/**
*
*/
private String addresses;
public String getAddresses() {
return addresses;
}
public void setAddresses(String addresses) {
this.addresses = addresses;
}
@Override
public String toString() {
return "AdminProperties{" +
"addresses='" + addresses + '\'' +
'}';
}
}
/**
* XXL-Job
*/
public static class ExecutorProperties {
/**
*
*
* 使 -1
*/
private static final Integer PORT_DEFAULT = -1;
/**
*
*
* -1
*/
private static final Integer LOG_RETENTION_DAYS_DEFAULT = -1;
/**
*
*/
private String appName;
/**
* IP
*/
private String ip;
/**
* Port
*/
private Integer port = PORT_DEFAULT;
/**
*
*/
private String logPath;
/**
*
*/
private Integer logRetentionDays = LOG_RETENTION_DAYS_DEFAULT;
public String getAppName() {
return appName;
}
public void setAppName(String appName) {
this.appName = appName;
}
public String getLogPath() {
return logPath;
}
public void setLogPath(String logPath) {
this.logPath = logPath;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
}
public Integer getLogRetentionDays() {
return logRetentionDays;
}
public void setLogRetentionDays(Integer logRetentionDays) {
this.logRetentionDays = logRetentionDays;
}
}
}

View File

@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.mall.xxljob.config.XxlJobAutoConfiguration

View File

@ -24,6 +24,7 @@
<module>mall-spring-boot-starter-dubbo</module> <module>mall-spring-boot-starter-dubbo</module>
<module>mall-spring-boot-starter-system-error-code</module> <module>mall-spring-boot-starter-system-error-code</module>
<module>mall-spring-boot-starter-rocketmq</module> <module>mall-spring-boot-starter-rocketmq</module>
<module>mall-spring-boot-starter-xxl-job</module>
</modules> </modules>
<dependencyManagement> <dependencyManagement>

View File

@ -43,9 +43,9 @@
<!-- RPC 相关 --> <!-- RPC 相关 -->
<dubbo.version>2.7.7</dubbo.version> <dubbo.version>2.7.7</dubbo.version>
<!-- MQ 相关 --> <!-- MQ 相关 -->
<rocketmq-spring-boot-starter.version>2.1.0</rocketmq-spring-boot-starter.version> <rocketmq-spring-boot-starter.version>2.1.1</rocketmq-spring-boot-starter.version>
<!-- Job 相关 --> <!-- Job 相关 -->
<xxl-job.version>2.0.1</xxl-job.version> <xxl-job.version>2.2.0</xxl-job.version>
<!-- Transaction 相关 --> <!-- Transaction 相关 -->
<seata.version>1.1.0</seata.version> <seata.version>1.1.0</seata.version>
<!-- 云服务相关 --> <!-- 云服务相关 -->
@ -249,6 +249,12 @@
<version>${xxl-job.version}</version> <version>${xxl-job.version}</version>
</dependency> </dependency>
<dependency>
<groupId>cn.iocoder.mall</groupId>
<artifactId>mall-spring-boot-starter-xxl-job</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba.nacos/nacos-client --> <!-- https://mvnrepository.com/artifact/com.alibaba.nacos/nacos-client -->
<!-- <dependency>--> <!-- <dependency>-->
<!-- <groupId>com.alibaba.nacos</groupId>--> <!-- <groupId>com.alibaba.nacos</groupId>-->

View File

@ -1,53 +0,0 @@
package cn.iocoder.mall.order.biz.dao.order;
import cn.iocoder.mall.order.biz.dataobject.OrderDO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
/**
* mapper
*
* @author Sin
* @time 2019-03-16 15:09
*/
@Repository
public interface OrderMapper extends BaseMapper<OrderDO> {
// /**
// * 更新 - 根据 id 更新
// *
// * @param orderDO
// * @return
// */
// int updateById(OrderDO orderDO);
//
// int updateByIdAndStatus(@Param("id") Integer id,
// @Param("status") Integer status,
// @Param("updateObj") OrderDO updateObj);
//
// /**
// * 查询 - 根据id 查询
// *
// * @param id
// * @return
// */
// OrderDO selectById(
// @Param("id") Integer id
// );
//
// /**
// * 查询 - 后台分页page
// *
// * @param orderQueryDTO
// * @return
// */
// int selectPageCount(OrderQueryDTO orderQueryDTO);
//
// /**
// * 查询 - 后台分页page
// *
// * @param orderQueryDTO
// * @return
// */
// List<OrderDO> selectPage(OrderQueryDTO orderQueryDTO);
}

View File

@ -17,28 +17,6 @@ import java.util.List;
@Repository @Repository
public interface OrderMapper extends BaseMapper<OrderDO> { public interface OrderMapper extends BaseMapper<OrderDO> {
/**
* - id
*
* @param orderDO
* @return
*/
int updateById(OrderDO orderDO);
int updateByIdAndStatus(@Param("id") Integer id,
@Param("status") Integer status,
@Param("updateObj") OrderDO updateObj);
/**
* - id
*
* @param id
* @return
*/
OrderDO selectById(
@Param("id") Integer id
);
/** /**
* - page * - page
* *

View File

@ -326,31 +326,6 @@ public class OrderServiceImpl implements OrderService {
return CommonResult.success(null); return CommonResult.success(null);
} }
@Override
public String updatePaySuccess(String orderId, Integer payAmount) {
OrderDO order = orderMapper.selectById(Integer.valueOf(orderId));
if (order == null) { // 订单不存在
return ServiceExceptionUtil.error(OrderErrorCodeEnum.ORDER_NOT_EXISTENT.getCode()).getMessage();
}
if (!order.getStatus().equals(OrderStatusEnum.WAITING_PAYMENT.getValue())) { // 状态不处于等待支付
return ServiceExceptionUtil.error(OrderErrorCodeEnum.ORDER_STATUS_NOT_WAITING_PAYMENT.getCode()).getMessage();
}
if (!order.getPresentPrice().equals(payAmount)) { // 支付金额不正确
return ServiceExceptionUtil.error(OrderErrorCodeEnum.ORDER_PAY_AMOUNT_ERROR.getCode()).getMessage();
}
// 更新 OrderDO 状态为已支付,等待发货
OrderDO updateOrderObj = new OrderDO()
.setStatus(OrderStatusEnum.WAIT_SHIPMENT.getValue())
.setPayAmount(payAmount)
.setPaymentTime(new Date());
int updateCount = orderMapper.updateByIdAndStatus(order.getId(), order.getStatus(), updateOrderObj);
if (updateCount <= 0) {
return ServiceExceptionUtil.error(OrderErrorCodeEnum.ORDER_STATUS_NOT_WAITING_PAYMENT.getCode()).getMessage();
}
// TODO FROM 芋艿 to 小范,把更新 OrderItem 给补全。
return "success";
}
@Override @Override
public CommonResult listenerConfirmGoods() { public CommonResult listenerConfirmGoods() {
return null; return null;

View File

@ -2,100 +2,6 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.iocoder.mall.order.biz.dao.order.OrderMapper"> <mapper namespace="cn.iocoder.mall.order.biz.dao.order.OrderMapper">
<sql id="FIELDS">
id, user_id, order_no, buy_price, discount_price, logistics_price, present_price, pay_amount,
payment_time, delivery_time, receiver_time, closing_time,
has_return_exchange,
status, remark, create_time, update_time, `deleted`
</sql>
<sql id="updateFieldSql" >
<set>
<if test="orderNo != null">
, order_no = #{orderNo}
</if>
<if test="buyPrice != null">
, buy_price = #{buyPrice}
</if>
<if test="discountPrice != null">
, discount_price = #{discountPrice}
</if>
<if test="logisticsPrice != null">
, logistics_price = #{logisticsPrice}
</if>
<if test="logisticsPrice != null">
, logistics_price = #{logisticsPrice}
</if>
<if test="presentPrice != null">
, present_price = #{presentPrice}
</if>
<if test="payAmount != null">
, pay_amount = #{payAmount}
</if>
<if test="deliveryTime != null">
, delivery_time = #{deliveryTime}
</if>
<if test="paymentTime != null">
, payment_time = #{paymentTime}
</if>
<if test="receiverTime != null">
, receiver_time = #{receiverTime}
</if>
<if test="closingTime != null">
, closing_time = #{closingTime}
</if>
<if test="hasReturnExchange != null">
, has_return_exchange = #{hasReturnExchange}
</if>
<if test="status != null">
, status = #{status}
</if>
<if test="remark != null">
, remark = #{remark}
</if>
<if test="deleted != null">
, `deleted` = #{deleted}
</if>
<if test="createTime != null">
, create_time = #{createTime}
</if>
<if test="updateTime != null">
, update_time = #{updateTime}
</if>
</set>
</sql>
<update id="updateById" parameterType="OrderDO">
UPDATE `orders`
<include refid="updateFieldSql" />
WHERE id = #{id}
</update>
<update id="updateByIdAndStatus">
UPDATE `orders`
<set>
<if test="updateObj.payAmount != null">
, pay_amount = #{updateObj.payAmount}
</if>
<if test="updateObj.paymentTime != null">
, payment_time = #{updateObj.paymentTime}
</if>
<if test="updateObj.status != null">
, status = #{updateObj.status}
</if>
</set>
WHERE id = #{id}
AND status = #{status}
</update>
<select id="selectById" resultType="cn.iocoder.mall.order.biz.dataobject.OrderDO">
SELECT
<include refid="FIELDS" />
FROM `orders`
WHERE id = #{id}
</select>
<sql id="selectWhere"> <sql id="selectWhere">
<if test="status != null"> <if test="status != null">
AND `status` = #{status} AND `status` = #{status}

View File

@ -1,47 +0,0 @@
package cn.iocoder.mall.pay.biz.config;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration
@Profile("dev")
public class XxlJobConfiguration {
private Logger logger = LoggerFactory.getLogger(XxlJobConfiguration.class);
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.executor.appname}")
private String appName;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;
@Bean(initMethod = "start", destroyMethod = "destroy")
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppName(appName);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
}

View File

@ -1,52 +0,0 @@
package cn.iocoder.mall.pay.biz.job;
import cn.iocoder.mall.pay.biz.dao.PayNotifyTaskMapper;
import cn.iocoder.mall.pay.biz.dataobject.PayNotifyTaskDO;
import cn.iocoder.mall.pay.biz.service.PayNotifyServiceImpl;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.IJobHandler;
import com.xxl.job.core.handler.annotation.JobHandler;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
/**
* Job
*/
@Component
@JobHandler(value = "payTransactionNotifyJob")
public class PayNotifyJob extends IJobHandler {
@Autowired
private PayNotifyTaskMapper payTransactionNotifyTaskMapper;
@Autowired
private PayNotifyServiceImpl payNotifyService;
@Resource
private RocketMQTemplate rocketMQTemplate;
@Override
public ReturnT<String> execute(String param) {
// 获得需要通知的任务
List<PayNotifyTaskDO> notifyTasks = payTransactionNotifyTaskMapper.selectByNotify();
// 循环任务,发送通知
for (PayNotifyTaskDO notifyTask : notifyTasks) {
// 发送 MQ
payNotifyService.sendNotifyMessage(notifyTask);
// 更新最后通知时间
// 1. 这样操作,虽然可能会出现 MQ 消费快于下面 PayTransactionNotifyTaskDO 的更新语句。但是,因为更新字段不同,所以不会有问题。
// 2. 换个视角,如果先更新 PayTransactionNotifyTaskDO ,再发送 MQ 消息。如果 MQ 消息发送失败,则 PayTransactionNotifyTaskDO 再也不会被轮询到了。
// 3. 当然,最最最完美的话,就是做事务消息,不过这样又过于复杂~
PayNotifyTaskDO updateNotifyTask = new PayNotifyTaskDO()
.setId(notifyTask.getId()).setLastExecuteTime(new Date());
payTransactionNotifyTaskMapper.update(updateNotifyTask);
}
return new ReturnT<>("执行通知数:" + notifyTasks.size());
}
}

View File

@ -80,7 +80,4 @@ public class PayTransactionServiceImpl implements PayTransactionService {
return null; return null;
} }
} }

View File

@ -1,12 +0,0 @@
# xxl-job
xxl:
job:
admin:
addresses: http://s1.iocoder.cn:18079/
executor:
appname: pay-job-executor
ip:
port: 0
logpath: /Users/yunai/logs/xxl-job/
logretentiondays: 1
accessToken:

View File

@ -1,32 +0,0 @@
import org.apache.dubbo.config.ApplicationConfig;
import org.apache.dubbo.config.ReferenceConfig;
import org.apache.dubbo.config.RegistryConfig;
import org.apache.dubbo.rpc.service.GenericService;
public class DubboGenericInvokerTest {
public static void main(String[] args) {
ApplicationConfig application = new ApplicationConfig();
application.setName("api-generic-consumer");
RegistryConfig registry = new RegistryConfig();
registry.setAddress("zookeeper://127.0.0.1:2181");
application.setRegistry(registry);
ReferenceConfig<GenericService> reference = new ReferenceConfig<>();
// 弱类型接口名
reference.setInterface("cn.iocoder.mall.order.api.OrderService");
// 声明为泛化接口
reference.setGeneric(true);
reference.setApplication(application);
// 用com.alibaba.dubbo.rpc.service.GenericService可以替代所有接口引用
GenericService genericService = reference.get();
String name = (String) genericService.$invoke("updatePaySuccess", new String[]{String.class.getName()}, new Object[]{"1"});
System.out.println(name);
}
}

View File

@ -37,6 +37,12 @@
<artifactId>mall-spring-boot-starter-rocketmq</artifactId> <artifactId>mall-spring-boot-starter-rocketmq</artifactId>
</dependency> </dependency>
<!-- Job 相关 -->
<dependency>
<groupId>cn.iocoder.mall</groupId>
<artifactId>mall-spring-boot-starter-xxl-job</artifactId>
</dependency>
<!-- Registry 和 Config 相关 --> <!-- Registry 和 Config 相关 -->
<dependency> <dependency>
<groupId>com.alibaba.cloud</groupId> <groupId>com.alibaba.cloud</groupId>

View File

@ -46,9 +46,15 @@ public class DubboReferencePool {
@Value("${dubbo.application.name}") @Value("${dubbo.application.name}")
private String dubboApplicationName; private String dubboApplicationName;
public ReferenceMeta getReferenceMeta(String notifyUrl) {
DubboReferencePool.ReferenceMeta referenceMeta = referenceMetaCache.getUnchecked(notifyUrl);
Assert.notNull(referenceMeta, String.format("notifyUrl(%s) 不存在对应的 ReferenceMeta 对象", notifyUrl));
return referenceMeta;
}
private ReferenceMeta createGenericService(String notifyUrl) { private ReferenceMeta createGenericService(String notifyUrl) {
// 使用 # 号分隔,格式为 服务名#方法名#版本号 // 使用 # 号分隔,格式为 服务名#方法名#版本号
List<String> notifyUrlParts = StringUtils.split(notifyUrl, "#"); List<String> notifyUrlParts = this.parseNotifyUrl(notifyUrl);
// 创建 ApplicationConfig 对象 // 创建 ApplicationConfig 对象
ApplicationConfig application = new ApplicationConfig(); ApplicationConfig application = new ApplicationConfig();
application.setName(dubboApplicationName); application.setName(dubboApplicationName);
@ -69,10 +75,9 @@ public class DubboReferencePool {
return new ReferenceMeta(reference, genericService, notifyUrlParts.get(1)); return new ReferenceMeta(reference, genericService, notifyUrlParts.get(1));
} }
public ReferenceMeta getReferenceMeta(String notifyUrl) { // TODO 芋艿,后续重构成一个对象
DubboReferencePool.ReferenceMeta referenceMeta = referenceMetaCache.getUnchecked(notifyUrl); private List<String> parseNotifyUrl(String notifyUrl) {
Assert.notNull(referenceMeta, String.format("notifyUrl(%s) 不存在对应的 ReferenceMeta 对象", notifyUrl)); return StringUtils.split(notifyUrl, "#");
return referenceMeta;
} }
} }

View File

@ -0,0 +1,12 @@
package cn.iocoder.mall.payservice.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* Spring Aop
*/
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
public class AopConfiguration {
}

View File

@ -4,6 +4,8 @@ import cn.iocoder.mall.payservice.dal.mysql.dataobject.notify.PayNotifyTaskDO;
import cn.iocoder.mall.payservice.mq.producer.message.PayRefundSuccessMessage; import cn.iocoder.mall.payservice.mq.producer.message.PayRefundSuccessMessage;
import cn.iocoder.mall.payservice.mq.producer.message.PayTransactionSuccessMessage; import cn.iocoder.mall.payservice.mq.producer.message.PayTransactionSuccessMessage;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers; import org.mapstruct.factory.Mappers;
@Mapper @Mapper
@ -11,8 +13,18 @@ public interface PayNotifyConvert {
PayNotifyConvert INSTANCE = Mappers.getMapper(PayNotifyConvert.class); PayNotifyConvert INSTANCE = Mappers.getMapper(PayNotifyConvert.class);
PayTransactionSuccessMessage convertTransaction(PayNotifyTaskDO payTransactionNotifyTaskDO); @Mappings({
@Mapping(source = "transaction.transactionId", target = "transactionId"),
@Mapping(source = "transaction.orderId", target = "orderId"),
})
PayTransactionSuccessMessage convertTransaction(PayNotifyTaskDO entity);
@Mappings({
@Mapping(source = "refund.transactionId", target = "transactionId"),
@Mapping(source = "refund.orderId", target = "orderId"),
@Mapping(source = "refund.refundId", target = "refundId"),
})
PayRefundSuccessMessage convertRefund(PayNotifyTaskDO entity);
PayRefundSuccessMessage convertRefund(PayNotifyTaskDO payTransactionNotifyTaskDO);
} }

View File

@ -5,7 +5,6 @@ import cn.iocoder.mall.payservice.dal.mysql.dataobject.transaction.PayTransactio
import cn.iocoder.mall.payservice.dal.mysql.dataobject.transaction.PayTransactionExtensionDO; import cn.iocoder.mall.payservice.dal.mysql.dataobject.transaction.PayTransactionExtensionDO;
import cn.iocoder.mall.payservice.enums.notify.PayNotifyStatusEnum; import cn.iocoder.mall.payservice.enums.notify.PayNotifyStatusEnum;
import cn.iocoder.mall.payservice.enums.notify.PayNotifyType; import cn.iocoder.mall.payservice.enums.notify.PayNotifyType;
import cn.iocoder.mall.payservice.service.transaction.PayTransactionService;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler; import com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler;
@ -56,25 +55,18 @@ public class PayNotifyTaskDO extends DeletableDO {
* {@link PayNotifyStatusEnum} * {@link PayNotifyStatusEnum}
*/ */
private Integer status; private Integer status;
/**
* MQ
*
* @see cn.iocoder.mall.payservice.job.notify.PayNotifyRetryJob
*/
private Boolean active;
/** /**
* *
*/ */
private Date nextNotifyTime; private Date nextNotifyTime;
/** /**
* *
*
* {@link #nextNotifyTime} 使
*
* 1. {@link PayTransactionService#updateTransactionPaySuccess(Integer, String)}
* nextNotifyTime + 15
* lastExecuteTime
* MQ
*
* 2. MQ lastExecuteTime
*
* 3. nextNotifyTime < lastExecuteTime
* nextNotifyTime + N N
* lastExecuteTime
*/ */
private Date lastExecuteTime; private Date lastExecuteTime;
/** /**

View File

@ -16,16 +16,21 @@ public interface PayNotifyTaskMapper extends BaseMapper<PayNotifyTaskDO> {
* *
* 1. status * 1. status
* 2. nextNotifyTime * 2. nextNotifyTime
* 3. lastExecuteTime > nextNotifyTime * 3. active false
* *
* @return PayTransactionNotifyTaskDO * @return PayTransactionNotifyTaskDO
*/ */
default List<PayNotifyTaskDO> selectListByNotify() { default List<PayNotifyTaskDO> selectListByNotify() {
return selectList(new QueryWrapper<PayNotifyTaskDO>() return selectList(new QueryWrapper<PayNotifyTaskDO>()
.in("status", PayNotifyStatusEnum.WAITING.getName(), PayNotifyStatusEnum.REQUEST_SUCCESS.getName(), .in("status", PayNotifyStatusEnum.WAITING.getStatus(), PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus(),
PayNotifyStatusEnum.REQUEST_FAILURE.getName()) PayNotifyStatusEnum.REQUEST_FAILURE.getStatus())
.le("next_notify_time", "NOW()") .le("next_notify_time", "NOW()")
.gt("last_execute_time", "next_notify_time")); .eq("active", Boolean.FALSE));
}
default int update(PayNotifyTaskDO update, Integer whereNotifyTimes) {
return update(update, new QueryWrapper<PayNotifyTaskDO>()
.eq("id", update.getId()).eq("notify_times", whereNotifyTimes));
} }
// //

View File

@ -0,0 +1,51 @@
package cn.iocoder.mall.payservice.job.notify;
import cn.iocoder.mall.payservice.dal.mysql.dataobject.notify.PayNotifyTaskDO;
import cn.iocoder.mall.payservice.dal.mysql.mapper.notify.PayNotifyTaskMapper;
import cn.iocoder.mall.payservice.service.notify.PayNotifyService;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.IJobHandler;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* Job
*
* RocketMQ Job {@link PayNotifyTaskDO#getNextNotifyTime()}
* MQ
*
* MQ {@link PayNotifyTaskDO} {@link PayNotifyTaskDO#getActive()}
*/
@Component
@Slf4j
public class PayNotifyRetryJob extends IJobHandler {
@Autowired
private PayNotifyTaskMapper payNotifyTaskMapper;
@Autowired
private PayNotifyService payNotifyService;
@Override
@XxlJob("payNotifyRetryJob")
public ReturnT<String> execute(String param) {
// 获得需要通知的任务
List<PayNotifyTaskDO> notifyTasks = payNotifyTaskMapper.selectListByNotify();
// 循环任务,发送通知
for (PayNotifyTaskDO notifyTask : notifyTasks) {
// 发送 MQ
payNotifyService.sendNotifyMessage(notifyTask);
// 标记任务执行中。考虑到 MQ 可能会存在先于该操作执行完,所以更新时,增加一个 notifyTimes 作为额外条件,避免覆盖更新的问题。
PayNotifyTaskDO updateNotifyTask = new PayNotifyTaskDO().setId(notifyTask.getId()).setActive(true);
payNotifyTaskMapper.update(updateNotifyTask, notifyTask.getNotifyTimes());
}
return new ReturnT<>("执行通知数:" + notifyTasks.size());
}
}

View File

@ -0,0 +1 @@
package cn.iocoder.mall.payservice.job;

View File

@ -2,6 +2,7 @@ package cn.iocoder.mall.payservice.mq.consumer;
import cn.iocoder.common.framework.util.DateUtil; import cn.iocoder.common.framework.util.DateUtil;
import cn.iocoder.common.framework.util.ExceptionUtil; import cn.iocoder.common.framework.util.ExceptionUtil;
import cn.iocoder.common.framework.vo.CommonResult;
import cn.iocoder.mall.payservice.common.dubbo.DubboReferencePool; import cn.iocoder.mall.payservice.common.dubbo.DubboReferencePool;
import cn.iocoder.mall.payservice.dal.mysql.dataobject.notify.PayNotifyLogDO; import cn.iocoder.mall.payservice.dal.mysql.dataobject.notify.PayNotifyLogDO;
import cn.iocoder.mall.payservice.dal.mysql.dataobject.notify.PayNotifyTaskDO; import cn.iocoder.mall.payservice.dal.mysql.dataobject.notify.PayNotifyTaskDO;
@ -10,14 +11,15 @@ import cn.iocoder.mall.payservice.dal.mysql.mapper.notify.PayNotifyTaskMapper;
import cn.iocoder.mall.payservice.enums.notify.PayNotifyStatusEnum; import cn.iocoder.mall.payservice.enums.notify.PayNotifyStatusEnum;
import cn.iocoder.mall.payservice.mq.producer.message.AbstractPayNotifySuccessMessage; import cn.iocoder.mall.payservice.mq.producer.message.AbstractPayNotifySuccessMessage;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.Map;
public abstract class AbstractPayNotifySuccessMQConsumer<T extends AbstractPayNotifySuccessMessage> implements RocketMQListener<T> { public abstract class AbstractPayNotifySuccessMQConsumer<T extends AbstractPayNotifySuccessMessage> {
// implements RocketMQListener<T> TODO 芋艿,理论来说,可以实现 RocketMQListener 接口,然后 execute 作为 onMessage 的具体实现。但是新版本貌似不行,后续在排查下;
@Autowired @Autowired
private DubboReferencePool dubboReferencePool; private DubboReferencePool dubboReferencePool;
@ -27,21 +29,22 @@ public abstract class AbstractPayNotifySuccessMQConsumer<T extends AbstractPayNo
@Autowired @Autowired
private PayNotifyLogMapper payTransactionNotifyLogMapper; private PayNotifyLogMapper payTransactionNotifyLogMapper;
@Override
@Transactional @Transactional
public void onMessage(T message) { public void execute(T message) {
// 获得 ReferenceMeta 对象
DubboReferencePool.ReferenceMeta referenceMeta = dubboReferencePool.getReferenceMeta(message.getNotifyUrl());
// 发起调用 // 发起调用
String response = null; // RPC / HTTP 调用的响应 CommonResult<Boolean> invokeResult = null; // RPC / HTTP 调用的响应
Throwable invokeException = null; //
PayNotifyTaskDO updateTask = new PayNotifyTaskDO() // 更新 PayTransactionNotifyTaskDO 对象 PayNotifyTaskDO updateTask = new PayNotifyTaskDO() // 更新 PayTransactionNotifyTaskDO 对象
.setId(message.getId()) .setId(message.getId())
.setActive(false) // 标记本地通知已经完成
.setLastExecuteTime(new Date()) .setLastExecuteTime(new Date())
.setNotifyTimes(message.getNotifyTimes() + 1); .setNotifyTimes(message.getNotifyTimes() + 1);
try { try {
// 获得 ReferenceMeta 对象
DubboReferencePool.ReferenceMeta referenceMeta = dubboReferencePool.getReferenceMeta(message.getNotifyUrl());
// TODO 芋艿,这里要优化下,不要在事务里,进行 RPC 调用 // TODO 芋艿,这里要优化下,不要在事务里,进行 RPC 调用
response = invoke(message, referenceMeta); invokeResult = invoke(message, referenceMeta);
if ("success".equals(response)) { // 情况一,请求成功且返回成功 if (invokeResult.isSuccess()) { // 情况一,请求成功且返回成功
// 更新通知成功 // 更新通知成功
updateTask.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus()); updateTask.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus());
payNotifyTaskMapper.updateById(updateTask); payNotifyTaskMapper.updateById(updateTask);
@ -53,8 +56,8 @@ public abstract class AbstractPayNotifySuccessMQConsumer<T extends AbstractPayNo
payNotifyTaskMapper.updateById(updateTask); payNotifyTaskMapper.updateById(updateTask);
} }
} catch (Throwable e) { // 请求失败 } catch (Throwable e) { // 请求失败
invokeException = e;
// 更新通知请求失败 // 更新通知请求失败
response = ExceptionUtil.getRootCauseMessage(e);
handleFailure(updateTask, PayNotifyStatusEnum.REQUEST_FAILURE.getStatus()); handleFailure(updateTask, PayNotifyStatusEnum.REQUEST_FAILURE.getStatus());
payNotifyTaskMapper.updateById(updateTask); payNotifyTaskMapper.updateById(updateTask);
// 抛出异常,回滚事务 // 抛出异常,回滚事务
@ -63,7 +66,9 @@ public abstract class AbstractPayNotifySuccessMQConsumer<T extends AbstractPayNo
} finally { } finally {
// 插入 PayTransactionNotifyLogDO 日志 // 插入 PayTransactionNotifyLogDO 日志
PayNotifyLogDO notifyLog = new PayNotifyLogDO().setNotifyId(message.getId()) PayNotifyLogDO notifyLog = new PayNotifyLogDO().setNotifyId(message.getId())
.setRequest(JSON.toJSONString(message)).setResponse(response).setStatus(updateTask.getStatus()); .setStatus(updateTask.getStatus())
.setRequest(JSON.toJSONString(message))
.setResponse(invokeResult != null ? JSON.toJSONString(invokeResult) : ExceptionUtil.getRootCauseMessage(invokeException));
payTransactionNotifyLogMapper.insert(notifyLog); payTransactionNotifyLogMapper.insert(notifyLog);
} }
} }
@ -77,8 +82,26 @@ public abstract class AbstractPayNotifySuccessMQConsumer<T extends AbstractPayNo
} }
} }
protected abstract String invoke(T message, DubboReferencePool.ReferenceMeta referenceMeta); protected abstract CommonResult<Boolean> invoke(T message, DubboReferencePool.ReferenceMeta referenceMeta);
protected abstract void afterInvokeSuccess(T message); protected abstract void afterInvokeSuccess(T message);
/**
* Dubbo CommonResult
*
* Dubbo CommonResult<Boolean>
*
* @param dubboResult Dubbo
* @return CommonResult
*/
protected static CommonResult<Boolean> parseDubboGenericResult(Object dubboResult) {
// TODO 芋艿,目前暂时这么实现,未来找下更合适的
Map<String, Object> dubboResultMap = (Map<String, Object>) dubboResult;
CommonResult<Boolean> commonResult = new CommonResult<>();
commonResult.setCode((Integer) dubboResultMap.get("code"));
commonResult.setMessage((String) dubboResultMap.get("message"));
commonResult.setData((Boolean) dubboResultMap.get("data"));
return commonResult;
}
} }

View File

@ -1,5 +1,6 @@
package cn.iocoder.mall.payservice.mq.consumer; package cn.iocoder.mall.payservice.mq.consumer;
import cn.iocoder.common.framework.vo.CommonResult;
import cn.iocoder.mall.payservice.common.dubbo.DubboReferencePool; import cn.iocoder.mall.payservice.common.dubbo.DubboReferencePool;
import cn.iocoder.mall.payservice.dal.mysql.dataobject.refund.PayRefundDO; import cn.iocoder.mall.payservice.dal.mysql.dataobject.refund.PayRefundDO;
import cn.iocoder.mall.payservice.dal.mysql.mapper.refund.PayRefundMapper; import cn.iocoder.mall.payservice.dal.mysql.mapper.refund.PayRefundMapper;
@ -25,15 +26,22 @@ public class PayRefundSuccessMQConsumer extends AbstractPayNotifySuccessMQConsum
private PayRefundMapper payRefundMapper; private PayRefundMapper payRefundMapper;
@Override @Override
protected String invoke(PayRefundSuccessMessage message, DubboReferencePool.ReferenceMeta referenceMeta) { public void onMessage(PayRefundSuccessMessage message) {
super.execute(message);
}
@Override
protected CommonResult<Boolean> invoke(PayRefundSuccessMessage message, DubboReferencePool.ReferenceMeta referenceMeta) {
// 查询支付交易 // 查询支付交易
PayRefundDO refund = payRefundMapper.selectById(message.getRefundId()); PayRefundDO refund = payRefundMapper.selectById(message.getRefundId());
Assert.notNull(refund, String.format("回调消息(%s) 退款单不能为空", message.toString())); Assert.notNull(refund, String.format("回调消息(%s) 退款单不能为空", message.toString()));
// 执行调用 // 执行调用
GenericService genericService = referenceMeta.getService(); GenericService genericService = referenceMeta.getService();
String methodName = referenceMeta.getMethodName(); String methodName = referenceMeta.getMethodName();
return (String) genericService.$invoke(methodName, new String[]{String.class.getName(), Integer.class.getName()}, Object dubboResult = genericService.$invoke(methodName,
new String[]{String.class.getName(), Integer.class.getName()},
new Object[]{message.getOrderId(), refund.getPrice()}); new Object[]{message.getOrderId(), refund.getPrice()});
return parseDubboGenericResult(dubboResult);
} }
@Override @Override

View File

@ -1,5 +1,6 @@
package cn.iocoder.mall.payservice.mq.consumer; package cn.iocoder.mall.payservice.mq.consumer;
import cn.iocoder.common.framework.vo.CommonResult;
import cn.iocoder.mall.payservice.common.dubbo.DubboReferencePool; import cn.iocoder.mall.payservice.common.dubbo.DubboReferencePool;
import cn.iocoder.mall.payservice.dal.mysql.dataobject.transaction.PayTransactionDO; import cn.iocoder.mall.payservice.dal.mysql.dataobject.transaction.PayTransactionDO;
import cn.iocoder.mall.payservice.dal.mysql.mapper.transaction.PayTransactionMapper; import cn.iocoder.mall.payservice.dal.mysql.mapper.transaction.PayTransactionMapper;
@ -25,15 +26,22 @@ public class PayTransactionSuccessMQConsumer extends AbstractPayNotifySuccessMQC
private PayTransactionMapper payTransactionMapper; private PayTransactionMapper payTransactionMapper;
@Override @Override
protected String invoke(PayTransactionSuccessMessage message, DubboReferencePool.ReferenceMeta referenceMeta) { public void onMessage(PayTransactionSuccessMessage message) {
super.execute(message);
}
@Override
protected CommonResult<Boolean> invoke(PayTransactionSuccessMessage message, DubboReferencePool.ReferenceMeta referenceMeta) {
// 查询支付交易 // 查询支付交易
PayTransactionDO transaction = payTransactionMapper.selectById(message.getTransactionId()); PayTransactionDO transaction = payTransactionMapper.selectById(message.getTransactionId());
Assert.notNull(transaction, String.format("回调消息(%s) 订单交易不能为空", message.toString())); Assert.notNull(transaction, String.format("回调消息(%s) 订单交易不能为空", message.toString()));
// 执行调用 // 执行调用
GenericService genericService = referenceMeta.getService(); GenericService genericService = referenceMeta.getService();
String methodName = referenceMeta.getMethodName(); String methodName = referenceMeta.getMethodName();
return (String) genericService.$invoke(methodName, new String[]{String.class.getName(), Integer.class.getName()}, Object dubboResult = genericService.$invoke(methodName,
new String[]{String.class.getName(), Integer.class.getName()},
new Object[]{message.getOrderId(), transaction.getPrice()}); new Object[]{message.getOrderId(), transaction.getPrice()});
return parseDubboGenericResult(dubboResult);
} }
@Override @Override

View File

@ -17,8 +17,7 @@ public class PayMQProducer {
@Autowired @Autowired
private RocketMQTemplate template; private RocketMQTemplate template;
public void sendPayRefundNotifyTaskMessage(PayRefundSuccessMessage message, Integer refundId, Integer transactionId, String orderId) { public void sendPayRefundNotifyTaskMessage(PayRefundSuccessMessage message) {
message.setRefundId(refundId).setTransactionId(transactionId).setOrderId(orderId);
try { try {
SendResult sendResult = template.syncSend(PayTransactionSuccessMessage.TOPIC, message); SendResult sendResult = template.syncSend(PayTransactionSuccessMessage.TOPIC, message);
if (!SendStatus.SEND_OK.equals(sendResult.getSendStatus())) { if (!SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {
@ -29,8 +28,7 @@ public class PayMQProducer {
} }
} }
public void sendPayTransactionNotifyTaskMessage(PayTransactionSuccessMessage message, Integer transactionId, String orderId) { public void sendPayTransactionNotifyTaskMessage(PayTransactionSuccessMessage message) {
message.setTransactionId(transactionId).setOrderId(orderId);
try { try {
SendResult sendResult = template.syncSend(PayTransactionSuccessMessage.TOPIC, message); SendResult sendResult = template.syncSend(PayTransactionSuccessMessage.TOPIC, message);
if (!SendStatus.SEND_OK.equals(sendResult.getSendStatus())) { if (!SendStatus.SEND_OK.equals(sendResult.getSendStatus())) {

View File

@ -1,5 +1,6 @@
package cn.iocoder.mall.payservice.service.notify; package cn.iocoder.mall.payservice.service.notify;
import cn.iocoder.mall.payservice.dal.mysql.dataobject.notify.PayNotifyTaskDO;
import cn.iocoder.mall.payservice.dal.mysql.dataobject.refund.PayRefundDO; import cn.iocoder.mall.payservice.dal.mysql.dataobject.refund.PayRefundDO;
import cn.iocoder.mall.payservice.dal.mysql.dataobject.transaction.PayTransactionDO; import cn.iocoder.mall.payservice.dal.mysql.dataobject.transaction.PayTransactionDO;
import cn.iocoder.mall.payservice.dal.mysql.dataobject.transaction.PayTransactionExtensionDO; import cn.iocoder.mall.payservice.dal.mysql.dataobject.transaction.PayTransactionExtensionDO;
@ -15,4 +16,7 @@ public interface PayNotifyService {
// TODO 芋艿:后续优化下,不要暴露 entity 出来 // TODO 芋艿:后续优化下,不要暴露 entity 出来
void addPayTransactionNotifyTask(PayTransactionDO transaction, PayTransactionExtensionDO extension); void addPayTransactionNotifyTask(PayTransactionDO transaction, PayTransactionExtensionDO extension);
// TODO 芋艿:后续优化下,不要暴露 entity 出来
void sendNotifyMessage(PayNotifyTaskDO notifyTask);
} }

View File

@ -39,8 +39,7 @@ public class PayNotifyServiceImpl implements PayNotifyService {
payNotifyTaskMapper.insert(payNotifyTaskDO); payNotifyTaskMapper.insert(payNotifyTaskDO);
// 发送 MQ 消息 // 发送 MQ 消息
payMQProducer.sendPayRefundNotifyTaskMessage(PayNotifyConvert.INSTANCE.convertRefund(payNotifyTaskDO), sendNotifyMessage(payNotifyTaskDO);
refund.getId(), refund.getTransactionId(), refund.getOrderId());
} }
@Override @Override
@ -54,14 +53,24 @@ public class PayNotifyServiceImpl implements PayNotifyService {
payNotifyTaskMapper.insert(payNotifyTaskDO); payNotifyTaskMapper.insert(payNotifyTaskDO);
// 发送 MQ 消息 // 发送 MQ 消息
payMQProducer.sendPayTransactionNotifyTaskMessage(PayNotifyConvert.INSTANCE.convertTransaction(payNotifyTaskDO), sendNotifyMessage(payNotifyTaskDO);
transaction.getId(), transaction.getOrderId()); }
@Override
public void sendNotifyMessage(PayNotifyTaskDO notifyTask) {
if (PayNotifyType.TRANSACTION.getType().equals(notifyTask.getType())) {
payMQProducer.sendPayTransactionNotifyTaskMessage(PayNotifyConvert.INSTANCE.convertTransaction(notifyTask));
} else if (PayNotifyType.REFUND.getType().equals(notifyTask.getType())) {
payMQProducer.sendPayRefundNotifyTaskMessage(PayNotifyConvert.INSTANCE.convertRefund(notifyTask));
} else {
throw new IllegalArgumentException(String.format("通知任务(%s) 无法发送通知消息", notifyTask.toString()));
}
} }
private PayNotifyTaskDO createBasePayNotifyTaskDO(String appId, String notifyUrl) { private PayNotifyTaskDO createBasePayNotifyTaskDO(String appId, String notifyUrl) {
return new PayNotifyTaskDO() return new PayNotifyTaskDO()
.setAppId(appId) .setAppId(appId)
.setStatus(PayNotifyStatusEnum.WAITING.getStatus()) .setStatus(PayNotifyStatusEnum.WAITING.getStatus()).setActive(true)
.setNotifyTimes(0).setMaxNotifyTimes(PayNotifyTaskDO.NOTIFY_FREQUENCY.length + 1) .setNotifyTimes(0).setMaxNotifyTimes(PayNotifyTaskDO.NOTIFY_FREQUENCY.length + 1)
.setNextNotifyTime(DateUtil.addDate(Calendar.SECOND, PayNotifyTaskDO.NOTIFY_FREQUENCY[0])) .setNextNotifyTime(DateUtil.addDate(Calendar.SECOND, PayNotifyTaskDO.NOTIFY_FREQUENCY[0]))
.setNotifyUrl(notifyUrl); .setNotifyUrl(notifyUrl);

View File

@ -1,7 +1,7 @@
spring: spring:
# 数据源配置项 # 数据源配置项
datasource: datasource:
url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_pay?useSSL=false&useUnicode=true&characterEncoding=UTF-8 url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_pay?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
driver-class-name: com.mysql.jdbc.Driver driver-class-name: com.mysql.jdbc.Driver
username: root username: root
password: 3WLiVUBEwTbvAfsh password: 3WLiVUBEwTbvAfsh
@ -19,3 +19,13 @@ dubbo:
registry: registry:
# address: spring-cloud://400-infra.server.iocoder.cn:8848 # 指定 Dubbo 服务注册中心的地址 # address: spring-cloud://400-infra.server.iocoder.cn:8848 # 指定 Dubbo 服务注册中心的地址
address: nacos://400-infra.server.iocoder.cn:8848?namespace=dev # 指定 Dubbo 服务注册中心的地址 address: nacos://400-infra.server.iocoder.cn:8848?namespace=dev # 指定 Dubbo 服务注册中心的地址
# XXL-Job 配置项
xxl:
job:
admin:
addresses: http://127.0.0.1:9099/
executor:
appname: ${spring.application.name}
logpath: /data/applogs/xxl-job/
accessToken:

View File

@ -1,7 +1,7 @@
spring: spring:
# 数据源配置项 # 数据源配置项
datasource: datasource:
url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_pay?useSSL=false&useUnicode=true&characterEncoding=UTF-8 url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_pay?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
driver-class-name: com.mysql.jdbc.Driver driver-class-name: com.mysql.jdbc.Driver
username: root username: root
password: 3WLiVUBEwTbvAfsh password: 3WLiVUBEwTbvAfsh
@ -22,3 +22,19 @@ dubbo:
# Dubbo 服务提供者的配置 # Dubbo 服务提供者的配置
provider: provider:
tag: ${DUBBO_TAG} # Dubbo 路由分组 tag: ${DUBBO_TAG} # Dubbo 路由分组
# XXL-Job 配置项
xxl:
job:
enabled: false # 本地开发时,关闭 XXL-Job
admin:
addresses: http://400-infra.server.iocoder.cn:9099
executor:
appname: ${spring.application.name}
accessToken:
# MyBatis Plus 配置
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 本地开发环境下,多打印 SQL 到控制台

View File

@ -0,0 +1,49 @@
package cn.iocoder.mall.payservice.common.dubbo;
import cn.iocoder.common.framework.vo.CommonResult;
import org.apache.dubbo.config.ApplicationConfig;
import org.apache.dubbo.config.ReferenceConfig;
import org.apache.dubbo.config.RegistryConfig;
import org.apache.dubbo.rpc.service.GenericService;
import java.util.Map;
public class DubboGenericInvokerTest {
public static void main(String[] args) {
ApplicationConfig application = new ApplicationConfig();
application.setName("api-generic-consumer");
RegistryConfig registry = new RegistryConfig();
registry.setAddress("nacos://400-infra.server.iocoder.cn:8848?namespace=dev");
application.setRegistry(registry);
ReferenceConfig<GenericService> reference = new ReferenceConfig<>();
// 弱类型接口名
reference.setInterface("cn.iocoder.mall.tradeservice.rpc.order.TradeOrderRpc");
reference.setVersion("1.0.0");
// 声明为泛化接口
reference.setGeneric(true);
reference.setApplication(application);
// 用com.alibaba.dubbo.rpc.service.GenericService可以替代所有接口引用
GenericService genericService = reference.get();
Object result = genericService.$invoke("updateTradeOrderPaySuccess",
new String[]{String.class.getName(), Integer.class.getName()},
new Object[]{"1", 100});
CommonResult<Boolean> commonResult = parseCommonResult((Map<String, Object>) result);
System.out.println(result);
}
private static CommonResult<Boolean> parseCommonResult(Map<String, Object> dubboResult) {
CommonResult<Boolean> commonResult = new CommonResult<>();
commonResult.setCode((Integer) dubboResult.get("code"));
commonResult.setMessage((String) dubboResult.get("message"));
commonResult.setData((Boolean) dubboResult.get("data"));
return commonResult;
}
}

View File

@ -0,0 +1 @@
package cn.iocoder.mall.payservice.common;

View File

@ -1,7 +1,7 @@
spring: spring:
# 数据源配置项 # 数据源配置项
datasource: datasource:
url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_product?useSSL=false&useUnicode=true&characterEncoding=UTF-8 url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_product?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
driver-class-name: com.mysql.jdbc.Driver driver-class-name: com.mysql.jdbc.Driver
username: root username: root
password: 3WLiVUBEwTbvAfsh password: 3WLiVUBEwTbvAfsh

View File

@ -1,7 +1,7 @@
spring: spring:
# 数据源配置项 # 数据源配置项
datasource: datasource:
url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_product?useSSL=false&useUnicode=true&characterEncoding=UTF-8 url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_product?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
driver-class-name: com.mysql.jdbc.Driver driver-class-name: com.mysql.jdbc.Driver
username: root username: root
password: 3WLiVUBEwTbvAfsh password: 3WLiVUBEwTbvAfsh

View File

@ -1,7 +1,7 @@
spring: spring:
# 数据源配置项 # 数据源配置项
datasource: datasource:
url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_promotion?useSSL=false&useUnicode=true&characterEncoding=UTF-8 url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_promotion?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
driver-class-name: com.mysql.jdbc.Driver driver-class-name: com.mysql.jdbc.Driver
username: root username: root
password: 3WLiVUBEwTbvAfsh password: 3WLiVUBEwTbvAfsh

View File

@ -1,7 +1,7 @@
spring: spring:
# 数据源配置项 # 数据源配置项
datasource: datasource:
url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_promotion?useSSL=false&useUnicode=true&characterEncoding=UTF-8 url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_promotion?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
driver-class-name: com.mysql.jdbc.Driver driver-class-name: com.mysql.jdbc.Driver
username: root username: root
password: 3WLiVUBEwTbvAfsh password: 3WLiVUBEwTbvAfsh

View File

@ -1,7 +1,7 @@
spring: spring:
# 数据源配置项 # 数据源配置项
datasource: datasource:
url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_system?useSSL=false&useUnicode=true&characterEncoding=UTF-8 url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_system?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
driver-class-name: com.mysql.jdbc.Driver driver-class-name: com.mysql.jdbc.Driver
username: root username: root
password: 3WLiVUBEwTbvAfsh password: 3WLiVUBEwTbvAfsh

View File

@ -1,7 +1,7 @@
spring: spring:
# 数据源配置项 # 数据源配置项
datasource: datasource:
url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_system?useSSL=false&useUnicode=true&characterEncoding=UTF-8 url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_system?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
driver-class-name: com.mysql.jdbc.Driver driver-class-name: com.mysql.jdbc.Driver
username: root username: root
password: 3WLiVUBEwTbvAfsh password: 3WLiVUBEwTbvAfsh

View File

@ -38,6 +38,17 @@ public interface TradeOrderRpc {
*/ */
CommonResult<PageResult<TradeOrderRespDTO>> pageTradeOrder(TradeOrderPageReqDTO pageDTO); CommonResult<PageResult<TradeOrderRespDTO>> pageTradeOrder(TradeOrderPageReqDTO pageDTO);
// TODO 芋艿:需要重构成入参是 DTO方便后续升级返回是 CommonResult用于返回失败的原因
/**
*
*
* pay-service
*
* @param tradeOrderId
* @param payAmount
* @return
*/
CommonResult<Boolean> updateTradeOrderPaySuccess(String tradeOrderId, Integer payAmount);
} }

View File

@ -20,4 +20,9 @@ public interface TradeOrderItemMapper extends BaseMapper<TradeOrderItemDO> {
return selectList(new QueryWrapper<TradeOrderItemDO>().in("order_id", orderIds)); return selectList(new QueryWrapper<TradeOrderItemDO>().in("order_id", orderIds));
} }
default int updateListByOrderId(TradeOrderItemDO update, Integer orderId, Integer whereStatus) {
return update(update, new QueryWrapper<TradeOrderItemDO>().eq("order_id", orderId)
.eq("status", whereStatus));
}
} }

View File

@ -4,6 +4,7 @@ import cn.iocoder.mall.mybatis.core.query.QueryWrapperX;
import cn.iocoder.mall.mybatis.core.util.PageUtil; import cn.iocoder.mall.mybatis.core.util.PageUtil;
import cn.iocoder.mall.tradeservice.dal.mysql.dataobject.order.TradeOrderDO; import cn.iocoder.mall.tradeservice.dal.mysql.dataobject.order.TradeOrderDO;
import cn.iocoder.mall.tradeservice.rpc.order.dto.TradeOrderPageReqDTO; import cn.iocoder.mall.tradeservice.rpc.order.dto.TradeOrderPageReqDTO;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@ -17,4 +18,9 @@ public interface TradeOrderMapper extends BaseMapper<TradeOrderDO> {
.eqIfPresent("status", pageReqDTO.getOrderStatus())); .eqIfPresent("status", pageReqDTO.getOrderStatus()));
} }
default int update(TradeOrderDO update, Integer whereOrderStatus) {
return update(update, new QueryWrapper<TradeOrderDO>()
.eq("id", update.getId()).eq("order_status", whereOrderStatus));
}
} }

View File

@ -37,4 +37,10 @@ public class TradeOrderRpcImpl implements TradeOrderRpc {
return success(tradeOrderService.pageTradeOrder(pageDTO)); return success(tradeOrderService.pageTradeOrder(pageDTO));
} }
@Override
public CommonResult<Boolean> updateTradeOrderPaySuccess(String tradeOrderId, Integer payAmount) {
tradeOrderService.updateTradeOrderPaySuccess(Integer.valueOf(tradeOrderId), payAmount);
return success(true);
}
} }

View File

@ -37,4 +37,12 @@ public interface TradeOrderService {
*/ */
PageResult<TradeOrderRespDTO> pageTradeOrder(TradeOrderPageReqDTO pageReqDTO); PageResult<TradeOrderRespDTO> pageTradeOrder(TradeOrderPageReqDTO pageReqDTO);
/**
*
*
* @param tradeOrderId
* @param payAmount
*/
void updateTradeOrderPaySuccess(Integer tradeOrderId, Integer payAmount);
} }

View File

@ -39,7 +39,7 @@ import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static cn.iocoder.common.framework.util.CollectionUtils.convertSet; import static cn.iocoder.common.framework.util.CollectionUtils.convertSet;
import static cn.iocoder.mall.tradeservice.enums.OrderErrorCodeConstants.ORDER_GET_GOODS_INFO_INCORRECT; import static cn.iocoder.mall.tradeservice.enums.OrderErrorCodeConstants.*;
import static cn.iocoder.mall.userservice.enums.UserErrorCodeConstants.USER_ADDRESS_NOT_FOUND; import static cn.iocoder.mall.userservice.enums.UserErrorCodeConstants.USER_ADDRESS_NOT_FOUND;
/** /**
@ -240,4 +240,40 @@ public class TradeOrderServiceImpl implements TradeOrderService {
return pageResult; return pageResult;
} }
@Override
@Transactional
public void updateTradeOrderPaySuccess(Integer tradeOrderId, Integer payAmount) {
// if (true) {
// throw new IllegalArgumentException("测试失败的情况");
// }
// 校验交易订单,是否可以
TradeOrderDO tradeOrderDO = tradeOrderMapper.selectById(tradeOrderId);
if (tradeOrderDO == null) { // 订单不存在
throw ServiceExceptionUtil.exception(ORDER_NOT_EXISTENT);
}
if (!tradeOrderDO.getOrderStatus().equals(TradeOrderStatusEnum.WAITING_PAYMENT.getValue())) { // 状态不处于等待支付
throw ServiceExceptionUtil.exception(ORDER_STATUS_NOT_WAITING_PAYMENT);
}
if (!tradeOrderDO.getPresentPrice().equals(payAmount)) { // 支付金额不正确
throw ServiceExceptionUtil.exception(ORDER_PAY_AMOUNT_ERROR);
}
// 更新 TradeOrderDO 状态为已支付,等待发货
TradeOrderDO updateOrderObj = new TradeOrderDO().setId(tradeOrderId)
.setOrderStatus(TradeOrderStatusEnum.WAIT_SHIPMENT.getValue())
.setPayPrice(payAmount)
.setPayTime(new Date());
int updateCount = tradeOrderMapper.update(updateOrderObj, TradeOrderStatusEnum.WAITING_PAYMENT.getValue());
if (updateCount <= 0) {
throw ServiceExceptionUtil.exception(ORDER_STATUS_NOT_WAITING_PAYMENT);
}
// 更新 TradeOrderItemDO 状态为已支付,等待发货
TradeOrderItemDO updateOrderItemObj = new TradeOrderItemDO()
.setStatus(TradeOrderStatusEnum.WAIT_SHIPMENT.getValue());
tradeOrderItemMapper.updateListByOrderId(updateOrderItemObj, tradeOrderId,
TradeOrderStatusEnum.WAITING_PAYMENT.getValue());
}
} }

View File

@ -1,7 +1,7 @@
spring: spring:
# 数据源配置项 # 数据源配置项
datasource: datasource:
url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_trade?useSSL=false&useUnicode=true&characterEncoding=UTF-8 url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_trade?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
driver-class-name: com.mysql.jdbc.Driver driver-class-name: com.mysql.jdbc.Driver
username: root username: root
password: 3WLiVUBEwTbvAfsh password: 3WLiVUBEwTbvAfsh

View File

@ -1,7 +1,7 @@
spring: spring:
# 数据源配置项 # 数据源配置项
datasource: datasource:
url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_trade?useSSL=false&useUnicode=true&characterEncoding=UTF-8 url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_trade?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
driver-class-name: com.mysql.jdbc.Driver driver-class-name: com.mysql.jdbc.Driver
username: root username: root
password: 3WLiVUBEwTbvAfsh password: 3WLiVUBEwTbvAfsh
@ -25,3 +25,8 @@ dubbo:
# Dubbo 服务提供者的配置 # Dubbo 服务提供者的配置
provider: provider:
tag: ${DUBBO_TAG} # Dubbo 路由分组 tag: ${DUBBO_TAG} # Dubbo 路由分组
# MyBatis Plus 配置
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 本地开发环境下,多打印 SQL 到控制台

View File

@ -1,7 +1,7 @@
spring: spring:
# 数据源配置项 # 数据源配置项
datasource: datasource:
url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_user?useSSL=false&useUnicode=true&characterEncoding=UTF-8 url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_user?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
driver-class-name: com.mysql.jdbc.Driver driver-class-name: com.mysql.jdbc.Driver
username: root username: root
password: 3WLiVUBEwTbvAfsh password: 3WLiVUBEwTbvAfsh

View File

@ -1,7 +1,7 @@
spring: spring:
# 数据源配置项 # 数据源配置项
datasource: datasource:
url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_user?useSSL=false&useUnicode=true&characterEncoding=UTF-8 url: jdbc:mysql://400-infra.server.iocoder.cn:3306/mall_user?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT
driver-class-name: com.mysql.jdbc.Driver driver-class-name: com.mysql.jdbc.Driver
username: root username: root
password: 3WLiVUBEwTbvAfsh password: 3WLiVUBEwTbvAfsh