健步走提交

This commit is contained in:
2026-07-21 14:54:52 +08:00
parent 80618129a1
commit 530bb9ce0a
6 changed files with 674 additions and 164 deletions
+7 -1
View File
@@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<groupId>com.budwk</groupId>
<artifactId>zhgh_jsahvc</artifactId>
<artifactId>zhgh_nwnu</artifactId>
<version>5.6.0-plus</version>
<properties>
<nutzboot.version>2.6.0-SNAPSHOT</nutzboot.version>
@@ -487,6 +487,12 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 显式锁定 Nutz 核心版本,避免 SNAPSHOT BOM 更新后导致本地编译类路径发生漂移。 -->
<dependency>
<groupId>org.nutz</groupId>
<artifactId>nutz</artifactId>
<version>${nutz.version}</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
@@ -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 Resultdata 中包含最终生效的 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("个人信息保存失败,请稍后重试!");
}
}
}
@@ -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 Resultdata.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 Resultdata 包含 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<NutMap> 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<NutMap> stepRankingList = fitnessWalkCommonService.getStepRankingList(
searchName,
searchKeyword,
activityId,
unionId,
unitId,
mode,
sex,
ageRange
);
List<ExcelExportEntity> 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();
@@ -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<FitnessWalkAwardUser> {
@@ -64,15 +65,64 @@ public interface FitnessWalkCommonService extends BaseService<FitnessWalkAwardUs
/**
* 获取步数排行榜
*
* @param pageForm 分页及姓名、工号查询参数
* @param activityId 活动id
* @param unionId 工会id
* @param unitId 单位id
* @param mode today all
* @return
* @param sex 性别,空字符串表示全部
* @param ageRange 年龄段,空字符串表示全体
* @return 分页排行榜,list 中包含用户、步数、性别、年龄和达标天数
*/
Pagination getStepRankingPagination(PageForm pageForm, String activityId, String unionId, String unitId, String mode);
Pagination getStepRankingPagination(PageForm pageForm,
String activityId,
String unionId,
String unitId,
String mode,
String sex,
String ageRange);
List<NutMap> 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<NutMap> 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<FitnessWalkAwardUs
*/
List<NutMap> 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);
}
@@ -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<FitnessWalkAwa
@Inject
private SysTaskService sysTaskService;
@Inject
private SysUserService sysUserService;
@Inject
private TaskPlatformService taskPlatformService;
@@ -434,17 +440,352 @@ public class FitnessWalkCommonServiceImpl extends BaseServiceImpl<FitnessWalkAwa
@Override
public Pagination getStepRankingPagination(PageForm pageForm, String activityId, String unionId, String unitId, String mode) {
Sql sql = rankingListSql(pageForm.getSearchName(), pageForm.getSearchKeyword(), activityId, unionId, unitId, mode);
public Pagination getStepRankingPagination(PageForm pageForm,
String activityId,
String unionId,
String unitId,
String mode,
String sex,
String ageRange) {
Sql sql = rankingListSql(
pageForm.getSearchName(),
pageForm.getSearchKeyword(),
activityId,
unionId,
unitId,
mode,
sex,
ageRange
);
return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
}
@Override
public List<NutMap> getStepRankingList(String searchName, String searchKeyword, String activityId, String unionId, String unitId, String mode) {
Sql sql = rankingListSql(searchName, searchKeyword, activityId, unionId, unitId, mode);
public List<NutMap> 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<NutMap> filteredRows = queryRankingRows(
activityId,
rankingType,
sex,
ageRange,
null,
queryStartDate,
queryEndDate,
targetStep,
periodDays
);
List<NutMap> 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<NutMap> 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<NutMap> 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<NutMap> 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<FitnessWalkAwa
return collect;
}
/**
* 保存用户在健步走首页确认的性别和出生日期,并以本次提交内容覆盖原有资料。
*
* @param userId 用户ID,用于查询 sys_user
* @param sex 用户本次选择的性别,只能为“男”或“女”
* @param birthday 用户本次选择的出生日期,不能晚于当前日期
* @return 本次实际保存的性别和出生日期;用户不存在时返回 null
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public NutMap completeUserInfo(String userId, String sex, Date birthday) {
Sys_user user = sysUserService.fetch(userId);
if (Lang.isEmpty(user)) {
return null;
}
private Sql rankingListSql(String searchName, String searchKeyword, String activityId, String unionId, String unitId, String mode) {
// 始终保存本次确认值,避免数据库已有旧值时忽略用户在首页的新选择。
Chain updateChain = Chain.make("sex", sex)
.add("birthday", birthday);
sysUserService.update(updateChain, Cnd.where("id", "=", userId));
sysUserService.deleteCache(userId);
return NutMap.NEW()
.addv("sex", sex)
.addv("birthday", DateUtil.format(birthday, "yyyy-MM-dd"));
}
/**
* 生成PC端排行榜查询。性别使用用户资料中的 sex 字段,年龄根据 birthday 实时计算;
* ageRange 为空时不限制年龄,under40、41to50、51to55 分别对应页面的三个年龄段。
*/
private Sql rankingListSql(String searchName,
String searchKeyword,
String activityId,
String unionId,
String unitId,
String mode,
String sex,
String ageRange) {
FitnessWalkActivity activity = getActivity(activityId);
CndPlus cnd = CndPlus.create();
@@ -542,9 +921,11 @@ public class FitnessWalkCommonServiceImpl extends BaseServiceImpl<FitnessWalkAwa
COALESCE(SUM(sub.step), 0) AS total_steps,
u.username,
u.loginname,
u.sex,
TIMESTAMPDIFF(YEAR, u.birthday, CURDATE()) AS age,
u.unionname,
u.unitname,
over3k.standardsDays
COALESCE(over3k.standardsDays, 0) AS standardsDays
FROM
`vw_user` u
LEFT JOIN (
@@ -595,12 +976,27 @@ public class FitnessWalkCommonServiceImpl extends BaseServiceImpl<FitnessWalkAwa
if (Strings.isNotBlank(unionId)) {
cnd.and("u.unionId", "=", unionId);
}
if (Strings.isNotBlank(sex)) {
cnd.and("u.sex", "=", sex);
}
// 年龄筛选与移动端排行榜保持同一分段;生日为空时不会命中任一指定年龄段。
String ageExpression = "TIMESTAMPDIFF(YEAR, u.birthday, CURDATE())";
if ("under40".equals(ageRange)) {
cnd.and(ageExpression, "<=", 40);
} else if ("41to50".equals(ageRange)) {
cnd.and(ageExpression, ">=", 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);
@@ -34,6 +34,29 @@ layout("/layouts/platform.html"){
</div>
</div>
<div class="search-item">
<div class="search-item-label">性别:</div>
<div class="search-item-option">
<el-select @change="doSearch" clearable style="width: 100%"
placeholder="请选择性别" v-model="pageForm.sex">
<el-option label="男" value="男"></el-option>
<el-option label="女" value="女"></el-option>
</el-select>
</div>
</div>
<div class="search-item">
<div class="search-item-label">年龄:</div>
<div class="search-item-option">
<el-select @change="doSearch" clearable style="width: 100%"
placeholder="请选择年龄段" v-model="pageForm.ageRange">
<el-option label="40岁及以下" value="under40"></el-option>
<el-option label="41-50岁" value="41to50"></el-option>
<el-option label="51-55岁" value="51to55"></el-option>
</el-select>
</div>
</div>
<div class="search-item">
<div class="search-item-label">姓名工号:</div>
<div class="search-item-option">
@@ -90,9 +113,13 @@ layout("/layouts/platform.html"){
<el-radio-button label="all">总排行榜</el-radio-button>
</el-radio-group>
<el-button @click="exportExcel" class="ml10" size="small" type="primary">导出{{pageForm.mode == 'today' ? '今日排行榜' : '总排行榜'}}</el-button>
<el-button @click="exportExcel2" class="ml10" size="small" type="primary">导出参与人员总步数</el-button>
<!-- 独立导出查询尚未修复,暂时隐藏入口,避免用户导出只有表头的空文件。 -->
<!-- <el-button @click="exportExcel2" class="ml10" size="small" type="primary">导出参与人员总步数</el-button> -->
</table-tool>
<el-table :data="tableData" :size="tableSize">
<el-table :data="tableData"
:size="tableSize"
v-loading="tableLoading"
element-loading-text="数据加载中">
<el-table-column :index="indexMethod" align="center" header-align="center" label="排名" type="index"
width="80px"></el-table-column>
<el-table-column
@@ -128,7 +155,9 @@ layout("/layouts/platform.html"){
mode:'today',
searchName:'u.username',
unionId:'',
unitId:''
unitId:'',
sex:'',
ageRange:''
},
unionOptions: [],
unitOptions: [],
@@ -136,17 +165,21 @@ layout("/layouts/platform.html"){
tableColumns: [
{label: '姓名', prop: 'username'},
{label: '工号', prop: 'loginname'},
{label: '性别', prop: 'sex'},
{label: '年龄', prop: 'age', sortable: true},
{label: '分工会', prop: 'unionname', sortable: true},
{label: '单位', prop: 'unitname'},
{label: '步数', prop: 'total_steps', sortable: true},
{label: '达标天数', prop: 'standardsDays', sortable: true},
],
}
},
methods: {
exportExcel(){
const {searchName,searchKeyword,activityId,unionId,unitId,mode} = this.pageForm
const {searchName,searchKeyword,activityId,unionId,unitId,mode,sex,ageRange} = this.pageForm
window.open( loc() + '/exportExcel?searchName=' + searchName + '&searchKeyword=' + searchKeyword +
'&activityId=' + activityId +'&unionId=' + unionId + '&unitId=' + unitId + '&mode=' + mode)
'&activityId=' + activityId +'&unionId=' + unionId + '&unitId=' + unitId + '&mode=' + mode +
'&sex=' + sex + '&ageRange=' + ageRange)
},
exportExcel2(){
const {searchName,searchKeyword,activityId,unionId,unitId,mode} = this.pageForm