diff --git a/backend/src/main/java/com/designhub/controller/RewardRuleController.java b/backend/src/main/java/com/designhub/controller/RewardRuleController.java index b25db21..ba03d58 100644 --- a/backend/src/main/java/com/designhub/controller/RewardRuleController.java +++ b/backend/src/main/java/com/designhub/controller/RewardRuleController.java @@ -2,14 +2,18 @@ package com.designhub.controller; import com.baomidou.mybatisplus.core.metadata.IPage; import com.designhub.common.Result; -import com.designhub.entity.RewardRule; +import com.designhub.entity.*; +import com.designhub.service.RewardRecordService; import com.designhub.service.RewardRuleService; import com.designhub.util.JwtUtil; +import com.designhub.vo.RewardStatsVO; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.web.bind.annotation.*; +import java.time.LocalDateTime; import java.util.List; /** @@ -22,6 +26,7 @@ import java.util.List; public class RewardRuleController { private final RewardRuleService rewardRuleService; + private final RewardRecordService rewardRecordService; private final JwtUtil jwtUtil; @Operation(summary = "创建奖励规则") @@ -89,4 +94,101 @@ public class RewardRuleController { rewardRuleService.initDefaultRules(); return Result.success(); } + + // ==================== 奖励统计 ==================== + + @Operation(summary = "获取奖励统计数据") + @GetMapping("/stats") + public Result getStats(@RequestParam(defaultValue = "month") String period) { + return Result.success(rewardRecordService.getStats(period)); + } + + // ==================== 奖励记录 ==================== + + @Operation(summary = "获取奖励发放记录") + @GetMapping("/records") + public Result> getRecords( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "20") Integer pageSize, + @RequestParam(required = false) Long userId, + @RequestParam(required = false) Long ruleId, + @RequestParam(required = false) String rewardType, + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime startTime, + @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime endTime) { + return Result.success(rewardRecordService.getRecordList(page, pageSize, userId, ruleId, rewardType, startTime, endTime)); + } + + @Operation(summary = "获取我的奖励记录") + @GetMapping("/records/my") + public Result> getMyRecords( + @RequestParam(defaultValue = "1") Integer page, + @RequestParam(defaultValue = "20") Integer pageSize, + @RequestHeader("Authorization") String authHeader) { + String token = jwtUtil.extractTokenFromHeader(authHeader); + Long userId = jwtUtil.getUserIdFromToken(token); + return Result.success(rewardRecordService.getMyRecords(userId, page, pageSize)); + } + + // ==================== 徽章管理 ==================== + + @Operation(summary = "获取所有可用徽章") + @GetMapping("/badges") + public Result> getAllBadges() { + return Result.success(rewardRecordService.getAllBadges()); + } + + @Operation(summary = "获取用户徽章") + @GetMapping("/users/{userId}/badges") + public Result> getUserBadges(@PathVariable Long userId) { + return Result.success(rewardRecordService.getUserBadges(userId)); + } + + @Operation(summary = "获取我的徽章") + @GetMapping("/badges/my") + public Result> getMyBadges(@RequestHeader("Authorization") String authHeader) { + String token = jwtUtil.extractTokenFromHeader(authHeader); + Long userId = jwtUtil.getUserIdFromToken(token); + return Result.success(rewardRecordService.getUserBadges(userId)); + } + + // ==================== 称号管理 ==================== + + @Operation(summary = "获取所有可用称号") + @GetMapping("/titles") + public Result> getAllTitles() { + return Result.success(rewardRecordService.getAllTitles()); + } + + @Operation(summary = "获取用户称号") + @GetMapping("/users/{userId}/titles") + public Result> getUserTitles(@PathVariable Long userId) { + return Result.success(rewardRecordService.getUserTitles(userId)); + } + + @Operation(summary = "获取我的称号") + @GetMapping("/titles/my") + public Result> getMyTitles(@RequestHeader("Authorization") String authHeader) { + String token = jwtUtil.extractTokenFromHeader(authHeader); + Long userId = jwtUtil.getUserIdFromToken(token); + return Result.success(rewardRecordService.getUserTitles(userId)); + } + + @Operation(summary = "设置激活称号") + @PutMapping("/titles/active") + public Result setActiveTitle( + @RequestParam(required = false) Long titleId, + @RequestHeader("Authorization") String authHeader) { + String token = jwtUtil.extractTokenFromHeader(authHeader); + Long userId = jwtUtil.getUserIdFromToken(token); + rewardRecordService.setActiveTitle(userId, titleId); + return Result.success(); + } + + @Operation(summary = "获取我的激活称号") + @GetMapping("/titles/active") + public Result getMyActiveTitle(@RequestHeader("Authorization") String authHeader) { + String token = jwtUtil.extractTokenFromHeader(authHeader); + Long userId = jwtUtil.getUserIdFromToken(token); + return Result.success(rewardRecordService.getActiveTitle(userId)); + } } diff --git a/backend/src/main/java/com/designhub/entity/BadgeDefinition.java b/backend/src/main/java/com/designhub/entity/BadgeDefinition.java new file mode 100644 index 0000000..8d68900 --- /dev/null +++ b/backend/src/main/java/com/designhub/entity/BadgeDefinition.java @@ -0,0 +1,67 @@ +package com.designhub.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 徽章定义实体 + */ +@Data +@TableName("badge_definition") +public class BadgeDefinition implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 徽章代码 + */ + private String badgeCode; + + /** + * 徽章名称 + */ + private String badgeName; + + /** + * 徽章图标URL + */ + private String badgeIcon; + + /** + * 徽章颜色 + */ + private String badgeColor; + + /** + * 徽章描述 + */ + private String description; + + /** + * 分类:general/task/rating/special + */ + private String category; + + /** + * 排序 + */ + private Integer sortOrder; + + /** + * 是否启用 + */ + private Integer enabled; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; +} + diff --git a/backend/src/main/java/com/designhub/entity/RewardRecord.java b/backend/src/main/java/com/designhub/entity/RewardRecord.java new file mode 100644 index 0000000..20e14c7 --- /dev/null +++ b/backend/src/main/java/com/designhub/entity/RewardRecord.java @@ -0,0 +1,65 @@ +package com.designhub.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 奖励发放记录实体 + */ +@Data +@TableName("reward_record") +public class RewardRecord implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 用户ID + */ + private Long userId; + + /** + * 规则ID + */ + private Long ruleId; + + /** + * 规则名称(冗余) + */ + private String ruleName; + + /** + * 奖励类型:score/exp/badge/title + */ + private String rewardType; + + /** + * 奖励内容JSON + */ + private String rewardValue; + + /** + * 触发时的数据快照 + */ + private String triggerData; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + + // ========== 非数据库字段 ========== + + @TableField(exist = false) + private String userName; + + @TableField(exist = false) + private String userAvatar; +} + diff --git a/backend/src/main/java/com/designhub/entity/TitleDefinition.java b/backend/src/main/java/com/designhub/entity/TitleDefinition.java new file mode 100644 index 0000000..5ef1b1b --- /dev/null +++ b/backend/src/main/java/com/designhub/entity/TitleDefinition.java @@ -0,0 +1,62 @@ +package com.designhub.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 称号定义实体 + */ +@Data +@TableName("title_definition") +public class TitleDefinition implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 称号代码 + */ + private String titleCode; + + /** + * 称号名称 + */ + private String titleName; + + /** + * 称号颜色 + */ + private String titleColor; + + /** + * 称号描述 + */ + private String description; + + /** + * 分类 + */ + private String category; + + /** + * 排序 + */ + private Integer sortOrder; + + /** + * 是否启用 + */ + private Integer enabled; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; +} + diff --git a/backend/src/main/java/com/designhub/entity/UserBadge.java b/backend/src/main/java/com/designhub/entity/UserBadge.java new file mode 100644 index 0000000..43d4578 --- /dev/null +++ b/backend/src/main/java/com/designhub/entity/UserBadge.java @@ -0,0 +1,67 @@ +package com.designhub.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 用户徽章实体 + */ +@Data +@TableName("user_badge") +public class UserBadge implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 用户ID + */ + private Long userId; + + /** + * 徽章代码 + */ + private String badgeCode; + + /** + * 徽章名称 + */ + private String badgeName; + + /** + * 徽章图标URL + */ + private String badgeIcon; + + /** + * 徽章颜色 + */ + private String badgeColor; + + /** + * 徽章描述 + */ + private String description; + + /** + * 来源类型:rule/manual/achievement + */ + private String sourceType; + + /** + * 来源ID + */ + private Long sourceId; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; +} + diff --git a/backend/src/main/java/com/designhub/entity/UserTitle.java b/backend/src/main/java/com/designhub/entity/UserTitle.java new file mode 100644 index 0000000..d7181eb --- /dev/null +++ b/backend/src/main/java/com/designhub/entity/UserTitle.java @@ -0,0 +1,72 @@ +package com.designhub.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 用户称号实体 + */ +@Data +@TableName("user_title") +public class UserTitle implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 用户ID + */ + private Long userId; + + /** + * 称号代码 + */ + private String titleCode; + + /** + * 称号名称 + */ + private String titleName; + + /** + * 称号颜色 + */ + private String titleColor; + + /** + * 称号描述 + */ + private String description; + + /** + * 是否激活显示:0-否 1-是 + */ + private Integer isActive; + + /** + * 过期时间(NULL=永久) + */ + private LocalDateTime expireTime; + + /** + * 来源类型:rule/manual/achievement + */ + private String sourceType; + + /** + * 来源ID + */ + private Long sourceId; + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; +} + diff --git a/backend/src/main/java/com/designhub/mapper/BadgeDefinitionMapper.java b/backend/src/main/java/com/designhub/mapper/BadgeDefinitionMapper.java new file mode 100644 index 0000000..d860ea7 --- /dev/null +++ b/backend/src/main/java/com/designhub/mapper/BadgeDefinitionMapper.java @@ -0,0 +1,35 @@ +package com.designhub.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.designhub.entity.BadgeDefinition; +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 BadgeDefinitionMapper extends BaseMapper { + + /** + * 获取所有启用的徽章 + */ + @Select("SELECT * FROM badge_definition WHERE enabled = 1 ORDER BY sort_order, id") + List selectAllEnabled(); + + /** + * 根据代码获取徽章 + */ + @Select("SELECT * FROM badge_definition WHERE badge_code = #{badgeCode} AND enabled = 1") + BadgeDefinition selectByCode(@Param("badgeCode") String badgeCode); + + /** + * 按分类获取徽章 + */ + @Select("SELECT * FROM badge_definition WHERE category = #{category} AND enabled = 1 ORDER BY sort_order") + List selectByCategory(@Param("category") String category); +} + diff --git a/backend/src/main/java/com/designhub/mapper/RewardRecordMapper.java b/backend/src/main/java/com/designhub/mapper/RewardRecordMapper.java new file mode 100644 index 0000000..70e80b5 --- /dev/null +++ b/backend/src/main/java/com/designhub/mapper/RewardRecordMapper.java @@ -0,0 +1,110 @@ +package com.designhub.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.designhub.entity.RewardRecord; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * 奖励发放记录Mapper + */ +@Mapper +public interface RewardRecordMapper extends BaseMapper { + + /** + * 分页查询奖励记录(带用户信息) + */ + @Select("") + IPage selectPageWithUser(Page page, + @Param("userId") Long userId, + @Param("ruleId") Long ruleId, + @Param("rewardType") String rewardType, + @Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime); + + /** + * 统计用户某规则获得次数 + */ + @Select("SELECT COUNT(*) FROM reward_record WHERE user_id = #{userId} AND rule_id = #{ruleId}") + int countByUserAndRule(@Param("userId") Long userId, @Param("ruleId") Long ruleId); + + /** + * 统计时间范围内的奖励次数 + */ + @Select("SELECT COUNT(*) FROM reward_record WHERE create_time >= #{startTime} AND create_time <= #{endTime}") + int countByTimeRange(@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime); + + /** + * 统计时间范围内发放的总积分 + */ + @Select("SELECT COALESCE(SUM(CAST(JSON_EXTRACT(reward_value, '$.score') AS SIGNED)), 0) " + + "FROM reward_record " + + "WHERE reward_type = 'score' AND create_time >= #{startTime} AND create_time <= #{endTime}") + int sumScoreByTimeRange(@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime); + + /** + * 统计时间范围内的获奖用户数 + */ + @Select("SELECT COUNT(DISTINCT user_id) FROM reward_record WHERE create_time >= #{startTime} AND create_time <= #{endTime}") + int countDistinctUsersByTimeRange(@Param("startTime") LocalDateTime startTime, @Param("endTime") LocalDateTime endTime); + + /** + * 按规则统计触发次数(排行) + */ + @Select("SELECT rule_id, rule_name, COUNT(*) as trigger_count " + + "FROM reward_record " + + "WHERE create_time >= #{startTime} AND create_time <= #{endTime} AND rule_id IS NOT NULL " + + "GROUP BY rule_id, rule_name " + + "ORDER BY trigger_count DESC " + + "LIMIT #{limit}") + List> getRuleRanking(@Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime, + @Param("limit") int limit); + + /** + * 按用户统计获奖积分(排行) + */ + @Select("SELECT r.user_id, u.real_name as user_name, u.avatar as user_avatar, " + + "COALESCE(SUM(CAST(JSON_EXTRACT(r.reward_value, '$.score') AS SIGNED)), 0) as total_score " + + "FROM reward_record r " + + "LEFT JOIN sys_user u ON r.user_id = u.id " + + "WHERE r.reward_type = 'score' AND r.create_time >= #{startTime} AND r.create_time <= #{endTime} " + + "GROUP BY r.user_id, u.real_name, u.avatar " + + "ORDER BY total_score DESC " + + "LIMIT #{limit}") + List> getUserRanking(@Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime, + @Param("limit") int limit); + + /** + * 按天统计奖励趋势 + */ + @Select("SELECT DATE(create_time) as date, COUNT(*) as count, " + + "COALESCE(SUM(CAST(JSON_EXTRACT(reward_value, '$.score') AS SIGNED)), 0) as total_score " + + "FROM reward_record " + + "WHERE create_time >= #{startTime} AND create_time <= #{endTime} " + + "GROUP BY DATE(create_time) " + + "ORDER BY date") + List> getDailyTrend(@Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime); +} + diff --git a/backend/src/main/java/com/designhub/mapper/TitleDefinitionMapper.java b/backend/src/main/java/com/designhub/mapper/TitleDefinitionMapper.java new file mode 100644 index 0000000..4da2698 --- /dev/null +++ b/backend/src/main/java/com/designhub/mapper/TitleDefinitionMapper.java @@ -0,0 +1,35 @@ +package com.designhub.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.designhub.entity.TitleDefinition; +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 TitleDefinitionMapper extends BaseMapper { + + /** + * 获取所有启用的称号 + */ + @Select("SELECT * FROM title_definition WHERE enabled = 1 ORDER BY sort_order, id") + List selectAllEnabled(); + + /** + * 根据代码获取称号 + */ + @Select("SELECT * FROM title_definition WHERE title_code = #{titleCode} AND enabled = 1") + TitleDefinition selectByCode(@Param("titleCode") String titleCode); + + /** + * 按分类获取称号 + */ + @Select("SELECT * FROM title_definition WHERE category = #{category} AND enabled = 1 ORDER BY sort_order") + List selectByCategory(@Param("category") String category); +} + diff --git a/backend/src/main/java/com/designhub/mapper/UserBadgeMapper.java b/backend/src/main/java/com/designhub/mapper/UserBadgeMapper.java new file mode 100644 index 0000000..ffc01e2 --- /dev/null +++ b/backend/src/main/java/com/designhub/mapper/UserBadgeMapper.java @@ -0,0 +1,29 @@ +package com.designhub.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.designhub.entity.UserBadge; +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 UserBadgeMapper extends BaseMapper { + + /** + * 获取用户的所有徽章 + */ + @Select("SELECT * FROM user_badge WHERE user_id = #{userId} ORDER BY create_time DESC") + List selectByUserId(@Param("userId") Long userId); + + /** + * 检查用户是否拥有某徽章 + */ + @Select("SELECT COUNT(*) FROM user_badge WHERE user_id = #{userId} AND badge_code = #{badgeCode}") + int countByUserAndBadge(@Param("userId") Long userId, @Param("badgeCode") String badgeCode); +} + diff --git a/backend/src/main/java/com/designhub/mapper/UserTitleMapper.java b/backend/src/main/java/com/designhub/mapper/UserTitleMapper.java new file mode 100644 index 0000000..7b0e1a9 --- /dev/null +++ b/backend/src/main/java/com/designhub/mapper/UserTitleMapper.java @@ -0,0 +1,42 @@ +package com.designhub.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.designhub.entity.UserTitle; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; + +import java.util.List; + +/** + * 用户称号Mapper + */ +@Mapper +public interface UserTitleMapper extends BaseMapper { + + /** + * 获取用户的所有称号 + */ + @Select("SELECT * FROM user_title WHERE user_id = #{userId} ORDER BY is_active DESC, create_time DESC") + List selectByUserId(@Param("userId") Long userId); + + /** + * 获取用户激活的称号 + */ + @Select("SELECT * FROM user_title WHERE user_id = #{userId} AND is_active = 1 LIMIT 1") + UserTitle selectActiveByUserId(@Param("userId") Long userId); + + /** + * 检查用户是否拥有某称号 + */ + @Select("SELECT COUNT(*) FROM user_title WHERE user_id = #{userId} AND title_code = #{titleCode}") + int countByUserAndTitle(@Param("userId") Long userId, @Param("titleCode") String titleCode); + + /** + * 取消用户所有称号的激活状态 + */ + @Update("UPDATE user_title SET is_active = 0 WHERE user_id = #{userId}") + int deactivateAllByUserId(@Param("userId") Long userId); +} + diff --git a/backend/src/main/java/com/designhub/service/RewardRecordService.java b/backend/src/main/java/com/designhub/service/RewardRecordService.java new file mode 100644 index 0000000..66f3ba4 --- /dev/null +++ b/backend/src/main/java/com/designhub/service/RewardRecordService.java @@ -0,0 +1,248 @@ +package com.designhub.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.designhub.entity.*; +import com.designhub.mapper.*; +import com.designhub.vo.RewardStatsVO; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.temporal.TemporalAdjusters; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 奖励记录服务 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class RewardRecordService { + + private final RewardRecordMapper rewardRecordMapper; + private final UserBadgeMapper userBadgeMapper; + private final UserTitleMapper userTitleMapper; + private final BadgeDefinitionMapper badgeDefinitionMapper; + private final TitleDefinitionMapper titleDefinitionMapper; + private final ObjectMapper objectMapper; + + /** + * 记录奖励发放 + */ + @Transactional(rollbackFor = Exception.class) + public RewardRecord createRecord(Long userId, Long ruleId, String ruleName, + String rewardType, Object rewardValue, Object triggerData) { + RewardRecord record = new RewardRecord(); + record.setUserId(userId); + record.setRuleId(ruleId); + record.setRuleName(ruleName); + record.setRewardType(rewardType); + record.setCreateTime(LocalDateTime.now()); + + try { + if (rewardValue != null) { + record.setRewardValue(objectMapper.writeValueAsString(rewardValue)); + } + if (triggerData != null) { + record.setTriggerData(objectMapper.writeValueAsString(triggerData)); + } + } catch (Exception e) { + log.error("序列化奖励数据失败", e); + } + + rewardRecordMapper.insert(record); + log.info("记录奖励发放: userId={}, ruleId={}, type={}", userId, ruleId, rewardType); + return record; + } + + /** + * 分页查询奖励记录 + */ + public IPage getRecordList(Integer page, Integer pageSize, + Long userId, Long ruleId, String rewardType, + LocalDateTime startTime, LocalDateTime endTime) { + Page pageParam = new Page<>(page, pageSize); + return rewardRecordMapper.selectPageWithUser(pageParam, userId, ruleId, rewardType, startTime, endTime); + } + + /** + * 获取我的奖励记录 + */ + public IPage getMyRecords(Long userId, Integer page, Integer pageSize) { + return getRecordList(page, pageSize, userId, null, null, null, null); + } + + /** + * 检查用户某规则获得次数 + */ + public int getUserRuleCount(Long userId, Long ruleId) { + return rewardRecordMapper.countByUserAndRule(userId, ruleId); + } + + /** + * 获取奖励统计数据 + */ + public RewardStatsVO getStats(String period) { + LocalDateTime startTime; + LocalDateTime endTime = LocalDateTime.now(); + + switch (period) { + case "today": + startTime = LocalDate.now().atStartOfDay(); + break; + case "week": + startTime = LocalDate.now().with(TemporalAdjusters.previousOrSame(java.time.DayOfWeek.MONDAY)).atStartOfDay(); + break; + case "month": + startTime = LocalDate.now().with(TemporalAdjusters.firstDayOfMonth()).atStartOfDay(); + break; + default: + startTime = LocalDate.now().with(TemporalAdjusters.firstDayOfMonth()).atStartOfDay(); + } + + RewardStatsVO stats = new RewardStatsVO(); + stats.setTotalCount(rewardRecordMapper.countByTimeRange(startTime, endTime)); + stats.setTotalScore(rewardRecordMapper.sumScoreByTimeRange(startTime, endTime)); + stats.setUserCount(rewardRecordMapper.countDistinctUsersByTimeRange(startTime, endTime)); + stats.setRuleRanking(rewardRecordMapper.getRuleRanking(startTime, endTime, 10)); + stats.setUserRanking(rewardRecordMapper.getUserRanking(startTime, endTime, 10)); + stats.setDailyTrend(rewardRecordMapper.getDailyTrend(startTime, endTime)); + + return stats; + } + + // ==================== 徽章相关 ==================== + + /** + * 授予用户徽章 + */ + @Transactional(rollbackFor = Exception.class) + public UserBadge grantBadge(Long userId, String badgeCode, String sourceType, Long sourceId) { + // 检查是否已拥有 + if (userBadgeMapper.countByUserAndBadge(userId, badgeCode) > 0) { + log.info("用户已拥有该徽章: userId={}, badgeCode={}", userId, badgeCode); + return null; + } + + // 获取徽章定义 + BadgeDefinition badge = badgeDefinitionMapper.selectByCode(badgeCode); + if (badge == null) { + log.warn("徽章不存在: badgeCode={}", badgeCode); + return null; + } + + UserBadge userBadge = new UserBadge(); + userBadge.setUserId(userId); + userBadge.setBadgeCode(badgeCode); + userBadge.setBadgeName(badge.getBadgeName()); + userBadge.setBadgeIcon(badge.getBadgeIcon()); + userBadge.setBadgeColor(badge.getBadgeColor()); + userBadge.setDescription(badge.getDescription()); + userBadge.setSourceType(sourceType); + userBadge.setSourceId(sourceId); + userBadge.setCreateTime(LocalDateTime.now()); + + userBadgeMapper.insert(userBadge); + log.info("授予用户徽章: userId={}, badgeCode={}", userId, badgeCode); + return userBadge; + } + + /** + * 获取用户徽章列表 + */ + public List getUserBadges(Long userId) { + return userBadgeMapper.selectByUserId(userId); + } + + /** + * 获取所有可用徽章 + */ + public List getAllBadges() { + return badgeDefinitionMapper.selectAllEnabled(); + } + + // ==================== 称号相关 ==================== + + /** + * 授予用户称号 + */ + @Transactional(rollbackFor = Exception.class) + public UserTitle grantTitle(Long userId, String titleCode, String sourceType, Long sourceId, LocalDateTime expireTime) { + // 检查是否已拥有 + if (userTitleMapper.countByUserAndTitle(userId, titleCode) > 0) { + log.info("用户已拥有该称号: userId={}, titleCode={}", userId, titleCode); + return null; + } + + // 获取称号定义 + TitleDefinition title = titleDefinitionMapper.selectByCode(titleCode); + if (title == null) { + log.warn("称号不存在: titleCode={}", titleCode); + return null; + } + + UserTitle userTitle = new UserTitle(); + userTitle.setUserId(userId); + userTitle.setTitleCode(titleCode); + userTitle.setTitleName(title.getTitleName()); + userTitle.setTitleColor(title.getTitleColor()); + userTitle.setDescription(title.getDescription()); + userTitle.setIsActive(0); + userTitle.setExpireTime(expireTime); + userTitle.setSourceType(sourceType); + userTitle.setSourceId(sourceId); + userTitle.setCreateTime(LocalDateTime.now()); + + userTitleMapper.insert(userTitle); + log.info("授予用户称号: userId={}, titleCode={}", userId, titleCode); + return userTitle; + } + + /** + * 获取用户称号列表 + */ + public List getUserTitles(Long userId) { + return userTitleMapper.selectByUserId(userId); + } + + /** + * 获取用户当前激活的称号 + */ + public UserTitle getActiveTitle(Long userId) { + return userTitleMapper.selectActiveByUserId(userId); + } + + /** + * 设置激活称号 + */ + @Transactional(rollbackFor = Exception.class) + public void setActiveTitle(Long userId, Long titleId) { + // 先取消所有激活 + userTitleMapper.deactivateAllByUserId(userId); + + // 激活指定称号 + if (titleId != null && titleId > 0) { + UserTitle title = userTitleMapper.selectById(titleId); + if (title != null && title.getUserId().equals(userId)) { + title.setIsActive(1); + userTitleMapper.updateById(title); + } + } + } + + /** + * 获取所有可用称号 + */ + public List getAllTitles() { + return titleDefinitionMapper.selectAllEnabled(); + } +} + diff --git a/backend/src/main/java/com/designhub/service/RewardRuleService.java b/backend/src/main/java/com/designhub/service/RewardRuleService.java index 35f67a0..b98aedc 100644 --- a/backend/src/main/java/com/designhub/service/RewardRuleService.java +++ b/backend/src/main/java/com/designhub/service/RewardRuleService.java @@ -4,10 +4,12 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.designhub.common.ResultCode; +import com.designhub.entity.RewardRecord; import com.designhub.entity.RewardRule; import com.designhub.entity.Task; import com.designhub.entity.User; import com.designhub.exception.BusinessException; +import com.designhub.mapper.RewardRecordMapper; import com.designhub.mapper.RewardRuleMapper; import com.designhub.mapper.TaskMapper; import com.designhub.mapper.UserMapper; @@ -34,6 +36,7 @@ import java.util.Map; public class RewardRuleService { private final RewardRuleMapper rewardRuleMapper; + private final RewardRecordMapper rewardRecordMapper; private final UserMapper userMapper; private final TaskMapper taskMapper; private final NotificationService notificationService; @@ -240,11 +243,43 @@ public class RewardRuleService { */ @Transactional public void applyReward(RewardRule rule, User user) { + applyReward(rule, user, null); + } + + /** + * 应用奖励(带触发数据) + */ + @Transactional + public void applyReward(RewardRule rule, User user, Object triggerData) { if (rule.getRewardScore() != null && rule.getRewardScore() > 0) { int currentScore = user.getScore() != null ? user.getScore() : 0; user.setScore(currentScore + rule.getRewardScore()); userMapper.updateById(user); + // 记录奖励发放 + try { + Map rewardValue = new java.util.HashMap<>(); + rewardValue.put("score", rule.getRewardScore()); + + RewardRecord record = new RewardRecord(); + record.setUserId(user.getId()); + record.setRuleId(rule.getId()); + record.setRuleName(rule.getName()); + record.setRewardType("score"); + record.setRewardValue(objectMapper.writeValueAsString(rewardValue)); + if (triggerData != null) { + record.setTriggerData(objectMapper.writeValueAsString(triggerData)); + } + record.setCreateTime(LocalDateTime.now()); + rewardRecordMapper.insert(record); + } catch (Exception e) { + log.error("记录奖励发放失败", e); + } + + // 更新规则触发次数 + rule.setTriggerCount(rule.getTriggerCount() != null ? rule.getTriggerCount() + 1 : 1); + rewardRuleMapper.updateById(rule); + // 发送通知 notificationService.sendNotification( user.getId(), diff --git a/backend/src/main/java/com/designhub/vo/RewardStatsVO.java b/backend/src/main/java/com/designhub/vo/RewardStatsVO.java new file mode 100644 index 0000000..aae35c4 --- /dev/null +++ b/backend/src/main/java/com/designhub/vo/RewardStatsVO.java @@ -0,0 +1,52 @@ +package com.designhub.vo; + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +/** + * 奖励统计VO + */ +@Data +public class RewardStatsVO { + + /** + * 奖励发放总次数 + */ + private Integer totalCount; + + /** + * 发放总积分 + */ + private Integer totalScore; + + /** + * 获奖用户数 + */ + private Integer userCount; + + /** + * 规则命中率(已触发规则数/总规则数) + */ + private Double hitRate; + + /** + * 规则触发排行 + * [{ruleId, ruleName, triggerCount}, ...] + */ + private List> ruleRanking; + + /** + * 用户获奖排行 + * [{userId, userName, userAvatar, totalScore}, ...] + */ + private List> userRanking; + + /** + * 每日趋势 + * [{date, count, totalScore}, ...] + */ + private List> dailyTrend; +} + diff --git a/database/reward_system_upgrade.sql b/database/reward_system_upgrade.sql new file mode 100644 index 0000000..01f8850 --- /dev/null +++ b/database/reward_system_upgrade.sql @@ -0,0 +1,116 @@ +-- ======================================== +-- 奖励系统升级脚本 +-- 版本:v2.0 +-- 日期:2026-01-04 +-- ======================================== + +-- 1. 新增奖励发放记录表 +CREATE TABLE IF NOT EXISTS reward_record ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + user_id BIGINT NOT NULL COMMENT '用户ID', + rule_id BIGINT COMMENT '规则ID', + rule_name VARCHAR(100) COMMENT '规则名称', + reward_type VARCHAR(50) NOT NULL COMMENT '奖励类型:score/exp/badge/title', + reward_value TEXT COMMENT '奖励内容JSON', + trigger_data TEXT COMMENT '触发时的数据快照', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_user_id (user_id), + INDEX idx_rule_id (rule_id), + INDEX idx_create_time (create_time), + INDEX idx_reward_type (reward_type) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='奖励发放记录'; + +-- 2. 扩展奖励规则表 +ALTER TABLE reward_rule ADD COLUMN IF NOT EXISTS reward_exp INT DEFAULT 0 COMMENT '奖励经验值'; +ALTER TABLE reward_rule ADD COLUMN IF NOT EXISTS reward_title VARCHAR(50) COMMENT '奖励称号'; +ALTER TABLE reward_rule ADD COLUMN IF NOT EXISTS reward_privilege TEXT COMMENT '奖励特权JSON'; +ALTER TABLE reward_rule ADD COLUMN IF NOT EXISTS max_times_per_user INT DEFAULT 0 COMMENT '每用户最大获得次数(0=无限)'; +ALTER TABLE reward_rule ADD COLUMN IF NOT EXISTS total_budget INT DEFAULT 0 COMMENT '总预算积分(0=无限)'; +ALTER TABLE reward_rule ADD COLUMN IF NOT EXISTS used_budget INT DEFAULT 0 COMMENT '已使用预算'; +ALTER TABLE reward_rule ADD COLUMN IF NOT EXISTS trigger_count INT DEFAULT 0 COMMENT '触发次数'; +ALTER TABLE reward_rule ADD COLUMN IF NOT EXISTS start_time DATETIME COMMENT '生效开始时间'; +ALTER TABLE reward_rule ADD COLUMN IF NOT EXISTS end_time DATETIME COMMENT '生效结束时间'; + +-- 3. 用户徽章表 +CREATE TABLE IF NOT EXISTS user_badge ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + user_id BIGINT NOT NULL COMMENT '用户ID', + badge_code VARCHAR(50) NOT NULL COMMENT '徽章代码', + badge_name VARCHAR(100) NOT NULL COMMENT '徽章名称', + badge_icon VARCHAR(255) COMMENT '徽章图标URL', + badge_color VARCHAR(20) DEFAULT '#31f08b' COMMENT '徽章颜色', + description VARCHAR(255) COMMENT '徽章描述', + source_type VARCHAR(50) COMMENT '来源类型:rule/manual/achievement', + source_id BIGINT COMMENT '来源ID', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uk_user_badge (user_id, badge_code), + INDEX idx_user_id (user_id), + INDEX idx_badge_code (badge_code) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户徽章'; + +-- 4. 用户称号表 +CREATE TABLE IF NOT EXISTS user_title ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + user_id BIGINT NOT NULL COMMENT '用户ID', + title_code VARCHAR(50) NOT NULL COMMENT '称号代码', + title_name VARCHAR(100) NOT NULL COMMENT '称号名称', + title_color VARCHAR(20) DEFAULT '#31f08b' COMMENT '称号颜色', + description VARCHAR(255) COMMENT '称号描述', + is_active INT DEFAULT 0 COMMENT '是否激活显示:0-否 1-是', + expire_time DATETIME COMMENT '过期时间(NULL=永久)', + source_type VARCHAR(50) COMMENT '来源类型:rule/manual/achievement', + source_id BIGINT COMMENT '来源ID', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uk_user_title (user_id, title_code), + INDEX idx_user_id (user_id), + INDEX idx_is_active (is_active) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户称号'; + +-- 5. 徽章定义表(系统预设徽章) +CREATE TABLE IF NOT EXISTS badge_definition ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + badge_code VARCHAR(50) NOT NULL UNIQUE COMMENT '徽章代码', + badge_name VARCHAR(100) NOT NULL COMMENT '徽章名称', + badge_icon VARCHAR(255) COMMENT '徽章图标URL', + badge_color VARCHAR(20) DEFAULT '#31f08b' COMMENT '徽章颜色', + description VARCHAR(255) COMMENT '徽章描述', + category VARCHAR(50) DEFAULT 'general' COMMENT '分类:general/task/rating/special', + sort_order INT DEFAULT 0 COMMENT '排序', + enabled INT DEFAULT 1 COMMENT '是否启用', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_category (category), + INDEX idx_enabled (enabled) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='徽章定义'; + +-- 6. 称号定义表(系统预设称号) +CREATE TABLE IF NOT EXISTS title_definition ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + title_code VARCHAR(50) NOT NULL UNIQUE COMMENT '称号代码', + title_name VARCHAR(100) NOT NULL COMMENT '称号名称', + title_color VARCHAR(20) DEFAULT '#31f08b' COMMENT '称号颜色', + description VARCHAR(255) COMMENT '称号描述', + category VARCHAR(50) DEFAULT 'general' COMMENT '分类', + sort_order INT DEFAULT 0 COMMENT '排序', + enabled INT DEFAULT 1 COMMENT '是否启用', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_category (category), + INDEX idx_enabled (enabled) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='称号定义'; + +-- 7. 初始化一些默认徽章 +INSERT IGNORE INTO badge_definition (badge_code, badge_name, badge_icon, badge_color, description, category, sort_order) VALUES +('efficiency_star', '效率之星', NULL, '#31f08b', '每周完成20个任务获得', 'task', 1), +('five_star_master', '好评大师', NULL, '#FFD700', '累计获得50个五星好评', 'rating', 2), +('task_master', '任务达人', NULL, '#1890ff', '累计完成100个任务', 'task', 3), +('speed_demon', '极速先锋', NULL, '#ff4d4f', '连续7天准时完成所有任务', 'task', 4), +('newcomer', '新人之星', NULL, '#52c41a', '首次完成任务获得', 'special', 5), +('perfect_week', '完美周', NULL, '#722ed1', '一周内所有任务都获得五星好评', 'rating', 6); + +-- 8. 初始化一些默认称号 +INSERT IGNORE INTO title_definition (title_code, title_name, title_color, description, category, sort_order) VALUES +('design_master', '设计大师', '#FFD700', '设计领域的佼佼者', 'general', 1), +('efficiency_king', '效率之王', '#31f08b', '高效完成任务的典范', 'general', 2), +('quality_guardian', '品质卫士', '#1890ff', '始终保持高品质输出', 'general', 3), +('team_star', '团队之星', '#ff4d4f', '团队贡献突出', 'general', 4), +('rising_star', '新星', '#52c41a', '快速成长的新人', 'general', 5); + diff --git a/frontend/src/api/rewardRule.ts b/frontend/src/api/rewardRule.ts index 6139a15..fe7af8c 100644 --- a/frontend/src/api/rewardRule.ts +++ b/frontend/src/api/rewardRule.ts @@ -21,11 +21,96 @@ export interface RewardRule { cronExpression?: string lastExecuteTime?: string priority: number + triggerCount?: number + totalBudget?: number + usedBudget?: number + maxTimesPerUser?: number + startTime?: string + endTime?: string creatorId: number createTime: string updateTime: string } +// 奖励发放记录 +export interface RewardRecord { + id: number + userId: number + ruleId: number + ruleName: string + rewardType: string + rewardValue: string + triggerData: string + createTime: string + userName?: string + userAvatar?: string +} + +// 用户徽章 +export interface UserBadge { + id: number + userId: number + badgeCode: string + badgeName: string + badgeIcon: string + badgeColor: string + description: string + sourceType: string + sourceId: number + createTime: string +} + +// 徽章定义 +export interface BadgeDefinition { + id: number + badgeCode: string + badgeName: string + badgeIcon: string + badgeColor: string + description: string + category: string + sortOrder: number + enabled: number +} + +// 用户称号 +export interface UserTitle { + id: number + userId: number + titleCode: string + titleName: string + titleColor: string + description: string + isActive: number + expireTime: string + sourceType: string + sourceId: number + createTime: string +} + +// 称号定义 +export interface TitleDefinition { + id: number + titleCode: string + titleName: string + titleColor: string + description: string + category: string + sortOrder: number + enabled: number +} + +// 奖励统计 +export interface RewardStats { + totalCount: number + totalScore: number + userCount: number + hitRate: number + ruleRanking: Array<{ ruleId: number; ruleName: string; triggerCount: number }> + userRanking: Array<{ userId: number; userName: string; userAvatar: string; totalScore: number }> + dailyTrend: Array<{ date: string; count: number; totalScore: number }> +} + export interface RewardRuleCreateRequest { name: string description?: string @@ -116,6 +201,115 @@ export function initDefaultRewardRules(): Promise { .then(res => res.data) } +// ==================== 奖励统计 API ==================== + +/** + * 获取奖励统计数据 + * @param period today/week/month + */ +export function getRewardStats(period: string = 'month'): Promise { + return request.get>('/reward-rules/stats', { params: { period } }) + .then(res => res.data) +} + +// ==================== 奖励记录 API ==================== + +/** + * 获取奖励发放记录 + */ +export function getRewardRecords(params: { + page?: number + pageSize?: number + userId?: number + ruleId?: number + rewardType?: string + startTime?: string + endTime?: string +}): Promise> { + return request.get>>('/reward-rules/records', { params }) + .then(res => res.data) +} + +/** + * 获取我的奖励记录 + */ +export function getMyRewardRecords(params: { + page?: number + pageSize?: number +}): Promise> { + return request.get>>('/reward-rules/records/my', { params }) + .then(res => res.data) +} + +// ==================== 徽章 API ==================== + +/** + * 获取所有可用徽章 + */ +export function getAllBadges(): Promise { + return request.get>('/reward-rules/badges') + .then(res => res.data) +} + +/** + * 获取用户徽章 + */ +export function getUserBadges(userId: number): Promise { + return request.get>(`/reward-rules/users/${userId}/badges`) + .then(res => res.data) +} + +/** + * 获取我的徽章 + */ +export function getMyBadges(): Promise { + return request.get>('/reward-rules/badges/my') + .then(res => res.data) +} + +// ==================== 称号 API ==================== + +/** + * 获取所有可用称号 + */ +export function getAllTitles(): Promise { + return request.get>('/reward-rules/titles') + .then(res => res.data) +} + +/** + * 获取用户称号 + */ +export function getUserTitles(userId: number): Promise { + return request.get>(`/reward-rules/users/${userId}/titles`) + .then(res => res.data) +} + +/** + * 获取我的称号 + */ +export function getMyTitles(): Promise { + return request.get>('/reward-rules/titles/my') + .then(res => res.data) +} + +/** + * 设置激活称号 + */ +export function setActiveTitle(titleId?: number): Promise { + return request.put>('/reward-rules/titles/active', null, { + params: { titleId } + }).then(res => res.data) +} + +/** + * 获取我的激活称号 + */ +export function getMyActiveTitle(): Promise { + return request.get>('/reward-rules/titles/active') + .then(res => res.data) +} + // 规则类型选项 export const RULE_TYPE_OPTIONS = [ { value: 'daily', label: '每日奖励' }, diff --git a/frontend/src/views/admin/RewardRules.vue b/frontend/src/views/admin/RewardRules.vue index 574fe41..721bea1 100644 --- a/frontend/src/views/admin/RewardRules.vue +++ b/frontend/src/views/admin/RewardRules.vue @@ -7,9 +7,9 @@

