1. 增加 XXL-Job starter

2. 迁移 pay 服务的 Job 逻辑
This commit is contained in:
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 商城
体验传送门:<http://h5.shop.iocoder.cn>
@ -42,14 +48,6 @@
![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**
* 地址:<http://job.shop.iocoder.cn>
* 管理员账号admin / 233666
* 地址:<http://xxl-job.shop.iocoder.cn>
* 管理员账号admin / 123456
> 教程:[《芋道 RocketMQ 安装部署》](http://www.iocoder.cn/XXL-JOB/install/?onemall)
> 教程:[《芋道 XXL-Job 安装部署》](http://www.iocoder.cn/XXL-JOB/install/?onemall)
**Sentinel Console**
@ -107,33 +105,35 @@ TODO 此处应有一个架构图的装逼 JPG 图。
| 模块 | 名称 | 端口 | |
| --- | --- | --- | --- |
| `admin-web` | 【前端】管理后台 | HTTP 8080 | |
| `mobile-web` | 【前端】商城 H5 | HTTP 8000 | |
| `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) |
| `pay-application` | 支付 HTTP 服务 | HTTP 18084 | [接口文档](http://api.shop.iocoder.cn/pay-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) |
| [`admin-dashboard-vue`](https://github.com/YunaiV/onemall-web/tree/master/admin-dashboard-vue) | 【前端】管理后台 | HTTP 9527 | |
| [`user-dashboard-vue`](https://github.com/YunaiV/onemall-web/tree/master/user-h5-vue) | 【前端】商城平台 | HTTP 8080 | |
| | | |
| | | |
| `management-web-app` | 【后端】管理平台 HTTP 服务 | HTTP 18083 | [接口文档](http://api-dashboard.shop.iocoder.cn/management-api/doc.html) |
| `shop-web-app` | 【后端】商城平台 HTTP 服务 | HTTP 18084 | [接口文档](http://api-h5.shop.iocoder.cn/shop-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
[-] xxx
├──[-] xxx-application // 提供对外 HTTP API 。
├──[-] xxx-service-api // 提供 Dubbo 服务 API 。
├──[-] xxx-service-impl // 提供 Dubbo 服务 Service 实现。
[-] xxx-web-app // 提供对外 HTTP API。
[-] xxx-service-project
├──[-] 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**
| 框架 | 说明 | 版本 |
@ -178,8 +176,8 @@ TODO 此处应有一个架构图的装逼 JPG 图。
| 框架 | 说明 | 版本 |
| --- | --- | --- |
| [React](https://reactjs.org/) | JavaScript 框架 | 16.7.0 |
| [Ant Design](https://ant.design/docs/react/introduce-cn) | React UI 组件库 | 3.13.0 |
| [Vue](https://cn.vuejs.org/index.html) | JavaScript 框架 | 2.5.17 |
| [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-system-error-code</module>
<module>mall-spring-boot-starter-rocketmq</module>
<module>mall-spring-boot-starter-xxl-job</module>
</modules>
<dependencyManagement>

View File

@ -43,9 +43,9 @@
<!-- RPC 相关 -->
<dubbo.version>2.7.7</dubbo.version>
<!-- 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 相关 -->
<xxl-job.version>2.0.1</xxl-job.version>
<xxl-job.version>2.2.0</xxl-job.version>
<!-- Transaction 相关 -->
<seata.version>1.1.0</seata.version>
<!-- 云服务相关 -->
@ -249,6 +249,12 @@
<version>${xxl-job.version}</version>
</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 -->
<!-- <dependency>-->
<!-- <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
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
*

View File

@ -326,31 +326,6 @@ public class OrderServiceImpl implements OrderService {
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
public CommonResult listenerConfirmGoods() {
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">
<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">
<if test="status != null">
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;
}
}

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>
</dependency>
<!-- Job 相关 -->
<dependency>
<groupId>cn.iocoder.mall</groupId>
<artifactId>mall-spring-boot-starter-xxl-job</artifactId>
</dependency>
<!-- Registry 和 Config 相关 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>

View File

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

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.PayTransactionSuccessMessage;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
@Mapper
@ -11,8 +13,18 @@ public interface PayNotifyConvert {
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.enums.notify.PayNotifyStatusEnum;
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.TableName;
import com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler;
@ -56,25 +55,18 @@ public class PayNotifyTaskDO extends DeletableDO {
* 外键 {@link PayNotifyStatusEnum}
*/
private Integer status;
/**
* 是否激活中即处于正在 MQ 异步通知中
*
* @see cn.iocoder.mall.payservice.job.notify.PayNotifyRetryJob
*/
private Boolean active;
/**
* 下一次通知时间
*/
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;
/**

View File

@ -16,16 +16,21 @@ public interface PayNotifyTaskMapper extends BaseMapper<PayNotifyTaskDO> {
*
* 1. status 非成功
* 2. nextNotifyTime 小于当前时间
* 3. lastExecuteTime > nextNotifyTime
* 3. active false 并未正在执行中
*
* @return PayTransactionNotifyTaskDO 数组
*/
default List<PayNotifyTaskDO> selectListByNotify() {
return selectList(new QueryWrapper<PayNotifyTaskDO>()
.in("status", PayNotifyStatusEnum.WAITING.getName(), PayNotifyStatusEnum.REQUEST_SUCCESS.getName(),
PayNotifyStatusEnum.REQUEST_FAILURE.getName())
.in("status", PayNotifyStatusEnum.WAITING.getStatus(), PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus(),
PayNotifyStatusEnum.REQUEST_FAILURE.getStatus())
.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.ExceptionUtil;
import cn.iocoder.common.framework.vo.CommonResult;
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.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.mq.producer.message.AbstractPayNotifySuccessMessage;
import com.alibaba.fastjson.JSON;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import java.util.Calendar;
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
private DubboReferencePool dubboReferencePool;
@ -27,21 +29,22 @@ public abstract class AbstractPayNotifySuccessMQConsumer<T extends AbstractPayNo
@Autowired
private PayNotifyLogMapper payTransactionNotifyLogMapper;
@Override
@Transactional
public void onMessage(T message) {
// 获得 ReferenceMeta 对象
DubboReferencePool.ReferenceMeta referenceMeta = dubboReferencePool.getReferenceMeta(message.getNotifyUrl());
public void execute(T message) {
// 发起调用
String response = null; // RPC / HTTP 调用的响应
CommonResult<Boolean> invokeResult = null; // RPC / HTTP 调用的响应
Throwable invokeException = null; //
PayNotifyTaskDO updateTask = new PayNotifyTaskDO() // 更新 PayTransactionNotifyTaskDO 对象
.setId(message.getId())
.setActive(false) // 标记本地通知已经完成
.setLastExecuteTime(new Date())
.setNotifyTimes(message.getNotifyTimes() + 1);
try {
// 获得 ReferenceMeta 对象
DubboReferencePool.ReferenceMeta referenceMeta = dubboReferencePool.getReferenceMeta(message.getNotifyUrl());
// TODO 芋艿这里要优化下不要在事务里进行 RPC 调用
response = invoke(message, referenceMeta);
if ("success".equals(response)) { // 情况一请求成功且返回成功
invokeResult = invoke(message, referenceMeta);
if (invokeResult.isSuccess()) { // 情况一请求成功且返回成功
// 更新通知成功
updateTask.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus());
payNotifyTaskMapper.updateById(updateTask);
@ -53,8 +56,8 @@ public abstract class AbstractPayNotifySuccessMQConsumer<T extends AbstractPayNo
payNotifyTaskMapper.updateById(updateTask);
}
} catch (Throwable e) { // 请求失败
invokeException = e;
// 更新通知请求失败
response = ExceptionUtil.getRootCauseMessage(e);
handleFailure(updateTask, PayNotifyStatusEnum.REQUEST_FAILURE.getStatus());
payNotifyTaskMapper.updateById(updateTask);
// 抛出异常回滚事务
@ -63,7 +66,9 @@ public abstract class AbstractPayNotifySuccessMQConsumer<T extends AbstractPayNo
} finally {
// 插入 PayTransactionNotifyLogDO 日志
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);
}
}
@ -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);
/**
* 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;
import cn.iocoder.common.framework.vo.CommonResult;
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.mapper.refund.PayRefundMapper;
@ -25,15 +26,22 @@ public class PayRefundSuccessMQConsumer extends AbstractPayNotifySuccessMQConsum
private PayRefundMapper payRefundMapper;
@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());
Assert.notNull(refund, String.format("回调消息(%s) 退款单不能为空", message.toString()));
// 执行调用
GenericService genericService = referenceMeta.getService();
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()});
return parseDubboGenericResult(dubboResult);
}
@Override

View File

@ -1,5 +1,6 @@
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.dal.mysql.dataobject.transaction.PayTransactionDO;
import cn.iocoder.mall.payservice.dal.mysql.mapper.transaction.PayTransactionMapper;
@ -25,15 +26,22 @@ public class PayTransactionSuccessMQConsumer extends AbstractPayNotifySuccessMQC
private PayTransactionMapper payTransactionMapper;
@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());
Assert.notNull(transaction, String.format("回调消息(%s) 订单交易不能为空", message.toString()));
// 执行调用
GenericService genericService = referenceMeta.getService();
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()});
return parseDubboGenericResult(dubboResult);
}
@Override

View File

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

View File

@ -1,5 +1,6 @@
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.transaction.PayTransactionDO;
import cn.iocoder.mall.payservice.dal.mysql.dataobject.transaction.PayTransactionExtensionDO;
@ -15,4 +16,7 @@ public interface PayNotifyService {
// TODO 芋艿后续优化下不要暴露 entity 出来
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);
// 发送 MQ 消息
payMQProducer.sendPayRefundNotifyTaskMessage(PayNotifyConvert.INSTANCE.convertRefund(payNotifyTaskDO),
refund.getId(), refund.getTransactionId(), refund.getOrderId());
sendNotifyMessage(payNotifyTaskDO);
}
@Override
@ -54,14 +53,24 @@ public class PayNotifyServiceImpl implements PayNotifyService {
payNotifyTaskMapper.insert(payNotifyTaskDO);
// 发送 MQ 消息
payMQProducer.sendPayTransactionNotifyTaskMessage(PayNotifyConvert.INSTANCE.convertTransaction(payNotifyTaskDO),
transaction.getId(), transaction.getOrderId());
sendNotifyMessage(payNotifyTaskDO);
}
@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) {
return new PayNotifyTaskDO()
.setAppId(appId)
.setStatus(PayNotifyStatusEnum.WAITING.getStatus())
.setStatus(PayNotifyStatusEnum.WAITING.getStatus()).setActive(true)
.setNotifyTimes(0).setMaxNotifyTimes(PayNotifyTaskDO.NOTIFY_FREQUENCY.length + 1)
.setNextNotifyTime(DateUtil.addDate(Calendar.SECOND, PayNotifyTaskDO.NOTIFY_FREQUENCY[0]))
.setNotifyUrl(notifyUrl);

View File

@ -1,7 +1,7 @@
spring:
# 数据源配置项
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
username: root
password: 3WLiVUBEwTbvAfsh
@ -19,3 +19,13 @@ dubbo:
registry:
# address: spring-cloud://400-infra.server.iocoder.cn:8848 # 指定 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:
# 数据源配置项
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
username: root
password: 3WLiVUBEwTbvAfsh
@ -22,3 +22,19 @@ dubbo:
# Dubbo 服务提供者的配置
provider:
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:
# 数据源配置项
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
username: root
password: 3WLiVUBEwTbvAfsh

View File

@ -1,7 +1,7 @@
spring:
# 数据源配置项
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
username: root
password: 3WLiVUBEwTbvAfsh

View File

@ -1,7 +1,7 @@
spring:
# 数据源配置项
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
username: root
password: 3WLiVUBEwTbvAfsh

View File

@ -1,7 +1,7 @@
spring:
# 数据源配置项
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
username: root
password: 3WLiVUBEwTbvAfsh

View File

@ -1,7 +1,7 @@
spring:
# 数据源配置项
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
username: root
password: 3WLiVUBEwTbvAfsh

View File

@ -1,7 +1,7 @@
spring:
# 数据源配置项
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
username: root
password: 3WLiVUBEwTbvAfsh

View File

@ -38,6 +38,17 @@ public interface TradeOrderRpc {
*/
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));
}
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.tradeservice.dal.mysql.dataobject.order.TradeOrderDO;
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.metadata.IPage;
import org.springframework.stereotype.Repository;
@ -17,4 +18,9 @@ public interface TradeOrderMapper extends BaseMapper<TradeOrderDO> {
.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));
}
@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);
/**
* 更新交易订单支付成功
*
* @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 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;
/**
@ -240,4 +240,40 @@ public class TradeOrderServiceImpl implements TradeOrderService {
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:
# 数据源配置项
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
username: root
password: 3WLiVUBEwTbvAfsh

View File

@ -1,7 +1,7 @@
spring:
# 数据源配置项
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
username: root
password: 3WLiVUBEwTbvAfsh
@ -25,3 +25,8 @@ dubbo:
# Dubbo 服务提供者的配置
provider:
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:
# 数据源配置项
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
username: root
password: 3WLiVUBEwTbvAfsh

View File

@ -1,7 +1,7 @@
spring:
# 数据源配置项
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
username: root
password: 3WLiVUBEwTbvAfsh