前言
电商开发始终离不开营销活动,营销活动的场次时间计算又是重中之重。 现需要开发支持各类营销活动场景的时间场次计算,要求点如下:
单场次
活动,开始至结束时间范围内进行;日场次
活动,支持每日多场次进行;周场次
活动,支持指定每周几进行,指定天可多场次;月场次
活动,支持指定每月几号进行,指定天可多场次;- 每场活动开始前需展示几点/周几几点开始,结束后立马切换为下一场开始时间,直至活动结束;
大家思考下该怎么做呢?
作者最近工作刚好对老营销活动重做开发,以此篇文章带你详细剖析营销活动的场次时间计算!
正文
分析上述需求点,形成页面如下:
前序
设计基础类如下:
java
/**
* 活动基础类
*/
public class Activity {
/**
* 活动开始时间 yyyy-MM-dd HH:mm:ss
*/
private String allStartTime;
/**
* 活动结束时间 yyyy-MM-dd HH:mm:ss
*/
private String allEndTime;
/**
* 活动规则类型NONE单场次;DAY日场次;WEEK周场次;MONTH月场次
*/
private String actRuleType;
/**
* 活动规则日期配置
* actRuleType为NONE/DAY时无值
* actRuleType为WEEK时1,2,3,4,5,6,7
* actRuleType为MONTH时1,2,...,28,29,30,31
*/
private String actRuleDays;
/**
* 活动规则时分秒配置 HH:mm:ss-HH:mm:ss,HH:mm:ss-HH:mm:ss...
* actRuleType为NONE时无值
* 例:08:00:00-09:59:59,18:00:00-19:59:59...
*/
private String actRuleTimes;
... 构造方法省略
}
java
/**
* 活动场次类
*/
public class ActivityTime {
//当前场次开始时间
private String currentStartTime;
//当前场次结束时间
private String currentEndTime;
//下一场次开始时间
private String nextStartTime;
//下一场次结束时间
private String nextEndTime;
... 构造方法省略
@Override
public String toString() {
return "ActivityTime{" +
"currentStartTime='" + currentStartTime + ''' +
", currentEndTime='" + currentEndTime + ''' +
", nextStartTime='" + nextStartTime + ''' +
", nextEndTime='" + nextEndTime + ''' +
'}';
}
下面开始主要方法分析编码:
活动单场次
单场次的最为简单,依据活动开始结束时间,生成当前场次活动时间即可
java
//此处为代码片段
if(ActivityRuleType.NONE.name().equals(actRuleType)){
return new ActivityTime(activity.getAllStartTime(), activity.getAllEndTime(), null, null);
}
活动日场次
- 计算当前场次
- 活动未开始时取活动开始第一天
+
第一个时间段拼接; - 活动开始后需判断当天是否还有场次,有则取当天
+
对应时间段拼接场次,无则推迟一天+
第一个时间段拼接;
- 活动未开始时取活动开始第一天
- 计算下一场次
- 当日还有场次则取当前场次的日期
+
下一时间段拼接; - 当日无场次则推迟一天
+
第一个时间段拼接;
- 当日还有场次则取当前场次的日期
java
/**
* 计算日规则场次时间
*/
private ActivityTime computeDayRuleTime(Activity activity, LocalDateTime nowDateTime){
String nowDateTimeStr = formatter.format(nowDateTime);
String allStartTimeStr = activity.getAllStartTime();
String allEndTimeStr = activity.getAllEndTime();
LocalDateTime allStartTime = LocalDateTime.parse(allStartTimeStr, formatter);
LocalDateTime allEndTime = LocalDateTime.parse(allEndTimeStr, formatter);
List<String> timeList = Arrays.stream(activity.getActRuleTimes().split(",")).collect(Collectors.toList());
ActivityTime time = new ActivityTime();
//计算当前场次
String currentStartTime = null;
String currentEndTime = null;
if(nowDateTime.isBefore(allStartTime)){//活动未开始
String[] strs = timeList.get(0).split("-");
currentStartTime = assembleDateTime(allStartTimeStr, strs[0]);
currentEndTime = assembleDateTime(allStartTimeStr, strs[1]);
}else{//活动已开始
for (String timeStr : timeList){
String[] strs = timeStr.split("-");
LocalTime endTime = LocalTime.parse(strs[1], formatterTime);
if(nowDateTime.toLocalTime().isBefore(endTime)){//判断当天是否还有场次
currentStartTime = assembleDateTime(nowDateTimeStr, strs[0]);
currentEndTime = assembleDateTime(nowDateTimeStr, strs[1]);
break;
}
}
if(currentStartTime==null){//当天无场次
//往后推一天且未结束
LocalDateTime dateTime = nowDateTime.plusDays(1);
if(!dateTime.isAfter(allEndTime)){
String[] strs = timeList.get(0).split("-");
nowDateTimeStr = formatter.format(dateTime);
currentStartTime = assembleDateTime(nowDateTimeStr, strs[0]);
currentEndTime = assembleDateTime(nowDateTimeStr, strs[1]);
}
}
}
if(currentStartTime==null){
return time;
}
//计算下一场次
String nextStartTime = null;
String nextEndTime = null;
//当前场次的开始时分秒-结束时分秒
String currentTimeStr = currentStartTime.substring(11,19)+"-"+currentEndTime.substring(11,19);
int timeIndex = timeList.indexOf(currentTimeStr);
if (timeIndex!=timeList.size()-1){//当日还有场次
String[] strs = timeList.get(timeIndex+1).split("-");
nextStartTime = assembleDateTime(currentStartTime, strs[0]);
nextEndTime = assembleDateTime(currentStartTime, strs[1]);
}else{//往后推一天且未结束
LocalDateTime currentDateTime = LocalDateTime.parse(currentStartTime,formatter);
LocalDateTime dateTime = currentDateTime.plusDays(1);
if(!dateTime.isAfter(allEndTime)){
nowDateTimeStr = formatter.format(dateTime);
String[] strs = timeList.get(0).split("-");
nextStartTime = assembleDateTime(nowDateTimeStr, strs[0]);
nextEndTime = assembleDateTime(nowDateTimeStr, strs[1]);
}
}
time.setCurrentStartTime(currentStartTime);
time.setCurrentEndTime(currentEndTime);
time.setNextStartTime(nextStartTime);
time.setNextEndTime(nextEndTime);
return time;
}
活动周场次
- 计算当前场次
- 活动未开始时取活动开始时间起第一个满足指定周几的日期
+
第一个时间段拼接; - 活动开始后取系统时间起第一个满足指定周几的日期判断
- 如不是当天则取该满足日期
+
第一个时间段拼接; - 如是当天时需判断是否还有场次,有则取该满足日期
+
对应时间段拼接场次,无则取下一个满足日期+
第一个时间段拼接;
- 如不是当天则取该满足日期
- 活动未开始时取活动开始时间起第一个满足指定周几的日期
- 计算下一场次
- 当日还有场次则取当前场次的日期
+
下一时间段拼接; - 当日无场次则推迟一天找满足日期
+
第一个时间段拼接;
- 当日还有场次则取当前场次的日期
java
/**
* 计算周规则场次时间
*/
private ActivityTime computeWeekRuleTime(Activity activity, LocalDateTime nowDateTime){
String allStartTimeStr = activity.getAllStartTime();
String allEndTimeStr = activity.getAllEndTime();
LocalDateTime allStartTime = LocalDateTime.parse(allStartTimeStr, formatter);
LocalDateTime allEndTime = LocalDateTime.parse(allEndTimeStr, formatter);
List<String> timeList = Arrays.stream(activity.getActRuleTimes().split(",")).collect(Collectors.toList());
List<Integer> weekDayList = Arrays.stream(activity.getActRuleDays().split(",")).map(Integer::parseInt).collect(Collectors.toList());
ActivityTime time = new ActivityTime();
//当前场次开始时间
String currentStartTimeStr = null;
String currentEndTimeStr = null;
if(nowDateTime.isBefore(allStartTime)){//活动未开始
List<LocalDateTime> nextOrSameWeekDays = this.getNextOrSameWeekDays(allStartTime, allEndTime, weekDayList);
if(!nextOrSameWeekDays.isEmpty()){
String[] strs = timeList.get(0).split("-");
String dateStr = formatterDate.format(nextOrSameWeekDays.get(0));
currentStartTimeStr = assembleDateTime(dateStr, strs[0]);
currentEndTimeStr = assembleDateTime(dateStr, strs[1]);
}
}else{//活动已开始
List<LocalDateTime> nextOrSameWeekDays = this.getNextOrSameWeekDays(nowDateTime, allEndTime, weekDayList);
if(!nextOrSameWeekDays.isEmpty()){
String dateStr = formatterDate.format(nextOrSameWeekDays.get(0));
String nowDateStr = formatterDate.format(nowDateTime);
if(dateStr.equals(nowDateStr)){//第一场和当前日期相同时,判断当天是否还有场次
boolean isHave = false;
for (String timeStr : timeList){
String[] strs = timeStr.split("-");
LocalTime endTime = LocalTime.parse(strs[1], formatterTime);
if(nowDateTime.toLocalTime().isBefore(endTime)){//当天还有场次
currentStartTimeStr = assembleDateTime(dateStr, strs[0]);
currentEndTimeStr = assembleDateTime(dateStr, strs[1]);
isHave = true;
break;
}
}
if(!isHave && nextOrSameWeekDays.size()>1){//当天没有且有满足日期
LocalDateTime dateTime = nextOrSameWeekDays.get(1);
String theDateStr = formatterDate.format(dateTime);
String[] strs = timeList.get(0).split("-");
currentStartTimeStr = assembleDateTime(theDateStr, strs[0]);
currentEndTimeStr = assembleDateTime(theDateStr, strs[1]);
}
}else{
String[] strs = timeList.get(0).split("-");
currentStartTimeStr = assembleDateTime(dateStr, strs[0]);
currentEndTimeStr = assembleDateTime(dateStr, strs[1]);
}
}
}
if(currentStartTimeStr==null){
return time;
}
//计算下一场次
String nextStartTime = null;
String nextEndTime = null;
LocalDateTime currentStartTime = LocalDateTime.parse(currentStartTimeStr, formatter);
//当前场次的开始时分秒-结束时分秒
String currentTimeStr = currentStartTimeStr.substring(11,19)+"-"+currentEndTimeStr.substring(11,19);
int timeIndex = timeList.indexOf(currentTimeStr);
if (timeIndex!=timeList.size()-1){//当日还有场次
String[] strs = timeList.get(timeIndex+1).split("-");
nextStartTime = assembleDateTime(currentStartTimeStr, strs[0]);
nextEndTime = assembleDateTime(currentStartTimeStr, strs[1]);
}else{//往后推一天找满足日期
LocalDateTime theStartTime = currentStartTime.plusDays(1);
List<LocalDateTime> nextOrSameWeekDays = this.getNextOrSameWeekDays(theStartTime, allEndTime, weekDayList);
if(!nextOrSameWeekDays.isEmpty()){
LocalDateTime dateTime = nextOrSameWeekDays.get(0);
String dateStr = formatterDate.format(dateTime);
String[] strs = timeList.get(0).split("-");
nextStartTime = assembleDateTime(dateStr, strs[0]);
nextEndTime = assembleDateTime(dateStr, strs[1]);
}
}
time.setCurrentStartTime(currentStartTimeStr);
time.setCurrentEndTime(currentEndTimeStr);
time.setNextStartTime(nextStartTime);
time.setNextEndTime(nextEndTime);
return time;
}
活动月场次
- 计算当前场次
- 活动未开始时取活动开始时间起第一个满足指定几号的日期
+
第一个时间段拼接; - 活动开始后取系统时间起第一个满足指定几号的日期判断
- 如不是当天则取该满足日期
+
第一个时间段拼接; - 如是当天时需判断是否还有场次,有则取该满足日期
+
对应时间段拼接场次,无则取下一个满足日期+
第一个时间段拼接;
- 如不是当天则取该满足日期
- 活动未开始时取活动开始时间起第一个满足指定几号的日期
- 计算下一场次
- 当日还有场次则取当前场次的日期
+
下一时间段拼接; - 当日无场次则推迟一天找满足指定几号的日期
+
第一个时间段拼接;
- 当日还有场次则取当前场次的日期
java
/**
* 计算月规则场次时间
*/
private ActivityTime computeMonthRuleTime(Activity activity, LocalDateTime nowDateTime){
LocalDateTime allStartTime = LocalDateTime.parse(activity.getAllStartTime(), formatter);
LocalDateTime allEndTime = LocalDateTime.parse(activity.getAllEndTime(), formatter);
List<String> timeList = Arrays.stream(activity.getActRuleTimes().split(",")).collect(Collectors.toList());
List<Integer> monthDayList = Arrays.stream(activity.getActRuleDays().split(",")).map(Integer::parseInt).collect(Collectors.toList());
ActivityTime time = new ActivityTime();
//当前场次开始时间
String currentStartTimeStr = null;
String currentEndTimeStr = null;
if(nowDateTime.isBefore(allStartTime)){//活动未开始
LocalDate theDate = this.getNextOrSameMonthDay(allStartTime, allEndTime, monthDayList);
if(theDate!=null){
String[] strs = timeList.get(0).split("-");
currentStartTimeStr = assembleDateTime(theDate.toString(), strs[0]);
currentEndTimeStr = assembleDateTime(theDate.toString(), strs[1]);
}
}else{//活动已开始
LocalDate theDate = this.getNextOrSameMonthDay(nowDateTime, allEndTime, monthDayList);
if(theDate!=null){
String dateStr = theDate.toString();
String nowDateStr = formatterDate.format(nowDateTime);
if(dateStr.equals(nowDateStr)){//第一场和当前日期相同时,判断当天是否还有场次
boolean isHave = false;
for (String timeStr : timeList){
String[] strs = timeStr.split("-");
LocalTime endTime = LocalTime.parse(strs[1], formatterTime);
if(nowDateTime.toLocalTime().isBefore(endTime)){//当天还有场次
currentStartTimeStr = assembleDateTime(dateStr, strs[0]);
currentEndTimeStr = assembleDateTime(dateStr, strs[1]);
isHave = true;
break;
}
}
LocalDate theNextDate = this.getNextOrSameMonthDay(nowDateTime.plusDays(1), allEndTime, monthDayList);
if(!isHave && theNextDate!=null){//当天没有且有满足日期
String[] strs = timeList.get(0).split("-");
currentStartTimeStr = assembleDateTime(theNextDate.toString(), strs[0]);
currentEndTimeStr = assembleDateTime(theNextDate.toString(), strs[1]);
}
}else{
String[] strs = timeList.get(0).split("-");
currentStartTimeStr = assembleDateTime(dateStr, strs[0]);
currentEndTimeStr = assembleDateTime(dateStr, strs[1]);
}
}
}
if(currentStartTimeStr==null){
return time;
}
//计算下一场次
String nextStartTime = null;
String nextEndTime = null;
LocalDateTime currentStartTime = LocalDateTime.parse(currentStartTimeStr, formatter);
//当前场次的开始时分秒-结束时分秒
String currentTimeStr = currentStartTimeStr.substring(11,19)+"-"+currentEndTimeStr.substring(11,19);
int timeIndex = timeList.indexOf(currentTimeStr);
if (timeIndex!=timeList.size()-1){//当日还有场次
String[] strs = timeList.get(timeIndex+1).split("-");
nextStartTime = assembleDateTime(currentStartTimeStr, strs[0]);
nextEndTime = assembleDateTime(currentStartTimeStr, strs[1]);
}else{//往后推一天找满足日期
LocalDate theNextDate = this.getNextOrSameMonthDay(currentStartTime.plusDays(1), allEndTime, monthDayList);
if(theNextDate!=null){
String[] strs = timeList.get(0).split("-");
nextStartTime = assembleDateTime(theNextDate.toString(), strs[0]);
nextEndTime = assembleDateTime(theNextDate.toString(), strs[1]);
}
}
time.setCurrentStartTime(currentStartTimeStr);
time.setCurrentEndTime(currentEndTimeStr);
time.setNextStartTime(nextStartTime);
time.setNextEndTime(nextEndTime);
return time;
}
额外类方法
java
/**
* 活动规则类型枚举 NONE单场次;DAY日场次;WEEK周场次;MONTH月场次
*/
public enum ActivityRuleType {
NONE,DAY,WEEK,MONTH
}
java
/**
* 计算活动场次时间
* @param activity
* @return
*/
public ActivityTime computeTime(Activity activity){
LocalDateTime nowDateTime = LocalDateTime.now();
LocalDateTime allEndTime = LocalDateTime.parse(activity.getAllEndTime(), formatter);
if(allEndTime.isBefore(nowDateTime)){//已结束
return null;
}
//判断活动规则
String actRuleType = activity.getActRuleType();
if(ActivityRuleType.NONE.name().equals(actRuleType)){
return new ActivityTime(activity.getAllStartTime(), activity.getAllEndTime(), null, null);
}else if(ActivityRuleType.DAY.name().equals(actRuleType)) {
return this.computeDayRuleTime(activity, nowDateTime);
}else if(ActivityRuleType.WEEK.name().equals(actRuleType)) {
return this.computeWeekRuleTime(activity, nowDateTime);
}else if(ActivityRuleType.MONTH.name().equals(actRuleType)) {
return this.computeMonthRuleTime(activity, nowDateTime);
}else{
return null;
}
}
java
/**
* 组装时间 格式yyyy-MM-dd HH:mm:ss
* @param dateTimeStr yyyy-MM-dd HH:mm:ss
* @param timeStr HH:mm:ss
* @return
*/
private String assembleDateTime(String dateTimeStr,String timeStr){
return String.format("%s %s",dateTimeStr.substring(0,10),timeStr);
}
java
/**
* 根据开始时间获取在范围内的下一个周几list
* @param startTime
* @param endTime
* @param weekDayList
* @return
*/
private List<LocalDateTime> getNextOrSameWeekDays(LocalDateTime startTime,LocalDateTime endTime,List<Integer> weekDayList){
List<LocalDateTime> dayList = new ArrayList<>();
if(weekDayList.size()==1){//只有周几一个时获取两次
LocalDateTime dateTime = startTime.with(TemporalAdjusters.nextOrSame(DayOfWeek.of(weekDayList.get(0))));
if(dateTime.isBefore(endTime)){//未结束
dayList.add(dateTime);
}
LocalDateTime dateTime1 = dateTime.plusDays(7);
if(dateTime1.isBefore(endTime)){//未结束
dayList.add(dateTime1);
}
}else{
for (Integer weekDay : weekDayList) {
LocalDateTime dateTime = startTime.with(TemporalAdjusters.nextOrSame(DayOfWeek.of(weekDay)));
if(dateTime.isBefore(endTime)){//未结束
dayList.add(dateTime);
}
}
}
//排序确保最近日期在前面
Collections.sort(dayList);
return dayList;
}
java
/**
* 根据开始时间获取在月规则范围内的下一个日期
* @param startTime
* @param endTime
* @param monthDayList
* @return
*/
private LocalDate getNextOrSameMonthDay(LocalDateTime startTime,LocalDateTime endTime,List<Integer> monthDayList){
LocalDate localDate = startTime.toLocalDate();
int dayOfMonth = localDate.getDayOfMonth();
int currentMax = localDate.lengthOfMonth();
//当前月是否还有日期
Optional<Integer> currentMonthDay = monthDayList.stream().filter(t -> t >= dayOfMonth).findFirst();
LocalDate returnDate;
if(currentMonthDay.isPresent()){
//如果配置29,30,31号 取不到,则取当月最后一天
returnDate = localDate.withDayOfMonth(currentMonthDay.get()<=currentMax?currentMonthDay.get():currentMax);
}else{//取下个月的第一天
LocalDate theDate = localDate.plusMonths(1);
int theMax = theDate.lengthOfMonth();
returnDate = theDate.withDayOfMonth(monthDayList.get(0)<=theMax?monthDayList.get(0):theMax);
}
return returnDate.isAfter(endTime.toLocalDate())?null:returnDate;
}
java
/**
* 校验时分秒按场次顺序配置
* @param actRuleTimes
* @return
*/
private boolean checkActRuleTimes(String actRuleTimes){
boolean flag = true;
if(Objects.isNull(actRuleTimes) || actRuleTimes.isEmpty()) return flag;
List<String> timeList = Arrays.stream(actRuleTimes.split(",")).collect(Collectors.toList());
for (int i = 0; i < timeList.size(); i++) {
String[] timeStrs = timeList.get(i).split("-");
String startTime = timeStrs[0];
String endTime = timeStrs[1];
if(endTime.compareTo(startTime)<=0){//结束时间小于等于开始时间
flag = false;
break;
}
if(i>0){
String[] preTimeStrs = timeList.get(i-1).split("-");
String preEndTime = preTimeStrs[1];
if(startTime.compareTo(preEndTime)<=0){//下一场开始时间小于等于前一场结束时间
flag = false;
break;
}
}
}
return flag;
}
运行测试
依赖包均为jdk8
以上自带,main
方法如下:
java
public class ActivityTimeCompute {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter formatterTime = DateTimeFormatter.ofPattern("HH:mm:ss");
private static final DateTimeFormatter formatterDate = DateTimeFormatter.ofPattern("yyyy-MM-dd");
public static void main(String[] args) {
ActivityTimeCompute test = new ActivityTimeCompute();
//校验活动规则时分秒配置
boolean b = test.checkActRuleTimes("10:00:00-11:30:00,11:00:00-19:30:00");
if(!b){
System.err.println("校验活动规则时分秒配置不满足规范");
}
//单场次活动
Activity activity1 = new Activity("2024-12-01 00:00:00","2024-12-31 23:00:00",
ActivityRuleType.NONE.name(),null,null);
ActivityTime activityTime1 = test.computeTime(activity1);
System.err.println(activityTime1==null?null:activityTime1.toString());
//日场次活动
Activity activity2 = new Activity("2024-12-01 00:00:00","2024-12-31 23:00:00",
ActivityRuleType.DAY.name(),null,"10:00:00-11:30:00,18:00:00-19:30:00");
ActivityTime activityTime2 = test.computeTime(activity2);
System.err.println(activityTime2==null?null:activityTime2.toString());
//周场次活动
Activity activity3 = new Activity("2024-12-01 00:00:00","2024-12-31 23:00:00",
ActivityRuleType.WEEK.name(),"6,7","10:00:00-11:30:00,18:00:00-19:30:00");
ActivityTime activityTime3 = test.computeTime(activity3);
System.err.println(activityTime3==null?null:activityTime3.toString());
//月场次活动
Activity activity4 = new Activity("2025-01-01 00:00:00","2025-07-31 23:00:00",
ActivityRuleType.MONTH.name(),"30","10:00:00-11:30:00");
ActivityTime activityTime4 = test.computeTime(activity4);
System.err.println(activityTime4==null?null:activityTime4.toString());
}
}
运行结果:
java
校验活动规则时分秒配置不满足规范
ActivityTime{currentStartTime='2024-12-01 00:00:00', currentEndTime='2024-12-31 23:00:00', nextStartTime='null', nextEndTime='null'}
ActivityTime{currentStartTime='2024-12-19 18:00:00', currentEndTime='2024-12-19 19:30:00', nextStartTime='2024-12-20 10:00:00', nextEndTime='2024-12-20 11:30:00'}
ActivityTime{currentStartTime='2024-12-21 10:00:00', currentEndTime='2024-12-21 11:30:00', nextStartTime='2024-12-21 18:00:00', nextEndTime='2024-12-21 19:30:00'}
ActivityTime{currentStartTime='2025-01-30 10:00:00', currentEndTime='2025-01-30 11:30:00', nextStartTime='2025-02-28 10:00:00', nextEndTime='2025-02-28 11:30:00'}
总结
总体核心方法为computeTime
,然后根据单
场次、指定日/周/月
多场次规则类型分别计算对应活动当前场次和下一场次周期,由此满足各类营销活动场景的时间场次计算。
工作项目中实际使用时需联合用户端和管理端一起使用,关键点如下:
- 管理端 在
营销活动审核通过时
/定时任务扫描当前场次结束且有下一场次活动时
,触发活动场次时间计算方法,获取最新当前场次区间和下一场次区间,并放入redis/本地缓存
供用户端使用; - 用户端 获取对应活动场次时间做如下判断
- 当前场次
未开始
时活动状态未开始,可展示具体时间段/周几/日期; - 当前场次
已开始
时活动状态进行中,展示对应按钮/活动参与方式; - 当前场次
已结束
时需判断是否有下一场次,如无则活动状态已结束,如有则依据1,2步骤判断展示;
- 当前场次
JYM
工作中营销活动场次是怎么计算的呢,欢迎评论交流!
创作不易,求赞+收藏,关注不迷路!!!