新增分工会活动达标统计菜单
This commit is contained in:
+2
@@ -302,6 +302,8 @@ public class FitnessWalkStepRankingController {
|
||||
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("达标天数", "standardsDays", 20));
|
||||
try {
|
||||
String fileName = ("today".equals(mode) ? DateUtil.format(new Date(), "yyyy年MM月dd日") : "活动期间总") + "步数榜.xlsx";
|
||||
ExportParams exportParams = new ExportParams();
|
||||
|
||||
+3
-3
@@ -88,9 +88,9 @@ public class FitnessWalkStepStatisticsController {
|
||||
@Param("year") Integer year,
|
||||
@Param("activityId") String activityId,
|
||||
@Param("activityDate") String[] activityDate,
|
||||
@Param("unionId") String unionId,
|
||||
@Param("unitId") String unitId) {
|
||||
CndPlus cnd = CndPlus.create();
|
||||
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]);
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package com.budwk.app.zhgh.activity.fitnesswalk.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.zhgh.activity.fitnesswalk.service.FitnessWalkStepUnionStatisticsService;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 分工会活动达标统计,活动资格判断及人数汇总统一交由 service 处理。
|
||||
*/
|
||||
@IocBean
|
||||
@Ok("json")
|
||||
@At("/platform/fitnessWalk/stepUnionStatistics")
|
||||
public class FitnessWalkStepUnionStatisticsController {
|
||||
|
||||
@Inject
|
||||
private FitnessWalkStepUnionStatisticsService fitnessWalkStepUnionStatisticsService;
|
||||
|
||||
/**
|
||||
* 打开分工会活动达标统计页面,菜单链接与当前路由保持一致。
|
||||
*/
|
||||
@At("")
|
||||
@Ok("beetl:/platform/zhgh/activity/fitnesswalk/stepUnionStatistics.html")
|
||||
@SaCheckPermission("fitnessWalk.stepUnionStatistics")
|
||||
public void index() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取年份对应的活动选项,供页面多选;默认查询本年度。
|
||||
*
|
||||
* @param year 活动开始年份,不传时使用当前年份
|
||||
* @return Result;data 为活动选项数组,选项的 value 是活动 ID,label 和 ruleLabel 用于展示名称及达标标准
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission("fitnessWalk.stepUnionStatistics")
|
||||
public Result activityOptions(@Param("year") Integer year) {
|
||||
try {
|
||||
return Result.success(fitnessWalkStepUnionStatisticsService.getActivityOptions(year == null ? DateUtil.thisYear() : year));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收活动多选及分工会筛选,返回各活动的达标结果和所选范围的平均达标率。
|
||||
*
|
||||
* @param year 活动开始年份,不传时使用当前年份
|
||||
* @param activityIds 活动 ID 数组,页面通过 JSON 数组提交;空数组表示该年份全部活动
|
||||
* @param unionId 分工会 ID,空值表示查询当前账号可查看的全部分工会
|
||||
* @return Result;data.activities 为实际统计活动,data.list 为分工会行,
|
||||
* activityStatistics 按活动 ID 保存人数及比例,averageQualifiedRate 为所选活动平均达标率
|
||||
*/
|
||||
@At
|
||||
@Ok("json:full")
|
||||
@SaCheckPermission("fitnessWalk.stepUnionStatistics")
|
||||
public Result pageData(@Param("year") Integer year,
|
||||
@Param("activityIds") String[] activityIds,
|
||||
@Param("unionId") String unionId) {
|
||||
try {
|
||||
return Result.success(fitnessWalkStepUnionStatisticsService.getUnionStatistics(year == null ? DateUtil.thisYear() : year, activityIds, unionId));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.budwk.app.zhgh.activity.fitnesswalk.service;
|
||||
|
||||
import com.budwk.app.base.service.BaseService;
|
||||
import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkStep;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 按计步活动统计分工会会员的达标人数、达标率及所选活动的平均达标率。
|
||||
*/
|
||||
public interface FitnessWalkStepUnionStatisticsService extends BaseService<FitnessWalkStep> {
|
||||
|
||||
/**
|
||||
* 查询指定年份开始、且当前账号可查看的计步活动。
|
||||
*
|
||||
* @param year 活动开始年份,例如 2026
|
||||
* @return 活动选项列表;value 为活动 ID,label 为活动名称及日期,ruleLabel 为该活动的达标规则
|
||||
*/
|
||||
List<NutMap> getActivityOptions(int year);
|
||||
|
||||
/**
|
||||
* 使用各活动自己的达标配置统计分工会,活动内按人员去重,活动之间分别计算。
|
||||
*
|
||||
* @param year 活动开始年份,例如 2026
|
||||
* @param activityIds 所选活动 ID 数组;未选择时统计该年份全部可查看活动,已选择时严格限定所选活动
|
||||
* @param unionId 分工会 ID;管理员传空值时统计全部分工会,其他账号仅统计自己所属分工会
|
||||
* @return 统计结果 Map;activities 为实际统计的活动选项,list 为分工会汇总行。
|
||||
* 每行包含 unionId、unionName、memberNum、activityStatistics 和 averageQualifiedRate;
|
||||
* activityStatistics 按活动 ID 保存 qualifiedNum(达标人数)及 qualifiedRate(百分比文本)
|
||||
*/
|
||||
NutMap getUnionStatistics(int year, String[] activityIds, String unionId);
|
||||
}
|
||||
+24
-4
@@ -547,6 +547,17 @@ public class FitnessWalkCommonServiceImpl extends BaseServiceImpl<FitnessWalkAwa
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 构建排行榜查询,步数按排行榜模式统计,达标天数始终按所选活动期间统计。
|
||||
*
|
||||
* @param searchName 姓名或工号查询字段,沿用页面传入的 u.username 或 u.loginname
|
||||
* @param searchKeyword 姓名或工号关键字,空值表示不按关键字筛选
|
||||
* @param activityId 所选活动 ID,用于读取活动起止日期及抽奖资格配置
|
||||
* @param unionId 分工会 ID,空值表示不限制分工会
|
||||
* @param unitId 单位 ID,空值表示不限制单位
|
||||
* @param mode today 统计今日步数,all 统计活动期间总步数
|
||||
* @return 排行榜查询 SQL;total_steps 为当前模式步数,standardsDays 为活动期间累计达标天数,未达标返回 0
|
||||
*/
|
||||
private Sql rankingListSql(String searchName, String searchKeyword, String activityId, String unionId, String unitId, String mode) {
|
||||
FitnessWalkActivity activity = getActivity(activityId);
|
||||
CndPlus cnd = CndPlus.create();
|
||||
@@ -559,7 +570,7 @@ public class FitnessWalkCommonServiceImpl extends BaseServiceImpl<FitnessWalkAwa
|
||||
u.loginname,
|
||||
u.unionName AS unionname,
|
||||
u.unitName AS unitname,
|
||||
over3k.standardsDays
|
||||
COALESCE(over3k.standardsDays, 0) AS standardsDays
|
||||
FROM
|
||||
vw_user u
|
||||
LEFT JOIN (
|
||||
@@ -584,15 +595,24 @@ public class FitnessWalkCommonServiceImpl extends BaseServiceImpl<FitnessWalkAwa
|
||||
FROM
|
||||
`fitness_walk_step`
|
||||
WHERE
|
||||
step >= @step
|
||||
AND activityId=@activityId
|
||||
AND $dateSql
|
||||
activityId=@activityId
|
||||
AND DATE(applyDate) >= @activityStartDate
|
||||
AND DATE(applyDate) <= @activityEndDate
|
||||
AND (@requireDailyQualification = 0 OR step >= @qualificationStep)
|
||||
GROUP BY
|
||||
userId
|
||||
) AS over3k ON u.id = over3k.userId
|
||||
$condition
|
||||
""").setParam("activityId",activityId).setParam("step",0);
|
||||
|
||||
// 达标天数独立使用活动日期,切换到今日榜时也保留整个活动期间的累计结果。
|
||||
sql.setParam("activityStartDate", DateUtil.format(DateUtil.date(activity.getStartTime()), "yyyy-MM-dd"));
|
||||
sql.setParam("activityEndDate", DateUtil.format(DateUtil.date(activity.getEndTime()), "yyyy-MM-dd"));
|
||||
// 与计步抽奖页一致:配置资格天数时按每日资格步数计数,否则统计有记录的日期。
|
||||
// 历史活动的资格步数为空时按 0 处理;同一天重复记录通过 COUNT(DISTINCT DATE) 只计一次。
|
||||
sql.setParam("requireDailyQualification", activity.getLotteryQualificationDays() == null ? 0 : 1);
|
||||
sql.setParam("qualificationStep", activity.getLotteryQualificationSteps() == null ? 0 : activity.getLotteryQualificationSteps());
|
||||
|
||||
if ("today".equals(mode)) {
|
||||
sql.setVar("dateSql",new Static("DATE(applyDate) = '"+DateUtil.today()+"'"));
|
||||
// cnd.and("DATE( s.applyDate )", "=", DateUtil.today());
|
||||
|
||||
+286
@@ -0,0 +1,286 @@
|
||||
package com.budwk.app.zhgh.activity.fitnesswalk.service.impl;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||
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.contants.FitnessWalkMode;
|
||||
import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkActivity;
|
||||
import com.budwk.app.zhgh.activity.fitnesswalk.model.FitnessWalkStep;
|
||||
import com.budwk.app.zhgh.activity.fitnesswalk.service.FitnessWalkStepUnionStatisticsService;
|
||||
import com.budwk.app.zhgh.activity.fitnesswalk.utils.WeAppCloudUtil;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
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.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 分工会计步活动达标统计;使用当前会员人数作为分母,每个活动单独判断人员资格。
|
||||
*/
|
||||
@IocBean(args = {"refer:dao"})
|
||||
public class FitnessWalkStepUnionStatisticsServiceImpl extends BaseServiceImpl<FitnessWalkStep> implements FitnessWalkStepUnionStatisticsService {
|
||||
private static final int ACTIVITY_QUERY_BATCH_SIZE = 100;
|
||||
|
||||
@Inject
|
||||
private WeAppCloudUtil weAppCloudUtil;
|
||||
|
||||
/**
|
||||
* @param dao 项目注入的 Nutz 数据访问对象,用于查询当前会员及活动步数记录
|
||||
*/
|
||||
public FitnessWalkStepUnionStatisticsServiceImpl(Dao dao) {
|
||||
super(dao);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param year 活动开始年份,允许查询 1970 年至当前年份
|
||||
* @return 当前账号可查看的活动选项,包含 ID、名称、日期和达标规则
|
||||
*/
|
||||
@Override
|
||||
public List<NutMap> getActivityOptions(int year) {
|
||||
return queryYearActivities(year).stream().map(this::toActivityOption).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param year 活动开始年份
|
||||
* @param activityIds 所选活动 ID;空数组查询全部活动,非空数组只查询所选活动
|
||||
* @param unionId 分工会 ID;非管理员的查询范围由服务层限制为本人分工会
|
||||
* @return activities 为实际统计活动,list 为全部可查看分工会的活动达标矩阵;比例以两位小数百分比返回
|
||||
*/
|
||||
@Override
|
||||
public NutMap getUnionStatistics(int year, String[] activityIds, String unionId) {
|
||||
List<FitnessWalkActivity> activities = resolveSelectedActivities(queryYearActivities(year), activityIds);
|
||||
List<NutMap> activityOptions = activities.stream().map(this::toActivityOption).collect(Collectors.toList());
|
||||
if (activities.isEmpty()) {
|
||||
return NutMap.NEW().addv("activities", activityOptions).addv("list", Collections.emptyList());
|
||||
}
|
||||
|
||||
String effectiveUnionId = resolveUnionId(unionId);
|
||||
// 分母和达标人数均限定为当前会员,避免未入会人员计入分子后出现超过 100% 的比例。
|
||||
Sql memberSql = Sqls.create("""
|
||||
SELECT
|
||||
un.id AS unionId,
|
||||
un.name AS unionName,
|
||||
COALESCE(m.memberNum, 0) AS memberNum
|
||||
FROM sys_union un
|
||||
LEFT JOIN (
|
||||
SELECT unionId, COUNT(DISTINCT id) AS memberNum
|
||||
FROM vw_user
|
||||
WHERE member = 1
|
||||
GROUP BY unionId
|
||||
) m ON m.unionId = un.id
|
||||
WHERE (@hasUnionFilter = 0 OR un.id = @unionId)
|
||||
ORDER BY un.unionCode
|
||||
""");
|
||||
memberSql.setParam("hasUnionFilter", Strings.isBlank(effectiveUnionId) ? 0 : 1);
|
||||
memberSql.setParam("unionId", Strings.sNull(effectiveUnionId));
|
||||
List<NutMap> rows = listMap(memberSql);
|
||||
|
||||
Map<String, Map<String, Long>> activityCounts = new LinkedHashMap<>();
|
||||
for (FitnessWalkActivity activity : activities) {
|
||||
Map<String, Long> unionCounts = new LinkedHashMap<>();
|
||||
for (NutMap countRow : listMap(buildQualifiedCountSql(activity, effectiveUnionId))) {
|
||||
unionCounts.put(countRow.getString("unionId"), countRow.getLong("qualifiedNum", 0));
|
||||
}
|
||||
activityCounts.put(activity.get_id(), unionCounts);
|
||||
}
|
||||
|
||||
for (NutMap row : rows) {
|
||||
String rowUnionId = row.getString("unionId");
|
||||
long memberNum = row.getLong("memberNum", 0);
|
||||
long qualifiedTotal = 0;
|
||||
NutMap activityStatistics = NutMap.NEW();
|
||||
for (FitnessWalkActivity activity : activities) {
|
||||
long qualifiedNum = activityCounts.get(activity.get_id()).getOrDefault(rowUnionId, 0L);
|
||||
qualifiedTotal += qualifiedNum;
|
||||
activityStatistics.put(activity.get_id(), NutMap.NEW()
|
||||
.addv("qualifiedNum", qualifiedNum)
|
||||
.addv("qualifiedRate", formatRate(qualifiedNum, BigDecimal.valueOf(memberNum))));
|
||||
}
|
||||
row.put("activityStatistics", activityStatistics);
|
||||
// 每个活动使用同一分工会会员分母,因此先汇总人数再除以会员人数×活动数,等价于活动达标率的算术平均。
|
||||
// 直接使用原始人数计算,避免先对单活动比例四舍五入后再平均造成误差;零达标活动仍计入活动数。
|
||||
row.put("averageQualifiedRate", formatRate(qualifiedTotal, BigDecimal.valueOf(memberNum).multiply(BigDecimal.valueOf(activities.size()))));
|
||||
}
|
||||
return NutMap.NEW().addv("activities", activityOptions).addv("list", rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分批获取指定年份的活动及资格配置,避免云查询默认条数和公共活动缓存影响新统计页面。
|
||||
*/
|
||||
private List<FitnessWalkActivity> queryYearActivities(int year) {
|
||||
if (year < 1970 || year > DateUtil.thisYear()) {
|
||||
throw new IllegalArgumentException("请选择有效的活动年份");
|
||||
}
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.set(Calendar.YEAR, year);
|
||||
long startTs = DateUtil.beginOfYear(calendar.getTime()).getTime();
|
||||
long endTs = DateUtil.endOfYear(calendar.getTime()).getTime();
|
||||
String effectiveUnionId = resolveUnionId(null);
|
||||
|
||||
StringBuilder query = new StringBuilder("db.collection('activity').where({activityModel:");
|
||||
query.append(JSON.toJSONString(FitnessWalkMode.STEP_COUNT.getValue()))
|
||||
.append(",startTime:_.gte(").append(startTs).append(").and(_.lte(").append(endTs).append("))");
|
||||
if (Strings.isNotBlank(effectiveUnionId)) {
|
||||
query.append(",unionId:").append(JSON.toJSONString(effectiveUnionId));
|
||||
}
|
||||
query.append("}).field({_id:true,masterActivityName:true,branchActivityName:true,activityModel:true,")
|
||||
.append("startTime:true,endTime:true,lotteryQualificationDays:true,lotteryQualificationSteps:true,unionId:true})")
|
||||
.append(".orderBy('startTime','desc').orderBy('_id','asc')");
|
||||
|
||||
Map<String, FitnessWalkActivity> activityMap = new LinkedHashMap<>();
|
||||
int skip = 0;
|
||||
while (true) {
|
||||
String sql = query + ".skip(" + skip + ").limit(" + ACTIVITY_QUERY_BATCH_SIZE + ").get()";
|
||||
JSONObject response = weAppCloudUtil.request(WeAppCloudUtil.CRUD.QUERY, sql);
|
||||
JSONArray data = response.getJSONArray("data");
|
||||
if (data == null) {
|
||||
throw new IllegalArgumentException("活动列表获取失败,请稍后重试");
|
||||
}
|
||||
for (Object item : data) {
|
||||
FitnessWalkActivity activity = JSON.parseObject((String) item, FitnessWalkActivity.class);
|
||||
// 历史活动缺失日期时不进入统计,防止空日期变成当前日期而误算达标人数。
|
||||
if (Strings.isNotBlank(activity.get_id()) && activity.getStartTime() != null && activity.getEndTime() != null
|
||||
&& activity.getStartTime() <= activity.getEndTime()) {
|
||||
activityMap.put(activity.get_id(), activity);
|
||||
}
|
||||
}
|
||||
if (data.size() < ACTIVITY_QUERY_BATCH_SIZE) {
|
||||
break;
|
||||
}
|
||||
skip += ACTIVITY_QUERY_BATCH_SIZE;
|
||||
}
|
||||
return new ArrayList<>(activityMap.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 已选择活动时逐个校验年份和可见范围,失效选项报错,避免错误地扩大为全部活动。
|
||||
*/
|
||||
private List<FitnessWalkActivity> resolveSelectedActivities(List<FitnessWalkActivity> availableActivities, String[] activityIds) {
|
||||
if (activityIds == null || activityIds.length == 0) {
|
||||
return availableActivities;
|
||||
}
|
||||
Set<String> selectedIds = new LinkedHashSet<>();
|
||||
for (String activityId : activityIds) {
|
||||
if (Strings.isBlank(activityId)) {
|
||||
throw new IllegalArgumentException("所选活动无效,请重新选择");
|
||||
}
|
||||
selectedIds.add(activityId);
|
||||
}
|
||||
Set<String> availableIds = availableActivities.stream().map(FitnessWalkActivity::get_id).collect(Collectors.toSet());
|
||||
if (!availableIds.containsAll(selectedIds)) {
|
||||
throw new IllegalArgumentException("所选活动不属于当前年份或不可查看,请刷新活动列表后重新选择");
|
||||
}
|
||||
return availableActivities.stream().filter(activity -> selectedIds.contains(activity.get_id())).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员可筛选全部分工会;其他账号忽略外部传入范围,只统计本人所属分工会。
|
||||
*/
|
||||
private String resolveUnionId(String unionId) {
|
||||
if (AuthUtil.hasRoleOr("SYSADMIN", "SCHOOL_UNION_ADMIN")) {
|
||||
return unionId;
|
||||
}
|
||||
String currentUnionId = SecurityUtil.getUnionId();
|
||||
if (Strings.isBlank(currentUnionId)) {
|
||||
throw new IllegalArgumentException("当前账号未关联分工会,无法查询统计");
|
||||
}
|
||||
return currentUnionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按计步抽奖页的三种资格口径统计人数:达标天数、累计步数或有步数记录。
|
||||
* 有资格天数时只计达到每日步数门槛的日期;同日重复上传保留最大步数,避免累计步数重复计算。
|
||||
*/
|
||||
private Sql buildQualifiedCountSql(FitnessWalkActivity activity, String unionId) {
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT qualified.unionId, COUNT(DISTINCT qualified.userId) AS qualifiedNum
|
||||
FROM (
|
||||
SELECT u.id AS userId, u.unionId
|
||||
FROM vw_user u
|
||||
LEFT JOIN (
|
||||
SELECT userId, DATE(applyDate) AS applyDay, MAX(step) AS daySteps
|
||||
FROM fitness_walk_step
|
||||
WHERE activityId = @activityId
|
||||
AND DATE(applyDate) >= @startDate
|
||||
AND DATE(applyDate) <= @endDate
|
||||
AND (@hasDayRequirement = 0 OR step >= @qualificationSteps)
|
||||
GROUP BY userId, DATE(applyDate)
|
||||
) daily ON daily.userId = u.id
|
||||
WHERE u.member = 1 AND (@hasUnionFilter = 0 OR u.unionId = @unionId)
|
||||
GROUP BY u.id, u.unionId
|
||||
HAVING (
|
||||
(@hasDayRequirement = 1 AND COUNT(DISTINCT daily.applyDay) > 0
|
||||
AND COUNT(DISTINCT daily.applyDay) >= @qualificationDays)
|
||||
OR (@hasDayRequirement = 0 AND @hasStepRequirement = 1
|
||||
AND COALESCE(SUM(daily.daySteps), 0) >= @qualificationSteps)
|
||||
OR (@hasDayRequirement = 0 AND @hasStepRequirement = 0
|
||||
AND COALESCE(SUM(daily.daySteps), 0) > 0)
|
||||
)
|
||||
) qualified
|
||||
GROUP BY qualified.unionId
|
||||
""");
|
||||
sql.setParam("activityId", activity.get_id());
|
||||
sql.setParam("startDate", DateUtil.format(DateUtil.date(activity.getStartTime()), "yyyy-MM-dd"));
|
||||
sql.setParam("endDate", DateUtil.format(DateUtil.date(activity.getEndTime()), "yyyy-MM-dd"));
|
||||
sql.setParam("hasDayRequirement", activity.getLotteryQualificationDays() == null ? 0 : 1);
|
||||
sql.setParam("hasStepRequirement", activity.getLotteryQualificationSteps() == null ? 0 : 1);
|
||||
sql.setParam("qualificationDays", activity.getLotteryQualificationDays() == null ? 0 : activity.getLotteryQualificationDays());
|
||||
sql.setParam("qualificationSteps", activity.getLotteryQualificationSteps() == null ? 0 : activity.getLotteryQualificationSteps());
|
||||
sql.setParam("hasUnionFilter", Strings.isBlank(unionId) ? 0 : 1);
|
||||
sql.setParam("unionId", Strings.sNull(unionId));
|
||||
return sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* 名称同时包含主活动、分活动及日期,避免同名月度活动在多选和表头中无法区分。
|
||||
*/
|
||||
private NutMap toActivityOption(FitnessWalkActivity activity) {
|
||||
String name = Strings.sNull(activity.getMasterActivityName());
|
||||
if (Strings.isNotBlank(activity.getBranchActivityName()) && !activity.getBranchActivityName().equals(name)) {
|
||||
name += " / " + activity.getBranchActivityName();
|
||||
}
|
||||
String startDate = DateUtil.format(DateUtil.date(activity.getStartTime()), "yyyy-MM-dd");
|
||||
String endDate = DateUtil.format(DateUtil.date(activity.getEndTime()), "yyyy-MM-dd");
|
||||
String ruleLabel;
|
||||
if (activity.getLotteryQualificationDays() != null) {
|
||||
int steps = activity.getLotteryQualificationSteps() == null ? 0 : activity.getLotteryQualificationSteps();
|
||||
ruleLabel = "每日至少" + steps + "步,活动期间至少达标" + activity.getLotteryQualificationDays() + "天";
|
||||
} else if (activity.getLotteryQualificationSteps() != null) {
|
||||
ruleLabel = "活动期间累计至少" + activity.getLotteryQualificationSteps() + "步";
|
||||
} else {
|
||||
ruleLabel = "活动期间总步数大于0";
|
||||
}
|
||||
return NutMap.NEW().addv("value", activity.get_id()).addv("name", name)
|
||||
.addv("label", name + "(" + startDate + " 至 " + endDate + ")")
|
||||
.addv("ruleLabel", ruleLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一输出两位小数百分比;没有会员或没有统计活动时返回 0.00%,避免除零。
|
||||
*/
|
||||
private String formatRate(long qualifiedNum, BigDecimal denominator) {
|
||||
if (denominator.signum() <= 0) {
|
||||
return "0.00%";
|
||||
}
|
||||
return BigDecimal.valueOf(qualifiedNum).multiply(BigDecimal.valueOf(100))
|
||||
.divide(denominator, 2, RoundingMode.HALF_UP).toPlainString() + "%";
|
||||
}
|
||||
}
|
||||
@@ -139,6 +139,8 @@ layout("/layouts/platform.html"){
|
||||
{label: '分工会', prop: 'unionname', sortable: true},
|
||||
{label: '单位', prop: 'unitname'},
|
||||
{label: '步数', prop: 'total_steps', sortable: true},
|
||||
// 今日榜和总榜均展示所选活动期间累计的达标天数。
|
||||
{label: '达标天数', prop: 'standardsDays', sortable: true},
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
<!--#
|
||||
layout("/layouts/platform.html"){
|
||||
#-->
|
||||
<style>
|
||||
/* 当前平台顶部为 64px,内容区上下留白共 20px;限制页面高度,避免数据行撑高外层。 */
|
||||
.step-union-statistics {
|
||||
height: calc(100vh - 84px);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.step-union-statistics > .guava-main-content,
|
||||
.step-union-statistics > .guava-main-content > .transition-item {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.step-union-statistics > .guava-main-content > .transition-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.step-union-statistics .statistics-search-card,
|
||||
.step-union-statistics .ele-table-tool {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 表格卡片和表体允许缩小到剩余空间,筛选区与标题保持固定,由 el-table 内部滚动。 */
|
||||
.step-union-statistics .statistics-table-card {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.step-union-statistics .statistics-table-card > .el-card__body {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.step-union-statistics .statistics-table-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.step-union-statistics .el-table__body-wrapper {
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
</style>
|
||||
<div id="app" class="step-union-statistics" v-cloak>
|
||||
<guava>
|
||||
<template>
|
||||
<el-card class="statistics-search-card" shadow="never">
|
||||
<div class="search">
|
||||
<div class="search-item">
|
||||
<div class="search-item-label">年份:</div>
|
||||
<div class="search-item-option">
|
||||
<el-date-picker
|
||||
v-model="pageForm.year"
|
||||
type="year"
|
||||
value-format="yyyy"
|
||||
placeholder="选择年"
|
||||
style="width: 100%"
|
||||
:clearable="false"
|
||||
:picker-options="pickerOptions"
|
||||
:disabled="formLoading"
|
||||
@change="getActivityOptions">
|
||||
</el-date-picker>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-item">
|
||||
<div class="search-item-label">活动:</div>
|
||||
<div class="search-item-option">
|
||||
<el-select
|
||||
v-model="pageForm.activityIds"
|
||||
multiple
|
||||
collapse-tags
|
||||
filterable
|
||||
clearable
|
||||
:disabled="formLoading"
|
||||
placeholder="不选择则统计所选年份全部活动"
|
||||
style="width: 100%"
|
||||
@change="queryActivityStatistics">
|
||||
<el-option
|
||||
v-for="item in activityOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-item">
|
||||
<div class="search-item-label">分工会:</div>
|
||||
<div class="search-item-option">
|
||||
<el-select
|
||||
v-model="pageForm.unionId"
|
||||
clearable
|
||||
filterable
|
||||
:disabled="formLoading"
|
||||
placeholder="请选择分工会"
|
||||
style="width: 100%"
|
||||
@change="queryActivityStatistics">
|
||||
<el-option
|
||||
v-for="item in unionOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-query">
|
||||
<el-button
|
||||
@click="queryActivityStatistics"
|
||||
:disabled="formLoading"
|
||||
:loading="tableLoading"
|
||||
icon="el-icon-search"
|
||||
type="primary">
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-alert
|
||||
class="mt10"
|
||||
title="各活动按自己的达标规则统计;分工会人数为当前会员人数。平均达标率为本次统计活动达标率的平均值。"
|
||||
type="info"
|
||||
:closable="false"
|
||||
show-icon>
|
||||
</el-alert>
|
||||
</el-card>
|
||||
<el-card class="mt10 statistics-table-card" shadow="never">
|
||||
<table-tool label="分工会活动达标统计">
|
||||
<span>统计活动:{{ statisticsActivities.length }} 个</span>
|
||||
</table-tool>
|
||||
<div class="statistics-table-container">
|
||||
<el-table :data="tableData" :size="tableSize" v-loading="tableLoading" height="100%" border>
|
||||
<el-table-column :index="indexMethod" label="序号" type="index" width="70"
|
||||
align="center" header-align="center" fixed="left"></el-table-column>
|
||||
<el-table-column label="分工会" prop="unionName" min-width="180"
|
||||
align="center" header-align="center" fixed="left"></el-table-column>
|
||||
<el-table-column label="分工会人数" prop="memberNum" width="120"
|
||||
align="center" header-align="center"></el-table-column>
|
||||
<el-table-column
|
||||
v-for="activity in statisticsActivities"
|
||||
:key="activity.value"
|
||||
:label="activity.label"
|
||||
align="center"
|
||||
header-align="center">
|
||||
<template slot="header">
|
||||
<el-tooltip :content="activity.ruleLabel" placement="top">
|
||||
<span>{{ activity.label }}</span>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-table-column label="达标人数" width="110" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
{{ row.activityStatistics[activity.value].qualifiedNum }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="达标率" width="110" align="center" header-align="center">
|
||||
<template slot-scope="{row}">
|
||||
{{ row.activityStatistics[activity.value].qualifiedRate }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table-column>
|
||||
<el-table-column label="平均达标率" prop="averageQualifiedRate" width="130"
|
||||
align="center" header-align="center" fixed="right"></el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
</guava>
|
||||
</div>
|
||||
<script nonce="${cspNonce!}">
|
||||
const vue = new Vue({
|
||||
el: '#app',
|
||||
mixins: [initTableMixins],
|
||||
data() {
|
||||
return {
|
||||
pageForm: {
|
||||
year: new Date().getFullYear() + '',
|
||||
activityIds: [],
|
||||
unionId: ''
|
||||
},
|
||||
pickerOptions: {
|
||||
disabledDate(time) {
|
||||
return time.getFullYear() < 1970 || time.getFullYear() > new Date().getFullYear()
|
||||
}
|
||||
},
|
||||
unionOptions: [],
|
||||
activityOptions: [],
|
||||
statisticsActivities: [],
|
||||
statisticsRequestVersion: 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 切换年份后清除上一年的所选活动,加载当前年份活动并自动查询全部活动。
|
||||
*/
|
||||
getActivityOptions() {
|
||||
this.$set(this.pageForm, 'activityIds', [])
|
||||
this.$set(this, 'activityOptions', [])
|
||||
this.$set(this, 'statisticsActivities', [])
|
||||
this.$set(this, 'tableData', [])
|
||||
// 使上一轮统计失效,避免年份切换期间旧请求覆盖新表格。
|
||||
this.$set(this, 'statisticsRequestVersion', this.statisticsRequestVersion + 1)
|
||||
this.$set(this, 'tableLoading', false)
|
||||
this.$set(this, 'formLoading', true)
|
||||
this.$axios.post(loc() + '/activityOptions', {year: this.pageForm.year})
|
||||
.then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.$set(this, 'activityOptions', res.data || [])
|
||||
this.queryActivityStatistics()
|
||||
} else {
|
||||
this.$message.error(res.msg)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 请求错误由全局 axios 提示,清除选项及统计结果,避免继续选择失效活动。
|
||||
this.$set(this, 'activityOptions', [])
|
||||
this.$set(this, 'statisticsActivities', [])
|
||||
this.$set(this, 'tableData', [])
|
||||
})
|
||||
.finally(() => {
|
||||
this.$set(this, 'formLoading', false)
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 查询完整分工会活动矩阵,表头以后台实际统计活动为准,避免活动列和结果范围不一致。
|
||||
* 活动 ID 以 JSON 数组提交;未选择时为空数组,选择后只提交所选 ID。
|
||||
* 此页面返回活动表头及全部分工会汇总,复用 mixin 的表格能力,不重复编写分页方法。
|
||||
*/
|
||||
queryActivityStatistics() {
|
||||
const requestVersion = this.statisticsRequestVersion + 1
|
||||
this.$set(this, 'statisticsRequestVersion', requestVersion)
|
||||
this.$set(this, 'tableLoading', true)
|
||||
// 新筛选条件生效时清除旧结果,接口失败时也不会保留上一组活动的统计。
|
||||
this.$set(this, 'tableData', [])
|
||||
this.$set(this, 'statisticsActivities', [])
|
||||
const params = {
|
||||
year: this.pageForm.year,
|
||||
activityIds: JSON.stringify(this.pageForm.activityIds),
|
||||
unionId: this.pageForm.unionId
|
||||
}
|
||||
this.$axios.post(loc() + '/pageData', params)
|
||||
.then((res) => {
|
||||
// 快速切换活动或工会时,只展示最后一次请求对应的数据。
|
||||
if (requestVersion !== this.statisticsRequestVersion) {
|
||||
return
|
||||
}
|
||||
if (res.code === 0) {
|
||||
this.$set(this, 'statisticsActivities', res.data.activities || [])
|
||||
this.$set(this, 'tableData', res.data.list || [])
|
||||
} else {
|
||||
this.$message.error(res.msg)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 只清除本轮失败查询,较早请求失败时保留后来成功查询的结果。
|
||||
if (requestVersion === this.statisticsRequestVersion) {
|
||||
this.$set(this, 'statisticsActivities', [])
|
||||
this.$set(this, 'tableData', [])
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (requestVersion === this.statisticsRequestVersion) {
|
||||
this.$set(this, 'tableLoading', false)
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$businessTool.listUnion().then((res) => {
|
||||
this.$set(this, 'unionOptions', res)
|
||||
this.getActivityOptions()
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<!--#
|
||||
}
|
||||
#-->
|
||||
Reference in New Issue
Block a user