This commit is contained in:
2026-06-02 09:21:40 +08:00
parent 22c82c6751
commit 12bd823da6
10 changed files with 228 additions and 30 deletions
@@ -59,17 +59,11 @@ public class QsvActivityController {
//保存问卷基础信息
@At
@SaCheckPermission("qsv.activity")
@Aop(TransAop.READ_COMMITTED)
@ApiOperation("保存问卷基础信息")
@SLog(type = "qsv.activity", tag = "保存问卷基础信息", msg = "保存问卷基础信息")
public Result save(QsvActivity qsvActivity) {
if (qsvActivity.getCategory().equals("QUIZ")) {
if (qsvActivity.getMode().equals("SCHEDULED")) {
qsvActivity.setRepeatMode("DAILY");
} else if (qsvActivity.getMode().equals("REGULAR")) {
qsvActivity.setRepeatMode("TOTAL");
}
}
dao.insertOrUpdate(qsvActivity);
qsvActivityService.saveActivity(qsvActivity);
return Result.success();
}
@@ -83,13 +77,25 @@ public class QsvActivityController {
// 删除问卷
@At
@SaCheckPermission("qsv.activity")
@Aop(TransAop.READ_COMMITTED)
@ApiOperation("删除问卷")
@SLog(type = "qsv.activity", tag = "删除问卷", msg = "删除问卷")
public Result delete(@Valid String id) {
dao.delete(QsvActivity.class, id);
qsvActivityService.deleteActivity(id);
return Result.success();
}
// 更新活动开启状态
@At
@SaCheckPermission("qsv.activity")
@Aop(TransAop.READ_COMMITTED)
@ApiOperation("更新活动开启状态")
@SLog(type = "qsv.activity", tag = "更新活动开启状态", msg = "更新活动开启状态")
public Result updateEnabled(@Valid String id, Boolean enabled) {
qsvActivityService.updateEnabled(id, enabled);
return Result.success(enabled ? "开启成功" : "关闭成功");
}
// 保存问卷题目
@At
@@ -36,6 +36,7 @@ public class H5QsvController {
public Result pageData(@Valid PageForm pageForm, @Valid String category) {
Cnd cnd = Cnd.NEW();
cnd.andEX("category", "=", category);
cnd.and(Cnd.exps("enabled", "=", true).or("enabled", "is", null));
cnd.desc("startTime");
Pagination pagination = qsvActivityService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd);
return Result.success(pagination);
@@ -46,6 +46,10 @@ public class H5QsvSurveyController {
@Aop(TransAop.READ_COMMITTED)
public Result subjects(String activityId) {
QsvActivity activity = dao.fetch(QsvActivity.class, activityId);
Result activityStatusResult = checkActivityStatus(activity);
if (activityStatusResult != null) {
return activityStatusResult;
}
int count = dao.count(ActivityUserScope.class, Cnd.where("groupId", "=", activity.getGroupId())
.and("userId", "=", SecurityUtil.getUserId()));
@@ -82,6 +86,30 @@ public class H5QsvSurveyController {
return Result.success(result);
}
/**
* 校验调查活动是否处于可答题状态。activity 为空表示活动不存在;
* enabled 为 false 表示活动已关闭,当前时间早于 startTime 表示未开始,当前时间晚于 endTime 表示已结束。
*
* @param activity 当前调查活动,包含开启状态、开始时间和结束时间
* @return 状态或时间不允许答题时返回错误结果,允许答题时返回 null
*/
private Result checkActivityStatus(QsvActivity activity) {
if (activity == null) {
return Result.error("调查不存在");
}
if (Boolean.FALSE.equals(activity.getEnabled())) {
return Result.error("活动已关闭,暂不能参与");
}
Date now = new Date();
if (activity.getStartTime() != null && activity.getStartTime().after(now)) {
return Result.error("调查尚未开始,请在开始后再参与");
}
if (activity.getEndTime() != null && activity.getEndTime().before(now)) {
return Result.error("调查已结束");
}
return null;
}
/**
* 同步问卷当前题目到未完成答题记录。后台新增题目后,已进入过问卷的用户记录里没有新题ID,
* 这里补齐 subjectIds 和 extJson,保证手机端能拿到最新题目并正常提交答案。
@@ -133,6 +161,12 @@ public class H5QsvSurveyController {
@At
@Aop(TransAop.READ_COMMITTED)
public Result submitAnswer(@Valid @Param("answer") QsvAnswerParam qsvAnswerParam) {
QsvActivity activity = dao.fetch(QsvActivity.class, qsvAnswerParam.getActivityId());
Result activityStatusResult = checkActivityStatus(activity);
if (activityStatusResult != null) {
return activityStatusResult;
}
QsvUserAnswerRecord answerRecord = dao.fetch(QsvUserAnswerRecord.class, qsvAnswerParam.getAnswerRecordId());
JSONObject extJson = answerRecord.getExtJson();
@@ -1,6 +1,8 @@
package com.budwk.app.zhgh.dayofficework.qsv.models;
import com.budwk.app.base.model.BaseModel;
import com.budwk.app.sys.models.Sys_home_activity;
import com.budwk.app.sys.services.SysHomeConvert;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*;
@@ -14,7 +16,7 @@ import java.util.Date;
@Table("qsv_activity")
@TableMeta("{'mysql-charset':'utf8mb4'}")
@Comment("问卷调查投票活动表")
public class QsvActivity extends BaseModel implements Serializable {
public class QsvActivity extends BaseModel implements Serializable, SysHomeConvert {
@Name
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@@ -46,6 +48,12 @@ public class QsvActivity extends BaseModel implements Serializable {
@ColDefine(type = ColType.DATETIME)
private Date endTime;
@Column
@Comment("是否开启")
@ColDefine(type = ColType.BOOLEAN)
@Default("1")
private Boolean enabled;
@Column
@Comment("活动分组ID")
@ColDefine(type = ColType.INT)
@@ -107,4 +115,35 @@ public class QsvActivity extends BaseModel implements Serializable {
@Comment("封面图片")
@ColDefine(type = ColType.VARCHAR, width = 255)
private String cover;
@Override
public Sys_home_activity covertToSysHomeActivity() {
Sys_home_activity sysHomeActivity = new Sys_home_activity();
sysHomeActivity.setId(this.getId());
sysHomeActivity.setName(this.getTitle());
sysHomeActivity.setCover(this.getCover());
sysHomeActivity.setContent(this.getDescription());
sysHomeActivity.setUrl("/platform/qsv/activity");
sysHomeActivity.setH5Url(getH5Url());
sysHomeActivity.setStartDate(this.getStartTime());
sysHomeActivity.setEndDate(this.getEndTime());
sysHomeActivity.setAllowUserGroupId(this.getGroupId());
sysHomeActivity.setEnable(!Boolean.FALSE.equals(this.getEnabled()));
sysHomeActivity.setClassPath(this.getClass().getPackageName() + this.getClass().getName());
return sysHomeActivity;
}
/**
* 根据活动类型生成移动端首页跳转地址。
* QUIZ 跳转答题页,SURVEY 跳转调查页,VOTE 预留投票页地址,返回值为移动端路由字符串。
*/
private String getH5Url() {
if ("SURVEY".equals(this.getCategory())) {
return "/platform/h5/qsv/survey?id=" + this.getId();
}
if ("VOTE".equals(this.getCategory())) {
return "/platform/h5/qsv/vote?id=" + this.getId();
}
return "/platform/h5/qsv/quiz?id=" + this.getId();
}
}
@@ -5,4 +5,10 @@ import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
public interface QsvActivityService extends BaseService<QsvActivity> {
void saveActivity(QsvActivity qsvActivity);
void deleteActivity(String id);
void updateEnabled(String id, Boolean enabled);
}
@@ -1,9 +1,13 @@
package com.budwk.app.zhgh.dayofficework.qsv.service.impl;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.sys.models.Sys_home_activity;
import com.budwk.app.zhgh.dayofficework.qsv.models.QsvActivity;
import com.budwk.app.zhgh.dayofficework.qsv.service.QsvActivityService;
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.IocBean;
@IocBean(args = {"refer:dao"})
@@ -12,4 +16,53 @@ public class QsvActivityServiceImpl extends BaseServiceImpl<QsvActivity> impleme
public QsvActivityServiceImpl(Dao dao) {
super(dao);
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void saveActivity(QsvActivity qsvActivity) {
fillQuizRepeatMode(qsvActivity);
dao().insertOrUpdate(qsvActivity);
syncHomeActivity(qsvActivity);
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void deleteActivity(String id) {
dao().delete(QsvActivity.class, id);
dao().delete(Sys_home_activity.class, id);
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void updateEnabled(String id, Boolean enabled) {
QsvActivity activity = dao().fetch(QsvActivity.class, id);
if (activity == null) {
return;
}
activity.setEnabled(enabled);
dao().update(activity);
syncHomeActivity(activity);
}
/**
* 答题活动的重复模式由出题模式决定。定时定题按天统计,常规模式按活动总次数统计。
*/
private void fillQuizRepeatMode(QsvActivity qsvActivity) {
if (!"QUIZ".equals(qsvActivity.getCategory())) {
return;
}
if ("SCHEDULED".equals(qsvActivity.getMode())) {
qsvActivity.setRepeatMode("DAILY");
} else if ("REGULAR".equals(qsvActivity.getMode())) {
qsvActivity.setRepeatMode("TOTAL");
}
}
/**
* 同步移动端首页活动。问卷活动保存或启停后,都以活动自身 enabled、时间、人员范围生成首页记录。
*/
private void syncHomeActivity(QsvActivity activity) {
Sys_home_activity sysHomeActivity = activity.covertToSysHomeActivity();
dao().insertOrUpdate(sysHomeActivity);
}
}
@@ -58,6 +58,17 @@ const basicForm = {
</div>
</el-form-item>
<el-form-item label="封面" prop="cover">
<file-upload
:upload_number="1"
:value.sync="formData.cover"
accept=".jpg,.jpeg,.png"
upload_mode="image"
upload_result_category="interval"
upload_result_type="url"
></file-upload>
</el-form-item>
<template v-if="formData.category==='QUIZ'">
<el-form-item label="出题模式" prop="mode">
<span slot="label">
@@ -171,17 +182,6 @@ const basicForm = {
</el-radio-group>
</el-form-item>
<!-- <el-form-item label="封面" prop="cover">-->
<!-- <file-upload-->
<!-- :upload_number="1"-->
<!-- :value.sync="formData.cover"-->
<!-- accept=".jpg,.jpeg,.png"-->
<!-- complete_result-->
<!-- upload_mode="image"-->
<!-- upload_result_category="interval"-->
<!-- ></file-upload>-->
<!-- </el-form-item>-->
</template>
</el-form>
</div>
@@ -204,7 +204,8 @@ const basicForm = {
return {
dialogVisible: false,
formData: {
category: ''
category: '',
cover: ''
},
formRules: {
category: [{ required: true, message: "请选择类型", trigger: "change" }],
@@ -228,7 +229,9 @@ const basicForm = {
}
})
} else {
this.formData = {}
this.formData = {
cover: ''
}
}
},
onSubmit() {
@@ -31,11 +31,27 @@ layout("/layouts/platform.html"){
</el-table-column>
<el-table-column label="开始时间" prop="startTime" sortable></el-table-column>
<el-table-column label="结束时间" prop="endTime" sortable></el-table-column>
<el-table-column label="操作" fixed="right" width="450px">
<el-table-column label="状态" width="100">
<template scope="{row}">
<el-tag v-if="row.enabled !== false" type="success">开启</el-tag>
<el-tag v-else type="info">关闭</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="520px">
<template scope="{row}">
<el-button size="mini" type="primary" @click="openQrCode(row)">二维码</el-button>
<el-button size="mini" type="primary" @click="openEdit(row)">编辑</el-button>
<el-button size="mini" type="primary" @click="openSubject(row.id)">题目设置</el-button>
<el-button
v-if="row.enabled === false"
size="mini"
type="success"
@click="updateEnabled(row, true)">开启</el-button>
<el-button
v-else
size="mini"
type="warning"
@click="updateEnabled(row, false)">关闭</el-button>
<el-button size="mini" type="danger" @click="onDelete(row.id)">删除</el-button>
</template>
</el-table-column>
@@ -94,6 +110,26 @@ layout("/layouts/platform.html"){
})
},
// 开启或关闭活动,关闭后移动端首页不展示,直达手机端问卷也不能答题
updateEnabled(row, enabled) {
const actionText = enabled ? "开启" : "关闭"
this.$confirm("您确认" + actionText + "该活动吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
$.post("/platform/qsv/activity/updateEnabled", {
id: row.id,
enabled
}).then((res) => {
if (res.code === 0) {
this.$message.success(res.msg)
this.pageData()
}
})
})
},
// 删除
onDelete(id) {
this.$confirm("您确认删除吗, 是否继续?", "提示", {
@@ -12,19 +12,18 @@ const subjectForm = {
<transition-group>
<div v-for="(subject, subjectIndex) in subjects" :key="subject.id" class="subject-item">
<div class="subject-header">
<div style="display: flex; align-items: center;">
<div class="subject-title-row">
<i class="el-icon-rank drag-handler"></i>
<span style="font-weight: bold; margin-right: 10px;">第 {{subjectIndex + 1}} 题</span>
<el-input
class="subject-title-input"
v-model="subject.title"
placeholder="请输入题目"
style="flex: 1">
placeholder="请输入题目">
</el-input>
<el-button
class="subject-visible-btn"
type="primary"
icon="el-icon-setting"
size="mini"
style="margin-left: 8px;"
@click="openSubjectVisibleSetting(subjectIndex, subject)">
隐显逻辑
</el-button>
@@ -663,6 +662,21 @@ const subjectForm = {
margin-bottom: 15px;
}
/deep/ .subject-title-row {
display: flex;
align-items: center;
}
/deep/ .subject-title-input {
flex: 1;
}
/deep/ .subject-visible-btn {
height: 40px;
margin-left: 8px;
flex-shrink: 0;
}
/deep/ .subject-toolbar {
display: flex;
justify-content: space-between;
@@ -101,6 +101,12 @@ layout("/layouts/platform_h5.html"){
resize: none;
width: 100%;
}
.required-star {
color: #ee0a24;
margin-left: 4px;
font-weight: bold;
}
</style>
<div id="app" v-cloak>
@@ -109,7 +115,7 @@ layout("/layouts/platform_h5.html"){
<h2 class="title">{{ activity.title }}</h2>
<div v-if="!isFinished">
<div v-for="(subject, index) in visibleSubjects" :key="subject.id" class="subject">
<p>{{index+1}}、{{ subject.title }}</p>
<p>{{index+1}}、{{ subject.title }}<span class="required-star">*</span></p>
<div class="tag">
<van-tag v-if="subject.type === 'checkbox'" type="primary" size="large">多选</van-tag>
<van-tag v-if="subject.type === 'radio'" type="primary" size="large">单选</van-tag>