feat: 积分发放模块

This commit is contained in:
2026-07-27 11:03:19 +08:00
parent dd7c35eb50
commit 784350bb42
6 changed files with 504 additions and 0 deletions
@@ -0,0 +1,68 @@
package com.budwk.app.zhgh.pointsmall.pointsgrant.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.budwk.app.base.annotation.SLog;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.pointsmall.pointsgrant.models.PointsMallUserPointsGrantPlan;
import com.budwk.app.zhgh.pointsmall.pointsgrant.param.PointsMallUserPointsGrantPlanPageParam;
import com.budwk.app.zhgh.pointsmall.pointsgrant.service.PointsMallUserPointsGrantPlanService;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param;
@IocBean
@Ok("json:full")
@At("/platform/zhgh/points-mall/pointsgrant")
public class PointsMallUserPointsGrantPlanController {
@Inject
private PointsMallUserPointsGrantPlanService grantPlanService;
@At("")
@Ok("beetl:/platform/zhgh/points-mall/pointsgrant/index.html")
@SaCheckPermission("points.mall.pointsgrant")
public void index() {
}
@At
@SaCheckPermission("points.mall.pointsgrant")
public Result pageData(PointsMallUserPointsGrantPlanPageParam param) {
return Result.success(grantPlanService.page(param));
}
@At
@SLog(tag = "积分发放", msg = "创建发放计划")
@SaCheckPermission("points.mall.pointsgrant")
public Result doAdd(PointsMallUserPointsGrantPlan plan) {
try {
grantPlanService.createPlan(plan);
return Result.success();
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
@At
@SLog(tag = "积分发放", msg = "执行发放计划")
@SaCheckPermission("points.mall.pointsgrant")
public Result startPlan(@Param("id") String id) {
try {
return Result.success(grantPlanService.startPlan(id));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
@At
@SLog(tag = "积分发放", msg = "取消发放计划")
@SaCheckPermission("points.mall.pointsgrant")
public Result cancelPlan(@Param("id") String id) {
try {
return Result.success(grantPlanService.cancelPlan(id));
} catch (IllegalArgumentException e) {
return Result.error(e.getMessage());
}
}
}
@@ -0,0 +1,71 @@
package com.budwk.app.zhgh.pointsmall.pointsgrant.models;
import com.budwk.app.base.model.BaseModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.ColDefine;
import org.nutz.dao.entity.annotation.ColType;
import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Default;
import org.nutz.dao.entity.annotation.Name;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.interceptor.annotation.PrevInsert;
import java.io.Serializable;
import java.math.BigDecimal;
@Data
@EqualsAndHashCode(callSuper = true)
@Table("points_mall_user_points_grant_plan")
@Comment("积分商城积分发放计划")
public class PointsMallUserPointsGrantPlan extends BaseModel implements Serializable {
@Name
@Column
@Comment("ID")
@ColDefine(type = ColType.VARCHAR, width = 32)
@PrevInsert(uu32 = true)
private String id;
@Column
@Comment("计划名称")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String planName;
@Column
@Comment("计划描述")
@ColDefine(type = ColType.VARCHAR, width = 1000)
private String planDescription;
@Column
@Comment("发放模式:1手动发放,2定时发放")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String grantMode;
@Column
@Comment("发放时间")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String grantTime;
@Column
@Comment("Cron表达式")
@ColDefine(type = ColType.VARCHAR, width = 100)
private String cronExp;
@Column
@Comment("计划状态")
@ColDefine(type = ColType.VARCHAR, width = 20)
private String status;
@Column
@Comment("发放用户ID列表")
@ColDefine(type = ColType.VARCHAR, customType = "mediumtext")
private String userIdList;
@Column
@Comment("发放积分")
@Default("0")
@ColDefine(customType = "decimal(18,2)")
private BigDecimal grantPoints;
}
@@ -0,0 +1,13 @@
package com.budwk.app.zhgh.pointsmall.pointsgrant.param;
import com.budwk.app.base.param.PageForm;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class PointsMallUserPointsGrantPlanPageParam extends PageForm {
private String planName;
private String status;
}
@@ -0,0 +1,25 @@
package com.budwk.app.zhgh.pointsmall.pointsgrant.service;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.zhgh.pointsmall.pointsgrant.models.PointsMallUserPointsGrantPlan;
import com.budwk.app.zhgh.pointsmall.pointsgrant.param.PointsMallUserPointsGrantPlanPageParam;
public interface PointsMallUserPointsGrantPlanService extends BaseService<PointsMallUserPointsGrantPlan> {
String STATUS_PENDING = "待执行";
String STATUS_IN_PROGRESS = "执行中";
String STATUS_COMPLETED = "已完成";
String STATUS_CANCELLED = "已取消";
String STATUS_FAILED = "已失败";
Pagination<PointsMallUserPointsGrantPlan> page(PointsMallUserPointsGrantPlanPageParam param);
void createPlan(PointsMallUserPointsGrantPlan plan);
boolean startPlan(String id);
boolean cancelPlan(String id);
int scheduledGrantPlan();
}
@@ -0,0 +1,128 @@
package com.budwk.app.zhgh.pointsmall.pointsgrant.service.impl;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.date.DateUtil;
import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.zhgh.pointsmall.points.param.PointsMallUserPointsBatchParam;
import com.budwk.app.zhgh.pointsmall.points.service.PointsMallUserPointsService;
import com.budwk.app.zhgh.pointsmall.pointsgrant.models.PointsMallUserPointsGrantPlan;
import com.budwk.app.zhgh.pointsmall.pointsgrant.param.PointsMallUserPointsGrantPlanPageParam;
import com.budwk.app.zhgh.pointsmall.pointsgrant.service.PointsMallUserPointsGrantPlanService;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.math.BigDecimal;
import java.util.List;
@IocBean(args = {"refer:dao"})
public class PointsMallUserPointsGrantPlanServiceImpl extends BaseServiceImpl<PointsMallUserPointsGrantPlan> implements PointsMallUserPointsGrantPlanService {
@Inject
private PointsMallUserPointsService pointsMallUserPointsService;
public PointsMallUserPointsGrantPlanServiceImpl(Dao dao) {
super(dao);
}
@Override
public Pagination<PointsMallUserPointsGrantPlan> page(PointsMallUserPointsGrantPlanPageParam param) {
Cnd cnd = Cnd.where("delFlag", "=", false);
if (StrUtil.isNotBlank(param.getPlanName())) {
cnd.and("planName", "like", "%" + param.getPlanName() + "%");
}
if (StrUtil.isNotBlank(param.getStatus())) {
cnd.and("status", "=", param.getStatus());
}
cnd.desc("createdAt");
return listPage(param.getPageNumber(), param.getPageSize(), PointsMallUserPointsGrantPlan.class, cnd);
}
@Override
public void createPlan(PointsMallUserPointsGrantPlan plan) {
checkPlan(plan);
plan.setStatus(STATUS_PENDING);
insert(plan);
}
@Override
public boolean startPlan(String id) {
if (StrUtil.isBlank(id)) {
throw new IllegalArgumentException("计划ID不能为空");
}
PointsMallUserPointsGrantPlan plan = fetch(id);
if (plan == null || Boolean.TRUE.equals(plan.getDelFlag())) {
throw new IllegalArgumentException("计划不存在");
}
if (!STATUS_PENDING.equals(plan.getStatus())) {
throw new IllegalArgumentException("计划未处于待执行状态,无法启动");
}
update(Chain.make("status", STATUS_IN_PROGRESS), Cnd.where("id", "=", id));
PointsMallUserPointsBatchParam batchParam = new PointsMallUserPointsBatchParam();
batchParam.setUserIds(plan.getUserIdList());
batchParam.setPoints(plan.getGrantPoints());
batchParam.setRemark("积分发放计划:" + plan.getPlanName());
try {
pointsMallUserPointsService.batchGrant(batchParam);
update(Chain.make("status", STATUS_COMPLETED), Cnd.where("id", "=", id));
} catch (RuntimeException e) {
update(Chain.make("status", STATUS_FAILED), Cnd.where("id", "=", id));
throw e;
}
return true;
}
@Override
public boolean cancelPlan(String id) {
if (StrUtil.isBlank(id)) {
throw new IllegalArgumentException("计划ID不能为空");
}
PointsMallUserPointsGrantPlan plan = fetch(id);
if (plan == null || Boolean.TRUE.equals(plan.getDelFlag())) {
throw new IllegalArgumentException("计划不存在");
}
if (!STATUS_PENDING.equals(plan.getStatus())) {
throw new IllegalArgumentException("计划未处于待执行状态,无法取消");
}
update(Chain.make("status", STATUS_CANCELLED), Cnd.where("id", "=", id));
return true;
}
@Override
public int scheduledGrantPlan() {
List<PointsMallUserPointsGrantPlan> plans = query(Cnd.where("delFlag", "=", false)
.and("status", "=", STATUS_PENDING)
.and("grantMode", "=", "2")
.and("grantTime", "<=", DateUtil.now()));
plans.forEach(plan -> startPlan(plan.getId()));
return plans.size();
}
private void checkPlan(PointsMallUserPointsGrantPlan plan) {
if (plan == null) {
throw new IllegalArgumentException("计划信息不能为空");
}
if (StrUtil.isBlank(plan.getPlanName())) {
throw new IllegalArgumentException("计划名称不能为空");
}
if (StrUtil.isBlank(plan.getGrantMode())) {
throw new IllegalArgumentException("发放模式不能为空");
}
if (!"1".equals(plan.getGrantMode()) && !"2".equals(plan.getGrantMode())) {
throw new IllegalArgumentException("发放模式不正确");
}
if ("2".equals(plan.getGrantMode()) && StrUtil.isBlank(plan.getGrantTime())) {
throw new IllegalArgumentException("定时发放时间不能为空");
}
if (StrUtil.isBlank(plan.getUserIdList())) {
throw new IllegalArgumentException("发放用户不能为空");
}
BigDecimal grantPoints = plan.getGrantPoints();
if (grantPoints == null || grantPoints.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("发放积分必须大于0");
}
}
}
@@ -0,0 +1,199 @@
<!--#
layout("/layouts/platform.html"){
#-->
<div id="app" v-cloak>
<el-card shadow="never">
<search @search="doSearch">
<search-item label="计划名称">
<el-input clearable placeholder="请输入计划名称" v-model="pageForm.planName"></el-input>
</search-item>
<search-item label="状态">
<el-select clearable placeholder="请选择状态" v-model="pageForm.status">
<el-option label="待执行" value="待执行"></el-option>
<el-option label="执行中" value="执行中"></el-option>
<el-option label="已完成" value="已完成"></el-option>
<el-option label="已取消" value="已取消"></el-option>
<el-option label="已失败" value="已失败"></el-option>
</el-select>
</search-item>
</search>
</el-card>
<el-card class="mt10" shadow="never">
<table-tool label="积分发放">
<el-button @click="openAdd" icon="el-icon-plus" size="small" type="primary">新建</el-button>
</table-tool>
<el-table :data="tableData" border size="small" style="width:100%" v-loading="tableLoading">
<el-table-column :index="indexMethod" align="center" label="序号" type="index" width="70"></el-table-column>
<el-table-column align="center" label="计划名称" min-width="180" prop="planName" show-overflow-tooltip></el-table-column>
<el-table-column align="center" label="计划描述" min-width="180" prop="planDescription" show-overflow-tooltip></el-table-column>
<el-table-column align="center" label="发放模式" prop="grantMode" width="110">
<template slot-scope="{row}">{{ row.grantMode === '2' ? '定时发放' : '手动发放' }}</template>
</el-table-column>
<el-table-column align="center" label="发放时间" prop="grantTime" width="170"></el-table-column>
<el-table-column align="center" label="发放积分" prop="grantPoints" width="110"></el-table-column>
<el-table-column align="center" label="当前状态" prop="status" width="110">
<template slot-scope="{row}">
<el-tag :type="statusTag(row.status)" size="small">{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column align="center" fixed="right" label="操作" width="170">
<template slot-scope="{row}">
<template v-if="row.status === '待执行'">
<el-button @click="startPlan(row)" size="mini" type="text">立即执行</el-button>
<el-button @click="cancelPlan(row)" size="mini" type="text">取消执行</el-button>
</template>
<span v-else>-</span>
</template>
</el-table-column>
</el-table>
<el-pagination :current-page="pageForm.pageNumber" :page-size="pageForm.pageSize" :page-sizes="[10,20,30,50]" :total="pageForm.totalCount" @current-change="pageChange" @size-change="sizeChange" layout="total, sizes, prev, pager, next" style="margin-top:16px;text-align:right"></el-pagination>
</el-card>
<el-dialog :close-on-click-modal="false" title="新建发放计划" :visible.sync="editDialogVisible" width="720px">
<el-form :model="formData" :rules="rules" label-width="120px" ref="form">
<el-form-item label="计划名称" prop="planName">
<el-input clearable placeholder="请输入计划名称" v-model="formData.planName"></el-input>
</el-form-item>
<el-form-item label="发放积分" prop="grantPoints">
<el-input-number :min="0.01" :precision="2" :step="1" controls-position="right" style="width:100%" v-model="formData.grantPoints"></el-input-number>
</el-form-item>
<el-form-item label="发放模式" prop="grantMode">
<el-radio-group v-model="formData.grantMode">
<el-radio border label="1">手动发放</el-radio>
<el-radio border label="2">定时发放</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="formData.grantMode === '2'" label="发放时间" prop="grantTime">
<el-date-picker placeholder="请选择发放时间" style="width:100%" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" v-model="formData.grantTime"></el-date-picker>
</el-form-item>
<el-form-item label="用户ID列表" prop="userIdList">
<el-input :rows="4" placeholder="请输入用户ID,多个用户用英文逗号分隔" type="textarea" v-model="formData.userIdList"></el-input>
</el-form-item>
<el-form-item label="计划描述">
<el-input :rows="3" maxlength="500" placeholder="请输入计划描述" show-word-limit type="textarea" v-model="formData.planDescription"></el-input>
</el-form-item>
</el-form>
<span slot="footer">
<el-button @click="editDialogVisible=false">取消</el-button>
<el-button :loading="submitLoading" @click="submitForm" type="primary">保存</el-button>
</span>
</el-dialog>
</div>
<script nonce="${cspNonce!}">
new Vue({
el: "#app",
data() {
return {
tableData: [],
tableLoading: false,
submitLoading: false,
editDialogVisible: false,
pageForm: {pageNumber: 1, pageSize: 10, totalCount: 0, planName: "", status: ""},
formData: {},
rules: {
planName: [{required: true, message: "请输入计划名称", trigger: "blur"}],
grantPoints: [{required: true, message: "请输入发放积分", trigger: "blur"}],
grantMode: [{required: true, message: "请选择发放模式", trigger: "change"}],
grantTime: [{required: true, message: "请选择发放时间", trigger: "change"}],
userIdList: [{required: true, message: "请输入用户ID列表", trigger: "blur"}]
}
}
},
methods: {
indexMethod(index) {
return index + 1 + (this.pageForm.pageNumber - 1) * this.pageForm.pageSize
},
statusTag(status) {
if (status === "已完成") return "success"
if (status === "已取消") return "info"
if (status === "已失败") return "danger"
if (status === "执行中") return "warning"
return "primary"
},
doSearch() {
this.pageForm.pageNumber = 1
this.pageData()
},
async pageData() {
this.tableLoading = true
const res = await this.$axios.post("/platform/zhgh/points-mall/pointsgrant/pageData", this.pageForm)
this.tableLoading = false
if (res.code === 0) {
this.tableData = res.data.list || []
this.pageForm.totalCount = res.data.totalCount || 0
} else {
this.$message.warning(res.msg)
}
},
pageChange(value) {
this.pageForm.pageNumber = value
this.pageData()
},
sizeChange(value) {
this.pageForm.pageSize = value
this.pageForm.pageNumber = 1
this.pageData()
},
openAdd() {
this.formData = {planName: "", grantMode: "1", grantTime: "", cronExp: "", userIdList: "", grantPoints: 1, planDescription: ""}
this.editDialogVisible = true
this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate())
},
submitForm() {
this.$refs.form.validate(async valid => {
if (!valid) return
this.submitLoading = true
const res = await this.$axios.post("/platform/zhgh/points-mall/pointsgrant/doAdd", this.formData)
this.submitLoading = false
if (res.code === 0) {
this.$message.success(res.msg)
this.editDialogVisible = false
this.pageData()
} else {
this.$message.warning(res.msg)
}
})
},
startPlan(row) {
this.$confirm("确定立即执行该积分发放计划吗?", "提示", {
type: "warning",
callback: async action => {
if (action === "confirm") {
const res = await this.$axios.post("/platform/zhgh/points-mall/pointsgrant/startPlan", {id: row.id})
if (res.code === 0) {
this.$message.success("执行成功")
this.pageData()
} else {
this.$message.warning(res.msg)
}
}
}
})
},
cancelPlan(row) {
this.$confirm("确定取消该积分发放计划吗?", "提示", {
type: "warning",
callback: async action => {
if (action === "confirm") {
const res = await this.$axios.post("/platform/zhgh/points-mall/pointsgrant/cancelPlan", {id: row.id})
if (res.code === 0) {
this.$message.success("取消成功")
this.pageData()
} else {
this.$message.warning(res.msg)
}
}
}
})
}
},
created() {
this.pageData()
}
})
</script>
<!--#
}
#-->