feat: 需求部门积分池自动发放功能
- 数据库:新增team_score_pool自动发放字段,新建score_grant_record表 - 后端:新增ScoreAutoGrantService定时任务服务 - 后端:新增ScoreGrantRecord实体和Mapper - 后端:DepartmentController新增发放记录和手动发放接口 - 后端:TeamScorePoolService新增自动发放配置更新方法 - 前端:部门编辑弹窗新增积分自动发放配置 - 前端:积分池弹窗新增发放记录展示和手动发放按钮 - 前端:department.ts新增相关API - 前端:config.ts TeamScorePool类型新增自动发放字段
This commit is contained in:
parent
3a345c7994
commit
d412a7ad42
@ -8,8 +8,10 @@ import com.designhub.dto.DepartmentUpdateRequest;
|
||||
import com.designhub.entity.Brand;
|
||||
import com.designhub.entity.Department;
|
||||
import com.designhub.entity.DeptBrandRelation;
|
||||
import com.designhub.entity.ScoreGrantRecord;
|
||||
import com.designhub.entity.TeamScorePool;
|
||||
import com.designhub.service.DepartmentService;
|
||||
import com.designhub.service.ScoreAutoGrantService;
|
||||
import com.designhub.service.TeamScorePoolService;
|
||||
import com.designhub.vo.DepartmentOverviewVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@ -31,6 +33,7 @@ public class DepartmentController {
|
||||
|
||||
private final DepartmentService departmentService;
|
||||
private final TeamScorePoolService teamScorePoolService;
|
||||
private final ScoreAutoGrantService scoreAutoGrantService;
|
||||
|
||||
@Operation(summary = "获取所有部门列表")
|
||||
@GetMapping
|
||||
@ -112,6 +115,20 @@ public class DepartmentController {
|
||||
return Result.success(teamScorePoolService.getByDeptId(deptId));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新部门积分池自动发放配置")
|
||||
@PutMapping("/{deptId}/auto-grant-config")
|
||||
public Result<Void> updateAutoGrantConfig(
|
||||
@PathVariable Long deptId,
|
||||
@RequestParam(required = false) Integer autoGrantEnabled,
|
||||
@RequestParam(required = false) String autoGrantPeriod,
|
||||
@RequestParam(required = false) Integer autoGrantAmount,
|
||||
@RequestParam(required = false) Integer autoGrantDay,
|
||||
@RequestParam(required = false) Integer expireUnused) {
|
||||
teamScorePoolService.updateAutoGrantConfig(deptId, autoGrantEnabled, autoGrantPeriod,
|
||||
autoGrantAmount, autoGrantDay, expireUnused);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
// ==================== 品牌关联管理 ====================
|
||||
|
||||
@Operation(summary = "设置部门负责的品牌")
|
||||
@ -150,6 +167,23 @@ public class DepartmentController {
|
||||
public Result<List<Department>> getPrimaryDemandDepartments() {
|
||||
return Result.success(departmentService.getPrimaryDemandDepartments());
|
||||
}
|
||||
|
||||
// ==================== 积分发放记录 ====================
|
||||
|
||||
@Operation(summary = "获取部门积分发放记录")
|
||||
@GetMapping("/{deptId}/grant-records")
|
||||
public Result<List<ScoreGrantRecord>> getGrantRecords(
|
||||
@PathVariable Long deptId,
|
||||
@RequestParam(defaultValue = "50") int limit) {
|
||||
return Result.success(scoreAutoGrantService.getGrantRecords(deptId, limit));
|
||||
}
|
||||
|
||||
@Operation(summary = "手动触发积分发放(测试/补发)")
|
||||
@PostMapping("/{deptId}/manual-grant")
|
||||
public Result<Void> manualGrant(@PathVariable Long deptId) {
|
||||
scoreAutoGrantService.manualGrant(deptId);
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -0,0 +1,84 @@
|
||||
package com.designhub.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 积分发放记录实体类
|
||||
*/
|
||||
@Data
|
||||
@TableName("score_grant_record")
|
||||
public class ScoreGrantRecord implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 部门ID
|
||||
*/
|
||||
private Long deptId;
|
||||
|
||||
/**
|
||||
* 部门名称
|
||||
*/
|
||||
private String deptName;
|
||||
|
||||
/**
|
||||
* 发放类型:AUTO-自动发放,MANUAL-手动充值
|
||||
*/
|
||||
private String grantType;
|
||||
|
||||
/**
|
||||
* 发放积分数量
|
||||
*/
|
||||
private Integer grantAmount;
|
||||
|
||||
/**
|
||||
* 过期清零积分数量
|
||||
*/
|
||||
private Integer expiredAmount;
|
||||
|
||||
/**
|
||||
* 发放前余额
|
||||
*/
|
||||
private Integer balanceBefore;
|
||||
|
||||
/**
|
||||
* 发放后余额
|
||||
*/
|
||||
private Integer balanceAfter;
|
||||
|
||||
/**
|
||||
* 发放周期:WEEKLY/MONTHLY
|
||||
*/
|
||||
private String grantPeriod;
|
||||
|
||||
/**
|
||||
* 周期开始日期
|
||||
*/
|
||||
private LocalDate periodStart;
|
||||
|
||||
/**
|
||||
* 周期结束日期
|
||||
*/
|
||||
private LocalDate periodEnd;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
private String remark;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@ -120,6 +120,43 @@ public class TeamScorePool implements Serializable {
|
||||
*/
|
||||
private Integer alertThreshold;
|
||||
|
||||
// ==================== 自动发放相关字段 ====================
|
||||
|
||||
/**
|
||||
* 是否启用自动发放:0-否,1-是
|
||||
*/
|
||||
private Integer autoGrantEnabled;
|
||||
|
||||
/**
|
||||
* 自动发放周期:WEEKLY-每周,MONTHLY-每月
|
||||
*/
|
||||
private String autoGrantPeriod;
|
||||
|
||||
/**
|
||||
* 每次自动发放积分数量
|
||||
*/
|
||||
private Integer autoGrantAmount;
|
||||
|
||||
/**
|
||||
* 发放日期:周发放为1-7(周一到周日),月发放为1-28
|
||||
*/
|
||||
private Integer autoGrantDay;
|
||||
|
||||
/**
|
||||
* 过期未使用是否清零:0-否,1-是
|
||||
*/
|
||||
private Integer expireUnused;
|
||||
|
||||
/**
|
||||
* 上次自动发放时间
|
||||
*/
|
||||
private LocalDateTime lastGrantTime;
|
||||
|
||||
/**
|
||||
* 下次自动发放时间
|
||||
*/
|
||||
private LocalDateTime nextGrantTime;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
package com.designhub.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.designhub.entity.ScoreGrantRecord;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 积分发放记录Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface ScoreGrantRecordMapper extends BaseMapper<ScoreGrantRecord> {
|
||||
|
||||
/**
|
||||
* 根据部门ID查询发放记录
|
||||
*/
|
||||
@Select("SELECT * FROM score_grant_record WHERE dept_id = #{deptId} ORDER BY create_time DESC LIMIT #{limit}")
|
||||
List<ScoreGrantRecord> selectByDeptId(@Param("deptId") Long deptId, @Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 根据部门ID查询最近一次自动发放记录
|
||||
*/
|
||||
@Select("SELECT * FROM score_grant_record WHERE dept_id = #{deptId} AND grant_type = 'AUTO' ORDER BY create_time DESC LIMIT 1")
|
||||
ScoreGrantRecord selectLastAutoGrant(@Param("deptId") Long deptId);
|
||||
}
|
||||
@ -0,0 +1,292 @@
|
||||
package com.designhub.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.designhub.entity.Department;
|
||||
import com.designhub.entity.ScoreGrantRecord;
|
||||
import com.designhub.entity.TeamScorePool;
|
||||
import com.designhub.mapper.DepartmentMapper;
|
||||
import com.designhub.mapper.ScoreGrantRecordMapper;
|
||||
import com.designhub.mapper.TeamScorePoolMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.TemporalAdjusters;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 积分自动发放服务
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class ScoreAutoGrantService {
|
||||
|
||||
@Autowired
|
||||
private TeamScorePoolMapper teamScorePoolMapper;
|
||||
|
||||
@Autowired
|
||||
private ScoreGrantRecordMapper scoreGrantRecordMapper;
|
||||
|
||||
@Autowired
|
||||
private DepartmentMapper departmentMapper;
|
||||
|
||||
@Autowired
|
||||
private NotificationService notificationService;
|
||||
|
||||
/**
|
||||
* 定时任务:每天凌晨1点检查并执行自动发放
|
||||
*/
|
||||
@Scheduled(cron = "0 0 1 * * ?")
|
||||
@Transactional
|
||||
public void autoGrantScores() {
|
||||
log.info("开始执行积分自动发放定时任务...");
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
int dayOfWeek = now.getDayOfWeek().getValue(); // 1-7 (周一到周日)
|
||||
int dayOfMonth = now.getDayOfMonth();
|
||||
|
||||
// 查询所有启用自动发放的积分池
|
||||
LambdaQueryWrapper<TeamScorePool> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(TeamScorePool::getAutoGrantEnabled, 1);
|
||||
|
||||
List<TeamScorePool> pools = teamScorePoolMapper.selectList(wrapper);
|
||||
log.info("找到 {} 个启用自动发放的积分池", pools.size());
|
||||
|
||||
int successCount = 0;
|
||||
int skipCount = 0;
|
||||
int errorCount = 0;
|
||||
|
||||
for (TeamScorePool pool : pools) {
|
||||
try {
|
||||
boolean processed = processAutoGrant(pool, now, dayOfWeek, dayOfMonth);
|
||||
if (processed) {
|
||||
successCount++;
|
||||
} else {
|
||||
skipCount++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
errorCount++;
|
||||
log.error("自动发放积分失败: deptId={}, error={}", pool.getDeptId(), e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("积分自动发放定时任务完成: 成功={}, 跳过={}, 失败={}", successCount, skipCount, errorCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单个积分池的自动发放
|
||||
* @return true-已执行发放, false-未执行(不满足条件)
|
||||
*/
|
||||
private boolean processAutoGrant(TeamScorePool pool, LocalDateTime now,
|
||||
int dayOfWeek, int dayOfMonth) {
|
||||
String period = pool.getAutoGrantPeriod();
|
||||
Integer grantDay = pool.getAutoGrantDay();
|
||||
|
||||
if (period == null || grantDay == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否是发放日
|
||||
boolean shouldGrant = false;
|
||||
|
||||
if ("WEEKLY".equals(period) && dayOfWeek == grantDay) {
|
||||
shouldGrant = true;
|
||||
} else if ("MONTHLY".equals(period) && dayOfMonth == grantDay) {
|
||||
shouldGrant = true;
|
||||
}
|
||||
|
||||
if (!shouldGrant) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否已经发放过(防止重复发放)
|
||||
if (pool.getLastGrantTime() != null) {
|
||||
if ("WEEKLY".equals(period)) {
|
||||
// 本周是否已发放:检查上次发放是否在本周
|
||||
LocalDateTime weekStart = now.with(DayOfWeek.MONDAY).withHour(0).withMinute(0).withSecond(0).withNano(0);
|
||||
if (pool.getLastGrantTime().isAfter(weekStart)) {
|
||||
log.debug("部门 {} 本周已发放,跳过", pool.getDeptId());
|
||||
return false;
|
||||
}
|
||||
} else if ("MONTHLY".equals(period)) {
|
||||
// 本月是否已发放
|
||||
if (pool.getLastGrantTime().getMonthValue() == now.getMonthValue() &&
|
||||
pool.getLastGrantTime().getYear() == now.getYear()) {
|
||||
log.debug("部门 {} 本月已发放,跳过", pool.getDeptId());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 执行发放
|
||||
executeGrant(pool, now, period);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行积分发放
|
||||
*/
|
||||
@Transactional
|
||||
public void executeGrant(TeamScorePool pool, LocalDateTime now, String period) {
|
||||
int balanceBefore = pool.getBalance() != null ? pool.getBalance() : 0;
|
||||
int expiredAmount = 0;
|
||||
|
||||
// 如果启用过期清零,先清零旧积分
|
||||
if (pool.getExpireUnused() != null && pool.getExpireUnused() == 1 && balanceBefore > 0) {
|
||||
expiredAmount = balanceBefore;
|
||||
pool.setBalance(0);
|
||||
log.info("部门 {} 过期清零 {} 积分", pool.getDeptId(), expiredAmount);
|
||||
}
|
||||
|
||||
// 发放新积分
|
||||
int grantAmount = pool.getAutoGrantAmount() != null ? pool.getAutoGrantAmount() : 0;
|
||||
int currentBalance = pool.getBalance() != null ? pool.getBalance() : 0;
|
||||
pool.setBalance(currentBalance + grantAmount);
|
||||
|
||||
// 更新累计充值
|
||||
int totalRecharge = pool.getTotalRecharge() != null ? pool.getTotalRecharge() : 0;
|
||||
pool.setTotalRecharge(totalRecharge + grantAmount);
|
||||
|
||||
// 更新发放时间
|
||||
pool.setLastGrantTime(now);
|
||||
pool.setNextGrantTime(calculateNextGrantTime(now, period, pool.getAutoGrantDay()));
|
||||
pool.setUpdateTime(now);
|
||||
|
||||
// 重置周期使用量
|
||||
pool.setPeriodUsed(0);
|
||||
pool.setPeriodStartTime(now);
|
||||
|
||||
teamScorePoolMapper.updateById(pool);
|
||||
|
||||
// 获取部门名称
|
||||
String deptName = pool.getDeptName();
|
||||
if (deptName == null) {
|
||||
Department dept = departmentMapper.selectById(pool.getDeptId());
|
||||
if (dept != null) {
|
||||
deptName = dept.getDeptName();
|
||||
}
|
||||
}
|
||||
|
||||
// 记录发放历史
|
||||
ScoreGrantRecord record = new ScoreGrantRecord();
|
||||
record.setDeptId(pool.getDeptId());
|
||||
record.setDeptName(deptName);
|
||||
record.setGrantType("AUTO");
|
||||
record.setGrantAmount(grantAmount);
|
||||
record.setExpiredAmount(expiredAmount);
|
||||
record.setBalanceBefore(balanceBefore);
|
||||
record.setBalanceAfter(pool.getBalance());
|
||||
record.setGrantPeriod(period);
|
||||
record.setPeriodStart(calculatePeriodStart(now, period));
|
||||
record.setPeriodEnd(calculatePeriodEnd(now, period));
|
||||
record.setRemark(String.format("自动发放%d积分%s",
|
||||
grantAmount,
|
||||
expiredAmount > 0 ? ",过期清零" + expiredAmount + "积分" : ""));
|
||||
record.setCreateTime(now);
|
||||
|
||||
scoreGrantRecordMapper.insert(record);
|
||||
|
||||
log.info("自动发放积分成功: deptId={}, deptName={}, amount={}, expired={}, newBalance={}",
|
||||
pool.getDeptId(), deptName, grantAmount, expiredAmount, pool.getBalance());
|
||||
|
||||
// 发送通知给部门负责人
|
||||
sendGrantNotification(pool, grantAmount, expiredAmount, deptName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发积分发放(用于测试或补发)
|
||||
*/
|
||||
@Transactional
|
||||
public void manualGrant(Long deptId) {
|
||||
TeamScorePool pool = teamScorePoolMapper.selectOne(
|
||||
new LambdaQueryWrapper<TeamScorePool>().eq(TeamScorePool::getDeptId, deptId)
|
||||
);
|
||||
|
||||
if (pool == null || pool.getAutoGrantEnabled() == null || pool.getAutoGrantEnabled() != 1) {
|
||||
throw new RuntimeException("该部门未启用自动发放功能");
|
||||
}
|
||||
|
||||
executeGrant(pool, LocalDateTime.now(), pool.getAutoGrantPeriod());
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算下次发放时间
|
||||
*/
|
||||
private LocalDateTime calculateNextGrantTime(LocalDateTime now, String period, Integer grantDay) {
|
||||
if ("WEEKLY".equals(period)) {
|
||||
// 下周的发放日
|
||||
LocalDateTime nextWeek = now.plusWeeks(1);
|
||||
return nextWeek.with(DayOfWeek.of(grantDay)).withHour(1).withMinute(0).withSecond(0).withNano(0);
|
||||
} else {
|
||||
// 下月的发放日
|
||||
LocalDateTime nextMonth = now.plusMonths(1);
|
||||
int day = Math.min(grantDay, nextMonth.toLocalDate().lengthOfMonth());
|
||||
return nextMonth.withDayOfMonth(day).withHour(1).withMinute(0).withSecond(0).withNano(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算周期开始日期
|
||||
*/
|
||||
private LocalDate calculatePeriodStart(LocalDateTime now, String period) {
|
||||
if ("WEEKLY".equals(period)) {
|
||||
return now.toLocalDate().with(DayOfWeek.MONDAY);
|
||||
} else {
|
||||
return now.toLocalDate().withDayOfMonth(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算周期结束日期
|
||||
*/
|
||||
private LocalDate calculatePeriodEnd(LocalDateTime now, String period) {
|
||||
if ("WEEKLY".equals(period)) {
|
||||
return now.toLocalDate().with(DayOfWeek.SUNDAY);
|
||||
} else {
|
||||
return now.toLocalDate().with(TemporalAdjusters.lastDayOfMonth());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送发放通知
|
||||
*/
|
||||
private void sendGrantNotification(TeamScorePool pool, int grantAmount, int expiredAmount, String deptName) {
|
||||
try {
|
||||
// 查找部门负责人
|
||||
Department dept = departmentMapper.selectById(pool.getDeptId());
|
||||
if (dept != null && dept.getLeaderId() != null) {
|
||||
String title = "积分自动发放通知";
|
||||
String content;
|
||||
if (expiredAmount > 0) {
|
||||
content = String.format("您的部门「%s」已自动发放%d积分,上期未使用的%d积分已清零,当前余额%d积分。",
|
||||
deptName, grantAmount, expiredAmount, pool.getBalance());
|
||||
} else {
|
||||
content = String.format("您的部门「%s」已自动发放%d积分,当前余额%d积分。",
|
||||
deptName, grantAmount, pool.getBalance());
|
||||
}
|
||||
|
||||
notificationService.sendNotification(
|
||||
dept.getLeaderId(),
|
||||
title,
|
||||
content,
|
||||
"score_grant",
|
||||
pool.getId()
|
||||
);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("发送积分发放通知失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门的发放记录
|
||||
*/
|
||||
public List<ScoreGrantRecord> getGrantRecords(Long deptId, int limit) {
|
||||
return scoreGrantRecordMapper.selectByDeptId(deptId, limit);
|
||||
}
|
||||
}
|
||||
@ -279,6 +279,71 @@ public class TeamScorePoolService {
|
||||
teamScorePoolMapper.updateById(pool);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新积分池的自动发放配置
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateAutoGrantConfig(Long deptId, Integer autoGrantEnabled, String autoGrantPeriod,
|
||||
Integer autoGrantAmount, Integer autoGrantDay, Integer expireUnused) {
|
||||
TeamScorePool pool = upsertPool(deptId, null);
|
||||
|
||||
if (autoGrantEnabled != null) {
|
||||
pool.setAutoGrantEnabled(autoGrantEnabled);
|
||||
}
|
||||
if (autoGrantPeriod != null) {
|
||||
pool.setAutoGrantPeriod(autoGrantPeriod);
|
||||
}
|
||||
if (autoGrantAmount != null) {
|
||||
pool.setAutoGrantAmount(autoGrantAmount);
|
||||
}
|
||||
if (autoGrantDay != null) {
|
||||
pool.setAutoGrantDay(autoGrantDay);
|
||||
}
|
||||
if (expireUnused != null) {
|
||||
pool.setExpireUnused(expireUnused);
|
||||
}
|
||||
|
||||
// 如果启用自动发放且没有设置下次发放时间,计算下次发放时间
|
||||
if (autoGrantEnabled != null && autoGrantEnabled == 1 && pool.getNextGrantTime() == null) {
|
||||
pool.setNextGrantTime(calculateNextGrantTime(autoGrantPeriod, autoGrantDay));
|
||||
}
|
||||
|
||||
teamScorePoolMapper.updateById(pool);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算下次发放时间
|
||||
*/
|
||||
private LocalDateTime calculateNextGrantTime(String period, Integer grantDay) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
int day = grantDay != null ? grantDay : 1;
|
||||
|
||||
if ("WEEKLY".equals(period)) {
|
||||
// 计算下一个发放日(本周或下周)
|
||||
int currentDayOfWeek = now.getDayOfWeek().getValue();
|
||||
if (currentDayOfWeek < day) {
|
||||
// 本周还没到发放日
|
||||
return now.with(java.time.DayOfWeek.of(day)).withHour(1).withMinute(0).withSecond(0).withNano(0);
|
||||
} else {
|
||||
// 下周的发放日
|
||||
return now.plusWeeks(1).with(java.time.DayOfWeek.of(day)).withHour(1).withMinute(0).withSecond(0).withNano(0);
|
||||
}
|
||||
} else {
|
||||
// 计算下一个发放日(本月或下月)
|
||||
int currentDayOfMonth = now.getDayOfMonth();
|
||||
int targetDay = Math.min(day, now.toLocalDate().lengthOfMonth());
|
||||
if (currentDayOfMonth < targetDay) {
|
||||
// 本月还没到发放日
|
||||
return now.withDayOfMonth(targetDay).withHour(1).withMinute(0).withSecond(0).withNano(0);
|
||||
} else {
|
||||
// 下月的发放日
|
||||
LocalDateTime nextMonth = now.plusMonths(1);
|
||||
int nextTargetDay = Math.min(day, nextMonth.toLocalDate().lengthOfMonth());
|
||||
return nextMonth.withDayOfMonth(nextTargetDay).withHour(1).withMinute(0).withSecond(0).withNano(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接设置积分余额(用于初始化或同步)
|
||||
*/
|
||||
|
||||
61
database/V14__score_pool_auto_grant.sql
Normal file
61
database/V14__score_pool_auto_grant.sql
Normal file
@ -0,0 +1,61 @@
|
||||
-- V14__score_pool_auto_grant.sql
|
||||
-- 需求部门积分池自动发放功能数据库迁移脚本
|
||||
-- 创建日期:2026-01-19
|
||||
|
||||
-- =====================================================
|
||||
-- 1. 修改 team_score_pool 表,新增自动发放相关字段
|
||||
-- =====================================================
|
||||
|
||||
-- 是否启用自动发放
|
||||
ALTER TABLE team_score_pool ADD COLUMN auto_grant_enabled TINYINT DEFAULT 0 COMMENT '是否启用自动发放:0-否,1-是';
|
||||
|
||||
-- 自动发放周期
|
||||
ALTER TABLE team_score_pool ADD COLUMN auto_grant_period VARCHAR(20) DEFAULT 'WEEKLY' COMMENT '自动发放周期:WEEKLY-每周,MONTHLY-每月';
|
||||
|
||||
-- 每次自动发放积分数量
|
||||
ALTER TABLE team_score_pool ADD COLUMN auto_grant_amount INT DEFAULT 0 COMMENT '每次自动发放积分数量';
|
||||
|
||||
-- 发放日期
|
||||
ALTER TABLE team_score_pool ADD COLUMN auto_grant_day INT DEFAULT 1 COMMENT '发放日期:周发放为1-7(周一到周日),月发放为1-28';
|
||||
|
||||
-- 过期未使用是否清零
|
||||
ALTER TABLE team_score_pool ADD COLUMN expire_unused TINYINT DEFAULT 0 COMMENT '过期未使用是否清零:0-否,1-是';
|
||||
|
||||
-- 上次自动发放时间
|
||||
ALTER TABLE team_score_pool ADD COLUMN last_grant_time DATETIME COMMENT '上次自动发放时间';
|
||||
|
||||
-- 下次自动发放时间
|
||||
ALTER TABLE team_score_pool ADD COLUMN next_grant_time DATETIME COMMENT '下次自动发放时间';
|
||||
|
||||
-- =====================================================
|
||||
-- 2. 创建积分发放记录表
|
||||
-- =====================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS score_grant_record (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
|
||||
dept_id BIGINT NOT NULL COMMENT '部门ID',
|
||||
dept_name VARCHAR(100) COMMENT '部门名称',
|
||||
grant_type VARCHAR(20) NOT NULL COMMENT '发放类型:AUTO-自动发放,MANUAL-手动充值',
|
||||
grant_amount INT NOT NULL COMMENT '发放积分数量',
|
||||
expired_amount INT DEFAULT 0 COMMENT '过期清零积分数量',
|
||||
balance_before INT COMMENT '发放前余额',
|
||||
balance_after INT COMMENT '发放后余额',
|
||||
grant_period VARCHAR(20) COMMENT '发放周期:WEEKLY/MONTHLY',
|
||||
period_start DATE COMMENT '周期开始日期',
|
||||
period_end DATE COMMENT '周期结束日期',
|
||||
remark VARCHAR(500) COMMENT '备注',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
INDEX idx_dept_id (dept_id),
|
||||
INDEX idx_grant_type (grant_type),
|
||||
INDEX idx_create_time (create_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分发放记录表';
|
||||
|
||||
-- =====================================================
|
||||
-- 3. 为现有数据设置默认值
|
||||
-- =====================================================
|
||||
|
||||
UPDATE team_score_pool SET auto_grant_enabled = 0 WHERE auto_grant_enabled IS NULL;
|
||||
UPDATE team_score_pool SET auto_grant_period = 'WEEKLY' WHERE auto_grant_period IS NULL;
|
||||
UPDATE team_score_pool SET auto_grant_amount = 0 WHERE auto_grant_amount IS NULL;
|
||||
UPDATE team_score_pool SET auto_grant_day = 1 WHERE auto_grant_day IS NULL;
|
||||
UPDATE team_score_pool SET expire_unused = 0 WHERE expire_unused IS NULL;
|
||||
350
docs/需求部门积分池增强方案.md
Normal file
350
docs/需求部门积分池增强方案.md
Normal file
@ -0,0 +1,350 @@
|
||||
# 需求部门积分池增强方案
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
为**需求部门**(包括普通需求部门和一级需求部门)的积分池增加以下功能:
|
||||
1. **定时自动发放积分** - 支持按周/月自动发放固定积分值
|
||||
2. **过期未使用清零** - 周期结束时未使用的积分自动清零
|
||||
3. **积分发放记录** - 记录每次自动发放的历史
|
||||
|
||||
## 二、数据库设计
|
||||
|
||||
### 2.1 修改 `team_score_pool` 表
|
||||
|
||||
```sql
|
||||
-- 新增字段
|
||||
ALTER TABLE team_score_pool ADD COLUMN auto_grant_enabled TINYINT DEFAULT 0 COMMENT '是否启用自动发放:0-否,1-是';
|
||||
ALTER TABLE team_score_pool ADD COLUMN auto_grant_period VARCHAR(20) DEFAULT 'WEEKLY' COMMENT '自动发放周期:WEEKLY-每周,MONTHLY-每月';
|
||||
ALTER TABLE team_score_pool ADD COLUMN auto_grant_amount INT DEFAULT 0 COMMENT '每次自动发放积分数量';
|
||||
ALTER TABLE team_score_pool ADD COLUMN auto_grant_day INT DEFAULT 1 COMMENT '发放日期:周发放为1-7(周一到周日),月发放为1-28';
|
||||
ALTER TABLE team_score_pool ADD COLUMN expire_unused TINYINT DEFAULT 0 COMMENT '过期未使用是否清零:0-否,1-是';
|
||||
ALTER TABLE team_score_pool ADD COLUMN last_grant_time DATETIME COMMENT '上次自动发放时间';
|
||||
ALTER TABLE team_score_pool ADD COLUMN next_grant_time DATETIME COMMENT '下次自动发放时间';
|
||||
```
|
||||
|
||||
### 2.2 新增积分发放记录表
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS score_grant_record (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID',
|
||||
dept_id BIGINT NOT NULL COMMENT '部门ID',
|
||||
grant_type VARCHAR(20) NOT NULL COMMENT '发放类型:AUTO-自动发放,MANUAL-手动充值',
|
||||
grant_amount INT NOT NULL COMMENT '发放积分数量',
|
||||
expired_amount INT DEFAULT 0 COMMENT '过期清零积分数量',
|
||||
balance_before INT COMMENT '发放前余额',
|
||||
balance_after INT COMMENT '发放后余额',
|
||||
grant_period VARCHAR(20) COMMENT '发放周期:WEEKLY/MONTHLY',
|
||||
period_start DATE COMMENT '周期开始日期',
|
||||
period_end DATE COMMENT '周期结束日期',
|
||||
remark VARCHAR(500) COMMENT '备注',
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
INDEX idx_dept_id (dept_id),
|
||||
INDEX idx_create_time (create_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='积分发放记录表';
|
||||
```
|
||||
|
||||
## 三、后端开发
|
||||
|
||||
### 3.1 实体类修改
|
||||
|
||||
**TeamScorePool.java 新增字段:**
|
||||
```java
|
||||
private Integer autoGrantEnabled; // 是否启用自动发放
|
||||
private String autoGrantPeriod; // 自动发放周期:WEEKLY/MONTHLY
|
||||
private Integer autoGrantAmount; // 每次自动发放积分数量
|
||||
private Integer autoGrantDay; // 发放日期
|
||||
private Integer expireUnused; // 过期未使用是否清零
|
||||
private LocalDateTime lastGrantTime; // 上次自动发放时间
|
||||
private LocalDateTime nextGrantTime; // 下次自动发放时间
|
||||
```
|
||||
|
||||
### 3.2 新增实体类
|
||||
|
||||
**ScoreGrantRecord.java**
|
||||
```java
|
||||
@Data
|
||||
@TableName("score_grant_record")
|
||||
public class ScoreGrantRecord {
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long deptId;
|
||||
private String grantType; // AUTO/MANUAL
|
||||
private Integer grantAmount;
|
||||
private Integer expiredAmount;
|
||||
private Integer balanceBefore;
|
||||
private Integer balanceAfter;
|
||||
private String grantPeriod;
|
||||
private LocalDate periodStart;
|
||||
private LocalDate periodEnd;
|
||||
private String remark;
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 定时任务服务
|
||||
|
||||
**ScoreAutoGrantService.java**
|
||||
```java
|
||||
@Service
|
||||
@Slf4j
|
||||
public class ScoreAutoGrantService {
|
||||
|
||||
@Autowired
|
||||
private TeamScorePoolMapper teamScorePoolMapper;
|
||||
|
||||
@Autowired
|
||||
private ScoreGrantRecordMapper scoreGrantRecordMapper;
|
||||
|
||||
/**
|
||||
* 定时任务:每天凌晨1点检查并执行自动发放
|
||||
*/
|
||||
@Scheduled(cron = "0 0 1 * * ?")
|
||||
@Transactional
|
||||
public void autoGrantScores() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
int dayOfWeek = now.getDayOfWeek().getValue(); // 1-7
|
||||
int dayOfMonth = now.getDayOfMonth();
|
||||
|
||||
// 查询需要自动发放的积分池
|
||||
LambdaQueryWrapper<TeamScorePool> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(TeamScorePool::getAutoGrantEnabled, 1);
|
||||
|
||||
List<TeamScorePool> pools = teamScorePoolMapper.selectList(wrapper);
|
||||
|
||||
for (TeamScorePool pool : pools) {
|
||||
try {
|
||||
processAutoGrant(pool, now, dayOfWeek, dayOfMonth);
|
||||
} catch (Exception e) {
|
||||
log.error("自动发放积分失败: deptId={}", pool.getDeptId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processAutoGrant(TeamScorePool pool, LocalDateTime now,
|
||||
int dayOfWeek, int dayOfMonth) {
|
||||
String period = pool.getAutoGrantPeriod();
|
||||
int grantDay = pool.getAutoGrantDay();
|
||||
|
||||
boolean shouldGrant = false;
|
||||
|
||||
if ("WEEKLY".equals(period) && dayOfWeek == grantDay) {
|
||||
shouldGrant = true;
|
||||
} else if ("MONTHLY".equals(period) && dayOfMonth == grantDay) {
|
||||
shouldGrant = true;
|
||||
}
|
||||
|
||||
if (!shouldGrant) return;
|
||||
|
||||
// 检查是否已经发放过
|
||||
if (pool.getLastGrantTime() != null) {
|
||||
if ("WEEKLY".equals(period)) {
|
||||
// 本周是否已发放
|
||||
LocalDateTime weekStart = now.with(DayOfWeek.MONDAY).withHour(0).withMinute(0).withSecond(0);
|
||||
if (pool.getLastGrantTime().isAfter(weekStart)) {
|
||||
return; // 本周已发放
|
||||
}
|
||||
} else if ("MONTHLY".equals(period)) {
|
||||
// 本月是否已发放
|
||||
if (pool.getLastGrantTime().getMonthValue() == now.getMonthValue() &&
|
||||
pool.getLastGrantTime().getYear() == now.getYear()) {
|
||||
return; // 本月已发放
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 执行发放
|
||||
int balanceBefore = pool.getBalance();
|
||||
int expiredAmount = 0;
|
||||
|
||||
// 如果启用过期清零,先清零旧积分
|
||||
if (pool.getExpireUnused() == 1 && balanceBefore > 0) {
|
||||
expiredAmount = balanceBefore;
|
||||
pool.setBalance(0);
|
||||
}
|
||||
|
||||
// 发放新积分
|
||||
int grantAmount = pool.getAutoGrantAmount();
|
||||
pool.setBalance(pool.getBalance() + grantAmount);
|
||||
pool.setTotalRecharge(pool.getTotalRecharge() + grantAmount);
|
||||
pool.setLastGrantTime(now);
|
||||
pool.setNextGrantTime(calculateNextGrantTime(now, period, grantDay));
|
||||
|
||||
teamScorePoolMapper.updateById(pool);
|
||||
|
||||
// 记录发放历史
|
||||
ScoreGrantRecord record = new ScoreGrantRecord();
|
||||
record.setDeptId(pool.getDeptId());
|
||||
record.setGrantType("AUTO");
|
||||
record.setGrantAmount(grantAmount);
|
||||
record.setExpiredAmount(expiredAmount);
|
||||
record.setBalanceBefore(balanceBefore);
|
||||
record.setBalanceAfter(pool.getBalance());
|
||||
record.setGrantPeriod(period);
|
||||
record.setPeriodStart(calculatePeriodStart(now, period));
|
||||
record.setPeriodEnd(calculatePeriodEnd(now, period));
|
||||
record.setRemark(String.format("自动发放%s积分,过期清零%s积分",
|
||||
grantAmount, expiredAmount));
|
||||
record.setCreateTime(now);
|
||||
|
||||
scoreGrantRecordMapper.insert(record);
|
||||
|
||||
log.info("自动发放积分成功: deptId={}, amount={}, expired={}",
|
||||
pool.getDeptId(), grantAmount, expiredAmount);
|
||||
}
|
||||
|
||||
private LocalDateTime calculateNextGrantTime(LocalDateTime now, String period, int grantDay) {
|
||||
if ("WEEKLY".equals(period)) {
|
||||
return now.plusWeeks(1).with(DayOfWeek.of(grantDay)).withHour(1).withMinute(0);
|
||||
} else {
|
||||
return now.plusMonths(1).withDayOfMonth(grantDay).withHour(1).withMinute(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 四、前端开发
|
||||
|
||||
### 4.1 部门编辑弹窗增加配置
|
||||
|
||||
在部门编辑弹窗中,当部门类型为 `demand` 或 `primary_demand` 时,显示积分自动发放配置:
|
||||
|
||||
```vue
|
||||
<!-- 积分自动发放配置(仅需求部门和一级需求部门) -->
|
||||
<a-divider v-if="isDemandDept">积分自动发放配置</a-divider>
|
||||
<template v-if="isDemandDept">
|
||||
<a-form-item label="启用自动发放">
|
||||
<a-switch v-model:checked="formData.department.autoGrantEnabled" />
|
||||
</a-form-item>
|
||||
|
||||
<template v-if="formData.department.autoGrantEnabled">
|
||||
<a-form-item label="发放周期">
|
||||
<a-radio-group v-model:value="formData.department.autoGrantPeriod">
|
||||
<a-radio value="WEEKLY">每周发放</a-radio>
|
||||
<a-radio value="MONTHLY">每月发放</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="发放日期">
|
||||
<a-select v-model:value="formData.department.autoGrantDay">
|
||||
<template v-if="formData.department.autoGrantPeriod === 'WEEKLY'">
|
||||
<a-select-option :value="1">周一</a-select-option>
|
||||
<a-select-option :value="2">周二</a-select-option>
|
||||
<a-select-option :value="3">周三</a-select-option>
|
||||
<a-select-option :value="4">周四</a-select-option>
|
||||
<a-select-option :value="5">周五</a-select-option>
|
||||
<a-select-option :value="6">周六</a-select-option>
|
||||
<a-select-option :value="7">周日</a-select-option>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-select-option v-for="d in 28" :key="d" :value="d">{{ d }}号</a-select-option>
|
||||
</template>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="发放积分数量">
|
||||
<a-input-number
|
||||
v-model:value="formData.department.autoGrantAmount"
|
||||
:min="0"
|
||||
:max="100000"
|
||||
placeholder="每次发放的积分数量"
|
||||
style="width: 200px;"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="过期清零">
|
||||
<a-switch v-model:checked="formData.department.expireUnused" />
|
||||
<span style="margin-left: 8px; color: #888;">
|
||||
开启后,新积分发放时会清零上期未使用的积分
|
||||
</span>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 4.2 积分池弹窗显示发放记录
|
||||
|
||||
在积分池管理弹窗中增加"发放记录"tab:
|
||||
|
||||
```vue
|
||||
<a-tabs>
|
||||
<a-tab-pane key="overview" tab="积分概览">
|
||||
<!-- 现有的积分概览内容 -->
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="grant-records" tab="发放记录">
|
||||
<a-table
|
||||
:columns="grantRecordColumns"
|
||||
:data-source="grantRecords"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'grantType'">
|
||||
<a-tag :color="record.grantType === 'AUTO' ? 'blue' : 'green'">
|
||||
{{ record.grantType === 'AUTO' ? '自动发放' : '手动充值' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'expiredAmount'">
|
||||
<span v-if="record.expiredAmount > 0" style="color: #ff4d4f;">
|
||||
-{{ record.expiredAmount }}
|
||||
</span>
|
||||
<span v-else>--</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
```
|
||||
|
||||
## 五、使用场景示例
|
||||
|
||||
### 场景1:每周发放固定积分,过期清零
|
||||
|
||||
**配置:**
|
||||
- 自动发放:开启
|
||||
- 发放周期:每周
|
||||
- 发放日期:周一
|
||||
- 发放积分:500分
|
||||
- 过期清零:开启
|
||||
|
||||
**效果:**
|
||||
- 每周一凌晨自动发放500积分
|
||||
- 上周未使用的积分自动清零
|
||||
- 部门每周有固定500积分预算发起任务
|
||||
|
||||
### 场景2:每月发放积分,累积使用
|
||||
|
||||
**配置:**
|
||||
- 自动发放:开启
|
||||
- 发放周期:每月
|
||||
- 发放日期:1号
|
||||
- 发放积分:2000分
|
||||
- 过期清零:关闭
|
||||
|
||||
**效果:**
|
||||
- 每月1号自动发放2000积分
|
||||
- 未使用的积分累积到下月
|
||||
- 部门可以灵活使用积分
|
||||
|
||||
## 六、开发工时估算
|
||||
|
||||
| 阶段 | 内容 | 工时 |
|
||||
|------|------|------|
|
||||
| 数据库 | 表结构修改和新建 | 0.5天 |
|
||||
| 后端 | 实体类、Service、定时任务 | 1天 |
|
||||
| 后端 | API接口开发 | 0.5天 |
|
||||
| 前端 | 部门编辑弹窗修改 | 0.5天 |
|
||||
| 前端 | 积分池弹窗增强 | 0.5天 |
|
||||
| 测试 | 功能测试和联调 | 0.5天 |
|
||||
| **合计** | | **3.5天** |
|
||||
|
||||
## 七、注意事项
|
||||
|
||||
1. 定时任务需要确保服务器时间准确
|
||||
2. 过期清零会影响部门的任务发起能力,需要提前通知
|
||||
3. 建议在发放前一天发送提醒通知
|
||||
4. 需要考虑服务重启时定时任务的补偿机制
|
||||
|
||||
---
|
||||
|
||||
**是否需要我开始实现这个功能?**
|
||||
@ -62,6 +62,14 @@ export interface TeamScorePool {
|
||||
periodUsed?: number
|
||||
periodStartTime?: string
|
||||
alertThreshold?: number
|
||||
// 自动发放相关字段
|
||||
autoGrantEnabled?: number
|
||||
autoGrantPeriod?: string
|
||||
autoGrantAmount?: number
|
||||
autoGrantDay?: number
|
||||
expireUnused?: number
|
||||
lastGrantTime?: string
|
||||
nextGrantTime?: string
|
||||
}
|
||||
|
||||
export interface TaskSubType {
|
||||
|
||||
@ -142,3 +142,50 @@ export const getDemandDeptByBrandId = (brandId: number) => {
|
||||
export const getPrimaryDemandDepartments = () => {
|
||||
return request.get<DepartmentOverview[]>('/departments/primary-demand')
|
||||
}
|
||||
|
||||
// ==================== 积分发放记录 ====================
|
||||
|
||||
export interface ScoreGrantRecord {
|
||||
id: number
|
||||
deptId: number
|
||||
deptName?: string
|
||||
grantType: 'AUTO' | 'MANUAL'
|
||||
grantAmount: number
|
||||
expiredAmount: number
|
||||
balanceBefore: number
|
||||
balanceAfter: number
|
||||
grantPeriod?: string
|
||||
periodStart?: string
|
||||
periodEnd?: string
|
||||
remark?: string
|
||||
createTime: string
|
||||
}
|
||||
|
||||
// 获取部门积分发放记录
|
||||
export const getGrantRecords = (deptId: number, limit: number = 50) => {
|
||||
return request.get<ScoreGrantRecord[]>(`/departments/${deptId}/grant-records`, { params: { limit } })
|
||||
}
|
||||
|
||||
// 手动触发积分发放
|
||||
export const manualGrant = (deptId: number) => {
|
||||
return request.post(`/departments/${deptId}/manual-grant`)
|
||||
}
|
||||
|
||||
// 更新部门积分池自动发放配置
|
||||
export interface AutoGrantConfig {
|
||||
autoGrantEnabled?: number
|
||||
autoGrantPeriod?: 'WEEKLY' | 'MONTHLY'
|
||||
autoGrantAmount?: number
|
||||
autoGrantDay?: number
|
||||
expireUnused?: number
|
||||
}
|
||||
|
||||
export const updateAutoGrantConfig = (deptId: number, config: AutoGrantConfig) => {
|
||||
const params = new URLSearchParams()
|
||||
if (config.autoGrantEnabled !== undefined) params.append('autoGrantEnabled', String(config.autoGrantEnabled))
|
||||
if (config.autoGrantPeriod) params.append('autoGrantPeriod', config.autoGrantPeriod)
|
||||
if (config.autoGrantAmount !== undefined) params.append('autoGrantAmount', String(config.autoGrantAmount))
|
||||
if (config.autoGrantDay !== undefined) params.append('autoGrantDay', String(config.autoGrantDay))
|
||||
if (config.expireUnused !== undefined) params.append('expireUnused', String(config.expireUnused))
|
||||
return request.put(`/departments/${deptId}/auto-grant-config?${params.toString()}`)
|
||||
}
|
||||
|
||||
@ -2083,6 +2083,62 @@
|
||||
placeholder="数字越小越靠前"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<!-- 积分自动发放配置(仅需求部门和一级需求部门) -->
|
||||
<template v-if="isDemandDept">
|
||||
<a-divider>积分自动发放配置</a-divider>
|
||||
|
||||
<a-form-item label="启用自动发放">
|
||||
<a-switch v-model:checked="formData.department.autoGrantEnabled" />
|
||||
<span style="margin-left: 8px; color: #888; font-size: 12px;">
|
||||
开启后系统将按设定周期自动发放积分
|
||||
</span>
|
||||
</a-form-item>
|
||||
|
||||
<template v-if="formData.department.autoGrantEnabled">
|
||||
<a-form-item label="发放周期">
|
||||
<a-radio-group v-model:value="formData.department.autoGrantPeriod">
|
||||
<a-radio value="WEEKLY">每周发放</a-radio>
|
||||
<a-radio value="MONTHLY">每月发放</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="发放日期">
|
||||
<a-select v-model:value="formData.department.autoGrantDay" style="width: 200px;">
|
||||
<template v-if="formData.department.autoGrantPeriod === 'WEEKLY'">
|
||||
<a-select-option :value="1">周一</a-select-option>
|
||||
<a-select-option :value="2">周二</a-select-option>
|
||||
<a-select-option :value="3">周三</a-select-option>
|
||||
<a-select-option :value="4">周四</a-select-option>
|
||||
<a-select-option :value="5">周五</a-select-option>
|
||||
<a-select-option :value="6">周六</a-select-option>
|
||||
<a-select-option :value="7">周日</a-select-option>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-select-option v-for="d in 28" :key="d" :value="d">{{ d }}号</a-select-option>
|
||||
</template>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="发放积分数量">
|
||||
<a-input-number
|
||||
v-model:value="formData.department.autoGrantAmount"
|
||||
:min="0"
|
||||
:max="100000"
|
||||
placeholder="每次发放的积分数量"
|
||||
style="width: 200px;"
|
||||
/>
|
||||
<span style="margin-left: 8px;">分</span>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="过期清零">
|
||||
<a-switch v-model:checked="formData.department.expireUnused" />
|
||||
<span style="margin-left: 8px; color: #888; font-size: 12px;">
|
||||
开启后,新积分发放时会清零上期未使用的积分
|
||||
</span>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
@ -2204,6 +2260,71 @@
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
|
||||
<!-- 自动发放配置显示 -->
|
||||
<template v-if="currentScorePool.autoGrantEnabled === 1">
|
||||
<a-divider>自动发放配置</a-divider>
|
||||
<a-descriptions :column="2" bordered size="small">
|
||||
<a-descriptions-item label="发放周期">
|
||||
{{ currentScorePool.autoGrantPeriod === 'WEEKLY' ? '每周' : '每月' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="发放日期">
|
||||
{{ currentScorePool.autoGrantPeriod === 'WEEKLY'
|
||||
? ['', '周一', '周二', '周三', '周四', '周五', '周六', '周日'][currentScorePool.autoGrantDay || 1]
|
||||
: (currentScorePool.autoGrantDay || 1) + '号' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="发放积分">
|
||||
{{ currentScorePool.autoGrantAmount || 0 }} 分/次
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="过期清零">
|
||||
<a-tag :color="currentScorePool.expireUnused === 1 ? 'orange' : 'default'">
|
||||
{{ currentScorePool.expireUnused === 1 ? '是' : '否' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="上次发放" v-if="currentScorePool.lastGrantTime">
|
||||
{{ formatDateTime(currentScorePool.lastGrantTime) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="下次发放" v-if="currentScorePool.nextGrantTime">
|
||||
{{ formatDateTime(currentScorePool.nextGrantTime) }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<div style="margin-top: 16px;">
|
||||
<a-button type="default" @click="handleManualGrant" :loading="manualGrantLoading">
|
||||
手动触发发放
|
||||
</a-button>
|
||||
<span style="margin-left: 8px; color: #888; font-size: 12px;">
|
||||
(用于测试或补发)
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 发放记录 -->
|
||||
<a-divider>积分发放记录</a-divider>
|
||||
<a-table
|
||||
:columns="grantRecordColumns"
|
||||
:data-source="grantRecords"
|
||||
:loading="loadingGrantRecords"
|
||||
:pagination="{ pageSize: 5 }"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'grantType'">
|
||||
<a-tag :color="record.grantType === 'AUTO' ? 'blue' : 'green'">
|
||||
{{ record.grantType === 'AUTO' ? '自动发放' : '手动充值' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'createTime'">
|
||||
{{ formatDateTime(record.createTime) }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'amounts'">
|
||||
<span style="color: #52c41a;">+{{ record.grantAmount }}</span>
|
||||
<span v-if="record.expiredAmount > 0" style="color: #ff4d4f; margin-left: 8px;">
|
||||
-{{ record.expiredAmount }}(过期)
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
<a-empty v-else description="加载中..." />
|
||||
</a-modal>
|
||||
@ -2438,6 +2559,12 @@ import 'dayjs/locale/zh-cn'
|
||||
// 设置 dayjs 中文
|
||||
dayjs.locale('zh-cn')
|
||||
|
||||
// 格式化日期时间
|
||||
const formatDateTime = (dateStr: string | null | undefined): string => {
|
||||
if (!dateStr) return '--'
|
||||
return dayjs(dateStr).format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
|
||||
// Ant Design 中文配置
|
||||
const locale = zhCN
|
||||
import Icon from '@/components/Icon.vue'
|
||||
@ -2463,6 +2590,9 @@ import {
|
||||
getDepartmentScorePool,
|
||||
setDeptResponsibleBrands,
|
||||
getDeptResponsibleBrandIds,
|
||||
updateAutoGrantConfig,
|
||||
getGrantRecords,
|
||||
manualGrant,
|
||||
type DepartmentOverview,
|
||||
type DepartmentPayload,
|
||||
type ScoreAdjustPayload,
|
||||
@ -2529,6 +2659,12 @@ interface DepartmentForm {
|
||||
overLimitAction: 'alert' | 'block'
|
||||
initialScoreBalance?: number
|
||||
responsibleBrandIds?: number[] // 负责的品牌ID列表(仅需求部门)
|
||||
// 积分自动发放配置
|
||||
autoGrantEnabled?: boolean
|
||||
autoGrantPeriod?: 'WEEKLY' | 'MONTHLY'
|
||||
autoGrantAmount?: number
|
||||
autoGrantDay?: number
|
||||
expireUnused?: boolean
|
||||
}
|
||||
|
||||
const activeTab = ref('demand-types')
|
||||
@ -2605,6 +2741,20 @@ const scoreAdjustForm = reactive<ScoreAdjustPayload>({
|
||||
})
|
||||
const rechargeAmount = ref<number>(0)
|
||||
|
||||
// 积分发放记录相关
|
||||
const grantRecords = ref<any[]>([])
|
||||
const loadingGrantRecords = ref(false)
|
||||
const manualGrantLoading = ref(false)
|
||||
|
||||
const grantRecordColumns = [
|
||||
{ title: '发放类型', key: 'grantType', dataIndex: 'grantType', width: 100 },
|
||||
{ title: '发放/过期积分', key: 'amounts', width: 150 },
|
||||
{ title: '发放前余额', dataIndex: 'balanceBefore', width: 100 },
|
||||
{ title: '发放后余额', dataIndex: 'balanceAfter', width: 100 },
|
||||
{ title: '备注', dataIndex: 'remark', ellipsis: true },
|
||||
{ title: '时间', key: 'createTime', dataIndex: 'createTime', width: 160 }
|
||||
]
|
||||
|
||||
const userLoading = ref(false)
|
||||
const usersLoaded = ref(false)
|
||||
const usersByDept = ref<Record<number, User[]>>({})
|
||||
@ -2749,7 +2899,13 @@ const defaultDepartmentForm = (): DepartmentForm => ({
|
||||
alertThreshold: 80,
|
||||
overLimitAction: 'alert',
|
||||
initialScoreBalance: 0,
|
||||
responsibleBrandIds: []
|
||||
responsibleBrandIds: [],
|
||||
// 积分自动发放配置
|
||||
autoGrantEnabled: false,
|
||||
autoGrantPeriod: 'WEEKLY',
|
||||
autoGrantAmount: 500,
|
||||
autoGrantDay: 1,
|
||||
expireUnused: true
|
||||
})
|
||||
|
||||
const formData = reactive({
|
||||
@ -3645,6 +3801,9 @@ const showScorePoolModal = async (dept: DepartmentOverview) => {
|
||||
scoreAdjustForm.remark = ''
|
||||
rechargeAmount.value = 0
|
||||
modals.scorePool = true
|
||||
|
||||
// 加载发放记录
|
||||
loadGrantRecords(dept.id)
|
||||
} else {
|
||||
message.warning('该部门暂无积分池')
|
||||
}
|
||||
@ -3653,6 +3812,38 @@ const showScorePoolModal = async (dept: DepartmentOverview) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载发放记录
|
||||
const loadGrantRecords = async (deptId: number) => {
|
||||
loadingGrantRecords.value = true
|
||||
try {
|
||||
const result = await getGrantRecords(deptId, 50)
|
||||
grantRecords.value = result?.data || result || []
|
||||
} catch (error) {
|
||||
console.error('加载发放记录失败:', error)
|
||||
grantRecords.value = []
|
||||
} finally {
|
||||
loadingGrantRecords.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 手动触发发放
|
||||
const handleManualGrant = async () => {
|
||||
if (!currentDept.value?.id) return
|
||||
|
||||
manualGrantLoading.value = true
|
||||
try {
|
||||
await manualGrant(currentDept.value.id)
|
||||
message.success('手动发放成功')
|
||||
// 刷新积分池信息和发放记录
|
||||
await showScorePoolModal(currentDept.value)
|
||||
await loadDepartments()
|
||||
} catch (error: any) {
|
||||
message.error(error?.response?.data?.message || '手动发放失败')
|
||||
} finally {
|
||||
manualGrantLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理积分充值
|
||||
const handleRecharge = async () => {
|
||||
if (!currentDept.value?.id) {
|
||||
@ -3876,7 +4067,30 @@ const showDepartmentModal = async (item?: DepartmentOverview) => {
|
||||
alertThreshold: item.alertThreshold ?? 80,
|
||||
overLimitAction: (item.overLimitAction as 'alert' | 'block') || 'alert',
|
||||
initialScoreBalance: item.scoreBalance ?? 0,
|
||||
responsibleBrandIds
|
||||
responsibleBrandIds,
|
||||
// 积分自动发放配置(从积分池获取)
|
||||
autoGrantEnabled: false,
|
||||
autoGrantPeriod: 'WEEKLY',
|
||||
autoGrantAmount: 500,
|
||||
autoGrantDay: 1,
|
||||
expireUnused: true
|
||||
}
|
||||
|
||||
// 加载积分池中的自动发放配置
|
||||
if ((item.deptType === 'demand' || item.deptType === 'primary_demand') && item.id) {
|
||||
try {
|
||||
const poolResult = await getDepartmentScorePool(item.id)
|
||||
const pool = poolResult?.data || poolResult
|
||||
if (pool) {
|
||||
formData.department.autoGrantEnabled = pool.autoGrantEnabled === 1
|
||||
formData.department.autoGrantPeriod = pool.autoGrantPeriod || 'WEEKLY'
|
||||
formData.department.autoGrantAmount = pool.autoGrantAmount || 500
|
||||
formData.department.autoGrantDay = pool.autoGrantDay || 1
|
||||
formData.department.expireUnused = pool.expireUnused === 1
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('加载积分池配置失败:', e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
formData.department = defaultDepartmentForm()
|
||||
@ -4126,6 +4340,22 @@ const saveDepartment = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是需求部门或一级需求部门,保存自动发放配置
|
||||
if ((formData.department.deptType === 'demand' || formData.department.deptType === 'primary_demand') && deptId) {
|
||||
try {
|
||||
await updateAutoGrantConfig(deptId, {
|
||||
autoGrantEnabled: formData.department.autoGrantEnabled ? 1 : 0,
|
||||
autoGrantPeriod: formData.department.autoGrantPeriod,
|
||||
autoGrantAmount: formData.department.autoGrantAmount,
|
||||
autoGrantDay: formData.department.autoGrantDay,
|
||||
expireUnused: formData.department.expireUnused ? 1 : 0
|
||||
})
|
||||
} catch (grantError: any) {
|
||||
console.error('保存自动发放配置失败:', grantError)
|
||||
message.warning('部门保存成功,但自动发放配置保存失败:' + (grantError?.response?.data?.message || grantError?.message))
|
||||
}
|
||||
}
|
||||
|
||||
closeModal('department')
|
||||
await loadDepartments()
|
||||
} catch (error: any) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user