Browse Source

对接拼团数据1

master
wangfukang 4 weeks ago
parent
commit
b6897738ce
  1. 1
      hiver-admin/src/main/resources/application.yml
  2. 16
      hiver-admin/test-output/test-report.html
  3. 5
      hiver-modules/hiver-mall/pom.xml
  4. 20
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/controller/ShopAreaController.java
  5. 21
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/dao/ShopAreaDao.java
  6. 2
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/entity/MallOrder.java
  7. 34
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/config/IeRedisPubSubConfig.java
  8. 25
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/config/IeWebSocketConfig.java
  9. 14
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/constant/IeChatEventType.java
  10. 23
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/constant/IeConstants.java
  11. 64
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/constant/IeRedisKey.java
  12. 27
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/controller/IeAdminController.java
  13. 149
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/controller/IeController.java
  14. 16
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeMatchStartDTO.java
  15. 10
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IePresenceDTO.java
  16. 18
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeProfileDTO.java
  17. 12
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeReportDTO.java
  18. 15
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeRoomMessageDTO.java
  19. 15
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/dto/IeStatusDTO.java
  20. 26
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeBaseEntity.java
  21. 14
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeBlock.java
  22. 20
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeContentAuditLog.java
  23. 22
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeMatchAttempt.java
  24. 15
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IePresenceEvent.java
  25. 29
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeRecord.java
  26. 19
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeReport.java
  27. 23
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeRoom.java
  28. 34
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeRoomMessage.java
  29. 16
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeSensitiveWord.java
  30. 30
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeUserProfile.java
  31. 19
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/entity/IeUserStatus.java
  32. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeBlockMapper.java
  33. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeContentAuditLogMapper.java
  34. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeMatchAttemptMapper.java
  35. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IePresenceEventMapper.java
  36. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeRecordMapper.java
  37. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeReportMapper.java
  38. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeRoomMapper.java
  39. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeRoomMessageMapper.java
  40. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeSensitiveWordMapper.java
  41. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeUserProfileMapper.java
  42. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mapper/IeUserStatusMapper.java
  43. 25
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mq/IeChatDelayConsumer.java
  44. 16
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mq/IeChatEventConsumer.java
  45. 25
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mq/IeChatEventProducer.java
  46. 69
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/mq/IeChatMqConfig.java
  47. 39
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeChatService.java
  48. 22
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeMatchService.java
  49. 55
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeRedisService.java
  50. 9
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeSecurityAuditService.java
  51. 7
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/IeWechatSecurityService.java
  52. 454
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeChatServiceImpl.java
  53. 589
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeMatchServiceImpl.java
  54. 275
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeRedisServiceImpl.java
  55. 184
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeSecurityAuditServiceImpl.java
  56. 112
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/service/impl/IeWechatSecurityServiceImpl.java
  57. 17
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeAuditResult.java
  58. 15
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeChatEvent.java
  59. 20
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeHomeVO.java
  60. 21
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeMatchVO.java
  61. 20
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeMessageAckVO.java
  62. 24
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeUserProfileVO.java
  63. 14
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/vo/IeWechatMsgSecResult.java
  64. 83
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/websocket/IeWebSocketController.java
  65. 37
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/ie/websocket/IeWebSocketEventListener.java
  66. 3
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/pojo/dto/CreateOrderDTO.java
  67. 10
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/ShopAreaService.java
  68. 10
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/ShopAreaServiceImpl.java
  69. 1
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderServiceImpl.java
  70. 8
      hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/utils/WechatPayConfig.java
  71. 4
      hiver-modules/hiver-mall/src/main/resources/db/ie_room_message_media_fields.sql
  72. 124
      hiver-modules/hiver-mall/src/main/resources/db/ie_sensitive_words_seed.sql
  73. 6
      hiver-modules/hiver-mall/src/main/resources/db/ie_user_profile_gender_fields.sql
  74. 6
      hiver-modules/hiver-mall/src/main/resources/db/ie_user_profile_onboarding_fields.sql
  75. 3
      hiver-modules/hiver-mall/src/main/resources/mapper/MallOrderMapper.xml

