提交 0017e0f4 authored 作者: 吕本才's avatar 吕本才

feat(activity): 新增活动打卡任务状态枚举及随机任务处理逻辑

- 新增活动打卡任务状态枚举 `ActivityClockTaskStatus`,包含待开始、进行中、未完成、已完成四种状态 - 在 `ActivityPhotoType` 枚举中新增 POS照片 和 随机任务照片 类型- 实现 XXL-JOB 任务处理器 `ActivityStautsHandler`,用于发送微信订阅消息和WebSocket通知 - 添加 `AuthUtils` 工具类,用于处理 JWT token 的解析与用户身份获取 - 在 `ClockType` 枚举中增加随机打卡和POS打卡类型及其时间范围 - 增加 `DateUtils.parseDateByLocalTime` 方法,支持将 LocalTime 转换为 Date 对象-为 `EmployeeCoreTemporaryInfoService` 接口及其实现类添加 `selectById` 方法- 引入 WebSocket 相关配置与拦截器,支持基于 JWT 的握手验证 - 更新 pom.xml 文件,启用 spring-boot-starter-web 并引入 websocket 依赖 - 新增 `RestTemplateConfig` 配置类,用于初始化 RestTemplate Bean- 新增 `ServletUtils` 工具类,封装常用的 Servlet 操作方法 - 实现微信订阅消息服务 `SubscribeMessageService`,支持向用户推送打卡提醒- 修改打卡控制器中的校验逻辑,优化参数判断条件- 在打卡核心服务中集成随机任务和 POS 上传任务的生成逻辑 - 扩展临时活动照片 DAO 层接口,支持任务照片的保存与查询操作
上级 1b120964
......@@ -90,10 +90,10 @@
</dependency>
<!-- Web 场景启动器) 来为 Web 开发予以支持。spring-boot-starter-web 为我们提供了嵌入的 Servlet 容器以及 SpringMVC 的依赖,并为 Spring MVC 提供了大量自动配置,可以适用于大多数 Web 开发场景。-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-web</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
......@@ -222,17 +222,18 @@
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<!-- <version>${spring-cloud-gateway.version}</version>-->
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-web</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-gateway</artifactId>-->
<!--&lt;!&ndash; <version>${spring-cloud-gateway.version}</version>&ndash;&gt;-->
<!-- <exclusions>-->
<!-- <exclusion>-->
<!-- <artifactId>spring-boot-starter-web</artifactId>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- </exclusion>-->
<!-- </exclusions>-->
<!-- </dependency>-->
<dependency>
<groupId>com.fasterxml.uuid</groupId>
......@@ -252,6 +253,29 @@
<version>${aliyun.sts.version}</version>
</dependency>
<!-- pom.xml 中需包含以下依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-webflux</artifactId>-->
<!-- <exclusions>-->
<!-- <exclusion>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-tomcat</artifactId>-->
<!-- </exclusion>-->
<!-- </exclusions>-->
<!-- </dependency>-->
<!-- Maven依赖示例 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.4</version> <!-- 与SpringBoot版本兼容的版本 -->
</dependency>
</dependencies>
<build>
......
......@@ -3,10 +3,9 @@ package com.wangxiaolu.promotion;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.web.context.request.RequestContextListener;
@EnableAsync
@EnableConfigurationProperties
......
package com.wangxiaolu.promotion.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
package com.wangxiaolu.promotion.config;
import com.wangxiaolu.promotion.websocket.JwtHandshakeInterceptor;
import com.wangxiaolu.promotion.websocket.TemporaryActivityTaskClockSocketHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.HandshakeInterceptor;
/**
* WebSocket配置类
* @author : lvbencai
* @date : 2024/4/16
*/
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry
// 注册WebSocket处理器和路径
.addHandler(webSocketHandler(), "/ws/temporary/plan")
// 允许跨域(根据实际前端域名配置)setAllowedOriginPatterns
.setAllowedOrigins("*")
// 添加握手拦截器(用于JWT验证)
.addInterceptors(handshakeInterceptor());
}
@Bean
public WebSocketHandler webSocketHandler() {
return new TemporaryActivityTaskClockSocketHandler();
}
@Bean
public HandshakeInterceptor handshakeInterceptor() {
return new JwtHandshakeInterceptor();
}
}
......@@ -82,7 +82,7 @@ public class TemporaryActivityClockCoreController {
Integer clockType = clockVo.getClockType();
boolean isClockIn = ClockType.TEMPORARY_CLOCK_IN.equals(clockType);
// 上班卡必需有促销计划ID
if (isClockIn && (Objects.isNull(clockVo.getPlanId()) || clockVo.getPlanId() <= 0)) {
if (isClockIn && Objects.isNull(clockVo.getPlanId())) {
throw new ParamException(RCode.NOT_CLOCK_STORE_ERROR, null);
}
// 非上班卡必需有打卡记录ID
......@@ -114,25 +114,26 @@ public class TemporaryActivityClockCoreController {
/**
* 打卡照片更换
*
* @param clockVo 更换照片信息
* @return 是否成功
*/
@PutMapping("/today/clock/update")
public R updateClockPhoto(@RequestBody TemporaryClockVo clockVo) {
if (Objects.isNull(clockVo.getTemporaryId()) ||clockVo.getTemporaryId()<=0){
if (Objects.isNull(clockVo.getTemporaryId()) || clockVo.getTemporaryId() <= 0) {
throw new ParamException(RCode.LOGIN_PARAM_ERROR, null);
}
if (StringUtils.isBlank(clockVo.getClockPhoto())){
if (StringUtils.isBlank(clockVo.getClockPhoto())) {
throw new ParamException(RCode.NOT_CLOCK_PHOTO_ERROR, null);
}
long minuteBetween = DateUtil.between(clockVo.getLastClockTime(), new Date(), DateUnit.MINUTE);
if (minuteBetween > 30){
if (minuteBetween > 30) {
throw new ParamException(RCode.UPDATE_CLOCK_PHOTO_TIME_LONG_ERROR, null);
}
TemporaryClockDto dto = new TemporaryClockDto(clockVo.getId(),clockVo.getTemporaryId(),clockVo.getClockPhoto(),clockVo.getPhotoType());
TemporaryClockDto dto = new TemporaryClockDto(clockVo.getId(), clockVo.getTemporaryId(), clockVo.getClockPhoto(), clockVo.getPhotoType());
tempActivityClockCoreService.updateClockPhoto(dto);
return R.success();
}
......
package com.wangxiaolu.promotion.controller.activity.temporary;
import com.wangxiaolu.promotion.pojo.activity.temporary.vo.TemporaryActivityTaskClockReq;
import com.wangxiaolu.promotion.result.basedata.R;
import com.wangxiaolu.promotion.domain.activity.mapper.entity.TemporaryActivityTaskClockDO;
import com.wangxiaolu.promotion.service.activity.temporary.TemporaryActivityTaskClockService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* 临时活动相关任务打卡核心控制层
* 包含 随机任务和pos打卡任务
*/
@RestController
@RequestMapping("/activity/task-clock/core")
public class TemporaryActivityTaskClockCoreController {
@Resource
private TemporaryActivityTaskClockService service;
/**
* 打卡更新打卡状态
* @param clockVo
* @return
*/
@PostMapping("/clock")
public R clock(@RequestBody TemporaryActivityTaskClockReq clockVo) {
service.clock(clockVo);
return R.success();
}
@PutMapping("/update")
public R update(@RequestBody TemporaryActivityTaskClockDO taskClock) {
return R.success(service.updateById(taskClock));
}
@DeleteMapping("/delete/{id}")
public R delete(@PathVariable Long id) {
TemporaryActivityTaskClockDO taskClock = TemporaryActivityTaskClockDO.builder()
.id(id).isDelete(0).build();
return R.success(service.updateById(taskClock));
}
}
package com.wangxiaolu.promotion.controller.activity.temporary;
import com.wangxiaolu.promotion.domain.activity.mapper.entity.TemporaryActivityTaskClockDO;
import com.wangxiaolu.promotion.pojo.activity.temporary.res.TemporaryActivityTaskClockRes;
import com.wangxiaolu.promotion.service.activity.temporary.TemporaryActivityTaskClockService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* @author lvbencai
* @date 2025年11月11日16:45:27
* @description: 临时活动任务打卡查询控制层
*/
@RestController
@RequestMapping("/activity/task-clock/query")
public class TemporaryActivityTaskClockQueryController {
@Resource
private TemporaryActivityTaskClockService service;
@GetMapping("/byId/{id}")
public TemporaryActivityTaskClockDO getById(@PathVariable Long id) {
return service.getById(id);
}
/**
* 查询任务打卡列表
*
* @return
*/
@GetMapping("/list")
public List<TemporaryActivityTaskClockDO> listByTemporaryId() {
return service.listByUserId();
}
/**
* 按照任务类型查询任务信息
*
* @return
*/
@GetMapping("/my/{taskType}")
public TemporaryActivityTaskClockRes queryByMyTaskType(@PathVariable Integer taskType) {
return service.queryByMyTaskType(taskType);
}
}
......@@ -40,4 +40,8 @@ public interface TemporaryActivityPhotoDao {
Map<Integer, List<TemporaryActivityPhotoDto>> findReportedInfoGroup(Integer temporaryId, Long reportedId);
void deleteList(Long reportedId, int type);
void saveClockTaskPhoto(TemporaryActivityPhotoDto dto, List<String> clockPhtos);
List<TemporaryActivityPhotoDto> selectPhotos(TemporaryPhotoWrapper clockId);
}
package com.wangxiaolu.promotion.domain.activity.dao;
import com.wangxiaolu.promotion.domain.activity.mapper.entity.TemporaryActivityTaskClockDO;
import com.wangxiaolu.promotion.domain.activity.wrapperQo.TemporaryActivityTaskWrapperDto;
import java.util.List;
public interface TemporaryActivityTaskClockDao {
List<TemporaryActivityTaskClockDO> selectList(TemporaryActivityTaskWrapperDto wrapper);
void save(TemporaryActivityTaskClockDO taskClockDO);
TemporaryActivityTaskClockDO selectById(Long id);
void update(TemporaryActivityTaskClockDO taskClockDO);
TemporaryActivityTaskClockDO selectOne(TemporaryActivityTaskWrapperDto wrapper);
}
package com.wangxiaolu.promotion.domain.activity.dao.impl;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.wangxiaolu.promotion.common.enums.StatusType;
import com.wangxiaolu.promotion.domain.activity.dao.TemporaryActivityPhotoDao;
......@@ -134,6 +135,25 @@ public class TemporaryActivityPhotoDaoImpl implements TemporaryActivityPhotoDao
temporaryActivityPhotoMapper.deletebyReportedId(reportedId,type);
}
@Override
public void saveClockTaskPhoto(TemporaryActivityPhotoDto dto, List<String> clockPhtos) {
// 新增数据
saveReportedList(dto.getTemporaryId(), dto.getReportedId(), dto.getType(), clockPhtos);
}
@Override
public List<TemporaryActivityPhotoDto> selectPhotos(TemporaryPhotoWrapper wrapperDto) {
// 根据关联的上班打卡任务,查询图片信息
LambdaQueryWrapper<TemporaryActivityPhotoDO> wrapper = new LambdaQueryWrapper<TemporaryActivityPhotoDO>()
.eq(ObjectUtil.isNotEmpty(wrapperDto.getClockId()), TemporaryActivityPhotoDO::getClockId, wrapperDto.getClockId())
.eq(ObjectUtil.isNotEmpty(wrapperDto.getReportedId()), TemporaryActivityPhotoDO::getReportedId, wrapperDto.getReportedId())
.eq(ObjectUtil.isNotEmpty(wrapperDto.getTemporaryId()), TemporaryActivityPhotoDO::getTemporaryId, wrapperDto.getTemporaryId())
.eq(TemporaryActivityPhotoDO::getIsDelete, StatusType.VALID.getType());
List<TemporaryActivityPhotoDO> dos = temporaryActivityPhotoMapper.selectList(wrapper);
List<TemporaryActivityPhotoDto> photoDtos = transitionDtos(dos);
return photoDtos;
}
/**
* 活动上报图片查询
......
package com.wangxiaolu.promotion.domain.activity.dao.impl;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.wangxiaolu.promotion.domain.activity.dao.TemporaryActivityTaskClockDao;
import com.wangxiaolu.promotion.domain.activity.mapper.TemporaryActivityTaskClockMapper;
import com.wangxiaolu.promotion.domain.activity.mapper.entity.TemporaryActivityTaskClockDO;
import com.wangxiaolu.promotion.domain.activity.wrapperQo.TemporaryActivityTaskWrapperDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author : liqiulin
* @date : 2024-04-23 17
* @describe :
*/
@Service
@Slf4j
public class TemporaryActivityTaskClockDaoImpl implements TemporaryActivityTaskClockDao {
@Autowired
TemporaryActivityTaskClockMapper temporaryActivityTaskClockMapper;
@Override
public List<TemporaryActivityTaskClockDO> selectList(TemporaryActivityTaskWrapperDto wrapper) {
List<TemporaryActivityTaskClockDO> temporaryActivityTaskClockDOS = temporaryActivityTaskClockMapper.selectList(new LambdaQueryWrapper<TemporaryActivityTaskClockDO>()
.eq(ObjectUtil.isNotEmpty(wrapper.getTemporaryId()), TemporaryActivityTaskClockDO::getTemporaryId, wrapper.getTemporaryId())
.eq(ObjectUtil.isNotEmpty(wrapper.getUserId()), TemporaryActivityTaskClockDO::getTemporaryId, wrapper.getUserId())
.eq(ObjectUtil.isNotEmpty(wrapper.getIsDelete()), TemporaryActivityTaskClockDO::getIsDelete, wrapper.getIsDelete())
.orderByAsc(TemporaryActivityTaskClockDO::getTaskType));
return temporaryActivityTaskClockDOS;
}
@Override
public void save(TemporaryActivityTaskClockDO taskClockDO) {
temporaryActivityTaskClockMapper.insert(taskClockDO);
}
@Override
public TemporaryActivityTaskClockDO selectById(Long id) {
return temporaryActivityTaskClockMapper.selectById(id);
}
@Override
public void update(TemporaryActivityTaskClockDO taskClockDO) {
temporaryActivityTaskClockMapper.updateById(taskClockDO);
}
@Override
public TemporaryActivityTaskClockDO selectOne(TemporaryActivityTaskWrapperDto wrapper) {
TemporaryActivityTaskClockDO temporaryActivityTaskClockDO = temporaryActivityTaskClockMapper.selectOne(new LambdaQueryWrapper<TemporaryActivityTaskClockDO>()
.eq(TemporaryActivityTaskClockDO::getTemporaryId, wrapper.getTemporaryId())
.eq(TemporaryActivityTaskClockDO::getTaskType, wrapper.getTaskType())
.eq(TemporaryActivityTaskClockDO::getIsDelete, wrapper.getIsDelete()));
return temporaryActivityTaskClockDO;
}
}
// 文件路径:promotion-service/src/main/java/com/wangxiaolu/promotion/domain/activity/mapper/TemporaryActivityTaskClockMapper.java
package com.wangxiaolu.promotion.domain.activity.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.wangxiaolu.promotion.domain.activity.mapper.entity.TemporaryActivityTaskClockDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TemporaryActivityTaskClockMapper extends BaseMapper<TemporaryActivityTaskClockDO> {
}
......@@ -11,7 +11,7 @@ import lombok.Data;
import lombok.experimental.Accessors;
/**
*
*
* @TableName temporary_activity_photo
*/
@TableName(value ="temporary_activity_photo")
......@@ -40,7 +40,9 @@ public class TemporaryActivityPhotoDO implements Serializable {
private Long clockId;
/**
* 图片所属类别:1:推广试吃;2……
* 图片所属类别:1: 推广试吃照片 2推广互动照片 3:推广成交照片4:上班打卡图片 5: 午休下班打卡图片
* 6:午休上班打卡图片 7: 下班打卡图片 8: 当日销量POS机页面凭证 9: 随机任务 10: POS照片
*
*/
private Integer type;
......@@ -69,4 +71,4 @@ public class TemporaryActivityPhotoDO implements Serializable {
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
\ No newline at end of file
}
package com.wangxiaolu.promotion.domain.activity.mapper.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* @TableName temporary_activity_clock
*/
@TableName(value = "temporary_activity_task_clock")
@Data
@Builder
public class TemporaryActivityTaskClockDO implements Serializable {
@TableField(exist = false)
private static final long serialVersionUID = 1L;
/**
* 主键id
*/
@TableId(type = IdType.AUTO)
private Long id;
private Integer temporaryId;
/**
* 活动打卡id
*/
private Long clockId;
/**
* temporary_info表name
*/
private String temporaryName;
/**
* 关联活动上报id
*/
private Long reportedId;
/**
* 任务类型 和图片的工业
* 10、随机任务 9、pos上传任务
*/
private Integer taskType;
/**
* 任务状态 1、进行中 2、已完成 3、未完成
*/
private Integer taskStatus;
/**
* activity_plan_info表id
*/
private Long planId;
/**
* 打卡时间
*/
private Date clockTime;
/**
* 要求打卡时间
*/
private Date requiredlockTime;
/**
* 创建日期YYYY-MM-DD
*/
private String createDate;
/**
* 创建时间
*/
private Date createTime;
/**
* 修改时间
*/
private Date modifyTime;
// 活动模式Id
private Integer activityPatternId;
// 活动模式
private String activityPattern;
/**
* 是否删除
* 1:有效;0:删除;
*/
private Integer isDelete;
/**
* 订阅状态 0:未订阅;1:已订阅
*/
private Integer subscribeStatus;
// 已发送订阅消息
private Integer isSendSubscribe;
/**
* 订阅消息发送时间
*/
private Date subscribeTime;
}
package com.wangxiaolu.promotion.domain.activity.wrapperQo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
/**
* @author : liqiulin
* @date : 2024-04-23 19
* @describe :
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class TemporaryActivityTaskWrapperDto {
/**
* temporary_info表id
*/
private Integer temporaryId;
/**
* 任务类型
*/
private Integer taskType;
/**
* 用户id
*/
private Long userId;
/**
* 是否删除
* 0:删除;1:可用
*/
private Integer isDelete;
}
......@@ -12,7 +12,6 @@ import org.springframework.stereotype.Repository;
* @Entity com.wangxiaolu.promotion.domain.examine.mapper.entity.ActivityExamineDO
*/
@Mapper
@Repository
public interface ActivityExamineMapper extends BaseMapper<ActivityExamineDO> {
ActivityExamineDO selectByPlanId(Long planId);
......
package com.wangxiaolu.promotion.enums.activity;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 活动打卡任务状态
*/
@Getter
@AllArgsConstructor
public enum ActivityClockTaskStatus {
/**
* 待开始
*/
TO_BE_START(0, "待开始"),
/**
* 进行中
*/
STARTING(2, "进行中"),
/**
* 未完成
*/
UNCOMPLETED(3, "未完成"),
/**
* 已完成
*/
COMPLETED(1, "已完成"),
;
private int type;
private String typeName;
}
......@@ -43,6 +43,16 @@ public enum ActivityPhotoType {
* 当日销量POS机页面凭证
*/
POS_SELL_VOUCHER(7),
/**
* POS照片
*/
POS_PHOTO(9),
/**
* 随机任务照片
*/
RANDOM_TASK(10)
;
private int type;
......
......@@ -25,6 +25,20 @@ public interface ClockType {
Integer TEMPORARY_CLOCK_OUT = 4;
String TEMPORARY_CLOCK_OUT_BEGIN_TIME = "19:00:00";
String TEMPORARY_CLOCK_OUT_END_TIME = "23:59:00";
/**
* 随机打卡
*/
Integer TEMPORARY_RAND_CLOCK = 8;
String TEMPORARY_RAND_CLOCK_BEGIN_TIME = "17:30:00";
String TEMPORARY_RAND_CLOCK_END_TIME = "19:30:00";
/**
* 随机打卡
* 11月的时候 先提示当天不拍pos数据 场次费用扣减10元
* 然后不挂接下班卡 先试行看看执行效果 大部分能拍的话 下个月就强制
*/
Integer POS_CLOCK = 9;
String POS_BEGIN_TIME = "17:30:00";
String POS_END_TIME = "19:30:00";
}
package com.wangxiaolu.promotion.pojo.activity.temporary.res;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.List;
/**
*
* @TableName temporary_activity_clock
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TemporaryActivityTaskClockRes {
/**
* 主键id
*/
private Long id;
/**
* 活动打卡id
*/
private Long clockId;
private Integer temporaryId;
/**
* temporary_info表name
*/
private String temporaryName;
/**
* 关联活动上报id
*/
private Long reportedId;
/**
* 任务类型 和图片的工业
* 10、随机任务 9、pos上传任务
*/
private Integer taskType;
/**
* 任务状态
* 1、待开始 2、进行中 3、未完成 4、已完成
*/
private Integer taskStatus;
/**
* activity_plan_info表id
*/
private Long planId;
/**
* 打卡时间
*/
private Date clockTime;
private List<String> clockPhtos;
}
package com.wangxiaolu.promotion.pojo.activity.temporary.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Builder;
import lombok.Data;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
/**
*
* @TableName temporary_activity_clock
*/
@Data
@Builder
public class TemporaryActivityTaskClockReq {
/**
* 主键id
*/
private Long id;
/**
* 活动打卡id
*/
@NotNull(message = "活动打卡id不能为空")
private Long clockId;
@NotNull(message = "促销员id不能为空")
private Integer temporaryId;
/**
* temporary_info表name
*/
@NotNull(message = "促销员名称不能为空")
private String temporaryName;
/**
* 关联活动上报id
*/
private Long reportedId;
/**
* 任务类型 和图片的工业
* 10、随机任务 9、pos上传任务
*/
@Min(value = 9, message = "任务类型只能是10、9")
@Max(value = 10, message = "任务类型只能是10、9")
private Integer taskType;
/**
* 任务状态
* 1、待开始 2、进行中 3、未完成 4、已完成
*/
@Size(min = 1, max = 4, message = "任务状态只能是1、2、3、4")
private Integer taskStatus;
/**
* activity_plan_info表id
*/
private Long planId;
private List<String> clockPhotos;
}
......@@ -9,4 +9,6 @@ import com.wangxiaolu.promotion.pojo.user.dto.WxTemporaryInfoDto;
*/
public interface EmployeeCoreTemporaryInfoService {
void updateTemporaryInfoById(WxTemporaryInfoDto temporaryDto);
WxTemporaryInfoDto selectById(Integer temporaryId);
}
......@@ -20,4 +20,11 @@ public class EmployeeCoreTemporaryInfoServiceImpl implements EmployeeCoreTempora
public void updateTemporaryInfoById(WxTemporaryInfoDto temporaryDto) {
temporaryInfoDao.updateById(temporaryDto);
}
@Override
public WxTemporaryInfoDto selectById(Integer temporaryId) {
WxTemporaryInfoDto wxTemporaryInfoDto = temporaryInfoDao.selectOneById(temporaryId);
return wxTemporaryInfoDto;
}
}
package com.wangxiaolu.promotion.service.activity.temporary;
import com.baomidou.mybatisplus.extension.service.IService;
import com.wangxiaolu.promotion.domain.activity.mapper.entity.TemporaryActivityTaskClockDO;
import com.wangxiaolu.promotion.pojo.activity.temporary.res.TemporaryActivityTaskClockRes;
import com.wangxiaolu.promotion.pojo.activity.temporary.dto.TemporaryClockDto;
import com.wangxiaolu.promotion.pojo.activity.temporary.vo.TemporaryActivityTaskClockReq;
import java.util.List;
public interface TemporaryActivityTaskClockService extends IService<TemporaryActivityTaskClockDO> {
List<TemporaryActivityTaskClockDO> listByTemporaryId(Integer temporaryId);
void generateRandomClockTask(TemporaryClockDto dto);
void generatePosUploadTask(TemporaryClockDto dto);
List<TemporaryActivityTaskClockDO> listByUserId();
void clock(TemporaryActivityTaskClockReq clockVo);
TemporaryActivityTaskClockRes queryByMyTaskType(Integer taskType);
}
package com.wangxiaolu.promotion.service.activity.temporary.impl;
import cn.hutool.json.JSONObject;
import com.wangxiaolu.promotion.utils.WechatAccessTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
/**
* 微信订阅消息服务
*/
@Service
@Slf4j
public class SubscribeMessageService {
// @Value("${wx.miniapp.subscribe-message-url}")
// private String subscribeUrl;
@Autowired
private RestTemplate restTemplate;
@Autowired
private WechatAccessTokenUtil tokenUtil;
/**
* 发送订阅通知
* @param openid 用户openid
* @param templateId 订阅模板ID
* @param page 点击通知跳转的小程序页面(如 "/pages/order/detail?id=123")
* @param data 模板数据(键为模板中的字段名,值为具体内容)
* @return 发送结果
*/
public boolean sendSubscribeMessage(String openid, String templateId, String page, Map<String, String> data) {
try {
// 构建请求参数
Map<String, Object> requestBody = new HashMap<>();
// 用户openid
requestBody.put("touser", openid);
// 模板ID
requestBody.put("template_id", templateId);
// 跳转页面
requestBody.put("page", page);
// 格式化模板数据(微信要求格式:{"字段名":{"value":"内容"}})
Map<String, Map<String, String>> dataMap = new HashMap<>();
for (Map.Entry<String, String> entry : data.entrySet()) {
Map<String, String> valueMap = new HashMap<>();
valueMap.put("value", entry.getValue());
dataMap.put(entry.getKey(), valueMap);
}
requestBody.put("data", dataMap);
// 获取access_token,拼接完整URL
String accessToken = tokenUtil.getAccessToken();
// String url = subscribeUrl + accessToken;
// 发送订阅通知的API地址
String url = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token="+ accessToken;
// 发送POST请求
String response = restTemplate.postForObject(url, requestBody, String.class);
JSONObject json = new JSONObject(response);
int errcode = json.getInt("errcode");
return errcode == 0; // 0表示成功
} catch (Exception e) {
log.error("发送订阅通知失败", e);
return false;
}
}
}
......@@ -18,6 +18,7 @@ import com.wangxiaolu.promotion.pojo.activity.temporary.dto.TemporaryClockDto;
import com.wangxiaolu.promotion.pojo.user.dto.ManageEmployeeInfoDto;
import com.wangxiaolu.promotion.result.basedata.RCode;
import com.wangxiaolu.promotion.service.activity.temporary.TemporaryActivityClockCoreService;
import com.wangxiaolu.promotion.service.activity.temporary.TemporaryActivityTaskClockService;
import com.wangxiaolu.promotion.service.activityplanv2.PromPlanCoreService;
import com.wangxiaolu.promotion.service.activityplanv2.PromPlanQueryService;
import com.wangxiaolu.promotion.utils.OkHttp;
......@@ -28,9 +29,11 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ThreadLocalRandom;
/**
* @author : liqiulin
......@@ -57,6 +60,9 @@ public class TemporaryActivityClockCoreServiceImpl implements TemporaryActivityC
private PromPlanQueryService promPlanQueryService;
@Autowired
private PromPlanCoreService promPlanCoreService;
@Autowired
private TemporaryActivityTaskClockService temporaryActivityTaskClockService;
/**
* 促销员当日打卡信息保存
*/
......@@ -152,8 +158,10 @@ public class TemporaryActivityClockCoreServiceImpl implements TemporaryActivityC
saveClockPhoto(dto, clockType);
// 日志保存
// tempActivityLogDao.save(dto.getTemporaryId(), dto.getTemporaryName(), LogType.t_1, dto.getId(), dto);
// 生成随机任务和pos上传任务
temporaryActivityTaskClockService.generateRandomClockTask(dto);
temporaryActivityTaskClockService.generatePosUploadTask(dto);
}
private void clockStoreCalDistanceByStoreQcId(String storeQcId, String clockCoordinates) {
// 查询组织架构参数、创建url
String[] clockCoordinateArr = clockCoordinates.split(",");
......
package com.wangxiaolu.promotion.service.activity.temporary.impl;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wangxiaolu.promotion.common.util.BeanUtils;
import com.wangxiaolu.promotion.domain.activity.dao.TemporaryActivityPhotoDao;
import com.wangxiaolu.promotion.domain.activity.mapper.TemporaryActivityTaskClockMapper;
import com.wangxiaolu.promotion.domain.activity.mapper.entity.TemporaryActivityTaskClockDO;
import com.wangxiaolu.promotion.domain.activity.dao.TemporaryActivityTaskClockDao;
import com.wangxiaolu.promotion.domain.activity.wrapperQo.TemporaryActivityTaskWrapperDto;
import com.wangxiaolu.promotion.domain.activity.wrapperQo.TemporaryPhotoWrapper;
import com.wangxiaolu.promotion.enums.activity.ActivityClockTaskStatus;
import com.wangxiaolu.promotion.enums.activity.ActivityPhotoType;
import com.wangxiaolu.promotion.pojo.activity.temporary.dto.TemporaryActivityPhotoDto;
import com.wangxiaolu.promotion.pojo.activity.temporary.res.TemporaryActivityTaskClockRes;
import com.wangxiaolu.promotion.pojo.activity.temporary.dto.TemporaryClockDto;
import com.wangxiaolu.promotion.pojo.activity.temporary.vo.TemporaryActivityTaskClockReq;
import com.wangxiaolu.promotion.service.activity.temporary.TemporaryActivityTaskClockService;
import com.wangxiaolu.promotion.utils.AuthUtils;
import com.wangxiaolu.promotion.utils.DateUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalTime;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;
@Service
public class TemporaryActivityTaskClockServiceImpl extends ServiceImpl<TemporaryActivityTaskClockMapper, TemporaryActivityTaskClockDO> implements TemporaryActivityTaskClockService {
@Resource
private TemporaryActivityTaskClockDao temporaryActivityTaskClockDao;
@Resource
private TemporaryActivityPhotoDao photoDao;
@Override
public List<TemporaryActivityTaskClockDO> listByTemporaryId(Integer temporaryId) {
TemporaryActivityTaskWrapperDto wrapper = new TemporaryActivityTaskWrapperDto()
.setTemporaryId(temporaryId)
.setIsDelete(1);
// 过滤未删除数据
return temporaryActivityTaskClockDao.selectList(wrapper);
}
@Override
public void generateRandomClockTask(TemporaryClockDto dto) {
// 随机生成 下午5:30~7:30 之间的Date
LocalTime randomTime = LocalTime.of(17, 30, 0).plusMinutes(ThreadLocalRandom.current().nextInt(0, 120));
Date randomDate = DateUtils.parseDateByLocalTime(randomTime);
// 生成
TemporaryActivityTaskClockDO taskClockDO = TemporaryActivityTaskClockDO.builder()
.temporaryId(dto.getTemporaryId())
.clockId(dto.getId())
.temporaryName(dto.getTemporaryName())
.reportedId(dto.getReportedId())
.planId(dto.getPlanId())
.clockTime(null)
.taskType(ActivityPhotoType.RANDOM_TASK.getType())
.taskStatus(ActivityClockTaskStatus.STARTING.getType())
.requiredlockTime(randomDate)
.activityPatternId(dto.getActivityPatternId())
.activityPattern(dto.getActivityPattern())
.isDelete(1)
.build();
temporaryActivityTaskClockDao.save(taskClockDO);
}
@Override
public void generatePosUploadTask(TemporaryClockDto dto) {
// 生成
TemporaryActivityTaskClockDO taskClockDO = TemporaryActivityTaskClockDO.builder()
.temporaryId(dto.getTemporaryId())
.clockId(dto.getId())
.temporaryName(dto.getTemporaryName())
.reportedId(dto.getReportedId())
.planId(dto.getPlanId())
.clockTime(null)
.taskType(ActivityPhotoType.POS_PHOTO.getType())
.taskStatus(ActivityClockTaskStatus.TO_BE_START.getType())
.requiredlockTime(DateUtil.endOfDay(new Date()))
.activityPatternId(dto.getActivityPatternId())
.activityPattern(dto.getActivityPattern())
.isDelete(1)
.build();
temporaryActivityTaskClockDao.save(taskClockDO);
}
@Override
public List<TemporaryActivityTaskClockDO> listByUserId() {
// 从header获取token标识
String token = AuthUtils.getToken();
String userId = AuthUtils.getUserId(token);
TemporaryActivityTaskWrapperDto wrapper = new TemporaryActivityTaskWrapperDto()
.setUserId(Long.parseLong(userId))
.setTemporaryId(Integer.parseInt(userId))
.setIsDelete(1);
List<TemporaryActivityTaskClockDO> list = temporaryActivityTaskClockDao.selectList(wrapper);
return list;
}
@Override
public void clock(TemporaryActivityTaskClockReq clockVo) {
TemporaryActivityTaskClockDO taskClockDO = temporaryActivityTaskClockDao.selectById(clockVo.getId());
if (taskClockDO == null) {
throw new IllegalArgumentException("打卡任务不存在");
}
// 随机打卡
if (ActivityPhotoType.RANDOM_TASK.getType() == clockVo.getTaskType()) {
// 检查当前时间是否在打卡要求的时间内
Date requiredLockTime = taskClockDO.getRequiredlockTime();
DateTime latestRequiredLockTime = DateUtil.offsetMinute(requiredLockTime, 15);
if (new Date().before(requiredLockTime) || new Date().after(latestRequiredLockTime)) {
throw new IllegalArgumentException("当前时间不在打卡要求的时间内");
}
}
BeanUtils.copyProperties(clockVo, taskClockDO);
// 更新 任务状态为未完成,打卡时间为当前时间
taskClockDO.setTaskStatus(ActivityClockTaskStatus.UNCOMPLETED.getType());
taskClockDO.setClockTime(new Date());
temporaryActivityTaskClockDao.update(taskClockDO);
TemporaryActivityPhotoDto dto = new TemporaryActivityPhotoDto();
dto.setClockId(taskClockDO.getId())
.setTemporaryId(taskClockDO.getTemporaryId())
.setReportedId(taskClockDO.getReportedId())
.setType(clockVo.getTaskType());
// 保存打卡图片
photoDao.saveClockTaskPhoto(dto,clockVo.getClockPhotos());
}
@Override
public TemporaryActivityTaskClockRes queryByMyTaskType(Integer taskType) {
// 从header获取token标识
String token = AuthUtils.getToken();
String userId = AuthUtils.getUserId(token);
TemporaryActivityTaskWrapperDto wrapper = new TemporaryActivityTaskWrapperDto()
.setUserId(Long.parseLong(userId))
.setTaskType(taskType)
.setTemporaryId(Integer.parseInt(userId))
.setIsDelete(1);
TemporaryActivityTaskClockDO taskClockDO = temporaryActivityTaskClockDao.selectOne(wrapper);
TemporaryActivityTaskClockRes res = new TemporaryActivityTaskClockRes();
if (taskClockDO != null) {
BeanUtils.copyProperties(taskClockDO, res);
// 查询图片
TemporaryPhotoWrapper photoWrapper = new TemporaryPhotoWrapper()
.setReportedId(taskClockDO.getReportedId())
.setTemporaryId(taskClockDO.getTemporaryId());
List<TemporaryActivityPhotoDto> photoDtos = photoDao.selectPhotos(photoWrapper);
List<String> photoUrls = photoDtos.stream()
.filter(photoDto -> photoDto.getType().equals(taskType))
.map(TemporaryActivityPhotoDto::getPhotoUrl)
.collect(Collectors.toList());
res.setClockPhtos(photoUrls);
return res;
}
return res;
}
}
package com.wangxiaolu.promotion.utils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.wangxiaolu.promotion.common.constant.TokenConstants;
import com.wangxiaolu.promotion.common.util.JwtTokenUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import javax.servlet.http.HttpServletRequest;
public class AuthUtils {
private static String secret = TokenConstants.SECRET;
/**
* 获取请求token
*/
public static String getToken()
{
return getToken(ServletUtils.getRequest());
}
/**
* 根据request获取请求token
*/
public static String getToken(HttpServletRequest request)
{
// 从header获取token标识
String token = request.getHeader(TokenConstants.AUTHENTICATION);
return replaceTokenPrefix(token);
}
/**
* 裁剪token前缀
*/
public static String replaceTokenPrefix(String token)
{
// 如果前端设置了令牌前缀,则裁剪掉前缀
if (StringUtils.isNotBlank(token) && token.startsWith(TokenConstants.PREFIX))
{
token = token.replaceFirst(TokenConstants.PREFIX, "");
}
return token;
}
/**
* 从令牌中获取数据声明
*
* @param token 令牌
* @return 数据声明
*/
public static Claims parseToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}
public static String getUserId(String token) {
return JwtTokenUtils.getUserId(parseToken(token));
}
}
......@@ -51,6 +51,32 @@ public class DateUtils {
return Date.from(instant);
}
public static Date parseDateByLocalTime(LocalTime localTime) {
if (localTime == null) {
return null;
}
// 1. 获取当前日期(可替换为任意日期)
LocalDate today = LocalDate.now();
// 2. 组合成 LocalDateTime
LocalDateTime localDateTime = today.atTime(localTime);
// 3. 指定时区(示例:系统默认时区)
ZoneId zoneId = ZoneId.systemDefault(); // 或 ZoneId.of("UTC")
ZonedDateTime zonedDateTime = localDateTime.atZone(zoneId);
// 4. 转换为 Date
Date date = Date.from(zonedDateTime.toInstant());
// 输出结果
System.out.println("LocalTime: " + localTime);
System.out.println("Date: " + date);
return date;
}
public static LocalDate parseDateBylocalDate(Date date) {
if (date == null) {
return null;
......
package com.wangxiaolu.promotion.utils;
import cn.hutool.core.convert.Convert;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.extension.api.R;
import com.wangxiaolu.promotion.common.constant.Constants;
import org.apache.poi.util.StringUtil;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import reactor.core.publisher.Mono;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
/**
* 客户端工具类
*
* @author ruoyi
*/
public class ServletUtils {
/**
* 获取String参数
*/
public static String getParameter(String name) {
return getRequest().getParameter(name);
}
/**
* 获取String参数
*/
public static String getParameter(String name, String defaultValue) {
return Convert.toStr(getRequest().getParameter(name), defaultValue);
}
/**
* 获取Integer参数
*/
public static Integer getParameterToInt(String name) {
return Convert.toInt(getRequest().getParameter(name));
}
/**
* 获取Integer参数
*/
public static Integer getParameterToInt(String name, Integer defaultValue) {
return Convert.toInt(getRequest().getParameter(name), defaultValue);
}
/**
* 获取Boolean参数
*/
public static Boolean getParameterToBool(String name) {
return Convert.toBool(getRequest().getParameter(name));
}
/**
* 获取Boolean参数
*/
public static Boolean getParameterToBool(String name, Boolean defaultValue) {
return Convert.toBool(getRequest().getParameter(name), defaultValue);
}
/**
* 获得所有请求参数
*
* @param request 请求对象{@link ServletRequest}
* @return Map
*/
public static Map<String, String[]> getParams(ServletRequest request) {
final Map<String, String[]> map = request.getParameterMap();
return Collections.unmodifiableMap(map);
}
/**
* 获得所有请求参数
*
* @param request 请求对象{@link ServletRequest}
* @return Map
*/
public static Map<String, String> getParamMap(ServletRequest request) {
Map<String, String> params = new HashMap<>();
for (Map.Entry<String, String[]> entry : getParams(request).entrySet()) {
params.put(entry.getKey(), StringUtil.join(entry.getValue(), ","));
}
return params;
}
/**
* 获取request
*/
public static HttpServletRequest getRequest() {
try {
return getRequestAttributes().getRequest();
} catch (Exception e) {
return null;
}
}
/**
* 获取response
*/
public static HttpServletResponse getResponse() {
try {
return getRequestAttributes().getResponse();
} catch (Exception e) {
return null;
}
}
/**
* 获取session
*/
public static HttpSession getSession() {
return getRequest().getSession();
}
public static ServletRequestAttributes getRequestAttributes() {
try {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
return (ServletRequestAttributes) attributes;
} catch (Exception e) {
return null;
}
}
public static String getHeader(HttpServletRequest request, String name) {
String value = request.getHeader(name);
if (StringUtils.hasLength(value)) {
return com.baomidou.mybatisplus.core.toolkit.StringUtils.EMPTY;
}
return urlDecode(value);
}
public static Map<String, String> getHeaders(HttpServletRequest request) {
Map<String, String> map = new LinkedCaseInsensitiveMap<>();
Enumeration<String> enumeration = request.getHeaderNames();
if (enumeration != null) {
while (enumeration.hasMoreElements()) {
String key = enumeration.nextElement();
String value = request.getHeader(key);
map.put(key, value);
}
}
return map;
}
/**
* 将字符串渲染到客户端
*
* @param response 渲染对象
* @param string 待渲染的字符串
*/
public static void renderString(HttpServletResponse response, String string) {
try {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(string);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 是否是Ajax异步请求
*
* @param request
*/
public static boolean isAjaxRequest(HttpServletRequest request) {
String accept = request.getHeader("accept");
if (accept != null && accept.contains("application/json")) {
return true;
}
String xRequestedWith = request.getHeader("X-Requested-With");
if (xRequestedWith != null && xRequestedWith.contains("XMLHttpRequest")) {
return true;
}
String uri = request.getRequestURI();
if (uri.contains(".json") || uri.contains(".xml")) {
return true;
}
String ajax = request.getParameter("__ajax");
// return StringUtils.inStringIgnoreCase(ajax, "json", "xml");
return ajax.contains(".json") || ajax.contains(".xml");
}
/**
* 内容编码
*
* @param str 内容
* @return 编码后的内容
*/
public static String urlEncode(String str) {
try {
return URLEncoder.encode(str, Constants.UTF8);
} catch (UnsupportedEncodingException e) {
return com.baomidou.mybatisplus.core.toolkit.StringUtils.EMPTY;
}
}
/**
* 内容解码
*
* @param str 内容
* @return 解码后的内容
*/
public static String urlDecode(String str) {
try {
return URLDecoder.decode(str, Constants.UTF8);
} catch (UnsupportedEncodingException e) {
return com.baomidou.mybatisplus.core.toolkit.StringUtils.EMPTY;
}
}
}
package com.wangxiaolu.promotion.utils;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.config.WxMaConfig;
import com.alibaba.fastjson2.JSONObject;
import com.wangxiaolu.promotion.common.redis.service.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
@Component
public class WechatAccessTokenUtil {
// @Value("${wx.miniapp.appid}")
// private String appid;
// @Value("${wx.miniapp.secret}")
// private String secret;
private static final String TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
// 缓存access_token,避免频繁调用
private String accessToken;
private long expireTime;
@Resource
private WxMaService wxMaService ;
@Autowired
private RestTemplate restTemplate;
@Autowired
private RedisCache redisCache;
// 获取有效的access_token
public String getAccessToken() {
// 检查是否过期,未过期直接返回
if (System.currentTimeMillis() < expireTime && StringUtils.hasText(accessToken)) {
return accessToken;
}
WxMaConfig wxMaConfig = wxMaService.getWxMaConfig();
String appid = wxMaConfig.getAppid();
String secret = wxMaConfig.getSecret();
// 过期则重新获取
String url = String.format(TOKEN_URL, appid, secret);
String response = restTemplate.getForObject(url, String.class);
// 解析JSON(推荐使用Jackson或FastJSON)
JSONObject json = JSONObject.from(response);
accessToken = json.getString("access_token");
// 有效期(秒)
int expiresIn = json.getIntValue("expires_in");
// 提前5分钟过期
expireTime = System.currentTimeMillis() + (expiresIn - 300) * 1000;
return accessToken;
}
}
package com.wangxiaolu.promotion.websocket;
import com.wangxiaolu.promotion.common.util.JwtTokenUtils;
import com.wangxiaolu.promotion.common.util.JwtUtils;
import io.jsonwebtoken.JwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
@Slf4j
public class JwtHandshakeInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
// 从请求参数中获取token(前端连接时需携带token参数)
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
String token = servletRequest.getParameter("token");
if (token == null || token.isEmpty()) {
throw new IllegalArgumentException("Token is required");
}
try {
// 验证token并获取用户ID
// String userId = JwtUtils.validateToken(token);
String userId = JwtTokenUtils.getUserId(JwtTokenUtils.parseToken(token));
// 将用户ID存入属性,供后续WebSocket处理器使用
attributes.put("userId", userId);
return true; // 验证通过,允许握手
} catch (JwtException e) {
// 验证失败,拒绝连接
response.setStatusCode(org.springframework.http.HttpStatus.UNAUTHORIZED);
return false;
}
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) {
// 握手后操作(可选)
log.info("Handshake completed");
}
}
package com.wangxiaolu.promotion.websocket;
import com.fasterxml.jackson.annotation.JsonProperty;
public class MessageBean {
@JsonProperty("msgType")
private String type;
@JsonProperty("data")
private String content;
// Getter and Setter
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
package com.wangxiaolu.promotion.websocket;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.*;
@Slf4j
@Component
public class TemporaryActivityTaskClockSocketHandler extends TextWebSocketHandler {
// 存储在线会话(用户ID -> 会话)
private static final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
// 心跳检测线程池
private final ScheduledExecutorService heartBeatExecutor = new ScheduledThreadPoolExecutor(
1,
r -> {
Thread t = new Thread(r, "TemporaryActivityTaskClock-HeartBeat");
t.setDaemon(false);
return t;
},
new ThreadPoolExecutor.AbortPolicy()
);
private final ObjectMapper objectMapper = new ObjectMapper();
public TemporaryActivityTaskClockSocketHandler() {
// 初始化心跳检测(每30秒发送一次ping)
heartBeatExecutor.scheduleAtFixedRate(this::sendHeartBeat, 30, 30, TimeUnit.SECONDS);
}
/**
* 连接建立后调用
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 从握手属性中获取用户ID
String userId = (String) session.getAttributes().get("userId");
sessions.put(userId, session);
log.info("用户[" + userId + "]连接成功,当前在线:" + sessions.size());
// 发送连接成功消息
session.sendMessage(new TextMessage("连接成功,用户ID:" + userId));
}
/**
* 收到前端消息时调用
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String userId = (String) session.getAttributes().get("userId");
String payload = message.getPayload();
log.info("收到用户[" + userId + "]的消息:" + payload);
// 处理心跳响应(前端收到ping后返回pong)
if ("pong".equals(payload)) {
log.info("用户[" + userId + "]心跳正常");
return;
}
// 业务处理
MessageBean messageBean = objectMapper.readValue(payload, MessageBean.class);
handleMessageType(messageBean);
// 业务消息处理(示例:广播消息)
broadcast("用户[" + userId + "]:" + payload);
}
private void handleMessageType(MessageBean messageBean) {
switch (messageBean.getType()) {
case "ACTIVITY_START":
// startActivity(messageBean.getContent());
break;
case "ACTIVITY_END":
// endActivity(messageBean.getContent());
break;
default:
log.warn("未知消息类型: {}", messageBean.getType());
}
}
/**
* 连接关闭时调用
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
String userId = (String) session.getAttributes().get("userId");
sessions.remove(userId);
log.info("用户[" + userId + "]断开连接,当前在线:" + sessions.size());
}
/**
* 发送心跳(ping)
*/
private void sendHeartBeat() {
for (Map.Entry<String, WebSocketSession> entry : sessions.entrySet()) {
WebSocketSession session = entry.getValue();
String userId = entry.getKey();
if (session.isOpen()) {
try {
session.sendMessage(new TextMessage("ping"));
} catch (IOException e) {
log.error("用户[" + userId + "]心跳发送失败,强制断开");
try {
session.close();
} catch (IOException ex) {
ex.printStackTrace();
}
sessions.remove(userId);
}
}
}
}
/**
* 广播消息给所有在线用户
*/
public void broadcast(String message) throws IOException {
for (WebSocketSession session : sessions.values()) {
if (session.isOpen()) {
session.sendMessage(new TextMessage(message));
}
}
}
/**
* 向指定用户发送消息
*/
public void sendToUser(String userId, String message) throws IOException {
WebSocketSession session = sessions.get(userId);
if (session != null && session.isOpen()) {
session.sendMessage(new TextMessage(message));
}
}
}
package com.wangxiaolu.promotion.websocket;
public enum TemporaryActivityTaskClockType {
RANDOM_TASK_TART("randomTaskStart","随机任务开始"),
RANDOM_TASK_CLOCK("randomTaskClock","随机任务打卡"),
POS_CLOCK("posClock","Pos机任务打卡"),
TASK_CLOCK_QUERY("taskClockQuery","任务信息查询"),
TASK_DATA_APPROVE_PASS("taskDataApprovePass","任务详情查询"),
;
private String type;
private String name;
TemporaryActivityTaskClockType(String type, String name) {
this.type = type;
this.name = name;
}
public String getName(String type) {
for (TemporaryActivityTaskClockType value : values()) {
if (value.type.equals(type)) {
return value.name;
}
}
return "";
}
}
package com.wangxiaolu.promotion.xxljobtask;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.wangxiaolu.promotion.domain.activity.mapper.entity.TemporaryActivityTaskClockDO;
import com.wangxiaolu.promotion.enums.activity.ActivityClockTaskStatus;
import com.wangxiaolu.promotion.pojo.user.dto.WxTemporaryInfoDto;
import com.wangxiaolu.promotion.service.activity.manage.ActivityTypeQueryService;
import com.wangxiaolu.promotion.service.activity.manage.EmployeeCoreTemporaryInfoService;
import com.wangxiaolu.promotion.service.activity.temporary.TemporaryActivityTaskClockService;
import com.wangxiaolu.promotion.service.activity.temporary.impl.SubscribeMessageService;
import com.wangxiaolu.promotion.utils.DateUtils;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.formula.functions.T;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.wangxiaolu.promotion.common.constant.WechatConstants.SUBSCRIBE_MESSAGE_TEMPLATE_ID;
/**
* @author : liqiulin
* @date : 2024-09-10 12
* @describe : 活动类型操作
*/
@Component
@Slf4j
public class ActivityStautsHandler {
@Autowired
private TemporaryActivityTaskClockService taskClockService;
@Autowired
private SubscribeMessageService messageService;
@Autowired
private EmployeeCoreTemporaryInfoService temporaryInfoService;
@XxlJob("sendSubscribeMessage")
public void sendSubscribeMessage() {
DateTime dateTime = DateUtil.offsetMinute(new Date(), 10);
// 1. 查询订单信息和用户订阅记录
List<TemporaryActivityTaskClockDO> list = taskClockService.list(new LambdaQueryWrapper<TemporaryActivityTaskClockDO>()
.eq(TemporaryActivityTaskClockDO::getTaskStatus, ActivityClockTaskStatus.TO_BE_START)
// .eq(TemporaryActivityTaskClockDO::getSubscribeStatus, 1)
// .eq(TemporaryActivityTaskClockDO::getIsSendSubscribe, 0)
.le(TemporaryActivityTaskClockDO::getRequiredlockTime, dateTime)
.ge(TemporaryActivityTaskClockDO::getRequiredlockTime, new Date()));
for (TemporaryActivityTaskClockDO taskClockDO : list) {
if (taskClockDO.getSubscribeStatus() != 1) {
log.info("用户{}未订阅活动打卡通知", taskClockDO.getTemporaryName());
taskClockDO.setIsSendSubscribe(1);
taskClockService.updateById(taskClockDO);
continue;
}
// 2. 构建模板数据(根据小程序订阅模板的字段定义)
Map<String, String> data = new HashMap<>();
// 模板中的字段1
data.put("thing1", "随机任务已开启");
// 模板中的字段2
data.put("time2", new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new Date()));
// 模板中的字段3
data.put("requiredlockTime", DateUtil.format(taskClockDO.getRequiredlockTime(), "yyyy-MM-dd HH:mm"));
// 3. 发送通知
// 获取openid
WxTemporaryInfoDto wxTemporaryInfoDto = temporaryInfoService.selectById(taskClockDO.getTemporaryId());
String openid = wxTemporaryInfoDto.getOpenId();
// 3. 发送通知
String templateId = SUBSCRIBE_MESSAGE_TEMPLATE_ID;
boolean success = messageService.sendSubscribeMessage(
openid,
templateId,
// 跳转页面
"/pages/order/detail?",
data
);
if (success) {
log.info("促销员{}订阅通知发送成功", taskClockDO.getTemporaryName());
taskClockDO.setSubscribeTime(new Date());
}
// 修改状态 待开始 -> 进行中
taskClockDO.setTaskStatus(ActivityClockTaskStatus.STARTING.getType());
taskClockService.updateById(taskClockDO);
}
}
@XxlJob("sendWebsocketMessage")
public void sendWebsocketMessage() {
DateTime dateTime = DateUtil.offsetMinute(new Date(), 10);
/**
* 10分钟后结束打卡的,(已经进行中5分钟了)
* 发送websocket消息
*/
// 1. 查询订单信息和用户订阅记录
List<TemporaryActivityTaskClockDO> list = taskClockService.list(new LambdaQueryWrapper<TemporaryActivityTaskClockDO>()
.eq(TemporaryActivityTaskClockDO::getTaskStatus, ActivityClockTaskStatus.STARTING)
.eq(TemporaryActivityTaskClockDO::getSubscribeStatus, 1)
.eq(TemporaryActivityTaskClockDO::getIsSendSubscribe, 0)
.le(TemporaryActivityTaskClockDO::getRequiredlockTime, dateTime)
.ge(TemporaryActivityTaskClockDO::getRequiredlockTime, new Date()));
for (TemporaryActivityTaskClockDO taskClockDO : list) {
// 2. 构建模板数据(根据小程序订阅模板的字段定义)
Map<String, String> data = new HashMap<>();
// 模板中的字段1
data.put("thing1", "随机任务已开启");
// 模板中的字段2
data.put("time2", new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new Date()));
// 模板中的字段3
data.put("requiredlockTime", DateUtil.format(taskClockDO.getRequiredlockTime(), "yyyy-MM-dd HH:mm"));
// 3. 发送通知
// 获取openid
WxTemporaryInfoDto wxTemporaryInfoDto = temporaryInfoService.selectById(taskClockDO.getTemporaryId());
String openid = wxTemporaryInfoDto.getOpenId();
// 3. 发送通知
String templateId = SUBSCRIBE_MESSAGE_TEMPLATE_ID;
boolean success = messageService.sendSubscribeMessage(
openid,
templateId,
// 跳转页面
"/pages/order/detail?",
data
);
if (success) {
log.info("促销员{}订阅通知发送成功", taskClockDO.getTemporaryName());
taskClockDO.setSubscribeTime(new Date());
}
// 修改状态 待开始 -> 进行中
taskClockDO.setTaskStatus(ActivityClockTaskStatus.STARTING.getType());
taskClockService.updateById(taskClockDO);
}
}
}
......@@ -100,4 +100,6 @@ public class XxlJobHandler {
log.info("[xxl-job] end === 同步昨日经销商");
}
}
......@@ -12,8 +12,7 @@ spring:
database: 0
password: QjL6H5nH
main:
web-application-type: reactive
cloud:
nacos:
......@@ -22,8 +21,7 @@ spring:
namespace: 68c8d97c-715a-4983-99b7-9df9b99f89e7
group: promotion
logging:
config: classpath:logback-spring.xml
async:
executor:
......@@ -106,4 +104,4 @@ aliyun:
sts-role-arm: acs:ram::1819206190412770:role/oss-admin-role
session-name: promotion-dev-miniapp
bucket-name: link-promotion-dev
web-js-link: link-promotion-dev.oss-cn-shanghai.aliyuncs.com
\ No newline at end of file
web-js-link: link-promotion-dev.oss-cn-shanghai.aliyuncs.com
......@@ -11,8 +11,6 @@ spring:
database: 0
password: Wxl2025!@#$
main:
web-application-type: reactive
cloud:
nacos:
......@@ -21,8 +19,7 @@ spring:
namespace: a9d4d153-3d4e-4d2f-968e-dffdfcc24bab
group: promotion
logging:
config: classpath:logback-spring.xml
async:
executor:
......@@ -97,4 +94,4 @@ aliyun:
sts-role-arm: acs:ram::1819206190412770:role/oss-admin-role
session-name: promotion-live-miniapp
bucket-name: link-promotion
web-js-link: link-promotion.oss-cn-shanghai.aliyuncs.com
\ No newline at end of file
web-js-link: link-promotion.oss-cn-shanghai.aliyuncs.com
......@@ -12,8 +12,6 @@ spring:
database: 0
password: QjL6H5nH
main:
web-application-type: reactive
cloud:
nacos:
......@@ -22,8 +20,6 @@ spring:
namespace: 3b774c2d-b03b-4816-8fe8-a41f458ebbcc
group: promotion
logging:
config: classpath:logback-spring.xml
async:
executor:
......@@ -104,4 +100,4 @@ aliyun:
sts-role-arm: acs:ram::1819206190412770:role/oss-admin-role
session-name: promotion-dev-miniapp
bucket-name: link-promotion-dev
web-js-link: link-promotion-dev.oss-cn-shanghai.aliyuncs.com
\ No newline at end of file
web-js-link: link-promotion-dev.oss-cn-shanghai.aliyuncs.com
......@@ -4,6 +4,12 @@ server:
spring:
application:
name: wangxiaolu-promotion-service
main:
# web-application-type: reactive
web-application-type: servlet
profiles:
active: dev
logging:
config: classpath:logback-spring.xml
......@@ -4,11 +4,16 @@ import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.tomcat.jni.Local;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
......@@ -22,6 +27,7 @@ import static org.junit.jupiter.api.Assertions.*;
*/
@SpringBootTest
@RunWith(SpringRunner.class)
@Slf4j
class DateUtilsTest {
@Test
......@@ -30,6 +36,14 @@ class DateUtilsTest {
System.out.println("是否在时间范围内:" + b);
}
@Test
void parseDateByLocalTime() {
LocalTime localTime = LocalTime.of(16, 0, 0);
Date date = DateUtils.parseDateByLocalTime(localTime);
log.info("时间:{}", DateUtil.format(date, "yyyy-MM-dd HH:mm:ss"));
log.info("时间:{}", localTime);
}
public static void main(String[] args) {
Map<String, Object> params = new HashMap<>();
// 根据来源第三方系统的员工唯一标识精确查询,id、emp_id如果同时存在优先取id
......@@ -50,4 +64,4 @@ class DateUtilsTest {
params.put("emp_mobile", "");
System.out.println(JSONObject.toJSONString(params));
}
}
\ No newline at end of file
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论