- 自动奖励规则 + 奖励规则管理

-

配置系统自动奖励规则,激励团队成长

+

配置自动奖励规则,查看奖励发放统计

@@ -19,88 +19,223 @@
- -
- - - {{ opt.label }} - - -
+ + + + + +
+ + + + {{ opt.label }} + + + + 已启用 + 已禁用 + + +
- -
-
- -
-
- -

暂无奖励规则

- 创建规则 -
-
-
-
-
-

{{ rule.name }}

- - {{ getRuleTypeName(rule.ruleType) }} - -
-
- -
+ +
+
+
- -

{{ rule.description }}

- -
-
- 触发条件 - {{ formatCondition(rule.triggerCondition) }} -
-
- 奖励 - +{{ rule.rewardScore }} 积分 -
-
- 适用范围 - {{ getScopeName(rule.scope) }} -
-
- 优先级 - {{ rule.priority }} -
+
+ +

暂无奖励规则

+ 创建规则
+
+
+
+
+

{{ rule.name }}

+ + {{ getRuleTypeName(rule.ruleType) }} + +
+
+ +
+
+ +

{{ rule.description }}

+ +
+
+ 触发条件 + {{ formatCondition(rule.triggerCondition) }} +
+
+ 奖励 + +{{ rule.rewardScore }} 积分 +
+
+ 适用范围 + {{ getScopeName(rule.scope) }} +
+
+ 触发次数 + {{ rule.triggerCount || 0 }} 次 +
+
-
-
-
- -
- -
+ +
+ +
+ + + + +
+ +
+ + 今日 + 本周 + 本月 + +
+ + +
+
+
{{ stats.totalCount || 0 }}
+
奖励次数
+
+
+
{{ stats.totalScore || 0 }}
+
发放积分
+
+
+
{{ stats.userCount || 0 }}
+
获奖人数
+
+
+ + +
+ + +
+

