From b6897738ce7f217cf8f6a62e6fe796a5a3f0bd62 Mon Sep 17 00:00:00 2001 From: wangfukang <15630117759@163.com> Date: Tue, 19 May 2026 15:47:30 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E6=8E=A5=E6=8B=BC=E5=9B=A2=E6=95=B0?= =?UTF-8?q?=E6=8D=AE1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application.yml | 1 + hiver-admin/test-output/test-report.html | 16 +- hiver-modules/hiver-mall/pom.xml | 5 + .../mall/controller/ShopAreaController.java | 20 +- .../java/cc/hiver/mall/dao/ShopAreaDao.java | 21 +- .../java/cc/hiver/mall/entity/MallOrder.java | 2 +- .../mall/ie/config/IeRedisPubSubConfig.java | 34 + .../mall/ie/config/IeWebSocketConfig.java | 25 + .../mall/ie/constant/IeChatEventType.java | 14 + .../hiver/mall/ie/constant/IeConstants.java | 23 + .../cc/hiver/mall/ie/constant/IeRedisKey.java | 64 ++ .../mall/ie/controller/IeAdminController.java | 27 + .../mall/ie/controller/IeController.java | 149 +++++ .../cc/hiver/mall/ie/dto/IeMatchStartDTO.java | 16 + .../cc/hiver/mall/ie/dto/IePresenceDTO.java | 10 + .../cc/hiver/mall/ie/dto/IeProfileDTO.java | 18 + .../cc/hiver/mall/ie/dto/IeReportDTO.java | 12 + .../hiver/mall/ie/dto/IeRoomMessageDTO.java | 15 + .../cc/hiver/mall/ie/dto/IeStatusDTO.java | 15 + .../cc/hiver/mall/ie/entity/IeBaseEntity.java | 26 + .../java/cc/hiver/mall/ie/entity/IeBlock.java | 14 + .../mall/ie/entity/IeContentAuditLog.java | 20 + .../hiver/mall/ie/entity/IeMatchAttempt.java | 22 + .../hiver/mall/ie/entity/IePresenceEvent.java | 15 + .../cc/hiver/mall/ie/entity/IeRecord.java | 29 + .../cc/hiver/mall/ie/entity/IeReport.java | 19 + .../java/cc/hiver/mall/ie/entity/IeRoom.java | 23 + .../hiver/mall/ie/entity/IeRoomMessage.java | 34 + .../hiver/mall/ie/entity/IeSensitiveWord.java | 16 + .../hiver/mall/ie/entity/IeUserProfile.java | 30 + .../cc/hiver/mall/ie/entity/IeUserStatus.java | 19 + .../hiver/mall/ie/mapper/IeBlockMapper.java | 9 + .../ie/mapper/IeContentAuditLogMapper.java | 9 + .../mall/ie/mapper/IeMatchAttemptMapper.java | 9 + .../mall/ie/mapper/IePresenceEventMapper.java | 9 + .../hiver/mall/ie/mapper/IeRecordMapper.java | 9 + .../hiver/mall/ie/mapper/IeReportMapper.java | 9 + .../cc/hiver/mall/ie/mapper/IeRoomMapper.java | 9 + .../mall/ie/mapper/IeRoomMessageMapper.java | 9 + .../mall/ie/mapper/IeSensitiveWordMapper.java | 9 + .../mall/ie/mapper/IeUserProfileMapper.java | 9 + .../mall/ie/mapper/IeUserStatusMapper.java | 9 + .../hiver/mall/ie/mq/IeChatDelayConsumer.java | 25 + .../hiver/mall/ie/mq/IeChatEventConsumer.java | 16 + .../hiver/mall/ie/mq/IeChatEventProducer.java | 25 + .../cc/hiver/mall/ie/mq/IeChatMqConfig.java | 69 ++ .../hiver/mall/ie/service/IeChatService.java | 39 ++ .../hiver/mall/ie/service/IeMatchService.java | 22 + .../hiver/mall/ie/service/IeRedisService.java | 55 ++ .../ie/service/IeSecurityAuditService.java | 9 + .../ie/service/IeWechatSecurityService.java | 7 + .../ie/service/impl/IeChatServiceImpl.java | 454 ++++++++++++++ .../ie/service/impl/IeMatchServiceImpl.java | 589 ++++++++++++++++++ .../ie/service/impl/IeRedisServiceImpl.java | 275 ++++++++ .../impl/IeSecurityAuditServiceImpl.java | 184 ++++++ .../impl/IeWechatSecurityServiceImpl.java | 112 ++++ .../cc/hiver/mall/ie/vo/IeAuditResult.java | 17 + .../java/cc/hiver/mall/ie/vo/IeChatEvent.java | 15 + .../java/cc/hiver/mall/ie/vo/IeHomeVO.java | 20 + .../java/cc/hiver/mall/ie/vo/IeMatchVO.java | 21 + .../cc/hiver/mall/ie/vo/IeMessageAckVO.java | 20 + .../cc/hiver/mall/ie/vo/IeUserProfileVO.java | 24 + .../mall/ie/vo/IeWechatMsgSecResult.java | 14 + .../ie/websocket/IeWebSocketController.java | 83 +++ .../websocket/IeWebSocketEventListener.java | 37 ++ .../hiver/mall/pojo/dto/CreateOrderDTO.java | 3 + .../hiver/mall/service/ShopAreaService.java | 10 + .../mall/serviceimpl/ShopAreaServiceImpl.java | 10 + .../mybatis/MallOrderServiceImpl.java | 1 + .../cc/hiver/mall/utils/WechatPayConfig.java | 8 + .../db/ie_room_message_media_fields.sql | 4 + .../resources/db/ie_sensitive_words_seed.sql | 124 ++++ .../db/ie_user_profile_gender_fields.sql | 6 + .../db/ie_user_profile_onboarding_fields.sql | 6 + .../main/resources/mapper/MallOrderMapper.xml | 3 + 75 files changed, 3148 insertions(+), 12 deletions(-) create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/config/IeRedisPubSubConfig.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/config/IeWebSocketConfig.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/constant/IeChatEventType.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/constant/IeConstants.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/constant/IeRedisKey.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/controller/IeAdminController.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/controller/IeController.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeMatchStartDTO.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IePresenceDTO.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeProfileDTO.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeReportDTO.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeRoomMessageDTO.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeStatusDTO.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeBaseEntity.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeBlock.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeContentAuditLog.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeMatchAttempt.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IePresenceEvent.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeRecord.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeReport.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeRoom.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeRoomMessage.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeSensitiveWord.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeUserProfile.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeUserStatus.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeBlockMapper.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeContentAuditLogMapper.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeMatchAttemptMapper.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IePresenceEventMapper.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeRecordMapper.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeReportMapper.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeRoomMapper.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeRoomMessageMapper.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeSensitiveWordMapper.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeUserProfileMapper.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeUserStatusMapper.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mq/IeChatDelayConsumer.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mq/IeChatEventConsumer.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mq/IeChatEventProducer.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mq/IeChatMqConfig.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeChatService.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeMatchService.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeRedisService.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeSecurityAuditService.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeWechatSecurityService.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeChatServiceImpl.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMatchServiceImpl.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeRedisServiceImpl.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeSecurityAuditServiceImpl.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeWechatSecurityServiceImpl.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeAuditResult.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeChatEvent.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeHomeVO.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeMatchVO.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeMessageAckVO.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeUserProfileVO.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeWechatMsgSecResult.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/websocket/IeWebSocketController.java create mode 100644 hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/websocket/IeWebSocketEventListener.java create mode 100644 hiver-modules/hiver-mall/src/main/resources/db/ie_room_message_media_fields.sql create mode 100644 hiver-modules/hiver-mall/src/main/resources/db/ie_sensitive_words_seed.sql create mode 100644 hiver-modules/hiver-mall/src/main/resources/db/ie_user_profile_gender_fields.sql create mode 100644 hiver-modules/hiver-mall/src/main/resources/db/ie_user_profile_onboarding_fields.sql diff --git a/hiver-admin/src/main/resources/application.yml b/hiver-admin/src/main/resources/application.yml index 7d87d5de..08fa820f 100644 --- a/hiver-admin/src/main/resources/application.yml +++ b/hiver-admin/src/main/resources/application.yml @@ -504,6 +504,7 @@ aliyun: accessKeySecret: sk-bcfa4865b89548acb8225f910f13d682 wechatpay: appid: wx21a1caccef085db7 + appSecret: dd1642569f7484664d62ff0fd811bc54 mchId: 1106375960 apiV3Key: qwaszxcdfertgfdertyhjyijm2145632 merchantSerialNo: 4B42A5EDF9CA2FC2ACEB21271A773A4F15B432A4 diff --git a/hiver-admin/test-output/test-report.html b/hiver-admin/test-output/test-report.html index ce3d4dff..a6493094 100644 --- a/hiver-admin/test-output/test-report.html +++ b/hiver-admin/test-output/test-report.html @@ -35,7 +35,7 @@ Hiver
  • -五月 16, 2026 17:37:44 +五月 19, 2026 15:04:26
  • @@ -84,7 +84,7 @@

    passTest

    -

    17:37:45 下午 / 0.018 secs

    +

    15:04:27 下午 / 0.017 secs

    @@ -92,9 +92,9 @@
    #test-id=1
    passTest
    -05.16.2026 17:37:45 -05.16.2026 17:37:45 -0.018 secs +05.19.2026 15:04:27 +05.19.2026 15:04:27 +0.017 secs
    @@ -104,7 +104,7 @@ Pass - 17:37:45 + 15:04:27 Test passed @@ -128,13 +128,13 @@

    Started

    -

    五月 16, 2026 17:37:44

    +

    五月 19, 2026 15:04:26

    Ended

    -

    五月 16, 2026 17:37:45

    +

    五月 19, 2026 15:04:27

    diff --git a/hiver-modules/hiver-mall/pom.xml b/hiver-modules/hiver-mall/pom.xml index a452f94e..6e1aead1 100644 --- a/hiver-modules/hiver-mall/pom.xml +++ b/hiver-modules/hiver-mall/pom.xml @@ -39,6 +39,11 @@ com.alibaba fastjson + + com.github.houbb + sensitive-word + 0.26.0 + \ No newline at end of file diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopAreaController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopAreaController.java index b8b3d7ba..a76a55d7 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopAreaController.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopAreaController.java @@ -52,9 +52,12 @@ public class ShopAreaController { @RequestMapping(value = "/getByParentId/{parentId}", method = RequestMethod.GET) @ApiOperation(value = "閫氳繃parentId鑾峰彇") public Result> getByParentId(@PathVariable String parentId, + @ApiParam("鍚嶇О妯$硦鎼滅储") + @RequestParam(required = false) String title, @ApiParam("鏄惁寮濮嬫暟鎹潈闄愯繃婊") @RequestParam(required = false, defaultValue = "true") Boolean openDataFilter) { List list; + String titleKeyword = StrUtil.isBlank(title) ? null : title.trim(); List values = redisTemplate.hValues("SHOPAREA_CACHE:" + parentId); if (values != null && !values.isEmpty()) { list = new java.util.ArrayList<>(); @@ -80,12 +83,25 @@ public class ShopAreaController { } list = filteredDatas; } + if (StrUtil.isNotBlank(titleKeyword)) { + List filteredDatas = new java.util.ArrayList<>(); + for (ShopArea allData : list) { + if (StrUtil.isNotBlank(allData.getTitle()) && allData.getTitle().contains(titleKeyword)) { + filteredDatas.add(allData); + } + } + list = filteredDatas; + } //setInfo(list); return new ResultUtil>().setData(list); } - list = shopAreaService.findByParentIdOrderBySortOrder(parentId, openDataFilter); + if (StrUtil.isNotBlank(titleKeyword)) { + list = shopAreaService.findByParentIdAndTitleLikeOrderBySortOrder(parentId, "%" + titleKeyword + "%", openDataFilter); + } else { + list = shopAreaService.findByParentIdOrderBySortOrder(parentId, openDataFilter); + } setInfo(list); - if (list != null && list.size() > 0) { + if (StrUtil.isBlank(titleKeyword) && list != null && list.size() > 0) { for(ShopArea sa : list){ shopAreaService.refreshShopAreaCache(sa.getParentId(), sa.getId()); } diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/ShopAreaDao.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/ShopAreaDao.java index 349ac35d..174e64ae 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/ShopAreaDao.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/ShopAreaDao.java @@ -1,7 +1,7 @@ package cc.hiver.mall.dao; -import cc.hiver.mall.entity.ShopArea; import cc.hiver.core.base.HiverBaseDao; +import cc.hiver.mall.entity.ShopArea; import java.util.List; @@ -28,6 +28,25 @@ public interface ShopAreaDao extends HiverBaseDao { */ List findByParentIdAndIdInOrderBySortOrder(String parentId, List shopAreas); + /** + * 閫氳繃鐖秈d鍜屽湀灞傚悕妯$硦鎼滅储 鍗囧簭 + * + * @param parentId + * @param title + * @return + */ + List findByParentIdAndTitleLikeOrderBySortOrder(String parentId, String title); + + /** + * 閫氳繃鐖秈d鍜屽湀灞傚悕妯$硦鎼滅储 鍗囧簭 鏁版嵁鏉冮檺 + * + * @param parentId + * @param title + * @param shopAreas + * @return + */ + List findByParentIdAndTitleLikeAndIdInOrderBySortOrder(String parentId, String title, List shopAreas); + /** * 閫氳繃鐖秈d鍜岀姸鎬佽幏鍙 鍗囧簭 * diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/MallOrder.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/MallOrder.java index 719e2c49..3dcd1074 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/MallOrder.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/MallOrder.java @@ -34,7 +34,7 @@ public class MallOrder implements Serializable { private String shopId; @ApiModelProperty(value = "璁㈠崟绫诲瀷 1:鐩存帴璐拱 2:鎷煎洟璐拱 3:闈㈠闈㈠洟") private Integer orderType; - @ApiModelProperty(value = "璁㈠崟绫诲瀷 null:澶栧崠 1:蹇掕窇鑵 2:浜屾墜") + @ApiModelProperty(value = "璁㈠崟绫诲瀷 null:澶栧崠 1:蹇掕窇鑵 2:鍥㈣喘 3锛氫簩鎵") private Integer otherOrder; @ApiModelProperty(value = "閰嶉佹柟寮 1:澶栧崠閰嶉 2:鍒板簵鑷彇") private Integer deliveryType; diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/config/IeRedisPubSubConfig.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/config/IeRedisPubSubConfig.java new file mode 100644 index 00000000..b62750d1 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/config/IeRedisPubSubConfig.java @@ -0,0 +1,34 @@ +package cc.hiver.mall.ie.config; + +import cc.hiver.mall.ie.constant.IeRedisKey; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.listener.ChannelTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; + +@Slf4j +@Configuration +public class IeRedisPubSubConfig { + + @Bean + public RedisMessageListenerContainer ieRedisMessageListenerContainer(RedisConnectionFactory connectionFactory, + MessageListenerAdapter ieChatEventListenerAdapter) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + container.addMessageListener(ieChatEventListenerAdapter, new ChannelTopic(IeRedisKey.CHAT_EVENT_CHANNEL)); + return container; + } + + @Bean + public MessageListenerAdapter ieChatEventListenerAdapter() { + return new MessageListenerAdapter(new Object() { + @SuppressWarnings("unused") + public void handleMessage(String message) { + log.info("銆恑/e Redis鑱婂ぉ浜嬩欢銆憑}", message); + } + }); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/config/IeWebSocketConfig.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/config/IeWebSocketConfig.java new file mode 100644 index 00000000..5066529a --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/config/IeWebSocketConfig.java @@ -0,0 +1,25 @@ +package cc.hiver.mall.ie.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.simp.config.MessageBrokerRegistry; +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; +import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; + +@Configuration +@EnableWebSocketMessageBroker +public class IeWebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableSimpleBroker("/topic", "/queue"); + registry.setApplicationDestinationPrefixes("/app"); + registry.setUserDestinationPrefix("/user"); + } + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/hiver/ws/websocket") + .setAllowedOriginPatterns("*"); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/constant/IeChatEventType.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/constant/IeChatEventType.java new file mode 100644 index 00000000..a6e8bd0a --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/constant/IeChatEventType.java @@ -0,0 +1,14 @@ +package cc.hiver.mall.ie.constant; + +public final class IeChatEventType { + public static final String MESSAGE_SAVED = "MESSAGE_SAVED"; + public static final String MESSAGE_DELIVERED = "MESSAGE_DELIVERED"; + public static final String MESSAGE_BLOCKED = "MESSAGE_BLOCKED"; + public static final String ROOM_FINISHED = "ROOM_FINISHED"; + public static final String REPORT_CREATED = "REPORT_CREATED"; + public static final String USER_CONNECTED = "USER_CONNECTED"; + public static final String USER_DISCONNECTED = "USER_DISCONNECTED"; + + private IeChatEventType() { + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/constant/IeConstants.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/constant/IeConstants.java new file mode 100644 index 00000000..8d3624c1 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/constant/IeConstants.java @@ -0,0 +1,23 @@ +package cc.hiver.mall.ie.constant; + +public final class IeConstants { + + public static final int DEFAULT_ROOM_MINUTES = 15; + public static final int DEFAULT_DAILY_QUOTA = 3; + + public static final int ROOM_STATUS_ACTIVE = 0; + public static final int ROOM_STATUS_NATURAL_END = 1; + public static final int ROOM_STATUS_EARLY_END = 2; + + public static final int AUDIT_PASS = 1; + public static final int AUDIT_REPLACE = 2; + public static final int AUDIT_BLOCK = 3; + + public static final int RISK_NONE = 0; + public static final int RISK_LOW = 1; + public static final int RISK_MEDIUM = 2; + public static final int RISK_HIGH = 3; + + private IeConstants() { + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/constant/IeRedisKey.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/constant/IeRedisKey.java new file mode 100644 index 00000000..ae389db0 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/constant/IeRedisKey.java @@ -0,0 +1,64 @@ +package cc.hiver.mall.ie.constant; + +/** + * i/e 闅忔満闄即 Redis Key 璁捐銆 + */ +public final class IeRedisKey { + + private static final String PREFIX = "ie:"; + + /** 鍦ㄧ嚎鐢ㄦ埛缂撳瓨锛歓SET, member=userId, score=lastActiveMillis */ + public static final String ONLINE_USERS = PREFIX + "online:users"; + + /** 鐢ㄦ埛鐘舵佺紦瀛橈細HASH, key=ie:user:status:{userId} */ + public static final String USER_STATUS = PREFIX + "user:status:"; + + /** 鍖归厤姹狅細ZSET, key=ie:match:pool:{mode}:{mood}, member=userId, score=matchScore */ + public static final String MATCH_POOL = PREFIX + "match:pool:"; + + /** 鐢ㄦ埛鎵鍦ㄥ尮閰嶆睜绱㈠紩锛歋TRING, key=ie:match:user-pool:{userId}, value=poolKey */ + public static final String USER_MATCH_POOL = PREFIX + "match:user-pool:"; + + /** 鍖归厤璁ら閿侊細STRING, key=ie:match:lock:{userId} */ + public static final String MATCH_LOCK = PREFIX + "match:lock:"; + + /** 鐢ㄦ埛鏈杩戝尮閰嶉泦鍚堬細SET, key=ie:match:recent:{userId}, member=targetUserId */ + public static final String RECENT_MATCH = PREFIX + "match:recent:"; + + /** 姣忔棩娆℃暟闄愬埗锛歋TRING, key=ie:quota:{yyyyMMdd}:{userId} */ + public static final String DAILY_QUOTA = PREFIX + "quota:"; + + /** 鑱婂ぉ浼氳瘽缂撳瓨锛欻ASH, key=ie:room:{roomId} */ + public static final String ROOM = PREFIX + "room:"; + + /** WebSocket杩炴帴缂撳瓨锛欻ASH, key=ie:ws:conn:{userId} */ + public static final String WS_CONNECTION = PREFIX + "ws:conn:"; + + /** WebSocket session -> userId 鏄犲皠 */ + public static final String WS_SESSION_USER = PREFIX + "ws:session:"; + + /** 绂荤嚎娑堟伅缂撳瓨锛歀IST, key=ie:offline:{userId} */ + public static final String OFFLINE_MESSAGE = PREFIX + "offline:"; + + /** 鐑棬鐘舵佺紦瀛橈細ZSET, member=statusText, score=heat */ + public static final String HOT_STATUS = PREFIX + "hot:status"; + + /** Redis 鍙戝竷璁㈤槄棰戦亾锛氳法瀹炰緥鑱婂ぉ浜嬩欢 */ + public static final String CHAT_EVENT_CHANNEL = PREFIX + "chat:event"; + + /** 闃插埛璁℃暟锛歋TRING, key=ie:rate:msg:{roomId}:{userId} */ + public static final String MESSAGE_RATE = PREFIX + "rate:msg:"; + + /** 鐢ㄦ埛椋庢帶璁℃暟锛歋TRING, key=ie:risk:{userId} */ + public static final String USER_RISK = PREFIX + "risk:"; + + /** 鏁忔劅璇嶇紦瀛橈細STRING json array */ + public static final String SENSITIVE_WORDS = PREFIX + "sensitive:words"; + + private IeRedisKey() { + } + + public static String matchPool(String mode, String mood) { + return MATCH_POOL + mode + ":" + mood; + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/controller/IeAdminController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/controller/IeAdminController.java new file mode 100644 index 00000000..381e6890 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/controller/IeAdminController.java @@ -0,0 +1,27 @@ +package cc.hiver.mall.ie.controller; + +import cc.hiver.core.common.utils.ResultUtil; +import cc.hiver.core.common.vo.Result; +import cc.hiver.mall.ie.service.IeChatService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@Api(tags = "i/e鍚庡彴缁存姢鎺ュ彛") +@RequestMapping("/hiver/admin/ie") +public class IeAdminController { + + @Autowired + private IeChatService chatService; + + @RequestMapping(value = "/rooms/finishExpired", method = RequestMethod.POST) + @ApiOperation("鎵归噺缁撴潫宸茶繃鏈熼櫔浼存埧闂") + public Result finishExpiredRooms(Integer limit) { + chatService.finishExpiredRooms(limit == null ? 100 : limit); + return ResultUtil.success("澶勭悊瀹屾垚"); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/controller/IeController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/controller/IeController.java new file mode 100644 index 00000000..cb4ba3b6 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/controller/IeController.java @@ -0,0 +1,149 @@ +package cc.hiver.mall.ie.controller; + +import cc.hiver.core.common.utils.ResultUtil; +import cc.hiver.core.common.utils.SecurityUtil; +import cc.hiver.core.common.vo.Result; +import cc.hiver.core.entity.User; +import cc.hiver.mall.ie.dto.*; +import cc.hiver.mall.ie.entity.IeRecord; +import cc.hiver.mall.ie.entity.IeReport; +import cc.hiver.mall.ie.entity.IeRoomMessage; +import cc.hiver.mall.ie.service.IeChatService; +import cc.hiver.mall.ie.service.IeMatchService; +import cc.hiver.mall.ie.service.IeRedisService; +import cc.hiver.mall.ie.vo.IeHomeVO; +import cc.hiver.mall.ie.vo.IeMatchVO; +import cc.hiver.mall.ie.vo.IeMessageAckVO; +import cc.hiver.mall.ie.vo.IeUserProfileVO; +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@Api(tags = "i/e闅忔満闄即鎺ュ彛") +@RequestMapping("/hiver/app/ie") +public class IeController { + + @Autowired + private SecurityUtil securityUtil; + + @Autowired + private IeMatchService matchService; + + @Autowired + private IeChatService chatService; + + @Autowired + private IeRedisService redisService; + + @RequestMapping(value = "/home", method = RequestMethod.GET) + @ApiOperation("棣栭〉鐘舵") + public Result home() { + return new ResultUtil().setData(matchService.home(currentUserId())); + } + + @RequestMapping(value = "/profile", method = RequestMethod.GET) + @ApiOperation("鑾峰彇i/e璧勬枡") + public Result profile() { + return new ResultUtil().setData(matchService.profile(currentUserId())); + } + + @RequestMapping(value = "/profiles/{targetUserId}", method = RequestMethod.GET) + @ApiOperation("鑾峰彇瀵规柟鍗婂尶鍚峣/e璧勬枡") + public Result profileByUserId(@PathVariable Long targetUserId) { + return new ResultUtil().setData(matchService.profileByUserId(currentUserId(), targetUserId)); + } + + @RequestMapping(value = "/profile", method = RequestMethod.POST) + @ApiOperation("淇濆瓨i/e璧勬枡") + public Result saveProfile(@RequestBody IeProfileDTO dto) { + return new ResultUtil().setData(matchService.saveProfile(currentUserId(), dto)); + } + + @RequestMapping(value = "/status", method = RequestMethod.POST) + @ApiOperation("鏇存柊褰撳墠i/e鍜屾儏缁姸鎬") + public Result updateStatus(@RequestBody IeStatusDTO dto) { + matchService.updateStatus(currentUserId(), dto); + return ResultUtil.success("鏇存柊鎴愬姛"); + } + + @RequestMapping(value = "/match/start", method = RequestMethod.POST) + @ApiOperation("鍙戣捣闅忔満鍖归厤") + public Result startMatch(@RequestBody IeMatchStartDTO dto) { + return new ResultUtil().setData(matchService.startMatch(currentUserId(), dto)); + } + + @RequestMapping(value = "/rooms/{roomId}/presence", method = RequestMethod.POST) + @ApiOperation("鍙戦佽交浜掑姩/杈撳叆鐘舵/蹇冭烦浜嬩欢") + public Result presence(@PathVariable Long roomId, @RequestBody IePresenceDTO dto) { + dto.setRoomId(roomId); + chatService.sendPresence(currentUserId(), dto); + return ResultUtil.success("鍙戦佹垚鍔"); + } + + @RequestMapping(value = "/rooms/{roomId}/messages", method = RequestMethod.POST) + @ApiOperation("鍙戦佽亰澶╂秷鎭") + public Result sendMessage(@PathVariable Long roomId, @RequestBody IeRoomMessageDTO dto) { + dto.setRoomId(roomId); + return new ResultUtil().setData(chatService.sendMessage(currentUserId(), dto)); + } + + @RequestMapping(value = "/rooms/{roomId}/finish", method = RequestMethod.POST) + @ApiOperation("鎻愬墠缁撴潫闄即鎴块棿") + public Result finishRoom(@PathVariable Long roomId) { + chatService.finishRoom(currentUserId(), roomId, 2); + return ResultUtil.success("宸茬粨鏉"); + } + + @RequestMapping(value = "/rooms/{roomId}/report", method = RequestMethod.POST) + @ApiOperation("涓炬姤涓嶉傚唴瀹") + public Result report(@PathVariable Long roomId, @RequestBody IeReportDTO dto) { + dto.setRoomId(roomId); + chatService.report(currentUserId(), dto); + return ResultUtil.success("宸叉敹鍒颁妇鎶"); + } + + @RequestMapping(value = "/block/{blockedUserId}", method = RequestMethod.POST) + @ApiOperation("鎷夐粦闄即瀵硅薄") + public Result block(@PathVariable Long blockedUserId, String reason) { + chatService.block(currentUserId(), blockedUserId, reason); + return ResultUtil.success("宸叉媺榛"); + } + + @RequestMapping(value = "/offline", method = RequestMethod.GET) + @ApiOperation("鎷夊彇绂荤嚎娑堟伅") + public Result> offline() { + return new ResultUtil>().setData(redisService.popOfflineMessages(currentUserId(), 50)); + } + + @RequestMapping(value = "/rooms/{roomId}/messages/page", method = RequestMethod.GET) + @ApiOperation("鍒嗛〉鏌ヨ鎴块棿娑堟伅") + public Result> pageMessages(@PathVariable Long roomId, + @RequestParam(required = false, defaultValue = "1") Integer pageNumber, + @RequestParam(required = false, defaultValue = "20") Integer pageSize) { + return new ResultUtil>().setData(chatService.pageMessages(currentUserId(), roomId, pageNumber, pageSize)); + } + + @RequestMapping(value = "/records/page", method = RequestMethod.GET) + @ApiOperation("鍒嗛〉鏌ヨ鎰熷彈璁板綍") + public Result> pageRecords(@RequestParam(required = false, defaultValue = "1") Integer pageNumber, + @RequestParam(required = false, defaultValue = "10") Integer pageSize) { + return new ResultUtil>().setData(chatService.pageRecords(currentUserId(), pageNumber, pageSize)); + } + + @RequestMapping(value = "/reports/page", method = RequestMethod.GET) + @ApiOperation("鍒嗛〉鏌ヨ鎴戠殑涓炬姤璁板綍") + public Result> pageReports(@RequestParam(required = false, defaultValue = "1") Integer pageNumber, + @RequestParam(required = false, defaultValue = "10") Integer pageSize) { + return new ResultUtil>().setData(chatService.pageReports(currentUserId(), pageNumber, pageSize)); + } + + private Long currentUserId() { + User user = securityUtil.getCurrUser(); + return Long.valueOf(user.getId()); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeMatchStartDTO.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeMatchStartDTO.java new file mode 100644 index 00000000..ba9eab0c --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeMatchStartDTO.java @@ -0,0 +1,16 @@ +package cc.hiver.mall.ie.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class IeMatchStartDTO { + private String mode; + private String targetMode; + private String targetGender; + private String mood; + private Double longitude; + private Double latitude; + private List interestTags; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IePresenceDTO.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IePresenceDTO.java new file mode 100644 index 00000000..1fddd89a --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IePresenceDTO.java @@ -0,0 +1,10 @@ +package cc.hiver.mall.ie.dto; + +import lombok.Data; + +@Data +public class IePresenceDTO { + private Long roomId; + private String eventType; + private String eventText; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeProfileDTO.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeProfileDTO.java new file mode 100644 index 00000000..216a9855 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeProfileDTO.java @@ -0,0 +1,18 @@ +package cc.hiver.mall.ie.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class IeProfileDTO { + private String anonymousName; + private String avatarText; + private String avatarUrl; + private String gender; + private String intro; + private List interestTags; + private String currentMode; + private String targetModePreference; + private String targetGenderPreference; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeReportDTO.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeReportDTO.java new file mode 100644 index 00000000..c8092697 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeReportDTO.java @@ -0,0 +1,12 @@ +package cc.hiver.mall.ie.dto; + +import lombok.Data; + +@Data +public class IeReportDTO { + private Long roomId; + private Long messageId; + private Long reportedUserId; + private String reasonType; + private String reasonText; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeRoomMessageDTO.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeRoomMessageDTO.java new file mode 100644 index 00000000..8886cdfb --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeRoomMessageDTO.java @@ -0,0 +1,15 @@ +package cc.hiver.mall.ie.dto; + +import lombok.Data; + +@Data +public class IeRoomMessageDTO { + private Long roomId; + private String roomNo; + private String clientMsgId; + private Integer messageType; + private String content; + private Integer mediaDuration; + private Long mediaSize; + private String mediaFormat; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeStatusDTO.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeStatusDTO.java new file mode 100644 index 00000000..a724a035 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeStatusDTO.java @@ -0,0 +1,15 @@ +package cc.hiver.mall.ie.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class IeStatusDTO { + private String mode; + private String mood; + private String statusText; + private Double longitude; + private Double latitude; + private List interestTags; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeBaseEntity.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeBaseEntity.java new file mode 100644 index 00000000..400b64bf --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeBaseEntity.java @@ -0,0 +1,26 @@ +package cc.hiver.mall.ie.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.io.Serializable; +import java.util.Date; + +@Data +public class IeBaseEntity implements Serializable { + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Long id; + + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeBlock.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeBlock.java new file mode 100644 index 00000000..3a509589 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeBlock.java @@ -0,0 +1,14 @@ +package cc.hiver.mall.ie.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("ie_block") +public class IeBlock extends IeBaseEntity { + private Long userId; + private Long blockedUserId; + private String reason; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeContentAuditLog.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeContentAuditLog.java new file mode 100644 index 00000000..6acb7734 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeContentAuditLog.java @@ -0,0 +1,20 @@ +package cc.hiver.mall.ie.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("ie_content_audit_log") +public class IeContentAuditLog extends IeBaseEntity { + private String bizType; + private Long bizId; + private Long userId; + private String rawContent; + private String filteredContent; + private Integer auditStatus; + private Integer riskLevel; + private String hitWords; + private String hitCategories; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeMatchAttempt.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeMatchAttempt.java new file mode 100644 index 00000000..66672036 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeMatchAttempt.java @@ -0,0 +1,22 @@ +package cc.hiver.mall.ie.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("ie_match_attempt") +public class IeMatchAttempt extends IeBaseEntity { + private String matchNo; + private Long userId; + private Long targetUserId; + private String mode; + private String mood; + private Integer status; + private String anonymousName; + private String avatarText; + private String stateText; + private String quoteText; + private String failReason; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IePresenceEvent.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IePresenceEvent.java new file mode 100644 index 00000000..17e1c66a --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IePresenceEvent.java @@ -0,0 +1,15 @@ +package cc.hiver.mall.ie.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("ie_presence_event") +public class IePresenceEvent extends IeBaseEntity { + private Long roomId; + private Long senderId; + private String eventType; + private String eventText; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeRecord.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeRecord.java new file mode 100644 index 00000000..2d284cb7 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeRecord.java @@ -0,0 +1,29 @@ +package cc.hiver.mall.ie.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("ie_record") +public class IeRecord extends IeBaseEntity { + private Long roomId; + private Long userId; + private Long targetUserId; + private String mode; + private String mood; + private String anonymousName; + private Integer durationSeconds; + private String feeling; + private String summary; + private String tags; + private Integer remeetAvailable; + private Date remeetExpireTime; + private Date lastReadTime; + @TableField(exist = false) + private Integer unreadCount; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeReport.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeReport.java new file mode 100644 index 00000000..f36b5f91 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeReport.java @@ -0,0 +1,19 @@ +package cc.hiver.mall.ie.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("ie_report") +public class IeReport extends IeBaseEntity { + private Long reporterId; + private Long reportedUserId; + private Long roomId; + private Long messageId; + private String reasonType; + private String reasonText; + private Integer status; + private String handleResult; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeRoom.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeRoom.java new file mode 100644 index 00000000..525c46c2 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeRoom.java @@ -0,0 +1,23 @@ +package cc.hiver.mall.ie.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("ie_room") +public class IeRoom extends IeBaseEntity { + private String roomNo; + private Long matchId; + private Long userAId; + private Long userBId; + private String mode; + private String mood; + private Integer status; + private Date startTime; + private Date endTime; + private Date expireTime; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeRoomMessage.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeRoomMessage.java new file mode 100644 index 00000000..6950950a --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeRoomMessage.java @@ -0,0 +1,34 @@ +package cc.hiver.mall.ie.entity; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("ie_room_message") +public class IeRoomMessage extends IeBaseEntity { + private Long roomId; + private Long senderId; + private Long receiverId; + private Integer messageType; + private String rawContent; + private String filteredContent; + private Integer auditStatus; + private Integer riskLevel; + private String hitWords; + private Integer isBlocked; + private Integer mediaDuration; + private Long mediaSize; + private String mediaFormat; + private Date expireTime; + + @TableField(exist = false) + private String clientMsgId; + + @TableField(exist = false) + private Boolean mine; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeSensitiveWord.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeSensitiveWord.java new file mode 100644 index 00000000..0ebdf3e5 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeSensitiveWord.java @@ -0,0 +1,16 @@ +package cc.hiver.mall.ie.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("ie_sensitive_word") +public class IeSensitiveWord extends IeBaseEntity { + private String word; + private String category; + private Integer level; + private String replacement; + private Integer enabled; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeUserProfile.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeUserProfile.java new file mode 100644 index 00000000..db1ea310 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeUserProfile.java @@ -0,0 +1,30 @@ +package cc.hiver.mall.ie.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("ie_user_profile") +public class IeUserProfile extends IeBaseEntity { + private Long userId; + private String anonymousName; + private String avatarText; + private String avatarUrl; + private String gender; + private String intro; + private String interestTags; + private String currentMode; + private String recentPreference; + private String targetModePreference; + private String targetGenderPreference; + private Integer defaultRoomMinutes; + private Integer dailyQuota; + private Integer usedQuota; + private Date lastActiveTime; + private Integer profileCompleted; + private Integer isDeleted; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeUserStatus.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeUserStatus.java new file mode 100644 index 00000000..f33662ac --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeUserStatus.java @@ -0,0 +1,19 @@ +package cc.hiver.mall.ie.entity; + +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("ie_user_status") +public class IeUserStatus extends IeBaseEntity { + private Long userId; + private String mode; + private String mood; + private String statusText; + private Integer onlineStatus; + private Date lastActiveTime; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeBlockMapper.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeBlockMapper.java new file mode 100644 index 00000000..c50346c3 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeBlockMapper.java @@ -0,0 +1,9 @@ +package cc.hiver.mall.ie.mapper; + +import cc.hiver.mall.ie.entity.IeBlock; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springframework.stereotype.Repository; + +@Repository +public interface IeBlockMapper extends BaseMapper { +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeContentAuditLogMapper.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeContentAuditLogMapper.java new file mode 100644 index 00000000..6665d9f9 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeContentAuditLogMapper.java @@ -0,0 +1,9 @@ +package cc.hiver.mall.ie.mapper; + +import cc.hiver.mall.ie.entity.IeContentAuditLog; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springframework.stereotype.Repository; + +@Repository +public interface IeContentAuditLogMapper extends BaseMapper { +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeMatchAttemptMapper.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeMatchAttemptMapper.java new file mode 100644 index 00000000..24a41f1b --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeMatchAttemptMapper.java @@ -0,0 +1,9 @@ +package cc.hiver.mall.ie.mapper; + +import cc.hiver.mall.ie.entity.IeMatchAttempt; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springframework.stereotype.Repository; + +@Repository +public interface IeMatchAttemptMapper extends BaseMapper { +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IePresenceEventMapper.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IePresenceEventMapper.java new file mode 100644 index 00000000..38b101b8 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IePresenceEventMapper.java @@ -0,0 +1,9 @@ +package cc.hiver.mall.ie.mapper; + +import cc.hiver.mall.ie.entity.IePresenceEvent; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springframework.stereotype.Repository; + +@Repository +public interface IePresenceEventMapper extends BaseMapper { +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeRecordMapper.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeRecordMapper.java new file mode 100644 index 00000000..83b8f19b --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeRecordMapper.java @@ -0,0 +1,9 @@ +package cc.hiver.mall.ie.mapper; + +import cc.hiver.mall.ie.entity.IeRecord; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springframework.stereotype.Repository; + +@Repository +public interface IeRecordMapper extends BaseMapper { +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeReportMapper.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeReportMapper.java new file mode 100644 index 00000000..caa434a0 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeReportMapper.java @@ -0,0 +1,9 @@ +package cc.hiver.mall.ie.mapper; + +import cc.hiver.mall.ie.entity.IeReport; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springframework.stereotype.Repository; + +@Repository +public interface IeReportMapper extends BaseMapper { +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeRoomMapper.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeRoomMapper.java new file mode 100644 index 00000000..deea63d1 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeRoomMapper.java @@ -0,0 +1,9 @@ +package cc.hiver.mall.ie.mapper; + +import cc.hiver.mall.ie.entity.IeRoom; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springframework.stereotype.Repository; + +@Repository +public interface IeRoomMapper extends BaseMapper { +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeRoomMessageMapper.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeRoomMessageMapper.java new file mode 100644 index 00000000..f11852cf --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeRoomMessageMapper.java @@ -0,0 +1,9 @@ +package cc.hiver.mall.ie.mapper; + +import cc.hiver.mall.ie.entity.IeRoomMessage; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springframework.stereotype.Repository; + +@Repository +public interface IeRoomMessageMapper extends BaseMapper { +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeSensitiveWordMapper.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeSensitiveWordMapper.java new file mode 100644 index 00000000..eaa492ae --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeSensitiveWordMapper.java @@ -0,0 +1,9 @@ +package cc.hiver.mall.ie.mapper; + +import cc.hiver.mall.ie.entity.IeSensitiveWord; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springframework.stereotype.Repository; + +@Repository +public interface IeSensitiveWordMapper extends BaseMapper { +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeUserProfileMapper.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeUserProfileMapper.java new file mode 100644 index 00000000..b3eaa013 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeUserProfileMapper.java @@ -0,0 +1,9 @@ +package cc.hiver.mall.ie.mapper; + +import cc.hiver.mall.ie.entity.IeUserProfile; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springframework.stereotype.Repository; + +@Repository +public interface IeUserProfileMapper extends BaseMapper { +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeUserStatusMapper.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeUserStatusMapper.java new file mode 100644 index 00000000..7ea3c323 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeUserStatusMapper.java @@ -0,0 +1,9 @@ +package cc.hiver.mall.ie.mapper; + +import cc.hiver.mall.ie.entity.IeUserStatus; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springframework.stereotype.Repository; + +@Repository +public interface IeUserStatusMapper extends BaseMapper { +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mq/IeChatDelayConsumer.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mq/IeChatDelayConsumer.java new file mode 100644 index 00000000..2d3bf760 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mq/IeChatDelayConsumer.java @@ -0,0 +1,25 @@ +package cc.hiver.mall.ie.mq; + +import cc.hiver.mall.ie.constant.IeConstants; +import cc.hiver.mall.ie.service.IeChatService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class IeChatDelayConsumer { + + @Autowired + private IeChatService chatService; + + @RabbitListener(queues = IeChatMqConfig.IE_CHAT_DEAD_QUEUE) + public void handleRoomExpire(String roomId) { + try { + chatService.finishRoomBySystem(Long.valueOf(roomId), IeConstants.ROOM_STATUS_NATURAL_END); + } catch (Exception e) { + log.warn("銆恑/e闄愭椂鎴块棿銆戣嚜鍔ㄧ粨鏉熷け璐 roomId={}", roomId, e); + } + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mq/IeChatEventConsumer.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mq/IeChatEventConsumer.java new file mode 100644 index 00000000..848d7614 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mq/IeChatEventConsumer.java @@ -0,0 +1,16 @@ +package cc.hiver.mall.ie.mq; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class IeChatEventConsumer { + + @RabbitListener(queues = IeChatMqConfig.IE_CHAT_EVENT_QUEUE) + public void handleChatEvent(String message) { + log.info("銆恑/e鑱婂ぉ寮傛浜嬩欢銆憑}", message); + // 棰勭暀锛氬悗缁彲鎺ュ叆椋庢帶鐢诲儚銆佽繍钀ョ粺璁°佷汉宸ュ鏍搁槦鍒椼佹秷鎭竻鐞嗕换鍔° + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mq/IeChatEventProducer.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mq/IeChatEventProducer.java new file mode 100644 index 00000000..4913128f --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mq/IeChatEventProducer.java @@ -0,0 +1,25 @@ +package cc.hiver.mall.ie.mq; + +import cc.hiver.mall.ie.vo.IeChatEvent; +import cn.hutool.json.JSONUtil; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class IeChatEventProducer { + + @Autowired + private RabbitTemplate rabbitTemplate; + + public void sendEvent(IeChatEvent event) { + rabbitTemplate.convertAndSend(IeChatMqConfig.IE_CHAT_EXCHANGE, IeChatMqConfig.IE_CHAT_EVENT_ROUTING, JSONUtil.toJsonStr(event)); + } + + public void sendRoomExpire(Long roomId, long delayMillis) { + rabbitTemplate.convertAndSend(IeChatMqConfig.IE_CHAT_DELAY_EXCHANGE, IeChatMqConfig.IE_CHAT_DELAY_ROUTING, String.valueOf(roomId), message -> { + message.getMessageProperties().setExpiration(String.valueOf(Math.max(delayMillis, 1000L))); + return message; + }); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mq/IeChatMqConfig.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mq/IeChatMqConfig.java new file mode 100644 index 00000000..f56f04c1 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mq/IeChatMqConfig.java @@ -0,0 +1,69 @@ +package cc.hiver.mall.ie.mq; + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class IeChatMqConfig { + public static final String IE_CHAT_EXCHANGE = "ie.chat.direct.exchange"; + public static final String IE_CHAT_EVENT_QUEUE = "ie.chat.event.queue"; + public static final String IE_CHAT_EVENT_ROUTING = "ie.chat.event.routing"; + public static final String IE_CHAT_DELAY_EXCHANGE = "ie.chat.delay.exchange"; + public static final String IE_CHAT_DELAY_QUEUE = "ie.chat.delay.queue"; + public static final String IE_CHAT_DELAY_ROUTING = "ie.chat.delay.routing"; + public static final String IE_CHAT_DEAD_QUEUE = "ie.chat.dead.queue"; + public static final String IE_CHAT_DEAD_EXCHANGE = "ie.chat.dead.exchange"; + public static final String IE_CHAT_DEAD_ROUTING = "ie.chat.dead.routing"; + + @Bean + public DirectExchange ieChatExchange() { + return new DirectExchange(IE_CHAT_EXCHANGE, true, false); + } + + @Bean + public Queue ieChatEventQueue() { + return new Queue(IE_CHAT_EVENT_QUEUE, true); + } + + @Bean + public Binding bindingIeChatEventQueue() { + return BindingBuilder.bind(ieChatEventQueue()).to(ieChatExchange()).with(IE_CHAT_EVENT_ROUTING); + } + + @Bean + public DirectExchange ieChatDelayExchange() { + return new DirectExchange(IE_CHAT_DELAY_EXCHANGE, true, false); + } + + @Bean + public DirectExchange ieChatDeadExchange() { + return new DirectExchange(IE_CHAT_DEAD_EXCHANGE, true, false); + } + + @Bean + public Queue ieChatDelayQueue() { + java.util.Map args = new java.util.HashMap<>(); + args.put("x-dead-letter-exchange", IE_CHAT_DEAD_EXCHANGE); + args.put("x-dead-letter-routing-key", IE_CHAT_DEAD_ROUTING); + return new Queue(IE_CHAT_DELAY_QUEUE, true, false, false, args); + } + + @Bean + public Queue ieChatDeadQueue() { + return new Queue(IE_CHAT_DEAD_QUEUE, true); + } + + @Bean + public Binding bindingIeChatDelayQueue() { + return BindingBuilder.bind(ieChatDelayQueue()).to(ieChatDelayExchange()).with(IE_CHAT_DELAY_ROUTING); + } + + @Bean + public Binding bindingIeChatDeadQueue() { + return BindingBuilder.bind(ieChatDeadQueue()).to(ieChatDeadExchange()).with(IE_CHAT_DEAD_ROUTING); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeChatService.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeChatService.java new file mode 100644 index 00000000..c9f0f2cd --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeChatService.java @@ -0,0 +1,39 @@ +package cc.hiver.mall.ie.service; + +import cc.hiver.mall.ie.dto.IePresenceDTO; +import cc.hiver.mall.ie.dto.IeReportDTO; +import cc.hiver.mall.ie.dto.IeRoomMessageDTO; +import cc.hiver.mall.ie.entity.IeRecord; +import cc.hiver.mall.ie.entity.IeReport; +import cc.hiver.mall.ie.entity.IeRoom; +import cc.hiver.mall.ie.entity.IeRoomMessage; +import cc.hiver.mall.ie.vo.IeMessageAckVO; +import com.baomidou.mybatisplus.core.metadata.IPage; + +public interface IeChatService { + IeRoom getRoom(Long roomId); + + IeMessageAckVO sendMessage(Long senderId, IeRoomMessageDTO dto); + + void sendPresence(Long senderId, IePresenceDTO dto); + + void heartbeat(Long userId); + + void finishRoom(Long userId, Long roomId, Integer status); + + void finishRoomBySystem(Long roomId, Integer status); + + void finishExpiredRooms(int limit); + + void report(Long reporterId, IeReportDTO dto); + + void block(Long userId, Long blockedUserId, String reason); + + IPage pageMessages(Long userId, Long roomId, Integer pageNumber, Integer pageSize); + + void markRead(Long userId, Long roomId); + + IPage pageRecords(Long userId, Integer pageNumber, Integer pageSize); + + IPage pageReports(Long userId, Integer pageNumber, Integer pageSize); +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeMatchService.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeMatchService.java new file mode 100644 index 00000000..a62b19f2 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeMatchService.java @@ -0,0 +1,22 @@ +package cc.hiver.mall.ie.service; + +import cc.hiver.mall.ie.dto.IeMatchStartDTO; +import cc.hiver.mall.ie.dto.IeProfileDTO; +import cc.hiver.mall.ie.dto.IeStatusDTO; +import cc.hiver.mall.ie.vo.IeHomeVO; +import cc.hiver.mall.ie.vo.IeMatchVO; +import cc.hiver.mall.ie.vo.IeUserProfileVO; + +public interface IeMatchService { + IeHomeVO home(Long userId); + + IeUserProfileVO profile(Long userId); + + IeUserProfileVO profileByUserId(Long userId, Long targetUserId); + + IeUserProfileVO saveProfile(Long userId, IeProfileDTO dto); + + void updateStatus(Long userId, IeStatusDTO dto); + + IeMatchVO startMatch(Long userId, IeMatchStartDTO dto); +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeRedisService.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeRedisService.java new file mode 100644 index 00000000..f6123933 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeRedisService.java @@ -0,0 +1,55 @@ +package cc.hiver.mall.ie.service; + +import cc.hiver.mall.ie.dto.IeStatusDTO; + +import java.util.List; + +public interface IeRedisService { + void refreshOnline(Long userId); + + void cacheUserStatus(Long userId, IeStatusDTO dto); + + void addToMatchPool(Long userId, IeStatusDTO dto); + + void removeFromMatchPools(Long userId); + + boolean tryLockMatchUser(Long userId, long seconds); + + void unlockMatchUser(Long userId); + + List candidates(String mode, String mood, int limit); + + boolean isRecentlyMatched(Long userId, Long targetUserId); + + void markRecentlyMatched(Long userId, Long targetUserId); + + long todayUsedQuota(Long userId); + + long increaseTodayQuota(Long userId, long maxQuota); + + void cacheRoom(Long roomId, String roomJson, long seconds); + + String getCachedRoom(Long roomId); + + void pushOfflineMessage(Long userId, String messageJson); + + List popOfflineMessages(Long userId, int limit); + + void cacheWebSocketConnection(Long userId, String sessionId); + + void removeWebSocketConnection(Long userId); + + Long userIdBySession(String sessionId); + + boolean allowMessage(Long userId, Long roomId); + + long increaseRisk(Long userId, long seconds); + + void publishChatEvent(String eventJson); + + long onlineCount(); + + long waitingCount(String mode, String mood); + + List hotStatuses(int limit); +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeSecurityAuditService.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeSecurityAuditService.java new file mode 100644 index 00000000..aa726e6c --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeSecurityAuditService.java @@ -0,0 +1,9 @@ +package cc.hiver.mall.ie.service; + +import cc.hiver.mall.ie.vo.IeAuditResult; + +public interface IeSecurityAuditService { + IeAuditResult audit(String bizType, Long bizId, Long userId, String content); + + IeAuditResult audit(String bizType, Long bizId, Long userId, String openid, String content); +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeWechatSecurityService.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeWechatSecurityService.java new file mode 100644 index 00000000..b487a54e --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeWechatSecurityService.java @@ -0,0 +1,7 @@ +package cc.hiver.mall.ie.service; + +import cc.hiver.mall.ie.vo.IeWechatMsgSecResult; + +public interface IeWechatSecurityService { + IeWechatMsgSecResult msgSecCheck(String openid, String content); +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeChatServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeChatServiceImpl.java new file mode 100644 index 00000000..d1978e1e --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeChatServiceImpl.java @@ -0,0 +1,454 @@ +package cc.hiver.mall.ie.service.impl; + +import cc.hiver.core.dao.UserDao; +import cc.hiver.core.entity.User; +import cc.hiver.mall.ie.constant.IeChatEventType; +import cc.hiver.mall.ie.constant.IeConstants; +import cc.hiver.mall.ie.dto.IePresenceDTO; +import cc.hiver.mall.ie.dto.IeReportDTO; +import cc.hiver.mall.ie.dto.IeRoomMessageDTO; +import cc.hiver.mall.ie.entity.*; +import cc.hiver.mall.ie.mapper.*; +import cc.hiver.mall.ie.mq.IeChatEventProducer; +import cc.hiver.mall.ie.service.IeChatService; +import cc.hiver.mall.ie.service.IeRedisService; +import cc.hiver.mall.ie.service.IeSecurityAuditService; +import cc.hiver.mall.ie.vo.IeAuditResult; +import cc.hiver.mall.ie.vo.IeChatEvent; +import cc.hiver.mall.ie.vo.IeMessageAckVO; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +@Service +public class IeChatServiceImpl implements IeChatService { + + @Autowired + private IeRoomMapper roomMapper; + + @Autowired + private IeRoomMessageMapper roomMessageMapper; + + @Autowired + private IePresenceEventMapper presenceEventMapper; + + @Autowired + private IeReportMapper reportMapper; + + @Autowired + private IeBlockMapper blockMapper; + + @Autowired + private IeRecordMapper recordMapper; + + @Autowired + private IeRedisService redisService; + + @Autowired + private IeSecurityAuditService auditService; + + @Autowired + private SimpMessagingTemplate messagingTemplate; + + @Autowired + private UserDao userDao; + + @Autowired + private IeChatEventProducer chatEventProducer; + + @Override + public IeRoom getRoom(Long roomId) { + String cache = redisService.getCachedRoom(roomId); + if (cache != null && cache.length() > 0) { + return JSONUtil.toBean(cache, IeRoom.class); + } + IeRoom room = roomMapper.selectById(roomId); + if (room != null && room.getStatus() != null && room.getStatus() == IeConstants.ROOM_STATUS_ACTIVE && room.getExpireTime() != null) { + long ttl = Math.max(60L, (room.getExpireTime().getTime() - System.currentTimeMillis()) / 1000L); + redisService.cacheRoom(room.getId(), JSONUtil.toJsonStr(room), ttl); + } + return room; + } + + @Override + @Transactional + public IeMessageAckVO sendMessage(Long senderId, IeRoomMessageDTO dto) { + if (!redisService.allowMessage(senderId, dto.getRoomId())) { + throw new RuntimeException("鍙戦佸お蹇簡锛屽厛鎱竴鐐"); + } + IeRoom room = assertActiveRoom(senderId, dto.getRoomId()); + Long receiverId = room.getUserAId().equals(senderId) ? room.getUserBId() : room.getUserAId(); + assertReplyWindow(senderId, receiverId, room.getId()); + Integer messageType = dto.getMessageType() == null ? 1 : dto.getMessageType(); + IeAuditResult audit = auditMessage(room.getId(), senderId, messageType, dto.getContent()); + + IeRoomMessage message = new IeRoomMessage(); + message.setRoomId(room.getId()); + message.setSenderId(senderId); + message.setReceiverId(receiverId); + message.setMessageType(messageType); + message.setRawContent(dto.getContent()); + message.setFilteredContent(audit.getFilteredContent()); + message.setAuditStatus(audit.getAuditStatus()); + message.setRiskLevel(audit.getRiskLevel()); + message.setHitWords(String.join(",", audit.getHitWords())); + message.setIsBlocked(Boolean.TRUE.equals(audit.getBlocked()) ? 1 : 0); + message.setMediaDuration(safeMediaDuration(dto.getMediaDuration())); + message.setMediaSize(dto.getMediaSize()); + message.setMediaFormat(dto.getMediaFormat()); + message.setExpireTime(cleanupTime()); + message.setCreateTime(new Date()); + roomMessageMapper.insert(message); + + IeMessageAckVO ack = new IeMessageAckVO(); + ack.setClientMsgId(dto.getClientMsgId()); + ack.setMessageId(message.getId()); + ack.setRoomId(room.getId()); + ack.setSenderId(senderId); + ack.setReceiverId(receiverId); + ack.setMessageType(message.getMessageType()); + ack.setAuditStatus(message.getAuditStatus()); + ack.setRiskLevel(message.getRiskLevel()); + ack.setIsBlocked(message.getIsBlocked()); + ack.setContent(message.getFilteredContent()); + ack.setMediaDuration(message.getMediaDuration()); + ack.setMediaSize(message.getMediaSize()); + ack.setMediaFormat(message.getMediaFormat()); + + messagingTemplate.convertAndSendToUser(String.valueOf(senderId), "/queue/ie/ack", ack); + if (message.getIsBlocked() == 0) { + messagingTemplate.convertAndSendToUser(String.valueOf(receiverId), "/queue/ie/message", ack); + messagingTemplate.convertAndSend("/topic/ie/room/" + room.getId(), ack); + redisService.pushOfflineMessage(receiverId, JSONUtil.toJsonStr(ack)); + publishEvent(IeChatEventType.MESSAGE_DELIVERED, room.getId(), message.getId(), senderId, receiverId, message.getRiskLevel(), JSONUtil.toJsonStr(ack)); + } else { + long riskCount = redisService.increaseRisk(senderId, 3600); + publishEvent(IeChatEventType.MESSAGE_BLOCKED, room.getId(), message.getId(), senderId, receiverId, message.getRiskLevel(), "riskCount=" + riskCount); + } + return ack; + } + + private void assertReplyWindow(Long senderId, Long receiverId, Long roomId) { + Long replyCount = roomMessageMapper.selectCount(new LambdaQueryWrapper() + .eq(IeRoomMessage::getRoomId, roomId) + .eq(IeRoomMessage::getSenderId, receiverId) + .eq(IeRoomMessage::getReceiverId, senderId) + .eq(IeRoomMessage::getIsBlocked, 0) + .last("limit 1")); + if (replyCount != null && replyCount > 0) { + return; + } + Long sentBeforeReply = roomMessageMapper.selectCount(new LambdaQueryWrapper() + .eq(IeRoomMessage::getRoomId, roomId) + .eq(IeRoomMessage::getSenderId, senderId) + .eq(IeRoomMessage::getReceiverId, receiverId) + .eq(IeRoomMessage::getIsBlocked, 0)); + if (sentBeforeReply != null && sentBeforeReply >= 3) { + throw new RuntimeException("鍏堢瓑绛夊鏂瑰洖澶嶅惂锛岄娆$牬鍐版渶澶氬彂閫 3 鏉"); + } + } + + private IeAuditResult auditMessage(Long roomId, Long senderId, Integer messageType, String content) { + if (messageType != null && (messageType == 2 || messageType == 4)) { + IeAuditResult result = new IeAuditResult(); + result.setRawContent(content); + result.setFilteredContent(content); + result.setAuditStatus(1); + result.setRiskLevel(0); + result.setBlocked(false); + return result; + } + return auditService.audit("message", roomId, senderId, senderOpenid(senderId), content); + } + + private Integer safeMediaDuration(Integer mediaDuration) { + if (mediaDuration == null) { + return null; + } + return Math.max(1, Math.min(mediaDuration, 60)); + } + + @Override + public void sendPresence(Long senderId, IePresenceDTO dto) { + IeRoom room = assertActiveRoom(senderId, dto.getRoomId()); + IePresenceEvent event = new IePresenceEvent(); + event.setRoomId(room.getId()); + event.setSenderId(senderId); + event.setEventType(dto.getEventType()); + event.setEventText(dto.getEventText()); + event.setCreateTime(new Date()); + presenceEventMapper.insert(event); + Long receiverId = room.getUserAId().equals(senderId) ? room.getUserBId() : room.getUserAId(); + messagingTemplate.convertAndSendToUser(String.valueOf(receiverId), "/queue/ie/presence", event); + } + + @Override + public void heartbeat(Long userId) { + redisService.refreshOnline(userId); + } + + @Override + @Transactional + public void finishRoom(Long userId, Long roomId, Integer status) { + IeRoom room = assertActiveRoom(userId, roomId); + room.setStatus(status == null ? IeConstants.ROOM_STATUS_EARLY_END : status); + room.setEndTime(new Date()); + room.setUpdateTime(new Date()); + roomMapper.updateById(room); + createRecord(room, room.getUserAId(), room.getUserBId()); + createRecord(room, room.getUserBId(), room.getUserAId()); + messagingTemplate.convertAndSendToUser(String.valueOf(room.getUserAId()), "/queue/ie/room-end", room); + messagingTemplate.convertAndSendToUser(String.valueOf(room.getUserBId()), "/queue/ie/room-end", room); + publishEvent(IeChatEventType.ROOM_FINISHED, room.getId(), null, room.getUserAId(), room.getUserBId(), 0, String.valueOf(room.getStatus())); + } + + @Override + @Transactional + public void finishExpiredRooms(int limit) { + List rooms = roomMapper.selectList(new LambdaQueryWrapper() + .eq(IeRoom::getStatus, IeConstants.ROOM_STATUS_ACTIVE) + .le(IeRoom::getExpireTime, new Date()) + .last("limit " + Math.max(1, Math.min(limit, 200)))); + for (IeRoom room : rooms) { + finishExpiredRoom(room); + } + } + + @Override + public void report(Long reporterId, IeReportDTO dto) { + IeReport report = new IeReport(); + report.setReporterId(reporterId); + report.setReportedUserId(dto.getReportedUserId()); + report.setRoomId(dto.getRoomId()); + report.setMessageId(dto.getMessageId()); + report.setReasonType(dto.getReasonType()); + report.setReasonText(dto.getReasonText()); + report.setStatus(0); + report.setCreateTime(new Date()); + reportMapper.insert(report); + publishEvent(IeChatEventType.REPORT_CREATED, dto.getRoomId(), dto.getMessageId(), reporterId, dto.getReportedUserId(), 0, dto.getReasonType()); + } + + @Override + public void block(Long userId, Long blockedUserId, String reason) { + IeBlock exists = blockMapper.selectOne(new LambdaQueryWrapper() + .eq(IeBlock::getUserId, userId) + .eq(IeBlock::getBlockedUserId, blockedUserId) + .last("limit 1")); + if (exists != null) { + return; + } + IeBlock block = new IeBlock(); + block.setUserId(userId); + block.setBlockedUserId(blockedUserId); + block.setReason(reason); + block.setCreateTime(new Date()); + blockMapper.insert(block); + } + + @Override + public IPage pageMessages(Long userId, Long roomId, Integer pageNumber, Integer pageSize) { + assertRoomParticipant(userId, roomId); + markRead(userId, roomId); + Page page = new Page<>(safePageNumber(pageNumber), safePageSize(pageSize)); + IPage result = roomMessageMapper.selectPage(page, new LambdaQueryWrapper() + .eq(IeRoomMessage::getRoomId, roomId) + .eq(IeRoomMessage::getIsBlocked, 0) + .orderByDesc(IeRoomMessage::getCreateTime)); + for (IeRoomMessage message : result.getRecords()) { + message.setMine(message.getSenderId() != null && message.getSenderId().equals(userId)); + } + return result; + } + + @Override + public void markRead(Long userId, Long roomId) { + IeRecord record = recordMapper.selectOne(new LambdaQueryWrapper() + .eq(IeRecord::getUserId, userId) + .eq(IeRecord::getRoomId, roomId) + .last("limit 1")); + if (record == null) { + return; + } + record.setLastReadTime(new Date()); + record.setUpdateTime(new Date()); + recordMapper.updateById(record); + } + + @Override + public IPage pageRecords(Long userId, Integer pageNumber, Integer pageSize) { + Page page = new Page<>(safePageNumber(pageNumber), safePageSize(pageSize)); + IPage result = recordMapper.selectPage(page, new LambdaQueryWrapper() + .eq(IeRecord::getUserId, userId) + .orderByDesc(IeRecord::getCreateTime)); + for (IeRecord record : result.getRecords()) { + record.setUnreadCount(unreadMessageCount(userId, record.getRoomId(), record.getTargetUserId())); + } + return result; + } + + private int unreadMessageCount(Long userId, Long roomId, Long targetUserId) { + if (roomId == null || targetUserId == null) { + return 0; + } + Long count = roomMessageMapper.selectCount(new LambdaQueryWrapper() + .eq(IeRoomMessage::getRoomId, roomId) + .eq(IeRoomMessage::getSenderId, targetUserId) + .eq(IeRoomMessage::getReceiverId, userId) + .eq(IeRoomMessage::getIsBlocked, 0) + .gt(recordLastReadTime(userId, roomId) != null, IeRoomMessage::getCreateTime, recordLastReadTime(userId, roomId))); + return count == null ? 0 : count.intValue(); + } + + private Date recordLastReadTime(Long userId, Long roomId) { + IeRecord record = recordMapper.selectOne(new LambdaQueryWrapper() + .eq(IeRecord::getUserId, userId) + .eq(IeRecord::getRoomId, roomId) + .last("limit 1")); + return record == null ? null : record.getLastReadTime(); + } + + @Override + public IPage pageReports(Long userId, Integer pageNumber, Integer pageSize) { + Page page = new Page<>(safePageNumber(pageNumber), safePageSize(pageSize)); + return reportMapper.selectPage(page, new LambdaQueryWrapper() + .eq(IeReport::getReporterId, userId) + .orderByDesc(IeReport::getCreateTime)); + } + + @Override + @Transactional + public void finishRoomBySystem(Long roomId, Integer status) { + IeRoom room = roomMapper.selectById(roomId); + if (room == null || room.getStatus() == null || room.getStatus() != IeConstants.ROOM_STATUS_ACTIVE) { + return; + } + room.setStatus(status == null ? IeConstants.ROOM_STATUS_NATURAL_END : status); + room.setEndTime(new Date()); + room.setUpdateTime(new Date()); + roomMapper.updateById(room); + createRecord(room, room.getUserAId(), room.getUserBId()); + createRecord(room, room.getUserBId(), room.getUserAId()); + messagingTemplate.convertAndSendToUser(String.valueOf(room.getUserAId()), "/queue/ie/room-end", room); + messagingTemplate.convertAndSendToUser(String.valueOf(room.getUserBId()), "/queue/ie/room-end", room); + publishEvent(IeChatEventType.ROOM_FINISHED, room.getId(), null, room.getUserAId(), room.getUserBId(), 0, String.valueOf(room.getStatus())); + } + + private IeRoom assertActiveRoom(Long userId, Long roomId) { + IeRoom room = getRoom(roomId); + if (room == null) { + throw new RuntimeException("闄即鎴块棿涓嶅瓨鍦"); + } + if (!room.getUserAId().equals(userId) && !room.getUserBId().equals(userId)) { + throw new RuntimeException("鏃犳潈璁块棶璇ラ櫔浼存埧闂"); + } + if (room.getStatus() == null || room.getStatus() != IeConstants.ROOM_STATUS_ACTIVE) { + throw new RuntimeException("闄即鎴块棿宸茬粨鏉"); + } + return room; + } + + private void assertRoomParticipant(Long userId, Long roomId) { + IeRoom room = getRoom(roomId); + if (room == null) { + throw new RuntimeException("闄即鎴块棿涓嶅瓨鍦"); + } + if (!room.getUserAId().equals(userId) && !room.getUserBId().equals(userId)) { + throw new RuntimeException("鏃犳潈璁块棶璇ラ櫔浼存埧闂"); + } + } + + private long safePageNumber(Integer pageNumber) { + return pageNumber == null || pageNumber < 1 ? 1L : pageNumber; + } + + private long safePageSize(Integer pageSize) { + if (pageSize == null || pageSize < 1) { + return 10L; + } + return Math.min(pageSize, 50); + } + + private void finishExpiredRoom(IeRoom room) { + IeRoom latest = roomMapper.selectById(room.getId()); + if (latest == null || latest.getStatus() == null || latest.getStatus() != IeConstants.ROOM_STATUS_ACTIVE) { + return; + } + room = latest; + room.setStatus(IeConstants.ROOM_STATUS_NATURAL_END); + room.setEndTime(new Date()); + room.setUpdateTime(new Date()); + roomMapper.updateById(room); + createRecord(room, room.getUserAId(), room.getUserBId()); + createRecord(room, room.getUserBId(), room.getUserAId()); + messagingTemplate.convertAndSendToUser(String.valueOf(room.getUserAId()), "/queue/ie/room-end", room); + messagingTemplate.convertAndSendToUser(String.valueOf(room.getUserBId()), "/queue/ie/room-end", room); + publishEvent(IeChatEventType.ROOM_FINISHED, room.getId(), null, room.getUserAId(), room.getUserBId(), 0, String.valueOf(room.getStatus())); + } + + private Date cleanupTime() { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.HOUR, 24); + return calendar.getTime(); + } + + private String senderOpenid(Long senderId) { + User user = userDao.findById(String.valueOf(senderId)).orElse(null); + return user == null ? null : user.getMiniProgramOpenid(); + } + + private void createRecord(IeRoom room, Long userId, Long targetUserId) { + IeRecord exists = recordMapper.selectOne(new LambdaQueryWrapper() + .eq(IeRecord::getRoomId, room.getId()) + .eq(IeRecord::getUserId, userId) + .last("limit 1")); + if (exists != null) { + exists.setDurationSeconds((int) Math.max(0, ((room.getEndTime() == null ? System.currentTimeMillis() : room.getEndTime().getTime()) - room.getStartTime().getTime()) / 1000)); + exists.setSummary("杩欐闄即宸茬粨鏉燂紝浣犱粛鐒跺彲浠ヤ粠璁板綍閲屾煡鐪嬭亰澶╁巻鍙层"); + exists.setTags(room.getMode() + "," + room.getMood() + ",鑱婂ぉ璁板綍"); + exists.setRemeetAvailable(1); + exists.setUpdateTime(new Date()); + recordMapper.updateById(exists); + return; + } + IeRecord record = new IeRecord(); + record.setRoomId(room.getId()); + record.setUserId(userId); + record.setTargetUserId(targetUserId); + record.setMode(room.getMode()); + record.setMood(room.getMood()); + record.setAnonymousName("鍗婂尶鍚嶆紓娴佽"); + record.setDurationSeconds((int) Math.max(0, ((room.getEndTime() == null ? System.currentTimeMillis() : room.getEndTime().getTime()) - room.getStartTime().getTime()) / 1000)); + record.setSummary("杩欐闄即宸茬粨鏉燂紝浣犱粛鐒跺彲浠ヤ粠璁板綍閲屾煡鐪嬭亰澶╁巻鍙层"); + record.setTags(room.getMode() + "," + room.getMood() + ",鑱婂ぉ璁板綍"); + record.setRemeetAvailable(1); + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.HOUR, 24); + record.setRemeetExpireTime(calendar.getTime()); + record.setCreateTime(new Date()); + recordMapper.insert(record); + } + + private void publishEvent(String eventType, Long roomId, Long messageId, Long userId, Long targetUserId, Integer riskLevel, String payload) { + IeChatEvent event = new IeChatEvent(); + event.setEventType(eventType); + event.setRoomId(roomId); + event.setMessageId(messageId); + event.setUserId(userId); + event.setTargetUserId(targetUserId); + event.setRiskLevel(riskLevel); + event.setPayload(payload); + String json = JSONUtil.toJsonStr(event); + redisService.publishChatEvent(json); + chatEventProducer.sendEvent(event); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMatchServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMatchServiceImpl.java new file mode 100644 index 00000000..e7d423c9 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMatchServiceImpl.java @@ -0,0 +1,589 @@ +package cc.hiver.mall.ie.service.impl; + +import cc.hiver.mall.ie.constant.IeConstants; +import cc.hiver.mall.ie.dto.IeMatchStartDTO; +import cc.hiver.mall.ie.dto.IeProfileDTO; +import cc.hiver.mall.ie.dto.IeStatusDTO; +import cc.hiver.mall.ie.entity.*; +import cc.hiver.mall.ie.mapper.*; +import cc.hiver.mall.ie.service.IeMatchService; +import cc.hiver.mall.ie.service.IeRedisService; +import cc.hiver.mall.ie.service.IeSecurityAuditService; +import cc.hiver.mall.ie.vo.IeAuditResult; +import cc.hiver.mall.ie.vo.IeHomeVO; +import cc.hiver.mall.ie.vo.IeMatchVO; +import cc.hiver.mall.ie.vo.IeUserProfileVO; +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; + +@Service +public class IeMatchServiceImpl implements IeMatchService { + + @Autowired + private IeUserProfileMapper userProfileMapper; + + @Autowired + private IeUserStatusMapper userStatusMapper; + + @Autowired + private IeMatchAttemptMapper matchAttemptMapper; + + @Autowired + private IeRoomMapper roomMapper; + + @Autowired + private IeRecordMapper recordMapper; + + @Autowired + private IeRedisService redisService; + + @Autowired + private IeSecurityAuditService auditService; + + @Override + public IeHomeVO home(Long userId) { + IeUserProfile profile = ensureProfile(userId); + redisService.refreshOnline(userId); + IeHomeVO vo = new IeHomeVO(); + vo.setOnlineCount(redisService.onlineCount()); + vo.setWaitingCount(redisService.waitingCount(profile.getCurrentMode(), "quiet")); + vo.setDailyQuota(profile.getDailyQuota()); + vo.setUsedQuota((int) redisService.todayUsedQuota(userId)); + vo.setCurrentMode(profile.getCurrentMode()); + vo.setCurrentMood("quiet"); + vo.setTargetModePreference(profile.getTargetModePreference()); + vo.setTargetGenderPreference(profile.getTargetGenderPreference()); + vo.setProfileCompleted(profile.getProfileCompleted() == null ? 0 : profile.getProfileCompleted()); + vo.setProfile(toProfileVO(profile)); + vo.setHotStatuses(redisService.hotStatuses(8)); + return vo; + } + + @Override + public IeUserProfileVO profile(Long userId) { + return toProfileVO(ensureProfile(userId)); + } + + @Override + public IeUserProfileVO profileByUserId(Long userId, Long targetUserId) { + if (targetUserId == null || targetUserId.equals(userId)) { + return toProfileVO(ensureProfile(userId)); + } + IeUserProfile profile = findProfileByUserId(targetUserId); + if (profile == null || profile.getProfileCompleted() == null || profile.getProfileCompleted() != 1) { + throw new RuntimeException("瀵规柟璧勬枡鏆傛椂涓嶅彲瑙"); + } + return toProfileVO(profile); + } + + @Override + @Transactional + public IeUserProfileVO saveProfile(Long userId, IeProfileDTO dto) { + IeUserProfile profile = ensureProfile(userId); + if (dto == null) { + dto = new IeProfileDTO(); + } + String anonymousName = localAuditProfileText(defaultText(dto.getAnonymousName(), profile.getAnonymousName())); + String intro = localAuditProfileText(defaultText(dto.getIntro(), "")); + profile.setAnonymousName(anonymousName); + profile.setAvatarText(defaultText(dto.getAvatarText(), "鎴")); + if (dto.getAvatarUrl() != null && !dto.getAvatarUrl().trim().isEmpty()) { + profile.setAvatarUrl(dto.getAvatarUrl()); + } + profile.setGender(normalizeGender(dto.getGender(), "unknown")); + profile.setIntro(intro); + profile.setInterestTags(tagsToJson(dto.getInterestTags())); + profile.setCurrentMode(normalizeMode(dto.getCurrentMode(), "i")); + profile.setRecentPreference(profile.getCurrentMode()); + profile.setTargetModePreference(normalizeTargetMode(dto.getTargetModePreference())); + profile.setTargetGenderPreference(normalizeTargetGender(dto.getTargetGenderPreference())); + profile.setProfileCompleted(1); + profile.setLastActiveTime(new Date()); + profile.setUpdateTime(new Date()); + userProfileMapper.updateById(profile); + IeStatusDTO statusDTO = new IeStatusDTO(); + statusDTO.setMode(profile.getCurrentMode()); + statusDTO.setMood("quiet"); + statusDTO.setStatusText(defaultText(profile.getIntro(), profile.getCurrentMode())); + statusDTO.setInterestTags(jsonToTags(profile.getInterestTags())); + updateStatus(userId, statusDTO); + return toProfileVO(profile); + } + + @Override + public void updateStatus(Long userId, IeStatusDTO dto) { + if (dto == null) { + dto = new IeStatusDTO(); + } + if (dto.getMode() == null) { + dto.setMode("i"); + } + if (dto.getMood() == null) { + dto.setMood("quiet"); + } + ensureProfile(userId); + IeUserStatus status = userStatusMapper.selectOne(new LambdaQueryWrapper() + .eq(IeUserStatus::getUserId, userId) + .last("limit 1")); + Date now = new Date(); + if (status == null) { + status = new IeUserStatus(); + status.setUserId(userId); + status.setCreateTime(now); + } + status.setMode(dto.getMode()); + status.setMood(dto.getMood()); + status.setStatusText(dto.getStatusText()); + status.setOnlineStatus(1); + status.setLastActiveTime(now); + status.setUpdateTime(now); + if (status.getId() == null) { + userStatusMapper.insert(status); + } else { + userStatusMapper.updateById(status); + } + redisService.refreshOnline(userId); + redisService.cacheUserStatus(userId, dto); + redisService.addToMatchPool(userId, dto); + } + + @Override + @Transactional + public IeMatchVO startMatch(Long userId, IeMatchStartDTO dto) { + IeUserProfile profile = ensureProfile(userId); + long maxQuota = profile.getDailyQuota() == null ? IeConstants.DEFAULT_DAILY_QUOTA : profile.getDailyQuota(); + if (!redisService.tryLockMatchUser(userId, 8)) { + return fail(userId, dto, "姝e湪鍖归厤涓紝璇风◢鍚"); + } + + Long targetUserId = null; + try { + if (redisService.todayUsedQuota(userId) >= maxQuota) { + return fail(userId, dto, "浠婃棩闄即鏈轰細宸茬粡鐢ㄥ畬"); + } + IeStatusDTO statusDTO = new IeStatusDTO(); + BeanUtils.copyProperties(dto == null ? new IeMatchStartDTO() : dto, statusDTO); + updateStatus(userId, statusDTO); + + String mode = normalizeMode(statusDTO.getMode(), profile.getCurrentMode()); + String targetMode = normalizeTargetMode(dto == null ? null : dto.getTargetMode()); + String requestedTargetGender = dto == null || dto.getTargetGender() == null ? profile.getTargetGenderPreference() : dto.getTargetGender(); + String targetGender = normalizeTargetGender(requestedTargetGender); + String mood = statusDTO.getMood() == null ? "quiet" : statusDTO.getMood(); + List candidates = candidateUsers(targetMode, mode, mood); + List matchedCandidates = new ArrayList<>(); + for (Long candidateUserId : candidates) { + if (candidateUserId == null || candidateUserId.equals(userId)) { + continue; + } + if (redisService.isRecentlyMatched(userId, candidateUserId)) { + continue; + } + IeUserProfile candidateProfile = findProfileByUserId(candidateUserId); + if (!matchCandidate(candidateProfile, targetMode, targetGender)) { + continue; + } + matchedCandidates.add(new MatchCandidate(candidateUserId, matchScore(statusDTO, profile, candidateProfile))); + } + matchedCandidates.sort(Comparator.comparingInt(MatchCandidate::getScore).reversed()); + for (MatchCandidate candidate : matchedCandidates) { + if (!redisService.tryLockMatchUser(candidate.getUserId(), 8)) { + continue; + } + targetUserId = candidate.getUserId(); + break; + } + if (targetUserId == null) { + return fail(userId, dto, "鏆傛椂娌℃湁鍚岄鐨勪汉锛岀◢鍚庡啀璇曡瘯"); + } + long used = redisService.increaseTodayQuota(userId, maxQuota); + if (used > maxQuota) { + return fail(userId, dto, "浠婃棩闄即鏈轰細宸茬粡鐢ㄥ畬"); + } + IeUserProfile targetProfile = ensureProfile(targetUserId); + + IeMatchAttempt match = new IeMatchAttempt(); + match.setMatchNo(IdUtil.fastSimpleUUID()); + match.setUserId(userId); + match.setTargetUserId(targetUserId); + match.setMode(mode); + match.setMood(mood); + match.setStatus(3); + match.setAnonymousName(defaultText(targetProfile.getAnonymousName(), "鍗婂尶鍚嶆紓娴佽")); + match.setAvatarText(defaultText(targetProfile.getAvatarText(), "鈼")); + match.setStateText(targetProfile.getCurrentMode() == null || "i".equals(targetProfile.getCurrentMode()) ? "鍋忓畨闈欙紝閫傚悎鎱㈡參闈犺繎" : "鍋忚交鏉撅紝鎰挎剰鍏堝紑鍦"); + match.setQuoteText(defaultText(targetProfile.getIntro(), "鍙互鍏堝畨闈欏緟涓浼氾紝涓嶇敤鎬ョ潃鎵捐瘽棰樸")); + match.setCreateTime(new Date()); + match.setUpdateTime(new Date()); + matchAttemptMapper.insert(match); + + IeRoom room = createRoom(match, userId, targetUserId, mode, mood); + createChatRecord(room, userId, targetUserId, targetProfile); + createChatRecord(room, targetUserId, userId, profile); + redisService.markRecentlyMatched(userId, targetUserId); + redisService.markRecentlyMatched(targetUserId, userId); + + IeMatchVO vo = toMatchVO(match); + vo.setRoomId(room.getId()); + vo.setRoomNo(room.getRoomNo()); + vo.setAvatarUrl(targetProfile.getAvatarUrl()); + return vo; + } finally { + redisService.unlockMatchUser(userId); + if (targetUserId != null) { + redisService.unlockMatchUser(targetUserId); + } + } + } + + private IeRoom createRoom(IeMatchAttempt match, Long userId, Long targetUserId, String mode, String mood) { + Date now = new Date(); + IeRoom room = new IeRoom(); + room.setRoomNo(IdUtil.fastSimpleUUID()); + room.setMatchId(match.getId()); + room.setUserAId(userId); + room.setUserBId(targetUserId); + room.setMode(mode); + room.setMood(mood); + room.setStatus(IeConstants.ROOM_STATUS_ACTIVE); + room.setStartTime(now); + room.setExpireTime(longTermExpireTime(now)); + room.setCreateTime(now); + room.setUpdateTime(now); + roomMapper.insert(room); + redisService.cacheRoom(room.getId(), JSONUtil.toJsonStr(room), 20 * 60L); + return room; + } + + private Date longTermExpireTime(Date startTime) { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(startTime == null ? new Date() : startTime); + calendar.add(Calendar.YEAR, 10); + return calendar.getTime(); + } + + private void createChatRecord(IeRoom room, Long userId, Long targetUserId, IeUserProfile targetProfile) { + IeRecord exists = recordMapper.selectOne(new LambdaQueryWrapper() + .eq(IeRecord::getRoomId, room.getId()) + .eq(IeRecord::getUserId, userId) + .last("limit 1")); + if (exists != null) { + return; + } + IeRecord record = new IeRecord(); + record.setRoomId(room.getId()); + record.setUserId(userId); + record.setTargetUserId(targetUserId); + record.setMode(room.getMode()); + record.setMood(room.getMood()); + record.setAnonymousName(defaultText(targetProfile == null ? null : targetProfile.getAnonymousName(), "鍗婂尶鍚嶆紓娴佽")); + record.setDurationSeconds(0); + record.setSummary("浣犱滑宸茬粡鍖归厤鎴愬姛锛屽彲浠ヤ粠杩欓噷鍥炲埌鑱婂ぉ缁х画瀵硅瘽銆"); + record.setTags(room.getMode() + "," + room.getMood() + ",缁х画鑱婂ぉ"); + record.setRemeetAvailable(1); + record.setCreateTime(new Date()); + recordMapper.insert(record); + } + + private IeMatchVO fail(Long userId, IeMatchStartDTO dto, String reason) { + IeMatchAttempt match = new IeMatchAttempt(); + match.setMatchNo(IdUtil.fastSimpleUUID()); + match.setUserId(userId); + match.setMode(dto == null || dto.getMode() == null ? "i" : dto.getMode()); + match.setMood(dto == null || dto.getMood() == null ? "quiet" : dto.getMood()); + match.setStatus(4); + match.setFailReason(reason); + match.setCreateTime(new Date()); + match.setUpdateTime(new Date()); + matchAttemptMapper.insert(match); + IeMatchVO vo = toMatchVO(match); + vo.setFailReason(reason); + return vo; + } + + private IeMatchVO toMatchVO(IeMatchAttempt match) { + IeMatchVO vo = new IeMatchVO(); + vo.setMatchId(match.getId()); + vo.setMatchNo(match.getMatchNo()); + vo.setTargetUserId(match.getTargetUserId()); + vo.setAnonymousName(match.getAnonymousName()); + vo.setAvatarText(match.getAvatarText()); + vo.setMode(match.getMode()); + vo.setMood(match.getMood()); + vo.setStateText(match.getStateText()); + vo.setQuoteText(match.getQuoteText()); + vo.setStatus(match.getStatus()); + vo.setFailReason(match.getFailReason()); + return vo; + } + + private IeUserProfile ensureProfile(Long userId) { + IeUserProfile profile = findProfileByUserId(userId); + if (profile != null) { + return profile; + } + profile = new IeUserProfile(); + profile.setUserId(userId); + profile.setAnonymousName("鍗婂尶鍚嶆紓娴佽"); + profile.setAvatarText("澶"); + profile.setCurrentMode("i"); + profile.setRecentPreference("i"); + profile.setTargetModePreference("any"); + profile.setGender("unknown"); + profile.setTargetGenderPreference("any"); + profile.setDefaultRoomMinutes(IeConstants.DEFAULT_ROOM_MINUTES); + profile.setDailyQuota(IeConstants.DEFAULT_DAILY_QUOTA); + profile.setUsedQuota(0); + profile.setProfileCompleted(0); + profile.setIsDeleted(0); + profile.setCreateTime(new Date()); + profile.setUpdateTime(new Date()); + try { + userProfileMapper.insert(profile); + return profile; + } catch (DuplicateKeyException ignored) { + // 骞跺彂杩涘叆棣栭〉鏃讹紝鍙︿竴涓姹傚彲鑳藉凡缁忓垱寤烘垚鍔燂紱閲嶆柊璇诲彇鍗冲彲淇濇寔骞傜瓑銆 + IeUserProfile exists = findProfileByUserId(userId); + if (exists != null) { + return exists; + } + throw ignored; + } + } + + private IeUserProfile findProfileByUserId(Long userId) { + return userProfileMapper.selectOne(new LambdaQueryWrapper() + .eq(IeUserProfile::getUserId, userId) + .last("limit 1")); + } + + private List candidateUsers(String targetMode, String selfMode, String mood) { + LinkedHashSet set = new LinkedHashSet<>(); + if (!"any".equals(targetMode)) { + set.addAll(redisService.candidates(targetMode, mood, 50)); + set.addAll(statusCandidates(targetMode, 50)); + set.addAll(profileCandidates(targetMode, 50)); + return new ArrayList<>(set); + } + set.addAll(redisService.candidates(selfMode, mood, 30)); + set.addAll(redisService.candidates("i".equals(selfMode) ? "e" : "i", mood, 30)); + set.addAll(redisService.candidates("i", mood, 20)); + set.addAll(redisService.candidates("e", mood, 20)); + set.addAll(statusCandidates(selfMode, 30)); + set.addAll(statusCandidates("i".equals(selfMode) ? "e" : "i", 30)); + set.addAll(profileCandidates(selfMode, 30)); + set.addAll(profileCandidates("i".equals(selfMode) ? "e" : "i", 30)); + return new ArrayList<>(set); + } + + private List statusCandidates(String mode, int limit) { + List statuses = userStatusMapper.selectList(new LambdaQueryWrapper() + .eq(IeUserStatus::getOnlineStatus, 1) + .eq(IeUserStatus::getMode, normalizeMode(mode, "i")) + .orderByDesc(IeUserStatus::getLastActiveTime) + .last("limit " + Math.max(1, Math.min(limit, 100)))); + List users = new ArrayList<>(); + for (IeUserStatus status : statuses) { + if (status.getUserId() != null) { + users.add(status.getUserId()); + } + } + return users; + } + + private List profileCandidates(String mode, int limit) { + List profiles = userProfileMapper.selectList(new LambdaQueryWrapper() + .eq(IeUserProfile::getProfileCompleted, 1) + .eq(IeUserProfile::getCurrentMode, normalizeMode(mode, "i")) + .eq(IeUserProfile::getIsDeleted, 0) + .orderByDesc(IeUserProfile::getLastActiveTime) + .last("limit " + Math.max(1, Math.min(limit, 100)))); + List users = new ArrayList<>(); + for (IeUserProfile profile : profiles) { + if (profile.getUserId() != null) { + users.add(profile.getUserId()); + } + } + return users; + } + + private String auditProfileText(String bizType, Long userId, String content) { + IeAuditResult result = auditService.audit(bizType, userId, userId, content); + if (Boolean.TRUE.equals(result.getBlocked())) { + throw new RuntimeException("璧勬枡鍐呭鍖呭惈涓嶉傚悎灞曠ず鐨勮〃杈撅紝璇锋崲涓绉嶈娉"); + } + return result.getFilteredContent(); + } + + private String localAuditProfileText(String content) { + if (content == null) { + return ""; + } + String lower = content.toLowerCase(); + String[] riskyWords = {"榛勮壊", "鏀挎不", "杈遍獋", "鍌婚", "鎿嶄綘", "绾︾偖", "鑹叉儏"}; + for (String word : riskyWords) { + if (lower.contains(word)) { + throw new RuntimeException("璧勬枡鍐呭鍖呭惈涓嶉傚悎灞曠ず鐨勮〃杈撅紝璇锋崲涓绉嶈娉"); + } + } + return content; + } + + private IeUserProfileVO toProfileVO(IeUserProfile profile) { + IeUserProfileVO vo = new IeUserProfileVO(); + vo.setUserId(profile.getUserId()); + vo.setAnonymousName(profile.getAnonymousName()); + vo.setAvatarText(profile.getAvatarText()); + vo.setAvatarUrl(profile.getAvatarUrl()); + vo.setGender(profile.getGender()); + vo.setIntro(profile.getIntro()); + vo.setInterestTags(jsonToTags(profile.getInterestTags())); + vo.setCurrentMode(profile.getCurrentMode()); + vo.setRecentPreference(profile.getRecentPreference()); + vo.setTargetModePreference(profile.getTargetModePreference()); + vo.setTargetGenderPreference(profile.getTargetGenderPreference()); + vo.setDefaultRoomMinutes(profile.getDefaultRoomMinutes()); + vo.setDailyQuota(profile.getDailyQuota()); + vo.setUsedQuota(profile.getUsedQuota()); + vo.setProfileCompleted(profile.getProfileCompleted() == null ? 0 : profile.getProfileCompleted()); + return vo; + } + + private String defaultText(String value, String fallback) { + return value == null || value.trim().isEmpty() ? fallback : value.trim(); + } + + private String normalizeMode(String value, String fallback) { + if ("e".equalsIgnoreCase(value)) { + return "e"; + } + if ("i".equalsIgnoreCase(value)) { + return "i"; + } + return fallback == null ? "i" : fallback; + } + + private String normalizeTargetMode(String value) { + if ("i".equalsIgnoreCase(value) || "e".equalsIgnoreCase(value)) { + return value.toLowerCase(); + } + return "any"; + } + + private String normalizeGender(String value, String fallback) { + if ("male".equalsIgnoreCase(value) || "female".equalsIgnoreCase(value)) { + return value.toLowerCase(); + } + if ("unknown".equalsIgnoreCase(value)) { + return "unknown"; + } + return fallback == null ? "unknown" : fallback; + } + + private String normalizeTargetGender(String value) { + if ("male".equalsIgnoreCase(value) || "female".equalsIgnoreCase(value)) { + return value.toLowerCase(); + } + return "any"; + } + + private boolean matchCandidate(IeUserProfile candidateProfile, String selfTargetMode, String selfTargetGender) { + if (candidateProfile == null) { + return false; + } + String candidateMode = normalizeMode(candidateProfile.getCurrentMode(), "i"); + if (!"any".equals(selfTargetMode) && !selfTargetMode.equals(candidateMode)) { + return false; + } + return matchGender(candidateProfile, selfTargetGender); + } + + private boolean matchGender(IeUserProfile profile, String targetGender) { + if ("any".equals(targetGender)) { + return true; + } + return profile != null && targetGender.equals(profile.getGender()); + } + + private int matchScore(IeStatusDTO selfStatus, IeUserProfile selfProfile, IeUserProfile candidateProfile) { + int score = 0; + List selfTags = new ArrayList<>(); + if (selfStatus != null && selfStatus.getInterestTags() != null) { + selfTags.addAll(selfStatus.getInterestTags()); + } + if (selfProfile != null) { + selfTags.addAll(jsonToTags(selfProfile.getInterestTags())); + } + List candidateTags = candidateProfile == null ? new ArrayList<>() : jsonToTags(candidateProfile.getInterestTags()); + for (String tag : selfTags) { + if (tag != null && candidateTags.contains(tag)) { + score += 30; + } + } + if (selfProfile != null && candidateProfile != null) { + String selfMode = normalizeMode(selfProfile.getCurrentMode(), "i"); + String candidateTargetMode = normalizeTargetMode(candidateProfile.getTargetModePreference()); + if ("any".equals(candidateTargetMode) || candidateTargetMode.equals(selfMode)) { + score += 20; + } + String selfTargetGender = normalizeTargetGender(selfProfile.getTargetGenderPreference()); + if ("any".equals(selfTargetGender) || selfTargetGender.equals(candidateProfile.getGender())) { + score += 10; + } + if (candidateProfile.getIntro() != null && !candidateProfile.getIntro().trim().isEmpty()) { + score += 5; + } + } + return score; + } + + private String tagsToJson(List tags) { + if (tags == null) { + return "[]"; + } + return JSONUtil.toJsonStr(tags); + } + + private List jsonToTags(String tags) { + if (tags == null || tags.trim().isEmpty()) { + return new ArrayList<>(); + } + try { + return JSONUtil.toList(JSONUtil.parseArray(tags), String.class); + } catch (Exception ignored) { + List list = new ArrayList<>(); + for (String tag : tags.split(",")) { + if (tag != null && !tag.trim().isEmpty()) { + list.add(tag.trim()); + } + } + return list; + } + } + + private static class MatchCandidate { + private final Long userId; + private final int score; + + private MatchCandidate(Long userId, int score) { + this.userId = userId; + this.score = score; + } + + private Long getUserId() { + return userId; + } + + private int getScore() { + return score; + } + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeRedisServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeRedisServiceImpl.java new file mode 100644 index 00000000..3d3d2802 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeRedisServiceImpl.java @@ -0,0 +1,275 @@ +package cc.hiver.mall.ie.service.impl; + +import cc.hiver.mall.ie.constant.IeRedisKey; +import cc.hiver.mall.ie.dto.IeStatusDTO; +import cc.hiver.mall.ie.service.IeRedisService; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.date.DateUtil; +import cn.hutool.json.JSONUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Service +public class IeRedisServiceImpl implements IeRedisService { + + private static final long ONLINE_SECONDS = 90L; + private static final long STATUS_SECONDS = 300L; + private static final long RECENT_MATCH_MINUTES = 10L; + private static final long ROOM_CACHE_SECONDS = 20 * 60L; + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + @Override + public void refreshOnline(Long userId) { + long now = System.currentTimeMillis(); + stringRedisTemplate.opsForZSet().add(IeRedisKey.ONLINE_USERS, String.valueOf(userId), now); + stringRedisTemplate.opsForZSet().removeRangeByScore(IeRedisKey.ONLINE_USERS, 0, now - ONLINE_SECONDS * 1000); + } + + @Override + public void cacheUserStatus(Long userId, IeStatusDTO dto) { + String key = IeRedisKey.USER_STATUS + userId; + stringRedisTemplate.opsForHash().put(key, "mode", defaultText(dto.getMode(), "i")); + stringRedisTemplate.opsForHash().put(key, "mood", defaultText(dto.getMood(), "quiet")); + stringRedisTemplate.opsForHash().put(key, "statusText", defaultText(dto.getStatusText(), "")); + stringRedisTemplate.opsForHash().put(key, "longitude", dto.getLongitude() == null ? "" : String.valueOf(dto.getLongitude())); + stringRedisTemplate.opsForHash().put(key, "latitude", dto.getLatitude() == null ? "" : String.valueOf(dto.getLatitude())); + stringRedisTemplate.opsForHash().put(key, "interestTags", dto.getInterestTags() == null ? "[]" : JSONUtil.toJsonStr(dto.getInterestTags())); + stringRedisTemplate.opsForHash().put(key, "lastActiveTime", DateUtil.now()); + stringRedisTemplate.expire(key, STATUS_SECONDS, TimeUnit.SECONDS); + if (dto.getStatusText() != null && dto.getStatusText().trim().length() > 0) { + stringRedisTemplate.opsForZSet().incrementScore(IeRedisKey.HOT_STATUS, dto.getStatusText().trim(), 1D); + } + } + + @Override + public void addToMatchPool(Long userId, IeStatusDTO dto) { + removeFromMatchPools(userId); + String mode = defaultText(dto.getMode(), "i"); + String mood = defaultText(dto.getMood(), "quiet"); + String poolKey = IeRedisKey.matchPool(mode, mood); + double score = System.currentTimeMillis(); + if ("i".equals(mode)) { + score += 5000; + } + if (dto.getInterestTags() != null) { + score += dto.getInterestTags().size() * 100; + } + stringRedisTemplate.opsForZSet().add(poolKey, String.valueOf(userId), score); + stringRedisTemplate.expire(poolKey, STATUS_SECONDS, TimeUnit.SECONDS); + stringRedisTemplate.opsForValue().set(IeRedisKey.USER_MATCH_POOL + userId, poolKey, STATUS_SECONDS, TimeUnit.SECONDS); + } + + @Override + public void removeFromMatchPools(Long userId) { + String poolKey = stringRedisTemplate.opsForValue().get(IeRedisKey.USER_MATCH_POOL + userId); + if (poolKey != null && poolKey.length() > 0) { + stringRedisTemplate.opsForZSet().remove(poolKey, String.valueOf(userId)); + } + stringRedisTemplate.delete(IeRedisKey.USER_MATCH_POOL + userId); + } + + @Override + public boolean tryLockMatchUser(Long userId, long seconds) { + Boolean locked = stringRedisTemplate.opsForValue().setIfAbsent(IeRedisKey.MATCH_LOCK + userId, "1", seconds, TimeUnit.SECONDS); + return Boolean.TRUE.equals(locked); + } + + @Override + public void unlockMatchUser(Long userId) { + stringRedisTemplate.delete(IeRedisKey.MATCH_LOCK + userId); + } + + @Override + public List candidates(String mode, String mood, int limit) { + List keys = new ArrayList<>(); + String targetMode = defaultText(mode, "i"); + String targetMood = defaultText(mood, "quiet"); + keys.add(IeRedisKey.matchPool(targetMode, targetMood)); + if (!"quiet".equals(targetMood)) { + keys.add(IeRedisKey.matchPool(targetMode, "quiet")); + } + addPoolKey(keys, targetMode, "talk"); + addPoolKey(keys, targetMode, "listen"); + addPoolKey(keys, targetMode, "drift"); + List result = new ArrayList<>(); + for (String key : keys) { + Set members = stringRedisTemplate.opsForZSet().reverseRange(key, 0, limit - 1L); + if (CollectionUtil.isEmpty(members)) { + continue; + } + for (String member : members) { + result.add(Long.valueOf(member)); + if (result.size() >= limit) { + return result; + } + } + } + return result; + } + + private void addPoolKey(List keys, String mode, String mood) { + String key = IeRedisKey.matchPool(mode, mood); + if (!keys.contains(key)) { + keys.add(key); + } + } + + @Override + public boolean isRecentlyMatched(Long userId, Long targetUserId) { + Boolean result = stringRedisTemplate.opsForSet().isMember(IeRedisKey.RECENT_MATCH + userId, String.valueOf(targetUserId)); + return Boolean.TRUE.equals(result); + } + + @Override + public void markRecentlyMatched(Long userId, Long targetUserId) { + String key = IeRedisKey.RECENT_MATCH + userId; + stringRedisTemplate.opsForSet().add(key, String.valueOf(targetUserId)); + stringRedisTemplate.expire(key, RECENT_MATCH_MINUTES, TimeUnit.MINUTES); + } + + @Override + public long todayUsedQuota(Long userId) { + String value = stringRedisTemplate.opsForValue().get(dailyQuotaKey(userId)); + return value == null ? 0L : Long.parseLong(value); + } + + @Override + public long increaseTodayQuota(Long userId, long maxQuota) { + String key = dailyQuotaKey(userId); + Long value = stringRedisTemplate.opsForValue().increment(key); + if (value != null && value == 1L) { + stringRedisTemplate.expire(key, Duration.between(LocalDateTime.now(), LocalDateTime.now().with(LocalTime.MAX))); + } + long used = value == null ? 1L : value; + if (used > maxQuota) { + stringRedisTemplate.opsForValue().decrement(key); + } + return used; + } + + @Override + public void cacheRoom(Long roomId, String roomJson, long seconds) { + stringRedisTemplate.opsForValue().set(IeRedisKey.ROOM + roomId, roomJson, seconds <= 0 ? ROOM_CACHE_SECONDS : seconds, TimeUnit.SECONDS); + } + + @Override + public String getCachedRoom(Long roomId) { + return stringRedisTemplate.opsForValue().get(IeRedisKey.ROOM + roomId); + } + + @Override + public void pushOfflineMessage(Long userId, String messageJson) { + String key = IeRedisKey.OFFLINE_MESSAGE + userId; + stringRedisTemplate.opsForList().leftPush(key, messageJson); + stringRedisTemplate.opsForList().trim(key, 0, 99); + stringRedisTemplate.expire(key, 24, TimeUnit.HOURS); + } + + @Override + public List popOfflineMessages(Long userId, int limit) { + String key = IeRedisKey.OFFLINE_MESSAGE + userId; + List messages = stringRedisTemplate.opsForList().range(key, 0, Math.max(limit - 1, 0)); + if (messages != null && !messages.isEmpty()) { + stringRedisTemplate.opsForList().trim(key, messages.size(), -1); + } + return messages == null ? new ArrayList<>() : messages; + } + + @Override + public void cacheWebSocketConnection(Long userId, String sessionId) { + String key = IeRedisKey.WS_CONNECTION + userId; + stringRedisTemplate.opsForHash().put(key, "sessionId", sessionId == null ? "" : sessionId); + stringRedisTemplate.opsForHash().put(key, "connectTime", DateUtil.now()); + stringRedisTemplate.expire(key, ONLINE_SECONDS, TimeUnit.SECONDS); + if (sessionId != null && sessionId.length() > 0) { + stringRedisTemplate.opsForValue().set(IeRedisKey.WS_SESSION_USER + sessionId, String.valueOf(userId), ONLINE_SECONDS, TimeUnit.SECONDS); + } + } + + @Override + public void removeWebSocketConnection(Long userId) { + String key = IeRedisKey.WS_CONNECTION + userId; + Object sessionId = stringRedisTemplate.opsForHash().get(key, "sessionId"); + if (sessionId != null) { + stringRedisTemplate.delete(IeRedisKey.WS_SESSION_USER + sessionId); + } + stringRedisTemplate.delete(key); + } + + @Override + public Long userIdBySession(String sessionId) { + String value = stringRedisTemplate.opsForValue().get(IeRedisKey.WS_SESSION_USER + sessionId); + return value == null ? null : Long.valueOf(value); + } + + @Override + public boolean allowMessage(Long userId, Long roomId) { + String key = IeRedisKey.MESSAGE_RATE + roomId + ":" + userId; + Long count = stringRedisTemplate.opsForValue().increment(key); + if (count != null && count == 1L) { + stringRedisTemplate.expire(key, 10, TimeUnit.SECONDS); + } + return count == null || count <= 8; + } + + @Override + public long increaseRisk(Long userId, long seconds) { + String key = IeRedisKey.USER_RISK + userId; + Long count = stringRedisTemplate.opsForValue().increment(key); + if (count != null && count == 1L) { + stringRedisTemplate.expire(key, seconds, TimeUnit.SECONDS); + } + return count == null ? 1L : count; + } + + @Override + public void publishChatEvent(String eventJson) { + stringRedisTemplate.convertAndSend(IeRedisKey.CHAT_EVENT_CHANNEL, eventJson); + } + + @Override + public long onlineCount() { + long now = System.currentTimeMillis(); + stringRedisTemplate.opsForZSet().removeRangeByScore(IeRedisKey.ONLINE_USERS, 0, now - ONLINE_SECONDS * 1000); + Long count = stringRedisTemplate.opsForZSet().zCard(IeRedisKey.ONLINE_USERS); + return count == null ? 0L : count; + } + + @Override + public long waitingCount(String mode, String mood) { + Long count = stringRedisTemplate.opsForZSet().zCard(IeRedisKey.matchPool(defaultText(mode, "i"), defaultText(mood, "quiet"))); + return count == null ? 0L : count; + } + + @Override + public List hotStatuses(int limit) { + Set values = stringRedisTemplate.opsForZSet().reverseRange(IeRedisKey.HOT_STATUS, 0, Math.max(limit - 1, 0)); + if (values == null) { + return new ArrayList<>(); + } + return values.stream().collect(Collectors.toList()); + } + + private String dailyQuotaKey(Long userId) { + String date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); + return IeRedisKey.DAILY_QUOTA + date + ":" + userId; + } + + private String defaultText(String value, String defaultValue) { + return value == null || value.trim().isEmpty() ? defaultValue : value.trim(); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeSecurityAuditServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeSecurityAuditServiceImpl.java new file mode 100644 index 00000000..638b31c3 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeSecurityAuditServiceImpl.java @@ -0,0 +1,184 @@ +package cc.hiver.mall.ie.service.impl; + +import cc.hiver.mall.ie.constant.IeConstants; +import cc.hiver.mall.ie.constant.IeRedisKey; +import cc.hiver.mall.ie.entity.IeContentAuditLog; +import cc.hiver.mall.ie.entity.IeSensitiveWord; +import cc.hiver.mall.ie.mapper.IeContentAuditLogMapper; +import cc.hiver.mall.ie.mapper.IeSensitiveWordMapper; +import cc.hiver.mall.ie.service.IeSecurityAuditService; +import cc.hiver.mall.ie.service.IeWechatSecurityService; +import cc.hiver.mall.ie.vo.IeAuditResult; +import cc.hiver.mall.ie.vo.IeWechatMsgSecResult; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.text.CharSequenceUtil; +import cn.hutool.json.JSONUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.github.houbb.sensitive.word.api.IWordDeny; +import com.github.houbb.sensitive.word.bs.SensitiveWordBs; +import com.github.houbb.sensitive.word.support.deny.WordDenys; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Service +public class IeSecurityAuditServiceImpl implements IeSecurityAuditService { + + private volatile SensitiveWordBs cachedWordBs; + private volatile Map cachedWordMap = new HashMap<>(); + private volatile long cachedExpireAt = 0L; + + @Autowired + private IeSensitiveWordMapper sensitiveWordMapper; + + @Autowired + private IeContentAuditLogMapper auditLogMapper; + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + @Autowired + private IeWechatSecurityService wechatSecurityService; + + @Override + public IeAuditResult audit(String bizType, Long bizId, Long userId, String content) { + return audit(bizType, bizId, userId, null, content); + } + + @Override + public IeAuditResult audit(String bizType, Long bizId, Long userId, String openid, String content) { + IeAuditResult result = new IeAuditResult(); + result.setRawContent(content); + result.setFilteredContent(content); + result.setAuditStatus(IeConstants.AUDIT_PASS); + result.setRiskLevel(IeConstants.RISK_NONE); + result.setBlocked(false); + if (CharSequenceUtil.isBlank(content)) { + return result; + } + + SensitiveWordBs wordBs = getSensitiveWordBs(); + List hitWords = wordBs.findAll(content); + String filtered = CollectionUtil.isEmpty(hitWords) ? content : wordBs.replace(content); + int maxLevel = 0; + Map wordMap = cachedWordMap; + if (CollectionUtil.isNotEmpty(hitWords)) { + for (String hitWord : hitWords) { + if (CharSequenceUtil.isBlank(hitWord)) { + continue; + } + IeSensitiveWord word = wordMap.get(hitWord.toLowerCase()); + Integer level = word == null || word.getLevel() == null ? 1 : word.getLevel(); + maxLevel = Math.max(maxLevel, level); + result.getHitWords().add(hitWord); + result.getHitCategories().add(word == null ? "builtin" : word.getCategory()); + } + } + + result.setFilteredContent(filtered); + if (maxLevel >= 3) { + result.setAuditStatus(IeConstants.AUDIT_BLOCK); + result.setRiskLevel(IeConstants.RISK_HIGH); + result.setBlocked(true); + } else if (maxLevel == 2) { + result.setAuditStatus(IeConstants.AUDIT_BLOCK); + result.setRiskLevel(IeConstants.RISK_MEDIUM); + result.setBlocked(true); + } else if (maxLevel == 1) { + result.setAuditStatus(IeConstants.AUDIT_REPLACE); + result.setRiskLevel(IeConstants.RISK_LOW); + } + + if (!Boolean.TRUE.equals(result.getBlocked())) { + IeWechatMsgSecResult wxResult = wechatSecurityService.msgSecCheck(openid, content); + if (wxResult.isChecked() && !wxResult.isPass()) { + result.setAuditStatus(IeConstants.AUDIT_BLOCK); + result.setRiskLevel(IeConstants.RISK_HIGH); + result.setBlocked(true); + result.getHitWords().add("wechat:" + wxResult.getSuggest()); + result.getHitCategories().add(wxResult.getLabel() == null ? "wechat" : wxResult.getLabel()); + } + } + + if (CollectionUtil.isNotEmpty(result.getHitWords())) { + IeContentAuditLog log = new IeContentAuditLog(); + log.setBizType(bizType); + log.setBizId(bizId); + log.setUserId(userId); + log.setRawContent(content); + log.setFilteredContent(filtered); + log.setAuditStatus(result.getAuditStatus()); + log.setRiskLevel(result.getRiskLevel()); + log.setHitWords(String.join(",", result.getHitWords())); + log.setHitCategories(result.getHitCategories().stream().distinct().collect(Collectors.joining(","))); + log.setCreateTime(new Date()); + auditLogMapper.insert(log); + } + return result; + } + + private SensitiveWordBs getSensitiveWordBs() { + long now = System.currentTimeMillis(); + if (cachedWordBs != null && now < cachedExpireAt) { + return cachedWordBs; + } + synchronized (this) { + if (cachedWordBs != null && System.currentTimeMillis() < cachedExpireAt) { + return cachedWordBs; + } + List words = loadSensitiveWords(); + cachedWordMap = words.stream() + .collect(Collectors.toMap(item -> item.getWord().toLowerCase(), item -> item, (a, b) -> a, HashMap::new)); + cachedWordBs = buildSensitiveWordBs(words); + cachedExpireAt = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5); + return cachedWordBs; + } + } + + private SensitiveWordBs buildSensitiveWordBs(List words) { + List denyWords = words.stream() + .map(IeSensitiveWord::getWord) + .filter(CharSequenceUtil::isNotBlank) + .collect(Collectors.toList()); + return SensitiveWordBs.newInstance() + .wordDeny(WordDenys.chains(WordDenys.defaults(), new IWordDeny() { + @Override + public List deny() { + return denyWords; + } + })) + .ignoreCase(true) + .ignoreWidth(true) + .ignoreNumStyle(true) + .ignoreChineseStyle(true) + .ignoreEnglishStyle(true) + .ignoreRepeat(true) + .wordFailFast(false) + .enableEmailCheck(true) + .enableUrlCheck(true) + .enableNumCheck(true) + .init(); + } + + private List loadSensitiveWords() { + String cache = stringRedisTemplate.opsForValue().get(IeRedisKey.SENSITIVE_WORDS); + if (CharSequenceUtil.isNotBlank(cache)) { + return JSONUtil.toList(cache, IeSensitiveWord.class); + } + List words = sensitiveWordMapper.selectList(new LambdaQueryWrapper() + .eq(IeSensitiveWord::getEnabled, 1)); + words = words.stream() + .filter(item -> item != null && CharSequenceUtil.isNotBlank(item.getWord())) + .sorted((a, b) -> Integer.compare(b.getWord().length(), a.getWord().length())) + .collect(Collectors.toList()); + stringRedisTemplate.opsForValue().set(IeRedisKey.SENSITIVE_WORDS, JSONUtil.toJsonStr(words), 5, TimeUnit.MINUTES); + return words; + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeWechatSecurityServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeWechatSecurityServiceImpl.java new file mode 100644 index 00000000..1eed8498 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeWechatSecurityServiceImpl.java @@ -0,0 +1,112 @@ +package cc.hiver.mall.ie.service.impl; + +import cc.hiver.mall.ie.service.IeWechatSecurityService; +import cc.hiver.mall.ie.vo.IeWechatMsgSecResult; +import cc.hiver.mall.utils.WechatPayConfig; +import cn.hutool.core.text.CharSequenceUtil; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +public class IeWechatSecurityServiceImpl implements IeWechatSecurityService { + + private static final String ACCESS_TOKEN_KEY = "ie:wechat:access_token"; + private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}"; + private static final String MSG_SEC_CHECK_URL = "https://api.weixin.qq.com/wxa/msg_sec_check?access_token={accessToken}"; + + @Autowired + private StringRedisTemplate stringRedisTemplate; + + @Autowired + private WechatPayConfig wechatPayConfig; + + private final RestTemplate restTemplate = new RestTemplate(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public IeWechatMsgSecResult msgSecCheck(String openid, String content) { + IeWechatMsgSecResult result = new IeWechatMsgSecResult(); + result.setPass(true); + result.setChecked(false); + if (CharSequenceUtil.isBlank(openid) || CharSequenceUtil.isBlank(content)) { + return result; + } + String accessToken = getAccessToken(); + if (CharSequenceUtil.isBlank(accessToken)) { + log.warn("寰俊鍐呭瀹夊叏妫鏌ヨ烦杩囷紝access_token涓虹┖"); + return result; + } + try { + Map body = new HashMap<>(); + body.put("content", content); + body.put("version", 2); + body.put("scene", 4); + body.put("openid", openid); + ResponseEntity response = restTemplate.postForEntity(MSG_SEC_CHECK_URL, body, String.class, accessToken); + JsonNode root = objectMapper.readTree(response.getBody()); + int errcode = root.path("errcode").asInt(-1); + result.setChecked(true); + result.setErrcode(errcode); + result.setErrmsg(root.path("errmsg").asText()); + if (errcode != 0) { + log.warn("寰俊鍐呭瀹夊叏妫鏌ュけ璐 errcode={}, body={}", errcode, response.getBody()); + return result; + } + JsonNode detail = root.path("detail"); + if (detail.isArray() && detail.size() > 0) { + JsonNode first = detail.get(0); + result.setStrategy(first.path("strategy").asInt(0)); + result.setErrcode(first.path("errcode").asInt(0)); + result.setSuggest(first.path("suggest").asText(null)); + result.setLabel(first.path("label").asText(null)); + } + if (CharSequenceUtil.isBlank(result.getSuggest())) { + result.setSuggest(root.path("result").path("suggest").asText("pass")); + result.setLabel(root.path("result").path("label").asText(null)); + } + result.setPass("pass".equalsIgnoreCase(result.getSuggest())); + return result; + } catch (Exception e) { + log.warn("寰俊鍐呭瀹夊叏妫鏌ュ紓甯革紝闄嶇骇涓烘湰鍦板鏍哥粨鏋", e); + return result; + } + } + + private String getAccessToken() { + String cached = stringRedisTemplate.opsForValue().get(ACCESS_TOKEN_KEY); + if (CharSequenceUtil.isNotBlank(cached)) { + return cached; + } + String appId = wechatPayConfig.getAppId(); + String appSecret = wechatPayConfig.getAppSecret(); + if (CharSequenceUtil.isBlank(appId) || CharSequenceUtil.isBlank(appSecret)) { + log.warn("寰俊灏忕▼搴 appId/appSecret 鏈厤缃紝璺宠繃 msgSecCheck"); + return null; + } + try { + ResponseEntity response = restTemplate.getForEntity(ACCESS_TOKEN_URL, String.class, appId, appSecret); + JsonNode root = objectMapper.readTree(response.getBody()); + String accessToken = root.path("access_token").asText(null); + int expiresIn = root.path("expires_in").asInt(7200); + if (CharSequenceUtil.isNotBlank(accessToken)) { + stringRedisTemplate.opsForValue().set(ACCESS_TOKEN_KEY, accessToken, Math.max(expiresIn - 300, 300), TimeUnit.SECONDS); + return accessToken; + } + log.warn("寰俊 access_token 鑾峰彇澶辫触: {}", response.getBody()); + } catch (Exception e) { + log.warn("寰俊 access_token 鑾峰彇寮傚父", e); + } + return null; + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeAuditResult.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeAuditResult.java new file mode 100644 index 00000000..69c4fcd8 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeAuditResult.java @@ -0,0 +1,17 @@ +package cc.hiver.mall.ie.vo; + +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class IeAuditResult { + private String rawContent; + private String filteredContent; + private Integer auditStatus; + private Integer riskLevel; + private Boolean blocked; + private List hitWords = new ArrayList<>(); + private List hitCategories = new ArrayList<>(); +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeChatEvent.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeChatEvent.java new file mode 100644 index 00000000..aa5eb438 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeChatEvent.java @@ -0,0 +1,15 @@ +package cc.hiver.mall.ie.vo; + +import lombok.Data; + +@Data +public class IeChatEvent { + private String eventType; + private Long roomId; + private Long messageId; + private Long userId; + private Long targetUserId; + private Integer riskLevel; + private String payload; + private Long timestamp = System.currentTimeMillis(); +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeHomeVO.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeHomeVO.java new file mode 100644 index 00000000..3a7c7d89 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeHomeVO.java @@ -0,0 +1,20 @@ +package cc.hiver.mall.ie.vo; + +import lombok.Data; + +import java.util.List; + +@Data +public class IeHomeVO { + private Long onlineCount; + private Long waitingCount; + private Integer dailyQuota; + private Integer usedQuota; + private String currentMode; + private String currentMood; + private String targetModePreference; + private String targetGenderPreference; + private Integer profileCompleted; + private IeUserProfileVO profile; + private List hotStatuses; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeMatchVO.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeMatchVO.java new file mode 100644 index 00000000..b9a291c0 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeMatchVO.java @@ -0,0 +1,21 @@ +package cc.hiver.mall.ie.vo; + +import lombok.Data; + +@Data +public class IeMatchVO { + private Long matchId; + private String matchNo; + private Long targetUserId; + private String anonymousName; + private String avatarText; + private String avatarUrl; + private String mode; + private String mood; + private String stateText; + private String quoteText; + private Long roomId; + private String roomNo; + private Integer status; + private String failReason; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeMessageAckVO.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeMessageAckVO.java new file mode 100644 index 00000000..d2c6a9f3 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeMessageAckVO.java @@ -0,0 +1,20 @@ +package cc.hiver.mall.ie.vo; + +import lombok.Data; + +@Data +public class IeMessageAckVO { + private String clientMsgId; + private Long messageId; + private Long roomId; + private Long senderId; + private Long receiverId; + private Integer messageType; + private Integer auditStatus; + private Integer riskLevel; + private Integer isBlocked; + private String content; + private Integer mediaDuration; + private Long mediaSize; + private String mediaFormat; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeUserProfileVO.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeUserProfileVO.java new file mode 100644 index 00000000..bb810025 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeUserProfileVO.java @@ -0,0 +1,24 @@ +package cc.hiver.mall.ie.vo; + +import lombok.Data; + +import java.util.List; + +@Data +public class IeUserProfileVO { + private Long userId; + private String anonymousName; + private String avatarText; + private String avatarUrl; + private String gender; + private String intro; + private List interestTags; + private String currentMode; + private String recentPreference; + private String targetModePreference; + private String targetGenderPreference; + private Integer defaultRoomMinutes; + private Integer dailyQuota; + private Integer usedQuota; + private Integer profileCompleted; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeWechatMsgSecResult.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeWechatMsgSecResult.java new file mode 100644 index 00000000..737825dd --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeWechatMsgSecResult.java @@ -0,0 +1,14 @@ +package cc.hiver.mall.ie.vo; + +import lombok.Data; + +@Data +public class IeWechatMsgSecResult { + private boolean pass; + private boolean checked; + private Integer errcode; + private String errmsg; + private String suggest; + private String label; + private Integer strategy; +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/websocket/IeWebSocketController.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/websocket/IeWebSocketController.java new file mode 100644 index 00000000..fb14f529 --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/websocket/IeWebSocketController.java @@ -0,0 +1,83 @@ +package cc.hiver.mall.ie.websocket; + +import cc.hiver.core.common.utils.SecurityUtil; +import cc.hiver.core.entity.User; +import cc.hiver.mall.ie.dto.IePresenceDTO; +import cc.hiver.mall.ie.dto.IeRoomMessageDTO; +import cc.hiver.mall.ie.service.IeChatService; +import cc.hiver.mall.ie.service.IeRedisService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.simp.SimpMessageHeaderAccessor; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Controller; + +import java.security.Principal; + +@Controller +public class IeWebSocketController { + + @Autowired + private SecurityUtil securityUtil; + + @Autowired + private IeChatService chatService; + + @Autowired + private IeRedisService redisService; + + @Autowired + private SimpMessagingTemplate messagingTemplate; + + /** + * 杩炴帴鍚庨涓姩浣滐細鐧昏 WebSocket 杩炴帴骞惰ˉ鍙戠绾挎秷鎭 + * 瀹㈡埛绔彂閫 /app/ie/connect锛岃闃 /user/queue/ie/* + */ + @MessageMapping("/ie/connect") + public void connect(SimpMessageHeaderAccessor accessor, Principal principal) { + Long userId = currentUserId(principal); + redisService.refreshOnline(userId); + redisService.cacheWebSocketConnection(userId, accessor.getSessionId()); + messagingTemplate.convertAndSendToUser(String.valueOf(userId), "/queue/ie/offline", + redisService.popOfflineMessages(userId, 50)); + } + + /** + * 蹇冭烦锛氬鎴风姣 20-30 绉掑彂閫佷竴娆★紝缁存寔鍦ㄧ嚎鐢ㄦ埛缂撳瓨鍜 WebSocket 杩炴帴缂撳瓨銆 + */ + @MessageMapping("/ie/heartbeat") + public void heartbeat(SimpMessageHeaderAccessor accessor, Principal principal) { + Long userId = currentUserId(principal); + chatService.heartbeat(userId); + redisService.cacheWebSocketConnection(userId, accessor.getSessionId()); + } + + /** + * 鑱婂ぉ娑堟伅鎶曢掞細鍚庣寮哄埗鏁忔劅璇嶈繃婊わ紝鍐嶈繘琛 ACK 鍜屽绔姇閫掋 + */ + @MessageMapping("/ie/message") + public void message(IeRoomMessageDTO dto, Principal principal) { + Long userId = currentUserId(principal); + chatService.sendMessage(userId, dto); + } + + /** + * 杞讳簰鍔/杈撳叆鐘舵/鍒犻櫎鎻愮ず绛変簨浠躲 + */ + @MessageMapping("/ie/presence") + public void presence(IePresenceDTO dto, Principal principal) { + chatService.sendPresence(currentUserId(principal), dto); + } + + private Long currentUserId(Principal principal) { + if (principal != null && principal.getName() != null) { + try { + return Long.valueOf(principal.getName()); + } catch (NumberFormatException ignored) { + // Fall back to SecurityUtil, because existing project principal may be username. + } + } + User user = securityUtil.getCurrUser(); + return Long.valueOf(user.getId()); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/websocket/IeWebSocketEventListener.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/websocket/IeWebSocketEventListener.java new file mode 100644 index 00000000..1a3aed5e --- /dev/null +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/websocket/IeWebSocketEventListener.java @@ -0,0 +1,37 @@ +package cc.hiver.mall.ie.websocket; + +import cc.hiver.mall.ie.constant.IeChatEventType; +import cc.hiver.mall.ie.service.IeRedisService; +import cc.hiver.mall.ie.vo.IeChatEvent; +import cn.hutool.json.JSONUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.messaging.SessionDisconnectEvent; + +@Slf4j +@Component +public class IeWebSocketEventListener { + + @Autowired + private IeRedisService redisService; + + @EventListener + public void handleDisconnect(SessionDisconnectEvent event) { + StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage()); + String sessionId = accessor.getSessionId(); + Long userId = redisService.userIdBySession(sessionId); + if (userId == null) { + return; + } + redisService.removeWebSocketConnection(userId); + IeChatEvent chatEvent = new IeChatEvent(); + chatEvent.setEventType(IeChatEventType.USER_DISCONNECTED); + chatEvent.setUserId(userId); + chatEvent.setPayload(sessionId); + redisService.publishChatEvent(JSONUtil.toJsonStr(chatEvent)); + log.info("i/e websocket disconnected userId={}, sessionId={}", userId, sessionId); + } +} diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/dto/CreateOrderDTO.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/dto/CreateOrderDTO.java index 6940972f..2ea4da00 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/dto/CreateOrderDTO.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/dto/CreateOrderDTO.java @@ -30,6 +30,9 @@ public class CreateOrderDTO { @ApiModelProperty(value = "閰嶉佹柟寮 1:澶栧崠閰嶉 2:鍒板簵鑷彇", required = true) private Integer deliveryType; + @ApiModelProperty(value = "璁㈠崟绫诲瀷 null:澶栧崠 1:蹇掕窇鑵 2:鍥㈣喘 3锛氫簩鎵") + private Integer otherOrder; + @ApiModelProperty(value = "鏀惰揣鍦板潃ID锛堝鍗栭厤閫佹椂蹇呭~锛") private String addressId; diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/ShopAreaService.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/ShopAreaService.java index 23b5bab2..b7794143 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/ShopAreaService.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/ShopAreaService.java @@ -20,6 +20,16 @@ public interface ShopAreaService extends HiverBaseService { */ List findByParentIdOrderBySortOrder(String parentId, Boolean openDataFilter); + /** + * 閫氳繃鐖秈d鍜屽湀灞傚悕妯$硦鎼滅储 鍗囧簭 + * + * @param parentId + * @param title + * @param openDataFilter 鏄惁寮鍚暟鎹潈闄 + * @return + */ + List findByParentIdAndTitleLikeOrderBySortOrder(String parentId, String title, Boolean openDataFilter); + /** * 閫氳繃鐖秈d鍜岀姸鎬佽幏鍙 * diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/ShopAreaServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/ShopAreaServiceImpl.java index e7594376..16daf606 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/ShopAreaServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/ShopAreaServiceImpl.java @@ -47,6 +47,16 @@ public class ShopAreaServiceImpl implements ShopAreaService { return shopAreaDao.findByParentIdOrderBySortOrder(parentId); } + @Override + public List findByParentIdAndTitleLikeOrderBySortOrder(String parentId, String title, Boolean openDataFilter) { + // 鏁版嵁鏉冮檺 + List depIds = securityUtil.getDeparmentIds(); + if (depIds != null && depIds.size() > 0 && openDataFilter) { + return shopAreaDao.findByParentIdAndTitleLikeAndIdInOrderBySortOrder(parentId, title, depIds); + } + return shopAreaDao.findByParentIdAndTitleLikeOrderBySortOrder(parentId, title); + } + @Override public List findByParentIdAndStatusOrderBySortOrder(String parentId, Integer status) { return shopAreaDao.findByParentIdAndStatusOrderBySortOrder(parentId, status); diff --git a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderServiceImpl.java b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderServiceImpl.java index 57162358..0f5873ed 100644 --- a/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderServiceImpl.java +++ b/hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderServiceImpl.java @@ -1481,6 +1481,7 @@ public class MallOrderServiceImpl extends ServiceImpl AND o.other_order = 2 + + AND o.other_order = 3 + AND o.status = 0