|
@@ -14,6 +14,9 @@ import org.apache.commons.lang3.StringUtils;
|
|
|
import org.apache.commons.lang3.tuple.Triple;
|
|
import org.apache.commons.lang3.tuple.Triple;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
+import org.springframework.dao.DataIntegrityViolationException;
|
|
|
|
|
+import org.springframework.dao.DuplicateKeyException;
|
|
|
|
|
+import org.springframework.jdbc.core.JdbcTemplate;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Propagation;
|
|
import org.springframework.transaction.annotation.Propagation;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
@@ -22,6 +25,7 @@ import shop.alien.config.properties.RiskControlProperties;
|
|
|
import shop.alien.entity.second.LifeUserLog;
|
|
import shop.alien.entity.second.LifeUserLog;
|
|
|
import shop.alien.entity.second.SecondRiskControlRecord;
|
|
import shop.alien.entity.second.SecondRiskControlRecord;
|
|
|
import shop.alien.entity.store.*;
|
|
import shop.alien.entity.store.*;
|
|
|
|
|
+import shop.alien.entity.store.dto.LifeFansFollowOutcome;
|
|
|
import shop.alien.entity.store.vo.LifeMessageVo;
|
|
import shop.alien.entity.store.vo.LifeMessageVo;
|
|
|
import shop.alien.entity.store.vo.LifeUserVo;
|
|
import shop.alien.entity.store.vo.LifeUserVo;
|
|
|
import shop.alien.entity.store.vo.WebSocketVo;
|
|
import shop.alien.entity.store.vo.WebSocketVo;
|
|
@@ -38,6 +42,7 @@ import java.text.SimpleDateFormat;
|
|
|
import java.time.LocalDateTime;
|
|
import java.time.LocalDateTime;
|
|
|
import java.time.format.DateTimeFormatter;
|
|
import java.time.format.DateTimeFormatter;
|
|
|
import java.util.*;
|
|
import java.util.*;
|
|
|
|
|
+import java.util.regex.Pattern;
|
|
|
import java.util.stream.Collectors;
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -54,6 +59,12 @@ public class LifeUserService extends ServiceImpl<LifeUserMapper, LifeUser> {
|
|
|
private static final String FOLLOW_APP_PUSH_OPEN_PATH =
|
|
private static final String FOLLOW_APP_PUSH_OPEN_PATH =
|
|
|
"pages/secondHandTransactions/pages/message/noticesAndMessage";
|
|
"pages/secondHandTransactions/pages/message/noticesAndMessage";
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 关注双方标识:`store_xxx` / `user_xxx`(整体无空格,后缀最长 120 字符)
|
|
|
|
|
+ */
|
|
|
|
|
+ private static final Pattern LIFE_FANS_PAIR_ID_PATTERN = Pattern.compile("^(store|user)_[^\\s]{1,120}$");
|
|
|
|
|
+ private static final int FAN_IDS_MAX_CHARS = 160;
|
|
|
|
|
+
|
|
|
private final LifeUserMapper lifeUserMapper;
|
|
private final LifeUserMapper lifeUserMapper;
|
|
|
|
|
|
|
|
private final LifeFansMapper lifeFansMapper;
|
|
private final LifeFansMapper lifeFansMapper;
|
|
@@ -84,6 +95,8 @@ public class LifeUserService extends ServiceImpl<LifeUserMapper, LifeUser> {
|
|
|
|
|
|
|
|
private final StoreUserMapper storeUserMapper;
|
|
private final StoreUserMapper storeUserMapper;
|
|
|
|
|
|
|
|
|
|
+ private final JdbcTemplate jdbcTemplate;
|
|
|
|
|
+
|
|
|
@Autowired
|
|
@Autowired
|
|
|
private RiskControlProperties riskControlProperties;
|
|
private RiskControlProperties riskControlProperties;
|
|
|
|
|
|
|
@@ -118,56 +131,164 @@ public class LifeUserService extends ServiceImpl<LifeUserMapper, LifeUser> {
|
|
|
return lifeUserMapper.selectById(id);
|
|
return lifeUserMapper.selectById(id);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public int addFans(LifeFans fans) {
|
|
|
|
|
- fans.setCreatedTime(new Date());
|
|
|
|
|
- LambdaQueryWrapper<LifeFans> lambdaQueryWrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
- lambdaQueryWrapper.eq(LifeFans::getFollowedId, fans.getFollowedId())
|
|
|
|
|
- .eq(LifeFans::getFansId, fans.getFansId())
|
|
|
|
|
- .eq(LifeFans::getDeleteFlag, 0);
|
|
|
|
|
- int number = lifeFansMapper.selectCount(lambdaQueryWrapper);
|
|
|
|
|
- if (number >= 1) {
|
|
|
|
|
- return 0;
|
|
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public LifeFansFollowOutcome addFans(LifeFans fans) {
|
|
|
|
|
+ Optional<String> paramError = validateAndNormalizeLifeFansPair(fans);
|
|
|
|
|
+ if (paramError.isPresent()) {
|
|
|
|
|
+ return LifeFansFollowOutcome.failure(paramError.get());
|
|
|
}
|
|
}
|
|
|
- int num = lifeFansMapper.insert(fans);
|
|
|
|
|
-
|
|
|
|
|
- if (num == 1) {
|
|
|
|
|
- LifeNotice notice = new LifeNotice();
|
|
|
|
|
- notice.setTitle("关注通知");
|
|
|
|
|
- notice.setContext("关注了你");
|
|
|
|
|
- notice.setNoticeType(0);
|
|
|
|
|
- notice.setReceiverId(fans.getFollowedId());
|
|
|
|
|
- notice.setSenderId(fans.getFansId());
|
|
|
|
|
-
|
|
|
|
|
- // 根据手机号查询发送人信息
|
|
|
|
|
- String storePhones = "''";
|
|
|
|
|
- String userPhones = "''";
|
|
|
|
|
- if (fans.getFansId().split("_")[0].equals("store")) {
|
|
|
|
|
- storePhones = "'" + fans.getFansId().split("_")[1] + "'";
|
|
|
|
|
- } else {
|
|
|
|
|
- userPhones = "'" + fans.getFansId().split("_")[1] + "'";
|
|
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ fans.setCreatedTime(new Date());
|
|
|
|
|
+
|
|
|
|
|
+ LambdaQueryWrapper<LifeFans> activeQ = new LambdaQueryWrapper<>();
|
|
|
|
|
+ activeQ.eq(LifeFans::getFollowedId, fans.getFollowedId())
|
|
|
|
|
+ .eq(LifeFans::getFansId, fans.getFansId())
|
|
|
|
|
+ .last("LIMIT 1");
|
|
|
|
|
+ LifeFans active = lifeFansMapper.selectOne(activeQ);
|
|
|
|
|
+ if (active != null) {
|
|
|
|
|
+ return LifeFansFollowOutcome.success("您已关注,无需重复操作");
|
|
|
}
|
|
}
|
|
|
- List<LifeMessageVo> userList = messageMapper.getLifeUserAndStoreUserByPhone(storePhones, userPhones);
|
|
|
|
|
- if (!CollectionUtils.isEmpty(userList)) {
|
|
|
|
|
- notice.setBusinessId(userList.get(0).getId());
|
|
|
|
|
|
|
+
|
|
|
|
|
+ int revived = reviveDeletedFollowRow(fans.getFollowedId(), fans.getFansId());
|
|
|
|
|
+ if (revived > 0) {
|
|
|
|
|
+ sendFollowNoticeIfAllowed(fans);
|
|
|
|
|
+ return LifeFansFollowOutcome.success("关注成功");
|
|
|
}
|
|
}
|
|
|
- Integer followedLifeUserId = resolveLifeUserIdFromFollowedId(fans.getFollowedId());
|
|
|
|
|
- boolean suppressNotice = followedLifeUserId != null
|
|
|
|
|
- && lifeUserPersonalizationSettingService.shouldSuppressFollowRelatedNotice(followedLifeUserId);
|
|
|
|
|
- if (!suppressNotice) {
|
|
|
|
|
- lifeNoticeMapper.insert(notice);
|
|
|
|
|
-
|
|
|
|
|
- // 发送系统通知开关
|
|
|
|
|
- if (uniPushOn) {
|
|
|
|
|
- try {
|
|
|
|
|
- sendFollowRelationAppPush(fans.getFollowedId());
|
|
|
|
|
- } catch (Exception e) {
|
|
|
|
|
- log.warn("关注 App 推送失败,followedId={},err={}", fans.getFollowedId(), e.getMessage());
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+
|
|
|
|
|
+ int num = lifeFansMapper.insert(fans);
|
|
|
|
|
+ if (num == 1) {
|
|
|
|
|
+ sendFollowNoticeIfAllowed(fans);
|
|
|
|
|
+ return LifeFansFollowOutcome.success("关注成功");
|
|
|
|
|
+ }
|
|
|
|
|
+ return LifeFansFollowOutcome.failure("关注失败:数据未保存成功,请稍后重试");
|
|
|
|
|
+ } catch (DuplicateKeyException ex) {
|
|
|
|
|
+ log.debug("addFans duplicate key (concurrent), followedId={} fansId={}", fans.getFollowedId(), fans.getFansId());
|
|
|
|
|
+ return LifeFansFollowOutcome.success("关注成功");
|
|
|
|
|
+ } catch (DataIntegrityViolationException ex) {
|
|
|
|
|
+ if (isDuplicateKeyMessage(ex)) {
|
|
|
|
|
+ return LifeFansFollowOutcome.success("关注成功");
|
|
|
}
|
|
}
|
|
|
|
|
+ log.warn("addFans 数据约束异常 followedId={} fansId={} err={}",
|
|
|
|
|
+ fans.getFollowedId(), fans.getFansId(), ex.getMessage());
|
|
|
|
|
+ return LifeFansFollowOutcome.failure("关注失败:数据校验未通过,请检查参数或稍后再试");
|
|
|
|
|
+ } catch (Exception ex) {
|
|
|
|
|
+ log.error("addFans 未预期异常 followedId={} fansId={}", fans.getFollowedId(), fans.getFansId(), ex);
|
|
|
|
|
+ return LifeFansFollowOutcome.failure("关注失败:系统繁忙,请稍后重试");
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- return num;
|
|
|
|
|
|
|
+ private static boolean isDuplicateKeyMessage(DataIntegrityViolationException ex) {
|
|
|
|
|
+ Throwable t = ex.getMostSpecificCause();
|
|
|
|
|
+ String a = StringUtils.trimToEmpty(t != null ? t.getMessage() : "");
|
|
|
|
|
+ String b = StringUtils.trimToEmpty(ex.getMessage());
|
|
|
|
|
+ String m = (a + " | " + b).toLowerCase(Locale.ROOT);
|
|
|
|
|
+ return m.contains("duplicate") || m.contains("unique") || m.contains("uk_life_fans_follow_pair");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 校验并规范化一组关注标识;有错返回错误文案。
|
|
|
|
|
+ */
|
|
|
|
|
+ private Optional<String> validateAndNormalizeLifeFansPair(LifeFans fans) {
|
|
|
|
|
+ if (fans == null) {
|
|
|
|
|
+ return Optional.of("请求参数不能为空:请传入被关注方与粉丝标识");
|
|
|
|
|
+ }
|
|
|
|
|
+ String fid = StringUtils.trimToEmpty(fans.getFollowedId());
|
|
|
|
|
+ String sid = StringUtils.trimToEmpty(fans.getFansId());
|
|
|
|
|
+
|
|
|
|
|
+ fans.setFollowedId(normalizeFansPhoneId(fid));
|
|
|
|
|
+ fans.setFansId(normalizeFansPhoneId(sid));
|
|
|
|
|
+ fid = fans.getFollowedId();
|
|
|
|
|
+ sid = fans.getFansId();
|
|
|
|
|
+
|
|
|
|
|
+ if (StringUtils.isBlank(fid)) {
|
|
|
|
|
+ return Optional.of("被关注方标识(followedId)不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StringUtils.isBlank(sid)) {
|
|
|
|
|
+ return Optional.of("粉丝方标识(fansId)不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (fid.length() > FAN_IDS_MAX_CHARS || sid.length() > FAN_IDS_MAX_CHARS) {
|
|
|
|
|
+ return Optional.of("关注标识过长(单字段不超过 " + FAN_IDS_MAX_CHARS + " 字符)");
|
|
|
|
|
+ }
|
|
|
|
|
+ Optional<String> fErr = assertValidFanPartyId("被关注方标识(followedId)", fid);
|
|
|
|
|
+ if (fErr.isPresent()) {
|
|
|
|
|
+ return fErr;
|
|
|
|
|
+ }
|
|
|
|
|
+ Optional<String> sErr = assertValidFanPartyId("粉丝方标识(fansId)", sid);
|
|
|
|
|
+ if (sErr.isPresent()) {
|
|
|
|
|
+ return sErr;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (fid.equals(sid)) {
|
|
|
|
|
+ return Optional.of("无效操作:粉丝方与被关注方不能相同");
|
|
|
|
|
+ }
|
|
|
|
|
+ return Optional.empty();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static String normalizeFansPhoneId(String raw) {
|
|
|
|
|
+ int u = raw.indexOf('_');
|
|
|
|
|
+ if (u <= 0) {
|
|
|
|
|
+ return raw;
|
|
|
|
|
+ }
|
|
|
|
|
+ String prefix = raw.substring(0, u).toLowerCase(Locale.ROOT);
|
|
|
|
|
+ return prefix + raw.substring(u);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static Optional<String> assertValidFanPartyId(String label, String value) {
|
|
|
|
|
+ if (!LIFE_FANS_PAIR_ID_PATTERN.matcher(value).matches()) {
|
|
|
|
|
+ return Optional.of(label + "格式错误:须为 store_ 或 user_ 开头,且整体不能包含空格");
|
|
|
|
|
+ }
|
|
|
|
|
+ return Optional.empty();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 绕过 MyBatis-Plus 逻辑删除插件对 Mapper 手写 UPDATE 的条件注入风险,直接恢复一条软删的关注记录。
|
|
|
|
|
+ */
|
|
|
|
|
+ private int reviveDeletedFollowRow(String followedId, String fansId) {
|
|
|
|
|
+ return jdbcTemplate.update(
|
|
|
|
|
+ "UPDATE life_fans SET delete_flag = 0 WHERE followed_id = ? AND fans_id = ? AND delete_flag = 1 LIMIT 1",
|
|
|
|
|
+ followedId,
|
|
|
|
|
+ fansId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void sendFollowNoticeIfAllowed(LifeFans fans) {
|
|
|
|
|
+ LifeNotice notice = new LifeNotice();
|
|
|
|
|
+ notice.setTitle("关注通知");
|
|
|
|
|
+ notice.setContext("关注了你");
|
|
|
|
|
+ notice.setNoticeType(0);
|
|
|
|
|
+ notice.setReceiverId(fans.getFollowedId());
|
|
|
|
|
+ notice.setSenderId(fans.getFansId());
|
|
|
|
|
+
|
|
|
|
|
+ String storePhones = "''";
|
|
|
|
|
+ String userPhones = "''";
|
|
|
|
|
+ String sid = fans.getFansId();
|
|
|
|
|
+ String[] fp = sid.split("_", 2);
|
|
|
|
|
+ if (fp.length >= 2 && StringUtils.isNotBlank(fp[1])) {
|
|
|
|
|
+ if ("store".equalsIgnoreCase(fp[0])) {
|
|
|
|
|
+ storePhones = "'" + fp[1] + "'";
|
|
|
|
|
+ } else if ("user".equalsIgnoreCase(fp[0])) {
|
|
|
|
|
+ userPhones = "'" + fp[1] + "'";
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ log.warn("fansId 无法解析手机号段,跳过业务 id 回填,fansId={}", sid);
|
|
|
|
|
+ }
|
|
|
|
|
+ List<LifeMessageVo> userList = messageMapper.getLifeUserAndStoreUserByPhone(storePhones, userPhones);
|
|
|
|
|
+ if (!CollectionUtils.isEmpty(userList)) {
|
|
|
|
|
+ notice.setBusinessId(userList.get(0).getId());
|
|
|
|
|
+ }
|
|
|
|
|
+ Integer followedLifeUserId = resolveLifeUserIdFromFollowedId(fans.getFollowedId());
|
|
|
|
|
+ boolean suppressNotice = followedLifeUserId != null
|
|
|
|
|
+ && lifeUserPersonalizationSettingService.shouldSuppressFollowRelatedNotice(followedLifeUserId);
|
|
|
|
|
+ if (!suppressNotice) {
|
|
|
|
|
+ lifeNoticeMapper.insert(notice);
|
|
|
|
|
+
|
|
|
|
|
+ if (uniPushOn) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ sendFollowRelationAppPush(fans.getFollowedId());
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("关注 App 推送失败,followedId={},err={}", fans.getFollowedId(), e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -238,11 +359,25 @@ public class LifeUserService extends ServiceImpl<LifeUserMapper, LifeUser> {
|
|
|
return u != null ? u.getId() : null;
|
|
return u != null ? u.getId() : null;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- public int cancelFans(LifeFans fans) {
|
|
|
|
|
- LambdaUpdateWrapper<LifeFans> wrapper = new LambdaUpdateWrapper<>();
|
|
|
|
|
- wrapper.eq(LifeFans::getFansId, fans.getFansId());
|
|
|
|
|
- wrapper.eq(LifeFans::getFollowedId, fans.getFollowedId());
|
|
|
|
|
- return lifeFansMapper.delete(wrapper);
|
|
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public LifeFansFollowOutcome cancelFans(LifeFans fans) {
|
|
|
|
|
+ Optional<String> paramError = validateAndNormalizeLifeFansPair(fans);
|
|
|
|
|
+ if (paramError.isPresent()) {
|
|
|
|
|
+ return LifeFansFollowOutcome.failure(paramError.get());
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ LambdaUpdateWrapper<LifeFans> wrapper = new LambdaUpdateWrapper<>();
|
|
|
|
|
+ wrapper.eq(LifeFans::getFansId, fans.getFansId());
|
|
|
|
|
+ wrapper.eq(LifeFans::getFollowedId, fans.getFollowedId());
|
|
|
|
|
+ int n = lifeFansMapper.delete(wrapper);
|
|
|
|
|
+ if (n <= 0) {
|
|
|
|
|
+ return LifeFansFollowOutcome.success("当前未关注或已取消,无需重复操作");
|
|
|
|
|
+ }
|
|
|
|
|
+ return LifeFansFollowOutcome.success("取消关注成功");
|
|
|
|
|
+ } catch (Exception ex) {
|
|
|
|
|
+ log.error("cancelFans 失败 followedId={} fansId={}", fans.getFollowedId(), fans.getFansId(), ex);
|
|
|
|
|
+ return LifeFansFollowOutcome.failure("取消关注失败:系统繁忙,请稍后重试");
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public IPage<LifeUserVo> getStoreAndUserByName(LifeUserVo vo) {
|
|
public IPage<LifeUserVo> getStoreAndUserByName(LifeUserVo vo) {
|