This commit is contained in:
2026-09-11 16:39:50 +08:00
parent dc63a51638
commit 7d1e7dc9a8
27 changed files with 3471 additions and 1 deletions
@@ -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;
}
}
@@ -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 + "%"));
}
}
@@ -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<NutMap> 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<FitnessWalkRaffleUser> 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<NutMap> lotteryUserList = userList.stream().limit(lotteryUserNum).collect(Collectors.toList());
Date finalStartDate = startDate;
Date finalEndDate = endDate;
List<FitnessWalkAwardUser> 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 + "%"));
}
}
@@ -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 注解认证白名单工具。
* <p>
* 用于集中维护不需要登录、权限、角色校验即可访问的 URL,匹配规则与旧 Shiro 体系中的
* authIgnoreUrlArr 保持一致:数组中的每一项都按正则表达式与当前请求 URI 完整匹配。
*/
public class SaTokenAuthIgnoreUtil {
/**
* 不需要认证授权的 URL。
* <p>
* 参数说明:数组中的字符串为请求 URI 正则表达式,匹配的是去除项目 contextPath 后的路径。
* 返回值说明:通过 {@link #isIgnore()} 返回 booleantrue 表示当前请求跳过 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());
}
}
@@ -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 {
@@ -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 Resultdata.list为当前页记录,data.totalCount为符合条件的总条数
*/
@At
@SaCheckPermission("fitnessWalk.activityRelation")
public Result pageData(PageForm pageForm) {
return Result.success(relationProjectService.pageData(pageForm));
}
/**
* 保存关联项目。
* 支持新增和编辑,保存字段仅包含主活动名称和达标活动数量。
* @param relationProject 关联项目配置,包含id、masterActivityName和standardActivityNum
* @return Resultcode为0表示成功,data为业务结果,msg为操作说明
*/
@At
@SaCheckPermission("fitnessWalk.activityRelation")
@Aop(TransAop.READ_COMMITTED)
public Result doSave(FitnessWalkActivityRelationProject relationProject) {
return relationProjectService.saveRelationProject(relationProject);
}
/**
* 删除关联项目。
* @param id 待操作记录的主键ID
* @return Resultcode为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 Resultcode为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 Resultcode为0表示成功,data为业务结果,msg为操作说明
*/
@At
@SaCheckPermission("fitnessWalk.activityRelation")
public Result isCurrentUserRelationActivityQualified(String relationActivityId,String userId) {
return Result.success(relationActivityCompletionService.isCurrentUserRelationActivityQualified(relationActivityId,userId));
}
}
@@ -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 Resultcode为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 Resultdata.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);
}
}
@@ -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;
}
@@ -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<FitnessWalkActivityRelationProject> {
/**
* 查询关联项目列表。
*
* @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<NutMap> getRelationActivityList(String id);
}
@@ -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<FitnessWalkActivityRelationProject> {
/**
* 获取当前年份可关联的活动下拉数据。
* 下拉内容统一返回活动ID、活动名称和达标活动数量,供创建/编辑页面直接使用。
*
* @return 关联活动下拉数据
*/
List<FitnessWalkActivityRelationProject> getCurrentYearRelationActivityOptions();
/**
* 获取福利项目下拉数据。
* 创建/编辑活动时统一从 service 层查询福利项目,避免页面直接跨模块拼查询。
*
* @param year 年份,为空时默认当前年份
* @return 福利项目下拉数据,仅返回页面下拉所需字段
*/
List<NutMap> getWelfareProjectOptions(Integer year);
/**
* 分页检索云端活动并保留当前分工会数据权限。
*
* @param year 查询年度,四位年份
* @param unionId 分工会ID,空值表示不追加筛选,已有数据权限仍生效
* @param searchName 查询字段,使用页面提供的字段名
* @param searchKeyword 姓名、工号或当前查询字段的检索内容
* @param pageNumber 页码,从1开始
* @param pageSize 每页记录数
* @param pageOrderName 列表排序字段
* @param pageOrderBy 排序方向,ascending或descending
* @return Resultcode为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 Resultcode为0表示成功,data承载创建云端活动并登记抽奖、连续创建任务和活动缓存结果,失败说明见msg
*/
Result doAdd(String data, FitnessWalkActivity fitnessWalkActivity,
TempFile tempFile,
TempFile[] stepFiles,
TempFile certificateTempFile);
/**
* 将微信云存储文件ID转换为上传组件回显信息。
*
* @param pic 微信云存储文件ID
* @return Resultcode为0表示成功,data承载将微信云存储文件ID转换为上传组件回显信息结果,失败说明见msg
*/
Result getFile(String pic);
/**
* 更新活动配置并同步关联任务和活动缓存。
*
* @param fitnessWalkActivity 页面提交的活动配置,包含模式、时间、点位和抽奖规则
* @param tempFile 活动封面上传文件,不更换时传空
* @param stepFiles 计步展示图片文件数组,不更换时传空
* @param certificateTempFile 完赛证书背景文件,不更换时传空
* @return Resultcode为0表示成功,data承载更新活动配置并同步关联任务和活动缓存结果,失败说明见msg
*/
Result doEdit(FitnessWalkActivity fitnessWalkActivity,
TempFile tempFile,
TempFile[] stepFiles,
TempFile certificateTempFile);
/**
* 删除云端活动及报名、打卡、礼品券和关联任务。
*
* @param id 待操作记录的主键ID
* @return Resultcode为0表示成功,data承载删除云端活动及报名、打卡、礼品券和关联任务结果,失败说明见msg
*/
Result deleteActivity(String id);
/**
* 检索腾讯地图地址候选项,使用当前项目配置的AppTMapKey与CityName。
* @param keyword 地名或地址关键字
* @return Resultdata为腾讯地图返回的状态及地址候选列表
*/
Result suggestMapAddress(String keyword);
}
@@ -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 Resultcode为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 Resultcode为0表示成功,data承载按记录ID删除选中的云端绑定记录结果,失败说明见msg
*/
Result delete(String[] ids);
/**
* 清除云端绑定记录,供管理员重新同步人员绑定。
*
* @return Resultcode为0表示成功,data承载清除云端绑定记录,供管理员重新同步人员绑定结果,失败说明见msg
*/
Result deleteAll();
}
@@ -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 Resultcode为0表示成功,data承载按源版奖池和报名人数分配打卡抽奖结果结果,失败说明见msg
*/
Result judgeWinningToPunch(String activityId, String userId);
/**
* 查询用户在指定活动中的打卡抽奖记录。
*
* @param activityId 健步走云端活动ID
* @param userId 登录接口返回的用户ID
* @return Resultcode为0表示成功,data承载查询用户在指定活动中的打卡抽奖记录结果,失败说明见msg
*/
Result getLotteryRecord(String activityId, String userId);
/**
* 核销奖品并记录兑换时间,重复核销返回状态2。
*
* @param id 待操作记录的主键ID
* @return Resultcode为0表示成功,data承载核销奖品并记录兑换时间,重复核销返回状态2结果,失败说明见msg
*/
Result doExchange(String id);
/**
* 将用户在指定活动中的抽奖记录标记为已读。
*
* @param activityId 健步走云端活动ID
* @param userId 登录接口返回的用户ID
* @return Resultcode为0表示成功,data承载将用户在指定活动中的抽奖记录标记为已读结果,失败说明见msg
*/
Result doReadLottery(String activityId, String userId);
/**
* 查询指定打卡活动中已中奖的人员。
*
* @param activityId 健步走云端活动ID
* @return Resultcode为0表示成功,data承载查询指定打卡活动中已中奖的人员结果,失败说明见msg
*/
Result getPunchWining(String activityId);
/**
* 生成不重复的中奖序号,边界规则沿用源版本。
*
* @param minNum 随机中奖序号下界
* @param maxNum 随机中奖序号上界
* @param count 需要抽取的中奖序号数量
* @return List<Integer>,用于生成不重复的中奖序号,边界规则沿用源版本
*/
List<Integer> generateRandomNumber(Integer minNum, Integer maxNum, Integer count);
}
@@ -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<FitnessWalkActivityRelationProject> {
/**
* 根据活动关联项目ID,判断当前登录人是否满足关联活动达标要求。
* 只有当当前用户完成的达标活动数等于关联配置的 standardActivityNum 时,才返回 true。
*
* @param relationActivityId 活动关联项目ID
* @return true 代表全部合格,false 代表未达标
*/
boolean isCurrentUserRelationActivityQualified(String relationActivityId,String userId);
}
@@ -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 Resultcode为0表示成功,data承载合并微信运动最近30天步数,按每天最大步数保存并计算积分结果,失败说明见msg
*/
Result updateStepMonth(String activityId, String userId, String monthSteps);
/**
* 查询活动日期步数,返回step和毫秒级timestamp。
*
* @param activityId 健步走云端活动ID
* @param userId 登录接口返回的用户ID
* @return Resultcode为0表示成功,data承载查询活动日期步数,返回step和毫秒级timestamp结果,失败说明见msg
*/
Result getActivityDateStep(String activityId, String userId);
/**
* 按每日或自定义规则计算用户达标步数与完成进度。
*
* @param activityId 健步走云端活动ID
* @param userId 登录接口返回的用户ID
* @return Resultcode为0表示成功,data承载按每日或自定义规则计算用户达标步数与完成进度结果,失败说明见msg
*/
Result getActivityQualifyProgressBar(String activityId, String userId);
/**
* 查询指定用户在活动中的中奖记录。
*
* @param activityId 健步走云端活动ID
* @param userId 登录接口返回的用户ID
* @return Resultcode为0表示成功,data承载查询指定用户在活动中的中奖记录结果,失败说明见msg
*/
Result winningRecord(String activityId, String userId);
/**
* 标记活动奖项已读,沿用源版活动维度更新范围。
*
* @param activityId 健步走云端活动ID
* @return Resultcode为0表示成功,data承载标记活动奖项已读,沿用源版活动维度更新范围结果,失败说明见msg
*/
Result hasReadAward(String activityId);
}
@@ -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 Resultcode为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);
}
@@ -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<FitnessWalkActivityRelationProject>
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<NutMap> 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 "";
}
}
@@ -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<FitnessWalkActivityRelationProject> 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<FitnessWalkActivityRelationProject> getCurrentYearRelationActivityOptions() {
List<FitnessWalkActivityRelationProject> projectList = dao().query(FitnessWalkActivityRelationProject.class, CndPlus.create().andEX("YEAR(FROM_UNIXTIME(createdAt/ 1000))", "=", DateUtil.thisYear()));
return projectList;
}
@Override
public List<NutMap> 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 Resultcode为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<Object> 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 Resultcode为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<String> 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 Resultcode为0表示成功,data承载将微信云存储文件ID转换为上传组件回显信息结果,失败说明见msg
*/
@Override
public Result getFile(String pic) {
try {
NutMap res = NutMap.NEW();
String imgurl = weAppCloudUtil.httpFile(pic);
HashMap<String, Object> 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 Resultcode为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<String> 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 Resultcode为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_task> 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 Resultdata.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));
}
}
@@ -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 Resultcode为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<NutMap> 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 Resultcode为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 Resultcode为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();
}
}
}
@@ -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 Resultcode为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<FitnessWalkActivity.PunchLotteryPrize> 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<FitnessWalkActivity.PunchLotteryPrize> prizeList = JSON.parseObject((String) prizeJsonObject.getJSONArray("data").get(0)).getJSONArray("punchLotteryPrizes").toJavaList(FitnessWalkActivity.PunchLotteryPrize.class);
redisService.set(redisKey, JSON.toJSONString(prizeList));
punchLotteryPrizes = prizeList;
}
List<Integer> 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<FitnessWalkPunchLottery> 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<String, String> prizeMap = punchLotteryPrizes.stream().collect(Collectors.toMap(v -> v.getValue(), v -> v.getName()));
//奖项池
List<String> prizeIdList = new ArrayList<>();
punchLotteryPrizes.forEach(v -> {
for (int i = 0; i < Integer.parseInt(v.getNum()); i++) {
prizeIdList.add(v.getValue());
}
});
//已经中奖的奖项ID集合
List<String> 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 Resultcode为0表示成功,data承载查询用户在指定活动中的打卡抽奖记录结果,失败说明见msg
*/
@Override
public Result getLotteryRecord(String activityId, String userId) {
List<FitnessWalkPunchLottery> list = fitnessWalkCommonService.dao().query(FitnessWalkPunchLottery.class, Cnd.where("activityId", "=", activityId)
.and("userId", "=", userId));
return Result.success(list);
}
/**
* 核销奖品并记录兑换时间,重复核销返回状态2。
*
* @param id 待操作记录的主键ID
* @return Resultcode为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 Resultcode为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 Resultcode为0表示成功,data承载查询指定打卡活动中已中奖的人员结果,失败说明见msg
*/
@Override
public Result getPunchWining(String activityId) {
List<FitnessWalkPunchLottery> 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<Integer>,用于生成不重复的中奖序号,边界规则沿用源版本
*/
@Override
public List<Integer> generateRandomNumber(Integer minNum, Integer maxNum, Integer count) {
Set<Integer> 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);
}
}
@@ -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<FitnessWalkActivityRelationProject>
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<FitnessWalkActivity> relationActivityList = queryRelationActivityList(relationActivityId);
Map<String, Integer> 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<FitnessWalkActivity> 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<String, Integer> queryUserSignedPtsNumMap(String userId, List<FitnessWalkActivity> relationActivityList) {
if (relationActivityList == null || relationActivityList.isEmpty()) {
return Collections.emptyMap();
}
List<String> 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<String, Integer> 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));
}
}
@@ -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 Resultcode为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<NutMap> steps = Json.fromJsonAsList(NutMap.class, monthSteps);
//判断数据是否完整 微信运动会在晚上10点多进行步数的更新 会导致31天前的步数获取变为0 所以这里去除掉第一个元素 即为31天前的数据 每次只保存最近30天的数据
if (Lang.isNotEmpty(steps) && steps.size() == 31) {
steps.remove(0);
}
// 传递过来的步数数据
List<FitnessWalkStep> 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<Date> dateList = wxSteps.stream().map(FitnessWalkStep::getApplyDate).collect(Collectors.toList());
// 去数据库查询日期,这个玩意还要保证插入到数据库的单个日期只有一条,并且这个日期数据库之前有步数,传过来没步数那就要保留数据库的步数
// 上面日期集合数据库的步数
List<FitnessWalkStep> stepList = dao.query(FitnessWalkStep.class, Cnd.where("activityId", "=", activityId)
.and("userId", "=", userId).and("applyDate", "in", dateList));
// 这就是保留每天最大步数的集合,那这个集合里面applyDate就是唯一的了
List<FitnessWalkStep> dupStepList = new ArrayList<>(stepList.stream()
.collect(Collectors.toMap(FitnessWalkStep::getApplyDate, Function.identity(),
BinaryOperator.maxBy(Comparator.comparing(FitnessWalkStep::getStep))))
.values());
// 这是过滤的相同的applyDate的集合,这些数据要删除
List<String> delStepIds = stepList.stream()
.filter(step -> !dupStepList.contains(step))
.map(FitnessWalkStep::getId)
.collect(Collectors.toList());
// 数据库的数据 这个map就用于判断数据库的日期和传递过来日期的步数,两个是不是相等的,保留步数多的数据
Map<Date, FitnessWalkStep> dateStepMap = dupStepList.stream().collect(Collectors.toMap(FitnessWalkStep::getApplyDate, v -> v));
List<FitnessWalkStep> 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<IntegralDetail> 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<IntegralDetail> 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 Resultcode为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<FitnessWalkStep> stepList = baseService.dao().query(FitnessWalkStep.class, cnd);
List<FitnessWalkDateStepVO> 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 Resultcode为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<FitnessWalkActivity.CustomLotteryRule> customLotteryRules = activity.getCustomLotteryRules();
List<FitnessWalkStep> 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<NutMap> result = new ArrayList<>();
for (FitnessWalkActivity.CustomLotteryRule rule : customLotteryRules) {
NutMap nutMap = NutMap.NEW();
List<FitnessWalkStep> 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 Resultcode为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<FitnessWalkAwardUser> 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 Resultcode为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;
}
}
@@ -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 Resultcode为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<NutMap> exportList = baseService.listMap(sql);
List<ExcelExportEntity> 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();
}
}
}
@@ -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;
}
@@ -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 Resultdata.status为腾讯地图状态,data.data为含title、address、location的候选列表
*/
@At
@SaCheckLogin
public Result suggestion(String keyword) {
if (StrUtil.isBlank(keyword)) {
return Result.error("关键词不能为空");
}
return fitnessWalkActivityService.suggestMapAddress(keyword);
}
}
@@ -0,0 +1,163 @@
<template>
<div style="position: relative !important">
<div style="margin-bottom: 10px">
<el-autocomplete
v-model="addressValue"
style="width: 100%"
:fetch-suggestions="querySearchAsync"
placeholder="请输入关键词查询地址"
@select="handleSelect"
:trigger-on-focus="false"
/>
</div>
<div id="mapContainer"></div>
<slot></slot>
</div>
</template>
<script>
module.exports = {
name: "TMap",
props: {
radius: {
type: Number,
default: 50
},
position: {
type: Array,
default: () => {
return []
}
},
view: { type: Boolean, default: false }
},
data() {
return {
map: null,
poi: this.position,
appMapCenterPointX: 0,
appMapCenterPointY: 0,
markerLayer: null,
addressValue: '',
circle: null,
}
},
methods: {
handleSelect(item) {
if (item.location) {
// 定位到中心点
const center = new TMap.LatLng(item.location.lat, item.location.lng)
this.map.setCenter(center)
this.clearMarker()
this.createMarker(center)
this.$set(this, "poi", [item.location.lat, item.location.lng])
}
},
// keyword为地址关键字;候选结果提供title、address和经纬度location。
querySearchAsync(queryString, cb) {
if (!queryString) {
cb([])
return
}
this.$axios.post("/platform/TMap/suggestion", {keyword: queryString}).then((res) => {
if (res.code === 0 && res.data.status === 0) {
cb((res.data.data || []).map((item) => Object.assign({}, item, {
value: item.title + "【详细地址:" + item.address + "】"
})))
} else {
this.$message.error(res.msg || res.data.message)
cb([])
}
}).catch(() => {
cb([])
})
},
initMap() {
const center = new TMap.LatLng(this.appMapCenterPointY, this.appMapCenterPointX)
this.$set(this, "map", new TMap.Map('mapContainer', {
resizeEnable: true,
zoom: 16,
center: center
}))
// 初始化
this.$set(this, "markerLayer", new TMap.MultiMarker({
map: this.map,
geometries: []
}));
if (this.poi && this.poi.length > 0) {
const posi = new TMap.LatLng(this.poi[0], this.poi[1])
this.createMarker(posi)
this.map.setCenter(posi)
} else {
this.createMarker(center)
}
this.map.on("click", (event) => {
if (!this.view) {
this.clearMarker()
this.createMarker(event.latLng)
this.$set(this, "poi", [event.latLng.getLat(), event.latLng.getLng()])
}
})
},
// 清除签到点位
clearMarker() {
if (this.markerLayer) {
this.markerLayer.setGeometries([])
}
if(this.circle) {
this.circle.setGeometries([])
}
},
// 创建签到点位
createMarker(position) {
this.markerLayer.add([
{
id: 'marker_' + Date.now(),
position: position
}
]);
this.$set(this, "circle", new TMap.MultiCircle({
map: this.map,
geometries: [{
center: position,
radius: this.radius,
}],
}));
},
getConfigKey(key) {
return this.$axios.post("/open/common/getConfigKey", {key}).then((resp) => resp.data)
}
},
watch: {
position(newVal) {
this.$set(this, "poi", newVal)
},
poi(newVal) {
this.$emit("update:position", newVal)
}
},
mounted() {
// 父页面已加载腾讯地图SDK;中心点沿用当前项目系统配置。
Promise.all([this.getConfigKey("AppMapCenterPointX"), this.getConfigKey("AppMapCenterPointY")])
.then(([lng, lat]) => {
this.$set(this, "appMapCenterPointX", Number(lng))
this.$set(this, "appMapCenterPointY", Number(lat))
this.$nextTick(() => this.initMap())
})
},
beforeDestroy() {
if (this.map) {
this.map.destroy()
}
}
}
</script>
<style scoped>
#mapContainer {
padding: 0;
margin: 0;
width: 100%;
height: 500px;
}
</style>
@@ -0,0 +1,229 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<el-card shadow="never">
<div class="search">
<div class="search-item">
<div class="search-item-label">主活动名称:</div>
<div class="search-item-option">
<el-input v-model="pageForm.searchKeyword"
clearable
placeholder="请输入主活动名称"
@keyup.enter.native="doSearch">
</el-input>
</div>
</div>
<div class="search-query">
<el-button icon="el-icon-search" type="primary" @click="doSearch"></el-button>
</div>
</div>
</el-card>
<el-card shadow="never" class="mt10">
<table-tool :app="this" label="关联项目列表">
<el-button type="primary" size="small" icon="el-icon-plus" @click="openAdd">创建关联项目</el-button>
</table-tool>
<el-table :data="tableData"
:size="tableSize"
class="vi-table"
row-key="id"
style="width: 100%"
v-loading="tableLoading">
<el-table-column type="index"
label="序号"
width="80px"
align="center"
header-align="center"
:index="indexMethod"></el-table-column>
<el-table-column prop="masterActivityName"
label="主活动名称"
align="center"
header-align="center"
show-overflow-tooltip></el-table-column>
<el-table-column prop="standardActivityNum"
label="达标活动数量"
align="center"
header-align="center"></el-table-column>
<el-table-column label="创建时间"
align="center"
header-align="center">
<template scope="{row}">
{{row.createdAt ? $moment(row.createdAt).format('YYYY-MM-DD HH:mm:ss') : ''}}
</template>
</el-table-column>
<el-table-column label="操作"
width="240px"
align="center"
header-align="center">
<template scope="{row}">
<el-button type="success" size="mini" @click="openView(row)">查看</el-button>
<el-button type="primary" size="mini" @click="openEdit(row)">编辑</el-button>
<el-button type="danger" size="mini" @click="doDelete(row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-row class="el-pagination-container">
<el-pagination
@size-change="pageSizeChange"
@current-change="pageNumberChange"
:current-page="pageForm.pageNumber"
:page-sizes="[10, 20, 30, 50]"
:page-size="pageForm.pageSize"
layout="total, sizes, prev, pager, next"
:total="pageForm.totalCount">
</el-pagination>
</el-row>
</el-card>
<template #edit>
<el-form :model="formData" :rules="rules" label-width="120px" ref="form">
<el-form-item label="主活动名称" prop="masterActivityName">
<el-input v-model="formData.masterActivityName" maxlength="100" placeholder="请填写主活动名称"></el-input>
</el-form-item>
<el-form-item label="达标活动数量" prop="standardActivityNum">
<el-input-number v-model="formData.standardActivityNum"
:min="1"
:precision="0"
style="width: 100%"></el-input-number>
</el-form-item>
</el-form>
<div style="display:flex;justify-content:flex-end">
<el-button type="primary" @click="doSubmit" :loading="formLoading">确定</el-button>
</div>
</template>
<template #view>
<el-form label-width="120px">
<el-form-item label="主活动名称">
{{viewData.masterActivityName}}
</el-form-item>
<el-form-item label="达标活动数量">
{{viewData.standardActivityNum}}
</el-form-item>
<el-form-item label="关联活动数">
{{viewActivityList.length}}
</el-form-item>
</el-form>
<el-table :data="viewActivityList" border style="width:100%">
<el-table-column prop="activityName"
label="活动名称"
align="center"
header-align="center"
show-overflow-tooltip></el-table-column>
<el-table-column prop="activityModelName"
label="活动模式"
width="120"
align="center"
header-align="center"></el-table-column>
<el-table-column label="创建时间"
width="180"
align="center"
header-align="center">
<template scope="{row}">
{{row.createdAt ? $moment(row.createdAt).format('YYYY-MM-DD HH:mm:ss') : ''}}
</template>
</el-table-column>
</el-table>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
const vue = new Vue({
el: '#app',
mixins: [initTableMixins],
data() {
return {
viewData: {},
viewActivityList: [],
rules: {
masterActivityName: [{required: true, message: '请填写主活动名称', trigger: ['change', 'blur']}],
standardActivityNum: [{required: true, message: '请填写达标活动数量', trigger: ['change', 'blur']}]
}
}
},
methods: {
openView(row) {
this.$axios.post(loc() + '/relationActivityList', {id: row.id}).then((resp) => {
if (resp.code === 0) {
this.$set(this, "viewData", Object.assign({}, row))
this.$set(this, "viewActivityList", resp.data || [])
this.$refs.guava.view()
} else {
this.notifyWarning(resp.msg)
}
})
},
openAdd() {
this.$refs.guava.edit()
this.$nextTick(() => {
this.$set(this, "formData", {
standardActivityNum: 1
})
if (this.$refs.form) {
this.$refs.form.clearValidate()
}
})
},
openEdit(row) {
this.$refs.guava.edit()
this.$nextTick(() => {
this.$set(this, "formData", Object.assign({}, row))
if (this.$refs.form) {
this.$refs.form.clearValidate()
}
})
},
doSubmit() {
this.$refs.form.validate((valid) => {
if (!valid) {
return
}
this.$set(this, "formLoading", true)
this.$axios.post(loc() + '/doSave', this.formData).then((resp) => {
if (resp.code === 0) {
this.notifySuccess(resp.msg)
this.$refs.guava.index()
this.pageData()
} else {
this.notifyWarning(resp.msg)
}
}).finally(() => {
this.$set(this, "formLoading", false)
})
})
},
doDelete(id) {
this.$confirm('您确定要删除该关联项目吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$set(this, "formLoading", true)
this.$axios.post(loc() + '/doRemove', {id: id}).then((resp) => {
if (resp.code === 0) {
this.notifySuccess(resp.msg)
this.pageData()
} else {
this.notifyWarning(resp.msg)
}
}).finally(() => {
this.$set(this, "formLoading", false)
})
}).catch(() => {
})
}
},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->
@@ -0,0 +1,281 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<guava ref="guava">
<template>
<el-card shadow="never">
<div class="search">
<div class="search-item">
<div class="search-item-label">年份:</div>
<div class="search-item-option">
<el-date-picker
:clearable="false"
placeholder="选择年"
style="width: 100%"
@change="getCascadersActivity"
type="year"
v-model="pageForm.year"
value-format="yyyy">
</el-date-picker>
</div>
</div>
<div class="search-item">
<div class="search-item-label">活动:</div>
<div class="search-item-option">
<div style="display: flex; align-items: center; gap: 10px;">
<el-select
:style="{width: subActivityOptions.length > 0 ? '50%' : '100%'}"
placeholder="请选择主活动"
@change="mainActivityChange"
v-model="pageForm.mainActivityId"
clearable>
<el-option
:key="item.value"
:label="item.label"
:value="item.value"
v-for="item in activityOptions"></el-option>
</el-select>
<el-select
v-if="subActivityOptions.length > 0"
style="width: 50%"
placeholder="请选择分活动"
@change="subActivityChange"
v-model="pageForm.subActivityId"
clearable>
<el-option
:key="item.value"
:label="item.label"
:value="item.value"
v-for="item in subActivityOptions"></el-option>
</el-select>
</div>
</div>
</div>
<div class="search-item">
<div class="search-item-label">分工会:</div>
<div class="search-item-option">
<el-select
@change="doSearch"
clearable
filterable
placeholder="请选择分工会"
style="width: 100%"
v-model="pageForm.unionId">
<el-option
:key="item.id"
:label="item.unionname"
:value="item.id"
v-for="item in unionOptions"></el-option>
</el-select>
</div>
</div>
<div class="search-item">
<div class="search-item-label">姓名/工号:</div>
<div class="search-item-option">
<el-input
@keyup.enter.native="doSearch"
clearable
placeholder="请输入姓名或工号"
v-model="pageForm.searchKeyword"></el-input>
</div>
</div>
<div class="search-query">
<el-button @click="doSearch" icon="el-icon-search" type="primary"></el-button>
</div>
</div>
</el-card>
<el-card class="mt10" shadow="never">
<table-tool :app="this" label="人员打卡情况">
<el-radio-group
v-model="pageForm.qualifiedStatus"
@change="doSearch"
size="mini">
<el-radio-button label="qualified">已达标人员</el-radio-button>
<el-radio-button label="unqualified">未达标人员</el-radio-button>
<el-radio-button label="all">全部</el-radio-button>
</el-radio-group>
<el-button
@click="exportExcel"
class="ml10"
icon="el-icon-download"
size="small"
type="primary">
导出
</el-button>
</table-tool>
<el-table :data="tableData" :size="tableSize" v-loading="tableLoading">
<el-table-column :index="indexMethod" align="center" header-align="center" label="序号" type="index"
width="80px"></el-table-column>
<el-table-column
:label="column.label"
:prop="column.prop"
:key="column.prop"
:sortable="column.sortable"
align="center"
header-align="center"
show-overflow-tooltip
v-for="column in tableColumns">
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
</el-card>
</template>
</guava>
</div>
<script nonce="${cspNonce!}">
let vue = new Vue({
el: '#app',
mixins: [initTableMixins],
data() {
return {
tableColumns: [
{label: '姓名', prop: 'username'},
{label: '工号', prop: 'loginname'},
{label: '分工会', prop: 'unionname'},
{label: '单位', prop: 'unitname'},
{label: '达标几个活动', prop: 'qualifiedActivityNum', sortable: true},
{label: '点位总数', prop: 'pts_size', sortable: true},
{label: '已打卡点位数', prop: 'sign_size', sortable: true},
{label: '是否全部打卡完全部点位', prop: 'isAllSigned', sortable: true}
],
unionOptions: [],
pageForm: {
unionId: '',
year: new Date().getFullYear() + '',
mainActivityId: '',
subActivityId: '',
activityId: '',
qualifiedStatus: 'all',
searchKeyword: '',
pageNumber: 1,
pageSize: 10,
totalCount: 0
},
activityOptions: [],
subActivityOptions: []
}
},
methods: {
/**
* 根据当前页面选择,统一返回活动查询参数。
* 后端会按主活动、分活动与最终 activityId 自动判断查询单个活动还是整个关联活动集合。
*/
buildActivityRequestParams() {
return {
mainActivityId: this.pageForm.mainActivityId,
subActivityId: this.pageForm.subActivityId,
activityId: this.pageForm.activityId,
qualifiedStatus: this.pageForm.qualifiedStatus
}
},
/**
* 导出与列表复用同一套筛选条件,保证当前可见条件与导出结果一致。
*/
buildExportQueryString() {
const params = Object.assign({
unionId: this.pageForm.unionId,
searchKeyword: this.pageForm.searchKeyword
}, this.buildActivityRequestParams())
return 'mainActivityId=' + encodeURIComponent(params.mainActivityId || '')
+ '&subActivityId=' + encodeURIComponent(params.subActivityId || '')
+ '&activityId=' + encodeURIComponent(params.activityId || '')
+ '&unionId=' + encodeURIComponent(params.unionId || '')
+ '&searchKeyword=' + encodeURIComponent(params.searchKeyword || '')
+ '&qualifiedStatus=' + encodeURIComponent(params.qualifiedStatus || 'all')
},
exportExcel() {
window.open(loc() + '/exportExcel?' + this.buildExportQueryString())
},
/**
* 根据主活动构建分活动选项,并同步当前真正查询用的 activityId。
* 当主活动存在分活动时,默认保持“只选主活动”的聚合查询,用户手动选择分活动后再切到单活动查询。
*/
syncActivitySelection(mainActivityId, needSearch) {
const currentMainActivity = this.activityOptions.find(item => {
return item.value === mainActivityId
})
const children = currentMainActivity && currentMainActivity.children ? currentMainActivity.children : []
this.$set(this, 'subActivityOptions', children)
if (!currentMainActivity) {
this.$set(this.pageForm, 'mainActivityId', '')
this.$set(this.pageForm, 'subActivityId', '')
this.$set(this.pageForm, 'activityId', '')
this.$set(this, 'tableData', [])
this.$set(this.pageForm, 'totalCount', 0)
return
}
if (children.length > 0) {
const currentSubActivityId = this.pageForm.subActivityId
const hasMatchedSubActivity = currentSubActivityId && children.some(item => {
return item.value === currentSubActivityId
})
this.$set(this.pageForm, 'subActivityId', hasMatchedSubActivity ? currentSubActivityId : '')
this.$set(this.pageForm, 'activityId', hasMatchedSubActivity ? currentSubActivityId : mainActivityId)
} else {
this.$set(this.pageForm, 'subActivityId', '')
this.$set(this.pageForm, 'activityId', mainActivityId)
}
if (needSearch) {
this.doSearch()
}
},
/**
* 主活动切换后,重新联动分活动并立即刷新列表。
*/
mainActivityChange(val) {
this.$set(this.pageForm, 'mainActivityId', val || '')
this.$set(this.pageForm, 'subActivityId', '')
this.syncActivitySelection(this.pageForm.mainActivityId, true)
},
/**
* 分活动切换后,更新最终查询 activityId 并刷新列表。
*/
subActivityChange(val) {
this.$set(this.pageForm, 'subActivityId', val || '')
this.$set(this.pageForm, 'activityId', val || this.pageForm.mainActivityId || '')
this.doSearch()
},
/**
* 年份切换后重载活动级联数据,并默认选中首个可用活动。
*/
getCascadersActivity() {
this.$axios.post(loc() + '/getCascadersActivity', {year: this.pageForm.year}).then(res => {
if (res.code !== 0) {
this.$message.error(res.msg)
return
}
this.$set(this, 'activityOptions', res.data || [])
if (this.activityOptions.length > 0) {
this.$set(this.pageForm, 'mainActivityId', this.activityOptions[0].value)
this.$set(this.pageForm, 'subActivityId', '')
this.syncActivitySelection(this.pageForm.mainActivityId, false)
this.pageData()
} else {
this.$set(this, 'subActivityOptions', [])
this.$set(this.pageForm, 'mainActivityId', '')
this.$set(this.pageForm, 'subActivityId', '')
this.$set(this.pageForm, 'activityId', '')
this.$set(this, 'tableData', [])
this.$set(this.pageForm, 'totalCount', 0)
}
})
}
},
created() {
this.$businessTool.listUnion().then(res => {
this.$set(this, 'unionOptions', res || [])
this.getCascadersActivity()
})
}
})
</script>
<!--#
}
#-->