1
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

16
hiver-admin/test-output/test-report.html

@ -35,7 +35,7 @@
<a href="#"><span class="badge badge-primary">Hiver</span></a>
</li>
<li class="m-r-10">
<a href="#"><span class="badge badge-primary">五月 16, 2026 17:37:44</span></a>
<a href="#"><span class="badge badge-primary">五月 19, 2026 15:04:26</span></a>
</li>
</ul>
</div>
@ -84,7 +84,7 @@
<div class="test-detail">
<span class="meta text-white badge badge-sm"></span>
<p class="name">passTest</p>
<p class="text-sm"><span>17:37:45 下午</span> / <span>0.018 secs</span></p>
<p class="text-sm"><span>15:04:27 下午</span> / <span>0.017 secs</span></p>
</div>
<div class="test-contents d-none">
<div class="detail-head">
@ -92,9 +92,9 @@
<div class="info">
<div class='float-right'><span class='badge badge-default'>#test-id=1</span></div>
<h5 class="test-status text-pass">passTest</h5>
<span class='badge badge-success'>05.16.2026 17:37:45</span>
<span class='badge badge-danger'>05.16.2026 17:37:45</span>
<span class='badge badge-default'>0.018 secs</span>
<span class='badge badge-success'>05.19.2026 15:04:27</span>
<span class='badge badge-danger'>05.19.2026 15:04:27</span>
<span class='badge badge-default'>0.017 secs</span>
</div>
<div class="m-t-10 m-l-5"></div>
</div>
@ -104,7 +104,7 @@
<tbody>
<tr class="event-row">
<td><span class="badge log pass-bg">Pass</span></td>
<td>17:37:45</td>
<td>15:04:27</td>
<td>
Test passed
</td>
@ -128,13 +128,13 @@
<div class="col-md-3">
<div class="card"><div class="card-body">
<p class="m-b-0">Started</p>
<h3>五月 16, 2026 17:37:44</h3>
<h3>五月 19, 2026 15:04:26</h3>
</div></div>
</div>
<div class="col-md-3">
<div class="card"><div class="card-body">
<p class="m-b-0">Ended</p>
<h3>五月 16, 2026 17:37:45</h3>
<h3>五月 19, 2026 15:04:27</h3>
</div></div>
</div>
<div class="col-md-3">

5
hiver-modules/hiver-mall/pom.xml

@ -39,6 +39,11 @@
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>sensitive-word</artifactId>
<version>0.26.0</version>
</dependency>
</dependencies>
</project>

20
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<List<ShopArea>> getByParentId(@PathVariable String parentId,
@ApiParam("名称模糊搜索")
@RequestParam(required = false) String title,
@ApiParam("是否开始数据权限过滤")
@RequestParam(required = false, defaultValue = "true") Boolean openDataFilter) {
List<ShopArea> list;
String titleKeyword = StrUtil.isBlank(title) ? null : title.trim();
List<Object> 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<ShopArea> 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<List<ShopArea>>().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());
}

