qiangdanxitong/docs/需求部门积分池增强方案.md
Your Name d412a7ad42 feat: 需求部门积分池自动发放功能
- 数据库:新增team_score_pool自动发放字段,新建score_grant_record表
- 后端:新增ScoreAutoGrantService定时任务服务
- 后端:新增ScoreGrantRecord实体和Mapper
- 后端:DepartmentController新增发放记录和手动发放接口
- 后端:TeamScorePoolService新增自动发放配置更新方法
- 前端:部门编辑弹窗新增积分自动发放配置
- 前端:积分池弹窗新增发放记录展示和手动发放按钮
- 前端:department.ts新增相关API
- 前端:config.ts TeamScorePool类型新增自动发放字段
2026-01-19 22:44:25 +08:00

12 KiB
Raw Permalink Blame History

需求部门积分池增强方案

一、功能概述

为需求部门(包括普通需求部门和一级需求部门)的积分池增加以下功能:

  1. 定时自动发放积分 - 支持按周/月自动发放固定积分值
  2. 过期未使用清零 - 周期结束时未使用的积分自动清零
  3. 积分发放记录 - 记录每次自动发放的历史

二、数据库设计

2.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.2 新增积分发放记录表

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 新增字段:

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

@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

@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 时,显示积分自动发放配置:

<!-- 积分自动发放配置(仅需求部门和一级需求部门) -->
<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:

<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. 需要考虑服务重启时定时任务的补偿机制

是否需要我开始实现这个功能?