diff --git a/pom.xml b/pom.xml index 915519e..e9cef7b 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 jar com.budwk - zhgh_jsahvc + zhgh_nwnu 5.6.0-plus 2.6.0-SNAPSHOT @@ -487,6 +487,12 @@ pom import + + + org.nutz + nutz + ${nutz.version} + javax.xml.bind jaxb-api diff --git a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/controller/FitnessWalkLoginController.java b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/controller/FitnessWalkLoginController.java index f1756b2..852ec4f 100644 --- a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/controller/FitnessWalkLoginController.java +++ b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/controller/FitnessWalkLoginController.java @@ -1,17 +1,23 @@ package com.budwk.app.zhgh.activity.fitnesswalk.controller; +import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; import com.budwk.app.base.result.Result; import com.budwk.app.sys.services.SysUserService; +import com.budwk.app.zhgh.activity.fitnesswalk.service.FitnessWalkCommonService; +import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.dao.Cnd; 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.lang.util.NutMap; import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.Ok; +import java.util.Date; + /** * @author: Aaron * @create: 2020-09-11 13:50 @@ -25,6 +31,9 @@ public class FitnessWalkLoginController { @Inject private SysUserService sysUserService; + @Inject + private FitnessWalkCommonService fitnessWalkCommonService; + @At("/") public Result index(String username, String loginname) { try { @@ -72,6 +81,49 @@ public class FitnessWalkLoginController { return Result.error(); } } + + /** + * 补全健步走用户基础资料。 + * + * @param userId 用户ID,传登录接口返回的 userid + * @param sex 性别,只能传“男”或“女” + * @param birthday 出生日期,格式为 yyyy-MM-dd,且不能晚于当前日期 + * @return Result,data 中包含最终生效的 sex 和 birthday + */ + @At("/completeUserInfo") + @Aop(TransAop.READ_COMMITTED) + public Result completeUserInfo(String userId, String sex, String birthday) { + if (StrUtil.hasBlank(userId, sex, birthday)) { + return Result.error().addMsg("请完整填写性别和出生日期!"); + } + if (!"男".equals(sex) && !"女".equals(sex)) { + return Result.error().addMsg("性别参数不正确!"); + } + + Date birthdayDate; + try { + birthdayDate = DateUtil.parse(birthday, "yyyy-MM-dd"); + if (!birthday.equals(DateUtil.format(birthdayDate, "yyyy-MM-dd"))) { + return Result.error().addMsg("出生日期格式不正确!"); + } + } catch (Exception e) { + return Result.error().addMsg("出生日期格式不正确!"); + } + + if (birthdayDate.after(new Date())) { + return Result.error().addMsg("出生日期不能晚于当前日期!"); + } + + try { + NutMap userInfo = fitnessWalkCommonService.completeUserInfo(userId, sex, birthdayDate); + if (userInfo == null) { + return Result.error().addMsg("系统查询不到您的信息,请重新登录!"); + } + return Result.success().addData(userInfo); + } catch (Exception e) { + return Result.error().addMsg("个人信息保存失败,请稍后重试!"); + } + } } diff --git a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/controller/FitnessWalkStepRankingController.java b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/controller/FitnessWalkStepRankingController.java index 0c345e7..3d46c0c 100644 --- a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/controller/FitnessWalkStepRankingController.java +++ b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/controller/FitnessWalkStepRankingController.java @@ -60,165 +60,96 @@ public class FitnessWalkStepRankingController { } + /** + * 查询PC端步数排行榜。 + * + * @param pageForm 分页及姓名、工号查询参数,页码从1开始 + * @param activityId 活动ID,用于限定排行榜所属活动 + * @param unionId 分工会ID,空字符串表示全部分工会 + * @param unitId 单位ID,空字符串表示全部单位 + * @param mode 排行范围:today 表示今日,all 表示活动期间 + * @param sex 性别:空字符串表示全部,男或女表示指定性别 + * @param ageRange 年龄段:空字符串表示全体,under40、41to50、51to55 表示指定年龄段 + * @return Result,data.list 为排行榜数据,data.totalCount 为符合条件的总人数 + */ @At @SaCheckPermission("fitnessWalk.stepRanking") public Result pageData(PageForm pageForm, @Param("activityId") String activityId, @Param("unionId") String unionId, @Param("unitId") String unitId, - @Param("mode") String mode) { - return Result.success(fitnessWalkCommonService.getStepRankingPagination(pageForm, activityId, unionId, unitId, mode)); + @Param("mode") String mode, + @Param("sex") String sex, + @Param("ageRange") String ageRange) { + if (Strings.isNotBlank(sex) && !List.of("男", "女").contains(sex)) { + return Result.error("性别筛选参数不正确"); + } + if (Strings.isNotBlank(ageRange) && !List.of("under40", "41to50", "51to55").contains(ageRange)) { + return Result.error("年龄段筛选参数不正确"); + } + return Result.success(fitnessWalkCommonService.getStepRankingPagination( + pageForm, + activityId, + unionId, + unitId, + mode, + sex, + ageRange + )); } /** - * 获取所有教工 今天的步数及活动时间范围内的步数 + * 查询小程序筛选排行榜。 * - * @param activityId - * @param mode day:今天 all:所有 - * @return {@link Object} + * @param activityId 活动ID,用于限定健步走活动数据 + * @param userId 当前登录用户ID,用于返回“我的指标”和“我的名次” + * @param rankingType 排行指标:totalSteps 累计步数、completionRate 目标完成率、complianceDays 达标天数 + * @param sex 性别筛选:空字符串表示全部,男或女表示指定性别 + * @param ageRange 年龄段:空字符串表示全体,under40、41to50、51to55 表示指定年龄段 + * @param pageNumber 页码,从1开始 + * @param pageSize 每页条数,最大50条 + * @return Result,data 包含 list 排名列表、分页信息以及 currentUser 当前用户指标 */ @At @Ok("json:full") - @SLog(type = "校工会特色活动", tag = "健身走-计步步数排行榜", msg = "小程序接口(获取今日步数及活动范围内步数)", param = true, result = true) - public Result getUserStepRanking(String activityId, String mode, Integer pageNumber, Integer pageSize) { - if ("day".equals(mode)) { - Sql sql = Sqls.create(""" - SELECT - t.step, - t.applyDate, - u.username - FROM - `fitness_walk_step` t - LEFT JOIN sys_user u ON u.id = t.userId - WHERE - t.activityId = @activityId - AND t.applyDate = @today - GROUP BY u.id - ORDER BY t.step DESC - """); - sql.setParam("activityId", activityId); - sql.setParam("today", DateUtil.today()); - return Result.success(baseService.listPageMap(pageNumber, 50, sql)); - } else if ("week".equals(mode)) { - Date startOfWeek = DateUtil.beginOfWeek(new Date()); - Date endOfWeek = DateUtil.endOfWeek(new Date()); + @SLog(type = "校工会特色活动", tag = "健身走-计步步数排行榜", msg = "小程序接口(获取活动范围内步数排行榜)", param = true, result = true) + public Result getUserStepRanking(String activityId, + String userId, + String rankingType, + String sex, + String ageRange, + Integer pageNumber, + Integer pageSize) { + if (Strings.isBlank(activityId) || Strings.isBlank(userId)) { + return Result.error("活动ID和用户ID不能为空"); + } - Sql sql = Sqls.create(""" - SELECT - t.userId, - sum( t.step ) AS step, - u.username - FROM - `fitness_walk_step` t - LEFT JOIN sys_user u ON u.id = t.userId - WHERE - t.activityId = @activityId - AND t.applyDate >= @startTime - AND t.applyDate <= @endTime - GROUP BY - t.userId - ORDER BY - step DESC - """); - sql.setParam("activityId", activityId); - sql.setParam("startTime", startOfWeek); - sql.setParam("endTime", endOfWeek); - return Result.success(baseService.listPageMap(pageNumber, 50, sql)); - } else if ("month".equals(mode)) { - Date firstDayOfMonth = DateUtil.beginOfMonth(new Date()); - Date lastDayOfMonth = DateUtil.endOfMonth(new Date()); - Sql sql = Sqls.create(""" - SELECT - t.userId, - sum( t.step ) AS step, - u.username - FROM - `fitness_walk_step` t - LEFT JOIN sys_user u ON u.id = t.userId - WHERE - t.activityId = @activityId - AND t.applyDate >= @startTime - AND t.applyDate <= @endTime - GROUP BY - t.userId - ORDER BY - step DESC - """); - sql.setParam("activityId", activityId); - sql.setParam("startTime", firstDayOfMonth); - sql.setParam("endTime", lastDayOfMonth); - return Result.success(baseService.listPageMap(pageNumber, 50, sql)); - } else if ("all".equals(mode)) { - FitnessWalkActivity activity = fitnessWalkCommonService.getActivity(activityId); - DateTime startTime = DateUtil.date(activity.getStartTime()); - DateTime endTime = DateUtil.date(activity.getEndTime()); - Sql sql = Sqls.create(""" - SELECT - SUM(sub.max_step) AS step, - u.username - FROM ( - SELECT - userId, - activityId, - applyDate, - MAX(step) AS max_step - FROM - fitness_walk_step - WHERE - activityId = @activityId - AND applyDate >= @startTime AND applyDate <= @endTime - GROUP BY - userId, - activityId, - applyDate - ) sub - left join vw_user u ON u.id = sub.userId - GROUP BY - userId - ORDER BY - step desc - """); - sql.setParam("activityId", activityId); - sql.setParam("startTime", startTime); - sql.setParam("endTime", endTime); - return Result.success(baseService.listPageMap(pageNumber, 50, sql)); - } else if ("points".equals(mode)) { - FitnessWalkActivity activity = fitnessWalkCommonService.getActivity(activityId); - DateTime startTime = DateUtil.date(activity.getStartTime()); - DateTime endTime = DateUtil.date(activity.getEndTime()); - Sql sql = Sqls.create(""" - SELECT - SUM(sub.max_step) AS step, - u.username - FROM ( - SELECT - userId, - activityId, - applyDate, - MAX(points) AS max_step - FROM - fitness_walk_step - WHERE - activityId = @activityId - AND applyDate >= @startTime AND applyDate <= @endTime - GROUP BY - userId, - activityId, - applyDate - ) sub - left join vw_user u ON u.id = sub.userId - GROUP BY - userId - ORDER BY - step desc - """); - sql.setParam("activityId", activityId); - sql.setParam("startTime", startTime); - sql.setParam("endTime", endTime); - return Result.success(baseService.listPageMap(pageNumber, 50, sql)); - } else { - return Result.error("不支持的排行榜模式"); + String actualRankingType = Strings.isBlank(rankingType) ? "totalSteps" : rankingType; + if (!List.of("totalSteps", "completionRate", "complianceDays").contains(actualRankingType)) { + return Result.error("不支持的排行榜指标"); + } + if (Strings.isNotBlank(sex) && !List.of("男", "女").contains(sex)) { + return Result.error("性别筛选参数不正确"); + } + if (Strings.isNotBlank(ageRange) && !List.of("under40", "41to50", "51to55").contains(ageRange)) { + return Result.error("年龄段筛选参数不正确"); + } + + int actualPageNumber = pageNumber == null || pageNumber < 1 ? 1 : pageNumber; + int actualPageSize = pageSize == null || pageSize < 1 ? 20 : Math.min(pageSize, 50); + try { + return Result.success(fitnessWalkCommonService.getUserStepRanking( + activityId, + userId, + actualRankingType, + sex, + ageRange, + actualPageNumber, + actualPageSize + )); + } catch (IllegalArgumentException e) { + return Result.error(e.getMessage()); } } @@ -278,7 +209,19 @@ public class FitnessWalkStepRankingController { } } - + /** + * 导出PC端当前排行榜。 + * + * @param searchName 姓名或工号对应的查询字段 + * @param searchKeyword 姓名或工号查询内容 + * @param activityId 活动ID + * @param unionId 分工会ID,空字符串表示全部 + * @param unitId 单位ID,空字符串表示全部 + * @param mode 排行范围:today表示今日,all表示活动期间 + * @param sex 性别:空字符串表示全部,男或女表示指定性别 + * @param ageRange 年龄段:空字符串表示全部,under40、41to50、51to55表示指定年龄段 + * @param response HTTP响应,返回XSSF格式的Excel文件流 + */ @At @Ok("void") @SLog(type = "校工会特色活动", tag = "健身走-计步步数排行榜", msg = "步数排行榜导出", param = true, result = true) @@ -289,18 +232,38 @@ public class FitnessWalkStepRankingController { @Param("unionId") String unionId, @Param("unitId") String unitId, @Param("mode") String mode, + @Param("sex") String sex, + @Param("ageRange") String ageRange, HttpServletResponse response) { - List stepRankingList = fitnessWalkCommonService.getStepRankingList(searchName, searchKeyword, activityId, unionId, unitId, mode); + if (Strings.isNotBlank(sex) && !List.of("男", "女").contains(sex)) { + throw new IllegalArgumentException("性别筛选参数不正确"); + } + if (Strings.isNotBlank(ageRange) && !List.of("under40", "41to50", "51to55").contains(ageRange)) { + throw new IllegalArgumentException("年龄段筛选参数不正确"); + } + List stepRankingList = fitnessWalkCommonService.getStepRankingList( + searchName, + searchKeyword, + activityId, + unionId, + unitId, + mode, + sex, + ageRange + ); List entityList = new ArrayList<>(); - ExcelExportEntity no = new ExcelExportEntity("序号", "no", 10); + ExcelExportEntity no = new ExcelExportEntity("排名", "no", 10); no.setFormat("isAddIndex"); entityList.add(no); entityList.add(new ExcelExportEntity("姓名", "username", 20)); entityList.add(new ExcelExportEntity("工号", "loginname", 20)); + entityList.add(new ExcelExportEntity("性别", "sex", 10)); + entityList.add(new ExcelExportEntity("年龄", "age", 10)); entityList.add(new ExcelExportEntity("分工会", "unionname", 20)); entityList.add(new ExcelExportEntity("单位", "unitname", 20)); - entityList.add(new ExcelExportEntity("today".equals(mode) ? "今日步数" : "活动期间总步数", "total_steps", 20)); + entityList.add(new ExcelExportEntity("步数", "total_steps", 20)); + entityList.add(new ExcelExportEntity("达标天数", "standardsDays", 15)); try { String fileName = ("today".equals(mode) ? DateUtil.format(new Date(), "yyyy年MM月dd日") : "活动期间总") + "步数榜.xlsx"; ExportParams exportParams = new ExportParams(); diff --git a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkCommonService.java b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkCommonService.java index 137c199..a03c8dc 100644 --- a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkCommonService.java +++ b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/FitnessWalkCommonService.java @@ -9,6 +9,7 @@ import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkActivity; import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkAwardUser; import org.nutz.lang.util.NutMap; +import java.util.Date; import java.util.List; public interface FitnessWalkCommonService extends BaseService { @@ -64,15 +65,64 @@ public interface FitnessWalkCommonService extends BaseService getStepRankingList(String searchName, String searchKeyword, String activityId, String unionId, String unitId, String mode); + /** + * 查询PC端排行榜导出数据,查询条件及返回字段与页面表格保持一致。 + * + * @param searchName 姓名或工号对应的查询字段 + * @param searchKeyword 姓名或工号查询内容 + * @param activityId 活动ID + * @param unionId 分工会ID,空字符串表示全部 + * @param unitId 单位ID,空字符串表示全部 + * @param mode 排行范围:today表示今日,all表示活动期间 + * @param sex 性别:空字符串表示全部,男或女表示指定性别 + * @param ageRange 年龄段:空字符串表示全部,under40、41to50、51to55表示指定年龄段 + * @return 排行榜导出数据,包含页面表格展示的用户资料、步数和达标天数 + */ + List getStepRankingList(String searchName, + String searchKeyword, + String activityId, + String unionId, + String unitId, + String mode, + String sex, + String ageRange); + + /** + * 查询小程序筛选排行榜。 + * + * @param activityId 活动ID + * @param userId 当前登录用户ID + * @param rankingType 排行指标:totalSteps、completionRate、complianceDays + * @param sex 性别筛选,空字符串表示全部 + * @param ageRange 年龄段筛选,空字符串表示全体 + * @param pageNumber 页码,从1开始 + * @param pageSize 每页条数 + * @return 包含排名列表、分页信息以及当前用户指标的 NutMap + */ + NutMap getUserStepRanking(String activityId, + String userId, + String rankingType, + String sex, + String ageRange, + Integer pageNumber, + Integer pageSize); /** @@ -90,6 +140,16 @@ public interface FitnessWalkCommonService extends BaseService prizeOption(String activityId); + /** + * 补全健步走用户的基础资料。 + * + * @param userId 用户ID,用于定位 sys_user 中的当前用户 + * @param sex 性别,当前接口只接受“男”或“女” + * @param birthday 出生日期,不能晚于当前日期 + * @return 包含 sex、birthday 的 NutMap;用户不存在时返回 null + */ + NutMap completeUserInfo(String userId, String sex, Date birthday); + } diff --git a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkCommonServiceImpl.java b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkCommonServiceImpl.java index 3614b84..0ed3672 100644 --- a/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkCommonServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/activity/fitnesswalk/service/impl/FitnessWalkCommonServiceImpl.java @@ -3,6 +3,7 @@ package com.budwk.app.zhgh.activity.fitnesswalk.service.impl; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.date.DateField; import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUnit; import cn.hutool.core.date.DateUtil; import com.budwk.app.base.page.Pagination; import com.alibaba.fastjson.JSON; @@ -13,6 +14,7 @@ import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.sys.models.Sys_task; import com.budwk.app.sys.models.Sys_user; import com.budwk.app.sys.services.SysTaskService; +import com.budwk.app.sys.services.SysUserService; import com.budwk.app.task.services.TaskPlatformService; import com.budwk.app.zhgh.activity.fitnesswalk.contants.Cascader; import com.budwk.app.zhgh.activity.fitnesswalk.contants.FitnessWalkMode; @@ -21,6 +23,7 @@ import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkAwardUser; import com.budwk.app.zhgh.activity.fitnesswalk.service.FitnessWalkCommonService; import com.budwk.app.zhgh.activity.fitnesswalk.utils.WeAppCloudUtil; import org.nutz.dao.Cnd; +import org.nutz.dao.Chain; import org.nutz.dao.Dao; import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.dao.Sqls; @@ -54,6 +57,9 @@ public class FitnessWalkCommonServiceImpl extends BaseServiceImpl getStepRankingList(String searchName, String searchKeyword, String activityId, String unionId, String unitId, String mode) { - Sql sql = rankingListSql(searchName, searchKeyword, activityId, unionId, unitId, mode); + public List getStepRankingList(String searchName, + String searchKeyword, + String activityId, + String unionId, + String unitId, + String mode, + String sex, + String ageRange) { + Sql sql = rankingListSql(searchName, searchKeyword, activityId, unionId, unitId, mode, sex, ageRange); return listMap(sql); } + /** + * 查询小程序筛选排行榜。查询先按用户和日期保留最大步数,避免同一天重复上传造成累计值偏高; + * 再在活动有效时间范围内根据性别和年龄段汇总累计步数、目标完成率及达标天数。 + * + * @param activityId 活动ID + * @param userId 当前登录用户ID,用于定位当前用户行和名次 + * @param rankingType totalSteps、completionRate 或 complianceDays + * @param sex 空字符串、男或女 + * @param ageRange 空字符串、under40、41to50 或 51to55 + * @param pageNumber 页码,从1开始 + * @param pageSize 每页条数 + * @return list 为当前页数据,currentUser 为当前用户指标,其他字段为分页信息 + */ + @Override + public NutMap getUserStepRanking(String activityId, + String userId, + String rankingType, + String sex, + String ageRange, + Integer pageNumber, + Integer pageSize) { + FitnessWalkActivity activity = getRankingActivity(activityId); + if (activity == null || activity.getStartTime() == null || activity.getEndTime() == null) { + throw new IllegalArgumentException("活动不存在或活动时间配置不完整"); + } + + DateTime queryStartDate = DateUtil.beginOfDay(DateUtil.date(activity.getStartTime())); + DateTime queryEndDate = DateUtil.endOfDay(new Date()); + DateTime activityEndDate = DateUtil.endOfDay(DateUtil.date(activity.getEndTime())); + if (queryEndDate.isAfter(activityEndDate)) { + queryEndDate = activityEndDate; + } + + if (queryStartDate.isAfter(queryEndDate)) { + return emptyRankingResult(pageNumber, pageSize, userId); + } + + int targetStep = resolveRankingTargetStep(activity, rankingType, queryStartDate, queryEndDate); + long periodDays = Math.max(1L, DateUtil.between(queryStartDate, queryEndDate, DateUnit.DAY) + 1L); + + List filteredRows = queryRankingRows( + activityId, + rankingType, + sex, + ageRange, + null, + queryStartDate, + queryEndDate, + targetStep, + periodDays + ); + + List rankingRows = new ArrayList<>(filteredRows.size()); + NutMap currentUser = null; + NutMap previousRow = null; + Integer previousRank = null; + for (int index = 0; index < filteredRows.size(); index++) { + NutMap row = filteredRows.get(index); + // 三项排名指标完全相同时沿用上一名次;指标不同时在上一名次基础上递增,形成 1、2、2、3 的稠密排名。 + Integer rank = previousRow != null && hasSameRankingMetrics(previousRow, row) + ? previousRank + : previousRank == null ? 1 : previousRank + 1; + NutMap rankingRow = normalizeRankingRow(row, rank, userId, rankingType); + rankingRows.add(rankingRow); + if (Boolean.TRUE.equals(rankingRow.getBoolean("currentUser"))) { + currentUser = rankingRow; + } + previousRow = row; + previousRank = rank; + } + + // 当前用户不符合所选性别或年龄段时仍返回个人指标,但不伪造其在当前筛选范围内的名次。 + if (currentUser == null) { + List currentUserRows = queryRankingRows( + activityId, + rankingType, + null, + null, + userId, + queryStartDate, + queryEndDate, + targetStep, + periodDays + ); + if (Lang.isNotEmpty(currentUserRows)) { + currentUser = normalizeRankingRow(currentUserRows.get(0), null, userId, rankingType); + } else { + Sys_user user = sysUserService.fetch(userId); + currentUser = NutMap.NEW() + .addv("userId", userId) + .addv("username", user == null ? "我" : user.getUsername()) + .addv("rank", null) + .addv("value", 0) + .addv("currentUser", true); + } + } + + int totalCount = rankingRows.size(); + int totalPage = totalCount == 0 ? 0 : (int) Math.ceil(totalCount * 1.0D / pageSize); + int fromIndex = Math.min((pageNumber - 1) * pageSize, totalCount); + int toIndex = Math.min(fromIndex + pageSize, totalCount); + List pageList = new ArrayList<>(rankingRows.subList(fromIndex, toIndex)); + + return NutMap.NEW() + .addv("list", pageList) + .addv("pageNumber", pageNumber) + .addv("pageSize", pageSize) + .addv("totalCount", totalCount) + .addv("totalPage", totalPage) + .addv("currentUser", currentUser); + } + + /** + * 读取排行榜计算所需的活动配置。排行榜不需要封面、证书等临时文件地址, + * 因此缓存命中时直接反序列化活动数据,避免重复访问微信云数据库和文件接口。 + * + * @param activityId 活动ID,对应 Redis 中的 FitnessWalk:{activityId} + * @return 活动配置;缓存不存在时回退到现有活动查询逻辑并自动写入缓存 + */ + private FitnessWalkActivity getRankingActivity(String activityId) { + String activityJson = redisService.get("FitnessWalk:" + activityId); + if (Strings.isNotBlank(activityJson)) { + return JSONObject.parseObject(activityJson, FitnessWalkActivity.class); + } + return getActivity(activityId); + } + + /** + * 获取活动达标步数。普通计步活动优先使用 lotteryQualificationSteps; + * 每日或自定义抽奖活动则使用对应规则中的有效步数,无法取得时不允许计算完成率和达标天数。 + */ + private int resolveRankingTargetStep(FitnessWalkActivity activity, + String rankingType, + Date queryStartDate, + Date queryEndDate) { + Integer targetStep = activity.getLotteryQualificationSteps(); + if (targetStep == null || targetStep <= 0) { + FitnessWalkActivity.DayLotteryRule dayLotteryRule = activity.getDayLotteryRule(); + if (dayLotteryRule != null && dayLotteryRule.getLotteryQualificationStep() != null + && dayLotteryRule.getLotteryQualificationStep() > 0) { + targetStep = dayLotteryRule.getLotteryQualificationStep(); + } + } + if ((targetStep == null || targetStep <= 0) && Lang.isNotEmpty(activity.getCustomLotteryRules())) { + for (FitnessWalkActivity.CustomLotteryRule rule : activity.getCustomLotteryRules()) { + if (rule.getLotteryQualificationStep() == null || rule.getLotteryQualificationStep() <= 0) { + continue; + } + boolean hasStartOverlap = rule.getEndDate() == null || !rule.getEndDate().before(queryStartDate); + boolean hasEndOverlap = rule.getStartDate() == null || !rule.getStartDate().after(queryEndDate); + if (hasStartOverlap && hasEndOverlap) { + targetStep = rule.getLotteryQualificationStep(); + break; + } + } + } + + if (targetStep == null || targetStep <= 0) { + if ("totalSteps".equals(rankingType)) { + return 1; + } + throw new IllegalArgumentException("当前活动未配置有效达标步数,无法计算该排行榜"); + } + return targetStep; + } + + /** + * 执行排行榜聚合查询。userId 不为空时仅查询当前用户,用于其被筛选条件排除后的个人指标展示。 + */ + private List queryRankingRows(String activityId, + String rankingType, + String sex, + String ageRange, + String userId, + Date queryStartDate, + Date queryEndDate, + int targetStep, + long periodDays) { + // 所选指标作为第一排序条件,另外两项依次打破同分;用户ID只保证完全并列数据的返回顺序稳定。 + String orderSql = "total_steps DESC, completion_rate DESC, compliance_days DESC, u.id ASC"; + if ("completionRate".equals(rankingType)) { + orderSql = "completion_rate DESC, compliance_days DESC, total_steps DESC, u.id ASC"; + } else if ("complianceDays".equals(rankingType)) { + orderSql = "compliance_days DESC, completion_rate DESC, total_steps DESC, u.id ASC"; + } + + String ageCondition = ""; + if ("under40".equals(ageRange)) { + ageCondition = "AND u.birthday IS NOT NULL AND TIMESTAMPDIFF(YEAR, u.birthday, CURDATE()) <= 40"; + } else if ("41to50".equals(ageRange)) { + ageCondition = "AND u.birthday IS NOT NULL AND TIMESTAMPDIFF(YEAR, u.birthday, CURDATE()) BETWEEN 41 AND 50"; + } else if ("51to55".equals(ageRange)) { + ageCondition = "AND u.birthday IS NOT NULL AND TIMESTAMPDIFF(YEAR, u.birthday, CURDATE()) BETWEEN 51 AND 55"; + } + + Sql sql = Sqls.create(""" + SELECT + u.id AS user_id, + u.username, + u.sex, + TIMESTAMPDIFF(YEAR, u.birthday, CURDATE()) AS age, + COALESCE(SUM(d.daily_step), 0) AS total_steps, + LEAST(100, ROUND(SUM(LEAST(d.daily_step, @targetStep)) * 100.0 / (@targetStep * @periodDays), 0)) AS completion_rate, + SUM(CASE WHEN d.daily_step >= @targetStep THEN 1 ELSE 0 END) AS compliance_days + FROM ( + SELECT + userId, + applyDate, + MAX(step) AS daily_step + FROM fitness_walk_step + WHERE activityId = @activityId + AND applyDate >= @startDate + AND applyDate <= @endDate + GROUP BY userId, applyDate + ) d + INNER JOIN sys_user u ON u.id = d.userId + WHERE 1 = 1 + AND (@sex IS NULL OR u.sex = @sex) + $ageCondition + AND (@userId IS NULL OR u.id = @userId) + GROUP BY u.id, u.username, u.sex, u.birthday + HAVING COALESCE(SUM(d.daily_step), 0) > 0 + ORDER BY $orderSql + """); + sql.setParam("activityId", activityId); + sql.setParam("startDate", queryStartDate); + sql.setParam("endDate", queryEndDate); + sql.setParam("targetStep", targetStep); + sql.setParam("periodDays", periodDays); + // 性别和用户ID必须直接出现在原始SQL中,确保Nutz能够识别并绑定命名参数;空值表示不启用对应筛选。 + sql.setParam("sex", Strings.isBlank(sex) ? null : sex); + sql.setParam("userId", Strings.isBlank(userId) ? null : userId); + sql.setVar("ageCondition", new Static(ageCondition)); + sql.setVar("orderSql", new Static(orderSql)); + return listMap(sql); + } + + /** + * 将数据库聚合字段转换成前端统一使用的排名结构。 + */ + private NutMap normalizeRankingRow(NutMap row, Integer rank, String currentUserId, String rankingType) { + String rowUserId = row.getString("user_id"); + Integer age = row.getInt("age"); + Number value; + if ("completionRate".equals(rankingType)) { + value = defaultNumber(row.get("completion_rate")); + } else if ("complianceDays".equals(rankingType)) { + value = defaultNumber(row.get("compliance_days")); + } else { + value = defaultNumber(row.get("total_steps")); + } + + return NutMap.NEW() + .addv("userId", rowUserId) + .addv("username", row.getString("username")) + .addv("sex", row.getString("sex")) + .addv("age", age) + .addv("ageGroup", getAgeGroup(age)) + .addv("rank", rank) + .addv("value", value) + .addv("currentUser", Objects.equals(rowUserId, currentUserId)); + } + + private Number defaultNumber(Object value) { + return value instanceof Number ? (Number) value : 0; + } + + /** + * 判断两位用户的三个排名指标是否完全相同。只有完成率、达标天数和累计步数均相同才并列排名。 + */ + private boolean hasSameRankingMetrics(NutMap previousRow, NutMap currentRow) { + return Double.compare( + defaultNumber(previousRow.get("completion_rate")).doubleValue(), + defaultNumber(currentRow.get("completion_rate")).doubleValue() + ) == 0 && Double.compare( + defaultNumber(previousRow.get("compliance_days")).doubleValue(), + defaultNumber(currentRow.get("compliance_days")).doubleValue() + ) == 0 && Double.compare( + defaultNumber(previousRow.get("total_steps")).doubleValue(), + defaultNumber(currentRow.get("total_steps")).doubleValue() + ) == 0; + } + + private String getAgeGroup(Integer age) { + if (age == null) { + return "年龄未知"; + } + if (age <= 40) { + return "40岁以下"; + } + if (age <= 50) { + return "41-50岁"; + } + if (age <= 55) { + return "51-55岁"; + } + return "55岁以上"; + } + + private NutMap emptyRankingResult(Integer pageNumber, Integer pageSize, String userId) { + return NutMap.NEW() + .addv("list", new ArrayList<>()) + .addv("pageNumber", pageNumber) + .addv("pageSize", pageSize) + .addv("totalCount", 0) + .addv("totalPage", 0) + .addv("currentUser", NutMap.NEW() + .addv("userId", userId) + .addv("rank", null) + .addv("value", 0) + .addv("currentUser", true)); + } + /** * 获取中奖榜 @@ -531,8 +872,46 @@ public class FitnessWalkCommonServiceImpl extends BaseServiceImpl=", 41); + cnd.and(ageExpression, "<=", 50); + } else if ("51to55".equals(ageRange)) { + cnd.and(ageExpression, ">=", 51); + cnd.and(ageExpression, "<=", 55); + } if (Strings.isNotBlank(searchName) && Strings.isNotBlank(searchKeyword)) { cnd.and(searchName, "LIKE", "%" + searchKeyword + "%"); } // cnd.and("u.id","in",Sqls.create("select userId from activity_user_scope where groupId = @groupId").setParam("groupId",activity.getGroupId())); - cnd.groupBy("u.id","u.username","u.loginname","u.unionname","u.unitname","over3k.standardsDays"); + cnd.groupBy("u.id","u.username","u.loginname","u.sex","u.birthday","u.unionname","u.unitname","over3k.standardsDays"); cnd.desc("total_steps"); cnd.having(Cnd.NEW().and("COALESCE(SUM(sub.step), 0)",">",0)); sql.setCondition(cnd); diff --git a/src/main/resources/views/platform/zhgh/activity/fitnesswalk/stepRanking.html b/src/main/resources/views/platform/zhgh/activity/fitnesswalk/stepRanking.html index a710f0e..ba62f3a 100644 --- a/src/main/resources/views/platform/zhgh/activity/fitnesswalk/stepRanking.html +++ b/src/main/resources/views/platform/zhgh/activity/fitnesswalk/stepRanking.html @@ -34,6 +34,29 @@ layout("/layouts/platform.html"){ + + 性别: + + + + + + + + + + 年龄: + + + + + + + + + 姓名工号: @@ -90,9 +113,13 @@ layout("/layouts/platform.html"){ 总排行榜 导出{{pageForm.mode == 'today' ? '今日排行榜' : '总排行榜'}} - 导出参与人员总步数 + + - +