Add points task submission and review workflow

This commit is contained in:
王鹏
2026-09-16 16:50:05 +08:00
parent 0e24c9e235
commit e31af58d58
24 changed files with 2097 additions and 18 deletions

48
docs/point-task-setup.md Normal file
View File

@@ -0,0 +1,48 @@
# 积分任务中心上线说明
## 1. 初始化数据库
在目标业务数据库执行:
```text
sql/point_task.sql
```
脚本会创建任务、提交、附件、审核日志表,为积分流水增加业务幂等字段,并添加“积分任务”和“任务审核”后台菜单。
## 2. 七牛云配置
确认 `ruoyi-admin/src/main/resources/qiniu.properties` 中以下配置与七牛空间一致:
```properties
qiniu.bucket=blog
qiniu.zone=zone0
qiniu.domain-of-bucket=img.yidaima.cn
qiniu.upload-host=https://upload.qiniup.com
```
任务截图使用客户端直传。后端签发10分钟有效、绑定单个对象Key、限制5MB且仅允许 JPG/PNG/WebP 的上传凭证。
上传凭证接口按IP限制每分钟最多20次;每个用户最多保留9张未提交截图,每天最多申请30张截图凭证。
后端每天凌晨 03:30 自动清理超过24小时仍未绑定任务的七牛临时截图和数据库临时记录。
## 3. 微信公众平台合法域名
在小程序后台配置:
- `uploadFile` 合法域名:`https://upload.qiniup.com`
- `downloadFile` 合法域名:七牛图片域名,例如 `https://img.yidaima.cn`
- `request` 合法域名:现有业务接口域名
如果七牛空间区域发生变化,请同步修改 `qiniu.upload-host` 和微信小程序合法域名。
## 4. 发布顺序
1. 执行数据库脚本。
2. 发布 RuoYi 后端。
3. 发布 RuoYi 管理端。
4. 在后台创建一次性或每日任务。
5. 发布小程序前端。
未执行数据库脚本前,不要调用新的任务接口。

View File

@@ -0,0 +1,267 @@
package com.ruoyi.web.controller.app;
import java.io.UnsupportedEncodingException;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.ruoyi.app.domain.AppPointTask;
import com.ruoyi.app.domain.AppPointTaskAttachment;
import com.ruoyi.app.domain.AppPointTaskSubmission;
import com.ruoyi.app.domain.AppPointTaskUploadRequest;
import com.ruoyi.app.service.IAppPointTaskService;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.annotation.RateLimiter;
import com.ruoyi.common.annotation.RepeatSubmit;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.enums.LimitType;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.uuid.IdUtils;
import com.ruoyi.web.qiniu.QiNiuConfig;
import com.ruoyi.web.qiniu.QiNiuUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/** 积分任务、小程序提交和后台审核接口。 */
@RestController
@RequestMapping("/app/pointTask")
public class AppPointTaskController extends BaseController
{
private static final long MAX_FILE_SIZE = 5L * 1024L * 1024L;
private static final int UPLOAD_TOKEN_EXPIRES = 600;
@Autowired
private IAppPointTaskService pointTaskService;
@PreAuthorize("isAuthenticated()")
@GetMapping("/active")
public AjaxResult activeList()
{
return success(pointTaskService.selectActiveTaskList(getUserId()));
}
@PreAuthorize("isAuthenticated()")
@GetMapping("/active/{id}")
public AjaxResult activeDetail(@PathVariable Long id)
{
return success(pointTaskService.selectActiveTaskDetail(id, getUserId()));
}
@PreAuthorize("isAuthenticated()")
@RateLimiter(time = 60, count = 20, limitType = LimitType.IP)
@PostMapping("/upload-token")
public AjaxResult uploadToken(@RequestBody AppPointTaskUploadRequest request)
{
validateUploadRequest(request);
Long userId = getUserId();
String extension = extensionOf(request.getFileName(), request.getContentType());
String key = "point-task/" + userId + "/"
+ LocalDate.now(ZoneId.of("Asia/Shanghai")).toString().substring(0, 7).replace("-", "")
+ "/" + IdUtils.fastSimpleUUID() + extension;
AppPointTaskAttachment attachment = new AppPointTaskAttachment();
attachment.setUserId(userId);
attachment.setObjectKey(key);
attachment.setOriginalName(request.getFileName());
attachment.setFileSize(request.getFileSize());
attachment.setFileType(request.getContentType());
pointTaskService.createTempAttachment(attachment);
Map<String, Object> data = new HashMap<>();
data.put("uploadToken", QiNiuUtil.createTaskImageUploadToken(key, UPLOAD_TOKEN_EXPIRES, MAX_FILE_SIZE));
data.put("objectKey", key);
data.put("uploadHost", QiNiuConfig.getInstance().getUploadHost());
data.put("expiresIn", UPLOAD_TOKEN_EXPIRES);
return success(data);
}
@PreAuthorize("isAuthenticated()")
@Log(title = "积分任务提交", businessType = BusinessType.INSERT)
@RepeatSubmit(interval = 3000)
@PostMapping("/{taskId}/submit")
public AjaxResult submit(@PathVariable Long taskId, @RequestBody AppPointTaskSubmission request)
{
verifyUploadedFiles(request.getAttachmentKeys(), getUserId());
return success(pointTaskService.submitTask(taskId, request, getUserId()));
}
@PreAuthorize("isAuthenticated()")
@Log(title = "积分任务重新提交", businessType = BusinessType.UPDATE)
@RepeatSubmit(interval = 3000)
@PostMapping("/submission/{id}/resubmit")
public AjaxResult resubmit(@PathVariable Long id, @RequestBody AppPointTaskSubmission request)
{
verifyUploadedFiles(request.getAttachmentKeys(), getUserId());
return success(pointTaskService.resubmitTask(id, request, getUserId()));
}
@PreAuthorize("isAuthenticated()")
@GetMapping("/submission/my")
public TableDataInfo myList()
{
startPage();
return getDataTable(pointTaskService.selectMySubmissionList(getUserId()));
}
@PreAuthorize("isAuthenticated()")
@GetMapping("/submission/my/{id}")
public AjaxResult myDetail(@PathVariable Long id)
{
AppPointTaskSubmission submission = pointTaskService.selectMySubmissionDetail(id, getUserId());
populatePreviewUrls(submission);
return success(submission);
}
@PreAuthorize("@ss.hasPermi('app:pointTask:list')")
@GetMapping("/list")
public TableDataInfo list(AppPointTask task)
{
startPage();
return getDataTable(pointTaskService.selectAppPointTaskList(task));
}
@PreAuthorize("@ss.hasPermi('app:pointTask:query')")
@GetMapping("/{id}")
public AjaxResult detail(@PathVariable Long id)
{
return success(pointTaskService.selectAppPointTaskById(id));
}
@PreAuthorize("@ss.hasPermi('app:pointTask:add')")
@Log(title = "积分任务", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody AppPointTask task)
{
task.setCreateBy(getUsername());
task.setUpdateBy(getUsername());
return toAjax(pointTaskService.insertAppPointTask(task));
}
@PreAuthorize("@ss.hasPermi('app:pointTask:edit')")
@Log(title = "积分任务", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody AppPointTask task)
{
task.setUpdateBy(getUsername());
return toAjax(pointTaskService.updateAppPointTask(task));
}
@PreAuthorize("@ss.hasPermi('app:pointTask:remove')")
@Log(title = "积分任务", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids)
{
return toAjax(pointTaskService.deleteAppPointTaskByIds(ids));
}
@PreAuthorize("@ss.hasPermi('app:pointTaskReview:list')")
@GetMapping("/submission/list")
public TableDataInfo submissionList(AppPointTaskSubmission submission)
{
startPage();
return getDataTable(pointTaskService.selectSubmissionList(submission));
}
@PreAuthorize("@ss.hasPermi('app:pointTaskReview:query')")
@GetMapping("/submission/{id}")
public AjaxResult submissionDetail(@PathVariable Long id)
{
AppPointTaskSubmission submission = pointTaskService.selectSubmissionDetail(id);
if (submission == null)
{
return error("任务记录不存在");
}
populatePreviewUrls(submission);
return success(submission);
}
@PreAuthorize("@ss.hasPermi('app:pointTaskReview:edit')")
@Log(title = "积分任务审核通过", businessType = BusinessType.UPDATE)
@PostMapping("/submission/{id}/approve")
public AjaxResult approve(@PathVariable Long id)
{
return toAjax(pointTaskService.approveSubmission(id, getUsername()));
}
@PreAuthorize("@ss.hasPermi('app:pointTaskReview:edit')")
@Log(title = "积分任务审核驳回", businessType = BusinessType.UPDATE)
@PostMapping("/submission/{id}/reject")
public AjaxResult reject(@PathVariable Long id, @RequestBody AppPointTaskSubmission request)
{
return toAjax(pointTaskService.rejectSubmission(id, request.getRejectReason(), getUsername()));
}
@PreAuthorize("@ss.hasPermi('app:pointTaskReview:revoke')")
@Log(title = "积分任务撤销奖励", businessType = BusinessType.UPDATE)
@PostMapping("/submission/{id}/revoke")
public AjaxResult revoke(@PathVariable Long id, @RequestBody AppPointTaskSubmission request)
{
return toAjax(pointTaskService.revokeSubmission(id, request.getRejectReason(), getUsername()));
}
private void validateUploadRequest(AppPointTaskUploadRequest request)
{
if (request == null || StringUtils.isEmpty(request.getFileName()) || request.getFileSize() == null
|| request.getFileSize() <= 0 || request.getFileSize() > MAX_FILE_SIZE)
{
throw new ServiceException("截图大小不能超过5MB");
}
String contentType = StringUtils.trim(request.getContentType());
if (!"image/jpeg".equals(contentType) && !"image/png".equals(contentType) && !"image/webp".equals(contentType))
{
throw new ServiceException("仅支持JPG、PNG和WebP图片");
}
}
private String extensionOf(String fileName, String contentType)
{
if ("image/png".equals(contentType)) return ".png";
if ("image/webp".equals(contentType)) return ".webp";
return ".jpg";
}
private void verifyUploadedFiles(List<String> keys, Long userId)
{
if (keys == null || keys.isEmpty() || keys.size() > 3)
{
throw new ServiceException("请上传1至3张任务截图");
}
String prefix = "point-task/" + userId + "/";
for (String key : keys)
{
if (StringUtils.isEmpty(key) || !key.startsWith(prefix) || !QiNiuUtil.exists(key))
{
throw new ServiceException("截图上传未完成,请重新上传");
}
}
}
private void populatePreviewUrls(AppPointTaskSubmission submission)
{
if (submission == null || submission.getAttachments() == null) return;
for (AppPointTaskAttachment attachment : submission.getAttachments())
{
try
{
attachment.setPreviewUrl(QiNiuUtil.fileUrl(attachment.getObjectKey()));
}
catch (UnsupportedEncodingException ex)
{
attachment.setPreviewUrl(null);
}
}
}
}

View File

