feat: 奖励系统升级 - 添加奖励记录、徽章、称号功能
This commit is contained in:
parent
43d13d1a95
commit
91e3dc3eaa
@ -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<RewardStatsVO> getStats(@RequestParam(defaultValue = "month") String period) {
|
||||
return Result.success(rewardRecordService.getStats(period));
|
||||
}
|
||||
|
||||
// ==================== 奖励记录 ====================
|
||||
|
||||
@Operation(summary = "获取奖励发放记录")
|
||||
@GetMapping("/records")
|
||||
public Result<IPage<RewardRecord>> 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<IPage<RewardRecord>> 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<List<BadgeDefinition>> getAllBadges() {
|
||||
return Result.success(rewardRecordService.getAllBadges());
|
||||
}
|
||||
|
||||
@Operation(summary = "获取用户徽章")
|
||||
@GetMapping("/users/{userId}/badges")
|
||||
public Result<List<UserBadge>> getUserBadges(@PathVariable Long userId) {
|
||||
return Result.success(rewardRecordService.getUserBadges(userId));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取我的徽章")
|
||||
@GetMapping("/badges/my")
|
||||
public Result<List<UserBadge>> 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<List<TitleDefinition>> getAllTitles() {
|
||||
return Result.success(rewardRecordService.getAllTitles());
|
||||
}
|
||||
|
||||
@Operation(summary = "获取用户称号")
|
||||
@GetMapping("/users/{userId}/titles")
|
||||
public Result<List<UserTitle>> getUserTitles(@PathVariable Long userId) {
|
||||
return Result.success(rewardRecordService.getUserTitles(userId));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取我的称号")
|
||||
@GetMapping("/titles/my")
|
||||
public Result<List<UserTitle>> 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<Void> 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<UserTitle> getMyActiveTitle(@RequestHeader("Authorization") String authHeader) {
|
||||
String token = jwtUtil.extractTokenFromHeader(authHeader);
|
||||
Long userId = jwtUtil.getUserIdFromToken(token);
|
||||
return Result.success(rewardRecordService.getActiveTitle(userId));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
65
backend/src/main/java/com/designhub/entity/RewardRecord.java
Normal file
65
backend/src/main/java/com/designhub/entity/RewardRecord.java
Normal file
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
67
backend/src/main/java/com/designhub/entity/UserBadge.java
Normal file
67
backend/src/main/java/com/designhub/entity/UserBadge.java
Normal file
@ -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;
|
||||
}
|
||||
|
||||
72
backend/src/main/java/com/designhub/entity/UserTitle.java
Normal file
72
backend/src/main/java/com/designhub/entity/UserTitle.java
Normal file
@ -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;
|
||||
}
|
||||
|
||||
@ -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<BadgeDefinition> {
|
||||
|
||||
/**
|
||||
* 获取所有启用的徽章
|
||||
*/
|
||||
@Select("SELECT * FROM badge_definition WHERE enabled = 1 ORDER BY sort_order, id")
|
||||
List<BadgeDefinition> 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<BadgeDefinition> selectByCategory(@Param("category") String category);
|
||||
}
|
||||
|
||||
@ -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<RewardRecord> {
|
||||
|
||||
/**
|
||||
* 分页查询奖励记录(带用户信息)
|
||||
*/
|
||||
@Select("<script>" +
|
||||
"SELECT r.*, u.real_name as user_name, u.avatar as user_avatar " +
|
||||
"FROM reward_record r " +
|
||||
"LEFT JOIN sys_user u ON r.user_id = u.id " +
|
||||
"<where>" +
|
||||
" <if test='userId != null'>AND r.user_id = #{userId}</if>" +
|
||||
" <if test='ruleId != null'>AND r.rule_id = #{ruleId}</if>" +
|
||||
" <if test='rewardType != null'>AND r.reward_type = #{rewardType}</if>" +
|
||||
" <if test='startTime != null'>AND r.create_time >= #{startTime}</if>" +
|
||||
" <if test='endTime != null'>AND r.create_time <= #{endTime}</if>" +
|
||||
"</where>" +
|
||||
"ORDER BY r.create_time DESC" +
|
||||
"</script>")
|
||||
IPage<RewardRecord> selectPageWithUser(Page<RewardRecord> 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<Map<String, Object>> 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<Map<String, Object>> 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<Map<String, Object>> getDailyTrend(@Param("startTime") LocalDateTime startTime,
|
||||
@Param("endTime") LocalDateTime endTime);
|
||||
}
|
||||
|
||||
@ -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<TitleDefinition> {
|
||||
|
||||
/**
|
||||
* 获取所有启用的称号
|
||||
*/
|
||||
@Select("SELECT * FROM title_definition WHERE enabled = 1 ORDER BY sort_order, id")
|
||||
List<TitleDefinition> 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<TitleDefinition> selectByCategory(@Param("category") String category);
|
||||
}
|
||||
|
||||
@ -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<UserBadge> {
|
||||
|
||||
/**
|
||||
* 获取用户的所有徽章
|
||||
*/
|
||||
@Select("SELECT * FROM user_badge WHERE user_id = #{userId} ORDER BY create_time DESC")
|
||||
List<UserBadge> 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);
|
||||
}
|
||||
|
||||
@ -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<UserTitle> {
|
||||
|
||||
/**
|
||||
* 获取用户的所有称号
|
||||
*/
|
||||
@Select("SELECT * FROM user_title WHERE user_id = #{userId} ORDER BY is_active DESC, create_time DESC")
|
||||
List<UserTitle> 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);
|
||||
}
|
||||
|
||||
@ -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<RewardRecord> getRecordList(Integer page, Integer pageSize,
|
||||
Long userId, Long ruleId, String rewardType,
|
||||
LocalDateTime startTime, LocalDateTime endTime) {
|
||||
Page<RewardRecord> pageParam = new Page<>(page, pageSize);
|
||||
return rewardRecordMapper.selectPageWithUser(pageParam, userId, ruleId, rewardType, startTime, endTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的奖励记录
|
||||
*/
|
||||
public IPage<RewardRecord> 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<UserBadge> getUserBadges(Long userId) {
|
||||
return userBadgeMapper.selectByUserId(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用徽章
|
||||
*/
|
||||
public List<BadgeDefinition> 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<UserTitle> 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<TitleDefinition> getAllTitles() {
|
||||
return titleDefinitionMapper.selectAllEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String, Object> 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(),
|
||||
|
||||
52
backend/src/main/java/com/designhub/vo/RewardStatsVO.java
Normal file
52
backend/src/main/java/com/designhub/vo/RewardStatsVO.java
Normal file
@ -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<Map<String, Object>> ruleRanking;
|
||||
|
||||
/**
|
||||
* 用户获奖排行
|
||||
* [{userId, userName, userAvatar, totalScore}, ...]
|
||||
*/
|
||||
private List<Map<String, Object>> userRanking;
|
||||
|
||||
/**
|
||||
* 每日趋势
|
||||
* [{date, count, totalScore}, ...]
|
||||
*/
|
||||
private List<Map<String, Object>> dailyTrend;
|
||||
}
|
||||
|
||||
116
database/reward_system_upgrade.sql
Normal file
116
database/reward_system_upgrade.sql
Normal file
@ -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);
|
||||
|
||||
@ -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<void> {
|
||||
.then(res => res.data)
|
||||
}
|
||||
|
||||
// ==================== 奖励统计 API ====================
|
||||
|
||||
/**
|
||||
* 获取奖励统计数据
|
||||
* @param period today/week/month
|
||||
*/
|
||||
export function getRewardStats(period: string = 'month'): Promise<RewardStats> {
|
||||
return request.get<Result<RewardStats>>('/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<PageResult<RewardRecord>> {
|
||||
return request.get<Result<PageResult<RewardRecord>>>('/reward-rules/records', { params })
|
||||
.then(res => res.data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的奖励记录
|
||||
*/
|
||||
export function getMyRewardRecords(params: {
|
||||
page?: number
|
||||
pageSize?: number
|
||||
}): Promise<PageResult<RewardRecord>> {
|
||||
return request.get<Result<PageResult<RewardRecord>>>('/reward-rules/records/my', { params })
|
||||
.then(res => res.data)
|
||||
}
|
||||
|
||||
// ==================== 徽章 API ====================
|
||||
|
||||
/**
|
||||
* 获取所有可用徽章
|
||||
*/
|
||||
export function getAllBadges(): Promise<BadgeDefinition[]> {
|
||||
return request.get<Result<BadgeDefinition[]>>('/reward-rules/badges')
|
||||
.then(res => res.data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户徽章
|
||||
*/
|
||||
export function getUserBadges(userId: number): Promise<UserBadge[]> {
|
||||
return request.get<Result<UserBadge[]>>(`/reward-rules/users/${userId}/badges`)
|
||||
.then(res => res.data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的徽章
|
||||
*/
|
||||
export function getMyBadges(): Promise<UserBadge[]> {
|
||||
return request.get<Result<UserBadge[]>>('/reward-rules/badges/my')
|
||||
.then(res => res.data)
|
||||
}
|
||||
|
||||
// ==================== 称号 API ====================
|
||||
|
||||
/**
|
||||
* 获取所有可用称号
|
||||
*/
|
||||
export function getAllTitles(): Promise<TitleDefinition[]> {
|
||||
return request.get<Result<TitleDefinition[]>>('/reward-rules/titles')
|
||||
.then(res => res.data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户称号
|
||||
*/
|
||||
export function getUserTitles(userId: number): Promise<UserTitle[]> {
|
||||
return request.get<Result<UserTitle[]>>(`/reward-rules/users/${userId}/titles`)
|
||||
.then(res => res.data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的称号
|
||||
*/
|
||||
export function getMyTitles(): Promise<UserTitle[]> {
|
||||
return request.get<Result<UserTitle[]>>('/reward-rules/titles/my')
|
||||
.then(res => res.data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置激活称号
|
||||
*/
|
||||
export function setActiveTitle(titleId?: number): Promise<void> {
|
||||
return request.put<Result<void>>('/reward-rules/titles/active', null, {
|
||||
params: { titleId }
|
||||
}).then(res => res.data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的激活称号
|
||||
*/
|
||||
export function getMyActiveTitle(): Promise<UserTitle | null> {
|
||||
return request.get<Result<UserTitle | null>>('/reward-rules/titles/active')
|
||||
.then(res => res.data)
|
||||
}
|
||||
|
||||
// 规则类型选项
|
||||
export const RULE_TYPE_OPTIONS = [
|
||||
{ value: 'daily', label: '每日奖励' },
|
||||
|
||||
@ -7,9 +7,9 @@
|
||||
<div class="header-left">
|
||||
<h1 class="page-title">
|
||||
<Icon name="settings" size="xl" color="#31f08b" style="margin-right: 12px;" />
|
||||
自动奖励规则
|
||||
奖励规则管理
|
||||
</h1>
|
||||
<p class="page-desc">配置系统自动奖励规则,激励团队成长</p>
|
||||
<p class="page-desc">配置自动奖励规则,查看奖励发放统计</p>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<a-button type="primary" @click="showCreateModal">
|
||||
@ -19,88 +19,223 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选 -->
|
||||
<div class="filter-bar">
|
||||
<a-select v-model:value="filterType" placeholder="规则类型" style="width: 150px" allowClear @change="loadData">
|
||||
<a-select-option v-for="opt in RULE_TYPE_OPTIONS" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
<!-- Tab切换 -->
|
||||
<a-tabs v-model:activeKey="activeTab" class="reward-tabs">
|
||||
<!-- 规则管理 -->
|
||||
<a-tab-pane key="rules" tab="规则管理">
|
||||
<!-- 筛选 -->
|
||||
<div class="filter-bar">
|
||||
<a-space>
|
||||
<a-select v-model:value="filterType" placeholder="规则类型" style="width: 150px" allowClear @change="loadData">
|
||||
<a-select-option v-for="opt in RULE_TYPE_OPTIONS" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model:value="filterStatus" placeholder="状态" style="width: 120px" allowClear @change="loadData">
|
||||
<a-select-option value="1">已启用</a-select-option>
|
||||
<a-select-option value="0">已禁用</a-select-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 规则列表 -->
|
||||
<div class="rules-list">
|
||||
<div v-if="loading" class="loading-container">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
<div v-else-if="rules.length === 0" class="empty-state">
|
||||
<Icon name="inbox" size="xl" color="#6B6B6B" />
|
||||
<p>暂无奖励规则</p>
|
||||
<a-button type="primary" @click="showCreateModal">创建规则</a-button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-for="rule in rules" :key="rule.id" class="rule-card">
|
||||
<div class="rule-header">
|
||||
<div class="rule-info">
|
||||
<h3 class="rule-name">{{ rule.name }}</h3>
|
||||
<a-tag :color="getRuleTypeColor(rule.ruleType)">
|
||||
{{ getRuleTypeName(rule.ruleType) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="rule-status">
|
||||
<a-switch
|
||||
:checked="rule.enabled === 1"
|
||||
@change="(checked: boolean) => toggleRule(rule, checked)"
|
||||
checked-children="启用"
|
||||
un-checked-children="禁用"
|
||||
/>
|
||||
</div>
|
||||
<!-- 规则列表 -->
|
||||
<div class="rules-list">
|
||||
<div v-if="loading" class="loading-container">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
|
||||
<p class="rule-desc">{{ rule.description }}</p>
|
||||
|
||||
<div class="rule-details">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">触发条件</span>
|
||||
<span class="detail-value">{{ formatCondition(rule.triggerCondition) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">奖励</span>
|
||||
<span class="detail-value reward">+{{ rule.rewardScore }} 积分</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">适用范围</span>
|
||||
<span class="detail-value">{{ getScopeName(rule.scope) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">优先级</span>
|
||||
<span class="detail-value">{{ rule.priority }}</span>
|
||||
</div>
|
||||
<div v-else-if="rules.length === 0" class="empty-state">
|
||||
<Icon name="inbox" size="xl" color="#6B6B6B" />
|
||||
<p>暂无奖励规则</p>
|
||||
<a-button type="primary" @click="showCreateModal">创建规则</a-button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-for="rule in rules" :key="rule.id" class="rule-card">
|
||||
<div class="rule-header">
|
||||
<div class="rule-info">
|
||||
<h3 class="rule-name">{{ rule.name }}</h3>
|
||||
<a-tag :color="getRuleTypeColor(rule.ruleType)">
|
||||
{{ getRuleTypeName(rule.ruleType) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="rule-status">
|
||||
<a-switch
|
||||
:checked="rule.enabled === 1"
|
||||
@change="(checked: boolean) => toggleRule(rule, checked)"
|
||||
checked-children="启用"
|
||||
un-checked-children="禁用"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="rule-desc">{{ rule.description }}</p>
|
||||
|
||||
<div class="rule-details">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">触发条件</span>
|
||||
<span class="detail-value">{{ formatCondition(rule.triggerCondition) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">奖励</span>
|
||||
<span class="detail-value reward">+{{ rule.rewardScore }} 积分</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">适用范围</span>
|
||||
<span class="detail-value">{{ getScopeName(rule.scope) }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">触发次数</span>
|
||||
<span class="detail-value">{{ rule.triggerCount || 0 }} 次</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rule-footer">
|
||||
<span class="rule-time">创建于 {{ formatDate(rule.createTime) }}</span>
|
||||
<div class="rule-actions">
|
||||
<a-button size="small" @click="editRule(rule)">编辑</a-button>
|
||||
<a-popconfirm title="确定删除此规则吗?" @confirm="handleDelete(rule.id)">
|
||||
<a-button size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
<div class="rule-footer">
|
||||
<span class="rule-time">创建于 {{ formatDate(rule.createTime) }}</span>
|
||||
<div class="rule-actions">
|
||||
<a-button size="small" @click="editRule(rule)">编辑</a-button>
|
||||
<a-popconfirm title="确定删除此规则吗?" @confirm="handleDelete(rule.id)">
|
||||
<a-button size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<a-pagination
|
||||
v-model:current="currentPage"
|
||||
:total="total"
|
||||
:page-size="pageSize"
|
||||
show-size-changer
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container" v-if="total > 0">
|
||||
<a-pagination
|
||||
v-model:current="currentPage"
|
||||
:total="total"
|
||||
:page-size="pageSize"
|
||||
show-size-changer
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 奖励统计 -->
|
||||
<a-tab-pane key="stats" tab="奖励统计">
|
||||
<div class="stats-section">
|
||||
<!-- 时间筛选 -->
|
||||
<div class="stats-filter">
|
||||
<a-radio-group v-model:value="statsPeriod" button-style="solid" @change="loadStats">
|
||||
<a-radio-button value="today">今日</a-radio-button>
|
||||
<a-radio-button value="week">本周</a-radio-button>
|
||||
<a-radio-button value="month">本月</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-cards">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ stats.totalCount || 0 }}</div>
|
||||
<div class="stat-label">奖励次数</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ stats.totalScore || 0 }}</div>
|
||||
<div class="stat-label">发放积分</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ stats.userCount || 0 }}</div>
|
||||
<div class="stat-label">获奖人数</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 排行榜 -->
|
||||
<div class="ranking-section">
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<div class="ranking-card">
|
||||
<h3 class="ranking-title">规则触发排行</h3>
|
||||
<div class="ranking-list">
|
||||
<div v-for="(item, index) in (stats.ruleRanking || [])" :key="index" class="ranking-item">
|
||||
<span class="rank" :class="{ 'top3': index < 3 }">{{ index + 1 }}</span>
|
||||
<span class="name">{{ item.rule_name || item.ruleName }}</span>
|
||||
<span class="count">{{ item.trigger_count || item.triggerCount }} 次</span>
|
||||
</div>
|
||||
<div v-if="!stats.ruleRanking?.length" class="empty-ranking">暂无数据</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<div class="ranking-card">
|
||||
<h3 class="ranking-title">用户获奖排行</h3>
|
||||
<div class="ranking-list">
|
||||
<div v-for="(item, index) in (stats.userRanking || [])" :key="index" class="ranking-item">
|
||||
<span class="rank" :class="{ 'top3': index < 3 }">{{ index + 1 }}</span>
|
||||
<span class="name">{{ item.user_name || item.userName }}</span>
|
||||
<span class="score">+{{ item.total_score || item.totalScore }} 分</span>
|
||||
</div>
|
||||
<div v-if="!stats.userRanking?.length" class="empty-ranking">暂无数据</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 发放记录 -->
|
||||
<a-tab-pane key="records" tab="发放记录">
|
||||
<div class="records-section">
|
||||
<a-table
|
||||
:columns="recordColumns"
|
||||
:data-source="records"
|
||||
:loading="recordsLoading"
|
||||
:pagination="{ current: recordPage, pageSize: 20, total: recordTotal, onChange: loadRecords }"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'rewardValue'">
|
||||
<span class="reward-value">{{ parseRewardValue(record.rewardValue) }}</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'createTime'">
|
||||
{{ formatDate(record.createTime) }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 徽章管理 -->
|
||||
<a-tab-pane key="badges" tab="徽章管理">
|
||||
<div class="badges-section">
|
||||
<div class="badges-grid">
|
||||
<div v-for="badge in badges" :key="badge.id" class="badge-card">
|
||||
<div class="badge-icon" :style="{ backgroundColor: badge.badgeColor }">
|
||||
{{ badge.badgeName.charAt(0) }}
|
||||
</div>
|
||||
<div class="badge-info">
|
||||
<h4>{{ badge.badgeName }}</h4>
|
||||
<p>{{ badge.description }}</p>
|
||||
<a-tag>{{ badge.category }}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!badges.length" class="empty-badges">
|
||||
暂无徽章定义
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 称号管理 -->
|
||||
<a-tab-pane key="titles" tab="称号管理">
|
||||
<div class="titles-section">
|
||||
<div class="titles-grid">
|
||||
<div v-for="title in titles" :key="title.id" class="title-card">
|
||||
<div class="title-name" :style="{ color: title.titleColor }">
|
||||
{{ title.titleName }}
|
||||
</div>
|
||||
<p class="title-desc">{{ title.description }}</p>
|
||||
<a-tag>{{ title.category }}</a-tag>
|
||||
</div>
|
||||
<div v-if="!titles.length" class="empty-titles">
|
||||
暂无称号定义
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -184,7 +319,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.vue'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
@ -196,24 +331,39 @@ import {
|
||||
deleteRewardRule,
|
||||
enableRewardRule,
|
||||
disableRewardRule,
|
||||
getRewardStats,
|
||||
getRewardRecords,
|
||||
getAllBadges,
|
||||
getAllTitles,
|
||||
RULE_TYPE_OPTIONS,
|
||||
TRIGGER_TYPE_OPTIONS,
|
||||
OPERATOR_OPTIONS,
|
||||
type RewardRule
|
||||
type RewardRule,
|
||||
type RewardStats,
|
||||
type RewardRecord,
|
||||
type BadgeDefinition,
|
||||
type TitleDefinition
|
||||
} from '@/api/rewardRule'
|
||||
|
||||
// Tab状态
|
||||
const activeTab = ref('rules')
|
||||
|
||||
// 规则列表状态
|
||||
const loading = ref(false)
|
||||
const rules = ref<RewardRule[]>([])
|
||||
const total = ref(0)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(10)
|
||||
const filterType = ref<string | undefined>(undefined)
|
||||
const filterStatus = ref<string | undefined>(undefined)
|
||||
|
||||
// 弹窗状态
|
||||
const modalVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const submitting = ref(false)
|
||||
const editingId = ref<number | null>(null)
|
||||
|
||||
// 规则表单
|
||||
const ruleForm = ref({
|
||||
name: '',
|
||||
description: '',
|
||||
@ -226,6 +376,29 @@ const ruleForm = ref({
|
||||
priority: 100
|
||||
})
|
||||
|
||||
// 统计数据
|
||||
const statsPeriod = ref('month')
|
||||
const stats = ref<Partial<RewardStats>>({})
|
||||
|
||||
// 发放记录
|
||||
const recordsLoading = ref(false)
|
||||
const records = ref<RewardRecord[]>([])
|
||||
const recordPage = ref(1)
|
||||
const recordTotal = ref(0)
|
||||
|
||||
const recordColumns = [
|
||||
{ title: '用户', dataIndex: 'userName', key: 'userName' },
|
||||
{ title: '规则', dataIndex: 'ruleName', key: 'ruleName' },
|
||||
{ title: '奖励类型', dataIndex: 'rewardType', key: 'rewardType' },
|
||||
{ title: '奖励内容', dataIndex: 'rewardValue', key: 'rewardValue' },
|
||||
{ title: '发放时间', dataIndex: 'createTime', key: 'createTime' },
|
||||
]
|
||||
|
||||
// 徽章和称号
|
||||
const badges = ref<BadgeDefinition[]>([])
|
||||
const titles = ref<TitleDefinition[]>([])
|
||||
|
||||
// 加载规则列表
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
@ -244,6 +417,56 @@ const loadData = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载统计数据
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
stats.value = await getRewardStats(statsPeriod.value)
|
||||
} catch (error) {
|
||||
console.error('加载统计失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载发放记录
|
||||
const loadRecords = async (page: number = 1) => {
|
||||
recordsLoading.value = true
|
||||
recordPage.value = page
|
||||
try {
|
||||
const result = await getRewardRecords({ page, pageSize: 20 })
|
||||
records.value = result.records || []
|
||||
recordTotal.value = result.total || 0
|
||||
} catch (error) {
|
||||
console.error('加载记录失败:', error)
|
||||
} finally {
|
||||
recordsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载徽章
|
||||
const loadBadges = async () => {
|
||||
try {
|
||||
badges.value = await getAllBadges()
|
||||
} catch (error) {
|
||||
console.error('加载徽章失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载称号
|
||||
const loadTitles = async () => {
|
||||
try {
|
||||
titles.value = await getAllTitles()
|
||||
} catch (error) {
|
||||
console.error('加载称号失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听Tab切换
|
||||
watch(activeTab, (newTab) => {
|
||||
if (newTab === 'stats') loadStats()
|
||||
if (newTab === 'records') loadRecords()
|
||||
if (newTab === 'badges') loadBadges()
|
||||
if (newTab === 'titles') loadTitles()
|
||||
})
|
||||
|
||||
const showCreateModal = () => {
|
||||
isEdit.value = false
|
||||
editingId.value = null
|
||||
@ -382,7 +605,11 @@ const getScopeName = (scope: string) => {
|
||||
|
||||
const formatCondition = (condition: any) => {
|
||||
if (typeof condition === 'string') {
|
||||
condition = JSON.parse(condition)
|
||||
try {
|
||||
condition = JSON.parse(condition)
|
||||
} catch {
|
||||
return condition
|
||||
}
|
||||
}
|
||||
const typeOpt = TRIGGER_TYPE_OPTIONS.find(o => o.value === condition.type)
|
||||
const opOpt = OPERATOR_OPTIONS.find(o => o.value === condition.operator)
|
||||
@ -391,19 +618,32 @@ const formatCondition = (condition: any) => {
|
||||
|
||||
const formatDate = (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm')
|
||||
|
||||
const parseRewardValue = (value: string) => {
|
||||
try {
|
||||
const obj = JSON.parse(value)
|
||||
if (obj.score) return `+${obj.score} 积分`
|
||||
return value
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/variables.scss";
|
||||
|
||||
.reward-rules-page {
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
background: $bg-primary;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@ -417,14 +657,14 @@ onMounted(() => {
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #ECECEC;
|
||||
color: $text-primary;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
color: #AFAFAF;
|
||||
color: $text-secondary;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@ -439,10 +679,15 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.rule-card {
|
||||
background: #2A2A2A;
|
||||
border: 1px solid #3A3A3A;
|
||||
background: $bg-card;
|
||||
border: 1px solid $border-primary;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: $brand-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.rule-header {
|
||||
@ -461,11 +706,11 @@ onMounted(() => {
|
||||
.rule-name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #ECECEC;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.rule-desc {
|
||||
color: #AFAFAF;
|
||||
color: $text-secondary;
|
||||
font-size: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
@ -476,7 +721,7 @@ onMounted(() => {
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #333;
|
||||
background: $bg-secondary;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@ -488,15 +733,15 @@ onMounted(() => {
|
||||
|
||||
.detail-label {
|
||||
font-size: 12px;
|
||||
color: #6B6B6B;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 14px;
|
||||
color: #ECECEC;
|
||||
color: $text-primary;
|
||||
|
||||
&.reward {
|
||||
color: #31f08b;
|
||||
color: $brand-primary;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
@ -506,12 +751,12 @@ onMounted(() => {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #3A3A3A;
|
||||
border-top: 1px solid $border-primary;
|
||||
}
|
||||
|
||||
.rule-time {
|
||||
font-size: 12px;
|
||||
color: #6B6B6B;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.rule-actions {
|
||||
@ -528,7 +773,7 @@ onMounted(() => {
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 48px;
|
||||
color: #6B6B6B;
|
||||
color: $text-tertiary;
|
||||
|
||||
p {
|
||||
margin: 16px 0;
|
||||
@ -540,4 +785,206 @@ onMounted(() => {
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
}
|
||||
|
||||
// 统计样式
|
||||
.stats-section {
|
||||
.stats-filter {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stats-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 24px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: $bg-card;
|
||||
border: 1px solid $border-primary;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
|
||||
.stat-value {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: $text-secondary;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ranking-section {
|
||||
.ranking-card {
|
||||
background: $bg-card;
|
||||
border: 1px solid $border-primary;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.ranking-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: $text-primary;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ranking-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ranking-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 12px;
|
||||
background: $bg-secondary;
|
||||
border-radius: 8px;
|
||||
|
||||
.rank {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: $bg-tertiary;
|
||||
color: $text-secondary;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
|
||||
&.top3 {
|
||||
background: $brand-primary;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
flex: 1;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.count, .score {
|
||||
color: $brand-primary;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-ranking {
|
||||
text-align: center;
|
||||
color: $text-tertiary;
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
// 徽章样式
|
||||
.badges-section {
|
||||
.badges-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.badge-card {
|
||||
background: $bg-card;
|
||||
border: 1px solid $border-primary;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
|
||||
.badge-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
flex: 1;
|
||||
|
||||
h4 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: $text-primary;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
color: $text-secondary;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-badges {
|
||||
text-align: center;
|
||||
color: $text-tertiary;
|
||||
padding: 48px;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
// 称号样式
|
||||
.titles-section {
|
||||
.titles-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.title-card {
|
||||
background: $bg-card;
|
||||
border: 1px solid $border-primary;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
|
||||
.title-name {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.title-desc {
|
||||
font-size: 12px;
|
||||
color: $text-secondary;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-titles {
|
||||
text-align: center;
|
||||
color: $text-tertiary;
|
||||
padding: 48px;
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Tabs样式
|
||||
.reward-tabs {
|
||||
:deep(.ant-tabs-nav) {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.reward-value {
|
||||
color: $brand-primary;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
356
奖励规则优化方案.md
Normal file
356
奖励规则优化方案.md
Normal file
@ -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*
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user