规则触发排行

+
+
+ {{ index + 1 }} + {{ item.rule_name || item.ruleName }} + {{ item.trigger_count || item.triggerCount }} 次 +
+
暂无数据
+
+
+
+ +
+

用户获奖排行

+
+
+ {{ index + 1 }} + {{ item.user_name || item.userName }} + +{{ item.total_score || item.totalScore }} 分 +
+
暂无数据
+
+
+
+
+
+
+
+ + + +
+ + + +
+
+ + + +
+
+
+
+ {{ badge.badgeName.charAt(0) }} +
+
+

{{ badge.badgeName }}

+

{{ badge.description }}

+ {{ badge.category }} +
+
+
+ 暂无徽章定义 +
+
+
+
+ + + +
+
+
+
+ {{ title.titleName }} +
+

{{ title.description }}

+ {{ title.category }} +
+
+ 暂无称号定义 +
+
+
+
+
@@ -184,7 +319,7 @@ diff --git a/奖励规则优化方案.md b/奖励规则优化方案.md new file mode 100644 index 0000000..6b39274 --- /dev/null +++ b/奖励规则优化方案.md @@ -0,0 +1,356 @@ +# 奖励规则功能优化方案 + +## 一、现状分析 + +### 1.1 当前功能 +| 模块 | 功能 | 状态 | +|------|------|------| +| 规则管理 | 创建/编辑/删除/启用/禁用 | ✅ 已实现 | +| 规则类型 | 每日/每周/每月/任务/好评奖励 | ✅ 已实现 | +| 触发条件 | 任务数量/评分/五星数量 | ✅ 已实现 | +| 定时执行 | 每日/每周/每月定时检查 | ✅ 已实现 | +| 奖励发放 | 积分奖励+通知 | ✅ 已实现 | + +### 1.2 存在的问题 + +1. **奖励类型单一**:目前只支持积分奖励,缺少徽章、称号等多样化奖励 +2. **触发条件有限**:仅支持简单的数值比较,缺少复合条件、时间条件等 +3. **缺乏奖励记录**:无法追溯历史奖励发放情况 +4. **UI体验一般**:配置界面不够直观,缺乏可视化编辑 +5. **无预览功能**:无法预估规则影响范围和预期奖励 +6. **缺少数据统计**:管理员无法看到奖励发放的整体数据 + +--- + +## 二、优化目标 + +1. **丰富奖励类型**:支持积分、徽章、称号、经验值等多种奖励 +2. **增强触发条件**:支持更灵活的条件组合 +3. **完善奖励追溯**:记录每一次奖励发放 +4. **提升用户体验**:可视化配置、实时预览 +5. **数据分析支持**:奖励发放统计和效果分析 + +--- + +## 三、功能设计 + +### 3.1 奖励类型扩展 + +| 奖励类型 | 说明 | 示例 | +|----------|------|------| +| 积分 (score) | 通用积分奖励 | +50 积分 | +| 经验值 (exp) | 用户等级经验 | +100 经验 | +| 徽章 (badge) | 特殊成就标识 | "效率之星" 徽章 | +| 称号 (title) | 显示在用户名旁 | "设计大师" | +| 特权 (privilege) | 临时功能权限 | 7天优先接单权 | +| 实物兑换券 (coupon) | 可兑换实物 | 咖啡券 x1 | + +### 3.2 触发条件扩展 + +#### 基础条件 +| 条件类型 | 参数 | 说明 | +|----------|------|------| +| task_count | 任务数量 | 完成任务数 ≥ N | +| score_earned | 获得积分 | 累计获得积分 ≥ N | +| five_star_rate | 好评率 | 好评率 ≥ N% | +| on_time_rate | 准时率 | 准时完成率 ≥ N% | +| consecutive_days | 连续天数 | 连续 N 天完成任务 | +| first_task | 首单 | 首次完成任务 | +| login_days | 登录天数 | 本月登录天数 ≥ N | + +#### 复合条件(AND/OR) +```json +{ + "logic": "AND", + "conditions": [ + { "type": "task_count", "operator": ">=", "value": 10 }, + { "type": "five_star_rate", "operator": ">=", "value": 90 } + ] +} +``` + +#### 时间范围条件 +```json +{ + "type": "task_count", + "operator": ">=", + "value": 5, + "timeRange": "today" // today/this_week/this_month/custom +} +``` + +### 3.3 新增功能模块 + +#### 3.3.1 奖励发放记录表 `reward_record` +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | 主键 | +| user_id | bigint | 用户ID | +| rule_id | bigint | 规则ID | +| rule_name | varchar | 规则名称(冗余) | +| reward_type | varchar | 奖励类型 | +| reward_value | varchar | 奖励内容(JSON) | +| trigger_data | text | 触发时的数据快照 | +| create_time | datetime | 发放时间 | + +#### 3.3.2 奖励统计面板 +- **今日奖励**:今日发放奖励次数、总积分 +- **本周奖励**:本周奖励趋势图 +- **规则效果**:各规则触发次数排行 +- **用户受益**:获奖用户排行榜 + +#### 3.3.3 用户奖励中心 +- 我的奖励历史 +- 我的徽章墙 +- 我的称号 +- 奖励日历(按日期查看获奖记录) + +--- + +## 四、UI/UX 设计 + +### 4.1 规则配置页面重构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 自动奖励规则管理 [+ 新建规则] │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─ 筛选 ─────────────────────────────────────────────────┐ │ +│ │ 类型: [全部 ▼] 状态: [全部 ▼] 搜索: [___________] │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ 规则卡片 ──────────────────────────────────────────────┐ │ +│ │ 🏆 每日任务达人 [启用 ○] [编辑] │ │ +│ │ ───────────────────────────────────────────────────── │ │ +│ │ 📋 触发: 每天完成 ≥ 5 个任务 │ │ +│ │ 🎁 奖励: +20 积分 │ │ +│ │ 👥 范围: 全体用户 │ │ +│ │ 📊 统计: 本周触发 127 次 | 共奖励 2,540 积分 │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ 规则卡片 ──────────────────────────────────────────────┐ │ +│ │ ⭐ 五星好评奖励 [启用 ○] [编辑] │ │ +│ │ ───────────────────────────────────────────────────── │ │ +│ │ 📋 触发: 获得 5 星评价 │ │ +│ │ 🎁 奖励: +10 积分 + "好评之星" 徽章 │ │ +│ │ 👥 范围: 设计师 │ │ +│ │ 📊 统计: 本周触发 89 次 | 共奖励 890 积分 │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 4.2 可视化规则编辑器 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 新建奖励规则 [X 关闭] │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ 基本信息 │ +│ ├─ 规则名称: [每周效率之星_______________] │ +│ ├─ 规则描述: [每周完成20个任务的设计师奖励] │ +│ └─ 规则类型: ○每日 ●每周 ○每月 ○即时 ○好评 │ +│ │ +│ ───────────────────────────────────────────────────────── │ +│ │ +│ 触发条件 [+ 添加条件] │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ [完成任务数 ▼] [≥ ▼] [20____] [本周内 ▼] [✕] │ │ +│ └───────────────────────────────────────────────────────┘ │ +│ │ │ +│ ├─ 逻辑: ○ 满足全部条件(AND) ○ 满足任一条件(OR) │ +│ │ +│ ───────────────────────────────────────────────────────── │ +│ │ +│ 奖励设置 [+ 添加奖励] │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ [积分 ▼] 值: [100___] [✕] │ │ +│ ├───────────────────────────────────────────────────────┤ │ +│ │ [徽章 ▼] 选择: [效率之星 ▼] [✕] │ │ +│ └───────────────────────────────────────────────────────┘ │ +│ │ +│ ───────────────────────────────────────────────────────── │ +│ │ +│ 适用范围 │ +│ ├─ ○ 全体用户 │ +│ ├─ ● 指定角色: [☑设计师 ☐需求方 ☐部门负责人] │ +│ └─ ○ 指定部门: [选择部门...] │ +│ │ +│ ───────────────────────────────────────────────────────── │ +│ │ +│ 规则预览 │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ 📋 当设计师在本周内完成任务数 ≥ 20 时 │ │ +│ │ 🎁 自动发放: 100 积分 + "效率之星" 徽章 │ │ +│ │ 👥 预计影响: 约 23 人可能获得此奖励 │ │ +│ └───────────────────────────────────────────────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ [取消] [保存规则] │ │ +│ └──────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 4.3 奖励统计面板 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 奖励数据统计 [本月 ▼] │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ 1,234 │ │ 15,680 │ │ 89% │ │ 47 │ │ +│ │ 奖励次数 │ │ 发放积分 │ │ 规则命中率│ │ 获奖人数 │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +│ │ +│ ┌─ 奖励趋势 ───────────────────────────────────────────┐ │ +│ │ ▂▃▄▅▆▇█▇▆▅▄▃▂▁▂▃▄▅▆▇█▇▆▅▄▃▂ │ │ +│ │ 1 3 5 7 9 11 13 15 17 19 21 23 25 27 29 31 │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─ 规则触发排行 ─────────┐ ┌─ 获奖用户排行 ─────────┐ │ +│ │ 1. 五星好评奖励 423次 │ │ 1. 张三 +580 积分 │ │ +│ │ 2. 每日任务达人 312次 │ │ 2. 李四 +520 积分 │ │ +│ │ 3. 每周效率之星 89次 │ │ 3. 王五 +490 积分 │ │ +│ │ 4. 连续签到奖励 67次 │ │ 4. 赵六 +450 积分 │ │ +│ │ 5. 首单奖励 45次 │ │ 5. 钱七 +420 积分 │ │ +│ └───────────────────────┘ └───────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 五、技术实现 + +### 5.1 数据库变更 + +```sql +-- 1. 新增奖励发放记录表 +CREATE TABLE reward_record ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + user_id BIGINT NOT NULL COMMENT '用户ID', + rule_id BIGINT COMMENT '规则ID', + rule_name VARCHAR(100) COMMENT '规则名称', + reward_type VARCHAR(50) NOT NULL COMMENT '奖励类型', + reward_value TEXT COMMENT '奖励内容JSON', + trigger_data TEXT COMMENT '触发数据快照', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_user_id (user_id), + INDEX idx_rule_id (rule_id), + INDEX idx_create_time (create_time) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='奖励发放记录'; + +-- 2. 扩展奖励规则表 +ALTER TABLE reward_rule ADD COLUMN reward_exp INT DEFAULT 0 COMMENT '奖励经验值'; +ALTER TABLE reward_rule ADD COLUMN reward_title VARCHAR(50) COMMENT '奖励称号'; +ALTER TABLE reward_rule ADD COLUMN reward_privilege TEXT COMMENT '奖励特权JSON'; +ALTER TABLE reward_rule ADD COLUMN max_times_per_user INT DEFAULT 0 COMMENT '每用户最大获得次数(0=无限)'; +ALTER TABLE reward_rule ADD COLUMN total_budget INT DEFAULT 0 COMMENT '总预算积分(0=无限)'; +ALTER TABLE reward_rule ADD COLUMN used_budget INT DEFAULT 0 COMMENT '已使用预算'; +ALTER TABLE reward_rule ADD COLUMN trigger_count INT DEFAULT 0 COMMENT '触发次数'; +ALTER TABLE reward_rule ADD COLUMN start_time DATETIME COMMENT '生效开始时间'; +ALTER TABLE reward_rule ADD COLUMN end_time DATETIME COMMENT '生效结束时间'; + +-- 3. 用户徽章表 +CREATE TABLE user_badge ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + user_id BIGINT NOT NULL, + badge_code VARCHAR(50) NOT NULL COMMENT '徽章代码', + badge_name VARCHAR(100) NOT NULL COMMENT '徽章名称', + badge_icon VARCHAR(255) COMMENT '徽章图标', + source_type VARCHAR(50) COMMENT '来源类型', + source_id BIGINT COMMENT '来源ID', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uk_user_badge (user_id, badge_code), + INDEX idx_user_id (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户徽章'; + +-- 4. 用户称号表 +CREATE TABLE user_title ( + id BIGINT PRIMARY KEY AUTO_INCREMENT, + user_id BIGINT NOT NULL, + title_code VARCHAR(50) NOT NULL COMMENT '称号代码', + title_name VARCHAR(100) NOT NULL COMMENT '称号名称', + is_active INT DEFAULT 0 COMMENT '是否激活显示', + expire_time DATETIME COMMENT '过期时间', + create_time DATETIME DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uk_user_title (user_id, title_code), + INDEX idx_user_id (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户称号'; +``` + +### 5.2 后端 API 变更 + +| 接口 | 方法 | 说明 | +|------|------|------| +| /api/reward-rules | GET | 获取规则列表(增加统计数据) | +| /api/reward-rules/stats | GET | 获取奖励统计数据 | +| /api/reward-rules/preview | POST | 预览规则影响范围 | +| /api/reward-records | GET | 获取奖励发放记录 | +| /api/reward-records/my | GET | 获取我的奖励记录 | +| /api/users/{id}/badges | GET | 获取用户徽章 | +| /api/users/{id}/titles | GET | 获取用户称号 | +| /api/users/{id}/titles/active | PUT | 设置激活称号 | + +### 5.3 前端组件 + +1. **RewardRuleEditor.vue** - 可视化规则编辑器 +2. **RewardStats.vue** - 奖励统计面板 +3. **RewardRecordList.vue** - 奖励记录列表 +4. **UserBadgeWall.vue** - 用户徽章墙 +5. **UserTitleSelect.vue** - 用户称号选择器 + +--- + +## 六、实施计划 + +### 第一阶段:基础优化(预计 2 天) +- [ ] 新增奖励发放记录表 +- [ ] 实现奖励记录的存储和查询 +- [ ] 规则列表增加统计数据显示 +- [ ] 优化规则卡片UI + +### 第二阶段:功能扩展(预计 3 天) +- [ ] 扩展触发条件类型 +- [ ] 支持复合条件配置 +- [ ] 实现规则预览功能 +- [ ] 新增徽章和称号奖励 + +### 第三阶段:统计分析(预计 2 天) +- [ ] 实现奖励统计面板 +- [ ] 奖励趋势图表 +- [ ] 规则效果分析 + +### 第四阶段:用户中心(预计 2 天) +- [ ] 用户奖励历史页面 +- [ ] 徽章墙功能 +- [ ] 称号管理功能 + +--- + +## 七、预期效果 + +1. **管理效率提升**:可视化配置,减少配置错误 +2. **激励效果增强**:多样化奖励,提升用户参与度 +3. **数据驱动决策**:清晰的统计数据,优化奖励策略 +4. **用户体验优化**:清晰的奖励反馈,增强成就感 + +--- + +## 八、风险与应对 + +| 风险 | 影响 | 应对措施 | +|------|------|----------| +| 奖励发放过多 | 积分通胀 | 设置规则预算上限 | +| 条件配置复杂 | 用户困惑 | 提供常用模板 | +| 性能压力 | 系统变慢 | 异步处理+缓存 | +| 规则冲突 | 重复奖励 | 优先级+互斥设置 | + +--- + +*文档版本:v1.0* +*创建时间:2026-01-04* +