@@ -20,6 +20,7 @@ import org.springframework.web.multipart.MultipartFile;
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUploadUtils;
import com.ruoyi.common.utils.file.FileUtils;
@@ -94,21 +95,34 @@ public class CommonController {
@PostMapping("/qiniuUpload")
public AjaxResult qiniuUploadFile(MultipartFile file) throws Exception {
try {
// 上传并返回新文件名称
if (file == null || file.isEmpty()) {
return AjaxResult.error("上传文件不能为空");
}
String originalFilename = file.getOriginalFilename();
String fname = originalFilename.substring(0, originalFilename.lastIndexOf('.')) + "-" + UUID.randomUUID().toString().replace("-", "");
String fkey = "http://" + QiNiuConfig.getInstance().getDomainOfBucket() + "/" + fname;
String extension = "";
if (originalFilename != null && originalFilename.lastIndexOf('.') >= 0) {
extension = originalFilename.substring(originalFilename.lastIndexOf('.')).toLowerCase();
}
if (!extension.equals(".jpg") && !extension.equals(".jpeg")
&& !extension.equals(".png") && !extension.equals(".webp")) {
return AjaxResult.error("仅支持JPG、PNG和WebP图片");
}
String fname = "admin-images/" + DateUtils.datePath() + "/"
+ UUID.randomUUID().toString().replace("-", "") + extension;
String domain = QiNiuConfig.getInstance().getDomainOfBucket();
if (!domain.startsWith("http://") && !domain.startsWith("https://")) {
domain = "https://" + domain;
}
String fkey = domain.replaceAll("/$", "") + "/" + fname;
if(!StringUtils.isNull(QiNiuConfig.getInstance().getPictureStyle()) && !QiNiuConfig.getInstance().getPictureStyle().equals("")){
fkey = fkey + "?" + QiNiuConfig.getInstance().getPictureStyle();
}
try {
QiNiuUtil.uploadMultipartFile(file, fname, false);
} catch (Exception e) {
e.printStackTrace();
if (!QiNiuUtil.uploadMultipartFile(file, fname, false)) {
return AjaxResult.error("七牛云上传失败,请检查七牛配置或网络");
}
AjaxResult ajax = AjaxResult.success();
ajax.put("url", fkey);
ajax.put("fileName", file.getOriginalFilename());
ajax.put("fileName", originalFilename);
return ajax;
} catch (Exception e) {
return AjaxResult.error(e.getMessage());

View File

@@ -10,6 +10,7 @@ public class QiNiuConfig {
private String bucket;
private Zone zone;
private String domainOfBucket;
private String uploadHost;
private long expireInSeconds;
private String pictureStyle;
@@ -23,6 +24,7 @@ public class QiNiuConfig {
secretKey = prop.getProperty("qiniu.secret-key");
bucket = prop.getProperty("qiniu.bucket");
domainOfBucket = prop.getProperty("qiniu.domain-of-bucket");
uploadHost = prop.getProperty("qiniu.upload-host", "https://upload.qiniup.com");
expireInSeconds = Long.parseLong(prop.getProperty("qiniu.expire-in-seconds"));
pictureStyle = prop.getProperty("qiniu.picture-style");
String zoneName = prop.getProperty("qiniu.zone");
@@ -88,6 +90,14 @@ public class QiNiuConfig {
this.domainOfBucket = domainOfBucket;
}
public String getUploadHost() {
return uploadHost;
}
public void setUploadHost(String uploadHost) {
this.uploadHost = uploadHost;
}
public long getExpireInSeconds() {
return expireInSeconds;
}

View File

@@ -7,7 +7,9 @@ import com.qiniu.storage.BucketManager;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.UploadManager;
import com.qiniu.storage.model.DefaultPutRet;
import com.qiniu.storage.model.FileInfo;
import com.qiniu.util.Auth;
import com.qiniu.util.StringMap;
import com.qiniu.util.UrlSafeBase64;
import okhttp3.OkHttpClient;
import okhttp3.Request;
@@ -101,8 +103,19 @@ public class QiNiuUtil {
* @throws UnsupportedEncodingException
*/
public static String fileUrl(String fileName) throws UnsupportedEncodingException {
String encodedFileName = URLEncoder.encode(fileName, "utf-8");
String publicUrl = String.format("%s/%s", QiNiuConfig.getInstance().getDomainOfBucket(), encodedFileName);
String[] parts = fileName.split("/");
StringBuilder encoded = new StringBuilder();
for (String part : parts) {
if (encoded.length() > 0) {
encoded.append('/');
}
encoded.append(URLEncoder.encode(part, "utf-8").replace("+", "%20"));
}
String domain = QiNiuConfig.getInstance().getDomainOfBucket();
if (!domain.startsWith("http://") && !domain.startsWith("https://")) {
domain = "https://" + domain;
}
String publicUrl = String.format("%s/%s", domain.replaceAll("/$", ""), encoded);
Auth auth = getAuth();
long expireInSeconds = QiNiuConfig.getInstance().getExpireInSeconds();
if(-1 == expireInSeconds){
@@ -168,20 +181,46 @@ public class QiNiuUtil {
return auth;
}
/** 生成只允许上传指定任务截图的短期凭证。 */
public static String createTaskImageUploadToken(String key, int expiresSeconds, long fileSizeLimit) {
StringMap policy = new StringMap();
policy.put("insertOnly", 1);
policy.put("fsizeLimit", fileSizeLimit);
policy.put("mimeLimit", "image/jpeg;image/png;image/webp");
return getAuth().uploadToken(QiNiuConfig.getInstance().getBucket(), key, expiresSeconds, policy);
}
/** 判断七牛对象是否真实存在。 */
public static boolean exists(String key) {
Configuration cfg = new Configuration(QiNiuConfig.getInstance().getZone());
BucketManager bucketManager = new BucketManager(getAuth(), cfg);
try {
FileInfo info = bucketManager.stat(QiNiuConfig.getInstance().getBucket(), key);
return info != null && info.fsize > 0;
} catch (QiniuException ex) {
return false;
}
}
/**
* 删除文件
* @param key
*/
public static void delete(String key){
public static boolean delete(String key){
Configuration cfg = new Configuration(QiNiuConfig.getInstance().getZone());
Auth auth = getAuth();
BucketManager bucketManager = new BucketManager(auth, cfg);
try {
bucketManager.delete(QiNiuConfig.getInstance().getBucket(), key);
return true;
} catch (QiniuException ex) {
if (ex.code() == 612) {
return true;
}
//如果遇到异常,说明删除失败
System.err.println(ex.code());
System.err.println(ex.response.toString());
return false;
}
}
}

View File

@@ -0,0 +1,36 @@
package com.ruoyi.web.task;
import java.util.Date;
import java.util.List;
import com.ruoyi.app.domain.AppPointTaskAttachment;
import com.ruoyi.app.mapper.AppPointTaskSubmissionMapper;
import com.ruoyi.web.qiniu.QiNiuUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
/** 每天清理超过24小时仍未绑定任务的七牛临时截图。 */
@Configuration
@EnableScheduling
public class PointTaskAttachmentCleanupJob
{
private static final long TEMP_RETENTION_MILLIS = 24L * 60L * 60L * 1000L;
@Autowired
private AppPointTaskSubmissionMapper submissionMapper;
@Scheduled(cron = "0 30 3 * * ?")
public void cleanExpiredTaskImages()
{
Date beforeTime = new Date(System.currentTimeMillis() - TEMP_RETENTION_MILLIS);
List<AppPointTaskAttachment> attachments = submissionMapper.selectExpiredTempAttachments(beforeTime);
for (AppPointTaskAttachment attachment : attachments)
{
if (QiNiuUtil.delete(attachment.getObjectKey()))
{
submissionMapper.deleteTempAttachmentById(attachment.getId());
}
}
}
}

View File

@@ -54,6 +54,10 @@ public class AppIntegralRecord extends BaseEntity {
private Date integralTime;
private Long fId;
private Long resourceId;
/** 业务类型,用于积分操作幂等。 */
private String bizType;
/** 业务记录主键。 */
private Long bizId;
/**
* 资源标题
@@ -123,6 +127,22 @@ public class AppIntegralRecord extends BaseEntity {
public void setResourceId(Long resourceId) {
this.resourceId = resourceId;
}
public String getBizType() {
return bizType;
}
public void setBizType(String bizType) {
this.bizType = bizType;
}
public Long getBizId() {
return bizId;
}
public void setBizId(Long bizId) {
this.bizId = bizId;
}
public String getResourceTitle() {
return resourceTitle;
@@ -141,6 +161,8 @@ public class AppIntegralRecord extends BaseEntity {
.append("integralNumber", getIntegralNumber())
.append("userId", getUserId())
.append("resourceId", getResourceId())
.append("bizType", getBizType())
.append("bizId", getBizId())
.append("resourceTitle", getResourceTitle())
.append("integralTime", getIntegralTime())
.toString();

View File

@@ -0,0 +1,72 @@
package com.ruoyi.app.domain;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.domain.BaseEntity;
/** 积分任务。 */
public class AppPointTask extends BaseEntity
{
private static final long serialVersionUID = 1L;
private Long id;
private String taskName;
private String taskType;
private String cycleType;
private String coverUrl;
private String description;
private String operationGuide;
private String exampleImages;
private Integer rewardPoints;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date startTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date endTime;
private Integer allowResubmit;
private String status;
private Integer sortOrder;
/** 当前用户在本周期的状态,非数据库字段。 */
private Integer currentStatus;
private Boolean canSubmit;
private Long currentSubmissionId;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date nextAvailableAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getTaskName() { return taskName; }
public void setTaskName(String taskName) { this.taskName = taskName; }
public String getTaskType() { return taskType; }
public void setTaskType(String taskType) { this.taskType = taskType; }
public String getCycleType() { return cycleType; }
public void setCycleType(String cycleType) { this.cycleType = cycleType; }
public String getCoverUrl() { return coverUrl; }
public void setCoverUrl(String coverUrl) { this.coverUrl = coverUrl; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getOperationGuide() { return operationGuide; }
public void setOperationGuide(String operationGuide) { this.operationGuide = operationGuide; }
public String getExampleImages() { return exampleImages; }
public void setExampleImages(String exampleImages) { this.exampleImages = exampleImages; }
public Integer getRewardPoints() { return rewardPoints; }
public void setRewardPoints(Integer rewardPoints) { this.rewardPoints = rewardPoints; }
public Date getStartTime() { return startTime; }
public void setStartTime(Date startTime) { this.startTime = startTime; }
public Date getEndTime() { return endTime; }
public void setEndTime(Date endTime) { this.endTime = endTime; }
public Integer getAllowResubmit() { return allowResubmit; }
public void setAllowResubmit(Integer allowResubmit) { this.allowResubmit = allowResubmit; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public Integer getSortOrder() { return sortOrder; }
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
public Integer getCurrentStatus() { return currentStatus; }
public void setCurrentStatus(Integer currentStatus) { this.currentStatus = currentStatus; }
public Boolean getCanSubmit() { return canSubmit; }
public void setCanSubmit(Boolean canSubmit) { this.canSubmit = canSubmit; }
public Long getCurrentSubmissionId() { return currentSubmissionId; }
public void setCurrentSubmissionId(Long currentSubmissionId) { this.currentSubmissionId = currentSubmissionId; }
public Date getNextAvailableAt() { return nextAvailableAt; }
public void setNextAvailableAt(Date nextAvailableAt) { this.nextAvailableAt = nextAvailableAt; }
}

View File

@@ -0,0 +1,51 @@
package com.ruoyi.app.domain;
import java.io.Serializable;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
/** 积分任务截图附件。 */
public class AppPointTaskAttachment implements Serializable
{
private static final long serialVersionUID = 1L;
private Long id;
private Long submissionId;
private Long userId;
private String objectKey;
private String originalName;
private Long fileSize;
private String fileType;
private String uploadStatus;
private Integer submitBatch;
private Integer isCurrent;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/** 七牛临时访问地址,非数据库字段。 */
private String previewUrl;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Long getSubmissionId() { return submissionId; }
public void setSubmissionId(Long submissionId) { this.submissionId = submissionId; }
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public String getObjectKey() { return objectKey; }
public void setObjectKey(String objectKey) { this.objectKey = objectKey; }
public String getOriginalName() { return originalName; }
public void setOriginalName(String originalName) { this.originalName = originalName; }
public Long getFileSize() { return fileSize; }
public void setFileSize(Long fileSize) { this.fileSize = fileSize; }
public String getFileType() { return fileType; }
public void setFileType(String fileType) { this.fileType = fileType; }
public String getUploadStatus() { return uploadStatus; }
public void setUploadStatus(String uploadStatus) { this.uploadStatus = uploadStatus; }
public Integer getSubmitBatch() { return submitBatch; }
public void setSubmitBatch(Integer submitBatch) { this.submitBatch = submitBatch; }
public Integer getIsCurrent() { return isCurrent; }
public void setIsCurrent(Integer isCurrent) { this.isCurrent = isCurrent; }
public Date getCreateTime() { return createTime; }
public void setCreateTime(Date createTime) { this.createTime = createTime; }
public String getPreviewUrl() { return previewUrl; }
public void setPreviewUrl(String previewUrl) { this.previewUrl = previewUrl; }
}

View File

@@ -0,0 +1,29 @@
package com.ruoyi.app.domain;
import java.io.Serializable;
import java.util.Date;
/** 积分任务审核日志。 */
public class AppPointTaskAuditLog implements Serializable
{
private static final long serialVersionUID = 1L;
private Long submissionId;
private Integer beforeStatus;
private Integer afterStatus;
private String operatorName;
private String remark;
private Date createTime;
public Long getSubmissionId() { return submissionId; }
public void setSubmissionId(Long submissionId) { this.submissionId = submissionId; }
public Integer getBeforeStatus() { return beforeStatus; }
public void setBeforeStatus(Integer beforeStatus) { this.beforeStatus = beforeStatus; }
public Integer getAfterStatus() { return afterStatus; }
public void setAfterStatus(Integer afterStatus) { this.afterStatus = afterStatus; }
public String getOperatorName() { return operatorName; }
public void setOperatorName(String operatorName) { this.operatorName = operatorName; }
public String getRemark() { return remark; }
public void setRemark(String remark) { this.remark = remark; }
public Date getCreateTime() { return createTime; }
public void setCreateTime(Date createTime) { this.createTime = createTime; }
}

View File

@@ -0,0 +1,81 @@
package com.ruoyi.app.domain;
import java.util.Date;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.domain.BaseEntity;
/** 积分任务提交记录。 */
public class AppPointTaskSubmission extends BaseEntity
{
private static final long serialVersionUID = 1L;
private Long id;
private String submissionNo;
private Long taskId;
private Long userId;
private String cycleKey;
private Integer rewardPoints;
private Integer status;
private Integer attemptCount;
private String rejectReason;
private String reviewBy;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date reviewTime;
private Long rewardRecordId;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date firstSubmitTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date lastSubmitTime;
private String taskName;
private String taskType;
private String cycleType;
private String userName;
private String nickName;
private List<String> attachmentKeys;
private List<AppPointTaskAttachment> attachments;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getSubmissionNo() { return submissionNo; }
public void setSubmissionNo(String submissionNo) { this.submissionNo = submissionNo; }
public Long getTaskId() { return taskId; }
public void setTaskId(Long taskId) { this.taskId = taskId; }
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
public String getCycleKey() { return cycleKey; }
public void setCycleKey(String cycleKey) { this.cycleKey = cycleKey; }
public Integer getRewardPoints() { return rewardPoints; }
public void setRewardPoints(Integer rewardPoints) { this.rewardPoints = rewardPoints; }
public Integer getStatus() { return status; }
public void setStatus(Integer status) { this.status = status; }
public Integer getAttemptCount() { return attemptCount; }
public void setAttemptCount(Integer attemptCount) { this.attemptCount = attemptCount; }
public String getRejectReason() { return rejectReason; }
public void setRejectReason(String rejectReason) { this.rejectReason = rejectReason; }
public String getReviewBy() { return reviewBy; }
public void setReviewBy(String reviewBy) { this.reviewBy = reviewBy; }
public Date getReviewTime() { return reviewTime; }
public void setReviewTime(Date reviewTime) { this.reviewTime = reviewTime; }
public Long getRewardRecordId() { return rewardRecordId; }
public void setRewardRecordId(Long rewardRecordId) { this.rewardRecordId = rewardRecordId; }
public Date getFirstSubmitTime() { return firstSubmitTime; }
public void setFirstSubmitTime(Date firstSubmitTime) { this.firstSubmitTime = firstSubmitTime; }
public Date getLastSubmitTime() { return lastSubmitTime; }
public void setLastSubmitTime(Date lastSubmitTime) { this.lastSubmitTime = lastSubmitTime; }
public String getTaskName() { return taskName; }
public void setTaskName(String taskName) { this.taskName = taskName; }
public String getTaskType() { return taskType; }
public void setTaskType(String taskType) { this.taskType = taskType; }
public String getCycleType() { return cycleType; }
public void setCycleType(String cycleType) { this.cycleType = cycleType; }
public String getUserName() { return userName; }
public void setUserName(String userName) { this.userName = userName; }
public String getNickName() { return nickName; }
public void setNickName(String nickName) { this.nickName = nickName; }
public List<String> getAttachmentKeys() { return attachmentKeys; }
public void setAttachmentKeys(List<String> attachmentKeys) { this.attachmentKeys = attachmentKeys; }
public List<AppPointTaskAttachment> getAttachments() { return attachments; }
public void setAttachments(List<AppPointTaskAttachment> attachments) { this.attachments = attachments; }
}

View File

@@ -0,0 +1,19 @@
package com.ruoyi.app.domain;
import java.io.Serializable;
/** 七牛任务截图上传凭证请求。 */
public class AppPointTaskUploadRequest implements Serializable
{
private static final long serialVersionUID = 1L;
private String fileName;
private Long fileSize;
private String contentType;
public String getFileName() { return fileName; }
public void setFileName(String fileName) { this.fileName = fileName; }
public Long getFileSize() { return fileSize; }
public void setFileSize(Long fileSize) { this.fileSize = fileSize; }
public String getContentType() { return contentType; }
public void setContentType(String contentType) { this.contentType = contentType; }
}

View File

@@ -0,0 +1,15 @@
package com.ruoyi.app.mapper;
import java.util.List;
import com.ruoyi.app.domain.AppPointTask;
public interface AppPointTaskMapper
{
AppPointTask selectAppPointTaskById(Long id);
List<AppPointTask> selectAppPointTaskList(AppPointTask task);
List<AppPointTask> selectActiveAppPointTaskList();
int insertAppPointTask(AppPointTask task);
int updateAppPointTask(AppPointTask task);
int deleteAppPointTaskByIds(Long[] ids);
int countSubmissionByTaskIds(Long[] ids);
}

View File

@@ -0,0 +1,37 @@
package com.ruoyi.app.mapper;
import java.util.List;
import java.util.Date;
import com.ruoyi.app.domain.AppPointTaskAttachment;
import com.ruoyi.app.domain.AppPointTaskAuditLog;
import com.ruoyi.app.domain.AppPointTaskSubmission;
import org.apache.ibatis.annotations.Param;
public interface AppPointTaskSubmissionMapper
{
AppPointTaskSubmission selectSubmissionById(Long id);
AppPointTaskSubmission selectSubmissionByIdForUpdate(Long id);
AppPointTaskSubmission selectByTaskUserCycle(@Param("taskId") Long taskId,
@Param("userId") Long userId,
@Param("cycleKey") String cycleKey);
List<AppPointTaskSubmission> selectSubmissionList(AppPointTaskSubmission submission);
List<AppPointTaskSubmission> selectMySubmissionList(@Param("userId") Long userId);
int insertSubmission(AppPointTaskSubmission submission);
int updateSubmission(AppPointTaskSubmission submission);
int insertTempAttachment(AppPointTaskAttachment attachment);
Long lockUserForUpload(@Param("userId") Long userId);
int countTempAttachmentsByUser(@Param("userId") Long userId);
int countAttachmentsByUserSince(@Param("userId") Long userId, @Param("since") String since);
List<AppPointTaskAttachment> selectTempAttachments(@Param("userId") Long userId,
@Param("keys") List<String> keys);
int bindAttachments(@Param("userId") Long userId,
@Param("submissionId") Long submissionId,
@Param("submitBatch") Integer submitBatch,
@Param("keys") List<String> keys);
int markAttachmentsNotCurrent(Long submissionId);
List<AppPointTaskAttachment> selectAttachmentsBySubmissionId(Long submissionId);
List<AppPointTaskAttachment> selectExpiredTempAttachments(Date beforeTime);
int deleteTempAttachmentById(Long id);
int insertAuditLog(AppPointTaskAuditLog auditLog);
}

View File

@@ -0,0 +1,28 @@
package com.ruoyi.app.service;
import java.util.List;
import com.ruoyi.app.domain.AppPointTask;
import com.ruoyi.app.domain.AppPointTaskAttachment;
import com.ruoyi.app.domain.AppPointTaskSubmission;
public interface IAppPointTaskService
{
AppPointTask selectAppPointTaskById(Long id);
List<AppPointTask> selectAppPointTaskList(AppPointTask task);
List<AppPointTask> selectActiveTaskList(Long userId);
AppPointTask selectActiveTaskDetail(Long id, Long userId);
int insertAppPointTask(AppPointTask task);
int updateAppPointTask(AppPointTask task);
int deleteAppPointTaskByIds(Long[] ids);
AppPointTaskAttachment createTempAttachment(AppPointTaskAttachment attachment);
AppPointTaskSubmission submitTask(Long taskId, AppPointTaskSubmission request, Long userId);
AppPointTaskSubmission resubmitTask(Long submissionId, AppPointTaskSubmission request, Long userId);
List<AppPointTaskSubmission> selectMySubmissionList(Long userId);
AppPointTaskSubmission selectMySubmissionDetail(Long id, Long userId);
List<AppPointTaskSubmission> selectSubmissionList(AppPointTaskSubmission submission);
AppPointTaskSubmission selectSubmissionDetail(Long id);
int approveSubmission(Long id, String operator);
int rejectSubmission(Long id, String rejectReason, String operator);
int revokeSubmission(Long id, String reason, String operator);
}

View File

@@ -0,0 +1,544 @@
package com.ruoyi.app.service.impl;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
import com.ruoyi.app.domain.AppIntegralRecord;
import com.ruoyi.app.domain.AppPointTask;
import com.ruoyi.app.domain.AppPointTaskAttachment;
import com.ruoyi.app.domain.AppPointTaskAuditLog;
import com.ruoyi.app.domain.AppPointTaskSubmission;
import com.ruoyi.app.mapper.AppIntegralRecordMapper;
import com.ruoyi.app.mapper.AppPointTaskMapper;
import com.ruoyi.app.mapper.AppPointTaskSubmissionMapper;
import com.ruoyi.app.service.IAppPointTaskService;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.uuid.IdUtils;
import com.ruoyi.system.mapper.SysUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AppPointTaskServiceImpl implements IAppPointTaskService
{
private static final String CYCLE_ONCE = "ONCE";
private static final String CYCLE_DAILY = "DAILY";
private static final int STATUS_PENDING = 0;
private static final int STATUS_APPROVED = 1;
private static final int STATUS_REJECTED = 2;
private static final int STATUS_REVOKED = 3;
private static final int MAX_TEMP_ATTACHMENTS_PER_USER = 9;
private static final int MAX_DAILY_ATTACHMENTS_PER_USER = 30;
private static final ZoneId SHANGHAI_ZONE = ZoneId.of("Asia/Shanghai");
@Autowired
private AppPointTaskMapper taskMapper;
@Autowired
private AppPointTaskSubmissionMapper submissionMapper;
@Autowired
private AppIntegralRecordMapper integralRecordMapper;
@Autowired
private SysUserMapper userMapper;
@Override
public AppPointTask selectAppPointTaskById(Long id)
{
return taskMapper.selectAppPointTaskById(id);
}
@Override
public List<AppPointTask> selectAppPointTaskList(AppPointTask task)
{
return taskMapper.selectAppPointTaskList(task);
}
@Override
public List<AppPointTask> selectActiveTaskList(Long userId)
{
List<AppPointTask> tasks = taskMapper.selectActiveAppPointTaskList();
for (AppPointTask task : tasks)
{
populateUserState(task, userId);
}
return tasks;
}
@Override
public AppPointTask selectActiveTaskDetail(Long id, Long userId)
{
AppPointTask task = taskMapper.selectAppPointTaskById(id);
if (task == null)
{
throw new ServiceException("任务不存在");
}
populateUserState(task, userId);
if (!isTaskActive(task, DateUtils.getNowDate()))
{
task.setCanSubmit(false);
}
return task;
}
@Override
public int insertAppPointTask(AppPointTask task)
{
normalizeAndValidateTask(task);
Date now = DateUtils.getNowDate();
task.setCreateTime(now);
task.setUpdateTime(now);
return taskMapper.insertAppPointTask(task);
}
@Override
public int updateAppPointTask(AppPointTask task)
{
if (task.getId() == null)
{
throw new ServiceException("任务不存在");
}
AppPointTask current = taskMapper.selectAppPointTaskById(task.getId());
if (current == null)
{
throw new ServiceException("任务不存在");
}
normalizeAndValidateTask(task);
if (taskMapper.countSubmissionByTaskIds(new Long[] { task.getId() }) > 0
&& (!current.getRewardPoints().equals(task.getRewardPoints())
|| !current.getCycleType().equals(task.getCycleType())))
{
throw new ServiceException("任务已有用户提交,不能修改奖励积分或任务周期");
}
task.setUpdateTime(DateUtils.getNowDate());
return taskMapper.updateAppPointTask(task);
}
@Override
public int deleteAppPointTaskByIds(Long[] ids)
{
if (ids == null || ids.length == 0)
{
return 0;
}
if (taskMapper.countSubmissionByTaskIds(ids) > 0)
{
throw new ServiceException("任务已有用户提交,请停用任务,不要删除");
}
return taskMapper.deleteAppPointTaskByIds(ids);
}
@Override
@Transactional
public AppPointTaskAttachment createTempAttachment(AppPointTaskAttachment attachment)
{
Long userId = attachment.getUserId();
if (userId == null || submissionMapper.lockUserForUpload(userId) == null)
{
throw new ServiceException("用户不存在,请重新登录");
}
if (submissionMapper.countTempAttachmentsByUser(userId) >= MAX_TEMP_ATTACHMENTS_PER_USER)
{
throw new ServiceException("未提交截图已达上限,请先完成任务提交或等待临时截图清理后再试");
}
String todayStart = LocalDate.now(SHANGHAI_ZONE) + " 00:00:00";
if (submissionMapper.countAttachmentsByUserSince(userId, todayStart) >= MAX_DAILY_ATTACHMENTS_PER_USER)
{
throw new ServiceException("今日上传次数已达上限,请明日再试");
}
attachment.setUploadStatus("TEMP");
attachment.setSubmitBatch(0);
attachment.setIsCurrent(0);
attachment.setCreateTime(DateUtils.getNowDate());
if (submissionMapper.insertTempAttachment(attachment) != 1)
{
throw new ServiceException("创建上传记录失败,请重试");
}
return attachment;
}
@Override
@Transactional
public AppPointTaskSubmission submitTask(Long taskId, AppPointTaskSubmission request, Long userId)
{
AppPointTask task = requireActiveTask(taskId);
List<String> keys = normalizeAttachmentKeys(request.getAttachmentKeys());
String cycleKey = cycleKey(task);
if (submissionMapper.selectByTaskUserCycle(taskId, userId, cycleKey) != null)
{
throw new ServiceException(CYCLE_DAILY.equals(task.getCycleType())
? "今日已经提交过该任务" : "该任务已经提交过");
}
assertTempAttachments(userId, keys);
Date now = DateUtils.getNowDate();
AppPointTaskSubmission submission = new AppPointTaskSubmission();
submission.setSubmissionNo("RW" + DateUtils.dateTimeNow() + IdUtils.fastSimpleUUID().substring(0, 6).toUpperCase());
submission.setTaskId(taskId);
submission.setUserId(userId);
submission.setCycleKey(cycleKey);
submission.setRewardPoints(task.getRewardPoints());
submission.setRemark(normalizeRemark(request.getRemark()));
submission.setStatus(STATUS_PENDING);
submission.setAttemptCount(1);
submission.setFirstSubmitTime(now);
submission.setLastSubmitTime(now);
submission.setCreateBy(String.valueOf(userId));
submission.setCreateTime(now);
submission.setUpdateBy(String.valueOf(userId));
submission.setUpdateTime(now);
try
{
if (submissionMapper.insertSubmission(submission) != 1)
{
throw new ServiceException("任务提交失败,请重试");
}
}
catch (DuplicateKeyException ex)
{
throw new ServiceException("该任务本周期已经提交过");
}
bindAttachments(userId, submission.getId(), 1, keys);
insertAudit(submission.getId(), null, STATUS_PENDING, String.valueOf(userId), "用户提交任务");
return selectSubmissionDetail(submission.getId());
}
@Override
@Transactional
public AppPointTaskSubmission resubmitTask(Long submissionId, AppPointTaskSubmission request, Long userId)
{
AppPointTaskSubmission current = submissionMapper.selectSubmissionByIdForUpdate(submissionId);
if (current == null || !userId.equals(current.getUserId()))
{
throw new ServiceException("任务记录不存在");
}
if (current.getStatus() != STATUS_REJECTED)
{
throw new ServiceException("只有审核未通过的任务可以重新提交");
}
AppPointTask task = requireActiveTask(current.getTaskId());
if (CYCLE_DAILY.equals(task.getCycleType()) && !cycleKey(task).equals(current.getCycleKey()))
{
throw new ServiceException("该每日任务周期已结束,请提交今日任务");
}
List<String> keys = normalizeAttachmentKeys(request.getAttachmentKeys());
assertTempAttachments(userId, keys);
int nextBatch = current.getAttemptCount() + 1;
submissionMapper.markAttachmentsNotCurrent(submissionId);
bindAttachments(userId, submissionId, nextBatch, keys);
current.setRemark(normalizeRemark(request.getRemark()));
current.setStatus(STATUS_PENDING);
current.setAttemptCount(nextBatch);
current.setRejectReason(null);
current.setReviewBy(null);
current.setReviewTime(null);
current.setLastSubmitTime(DateUtils.getNowDate());
current.setUpdateBy(String.valueOf(userId));
current.setUpdateTime(DateUtils.getNowDate());
submissionMapper.updateSubmission(current);
insertAudit(submissionId, STATUS_REJECTED, STATUS_PENDING, String.valueOf(userId), "用户重新提交任务");
return selectSubmissionDetail(submissionId);
}
@Override
public List<AppPointTaskSubmission> selectMySubmissionList(Long userId)
{
return submissionMapper.selectMySubmissionList(userId);
}
@Override
public AppPointTaskSubmission selectMySubmissionDetail(Long id, Long userId)
{
AppPointTaskSubmission submission = selectSubmissionDetail(id);
if (submission == null || !userId.equals(submission.getUserId()))
{
throw new ServiceException("任务记录不存在");
}
return submission;
}
@Override
public List<AppPointTaskSubmission> selectSubmissionList(AppPointTaskSubmission submission)
{
return submissionMapper.selectSubmissionList(submission);
}
@Override
public AppPointTaskSubmission selectSubmissionDetail(Long id)
{
AppPointTaskSubmission submission = submissionMapper.selectSubmissionById(id);
if (submission != null)
{
submission.setAttachments(submissionMapper.selectAttachmentsBySubmissionId(id));
}
return submission;
}
@Override
@Transactional
public int approveSubmission(Long id, String operator)
{
AppPointTaskSubmission submission = requirePendingSubmission(id);
AppPointTask task = taskMapper.selectAppPointTaskById(submission.getTaskId());
if (task == null)
{
throw new ServiceException("关联任务不存在");
}
if (userMapper.addIntegralByUserId(submission.getRewardPoints(), submission.getUserId()) != 1)
{
throw new ServiceException("积分发放失败,请重试");
}
AppIntegralRecord record = insertIntegralRecord(submission, 0L, task.getTaskName() + "奖励", "POINT_TASK");
submission.setStatus(STATUS_APPROVED);
submission.setRewardRecordId(record.getId());
submission.setRejectReason(null);
submission.setReviewBy(operator);
submission.setReviewTime(DateUtils.getNowDate());
submission.setUpdateBy(operator);
submission.setUpdateTime(DateUtils.getNowDate());
int rows = submissionMapper.updateSubmission(submission);
insertAudit(id, STATUS_PENDING, STATUS_APPROVED, operator, "审核通过并发放" + submission.getRewardPoints() + "积分");
return rows;
}
@Override
@Transactional
public int rejectSubmission(Long id, String rejectReason, String operator)
{
String reason = StringUtils.trim(rejectReason);
if (StringUtils.isEmpty(reason))
{
throw new ServiceException("审核不通过时必须填写原因");
}
AppPointTaskSubmission submission = requirePendingSubmission(id);
submission.setStatus(STATUS_REJECTED);
submission.setRejectReason(reason);
submission.setReviewBy(operator);
submission.setReviewTime(DateUtils.getNowDate());
submission.setUpdateBy(operator);
submission.setUpdateTime(DateUtils.getNowDate());
int rows = submissionMapper.updateSubmission(submission);
insertAudit(id, STATUS_PENDING, STATUS_REJECTED, operator, reason);
return rows;
}
@Override
@Transactional
public int revokeSubmission(Long id, String reason, String operator)
{
String revokeReason = StringUtils.trim(reason);
if (StringUtils.isEmpty(revokeReason))
{
throw new ServiceException("撤销奖励时必须填写原因");
}
AppPointTaskSubmission submission = submissionMapper.selectSubmissionByIdForUpdate(id);
if (submission == null || submission.getStatus() != STATUS_APPROVED)
{
throw new ServiceException("只有已通过的任务可以撤销奖励");
}
AppPointTask task = taskMapper.selectAppPointTaskById(submission.getTaskId());
if (userMapper.delIntegralByUserId(submission.getRewardPoints(), submission.getUserId()) != 1)
{
throw new ServiceException("用户当前积分不足,无法撤销奖励");
}
insertIntegralRecord(submission, 1L, "撤销:" + (task == null ? "积分任务" : task.getTaskName()), "POINT_TASK_REVOKE");
submission.setStatus(STATUS_REVOKED);
submission.setRejectReason(revokeReason);
submission.setReviewBy(operator);
submission.setReviewTime(DateUtils.getNowDate());
submission.setUpdateBy(operator);
submission.setUpdateTime(DateUtils.getNowDate());
int rows = submissionMapper.updateSubmission(submission);
insertAudit(id, STATUS_APPROVED, STATUS_REVOKED, operator, revokeReason);
return rows;
}
private void populateUserState(AppPointTask task, Long userId)
{
AppPointTaskSubmission submission = submissionMapper.selectByTaskUserCycle(task.getId(), userId, cycleKey(task));
if (submission == null)
{
task.setCanSubmit(true);
return;
}
task.setCurrentStatus(submission.getStatus());
task.setCurrentSubmissionId(submission.getId());
task.setCanSubmit(submission.getStatus() == STATUS_REJECTED);
if (CYCLE_DAILY.equals(task.getCycleType()) && submission.getStatus() == STATUS_APPROVED)
{
Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("Asia/Shanghai"));
calendar.add(Calendar.DAY_OF_MONTH, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
task.setNextAvailableAt(calendar.getTime());
}
}
private AppPointTask requireActiveTask(Long id)
{
AppPointTask task = taskMapper.selectAppPointTaskById(id);
Date now = DateUtils.getNowDate();
if (task == null || !"0".equals(task.getStatus()))
{
throw new ServiceException("任务不存在或已停用");
}
if (task.getStartTime() != null && task.getStartTime().after(now))
{
throw new ServiceException("任务尚未开始");
}
if (task.getEndTime() != null && task.getEndTime().before(now))
{
throw new ServiceException("任务已经结束");
}
return task;
}
private boolean isTaskActive(AppPointTask task, Date now)
{
return task != null && "0".equals(task.getStatus())
&& (task.getStartTime() == null || !task.getStartTime().after(now))
&& (task.getEndTime() == null || !task.getEndTime().before(now));
}
private AppPointTaskSubmission requirePendingSubmission(Long id)
{
AppPointTaskSubmission submission = submissionMapper.selectSubmissionByIdForUpdate(id);
if (submission == null)
{
throw new ServiceException("任务记录不存在");
}
if (submission.getStatus() != STATUS_PENDING)
{
throw new ServiceException("该任务已经处理,请勿重复操作");
}
return submission;
}
private String cycleKey(AppPointTask task)
{
return CYCLE_DAILY.equals(task.getCycleType()) ? LocalDate.now(SHANGHAI_ZONE).toString() : CYCLE_ONCE;
}
private List<String> normalizeAttachmentKeys(List<String> keys)
{
if (keys == null)
{
throw new ServiceException("请上传任务截图");
}
Set<String> unique = new LinkedHashSet<>();
for (String key : keys)
{
if (StringUtils.isNotEmpty(StringUtils.trim(key)))
{
unique.add(StringUtils.trim(key));
}
}
if (unique.isEmpty() || unique.size() > 3)
{
throw new ServiceException("请上传1至3张任务截图");
}
return new ArrayList<>(unique);
}
private void assertTempAttachments(Long userId, List<String> keys)
{
if (submissionMapper.selectTempAttachments(userId, keys).size() != keys.size())
{
throw new ServiceException("截图无效或已经被其他任务使用,请重新上传");
}
}
private void bindAttachments(Long userId, Long submissionId, Integer batch, List<String> keys)
{
if (submissionMapper.bindAttachments(userId, submissionId, batch, keys) != keys.size())
{
throw new ServiceException("任务截图绑定失败,请重新上传");
}
}
private void insertAudit(Long submissionId, Integer before, Integer after, String operator, String remark)
{
AppPointTaskAuditLog log = new AppPointTaskAuditLog();
log.setSubmissionId(submissionId);
log.setBeforeStatus(before);
log.setAfterStatus(after);
log.setOperatorName(operator);
log.setRemark(remark);
log.setCreateTime(DateUtils.getNowDate());
submissionMapper.insertAuditLog(log);
}
private AppIntegralRecord insertIntegralRecord(AppPointTaskSubmission submission, Long isAdd, String source, String bizType)
{
AppIntegralRecord record = new AppIntegralRecord();
record.setSource(source);
record.setIsAdd(isAdd);
record.setIntegralNumber(submission.getRewardPoints().longValue());
record.setUserId(submission.getUserId());
record.setIntegralTime(DateUtils.getNowDate());
record.setBizType(bizType);
record.setBizId(submission.getId());
if (integralRecordMapper.insertAppIntegralRecord(record) != 1)
{
throw new ServiceException("积分流水记录失败,请重试");
}
return record;
}
private void normalizeAndValidateTask(AppPointTask task)
{
task.setTaskName(StringUtils.trim(task.getTaskName()));
task.setDescription(StringUtils.trim(task.getDescription()));
task.setOperationGuide(StringUtils.trim(task.getOperationGuide()));
if (StringUtils.isEmpty(task.getTaskName()) || task.getTaskName().length() > 80)
{
throw new ServiceException("任务名称需填写1至80个字");
}
if (!CYCLE_ONCE.equals(task.getCycleType()) && !CYCLE_DAILY.equals(task.getCycleType()))
{
throw new ServiceException("任务周期不正确");
}
if (task.getRewardPoints() == null || task.getRewardPoints() <= 0 || task.getRewardPoints() > 100000)
{
throw new ServiceException("奖励积分必须在1至100000之间");
}
if (task.getStartTime() != null && task.getEndTime() != null && !task.getStartTime().before(task.getEndTime()))
{
throw new ServiceException("结束时间必须晚于开始时间");
}
task.setAllowResubmit(1);
if (task.getSortOrder() == null || task.getSortOrder() < 0)
{
task.setSortOrder(0);
}
if (!"1".equals(task.getStatus()))
{
task.setStatus("0");
}
if (StringUtils.isEmpty(task.getTaskType()))
{
task.setTaskType("OTHER");
}
}
private String normalizeRemark(String remark)
{
String value = StringUtils.trim(remark);
if (value != null && value.length() > 300)
{
throw new ServiceException("补充说明不能超过300个字");
}
return value;
}
}

View File

@@ -11,12 +11,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="integralNumber" column="integral_number" />
<result property="userId" column="user_id" />
<result property="resourceId" column="resource_id" />
<result property="bizType" column="biz_type" />
<result property="bizId" column="biz_id" />
<result property="resourceTitle" column="resource_title" />
<result property="integralTime" column="integral_time" />
</resultMap>
<sql id="selectAppIntegralRecordVo">
select a.id, a.source, a.is_add, a.integral_number, a.user_id, a.resource_id, a.integral_time, r.resource_title
select a.id, a.source, a.is_add, a.integral_number, a.user_id, a.resource_id,
a.biz_type, a.biz_id, a.integral_time, r.resource_title
from app_integral_record a
left join app_resource r on a.resource_id = r.id
</sql>
@@ -68,6 +71,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="integralNumber != null">integral_number,</if>
<if test="userId != null">user_id,</if>
<if test="resourceId != null">resource_id,</if>
<if test="bizType != null">biz_type,</if>
<if test="bizId != null">biz_id,</if>
<if test="integralTime != null">integral_time,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
@@ -76,6 +81,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="integralNumber != null">#{integralNumber},</if>
<if test="userId != null">#{userId},</if>
<if test="resourceId != null">#{resourceId},</if>
<if test="bizType != null">#{bizType},</if>
<if test="bizId != null">#{bizId},</if>
<if test="integralTime != null">#{integralTime},</if>
</trim>
</insert>
@@ -88,6 +95,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="integralNumber != null">integral_number = #{integralNumber},</if>
<if test="userId != null">user_id = #{userId},</if>
<if test="resourceId != null">resource_id = #{resourceId},</if>
<if test="bizType != null">biz_type = #{bizType},</if>
<if test="bizId != null">biz_id = #{bizId},</if>
<if test="integralTime != null">integral_time = #{integralTime},</if>
</trim>
where id = #{id}

View File

@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.app.mapper.AppPointTaskMapper">
<resultMap type="AppPointTask" id="AppPointTaskResult">
<result property="id" column="id" />
<result property="taskName" column="task_name" />
<result property="taskType" column="task_type" />
<result property="cycleType" column="cycle_type" />
<result property="coverUrl" column="cover_url" />
<result property="description" column="description" />
<result property="operationGuide" column="operation_guide" />
<result property="exampleImages" column="example_images" />
<result property="rewardPoints" column="reward_points" />
<result property="startTime" column="start_time" />
<result property="endTime" column="end_time" />
<result property="allowResubmit" column="allow_resubmit" />
<result property="status" column="status" />
<result property="sortOrder" column="sort_order" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
</resultMap>
<sql id="selectTaskVo">
select id, task_name, task_type, cycle_type, cover_url, description, operation_guide,
example_images, reward_points, start_time, end_time, allow_resubmit, status,
sort_order, create_by, create_time, update_by, update_time
from app_point_task
</sql>
<select id="selectAppPointTaskById" resultMap="AppPointTaskResult">
<include refid="selectTaskVo" /> where id = #{id}
</select>
<select id="selectAppPointTaskList" parameterType="AppPointTask" resultMap="AppPointTaskResult">
<include refid="selectTaskVo" />
<where>
<if test="taskName != null and taskName != ''">and task_name like concat('%', #{taskName}, '%')</if>
<if test="taskType != null and taskType != ''">and task_type = #{taskType}</if>
<if test="cycleType != null and cycleType != ''">and cycle_type = #{cycleType}</if>
<if test="status != null and status != ''">and status = #{status}</if>
</where>
order by sort_order asc, id desc
</select>
<select id="selectActiveAppPointTaskList" resultMap="AppPointTaskResult">
<include refid="selectTaskVo" />
where status = '0'
and (start_time is null or start_time &lt;= now())
and (end_time is null or end_time &gt;= now())
order by sort_order asc, id desc
</select>
<insert id="insertAppPointTask" parameterType="AppPointTask" useGeneratedKeys="true" keyProperty="id">
insert into app_point_task
(task_name, task_type, cycle_type, cover_url, description, operation_guide, example_images,
reward_points, start_time, end_time, allow_resubmit, status, sort_order,
create_by, create_time, update_by, update_time)
values
(#{taskName}, #{taskType}, #{cycleType}, #{coverUrl}, #{description}, #{operationGuide}, #{exampleImages},
#{rewardPoints}, #{startTime}, #{endTime}, #{allowResubmit}, #{status}, #{sortOrder},
#{createBy}, #{createTime}, #{updateBy}, #{updateTime})
</insert>
<update id="updateAppPointTask" parameterType="AppPointTask">
update app_point_task
<set>
<if test="taskName != null">task_name = #{taskName},</if>
<if test="taskType != null">task_type = #{taskType},</if>
<if test="cycleType != null">cycle_type = #{cycleType},</if>
<if test="coverUrl != null">cover_url = #{coverUrl},</if>
<if test="description != null">description = #{description},</if>
<if test="operationGuide != null">operation_guide = #{operationGuide},</if>
<if test="exampleImages != null">example_images = #{exampleImages},</if>
<if test="rewardPoints != null">reward_points = #{rewardPoints},</if>
start_time = #{startTime},
end_time = #{endTime},
<if test="allowResubmit != null">allow_resubmit = #{allowResubmit},</if>
<if test="status != null">status = #{status},</if>
<if test="sortOrder != null">sort_order = #{sortOrder},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
update_time = #{updateTime}
</set>
where id = #{id}
</update>
<delete id="deleteAppPointTaskByIds">
delete from app_point_task where id in
<foreach item="id" collection="array" open="(" separator="," close=")">#{id}</foreach>
</delete>
<select id="countSubmissionByTaskIds" resultType="int">
select count(1) from app_point_task_submission where task_id in
<foreach item="id" collection="array" open="(" separator="," close=")">#{id}</foreach>
</select>
</mapper>

View File

@@ -0,0 +1,201 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.app.mapper.AppPointTaskSubmissionMapper">
<resultMap type="AppPointTaskSubmission" id="SubmissionResult">
<result property="id" column="id" />
<result property="submissionNo" column="submission_no" />
<result property="taskId" column="task_id" />
<result property="userId" column="user_id" />
<result property="cycleKey" column="cycle_key" />
<result property="rewardPoints" column="reward_points" />
<result property="remark" column="remark" />
<result property="status" column="status" />
<result property="attemptCount" column="attempt_count" />
<result property="rejectReason" column="reject_reason" />
<result property="reviewBy" column="review_by" />
<result property="reviewTime" column="review_time" />
<result property="rewardRecordId" column="reward_record_id" />
<result property="firstSubmitTime" column="first_submit_time" />
<result property="lastSubmitTime" column="last_submit_time" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="taskName" column="task_name" />
<result property="taskType" column="task_type" />
<result property="cycleType" column="cycle_type" />
<result property="userName" column="user_name" />
<result property="nickName" column="nick_name" />
</resultMap>
<resultMap type="AppPointTaskAttachment" id="AttachmentResult">
<result property="id" column="id" />
<result property="submissionId" column="submission_id" />
<result property="userId" column="user_id" />
<result property="objectKey" column="object_key" />
<result property="originalName" column="original_name" />
<result property="fileSize" column="file_size" />
<result property="fileType" column="file_type" />
<result property="uploadStatus" column="upload_status" />
<result property="submitBatch" column="submit_batch" />
<result property="isCurrent" column="is_current" />
<result property="createTime" column="create_time" />
</resultMap>
<sql id="selectSubmissionVo">
select s.id, s.submission_no, s.task_id, s.user_id, s.cycle_key, s.reward_points,
s.remark, s.status, s.attempt_count, s.reject_reason, s.review_by, s.review_time,
s.reward_record_id, s.first_submit_time, s.last_submit_time,
s.create_by, s.create_time, s.update_by, s.update_time,
t.task_name, t.task_type, t.cycle_type, u.user_name, u.nick_name
from app_point_task_submission s
left join app_point_task t on t.id = s.task_id
left join sys_user u on u.user_id = s.user_id
</sql>
<select id="selectSubmissionById" resultMap="SubmissionResult">
<include refid="selectSubmissionVo" /> where s.id = #{id}
</select>
<select id="selectSubmissionByIdForUpdate" resultMap="SubmissionResult">
select id, submission_no, task_id, user_id, cycle_key, reward_points, remark, status,
attempt_count, reject_reason, review_by, review_time, reward_record_id,
first_submit_time, last_submit_time, create_by, create_time, update_by, update_time
from app_point_task_submission where id = #{id} for update
</select>
<select id="selectByTaskUserCycle" resultMap="SubmissionResult">
select id, submission_no, task_id, user_id, cycle_key, reward_points, remark, status,
attempt_count, reject_reason, review_by, review_time, reward_record_id,
first_submit_time, last_submit_time, create_by, create_time, update_by, update_time
from app_point_task_submission
where task_id = #{taskId} and user_id = #{userId} and cycle_key = #{cycleKey}
</select>
<select id="selectSubmissionList" parameterType="AppPointTaskSubmission" resultMap="SubmissionResult">
<include refid="selectSubmissionVo" />
<where>
<if test="submissionNo != null and submissionNo != ''">and s.submission_no like concat('%', #{submissionNo}, '%')</if>
<if test="taskId != null">and s.task_id = #{taskId}</if>
<if test="taskName != null and taskName != ''">and t.task_name like concat('%', #{taskName}, '%')</if>
<if test="status != null">and s.status = #{status}</if>
<if test="cycleKey != null and cycleKey != ''">and s.cycle_key = #{cycleKey}</if>
<if test="userName != null and userName != ''">
and (u.user_name like concat('%', #{userName}, '%') or u.nick_name like concat('%', #{userName}, '%'))
</if>
<if test="params.beginTime != null and params.beginTime != ''">
and date_format(s.last_submit_time, '%Y%m%d') &gt;= date_format(#{params.beginTime}, '%Y%m%d')
</if>
<if test="params.endTime != null and params.endTime != ''">
and date_format(s.last_submit_time, '%Y%m%d') &lt;= date_format(#{params.endTime}, '%Y%m%d')
</if>
</where>
order by s.last_submit_time desc, s.id desc
</select>
<select id="selectMySubmissionList" resultMap="SubmissionResult">
<include refid="selectSubmissionVo" />
where s.user_id = #{userId}
order by s.last_submit_time desc, s.id desc
</select>
<insert id="insertSubmission" parameterType="AppPointTaskSubmission" useGeneratedKeys="true" keyProperty="id">
insert into app_point_task_submission
(submission_no, task_id, user_id, cycle_key, reward_points, remark, status, attempt_count,
reject_reason, review_by, review_time, reward_record_id, first_submit_time, last_submit_time,
create_by, create_time, update_by, update_time)
values
(#{submissionNo}, #{taskId}, #{userId}, #{cycleKey}, #{rewardPoints}, #{remark}, #{status}, #{attemptCount},
#{rejectReason}, #{reviewBy}, #{reviewTime}, #{rewardRecordId}, #{firstSubmitTime}, #{lastSubmitTime},
#{createBy}, #{createTime}, #{updateBy}, #{updateTime})
</insert>
<update id="updateSubmission" parameterType="AppPointTaskSubmission">
update app_point_task_submission
<set>
<if test="remark != null">remark = #{remark},</if>
<if test="status != null">status = #{status},</if>
<if test="attemptCount != null">attempt_count = #{attemptCount},</if>
reject_reason = #{rejectReason},
review_by = #{reviewBy},
review_time = #{reviewTime},
<if test="rewardRecordId != null">reward_record_id = #{rewardRecordId},</if>
<if test="lastSubmitTime != null">last_submit_time = #{lastSubmitTime},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
update_time = #{updateTime}
</set>
where id = #{id}
</update>
<insert id="insertTempAttachment" parameterType="AppPointTaskAttachment" useGeneratedKeys="true" keyProperty="id">
insert into app_point_task_attachment
(submission_id, user_id, object_key, original_name, file_size, file_type,
upload_status, submit_batch, is_current, create_time)
values
(null, #{userId}, #{objectKey}, #{originalName}, #{fileSize}, #{fileType},
'TEMP', 0, 0, #{createTime})
</insert>
<select id="lockUserForUpload" resultType="long">
select user_id from sys_user where user_id = #{userId} for update
</select>
<select id="countTempAttachmentsByUser" resultType="int">
select count(1)
from app_point_task_attachment
where user_id = #{userId} and upload_status = 'TEMP'
</select>
<select id="countAttachmentsByUserSince" resultType="int">
select count(1)
from app_point_task_attachment
where user_id = #{userId} and create_time &gt;= #{since}
</select>
<select id="selectTempAttachments" resultMap="AttachmentResult">
select id, submission_id, user_id, object_key, original_name, file_size, file_type,
upload_status, submit_batch, is_current, create_time
from app_point_task_attachment
where user_id = #{userId} and upload_status = 'TEMP' and object_key in
<foreach item="key" collection="keys" open="(" separator="," close=")">#{key}</foreach>
</select>
<update id="bindAttachments">
update app_point_task_attachment
set submission_id = #{submissionId}, upload_status = 'BOUND', submit_batch = #{submitBatch}, is_current = 1
where user_id = #{userId} and upload_status = 'TEMP' and object_key in
<foreach item="key" collection="keys" open="(" separator="," close=")">#{key}</foreach>
</update>
<update id="markAttachmentsNotCurrent">
update app_point_task_attachment set is_current = 0 where submission_id = #{submissionId}
</update>
<select id="selectAttachmentsBySubmissionId" resultMap="AttachmentResult">
select id, submission_id, user_id, object_key, original_name, file_size, file_type,
upload_status, submit_batch, is_current, create_time
from app_point_task_attachment
where submission_id = #{submissionId}
order by submit_batch desc, id asc
</select>
<select id="selectExpiredTempAttachments" resultMap="AttachmentResult">
select id, submission_id, user_id, object_key, original_name, file_size, file_type,
upload_status, submit_batch, is_current, create_time
from app_point_task_attachment
where upload_status = 'TEMP' and create_time &lt; #{beforeTime}
order by id asc
limit 500
</select>
<delete id="deleteTempAttachmentById">
delete from app_point_task_attachment where id = #{id} and upload_status = 'TEMP'
</delete>
<insert id="insertAuditLog" parameterType="AppPointTaskAuditLog">
insert into app_point_task_audit_log
(submission_id, before_status, after_status, operator_name, remark, create_time)
values
(#{submissionId}, #{beforeStatus}, #{afterStatus}, #{operatorName}, #{remark}, #{createTime})
</insert>
</mapper>

View File

@@ -0,0 +1,41 @@
import request from '@/utils/request'
export function listPointTask(query) {
return request({ url: '/app/pointTask/list', method: 'get', params: query })
}
export function getPointTask(id) {
return request({ url: '/app/pointTask/' + id, method: 'get' })
}
export function addPointTask(data) {
return request({ url: '/app/pointTask', method: 'post', data })
}
export function updatePointTask(data) {
return request({ url: '/app/pointTask', method: 'put', data })
}
export function delPointTask(ids) {
return request({ url: '/app/pointTask/' + ids, method: 'delete' })
}
export function listPointTaskSubmission(query) {
return request({ url: '/app/pointTask/submission/list', method: 'get', params: query })
}
export function getPointTaskSubmission(id) {
return request({ url: '/app/pointTask/submission/' + id, method: 'get' })
}
export function approvePointTaskSubmission(id) {
return request({ url: '/app/pointTask/submission/' + id + '/approve', method: 'post' })
}
export function rejectPointTaskSubmission(id, rejectReason) {
return request({ url: '/app/pointTask/submission/' + id + '/reject', method: 'post', data: { rejectReason } })
}
export function revokePointTaskSubmission(id, rejectReason) {
return request({ url: '/app/pointTask/submission/' + id + '/revoke', method: 'post', data: { rejectReason } })
}

View File

@@ -1,7 +1,7 @@
<template>
<div class="component-upload-image">
<el-upload
drag
:drag="!compact"
multiple
:action="uploadImgUrl"
:on-success="handleUploadSuccess"
@@ -14,13 +14,18 @@
:show-file-list="true"
:headers="headers"
:file-list="fileList"
list-type="picture"
:list-type="compact ? 'picture-card' : 'picture'"
:on-preview="handlePictureCardPreview"
:class="{hide: this.fileList.length >= this.limit}"
:class="{ hide: this.fileList.length >= this.limit, 'is-compact': compact }"
:disabled="disabled"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
<template v-if="compact">
<i class="el-icon-plus"></i>
</template>
<template v-else>
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
</template>
</el-upload>
<!-- 上传提示 -->
@@ -74,6 +79,11 @@ export default {
disabled: {
type: Boolean,
default: false
},
// 紧凑图片卡片模式,适合弹窗或栅格表单
compact: {
type: Boolean,
default: false
}
},
data() {
@@ -99,7 +109,6 @@ export default {
const list = Array.isArray(val) ? val : this.value.split(',');
// 然后将数组转为对象数组
this.fileList = list.map(item => {
console.log(item)
if (typeof item === "string") {
item = { name: item, url: item };
}
@@ -179,6 +188,7 @@ export default {
},
// 上传失败
handleUploadError() {
if (this.number > 0) this.number--;
this.$modal.msgError("上传图片失败,请重试");
this.$modal.closeLoading();
},
@@ -216,6 +226,9 @@ export default {
::v-deep.hide .el-upload--picture-card {
display: none;
}
::v-deep.hide .el-upload-dragger {
display: none;
}
// 去掉动画效果
::v-deep .el-list-enter-active,
::v-deep .el-list-leave-active {

View File

@@ -0,0 +1,93 @@
<template>
<div class="app-container">
<el-form v-show="showSearch" ref="queryForm" :model="queryParams" size="small" :inline="true">
<el-form-item label="任务名称" prop="taskName">
<el-input v-model="queryParams.taskName" placeholder="请输入任务名称" clearable @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="任务周期" prop="cycleType">
<el-select v-model="queryParams.cycleType" placeholder="全部" clearable>
<el-option label="单次任务" value="ONCE" />
<el-option label="每日任务" value="DAILY" />
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="全部" clearable>
<el-option label="启用" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5"><el-button v-hasPermi="['app:pointTask:add']" type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd">新增任务</el-button></el-col>
<right-toolbar :show-search.sync="showSearch" @queryTable="getList" />
</el-row>
<el-table v-loading="loading" :data="taskList">
<el-table-column label="任务名称" prop="taskName" min-width="180" show-overflow-tooltip />
<el-table-column label="周期" align="center" width="100">
<template slot-scope="scope"><el-tag size="small" :type="scope.row.cycleType === 'DAILY' ? 'warning' : 'success'">{{ scope.row.cycleType === 'DAILY' ? '每日任务' : '单次任务' }}</el-tag></template>
</el-table-column>
<el-table-column label="奖励积分" prop="rewardPoints" align="center" width="100" />
<el-table-column label="活动时间" min-width="285"><template slot-scope="scope">{{ scope.row.startTime || '立即开始' }} 至 {{ scope.row.endTime || '长期有效' }}</template></el-table-column>
<el-table-column label="状态" align="center" width="90"><template slot-scope="scope"><el-tag size="small" :type="scope.row.status === '0' ? 'success' : 'info'">{{ scope.row.status === '0' ? '启用' : '停用' }}</el-tag></template></el-table-column>
<el-table-column label="排序" prop="sortOrder" align="center" width="75" />
<el-table-column label="操作" align="center" width="150" fixed="right">
<template slot-scope="scope">
<el-button v-hasPermi="['app:pointTask:edit']" type="text" icon="el-icon-edit" size="mini" @click="handleUpdate(scope.row)">修改</el-button>
<el-button v-hasPermi="['app:pointTask:remove']" type="text" icon="el-icon-delete" size="mini" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
<el-dialog :title="title" :visible.sync="open" width="760px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="110px">
<el-row :gutter="20">
<el-col :span="16"><el-form-item label="任务名称" prop="taskName"><el-input v-model="form.taskName" maxlength="80" show-word-limit /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="奖励积分" prop="rewardPoints"><el-input-number v-model="form.rewardPoints" :min="1" :max="100000" style="width:100%" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="任务周期" prop="cycleType"><el-select v-model="form.cycleType" style="width:100%"><el-option label="单次任务" value="ONCE" /><el-option label="每日任务" value="DAILY" /></el-select></el-form-item></el-col>
<el-col :span="12"><el-form-item label="状态"><el-radio-group v-model="form.status"><el-radio label="0">启用</el-radio><el-radio label="1">停用</el-radio></el-radio-group></el-form-item></el-col>
<el-col :span="12"><el-form-item label="开始时间"><el-date-picker v-model="form.startTime" value-format="yyyy-MM-dd HH:mm:ss" type="datetime" placeholder="留空立即开始" style="width:100%" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="结束时间"><el-date-picker v-model="form.endTime" value-format="yyyy-MM-dd HH:mm:ss" type="datetime" placeholder="留空长期有效" style="width:100%" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="任务封面"><qiniu-image-upload v-model="form.coverUrl" compact :limit="1" :file-size="5" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="示例截图"><qiniu-image-upload v-model="form.exampleImages" compact :limit="3" :file-size="5" /></el-form-item></el-col>
<el-col :span="24"><el-form-item label="任务说明"><el-input v-model="form.description" type="textarea" :rows="3" maxlength="1000" show-word-limit /></el-form-item></el-col>
<el-col :span="24"><el-form-item label="操作步骤"><el-input v-model="form.operationGuide" type="textarea" :rows="5" :placeholder="operationGuidePlaceholder" /></el-form-item></el-col>
<el-col :span="8"><el-form-item label="排序"><el-input-number v-model="form.sortOrder" :min="0" style="width:100%" /></el-form-item></el-col>
</el-row>
</el-form>
<div slot="footer"><el-button @click="open=false">取消</el-button><el-button type="primary" :loading="submitting" @click="submitForm">确定</el-button></div>
</el-dialog>
</div>
</template>
<script>
import { listPointTask, getPointTask, addPointTask, updatePointTask, delPointTask } from '@/api/app/pointTask'
export default {
name: 'PointTask',
data() {
return {
loading: false, submitting: false, showSearch: true, total: 0, taskList: [], open: false, title: '', operationGuidePlaceholder: '例如:\n1. 扫描二维码进群\n2. 截图群聊页面\n3. 上传截图并等待审核', queryParams: { pageNum: 1, pageSize: 10, taskName: undefined, cycleType: undefined, status: undefined }, form: {},
rules: { taskName: [{ required: true, message: '请输入任务名称', trigger: 'blur' }], cycleType: [{ required: true, message: '请选择任务周期', trigger: 'change' }], rewardPoints: [{ required: true, message: '请输入奖励积分', trigger: 'blur' }] }
}
},
created() { this.getList() },
methods: {
getList() { this.loading = true; listPointTask(this.queryParams).then(res => { this.taskList = res.rows || []; this.total = res.total || 0 }).finally(() => { this.loading = false }) },
reset() { this.form = { id: undefined, taskName: '', cycleType: 'ONCE', coverUrl: '', description: '', operationGuide: '', exampleImages: '', rewardPoints: 10, startTime: null, endTime: null, status: '0', sortOrder: 0 } },
handleQuery() { this.queryParams.pageNum = 1; this.getList() },
resetQuery() { this.resetForm('queryForm'); this.handleQuery() },
handleAdd() { this.reset(); this.title = '新增积分任务'; this.open = true },
handleUpdate(row) { this.reset(); getPointTask(row.id).then(res => { this.form = res.data; this.title = '修改积分任务'; this.open = true }) },
handleDelete(row) { this.$modal.confirm('确认删除任务“' + row.taskName + '”吗?').then(() => delPointTask(row.id)).then(() => { this.$modal.msgSuccess('删除成功'); this.getList() }).catch(() => {}) },
submitForm() { this.$refs.form.validate(valid => { if (!valid) return; this.submitting = true; const action = this.form.id ? updatePointTask : addPointTask; action(this.form).then(() => { this.$modal.msgSuccess(this.form.id ? '修改成功' : '新增成功'); this.open = false; this.getList() }).finally(() => { this.submitting = false }) }) }
}
}
</script>

View File

@@ -0,0 +1,97 @@
<template>
<div class="app-container">
<el-form v-show="showSearch" ref="queryForm" :model="queryParams" size="small" :inline="true">
<el-form-item label="任务名称" prop="taskName"><el-input v-model="queryParams.taskName" clearable placeholder="任务名称" @keyup.enter.native="handleQuery" /></el-form-item>
<el-form-item label="用户" prop="userName"><el-input v-model="queryParams.userName" clearable placeholder="账号或昵称" @keyup.enter.native="handleQuery" /></el-form-item>
<el-form-item label="状态" prop="status"><el-select v-model="queryParams.status" clearable placeholder="全部"><el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" /></el-select></el-form-item>
<el-form-item label="周期" prop="cycleKey"><el-input v-model="queryParams.cycleKey" clearable placeholder="ONCE或2026-09-16" /></el-form-item>
<el-form-item><el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button><el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button></el-form-item>
</el-form>
<right-toolbar :show-search.sync="showSearch" @queryTable="getList" />
<el-table v-loading="loading" :data="submissionList">
<el-table-column label="提交单号" prop="submissionNo" min-width="185" />
<el-table-column label="任务" prop="taskName" min-width="160" show-overflow-tooltip />
<el-table-column label="用户" min-width="130"><template slot-scope="scope"><div>{{ scope.row.nickName || scope.row.userName }}</div><small class="muted">ID: {{ scope.row.userId }}</small></template></el-table-column>
<el-table-column label="周期" prop="cycleKey" align="center" width="110" />
<el-table-column label="积分" prop="rewardPoints" align="center" width="75" />
<el-table-column label="提交次数" prop="attemptCount" align="center" width="85" />
<el-table-column label="状态" align="center" width="100"><template slot-scope="scope"><el-tag :type="statusType(scope.row.status)" size="small">{{ statusLabel(scope.row.status) }}</el-tag></template></el-table-column>
<el-table-column label="提交时间" prop="lastSubmitTime" width="165" />
<el-table-column label="操作" align="center" width="225" fixed="right">
<template slot-scope="scope">
<el-button v-hasPermi="['app:pointTaskReview:query']" type="text" icon="el-icon-view" size="mini" @click="handleView(scope.row)">详情</el-button>
<el-button v-if="scope.row.status === 0" v-hasPermi="['app:pointTaskReview:edit']" type="text" icon="el-icon-check" size="mini" @click="handleApprove(scope.row)">通过</el-button>
<el-button v-if="scope.row.status === 0" v-hasPermi="['app:pointTaskReview:edit']" type="text" icon="el-icon-close" size="mini" @click="openReason(scope.row, 'reject')">驳回</el-button>
<el-button v-if="scope.row.status === 1" v-hasPermi="['app:pointTaskReview:revoke']" type="text" icon="el-icon-refresh-left" size="mini" @click="openReason(scope.row, 'revoke')">撤销</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
<el-dialog title="任务提交详情" :visible.sync="detailOpen" width="820px" append-to-body>
<el-descriptions v-if="detail.id" :column="2" border>
<el-descriptions-item label="提交单号" :span="2">{{ detail.submissionNo }}</el-descriptions-item>
<el-descriptions-item label="任务名称">{{ detail.taskName }}</el-descriptions-item>
<el-descriptions-item label="奖励积分">{{ detail.rewardPoints }}</el-descriptions-item>
<el-descriptions-item label="用户">{{ detail.nickName || detail.userName }}({{ detail.userId }})</el-descriptions-item>
<el-descriptions-item label="任务周期">{{ detail.cycleKey }}</el-descriptions-item>
<el-descriptions-item label="状态"><el-tag :type="statusType(detail.status)" size="small">{{ statusLabel(detail.status) }}</el-tag></el-descriptions-item>
<el-descriptions-item label="提交次数">{{ detail.attemptCount }}</el-descriptions-item>
<el-descriptions-item label="用户说明" :span="2"><div class="pre-wrap">{{ detail.remark || '-' }}</div></el-descriptions-item>
<el-descriptions-item v-if="detail.rejectReason" label="驳回/撤销原因" :span="2">{{ detail.rejectReason }}</el-descriptions-item>
</el-descriptions>
<h4>任务截图</h4>
<div class="image-list">
<div v-for="item in detail.attachments || []" :key="item.id" class="image-card" :class="{ history: item.isCurrent !== 1 }">
<el-image :src="item.previewUrl" :preview-src-list="previewUrls" fit="cover" />
<div>{{ item.isCurrent === 1 ? '当前截图' : '历史第' + item.submitBatch + '次' }}</div>
</div>
</div>
<div slot="footer"><el-button @click="detailOpen=false">关闭</el-button></div>
</el-dialog>
<el-dialog :title="reasonMode === 'reject' ? '驳回任务' : '撤销积分奖励'" :visible.sync="reasonOpen" width="520px" append-to-body>
<el-input v-model="reason" type="textarea" :rows="4" maxlength="300" show-word-limit :placeholder="reasonMode === 'reject' ? '请填写审核未通过原因' : '请填写撤销原因'" />
<el-alert v-if="reasonMode === 'revoke'" title="撤销将扣回已发放积分;用户积分不足时操作会失败。" type="warning" :closable="false" show-icon style="margin-top:15px" />
<div slot="footer"><el-button @click="reasonOpen=false">取消</el-button><el-button type="primary" :loading="submitting" @click="submitReason">确定</el-button></div>
</el-dialog>
</div>
</template>
<script>
import { listPointTaskSubmission, getPointTaskSubmission, approvePointTaskSubmission, rejectPointTaskSubmission, revokePointTaskSubmission } from '@/api/app/pointTask'
export default {
name: 'PointTaskReview',
data() {
return {
loading: false, submitting: false, showSearch: true, total: 0, submissionList: [], detailOpen: false, detail: {}, reasonOpen: false, reason: '', reasonMode: 'reject', currentRow: {},
statusOptions: [{ value: 0, label: '待审核' }, { value: 1, label: '已通过' }, { value: 2, label: '已驳回' }, { value: 3, label: '已撤销' }],
queryParams: { pageNum: 1, pageSize: 10, taskName: undefined, userName: undefined, status: undefined, cycleKey: undefined }
}
},
computed: { previewUrls() { return (this.detail.attachments || []).map(item => item.previewUrl).filter(Boolean) } },
created() { this.getList() },
methods: {
getList() { this.loading = true; listPointTaskSubmission(this.queryParams).then(res => { this.submissionList = res.rows || []; this.total = res.total || 0 }).finally(() => { this.loading = false }) },
handleQuery() { this.queryParams.pageNum = 1; this.getList() },
resetQuery() { this.resetForm('queryForm'); this.handleQuery() },
handleView(row) { getPointTaskSubmission(row.id).then(res => { this.detail = res.data; this.detailOpen = true }) },
handleApprove(row) { this.$modal.confirm('确认通过该任务并发放 ' + row.rewardPoints + ' 积分吗?').then(() => approvePointTaskSubmission(row.id)).then(() => { this.$modal.msgSuccess('审核通过,积分已发放'); this.getList() }).catch(() => {}) },
openReason(row, mode) { this.currentRow = row; this.reasonMode = mode; this.reason = ''; this.reasonOpen = true },
submitReason() { const reason = String(this.reason || '').trim(); if (!reason) { this.$modal.msgError('请填写原因'); return } this.submitting = true; const action = this.reasonMode === 'reject' ? rejectPointTaskSubmission : revokePointTaskSubmission; action(this.currentRow.id, reason).then(() => { this.$modal.msgSuccess(this.reasonMode === 'reject' ? '已驳回' : '奖励已撤销'); this.reasonOpen = false; this.getList() }).finally(() => { this.submitting = false }) },
statusLabel(status) { const item = this.statusOptions.find(v => v.value === status); return item ? item.label : '未知' },
statusType(status) { return { 0: 'warning', 1: 'success', 2: 'danger', 3: 'info' }[status] || 'info' }
}
}
</script>
<style scoped>
.muted { color: #909399; }
.pre-wrap { white-space: pre-wrap; }
.image-list { display: flex; flex-wrap: wrap; gap: 14px; }
.image-card { width: 150px; text-align: center; color: #606266; }
.image-card .el-image { width: 150px; height: 150px; border-radius: 6px; }
.image-card.history { opacity: .58; }
</style>

216
sql/point_task.sql Normal file
View File

@@ -0,0 +1,216 @@
-- 积分任务中心:一次性/每日任务、七牛截图、人工审核和积分发放。
CREATE TABLE IF NOT EXISTS `app_point_task` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '任务ID',
`task_name` varchar(80) NOT NULL COMMENT '任务名称',
`task_type` varchar(32) NOT NULL DEFAULT 'OTHER' COMMENT 'GROUP_JOIN/ARTICLE_SHARE/OTHER',
`cycle_type` varchar(16) NOT NULL COMMENT 'ONCE一次性 DAILY每日一次',
`cover_url` varchar(500) DEFAULT NULL COMMENT '任务封面',
`description` varchar(1000) DEFAULT NULL COMMENT '任务说明',
`operation_guide` text COMMENT '操作步骤',
`example_images` varchar(2000) DEFAULT NULL COMMENT '示例截图,逗号分隔',
`reward_points` int NOT NULL COMMENT '奖励积分',
`start_time` datetime DEFAULT NULL COMMENT '开始时间',
`end_time` datetime DEFAULT NULL COMMENT '结束时间',
`allow_resubmit` tinyint NOT NULL DEFAULT 1 COMMENT '驳回后允许重提 0否 1是',
`status` char(1) NOT NULL DEFAULT '0' COMMENT '0启用 1停用',
`sort_order` int NOT NULL DEFAULT 0 COMMENT '排序',
`create_by` varchar(64) DEFAULT '',
`create_time` datetime NOT NULL,
`update_by` varchar(64) DEFAULT '',
`update_time` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_point_task_status_time` (`status`, `start_time`, `end_time`, `sort_order`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分任务表';
CREATE TABLE IF NOT EXISTS `app_point_task_submission` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '提交ID',
`submission_no` varchar(40) NOT NULL COMMENT '提交单号',
`task_id` bigint NOT NULL COMMENT '任务ID',
`user_id` bigint NOT NULL COMMENT '用户ID',
`cycle_key` varchar(16) NOT NULL COMMENT 'ONCE或yyyy-MM-dd',
`reward_points` int NOT NULL COMMENT '提交时奖励积分快照',
`remark` varchar(300) DEFAULT NULL COMMENT '用户说明',
`status` tinyint NOT NULL DEFAULT 0 COMMENT '0待审核 1已通过 2已驳回 3已撤销',
`attempt_count` int NOT NULL DEFAULT 1 COMMENT '提交次数',
`reject_reason` varchar(300) DEFAULT NULL COMMENT '驳回或撤销原因',
`review_by` varchar(64) DEFAULT NULL COMMENT '审核人',
`review_time` datetime DEFAULT NULL COMMENT '审核时间',
`reward_record_id` bigint DEFAULT NULL COMMENT '奖励积分流水ID',
`first_submit_time` datetime NOT NULL COMMENT '首次提交时间',
`last_submit_time` datetime NOT NULL COMMENT '最近提交时间',
`create_by` varchar(64) DEFAULT '',
`create_time` datetime NOT NULL,
`update_by` varchar(64) DEFAULT '',
`update_time` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_point_task_submission_no` (`submission_no`),
UNIQUE KEY `uk_point_task_user_cycle` (`task_id`, `user_id`, `cycle_key`),
KEY `idx_point_task_submission_status` (`status`, `last_submit_time`),
KEY `idx_point_task_submission_user` (`user_id`, `last_submit_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分任务提交表';
CREATE TABLE IF NOT EXISTS `app_point_task_attachment` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '附件ID',
`submission_id` bigint DEFAULT NULL COMMENT '提交ID,上传阶段为空',
`user_id` bigint NOT NULL COMMENT '用户ID',
`object_key` varchar(500) NOT NULL COMMENT '七牛对象Key',
`original_name` varchar(255) DEFAULT NULL COMMENT '原文件名',
`file_size` bigint DEFAULT NULL COMMENT '文件大小',
`file_type` varchar(64) DEFAULT NULL COMMENT 'MIME类型',
`upload_status` varchar(16) NOT NULL DEFAULT 'TEMP' COMMENT 'TEMP/BOUND',
`submit_batch` int NOT NULL DEFAULT 0 COMMENT '第几次提交',
`is_current` tinyint NOT NULL DEFAULT 0 COMMENT '是否当前审核附件',
`create_time` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_point_task_object_key` (`object_key`),
KEY `idx_point_task_attachment_submission` (`submission_id`, `submit_batch`, `is_current`),
KEY `idx_point_task_attachment_temp` (`upload_status`, `create_time`),
KEY `idx_point_task_attachment_user_quota` (`user_id`, `upload_status`, `create_time`),
KEY `idx_point_task_attachment_user_daily` (`user_id`, `create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分任务截图附件表';
-- 已有积分任务表时补充用户上传配额查询索引。
SET @attachment_quota_index_exists := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'app_point_task_attachment'
AND INDEX_NAME = 'idx_point_task_attachment_user_quota'
);
SET @attachment_quota_index_sql := IF(@attachment_quota_index_exists = 0,
'ALTER TABLE `app_point_task_attachment` ADD KEY `idx_point_task_attachment_user_quota` (`user_id`, `upload_status`, `create_time`)',
'SELECT 1');
PREPARE attachment_quota_index_stmt FROM @attachment_quota_index_sql;
EXECUTE attachment_quota_index_stmt;
DEALLOCATE PREPARE attachment_quota_index_stmt;
SET @attachment_daily_index_exists := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'app_point_task_attachment'
AND INDEX_NAME = 'idx_point_task_attachment_user_daily'
);
SET @attachment_daily_index_sql := IF(@attachment_daily_index_exists = 0,
'ALTER TABLE `app_point_task_attachment` ADD KEY `idx_point_task_attachment_user_daily` (`user_id`, `create_time`)',
'SELECT 1');
PREPARE attachment_daily_index_stmt FROM @attachment_daily_index_sql;
EXECUTE attachment_daily_index_stmt;
DEALLOCATE PREPARE attachment_daily_index_stmt;
CREATE TABLE IF NOT EXISTS `app_point_task_audit_log` (
`id` bigint NOT NULL AUTO_INCREMENT,
`submission_id` bigint NOT NULL,
`before_status` tinyint DEFAULT NULL,
`after_status` tinyint NOT NULL,
`operator_name` varchar(64) NOT NULL,
`remark` varchar(500) DEFAULT NULL,
`create_time` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_point_task_audit_submission` (`submission_id`, `create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分任务审核日志表';
-- 积分流水业务幂等字段。
SET @biz_type_exists := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'app_integral_record' AND COLUMN_NAME = 'biz_type'
);
SET @biz_type_sql := IF(@biz_type_exists = 0,
'ALTER TABLE `app_integral_record` ADD COLUMN `biz_type` varchar(32) DEFAULT NULL COMMENT ''业务类型'' AFTER `resource_id`',
'SELECT 1');
PREPARE biz_type_stmt FROM @biz_type_sql;
EXECUTE biz_type_stmt;
DEALLOCATE PREPARE biz_type_stmt;
SET @biz_id_exists := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'app_integral_record' AND COLUMN_NAME = 'biz_id'
);
SET @biz_id_sql := IF(@biz_id_exists = 0,
'ALTER TABLE `app_integral_record` ADD COLUMN `biz_id` bigint DEFAULT NULL COMMENT ''业务记录ID'' AFTER `biz_type`',
'SELECT 1');
PREPARE biz_id_stmt FROM @biz_id_sql;
EXECUTE biz_id_stmt;
DEALLOCATE PREPARE biz_id_stmt;
SET @biz_index_exists := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'app_integral_record' AND INDEX_NAME = 'uk_integral_biz'
);
SET @biz_index_sql := IF(@biz_index_exists = 0,
'ALTER TABLE `app_integral_record` ADD UNIQUE KEY `uk_integral_biz` (`biz_type`, `biz_id`)',
'SELECT 1');
PREPARE biz_index_stmt FROM @biz_index_sql;
EXECUTE biz_index_stmt;
DEALLOCATE PREPARE biz_index_stmt;
-- 后台菜单。
SET @point_task_parent_id := COALESCE(
(SELECT parent_id FROM sys_menu WHERE perms = 'app:resource:list' LIMIT 1),
(SELECT parent_id FROM sys_menu WHERE perms = 'app:article:list' LIMIT 1),
0
);
INSERT INTO sys_menu
(menu_name, parent_id, order_num, path, component, query, is_frame, is_cache,
menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
SELECT '积分任务', @point_task_parent_id, 30, 'pointTask', 'app/pointTask/index', '', 1, 0,
'C', '0', '0', 'app:pointTask:list', 'guide', 'admin', NOW(), '', NULL, '积分任务配置'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'app:pointTask:list');
SET @point_task_menu_id := (SELECT menu_id FROM sys_menu WHERE perms = 'app:pointTask:list' LIMIT 1);
INSERT INTO sys_menu
(menu_name, parent_id, order_num, path, component, query, is_frame, is_cache,
menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
SELECT '任务查询', @point_task_menu_id, 1, '', '', '', 1, 0, 'F', '0', '0',
'app:pointTask:query', '#', 'admin', NOW(), '', NULL, ''
WHERE @point_task_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'app:pointTask:query');
INSERT INTO sys_menu
(menu_name, parent_id, order_num, path, component, query, is_frame, is_cache,
menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
SELECT '任务新增', @point_task_menu_id, 2, '', '', '', 1, 0, 'F', '0', '0',
'app:pointTask:add', '#', 'admin', NOW(), '', NULL, ''
WHERE @point_task_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'app:pointTask:add');
INSERT INTO sys_menu
(menu_name, parent_id, order_num, path, component, query, is_frame, is_cache,
menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
SELECT '任务修改', @point_task_menu_id, 3, '', '', '', 1, 0, 'F', '0', '0',
'app:pointTask:edit', '#', 'admin', NOW(), '', NULL, ''
WHERE @point_task_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'app:pointTask:edit');
INSERT INTO sys_menu
(menu_name, parent_id, order_num, path, component, query, is_frame, is_cache,
menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
SELECT '任务删除', @point_task_menu_id, 4, '', '', '', 1, 0, 'F', '0', '0',
'app:pointTask:remove', '#', 'admin', NOW(), '', NULL, ''
WHERE @point_task_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'app:pointTask:remove');
INSERT INTO sys_menu
(menu_name, parent_id, order_num, path, component, query, is_frame, is_cache,
menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
SELECT '任务审核', @point_task_parent_id, 31, 'pointTaskReview', 'app/pointTaskReview/index', '', 1, 0,
'C', '0', '0', 'app:pointTaskReview:list', 'audit', 'admin', NOW(), '', NULL, '积分任务审核'
WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'app:pointTaskReview:list');
SET @point_review_menu_id := (SELECT menu_id FROM sys_menu WHERE perms = 'app:pointTaskReview:list' LIMIT 1);
INSERT INTO sys_menu
(menu_name, parent_id, order_num, path, component, query, is_frame, is_cache,
menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
SELECT '审核查询', @point_review_menu_id, 1, '', '', '', 1, 0, 'F', '0', '0',
'app:pointTaskReview:query', '#', 'admin', NOW(), '', NULL, ''
WHERE @point_review_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'app:pointTaskReview:query');
INSERT INTO sys_menu
(menu_name, parent_id, order_num, path, component, query, is_frame, is_cache,
menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
SELECT '任务审核操作', @point_review_menu_id, 2, '', '', '', 1, 0, 'F', '0', '0',
'app:pointTaskReview:edit', '#', 'admin', NOW(), '', NULL, ''
WHERE @point_review_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'app:pointTaskReview:edit');
INSERT INTO sys_menu
(menu_name, parent_id, order_num, path, component, query, is_frame, is_cache,
menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark)
SELECT '撤销任务奖励', @point_review_menu_id, 3, '', '', '', 1, 0, 'F', '0', '0',
'app:pointTaskReview:revoke', '#', 'admin', NOW(), '', NULL, ''
WHERE @point_review_menu_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms = 'app:pointTaskReview:revoke');