Преглед на файлове

阿里云MNS修改为AMQP订阅

skyline преди 8 месеца
родител
ревизия
8345b37cbc

+ 4 - 2
admin-web/src/views/admin/account/index.vue

@@ -98,7 +98,7 @@
             :show-overflow-tooltip="!field.fixed&&field.width>150"
         >
           <template #default="{row}">
-            <template v-if="['rechargeAmount','amount','refundAmount','balance','frozenAmount','amountReceivable','amountReceived','discountAmount','refundDiscountAmount'].includes(field.prop)">
+            <template v-if="['rechargeAmount','amount','refundAmount','balance','rechargeBalance','grantsBalance','frozenAmount','amountReceivable','amountReceived','discountAmount','refundDiscountAmount'].includes(field.prop)">
               {{ u.fmt.fmtMoney(row[field.prop]) }}
             </template>
             <template v-else-if="'status'===field.prop">
@@ -172,7 +172,9 @@ const state = reactive({
       {label: '用户ID',width: 200,  prop: 'userId', resizable: true, fixed: 'left'},
       {label: '归属站点',width: 200,  prop: 'stationName', resizable: true, fixed: 'left'},
       {label: '手机号', width: 120, prop: 'mobilePhone', resizable: true, fixed: 'left'},
-      {label: '余额', width: 80, prop: 'balance', resizable: true, fixed: 'left'},
+      {label: '总余额', width: 80, prop: 'balance', resizable: true, fixed: 'left'},
+      {label: '充值余额', width: 90, prop: 'rechargeBalance', resizable: true, fixed: 'left'},
+      {label: '赠金余额', width: 90, prop: 'grantsBalance', resizable: true, fixed: 'left'},
       {label: '冻结余额', width: 90, prop: 'frozenAmount', resizable: true, fixed: 'left'},
       {label: '状态', width: 80, prop: 'status', align: 'center'},
       {label: '注册时间', width: 160, prop: 'registerTime', resizable: true},

+ 2 - 2
car-wash-admin/src/main/java/com/kym/admin/AdminApplication.java

@@ -2,7 +2,7 @@ package com.kym.admin;
 
 
 import cn.hutool.crypto.SecureUtil;
-import com.kym.service.mq.MnsHandler;
+import com.kym.service.aliyun.lot.AmqpConsumer;
 import org.mybatis.spring.annotation.MapperScan;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
@@ -18,7 +18,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
 @Controller
 @EnableScheduling
 @SpringBootApplication
-@ComponentScan(basePackages = {"com.kym"}, excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = MnsHandler.class))
+@ComponentScan(basePackages = {"com.kym"}, excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = AmqpConsumer.class))
 @MapperScan(basePackages = {"com.kym.mapper"})
 public class AdminApplication {
 

+ 7 - 7
car-wash-admin/src/main/resources/application.yml

@@ -103,10 +103,10 @@ password:
 
 aliyun:
   lot:
-    accessKey: LTAI5tNhD2KFuLUN1hMEukmS
-    accessSecret: dtVF6na8Hp9W8DmAoWI9k24VXwjNyM
-    consumerGroupId: DEFAULT_GROUP
-    iotInstanceId: iot-06z00hb4ys0z7ri
-    clientId: car-wash-01
-    ampq-host: iot-06z00hb4ys0z7ri.amqp.iothub.aliyuncs.com
-    mns-host: http://1757940634296846.mns.cn-shanghai.aliyuncs.com
+    amqp:
+      accessKey: LTAI5tPSodsNfiWEQxqDe4XT
+      accessSecret: xVswlKsGS6zHRixVwRjUmgGDuOxI2r
+      consumerGroupId: DEFAULT_GROUP
+      iotInstanceId: iot-06z00bnaqryq2gj
+      host: iot-06z00bnaqryq2gj.amqp.iothub.aliyuncs.com
+      clientId: wash-dev

+ 1 - 4
car-wash-entity/src/main/java/com/kym/entity/Account.java

@@ -1,13 +1,10 @@
 package com.kym.entity;
 
 import com.baomidou.mybatisplus.annotation.TableName;
-import com.kym.entity.BaseEntity;
 import lombok.Getter;
 import lombok.Setter;
 import lombok.experimental.Accessors;
 
-import java.io.Serializable;
-
 /**
  * <p>
  * 用户账户表
@@ -20,7 +17,7 @@ import java.io.Serializable;
 @Setter
 @TableName("t_account")
 @Accessors(chain = true)
-public class Account extends BaseEntity implements Serializable {
+public class Account extends BaseEntity {
 
     public static final int MIN_BALANCE = 200;
     /**

+ 1 - 1
car-wash-entity/src/main/java/com/kym/entity/awoara/MessageBody.java

@@ -18,7 +18,7 @@ public class MessageBody<T> {
      */
     private String messagetype;
     private String topic;
-    private Long messageid;
+    private String messageid;
     private Long timestamp;
 
 

+ 11 - 0
car-wash-entity/src/main/java/com/kym/entity/vo/CustomUserVo.java

@@ -38,7 +38,18 @@ public class CustomUserVo {
      */
     private int amountReceived;
     private int discountAmount;
+    /**
+     * 账户总余额
+     */
     private int balance;
+    /**
+     * 充值余额
+     */
+    private int rechargeBalance;
+    /**
+     * 赠金余额
+     */
+    private int grantsBalance;
     private int frozenAmount;
     private Long refundTimes;
     private int refundAmount;

+ 8 - 7
car-wash-miniapp/src/main/resources/application.yml

@@ -102,12 +102,13 @@ password:
 
 aliyun:
   lot:
-    accessKey: LTAI5tNhD2KFuLUN1hMEukmS
-    accessSecret: dtVF6na8Hp9W8DmAoWI9k24VXwjNyM
-    consumerGroupId: DEFAULT_GROUP
-    iotInstanceId: iot-06z00hb4ys0z7ri
-    clientId: car-wash-01
-    ampq-host: iot-06z00hb4ys0z7ri.amqp.iothub.aliyuncs.com
-    mns-host: http://1757940634296846.mns.cn-shanghai.aliyuncs.com
+    amqp:
+      accessKey: LTAI5tPSodsNfiWEQxqDe4XT
+      accessSecret: xVswlKsGS6zHRixVwRjUmgGDuOxI2r
+      consumerGroupId: DEFAULT_GROUP
+      iotInstanceId: iot-06z00bnaqryq2gj
+      host: iot-06z00bnaqryq2gj.amqp.iothub.aliyuncs.com
+      clientId: wash-dev
+
 
 

+ 6 - 0
car-wash-service/pom.xml

@@ -72,6 +72,12 @@
             <version>1.12.0</version>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.qpid</groupId>
+            <artifactId>qpid-jms-client</artifactId>
+            <version>0.57.0</version>
+        </dependency>
+
     </dependencies>
 
 </project>

+ 25 - 24
car-wash-service/src/main/java/com/kym/service/mq/AliyunLotClient.java → car-wash-service/src/main/java/com/kym/service/aliyun/lot/AliyunLotClient.java

@@ -1,11 +1,12 @@
-package com.kym.service.mq;
+package com.kym.service.aliyun.lot;
 
 import com.aliyun.iot20180120.models.*;
 import com.aliyun.tea.TeaModel;
-import com.kym.service.aliyun.lot.AliyunLotConfig;
-import jakarta.annotation.PostConstruct;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
 
 import java.util.Arrays;
 import java.util.Base64;
@@ -17,29 +18,18 @@ import java.util.List;
  * @author skyline
  */
 @Slf4j
-@Service
+@Component
 public class AliyunLotClient {
 
-    private static AliyunLotConfig aliyunLotConfig;
     private static com.aliyun.iot20180120.Client rRpcClient;
 
-    private static Base64.Encoder encoder = Base64.getEncoder();
-
-    public AliyunLotClient(AliyunLotConfig aliyunLotConfig) {
-        this.aliyunLotConfig = aliyunLotConfig;
-    }
-
-    /**
-     * 初始化IoT(Iot20180120)客户端
-     */
-    @PostConstruct
-    public void init() throws Exception {
-        com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
-                .setRegionId("cn-shanghai")
-                .setAccessKeyId(aliyunLotConfig.accessKey)
-                .setAccessKeySecret(aliyunLotConfig.accessSecret);
-        rRpcClient = new com.aliyun.iot20180120.Client(config);
-    }
+    private static final Base64.Encoder encoder = Base64.getEncoder();
+    @Value("${aliyun.lot.amqp.iotInstanceId}")
+    private static String iotInstanceId;
+    @Value("${aliyun.lot.amqp.accessKey}")
+    private String accessKey;
+    @Value("${aliyun.lot.amqp.accessSecret}")
+    private String accessSecret;
 
     /**
      * 调用Iot20180120客户端发送请求
@@ -89,7 +79,7 @@ public class AliyunLotClient {
 
     public static RRpcResponse rRpc(String productKey, String deviceName, String requestJson) throws Exception {
         var requestBase64Byte = new String(encoder.encode(requestJson.getBytes()));
-        var request = new RRpcRequest().setIotInstanceId(aliyunLotConfig.getIotInstanceId()).setProductKey(productKey).setDeviceName(deviceName).setRequestBase64Byte(requestBase64Byte);
+        var request = new RRpcRequest().setIotInstanceId(iotInstanceId).setProductKey(productKey).setDeviceName(deviceName).setRequestBase64Byte(requestBase64Byte);
         // 等待设备回复消息的时间,单位是毫秒,取值范围是1,000 ~8,000。
         // 校验integer型入参
         Integer iTimeout = 8000;
@@ -155,6 +145,17 @@ public class AliyunLotClient {
         log.info(com.aliyun.teautil.Common.toJSONString(TeaModel.buildMap(response.body)));
     }
 
+    /**
+     * 初始化IoT(Iot20180120)客户端
+     */
+    @EventListener(classes = {ContextRefreshedEvent.class})
+    public void init() throws Exception {
+        com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config()
+                .setRegionId("cn-shanghai")
+                .setAccessKeyId(accessKey)
+                .setAccessKeySecret(accessSecret);
+        rRpcClient = new com.aliyun.iot20180120.Client(config);
+    }
 
     void t1() {
         java.util.List<String> args = java.util.Arrays.asList("");

+ 2 - 2
car-wash-service/src/main/java/com/kym/service/aliyun/lot/AliyunLotConfig.java → car-wash-service/src/main/java/com/kym/service/aliyun/lot/AliyunLotMnsConfig.java

@@ -9,10 +9,10 @@ import org.springframework.context.annotation.Configuration;
  *
  * @author skyline
  */
-@ConfigurationProperties(prefix = "aliyun.lot")
+@ConfigurationProperties(prefix = "aliyun.lot.mns")
 @Configuration
 @Data
-public class AliyunLotConfig {
+public class AliyunLotMnsConfig {
     public String accessKey;
     public String accessSecret;
     public String consumerGroupId;

+ 0 - 5
car-wash-service/src/main/java/com/kym/service/aliyun/lot/AliyunLotService.java

@@ -1,5 +0,0 @@
-package com.kym.service.aliyun.lot;
-
-public interface AliyunLotService {
-
-}

+ 212 - 0
car-wash-service/src/main/java/com/kym/service/aliyun/lot/AmqpConsumer.java

@@ -0,0 +1,212 @@
+package com.kym.service.aliyun.lot;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.alibaba.fastjson2.TypeReference;
+import com.kym.entity.awoara.Event;
+import com.kym.entity.awoara.MessageBody;
+import com.kym.service.awoara.factory.AwoaraEventHandlerFactory;
+import jakarta.annotation.PreDestroy;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.text.StringEscapeUtils;
+import org.apache.qpid.jms.JmsConnection;
+import org.apache.qpid.jms.JmsConnectionListener;
+import org.apache.qpid.jms.message.JmsInboundMessageDispatch;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import javax.jms.*;
+import javax.naming.Context;
+import javax.naming.InitialContext;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * AMQP订阅消息处理
+ */
+@Component
+@Slf4j
+public class AmqpConsumer {
+    // 业务处理异步线程池
+    private final static ExecutorService executorService = new ThreadPoolExecutor(
+            Runtime.getRuntime().availableProcessors(),
+            Runtime.getRuntime().availableProcessors() * 2, 60, TimeUnit.SECONDS,
+            new LinkedBlockingQueue<>(50000));
+    private static final MessageListener MESSAGE_LISTENER = new MessageListener() {
+        @Override
+        public void onMessage(final Message message) {
+            try {
+                executorService.submit(() -> processMessage(message));
+            } catch (Exception e) {
+                log.error("submit task occurs exception ", e);
+            }
+        }
+    };
+    private static final JmsConnectionListener myJmsConnectionListener = new JmsConnectionListener() {
+        @Override
+        public void onConnectionEstablished(URI remoteURI) {
+            log.info("onConnectionEstablished, remoteUri:{}", remoteURI);
+        }
+
+        @Override
+        public void onConnectionFailure(Throwable error) {
+            log.error("onConnectionFailure, {}", error.getMessage());
+        }
+
+        @Override
+        public void onConnectionInterrupted(URI remoteURI) {
+            log.info("onConnectionInterrupted, remoteUri:{}", remoteURI);
+        }
+
+        @Override
+        public void onConnectionRestored(URI remoteURI) {
+            log.info("onConnectionRestored, remoteUri:{}", remoteURI);
+        }
+
+        @Override
+        public void onInboundMessage(JmsInboundMessageDispatch envelope) {
+        }
+
+        @Override
+        public void onSessionClosed(Session session, Throwable cause) {
+        }
+
+        @Override
+        public void onConsumerClosed(MessageConsumer consumer, Throwable cause) {
+        }
+
+        @Override
+        public void onProducerClosed(MessageProducer producer, Throwable cause) {
+        }
+    };
+    private final List<Connection> connections = new ArrayList<>();
+    @Value("${aliyun.lot.amqp.accessKey}")
+    private String accessKey;
+    @Value("${aliyun.lot.amqp.accessSecret}")
+    private String accessSecret;
+    @Value("${aliyun.lot.amqp.consumerGroupId}")
+    private String consumerGroupId;
+    @Value("${aliyun.lot.amqp.iotInstanceId}")
+    private String iotInstanceId;
+    @Value("${aliyun.lot.amqp.clientId}")
+    private String clientId;
+    @Value("${aliyun.lot.amqp.host}")
+    private String host;
+    private int connectionCount = 4;
+
+    private static void processMessage(Message message) {
+        try {
+            var jsonObject = new JSONObject();
+            byte[] body = message.getBody(byte[].class);
+            String content = new String(body);
+            String topic = message.getStringProperty("topic");
+            String messageId = message.getStringProperty("messageId");
+            long generateTime = message.getLongProperty("generateTime");
+            log.info("消息内容:{}", content);
+            var payload = JSONObject.parseObject(content);
+            jsonObject.put("payload", payload);
+            jsonObject.put("messagetype", "upload");
+            jsonObject.put("topic", topic);
+            jsonObject.put("messageid", messageId);
+            jsonObject.put("timestamp", generateTime);
+
+            var event = payload.getString("event");
+            var eventHandler = AwoaraEventHandlerFactory.getEventHandler(event);
+            var msg = parseMessageBody(jsonObject.toJSONString(), Event.getClazz(event));
+            eventHandler.handle(msg);
+        } catch (Exception e) {
+            log.error("processMessage occurs error ", e);
+        }
+    }
+
+    private static <T> MessageBody<T> parseMessageBody(String jsonStr, Class<T> clazz) {
+        var json = StringEscapeUtils.unescapeJava(jsonStr);
+        return JSONObject.parseObject(json, new TypeReference<>(clazz) {
+        });
+    }
+
+    private static String doSign(String toSignString, String secret, String signMethod) throws Exception {
+        SecretKeySpec signingKey = new SecretKeySpec(secret.getBytes(), signMethod);
+        Mac mac = Mac.getInstance(signMethod);
+        mac.init(signingKey);
+        byte[] rawHmac = mac.doFinal(toSignString.getBytes());
+        return Base64.encodeBase64String(rawHmac);
+    }
+
+    public void start() throws Exception {
+        for (int i = 0; i < connectionCount; i++) {
+            long timeStamp = System.currentTimeMillis();
+            String signMethod = "hmacsha1";
+
+            String userName = clientId + "-" + i + "|authMode=aksign"
+                    + ",signMethod=" + signMethod
+                    + ",timestamp=" + timeStamp
+                    + ",authId=" + accessKey
+                    + ",iotInstanceId=" + iotInstanceId
+                    + ",consumerGroupId=" + consumerGroupId
+                    + "|";
+
+            String signContent = "authId=" + accessKey + "&timestamp=" + timeStamp;
+            String password = doSign(signContent, accessSecret, signMethod);
+            String connectionUrl = "failover:(amqps://" + host + ":5671?amqp.idleTimeout=80000)" + "?failover.reconnectDelay=30";
+
+            Hashtable<String, String> hashtable = new Hashtable<>();
+            hashtable.put("connectionfactory.SBCF", connectionUrl);
+            hashtable.put("queue.QUEUE", "default");
+            hashtable.put(Context.INITIAL_CONTEXT_FACTORY, "org.apache.qpid.jms.jndi.JmsInitialContextFactory");
+            Context context = new InitialContext(hashtable);
+            ConnectionFactory cf = (ConnectionFactory) context.lookup("SBCF");
+            Destination queue = (Destination) context.lookup("QUEUE");
+
+            Connection connection = cf.createConnection(userName, password);
+            connections.add(connection);
+
+            ((JmsConnection) connection).addConnectionListener(myJmsConnectionListener);
+            Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
+            connection.start();
+
+            MessageConsumer consumer = session.createConsumer(queue);
+            consumer.setMessageListener(MESSAGE_LISTENER);
+        }
+
+        log.info("amqp is started successfully");
+    }
+
+    @EventListener(classes = {ContextRefreshedEvent.class})
+    @Async
+    public void init() throws Exception {
+        start();
+    }
+
+    @PreDestroy
+    public void shutdown() {
+        connections.forEach(connection -> {
+            try {
+                connection.close();
+            } catch (JMSException e) {
+                log.error("Failed to close connection", e);
+            }
+        });
+
+        executorService.shutdown();
+        try {
+            if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
+                executorService.shutdownNow();
+            }
+        } catch (InterruptedException e) {
+            executorService.shutdownNow();
+            Thread.currentThread().interrupt();
+        }
+    }
+}

+ 4 - 4
car-wash-service/src/main/java/com/kym/service/mq/MnsHandler.java → car-wash-service/src/main/java/com/kym/service/aliyun/lot/MnsHandler.java

@@ -1,4 +1,4 @@
-package com.kym.service.mq;
+package com.kym.service.aliyun.lot;
 
 import cn.hutool.core.util.CharsetUtil;
 import com.alibaba.fastjson2.JSONObject;
@@ -29,7 +29,7 @@ import java.util.concurrent.TimeUnit;
  *
  * @author skyline
  */
-@Component
+//@Component
 @Slf4j
 public class MnsHandler {
 
@@ -134,7 +134,7 @@ public class MnsHandler {
             log.info("message dequeue count:" + popMsg.getDequeueCount());
 
             try {
-                handleMessage(queue, popMsg);
+                handleMessage(popMsg);
                 queue.deleteMessage(popMsg.getReceiptHandle());
             } catch (Exception e) {
                 log.error("Failed to process message: " + popMsg.getMessageId(), e);
@@ -144,7 +144,7 @@ public class MnsHandler {
     }
 
 
-    static void handleMessage(CloudQueue queue, Message popMsg) {
+    static void handleMessage(Message popMsg) {
         var base64 = popMsg.getMessageBodyAsRawString();
         // 对messageBody进行base64解码
         var messageBodyStr = new String(decoder.decode(base64), CharsetUtil.CHARSET_UTF_8);

+ 1 - 1
car-wash-service/src/main/java/com/kym/service/awoara/AwoaraServiceImpl.java

@@ -16,7 +16,7 @@ import com.kym.entity.awoara.response.AwoaraResponse;
 import com.kym.entity.awoara.response.CreateOrder;
 import com.kym.entity.awoara.response.HardwareInfo;
 import com.kym.entity.awoara.response.State;
-import com.kym.service.mq.AliyunLotClient;
+import com.kym.service.aliyun.lot.AliyunLotClient;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 

+ 4 - 0
car-wash-service/src/main/java/com/kym/service/impl/UserServiceImpl.java

@@ -273,6 +273,8 @@ public class UserServiceImpl extends MPJBaseServiceImpl<UserMapper, User> implem
         // 用户余额,退款次数,退款金额
         var account = accountService.lambdaQuery().in(Account::getUserId, result.stream().map(CustomUserVo::getUserId).toList()).list();
         var user2Balance = account.stream().collect(Collectors.groupingBy(Account::getUserId, Collectors.summingInt(Account::getBalance)));
+        var user2rechargeBalance = account.stream().collect(Collectors.groupingBy(Account::getUserId, Collectors.summingInt(Account::getRechargeBalance)));
+        var user2GrantsBalance= account.stream().collect(Collectors.groupingBy(Account::getUserId, Collectors.summingInt(Account::getGrantsBalance)));
         var user2FrozenAmount = account.stream().collect(Collectors.groupingBy(Account::getUserId, Collectors.summingInt(Account::getFrozenAmount)));
         var refund = refundLogService.lambdaQuery().in(RefundLog::getUserId, result.stream().map(CustomUserVo::getUserId).toList()).list();
         // refund按照用户维度计算退款次数和退款总金额
@@ -283,6 +285,8 @@ public class UserServiceImpl extends MPJBaseServiceImpl<UserMapper, User> implem
         // 将用户余额,退款次数,退款金额放入result中
         var res = result.stream().peek(vo -> {
             vo.setBalance(user2Balance.getOrDefault(vo.getUserId(), 0));
+            vo.setRechargeBalance(user2rechargeBalance.getOrDefault(vo.getUserId(), 0));
+            vo.setGrantsBalance(user2GrantsBalance.getOrDefault(vo.getUserId(), 0));
             vo.setFrozenAmount(user2FrozenAmount.getOrDefault(vo.getUserId(), 0));
             vo.setRefundTimes(user2RefundTimes.getOrDefault(vo.getUserId(), 0L));
             vo.setRefundAmount(user2RefundAmount.getOrDefault(vo.getUserId(), 0));