diff --git a/src/main/java/com/budwk/app/base/dao/CndPlus.java b/src/main/java/com/budwk/app/base/dao/CndPlus.java new file mode 100644 index 0000000..8417fea --- /dev/null +++ b/src/main/java/com/budwk/app/base/dao/CndPlus.java @@ -0,0 +1,44 @@ +package com.budwk.app.base.dao; + +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.param.PageForm; +import org.nutz.dao.Cnd; +import org.nutz.lang.Lang; +import org.nutz.lang.util.NutMap; + +/** + * 条件构造辅助类。 + * 当前健步走移植代码需要按页面查询条件动态追加字段查询和排序,空字段不应参与 SQL 条件。 + */ +public class CndPlus extends Cnd { + private static final NutMap ORDER_MAP = NutMap.NEW().addv("ascending", "asc").addv("descending", "desc"); + + public static CndPlus create() { + return new CndPlus(); + } + + public CndPlus and(PageForm pageForm) { + if (pageForm != null && StrUtil.isAllNotBlank(pageForm.getSearchName(), pageForm.getSearchKeyword())) { + this.where().andLike(pageForm.getSearchName(), pageForm.getSearchKeyword()); + } + if (pageForm != null && StrUtil.isAllNotBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) { + this.orderBy(pageForm.getPageOrderName(), ORDER_MAP.getString(pageForm.getPageOrderBy())); + } + return this; + } + + /** + * 仅在字段名和值都有效时追加条件,避免可选筛选项为空时生成无效 SQL。 + * + * @param name 字段名或表达式 + * @param op 比较操作符 + * @param value 比较值 + * @return 当前条件对象 + */ + public CndPlus andEX(String name, String op, Object value) { + if (StrUtil.isNotBlank(name) && !Lang.isEmpty(value)) { + this.and(name, op, value); + } + return this; + } +} diff --git a/src/main/java/com/budwk/app/task/job/fitnesswalk/FitnessWalkAddActivityJob.java b/src/main/java/com/budwk/app/task/job/fitnesswalk/FitnessWalkAddActivityJob.java new file mode 100644 index 0000000..5fd5b51 --- /dev/null +++ b/src/main/java/com/budwk/app/task/job/fitnesswalk/FitnessWalkAddActivityJob.java @@ -0,0 +1,194 @@ +package com.budwk.app.task.job.fitnesswalk; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.budwk.app.sys.models.Sys_task; +import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkActivity; +import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkActivityRelationProject; +import com.budwk.app.zhgh.activity.fitnesswalk.service.FitnessWalkCommonService; +import com.budwk.app.zhgh.activity.fitnesswalk.service.impl.FitnessWalkCommonServiceImpl; +import com.budwk.app.zhgh.activity.fitnesswalk.utils.WeAppCloudUtil; +import lombok.extern.slf4j.Slf4j; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Chain; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +import org.nutz.ioc.Ioc; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.json.Json; +import org.nutz.mvc.Mvcs; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +import java.util.Arrays; + +/** + * @author zhf + * @date 2026/4/11 14:01 + * @description 自动创建活动 + */ +@IocBean +@Slf4j +public class FitnessWalkAddActivityJob implements Job { + @Override + @Aop(TransAop.READ_COMMITTED) + public void execute(JobExecutionContext context) throws JobExecutionException { + Ioc ioc = Mvcs.ctx().getDefaultIoc(); + Dao dao = ioc.get(Dao.class); + WeAppCloudUtil weAppCloudUtil = ioc.get(WeAppCloudUtil.class); + // Quartz 任务上下文里按接口取 Bean 容易命中 Nutz 的接口实例化异常,这里统一按实现类获取后再走 service 公共方法。 + FitnessWalkCommonService fitnessWalkCommonService = ioc.get(FitnessWalkCommonServiceImpl.class); + + JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); + String sourceActivityId = getJobString(jobDataMap, "sourceActivityId"); + String deleteId = getJobString(jobDataMap, "deleteId"); + long targetDate = getJobLong(jobDataMap, "targetDate"); + + try { + FitnessWalkActivity sourceActivity = loadSourceActivity(weAppCloudUtil, sourceActivityId); + if (sourceActivity == null) { + disableCurrentTask(dao, deleteId, sourceActivityId); + return; + } + + FitnessWalkActivity newActivity = JSON.parseObject(JSON.toJSONString(sourceActivity), FitnessWalkActivity.class); + shiftActivityDate(sourceActivity, newActivity, targetDate); + resetBranchActivityNameByRelationProject(dao, newActivity, targetDate); + long currentTime = System.currentTimeMillis(); + long normalizedTargetDate = DateUtil.beginOfDay(DateUtil.date(targetDate)).getTime(); + newActivity.set_id(null); + newActivity.setCreatedAt(currentTime); + newActivity.setUpdatedAt(currentTime); + // 自动复制出的活动补充来源标记,便于后续编辑原活动时识别哪些日期已经真正生成过活动。 + newActivity.setAutoCreateSourceActivityId(sourceActivityId); + newActivity.setAutoCreateTargetDate(normalizedTargetDate); + + JSONObject insertResult = weAppCloudUtil.request(WeAppCloudUtil.CRUD.INSERT, + "db.collection('activity').add({data:" + Json.toJson(newActivity) + "})"); + if (insertResult.containsKey("id_list") && insertResult.getJSONArray("id_list") != null && !insertResult.getJSONArray("id_list").isEmpty()) { + newActivity.set_id(insertResult.getJSONArray("id_list").getString(0)); + } + + // 自动复制出的活动也要同步刷新缓存,并补齐该活动自身的抽奖任务。 + fitnessWalkCommonService.addLotteryTask(newActivity); + fitnessWalkCommonService.addOrEditDoSaveRedis(newActivity); + } catch (Exception e) { + log.error("自动连续创建活动失败, sourceActivityId={}", sourceActivityId, e); + throw new JobExecutionException(e); + } finally { + disableCurrentTask(dao, deleteId, sourceActivityId); + } + } + + /** + * Quartz 的 JobDataMap 可能直接存的是 Long、Integer 等对象,不能一律用 getString 读取。 + * 这里统一兜底转成字符串,避免任务参数类型变化时再次触发 ClassCastException。 + */ + private String getJobString(JobDataMap jobDataMap, String key) { + Object value = jobDataMap.get(key); + return value == null ? null : String.valueOf(value); + } + + /** + * 定时任务中的日期时间参数既可能是数字,也可能是字符串。 + * 这里统一兼容处理,确保 targetDate 在测试和正式任务下都能稳定转成时间戳。 + */ + private long getJobLong(JobDataMap jobDataMap, String key) { + Object value = jobDataMap.get(key); + if (value == null) { + throw new IllegalArgumentException("定时任务缺少参数:" + key); + } + if (value instanceof Number) { + return ((Number) value).longValue(); + } + return Long.parseLong(String.valueOf(value)); + } + + /** + * 根据原活动ID查询原活动。 + * 定时任务执行时统一从云库读取最新活动内容,确保复制时拿到的是当前活动的最新配置。 + */ + private FitnessWalkActivity loadSourceActivity(WeAppCloudUtil weAppCloudUtil, String sourceActivityId) { + JSONObject jsonObject = weAppCloudUtil.request(WeAppCloudUtil.CRUD.QUERY, + "db.collection('activity').doc('" + sourceActivityId + "').get()"); + return jsonObject.getJSONArray("data").stream() + .map(item -> JSON.parseObject((String) item, FitnessWalkActivity.class)) + .findFirst() + .orElse(null); + } + + /** + * 将活动的报名时间和活动时间整体平移到目标日期。 + * 这里保留原活动的时分秒和时长,仅根据目标日期替换所属自然日。 + */ + private void shiftActivityDate(FitnessWalkActivity sourceActivity, FitnessWalkActivity newActivity, long targetDate) { + DateTime sourceStartDate = DateUtil.beginOfDay(DateUtil.date(sourceActivity.getStartTime())); + DateTime targetStartDate = DateUtil.beginOfDay(DateUtil.date(targetDate)); + long offsetMillis = targetStartDate.getTime() - sourceStartDate.getTime(); + + if (sourceActivity.getApplyTime() != null && sourceActivity.getApplyTime().length == 2) { + Long[] applyTime = Arrays.copyOf(sourceActivity.getApplyTime(), 2); + applyTime[0] = applyTime[0] + offsetMillis; + applyTime[1] = applyTime[1] + offsetMillis; + newActivity.setApplyTime(applyTime); + newActivity.setApplyStartTime(applyTime[0]); + newActivity.setApplyEndTime(applyTime[1]); + } + + if (sourceActivity.getTime() != null && sourceActivity.getTime().length == 2) { + Long[] time = Arrays.copyOf(sourceActivity.getTime(), 2); + time[0] = time[0] + offsetMillis; + time[1] = time[1] + offsetMillis; + newActivity.setTime(time); + newActivity.setStartTime(time[0]); + newActivity.setEndTime(time[1]); + } + } + + /** + * 自动连续创建活动时,分活动名称统一取“关联活动配置名称 + 日期前缀”。 + * 优先读取关联活动配置表中的主活动名称;若当前活动没有配置关联活动,则回退到原分活动名称。 + */ + private void resetBranchActivityNameByRelationProject(Dao dao, FitnessWalkActivity newActivity, long targetDate) { + if (newActivity == null) { + return; + } + String datePrefix = DateUtil.format(DateUtil.date(targetDate), "yyyy年MM月dd日"); + String targetName = null; + if (StrUtil.isNotBlank(newActivity.getRelationActivityId())) { + FitnessWalkActivityRelationProject relationProject = dao.fetch( + FitnessWalkActivityRelationProject.class, + Cnd.where("id", "=", newActivity.getRelationActivityId()) + ); + if (relationProject != null && StrUtil.isNotBlank(relationProject.getMasterActivityName())) { + targetName = relationProject.getMasterActivityName().trim(); + } + } + + if (StrUtil.isBlank(targetName) && StrUtil.isNotBlank(newActivity.getBranchActivityName())) { + targetName = newActivity.getBranchActivityName().trim(); + } + if (StrUtil.isBlank(targetName)) { + return; + } + if (targetName.startsWith(datePrefix)) { + newActivity.setBranchActivityName(targetName); + return; + } + newActivity.setBranchActivityName(datePrefix + " " + targetName); + } + + /** + * 当前任务执行完成后禁用自身,避免同一日期重复创建活动。 + */ + private void disableCurrentTask(Dao dao, String deleteId, String sourceActivityId) { + dao.update(Sys_task.class, Chain.make("disabled", 1), + Cnd.where("data", "like", "%" + deleteId + "%").and("data", "like", "%" + sourceActivityId + "%")); + } +} diff --git a/src/main/java/com/budwk/app/task/job/fitnesswalk/FitnessWalkLotteryJob.java b/src/main/java/com/budwk/app/task/job/fitnesswalk/FitnessWalkLotteryJob.java new file mode 100644 index 0000000..b2b256d --- /dev/null +++ b/src/main/java/com/budwk/app/task/job/fitnesswalk/FitnessWalkLotteryJob.java @@ -0,0 +1,207 @@ +package com.budwk.app.task.job.fitnesswalk; + +import cn.hutool.core.date.DateUtil; +import com.budwk.app.sys.models.Sys_task; +import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkAwardUser; +import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkRaffleUser; +import lombok.extern.slf4j.Slf4j; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Chain; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.dao.util.cri.Static; +import org.nutz.ioc.Ioc; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.Strings; +import org.nutz.lang.random.R; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.Mvcs; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; + +import java.util.*; +import java.util.stream.Collectors; + +@IocBean +@Slf4j +public class FitnessWalkLotteryJob implements Job { + + @Override + @Aop(TransAop.READ_COMMITTED) + public void execute(JobExecutionContext context) throws JobExecutionException { + Ioc ioc = Mvcs.ctx().getDefaultIoc(); + Dao dao = ioc.get(Dao.class); + + JobDataMap jobDataMap = context.getJobDetail().getJobDataMap(); + String deleteId = jobDataMap.getString("deleteId"); + + int lotteryQualificationStep = Integer.parseInt(jobDataMap.getString("lotteryQualificationStep")); + int lotteryUserNum = Integer.parseInt(jobDataMap.getString("lotteryUserNum")); + String mode = jobDataMap.getString("mode"); + String activityId = jobDataMap.getString("id"); + String lotteryQualificationDay = jobDataMap.getString("lotteryQualificationDay"); + + String awardName; + Date startDate = null; + Date endDate = null; + if ("custom".equals(mode)) { + awardName = jobDataMap.getString("awardName"); + String startTimeStr = jobDataMap.getString("startDate"); + String endTimeStr = jobDataMap.getString("endDate"); + startDate = DateUtil.parse(startTimeStr, "yyyy-MM-dd"); + endDate = DateUtil.parse(endTimeStr, "yyyy-MM-dd"); + } else { + awardName = null; + } + List userList = new ArrayList<>(); + + if ("custom".equals(mode)) { + // 按当前项目 vw_user 视图查询用户及分工会、单位名称,保持抽奖记录字段一致。 + Sql sql = Sqls.create(""" + SELECT + u.id, + COALESCE(SUM(sub.step), 0) AS total_steps, + u.loginname, + u.username, + u.unionid, + u.unionname, + u.unitid, + u.unitname, + over3k.standardsDays + FROM + `vw_user` u + LEFT JOIN ( + SELECT + userId, + applyDate, + step + FROM + `fitness_walk_step` + WHERE + activityId = @activityId + AND DATE(applyDate) >= @startDate + AND DATE(applyDate) <= @endDate + $lotteryQualificationDaySql + GROUP BY + userId, + applyDate + ) AS sub ON u.id = sub.userId + LEFT JOIN ( + SELECT + userId, + COUNT(DISTINCT DATE(applyDate)) AS standardsDays + FROM + `fitness_walk_step` + WHERE + activityId = @activityId + AND DATE(applyDate) >= @startDate + AND DATE(applyDate) <= @endDate + $lotteryQualificationDaySql + GROUP BY + userId + ) AS over3k ON u.id = over3k.userId + WHERE u.loginname NOT IN ( + SELECT loginName FROM fitness_walk_award_user WHERE activityId = @activityId + ) + GROUP BY + u.id, + u.username, + u.loginname, + u.unionname, + u.unitname + HAVING + $stepHavingSql + """); + if (Strings.isNotBlank(lotteryQualificationDay)) { + sql.setVar("lotteryQualificationDaySql", new Static(" AND step >= '%s' ".formatted(lotteryQualificationStep))); + sql.setVar("stepHavingSql", new Static(" over3k.standardsDays >= '%s' ".formatted(lotteryQualificationDay))); + } else { + sql.setVar("stepHavingSql", new Static(" COALESCE(SUM(sub.step), 0) >= '%s' ".formatted(lotteryQualificationStep))); + } + sql.setParam("activityId", activityId); + sql.setParam("step", lotteryQualificationStep); + sql.setParam("startDate", startDate); + sql.setParam("endDate", endDate); + // 保留Sql对象中的绑定参数,云端活动ID作为字符串传入,不能拼入SQL或重新解析toString结果。 + sql.setCallback(Sqls.callback.maps()); + dao.execute(sql); + userList = sql.getList(NutMap.class); + } else if ("everyday".equals(mode)) { + Sql sql = Sqls.create(""" + SELECT + jws.userId AS id, + u.username, + u.loginname, + u.unionid, + u.unionname, + u.unitid, + u.unitname + FROM + fitness_walk_step jws + LEFT JOIN `vw_user` u ON u.id = jws.userId + WHERE + jws.activityId = @activityId + AND jws.step >= @step + AND jws.applyDate = @date + """); + sql.setParam("activityId", activityId); + sql.setParam("step", lotteryQualificationStep); + sql.setParam("date", DateUtil.today()); + // 保留Sql对象中的绑定参数,云端活动ID作为字符串传入,不能拼入SQL或重新解析toString结果。 + sql.setCallback(Sqls.callback.maps()); + dao.execute(sql); + userList = sql.getList(NutMap.class); + } + + // 记录达标人员,表明这些人有资格抽奖 + List raffleUserList = userList.stream().map(v -> { + FitnessWalkRaffleUser raffleUser = new FitnessWalkRaffleUser(); + raffleUser.setId(R.UU32()); + raffleUser.setActivityId(activityId); + raffleUser.setUserId(v.getString("id")); + raffleUser.setLoginName(v.getString("loginname")); + raffleUser.setUsername(v.getString("username")); + if ("custom".equals(mode)) { + raffleUser.setStandardsDays(v.getInt("standardsDays")); + raffleUser.setAwardSteps(v.getString("total_steps")); + } + raffleUser.setIsRaffle(false); + return raffleUser; + }).collect(Collectors.toList()); + + dao.fastInsert(raffleUserList); + + // 随机抽奖,中奖人数为传递过来的 lotteryUserNum + Collections.shuffle(userList); + List lotteryUserList = userList.stream().limit(lotteryUserNum).collect(Collectors.toList()); + + Date finalStartDate = startDate; + Date finalEndDate = endDate; + List awardLists = lotteryUserList.stream().map(v -> { + FitnessWalkAwardUser awardUser = new FitnessWalkAwardUser(); + awardUser.setActivityId(activityId); + awardUser.setUserId(v.getString("id")); + awardUser.setUsername(v.getString("username")); + awardUser.setLoginName(v.getString("loginname")); + awardUser.setAwardName(awardName); + awardUser.setUnionId(v.getString("unionid")); + awardUser.setUnionName(v.getString("unionname")); + awardUser.setUnitId(v.getString("unitid")); + awardUser.setUnitName(v.getString("unitname")); + awardUser.setApplyDate(new Date()); + awardUser.setStartDate(finalStartDate); + awardUser.setEndDate(finalEndDate); + awardUser.setAwardSteps(v.getString("total_steps")); + return awardUser; + }).collect(Collectors.toList()); + dao.insert(awardLists); + + // 执行完此定时任务后,禁用此定时任务确保不会下次执行 + dao.update(Sys_task.class, Chain.make("disabled", 1), Cnd.where("data", "like", "%" + deleteId + "%").and("data", "like", "%" + activityId + "%")); + } +} diff --git a/src/main/java/com/budwk/app/web/commons/auth/satoken/aop/SaTokenAuthIgnoreUtil.java b/src/main/java/com/budwk/app/web/commons/auth/satoken/aop/SaTokenAuthIgnoreUtil.java new file mode 100644 index 0000000..c1f6dd8 --- /dev/null +++ b/src/main/java/com/budwk/app/web/commons/auth/satoken/aop/SaTokenAuthIgnoreUtil.java @@ -0,0 +1,57 @@ +package com.budwk.app.web.commons.auth.satoken.aop; + +import com.budwk.app.web.commons.base.Globals; +import org.nutz.lang.Lang; +import org.nutz.lang.Strings; +import org.nutz.mvc.Mvcs; + +import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.regex.Pattern; + +/** + * Sa-Token 注解认证白名单工具。 + *

+ * 用于集中维护不需要登录、权限、角色校验即可访问的 URL,匹配规则与旧 Shiro 体系中的 + * authIgnoreUrlArr 保持一致:数组中的每一项都按正则表达式与当前请求 URI 完整匹配。 + */ +public class SaTokenAuthIgnoreUtil { + + /** + * 不需要认证授权的 URL。 + *

+ * 参数说明:数组中的字符串为请求 URI 正则表达式,匹配的是去除项目 contextPath 后的路径。 + * 返回值说明:通过 {@link #isIgnore()} 返回 boolean,true 表示当前请求跳过 Sa-Token 注解校验。 + */ + private static final String[] AUTH_IGNORE_URL_ARR = Lang.array( + "/platform/activity/basic/scope/getScopeUser", + "/platform/fitnessWalk/stepManage/basic/scope/getScopeUser", + "/platform/fitnessWalk/stepManage/getActivityDateStep", + "/platform/fitnessWalk/stepManage/winningRecord", + "/platform/fitnessWalk/stepManage/updateStepMonth", + "/platform/fitnessWalk/stepRanking/getUserStepRanking", + "/platform/fitnessWalk/stepManage/getActivityQualifyProgressBar", + "/platform/fitnessWalk/stepWining/getUserStepWining", + "/platform/fitnessWalk/stepWining/prizeOption", + "/platform/fitnessWalk/stepWining/prizeUsers", + "/platform/fitnessWalk/punchLottery/judgeWinningToPunch", + "/platform/fitnessWalk/punchLottery/getLotteryRecord", + "/platform/fitnessWalk/punchLottery/doExchange", + "/platform/fitnessWalk/punchLottery/getPunchWining", + "/platform/fitnessWalk/punchLottery/doReadLottery", + "/platform/fitnessWalk/activityRelation/isCurrentUserRelationActivityQualified"); + + /** + * 判断当前请求是否命中匿名访问白名单。 + * + * @return boolean true 表示当前 URL 不需要执行登录、权限、角色注解校验;false 表示继续走原有 Sa-Token 校验逻辑 + */ + public static boolean isIgnore() { + HttpServletRequest request = Mvcs.getReq(); + if (request == null) { + return false; + } + String requestURI = Strings.sNull(request.getRequestURI()).replaceFirst("^" + Pattern.quote(Strings.sNull(Globals.AppBase)), ""); + return Arrays.stream(AUTH_IGNORE_URL_ARR).anyMatch(v -> Pattern.compile(v).matcher(requestURI).matches()); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/activity/basic/controller/ActivityBasicScopeController.java b/src/main/java/com/budwk/app/zhgh/activity/basic/controller/ActivityBasicScopeController.java index 0c83832..9b32e73 100644 --- a/src/main/java/com/budwk/app/zhgh/activity/basic/controller/ActivityBasicScopeController.java +++ b/src/main/java/com/budwk/app/zhgh/activity/basic/controller/ActivityBasicScopeController.java @@ -60,7 +60,7 @@ import java.util.stream.Collectors; * @createTime 2022年01月04日 10:15:00 */ @IocBean -@At("/platform/activity/basic/scope") +@At({"/platform/activity/basic/scope", "/platform/fitnessWalk/stepManage/basic/scope"}) @Ok("json:full") @Slf4j public class ActivityBasicScopeController { diff --git a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/controller/FitnessWalkActivityRelationController.java b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/controller/FitnessWalkActivityRelationController.java new file mode 100644 index 0000000..989fc25 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/controller/FitnessWalkActivityRelationController.java @@ -0,0 +1,98 @@ +package com.budwk.app.zhgh.activity.fitnesswalk.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.budwk.app.base.param.PageForm; +import com.budwk.app.base.result.Result; +import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkActivityRelationProject; +import com.budwk.app.zhgh.activity.fitnesswalk.service.FitnessWalkActivityRelationProjectService; +import com.budwk.app.zhgh.activity.fitnesswalk.service.FitnessWalkRelationActivityCompletionService; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +/** + * 健步走活动关联菜单。 + * 这里新增“关联项目”管理能力,主活动名称与达标活动数量统一落在 MySQL 中。 + */ +@IocBean +@Ok("json") +@At("/platform/fitnessWalk/activityRelation") +public class FitnessWalkActivityRelationController { + + @Inject + private FitnessWalkActivityRelationProjectService relationProjectService; + + @Inject + private FitnessWalkRelationActivityCompletionService relationActivityCompletionService; + + @At("") + @Ok("beetl:/platform/zhgh/activity/fitnesswalk/activityRelation.html") + @SaCheckPermission("fitnessWalk.activityRelation") + public void index() { + } + + /** + * 查询关联项目分页列表。 + * @param pageForm 页码、每页条数及查询排序参数 + * @return Result,data.list为当前页记录,data.totalCount为符合条件的总条数 + */ + @At + @SaCheckPermission("fitnessWalk.activityRelation") + public Result pageData(PageForm pageForm) { + return Result.success(relationProjectService.pageData(pageForm)); + } + + /** + * 保存关联项目。 + * 支持新增和编辑,保存字段仅包含主活动名称和达标活动数量。 + * @param relationProject 关联项目配置,包含id、masterActivityName和standardActivityNum + * @return Result,code为0表示成功,data为业务结果,msg为操作说明 + */ + @At + @SaCheckPermission("fitnessWalk.activityRelation") + @Aop(TransAop.READ_COMMITTED) + public Result doSave(FitnessWalkActivityRelationProject relationProject) { + return relationProjectService.saveRelationProject(relationProject); + } + + /** + * 删除关联项目。 + * @param id 待操作记录的主键ID + * @return Result,code为0表示成功,data为业务结果,msg为操作说明 + */ + @At + @SaCheckPermission("fitnessWalk.activityRelation") + @Aop(TransAop.READ_COMMITTED) + public Result doRemove(String id) { + return relationProjectService.deleteRelationProject(id); + } + + /** + * 查看关联项目下已关联的活动。 + * 活动列表按活动数据中保存的关联ID查询,避免列表展示与实际活动数据不一致。 + * @param id 待操作记录的主键ID + * @return Result,code为0表示成功,data为业务结果,msg为操作说明 + */ + @At + @SaCheckPermission("fitnessWalk.activityRelation") + public Result relationActivityList(String id) { + return Result.success(relationProjectService.getRelationActivityList(id)); + } + + /** + * 判断当前登录人是否满足某个关联项目的达标要求。 + * controller 仅负责接参与返回结果,具体的关联活动和打卡完成判断统一放在独立 service 中处理。 + * @param relationActivityId 关联项目ID,对应fitness_walk_activity_relation_project主键 + * @param userId 小程序登录接口返回的用户ID + * @return Result,code为0表示成功,data为业务结果,msg为操作说明 + */ + @At + @SaCheckPermission("fitnessWalk.activityRelation") + public Result isCurrentUserRelationActivityQualified(String relationActivityId,String userId) { + return Result.success(relationActivityCompletionService.isCurrentUserRelationActivityQualified(relationActivityId,userId)); + } + +} diff --git a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/controller/FitnessWalkUserPunchStatisticsController.java b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/controller/FitnessWalkUserPunchStatisticsController.java new file mode 100644 index 0000000..6f0af6f --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/controller/FitnessWalkUserPunchStatisticsController.java @@ -0,0 +1,88 @@ +package com.budwk.app.zhgh.activity.fitnesswalk.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.budwk.app.base.page.Pagination; +import com.budwk.app.base.result.Result; +import com.budwk.app.zhgh.activity.fitnesswalk.service.FitnessWalkPunchStatisticsService; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; +import org.nutz.mvc.annotation.Param; +import javax.servlet.http.HttpServletResponse; + +/** + * 人员打卡情况。 + * 该页面按人员维度展示所选活动范围内的打卡完成情况,支持主活动聚合查询和分活动单独查询。 + */ +@IocBean +@Ok("json") +@At("/platform/fitnessWalk/userPunchStatistics") +public class FitnessWalkUserPunchStatisticsController { + + @Inject + private FitnessWalkPunchStatisticsService fitnessWalkPunchStatisticsService; + + @At("") + @Ok("beetl:/platform/zhgh/activity/fitnesswalk/userPunchStatistics.html") + @SaCheckPermission("fitnessWalk.userPunchStatistics") + public void index() { + } + + /** + * 查询活动级联选项。 + * 这里只返回打卡模式活动,供前端年份切换后联动刷新主活动和分活动下拉。 + * @param year 四位查询年度 + * @return Result,code为0表示成功,data为业务结果,msg为操作说明 + */ + @At + @SaCheckPermission("fitnessWalk.userPunchStatistics") + public Result getCascadersActivity(@Param("year") int year) { + return Result.success(fitnessWalkPunchStatisticsService.userPunchCascaderActivity(year)); + } + + /** + * 查询人员打卡情况列表。 + * 当前接口按人员维度返回点位总数、已打卡点位数以及是否全部完成全部点位。 + * 当页面只选择主活动且该主活动存在 relationActivityId 时,还支持按达标/未达标筛选人员。 + * @param mainActivityId 主活动ID或relation:关联项目ID + * @param subActivityId 分活动ID,空值表示汇总主活动 + * @param activityId 健步走云端活动ID + * @param unionId 分工会ID,空值表示不追加组织筛选 + * @param searchKeyword 姓名或工号查询内容 + * @param qualifiedStatus qualified表示已达标,unqualified表示未达标,all表示全部 + * @param pageNumber 页码,从1开始 + * @param pageSize 每页记录数 + * @return Result,data.list为当前页记录,data.totalCount为符合条件的总条数 + */ + @At + @Ok("json:full") + @SaCheckPermission("fitnessWalk.userPunchStatistics") + public Result pageData(String mainActivityId, String subActivityId, String activityId, String unionId, String searchKeyword, + String qualifiedStatus, @Param("pageNumber") int pageNumber, @Param("pageSize") int pageSize) { + Pagination pagination = fitnessWalkPunchStatisticsService.userPunchPageData( + mainActivityId, subActivityId, activityId, unionId, searchKeyword, qualifiedStatus, pageNumber, pageSize + ); + return Result.success(pagination); + } + + /** + * 按主分活动和达标筛选导出全部人员打卡汇总。 + * + * @param mainActivityId 主活动ID或relation:关联项目ID + * @param subActivityId 分活动ID,空值表示主活动汇总 + * @param activityId 健步走云端活动ID + * @param unionId 分工会ID,空值表示不追加筛选,已有数据权限仍生效 + * @param searchKeyword 姓名、工号或当前查询字段的检索内容 + * @param qualifiedStatus 达标筛选,qualified、unqualified或all + * @param response HTTP响应,用于写入XSSF格式Excel文件流 + * 无返回对象,通过response写出Excel文件流。 + */ + @At + @Ok("void") + @SaCheckPermission("fitnessWalk.userPunchStatistics") + public void exportExcel(String mainActivityId, String subActivityId, String activityId, String unionId, String searchKeyword, String qualifiedStatus, + HttpServletResponse response) { + fitnessWalkPunchStatisticsService.exportUserPunchExcel(mainActivityId, subActivityId, activityId, unionId, searchKeyword, qualifiedStatus, response); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/model/FitnessWalkActivityRelationProject.java b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/model/FitnessWalkActivityRelationProject.java new file mode 100644 index 0000000..927676a --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/model/FitnessWalkActivityRelationProject.java @@ -0,0 +1,41 @@ +package com.budwk.app.zhgh.activity.fitnesswalk.model; + +import com.budwk.app.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.nutz.dao.entity.annotation.ColDefine; +import org.nutz.dao.entity.annotation.ColType; +import org.nutz.dao.entity.annotation.Column; +import org.nutz.dao.entity.annotation.Comment; +import org.nutz.dao.entity.annotation.Name; +import org.nutz.dao.entity.annotation.Table; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +/** + * 健步走活动关联项目。 + * 该表用于保存用户手工创建的“主活动名称 + 达标活动数量”配置,数据统一落在 MySQL 中。 + */ +@Data +@Table("fitness_walk_activity_relation_project") +@EqualsAndHashCode(callSuper = true) +public class FitnessWalkActivityRelationProject extends BaseModel { + + private static final long serialVersionUID = 1L; + + @Name + @Column + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + @Comment("主键ID") + private String id; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 100) + @Comment("主活动名称") + private String masterActivityName; + + @Column + @ColDefine(type = ColType.INT) + @Comment("达标活动数量") + private Integer standardActivityNum; +} diff --git a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkActivityRelationProjectService.java b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkActivityRelationProjectService.java new file mode 100644 index 0000000..e2cc75a --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkActivityRelationProjectService.java @@ -0,0 +1,49 @@ +package com.budwk.app.zhgh.activity.fitnesswalk.service; + +import com.budwk.app.base.page.Pagination; +import com.budwk.app.base.param.PageForm; +import com.budwk.app.base.result.Result; +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkActivityRelationProject; +import java.util.List; +import org.nutz.lang.util.NutMap; + +/** + * 健步走活动关联项目 service。 + * 负责关联项目在 MySQL 中的分页查询、保存和删除。 + */ +public interface FitnessWalkActivityRelationProjectService extends BaseService { + + /** + * 查询关联项目列表。 + * + * @param pageForm 分页参数 + * @return 分页结果 + */ + Pagination pageData(PageForm pageForm); + + /** + * 保存关联项目。 + * + * @param relationProject 关联项目 + * @return 保存结果 + */ + Result saveRelationProject(FitnessWalkActivityRelationProject relationProject); + + /** + * 删除关联项目。 + * + * @param id 主键ID + * @return 删除结果 + */ + Result deleteRelationProject(String id); + + /** + * 查询某个关联项目下的活动列表。 + * 这里按活动数据中保存的关联ID查询,保证查看结果与活动实际保存的数据一致。 + * + * @param id 关联项目ID + * @return 关联活动列表 + */ + List getRelationActivityList(String id); +} diff --git a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkActivityService.java b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkActivityService.java new file mode 100644 index 0000000..e1ae2e8 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkActivityService.java @@ -0,0 +1,102 @@ +package com.budwk.app.zhgh.activity.fitnesswalk.service; + +import com.budwk.app.base.result.Result; +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkActivity; +import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkActivityRelationProject; +import com.budwk.app.zhgh.activity.fitnesswalk.service.FitnessWalkActivityService; +import java.util.*; +import java.util.List; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.upload.TempFile; + +/** + * 健步走活动管理 service。 + * 统一负责活动在 MySQL 中的查询、保存、更新、删除以及云库历史数据同步。 + */ +public interface FitnessWalkActivityService extends BaseService { + + /** + * 获取当前年份可关联的活动下拉数据。 + * 下拉内容统一返回活动ID、活动名称和达标活动数量,供创建/编辑页面直接使用。 + * + * @return 关联活动下拉数据 + */ + List getCurrentYearRelationActivityOptions(); + + /** + * 获取福利项目下拉数据。 + * 创建/编辑活动时统一从 service 层查询福利项目,避免页面直接跨模块拼查询。 + * + * @param year 年份,为空时默认当前年份 + * @return 福利项目下拉数据,仅返回页面下拉所需字段 + */ + List getWelfareProjectOptions(Integer year); + + /** + * 分页检索云端活动并保留当前分工会数据权限。 + * + * @param year 查询年度,四位年份 + * @param unionId 分工会ID,空值表示不追加筛选,已有数据权限仍生效 + * @param searchName 查询字段,使用页面提供的字段名 + * @param searchKeyword 姓名、工号或当前查询字段的检索内容 + * @param pageNumber 页码,从1开始 + * @param pageSize 每页记录数 + * @param pageOrderName 列表排序字段 + * @param pageOrderBy 排序方向,ascending或descending + * @return Result,code为0表示成功,data承载分页检索云端活动并保留当前分工会数据权限结果,失败说明见msg + */ + Result pageData(Integer year, String unionId, String searchName, String searchKeyword, int pageNumber, int pageSize, String pageOrderName, String pageOrderBy); + + /** + * 创建云端活动并登记抽奖、连续创建任务和活动缓存。 + * + * @param data 兼容原活动提交参数,活动配置以fitnessWalkActivity为准 + * @param fitnessWalkActivity 页面提交的活动配置,包含模式、时间、点位和抽奖规则 + * @param tempFile 活动封面上传文件,不更换时传空 + * @param stepFiles 计步展示图片文件数组,不更换时传空 + * @param certificateTempFile 完赛证书背景文件,不更换时传空 + * @return Result,code为0表示成功,data承载创建云端活动并登记抽奖、连续创建任务和活动缓存结果,失败说明见msg + */ + Result doAdd(String data, FitnessWalkActivity fitnessWalkActivity, + TempFile tempFile, + TempFile[] stepFiles, + TempFile certificateTempFile); + + /** + * 将微信云存储文件ID转换为上传组件回显信息。 + * + * @param pic 微信云存储文件ID + * @return Result,code为0表示成功,data承载将微信云存储文件ID转换为上传组件回显信息结果,失败说明见msg + */ + Result getFile(String pic); + + /** + * 更新活动配置并同步关联任务和活动缓存。 + * + * @param fitnessWalkActivity 页面提交的活动配置,包含模式、时间、点位和抽奖规则 + * @param tempFile 活动封面上传文件,不更换时传空 + * @param stepFiles 计步展示图片文件数组,不更换时传空 + * @param certificateTempFile 完赛证书背景文件,不更换时传空 + * @return Result,code为0表示成功,data承载更新活动配置并同步关联任务和活动缓存结果,失败说明见msg + */ + Result doEdit(FitnessWalkActivity fitnessWalkActivity, + TempFile tempFile, + TempFile[] stepFiles, + TempFile certificateTempFile); + + /** + * 删除云端活动及报名、打卡、礼品券和关联任务。 + * + * @param id 待操作记录的主键ID + * @return Result,code为0表示成功,data承载删除云端活动及报名、打卡、礼品券和关联任务结果,失败说明见msg + */ + Result deleteActivity(String id); + + /** + * 检索腾讯地图地址候选项,使用当前项目配置的AppTMapKey与CityName。 + * @param keyword 地名或地址关键字 + * @return Result,data为腾讯地图返回的状态及地址候选列表 + */ + Result suggestMapAddress(String keyword); +} diff --git a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkBindingRecordService.java b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkBindingRecordService.java new file mode 100644 index 0000000..9851d55 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkBindingRecordService.java @@ -0,0 +1,46 @@ +package com.budwk.app.zhgh.activity.fitnesswalk.service; + +import com.budwk.app.base.result.Result; +import java.io.IOException; + +/** + * 小程序绑定记录公共业务接口。 + */ +public interface FitnessWalkBindingRecordService { + + /** + * 按工号、姓名及分工会权限分页检索绑定记录。 + * + * @param keyWord 绑定记录的姓名或工号关键字 + * @param unionId 分工会ID,空值表示不追加筛选,已有数据权限仍生效 + * @param searchName 查询字段,使用页面提供的字段名 + * @param searchKeyword 姓名、工号或当前查询字段的检索内容 + * @param pageNumber 页码,从1开始 + * @param pageSize 每页记录数 + * @param pageOrderName 列表排序字段 + * @param pageOrderBy 排序方向,ascending或descending + * @return Result,code为0表示成功,data承载按工号、姓名及分工会权限分页检索绑定记录结果,失败说明见msg + */ + Result pageData(String keyWord, + String unionId, + String searchName, + String searchKeyword, + int pageNumber,int pageSize, + String pageOrderName, + String pageOrderBy) throws IOException; + + /** + * 按记录ID删除选中的云端绑定记录。 + * + * @param ids 待删除记录的主键ID数组 + * @return Result,code为0表示成功,data承载按记录ID删除选中的云端绑定记录结果,失败说明见msg + */ + Result delete(String[] ids); + + /** + * 清除云端绑定记录,供管理员重新同步人员绑定。 + * + * @return Result,code为0表示成功,data承载清除云端绑定记录,供管理员重新同步人员绑定结果,失败说明见msg + */ + Result deleteAll(); +} diff --git a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkPunchLotteryService.java b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkPunchLotteryService.java new file mode 100644 index 0000000..637d712 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkPunchLotteryService.java @@ -0,0 +1,63 @@ +package com.budwk.app.zhgh.activity.fitnesswalk.service; + +import com.budwk.app.base.result.Result; +import java.util.*; + +/** + * 打卡抽奖兑奖公共业务接口。 + */ +public interface FitnessWalkPunchLotteryService { + + /** + * 按源版奖池和报名人数分配打卡抽奖结果。 + * + * @param activityId 健步走云端活动ID + * @param userId 登录接口返回的用户ID + * @return Result,code为0表示成功,data承载按源版奖池和报名人数分配打卡抽奖结果结果,失败说明见msg + */ + Result judgeWinningToPunch(String activityId, String userId); + + /** + * 查询用户在指定活动中的打卡抽奖记录。 + * + * @param activityId 健步走云端活动ID + * @param userId 登录接口返回的用户ID + * @return Result,code为0表示成功,data承载查询用户在指定活动中的打卡抽奖记录结果,失败说明见msg + */ + Result getLotteryRecord(String activityId, String userId); + + /** + * 核销奖品并记录兑换时间,重复核销返回状态2。 + * + * @param id 待操作记录的主键ID + * @return Result,code为0表示成功,data承载核销奖品并记录兑换时间,重复核销返回状态2结果,失败说明见msg + */ + Result doExchange(String id); + + /** + * 将用户在指定活动中的抽奖记录标记为已读。 + * + * @param activityId 健步走云端活动ID + * @param userId 登录接口返回的用户ID + * @return Result,code为0表示成功,data承载将用户在指定活动中的抽奖记录标记为已读结果,失败说明见msg + */ + Result doReadLottery(String activityId, String userId); + + /** + * 查询指定打卡活动中已中奖的人员。 + * + * @param activityId 健步走云端活动ID + * @return Result,code为0表示成功,data承载查询指定打卡活动中已中奖的人员结果,失败说明见msg + */ + Result getPunchWining(String activityId); + + /** + * 生成不重复的中奖序号,边界规则沿用源版本。 + * + * @param minNum 随机中奖序号下界 + * @param maxNum 随机中奖序号上界 + * @param count 需要抽取的中奖序号数量 + * @return List,用于生成不重复的中奖序号,边界规则沿用源版本 + */ + List generateRandomNumber(Integer minNum, Integer maxNum, Integer count); +} diff --git a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkRelationActivityCompletionService.java b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkRelationActivityCompletionService.java new file mode 100644 index 0000000..8534770 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkRelationActivityCompletionService.java @@ -0,0 +1,20 @@ +package com.budwk.app.zhgh.activity.fitnesswalk.service; + +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkActivityRelationProject; + +/** + * 健步走关联活动完成情况 service。 + * 该 service 专门处理“按关联项目查询当前登录人的关联活动完成进度”场景,避免把业务判断堆到通用 service 中。 + */ +public interface FitnessWalkRelationActivityCompletionService extends BaseService { + + /** + * 根据活动关联项目ID,判断当前登录人是否满足关联活动达标要求。 + * 只有当当前用户完成的达标活动数等于关联配置的 standardActivityNum 时,才返回 true。 + * + * @param relationActivityId 活动关联项目ID + * @return true 代表全部合格,false 代表未达标 + */ + boolean isCurrentUserRelationActivityQualified(String relationActivityId,String userId); +} diff --git a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkStepManageService.java b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkStepManageService.java new file mode 100644 index 0000000..315bd18 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkStepManageService.java @@ -0,0 +1,55 @@ +package com.budwk.app.zhgh.activity.fitnesswalk.service; + +import com.budwk.app.base.result.Result; +import java.util.*; + +/** + * 活动步数与积分公共业务接口。 + */ +public interface FitnessWalkStepManageService { + + /** + * 合并微信运动最近30天步数,按每天最大步数保存并计算积分。 + * + * @param activityId 健步走云端活动ID + * @param userId 登录接口返回的用户ID + * @param monthSteps 微信运动步数JSON数组,每项包含step及秒级timestamp + * @return Result,code为0表示成功,data承载合并微信运动最近30天步数,按每天最大步数保存并计算积分结果,失败说明见msg + */ + Result updateStepMonth(String activityId, String userId, String monthSteps); + + /** + * 查询活动日期步数,返回step和毫秒级timestamp。 + * + * @param activityId 健步走云端活动ID + * @param userId 登录接口返回的用户ID + * @return Result,code为0表示成功,data承载查询活动日期步数,返回step和毫秒级timestamp结果,失败说明见msg + */ + Result getActivityDateStep(String activityId, String userId); + + /** + * 按每日或自定义规则计算用户达标步数与完成进度。 + * + * @param activityId 健步走云端活动ID + * @param userId 登录接口返回的用户ID + * @return Result,code为0表示成功,data承载按每日或自定义规则计算用户达标步数与完成进度结果,失败说明见msg + */ + Result getActivityQualifyProgressBar(String activityId, String userId); + + /** + * 查询指定用户在活动中的中奖记录。 + * + * @param activityId 健步走云端活动ID + * @param userId 登录接口返回的用户ID + * @return Result,code为0表示成功,data承载查询指定用户在活动中的中奖记录结果,失败说明见msg + */ + Result winningRecord(String activityId, String userId); + + /** + * 标记活动奖项已读,沿用源版活动维度更新范围。 + * + * @param activityId 健步走云端活动ID + * @return Result,code为0表示成功,data承载标记活动奖项已读,沿用源版活动维度更新范围结果,失败说明见msg + */ + Result hasReadAward(String activityId); +} diff --git a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkStepStatisticsService.java b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkStepStatisticsService.java new file mode 100644 index 0000000..36af175 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkStepStatisticsService.java @@ -0,0 +1,45 @@ +package com.budwk.app.zhgh.activity.fitnesswalk.service; + +import com.budwk.app.base.param.PageForm; +import com.budwk.app.base.result.Result; +import java.util.*; +import javax.servlet.http.HttpServletResponse; + +/** + * 计步明细统计公共业务接口。 + */ +public interface FitnessWalkStepStatisticsService { + + /** + * 按活动、日期、工号及组织条件分页统计用户每日步数。 + * + * @param pageForm 分页、查询字段和排序参数,页码从1开始 + * @param year 查询年度,四位年份 + * @param activityId 健步走云端活动ID + * @param activityDate 起止日期数组,两项均为yyyy-MM-dd + * @param unionId 分工会ID,空值表示不追加筛选,已有数据权限仍生效 + * @param unitId 单位ID,空值表示不追加单位筛选 + * @return Result,code为0表示成功,data承载按活动、日期、工号及组织条件分页统计用户每日步数结果,失败说明见msg + */ + Result pageData(PageForm pageForm, + Integer year, + String activityId, + String[] activityDate, + String unionId, + String unitId); + + /** + * 按当前活动和日期条件导出计步明细。 + * + * @param activityId 健步走云端活动ID + * @param activityDate 起止日期数组,两项均为yyyy-MM-dd + * @param unionId 分工会ID,空值表示不追加筛选,已有数据权限仍生效 + * @param unitId 单位ID,空值表示不追加单位筛选 + * @param response HTTP响应,用于写入XSSF格式Excel文件流 + * 无返回对象,通过response写出Excel文件流。 + */ + void exportExcel(String activityId, + String[] activityDate, + String unionId, + String unitId, HttpServletResponse response); +} diff --git a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkActivityRelationProjectServiceImpl.java b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkActivityRelationProjectServiceImpl.java new file mode 100644 index 0000000..931c3cb --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkActivityRelationProjectServiceImpl.java @@ -0,0 +1,187 @@ +package com.budwk.app.zhgh.activity.fitnesswalk.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.budwk.app.base.page.Pagination; +import com.budwk.app.base.param.PageForm; +import com.budwk.app.base.result.Result; +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkActivity; +import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkActivityRelationProject; +import com.budwk.app.zhgh.activity.fitnesswalk.service.FitnessWalkActivityRelationProjectService; +import com.budwk.app.zhgh.activity.fitnesswalk.utils.WeAppCloudUtil; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.Strings; +import org.nutz.lang.util.NutMap; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 健步走活动关联项目 MySQL 实现。 + */ +@IocBean(args = {"refer:dao"}) +public class FitnessWalkActivityRelationProjectServiceImpl extends BaseServiceImpl + implements FitnessWalkActivityRelationProjectService { + + @Inject + private WeAppCloudUtil weAppCloudUtil; + + public FitnessWalkActivityRelationProjectServiceImpl(Dao dao) { + super(dao); + } + + @Override + public Pagination pageData(PageForm pageForm) { + PageForm currentPageForm = pageForm == null ? new PageForm() : pageForm; + Cnd cnd = buildPageCnd(currentPageForm); + Integer pageNumber = currentPageForm.getPageNumber() == null ? 1 : currentPageForm.getPageNumber(); + Integer pageSize = currentPageForm.getPageSize() == null ? 10 : currentPageForm.getPageSize(); + return listPageMap(pageNumber, pageSize, cnd); + } + + @Override + @Aop(TransAop.READ_COMMITTED) + public Result saveRelationProject(FitnessWalkActivityRelationProject relationProject) { + try { + validateAndNormalize(relationProject); + if (StrUtil.isBlank(relationProject.getId())) { + insert(relationProject); + } else { + updateIgnoreNull(relationProject); + } + return Result.success("保存成功"); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } + + @Override + @Aop(TransAop.READ_COMMITTED) + public Result deleteRelationProject(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("未获取到需要删除的关联项目"); + } + delete(id); + return Result.success("删除成功"); + } + + @Override + public List getRelationActivityList(String id) { + if (StrUtil.isBlank(id)) { + return new ArrayList<>(); + } + + StringBuilder sql = new StringBuilder("db.collection('activity').where({relationActivityId:'") + .append(id) + .append("'"); + + sql.append("}).field({") + .append("masterActivityName:true,") + .append("branchActivityName:true,") + .append("activityModel:true,") + .append("createdAt:true,") + .append("relationActivityId:true") + .append("}).orderBy('createdAt','desc').limit(1000).get()"); + + try { + JSONObject jsonObject = weAppCloudUtil.request(WeAppCloudUtil.CRUD.QUERY, sql.toString()); + return jsonObject.getJSONArray("data").stream() + .map(item -> JSON.parseObject((String) item, FitnessWalkActivity.class)) + .map(this::buildRelationActivityItem) + .collect(Collectors.toList()); + } catch (Exception e) { + throw new RuntimeException("查询关联活动失败:" + e.getMessage()); + } + } + + /** + * 组装分页查询条件。 + * 当前列表仅支持按主活动名称模糊查询,并默认按创建时间倒序展示。 + */ + private Cnd buildPageCnd(PageForm pageForm) { + Cnd cnd = Cnd.NEW(); + if (pageForm != null && StrUtil.isNotBlank(pageForm.getSearchKeyword())) { + cnd.and("masterActivityName", "like", "%" + pageForm.getSearchKeyword().trim() + "%"); + } + cnd.desc("createdAt"); + return cnd; + } + + /** + * 保存前统一校验和标准化字段。 + * 这里同时限制主活动名称唯一,避免重复创建同名关联项目影响后续配置。 + */ + private void validateAndNormalize(FitnessWalkActivityRelationProject relationProject) { + if (relationProject == null) { + throw new RuntimeException("关联项目信息不能为空"); + } + + String masterActivityName = Strings.sNull(relationProject.getMasterActivityName()).trim(); + if (Strings.isBlank(masterActivityName)) { + throw new RuntimeException("请填写主活动名称"); + } + relationProject.setMasterActivityName(masterActivityName); + + if (relationProject.getStandardActivityNum() == null || relationProject.getStandardActivityNum() <= 0) { + throw new RuntimeException("达标活动数量必须大于0"); + } + + Cnd cnd = Cnd.where("masterActivityName", "=", masterActivityName); + if (Strings.isNotBlank(relationProject.getId())) { + cnd.and("id", "<>", relationProject.getId()); + } + if (count(cnd) > 0) { + throw new RuntimeException("主活动名称已存在,请勿重复创建"); + } + } + + /** + * 将活动对象转换为查看页需要的展示数据。 + * 这里统一补充活动名称和活动模式文本,前端只负责展示,不再重复拼接字段。 + */ + private NutMap buildRelationActivityItem(FitnessWalkActivity activity) { + NutMap item = NutMap.NEW(); + item.put("activityId", activity.get_id()); + item.put("activityName", buildRelationActivityName(activity)); + item.put("activityModel", activity.getActivityModel()); + item.put("activityModelName", getActivityModelName(activity.getActivityModel())); + item.put("createdAt", activity.getCreatedAt()); + return item; + } + + /** + * 统一生成活动展示名称。 + * 没有分活动名称时仅显示主活动名称;有分活动名称时按“主活动(分活动)”展示。 + */ + private String buildRelationActivityName(FitnessWalkActivity activity) { + String masterActivityName = Strings.sNull(activity.getMasterActivityName()).trim(); + String branchActivityName = Strings.sNull(activity.getBranchActivityName()).trim(); + if (Strings.isBlank(branchActivityName)) { + return masterActivityName; + } + return masterActivityName + "(" + branchActivityName + ")"; + } + + /** + * 返回活动模式中文名称,便于查看页面直接展示。 + */ + private String getActivityModelName(String activityModel) { + if ("punch".equals(activityModel)) { + return "打卡模式"; + } + if ("stepCount".equals(activityModel)) { + return "计步模式"; + } + if ("GPS".equals(activityModel)) { + return "GPS模式"; + } + return ""; + } +} diff --git a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkActivityServiceImpl.java b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkActivityServiceImpl.java new file mode 100644 index 0000000..52eecd2 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkActivityServiceImpl.java @@ -0,0 +1,354 @@ +package com.budwk.app.zhgh.activity.fitnesswalk.service.impl; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.budwk.app.base.constant.RoleConstant; +import com.budwk.app.base.dao.CndPlus; +import com.budwk.app.base.page.Pagination; +import com.budwk.app.base.result.Result; +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.sys.models.Sys_config; +import com.budwk.app.sys.models.Sys_task; +import com.budwk.app.sys.services.SysConfigService; +import com.budwk.app.sys.services.SysTaskService; +import com.budwk.app.task.services.TaskPlatformService; +import com.budwk.app.web.commons.auth.utils.AuthUtil; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkActivity; +import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkActivityRelationProject; +import com.budwk.app.zhgh.activity.fitnesswalk.service.FitnessWalkActivityService; +import com.budwk.app.zhgh.activity.fitnesswalk.service.FitnessWalkCommonService; +import com.budwk.app.zhgh.activity.fitnesswalk.utils.WeAppCloudUtil; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.json.Json; +import org.nutz.lang.Lang; +import org.nutz.lang.Strings; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.upload.TempFile; +import java.util.*; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 健步走活动管理实现。 + * 负责活动读写、关联项目及福利下拉、腾讯地图地址检索。 + */ +@IocBean(args = {"refer:dao"}) +public class FitnessWalkActivityServiceImpl extends BaseServiceImpl implements FitnessWalkActivityService { + + @Inject + private FitnessWalkCommonService fitnessWalkCommonService; + + @Inject + private SysTaskService sysTaskService; + + @Inject + private TaskPlatformService taskPlatformService; + + @Inject + private WeAppCloudUtil weAppCloudUtil; + + public FitnessWalkActivityServiceImpl(Dao dao) { + super(dao); + } + + @Override + public List getCurrentYearRelationActivityOptions() { + List projectList = dao().query(FitnessWalkActivityRelationProject.class, CndPlus.create().andEX("YEAR(FROM_UNIXTIME(createdAt/ 1000))", "=", DateUtil.thisYear())); + return projectList; + } + + @Override + public List getWelfareProjectOptions(Integer year) { + int targetYear = year == null ? DateUtil.thisYear() : year; + Sql sql = Sqls.create(""" + SELECT + id, + `name` + FROM + welfare_project + $condition + """); + CndPlus cnd = CndPlus.create(); + cnd.and("isDisabled", "=", false); + cnd.andEX("year", "=", targetYear); + sql.setCondition(cnd); + return listMap(sql); + } + + /** + * 分页检索云端活动并保留当前分工会数据权限。 + * + * @param year 查询年度,四位年份 + * @param unionId 分工会ID,空值表示不追加筛选,已有数据权限仍生效 + * @param searchName 查询字段,使用页面提供的字段名 + * @param searchKeyword 姓名、工号或当前查询字段的检索内容 + * @param pageNumber 页码,从1开始 + * @param pageSize 每页记录数 + * @param pageOrderName 列表排序字段 + * @param pageOrderBy 排序方向,ascending或descending + * @return Result,code为0表示成功,data承载分页检索云端活动并保留当前分工会数据权限结果,失败说明见msg + */ + @Override + public Result pageData(Integer year, String unionId, String searchName, String searchKeyword, int pageNumber, int pageSize, String pageOrderName, String pageOrderBy) { + + int skip = (pageNumber == 1 ? 0 : (pageNumber - 1)) * pageSize; + + StringBuilder sql = new StringBuilder("db.collection('activity')"); + if (Strings.isNotBlank(searchName) && Strings.isNotBlank(searchKeyword)) { + sql.append("where("); + sql.append("_.or(["); + sql.append("{name:{$regex:'").append(searchKeyword).append("',$options:'i'}}"); + sql.append("{tag:{$regex:'").append(searchKeyword).append("',$options:'i'}}"); + sql.append("])"); + sql.append(")"); + } + if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) { + sql.append(".where({unionId:'").append(SecurityUtil.getUnionId()).append("'})"); + } else if (StrUtil.isNotBlank(unionId)) { + sql.append(".where({unionId:'").append(unionId).append("'})"); + } + sql.append(".skip(").append(skip).append(")"); + sql.append(".limit(").append(pageSize).append(")"); + sql.append(".get()"); + try { + JSONObject jsonObject = weAppCloudUtil.request(WeAppCloudUtil.CRUD.QUERY, sql.toString()); + List data = jsonObject.getJSONArray("data").stream().map(v -> JSON.parseObject((String) v, FitnessWalkActivity.class)).collect(Collectors.toList()); + JSONObject pager = jsonObject.getJSONObject("pager"); + return Result.success(new Pagination(pageNumber, pageSize, pager.getIntValue("Total"), data)); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + + } + + /** + * 创建云端活动并登记抽奖、连续创建任务和活动缓存。 + * + * @param data 兼容原活动提交参数,活动配置以fitnessWalkActivity为准 + * @param fitnessWalkActivity 页面提交的活动配置,包含模式、时间、点位和抽奖规则 + * @param tempFile 活动封面上传文件,不更换时传空 + * @param stepFiles 计步展示图片文件数组,不更换时传空 + * @param certificateTempFile 完赛证书背景文件,不更换时传空 + * @return Result,code为0表示成功,data承载创建云端活动并登记抽奖、连续创建任务和活动缓存结果,失败说明见msg + */ + @Override + @Aop(TransAop.READ_COMMITTED) + public Result doAdd(String data, FitnessWalkActivity fitnessWalkActivity, + TempFile tempFile, + TempFile[] stepFiles, + TempFile certificateTempFile) { + try { + long currentTime = System.currentTimeMillis(); + fitnessWalkActivity.setCreatedAt(currentTime); + fitnessWalkActivity.setUpdatedAt(currentTime); + + //设置时间 + if ("punch".equals(fitnessWalkActivity.getActivityModel())) { + fitnessWalkActivity.setApplyStartTime(fitnessWalkActivity.getApplyTime()[0]); + fitnessWalkActivity.setApplyEndTime(fitnessWalkActivity.getApplyTime()[1]); + } + fitnessWalkActivity.setStartTime(fitnessWalkActivity.getTime()[0]); + fitnessWalkActivity.setEndTime(fitnessWalkActivity.getTime()[1]); + + if (tempFile != null) { + String fileId = weAppCloudUtil.uploadFile("activity/", tempFile.getFile()); + fitnessWalkActivity.setCover(fileId); + } + + if (Lang.isNotEmpty(stepFiles)) { + List stepFileIdList = new ArrayList<>(); + for (TempFile file : stepFiles) { + String fileId = weAppCloudUtil.uploadFile("activity/", file.getFile()); + stepFileIdList.add(fileId); + } + fitnessWalkActivity.setRandomPics(stepFileIdList); + } + + if (certificateTempFile != null) { + String fileId = weAppCloudUtil.uploadFile("activity/", certificateTempFile.getFile()); + fitnessWalkActivity.setCompletionCertificateCover(fileId); + } + + if (AuthUtil.hasRoleOr("BRANCH_UNION_CHAIRMAN", "BRANCH_UNION_OPERATOR", "BRANCH_UNION_ADMIN") && !AuthUtil.hasRoleOr("SYSADMIN", "SCHOOL_UNION_ADMIN")) { + fitnessWalkActivity.setUnionId(SecurityUtil.getUnionId()); + } else { + fitnessWalkActivity.setUnionId(""); + } + + fitnessWalkActivity.setNote(fitnessWalkActivity.getNote().replace("\"", "'")); + StringBuilder sql = new StringBuilder(); + sql.append("db.collection('activity').add({data:").append(Json.toJson(fitnessWalkActivity)).append("})"); + + try { + JSONObject jsonObject = weAppCloudUtil.request(WeAppCloudUtil.CRUD.INSERT, sql.toString()); + // 新增成功后回填云库生成的活动ID,后续创建定时任务和刷新缓存都依赖这个ID。 + if (jsonObject.containsKey("id_list") && jsonObject.getJSONArray("id_list") != null && !jsonObject.getJSONArray("id_list").isEmpty()) { + fitnessWalkActivity.set_id(jsonObject.getJSONArray("id_list").getString(0)); + } + fitnessWalkCommonService.addLotteryTask(fitnessWalkActivity); + fitnessWalkCommonService.addCreateActivityTask(fitnessWalkActivity); + fitnessWalkCommonService.addOrEditDoSaveRedis(fitnessWalkActivity); + return Result.success(); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } catch (Exception e) { + e.printStackTrace(); + return Result.error(); + } + } + + /** + * 将微信云存储文件ID转换为上传组件回显信息。 + * + * @param pic 微信云存储文件ID + * @return Result,code为0表示成功,data承载将微信云存储文件ID转换为上传组件回显信息结果,失败说明见msg + */ + @Override + public Result getFile(String pic) { + try { + NutMap res = NutMap.NEW(); + String imgurl = weAppCloudUtil.httpFile(pic); + HashMap imgmap = new HashMap<>(); + imgmap.put("status", "success"); + imgmap.put("name", imgurl.substring(imgurl.lastIndexOf("/") + 1)); + imgmap.put("id", imgurl); + imgmap.put("url", imgurl); + res.setv("img", imgmap); + return Result.success(res); + } catch (Exception e) { + return Result.error(); + } + } + + /** + * 更新活动配置并同步关联任务和活动缓存。 + * + * @param fitnessWalkActivity 页面提交的活动配置,包含模式、时间、点位和抽奖规则 + * @param tempFile 活动封面上传文件,不更换时传空 + * @param stepFiles 计步展示图片文件数组,不更换时传空 + * @param certificateTempFile 完赛证书背景文件,不更换时传空 + * @return Result,code为0表示成功,data承载更新活动配置并同步关联任务和活动缓存结果,失败说明见msg + */ + @Override + @Aop(TransAop.READ_COMMITTED) + public Result doEdit(FitnessWalkActivity fitnessWalkActivity, + TempFile tempFile, + TempFile[] stepFiles, + TempFile certificateTempFile) { + fitnessWalkActivity.setUpdatedAt(System.currentTimeMillis()); + + //设置时间 + if ("punch".equals(fitnessWalkActivity.getActivityModel())) { + fitnessWalkActivity.setApplyStartTime(fitnessWalkActivity.getApplyTime()[0]); + fitnessWalkActivity.setApplyEndTime(fitnessWalkActivity.getApplyTime()[1]); + } + fitnessWalkActivity.setStartTime(fitnessWalkActivity.getTime()[0]); + fitnessWalkActivity.setEndTime(fitnessWalkActivity.getTime()[1]); + + if (tempFile != null) { + String fileId = weAppCloudUtil.uploadFile("activity/", tempFile.getFile()); + fitnessWalkActivity.setCover(fileId); + } + + if (Lang.isNotEmpty(stepFiles)) { + List stepFileIdList = new ArrayList<>(); + for (TempFile file : stepFiles) { + String fileId = weAppCloudUtil.uploadFile("activity/", file.getFile()); + stepFileIdList.add(fileId); + } + fitnessWalkActivity.setRandomPics(stepFileIdList); + } + + if (certificateTempFile != null) { + String fileId = weAppCloudUtil.uploadFile("activity/", certificateTempFile.getFile()); + fitnessWalkActivity.setCompletionCertificateCover(fileId); + } + + if (AuthUtil.hasRoleOr("BRANCH_UNION_CHAIRMAN", "BRANCH_UNION_OPERATOR", "BRANCH_UNION_ADMIN") && !AuthUtil.hasRoleOr("SYSADMIN", "SCHOOL_UNION_ADMIN")) { + fitnessWalkActivity.setUnionId(SecurityUtil.getUnionId()); + } else { + fitnessWalkActivity.setUnionId(""); + } + + fitnessWalkActivity.setNote(fitnessWalkActivity.getNote().replace("\"", "'")); + + StringBuilder sql = new StringBuilder(); + sql.append("db.collection('activity').doc('") + .append(fitnessWalkActivity.get_id()) + .append("').update({data:").append(Json.toJson(fitnessWalkActivity)).append("})"); + + try { + JSONObject jsonObject = weAppCloudUtil.request(WeAppCloudUtil.CRUD.UPDATE, sql.toString()); + fitnessWalkCommonService.addLotteryTask(fitnessWalkActivity); + fitnessWalkCommonService.addCreateActivityTask(fitnessWalkActivity); + fitnessWalkCommonService.addOrEditDoSaveRedis(fitnessWalkActivity); + return Result.success(); + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } + + /** + * 删除云端活动及报名、打卡、礼品券和关联任务。 + * + * @param id 待操作记录的主键ID + * @return Result,code为0表示成功,data承载删除云端活动及报名、打卡、礼品券和关联任务结果,失败说明见msg + */ + @Override + @Aop(TransAop.READ_COMMITTED) + public Result deleteActivity(String id) { + try { + weAppCloudUtil.request(WeAppCloudUtil.CRUD.DELETE, "db.collection('activity').doc('" + id + "').remove()"); + weAppCloudUtil.request(WeAppCloudUtil.CRUD.DELETE, "db.collection('register').where({activity_id:'" + id + "'}).remove()"); + weAppCloudUtil.request(WeAppCloudUtil.CRUD.DELETE, "db.collection('sign').where({activity_id:'" + id + "'}).remove()"); + weAppCloudUtil.request(WeAppCloudUtil.CRUD.DELETE, "db.collection('gift_voucher').where({activity_id:'" + id + "'}).remove()"); + + List sys_tasks = sysTaskService.query(Cnd.where("note", "=", id)); + if (sys_tasks != null) { + sys_tasks.forEach(v -> { + taskPlatformService.delete(v.getId(), v.getId()); + sysTaskService.delete(v.getId()); + }); + + } + return Result.success(); + } catch (Exception e) { + return Result.error(); + } + } + + @Inject + private SysConfigService sysConfigService; + + /** + * 从腾讯地图检索候选地址,Key读取当前项目配置,城市用于限定搜索区域。 + * @param keyword 地名或地址关键字 + * @return Result,data.status为地图状态,data.data为地址候选数组 + */ + @Override + public Result suggestMapAddress(String keyword) { + Sys_config keyConfig = sysConfigService.getValueByKey("AppTMapKey"); + String mapKey = keyConfig == null ? "" : keyConfig.getConfigValue(); + if (Strings.isBlank(mapKey)) { + return Result.error("请先配置腾讯地图系统参数AppTMapKey"); + } + Sys_config cityConfig = sysConfigService.getValueByKey("CityName"); + String cityName = cityConfig == null ? "" : Strings.sNull(cityConfig.getConfigValue()); + // 参数编码由HttpUtil完成,地址中的空格和&不得改变请求参数含义。 + String response = HttpUtil.get("https://apis.map.qq.com/ws/place/v1/suggestion", + Map.of("key", mapKey, "keyword", keyword, "region", cityName)); + return Result.success(JSON.parseObject(response)); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkBindingRecordServiceImpl.java b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkBindingRecordServiceImpl.java new file mode 100644 index 0000000..e242f9e --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkBindingRecordServiceImpl.java @@ -0,0 +1,121 @@ +package com.budwk.app.zhgh.activity.fitnesswalk.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.budwk.app.base.constant.RoleConstant; +import com.budwk.app.base.page.Pagination; +import com.budwk.app.base.result.Result; +import com.budwk.app.web.commons.auth.utils.AuthUtil; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.activity.fitnesswalk.service.FitnessWalkBindingRecordService; +import com.budwk.app.zhgh.activity.fitnesswalk.utils.WeAppCloudUtil; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.json.Json; +import org.nutz.lang.Strings; +import org.nutz.lang.util.NutMap; +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 小程序绑定记录业务服务,集中处理查询、数据写入和导出。 + */ +@IocBean +public class FitnessWalkBindingRecordServiceImpl implements FitnessWalkBindingRecordService { + + @Inject + private WeAppCloudUtil weAppCloudUtil; + + /** + * 按工号、姓名及分工会权限分页检索绑定记录。 + * + * @param keyWord 绑定记录的姓名或工号关键字 + * @param unionId 分工会ID,空值表示不追加筛选,已有数据权限仍生效 + * @param searchName 查询字段,使用页面提供的字段名 + * @param searchKeyword 姓名、工号或当前查询字段的检索内容 + * @param pageNumber 页码,从1开始 + * @param pageSize 每页记录数 + * @param pageOrderName 列表排序字段 + * @param pageOrderBy 排序方向,ascending或descending + * @return Result,code为0表示成功,data承载按工号、姓名及分工会权限分页检索绑定记录结果,失败说明见msg + */ + @Override + public Result pageData(String keyWord, + String unionId, + String searchName, + String searchKeyword, + int pageNumber,int pageSize, + String pageOrderName, + String pageOrderBy) throws IOException { + int skip = (pageNumber == 1 ? 0 : (pageNumber - 1)) * pageSize; + + StringBuilder sql = new StringBuilder("db.collection('login_record')"); + if (StrUtil.isNotBlank(keyWord)) { + sql.append(".where("); + sql.append("_.or(["); + sql.append("{loginname:{$regex:'").append(keyWord).append("',$options:'i'}},"); + sql.append("{username:{$regex:'").append(keyWord).append("',$options:'i'}}"); + sql.append("])"); + sql.append(")"); + } + if (!AuthUtil.hasRoleOr(RoleConstant.SYSADMIN.name(), RoleConstant.SCHOOL_UNION_ADMIN.name())) { + sql.append(".where({'data.unionid':'").append(SecurityUtil.getUnionId()).append("'})"); + } else { + if (Strings.isNotBlank(unionId)) { + sql.append(".where({'data.unionid':'").append(unionId).append("'})"); + } + } + sql.append(".field({ data: false })"); + sql.append(".skip(").append(skip).append(")"); + sql.append(".limit(").append(pageSize).append(")"); + sql.append(".get()"); + + try { + JSONObject jsonObject = weAppCloudUtil.request(WeAppCloudUtil.CRUD.QUERY, sql.toString()); + List data = jsonObject.getJSONArray("data").stream().map(v -> JSON.parseObject((String) v, NutMap.class)).collect(Collectors.toList()); + JSONObject pager = jsonObject.getJSONObject("pager"); + return Result.success(new Pagination(pageNumber, pageSize, pager.getIntValue("Total"), data)); + } catch (Exception e){ + return Result.error(e.getMessage()); + } + } + + /** + * 按记录ID删除选中的云端绑定记录。 + * + * @param ids 待删除记录的主键ID数组 + * @return Result,code为0表示成功,data承载按记录ID删除选中的云端绑定记录结果,失败说明见msg + */ + @Override + @Aop(TransAop.READ_COMMITTED) + public Result delete(String[] ids) { + try { + String sql = "db.collection('login_record').where({_id:_.in(" + Json.toJson(ids) + ")}).remove()"; + weAppCloudUtil.request(WeAppCloudUtil.CRUD.DELETE,sql); + return Result.success(); + } catch (Exception e) { + return Result.error(); + } + } + + /** + * 清除云端绑定记录,供管理员重新同步人员绑定。 + * + * @return Result,code为0表示成功,data承载清除云端绑定记录,供管理员重新同步人员绑定结果,失败说明见msg + */ + @Override + @Aop(TransAop.READ_COMMITTED) + public Result deleteAll() { + try { + String sql = "db.collection('login_record').where({_id: _.neq('0')}).remove()"; + weAppCloudUtil.request(WeAppCloudUtil.CRUD.DELETE,sql); + return Result.success(); + } catch (Exception e) { + return Result.error(); + } + } +} diff --git a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkPunchLotteryServiceImpl.java b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkPunchLotteryServiceImpl.java new file mode 100644 index 0000000..960852c --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkPunchLotteryServiceImpl.java @@ -0,0 +1,250 @@ +package com.budwk.app.zhgh.activity.fitnesswalk.service.impl; + +import cn.hutool.core.util.StrUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.budwk.app.base.result.Result; +import com.budwk.app.sys.models.Sys_user; +import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkActivity; +import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkPunchLottery; +import com.budwk.app.zhgh.activity.fitnesswalk.service.FitnessWalkCommonService; +import com.budwk.app.zhgh.activity.fitnesswalk.service.FitnessWalkPunchLotteryService; +import com.budwk.app.zhgh.activity.fitnesswalk.utils.WeAppCloudUtil; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Chain; +import org.nutz.dao.Cnd; +import org.nutz.integration.jedis.RedisService; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.random.R; +import java.util.*; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +/** + * 打卡抽奖兑奖业务服务,集中处理查询、数据写入和导出。 + */ +@IocBean +public class FitnessWalkPunchLotteryServiceImpl implements FitnessWalkPunchLotteryService { + private final ReentrantLock lock = new ReentrantLock(true); + + @Inject + private RedisService redisService; + + @Inject + private WeAppCloudUtil weAppCloudUtil; + + @Inject + private FitnessWalkCommonService fitnessWalkCommonService; + + /** + * 按源版奖池和报名人数分配打卡抽奖结果。 + * + * @param activityId 健步走云端活动ID + * @param userId 登录接口返回的用户ID + * @return Result,code为0表示成功,data承载按源版奖池和报名人数分配打卡抽奖结果结果,失败说明见msg + */ + @Override + @Aop(TransAop.READ_COMMITTED) + public Result judgeWinningToPunch(String activityId, String userId) { + try { + if (StrUtil.isBlank(activityId) && StrUtil.isBlank(userId)) { + return Result.error("参数错误"); + } + + lock.lock(); + int winingCount = fitnessWalkCommonService.dao().count(FitnessWalkPunchLottery.class, Cnd.where("activityId", "=", activityId).and("userId", "=", userId)); + if (winingCount > 0) { + return Result.success(); + } + + String redisKey = this.getClass().getName() + "#judgeWinningToPunch#activityid:" + activityId + "#PRIZELIST:"; + String getPrizeIndexesKey = this.getClass().getName() + "#judgeWinningToPunch#activityid:" + activityId + "#GETPRIZEINDEXES:"; + +// redisService.del(getPrizeIndexesKey); + List punchLotteryPrizes = new ArrayList<>(); + String punchLotteryPrizesJson = redisService.get(redisKey); + if (StrUtil.isNotBlank(punchLotteryPrizesJson)) { + punchLotteryPrizes = JSON.parseArray(punchLotteryPrizesJson, FitnessWalkActivity.PunchLotteryPrize.class); + } else { + JSONObject prizeJsonObject = weAppCloudUtil.request(WeAppCloudUtil.CRUD.QUERY, "db.collection('activity').doc('%s').field({punchLotteryPrizes: true}).get()".formatted(activityId)); + List prizeList = JSON.parseObject((String) prizeJsonObject.getJSONArray("data").get(0)).getJSONArray("punchLotteryPrizes").toJavaList(FitnessWalkActivity.PunchLotteryPrize.class); + redisService.set(redisKey, JSON.toJSONString(prizeList)); + punchLotteryPrizes = prizeList; + } + + List winningUserIndexList = null; + //奖项,抽取多少个 + int sum = punchLotteryPrizes.stream().mapToInt(v -> Integer.parseInt(v.getNum())).sum(); + //判断是否有key,可以获得奖项的下标数组 + if (!redisService.exists(getPrizeIndexesKey)) { + JSONObject jsonObject = weAppCloudUtil.request(WeAppCloudUtil.CRUD.QUERY, "db.collection('register').where({activity_id:'%s'}).count()".formatted(activityId)); + JSONObject pager = jsonObject.getJSONObject("pager"); + String total = pager.getString("Total"); + //报名人数的一半,从这里的人中取数(最多抽取人数) + Integer lotteryNum = (int) Math.ceil(Double.parseDouble(total) / 2); + winningUserIndexList = generateRandomNumber(1, lotteryNum, sum); + redisService.set(getPrizeIndexesKey, JSON.toJSONString(winningUserIndexList)); + } else { + String data = redisService.get(getPrizeIndexesKey); + winningUserIndexList = JSONObject.parseArray(data, Integer.class); + } + + List punchLotteryList = fitnessWalkCommonService.dao().query(FitnessWalkPunchLottery.class, Cnd.where("activityId", "=", activityId)); + + Sys_user user = fitnessWalkCommonService.dao().fetch(Sys_user.class, Cnd.where("id", "=", userId)); + + FitnessWalkPunchLottery punchLottery = new FitnessWalkPunchLottery(); + punchLottery.setId(R.UU32()); + punchLottery.setActivityId(activityId); + punchLottery.setUserId(userId); + punchLottery.setLoginName(user.getLoginname()); + punchLottery.setUserName(user.getUsername()); + punchLottery.setWinDate(new Date()); + punchLottery.setIsRead(false); + punchLottery.setIsWin(false); + punchLottery.setIsExchange(false); + + if (winningUserIndexList.contains((punchLotteryList.size()))) { + + //奖项map + Map prizeMap = punchLotteryPrizes.stream().collect(Collectors.toMap(v -> v.getValue(), v -> v.getName())); + + //奖项池 + List prizeIdList = new ArrayList<>(); + + punchLotteryPrizes.forEach(v -> { + for (int i = 0; i < Integer.parseInt(v.getNum()); i++) { + prizeIdList.add(v.getValue()); + } + }); + + //已经中奖的奖项ID集合 + List winPrizeIdList = punchLotteryList.stream().filter(v -> v.getIsWin()).map(v -> v.getPrizeId()).collect(Collectors.toList()); + winPrizeIdList.forEach(prizeId -> { + int i = prizeIdList.indexOf(prizeId); + prizeIdList.remove(i); + }); + + Random random = new Random(); + if (prizeIdList.size() != 0) { + int index = random.nextInt(prizeIdList.size()); + + // 获取随机元素 + String winingPrizeId = prizeIdList.get(index); + String winingPrizeName = prizeMap.get(winingPrizeId); + punchLottery.setPrizeName(winingPrizeName); + punchLottery.setPrizeId(winingPrizeId); + punchLottery.setIsWin(true); + + } + + } + + fitnessWalkCommonService.dao().insert(punchLottery); + if (punchLottery.getIsWin()) { + return Result.success().addData(punchLottery.getPrizeId()); + } else { + return Result.success().addData(null); + } + } catch (Exception e) { + e.printStackTrace(); + return Result.error(e.getMessage()); + } finally { + if (lock.isLocked()) { + lock.unlock(); + } + } + } + + /** + * 查询用户在指定活动中的打卡抽奖记录。 + * + * @param activityId 健步走云端活动ID + * @param userId 登录接口返回的用户ID + * @return Result,code为0表示成功,data承载查询用户在指定活动中的打卡抽奖记录结果,失败说明见msg + */ + @Override + public Result getLotteryRecord(String activityId, String userId) { + List list = fitnessWalkCommonService.dao().query(FitnessWalkPunchLottery.class, Cnd.where("activityId", "=", activityId) + .and("userId", "=", userId)); + return Result.success(list); + } + + /** + * 核销奖品并记录兑换时间,重复核销返回状态2。 + * + * @param id 待操作记录的主键ID + * @return Result,code为0表示成功,data承载核销奖品并记录兑换时间,重复核销返回状态2结果,失败说明见msg + */ + @Override + @Aop(TransAop.READ_COMMITTED) + public Result doExchange(String id) { + + int count = fitnessWalkCommonService.dao().count(FitnessWalkPunchLottery.class, Cnd.where("id", "=", id).and("isExchange", "=", 1)); + //返回2 "该二维码已兑奖!" + if (count > 0) { + return Result.success(2); + } + + int update = fitnessWalkCommonService.dao().update(FitnessWalkPunchLottery.class, + Chain.make("isExchange", 1).add("exchangeDate", new Date()), + Cnd.where("id", "=", id)); + return update > 0 ? Result.success() : Result.error(); + } + + /** + * 将用户在指定活动中的抽奖记录标记为已读。 + * + * @param activityId 健步走云端活动ID + * @param userId 登录接口返回的用户ID + * @return Result,code为0表示成功,data承载将用户在指定活动中的抽奖记录标记为已读结果,失败说明见msg + */ + @Override + @Aop(TransAop.READ_COMMITTED) + public Result doReadLottery(String activityId, String userId) { + fitnessWalkCommonService.dao().update(FitnessWalkPunchLottery.class, Chain.make("isRead", 1), + Cnd.where("userId", "=", userId).and("activityId", "=", activityId)); + return Result.success(); + } + + /** + * 查询指定打卡活动中已中奖的人员。 + * + * @param activityId 健步走云端活动ID + * @return Result,code为0表示成功,data承载查询指定打卡活动中已中奖的人员结果,失败说明见msg + */ + @Override + public Result getPunchWining(String activityId) { + List punchLotteryList = fitnessWalkCommonService.dao().query(FitnessWalkPunchLottery.class, + Cnd.where("isWin", "=", 1).and("activityId", "=", activityId)); + return Result.success(punchLotteryList); + } + + /** + * 生成不重复的中奖序号,边界规则沿用源版本。 + * + * @param minNum 随机中奖序号下界 + * @param maxNum 随机中奖序号上界 + * @param count 需要抽取的中奖序号数量 + * @return List,用于生成不重复的中奖序号,边界规则沿用源版本 + */ + @Override + public List generateRandomNumber(Integer minNum, Integer maxNum, Integer count) { + Set randomNumbers = new HashSet<>(); + Random rand = new Random(); + //如果是 抽奖人数小于了 奖品数量,会陷入死循环 + if (maxNum <= count) { + for (int i = 0; i <= maxNum; i++) { + randomNumbers.add(i + 1); + } + } else { + while (randomNumbers.size() < count) { + int randomNum = rand.nextInt(maxNum - minNum + 1) + minNum; + randomNumbers.add(randomNum); + } + } + return new ArrayList<>(randomNumbers); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkRelationActivityCompletionServiceImpl.java b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkRelationActivityCompletionServiceImpl.java new file mode 100644 index 0000000..13244f7 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkRelationActivityCompletionServiceImpl.java @@ -0,0 +1,166 @@ +package com.budwk.app.zhgh.activity.fitnesswalk.service.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkActivity; +import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkActivityRelationProject; +import com.budwk.app.zhgh.activity.fitnesswalk.service.FitnessWalkRelationActivityCompletionService; +import com.budwk.app.zhgh.activity.fitnesswalk.utils.WeAppCloudUtil; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.Strings; +import org.nutz.lang.util.NutMap; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 健步走关联活动完成情况实现。 + * 这里统一处理“查关联活动配置 + 查当前登录人的打卡情况 + 计算是否满足达标数量”的完整流程。 + */ +@IocBean(args = {"refer:dao"}) +public class FitnessWalkRelationActivityCompletionServiceImpl extends BaseServiceImpl + implements FitnessWalkRelationActivityCompletionService { + + @Inject + private WeAppCloudUtil weAppCloudUtil; + + public FitnessWalkRelationActivityCompletionServiceImpl(Dao dao) { + super(dao); + } + + /** + * 根据关联项目ID查询当前登录人的关联活动完成情况。 + * 只有当前用户在某个活动中完成了全部点位打卡,该活动才计入“已达标活动数”。 + * 最终必须达到关联项目配置的 standardActivityNum,才算整体合格。 + * @param relationActivityId 关联项目ID,对应fitness_walk_activity_relation_project主键 + * @param userId 小程序登录接口返回的用户ID + */ + @Override + public boolean isCurrentUserRelationActivityQualified(String relationActivityId,String userId) { + if (Strings.isBlank(relationActivityId)) { + throw new RuntimeException("活动关联ID不能为空"); + } + + if (Strings.isBlank(userId)) { + throw new RuntimeException("未获取到当前登录人"); + } + + FitnessWalkActivityRelationProject relationProject = dao().fetch(FitnessWalkActivityRelationProject.class, + Cnd.where("id", "=", relationActivityId)); + if (relationProject == null) { + throw new RuntimeException("未找到对应的活动关联配置"); + } + + List relationActivityList = queryRelationActivityList(relationActivityId); + Map signedPtsNumMap = queryUserSignedPtsNumMap(userId, relationActivityList); + + int qualifiedActivityNum = (int) relationActivityList.stream() + .filter(activity -> isActivityCompleted(activity, signedPtsNumMap)) + .count(); + int standardActivityNum = relationProject.getStandardActivityNum() == null ? 0 : relationProject.getStandardActivityNum(); + return qualifiedActivityNum == standardActivityNum; + } + + /** + * 查询某个关联项目下的所有活动。 + * 这里只取完成情况计算需要的字段,避免无关字段增加云函数返回体积。 + */ + private List queryRelationActivityList(String relationActivityId) { + StringBuilder sql = new StringBuilder("db.collection('activity').where({relationActivityId:'") + .append(relationActivityId) + .append("'}).field({") + .append("_id:true,") + .append("masterActivityName:true,") + .append("branchActivityName:true,") + .append("pts:true,") + .append("relationActivityId:true") + .append("}).orderBy('createdAt','desc').limit(1000).get()"); + + JSONObject jsonObject = weAppCloudUtil.request(WeAppCloudUtil.CRUD.QUERY, sql.toString()); + if (jsonObject.getJSONArray("data") == null || jsonObject.getJSONArray("data").isEmpty()) { + return new ArrayList<>(); + } + + return jsonObject.getJSONArray("data").stream() + .map(item -> JSON.parseObject((String) item, FitnessWalkActivity.class)) + .collect(Collectors.toList()); + } + + /** + * 按活动维度统计当前登录人在 sign 表中已打卡的点位数量。 + * 这里按 pts_code 去重后再计数,避免同一点位重复打卡时把活动误判成已完成。 + */ + private Map queryUserSignedPtsNumMap(String userId, List relationActivityList) { + if (relationActivityList == null || relationActivityList.isEmpty()) { + return Collections.emptyMap(); + } + + List activityIds = relationActivityList.stream() + .map(FitnessWalkActivity::get_id) + .filter(Strings::isNotBlank) + .collect(Collectors.toList()); + if (activityIds.isEmpty()) { + return Collections.emptyMap(); + } + + StringBuilder sql = new StringBuilder("db.collection('sign').aggregate()"); + sql.append(".match({userid:'").append(userId).append("',activity_id:_.in(") + .append(JSON.toJSONString(activityIds)) + .append(")})"); + sql.append(".group({_id:'$activity_id',signedPtsCodeSet:$.addToSet('$pts_code')})"); + sql.append(".project({_id:0,activityId:'$_id',signedPtsNum:$.size('$signedPtsCodeSet')})"); + sql.append(".end()"); + + JSONObject jsonObject = weAppCloudUtil.request(WeAppCloudUtil.CRUD.AGGREGATE, sql.toString()); + if (jsonObject.getJSONArray("data") == null || jsonObject.getJSONArray("data").isEmpty()) { + return Collections.emptyMap(); + } + + return jsonObject.getJSONArray("data").stream().map(item -> { + JSONObject rowData = JSONObject.parseObject((String) item); + String activityId = rowData.getString("activityId"); + int signedPtsNum = parseCloudNumber(rowData.get("signedPtsNum")); + return NutMap.NEW().addv("activityId", activityId).addv("signedPtsNum", signedPtsNum); + }).collect(Collectors.toMap(v -> v.getString("activityId"), v -> v.getInt("signedPtsNum"))); + } + + /** + * 单个活动只有在存在点位,且当前用户已完成该活动全部点位打卡时,才算这个活动达标。 + */ + private boolean isActivityCompleted(FitnessWalkActivity activity, Map signedPtsNumMap) { + int totalPtsNum = activity.getPts() == null ? 0 : activity.getPts().size(); + int signedPtsNum = signedPtsNumMap.getOrDefault(activity.get_id(), 0); + if (signedPtsNum > totalPtsNum && totalPtsNum > 0) { + signedPtsNum = totalPtsNum; + } + return totalPtsNum > 0 && signedPtsNum >= totalPtsNum; + } + + /** + * 云函数聚合返回的数字字段可能是包装对象,这里统一兼容 Integer、Long 和 {$numberInt:xx} 结构。 + */ + private int parseCloudNumber(Object value) { + if (value == null) { + return 0; + } + if (value instanceof Number) { + return ((Number) value).intValue(); + } + if (value instanceof JSONObject) { + JSONObject jsonObject = (JSONObject) value; + if (jsonObject.containsKey("$numberInt")) { + return jsonObject.getIntValue("$numberInt"); + } + if (jsonObject.containsKey("$numberLong")) { + return jsonObject.getIntValue("$numberLong"); + } + } + return Integer.parseInt(String.valueOf(value)); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkStepManageServiceImpl.java b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkStepManageServiceImpl.java new file mode 100644 index 0000000..78ae5fd --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkStepManageServiceImpl.java @@ -0,0 +1,407 @@ +package com.budwk.app.zhgh.activity.fitnesswalk.service.impl; + +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUnit; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.result.Result; +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkActivity; +import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkAwardUser; +import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkStep; +import com.budwk.app.zhgh.activity.fitnesswalk.service.FitnessWalkCommonService; +import com.budwk.app.zhgh.activity.fitnesswalk.service.FitnessWalkStepManageService; +import com.budwk.app.zhgh.activity.fitnesswalk.vo.FitnessWalkDateStepVO; +import com.budwk.app.zhgh.dayofficework.integral.model.IntegralDetail; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.json.Json; +import org.nutz.lang.Lang; +import org.nutz.lang.util.NutMap; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.DecimalFormat; +import java.util.*; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 活动步数与积分业务服务,集中处理查询、数据写入和导出。 + */ +@IocBean +public class FitnessWalkStepManageServiceImpl implements FitnessWalkStepManageService { + + @Inject + private FitnessWalkCommonService fitnessWalkCommonService; + + @Inject + private BaseService baseService; + + @Inject + private Dao dao; + + /** + * 合并微信运动最近30天步数,按每天最大步数保存并计算积分。 + * + * @param activityId 健步走云端活动ID + * @param userId 登录接口返回的用户ID + * @param monthSteps 微信运动步数JSON数组,每项包含step及秒级timestamp + * @return Result,code为0表示成功,data承载合并微信运动最近30天步数,按每天最大步数保存并计算积分结果,失败说明见msg + */ + @Override + @Aop(TransAop.READ_COMMITTED) + public Result updateStepMonth(String activityId, String userId, String monthSteps) { + try { + if (StrUtil.isBlank(userId) || StrUtil.isBlank(monthSteps) || StrUtil.isBlank(activityId)) { + return Result.error("必要参数未传递"); + } + + // 健步走的活动数据 + FitnessWalkActivity activity = fitnessWalkCommonService.getActivity(activityId); + // 是否启用了积分 + boolean isPoints = activity.getIsPoints() != null && activity.getIsPoints(); + + // 使用对象解构获取活动参数 + final Integer standSteps = activity.getLotteryQualificationSteps(); + final Integer standPoints = activity.getLotteryQualificationPoints(); + final Integer otherSteps = activity.getOtherSteps(); + final Integer otherPoints = activity.getOtherPoints(); + final Integer maxPoints = activity.getMaxPoints(); + + String integralNotes = activity.getMasterActivityName() + "所获积分"; + + // 获取到的用户的步数数据 + List steps = Json.fromJsonAsList(NutMap.class, monthSteps); + + //判断数据是否完整 微信运动会在晚上10点多进行步数的更新 会导致31天前的步数获取变为0 所以这里去除掉第一个元素 即为31天前的数据 每次只保存最近30天的数据 + if (Lang.isNotEmpty(steps) && steps.size() == 31) { + steps.remove(0); + } + + // 传递过来的步数数据 + List wxSteps = steps.stream().map(v -> { + FitnessWalkStep step = new FitnessWalkStep(); + step.setActivityId(activityId); + step.setUserId(userId); + step.setStep(v.getInt("step")); + // 计算积分,判断有没有设置,超过达标步数后,每达标XX步数,获得XX积分 + int calculatedPoints = calculatePoints(isPoints, standSteps, standPoints, otherSteps, otherPoints, maxPoints, step.getStep()); + step.setPoints(calculatedPoints); + DateTime date = DateUtil.parse(DateUtil.format(DateUtil.date(v.getLong("timestamp") * 1000), "yyyy-MM-dd"), "yyyy-MM-dd"); + step.setApplyDate(date); + return step; + }).toList(); + + // 传递过来的所有日期的集合 + List dateList = wxSteps.stream().map(FitnessWalkStep::getApplyDate).collect(Collectors.toList()); + + // 去数据库查询日期,这个玩意还要保证插入到数据库的单个日期只有一条,并且这个日期数据库之前有步数,传过来没步数那就要保留数据库的步数 + // 上面日期集合数据库的步数 + List stepList = dao.query(FitnessWalkStep.class, Cnd.where("activityId", "=", activityId) + .and("userId", "=", userId).and("applyDate", "in", dateList)); + + // 这就是保留每天最大步数的集合,那这个集合里面applyDate就是唯一的了 + List dupStepList = new ArrayList<>(stepList.stream() + .collect(Collectors.toMap(FitnessWalkStep::getApplyDate, Function.identity(), + BinaryOperator.maxBy(Comparator.comparing(FitnessWalkStep::getStep)))) + .values()); + + // 这是过滤的相同的applyDate的集合,这些数据要删除 + List delStepIds = stepList.stream() + .filter(step -> !dupStepList.contains(step)) + .map(FitnessWalkStep::getId) + .collect(Collectors.toList()); + + // 数据库的数据 这个map就用于判断数据库的日期和传递过来日期的步数,两个是不是相等的,保留步数多的数据 + Map dateStepMap = dupStepList.stream().collect(Collectors.toMap(FitnessWalkStep::getApplyDate, v -> v)); + + List resultStepList = new ArrayList<>(); + // 这里循环就是,循环的传递过来的数据 + for (FitnessWalkStep step : wxSteps) { + // 数据库存储的步数 + FitnessWalkStep walkStep = dateStepMap.get(step.getApplyDate()); + // 如果数据库没有,那就要新增 + if (Lang.isEmpty(walkStep)) { + resultStepList.add(step); + } else { + // 如果数据库有,那就要保留步数多的 + if (walkStep.getStep() < step.getStep()) { + walkStep.setStep(step.getStep()); + resultStepList.add(walkStep); + } + } + } + + // 最后删除重复日期,更新步数数据 + if (Lang.isNotEmpty(delStepIds)) { + dao.clear(FitnessWalkStep.class, Cnd.where("id", "in", delStepIds)); + if (standSteps != null && standSteps > 0) { + // 删除积分明细 + dao.clear(IntegralDetail.class, Cnd.where("id", "in", delStepIds)); + } + } + if (Lang.isNotEmpty(resultStepList)) { + dao.insertOrUpdate(resultStepList); + if (standSteps != null && standSteps > 0) { + // 增加积分明细 + int year = DateUtil.thisYear(); + List integralDetailList = resultStepList.stream().map(v -> { + IntegralDetail detail = new IntegralDetail(); + detail.setId(v.getId()); + detail.setYear(String.valueOf(year)); + detail.setUserId(userId); + detail.setIntegral(String.valueOf(v.getPoints())); + detail.setIntegralNotes(integralNotes); + detail.setBizId(activityId); + detail.setIntegralTime(v.getApplyDate()); + return detail; + }).toList(); + + List list = integralDetailList.stream().filter(v -> !"0".equals(v.getIntegral())).toList(); + if (Lang.isNotEmpty(list)) { + dao.insertOrUpdate(list); + } + } + } + return Result.success(); + } catch (Exception e){ + e.printStackTrace(); + return Result.error("步数上传失败,请稍后重试,必要时请及时联系管理员"); + } + } + + /** + * 查询活动日期步数,返回step和毫秒级timestamp。 + * + * @param activityId 健步走云端活动ID + * @param userId 登录接口返回的用户ID + * @return Result,code为0表示成功,data承载查询活动日期步数,返回step和毫秒级timestamp结果,失败说明见msg + */ + @Override + public Result getActivityDateStep(String activityId, String userId) { + try { + Cnd cnd = Cnd.NEW(); + FitnessWalkActivity activity = fitnessWalkCommonService.getActivity(activityId); + DateTime startTime = DateUtil.date(activity.getStartTime()); + DateTime endTime = DateUtil.date(activity.getEndTime()); + + cnd.and("activityId", "=", activityId); + cnd.and("userId", "=", userId); + cnd.and("applyDate", ">=", startTime); + cnd.and("applyDate", "<=", endTime); + cnd.asc("applyDate"); + cnd.groupBy("applyDate"); + List stepList = baseService.dao().query(FitnessWalkStep.class, cnd); + + List list = stepList.stream().map(v -> { + // 返回给小程序的折线图数据,timestamp 允许为空以兼容历史异常日期数据。 + Long timestamp = v.getApplyDate() == null ? null : v.getApplyDate().getTime(); + return new FitnessWalkDateStepVO(v.getStep(), timestamp); + }).collect(Collectors.toList()); + return Result.success(list); + + } catch (Exception e) { + return Result.error(e.getMessage()); + } + } + + /** + * 按每日或自定义规则计算用户达标步数与完成进度。 + * + * @param activityId 健步走云端活动ID + * @param userId 登录接口返回的用户ID + * @return Result,code为0表示成功,data承载按每日或自定义规则计算用户达标步数与完成进度结果,失败说明见msg + */ + @Override + public Result getActivityQualifyProgressBar(String activityId, String userId) { + NutMap resultMap = NutMap.NEW(); + + FitnessWalkActivity activity = fitnessWalkCommonService.getActivity(activityId); + //计步的抽奖模式 everyday custom + String lotteryMode = activity.getLotteryMode(); + if ("everyday".equals(lotteryMode)) { + FitnessWalkActivity.DayLotteryRule dayLotteryRule = activity.getDayLotteryRule(); + //每日抽奖资格步数 + Integer lotteryQualificationStep = dayLotteryRule.getLotteryQualificationStep(); + + FitnessWalkStep userWalkStep = baseService.dao().fetch(FitnessWalkStep.class, Cnd.where("userId", "=", userId) + .and("DATE( applyDate )", "=", DateUtil.today()).and("activityId", "=", activityId)); + + int complianceDay = baseService.dao().count(FitnessWalkStep.class, Cnd.where("userId", "=", userId) + .and("DATE( applyDate )", "=", DateUtil.today()).and("activityId", "=", activityId) + .and("step", ">", lotteryQualificationStep)); + + Integer thisDayStep = userWalkStep.getStep(); + + BigDecimal lotteryQualificationStepBig = new BigDecimal(lotteryQualificationStep); + BigDecimal thisDayStepBig = new BigDecimal(thisDayStep); + //达标的小数,保留四位 + BigDecimal divide = thisDayStepBig.divide(lotteryQualificationStepBig, 4, RoundingMode.HALF_UP); + BigDecimal percentValue = divide.multiply(new BigDecimal("100")); + DecimalFormat decimalFormat = new DecimalFormat("#.##"); // 格式化为两位小数 + //百分数 + String formattedPercent = decimalFormat.format(percentValue); + + long betweenDay = DateUtil.date(activity.getStartTime()).between(DateUtil.date(activity.getEndTime()), DateUnit.DAY); + NutMap map = new NutMap(); + map.setv("mode", "everyDayLottery"); + //当前达标步数 + map.setv("userStep", thisDayStep); + //达标总步数 + map.setv("complianceSumStep", lotteryQualificationStep); + //达标比例 + map.setv("complianceProportion", Integer.parseInt(formattedPercent)); + //活动天数 + map.setv("betweenDay", betweenDay); + //达标天数 + map.setv("complianceDay", complianceDay); + resultMap.addv("everyDayLotteryMode", map); + } else if ("custom".equals(lotteryMode)) { + List customLotteryRules = activity.getCustomLotteryRules(); + List userWalkSteps = baseService.dao(). + query(FitnessWalkStep.class, Cnd.where("userId", "=", userId) + .and("activityId", "=", activityId) + .and("DATE(applyDate)", ">=", DateUtil.format(DateUtil.date(activity.getStartTime()), "yyyy-MM-dd")) + .and("DATE(applyDate)", "<=", DateUtil.format(DateUtil.date(activity.getEndTime()), "yyyy-MM-dd")) + ); + + List result = new ArrayList<>(); + + for (FitnessWalkActivity.CustomLotteryRule rule : customLotteryRules) { + NutMap nutMap = NutMap.NEW(); + + List partSteps = userWalkSteps.stream() + .filter(s -> (DateUtil.compare(s.getApplyDate(), rule.getStartDate()) >= 0 && DateUtil.compare(s.getApplyDate(), rule.getEndDate()) <= 0)) + .collect(Collectors.toList()); + + Integer lotteryQualificationDay = rule.getLotteryQualificationDay(); + Integer lotteryQualificationStep = rule.getLotteryQualificationStep(); + + if (lotteryQualificationDay != null && lotteryQualificationStep != null) { + //每天步数 + nutMap.put("mode", "customLotteryEveryDayStepMode"); + //用户总达标步数 + nutMap.put("userSumStep", partSteps.stream().filter(s -> DateUtil.compare(s.getApplyDate(), new Date(), "yyyy-MM-dd") == 0).findFirst().map(s -> s.getStep()).orElse(0)); + //今天达标步数 + nutMap.put("complianceStep", lotteryQualificationStep); + //总达标步数 + nutMap.put("complianceSumStep", lotteryQualificationDay * lotteryQualificationStep); + //用户当前达标总天数 + nutMap.put("userComplianceSumDay", partSteps.stream().filter(s -> s.getStep() > lotteryQualificationStep).count()); + //总达标天数 + nutMap.put("complianceSumDay", lotteryQualificationDay); + } else if (lotteryQualificationDay == null && lotteryQualificationStep != null) { + //总步数 + nutMap.put("mode", "customLotterySumStepMode"); + //用户总达标步数 + nutMap.put("userSumStep", partSteps.stream().mapToInt(s -> s.getStep()).sum()); + //总达标步数 + nutMap.put("complianceSumStep", lotteryQualificationStep); + + nutMap.put("title", DateUtil.format(rule.getStartDate(), "MM月dd日") + "至" + DateUtil.format(rule.getEndDate(), "MM月dd日")); + + } + result.add(nutMap); + } + resultMap.addv("customLotteryMode", result); + } + return Result.success(resultMap); + } + + /** + * 查询指定用户在活动中的中奖记录。 + * + * @param activityId 健步走云端活动ID + * @param userId 登录接口返回的用户ID + * @return Result,code为0表示成功,data承载查询指定用户在活动中的中奖记录结果,失败说明见msg + */ + @Override + public Result winningRecord(String activityId, String userId) { + Sql sql = Sqls.create(""" + select awardName,startDate,endDate from fitness_walk_award_user where activityId=@activityId and userId=@userId and isRead=@isRead + """).setParam("activityId",activityId).setParam("userId", userId).setParam("isRead",true); + + /* List awardUsers = baseService.dao().query(FitnessWalkAwardUser.class, + Cnd.where("activityId", "=", activityId) + .and("userId", "=", userId).and("isRead", "=", true));*/ + return Result.success(baseService.listMap(sql)); + } + + /** + * 标记活动奖项已读,沿用源版活动维度更新范围。 + * + * @param activityId 健步走云端活动ID + * @return Result,code为0表示成功,data承载标记活动奖项已读,沿用源版活动维度更新范围结果,失败说明见msg + */ + @Override + public Result hasReadAward(String activityId) { + return Result.success(); + } + + /** + * 根据基础达标、额外步数和封顶规则计算单日积分。 + * + * @param isPointsEnabled 源页面提交的isPointsEnabled业务参数 + * @param standSteps 源页面提交的standSteps业务参数 + * @param standPoints 源页面提交的standPoints业务参数 + * @param otherSteps 源页面提交的otherSteps业务参数 + * @param otherPoints 源页面提交的otherPoints业务参数 + * @param maxPoints 源页面提交的maxPoints业务参数 + * @param actualSteps 源页面提交的actualSteps业务参数 + * @return int,用于根据基础达标、额外步数和封顶规则计算单日积分 + */ + private int calculatePoints(boolean isPointsEnabled, + Integer standSteps, Integer standPoints, + Integer otherSteps, Integer otherPoints, + Integer maxPoints, int actualSteps) { + // 基础条件检查 + if (!isPointsEnabled || standSteps == null || standPoints == null || actualSteps < standSteps) { + return 0; + } + + // 计算基础积分 + int totalPoints = standPoints; + + // 计算额外积分 + if (hasExtraPoints(otherSteps, otherPoints)) { + int extraSteps = actualSteps - standSteps; + int extraPoints = (extraSteps / otherSteps) * otherPoints; + totalPoints += extraPoints; + } + + // 应用积分上限 + if (hasMaxPointsLimit(maxPoints)) { + return Math.min(totalPoints, maxPoints); + } + + return totalPoints; + } + + /** + * 判断额外步数和积分规则是否均配置为有效正数。 + * + * @param otherSteps 源页面提交的otherSteps业务参数 + * @param otherPoints 源页面提交的otherPoints业务参数 + * @return boolean,用于判断额外步数和积分规则是否均配置为有效正数 + */ + private boolean hasExtraPoints(Integer otherSteps, Integer otherPoints) { + return otherSteps != null && otherPoints != null && otherSteps > 0 && otherPoints > 0; + } + + /** + * 判断是否配置了有效的单日积分上限。 + * + * @param maxPoints 源页面提交的maxPoints业务参数 + * @return boolean,用于判断是否配置了有效的单日积分上限 + */ + private boolean hasMaxPointsLimit(Integer maxPoints) { + return maxPoints != null && maxPoints > 0; + } +} diff --git a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkStepStatisticsServiceImpl.java b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkStepStatisticsServiceImpl.java new file mode 100644 index 0000000..92a1370 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkStepStatisticsServiceImpl.java @@ -0,0 +1,149 @@ +package com.budwk.app.zhgh.activity.fitnesswalk.service.impl; + +import cn.afterturn.easypoi.excel.ExcelExportUtil; +import cn.afterturn.easypoi.excel.entity.ExportParams; +import cn.afterturn.easypoi.excel.entity.enmus.ExcelType; +import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.dao.CndPlus; +import com.budwk.app.base.param.PageForm; +import com.budwk.app.base.result.Result; +import com.budwk.app.base.service.BaseService; +import com.budwk.app.base.utils.CommonDownloadUtil; +import com.budwk.app.zhgh.activity.fitnesswalk.service.FitnessWalkStepStatisticsService; +import org.apache.poi.ss.usermodel.Workbook; +import org.nutz.dao.Cnd; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.dao.util.cri.SqlExpressionGroup; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.Lang; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.annotation.Ok; +import java.util.*; +import javax.servlet.http.HttpServletResponse; + +/** + * 计步明细统计业务服务,集中处理查询、数据写入和导出。 + */ +@IocBean +public class FitnessWalkStepStatisticsServiceImpl implements FitnessWalkStepStatisticsService { + + @Inject + private BaseService baseService; + + /** + * 构建每日步数与用户单位信息的联查语句。 + * + * @return String,用于构建每日步数与用户单位信息的联查语句 + */ + private String getSql() { + return """ + SELECT + u.loginname, + u.username, + u.unionname, + u.unitname, + al.applyDate, + al.step, + date_format(al.applyDate,'%Y-%m-%d') as prizeData + FROM + `fitness_walk_step` al + LEFT JOIN vw_user u ON u.id = al.userId + $condition + """; + } + + /** + * 按活动、日期、工号及组织条件分页统计用户每日步数。 + * + * @param pageForm 分页、查询字段和排序参数,页码从1开始 + * @param year 查询年度,四位年份 + * @param activityId 健步走云端活动ID + * @param activityDate 起止日期数组,两项均为yyyy-MM-dd + * @param unionId 分工会ID,空值表示不追加筛选,已有数据权限仍生效 + * @param unitId 单位ID,空值表示不追加单位筛选 + * @return Result,code为0表示成功,data承载按活动、日期、工号及组织条件分页统计用户每日步数结果,失败说明见msg + */ + @Override + public Result pageData(PageForm pageForm, + Integer year, + String activityId, + String[] activityDate, + String unionId, + String unitId) { + Cnd cnd = Cnd.NEW(); + Sql sql = Sqls.create(getSql()); + if (Lang.isNotEmpty(activityDate)) { + cnd.and("date_format(al.applyDate,'%Y-%m-%d')", ">=", activityDate[0]); + cnd.and("date_format(al.applyDate,'%Y-%m-%d')", "<=", activityDate[1]); + } + if(StrUtil.isNotBlank(pageForm.getSearchKeyword())){ + SqlExpressionGroup seg = new SqlExpressionGroup(); + seg.orLike("u.username",pageForm.getSearchKeyword()); + seg.orLike("u.loginname",pageForm.getSearchKeyword()); + cnd.and(seg); + } + cnd.andEX("YEAR(al.applyDate)", "=", year); + cnd.andEX("al.activityId", "=", activityId); + cnd.andEX("u.unionid", "=", unionId); + cnd.andEX("u.unitId", "=", unitId); + cnd.desc("al.applyDate"); + cnd.desc("u.unioncode"); + cnd.groupBy("al.applyDate", "al.userId"); + + sql.setCondition(cnd); + return Result.success(baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql)); + } + + /** + * 按当前活动和日期条件导出计步明细。 + * + * @param activityId 健步走云端活动ID + * @param activityDate 起止日期数组,两项均为yyyy-MM-dd + * @param unionId 分工会ID,空值表示不追加筛选,已有数据权限仍生效 + * @param unitId 单位ID,空值表示不追加单位筛选 + * @param response HTTP响应,用于写入XSSF格式Excel文件流 + * 无返回对象,通过response写出Excel文件流。 + */ + @Override + @Ok("void") + public void exportExcel(String activityId, + String[] activityDate, + String unionId, + String unitId, HttpServletResponse response) { + CndPlus cnd = CndPlus.create(); + Sql sql = Sqls.create(getSql()); + if (activityDate.length > 0) { + cnd.and("date_format(al.applyDate,'%Y-%m-%d')", ">=", activityDate[0]); + cnd.and("date_format(al.applyDate,'%Y-%m-%d')", "<=", activityDate[1]); + } + cnd.andEX("al.activityId", "=", activityId); + cnd.andEX("u.unionid", "=", unionId); + cnd.andEX("u.unitId", "=", unitId); + cnd.desc("al.applyDate"); + cnd.desc("u.unioncode"); + sql.setCondition(cnd); + + List exportList = baseService.listMap(sql); + + List entityList = new ArrayList<>() {{ + add(new ExcelExportEntity("姓名", "username", 20)); + add(new ExcelExportEntity("工号", "loginname", 20)); + add(new ExcelExportEntity("单位", "unitname", 20)); + add(new ExcelExportEntity("分工会", "unionname", 20)); + add(new ExcelExportEntity("当前步数", "step", 20)); + add(new ExcelExportEntity("日期", "prizeData", 20)); + }}; + + try { + ExportParams exportParams = new ExportParams(); + exportParams.setType(ExcelType.XSSF); + Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entityList, exportList); + CommonDownloadUtil.download("计步步数统计表.xlsx", workbook, response); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/vo/FitnessWalkDateStepVO.java b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/vo/FitnessWalkDateStepVO.java new file mode 100644 index 0000000..1c61795 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/vo/FitnessWalkDateStepVO.java @@ -0,0 +1,21 @@ +package com.budwk.app.zhgh.activity.fitnesswalk.vo; + +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 小程序活动步数日期数据:step 表示当天步数,timestamp 表示 applyDate 对应的毫秒时间戳。 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class FitnessWalkDateStepVO implements Serializable { + + private static final long serialVersionUID = 1L; + + private Integer step; + + private Long timestamp; +} diff --git a/src/main/java/com/budwk/app/zhgh/map/TMapController.java b/src/main/java/com/budwk/app/zhgh/map/TMapController.java new file mode 100644 index 0000000..1635448 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/map/TMapController.java @@ -0,0 +1,33 @@ +package com.budwk.app.zhgh.map; + +import cn.dev33.satoken.annotation.SaCheckLogin; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.result.Result; +import com.budwk.app.zhgh.activity.fitnesswalk.service.FitnessWalkActivityService; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +/** 健步走腾讯地图地址检索入口。 */ +@IocBean +@Ok("json") +@At("/platform/TMap") +public class TMapController { + @Inject + private FitnessWalkActivityService fitnessWalkActivityService; + + /** + * 检索活动点位的候选地址。 + * @param keyword 用户输入的地名或地址关键字,不能为空 + * @return Result,data.status为腾讯地图状态,data.data为含title、address、location的候选列表 + */ + @At + @SaCheckLogin + public Result suggestion(String keyword) { + if (StrUtil.isBlank(keyword)) { + return Result.error("关键词不能为空"); + } + return fitnessWalkActivityService.suggestMapAddress(keyword); + } +} diff --git a/src/main/resources/static/components/plugins/mapContainer/TMap.vue b/src/main/resources/static/components/plugins/mapContainer/TMap.vue new file mode 100644 index 0000000..d13670a --- /dev/null +++ b/src/main/resources/static/components/plugins/mapContainer/TMap.vue @@ -0,0 +1,163 @@ + + + + + diff --git a/src/main/resources/views/platform/zhgh/activity/fitnesswalk/activityRelation.html b/src/main/resources/views/platform/zhgh/activity/fitnesswalk/activityRelation.html new file mode 100644 index 0000000..04548a2 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/activity/fitnesswalk/activityRelation.html @@ -0,0 +1,229 @@ + +
+ + + + + + + + 创建关联项目 + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/src/main/resources/views/platform/zhgh/activity/fitnesswalk/userPunchStatistics.html b/src/main/resources/views/platform/zhgh/activity/fitnesswalk/userPunchStatistics.html new file mode 100644 index 0000000..19aa5f9 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/activity/fitnesswalk/userPunchStatistics.html @@ -0,0 +1,281 @@ + +
+ + + +
+ + + +