21
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<ShopArea, String> {
*/
List<ShopArea> findByParentIdAndIdInOrderBySortOrder(String parentId, List<String> shopAreas);
/**
* 通过父id和圈层名模糊搜索 升序
*
* @param parentId
* @param title
* @return
*/
List<ShopArea> findByParentIdAndTitleLikeOrderBySortOrder(String parentId, String title);
/**
* 通过父id和圈层名模糊搜索 升序 数据权限
*
* @param parentId
* @param title
* @param shopAreas
* @return
*/
List<ShopArea> findByParentIdAndTitleLikeAndIdInOrderBySortOrder(String parentId, String title, List<String> shopAreas);
/**
* 通过父id和状态获取 升序
*

2
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;

34
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("【i/e Redis聊天事件】{}", message);
}
});
}
}

25
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("*");
}
}

14
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() {
}
}

23
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() {
}
}

64
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:";
/** 在线用户缓存:ZSET, 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:";
/** 用户所在匹配池索引:STRING, 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:";
/** 每日次数限制:STRING, key=ie:quota:{yyyyMMdd}:{userId} */
public static final String DAILY_QUOTA = PREFIX + "quota:";
/** 聊天会话缓存:HASH, key=ie:room:{roomId} */
public static final String ROOM = PREFIX + "room:";
/** WebSocket连接缓存:HASH, 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:";
/** 离线消息缓存:LIST, 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";
/** 防刷计数:STRING, key=ie:rate:msg:{roomId}:{userId} */
public static final String MESSAGE_RATE = PREFIX + "rate:msg:";
/** 用户风控计数:STRING, 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;
}
}

27
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<Object> finishExpiredRooms(Integer limit) {
chatService.finishExpiredRooms(limit == null ? 100 : limit);
return ResultUtil.success("处理完成");
}
}

149
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<IeHomeVO> home() {
return new ResultUtil<IeHomeVO>().setData(matchService.home(currentUserId()));
}
@RequestMapping(value = "/profile", method = RequestMethod.GET)
@ApiOperation("获取i/e资料")
public Result<IeUserProfileVO> profile() {
return new ResultUtil<IeUserProfileVO>().setData(matchService.profile(currentUserId()));
}
@RequestMapping(value = "/profiles/{targetUserId}", method = RequestMethod.GET)
@ApiOperation("获取对方半匿名i/e资料")
public Result<IeUserProfileVO> profileByUserId(@PathVariable Long targetUserId) {
return new ResultUtil<IeUserProfileVO>().setData(matchService.profileByUserId(currentUserId(), targetUserId));
}
@RequestMapping(value = "/profile", method = RequestMethod.POST)
@ApiOperation("保存i/e资料")
public Result<IeUserProfileVO> saveProfile(@RequestBody IeProfileDTO dto) {
return new ResultUtil<IeUserProfileVO>().setData(matchService.saveProfile(currentUserId(), dto));
}
@RequestMapping(value = "/status", method = RequestMethod.POST)
@ApiOperation("更新当前i/e和情绪状态")
public Result<Object> updateStatus(@RequestBody IeStatusDTO dto) {
matchService.updateStatus(currentUserId(), dto);
return ResultUtil.success("更新成功");
}
@RequestMapping(value = "/match/start", method = RequestMethod.POST)
@ApiOperation("发起随机匹配")
public Result<IeMatchVO> startMatch(@RequestBody IeMatchStartDTO dto) {
return new ResultUtil<IeMatchVO>().setData(matchService.startMatch(currentUserId(), dto));
}
@RequestMapping(value = "/rooms/{roomId}/presence", method = RequestMethod.POST)
@ApiOperation("发送轻互动/输入状态/心跳事件")
public Result<Object> 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<IeMessageAckVO> sendMessage(@PathVariable Long roomId, @RequestBody IeRoomMessageDTO dto) {
dto.setRoomId(roomId);
return new ResultUtil<IeMessageAckVO>().setData(chatService.sendMessage(currentUserId(), dto));
}
@RequestMapping(value = "/rooms/{roomId}/finish", method = RequestMethod.POST)
@ApiOperation("提前结束陪伴房间")
public Result<Object> finishRoom(@PathVariable Long roomId) {
chatService.finishRoom(currentUserId(), roomId, 2);
return ResultUtil.success("已结束");
}
@RequestMapping(value = "/rooms/{roomId}/report", method = RequestMethod.POST)
@ApiOperation("举报不适内容")
public Result<Object> 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<Object> block(@PathVariable Long blockedUserId, String reason) {
chatService.block(currentUserId(), blockedUserId, reason);
return ResultUtil.success("已拉黑");
}
@RequestMapping(value = "/offline", method = RequestMethod.GET)
@ApiOperation("拉取离线消息")
public Result<List<String>> offline() {
return new ResultUtil<List<String>>().setData(redisService.popOfflineMessages(currentUserId(), 50));
}
@RequestMapping(value = "/rooms/{roomId}/messages/page", method = RequestMethod.GET)
@ApiOperation("分页查询房间消息")
public Result<IPage<IeRoomMessage>> pageMessages(@PathVariable Long roomId,
@RequestParam(required = false, defaultValue = "1") Integer pageNumber,
@RequestParam(required = false, defaultValue = "20") Integer pageSize) {
return new ResultUtil<IPage<IeRoomMessage>>().setData(chatService.pageMessages(currentUserId(), roomId, pageNumber, pageSize));
}
@RequestMapping(value = "/records/page", method = RequestMethod.GET)
@ApiOperation("分页查询感受记录")
public Result<IPage<IeRecord>> pageRecords(@RequestParam(required = false, defaultValue = "1") Integer pageNumber,
@RequestParam(required = false, defaultValue = "10") Integer pageSize) {
return new ResultUtil<IPage<IeRecord>>().setData(chatService.pageRecords(currentUserId(), pageNumber, pageSize));
}
@RequestMapping(value = "/reports/page", method = RequestMethod.GET)
@ApiOperation("分页查询我的举报记录")
public Result<IPage<IeReport>> pageReports(@RequestParam(required = false, defaultValue = "1") Integer pageNumber,
@RequestParam(required = false, defaultValue = "10") Integer pageSize) {
return new ResultUtil<IPage<IeReport>>().setData(chatService.pageReports(currentUserId(), pageNumber, pageSize));
}
private Long currentUserId() {
User user = securityUtil.getCurrUser();
return Long.valueOf(user.getId());
}
}

16
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<String> interestTags;
}

10
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;
}

18
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<String> interestTags;
private String currentMode;
private String targetModePreference;
private String targetGenderPreference;
}

12
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;
}

15
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;
}

15
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<String> interestTags;
}

26
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;
}

14
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;
}

20
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;
}

22
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;
}

15
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;
}

29
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;
}

19
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;
}

23
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;
}

34
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;
}

16
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;
}

30
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;
}

19
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;
}

9
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<IeBlock> {
}

9
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<IeContentAuditLog> {
}

9
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<IeMatchAttempt> {
}

9
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<IePresenceEvent> {
}

9
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<IeRecord> {
}

9
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<IeReport> {
}

9
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<IeRoom> {
}

9
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<IeRoomMessage> {
}

9
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<IeSensitiveWord> {
}

9
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<IeUserProfile> {
}

9
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<IeUserStatus> {
}

25
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("【i/e限时房间】自动结束失败 roomId={}", roomId, e);
}
}
}

16
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("【i/e聊天异步事件】{}", message);
// 预留:后续可接入风控画像、运营统计、人工审核队列、消息清理任务。
}
}

25
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;
});
}
}

69
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<String, Object> 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);
}
}

39
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<IeRoomMessage> pageMessages(Long userId, Long roomId, Integer pageNumber, Integer pageSize);
void markRead(Long userId, Long roomId);
IPage<IeRecord> pageRecords(Long userId, Integer pageNumber, Integer pageSize);
IPage<IeReport> pageReports(Long userId, Integer pageNumber, Integer pageSize);
}

22
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);
}

55
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<Long> 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<String> 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<String> hotStatuses(int limit);
}

9
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);
}

7
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);
}

454
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<IeRoomMessage>()
.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<IeRoomMessage>()
.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<IeRoom> rooms = roomMapper.selectList(new LambdaQueryWrapper<IeRoom>()
.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<IeBlock>()
.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<IeRoomMessage> pageMessages(Long userId, Long roomId, Integer pageNumber, Integer pageSize) {
assertRoomParticipant(userId, roomId);
markRead(userId, roomId);
Page<IeRoomMessage> page = new Page<>(safePageNumber(pageNumber), safePageSize(pageSize));
IPage<IeRoomMessage> result = roomMessageMapper.selectPage(page, new LambdaQueryWrapper<IeRoomMessage>()
.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<IeRecord>()
.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<IeRecord> pageRecords(Long userId, Integer pageNumber, Integer pageSize) {
Page<IeRecord> page = new Page<>(safePageNumber(pageNumber), safePageSize(pageSize));
IPage<IeRecord> result = recordMapper.selectPage(page, new LambdaQueryWrapper<IeRecord>()
.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<IeRoomMessage>()
.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<IeRecord>()
.eq(IeRecord::getUserId, userId)
.eq(IeRecord::getRoomId, roomId)
.last("limit 1"));
return record == null ? null : record.getLastReadTime();
}
@Override
public IPage<IeReport> pageReports(Long userId, Integer pageNumber, Integer pageSize) {
Page<IeReport> page = new Page<>(safePageNumber(pageNumber), safePageSize(pageSize));
return reportMapper.selectPage(page, new LambdaQueryWrapper<IeReport>()
.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<IeRecord>()
.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);
}
}

589
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<IeUserStatus>()
.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, "正在匹配中,请稍后");
}
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<Long> candidates = candidateUsers(targetMode, mode, mood);
List<MatchCandidate> 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<IeRecord>()
.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<IeUserProfile>()
.eq(IeUserProfile::getUserId, userId)
.last("limit 1"));
}
private List<Long> candidateUsers(String targetMode, String selfMode, String mood) {
LinkedHashSet<Long> 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<Long> statusCandidates(String mode, int limit) {
List<IeUserStatus> statuses = userStatusMapper.selectList(new LambdaQueryWrapper<IeUserStatus>()
.eq(IeUserStatus::getOnlineStatus, 1)
.eq(IeUserStatus::getMode, normalizeMode(mode, "i"))
.orderByDesc(IeUserStatus::getLastActiveTime)
.last("limit " + Math.max(1, Math.min(limit, 100))));
List<Long> users = new ArrayList<>();
for (IeUserStatus status : statuses) {
if (status.getUserId() != null) {
users.add(status.getUserId());
}
}
return users;
}
private List<Long> profileCandidates(String mode, int limit) {
List<IeUserProfile> profiles = userProfileMapper.selectList(new LambdaQueryWrapper<IeUserProfile>()
.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<Long> 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<String> selfTags = new ArrayList<>();
if (selfStatus != null && selfStatus.getInterestTags() != null) {
selfTags.addAll(selfStatus.getInterestTags());
}
if (selfProfile != null) {
selfTags.addAll(jsonToTags(selfProfile.getInterestTags()));
}
List<String> 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<String> tags) {
if (tags == null) {
return "[]";
}
return JSONUtil.toJsonStr(tags);
}
private List<String> 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<String> 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;
}
}
}

275
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<Long> candidates(String mode, String mood, int limit) {
List<String> 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<Long> result = new ArrayList<>();
for (String key : keys) {
Set<String> 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<String> 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<String> popOfflineMessages(Long userId, int limit) {
String key = IeRedisKey.OFFLINE_MESSAGE + userId;
List<String> 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<String> hotStatuses(int limit) {
Set<String> 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();
}
}

184
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<String, IeSensitiveWord> 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<String> hitWords = wordBs.findAll(content);
String filtered = CollectionUtil.isEmpty(hitWords) ? content : wordBs.replace(content);
int maxLevel = 0;
Map<String, IeSensitiveWord> 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<IeSensitiveWord> 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<IeSensitiveWord> words) {
List<String> 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<String> 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<IeSensitiveWord> loadSensitiveWords() {
String cache = stringRedisTemplate.opsForValue().get(IeRedisKey.SENSITIVE_WORDS);
if (CharSequenceUtil.isNotBlank(cache)) {
return JSONUtil.toList(cache, IeSensitiveWord.class);
}
List<IeSensitiveWord> words = sensitiveWordMapper.selectList(new LambdaQueryWrapper<IeSensitiveWord>()
.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;
}
}

112
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<String, Object> body = new HashMap<>();
body.put("content", content);
body.put("version", 2);
body.put("scene", 4);
body.put("openid", openid);
ResponseEntity<String> 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<String> 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;
}
}

17
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<String> hitWords = new ArrayList<>();
private List<String> hitCategories = new ArrayList<>();
}

15
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();
}

20
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<String> hotStatuses;
}

21
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;
}

20
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;
}

24
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<String> 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;
}

14
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;
}

83
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());
}
}

37
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);
}
}

3
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;

10
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/service/ShopAreaService.java

@ -20,6 +20,16 @@ public interface ShopAreaService extends HiverBaseService<ShopArea, String> {
*/
List<ShopArea> findByParentIdOrderBySortOrder(String parentId, Boolean openDataFilter);
/**
* 通过父id和圈层名模糊搜索 升序
*
* @param parentId
* @param title
* @param openDataFilter 是否开启数据权限
* @return
*/
List<ShopArea> findByParentIdAndTitleLikeOrderBySortOrder(String parentId, String title, Boolean openDataFilter);
/**
* 通过父id和状态获取
*

10
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<ShopArea> findByParentIdAndTitleLikeOrderBySortOrder(String parentId, String title, Boolean openDataFilter) {
// 数据权限
List<String> depIds = securityUtil.getDeparmentIds();
if (depIds != null && depIds.size() > 0 && openDataFilter) {
return shopAreaDao.findByParentIdAndTitleLikeAndIdInOrderBySortOrder(parentId, title, depIds);
}
return shopAreaDao.findByParentIdAndTitleLikeOrderBySortOrder(parentId, title);
}
@Override
public List<ShopArea> findByParentIdAndStatusOrderBySortOrder(String parentId, Integer status) {
return shopAreaDao.findByParentIdAndStatusOrderBySortOrder(parentId, status);

1
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/serviceimpl/mybatis/MallOrderServiceImpl.java

@ -1481,6 +1481,7 @@ public class MallOrderServiceImpl extends ServiceImpl<MallOrderMapper, MallOrder
order.setShopId(dto.getShopId());
order.setOrderType(orderType);
order.setIsPack(dto.getIsPack() == null ? 1 : dto.getIsPack());
order.setOtherOrder(dto.getOtherOrder());
order.setReceiverPhone(user.getMobile());
order.setDeliveryType(dto.getDeliveryType());
order.setAddressId(dto.getAddressId());

8
hiver-modules/hiver-mall/src/main/java/cc/hiver/mall/utils/WechatPayConfig.java

@ -9,6 +9,10 @@ public class WechatPayConfig {
@Value("${wechatpay.appid}")
private String appId;
// 小程序APPSECRET,用于获取普通 access_token(内容安全等)
@Value("${wechatpay.appSecret:}")
private String appSecret;
// 微信支付商户号
@Value("${wechatpay.mchId}")
private String mchId;
@ -71,6 +75,10 @@ public class WechatPayConfig {
return appId;
}
public String getAppSecret() {
return appSecret;
}
// 获取商户号
public String getMchId() {
return mchId;

4
hiver-modules/hiver-mall/src/main/resources/db/ie_room_message_media_fields.sql

@ -0,0 +1,4 @@
ALTER TABLE ie_room_message
ADD COLUMN media_duration INT NULL COMMENT '媒体时长,秒',
ADD COLUMN media_size BIGINT NULL COMMENT '媒体大小,字节',
ADD COLUMN media_format VARCHAR(20) NULL COMMENT '媒体格式';

124
hiver-modules/hiver-mall/src/main/resources/db/ie_sensitive_words_seed.sql

@ -0,0 +1,124 @@
-- i/e 随机陪伴敏感词初始化种子。
-- 说明:
-- 1. 这是一份上线前基础词库,不替代第三方内容安全服务和人工复核。
-- 2. level=1 替换放行;level=2 拦截;level=3 高危拦截并建议人工复核。
-- 3. houbb/sensitive-word 内置词库已在代码中启用;本表维护业务自定义词、社交风控词和补充词。
-- 4. 修改本表后建议删除 Redis key: ie:sensitive:words,让服务尽快重建 DFA。
INSERT INTO ie_sensitive_word (word, category, level, replacement, enabled)
VALUES
-- 色情低俗:高危明确词直接拦截,擦边词替换或拦截
('约炮', 'porn', 3, '**', 1),
('做爱', 'porn', 3, '**', 1),
('鸡巴', 'porn', 3, '**', 1),
('操逼', 'porn', 3, '**', 1),
('裸聊', 'porn', 3, '**', 1),
('成人视频', 'porn', 3, '**', 1),
('黄色网站', 'porn', 3, '**', 1),
('色情网', 'porn', 3, '**', 1),
('援交', 'porn', 3, '**', 1),
('卖淫', 'porn', 3, '**', 1),
('嫖娼', 'porn', 3, '**', 1),
('包夜', 'porn', 3, '**', 1),
('上床', 'porn', 2, '**', 1),
('开房', 'porn', 2, '**', 1),
('口交', 'porn', 3, '**', 1),
('性交', 'porn', 3, '**', 1),
('自慰', 'porn', 2, '**', 1),
('露点', 'porn', 2, '**', 1),
('裸照', 'porn', 3, '**', 1),
('私密照', 'porn', 2, '**', 1),
('看片', 'porn', 2, '**', 1),
('AV资源', 'porn', 3, '**', 1),
('成人资源', 'porn', 3, '**', 1),
-- 侮辱辱骂:轻度替换,强侮辱拦截
('傻逼', 'abuse', 2, '**', 1),
('煞笔', 'abuse', 2, '**', 1),
('蠢货', 'abuse', 1, '**', 1),
('废物', 'abuse', 1, '**', 1),
('垃圾人', 'abuse', 1, '**', 1),
('你妈死了', 'abuse', 3, '**', 1),
('死全家', 'abuse', 3, '**', 1),
('滚蛋', 'abuse', 1, '**', 1),
('脑残', 'abuse', 1, '**', 1),
('贱人', 'abuse', 2, '**', 1),
('婊子', 'abuse', 2, '**', 1),
('狗东西', 'abuse', 2, '**', 1),
('畜生', 'abuse', 2, '**', 1),
('去死吧', 'abuse', 3, '**', 1),
-- 暴力威胁/自伤风险:高危拦截并留审核日志
('杀了你', 'violence', 3, '**', 1),
('弄死你', 'violence', 3, '**', 1),
('打死你', 'violence', 3, '**', 1),
('砍人', 'violence', 3, '**', 1),
('带刀', 'violence', 2, '**', 1),
('炸学校', 'violence', 3, '**', 1),
('自杀', 'self_harm', 3, '**', 1),
('想死', 'self_harm', 3, '**', 1),
('割腕', 'self_harm', 3, '**', 1),
('跳楼', 'self_harm', 3, '**', 1),
('吃药自杀', 'self_harm', 3, '**', 1),
-- 政治违法风险:只放明确高风险组织/行动/违法表达,减少正常讨论误伤
('反动组织', 'politics', 3, '**', 1),
('共产党', 'politics', 3, '**', 1),
('颠覆国家', 'politics', 3, '**', 1),
('分裂国家', 'politics', 3, '**', 1),
('煽动游行', 'politics', 3, '**', 1),
('非法集会', 'politics', 3, '**', 1),
('暴恐', 'politics', 3, '**', 1),
('恐怖组织', 'politics', 3, '**', 1),
('极端组织', 'politics', 3, '**', 1),
-- 广告/导流/联系方式:社交产品重点防外流
('加微信', 'contact', 2, '**', 1),
('加我微信', 'contact', 2, '**', 1),
('微信号', 'contact', 2, '**', 1),
('vx', 'contact', 2, '**', 1),
('v我', 'contact', 2, '**', 1),
('QQ号', 'contact', 2, '**', 1),
('加QQ', 'contact', 2, '**', 1),
('手机号', 'contact', 2, '**', 1),
('私加', 'contact', 2, '**', 1),
('私聊加', 'contact', 2, '**', 1),
('进群', 'ad', 2, '**', 1),
('刷单', 'ad', 3, '**', 1),
('兼职日结', 'ad', 2, '**', 1),
('网赚', 'ad', 2, '**', 1),
('返利群', 'ad', 2, '**', 1),
('贷款', 'ad', 2, '**', 1),
('套现', 'ad', 3, '**', 1),
('博彩', 'ad', 3, '**', 1),
('赌博', 'ad', 3, '**', 1),
('下注', 'ad', 3, '**', 1),
('开盘', 'ad', 2, '**', 1),
-- 诈骗违法
('卖号', 'fraud', 2, '**', 1),
('买号', 'fraud', 2, '**', 1),
('盗号', 'fraud', 3, '**', 1),
('洗钱', 'fraud', 3, '**', 1),
('跑分', 'fraud', 3, '**', 1),
('代实名', 'fraud', 3, '**', 1),
('出售银行卡', 'fraud', 3, '**', 1),
('收银行卡', 'fraud', 3, '**', 1),
('身份证代办', 'fraud', 3, '**', 1),
-- 校园安全/隐私保护
('发定位', 'privacy', 2, '**', 1),
('发照片', 'privacy', 1, '**', 1),
('要照片', 'privacy', 1, '**', 1),
('真实姓名', 'privacy', 1, '**', 1),
('宿舍号', 'privacy', 2, '**', 1),
('寝室号', 'privacy', 2, '**', 1),
('线下见面', 'privacy', 2, '**', 1),
('来我宿舍', 'privacy', 3, '**', 1),
('去你宿舍', 'privacy', 3, '**', 1)
ON DUPLICATE KEY UPDATE
category = VALUES(category),
level = VALUES(level),
replacement = VALUES(replacement),
enabled = VALUES(enabled),
update_time = CURRENT_TIMESTAMP;

6
hiver-modules/hiver-mall/src/main/resources/db/ie_user_profile_gender_fields.sql

@ -0,0 +1,6 @@
ALTER TABLE ie_user_profile
ADD COLUMN gender VARCHAR(20) NULL COMMENT '性别:male/female/unknown',
ADD COLUMN target_gender_preference VARCHAR(20) NULL COMMENT '默认想匹配性别:male/female/any';
ALTER TABLE ie_record
ADD COLUMN last_read_time DATETIME NULL COMMENT '用户最后进入该房间阅读消息时间';

6
hiver-modules/hiver-mall/src/main/resources/db/ie_user_profile_onboarding_fields.sql

@ -0,0 +1,6 @@
ALTER TABLE ie_user_profile
ADD COLUMN avatar_url VARCHAR(500) NULL COMMENT '头像图片地址',
ADD COLUMN intro VARCHAR(500) NULL COMMENT '个人介绍',
ADD COLUMN interest_tags VARCHAR(1000) NULL COMMENT '兴趣/情绪标签,JSON数组',
ADD COLUMN target_mode_preference VARCHAR(20) NULL COMMENT '默认想匹配对象类型:i/e/any',
ADD COLUMN profile_completed TINYINT NULL DEFAULT 0 COMMENT '是否完成i/e首次资料';

3
hiver-modules/hiver-mall/src/main/resources/mapper/MallOrderMapper.xml

@ -145,6 +145,9 @@
<if test="q.searchType != null and q.searchType == 3">
AND o.other_order = 2
</if>
<if test="q.searchType != null and q.searchType == 4">
AND o.other_order = 3
</if>
<if test="q.searchStatus != null and q.searchStatus == 0">
AND o.status = 0
</if>

Loading…
Cancel
Save