记得上下班打卡 | git大法好,push需谨慎

Commit bafdd5d7 authored by wangyifan's avatar wangyifan

拼团消息短信通知功能

parent 7b1f5153
......@@ -37,6 +37,10 @@ public class KylinRedisConst {
public static final String GROUP_BUY_ORDER = "kylin:groupBuy:order:id:";
public static final String GROUP_BUY_ORDER_LOCK = "kylin:groupBuy:lock:";
public static final String GROUP_BUY_ORDER_SHARE = "kylin:groupBuy:order:share:";
/** 拼团短信总开关:无 key 或非 0/false 则发送;SET 为 0/false 关停 */
public static final String GROUP_BUY_SMS_ENABLED = "kylin:groupBuy:sms:enabled";
/** 拼团 1h 提醒防重:key + groupOrderId */
public static final String GROUP_BUY_REMIND_1H = "kylin:groupBuy:remind1h:";
public static final String CAMERA_DEVICES = "kylin:cameraDevices:list";
public static final String CAMERA_DEVICES_FIELD = "kylin:cameraDevices:fieldId:";
......
......@@ -52,6 +52,8 @@ public class KylinGroupBuyOrderVo implements Serializable {
@ApiModelProperty(value = "更新时间")
private String updatedAt;
@ApiModelProperty(value = "是否已发送到期前1小时提醒 1已发 0/null未发")
private Integer remind1hSent;
@ApiModelProperty(value = "成员")
private List<KylinGroupBuyMemberVo> members;
......
package com.liquidnet.service.kylin.service;
/**
* 拼团短信通知(成功 / 超时失败 / 到期前 1h)。
* 主挂载点在 order(成功)与 platform(失败、1h);本接口供 kylin 侧复用。
*/
public interface GroupBuyNotifyService {
void notifyTimeoutRefund(String groupOrderId);
void notifySuccess(String groupOrderId);
void notifyLeaderCancelled(String groupOrderId);
void notifyFailed(String groupOrderId);
void notifyRemind1h(String groupOrderId);
}
......@@ -82,6 +82,24 @@ public class SmsEnum {
* - 模版内容:欢迎加入登登登VIP大家庭。请微信关注“摩登天空服务号”,回复VIP,获取专属小助理客服服务了,回T退订
*/
SMS_232908714,
/**
* 拼团成功
* - 恭喜拼团成功,您购买的是「${name}」演出的「${time}」场次「${ticket}」票种,祝您观演愉快
* - {"name":"...","time":"...","ticket":"..."}
*/
SMS_510540063,
/**
* 拼团失败/超时未成团
* - 实在抱歉,您的拼团订单在「${hours}」小时内无人拼单,拼团失败,已支付金额会在 1-3 个工作日原路退回,请注意查收。
* - {"hours":"24"}
*/
SMS_510615071,
/**
* 拼团倒计时剩 1 小时提醒
* - 您的拼团订单还有 1 小时到期,快转发朋友一起拼单吧,逾期未成团,订单将自动取消。
* - {}
*/
SMS_510285138,
;
}
......
......@@ -60,4 +60,7 @@ public interface FeignPlatformTaskClient {
@PostMapping("platform/groupBuy/activitySchedule")
ResponseDto<Boolean> groupBuyActivitySchedule();
@PostMapping("platform/groupBuy/remind1h")
ResponseDto<Boolean> groupBuyRemind1h();
}
......@@ -142,4 +142,14 @@ public class KylinTaskHandler {
}
}
@XxlJob(value = "sev-platform:groupBuyRemind1h")
public void groupBuyRemind1h() {
try {
XxlJobHelper.handleSuccess("结果:" + feignPlatformTaskClient.groupBuyRemind1h().getData());
} catch (Exception e) {
XxlJobHelper.log(e);
XxlJobHelper.handleFail();
}
}
}
......@@ -55,6 +55,13 @@
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<!-- SmsProcessor / SmsEnum:拼团短信 -->
<dependency>
<groupId>com.liquidnet</groupId>
<artifactId>liquidnet-common-sms</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
......
package com.liquidnet.service.kylin.service.impl;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyOrderVo;
import com.liquidnet.service.kylin.service.GroupBuyNotifyService;
import com.liquidnet.service.kylin.utils.GroupBuySmsSender;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class GroupBuyNotifyServiceImpl implements GroupBuyNotifyService {
@Autowired
private MongoTemplate mongoTemplate;
@Autowired
private GroupBuySmsSender groupBuySmsSender;
@Override
public void notifySuccess(String groupOrderId) {
KylinGroupBuyOrderVo group = load(groupOrderId);
if (group == null) {
return;
}
groupBuySmsSender.notifySuccess(group);
}
@Override
public void notifyTimeoutRefund(String groupOrderId) {
log.info("拼团超时退款通知预留,groupOrderId={}", groupOrderId);
public void notifyFailed(String groupOrderId) {
KylinGroupBuyOrderVo group = load(groupOrderId);
if (group == null) {
return;
}
groupBuySmsSender.notifyFailed(group);
}
@Override
public void notifyLeaderCancelled(String groupOrderId) {
log.info("团长取消拼团通知预留,groupOrderId={}", groupOrderId);
public void notifyRemind1h(String groupOrderId) {
KylinGroupBuyOrderVo group = load(groupOrderId);
if (group == null) {
return;
}
groupBuySmsSender.notifyRemind1h(group);
}
private KylinGroupBuyOrderVo load(String groupOrderId) {
if (groupOrderId == null) {
return null;
}
return mongoTemplate.findOne(
Query.query(Criteria.where("groupOrderId").is(groupOrderId)),
KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName());
}
}
......@@ -23,7 +23,6 @@ import com.liquidnet.service.kylin.dto.vo.mongo.KylinOrderTicketVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinPerformanceVo;
import com.liquidnet.service.kylin.dto.vo.middle.KylinTicketTimesVo;
import com.liquidnet.service.kylin.dto.vo.middle.KylinTicketVo;
import com.liquidnet.service.kylin.service.GroupBuyNotifyService;
import com.liquidnet.service.kylin.service.IKylinGroupBuyOrderService;
import com.liquidnet.service.kylin.utils.DataUtils;
import com.liquidnet.service.kylin.utils.QueueUtils;
......@@ -56,8 +55,6 @@ public class KylinGroupBuyOrderServiceImpl implements IKylinGroupBuyOrderService
@Autowired
private KylinOrderTicketsRefundServiceImpl refundService;
@Autowired
private GroupBuyNotifyService notifyService;
@Autowired
private RedisUtil redisUtil;
@Autowired
private QueueUtils queueUtils;
......@@ -219,7 +216,6 @@ public class KylinGroupBuyOrderServiceImpl implements IKylinGroupBuyOrderService
for (KylinGroupBuyMemberVo member : joinedToRefund) {
refundJoinedMember(member.getOrderTicketsId(), "团长取消拼团,全额退款");
}
notifyLeaderCancelled(groupOrderId);
return ResponseDto.success(true);
}
......@@ -371,14 +367,6 @@ public class KylinGroupBuyOrderServiceImpl implements IKylinGroupBuyOrderService
}
}
private void notifyLeaderCancelled(String groupOrderId) {
try {
notifyService.notifyLeaderCancelled(groupOrderId);
} catch (Exception e) {
log.error("团长取消拼团通知失败 groupOrderId={}", groupOrderId, e);
}
}
private String now() {
return DateUtil.Formatter.yyyyMMddHHmmss.format(LocalDateTime.now());
}
......
package com.liquidnet.service.kylin.utils;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.liquidnet.common.cache.redis.util.RedisUtil;
import com.liquidnet.common.sms.constant.SmsEnum;
import com.liquidnet.common.sms.processor.SmsProcessor;
import com.liquidnet.commons.lang.util.DateUtil;
import com.liquidnet.commons.lang.util.JsonUtils;
import com.liquidnet.service.base.constant.RedisKeyExpireConst;
import com.liquidnet.service.kylin.constant.GroupBuyStatusConst;
import com.liquidnet.service.kylin.constant.KylinRedisConst;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyActivityVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyMemberVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyOrderVo;
import com.liquidnet.service.kylin.dto.vo.middle.KylinTicketTimesVo;
import com.liquidnet.service.kylin.dto.vo.middle.KylinTicketVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinOrderTicketVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinPerformanceVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 拼团短信发送(kylin 侧)。总开关:{@link KylinRedisConst#GROUP_BUY_SMS_ENABLED}。
*/
@Component
@Slf4j
public class GroupBuySmsSender {
@Autowired
private SmsProcessor smsProcessor;
@Autowired
private RedisUtil redisUtil;
@Autowired
private MongoTemplate mongoTemplate;
@Autowired
private DataUtils dataUtils;
public void notifySuccess(KylinGroupBuyOrderVo group) {
if (group == null || !isSmsEnabled()) {
return;
}
List<String> phones = collectPhones(group, true, false);
if (phones.isEmpty()) {
return;
}
ObjectNode params = buildSuccessParams(group);
for (String phone : phones) {
sendSafe(phone, SmsEnum.ADTemplate.SMS_510540063.name(),
params.toString(), group.getGroupOrderId(), "success");
}
}
public void notifyFailed(KylinGroupBuyOrderVo group) {
if (group == null || !isSmsEnabled()) {
return;
}
List<String> phones = collectPhones(group, true, false);
if (phones.isEmpty()) {
return;
}
ObjectNode params = JsonUtils.OM().createObjectNode();
params.put("hours", resolveHours(group));
for (String phone : phones) {
sendSafe(phone, SmsEnum.ADTemplate.SMS_510615071.name(),
params.toString(), group.getGroupOrderId(), "failed");
}
}
public void notifyRemind1h(KylinGroupBuyOrderVo group) {
if (group == null || !isSmsEnabled()) {
return;
}
List<String> phones = collectPhones(group, false, true);
if (phones.isEmpty()) {
return;
}
String templateParam = JsonUtils.OM().createObjectNode().toString();
for (String phone : phones) {
sendSafe(phone, SmsEnum.ADTemplate.SMS_510285138.name(),
templateParam, group.getGroupOrderId(), "remind1h");
}
redisUtil.set(KylinRedisConst.GROUP_BUY_REMIND_1H.concat(group.getGroupOrderId()), "1",
RedisKeyExpireConst.KYLIN_GROUP_BUY_EXPIRE);
}
private boolean isSmsEnabled() {
Object v = redisUtil.get(KylinRedisConst.GROUP_BUY_SMS_ENABLED);
if (v == null) {
return true;
}
String s = String.valueOf(v).trim();
return !("0".equals(s) || "false".equalsIgnoreCase(s));
}
private List<String> collectPhones(KylinGroupBuyOrderVo group, boolean joinedOnly, boolean includePendingPay) {
Set<String> phones = new HashSet<>();
if (group == null || CollectionUtils.isEmpty(group.getMembers())) {
return new ArrayList<>();
}
for (KylinGroupBuyMemberVo m : group.getMembers()) {
if (m == null || StringUtils.isBlank(m.getOrderTicketsId())
|| m.getOrderTicketsId().startsWith("ROBOT_")) {
continue;
}
Integer st = m.getStatus();
boolean ok = joinedOnly
? GroupBuyStatusConst.MemberStatus.JOINED.equals(st)
: GroupBuyStatusConst.MemberStatus.JOINED.equals(st)
|| (includePendingPay && GroupBuyStatusConst.MemberStatus.PENDING_PAY.equals(st));
if (!ok) {
continue;
}
String mobile = resolveMobile(m.getOrderTicketsId());
if (StringUtils.isNotBlank(mobile)) {
phones.add(mobile.trim());
}
}
return new ArrayList<>(phones);
}
private String resolveMobile(String orderTicketsId) {
Object cached = redisUtil.get(KylinRedisConst.ORDER.concat(orderTicketsId));
if (cached instanceof KylinOrderTicketVo) {
return ((KylinOrderTicketVo) cached).getUserMobile();
}
KylinOrderTicketVo ticket = mongoTemplate.findOne(
Query.query(Criteria.where("orderTicketsId").is(orderTicketsId)),
KylinOrderTicketVo.class, KylinOrderTicketVo.class.getSimpleName());
return ticket == null ? null : ticket.getUserMobile();
}
private ObjectNode buildSuccessParams(KylinGroupBuyOrderVo group) {
ObjectNode node = JsonUtils.OM().createObjectNode();
String name = "";
String time = "";
String ticket = "";
try {
KylinGroupBuyActivityVo activity = mongoTemplate.findOne(
Query.query(Criteria.where("activityId").is(group.getActivityId())),
KylinGroupBuyActivityVo.class, KylinGroupBuyActivityVo.class.getSimpleName());
if (activity != null && StringUtils.isNotBlank(activity.getPerformanceId())) {
KylinPerformanceVo p = dataUtils.getPerformanceVo(activity.getPerformanceId());
if (p != null) {
name = p.getTitle() == null ? "" : p.getTitle();
if (p.getTicketTimeList() != null && group.getTimeId() != null) {
for (KylinTicketTimesVo t : p.getTicketTimeList()) {
if (!group.getTimeId().equals(t.getTimeId())) {
continue;
}
time = t.getTitle() == null ? "" : t.getTitle();
if (t.getTicketList() != null && group.getTicketId() != null) {
for (KylinTicketVo tk : t.getTicketList()) {
if (group.getTicketId().equals(tk.getTicketsId())) {
ticket = tk.getTitle() == null ? "" : tk.getTitle();
break;
}
}
}
break;
}
}
}
}
} catch (Exception e) {
log.warn("拼团成功短信变量组装失败 groupOrderId={}", group.getGroupOrderId(), e);
}
node.put("name", name);
node.put("time", time);
node.put("ticket", ticket);
return node;
}
private String resolveHours(KylinGroupBuyOrderVo group) {
try {
if (StringUtils.isNotBlank(group.getCreatedAt()) && StringUtils.isNotBlank(group.getExpiresAt())) {
LocalDateTime created = DateUtil.Formatter.yyyyMMddHHmmss.parse(group.getCreatedAt());
LocalDateTime expires = DateUtil.Formatter.yyyyMMddHHmmss.parse(group.getExpiresAt());
long hours = Duration.between(created, expires).toHours();
return String.valueOf(hours < 1 ? 1 : hours);
}
} catch (Exception e) {
log.warn("拼团失败短信 hours 解析失败 groupOrderId={}", group.getGroupOrderId(), e);
}
return "24";
}
private void sendSafe(String phone, String templateCode, String templateParam, String groupOrderId, String scene) {
try {
boolean ok = smsProcessor.send(phone, SmsEnum.ADSignName.M02.getVal(), templateCode, templateParam);
if (!ok) {
log.warn("拼团短信发送失败 scene={} groupOrderId={} phone={}", scene, groupOrderId, phone);
}
} catch (Exception e) {
log.error("拼团短信发送异常 scene={} groupOrderId={} phone={}", scene, groupOrderId, phone, e);
}
}
}
......@@ -26,6 +26,11 @@
<groupId>com.liquidnet</groupId>
<artifactId>liquidnet-common-web</artifactId>
</dependency>
<dependency>
<groupId>com.liquidnet</groupId>
<artifactId>liquidnet-common-sms</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
......@@ -46,6 +46,8 @@ public class GroupBuyOrderHelper {
private RedisUtil redisUtil;
@Autowired
private QueueUtils queueUtils;
@Autowired
private GroupBuySmsSender groupBuySmsSender;
public boolean isGroupBuyOrder(PayOrderParam param) {
return param != null && (StringUtils.isNotBlank(param.getGroupBuyActivityId())
......@@ -344,12 +346,21 @@ public class GroupBuyOrderHelper {
}
if (GroupBuyStatusConst.OrderStatus.PENDING.equals(groupStatus)) {
member.setStatus(GroupBuyStatusConst.MemberStatus.JOINED);
boolean becameSuccess = false;
if (group.getCurrentCount() != null && group.getTargetCount() != null
&& group.getCurrentCount() >= group.getTargetCount()) {
group.setStatus(GroupBuyStatusConst.OrderStatus.SUCCESS);
becameSuccess = true;
}
group.setUpdatedAt(now);
saveGroup(group);
if (becameSuccess) {
try {
groupBuySmsSender.notifySuccess(group);
} catch (Exception e) {
log.error("拼团成功短信通知失败 groupOrderId={}", group.getGroupOrderId(), e);
}
}
return 1;
}
return 0;
......
package com.liquidnet.service.order.utils;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.liquidnet.common.cache.redis.util.RedisUtil;
import com.liquidnet.common.sms.constant.SmsEnum;
import com.liquidnet.common.sms.processor.SmsProcessor;
import com.liquidnet.commons.lang.util.JsonUtils;
import com.liquidnet.service.kylin.constant.GroupBuyStatusConst;
import com.liquidnet.service.kylin.constant.KylinRedisConst;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyActivityVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyMemberVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyOrderVo;
import com.liquidnet.service.kylin.dto.vo.middle.KylinTicketTimesVo;
import com.liquidnet.service.kylin.dto.vo.middle.KylinTicketVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinOrderTicketVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinPerformanceVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 拼团短信:成功。复用 SmsProcessor;总开关见 Redis {@link KylinRedisConst#GROUP_BUY_SMS_ENABLED}。
*/
@Component
@Slf4j
public class GroupBuySmsSender {
@Autowired
private SmsProcessor smsProcessor;
@Autowired
private RedisUtil redisUtil;
@Autowired
private MongoTemplate mongoTemplate;
@Autowired
private DataUtils dataUtils;
public void notifySuccess(KylinGroupBuyOrderVo group) {
if (group == null || !isSmsEnabled()) {
return;
}
List<String> phones = collectPhones(group, true, false);
if (phones.isEmpty()) {
log.info("拼团成功短信无有效手机号 groupOrderId={}", group.getGroupOrderId());
return;
}
ObjectNode params = buildSuccessParams(group);
String templateParam = params.toString();
for (String phone : phones) {
sendSafe(phone, SmsEnum.ADTemplate.SMS_510540063.name(), templateParam,
group.getGroupOrderId(), "success");
}
}
private boolean isSmsEnabled() {
Object v = redisUtil.get(KylinRedisConst.GROUP_BUY_SMS_ENABLED);
if (v == null) {
return true;
}
String s = String.valueOf(v).trim();
return !("0".equals(s) || "false".equalsIgnoreCase(s));
}
/**
* @param joinedOnly true=仅 JOINED;false 且 includePendingPay=true 时含 JOINED+PENDING_PAY
*/
private List<String> collectPhones(KylinGroupBuyOrderVo group, boolean joinedOnly, boolean includePendingPay) {
Set<String> phones = new HashSet<>();
if (group == null || CollectionUtils.isEmpty(group.getMembers())) {
return new ArrayList<>();
}
for (KylinGroupBuyMemberVo m : group.getMembers()) {
if (m == null || StringUtils.isBlank(m.getOrderTicketsId())) {
continue;
}
if (m.getOrderTicketsId().startsWith("ROBOT_")) {
continue;
}
Integer st = m.getStatus();
boolean ok = GroupBuyStatusConst.MemberStatus.JOINED.equals(st)
|| (includePendingPay && GroupBuyStatusConst.MemberStatus.PENDING_PAY.equals(st));
if (joinedOnly) {
ok = GroupBuyStatusConst.MemberStatus.JOINED.equals(st);
}
if (!ok) {
continue;
}
String mobile = resolveMobile(m.getOrderTicketsId());
if (StringUtils.isNotBlank(mobile)) {
phones.add(mobile.trim());
}
}
return new ArrayList<>(phones);
}
private String resolveMobile(String orderTicketsId) {
Object cached = redisUtil.get(KylinRedisConst.ORDER.concat(orderTicketsId));
if (cached instanceof KylinOrderTicketVo) {
return ((KylinOrderTicketVo) cached).getUserMobile();
}
KylinOrderTicketVo ticket = mongoTemplate.findOne(
Query.query(Criteria.where("orderTicketsId").is(orderTicketsId)),
KylinOrderTicketVo.class, KylinOrderTicketVo.class.getSimpleName());
return ticket == null ? null : ticket.getUserMobile();
}
private ObjectNode buildSuccessParams(KylinGroupBuyOrderVo group) {
ObjectNode node = JsonUtils.OM().createObjectNode();
String name = "";
String time = "";
String ticket = "";
try {
KylinGroupBuyActivityVo activity = mongoTemplate.findOne(
Query.query(Criteria.where("activityId").is(group.getActivityId())),
KylinGroupBuyActivityVo.class, KylinGroupBuyActivityVo.class.getSimpleName());
if (activity != null && StringUtils.isNotBlank(activity.getPerformanceId())) {
KylinPerformanceVo p = dataUtils.getPerformanceVo(activity.getPerformanceId());
if (p != null) {
name = p.getTitle() == null ? "" : p.getTitle();
String[] titles = resolveTimeTicketTitle(p, group.getTimeId(), group.getTicketId());
time = titles[0];
ticket = titles[1];
}
}
} catch (Exception e) {
log.warn("拼团成功短信组装变量失败 groupOrderId={}", group.getGroupOrderId(), e);
}
node.put("name", name);
node.put("time", time);
node.put("ticket", ticket);
return node;
}
private String[] resolveTimeTicketTitle(KylinPerformanceVo p, String timeId, String ticketId) {
String timeTitle = "";
String ticketTitle = "";
if (p.getTicketTimeList() == null || timeId == null) {
return new String[]{timeTitle, ticketTitle};
}
for (KylinTicketTimesVo t : p.getTicketTimeList()) {
if (!timeId.equals(t.getTimeId())) {
continue;
}
timeTitle = t.getTitle() == null ? "" : t.getTitle();
if (t.getTicketList() != null && ticketId != null) {
for (KylinTicketVo tk : t.getTicketList()) {
if (ticketId.equals(tk.getTicketsId())) {
ticketTitle = tk.getTitle() == null ? "" : tk.getTitle();
break;
}
}
}
break;
}
return new String[]{timeTitle, ticketTitle};
}
private void sendSafe(String phone, String templateCode, String templateParam, String groupOrderId, String scene) {
try {
boolean ok = smsProcessor.send(phone, SmsEnum.ADSignName.M02.getVal(), templateCode, templateParam);
if (!ok) {
log.warn("拼团短信发送失败 scene={} groupOrderId={} phone={}", scene, groupOrderId, phone);
}
} catch (Exception e) {
log.error("拼团短信发送异常 scene={} groupOrderId={} phone={}", scene, groupOrderId, phone, e);
}
}
}
......@@ -30,4 +30,10 @@ public class PlatformGroupBuyTaskController {
public ResponseDto<Boolean> activitySchedule() {
return ResponseDto.success(groupBuyTaskService.activitySchedule());
}
@PostMapping("remind1h")
@ApiOperation("拼团到期前1小时提醒任务")
public ResponseDto<Boolean> remind1h() {
return ResponseDto.success(groupBuyTaskService.remind1h());
}
}
......@@ -14,6 +14,7 @@ import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyOrderVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinOrderTicketEntitiesVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinOrderTicketVo;
import com.liquidnet.service.platform.utils.GroupBuyPlatformHelper;
import com.liquidnet.service.platform.utils.GroupBuySmsSender;
import com.liquidnet.service.platform.utils.QueueUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -43,6 +44,8 @@ public class PlatformGroupBuyTaskServiceImpl {
private QueueUtils queueUtils;
@Autowired
private GroupBuyPlatformHelper groupBuyPlatformHelper;
@Autowired
private GroupBuySmsSender groupBuySmsSender;
@Value("${liquidnet.service.kylin.url}")
private String kylinUrl;
......@@ -70,6 +73,12 @@ public class PlatformGroupBuyTaskServiceImpl {
}
fresh.setStatus(GroupBuyStatusConst.OrderStatus.FAILED);
fresh.setUpdatedAt(now);
// 失败短信需在 JOINED 改成 REFUNDED 之前发送
try {
groupBuySmsSender.notifyFailed(fresh);
} catch (Exception e) {
log.error("拼团失败短信通知失败 groupOrderId={}", groupOrderId, e);
}
List<String> joinedOrderTicketIds = new ArrayList<>();
if (!CollectionUtils.isEmpty(fresh.getMembers())) {
for (KylinGroupBuyMemberVo member : fresh.getMembers()) {
......@@ -82,7 +91,6 @@ public class PlatformGroupBuyTaskServiceImpl {
}
}
fresh.setCurrentCount(0);
// Mongo + Redis + MySQL(delete/insert 全量同步团与成员)
groupBuyPlatformHelper.saveGroup(fresh);
for (String orderTicketsId : joinedOrderTicketIds) {
refundJoinedTicket(orderTicketsId, "拼团超时未成团,全额退款");
......@@ -96,6 +104,56 @@ public class PlatformGroupBuyTaskServiceImpl {
return true;
}
/**
* 到期前 1 小时提醒:每个团只发一次(remind1hSent + Redis 防重)。
*/
public boolean remind1h() {
LocalDateTime nowLdt = LocalDateTime.now();
String now = DateUtil.Formatter.yyyyMMddHHmmss.format(nowLdt);
String deadline = DateUtil.Formatter.yyyyMMddHHmmss.format(nowLdt.plusHours(1));
Query query = Query.query(Criteria.where("status").is(GroupBuyStatusConst.OrderStatus.PENDING)
.and("expiresAt").gt(now).lte(deadline)
.and("remind1hSent").ne(1));
List<KylinGroupBuyOrderVo> orders = mongoTemplate.find(query,
KylinGroupBuyOrderVo.class, KylinGroupBuyOrderVo.class.getSimpleName());
for (KylinGroupBuyOrderVo order : orders) {
String groupOrderId = order.getGroupOrderId();
if (!groupBuyPlatformHelper.tryLock(groupOrderId)) {
log.warn("拼团1h提醒获取锁失败 groupOrderId={}", groupOrderId);
continue;
}
try {
KylinGroupBuyOrderVo fresh = groupBuyPlatformHelper.loadFromMongo(groupOrderId);
if (fresh == null || !GroupBuyStatusConst.OrderStatus.PENDING.equals(fresh.getStatus())) {
continue;
}
if (groupBuySmsSender.isRemind1hAlreadySent(fresh)) {
continue;
}
if (fresh.getExpiresAt() == null) {
continue;
}
LocalDateTime expiresAt = DateUtil.Formatter.yyyyMMddHHmmss.parse(fresh.getExpiresAt());
if (!expiresAt.isAfter(nowLdt) || expiresAt.isAfter(nowLdt.plusHours(1))) {
continue;
}
try {
groupBuySmsSender.notifyRemind1h(fresh);
} catch (Exception e) {
log.error("拼团1h提醒短信失败 groupOrderId={}", groupOrderId, e);
}
fresh.setRemind1hSent(1);
fresh.setUpdatedAt(now);
groupBuyPlatformHelper.saveGroup(fresh);
} catch (Exception e) {
log.error("拼团1h提醒处理失败 groupOrderId={}", groupOrderId, e);
} finally {
groupBuyPlatformHelper.unlock(groupOrderId);
}
}
return true;
}
public boolean activitySchedule() {
String now = now();
List<KylinGroupBuyActivityVo> dueStartList = mongoTemplate.find(Query.query(Criteria.where("status").is(GroupBuyStatusConst.ActivityStatus.PENDING)
......
package com.liquidnet.service.platform.utils;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.liquidnet.common.cache.redis.util.RedisUtil;
import com.liquidnet.common.sms.constant.SmsEnum;
import com.liquidnet.common.sms.processor.SmsProcessor;
import com.liquidnet.commons.lang.util.DateUtil;
import com.liquidnet.commons.lang.util.JsonUtils;
import com.liquidnet.service.base.constant.RedisKeyExpireConst;
import com.liquidnet.service.kylin.constant.GroupBuyStatusConst;
import com.liquidnet.service.kylin.constant.KylinRedisConst;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyMemberVo;
import com.liquidnet.service.kylin.dto.vo.groupbuy.KylinGroupBuyOrderVo;
import com.liquidnet.service.kylin.dto.vo.mongo.KylinOrderTicketVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 拼团短信:失败 / 1h 提醒。复用 SmsProcessor;总开关 Redis {@link KylinRedisConst#GROUP_BUY_SMS_ENABLED}。
*/
@Component
@Slf4j
public class GroupBuySmsSender {
@Autowired
private SmsProcessor smsProcessor;
@Autowired
private RedisUtil redisUtil;
@Autowired
private MongoTemplate mongoTemplate;
public void notifyFailed(KylinGroupBuyOrderVo group) {
if (group == null || !isSmsEnabled()) {
return;
}
List<String> phones = collectPhones(group, true, false);
if (phones.isEmpty()) {
log.info("拼团失败短信无有效手机号 groupOrderId={}", group.getGroupOrderId());
return;
}
ObjectNode params = JsonUtils.OM().createObjectNode();
params.put("hours", resolveHours(group));
String templateParam = params.toString();
for (String phone : phones) {
sendSafe(phone, SmsEnum.ADTemplate.SMS_510615071.name(), templateParam,
group.getGroupOrderId(), "failed");
}
}
public void notifyRemind1h(KylinGroupBuyOrderVo group) {
if (group == null || !isSmsEnabled()) {
return;
}
List<String> phones = collectPhones(group, false, true);
if (phones.isEmpty()) {
log.info("拼团1h提醒短信无有效手机号 groupOrderId={}", group.getGroupOrderId());
return;
}
String templateParam = JsonUtils.OM().createObjectNode().toString();
for (String phone : phones) {
sendSafe(phone, SmsEnum.ADTemplate.SMS_510285138.name(), templateParam,
group.getGroupOrderId(), "remind1h");
}
redisUtil.set(KylinRedisConst.GROUP_BUY_REMIND_1H.concat(group.getGroupOrderId()), "1",
RedisKeyExpireConst.KYLIN_GROUP_BUY_EXPIRE);
}
public boolean isRemind1hAlreadySent(KylinGroupBuyOrderVo group) {
if (group == null) {
return true;
}
if (Integer.valueOf(1).equals(group.getRemind1hSent())) {
return true;
}
Object v = redisUtil.get(KylinRedisConst.GROUP_BUY_REMIND_1H.concat(group.getGroupOrderId()));
return v != null && ("1".equals(String.valueOf(v)) || "true".equalsIgnoreCase(String.valueOf(v)));
}
private boolean isSmsEnabled() {
Object v = redisUtil.get(KylinRedisConst.GROUP_BUY_SMS_ENABLED);
if (v == null) {
return true;
}
String s = String.valueOf(v).trim();
return !("0".equals(s) || "false".equalsIgnoreCase(s));
}
private List<String> collectPhones(KylinGroupBuyOrderVo group, boolean joinedOnly, boolean includePendingPay) {
Set<String> phones = new HashSet<>();
if (group == null || CollectionUtils.isEmpty(group.getMembers())) {
return new ArrayList<>();
}
for (KylinGroupBuyMemberVo m : group.getMembers()) {
if (m == null || StringUtils.isBlank(m.getOrderTicketsId())) {
continue;
}
if (m.getOrderTicketsId().startsWith("ROBOT_")) {
continue;
}
Integer st = m.getStatus();
boolean ok;
if (joinedOnly) {
ok = GroupBuyStatusConst.MemberStatus.JOINED.equals(st);
} else {
ok = GroupBuyStatusConst.MemberStatus.JOINED.equals(st)
|| (includePendingPay && GroupBuyStatusConst.MemberStatus.PENDING_PAY.equals(st));
}
if (!ok) {
continue;
}
String mobile = resolveMobile(m.getOrderTicketsId());
if (StringUtils.isNotBlank(mobile)) {
phones.add(mobile.trim());
}
}
return new ArrayList<>(phones);
}
private String resolveMobile(String orderTicketsId) {
Object cached = redisUtil.get(KylinRedisConst.ORDER.concat(orderTicketsId));
if (cached instanceof KylinOrderTicketVo) {
return ((KylinOrderTicketVo) cached).getUserMobile();
}
KylinOrderTicketVo ticket = mongoTemplate.findOne(
Query.query(Criteria.where("orderTicketsId").is(orderTicketsId)),
KylinOrderTicketVo.class, KylinOrderTicketVo.class.getSimpleName());
return ticket == null ? null : ticket.getUserMobile();
}
private String resolveHours(KylinGroupBuyOrderVo group) {
try {
if (StringUtils.isNotBlank(group.getCreatedAt()) && StringUtils.isNotBlank(group.getExpiresAt())) {
LocalDateTime created = DateUtil.Formatter.yyyyMMddHHmmss.parse(group.getCreatedAt());
LocalDateTime expires = DateUtil.Formatter.yyyyMMddHHmmss.parse(group.getExpiresAt());
long hours = Duration.between(created, expires).toHours();
if (hours < 1) {
hours = 1;
}
return String.valueOf(hours);
}
} catch (Exception e) {
log.warn("拼团失败短信 hours 解析失败 groupOrderId={}", group.getGroupOrderId(), e);
}
return "24";
}
private void sendSafe(String phone, String templateCode, String templateParam, String groupOrderId, String scene) {
try {
boolean ok = smsProcessor.send(phone, SmsEnum.ADSignName.M02.getVal(), templateCode, templateParam);
if (!ok) {
log.warn("拼团短信发送失败 scene={} groupOrderId={} phone={}", scene, groupOrderId, phone);
}
} catch (Exception e) {
log.error("拼团短信发送异常 scene={} groupOrderId={} phone={}", scene, groupOrderId, phone, e);
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment